8e2afb93f3
- NCWF-5: scaffold /analysis route with ChessBoard viewer and navigation - NCWF-6: FEN / PGN / Game-ID input form with depth selector - NCWF-7: extend GameApiService with analyzePosition(); add AnalysisService with game-wide annotation pipeline; proxy /api/analysis -> :8087 - NCWF-8: EvalTimelineComponent — SVG win-chance chart per ply - NCWF-9: AnnotatedMoveListComponent — quality labels (!! ! ?! ? ??) derived from win-chance delta Also fix pre-existing app.spec.ts failure (missing provideHttpClient). Apply project-wide prettier formatting pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
356 lines
11 KiB
HTML
356 lines
11 KiB
HTML
<div class="analysis-shell">
|
|
<div class="page">
|
|
<!-- Breadcrumb -->
|
|
<nav class="crumb" aria-label="Breadcrumb">
|
|
<a routerLink="/" class="crumb-link">
|
|
<svg
|
|
width="11"
|
|
height="11"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2.4"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
>
|
|
<polyline points="15 18 9 12 15 6" />
|
|
</svg>
|
|
Back to lobby
|
|
</a>
|
|
<span class="crumb-sep">/</span>
|
|
<span class="crumb-current">Analysis</span>
|
|
</nav>
|
|
|
|
<!-- Page header -->
|
|
<header class="page-header">
|
|
<div class="page-title">
|
|
<h1>Chess Analysis</h1>
|
|
<p class="page-subtitle">Analyse positions, games or custom PGN with the engine</p>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Error -->
|
|
@if (errorMessage) {
|
|
<div class="state-error">{{ errorMessage }}</div>
|
|
}
|
|
|
|
<!-- Input section -->
|
|
<section class="input-section">
|
|
<!-- Mode tabs -->
|
|
<div class="mode-tabs" role="tablist" aria-label="Analysis input mode">
|
|
<button
|
|
class="mode-tab"
|
|
[class.active]="inputMode === 'fen'"
|
|
role="tab"
|
|
(click)="setInputMode('fen')"
|
|
>
|
|
FEN
|
|
</button>
|
|
<button
|
|
class="mode-tab"
|
|
[class.active]="inputMode === 'pgn'"
|
|
role="tab"
|
|
(click)="setInputMode('pgn')"
|
|
>
|
|
PGN
|
|
</button>
|
|
<button
|
|
class="mode-tab"
|
|
[class.active]="inputMode === 'game'"
|
|
role="tab"
|
|
(click)="setInputMode('game')"
|
|
>
|
|
Game ID
|
|
</button>
|
|
</div>
|
|
|
|
<!-- FEN input -->
|
|
@if (inputMode === 'fen') {
|
|
<div class="input-row">
|
|
<input
|
|
id="fen-input"
|
|
type="text"
|
|
class="text-input"
|
|
placeholder="Paste FEN string here…"
|
|
autocomplete="off"
|
|
[(ngModel)]="fenInput"
|
|
(keydown.enter)="loadFen()"
|
|
/>
|
|
<button
|
|
class="btn btn-primary"
|
|
type="button"
|
|
(click)="loadFen()"
|
|
[disabled]="!fenInput.trim()"
|
|
>
|
|
Load
|
|
</button>
|
|
</div>
|
|
}
|
|
|
|
<!-- PGN input -->
|
|
@if (inputMode === 'pgn') {
|
|
<div class="pgn-col">
|
|
<textarea
|
|
id="pgn-input"
|
|
class="text-area"
|
|
rows="5"
|
|
placeholder="Paste PGN here…"
|
|
[(ngModel)]="pgnInput"
|
|
></textarea>
|
|
<button
|
|
class="btn btn-primary"
|
|
type="button"
|
|
(click)="loadPgn()"
|
|
[disabled]="!pgnInput.trim() || loading"
|
|
>
|
|
@if (loading) {
|
|
Loading…
|
|
} @else {
|
|
Import PGN
|
|
}
|
|
</button>
|
|
</div>
|
|
}
|
|
|
|
<!-- Game ID input -->
|
|
@if (inputMode === 'game') {
|
|
<div class="input-row">
|
|
<input
|
|
id="gameid-input"
|
|
type="text"
|
|
class="text-input"
|
|
placeholder="Game ID"
|
|
autocomplete="off"
|
|
[(ngModel)]="fenInput"
|
|
(keydown.enter)="loadGame(fenInput)"
|
|
/>
|
|
<button
|
|
class="btn btn-primary"
|
|
type="button"
|
|
(click)="loadGame(fenInput)"
|
|
[disabled]="!fenInput.trim() || loading"
|
|
>
|
|
@if (loading) {
|
|
Loading…
|
|
} @else {
|
|
Load game
|
|
}
|
|
</button>
|
|
</div>
|
|
}
|
|
|
|
<!-- Depth + analyse -->
|
|
<div class="depth-row">
|
|
<label class="depth-label" for="depth-input">Depth</label>
|
|
<input
|
|
id="depth-input"
|
|
type="number"
|
|
class="depth-input"
|
|
min="1"
|
|
max="18"
|
|
[(ngModel)]="depth"
|
|
/>
|
|
<button
|
|
class="btn btn-analyse"
|
|
type="button"
|
|
(click)="runAnalysis()"
|
|
[disabled]="analysing"
|
|
>
|
|
@if (analysing) {
|
|
Analysing…
|
|
} @else {
|
|
Analyse
|
|
}
|
|
</button>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Main layout: board + sidebar -->
|
|
<div class="layout">
|
|
<!-- Board column -->
|
|
<div class="board-col">
|
|
<div class="board-wrap">
|
|
<app-chess-board
|
|
[fen]="displayFen"
|
|
[selectedSquare]="null"
|
|
[highlightedSquares]="[]"
|
|
boardTheme="arabian"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Navigation bar -->
|
|
@if (fenHistory.length > 1) {
|
|
<div class="nav-bar">
|
|
<button class="icon-btn" title="First position" (click)="navigateHistory('first')">
|
|
<svg
|
|
width="12"
|
|
height="12"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
>
|
|
<polyline points="11 17 6 12 11 7" />
|
|
<polyline points="18 17 13 12 18 7" />
|
|
</svg>
|
|
</button>
|
|
<button class="icon-btn" title="Previous" (click)="navigateHistory('prev')">
|
|
<svg
|
|
width="12"
|
|
height="12"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
>
|
|
<polyline points="15 18 9 12 15 6" />
|
|
</svg>
|
|
</button>
|
|
<button class="icon-btn" title="Next" (click)="navigateHistory('next')">
|
|
<svg
|
|
width="12"
|
|
height="12"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
>
|
|
<polyline points="9 18 15 12 9 6" />
|
|
</svg>
|
|
</button>
|
|
<button class="icon-btn" title="Last position" (click)="navigateHistory('last')">
|
|
<svg
|
|
width="12"
|
|
height="12"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
>
|
|
<polyline points="13 17 18 12 13 7" />
|
|
<polyline points="6 17 11 12 6 7" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<!-- Side column -->
|
|
<aside class="side">
|
|
<!-- Single position analysis result -->
|
|
@if (positionAnalysis && !hasAnnotations) {
|
|
<details class="side-card" open>
|
|
<summary class="side-card-summary">
|
|
<span class="side-card-title">Position Analysis</span>
|
|
<svg
|
|
class="chev"
|
|
width="12"
|
|
height="12"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
>
|
|
<polyline points="6 9 12 15 18 9" />
|
|
</svg>
|
|
</summary>
|
|
<div class="side-card-body eval-grid">
|
|
<div class="eval-row">
|
|
<span class="eval-label">Evaluation</span>
|
|
<span
|
|
class="eval-value"
|
|
[class.positive]="positionAnalysis.eval > 0"
|
|
[class.negative]="positionAnalysis.eval < 0"
|
|
>
|
|
{{ positionAnalysis.eval > 0 ? '+' : '' }}{{ positionAnalysis.eval.toFixed(2) }}
|
|
</span>
|
|
</div>
|
|
<div class="eval-row">
|
|
<span class="eval-label">Win chance</span>
|
|
<span class="eval-value">{{ (positionAnalysis.winChance * 100).toFixed(1) }}%</span>
|
|
</div>
|
|
<div class="eval-row">
|
|
<span class="eval-label">Best move</span>
|
|
<span class="eval-value mono">{{ positionAnalysis.bestMove }}</span>
|
|
</div>
|
|
<div class="eval-row">
|
|
<span class="eval-label">Depth</span>
|
|
<span class="eval-value mono">{{ positionAnalysis.depth }}</span>
|
|
</div>
|
|
@if (positionAnalysis.continuations.length > 0) {
|
|
<div class="eval-row eval-row--col">
|
|
<span class="eval-label">Continuation</span>
|
|
<span class="eval-value mono continuation">{{
|
|
positionAnalysis.continuations.join(' ')
|
|
}}</span>
|
|
</div>
|
|
}
|
|
</div>
|
|
</details>
|
|
}
|
|
|
|
<!-- Evaluation timeline -->
|
|
@if (hasAnnotations) {
|
|
<details class="side-card" open>
|
|
<summary class="side-card-summary">
|
|
<span class="side-card-title">Evaluation</span>
|
|
<svg
|
|
class="chev"
|
|
width="12"
|
|
height="12"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
>
|
|
<polyline points="6 9 12 15 18 9" />
|
|
</svg>
|
|
</summary>
|
|
<div class="side-card-body timeline-body">
|
|
<app-eval-timeline [moves]="annotatedMoves" [activePly]="activePly" />
|
|
</div>
|
|
</details>
|
|
|
|
<!-- Annotated moves -->
|
|
<details class="side-card" open>
|
|
<summary class="side-card-summary">
|
|
<span class="side-card-title">Moves</span>
|
|
<span class="side-card-meta">{{ annotatedMoves.length }} plies</span>
|
|
<svg
|
|
class="chev"
|
|
width="12"
|
|
height="12"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
>
|
|
<polyline points="6 9 12 15 18 9" />
|
|
</svg>
|
|
</summary>
|
|
<app-annotated-move-list
|
|
[moves]="annotatedMoves"
|
|
[activePly]="activePly"
|
|
(plySelected)="navigateToPly($event)"
|
|
/>
|
|
</details>
|
|
}
|
|
</aside>
|
|
</div>
|
|
</div>
|
|
</div>
|