feat(ai): server-side chat history + assistant UX fixes

Server-side, per-user chat history (the thread now survives reload):
- new ai_chat_messages table + migration; getChatHistory/appendChatMessages/
  clearChatHistory; GET/POST/DELETE /api/admin/ai/history (ai.use-guarded);
  service + HTTP tests (isolation, ordering, clear, perm, validation).
- DashAssistant loads the thread on mount and persists each turn; "Vymazat"
  clears it. gcTime:0 so each mount re-reads the DB (no stale-cache resurrection);
  composer gated until history settles + seed flag flipped on submit (no
  seed/submit race clobbering a freshly-sent turn).

UX + correctness fixes from review/feedback:
- review cards moved OUT of the scrollable thread into a "Faktury k potvrzení"
  section so Uložit is always reachable; chat auto-scroll no longer fires on
  field edits.
- chat bubble text colour set on the Typography (GlobalStyles pins `p` to
  text.secondary, which was overriding the inherited bubble colour → unreadable).
- never clear input/staged PDFs on a failed submit (rollback + keep), so a
  budget error can't wipe staged invoices; roll back the optimistic user turn.
- stable attachment ids (no crypto.randomUUID — undefined over plain HTTP).
- editable Datum splatnosti + Popis fields; client-side extraction summary.

Security: DashProfile clipboard copy now uses an execCommand fallback (works
over plain HTTP) and only shows the success toast when the copy actually
succeeded — previously it falsely claimed to copy 2FA codes / the TOTP secret.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-06-08 16:02:26 +02:00
parent 0226cdd52b
commit 58e5fd2b1d
9 changed files with 771 additions and 167 deletions

View File

@@ -19,9 +19,28 @@ export interface ExtractedInvoice {
description: string | null;
}
export interface StoredChatMessage {
role: "user" | "assistant";
content: string;
created_at: string;
}
export const aiUsageOptions = () =>
queryOptions({
queryKey: ["ai", "usage"],
queryFn: () => jsonQuery<AiUsage>("/api/admin/ai/usage"),
staleTime: 30_000,
});
export const aiHistoryOptions = () =>
queryOptions({
queryKey: ["ai", "history"],
queryFn: () =>
jsonQuery<{ messages: StoredChatMessage[] }>("/api/admin/ai/history"),
// Within a mount the thread is seeded once and mutated locally, so don't
// refetch on focus (staleTime Infinity). But gcTime 0 drops the cache on
// unmount, so every remount re-reads the DB (the source of truth) — this
// is what keeps a cleared/appended thread from resurrecting stale messages.
staleTime: Infinity,
gcTime: 0,
});