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:
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>
</>
)}