diff --git a/backlog/tasks/task-10 - Modernize-savings-chat-agent-UI-with-AI-Elements-phases.md b/backlog/tasks/task-10 - Modernize-savings-chat-agent-UI-with-AI-Elements-phases.md new file mode 100644 index 0000000..c2aa1fc --- /dev/null +++ b/backlog/tasks/task-10 - Modernize-savings-chat-agent-UI-with-AI-Elements-phases.md @@ -0,0 +1,44 @@ +--- +id: TASK-10 +title: Modernize savings chat agent UI with AI Elements phases +status: In Progress +assignee: [] +created_date: '2026-06-16 08:38' +updated_date: '2026-06-16 08:41' +labels: [] +dependencies: [] +priority: high +ordinal: 10000 +--- + +## Description + + +Implement the planned AI Elements-inspired savings chat agent UI in three sequential phases: conversation/message/prompt/tool trace primitives, reasoning/work-progress disclosure, and sources/citation support. + + +## Acceptance Criteria + +- [x] #1 Phase 1 replaces the basic chat surface with reusable conversation, message, prompt input, and tool trace UI primitives +- [ ] #2 Phase 2 adds a safe reasoning/work-progress disclosure derived from existing tool traces, without exposing hidden chain-of-thought +- [ ] #3 Phase 3 adds structured source/citation support through stored assistant metadata and visible UI affordances +- [ ] #4 Each phase is covered by failing-first tests, verified after implementation, and committed separately + + +## Implementation Plan + + +1. Phase 1 TDD: add tested chat UI view-model helpers, reusable conversation/message/prompt/tool trace components, and integrate them into SavingsChatPage. +2. Commit Phase 1. +3. Phase 2 TDD: derive safe reasoning/work-progress summaries from tool traces and render them as collapsible disclosure. +4. Commit Phase 2. +5. Phase 3 TDD: extend chat message metadata with structured sources/citations, persist it through Convex history, and render sources/inline citations. +6. Commit Phase 3. +7. Run focused tests, lint, build, and record verification notes without closing the task until user confirmation. + + +## Implementation Notes + + +Phase 1 complete: added AgentChat primitives for conversation, message rendering, prompt input, and tool trace disclosure; integrated SavingsChatPage. Verification: npx vitest src/components/chat/AgentChat.test.tsx --run, npx eslint targeted chat/page files, npm run build (Vite chunk-size warning only). + diff --git a/src/components/chat/AgentChat.test.tsx b/src/components/chat/AgentChat.test.tsx new file mode 100644 index 0000000..14f8284 --- /dev/null +++ b/src/components/chat/AgentChat.test.tsx @@ -0,0 +1,113 @@ +import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, test } from "vitest"; +import { + AgentConversation, + AgentMessage, + AgentPromptInput, + type AgentChatMessage, +} from "./AgentChat"; +import { getToolTraceSummary } from "./agentChatModel"; + +const assistantMessage: AgentChatMessage = { + id: "assistant-1", + role: "assistant", + content: "Deine groesste Ausgabe war Miete.", + toolTrace: [ + { + name: "summarize_transactions", + inputSummary: "Mai 2026", + resultSummary: "12 Umsaetze zusammengefasst", + }, + { + name: "list_transactions", + inputSummary: "Miete", + resultSummary: "1 Treffer", + }, + ], +}; + +describe("AgentChat phase 1 components", () => { + test("summarizes tool traces for compact agent transparency", () => { + expect(getToolTraceSummary(undefined)).toBe("Keine Werkzeuge"); + expect(getToolTraceSummary([])).toBe("Keine Werkzeuge"); + expect(getToolTraceSummary(assistantMessage.toolTrace)).toBe("2 Werkzeuge verwendet"); + }); + + test("renders conversation messages with assistant tool trace details", () => { + const markup = renderToStaticMarkup( + , + ); + + expect(markup).toContain("Was war teuer?"); + expect(markup).toContain("Deine groesste Ausgabe war Miete."); + expect(markup).toContain("2 Werkzeuge verwendet"); + expect(markup).toContain("summarize_transactions"); + expect(markup).toContain("12 Umsaetze zusammengefasst"); + }); + + test("renders the prompt input with ready and busy states", () => { + const readyMarkup = renderToStaticMarkup( + undefined} + onSubmit={() => undefined} + />, + ); + const busyMarkup = renderToStaticMarkup( + undefined} + onSubmit={() => undefined} + />, + ); + + expect(readyMarkup).toContain("Welche Auswertung soll ich machen?"); + expect(readyMarkup).toContain("Senden"); + expect(busyMarkup).toContain("Antwort laeuft"); + expect(busyMarkup).toContain("disabled"); + }); + + test("disables prompt submit for whitespace-only input", () => { + const markup = renderToStaticMarkup( + undefined} + onSubmit={() => undefined} + />, + ); + + expect(markup).toContain("disabled"); + }); + + test("renders a thinking indicator while the agent is submitting", () => { + const markup = renderToStaticMarkup( + , + ); + + expect(markup).toContain("Denk mit der KI nach..."); + }); + + test("renders a single message without a tool section when no trace exists", () => { + const markup = renderToStaticMarkup( + , + ); + + expect(markup).toContain("Assistant"); + expect(markup).toContain("Hallo"); + expect(markup).not.toContain("Werkzeuge verwendet"); + }); +}); diff --git a/src/components/chat/AgentChat.tsx b/src/components/chat/AgentChat.tsx new file mode 100644 index 0000000..767b7ac --- /dev/null +++ b/src/components/chat/AgentChat.tsx @@ -0,0 +1,162 @@ +import { + type ChangeEvent, + type FormEvent, + type HTMLAttributes, + type ReactNode, + type Ref, +} from "react"; +import { Loader2, Send, Wrench } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; +import { + getToolTraceSummary, + type AgentChatMessage, + type AgentToolTrace, +} from "./agentChatModel"; +export type { AgentChatMessage, AgentToolTrace } from "./agentChatModel"; + +type AgentConversationProps = HTMLAttributes & { + messages: AgentChatMessage[]; + isSubmitting?: boolean; + scrollRef?: Ref; +}; + +type AgentMessageProps = HTMLAttributes & { + message: AgentChatMessage; +}; + +type AgentPromptInputProps = { + value: string; + placeholder: string; + disabled: boolean; + isSubmitting: boolean; + onChange: (value: string) => void; + onSubmit: (event: FormEvent) => void; +}; + +export function AgentConversation({ + messages, + isSubmitting = false, + scrollRef, + className, + ...props +}: AgentConversationProps) { + return ( +
+
+ {messages.map((message) => ( + + ))} + {isSubmitting && } +
+
+ ); +} + +export function AgentMessage({ message, className, ...props }: AgentMessageProps) { + const isUser = message.role === "user"; + + return ( +
+
+

+ {isUser ? "User" : "Assistant"} +

+

{message.content}

+ {!isUser && message.toolTrace && message.toolTrace.length > 0 && ( + + )} +
+
+ ); +} + +function AgentToolTracePanel({ toolTrace }: { toolTrace: AgentToolTrace[] }) { + return ( +
+ + + {getToolTraceSummary(toolTrace)} + +
+ {toolTrace.map((tool, toolIndex) => ( +
+

{tool.name}

+ + +
+ ))} +
+
+ ); +} + +function ToolTraceLine({ label, value }: { label: ReactNode; value: ReactNode }) { + return ( +

+ {label}: + {value} +

+ ); +} + +function AgentThinkingIndicator() { + return ( +
+ + Denk mit der KI nach... +
+ ); +} + +export function AgentPromptInput({ + value, + placeholder, + disabled, + isSubmitting, + onChange, + onSubmit, +}: AgentPromptInputProps) { + const submitDisabled = disabled || value.trim().length === 0; + const handleChange = (event: ChangeEvent) => onChange(event.target.value); + + return ( +
+
+