@brian/claude-chat-ui (0.1.3)

Published 2026-07-20 22:28:09 +00:00 by brian in brian/claudechatwindow

Installation

@brian:registry=
npm install @brian/claude-chat-ui@0.1.3
"@brian/claude-chat-ui": "0.1.3"

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 + optional image attachments), 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",
  hints: ["Marian", "Metformin"], // optional: best-effort recognition nudges, Chromium-only
});
// 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()

Image attachments

Off by default — pass allowImages to turn on the attach button, pasting an image, and dragging one in:

<ChatInput
  value={draft}
  onChange={setDraft}
  allowImages
  onSend={(text, images) => {
    // images -> ImageAttachment[] | undefined, each { mediaType, base64, name, previewUrl }
    // Send `mediaType` + `base64` to your backend, which builds an Anthropic
    // image content block with imageContentBlock() from claude-chat-client /
    // claude-chat-client-py.
  }}
/>

useImageAttachments is also exported standalone, for apps with their own input UI (same reasoning as useSpeechToText — every app so far has rolled its own JSX rather than adopting ChatInput wholesale):

const images = useImageAttachments({ maxBytes: 5 * 1024 * 1024, maxCount: 20 });
// images.attachments, images.addFiles(fileListOrArray), images.removeAttachment(id),
// images.clear(), images.error (set when a file was rejected — bad type, too large, too many)

Only image/jpeg, image/png, image/gif, and image/webp are accepted (what Claude's Messages API takes as native image input) — anything else rejects with UnsupportedImageTypeError. Document/PDF attachments (which homeclaude also supports today) aren't covered yet; add them here if/when an app actually needs them rather than speculatively now.

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
Details
npm
2026-07-20 22:28:09 +00:00
8
latest
9.7 KiB
Assets (1)
Versions (4) View all
0.1.3 2026-07-20
0.1.2 2026-07-20
0.1.1 2026-07-20
0.1.0 2026-07-20