← back to Dear Bubbe Nextjs

components/BubbeChatModal.tsx

276 lines

'use client'

import { useState, useEffect, useRef } from 'react'
import Image from 'next/image'

interface BubbeChatModalProps {
  isOpen: boolean
  onClose: () => void
  onSendMessage: (message: string) => void
  messages: Array<{
    role: 'user' | 'assistant' | 'system'
    content: string
    timestamp: Date
  }>
  isListening: boolean
  isBubbeSpeaking: boolean
  transcript: string
  onStartListening: () => void
  onStopListening: () => void
  voiceEnabled: boolean
  onToggleVoice: () => void
}

export default function BubbeChatModal({
  isOpen,
  onClose,
  onSendMessage,
  messages,
  isListening,
  isBubbeSpeaking,
  transcript,
  onStartListening,
  onStopListening,
  voiceEnabled,
  onToggleVoice
}: BubbeChatModalProps) {
  const [input, setInput] = useState('')
  const messagesEndRef = useRef<HTMLDivElement>(null)
  const inputRef = useRef<HTMLTextAreaElement>(null)

  // Auto-scroll to bottom when new messages arrive
  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
  }, [messages])

  // Focus input when modal opens
  useEffect(() => {
    if (isOpen) {
      setTimeout(() => {
        inputRef.current?.focus()
      }, 100)
    }
  }, [isOpen])

  // Listen for voice messages from speech recognition
  useEffect(() => {
    const handleChatModalSendMessage = (event: CustomEvent) => {
      const { message } = event.detail
      if (message && message.trim()) {
        console.log('[CHAT_MODAL] Received voice message:', message)
        onSendMessage(message.trim())
      }
    }

    if (isOpen) {
      window.addEventListener('chatModalSendMessage', handleChatModalSendMessage as EventListener)
    }

    return () => {
      window.removeEventListener('chatModalSendMessage', handleChatModalSendMessage as EventListener)
    }
  }, [isOpen, onSendMessage])

  const handleSend = () => {
    if (input.trim()) {
      onSendMessage(input.trim())
      setInput('')
    }
  }

  const handleKeyPress = (e: React.KeyboardEvent) => {
    if (e.key === 'Enter' && !e.shiftKey) {
      e.preventDefault()
      handleSend()
    }
  }

  if (!isOpen) return null

  return (
    <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4 md:p-4 sm:p-2">
      <div className="bg-white rounded-2xl max-w-4xl w-full max-h-[85vh] flex flex-col shadow-2xl md:rounded-2xl sm:rounded-lg md:max-h-[85vh] sm:max-h-[95vh] sm:h-full">
        {/* Header with Logo */}
        <div className="bg-gradient-to-r from-amber-500 to-orange-600 p-4 rounded-t-2xl md:rounded-t-2xl sm:rounded-t-lg md:p-4 sm:p-3">
          <div className="flex items-center justify-between">
            <div className="flex items-center gap-3 md:gap-3 sm:gap-2">
              {/* Bubbe Logo/Avatar */}
              <div className="w-16 h-16 md:w-16 md:h-16 sm:w-12 sm:h-12 bg-white rounded-full flex items-center justify-center shadow-lg">
                <span className="text-3xl md:text-3xl sm:text-2xl">👵</span>
              </div>
              <div>
                <h2 className="text-2xl md:text-2xl sm:text-xl font-bold text-white">Bubbe Chat</h2>
                <p className="text-white/90 text-sm md:text-sm sm:text-xs">Your AI Jewish Grandmother</p>
              </div>
            </div>
            
            {/* Controls */}
            <div className="flex items-center gap-2 md:gap-2 sm:gap-1">
              {/* Voice Toggle */}
              <button
                onClick={onToggleVoice}
                className={`px-3 py-2 md:px-3 md:py-2 sm:px-2 sm:py-2 min-w-[44px] min-h-[44px] rounded-lg transition-colors touch-manipulation ${
                  voiceEnabled 
                    ? 'bg-green-500 text-white hover:bg-green-600 active:bg-green-700' 
                    : 'bg-gray-300 text-gray-700 hover:bg-gray-400 active:bg-gray-500'
                }`}
                title={voiceEnabled ? 'Voice On' : 'Voice Off'}
              >
                <span className="text-lg md:text-base sm:text-sm">{voiceEnabled ? '🔊' : '🔇'}</span>
              </button>
              
              {/* Close Button */}
              <button
                onClick={onClose}
                className="text-white hover:text-gray-200 active:text-gray-300 transition-colors min-w-[44px] min-h-[44px] flex items-center justify-center touch-manipulation"
                aria-label="Close"
              >
                <svg className="w-6 h-6 md:w-6 md:h-6 sm:w-5 sm:h-5" 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>
          </div>
        </div>

        {/* Status Bar */}
        {(isListening || isBubbeSpeaking) && (
          <div className="bg-amber-100 px-4 py-2 border-b border-amber-200">
            <div className="flex items-center gap-2">
              {isBubbeSpeaking && (
                <div className="flex items-center gap-2">
                  <div className="flex gap-1">
                    <div className="w-2 h-2 bg-amber-600 rounded-full animate-pulse"></div>
                    <div className="w-2 h-2 bg-amber-600 rounded-full animate-pulse delay-75"></div>
                    <div className="w-2 h-2 bg-amber-600 rounded-full animate-pulse delay-150"></div>
                  </div>
                  <span className="text-sm text-amber-800">Bubbe is speaking...</span>
                </div>
              )}
              {isListening && !isBubbeSpeaking && (
                <div className="flex items-center gap-2">
                  <div className="w-3 h-3 bg-red-500 rounded-full animate-pulse"></div>
                  <span className="text-sm text-red-800">Listening...</span>
                  {transcript && (
                    <span className="text-sm text-gray-600 italic">{transcript}</span>
                  )}
                </div>
              )}
            </div>
          </div>
        )}

        {/* Messages */}
        <div className="flex-1 overflow-y-auto p-4 md:p-4 sm:p-3 space-y-3 md:space-y-3 sm:space-y-2">
          {messages.map((message, index) => (
            <div
              key={index}
              className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
            >
              {message.role === 'assistant' && (
                <div className="w-10 h-10 md:w-10 md:h-10 sm:w-8 sm:h-8 bg-amber-200 rounded-full flex items-center justify-center mr-2 flex-shrink-0">
                  <span className="text-xl md:text-xl sm:text-lg">👵</span>
                </div>
              )}
              <div
                className={`max-w-[70%] md:max-w-[70%] sm:max-w-[80%] p-3 md:p-3 sm:p-2 rounded-lg ${
                  message.role === 'user'
                    ? 'bg-blue-500 text-white'
                    : message.role === 'system'
                    ? 'bg-gray-200 text-gray-700 italic'
                    : 'bg-gray-100 text-gray-800'
                }`}
              >
                <p className="whitespace-pre-wrap">{message.content}</p>
                <p className="text-xs opacity-70 mt-1">
                  {new Date(message.timestamp).toLocaleTimeString()}
                </p>
              </div>
              {message.role === 'user' && (
                <div className="w-10 h-10 md:w-10 md:h-10 sm:w-8 sm:h-8 bg-blue-200 rounded-full flex items-center justify-center ml-2 flex-shrink-0">
                  <span className="text-xl md:text-xl sm:text-lg">👤</span>
                </div>
              )}
            </div>
          ))}
          <div ref={messagesEndRef} />
        </div>

        {/* Input Area */}
        <div className="border-t border-gray-200 p-4 md:p-4 sm:p-3 bg-gray-50 rounded-b-2xl md:rounded-b-2xl sm:rounded-b-lg">
          <div className="flex gap-2 md:gap-2 sm:gap-1">
            {/* Voice Input Button */}
            {voiceEnabled && (
              <button
                onClick={isListening ? onStopListening : onStartListening}
                className={`px-4 py-2 md:px-4 md:py-2 sm:px-3 sm:py-2 min-w-[44px] min-h-[44px] rounded-lg transition-colors touch-manipulation ${
                  isListening
                    ? 'bg-red-500 text-white hover:bg-red-600 active:bg-red-700'
                    : 'bg-amber-500 text-white hover:bg-amber-600 active:bg-amber-700'
                }`}
                disabled={isBubbeSpeaking}
              >
                <span className="text-sm md:text-sm sm:text-xs">{isListening ? '🛑 Stop' : '🎤 Talk'}</span>
              </button>
            )}
            
            {/* Text Input */}
            <textarea
              ref={inputRef}
              value={input}
              onChange={(e) => setInput(e.target.value)}
              onKeyDown={handleKeyPress}
              placeholder="Type your message to Bubbe..."
              className="flex-1 px-4 py-2 md:px-4 md:py-2 sm:px-3 sm:py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-500 resize-none text-base"
              style={{ fontSize: '16px', WebkitAppearance: 'none' }}
              rows={1}
              disabled={isListening || isBubbeSpeaking}
              autoComplete="off"
              autoCorrect="on"
              autoCapitalize="sentences"
              spellCheck="true"
              enterKeyHint="send"
            />
            
            {/* Send Button */}
            <button
              onClick={handleSend}
              disabled={!input.trim() || isListening || isBubbeSpeaking}
              className="px-4 py-2 md:px-4 md:py-2 sm:px-3 sm:py-2 min-w-[44px] min-h-[44px] bg-amber-500 text-white rounded-lg hover:bg-amber-600 active:bg-amber-700 disabled:bg-gray-300 disabled:cursor-not-allowed transition-colors touch-manipulation"
            >
              <span className="text-sm md:text-sm sm:text-xs">Send</span>
            </button>
          </div>
          
          {/* Quick Actions */}
          <div className="mt-2 flex gap-2 md:gap-2 sm:gap-1 flex-wrap">
            <button
              onClick={() => onSendMessage("Tell me about the weather")}
              className="text-xs md:text-xs sm:text-xs px-3 py-1 md:px-3 md:py-1 sm:px-2 sm:py-1 min-h-[36px] bg-gray-200 text-gray-700 rounded-full hover:bg-gray-300 active:bg-gray-400 transition-colors touch-manipulation"
            >
              Weather
            </button>
            <button
              onClick={() => onSendMessage("Give me some news")}
              className="text-xs md:text-xs sm:text-xs px-3 py-1 md:px-3 md:py-1 sm:px-2 sm:py-1 min-h-[36px] bg-gray-200 text-gray-700 rounded-full hover:bg-gray-300 active:bg-gray-400 transition-colors touch-manipulation"
            >
              News
            </button>
            <button
              onClick={() => onSendMessage("I need advice")}
              className="text-xs md:text-xs sm:text-xs px-3 py-1 md:px-3 md:py-1 sm:px-2 sm:py-1 min-h-[36px] bg-gray-200 text-gray-700 rounded-full hover:bg-gray-300 active:bg-gray-400 transition-colors touch-manipulation"
            >
              Advice
            </button>
            <button
              onClick={() => onSendMessage("Tell me a joke")}
              className="text-xs md:text-xs sm:text-xs px-3 py-1 md:px-3 md:py-1 sm:px-2 sm:py-1 min-h-[36px] bg-gray-200 text-gray-700 rounded-full hover:bg-gray-300 active:bg-gray-400 transition-colors touch-manipulation"
            >
              Joke
            </button>
          </div>
        </div>
      </div>
    </div>
  )
}