@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ui-tools",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.412",
|
|
4
4
|
"description": "Heavy React tools with lazy loading - for Electron, Vite, CRA, Next.js apps",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ui-tools",
|
|
@@ -154,13 +154,13 @@
|
|
|
154
154
|
"test:watch": "vitest"
|
|
155
155
|
},
|
|
156
156
|
"peerDependencies": {
|
|
157
|
-
"@djangocfg/i18n": "^2.1.
|
|
158
|
-
"@djangocfg/ui-core": "^2.1.
|
|
157
|
+
"@djangocfg/i18n": "^2.1.412",
|
|
158
|
+
"@djangocfg/ui-core": "^2.1.412",
|
|
159
159
|
"consola": "^3.4.2",
|
|
160
160
|
"lodash-es": "^4.18.1",
|
|
161
161
|
"lucide-react": "^0.545.0",
|
|
162
|
-
"react": "^19.
|
|
163
|
-
"react-dom": "^19.
|
|
162
|
+
"react": "^19.2.4",
|
|
163
|
+
"react-dom": "^19.2.4",
|
|
164
164
|
"tailwindcss": "^4.1.18",
|
|
165
165
|
"zustand": "^5.0.0"
|
|
166
166
|
},
|
|
@@ -210,19 +210,19 @@
|
|
|
210
210
|
"@maplibre/maplibre-gl-geocoder": "^1.7.0"
|
|
211
211
|
},
|
|
212
212
|
"devDependencies": {
|
|
213
|
-
"@djangocfg/i18n": "^2.1.
|
|
214
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
215
|
-
"@djangocfg/ui-core": "^2.1.
|
|
213
|
+
"@djangocfg/i18n": "^2.1.412",
|
|
214
|
+
"@djangocfg/typescript-config": "^2.1.412",
|
|
215
|
+
"@djangocfg/ui-core": "^2.1.412",
|
|
216
216
|
"@types/lodash-es": "^4.17.12",
|
|
217
217
|
"@types/mapbox__mapbox-gl-draw": "^1.4.8",
|
|
218
|
-
"@types/node": "^
|
|
219
|
-
"@types/react": "^19.
|
|
220
|
-
"@types/react-dom": "^19.
|
|
218
|
+
"@types/node": "^25.2.3",
|
|
219
|
+
"@types/react": "^19.2.15",
|
|
220
|
+
"@types/react-dom": "^19.2.3",
|
|
221
221
|
"jsdom": "^29.1.1",
|
|
222
222
|
"lodash-es": "^4.18.1",
|
|
223
223
|
"lucide-react": "^0.545.0",
|
|
224
|
-
"react": "^19.
|
|
225
|
-
"react-dom": "^19.
|
|
224
|
+
"react": "^19.2.4",
|
|
225
|
+
"react-dom": "^19.2.4",
|
|
226
226
|
"tailwindcss": "^4.1.18",
|
|
227
227
|
"tsup": "^8.5.0",
|
|
228
228
|
"typescript": "^5.9.3",
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
# Chat
|
|
1
|
+
# Chat bridge — how the AI drives the user's page
|
|
2
2
|
|
|
3
3
|
When the assistant answers a question about the page, it can also
|
|
4
|
-
**point at the screen
|
|
5
|
-
|
|
4
|
+
**act on it** — point at the screen, move focus, scroll an element into
|
|
5
|
+
view. This module is that control surface, plus the overlay that draws
|
|
6
|
+
the spotlight.
|
|
6
7
|
|
|
7
8
|
---
|
|
8
9
|
|
|
@@ -22,8 +23,9 @@ the spotlight is the same, but *what* it points at is decided by the AI
|
|
|
22
23
|
in context, not a hard-coded step list. The SVG renderer here is in
|
|
23
24
|
fact adapted from that deleted Tour component.
|
|
24
25
|
|
|
25
|
-
**Read-only.** A `point` directive highlights / focuses an
|
|
26
|
-
never types, clicks, or changes the user's data.
|
|
26
|
+
**Read-only by default.** A `point` directive highlights / focuses an
|
|
27
|
+
element; it never types, clicks, or changes the user's data. The
|
|
28
|
+
write-style commands (`fill`, `click`) are deliberately stubbed.
|
|
27
29
|
|
|
28
30
|
---
|
|
29
31
|
|
|
@@ -46,23 +48,42 @@ never types, clicks, or changes the user's data.
|
|
|
46
48
|
|
|
47
49
|
---
|
|
48
50
|
|
|
49
|
-
##
|
|
51
|
+
## Layout
|
|
50
52
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
The module has two layers — `browser-bridge/` is *how the AI drives the
|
|
54
|
+
browser*, `overlay/` is *how a highlight is drawn*.
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
browser-bridge/
|
|
58
|
+
index.ts Public barrel.
|
|
59
|
+
registry.ts BridgeCommand interface + the command registry.
|
|
60
|
+
setBridgeResolver.ts Holds the live ref→element resolver the commands read.
|
|
61
|
+
directive-bus.ts SSE `directive` event → singleton bus.
|
|
62
|
+
window.ts installChatBridge — dev-only window.__chatBridge.
|
|
63
|
+
commands/
|
|
64
|
+
index.ts Registers + re-exports every built-in command.
|
|
65
|
+
highlight.ts highlight, focus, clear.
|
|
66
|
+
scroll.ts scrollTo.
|
|
67
|
+
inspect.ts inspect.
|
|
68
|
+
write.ts fill, click (stubs — need a confirmation gate).
|
|
69
|
+
overlay/
|
|
70
|
+
index.ts Overlay barrel.
|
|
71
|
+
types.ts PointDirective, HighlightTarget, SpotlightRect, CSTRefId.
|
|
72
|
+
resolveRef.ts resolveRefs() — CST ref → live element via a RefResolver.
|
|
73
|
+
useHighlightTargets.ts Hook: directives → geometry-tracked targets.
|
|
74
|
+
SpotlightCanvas.tsx Pure SVG-mask renderer (scrim, cut-outs, pulse).
|
|
75
|
+
HighlightOverlay.tsx The component: hook + canvas + captions.
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Commands are one-per-file: adding a capability is a new file in
|
|
79
|
+
`commands/`, registered in `commands/index.ts`.
|
|
59
80
|
|
|
60
81
|
---
|
|
61
82
|
|
|
62
83
|
## Usage
|
|
63
84
|
|
|
64
85
|
```tsx
|
|
65
|
-
import { HighlightOverlay } from '@djangocfg/ui-tools
|
|
86
|
+
import { HighlightOverlay } from '@djangocfg/ui-tools/chat';
|
|
66
87
|
|
|
67
88
|
<HighlightOverlay
|
|
68
89
|
directives={directivesFromLastDirectiveEvent}
|
|
@@ -78,6 +99,11 @@ import { HighlightOverlay } from '@djangocfg/ui-tools/.../Chat/highlight';
|
|
|
78
99
|
`page-snapshot` engine's `RefRegistry` satisfies this. Structurally
|
|
79
100
|
typed on purpose, so this module does not import the capture engine.
|
|
80
101
|
|
|
102
|
+
In development, `installChatBridge()` exposes the registry on
|
|
103
|
+
`window.__chatBridge` — run `__chatBridge.help()` for the command list,
|
|
104
|
+
or `__chatBridge.highlight(['@e4'])` to see the spotlight without an
|
|
105
|
+
AI / SSE round-trip.
|
|
106
|
+
|
|
81
107
|
---
|
|
82
108
|
|
|
83
109
|
## Staleness
|
|
@@ -91,9 +117,11 @@ only ever shows live elements.
|
|
|
91
117
|
|
|
92
118
|
## Decoupling notes
|
|
93
119
|
|
|
94
|
-
- This module lives under `
|
|
95
|
-
|
|
96
|
-
|
|
120
|
+
- This module lives under `lib/` — it is infrastructure ("the AI reads
|
|
121
|
+
and acts on the user's page"), the sibling of `lib/page-snapshot/`.
|
|
122
|
+
Chat is merely a consumer: it re-exports this barrel via
|
|
123
|
+
`@djangocfg/ui-tools/chat`. `lib/` must never import from `tools/`,
|
|
124
|
+
so the bridge carries its own minimal `logger.ts`.
|
|
97
125
|
- It does not import the `page-snapshot` capture engine. It depends only
|
|
98
126
|
on the structural `RefResolver` interface, so the two stay
|
|
99
127
|
independent.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Chat commands — drive the chat conversation from the bridge.
|
|
5
|
+
*
|
|
6
|
+
* `sendMessage` posts a message exactly as if typed into the composer
|
|
7
|
+
* (same transport, same SSE response). Useful for testing the chat —
|
|
8
|
+
* incl. the page-context highlight flow — from the devtools console
|
|
9
|
+
* without retyping prompts.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { log } from '../logger';
|
|
13
|
+
import { registerBridgeCommand } from '../registry';
|
|
14
|
+
import { getActiveSender } from '../setBridgeResolver';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* sendMessage — send a chat message programmatically.
|
|
18
|
+
*
|
|
19
|
+
* Goes through the same `sendMessage` the composer uses, so the full
|
|
20
|
+
* round-trip runs (transport, streamed reply, any directives). Resolves
|
|
21
|
+
* when the send is dispatched; the reply streams asynchronously after.
|
|
22
|
+
*/
|
|
23
|
+
export const sendMessage = registerBridgeCommand({
|
|
24
|
+
name: 'sendMessage',
|
|
25
|
+
description: 'sendMessage(text) — send a chat message as if typed',
|
|
26
|
+
mutates: true,
|
|
27
|
+
run: async (text: string): Promise<{ sent: boolean }> => {
|
|
28
|
+
const sender = getActiveSender();
|
|
29
|
+
if (!sender) {
|
|
30
|
+
log.warn('bridge.sendMessage: no chat sender registered');
|
|
31
|
+
return { sent: false };
|
|
32
|
+
}
|
|
33
|
+
const content = String(text ?? '').trim();
|
|
34
|
+
if (!content) {
|
|
35
|
+
log.warn('bridge.sendMessage: empty message');
|
|
36
|
+
return { sent: false };
|
|
37
|
+
}
|
|
38
|
+
log.info('bridge.sendMessage', { chars: content.length });
|
|
39
|
+
await sender(content);
|
|
40
|
+
return { sent: true };
|
|
41
|
+
},
|
|
42
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Spotlight commands — `highlight`, `focus`, `clear`.
|
|
5
|
+
*
|
|
6
|
+
* All three drive the directive bus the AI uses, so the overlay reacts
|
|
7
|
+
* identically whether the source is an SSE event or a console call.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { log } from '../logger';
|
|
11
|
+
import { pushDirectives, clearDirectives } from '../directive-bus';
|
|
12
|
+
import { registerBridgeCommand } from '../registry';
|
|
13
|
+
import type { CSTRefId, PointDirective } from '../overlay/types';
|
|
14
|
+
|
|
15
|
+
/** Coerce loose console input ("@e4" or ["@e4","@e5"]) into a ref list. */
|
|
16
|
+
function toRefList(input: CSTRefId | CSTRefId[]): CSTRefId[] {
|
|
17
|
+
return Array.isArray(input) ? input : [input];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* highlight — spotlight one or more elements by CST ref.
|
|
22
|
+
* Drives the same directive bus the AI uses, so the overlay reacts
|
|
23
|
+
* identically whether the source is an SSE event or a console call.
|
|
24
|
+
*/
|
|
25
|
+
export const highlight = registerBridgeCommand({
|
|
26
|
+
name: 'highlight',
|
|
27
|
+
description: 'highlight(refs, opts?) — spotlight element(s) by CST ref',
|
|
28
|
+
mutates: false,
|
|
29
|
+
run: (
|
|
30
|
+
refs: CSTRefId | CSTRefId[],
|
|
31
|
+
opts?: { focus?: boolean; label?: string },
|
|
32
|
+
): { dispatched: number } => {
|
|
33
|
+
const list = toRefList(refs);
|
|
34
|
+
const directives: PointDirective[] = list.map((ref) => ({
|
|
35
|
+
type: 'point',
|
|
36
|
+
ref,
|
|
37
|
+
focus: opts?.focus,
|
|
38
|
+
label: opts?.label,
|
|
39
|
+
}));
|
|
40
|
+
log.info('bridge.highlight', { refs: list, opts });
|
|
41
|
+
pushDirectives(directives);
|
|
42
|
+
return { dispatched: directives.length };
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* focus — spotlight a single element AND move keyboard focus into it.
|
|
48
|
+
* A thin convenience over `highlight` with `focus: true`.
|
|
49
|
+
*/
|
|
50
|
+
export const focus = registerBridgeCommand({
|
|
51
|
+
name: 'focus',
|
|
52
|
+
description: 'focus(ref, label?) — highlight an element and focus it',
|
|
53
|
+
mutates: false,
|
|
54
|
+
run: (ref: CSTRefId, label?: string): { dispatched: number } => {
|
|
55
|
+
log.info('bridge.focus', { ref, label });
|
|
56
|
+
pushDirectives([{ type: 'point', ref, focus: true, label }]);
|
|
57
|
+
return { dispatched: 1 };
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
/** clear — dismiss any active highlight overlay. */
|
|
62
|
+
export const clear = registerBridgeCommand({
|
|
63
|
+
name: 'clear',
|
|
64
|
+
description: 'clear() — remove the active highlight overlay',
|
|
65
|
+
mutates: false,
|
|
66
|
+
run: (): void => {
|
|
67
|
+
log.info('bridge.clear');
|
|
68
|
+
clearDirectives();
|
|
69
|
+
},
|
|
70
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Built-in bridge commands.
|
|
5
|
+
*
|
|
6
|
+
* Importing this module registers every command into the shared
|
|
7
|
+
* registry (registration is a side effect of each command file). New
|
|
8
|
+
* capabilities are added as a new file here, re-exported below.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export { highlight, focus, clear } from './highlight';
|
|
12
|
+
export { scrollTo } from './scroll';
|
|
13
|
+
export { inspect } from './inspect';
|
|
14
|
+
export { fill, click } from './write';
|
|
15
|
+
export { sendMessage } from './chat';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* inspect command — a read-only probe of ref freshness.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { log } from '../logger';
|
|
8
|
+
import { registerBridgeCommand } from '../registry';
|
|
9
|
+
import { getActiveResolver } from '../setBridgeResolver';
|
|
10
|
+
import { resolveRefs } from '../overlay/resolveRef';
|
|
11
|
+
import type { CSTRefId } from '../overlay/types';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* inspect — report which CST refs currently resolve to live elements.
|
|
15
|
+
* A read-only probe: useful for the AI (or a tester) to confirm the
|
|
16
|
+
* snapshot is still fresh before pointing at something.
|
|
17
|
+
*/
|
|
18
|
+
export const inspect = registerBridgeCommand({
|
|
19
|
+
name: 'inspect',
|
|
20
|
+
description: 'inspect(refs) — report which refs resolve to live elements',
|
|
21
|
+
mutates: false,
|
|
22
|
+
run: (refs: CSTRefId[]): { resolved: CSTRefId[]; missing: CSTRefId[] } => {
|
|
23
|
+
const ok = new Set(resolveRefs(refs, getActiveResolver()).map((r) => r.ref));
|
|
24
|
+
const result = {
|
|
25
|
+
resolved: refs.filter((r) => ok.has(r)),
|
|
26
|
+
missing: refs.filter((r) => !ok.has(r)),
|
|
27
|
+
};
|
|
28
|
+
log.info('bridge.inspect', result);
|
|
29
|
+
return result;
|
|
30
|
+
},
|
|
31
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* scroll command — bring an element into view without a spotlight.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { log } from '../logger';
|
|
8
|
+
import { registerBridgeCommand } from '../registry';
|
|
9
|
+
import { getActiveResolver } from '../setBridgeResolver';
|
|
10
|
+
import { resolveRefs } from '../overlay/resolveRef';
|
|
11
|
+
import type { CSTRefId } from '../overlay/types';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* scrollTo — bring an element into view without drawing a spotlight.
|
|
15
|
+
* Resolves the ref directly (no overlay) and scrolls.
|
|
16
|
+
*/
|
|
17
|
+
export const scrollTo = registerBridgeCommand({
|
|
18
|
+
name: 'scrollTo',
|
|
19
|
+
description: 'scrollTo(ref) — scroll an element into view',
|
|
20
|
+
mutates: false,
|
|
21
|
+
run: (ref: CSTRefId): { ok: boolean } => {
|
|
22
|
+
const [resolved] = resolveRefs([ref], getActiveResolver());
|
|
23
|
+
if (!resolved) {
|
|
24
|
+
log.warn('bridge.scrollTo: ref did not resolve', { ref });
|
|
25
|
+
return { ok: false };
|
|
26
|
+
}
|
|
27
|
+
resolved.element.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
28
|
+
log.info('bridge.scrollTo', { ref });
|
|
29
|
+
return { ok: true };
|
|
30
|
+
},
|
|
31
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Write-style command stubs — `fill`, `click`.
|
|
5
|
+
*
|
|
6
|
+
* Filling an input or clicking a control changes the user's data, so
|
|
7
|
+
* each needs an explicit confirmation gate before it can be enabled.
|
|
8
|
+
* They are registered (so they appear in `__chatBridge.help()`) but
|
|
9
|
+
* intentionally not implemented yet.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { log } from '../logger';
|
|
13
|
+
import { registerBridgeCommand } from '../registry';
|
|
14
|
+
import type { CSTRefId } from '../overlay/types';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Stub for a future write-style command. Filling an input changes the
|
|
18
|
+
* user's data, so it needs an explicit confirmation gate before it can
|
|
19
|
+
* be enabled — intentionally not implemented yet.
|
|
20
|
+
*/
|
|
21
|
+
export const fill = registerBridgeCommand({
|
|
22
|
+
name: 'fill',
|
|
23
|
+
description: 'fill(ref, value) — [not implemented] type into an input',
|
|
24
|
+
mutates: true,
|
|
25
|
+
run: (ref: CSTRefId, value: string): { ok: false; reason: string } => {
|
|
26
|
+
log.warn('bridge.fill: not implemented (needs a confirmation gate)', {
|
|
27
|
+
ref,
|
|
28
|
+
value,
|
|
29
|
+
});
|
|
30
|
+
return { ok: false, reason: 'not-implemented' };
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
/** Stub for a future write-style command — see `fill`. */
|
|
35
|
+
export const click = registerBridgeCommand({
|
|
36
|
+
name: 'click',
|
|
37
|
+
description: 'click(ref) — [not implemented] click an element',
|
|
38
|
+
mutates: true,
|
|
39
|
+
run: (ref: CSTRefId): { ok: false; reason: string } => {
|
|
40
|
+
log.warn('bridge.click: not implemented (needs a confirmation gate)', {
|
|
41
|
+
ref,
|
|
42
|
+
});
|
|
43
|
+
return { ok: false, reason: 'not-implemented' };
|
|
44
|
+
},
|
|
45
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AI directive bus — receives `point` directives off the chat SSE
|
|
5
|
+
* stream and exposes them to the highlight overlay.
|
|
6
|
+
*
|
|
7
|
+
* The backend emits a `directive` SSE event carrying highlight/focus
|
|
8
|
+
* instructions keyed by CST ref ids. A host transport observes the raw
|
|
9
|
+
* frame and calls `pushDirectives`; the chat UI subscribes through
|
|
10
|
+
* `useChatDirectives` and feeds the result to `<HighlightOverlay>`.
|
|
11
|
+
*
|
|
12
|
+
* A singleton bus keeps the transport (which has no React context)
|
|
13
|
+
* decoupled from the consuming component.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { useSyncExternalStore } from 'react';
|
|
17
|
+
|
|
18
|
+
import { log } from './logger';
|
|
19
|
+
import type { PointDirective } from './overlay/types';
|
|
20
|
+
|
|
21
|
+
// ─── Wire parsing ───────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
/** A CST ref looks like "@e4". */
|
|
24
|
+
const REF_RE = /^@e\d+$/;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Validate the raw `directives` array from a `directive` SSE frame.
|
|
28
|
+
*
|
|
29
|
+
* The backend already validates refs against the snapshot, but the wire
|
|
30
|
+
* is untyped at this boundary — drop anything malformed so a bad frame
|
|
31
|
+
* can never crash the overlay.
|
|
32
|
+
*/
|
|
33
|
+
export function parseDirectives(raw: unknown): PointDirective[] {
|
|
34
|
+
if (!Array.isArray(raw)) {
|
|
35
|
+
log.warn('parseDirectives: payload is not an array', { raw });
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
const out: PointDirective[] = [];
|
|
39
|
+
let dropped = 0;
|
|
40
|
+
for (const item of raw) {
|
|
41
|
+
if (!item || typeof item !== 'object') {
|
|
42
|
+
dropped++;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const d = item as Record<string, unknown>;
|
|
46
|
+
if (d.type !== 'point') {
|
|
47
|
+
dropped++;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (typeof d.ref !== 'string' || !REF_RE.test(d.ref)) {
|
|
51
|
+
log.warn('parseDirectives: dropped directive with bad ref', { ref: d.ref });
|
|
52
|
+
dropped++;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const directive: PointDirective = {
|
|
56
|
+
type: 'point',
|
|
57
|
+
ref: d.ref as PointDirective['ref'],
|
|
58
|
+
};
|
|
59
|
+
if (d.highlight === false) directive.highlight = false;
|
|
60
|
+
if (d.focus === true) directive.focus = true;
|
|
61
|
+
if (typeof d.label === 'string' && d.label.trim()) {
|
|
62
|
+
directive.label = d.label.trim();
|
|
63
|
+
}
|
|
64
|
+
out.push(directive);
|
|
65
|
+
}
|
|
66
|
+
log.info('parseDirectives', { in: raw.length, out: out.length, dropped });
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ─── Singleton bus ──────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
let current: PointDirective[] = [];
|
|
73
|
+
const subscribers = new Set<() => void>();
|
|
74
|
+
|
|
75
|
+
function notify(): void {
|
|
76
|
+
for (const s of subscribers) s();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Transport-side entry point. Replaces the active directive set — the
|
|
81
|
+
* latest `directive` event for a turn wins; an empty array clears the
|
|
82
|
+
* overlay. Called synchronously from a host transport's event handler.
|
|
83
|
+
*/
|
|
84
|
+
export function pushDirectives(directives: PointDirective[]): void {
|
|
85
|
+
log.info('pushDirectives → bus', {
|
|
86
|
+
count: directives.length,
|
|
87
|
+
refs: directives.map((d) => d.ref),
|
|
88
|
+
subscribers: subscribers.size,
|
|
89
|
+
});
|
|
90
|
+
current = directives;
|
|
91
|
+
notify();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Clear the active directives (e.g. when the overlay is dismissed). */
|
|
95
|
+
export function clearDirectives(): void {
|
|
96
|
+
if (current.length === 0) return;
|
|
97
|
+
log.info('clearDirectives');
|
|
98
|
+
current = [];
|
|
99
|
+
notify();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function subscribe(cb: () => void): () => void {
|
|
103
|
+
subscribers.add(cb);
|
|
104
|
+
return () => subscribers.delete(cb);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function getSnapshot(): PointDirective[] {
|
|
108
|
+
return current;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Stable empty reference for the SSR snapshot. */
|
|
112
|
+
const EMPTY: PointDirective[] = [];
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Subscribe to the active `point` directives. Re-renders the caller
|
|
116
|
+
* whenever a new `directive` event arrives or the set is cleared.
|
|
117
|
+
*/
|
|
118
|
+
export function useChatDirectives(): PointDirective[] {
|
|
119
|
+
return useSyncExternalStore(subscribe, getSnapshot, () => EMPTY);
|
|
120
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Chat bridge — how the AI drives the user's live page.
|
|
5
|
+
*
|
|
6
|
+
* Two layers:
|
|
7
|
+
*
|
|
8
|
+
* - `bridge/` proper — the command registry, the built-in commands, and
|
|
9
|
+
* the directive transport (SSE → singleton bus). This is the AI's
|
|
10
|
+
* control surface over the browser.
|
|
11
|
+
* - `overlay/` — how a `point` directive is drawn: ref resolution,
|
|
12
|
+
* geometry tracking, and the SVG-mask spotlight.
|
|
13
|
+
*
|
|
14
|
+
* Read-only by default. Commands that would change the user's data
|
|
15
|
+
* (`fill`, `click`) are deliberately stubbed.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// Overlay — how a highlight is drawn.
|
|
19
|
+
export {
|
|
20
|
+
HighlightOverlay,
|
|
21
|
+
type HighlightOverlayProps,
|
|
22
|
+
SpotlightCanvas,
|
|
23
|
+
type SpotlightCanvasProps,
|
|
24
|
+
useHighlightTargets,
|
|
25
|
+
resolveRefs,
|
|
26
|
+
type RefResolver,
|
|
27
|
+
type PointDirective,
|
|
28
|
+
type HighlightTarget,
|
|
29
|
+
type SpotlightRect,
|
|
30
|
+
type CSTRefId,
|
|
31
|
+
} from './overlay';
|
|
32
|
+
|
|
33
|
+
// Directive transport — SSE `directive` event → singleton bus.
|
|
34
|
+
export {
|
|
35
|
+
useChatDirectives,
|
|
36
|
+
pushDirectives,
|
|
37
|
+
clearDirectives,
|
|
38
|
+
parseDirectives,
|
|
39
|
+
} from './directive-bus';
|
|
40
|
+
|
|
41
|
+
// Command registry + the ref resolver the commands read.
|
|
42
|
+
export {
|
|
43
|
+
registerBridgeCommand,
|
|
44
|
+
getBridgeCommand,
|
|
45
|
+
type BridgeCommand,
|
|
46
|
+
} from './registry';
|
|
47
|
+
export {
|
|
48
|
+
setBridgeResolver,
|
|
49
|
+
setBridgeSender,
|
|
50
|
+
type BridgeSender,
|
|
51
|
+
} from './setBridgeResolver';
|
|
52
|
+
|
|
53
|
+
// `window.__chatBridge` install (dev-gated). Importing this transitively
|
|
54
|
+
// pulls in `./commands`, whose modules register the built-in commands
|
|
55
|
+
// into the registry as a side effect.
|
|
56
|
+
export { installChatBridge, type ChatBridge } from './window';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-bridge dev logger.
|
|
3
|
+
*
|
|
4
|
+
* A self-contained namespaced `consola` logger for the bridge subsystem
|
|
5
|
+
* (page-context `point` directives: SSE event → bus → overlay). It is a
|
|
6
|
+
* sibling of `lib/page-snapshot/`, so it must NOT reach up into `tools/` —
|
|
7
|
+
* the bridge is infrastructure, Chat is merely a consumer.
|
|
8
|
+
*
|
|
9
|
+
* Mirrors the gating of the chat logger (`tools/Chat/core/logger.ts`): the
|
|
10
|
+
* `chat:directives` sub-logger is silenced unless `isDev`, so noisy bridge
|
|
11
|
+
* events never leak into production builds. Kept local and minimal — only
|
|
12
|
+
* the `directives` scope is needed here.
|
|
13
|
+
*/
|
|
14
|
+
import { consola } from 'consola';
|
|
15
|
+
|
|
16
|
+
import { isDev } from '@djangocfg/ui-core/lib';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Namespaced logger for bridge directives. Tagged `chat:directives` to stay
|
|
20
|
+
* consistent with the chat logger's scope naming. No-ops in production.
|
|
21
|
+
*/
|
|
22
|
+
export const log = consola.withTag('chat').withTag('directives');
|
|
23
|
+
|
|
24
|
+
// Silence in production — bridge directive logs are dev-only diagnostics.
|
|
25
|
+
if (!isDev) {
|
|
26
|
+
log.level = -999;
|
|
27
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { useEffect } from 'react';
|
|
4
4
|
import { createPortal } from 'react-dom';
|
|
5
5
|
|
|
6
|
+
import { log } from '../logger';
|
|
6
7
|
import { SpotlightCanvas } from './SpotlightCanvas';
|
|
7
8
|
import type { PointDirective, SpotlightRect } from './types';
|
|
8
9
|
import { useHighlightTargets } from './useHighlightTargets';
|
|
@@ -40,6 +41,16 @@ export function HighlightOverlay({
|
|
|
40
41
|
}: HighlightOverlayProps) {
|
|
41
42
|
const targets = useHighlightTargets(directives, resolver);
|
|
42
43
|
|
|
44
|
+
// Trace why the overlay does or does not show — the common failure is
|
|
45
|
+
// directives arriving with no resolver, or refs that resolve to nothing.
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
log.info('HighlightOverlay render', {
|
|
48
|
+
directives: directives.length,
|
|
49
|
+
hasResolver: resolver !== null,
|
|
50
|
+
resolvedTargets: targets.length,
|
|
51
|
+
});
|
|
52
|
+
}, [directives, resolver, targets.length]);
|
|
53
|
+
|
|
43
54
|
// Auto-dismiss after the TTL once something is showing.
|
|
44
55
|
useEffect(() => {
|
|
45
56
|
if (targets.length === 0 || ttlMs <= 0) return;
|
|
@@ -67,6 +78,9 @@ export function HighlightOverlay({
|
|
|
67
78
|
|
|
68
79
|
return createPortal(
|
|
69
80
|
<div
|
|
81
|
+
// Above page content (sticky headers ~z-40) but below the chat
|
|
82
|
+
// companion/dock tiers (99/100) — it points at page elements, so it
|
|
83
|
+
// must not cover the chat or any ui-core overlay.
|
|
70
84
|
className="pointer-events-none fixed inset-0 z-[60]"
|
|
71
85
|
data-chat-highlight-overlay=""
|
|
72
86
|
>
|