Files
Loki/frontend/src/store/useStore.ts
T
MichaelandClaude Opus 4.8 48cbcaafaf 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
2026-06-29 18:46:51 +00:00

155 lines
4.0 KiB
TypeScript

import { create } from "zustand";
import {
createSession,
deleteSession,
getModels,
getSession,
getStatus,
listSessions,
streamChat,
type Message,
type OllamaModel,
type OllamaStatus,
type Session,
} from "../api/client";
interface LokiState {
status: OllamaStatus | null;
models: OllamaModel[];
selectedModel: string;
loadingModels: boolean;
sessions: Session[];
currentSessionId: string | null;
messages: Message[];
streaming: boolean;
streamContent: string; // réponse de l'agent en cours de frappe
setSelectedModel: (name: string) => void;
refreshStatus: () => Promise<void>;
refreshModels: () => Promise<void>;
refreshSessions: () => Promise<void>;
newSession: () => Promise<void>;
openSession: (id: string) => Promise<void>;
removeSession: (id: string) => Promise<void>;
sendMessage: (content: string) => Promise<void>;
}
export const useStore = create<LokiState>((set, get) => ({
status: null,
models: [],
selectedModel: "",
loadingModels: false,
sessions: [],
currentSessionId: null,
messages: [],
streaming: false,
streamContent: "",
setSelectedModel: (name) => set({ selectedModel: name }),
refreshStatus: async () => {
try {
const status = await getStatus();
set({ status });
if (!get().selectedModel && status.default_model) {
set({ selectedModel: status.default_model });
}
} catch {
set({ status: { connected: false, host: "", default_model: "" } });
}
},
refreshModels: async () => {
set({ loadingModels: true });
try {
const { models, default: def } = await getModels();
set({ models });
if (!get().selectedModel && def) set({ selectedModel: def });
} finally {
set({ loadingModels: false });
}
},
refreshSessions: async () => {
const sessions = await listSessions();
set({ sessions });
},
newSession: async () => {
const s = await createSession(get().selectedModel || undefined);
set({ currentSessionId: s.id, messages: [], streamContent: "" });
await get().refreshSessions();
},
openSession: async (id) => {
const { messages } = await getSession(id);
set({ currentSessionId: id, messages, streamContent: "" });
},
removeSession: async (id) => {
await deleteSession(id);
if (get().currentSessionId === id) {
set({ currentSessionId: null, messages: [] });
}
await get().refreshSessions();
},
sendMessage: async (content) => {
if (get().streaming) return;
content = content.trim();
if (!content) return;
// Crée une session à la volée si aucune n'est ouverte.
let sid = get().currentSessionId;
if (!sid) {
const s = await createSession(get().selectedModel || undefined);
sid = s.id;
set({ currentSessionId: s.id });
}
// Affichage optimiste du message utilisateur.
const userMsg: Message = {
id: `tmp-${Date.now()}`,
session_id: sid,
role: "user",
content,
created_at: Date.now() / 1000,
};
set({
messages: [...get().messages, userMsg],
streaming: true,
streamContent: "",
});
await streamChat(
{ session_id: sid, content, model: get().selectedModel || undefined },
{
onToken: (t) => set({ streamContent: get().streamContent + t }),
onDone: async () => {
set({ streaming: false, streamContent: "" });
// Recharge depuis la base pour récupérer les ids/horodatages réels.
if (get().currentSessionId === sid) await get().openSession(sid!);
await get().refreshSessions();
},
onError: (msg) => {
const errMsg: Message = {
id: `err-${Date.now()}`,
session_id: sid!,
role: "assistant",
content: `⚠️ Erreur : ${msg}`,
created_at: Date.now() / 1000,
};
set({
streaming: false,
streamContent: "",
messages: [...get().messages, errMsg],
});
},
}
);
},
}));