[object Object]

← back to Dear Bubbe Nextjs

refactor(bubbe): delete dead ExpandableChat + VoiceManager

3899565343030d41bfd4966d72e626e45b28d0c9 · 2026-07-17 07:41:08 -0700 · Steve Abrams

Both unreachable from the live page→BubbeResponsive→Mobile/Desktop chain
(ExpandableChat had 0 importers; VoiceManager only imported by ExpandableChat).
Removes the Finding-C overlap/retry-storm landmine the contrarian flagged.
Build clean, homepage 200.

Files touched

Diff

commit 3899565343030d41bfd4966d72e626e45b28d0c9
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Jul 17 07:41:08 2026 -0700

    refactor(bubbe): delete dead ExpandableChat + VoiceManager
    
    Both unreachable from the live page→BubbeResponsive→Mobile/Desktop chain
    (ExpandableChat had 0 importers; VoiceManager only imported by ExpandableChat).
    Removes the Finding-C overlap/retry-storm landmine the contrarian flagged.
    Build clean, homepage 200.
---
 components/ExpandableChat.tsx | 551 -------------------------------
 components/VoiceManager.tsx   | 749 ------------------------------------------
 2 files changed, 1300 deletions(-)

diff --git a/components/ExpandableChat.tsx b/components/ExpandableChat.tsx
deleted file mode 100644
index a7893a8..0000000
--- a/components/ExpandableChat.tsx
+++ /dev/null
@@ -1,551 +0,0 @@
-'use client'
-
-import { useState, useEffect, useRef } from 'react'
-import VoiceManager, { VoiceStatus } from '@/components/VoiceManager'
-import GoogleSignIn from '@/components/GoogleSignIn'
-import InputModeSelector, { InputMode } from '@/components/InputModeSelector'
-import TextInput from '@/components/TextInput'
-import { motion, AnimatePresence } from 'framer-motion'
-
-interface Message {
-  role: 'user' | 'assistant' | 'system'
-  content: string
-  timestamp: Date
-}
-
-export default function ExpandableChat() {
-  const [isOpen, setIsOpen] = useState(false)
-  const [messages, setMessages] = useState<Message[]>([])
-  const [isLoading, setIsLoading] = useState(false)
-  const [bubbeResponse, setBubbeResponse] = useState<string | null>(null)
-  const lastResponseTimeRef = useRef(0)
-  const [voiceStatus, setVoiceStatus] = useState<VoiceStatus>('initializing')
-  const [userId, setUserId] = useState<string>('')
-  const [isMobile, setIsMobile] = useState(false)
-  const [inputMode, setInputMode] = useState<InputMode | null>(null) // Start with null to show selector
-  const [showModeSelector, setShowModeSelector] = useState(false)
-  const hasGreeted = useRef(false)
-  const messagesEndRef = useRef<HTMLDivElement>(null)
-  const chatContainerRef = useRef<HTMLDivElement>(null)
-
-  // Initialize user ID and detect mobile
-  useEffect(() => {
-    // Detect mobile device
-    const checkMobile = () => {
-      const mobile = window.innerWidth <= 768 || /iPhone|iPad|iPod|Android/i.test(navigator.userAgent)
-      setIsMobile(mobile)
-      return mobile
-    }
-    
-    checkMobile()
-    window.addEventListener('resize', checkMobile)
-    
-    // Get or create user ID
-    let id = localStorage.getItem('bubbeUserId')
-    if (!id) {
-      id = `user_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`
-      localStorage.setItem('bubbeUserId', id)
-    }
-    setUserId(id)
-    
-    return () => window.removeEventListener('resize', checkMobile)
-  }, [])
-
-  // Scroll to bottom when messages change
-  useEffect(() => {
-    if (isOpen && messagesEndRef.current) {
-      messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
-    }
-  }, [messages, isOpen])
-
-  // Handle opening/closing
-  const handleToggle = () => {
-    if (!isOpen) {
-      setIsOpen(true)
-      setShowModeSelector(true)
-    } else {
-      setIsOpen(false)
-      setShowModeSelector(false)
-      // Don't reset mode, keep it for next time
-    }
-  }
-
-  // Handle mode selection
-  const handleModeSelect = (mode: InputMode) => {
-    setInputMode(mode)
-    setShowModeSelector(false)
-    
-    // If this is the first time opening, trigger greeting
-    if (!hasGreeted.current && mode === 'talk') {
-      hasGreeted.current = true
-      setTimeout(() => sendInitialGreeting(), 500)
-    }
-  }
-
-  // Handle voice transcript from VoiceManager
-  const handleVoiceTranscript = async (transcript: string) => {
-    console.log('[ExpandableChat] 📨 RECEIVED TRANSCRIPT:', transcript)
-    
-    // Special init message - send greeting IMMEDIATELY
-    if (transcript === '__voice_init__') {
-      console.log('[ExpandableChat] 🎯 VOICE INIT DETECTED!')
-      if (!hasGreeted.current) {
-        hasGreeted.current = true
-        sendInitialGreeting()
-      }
-      return
-    }
-    
-    // Regular user message
-    if (transcript && transcript.trim() !== '') {
-      await sendMessage(transcript)
-    }
-  }
-
-  // Send initial greeting
-  const sendInitialGreeting = async () => {
-    console.log('[ExpandableChat] Sending initial greeting...')
-    
-    setIsLoading(true)
-    
-    const defaultGreeting = "Oy vey, look who finally shows up! You couldn't call, you couldn't write? I've been sitting here waiting like a schmuck! So nu, what's so important that you couldn't visit your Bubbe sooner?"
-    
-    let userProfile = null
-    try {
-      const storedProfile = sessionStorage.getItem('userProfile')
-      if (storedProfile) {
-        userProfile = JSON.parse(storedProfile)
-      }
-    } catch (e) {
-      console.log('[ExpandableChat] Could not get profile from session storage')
-    }
-    
-    try {
-      const controller = new AbortController()
-      const timeout = setTimeout(() => controller.abort(), 5000)
-      
-      const response = await fetch('/api/chat', {
-        method: 'POST',
-        headers: { 'Content-Type': 'application/json' },
-        body: JSON.stringify({
-          message: '__init__',
-          userId: userId || `temp_${Date.now()}`,
-          mode: 'bubbe',
-          userProfile
-        }),
-        signal: controller.signal
-      })
-      
-      clearTimeout(timeout)
-      
-      if (response.ok) {
-        const data = await response.json()
-        const greeting: Message = {
-          role: 'assistant',
-          content: data.response || defaultGreeting,
-          timestamp: new Date()
-        }
-        
-        setMessages([greeting])
-        
-        const now = Date.now()
-        if (now - lastResponseTimeRef.current > 1000) {
-          lastResponseTimeRef.current = now
-          setBubbeResponse(greeting.content)
-        }
-      } else {
-        throw new Error('API returned non-OK status')
-      }
-    } catch (error: any) {
-      console.error('[ExpandableChat] Failed to get greeting, using default:', error.message)
-      
-      const greeting: Message = {
-        role: 'assistant',
-        content: defaultGreeting,
-        timestamp: new Date()
-      }
-      
-      setMessages([greeting])
-      
-      const now = Date.now()
-      if (now - lastResponseTimeRef.current > 1000) {
-        lastResponseTimeRef.current = now
-        setBubbeResponse(greeting.content)
-      }
-    } finally {
-      setIsLoading(false)
-    }
-  }
-
-  // Send user message and get response
-  const sendMessage = async (text: string) => {
-    console.log('[ExpandableChat] Sending message:', text)
-    setIsLoading(true)
-    
-    const userMessage: Message = {
-      role: 'user',
-      content: text,
-      timestamp: new Date()
-    }
-    setMessages(prev => [...prev, userMessage])
-    
-    let userProfile = null
-    try {
-      const storedProfile = sessionStorage.getItem('userProfile')
-      if (storedProfile) {
-        userProfile = JSON.parse(storedProfile)
-      }
-    } catch (e) {
-      console.log('[ExpandableChat] Could not get profile from session storage')
-    }
-    
-    try {
-      const response = await fetch('/api/chat', {
-        method: 'POST',
-        headers: { 'Content-Type': 'application/json' },
-        body: JSON.stringify({
-          message: text,
-          userId,
-          mode: 'bubbe',
-          messages: messages.slice(-10),
-          userProfile
-        })
-      })
-      
-      if (response.ok) {
-        const data = await response.json()
-        const assistantMessage: Message = {
-          role: 'assistant',
-          content: data.response,
-          timestamp: new Date()
-        }
-        
-        setMessages(prev => [...prev, assistantMessage])
-        
-        const now = Date.now()
-        if (now - lastResponseTimeRef.current > 1000) {
-          lastResponseTimeRef.current = now
-          setBubbeResponse(data.response)
-        }
-      }
-    } catch (error) {
-      console.error('[ExpandableChat] Failed to send message:', error)
-    } finally {
-      setIsLoading(false)
-    }
-  }
-
-  // Handle voice status changes
-  const handleVoiceStatusChange = (status: VoiceStatus) => {
-    setVoiceStatus(status)
-  }
-  
-  // Handle profile saved
-  const handleProfileSaved = async (profile: any) => {
-    console.log('[ExpandableChat] Profile saved, triggering Bubbe response')
-    setIsLoading(true)
-    
-    try {
-      const response = await fetch('/api/chat', {
-        method: 'POST',
-        headers: { 'Content-Type': 'application/json' },
-        body: JSON.stringify({
-          message: '__profile_complete__',
-          userId,
-          mode: 'bubbe',
-          userProfile: profile
-        })
-      })
-      
-      if (response.ok) {
-        const data = await response.json()
-        const profileMessage: Message = {
-          role: 'assistant',
-          content: data.response,
-          timestamp: new Date()
-        }
-        
-        setMessages(prev => [...prev, profileMessage])
-        
-        const now = Date.now()
-        if (now - lastResponseTimeRef.current > 1000) {
-          lastResponseTimeRef.current = now
-          setBubbeResponse(data.response)
-        }
-      }
-    } catch (error) {
-      console.error('[ExpandableChat] Failed to get profile reaction:', error)
-    } finally {
-      setIsLoading(false)
-    }
-  }
-
-  // Listen for profile save events
-  useEffect(() => {
-    const handleProfileEvent = (e: CustomEvent) => {
-      handleProfileSaved(e.detail)
-    }
-    
-    window.addEventListener('bubbeProfileSaved', handleProfileEvent as any)
-    return () => {
-      window.removeEventListener('bubbeProfileSaved', handleProfileEvent as any)
-    }
-  }, [userId])
-
-  return (
-    <>
-      {/* Floating Action Button */}
-      <AnimatePresence>
-        {!isOpen && (
-          <motion.button
-            initial={{ scale: 0, opacity: 0 }}
-            animate={{ scale: 1, opacity: 1 }}
-            exit={{ scale: 0, opacity: 0 }}
-            whileHover={{ scale: 1.1 }}
-            whileTap={{ scale: 0.95 }}
-            onClick={handleToggle}
-            className="expandable-chat-fab"
-            aria-label="Open chat with Bubbe"
-          >
-            <div className="fab-content">
-              <img 
-                src="/bubbe.png" 
-                alt="Bubbe" 
-                className="fab-avatar"
-                onError={(e) => {
-                  e.currentTarget.style.display = 'none'
-                  e.currentTarget.nextElementSibling?.classList.remove('hidden')
-                }}
-              />
-              <span className="fab-emoji hidden">👵</span>
-              <span className="fab-badge">Chat with Bubbe!</span>
-            </div>
-          </motion.button>
-        )}
-      </AnimatePresence>
-
-      {/* Expandable Chat Container */}
-      <AnimatePresence>
-        {isOpen && (
-          <>
-            {/* Backdrop for desktop */}
-            {!isMobile && (
-              <motion.div
-                initial={{ opacity: 0 }}
-                animate={{ opacity: 1 }}
-                exit={{ opacity: 0 }}
-                onClick={handleToggle}
-                className="expandable-chat-backdrop"
-              />
-            )}
-
-            {/* Chat Container */}
-            <motion.div
-              ref={chatContainerRef}
-              initial={isMobile ? { y: '100%' } : { scale: 0.8, opacity: 0 }}
-              animate={isMobile ? { y: 0 } : { scale: 1, opacity: 1 }}
-              exit={isMobile ? { y: '100%' } : { scale: 0.8, opacity: 0 }}
-              transition={{ 
-                type: 'spring', 
-                damping: 25, 
-                stiffness: 300 
-              }}
-              className={`expandable-chat-container ${isMobile ? 'mobile' : 'desktop'}`}
-            >
-              {/* Header */}
-              <div className="expandable-chat-header">
-                <div className="header-content">
-                  <div className="header-avatar-wrapper">
-                    <img 
-                      src="/bubbe.png" 
-                      alt="Bubbe" 
-                      className="header-avatar"
-                      onError={(e) => {
-                        e.currentTarget.style.display = 'none'
-                        e.currentTarget.nextElementSibling?.classList.remove('hidden')
-                      }}
-                    />
-                    <span className="header-emoji hidden">👵</span>
-                  </div>
-                  <div className="header-info">
-                    <h2 className="header-title">Dear Bubbe</h2>
-                    <p className="header-subtitle">Your sarcastic Jewish grandma</p>
-                  </div>
-                </div>
-                <button
-                  onClick={handleToggle}
-                  className="header-close-btn"
-                  aria-label="Close chat"
-                >
-                  <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
-                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
-                  </svg>
-                </button>
-              </div>
-
-              {/* Google Sign In */}
-              {!inputMode && (
-                <div className="expandable-chat-auth">
-                  <GoogleSignIn onProfileSaved={handleProfileSaved} />
-                </div>
-              )}
-
-              {/* Mode Selector */}
-              {showModeSelector && (
-                <motion.div
-                  initial={{ opacity: 0, y: 20 }}
-                  animate={{ opacity: 1, y: 0 }}
-                  className="expandable-mode-selector"
-                >
-                  <h3 className="mode-selector-title">How would you like to chat?</h3>
-                  <div className="mode-selector-options">
-                    <button
-                      onClick={() => handleModeSelect('talk')}
-                      className="mode-option-btn talk"
-                    >
-                      <span className="mode-option-icon">🎤</span>
-                      <span className="mode-option-title">Talk Only</span>
-                      <span className="mode-option-desc">Voice conversation</span>
-                    </button>
-                    <button
-                      onClick={() => handleModeSelect('text')}
-                      className="mode-option-btn text"
-                    >
-                      <span className="mode-option-icon">⌨️</span>
-                      <span className="mode-option-title">Text Only</span>
-                      <span className="mode-option-desc">Type messages</span>
-                    </button>
-                    <button
-                      onClick={() => handleModeSelect('both')}
-                      className="mode-option-btn both"
-                    >
-                      <span className="mode-option-icon">💬</span>
-                      <span className="mode-option-title">Both</span>
-                      <span className="mode-option-desc">Talk or type</span>
-                    </button>
-                  </div>
-                </motion.div>
-              )}
-
-              {/* Chat Interface (shown after mode selection) */}
-              {inputMode && !showModeSelector && (
-                <motion.div
-                  initial={{ opacity: 0 }}
-                  animate={{ opacity: 1 }}
-                  className="expandable-chat-body"
-                >
-                  {/* Messages Container */}
-                  <div className="expandable-messages-container">
-                    {messages.length === 0 && !isLoading && (
-                      <div className="expandable-empty-state">
-                        <p className="empty-state-text">
-                          {inputMode === 'talk' 
-                            ? "🎤 Start talking, I'm listening!"
-                            : inputMode === 'text'
-                            ? "⌨️ Type something already!"
-                            : "💬 Talk or type, I'm ready!"}
-                        </p>
-                      </div>
-                    )}
-                    
-                    {messages.map((message, index) => (
-                      <motion.div
-                        key={index}
-                        initial={{ opacity: 0, y: 10 }}
-                        animate={{ opacity: 1, y: 0 }}
-                        className={`expandable-message ${message.role}`}
-                      >
-                        <div className={`expandable-message-bubble ${message.role}`}>
-                          <p className="message-content">{message.content}</p>
-                          <p className="message-time">
-                            {new Date(message.timestamp).toLocaleTimeString()}
-                          </p>
-                        </div>
-                      </motion.div>
-                    ))}
-                    
-                    {isLoading && (
-                      <motion.div
-                        initial={{ opacity: 0 }}
-                        animate={{ opacity: 1 }}
-                        className="expandable-loading"
-                      >
-                        <div className="loading-dots">
-                          <span></span>
-                          <span></span>
-                          <span></span>
-                        </div>
-                      </motion.div>
-                    )}
-                    
-                    <div ref={messagesEndRef} />
-                  </div>
-
-                  {/* Input Controls */}
-                  <div className="expandable-input-container">
-                    {/* Voice Manager - Only show when not in text-only mode */}
-                    {inputMode !== 'text' && (
-                      <div className="expandable-voice-section">
-                        <VoiceManager
-                          onTranscript={handleVoiceTranscript}
-                          onStatusChange={handleVoiceStatusChange}
-                          autoStart={inputMode === 'talk'}
-                          bubbeResponse={bubbeResponse}
-                        />
-                      </div>
-                    )}
-
-                    {/* Text Input - Only show when not in talk-only mode */}
-                    {inputMode !== 'talk' && (
-                      <div className="expandable-text-section">
-                        <TextInput 
-                          onSend={sendMessage}
-                          disabled={isLoading}
-                          className="expandable-text-input"
-                        />
-                      </div>
-                    )}
-
-                    {/* Mode switcher button */}
-                    <button
-                      onClick={() => setShowModeSelector(true)}
-                      className="mode-switch-btn"
-                      title="Change chat mode"
-                    >
-                      <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
-                        <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" />
-                      </svg>
-                    </button>
-                  </div>
-
-                  {/* Status Bar */}
-                  <div className="expandable-status-bar">
-                    {inputMode === 'text' ? (
-                      isLoading ? "💭 Bubbe is thinking..." : "✨ Type your message"
-                    ) : inputMode === 'talk' ? (
-                      <>
-                        {voiceStatus === 'initializing' && "⏳ Waking up..."}
-                        {voiceStatus === 'listening' && "🎤 Listening..."}
-                        {voiceStatus === 'processing' && "🤔 Thinking..."}
-                        {voiceStatus === 'speaking' && "🗣️ Speaking..."}
-                        {voiceStatus === 'ready' && "✨ Ready"}
-                        {voiceStatus === 'permission-required' && "👆 Tap to allow mic"}
-                        {voiceStatus === 'error' && "❌ Oy vey!"}
-                      </>
-                    ) : (
-                      <>
-                        {voiceStatus === 'listening' ? "🎤 Listening..." : 
-                         voiceStatus === 'speaking' ? "🗣️ Speaking..." :
-                         isLoading ? "💭 Processing..." : "✨ Ready"}
-                      </>
-                    )}
-                  </div>
-                </motion.div>
-              )}
-            </motion.div>
-          </>
-        )}
-      </AnimatePresence>
-    </>
-  )
-}
\ No newline at end of file
diff --git a/components/VoiceManager.tsx b/components/VoiceManager.tsx
deleted file mode 100644
index 3f84aa5..0000000
--- a/components/VoiceManager.tsx
+++ /dev/null
@@ -1,749 +0,0 @@
-'use client'
-
-import { useEffect, useRef, useState, useCallback } from 'react'
-import MobileStartOverlay from './MobileStartOverlay'
-
-interface VoiceManagerProps {
-  onTranscript: (text: string) => void
-  onStatusChange?: (status: VoiceStatus) => void
-  autoStart?: boolean
-  bubbeResponse?: string | null
-}
-
-export type VoiceStatus = 
-  | 'initializing'
-  | 'ready'
-  | 'listening'
-  | 'processing'
-  | 'speaking'
-  | 'error'
-  | 'permission-required'
-
-export default function VoiceManager({ 
-  onTranscript, 
-  onStatusChange, 
-  autoStart = true,
-  bubbeResponse = null 
-}: VoiceManagerProps) {
-  const [status, setStatus] = useState<VoiceStatus>('initializing')
-  const [transcript, setTranscript] = useState('')
-  const [errorMessage, setErrorMessage] = useState<string | null>(null)
-  const [isAutoplayBlocked, setIsAutoplayBlocked] = useState(false)
-  const [isPaused, setIsPaused] = useState(false)
-  const [isMobile, setIsMobile] = useState(false)
-  
-  const recognitionRef = useRef<any>(null)
-  const audioRef = useRef<HTMLAudioElement | null>(null)
-  const isProcessingRef = useRef(false)
-  const initializeAttempted = useRef(false)
-  const pausedStateRef = useRef(false)
-  const conversationActiveRef = useRef(true) // Keep conversation going by default
-
-  // Detect mobile device
-  useEffect(() => {
-    const checkMobile = () => {
-      const mobileCheck = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent)
-      setIsMobile(mobileCheck)
-    }
-    checkMobile()
-    window.addEventListener('resize', checkMobile)
-    return () => window.removeEventListener('resize', checkMobile)
-  }, [])
-  
-  // Update parent component when status changes
-  useEffect(() => {
-    onStatusChange?.(status)
-  }, [status, onStatusChange])
-
-  // Initialize speech recognition
-  const initializeSpeechRecognition = useCallback(() => {
-    if (typeof window === 'undefined') return false
-
-    const SpeechRecognition = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition
-    if (!SpeechRecognition) {
-      console.error('[VoiceManager] Speech recognition not supported')
-      setStatus('error')
-      setErrorMessage('Speech recognition not supported in this browser')
-      return false
-    }
-
-    if (!recognitionRef.current) {
-      const recognition = new SpeechRecognition()
-      recognition.continuous = true
-      recognition.interimResults = true
-      recognition.lang = 'en-US'
-      recognition.maxAlternatives = 1
-      
-      let silenceTimer: NodeJS.Timeout | null = null
-      
-      recognition.onresult = (event: any) => {
-        let currentTranscript = ''
-        
-        // Get the latest result
-        for (let i = event.resultIndex; i < event.results.length; i++) {
-          const result = event.results[i]
-          if (result.isFinal) {
-            currentTranscript += result[0].transcript + ' '
-          } else {
-            currentTranscript = result[0].transcript
-          }
-        }
-        
-        if (currentTranscript.trim()) {
-          setTranscript(currentTranscript.trim())
-          
-          // Clear existing timer
-          if (silenceTimer) {
-            clearTimeout(silenceTimer)
-          }
-          
-          // Set new timer for silence detection (2 seconds)
-          silenceTimer = setTimeout(() => {
-            if (currentTranscript.trim().length > 3 && !isProcessingRef.current) {
-              console.log('[VoiceManager] Silence detected, processing:', currentTranscript)
-              handleTranscriptComplete(currentTranscript.trim())
-            }
-          }, 2000)
-        }
-      }
-
-      recognition.onerror = (event: any) => {
-        console.error('[VoiceManager] Recognition error:', event.error)
-        if (event.error === 'not-allowed') {
-          setStatus('permission-required')
-          setErrorMessage('Microphone permission denied')
-        } else if (event.error === 'no-speech') {
-          // Ignore no-speech errors, they're normal
-          return
-        } else {
-          setStatus('error')
-          setErrorMessage(`Speech recognition error: ${event.error}`)
-        }
-      }
-
-      recognition.onend = () => {
-        console.log('[VoiceManager] Recognition ended')
-        // Auto-restart if we should be listening
-        if (status === 'listening' && !isProcessingRef.current) {
-          setTimeout(() => startListening(), 500)
-        }
-      }
-
-      recognitionRef.current = recognition
-      console.log('[VoiceManager] Speech recognition initialized')
-    }
-    
-    return true
-  }, [status])
-
-  // Handle completed transcript
-  const handleTranscriptComplete = useCallback((text: string) => {
-    if (isProcessingRef.current) return
-    
-    isProcessingRef.current = true
-    setStatus('processing')
-    setTranscript('')
-    stopListening()
-    
-    // Send to parent component
-    onTranscript(text)
-    
-    // Reset processing flag after a short delay
-    setTimeout(() => {
-      isProcessingRef.current = false
-    }, 1000)
-  }, [onTranscript])
-
-  // Pause/Resume functions
-  const pauseConversation = useCallback(() => {
-    console.log('[VoiceManager] Pausing conversation...')
-    setIsPaused(true)
-    pausedStateRef.current = true
-    conversationActiveRef.current = false
-    
-    // Stop listening if currently listening
-    if (recognitionRef.current && status === 'listening') {
-      try {
-        recognitionRef.current.stop()
-        setStatus('ready')
-      } catch (e) {
-        console.error('[VoiceManager] Error stopping recognition:', e)
-      }
-    }
-    
-    // Pause audio if playing
-    if (audioRef.current && !audioRef.current.paused) {
-      audioRef.current.pause()
-      setStatus('ready')
-    }
-  }, [status])
-
-  // Internal start listening function that doesn't depend on startListening
-  const startListeningInternal = useCallback(async () => {
-    if (pausedStateRef.current) return
-    
-    // Initialize if needed
-    if (!recognitionRef.current) {
-      if (!initializeSpeechRecognition()) {
-        return
-      }
-    }
-    
-    // Don't start if already listening
-    if (status === 'listening') return
-    
-    try {
-      // Request microphone permission if needed
-      if (navigator.mediaDevices?.getUserMedia) {
-        try {
-          const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
-          stream.getTracks().forEach(track => track.stop())
-        } catch (micError) {
-          console.error('[VoiceManager] Microphone permission denied:', micError)
-          setStatus('permission-required')
-          return
-        }
-      }
-      
-      setStatus('listening')
-      setTranscript('')
-      setErrorMessage(null)
-      recognitionRef.current.start()
-      console.log('[VoiceManager] Listening started from resume')
-    } catch (error: any) {
-      console.error('[VoiceManager] Failed to start listening:', error)
-    }
-  }, [status, initializeSpeechRecognition])
-
-  const resumeConversation = useCallback(async () => {
-    console.log('[VoiceManager] Resuming conversation...')
-    setIsPaused(false)
-    pausedStateRef.current = false
-    conversationActiveRef.current = true
-    
-    // Resume audio if it was paused
-    if (audioRef.current && audioRef.current.paused && audioRef.current.src) {
-      try {
-        await audioRef.current.play()
-        setStatus('speaking')
-        console.log('[VoiceManager] Resumed audio playback')
-      } catch (e) {
-        console.error('[VoiceManager] Failed to resume audio:', e)
-        setStatus('ready')
-        // Try to start listening after a delay
-        setTimeout(() => {
-          startListeningInternal()
-        }, 500)
-      }
-    } else {
-      // Start listening after a delay
-      setStatus('ready')
-      setTimeout(() => {
-        startListeningInternal()
-      }, 500)
-    }
-  }, [startListeningInternal])
-
-  // Start listening
-  const startListening = useCallback(async () => {
-    console.log('[VoiceManager] Starting listening...')
-    
-    // Don't start if explicitly paused
-    if (pausedStateRef.current || !conversationActiveRef.current) {
-      console.log('[VoiceManager] Conversation paused or stopped, not starting')
-      return
-    }
-    
-    // Don't start if audio is playing
-    if (audioRef.current && !audioRef.current.paused) {
-      console.log('[VoiceManager] Audio playing, waiting...')
-      return
-    }
-
-    // Initialize if needed
-    if (!recognitionRef.current) {
-      if (!initializeSpeechRecognition()) {
-        return
-      }
-    }
-
-    // Check if already listening
-    if (status === 'listening') {
-      console.log('[VoiceManager] Already listening')
-      return
-    }
-
-    try {
-      // Request microphone permission
-      if (navigator.mediaDevices?.getUserMedia) {
-        try {
-          const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
-          console.log('[VoiceManager] Microphone permission granted')
-          // Stop the stream immediately after permission check
-          stream.getTracks().forEach(track => track.stop())
-        } catch (micError: any) {
-          console.error('[VoiceManager] Microphone permission denied:', micError)
-          setStatus('permission-required')
-          setErrorMessage('Tap to allow microphone access')
-          setIsAutoplayBlocked(true)
-          return
-        }
-      }
-
-      setStatus('listening')
-      setTranscript('')
-      setErrorMessage(null)
-      recognitionRef.current.start()
-      console.log('[VoiceManager] Listening started successfully')
-      
-    } catch (error: any) {
-      console.error('[VoiceManager] Failed to start listening:', error)
-      setStatus('error')
-      setErrorMessage('Voice recognition failed to start')
-    }
-  }, [status, initializeSpeechRecognition, isPaused])
-
-  // Stop listening
-  const stopListening = useCallback(() => {
-    if (recognitionRef.current && status === 'listening') {
-      try {
-        recognitionRef.current.stop()
-        setStatus('ready')
-        console.log('[VoiceManager] Stopped listening')
-      } catch (error) {
-        console.error('[VoiceManager] Failed to stop:', error)
-      }
-    }
-  }, [status])
-
-  // SIMPLIFIED: Play Bubbe's voice response
-  const playBubbeVoice = useCallback(async (text: string) => {
-    if (!text) return
-    
-    // Don't play if paused
-    if (isPaused || pausedStateRef.current) {
-      console.log('[VoiceManager] Conversation paused, not playing voice')
-      return
-    }
-    
-    console.log('[VoiceManager] 🎯 SIMPLIFIED: Playing Bubbe voice:', text.substring(0, 50))
-    setStatus('speaking')
-    
-    try {
-      // Get voice from API
-      console.log('[VoiceManager] 📡 Fetching audio from API...')
-      const response = await fetch('/api/bubbe-voice', {
-        method: 'POST',
-        headers: { 'Content-Type': 'application/json' },
-        body: JSON.stringify({
-          text,
-          mode: 'bubbe',
-          voiceId: 'yU6e7uJLZCE8eA26VAqb'
-        })
-      })
-      
-      if (!response.ok) {
-        throw new Error(`API failed with status ${response.status}`)
-      }
-      
-      console.log('[VoiceManager] 📦 Got response, converting to blob...')
-      const blob = await response.blob()
-      console.log('[VoiceManager] 📦 Blob size:', blob.size)
-      
-      if (blob.size === 0) {
-        throw new Error('Empty audio blob received')
-      }
-      
-      const audioUrl = URL.createObjectURL(blob)
-      console.log('[VoiceManager] 🔗 Created object URL:', audioUrl.substring(0, 50) + '...')
-      
-      // SIMPLE AUDIO PLAYBACK - NO COMPLEX STUFF
-      const audio = new Audio(audioUrl)
-      audio.volume = 0.8
-      audio.preload = 'auto'
-      
-      // Store reference
-      audioRef.current = audio
-      
-      // Simple event handlers
-      audio.onended = () => {
-        console.log('[VoiceManager] ✅ Audio playback finished - RESTARTING LISTENING NOW!')
-        setStatus('ready')
-        URL.revokeObjectURL(audioUrl)
-        audioRef.current = null
-        
-        // CRITICAL: Always restart listening after Bubbe finishes talking unless explicitly paused
-        setTimeout(() => {
-          console.log('[VoiceManager] 🔄 FORCING listening restart after audio')
-          if (!pausedStateRef.current && conversationActiveRef.current) {
-            console.log('[VoiceManager] 📢 Conversation active - restarting listening...')
-            startListening()
-          } else {
-            console.log('[VoiceManager] 🛑 Not restarting - conversation paused or stopped')
-          }
-        }, 500); // Reduced delay for more responsive conversation
-      };
-      
-      audio.onerror = (e) => {
-        console.error('[VoiceManager] ❌ Audio playback error:', e)
-        setStatus('ready')
-        URL.revokeObjectURL(audioUrl)
-        audioRef.current = null
-        setErrorMessage('Audio playback failed')
-        
-        // Still try to start listening
-        if (autoStart) {
-          setTimeout(() => startListening(), 500);
-        }
-      };
-      
-      audio.onplay = () => {
-        console.log('[VoiceManager] 🔊 Audio started playing!')
-        setIsAutoplayBlocked(false)
-        setErrorMessage(null)
-      }
-      
-      // SIMPLE PLAY - NO PROMISES OR COMPLEX HANDLING
-      console.log('[VoiceManager] ▶️ Attempting to play audio...')
-      
-      try {
-        // Load the audio first
-        audio.load()
-        
-        // Try to play
-        const playPromise = audio.play()
-        
-        if (playPromise && typeof playPromise.then === 'function') {
-          // Modern browsers return a promise
-          playPromise
-            .then(() => {
-              console.log('[VoiceManager] ✅ Audio playing successfully!')
-            })
-            .catch((playError: any) => {
-              console.warn('[VoiceManager] ⚠️ Autoplay blocked:', playError.message)
-              setIsAutoplayBlocked(true)
-              setStatus('permission-required')
-              setErrorMessage('Tap to enable audio')
-            })
-        } else {
-          // Older browsers
-          console.log('[VoiceManager] ✅ Audio play() called (legacy mode)')
-        }
-      } catch (playError: any) {
-        console.warn('[VoiceManager] ⚠️ Play failed:', playError.message)
-        setIsAutoplayBlocked(true)
-        setStatus('permission-required')
-        setErrorMessage('Tap to enable audio')
-      }
-      
-    } catch (error: any) {
-      console.error('[VoiceManager] ❌ Failed to play voice:', error)
-      setStatus('ready')
-      setErrorMessage(`Voice error: ${error.message}`)
-      
-      // Still try to start listening even if voice fails
-      if (autoStart) {
-        setTimeout(() => startListening(), 1000)
-      }
-    }
-  }, [autoStart, startListening, isPaused])
-
-  // Handle Bubbe response
-  const lastResponseRef = useRef<string>('')
-  
-  useEffect(() => {
-    console.log('[VoiceManager] 🔊 bubbeResponse changed:', bubbeResponse?.substring(0, 50))
-    if (bubbeResponse && bubbeResponse.trim() && bubbeResponse !== lastResponseRef.current) {
-      console.log('[VoiceManager] 🎵 New response detected, playing voice!')
-      lastResponseRef.current = bubbeResponse
-      playBubbeVoice(bubbeResponse)
-    } else {
-      if (!bubbeResponse) {
-        console.log('[VoiceManager] ⏭️ No bubbeResponse to play')
-      } else if (bubbeResponse === lastResponseRef.current) {
-        console.log('[VoiceManager] ⏭️ Same response, skipping duplicate')
-      }
-    }
-  }, [bubbeResponse, playBubbeVoice])
-
-  // Handle autoplay unlock
-  const unlockAutoplay = useCallback(async () => {
-    console.log('[VoiceManager] 🔓 Unlocking autoplay...')
-    
-    try {
-      // If we have a saved audio, play it now
-      if (audioRef.current) {
-        console.log('[VoiceManager] 🎵 Playing saved audio after unlock...')
-        try {
-          await audioRef.current.play()
-          console.log('[VoiceManager] ✅ Saved audio now playing!')
-          setIsAutoplayBlocked(false)
-          setErrorMessage(null)
-          setStatus('speaking')
-          return
-        } catch (playError: any) {
-          console.warn('[VoiceManager] ⚠️ Failed to play saved audio:', playError.message)
-        }
-      }
-      
-      // Try playing silent audio to unlock
-      const silentAudio = new Audio('data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4Ljc2LjEwMAAAAAAAAAAAAAAA//tQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWGluZwAAAA8AAAACAAADhAC7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7v//////////////////////////////////////////////////////////////////8AAAAATEFNRTMuMTAwBLkAAAAAAAAAABUgJAUHQQAB4AAAA4S0PQ9ZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//tQxAADB8AhSmxhIIEVCSiJrDJMQBEAAAQCCAAAABLA')
-      silentAudio.volume = 0.01
-      
-      await silentAudio.play()
-      console.log('[VoiceManager] ✅ Silent audio unlock successful')
-      
-      setIsAutoplayBlocked(false)
-      setErrorMessage(null)
-      setStatus('ready')
-      
-      // Now start the conversation with Bubbe's greeting
-      if (autoStart) {
-        console.log('[VoiceManager] 🚀 Triggering Bubbe greeting after unlock...')
-        setTimeout(() => {
-          onTranscript('__voice_init__')
-        }, 200)
-      }
-      
-    } catch (error: any) {
-      console.error('[VoiceManager] ❌ Failed to unlock autoplay:', error)
-      
-      // Fallback: just proceed
-      setIsAutoplayBlocked(false)
-      setErrorMessage('Audio may be limited - continue anyway')
-      setStatus('ready')
-      
-      if (autoStart) {
-        setTimeout(() => {
-          onTranscript('__voice_init__')
-        }, 200)
-      }
-    }
-  }, [autoStart, onTranscript])
-
-  // Initialize on mount
-  useEffect(() => {
-    // Use global flag to prevent duplicate initialization
-    if ((window as any).__bubbeAutoStarted) {
-      console.log('[VoiceManager] ⚠️ Already auto-started, skipping...')
-      return
-    }
-    
-    if (!initializeAttempted.current && autoStart) {
-      initializeAttempted.current = true;
-      (window as any).__bubbeAutoStarted = true; // Set global flag
-      
-      console.log('[VoiceManager] ⚡ AUTO-START: Initializing voice chat...')
-      
-      const initialize = async () => {
-        try {
-          // Initialize speech recognition
-          const initialized = initializeSpeechRecognition()
-          
-          if (initialized) {
-            console.log('[VoiceManager] ✅ Speech recognition initialized')
-            setStatus('ready')
-            
-            // FORCE Bubbe greeting with multiple attempts
-            console.log('[VoiceManager] 🚀 FORCING BUBBE GREETING WITH RETRIES!')
-            
-            // First attempt - immediate
-            onTranscript('__voice_init__')
-            
-            // Second attempt - after 500ms
-            setTimeout(() => {
-              if (!lastResponseRef.current) {
-                console.log('[VoiceManager] 🔄 Retry 1: Triggering greeting...')
-                onTranscript('__voice_init__')
-              }
-            }, 500)
-            
-            // Third attempt - after 1s
-            setTimeout(() => {
-              if (!lastResponseRef.current) {
-                console.log('[VoiceManager] 🔄 Retry 2: Forcing greeting...')
-                onTranscript('__voice_init__')
-              }
-            }, 1000)
-            
-            // Fourth attempt - after 2s
-            setTimeout(() => {
-              if (!lastResponseRef.current) {
-                console.log('[VoiceManager] 🔄 Retry 3: Last attempt greeting...')
-                onTranscript('__voice_init__')
-              }
-            }, 2000)
-          } else {
-            setStatus('error')
-            setErrorMessage('Voice recognition not supported')
-          }
-        } catch (error: any) {
-          console.error('[VoiceManager] ❌ Initialization failed:', error)
-          setStatus('error')
-          setErrorMessage('Failed to initialize voice features')
-        }
-      }
-      
-      initialize()
-    }
-  }, [autoStart, initializeSpeechRecognition, onTranscript])
-
-  // Clean up on unmount
-  useEffect(() => {
-    return () => {
-      if (recognitionRef.current) {
-        try {
-          recognitionRef.current.stop()
-        } catch (e) {}
-      }
-      if (audioRef.current) {
-        audioRef.current.pause()
-        audioRef.current = null
-      }
-    }
-  }, [])
-
-  return (
-    <div className="voice-manager-mobile">
-      {/* Pause/Resume button - Mobile optimized */}
-      <div className="pause-resume-container">
-        <button
-          onClick={isPaused ? resumeConversation : pauseConversation}
-          className={`pause-resume-button ${isPaused ? 'paused' : 'active'}`}
-          aria-label={isPaused ? 'Resume conversation' : 'Pause conversation'}
-        >
-          {isPaused ? (
-            <>
-              <span className="pause-icon">▶️</span>
-              <span className="pause-text">Resume</span>
-            </>
-          ) : (
-            <>
-              <span className="pause-icon">⏸️</span>
-              <span className="pause-text">Pause</span>
-            </>
-          )}
-        </button>
-      </div>
-
-      {/* Status indicator - Mobile optimized */}
-      <div className="mobile-voice-status">
-        <div className="mobile-voice-indicator">
-          {isPaused ? (
-            <>
-              <div className="mobile-status-dot mobile-status-paused" />
-              <span className="mobile-status-text">Conversation Paused</span>
-            </>
-          ) : (
-            <>
-              {status === 'listening' && (
-                <>
-                  <div className="mobile-status-dot mobile-status-listening" />
-                  <span className="mobile-status-text">Listening...</span>
-                </>
-              )}
-              {status === 'processing' && (
-                <>
-                  <div className="mobile-status-dot mobile-status-processing" />
-                  <span className="mobile-status-text">Processing...</span>
-                </>
-              )}
-              {status === 'speaking' && (
-                <>
-                  <div className="mobile-status-dot mobile-status-speaking" />
-                  <span className="mobile-status-text">Bubbe is talking...</span>
-                </>
-              )}
-              {status === 'ready' && (
-                <>
-                  <div className="mobile-status-dot mobile-status-ready" />
-                  <span className="mobile-status-text">Ready</span>
-                </>
-              )}
-              {status === 'error' && (
-                <>
-                  <div className="mobile-status-dot mobile-status-error" />
-                  <span className="mobile-status-text-error">{errorMessage}</span>
-                </>
-              )}
-            </>
-          )}
-        </div>
-      </div>
-
-      {/* Transcript display - Mobile optimized */}
-      {transcript && (
-        <div className="mobile-transcript">
-          <p className="mobile-transcript-text">
-            <span className="mobile-transcript-label">You:</span> {transcript}
-          </p>
-        </div>
-      )}
-
-      {/* Pause/Resume Button */}
-      <div className="voice-control-container">
-        <button
-          onClick={isPaused ? resumeConversation : pauseConversation}
-          className="voice-control-button"
-          style={{ 
-            minHeight: '44px', 
-            minWidth: '120px',
-            padding: '8px 16px',
-            backgroundColor: isPaused ? '#22c55e' : '#ef4444',
-            color: 'white',
-            border: 'none',
-            borderRadius: '8px',
-            fontSize: '14px',
-            fontWeight: 'bold',
-            display: 'flex',
-            alignItems: 'center',
-            justifyContent: 'center',
-            gap: '6px',
-            cursor: 'pointer',
-            zIndex: '10'
-          }}
-        >
-          <span style={{ fontSize: '16px' }}>
-            {isPaused ? '▶️' : '⏸️'}
-          </span>
-          <span>{isPaused ? 'Resume' : 'Pause'}</span>
-        </button>
-      </div>
-
-      {/* Mobile Start Overlay - Shows when autoplay is blocked */}
-      <MobileStartOverlay 
-        show={status === 'permission-required' || isAutoplayBlocked}
-        onStart={unlockAutoplay}
-      />
-
-      {/* Debug controls - Hidden on mobile, visible on desktop */}
-      {!isMobile && (
-        <div className="mobile-debug-controls">
-          <button
-            onClick={() => startListening()}
-            className="mobile-debug-button mobile-debug-start"
-            disabled={status === 'listening'}
-          >
-            🎤
-          </button>
-          <button
-            onClick={() => stopListening()}
-            className="mobile-debug-button mobile-debug-stop"
-            disabled={status !== 'listening'}
-          >
-            ⏹️
-          </button>
-          <button
-            onClick={async () => {
-              console.log('🔊 TEST VOICE BUTTON CLICKED!')
-              await playBubbeVoice('Oy vey, you want to test my voice? What am I, a machine? Fine, here is your test, schmuck!')
-            }}
-            className="mobile-debug-button mobile-debug-test"
-          >
-            🔊
-          </button>
-        </div>
-      )}
-    </div>
-  )
-}
\ No newline at end of file

← f3aff35 refactor(bubbe): typed SpeechRecognition shim + drop dead wi  ·  back to Dear Bubbe Nextjs  ·  fix(bubbe/mobile): create SpeechRecognition once + latest-re 557b99d →