// 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