83a990e838
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.
86 lines
2.7 KiB
TypeScript
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>
|
|
);
|
|
}
|