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:
@@ -3,7 +3,11 @@ import multipart from "@fastify/multipart";
|
||||
import { requirePermission } from "../../middleware/auth";
|
||||
import { success, error } from "../../utils/response";
|
||||
import { parseBody } from "../../schemas/common";
|
||||
import { AiChatSchema, AiBudgetSchema } from "../../schemas/ai.schema";
|
||||
import {
|
||||
AiChatSchema,
|
||||
AiBudgetSchema,
|
||||
AiHistoryAppendSchema,
|
||||
} from "../../schemas/ai.schema";
|
||||
import { logAudit } from "../../services/audit";
|
||||
import { config } from "../../config/env";
|
||||
import {
|
||||
@@ -14,6 +18,9 @@ import {
|
||||
setBudgetUsd,
|
||||
chat,
|
||||
extractInvoice,
|
||||
getChatHistory,
|
||||
appendChatMessages,
|
||||
clearChatHistory,
|
||||
type ExtractedInvoice,
|
||||
} from "../../services/ai.service";
|
||||
|
||||
@@ -123,4 +130,36 @@ export default async function aiRoutes(app: FastifyInstance): Promise<void> {
|
||||
return success(reply, { invoices: results });
|
||||
},
|
||||
);
|
||||
|
||||
// GET /api/admin/ai/history — this user's stored chat thread
|
||||
app.get(
|
||||
"/history",
|
||||
{ preHandler: requirePermission("ai.use") },
|
||||
async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const messages = await getChatHistory(request.authData!.userId);
|
||||
return success(reply, { messages });
|
||||
},
|
||||
);
|
||||
|
||||
// POST /api/admin/ai/history — append turns to this user's thread
|
||||
app.post(
|
||||
"/history",
|
||||
{ preHandler: requirePermission("ai.use") },
|
||||
async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const body = parseBody(AiHistoryAppendSchema, request.body);
|
||||
if ("error" in body) return error(reply, body.error, 400);
|
||||
await appendChatMessages(request.authData!.userId, body.data.messages);
|
||||
return success(reply, { ok: true });
|
||||
},
|
||||
);
|
||||
|
||||
// DELETE /api/admin/ai/history — clear this user's thread
|
||||
app.delete(
|
||||
"/history",
|
||||
{ preHandler: requirePermission("ai.use") },
|
||||
async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
await clearChatHistory(request.authData!.userId);
|
||||
return success(reply, { ok: true });
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user