import { useEffect, useRef } from "react" import { Mic, RotateCcw, Loader2, Volume2 } from "lucide-react" import { Avatar3D } from "@/components/voice/Avatar3D" import { VoiceControls } from "@/components/voice/AvatarPicker" import { useVoiceAgent } from "@/lib/voice/useVoiceAgent" import { cn } from "@/lib/utils" // Fester Avatar (das eine, gewählte VRM). Liegt unter frontend/public/avatar.vrm → /avatar.vrm. const FIXED_AVATAR = "/avatar.vrm" // **fett** als echte Fettschrift rendern (Rest als Text; Zeilenumbrüche via whitespace-pre-wrap). function renderRich(text: string) { return text.split(/(\*\*[^*\n]+\*\*)/g).map((p, i) => { const m = p.match(/^\*\*([^*]+)\*\*$/) return m ? {m[1]} : {p} }) } // Kleiner Tipp-Indikator („Hermes schreibt …"). function Dots() { return ( {[0, 1, 2].map((i) => ( ))} ) } const STATUS_LABEL: Record = { idle: "Bereit — halte zum Sprechen", listening: "Höre zu …", transcribing: "Verstehe …", thinking: "Hermes denkt …", speaking: "Hermes spricht …", error: "Fehler", } export function VoiceView() { const { status, messages, error, recording, audioLevel, emotion, pressStart, pressEnd, reset } = useVoiceAgent() const holding = useRef(false) const convEndRef = useRef(null) // Gespräch automatisch nach unten scrollen, wenn neue Tokens/Nachrichten kommen. useEffect(() => { convEndRef.current?.scrollIntoView({ behavior: "smooth", block: "end" }) }, [messages]) // Push-to-talk per Leertaste (solange der Sprechen-Tab fokussiert ist und kein Eingabefeld aktiv). useEffect(() => { const isField = (el: EventTarget | null) => el instanceof HTMLElement && /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) const down = (e: KeyboardEvent) => { if (e.code !== "Space" || e.repeat || holding.current || isField(e.target)) return e.preventDefault(); holding.current = true; pressStart() } const up = (e: KeyboardEvent) => { if (e.code !== "Space" || !holding.current) return e.preventDefault(); holding.current = false; pressEnd() } window.addEventListener("keydown", down) window.addEventListener("keyup", up) return () => { window.removeEventListener("keydown", down); window.removeEventListener("keyup", up) } }, [pressStart, pressEnd]) const speaking = status === "speaking" const busy = status === "transcribing" || status === "thinking" return ( {/* Avatar-Bühne */} {/* Status + Push-to-talk */} {busy && } {speaking && } {error || STATUS_LABEL[status]} { e.preventDefault(); holding.current = true; pressStart() }} onPointerUp={() => { if (holding.current) { holding.current = false; pressEnd() } }} onPointerLeave={() => { if (holding.current) { holding.current = false; pressEnd() } }} className={cn( "flex h-20 w-20 items-center justify-center rounded-full border-2 transition-all select-none touch-none", recording ? "border-red-500 bg-red-500/20 scale-110 shadow-lg shadow-red-500/30" : "border-primary/60 bg-primary/15 hover:bg-primary/25 hover:scale-105", )} title="Gedrückt halten zum Sprechen (oder Leertaste halten)" > Halten zum Sprechen · Leertaste geht auch {/* Seitenspalte: Einstellungen + Transcript */} Gespräch {messages.length === 0 && ( Halte den Knopf (oder die Leertaste) und sprich. Hermes hört zu, denkt mit seinem vollen Gedächtnis und seinen Werkzeugen — und antwortet hörbar. )} {messages.map((m, i) => ( {m.role === "user" ? "Du" : "Hermes"} {m.text ? renderRich(m.text) : } ))} ) }
Halte den Knopf (oder die Leertaste) und sprich. Hermes hört zu, denkt mit seinem vollen Gedächtnis und seinen Werkzeugen — und antwortet hörbar.