import { listen } from "@tauri-apps/api/event"; import { FormEvent, useEffect, useMemo, useState } from "react"; import { Link, useParams } from "react-router-dom"; import { createLiveSession, listAgents, listLiveMessages, listProjectModules, listLiveSessions, setLiveSessionArchived, sendLiveMessage, } from "../../lib/api"; import { getErrorMessage } from "../../lib/errors"; import type { Agent, LiveMessage, LiveSession } from "../../lib/types"; import { backLinkClass, buttonClass, cardContentClass, inputClass, noticeClass, pageStackClass, pageTitleClass, pillClass, } from "../ui/primitives"; interface LiveEventPayload { project_id: string; session_id: string; message: LiveMessage; } interface LiveStreamChunkPayload { project_id: string; session_id: string; chunk: string; } interface LiveStreamStatusPayload { project_id: string; session_id: string; error?: string | null; } function StreamingAgentBubble({ content }: { content: string }) { return (
agent
{content || "Réponse en cours..."}
); } export default function ProjectLiveAgent() { const { projectId } = useParams<{ projectId: string }>(); const [agents, setAgents] = useState([]); const [sessions, setSessions] = useState([]); const [messages, setMessages] = useState([]); const [selectedSessionId, setSelectedSessionId] = useState(""); const [selectedAgentId, setSelectedAgentId] = useState(""); const [sessionTitle, setSessionTitle] = useState(""); const [draft, setDraft] = useState(""); const [sending, setSending] = useState(false); const [creatingSession, setCreatingSession] = useState(false); const [updatingSessionStatus, setUpdatingSessionStatus] = useState(false); const [moduleEnabled, setModuleEnabled] = useState(true); const [streamingAgentResponse, setStreamingAgentResponse] = useState(null); const [error, setError] = useState(null); const usableAgents = useMemo( () => agents.filter((agent) => agent.role === "analyst" || agent.role === "developer"), [agents] ); const selectedSession = useMemo( () => sessions.find((session) => session.id === selectedSessionId) ?? null, [sessions, selectedSessionId] ); const activeSessions = useMemo( () => sessions.filter((session) => session.status !== "archived"), [sessions] ); const archivedSessions = useMemo( () => sessions.filter((session) => session.status === "archived"), [sessions] ); async function refreshSessions(defaultSessionId?: string) { if (!projectId) return; const result = await listLiveSessions(projectId); setSessions(result); const targetSessionId = defaultSessionId ?? selectedSessionId; const firstSessionId = result[0]?.id; if (targetSessionId && result.some((session) => session.id === targetSessionId)) { setSelectedSessionId(targetSessionId); const sessionMessages = await listLiveMessages(targetSessionId); setMessages(sessionMessages); return; } if (firstSessionId) { setSelectedSessionId(firstSessionId); const sessionMessages = await listLiveMessages(firstSessionId); setMessages(sessionMessages); return; } setSelectedSessionId(""); setMessages([]); } useEffect(() => { async function load() { if (!projectId) return; setError(null); try { const [availableAgents, modules] = await Promise.all([ listAgents(), listProjectModules(projectId), ]); setAgents(availableAgents); setModuleEnabled( modules.find((mod) => mod.module_key === "ai_live_chat")?.enabled ?? true ); const firstAgentId = availableAgents[0]?.id ?? ""; setSelectedAgentId((current) => current || firstAgentId); await refreshSessions(); } catch (err: unknown) { setError(getErrorMessage(err)); } } void load(); }, [projectId]); useEffect(() => { if (!projectId) return; let cancelled = false; let stop: (() => void) | null = null; const setup = async () => { try { const [unlistenMessage, unlistenStarted, unlistenChunk, unlistenFinished, unlistenError] = await Promise.all([ listen("live-agent-message", (event) => { const payload = event.payload; if (payload.project_id !== projectId) return; if (payload.session_id !== selectedSessionId) return; setMessages((prev) => { const existingIndex = prev.findIndex((msg) => msg.id === payload.message.id); if (existingIndex === -1) { return [...prev, payload.message]; } const next = [...prev]; next[existingIndex] = payload.message; return next; }); if (payload.message.sender === "agent" && payload.message.content.trim() !== "") { setStreamingAgentResponse(null); } }), listen("live-agent-stream-started", (event) => { const payload = event.payload; if (payload.project_id !== projectId) return; if (payload.session_id !== selectedSessionId) return; setStreamingAgentResponse(""); }), listen("live-agent-stream-chunk", (event) => { const payload = event.payload; if (payload.project_id !== projectId) return; if (payload.session_id !== selectedSessionId) return; setStreamingAgentResponse((prev) => `${prev ?? ""}${payload.chunk}`); }), listen("live-agent-stream-finished", (event) => { const payload = event.payload; if (payload.project_id !== projectId) return; if (payload.session_id !== selectedSessionId) return; setStreamingAgentResponse(null); }), listen("live-agent-stream-error", (event) => { const payload = event.payload; if (payload.project_id !== projectId) return; if (payload.session_id !== selectedSessionId) return; setStreamingAgentResponse(null); setMessages((prev) => prev.filter((msg) => !(msg.sender === "agent" && msg.content.trim() === "")) ); if (payload.error) { setError(payload.error); } }), ]); const cleanup = () => { unlistenMessage(); unlistenStarted(); unlistenChunk(); unlistenFinished(); unlistenError(); }; if (cancelled) { cleanup(); return; } stop = cleanup; } catch (err: unknown) { if (!cancelled) { setError(getErrorMessage(err)); } } }; void setup(); return () => { cancelled = true; if (stop) { stop(); } }; }, [projectId, selectedSessionId]); async function handleCreateSession(event: FormEvent) { event.preventDefault(); if (!projectId || !selectedAgentId) return; if (!moduleEnabled) { setError("Le module Live chat agent est désactivé pour ce projet."); return; } setCreatingSession(true); setError(null); try { const created = await createLiveSession(projectId, selectedAgentId, sessionTitle); setSessionTitle(""); await refreshSessions(created.id); } catch (err: unknown) { setError(getErrorMessage(err)); } finally { setCreatingSession(false); } } async function handleSendMessage(event: FormEvent) { event.preventDefault(); if (!selectedSessionId || !draft.trim()) return; if (!moduleEnabled) { setError("Le module Live chat agent est désactivé pour ce projet."); return; } if (selectedSession?.status === "archived") { setError("Cette discussion live est archivée et ne peut plus recevoir de message."); return; } const content = draft.trim(); setDraft(""); setSending(true); setError(null); try { await sendLiveMessage(selectedSessionId, content); } catch (err: unknown) { setDraft(content); setStreamingAgentResponse(null); setError(getErrorMessage(err)); } finally { setSending(false); } } async function handleToggleArchive(archived: boolean) { if (!selectedSession) return; setUpdatingSessionStatus(true); setError(null); setStreamingAgentResponse(null); try { await setLiveSessionArchived(selectedSession.id, archived); await refreshSessions(selectedSession.id); } catch (err: unknown) { setError(getErrorMessage(err)); } finally { setUpdatingSessionStatus(false); } } async function handleSessionChange(sessionId: string) { setSelectedSessionId(sessionId); setStreamingAgentResponse(null); if (!sessionId) { setMessages([]); return; } try { const result = await listLiveMessages(sessionId); setMessages(result); } catch (err: unknown) { setError(getErrorMessage(err)); } } return (
{projectId && ( Back )}

Live agent

{error && (
{error}
)} {!moduleEnabled && (
Le module est désactivé. La lecture reste possible, mais la création de session et l'envoi de message sont bloqués.
)}

Nouvelle session

setSessionTitle(e.target.value)} placeholder="Titre de session (optionnel)" className={inputClass} />
Sessions
Actives
{activeSessions.map((session) => ( ))} {activeSessions.length === 0 && sessions.length > 0 && (
Aucune session active.
)}
Archivées
{archivedSessions.map((session) => ( ))} {archivedSessions.length === 0 && sessions.length > 0 && (
Aucune session archivée.
)}
{sessions.length === 0 &&
Aucune session.
}
Discussion
{selectedSession && (
{selectedSession.title} {selectedSession.status === "archived" && ( Archivée )}
)}
{selectedSession && ( )}
{selectedSession?.status === "archived" && (
Cette discussion est archivée. Elle reste consultable, mais l'envoi de nouveaux messages est désactivé.
)}
{messages .filter((msg) => !(msg.sender === "agent" && msg.content.trim() === "")) .map((msg) => (
{msg.sender}
{msg.content}
))} {streamingAgentResponse !== null && ( )} {messages.length === 0 && streamingAgentResponse === null && (
Pas encore de message.
)}
setDraft(e.target.value)} placeholder={ selectedSession?.status === "archived" ? "Discussion archivée en lecture seule" : "Ton message..." } disabled={!selectedSessionId || sending || selectedSession?.status === "archived"} className={`flex-1 ${inputClass}`} />
); }