mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-21 22:18:13 +02:00
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:
1 parent
3ad37ae2c7
commit
48cbcaafaf
9 files changed
+688
-50
No files matched your search
@@ -1,9 +1,16 @@
|
||||
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 {
|
||||
@@ -12,9 +19,21 @@ interface LokiState {
|
||||
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) => ({
|
||||
@@ -23,6 +42,12 @@ export const useStore = create<LokiState>((set, get) => ({
|
||||
selectedModel: "",
|
||||
loadingModels: false,
|
||||
|
||||
sessions: [],
|
||||
currentSessionId: null,
|
||||
messages: [],
|
||||
streaming: false,
|
||||
streamContent: "",
|
||||
|
||||
setSelectedModel: (name) => set({ selectedModel: name }),
|
||||
|
||||
refreshStatus: async () => {
|
||||
@@ -47,4 +72,83 @@ export const useStore = create<LokiState>((set, get) => ({
|
||||
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],
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
}));
|
||||
Reference in new issue
Block a user