2026-04-15 15:17:23 +00:00
|
|
|
import { listen } from "@tauri-apps/api/event";
|
|
|
|
|
import { FormEvent, useEffect, useMemo, useState } from "react";
|
2026-04-16 06:42:41 +00:00
|
|
|
import { Link, useParams } from "react-router-dom";
|
2026-04-15 15:17:23 +00:00
|
|
|
import {
|
|
|
|
|
createLiveSession,
|
|
|
|
|
listAgents,
|
|
|
|
|
listLiveMessages,
|
2026-04-15 15:33:20 +00:00
|
|
|
listProjectModules,
|
2026-04-15 15:17:23 +00:00
|
|
|
listLiveSessions,
|
2026-04-16 06:38:18 +00:00
|
|
|
setLiveSessionArchived,
|
2026-04-15 15:17:23 +00:00
|
|
|
sendLiveMessage,
|
|
|
|
|
} from "../../lib/api";
|
|
|
|
|
import { getErrorMessage } from "../../lib/errors";
|
|
|
|
|
import type { Agent, LiveMessage, LiveSession } from "../../lib/types";
|
2026-04-20 06:55:26 +00:00
|
|
|
import {
|
|
|
|
|
backLinkClass,
|
|
|
|
|
buttonClass,
|
|
|
|
|
cardContentClass,
|
|
|
|
|
inputClass,
|
|
|
|
|
noticeClass,
|
|
|
|
|
pageStackClass,
|
|
|
|
|
pageTitleClass,
|
|
|
|
|
pillClass,
|
|
|
|
|
} from "../ui/primitives";
|
2026-04-15 15:17:23 +00:00
|
|
|
|
|
|
|
|
interface LiveEventPayload {
|
|
|
|
|
project_id: string;
|
|
|
|
|
session_id: string;
|
|
|
|
|
message: LiveMessage;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 15:42:45 +00:00
|
|
|
interface LiveStreamChunkPayload {
|
|
|
|
|
project_id: string;
|
|
|
|
|
session_id: string;
|
|
|
|
|
chunk: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface LiveStreamStatusPayload {
|
|
|
|
|
project_id: string;
|
|
|
|
|
session_id: string;
|
|
|
|
|
error?: string | null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-16 06:30:41 +00:00
|
|
|
function StreamingAgentBubble({ content }: { content: string }) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="rounded bg-blue-100 px-3 py-2 text-sm text-blue-900">
|
|
|
|
|
<div className="mb-1 text-[11px] font-semibold uppercase tracking-wide opacity-70">
|
|
|
|
|
agent
|
|
|
|
|
</div>
|
|
|
|
|
<div className="whitespace-pre-wrap">{content || "Réponse en cours..."}</div>
|
|
|
|
|
<div
|
|
|
|
|
role="status"
|
|
|
|
|
aria-live="polite"
|
|
|
|
|
className="mt-3 flex items-center gap-2 text-[11px] font-semibold uppercase tracking-wide opacity-70"
|
|
|
|
|
>
|
|
|
|
|
<span aria-hidden="true" className="flex items-center gap-1">
|
|
|
|
|
<span
|
|
|
|
|
className="h-1.5 w-1.5 animate-bounce rounded-full bg-current"
|
|
|
|
|
style={{ animationDelay: "-0.3s" }}
|
|
|
|
|
/>
|
|
|
|
|
<span
|
|
|
|
|
className="h-1.5 w-1.5 animate-bounce rounded-full bg-current"
|
|
|
|
|
style={{ animationDelay: "-0.15s" }}
|
|
|
|
|
/>
|
|
|
|
|
<span className="h-1.5 w-1.5 animate-bounce rounded-full bg-current" />
|
|
|
|
|
</span>
|
|
|
|
|
<span>Génération en cours</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 15:17:23 +00:00
|
|
|
export default function ProjectLiveAgent() {
|
|
|
|
|
const { projectId } = useParams<{ projectId: string }>();
|
|
|
|
|
const [agents, setAgents] = useState<Agent[]>([]);
|
|
|
|
|
const [sessions, setSessions] = useState<LiveSession[]>([]);
|
|
|
|
|
const [messages, setMessages] = useState<LiveMessage[]>([]);
|
|
|
|
|
const [selectedSessionId, setSelectedSessionId] = useState<string>("");
|
|
|
|
|
const [selectedAgentId, setSelectedAgentId] = useState<string>("");
|
|
|
|
|
const [sessionTitle, setSessionTitle] = useState("");
|
|
|
|
|
const [draft, setDraft] = useState("");
|
|
|
|
|
const [sending, setSending] = useState(false);
|
|
|
|
|
const [creatingSession, setCreatingSession] = useState(false);
|
2026-04-16 06:38:18 +00:00
|
|
|
const [updatingSessionStatus, setUpdatingSessionStatus] = useState(false);
|
2026-04-15 15:33:20 +00:00
|
|
|
const [moduleEnabled, setModuleEnabled] = useState(true);
|
2026-04-15 15:42:45 +00:00
|
|
|
const [streamingAgentResponse, setStreamingAgentResponse] = useState<string | null>(null);
|
2026-04-15 15:17:23 +00:00
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
|
|
|
|
|
|
const usableAgents = useMemo(
|
2026-04-22 07:06:00 +00:00
|
|
|
() =>
|
|
|
|
|
agents.filter(
|
|
|
|
|
(agent) =>
|
|
|
|
|
agent.role === "analyst" || agent.role === "developer" || agent.role === "reviewer"
|
|
|
|
|
),
|
2026-04-15 15:17:23 +00:00
|
|
|
[agents]
|
|
|
|
|
);
|
2026-04-16 06:38:18 +00:00
|
|
|
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]
|
|
|
|
|
);
|
2026-04-15 15:17:23 +00:00
|
|
|
|
|
|
|
|
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 {
|
2026-04-15 15:33:20 +00:00
|
|
|
const [availableAgents, modules] = await Promise.all([
|
|
|
|
|
listAgents(),
|
|
|
|
|
listProjectModules(projectId),
|
|
|
|
|
]);
|
2026-04-15 15:17:23 +00:00
|
|
|
setAgents(availableAgents);
|
2026-04-15 15:33:20 +00:00
|
|
|
setModuleEnabled(
|
|
|
|
|
modules.find((mod) => mod.module_key === "ai_live_chat")?.enabled ?? true
|
|
|
|
|
);
|
2026-04-15 15:17:23 +00:00
|
|
|
|
|
|
|
|
const firstAgentId = availableAgents[0]?.id ?? "";
|
|
|
|
|
setSelectedAgentId((current) => current || firstAgentId);
|
|
|
|
|
|
|
|
|
|
await refreshSessions();
|
|
|
|
|
} catch (err: unknown) {
|
|
|
|
|
setError(getErrorMessage(err));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void load();
|
|
|
|
|
}, [projectId]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!projectId) return;
|
|
|
|
|
|
2026-04-21 08:33:12 +00:00
|
|
|
let cancelled = false;
|
2026-04-15 15:17:23 +00:00
|
|
|
let stop: (() => void) | null = null;
|
|
|
|
|
|
2026-04-21 08:33:12 +00:00
|
|
|
const setup = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const [unlistenMessage, unlistenStarted, unlistenChunk, unlistenFinished, unlistenError] =
|
|
|
|
|
await Promise.all([
|
|
|
|
|
listen<LiveEventPayload>("live-agent-message", (event) => {
|
|
|
|
|
const payload = event.payload;
|
|
|
|
|
if (payload.project_id !== projectId) return;
|
|
|
|
|
if (payload.session_id !== selectedSessionId) return;
|
2026-04-15 15:42:45 +00:00
|
|
|
|
2026-04-21 08:33:12 +00:00
|
|
|
setMessages((prev) => {
|
|
|
|
|
const existingIndex = prev.findIndex((msg) => msg.id === payload.message.id);
|
|
|
|
|
if (existingIndex === -1) {
|
|
|
|
|
return [...prev, payload.message];
|
|
|
|
|
}
|
2026-04-15 15:42:45 +00:00
|
|
|
|
2026-04-21 08:33:12 +00:00
|
|
|
const next = [...prev];
|
|
|
|
|
next[existingIndex] = payload.message;
|
|
|
|
|
return next;
|
|
|
|
|
});
|
2026-04-15 15:42:45 +00:00
|
|
|
|
2026-04-21 08:33:12 +00:00
|
|
|
if (payload.message.sender === "agent" && payload.message.content.trim() !== "") {
|
|
|
|
|
setStreamingAgentResponse(null);
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
listen<LiveStreamStatusPayload>("live-agent-stream-started", (event) => {
|
|
|
|
|
const payload = event.payload;
|
|
|
|
|
if (payload.project_id !== projectId) return;
|
|
|
|
|
if (payload.session_id !== selectedSessionId) return;
|
|
|
|
|
setStreamingAgentResponse("");
|
|
|
|
|
}),
|
|
|
|
|
listen<LiveStreamChunkPayload>("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<LiveStreamStatusPayload>("live-agent-stream-finished", (event) => {
|
|
|
|
|
const payload = event.payload;
|
|
|
|
|
if (payload.project_id !== projectId) return;
|
|
|
|
|
if (payload.session_id !== selectedSessionId) return;
|
2026-04-15 15:42:45 +00:00
|
|
|
setStreamingAgentResponse(null);
|
2026-04-21 08:33:12 +00:00
|
|
|
}),
|
|
|
|
|
listen<LiveStreamStatusPayload>("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();
|
|
|
|
|
};
|
2026-04-15 15:42:45 +00:00
|
|
|
|
2026-04-21 08:33:12 +00:00
|
|
|
if (cancelled) {
|
|
|
|
|
cleanup();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stop = cleanup;
|
|
|
|
|
} catch (err: unknown) {
|
|
|
|
|
if (!cancelled) {
|
|
|
|
|
setError(getErrorMessage(err));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
void setup();
|
2026-04-15 15:17:23 +00:00
|
|
|
|
|
|
|
|
return () => {
|
2026-04-21 08:33:12 +00:00
|
|
|
cancelled = true;
|
|
|
|
|
if (stop) {
|
|
|
|
|
stop();
|
|
|
|
|
}
|
2026-04-15 15:17:23 +00:00
|
|
|
};
|
|
|
|
|
}, [projectId, selectedSessionId]);
|
|
|
|
|
|
|
|
|
|
async function handleCreateSession(event: FormEvent) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
if (!projectId || !selectedAgentId) return;
|
2026-04-15 15:33:20 +00:00
|
|
|
if (!moduleEnabled) {
|
|
|
|
|
setError("Le module Live chat agent est désactivé pour ce projet.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-04-15 15:17:23 +00:00
|
|
|
|
|
|
|
|
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;
|
2026-04-15 15:33:20 +00:00
|
|
|
if (!moduleEnabled) {
|
|
|
|
|
setError("Le module Live chat agent est désactivé pour ce projet.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-04-16 06:38:18 +00:00
|
|
|
if (selectedSession?.status === "archived") {
|
|
|
|
|
setError("Cette discussion live est archivée et ne peut plus recevoir de message.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-04-15 15:17:23 +00:00
|
|
|
|
|
|
|
|
const content = draft.trim();
|
|
|
|
|
setDraft("");
|
|
|
|
|
setSending(true);
|
|
|
|
|
setError(null);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await sendLiveMessage(selectedSessionId, content);
|
|
|
|
|
} catch (err: unknown) {
|
|
|
|
|
setDraft(content);
|
2026-04-15 15:42:45 +00:00
|
|
|
setStreamingAgentResponse(null);
|
2026-04-15 15:17:23 +00:00
|
|
|
setError(getErrorMessage(err));
|
|
|
|
|
} finally {
|
|
|
|
|
setSending(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-16 06:38:18 +00:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 15:17:23 +00:00
|
|
|
async function handleSessionChange(sessionId: string) {
|
|
|
|
|
setSelectedSessionId(sessionId);
|
2026-04-15 15:42:45 +00:00
|
|
|
setStreamingAgentResponse(null);
|
2026-04-15 15:17:23 +00:00
|
|
|
if (!sessionId) {
|
|
|
|
|
setMessages([]);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const result = await listLiveMessages(sessionId);
|
|
|
|
|
setMessages(result);
|
|
|
|
|
} catch (err: unknown) {
|
|
|
|
|
setError(getErrorMessage(err));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
2026-04-20 06:55:26 +00:00
|
|
|
<div className={pageStackClass}>
|
2026-04-20 06:30:00 +00:00
|
|
|
<div>
|
2026-04-16 06:42:41 +00:00
|
|
|
{projectId && (
|
2026-04-20 06:55:26 +00:00
|
|
|
<Link to={`/projects/${projectId}`} className={backLinkClass}>
|
2026-04-20 06:30:00 +00:00
|
|
|
Back
|
2026-04-16 06:42:41 +00:00
|
|
|
</Link>
|
|
|
|
|
)}
|
2026-04-20 06:55:26 +00:00
|
|
|
<h2 className={pageTitleClass}>Live agent</h2>
|
2026-04-16 06:42:41 +00:00
|
|
|
</div>
|
2026-04-15 15:17:23 +00:00
|
|
|
|
|
|
|
|
{error && (
|
2026-04-20 06:55:26 +00:00
|
|
|
<div className={noticeClass("error", true)}>
|
2026-04-15 15:17:23 +00:00
|
|
|
{error}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-04-15 15:33:20 +00:00
|
|
|
{!moduleEnabled && (
|
2026-04-20 06:55:26 +00:00
|
|
|
<div className={noticeClass("warning", true)}>
|
2026-04-15 15:33:20 +00:00
|
|
|
Le module est désactivé. La lecture reste possible, mais la création de session et l'envoi de message sont bloqués.
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-04-15 15:17:23 +00:00
|
|
|
|
2026-04-20 06:55:26 +00:00
|
|
|
<form onSubmit={handleCreateSession} className={cardContentClass}>
|
2026-04-15 15:17:23 +00:00
|
|
|
<h3 className="mb-3 text-sm font-semibold text-gray-800">Nouvelle session</h3>
|
|
|
|
|
<div className="grid gap-3 md:grid-cols-3">
|
|
|
|
|
<select
|
|
|
|
|
value={selectedAgentId}
|
|
|
|
|
onChange={(e) => setSelectedAgentId(e.target.value)}
|
2026-04-20 06:55:26 +00:00
|
|
|
className={inputClass}
|
2026-04-15 15:17:23 +00:00
|
|
|
>
|
|
|
|
|
{usableAgents.map((agent) => (
|
|
|
|
|
<option key={agent.id} value={agent.id}>
|
|
|
|
|
{agent.name} ({agent.tool})
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={sessionTitle}
|
|
|
|
|
onChange={(e) => setSessionTitle(e.target.value)}
|
|
|
|
|
placeholder="Titre de session (optionnel)"
|
2026-04-20 06:55:26 +00:00
|
|
|
className={inputClass}
|
2026-04-15 15:17:23 +00:00
|
|
|
/>
|
|
|
|
|
<button
|
|
|
|
|
type="submit"
|
2026-04-15 15:33:20 +00:00
|
|
|
disabled={creatingSession || !selectedAgentId || !moduleEnabled}
|
2026-04-20 06:55:26 +00:00
|
|
|
className={buttonClass({ variant: "primary" })}
|
2026-04-15 15:17:23 +00:00
|
|
|
>
|
|
|
|
|
{creatingSession ? "Création..." : "Créer la session"}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
|
|
|
|
|
|
<div className="grid gap-4 md:grid-cols-[260px,1fr]">
|
2026-04-20 06:55:26 +00:00
|
|
|
<div className={`${cardContentClass} p-3`}>
|
2026-04-15 15:17:23 +00:00
|
|
|
<div className="mb-2 text-xs font-semibold uppercase tracking-wide text-gray-500">
|
|
|
|
|
Sessions
|
|
|
|
|
</div>
|
2026-04-16 06:38:18 +00:00
|
|
|
<div className="space-y-4">
|
|
|
|
|
<div>
|
|
|
|
|
<div className="mb-2 text-[11px] font-semibold uppercase tracking-wide text-gray-400">
|
|
|
|
|
Actives
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
{activeSessions.map((session) => (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
key={session.id}
|
|
|
|
|
onClick={() => void handleSessionChange(session.id)}
|
2026-04-20 06:55:26 +00:00
|
|
|
className={`${pillClass(selectedSessionId === session.id)} w-full text-left`}
|
2026-04-16 06:38:18 +00:00
|
|
|
>
|
|
|
|
|
<div className="truncate">{session.title}</div>
|
|
|
|
|
<div className="mt-1 text-[11px] opacity-70">
|
|
|
|
|
{new Date(session.updated_at).toLocaleString()}
|
|
|
|
|
</div>
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
{activeSessions.length === 0 && sessions.length > 0 && (
|
|
|
|
|
<div className="text-xs text-gray-400">Aucune session active.</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<div className="mb-2 text-[11px] font-semibold uppercase tracking-wide text-gray-400">
|
|
|
|
|
Archivées
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
{archivedSessions.map((session) => (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
key={session.id}
|
|
|
|
|
onClick={() => void handleSessionChange(session.id)}
|
|
|
|
|
className={`w-full rounded px-3 py-2 text-left text-sm ${
|
|
|
|
|
selectedSessionId === session.id
|
|
|
|
|
? "bg-amber-100 text-amber-900"
|
|
|
|
|
: "bg-gray-100 text-gray-700 hover:bg-gray-200"
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-center justify-between gap-2">
|
|
|
|
|
<div className="truncate">{session.title}</div>
|
|
|
|
|
<span className="shrink-0 rounded-full bg-amber-200 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-amber-800">
|
|
|
|
|
Archivée
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="mt-1 text-[11px] opacity-70">
|
|
|
|
|
{new Date(session.updated_at).toLocaleString()}
|
|
|
|
|
</div>
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
{archivedSessions.length === 0 && sessions.length > 0 && (
|
|
|
|
|
<div className="text-xs text-gray-400">Aucune session archivée.</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{sessions.length === 0 && <div className="text-xs text-gray-400">Aucune session.</div>}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-04-20 06:55:26 +00:00
|
|
|
<div className={cardContentClass}>
|
2026-04-16 06:38:18 +00:00
|
|
|
<div className="mb-3 flex items-center justify-between gap-3">
|
|
|
|
|
<div>
|
|
|
|
|
<div className="text-sm font-semibold text-gray-800">Discussion</div>
|
|
|
|
|
{selectedSession && (
|
|
|
|
|
<div className="mt-1 text-xs text-gray-500">
|
|
|
|
|
{selectedSession.title}
|
|
|
|
|
{selectedSession.status === "archived" && (
|
|
|
|
|
<span className="ml-2 rounded-full bg-amber-100 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-amber-800">
|
|
|
|
|
Archivée
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
{selectedSession && (
|
2026-04-15 15:17:23 +00:00
|
|
|
<button
|
|
|
|
|
type="button"
|
2026-04-16 06:38:18 +00:00
|
|
|
onClick={() => void handleToggleArchive(selectedSession.status !== "archived")}
|
|
|
|
|
disabled={updatingSessionStatus || sending || streamingAgentResponse !== null}
|
|
|
|
|
className={`rounded px-3 py-1.5 text-xs font-medium disabled:opacity-50 ${
|
|
|
|
|
selectedSession.status === "archived"
|
|
|
|
|
? "bg-emerald-100 text-emerald-700 hover:bg-emerald-200"
|
|
|
|
|
: "bg-amber-100 text-amber-800 hover:bg-amber-200"
|
2026-04-15 15:17:23 +00:00
|
|
|
}`}
|
|
|
|
|
>
|
2026-04-16 06:38:18 +00:00
|
|
|
{updatingSessionStatus
|
|
|
|
|
? "Mise à jour..."
|
|
|
|
|
: selectedSession.status === "archived"
|
|
|
|
|
? "Restaurer"
|
|
|
|
|
: "Archiver"}
|
2026-04-15 15:17:23 +00:00
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2026-04-16 06:38:18 +00:00
|
|
|
{selectedSession?.status === "archived" && (
|
|
|
|
|
<div className="mb-3 rounded border border-amber-200 bg-amber-50 p-2 text-sm text-amber-700">
|
|
|
|
|
Cette discussion est archivée. Elle reste consultable, mais l'envoi de nouveaux
|
|
|
|
|
messages est désactivé.
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-04-15 15:17:23 +00:00
|
|
|
<div className="max-h-[360px] space-y-2 overflow-y-auto rounded border border-gray-100 bg-gray-50 p-3">
|
2026-04-15 15:42:45 +00:00
|
|
|
{messages
|
|
|
|
|
.filter((msg) => !(msg.sender === "agent" && msg.content.trim() === ""))
|
|
|
|
|
.map((msg) => (
|
2026-04-15 15:17:23 +00:00
|
|
|
<div
|
|
|
|
|
key={msg.id}
|
|
|
|
|
className={`rounded px-3 py-2 text-sm ${
|
|
|
|
|
msg.sender === "agent"
|
|
|
|
|
? "bg-blue-100 text-blue-900"
|
|
|
|
|
: msg.sender === "system"
|
|
|
|
|
? "bg-amber-100 text-amber-900"
|
|
|
|
|
: "bg-gray-200 text-gray-800"
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
<div className="mb-1 text-[11px] font-semibold uppercase tracking-wide opacity-70">
|
|
|
|
|
{msg.sender}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="whitespace-pre-wrap">{msg.content}</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
2026-04-15 15:42:45 +00:00
|
|
|
{streamingAgentResponse !== null && (
|
2026-04-16 06:30:41 +00:00
|
|
|
<StreamingAgentBubble content={streamingAgentResponse} />
|
2026-04-15 15:42:45 +00:00
|
|
|
)}
|
|
|
|
|
{messages.length === 0 && streamingAgentResponse === null && (
|
2026-04-15 15:17:23 +00:00
|
|
|
<div className="text-sm text-gray-400">Pas encore de message.</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<form onSubmit={handleSendMessage} className="mt-3 flex gap-2">
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={draft}
|
|
|
|
|
onChange={(e) => setDraft(e.target.value)}
|
2026-04-16 06:38:18 +00:00
|
|
|
placeholder={
|
|
|
|
|
selectedSession?.status === "archived"
|
|
|
|
|
? "Discussion archivée en lecture seule"
|
|
|
|
|
: "Ton message..."
|
|
|
|
|
}
|
|
|
|
|
disabled={!selectedSessionId || sending || selectedSession?.status === "archived"}
|
2026-04-20 06:55:26 +00:00
|
|
|
className={`flex-1 ${inputClass}`}
|
2026-04-15 15:17:23 +00:00
|
|
|
/>
|
|
|
|
|
<button
|
|
|
|
|
type="submit"
|
2026-04-16 06:38:18 +00:00
|
|
|
disabled={
|
|
|
|
|
!selectedSessionId ||
|
|
|
|
|
sending ||
|
|
|
|
|
!draft.trim() ||
|
|
|
|
|
!moduleEnabled ||
|
|
|
|
|
selectedSession?.status === "archived"
|
|
|
|
|
}
|
2026-04-20 06:55:26 +00:00
|
|
|
className={buttonClass({ variant: "neutralDark" })}
|
2026-04-15 15:17:23 +00:00
|
|
|
>
|
|
|
|
|
{sending ? "Envoi..." : "Envoyer"}
|
|
|
|
|
</button>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|