import React, { useState, useRef, useEffect, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { useWorkerProfile, useUpdateWorkerProfile } from '@/lib/krowHooks'; import { recalcProfilePatch } from '@/lib/krowScore'; import { owliverQuestion, buildProfileFromConversation } from '@/lib/krowAi'; import { Button } from '@/components/ui/button'; import { Mic, MicOff, Volume2, VolumeX, Loader2, ArrowRight, Send, Sparkles, RefreshCw } from 'lucide-react'; import { cn } from '@/lib/utils'; import OwliverAvatar from '@/components/krow/OwliverAvatar'; const TOTAL_SECONDS = 300; // 5 minutes const QUESTION_COUNT = 6; const AVAIL_TOKENS = ['Weekdays', 'Weekends', 'Evenings', 'Mornings', 'Overnight', 'On-Call']; // Owliver's wisdom — Career DNA: a living professional identity, built from signals not claims const UI = { en: { badge: 'Step 2 · Meet Owliver', title: 'Talk to Owliver', subtitle: 'Not forms. Just a 5-minute conversation. Owliver learns who you are and builds your profile automatically.', hi: (n) => `Hi ${n}, I'm Owliver`, introP: "I'll ask a few friendly questions. Just speak naturally — there are no wrong answers.", banner: "Just a casual chat — no right or wrong answers. I'll turn it into your profile for you.", speaks: 'Owliver speaks', muted: 'Muted', start: 'Start talking', question: 'Question', of: 'of', skipped: 'skipped', listening: 'Listening…', placeholder: 'Type your answer or tap the mic to speak', speak: 'Speak', stop: 'Stop', skip: 'Skip', send: 'Send', building1: 'Owliver is building your profile…', building2: 'Turning our conversation into your KROW Identity.', done: 'Owliver built your profile', seeId: 'See my KROW ID', yrs: 'yrs', dash: '—', notEnoughTitle: "Owliver didn't catch enough", notEnoughBody: "I didn't get much from our chat — try again and answer a couple of questions so I can build your profile.", chatAgain: "Let's chat again", goToProfile: 'Go to my profile', labels: { desired: 'Desired role', experience: 'Experience', transport: 'Transportation', salary: 'Salary', leadership: 'Leadership', skills: 'Skills', languages: 'Languages', industries: 'Industries', availability: 'Availability', strengths: 'Strengths', workingOn: 'Working on', personality: 'Personality', identity: 'Identity', learningSpeed: 'Learning speed', reliability: 'Reliability', communication: 'Communication', leadershipIndex: 'Leadership index', adaptability: 'Adaptability', workStyle: 'Work style', cultureMatch: 'Culture match', reputation: 'Reputation baseline' }, dnaHeader: 'Career DNA', dnaSub: 'Built from this conversation — signals, not claims. It grows with every verified shift, certification, and review.', }, es: { badge: 'Paso 2 · Conoce a Owliver', title: 'Habla con Owliver', subtitle: 'No formularios. Solo una conversación de 5 minutos. Owliver aprende quién eres y crea tu perfil automáticamente.', hi: (n) => `Hola ${n}, soy Owliver`, introP: 'Te haré algunas preguntas amigables. Solo habla naturalmente — no hay respuestas incorrectas.', banner: 'Solo una charla casual — no hay respuestas correctas ni incorrectas. Lo convierto en tu perfil por ti.', speaks: 'Owliver habla', muted: 'Silenciado', start: 'Empezar a hablar', question: 'Pregunta', of: 'de', skipped: 'omitido', listening: 'Escuchando…', placeholder: 'Escribe tu respuesta o toca el micrófono para hablar', speak: 'Hablar', stop: 'Detener', skip: 'Saltar', send: 'Enviar', building1: 'Owliver está creando tu perfil…', building2: 'Convirtiendo nuestra conversación en tu KROW Identity.', done: 'Owliver creó tu perfil', seeId: 'Ver mi KROW ID', yrs: 'años', dash: '—', notEnoughTitle: 'Owliver no captó suficiente', notEnoughBody: 'No obtuve mucho de nuestra charla — intenta de nuevo y responde un par de preguntas para que pueda crear tu perfil.', chatAgain: 'Charlemos de nuevo', goToProfile: 'Ir a mi perfil', labels: { desired: 'Rol deseado', experience: 'Experiencia', transport: 'Transporte', salary: 'Salario', leadership: 'Liderazgo', skills: 'Habilidades', languages: 'Idiomas', industries: 'Industrias', availability: 'Disponibilidad', strengths: 'Fortalezas', workingOn: 'Por mejorar', personality: 'Personalidad', identity: 'Identidad', learningSpeed: 'Velocidad de aprendizaje', reliability: 'Confiabilidad', communication: 'Comunicación', leadershipIndex: 'Índice de liderazgo', adaptability: 'Adaptabilidad', workStyle: 'Estilo de trabajo', cultureMatch: 'Encaje cultural', reputation: 'Reputación base' }, dnaHeader: 'Career DNA', dnaSub: 'Creado a partir de esta conversación — señales, no afirmaciones. Crece con cada turno, certificación y reseña verificados.', }, }; interface OwliverProps { compact?: any; } export default function Owliver({ compact = false }: OwliverProps) { const navigate = useNavigate(); const { data: profile, isLoading } = useWorkerProfile(); const updateProfile = useUpdateWorkerProfile(); const [phase, setPhase] = useState('intro'); // intro | active | building | done const [messages, setMessages] = useState([]); const [transcript, setTranscript] = useState(''); const [isListening, setIsListening] = useState(false); const [isSpeaking, setIsSpeaking] = useState(false); const [, setTimeLeft] = useState(TOTAL_SECONDS); const [extracted, setExtracted] = useState(null); const [error, setError] = useState(''); const [ttsEnabled, setTtsEnabled] = useState(true); const [language, setLanguage] = useState('en'); const recognitionRef = useRef(null); const lastUserTime = useRef(null); const timerRef = useRef(null); const scrollRef = useRef(null); const workerName = profile?.full_name || 'there'; const t = UI[language]; useEffect(() => { if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight; }, [messages, isSpeaking]); // Warm owl voice — slightly higher pitch, slower, gentle const speak = useCallback((text) => { return new Promise((resolve) => { if (!ttsEnabled || !window.speechSynthesis) { resolve(); return; } window.speechSynthesis.cancel(); const u = new SpeechSynthesisUtterance(text); const target = language === 'es' ? 'es' : 'en'; u.lang = language === 'es' ? 'es-ES' : 'en-US'; const voices = window.speechSynthesis.getVoices(); const langVoices = voices.filter(v => v.lang?.toLowerCase().startsWith(target)); if (langVoices.length > 0) { const preferred = langVoices.find(v => v.localService && v.lang.toLowerCase() === (language === 'es' ? 'es-es' : 'en-us')) || langVoices.find(v => v.localService) || langVoices.find(v => v.name.toLowerCase().includes('google')) || langVoices[0]; u.voice = preferred; } u.rate = 0.96; u.pitch = 1.12; u.onstart = () => setIsSpeaking(true); u.onend = () => { setIsSpeaking(false); resolve(); }; u.onerror = () => { setIsSpeaking(false); resolve(); }; window.speechSynthesis.speak(u); }); }, [ttsEnabled, language]); const stopListening = useCallback(() => { if (recognitionRef.current) { recognitionRef.current.stop(); recognitionRef.current = null; } setIsListening(false); }, []); const askNext = useCallback(async (currentMessages) => { const qNumber = currentMessages.filter(m => m.role === 'assistant').length + 1; if (qNumber > QUESTION_COUNT) { await finishChat(currentMessages); return; } try { const question = await owliverQuestion(currentMessages, workerName, qNumber, language); const aiMsg = { role: 'assistant', content: question, timestamp: new Date().toISOString() }; const newMessages = [...currentMessages, aiMsg]; setMessages(newMessages); lastUserTime.current = Date.now(); await speak(question); } catch { setError('Owliver lost his train of thought. Try again.'); } }, [workerName, speak, language]); const startListening = useCallback(() => { const win = /** @type {any} */ (window); const SR = win.SpeechRecognition || win.webkitSpeechRecognition; if (!SR) { setError('Voice input not supported here — type your answer below.'); return; } // Yield Owliver's voice so the mic picks up the user, not Owliver. if (window.speechSynthesis) window.speechSynthesis.cancel(); setIsSpeaking(false); const base = transcript; if (!base) setTranscript(''); setError(''); const rec = new SR(); rec.continuous = true; rec.interimResults = true; rec.lang = language === 'es' ? 'es-ES' : 'en-US'; rec.onresult = (event) => { let text = ''; for (let i = 0; i < event.results.length; i++) text += event.results[i][0].transcript; setTranscript((base ? base + ' ' : '') + text.trim()); }; rec.onerror = (event) => { if (event.error !== 'no-speech' && event.error !== 'aborted') setError(`Mic error: ${event.error}`); setIsListening(false); }; rec.onend = () => setIsListening(false); recognitionRef.current = rec; rec.start(); setIsListening(true); }, [language, transcript]); const submitAnswer = useCallback(() => { if (!transcript.trim()) return; stopListening(); const userMsg = { role: 'user', content: transcript.trim(), timestamp: new Date().toISOString() }; const newMessages = [...messages, userMsg]; setMessages(newMessages); setTranscript(''); setTimeout(() => askNext(newMessages), 300); }, [transcript, messages, stopListening, askNext]); const skipAnswer = useCallback(() => { stopListening(); const userMsg = { role: 'user', content: language === 'es' ? '(Prefiero saltar esta)' : "(I'd rather skip this one)", timestamp: new Date().toISOString(), skipped: true }; const newMessages = [...messages, userMsg]; setMessages(newMessages); setTranscript(''); setTimeout(() => askNext(newMessages), 250); }, [messages, stopListening, askNext, language]); const normalizeAvail = (arr) => { if (!Array.isArray(arr)) return []; return arr.map(a => { const hit = AVAIL_TOKENS.find(t => t.toLowerCase() === String(a).toLowerCase()); return hit || a; }); }; const finishChat = useCallback(async (finalMessages) => { setPhase('building'); stopListening(); if (window.speechSynthesis) window.speechSynthesis.cancel(); // Only send real (non-skipped) answers to the profile builder. const realMessages = (finalMessages || []).filter(m => !(m.role === 'user' && m.skipped)); const realAnswers = realMessages.filter(m => m.role === 'user'); if (realAnswers.length === 0) { setPhase('notenough'); return; } try { const result = await buildProfileFromConversation(realMessages, workerName, language); const patch = { career_goals: result.career_goals || '', desired_position: result.desired_position || '', current_position: result.current_position || '', experience_years: result.experience_years || 0, experience: result.experience || [], skills: result.skills || [], languages: result.languages || [], availability: normalizeAvail(result.availability), transportation: result.transportation || '', certifications: result.certifications || [], personality: result.personality || '', strengths: result.strengths || [], weaknesses: result.weaknesses || [], industries: result.industries || [], salary_expectations: result.salary_expectations || '', leadership_potential: result.leadership_potential || 0, communication_style: result.communication_style || '', }; const merged = { ...(profile || {}), ...patch }; const fullPatch = { ...patch, ...recalcProfilePatch(merged) }; if (profile?.id) { await updateProfile.mutateAsync({ id: profile.id, data: fullPatch }); } setExtracted({ ...result, availability: normalizeAvail(result.availability) }); setPhase('done'); } catch { setError('Owliver could not finish building your profile. Try again.'); setPhase('active'); } }, [workerName, profile, updateProfile, language, stopListening]); const startChat = useCallback(() => { setPhase('active'); setTimeLeft(TOTAL_SECONDS); setMessages([]); askNext([]); }, [askNext]); // Timer useEffect(() => { if (phase !== 'active') return; timerRef.current = setInterval(() => { setTimeLeft((prev) => { if (prev <= 1) { clearInterval(timerRef.current); stopListening(); setMessages((cur) => { if (cur.length > 0) finishChat(cur); return cur; }); return 0; } return prev - 1; }); }, 1000); return () => clearInterval(timerRef.current); }, [phase, finishChat, stopListening]); // Cleanup on unmount useEffect(() => { return () => { stopListening(); if (window.speechSynthesis) window.speechSynthesis.cancel(); if (timerRef.current) clearInterval(timerRef.current); }; }, [stopListening]); if (isLoading) { return
; } return (
{/* Hero — page only */} {!compact && (
🦉 {t.badge}

{t.title}

{t.subtitle}

)} {/* Intro */} {phase === 'intro' && ( compact ? (

{t.introP}

) : (

{t.hi(workerName.split(' ')[0])}

{t.introP}

{t.banner}
) )} {/* Active conversation */} {phase === 'active' && (
Owliver
{t.question} {Math.min(messages.filter(m => m.role === 'assistant').length, QUESTION_COUNT)} {t.of} {QUESTION_COUNT}
m.role === 'user').length / QUESTION_COUNT) * 100)}%` }} />
{messages.map((msg, i) => (
{msg.skipped ? ( {t.skipped} ) : (

{msg.content}

)}
))} {isSpeaking && (
)}
{error &&

{error}

}