@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,87 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { forwardRef } from 'react';
|
|
4
|
+
import type { ButtonHTMLAttributes, ReactNode } from 'react';
|
|
5
|
+
|
|
6
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
7
|
+
|
|
8
|
+
export interface ChatHeaderActionButtonProps
|
|
9
|
+
extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {
|
|
10
|
+
/** Icon (required). */
|
|
11
|
+
icon: ReactNode;
|
|
12
|
+
/** Accessible label + native tooltip. */
|
|
13
|
+
ariaLabel: string;
|
|
14
|
+
/** Optional unread / status badge — small number on top-right. */
|
|
15
|
+
badge?: number;
|
|
16
|
+
/** Mark as destructive — uses destructive hover tokens. */
|
|
17
|
+
destructive?: boolean;
|
|
18
|
+
/** Optional visual loading state (e.g. while reset is in flight). */
|
|
19
|
+
loading?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Compact icon button for the chat header actions slot.
|
|
24
|
+
*
|
|
25
|
+
* Standard chrome: 28×28 ghost button, hover bg-accent, focus ring, optional
|
|
26
|
+
* destructive variant, optional numeric badge for unread / pending.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```tsx
|
|
30
|
+
* <ChatHeader
|
|
31
|
+
* title="Assistant"
|
|
32
|
+
* onClose={close}
|
|
33
|
+
* actions={
|
|
34
|
+
* <>
|
|
35
|
+
* <ChatHeaderActionButton
|
|
36
|
+
* icon={<RotateCcw className="h-3.5 w-3.5" />}
|
|
37
|
+
* ariaLabel="Clear context"
|
|
38
|
+
* onClick={handleReset}
|
|
39
|
+
* loading={isResetting}
|
|
40
|
+
* />
|
|
41
|
+
* <ChatHeaderActionButton
|
|
42
|
+
* icon={<Settings className="h-3.5 w-3.5" />}
|
|
43
|
+
* ariaLabel="Settings"
|
|
44
|
+
* onClick={openSettings}
|
|
45
|
+
* />
|
|
46
|
+
* </>
|
|
47
|
+
* }
|
|
48
|
+
* />
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export const ChatHeaderActionButton = forwardRef<HTMLButtonElement, ChatHeaderActionButtonProps>(
|
|
52
|
+
function ChatHeaderActionButton(
|
|
53
|
+
{ icon, ariaLabel, badge, destructive, loading, disabled, className, ...rest },
|
|
54
|
+
ref,
|
|
55
|
+
) {
|
|
56
|
+
return (
|
|
57
|
+
<button
|
|
58
|
+
ref={ref}
|
|
59
|
+
type="button"
|
|
60
|
+
aria-label={ariaLabel}
|
|
61
|
+
title={ariaLabel}
|
|
62
|
+
disabled={disabled || loading}
|
|
63
|
+
className={cn(
|
|
64
|
+
'relative inline-flex h-7 w-7 items-center justify-center rounded-md',
|
|
65
|
+
'text-muted-foreground transition-colors',
|
|
66
|
+
'hover:bg-accent hover:text-foreground',
|
|
67
|
+
'focus:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
68
|
+
'disabled:opacity-50 disabled:cursor-not-allowed',
|
|
69
|
+
destructive && 'hover:bg-destructive/15 hover:text-destructive',
|
|
70
|
+
loading && 'animate-pulse',
|
|
71
|
+
className,
|
|
72
|
+
)}
|
|
73
|
+
{...rest}
|
|
74
|
+
>
|
|
75
|
+
{icon}
|
|
76
|
+
{badge !== undefined && (
|
|
77
|
+
<span
|
|
78
|
+
aria-hidden="true"
|
|
79
|
+
className="absolute -right-0.5 -top-0.5 inline-flex min-w-[14px] h-[14px] items-center justify-center rounded-full bg-destructive px-1 text-[9px] font-semibold leading-none text-destructive-foreground ring-2 ring-background"
|
|
80
|
+
>
|
|
81
|
+
{badge > 9 ? '9+' : badge}
|
|
82
|
+
</span>
|
|
83
|
+
)}
|
|
84
|
+
</button>
|
|
85
|
+
);
|
|
86
|
+
},
|
|
87
|
+
);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Volume2, VolumeX } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
import { ChatHeaderActionButton } from './ChatHeaderActionButton';
|
|
6
|
+
|
|
7
|
+
export interface ChatHeaderAudioToggleProps {
|
|
8
|
+
/** Current muted state. */
|
|
9
|
+
muted: boolean;
|
|
10
|
+
/** Toggle handler. Wire to `useChatAudio().setMuted` or `toggleMute`. */
|
|
11
|
+
onToggle: () => void;
|
|
12
|
+
/** Override tooltip label for muted → unmuted. */
|
|
13
|
+
unmuteLabel?: string;
|
|
14
|
+
/** Override tooltip label for unmuted → muted. */
|
|
15
|
+
muteLabel?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Mute / unmute notification sounds. Drop into a `<ChatHeader>` actions
|
|
20
|
+
* slot or into `<ChatDock headerActions>`.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```tsx
|
|
24
|
+
* const audio = useChatAudio({ sounds: {...} });
|
|
25
|
+
* <ChatHeaderAudioToggle muted={audio.muted} onToggle={() => audio.setMuted(!audio.muted)} />
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export function ChatHeaderAudioToggle({
|
|
29
|
+
muted,
|
|
30
|
+
onToggle,
|
|
31
|
+
unmuteLabel = 'Unmute notifications',
|
|
32
|
+
muteLabel = 'Mute notifications',
|
|
33
|
+
}: ChatHeaderAudioToggleProps) {
|
|
34
|
+
return (
|
|
35
|
+
<ChatHeaderActionButton
|
|
36
|
+
icon={
|
|
37
|
+
muted ? (
|
|
38
|
+
<VolumeX className="h-3.5 w-3.5" />
|
|
39
|
+
) : (
|
|
40
|
+
<Volume2 className="h-3.5 w-3.5" />
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
ariaLabel={muted ? unmuteLabel : muteLabel}
|
|
44
|
+
onClick={onToggle}
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type * as React from 'react';
|
|
4
|
+
import { useMemo } from 'react';
|
|
5
|
+
import { Globe } from 'lucide-react';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
Combobox,
|
|
9
|
+
type ComboboxOption,
|
|
10
|
+
Flag,
|
|
11
|
+
} from '@djangocfg/ui-core/components';
|
|
12
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
13
|
+
|
|
14
|
+
import { findSpeechLanguage } from '../../SpeechRecognition';
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
WEB_SPEECH_LANGUAGES,
|
|
18
|
+
countryFromTag,
|
|
19
|
+
useResolvedLanguage,
|
|
20
|
+
useSpeechPrefs,
|
|
21
|
+
} from '../../SpeechRecognition';
|
|
22
|
+
|
|
23
|
+
export interface ChatHeaderLanguageButtonProps {
|
|
24
|
+
/** Override aria-label. Default "Speech language". */
|
|
25
|
+
ariaLabel?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Subset of BCP-47 tags to offer. Default: every entry from the
|
|
28
|
+
* Web Speech catalogue (~66 tags incl. regional variants). Pass a
|
|
29
|
+
* tighter list when your backend STT only supports a subset.
|
|
30
|
+
*/
|
|
31
|
+
allowedTags?: string[];
|
|
32
|
+
/** Hide the globe-fallback icon when no flag resolves. */
|
|
33
|
+
hideFallbackIcon?: boolean;
|
|
34
|
+
className?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Compact flag-button language picker for the chat header. Built on
|
|
39
|
+
* top of the ui-core `<Combobox>` — searchable autocomplete with
|
|
40
|
+
* flags, ~66 BCP-47 tags from the official Chrome Web Speech demo, and
|
|
41
|
+
* a custom 28×28 trigger via `renderTrigger`.
|
|
42
|
+
*
|
|
43
|
+
* The selection persists into `useSpeechPrefs` (zustand+localStorage)
|
|
44
|
+
* so `useSpeechRecognition` picks it up as the second-priority resolver
|
|
45
|
+
* value (above i18n locale, below an explicit `language` prop).
|
|
46
|
+
*/
|
|
47
|
+
export function ChatHeaderLanguageButton({
|
|
48
|
+
ariaLabel = 'Speech language',
|
|
49
|
+
allowedTags,
|
|
50
|
+
hideFallbackIcon,
|
|
51
|
+
className,
|
|
52
|
+
}: ChatHeaderLanguageButtonProps): React.ReactElement {
|
|
53
|
+
const prefs = useSpeechPrefs();
|
|
54
|
+
const active = useResolvedLanguage();
|
|
55
|
+
|
|
56
|
+
// Flatten every dialect into one Combobox option. Display name keeps
|
|
57
|
+
// the native language label; we stash the English aliases + BCP-47
|
|
58
|
+
// tag + region in `description` purely as a hidden search index.
|
|
59
|
+
// (We intentionally hide the description from the rendered row in
|
|
60
|
+
// `renderOption` — it would just clutter the dropdown.)
|
|
61
|
+
const options = useMemo<ComboboxOption[]>(() => {
|
|
62
|
+
const allow = allowedTags ? new Set(allowedTags) : null;
|
|
63
|
+
const out: ComboboxOption[] = [];
|
|
64
|
+
for (const lang of WEB_SPEECH_LANGUAGES) {
|
|
65
|
+
for (const d of lang.dialects) {
|
|
66
|
+
if (allow && !allow.has(d.code)) continue;
|
|
67
|
+
out.push({
|
|
68
|
+
value: d.code,
|
|
69
|
+
// "Русский" / "Español — Argentina" / "English — United States"
|
|
70
|
+
label:
|
|
71
|
+
lang.dialects.length === 1
|
|
72
|
+
? lang.name
|
|
73
|
+
: `${lang.name} — ${d.region}`,
|
|
74
|
+
// Search-only index: English name, BCP-47 tag, ISO, region.
|
|
75
|
+
// Lets users type "russian" / "ru-RU" / "ru" / "argentina"
|
|
76
|
+
// and still find the row regardless of native script.
|
|
77
|
+
description: `${lang.englishName} ${d.code} ${lang.iso} ${d.region}`.toLowerCase(),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}, [allowedTags]);
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<Combobox
|
|
86
|
+
options={options}
|
|
87
|
+
value={prefs.language ?? active}
|
|
88
|
+
onValueChange={(v) => prefs.setLanguage(v || null)}
|
|
89
|
+
placeholder={ariaLabel}
|
|
90
|
+
searchPlaceholder="Search language…"
|
|
91
|
+
filterFunction={(opt, search) => {
|
|
92
|
+
const s = search.toLowerCase();
|
|
93
|
+
// Match label (native script), value (BCP-47), and our packed
|
|
94
|
+
// search index in description (English name + tag + region).
|
|
95
|
+
return (
|
|
96
|
+
opt.label.toLowerCase().includes(s) ||
|
|
97
|
+
opt.value.toLowerCase().includes(s) ||
|
|
98
|
+
(opt.description?.includes(s) ?? false)
|
|
99
|
+
);
|
|
100
|
+
}}
|
|
101
|
+
// Popover width follows the trigger by default (28px → narrow).
|
|
102
|
+
// Force a usable width and bump z-index above ChatDock (z-10000).
|
|
103
|
+
contentClassName="w-[280px]"
|
|
104
|
+
contentStyle={{ zIndex: 10001 }}
|
|
105
|
+
// Custom row: country flag + native language label + BCP-47 tag.
|
|
106
|
+
// ui-core Combobox default rendering only shows label/description
|
|
107
|
+
// — feeding the flag here keeps every list row instantly
|
|
108
|
+
// identifiable. Fallback to a neutral globe glyph when the tag
|
|
109
|
+
// has no resolvable country (rare: bn-BD etc. all have flags).
|
|
110
|
+
renderOption={(option) => {
|
|
111
|
+
const country = countryFromTag(option.value);
|
|
112
|
+
return (
|
|
113
|
+
<div className="flex min-w-0 flex-1 items-center gap-2">
|
|
114
|
+
{country ? (
|
|
115
|
+
<Flag
|
|
116
|
+
countryCode={country}
|
|
117
|
+
className="h-4 w-5 shrink-0 overflow-hidden rounded-[2px] border border-border/60 ring-1 ring-black/5"
|
|
118
|
+
/>
|
|
119
|
+
) : (
|
|
120
|
+
<Globe className="h-4 w-4 shrink-0 text-muted-foreground" aria-hidden />
|
|
121
|
+
)}
|
|
122
|
+
{/* Native-script label only — BCP-47 subtitle removed to
|
|
123
|
+
keep the dropdown visually quiet. Tag + region still
|
|
124
|
+
live in `description` for search. */}
|
|
125
|
+
<span className="truncate text-sm">{option.label}</span>
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
}}
|
|
129
|
+
// Compact icon-only trigger — replaces the default wide outline
|
|
130
|
+
// button. Stays the same 28×28 footprint as ChatHeaderActionButton
|
|
131
|
+
// siblings (audio toggle, reset).
|
|
132
|
+
renderTrigger={(selected, open) => {
|
|
133
|
+
const tag = selected?.value ?? active;
|
|
134
|
+
const country = countryFromTag(tag);
|
|
135
|
+
const found = findSpeechLanguage(tag);
|
|
136
|
+
// Tooltip copy: native language name + dialect region + tag.
|
|
137
|
+
// Falls back to just the tag for unknown / custom-engine codes.
|
|
138
|
+
const tooltipLabel = found
|
|
139
|
+
? `${found.language.name}${
|
|
140
|
+
found.language.dialects.length > 1 ? ` — ${found.dialect.region}` : ''
|
|
141
|
+
} · ${tag}`
|
|
142
|
+
: tag;
|
|
143
|
+
// Note on tooltip: we intentionally use the native `title`
|
|
144
|
+
// attribute instead of `<Tooltip>` here. The button is already
|
|
145
|
+
// wrapped by Combobox's `PopoverTrigger asChild`, and stacking
|
|
146
|
+
// a second `TooltipTrigger asChild` on the same node makes
|
|
147
|
+
// Radix Slot merge two competing refs/handlers — popover click
|
|
148
|
+
// stops working. Native `title` is "good enough" for a single
|
|
149
|
+
// status label and ships zero extra DOM.
|
|
150
|
+
return (
|
|
151
|
+
<button
|
|
152
|
+
type="button"
|
|
153
|
+
aria-label={`${ariaLabel}: ${tooltipLabel}`}
|
|
154
|
+
aria-expanded={open}
|
|
155
|
+
title={tooltipLabel}
|
|
156
|
+
className={cn(
|
|
157
|
+
'inline-flex h-7 w-7 items-center justify-center rounded-md',
|
|
158
|
+
'text-muted-foreground transition-colors',
|
|
159
|
+
'hover:bg-accent hover:text-foreground',
|
|
160
|
+
'focus:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
161
|
+
className,
|
|
162
|
+
)}
|
|
163
|
+
>
|
|
164
|
+
{country ? (
|
|
165
|
+
<Flag
|
|
166
|
+
countryCode={country}
|
|
167
|
+
// Subtle hairline so flags with light edges (Japan,
|
|
168
|
+
// Switzerland, …) don't blend into the header bg.
|
|
169
|
+
className="h-4 w-5 overflow-hidden rounded-[2px] border border-border/60 ring-1 ring-black/5"
|
|
170
|
+
/>
|
|
171
|
+
) : hideFallbackIcon ? null : (
|
|
172
|
+
<Globe className="h-3.5 w-3.5" aria-hidden />
|
|
173
|
+
)}
|
|
174
|
+
</button>
|
|
175
|
+
);
|
|
176
|
+
}}
|
|
177
|
+
/>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { PanelRightOpen, PanelRightClose } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
import { useIsTabletOrBelow } from '@djangocfg/ui-core/hooks';
|
|
6
|
+
|
|
7
|
+
import { ChatHeaderActionButton } from './ChatHeaderActionButton';
|
|
8
|
+
import type { ChatDockMode } from './ChatDock';
|
|
9
|
+
|
|
10
|
+
export interface ChatHeaderModeToggleProps {
|
|
11
|
+
/** Current dock mode. */
|
|
12
|
+
mode: ChatDockMode;
|
|
13
|
+
/** Toggle handler. Wire to `useChatDockPrefs().toggleMode`. */
|
|
14
|
+
onToggle: () => void;
|
|
15
|
+
/** Override aria/tooltip label for popover→side. */
|
|
16
|
+
expandLabel?: string;
|
|
17
|
+
/** Override aria/tooltip label for side→popover. */
|
|
18
|
+
collapseLabel?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Always render — useful for stories. By default the toggle hides itself on
|
|
21
|
+
* viewports below `lg` (1024px) since side mode falls back to popover there.
|
|
22
|
+
*/
|
|
23
|
+
forceVisible?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* "Dock to side" / "back to popover" toggle.
|
|
28
|
+
*
|
|
29
|
+
* Side mode is desktop-only — on viewports below `lg` (1024px) `<ChatDock>`
|
|
30
|
+
* silently falls back to popover anyway, so the toggle has nothing to do
|
|
31
|
+
* and auto-hides. Pass `forceVisible` to override (e.g. in stories).
|
|
32
|
+
*/
|
|
33
|
+
export function ChatHeaderModeToggle({
|
|
34
|
+
mode,
|
|
35
|
+
onToggle,
|
|
36
|
+
expandLabel = 'Dock to side',
|
|
37
|
+
collapseLabel = 'Back to popover',
|
|
38
|
+
forceVisible = false,
|
|
39
|
+
}: ChatHeaderModeToggleProps) {
|
|
40
|
+
const isBelowDesktop = useIsTabletOrBelow();
|
|
41
|
+
if (isBelowDesktop && !forceVisible) return null;
|
|
42
|
+
|
|
43
|
+
const isSide = mode === 'side';
|
|
44
|
+
return (
|
|
45
|
+
<ChatHeaderActionButton
|
|
46
|
+
icon={
|
|
47
|
+
isSide ? (
|
|
48
|
+
<PanelRightClose className="h-3.5 w-3.5" />
|
|
49
|
+
) : (
|
|
50
|
+
<PanelRightOpen className="h-3.5 w-3.5" />
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
ariaLabel={isSide ? collapseLabel : expandLabel}
|
|
54
|
+
onClick={onToggle}
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { RotateCcw } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
import { useChatReset } from '../hooks/useChatReset';
|
|
6
|
+
import { ChatHeaderActionButton } from './ChatHeaderActionButton';
|
|
7
|
+
|
|
8
|
+
export interface ChatHeaderResetButtonProps {
|
|
9
|
+
/**
|
|
10
|
+
* Backend reset call. Should resolve to `true` on success.
|
|
11
|
+
* Plugged into `useChatReset` for in-flight state.
|
|
12
|
+
*/
|
|
13
|
+
onReset: () => Promise<boolean>;
|
|
14
|
+
/** Called after a successful reset (e.g. clear local messages, refetch). */
|
|
15
|
+
onSuccess?: () => void;
|
|
16
|
+
/** Called when reset fails (returned `false` or threw). */
|
|
17
|
+
onError?: (error?: unknown) => void;
|
|
18
|
+
/**
|
|
19
|
+
* Show a `window.dialog.confirm` before calling `onReset`. @default true
|
|
20
|
+
*
|
|
21
|
+
* Requires the host to mount `<DialogProvider>` from `@djangocfg/ui-core`
|
|
22
|
+
* — it installs the `window.dialog` API used here.
|
|
23
|
+
*/
|
|
24
|
+
confirm?: boolean;
|
|
25
|
+
/** Confirm dialog title. */
|
|
26
|
+
confirmTitle?: string;
|
|
27
|
+
/** Confirm dialog message. */
|
|
28
|
+
confirmMessage?: string;
|
|
29
|
+
/** Override tooltip / aria label. */
|
|
30
|
+
ariaLabel?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const DEFAULT_TITLE = 'Clear conversation?';
|
|
34
|
+
const DEFAULT_MESSAGE =
|
|
35
|
+
'The assistant will forget this session and start a new one. This cannot be undone.';
|
|
36
|
+
const DEFAULT_LABEL = 'Clear conversation';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Standard chat-reset action: prompts the user via `window.dialog.confirm`,
|
|
40
|
+
* then runs the backend reset call through `useChatReset` so the button
|
|
41
|
+
* spins while it's in flight.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```tsx
|
|
45
|
+
* <ChatHeaderResetButton
|
|
46
|
+
* onReset={api.clearChat}
|
|
47
|
+
* onSuccess={() => chat.clearMessages()}
|
|
48
|
+
* />
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export function ChatHeaderResetButton({
|
|
52
|
+
onReset,
|
|
53
|
+
onSuccess,
|
|
54
|
+
onError,
|
|
55
|
+
confirm = true,
|
|
56
|
+
confirmTitle = DEFAULT_TITLE,
|
|
57
|
+
confirmMessage = DEFAULT_MESSAGE,
|
|
58
|
+
ariaLabel = DEFAULT_LABEL,
|
|
59
|
+
}: ChatHeaderResetButtonProps) {
|
|
60
|
+
const { reset, isResetting } = useChatReset({ onReset, onSuccess, onError });
|
|
61
|
+
|
|
62
|
+
const handleClick = async () => {
|
|
63
|
+
if (confirm) {
|
|
64
|
+
const api = typeof window !== 'undefined' ? window.dialog : undefined;
|
|
65
|
+
if (api?.confirm) {
|
|
66
|
+
const ok = await api.confirm({
|
|
67
|
+
title: confirmTitle,
|
|
68
|
+
message: confirmMessage,
|
|
69
|
+
variant: 'destructive',
|
|
70
|
+
confirmText: 'Clear',
|
|
71
|
+
cancelText: 'Cancel',
|
|
72
|
+
});
|
|
73
|
+
if (!ok) return;
|
|
74
|
+
} else if (typeof window !== 'undefined' && typeof window.confirm === 'function') {
|
|
75
|
+
// Fallback to the native browser confirm when the dialog service
|
|
76
|
+
// isn't wired (e.g. host forgot to mount DialogProvider).
|
|
77
|
+
const ok = window.confirm(`${confirmTitle}\n\n${confirmMessage}`);
|
|
78
|
+
if (!ok) return;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
await reset();
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<ChatHeaderActionButton
|
|
86
|
+
icon={<RotateCcw className="h-3.5 w-3.5" />}
|
|
87
|
+
ariaLabel={ariaLabel}
|
|
88
|
+
onClick={handleClick}
|
|
89
|
+
loading={isResetting}
|
|
90
|
+
destructive
|
|
91
|
+
/>
|
|
92
|
+
);
|
|
93
|
+
}
|