/** * AssistantIA.jsx — Page dédiée de l'assistant IA (menu de gauche, sous * Fiscalité) : le même chat que la bulle flottante (components/AiAssistant.jsx), * en grand, avec la liste des conversations passées dans une colonne latérale * (voir components/ai/aiChatShared.jsx pour la logique et le rendu partagés). * * La conversation affichée est pilotée par le paramètre d'URL ?conversation=, * pour qu'un lien direct (ex. le bouton « Agrandir » du widget flottant) * l'ouvre directement, et que le bouton retour du navigateur fonctionne. */ import { useCallback, useEffect, useState } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { api } from '../api.js'; import ConfirmModal from '../components/ConfirmModal.jsx'; import { IconSparkle, IconNewChat, IconTrash, MessageThread, Composer, useAiChat, relativeDate, } from '../components/ai/aiChatShared.jsx'; export default function AssistantIA() { const [enabled, setEnabled] = useState(null); // null = pas encore su const [conversations, setConversations] = useState([]); const [loadingList, setLoadingList] = useState(true); const [input, setInput] = useState(''); const [confirmDeleteId, setConfirmDeleteId] = useState(null); const [searchParams, setSearchParams] = useSearchParams(); const navigate = useNavigate(); const syncUrl = useCallback((id) => { setSearchParams(id ? { conversation: String(id) } : {}, { replace: true }); }, [setSearchParams]); const loadConversations = useCallback(async () => { try { const r = await api.get('/ai/conversations'); setConversations(r.conversations || []); return r.conversations || []; } catch { setConversations([]); return []; } finally { setLoadingList(false); } }, []); const chat = useAiChat({ onConversationChange: syncUrl, onTurnComplete: loadConversations, }); const { conversationId, messages, sending, loadingHistory, selectConversation, startNewConversation, sendMessage, regenerateMessage } = chat; /* ── Statut d'accès ─────────────────────────────────────────────────── */ useEffect(() => { let cancelled = false; api.get('/ai/status').then((r) => { if (!cancelled) setEnabled(!!r.enabled); }).catch(() => { if (!cancelled) setEnabled(false); }); return () => { cancelled = true; }; }, []); /* ── Chargement initial : conversation demandée dans l'URL, sinon la plus * récente de la liste (reprendre là où on s'était arrêté). ────────────── */ useEffect(() => { if (enabled !== true) return; let cancelled = false; (async () => { const list = await loadConversations(); if (cancelled) return; const fromUrl = searchParams.get('conversation'); if (fromUrl) { selectConversation(Number(fromUrl)); } else if (list.length > 0) { selectConversation(list[0].id); } })(); return () => { cancelled = true; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [enabled]); const handleSend = useCallback(() => { const text = input; setInput(''); sendMessage(text); }, [input, sendMessage]); const handleNewConversation = useCallback(() => { if (sending) return; startNewConversation(); }, [sending, startNewConversation]); const handleSelect = useCallback((id) => { if (sending || id === conversationId) return; selectConversation(id); }, [sending, conversationId, selectConversation]); const handleDelete = useCallback(async () => { const id = confirmDeleteId; if (id == null) return; try { await api.del(`/ai/conversations/${id}`); if (id === conversationId) startNewConversation(); await loadConversations(); } catch { /* best-effort */ } finally { setConfirmDeleteId(null); } }, [confirmDeleteId, conversationId, startNewConversation, loadConversations]); if (enabled === false) { return (

L'assistant IA n'est pas activé pour votre compte.

Contactez un administrateur si vous pensez qu'il s'agit d'une erreur.

); } const currentTitle = conversations.find((c) => c.id === conversationId)?.titre || 'Nouvelle conversation'; return (
{currentTitle}
{loadingHistory ? (
Chargement de la conversation…
) : ( )}
setConfirmDeleteId(null)} />
); }