From 3cdb95e488667b350be35de97fb8b6fc8d3a2344 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 6 Apr 2026 14:44:28 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20rebuild=20voice=20agent=20UI=20?= =?UTF-8?q?=E2=80=94=20larger=20layout,=20contact=20card,=20reconnect,=20n?= =?UTF-8?q?o=20chips?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Larger orb (w-24), taller transcript (max-h-72), proper scrollIntoView - On-screen contact confirmation card replaces verbal spell-back - Reconnect button on connection loss - Selection chips removed (structured data captured silently) - Mobile-sticky controls for thumb reach Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/configurator/VoiceAgent.tsx | 141 +++++++++++---------- 1 file changed, 76 insertions(+), 65 deletions(-) diff --git a/src/components/configurator/VoiceAgent.tsx b/src/components/configurator/VoiceAgent.tsx index c878c77..70a60de 100644 --- a/src/components/configurator/VoiceAgent.tsx +++ b/src/components/configurator/VoiceAgent.tsx @@ -5,7 +5,6 @@ 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'; @@ -53,7 +52,6 @@ export default function VoiceAgent({ locale, onComplete }: VoiceAgentProps) { isMicActive, toggleMic, transcript, - selections, isAnalyzingSite, isGeneratingBrief, agentAmplitude, @@ -61,16 +59,18 @@ export default function VoiceAgent({ locale, onComplete }: VoiceAgentProps) { endConversation, completedBrief, completedFormData, + pendingContact, + confirmContact, + updatePendingContact, + canReconnect, + reconnect, } = useVoiceAgent(); const transcriptEndRef = useRef(null); - // Auto-scroll transcript within its container only + // Auto-scroll transcript useEffect(() => { - const el = transcriptEndRef.current; - if (el?.parentElement) { - el.parentElement.scrollTop = el.parentElement.scrollHeight; - } + transcriptEndRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' }); }, [transcript]); // Handle completion — end the call, then transition @@ -98,32 +98,6 @@ export default function VoiceAgent({ locale, onComplete }: VoiceAgentProps) { ['0px 0px 0px rgba(0,100,148,0)', '0px 0px 30px rgba(0,100,148,0.3)'], ); - // Build selection chips — use i18n for known keys, raw value otherwise - const KNOWN_SERVICES = ['web', 'systems', 'infrastructure']; - const KNOWN_AI_TYPES = ['teammate', 'customer-facing', 'data-intelligence', 'notsure']; - const KNOWN_INDUSTRIES = ['maritime', 'hospitality', 'technology', 'realestate', 'finance', 'ngo', 'other']; - const KNOWN_TIMELINES = ['asap', '1-3months', '3-6months', 'exploring']; - - const chipLabels: string[] = []; - if (selections.services) { - for (const svc of selections.services) { - chipLabels.push(KNOWN_SERVICES.includes(svc) ? t(`services.${svc}.title`) : svc); - } - } - if (selections.aiEnabled && selections.aiTypes) { - for (const ai of selections.aiTypes) { - chipLabels.push(KNOWN_AI_TYPES.includes(ai) ? t(`aiTypes.${ai}.title`) : ai); - } - } - if (selections.industry) { - const ind = selections.industry; - chipLabels.push(KNOWN_INDUSTRIES.includes(ind) ? t(`industries.${ind}`) : ind); - } - if (selections.timeline) { - const tl = selections.timeline; - chipLabels.push(KNOWN_TIMELINES.includes(tl) ? t(`timelines.${tl}`) : tl); - } - return (
{/* Agent card header */} @@ -152,20 +126,20 @@ export default function VoiceAgent({ locale, onComplete }: VoiceAgentProps) { - {status === 'idle' && ( - + {status === 'idle' && !isGeneratingBrief && ( + )} - {status === 'connecting' && ( + {(status === 'connecting' || (status === 'idle' && isGeneratingBrief)) && ( - + )} {status === 'active' && ( @@ -173,7 +147,7 @@ export default function VoiceAgent({ locale, onComplete }: VoiceAgentProps) { animate={{ scale: [1, 1.1, 1] }} transition={{ duration: 2, repeat: Infinity, ease: 'easeInOut' }} > - + )} @@ -203,20 +177,20 @@ export default function VoiceAgent({ locale, onComplete }: VoiceAgentProps) { - {locale === 'fr' ? 'Génération de votre brief...' : 'Generating your brief...'} + {t('voice.generatingBrief')} )} {/* Error message */} - {errorMessage && ( + {errorMessage && !canReconnect && (

{errorMessage}

)}
{/* Live transcript */} {transcript.length > 0 && ( -
+
{transcript.map((entry, i) => ( @@ -226,37 +200,58 @@ export default function VoiceAgent({ locale, onComplete }: VoiceAgentProps) {
)} - {/* Selection chips */} + {/* Contact confirmation card */} - {chipLabels.length > 0 && ( + {pendingContact && !completedBrief && ( -

- {t('voice.capturedSoFar')} +

+ {t('voice.contactConfirm')}

-
- {chipLabels.map((label, i) => ( - - {label} - - ))} +
+
+ + updatePendingContact('name', e.target.value)} + className="flex-1 text-sm text-on-surface bg-white rounded-lg border border-outline-variant/30 px-3 py-1.5 focus:outline-none focus:ring-1 focus:ring-primary/40" + /> +
+
+ + updatePendingContact('email', e.target.value)} + className="flex-1 text-sm text-on-surface bg-white rounded-lg border border-outline-variant/30 px-3 py-1.5 focus:outline-none focus:ring-1 focus:ring-primary/40" + /> +
+ )} - {/* Controls */} -
- {status === 'idle' && !completedBrief && ( + {/* Controls — sticky on mobile for thumb reach */} +
+ {status === 'idle' && !completedBrief && !isGeneratingBrief && ( +
+ )}
);