Archilyzer · Source

archilyzer

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

commit 51444d2cf7bb7e0e63ba73087788e52f9fa6202a
parent 4b6cd617655b4739b09053e9f5dc0a1a26299f34
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Sat, 26 Sep 2026 04:10:25 -0400

hub: NO_ARCHIVES_IN_SCOPE draft — "Choose some on the hub's front page to ask.", the phrase a link to it (re-read)

/ask shows no chips, so "Turn one on above" was wrong there. The constant
stays the one place for the operator's copy. Its [bracketed] words become
a link to "/", where the chips are, and dropping the brackets removes the
link. copyWithLink / copyText (hubScopeCopy.ts) split and flatten it for
AskChat and the e2e. The Composer's `blocked` prop is now a ReactNode. The
e2e asserts the flattened text, one line, and the link's href.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Diffstat:
Mexport/app/ask/AskChat.tsx | 18++++++++++++++++--
Mexport/app/ask/Composer.tsx | 4++--
Mexport/app/ask/hubScopeCopy.ts | 22++++++++++++++++++++--
Mexport/e2e-hub/federated-search.spec.ts | 18+++++++++++++++---
4 files changed, 53 insertions(+), 9 deletions(-)

diff --git a/export/app/ask/AskChat.tsx b/export/app/ask/AskChat.tsx @@ -1,6 +1,7 @@ "use client"; import { useEffect, useMemo, useRef, useState } from "react"; +import Link from "next/link"; import { ArrowDownIcon, PlusIcon } from "lucide-react"; import { usePlayerOptional } from "yt-dlp-transcript-common/components/PlayerProvider"; import { useSearchData } from "yt-dlp-transcript-common/components/SearchDataContext"; @@ -14,7 +15,7 @@ import { GroundingPalette } from "./GroundingPalette"; import { SavedChatsRow } from "./SavedChatsRow"; import { MessageBubble } from "./MessageBubble"; import { Composer } from "./Composer"; -import { NO_ARCHIVES_IN_SCOPE } from "./hubScopeCopy"; +import { NO_ARCHIVES_IN_SCOPE, copyWithLink } from "./hubScopeCopy"; export default function AskChat() { const s = useAskChat(); @@ -39,6 +40,7 @@ export default function AskChat() { !!federation && federation.sites.length > 0 && federation.sites.every((f) => f.status === "off"); + const noneLine = copyWithLink(NO_ARCHIVES_IN_SCOPE); // Opening a citation seeks the shared transcript modal to the cited line. It // writes ?v=&t=&vm= via replaceState on this same /ask route (no navigation, @@ -473,7 +475,19 @@ export default function AskChat() { stop={s.stop} busy={busy} summariesReady={summariesReady} - blocked={noneInScope ? NO_ARCHIVES_IN_SCOPE : undefined} + blocked={ + noneInScope ? ( + <> + {noneLine.before} + {noneLine.link && ( + <Link href="/" className="text-brand underline-offset-2 hover:underline"> + {noneLine.link} + </Link> + )} + {noneLine.after} + </> + ) : undefined + } hasKey={!!apiKey.trim()} markdownOn={markdownOn} setMarkdownOn={s.setMarkdownOn} diff --git a/export/app/ask/Composer.tsx b/export/app/ask/Composer.tsx @@ -1,6 +1,6 @@ "use client"; -import type { RefObject } from "react"; +import type { ReactNode, RefObject } from "react"; type Props = { input: string; @@ -11,7 +11,7 @@ type Props = { summariesReady: boolean; // Why the chat cannot ask at all, shown as one line under the box (the box is // disabled). Set on the hub when every archive is switched off. - blocked?: string; + blocked?: ReactNode; hasKey: boolean; markdownOn: boolean; setMarkdownOn: (v: boolean) => void; diff --git a/export/app/ask/hubScopeCopy.ts b/export/app/ask/hubScopeCopy.ts @@ -1,6 +1,24 @@ // The hub's /ask line for a browser that has switched EVERY archive off with // the front page's scope chips: the chat has nothing to search, so its composer // is disabled and this one line says why. The operator's copy — change it here -// and nowhere else (AskChat renders it, the hub e2e asserts it). +// and nowhere else (AskChat renders it, the hub e2e asserts it). The words in +// [brackets] become a link to the hub's front page, where the chips are; drop +// the brackets for no link. export const NO_ARCHIVES_IN_SCOPE = - "No archives selected. Turn one on above to ask."; + "No archives selected. Choose some on the [hub's front page] to ask."; + +// A copy line split around its one [bracketed] link. +export function copyWithLink(copy: string): { + before: string; + link?: string; + after: string; +} { + const m = /^([^[]*)\[([^\]]+)\]([\s\S]*)$/.exec(copy); + return m ? { before: m[1], link: m[2], after: m[3] } : { before: copy, after: "" }; +} + +// The line as a reader sees it (the brackets gone). +export function copyText(copy: string): string { + const { before, link = "", after } = copyWithLink(copy); + return before + link + after; +} diff --git a/export/e2e-hub/federated-search.spec.ts b/export/e2e-hub/federated-search.spec.ts @@ -1,5 +1,9 @@ import { expect, test, type Page, type Route } from "@playwright/test"; -import { NO_ARCHIVES_IN_SCOPE } from "../app/ask/hubScopeCopy"; +import { + NO_ARCHIVES_IN_SCOPE, + copyText, + copyWithLink, +} from "../app/ask/hubScopeCopy"; // The hub's federated search, per archive: two official members (hub-sites.json) // served by route mocks WITH CORS, each with one video. Proves the scope chips @@ -475,8 +479,16 @@ test.describe("hub federated search — scope, per-archive state, attribution", .locator("form") .filter({ has: page.getByRole("button", { name: "Ask", exact: true }) }); const blocked = composer.getByTestId("ask-blocked"); - await expect(blocked).toHaveText(NO_ARCHIVES_IN_SCOPE); - await expect(page.getByText(NO_ARCHIVES_IN_SCOPE)).toHaveCount(1); + await expect(blocked).toHaveText(copyText(NO_ARCHIVES_IN_SCOPE)); + await expect(page.getByTestId("ask-blocked")).toHaveCount(1); + // Its [bracketed] words (when the copy has any) link to the front page, + // where the chips are. + const { link } = copyWithLink(NO_ARCHIVES_IN_SCOPE); + if (link) { + await expect( + blocked.getByRole("link", { name: link, exact: true }), + ).toHaveAttribute("href", "/"); + } await expect(composer.locator("textarea")).toBeDisabled(); await expect( composer.getByRole("button", { name: "Ask", exact: true }),