@djangocfg/ui-tools 2.1.335 → 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.
- package/README.md +68 -2
- package/dist/ChatRoot-IIYQEWUU.mjs +5 -0
- package/dist/ChatRoot-IIYQEWUU.mjs.map +1 -0
- package/dist/ChatRoot-PNNGQCYF.css +7 -0
- package/dist/ChatRoot-PNNGQCYF.css.map +1 -0
- package/dist/ChatRoot-UUKTYM4N.cjs +14 -0
- package/dist/ChatRoot-UUKTYM4N.cjs.map +1 -0
- package/dist/{CronScheduler.client-3O3VU4CI.mjs → CronScheduler.client-DLMXCPAJ.mjs} +4 -4
- package/dist/{CronScheduler.client-3O3VU4CI.mjs.map → CronScheduler.client-DLMXCPAJ.mjs.map} +1 -1
- package/dist/{CronScheduler.client-A4GO6YBY.cjs → CronScheduler.client-WEJF4PWQ.cjs} +14 -14
- package/dist/{CronScheduler.client-A4GO6YBY.cjs.map → CronScheduler.client-WEJF4PWQ.cjs.map} +1 -1
- package/dist/{DocsLayout-XLDB6CJ2.cjs → DocsLayout-N5ZJZPBY.cjs} +200 -199
- package/dist/DocsLayout-N5ZJZPBY.cjs.map +1 -0
- package/dist/{DocsLayout-CTJINVBM.mjs → DocsLayout-VFPPNKSQ.mjs} +7 -6
- package/dist/DocsLayout-VFPPNKSQ.mjs.map +1 -0
- package/dist/JsonSchemaForm-DD7CLRIG.cjs +13 -0
- package/dist/{JsonSchemaForm-6WMS4CIY.cjs.map → JsonSchemaForm-DD7CLRIG.cjs.map} +1 -1
- package/dist/JsonSchemaForm-XKUIVELK.mjs +4 -0
- package/dist/{JsonSchemaForm-KX4JT3M4.mjs.map → JsonSchemaForm-XKUIVELK.mjs.map} +1 -1
- package/dist/JsonTree-55625VVH.mjs +5 -0
- package/dist/{JsonTree-F27RMYSI.cjs.map → JsonTree-55625VVH.mjs.map} +1 -1
- package/dist/JsonTree-DCM5QGWF.cjs +11 -0
- package/dist/{JsonTree-QTJYSHCV.mjs.map → JsonTree-DCM5QGWF.cjs.map} +1 -1
- package/dist/{LottiePlayer.client-6WVWDO75.cjs → LottiePlayer.client-2S7ISJ2S.cjs} +6 -6
- package/dist/{LottiePlayer.client-6WVWDO75.cjs.map → LottiePlayer.client-2S7ISJ2S.cjs.map} +1 -1
- package/dist/{LottiePlayer.client-B4I6WNZM.mjs → LottiePlayer.client-5LDSSJWS.mjs} +4 -4
- package/dist/{LottiePlayer.client-B4I6WNZM.mjs.map → LottiePlayer.client-5LDSSJWS.mjs.map} +1 -1
- package/dist/{MapContainer-RYG4HPH4.cjs → MapContainer-76YL2JXL.cjs} +8 -8
- package/dist/{MapContainer-RYG4HPH4.cjs.map → MapContainer-76YL2JXL.cjs.map} +1 -1
- package/dist/{MapContainer-GXQLP5WY.mjs → MapContainer-7HXBI3OH.mjs} +3 -3
- package/dist/{MapContainer-GXQLP5WY.mjs.map → MapContainer-7HXBI3OH.mjs.map} +1 -1
- package/dist/{Mermaid.client-SXRRI2YW.mjs → Mermaid.client-NL4SVR7F.mjs} +4 -4
- package/dist/{Mermaid.client-SXRRI2YW.mjs.map → Mermaid.client-NL4SVR7F.mjs.map} +1 -1
- package/dist/{Mermaid.client-W76R5AKJ.cjs → Mermaid.client-NNTI6DFX.cjs} +26 -26
- package/dist/{Mermaid.client-W76R5AKJ.cjs.map → Mermaid.client-NNTI6DFX.cjs.map} +1 -1
- package/dist/Player-BRV7XTWR.mjs +4 -0
- package/dist/{Player-M3GC3VPE.mjs.map → Player-BRV7XTWR.mjs.map} +1 -1
- package/dist/Player-PM7F7DD7.cjs +13 -0
- package/dist/{Player-ZL2X5LGG.cjs.map → Player-PM7F7DD7.cjs.map} +1 -1
- package/dist/{PrettyCode.client-RPDIE5CH.cjs → PrettyCode.client-KOHDVPPN.cjs} +13 -13
- package/dist/{PrettyCode.client-RPDIE5CH.cjs.map → PrettyCode.client-KOHDVPPN.cjs.map} +1 -1
- package/dist/{PrettyCode.client-SPMTQEG4.mjs → PrettyCode.client-ZGYGKE7G.mjs} +4 -4
- package/dist/{PrettyCode.client-SPMTQEG4.mjs.map → PrettyCode.client-ZGYGKE7G.mjs.map} +1 -1
- package/dist/TreeRoot-N72OYKXU.cjs +19 -0
- package/dist/{TreeRoot-A3J65L6F.mjs.map → TreeRoot-N72OYKXU.cjs.map} +1 -1
- package/dist/TreeRoot-VGAIXCUA.mjs +4 -0
- package/dist/{TreeRoot-DSK5JILT.cjs.map → TreeRoot-VGAIXCUA.mjs.map} +1 -1
- package/dist/chunk-2ZLKZ5VR.mjs +631 -0
- package/dist/chunk-2ZLKZ5VR.mjs.map +1 -0
- package/dist/{chunk-LFWQ36LJ.mjs → chunk-5G5YBFS6.mjs} +4 -4
- package/dist/{chunk-LFWQ36LJ.mjs.map → chunk-5G5YBFS6.mjs.map} +1 -1
- package/dist/{chunk-IHAY6FO6.cjs → chunk-5I5QNGUG.cjs} +17 -17
- package/dist/{chunk-IHAY6FO6.cjs.map → chunk-5I5QNGUG.cjs.map} +1 -1
- package/dist/{chunk-F2CMIIOH.cjs → chunk-76NNDZH6.cjs} +42 -42
- package/dist/{chunk-F2CMIIOH.cjs.map → chunk-76NNDZH6.cjs.map} +1 -1
- package/dist/chunk-B5AWZOHJ.cjs +649 -0
- package/dist/chunk-B5AWZOHJ.cjs.map +1 -0
- package/dist/{chunk-KR6B3LVY.mjs → chunk-B6IR5KSC.mjs} +3 -3
- package/dist/{chunk-KR6B3LVY.mjs.map → chunk-B6IR5KSC.mjs.map} +1 -1
- package/dist/{chunk-5LBDYFWH.mjs → chunk-C6GXVH5J.mjs} +3 -3
- package/dist/{chunk-5LBDYFWH.mjs.map → chunk-C6GXVH5J.mjs.map} +1 -1
- package/dist/{chunk-NRKD4F5X.cjs → chunk-FEN5S772.cjs} +36 -36
- package/dist/{chunk-NRKD4F5X.cjs.map → chunk-FEN5S772.cjs.map} +1 -1
- package/dist/{chunk-2SMCH62O.cjs → chunk-FP2RLYQZ.cjs} +11 -11
- package/dist/{chunk-2SMCH62O.cjs.map → chunk-FP2RLYQZ.cjs.map} +1 -1
- package/dist/{chunk-MOME6KYD.mjs → chunk-G5IEC7SR.mjs} +3 -3
- package/dist/{chunk-MOME6KYD.mjs.map → chunk-G5IEC7SR.mjs.map} +1 -1
- package/dist/{chunk-SE5IERVH.mjs → chunk-GYIO7W7M.mjs} +3 -3
- package/dist/{chunk-SE5IERVH.mjs.map → chunk-GYIO7W7M.mjs.map} +1 -1
- package/dist/{chunk-3Z3A7FHA.cjs → chunk-IEEAENLX.cjs} +48 -48
- package/dist/{chunk-3Z3A7FHA.cjs.map → chunk-IEEAENLX.cjs.map} +1 -1
- package/dist/{chunk-DFTVB66S.cjs → chunk-KNDLV4PI.cjs} +85 -85
- package/dist/{chunk-DFTVB66S.cjs.map → chunk-KNDLV4PI.cjs.map} +1 -1
- package/dist/{chunk-SSUOENAZ.mjs → chunk-KNEQRUBA.mjs} +3 -3
- package/dist/{chunk-SSUOENAZ.mjs.map → chunk-KNEQRUBA.mjs.map} +1 -1
- package/dist/chunk-KRETIZU6.mjs +2218 -0
- package/dist/chunk-KRETIZU6.mjs.map +1 -0
- package/dist/{chunk-CGILA3WO.mjs → chunk-N2XQF2OL.mjs} +5 -3
- package/dist/{chunk-CGILA3WO.mjs.map → chunk-N2XQF2OL.mjs.map} +1 -1
- package/dist/{chunk-EUADAUBQ.mjs → chunk-N4MZYNR4.mjs} +4 -4
- package/dist/{chunk-EUADAUBQ.mjs.map → chunk-N4MZYNR4.mjs.map} +1 -1
- package/dist/chunk-NRXYYO5V.cjs +2257 -0
- package/dist/chunk-NRXYYO5V.cjs.map +1 -0
- package/dist/{chunk-GGKGH5PM.mjs → chunk-OBRSGM64.mjs} +4 -4
- package/dist/{chunk-GGKGH5PM.mjs.map → chunk-OBRSGM64.mjs.map} +1 -1
- package/dist/{chunk-6JTB2X72.mjs → chunk-ODO4GMW7.mjs} +3 -3
- package/dist/{chunk-6JTB2X72.mjs.map → chunk-ODO4GMW7.mjs.map} +1 -1
- package/dist/{chunk-WGEGR3DF.cjs → chunk-OLISEQHS.cjs} +5 -2
- package/dist/{chunk-WGEGR3DF.cjs.map → chunk-OLISEQHS.cjs.map} +1 -1
- package/dist/{chunk-PZKAH7WQ.mjs → chunk-PVAX67JG.mjs} +3 -3
- package/dist/{chunk-PZKAH7WQ.mjs.map → chunk-PVAX67JG.mjs.map} +1 -1
- package/dist/{chunk-PRPG2T2E.cjs → chunk-QJ6GTUCO.cjs} +6 -6
- package/dist/{chunk-PRPG2T2E.cjs.map → chunk-QJ6GTUCO.cjs.map} +1 -1
- package/dist/chunk-QW4RBGHN.cjs +961 -0
- package/dist/chunk-QW4RBGHN.cjs.map +1 -0
- package/dist/{chunk-33AMWFBZ.cjs → chunk-SGP7V2UW.cjs} +15 -15
- package/dist/{chunk-33AMWFBZ.cjs.map → chunk-SGP7V2UW.cjs.map} +1 -1
- package/dist/{chunk-FX2QFYWF.mjs → chunk-VWQ5WOIL.mjs} +3 -3
- package/dist/{chunk-FX2QFYWF.mjs.map → chunk-VWQ5WOIL.mjs.map} +1 -1
- package/dist/{chunk-ZLQHUZDU.cjs → chunk-YDPDTOSP.cjs} +139 -139
- package/dist/{chunk-ZLQHUZDU.cjs.map → chunk-YDPDTOSP.cjs.map} +1 -1
- package/dist/{chunk-77HQWEQ6.cjs → chunk-YW5IVWHQ.cjs} +33 -33
- package/dist/{chunk-77HQWEQ6.cjs.map → chunk-YW5IVWHQ.cjs.map} +1 -1
- package/dist/{chunk-YXBOAGIM.cjs → chunk-YXZ6GU7H.cjs} +7 -7
- package/dist/{chunk-YXBOAGIM.cjs.map → chunk-YXZ6GU7H.cjs.map} +1 -1
- package/dist/{chunk-62Y65TGK.mjs → chunk-ZUFTH5IR.mjs} +8 -631
- package/dist/chunk-ZUFTH5IR.mjs.map +1 -0
- package/dist/components-EHOGXATG.cjs +22 -0
- package/dist/{components-5UXYNAKR.cjs.map → components-EHOGXATG.cjs.map} +1 -1
- package/dist/components-MQ6DR7TX.cjs +26 -0
- package/dist/{components-CFXOEVPN.mjs.map → components-MQ6DR7TX.cjs.map} +1 -1
- package/dist/components-XRX7QGLB.mjs +5 -0
- package/dist/{components-WYEZL5TE.cjs.map → components-XRX7QGLB.mjs.map} +1 -1
- package/dist/components-YATKRWLH.mjs +5 -0
- package/dist/{components-ZAGG2PBO.mjs.map → components-YATKRWLH.mjs.map} +1 -1
- package/dist/file-icon/index.cjs +6 -6
- package/dist/file-icon/index.mjs +1 -1
- package/dist/index.cjs +735 -215
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +972 -39
- package/dist/index.d.ts +972 -39
- package/dist/index.mjs +387 -31
- package/dist/index.mjs.map +1 -1
- package/dist/tree/index.cjs +38 -38
- package/dist/tree/index.d.cts +2 -2
- package/dist/tree/index.d.ts +2 -2
- package/dist/tree/index.mjs +3 -3
- package/package.json +6 -6
- package/src/index.ts +5 -0
- package/src/stories/index.ts +3 -1
- package/src/tools/Chat/Chat.story.tsx +1006 -0
- package/src/tools/Chat/README.md +528 -0
- package/src/tools/Chat/components/Attachments.tsx +192 -0
- package/src/tools/Chat/components/ChatRoot.tsx +201 -0
- package/src/tools/Chat/components/Composer.tsx +134 -0
- package/src/tools/Chat/components/EmptyState.tsx +47 -0
- package/src/tools/Chat/components/ErrorBanner.tsx +47 -0
- package/src/tools/Chat/components/JumpToLatest.tsx +30 -0
- package/src/tools/Chat/components/MessageActions.tsx +72 -0
- package/src/tools/Chat/components/MessageBubble.tsx +228 -0
- package/src/tools/Chat/components/MessageList.tsx +82 -0
- package/src/tools/Chat/components/Sources.tsx +55 -0
- package/src/tools/Chat/components/StreamingIndicator.tsx +29 -0
- package/src/tools/Chat/components/ToolCalls.tsx +172 -0
- package/src/tools/Chat/components/index.ts +24 -0
- package/src/tools/Chat/config.ts +55 -0
- package/src/tools/Chat/context/ChatProvider.tsx +122 -0
- package/src/tools/Chat/context/index.ts +9 -0
- package/src/tools/Chat/core/audio/audioBus.ts +172 -0
- package/src/tools/Chat/core/audio/index.ts +8 -0
- package/src/tools/Chat/core/audio/preferences.ts +68 -0
- package/src/tools/Chat/core/audio/types.ts +49 -0
- package/src/tools/Chat/core/ids.ts +16 -0
- package/src/tools/Chat/core/index.ts +5 -0
- package/src/tools/Chat/core/markdown.ts +56 -0
- package/src/tools/Chat/core/payload-dispatch.ts +54 -0
- package/src/tools/Chat/core/persona.ts +35 -0
- package/src/tools/Chat/core/reducer.ts +335 -0
- package/src/tools/Chat/core/transport/http.ts +167 -0
- package/src/tools/Chat/core/transport/index.ts +13 -0
- package/src/tools/Chat/core/transport/mock.ts +134 -0
- package/src/tools/Chat/core/transport/sse.ts +116 -0
- package/src/tools/Chat/core/transport/types.ts +24 -0
- package/src/tools/Chat/hooks/index.ts +26 -0
- package/src/tools/Chat/hooks/useChat.ts +440 -0
- package/src/tools/Chat/hooks/useChatAudio.ts +191 -0
- package/src/tools/Chat/hooks/useChatComposer.ts +227 -0
- package/src/tools/Chat/hooks/useChatHistory.ts +59 -0
- package/src/tools/Chat/hooks/useChatLayout.ts +111 -0
- package/src/tools/Chat/hooks/useChatLightbox.ts +34 -0
- package/src/tools/Chat/hooks/useChatScroll.ts +132 -0
- package/src/tools/Chat/index.ts +158 -0
- package/src/tools/Chat/lazy.tsx +14 -0
- package/src/tools/Chat/types.ts +237 -0
- package/src/tools/Chat/utils/collectImageAttachments.ts +13 -0
- package/src/tools/Map/README.md +384 -0
- package/dist/DocsLayout-CTJINVBM.mjs.map +0 -1
- package/dist/DocsLayout-XLDB6CJ2.cjs.map +0 -1
- package/dist/JsonSchemaForm-6WMS4CIY.cjs +0 -13
- package/dist/JsonSchemaForm-KX4JT3M4.mjs +0 -4
- package/dist/JsonTree-F27RMYSI.cjs +0 -11
- package/dist/JsonTree-QTJYSHCV.mjs +0 -5
- package/dist/Player-M3GC3VPE.mjs +0 -4
- package/dist/Player-ZL2X5LGG.cjs +0 -13
- package/dist/TreeRoot-A3J65L6F.mjs +0 -4
- package/dist/TreeRoot-DSK5JILT.cjs +0 -19
- package/dist/chunk-62Y65TGK.mjs.map +0 -1
- package/dist/chunk-TKSFZHCG.cjs +0 -1597
- package/dist/chunk-TKSFZHCG.cjs.map +0 -1
- package/dist/components-5UXYNAKR.cjs +0 -22
- package/dist/components-CFXOEVPN.mjs +0 -5
- package/dist/components-WYEZL5TE.cjs +0 -26
- 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"><pre></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"><EmptyState></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 <LazyMap></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
|
+
};
|