diff --git a/CHANGELOG.md b/CHANGELOG.md index cbbeb3a..5bae654 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ incompatibles. Chaque ligne renvoie à un commit dédié (un artefact = un commi ## [Unreleased] +## [0.23.0] — 2026-06-23 +### Added +- UI : vue **Chat** (`web/src/pages/Chat.tsx`) — conversation avec l'agent via + `POST /api/chat`, bulles user/assistant, état "réfléchit", auto-scroll, gestion + d'erreur, déconnexion auto sur 401. Build OK. + ## [0.22.0] — 2026-06-23 ### Added - Package **`web/`** : scaffold UI (React 19 + Vite 8 + Tailwind 4 + react-router 7, diff --git a/web/src/pages/Chat.tsx b/web/src/pages/Chat.tsx index fbd37e0..a9fb07d 100644 --- a/web/src/pages/Chat.tsx +++ b/web/src/pages/Chat.tsx @@ -1,4 +1,87 @@ -// Vue Chat — remplie en v0.23.0. -export function Chat() { - return
Chat (v0.23.0)…
; +import { useEffect, useRef, useState, type FormEvent } from "react"; +import { useAuth } from "../auth"; +import { api, ApiError } from "../api"; + +interface Msg { + role: "user" | "assistant"; + text: string; +} + +export function Chat() { + const { token, logout } = useAuth(); + const [messages, setMessages] = useState([]); + const [input, setInput] = useState(""); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + const bottom = useRef(null); + + useEffect(() => { + bottom.current?.scrollIntoView({ behavior: "smooth" }); + }, [messages, busy]); + + const send = async (e: FormEvent): Promise => { + e.preventDefault(); + const text = input.trim(); + if (!text || busy || !token) return; + setInput(""); + setError(null); + setMessages((m) => [...m, { role: "user", text }]); + setBusy(true); + try { + const { reply } = await api.chat(token, text); + setMessages((m) => [...m, { role: "assistant", text: reply }]); + } catch (err) { + if (err instanceof ApiError && err.status === 401) { + logout(); + return; + } + setError(err instanceof Error ? err.message : "Erreur"); + } finally { + setBusy(false); + } + }; + + return ( +
+
+ {messages.length === 0 && ( +

Pose une question à CHLOVA…

+ )} + {messages.map((m, i) => ( +
+
+ {m.text} +
+
+ ))} + {busy &&

CHLOVA réfléchit…

} + {error &&

{error}

} +
+
+ +
+ setInput(e.target.value)} + disabled={busy} + /> + +
+
+ ); }