feat: UI vue Chat (v0.23.0)

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.

Palier de risque : reversible (front).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Kantin-Petit
2026-06-23 02:21:08 +02:00
parent 9de0132676
commit aee86b811e
2 changed files with 92 additions and 3 deletions
+6
View File
@@ -6,6 +6,12 @@ incompatibles. Chaque ligne renvoie à un commit dédié (un artefact = un commi
## [Unreleased] ## [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 ## [0.22.0] — 2026-06-23
### Added ### Added
- Package **`web/`** : scaffold UI (React 19 + Vite 8 + Tailwind 4 + react-router 7, - Package **`web/`** : scaffold UI (React 19 + Vite 8 + Tailwind 4 + react-router 7,
+86 -3
View File
@@ -1,4 +1,87 @@
// Vue Chat — remplie en v0.23.0. import { useEffect, useRef, useState, type FormEvent } from "react";
export function Chat() { import { useAuth } from "../auth";
return <div className="p-6 text-muted">Chat (v0.23.0)</div>; import { api, ApiError } from "../api";
interface Msg {
role: "user" | "assistant";
text: string;
}
export function Chat() {
const { token, logout } = useAuth();
const [messages, setMessages] = useState<Msg[]>([]);
const [input, setInput] = useState("");
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const bottom = useRef<HTMLDivElement>(null);
useEffect(() => {
bottom.current?.scrollIntoView({ behavior: "smooth" });
}, [messages, busy]);
const send = async (e: FormEvent): Promise<void> => {
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 (
<div className="flex h-full flex-col">
<div className="flex-1 overflow-y-auto px-4 py-4 space-y-3">
{messages.length === 0 && (
<p className="text-muted text-sm">Pose une question à CHLOVA</p>
)}
{messages.map((m, i) => (
<div key={i} className={m.role === "user" ? "flex justify-end" : "flex justify-start"}>
<div
className={
"max-w-[80%] whitespace-pre-wrap rounded-lg px-3 py-2 text-sm " +
(m.role === "user"
? "bg-surface-2 border border-accent/40"
: "bg-surface border border-border font-mono")
}
>
{m.text}
</div>
</div>
))}
{busy && <p className="text-muted text-sm animate-pulse">CHLOVA réfléchit</p>}
{error && <p role="alert" className="text-danger text-sm">{error}</p>}
<div ref={bottom} />
</div>
<form onSubmit={send} className="flex gap-2 border-t border-border bg-surface p-3">
<input
className="flex-1 rounded-md bg-surface-2 border border-border px-3 py-2 text-fg placeholder:text-muted ring-accent"
placeholder="Message…"
value={input}
onChange={(e) => setInput(e.target.value)}
disabled={busy}
/>
<button
type="submit"
disabled={busy || !input.trim()}
className="rounded-md bg-accent px-4 py-2 font-medium text-bg disabled:opacity-50 ring-accent cursor-pointer"
>
Envoyer
</button>
</form>
</div>
);
} }