import { useRef, useState, useCallback } from "preact/hooks"; import type { SpeechRecognizer } from "@palace/sdk/speech"; import { useSpeechRecognizer } from "@palace/sdk/speech/preact"; import { sendChatStream } from "../../lib/api"; import { renderMarkdown } from "../../lib/render-md"; interface Props { threadId: string; recognizer: SpeechRecognizer; onSent: () => void; } export default function ChatReply({ threadId, recognizer, onSent }: Props) { const inputRef = useRef(null); const [busy, setBusy] = useState(false); const [responseHtml, setResponseHtml] = useState(null); const [status, setStatus] = useState(null); const onTranscribed = useCallback((text: string) => { if (inputRef.current) { inputRef.current.value += (inputRef.current.value ? " " : "") + text; inputRef.current.focus(); } }, []); const { recording, transcribing, toggle } = useSpeechRecognizer(recognizer, { onResult: onTranscribed, onStatus: setStatus }); const send = useCallback(async () => { const msg = inputRef.current?.value.trim(); if (!msg || busy) return; inputRef.current!.value = ""; setBusy(true); setResponseHtml(null); let full = ""; try { await sendChatStream(threadId, msg, (chunk) => { full += chunk; setResponseHtml(renderMarkdown(full)); }); setResponseHtml(renderMarkdown(full)); onSent(); } catch (e: any) { setResponseHtml("Error: " + e.message + ""); } setBusy(false); }, [threadId, busy, onSent]); const micClass = `chat-reply-mic${recording ? " recording" : ""}${transcribing ? " transcribing" : ""}`; return ( <>
{ if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); } }} disabled={busy} />
{status &&
{status}
} {busy && !responseHtml && !status &&
Thinking...
} {responseHtml &&
} ); }