// AAP — Components: Sidebar, Welcome, Messages, Composer
const { useState, useEffect, useRef, useCallback } = React;
const Icon = window.AAP_Icon;
// ============ Sidebar ============
// Helpers pour grouper les conversations par date
function _sidebarDateLabel(isoStr) {
if (!isoStr) return 'Plus ancien';
const d = new Date(isoStr);
const now = new Date();
const diffMs = now - d;
const diffDays = Math.floor(diffMs / 86400000);
if (diffDays === 0) return "Aujourd'hui";
if (diffDays === 1) return 'Hier';
if (diffDays <= 7) return '7 derniers jours';
const months = ['Janvier','Février','Mars','Avril','Mai','Juin',
'Juillet','Août','Septembre','Octobre','Novembre','Décembre'];
return d.getFullYear() === now.getFullYear()
? months[d.getMonth()]
: `${months[d.getMonth()]} ${d.getFullYear()}`;
}
function _truncate(str, n) {
if (!str) return '(sans titre)';
return str.length > n ? str.slice(0, n) + '…' : str;
}
function _formatHour(isoStr) {
if (!isoStr) return '';
const d = new Date(isoStr);
return d.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' });
}
function Sidebar({ activeId, onNew, onSelect, conversations = [], testerName = 'Jeremy' }) {
// Grouper les conversations réelles par date
const groups = {};
conversations.forEach(c => {
const label = _sidebarDateLabel(c.last_query_at);
(groups[label] = groups[label] || []).push(c);
});
// Ordre des groupes
const GROUP_ORDER = ["Aujourd'hui", 'Hier', '7 derniers jours'];
const sortedGroups = [
...GROUP_ORDER.filter(g => groups[g]),
...Object.keys(groups).filter(g => !GROUP_ORDER.includes(g)),
];
// Initiales pour l'avatar
const initials = testerName.split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase();
return (
);
}
// ============ Particles bg ============
function Particles() {
const dots = Array.from({ length: 14 }, (_, i) => ({
left: `${i * 7.3 % 100}%`,
size: 4 + i % 5 * 2,
delay: -(i * 0.9),
duration: 12 + i % 4 * 2
}));
return (
);
}
// ============ Welcome ============
function Welcome({ onPick }) {
const sugg = window.AAP_SUGGESTIONS;
const hour = new Date().getHours();
const greet = hour < 12 ? 'Bonjour' : hour < 18 ? 'Bon après-midi' : 'Bonsoir';
return (
{greet} Marc, je suis AAP
Votre assistant commercial Calipso. analyser votre pipeline, comparer nos garanties Decenal.
);
}
// ============ Tool Call ============
function ToolCall({ event, running }) {
const [expanded, setExpanded] = useState(true);
useEffect(() => {
if (!running) {
// collapse after a beat
const t = setTimeout(() => setExpanded(false), 1800);
return () => clearTimeout(t);
}
}, [running]);
return (
setExpanded((e) => !e)}>
{event.icon === 'crm' && }
{event.icon === 'calc' && }
{event.icon === 'search' && }
{running ? 'en cours…' : terminé }
{event.params && event.params.map((p, i) =>
{p.k}
{p.v}
)}
{!running && event.result &&
→ résultat
{event.result}
}
);
}
// ============ Cards ============
function QuoteCard({ data }) {
return (
{data.title}
Garantie Prime
{data.rows.map((r, i) =>
{r.label}
{r.value}
)}
{data.totalLabel}
{data.total}
);
}
function LeadsCard({ data }) {
return (
Top 5 · pipeline chaud
{data.map((l, i) =>
{l.name.split(' ').map((n) => n[0]).slice(0, 2).join('')}
{l.name}
{l.co} · {l.signal}
{l.score}
)}
);
}
function CompareCard({ data }) {
return (
Comparatif RC Pro · benchmarks
Critère
Calipso
AXA Pro
Generali
{data.map((r, i) =>
{r.critere}
{r.calipso}
{r.axa}
{r.gen}
)}
);
}
function EmailCard({ data }) {
return (
Brouillon · email
Objet : {data.subject}
{data.body}
);
}
// ============ Markdown-ish text ============
function MdText({ text }) {
// Inline **bold**, then split by newlines
const lines = text.split('\n').map((line, li) => {
const parts = [];
let last = 0;
const re = /\*\*(.+?)\*\*/g;
let m;
while ((m = re.exec(line)) !== null) {
if (m.index > last) parts.push(line.slice(last, m.index));
parts.push({m[1]} );
last = m.index + m[0].length;
}
if (last < line.length) parts.push(line.slice(last));
if (line.trim().startsWith('•')) {
return {parts.map((p, i) => {i === 0 ? String(p).replace('•', '▸ ') : p} )} ;
}
return {parts}
;
});
return <>{lines}>;
}
// ============ Streaming Message ============
function StreamingMessage({ scenario, onDone, onCancelRef, onFlag }) {
const [renderedEvents, setRenderedEvents] = useState([]);
const [textBuf, setTextBuf] = useState('');
const [streamingTextIndex, setStreamingTextIndex] = useState(null);
const [thinking, setThinking] = useState(null);
const [runningToolIdx, setRunningToolIdx] = useState(null);
const [finished, setFinished] = useState(false);
const [flagged, setFlagged] = useState(false);
const cancelledRef = useRef(false);
useEffect(() => {
if (onCancelRef) onCancelRef.current = () => {cancelledRef.current = true;};
let mounted = true;
async function run() {
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const events = scenario.events;
for (let i = 0; i < events.length; i++) {
if (cancelledRef.current || !mounted) break;
const ev = events[i];
if (ev.type === 'thinking') {
setThinking(ev.text);
await sleep(ev.ms);
setThinking(null);
} else if (ev.type === 'tool') {
// push tool, mark running
setRenderedEvents((p) => [...p, { ...ev, _key: i }]);
setRunningToolIdx(i);
await sleep(ev.durationMs);
if (cancelledRef.current) break;
setRunningToolIdx(null);
} else if (ev.type === 'text') {
// push placeholder text event, then stream tokens
const idx = i;
setRenderedEvents((p) => [...p, { ...ev, _key: i, _content: '' }]);
setStreamingTextIndex(idx);
const tokens = ev.content.split(/(\s+)/);
let acc = '';
for (let t = 0; t < tokens.length; t++) {
if (cancelledRef.current) break;
acc += tokens[t];
setRenderedEvents((p) => p.map((e) => e._key === idx ? { ...e, _content: acc } : e));
// randomize delay slightly
await sleep(18 + Math.random() * 35);
}
setStreamingTextIndex(null);
} else if (ev.type === 'card') {
setRenderedEvents((p) => [...p, { ...ev, _key: i }]);
await sleep(250);
}
}
if (mounted && !cancelledRef.current) { setFinished(true); onDone(); }
}
run();
return () => {mounted = false;};
// eslint-disable-next-line
}, []);
return (
AAP à l'instant
{renderedEvents.map((ev, i) => {
if (ev.type === 'tool') {
return
;
}
if (ev.type === 'text') {
const isStreaming = streamingTextIndex === ev._key;
return (
{isStreaming && }
);
}
if (ev.type === 'card') {
if (ev.kind === 'quote') return
;
if (ev.kind === 'leads') return
;
if (ev.kind === 'compare') return
;
if (ev.kind === 'email') return
;
}
return null;
})}
{thinking &&
}
);
}
// ============ User Message ============
function UserMessage({ text }) {
return (
);
}
// ============ Composer ============
function Composer({ onSend, streaming, onStop }) {
const [val, setVal] = useState('');
const [focused, setFocused] = useState(false);
const ref = useRef(null);
const autosize = useCallback(() => {
if (!ref.current) return;
ref.current.style.height = 'auto';
ref.current.style.height = Math.min(ref.current.scrollHeight, 200) + 'px';
}, []);
const submit = () => {
if (!val.trim() || streaming) return;
onSend(val.trim());
setVal('');
setTimeout(() => {if (ref.current) ref.current.style.height = 'auto';}, 0);
};
const handleKey = (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
submit();
}
};
return (
AAP peut faire des erreurs. Vérifiez les informations sensibles. Appuyez sur ↵ pour envoyer · ⇧↵ nouvelle ligne
);
}
// ============ Login Gate ============
function LoginGate({ onUnlock }) {
const [code, setCode] = useState('');
const [error, setError] = useState(false);
const [shake, setShake] = useState(false);
const handleSubmit = (e) => {
e.preventDefault();
if (code.trim() === 'QA-aap-v8@2026') {
setError(false);
onUnlock();
} else {
setError(true);
setShake(true);
setTimeout(() => setShake(false), 600);
setCode('');
}
};
return (
Interface QA · AAP
Banc de test — accès restreint
Interface de test interne — Calipso Assurances
);
}
// ============ AAPResponse — vraie réponse AAP avec animation typewriter ============
function AAPResponse({ text, status, latencyMs, score, streaming, onFlag, animate = false }) {
const [displayed, setDisplayed] = useState(() => animate ? '' : (text || ''));
const [typewriterDone, setTypewriterDone]= useState(() => !animate);
const [flagged, setFlagged] = useState(false);
const indexRef = useRef(0);
useEffect(() => {
if (!text) return;
// Historique chargé → affichage immédiat, sans animation
if (!animate) {
setDisplayed(text);
setTypewriterDone(true);
return;
}
// Nouvelle réponse → typewriter
setDisplayed('');
setTypewriterDone(false);
indexRef.current = 0;
const tokens = text.split(/(\s+)/);
let cancelled = false;
const step = () => {
if (cancelled) return;
if (indexRef.current >= tokens.length) { setTypewriterDone(true); return; }
indexRef.current++;
setDisplayed(tokens.slice(0, indexRef.current).join(''));
setTimeout(step, 14 + Math.random() * 22);
};
step();
return () => { cancelled = true; };
}, [text, animate]);
const handleFlag = () => {
setFlagged(true);
if (onFlag) onFlag(displayed.slice(0, 140));
};
// Indicateur de chargement pendant l'appel AAP
if (streaming && !text) {
return (
AAP à l'instant
Interrogation de l'AAP en cours…
);
}
return (
AAP
{latencyMs != null && (
{(latencyMs / 1000).toFixed(1)}s
{score != null && ` · score ${score}`}
{status && status !== 'SUCCESS' && (
{status}
)}
)}
{!typewriterDone && text && }
{typewriterDone && text && (
{flagged ? 'Anomalie signalée' : 'Signaler une anomalie'}
)}
);
}
Object.assign(window, {
LoginGate, AAPResponse,
Sidebar, Welcome, StreamingMessage, UserMessage, Composer,
ToolCall, QuoteCard, LeadsCard, CompareCard, EmailCard, MdText,
});