// AAP — Issue Tracker (drawer + form + flag button)
const { useState: useState_i, useEffect: useEffect_i, useRef: useRef_i } = React;
const Icon_i = window.AAP_Icon;
const SEVERITIES = [
{ id: 'critical', label: 'Critique', desc: 'Bloquant / erreur factuelle grave' },
{ id: 'warning', label: 'Modéré', desc: 'Imprécision, ton inadapté' },
{ id: 'minor', label: 'Mineur', desc: 'Suggestion d\'amélioration' },
];
const CATEGORIES = [
'Données incorrectes',
'Hallucination',
'Ton / formulation',
'Calcul erroné',
'Outil mal utilisé',
'Compliance / RGPD',
'UX / lisibilité',
'Autre',
];
const STORAGE_KEY = 'aap_issues_v1';
function loadIssues() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) return JSON.parse(raw);
} catch (e) {}
// Seed with one demo issue so the panel isn't empty on first load
return [
{
id: 'demo-1',
title: 'Plafond RC Pro mal indiqué pour 80 sal.',
desc: 'AAP a annoncé 8 M€ alors que la grille actuarielle plafonne à 6 M€ pour ce profil. Vérifier la table v4.2.',
quote: 'Notre avantage clé : cyber natif et plafond supérieur à prix égal…',
severity: 'warning',
category: 'Données incorrectes',
createdAt: Date.now() - 1000 * 60 * 47,
resolved: false,
},
];
}
function saveIssues(issues) {
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(issues)); } catch (e) {}
}
function relativeTime(ts) {
const diff = (Date.now() - ts) / 1000;
if (diff < 60) return 'à l\'instant';
if (diff < 3600) return `il y a ${Math.floor(diff/60)} min`;
if (diff < 86400) return `il y a ${Math.floor(diff/3600)} h`;
return `il y a ${Math.floor(diff/86400)} j`;
}
// ============ Issue Form ============
function IssueForm({ initial, onSave, onCancel }) {
const [title, setTitle] = useState_i(initial?.title || '');
const [desc, setDesc] = useState_i(initial?.desc || '');
const [quote, setQuote] = useState_i(initial?.quote || '');
const [severity, setSeverity] = useState_i(initial?.severity || 'warning');
const [category, setCategory] = useState_i(initial?.category || CATEGORIES[0]);
const canSave = title.trim().length > 0;
const submit = () => {
if (!canSave) return;
onSave({
...initial,
title: title.trim(),
desc: desc.trim(),
quote: quote.trim(),
severity,
category,
createdAt: initial?.createdAt || Date.now(),
updatedAt: initial ? Date.now() : undefined,
});
};
return (
{initial ? 'Modifier l\'anomalie' : 'Nouvelle anomalie'}
{SEVERITIES.map(s => (
))}
setTitle(e.target.value)}
placeholder="Ex : AAP cite un mauvais plafond RC Pro"
autoFocus
/>
);
}
// ============ Issue Card ============
function IssueCard({ issue, onEdit, onDelete }) {
const sev = SEVERITIES.find(s => s.id === issue.severity) || SEVERITIES[1];
return (
{sev.label}
{issue.category}
{relativeTime(issue.createdAt)}
{issue.title}
{issue.desc &&
{issue.desc}
}
{issue.quote &&
« {issue.quote} »
}
);
}
// ============ Issues Drawer ============
function IssuesDrawer({ open, onClose, issues, setIssues, draftFromMessage, clearDraft,
sessionUuid, testerName, testerEmail }) {
const [editing, setEditing] = useState_i(null); // issue or { _new: true, ...prefill }
useEffect_i(() => {
if (draftFromMessage && open) {
setEditing({ _new: true, quote: draftFromMessage, severity: 'warning', category: CATEGORIES[0] });
}
}, [draftFromMessage, open]);
const counts = {
total: issues.length,
critical: issues.filter(i => i.severity === 'critical').length,
warning: issues.filter(i => i.severity === 'warning').length,
minor: issues.filter(i => i.severity === 'minor').length,
};
// Sync issue vers le tracking server (fire-and-forget)
const trackIssue = (issue) => {
fetch('/track', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
event: 'issue',
session_uuid: sessionUuid || '',
tester_name: testerName || 'Jeremy',
tester_email: testerEmail || '',
issue_client_id: issue.id || '',
title: issue.title || '',
description: issue.desc || '',
quote_text: issue.quote || '',
severity: issue.severity || 'warning',
category: issue.category || 'Autre',
}),
}).catch(() => {});
};
const handleSave = (data) => {
if (data.id) {
setIssues(issues.map(i => i.id === data.id ? { ...i, ...data } : i));
// Mise à jour : pas de re-tracking pour les modifications
} else {
const nu = { ...data, id: 'i_' + Date.now() };
setIssues([nu, ...issues]);
// Nouvelle issue → tracker en DB
trackIssue(nu);
}
setEditing(null);
if (clearDraft) clearDraft();
};
const handleCancel = () => {
setEditing(null);
if (clearDraft) clearDraft();
};
const handleDelete = (id) => {
setIssues(issues.filter(i => i.id !== id));
};
return (
<>
>
);
}
// ============ Flag button (for AI messages) ============
function FlagButton({ onFlag, flagged }) {
return (
);
}
Object.assign(window, { IssuesDrawer, FlagButton, loadIssues, saveIssues });