@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,59 @@
1
+ import { useMemo } from 'react';
2
+
3
+ import { defineStory } from '@djangocfg/playground';
4
+
5
+ import { LazyJsonTree } from '../../JsonTree/lazy';
6
+ import { ChatRoot } from '../components/ChatRoot';
7
+ import { ToolCalls } from '../components/ToolCalls';
8
+ import { createMockTransport } from '../core/transport/mock';
9
+ import {
10
+ Frame,
11
+ SEED_JSON_PAYLOAD,
12
+ SEED_TOOL_CALLS,
13
+ makeToolCallsTransport,
14
+ } from './shared';
15
+
16
+ export default defineStory({
17
+ title: 'Tools/Chat/ToolCalls',
18
+ component: ToolCalls,
19
+ description:
20
+ 'Streaming tool-call panels (auto-open while running, auto-close on success) and the `renderInput`/`renderOutput` payload dispatcher.',
21
+ });
22
+
23
+ export const Default = () => {
24
+ const transport = useMemo(() => makeToolCallsTransport(SEED_TOOL_CALLS), []);
25
+ return (
26
+ <Frame h={620}>
27
+ <ChatRoot
28
+ transport={transport}
29
+ config={{ greeting: 'Tool-call panel — try sending a message to trigger another search.' }}
30
+ />
31
+ </Frame>
32
+ );
33
+ };
34
+
35
+ export const WithJsonTreePayload = () => {
36
+ const transport = useMemo(
37
+ () =>
38
+ createMockTransport({
39
+ initialMessages: SEED_JSON_PAYLOAD,
40
+ replies: ['Open the tool panel above to inspect the payload.'],
41
+ latencyMs: 40,
42
+ }),
43
+ [],
44
+ );
45
+
46
+ return (
47
+ <Frame h={620}>
48
+ <ChatRoot
49
+ transport={transport}
50
+ config={{ greeting: 'JsonTree payload demo' }}
51
+ toolCallsProps={{
52
+ defaultExpanded: true,
53
+ renderInput: (input) => <LazyJsonTree data={input} mode="compact" />,
54
+ renderOutput: (output) => <LazyJsonTree data={output} mode="compact" />,
55
+ }}
56
+ />
57
+ </Frame>
58
+ );
59
+ };
@@ -0,0 +1,78 @@
1
+ import { useMemo } from 'react';
2
+
3
+ import { defineStory } from '@djangocfg/playground';
4
+
5
+ import { MessageBubble } from '../components/MessageBubble';
6
+ import { ChatRoot } from '../components/ChatRoot';
7
+ import type { ChatMessage } from '../types';
8
+ import {
9
+ ASSISTANT_AURA,
10
+ Frame,
11
+ SEED_BASIC,
12
+ USER_ANNA,
13
+ makeBasicTransport,
14
+ } from './shared';
15
+
16
+ export default defineStory({
17
+ title: 'Tools/Chat/Personas',
18
+ component: ChatRoot,
19
+ description:
20
+ 'Default user/assistant identities via `config.user` / `config.assistant`, and per-message overrides via `message.sender` (multi-user / multi-bot).',
21
+ });
22
+
23
+ export const Default = () => {
24
+ const transport = useMemo(() => makeBasicTransport(SEED_BASIC), []);
25
+ return (
26
+ <Frame>
27
+ <ChatRoot
28
+ transport={transport}
29
+ config={{
30
+ greeting: `Hi ${USER_ANNA.name} 👋`,
31
+ user: USER_ANNA,
32
+ assistant: ASSISTANT_AURA,
33
+ }}
34
+ />
35
+ </Frame>
36
+ );
37
+ };
38
+
39
+ const MULTI_USER: ChatMessage[] = [
40
+ {
41
+ id: 'm1',
42
+ role: 'user',
43
+ content: '@aura can you summarise the spec?',
44
+ createdAt: Date.now() - 90_000,
45
+ sender: USER_ANNA,
46
+ },
47
+ {
48
+ id: 'm2',
49
+ role: 'assistant',
50
+ content: 'Pulling the latest version from the docs index now.',
51
+ createdAt: Date.now() - 80_000,
52
+ sender: ASSISTANT_AURA,
53
+ },
54
+ {
55
+ id: 'm3',
56
+ role: 'user',
57
+ content: 'Make sure to flag the auth migration section.',
58
+ createdAt: Date.now() - 60_000,
59
+ sender: { name: 'Mark', initials: 'MK' },
60
+ },
61
+ {
62
+ id: 'm4',
63
+ role: 'assistant',
64
+ content: '👍 Flagged section 4.2 — needs DBA review.',
65
+ createdAt: Date.now() - 30_000,
66
+ sender: ASSISTANT_AURA,
67
+ },
68
+ ];
69
+
70
+ export const MultiUser = () => (
71
+ <Frame h={520}>
72
+ <div className="h-full overflow-y-auto py-2">
73
+ {MULTI_USER.map((m) => (
74
+ <MessageBubble key={m.id} message={m} showActions={false} showTimestamp />
75
+ ))}
76
+ </div>
77
+ </Frame>
78
+ );
@@ -0,0 +1,321 @@
1
+ import { useEffect, useMemo, useState } from 'react';
2
+ import { Bot, MessageCircle, Sparkles, Zap } from 'lucide-react';
3
+
4
+ import { defineStory, useBoolean, useNumber, useSelect } from '@djangocfg/playground';
5
+ import { Avatar, AvatarFallback, AvatarImage } from '@djangocfg/ui-core/components';
6
+
7
+ import { ChatRoot } from '../components/ChatRoot';
8
+ import { Composer } from '../components/Composer';
9
+ import { EmptyState } from '../components/EmptyState';
10
+ import { MessageList } from '../components/MessageList';
11
+ import { ChatProvider, useChatContext } from '../context';
12
+ import { useChatComposer } from '../hooks/useChatComposer';
13
+ import { useChatUnread } from '../hooks/useChatUnread';
14
+ import {
15
+ ChatFAB,
16
+ type ChatFABPosition,
17
+ type ChatFABSize,
18
+ type ChatFABVariant,
19
+ } from '../launcher/ChatFAB';
20
+ import { ChatHeaderLanguageButton } from '../launcher/ChatHeaderLanguageButton';
21
+ import { ChatLauncher } from '../launcher/ChatLauncher';
22
+ import { VoiceComposerSlot } from '../../SpeechRecognition/widgets/VoiceComposerSlot';
23
+ import type { ChatMessage } from '../types';
24
+ import { SEED_BASIC, makeBasicTransport } from './shared';
25
+
26
+ export default defineStory({
27
+ title: 'Tools/Chat/Launcher',
28
+ component: ChatLauncher,
29
+ description:
30
+ 'Floating chat launcher (FAB + Dock + Greeting + hotkey). Production-realistic stories pin the FAB to the viewport corner like any chat widget; isolated previews use `inline` mode for the FAB primitive.',
31
+ });
32
+
33
+ function DemoChat() {
34
+ const transport = useMemo(() => makeBasicTransport(SEED_BASIC), []);
35
+ // Drop the voice slot into the composer toolbar. No props needed —
36
+ // it pulls value/setValue from the registered ComposerHandle and
37
+ // auto-hides on browsers that can't dictate.
38
+ return (
39
+ <ChatRoot
40
+ transport={transport}
41
+ config={{ greeting: 'How can I help?' }}
42
+ composerToolbarEnd={<VoiceComposerSlot />}
43
+ />
44
+ );
45
+ }
46
+
47
+ export const Default = () => (
48
+ <ChatLauncher
49
+ hotkey={{ key: '/', meta: true }}
50
+ fab={{ ariaLabel: 'Open chat', tooltip: 'Open chat (⌘/)' }}
51
+ dock={{
52
+ title: 'Assistant',
53
+ height: 600,
54
+ // Kebab settings menu in the dock header — language picker
55
+ // persists into `useSpeechPrefs` so dictation honours the
56
+ // choice across reloads. Composable: pass children to override.
57
+ headerActions: <ChatHeaderLanguageButton />,
58
+ }}
59
+ >
60
+ <DemoChat />
61
+ </ChatLauncher>
62
+ );
63
+
64
+ export const Playground = () => {
65
+ const [variant] = useSelect('variant', {
66
+ options: ['simple', 'animated', 'glass'] as const,
67
+ defaultValue: 'simple',
68
+ label: 'FAB variant',
69
+ });
70
+ const [size] = useSelect('size', {
71
+ options: ['sm', 'md', 'lg'] as const,
72
+ defaultValue: 'md',
73
+ label: 'FAB size',
74
+ });
75
+ const [position] = useSelect('position', {
76
+ options: ['bottom-right', 'bottom-left', 'top-right', 'top-left'] as const,
77
+ defaultValue: 'bottom-right',
78
+ label: 'Position',
79
+ });
80
+ const [pulse] = useBoolean('pulse', { defaultValue: false });
81
+ const [withBadge] = useBoolean('badge', { defaultValue: false });
82
+ const [badgeCount] = useNumber('badge count', { defaultValue: 3, min: 1, max: 99 });
83
+ const [withTooltip] = useBoolean('tooltip', { defaultValue: true });
84
+ const [withGreeting] = useBoolean('greeting', { defaultValue: false });
85
+
86
+ return (
87
+ <ChatLauncher
88
+ fab={{
89
+ variant: variant as ChatFABVariant,
90
+ size: size as ChatFABSize,
91
+ position: position as ChatFABPosition,
92
+ pulse,
93
+ badge: withBadge ? badgeCount : undefined,
94
+ tooltip: withTooltip ? 'Open chat (⌘/)' : undefined,
95
+ icon: <MessageCircle className="h-6 w-6" />,
96
+ ariaLabel: 'Open chat',
97
+ }}
98
+ dock={{
99
+ title: 'Support',
100
+ icon: <MessageCircle className="text-primary h-4 w-4" />,
101
+ position: position as ChatFABPosition,
102
+ height: 600,
103
+ headerActions: <ChatHeaderLanguageButton />,
104
+ }}
105
+ greeting={
106
+ withGreeting
107
+ ? {
108
+ content: 'Hey 👋 Got a question? Happy to help.',
109
+ senderName: 'Anna · Support',
110
+ avatar: (
111
+ <Avatar className="h-8 w-8">
112
+ <AvatarImage src="https://i.pravatar.cc/64?img=47" />
113
+ <AvatarFallback>A</AvatarFallback>
114
+ </Avatar>
115
+ ),
116
+ delayMs: 600,
117
+ dismissStorageKey: null,
118
+ }
119
+ : undefined
120
+ }
121
+ hotkey={{ key: '/', meta: true }}
122
+ >
123
+ <DemoChat />
124
+ </ChatLauncher>
125
+ );
126
+ };
127
+
128
+ export const MobileFullscreen = () => (
129
+ <ChatLauncher
130
+ fab={{ icon: <MessageCircle className="h-6 w-6" />, tooltip: 'Open chat' }}
131
+ dock={{ title: 'Mobile-friendly', width: 480, height: 600 }}
132
+ >
133
+ <DemoChat />
134
+ </ChatLauncher>
135
+ );
136
+
137
+ /**
138
+ * LiveChat / Intercom-style proactive invite: animated FAB + greeting
139
+ * bubble with avatar, sender name, and a friendly nudge. The greeting
140
+ * fades in after a short delay; click the bubble or the FAB to open
141
+ * the chat. Composer auto-focuses on open.
142
+ */
143
+ export const LiveChatStyle = () => (
144
+ <ChatLauncher
145
+ fab={{
146
+ variant: 'animated',
147
+ icon: <Bot className="h-6 w-6" />,
148
+ ariaLabel: 'Chat with us',
149
+ tooltip: 'Chat with us',
150
+ }}
151
+ dock={{
152
+ title: 'Anna · Sales',
153
+ icon: (
154
+ <Avatar className="h-5 w-5">
155
+ <AvatarImage src="https://i.pravatar.cc/64?img=47" />
156
+ <AvatarFallback>A</AvatarFallback>
157
+ </Avatar>
158
+ ),
159
+ height: 620,
160
+ }}
161
+ greeting={{
162
+ content: (
163
+ <>
164
+ Hi 👋 Looking for something? I&apos;m around — we usually reply{' '}
165
+ <strong>within a minute</strong>.
166
+ </>
167
+ ),
168
+ senderName: 'Anna · Sales',
169
+ avatar: (
170
+ <Avatar className="h-9 w-9">
171
+ <AvatarImage src="https://i.pravatar.cc/64?img=47" />
172
+ <AvatarFallback>A</AvatarFallback>
173
+ </Avatar>
174
+ ),
175
+ delayMs: 1200,
176
+ dismissStorageKey: null,
177
+ }}
178
+ hotkey={{ key: '/', meta: true }}
179
+ >
180
+ <DemoChat />
181
+ </ChatLauncher>
182
+ );
183
+
184
+ // ── Live push demo ─────────────────────────────────────────────────────────
185
+ //
186
+ // Hosts wire push notifications via:
187
+ // 1. `useChatUnread({ open })` inside the `<ChatProvider>` for state.
188
+ // 2. `chat.injectMessage(...)` to feed inbound messages from Centrifugo / WS.
189
+ // 3. `<ChatLauncher unreadMessage onMarkRead>` to surface the preview bubble.
190
+
191
+ function PushInjector({ enabled, intervalMs }: { enabled: boolean; intervalMs: number }) {
192
+ const chat = useChatContext();
193
+ useEffect(() => {
194
+ if (!enabled) return;
195
+ let i = 0;
196
+ const samples = [
197
+ 'Hey 👋 just checking in — anything I can help with?',
198
+ 'Heads up: your order #1042 just shipped 🚀',
199
+ 'New: try the redesigned dashboard — feedback welcome.',
200
+ ];
201
+ const id = setInterval(() => {
202
+ chat.injectMessage({
203
+ id: `push-${Date.now()}-${i}`,
204
+ role: 'assistant',
205
+ content: samples[i % samples.length]!,
206
+ createdAt: Date.now(),
207
+ sender: { name: 'Anna · Sales', avatarUrl: 'https://i.pravatar.cc/64?img=47' },
208
+ });
209
+ i++;
210
+ }, intervalMs);
211
+ return () => clearInterval(id);
212
+ }, [chat, enabled, intervalMs]);
213
+ return null;
214
+ }
215
+
216
+ function InlineChatUI() {
217
+ // Lightweight chat shell rendered inside an existing ChatProvider — used
218
+ // so we don't double-mount provider state in the push demo.
219
+ const chat = useChatContext();
220
+ const composer = useChatComposer({
221
+ onSubmit: (content, attachments) => chat.sendMessage(content, attachments),
222
+ disabled: chat.isStreaming,
223
+ });
224
+ return (
225
+ <div className="flex h-full flex-col">
226
+ <MessageList
227
+ className="flex-1"
228
+ renderEmpty={() => <EmptyState greeting="Anna · Sales" />}
229
+ />
230
+ <Composer composer={composer} />
231
+ </div>
232
+ );
233
+ }
234
+
235
+ function LivePushShell({ intervalMs, autoPush }: { intervalMs: number; autoPush: boolean }) {
236
+ const [open, setOpen] = useState(false);
237
+ const { unread, markRead } = useChatUnread({ open });
238
+ return (
239
+ <>
240
+ <PushInjector enabled={autoPush} intervalMs={intervalMs} />
241
+ <ChatLauncher
242
+ open={open}
243
+ onOpenChange={setOpen}
244
+ fab={{ ariaLabel: 'Open chat', tooltip: 'Open chat' }}
245
+ dock={{ title: 'Sales · Anna', height: 580 }}
246
+ unreadMessage={unread}
247
+ onMarkRead={markRead}
248
+ >
249
+ <InlineChatUI />
250
+ </ChatLauncher>
251
+ </>
252
+ );
253
+ }
254
+
255
+ /**
256
+ * Simulates server-pushed messages every N seconds. While the dock is
257
+ * closed, the FAB badge shows the unread indicator and a notification
258
+ * bubble appears next to the FAB with sender + message preview. Open the
259
+ * chat (FAB / preview / ⌘/) and unread resets to zero.
260
+ */
261
+ export const WithLivePush = () => {
262
+ const [autoPush] = useBoolean('auto-push', { defaultValue: true });
263
+ const [intervalSec] = useNumber('interval (s)', {
264
+ defaultValue: 6,
265
+ min: 2,
266
+ max: 30,
267
+ });
268
+
269
+ const transport = useMemo(() => makeBasicTransport(SEED_BASIC), []);
270
+
271
+ return (
272
+ <ChatProvider transport={transport} config={{ greeting: 'Sales chat — push demo' }}>
273
+ <LivePushShell intervalMs={intervalSec * 1000} autoPush={autoPush} />
274
+ </ChatProvider>
275
+ );
276
+ };
277
+
278
+ export const VariantsAndSizes = () => (
279
+ <div className="p-6 space-y-6">
280
+ <div>
281
+ <p className="text-sm font-medium">Variants (inline)</p>
282
+ <p className="text-muted-foreground text-xs mb-3">
283
+ Inline preview only — production usage drops <code className="text-xs">inline</code> and pins to the viewport corner.
284
+ </p>
285
+ <div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
286
+ {(['simple', 'animated', 'glass'] as const).map((variant) => (
287
+ <div key={variant} className="flex flex-col items-center gap-3 rounded-lg border bg-card p-6">
288
+ <div className="text-xs font-medium text-muted-foreground">{variant}</div>
289
+ <ChatFAB
290
+ inline
291
+ variant={variant}
292
+ size="lg"
293
+ onClick={() => {}}
294
+ ariaLabel={`${variant} FAB`}
295
+ tooltip={variant === 'simple' ? 'Open chat' : undefined}
296
+ badge={variant === 'glass' ? 5 : undefined}
297
+ pulse={variant === 'simple'}
298
+ icon={
299
+ variant === 'animated' ? <Zap size={26} /> :
300
+ variant === 'glass' ? <Sparkles className="h-6 w-6" /> :
301
+ <Bot size={26} />
302
+ }
303
+ />
304
+ </div>
305
+ ))}
306
+ </div>
307
+ </div>
308
+
309
+ <div>
310
+ <p className="text-sm font-medium">Sizes (inline)</p>
311
+ <div className="mt-3 grid grid-cols-3 gap-4">
312
+ {(['sm', 'md', 'lg'] as const).map((s) => (
313
+ <div key={s} className="flex flex-col items-center gap-2 rounded-lg border bg-card p-4">
314
+ <div className="text-xs text-muted-foreground">size={s}</div>
315
+ <ChatFAB inline size={s} onClick={() => {}} ariaLabel={`${s} FAB`} />
316
+ </div>
317
+ ))}
318
+ </div>
319
+ </div>
320
+ </div>
321
+ );
@@ -0,0 +1,147 @@
1
+ import { useMemo, useState } from 'react';
2
+ import { RotateCcw, Settings } from 'lucide-react';
3
+
4
+ import { defineStory } from '@djangocfg/playground';
5
+ import { Button } from '@djangocfg/ui-core/components';
6
+
7
+ import { ChatRoot } from '../components/ChatRoot';
8
+ import { ChatDock } from '../launcher/ChatDock';
9
+ import { ChatHeader } from '../launcher/ChatHeader';
10
+ import { ChatHeaderActionButton } from '../launcher/ChatHeaderActionButton';
11
+ import { ChatHeaderModeToggle } from '../launcher/ChatHeaderModeToggle';
12
+ import { useChatDockPrefs } from '../hooks/useChatDockPrefs';
13
+ import { useChatReset } from '../hooks/useChatReset';
14
+ import { SEED_BASIC, makeBasicTransport } from './shared';
15
+
16
+ export default defineStory({
17
+ title: 'Tools/Chat/Header',
18
+ component: ChatHeader,
19
+ description:
20
+ '`<ChatHeader>` + `headerActions` slot + helpers: `ChatHeaderActionButton`, `ChatHeaderModeToggle`. State persisted via `useChatDockPrefs` (ui-core localStorage).',
21
+ });
22
+
23
+ function ResetButton() {
24
+ const { reset, isResetting } = useChatReset({
25
+ onReset: async () => {
26
+ await new Promise((r) => setTimeout(r, 800));
27
+ return true;
28
+ },
29
+ onSuccess: () => alert('Context cleared.'),
30
+ });
31
+ return (
32
+ <ChatHeaderActionButton
33
+ icon={<RotateCcw className="h-3.5 w-3.5" />}
34
+ ariaLabel="Clear conversation context"
35
+ onClick={() => reset()}
36
+ loading={isResetting}
37
+ />
38
+ );
39
+ }
40
+
41
+ /** Standalone `<ChatHeader>` — title + actions + close. No surrounding chrome. */
42
+ export const HeaderOnly = () => (
43
+ <div className="p-6">
44
+ <div className="overflow-hidden rounded-lg border border-border bg-popover">
45
+ <ChatHeader
46
+ title="Assistant"
47
+ onClose={() => alert('close')}
48
+ actions={
49
+ <>
50
+ <ResetButton />
51
+ <ChatHeaderActionButton
52
+ icon={<Settings className="h-3.5 w-3.5" />}
53
+ ariaLabel="Settings"
54
+ onClick={() => alert('settings')}
55
+ badge={2}
56
+ />
57
+ </>
58
+ }
59
+ />
60
+ </div>
61
+ </div>
62
+ );
63
+
64
+ /**
65
+ * Production-like dock with persistent prefs + mode toggle.
66
+ * Reload the story — `mode` and `side` survive via localStorage.
67
+ */
68
+ export const WithPersistentPrefs = () => {
69
+ const [open, setOpen] = useState(true);
70
+ const prefs = useChatDockPrefs({ storageKey: 'story.chat.dock.prefs' });
71
+ const transport = useMemo(() => makeBasicTransport(SEED_BASIC), []);
72
+
73
+ return (
74
+ <>
75
+ <div className="p-6">
76
+ <h2 className="text-foreground text-lg font-semibold">Page content</h2>
77
+ <p className="text-muted-foreground mt-2 max-w-md text-sm">
78
+ The header toggle switches between popover and side-docked layouts.
79
+ When the side dock is active in production, ChatDock auto-reserves
80
+ <code className="text-xs"> padding-{prefs.side}</code> on the body so this content
81
+ shifts away from the dock. Current prefs: <code className="text-xs">mode={prefs.mode}</code> ·{' '}
82
+ <code className="text-xs">side={prefs.side}</code>.
83
+ </p>
84
+ {!open && (
85
+ <Button className="mt-4" onClick={() => setOpen(true)}>
86
+ Open chat
87
+ </Button>
88
+ )}
89
+ </div>
90
+ <ChatDock
91
+ open={open}
92
+ onClose={() => setOpen(false)}
93
+ mode={prefs.mode}
94
+ side={prefs.side}
95
+ width={prefs.mode === 'side' ? prefs.sideWidth : 440}
96
+ height={600}
97
+ title="CRM Assistant"
98
+ headerActions={
99
+ <>
100
+ <ResetButton />
101
+ <ChatHeaderModeToggle mode={prefs.mode} onToggle={prefs.toggleMode} />
102
+ </>
103
+ }
104
+ >
105
+ <ChatRoot transport={transport} config={{ greeting: 'How can I help?' }} />
106
+ </ChatDock>
107
+ </>
108
+ );
109
+ };
110
+
111
+ /**
112
+ * `mode='side'` pinned to the viewport edge, reserves body padding so page
113
+ * content stays visible. This is the production behaviour — no `inline`.
114
+ */
115
+ export const SideMode = () => {
116
+ const [open, setOpen] = useState(true);
117
+ const transport = useMemo(() => makeBasicTransport(SEED_BASIC), []);
118
+
119
+ return (
120
+ <>
121
+ <div className="p-6">
122
+ <h2 className="text-foreground text-lg font-semibold">Page content</h2>
123
+ <p className="text-muted-foreground mt-2 max-w-md text-sm">
124
+ The dock is pinned full-height to the right edge. The browser body
125
+ gets <code className="text-xs">padding-right</code> equal to the dock width while
126
+ it&apos;s open, so this column stays visible.
127
+ </p>
128
+ {!open && (
129
+ <Button className="mt-4" onClick={() => setOpen(true)}>
130
+ Open side dock
131
+ </Button>
132
+ )}
133
+ </div>
134
+ <ChatDock
135
+ open={open}
136
+ onClose={() => setOpen(false)}
137
+ mode="side"
138
+ side="right"
139
+ width={400}
140
+ title="Workspace Chat"
141
+ headerActions={<ResetButton />}
142
+ >
143
+ <ChatRoot transport={transport} config={{ greeting: 'Side-docked chat.' }} />
144
+ </ChatDock>
145
+ </>
146
+ );
147
+ };