@braincrew-lab/langchain-canvas 0.1.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.
@@ -0,0 +1,12 @@
1
+ import { useCanvasStore } from './chunk-PSIT32I5.js';
2
+ import { useCallback } from 'react';
3
+
4
+ function useArtifactPatch(id) {
5
+ const applyUserEvent = useCanvasStore((s) => s.applyUserEvent);
6
+ return useCallback(
7
+ (patch) => applyUserEvent({ type: "canvas.patch", id, patch }),
8
+ [applyUserEvent, id]
9
+ );
10
+ }
11
+
12
+ export { useArtifactPatch };
@@ -0,0 +1,242 @@
1
+ import { createStore } from 'zustand/vanilla';
2
+ import { createContext, useState, useContext } from 'react';
3
+ import { useStore } from 'zustand';
4
+ import { jsx } from 'react/jsx-runtime';
5
+
6
+ // src/protocol/events.ts
7
+ function isCanvasEvent(event) {
8
+ return event.type.startsWith("canvas.");
9
+ }
10
+ function isChatEvent(event) {
11
+ return event.type.startsWith("message.") || event.type.startsWith("tool.");
12
+ }
13
+
14
+ // src/client/reconcile.ts
15
+ function emptyCanvasState() {
16
+ return { artifacts: {}, history: {}, order: [], activeId: null };
17
+ }
18
+ function reduceCanvas(state, event) {
19
+ switch (event.type) {
20
+ case "canvas.create":
21
+ return create(state, event.artifact);
22
+ case "canvas.append": {
23
+ const current = state.artifacts[event.id];
24
+ if (!current) return state;
25
+ const data = appendAtPath(current.data, event.path, event.text);
26
+ return replaceInPlace(state, { ...current, data });
27
+ }
28
+ case "canvas.patch": {
29
+ const current = state.artifacts[event.id];
30
+ if (!current) return state;
31
+ const data = mergePatch(current.data, event.patch);
32
+ return replaceInPlace(state, { ...current, data });
33
+ }
34
+ case "canvas.node_patch": {
35
+ const current = state.artifacts[event.id];
36
+ const html = current?.data?.html;
37
+ if (!current || typeof html !== "string") return state;
38
+ const next = applyNodePatch(html, event.cid, event.html);
39
+ const data = { ...current.data, html: next };
40
+ return replaceInPlace(state, { ...current, data });
41
+ }
42
+ case "canvas.replace":
43
+ return pushVersion(state, event.artifact);
44
+ case "canvas.status": {
45
+ const current = state.artifacts[event.id];
46
+ if (!current) return state;
47
+ return replaceInPlace(state, { ...current, status: event.status });
48
+ }
49
+ case "canvas.close":
50
+ return state.activeId === event.id ? { ...state, activeId: lastOf(state.order, event.id) } : state;
51
+ }
52
+ }
53
+ function create(state, artifact) {
54
+ const known = state.order.includes(artifact.id);
55
+ return {
56
+ artifacts: { ...state.artifacts, [artifact.id]: artifact },
57
+ history: { ...state.history, [artifact.id]: [artifact] },
58
+ order: known ? state.order : [...state.order, artifact.id],
59
+ activeId: artifact.id
60
+ };
61
+ }
62
+ function replaceInPlace(state, artifact) {
63
+ const versions = state.history[artifact.id] ?? [];
64
+ const history = versions.length ? { ...state.history, [artifact.id]: [...versions.slice(0, -1), artifact] } : { ...state.history, [artifact.id]: [artifact] };
65
+ return { ...state, artifacts: { ...state.artifacts, [artifact.id]: artifact }, history };
66
+ }
67
+ function pushVersion(state, artifact) {
68
+ const versions = state.history[artifact.id] ?? [];
69
+ const known = state.order.includes(artifact.id);
70
+ return {
71
+ artifacts: { ...state.artifacts, [artifact.id]: artifact },
72
+ history: { ...state.history, [artifact.id]: [...versions, artifact] },
73
+ order: known ? state.order : [...state.order, artifact.id],
74
+ activeId: artifact.id
75
+ };
76
+ }
77
+ function appendAtPath(data, path, text) {
78
+ const keys = path.split(".");
79
+ const clone = structuredClone(data);
80
+ let node = clone;
81
+ for (let i = 0; i < keys.length - 1; i++) {
82
+ const next = node[keys[i]];
83
+ if (!isPlainObject(next)) return data;
84
+ node = next;
85
+ }
86
+ const leaf = keys[keys.length - 1];
87
+ node[leaf] = `${node[leaf] ?? ""}${text}`;
88
+ return clone;
89
+ }
90
+ function applyNodePatch(html, cid, fragment) {
91
+ if (typeof DOMParser === "undefined") return html;
92
+ try {
93
+ const doc = new DOMParser().parseFromString(html, "text/html");
94
+ const target = resolveCid(doc.body, cid);
95
+ if (!target || !target.parentNode) return html;
96
+ target.outerHTML = fragment;
97
+ return `<!doctype html>
98
+ ${doc.documentElement.outerHTML}`;
99
+ } catch {
100
+ return html;
101
+ }
102
+ }
103
+ function resolveCid(body, cid) {
104
+ if (!body) return null;
105
+ const indices = cid.split("-").slice(1);
106
+ let node = body;
107
+ for (const raw of indices) {
108
+ const index = Number(raw);
109
+ if (!Number.isInteger(index) || index < 0) return null;
110
+ const child = node.children[index];
111
+ if (!child) return null;
112
+ node = child;
113
+ }
114
+ return node;
115
+ }
116
+ function mergePatch(target, patch) {
117
+ if (!isPlainObject(patch)) return patch;
118
+ const base = isPlainObject(target) ? { ...target } : {};
119
+ for (const [key, value] of Object.entries(patch)) {
120
+ if (value === null) delete base[key];
121
+ else base[key] = mergePatch(base[key], value);
122
+ }
123
+ return base;
124
+ }
125
+ function isPlainObject(value) {
126
+ return typeof value === "object" && value !== null && !Array.isArray(value);
127
+ }
128
+ function lastOf(order, excludeId) {
129
+ const remaining = order.filter((id) => id !== excludeId);
130
+ return remaining.length ? remaining[remaining.length - 1] : null;
131
+ }
132
+ var initialState = () => ({
133
+ canvas: emptyCanvasState(),
134
+ messages: [],
135
+ isStreaming: false,
136
+ error: null,
137
+ selections: [],
138
+ iframeCommand: null,
139
+ undoStack: [],
140
+ redoStack: []
141
+ });
142
+ function createCanvasStore() {
143
+ return createStore((set) => ({
144
+ ...initialState(),
145
+ applyEvent: (event) => set((state) => foldEvent(state, event)),
146
+ applyEvents: (events) => set((state) => events.reduce(foldEvent, state)),
147
+ applyUserEvent: (event) => set((state) => {
148
+ const undoStack = [...state.undoStack, state.canvas].slice(-50);
149
+ return { ...foldEvent(state, event), undoStack, redoStack: [] };
150
+ }),
151
+ undo: () => set((state) => {
152
+ if (!state.undoStack.length) return state;
153
+ const previous = state.undoStack[state.undoStack.length - 1];
154
+ return {
155
+ canvas: previous,
156
+ undoStack: state.undoStack.slice(0, -1),
157
+ redoStack: [...state.redoStack, state.canvas].slice(-50),
158
+ selections: []
159
+ };
160
+ }),
161
+ redo: () => set((state) => {
162
+ if (!state.redoStack.length) return state;
163
+ const next = state.redoStack[state.redoStack.length - 1];
164
+ return {
165
+ canvas: next,
166
+ redoStack: state.redoStack.slice(0, -1),
167
+ undoStack: [...state.undoStack, state.canvas].slice(-50),
168
+ selections: []
169
+ };
170
+ }),
171
+ addUserMessage: (text) => set((state) => ({
172
+ messages: [...state.messages, { id: `user_${state.messages.length}`, role: "user", text }],
173
+ error: null
174
+ })),
175
+ setStreaming: (value) => set({ isStreaming: value }),
176
+ setActiveArtifact: (id) => set((state) => ({ canvas: { ...state.canvas, activeId: id } })),
177
+ setSelections: (selections) => set({ selections }),
178
+ sendIframeCommand: (command) => set((state) => ({ iframeCommand: { ...command, seq: (state.iframeCommand?.seq ?? 0) + 1 } })),
179
+ reset: () => set(initialState())
180
+ }));
181
+ }
182
+ function foldEvent(state, event) {
183
+ try {
184
+ return reduceEvent(state, event);
185
+ } catch (err) {
186
+ console.error("[langchain-canvas] event skipped:", event, err);
187
+ return state;
188
+ }
189
+ }
190
+ function reduceEvent(state, event) {
191
+ if (isCanvasEvent(event)) {
192
+ const canvas = reduceCanvas(state.canvas, event);
193
+ if (event.type === "canvas.create") {
194
+ return { ...state, canvas, messages: linkArtifact(state.messages, event.artifact.id) };
195
+ }
196
+ return { ...state, canvas };
197
+ }
198
+ switch (event.type) {
199
+ case "message.delta":
200
+ return { ...state, messages: appendDelta(state.messages, event.messageId, event.text) };
201
+ case "error":
202
+ return { ...state, error: event.message };
203
+ case "done":
204
+ return { ...state, isStreaming: false };
205
+ // message.end / tool.* — no store change in the reference UI.
206
+ default:
207
+ return state;
208
+ }
209
+ }
210
+ function appendDelta(messages, id, text) {
211
+ const index = messages.findIndex((m) => m.id === id);
212
+ if (index === -1) return [...messages, { id, role: "assistant", text }];
213
+ const next = messages.slice();
214
+ next[index] = { ...next[index], text: next[index].text + text };
215
+ return next;
216
+ }
217
+ function linkArtifact(messages, artifactId) {
218
+ let index = messages.length - 1;
219
+ while (index >= 0 && messages[index].role !== "assistant") index--;
220
+ if (index === -1) {
221
+ return [...messages, { id: `assistant_${messages.length}`, role: "assistant", text: "", artifactIds: [artifactId] }];
222
+ }
223
+ const next = messages.slice();
224
+ const current = next[index];
225
+ next[index] = { ...current, artifactIds: [...current.artifactIds ?? [], artifactId] };
226
+ return next;
227
+ }
228
+ var CanvasStoreContext = createContext(null);
229
+ var defaultStore = null;
230
+ var getDefaultStore = () => defaultStore ??= createCanvasStore();
231
+ function CanvasProvider({ children, store }) {
232
+ const [instance] = useState(() => store ?? createCanvasStore());
233
+ return /* @__PURE__ */ jsx(CanvasStoreContext.Provider, { value: instance, children });
234
+ }
235
+ function useCanvasStoreApi() {
236
+ return useContext(CanvasStoreContext) ?? getDefaultStore();
237
+ }
238
+ function useCanvasStore(selector) {
239
+ return useStore(useCanvasStoreApi(), selector);
240
+ }
241
+
242
+ export { CanvasProvider, createCanvasStore, emptyCanvasState, isCanvasEvent, isChatEvent, mergePatch, reduceCanvas, useCanvasStore, useCanvasStoreApi };
@@ -0,0 +1,14 @@
1
+ // src/optionalImport.ts
2
+ async function loadOptional(pkg, load) {
3
+ try {
4
+ return await load();
5
+ } catch (err) {
6
+ const error = new Error(
7
+ `langchain-canvas: this feature requires the optional package "${pkg}", which isn't installed. Add it with: npm i ${pkg}`
8
+ );
9
+ error.cause = err;
10
+ throw error;
11
+ }
12
+ }
13
+
14
+ export { loadOptional };