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]}
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) : }
))}
) }