Add full-featured AIChat component

AIChat (elements/ai/AIChat) is a complete LLM chat UI built on new hooks:
- useAIChat: streaming conversation state (send, stop, regenerate,
  edit, feedback); `send` may return a string, async iterable,
  ReadableStream or fetch Response
- useAIChatHistory: saved conversations with optional localStorage
  persistence and generated titles, shown in AIChatSidebar
- useAIChatSpeech: read aloud via browser speech synthesis or a
  custom `speak` returning audio
- useAIChatDictation: voice input via browser recognition or a
  custom `transcribe`

Also adds a model picker, message editing, thumbs up/down, markdown
export, scoped markdown styles in base.css, the twuiReadTextStream,
twuiStripMarkdown and twuiGroupByRecency utils (with tests), and a
playground page. AIPromptBlock is kept and marked deprecated.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-26 10:45:46 +01:00
co-authored by Claude Opus 5.5
parent 1f74447d1c
commit 1c607c4bcd
21 changed files with 3933 additions and 1 deletions
+34 -1
View File
@@ -25,7 +25,7 @@ Only install these if you use the matching components:
| `editors/AceEditor` | `ace-builds` |
| `elements/RemoteCodeBlock`, `mdx/markdown/*` | `next-mdx-remote`, `remark-gfm`, `rehype-prism-plus` |
| `mdx/mdx-components`, `next-js/hooks/useMDXComponents` | `@types/mdx` (dev) |
| `elements/ai/*` | `openai` (types), `react-markdown` |
| `elements/ai/*` | `react-markdown`, `remark-gfm` (+ `openai` types for the legacy `AIPrompt*` blocks) |
`editors/TinyMCE` loads TinyMCE from a URL at runtime. Set it with the `base_url` prop, or with the `NEXT_PUBLIC_TINYMCE_BASE_URL` / `BUNEXT_PUBLIC_TINYMCE_BASE_URL` public env vars.
@@ -67,6 +67,39 @@ import TinyMCEEditor from "@moduletrace/twui/editors/TinyMCE";
Editors, MDX, Next.js helpers, the AI blocks and `RemoteCodeBlock` are **deep-import only**, so the root entry never pulls in optional peers.
### AI chat
`elements/ai/AIChat` is a complete chat UI. Give it a height and a `send` function. `send` can return a string, an async iterable of chunks, a `ReadableStream`, or a fetch `Response`:
```tsx
import AIChat from "@moduletrace/twui/elements/ai/AIChat";
<AIChat
className="h-[80vh]"
history={{ storageKey: "my-app-chats" }}
models={["gpt-5", "claude-opus-5-5"]}
send={({ messages, model, signal }) =>
fetch("/api/chat", {
method: "POST",
body: JSON.stringify({ messages, model }),
signal,
})
}
/>;
```
| Feature | Props |
| --- | --- |
| Streaming replies, stop, regenerate, retry | built in |
| Saved conversations (sidebar, search, pin, rename, delete) | `history` (`true`, options, or a `useAIChatHistory()` result), `generateTitle` in the options |
| Read aloud (text-to-speech) | `speech` (browser voice by default, or `{ speak }` returning audio), `autoSpeak` |
| Voice input (speech-to-text) | `dictation` (browser recognition by default, or `{ transcribe }`) |
| Model picker | `models`, `model`, `onModelChange` |
| Edit messages, thumbs up/down, markdown export | `editable`, `feedback` / `onFeedback`, `exportable` |
| Empty state, avatars, custom rendering | `suggestions`, `emptyState`, `userImageUrl`, `renderMessageContent`, `renderMessageActions` |
To drive the chat from outside, call `useAIChat` (and `useAIChatHistory`) yourself and pass the results as `chat` and `history`. The pieces (`AIChatMessage`, `AIChatComposer`, `AIChatSidebar`, `useAIChatSpeech`, `useAIChatDictation`) are exported from `elements/ai/` for custom layouts.
Component modules are marked `"use client"`, so they work in the Next.js App Router.
## Development