@jxsuite/studio 0.28.5 → 0.29.0
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/studio.js +6769 -5399
- package/dist/studio.js.map +86 -79
- package/package.json +5 -4
- package/src/browse/browse-modal.ts +2 -2
- package/src/browse/browse.ts +20 -19
- package/src/canvas/canvas-diff.ts +3 -3
- package/src/canvas/canvas-helpers.ts +15 -2
- package/src/canvas/canvas-live-render.ts +168 -86
- package/src/canvas/canvas-patcher.ts +656 -0
- package/src/canvas/canvas-perf.ts +68 -0
- package/src/canvas/canvas-render.ts +135 -23
- package/src/canvas/canvas-subtree-render.ts +113 -0
- package/src/canvas/canvas-utils.ts +4 -0
- package/src/editor/component-inline-edit.ts +4 -35
- package/src/editor/context-menu.ts +92 -74
- package/src/editor/convert-to-component.ts +11 -2
- package/src/editor/convert-to-repeater.ts +4 -5
- package/src/editor/inline-edit.ts +9 -9
- package/src/editor/inline-format.ts +11 -11
- package/src/editor/merge-tags.ts +120 -0
- package/src/editor/shortcuts.ts +4 -4
- package/src/files/components.ts +37 -0
- package/src/files/file-ops.ts +28 -7
- package/src/files/files.ts +25 -21
- package/src/github/github-auth.ts +14 -2
- package/src/github/github-publish.ts +12 -2
- package/src/panels/activity-bar.ts +1 -1
- package/src/panels/ai-panel.ts +67 -39
- package/src/panels/block-action-bar.ts +72 -1
- package/src/panels/canvas-dnd.ts +55 -26
- package/src/panels/data-explorer.ts +5 -3
- package/src/panels/dnd.ts +12 -15
- package/src/panels/editors.ts +5 -23
- package/src/panels/elements-panel.ts +2 -12
- package/src/panels/events-panel.ts +1 -1
- package/src/panels/git-panel.ts +6 -6
- package/src/panels/head-panel.ts +1 -1
- package/src/panels/layers-panel.ts +177 -147
- package/src/panels/preview-render.ts +15 -0
- package/src/panels/properties-panel.ts +16 -27
- package/src/panels/quick-search.ts +2 -2
- package/src/panels/right-panel.ts +2 -6
- package/src/panels/shared.ts +3 -0
- package/src/panels/signals-panel.ts +24 -22
- package/src/panels/statusbar.ts +15 -6
- package/src/panels/style-panel.ts +3 -3
- package/src/panels/style-utils.ts +3 -3
- package/src/panels/stylebook-panel.ts +4 -4
- package/src/panels/tab-bar.ts +198 -0
- package/src/panels/tab-strip.ts +2 -2
- package/src/panels/toolbar.ts +6 -75
- package/src/platforms/devserver.ts +60 -34
- package/src/reactivity.ts +2 -0
- package/src/services/cem-export.ts +23 -2
- package/src/services/code-services.ts +16 -2
- package/src/services/monaco-setup.ts +2 -1
- package/src/settings/content-types-editor.ts +16 -16
- package/src/settings/css-vars-editor.ts +2 -2
- package/src/settings/defs-editor.ts +14 -14
- package/src/settings/general-settings.ts +6 -6
- package/src/settings/head-editor.ts +2 -2
- package/src/site-context.ts +1 -1
- package/src/state.ts +66 -12
- package/src/store.ts +15 -3
- package/src/studio.ts +73 -33
- package/src/tabs/patch-ops.ts +143 -0
- package/src/tabs/tab.ts +15 -1
- package/src/tabs/transact.ts +454 -27
- package/src/types.ts +41 -0
- package/src/ui/color-selector.ts +7 -7
- package/src/ui/icons.ts +0 -30
- package/src/ui/media-picker.ts +2 -2
- package/src/ui/panel-resize.ts +6 -1
- package/src/ui/unit-selector.ts +2 -2
- package/src/ui/value-selector.ts +3 -3
- package/src/utils/canvas-media.ts +6 -6
- package/src/utils/edit-display.ts +125 -83
- package/src/utils/google-fonts.ts +1 -1
- package/src/utils/inherited-style.ts +3 -2
- package/src/utils/studio-utils.ts +1 -1
- package/src/view.ts +4 -1
package/src/files/file-ops.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { locateDocument } from "../services/code-services";
|
|
11
11
|
import { errorMessage } from "@jxsuite/schema/parse";
|
|
12
12
|
import { statusMessage } from "../panels/statusbar";
|
|
13
|
+
import { validateComponentSlots } from "../services/cem-export";
|
|
13
14
|
import { getPlatform } from "../platform";
|
|
14
15
|
import { activeTab, openTab } from "../workspace/workspace";
|
|
15
16
|
import { isEditing, stopEditing } from "../editor/inline-edit";
|
|
@@ -94,7 +95,7 @@ export async function openFile() {
|
|
|
94
95
|
sourceFormat: format.name,
|
|
95
96
|
});
|
|
96
97
|
} else if (name.endsWith(".json")) {
|
|
97
|
-
const document = JSON.parse(text)
|
|
98
|
+
const document = JSON.parse(text) as Record<string, unknown>;
|
|
98
99
|
openTab({ document, documentPath, fileHandle: handle, id: name });
|
|
99
100
|
} else {
|
|
100
101
|
throw noFormatError(name);
|
|
@@ -108,8 +109,8 @@ export async function openFile() {
|
|
|
108
109
|
showOpenFilePicker: (options?: unknown) => Promise<FileSystemFileHandle[]>;
|
|
109
110
|
}
|
|
110
111
|
).showOpenFilePicker({ types: pickerTypes });
|
|
111
|
-
const file = await handle
|
|
112
|
-
await handleSource(handle
|
|
112
|
+
const file = await handle!.getFile();
|
|
113
|
+
await handleSource(handle!.name, await file.text(), handle!);
|
|
113
114
|
} else {
|
|
114
115
|
// Fallback: file input
|
|
115
116
|
const input = document.createElement("input");
|
|
@@ -131,6 +132,26 @@ export async function openFile() {
|
|
|
131
132
|
}
|
|
132
133
|
}
|
|
133
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Report a slot-validation warning for component documents (root tagName with a hyphen). The save
|
|
137
|
+
* still proceeds — the warning replaces the plain success message.
|
|
138
|
+
*
|
|
139
|
+
* @param {Tab} tab
|
|
140
|
+
* @param {string} savedMsg
|
|
141
|
+
*/
|
|
142
|
+
function savedMessage(tab: Tab, savedMsg: string) {
|
|
143
|
+
const doc = tab.doc.document;
|
|
144
|
+
const warning =
|
|
145
|
+
typeof doc.tagName === "string" && doc.tagName.includes("-")
|
|
146
|
+
? validateComponentSlots(doc)
|
|
147
|
+
: null;
|
|
148
|
+
if (warning) {
|
|
149
|
+
statusMessage(`${savedMsg} — Warning: ${warning}`, 6000);
|
|
150
|
+
} else {
|
|
151
|
+
statusMessage(savedMsg);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
134
155
|
/** Save the current document back to its source location. */
|
|
135
156
|
export async function saveFile() {
|
|
136
157
|
if (isEditing()) {
|
|
@@ -147,7 +168,7 @@ export async function saveFile() {
|
|
|
147
168
|
const platform = getPlatform();
|
|
148
169
|
await platform.writeFile(tab.documentPath, output);
|
|
149
170
|
tab.doc.dirty = false;
|
|
150
|
-
|
|
171
|
+
savedMessage(tab, "Saved");
|
|
151
172
|
} else if (tab.fileHandle && "createWritable" in tab.fileHandle) {
|
|
152
173
|
const writable =
|
|
153
174
|
await /**
|
|
@@ -161,7 +182,7 @@ export async function saveFile() {
|
|
|
161
182
|
await writable.write(output);
|
|
162
183
|
await writable.close();
|
|
163
184
|
tab.doc.dirty = false;
|
|
164
|
-
|
|
185
|
+
savedMessage(tab, "Saved");
|
|
165
186
|
} else {
|
|
166
187
|
statusMessage("No save target — use Export");
|
|
167
188
|
}
|
|
@@ -209,7 +230,7 @@ export async function exportFile() {
|
|
|
209
230
|
await writable.write(output);
|
|
210
231
|
await writable.close();
|
|
211
232
|
tab.doc.dirty = false;
|
|
212
|
-
|
|
233
|
+
savedMessage(tab, `Exported as ${handle.name}`);
|
|
213
234
|
} else {
|
|
214
235
|
// Fallback: download
|
|
215
236
|
const blob = new Blob([output], { type: mimeType });
|
|
@@ -220,7 +241,7 @@ export async function exportFile() {
|
|
|
220
241
|
a.click();
|
|
221
242
|
URL.revokeObjectURL(url);
|
|
222
243
|
tab.doc.dirty = false;
|
|
223
|
-
|
|
244
|
+
savedMessage(tab, "Downloaded");
|
|
224
245
|
}
|
|
225
246
|
} catch (error) {
|
|
226
247
|
if (!(error instanceof Error && error.name === "AbortError")) {
|
package/src/files/files.ts
CHANGED
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
import { view } from "../view";
|
|
42
42
|
import { addRecentProject, trackRecentFile } from "../recent-projects";
|
|
43
43
|
import type { TemplateResult } from "lit-html";
|
|
44
|
+
import type { JxMutableNode } from "@jxsuite/schema/types";
|
|
44
45
|
import type { StudioState } from "../state.js";
|
|
45
46
|
import type { Tab } from "../tabs/tab.js";
|
|
46
47
|
|
|
@@ -339,7 +340,7 @@ function renderTreeLevelTemplate(
|
|
|
339
340
|
): TemplateResult | TemplateResult[] {
|
|
340
341
|
const entries = requireProjectState().dirs.get(dirPath);
|
|
341
342
|
if (!entries) {
|
|
342
|
-
loadDirectory(dirPath).then(() => ctx.renderLeftPanel());
|
|
343
|
+
void loadDirectory(dirPath).then(() => ctx.renderLeftPanel());
|
|
343
344
|
return html`<div
|
|
344
345
|
class="file-tree-item"
|
|
345
346
|
style="padding-left:${8 + depth * 16}px;color:var(--fg-dim);font-style:italic"
|
|
@@ -427,13 +428,13 @@ export function setupTreeKeyboard(tree: HTMLElement) {
|
|
|
427
428
|
switch (e.key) {
|
|
428
429
|
case "ArrowDown": {
|
|
429
430
|
if (idx < items.length - 1) {
|
|
430
|
-
items[idx + 1]
|
|
431
|
+
items[idx + 1]!.focus();
|
|
431
432
|
}
|
|
432
433
|
break;
|
|
433
434
|
}
|
|
434
435
|
case "ArrowUp": {
|
|
435
436
|
if (idx > 0) {
|
|
436
|
-
items[idx - 1]
|
|
437
|
+
items[idx - 1]!.focus();
|
|
437
438
|
}
|
|
438
439
|
break;
|
|
439
440
|
}
|
|
@@ -442,7 +443,7 @@ export function setupTreeKeyboard(tree: HTMLElement) {
|
|
|
442
443
|
const path = focused.dataset.path as string;
|
|
443
444
|
if (!requireProjectState().expanded.has(path)) {
|
|
444
445
|
requireProjectState().expanded.add(path);
|
|
445
|
-
loadDirectory(path).then(() => {
|
|
446
|
+
void loadDirectory(path).then(() => {
|
|
446
447
|
const panel = tree.closest(".panel-body");
|
|
447
448
|
if (panel) {
|
|
448
449
|
(panel.querySelector(".file-tree-item:focus") as HTMLElement | null)?.click();
|
|
@@ -619,7 +620,7 @@ export function registerFileTreeDnD({ renderLeftPanel }: { renderLeftPanel: () =
|
|
|
619
620
|
return;
|
|
620
621
|
}
|
|
621
622
|
|
|
622
|
-
moveFileEntry(srcPath, newPath!, renderLeftPanel);
|
|
623
|
+
void moveFileEntry(srcPath, newPath!, renderLeftPanel);
|
|
623
624
|
},
|
|
624
625
|
});
|
|
625
626
|
_fileTreeDndCleanups.push(monitorCleanup);
|
|
@@ -707,16 +708,18 @@ function showFileContextMenu(
|
|
|
707
708
|
label: "New File\u2026",
|
|
708
709
|
});
|
|
709
710
|
}
|
|
710
|
-
items.push(
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
711
|
+
items.push(
|
|
712
|
+
{ label: "\u2014" },
|
|
713
|
+
{
|
|
714
|
+
action: () => renameFile(entry, ctx.renderLeftPanel),
|
|
715
|
+
label: "Rename\u2026",
|
|
716
|
+
},
|
|
717
|
+
{
|
|
718
|
+
action: () => deleteFile(entry, ctx.renderLeftPanel),
|
|
719
|
+
danger: true,
|
|
720
|
+
label: "Delete",
|
|
721
|
+
},
|
|
722
|
+
);
|
|
720
723
|
|
|
721
724
|
let x = e.clientX,
|
|
722
725
|
y = e.clientY;
|
|
@@ -751,7 +754,7 @@ function showFileContextMenu(
|
|
|
751
754
|
style=${item.danger ? "color: var(--danger)" : ""}
|
|
752
755
|
@click=${() => {
|
|
753
756
|
dismissFileContextMenu();
|
|
754
|
-
item.action?.();
|
|
757
|
+
void item.action?.();
|
|
755
758
|
}}
|
|
756
759
|
>${item.label}</sp-menu-item
|
|
757
760
|
>`,
|
|
@@ -953,12 +956,12 @@ export async function openFileFromTree(
|
|
|
953
956
|
}
|
|
954
957
|
|
|
955
958
|
if (formatForPath(path)) {
|
|
956
|
-
|
|
959
|
+
ctx.loadMarkdown(content, null);
|
|
957
960
|
ctx.S.documentPath = path;
|
|
958
961
|
ctx.S.dirty = false;
|
|
959
962
|
ctx.commit(ctx.S);
|
|
960
963
|
} else if (path.endsWith(".json")) {
|
|
961
|
-
const doc = JSON.parse(content);
|
|
964
|
+
const doc = JSON.parse(content) as JxMutableNode;
|
|
962
965
|
const newS = createState(doc);
|
|
963
966
|
newS.documentPath = path;
|
|
964
967
|
newS.dirty = false;
|
|
@@ -999,14 +1002,15 @@ export async function openFileInTab(path: string) {
|
|
|
999
1002
|
}
|
|
1000
1003
|
|
|
1001
1004
|
await loadFormats();
|
|
1002
|
-
let document,
|
|
1005
|
+
let document: Record<string, unknown>;
|
|
1006
|
+
let frontmatter: Record<string, unknown> | undefined;
|
|
1003
1007
|
const format = formatForPath(path);
|
|
1004
1008
|
if (format) {
|
|
1005
1009
|
const result = await parseSourceForPath(path, content);
|
|
1006
1010
|
({ document } = result);
|
|
1007
1011
|
({ frontmatter } = result);
|
|
1008
1012
|
} else if (path.endsWith(".json")) {
|
|
1009
|
-
document = JSON.parse(content)
|
|
1013
|
+
document = JSON.parse(content) as Record<string, unknown>;
|
|
1010
1014
|
} else {
|
|
1011
1015
|
throw noFormatError(path);
|
|
1012
1016
|
}
|
|
@@ -1056,7 +1060,7 @@ export async function reloadFileInTab(path: string) {
|
|
|
1056
1060
|
tab.doc.document = document;
|
|
1057
1061
|
tab.doc.content.frontmatter = frontmatter;
|
|
1058
1062
|
} else if (path.endsWith(".json")) {
|
|
1059
|
-
tab.doc.document = JSON.parse(content);
|
|
1063
|
+
tab.doc.document = JSON.parse(content) as JxMutableNode;
|
|
1060
1064
|
}
|
|
1061
1065
|
tab.doc.dirty = false;
|
|
1062
1066
|
} catch {}
|
|
@@ -12,6 +12,18 @@ import { showDialog } from "../ui/layers";
|
|
|
12
12
|
const CLIENT_ID = "Ov23liYVlMFpgjOEPXJH";
|
|
13
13
|
const STORAGE_KEY = "jx_github_token";
|
|
14
14
|
|
|
15
|
+
interface DeviceCodeResponse {
|
|
16
|
+
device_code: string;
|
|
17
|
+
user_code: string;
|
|
18
|
+
verification_uri: string;
|
|
19
|
+
interval?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface TokenResponse {
|
|
23
|
+
access_token?: string;
|
|
24
|
+
error?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
15
27
|
export function getGithubToken() {
|
|
16
28
|
return localStorage.getItem(STORAGE_KEY);
|
|
17
29
|
}
|
|
@@ -44,7 +56,7 @@ export async function authenticateGithub() {
|
|
|
44
56
|
if (!codeRes.ok) {
|
|
45
57
|
throw new Error("Failed to initiate GitHub device flow");
|
|
46
58
|
}
|
|
47
|
-
const codeData = await codeRes.json();
|
|
59
|
+
const codeData = (await codeRes.json()) as DeviceCodeResponse;
|
|
48
60
|
|
|
49
61
|
const { device_code, user_code, verification_uri, interval = 5 } = codeData;
|
|
50
62
|
|
|
@@ -69,7 +81,7 @@ export async function authenticateGithub() {
|
|
|
69
81
|
},
|
|
70
82
|
method: "POST",
|
|
71
83
|
});
|
|
72
|
-
const tokenData = await tokenRes.json();
|
|
84
|
+
const tokenData = (await tokenRes.json()) as TokenResponse;
|
|
73
85
|
|
|
74
86
|
if (tokenData.access_token) {
|
|
75
87
|
localStorage.setItem(STORAGE_KEY, tokenData.access_token);
|
|
@@ -9,6 +9,16 @@ import { authenticateGithub } from "./github-auth";
|
|
|
9
9
|
import { getPlatform } from "../platform";
|
|
10
10
|
import { statusMessage } from "../panels/statusbar";
|
|
11
11
|
|
|
12
|
+
interface GithubErrorResponse {
|
|
13
|
+
errors?: { message?: string }[];
|
|
14
|
+
message?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface GithubRepoResponse {
|
|
18
|
+
clone_url: string;
|
|
19
|
+
html_url: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
12
22
|
/**
|
|
13
23
|
* Full "Publish to GitHub" flow: 1. Authenticate (or reuse stored token) 2. Prompt for repo name /
|
|
14
24
|
* visibility 3. Create the repo via GitHub API 4. Add remote + push
|
|
@@ -105,13 +115,13 @@ export async function publishToGithub({ projectName }: { projectName: string })
|
|
|
105
115
|
});
|
|
106
116
|
|
|
107
117
|
if (!createRes.ok) {
|
|
108
|
-
const err = await createRes.json();
|
|
118
|
+
const err = (await createRes.json()) as GithubErrorResponse;
|
|
109
119
|
const msg = err.errors?.[0]?.message || err.message || "Failed to create repository";
|
|
110
120
|
statusMessage(`Error: ${msg}`);
|
|
111
121
|
return false;
|
|
112
122
|
}
|
|
113
123
|
|
|
114
|
-
const repo = await createRes.json();
|
|
124
|
+
const repo = (await createRes.json()) as GithubRepoResponse;
|
|
115
125
|
const platform = getPlatform();
|
|
116
126
|
|
|
117
127
|
statusMessage("Setting remote and pushing…");
|
package/src/panels/ai-panel.ts
CHANGED
|
@@ -43,6 +43,14 @@ interface QuikChatInstance {
|
|
|
43
43
|
historyImport: (history: unknown[]) => void;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
type QuikChatCtor = new (
|
|
47
|
+
container: HTMLElement,
|
|
48
|
+
onSend: (chat: unknown, msg: string) => void,
|
|
49
|
+
opts: Record<string, unknown>,
|
|
50
|
+
) => QuikChatInstance;
|
|
51
|
+
|
|
52
|
+
const QuikChat = quikchat as unknown as QuikChatCtor;
|
|
53
|
+
|
|
46
54
|
let chatInstance: QuikChatInstance | null = null;
|
|
47
55
|
let chatContainerEl: Element | null = null;
|
|
48
56
|
let _quikChatEl: HTMLElement | null = null;
|
|
@@ -57,7 +65,7 @@ export function mountAiPanel() {
|
|
|
57
65
|
return;
|
|
58
66
|
}
|
|
59
67
|
mounted = true;
|
|
60
|
-
checkAuth();
|
|
68
|
+
void checkAuth();
|
|
61
69
|
}
|
|
62
70
|
|
|
63
71
|
const _g = globalThis as unknown as {
|
|
@@ -87,10 +95,10 @@ export function mountQuikChat() {
|
|
|
87
95
|
return;
|
|
88
96
|
}
|
|
89
97
|
|
|
90
|
-
chatInstance = new
|
|
98
|
+
chatInstance = new QuikChat(
|
|
91
99
|
container,
|
|
92
100
|
(_chat: unknown, msg: string) => {
|
|
93
|
-
handleUserSend(msg);
|
|
101
|
+
void handleUserSend(msg);
|
|
94
102
|
},
|
|
95
103
|
{
|
|
96
104
|
messagesArea: { alternating: false },
|
|
@@ -197,14 +205,14 @@ function stop() {
|
|
|
197
205
|
return;
|
|
198
206
|
}
|
|
199
207
|
const plat = getPlatform();
|
|
200
|
-
plat.aiStopSession(sessionId);
|
|
208
|
+
void plat.aiStopSession(sessionId);
|
|
201
209
|
finishStream();
|
|
202
210
|
}
|
|
203
211
|
|
|
204
212
|
function newChat() {
|
|
205
213
|
if (sessionId) {
|
|
206
214
|
const plat = getPlatform();
|
|
207
|
-
plat.aiDeleteSession(sessionId);
|
|
215
|
+
void plat.aiDeleteSession(sessionId);
|
|
208
216
|
}
|
|
209
217
|
disconnectStream();
|
|
210
218
|
messages = [];
|
|
@@ -223,6 +231,31 @@ function newChat() {
|
|
|
223
231
|
|
|
224
232
|
// ─── SSE Stream ─────────────────────────────────────────────────────────────
|
|
225
233
|
|
|
234
|
+
/** SSE `stream_event` payload (subset used here). */
|
|
235
|
+
interface StreamEventData {
|
|
236
|
+
event?: { type?: string; delta?: { type?: string; text?: string } };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** SSE `result` payload (subset used here). */
|
|
240
|
+
interface ResultData {
|
|
241
|
+
result?: string;
|
|
242
|
+
is_error?: boolean;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** SSE `error` payload (subset used here). */
|
|
246
|
+
interface ErrorData {
|
|
247
|
+
error?: string;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Parse SSE JSON into the expected shape. Returns null on malformed input. */
|
|
251
|
+
function parseSse<T>(raw: unknown): T | null {
|
|
252
|
+
try {
|
|
253
|
+
return JSON.parse(String(raw)) as T;
|
|
254
|
+
} catch {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
226
259
|
async function connectStream(id: string) {
|
|
227
260
|
disconnectStream();
|
|
228
261
|
const plat = getPlatform();
|
|
@@ -230,40 +263,37 @@ async function connectStream(id: string) {
|
|
|
230
263
|
eventSource = new EventSource(url);
|
|
231
264
|
|
|
232
265
|
eventSource.addEventListener("stream_event", (e) => {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if (
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
chatInstance.messageAppendContent(currentStreamMsgId, token);
|
|
245
|
-
}
|
|
266
|
+
const data = parseSse<StreamEventData>((e as MessageEvent).data);
|
|
267
|
+
const evt = data?.event;
|
|
268
|
+
if (evt?.type === "content_block_delta" && evt.delta?.type === "text_delta") {
|
|
269
|
+
const token = evt.delta.text ?? "";
|
|
270
|
+
currentAssistantText += token;
|
|
271
|
+
if (chatInstance && currentStreamMsgId != null) {
|
|
272
|
+
if (!streamStarted) {
|
|
273
|
+
chatInstance.messageReplaceContent(currentStreamMsgId, currentAssistantText);
|
|
274
|
+
streamStarted = true;
|
|
275
|
+
} else {
|
|
276
|
+
chatInstance.messageAppendContent(currentStreamMsgId, token);
|
|
246
277
|
}
|
|
247
278
|
}
|
|
248
|
-
}
|
|
279
|
+
}
|
|
249
280
|
});
|
|
250
281
|
|
|
251
282
|
eventSource.addEventListener("assistant", (e) => {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
283
|
+
const data = parseSse<AssistantMessageData>((e as MessageEvent).data);
|
|
284
|
+
if (data) {
|
|
285
|
+
handleAssistantMessage(data);
|
|
286
|
+
}
|
|
255
287
|
});
|
|
256
288
|
|
|
257
289
|
eventSource.addEventListener("result", (e) => {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
if (
|
|
261
|
-
|
|
262
|
-
chatInstance.messageReplaceContent(currentStreamMsgId, `Error: ${data.result}`);
|
|
263
|
-
}
|
|
264
|
-
currentAssistantText = `Error: ${data.result}`;
|
|
290
|
+
const data = parseSse<ResultData>((e as MessageEvent).data);
|
|
291
|
+
if (data?.result && data.is_error) {
|
|
292
|
+
if (chatInstance && currentStreamMsgId != null) {
|
|
293
|
+
chatInstance.messageReplaceContent(currentStreamMsgId, `Error: ${data.result}`);
|
|
265
294
|
}
|
|
266
|
-
|
|
295
|
+
currentAssistantText = `Error: ${data.result}`;
|
|
296
|
+
}
|
|
267
297
|
finishStream();
|
|
268
298
|
});
|
|
269
299
|
|
|
@@ -272,15 +302,13 @@ async function connectStream(id: string) {
|
|
|
272
302
|
});
|
|
273
303
|
|
|
274
304
|
eventSource.addEventListener("error", (e) => {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
if (
|
|
278
|
-
|
|
279
|
-
chatInstance.messageReplaceContent(currentStreamMsgId, `Error: ${data.error}`);
|
|
280
|
-
}
|
|
281
|
-
currentAssistantText = `Error: ${data.error}`;
|
|
305
|
+
const data = parseSse<ErrorData>((e as MessageEvent).data);
|
|
306
|
+
if (data?.error) {
|
|
307
|
+
if (chatInstance && currentStreamMsgId != null) {
|
|
308
|
+
chatInstance.messageReplaceContent(currentStreamMsgId, `Error: ${data.error}`);
|
|
282
309
|
}
|
|
283
|
-
|
|
310
|
+
currentAssistantText = `Error: ${data.error}`;
|
|
311
|
+
}
|
|
284
312
|
finishStream();
|
|
285
313
|
});
|
|
286
314
|
|
|
@@ -306,7 +334,7 @@ function finishStream() {
|
|
|
306
334
|
|
|
307
335
|
if (pendingFileReloads.size > 0) {
|
|
308
336
|
for (const fp of pendingFileReloads) {
|
|
309
|
-
reloadFileInTab(fp);
|
|
337
|
+
void reloadFileInTab(fp);
|
|
310
338
|
}
|
|
311
339
|
pendingFileReloads.clear();
|
|
312
340
|
}
|
|
@@ -9,13 +9,21 @@ import { styleMap } from "lit-html/directives/style-map.js";
|
|
|
9
9
|
import { ref } from "lit-html/directives/ref.js";
|
|
10
10
|
import { draggable } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
childIndex,
|
|
14
|
+
childList,
|
|
15
|
+
elToScope,
|
|
16
|
+
getNodeAtPath,
|
|
17
|
+
nodeLabel,
|
|
18
|
+
parentElementPath,
|
|
19
|
+
} from "../store";
|
|
13
20
|
import { activeTab } from "../workspace/workspace";
|
|
14
21
|
import { mutateMoveNode, mutateUpdateProperty, transactDoc } from "../tabs/transact";
|
|
15
22
|
import { view } from "../view";
|
|
16
23
|
import { getActiveElement, getInlineActions, isEditing } from "../editor/inline-edit";
|
|
17
24
|
import type { InlineAction } from "../editor/inline-edit";
|
|
18
25
|
import { isTagActiveInSelection, toggleInlineFormat } from "../editor/inline-format";
|
|
26
|
+
import { buildMergeTags } from "../editor/merge-tags";
|
|
19
27
|
import { componentRegistry } from "../files/components";
|
|
20
28
|
import { convertToComponent } from "../editor/convert-to-component";
|
|
21
29
|
import { findCanvasElement, getActivePanel } from "../canvas/canvas-helpers";
|
|
@@ -224,6 +232,60 @@ function applyInlineFormat(action: InlineAction) {
|
|
|
224
232
|
requestAnimationFrame(() => renderBlockActionBar());
|
|
225
233
|
}
|
|
226
234
|
|
|
235
|
+
/**
|
|
236
|
+
* Open the merge-tag menu — a searchable list of `${…}` template tokens for the data available in
|
|
237
|
+
* the current state. Reuses the shared slash-menu popover (filter + keyboard nav + dismiss).
|
|
238
|
+
*
|
|
239
|
+
* @param {MouseEvent} e
|
|
240
|
+
*/
|
|
241
|
+
function onMergeTagClick(e: MouseEvent) {
|
|
242
|
+
e.stopPropagation();
|
|
243
|
+
const anchorEl = e.currentTarget as HTMLElement;
|
|
244
|
+
const tab = activeTab.value;
|
|
245
|
+
const editable = getActiveElement();
|
|
246
|
+
const state = (tab?.doc.document.state ?? {}) as Record<string, unknown>;
|
|
247
|
+
const scope = getActivePanel()?.liveCtx?.scope ?? null;
|
|
248
|
+
const localScope = editable ? (elToScope.get(editable) ?? null) : null;
|
|
249
|
+
|
|
250
|
+
const commands = buildMergeTags(state, scope, localScope).map((t) => ({
|
|
251
|
+
description: t.hint,
|
|
252
|
+
label: t.label,
|
|
253
|
+
tag: t.token,
|
|
254
|
+
}));
|
|
255
|
+
|
|
256
|
+
showSlashMenu(anchorEl, "", {
|
|
257
|
+
commands,
|
|
258
|
+
onSelect: (cmd) => insertMergeTag(cmd.tag),
|
|
259
|
+
showFilter: true,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Insert a `${token}` template expression at the saved selection inside the active contenteditable.
|
|
265
|
+
* Mirrors onFormatClick's range-restore flow and inserts via execCommand so it joins the
|
|
266
|
+
* contenteditable's native undo stack (like paste).
|
|
267
|
+
*
|
|
268
|
+
* @param {string} token
|
|
269
|
+
*/
|
|
270
|
+
function insertMergeTag(token: string) {
|
|
271
|
+
if (!view.savedRange) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const anchor = view.savedRange.startContainer;
|
|
275
|
+
const editableRoot = (
|
|
276
|
+
anchor?.nodeType === Node.ELEMENT_NODE ? (anchor as Element) : anchor?.parentElement
|
|
277
|
+
)?.closest("[contenteditable]");
|
|
278
|
+
if (!editableRoot) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const sel = window.getSelection();
|
|
282
|
+
(editableRoot as HTMLElement).focus();
|
|
283
|
+
sel?.removeAllRanges();
|
|
284
|
+
sel?.addRange(view.savedRange);
|
|
285
|
+
document.execCommand("insertText", false, `\${${token}}`);
|
|
286
|
+
requestAnimationFrame(() => renderBlockActionBar());
|
|
287
|
+
}
|
|
288
|
+
|
|
227
289
|
/** Dismiss the link popover if open. */
|
|
228
290
|
export function dismissLinkPopover() {
|
|
229
291
|
const host = getLayerSlot("popover", "link-popover");
|
|
@@ -515,6 +577,15 @@ export function renderBlockActionBar() {
|
|
|
515
577
|
`,
|
|
516
578
|
)}
|
|
517
579
|
</sp-action-group>
|
|
580
|
+
<sp-action-button
|
|
581
|
+
size="xs"
|
|
582
|
+
quiet
|
|
583
|
+
title="Insert data"
|
|
584
|
+
@mousedown=${captureSelectionRange}
|
|
585
|
+
@click=${onMergeTagClick}
|
|
586
|
+
>
|
|
587
|
+
<sp-icon-data slot="icon"></sp-icon-data>
|
|
588
|
+
</sp-action-button>
|
|
518
589
|
`
|
|
519
590
|
: nothing}
|
|
520
591
|
</div>
|