Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit 6fa22dfd068b6e3d1d650a54ea4a043c756246b7
parent 191ff73bf1e0c0061994d828732e6571a1b3be64
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Wed, 15 Apr 2026 21:42:05 -0400

Add multi-channel and video grouping

Diffstat:
Mapp/TranscriptSearch.tsx | 138+++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
1 file changed, 92 insertions(+), 46 deletions(-)

diff --git a/app/TranscriptSearch.tsx b/app/TranscriptSearch.tsx @@ -16,11 +16,16 @@ type Summary = { }; type Hit = { + start: number; + text: string; +}; + +type HitGroup = { slug: string; title: string; + channel: string; date: string; - start: number; - text: string; + hits: Hit[]; }; const MAX_HITS = 200; @@ -132,27 +137,41 @@ export default function TranscriptSearch({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [trimmed, transcripts, useRegex, regex.re]); - const cueHits = useMemo<Hit[]>(() => { - if (!trimmed) return []; - if (useRegex && !regex.re) return []; - const hits: Hit[] = []; + const { groups, totalHits, capped } = useMemo<{ + groups: HitGroup[]; + totalHits: number; + capped: boolean; + }>(() => { + if (!trimmed) return { groups: [], totalHits: 0, capped: false }; + if (useRegex && !regex.re) + return { groups: [], totalHits: 0, capped: false }; + const groups: HitGroup[] = []; + let total = 0; for (const t of transcripts) { const full = fetched[t.slug]; if (!full?.cues) continue; + const hits: Hit[] = []; for (const c of full.cues) { if (matches(c.text)) { - hits.push({ - slug: t.slug, - title: t.title, - date: summaryBySlug.get(t.slug)?.date ?? "", - start: Math.round(c.start), - text: c.text, - }); - if (hits.length >= MAX_HITS) return hits; + hits.push({ start: Math.round(c.start), text: c.text }); + total++; + if (total >= MAX_HITS) break; } } + if (hits.length > 0) { + groups.push({ + slug: t.slug, + title: t.title, + channel: t.channel, + date: summaryBySlug.get(t.slug)?.date ?? "", + hits, + }); + } + if (total >= MAX_HITS) { + return { groups, totalHits: total, capped: true }; + } } - return hits; + return { groups, totalHits: total, capped: false }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [trimmed, transcripts, fetched, summaryBySlug, useRegex, regex.re]); @@ -227,9 +246,13 @@ export default function TranscriptSearch({ <h2 className="text-sm font-medium text-zinc-500 mb-2 flex items-center gap-2"> <span> Transcript mentions{" "} - {cueHits.length >= MAX_HITS - ? `(${MAX_HITS}+)` - : `(${cueHits.length})`} + {totalHits > 0 + ? `(${capped ? `${MAX_HITS}+` : totalHits} mention${ + totalHits === 1 ? "" : "s" + } across ${groups.length} video${ + groups.length === 1 ? "" : "s" + })` + : "(0)"} </span> {fetching && ( <span className="text-xs text-zinc-400 font-normal"> @@ -237,44 +260,67 @@ export default function TranscriptSearch({ </span> )} </h2> - {cueHits.length === 0 && !fetching && ( + {groups.length === 0 && !fetching && ( <p className="text-sm text-zinc-500">No transcript mentions.</p> )} - <ul className="flex flex-col divide-y divide-zinc-200 dark:divide-zinc-800 border border-zinc-200 dark:border-zinc-800 rounded-lg overflow-hidden bg-white dark:bg-zinc-900"> - {cueHits.map((h, i) => { - const isLast = - lastHit?.slug === h.slug && lastHit?.start === h.start; - return ( - <li key={i}> + {groups.length > 0 && ( + <div className="flex flex-col gap-3"> + {groups.map((g) => ( + <div + key={g.slug} + className="border border-zinc-200 dark:border-zinc-800 rounded-lg overflow-hidden bg-white dark:bg-zinc-900" + > <button type="button" onClick={() => { - setLastHit({ slug: h.slug, start: h.start }); - openTranscript(h.slug, h.start); + setLastHit({ slug: g.slug, start: 0 }); + openTranscript(g.slug); }} - className={`w-full text-left flex gap-3 px-4 py-2 hover:bg-zinc-50 dark:hover:bg-zinc-800 ${ - isLast - ? "bg-amber-100/70 dark:bg-amber-500/10 ring-1 ring-inset ring-amber-400/60" - : "" - }`} + className="w-full text-left px-4 py-2 bg-zinc-50 dark:bg-zinc-900/60 border-b border-zinc-200 dark:border-zinc-800 hover:bg-zinc-100 dark:hover:bg-zinc-800 flex items-baseline gap-2" > - <span className="text-xs font-mono text-zinc-500 shrink-0 w-28"> - {formatSeconds(h.start)} + <span className="font-medium truncate flex-1 min-w-0"> + {g.title} </span> - <span className="flex-1 min-w-0"> - <span className="block text-xs text-zinc-500 truncate"> - {h.title} - {h.date && ` · ${h.date}`} - </span> - <span className="block text-sm"> - {highlight(h.text, trimmed, useRegex)} - </span> + <span className="text-xs text-zinc-500 shrink-0"> + {g.channel && `${g.channel} · `} + {g.date} + {` · ${g.hits.length} mention${g.hits.length === 1 ? "" : "s"}`} </span> </button> - </li> - ); - })} - </ul> + <ul className="flex flex-col divide-y divide-zinc-200 dark:divide-zinc-800"> + {g.hits.map((h, i) => { + const isLast = + lastHit?.slug === g.slug && + lastHit?.start === h.start; + return ( + <li key={i}> + <button + type="button" + onClick={() => { + setLastHit({ slug: g.slug, start: h.start }); + openTranscript(g.slug, h.start); + }} + className={`w-full text-left flex gap-3 px-4 py-2 hover:bg-zinc-50 dark:hover:bg-zinc-800 ${ + isLast + ? "bg-amber-100/70 dark:bg-amber-500/10 ring-1 ring-inset ring-amber-400/60" + : "" + }`} + > + <span className="text-xs font-mono text-zinc-500 shrink-0 w-16"> + {formatSeconds(h.start)} + </span> + <span className="flex-1 min-w-0 text-sm"> + {highlight(h.text, trimmed, useRegex)} + </span> + </button> + </li> + ); + })} + </ul> + </div> + ))} + </div> + )} </section> </> )}