@djangocfg/ui-tools 2.1.334 → 2.1.336

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 (196) hide show
  1. package/README.md +68 -2
  2. package/dist/ChatRoot-IIYQEWUU.mjs +5 -0
  3. package/dist/ChatRoot-IIYQEWUU.mjs.map +1 -0
  4. package/dist/ChatRoot-PNNGQCYF.css +7 -0
  5. package/dist/ChatRoot-PNNGQCYF.css.map +1 -0
  6. package/dist/ChatRoot-UUKTYM4N.cjs +14 -0
  7. package/dist/ChatRoot-UUKTYM4N.cjs.map +1 -0
  8. package/dist/{CronScheduler.client-3O3VU4CI.mjs → CronScheduler.client-DLMXCPAJ.mjs} +4 -4
  9. package/dist/{CronScheduler.client-3O3VU4CI.mjs.map → CronScheduler.client-DLMXCPAJ.mjs.map} +1 -1
  10. package/dist/{CronScheduler.client-A4GO6YBY.cjs → CronScheduler.client-WEJF4PWQ.cjs} +14 -14
  11. package/dist/{CronScheduler.client-A4GO6YBY.cjs.map → CronScheduler.client-WEJF4PWQ.cjs.map} +1 -1
  12. package/dist/{DocsLayout-XLDB6CJ2.cjs → DocsLayout-N5ZJZPBY.cjs} +200 -199
  13. package/dist/DocsLayout-N5ZJZPBY.cjs.map +1 -0
  14. package/dist/{DocsLayout-CTJINVBM.mjs → DocsLayout-VFPPNKSQ.mjs} +7 -6
  15. package/dist/DocsLayout-VFPPNKSQ.mjs.map +1 -0
  16. package/dist/JsonSchemaForm-DD7CLRIG.cjs +13 -0
  17. package/dist/{JsonSchemaForm-OSPUUUHM.cjs.map → JsonSchemaForm-DD7CLRIG.cjs.map} +1 -1
  18. package/dist/JsonSchemaForm-XKUIVELK.mjs +4 -0
  19. package/dist/{JsonSchemaForm-TSLX2GRO.mjs.map → JsonSchemaForm-XKUIVELK.mjs.map} +1 -1
  20. package/dist/JsonTree-55625VVH.mjs +5 -0
  21. package/dist/{JsonTree-F27RMYSI.cjs.map → JsonTree-55625VVH.mjs.map} +1 -1
  22. package/dist/JsonTree-DCM5QGWF.cjs +11 -0
  23. package/dist/{JsonTree-QTJYSHCV.mjs.map → JsonTree-DCM5QGWF.cjs.map} +1 -1
  24. package/dist/{LottiePlayer.client-6WVWDO75.cjs → LottiePlayer.client-2S7ISJ2S.cjs} +6 -6
  25. package/dist/{LottiePlayer.client-6WVWDO75.cjs.map → LottiePlayer.client-2S7ISJ2S.cjs.map} +1 -1
  26. package/dist/{LottiePlayer.client-B4I6WNZM.mjs → LottiePlayer.client-5LDSSJWS.mjs} +4 -4
  27. package/dist/{LottiePlayer.client-B4I6WNZM.mjs.map → LottiePlayer.client-5LDSSJWS.mjs.map} +1 -1
  28. package/dist/{MapContainer-RYG4HPH4.cjs → MapContainer-76YL2JXL.cjs} +8 -8
  29. package/dist/{MapContainer-RYG4HPH4.cjs.map → MapContainer-76YL2JXL.cjs.map} +1 -1
  30. package/dist/{MapContainer-GXQLP5WY.mjs → MapContainer-7HXBI3OH.mjs} +3 -3
  31. package/dist/{MapContainer-GXQLP5WY.mjs.map → MapContainer-7HXBI3OH.mjs.map} +1 -1
  32. package/dist/{Mermaid.client-SXRRI2YW.mjs → Mermaid.client-NL4SVR7F.mjs} +4 -4
  33. package/dist/{Mermaid.client-SXRRI2YW.mjs.map → Mermaid.client-NL4SVR7F.mjs.map} +1 -1
  34. package/dist/{Mermaid.client-W76R5AKJ.cjs → Mermaid.client-NNTI6DFX.cjs} +26 -26
  35. package/dist/{Mermaid.client-W76R5AKJ.cjs.map → Mermaid.client-NNTI6DFX.cjs.map} +1 -1
  36. package/dist/Player-BRV7XTWR.mjs +4 -0
  37. package/dist/{Player-M3GC3VPE.mjs.map → Player-BRV7XTWR.mjs.map} +1 -1
  38. package/dist/Player-PM7F7DD7.cjs +13 -0
  39. package/dist/{Player-ZL2X5LGG.cjs.map → Player-PM7F7DD7.cjs.map} +1 -1
  40. package/dist/{PrettyCode.client-RPDIE5CH.cjs → PrettyCode.client-KOHDVPPN.cjs} +13 -13
  41. package/dist/{PrettyCode.client-RPDIE5CH.cjs.map → PrettyCode.client-KOHDVPPN.cjs.map} +1 -1
  42. package/dist/{PrettyCode.client-SPMTQEG4.mjs → PrettyCode.client-ZGYGKE7G.mjs} +4 -4
  43. package/dist/{PrettyCode.client-SPMTQEG4.mjs.map → PrettyCode.client-ZGYGKE7G.mjs.map} +1 -1
  44. package/dist/TreeRoot-N72OYKXU.cjs +19 -0
  45. package/dist/{TreeRoot-A3J65L6F.mjs.map → TreeRoot-N72OYKXU.cjs.map} +1 -1
  46. package/dist/TreeRoot-VGAIXCUA.mjs +4 -0
  47. package/dist/{TreeRoot-DSK5JILT.cjs.map → TreeRoot-VGAIXCUA.mjs.map} +1 -1
  48. package/dist/chunk-2ZLKZ5VR.mjs +631 -0
  49. package/dist/chunk-2ZLKZ5VR.mjs.map +1 -0
  50. package/dist/{chunk-LFWQ36LJ.mjs → chunk-5G5YBFS6.mjs} +4 -4
  51. package/dist/{chunk-LFWQ36LJ.mjs.map → chunk-5G5YBFS6.mjs.map} +1 -1
  52. package/dist/{chunk-IHAY6FO6.cjs → chunk-5I5QNGUG.cjs} +17 -17
  53. package/dist/{chunk-IHAY6FO6.cjs.map → chunk-5I5QNGUG.cjs.map} +1 -1
  54. package/dist/{chunk-F2CMIIOH.cjs → chunk-76NNDZH6.cjs} +42 -42
  55. package/dist/{chunk-F2CMIIOH.cjs.map → chunk-76NNDZH6.cjs.map} +1 -1
  56. package/dist/chunk-B5AWZOHJ.cjs +649 -0
  57. package/dist/chunk-B5AWZOHJ.cjs.map +1 -0
  58. package/dist/{chunk-KR6B3LVY.mjs → chunk-B6IR5KSC.mjs} +3 -3
  59. package/dist/{chunk-KR6B3LVY.mjs.map → chunk-B6IR5KSC.mjs.map} +1 -1
  60. package/dist/{chunk-5LBDYFWH.mjs → chunk-C6GXVH5J.mjs} +3 -3
  61. package/dist/{chunk-5LBDYFWH.mjs.map → chunk-C6GXVH5J.mjs.map} +1 -1
  62. package/dist/{chunk-4IW7GZFQ.cjs → chunk-FEN5S772.cjs} +74 -48
  63. package/dist/chunk-FEN5S772.cjs.map +1 -0
  64. package/dist/{chunk-2SMCH62O.cjs → chunk-FP2RLYQZ.cjs} +11 -11
  65. package/dist/{chunk-2SMCH62O.cjs.map → chunk-FP2RLYQZ.cjs.map} +1 -1
  66. package/dist/{chunk-MOME6KYD.mjs → chunk-G5IEC7SR.mjs} +3 -3
  67. package/dist/{chunk-MOME6KYD.mjs.map → chunk-G5IEC7SR.mjs.map} +1 -1
  68. package/dist/{chunk-EXGXUK2N.mjs → chunk-GYIO7W7M.mjs} +41 -15
  69. package/dist/chunk-GYIO7W7M.mjs.map +1 -0
  70. package/dist/{chunk-3Z3A7FHA.cjs → chunk-IEEAENLX.cjs} +48 -48
  71. package/dist/{chunk-3Z3A7FHA.cjs.map → chunk-IEEAENLX.cjs.map} +1 -1
  72. package/dist/{chunk-DFTVB66S.cjs → chunk-KNDLV4PI.cjs} +85 -85
  73. package/dist/{chunk-DFTVB66S.cjs.map → chunk-KNDLV4PI.cjs.map} +1 -1
  74. package/dist/{chunk-SSUOENAZ.mjs → chunk-KNEQRUBA.mjs} +3 -3
  75. package/dist/{chunk-SSUOENAZ.mjs.map → chunk-KNEQRUBA.mjs.map} +1 -1
  76. package/dist/chunk-KRETIZU6.mjs +2218 -0
  77. package/dist/chunk-KRETIZU6.mjs.map +1 -0
  78. package/dist/{chunk-CGILA3WO.mjs → chunk-N2XQF2OL.mjs} +5 -3
  79. package/dist/{chunk-CGILA3WO.mjs.map → chunk-N2XQF2OL.mjs.map} +1 -1
  80. package/dist/{chunk-EUADAUBQ.mjs → chunk-N4MZYNR4.mjs} +4 -4
  81. package/dist/{chunk-EUADAUBQ.mjs.map → chunk-N4MZYNR4.mjs.map} +1 -1
  82. package/dist/chunk-NRXYYO5V.cjs +2257 -0
  83. package/dist/chunk-NRXYYO5V.cjs.map +1 -0
  84. package/dist/{chunk-GGKGH5PM.mjs → chunk-OBRSGM64.mjs} +4 -4
  85. package/dist/{chunk-GGKGH5PM.mjs.map → chunk-OBRSGM64.mjs.map} +1 -1
  86. package/dist/{chunk-6JTB2X72.mjs → chunk-ODO4GMW7.mjs} +3 -3
  87. package/dist/{chunk-6JTB2X72.mjs.map → chunk-ODO4GMW7.mjs.map} +1 -1
  88. package/dist/{chunk-WGEGR3DF.cjs → chunk-OLISEQHS.cjs} +5 -2
  89. package/dist/{chunk-WGEGR3DF.cjs.map → chunk-OLISEQHS.cjs.map} +1 -1
  90. package/dist/{chunk-PZKAH7WQ.mjs → chunk-PVAX67JG.mjs} +3 -3
  91. package/dist/{chunk-PZKAH7WQ.mjs.map → chunk-PVAX67JG.mjs.map} +1 -1
  92. package/dist/{chunk-PRPG2T2E.cjs → chunk-QJ6GTUCO.cjs} +6 -6
  93. package/dist/{chunk-PRPG2T2E.cjs.map → chunk-QJ6GTUCO.cjs.map} +1 -1
  94. package/dist/chunk-QW4RBGHN.cjs +961 -0
  95. package/dist/chunk-QW4RBGHN.cjs.map +1 -0
  96. package/dist/{chunk-33AMWFBZ.cjs → chunk-SGP7V2UW.cjs} +15 -15
  97. package/dist/{chunk-33AMWFBZ.cjs.map → chunk-SGP7V2UW.cjs.map} +1 -1
  98. package/dist/{chunk-FX2QFYWF.mjs → chunk-VWQ5WOIL.mjs} +3 -3
  99. package/dist/{chunk-FX2QFYWF.mjs.map → chunk-VWQ5WOIL.mjs.map} +1 -1
  100. package/dist/{chunk-ZLQHUZDU.cjs → chunk-YDPDTOSP.cjs} +139 -139
  101. package/dist/{chunk-ZLQHUZDU.cjs.map → chunk-YDPDTOSP.cjs.map} +1 -1
  102. package/dist/{chunk-77HQWEQ6.cjs → chunk-YW5IVWHQ.cjs} +33 -33
  103. package/dist/{chunk-77HQWEQ6.cjs.map → chunk-YW5IVWHQ.cjs.map} +1 -1
  104. package/dist/{chunk-YXBOAGIM.cjs → chunk-YXZ6GU7H.cjs} +7 -7
  105. package/dist/{chunk-YXBOAGIM.cjs.map → chunk-YXZ6GU7H.cjs.map} +1 -1
  106. package/dist/{chunk-62Y65TGK.mjs → chunk-ZUFTH5IR.mjs} +8 -631
  107. package/dist/chunk-ZUFTH5IR.mjs.map +1 -0
  108. package/dist/components-EHOGXATG.cjs +22 -0
  109. package/dist/{components-5UXYNAKR.cjs.map → components-EHOGXATG.cjs.map} +1 -1
  110. package/dist/components-MQ6DR7TX.cjs +26 -0
  111. package/dist/{components-CFXOEVPN.mjs.map → components-MQ6DR7TX.cjs.map} +1 -1
  112. package/dist/components-XRX7QGLB.mjs +5 -0
  113. package/dist/{components-WYEZL5TE.cjs.map → components-XRX7QGLB.mjs.map} +1 -1
  114. package/dist/components-YATKRWLH.mjs +5 -0
  115. package/dist/{components-ZAGG2PBO.mjs.map → components-YATKRWLH.mjs.map} +1 -1
  116. package/dist/file-icon/index.cjs +6 -6
  117. package/dist/file-icon/index.mjs +1 -1
  118. package/dist/index.cjs +735 -215
  119. package/dist/index.cjs.map +1 -1
  120. package/dist/index.d.cts +972 -39
  121. package/dist/index.d.ts +972 -39
  122. package/dist/index.mjs +387 -31
  123. package/dist/index.mjs.map +1 -1
  124. package/dist/tree/index.cjs +38 -38
  125. package/dist/tree/index.d.cts +2 -2
  126. package/dist/tree/index.d.ts +2 -2
  127. package/dist/tree/index.mjs +3 -3
  128. package/package.json +6 -6
  129. package/src/index.ts +5 -0
  130. package/src/stories/index.ts +3 -1
  131. package/src/tools/Chat/Chat.story.tsx +1006 -0
  132. package/src/tools/Chat/README.md +528 -0
  133. package/src/tools/Chat/components/Attachments.tsx +192 -0
  134. package/src/tools/Chat/components/ChatRoot.tsx +201 -0
  135. package/src/tools/Chat/components/Composer.tsx +134 -0
  136. package/src/tools/Chat/components/EmptyState.tsx +47 -0
  137. package/src/tools/Chat/components/ErrorBanner.tsx +47 -0
  138. package/src/tools/Chat/components/JumpToLatest.tsx +30 -0
  139. package/src/tools/Chat/components/MessageActions.tsx +72 -0
  140. package/src/tools/Chat/components/MessageBubble.tsx +228 -0
  141. package/src/tools/Chat/components/MessageList.tsx +82 -0
  142. package/src/tools/Chat/components/Sources.tsx +55 -0
  143. package/src/tools/Chat/components/StreamingIndicator.tsx +29 -0
  144. package/src/tools/Chat/components/ToolCalls.tsx +172 -0
  145. package/src/tools/Chat/components/index.ts +24 -0
  146. package/src/tools/Chat/config.ts +55 -0
  147. package/src/tools/Chat/context/ChatProvider.tsx +122 -0
  148. package/src/tools/Chat/context/index.ts +9 -0
  149. package/src/tools/Chat/core/audio/audioBus.ts +172 -0
  150. package/src/tools/Chat/core/audio/index.ts +8 -0
  151. package/src/tools/Chat/core/audio/preferences.ts +68 -0
  152. package/src/tools/Chat/core/audio/types.ts +49 -0
  153. package/src/tools/Chat/core/ids.ts +16 -0
  154. package/src/tools/Chat/core/index.ts +5 -0
  155. package/src/tools/Chat/core/markdown.ts +56 -0
  156. package/src/tools/Chat/core/payload-dispatch.ts +54 -0
  157. package/src/tools/Chat/core/persona.ts +35 -0
  158. package/src/tools/Chat/core/reducer.ts +335 -0
  159. package/src/tools/Chat/core/transport/http.ts +167 -0
  160. package/src/tools/Chat/core/transport/index.ts +13 -0
  161. package/src/tools/Chat/core/transport/mock.ts +134 -0
  162. package/src/tools/Chat/core/transport/sse.ts +116 -0
  163. package/src/tools/Chat/core/transport/types.ts +24 -0
  164. package/src/tools/Chat/hooks/index.ts +26 -0
  165. package/src/tools/Chat/hooks/useChat.ts +440 -0
  166. package/src/tools/Chat/hooks/useChatAudio.ts +191 -0
  167. package/src/tools/Chat/hooks/useChatComposer.ts +227 -0
  168. package/src/tools/Chat/hooks/useChatHistory.ts +59 -0
  169. package/src/tools/Chat/hooks/useChatLayout.ts +111 -0
  170. package/src/tools/Chat/hooks/useChatLightbox.ts +34 -0
  171. package/src/tools/Chat/hooks/useChatScroll.ts +132 -0
  172. package/src/tools/Chat/index.ts +158 -0
  173. package/src/tools/Chat/lazy.tsx +14 -0
  174. package/src/tools/Chat/types.ts +237 -0
  175. package/src/tools/Chat/utils/collectImageAttachments.ts +13 -0
  176. package/src/tools/JsonForm/JsonSchemaForm.tsx +32 -1
  177. package/src/tools/Map/README.md +384 -0
  178. package/dist/DocsLayout-CTJINVBM.mjs.map +0 -1
  179. package/dist/DocsLayout-XLDB6CJ2.cjs.map +0 -1
  180. package/dist/JsonSchemaForm-OSPUUUHM.cjs +0 -13
  181. package/dist/JsonSchemaForm-TSLX2GRO.mjs +0 -4
  182. package/dist/JsonTree-F27RMYSI.cjs +0 -11
  183. package/dist/JsonTree-QTJYSHCV.mjs +0 -5
  184. package/dist/Player-M3GC3VPE.mjs +0 -4
  185. package/dist/Player-ZL2X5LGG.cjs +0 -13
  186. package/dist/TreeRoot-A3J65L6F.mjs +0 -4
  187. package/dist/TreeRoot-DSK5JILT.cjs +0 -19
  188. package/dist/chunk-4IW7GZFQ.cjs.map +0 -1
  189. package/dist/chunk-62Y65TGK.mjs.map +0 -1
  190. package/dist/chunk-EXGXUK2N.mjs.map +0 -1
  191. package/dist/chunk-TKSFZHCG.cjs +0 -1597
  192. package/dist/chunk-TKSFZHCG.cjs.map +0 -1
  193. package/dist/components-5UXYNAKR.cjs +0 -22
  194. package/dist/components-CFXOEVPN.mjs +0 -5
  195. package/dist/components-WYEZL5TE.cjs +0 -26
  196. package/dist/components-ZAGG2PBO.mjs +0 -5
@@ -0,0 +1,528 @@
1
+ # Chat
2
+
3
+ Decomposed, transport-agnostic chat. Streaming-aware, markdown-native, mobile-ready.
4
+
5
+ ## TL;DR
6
+
7
+ ```tsx
8
+ import { ChatRoot, createHttpTransport } from '@djangocfg/ui-tools';
9
+
10
+ const transport = createHttpTransport({
11
+ baseUrl: '/api/chat',
12
+ getAuthHeader: () => ({ Authorization: `Bearer ${getToken()}` }),
13
+ });
14
+
15
+ export function MyChat() {
16
+ return (
17
+ <div className="h-[600px]">
18
+ <ChatRoot transport={transport} config={{ greeting: 'How can I help?' }} />
19
+ </div>
20
+ );
21
+ }
22
+ ```
23
+
24
+ ## What you get
25
+
26
+ - **Headless first.** Pure reducer + hooks; UI is optional and replaceable.
27
+ - **Decomposed.** `MessageList`, `MessageBubble`, `Composer`, `Sources`, `ToolCalls`, `Attachments`, `EmptyState`, `ErrorBanner`, `JumpToLatest`, `StreamingIndicator` — every part exported.
28
+ - **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 panels — auto-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`.
38
+ - **A11y.** `role="log"` + polite live region, `aria-busy` on streaming bubbles, `role="alert"` errors, focus-visible actions.
39
+
40
+ ## Architecture
41
+
42
+ ```
43
+ Transport (interface) ← HTTP+SSE / Wails / WebSocket / mock
44
+
45
+ Reducer (pure state machine)
46
+
47
+ Hooks (useChat / useChatComposer / useChatScroll / useChatHistory / useChatLayout)
48
+
49
+ ChatProvider (context)
50
+
51
+ Components (MessageList, MessageBubble, Composer, Sources, ToolCalls, …)
52
+
53
+ ChatRoot (one-line preset)
54
+ ```
55
+
56
+ Module boundaries:
57
+
58
+ | Layer | May import | May NOT import |
59
+ | ---------------- | -------------------------------- | ------------------------ |
60
+ | `core/transport` | `core/types` | React, hooks, components |
61
+ | `core/reducer` | `core/types` | React, transport |
62
+ | `hooks` | `core/*`, `ui-core` hooks | components |
63
+ | `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.
69
+
70
+ ```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
+ ```
98
+
99
+ ### Slot inventory
100
+
101
+ ```
102
+ ┌───────────────────────────────┐
103
+ │ banner (sticky) │
104
+ ├───────────────────────────────┤
105
+ │ header │
106
+ ├───────────────────────────────┤
107
+ │ messages (jumpToLatest) │
108
+ ├───────────────────────────────┤
109
+ │ composerToolbarStart │
110
+ │ [textarea] composerToolbarEnd │
111
+ │ composerAttachmentTray │
112
+ ├───────────────────────────────┤
113
+ │ footer │
114
+ └───────────────────────────────┘
115
+ ```
116
+
117
+ ### ToolCalls — expand behavior
118
+
119
+ 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.
120
+
121
+ ```tsx
122
+ <ToolCalls
123
+ calls={message.toolCalls}
124
+ // Defaults:
125
+ defaultExpanded={false} // start closed
126
+ expandWhileStreaming={true} // open during run, close on completion
127
+ />
128
+ ```
129
+
130
+ Force every panel open from the start (e.g. for debug views) with `defaultExpanded`.
131
+
132
+ ### ToolCalls payload renderers
133
+
134
+ `<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"`:
135
+
136
+ ```tsx
137
+ import { LazyJsonTree } from '@djangocfg/ui-tools/json-tree';
138
+
139
+ <ToolCalls
140
+ calls={message.toolCalls}
141
+ renderInput={(input) => <LazyJsonTree data={input} mode="compact" />}
142
+ renderOutput={(output) => <LazyJsonTree data={output} mode="compact" />}
143
+ />
144
+ ```
145
+
146
+ `renderInput` / `renderOutput` / `renderStreaming` take precedence; `renderPayload(value, kind, call)` is a single fallback if you want one renderer for all three.
147
+
148
+ We don't import `LazyJsonTree` automatically — keeping the chat dep-light. The host opts in.
149
+
150
+ ## Personas (user & assistant identity)
151
+
152
+ 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`.
153
+
154
+ ```tsx
155
+ <ChatRoot
156
+ transport={transport}
157
+ config={{
158
+ user: {
159
+ name: 'Mark',
160
+ avatarUrl: '/me.jpg',
161
+ description: 'Senior engineer',
162
+ },
163
+ assistant: {
164
+ name: 'Claude',
165
+ avatarUrl: '/claude.png',
166
+ model: 'claude-opus-4-7',
167
+ },
168
+ }}
169
+ />
170
+ ```
171
+
172
+ Resolution cascade per bubble:
173
+
174
+ 1. `message.sender` — per-message override (multi-user / multi-bot chats)
175
+ 2. `config.user` / `config.assistant` — provider-level default
176
+ 3. Hardcoded `'You'` / `'AI'` fallback
177
+
178
+ Multi-user example — server returns `sender` per message:
179
+
180
+ ```ts
181
+ {
182
+ id: 'm3',
183
+ role: 'user',
184
+ content: 'Flag the auth migration section.',
185
+ sender: { name: 'Anna', avatarUrl: '/anna.jpg' },
186
+ }
187
+ ```
188
+
189
+ Initials are auto-derived from `name` (`'Mark Olofsen'` → `MO`); set `initials` explicitly to override.
190
+
191
+ Helpers exposed for hosts that build their own bubbles:
192
+
193
+ ```ts
194
+ import { resolvePersona, deriveInitials } from '@djangocfg/ui-tools';
195
+ const persona = resolvePersona(message, config.user, config.assistant);
196
+ const fallback = deriveInitials(persona, message.role);
197
+ ```
198
+
199
+ ## Attachments
200
+
201
+ Two specialised components plus a backwards-compatible facade:
202
+
203
+ | Component | Layout | Use it for |
204
+ | ------------------- | -------------- | -------------------------------------------- |
205
+ | `<AttachmentsGrid>` | `flex-wrap` | Thumbnails, file chips, no custom renderers |
206
+ | `<AttachmentsList>` | `flex-col` | Custom renderers (audio, video, doc viewer) |
207
+ | `<Attachments>` | picks for you | Backwards-compat: stacks when `renderers` is given, wraps otherwise |
208
+
209
+ `<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.
210
+
211
+ ```tsx
212
+ import { AttachmentsList } from '@djangocfg/ui-tools';
213
+
214
+ <AttachmentsList
215
+ attachments={msg.attachments}
216
+ renderers={{
217
+ audio: ({ attachment }) => <LazyAudioPlayer src={attachment.url} variant="compact" />,
218
+ }}
219
+ />
220
+ ```
221
+
222
+ ## Audio triggers
223
+
224
+ Optional sound effects on chat events. Off by default — you opt in with a `sounds` map. Six events:
225
+
226
+ | Event | Fires when |
227
+ | ------------------ | --------------------------------------------------- |
228
+ | `messageSent` | After `useChat.sendMessage()` adds the user message |
229
+ | `messageReceived` | On `STREAM_DONE` (final assistant message) |
230
+ | `streamStart` | First token / placeholder created |
231
+ | `error` | `STREAM_ERROR` or transport throw |
232
+ | `mention` | Host-fired (e.g. assistant addressed the user) |
233
+ | `notification` | Generic — host triggers manually |
234
+
235
+ ```tsx
236
+ <ChatRoot
237
+ transport={transport}
238
+ audio={{
239
+ sounds: {
240
+ messageSent: '/audio/chat/sent.mp3',
241
+ messageReceived: '/audio/chat/received.mp3',
242
+ streamStart: '/audio/chat/start.mp3',
243
+ error: '/audio/chat/error.mp3',
244
+ },
245
+ // Defaults: muteWhenHidden: true, respectReducedMotion: true, respectReducedData: true
246
+ }}
247
+ />
248
+ ```
249
+
250
+ Pitfalls handled inside (lifted from `AudioPlayer/audio`):
251
+
252
+ - **iOS/Safari autoplay block.** First `pointerdown` / `keydown` inside `<ChatProvider>` runs a transactional unlock — every cached `<audio>` is poked once with `muted=true`, then released.
253
+ - **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.
254
+ - **Cross-tab sync.** Volume / mute / per-event toggles live in a Zustand `persist` store with `localStorage` + the native `storage` event.
255
+ - **Visibility.** Auto-mute while `document.visibilityState === 'hidden'`.
256
+ - **Reduced motion / data.** Auto-suppress on `prefers-reduced-motion: reduce` and `prefers-reduced-data: reduce` (configurable).
257
+ - **SSR-safe.** `audioBus` falls back to a no-op when `window` is undefined.
258
+
259
+ For programmatic control (e.g. mute toggle in your header):
260
+
261
+ ```tsx
262
+ const ctx = useChatContext();
263
+ ctx.audio.setMuted(!ctx.audio.muted);
264
+ ctx.audio.setVolume(0.6);
265
+ ctx.audio.setEventEnabled('messageSent', false);
266
+ ctx.audio.play('mention'); // host-fired
267
+ ctx.audio.isUnlocked; // boolean
268
+ ```
269
+
270
+ Or directly: `useChatAudio(config)` — returns the same surface, no provider required.
271
+
272
+ ## Attachment renderers (registry)
273
+
274
+ `<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.
275
+
276
+ ```tsx
277
+ import { LazyPlayer as LazyAudioPlayer } from '@djangocfg/ui-tools/audio-player';
278
+
279
+ <ChatRoot
280
+ transport={transport}
281
+ attachmentRenderers={{
282
+ audio: ({ attachment }) => (
283
+ <div className="my-1 max-w-md">
284
+ <LazyAudioPlayer src={attachment.url} title={attachment.name} variant="compact" />
285
+ </div>
286
+ ),
287
+ // image / video / file / default — all optional
288
+ }}
289
+ onAttachmentOpen={(att) => openLightbox(att)}
290
+ />
291
+ ```
292
+
293
+ Renderer signature:
294
+
295
+ ```ts
296
+ type AttachmentRenderer = (args: {
297
+ attachment: ChatAttachment;
298
+ isInComposer: boolean; // true in the composer staging tray
299
+ onClick?: () => void; // forwarded from <ChatRoot onAttachmentOpen>
300
+ onRemove?: () => void; // present in the composer tray
301
+ }) => ReactNode;
302
+ ```
303
+
304
+ `<MessageBubble attachmentsRenderer>` (a wholesale slot) still wins over the registry — use it when you need to swap the entire attachments block.
305
+
306
+ ## Image lightbox helpers
307
+
308
+ ```tsx
309
+ import {
310
+ useChatLightbox,
311
+ collectImageAttachments,
312
+ } from '@djangocfg/ui-tools';
313
+ import { LazyImageViewer } from '@djangocfg/ui-tools/image-viewer';
314
+
315
+ function MyChat() {
316
+ const lightbox = useChatLightbox();
317
+ const ctx = useChatContext();
318
+ const gallery = useMemo(() => collectImageAttachments(ctx.messages), [ctx.messages]);
319
+
320
+ return (
321
+ <>
322
+ <ChatRoot
323
+ transport={transport}
324
+ onAttachmentOpen={(att) => att.type === 'image' && lightbox.open(att, gallery)}
325
+ />
326
+ <Dialog open={!!lightbox.state} onOpenChange={(o) => !o && lightbox.close()}>
327
+ <DialogContent className="max-w-5xl">
328
+ {lightbox.state && (
329
+ <LazyImageViewer
330
+ images={lightbox.state.gallery.map((a) => ({
331
+ file: { name: a.name ?? a.id, path: a.id },
332
+ src: a.url,
333
+ }))}
334
+ initialIndex={lightbox.state.index}
335
+ inDialog
336
+ />
337
+ )}
338
+ </DialogContent>
339
+ </Dialog>
340
+ </>
341
+ );
342
+ }
343
+ ```
344
+
345
+ `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`.
346
+
347
+ ## Tool-payload dispatcher
348
+
349
+ For tool-call panels, `dispatchToolPayload(matchers, fallback)` lets you pick a renderer per payload shape — without writing your own switch each time.
350
+
351
+ ```tsx
352
+ import {
353
+ dispatchToolPayload,
354
+ isLatLng,
355
+ isPlainObject,
356
+ isGeoJSONFeatureCollection,
357
+ } from '@djangocfg/ui-tools';
358
+ import { LazyJsonTree } from '@djangocfg/ui-tools/json-tree';
359
+ import { LazyMap } from '@djangocfg/ui-tools';
360
+
361
+ const renderPayload = dispatchToolPayload(
362
+ [
363
+ {
364
+ match: (v) => isGeoJSONFeatureCollection(v),
365
+ render: (v) => <LazyMap geojson={v as GeoJSON.FeatureCollection} />,
366
+ },
367
+ { match: (v) => isLatLng(v), render: (v) => <LazyMap markers={[v as { lat: number; lng: number }]} /> },
368
+ { match: (v) => isPlainObject(v), render: (v) => <LazyJsonTree data={v} mode="compact" /> },
369
+ ],
370
+ (v) => <pre className="text-[11px]">{String(v)}</pre>,
371
+ );
372
+
373
+ <ChatRoot toolCallsProps={{ defaultExpanded: true, renderPayload }} />;
374
+ ```
375
+
376
+ First match wins. Predicates `isLatLng` / `isPlainObject` / `isGeoJSONFeatureCollection` / `isStringValue` are exported as building blocks.
377
+
378
+ ## Three usage patterns
379
+
380
+ ### 1. One-line preset
381
+
382
+ ```tsx
383
+ <ChatRoot transport={transport} config={{ greeting: 'Hi!' }} />
384
+ ```
385
+
386
+ ### 2. Composition
387
+
388
+ Bring your own layout, reuse the parts.
389
+
390
+ ```tsx
391
+ <ChatProvider transport={transport}>
392
+ <MyHeader />
393
+ <MessageList renderEmpty={() => <EmptyState greeting="Custom" />} />
394
+ <MyComposer />
395
+ </ChatProvider>
396
+ ```
397
+
398
+ ### 3. Headless (just hooks)
399
+
400
+ ```tsx
401
+ const chat = useChat({ transport });
402
+ const composer = useChatComposer({ onSubmit: chat.sendMessage });
403
+ // render however you want
404
+ ```
405
+
406
+ ## Transport contract
407
+
408
+ A single I/O seam.
409
+
410
+ ```ts
411
+ interface ChatTransport {
412
+ createSession(opts?): Promise<SessionInfo>;
413
+ loadHistory(sessionId, cursor?, limit?): Promise<HistoryPage>;
414
+ stream(sessionId, content, { signal, attachments, metadata }): AsyncGenerator<ChatStreamEvent>;
415
+ send(sessionId, content, options?): Promise<ChatMessage>;
416
+ closeSession(sessionId): Promise<void>;
417
+ }
418
+ ```
419
+
420
+ Shipped implementations:
421
+
422
+ - **`createHttpTransport({ baseUrl, getAuthHeader, slug })`** — fetch + SSE, default for web.
423
+ - **`createMockTransport({ replies, latencyMs })`** — scripted in-memory replies for stories/tests.
424
+
425
+ 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.
426
+
427
+ ## Public surface
428
+
429
+ ```ts
430
+ // Types
431
+ ChatMessage, ChatRole, ChatPersona, ChatToolCall, ChatAttachment, ChatSource,
432
+ ChatConfig, ChatUserContext, ChatAssistantContext, ChatPrefs, ChatLabels,
433
+ ChatTransport, ChatStreamEvent, ChatDisplayMode,
434
+ SessionInfo, HistoryPage, StreamOptions, SendOptions
435
+
436
+ // Core (pure)
437
+ reducer, initialState, createId, createTokenBuffer,
438
+ resolvePersona, deriveInitials
439
+ type ChatState, type ChatAction
440
+
441
+ // Transport
442
+ createHttpTransport, createMockTransport, parseSSE, TransportError
443
+
444
+ // Hooks
445
+ useChat, useChatComposer, useChatScroll, useChatHistory, useChatLayout,
446
+ useChatAudio, useChatLightbox
447
+
448
+ // Audio
449
+ ChatAudioConfig, ChatAudioEvent, ChatAudioSounds, UseChatAudioReturn,
450
+ useChatAudioPrefs
451
+
452
+ // Tool-payload dispatch
453
+ dispatchToolPayload, isPlainObject, isLatLng,
454
+ isGeoJSONFeatureCollection, isStringValue,
455
+ type ToolPayloadMatcher, type ToolPayloadFallback
456
+
457
+ // Lightbox helpers
458
+ collectImageAttachments
459
+
460
+ // Context
461
+ ChatProvider, useChatContext, useChatContextOptional
462
+
463
+ // Components
464
+ ChatRoot, MessageList, MessageBubble, MessageActions, Composer,
465
+ Sources, ToolCalls, Attachments, EmptyState, ErrorBanner,
466
+ JumpToLatest, StreamingIndicator
467
+
468
+ // Lazy preset
469
+ LazyChat
470
+ ```
471
+
472
+ ## Mobile & a11y notes
473
+
474
+ - `useChatLayout` collapses `sidebar`/`floating` to `fullscreen` below `(max-width: 640px)`.
475
+ - Composer textarea is `font-size: 16px` to disable iOS focus-zoom; container uses `100dvh` and `env(safe-area-inset-bottom)`.
476
+ - `MessageList` is `role="log"` with `aria-live="polite"`; streaming bubbles set `aria-busy`.
477
+ - `ErrorBanner` is `role="alert"`.
478
+ - `JumpToLatest` announces unread count via `aria-live="polite"`.
479
+
480
+ ## Hotkeys
481
+
482
+ | Key | Behavior |
483
+ | ------------------ | --------------------------------------- |
484
+ | `Enter` | Send (configurable to `Cmd/Ctrl+Enter`) |
485
+ | `Shift+Enter` | Newline |
486
+ | `↑` (empty input) | Recall previous submission |
487
+ | `↓` | Recall next |
488
+ | `Esc` | (host-bound) cancel stream |
489
+
490
+ ## Performance
491
+
492
+ - **Token coalescing.** `createTokenBuffer` aggregates stream chunks within ~16ms before dispatching → ≤1 render per frame.
493
+ - **Plain text during stream.** `MessageBubble` skips ReactMarkdown until the message finishes, then re-renders once with full markdown.
494
+ - **Memoized bubbles.** Memo key `(id, content, isStreaming, version, toolActivity, toolCalls, sources, attachments)` — references only.
495
+ - **Virtualization is opt-in.** `@tanstack/react-virtual` stays a host-side decision via `MessageList renderItem` slot.
496
+
497
+ ## Design docs
498
+
499
+ Full implementation plan and rationale lives at [`@dev/@refactoring7-chat/`](../../../@dev/@refactoring7-chat/):
500
+
501
+ - `00-overview.md` — file layout & public API
502
+ - `01-architecture.md` — layered model, dataflow, transport contract
503
+ - `02-state-model.md` — reducer actions and invariants
504
+ - `03-hooks.md` — every hook signature
505
+ - `04-components.md` — every component prop shape
506
+ - `05-mobile-and-a11y.md` — breakpoints, iOS, ARIA, focus, RTL
507
+ - `06-integration.md` — adapter recipes
508
+ - `07-testing.md` — reducer / hook / component / a11y tests
509
+ - `08-migration.md` — step-by-step rollout
510
+
511
+ ## Storybook
512
+
513
+ `Tools/Chat` covers:
514
+
515
+ - `Default` — full ChatRoot with mock transport and suggestions
516
+ - `WithToolCalls` — scripted tool invocation + sources
517
+ - `Composition` — bring your own layout (`<ChatProvider>` + parts)
518
+ - `Bubbles` — visual matrix of message states
519
+ - `Parts` — every decomposed component on its own (incl. `ToolCalls` + `LazyJsonTree` compact)
520
+ - `WithSlots` — every named slot at once (banner / header / empty / composer toolbar / footer)
521
+ - `WithJsonTreePayload` — `<ChatRoot toolCallsProps>` with `LazyJsonTree` payload renderers
522
+ - `WithAudio` — chat-event sound triggers + unlock-state badge
523
+ - `WithAudioAttachment` — attachment registry mounts `<LazyAudioPlayer>` for `audio` items
524
+ - `WithImageLightbox` — `useChatLightbox` + host-side `<Dialog>` + `<LazyImageViewer>`
525
+ - `WithMapPayload` — `dispatchToolPayload` with `LazyJsonTree` fallback
526
+ - `WithPersonas` — config-level `user` + `assistant` identity (avatar, name)
527
+ - `MultiUser` — per-message `sender` overrides (multi-user / multi-bot)
528
+ - `Playground` — knobs (latency, streaming, suggestions)