@brian/claude-chat-ui (0.1.1)
Installation
@brian:registry=npm install @brian/claude-chat-ui@0.1.1"@brian/claude-chat-ui": "0.1.1"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: Android Chrome can put multiple entries in one event's results
array where each later entry is a fuller cumulative re-transcription of
everything said so far, not just the newly-finalized words — so summing
entries (even only final ones, even rebuilding from index 0 every event)
still double- and triple-counts words. Confirmed on a real device: v0.1.0
of this hook summed entries and reproduced exactly that failure. v0.1.1
fixes it by never summing at all — only the last entry is trusted, since
it's always the most complete version available.
Separately, a fresh SpeechRecognition instance is started on every
automatic restart (continuous mode still ends on its own periodically even
mid-utterance) — 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 |