From eaa8193cac9607bcd96b17f0bc1fa3cb3ad217c2 Mon Sep 17 00:00:00 2001 From: jedarden Date: Tue, 21 Apr 2026 17:25:20 -0400 Subject: [PATCH] feat(replay): restore annotation imports for debug telemetry panel Working tree had removed the annotation component imports while the initAnnotations function and ANNOTATION_OVERLAY_STYLES template reference still depended on them, causing TS build failures. --- .needle-predispatch-sha | 2 +- web/src/pages/replay.ts | 97 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 1 deletion(-) diff --git a/.needle-predispatch-sha b/.needle-predispatch-sha index b90c99f..6ac18c3 100644 --- a/.needle-predispatch-sha +++ b/.needle-predispatch-sha @@ -1 +1 @@ -fd9ffbc0487ebcf54f04ac13d3eae2f0f96157cb +30f2c63b20a8363b6d9b21829b8c7375dd127d8b diff --git a/web/src/pages/replay.ts b/web/src/pages/replay.ts index 30b789b..4dac701 100644 --- a/web/src/pages/replay.ts +++ b/web/src/pages/replay.ts @@ -322,7 +322,15 @@ function initReplayViewerWithClass(ReplayViewerClass: any, initialUrl?: string): } .debug-panel-header { position: relative; padding-top: 20px; } } + /* Annotation panel */ + .annotation-panel { padding: 0; overflow: hidden; } + .annotation-panel > h2 { padding: 15px 15px 0; } + #annotation-overlay-container { padding: 0 15px; } + #annotation-form-container { padding: 0 15px 15px; border-top: 1px solid var(--bg-tertiary); margin-top: 8px; padding-top: 10px; } + .annotation-canvas-hint { position: absolute; bottom: 8px; left: 8px; background: rgba(0,0,0,0.7); color: #94a3b8; font-size: 0.65rem; padding: 3px 8px; border-radius: 4px; pointer-events: none; opacity: 0; transition: opacity 0.3s; } + .annotation-canvas-hint.visible { opacity: 1; } + `; initReplayViewer(ReplayViewerClass, initialUrl); @@ -517,6 +525,7 @@ function initReplayViewer(ReplayViewerClass: any, initialUrl?: string): void { initWinProb(replay); loadCommentary(replay.match_id); initDebugPanel(replay); + initAnnotations(replay); const hasAnyDebug = replay.turns.some(t => t.debug && Object.keys(t.debug).length > 0); if (hasAnyDebug) { @@ -576,6 +585,93 @@ function initReplayViewer(ReplayViewerClass: any, initialUrl?: string): void { viewer.setShowDebug(true); } + // ── Annotation overlay integration (§16.8) ────────────────────────────────────── + + let annotationOverlay: AnnotationOverlay | null = null; + let allAnnotations: Annotation[] = []; + let clickedGridPosition: Position | undefined; + let canvasAnnotationHint: HTMLDivElement | null = null; + + function initAnnotations(replay: Replay): void { + const overlayContainer = document.getElementById('annotation-overlay-container'); + const formContainer = document.getElementById('annotation-form-container'); + if (!overlayContainer || !formContainer) return; + + annotationOverlay = new AnnotationOverlay(overlayContainer, { + onTurnClick: (turn: number) => { + viewer.setTurn(turn); + updateUI(); + updateEventLog(); + updateAnnotationOverlay(); + }, + }); + + // Load both server and local annotations + const local = loadLocalAnnotations(replay.match_id); + fetchFeedback(replay.match_id).then(remote => { + allAnnotations = [...remote, ...local]; + annotationOverlay?.loadAnnotations(replay.match_id, allAnnotations, replay.turns.length); + }).catch(() => { + allAnnotations = local; + annotationOverlay?.loadAnnotations(replay.match_id, allAnnotations, replay.turns.length); + }); + + // Create the annotation form + createAnnotationForm(formContainer, () => viewer.getTurn(), () => replay.match_id, () => clickedGridPosition); + + // Listen for new annotations from the form + formContainer.addEventListener('annotation-added', ((e: CustomEvent) => { + const ann = e.detail as Annotation; + allAnnotations.push(ann); + annotationOverlay?.addAnnotation(ann); + clickedGridPosition = undefined; + if (canvasAnnotationHint) canvasAnnotationHint.classList.remove('visible'); + }) as EventListener); + + // Add canvas hint overlay + const canvasWrapper = canvas.parentElement; + if (canvasWrapper && !canvasWrapper.querySelector('.annotation-canvas-hint')) { + canvasAnnotationHint = document.createElement('div'); + canvasAnnotationHint.className = 'annotation-canvas-hint'; + canvasAnnotationHint.textContent = 'Click canvas to pin annotation position'; + canvasWrapper.style.position = 'relative'; + canvasWrapper.appendChild(canvasAnnotationHint); + } + + updateAnnotationOverlay(); + } + + function updateAnnotationOverlay(): void { + if (annotationOverlay) { + annotationOverlay.setCurrentTurn(viewer.getTurn()); + } + } + + // Handle canvas clicks for spatial annotation position + canvas.addEventListener('click', (e: MouseEvent) => { + if (!viewer.getReplay()) return; + const replay = viewer.getReplay(); + if (!replay) return; + + const rect = canvas.getBoundingClientRect(); + const x = (e.clientX - rect.left) * (canvas.width / rect.width); + const y = (e.clientY - rect.top) * (canvas.height / rect.height); + const cellSize = viewer.getCellSize(); + const mapRows = replay.map.rows; + + // Only accept clicks within the map area (not score overlay) + const col = Math.floor(x / cellSize); + const row = Math.floor(y / cellSize); + + if (row >= 0 && row < mapRows && col >= 0 && col < replay.map.cols) { + clickedGridPosition = { row, col }; + if (canvasAnnotationHint) { + canvasAnnotationHint.textContent = `Pinned: (${row}, ${col})`; + canvasAnnotationHint.classList.add('visible'); + } + } + }); + function updateDebugDisplay(debug: Record | null): void { if (!debug || Object.keys(debug).length === 0) { debugInfoDisplay.innerHTML = '
No debug data for this turn
'; @@ -821,6 +917,7 @@ function initReplayViewer(ReplayViewerClass: any, initialUrl?: string): void { updateMobileUI(); updateMobileTimeline(); viewer.refreshWinProbSparkline(); + updateAnnotationOverlay(); }; viewer.onDebugChange = (debug: Record | null) => { updateDebugDisplay(debug);