feat: improve opening trainer flow and add deviation dialog

- Add tutor message guardrail to prevent rapid-fire messages
  - Track last message by move index
  - Wait for both player and opponent moves before speaking
  - Speak immediately when player deviates from theory

- Add deviation dialog with three options:
  - Continue Playing (Start Game) - transitions to game mode
  - Undo & Return to Opening - returns to theory
  - Explore This Variation - continues off-book practice

- Implement smooth game mode transition:
  - Pass opening context from trainer to game
  - Tutor welcomes player with context about their study
  - Computer makes first move if needed in starting position

- Fix generate-test-fixtures script:
  - Add safety check to prevent overwriting real data
  - Restore full opening database (12,379 openings)
  - Rebuild move index (12,377 sequences)
  - Add clear warnings about test fixtures

- Update ChessGame to accept openingContext prop
- Update Tutor to display contextual greeting in game mode

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Stefan
2025-12-08 15:05:05 +01:00
parent 1e78d8114e
commit 8ed402bbe0
34 changed files with 111269 additions and 480 deletions
+144
View File
@@ -0,0 +1,144 @@
import { test, expect } from '@playwright/test';
test.describe('Game Analysis Feature', () => {
test.beforeEach(async ({ page }) => {
// Set a mock API key in localStorage BEFORE any page loads
await page.addInitScript(() => {
localStorage.setItem('gemini_api_key', 'mock-api-key-for-testing');
});
// Mock the LLM API
await page.route('**/api/v1/llm/chat', async (route) => {
const request = route.request();
const postData = request.postDataJSON();
let reply = "Let me analyze this position for you.";
if (postData.message?.includes('ANALYSIS MODE')) {
reply = "I'll help you understand this game. Let's go through it move by move.";
} else if (postData.message?.includes('Move')) {
reply = "This is an interesting move. Let me explain the key ideas.";
}
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ reply }),
});
});
});
test('should navigate to analysis mode', async ({ page }) => {
// Navigate directly to analysis page
await page.goto('/analysis');
// Should be on analysis page
await expect(page).toHaveURL('/analysis');
// Should see the chessboard (react-chessboard uses SVG/images, not canvas)
// Look for the textarea which is always visible in analysis mode
await expect(page.locator('textarea').first()).toBeVisible({ timeout: 10000 });
});
test('should analyze a PGN game', async ({ page }) => {
await page.goto('/analysis');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(3000); // Give Stockfish time to initialize
// Should see the chessboard (react-chessboard uses SVG/images, not canvas)
await expect(page.locator('textarea').first()).toBeVisible({ timeout: 20000 });
// Should see PGN input
const pgnInput = page.locator('textarea').first();
await expect(pgnInput).toBeVisible();
// Enter a simple PGN (just moves)
const scholarsPgn = `1. e4 e5 2. Bc4 Nc6 3. Qh5 Nf6`;
await pgnInput.fill(scholarsPgn);
// Wait for the game to load automatically
await page.waitForTimeout(2000);
// Should see navigation controls
const nextButton = page.locator('button').filter({ has: page.locator('svg') }).filter({ hasText: '' });
// Just verify the textarea is still visible after loading
await expect(page.locator('textarea').first()).toBeVisible();
});
test('should analyze a FEN position', async ({ page }) => {
await page.goto('/analysis');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(3000); // Give Stockfish time to initialize
const fenInput = page.locator('textarea').first();
// Enter a FEN position
await fenInput.fill('rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1');
// Wait for position to load
await page.waitForTimeout(2000);
// Should see the chessboard (react-chessboard uses SVG/images, not canvas)
await expect(page.locator('textarea').first()).toBeVisible({ timeout: 20000 });
});
test('should navigate through moves in analysis', async ({ page }) => {
await page.goto('/analysis');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(3000); // Give Stockfish time to initialize
// Enter a PGN with multiple moves
const pgnInput = page.locator('textarea').first();
await pgnInput.fill('1. e4 e5 2. Nf3 Nc6 3. Bb5');
// Wait for game to load
await page.waitForTimeout(2000);
// Should see the chessboard (react-chessboard uses SVG/images, not canvas)
await expect(page.locator('textarea').first()).toBeVisible({ timeout: 20000 });
// Look for navigation buttons (they use icons, not text)
const buttons = page.locator('button').filter({ has: page.locator('svg') });
// Just verify we have navigation buttons
await expect(buttons.first()).toBeVisible();
});
test('should display analysis interface', async ({ page }) => {
await page.goto('/analysis');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(3000); // Give Stockfish time to initialize
// Should see the chessboard (react-chessboard uses SVG/images, not canvas)
await expect(page.locator('textarea').first()).toBeVisible({ timeout: 20000 });
// Should see input area
await expect(page.locator('textarea').first()).toBeVisible();
// Should see evaluation bar (or check for evaluation-related elements)
const evaluationElement = page.locator('text=Evaluation').or(page.locator('[class*="evaluation"]'));
if (await evaluationElement.count() > 0) {
await expect(evaluationElement.first()).toBeVisible();
}
});
test('should allow switching board orientation in analysis', async ({ page }) => {
await page.goto('/analysis');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(3000); // Give Stockfish time to initialize
// Should see the chessboard (react-chessboard uses SVG/images, not canvas)
await expect(page.locator('textarea').first()).toBeVisible({ timeout: 20000 });
// Look for orientation buttons (White/Black)
const whiteButton = page.locator('button').filter({ hasText: /white/i });
const blackButton = page.locator('button').filter({ hasText: /black/i });
// At least one orientation button should be visible
const orientationButtons = page.locator('button').filter({ hasText: /white|black/i });
if (await orientationButtons.count() > 0) {
await expect(orientationButtons.first()).toBeVisible();
}
});
});