@djangocfg/ui-tools 2.1.381 → 2.1.382

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/README.md +132 -899
  2. package/dist/ChatRoot-6IZFM5HM.mjs +5 -0
  3. package/dist/{ChatRoot-EJC5Y2YM.cjs.map → ChatRoot-6IZFM5HM.mjs.map} +1 -1
  4. package/dist/ChatRoot-LW4XNIKP.cjs +14 -0
  5. package/dist/{ChatRoot-QOSKJPM6.mjs.map → ChatRoot-LW4XNIKP.cjs.map} +1 -1
  6. package/dist/DictationField-2ZLQWLYV.mjs +4 -0
  7. package/dist/DictationField-2ZLQWLYV.mjs.map +1 -0
  8. package/dist/DictationField-IPPJ54CU.cjs +13 -0
  9. package/dist/DictationField-IPPJ54CU.cjs.map +1 -0
  10. package/dist/{DocsLayout-2YKPXZYO.mjs → DocsLayout-2P3ONDWJ.mjs} +3 -3
  11. package/dist/{DocsLayout-2YKPXZYO.mjs.map → DocsLayout-2P3ONDWJ.mjs.map} +1 -1
  12. package/dist/{DocsLayout-Q4KS3QWW.cjs → DocsLayout-2YZNS5VK.cjs} +8 -8
  13. package/dist/{DocsLayout-Q4KS3QWW.cjs.map → DocsLayout-2YZNS5VK.cjs.map} +1 -1
  14. package/dist/chunk-4LXG3NBV.mjs +833 -0
  15. package/dist/chunk-4LXG3NBV.mjs.map +1 -0
  16. package/dist/{chunk-XACCHZH2.cjs → chunk-FIRK5CEH.cjs} +42 -4
  17. package/dist/chunk-FIRK5CEH.cjs.map +1 -0
  18. package/dist/{chunk-NWUT327A.mjs → chunk-HIK6BPL7.mjs} +38 -5
  19. package/dist/chunk-HIK6BPL7.mjs.map +1 -0
  20. package/dist/chunk-KMSBGNVC.cjs +835 -0
  21. package/dist/chunk-KMSBGNVC.cjs.map +1 -0
  22. package/dist/chunk-OZAU3QWD.cjs +2493 -0
  23. package/dist/chunk-OZAU3QWD.cjs.map +1 -0
  24. package/dist/chunk-UWVP6LCW.mjs +2447 -0
  25. package/dist/chunk-UWVP6LCW.mjs.map +1 -0
  26. package/dist/index.cjs +1532 -100
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +1148 -107
  29. package/dist/index.d.ts +1148 -107
  30. package/dist/index.mjs +1421 -51
  31. package/dist/index.mjs.map +1 -1
  32. package/package.json +16 -8
  33. package/src/audio-assets.d.ts +8 -0
  34. package/src/components/markdown/MarkdownMessage/CollapseToggle.tsx +3 -1
  35. package/src/components/markdown/MarkdownMessage/components.tsx +2 -5
  36. package/src/stories/index.ts +32 -2
  37. package/src/tools/Chat/README.md +347 -530
  38. package/src/tools/Chat/components/Attachments.tsx +6 -1
  39. package/src/tools/Chat/components/ChatRoot.tsx +30 -2
  40. package/src/tools/Chat/components/Composer.tsx +20 -3
  41. package/src/tools/Chat/components/ErrorBanner.tsx +7 -3
  42. package/src/tools/Chat/components/MessageActions.tsx +3 -1
  43. package/src/tools/Chat/components/MessageBubble.tsx +6 -5
  44. package/src/tools/Chat/components/MessageList.tsx +87 -1
  45. package/src/tools/Chat/components/ToolCalls.tsx +21 -3
  46. package/src/tools/Chat/context/ChatProvider.tsx +21 -3
  47. package/src/tools/Chat/core/audio/audioBus.ts +10 -163
  48. package/src/tools/Chat/core/audio/defaults.ts +43 -0
  49. package/src/tools/Chat/core/audio/index.ts +1 -0
  50. package/src/tools/Chat/core/audio/preferences.ts +5 -59
  51. package/src/tools/Chat/core/audio/sounds/error.mp3 +0 -0
  52. package/src/tools/Chat/core/audio/sounds/mention.mp3 +0 -0
  53. package/src/tools/Chat/core/audio/sounds/notification.mp3 +0 -0
  54. package/src/tools/Chat/core/audio/sounds/received.mp3 +0 -0
  55. package/src/tools/Chat/core/audio/sounds/sent.mp3 +0 -0
  56. package/src/tools/Chat/core/audio/sounds/start.mp3 +0 -0
  57. package/src/tools/Chat/core/audio/types.ts +28 -0
  58. package/src/tools/Chat/core/reducer.ts +33 -0
  59. package/src/tools/Chat/core/transport/index.ts +13 -0
  60. package/src/tools/Chat/core/transport/mappers/index.ts +6 -0
  61. package/src/tools/Chat/core/transport/mappers/pydantic-ai.ts +142 -0
  62. package/src/tools/Chat/core/transport/pydantic-ai-transport.ts +208 -0
  63. package/src/tools/Chat/core/transport/sse.ts +18 -5
  64. package/src/tools/Chat/hooks/index.ts +25 -0
  65. package/src/tools/Chat/hooks/useAutoFocusOnStreamEnd.ts +5 -3
  66. package/src/tools/Chat/hooks/useChat.ts +28 -0
  67. package/src/tools/Chat/hooks/useChatAudio.ts +59 -180
  68. package/src/tools/Chat/hooks/useChatDockPrefs.ts +74 -0
  69. package/src/tools/Chat/hooks/useChatReset.ts +70 -0
  70. package/src/tools/Chat/hooks/useChatUnread.ts +87 -0
  71. package/src/tools/Chat/hooks/useFocusOnEmptyClick.ts +111 -0
  72. package/src/tools/Chat/hooks/useVisitorFingerprint.ts +48 -0
  73. package/src/tools/Chat/index.ts +69 -1
  74. package/src/tools/Chat/launcher/ChatDock.tsx +263 -0
  75. package/src/tools/Chat/launcher/ChatFAB.tsx +349 -0
  76. package/src/tools/Chat/launcher/ChatGreeting.tsx +200 -0
  77. package/src/tools/Chat/launcher/ChatHeader.tsx +76 -0
  78. package/src/tools/Chat/launcher/ChatHeaderActionButton.tsx +87 -0
  79. package/src/tools/Chat/launcher/ChatHeaderAudioToggle.tsx +47 -0
  80. package/src/tools/Chat/launcher/ChatHeaderLanguageButton.tsx +179 -0
  81. package/src/tools/Chat/launcher/ChatHeaderModeToggle.tsx +57 -0
  82. package/src/tools/Chat/launcher/ChatHeaderResetButton.tsx +93 -0
  83. package/src/tools/Chat/launcher/ChatLauncher.tsx +321 -0
  84. package/src/tools/Chat/launcher/ChatUnreadPreview.tsx +197 -0
  85. package/src/tools/Chat/launcher/index.ts +46 -0
  86. package/src/tools/Chat/launcher/useChatPresence.ts +44 -0
  87. package/src/tools/Chat/stories/01-basic.story.tsx +64 -0
  88. package/src/tools/Chat/stories/02-bubbles.story.tsx +21 -0
  89. package/src/tools/Chat/stories/03-tool-calls.story.tsx +59 -0
  90. package/src/tools/Chat/stories/04-personas.story.tsx +78 -0
  91. package/src/tools/Chat/stories/05-launcher.story.tsx +321 -0
  92. package/src/tools/Chat/stories/06-header.story.tsx +147 -0
  93. package/src/tools/Chat/stories/07-audio-actions.story.tsx +112 -0
  94. package/src/tools/Chat/stories/shared/Frame.tsx +21 -0
  95. package/src/tools/Chat/stories/shared/index.ts +5 -0
  96. package/src/tools/Chat/stories/shared/messages.ts +39 -0
  97. package/src/tools/Chat/stories/shared/personas.ts +13 -0
  98. package/src/tools/Chat/stories/shared/seeds.ts +92 -0
  99. package/src/tools/Chat/stories/shared/transports.ts +36 -0
  100. package/src/tools/Chat/styles/bubbleTokens.ts +71 -0
  101. package/src/tools/Chat/styles/index.ts +16 -0
  102. package/src/tools/Chat/styles/useChatStyles.ts +101 -0
  103. package/src/tools/Chat/types/attachment.ts +25 -0
  104. package/src/tools/Chat/types/config.ts +48 -0
  105. package/src/tools/Chat/types/events.ts +35 -0
  106. package/src/tools/Chat/types/index.ts +34 -0
  107. package/src/tools/Chat/types/labels.ts +38 -0
  108. package/src/tools/Chat/types/message.ts +32 -0
  109. package/src/tools/Chat/types/persona.ts +31 -0
  110. package/src/tools/Chat/types/session.ts +43 -0
  111. package/src/tools/Chat/types/tool-call.ts +17 -0
  112. package/src/tools/Chat/types/transport.ts +28 -0
  113. package/src/tools/Chat/types.ts +5 -240
  114. package/src/tools/MarkdownEditor/MarkdownEditor.tsx +50 -14
  115. package/src/tools/MarkdownEditor/index.ts +1 -1
  116. package/src/tools/SpeechRecognition/README.md +336 -0
  117. package/src/tools/SpeechRecognition/__tests__/ids.test.ts +15 -0
  118. package/src/tools/SpeechRecognition/__tests__/language.test.ts +59 -0
  119. package/src/tools/SpeechRecognition/__tests__/reducer.test.ts +71 -0
  120. package/src/tools/SpeechRecognition/__tests__/transcript.test.ts +52 -0
  121. package/src/tools/SpeechRecognition/components/DevicePicker.tsx +49 -0
  122. package/src/tools/SpeechRecognition/components/DictationButton.tsx +93 -0
  123. package/src/tools/SpeechRecognition/components/EngineBadge.tsx +30 -0
  124. package/src/tools/SpeechRecognition/components/ErrorBanner.tsx +52 -0
  125. package/src/tools/SpeechRecognition/components/LanguagePicker.tsx +63 -0
  126. package/src/tools/SpeechRecognition/components/MicMeter.tsx +63 -0
  127. package/src/tools/SpeechRecognition/components/PushToTalkHint.tsx +51 -0
  128. package/src/tools/SpeechRecognition/components/TranscriptView.tsx +55 -0
  129. package/src/tools/SpeechRecognition/components/index.ts +16 -0
  130. package/src/tools/SpeechRecognition/context/SpeechRecognitionProvider.tsx +47 -0
  131. package/src/tools/SpeechRecognition/context/index.ts +6 -0
  132. package/src/tools/SpeechRecognition/core/audio/defaults.ts +24 -0
  133. package/src/tools/SpeechRecognition/core/engine/external.ts +222 -0
  134. package/src/tools/SpeechRecognition/core/engine/http.ts +147 -0
  135. package/src/tools/SpeechRecognition/core/engine/index.ts +52 -0
  136. package/src/tools/SpeechRecognition/core/engine/mediarecorder.ts +105 -0
  137. package/src/tools/SpeechRecognition/core/engine/websocket.ts +211 -0
  138. package/src/tools/SpeechRecognition/core/engine/webspeech.ts +188 -0
  139. package/src/tools/SpeechRecognition/core/ids.ts +11 -0
  140. package/src/tools/SpeechRecognition/core/index.ts +14 -0
  141. package/src/tools/SpeechRecognition/core/language.ts +78 -0
  142. package/src/tools/SpeechRecognition/core/languages-catalog.ts +229 -0
  143. package/src/tools/SpeechRecognition/core/logger.ts +3 -0
  144. package/src/tools/SpeechRecognition/core/reducer.ts +105 -0
  145. package/src/tools/SpeechRecognition/core/transcript.ts +36 -0
  146. package/src/tools/SpeechRecognition/hooks/index.ts +14 -0
  147. package/src/tools/SpeechRecognition/hooks/useDictation.ts +59 -0
  148. package/src/tools/SpeechRecognition/hooks/useEnginePrefs.ts +15 -0
  149. package/src/tools/SpeechRecognition/hooks/useMicDevices.ts +57 -0
  150. package/src/tools/SpeechRecognition/hooks/useMicLevel.ts +52 -0
  151. package/src/tools/SpeechRecognition/hooks/usePushToTalk.ts +85 -0
  152. package/src/tools/SpeechRecognition/hooks/useResolvedLanguage.ts +28 -0
  153. package/src/tools/SpeechRecognition/hooks/useSpeechLanguageInfo.ts +108 -0
  154. package/src/tools/SpeechRecognition/hooks/useSpeechRecognition.ts +188 -0
  155. package/src/tools/SpeechRecognition/hooks/useVoiceSupport.ts +78 -0
  156. package/src/tools/SpeechRecognition/index.ts +82 -0
  157. package/src/tools/SpeechRecognition/lazy.tsx +19 -0
  158. package/src/tools/SpeechRecognition/store/index.ts +2 -0
  159. package/src/tools/SpeechRecognition/store/prefsStore.ts +54 -0
  160. package/src/tools/SpeechRecognition/stories/01-basic.story.tsx +32 -0
  161. package/src/tools/SpeechRecognition/stories/02-dictation-field.story.tsx +32 -0
  162. package/src/tools/SpeechRecognition/stories/03-push-to-talk.story.tsx +27 -0
  163. package/src/tools/SpeechRecognition/stories/04-mic-meter.story.tsx +35 -0
  164. package/src/tools/SpeechRecognition/stories/05-custom-engine-http.story.tsx +40 -0
  165. package/src/tools/SpeechRecognition/stories/06-custom-engine-ws.story.tsx +48 -0
  166. package/src/tools/SpeechRecognition/stories/07-language-device.story.tsx +57 -0
  167. package/src/tools/SpeechRecognition/stories/08-errors-permissions.story.tsx +25 -0
  168. package/src/tools/SpeechRecognition/stories/09-chat-voice.story.tsx +90 -0
  169. package/src/tools/SpeechRecognition/stories/shared.tsx +123 -0
  170. package/src/tools/SpeechRecognition/types.ts +133 -0
  171. package/src/tools/SpeechRecognition/widgets/DictationField.tsx +105 -0
  172. package/src/tools/SpeechRecognition/widgets/VoiceComposerSlot.tsx +305 -0
  173. package/src/tools/SpeechRecognition/widgets/VoiceMessageRecorder.tsx +88 -0
  174. package/src/tools/SpeechRecognition/widgets/index.ts +6 -0
  175. package/dist/ChatRoot-EJC5Y2YM.cjs +0 -14
  176. package/dist/ChatRoot-QOSKJPM6.mjs +0 -5
  177. package/dist/chunk-NWUT327A.mjs.map +0 -1
  178. package/dist/chunk-QLMKCSR6.mjs +0 -2420
  179. package/dist/chunk-QLMKCSR6.mjs.map +0 -1
  180. package/dist/chunk-SI5RD2GD.cjs +0 -2460
  181. package/dist/chunk-SI5RD2GD.cjs.map +0 -1
  182. package/dist/chunk-XACCHZH2.cjs.map +0 -1
  183. package/src/tools/Chat/Chat.story.tsx +0 -1457
@@ -0,0 +1,112 @@
1
+ import { useMemo } from 'react';
2
+
3
+ import { defineStory } from '@djangocfg/playground';
4
+ import { DialogProvider } from '@djangocfg/ui-core/lib';
5
+
6
+ import { ChatRoot } from '../components/ChatRoot';
7
+ import { useChatAudio } from '../hooks/useChatAudio';
8
+ import { ChatDock } from '../launcher/ChatDock';
9
+ import { ChatHeaderAudioToggle } from '../launcher/ChatHeaderAudioToggle';
10
+ import { ChatHeaderResetButton } from '../launcher/ChatHeaderResetButton';
11
+ import { ChatLauncher } from '../launcher/ChatLauncher';
12
+ import { SEED_BASIC, makeBasicTransport } from './shared';
13
+
14
+ export default defineStory({
15
+ title: 'Tools/Chat/Audio & Actions',
16
+ component: ChatHeaderAudioToggle,
17
+ description:
18
+ 'Header audio toggle (auto-injected when `audio` is passed) and header reset action with `window.dialog.confirm`. Wrap stories in `<DialogProvider>` so confirm prompts render.',
19
+ });
20
+
21
+ // ── Audio toggle ──────────────────────────────────────────────────────────
22
+
23
+ function AudioLauncher() {
24
+ const transport = useMemo(() => makeBasicTransport(SEED_BASIC), []);
25
+ // A real notification sound — bundle your own assets in production.
26
+ // No `sounds` prop — uses bundled DEFAULT_CHAT_SOUNDS automatically.
27
+ const audio = useChatAudio();
28
+
29
+ return (
30
+ <ChatLauncher
31
+ audio={audio}
32
+ hotkey={{ key: '/', meta: true }}
33
+ fab={{ ariaLabel: 'Open chat', tooltip: 'Open chat' }}
34
+ dock={{ title: 'Assistant', height: 600 }}
35
+ >
36
+ <ChatRoot transport={transport} />
37
+ </ChatLauncher>
38
+ );
39
+ }
40
+
41
+ export const AudioToggleAutoInjected = () => <AudioLauncher />;
42
+
43
+ // ── Reset with confirm ────────────────────────────────────────────────────
44
+
45
+ function ResetLauncher() {
46
+ const transport = useMemo(() => makeBasicTransport(SEED_BASIC), []);
47
+
48
+ return (
49
+ <DialogProvider>
50
+ <ChatLauncher
51
+ hotkey={{ key: '/', meta: true }}
52
+ fab={{ ariaLabel: 'Open chat', tooltip: 'Open chat' }}
53
+ dock={{
54
+ title: 'CRM Assistant',
55
+ height: 600,
56
+ headerActions: (
57
+ <ChatHeaderResetButton
58
+ onReset={async () => {
59
+ await new Promise((r) => setTimeout(r, 600));
60
+ return true;
61
+ }}
62
+ onSuccess={() => alert('Context cleared.')}
63
+ />
64
+ ),
65
+ }}
66
+ >
67
+ <ChatRoot transport={transport} />
68
+ </ChatLauncher>
69
+ </DialogProvider>
70
+ );
71
+ }
72
+
73
+ /**
74
+ * `ChatHeaderResetButton` prompts `window.dialog.confirm` (destructive
75
+ * variant) before running the backend reset. `DialogProvider` from
76
+ * `@djangocfg/ui-core` installs that API.
77
+ */
78
+ export const ResetWithConfirm = () => <ResetLauncher />;
79
+
80
+ // ── Both together ────────────────────────────────────────────────────────
81
+
82
+ function FullLauncher() {
83
+ const transport = useMemo(() => makeBasicTransport(SEED_BASIC), []);
84
+ // No `sounds` prop — uses bundled DEFAULT_CHAT_SOUNDS automatically.
85
+ const audio = useChatAudio();
86
+ return (
87
+ <DialogProvider>
88
+ <ChatLauncher
89
+ audio={audio}
90
+ hotkey={{ key: '/', meta: true }}
91
+ fab={{ ariaLabel: 'Open chat', tooltip: 'Open chat' }}
92
+ dock={{
93
+ title: 'CRM Assistant',
94
+ height: 600,
95
+ headerActions: (
96
+ <ChatHeaderResetButton
97
+ onReset={async () => {
98
+ await new Promise((r) => setTimeout(r, 600));
99
+ return true;
100
+ }}
101
+ onSuccess={() => alert('Context cleared.')}
102
+ />
103
+ ),
104
+ }}
105
+ >
106
+ <ChatRoot transport={transport} />
107
+ </ChatLauncher>
108
+ </DialogProvider>
109
+ );
110
+ }
111
+
112
+ export const AudioPlusReset = () => <FullLauncher />;
@@ -0,0 +1,21 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ /** Fixed-size container so chat stories have a viewport to live in. */
4
+ export function Frame({
5
+ children,
6
+ h = 560,
7
+ w = 480,
8
+ }: {
9
+ children: ReactNode;
10
+ h?: number | string;
11
+ w?: number | string;
12
+ }) {
13
+ return (
14
+ <div
15
+ className="overflow-hidden rounded-lg border border-border bg-background shadow-sm"
16
+ style={{ height: h, width: w }}
17
+ >
18
+ {children}
19
+ </div>
20
+ );
21
+ }
@@ -0,0 +1,5 @@
1
+ export { Frame } from './Frame';
2
+ export { makeBasicTransport, makeToolCallsTransport } from './transports';
3
+ export { BUBBLE_GALLERY } from './messages';
4
+ export { USER_ANNA, ASSISTANT_AURA } from './personas';
5
+ export { SEED_BASIC, SEED_TOOL_CALLS, SEED_JSON_PAYLOAD } from './seeds';
@@ -0,0 +1,39 @@
1
+ import type { ChatMessage } from '../../types';
2
+
3
+ /** Compact transcript that exercises every bubble state for visual review. */
4
+ export const BUBBLE_GALLERY: ChatMessage[] = [
5
+ {
6
+ id: 'u-1',
7
+ role: 'user',
8
+ content: 'How do I add a new persona?',
9
+ createdAt: Date.now() - 5 * 60_000,
10
+ },
11
+ {
12
+ id: 'a-1',
13
+ role: 'assistant',
14
+ content:
15
+ 'Set `config.assistant = { name, avatarUrl }`. You can also override per message via `message.sender`.\n\nMore info: [docs](https://example.com/docs/personas).',
16
+ createdAt: Date.now() - 4 * 60_000,
17
+ },
18
+ {
19
+ id: 'u-2',
20
+ role: 'user',
21
+ content: 'And errors? What if the turn fails?',
22
+ createdAt: Date.now() - 3 * 60_000,
23
+ },
24
+ {
25
+ id: 'a-err',
26
+ role: 'assistant',
27
+ content: 'Network blew up — try again in a moment.',
28
+ isError: true,
29
+ createdAt: Date.now() - 2 * 60_000,
30
+ },
31
+ {
32
+ id: 'a-streaming',
33
+ role: 'assistant',
34
+ content: 'Live reply tokens arrive here',
35
+ isStreaming: true,
36
+ createdAt: Date.now() - 60_000,
37
+ toolActivity: 'Searching knowledge base…',
38
+ },
39
+ ];
@@ -0,0 +1,13 @@
1
+ import type { ChatAssistantContext, ChatUserContext } from '../../types';
2
+
3
+ export const USER_ANNA: ChatUserContext = {
4
+ name: 'Anna',
5
+ email: 'anna@example.com',
6
+ avatarUrl: 'https://i.pravatar.cc/64?img=5',
7
+ };
8
+
9
+ export const ASSISTANT_AURA: ChatAssistantContext = {
10
+ name: 'Aura',
11
+ avatarUrl: 'https://i.pravatar.cc/64?img=11',
12
+ model: 'gpt-4o',
13
+ };
@@ -0,0 +1,92 @@
1
+ import type { ChatMessage, ChatToolCall } from '../../types';
2
+
3
+ const now = Date.now();
4
+
5
+ /** Two-turn transcript exercising markdown + plain reply. Used in Basic stories. */
6
+ export const SEED_BASIC: ChatMessage[] = [
7
+ {
8
+ id: 'seed-u-1',
9
+ role: 'user',
10
+ content: 'Give me a markdown sample with a list and a code snippet.',
11
+ createdAt: now - 4 * 60_000,
12
+ },
13
+ {
14
+ id: 'seed-a-1',
15
+ role: 'assistant',
16
+ content:
17
+ '**Sure!** Here is a list:\n\n- alpha\n- beta\n- gamma\n\nAnd a snippet:\n\n```ts\nconst answer = 42;\n```',
18
+ createdAt: now - 3 * 60_000,
19
+ },
20
+ {
21
+ id: 'seed-u-2',
22
+ role: 'user',
23
+ content: 'How does streaming work?',
24
+ createdAt: now - 2 * 60_000,
25
+ },
26
+ {
27
+ id: 'seed-a-2',
28
+ role: 'assistant',
29
+ content:
30
+ 'Each token arrives separately over SSE. The UI sticks to the bottom while the stream is in flight and shows a streaming indicator until `message_end` fires.',
31
+ createdAt: now - 60_000,
32
+ },
33
+ ];
34
+
35
+ /** A completed tool-call so the panel is visible without sending anything. */
36
+ const SEED_TOOL_CALL: ChatToolCall = {
37
+ id: 'tc-seed-1',
38
+ name: 'search_docs',
39
+ input: { query: 'streaming protocol' },
40
+ output: { hits: 3, top: 'streaming.md' },
41
+ status: 'success',
42
+ startedAt: now - 90_000,
43
+ endedAt: now - 89_000,
44
+ };
45
+
46
+ export const SEED_TOOL_CALLS: ChatMessage[] = [
47
+ {
48
+ id: 'seed-u-tool',
49
+ role: 'user',
50
+ content: 'Find docs about streaming.',
51
+ createdAt: now - 2 * 60_000,
52
+ },
53
+ {
54
+ id: 'seed-a-tool',
55
+ role: 'assistant',
56
+ content: 'Let me check the docs for you.\n\nFound 3 relevant chunks. Here is a summary…',
57
+ createdAt: now - 60_000,
58
+ toolCalls: [SEED_TOOL_CALL],
59
+ },
60
+ ];
61
+
62
+ /** Tool-call with a richer JSON payload — drives the JsonTree dispatcher story. */
63
+ const SEED_JSON_TOOL_CALL: ChatToolCall = {
64
+ id: 'tc-seed-json',
65
+ name: 'fetch_user',
66
+ input: { id: 'usr_42', fields: ['email', 'roles', 'metadata'] },
67
+ output: {
68
+ id: 'usr_42',
69
+ email: 'mark@example.com',
70
+ roles: ['admin', 'editor'],
71
+ metadata: { plan: 'pro', seats: 5, trial: false },
72
+ },
73
+ status: 'success',
74
+ startedAt: now - 90_000,
75
+ endedAt: now - 89_000,
76
+ };
77
+
78
+ export const SEED_JSON_PAYLOAD: ChatMessage[] = [
79
+ {
80
+ id: 'seed-u-json',
81
+ role: 'user',
82
+ content: 'Fetch user 42.',
83
+ createdAt: now - 2 * 60_000,
84
+ },
85
+ {
86
+ id: 'seed-a-json',
87
+ role: 'assistant',
88
+ content: 'Open the tool panel to inspect the payload.',
89
+ createdAt: now - 60_000,
90
+ toolCalls: [SEED_JSON_TOOL_CALL],
91
+ },
92
+ ];
@@ -0,0 +1,36 @@
1
+ import { createMockTransport } from '../../core/transport/mock';
2
+ import type { ChatMessage, ChatStreamEvent } from '../../types';
3
+
4
+ /** Scripted-replies transport. Pass `seed` to pre-populate the bubble area. */
5
+ export function makeBasicTransport(seed: ChatMessage[] = []) {
6
+ return createMockTransport({
7
+ initialMessages: seed,
8
+ replies: [
9
+ 'Hello! I am a mock assistant. Ask me anything — I will reply with scripted text.',
10
+ '**Sure!** Here is a list:\n\n- alpha\n- beta\n- gamma\n\nAnd a snippet:\n\n```ts\nconst answer = 42;\n```',
11
+ 'Streaming chunked text works too. Each token arrives separately and the UI sticks to the bottom while the stream is in flight.',
12
+ ],
13
+ latencyMs: 35,
14
+ });
15
+ }
16
+
17
+ /** Transport with an inline tool_call + result on send. Seedable. */
18
+ export function makeToolCallsTransport(seed: ChatMessage[] = []) {
19
+ const toolTurn: ChatStreamEvent[] = [
20
+ { type: 'chunk', delta: 'Let me check the docs for you.\n\n' },
21
+ {
22
+ type: 'tool_call_start',
23
+ toolId: 'search-live',
24
+ name: 'search_docs',
25
+ input: { query: 'streaming protocol' },
26
+ },
27
+ {
28
+ type: 'tool_call_end',
29
+ toolId: 'search-live',
30
+ status: 'success',
31
+ output: { hits: 3, top: 'streaming.md' },
32
+ },
33
+ { type: 'chunk', delta: 'Found 3 relevant chunks. Here is a summary…' },
34
+ ];
35
+ return createMockTransport({ initialMessages: seed, replies: [toolTurn], latencyMs: 25 });
36
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Chat color tokens — single source of truth.
3
+ *
4
+ * Every role-conditional class for chat surfaces lives here.
5
+ * Components consume via `useChatBubbleStyles` / `useChatRoleStyles`,
6
+ * never via raw Tailwind literals.
7
+ *
8
+ * Why centralize:
9
+ * 1. One file to edit when the design system changes (e.g. light-theme
10
+ * contrast tweaks, palette swap).
11
+ * 2. Eliminates the "first-token-on-bg-primary-was-text-white" class
12
+ * of bugs where each call site picks its own foreground.
13
+ * 3. Lets us snapshot-test color decisions later.
14
+ */
15
+
16
+ /** Bubble surface classes (background + text), keyed by message state. */
17
+ export const BUBBLE_SURFACE = {
18
+ /** User-authored bubble — solid brand color. */
19
+ user: 'bg-primary text-primary-foreground rounded-tr-md',
20
+ /** Assistant bubble in normal state — neutral muted surface. */
21
+ assistant: 'bg-muted text-foreground rounded-tl-md',
22
+ /** Assistant bubble when the turn failed — destructive tint. */
23
+ error: 'bg-destructive/10 text-destructive rounded-tl-md border border-destructive/30',
24
+ } as const;
25
+
26
+ /**
27
+ * Anchor (link) classes for markdown content rendered inside a bubble.
28
+ *
29
+ * On `bg-primary` the link MUST stay legible against the cyan/brand fill —
30
+ * `text-primary-foreground` matches the bubble's foreground token, so
31
+ * contrast tracks the design system automatically.
32
+ *
33
+ * On the neutral assistant bubble we keep the brand-primary color so links
34
+ * still pop without competing with the body text.
35
+ */
36
+ export const ANCHOR = {
37
+ user:
38
+ 'text-primary-foreground underline decoration-primary-foreground/60 underline-offset-2 ' +
39
+ 'hover:decoration-primary-foreground transition-colors break-all',
40
+ assistant: 'text-primary underline hover:text-primary/80 transition-colors break-all',
41
+ } as const;
42
+
43
+ /** Inline secondary action (e.g. "Show more / less"). Same logic as anchors. */
44
+ export const TOGGLE = {
45
+ user: 'text-primary-foreground/80 hover:text-primary-foreground',
46
+ assistant: 'text-primary hover:text-primary/80',
47
+ } as const;
48
+
49
+ /** Destructive surface — used by ErrorBanner and the delete action. */
50
+ export const DESTRUCTIVE_SURFACE = {
51
+ /** Banner / card variant: border + tint + text. */
52
+ banner:
53
+ 'border border-destructive/40 bg-destructive/10 text-destructive',
54
+ /** Subtle hover for destructive buttons inside the banner / menu. */
55
+ hover: 'hover:bg-destructive/15',
56
+ /** Strong-hover variant (e.g. trash overlay on attachments). */
57
+ hoverStrong:
58
+ 'hover:bg-destructive hover:text-destructive-foreground',
59
+ /** Inline destructive text utility. */
60
+ text: 'text-destructive',
61
+ /** Hover style for menu items that delete data. */
62
+ menuItem:
63
+ 'text-destructive focus:text-destructive hover:bg-destructive/15 hover:text-destructive',
64
+ } as const;
65
+
66
+ /** Tool-call result text. */
67
+ export const TOOL_CALL = {
68
+ errorText: 'text-destructive',
69
+ } as const;
70
+
71
+ export type ChatBubbleSurface = keyof typeof BUBBLE_SURFACE;
@@ -0,0 +1,16 @@
1
+ export {
2
+ BUBBLE_SURFACE,
3
+ ANCHOR,
4
+ TOGGLE,
5
+ DESTRUCTIVE_SURFACE,
6
+ TOOL_CALL,
7
+ type ChatBubbleSurface,
8
+ } from './bubbleTokens';
9
+ export {
10
+ useChatBubbleStyles,
11
+ useChatRoleStyles,
12
+ useChatDestructiveStyles,
13
+ type ChatBubbleStyles,
14
+ type ChatRoleStyles,
15
+ type ChatDestructiveStyles,
16
+ } from './useChatStyles';
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Role-aware style hooks for chat surfaces.
3
+ *
4
+ * Pure utility hooks — no React state, no side effects, just memoized
5
+ * className strings derived from `bubbleTokens`. They exist so:
6
+ * - components don't import token constants directly (single facade)
7
+ * - "the user-bubble link color" can be changed in one file
8
+ * - tests can mock the hook to assert intent without parsing classNames
9
+ */
10
+
11
+ import { useMemo } from 'react';
12
+
13
+ import {
14
+ ANCHOR,
15
+ BUBBLE_SURFACE,
16
+ DESTRUCTIVE_SURFACE,
17
+ TOGGLE,
18
+ TOOL_CALL,
19
+ type ChatBubbleSurface,
20
+ } from './bubbleTokens';
21
+
22
+ export interface ChatBubbleStyles {
23
+ /** className for the bubble container (background + text + border). */
24
+ surface: string;
25
+ /** className for an inline anchor inside markdown. */
26
+ anchor: string;
27
+ /** className for a secondary inline action (e.g. show more). */
28
+ toggle: string;
29
+ }
30
+
31
+ /**
32
+ * Resolve bubble + content styles for a single message.
33
+ *
34
+ * @param role 'user' | 'assistant' | 'system' (system defaults to assistant styling)
35
+ * @param isError marks the bubble as a failed turn (overrides surface)
36
+ *
37
+ * @example
38
+ * ```tsx
39
+ * const { surface, anchor } = useChatBubbleStyles(message.role, !!message.isError);
40
+ * <div className={cn('rounded-2xl px-3.5 py-2', surface)}>…</div>
41
+ * ```
42
+ */
43
+ export function useChatBubbleStyles(
44
+ role: 'user' | 'assistant' | 'system',
45
+ isError: boolean,
46
+ ): ChatBubbleStyles {
47
+ return useMemo(() => {
48
+ const isUser = role === 'user';
49
+ const variant: ChatBubbleSurface = isUser ? 'user' : isError ? 'error' : 'assistant';
50
+ return {
51
+ surface: BUBBLE_SURFACE[variant],
52
+ anchor: isUser ? ANCHOR.user : ANCHOR.assistant,
53
+ toggle: isUser ? TOGGLE.user : TOGGLE.assistant,
54
+ };
55
+ }, [role, isError]);
56
+ }
57
+
58
+ export interface ChatRoleStyles {
59
+ anchor: string;
60
+ toggle: string;
61
+ }
62
+
63
+ /**
64
+ * Lightweight variant when only role matters (no error state, no surface).
65
+ * Use in shared markdown renderers that don't know about bubble background.
66
+ */
67
+ export function useChatRoleStyles(isUser: boolean): ChatRoleStyles {
68
+ return useMemo(
69
+ () => ({
70
+ anchor: isUser ? ANCHOR.user : ANCHOR.assistant,
71
+ toggle: isUser ? TOGGLE.user : TOGGLE.assistant,
72
+ }),
73
+ [isUser],
74
+ );
75
+ }
76
+
77
+ export interface ChatDestructiveStyles {
78
+ banner: string;
79
+ hover: string;
80
+ hoverStrong: string;
81
+ text: string;
82
+ menuItem: string;
83
+ toolErrorText: string;
84
+ }
85
+
86
+ /**
87
+ * Destructive (delete / error) class facade. Hook form keeps the API
88
+ * symmetric with the others; under the hood it returns a frozen object.
89
+ */
90
+ export function useChatDestructiveStyles(): ChatDestructiveStyles {
91
+ return DESTRUCTIVE_STYLES;
92
+ }
93
+
94
+ const DESTRUCTIVE_STYLES: ChatDestructiveStyles = {
95
+ banner: DESTRUCTIVE_SURFACE.banner,
96
+ hover: DESTRUCTIVE_SURFACE.hover,
97
+ hoverStrong: DESTRUCTIVE_SURFACE.hoverStrong,
98
+ text: DESTRUCTIVE_SURFACE.text,
99
+ menuItem: DESTRUCTIVE_SURFACE.menuItem,
100
+ toolErrorText: TOOL_CALL.errorText,
101
+ };
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Attachments + sources attached to a chat message.
3
+ */
4
+
5
+ export interface ChatAttachment {
6
+ id: string;
7
+ type: 'image' | 'file' | 'audio' | 'video';
8
+ url: string;
9
+ thumbnailUrl?: string;
10
+ name?: string;
11
+ mimeType?: string;
12
+ sizeBytes?: number;
13
+ status?: 'uploading' | 'ready' | 'error';
14
+ /** 0..1 while uploading. */
15
+ progress?: number;
16
+ /** Extracted text from images (OCR). */
17
+ ocrText?: string;
18
+ }
19
+
20
+ export interface ChatSource {
21
+ title: string;
22
+ url: string;
23
+ snippet?: string;
24
+ chunkIndex?: number;
25
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Top-level chat configuration — placeholder, suggestions, identities, prefs.
3
+ */
4
+
5
+ import type { ChatAssistantContext, ChatUserContext } from './persona';
6
+ import type { ChatLabels } from './labels';
7
+
8
+ export type ChatDisplayMode = 'closed' | 'embedded' | 'floating' | 'sidebar' | 'fullscreen';
9
+
10
+ export interface ChatPrefs {
11
+ /** Submit hotkey for the composer. */
12
+ submitOn?: 'enter' | 'cmd+enter';
13
+ /** UI density. */
14
+ density?: 'comfortable' | 'compact';
15
+ /** Locale forwarded to transport metadata. */
16
+ locale?: string;
17
+ /** Show timestamps on each bubble. */
18
+ showTimestamps?: boolean;
19
+ }
20
+
21
+ export interface ChatConfig {
22
+ /** Window title / aria-label. */
23
+ title?: string;
24
+ /** Composer placeholder. */
25
+ placeholder?: string;
26
+ /** Empty-state greeting. */
27
+ greeting?: string;
28
+ /** Empty-state description. */
29
+ description?: string;
30
+ /** Suggested prompts shown on empty conversation. */
31
+ suggestions?: Array<{ label: string; prompt: string }>;
32
+ /** Locale forwarded to the transport via stream metadata. */
33
+ locale?: string;
34
+ /** Project / chat slug forwarded to the transport. */
35
+ slug?: string;
36
+ /** UI density. Use `prefs.density` for the same effect; this stays for
37
+ * backwards compatibility. */
38
+ density?: 'comfortable' | 'compact';
39
+ /** Identity of the human author. Renders avatar / name on user bubbles
40
+ * and gets stamped on outgoing messages as `message.sender`. */
41
+ user?: ChatUserContext;
42
+ /** Identity of the assistant. Renders avatar / name on assistant bubbles. */
43
+ assistant?: ChatAssistantContext;
44
+ /** UI preferences. */
45
+ prefs?: ChatPrefs;
46
+ /** Visual labels (i18n is the host's job). */
47
+ labels?: Partial<ChatLabels>;
48
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * SSE stream events — the canonical wire shape consumed by the reducer.
3
+ *
4
+ * Backend-specific shapes (e.g. pydantic-AI's `text_delta`) get normalized
5
+ * into this union by `mapPydanticAIEvent` and friends.
6
+ */
7
+
8
+ import type { ChatSource } from './attachment';
9
+
10
+ export type ChatStreamEvent =
11
+ | { type: 'message_start'; messageId: string; sessionId: string }
12
+ | { type: 'resume_start' }
13
+ | { type: 'chunk'; delta: string }
14
+ | { type: 'tool_activity'; tool: string; status: string }
15
+ | {
16
+ type: 'tool_call_start';
17
+ toolId: string;
18
+ name: string;
19
+ input: unknown;
20
+ sourceHostname?: string;
21
+ }
22
+ | { type: 'tool_call_delta'; toolId: string; delta: string }
23
+ | {
24
+ type: 'tool_call_end';
25
+ toolId: string;
26
+ output: unknown;
27
+ status: 'success' | 'error';
28
+ }
29
+ | {
30
+ type: 'message_end';
31
+ tokensIn?: number;
32
+ tokensOut?: number;
33
+ sources?: ChatSource[];
34
+ }
35
+ | { type: 'error'; code: string; message: string };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Public types for the Chat tool — single source of truth.
3
+ *
4
+ * Shapes here are the contract between transport adapters, the reducer,
5
+ * the hooks layer and the components. Keep serializable (no Date, no Map).
6
+ *
7
+ * Organized by domain:
8
+ * persona — identity (role, ChatPersona, user/assistant contexts)
9
+ * tool-call — ChatToolCall
10
+ * attachment — ChatAttachment, ChatSource
11
+ * message — ChatMessage (composes the above)
12
+ * labels — UI strings + DEFAULT_LABELS
13
+ * config — ChatConfig + ChatPrefs + display mode
14
+ * events — ChatStreamEvent SSE union
15
+ * session — Session/history/stream/send option shapes
16
+ * transport — ChatTransport interface
17
+ */
18
+
19
+ export type { ChatRole, ChatPersona, ChatUserContext, ChatAssistantContext } from './persona';
20
+ export type { ChatToolCall } from './tool-call';
21
+ export type { ChatAttachment, ChatSource } from './attachment';
22
+ export type { ChatMessage } from './message';
23
+ export { DEFAULT_LABELS } from './labels';
24
+ export type { ChatLabels } from './labels';
25
+ export type { ChatConfig, ChatPrefs, ChatDisplayMode } from './config';
26
+ export type { ChatStreamEvent } from './events';
27
+ export type {
28
+ CreateSessionOptions,
29
+ SessionInfo,
30
+ HistoryPage,
31
+ StreamOptions,
32
+ SendOptions,
33
+ } from './session';
34
+ export type { ChatTransport } from './transport';