import { Component, DestroyRef, inject, OnInit } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { ActivatedRoute, RouterLink } from '@angular/router'; import { FormsModule } from '@angular/forms'; import { switchMap, of } from 'rxjs'; import { ChessBoardComponent } from '../../components/chess-board/chess-board.component'; import { EvalTimelineComponent } from '../../components/eval-timeline/eval-timeline.component'; import { AnnotatedMoveListComponent } from '../../components/annotated-move-list/annotated-move-list.component'; import { GameApiService } from '../../services/game-api.service'; import { AnalysisService } from '../../services/analysis.service'; import { AnnotatedMove, AnalysisResponse } from '../../models/analysis.models'; import { GameFull } from '../../models/game.models'; import { getErrorMessage } from '../../core/http/error-message.util'; const ANALYSIS_DEPTH_DEFAULT = 14; const STARTING_FEN = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'; type InputMode = 'fen' | 'pgn' | 'game'; @Component({ selector: 'app-analysis', standalone: true, imports: [ RouterLink, FormsModule, ChessBoardComponent, EvalTimelineComponent, AnnotatedMoveListComponent, ], templateUrl: './analysis.component.html', styleUrl: './analysis.component.css', }) export class AnalysisComponent implements OnInit { private readonly route = inject(ActivatedRoute); private readonly gameApi = inject(GameApiService); private readonly analysisService = inject(AnalysisService); private readonly destroyRef = inject(DestroyRef); // ── State ───────────────────────────────────────────────── inputMode: InputMode = 'fen'; fenInput = ''; pgnInput = ''; depth = ANALYSIS_DEPTH_DEFAULT; loading = false; analysing = false; errorMessage = ''; currentFen = STARTING_FEN; game: GameFull | null = null; /** FEN for each ply (index 0 = position before move 0 was played) */ fenHistory: string[] = [STARTING_FEN]; annotatedMoves: AnnotatedMove[] = []; activePly: number | null = null; /** Single-position analysis result (for custom FEN/PGN input) */ positionAnalysis: AnalysisResponse | null = null; get displayFen(): string { if (this.activePly !== null && this.fenHistory[this.activePly + 1]) { return this.fenHistory[this.activePly + 1]; } return this.currentFen; } get hasAnnotations(): boolean { return this.annotatedMoves.length > 0; } // ── Lifecycle ───────────────────────────────────────────── ngOnInit(): void { // Support /analysis?gameId=xxx deep-link this.route.queryParamMap.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((params) => { const gameId = params.get('gameId'); if (gameId) { this.inputMode = 'game'; this.loadGame(gameId); } }); } // ── Input mode ───────────────────────────────────────────── setInputMode(mode: InputMode): void { this.inputMode = mode; this.errorMessage = ''; } // ── Load FEN ────────────────────────────────────────────── loadFen(): void { const fen = this.fenInput.trim(); if (!fen) return; this.reset(); this.currentFen = fen; this.fenHistory = [fen]; this.analyseSinglePosition(fen); } // ── Load PGN ────────────────────────────────────────────── loadPgn(): void { const pgn = this.pgnInput.trim(); if (!pgn) return; this.reset(); this.loading = true; this.gameApi .importPgn(pgn) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ next: (game) => { this.loading = false; this.applyGame(game); }, error: (err) => { this.loading = false; this.errorMessage = getErrorMessage(err, 'Could not import PGN.'); }, }); } // ── Load game by ID ─────────────────────────────────────── loadGame(gameId: string): void { this.reset(); this.loading = true; this.gameApi .getGame(gameId) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ next: (game) => { this.loading = false; this.applyGame(game); }, error: (err) => { this.loading = false; this.errorMessage = getErrorMessage(err, 'Could not load game.'); }, }); } // ── Run full analysis ───────────────────────────────────── runAnalysis(): void { if (this.fenHistory.length < 2) { this.analyseSinglePosition(this.currentFen); return; } this.analysing = true; this.errorMessage = ''; const sans = this.annotatedMoves.length > 0 ? this.annotatedMoves.map((m) => m.san) : (this.game?.state.moves ?? []); this.analysisService .analyzeGame(sans, this.fenHistory, this.depth) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ next: (annotated) => { this.annotatedMoves = annotated; this.analysing = false; }, error: (err) => { this.errorMessage = getErrorMessage(err, 'Analysis failed.'); this.analysing = false; }, }); } // ── Board navigation ────────────────────────────────────── navigateToPly(ply: number): void { this.activePly = ply; } navigateHistory(direction: 'first' | 'prev' | 'next' | 'last'): void { const total = this.fenHistory.length - 1; const current = this.activePly ?? total; let next: number; switch (direction) { case 'first': next = 0; break; case 'prev': next = Math.max(0, current - 1); break; case 'next': next = Math.min(total, current + 1); break; default: next = total; break; } this.activePly = next >= total ? null : next; } // ── Private helpers ─────────────────────────────────────── private reset(): void { this.errorMessage = ''; this.annotatedMoves = []; this.positionAnalysis = null; this.activePly = null; this.game = null; this.fenHistory = [STARTING_FEN]; this.currentFen = STARTING_FEN; } private applyGame(game: GameFull): void { this.game = game; this.currentFen = game.state.fen; // Build a flat FEN history from scratch using moves array // The server gives us the final FEN. We reconstruct history by // storing the final FEN; full per-ply history requires per-move API calls // which is out of scope here — we store what we have and allow analysis to proceed. this.fenHistory = [game.state.fen]; // Seed annotated moves with san strings, no quality yet this.annotatedMoves = game.state.moves.map((san) => ({ san, fen: game.state.fen, evalBefore: null, evalAfter: null, quality: null, bestMove: null, winChanceBefore: null, winChanceAfter: null, })); } private analyseSinglePosition(fen: string): void { this.analysing = true; this.analysisService .analyzePosition(fen, this.depth) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ next: (result) => { this.positionAnalysis = result; this.analysing = false; }, error: (err) => { this.errorMessage = getErrorMessage(err, 'Analysis failed.'); this.analysing = false; }, }); } }