@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.
- package/README.md +132 -899
- package/dist/ChatRoot-6IZFM5HM.mjs +5 -0
- package/dist/{ChatRoot-EJC5Y2YM.cjs.map → ChatRoot-6IZFM5HM.mjs.map} +1 -1
- package/dist/ChatRoot-LW4XNIKP.cjs +14 -0
- package/dist/{ChatRoot-QOSKJPM6.mjs.map → ChatRoot-LW4XNIKP.cjs.map} +1 -1
- package/dist/DictationField-2ZLQWLYV.mjs +4 -0
- package/dist/DictationField-2ZLQWLYV.mjs.map +1 -0
- package/dist/DictationField-IPPJ54CU.cjs +13 -0
- package/dist/DictationField-IPPJ54CU.cjs.map +1 -0
- package/dist/{DocsLayout-2YKPXZYO.mjs → DocsLayout-2P3ONDWJ.mjs} +3 -3
- package/dist/{DocsLayout-2YKPXZYO.mjs.map → DocsLayout-2P3ONDWJ.mjs.map} +1 -1
- package/dist/{DocsLayout-Q4KS3QWW.cjs → DocsLayout-2YZNS5VK.cjs} +8 -8
- package/dist/{DocsLayout-Q4KS3QWW.cjs.map → DocsLayout-2YZNS5VK.cjs.map} +1 -1
- package/dist/chunk-4LXG3NBV.mjs +833 -0
- package/dist/chunk-4LXG3NBV.mjs.map +1 -0
- package/dist/{chunk-XACCHZH2.cjs → chunk-FIRK5CEH.cjs} +42 -4
- package/dist/chunk-FIRK5CEH.cjs.map +1 -0
- package/dist/{chunk-NWUT327A.mjs → chunk-HIK6BPL7.mjs} +38 -5
- package/dist/chunk-HIK6BPL7.mjs.map +1 -0
- package/dist/chunk-KMSBGNVC.cjs +835 -0
- package/dist/chunk-KMSBGNVC.cjs.map +1 -0
- package/dist/chunk-OZAU3QWD.cjs +2493 -0
- package/dist/chunk-OZAU3QWD.cjs.map +1 -0
- package/dist/chunk-UWVP6LCW.mjs +2447 -0
- package/dist/chunk-UWVP6LCW.mjs.map +1 -0
- package/dist/index.cjs +1532 -100
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1148 -107
- package/dist/index.d.ts +1148 -107
- package/dist/index.mjs +1421 -51
- package/dist/index.mjs.map +1 -1
- package/package.json +16 -8
- package/src/audio-assets.d.ts +8 -0
- package/src/components/markdown/MarkdownMessage/CollapseToggle.tsx +3 -1
- package/src/components/markdown/MarkdownMessage/components.tsx +2 -5
- package/src/stories/index.ts +32 -2
- package/src/tools/Chat/README.md +347 -530
- package/src/tools/Chat/components/Attachments.tsx +6 -1
- package/src/tools/Chat/components/ChatRoot.tsx +30 -2
- package/src/tools/Chat/components/Composer.tsx +20 -3
- package/src/tools/Chat/components/ErrorBanner.tsx +7 -3
- package/src/tools/Chat/components/MessageActions.tsx +3 -1
- package/src/tools/Chat/components/MessageBubble.tsx +6 -5
- package/src/tools/Chat/components/MessageList.tsx +87 -1
- package/src/tools/Chat/components/ToolCalls.tsx +21 -3
- package/src/tools/Chat/context/ChatProvider.tsx +21 -3
- package/src/tools/Chat/core/audio/audioBus.ts +10 -163
- package/src/tools/Chat/core/audio/defaults.ts +43 -0
- package/src/tools/Chat/core/audio/index.ts +1 -0
- package/src/tools/Chat/core/audio/preferences.ts +5 -59
- package/src/tools/Chat/core/audio/sounds/error.mp3 +0 -0
- package/src/tools/Chat/core/audio/sounds/mention.mp3 +0 -0
- package/src/tools/Chat/core/audio/sounds/notification.mp3 +0 -0
- package/src/tools/Chat/core/audio/sounds/received.mp3 +0 -0
- package/src/tools/Chat/core/audio/sounds/sent.mp3 +0 -0
- package/src/tools/Chat/core/audio/sounds/start.mp3 +0 -0
- package/src/tools/Chat/core/audio/types.ts +28 -0
- package/src/tools/Chat/core/reducer.ts +33 -0
- package/src/tools/Chat/core/transport/index.ts +13 -0
- package/src/tools/Chat/core/transport/mappers/index.ts +6 -0
- package/src/tools/Chat/core/transport/mappers/pydantic-ai.ts +142 -0
- package/src/tools/Chat/core/transport/pydantic-ai-transport.ts +208 -0
- package/src/tools/Chat/core/transport/sse.ts +18 -5
- package/src/tools/Chat/hooks/index.ts +25 -0
- package/src/tools/Chat/hooks/useAutoFocusOnStreamEnd.ts +5 -3
- package/src/tools/Chat/hooks/useChat.ts +28 -0
- package/src/tools/Chat/hooks/useChatAudio.ts +59 -180
- package/src/tools/Chat/hooks/useChatDockPrefs.ts +74 -0
- package/src/tools/Chat/hooks/useChatReset.ts +70 -0
- package/src/tools/Chat/hooks/useChatUnread.ts +87 -0
- package/src/tools/Chat/hooks/useFocusOnEmptyClick.ts +111 -0
- package/src/tools/Chat/hooks/useVisitorFingerprint.ts +48 -0
- package/src/tools/Chat/index.ts +69 -1
- package/src/tools/Chat/launcher/ChatDock.tsx +263 -0
- package/src/tools/Chat/launcher/ChatFAB.tsx +349 -0
- package/src/tools/Chat/launcher/ChatGreeting.tsx +200 -0
- package/src/tools/Chat/launcher/ChatHeader.tsx +76 -0
- package/src/tools/Chat/launcher/ChatHeaderActionButton.tsx +87 -0
- package/src/tools/Chat/launcher/ChatHeaderAudioToggle.tsx +47 -0
- package/src/tools/Chat/launcher/ChatHeaderLanguageButton.tsx +179 -0
- package/src/tools/Chat/launcher/ChatHeaderModeToggle.tsx +57 -0
- package/src/tools/Chat/launcher/ChatHeaderResetButton.tsx +93 -0
- package/src/tools/Chat/launcher/ChatLauncher.tsx +321 -0
- package/src/tools/Chat/launcher/ChatUnreadPreview.tsx +197 -0
- package/src/tools/Chat/launcher/index.ts +46 -0
- package/src/tools/Chat/launcher/useChatPresence.ts +44 -0
- package/src/tools/Chat/stories/01-basic.story.tsx +64 -0
- package/src/tools/Chat/stories/02-bubbles.story.tsx +21 -0
- package/src/tools/Chat/stories/03-tool-calls.story.tsx +59 -0
- package/src/tools/Chat/stories/04-personas.story.tsx +78 -0
- package/src/tools/Chat/stories/05-launcher.story.tsx +321 -0
- package/src/tools/Chat/stories/06-header.story.tsx +147 -0
- package/src/tools/Chat/stories/07-audio-actions.story.tsx +112 -0
- package/src/tools/Chat/stories/shared/Frame.tsx +21 -0
- package/src/tools/Chat/stories/shared/index.ts +5 -0
- package/src/tools/Chat/stories/shared/messages.ts +39 -0
- package/src/tools/Chat/stories/shared/personas.ts +13 -0
- package/src/tools/Chat/stories/shared/seeds.ts +92 -0
- package/src/tools/Chat/stories/shared/transports.ts +36 -0
- package/src/tools/Chat/styles/bubbleTokens.ts +71 -0
- package/src/tools/Chat/styles/index.ts +16 -0
- package/src/tools/Chat/styles/useChatStyles.ts +101 -0
- package/src/tools/Chat/types/attachment.ts +25 -0
- package/src/tools/Chat/types/config.ts +48 -0
- package/src/tools/Chat/types/events.ts +35 -0
- package/src/tools/Chat/types/index.ts +34 -0
- package/src/tools/Chat/types/labels.ts +38 -0
- package/src/tools/Chat/types/message.ts +32 -0
- package/src/tools/Chat/types/persona.ts +31 -0
- package/src/tools/Chat/types/session.ts +43 -0
- package/src/tools/Chat/types/tool-call.ts +17 -0
- package/src/tools/Chat/types/transport.ts +28 -0
- package/src/tools/Chat/types.ts +5 -240
- package/src/tools/MarkdownEditor/MarkdownEditor.tsx +50 -14
- package/src/tools/MarkdownEditor/index.ts +1 -1
- package/src/tools/SpeechRecognition/README.md +336 -0
- package/src/tools/SpeechRecognition/__tests__/ids.test.ts +15 -0
- package/src/tools/SpeechRecognition/__tests__/language.test.ts +59 -0
- package/src/tools/SpeechRecognition/__tests__/reducer.test.ts +71 -0
- package/src/tools/SpeechRecognition/__tests__/transcript.test.ts +52 -0
- package/src/tools/SpeechRecognition/components/DevicePicker.tsx +49 -0
- package/src/tools/SpeechRecognition/components/DictationButton.tsx +93 -0
- package/src/tools/SpeechRecognition/components/EngineBadge.tsx +30 -0
- package/src/tools/SpeechRecognition/components/ErrorBanner.tsx +52 -0
- package/src/tools/SpeechRecognition/components/LanguagePicker.tsx +63 -0
- package/src/tools/SpeechRecognition/components/MicMeter.tsx +63 -0
- package/src/tools/SpeechRecognition/components/PushToTalkHint.tsx +51 -0
- package/src/tools/SpeechRecognition/components/TranscriptView.tsx +55 -0
- package/src/tools/SpeechRecognition/components/index.ts +16 -0
- package/src/tools/SpeechRecognition/context/SpeechRecognitionProvider.tsx +47 -0
- package/src/tools/SpeechRecognition/context/index.ts +6 -0
- package/src/tools/SpeechRecognition/core/audio/defaults.ts +24 -0
- package/src/tools/SpeechRecognition/core/engine/external.ts +222 -0
- package/src/tools/SpeechRecognition/core/engine/http.ts +147 -0
- package/src/tools/SpeechRecognition/core/engine/index.ts +52 -0
- package/src/tools/SpeechRecognition/core/engine/mediarecorder.ts +105 -0
- package/src/tools/SpeechRecognition/core/engine/websocket.ts +211 -0
- package/src/tools/SpeechRecognition/core/engine/webspeech.ts +188 -0
- package/src/tools/SpeechRecognition/core/ids.ts +11 -0
- package/src/tools/SpeechRecognition/core/index.ts +14 -0
- package/src/tools/SpeechRecognition/core/language.ts +78 -0
- package/src/tools/SpeechRecognition/core/languages-catalog.ts +229 -0
- package/src/tools/SpeechRecognition/core/logger.ts +3 -0
- package/src/tools/SpeechRecognition/core/reducer.ts +105 -0
- package/src/tools/SpeechRecognition/core/transcript.ts +36 -0
- package/src/tools/SpeechRecognition/hooks/index.ts +14 -0
- package/src/tools/SpeechRecognition/hooks/useDictation.ts +59 -0
- package/src/tools/SpeechRecognition/hooks/useEnginePrefs.ts +15 -0
- package/src/tools/SpeechRecognition/hooks/useMicDevices.ts +57 -0
- package/src/tools/SpeechRecognition/hooks/useMicLevel.ts +52 -0
- package/src/tools/SpeechRecognition/hooks/usePushToTalk.ts +85 -0
- package/src/tools/SpeechRecognition/hooks/useResolvedLanguage.ts +28 -0
- package/src/tools/SpeechRecognition/hooks/useSpeechLanguageInfo.ts +108 -0
- package/src/tools/SpeechRecognition/hooks/useSpeechRecognition.ts +188 -0
- package/src/tools/SpeechRecognition/hooks/useVoiceSupport.ts +78 -0
- package/src/tools/SpeechRecognition/index.ts +82 -0
- package/src/tools/SpeechRecognition/lazy.tsx +19 -0
- package/src/tools/SpeechRecognition/store/index.ts +2 -0
- package/src/tools/SpeechRecognition/store/prefsStore.ts +54 -0
- package/src/tools/SpeechRecognition/stories/01-basic.story.tsx +32 -0
- package/src/tools/SpeechRecognition/stories/02-dictation-field.story.tsx +32 -0
- package/src/tools/SpeechRecognition/stories/03-push-to-talk.story.tsx +27 -0
- package/src/tools/SpeechRecognition/stories/04-mic-meter.story.tsx +35 -0
- package/src/tools/SpeechRecognition/stories/05-custom-engine-http.story.tsx +40 -0
- package/src/tools/SpeechRecognition/stories/06-custom-engine-ws.story.tsx +48 -0
- package/src/tools/SpeechRecognition/stories/07-language-device.story.tsx +57 -0
- package/src/tools/SpeechRecognition/stories/08-errors-permissions.story.tsx +25 -0
- package/src/tools/SpeechRecognition/stories/09-chat-voice.story.tsx +90 -0
- package/src/tools/SpeechRecognition/stories/shared.tsx +123 -0
- package/src/tools/SpeechRecognition/types.ts +133 -0
- package/src/tools/SpeechRecognition/widgets/DictationField.tsx +105 -0
- package/src/tools/SpeechRecognition/widgets/VoiceComposerSlot.tsx +305 -0
- package/src/tools/SpeechRecognition/widgets/VoiceMessageRecorder.tsx +88 -0
- package/src/tools/SpeechRecognition/widgets/index.ts +6 -0
- package/dist/ChatRoot-EJC5Y2YM.cjs +0 -14
- package/dist/ChatRoot-QOSKJPM6.mjs +0 -5
- package/dist/chunk-NWUT327A.mjs.map +0 -1
- package/dist/chunk-QLMKCSR6.mjs +0 -2420
- package/dist/chunk-QLMKCSR6.mjs.map +0 -1
- package/dist/chunk-SI5RD2GD.cjs +0 -2460
- package/dist/chunk-SI5RD2GD.cjs.map +0 -1
- package/dist/chunk-XACCHZH2.cjs.map +0 -1
- 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'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'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
|
+
};
|