Commit d34a923d authored by Mahmoud Aglan's avatar Mahmoud Aglan

fix: analysis system — progressive per-move analysis with progress bar

Problems fixed:
- Analysis no longer freezes (was sending all positions in one request)
- Now analyzes position-by-position with live progress updates
- Progress bar shows "تحليل النقلة 5 من 30" with fill animation
- Fallback: loads moves from moves[] array when PGN is empty
- Added 'cache' API action to save analysis results with service key
- Fixed curl handle leak (unset → proper handling)

Flow: click analyze → progress bar fills per move → results render → cached
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 4eddc5a6
...@@ -182,6 +182,38 @@ switch ($action) { ...@@ -182,6 +182,38 @@ switch ($action) {
} }
break; break;
case 'cache':
$gameId = $input['game_id'] ?? '';
$evaluations = $input['evaluations'] ?? [];
if (!$gameId || empty($evaluations)) {
http_response_code(400);
echo json_encode(['error' => 'missing data']);
exit;
}
$analysisData = [
'evaluations' => $evaluations,
'analyzed_at' => date('c')
];
$gameRes = supabase_rest('GET', "matches?id=eq.{$gameId}&select=game_state", [], SUPABASE_SERVICE_KEY);
$existingState = [];
if (!empty($gameRes['data'][0]['game_state'])) {
$existingState = $gameRes['data'][0]['game_state'];
if (is_string($existingState)) {
$existingState = json_decode($existingState, true) ?? [];
}
}
$existingState['analysis'] = $analysisData;
supabase_rest('PATCH', "matches?id=eq.{$gameId}", [
'game_state' => json_encode($existingState)
], SUPABASE_SERVICE_KEY);
echo json_encode(['ok' => true]);
break;
default: default:
http_response_code(400); http_response_code(400);
echo json_encode(['error' => 'invalid action']); echo json_encode(['error' => 'invalid action']);
......
...@@ -144,9 +144,14 @@ const Analysis = { ...@@ -144,9 +144,14 @@ const Analysis = {
this.gameData = data.game; this.gameData = data.game;
this.playerColor = data.game.player_color || 'w'; this.playerColor = data.game.player_color || 'w';
// Parse PGN or replay moves // Parse PGN or replay moves from moves array
if (data.game.pgn) { var movesLoaded = false;
this.chess.load_pgn(data.game.pgn);
if (data.game.pgn && data.game.pgn.trim().length > 0) {
try {
this.chess.load_pgn(data.game.pgn);
movesLoaded = true;
} catch(e) {}
} }
// Build positions array // Build positions array
...@@ -154,16 +159,30 @@ const Analysis = { ...@@ -154,16 +159,30 @@ const Analysis = {
this.positions = [tempChess.fen()]; this.positions = [tempChess.fen()];
this.moves = []; this.moves = [];
if (data.game.pgn) { if (movesLoaded) {
tempChess.load_pgn(data.game.pgn);
const history = tempChess.history({ verbose: true });
const replayChess = new Chess(); const replayChess = new Chess();
const history = this.chess.history({ verbose: true });
for (const move of history) { for (const move of history) {
replayChess.move(move); replayChess.move(move);
this.positions.push(replayChess.fen()); this.positions.push(replayChess.fen());
this.moves.push(move); this.moves.push(move);
} }
} else if (data.game.moves && data.game.moves.length > 0) {
// Fallback: replay from moves array (SAN strings)
var moveArr = data.game.moves;
if (typeof moveArr === 'string') {
try { moveArr = JSON.parse(moveArr); } catch(e) { moveArr = []; }
}
const replayChess = new Chess();
for (var i = 0; i < moveArr.length; i++) {
var san = typeof moveArr[i] === 'string' ? moveArr[i] : (moveArr[i].san || moveArr[i]);
var result = replayChess.move(san);
if (result) {
this.positions.push(replayChess.fen());
this.moves.push(result);
}
}
this.chess.load(replayChess.fen());
} }
// Load cached analysis if available // Load cached analysis if available
...@@ -645,42 +664,76 @@ const Analysis = { ...@@ -645,42 +664,76 @@ const Analysis = {
const textEl = document.getElementById('analysis-progress-text'); const textEl = document.getElementById('analysis-progress-text');
const btnEl = document.getElementById('btn-analyze'); const btnEl = document.getElementById('btn-analyze');
progressEl.style.display = 'block'; if (progressEl) progressEl.style.display = 'block';
btnEl.disabled = true; if (btnEl) { btnEl.disabled = true; btnEl.textContent = 'جاري التحليل...'; }
btnEl.textContent = 'جاري التحليل...';
// Analyze position by position with progress
var evaluations = [];
var total = this.positions.length;
var failed = 0;
for (var i = 0; i < total; i++) {
var pct = Math.round(((i + 1) / total) * 100);
if (fillEl) fillEl.style.width = pct + '%';
if (textEl) textEl.textContent = 'تحليل النقلة ' + (i + 1) + ' من ' + total;
try {
var res = await App.fetch('/api/analysis', {
method: 'POST',
body: JSON.stringify({
action: 'analyze_single',
fen: this.positions[i]
})
});
if (res && res.evaluation !== undefined) {
evaluations.push({
move_index: i,
fen: this.positions[i],
best_move: res.best_move || null,
evaluation: res.evaluation || 0,
depth: res.depth || 20
});
} else {
evaluations.push({ move_index: i, fen: this.positions[i], best_move: null, evaluation: 0, depth: 0 });
failed++;
}
} catch(e) {
evaluations.push({ move_index: i, fen: this.positions[i], best_move: null, evaluation: 0, depth: 0 });
failed++;
}
}
try { // Save analysis to server cache
const res = await App.fetch('/api/analysis', { if (failed < total / 2) {
this.evaluations = evaluations;
this.classifyMoves();
this.renderMoveList();
this.renderEvalGraph();
this.renderTimeGraph();
this.renderCriticalMoments();
this.calculateAccuracy();
this.updateEvalBar(this.currentMoveIndex);
// Cache to server
App.fetch('/api/analysis', {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
action: 'analyze', action: 'cache',
game_id: this.gameId, game_id: this.gameId,
positions: this.positions evaluations: evaluations
}) })
}); }).catch(function(){});
if (res && res.ok && res.analysis) { App.toast('تم التحليل بنجاح', 'success');
this.evaluations = res.analysis.evaluations || []; } else {
this.classifyMoves(); App.toast('خطأ في التحليل — المحرك غير متاح', 'error');
this.renderMoveList();
this.renderEvalGraph();
this.renderTimeGraph();
this.renderCriticalMoments();
this.calculateAccuracy();
this.updateEvalBar(this.currentMoveIndex);
App.toast('تم التحليل بنجاح', 'success');
} else {
App.toast('خطأ في التحليل', 'error');
}
} catch (e) {
App.toast('خطأ في الاتصال', 'error');
} }
fillEl.style.width = '100%'; if (fillEl) fillEl.style.width = '100%';
textEl.textContent = 'اكتمل التحليل'; if (textEl) textEl.textContent = 'اكتمل التحليل';
this.isAnalyzing = false; this.isAnalyzing = false;
btnEl.disabled = false; if (btnEl) { btnEl.disabled = false; btnEl.innerHTML = '<svg class="icon"><use href="/public/icons/sprite.svg#icon-star"></use></svg> اعادة التحليل'; }
btnEl.innerHTML = '<svg class="icon"><use href="/public/icons/sprite.svg#icon-star"></use></svg> اعادة التحليل';
}, },
// Review mistakes mode // Review mistakes mode
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment