@brian/claude-chat-ui (0.1.0)
Installation
@brian:registry=npm install @brian/claude-chat-ui@0.1.0"@brian/claude-chat-ui": "0.1.0"About this package
@brian/claude-chat-ui
Shared React pieces for a Claude-powered chat window: a useSpeechToText
hook (with the Android duplication fix applied), a ChatInput (textarea +
send + mic button), and a bare-bones MessageList.
Deliberately does not ship a full chat drawer/panel component — every app's chat window has different chrome (bottom sheet vs. docked panel vs. floating launcher vs. sidebar-of-past-chats) and a different streaming wire format (NDJSON vs. SSE-like). Compose these pieces into your own layout.
Usage
import { useState } from "react";
import { ChatInput, MessageList, type ChatMessage } from "@brian/claude-chat-ui";
function ChatPanel() {
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [draft, setDraft] = useState("");
return (
<div className="chat-panel">
<MessageList messages={messages} className="chat-panel__messages" />
<ChatInput
value={draft}
onChange={setDraft}
onSend={(text) => {
setMessages((m) => [...m, { id: crypto.randomUUID(), role: "user", content: text }]);
setDraft("");
// POST to your backend, which uses @brian/claude-chat-client / claude-chat-client-py
}}
/>
</div>
);
}
useSpeechToText can also be used standalone if you want your own mic
button UI instead of ChatInput's:
const speech = useSpeechToText({ lang: "en-US" });
// speech.isSupported, speech.isListening, speech.transcript, speech.interimTranscript
// speech.error -> raw SpeechRecognition error code ("not-allowed", "no-speech", ...) or null;
// map it to your own copy, e.g. "not-allowed"/"service-not-allowed" -> "Microphone access denied."
// speech.start() / speech.stop() / speech.reset()
Why this fixes the Android duplication bug
See the doc comment on useSpeechToText in src/useSpeechToText.ts. Short
version: never accumulate transcript text across onresult events —
Android Chrome periodically re-sends the whole phrase as a new "final"
result, so incremental appending double-counts it. Rebuild the full
transcript from index 0 on every event instead, and start a fresh
SpeechRecognition instance on every automatic restart — except on a
terminal error (not-allowed, service-not-allowed, audio-capture),
which stops the session instead of retrying against a mic that won't work
this session.
Dependencies
Development dependencies
| ID | Version |
|---|---|
| @types/react | ^18.3.0 |
| @types/react-dom | ^18.3.0 |
| typescript | ^5.5.0 |
Peer dependencies
| ID | Version |
|---|---|
| react | >=18 |
| react-dom | >=18 |