From 42cc35dcd6b3733945c8f7c4533dbcf83c0dd13a Mon Sep 17 00:00:00 2001 From: sudacode Date: Thu, 9 Apr 2026 21:28:20 -0700 Subject: [PATCH] feat(stats): drop seek markers from session timeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seek-forward / seek-backward events cluttered sessions with lots of rewinds — a single episode could show dozens of << and >> icons overlapping the card and pause markers. Stop requesting them from the backend, drop them from buildSessionChartEvents, remove the seek variant from SessionChartMarker, and strip the matching ReferenceLines, overlay marker, popover branch, and legend entry from SessionDetail. --- changes/stats-dashboard-feedback-pass.md | 1 + .../src/components/sessions/SessionDetail.tsx | 56 +------------------ .../sessions/SessionEventOverlay.tsx | 6 -- .../sessions/SessionEventPopover.test.tsx | 29 ---------- .../sessions/SessionEventPopover.tsx | 20 ------- stats/src/lib/session-detail.test.tsx | 5 +- stats/src/lib/session-events.test.ts | 13 ++--- stats/src/lib/session-events.ts | 30 ---------- 8 files changed, 10 insertions(+), 150 deletions(-) diff --git a/changes/stats-dashboard-feedback-pass.md b/changes/stats-dashboard-feedback-pass.md index acbad33e..83bbbbcf 100644 --- a/changes/stats-dashboard-feedback-pass.md +++ b/changes/stats-dashboard-feedback-pass.md @@ -8,3 +8,4 @@ area: stats - Episode detail hides card events whose Anki notes have been deleted, instead of showing phantom mining activity. - Trend and watch-time charts share a unified theme with horizontal gridlines and larger ticks for legibility. - Overview, Library, Trends, Sessions, and Vocabulary now use generic "title" wording so YouTube videos and anime live comfortably side by side in the dashboard. +- Session timeline no longer plots seek-forward/seek-backward markers — they were too noisy on sessions with lots of rewinds. diff --git a/stats/src/components/sessions/SessionDetail.tsx b/stats/src/components/sessions/SessionDetail.tsx index 2eb0263d..2c8d776e 100644 --- a/stats/src/components/sessions/SessionDetail.tsx +++ b/stats/src/components/sessions/SessionDetail.tsx @@ -125,14 +125,13 @@ export function SessionDetail({ session }: SessionDetailProps) { const knownWordsMap = buildKnownWordsLookup(knownWordsTimeline); const hasKnownWords = knownWordsMap.size > 0; - const { cardEvents, seekEvents, yomitanLookupEvents, pauseRegions, markers } = + const { cardEvents, yomitanLookupEvents, pauseRegions, markers } = buildSessionChartEvents(events); const lookupRate = buildLookupRateDisplay( session.yomitanLookupCount, getSessionDisplayWordCount(session), ); const pauseCount = events.filter((e) => e.eventType === EventType.PAUSE_START).length; - const seekCount = seekEvents.length; const cardEventCount = cardEvents.length; const activeMarkerKey = resolveActiveSessionMarkerKey(hoveredMarkerKey, pinnedMarkerKey); const activeMarker = useMemo( @@ -230,7 +229,6 @@ export function SessionDetail({ session }: SessionDetailProps) { sorted={sorted} knownWordsMap={knownWordsMap} cardEvents={cardEvents} - seekEvents={seekEvents} yomitanLookupEvents={yomitanLookupEvents} pauseRegions={pauseRegions} markers={markers} @@ -242,7 +240,6 @@ export function SessionDetail({ session }: SessionDetailProps) { loadingNoteIds={loadingNoteIds} onOpenNote={handleOpenNote} pauseCount={pauseCount} - seekCount={seekCount} cardEventCount={cardEventCount} lookupRate={lookupRate} session={session} @@ -254,7 +251,6 @@ export function SessionDetail({ session }: SessionDetailProps) { ; cardEvents: SessionEvent[]; - seekEvents: SessionEvent[]; yomitanLookupEvents: SessionEvent[]; pauseRegions: Array<{ startMs: number; endMs: number }>; markers: SessionChartMarker[]; @@ -312,7 +304,6 @@ function RatioView({ loadingNoteIds: Set; onOpenNote: (noteId: number) => void; pauseCount: number; - seekCount: number; cardEventCount: number; lookupRate: ReturnType; session: SessionSummary; @@ -450,22 +441,6 @@ function RatioView({ /> ))} - {seekEvents.map((e, i) => { - const isBackward = e.eventType === EventType.SEEK_BACKWARD; - const stroke = isBackward ? '#f5bde6' : '#8bd5ca'; - return ( - - ); - })} - {/* Yomitan lookup markers */} {yomitanLookupEvents.map((e, i) => ( ; markers: SessionChartMarker[]; @@ -594,7 +565,6 @@ function FallbackView({ loadingNoteIds: Set; onOpenNote: (noteId: number) => void; pauseCount: number; - seekCount: number; cardEventCount: number; lookupRate: ReturnType; session: SessionSummary; @@ -680,20 +650,6 @@ function FallbackView({ strokeOpacity={0.8} /> ))} - {seekEvents.map((e, i) => { - const isBackward = e.eventType === EventType.SEEK_BACKWARD; - const stroke = isBackward ? '#f5bde6' : '#8bd5ca'; - return ( - - ); - })} {yomitanLookupEvents.map((e, i) => ( ; @@ -791,12 +744,7 @@ function StatsBar({ {pauseCount !== 1 ? 's' : ''} )} - {seekCount > 0 && ( - - {seekCount} seek{seekCount !== 1 ? 's' : ''} - - )} - {(pauseCount > 0 || seekCount > 0) && |} + {pauseCount > 0 && |} {/* Group 3: Learning events */} diff --git a/stats/src/components/sessions/SessionEventOverlay.tsx b/stats/src/components/sessions/SessionEventOverlay.tsx index 87322622..e1eee596 100644 --- a/stats/src/components/sessions/SessionEventOverlay.tsx +++ b/stats/src/components/sessions/SessionEventOverlay.tsx @@ -33,8 +33,6 @@ function markerLabel(marker: SessionChartMarker): string { switch (marker.kind) { case 'pause': return '||'; - case 'seek': - return marker.direction === 'backward' ? '<<' : '>>'; case 'card': return '\u26CF'; } @@ -44,10 +42,6 @@ function markerColors(marker: SessionChartMarker): { border: string; bg: string; switch (marker.kind) { case 'pause': return { border: '#f5a97f', bg: 'rgba(245,169,127,0.16)', text: '#f5a97f' }; - case 'seek': - return marker.direction === 'backward' - ? { border: '#f5bde6', bg: 'rgba(245,189,230,0.16)', text: '#f5bde6' } - : { border: '#8bd5ca', bg: 'rgba(139,213,202,0.16)', text: '#8bd5ca' }; case 'card': return { border: '#a6da95', bg: 'rgba(166,218,149,0.16)', text: '#a6da95' }; } diff --git a/stats/src/components/sessions/SessionEventPopover.test.tsx b/stats/src/components/sessions/SessionEventPopover.test.tsx index 801d5dd2..eb1b6c68 100644 --- a/stats/src/components/sessions/SessionEventPopover.test.tsx +++ b/stats/src/components/sessions/SessionEventPopover.test.tsx @@ -41,35 +41,6 @@ test('SessionEventPopover renders formatted card-mine details with fetched note assert.match(markup, /Open in Anki/); }); -test('SessionEventPopover renders seek metadata compactly', () => { - const marker: SessionChartMarker = { - key: 'seek-3000', - kind: 'seek', - anchorTsMs: 3_000, - eventTsMs: 3_000, - direction: 'backward', - fromMs: 5_000, - toMs: 1_500, - }; - - const markup = renderToStaticMarkup( - {}} - onClose={() => {}} - onOpenNote={() => {}} - />, - ); - - assert.match(markup, /Seek backward/); - assert.match(markup, /5\.0s/); - assert.match(markup, /1\.5s/); - assert.match(markup, /3\.5s/); -}); - test('SessionEventPopover renders a cleaner fallback when AnkiConnect provides no preview fields', () => { const marker: SessionChartMarker = { key: 'card-9000', diff --git a/stats/src/components/sessions/SessionEventPopover.tsx b/stats/src/components/sessions/SessionEventPopover.tsx index b9e3090b..2e397b1a 100644 --- a/stats/src/components/sessions/SessionEventPopover.tsx +++ b/stats/src/components/sessions/SessionEventPopover.tsx @@ -31,18 +31,12 @@ export function SessionEventPopover({ onClose, onOpenNote, }: SessionEventPopoverProps) { - const seekDurationLabel = - marker.kind === 'seek' && marker.fromMs !== null && marker.toMs !== null - ? formatEventSeconds(Math.abs(marker.toMs - marker.fromMs))?.replace(/\.0s$/, 's') - : null; - return (
{marker.kind === 'pause' && 'Paused'} - {marker.kind === 'seek' && `Seek ${marker.direction}`} {marker.kind === 'card' && 'Card mined'}
{formatEventTime(marker.eventTsMs)}
@@ -72,7 +66,6 @@ export function SessionEventPopover({ ) : null}
{marker.kind === 'pause' && '||'} - {marker.kind === 'seek' && (marker.direction === 'backward' ? '<<' : '>>')} {marker.kind === 'card' && '\u26CF'}
@@ -84,19 +77,6 @@ export function SessionEventPopover({
)} - {marker.kind === 'seek' && ( -
-
- From{' '} - {formatEventSeconds(marker.fromMs) ?? '\u2014'}{' '} - to {formatEventSeconds(marker.toMs) ?? '\u2014'} -
-
- Length {seekDurationLabel ?? '\u2014'} -
-
- )} - {marker.kind === 'card' && (
diff --git a/stats/src/lib/session-detail.test.tsx b/stats/src/lib/session-detail.test.tsx index e5d63aab..c8e3be80 100644 --- a/stats/src/lib/session-detail.test.tsx +++ b/stats/src/lib/session-detail.test.tsx @@ -46,9 +46,10 @@ test('buildSessionChartEvents keeps only chart-relevant events and pairs pause r { eventType: EventType.LOOKUP, tsMs: 8_000, payload: '{"hit":true}' }, ]); + // Seek events are intentionally dropped from the chart — they were too noisy. assert.deepEqual( - chartEvents.seekEvents.map((event) => event.eventType), - [EventType.SEEK_FORWARD, EventType.SEEK_BACKWARD], + chartEvents.markers.filter((marker) => marker.kind !== 'pause' && marker.kind !== 'card'), + [], ); assert.deepEqual( chartEvents.cardEvents.map((event) => event.tsMs), diff --git a/stats/src/lib/session-events.test.ts b/stats/src/lib/session-events.test.ts index cdfd990c..a8a3fcbc 100644 --- a/stats/src/lib/session-events.test.ts +++ b/stats/src/lib/session-events.test.ts @@ -29,25 +29,20 @@ test('buildSessionChartEvents produces typed hover markers with parsed payload m { eventType: EventType.YOMITAN_LOOKUP, tsMs: 7_000, payload: null }, ]); + // Seek events are intentionally dropped — too noisy on the session chart. assert.deepEqual( chartEvents.markers.map((marker) => marker.kind), - ['seek', 'pause', 'card'], + ['pause', 'card'], ); - const seekMarker = chartEvents.markers[0]!; - assert.equal(seekMarker.kind, 'seek'); - assert.equal(seekMarker.direction, 'forward'); - assert.equal(seekMarker.fromMs, 1_000); - assert.equal(seekMarker.toMs, 5_500); - - const pauseMarker = chartEvents.markers[1]!; + const pauseMarker = chartEvents.markers[0]!; assert.equal(pauseMarker.kind, 'pause'); assert.equal(pauseMarker.startMs, 2_000); assert.equal(pauseMarker.endMs, 5_000); assert.equal(pauseMarker.durationMs, 3_000); assert.equal(pauseMarker.anchorTsMs, 3_500); - const cardMarker = chartEvents.markers[2]!; + const cardMarker = chartEvents.markers[1]!; assert.equal(cardMarker.kind, 'card'); assert.deepEqual(cardMarker.noteIds, [11, 22]); assert.equal(cardMarker.cardsDelta, 2); diff --git a/stats/src/lib/session-events.ts b/stats/src/lib/session-events.ts index ddacfcdb..1ef473f9 100644 --- a/stats/src/lib/session-events.ts +++ b/stats/src/lib/session-events.ts @@ -2,8 +2,6 @@ import { EventType, type SessionEvent } from '../types/stats'; export const SESSION_CHART_EVENT_TYPES = [ EventType.CARD_MINED, - EventType.SEEK_FORWARD, - EventType.SEEK_BACKWARD, EventType.PAUSE_START, EventType.PAUSE_END, EventType.YOMITAN_LOOKUP, @@ -16,7 +14,6 @@ export interface PauseRegion { export interface SessionChartEvents { cardEvents: SessionEvent[]; - seekEvents: SessionEvent[]; yomitanLookupEvents: SessionEvent[]; pauseRegions: PauseRegion[]; markers: SessionChartMarker[]; @@ -58,15 +55,6 @@ export type SessionChartMarker = endMs: number; durationMs: number; } - | { - key: string; - kind: 'seek'; - anchorTsMs: number; - eventTsMs: number; - direction: 'forward' | 'backward'; - fromMs: number | null; - toMs: number | null; - } | { key: string; kind: 'card'; @@ -295,7 +283,6 @@ export function projectSessionMarkerLeftPx({ export function buildSessionChartEvents(events: SessionEvent[]): SessionChartEvents { const cardEvents: SessionEvent[] = []; - const seekEvents: SessionEvent[] = []; const yomitanLookupEvents: SessionEvent[] = []; const pauseRegions: PauseRegion[] = []; const markers: SessionChartMarker[] = []; @@ -317,22 +304,6 @@ export function buildSessionChartEvents(events: SessionEvent[]): SessionChartEve }); } break; - case EventType.SEEK_FORWARD: - case EventType.SEEK_BACKWARD: - seekEvents.push(event); - { - const payload = parsePayload(event.payload); - markers.push({ - key: `seek-${event.tsMs}-${event.eventType}`, - kind: 'seek', - anchorTsMs: event.tsMs, - eventTsMs: event.tsMs, - direction: event.eventType === EventType.SEEK_BACKWARD ? 'backward' : 'forward', - fromMs: readNumberField(payload?.fromMs), - toMs: readNumberField(payload?.toMs), - }); - } - break; case EventType.YOMITAN_LOOKUP: yomitanLookupEvents.push(event); break; @@ -376,7 +347,6 @@ export function buildSessionChartEvents(events: SessionEvent[]): SessionChartEve return { cardEvents, - seekEvents, yomitanLookupEvents, pauseRegions, markers,