- Conversation sidebar with create/delete/history - Chat area with streaming LLM responses (z-ai-web-dev-sdk) - Voice input via Web Speech API with recording indicator - Browser TTS auto-speak for assistant responses - Settings panel (voice, TTS, sidebar toggle) - Prisma schema: Conversation + Message models - API routes: /api/chat/stream, /api/conversations, /api/messages - Zustand store for state management - Web Speech API type declarations
101 lines
2.7 KiB
TypeScript
101 lines
2.7 KiB
TypeScript
"use client";
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { useEchoStore } from "@/stores/echo-store";
|
|
import { Settings2, Mic, Volume2 } from "lucide-react";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
export function ChatHeader() {
|
|
const {
|
|
activeConversationId,
|
|
conversations,
|
|
isRecording,
|
|
isSpeaking,
|
|
isGenerating,
|
|
setSettingsOpen,
|
|
setSidebarOpen,
|
|
sidebarOpen,
|
|
} = useEchoStore();
|
|
|
|
const activeConv = conversations.find(
|
|
(c) => c.id === activeConversationId
|
|
);
|
|
|
|
return (
|
|
<header className="h-14 border-b border-border bg-card/80 backdrop-blur-sm flex items-center justify-between px-4">
|
|
<div className="flex items-center gap-3 min-w-0">
|
|
{!sidebarOpen && (
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8 shrink-0"
|
|
onClick={() => setSidebarOpen(true)}
|
|
>
|
|
<svg
|
|
className="h-4 w-4"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
strokeWidth={2}
|
|
stroke="currentColor"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
|
|
/>
|
|
</svg>
|
|
</Button>
|
|
)}
|
|
<div className="min-w-0">
|
|
<h1 className="text-sm font-semibold truncate">
|
|
{activeConv ? activeConv.title : "Echo"}
|
|
</h1>
|
|
{activeConversationId && (
|
|
<p className="text-xs text-muted-foreground truncate">
|
|
{isGenerating
|
|
? "Thinking..."
|
|
: isRecording
|
|
? "Listening..."
|
|
: isSpeaking
|
|
? "Speaking..."
|
|
: "Ready"}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-1">
|
|
{/* Status indicators */}
|
|
<Badge
|
|
variant={isRecording ? "destructive" : "secondary"}
|
|
className={cn(
|
|
"text-xs gap-1 mr-1",
|
|
isRecording && "animate-pulse"
|
|
)}
|
|
>
|
|
<Mic className="h-3 w-3" />
|
|
{isRecording ? "Recording" : "Mic"}
|
|
</Badge>
|
|
|
|
<Badge
|
|
variant={isSpeaking ? "default" : "secondary"}
|
|
className="text-xs gap-1 mr-2"
|
|
>
|
|
<Volume2 className="h-3 w-3" />
|
|
{isSpeaking ? "Speaking" : "TTS"}
|
|
</Badge>
|
|
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8"
|
|
onClick={() => setSettingsOpen(true)}
|
|
>
|
|
<Settings2 className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</header>
|
|
);
|
|
}
|