"use client"; // Authoring UI for search aliases. Two independent sections — the global // dictionary and the site's overrides — each edits a list of concept rows and // saves the whole list via a server action. Triggers // are entered comma-separated and shown back as chips. Suggestions are // validated as regex when the row is in regex mode, mirroring the viewer's // leaf-input guard. import { useRef, useState } from "react"; import { Button } from "yt-dlp-transcript-common/components/ui/button"; import { Input } from "yt-dlp-transcript-common/components/ui/input"; import { Checkbox } from "yt-dlp-transcript-common/components/ui/checkbox"; import { slugifyAliasId, type AliasConfig, type SearchAlias, } from "yt-dlp-transcript-common/lib/searchAliases"; import { saveGlobalAliasesAction, saveSiteAliasesAction } from "../lib/aliasesActions"; type Row = { key: string; id: string; label: string; triggersText: string; suggestion: string; useRegex: boolean; enabled: boolean; note: string; }; function toRow(a: SearchAlias, key: string): Row { return { key, id: a.id, label: a.label, triggersText: a.triggers.join(", "), suggestion: a.suggestion, useRegex: a.useRegex, enabled: a.enabled !== false, note: a.note ?? "", }; } function parseTriggers(text: string): string[] { return text .split(",") .map((t) => t.trim()) .filter(Boolean); } function toAlias(r: Row): SearchAlias { const triggers = parseTriggers(r.triggersText); const label = r.label.trim(); const note = r.note.trim(); return { id: r.id.trim() || slugifyAliasId(label), label, triggers, suggestion: r.suggestion, useRegex: r.useRegex, enabled: r.enabled, ...(note ? { note } : {}), }; } function isValidRegex(s: string): boolean { try { new RegExp(s); return true; } catch { return false; } } export function EditorAliasesClient({ globalConfig, siteConfig, siteId, }: { globalConfig: AliasConfig; siteConfig: AliasConfig; siteId: string; }) { return (
saveGlobalAliasesAction(cfg)} testid="global" /> saveSiteAliasesAction(siteId, cfg)} testid="site" />
); } function AliasSection({ title, subtitle, initial, onSave, testid, }: { title: string; subtitle: string; initial: SearchAlias[]; onSave: (config: AliasConfig) => Promise<{ ok: true }>; testid: string; }) { const keyRef = useRef(0); const mkKey = () => `r${keyRef.current++}`; const [rows, setRows] = useState(() => initial.map((a) => toRow(a, mkKey())), ); const [dirty, setDirty] = useState(false); const [saving, setSaving] = useState(false); const [saved, setSaved] = useState(false); const patch = (key: string, next: Partial) => { setRows((rs) => rs.map((r) => (r.key === key ? { ...r, ...next } : r))); setDirty(true); setSaved(false); }; const remove = (key: string) => { setRows((rs) => rs.filter((r) => r.key !== key)); setDirty(true); setSaved(false); }; const add = () => { setRows((rs) => [ ...rs, { key: mkKey(), id: "", label: "", triggersText: "", suggestion: "", useRegex: true, enabled: true, note: "", }, ]); setDirty(true); setSaved(false); }; const anyRegexInvalid = rows.some( (r) => r.useRegex && r.suggestion.trim() !== "" && !isValidRegex(r.suggestion), ); const save = async () => { setSaving(true); try { // Drop rows missing a label, triggers, or a suggestion — coerce would too. const aliases = rows .map(toAlias) .filter((a) => a.label && a.triggers.length > 0 && a.suggestion); await onSave({ aliases }); setDirty(false); setSaved(true); } finally { setSaving(false); } }; return (

{title}

{subtitle}

{saved && !dirty && ( Saved )}
{rows.length === 0 ? (

No aliases yet. Add one to start suggesting better queries.

) : (
    {rows.map((r) => ( patch(r.key, next)} onRemove={() => remove(r.key)} testid={testid} /> ))}
)}
); } function AliasRow({ row, onPatch, onRemove, testid, }: { row: Row; onPatch: (next: Partial) => void; onRemove: () => void; testid: string; }) { const triggers = parseTriggers(row.triggersText); const regexInvalid = row.useRegex && row.suggestion.trim() !== "" && !isValidRegex(row.suggestion); return (
  • {triggers.length > 0 && (
    {triggers.map((t, i) => ( {t} ))}
    )}
    {regexInvalid && (

    Invalid regex

    )} {triggers[0] && row.suggestion && (

    Preview: typing {triggers[0]} suggests{" "} {row.suggestion}

    )}
  • ); } function cnRow(enabled: boolean): string { return [ "flex flex-col gap-2 rounded-lg border bg-card/60 px-3 py-3", enabled ? "" : "opacity-60", ] .filter(Boolean) .join(" "); }