feat: add live discussion archiving
This commit is contained in:
parent
4b051807ff
commit
b7d1087e35
5 changed files with 223 additions and 26 deletions
|
|
@ -112,6 +112,29 @@ pub fn list_live_messages(
|
||||||
Ok(messages)
|
Ok(messages)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn set_live_session_archived(
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
session_id: String,
|
||||||
|
archived: bool,
|
||||||
|
) -> Result<LiveSession, AppError> {
|
||||||
|
let db = state
|
||||||
|
.db
|
||||||
|
.lock()
|
||||||
|
.map_err(|e| AppError::from(format!("Database lock failed: {}", e)))?;
|
||||||
|
|
||||||
|
let session = LiveSession::get_by_id(&db, &session_id)?;
|
||||||
|
let enabled = ProjectModule::is_enabled(&db, &session.project_id, MODULE_AI_LIVE_CHAT)?;
|
||||||
|
if !enabled {
|
||||||
|
return Err(AppError::from(
|
||||||
|
"Le module de live chat est désactivé pour ce projet".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let updated = LiveSession::set_archived(&db, &session_id, archived)?;
|
||||||
|
Ok(updated)
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn send_live_message(
|
pub async fn send_live_message(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
|
|
@ -137,6 +160,12 @@ pub async fn send_live_message(
|
||||||
"Le module de live chat est désactivé pour ce projet".to_string(),
|
"Le module de live chat est désactivé pour ce projet".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
if session.status == "archived" {
|
||||||
|
return Err(AppError::from(
|
||||||
|
"Cette discussion live est archivée et ne peut plus recevoir de message."
|
||||||
|
.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
let project = Project::get_by_id(&db, &session.project_id)?;
|
let project = Project::get_by_id(&db, &session.project_id)?;
|
||||||
let agent = Agent::get_by_id(&db, &session.agent_id)?;
|
let agent = Agent::get_by_id(&db, &session.agent_id)?;
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,7 @@ pub fn run() {
|
||||||
commands::live_agent::create_live_session,
|
commands::live_agent::create_live_session,
|
||||||
commands::live_agent::list_live_sessions,
|
commands::live_agent::list_live_sessions,
|
||||||
commands::live_agent::list_live_messages,
|
commands::live_agent::list_live_messages,
|
||||||
|
commands::live_agent::set_live_session_archived,
|
||||||
commands::live_agent::send_live_message,
|
commands::live_agent::send_live_message,
|
||||||
commands::task::create_agent_task,
|
commands::task::create_agent_task,
|
||||||
commands::task::list_agent_tasks,
|
commands::task::list_agent_tasks,
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ impl LiveSession {
|
||||||
"SELECT id, project_id, agent_id, title, status, created_at, updated_at
|
"SELECT id, project_id, agent_id, title, status, created_at, updated_at
|
||||||
FROM project_live_sessions
|
FROM project_live_sessions
|
||||||
WHERE project_id = ?1
|
WHERE project_id = ?1
|
||||||
ORDER BY updated_at DESC",
|
ORDER BY CASE status WHEN 'active' THEN 0 ELSE 1 END, updated_at DESC",
|
||||||
)?;
|
)?;
|
||||||
let rows = stmt.query_map(params![project_id], session_from_row)?;
|
let rows = stmt.query_map(params![project_id], session_from_row)?;
|
||||||
rows.collect()
|
rows.collect()
|
||||||
|
|
@ -100,6 +100,22 @@ impl LiveSession {
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_archived(conn: &Connection, id: &str, archived: bool) -> Result<LiveSession> {
|
||||||
|
let now = chrono::Utc::now().to_rfc3339();
|
||||||
|
let status = if archived { "archived" } else { "active" };
|
||||||
|
|
||||||
|
let affected = conn.execute(
|
||||||
|
"UPDATE project_live_sessions SET status = ?1, updated_at = ?2 WHERE id = ?3",
|
||||||
|
params![status, now, id],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
if affected == 0 {
|
||||||
|
return Err(rusqlite::Error::QueryReturnedNoRows);
|
||||||
|
}
|
||||||
|
|
||||||
|
Self::get_by_id(conn, id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LiveMessage {
|
impl LiveMessage {
|
||||||
|
|
@ -242,4 +258,25 @@ mod tests {
|
||||||
LiveMessage::list_by_session(&conn, &session.id).expect("message list should work");
|
LiveMessage::list_by_session(&conn, &session.id).expect("message list should work");
|
||||||
assert!(messages_after_delete.is_empty());
|
assert!(messages_after_delete.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_archive_session_updates_status_and_sort_order() {
|
||||||
|
let (conn, project_id, agent_id) = setup();
|
||||||
|
let archived =
|
||||||
|
LiveSession::create(&conn, &project_id, &agent_id, "Archivee").expect("session create");
|
||||||
|
let active =
|
||||||
|
LiveSession::create(&conn, &project_id, &agent_id, "Active").expect("session create");
|
||||||
|
|
||||||
|
let archived_session =
|
||||||
|
LiveSession::set_archived(&conn, &archived.id, true).expect("archive should work");
|
||||||
|
assert_eq!(archived_session.status, "archived");
|
||||||
|
|
||||||
|
let sessions =
|
||||||
|
LiveSession::list_by_project(&conn, &project_id).expect("session list should work");
|
||||||
|
assert_eq!(sessions.len(), 2);
|
||||||
|
assert_eq!(sessions[0].id, active.id);
|
||||||
|
assert_eq!(sessions[0].status, "active");
|
||||||
|
assert_eq!(sessions[1].id, archived.id);
|
||||||
|
assert_eq!(sessions[1].status, "archived");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
listLiveMessages,
|
listLiveMessages,
|
||||||
listProjectModules,
|
listProjectModules,
|
||||||
listLiveSessions,
|
listLiveSessions,
|
||||||
|
setLiveSessionArchived,
|
||||||
sendLiveMessage,
|
sendLiveMessage,
|
||||||
} from "../../lib/api";
|
} from "../../lib/api";
|
||||||
import { getErrorMessage } from "../../lib/errors";
|
import { getErrorMessage } from "../../lib/errors";
|
||||||
|
|
@ -70,6 +71,7 @@ export default function ProjectLiveAgent() {
|
||||||
const [draft, setDraft] = useState("");
|
const [draft, setDraft] = useState("");
|
||||||
const [sending, setSending] = useState(false);
|
const [sending, setSending] = useState(false);
|
||||||
const [creatingSession, setCreatingSession] = useState(false);
|
const [creatingSession, setCreatingSession] = useState(false);
|
||||||
|
const [updatingSessionStatus, setUpdatingSessionStatus] = useState(false);
|
||||||
const [moduleEnabled, setModuleEnabled] = useState(true);
|
const [moduleEnabled, setModuleEnabled] = useState(true);
|
||||||
const [streamingAgentResponse, setStreamingAgentResponse] = useState<string | null>(null);
|
const [streamingAgentResponse, setStreamingAgentResponse] = useState<string | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
@ -78,6 +80,18 @@ export default function ProjectLiveAgent() {
|
||||||
() => agents.filter((agent) => agent.role === "analyst" || agent.role === "developer"),
|
() => agents.filter((agent) => agent.role === "analyst" || agent.role === "developer"),
|
||||||
[agents]
|
[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) {
|
async function refreshSessions(defaultSessionId?: string) {
|
||||||
if (!projectId) return;
|
if (!projectId) return;
|
||||||
|
|
@ -235,6 +249,10 @@ export default function ProjectLiveAgent() {
|
||||||
setError("Le module Live chat agent est désactivé pour ce projet.");
|
setError("Le module Live chat agent est désactivé pour ce projet.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (selectedSession?.status === "archived") {
|
||||||
|
setError("Cette discussion live est archivée et ne peut plus recevoir de message.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const content = draft.trim();
|
const content = draft.trim();
|
||||||
setDraft("");
|
setDraft("");
|
||||||
|
|
@ -252,6 +270,22 @@ export default function ProjectLiveAgent() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
async function handleSessionChange(sessionId: string) {
|
||||||
setSelectedSessionId(sessionId);
|
setSelectedSessionId(sessionId);
|
||||||
setStreamingAgentResponse(null);
|
setStreamingAgentResponse(null);
|
||||||
|
|
@ -319,8 +353,13 @@ export default function ProjectLiveAgent() {
|
||||||
<div className="mb-2 text-xs font-semibold uppercase tracking-wide text-gray-500">
|
<div className="mb-2 text-xs font-semibold uppercase tracking-wide text-gray-500">
|
||||||
Sessions
|
Sessions
|
||||||
</div>
|
</div>
|
||||||
|
<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">
|
<div className="space-y-2">
|
||||||
{sessions.map((session) => (
|
{activeSessions.map((session) => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
key={session.id}
|
key={session.id}
|
||||||
|
|
@ -337,15 +376,89 @@ export default function ProjectLiveAgent() {
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
{activeSessions.length === 0 && sessions.length > 0 && (
|
||||||
{sessions.length === 0 && (
|
<div className="text-xs text-gray-400">Aucune session active.</div>
|
||||||
<div className="text-xs text-gray-400">Aucune session.</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div className="rounded-lg border border-gray-200 bg-white p-4">
|
<div className="rounded-lg border border-gray-200 bg-white p-4">
|
||||||
<div className="mb-3 text-sm font-semibold text-gray-800">Discussion</div>
|
<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 && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
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"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{updatingSessionStatus
|
||||||
|
? "Mise à jour..."
|
||||||
|
: selectedSession.status === "archived"
|
||||||
|
? "Restaurer"
|
||||||
|
: "Archiver"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{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>
|
||||||
|
)}
|
||||||
<div className="max-h-[360px] space-y-2 overflow-y-auto rounded border border-gray-100 bg-gray-50 p-3">
|
<div className="max-h-[360px] space-y-2 overflow-y-auto rounded border border-gray-100 bg-gray-50 p-3">
|
||||||
{messages
|
{messages
|
||||||
.filter((msg) => !(msg.sender === "agent" && msg.content.trim() === ""))
|
.filter((msg) => !(msg.sender === "agent" && msg.content.trim() === ""))
|
||||||
|
|
@ -379,13 +492,23 @@ export default function ProjectLiveAgent() {
|
||||||
type="text"
|
type="text"
|
||||||
value={draft}
|
value={draft}
|
||||||
onChange={(e) => setDraft(e.target.value)}
|
onChange={(e) => setDraft(e.target.value)}
|
||||||
placeholder="Ton message..."
|
placeholder={
|
||||||
disabled={!selectedSessionId || sending}
|
selectedSession?.status === "archived"
|
||||||
|
? "Discussion archivée en lecture seule"
|
||||||
|
: "Ton message..."
|
||||||
|
}
|
||||||
|
disabled={!selectedSessionId || sending || selectedSession?.status === "archived"}
|
||||||
className="flex-1 rounded border border-gray-300 px-3 py-2 text-sm"
|
className="flex-1 rounded border border-gray-300 px-3 py-2 text-sm"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!selectedSessionId || sending || !draft.trim() || !moduleEnabled}
|
disabled={
|
||||||
|
!selectedSessionId ||
|
||||||
|
sending ||
|
||||||
|
!draft.trim() ||
|
||||||
|
!moduleEnabled ||
|
||||||
|
selectedSession?.status === "archived"
|
||||||
|
}
|
||||||
className="rounded bg-gray-900 px-4 py-2 text-sm text-white hover:bg-black disabled:opacity-50"
|
className="rounded bg-gray-900 px-4 py-2 text-sm text-white hover:bg-black disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{sending ? "Envoi..." : "Envoyer"}
|
{sending ? "Envoi..." : "Envoyer"}
|
||||||
|
|
|
||||||
|
|
@ -239,6 +239,13 @@ export async function listLiveMessages(sessionId: string): Promise<LiveMessage[]
|
||||||
return invoke("list_live_messages", { sessionId });
|
return invoke("list_live_messages", { sessionId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function setLiveSessionArchived(
|
||||||
|
sessionId: string,
|
||||||
|
archived: boolean
|
||||||
|
): Promise<LiveSession> {
|
||||||
|
return invoke("set_live_session_archived", { sessionId, archived });
|
||||||
|
}
|
||||||
|
|
||||||
export async function sendLiveMessage(
|
export async function sendLiveMessage(
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
message: string
|
message: string
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue