// 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 (
{dots.map((d, i) =>
)}
); } // ============ 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 (
Calipso Assurances

{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' && }
{event.name}
{event.sub}
{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}
{data.rows.map((r, i) => )}
GarantiePrime
{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
{data.map((r, i) => )}
Critère Calipso AXA Pro Generali
{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 &&
    {thinking}
    }
    ); } // ============ User Message ============ function UserMessage({ text }) { return (
    QA
    Testeur QA à l'instant

    {text}

    ); } // ============ 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 (