Files
kira/frontend/src/components/ChatBubble.tsx
T
hobokenchicken 83a990e838 feat(audio): Gemini Live API replaces Whisper+GPT+ElevenLabs
Single WebSocket proxy: frontend PCM16 16kHz → backend → Gemini Live API
Gemini returns PCM16 24kHz audio + text. Playback via Web Audio API queue.
Removed OpenAI/DeepSeek deps. Model: gemini-3.1-flash-live-preview.
Voice: Aoede. Streaming bidirectional audio with silence gating.
2026-06-05 23:36:29 -04:00

86 lines
2.7 KiB
TypeScript

import { useRef, useEffect } from 'react';
interface Message {
id: string;
role: 'user' | 'kira';
text: string;
timestamp: number;
}
interface Props {
messages: Message[];
isKiraSpeaking: boolean;
userName?: string;
}
export default function ChatBubble({ messages, isKiraSpeaking }: Props) {
const bottomRef = useRef<HTMLDivElement>(null);
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
return (
<div className="p-3 flex flex-col" style={{ maxHeight: 160 }}>
<h3 className="text-sm font-bold text-kira-plum mb-3 flex items-center gap-2">
<span>💬</span> Conversation
<span className={`w-2 h-2 rounded-full ${isKiraSpeaking ? 'bg-kira-pink animate-pulse' : 'bg-kira-mint'}`} />
</h3>
<div className="flex-1 overflow-y-auto space-y-2 scrollbar-thin pr-1">
{messages.length === 0 && (
<div className="text-xs text-kira-plum/30 text-center py-6">
click the mic or type to talk to Kira
</div>
)}
{messages.map((msg, i) => {
const isLastKira = msg.role === 'kira' && i === messages.length - 1 && isKiraSpeaking;
return (
<div
key={msg.id}
className={`flex gap-2 ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
>
{msg.role === 'kira' && (
<span className="text-sm mt-1">🌸</span>
)}
<div
className={`
max-w-[80%] px-3 py-2 rounded-2xl text-sm leading-relaxed
${msg.role === 'user'
? 'bg-kira-lav/30 text-kira-plum rounded-br-md'
: 'bg-white/60 text-kira-plum rounded-bl-md'
}
${isLastKira ? 'animate-fade-in' : ''}
`}
>
{msg.text}
{isLastKira && (
<span className="inline-block w-1.5 h-4 bg-kira-pink/60 ml-0.5 animate-blink" />
)}
</div>
{msg.role === 'user' && (
<span className="text-sm mt-1">👤</span>
)}
</div>
);
})}
<div ref={bottomRef} />
</div>
<style>{`
@keyframes fade-in {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
.animate-fade-in { animation: fade-in 0.3s ease-out; }
.animate-blink { animation: blink 0.8s infinite; }
`}</style>
</div>
);
}