import { useActionMutation, useActionQuery, } from "@agent-native/core/client/hooks"; import { useT } from "@agent-native/core/client/i18n"; import { IconBook2, IconChevronDown, IconChevronRight, IconPlus, IconTrash, } from "@tabler/icons-react"; import { useQueryClient } from "@tanstack/react-query"; import { useState } from "react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible"; import { Input } from "@/components/ui/input"; interface VocabularyEntry { id: string; term: string; replacement: string; confidence: number; usesCount: number; } const QUERY_KEY = ["action", "list-vocabulary", {}] as const; /** * Dictionary section for the /dictate page. Lists learned personal-vocabulary * entries (auto-learned from post-paste corrections, see * `personal-vocabulary.ts` on desktop) and lets the user manually add or * remove terms. Biases speech recognition toward preferred spellings — * explained in one line, not a full settings page. */ export function VocabularySection() { const t = useT(); const [open, setOpen] = useState(false); const [term, setTerm] = useState(""); const [replacement, setReplacement] = useState(""); const queryClient = useQueryClient(); const { data, isLoading } = useActionQuery<{ vocabulary: VocabularyEntry[]; }>("list-vocabulary", {}, { enabled: open }); const entries = data?.vocabulary ?? []; const addTerm = useActionMutation("add-vocabulary-term", { onSuccess: () => { setTerm(""); setReplacement(""); queryClient.invalidateQueries({ queryKey: QUERY_KEY }); }, onError: () => { toast.error(t("dictateRoute.vocabularyAddFailed")); }, }); const removeTerm = useActionMutation( "remove-vocabulary-term", { method: "DELETE", onMutate: async (vars) => { await queryClient.cancelQueries({ queryKey: QUERY_KEY }); const prev = queryClient.getQueryData(QUERY_KEY); queryClient.setQueryData( QUERY_KEY, (old: { vocabulary: VocabularyEntry[] } | undefined) => old ? { vocabulary: old.vocabulary.filter((v) => v.id !== vars.id), } : old, ); return { prev }; }, onError: (_err, _vars, ctx: any) => { if (ctx?.prev) queryClient.setQueryData(QUERY_KEY, ctx.prev); toast.error(t("dictateRoute.vocabularyRemoveFailed")); }, }, ); function handleAdd() { const trimmedTerm = term.trim(); const trimmedReplacement = replacement.trim() || trimmedTerm; if (!trimmedTerm || addTerm.isPending) return; addTerm.mutate({ term: trimmedTerm, replacement: trimmedReplacement, confidence: 1, }); } return (
{t("dictateRoute.dictionaryTitle")}
{open ? ( ) : ( )}

{t("dictateRoute.dictionaryDescription")}

setTerm(e.target.value)} placeholder={t("dictateRoute.dictionaryTermPlaceholder")} className="h-8 w-40 text-xs" onKeyDown={(e) => { if (e.key === "Enter") handleAdd(); }} /> setReplacement(e.target.value)} placeholder={t("dictateRoute.dictionaryReplacementPlaceholder")} className="h-8 w-48 text-xs" onKeyDown={(e) => { if (e.key === "Enter") handleAdd(); }} />
{isLoading ? (
{t("dictateRoute.dictionaryLoading")}
) : entries.length === 0 ? (
{t("dictateRoute.dictionaryEmpty")}
) : (
{entries.map((entry) => (
{entry.term} {entry.replacement !== entry.term ? ( <> {entry.replacement} ) : null}
{t("dictateRoute.dictionaryUsesCount", { count: entry.usesCount, })}
))}
)}
); }