@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.
@@ -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 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);
@@ -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 snapshot = {
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 pushVersion(state, snapshot);
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
- const before = get().canvas;
197
- set((state) => {
198
- if (!state.undoStack.length) return state;
199
- const previous = state.undoStack[state.undoStack.length - 1];
200
- return {
201
- canvas: previous,
202
- undoStack: state.undoStack.slice(0, -1),
203
- redoStack: [...state.redoStack, state.canvas].slice(-50),
204
- selections: []
205
- };
206
- });
207
- notifyTimeTravel(get(), before);
208
- },
209
- redo: () => {
210
- const before = get().canvas;
211
- set((state) => {
212
- if (!state.redoStack.length) return state;
213
- const next = state.redoStack[state.redoStack.length - 1];
214
- return {
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
- 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 })
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 };