Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e374d5e791 | |||
| 5b5fd6f027 | |||
| ea8048e064 | |||
| ce1fb0d60b |
@@ -76,3 +76,13 @@
|
||||
### Bug Fixes
|
||||
|
||||
* NCS-122 send WS token via first-message auth instead of query param ([#13](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/issues/13)) ([1d2c217](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/1d2c217da8982d361e2eb7de26f6447171a1dd43))
|
||||
## [0.0.0](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/compare/0.4.2...0.0.0) (2026-06-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **analysis:** fix API field mismatch and enable full game analysis ([ce1fb0d](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/ce1fb0d60b695093495ee0ad824c511dd2db7fbb))
|
||||
## [0.0.0](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/compare/0.4.3...0.0.0) (2026-06-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **tournaments:** load both user bots and official bots in join dialog ([5b5fd6f](https://git.janis-eccarius.de/NowChess/NowChess-Frontend/commit/5b5fd6f027b4aedb951a802725fcd929d514c359))
|
||||
|
||||
@@ -3,11 +3,21 @@ export interface AnalysisRequest {
|
||||
depth: number;
|
||||
}
|
||||
|
||||
export interface RawAnalysisResponse {
|
||||
fen: string;
|
||||
evaluation: number;
|
||||
depth: number;
|
||||
bestMove: string;
|
||||
mate: number | null;
|
||||
continuationMoves: string[];
|
||||
}
|
||||
|
||||
export interface AnalysisResponse {
|
||||
eval: number;
|
||||
winChance: number;
|
||||
depth: number;
|
||||
bestMove: string;
|
||||
mate: number | null;
|
||||
continuations: string[];
|
||||
}
|
||||
|
||||
|
||||
@@ -246,7 +246,7 @@
|
||||
<!-- Side column -->
|
||||
<aside class="side">
|
||||
<!-- Single position analysis result -->
|
||||
@if (positionAnalysis && !hasAnnotations) {
|
||||
@if (positionAnalysis) {
|
||||
<details class="side-card" open>
|
||||
<summary class="side-card-summary">
|
||||
<span class="side-card-title">Position Analysis</span>
|
||||
|
||||
@@ -2,7 +2,7 @@ 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 { switchMap, of, forkJoin } from 'rxjs';
|
||||
|
||||
import { ChessBoardComponent } from '../../components/chess-board/chess-board.component';
|
||||
import { EvalTimelineComponent } from '../../components/eval-timeline/eval-timeline.component';
|
||||
@@ -146,6 +146,7 @@ export class AnalysisComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
this.analysing = true;
|
||||
this.positionAnalysis = null;
|
||||
this.errorMessage = '';
|
||||
const sans =
|
||||
this.annotatedMoves.length > 0
|
||||
@@ -207,12 +208,7 @@ export class AnalysisComponent implements OnInit {
|
||||
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,
|
||||
@@ -223,6 +219,16 @@ export class AnalysisComponent implements OnInit {
|
||||
winChanceBefore: null,
|
||||
winChanceAfter: null,
|
||||
}));
|
||||
this.gameApi
|
||||
.getFenHistory(game.gameId)
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe({
|
||||
next: (fens) => {
|
||||
if (fens.length >= 2) {
|
||||
this.fenHistory = fens;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private analyseSinglePosition(fen: string): void {
|
||||
|
||||
@@ -202,53 +202,57 @@
|
||||
<span class="dialog-brand">Join with a bot</span>
|
||||
<button type="button" class="dialog-close" (click)="closeJoinDialog()">×</button>
|
||||
</div>
|
||||
<p class="join-hint">Select an official (engine-backed) bot to enter this tournament. These are the bots that actually play their moves.</p>
|
||||
|
||||
@if (botsLoading) {
|
||||
<div class="dialog-loading"><span class="pulse"></span>Loading bots…</div>
|
||||
} @else if (userBots.length === 0) {
|
||||
<p class="join-empty">No official bots are available. The official-bots engine service must be running to register them.</p>
|
||||
} @else {
|
||||
<div class="bot-pick-list">
|
||||
@for (bot of userBots; track bot.id) {
|
||||
<button type="button" class="bot-pick-row"
|
||||
[disabled]="!!joiningBotId"
|
||||
(click)="joinWithBot(bot)">
|
||||
<span class="bot-pick-avatar">{{ bot.name.charAt(0).toUpperCase() }}</span>
|
||||
<span class="bot-pick-name">{{ bot.name }}</span>
|
||||
<span class="bot-pick-rating">{{ bot.rating }}</span>
|
||||
@if (joiningBotId === bot.id) {
|
||||
<span class="bot-pick-spinner">…</span>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (joinError) {
|
||||
<div class="dialog-error">{{ joinError }}</div>
|
||||
}
|
||||
|
||||
<div class="join-divider">
|
||||
<span class="join-divider-label">or join with an official bot</span>
|
||||
</div>
|
||||
|
||||
<div class="official-bot-grid">
|
||||
@for (d of officialDifficulties; track d) {
|
||||
<button type="button" class="official-bot-btn"
|
||||
[class]="'official-btn-' + d"
|
||||
[disabled]="!!joiningOfficialDifficulty || !!joiningBotId"
|
||||
(click)="joinWithOfficialBot(d)">
|
||||
@if (joiningOfficialDifficulty === d) {
|
||||
<span class="pulse"></span>
|
||||
@if (userBots.length === 0) {
|
||||
<p class="join-empty">You have no bots. <a routerLink="/bots">Create one</a> to join with your own bot.</p>
|
||||
} @else {
|
||||
<div class="bot-pick-list">
|
||||
@for (bot of userBots; track bot.id) {
|
||||
<button type="button" class="bot-pick-row"
|
||||
[disabled]="!!joiningBotId || !!joiningOfficialDifficulty"
|
||||
(click)="joinWithBot(bot)">
|
||||
<span class="bot-pick-avatar">{{ bot.name.charAt(0).toUpperCase() }}</span>
|
||||
<span class="bot-pick-name">{{ bot.name }}</span>
|
||||
<span class="bot-pick-rating">{{ bot.rating }}</span>
|
||||
@if (joiningBotId === bot.id) {
|
||||
<span class="bot-pick-spinner">…</span>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
{{ d | titlecase }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (officialJoinError) {
|
||||
<div class="dialog-error">{{ officialJoinError }}</div>
|
||||
@if (joinError) {
|
||||
<div class="dialog-error">{{ joinError }}</div>
|
||||
}
|
||||
|
||||
<div class="join-divider">
|
||||
<span class="join-divider-label">or join with an official bot</span>
|
||||
</div>
|
||||
|
||||
@if (officialBots.length === 0) {
|
||||
<p class="join-empty">No official bots are available. The official-bots engine service must be running to register them.</p>
|
||||
} @else {
|
||||
<div class="official-bot-grid">
|
||||
@for (d of officialDifficulties; track d) {
|
||||
<button type="button" class="official-bot-btn"
|
||||
[class]="'official-btn-' + d"
|
||||
[disabled]="!!joiningOfficialDifficulty || !!joiningBotId"
|
||||
(click)="joinWithOfficialBot(d)">
|
||||
@if (joiningOfficialDifficulty === d) {
|
||||
<span class="pulse"></span>
|
||||
}
|
||||
{{ d | titlecase }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (officialJoinError) {
|
||||
<div class="dialog-error">{{ officialJoinError }}</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { CommonModule, TitleCasePipe } from '@angular/common';
|
||||
import { Router, RouterLink } from '@angular/router';
|
||||
import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { forkJoin } from 'rxjs';
|
||||
import { TournamentService } from '../../services/tournament.service';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
import { BotService } from '../../services/bot.service';
|
||||
@@ -58,6 +59,7 @@ export class TournamentsComponent implements OnInit {
|
||||
|
||||
joinDialogTournamentId: string | null = null;
|
||||
userBots: Bot[] = [];
|
||||
officialBots: Bot[] = [];
|
||||
botsLoading = false;
|
||||
joiningBotId: string | null = null;
|
||||
joinError: string | null = null;
|
||||
@@ -175,16 +177,22 @@ export class TournamentsComponent implements OnInit {
|
||||
this.joinDialogTournamentId = tournamentId;
|
||||
this.joinError = null;
|
||||
this.botsLoading = true;
|
||||
this.botService.listOfficial()
|
||||
forkJoin({ user: this.botService.list(), official: this.botService.listOfficial() })
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe({
|
||||
next: bots => { this.userBots = bots; this.botsLoading = false; },
|
||||
next: ({ user, official }) => {
|
||||
this.userBots = user;
|
||||
this.officialBots = official;
|
||||
this.botsLoading = false;
|
||||
},
|
||||
error: () => { this.botsLoading = false; }
|
||||
});
|
||||
}
|
||||
|
||||
closeJoinDialog(): void {
|
||||
this.joinDialogTournamentId = null;
|
||||
this.userBots = [];
|
||||
this.officialBots = [];
|
||||
this.joiningBotId = null;
|
||||
this.joinError = null;
|
||||
this.joiningOfficialDifficulty = null;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { environment } from '../../environments/environment';
|
||||
import {
|
||||
GameFull,
|
||||
@@ -9,7 +10,7 @@ import {
|
||||
LegalMovesResponse,
|
||||
PlayerInfo,
|
||||
} from '../models/game.models';
|
||||
import { AnalysisRequest, AnalysisResponse } from '../models/analysis.models';
|
||||
import { AnalysisRequest, AnalysisResponse, RawAnalysisResponse } from '../models/analysis.models';
|
||||
import { StreamHandlerService } from './stream-handler.service';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
@@ -78,8 +79,28 @@ export class GameApiService {
|
||||
return this.http.post<void>(`${this.apiBase}${this.apiPath}/${gameId}/draw/offer`, {});
|
||||
}
|
||||
|
||||
getFenHistory(gameId: string): Observable<string[]> {
|
||||
return this.http
|
||||
.get<{ fens: string[] }>(`${this.apiBase}${this.apiPath}/${gameId}/fen-history`)
|
||||
.pipe(map((r) => r.fens));
|
||||
}
|
||||
|
||||
analyzePosition(request: AnalysisRequest): Observable<AnalysisResponse> {
|
||||
return this.http.post<AnalysisResponse>(`${this.apiBase}/api/analysis/position`, request);
|
||||
return this.http
|
||||
.post<RawAnalysisResponse>(`${this.apiBase}/api/analysis/position`, request)
|
||||
.pipe(map((raw) => this.mapAnalysisResponse(raw)));
|
||||
}
|
||||
|
||||
private mapAnalysisResponse(raw: RawAnalysisResponse): AnalysisResponse {
|
||||
const evalPawns = raw.evaluation / 100;
|
||||
return {
|
||||
eval: evalPawns,
|
||||
winChance: 1 / (1 + Math.exp(-0.374 * evalPawns)),
|
||||
depth: raw.depth,
|
||||
bestMove: raw.bestMove,
|
||||
mate: raw.mate,
|
||||
continuations: raw.continuationMoves ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
private resolveWsBase(): string {
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
MAJOR=0
|
||||
MINOR=4
|
||||
PATCH=2
|
||||
PATCH=4
|
||||
|
||||
Reference in New Issue
Block a user