@djangocfg/ui-tools 2.1.381 → 2.1.382

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/README.md +132 -899
  2. package/dist/ChatRoot-6IZFM5HM.mjs +5 -0
  3. package/dist/{ChatRoot-EJC5Y2YM.cjs.map → ChatRoot-6IZFM5HM.mjs.map} +1 -1
  4. package/dist/ChatRoot-LW4XNIKP.cjs +14 -0
  5. package/dist/{ChatRoot-QOSKJPM6.mjs.map → ChatRoot-LW4XNIKP.cjs.map} +1 -1
  6. package/dist/DictationField-2ZLQWLYV.mjs +4 -0
  7. package/dist/DictationField-2ZLQWLYV.mjs.map +1 -0
  8. package/dist/DictationField-IPPJ54CU.cjs +13 -0
  9. package/dist/DictationField-IPPJ54CU.cjs.map +1 -0
  10. package/dist/{DocsLayout-2YKPXZYO.mjs → DocsLayout-2P3ONDWJ.mjs} +3 -3
  11. package/dist/{DocsLayout-2YKPXZYO.mjs.map → DocsLayout-2P3ONDWJ.mjs.map} +1 -1
  12. package/dist/{DocsLayout-Q4KS3QWW.cjs → DocsLayout-2YZNS5VK.cjs} +8 -8
  13. package/dist/{DocsLayout-Q4KS3QWW.cjs.map → DocsLayout-2YZNS5VK.cjs.map} +1 -1
  14. package/dist/chunk-4LXG3NBV.mjs +833 -0
  15. package/dist/chunk-4LXG3NBV.mjs.map +1 -0
  16. package/dist/{chunk-XACCHZH2.cjs → chunk-FIRK5CEH.cjs} +42 -4
  17. package/dist/chunk-FIRK5CEH.cjs.map +1 -0
  18. package/dist/{chunk-NWUT327A.mjs → chunk-HIK6BPL7.mjs} +38 -5
  19. package/dist/chunk-HIK6BPL7.mjs.map +1 -0
  20. package/dist/chunk-KMSBGNVC.cjs +835 -0
  21. package/dist/chunk-KMSBGNVC.cjs.map +1 -0
  22. package/dist/chunk-OZAU3QWD.cjs +2493 -0
  23. package/dist/chunk-OZAU3QWD.cjs.map +1 -0
  24. package/dist/chunk-UWVP6LCW.mjs +2447 -0
  25. package/dist/chunk-UWVP6LCW.mjs.map +1 -0
  26. package/dist/index.cjs +1532 -100
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +1148 -107
  29. package/dist/index.d.ts +1148 -107
  30. package/dist/index.mjs +1421 -51
  31. package/dist/index.mjs.map +1 -1
  32. package/package.json +16 -8
  33. package/src/audio-assets.d.ts +8 -0
  34. package/src/components/markdown/MarkdownMessage/CollapseToggle.tsx +3 -1
  35. package/src/components/markdown/MarkdownMessage/components.tsx +2 -5
  36. package/src/stories/index.ts +32 -2
  37. package/src/tools/Chat/README.md +347 -530
  38. package/src/tools/Chat/components/Attachments.tsx +6 -1
  39. package/src/tools/Chat/components/ChatRoot.tsx +30 -2
  40. package/src/tools/Chat/components/Composer.tsx +20 -3
  41. package/src/tools/Chat/components/ErrorBanner.tsx +7 -3
  42. package/src/tools/Chat/components/MessageActions.tsx +3 -1
  43. package/src/tools/Chat/components/MessageBubble.tsx +6 -5
  44. package/src/tools/Chat/components/MessageList.tsx +87 -1
  45. package/src/tools/Chat/components/ToolCalls.tsx +21 -3
  46. package/src/tools/Chat/context/ChatProvider.tsx +21 -3
  47. package/src/tools/Chat/core/audio/audioBus.ts +10 -163
  48. package/src/tools/Chat/core/audio/defaults.ts +43 -0
  49. package/src/tools/Chat/core/audio/index.ts +1 -0
  50. package/src/tools/Chat/core/audio/preferences.ts +5 -59
  51. package/src/tools/Chat/core/audio/sounds/error.mp3 +0 -0
  52. package/src/tools/Chat/core/audio/sounds/mention.mp3 +0 -0
  53. package/src/tools/Chat/core/audio/sounds/notification.mp3 +0 -0
  54. package/src/tools/Chat/core/audio/sounds/received.mp3 +0 -0
  55. package/src/tools/Chat/core/audio/sounds/sent.mp3 +0 -0
  56. package/src/tools/Chat/core/audio/sounds/start.mp3 +0 -0
  57. package/src/tools/Chat/core/audio/types.ts +28 -0
  58. package/src/tools/Chat/core/reducer.ts +33 -0
  59. package/src/tools/Chat/core/transport/index.ts +13 -0
  60. package/src/tools/Chat/core/transport/mappers/index.ts +6 -0
  61. package/src/tools/Chat/core/transport/mappers/pydantic-ai.ts +142 -0
  62. package/src/tools/Chat/core/transport/pydantic-ai-transport.ts +208 -0
  63. package/src/tools/Chat/core/transport/sse.ts +18 -5
  64. package/src/tools/Chat/hooks/index.ts +25 -0
  65. package/src/tools/Chat/hooks/useAutoFocusOnStreamEnd.ts +5 -3
  66. package/src/tools/Chat/hooks/useChat.ts +28 -0
  67. package/src/tools/Chat/hooks/useChatAudio.ts +59 -180
  68. package/src/tools/Chat/hooks/useChatDockPrefs.ts +74 -0
  69. package/src/tools/Chat/hooks/useChatReset.ts +70 -0
  70. package/src/tools/Chat/hooks/useChatUnread.ts +87 -0
  71. package/src/tools/Chat/hooks/useFocusOnEmptyClick.ts +111 -0
  72. package/src/tools/Chat/hooks/useVisitorFingerprint.ts +48 -0
  73. package/src/tools/Chat/index.ts +69 -1
  74. package/src/tools/Chat/launcher/ChatDock.tsx +263 -0
  75. package/src/tools/Chat/launcher/ChatFAB.tsx +349 -0
  76. package/src/tools/Chat/launcher/ChatGreeting.tsx +200 -0
  77. package/src/tools/Chat/launcher/ChatHeader.tsx +76 -0
  78. package/src/tools/Chat/launcher/ChatHeaderActionButton.tsx +87 -0
  79. package/src/tools/Chat/launcher/ChatHeaderAudioToggle.tsx +47 -0
  80. package/src/tools/Chat/launcher/ChatHeaderLanguageButton.tsx +179 -0
  81. package/src/tools/Chat/launcher/ChatHeaderModeToggle.tsx +57 -0
  82. package/src/tools/Chat/launcher/ChatHeaderResetButton.tsx +93 -0
  83. package/src/tools/Chat/launcher/ChatLauncher.tsx +321 -0
  84. package/src/tools/Chat/launcher/ChatUnreadPreview.tsx +197 -0
  85. package/src/tools/Chat/launcher/index.ts +46 -0
  86. package/src/tools/Chat/launcher/useChatPresence.ts +44 -0
  87. package/src/tools/Chat/stories/01-basic.story.tsx +64 -0
  88. package/src/tools/Chat/stories/02-bubbles.story.tsx +21 -0
  89. package/src/tools/Chat/stories/03-tool-calls.story.tsx +59 -0
  90. package/src/tools/Chat/stories/04-personas.story.tsx +78 -0
  91. package/src/tools/Chat/stories/05-launcher.story.tsx +321 -0
  92. package/src/tools/Chat/stories/06-header.story.tsx +147 -0
  93. package/src/tools/Chat/stories/07-audio-actions.story.tsx +112 -0
  94. package/src/tools/Chat/stories/shared/Frame.tsx +21 -0
  95. package/src/tools/Chat/stories/shared/index.ts +5 -0
  96. package/src/tools/Chat/stories/shared/messages.ts +39 -0
  97. package/src/tools/Chat/stories/shared/personas.ts +13 -0
  98. package/src/tools/Chat/stories/shared/seeds.ts +92 -0
  99. package/src/tools/Chat/stories/shared/transports.ts +36 -0
  100. package/src/tools/Chat/styles/bubbleTokens.ts +71 -0
  101. package/src/tools/Chat/styles/index.ts +16 -0
  102. package/src/tools/Chat/styles/useChatStyles.ts +101 -0
  103. package/src/tools/Chat/types/attachment.ts +25 -0
  104. package/src/tools/Chat/types/config.ts +48 -0
  105. package/src/tools/Chat/types/events.ts +35 -0
  106. package/src/tools/Chat/types/index.ts +34 -0
  107. package/src/tools/Chat/types/labels.ts +38 -0
  108. package/src/tools/Chat/types/message.ts +32 -0
  109. package/src/tools/Chat/types/persona.ts +31 -0
  110. package/src/tools/Chat/types/session.ts +43 -0
  111. package/src/tools/Chat/types/tool-call.ts +17 -0
  112. package/src/tools/Chat/types/transport.ts +28 -0
  113. package/src/tools/Chat/types.ts +5 -240
  114. package/src/tools/MarkdownEditor/MarkdownEditor.tsx +50 -14
  115. package/src/tools/MarkdownEditor/index.ts +1 -1
  116. package/src/tools/SpeechRecognition/README.md +336 -0
  117. package/src/tools/SpeechRecognition/__tests__/ids.test.ts +15 -0
  118. package/src/tools/SpeechRecognition/__tests__/language.test.ts +59 -0
  119. package/src/tools/SpeechRecognition/__tests__/reducer.test.ts +71 -0
  120. package/src/tools/SpeechRecognition/__tests__/transcript.test.ts +52 -0
  121. package/src/tools/SpeechRecognition/components/DevicePicker.tsx +49 -0
  122. package/src/tools/SpeechRecognition/components/DictationButton.tsx +93 -0
  123. package/src/tools/SpeechRecognition/components/EngineBadge.tsx +30 -0
  124. package/src/tools/SpeechRecognition/components/ErrorBanner.tsx +52 -0
  125. package/src/tools/SpeechRecognition/components/LanguagePicker.tsx +63 -0
  126. package/src/tools/SpeechRecognition/components/MicMeter.tsx +63 -0
  127. package/src/tools/SpeechRecognition/components/PushToTalkHint.tsx +51 -0
  128. package/src/tools/SpeechRecognition/components/TranscriptView.tsx +55 -0
  129. package/src/tools/SpeechRecognition/components/index.ts +16 -0
  130. package/src/tools/SpeechRecognition/context/SpeechRecognitionProvider.tsx +47 -0
  131. package/src/tools/SpeechRecognition/context/index.ts +6 -0
  132. package/src/tools/SpeechRecognition/core/audio/defaults.ts +24 -0
  133. package/src/tools/SpeechRecognition/core/engine/external.ts +222 -0
  134. package/src/tools/SpeechRecognition/core/engine/http.ts +147 -0
  135. package/src/tools/SpeechRecognition/core/engine/index.ts +52 -0
  136. package/src/tools/SpeechRecognition/core/engine/mediarecorder.ts +105 -0
  137. package/src/tools/SpeechRecognition/core/engine/websocket.ts +211 -0
  138. package/src/tools/SpeechRecognition/core/engine/webspeech.ts +188 -0
  139. package/src/tools/SpeechRecognition/core/ids.ts +11 -0
  140. package/src/tools/SpeechRecognition/core/index.ts +14 -0
  141. package/src/tools/SpeechRecognition/core/language.ts +78 -0
  142. package/src/tools/SpeechRecognition/core/languages-catalog.ts +229 -0
  143. package/src/tools/SpeechRecognition/core/logger.ts +3 -0
  144. package/src/tools/SpeechRecognition/core/reducer.ts +105 -0
  145. package/src/tools/SpeechRecognition/core/transcript.ts +36 -0
  146. package/src/tools/SpeechRecognition/hooks/index.ts +14 -0
  147. package/src/tools/SpeechRecognition/hooks/useDictation.ts +59 -0
  148. package/src/tools/SpeechRecognition/hooks/useEnginePrefs.ts +15 -0
  149. package/src/tools/SpeechRecognition/hooks/useMicDevices.ts +57 -0
  150. package/src/tools/SpeechRecognition/hooks/useMicLevel.ts +52 -0
  151. package/src/tools/SpeechRecognition/hooks/usePushToTalk.ts +85 -0
  152. package/src/tools/SpeechRecognition/hooks/useResolvedLanguage.ts +28 -0
  153. package/src/tools/SpeechRecognition/hooks/useSpeechLanguageInfo.ts +108 -0
  154. package/src/tools/SpeechRecognition/hooks/useSpeechRecognition.ts +188 -0
  155. package/src/tools/SpeechRecognition/hooks/useVoiceSupport.ts +78 -0
  156. package/src/tools/SpeechRecognition/index.ts +82 -0
  157. package/src/tools/SpeechRecognition/lazy.tsx +19 -0
  158. package/src/tools/SpeechRecognition/store/index.ts +2 -0
  159. package/src/tools/SpeechRecognition/store/prefsStore.ts +54 -0
  160. package/src/tools/SpeechRecognition/stories/01-basic.story.tsx +32 -0
  161. package/src/tools/SpeechRecognition/stories/02-dictation-field.story.tsx +32 -0
  162. package/src/tools/SpeechRecognition/stories/03-push-to-talk.story.tsx +27 -0
  163. package/src/tools/SpeechRecognition/stories/04-mic-meter.story.tsx +35 -0
  164. package/src/tools/SpeechRecognition/stories/05-custom-engine-http.story.tsx +40 -0
  165. package/src/tools/SpeechRecognition/stories/06-custom-engine-ws.story.tsx +48 -0
  166. package/src/tools/SpeechRecognition/stories/07-language-device.story.tsx +57 -0
  167. package/src/tools/SpeechRecognition/stories/08-errors-permissions.story.tsx +25 -0
  168. package/src/tools/SpeechRecognition/stories/09-chat-voice.story.tsx +90 -0
  169. package/src/tools/SpeechRecognition/stories/shared.tsx +123 -0
  170. package/src/tools/SpeechRecognition/types.ts +133 -0
  171. package/src/tools/SpeechRecognition/widgets/DictationField.tsx +105 -0
  172. package/src/tools/SpeechRecognition/widgets/VoiceComposerSlot.tsx +305 -0
  173. package/src/tools/SpeechRecognition/widgets/VoiceMessageRecorder.tsx +88 -0
  174. package/src/tools/SpeechRecognition/widgets/index.ts +6 -0
  175. package/dist/ChatRoot-EJC5Y2YM.cjs +0 -14
  176. package/dist/ChatRoot-QOSKJPM6.mjs +0 -5
  177. package/dist/chunk-NWUT327A.mjs.map +0 -1
  178. package/dist/chunk-QLMKCSR6.mjs +0 -2420
  179. package/dist/chunk-QLMKCSR6.mjs.map +0 -1
  180. package/dist/chunk-SI5RD2GD.cjs +0 -2460
  181. package/dist/chunk-SI5RD2GD.cjs.map +0 -1
  182. package/dist/chunk-XACCHZH2.cjs.map +0 -1
  183. package/src/tools/Chat/Chat.story.tsx +0 -1457
@@ -1,22 +1,28 @@
1
1
  # Chat
2
2
 
3
- Decomposed, transport-agnostic chat. Streaming-aware, markdown-native, mobile-ready.
3
+ Decomposed, transport-agnostic chat. Streaming-aware, markdown-native, mobile-ready, role-aware styling, pluggable backends.
4
4
 
5
5
  ## TL;DR
6
6
 
7
7
  ```tsx
8
- import { ChatRoot, createHttpTransport } from '@djangocfg/ui-tools';
8
+ import { ChatRoot, createPydanticAIChatTransport, ChatLauncher } from '@djangocfg/ui-tools';
9
9
 
10
- const transport = createHttpTransport({
11
- baseUrl: '/api/chat',
12
- getAuthHeader: () => ({ Authorization: `Bearer ${getToken()}` }),
10
+ const transport = createPydanticAIChatTransport({
11
+ buildStreamUrl: (sid, msg) => `${API}/chat/sessions/${sid}/stream?message=${encodeURIComponent(msg)}`,
12
+ streamMethod: 'GET',
13
+ buildHeaders: async () => ({ Authorization: `Bearer ${getToken()}` }),
13
14
  });
14
15
 
15
16
  export function MyChat() {
16
17
  return (
17
- <div className="h-[600px]">
18
+ <ChatLauncher
19
+ hotkey={{ key: '/', meta: true }}
20
+ fab={{ variant: 'animated', tooltip: 'Open chat (⌘/)' }}
21
+ dock={{ title: 'Assistant', height: 600 }}
22
+ greeting="Hi 👋 Need help?"
23
+ >
18
24
  <ChatRoot transport={transport} config={{ greeting: 'How can I help?' }} />
19
- </div>
25
+ </ChatLauncher>
20
26
  );
21
27
  }
22
28
  ```
@@ -24,23 +30,33 @@ export function MyChat() {
24
30
  ## What you get
25
31
 
26
32
  - **Headless first.** Pure reducer + hooks; UI is optional and replaceable.
33
+ - **Launcher primitives.** `ChatLauncher` / `ChatFAB` (3 variants, responsive sizing) / `ChatDock` (popover + side modes, mobile fullscreen) / `ChatGreeting` (proactive invite) / `ChatUnreadPreview` (live-push notification).
34
+ - **Header actions.** `ChatHeader` + `ChatHeaderActionButton` + ready-made `ChatHeaderModeToggle` / `ChatHeaderAudioToggle` / `ChatHeaderResetButton` (with `window.dialog.confirm`) / `ChatHeaderLanguageButton` (flag picker for speech-recognition language).
27
35
  - **Decomposed.** `MessageList`, `MessageBubble`, `Composer`, `Sources`, `ToolCalls`, `Attachments`, `EmptyState`, `ErrorBanner`, `JumpToLatest`, `StreamingIndicator` — every part exported.
28
36
  - **Markdown-native.** Sits on top of `MarkdownMessage` (GFM, code, mermaid, sanitized HTML).
29
- - **Streaming.** SSE-based `AsyncGenerator` transport. Token coalescing, cancel, regenerate, partial-keep on cancel.
30
- - **Tool calls.** Live streaming panelsauto-open while running, auto-close on completion. User toggles are remembered.
31
- - **Personas.** `config.user` + `config.assistant` for default identity; `message.sender` for per-message overrides (multi-user / multi-bot).
32
- - **Audio triggers.** Optional sounds on `messageSent`, `messageReceived`, `streamStart`, `error`, `mention`, `notification`. iOS/Safari unlock handled automatically.
33
- - **Rich attachments.** `AttachmentsGrid` for thumbnails, `AttachmentsList` for custom renderers; `onAttachmentOpen` for host-side lightbox.
34
- - **Tool-payload dispatcher.** `dispatchToolPayload(matchers, fallback)` — pluggable predicates choose `<LazyJsonTree>` / `<LazyMap>` / etc. without ui-tools owning those deps.
35
- - **Sources.** RAG citation chips on assistant messages.
36
- - **Slots everywhere.** `header`, `footer`, `banner`, `empty`, `composerToolbarStart/End`, `composerAttachmentTray`, `jumpToLatest` plus render-prop variants.
37
- - **Mobile.** `100dvh`, safe-area inset, 16px textarea (no iOS zoom), responsive `useChatLayout`.
37
+ - **Streaming.** SSE-based `AsyncGenerator` transport. Spec-compliant `parseSSE`, token coalescing, cancel, regenerate.
38
+ - **Pydantic-AI mapper.** Built-in adapter for Django/pydantic-AI backends `text_delta` / `tool_call` / `tool_result` canonical `ChatStreamEvent`. FIFO tool-id queue.
39
+ - **Tool calls.** Live streaming panels, auto-open while running, auto-close on completion. Per-call `memo` so streaming one tool doesn't re-render siblings.
40
+ - **Live push.** `chat.injectMessage(msg)` / `updateMessage(id, patch)` for admin takeover / Centrifugo / WebSocket inbound. `useChatUnread()` tracks unseen; `<ChatUnreadPreview>` surfaces them next to the FAB.
41
+ - **Personas.** `config.user` + `config.assistant` for default identity; `message.sender` for per-message overrides.
42
+ - **Audio.** `useChatAudio` over `@djangocfg/ui-core/hooks/useNotificationSounds` — Safari unlock, persisted mute, per-event toggles, reduced-motion respect. Slack/Linear-style per-event volume scale (error ≈ 0.25, mention ≈ 1.0). **Built-in sounds bundled as base64 data URLs** in the lazy chat chunk (~136KB) — `useChatAudio()` with no arguments just works. `silenced` + `onSoundEvent` for native hosts (cmdop_go / Tauri).
43
+ - **Rich attachments.** `AttachmentsGrid` for thumbnails, `AttachmentsList` for custom renderers; `onAttachmentOpen` for lightbox.
44
+ - **Tool-payload dispatcher.** `dispatchToolPayload(matchers, fallback)` pluggable predicates render `<LazyJsonTree>` / `<LazyMap>` / etc.
45
+ - **Persisted dock prefs.** `useChatDockPrefs()` stores `mode` / `side` / `width` in localStorage; `<ChatHeaderModeToggle>` lets users flip popover ↔ side and survives reloads.
46
+ - **UX guards.** Auto-focus composer on dock open, two-step Escape (textarea blur → close), click-to-focus on empty message area (Slack / Linear style), `useHotkey('mod+/')` toggle.
47
+ - **ChatGPT-style autoscroll.** `MessageList` follows the bottom while the user is within `atBottomThreshold` px (default 120). Every user-sent message bumps `scrollAnchorId` and re-anchors the viewport with `behavior: 'smooth'` — sending no longer leaves your own bubble stuck above the fold. Scrolling up by hand breaks the lock; `<JumpToLatest>` brings it back.
48
+ - **Voice composer slot.** `<VoiceComposerSlot />` drops into `composerToolbarEnd` with **zero props** — reads/writes the composer through the `ComposerHandle` registered in chat context. The built-in `<Composer>` and TipTap-backed `MarkdownEditor` register themselves automatically; custom composers wire it via `useRegisterComposer({ focus, moveCursorToEnd, getValue, setValue })`. Auto-gates on Firefox / in-app WebViews / missing `getUserMedia`, preserves typed prefix, 90-second countdown, silence auto-stop, Esc / Enter hotkeys, start / stop earcons. See [`SpeechRecognition`](../SpeechRecognition/README.md).
49
+ - **Language flag button.** `<ChatHeaderLanguageButton />` slots into the dock header — 28×28 country flag opens a searchable `<Combobox>` with 66 BCP-47 tags from the Chrome Web Speech catalogue. Selection persists via `useSpeechPrefs`, picked up by every `useSpeechRecognition` downstream.
50
+ - **Auto-focus on stream end.** `useAutoFocusOnStreamEnd()` (active inside `ChatRoot` by default) re-focuses the composer on the streaming → idle edge — type → send → read → keep typing without reaching for the mouse.
51
+ - **Centralized colors.** Role-aware className tokens (`BUBBLE_SURFACE` / `ANCHOR` / `TOGGLE` / `DESTRUCTIVE_SURFACE`) + hooks (`useChatBubbleStyles`, `useChatRoleStyles`, `useChatDestructiveStyles`).
52
+ - **Responsive.** FAB `size='responsive'` (default): phone → `sm`, tablet → `md`, desktop → `lg`. Side mode is desktop-only and falls back to popover below `lg`.
53
+ - **Mobile fullscreen.** Dock auto-fills viewport below 768px via `useIsMobile`. Heights use `dvh/svh/lvh` so iOS Safari URL bar doesn't clip the chat.
38
54
  - **A11y.** `role="log"` + polite live region, `aria-busy` on streaming bubbles, `role="alert"` errors, focus-visible actions.
39
55
 
40
56
  ## Architecture
41
57
 
42
58
  ```
43
- Transport (interface) ← HTTP+SSE / Wails / WebSocket / mock
59
+ Transport (interface) ← Pydantic-AI / HTTP+SSE / Wails / mock
44
60
 
45
61
  Reducer (pure state machine)
46
62
 
@@ -50,507 +66,342 @@ ChatProvider (context)
50
66
 
51
67
  Components (MessageList, MessageBubble, Composer, Sources, ToolCalls, …)
52
68
 
53
- ChatRoot (one-line preset)
69
+ ChatRoot (one-line preset) ◄── optionally wrapped by ──► ChatLauncher (FAB + Dock + Greeting)
54
70
  ```
55
71
 
56
72
  Module boundaries:
57
73
 
58
74
  | Layer | May import | May NOT import |
59
75
  | ---------------- | -------------------------------- | ------------------------ |
60
- | `core/transport` | `core/types` | React, hooks, components |
61
- | `core/reducer` | `core/types` | React, transport |
76
+ | `types/` | | anything |
77
+ | `core/transport` | `types/` | React, hooks, components |
78
+ | `core/reducer` | `types/` | React, transport |
62
79
  | `hooks` | `core/*`, `ui-core` hooks | components |
63
80
  | `context` | `hooks` | components |
64
- | `components` | `hooks`, `context`, `ui-core` UI | transport implementations |
65
-
66
- ## Slots
67
-
68
- `<ChatRoot>` exposes named-prop slots for the most common roles. None are required; omit any slot to fall back to the default.
81
+ | `styles/` | `types/` | hooks, components |
82
+ | `components` | `hooks`, `context`, `styles`, `ui-core` UI | transport implementations |
83
+ | `launcher/` | `components`, `styles`, `ui-core` UI | transport implementations |
84
+
85
+ ## Types
86
+
87
+ Single source of truth in [`types/`](./types/index.ts). Split by domain — import from the top-level `@djangocfg/ui-tools` barrel.
88
+
89
+ | Domain | What's there |
90
+ |---|---|
91
+ | `persona` | `ChatRole`, `ChatPersona`, `ChatUserContext`, `ChatAssistantContext` |
92
+ | `message` | `ChatMessage` |
93
+ | `tool-call` | `ChatToolCall` |
94
+ | `attachment` | `ChatAttachment`, `ChatSource` |
95
+ | `labels` | `ChatLabels` + `DEFAULT_LABELS` |
96
+ | `config` | `ChatConfig`, `ChatPrefs`, `ChatDisplayMode` |
97
+ | `events` | `ChatStreamEvent` SSE union |
98
+ | `session` | `SessionInfo`, `HistoryPage`, `Stream/Send/CreateSessionOptions` |
99
+ | `transport` | `ChatTransport` |
100
+
101
+ ## Launcher (FAB + Dock + Greeting)
102
+
103
+ Picks the highest level that fits.
104
+
105
+ | Component | Use when |
106
+ |---|---|
107
+ | `<ChatLauncher>` | FAB + dock + greeting + push-preview + hotkey + audio toggle (~99% of cases) |
108
+ | `<ChatDock>` | Custom trigger (header button, inline link). Popover or side mode. |
109
+ | `<ChatFAB>` | Floating button only |
110
+ | `<ChatGreeting>` | Standalone proactive bubble |
111
+ | `<ChatUnreadPreview>` | Standalone push-notification bubble |
112
+ | `<ChatHeader>` + `<ChatHeaderActionButton>` | Custom header chrome |
113
+ | `<ChatHeaderModeToggle>` | Popover ↔ side toggle (desktop-only, auto-hides below `lg`) |
114
+ | `<ChatHeaderAudioToggle>` | Mute / unmute notification sounds |
115
+ | `<ChatHeaderResetButton>` | Clear conversation with `window.dialog.confirm` |
116
+ | `<ChatHeaderLanguageButton>` | Flag-button language picker (66 BCP-47 tags, persists in `useSpeechPrefs`) |
117
+
118
+ ### FAB
69
119
 
70
120
  ```tsx
71
- <ChatRoot
72
- transport={transport}
73
- config={{ greeting: 'Hi!' }}
74
-
75
- // ReactNode slots
76
- banner={<UpgradeBanner />} // sticky note above the conversation
77
- header={<MyChatHeader />} // title / actions row
78
- footer={<Disclaimer />} // below the composer
79
- empty={<MyEmptyState />} // replaces default <EmptyState>
80
- composerToolbarStart={<VoiceButton />} // left of the textarea
81
- composerToolbarEnd={<MentionButton />} // right of the textarea
82
- composerAttachmentTray={<MyTray />} // replaces default attachment tray
83
- jumpToLatest={<MyJumpPill />} // replaces floating "↓ N new" pill
84
-
85
- // Render-prop slots (need access to data)
86
- renderMessage={(m, i) => <MyBubble message={m} />}
87
- renderHeader={(ctx) => (ctx.isStreaming ? <Streaming /> : <Idle />)}
88
- renderEmpty={({ setValue, focus }) => <MyOnboarding onPick={setValue} />}
89
-
90
- // Tool-call payload renderers — forwarded to <MessageBubble toolCallsProps>
91
- toolCallsProps={{
92
- defaultExpanded: true,
93
- renderInput: (v) => <LazyJsonTree data={v} mode="compact" />,
94
- renderOutput: (v) => <LazyJsonTree data={v} mode="compact" />,
95
- }}
96
-
97
- // Hide composer while agent is paused waiting for human input
98
- hideComposer={hasPendingApprovals}
99
- />
100
- ```
101
-
102
- ### Slot inventory
103
-
104
- ```
105
- ┌───────────────────────────────┐
106
- │ banner (sticky) │
107
- ├───────────────────────────────┤
108
- │ header │
109
- ├───────────────────────────────┤
110
- │ messages (jumpToLatest) │
111
- ├───────────────────────────────┤
112
- │ composerToolbarStart │ ← hidden when hideComposer={true}
113
- │ [textarea] composerToolbarEnd │
114
- │ composerAttachmentTray │
115
- ├───────────────────────────────┤
116
- │ footer │ ← use for approval panels, disclaimers
117
- └───────────────────────────────┘
121
+ fab={{
122
+ variant: 'simple' | 'animated' | 'glass', // default 'simple'
123
+ size: 'responsive', // default — phone=sm/tablet=md/desktop=lg
124
+ position: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left',
125
+ offset: 24,
126
+ pulse: true, // attention dot + ping
127
+ badge: 5, // unread count (renders "9+")
128
+ tooltip: 'Open chat', // hover/focus label
129
+ icon: <Sparkles />,
130
+ inline: true, // no fixed positioning (stories/previews)
131
+ }}
118
132
  ```
119
133
 
120
- ### hideComposer — agent-pause / human-in-the-loop
121
-
122
- Set `hideComposer={true}` to remove the composer while the agent is waiting for a human decision (approval gate, HITL). Combine with `footer` to show action buttons:
134
+ ### Dock
123
135
 
124
136
  ```tsx
125
- <ChatRoot
126
- transport={transport}
127
- hideComposer={pendingActions.length > 0}
128
- footer={
129
- pendingActions.length > 0 ? (
130
- <AgentActions actions={pendingActions} onResult={handleResult} />
131
- ) : null
132
- }
133
- />
137
+ dock={{
138
+ title: 'Assistant',
139
+ mode: 'popover' | 'side', // default 'popover'; 'side' = full-height edge panel
140
+ side: 'left' | 'right', // default 'right' (only for mode='side')
141
+ width: 480, height: 720,
142
+ position: 'bottom-right', // popover mode only
143
+ offset: { horizontal: 24, vertical: 96 },
144
+ mobileFullscreen: true, // default — fills viewport < 768px
145
+ reserveBodySpace: true, // side mode — sets body padding so content shifts
146
+ disablePortal: true, // render in-place (stories/iframes)
147
+ hideHeader: true, // children render their own header
148
+ headerActions: <ChatHeaderResetButton ... />,
149
+ }}
134
150
  ```
135
151
 
136
- The composer reappears automatically once `hideComposer` returns `false`.
137
-
138
- ### ToolCalls — expand behavior
152
+ **Side mode is desktop-only.** Below `lg` (1024px) it silently falls back to popover and the `<ChatHeaderModeToggle>` hides itself.
139
153
 
140
- Panels are **collapsed by default**. While a tool is `running`, the panel auto-expands so you can see live `streamingText`; on completion it auto-collapses again. Manual user toggles (open or close) are remembered and override the auto-behavior for that call.
154
+ **Persisted prefs.** `useChatDockPrefs()` stores `mode` / `side` / `sideWidth` in localStorage. Drop it into your launcher to give users a remembered layout:
141
155
 
142
156
  ```tsx
143
- <ToolCalls
144
- calls={message.toolCalls}
145
- // Defaults:
146
- defaultExpanded={false} // start closed
147
- expandWhileStreaming={true} // open during run, close on completion
148
- />
157
+ const prefs = useChatDockPrefs({ storageKey: 'crm.chat.dock' });
158
+ <ChatLauncher
159
+ dock={{
160
+ mode: prefs.mode,
161
+ side: prefs.side,
162
+ width: prefs.mode === 'side' ? prefs.sideWidth : 480,
163
+ headerActions: <ChatHeaderModeToggle mode={prefs.mode} onToggle={prefs.toggleMode} />,
164
+ }}
165
+ >{children}</ChatLauncher>
149
166
  ```
150
167
 
151
- Force every panel open from the start (e.g. for debug views) with `defaultExpanded`.
152
-
153
- ### ToolCalls payload renderers
154
-
155
- `<ToolCalls>` (and `<MessageBubble toolCallsProps>` / `<ChatRoot toolCallsProps>`) accept payload renderers so you control how tool input / output / streaming text is displayed. Defaults render a cheap `<pre>`. For a richer experience, plug in `LazyJsonTree` from `@djangocfg/ui-tools/json-tree` with `mode="compact"`:
168
+ ### Greeting
156
169
 
157
170
  ```tsx
158
- import { LazyJsonTree } from '@djangocfg/ui-tools/json-tree';
171
+ // Short
172
+ greeting="Got a minute? I'm here to help."
159
173
 
160
- <ToolCalls
161
- calls={message.toolCalls}
162
- renderInput={(input) => <LazyJsonTree data={input} mode="compact" />}
163
- renderOutput={(output) => <LazyJsonTree data={output} mode="compact" />}
164
- />
174
+ // Full config
175
+ greeting={{
176
+ content: <>Looking for something? <strong>Chat with us</strong>.</>,
177
+ senderName: 'Anna · Support',
178
+ avatar: <Avatar><AvatarImage src="…" /></Avatar>,
179
+ delayMs: 1500,
180
+ dismissStorageKey: 'crm-greeting-v1',
181
+ hideOnOpen: true,
182
+ }}
165
183
  ```
166
184
 
167
- `renderInput` / `renderOutput` / `renderStreaming` take precedence; `renderPayload(value, kind, call)` is a single fallback if you want one renderer for all three.
168
-
169
- We don't import `LazyJsonTree` automatically — keeping the chat dep-light. The host opts in.
170
-
171
- ### ToolCalls — rich UI after panels (`renderAfterCalls`)
172
-
173
- `renderAfterCalls` renders **outside and below** all collapsible panels — always visible, regardless of whether panels are expanded or hidden. It receives the full `calls` array so you can aggregate across multiple tool calls (e.g. collect all vehicle IDs from `search_vehicles` + `get_vehicle` and render cards).
185
+ ### Hotkey
174
186
 
175
187
  ```tsx
176
- <ChatRoot
177
- toolCallsProps={{
178
- // Rich UI below all panels — always visible, never inside collapsed accordions
179
- renderAfterCalls: (calls) => <VehicleCardList calls={calls} />,
180
- }}
181
- />
188
+ hotkey={{ key: '/', meta: true }} // ⌘/ or Ctrl+/
189
+ hotkey={{ key: 'K', meta: true, shift: true }}
182
190
  ```
183
191
 
184
- #### hideToolCalls show only rich UI, no raw panels
192
+ When `meta` is unset, modifier-bearing combos do **not** trigger — prevents shadowing native shortcuts.
185
193
 
186
- Set `hideToolCalls={true}` to suppress accordion panels entirely while `renderAfterCalls` still renders. Use when you want clean product UI without raw tool payloads visible.
194
+ ### Controlled mode
187
195
 
188
196
  ```tsx
189
- <ChatRoot
190
- toolCallsProps={{
191
- hideToolCalls: true, // hides accordion panels
192
- renderAfterCalls: (calls) => <VehicleCards calls={calls} />, // still runs
193
- }}
194
- />
195
- ```
197
+ const [open, setOpen] = useState(false);
196
198
 
197
- `hideToolCalls` only affects the accordion panels — `renderAfterCalls` is always independent.
199
+ <ChatLauncher open={open} onOpenChange={setOpen} dock={{ title: 'Helper' }}>
200
+ <Chat />
201
+ </ChatLauncher>
202
+ ```
198
203
 
199
- #### renderToolCall custom per-call panel renderer
204
+ ### Live push notifications
200
205
 
201
- Replace the default `<ToolCallItem>` accordion with your own UI. Return `null` to suppress a specific call.
206
+ Inbound messages from outside the chat session (admin takeover, server-pushed alerts, Centrifugo broadcasts) flow through the same reducer as normal turns:
202
207
 
203
208
  ```tsx
204
- <ChatRoot
205
- toolCallsProps={{
206
- renderToolCall: (call) => (
207
- <div className="flex items-center gap-2 rounded border px-2 py-1 text-xs">
208
- <span className="font-mono">{call.name}</span>
209
- <span className="ml-auto">{call.status}</span>
210
- </div>
211
- ),
212
- }}
213
- />
209
+ // Inside ChatProvider — anywhere
210
+ const chat = useChatContext();
211
+
212
+ centrifugo.on('chat:inbound', (msg) => {
213
+ chat.injectMessage({
214
+ id: msg.id,
215
+ role: 'assistant',
216
+ content: msg.text,
217
+ createdAt: Date.now(),
218
+ sender: { name: msg.from, avatarUrl: msg.avatar },
219
+ });
220
+ });
221
+
222
+ chat.updateMessage(id, { content: editedText }); // live-edit by admin
223
+ chat.deleteMessage(id); // retract
214
224
  ```
215
225
 
216
- #### Full example vehicle cards from tool output
226
+ Surface them next to the FAB while the dock is closed:
217
227
 
218
228
  ```tsx
219
- import { buildVamcarToolCallsProps } from './chat/toolPayloads';
220
-
221
- // toolPayloads.tsx:
222
- export function buildVamcarToolCallsProps(): Omit<ToolCallsProps, 'calls'> {
223
- return {
224
- renderAfterCalls: (calls) => <VehicleCardsFromCalls calls={calls} />,
225
- };
229
+ function Launcher() {
230
+ const [open, setOpen] = useState(false);
231
+ const { unread, markRead } = useChatUnread({ open }); // inside ChatProvider
232
+ return (
233
+ <ChatLauncher
234
+ open={open}
235
+ onOpenChange={setOpen}
236
+ unreadMessage={unread}
237
+ onMarkRead={markRead}
238
+ // FAB badge "1" derived automatically; override via `fab.badge`
239
+ >…</ChatLauncher>
240
+ );
226
241
  }
227
-
228
- // In your chat component:
229
- const toolCallsProps = useMemo(() => buildVamcarToolCallsProps(), []);
230
- <ChatRoot transport={transport} toolCallsProps={toolCallsProps} />
231
242
  ```
232
243
 
233
- ## Personas (user & assistant identity)
244
+ `<ChatUnreadPreview>` (auto-rendered by `ChatLauncher` when `unreadMessage` is set) shows avatar + sender + 2-line truncated body + timestamp. Click → open + mark read. × → mark read without opening.
234
245
 
235
- Pass identities once on `<ChatRoot config>` — every bubble gets the right name, avatar and `aria-label`. Outgoing user messages get the `sender` field auto-stamped from `config.user`.
246
+ ### Audio
236
247
 
237
- ```tsx
238
- <ChatRoot
239
- transport={transport}
240
- config={{
241
- user: {
242
- name: 'Mark',
243
- avatarUrl: '/me.jpg',
244
- description: 'Senior engineer',
245
- },
246
- assistant: {
247
- name: 'Claude',
248
- avatarUrl: '/claude.png',
249
- model: 'claude-opus-4-7',
250
- },
251
- }}
252
- />
253
- ```
248
+ Wire `useChatAudio` once and pass it to `ChatLauncher` — the header gets an auto-injected mute toggle:
254
249
 
255
- Resolution cascade per bubble:
250
+ ```tsx
251
+ // Zero-setup — built-in sounds are bundled as base64 data URLs
252
+ const audio = useChatAudio();
256
253
 
257
- 1. `message.sender` per-message override (multi-user / multi-bot chats)
258
- 2. `config.user` / `config.assistant` — provider-level default
259
- 3. Hardcoded `'You'` / `'AI'` fallback
254
+ <ChatLauncher audio={audio} dock={{ title: 'Helper' }}>
255
+ <ChatRoot transport={transport} />
256
+ </ChatLauncher>
257
+ ```
260
258
 
261
- Multi-user example server returns `sender` per message:
259
+ **Built-in sounds.** The chat tool ships its own notification pack (`messageSent`, `messageReceived`, `streamStart`, `error`, `mention`, `notification`) inlined into the lazy chunk via tsup's `dataurl` loader. ~136KB total, ~180KB after base64 — paid only by hosts that actually import Chat. No assets to copy, no CDN to host.
262
260
 
263
- ```ts
264
- {
265
- id: 'm3',
266
- role: 'user',
267
- content: 'Flag the auth migration section.',
268
- sender: { name: 'Anna', avatarUrl: '/anna.jpg' },
269
- }
270
- ```
261
+ Customize:
271
262
 
272
- Initials are auto-derived from `name` (`'Mark Olofsen'` → `MO`); set `initials` explicitly to override.
263
+ ```tsx
264
+ import { useChatAudio, DEFAULT_CHAT_SOUNDS } from '@djangocfg/ui-tools';
273
265
 
274
- Helpers exposed for hosts that build their own bubbles:
266
+ // Override one event, keep the rest
267
+ useChatAudio({ sounds: { ...DEFAULT_CHAT_SOUNDS, mention: '/sfx/custom.mp3' } });
275
268
 
276
- ```ts
277
- import { resolvePersona, deriveInitials } from '@djangocfg/ui-tools';
278
- const persona = resolvePersona(message, config.user, config.assistant);
279
- const fallback = deriveInitials(persona, message.role);
269
+ // Disable entirely (header toggle hides itself)
270
+ useChatAudio({ sounds: {} });
280
271
  ```
281
272
 
282
- ## Attachments
273
+ Per-event volume defaults — Slack / Linear / Intercom style:
283
274
 
284
- Two specialised components plus a backwards-compatible facade:
275
+ | Event | Scale | Why |
276
+ |---|---|---|
277
+ | `error` | 0.25 | The destructive UI is the loud signal; sound is a soft ack |
278
+ | `streamStart` | 0.3 | Fires often, must stay subtle |
279
+ | `messageSent` | 0.5 | Self-confirmation |
280
+ | `messageReceived` | 0.7 | Baseline |
281
+ | `notification` | 0.9 | Push to a background tab — needs to be heard |
282
+ | `mention` | 1.0 | Louder than baseline — personal |
285
283
 
286
- | Component | Layout | Use it for |
287
- | ------------------- | -------------- | -------------------------------------------- |
288
- | `<AttachmentsGrid>` | `flex-wrap` | Thumbnails, file chips, no custom renderers |
289
- | `<AttachmentsList>` | `flex-col` | Custom renderers (audio, video, doc viewer) |
290
- | `<Attachments>` | picks for you | Backwards-compat: stacks when `renderers` is given, wraps otherwise |
284
+ Override via `eventVolumes: { error: 0, mention: 0.8 }`. Pass `eventVolumes: {}` to skip defaults entirely.
291
285
 
292
- `<MessageBubble>` automatically uses `AttachmentsList` when you pass `attachmentRenderers` and `AttachmentsGrid` otherwise `flex-wrap` shrinks block-level renderers to min-content, so picking the right component matters.
286
+ `audio.muted` / `audio.toggleMute()` are persisted in localStorage. Header `<ChatHeaderAudioToggle>` is hidden automatically when `audio.isSilent` (no sounds wired).
293
287
 
294
- ```tsx
295
- import { AttachmentsList } from '@djangocfg/ui-tools';
288
+ **Native hosts (cmdop_go / Tauri / Electron).** Skip web playback but keep the trigger as a side-channel:
296
289
 
297
- <AttachmentsList
298
- attachments={msg.attachments}
299
- renderers={{
300
- audio: ({ attachment }) => <LazyAudioPlayer src={attachment.url} variant="compact" />,
301
- }}
302
- />
290
+ ```tsx
291
+ const audio = useChatAudio({
292
+ silenced: true,
293
+ onSoundEvent: (e) => window.go.playSound(e),
294
+ });
303
295
  ```
304
296
 
305
- ## Audio triggers
306
-
307
- Optional sound effects on chat events. Off by default — you opt in with a `sounds` map. Six events:
297
+ See [`@djangocfg/ui-core` Audio docs](../../../../ui-core/README.md#audio) for the underlying `useNotificationSounds` primitives.
308
298
 
309
- | Event | Fires when |
310
- | ------------------ | --------------------------------------------------- |
311
- | `messageSent` | After `useChat.sendMessage()` adds the user message |
312
- | `messageReceived` | On `STREAM_DONE` (final assistant message) |
313
- | `streamStart` | First token / placeholder created |
314
- | `error` | `STREAM_ERROR` or transport throw |
315
- | `mention` | Host-fired (e.g. assistant addressed the user) |
316
- | `notification` | Generic — host triggers manually |
299
+ ### Reset / clear conversation
317
300
 
318
301
  ```tsx
319
- <ChatRoot
320
- transport={transport}
321
- audio={{
322
- sounds: {
323
- messageSent: '/audio/chat/sent.mp3',
324
- messageReceived: '/audio/chat/received.mp3',
325
- streamStart: '/audio/chat/start.mp3',
326
- error: '/audio/chat/error.mp3',
327
- },
328
- // Defaults: muteWhenHidden: true, respectReducedMotion: true, respectReducedData: true
329
- }}
302
+ <ChatHeaderResetButton
303
+ onReset={api.clearChat} // your backend POST /chat/reset
304
+ onSuccess={() => chat.clearMessages()}
305
+ confirmMessage="Forget this conversation? The assistant won't remember it."
330
306
  />
331
307
  ```
332
308
 
333
- Pitfalls handled inside (lifted from `AudioPlayer/audio`):
334
-
335
- - **iOS/Safari autoplay block.** First `pointerdown` / `keydown` inside `<ChatProvider>` runs a transactional unlock — every cached `<audio>` is poked once with `muted=true`, then released.
336
- - **Same-element race.** Each `play()` clones a fresh `Audio(url)` so two rapid events don't cancel each other; the cached element warms the HTTP cache.
337
- - **Cross-tab sync.** Volume / mute / per-event toggles live in a Zustand `persist` store with `localStorage` + the native `storage` event.
338
- - **Visibility.** Auto-mute while `document.visibilityState === 'hidden'`.
339
- - **Reduced motion / data.** Auto-suppress on `prefers-reduced-motion: reduce` and `prefers-reduced-data: reduce` (configurable).
340
- - **SSR-safe.** `audioBus` falls back to a no-op when `window` is undefined.
309
+ Calls `window.dialog.confirm` (destructive variant) from `@djangocfg/ui-core/lib/dialog-service`. Host must mount `<DialogProvider>`. Falls back to native `window.confirm` if the dialog service isn't installed.
341
310
 
342
- For programmatic control (e.g. mute toggle in your header):
311
+ ### Anti-patterns
343
312
 
344
- ```tsx
345
- const ctx = useChatContext();
346
- ctx.audio.setMuted(!ctx.audio.muted);
347
- ctx.audio.setVolume(0.6);
348
- ctx.audio.setEventEnabled('messageSent', false);
349
- ctx.audio.play('mention'); // host-fired
350
- ctx.audio.isUnlocked; // boolean
351
- ```
313
+ | Don't | Why |
314
+ |---|---|
315
+ | Two `<ChatLauncher>` on one page | Hotkey opens both at once |
316
+ | `hotkey={{ key: '/' }}` (no modifier) | Fires when user types `/` in any input |
317
+ | Mismatched `exitDurationMs` vs CSS | Dock unmounts before/after animation |
318
+ | Reusing `dismissStorageKey` across products | Greeting won't show on the second product |
319
+ | Calling `useChatUnread()` outside `ChatProvider` | Hook reads the messages store; throws without provider |
320
+ | Forgetting `<DialogProvider>` for `<ChatHeaderResetButton confirm>` | Falls back to native browser confirm (ugly) |
352
321
 
353
- Or directly: `useChatAudio(config)` returns the same surface, no provider required.
322
+ ## Stylingrole-aware tokens
354
323
 
355
- ## Composer focus & auto-focus on stream end
324
+ Every chat surface that depends on role/error state uses the centralized tokens, not inline Tailwind literals. Change `bg-primary`/contrast in one file — fixes every consumer.
356
325
 
357
- The chat context exposes a small composer registry so other parts of
358
- the tree can drive the composer imperatively without prop-drilling a
359
- ref. The built-in `<Composer>` registers itself on mount; custom
360
- composers opt in with a one-line hook.
326
+ ```ts
327
+ import { BUBBLE_SURFACE, ANCHOR, TOGGLE, DESTRUCTIVE_SURFACE } from '@djangocfg/ui-tools';
361
328
 
362
- ```tsx
363
- import { useChatContext, useRegisterComposer } from '@djangocfg/ui-tools';
329
+ // Token shape
330
+ BUBBLE_SURFACE.user // 'bg-primary text-primary-foreground rounded-tr-md'
331
+ BUBBLE_SURFACE.assistant // 'bg-muted text-foreground rounded-tl-md'
332
+ BUBBLE_SURFACE.error // 'bg-destructive/10 text-destructive rounded-tl-md border border-destructive/30'
364
333
 
365
- // Inside any custom composer:
366
- function MyComposer() {
367
- const focus = useCallback(() => myEditorRef.current?.commands.focus(), []);
368
- useRegisterComposer(focus);
369
- // ...
370
- }
334
+ ANCHOR.user // legible on bg-primary (uses primary-foreground, not white)
335
+ ANCHOR.assistant // brand-primary on neutral bubble
371
336
 
372
- // Then anywhere inside <ChatProvider>:
373
- const ctx = useChatContext();
374
- ctx.composer?.focus(); // null until any composer has mounted
337
+ DESTRUCTIVE_SURFACE.banner / .hover / .hoverStrong / .text / .menuItem
375
338
  ```
376
339
 
377
- ### `useAutoFocusOnStreamEnd`refocus when the reply lands
378
-
379
- Standard chat UX: user types → sends → reads the reply → starts
380
- typing again without reaching for the mouse. The hook listens for the
381
- streaming → idle transition and calls `.focus()` exactly once. Reading
382
- mid-stream is undisturbed.
383
-
384
- Zero-config (default: focuses the registered composer):
340
+ Prefer hooks in components they memoize and present a stable facade:
385
341
 
386
342
  ```tsx
387
- import { useAutoFocusOnStreamEnd } from '@djangocfg/ui-tools';
343
+ import { useChatBubbleStyles, useChatRoleStyles, useChatDestructiveStyles } from '@djangocfg/ui-tools';
388
344
 
389
- function MyChatShell() {
390
- useAutoFocusOnStreamEnd(); // reads isStreaming + composer from context
391
- return <ChatProvider>{/* */}</ChatProvider>;
345
+ function MyBubble({ message }) {
346
+ const { surface, anchor } = useChatBubbleStyles(message.role, !!message.isError);
347
+ return <div className={cn('rounded-2xl px-3 py-2', surface)}>…</div>;
392
348
  }
393
- ```
394
-
395
- Options:
396
-
397
- | option | default | when to set |
398
- |---------------|------------------------|-----------------------------------------------------------------------------|
399
- | `isStreaming` | `ctx.isStreaming` | Driving stream state from your own store (e.g. an external event bus). |
400
- | `targetRef` | registered composer | Focus something other than the composer — an "approve" button, etc. |
401
- | `enabled` | `true` | User preference toggle (off without unmounting the hook). |
402
- | `delayMs` | `0` (next rAF) | 50–150ms helps when the composer re-mounts after the final chunk. |
403
-
404
- The hook only fires on the `true → false` edge — flipping `enabled`
405
- mid-stream won't steal focus.
406
349
 
407
- ## Draft sanitation (pre-submit)
408
-
409
- `useChatComposer.submit()` cleans the draft before handing it to your
410
- `onSubmit` — mirroring what ChatGPT / Claude / Telegram ship. The
411
- helper is also exported standalone for custom composers:
412
-
413
- ```ts
414
- import { sanitizeDraft, isSubmittableDraft } from '@djangocfg/ui-tools'
350
+ function MyToggle({ isUser }) {
351
+ const { toggle } = useChatRoleStyles(isUser);
352
+ return <button className={toggle}>Read more</button>;
353
+ }
415
354
 
416
- sanitizeDraft(' \n\nhello world​\n ') // → 'hello world'
417
- isSubmittableDraft(' \n ') // false
355
+ function MyError() {
356
+ const { banner, hover } = useChatDestructiveStyles();
357
+ return <div className={banner}>…<button className={hover}>Dismiss</button></div>;
358
+ }
418
359
  ```
419
360
 
420
- ### Rules (intentionally minimal)
421
-
422
- | Rule | Action | Why |
423
- |---|---|---|
424
- | Trim outer whitespace | ✅ | Stray newlines/spaces at the edges are never intentional. |
425
- | Normalise `\r\n` / `\r` → `\n` | ✅ | Deterministic shape for LLM tokeniser + markdown render. |
426
- | Strip ZWSP / ZWNJ / ZWJ / BOM | ✅ | Pasted from rich web pages, invisible, break tokenisation. |
427
- | **Collapse internal whitespace runs** | ❌ | Would mangle code indentation. |
428
- | **Cap consecutive blank lines** | ❌ | Could be intentional separator (markdown / structured prompt). |
429
- | **Strip bidi overrides** (LRM/RLM/U+202A..E) | ❌ | Legitimately used in Arabic/Hebrew RTL content. |
430
- | Touch tabs vs spaces, emoji, mentions, URLs | ❌ | Passthrough. |
431
-
432
- Idempotent: `sanitizeDraft(sanitizeDraft(x)) === sanitizeDraft(x)`.
433
-
434
- ### Escape hatch — `preserveExactValue`
361
+ ## Transport contract
435
362
 
436
- Niche flows (clipboard inspector, raw-prompt debug tool) want
437
- byte-perfect passthrough. Opt out per-composer:
363
+ A single I/O seam.
438
364
 
439
365
  ```ts
440
- const composer = useChatComposer({
441
- onSubmit,
442
- preserveExactValue: true, // skip sanitation; raw textarea value goes through
443
- })
366
+ interface ChatTransport {
367
+ createSession(opts?): Promise<SessionInfo>;
368
+ loadHistory(sessionId, cursor?, limit?): Promise<HistoryPage>;
369
+ stream(sessionId, content, { signal, attachments, metadata }): AsyncGenerator<ChatStreamEvent>;
370
+ send(sessionId, content, options?): Promise<ChatMessage>;
371
+ closeSession(sessionId): Promise<void>;
372
+ }
444
373
  ```
445
374
 
446
- `canSubmit` still gates on `value.trim().length > 0` in this mode —
447
- an empty message is rarely intentional even when sanitation is off.
448
-
449
- ## Attachment renderers (registry)
375
+ Shipped implementations:
450
376
 
451
- `<Attachments>` and `<ChatRoot>` accept a per-type renderer map. Default tile is used when no renderer matches. Plug in heavy viewers (`LazyAudioPlayer`, `LazyImageViewer`, `LazyMap`) host-side without forcing `ui-tools/Chat` to depend on them.
377
+ - **`createPydanticAIChatTransport({ buildStreamUrl, buildHeaders, bootstrapSession, loadHistory, ... })`** for Django/pydantic-AI–style backends. Composes `parseSSE` + `mapPydanticAIEvent` + tool-id FIFO queue. Side-channel via `onPydanticEvent` for non-stream events (e.g. `approval_required`).
378
+ - **`createHttpTransport({ baseUrl, getAuthHeader, slug })`** — fetch + SSE for the canonical `POST /sessions/:id/messages` shape.
379
+ - **`createMockTransport({ replies, latencyMs })`** — scripted in-memory replies for stories/tests.
452
380
 
453
- ```tsx
454
- import { LazyPlayer as LazyAudioPlayer } from '@djangocfg/ui-tools/audio-player';
455
-
456
- <ChatRoot
457
- transport={transport}
458
- attachmentRenderers={{
459
- audio: ({ attachment }) => (
460
- <div className="my-1 max-w-md">
461
- <LazyAudioPlayer src={attachment.url} title={attachment.name} variant="compact" />
462
- </div>
463
- ),
464
- // image / video / file / default — all optional
465
- }}
466
- onAttachmentOpen={(att) => openLightbox(att)}
467
- />
468
- ```
381
+ Build a custom transport directly when none of the above fit.
469
382
 
470
- Renderer signature:
383
+ ### Pydantic-AI shortcuts
471
384
 
472
385
  ```ts
473
- type AttachmentRenderer = (args: {
474
- attachment: ChatAttachment;
475
- isInComposer: boolean; // true in the composer staging tray
476
- onClick?: () => void; // forwarded from <ChatRoot onAttachmentOpen>
477
- onRemove?: () => void; // present in the composer tray
478
- }) => ReactNode;
479
- ```
480
-
481
- `<MessageBubble attachmentsRenderer>` (a wholesale slot) still wins over the registry — use it when you need to swap the entire attachments block.
482
-
483
- ## Image lightbox helpers
484
-
485
- ```tsx
486
386
  import {
487
- useChatLightbox,
488
- collectImageAttachments,
387
+ createPydanticAIChatTransport,
388
+ createPydanticAISSEMap,
389
+ mapPydanticAIEvent,
390
+ createToolIdQueue,
489
391
  } from '@djangocfg/ui-tools';
490
- import { LazyImageViewer } from '@djangocfg/ui-tools/image-viewer';
491
-
492
- function MyChat() {
493
- const lightbox = useChatLightbox();
494
- const ctx = useChatContext();
495
- const gallery = useMemo(() => collectImageAttachments(ctx.messages), [ctx.messages]);
496
392
 
497
- return (
498
- <>
499
- <ChatRoot
500
- transport={transport}
501
- onAttachmentOpen={(att) => att.type === 'image' && lightbox.open(att, gallery)}
502
- />
503
- <Dialog open={!!lightbox.state} onOpenChange={(o) => !o && lightbox.close()}>
504
- <DialogContent className="max-w-5xl">
505
- {lightbox.state && (
506
- <LazyImageViewer
507
- images={lightbox.state.gallery.map((a) => ({
508
- file: { name: a.name ?? a.id, path: a.id },
509
- src: a.url,
510
- }))}
511
- initialIndex={lightbox.state.index}
512
- inDialog
513
- />
514
- )}
515
- </DialogContent>
516
- </Dialog>
517
- </>
518
- );
393
+ // In a custom transport / hand-rolled stream loop:
394
+ const toolIds = createToolIdQueue();
395
+ for await (const event of parseSSE(res, { map: createPydanticAISSEMap() })) {
396
+ yield event;
519
397
  }
520
398
  ```
521
399
 
522
- `useChatLightbox` is just a tiny `{ gallery, index } | null` state container — the host owns the modal + viewer mount, so `<LazyImageViewer>` (~50 KB) stays out of `ui-tools/Chat`.
523
-
524
- ## Tool-payload dispatcher
525
-
526
- For tool-call panels, `dispatchToolPayload(matchers, fallback)` lets you pick a renderer per payload shape — without writing your own switch each time.
400
+ ## Slots
527
401
 
528
- ```tsx
529
- import {
530
- dispatchToolPayload,
531
- isLatLng,
532
- isPlainObject,
533
- isGeoJSONFeatureCollection,
534
- } from '@djangocfg/ui-tools';
535
- import { LazyJsonTree } from '@djangocfg/ui-tools/json-tree';
536
- import { LazyMap } from '@djangocfg/ui-tools';
537
-
538
- const renderPayload = dispatchToolPayload(
539
- [
540
- {
541
- match: (v) => isGeoJSONFeatureCollection(v),
542
- render: (v) => <LazyMap geojson={v as GeoJSON.FeatureCollection} />,
543
- },
544
- { match: (v) => isLatLng(v), render: (v) => <LazyMap markers={[v as { lat: number; lng: number }]} /> },
545
- { match: (v) => isPlainObject(v), render: (v) => <LazyJsonTree data={v} mode="compact" /> },
546
- ],
547
- (v) => <pre className="text-[11px]">{String(v)}</pre>,
548
- );
549
-
550
- <ChatRoot toolCallsProps={{ defaultExpanded: true, renderPayload }} />;
551
- ```
402
+ `<ChatRoot>` exposes named-prop slots — `header`, `footer`, `banner`, `empty`, `composerToolbarStart/End`, `composerAttachmentTray`, `jumpToLatest`. None are required.
552
403
 
553
- First match wins. Predicates `isLatLng` / `isPlainObject` / `isGeoJSONFeatureCollection` / `isStringValue` are exported as building blocks.
404
+ See **[Slots inventory](#slots-inventory)** below for the full list, plus `hideComposer` / `hideToolCalls` / `renderToolCall` / `renderAfterCalls` patterns.
554
405
 
555
406
  ## Three usage patterns
556
407
 
@@ -562,8 +413,6 @@ First match wins. Predicates `isLatLng` / `isPlainObject` / `isGeoJSONFeatureCol
562
413
 
563
414
  ### 2. Composition
564
415
 
565
- Bring your own layout, reuse the parts.
566
-
567
416
  ```tsx
568
417
  <ChatProvider transport={transport}>
569
418
  <MyHeader />
@@ -572,170 +421,138 @@ Bring your own layout, reuse the parts.
572
421
  </ChatProvider>
573
422
  ```
574
423
 
575
- ### 3. Headless (just hooks)
424
+ ### 3. Headless
576
425
 
577
426
  ```tsx
578
427
  const chat = useChat({ transport });
579
428
  const composer = useChatComposer({ onSubmit: chat.sendMessage });
580
- // render however you want
581
- ```
582
-
583
- ## Transport contract
584
-
585
- A single I/O seam.
586
-
587
- ```ts
588
- interface ChatTransport {
589
- createSession(opts?): Promise<SessionInfo>;
590
- loadHistory(sessionId, cursor?, limit?): Promise<HistoryPage>;
591
- stream(sessionId, content, { signal, attachments, metadata }): AsyncGenerator<ChatStreamEvent>;
592
- send(sessionId, content, options?): Promise<ChatMessage>;
593
- closeSession(sessionId): Promise<void>;
594
- }
595
429
  ```
596
430
 
597
- Shipped implementations:
431
+ ## Mobile & a11y
598
432
 
599
- - **`createHttpTransport({ baseUrl, getAuthHeader, slug })`** fetch + SSE, default for web.
600
- - **`createMockTransport({ replies, latencyMs })`** scripted in-memory replies for stories/tests.
601
-
602
- Custom hosts (Wails RPC, WebSocket, gRPC) implement the same interface — see [`@dev/@refactoring7-chat/06-integration.md`](../../../@dev/@refactoring7-chat/06-integration.md) for adapter recipes.
433
+ - `useChatLayout` collapses `sidebar`/`floating` to `fullscreen` below `(max-width: 640px)`.
434
+ - `ChatDock` fills the viewport below 768px (`mobileFullscreen` prop, default `true`).
435
+ - Composer textarea is `font-size: 16px` to disable iOS focus-zoom; container uses `100dvh` and `env(safe-area-inset-bottom)`.
436
+ - `MessageList` is `role="log"` with `aria-live="polite"`; streaming bubbles set `aria-busy`.
437
+ - `ErrorBanner` is `role="alert"`.
438
+ - `JumpToLatest` announces unread count via `aria-live="polite"`.
439
+ - All visible Boundary fallbacks and the Greeting bubble carry `role="alert"` / `aria-live`.
603
440
 
604
441
  ## Public surface
605
442
 
606
443
  ```ts
607
- // Types
444
+ // Types (re-exported from types/)
608
445
  ChatMessage, ChatRole, ChatPersona, ChatToolCall, ChatAttachment, ChatSource,
609
- ChatConfig, ChatUserContext, ChatAssistantContext, ChatPrefs, ChatLabels,
446
+ ChatConfig, ChatUserContext, ChatAssistantContext, ChatPrefs, ChatLabels, DEFAULT_LABELS,
610
447
  ChatTransport, ChatStreamEvent, ChatDisplayMode,
611
- SessionInfo, HistoryPage, StreamOptions, SendOptions
448
+ SessionInfo, HistoryPage, StreamOptions, SendOptions, CreateSessionOptions
612
449
 
613
- // Core (pure)
450
+ // Core
614
451
  reducer, initialState, createId, createTokenBuffer,
615
452
  resolvePersona, deriveInitials
616
- type ChatState, type ChatAction
453
+ type ChatState, type ChatAction, type TokenBuffer
617
454
 
618
455
  // Transport
619
- createHttpTransport, createMockTransport, parseSSE, TransportError
456
+ createHttpTransport, createMockTransport, createPydanticAIChatTransport,
457
+ mapPydanticAIEvent, createPydanticAISSEMap, createToolIdQueue,
458
+ parseSSE, TransportError
459
+ type HttpTransportConfig, type MockTransportOptions, type ParseSSEOptions,
460
+ type PydanticAIChatTransportOpts, type PydanticAIEvent, type ToolIdQueue
620
461
 
621
462
  // Hooks
622
463
  useChat, useChatComposer, useChatScroll, useChatHistory, useChatLayout,
623
- useChatAudio, useChatLightbox,
624
- useAutoFocusOnStreamEnd, useRegisterComposer,
625
- type UseAutoFocusOnStreamEndOptions, type Focusable
464
+ useChatAudio, useChatLightbox, useAutoFocusOnStreamEnd, useRegisterComposer,
465
+ useChatReset, useVisitorFingerprint, useChatDockPrefs, useChatUnread,
466
+ useFocusOnEmptyClick
467
+ type ChatDockPrefs, DEFAULT_DOCK_PREFS
468
+
469
+ // Styles — role-aware tokens + hooks
470
+ BUBBLE_SURFACE, ANCHOR, TOGGLE, DESTRUCTIVE_SURFACE, TOOL_CALL,
471
+ useChatBubbleStyles, useChatRoleStyles, useChatDestructiveStyles
472
+ type ChatBubbleSurface, type ChatBubbleStyles, type ChatRoleStyles, type ChatDestructiveStyles
473
+
474
+ // Launcher
475
+ ChatFAB, ChatDock, ChatHeader, ChatHeaderActionButton, ChatHeaderModeToggle,
476
+ ChatHeaderAudioToggle, ChatHeaderResetButton, ChatHeaderLanguageButton,
477
+ ChatLauncher, ChatGreeting, ChatUnreadPreview, useChatPresence
478
+ type ChatFABProps, type ChatFABVariant, type ChatFABSize, type ChatFABPosition,
479
+ type ChatDockProps, type ChatDockMode, type ChatDockSide,
480
+ type ChatHeaderProps, type ChatHeaderActionButtonProps, type ChatHeaderModeToggleProps,
481
+ type ChatHeaderAudioToggleProps, type ChatHeaderResetButtonProps,
482
+ type ChatHeaderLanguageButtonProps,
483
+ type ChatGreetingProps, type ChatUnreadPreviewProps,
484
+ type ChatLauncherProps, type ChatLauncherHotkey, type ChatLauncherGreeting,
485
+ type ChatPresencePhase
626
486
 
627
487
  // Audio
628
488
  ChatAudioConfig, ChatAudioEvent, ChatAudioSounds, UseChatAudioReturn,
629
- useChatAudioPrefs
489
+ useChatAudioPrefs, DEFAULT_CHAT_SOUNDS
630
490
 
631
491
  // Tool-payload dispatch
632
492
  dispatchToolPayload, isPlainObject, isLatLng,
633
493
  isGeoJSONFeatureCollection, isStringValue,
634
- type ToolPayloadMatcher, type ToolPayloadFallback,
635
- type ToolPayloadKind, type ToolCallsProps
494
+ type ToolPayloadMatcher, type ToolPayloadFallback, type ToolPayloadKind, type ToolCallsProps
636
495
 
637
- // Lightbox helpers
638
- collectImageAttachments
639
-
640
- // Draft sanitation (pre-submit cleanup; see "Draft sanitation" above)
496
+ // Draft sanitation
641
497
  sanitizeDraft, isSubmittableDraft
642
498
 
643
499
  // Context
644
- ChatProvider, useChatContext, useChatContextOptional,
645
- type ComposerHandle
500
+ ChatProvider, useChatContext, useChatContextOptional, type ComposerHandle
501
+ // `ComposerHandle` shape: { focus, moveCursorToEnd?, getValue?, setValue? }.
502
+ // Voice slot, autoFocus hook, and any external driver read it from
503
+ // context — built-in Composer registers itself, custom composers use
504
+ // `useRegisterComposer({...})`.
646
505
 
647
506
  // Components
648
507
  ChatRoot, MessageList, MessageBubble, MessageActions, Composer,
649
508
  Sources, ToolCalls, Attachments, EmptyState, ErrorBanner,
650
509
  JumpToLatest, StreamingIndicator
510
+ // `MessageList` exposes `atBottomThreshold` (default 120 px) and
511
+ // `scrollAnchorId` props for ChatGPT-style autoscroll — see "What you get".
651
512
 
652
513
  // Lazy preset
653
514
  LazyChat
654
515
  ```
655
516
 
656
- ## Mobile & a11y notes
657
-
658
- - `useChatLayout` collapses `sidebar`/`floating` to `fullscreen` below `(max-width: 640px)`.
659
- - Composer textarea is `font-size: 16px` to disable iOS focus-zoom; container uses `100dvh` and `env(safe-area-inset-bottom)`.
660
- - `MessageList` is `role="log"` with `aria-live="polite"`; streaming bubbles set `aria-busy`.
661
- - `ErrorBanner` is `role="alert"`.
662
- - `JumpToLatest` announces unread count via `aria-live="polite"`.
663
-
664
- ## Hotkeys
665
-
666
- | Key | Behavior |
667
- | ------------------ | --------------------------------------- |
668
- | `Enter` | Send (configurable to `Cmd/Ctrl+Enter`) |
669
- | `Shift+Enter` | Newline |
670
- | `↑` (empty input) | Recall previous submission |
671
- | `↓` | Recall next |
672
- | `Esc` | (host-bound) cancel stream |
673
-
674
- > **Custom composers built on `MarkdownEditor`:** pass `onSubmit` to
675
- > the editor for the same behaviour. A React `onKeyDown` wrapper does
676
- > NOT reliably intercept Enter before Tiptap commits the HardBreak
677
- > — see `MarkdownEditor/README.md#submit-on-enter` for the full
678
- > incident write-up and the keymap-extension fix.
679
-
680
- ## Performance
517
+ ## Storybook
681
518
 
682
- - **Token coalescing.** `createTokenBuffer` aggregates stream chunks within ~16ms before dispatching → ≤1 render per frame.
683
- - **Plain text during stream.** `MessageBubble` skips ReactMarkdown until the message finishes, then re-renders once with full markdown.
684
- - **Memoized bubbles.** Memo key `(id, content, isStreaming, version, toolActivity, toolCalls, sources, attachments)` — references only.
685
- - **Virtualization is on by default.** As of plan64, `<MessageList>` is built on [`react-virtuoso`](https://virtuoso.dev/). Sticky-bottom + auto-follow on streaming come from `followOutput`, top-of-list pagination from `startReached`, scroll-anchor preservation on prepend from `firstItemIndex`. No host-side virtualizer needed. Set `noVirtualize` on `<MessageList>` to opt out (stories / DevTools tracing).
519
+ Stories live next to components open `@djangocfg/playground`:
686
520
 
687
- ### Scroll API
521
+ - `Tools/Chat/Basic` — Default + Composition patterns.
522
+ - `Tools/Chat/Bubbles` — every bubble state in one screen.
523
+ - `Tools/Chat/ToolCalls` — streaming panels + JsonTree payload dispatch.
524
+ - `Tools/Chat/Personas` — per-message persona overrides, multi-user transcripts.
525
+ - `Tools/Chat/Launcher` — Default / Playground / LiveChatStyle / MobileFullscreen / WithLivePush / VariantsAndSizes.
526
+ - `Tools/Chat/Header` — `ChatHeader` standalone, persistent dock prefs, side mode.
527
+ - `Tools/Chat/Audio & Actions` — audio toggle auto-inject, reset with `window.dialog.confirm`.
528
+ - `Tools/Chat/Voice composer` — `<VoiceComposerSlot>` wired into the composer toolbar with a mock STT engine.
688
529
 
689
- `<MessageList>` exposes an imperative handle:
530
+ ---
690
531
 
691
- ```tsx
692
- const listRef = useRef<MessageListHandle>(null);
532
+ <a id="slots-inventory"></a>
693
533
 
694
- <MessageList
695
- ref={listRef}
696
- onAtBottomChange={setIsAtBottom} // drives "Jump to latest" pill
697
- onStartReached={() => void loadMore()} // top-of-list pagination
698
- />;
699
-
700
- listRef.current?.scrollToBottom(true); // smooth jump
701
- listRef.current?.scrollToIndex(42); // jump to a specific bubble
702
- ```
534
+ ## Slots inventory
703
535
 
704
- The legacy `useChatScroll` hook is kept for hosts that render their
705
- own (non-virtualized) scroll container, but is `@deprecated` for use
706
- with `<MessageList>` the component owns sticky-bottom internally.
707
-
708
- ## Design docs
536
+ | Slot | Position | Render-prop variant |
537
+ |---|---|---|
538
+ | `header` | Above messages | `renderHeader({ messages })` |
539
+ | `footer` | Below composer | `renderFooter({ canSend })` |
540
+ | `banner` | Top of message list | `renderBanner({ error, dismiss, retry })` |
541
+ | `empty` | Empty state | `renderEmpty({ config, send })` |
542
+ | `composerToolbarStart` / `composerToolbarEnd` | Composer toolbar | — |
543
+ | `composerAttachmentTray` | Above composer textarea | `renderAttachmentTray({ attachments })` |
544
+ | `jumpToLatest` | Sticky overlay | `renderJumpToLatest({ unread, scrollToBottom })` |
545
+ | `renderToolCall` | Per tool-call panel | `(call) => ReactNode` |
546
+ | `renderAfterCalls` | After all tool panels | `(calls) => ReactNode` (rich UI like vehicle cards) |
709
547
 
710
- Full implementation plan and rationale lives at [`@dev/@refactoring7-chat/`](../../../@dev/@refactoring7-chat/):
548
+ Flags:
711
549
 
712
- - `00-overview.md` — file layout & public API
713
- - `01-architecture.md` — layered model, dataflow, transport contract
714
- - `02-state-model.md` — reducer actions and invariants
715
- - `03-hooks.md` — every hook signature
716
- - `04-components.md` — every component prop shape
717
- - `05-mobile-and-a11y.md` — breakpoints, iOS, ARIA, focus, RTL
718
- - `06-integration.md` — adapter recipes
719
- - `07-testing.md` — reducer / hook / component / a11y tests
720
- - `08-migration.md` — step-by-step rollout
550
+ - `hideComposer` — agent-pause / human-in-the-loop pause; composer is unmounted.
551
+ - `hideToolCalls` — show only `renderAfterCalls` rich UI without raw tool panels.
721
552
 
722
- ## Storybook
553
+ ## Hotkeys
723
554
 
724
- `Tools/Chat` covers:
725
-
726
- - `Default` — full ChatRoot with mock transport and suggestions
727
- - `WithToolCalls` scripted tool invocation + sources
728
- - `Composition` — bring your own layout (`<ChatProvider>` + parts)
729
- - `Bubbles` — visual matrix of message states
730
- - `Parts` — every decomposed component on its own (incl. `ToolCalls` + `LazyJsonTree` compact)
731
- - `WithSlots` — every named slot at once (banner / header / empty / composer toolbar / footer)
732
- - `WithJsonTreePayload` — `<ChatRoot toolCallsProps>` with `LazyJsonTree` payload renderers
733
- - `WithAudio` — chat-event sound triggers + unlock-state badge
734
- - `WithAudioAttachment` — attachment registry mounts `<LazyAudioPlayer>` for `audio` items
735
- - `WithImageLightbox` — `useChatLightbox` + host-side `<Dialog>` + `<LazyImageViewer>`
736
- - `WithMapPayload` — `dispatchToolPayload` with `LazyJsonTree` fallback
737
- - `WithPersonas` — config-level `user` + `assistant` identity (avatar, name)
738
- - `MultiUser` — per-message `sender` overrides (multi-user / multi-bot)
739
- - `WithHideComposer` — `hideComposer` + `footer` approval gate (HITL / agent-pause pattern)
740
- - `WithRenderAfterCalls` — `renderAfterCalls` (vehicle cards outside panels), `hideToolCalls`, `renderToolCall` (custom per-call renderer) — knob-controlled
741
- - `Playground` — knobs (latency, streaming, suggestions)
555
+ - `Enter` — send (or `Cmd+Enter` if `prefs.submitOn = 'cmd+enter'`).
556
+ - `Shift+Enter` — newline.
557
+ - `Esc` — cancel current streaming turn (when focused inside the composer).
558
+ - Launcher hotkey: configurable via `<ChatLauncher hotkey={…}>` (e.g. `⌘/`).