Phase 3 : chat streaming SSE et persistance des sessions

Backend :
- db.py : SQLite (sessions + messages), schéma et CRUD
- routes/sessions.py : créer/lister/ouvrir/renommer/supprimer une session
- routes/chat.py : POST /api/chat, relais token par token depuis Ollama (SSE),
  persistance des messages, titrage auto de la session au 1er message
- main.py : lifespan -> init_db, montage des nouvelles routes

Frontend :
- api/client.ts : APIs sessions + streamChat (parsing SSE event/data)
- store : sessions, messages, état de streaming, envoi optimiste
- ChatPanel : fil de conversation réel, bulles user/agent, curseur de frappe
- LeftPanel : historique réel (ouvrir/supprimer, horodatage relatif)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVay7z3y7q2gEe54ByAE6N
This commit is contained in:
MichaelandClaude Opus 4.8 committed 2026-06-29 18:46:51 +00:00
1 parent 3ad37ae2c7
commit 48cbcaafaf
9 files changed
+688 -50

No files matched your search

+93
View File
@@ -29,6 +29,99 @@ export async function getModels(): Promise<{
return res.json();
}
export interface Session {
id: string;
title: string;
model?: string;
created_at: number;
updated_at: number;
message_count?: number;
}
export interface Message {
id: string;
session_id: string;
role: "user" | "assistant";
content: string;
model?: string;
created_at: number;
}
export async function listSessions(): Promise<Session[]> {
const res = await fetch("/api/sessions");
return (await res.json()).sessions;
}
export async function createSession(model?: string): Promise<Session> {
const res = await fetch("/api/sessions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: "Nouvelle session", model }),
});
return res.json();
}
export async function getSession(
id: string
): Promise<{ session: Session; messages: Message[] }> {
const res = await fetch(`/api/sessions/${id}`);
return res.json();
}
export async function deleteSession(id: string): Promise<void> {
await fetch(`/api/sessions/${id}`, { method: "DELETE" });
}
/** Envoie un message et streame la réponse de l'agent via SSE. */
export async function streamChat(
body: { session_id: string; content: string; model?: string },
handlers: {
onToken: (t: string) => void;
onDone: (full: string) => void;
onError: (msg: string) => void;
}
): Promise<void> {
const res = await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!res.body) {
handlers.onError("pas de flux de réponse");
return;
}
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const events = buffer.split("\n\n");
buffer = events.pop() ?? "";
for (const block of events) {
let event = "message";
let data = "";
for (const line of block.split("\n")) {
if (line.startsWith("event: ")) event = line.slice(7).trim();
else if (line.startsWith("data: ")) data += line.slice(6);
}
if (!data) continue;
try {
const payload = JSON.parse(data);
if (event === "token") handlers.onToken(payload.content);
else if (event === "done") handlers.onDone(payload.content);
else if (event === "error") handlers.onError(payload.message);
} catch {
/* bloc partiel */
}
}
}
}
/** Télécharge un modèle en streamant la progression via SSE. */
export async function pullModel(
name: string,