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";
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..."}
Génération 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 stop: (() => void) | null = null;
void (async () => {
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);
}
}),
]);
stop = () => {
unlistenMessage();
unlistenStarted();
unlistenChunk();
unlistenFinished();
unlistenError();
};
})();
return () => {
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 (
Live agent
{projectId && (
Retour
)}
{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.
)}
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.
)}
);
}