chore: cleanup dead code and unused files
Deleted 7 unused frontend components (ChatBubble, PetZone, Live2DCat, BackgroundScene, Clock, Toolbar, types/index.ts). Deleted unused backend: whisper_stream.py, empty models/ and routers/ dirs. Removed dead code: Message interface, messages/micError/sendText/addMessage, conversation_text handler, 3 unused memory.py methods. Fixed task persistence bug: added 'tasks' to DEFAULT_PREFERENCES. Stale comment in Live2DStage updated.
This commit is contained in:
@@ -383,29 +383,6 @@ async def gemini_voice_ws(websocket: WebSocket):
|
|||||||
await gemini_ws.send(json.dumps(gemini_msg))
|
await gemini_ws.send(json.dumps(gemini_msg))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if msg_type == "conversation_text":
|
|
||||||
text = msg.get("text", "").strip()
|
|
||||||
if not text:
|
|
||||||
continue
|
|
||||||
logger.info(f"[{session_id}] User (text): {text}")
|
|
||||||
if gemini_ws and gemini_ws.state.name == "OPEN":
|
|
||||||
user_part = {"text": text}
|
|
||||||
if memory_suffix:
|
|
||||||
user_part = {"text": f"[Context: {memory_suffix}]\n{text}"}
|
|
||||||
gemini_msg = {
|
|
||||||
"clientContent": {
|
|
||||||
"turns": [{"role": "user", "parts": [user_part]}],
|
|
||||||
"turnComplete": True,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await gemini_ws.send(json.dumps(gemini_msg))
|
|
||||||
await websocket.send_json({
|
|
||||||
"type": "transcript",
|
|
||||||
"role": "user",
|
|
||||||
"text": text,
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
|
|
||||||
if msg_type == "ping":
|
if msg_type == "ping":
|
||||||
await websocket.send_json({"type": "pong"})
|
await websocket.send_json({"type": "pong"})
|
||||||
|
|
||||||
|
|||||||
@@ -134,57 +134,12 @@ class KiraMemory:
|
|||||||
|
|
||||||
return "\n\n---\n### What Kira remembers:" + "".join(parts)
|
return "\n\n---\n### What Kira remembers:" + "".join(parts)
|
||||||
|
|
||||||
def store_messages(
|
|
||||||
self,
|
|
||||||
user_message: str,
|
|
||||||
kira_message: str,
|
|
||||||
) -> None:
|
|
||||||
"""Store a conversation exchange in Honcho."""
|
|
||||||
if not self.enabled or not self._session:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
messages = []
|
|
||||||
if self._user_peer:
|
|
||||||
messages.append(
|
|
||||||
self._user_peer.message(user_message)
|
|
||||||
)
|
|
||||||
if self._kira_peer:
|
|
||||||
messages.append(
|
|
||||||
self._kira_peer.message(kira_message)
|
|
||||||
)
|
|
||||||
|
|
||||||
if messages:
|
|
||||||
self._session.add_messages(messages)
|
|
||||||
logger.debug("Stored conversation exchange in Honcho")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Failed to store messages: {e}")
|
|
||||||
|
|
||||||
def store_user_message(self, text: str) -> None:
|
|
||||||
"""Store a single user message."""
|
|
||||||
if not self.enabled or not self._session or not self._user_peer:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
self._session.add_messages([self._user_peer.message(text)])
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Failed to store user message: {e}")
|
|
||||||
|
|
||||||
def store_kira_message(self, text: str) -> None:
|
|
||||||
"""Store a single Kira message."""
|
|
||||||
if not self.enabled or not self._session or not self._kira_peer:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
self._session.add_messages([self._kira_peer.message(text)])
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Failed to store Kira message: {e}")
|
|
||||||
|
|
||||||
# ─── User preferences (stored in Honcho peer metadata) ───
|
|
||||||
|
|
||||||
DEFAULT_PREFERENCES: dict[str, str] = {
|
DEFAULT_PREFERENCES: dict[str, str] = {
|
||||||
"name": "",
|
"name": "",
|
||||||
"scene": "cozy-room",
|
"scene": "cozy-room",
|
||||||
"outfit": "cozy-hoodie",
|
"outfit": "cozy-hoodie",
|
||||||
"accessory": "",
|
"accessory": "",
|
||||||
|
"tasks": "[]",
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_user_preferences(self, user_id: str) -> dict[str, str]:
|
def get_user_preferences(self, user_id: str) -> dict[str, str]:
|
||||||
|
|||||||
@@ -1,158 +0,0 @@
|
|||||||
"""Realtime streaming transcription service via gpt-realtime-whisper.
|
|
||||||
|
|
||||||
Connects to OpenAI Realtime API via WebSocket, configures the session
|
|
||||||
for pure transcription (no model responses), and streams word-level
|
|
||||||
transcript deltas back. Full utterances are then processed by the
|
|
||||||
cheap LLM + TTS pipeline.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import base64
|
|
||||||
import logging
|
|
||||||
import asyncio
|
|
||||||
from typing import Callable, Awaitable
|
|
||||||
from config import settings
|
|
||||||
|
|
||||||
logger = logging.getLogger("kira.whisper")
|
|
||||||
|
|
||||||
|
|
||||||
class WhisperStream:
|
|
||||||
"""Streaming transcription via gpt-realtime-whisper over WebSocket."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
on_transcript_delta: Callable[[str], Awaitable[None]],
|
|
||||||
on_transcript_done: Callable[[str], Awaitable[None]],
|
|
||||||
on_ready: Callable[[], Awaitable[None]],
|
|
||||||
on_error: Callable[[str], Awaitable[None]],
|
|
||||||
):
|
|
||||||
self._on_delta = on_transcript_delta
|
|
||||||
self._on_done = on_transcript_done
|
|
||||||
self._on_ready = on_ready
|
|
||||||
self._on_error = on_error
|
|
||||||
self._conn = None
|
|
||||||
self._connected = False
|
|
||||||
self._transcript = ""
|
|
||||||
|
|
||||||
async def connect(self):
|
|
||||||
if self._connected:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
import websockets
|
|
||||||
|
|
||||||
url = "wss://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview"
|
|
||||||
ws = await websockets.connect(
|
|
||||||
url,
|
|
||||||
additional_headers={
|
|
||||||
"Authorization": f"Bearer {settings.openai_api_key}",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
async with ws as conn:
|
|
||||||
self._conn = conn
|
|
||||||
self._connected = True
|
|
||||||
logger.info("Connected to Realtime transcription session")
|
|
||||||
|
|
||||||
# Configure: transcribe only with gpt-realtime-whisper, no model responses
|
|
||||||
await self._send({
|
|
||||||
"type": "session.update",
|
|
||||||
"session": {
|
|
||||||
"input_audio_format": "pcm16",
|
|
||||||
"input_audio_transcription": {
|
|
||||||
"model": "gpt-realtime-whisper",
|
|
||||||
"enabled": True,
|
|
||||||
},
|
|
||||||
"turn_detection": {
|
|
||||||
"type": "server_vad",
|
|
||||||
"threshold": 0.5,
|
|
||||||
"prefix_padding_ms": 300,
|
|
||||||
"silence_duration_ms": 600,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
await self._on_ready()
|
|
||||||
|
|
||||||
while self._connected:
|
|
||||||
try:
|
|
||||||
raw = await conn.recv()
|
|
||||||
if isinstance(raw, (str, bytes)):
|
|
||||||
data = json.loads(raw if isinstance(raw, str) else raw.decode())
|
|
||||||
await self._handle(data)
|
|
||||||
except Exception as e:
|
|
||||||
if self._connected:
|
|
||||||
logger.warning(f"recv: {e}")
|
|
||||||
break
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Whisper stream error: {e}")
|
|
||||||
await self._on_error(str(e))
|
|
||||||
finally:
|
|
||||||
self._connected = False
|
|
||||||
self._conn = None
|
|
||||||
|
|
||||||
async def _handle(self, data: dict):
|
|
||||||
et = data.get("type", "")
|
|
||||||
|
|
||||||
if et == "input_audio_buffer.speech_started":
|
|
||||||
self._transcript = ""
|
|
||||||
logger.debug("speech_started")
|
|
||||||
|
|
||||||
elif et in ("conversation.item.input_audio_transcription.delta", "input_audio_buffer.transcription.delta"):
|
|
||||||
# Partial streaming transcript
|
|
||||||
delta = data.get("delta", "") or data.get("transcript", "")
|
|
||||||
if delta:
|
|
||||||
self._transcript = delta # or append if cumulative
|
|
||||||
await self._on_delta(delta)
|
|
||||||
|
|
||||||
elif et in ("conversation.item.input_audio_transcription.completed", "input_audio_buffer.transcription.completed", "conversation.item.created"):
|
|
||||||
# Final or item created with transcript
|
|
||||||
item = data.get("item", {})
|
|
||||||
content = item.get("content", []) if item else []
|
|
||||||
transcript = data.get("transcript", "")
|
|
||||||
if not transcript:
|
|
||||||
for part in (content or []):
|
|
||||||
if part.get("type") in ("transcript", "text"):
|
|
||||||
transcript = part.get("transcript", "") or part.get("text", "")
|
|
||||||
break
|
|
||||||
if transcript:
|
|
||||||
self._transcript = transcript
|
|
||||||
await self._on_delta(transcript)
|
|
||||||
await self._on_done(transcript.strip())
|
|
||||||
self._transcript = ""
|
|
||||||
|
|
||||||
elif et == "input_audio_buffer.speech_stopped":
|
|
||||||
if self._transcript.strip():
|
|
||||||
await self._on_done(self._transcript.strip())
|
|
||||||
self._transcript = ""
|
|
||||||
|
|
||||||
elif et == "error":
|
|
||||||
err = data.get("error", {})
|
|
||||||
msg = err.get("message", str(data))
|
|
||||||
logger.warning(f"Whisper error: {msg}")
|
|
||||||
await self._on_error(msg)
|
|
||||||
|
|
||||||
async def send_audio(self, pcm16_bytes: bytes):
|
|
||||||
if not self._connected:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
b64 = base64.b64encode(pcm16_bytes).decode("utf-8")
|
|
||||||
await self._send({"type": "input_audio_buffer.append", "audio": b64})
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"send audio: {e}")
|
|
||||||
|
|
||||||
async def _send(self, data: dict):
|
|
||||||
try:
|
|
||||||
await self._conn.send(json.dumps(data))
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"send: {e}")
|
|
||||||
|
|
||||||
async def disconnect(self):
|
|
||||||
self._connected = False
|
|
||||||
if self._conn:
|
|
||||||
try:
|
|
||||||
await self._conn.close()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
self._conn = None
|
|
||||||
@@ -16,7 +16,6 @@ import { useConversation } from './hooks/useConversation';
|
|||||||
export default function App() {
|
export default function App() {
|
||||||
const [isLoggedIn, setIsLoggedIn] = useState(() => sessionStorage.getItem('kira-auth') === '1');
|
const [isLoggedIn, setIsLoggedIn] = useState(() => sessionStorage.getItem('kira-auth') === '1');
|
||||||
const {
|
const {
|
||||||
messages,
|
|
||||||
isConnected,
|
isConnected,
|
||||||
isKiraSpeaking,
|
isKiraSpeaking,
|
||||||
isRecording,
|
isRecording,
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
import { SCENES } from './scenes';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
currentScene: string;
|
|
||||||
onSelect: (id: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function BackgroundScene({ currentScene, onSelect }: Props) {
|
|
||||||
return (
|
|
||||||
<div className="p-4">
|
|
||||||
<h3 className="text-sm font-bold text-kira-plum mb-3 flex items-center gap-2">
|
|
||||||
<span>🎨</span> Scene
|
|
||||||
</h3>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{SCENES.map((s) => (
|
|
||||||
<button
|
|
||||||
key={s.id}
|
|
||||||
onClick={() => onSelect(s.id)}
|
|
||||||
className={`
|
|
||||||
flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-sm font-medium transition-all
|
|
||||||
${currentScene === s.id
|
|
||||||
? 'bg-kira-pink text-white shadow-md scale-105'
|
|
||||||
: 'bg-white/50 text-kira-plum/70 hover:bg-kira-glow hover:scale-102'
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
title={s.name}
|
|
||||||
>
|
|
||||||
<span>{s.icon}</span>
|
|
||||||
<span className="hidden sm:inline">{s.name}</span>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
export default function Clock() {
|
|
||||||
const [time, setTime] = useState(new Date());
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const id = setInterval(() => setTime(new Date()), 1000);
|
|
||||||
return () => clearInterval(id);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="px-5 py-3 text-center">
|
|
||||||
<div className="text-3xl font-bold tracking-tight text-kira-plum">
|
|
||||||
{time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-kira-violet/60 font-medium">
|
|
||||||
{time.toLocaleDateString([], { weekday: 'long', month: 'long', day: 'numeric' })}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
import { useEffect, useRef } from 'react';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Live2DCat({ className }: Props) {
|
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let mounted = true;
|
|
||||||
const canvas = canvasRef.current;
|
|
||||||
if (!canvas) return;
|
|
||||||
|
|
||||||
let app: any = null;
|
|
||||||
|
|
||||||
const init = async () => {
|
|
||||||
try {
|
|
||||||
const pixi = await import('pixi.js');
|
|
||||||
const { Live2DModel } = await import('pixi-live2d-display/cubism4');
|
|
||||||
(Live2DModel as any).registerTicker(pixi.Ticker as any);
|
|
||||||
|
|
||||||
const w = canvas.clientWidth || 120;
|
|
||||||
const h = canvas.clientHeight || 120;
|
|
||||||
|
|
||||||
// Use explicit CanvasRenderer to avoid WebGL context conflict with KiraAvatar
|
|
||||||
app = new pixi.Application({
|
|
||||||
view: canvas,
|
|
||||||
width: w,
|
|
||||||
height: h,
|
|
||||||
antialias: true,
|
|
||||||
resolution: Math.min(window.devicePixelRatio || 1, 2),
|
|
||||||
backgroundAlpha: 0,
|
|
||||||
autoDensity: true,
|
|
||||||
// Use WebGL1 context (more compatible with separate contexts)
|
|
||||||
preferWebGLVersion: 1,
|
|
||||||
} as any);
|
|
||||||
if (!mounted) { app.destroy(true); return; }
|
|
||||||
|
|
||||||
const model = await Live2DModel.from('/live2d/models/little-cat/LittleCat.model3.json', {
|
|
||||||
autoInteract: false,
|
|
||||||
});
|
|
||||||
if (!mounted) { app.destroy(true); return; }
|
|
||||||
|
|
||||||
const sw = app.screen.width;
|
|
||||||
const sh = app.screen.height;
|
|
||||||
const s = Math.min((sw * 0.85) / model.width, (sh * 0.85) / model.height);
|
|
||||||
model.scale.set(s);
|
|
||||||
model.anchor.set(0.5, 0.5);
|
|
||||||
model.position.set(sw / 2, sh / 2);
|
|
||||||
|
|
||||||
app.stage.addChild(model as any);
|
|
||||||
(model as any).isInteractive = () => false;
|
|
||||||
|
|
||||||
try { model.motion('Idle'); } catch { /* */ }
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('[Live2DCat]', e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
init();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
mounted = false;
|
|
||||||
if (app) { app.destroy(true, { children: true }); }
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<canvas
|
|
||||||
ref={canvasRef}
|
|
||||||
className={className}
|
|
||||||
style={{ display: 'block' }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -11,7 +11,7 @@ interface Props {
|
|||||||
/**
|
/**
|
||||||
* Single full-viewport Live2D stage. One WebGL context, two models:
|
* Single full-viewport Live2D stage. One WebGL context, two models:
|
||||||
* - Kira (center panel)
|
* - Kira (center panel)
|
||||||
* - Mochi the cat (PetZone, bottom of right sidebar)
|
// Mochi the cat (next to Kira in center panel)
|
||||||
*
|
*
|
||||||
* Canvas sits above UI panels (z-50, pointer-events: none) so Live2D
|
* Canvas sits above UI panels (z-50, pointer-events: none) so Live2D
|
||||||
* models render on top of the frosted-glass sidebars.
|
* models render on top of the frosted-glass sidebars.
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
export default function PetZone() {
|
|
||||||
return (
|
|
||||||
<div data-petzone className="p-4 relative overflow-hidden" style={{ minHeight: 140 }}>
|
|
||||||
<h3 className="text-sm font-bold text-kira-plum mb-2 flex items-center gap-2">
|
|
||||||
<span>🐱</span> Pet Zone
|
|
||||||
</h3>
|
|
||||||
<div className="flex flex-col items-center pt-2">
|
|
||||||
<span className="text-[10px] text-kira-plum/60 font-medium">Mochi</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { SCENES } from './scenes';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
currentScene: string;
|
|
||||||
onSceneChange: (id: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Toolbar({ currentScene, onSceneChange }: Props) {
|
|
||||||
return (
|
|
||||||
<div className="px-4 py-3 flex items-center justify-between gap-2">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<span className="text-lg font-bold text-kira-plum">Kira</span>
|
|
||||||
<span className="text-kira-plum/20">|</span>
|
|
||||||
<div className="flex gap-1.5">
|
|
||||||
{SCENES.filter((_, i) => i < 4).map((s) => (
|
|
||||||
<button
|
|
||||||
key={s.id}
|
|
||||||
onClick={() => onSceneChange(s.id)}
|
|
||||||
className={`text-sm ${currentScene === s.id ? 'opacity-100' : 'opacity-40 hover:opacity-70'} transition-opacity`}
|
|
||||||
title={s.name}
|
|
||||||
>
|
|
||||||
{s.icon}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-3 text-sm text-kira-plum/50">
|
|
||||||
<span>🐱 2</span>
|
|
||||||
<span className="hidden sm:inline">focus bestie</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -13,12 +13,6 @@ export interface Task {
|
|||||||
completed: boolean;
|
completed: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Message {
|
|
||||||
id: string;
|
|
||||||
role: 'user' | 'kira';
|
|
||||||
text: string;
|
|
||||||
timestamp: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const WS_URL = `${location.protocol === 'https:' ? 'wss:' : 'ws:'}//${location.host}/api/ws`;
|
const WS_URL = `${location.protocol === 'https:' ? 'wss:' : 'ws:'}//${location.host}/api/ws`;
|
||||||
const USER_ID_KEY = 'kira-user-id';
|
const USER_ID_KEY = 'kira-user-id';
|
||||||
@@ -56,7 +50,6 @@ function float32ToPcm16Base64(float32: Float32Array, srcRate: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useConversation() {
|
export function useConversation() {
|
||||||
const [messages, setMessages] = useState<Message[]>([]);
|
|
||||||
const [isConnected, setIsConnected] = useState(false);
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
const [isKiraSpeaking, setIsKiraSpeaking] = useState(false);
|
const [isKiraSpeaking, setIsKiraSpeaking] = useState(false);
|
||||||
const [isRecording, setIsRecording] = useState(false);
|
const [isRecording, setIsRecording] = useState(false);
|
||||||
@@ -68,7 +61,6 @@ export function useConversation() {
|
|||||||
accessory: '',
|
accessory: '',
|
||||||
});
|
});
|
||||||
const [loadingPrefs, setLoadingPrefs] = useState(true);
|
const [loadingPrefs, setLoadingPrefs] = useState(true);
|
||||||
const [micError, setMicError] = useState<string | null>(null);
|
|
||||||
const [tasks, setTasks] = useState<Task[]>([]);
|
const [tasks, setTasks] = useState<Task[]>([]);
|
||||||
const [celebrateTrigger, setCelebrateTrigger] = useState(0);
|
const [celebrateTrigger, setCelebrateTrigger] = useState(0);
|
||||||
|
|
||||||
@@ -130,7 +122,6 @@ export function useConversation() {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'transcript':
|
case 'transcript':
|
||||||
addMessage(msg.role === 'user' ? 'user' : 'kira', msg.text);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'audio': {
|
case 'audio': {
|
||||||
@@ -220,12 +211,6 @@ export function useConversation() {
|
|||||||
return playbackCtxRef.current;
|
return playbackCtxRef.current;
|
||||||
}
|
}
|
||||||
|
|
||||||
const addMessage = useCallback((role: 'user' | 'kira', text: string) => {
|
|
||||||
setMessages((prev) => [
|
|
||||||
...prev,
|
|
||||||
{ id: crypto.randomUUID(), role, text, timestamp: Date.now() },
|
|
||||||
]);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// ── Identity ──
|
// ── Identity ──
|
||||||
const identify = useCallback((name: string) => {
|
const identify = useCallback((name: string) => {
|
||||||
@@ -248,12 +233,10 @@ export function useConversation() {
|
|||||||
// ── Audio capture via ScriptProcessorNode (PCM16 16kHz stream) ──
|
// ── Audio capture via ScriptProcessorNode (PCM16 16kHz stream) ──
|
||||||
const startRecording = useCallback(async () => {
|
const startRecording = useCallback(async () => {
|
||||||
if (!navigator.mediaDevices?.getUserMedia) {
|
if (!navigator.mediaDevices?.getUserMedia) {
|
||||||
addMessage('kira', 'Mic requires HTTPS. Try accessing via HTTPS!');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setMicError(null);
|
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({
|
const stream = await navigator.mediaDevices.getUserMedia({
|
||||||
audio: { echoCancellation: true, noiseSuppression: true, sampleRate: 48000 },
|
audio: { echoCancellation: true, noiseSuppression: true, sampleRate: 48000 },
|
||||||
});
|
});
|
||||||
@@ -261,7 +244,6 @@ export function useConversation() {
|
|||||||
|
|
||||||
const ws = wsRef.current;
|
const ws = wsRef.current;
|
||||||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||||
addMessage('kira', 'Not connected to server yet...');
|
|
||||||
stream.getTracks().forEach((t) => t.stop());
|
stream.getTracks().forEach((t) => t.stop());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -295,10 +277,9 @@ export function useConversation() {
|
|||||||
setIsRecording(true);
|
setIsRecording(true);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const msg = err instanceof Error ? err.message : String(err);
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
setMicError(msg);
|
|
||||||
console.error('[Kira Mic]', msg);
|
console.error('[Kira Mic]', msg);
|
||||||
}
|
}
|
||||||
}, [addMessage]);
|
}, []);
|
||||||
|
|
||||||
const stopRecording = useCallback(() => {
|
const stopRecording = useCallback(() => {
|
||||||
processorRef.current?.disconnect();
|
processorRef.current?.disconnect();
|
||||||
@@ -328,14 +309,12 @@ export function useConversation() {
|
|||||||
}, [connect, stopRecording]);
|
}, [connect, stopRecording]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
messages,
|
|
||||||
isConnected,
|
isConnected,
|
||||||
isKiraSpeaking,
|
isKiraSpeaking,
|
||||||
isRecording,
|
isRecording,
|
||||||
identified,
|
identified,
|
||||||
preferences,
|
preferences,
|
||||||
loadingPrefs,
|
loadingPrefs,
|
||||||
micError,
|
|
||||||
identify,
|
identify,
|
||||||
setPreference,
|
setPreference,
|
||||||
sendText,
|
sendText,
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
--color-kira-bg: #FFF5F5;
|
--color-kira-bg: #FFF5F5;
|
||||||
--color-kira-plum: #4A1942;
|
--color-kira-plum: #4A1942;
|
||||||
--color-kira-violet: #7C3AED;
|
--color-kira-violet: #7C3AED;
|
||||||
--color-kira-card: #FFFFFF;
|
|
||||||
--color-kira-glow: #FDF2F8;
|
--color-kira-glow: #FDF2F8;
|
||||||
--font-nunito: 'Nunito', sans-serif;
|
--font-nunito: 'Nunito', sans-serif;
|
||||||
}
|
}
|
||||||
@@ -27,14 +26,6 @@ body {
|
|||||||
width: 100vw;
|
width: 100vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
.glass-card {
|
|
||||||
background: rgba(255, 255, 255, 0.7);
|
|
||||||
backdrop-filter: blur(12px);
|
|
||||||
border: 1px solid rgba(255, 182, 193, 0.3);
|
|
||||||
border-radius: 16px;
|
|
||||||
box-shadow: 0 4px 24px rgba(74, 25, 66, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-kira {
|
.btn-kira {
|
||||||
background: linear-gradient(135deg, #FFB6C1, #D8B4FE);
|
background: linear-gradient(135deg, #FFB6C1, #D8B4FE);
|
||||||
color: white;
|
color: white;
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
import { SCENES, type Scene } from '../components/scenes';
|
|
||||||
|
|
||||||
export interface KiraState {
|
|
||||||
currentScene: Scene;
|
|
||||||
isListening: boolean;
|
|
||||||
isSpeaking: boolean;
|
|
||||||
currentOutfit: string;
|
|
||||||
currentAccessory: string | null;
|
|
||||||
sessionNotes: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export type { Scene };
|
|
||||||
export { SCENES };
|
|
||||||
@@ -1 +1 @@
|
|||||||
{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/AnimatedAvatar.tsx","./src/components/BackgroundScene.tsx","./src/components/ChatBubble.tsx","./src/components/Clock.tsx","./src/components/KiraAvatar.tsx","./src/components/Live2DCat.tsx","./src/components/Live2DStage.tsx","./src/components/LoginScreen.tsx","./src/components/MusicPlayer.tsx","./src/components/Notes.tsx","./src/components/Particles.tsx","./src/components/PetZone.tsx","./src/components/TaskList.tsx","./src/components/Timer.tsx","./src/components/Toolbar.tsx","./src/components/Wardrobe.tsx","./src/components/WelcomeScreen.tsx","./src/components/WhiteNoise.tsx","./src/components/scenes.ts","./src/hooks/useConversation.ts","./src/types/index.ts"],"version":"6.0.3"}
|
{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/AnimatedAvatar.tsx","./src/components/KiraAvatar.tsx","./src/components/Live2DStage.tsx","./src/components/LoginScreen.tsx","./src/components/MusicPlayer.tsx","./src/components/Notes.tsx","./src/components/Particles.tsx","./src/components/TaskList.tsx","./src/components/Timer.tsx","./src/components/Wardrobe.tsx","./src/components/WelcomeScreen.tsx","./src/components/WhiteNoise.tsx","./src/components/scenes.ts","./src/hooks/useConversation.ts"],"version":"6.0.3"}
|
||||||
Reference in New Issue
Block a user