@braincrew-lab/langchain-canvas 0.3.0 → 0.4.9
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/README.ko.md +11 -23
- package/README.md +11 -23
- package/dist/{ChartRenderer-ABRQ5YFN.js → ChartRenderer-JGRJ23OB.js} +39 -67
- package/dist/{DocumentRenderer-YTEMC3Z4.js → DocumentRenderer-ZQDQMX7X.js} +18 -19
- package/dist/FileRenderer-3ZHNORZJ.js +39 -0
- package/dist/SlidesRenderer-56R3TNWN.js +496 -0
- package/dist/TableRenderer-3ESF577F.js +275 -0
- package/dist/chunk-7T5DRR3F.js +109 -0
- package/dist/{chunk-ZLXAWRUP.js → chunk-EHW446VF.js} +49 -50
- package/dist/chunk-FTNRRJ3K.js +13 -0
- package/dist/chunk-IFNRLN4Y.js +137 -0
- package/dist/{chunk-FSFOURG5.js → chunk-K2UZAYW2.js} +1 -1
- package/dist/chunk-SGOPRUQ4.js +182 -0
- package/dist/formula-27TCEZI5.js +2 -0
- package/dist/formula-cli.d.ts +2 -0
- package/dist/formula-cli.js +37 -0
- package/dist/index.d.ts +166 -1002
- package/dist/index.js +354 -2523
- package/dist/langgraph/index.d.ts +68 -0
- package/dist/langgraph/index.js +111 -0
- package/dist/styles.css +59 -409
- package/dist/types-BfGP9R2I.d.ts +324 -0
- package/package.json +15 -4
- package/dist/PdfRenderer-GDA67MES.js +0 -98
- package/dist/SlidesRenderer-JNPXNTNJ.js +0 -964
- package/dist/TableRenderer-BJQE2HMO.js +0 -636
- package/dist/chunk-QMOJEGRH.js +0 -207
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { mergeRowsIntoSheet } from './chunk-IFNRLN4Y.js';
|
|
2
|
+
import { computeFormulas } from './chunk-SGOPRUQ4.js';
|
|
3
|
+
import './chunk-YZZSJJMQ.js';
|
|
4
|
+
import { useCanvasStore } from './chunk-EHW446VF.js';
|
|
5
|
+
import { lazy, useMemo, useState, useRef, useEffect, useCallback, Suspense } from 'react';
|
|
6
|
+
import '@fortune-sheet/react/dist/index.css';
|
|
7
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
8
|
+
|
|
9
|
+
var Workbook = lazy(() => import('@fortune-sheet/react').then((m) => ({ default: m.Workbook })));
|
|
10
|
+
var isFormula = (v) => typeof v === "string" && v.startsWith("=");
|
|
11
|
+
function sheetHasContent(sheet) {
|
|
12
|
+
if (!sheet?.length) return false;
|
|
13
|
+
return sheet.some((s) => {
|
|
14
|
+
const celldata = s.celldata;
|
|
15
|
+
if (celldata?.some((cell) => cell?.v != null)) return true;
|
|
16
|
+
const data = s.data;
|
|
17
|
+
return !!data?.some((row) => row?.some((cell) => cell != null));
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function normalizeSheets(sheet) {
|
|
21
|
+
if (!sheet?.length) return sheet;
|
|
22
|
+
return sheet.map((s) => {
|
|
23
|
+
const { luckysheet_select_save: _selection, data, ...rest } = s;
|
|
24
|
+
if (!data || rest.celldata) return rest;
|
|
25
|
+
const celldata = [];
|
|
26
|
+
data.forEach(
|
|
27
|
+
(row, r) => row?.forEach((cell, c) => {
|
|
28
|
+
if (cell != null) celldata.push({ r, c, v: cell });
|
|
29
|
+
})
|
|
30
|
+
);
|
|
31
|
+
return { ...rest, celldata };
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function toWorkbook(columns, rows, formulas) {
|
|
35
|
+
const celldata = [];
|
|
36
|
+
columns.forEach((col, c) => {
|
|
37
|
+
const label = col.label ?? col.key;
|
|
38
|
+
celldata.push({ r: 0, c, v: { v: label, m: String(label), bl: 1, bg: "#f3f4f6" } });
|
|
39
|
+
});
|
|
40
|
+
rows.forEach((row, r) => {
|
|
41
|
+
columns.forEach((col, c) => {
|
|
42
|
+
const val = row[col.key];
|
|
43
|
+
if (val === void 0 || val === null || val === "") return;
|
|
44
|
+
if (isFormula(val)) {
|
|
45
|
+
const computed = formulas.get(`${r + 1},${c}`);
|
|
46
|
+
const v = { f: val };
|
|
47
|
+
if (computed !== void 0) {
|
|
48
|
+
v.v = computed;
|
|
49
|
+
v.m = String(computed);
|
|
50
|
+
if (typeof computed === "number") v.ct = { fa: "General", t: "n" };
|
|
51
|
+
}
|
|
52
|
+
celldata.push({ r: r + 1, c, v });
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const numeric = typeof val === "number";
|
|
56
|
+
celldata.push({
|
|
57
|
+
r: r + 1,
|
|
58
|
+
c,
|
|
59
|
+
v: { v: val, m: String(val), ...numeric ? { ct: { fa: "General", t: "n" } } : {} }
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
const sample = Math.min(rows.length, 400);
|
|
64
|
+
const columnlen = {};
|
|
65
|
+
columns.forEach((col, c) => {
|
|
66
|
+
let widest = String(col.label ?? col.key).length;
|
|
67
|
+
for (let ri = 0; ri < sample; ri++) {
|
|
68
|
+
let v = rows[ri][col.key];
|
|
69
|
+
if (isFormula(v)) v = formulas.get(`${ri + 1},${c}`) ?? "";
|
|
70
|
+
if (v != null && v !== "") widest = Math.max(widest, String(v).length);
|
|
71
|
+
}
|
|
72
|
+
columnlen[c] = Math.min(360, Math.max(64, Math.round(widest * 8.5) + 18));
|
|
73
|
+
});
|
|
74
|
+
return [
|
|
75
|
+
{
|
|
76
|
+
name: "Sheet1",
|
|
77
|
+
id: "sheet1",
|
|
78
|
+
order: 0,
|
|
79
|
+
// Size the grid to the data plus a modest buffer — big enough to feel like a
|
|
80
|
+
// real sheet and to keep growing, small enough that the scrollbar stays
|
|
81
|
+
// proportional (a huge empty grid makes scrolling feel disconnected).
|
|
82
|
+
row: Math.max(rows.length + 40, 60),
|
|
83
|
+
column: Math.max(columns.length + 2, 8),
|
|
84
|
+
celldata,
|
|
85
|
+
// No frozen pane: a freeze split offsets the initial scroll and hides the
|
|
86
|
+
// first data rows behind the split line. A plain grid scrolls cleanly.
|
|
87
|
+
config: { rowlen: { 0: 28 }, columnlen }
|
|
88
|
+
}
|
|
89
|
+
];
|
|
90
|
+
}
|
|
91
|
+
function deriveColumns(rows) {
|
|
92
|
+
const keys = /* @__PURE__ */ new Set();
|
|
93
|
+
for (let i = 0; i < Math.min(rows.length, 50); i++) Object.keys(rows[i] ?? {}).forEach((k) => keys.add(k));
|
|
94
|
+
return [...keys].map((key) => ({ key }));
|
|
95
|
+
}
|
|
96
|
+
var EMPTY_FORMULAS = /* @__PURE__ */ new Map();
|
|
97
|
+
function TableRenderer({ artifact }) {
|
|
98
|
+
const rows = artifact.data.rows;
|
|
99
|
+
const columns = useMemo(
|
|
100
|
+
() => artifact.data.columns.length ? artifact.data.columns : deriveColumns(rows),
|
|
101
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
102
|
+
[artifact.id, artifact.version, artifact.data.columns.length, rows.length]
|
|
103
|
+
);
|
|
104
|
+
const [mounted, setMounted] = useState(false);
|
|
105
|
+
const rootRef = useRef(null);
|
|
106
|
+
useEffect(() => setMounted(true), []);
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
const root = rootRef.current;
|
|
109
|
+
if (!root) return;
|
|
110
|
+
const onWheel = (e) => {
|
|
111
|
+
const y = root.querySelector(".luckysheet-scrollbar-y");
|
|
112
|
+
const x = root.querySelector(".luckysheet-scrollbar-x");
|
|
113
|
+
let moved = false;
|
|
114
|
+
if (y && e.deltaY && y.scrollHeight > y.clientHeight) {
|
|
115
|
+
const max = y.scrollHeight - y.clientHeight;
|
|
116
|
+
const next = Math.max(0, Math.min(max, y.scrollTop + e.deltaY));
|
|
117
|
+
if (next !== y.scrollTop) {
|
|
118
|
+
y.scrollTop = next;
|
|
119
|
+
moved = true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (x && e.deltaX && x.scrollWidth > x.clientWidth) {
|
|
123
|
+
const max = x.scrollWidth - x.clientWidth;
|
|
124
|
+
const next = Math.max(0, Math.min(max, x.scrollLeft + e.deltaX));
|
|
125
|
+
if (next !== x.scrollLeft) {
|
|
126
|
+
x.scrollLeft = next;
|
|
127
|
+
moved = true;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const horizontal = Math.abs(e.deltaX) > Math.abs(e.deltaY);
|
|
131
|
+
if (moved || horizontal && e.deltaX) {
|
|
132
|
+
e.preventDefault();
|
|
133
|
+
e.stopPropagation();
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
root.addEventListener("wheel", onWheel, { passive: false, capture: true });
|
|
137
|
+
return () => root.removeEventListener("wheel", onWheel, { capture: true });
|
|
138
|
+
}, [mounted]);
|
|
139
|
+
const rowsSig = useMemo(() => {
|
|
140
|
+
const json = JSON.stringify(rows);
|
|
141
|
+
let hash = 5381;
|
|
142
|
+
for (let i = 0; i < json.length; i++) hash = (hash << 5) + hash + json.charCodeAt(i) | 0;
|
|
143
|
+
return (hash >>> 0).toString(36);
|
|
144
|
+
}, [rows]);
|
|
145
|
+
const dataKey = `${artifact.id}:v${artifact.version}:${columns.length}x${rows.length}:${rowsSig}`;
|
|
146
|
+
const hasFormulas = useMemo(
|
|
147
|
+
() => rows.slice(0, 400).some((row) => columns.some((col) => isFormula(row[col.key]))),
|
|
148
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
149
|
+
[dataKey]
|
|
150
|
+
);
|
|
151
|
+
const [formulas, setFormulas] = useState(EMPTY_FORMULAS);
|
|
152
|
+
const [formulasReady, setFormulasReady] = useState(!hasFormulas);
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
if (!hasFormulas) {
|
|
155
|
+
setFormulas(EMPTY_FORMULAS);
|
|
156
|
+
setFormulasReady(true);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
let alive = true;
|
|
160
|
+
setFormulasReady(false);
|
|
161
|
+
computeFormulas(columns, rows).then((values) => {
|
|
162
|
+
if (!alive) return;
|
|
163
|
+
setFormulas(values);
|
|
164
|
+
setFormulasReady(true);
|
|
165
|
+
});
|
|
166
|
+
return () => {
|
|
167
|
+
alive = false;
|
|
168
|
+
};
|
|
169
|
+
}, [dataKey, hasFormulas]);
|
|
170
|
+
const hasSheet = sheetHasContent(artifact.data.sheet);
|
|
171
|
+
const [sortCol, setSortCol] = useState("");
|
|
172
|
+
const [sortDir, setSortDir] = useState(1);
|
|
173
|
+
const [filter, setFilter] = useState("");
|
|
174
|
+
const [appliedFilter, setAppliedFilter] = useState("");
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
const t = setTimeout(() => setAppliedFilter(filter), 300);
|
|
177
|
+
return () => clearTimeout(t);
|
|
178
|
+
}, [filter]);
|
|
179
|
+
const viewRows = useMemo(() => {
|
|
180
|
+
let r = rows;
|
|
181
|
+
const q = appliedFilter.trim().toLowerCase();
|
|
182
|
+
if (q) r = r.filter((row) => columns.some((c) => String(row[c.key] ?? "").toLowerCase().includes(q)));
|
|
183
|
+
if (sortCol) {
|
|
184
|
+
r = [...r].sort((a, b) => {
|
|
185
|
+
const av = a[sortCol];
|
|
186
|
+
const bv = b[sortCol];
|
|
187
|
+
const cmp = typeof av === "number" && typeof bv === "number" ? av - bv : String(av ?? "").localeCompare(String(bv ?? ""));
|
|
188
|
+
return cmp * sortDir;
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
return r;
|
|
192
|
+
}, [rows, columns, appliedFilter, sortCol, sortDir]);
|
|
193
|
+
const viewActive = !!appliedFilter.trim() || !!sortCol;
|
|
194
|
+
const wbKey = `${dataKey}:${viewActive ? `view-s${sortCol}${sortDir}-f${appliedFilter}` : hasSheet ? "sheet" : "rows"}`;
|
|
195
|
+
const initialData = useMemo(
|
|
196
|
+
() => viewActive ? toWorkbook(columns, viewRows, formulas) : hasSheet ? (
|
|
197
|
+
// Rows the agent wrote after the person's last edit win their cells;
|
|
198
|
+
// the person's formatting and out-of-table cells survive.
|
|
199
|
+
mergeRowsIntoSheet(columns, rows, normalizeSheets(artifact.data.sheet), formulas)
|
|
200
|
+
) : toWorkbook(columns, rows, formulas),
|
|
201
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
202
|
+
[wbKey, formulasReady]
|
|
203
|
+
);
|
|
204
|
+
const applyEvent = useCanvasStore((s) => s.applyUserEvent);
|
|
205
|
+
const persistTimer = useRef(null);
|
|
206
|
+
const handleChange = useCallback(
|
|
207
|
+
(sheets) => {
|
|
208
|
+
if (persistTimer.current) clearTimeout(persistTimer.current);
|
|
209
|
+
persistTimer.current = setTimeout(() => {
|
|
210
|
+
if (!sheetHasContent(sheets)) return;
|
|
211
|
+
applyEvent({
|
|
212
|
+
type: "canvas.patch",
|
|
213
|
+
id: artifact.id,
|
|
214
|
+
patch: { sheet: normalizeSheets(sheets) }
|
|
215
|
+
});
|
|
216
|
+
}, 400);
|
|
217
|
+
},
|
|
218
|
+
[applyEvent, artifact.id]
|
|
219
|
+
);
|
|
220
|
+
useEffect(() => () => {
|
|
221
|
+
if (persistTimer.current) clearTimeout(persistTimer.current);
|
|
222
|
+
}, []);
|
|
223
|
+
const wbRef = useRef(null);
|
|
224
|
+
const insert = (type) => {
|
|
225
|
+
const sel = wbRef.current?.getSelection?.();
|
|
226
|
+
const range = sel?.[0]?.[type] ?? [0, 0];
|
|
227
|
+
wbRef.current?.insertRowOrColumn(type, Math.max(0, range[1]), 1, "rightbottom");
|
|
228
|
+
};
|
|
229
|
+
if (!mounted) {
|
|
230
|
+
return /* @__PURE__ */ jsx("div", { className: "cv-sheet cv-sheet--empty", children: "Loading spreadsheet\u2026" });
|
|
231
|
+
}
|
|
232
|
+
if (!hasSheet && columns.length === 0) {
|
|
233
|
+
return /* @__PURE__ */ jsx("div", { className: "cv-sheet cv-sheet--empty", children: "Waiting for data\u2026" });
|
|
234
|
+
}
|
|
235
|
+
if (!formulasReady) {
|
|
236
|
+
return /* @__PURE__ */ jsx("div", { className: "cv-sheet cv-sheet--empty", children: "Calculating\u2026" });
|
|
237
|
+
}
|
|
238
|
+
return /* @__PURE__ */ jsxs("div", { className: "cv-sheet-panel", children: [
|
|
239
|
+
/* @__PURE__ */ jsxs("div", { className: "cv-sheet-tools", children: [
|
|
240
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: () => insert("column"), children: "\uFF0B Column" }),
|
|
241
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: () => insert("row"), children: "\uFF0B Row" }),
|
|
242
|
+
columns.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
243
|
+
/* @__PURE__ */ jsx("span", { className: "cv-sheet-tools__sep" }),
|
|
244
|
+
/* @__PURE__ */ jsxs(
|
|
245
|
+
"select",
|
|
246
|
+
{
|
|
247
|
+
className: "cv-sheet-tools__sort",
|
|
248
|
+
value: sortCol,
|
|
249
|
+
title: "Sort by column",
|
|
250
|
+
onChange: (e) => setSortCol(e.target.value),
|
|
251
|
+
children: [
|
|
252
|
+
/* @__PURE__ */ jsx("option", { value: "", children: "Sort\u2026" }),
|
|
253
|
+
columns.map((c) => /* @__PURE__ */ jsx("option", { value: c.key, children: c.label ?? c.key }, c.key))
|
|
254
|
+
]
|
|
255
|
+
}
|
|
256
|
+
),
|
|
257
|
+
sortCol && /* @__PURE__ */ jsx("button", { type: "button", title: sortDir === 1 ? "Ascending" : "Descending", onClick: () => setSortDir((d) => d === 1 ? -1 : 1), children: sortDir === 1 ? "\u25B2" : "\u25BC" }),
|
|
258
|
+
/* @__PURE__ */ jsx(
|
|
259
|
+
"input",
|
|
260
|
+
{
|
|
261
|
+
className: "cv-sheet-tools__filter",
|
|
262
|
+
value: filter,
|
|
263
|
+
placeholder: "Filter\u2026",
|
|
264
|
+
onChange: (e) => setFilter(e.target.value),
|
|
265
|
+
title: "Filter rows"
|
|
266
|
+
}
|
|
267
|
+
)
|
|
268
|
+
] }),
|
|
269
|
+
/* @__PURE__ */ jsx("span", { className: "cv-sheet-tools__hint", children: "Right-click a header for more, or drag to edit" })
|
|
270
|
+
] }),
|
|
271
|
+
/* @__PURE__ */ jsx("div", { className: "cv-sheet", ref: rootRef, children: /* @__PURE__ */ jsx(Suspense, { fallback: /* @__PURE__ */ jsx("div", { className: "cv-sheet--empty", children: "Loading\u2026" }), children: /* @__PURE__ */ jsx(Workbook, { ref: wbRef, data: initialData, onChange: handleChange }, wbKey) }) })
|
|
272
|
+
] });
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export { TableRenderer, normalizeSheets, sheetHasContent };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// src/io/canvasAssets.ts
|
|
2
|
+
var ASSET_REFERENCE_PREFIXES = ["assets/", "sources/"];
|
|
3
|
+
function normalizeAssetReference(src) {
|
|
4
|
+
if (typeof src !== "string") return null;
|
|
5
|
+
let folded = src;
|
|
6
|
+
while (folded.startsWith("./") || folded.startsWith("../")) {
|
|
7
|
+
folded = folded.startsWith("./") ? folded.slice(2) : folded.slice(3);
|
|
8
|
+
}
|
|
9
|
+
return ASSET_REFERENCE_PREFIXES.some((p) => folded.startsWith(p)) ? folded : null;
|
|
10
|
+
}
|
|
11
|
+
function isAssetReference(src) {
|
|
12
|
+
return normalizeAssetReference(src) !== null;
|
|
13
|
+
}
|
|
14
|
+
function resolveAssetUrl(src, assetBaseUrl) {
|
|
15
|
+
return assetBaseUrl + encodeURIComponent(normalizeAssetReference(src) ?? src);
|
|
16
|
+
}
|
|
17
|
+
var REF_ALTERNATION = ASSET_REFERENCE_PREFIXES.map((p) => p.slice(0, -1)).join("|");
|
|
18
|
+
var srcAttrPattern = () => new RegExp(`(src=(["']))((?:\\.\\.?/)*(?:${REF_ALTERNATION})/[^"']+)(\\2)`, "g");
|
|
19
|
+
var escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
20
|
+
async function fetchAssetDataUri(path, assetBaseUrl) {
|
|
21
|
+
try {
|
|
22
|
+
const res = await fetch(resolveAssetUrl(path, assetBaseUrl));
|
|
23
|
+
if (!res.ok) return null;
|
|
24
|
+
const type = res.headers.get("content-type")?.split(";")[0] || "application/octet-stream";
|
|
25
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
26
|
+
let binary = "";
|
|
27
|
+
for (let i = 0; i < bytes.length; i += 32768) {
|
|
28
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + 32768));
|
|
29
|
+
}
|
|
30
|
+
return `data:${type};base64,${btoa(binary)}`;
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function inlineHtmlAssets(html, assetBaseUrl) {
|
|
36
|
+
const relative = srcAttrPattern();
|
|
37
|
+
const absolute = new RegExp(
|
|
38
|
+
`(src=(["']))${escapeRegExp(assetBaseUrl)}([^"']+)(\\2)`,
|
|
39
|
+
"g"
|
|
40
|
+
);
|
|
41
|
+
const paths = /* @__PURE__ */ new Set();
|
|
42
|
+
for (const m of html.matchAll(relative)) paths.add(m[3]);
|
|
43
|
+
for (const m of html.matchAll(absolute)) {
|
|
44
|
+
const decoded = safeDecode(m[3]);
|
|
45
|
+
if (isAssetReference(decoded)) paths.add(decoded);
|
|
46
|
+
}
|
|
47
|
+
if (!paths.size) return html;
|
|
48
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
49
|
+
await Promise.all(
|
|
50
|
+
[...paths].map(async (p) => {
|
|
51
|
+
const uri = await fetchAssetDataUri(p, assetBaseUrl);
|
|
52
|
+
if (uri) resolved.set(p, uri);
|
|
53
|
+
})
|
|
54
|
+
);
|
|
55
|
+
return html.replace(relative, (whole, pre, _q, path, post) => {
|
|
56
|
+
const uri = resolved.get(path);
|
|
57
|
+
return uri ? `${pre}${uri}${post}` : whole;
|
|
58
|
+
}).replace(absolute, (whole, pre, _q, encoded, post) => {
|
|
59
|
+
const uri = resolved.get(safeDecode(encoded));
|
|
60
|
+
return uri ? `${pre}${uri}${post}` : whole;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function safeDecode(value) {
|
|
64
|
+
try {
|
|
65
|
+
return decodeURIComponent(value);
|
|
66
|
+
} catch {
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function inlineArtifactAssets(artifact, assetBaseUrl) {
|
|
71
|
+
if (!assetBaseUrl) return artifact;
|
|
72
|
+
if (artifact.type === "html") {
|
|
73
|
+
const data = artifact.data;
|
|
74
|
+
const html = await inlineHtmlAssets(data.html, assetBaseUrl);
|
|
75
|
+
return html === data.html ? artifact : { ...artifact, data: { ...data, html } };
|
|
76
|
+
}
|
|
77
|
+
if (artifact.type === "slides") {
|
|
78
|
+
const data = artifact.data;
|
|
79
|
+
let changed = false;
|
|
80
|
+
const slides = await Promise.all(
|
|
81
|
+
(data.slides ?? []).map(async (slide) => {
|
|
82
|
+
let next = slide;
|
|
83
|
+
if (isAssetReference(slide.image)) {
|
|
84
|
+
const uri = await fetchAssetDataUri(slide.image, assetBaseUrl);
|
|
85
|
+
if (uri) {
|
|
86
|
+
next = { ...next, image: uri };
|
|
87
|
+
changed = true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (next.elements?.some((el) => isAssetReference(el.src))) {
|
|
91
|
+
const elements = await Promise.all(
|
|
92
|
+
next.elements.map(async (el) => {
|
|
93
|
+
if (!isAssetReference(el.src)) return el;
|
|
94
|
+
const uri = await fetchAssetDataUri(el.src, assetBaseUrl);
|
|
95
|
+
return uri ? { ...el, src: uri } : el;
|
|
96
|
+
})
|
|
97
|
+
);
|
|
98
|
+
next = { ...next, elements };
|
|
99
|
+
changed = true;
|
|
100
|
+
}
|
|
101
|
+
return next;
|
|
102
|
+
})
|
|
103
|
+
);
|
|
104
|
+
return changed ? { ...artifact, data: { ...data, slides } } : artifact;
|
|
105
|
+
}
|
|
106
|
+
return artifact;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export { ASSET_REFERENCE_PREFIXES, fetchAssetDataUri, inlineArtifactAssets, inlineHtmlAssets, isAssetReference, normalizeAssetReference, resolveAssetUrl };
|
|
@@ -23,13 +23,13 @@ function reduceCanvas(state, event) {
|
|
|
23
23
|
const current = state.artifacts[event.id];
|
|
24
24
|
if (!current) return state;
|
|
25
25
|
const data = appendAtPath(current.data, event.path, event.text);
|
|
26
|
-
return
|
|
26
|
+
return updateLive(state, { ...current, data });
|
|
27
27
|
}
|
|
28
28
|
case "canvas.patch": {
|
|
29
29
|
const current = state.artifacts[event.id];
|
|
30
30
|
if (!current) return state;
|
|
31
31
|
const data = mergePatch(current.data, event.patch);
|
|
32
|
-
return
|
|
32
|
+
return updateLive(state, { ...current, data });
|
|
33
33
|
}
|
|
34
34
|
case "canvas.node_patch": {
|
|
35
35
|
const current = state.artifacts[event.id];
|
|
@@ -37,7 +37,7 @@ function reduceCanvas(state, event) {
|
|
|
37
37
|
if (!current || typeof html !== "string") return state;
|
|
38
38
|
const next = applyNodePatch(html, event.cid, event.html);
|
|
39
39
|
const data = { ...current.data, html: next };
|
|
40
|
-
return
|
|
40
|
+
return updateLive(state, { ...current, data });
|
|
41
41
|
}
|
|
42
42
|
case "canvas.replace":
|
|
43
43
|
return pushVersion(state, event.artifact);
|
|
@@ -49,19 +49,21 @@ function reduceCanvas(state, event) {
|
|
|
49
49
|
case "canvas.commit": {
|
|
50
50
|
const current = state.artifacts[event.id];
|
|
51
51
|
if (!current) return state;
|
|
52
|
-
const
|
|
52
|
+
const versions = state.history[event.id] ?? [];
|
|
53
|
+
const committed = {
|
|
53
54
|
...current,
|
|
54
|
-
version: current.version + 1,
|
|
55
55
|
meta: {
|
|
56
56
|
...current.meta ?? {},
|
|
57
57
|
commitDescription: event.description,
|
|
58
58
|
...event.revision ? { revision: event.revision } : {}
|
|
59
59
|
}
|
|
60
60
|
};
|
|
61
|
-
return
|
|
61
|
+
return {
|
|
62
|
+
...state,
|
|
63
|
+
artifacts: { ...state.artifacts, [event.id]: committed },
|
|
64
|
+
history: { ...state.history, [event.id]: [...versions.slice(0, -1), committed] }
|
|
65
|
+
};
|
|
62
66
|
}
|
|
63
|
-
case "canvas.close":
|
|
64
|
-
return state.activeId === event.id ? { ...state, activeId: lastOf(state.order, event.id) } : state;
|
|
65
67
|
default:
|
|
66
68
|
return state;
|
|
67
69
|
}
|
|
@@ -75,6 +77,20 @@ function create(state, artifact) {
|
|
|
75
77
|
activeId: artifact.id
|
|
76
78
|
};
|
|
77
79
|
}
|
|
80
|
+
function updateLive(state, artifact) {
|
|
81
|
+
const versions = state.history[artifact.id] ?? [];
|
|
82
|
+
const last = versions[versions.length - 1];
|
|
83
|
+
if (!last || typeof last.meta?.commitDescription !== "string") {
|
|
84
|
+
return replaceInPlace(state, artifact);
|
|
85
|
+
}
|
|
86
|
+
const { commitDescription: _stale, ...meta } = artifact.meta ?? {};
|
|
87
|
+
const working = { ...artifact, version: last.version + 1, meta };
|
|
88
|
+
return {
|
|
89
|
+
...state,
|
|
90
|
+
artifacts: { ...state.artifacts, [artifact.id]: working },
|
|
91
|
+
history: { ...state.history, [artifact.id]: [...versions, working] }
|
|
92
|
+
};
|
|
93
|
+
}
|
|
78
94
|
function replaceInPlace(state, artifact) {
|
|
79
95
|
const versions = state.history[artifact.id] ?? [];
|
|
80
96
|
const history = versions.length ? { ...state.history, [artifact.id]: [...versions.slice(0, -1), artifact] } : { ...state.history, [artifact.id]: [artifact] };
|
|
@@ -141,18 +157,6 @@ function mergePatch(target, patch) {
|
|
|
141
157
|
function isPlainObject(value) {
|
|
142
158
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
143
159
|
}
|
|
144
|
-
function lastOf(order, excludeId) {
|
|
145
|
-
const remaining = order.filter((id) => id !== excludeId);
|
|
146
|
-
return remaining.length ? remaining[remaining.length - 1] : null;
|
|
147
|
-
}
|
|
148
|
-
function notifyTimeTravel(store, before) {
|
|
149
|
-
const handler = store.onUserEdit;
|
|
150
|
-
if (!handler || store.canvas === before) return;
|
|
151
|
-
for (const id of Object.keys(store.canvas.artifacts)) {
|
|
152
|
-
const now = store.canvas.artifacts[id];
|
|
153
|
-
if (now && now !== before.artifacts[id]) handler(now);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
160
|
function editedArtifactId(event) {
|
|
157
161
|
switch (event.type) {
|
|
158
162
|
case "canvas.create":
|
|
@@ -175,7 +179,8 @@ var initialState = () => ({
|
|
|
175
179
|
iframeCommand: null,
|
|
176
180
|
undoStack: [],
|
|
177
181
|
redoStack: [],
|
|
178
|
-
onUserEdit: null
|
|
182
|
+
onUserEdit: null,
|
|
183
|
+
assetBaseUrl: null
|
|
179
184
|
});
|
|
180
185
|
function createCanvasStore() {
|
|
181
186
|
return createStore((set, get) => ({
|
|
@@ -192,34 +197,26 @@ function createCanvasStore() {
|
|
|
192
197
|
const artifact = id ? state.canvas.artifacts[id] : void 0;
|
|
193
198
|
if (artifact) state.onUserEdit?.(artifact);
|
|
194
199
|
},
|
|
195
|
-
undo: () => {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
canvas: next,
|
|
216
|
-
redoStack: state.redoStack.slice(0, -1),
|
|
217
|
-
undoStack: [...state.undoStack, state.canvas].slice(-50),
|
|
218
|
-
selections: []
|
|
219
|
-
};
|
|
220
|
-
});
|
|
221
|
-
notifyTimeTravel(get(), before);
|
|
222
|
-
},
|
|
200
|
+
undo: () => set((state) => {
|
|
201
|
+
if (!state.undoStack.length) return state;
|
|
202
|
+
const previous = state.undoStack[state.undoStack.length - 1];
|
|
203
|
+
return {
|
|
204
|
+
canvas: previous,
|
|
205
|
+
undoStack: state.undoStack.slice(0, -1),
|
|
206
|
+
redoStack: [...state.redoStack, state.canvas].slice(-50),
|
|
207
|
+
selections: []
|
|
208
|
+
};
|
|
209
|
+
}),
|
|
210
|
+
redo: () => set((state) => {
|
|
211
|
+
if (!state.redoStack.length) return state;
|
|
212
|
+
const next = state.redoStack[state.redoStack.length - 1];
|
|
213
|
+
return {
|
|
214
|
+
canvas: next,
|
|
215
|
+
redoStack: state.redoStack.slice(0, -1),
|
|
216
|
+
undoStack: [...state.undoStack, state.canvas].slice(-50),
|
|
217
|
+
selections: []
|
|
218
|
+
};
|
|
219
|
+
}),
|
|
223
220
|
addUserMessage: (text) => set((state) => ({
|
|
224
221
|
messages: [...state.messages, { id: `user_${state.messages.length}`, role: "user", text }],
|
|
225
222
|
error: null
|
|
@@ -229,7 +226,9 @@ function createCanvasStore() {
|
|
|
229
226
|
setSelections: (selections) => set({ selections }),
|
|
230
227
|
sendIframeCommand: (command) => set((state) => ({ iframeCommand: { ...command, seq: (state.iframeCommand?.seq ?? 0) + 1 } })),
|
|
231
228
|
setOnUserEdit: (handler) => set({ onUserEdit: handler }),
|
|
232
|
-
|
|
229
|
+
setAssetBaseUrl: (url) => set({ assetBaseUrl: url }),
|
|
230
|
+
// Host configuration (the asset endpoint) survives a session reset.
|
|
231
|
+
reset: () => set({ ...initialState(), assetBaseUrl: get().assetBaseUrl })
|
|
233
232
|
}));
|
|
234
233
|
}
|
|
235
234
|
function foldEvent(state, event) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { isAssetReference, resolveAssetUrl } from './chunk-7T5DRR3F.js';
|
|
2
|
+
import { useCanvasStore } from './chunk-EHW446VF.js';
|
|
3
|
+
import { useCallback } from 'react';
|
|
4
|
+
|
|
5
|
+
function useAssetUrl() {
|
|
6
|
+
const assetBaseUrl = useCanvasStore((s) => s.assetBaseUrl);
|
|
7
|
+
return useCallback(
|
|
8
|
+
(src) => src && assetBaseUrl && isAssetReference(src) ? resolveAssetUrl(src, assetBaseUrl) : src,
|
|
9
|
+
[assetBaseUrl]
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { useAssetUrl };
|