@djangocfg/ui-tools 2.1.409 → 2.1.412
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/composer/AttachContext.tsx +22 -0
- package/src/tools/Chat/composer/Composer.tsx +108 -6
- package/src/tools/Chat/composer/ComposerMenuButton.tsx +39 -2
- package/src/tools/Chat/composer/fileToAttachment.ts +53 -0
- package/src/tools/Chat/composer/index.ts +16 -1
- package/src/tools/Chat/composer/types.ts +71 -0
- package/src/tools/Chat/composer/useComposerAttach.tsx +218 -0
- 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/hooks/useChat.ts +32 -0
- package/src/tools/Chat/hooks/useChatComposer.ts +13 -0
- 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/messages/MessageBubble.tsx +1 -1
- package/src/tools/Chat/public.ts +17 -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/types/events.ts +50 -0
- package/src/tools/Chat/types/index.ts +1 -1
- package/src/tools/Chat/types/message.ts +5 -0
- package/src/tools/CronScheduler/CronScheduler.client.tsx +42 -15
- package/src/tools/CronScheduler/components/CustomInput.tsx +26 -7
- package/src/tools/CronScheduler/components/DayChips.tsx +20 -7
- package/src/tools/CronScheduler/components/MonthDayGrid.tsx +35 -10
- package/src/tools/CronScheduler/components/SchedulePreview.tsx +8 -5
- package/src/tools/CronScheduler/components/ScheduleTypeSelector.tsx +12 -3
- package/src/tools/CronScheduler/components/TimeSelector.tsx +36 -13
- package/src/tools/CronScheduler/context/CronSchedulerContext.tsx +4 -0
- package/src/tools/CronScheduler/context/hooks.ts +8 -0
- package/src/tools/CronScheduler/context/index.ts +1 -0
- package/src/tools/CronScheduler/index.tsx +2 -0
- package/src/tools/CronScheduler/lazy.tsx +1 -0
- package/src/tools/CronScheduler/types/index.ts +18 -1
- package/src/tools/Map/lazy.tsx +11 -4
- package/src/tools/Uploader/hooks/useClipboardPaste.ts +3 -1
- package/src/tools/index.ts +2 -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
|
@@ -12,7 +12,30 @@ export const CSS_VARS = {
|
|
|
12
12
|
reserve: '--djc-chat-reserve',
|
|
13
13
|
} as const;
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Z-index tier for the floating chat surface.
|
|
17
|
+
*
|
|
18
|
+
* The chat dock is page furniture — a peer of page content, not a modal.
|
|
19
|
+
* It must sit ABOVE the page yet BELOW every `@djangocfg/ui-core` overlay
|
|
20
|
+
* (sheet 200, drawer 500, dialog 600, anchored overlays 700) so that any
|
|
21
|
+
* dialog — including one launched from inside the chat — always wins.
|
|
22
|
+
*
|
|
23
|
+
* dock 100 the chat surface
|
|
24
|
+
* companion 99 FAB / greeting / unread preview (behind the open dock)
|
|
25
|
+
* tooltip 101 chat-header tooltips (just above the dock, still
|
|
26
|
+
* below ui-core's modal/overlay tiers)
|
|
27
|
+
*/
|
|
28
|
+
export const Z_INDEX = {
|
|
29
|
+
/** The chat dock surface. */
|
|
30
|
+
dock: 100,
|
|
31
|
+
/** FAB, greeting bubble and unread preview — sit just below the dock. */
|
|
32
|
+
companion: 99,
|
|
33
|
+
/** Tooltips portaled out of the chat header — just above the dock. */
|
|
34
|
+
tooltip: 101,
|
|
35
|
+
} as const;
|
|
36
|
+
|
|
37
|
+
/** @deprecated Use {@link Z_INDEX.dock}. Kept for the public API surface. */
|
|
38
|
+
export const DEFAULT_Z_INDEX = Z_INDEX.dock;
|
|
16
39
|
|
|
17
40
|
export const LIMITS = {
|
|
18
41
|
/** Max characters per single message. */
|
|
@@ -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>();
|
|
@@ -407,6 +407,38 @@ export function useChat(config: UseChatConfig): UseChatReturn {
|
|
|
407
407
|
sources: ev.sources?.length ?? 0,
|
|
408
408
|
});
|
|
409
409
|
return;
|
|
410
|
+
case 'message_metrics':
|
|
411
|
+
// Non-terminal: attach per-turn metrics to the streaming
|
|
412
|
+
// message. The stream stays open until message_end / error.
|
|
413
|
+
dispatch({
|
|
414
|
+
type: 'MESSAGE_PATCH',
|
|
415
|
+
id: targetId,
|
|
416
|
+
patch: {
|
|
417
|
+
metrics: ev.metrics,
|
|
418
|
+
...(ev.metrics.resolvedModel
|
|
419
|
+
? { resolvedModel: ev.metrics.resolvedModel }
|
|
420
|
+
: {}),
|
|
421
|
+
},
|
|
422
|
+
});
|
|
423
|
+
log.stream.debug('message_metrics', {
|
|
424
|
+
turns: ev.metrics.turns,
|
|
425
|
+
toolCallCount: ev.metrics.toolCallCount,
|
|
426
|
+
resolvedModel: ev.metrics.resolvedModel,
|
|
427
|
+
});
|
|
428
|
+
return;
|
|
429
|
+
case 'resolved_model':
|
|
430
|
+
// Non-terminal: model alias was resolved mid-run.
|
|
431
|
+
dispatch({
|
|
432
|
+
type: 'MESSAGE_PATCH',
|
|
433
|
+
id: targetId,
|
|
434
|
+
patch: { resolvedModel: ev.resolvedModel },
|
|
435
|
+
});
|
|
436
|
+
log.stream.debug('resolved_model', {
|
|
437
|
+
originalAlias: ev.originalAlias,
|
|
438
|
+
resolvedModel: ev.resolvedModel,
|
|
439
|
+
upgraded: ev.upgraded,
|
|
440
|
+
});
|
|
441
|
+
return;
|
|
410
442
|
case 'error':
|
|
411
443
|
tokenBuffer.flush();
|
|
412
444
|
dispatch({
|
|
@@ -50,6 +50,9 @@ export interface UseChatComposerReturn {
|
|
|
50
50
|
setValue: (next: string) => void;
|
|
51
51
|
attachments: ChatAttachment[];
|
|
52
52
|
addAttachment: (a: ChatAttachment) => void;
|
|
53
|
+
/** Patch an existing attachment in place — used by the upload lifecycle
|
|
54
|
+
* to flip `status` / `progress` / `url` without losing list order. */
|
|
55
|
+
updateAttachment: (id: string, patch: Partial<ChatAttachment>) => void;
|
|
53
56
|
removeAttachment: (id: string) => void;
|
|
54
57
|
isSubmitting: boolean;
|
|
55
58
|
canSubmit: boolean;
|
|
@@ -192,6 +195,15 @@ export function useChatComposer(options: UseChatComposerOptions): UseChatCompose
|
|
|
192
195
|
[maxAttachments],
|
|
193
196
|
);
|
|
194
197
|
|
|
198
|
+
const updateAttachment = useCallback(
|
|
199
|
+
(id: string, patch: Partial<ChatAttachment>) => {
|
|
200
|
+
setAttachments((prev) =>
|
|
201
|
+
prev.map((a) => (a.id === id ? { ...a, ...patch } : a)),
|
|
202
|
+
);
|
|
203
|
+
},
|
|
204
|
+
[],
|
|
205
|
+
);
|
|
206
|
+
|
|
195
207
|
const removeAttachment = useCallback((id: string) => {
|
|
196
208
|
setAttachments((prev) => prev.filter((a) => a.id !== id));
|
|
197
209
|
}, []);
|
|
@@ -276,6 +288,7 @@ export function useChatComposer(options: UseChatComposerOptions): UseChatCompose
|
|
|
276
288
|
setValue,
|
|
277
289
|
attachments,
|
|
278
290
|
addAttachment,
|
|
291
|
+
updateAttachment,
|
|
279
292
|
removeAttachment,
|
|
280
293
|
isSubmitting,
|
|
281
294
|
canSubmit,
|
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
|
@@ -34,6 +34,7 @@ export type {
|
|
|
34
34
|
ChatLabels,
|
|
35
35
|
ChatTransport,
|
|
36
36
|
ChatStreamEvent,
|
|
37
|
+
ChatMessageMetrics,
|
|
37
38
|
CreateSessionOptions,
|
|
38
39
|
SessionInfo,
|
|
39
40
|
HistoryPage,
|
|
@@ -61,6 +62,7 @@ export { DEFAULT_LABELS } from './types';
|
|
|
61
62
|
export {
|
|
62
63
|
STORAGE_KEYS,
|
|
63
64
|
CSS_VARS,
|
|
65
|
+
Z_INDEX,
|
|
64
66
|
DEFAULT_Z_INDEX,
|
|
65
67
|
LIMITS,
|
|
66
68
|
DEFAULT_SIDEBAR,
|
|
@@ -151,6 +153,21 @@ export {
|
|
|
151
153
|
type UseChatAudioReturn,
|
|
152
154
|
} from './core/audio';
|
|
153
155
|
|
|
156
|
+
// Settings — the single, centralized home for chat-owned persisted
|
|
157
|
+
// settings. Every chat feature reads/writes its slice through this hook.
|
|
158
|
+
export {
|
|
159
|
+
useChatSettings,
|
|
160
|
+
CHAT_SETTINGS_STORAGE_KEY,
|
|
161
|
+
DEFAULT_CHAT_SETTINGS,
|
|
162
|
+
type ChatSettings,
|
|
163
|
+
type ChatDockSettings,
|
|
164
|
+
type ChatAudioSettings,
|
|
165
|
+
type ChatSpeechSettings,
|
|
166
|
+
type ChatPageContextSettings,
|
|
167
|
+
type UseChatSettingsOptions,
|
|
168
|
+
type UseChatSettingsReturn,
|
|
169
|
+
} from './settings';
|
|
170
|
+
|
|
154
171
|
// Notifier — title rotation + favicon badge + page-visibility + cross-tab
|
|
155
172
|
export {
|
|
156
173
|
createBrowserNotifier,
|