@braincrew-lab/langchain-canvas 0.2.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.
@@ -1,77 +1,36 @@
1
- import { loadOptional } from './chunk-YZZSJJMQ.js';
2
- import { useCanvasStore } from './chunk-S54GJDSJ.js';
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';
3
5
  import { lazy, useMemo, useState, useRef, useEffect, useCallback, Suspense } from 'react';
4
6
  import '@fortune-sheet/react/dist/index.css';
5
7
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
6
8
 
7
- // src/io/formula.ts
8
- var EMPTY = /* @__PURE__ */ new Map();
9
- async function computeFormulas(columns, rows) {
10
- const formulaCells = [];
11
- rows.forEach(
12
- (row, dataIdx) => columns.forEach((col, c) => {
13
- const v = row[col.key];
14
- if (typeof v === "string" && v.startsWith("=")) formulaCells.push({ dataIdx, col: c, formula: v });
15
- })
16
- );
17
- if (formulaCells.length === 0) return EMPTY;
18
- const mod = await loadOptional("fast-formula-parser", () => import('fast-formula-parser'));
19
- const FormulaParser = mod.default ?? mod;
20
- const memo = /* @__PURE__ */ new Map();
21
- const inProgress = /* @__PURE__ */ new Set();
22
- const rawAt = (row, col) => {
23
- const colIdx = col - 1;
24
- if (row === 1) return columns[colIdx]?.label ?? columns[colIdx]?.key ?? null;
25
- const dataRow = rows[row - 2];
26
- const column = columns[colIdx];
27
- if (!dataRow || !column) return null;
28
- const v = dataRow[column.key];
29
- return v ?? null;
30
- };
31
- const valueAt = (row, col) => {
32
- const raw = rawAt(row, col);
33
- if (typeof raw !== "string" || !raw.startsWith("=")) return raw ?? 0;
34
- const key = `${row},${col}`;
35
- const cached = memo.get(key);
36
- if (cached !== void 0) return cached;
37
- if (inProgress.has(key)) return 0;
38
- inProgress.add(key);
39
- const value = evaluate(raw, row, col);
40
- inProgress.delete(key);
41
- memo.set(key, value);
42
- return value;
43
- };
44
- const parser = new FormulaParser({
45
- onCell: ({ row, col }) => valueAt(row, col),
46
- onRange: (ref) => {
47
- const maxRow = Math.min(ref.to.row, rows.length + 1);
48
- const maxCol = Math.min(ref.to.col, columns.length);
49
- const grid = [];
50
- for (let r = ref.from.row; r <= maxRow; r++) {
51
- const line = [];
52
- for (let c = ref.from.col; c <= maxCol; c++) line.push(valueAt(r, c));
53
- grid.push(line);
54
- }
55
- return grid;
56
- }
57
- });
58
- const evaluate = (formula, row, col) => {
59
- try {
60
- const result = parser.parse(formula.slice(1), { row, col });
61
- if (result != null && typeof result === "object") return "#ERR";
62
- return result ?? 0;
63
- } catch {
64
- return "#ERR";
65
- }
66
- };
67
- const out = /* @__PURE__ */ new Map();
68
- for (const { dataIdx, col, formula } of formulaCells) {
69
- out.set(`${dataIdx + 1},${col}`, evaluate(formula, dataIdx + 2, col + 1));
70
- }
71
- return out;
72
- }
73
9
  var Workbook = lazy(() => import('@fortune-sheet/react').then((m) => ({ default: m.Workbook })));
74
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
+ }
75
34
  function toWorkbook(columns, rows, formulas) {
76
35
  const celldata = [];
77
36
  columns.forEach((col, c) => {
@@ -177,7 +136,13 @@ function TableRenderer({ artifact }) {
177
136
  root.addEventListener("wheel", onWheel, { passive: false, capture: true });
178
137
  return () => root.removeEventListener("wheel", onWheel, { capture: true });
179
138
  }, [mounted]);
180
- const dataKey = `${artifact.id}:v${artifact.version}:${columns.length}x${rows.length}`;
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}`;
181
146
  const hasFormulas = useMemo(
182
147
  () => rows.slice(0, 400).some((row) => columns.some((col) => isFormula(row[col.key]))),
183
148
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -202,7 +167,7 @@ function TableRenderer({ artifact }) {
202
167
  alive = false;
203
168
  };
204
169
  }, [dataKey, hasFormulas]);
205
- const hasSheet = !!artifact.data.sheet?.length;
170
+ const hasSheet = sheetHasContent(artifact.data.sheet);
206
171
  const [sortCol, setSortCol] = useState("");
207
172
  const [sortDir, setSortDir] = useState(1);
208
173
  const [filter, setFilter] = useState("");
@@ -228,7 +193,11 @@ function TableRenderer({ artifact }) {
228
193
  const viewActive = !!appliedFilter.trim() || !!sortCol;
229
194
  const wbKey = `${dataKey}:${viewActive ? `view-s${sortCol}${sortDir}-f${appliedFilter}` : hasSheet ? "sheet" : "rows"}`;
230
195
  const initialData = useMemo(
231
- () => viewActive ? toWorkbook(columns, viewRows, formulas) : hasSheet ? artifact.data.sheet : toWorkbook(columns, rows, formulas),
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),
232
201
  // eslint-disable-next-line react-hooks/exhaustive-deps
233
202
  [wbKey, formulasReady]
234
203
  );
@@ -238,7 +207,12 @@ function TableRenderer({ artifact }) {
238
207
  (sheets) => {
239
208
  if (persistTimer.current) clearTimeout(persistTimer.current);
240
209
  persistTimer.current = setTimeout(() => {
241
- applyEvent({ type: "canvas.patch", id: artifact.id, patch: { sheet: sheets } });
210
+ if (!sheetHasContent(sheets)) return;
211
+ applyEvent({
212
+ type: "canvas.patch",
213
+ id: artifact.id,
214
+ patch: { sheet: normalizeSheets(sheets) }
215
+ });
242
216
  }, 400);
243
217
  },
244
218
  [applyEvent, artifact.id]
@@ -252,137 +226,19 @@ function TableRenderer({ artifact }) {
252
226
  const range = sel?.[0]?.[type] ?? [0, 0];
253
227
  wbRef.current?.insertRowOrColumn(type, Math.max(0, range[1]), 1, "rightbottom");
254
228
  };
255
- const [selection, setSelection] = useState(null);
256
- const [boldOn, setBoldOn] = useState(false);
257
- const [fillColor, setFillColor] = useState("#fef3c7");
258
- const [textColor, setTextColor] = useState("#111827");
259
- const handleSelectionChange = useCallback((_sheetId, sel) => {
260
- setSelection({ row: [...sel.row], column: [...sel.column] });
261
- const bl = wbRef.current?.getCellValue?.(sel.row[0], sel.column[0], { type: "bl" });
262
- setBoldOn(bl === 1 || bl === "1");
263
- }, []);
264
- const workbookHooks = useMemo(() => ({ afterSelectionChange: handleSelectionChange }), [handleSelectionChange]);
265
- const applyFormat = (attr, value) => {
266
- const sel = wbRef.current?.getSelection?.();
267
- if (!sel?.length) return;
268
- const ranges = sel.map((s) => ({ row: [s.row[0], s.row[1]], column: [s.column[0], s.column[1]] }));
269
- wbRef.current?.setCellFormatByRange(attr, value, ranges);
270
- };
271
- const toggleBold = () => {
272
- applyFormat("bl", boldOn ? 0 : 1);
273
- setBoldOn((b) => !b);
274
- };
275
- const cleanStyling = () => {
276
- const wb = wbRef.current;
277
- const sheet = wb?.getSheet?.();
278
- const sheetId = sheet?.id;
279
- if (!wb || !sheet || !sheetId) return;
280
- const ops = [];
281
- sheet.celldata.forEach(({ r, c, v }) => {
282
- if (!v || typeof v !== "object") return;
283
- const cell = v;
284
- if (cell.bg != null) ops.push({ op: "remove", id: sheetId, path: ["data", r, c, "bg"] });
285
- if (cell.fc != null) ops.push({ op: "remove", id: sheetId, path: ["data", r, c, "fc"] });
286
- const spans = cell.ct?.s;
287
- if (!Array.isArray(spans)) return;
288
- spans.forEach((span, i) => {
289
- if (!span || typeof span !== "object") return;
290
- const run = span;
291
- if (run.bg != null) ops.push({ op: "remove", id: sheetId, path: ["data", r, c, "ct", "s", i, "bg"] });
292
- if (run.fc != null) ops.push({ op: "remove", id: sheetId, path: ["data", r, c, "ct", "s", i, "fc"] });
293
- });
294
- });
295
- if (ops.length) wb.applyOp(ops);
296
- };
297
- const [frozen, setFrozen] = useState(false);
298
- const toggleFreeze = () => {
299
- const wb = wbRef.current;
300
- if (!wb) return;
301
- if (frozen) {
302
- const sheetId = wb.getSheet?.()?.id;
303
- if (sheetId) wb.applyOp([{ op: "remove", id: sheetId, path: ["frozen"] }]);
304
- } else {
305
- wb.freeze("row", { row: 0, column: 0 });
306
- }
307
- setFrozen((f) => !f);
308
- };
309
- useEffect(() => {
310
- setFrozen(!viewActive && hasSheet && !!artifact.data.sheet?.[0]?.frozen);
311
- setSelection(null);
312
- setBoldOn(false);
313
- }, [wbKey]);
314
229
  if (!mounted) {
315
230
  return /* @__PURE__ */ jsx("div", { className: "cv-sheet cv-sheet--empty", children: "Loading spreadsheet\u2026" });
316
231
  }
317
232
  if (!hasSheet && columns.length === 0) {
318
233
  return /* @__PURE__ */ jsx("div", { className: "cv-sheet cv-sheet--empty", children: "Waiting for data\u2026" });
319
234
  }
320
- if (!hasSheet && !formulasReady) {
235
+ if (!formulasReady) {
321
236
  return /* @__PURE__ */ jsx("div", { className: "cv-sheet cv-sheet--empty", children: "Calculating\u2026" });
322
237
  }
323
238
  return /* @__PURE__ */ jsxs("div", { className: "cv-sheet-panel", children: [
324
239
  /* @__PURE__ */ jsxs("div", { className: "cv-sheet-tools", children: [
325
240
  /* @__PURE__ */ jsx("button", { type: "button", onClick: () => insert("column"), children: "\uFF0B Column" }),
326
241
  /* @__PURE__ */ jsx("button", { type: "button", onClick: () => insert("row"), children: "\uFF0B Row" }),
327
- /* @__PURE__ */ jsx("span", { className: "cv-sheet-tools__sep" }),
328
- /* @__PURE__ */ jsxs("span", { className: "cv-sheet-tools__fmt", children: [
329
- /* @__PURE__ */ jsx(
330
- "button",
331
- {
332
- type: "button",
333
- className: `cv-sheet-tools__bold${boldOn ? " cv-sheet-tools__bold--on" : ""}`,
334
- title: "Bold",
335
- "aria-pressed": boldOn,
336
- disabled: !selection,
337
- onClick: toggleBold,
338
- children: "B"
339
- }
340
- ),
341
- /* @__PURE__ */ jsx(
342
- "input",
343
- {
344
- type: "color",
345
- className: "cv-sheet-tools__color cv-sheet-tools__color--fill",
346
- title: "Fill color",
347
- disabled: !selection,
348
- value: fillColor,
349
- onChange: (e) => {
350
- setFillColor(e.target.value);
351
- applyFormat("bg", e.target.value);
352
- }
353
- }
354
- ),
355
- /* @__PURE__ */ jsx(
356
- "input",
357
- {
358
- type: "color",
359
- className: "cv-sheet-tools__color cv-sheet-tools__color--text",
360
- title: "Text color",
361
- disabled: !selection,
362
- value: textColor,
363
- onChange: (e) => {
364
- setTextColor(e.target.value);
365
- applyFormat("fc", e.target.value);
366
- }
367
- }
368
- ),
369
- /* @__PURE__ */ jsx("button", { type: "button", className: "cv-sheet-tools__align", title: "Align left", disabled: !selection, onClick: () => applyFormat("ht", 1), children: "\u21E4" }),
370
- /* @__PURE__ */ jsx("button", { type: "button", className: "cv-sheet-tools__align", title: "Align center", disabled: !selection, onClick: () => applyFormat("ht", 0), children: "\u2194" }),
371
- /* @__PURE__ */ jsx("button", { type: "button", className: "cv-sheet-tools__align", title: "Align right", disabled: !selection, onClick: () => applyFormat("ht", 2), children: "\u21E5" })
372
- ] }),
373
- /* @__PURE__ */ jsx("span", { className: "cv-sheet-tools__sep" }),
374
- /* @__PURE__ */ jsx("button", { type: "button", className: "cv-sheet-tools__clean", title: "Remove all cell fills and font colors", onClick: cleanStyling, children: "Clean styling" }),
375
- /* @__PURE__ */ jsx(
376
- "button",
377
- {
378
- type: "button",
379
- className: `cv-sheet-tools__freeze${frozen ? " cv-sheet-tools__freeze--on" : ""}`,
380
- title: frozen ? "Unfreeze the header row" : "Keep the header row visible while scrolling",
381
- "aria-pressed": frozen,
382
- onClick: toggleFreeze,
383
- children: "Freeze header"
384
- }
385
- ),
386
242
  columns.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
387
243
  /* @__PURE__ */ jsx("span", { className: "cv-sheet-tools__sep" }),
388
244
  /* @__PURE__ */ jsxs(
@@ -412,8 +268,8 @@ function TableRenderer({ artifact }) {
412
268
  ] }),
413
269
  /* @__PURE__ */ jsx("span", { className: "cv-sheet-tools__hint", children: "Right-click a header for more, or drag to edit" })
414
270
  ] }),
415
- /* @__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, hooks: workbookHooks }, wbKey) }) })
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) }) })
416
272
  ] });
417
273
  }
418
274
 
419
- export { TableRenderer };
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 replaceInPlace(state, { ...current, data });
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 replaceInPlace(state, { ...current, data });
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 replaceInPlace(state, { ...current, data });
40
+ return updateLive(state, { ...current, data });
41
41
  }
42
42
  case "canvas.replace":
43
43
  return pushVersion(state, event.artifact);
@@ -46,8 +46,26 @@ function reduceCanvas(state, event) {
46
46
  if (!current) return state;
47
47
  return replaceInPlace(state, { ...current, status: event.status });
48
48
  }
49
- case "canvas.close":
50
- return state.activeId === event.id ? { ...state, activeId: lastOf(state.order, event.id) } : state;
49
+ case "canvas.commit": {
50
+ const current = state.artifacts[event.id];
51
+ if (!current) return state;
52
+ const versions = state.history[event.id] ?? [];
53
+ const committed = {
54
+ ...current,
55
+ meta: {
56
+ ...current.meta ?? {},
57
+ commitDescription: event.description,
58
+ ...event.revision ? { revision: event.revision } : {}
59
+ }
60
+ };
61
+ return {
62
+ ...state,
63
+ artifacts: { ...state.artifacts, [event.id]: committed },
64
+ history: { ...state.history, [event.id]: [...versions.slice(0, -1), committed] }
65
+ };
66
+ }
67
+ default:
68
+ return state;
51
69
  }
52
70
  }
53
71
  function create(state, artifact) {
@@ -59,6 +77,20 @@ function create(state, artifact) {
59
77
  activeId: artifact.id
60
78
  };
61
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
+ }
62
94
  function replaceInPlace(state, artifact) {
63
95
  const versions = state.history[artifact.id] ?? [];
64
96
  const history = versions.length ? { ...state.history, [artifact.id]: [...versions.slice(0, -1), artifact] } : { ...state.history, [artifact.id]: [artifact] };
@@ -125,18 +157,6 @@ function mergePatch(target, patch) {
125
157
  function isPlainObject(value) {
126
158
  return typeof value === "object" && value !== null && !Array.isArray(value);
127
159
  }
128
- function lastOf(order, excludeId) {
129
- const remaining = order.filter((id) => id !== excludeId);
130
- return remaining.length ? remaining[remaining.length - 1] : null;
131
- }
132
- function notifyTimeTravel(store, before) {
133
- const handler = store.onUserEdit;
134
- if (!handler || store.canvas === before) return;
135
- for (const id of Object.keys(store.canvas.artifacts)) {
136
- const now = store.canvas.artifacts[id];
137
- if (now && now !== before.artifacts[id]) handler(now);
138
- }
139
- }
140
160
  function editedArtifactId(event) {
141
161
  switch (event.type) {
142
162
  case "canvas.create":
@@ -159,7 +179,8 @@ var initialState = () => ({
159
179
  iframeCommand: null,
160
180
  undoStack: [],
161
181
  redoStack: [],
162
- onUserEdit: null
182
+ onUserEdit: null,
183
+ assetBaseUrl: null
163
184
  });
164
185
  function createCanvasStore() {
165
186
  return createStore((set, get) => ({
@@ -176,34 +197,26 @@ function createCanvasStore() {
176
197
  const artifact = id ? state.canvas.artifacts[id] : void 0;
177
198
  if (artifact) state.onUserEdit?.(artifact);
178
199
  },
179
- undo: () => {
180
- const before = get().canvas;
181
- set((state) => {
182
- if (!state.undoStack.length) return state;
183
- const previous = state.undoStack[state.undoStack.length - 1];
184
- return {
185
- canvas: previous,
186
- undoStack: state.undoStack.slice(0, -1),
187
- redoStack: [...state.redoStack, state.canvas].slice(-50),
188
- selections: []
189
- };
190
- });
191
- notifyTimeTravel(get(), before);
192
- },
193
- redo: () => {
194
- const before = get().canvas;
195
- set((state) => {
196
- if (!state.redoStack.length) return state;
197
- const next = state.redoStack[state.redoStack.length - 1];
198
- return {
199
- canvas: next,
200
- redoStack: state.redoStack.slice(0, -1),
201
- undoStack: [...state.undoStack, state.canvas].slice(-50),
202
- selections: []
203
- };
204
- });
205
- notifyTimeTravel(get(), before);
206
- },
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
+ }),
207
220
  addUserMessage: (text) => set((state) => ({
208
221
  messages: [...state.messages, { id: `user_${state.messages.length}`, role: "user", text }],
209
222
  error: null
@@ -213,7 +226,9 @@ function createCanvasStore() {
213
226
  setSelections: (selections) => set({ selections }),
214
227
  sendIframeCommand: (command) => set((state) => ({ iframeCommand: { ...command, seq: (state.iframeCommand?.seq ?? 0) + 1 } })),
215
228
  setOnUserEdit: (handler) => set({ onUserEdit: handler }),
216
- reset: () => set(initialState())
229
+ setAssetBaseUrl: (url) => set({ assetBaseUrl: url }),
230
+ // Host configuration (the asset endpoint) survives a session reset.
231
+ reset: () => set({ ...initialState(), assetBaseUrl: get().assetBaseUrl })
217
232
  }));
218
233
  }
219
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 };