@backmatter/studio 0.0.2 → 0.0.4
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/dist/chunks/EmbedPublishPage-hx1xoAhI.js +149 -0
- package/dist/chunks/{UiKitPage-CTZtLwJ0.js → UiKitPage-BOunq6mr.js} +12 -11
- package/dist/chunks/WorkspaceRoutes-CTFTWu3L.js +57731 -0
- package/dist/chunks/{dashboard-nvFBzpZv.js → dashboard-upmiawx5.js} +216 -215
- package/dist/chunks/{index-Cr-IOgdC.js → index-CBvevwfT.js} +9 -8
- package/dist/chunks/status-bar-action-DiXhfcj3.js +30254 -0
- package/dist/dashboard.js +2 -2
- package/dist/index.js +139 -228
- package/dist/site-widget.js +292 -0
- package/dist/studio.css +46 -6
- package/package.json +7 -2
- package/src/api.ts +4 -0
- package/src/chat/ChangeCard.tsx +11 -6
- package/src/chat/ChatMessage.tsx +27 -2
- package/src/chat/ChatPanel.tsx +12 -2
- package/src/chat/ChatProvider.tsx +40 -3
- package/src/chat/Composer.tsx +92 -6
- package/src/chat/DocLink.tsx +31 -0
- package/src/chat/DocListCard.tsx +3 -3
- package/src/chat/PointerChip.tsx +139 -0
- package/src/chat/chat-context.ts +80 -1
- package/src/chat/icons.tsx +20 -0
- package/src/chat/session-expiry.ts +13 -5
- package/src/chrome/OpenSiteButton.tsx +33 -25
- package/src/index.css +9 -1
- package/src/pages/EmbedChatPage.tsx +25 -1
- package/src/pages/EmbedPublishPage.tsx +279 -84
- package/src/pages/GithubConnectPage.tsx +44 -19
- package/src/pages/SelectRepositoriesLink.tsx +30 -0
- package/src/pages/SettingsGeneralPage.tsx +2 -0
- package/src/pages/SettingsGithubPage.tsx +8 -3
- package/src/pages/host-env.ts +7 -2
- package/src/site-pointer.ts +271 -0
- package/src/site-widget.tsx +391 -0
- package/src/site.tsx +11 -7
- package/dist/chunks/WorkspaceRoutes-DyRVES9t.js +0 -87308
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@backmatter/studio",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "The Backmatter editing surface: document editor, AI chat, publish. Mounted on your site or hosted on the dashboard.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -28,6 +28,11 @@
|
|
|
28
28
|
"default": "./dist/define.js"
|
|
29
29
|
},
|
|
30
30
|
"./styles": "./src/styles.ts",
|
|
31
|
+
"./widget": {
|
|
32
|
+
"types": "./src/site-widget.tsx",
|
|
33
|
+
"source": "./src/site-widget.tsx",
|
|
34
|
+
"default": "./dist/site-widget.js"
|
|
35
|
+
},
|
|
31
36
|
"./studio.css": "./dist/studio.css",
|
|
32
37
|
"./prose-ui-node-shim": "./prose-ui-node-shim.ts",
|
|
33
38
|
"./package.json": "./package.json",
|
|
@@ -47,7 +52,7 @@
|
|
|
47
52
|
},
|
|
48
53
|
"dependencies": {
|
|
49
54
|
"@ai-sdk/react": "^4.0.100",
|
|
50
|
-
"@backmatter/core": "^0.0.
|
|
55
|
+
"@backmatter/core": "^0.0.21",
|
|
51
56
|
"@base-ui/react": "^1.6.0",
|
|
52
57
|
"@dnd-kit/abstract": "^0.5.0",
|
|
53
58
|
"@dnd-kit/react": "^0.5.0",
|
package/src/api.ts
CHANGED
|
@@ -261,6 +261,10 @@ export function attachGithubInstallation(
|
|
|
261
261
|
});
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
+
export function fetchGithubRepositoryAccess(workspaceId: string) {
|
|
265
|
+
return api<{ url: string }>(`/workspaces/${workspaceId}/github/repository-access`);
|
|
266
|
+
}
|
|
267
|
+
|
|
264
268
|
export function fetchGithubRepos(workspaceId: string) {
|
|
265
269
|
return api<{
|
|
266
270
|
repos: Array<{ fullName: string; defaultBranch: string }>;
|
package/src/chat/ChangeCard.tsx
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// packages/studio/src/chat/ChangeCard.tsx
|
|
2
2
|
import { useState } from "react";
|
|
3
|
-
import { Link } from "react-router";
|
|
4
3
|
import * as stylex from "@stylexjs/stylex";
|
|
5
4
|
import type { ChatChangeSummary } from "../api";
|
|
6
5
|
import { colors, radius, space, type } from "../styles/tokens.stylex";
|
|
@@ -8,6 +7,7 @@ import { Tooltip } from "../ui/Tooltip";
|
|
|
8
7
|
import { contentDocPath } from "../workspace-path";
|
|
9
8
|
import { useWorkspaceNav } from "../chrome/WorkspaceNavContext";
|
|
10
9
|
import { CheckIcon, ChevronDownIcon, FileIcon, UndoIcon } from "./icons";
|
|
10
|
+
import { DocLink } from "./DocLink";
|
|
11
11
|
|
|
12
12
|
const hover = "@media (hover: hover)";
|
|
13
13
|
|
|
@@ -56,12 +56,17 @@ export function ChangeCard({
|
|
|
56
56
|
<span {...stylex.props(styles.fileIcon)}>
|
|
57
57
|
<FileIcon />
|
|
58
58
|
</span>
|
|
59
|
-
|
|
60
|
-
<
|
|
59
|
+
{nav ? (
|
|
60
|
+
<Tooltip label={`Open ${label} in the editor`}>
|
|
61
|
+
<DocLink to={to} {...stylex.props(styles.name)}>
|
|
62
|
+
{label}
|
|
63
|
+
</DocLink>
|
|
64
|
+
</Tooltip>
|
|
65
|
+
) : (
|
|
66
|
+
<DocLink to={to} {...stylex.props(styles.name)}>
|
|
61
67
|
{label}
|
|
62
|
-
</
|
|
63
|
-
|
|
64
|
-
<span {...stylex.props(styles.locale)}>{change.locale}</span>
|
|
68
|
+
</DocLink>
|
|
69
|
+
)}
|
|
65
70
|
{change.diff?.renamedFrom ? (
|
|
66
71
|
<span {...stylex.props(styles.badge)}>
|
|
67
72
|
{change.diff.renamedFrom} → {change.slug}
|
package/src/chat/ChatMessage.tsx
CHANGED
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
type WriteToolOutput,
|
|
20
20
|
} from "./chat-helpers";
|
|
21
21
|
import { CheckIcon, EyeIcon, FileIcon, SearchIcon, UndoIcon } from "./icons";
|
|
22
|
+
import { pointersFromMetadata } from "./chat-context";
|
|
23
|
+
import { PointerChip, pointerKey } from "./PointerChip";
|
|
22
24
|
|
|
23
25
|
const hover = "@media (hover: hover)";
|
|
24
26
|
|
|
@@ -117,9 +119,19 @@ export function ChatMessage({
|
|
|
117
119
|
.filter((p): p is { type: "text"; text: string } => p.type === "text")
|
|
118
120
|
.map((p) => p.text)
|
|
119
121
|
.join("\n");
|
|
122
|
+
const pointers = pointersFromMetadata(message.metadata);
|
|
120
123
|
return (
|
|
121
124
|
<div {...stylex.props(styles.userRow, entering && styles.enter)}>
|
|
122
|
-
<div {...stylex.props(styles.
|
|
125
|
+
<div {...stylex.props(styles.userStack)}>
|
|
126
|
+
{pointers.length > 0 ? (
|
|
127
|
+
<div {...stylex.props(styles.userChips)} aria-label="Pointed elements">
|
|
128
|
+
{pointers.map((pointer) => (
|
|
129
|
+
<PointerChip key={pointerKey(pointer)} pointer={pointer} />
|
|
130
|
+
))}
|
|
131
|
+
</div>
|
|
132
|
+
) : null}
|
|
133
|
+
{text ? <div {...stylex.props(styles.userBubble)}>{text}</div> : null}
|
|
134
|
+
</div>
|
|
123
135
|
</div>
|
|
124
136
|
);
|
|
125
137
|
}
|
|
@@ -312,8 +324,21 @@ const styles = stylex.create({
|
|
|
312
324
|
display: "flex",
|
|
313
325
|
justifyContent: "flex-end",
|
|
314
326
|
},
|
|
315
|
-
|
|
327
|
+
userStack: {
|
|
328
|
+
display: "flex",
|
|
329
|
+
flexDirection: "column",
|
|
330
|
+
alignItems: "flex-end",
|
|
331
|
+
gap: space.sm,
|
|
316
332
|
maxWidth: "85%",
|
|
333
|
+
},
|
|
334
|
+
userChips: {
|
|
335
|
+
display: "flex",
|
|
336
|
+
flexWrap: "wrap",
|
|
337
|
+
justifyContent: "flex-end",
|
|
338
|
+
gap: space.sm,
|
|
339
|
+
},
|
|
340
|
+
userBubble: {
|
|
341
|
+
maxWidth: "100%",
|
|
317
342
|
paddingInline: space.lg,
|
|
318
343
|
paddingBlock: space.md,
|
|
319
344
|
borderRadius: 10,
|
package/src/chat/ChatPanel.tsx
CHANGED
|
@@ -26,7 +26,7 @@ import { isPanelVisible } from "./chat-state";
|
|
|
26
26
|
|
|
27
27
|
const hover = "@media (hover: hover)";
|
|
28
28
|
|
|
29
|
-
export function ChatPanel({ fill = false }: { fill?: boolean } = {}) {
|
|
29
|
+
export function ChatPanel({ fill = false, site = false }: { fill?: boolean; site?: boolean } = {}) {
|
|
30
30
|
const ui = useChatUI();
|
|
31
31
|
const session = useChatSession();
|
|
32
32
|
const visible = isPanelVisible(ui.panel.mode);
|
|
@@ -70,7 +70,12 @@ export function ChatPanel({ fill = false }: { fill?: boolean } = {}) {
|
|
|
70
70
|
<section
|
|
71
71
|
role="dialog"
|
|
72
72
|
aria-label="AI chat"
|
|
73
|
-
|
|
73
|
+
data-backmatter-panel=""
|
|
74
|
+
{...stylex.props(
|
|
75
|
+
fill ? styles.panelFill : styles.panel,
|
|
76
|
+
site && styles.panelSite,
|
|
77
|
+
!fill && fullscreen && styles.panelFull,
|
|
78
|
+
)}
|
|
74
79
|
>
|
|
75
80
|
<header {...stylex.props(styles.header)}>
|
|
76
81
|
<h2 {...stylex.props(styles.title)}>{title}</h2>
|
|
@@ -245,6 +250,11 @@ const styles = stylex.create({
|
|
|
245
250
|
width: "auto",
|
|
246
251
|
height: "auto",
|
|
247
252
|
},
|
|
253
|
+
panelSite: {
|
|
254
|
+
right: 16,
|
|
255
|
+
bottom: 64,
|
|
256
|
+
zIndex: 2147483001,
|
|
257
|
+
},
|
|
248
258
|
panelFill: {
|
|
249
259
|
position: "fixed",
|
|
250
260
|
top: 0,
|
|
@@ -46,8 +46,11 @@ import {
|
|
|
46
46
|
} from "../chrome/EditorFocusContext";
|
|
47
47
|
import {
|
|
48
48
|
addMention as addMentionTo,
|
|
49
|
+
addPointer as addPointerTo,
|
|
49
50
|
buildChatContext,
|
|
50
51
|
removeMention as removeMentionFrom,
|
|
52
|
+
removePointer as removePointerFrom,
|
|
53
|
+
type ChatPointer,
|
|
51
54
|
type ChatRequestContext,
|
|
52
55
|
} from "./chat-context";
|
|
53
56
|
import {
|
|
@@ -85,6 +88,12 @@ export type ChatUIValue = {
|
|
|
85
88
|
mentions: DocRef[];
|
|
86
89
|
addMention: (ref: DocRef) => void;
|
|
87
90
|
removeMention: (ref: DocRef) => void;
|
|
91
|
+
/** Elements pointed at on the host site. Only the dev bar chat sets `pointer`. */
|
|
92
|
+
pointer: boolean;
|
|
93
|
+
pointers: ChatPointer[];
|
|
94
|
+
addPointer: (pointer: ChatPointer) => void;
|
|
95
|
+
removePointer: (pointer: ChatPointer) => void;
|
|
96
|
+
clearPointers: () => void;
|
|
88
97
|
includeFocus: boolean;
|
|
89
98
|
setIncludeFocus: (v: boolean) => void;
|
|
90
99
|
focus: EditorFocus | null;
|
|
@@ -171,10 +180,13 @@ function refFromInput(input: unknown): DocRef | null {
|
|
|
171
180
|
export function ChatProvider({
|
|
172
181
|
workspaceId,
|
|
173
182
|
startOpen = false,
|
|
183
|
+
pointer = false,
|
|
174
184
|
children,
|
|
175
185
|
}: {
|
|
176
186
|
workspaceId: string;
|
|
177
187
|
startOpen?: boolean;
|
|
188
|
+
/** Dev bar chat can point at elements on the host page. */
|
|
189
|
+
pointer?: boolean;
|
|
178
190
|
children: React.ReactNode;
|
|
179
191
|
}) {
|
|
180
192
|
const [panel, dispatch] = useReducer(
|
|
@@ -189,6 +201,7 @@ export function ChatProvider({
|
|
|
189
201
|
const [freshThread, setFreshThread] = useState(false);
|
|
190
202
|
const [pendingText, setPendingText] = useState<string | null>(null);
|
|
191
203
|
const [mentions, setMentions] = useState<DocRef[]>([]);
|
|
204
|
+
const [pointers, setPointers] = useState<ChatPointer[]>([]);
|
|
192
205
|
const [includeFocus, setIncludeFocus] = useState(true);
|
|
193
206
|
const [threadsVersion, setThreadsVersion] = useState(0);
|
|
194
207
|
const { focus } = useEditorFocus();
|
|
@@ -223,6 +236,7 @@ export function ChatProvider({
|
|
|
223
236
|
setFreshThread(false);
|
|
224
237
|
setPendingText(null);
|
|
225
238
|
setMentions([]);
|
|
239
|
+
setPointers([]);
|
|
226
240
|
setIncludeFocus(true);
|
|
227
241
|
}, [workspaceId]);
|
|
228
242
|
|
|
@@ -246,6 +260,7 @@ export function ChatProvider({
|
|
|
246
260
|
setFreshThread(false);
|
|
247
261
|
setPendingText(null);
|
|
248
262
|
setMentions([]);
|
|
263
|
+
setPointers([]);
|
|
249
264
|
dispatch({ type: "showChat" });
|
|
250
265
|
}, []);
|
|
251
266
|
|
|
@@ -255,6 +270,13 @@ export function ChatProvider({
|
|
|
255
270
|
const removeMention = useCallback((ref: DocRef) => {
|
|
256
271
|
setMentions((prev) => removeMentionFrom(prev, ref));
|
|
257
272
|
}, []);
|
|
273
|
+
const addPointer = useCallback((next: ChatPointer) => {
|
|
274
|
+
setPointers((prev) => addPointerTo(prev, next));
|
|
275
|
+
}, []);
|
|
276
|
+
const removePointer = useCallback((next: ChatPointer) => {
|
|
277
|
+
setPointers((prev) => removePointerFrom(prev, next));
|
|
278
|
+
}, []);
|
|
279
|
+
const clearPointers = useCallback(() => setPointers([]), []);
|
|
258
280
|
const bumpThreads = useCallback(() => setThreadsVersion((v) => v + 1), []);
|
|
259
281
|
|
|
260
282
|
// Context getter kept in a ref so the session never re-renders because of it.
|
|
@@ -262,8 +284,9 @@ export function ChatProvider({
|
|
|
262
284
|
focusDoc: null,
|
|
263
285
|
selection: null,
|
|
264
286
|
mentions: [],
|
|
287
|
+
pointers: [],
|
|
265
288
|
}));
|
|
266
|
-
contextRef.current = () => buildChatContext({ focus, includeFocus, mentions });
|
|
289
|
+
contextRef.current = () => buildChatContext({ focus, includeFocus, mentions, pointers });
|
|
267
290
|
const modelRef = useRef<string | null>(model);
|
|
268
291
|
modelRef.current = model;
|
|
269
292
|
|
|
@@ -295,6 +318,11 @@ export function ChatProvider({
|
|
|
295
318
|
mentions,
|
|
296
319
|
addMention,
|
|
297
320
|
removeMention,
|
|
321
|
+
pointer,
|
|
322
|
+
pointers,
|
|
323
|
+
addPointer,
|
|
324
|
+
removePointer,
|
|
325
|
+
clearPointers,
|
|
298
326
|
includeFocus,
|
|
299
327
|
setIncludeFocus,
|
|
300
328
|
focus,
|
|
@@ -315,6 +343,11 @@ export function ChatProvider({
|
|
|
315
343
|
mentions,
|
|
316
344
|
addMention,
|
|
317
345
|
removeMention,
|
|
346
|
+
pointer,
|
|
347
|
+
pointers,
|
|
348
|
+
addPointer,
|
|
349
|
+
removePointer,
|
|
350
|
+
clearPointers,
|
|
318
351
|
includeFocus,
|
|
319
352
|
focus,
|
|
320
353
|
threadsVersion,
|
|
@@ -539,9 +572,13 @@ function LiveSession({
|
|
|
539
572
|
|
|
540
573
|
const send = useCallback(
|
|
541
574
|
async (text: string) => {
|
|
575
|
+
const context = getContext();
|
|
542
576
|
await sendMessage(
|
|
543
|
-
{
|
|
544
|
-
|
|
577
|
+
{
|
|
578
|
+
text,
|
|
579
|
+
metadata: context.pointers.length ? { pointers: context.pointers } : undefined,
|
|
580
|
+
},
|
|
581
|
+
{ body: { model: getModel(), context } },
|
|
545
582
|
);
|
|
546
583
|
},
|
|
547
584
|
[sendMessage, getModel, getContext],
|
package/src/chat/Composer.tsx
CHANGED
|
@@ -7,8 +7,9 @@ import { Tooltip } from "../ui/Tooltip";
|
|
|
7
7
|
import { useChatSession, useChatUI } from "./ChatProvider";
|
|
8
8
|
import { MentionPopover, type MentionKey } from "./MentionPopover";
|
|
9
9
|
import { ModelPicker } from "./ModelPicker";
|
|
10
|
-
import { AtIcon, CloseIcon, FileIcon, FocusIcon, SendIcon, StopIcon } from "./icons";
|
|
10
|
+
import { AtIcon, CloseIcon, FileIcon, FocusIcon, PointerIcon, SendIcon, StopIcon } from "./icons";
|
|
11
11
|
import { mentionQueryAt } from "./chat-helpers";
|
|
12
|
+
import { PointerChip, pointerKey } from "./PointerChip";
|
|
12
13
|
|
|
13
14
|
const hover = "@media (hover: hover)";
|
|
14
15
|
|
|
@@ -22,11 +23,22 @@ function isMentionKey(key: string): key is MentionKey {
|
|
|
22
23
|
return MENTION_KEYS.has(key);
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
export const POINTER_ARM_EVENT = "backmatter:pointer-arm";
|
|
27
|
+
|
|
28
|
+
function postPointer(on: boolean) {
|
|
29
|
+
if (typeof window === "undefined") return;
|
|
30
|
+
window.dispatchEvent(new CustomEvent(POINTER_ARM_EVENT, { detail: { on } }));
|
|
31
|
+
if (window.parent !== window) {
|
|
32
|
+
window.parent.postMessage({ type: "backmatter:pointer", on }, location.origin);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
25
36
|
export function Composer({ autoFocus }: { autoFocus: boolean }) {
|
|
26
37
|
const ui = useChatUI();
|
|
27
38
|
const session = useChatSession();
|
|
28
39
|
const [text, setText] = useState("");
|
|
29
40
|
const [mention, setMention] = useState<{ start: number; query: string } | null>(null);
|
|
41
|
+
const [pointing, setPointing] = useState(false);
|
|
30
42
|
const ref = useRef<HTMLTextAreaElement>(null);
|
|
31
43
|
const boxRef = useRef<HTMLDivElement>(null);
|
|
32
44
|
const mentionKeys = useRef<((key: MentionKey) => void) | null>(null);
|
|
@@ -71,13 +83,40 @@ export function Composer({ autoFocus }: { autoFocus: boolean }) {
|
|
|
71
83
|
return () => window.removeEventListener("backmatter:chat-prefill", onPrefill);
|
|
72
84
|
}, []);
|
|
73
85
|
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (!ui.pointer) return;
|
|
88
|
+
function disarm() {
|
|
89
|
+
setPointing(false);
|
|
90
|
+
}
|
|
91
|
+
function onArm(event: Event) {
|
|
92
|
+
if ((event as CustomEvent<{ on?: boolean }>).detail?.on === false) disarm();
|
|
93
|
+
}
|
|
94
|
+
function onMessage(event: MessageEvent) {
|
|
95
|
+
if (event.source !== window.parent || event.origin !== location.origin) return;
|
|
96
|
+
const data = event.data as { type?: string; on?: boolean };
|
|
97
|
+
if (data?.type !== "backmatter:pointer" || data.on !== false) return;
|
|
98
|
+
disarm();
|
|
99
|
+
}
|
|
100
|
+
window.addEventListener(POINTER_ARM_EVENT, onArm);
|
|
101
|
+
window.addEventListener("message", onMessage);
|
|
102
|
+
return () => {
|
|
103
|
+
window.removeEventListener(POINTER_ARM_EVENT, onArm);
|
|
104
|
+
window.removeEventListener("message", onMessage);
|
|
105
|
+
};
|
|
106
|
+
}, [ui.pointer]);
|
|
107
|
+
|
|
74
108
|
const submit = useCallback(() => {
|
|
75
109
|
const value = text.trim();
|
|
76
110
|
if (!value || busy) return;
|
|
77
111
|
setText("");
|
|
78
112
|
setMention(null);
|
|
113
|
+
if (pointing) {
|
|
114
|
+
setPointing(false);
|
|
115
|
+
postPointer(false);
|
|
116
|
+
}
|
|
79
117
|
void session.send(value);
|
|
80
|
-
|
|
118
|
+
ui.clearPointers();
|
|
119
|
+
}, [text, busy, session, ui, pointing]);
|
|
81
120
|
|
|
82
121
|
function updateMention(el: HTMLTextAreaElement) {
|
|
83
122
|
setMention(mentionQueryAt(el.value, el.selectionStart ?? el.value.length));
|
|
@@ -141,6 +180,17 @@ export function Composer({ autoFocus }: { autoFocus: boolean }) {
|
|
|
141
180
|
</div>
|
|
142
181
|
) : null}
|
|
143
182
|
<div ref={boxRef} {...stylex.props(styles.box)}>
|
|
183
|
+
{ui.pointers.length > 0 ? (
|
|
184
|
+
<div {...stylex.props(styles.chips)} aria-label="Pointed elements">
|
|
185
|
+
{ui.pointers.map((pointer) => (
|
|
186
|
+
<PointerChip
|
|
187
|
+
key={pointerKey(pointer)}
|
|
188
|
+
pointer={pointer}
|
|
189
|
+
onRemove={() => ui.removePointer(pointer)}
|
|
190
|
+
/>
|
|
191
|
+
))}
|
|
192
|
+
</div>
|
|
193
|
+
) : null}
|
|
144
194
|
<textarea
|
|
145
195
|
ref={ref}
|
|
146
196
|
value={text}
|
|
@@ -170,6 +220,11 @@ export function Composer({ autoFocus }: { autoFocus: boolean }) {
|
|
|
170
220
|
e.preventDefault();
|
|
171
221
|
submit();
|
|
172
222
|
} else if (e.key === "Escape") {
|
|
223
|
+
if (pointing) {
|
|
224
|
+
setPointing(false);
|
|
225
|
+
postPointer(false);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
173
228
|
ui.dispatch({ type: "minimize" });
|
|
174
229
|
}
|
|
175
230
|
}}
|
|
@@ -222,6 +277,24 @@ export function Composer({ autoFocus }: { autoFocus: boolean }) {
|
|
|
222
277
|
</button>
|
|
223
278
|
</Tooltip>
|
|
224
279
|
) : null}
|
|
280
|
+
{ui.pointer ? (
|
|
281
|
+
<Tooltip label={pointing ? "Stop pointing" : "Point at an element on the page"}>
|
|
282
|
+
<button
|
|
283
|
+
type="button"
|
|
284
|
+
aria-label="Point at an element on the page"
|
|
285
|
+
aria-pressed={pointing}
|
|
286
|
+
disabled={ui.aiAvailable !== true}
|
|
287
|
+
onClick={() => {
|
|
288
|
+
const next = !pointing;
|
|
289
|
+
setPointing(next);
|
|
290
|
+
postPointer(next);
|
|
291
|
+
}}
|
|
292
|
+
{...stylex.props(styles.iconBtn, pointing && styles.iconBtnActive)}
|
|
293
|
+
>
|
|
294
|
+
<PointerIcon />
|
|
295
|
+
</button>
|
|
296
|
+
</Tooltip>
|
|
297
|
+
) : null}
|
|
225
298
|
<ModelPicker />
|
|
226
299
|
{busy ? (
|
|
227
300
|
<Tooltip label="Stop generating">
|
|
@@ -257,13 +330,15 @@ function ContextRow({
|
|
|
257
330
|
icon,
|
|
258
331
|
type,
|
|
259
332
|
slug,
|
|
333
|
+
label,
|
|
260
334
|
meta,
|
|
261
335
|
removeLabel,
|
|
262
336
|
onRemove,
|
|
263
337
|
}: {
|
|
264
338
|
icon: React.ReactNode;
|
|
265
|
-
type
|
|
266
|
-
slug
|
|
339
|
+
type?: string;
|
|
340
|
+
slug?: string;
|
|
341
|
+
label?: string;
|
|
267
342
|
meta?: string;
|
|
268
343
|
removeLabel: string;
|
|
269
344
|
onRemove: () => void;
|
|
@@ -272,8 +347,14 @@ function ContextRow({
|
|
|
272
347
|
<div {...stylex.props(styles.row)}>
|
|
273
348
|
<span {...stylex.props(styles.rowIcon)}>{icon}</span>
|
|
274
349
|
<span {...stylex.props(styles.rowText)}>
|
|
275
|
-
|
|
276
|
-
|
|
350
|
+
{label ? (
|
|
351
|
+
<span {...stylex.props(styles.rowSlug)}>{label}</span>
|
|
352
|
+
) : (
|
|
353
|
+
<>
|
|
354
|
+
<span {...stylex.props(styles.rowType)}>{type}/</span>
|
|
355
|
+
<span {...stylex.props(styles.rowSlug)}>{slug}</span>
|
|
356
|
+
</>
|
|
357
|
+
)}
|
|
277
358
|
</span>
|
|
278
359
|
{meta ? <span {...stylex.props(styles.rowMeta)}>{meta}</span> : null}
|
|
279
360
|
<button
|
|
@@ -364,6 +445,11 @@ const styles = stylex.create({
|
|
|
364
445
|
":hover": { backgroundColor: colors.bgHover, color: colors.text },
|
|
365
446
|
},
|
|
366
447
|
},
|
|
448
|
+
chips: {
|
|
449
|
+
display: "flex",
|
|
450
|
+
flexWrap: "wrap",
|
|
451
|
+
gap: space.sm,
|
|
452
|
+
},
|
|
367
453
|
box: {
|
|
368
454
|
position: "relative",
|
|
369
455
|
zIndex: 2,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// packages/studio/src/chat/DocLink.tsx
|
|
2
|
+
// A document name that opens in the Studio router. The on-site widget has no
|
|
3
|
+
// router, so the same label renders as text and the chat can still show it.
|
|
4
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
5
|
+
import { Link, useInRouterContext } from "react-router";
|
|
6
|
+
|
|
7
|
+
export function DocLink({
|
|
8
|
+
to,
|
|
9
|
+
children,
|
|
10
|
+
className,
|
|
11
|
+
style,
|
|
12
|
+
}: {
|
|
13
|
+
to: string;
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
className?: string;
|
|
16
|
+
style?: CSSProperties;
|
|
17
|
+
}) {
|
|
18
|
+
const routed = useInRouterContext();
|
|
19
|
+
if (!routed) {
|
|
20
|
+
return (
|
|
21
|
+
<span className={className} style={style}>
|
|
22
|
+
{children}
|
|
23
|
+
</span>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
return (
|
|
27
|
+
<Link to={to} className={className} style={style}>
|
|
28
|
+
{children}
|
|
29
|
+
</Link>
|
|
30
|
+
);
|
|
31
|
+
}
|
package/src/chat/DocListCard.tsx
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
// into the editor. Content models are user-defined, so rows lean on the
|
|
5
5
|
// server-derived `title` and fall back to the slug.
|
|
6
6
|
import { useState } from "react";
|
|
7
|
-
import { Link } from "react-router";
|
|
8
7
|
import * as stylex from "@stylexjs/stylex";
|
|
9
8
|
import { colors, radius, space, type } from "../styles/tokens.stylex";
|
|
10
9
|
import { contentDocPath } from "../workspace-path";
|
|
11
10
|
import { useWorkspaceNav } from "../chrome/WorkspaceNavContext";
|
|
11
|
+
import { DocLink } from "./DocLink";
|
|
12
12
|
import { ChevronDownIcon, FileIcon } from "./icons";
|
|
13
13
|
|
|
14
14
|
const hover = "@media (hover: hover)";
|
|
@@ -130,7 +130,7 @@ function DocRow({
|
|
|
130
130
|
const secondary = doc.path ?? `${doc.type}/${doc.slug}`;
|
|
131
131
|
|
|
132
132
|
return (
|
|
133
|
-
<
|
|
133
|
+
<DocLink to={to} {...stylex.props(styles.row)}>
|
|
134
134
|
<span {...stylex.props(styles.line)}>
|
|
135
135
|
<span {...stylex.props(styles.icon)}>
|
|
136
136
|
<FileIcon size={13} />
|
|
@@ -153,7 +153,7 @@ function DocRow({
|
|
|
153
153
|
</span>
|
|
154
154
|
</span>
|
|
155
155
|
{doc.excerpt ? <span {...stylex.props(styles.excerpt)}>{doc.excerpt}</span> : null}
|
|
156
|
-
</
|
|
156
|
+
</DocLink>
|
|
157
157
|
);
|
|
158
158
|
}
|
|
159
159
|
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// packages/studio/src/chat/PointerChip.tsx
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
import * as stylex from "@stylexjs/stylex";
|
|
4
|
+
import { colors, space, type } from "../styles/tokens.stylex";
|
|
5
|
+
import { findPointedElement, highlightPointedElement } from "../site-pointer";
|
|
6
|
+
import type { ChatPointer } from "./chat-context";
|
|
7
|
+
import { CloseIcon, PointerIcon } from "./icons";
|
|
8
|
+
|
|
9
|
+
const hover = "@media (hover: hover)";
|
|
10
|
+
|
|
11
|
+
export function pointerKey(pointer: ChatPointer): string {
|
|
12
|
+
return `${pointer.locale}/${pointer.type}/${pointer.slug}/${pointer.blockId ?? ""}/${pointer.quote}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function pointerLabel(pointer: ChatPointer): string {
|
|
16
|
+
const quote = pointer.quote.replace(/\s+/g, " ").trim();
|
|
17
|
+
const short = quote.length > 42 ? `${quote.slice(0, 41)}…` : quote;
|
|
18
|
+
if (pointer.blockType && short) return `${pointer.blockType} · “${short}”`;
|
|
19
|
+
if (pointer.blockType) return pointer.blockType;
|
|
20
|
+
return short ? `“${short}”` : "element";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function pointerChipLabel(pointer: ChatPointer): string {
|
|
24
|
+
const quote = pointer.quote.replace(/\s+/g, " ").trim();
|
|
25
|
+
const raw = quote || pointer.blockType || `${pointer.type}/${pointer.slug}`;
|
|
26
|
+
return raw.length > 32 ? `${raw.slice(0, 31)}…` : raw;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function canHighlightHere(): boolean {
|
|
30
|
+
return typeof window !== "undefined" && window.parent === window;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function PointerChip({
|
|
34
|
+
pointer,
|
|
35
|
+
onRemove,
|
|
36
|
+
}: {
|
|
37
|
+
pointer: ChatPointer;
|
|
38
|
+
onRemove?: () => void;
|
|
39
|
+
}) {
|
|
40
|
+
const label = pointerChipLabel(pointer);
|
|
41
|
+
const detail = pointerLabel(pointer);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
return () => {
|
|
45
|
+
if (canHighlightHere()) highlightPointedElement(document, null);
|
|
46
|
+
};
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
function show() {
|
|
50
|
+
if (!canHighlightHere()) return;
|
|
51
|
+
highlightPointedElement(document, findPointedElement(document, pointer));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function hide() {
|
|
55
|
+
if (!canHighlightHere()) return;
|
|
56
|
+
highlightPointedElement(document, null);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<span
|
|
61
|
+
{...stylex.props(styles.chip, !onRemove && styles.chipStatic)}
|
|
62
|
+
title={`${detail} · ${pointer.type}/${pointer.slug}`}
|
|
63
|
+
onMouseEnter={show}
|
|
64
|
+
onMouseLeave={hide}
|
|
65
|
+
>
|
|
66
|
+
<span {...stylex.props(styles.chipIcon)}>
|
|
67
|
+
<PointerIcon size={13} />
|
|
68
|
+
</span>
|
|
69
|
+
<span {...stylex.props(styles.chipLabel)}>{label}</span>
|
|
70
|
+
{onRemove ? (
|
|
71
|
+
<button
|
|
72
|
+
type="button"
|
|
73
|
+
aria-label={`Remove ${detail}`}
|
|
74
|
+
onClick={onRemove}
|
|
75
|
+
{...stylex.props(styles.chipRemove)}
|
|
76
|
+
>
|
|
77
|
+
<CloseIcon size={11} />
|
|
78
|
+
</button>
|
|
79
|
+
) : null}
|
|
80
|
+
</span>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const styles = stylex.create({
|
|
85
|
+
chip: {
|
|
86
|
+
display: "inline-flex",
|
|
87
|
+
alignItems: "center",
|
|
88
|
+
gap: space.xs,
|
|
89
|
+
maxWidth: "100%",
|
|
90
|
+
height: 26,
|
|
91
|
+
paddingLeft: space.sm,
|
|
92
|
+
paddingRight: 2,
|
|
93
|
+
borderWidth: 1,
|
|
94
|
+
borderStyle: "solid",
|
|
95
|
+
borderColor: colors.border,
|
|
96
|
+
borderRadius: 8,
|
|
97
|
+
backgroundColor: colors.bgSurface,
|
|
98
|
+
color: colors.text,
|
|
99
|
+
fontSize: type.sm,
|
|
100
|
+
},
|
|
101
|
+
chipStatic: {
|
|
102
|
+
paddingRight: space.sm,
|
|
103
|
+
},
|
|
104
|
+
chipIcon: {
|
|
105
|
+
display: "inline-flex",
|
|
106
|
+
flexShrink: 0,
|
|
107
|
+
color: colors.textMuted,
|
|
108
|
+
},
|
|
109
|
+
chipLabel: {
|
|
110
|
+
minWidth: 0,
|
|
111
|
+
overflow: "hidden",
|
|
112
|
+
textOverflow: "ellipsis",
|
|
113
|
+
whiteSpace: "nowrap",
|
|
114
|
+
fontWeight: 500,
|
|
115
|
+
},
|
|
116
|
+
chipRemove: {
|
|
117
|
+
display: "inline-flex",
|
|
118
|
+
alignItems: "center",
|
|
119
|
+
justifyContent: "center",
|
|
120
|
+
flexShrink: 0,
|
|
121
|
+
width: 22,
|
|
122
|
+
height: 22,
|
|
123
|
+
padding: 0,
|
|
124
|
+
borderWidth: 0,
|
|
125
|
+
borderRadius: 6,
|
|
126
|
+
backgroundColor: "transparent",
|
|
127
|
+
color: colors.textMuted,
|
|
128
|
+
cursor: "pointer",
|
|
129
|
+
[hover]: {
|
|
130
|
+
":hover": { backgroundColor: colors.bgHover, color: colors.text },
|
|
131
|
+
},
|
|
132
|
+
":focus-visible": {
|
|
133
|
+
outlineWidth: 2,
|
|
134
|
+
outlineStyle: "solid",
|
|
135
|
+
outlineColor: colors.focus,
|
|
136
|
+
outlineOffset: 1,
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
});
|