@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,1006 @@
1
+ import { useMemo, useState } from 'react';
2
+ import { Settings, Sparkles, Volume2, VolumeX } from 'lucide-react';
3
+ import { defineStory, useBoolean, useSelect } from '@djangocfg/playground';
4
+
5
+ import {
6
+ Dialog,
7
+ DialogContent,
8
+ DialogHeader,
9
+ DialogTitle,
10
+ } from '@djangocfg/ui-core/components';
11
+
12
+ import { LazyJsonTree } from '../JsonTree/lazy';
13
+ import { LazyPlayer as LazyAudioPlayer } from '../AudioPlayer/lazy';
14
+ import { LazyImageViewer } from '../ImageViewer/lazy';
15
+
16
+ import { ChatRoot } from './components/ChatRoot';
17
+ import { MessageBubble } from './components/MessageBubble';
18
+ import { MessageList } from './components/MessageList';
19
+ import { Composer } from './components/Composer';
20
+ import { Sources } from './components/Sources';
21
+ import { ToolCalls } from './components/ToolCalls';
22
+ import { Attachments } from './components/Attachments';
23
+ import { EmptyState } from './components/EmptyState';
24
+ import { ErrorBanner } from './components/ErrorBanner';
25
+ import { JumpToLatest } from './components/JumpToLatest';
26
+ import { StreamingIndicator } from './components/StreamingIndicator';
27
+
28
+ import { ChatProvider, useChatContext } from './context';
29
+ import { useChatComposer } from './hooks/useChatComposer';
30
+ import { useChatLightbox } from './hooks/useChatLightbox';
31
+ import { createMockTransport } from './core/transport/mock';
32
+ import { dispatchToolPayload, isLatLng, isPlainObject } from './core/payload-dispatch';
33
+ import { collectImageAttachments } from './utils/collectImageAttachments';
34
+ import type { ChatAttachment, ChatMessage, ChatStreamEvent } from './types';
35
+ import type { ChatAudioSounds } from './core/audio/types';
36
+
37
+ const CHAT_SOUNDS: ChatAudioSounds = {
38
+ messageSent: '/audio/chat/sent.mp3',
39
+ messageReceived: '/audio/chat/received.mp3',
40
+ streamStart: '/audio/chat/start.mp3',
41
+ error: '/audio/chat/error.mp3',
42
+ mention: '/audio/chat/mention.mp3',
43
+ notification: '/audio/chat/notification.mp3',
44
+ };
45
+
46
+ export default defineStory({
47
+ title: 'Tools/Chat',
48
+ component: ChatRoot,
49
+ description:
50
+ 'Decomposed, transport-agnostic chat. Markdown reuse, sticky scroll, streaming, tools, attachments.',
51
+ });
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // Frame helper — fixed-size container so the chat has somewhere to live.
55
+ // ---------------------------------------------------------------------------
56
+
57
+ function Frame({ children, h = 560, w = 480 }: { children: React.ReactNode; h?: number; w?: number }) {
58
+ return (
59
+ <div
60
+ className="overflow-hidden rounded-lg border border-border bg-background shadow-sm"
61
+ style={{ height: h, width: w }}
62
+ >
63
+ {children}
64
+ </div>
65
+ );
66
+ }
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // 1) Default — full ChatRoot with mock transport
70
+ // ---------------------------------------------------------------------------
71
+
72
+ export const Default = () => {
73
+ const transport = useMemo(
74
+ () =>
75
+ createMockTransport({
76
+ replies: [
77
+ 'Hello! I am a mock assistant. Ask me anything — I will reply with scripted text.',
78
+ '**Sure!** Here is a list:\n\n- alpha\n- beta\n- gamma\n\nAnd a snippet:\n\n```ts\nconst answer = 42;\n```',
79
+ 'Streaming chunked text works too. Each token arrives separately and the UI sticks to the bottom while the stream is in flight.',
80
+ ],
81
+ latencyMs: 35,
82
+ }),
83
+ [],
84
+ );
85
+
86
+ return (
87
+ <Frame>
88
+ <ChatRoot
89
+ transport={transport}
90
+ config={{
91
+ greeting: 'Hi there 👋',
92
+ description: 'Try sending a message — replies are scripted by the mock transport.',
93
+ placeholder: 'Ask anything…',
94
+ suggestions: [
95
+ { label: 'Show me a markdown reply', prompt: 'Give me a markdown sample' },
96
+ { label: 'Explain streaming', prompt: 'How does streaming work here?' },
97
+ ],
98
+ }}
99
+ />
100
+ </Frame>
101
+ );
102
+ };
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // 2) WithToolCalls — scripted tool invocations
106
+ // ---------------------------------------------------------------------------
107
+
108
+ export const WithToolCalls = () => {
109
+ const transport = useMemo(() => {
110
+ const sequence: ChatStreamEvent[] = [
111
+ { type: 'chunk', delta: 'Let me check the docs for you.\n\n' },
112
+ {
113
+ type: 'tool_call_start',
114
+ toolId: 't1',
115
+ name: 'search_docs',
116
+ input: { query: 'streaming tokens' },
117
+ },
118
+ { type: 'tool_call_delta', toolId: 't1', delta: 'Reading index…\n' },
119
+ { type: 'tool_call_delta', toolId: 't1', delta: 'Matched 3 chunks.\n' },
120
+ {
121
+ type: 'tool_call_end',
122
+ toolId: 't1',
123
+ output: { hits: 3, top: 'streaming.md' },
124
+ status: 'success',
125
+ },
126
+ { type: 'chunk', delta: 'Found 3 relevant chunks. Here is a summary…' },
127
+ {
128
+ type: 'message_end',
129
+ sources: [
130
+ { title: 'streaming.md', url: 'https://example.com/streaming', snippet: 'How streaming works' },
131
+ { title: 'sse.md', url: 'https://example.com/sse', snippet: 'Server-sent events' },
132
+ ],
133
+ },
134
+ ];
135
+ return createMockTransport({ replies: [sequence], latencyMs: 60 });
136
+ }, []);
137
+
138
+ return (
139
+ <Frame>
140
+ <ChatRoot
141
+ transport={transport}
142
+ config={{ greeting: 'Tool calls demo', placeholder: 'Ask "search the docs"…' }}
143
+ />
144
+ </Frame>
145
+ );
146
+ };
147
+
148
+ // ---------------------------------------------------------------------------
149
+ // 3) Composition — bring your own layout, just hooks + parts
150
+ // ---------------------------------------------------------------------------
151
+
152
+ export const Composition = () => {
153
+ const transport = useMemo(
154
+ () => createMockTransport({ replies: ['Composed shell — full control over layout.'], latencyMs: 30 }),
155
+ [],
156
+ );
157
+ return (
158
+ <Frame h={520}>
159
+ <ChatProvider transport={transport} config={{ placeholder: 'Type and press Enter…' }}>
160
+ <CustomShell />
161
+ </ChatProvider>
162
+ </Frame>
163
+ );
164
+ };
165
+
166
+ function CustomShell() {
167
+ const chat = useChatContext();
168
+ const composer = useChatComposer({
169
+ onSubmit: (c, a) => chat.sendMessage(c, a),
170
+ disabled: chat.isStreaming,
171
+ });
172
+ return (
173
+ <div className="flex h-full flex-col">
174
+ <header className="flex items-center justify-between border-b border-border bg-muted/40 px-3 py-2">
175
+ <div className="flex items-center gap-2 text-xs">
176
+ <span className="font-semibold">Custom shell</span>
177
+ {chat.isStreaming ? <StreamingIndicator label="thinking…" /> : null}
178
+ </div>
179
+ <button
180
+ type="button"
181
+ onClick={() => void chat.newSession()}
182
+ className="rounded border border-border bg-background px-2 py-0.5 text-[11px] hover:bg-accent"
183
+ >
184
+ New chat
185
+ </button>
186
+ </header>
187
+ <ErrorBanner error={chat.error} onDismiss={() => chat.clearMessages()} />
188
+ <MessageList
189
+ renderEmpty={() => <EmptyState greeting="Bring your own layout" />}
190
+ />
191
+ <Composer composer={composer} />
192
+ </div>
193
+ );
194
+ }
195
+
196
+ // ---------------------------------------------------------------------------
197
+ // 4) Bubbles — visual matrix of message states (no transport)
198
+ // ---------------------------------------------------------------------------
199
+
200
+ const sampleMessages: ChatMessage[] = [
201
+ {
202
+ id: 'u1',
203
+ role: 'user',
204
+ content: 'Quick user message',
205
+ createdAt: Date.now() - 60_000,
206
+ },
207
+ {
208
+ id: 'a1',
209
+ role: 'assistant',
210
+ content:
211
+ '**Markdown** is supported.\n\n- bullet one\n- bullet two\n\n```js\nconsole.log("hi")\n```',
212
+ createdAt: Date.now() - 50_000,
213
+ sources: [
214
+ { title: 'docs.md', url: 'https://example.com/docs' },
215
+ { title: 'guide.md', url: 'https://example.com/guide' },
216
+ ],
217
+ },
218
+ {
219
+ id: 'a2',
220
+ role: 'assistant',
221
+ content: 'Streaming…',
222
+ isStreaming: true,
223
+ toolActivity: 'Searching the knowledge base…',
224
+ createdAt: Date.now() - 30_000,
225
+ },
226
+ {
227
+ id: 'a3',
228
+ role: 'assistant',
229
+ content: '',
230
+ isError: true,
231
+ createdAt: Date.now() - 20_000,
232
+ },
233
+ {
234
+ id: 'a4',
235
+ role: 'assistant',
236
+ content: 'Here is the analysis.',
237
+ createdAt: Date.now() - 10_000,
238
+ toolCalls: [
239
+ {
240
+ id: 't1',
241
+ name: 'fetch',
242
+ input: { url: 'https://api.example.com' },
243
+ output: { status: 200 },
244
+ status: 'success',
245
+ startedAt: Date.now() - 12_000,
246
+ endedAt: Date.now() - 11_000,
247
+ },
248
+ {
249
+ id: 't2',
250
+ name: 'parse',
251
+ input: { format: 'json' },
252
+ streamingText: 'parsing…\nstep 1…\nstep 2…',
253
+ status: 'running',
254
+ startedAt: Date.now() - 9_000,
255
+ },
256
+ ],
257
+ },
258
+ ];
259
+
260
+ export const Bubbles = () => (
261
+ <Frame h={620}>
262
+ <div className="h-full overflow-y-auto py-2">
263
+ {sampleMessages.map((m) => (
264
+ <MessageBubble key={m.id} message={m} showActions={false} />
265
+ ))}
266
+ </div>
267
+ </Frame>
268
+ );
269
+
270
+ // ---------------------------------------------------------------------------
271
+ // 5) Parts — each decomposed component on its own
272
+ // ---------------------------------------------------------------------------
273
+
274
+ export const Parts = () => (
275
+ <div className="flex flex-col gap-6 p-3">
276
+ <section className="space-y-2">
277
+ <h3 className="text-xs font-semibold text-muted-foreground">StreamingIndicator</h3>
278
+ <div className="flex items-center gap-4">
279
+ <StreamingIndicator />
280
+ <StreamingIndicator label="searching…" />
281
+ <StreamingIndicator variant="pulse" label="thinking" />
282
+ </div>
283
+ </section>
284
+ <section className="space-y-2">
285
+ <h3 className="text-xs font-semibold text-muted-foreground">Sources</h3>
286
+ <Sources
287
+ sources={[
288
+ { title: 'guide.md', url: 'https://example.com/guide', snippet: 'How to use the chat' },
289
+ { title: 'api.md', url: 'https://example.com/api' },
290
+ { title: 'faq.md', url: 'https://example.com/faq' },
291
+ ]}
292
+ />
293
+ </section>
294
+ <section className="space-y-2">
295
+ <h3 className="text-xs font-semibold text-muted-foreground">
296
+ ToolCalls (default <code className="font-mono">&lt;pre&gt;</code> renderer)
297
+ </h3>
298
+ <ToolCalls
299
+ defaultExpanded
300
+ calls={[
301
+ {
302
+ id: 'a',
303
+ name: 'search',
304
+ input: { q: 'react' },
305
+ output: { hits: 7 },
306
+ status: 'success',
307
+ startedAt: 0,
308
+ endedAt: 1,
309
+ },
310
+ {
311
+ id: 'b',
312
+ name: 'fetch',
313
+ input: { url: '/api' },
314
+ streamingText: 'connecting…',
315
+ status: 'running',
316
+ startedAt: 0,
317
+ },
318
+ {
319
+ id: 'c',
320
+ name: 'parse',
321
+ input: {},
322
+ output: 'syntax error',
323
+ status: 'error',
324
+ startedAt: 0,
325
+ endedAt: 1,
326
+ },
327
+ ]}
328
+ />
329
+ </section>
330
+
331
+ <section className="space-y-2">
332
+ <h3 className="text-xs font-semibold text-muted-foreground">
333
+ ToolCalls + LazyJsonTree payloads (compact mode)
334
+ </h3>
335
+ <ToolCalls
336
+ defaultExpanded
337
+ calls={[
338
+ {
339
+ id: 'd',
340
+ name: 'fetch_user',
341
+ input: { id: 'usr_42', fields: ['email', 'roles', 'created_at'] },
342
+ output: {
343
+ id: 'usr_42',
344
+ email: 'mark@example.com',
345
+ roles: ['admin', 'editor'],
346
+ created_at: '2026-04-01T12:34:56Z',
347
+ metadata: { plan: 'pro', seats: 5, trial: false },
348
+ },
349
+ status: 'success',
350
+ startedAt: 0,
351
+ endedAt: 1,
352
+ },
353
+ ]}
354
+ renderInput={(input) => (
355
+ <LazyJsonTree data={input} mode="compact" />
356
+ )}
357
+ renderOutput={(output) => (
358
+ <LazyJsonTree data={output} mode="compact" />
359
+ )}
360
+ />
361
+ </section>
362
+ <section className="space-y-2">
363
+ <h3 className="text-xs font-semibold text-muted-foreground">Attachments</h3>
364
+ <Attachments
365
+ attachments={[
366
+ {
367
+ id: '1',
368
+ type: 'image',
369
+ url: 'https://images.unsplash.com/photo-1503023345310-bd7c1de61c7d?w=128',
370
+ name: 'photo.jpg',
371
+ },
372
+ {
373
+ id: '2',
374
+ type: 'file',
375
+ url: '#',
376
+ name: 'spec.pdf',
377
+ mimeType: 'application/pdf',
378
+ },
379
+ {
380
+ id: '3',
381
+ type: 'image',
382
+ url: 'https://images.unsplash.com/photo-1494526585095-c41746248156?w=128',
383
+ status: 'uploading',
384
+ progress: 0.55,
385
+ },
386
+ ]}
387
+ />
388
+ </section>
389
+ <section className="space-y-2">
390
+ <h3 className="text-xs font-semibold text-muted-foreground">EmptyState</h3>
391
+ <div className="rounded-lg border border-border bg-card">
392
+ <EmptyState
393
+ greeting="How can I help?"
394
+ description="Pick a starter prompt or just type."
395
+ suggestions={[
396
+ { label: 'Summarise an article', prompt: 'Summarise the latest blog post' },
397
+ { label: 'Generate code', prompt: 'Write a Pydantic model for users' },
398
+ ]}
399
+ />
400
+ </div>
401
+ </section>
402
+ <section className="space-y-2">
403
+ <h3 className="text-xs font-semibold text-muted-foreground">ErrorBanner</h3>
404
+ <ErrorBanner
405
+ error="Something went wrong while contacting the assistant."
406
+ onRetry={() => undefined}
407
+ onDismiss={() => undefined}
408
+ />
409
+ </section>
410
+ <section className="space-y-2">
411
+ <h3 className="text-xs font-semibold text-muted-foreground">JumpToLatest</h3>
412
+ <div className="flex items-center gap-3">
413
+ <JumpToLatest visible unreadCount={0} onClick={() => undefined} />
414
+ <JumpToLatest visible unreadCount={3} onClick={() => undefined} />
415
+ </div>
416
+ </section>
417
+ </div>
418
+ );
419
+
420
+ // ---------------------------------------------------------------------------
421
+ // 6) WithSlots — every named slot on ChatRoot
422
+ // ---------------------------------------------------------------------------
423
+
424
+ export const WithSlots = () => {
425
+ const transport = useMemo(
426
+ () =>
427
+ createMockTransport({
428
+ replies: [
429
+ 'I will use **the slots above and below**, plus a custom empty state. Try sending a message to see the composer toolbar buttons.',
430
+ 'Each slot is optional — omit any of them to fall back to the default.',
431
+ ],
432
+ latencyMs: 30,
433
+ }),
434
+ [],
435
+ );
436
+
437
+ return (
438
+ <Frame h={620}>
439
+ <ChatRoot
440
+ transport={transport}
441
+ config={{ placeholder: 'Slots demo…' }}
442
+ banner={
443
+ <div className="border-b border-amber-500/30 bg-amber-500/10 px-3 py-1.5 text-[11px] text-amber-700 dark:text-amber-400">
444
+ Banner slot — sticky note above the conversation
445
+ </div>
446
+ }
447
+ header={
448
+ <header className="flex items-center justify-between border-b border-border bg-muted/30 px-3 py-2">
449
+ <div className="flex items-center gap-2 text-xs">
450
+ <Sparkles aria-hidden className="size-3.5 text-primary" />
451
+ <span className="font-semibold">Custom header slot</span>
452
+ </div>
453
+ <button
454
+ type="button"
455
+ className="rounded p-1 text-muted-foreground hover:bg-accent hover:text-foreground"
456
+ aria-label="Settings"
457
+ >
458
+ <Settings aria-hidden className="size-3.5" />
459
+ </button>
460
+ </header>
461
+ }
462
+ empty={
463
+ <div className="grid place-items-center px-6 py-16 text-center">
464
+ <Sparkles aria-hidden className="mb-3 size-6 text-primary" />
465
+ <h3 className="text-sm font-semibold">Custom empty slot</h3>
466
+ <p className="mt-1 max-w-sm text-xs text-muted-foreground">
467
+ Replace the default <code className="font-mono">&lt;EmptyState&gt;</code> wholesale.
468
+ Type below to start.
469
+ </p>
470
+ </div>
471
+ }
472
+ composerToolbarStart={
473
+ <button
474
+ type="button"
475
+ aria-label="Slash commands"
476
+ className="grid h-9 w-9 shrink-0 place-items-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground"
477
+ >
478
+ /
479
+ </button>
480
+ }
481
+ composerToolbarEnd={
482
+ <button
483
+ type="button"
484
+ aria-label="Mentions"
485
+ className="grid h-9 w-9 shrink-0 place-items-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground"
486
+ >
487
+ @
488
+ </button>
489
+ }
490
+ footer={
491
+ <div className="border-t border-border bg-muted/20 px-3 py-1.5 text-center text-[10px] text-muted-foreground">
492
+ Footer slot — model: gpt-4o · responses can be inaccurate
493
+ </div>
494
+ }
495
+ />
496
+ </Frame>
497
+ );
498
+ };
499
+
500
+ // ---------------------------------------------------------------------------
501
+ // 7) WithJsonTreePayload — wire LazyJsonTree into ChatRoot.toolCallsProps
502
+ // ---------------------------------------------------------------------------
503
+
504
+ export const WithJsonTreePayload = () => {
505
+ const transport = useMemo(() => {
506
+ const sequence: ChatStreamEvent[] = [
507
+ { type: 'chunk', delta: 'Looking that up for you.\n\n' },
508
+ {
509
+ type: 'tool_call_start',
510
+ toolId: 't1',
511
+ name: 'fetch_user',
512
+ input: { id: 'usr_42', fields: ['email', 'roles', 'metadata'] },
513
+ },
514
+ {
515
+ type: 'tool_call_end',
516
+ toolId: 't1',
517
+ output: {
518
+ id: 'usr_42',
519
+ email: 'mark@example.com',
520
+ roles: ['admin', 'editor'],
521
+ metadata: {
522
+ plan: 'pro',
523
+ seats: 5,
524
+ trial: false,
525
+ features: ['streaming', 'tools', 'attachments'],
526
+ },
527
+ },
528
+ status: 'success',
529
+ },
530
+ { type: 'chunk', delta: 'Here is what I found — open the tool panel to inspect the payload.' },
531
+ { type: 'message_end' },
532
+ ];
533
+ return createMockTransport({ replies: [sequence], latencyMs: 40 });
534
+ }, []);
535
+
536
+ return (
537
+ <Frame h={620}>
538
+ <ChatRoot
539
+ transport={transport}
540
+ config={{
541
+ greeting: 'JsonTree payload demo',
542
+ placeholder: 'Ask "fetch user 42"…',
543
+ }}
544
+ toolCallsProps={{
545
+ defaultExpanded: true,
546
+ renderInput: (input) => <LazyJsonTree data={input} mode="compact" />,
547
+ renderOutput: (output) => <LazyJsonTree data={output} mode="compact" />,
548
+ }}
549
+ />
550
+ </Frame>
551
+ );
552
+ };
553
+
554
+ // ---------------------------------------------------------------------------
555
+ // 8) WithAudio — chat-event sound triggers
556
+ // ---------------------------------------------------------------------------
557
+
558
+ export const WithAudio = () => {
559
+ const [sentOn] = useBoolean('sent', { defaultValue: true, label: 'Play on sent' });
560
+ const [receivedOn] = useBoolean('received', { defaultValue: true, label: 'Play on received' });
561
+ const [errorOn] = useBoolean('error-toggle', { defaultValue: true, label: 'Play on error' });
562
+
563
+ const transport = useMemo(
564
+ () =>
565
+ createMockTransport({
566
+ replies: [
567
+ 'Pings! Listen for the sent / received cues. Try sending a couple of messages.',
568
+ 'Each event has its own sound — toggle them in the knobs above.',
569
+ 'The `error` event uses a longer drumroll so it stands out.',
570
+ ],
571
+ latencyMs: 35,
572
+ }),
573
+ [],
574
+ );
575
+
576
+ const sounds: ChatAudioSounds = useMemo(
577
+ () => ({
578
+ messageSent: sentOn ? CHAT_SOUNDS.messageSent : false,
579
+ messageReceived: receivedOn ? CHAT_SOUNDS.messageReceived : false,
580
+ streamStart: CHAT_SOUNDS.streamStart,
581
+ error: errorOn ? CHAT_SOUNDS.error : false,
582
+ notification: CHAT_SOUNDS.notification,
583
+ }),
584
+ [sentOn, receivedOn, errorOn],
585
+ );
586
+
587
+ return (
588
+ <Frame>
589
+ <ChatRoot
590
+ transport={transport}
591
+ config={{
592
+ greeting: 'Audio triggers',
593
+ description:
594
+ 'First click anywhere inside the chat unlocks audio (Safari/iOS quirk). After that, sounds play on send / receive.',
595
+ placeholder: 'Send a message to hear the cue…',
596
+ }}
597
+ audio={{
598
+ sounds,
599
+ // Default off-when-hidden + reduced-motion / reduced-data respect.
600
+ }}
601
+ header={
602
+ <header className="flex items-center justify-between border-b border-border bg-muted/30 px-3 py-1.5 text-[11px]">
603
+ <AudioStatusBadge />
604
+ <span className="text-muted-foreground">/audio/chat/*.mp3</span>
605
+ </header>
606
+ }
607
+ />
608
+ </Frame>
609
+ );
610
+ };
611
+
612
+ function AudioStatusBadge() {
613
+ const ctx = useChatContext();
614
+ return (
615
+ <button
616
+ type="button"
617
+ onClick={() => ctx.audio.setMuted(!ctx.audio.muted)}
618
+ className="inline-flex items-center gap-1.5 rounded px-1.5 py-0.5 hover:bg-accent"
619
+ >
620
+ {ctx.audio.muted ? (
621
+ <VolumeX aria-hidden className="size-3.5 text-muted-foreground" />
622
+ ) : (
623
+ <Volume2 aria-hidden className="size-3.5 text-primary" />
624
+ )}
625
+ <span className="font-mono">
626
+ {ctx.audio.isUnlocked ? 'unlocked' : 'locked'} · {ctx.audio.muted ? 'muted' : `${Math.round(ctx.audio.volume * 100)}%`}
627
+ </span>
628
+ </button>
629
+ );
630
+ }
631
+
632
+ // ---------------------------------------------------------------------------
633
+ // 9) WithAudioAttachment — registry mounts <LazyAudioPlayer> for audio msgs
634
+ // ---------------------------------------------------------------------------
635
+
636
+ const audioAttachmentMessages: ChatMessage[] = [
637
+ {
638
+ id: 'u1',
639
+ role: 'user',
640
+ content: "Here's the voice memo I recorded.",
641
+ createdAt: Date.now() - 60_000,
642
+ attachments: [
643
+ {
644
+ id: 'voice-1',
645
+ type: 'audio',
646
+ url: '/audio/voice.mp3',
647
+ name: 'voice memo.mp3',
648
+ mimeType: 'audio/mpeg',
649
+ },
650
+ ],
651
+ },
652
+ {
653
+ id: 'a1',
654
+ role: 'assistant',
655
+ content: 'Got it — playing back inline. Click play to listen.',
656
+ createdAt: Date.now() - 50_000,
657
+ attachments: [
658
+ {
659
+ id: 'reply-1',
660
+ type: 'audio',
661
+ url: '/audio/short.mp3',
662
+ name: 'reply.mp3',
663
+ mimeType: 'audio/mpeg',
664
+ },
665
+ ],
666
+ },
667
+ ];
668
+
669
+ export const WithAudioAttachment = () => (
670
+ <Frame h={620}>
671
+ <div className="h-full overflow-y-auto py-2">
672
+ {audioAttachmentMessages.map((m) => (
673
+ <MessageBubble
674
+ key={m.id}
675
+ message={m}
676
+ showActions={false}
677
+ attachmentRenderers={{
678
+ audio: ({ attachment }) => (
679
+ <div className="my-1 w-full max-w-md">
680
+ <LazyAudioPlayer
681
+ src={attachment.url}
682
+ title={attachment.name ?? 'audio'}
683
+ variant="compact"
684
+ />
685
+ </div>
686
+ ),
687
+ }}
688
+ />
689
+ ))}
690
+ </div>
691
+ </Frame>
692
+ );
693
+
694
+ // ---------------------------------------------------------------------------
695
+ // 10) WithImageLightbox — useChatLightbox + LazyImageViewer in a Dialog
696
+ // ---------------------------------------------------------------------------
697
+
698
+ const imageMessages: ChatMessage[] = [
699
+ {
700
+ id: 'u1',
701
+ role: 'user',
702
+ content: 'Found these in the assets folder.',
703
+ createdAt: Date.now() - 60_000,
704
+ attachments: [
705
+ {
706
+ id: 'img-1',
707
+ type: 'image',
708
+ url: 'https://images.unsplash.com/photo-1503023345310-bd7c1de61c7d?w=800',
709
+ thumbnailUrl: 'https://images.unsplash.com/photo-1503023345310-bd7c1de61c7d?w=128',
710
+ name: 'cliffside.jpg',
711
+ },
712
+ {
713
+ id: 'img-2',
714
+ type: 'image',
715
+ url: 'https://images.unsplash.com/photo-1494526585095-c41746248156?w=800',
716
+ thumbnailUrl: 'https://images.unsplash.com/photo-1494526585095-c41746248156?w=128',
717
+ name: 'cabin.jpg',
718
+ },
719
+ ],
720
+ },
721
+ {
722
+ id: 'a1',
723
+ role: 'assistant',
724
+ content: 'Here is one more from the same set.',
725
+ createdAt: Date.now() - 50_000,
726
+ attachments: [
727
+ {
728
+ id: 'img-3',
729
+ type: 'image',
730
+ url: 'https://images.unsplash.com/photo-1518791841217-8f162f1e1131?w=800',
731
+ thumbnailUrl: 'https://images.unsplash.com/photo-1518791841217-8f162f1e1131?w=128',
732
+ name: 'cat.jpg',
733
+ },
734
+ ],
735
+ },
736
+ ];
737
+
738
+ export const WithImageLightbox = () => {
739
+ const lightbox = useChatLightbox();
740
+ const gallery = useMemo(() => collectImageAttachments(imageMessages), []);
741
+
742
+ return (
743
+ <Frame h={560}>
744
+ <div className="h-full overflow-y-auto py-2">
745
+ {imageMessages.map((m) => (
746
+ <MessageBubble
747
+ key={m.id}
748
+ message={m}
749
+ showActions={false}
750
+ onAttachmentOpen={(att) => {
751
+ if (att.type === 'image') lightbox.open(att, gallery);
752
+ }}
753
+ />
754
+ ))}
755
+ </div>
756
+ <Dialog open={lightbox.state !== null} onOpenChange={(open) => !open && lightbox.close()}>
757
+ <DialogContent className="max-w-5xl">
758
+ <DialogHeader>
759
+ <DialogTitle>Image preview</DialogTitle>
760
+ </DialogHeader>
761
+ {lightbox.state ? (
762
+ <div className="h-[60vh]">
763
+ <LazyImageViewer
764
+ images={lightbox.state.gallery.map((a: ChatAttachment) => ({
765
+ file: { name: a.name ?? a.id, path: a.id },
766
+ src: a.url,
767
+ }))}
768
+ initialIndex={lightbox.state.index}
769
+ inDialog
770
+ />
771
+ </div>
772
+ ) : null}
773
+ </DialogContent>
774
+ </Dialog>
775
+ </Frame>
776
+ );
777
+ };
778
+
779
+ // ---------------------------------------------------------------------------
780
+ // 11) WithMapPayload — dispatchToolPayload + JsonTree fallback
781
+ // ---------------------------------------------------------------------------
782
+
783
+ export const WithMapPayload = () => {
784
+ const transport = useMemo(() => {
785
+ const sequence: ChatStreamEvent[] = [
786
+ { type: 'chunk', delta: 'Looking up the location.\n\n' },
787
+ {
788
+ type: 'tool_call_start',
789
+ toolId: 't1',
790
+ name: 'geocode',
791
+ input: { query: 'Bali, Indonesia' },
792
+ },
793
+ {
794
+ type: 'tool_call_end',
795
+ toolId: 't1',
796
+ output: { lat: -8.4095, lng: 115.1889, label: 'Bali, Indonesia' },
797
+ status: 'success',
798
+ },
799
+ { type: 'chunk', delta: 'Here are the coordinates — opening the panel renders a tree by default. Map embed wiring is host-side via `dispatchToolPayload`.' },
800
+ { type: 'message_end' },
801
+ ];
802
+ return createMockTransport({ replies: [sequence], latencyMs: 30 });
803
+ }, []);
804
+
805
+ const renderPayload = useMemo(
806
+ () =>
807
+ dispatchToolPayload(
808
+ [
809
+ {
810
+ // Single point → host would mount a Map preview here.
811
+ match: (v) => isLatLng(v),
812
+ render: (v) => {
813
+ const point = v as { lat: number; lng: number; label?: string };
814
+ return (
815
+ <div className="space-y-1">
816
+ <div className="rounded-md border border-border bg-emerald-500/5 px-2 py-1.5 text-[11px]">
817
+ <strong className="font-mono">{point.lat.toFixed(4)}, {point.lng.toFixed(4)}</strong>
818
+ {point.label ? <span className="ml-2 text-muted-foreground">— {point.label}</span> : null}
819
+ <span className="ml-2 text-emerald-500">→ would mount &lt;LazyMap&gt;</span>
820
+ </div>
821
+ <LazyJsonTree data={v} mode="inline" />
822
+ </div>
823
+ );
824
+ },
825
+ },
826
+ {
827
+ // Other objects → JsonTree.
828
+ match: (v) => isPlainObject(v),
829
+ render: (v) => <LazyJsonTree data={v} mode="compact" />,
830
+ },
831
+ ],
832
+ (v) => <pre className="text-[11px]">{String(v)}</pre>,
833
+ ),
834
+ [],
835
+ );
836
+
837
+ return (
838
+ <Frame>
839
+ <ChatRoot
840
+ transport={transport}
841
+ config={{
842
+ greeting: 'Tool payload dispatcher',
843
+ placeholder: 'Ask "where is Bali"…',
844
+ }}
845
+ toolCallsProps={{ defaultExpanded: true, renderPayload }}
846
+ />
847
+ </Frame>
848
+ );
849
+ };
850
+
851
+ // ---------------------------------------------------------------------------
852
+ // 12) WithPersonas — user / assistant identity (avatar, name, multi-user)
853
+ // ---------------------------------------------------------------------------
854
+
855
+ const MARK_PERSONA = {
856
+ name: 'Mark',
857
+ avatarUrl: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=128',
858
+ description: 'Senior engineer',
859
+ };
860
+ const CLAUDE_PERSONA = {
861
+ name: 'Claude',
862
+ avatarUrl: 'https://avatars.githubusercontent.com/u/76263028?s=128&v=4',
863
+ description: 'Anthropic · Opus 4.7',
864
+ model: 'claude-opus-4-7',
865
+ };
866
+
867
+ export const WithPersonas = () => {
868
+ const transport = useMemo(
869
+ () =>
870
+ createMockTransport({
871
+ replies: [
872
+ 'Hi Mark — happy to help. What are we working on?',
873
+ 'Got it. Streaming a response now…',
874
+ ],
875
+ latencyMs: 30,
876
+ }),
877
+ [],
878
+ );
879
+ return (
880
+ <Frame>
881
+ <ChatRoot
882
+ transport={transport}
883
+ config={{
884
+ greeting: 'Hi Mark 👋',
885
+ placeholder: 'Ask Claude anything…',
886
+ user: MARK_PERSONA,
887
+ assistant: CLAUDE_PERSONA,
888
+ }}
889
+ />
890
+ </Frame>
891
+ );
892
+ };
893
+
894
+ // Multi-user / multi-bot — per-message `sender` overrides config defaults.
895
+ const MULTI_USER_MESSAGES: ChatMessage[] = [
896
+ {
897
+ id: 'm1',
898
+ role: 'user',
899
+ content: '@claude can you summarise the spec?',
900
+ createdAt: Date.now() - 90_000,
901
+ sender: { name: 'Mark', avatarUrl: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=128' },
902
+ },
903
+ {
904
+ id: 'm2',
905
+ role: 'assistant',
906
+ content: "Sure — pulling the latest version from the docs index now.",
907
+ createdAt: Date.now() - 80_000,
908
+ sender: { ...CLAUDE_PERSONA, initials: 'CL' },
909
+ },
910
+ {
911
+ id: 'm3',
912
+ role: 'user',
913
+ content: 'Make sure to flag the auth migration section.',
914
+ createdAt: Date.now() - 60_000,
915
+ sender: { name: 'Anna', avatarUrl: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=128' },
916
+ },
917
+ {
918
+ id: 'm4',
919
+ role: 'assistant',
920
+ content: '👍 Flagged. Section 4.2 — needs DBA review before Friday.',
921
+ createdAt: Date.now() - 30_000,
922
+ sender: { ...CLAUDE_PERSONA, initials: 'CL' },
923
+ },
924
+ {
925
+ id: 'm5',
926
+ role: 'user',
927
+ content: 'Thanks both — I will sync with Anna tomorrow.',
928
+ createdAt: Date.now() - 10_000,
929
+ sender: { name: 'Lukas', initials: 'LK' },
930
+ },
931
+ ];
932
+
933
+ export const MultiUser = () => (
934
+ <Frame h={620}>
935
+ <div className="h-full overflow-y-auto py-2">
936
+ {MULTI_USER_MESSAGES.map((m) => (
937
+ <MessageBubble key={m.id} message={m} showActions={false} showTimestamp />
938
+ ))}
939
+ </div>
940
+ </Frame>
941
+ );
942
+
943
+ // ---------------------------------------------------------------------------
944
+ // 13) Playground — knobs
945
+ // ---------------------------------------------------------------------------
946
+
947
+ export const Playground = () => {
948
+ const [latencyStr] = useSelect('latency', {
949
+ options: ['10', '35', '80', '200'] as const,
950
+ defaultValue: '35',
951
+ label: 'Latency (ms/chunk)',
952
+ });
953
+ const latency = Number(latencyStr);
954
+ const [streaming] = useBoolean('streaming', {
955
+ defaultValue: true,
956
+ label: 'Streaming',
957
+ });
958
+ const [showSuggestions] = useBoolean('suggestions', {
959
+ defaultValue: true,
960
+ label: 'Show suggestions',
961
+ });
962
+ const [seed, setSeed] = useState(0);
963
+
964
+ const transport = useMemo(
965
+ () =>
966
+ createMockTransport({
967
+ latencyMs: latency,
968
+ replies: [
969
+ 'Token by token, this reply streams in. Try interrupting with the Stop button mid-stream.',
970
+ 'Here is a longer reply with **markdown** including a code block:\n\n```ts\nfunction add(a: number, b: number) {\n return a + b;\n}\n```\n\nAnd a list:\n- one\n- two\n- three',
971
+ 'Cancel mid-stream to keep partial text with a [cancelled] marker.',
972
+ ],
973
+ }),
974
+ // eslint-disable-next-line react-hooks/exhaustive-deps
975
+ [latency, seed],
976
+ );
977
+
978
+ return (
979
+ <div className="flex flex-col gap-2">
980
+ <button
981
+ type="button"
982
+ className="self-start rounded border border-border bg-background px-2 py-0.5 text-xs hover:bg-accent"
983
+ onClick={() => setSeed((n) => n + 1)}
984
+ >
985
+ Reset session
986
+ </button>
987
+ <Frame h={580}>
988
+ <ChatRoot
989
+ transport={transport}
990
+ streaming={streaming}
991
+ config={{
992
+ greeting: 'Playground',
993
+ description: 'Tweak knobs above to change behavior.',
994
+ placeholder: 'Type a message…',
995
+ suggestions: showSuggestions
996
+ ? [
997
+ { label: 'Markdown sample', prompt: 'Show me markdown' },
998
+ { label: 'Long reply', prompt: 'Give me a long answer' },
999
+ ]
1000
+ : undefined,
1001
+ }}
1002
+ />
1003
+ </Frame>
1004
+ </div>
1005
+ );
1006
+ };