// AAP — Main app (QA testure — branch feat/interface-testure-qa) const { useState, useRef, useEffect } = React; // ============================================================ // QA Tracking helpers // ============================================================ const QA_SESSION_UUID = crypto.randomUUID(); const QA_ACCESS_CODE = 'QA-aap-v8@2026'; // Lire les params URL ?tester=Jeremy&email=jeremy@... function getUrlParam(key) { return new URLSearchParams(window.location.search).get(key) || ''; } const QA_TESTER = { name: getUrlParam('tester') || localStorage.getItem('qa_tester_name') || 'Jeremy', email: getUrlParam('email') || localStorage.getItem('qa_tester_email') || 'jeremy.garcia@calipso-assurances.fr', }; if (getUrlParam('tester')) localStorage.setItem('qa_tester_name', QA_TESTER.name); if (getUrlParam('email')) localStorage.setItem('qa_tester_email', QA_TESTER.email); // Fire-and-forget — silencieux si le serveur QA est indisponible function track(event, data) { try { const body = JSON.stringify({ event, session_uuid: QA_SESSION_UUID, tester_name: QA_TESTER.name, tester_email: QA_TESTER.email, ...(data || {}), }); if (event === 'session_end') { // sendBeacon garantit l'envoi même si l'onglet se ferme navigator.sendBeacon('/track', new Blob([body], { type: 'application/json' })); } else { fetch('/track', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body }) .catch(() => {}); } } catch (_) {} } // ============================================================ // App principale // ============================================================ const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "accentColor": "#2D6BFF", "showParticles": true, "density": "comfortable" }/*EDITMODE-END*/; function App() { const [tweaks, setTweak] = window.useTweaks ? window.useTweaks(TWEAK_DEFAULTS) : [TWEAK_DEFAULTS, () => {}]; // ── Login gate ── const [unlocked, setUnlocked] = useState( localStorage.getItem('qa_unlocked') === 'true' ); // ── Chat state ── const [conv, setConv] = useState([]); // {role:'user'|'ai', text, status?, latency_ms?, score?} const [streaming, setStreaming] = useState(false); const [activeId, setActiveId] = useState(null); // session_id AAP actif const [conversations, setConversations] = useState([]); // sessions réelles depuis ia_aap_sessions const [issuesOpen, setIssuesOpen] = useState(false); const [issues, setIssues] = useState(() => window.loadIssues ? window.loadIssues() : []); const [draftQuote, setDraftQuote] = useState(null); const scrollRef = useRef(null); useEffect(() => { if (window.saveIssues) window.saveIssues(issues); }, [issues]); useEffect(() => { if (tweaks.accentColor) { document.documentElement.style.setProperty('--accent', tweaks.accentColor); } }, [tweaks.accentColor]); useEffect(() => { if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight; }); // Tracking session au déverrouillage useEffect(() => { if (!unlocked) return; track('session_start'); const onUnload = () => track('session_end'); window.addEventListener('beforeunload', onUnload); return () => window.removeEventListener('beforeunload', onUnload); }, [unlocked]); // Chargement de l'historique réel depuis ia_aap_sessions const refreshConversations = () => { fetch(`/conversations?email=${encodeURIComponent(QA_TESTER.email)}`) .then(r => r.ok ? r.json() : { conversations: [] }) .then(d => setConversations(d.conversations || [])) .catch(() => {}); }; useEffect(() => { if (!unlocked) return; refreshConversations(); }, [unlocked]); // ── Handlers ── const handleUnlock = () => { localStorage.setItem('qa_unlocked', 'true'); setUnlocked(true); }; // Appel réel à l'AAP via le proxy QA (tracking_server.py) const handleSend = async (text) => { if (streaming) return; setConv(c => [...c, { role: 'user', text }]); setStreaming(true); try { const res = await fetch('/ask', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ question: text, email: QA_TESTER.email, // suivi QA interne user_email: QA_TESTER.email, // champ attendu par l'AAP session_id: activeId || '', // sessionId AAP — vide = nouvelle session session_uuid: QA_SESSION_UUID, tester_name: QA_TESTER.name, qa_code: QA_ACCESS_CODE, }), }); if (!res.ok) { const err = await res.json().catch(() => ({ detail: `HTTP ${res.status}` })); throw new Error(err.detail || `Erreur ${res.status}`); } const data = await res.json(); // Mettre à jour le session_id AAP — pour continuer la même conversation if (data.session_id && !activeId) { setActiveId(data.session_id); } setConv(c => [...c, { role: 'ai', text: data.response || '(Aucune réponse)', status: data.status, latency_ms: data.latency_ms, score: data.watchdog_score, animate: true, }]); } catch (err) { setConv(c => [...c, { role: 'ai', text: `⚠️ Erreur lors de la connexion à l'AAP : ${err.message}`, status: 'FAILED', }]); } finally { setStreaming(false); // Rafraîchir la sidebar — l'AAP a pu créer une nouvelle session refreshConversations(); } }; // Charger une conversation existante depuis ia_chat_history const handleSelectConv = async (session_id) => { if (streaming) return; setActiveId(session_id); setStreaming(true); try { const res = await fetch(`/conversations/${session_id}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); const loaded = (data.messages || []).map(m => ({ role: m.role === 'human' ? 'user' : 'ai', text: m.content || '', })); setConv(loaded); } catch (e) { // Si la conversation ne charge pas, on reste sur l'état actuel } finally { setStreaming(false); } }; const handleNew = () => { setConv([]); setStreaming(false); setActiveId(null); }; const empty = conv.length === 0; const openWithDraft = (quote) => { setDraftQuote(quote || null); setIssuesOpen(true); }; const activeCount = issues.filter(i => !i.resolved).length; // ── Login gate ── if (!unlocked) { return ; } return (
AAP · Assistant Calipso {empty ? `Session QA — ${QA_TESTER.name}` : `${conv.filter(m => m.role === 'user').length} message(s)`}
Modèle v3.4
{empty ? ( ) : (
{conv.map((m, i) => { if (m.role === 'user') return ; return ( openWithDraft(quote)} /> ); })} {streaming && {}}/>}
)}
setStreaming(false)}/>
{window.IssuesDrawer && ( { setIssuesOpen(false); setDraftQuote(null); }} issues={issues} setIssues={setIssues} draftFromMessage={draftQuote} clearDraft={() => setDraftQuote(null)} sessionUuid={QA_SESSION_UUID} testerName={QA_TESTER.name} testerEmail={QA_TESTER.email} /> )} {window.TweaksPanel && ( setTweak('accentColor', v)}/> setTweak('showParticles', v)}/> )}
); } ReactDOM.createRoot(document.getElementById('root')).render();