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,