personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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.

+42 -31
+28 -17
apps/transcripts/workspace.html
··· 2266 2266 updateZoom(); 2267 2267 }).observe(zoom); 2268 2268 2269 - // Load transcript ranges and compute dynamic timeline bounds 2270 - fetch(`/app/transcripts/api/ranges/${day}`) 2271 - .then(r => r.json()) 2272 - .then(data => { 2273 - // Compute and apply dynamic bounds 2274 - const bounds = computeTimelineBounds(data); 2269 + // Load transcript ranges and segments in parallel, initialize after both resolve 2270 + const rangesFetch = fetch(`/app/transcripts/api/ranges/${day}`).then(r => { 2271 + if (!r.ok) throw new Error(`Ranges failed: ${r.status}`); 2272 + return r.json(); 2273 + }); 2274 + const segmentsFetch = fetch(`/app/transcripts/api/segments/${day}`).then(r => { 2275 + if (!r.ok) throw new Error(`Segments failed: ${r.status}`); 2276 + return r.json(); 2277 + }); 2278 + 2279 + Promise.all([rangesFetch, segmentsFetch]) 2280 + .then(([rangesData, segmentsData]) => { 2281 + // Apply dynamic timeline bounds from ranges 2282 + const bounds = computeTimelineBounds(rangesData); 2275 2283 timelineStart = bounds.start; 2276 2284 timelineEnd = bounds.end; 2277 2285 ··· 2283 2291 const mid = (timelineStart + timelineEnd) / 2; 2284 2292 range = { start: snap(mid - DEFAULT_LEN / 2), end: snap(mid + DEFAULT_LEN / 2) }; 2285 2293 2286 - // Now build the grid and render 2294 + // Build the grid and render timeline 2287 2295 buildGrid(); 2288 2296 renderTimeline(); 2289 2297 2290 - // Add segment indicators 2291 - (data.audio || []).forEach(rg => { 2298 + // Add segment indicators from ranges 2299 + (rangesData.audio || []).forEach(rg => { 2292 2300 const [s, e] = rg.map(parseTime); 2293 2301 addSegmentIndicator('audio', s, e, 0); 2294 2302 }); 2295 - (data.screen || []).forEach(rg => { 2303 + (rangesData.screen || []).forEach(rg => { 2296 2304 const [s, e] = rg.map(parseTime); 2297 2305 addSegmentIndicator('screen', s, e, 1); 2298 2306 }); 2299 - }); 2300 2307 2301 - // Load segments for the zoom view 2302 - fetch(`/app/transcripts/api/segments/${day}`) 2303 - .then(r => r.json()) 2304 - .then(data => { 2305 - allSegments = data.segments || []; 2308 + // Store segments and update zoom 2309 + allSegments = segmentsData.segments || []; 2306 2310 updateZoom(); 2307 2311 2308 2312 // Check for hash fragment to auto-select segment ··· 2310 2314 if (hash) { 2311 2315 const seg = allSegments.find(s => s.key === hash); 2312 2316 if (seg) { 2313 - // Adjust range to include the segment 2314 2317 const segStart = parseTime(seg.start); 2315 2318 const segEnd = parseTime(seg.end); 2316 2319 const rangeLen = range.end - range.start; ··· 2323 2326 selectSegment(seg, false); 2324 2327 } 2325 2328 } 2329 + }) 2330 + .catch(err => { 2331 + console.error('Failed to load transcript data:', err); 2332 + const errEl = document.createElement('div'); 2333 + errEl.className = 'tr-zoom-empty'; 2334 + errEl.textContent = 'Failed to load transcript data'; 2335 + zoomSegments.innerHTML = ''; 2336 + zoomSegments.appendChild(errEl); 2326 2337 }); 2327 2338 2328 2339 // Handle browser back/forward
+6 -6
tests/baselines/api/transcripts/segments.json
··· 6 6 "start": "09:00", 7 7 "stream": "default", 8 8 "types": [ 9 - "transcripts", 10 - "percepts" 9 + "audio", 10 + "screen" 11 11 ] 12 12 }, 13 13 { ··· 16 16 "start": "14:00", 17 17 "stream": "default", 18 18 "types": [ 19 - "transcripts", 20 - "percepts" 19 + "audio", 20 + "screen" 21 21 ] 22 22 }, 23 23 { ··· 26 26 "start": "18:00", 27 27 "stream": "default", 28 28 "types": [ 29 - "transcripts", 30 - "percepts" 29 + "audio", 30 + "screen" 31 31 ] 32 32 } 33 33 ]
+5 -5
tests/test_cluster.py
··· 134 134 assert segments[0]["key"] == "090000_300" 135 135 assert segments[0]["start"] == "09:00" 136 136 assert segments[0]["end"] == "09:05" 137 - assert segments[0]["types"] == ["transcripts"] 137 + assert segments[0]["types"] == ["audio"] 138 138 139 139 # Check second segment (both transcripts and screen) 140 140 assert segments[1]["key"] == "100000_600" 141 141 assert segments[1]["start"] == "10:00" 142 142 assert segments[1]["end"] == "10:10" 143 - assert "transcripts" in segments[1]["types"] 144 - assert "percepts" in segments[1]["types"] 143 + assert "audio" in segments[1]["types"] 144 + assert "screen" in segments[1]["types"] 145 145 146 146 # Check third segment (screen only) 147 147 assert segments[2]["key"] == "110000_300" 148 148 assert segments[2]["start"] == "11:00" 149 149 assert segments[2]["end"] == "11:05" 150 - assert segments[2]["types"] == ["percepts"] 150 + assert segments[2]["types"] == ["screen"] 151 151 152 152 153 153 def test_cluster_period_uses_raw_screen(tmp_path, monkeypatch): ··· 333 333 334 334 assert len(segments) == 1 335 335 assert segments[0]["key"] == "100000_300" 336 - assert "percepts" in segments[0]["types"] 336 + assert "screen" in segments[0]["types"] 337 337 338 338 339 339 def test_cluster_span(tmp_path, monkeypatch):
+3 -3
think/cluster.py
··· 463 463 - key: segment directory name (HHMMSS_LEN format) 464 464 - start: start time as HH:MM 465 465 - end: end time as HH:MM 466 - - types: list of content types present ("transcripts", "percepts", or both) 466 + - types: list of content types present ("audio", "screen", or both) 467 467 """ 468 468 from think.utils import segment_parse 469 469 ··· 490 490 or any(seg_path.glob("*_transcript.md")) 491 491 or (seg_path / "imported.md").exists() 492 492 ): 493 - types.append("transcripts") 493 + types.append("audio") 494 494 495 495 # Check for screen content 496 496 if (seg_path / "screen.jsonl").exists() or any(seg_path.glob("*_screen.jsonl")): 497 - types.append("percepts") 497 + types.append("screen") 498 498 499 499 if not types: 500 500 continue