← back to Dear Bubbe Nextjs
auto-save: 2026-07-16T17:43:55 (3 files) — app/api/social-post/route.ts components/SimpleConversation.tsx components/SimplifiedChat.tsx
0dbdfa0019c3caf0a3aebac2baabac433e23c3a3 · 2026-07-16 17:43:58 -0700 · Steve Abrams
Files touched
M app/api/social-post/route.tsD components/SimpleConversation.tsxD components/SimplifiedChat.tsx
Diff
commit 0dbdfa0019c3caf0a3aebac2baabac433e23c3a3
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Jul 16 17:43:58 2026 -0700
auto-save: 2026-07-16T17:43:55 (3 files) — app/api/social-post/route.ts components/SimpleConversation.tsx components/SimplifiedChat.tsx
---
app/api/social-post/route.ts | 7 +-
components/SimpleConversation.tsx | 371 ------------------------------
components/SimplifiedChat.tsx | 461 --------------------------------------
3 files changed, 5 insertions(+), 834 deletions(-)
diff --git a/app/api/social-post/route.ts b/app/api/social-post/route.ts
index a55a468..ad9085e 100644
--- a/app/api/social-post/route.ts
+++ b/app/api/social-post/route.ts
@@ -39,8 +39,11 @@ export async function POST(request: NextRequest) {
console.log('[SOCIAL-POST] User message:', userMessage.substring(0, 50))
console.log('[SOCIAL-POST] Bubbe response:', bubbeResponse.substring(0, 50))
- // Save to chat history for the auto-poster to process
- const chatHistoryPath = '/root/Projects/dear-bubbe-nextjs/logs/chat-history.json'
+ // Save to chat history for the auto-poster to process.
+ // Use cwd (not a hardcoded absolute path) so it works on any host, and
+ // ensure the logs/ dir exists — writeFile can't create parent dirs (was ENOENT).
+ const chatHistoryPath = path.join(process.cwd(), 'logs', 'chat-history.json')
+ await fs.mkdir(path.dirname(chatHistoryPath), { recursive: true })
let chats = []
try {
diff --git a/components/SimpleConversation.tsx b/components/SimpleConversation.tsx
deleted file mode 100644
index c04d07b..0000000
--- a/components/SimpleConversation.tsx
+++ /dev/null
@@ -1,371 +0,0 @@
-'use client'
-
-import { useState, useEffect, useRef } from 'react'
-import { Mic, MessageSquare, Send, Volume2, VolumeX } from 'lucide-react'
-
-interface Message {
- id: string
- role: 'user' | 'assistant'
- content: string
- timestamp: Date
-}
-
-interface SimpleConversationProps {
- currentMode: string
- className?: string
-}
-
-export default function SimpleConversation({ currentMode, className = '' }: SimpleConversationProps) {
- const [messages, setMessages] = useState<Message[]>([])
- const [isListening, setIsListening] = useState(false)
- const [isSpeaking, setIsSpeaking] = useState(false)
- const [isLoading, setIsLoading] = useState(false)
- const [textInput, setTextInput] = useState('')
- const [voiceEnabled, setVoiceEnabled] = useState(true)
- const [conversationStarted, setConversationStarted] = useState(false)
- const [inputMethod, setInputMethod] = useState<'text' | 'voice' | null>(null)
-
- const messagesEndRef = useRef<HTMLDivElement>(null)
- const recognitionRef = useRef<any>(null)
- const synthRef = useRef<SpeechSynthesis | null>(null)
-
- useEffect(() => {
- messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
- }, [messages])
-
- useEffect(() => {
- // Initialize speech synthesis
- if (typeof window !== 'undefined' && 'speechSynthesis' in window) {
- synthRef.current = window.speechSynthesis
- }
-
- // Initialize speech recognition
- if (typeof window !== 'undefined' && ('SpeechRecognition' in window || 'webkitSpeechRecognition' in window)) {
- const SpeechRecognition = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition
- recognitionRef.current = new SpeechRecognition()
- recognitionRef.current.continuous = false
- recognitionRef.current.interimResults = false
- recognitionRef.current.lang = 'en-US'
-
- recognitionRef.current.onresult = (event: any) => {
- const transcript = event.results[0][0].transcript
- handleUserMessage(transcript, 'voice')
- setIsListening(false)
- }
-
- recognitionRef.current.onerror = (event: any) => {
- console.error('Speech recognition error:', event.error)
- setIsListening(false)
- if (event.error === 'not-allowed') {
- alert('Microphone permission denied. Please allow microphone access.')
- }
- }
-
- recognitionRef.current.onend = () => {
- setIsListening(false)
- }
- }
-
- return () => {
- if (recognitionRef.current) {
- recognitionRef.current.abort()
- }
- if (synthRef.current) {
- synthRef.current.cancel()
- }
- }
- }, [])
-
- const handleUserMessage = async (content: string, source: 'voice' | 'text') => {
- if (!content.trim()) return
-
- // Add user message
- const userMessage: Message = {
- id: Date.now().toString(),
- role: 'user',
- content: content.trim(),
- timestamp: new Date()
- }
- setMessages(prev => [...prev, userMessage])
- setTextInput('')
- setIsLoading(true)
-
- try {
- // Call API
- const response = await fetch('/api/chat', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- message: content.trim(),
- mode: currentMode,
- conversationHistory: messages.slice(-5)
- })
- })
-
- if (!response.ok) throw new Error('Failed to get response')
-
- const data = await response.json()
-
- // Add assistant message
- const assistantMessage: Message = {
- id: (Date.now() + 1).toString(),
- role: 'assistant',
- content: data.response,
- timestamp: new Date()
- }
- setMessages(prev => [...prev, assistantMessage])
-
- // Speak response if voice is enabled
- if (voiceEnabled && synthRef.current) {
- speakText(data.response)
- }
-
- // If using voice, automatically start listening again after speaking
- if (source === 'voice' && inputMethod === 'voice') {
- setTimeout(() => {
- startListening()
- }, 500)
- }
-
- } catch (error) {
- console.error('Error:', error)
- const errorMessage: Message = {
- id: (Date.now() + 1).toString(),
- role: 'assistant',
- content: "Oy vey! Something's meshuga with the computer. Try again!",
- timestamp: new Date()
- }
- setMessages(prev => [...prev, errorMessage])
- } finally {
- setIsLoading(false)
- }
- }
-
- const speakText = (text: string) => {
- if (!synthRef.current || !voiceEnabled) return
-
- synthRef.current.cancel()
- setIsSpeaking(true)
-
- const utterance = new SpeechSynthesisUtterance(text)
- utterance.rate = 0.9
- utterance.pitch = 1.1
- utterance.onend = () => {
- setIsSpeaking(false)
- // Auto-listen after speaking if in voice mode
- if (inputMethod === 'voice') {
- setTimeout(() => startListening(), 300)
- }
- }
-
- synthRef.current.speak(utterance)
- }
-
- const startListening = () => {
- if (!recognitionRef.current || isListening) return
-
- try {
- setIsListening(true)
- recognitionRef.current.start()
- } catch (error: any) {
- console.error('Error starting recognition:', error)
- setIsListening(false)
-
- if (error.message?.includes('already started')) {
- recognitionRef.current.abort()
- setTimeout(() => {
- try {
- setIsListening(true)
- recognitionRef.current.start()
- } catch (e) {
- console.error('Error restarting:', e)
- setIsListening(false)
- }
- }, 100)
- }
- }
- }
-
- const stopListening = () => {
- if (recognitionRef.current) {
- recognitionRef.current.abort()
- }
- setIsListening(false)
- }
-
- const handleMethodSelect = (method: 'text' | 'voice') => {
- setInputMethod(method)
- setConversationStarted(true)
-
- if (method === 'voice') {
- // Start listening immediately for voice
- setTimeout(() => startListening(), 500)
- }
- }
-
- // Initial selection screen
- if (!conversationStarted) {
- return (
- <div className="flex flex-col items-center justify-center h-full p-4 sm:p-8">
- <div className="text-5xl sm:text-4xl mb-4 sm:mb-6">🥯</div>
- <h2 className="text-xl sm:text-2xl font-bold text-white mb-2 text-center">Nu, how do you want to kvetch?</h2>
- <p className="text-white/70 mb-6 sm:mb-8 text-center">Choose how to talk to Bubbe</p>
-
- <div className="grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4 w-full max-w-md">
- <button
- onClick={() => handleMethodSelect('text')}
- className="p-5 sm:p-6 bg-white/10 hover:bg-white/20 active:bg-white/30 rounded-xl border border-white/20 transition-all hover:scale-105 active:scale-95 group touch-manipulation"
- style={{ WebkitTapHighlightColor: 'transparent' }}
- >
- <MessageSquare className="w-10 h-10 sm:w-12 sm:h-12 text-white mb-2 sm:mb-3 mx-auto" />
- <h3 className="text-base sm:text-lg font-semibold text-white mb-1">Type</h3>
- <p className="text-xs sm:text-sm text-white/70">Type your messages</p>
- </button>
-
- <button
- onClick={() => handleMethodSelect('voice')}
- className="p-5 sm:p-6 bg-white/10 hover:bg-white/20 active:bg-white/30 rounded-xl border border-white/20 transition-all hover:scale-105 active:scale-95 group touch-manipulation"
- style={{ WebkitTapHighlightColor: 'transparent' }}
- >
- <Mic className="w-10 h-10 sm:w-12 sm:h-12 text-white mb-2 sm:mb-3 mx-auto" />
- <h3 className="text-base sm:text-lg font-semibold text-white mb-1">Talk</h3>
- <p className="text-xs sm:text-sm text-white/70">Have a conversation</p>
- </button>
- </div>
- </div>
- )
- }
-
- // Conversation interface
- return (
- <div className={`flex flex-col h-full ${className}`}>
- {/* Header */}
- <div className="p-3 sm:p-4 bg-black/20 border-b border-white/10">
- <div className="flex items-center justify-between">
- <div className="flex items-center space-x-2 sm:space-x-3">
- <span className="text-xl sm:text-2xl">🥯</span>
- <div>
- <h3 className="text-white font-semibold text-sm sm:text-base">Dear Bubbe</h3>
- <p className="text-white/60 text-xs sm:text-sm">
- {inputMethod === 'voice' ? 'Speaking mode' : 'Typing mode'}
- </p>
- </div>
- </div>
-
- <button
- onClick={() => setVoiceEnabled(!voiceEnabled)}
- className={`p-2 rounded-lg transition-colors touch-manipulation ${
- voiceEnabled ? 'bg-green-600 hover:bg-green-700 active:bg-green-800' : 'bg-gray-600 hover:bg-gray-700 active:bg-gray-800'
- }`}
- style={{ WebkitTapHighlightColor: 'transparent' }}
- >
- {voiceEnabled ? <Volume2 className="w-4 h-4 sm:w-5 sm:h-5 text-white" /> : <VolumeX className="w-4 h-4 sm:w-5 sm:h-5 text-white" />}
- </button>
- </div>
- </div>
-
- {/* Messages */}
- <div className="flex-1 overflow-y-auto p-3 sm:p-4 space-y-3 sm:space-y-4 -webkit-overflow-scrolling-touch">
- {messages.length === 0 ? (
- <div className="text-center text-white/60 mt-6 sm:mt-8 px-4">
- <p className="text-base sm:text-lg mb-2">
- {inputMethod === 'voice'
- ? "I'm listening... Nu, what's on your mind?"
- : "Type already! What are you waiting for, an invitation?"}
- </p>
- </div>
- ) : (
- messages.map((message) => (
- <div
- key={message.id}
- className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
- >
- <div
- className={`max-w-[85%] sm:max-w-[80%] rounded-2xl px-3 sm:px-4 py-2 sm:py-3 ${
- message.role === 'user'
- ? 'bg-purple-600 text-white'
- : 'bg-white/20 text-white'
- }`}
- >
- <p className="text-sm sm:text-sm leading-relaxed break-words">{message.content}</p>
- <p className="text-xs opacity-70 mt-1">
- {message.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
- </p>
- </div>
- </div>
- ))
- )}
- {isLoading && (
- <div className="flex justify-start">
- <div className="bg-white/20 rounded-2xl px-4 py-3">
- <div className="flex space-x-2">
- <div className="w-2 h-2 bg-white/60 rounded-full animate-bounce" />
- <div className="w-2 h-2 bg-white/60 rounded-full animate-bounce" style={{ animationDelay: '0.1s' }} />
- <div className="w-2 h-2 bg-white/60 rounded-full animate-bounce" style={{ animationDelay: '0.2s' }} />
- </div>
- </div>
- </div>
- )}
- <div ref={messagesEndRef} />
- </div>
-
- {/* Input area */}
- <div className="p-3 sm:p-4 bg-black/20 border-t border-white/10 safe-area-inset-bottom">
- {inputMethod === 'text' ? (
- // Text input
- <div className="flex space-x-2 sm:space-x-3">
- <input
- type="text"
- value={textInput}
- onChange={(e) => setTextInput(e.target.value)}
- onKeyPress={(e) => {
- if (e.key === 'Enter' && !e.shiftKey) {
- e.preventDefault()
- if (textInput.trim()) {
- handleUserMessage(textInput, 'text')
- }
- }
- }}
- placeholder="Type your message..."
- className="flex-1 bg-white/10 border border-white/20 rounded-lg px-3 sm:px-4 py-2.5 sm:py-3 text-white placeholder-white/50 focus:outline-none focus:border-white/40 text-base"
- style={{ fontSize: '16px' }} // Prevent iOS zoom on focus
- autoComplete="off"
- autoCorrect="on"
- autoCapitalize="sentences"
- />
- <button
- onClick={() => textInput.trim() && handleUserMessage(textInput, 'text')}
- disabled={!textInput.trim() || isLoading}
- className="p-2.5 sm:p-3 bg-purple-600 hover:bg-purple-700 active:bg-purple-800 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed transition-colors touch-manipulation"
- style={{ WebkitTapHighlightColor: 'transparent' }}
- >
- <Send className="w-5 h-5 text-white" />
- </button>
- </div>
- ) : (
- // Voice controls
- <div className="flex flex-col items-center py-2">
- <button
- onClick={isListening ? stopListening : startListening}
- disabled={isLoading || isSpeaking}
- className={`p-5 sm:p-6 rounded-full transition-all transform touch-manipulation ${
- isListening
- ? 'bg-red-600 hover:bg-red-700 active:bg-red-800 scale-110 animate-pulse'
- : 'bg-purple-600 hover:bg-purple-700 active:bg-purple-800 hover:scale-105 active:scale-95'
- } disabled:opacity-50 disabled:cursor-not-allowed`}
- style={{ WebkitTapHighlightColor: 'transparent' }}
- >
- <Mic className="w-7 h-7 sm:w-8 sm:h-8 text-white" />
- </button>
- <p className="text-white/70 mt-2 sm:mt-3 text-sm">
- {isListening ? 'Listening...' :
- isSpeaking ? 'Bubbe is talking...' :
- isLoading ? 'Thinking...' :
- 'Tap to speak'}
- </p>
- </div>
- )}
- </div>
- </div>
- )
-}
\ No newline at end of file
diff --git a/components/SimplifiedChat.tsx b/components/SimplifiedChat.tsx
deleted file mode 100644
index 72e6b2c..0000000
--- a/components/SimplifiedChat.tsx
+++ /dev/null
@@ -1,461 +0,0 @@
-'use client'
-
-import { useState, useEffect, useRef } from 'react'
-import { Mic, MicOff, Send, Volume2, VolumeX } from 'lucide-react'
-
-interface Message {
- id: string
- role: 'user' | 'assistant'
- content: string
- timestamp: Date
-}
-
-interface SimplifiedChatProps {
- currentMode: string
- className?: string
-}
-
-export default function SimplifiedChat({ currentMode, className = '' }: SimplifiedChatProps) {
- const [messages, setMessages] = useState<Message[]>([])
- const [isListening, setIsListening] = useState(false)
- const [isSpeaking, setIsSpeaking] = useState(false)
- const [isLoading, setIsLoading] = useState(false)
- const [textInput, setTextInput] = useState('')
- const [voiceEnabled, setVoiceEnabled] = useState(true)
- const [inputMode, setInputMode] = useState<'voice' | 'text'>('voice')
- const messagesEndRef = useRef<HTMLDivElement>(null)
- const recognitionRef = useRef<any>(null)
- const synthRef = useRef<SpeechSynthesis | null>(null)
-
- useEffect(() => {
- messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
- }, [messages])
-
- useEffect(() => {
- // Initialize speech synthesis
- if (typeof window !== 'undefined' && 'speechSynthesis' in window) {
- synthRef.current = window.speechSynthesis
- }
-
- // Initialize speech recognition
- if (typeof window !== 'undefined' && ('SpeechRecognition' in window || 'webkitSpeechRecognition' in window)) {
- const SpeechRecognition = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition
- recognitionRef.current = new SpeechRecognition()
- recognitionRef.current.continuous = false
- recognitionRef.current.interimResults = false
- recognitionRef.current.lang = 'en-US'
-
- recognitionRef.current.onresult = (event: any) => {
- const transcript = event.results[0][0].transcript
- handleUserMessage(transcript, 'voice')
- setIsListening(false)
- }
-
- recognitionRef.current.onerror = (event: any) => {
- console.error('Speech recognition error:', event.error)
- setIsListening(false)
- // Show user-friendly error message
- if (event.error === 'not-allowed') {
- alert('Microphone permission denied. Please allow microphone access and try again.')
- } else if (event.error === 'no-speech') {
- // Silently handle no-speech errors
- } else {
- console.log('Speech recognition error:', event.error)
- }
- }
-
- recognitionRef.current.onend = () => {
- setIsListening(false)
- }
- }
-
- return () => {
- if (recognitionRef.current) {
- recognitionRef.current.abort()
- }
- if (synthRef.current) {
- synthRef.current.cancel()
- }
- }
- }, [])
-
- const handleUserMessage = async (content: string, source: 'voice' | 'text') => {
- if (!content.trim()) return
-
- // Add user message
- const userMessage: Message = {
- id: Date.now().toString(),
- role: 'user',
- content: content.trim(),
- timestamp: new Date()
- }
- setMessages(prev => [...prev, userMessage])
- setTextInput('')
- setIsLoading(true)
-
- try {
- // Call API with current mode
- const response = await fetch('/api/chat', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- message: content.trim(),
- mode: currentMode,
- conversationHistory: messages.slice(-5) // Send last 5 messages for context
- })
- })
-
- if (!response.ok) throw new Error('Failed to get response')
-
- const data = await response.json()
-
- // Add assistant message
- const assistantMessage: Message = {
- id: (Date.now() + 1).toString(),
- role: 'assistant',
- content: data.response,
- timestamp: new Date()
- }
- setMessages(prev => [...prev, assistantMessage])
-
- // Speak response if voice is enabled
- if (voiceEnabled && synthRef.current) {
- speakText(data.response)
- }
-
- } catch (error) {
- console.error('Error getting response:', error)
- const errorMessage: Message = {
- id: (Date.now() + 1).toString(),
- role: 'assistant',
- content: "Oy vey! Something's wrong with the schmuck computer. Try again!",
- timestamp: new Date()
- }
- setMessages(prev => [...prev, errorMessage])
- } finally {
- setIsLoading(false)
- }
- }
-
- const speakText = (text: string) => {
- if (!synthRef.current || !voiceEnabled) return
-
- // Cancel any ongoing speech
- synthRef.current.cancel()
- setIsSpeaking(true)
-
- const utterance = new SpeechSynthesisUtterance(text)
- utterance.rate = 0.9
- utterance.pitch = 1.1
- utterance.volume = 0.8
-
- utterance.onend = () => {
- setIsSpeaking(false)
- }
-
- utterance.onerror = () => {
- setIsSpeaking(false)
- }
-
- synthRef.current.speak(utterance)
- }
-
- const startListening = async () => {
- if (!recognitionRef.current) {
- alert('Speech recognition not supported in your browser. Please use Chrome, Edge, or Safari.')
- return
- }
-
- if (isListening) return
-
- try {
- // Check for microphone permissions with proper fallback
- if (navigator && navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
- try {
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
- // Stop the stream immediately as we just needed to check permissions
- stream.getTracks().forEach(track => track.stop())
- } catch (permError) {
- console.log('Microphone permission check failed:', permError)
- // Continue anyway - the speech recognition API will handle permissions
- }
- }
-
- setIsListening(true)
- recognitionRef.current.start()
- } catch (error: any) {
- console.error('Error starting speech recognition:', error)
- setIsListening(false)
-
- if (error.name === 'NotAllowedError' || error.name === 'PermissionDeniedError') {
- alert('Microphone permission denied. Please allow microphone access and try again.')
- } else if (error.message && error.message.includes('already started')) {
- // If already started, stop and restart
- recognitionRef.current.abort()
- setTimeout(() => {
- try {
- setIsListening(true)
- recognitionRef.current.start()
- } catch (e) {
- console.error('Error restarting recognition:', e)
- setIsListening(false)
- }
- }, 100)
- } else {
- alert('Error accessing microphone. Please check your settings and try again.')
- }
- }
- }
-
- const stopListening = () => {
- if (recognitionRef.current) {
- recognitionRef.current.abort()
- }
- setIsListening(false)
- }
-
- const stopSpeaking = () => {
- if (synthRef.current) {
- synthRef.current.cancel()
- }
- setIsSpeaking(false)
- }
-
- const handleTextSubmit = () => {
- if (textInput.trim()) {
- handleUserMessage(textInput, 'text')
- }
- }
-
- const handleKeyPress = (e: React.KeyboardEvent) => {
- if (e.key === 'Enter' && !e.shiftKey) {
- e.preventDefault()
- handleTextSubmit()
- }
- }
-
- const getModeTheme = () => {
- switch (currentMode) {
- case 'bubbe':
- return {
- gradient: 'from-purple-600 to-pink-600',
- accent: 'bg-purple-500',
- userBubble: 'bg-purple-600',
- assistantBubble: 'bg-white/20'
- }
- case 'meshugana':
- return {
- gradient: 'from-orange-500 to-red-500',
- accent: 'bg-orange-500',
- userBubble: 'bg-orange-600',
- assistantBubble: 'bg-white/20'
- }
- case 'comedian':
- return {
- gradient: 'from-blue-500 to-indigo-600',
- accent: 'bg-blue-500',
- userBubble: 'bg-blue-600',
- assistantBubble: 'bg-white/20'
- }
- default:
- return {
- gradient: 'from-purple-600 to-pink-600',
- accent: 'bg-purple-500',
- userBubble: 'bg-purple-600',
- assistantBubble: 'bg-white/20'
- }
- }
- }
-
- const theme = getModeTheme()
-
- return (
- <div className={`flex flex-col h-full ${className}`}>
- {/* Messages Container */}
- <div className="flex-1 overflow-y-auto p-4 space-y-4 min-h-0">
- {messages.length === 0 ? (
- <div className="flex-1 flex items-center justify-center text-center">
- <div className="max-w-sm">
- <div className="text-4xl mb-4">
- {currentMode === 'bubbe' ? '🥯' : currentMode === 'meshugana' ? '🤪' : '🎤'}
- </div>
- <p className="text-white/80 text-lg font-medium mb-2">
- {currentMode === 'bubbe' ? 'Nu, so what\'s your question?' :
- currentMode === 'meshugana' ? 'You think I got all day?' :
- 'Ready for some laughs?'}
- </p>
- <p className="text-white/60 text-sm">
- {inputMode === 'voice' ? 'Tap the mic and start talking!' : 'Type your message below'}
- </p>
- </div>
- </div>
- ) : (
- messages.map((message) => (
- <div
- key={message.id}
- className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
- >
- <div
- className={`
- max-w-[85%] p-3 rounded-2xl
- ${message.role === 'user'
- ? `${theme.userBubble} text-white rounded-br-md`
- : `${theme.assistantBubble} text-white rounded-bl-md backdrop-blur-sm`
- }
- `}
- >
- <p className="text-sm leading-relaxed whitespace-pre-wrap">
- {message.content}
- </p>
- <p className="text-xs opacity-70 mt-1">
- {message.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
- </p>
- </div>
- </div>
- ))
- )}
-
- {/* Loading indicator */}
- {isLoading && (
- <div className="flex justify-start">
- <div className={`${theme.assistantBubble} backdrop-blur-sm p-3 rounded-2xl rounded-bl-md`}>
- <div className="flex space-x-1">
- <div className="w-2 h-2 bg-white rounded-full animate-bounce"></div>
- <div className="w-2 h-2 bg-white rounded-full animate-bounce" style={{ animationDelay: '0.1s' }}></div>
- <div className="w-2 h-2 bg-white rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
- </div>
- </div>
- </div>
- )}
-
- <div ref={messagesEndRef} />
- </div>
-
- {/* Input Controls */}
- <div className="p-4 bg-black/20 backdrop-blur-sm">
- {/* Mode Toggle */}
- <div className="flex justify-center mb-3">
- <div className="flex bg-black/30 rounded-full p-1">
- <button
- onClick={() => setInputMode('voice')}
- className={`
- px-4 py-2 rounded-full text-sm font-medium transition-all duration-200
- ${inputMode === 'voice'
- ? `bg-gradient-to-r ${theme.gradient} text-white`
- : 'text-white/70 hover:text-white'
- }
- `}
- >
- <Mic className="w-4 h-4 inline mr-2" />
- Voice
- </button>
- <button
- onClick={() => setInputMode('text')}
- className={`
- px-4 py-2 rounded-full text-sm font-medium transition-all duration-200
- ${inputMode === 'text'
- ? `bg-gradient-to-r ${theme.gradient} text-white`
- : 'text-white/70 hover:text-white'
- }
- `}
- >
- <Send className="w-4 h-4 inline mr-2" />
- Text
- </button>
- </div>
- </div>
-
- {/* Voice Controls */}
- {inputMode === 'voice' && (
- <div className="flex items-center justify-center space-x-4">
- <button
- onClick={() => setVoiceEnabled(!voiceEnabled)}
- className={`
- p-3 rounded-full transition-all duration-200
- ${voiceEnabled ? 'bg-green-600 hover:bg-green-700' : 'bg-gray-600 hover:bg-gray-700'}
- `}
- >
- {voiceEnabled ? <Volume2 className="w-5 h-5 text-white" /> : <VolumeX className="w-5 h-5 text-white" />}
- </button>
-
- <button
- onClick={isSpeaking ? stopSpeaking : (isListening ? stopListening : startListening)}
- disabled={isLoading}
- className={`
- p-6 rounded-full transition-all duration-200 transform
- ${isListening
- ? 'bg-red-600 hover:bg-red-700 scale-110 shadow-lg shadow-red-600/30'
- : isSpeaking
- ? 'bg-blue-600 hover:bg-blue-700 animate-pulse'
- : `bg-gradient-to-r ${theme.gradient} hover:scale-105`
- }
- ${isLoading ? 'opacity-50 cursor-not-allowed' : 'hover:shadow-lg'}
- disabled:hover:scale-100 disabled:hover:shadow-none
- `}
- >
- {isSpeaking ? (
- <Volume2 className="w-7 h-7 text-white" />
- ) : isListening ? (
- <div className="relative">
- <Mic className="w-7 h-7 text-white" />
- <div className="absolute inset-0 bg-red-500 rounded-full animate-ping opacity-30"></div>
- </div>
- ) : (
- <Mic className="w-7 h-7 text-white" />
- )}
- </button>
-
- <div className="text-center min-w-[80px]">
- <p className="text-white/80 text-sm font-medium">
- {isSpeaking ? 'Speaking...' :
- isListening ? 'Listening...' :
- isLoading ? 'Thinking...' :
- 'Tap to talk'}
- </p>
- </div>
- </div>
- )}
-
- {/* Text Input */}
- {inputMode === 'text' && (
- <div className="flex space-x-3">
- <div className="flex-1 relative">
- <textarea
- value={textInput}
- onChange={(e) => setTextInput(e.target.value)}
- onKeyDown={handleKeyPress}
- placeholder={
- currentMode === 'bubbe' ? 'Nu, what\'s your question?' :
- currentMode === 'meshugana' ? 'Spit it out already!' :
- 'What\'s the deal with...?'
- }
- disabled={isLoading}
- className="
- w-full bg-white/10 backdrop-blur-sm border border-white/20 rounded-2xl
- px-4 py-3 text-white placeholder-white/50 resize-none
- focus:outline-none focus:ring-2 focus:ring-white/30 focus:border-transparent
- min-h-[50px] max-h-[120px]
- disabled:opacity-50 disabled:cursor-not-allowed
- "
- rows={1}
- />
- </div>
- <button
- onClick={handleTextSubmit}
- disabled={!textInput.trim() || isLoading}
- className={`
- p-3 rounded-full transition-all duration-200
- ${textInput.trim() && !isLoading
- ? `bg-gradient-to-r ${theme.gradient} hover:scale-105 shadow-lg`
- : 'bg-white/20 cursor-not-allowed opacity-50'
- }
- `}
- >
- <Send className="w-5 h-5 text-white" />
- </button>
- </div>
- )}
- </div>
- </div>
- )
-}
\ No newline at end of file
← 84b91bc fix(csp): allow Cloudflare Insights beacon (+ GA4 regional)
·
back to Dear Bubbe Nextjs
·
debug+refactor(bubbe): fix social-post ENOENT, extract share 6ac4af5 →