'use client'; import { useEffect, useRef } from 'react'; import { useTranslations } from 'next-intl'; import { motion, AnimatePresence, useMotionValue, useTransform } from 'framer-motion'; import { Mic, MicOff, PhoneOff, Loader2 } from 'lucide-react'; import { cn } from '@/lib/utils'; import Chip from '@/components/ui/Chip'; import { useVoiceAgent, type TranscriptEntry } from './VoiceAgentProvider'; import type { WizardFormData } from './WizardContainer'; // ─── Types ─────────────────────────────────────────────────────────────────── interface VoiceAgentProps { locale: string; onComplete: (brief: string, formData: WizardFormData) => void; } // ─── Transcript Bubble ─────────────────────────────────────────────────────── function TranscriptBubble({ entry }: { entry: TranscriptEntry }) { return ( {entry.text} ); } // ─── Main Component ────────────────────────────────────────────────────────── export default function VoiceAgent({ locale, onComplete }: VoiceAgentProps) { const t = useTranslations('configurator'); const { status, errorMessage, isMicActive, toggleMic, transcript, selections, isAnalyzingSite, agentAmplitude, startConversation, endConversation, completedBrief, completedFormData, } = useVoiceAgent(); const transcriptEndRef = useRef(null); // Auto-scroll transcript useEffect(() => { transcriptEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [transcript]); // Handle completion useEffect(() => { if (completedBrief && completedFormData) { const timer = setTimeout(() => { onComplete(completedBrief, completedFormData); }, 1500); return () => clearTimeout(timer); } }, [completedBrief, completedFormData, onComplete]); // Orb animation driven by agent amplitude const amplitudeValue = useMotionValue(0); useEffect(() => { amplitudeValue.set(agentAmplitude); }, [agentAmplitude, amplitudeValue]); const orbScale = useTransform(amplitudeValue, [0, 0.5], [1, 1.18]); const orbGlow = useTransform( amplitudeValue, [0, 0.5], ['0px 0px 0px rgba(0,100,148,0)', '0px 0px 30px rgba(0,100,148,0.3)'], ); // Build selection chips const chipLabels: string[] = []; if (selections.services) { for (const svc of selections.services) { try { chipLabels.push(t(`services.${svc}.title`)); } catch { chipLabels.push(svc); } } } if (selections.aiEnabled && selections.aiTypes) { for (const ai of selections.aiTypes) { try { chipLabels.push(t(`aiTypes.${ai}.title`)); } catch { chipLabels.push(ai); } } } if (selections.industry) { try { chipLabels.push(t(`industries.${selections.industry}`)); } catch { chipLabels.push(selections.industry); } } if (selections.timeline) { try { chipLabels.push(t(`timelines.${selections.timeline}`)); } catch { chipLabels.push(selections.timeline); } } return ( {/* Agent card header */} L {t('voice.agentName')} {status === 'active' ? 'Connected' : status === 'connecting' ? t('voice.connecting') : 'Ready'} {/* Waveform orb */} {status === 'idle' && ( )} {status === 'connecting' && ( )} {status === 'active' && ( )} {/* Analyzing site badge */} {isAnalyzingSite && ( {t('voice.analyzingSite')} )} {/* Error message */} {errorMessage && ( {errorMessage} )} {/* Live transcript */} {transcript.length > 0 && ( {transcript.map((entry, i) => ( ))} )} {/* Selection chips */} {chipLabels.length > 0 && ( {t('voice.capturedSoFar')} {chipLabels.map((label, i) => ( {label} ))} )} {/* Controls */} {status === 'idle' && ( {locale === 'fr' ? 'Démarrer la conversation' : 'Start Conversation'} )} {status === 'active' && ( <> {isMicActive ? : } {t('voice.endConversation')} > )} {status === 'connecting' && ( {t('voice.connecting')} )} ); }
{t('voice.agentName')}
{errorMessage}
{t('voice.capturedSoFar')}
{t('voice.connecting')}