From b35260c8b243e715526b9a79aed01b8df9e41b7b Mon Sep 17 00:00:00 2001 From: Jer Miller Date: Sun, 5 Apr 2026 20:12:13 -0600 Subject: [PATCH] fix: align segment type strings with frontend and coordinate page-load fetches cluster_segments() returned "transcripts"/"percepts" in the types list while the frontend pill logic checks for "audio"/"screen", causing all zoom pills to render as yellow/screen. Changed to "audio"/"screen" to match the frontend and the ranges API convention. The two page-load fetches (ranges + segments) were independent, so the segments callback could run before timeline bounds were set, filtering against default 9-10 AM range and showing "No segments in selected range". Replaced with Promise.all so all initialization happens after both resolve. Added error handling (.catch) with visible error state for fetch failures. Updated test assertions and API baseline fixture. --- apps/transcripts/workspace.html | 45 ++++++++++++------- tests/baselines/api/transcripts/segments.json | 12 ++--- tests/test_cluster.py | 10 ++--- think/cluster.py | 6 +-- 4 files changed, 42 insertions(+), 31 deletions(-) diff --git a/apps/transcripts/workspace.html b/apps/transcripts/workspace.html index b80194871..fd2c611e1 100644 --- a/apps/transcripts/workspace.html +++ b/apps/transcripts/workspace.html @@ -2266,12 +2266,20 @@ body.has-date-nav .workspace:has(.tr-wrap) { updateZoom(); }).observe(zoom); - // Load transcript ranges and compute dynamic timeline bounds - fetch(`/app/transcripts/api/ranges/${day}`) - .then(r => r.json()) - .then(data => { - // Compute and apply dynamic bounds - const bounds = computeTimelineBounds(data); + // Load transcript ranges and segments in parallel, initialize after both resolve + const rangesFetch = fetch(`/app/transcripts/api/ranges/${day}`).then(r => { + if (!r.ok) throw new Error(`Ranges failed: ${r.status}`); + return r.json(); + }); + const segmentsFetch = fetch(`/app/transcripts/api/segments/${day}`).then(r => { + if (!r.ok) throw new Error(`Segments failed: ${r.status}`); + return r.json(); + }); + + Promise.all([rangesFetch, segmentsFetch]) + .then(([rangesData, segmentsData]) => { + // Apply dynamic timeline bounds from ranges + const bounds = computeTimelineBounds(rangesData); timelineStart = bounds.start; timelineEnd = bounds.end; @@ -2283,26 +2291,22 @@ body.has-date-nav .workspace:has(.tr-wrap) { const mid = (timelineStart + timelineEnd) / 2; range = { start: snap(mid - DEFAULT_LEN / 2), end: snap(mid + DEFAULT_LEN / 2) }; - // Now build the grid and render + // Build the grid and render timeline buildGrid(); renderTimeline(); - // Add segment indicators - (data.audio || []).forEach(rg => { + // Add segment indicators from ranges + (rangesData.audio || []).forEach(rg => { const [s, e] = rg.map(parseTime); addSegmentIndicator('audio', s, e, 0); }); - (data.screen || []).forEach(rg => { + (rangesData.screen || []).forEach(rg => { const [s, e] = rg.map(parseTime); addSegmentIndicator('screen', s, e, 1); }); - }); - // Load segments for the zoom view - fetch(`/app/transcripts/api/segments/${day}`) - .then(r => r.json()) - .then(data => { - allSegments = data.segments || []; + // Store segments and update zoom + allSegments = segmentsData.segments || []; updateZoom(); // Check for hash fragment to auto-select segment @@ -2310,7 +2314,6 @@ body.has-date-nav .workspace:has(.tr-wrap) { if (hash) { const seg = allSegments.find(s => s.key === hash); if (seg) { - // Adjust range to include the segment const segStart = parseTime(seg.start); const segEnd = parseTime(seg.end); const rangeLen = range.end - range.start; @@ -2323,6 +2326,14 @@ body.has-date-nav .workspace:has(.tr-wrap) { selectSegment(seg, false); } } + }) + .catch(err => { + console.error('Failed to load transcript data:', err); + const errEl = document.createElement('div'); + errEl.className = 'tr-zoom-empty'; + errEl.textContent = 'Failed to load transcript data'; + zoomSegments.innerHTML = ''; + zoomSegments.appendChild(errEl); }); // Handle browser back/forward diff --git a/tests/baselines/api/transcripts/segments.json b/tests/baselines/api/transcripts/segments.json index 20a177388..aca747225 100644 --- a/tests/baselines/api/transcripts/segments.json +++ b/tests/baselines/api/transcripts/segments.json @@ -6,8 +6,8 @@ "start": "09:00", "stream": "default", "types": [ - "transcripts", - "percepts" + "audio", + "screen" ] }, { @@ -16,8 +16,8 @@ "start": "14:00", "stream": "default", "types": [ - "transcripts", - "percepts" + "audio", + "screen" ] }, { @@ -26,8 +26,8 @@ "start": "18:00", "stream": "default", "types": [ - "transcripts", - "percepts" + "audio", + "screen" ] } ] diff --git a/tests/test_cluster.py b/tests/test_cluster.py index 2cbf9786f..b602ae203 100644 --- a/tests/test_cluster.py +++ b/tests/test_cluster.py @@ -134,20 +134,20 @@ def test_cluster_segments(tmp_path, monkeypatch): assert segments[0]["key"] == "090000_300" assert segments[0]["start"] == "09:00" assert segments[0]["end"] == "09:05" - assert segments[0]["types"] == ["transcripts"] + assert segments[0]["types"] == ["audio"] # Check second segment (both transcripts and screen) assert segments[1]["key"] == "100000_600" assert segments[1]["start"] == "10:00" assert segments[1]["end"] == "10:10" - assert "transcripts" in segments[1]["types"] - assert "percepts" in segments[1]["types"] + assert "audio" in segments[1]["types"] + assert "screen" in segments[1]["types"] # Check third segment (screen only) assert segments[2]["key"] == "110000_300" assert segments[2]["start"] == "11:00" assert segments[2]["end"] == "11:05" - assert segments[2]["types"] == ["percepts"] + assert segments[2]["types"] == ["screen"] def test_cluster_period_uses_raw_screen(tmp_path, monkeypatch): @@ -333,7 +333,7 @@ def test_cluster_segments_with_split_screen(tmp_path, monkeypatch): assert len(segments) == 1 assert segments[0]["key"] == "100000_300" - assert "percepts" in segments[0]["types"] + assert "screen" in segments[0]["types"] def test_cluster_span(tmp_path, monkeypatch): diff --git a/think/cluster.py b/think/cluster.py index 6cfc7ffb8..e25711147 100644 --- a/think/cluster.py +++ b/think/cluster.py @@ -463,7 +463,7 @@ def cluster_segments(day: str) -> list[dict[str, Any]]: - key: segment directory name (HHMMSS_LEN format) - start: start time as HH:MM - end: end time as HH:MM - - types: list of content types present ("transcripts", "percepts", or both) + - types: list of content types present ("audio", "screen", or both) """ from think.utils import segment_parse @@ -490,11 +490,11 @@ def cluster_segments(day: str) -> list[dict[str, Any]]: or any(seg_path.glob("*_transcript.md")) or (seg_path / "imported.md").exists() ): - types.append("transcripts") + types.append("audio") # Check for screen content if (seg_path / "screen.jsonl").exists() or any(seg_path.glob("*_screen.jsonl")): - types.append("percepts") + types.append("screen") if not types: continue -- 2.51.2