'use client'; import { FileText, Wand2, X, MessageSquare, Edit3, User, Headphones } from 'lucide-react'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { scrollToSection } from '@/config/navigation'; import { useAppStore } from '@/store/useAppStore'; import { apiService } from '@/services/api'; import { Button } from '@/components/ui/Button'; import { Card } from '@/components/ui/Card'; import { InlineError } from '@/components/ui/InlineError'; import { Reveal } from '@/components/ui/Reveal'; import { SectionHeader } from '@/components/ui/SectionHeader'; import { cn } from '@/lib/utils'; const getSpeakerGradient = (speaker: string) => { const s = speaker.toLowerCase(); if (s === 'customer') return 'from-cyan-500 to-emerald-500'; if (s === 'agent') return 'from-violet-500 to-indigo-600'; return 'from-amber-500 to-orange-500'; }; const getSpeakerBgBorder = (speaker: string) => { const s = speaker.toLowerCase(); if (s === 'customer') return 'bg-cyan-500/5 border-cyan-500/20'; if (s === 'agent') return 'bg-violet-500/5 border-violet-500/20'; return 'bg-amber-500/5 border-amber-500/20'; }; const getSpeakerTextColor = (speaker: string) => { const s = speaker.toLowerCase(); if (s === 'customer') return 'text-cyan-600 dark:text-cyan-400'; if (s === 'agent') return 'text-violet-600 dark:text-violet-400'; return 'text-amber-600 dark:text-amber-400'; }; const getSpeakerRingOffset = (speaker: string, isActive: boolean) => { if (!isActive) return ''; const s = speaker.toLowerCase(); if (s === 'customer') return 'shadow-[0_0_12px_rgba(6,182,212,0.15)] border-cyan-500/40 bg-cyan-500/10'; if (s === 'agent') return 'shadow-[0_0_12px_rgba(139,92,246,0.15)] border-violet-500/40 bg-violet-500/10'; return 'shadow-[0_0_12px_rgba(245,158,11,0.15)] border-amber-500/40 bg-amber-500/10'; }; const getSpeakerAlignment = (speaker: string) => { const s = speaker.toLowerCase(); if (s === 'agent') return 'flex-row-reverse'; return 'flex-row'; }; export function ConversationInputSection() { const { transcription, setTranscription, setExtracting, isExtracting, error, setError, features } = useAppStore(); const [localText, setLocalText] = useState(''); const [viewMode, setViewMode] = useState<'edit' | 'speaker'>('edit'); const [activeSegmentIndex, setActiveSegmentIndex] = useState(null); useEffect(() => { if (transcription) { setLocalText(transcription); } }, [transcription]); // Auto-switch to speaker view when diarized data arrives and set initial active segment useEffect(() => { if (features?.diarizedTranscript?.length) { setViewMode('speaker'); setActiveSegmentIndex(features.diarizedTranscript.length - 1); } }, [features?.diarizedTranscript]); const timelineSegments = useMemo(() => { if (!features?.diarizedTranscript) return []; const turns = features.diarizedTranscript; const wordCounts = turns.map(t => t.text.split(/\s+/).filter(w => w.length > 0).length); const totalWords = wordCounts.reduce((a, b) => a + b, 0) || 1; return turns.map((turn, index) => ({ speaker: turn.speaker, widthPercent: (wordCounts[index] / totalWords) * 100, text: turn.text, index, })); }, [features?.diarizedTranscript]); const wordCount = useMemo( () => localText.split(/\s+/).filter((w) => w.length > 0).length, [localText], ); const charCount = localText.length; const approxReadTime = Math.max(1, Math.round(wordCount / 200)); const handleAnalyze = useCallback(async () => { if (!localText.trim()) return; setError(null); setTranscription(localText); setExtracting(true); try { const isTextUnchanged = localText.trim() === transcription.trim(); const response = await apiService.extractFeatures( localText, isTextUnchanged ? (features?.diarizedTranscript ?? undefined) : undefined ); setTranscription(response.transcription); useAppStore.getState().setFeatures(response.features); useAppStore.getState().setPrediction(null); useAppStore.getState().refreshFollowUpAlerts(); setExtracting(false); scrollToSection('speakers'); } catch (err: unknown) { console.error(err); setExtracting(false); const message = err instanceof Error ? err.message : 'Backend feature extraction failed. Check LLaMA 3 connection.'; setError(message); } }, [localText, setTranscription, setExtracting, setError]); const handleClear = () => { setLocalText(''); setTranscription(''); setViewMode('edit'); }; const showError = error && error.toLowerCase().includes('feature extraction'); const hasDiarized = (features?.diarizedTranscript?.length ?? 0) > 0; return (
{showError && ( setError(null)} /> )} {/* Mini Waveform Visualization */}
{Array.from({ length: 75 }).map((_, i) => { const centerDist = Math.abs(i - 37) / 37; const envelope = Math.max(0.2, 1 - centerDist * centerDist); const height = (Math.sin(i * 0.15) * 6 + 10) * envelope; return (
); })}
{/* Header */}
Transcript
{/* Speaker / Edit toggle — only shown when diarized data is available */} {hasDiarized && (
)} {localText && ( )}
{/* Body */} {viewMode === 'speaker' && hasDiarized ? ( {/* Speaker Timeline Strip */} {timelineSegments.length > 0 && (
Conversation Timeline Active: {activeSegmentIndex !== null ? timelineSegments[activeSegmentIndex]?.speaker : 'None'}
{timelineSegments.map((seg, idx) => { const isActive = activeSegmentIndex === idx; const showLabel = seg.widthPercent > 12; return (
{ setActiveSegmentIndex(idx); const bubbleEl = document.getElementById(`bubble-${idx}`); if (bubbleEl) { bubbleEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } }} > {showLabel && ( {seg.speaker} )} {isActive && ( )}
); })}
Agent
Customer
Guest
)} {features!.diarizedTranscript!.map((turn, index) => { const isAgent = turn.speaker.toLowerCase() === 'agent'; return ( {/* Avatar */}
setActiveSegmentIndex(index)} > {activeSegmentIndex === index && ( )} {isAgent ? ( ) : ( )}
{/* Bubble */}
setActiveSegmentIndex(index)} > {/* Speaker label */}
{turn.speaker}

{turn.text}

); })}
) : ( setLocalText(e.target.value)} placeholder="Upload audio to populate the transcript, or paste conversation text here…" className="min-h-[280px] w-full resize-none border-0 bg-transparent px-5 py-4 text-base leading-relaxed text-nexus-fg placeholder:text-nexus-muted focus:outline-none focus:ring-0 md:text-lg" /> )}
{/* Footer */}
{wordCount.toLocaleString()} words {charCount.toLocaleString()} chars ~{approxReadTime} min read {hasDiarized && ( {features!.diarizedTranscript!.length} speaker turns )}
); }