import { useState, useCallback, useRef } from 'react'; import JarvisOrb from './components/JarvisOrb'; import ConversationOverlay from './components/ConversationOverlay'; import ProjectCardsDrawer from './components/ProjectCardsDrawer'; import useOperatorSSE from './hooks/useOperatorSSE'; import useWakeWord from './hooks/useWakeWord'; import useSpeechRecognition from './hooks/useSpeechRecognition'; import useTTS from './hooks/useTTS'; const PROJECTS = [ { id: 'school', name: 'Master IA \u2014 Sorbonne', contact: 'prof.martinez@sorbonne.fr', color: '#4285f4' }, { id: 'company', name: 'Alternance \u2014 BNP Paribas', contact: 'sophie.renard@bnpparibas.com', color: '#ea4335' }, { id: 'startup', name: 'Side Project \u2014 NoctaAI', contact: 'yassine@noctaai.com', color: '#fbbc04' }, ]; export default function App() { const { sendChat } = useOperatorSSE(); const { speak } = useTTS(); // Start directly in IDLE — no scan needed const [phase, setPhase] = useState('IDLE'); const [messages, setMessages] = useState([]); const phaseRef = useRef(phase); phaseRef.current = phase; const [inputText, setInputText] = useState(''); const [showTranscript, setShowTranscript] = useState(false); const handleWakeWord = useCallback(() => { if (phaseRef.current === 'IDLE') setPhase('LISTENING'); }, []); const { micActive, micError, requestMic } = useWakeWord({ enabled: phase === 'IDLE', onDetected: handleWakeWord, }); const handleOrbClick = useCallback(() => { if (phaseRef.current === 'IDLE') { setPhase('LISTENING'); } }, []); const handleUserSaid = useCallback(async (text) => { if (!text.trim()) return; const bye = text.toLowerCase(); if (bye.includes('merci') || bye.includes("c'est bon") || bye.includes('a plus') || bye.includes('au revoir')) { setMessages(prev => [...prev, { role: 'user', text }]); const farewell = "Ok, je reste la si tu as besoin. Dis 'Oppy' quand tu veux."; setMessages(prev => [...prev, { role: 'assistant', text: farewell }]); setPhase('SPEAKING'); await speak(farewell); setPhase('IDLE'); return; } setMessages(prev => [...prev, { role: 'user', text }]); setPhase('SPEAKING'); const acks = [ "D'accord, donne-moi un petit instant.", "Je regarde ca tout de suite.", "Laisse-moi verifier.", "Une seconde, je cherche.", ]; const ack = acks[Math.floor(Math.random() * acks.length)]; const ackDone = speak(ack); const replyPromise = sendChat(text); await ackDone; setPhase('THINKING'); try { const reply = await replyPromise; if (reply) { setMessages(prev => [...prev, { role: 'assistant', text: reply }]); setPhase('SPEAKING'); await speak(reply); if (phaseRef.current === 'SPEAKING') setPhase('LISTENING'); } else { setPhase('LISTENING'); } } catch (err) { console.error('Chat error:', err); setPhase('LISTENING'); } }, [sendChat, speak]); const handleTimeout = useCallback(() => { if (phaseRef.current === 'LISTENING') setPhase('IDLE'); }, []); const { transcript } = useSpeechRecognition({ enabled: phase === 'LISTENING', onResult: handleUserSaid, onTimeout: handleTimeout, timeoutMs: 20000, }); const handleSubmit = (e) => { e.preventDefault(); const text = inputText.trim(); if (!text || phase === 'THINKING' || phase === 'SPEAKING') return; setInputText(''); handleUserSaid(text); }; let label = ''; if (phase === 'IDLE') { if (micError) label = "Clique sur l'orbe pour autoriser le micro"; else if (!micActive) label = "Clique sur l'orbe pour activer le micro"; else label = "Micro actif — dis 'Oppy' pour commencer"; } else if (phase === 'LISTENING') label = transcript || "Je t'ecoute..."; else if (phase === 'THINKING') label = 'Oppy reflechit...'; else if (phase === 'SPEAKING') label = ''; const micDotColor = phase === 'IDLE' && micActive ? '#34a853' : phase === 'IDLE' && micError ? '#ea4335' : null; return (