Archilyzer · Source

archilyzer

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

commit 557da85c2551ee71dc143773a2d9bfa84a36486f
parent 96f61d843407b8b2a56c9a137fffa6a7bac9716f
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Sat, 18 Apr 2026 01:16:11 -0400

Advanced options

Diffstat:
Mapp/TranscriptSearch.tsx | 224++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Mapp/searchPipeline.ts | 15+++++++++++----
2 files changed, 223 insertions(+), 16 deletions(-)

diff --git a/app/TranscriptSearch.tsx b/app/TranscriptSearch.tsx @@ -25,10 +25,10 @@ type HitGroup = { hits: Hit[]; }; -const MAX_HITS = 500; -const FETCH_CONCURRENCY = 6; -const FLUSH_INTERVAL_MS = 120; -const GROUPS_PAGE_SIZE = 50; +const DEFAULT_MAX_HITS = 500; +const DEFAULT_FETCH_CONCURRENCY = 6; +const DEFAULT_FLUSH_INTERVAL_MS = 120; +const DEFAULT_GROUPS_PAGE_SIZE = 50; export default function TranscriptSearch() { const { @@ -90,16 +90,41 @@ export default function TranscriptSearch() { const [totalToProcess, setTotalToProcess] = useState(0); const [pipelineDone, setPipelineDone] = useState(false); const [capped, setCapped] = useState(false); - const [hitLimit, setHitLimit] = useState(MAX_HITS); - const [groupsShown, setGroupsShown] = useState(GROUPS_PAGE_SIZE); + + // Advanced options — configurable from the UI, pre-filled with defaults. + const [hitBatchValue, setHitBatchValue] = useState(DEFAULT_MAX_HITS); + const [hitBatchUnlimited, setHitBatchUnlimited] = useState(false); + const [groupsBatchValue, setGroupsBatchValue] = useState( + DEFAULT_GROUPS_PAGE_SIZE, + ); + const [groupsBatchUnlimited, setGroupsBatchUnlimited] = useState(false); + const [fetchConcurrency, setFetchConcurrency] = useState( + DEFAULT_FETCH_CONCURRENCY, + ); + const [flushIntervalMs, setFlushIntervalMs] = useState( + DEFAULT_FLUSH_INTERVAL_MS, + ); + const hitBatchSize = hitBatchUnlimited ? Infinity : hitBatchValue; + const groupsBatchSize = groupsBatchUnlimited + ? Infinity + : groupsBatchValue; + + const [hitLimit, setHitLimit] = useState(DEFAULT_MAX_HITS); + const [groupsShown, setGroupsShown] = useState<number>( + DEFAULT_GROUPS_PAGE_SIZE, + ); const pipelineRef = useRef<PipelineController | null>(null); const { openTranscript } = usePlayer(); const trimmed = searchExecuted ? query.trim() : ""; + // Reset the live caps on each new search. Uses the currently configured + // batch sizes so an advanced-options change only takes effect on the next + // search (no jarring mid-search restarts when tuning). useEffect(() => { - setHitLimit(MAX_HITS); - setGroupsShown(GROUPS_PAGE_SIZE); + setHitLimit(hitBatchSize); + setGroupsShown(groupsBatchSize); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [trimmed, useRegex]); const channelOptions = useMemo<string[]>(() => { @@ -165,8 +190,8 @@ export default function TranscriptSearch() { useRegex, regex: regex.re, initialHitLimit: hitLimit, - concurrency: FETCH_CONCURRENCY, - flushIntervalMs: FLUSH_INTERVAL_MS, + concurrency: fetchConcurrency, + flushIntervalMs, emit: (u) => { setHitsBySlug(u.hitsBySlug); setTotalHits(u.totalHits); @@ -356,6 +381,78 @@ export default function TranscriptSearch() { </div> )} + <details className="-mt-3 text-sm text-zinc-600 dark:text-zinc-400"> + <summary className="cursor-pointer select-none text-xs uppercase tracking-wide text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300"> + Advanced options + </summary> + <div className="mt-3 grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-3 p-4 rounded-md border border-zinc-200 dark:border-zinc-800 bg-white/40 dark:bg-zinc-900/40"> + <LimitField + label="Max hits per batch" + hint="Pause after this many matches; “Show more” adds another batch." + value={hitBatchValue} + unlimited={hitBatchUnlimited} + defaultValue={DEFAULT_MAX_HITS} + onValueChange={setHitBatchValue} + onUnlimitedChange={setHitBatchUnlimited} + /> + <LimitField + label="Videos per page" + hint="How many matching videos to show at a time." + value={groupsBatchValue} + unlimited={groupsBatchUnlimited} + defaultValue={DEFAULT_GROUPS_PAGE_SIZE} + onValueChange={setGroupsBatchValue} + onUnlimitedChange={setGroupsBatchUnlimited} + /> + <NumberField + label="Fetch concurrency" + hint="Parallel transcript downloads while searching. Applies on next search." + value={fetchConcurrency} + defaultValue={DEFAULT_FETCH_CONCURRENCY} + min={1} + max={32} + onChange={setFetchConcurrency} + /> + <NumberField + label="Flush interval (ms)" + hint="UI update cadence during search. Applies on next search." + value={flushIntervalMs} + defaultValue={DEFAULT_FLUSH_INTERVAL_MS} + min={16} + max={2000} + onChange={setFlushIntervalMs} + /> + <div className="sm:col-span-2 flex flex-wrap gap-2 pt-1"> + <button + type="button" + onClick={() => { + setHitBatchUnlimited(true); + setGroupsBatchUnlimited(true); + setHitLimit(Infinity); + setGroupsShown(Infinity); + }} + className="px-3 py-1.5 rounded-md border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 hover:bg-zinc-50 dark:hover:bg-zinc-800 text-xs" + > + Search through all transcripts + </button> + <button + type="button" + onClick={() => { + setHitBatchValue(DEFAULT_MAX_HITS); + setHitBatchUnlimited(false); + setGroupsBatchValue(DEFAULT_GROUPS_PAGE_SIZE); + setGroupsBatchUnlimited(false); + setFetchConcurrency(DEFAULT_FETCH_CONCURRENCY); + setFlushIntervalMs(DEFAULT_FLUSH_INTERVAL_MS); + }} + className="px-3 py-1.5 rounded-md border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 hover:bg-zinc-50 dark:hover:bg-zinc-800 text-xs" + > + Reset to defaults + </button> + </div> + </div> + </details> + {!trimmed && ( <p className="text-sm text-zinc-500"> Enter a query to search transcripts. @@ -449,8 +546,8 @@ export default function TranscriptSearch() { <button type="button" onClick={() => { - setGroupsShown((n) => n + GROUPS_PAGE_SIZE); - if (capped) setHitLimit((l) => l + MAX_HITS); + setGroupsShown((n) => n + groupsBatchSize); + if (capped) setHitLimit((l) => l + hitBatchSize); }} className="px-4 py-2 rounded-md border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 hover:bg-zinc-50 dark:hover:bg-zinc-800 text-sm" > @@ -473,6 +570,109 @@ function sameSet<T>(a: Set<T>, b: Set<T>): boolean { return true; } +function NumberField({ + label, + hint, + value, + defaultValue, + min, + max, + onChange, + disabled, +}: { + label: string; + hint?: string; + value: number; + defaultValue: number; + min: number; + max: number; + onChange: (n: number) => void; + disabled?: boolean; +}) { + return ( + <label className="flex flex-col gap-1"> + <span className="text-xs font-medium text-zinc-700 dark:text-zinc-300"> + {label} + </span> + <input + type="number" + min={min} + max={max} + value={Number.isFinite(value) ? value : ""} + placeholder={String(defaultValue)} + disabled={disabled} + onChange={(e) => { + const raw = e.target.value; + if (raw === "") { + onChange(defaultValue); + return; + } + const n = Math.max(min, Math.min(max, Math.floor(Number(raw)))); + if (Number.isFinite(n)) onChange(n); + }} + className="w-full rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm disabled:opacity-50" + /> + {hint && <span className="text-xs text-zinc-500">{hint}</span>} + </label> + ); +} + +function LimitField({ + label, + hint, + value, + unlimited, + defaultValue, + onValueChange, + onUnlimitedChange, +}: { + label: string; + hint?: string; + value: number; + unlimited: boolean; + defaultValue: number; + onValueChange: (n: number) => void; + onUnlimitedChange: (b: boolean) => void; +}) { + return ( + <div className="flex flex-col gap-1"> + <div className="flex items-baseline justify-between gap-2"> + <span className="text-xs font-medium text-zinc-700 dark:text-zinc-300"> + {label} + </span> + <label className="flex items-center gap-1 text-xs select-none"> + <input + type="checkbox" + checked={unlimited} + onChange={(e) => onUnlimitedChange(e.target.checked)} + className="accent-blue-600" + /> + No limit + </label> + </div> + <input + type="number" + min={1} + max={1_000_000} + value={unlimited ? "" : value} + placeholder={unlimited ? "∞" : String(defaultValue)} + disabled={unlimited} + onChange={(e) => { + const raw = e.target.value; + if (raw === "") { + onValueChange(defaultValue); + return; + } + const n = Math.max(1, Math.floor(Number(raw))); + if (Number.isFinite(n)) onValueChange(n); + }} + className="w-full rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm disabled:opacity-50" + /> + {hint && <span className="text-xs text-zinc-500">{hint}</span>} + </div> + ); +} + function formatSeconds(s: number): string { const h = Math.floor(s / 3600); const m = Math.floor((s % 3600) / 60); diff --git a/app/searchPipeline.ts b/app/searchPipeline.ts @@ -46,7 +46,12 @@ export function createSearchPipeline( } = config; let cancelled = false; + // `idx` is the next-to-claim cursor (leading edge); `completed` counts + // slugs that workers have finished scanning (trailing edge). We report + // `completed` to the UI so progress reflects actual work done, not + // in-flight claims — and it never exceeds slugs.length. let idx = 0; + let completed = 0; let totalSoFar = 0; let hitLimit = initialHitLimit; let activeWorkers = 0; @@ -58,8 +63,10 @@ export function createSearchPipeline( emit({ hitsBySlug: { ...localHits }, totalHits: totalSoFar, - processed: idx, + processed: completed, totalToProcess: slugs.length, + // `capped` keys off `idx` (claimed), not `completed` — raising the cap + // only helps if there are still unclaimed slugs a worker can pick up. capped: totalSoFar >= hitLimit && idx < slugs.length, done, ...overrides, @@ -90,14 +97,13 @@ export function createSearchPipeline( try { while (!cancelled) { if (totalSoFar >= hitLimit) return; + if (idx >= slugs.length) return; const my = idx++; - if (my >= slugs.length) return; const slug = slugs[my]; try { const full = await fetchTranscript(slug); if (cancelled) return; - if (totalSoFar >= hitLimit) return; - if (full.cues) { + if (full.cues && totalSoFar < hitLimit) { const remaining = hitLimit - totalSoFar; const hits = findHitsInCues( full.cues, @@ -114,6 +120,7 @@ export function createSearchPipeline( } catch { // ignore per-transcript failures } + completed++; scheduleFlush(); } } finally {