@djangocfg/ui-tools 2.1.409 → 2.1.411
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/package.json +13 -13
- package/src/{tools/Chat/highlight → lib/browser-bridge}/README.md +46 -18
- package/src/lib/browser-bridge/commands/chat.ts +42 -0
- package/src/lib/browser-bridge/commands/highlight.ts +70 -0
- package/src/lib/browser-bridge/commands/index.ts +15 -0
- package/src/lib/browser-bridge/commands/inspect.ts +31 -0
- package/src/lib/browser-bridge/commands/scroll.ts +31 -0
- package/src/lib/browser-bridge/commands/write.ts +45 -0
- package/src/lib/browser-bridge/directive-bus.ts +120 -0
- package/src/lib/browser-bridge/index.ts +56 -0
- package/src/lib/browser-bridge/logger.ts +27 -0
- package/src/{tools/Chat/highlight → lib/browser-bridge/overlay}/HighlightOverlay.tsx +14 -0
- package/src/{tools/Chat/highlight → lib/browser-bridge/overlay}/__tests__/HighlightOverlay.test.tsx +52 -0
- package/src/{tools/Chat/highlight → lib/browser-bridge/overlay}/__tests__/resolveRef.test.ts +39 -0
- package/src/{tools/Chat/highlight → lib/browser-bridge/overlay}/index.ts +8 -5
- package/src/{tools/Chat/highlight → lib/browser-bridge/overlay}/resolveRef.ts +5 -0
- package/src/{tools/Chat/highlight → lib/browser-bridge/overlay}/useHighlightTargets.ts +58 -27
- package/src/lib/browser-bridge/overlay/waitForVisible.ts +70 -0
- package/src/lib/browser-bridge/registry.ts +41 -0
- package/src/lib/browser-bridge/setBridgeResolver.ts +42 -0
- package/src/lib/browser-bridge/window.ts +76 -0
- package/src/lib/page-snapshot/capture/walk.ts +13 -5
- package/src/lib/page-snapshot/engine.ts +9 -4
- package/src/lib/page-snapshot/index.ts +5 -0
- package/src/lib/page-snapshot/react/provider.tsx +70 -3
- package/src/lib/page-snapshot/react/use-page-snapshot.ts +10 -0
- package/src/lib/page-snapshot/refs/__tests__/locator.test.ts +94 -0
- package/src/lib/page-snapshot/refs/__tests__/registry.test.ts +59 -3
- package/src/lib/page-snapshot/refs/locator.ts +218 -0
- package/src/lib/page-snapshot/refs/registry.ts +29 -14
- package/src/tools/Chat/README.md +1 -1
- package/src/tools/Chat/constants.ts +24 -1
- package/src/tools/Chat/context/ChatProvider.tsx +17 -2
- package/src/tools/Chat/core/logger.ts +15 -2
- package/src/tools/Chat/index.ts +34 -2
- package/src/tools/Chat/launcher/ChatDock.tsx +13 -3
- package/src/tools/Chat/launcher/ChatFAB.tsx +4 -2
- package/src/tools/Chat/launcher/ChatGreeting.tsx +3 -2
- package/src/tools/Chat/launcher/ChatLauncher.tsx +42 -7
- package/src/tools/Chat/launcher/ChatUnreadPreview.tsx +3 -2
- package/src/tools/Chat/launcher/header/ChatHeader.tsx +2 -0
- package/src/tools/Chat/launcher/header/ChatHeaderActionButton.tsx +2 -0
- package/src/tools/Chat/launcher/header/ChatHeaderLanguageButton.tsx +2 -2
- package/src/tools/Chat/launcher/header/HeaderSlots.tsx +16 -9
- package/src/tools/Chat/lazy.tsx +34 -2
- package/src/tools/Chat/public.ts +16 -0
- package/src/tools/Chat/settings/README.md +87 -0
- package/src/tools/Chat/settings/__tests__/useChatSettings.test.tsx +84 -0
- package/src/tools/Chat/settings/__tests__/useLocalStorage.test.tsx +138 -0
- package/src/tools/Chat/settings/index.ts +23 -0
- package/src/tools/Chat/settings/types.ts +108 -0
- package/src/tools/Chat/settings/useChatSettings.ts +168 -0
- /package/src/{tools/Chat/highlight → lib/browser-bridge/overlay}/SpotlightCanvas.tsx +0 -0
- /package/src/{tools/Chat/highlight → lib/browser-bridge/overlay}/types.ts +0 -0
|
@@ -15,6 +15,7 @@ import type { ChatConfig, ChatLabels, ChatTransport } from '../types';
|
|
|
15
15
|
import { DEFAULT_LABELS } from '../types';
|
|
16
16
|
import type { BlockRegistry } from '../messages/blocks';
|
|
17
17
|
|
|
18
|
+
import { setBridgeSender } from '../../../lib/browser-bridge';
|
|
18
19
|
import { useChat, type UseChatReturn } from '../hooks/useChat';
|
|
19
20
|
import { useChatLayout, type UseChatLayoutReturn } from '../hooks/useChatLayout';
|
|
20
21
|
import { useChatAudio } from '../hooks/useChatAudio';
|
|
@@ -212,14 +213,28 @@ export function ChatProvider({
|
|
|
212
213
|
};
|
|
213
214
|
}, [audioApi]);
|
|
214
215
|
|
|
216
|
+
// Publish `sendMessage` to the browser bridge so the dev-only
|
|
217
|
+
// `window.__chatBridge.sendMessage(...)` can drive the chat from the
|
|
218
|
+
// console. `setBridgeSender` no-ops outside dev; cleared on unmount.
|
|
219
|
+
useEffect(() => {
|
|
220
|
+
setBridgeSender(chat.sendMessage);
|
|
221
|
+
return () => setBridgeSender(null);
|
|
222
|
+
}, [chat.sendMessage]);
|
|
223
|
+
|
|
215
224
|
const labels = useMemo<ChatLabels>(
|
|
216
225
|
() => ({ ...DEFAULT_LABELS, ...(config.labels ?? {}) }),
|
|
217
226
|
[config.labels],
|
|
218
227
|
);
|
|
219
228
|
|
|
229
|
+
// True when audio is wired up — drives the auto-injected mute toggle.
|
|
230
|
+
// `audio={{}}` counts: `useChatAudio` falls back to the bundled
|
|
231
|
+
// DEFAULT_CHAT_SOUNDS, so an empty config still has sounds to mute.
|
|
232
|
+
// Only an absent `audio` prop or `silenced` means "nothing to mute".
|
|
220
233
|
const hasAudio = useMemo<boolean>(() => {
|
|
221
|
-
|
|
222
|
-
if (
|
|
234
|
+
if (!audio) return false;
|
|
235
|
+
if (audio.silenced) return false;
|
|
236
|
+
const sounds = audio.sounds;
|
|
237
|
+
if (sounds === undefined) return true;
|
|
223
238
|
return Object.values(sounds).some(
|
|
224
239
|
(v) => typeof v === 'string' && v.length > 0,
|
|
225
240
|
);
|
|
@@ -22,7 +22,13 @@ import { consola, type ConsolaInstance } from 'consola';
|
|
|
22
22
|
|
|
23
23
|
import { isDev } from '@djangocfg/ui-core/lib';
|
|
24
24
|
|
|
25
|
-
export type ChatLogScope =
|
|
25
|
+
export type ChatLogScope =
|
|
26
|
+
| 'bootstrap'
|
|
27
|
+
| 'transport'
|
|
28
|
+
| 'stream'
|
|
29
|
+
| 'lifecycle'
|
|
30
|
+
| 'tools'
|
|
31
|
+
| 'error';
|
|
26
32
|
|
|
27
33
|
export interface ChatLogger {
|
|
28
34
|
bootstrap: ConsolaInstance;
|
|
@@ -35,7 +41,14 @@ export interface ChatLogger {
|
|
|
35
41
|
enabled: boolean;
|
|
36
42
|
}
|
|
37
43
|
|
|
38
|
-
const SCOPES: ChatLogScope[] = [
|
|
44
|
+
const SCOPES: ChatLogScope[] = [
|
|
45
|
+
'bootstrap',
|
|
46
|
+
'transport',
|
|
47
|
+
'stream',
|
|
48
|
+
'lifecycle',
|
|
49
|
+
'tools',
|
|
50
|
+
'error',
|
|
51
|
+
];
|
|
39
52
|
|
|
40
53
|
/** Module-level cache so all hooks/components share the same logger instance per `enabled` mode. */
|
|
41
54
|
const cache = new Map<boolean, ChatLogger>();
|
package/src/tools/Chat/index.ts
CHANGED
|
@@ -18,12 +18,24 @@ export * from './messages';
|
|
|
18
18
|
export * from './composer';
|
|
19
19
|
export * from './shell';
|
|
20
20
|
|
|
21
|
-
//
|
|
21
|
+
// Bridge — AI-driven `point` directives (spotlight + focus overlay).
|
|
22
22
|
export {
|
|
23
23
|
HighlightOverlay,
|
|
24
24
|
SpotlightCanvas,
|
|
25
25
|
useHighlightTargets,
|
|
26
26
|
resolveRefs,
|
|
27
|
+
useChatDirectives,
|
|
28
|
+
pushDirectives,
|
|
29
|
+
clearDirectives,
|
|
30
|
+
parseDirectives,
|
|
31
|
+
installChatBridge,
|
|
32
|
+
registerBridgeCommand,
|
|
33
|
+
getBridgeCommand,
|
|
34
|
+
setBridgeResolver,
|
|
35
|
+
setBridgeSender,
|
|
36
|
+
type BridgeCommand,
|
|
37
|
+
type BridgeSender,
|
|
38
|
+
type ChatBridge,
|
|
27
39
|
type HighlightOverlayProps,
|
|
28
40
|
type SpotlightCanvasProps,
|
|
29
41
|
type RefResolver,
|
|
@@ -31,7 +43,27 @@ export {
|
|
|
31
43
|
type HighlightTarget,
|
|
32
44
|
type SpotlightRect,
|
|
33
45
|
type CSTRefId,
|
|
34
|
-
} from '
|
|
46
|
+
} from '../../lib/browser-bridge';
|
|
47
|
+
|
|
48
|
+
// Page-context snapshot — capture engine + React surface. The host
|
|
49
|
+
// wires `getChatMetadata` into `getDynamicMetadata` so the assistant
|
|
50
|
+
// receives a snapshot of the page the user is looking at.
|
|
51
|
+
export {
|
|
52
|
+
PageSnapshotProvider,
|
|
53
|
+
usePageSnapshot,
|
|
54
|
+
usePageSnapshotToggle,
|
|
55
|
+
PageSnapshotChip,
|
|
56
|
+
PageSnapshotPreview,
|
|
57
|
+
PageSnapshotEngine,
|
|
58
|
+
type PageSnapshotProviderProps,
|
|
59
|
+
type PageSnapshotContextValue,
|
|
60
|
+
type PageSnapshotToggle,
|
|
61
|
+
type PageSnapshotChipProps,
|
|
62
|
+
type PageSnapshotPreviewProps,
|
|
63
|
+
type CaptureEngineOptions,
|
|
64
|
+
type CaptureResult,
|
|
65
|
+
type PageContextPayload,
|
|
66
|
+
} from '../../lib/page-snapshot';
|
|
35
67
|
|
|
36
68
|
// Launcher — FAB + Dock + Header + Greeting composition (heavy, sync).
|
|
37
69
|
export {
|
|
@@ -7,6 +7,7 @@ import { Portal } from '@djangocfg/ui-core/components';
|
|
|
7
7
|
import { useIsMobile, useIsTabletOrBelow } from '@djangocfg/ui-core/hooks';
|
|
8
8
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
9
9
|
|
|
10
|
+
import { Z_INDEX } from '../constants';
|
|
10
11
|
import { ChatHeader } from './header';
|
|
11
12
|
import { useChatPresence } from './useChatPresence';
|
|
12
13
|
import type { ChatFABPosition } from './ChatFAB';
|
|
@@ -52,7 +53,11 @@ export interface ChatDockProps {
|
|
|
52
53
|
offset?: { horizontal?: number; vertical?: number };
|
|
53
54
|
/** Transition duration in ms — should match CSS animation. @default 200 */
|
|
54
55
|
exitDurationMs?: number;
|
|
55
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* z-index. @default 100 — page furniture: above page content but below
|
|
58
|
+
* every ui-core overlay (sheet/drawer/dialog/popover), so a dialog always
|
|
59
|
+
* covers the chat, including one opened from inside the chat itself.
|
|
60
|
+
*/
|
|
56
61
|
zIndex?: number;
|
|
57
62
|
/** Accessible dialog label. */
|
|
58
63
|
ariaLabel?: string;
|
|
@@ -118,7 +123,7 @@ export function ChatDock({
|
|
|
118
123
|
position = 'bottom-right',
|
|
119
124
|
offset,
|
|
120
125
|
exitDurationMs = 200,
|
|
121
|
-
zIndex =
|
|
126
|
+
zIndex = Z_INDEX.dock,
|
|
122
127
|
ariaLabel,
|
|
123
128
|
className,
|
|
124
129
|
mobileFullscreen = true,
|
|
@@ -236,7 +241,12 @@ export function ChatDock({
|
|
|
236
241
|
<div
|
|
237
242
|
role="dialog"
|
|
238
243
|
aria-label={ariaLabel ?? (typeof title === 'string' ? title : 'Chat')}
|
|
239
|
-
aria-hidden
|
|
244
|
+
// While leaving, mark the panel `inert` rather than `aria-hidden`.
|
|
245
|
+
// `aria-hidden` on an ancestor of the still-focused element (e.g.
|
|
246
|
+
// the close button just clicked, or the composer) trips the
|
|
247
|
+
// browser's "Blocked aria-hidden ... descendant retained focus"
|
|
248
|
+
// warning. `inert` hides it from the a11y tree AND blanks focus.
|
|
249
|
+
inert={phase === 'leaving' ? true : undefined}
|
|
240
250
|
className={cn(
|
|
241
251
|
'bg-popover text-popover-foreground border-border',
|
|
242
252
|
'flex flex-col overflow-hidden shadow-2xl',
|
|
@@ -6,6 +6,8 @@ import type { CSSProperties, ReactNode } from 'react';
|
|
|
6
6
|
import { useIsPhone, useIsTabletOrBelow } from '@djangocfg/ui-core/hooks';
|
|
7
7
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
8
8
|
|
|
9
|
+
import { Z_INDEX } from '../constants';
|
|
10
|
+
|
|
9
11
|
export type ChatFABPosition =
|
|
10
12
|
| 'bottom-right'
|
|
11
13
|
| 'bottom-left'
|
|
@@ -30,7 +32,7 @@ export interface ChatFABProps {
|
|
|
30
32
|
position?: ChatFABPosition;
|
|
31
33
|
/** Pixel offset from screen edges. @default 24 */
|
|
32
34
|
offset?: number;
|
|
33
|
-
/** z-index for the button. @default
|
|
35
|
+
/** z-index for the button. @default 99 — just below the open dock. */
|
|
34
36
|
zIndex?: number;
|
|
35
37
|
/** Show a small attention dot (unread / new). */
|
|
36
38
|
pulse?: boolean;
|
|
@@ -145,7 +147,7 @@ export function ChatFAB({
|
|
|
145
147
|
size = 'responsive',
|
|
146
148
|
position = 'bottom-right',
|
|
147
149
|
offset = 24,
|
|
148
|
-
zIndex =
|
|
150
|
+
zIndex = Z_INDEX.companion,
|
|
149
151
|
pulse = false,
|
|
150
152
|
badge,
|
|
151
153
|
tooltip,
|
|
@@ -6,6 +6,7 @@ import type { CSSProperties, ReactNode } from 'react';
|
|
|
6
6
|
|
|
7
7
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
8
8
|
|
|
9
|
+
import { Z_INDEX } from '../constants';
|
|
9
10
|
import { useChatPresence } from './useChatPresence';
|
|
10
11
|
import type { ChatFABPosition } from './ChatFAB';
|
|
11
12
|
|
|
@@ -32,7 +33,7 @@ export interface ChatGreetingProps {
|
|
|
32
33
|
fabClearance?: number;
|
|
33
34
|
/** Delay before the greeting appears, in ms. @default 1500 */
|
|
34
35
|
delayMs?: number;
|
|
35
|
-
/** z-index. @default
|
|
36
|
+
/** z-index. @default 99 — companion tier, just below the open dock. */
|
|
36
37
|
zIndex?: number;
|
|
37
38
|
/** Override classes on the bubble. */
|
|
38
39
|
className?: string;
|
|
@@ -110,7 +111,7 @@ export function ChatGreeting({
|
|
|
110
111
|
fabOffset = 24,
|
|
111
112
|
fabClearance = 96,
|
|
112
113
|
delayMs = 1500,
|
|
113
|
-
zIndex =
|
|
114
|
+
zIndex = Z_INDEX.companion,
|
|
114
115
|
className,
|
|
115
116
|
style,
|
|
116
117
|
avatar,
|
|
@@ -7,6 +7,7 @@ import { useHotkey } from '@djangocfg/ui-core/hooks';
|
|
|
7
7
|
|
|
8
8
|
import { ChatProvider, useChatContextOptional } from '../context';
|
|
9
9
|
import type { ChatAudioConfig } from '../core/audio/types';
|
|
10
|
+
import { useChatDockPrefs } from '../hooks/useChatDockPrefs';
|
|
10
11
|
import type {
|
|
11
12
|
ChatConfig,
|
|
12
13
|
ChatMessage,
|
|
@@ -275,10 +276,21 @@ export function ChatLauncher({
|
|
|
275
276
|
? { ...fab, badge: 1 }
|
|
276
277
|
: fab;
|
|
277
278
|
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
|
|
279
|
+
// Reused below for the ambient-provider branch; read up here so
|
|
280
|
+
// `audioConfigured` can fall back to the ambient provider's audio.
|
|
281
|
+
const ambient = useChatContextOptional();
|
|
282
|
+
|
|
283
|
+
// Whether audio wires up any actual sound. Used as the default for
|
|
284
|
+
// `headerSlots.audio` — no point auto-injecting the toggle when
|
|
285
|
+
// there's nothing to mute.
|
|
286
|
+
//
|
|
287
|
+
// When the launcher is rendered inside an existing `<ChatProvider>`
|
|
288
|
+
// (the host owns the provider), audio was configured there, not via
|
|
289
|
+
// this component's `audio` prop — so honor the ambient provider's
|
|
290
|
+
// `hasAudio` too. Without this the dock-header mute toggle silently
|
|
291
|
+
// never appears for host-owned-provider setups.
|
|
281
292
|
const audioConfigured = useMemo<boolean>(() => {
|
|
293
|
+
if (ambient?.hasAudio) return true;
|
|
282
294
|
if (!audio) return false;
|
|
283
295
|
if (audio.silenced) return false;
|
|
284
296
|
const sounds = audio.sounds;
|
|
@@ -288,13 +300,25 @@ export function ChatLauncher({
|
|
|
288
300
|
return Object.values(sounds).some(
|
|
289
301
|
(v) => typeof v === 'string' && v.length > 0,
|
|
290
302
|
);
|
|
291
|
-
}, [audio]);
|
|
303
|
+
}, [audio, ambient?.hasAudio]);
|
|
292
304
|
|
|
293
305
|
const resolvedSlots = useMemo(
|
|
294
306
|
() => resolveHeaderSlots(headerSlots, audioConfigured),
|
|
295
307
|
[headerSlots, audioConfigured],
|
|
296
308
|
);
|
|
297
309
|
|
|
310
|
+
// Single source of truth for dock layout prefs. The mode-toggle slot
|
|
311
|
+
// and `<ChatDock>` live in separate subtrees — if each called
|
|
312
|
+
// `useChatDockPrefs` independently they'd hold isolated `useState`
|
|
313
|
+
// (no same-tab cross-instance sync), so the toggle would flip
|
|
314
|
+
// localStorage but never re-render the dock. Owning the hook here and
|
|
315
|
+
// threading both the values and the toggle down keeps them in sync.
|
|
316
|
+
const modeToggleSlot = resolvedSlots.modeToggle;
|
|
317
|
+
const dockPrefs = useChatDockPrefs({
|
|
318
|
+
storageKey: modeToggleSlot?.persistAs,
|
|
319
|
+
defaults: modeToggleSlot?.defaults,
|
|
320
|
+
});
|
|
321
|
+
|
|
298
322
|
const hasAnySlot =
|
|
299
323
|
resolvedSlots.audio ||
|
|
300
324
|
resolvedSlots.modeToggle !== null ||
|
|
@@ -302,8 +326,6 @@ export function ChatLauncher({
|
|
|
302
326
|
resolvedSlots.reset !== null ||
|
|
303
327
|
resolvedSlots.custom !== null;
|
|
304
328
|
|
|
305
|
-
const ambient = useChatContextOptional();
|
|
306
|
-
|
|
307
329
|
const body = (
|
|
308
330
|
<>
|
|
309
331
|
<ChatFAB {...resolvedFab} onClick={toggleOpen} />
|
|
@@ -331,10 +353,23 @@ export function ChatLauncher({
|
|
|
331
353
|
) : null}
|
|
332
354
|
<ChatDock
|
|
333
355
|
{...dock}
|
|
356
|
+
// When the mode-toggle slot is active, dock prefs are the source
|
|
357
|
+
// of truth so the header toggle actually re-docks the panel.
|
|
358
|
+
// Otherwise fall back to the static `dock` props. `sideWidth`
|
|
359
|
+
// only governs side mode — leave popover width to `dock.width`.
|
|
360
|
+
mode={modeToggleSlot ? dockPrefs.mode : dock?.mode}
|
|
361
|
+
side={modeToggleSlot ? dockPrefs.side : dock?.side}
|
|
362
|
+
width={
|
|
363
|
+
modeToggleSlot && dockPrefs.mode === 'side'
|
|
364
|
+
? dockPrefs.sideWidth
|
|
365
|
+
: dock?.width
|
|
366
|
+
}
|
|
334
367
|
open={open}
|
|
335
368
|
onClose={() => setOpen(false)}
|
|
336
369
|
headerActions={
|
|
337
|
-
hasAnySlot ?
|
|
370
|
+
hasAnySlot ? (
|
|
371
|
+
<HeaderSlotsRenderer slots={resolvedSlots} dockPrefs={dockPrefs} />
|
|
372
|
+
) : undefined
|
|
338
373
|
}
|
|
339
374
|
>
|
|
340
375
|
<div ref={dockContentRef} className="flex h-full min-h-0 min-w-0 flex-col">
|
|
@@ -6,6 +6,7 @@ import type { CSSProperties, ReactNode } from 'react';
|
|
|
6
6
|
import { Avatar, AvatarFallback, AvatarImage } from '@djangocfg/ui-core/components';
|
|
7
7
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
8
8
|
|
|
9
|
+
import { Z_INDEX } from '../constants';
|
|
9
10
|
import type { ChatMessage, ChatPersona } from '../types';
|
|
10
11
|
import type { ChatFABPosition } from './ChatFAB';
|
|
11
12
|
import { useChatPresence } from './useChatPresence';
|
|
@@ -27,7 +28,7 @@ export interface ChatUnreadPreviewProps {
|
|
|
27
28
|
fabClearance?: number;
|
|
28
29
|
/** Lines of body text before ellipsis. @default 2 */
|
|
29
30
|
truncate?: number;
|
|
30
|
-
/** z-index. @default
|
|
31
|
+
/** z-index. @default 99 — companion tier, just below the open dock. */
|
|
31
32
|
zIndex?: number;
|
|
32
33
|
/** Render in-place (stories / previews). @default false */
|
|
33
34
|
inline?: boolean;
|
|
@@ -100,7 +101,7 @@ export function ChatUnreadPreview({
|
|
|
100
101
|
fabOffset = 24,
|
|
101
102
|
fabClearance = 96,
|
|
102
103
|
truncate = 2,
|
|
103
|
-
zIndex =
|
|
104
|
+
zIndex = Z_INDEX.companion,
|
|
104
105
|
inline = false,
|
|
105
106
|
className,
|
|
106
107
|
style,
|
|
@@ -79,6 +79,8 @@ export function ChatHeader({
|
|
|
79
79
|
<X className="h-4 w-4" />
|
|
80
80
|
</Button>
|
|
81
81
|
</TooltipTrigger>
|
|
82
|
+
{/* Tooltip portals to <body> in ui-core's anchored-overlay
|
|
83
|
+
tier, already above the dock — no override needed. */}
|
|
82
84
|
<TooltipContent side="bottom">{closeLabel}</TooltipContent>
|
|
83
85
|
</Tooltip>
|
|
84
86
|
</TooltipProvider>
|
|
@@ -110,6 +110,8 @@ export const ChatHeaderActionButton = forwardRef<HTMLButtonElement, ChatHeaderAc
|
|
|
110
110
|
<TooltipProvider delayDuration={300}>
|
|
111
111
|
<Tooltip>
|
|
112
112
|
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
|
113
|
+
{/* Tooltip portals to <body> in ui-core's anchored-overlay tier,
|
|
114
|
+
already above the dock — no z-index override needed. */}
|
|
113
115
|
<TooltipContent side="bottom">{tooltipLabel}</TooltipContent>
|
|
114
116
|
</Tooltip>
|
|
115
117
|
</TooltipProvider>
|
|
@@ -99,9 +99,9 @@ export function ChatHeaderLanguageButton({
|
|
|
99
99
|
);
|
|
100
100
|
}}
|
|
101
101
|
// Popover width follows the trigger by default (28px → narrow).
|
|
102
|
-
// Force a usable width
|
|
102
|
+
// Force a usable width; the popover already portals into ui-core's
|
|
103
|
+
// anchored-overlay tier, above the dock — no z-index override needed.
|
|
103
104
|
contentClassName="w-[280px]"
|
|
104
|
-
contentStyle={{ zIndex: 10001 }}
|
|
105
105
|
// Custom row: country flag + native language label + BCP-47 tag.
|
|
106
106
|
// ui-core Combobox default rendering only shows label/description
|
|
107
107
|
// — feeding the flag here keeps every list row instantly
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { Fragment } from 'react';
|
|
4
4
|
|
|
5
5
|
import { useChatContext } from '../../context';
|
|
6
|
-
import {
|
|
6
|
+
import type { UseChatDockPrefsReturn } from '../../hooks/useChatDockPrefs';
|
|
7
7
|
|
|
8
8
|
import { ChatHeaderAudioToggle } from './ChatHeaderAudioToggle';
|
|
9
9
|
import { ChatHeaderLanguageButton } from './ChatHeaderLanguageButton';
|
|
@@ -13,6 +13,13 @@ import type { ResolvedChatHeaderSlots } from '../types';
|
|
|
13
13
|
|
|
14
14
|
export interface HeaderSlotsRendererProps {
|
|
15
15
|
slots: ResolvedChatHeaderSlots;
|
|
16
|
+
/**
|
|
17
|
+
* Shared dock-prefs instance owned by `<ChatLauncher>`. The mode
|
|
18
|
+
* toggle MUST use the same instance the dock reads from — a separate
|
|
19
|
+
* `useChatDockPrefs` call would hold isolated state and the toggle
|
|
20
|
+
* would no-op.
|
|
21
|
+
*/
|
|
22
|
+
dockPrefs: UseChatDockPrefsReturn;
|
|
16
23
|
}
|
|
17
24
|
|
|
18
25
|
/**
|
|
@@ -22,7 +29,7 @@ export interface HeaderSlotsRendererProps {
|
|
|
22
29
|
* Order (left → right, before the close icon):
|
|
23
30
|
* custom · languagePicker · modeToggle · audio · reset
|
|
24
31
|
*/
|
|
25
|
-
export function HeaderSlotsRenderer({ slots }: HeaderSlotsRendererProps) {
|
|
32
|
+
export function HeaderSlotsRenderer({ slots, dockPrefs }: HeaderSlotsRendererProps) {
|
|
26
33
|
const ctx = useChatContext();
|
|
27
34
|
return (
|
|
28
35
|
<>
|
|
@@ -34,7 +41,9 @@ export function HeaderSlotsRenderer({ slots }: HeaderSlotsRendererProps) {
|
|
|
34
41
|
hideFallbackIcon={slots.languagePicker.hideFallbackIcon}
|
|
35
42
|
/>
|
|
36
43
|
) : null}
|
|
37
|
-
{slots.modeToggle ?
|
|
44
|
+
{slots.modeToggle ? (
|
|
45
|
+
<ModeToggleSlot slot={slots.modeToggle} dockPrefs={dockPrefs} />
|
|
46
|
+
) : null}
|
|
38
47
|
{slots.audio && !ctx.audio.isSilent ? (
|
|
39
48
|
<ChatHeaderAudioToggle
|
|
40
49
|
muted={ctx.audio.muted}
|
|
@@ -48,17 +57,15 @@ export function HeaderSlotsRenderer({ slots }: HeaderSlotsRendererProps) {
|
|
|
48
57
|
|
|
49
58
|
function ModeToggleSlot({
|
|
50
59
|
slot,
|
|
60
|
+
dockPrefs,
|
|
51
61
|
}: {
|
|
52
62
|
slot: NonNullable<ResolvedChatHeaderSlots['modeToggle']>;
|
|
63
|
+
dockPrefs: UseChatDockPrefsReturn;
|
|
53
64
|
}) {
|
|
54
|
-
const prefs = useChatDockPrefs({
|
|
55
|
-
storageKey: slot.persistAs,
|
|
56
|
-
defaults: slot.defaults,
|
|
57
|
-
});
|
|
58
65
|
return (
|
|
59
66
|
<ChatHeaderModeToggle
|
|
60
|
-
mode={
|
|
61
|
-
onToggle={
|
|
67
|
+
mode={dockPrefs.mode}
|
|
68
|
+
onToggle={dockPrefs.toggleMode}
|
|
62
69
|
forceVisible={slot.forceVisible ?? true}
|
|
63
70
|
expandLabel={slot.expandLabel}
|
|
64
71
|
collapseLabel={slot.collapseLabel}
|
package/src/tools/Chat/lazy.tsx
CHANGED
|
@@ -75,12 +75,24 @@ export * from './composer';
|
|
|
75
75
|
export * from './shell';
|
|
76
76
|
export * from './launcher';
|
|
77
77
|
|
|
78
|
-
//
|
|
78
|
+
// Bridge — AI-driven `point` directives (lightweight, no heavy deps).
|
|
79
79
|
export {
|
|
80
80
|
HighlightOverlay,
|
|
81
81
|
SpotlightCanvas,
|
|
82
82
|
useHighlightTargets,
|
|
83
83
|
resolveRefs,
|
|
84
|
+
useChatDirectives,
|
|
85
|
+
pushDirectives,
|
|
86
|
+
clearDirectives,
|
|
87
|
+
parseDirectives,
|
|
88
|
+
installChatBridge,
|
|
89
|
+
registerBridgeCommand,
|
|
90
|
+
getBridgeCommand,
|
|
91
|
+
setBridgeResolver,
|
|
92
|
+
setBridgeSender,
|
|
93
|
+
type BridgeCommand,
|
|
94
|
+
type BridgeSender,
|
|
95
|
+
type ChatBridge,
|
|
84
96
|
type HighlightOverlayProps,
|
|
85
97
|
type SpotlightCanvasProps,
|
|
86
98
|
type RefResolver,
|
|
@@ -88,4 +100,24 @@ export {
|
|
|
88
100
|
type HighlightTarget,
|
|
89
101
|
type SpotlightRect,
|
|
90
102
|
type CSTRefId,
|
|
91
|
-
} from '
|
|
103
|
+
} from '../../lib/browser-bridge';
|
|
104
|
+
|
|
105
|
+
// Page-context snapshot — capture engine + React surface. The host
|
|
106
|
+
// wires `getChatMetadata` into `getDynamicMetadata` so the assistant
|
|
107
|
+
// receives a snapshot of the page the user is looking at.
|
|
108
|
+
export {
|
|
109
|
+
PageSnapshotProvider,
|
|
110
|
+
usePageSnapshot,
|
|
111
|
+
usePageSnapshotToggle,
|
|
112
|
+
PageSnapshotChip,
|
|
113
|
+
PageSnapshotPreview,
|
|
114
|
+
PageSnapshotEngine,
|
|
115
|
+
type PageSnapshotProviderProps,
|
|
116
|
+
type PageSnapshotContextValue,
|
|
117
|
+
type PageSnapshotToggle,
|
|
118
|
+
type PageSnapshotChipProps,
|
|
119
|
+
type PageSnapshotPreviewProps,
|
|
120
|
+
type CaptureEngineOptions,
|
|
121
|
+
type CaptureResult,
|
|
122
|
+
type PageContextPayload,
|
|
123
|
+
} from '../../lib/page-snapshot';
|
package/src/tools/Chat/public.ts
CHANGED
|
@@ -61,6 +61,7 @@ export { DEFAULT_LABELS } from './types';
|
|
|
61
61
|
export {
|
|
62
62
|
STORAGE_KEYS,
|
|
63
63
|
CSS_VARS,
|
|
64
|
+
Z_INDEX,
|
|
64
65
|
DEFAULT_Z_INDEX,
|
|
65
66
|
LIMITS,
|
|
66
67
|
DEFAULT_SIDEBAR,
|
|
@@ -151,6 +152,21 @@ export {
|
|
|
151
152
|
type UseChatAudioReturn,
|
|
152
153
|
} from './core/audio';
|
|
153
154
|
|
|
155
|
+
// Settings — the single, centralized home for chat-owned persisted
|
|
156
|
+
// settings. Every chat feature reads/writes its slice through this hook.
|
|
157
|
+
export {
|
|
158
|
+
useChatSettings,
|
|
159
|
+
CHAT_SETTINGS_STORAGE_KEY,
|
|
160
|
+
DEFAULT_CHAT_SETTINGS,
|
|
161
|
+
type ChatSettings,
|
|
162
|
+
type ChatDockSettings,
|
|
163
|
+
type ChatAudioSettings,
|
|
164
|
+
type ChatSpeechSettings,
|
|
165
|
+
type ChatPageContextSettings,
|
|
166
|
+
type UseChatSettingsOptions,
|
|
167
|
+
type UseChatSettingsReturn,
|
|
168
|
+
} from './settings';
|
|
169
|
+
|
|
154
170
|
// Notifier — title rotation + favicon badge + page-visibility + cross-tab
|
|
155
171
|
export {
|
|
156
172
|
createBrowserNotifier,
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Chat settings — `tools/Chat/settings/`
|
|
2
|
+
|
|
3
|
+
The single, centralized home for every **chat-owned persisted setting**.
|
|
4
|
+
|
|
5
|
+
## Why this exists
|
|
6
|
+
|
|
7
|
+
Chat features used to each call `useLocalStorage` (or zustand `persist`)
|
|
8
|
+
with their own ad-hoc storage key. That made it impossible to reason
|
|
9
|
+
about, export, or reset "the chat's settings" as one thing, and
|
|
10
|
+
scattered the SSR / migration / coalescing concerns across the codebase.
|
|
11
|
+
|
|
12
|
+
This module gives chat **one typed object, one storage key, one hook**:
|
|
13
|
+
|
|
14
|
+
- `ChatSettings` — the typed shape of all chat-owned settings.
|
|
15
|
+
- `useChatSettings()` — the only hook a chat feature should use to read or
|
|
16
|
+
write a persisted preference. Built on the improved `useLocalStorage`
|
|
17
|
+
from `@djangocfg/ui-core`, which gives us:
|
|
18
|
+
- **Coalesced writes** — several features patching different slices in
|
|
19
|
+
the same render commit collapse into one `localStorage` write.
|
|
20
|
+
- **Cross-instance / cross-tab sync** — two `useChatSettings()`
|
|
21
|
+
consumers stay in lockstep, same tab and across tabs.
|
|
22
|
+
- **Safe partial updates** — a slice updater that changes nothing is a
|
|
23
|
+
no-op (no write, no re-render).
|
|
24
|
+
|
|
25
|
+
## API
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
const {
|
|
29
|
+
settings, // full typed ChatSettings object
|
|
30
|
+
patch, // grouped patch over whole slices
|
|
31
|
+
updateDock, // updateDock({ side: 'left' })
|
|
32
|
+
updateAudio,
|
|
33
|
+
updateSpeech,
|
|
34
|
+
updatePageContext,
|
|
35
|
+
setAudioMuted, // shortcut
|
|
36
|
+
setPageContextLinked,// shortcut
|
|
37
|
+
reset,
|
|
38
|
+
} = useChatSettings();
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Storage key: `djc.chat.settings`.
|
|
42
|
+
|
|
43
|
+
## Migration status
|
|
44
|
+
|
|
45
|
+
### Migrated
|
|
46
|
+
|
|
47
|
+
- **Page-context opt-in** — `lib/page-snapshot/react/provider.tsx` now
|
|
48
|
+
reads/writes `ChatSettings.pageContext.linked` via `useChatSettings`
|
|
49
|
+
instead of its own `useLocalStorage('djc.page-snapshot.linked')`.
|
|
50
|
+
Note: this changes the storage key, so an existing user's prior opt-in
|
|
51
|
+
is not carried over — they start from the default (`linked: false`).
|
|
52
|
+
That is acceptable: page-context is opt-in and a fresh default is safe.
|
|
53
|
+
|
|
54
|
+
### To migrate (safe follow-up — deferred to keep this pass low-risk)
|
|
55
|
+
|
|
56
|
+
These are mature, working call sites. They each persist correctly today;
|
|
57
|
+
moving them into `ChatSettings` is a clear next step but was deliberately
|
|
58
|
+
deferred so a single PR does not churn the whole chat. Each migration
|
|
59
|
+
also changes a storage key (acceptable — fresh defaults).
|
|
60
|
+
|
|
61
|
+
- **Dock / layout prefs**
|
|
62
|
+
- `tools/Chat/hooks/useChatDockPrefs.ts` — key `chat.dock.prefs`,
|
|
63
|
+
shape `{ mode, side, sideWidth }` → `ChatSettings.dock`.
|
|
64
|
+
- `tools/Chat/hooks/useChatLayout.ts` — keys `djc-chat-mode`
|
|
65
|
+
(`STORAGE_KEYS.mode`) and `djc-chat-sidebar-width`
|
|
66
|
+
(`STORAGE_KEYS.sidebarWidth`) → `ChatSettings.dock.mode` /
|
|
67
|
+
`ChatSettings.dock.width`. Note `useChatLayout` stores `mode` and
|
|
68
|
+
width as two separate keys; consolidate into the `dock` slice.
|
|
69
|
+
- Constants live in `tools/Chat/constants.ts` (`STORAGE_KEYS`).
|
|
70
|
+
|
|
71
|
+
- **Audio prefs**
|
|
72
|
+
- `tools/Chat/hooks/useChatAudio.ts` delegates to ui-core's
|
|
73
|
+
`useNotificationSounds`, which persists under
|
|
74
|
+
`djangocfg-chat-audio:prefs`. Migrating means either teaching
|
|
75
|
+
`useNotificationSounds` to accept an external value/setter, or
|
|
76
|
+
syncing `ChatSettings.audio` ⇆ that hook. Non-trivial — leave as is.
|
|
77
|
+
|
|
78
|
+
- **Speech / language prefs**
|
|
79
|
+
- `tools/SpeechRecognition/store/prefsStore.ts` — a zustand `persist`
|
|
80
|
+
store under `djangocfg-stt:prefs`, shape `{ language, deviceId,
|
|
81
|
+
engineId, earcons }` → `ChatSettings.speech`. This is a different
|
|
82
|
+
persistence mechanism (zustand, not `useLocalStorage`); migrating it
|
|
83
|
+
means reconciling the store with `useChatSettings` and is the
|
|
84
|
+
highest-effort of the three. Leave as is.
|
|
85
|
+
|
|
86
|
+
When migrating any of the above: update the consuming components, delete
|
|
87
|
+
the old key from `STORAGE_KEYS` where applicable, and update this file.
|