@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.
@@ -0,0 +1,68 @@
1
+ import { C as CanvasTransport, E as ElementSelection, S as StreamEvent } from '../types-BfGP9R2I.js';
2
+
3
+ /**
4
+ * `langgraphTransport` — speak to a LangGraph server without a translator
5
+ * in between.
6
+ *
7
+ * Built on the official `@langchain/langgraph-sdk` (the server's wire format
8
+ * belongs to LangGraph and evolves with it — we ride the official client
9
+ * rather than hand-parse it). Verified against `langgraph dev` (local);
10
+ * hosted LangGraph Platform is untested — open an issue if you need it.
11
+ *
12
+ * Per user turn it: maps the canvas thread id to the UUID LangGraph requires,
13
+ * makes sure the thread exists, frames any element selections into the
14
+ * message (targeted edits), then streams the run with
15
+ * `streamMode: ["messages-tuple", "custom"]` through the translation in
16
+ * `translate.ts`.
17
+ */
18
+
19
+ interface LangGraphTransportOptions {
20
+ /** LangGraph server URL, e.g. `http://127.0.0.1:2024` (`langgraph dev`). */
21
+ url: string;
22
+ /** Graph/assistant to run, e.g. `"canvas_agent"`. */
23
+ assistantId: string;
24
+ /** Extra headers (e.g. auth) passed to the SDK client. */
25
+ headers?: Record<string, string>;
26
+ }
27
+ /** Frame a targeted edit so the agent changes only the selected element(s). */
28
+ declare function withSelections(message: string, selections: ElementSelection[]): string;
29
+ /**
30
+ * LangGraph requires UUID thread ids; the canvas allows any string. Non-UUID
31
+ * ids map deterministically (RFC 4122 v5 over `canvas-thread:<id>`), matching
32
+ * the mapping the Python bridge example uses — same id in, same UUID out.
33
+ */
34
+ declare function threadUuid(threadId: string): Promise<string>;
35
+ declare function langgraphTransport(options: LangGraphTransportOptions): CanvasTransport;
36
+
37
+ /**
38
+ * Translate a LangGraph run stream into Canvas Wire Protocol events.
39
+ *
40
+ * Input: the `{event, data}` chunks the LangGraph SDK yields for a run
41
+ * streamed with `streamMode: ["messages-tuple", "custom"]`. Output: the
42
+ * `StreamEvent`s the canvas applies. The mapping:
43
+ *
44
+ * - `messages` AIMessageChunk text → `message.delta`
45
+ * - `messages` AIMessageChunk tool chunks → `tool.start` (once per call id)
46
+ * - `messages` tool result → `tool.end`
47
+ * - `custom` `canvas.*` → passed through untouched
48
+ * - `error` → `error`
49
+ * - stream end → `message.end` + `done`
50
+ *
51
+ * The chunk shapes are pinned by a captured fixture from a real
52
+ * `langgraph dev` run (`__fixtures__/langgraph-run.json`) — notably, model
53
+ * content arrives as block arrays (`{type: "text" | "tool_use"}`), not plain
54
+ * strings.
55
+ */
56
+
57
+ /** One chunk from `client.runs.stream(...)` — the SDK's `{event, data}` pair. */
58
+ interface LangGraphStreamChunk {
59
+ event: string;
60
+ data: unknown;
61
+ }
62
+ /** Text of a message chunk — models may stream content as block lists. */
63
+ declare function chunkText(content: unknown): string;
64
+ declare function translateLangGraphStream(chunks: AsyncIterable<LangGraphStreamChunk> | Iterable<LangGraphStreamChunk>, options?: {
65
+ messageId?: string;
66
+ }): AsyncGenerator<StreamEvent>;
67
+
68
+ export { type LangGraphStreamChunk, type LangGraphTransportOptions, chunkText, langgraphTransport, threadUuid, translateLangGraphStream, withSelections };
@@ -0,0 +1,111 @@
1
+ import { Client } from '@langchain/langgraph-sdk';
2
+
3
+ // src/langgraph/transport.ts
4
+
5
+ // src/langgraph/translate.ts
6
+ function chunkText(content) {
7
+ if (typeof content === "string") return content;
8
+ if (Array.isArray(content)) {
9
+ return content.filter(
10
+ (block) => typeof block === "object" && block !== null && block.type === "text" && typeof block.text === "string"
11
+ ).map((block) => block.text).join("");
12
+ }
13
+ return "";
14
+ }
15
+ function isRecord(value) {
16
+ return typeof value === "object" && value !== null && !Array.isArray(value);
17
+ }
18
+ async function* translateLangGraphStream(chunks, options = {}) {
19
+ const messageId = options.messageId ?? `msg_${Math.random().toString(36).slice(2, 14)}`;
20
+ const startedTools = /* @__PURE__ */ new Set();
21
+ for await (const chunk of chunks) {
22
+ if (chunk.event === "error") {
23
+ const detail = typeof chunk.data === "string" ? chunk.data : JSON.stringify(chunk.data);
24
+ yield { type: "error", message: `agent run failed: ${detail}` };
25
+ continue;
26
+ }
27
+ if (chunk.event === "custom") {
28
+ if (isRecord(chunk.data) && String(chunk.data.type ?? "").startsWith("canvas.")) {
29
+ yield chunk.data;
30
+ }
31
+ continue;
32
+ }
33
+ if (chunk.event !== "messages" || !Array.isArray(chunk.data) || chunk.data.length === 0) {
34
+ continue;
35
+ }
36
+ const msg = chunk.data[0];
37
+ if (!isRecord(msg)) continue;
38
+ const meta = chunk.data.length > 1 && isRecord(chunk.data[1]) ? chunk.data[1] : {};
39
+ const fromToolsNode = meta.langgraph_node === "tools";
40
+ if (msg.type === "AIMessageChunk") {
41
+ const calls = Array.isArray(msg.tool_call_chunks) ? msg.tool_call_chunks : [];
42
+ for (const call of calls) {
43
+ if (!isRecord(call)) continue;
44
+ const id = typeof call.id === "string" ? call.id : null;
45
+ const name = typeof call.name === "string" ? call.name : null;
46
+ if (id && name && !startedTools.has(id)) {
47
+ startedTools.add(id);
48
+ yield { type: "tool.start", toolCallId: id, name };
49
+ }
50
+ }
51
+ const text = chunkText(msg.content);
52
+ if (text && !fromToolsNode) yield { type: "message.delta", messageId, text };
53
+ } else if (msg.type === "tool") {
54
+ const id = typeof msg.tool_call_id === "string" ? msg.tool_call_id : null;
55
+ if (id) yield { type: "tool.end", toolCallId: id, ok: msg.status !== "error" };
56
+ }
57
+ }
58
+ yield { type: "message.end", messageId };
59
+ yield { type: "done" };
60
+ }
61
+
62
+ // src/langgraph/transport.ts
63
+ function withSelections(message, selections) {
64
+ if (selections.length === 0) return message;
65
+ const listed = selections.map((s) => `- \`${s.selector}\` (data-cid=${s.cid})`).join("\n");
66
+ const artifactId = selections[0].artifactId;
67
+ return `${message}
68
+
69
+ [Targeted edit] Apply the change to these selected element(s) in file \`${artifactId}\`:
70
+ ${listed}
71
+ First call read_canvas on the file to get its current content and revision, then call edit_canvas with the element's exact current outer HTML as \`old\` and your replacement as \`new\` (keep the data-cid attribute).`;
72
+ }
73
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
74
+ var NAMESPACE_URL = "6ba7b811-9dad-11d1-80b4-00c04fd430c8";
75
+ async function threadUuid(threadId) {
76
+ if (UUID_RE.test(threadId)) return threadId.toLowerCase();
77
+ const name = new TextEncoder().encode(`canvas-thread:${threadId}`);
78
+ const namespace = NAMESPACE_URL.replace(/-/g, "");
79
+ const namespaceBytes = new Uint8Array(16);
80
+ for (let i = 0; i < 16; i++) namespaceBytes[i] = parseInt(namespace.slice(i * 2, i * 2 + 2), 16);
81
+ const payload = new Uint8Array(namespaceBytes.length + name.length);
82
+ payload.set(namespaceBytes);
83
+ payload.set(name, namespaceBytes.length);
84
+ const digest = new Uint8Array(await crypto.subtle.digest("SHA-1", payload)).slice(0, 16);
85
+ digest[6] = digest[6] & 15 | 80;
86
+ digest[8] = digest[8] & 63 | 128;
87
+ const hex = [...digest].map((b) => b.toString(16).padStart(2, "0")).join("");
88
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
89
+ }
90
+ function langgraphTransport(options) {
91
+ const client = new Client({ apiUrl: options.url, defaultHeaders: options.headers });
92
+ const knownThreads = /* @__PURE__ */ new Set();
93
+ return {
94
+ async *stream(request) {
95
+ const threadId = await threadUuid(request.threadId);
96
+ if (!knownThreads.has(threadId)) {
97
+ await client.threads.create({ threadId, ifExists: "do_nothing" });
98
+ knownThreads.add(threadId);
99
+ }
100
+ const message = withSelections(request.message, request.selections ?? []);
101
+ const chunks = client.runs.stream(threadId, options.assistantId, {
102
+ input: { messages: [{ role: "user", content: message }] },
103
+ streamMode: ["messages-tuple", "custom"],
104
+ signal: request.signal
105
+ });
106
+ yield* translateLangGraphStream(chunks);
107
+ }
108
+ };
109
+ }
110
+
111
+ export { chunkText, langgraphTransport, threadUuid, translateLangGraphStream, withSelections };
package/dist/styles.css CHANGED
@@ -143,8 +143,74 @@
143
143
  .cv-undo button:hover:not(:disabled) { background: var(--cv-surface); }
144
144
  .cv-undo button:disabled { opacity: 0.35; cursor: default; }
145
145
 
146
- .cv-versions { display: flex; align-items: center; gap: 6px; }
147
- .cv-versions__label { font-size: 12px; color: var(--cv-muted); font-variant-numeric: tabular-nums; }
146
+ .cv-versions { position: relative; display: flex; align-items: center; gap: 6px; }
147
+ .cv-versions__label {
148
+ font-size: 12px;
149
+ color: var(--cv-muted);
150
+ font-variant-numeric: tabular-nums;
151
+ border: none;
152
+ background: none;
153
+ cursor: pointer;
154
+ padding: 2px 4px;
155
+ border-radius: 6px;
156
+ }
157
+ .cv-versions__label:hover { background: var(--cv-surface); color: var(--cv-text); }
158
+ .cv-versions__list {
159
+ position: absolute;
160
+ top: calc(100% + 6px);
161
+ right: 0;
162
+ z-index: 30;
163
+ min-width: 220px;
164
+ max-height: 260px;
165
+ overflow-y: auto;
166
+ margin: 0;
167
+ padding: 4px;
168
+ list-style: none;
169
+ border: 1px solid var(--cv-border);
170
+ border-radius: 10px;
171
+ background: var(--cv-surface);
172
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
173
+ }
174
+ .cv-versions__list button {
175
+ display: flex;
176
+ gap: 8px;
177
+ align-items: baseline;
178
+ width: 100%;
179
+ padding: 6px 8px;
180
+ border: none;
181
+ border-radius: 6px;
182
+ background: none;
183
+ color: var(--cv-text);
184
+ text-align: left;
185
+ font-size: 12px;
186
+ cursor: pointer;
187
+ }
188
+ .cv-versions__list button:hover { background: var(--cv-bg); }
189
+ .cv-versions__list button.is-current { background: var(--cv-bg); font-weight: 600; }
190
+ .cv-versions__v { color: var(--cv-muted); font-variant-numeric: tabular-nums; flex: none; }
191
+ .cv-versions__desc { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
192
+
193
+ /* Historical snapshot view: content is visible and scrollable but not editable
194
+ (children lose hit-testing; the scroll container itself still gets the wheel). */
195
+ .cv-history-banner {
196
+ display: flex;
197
+ gap: 8px;
198
+ align-items: center;
199
+ padding: 6px 14px;
200
+ font-size: 12px;
201
+ color: var(--cv-muted);
202
+ background: var(--cv-surface);
203
+ border-bottom: 1px solid var(--cv-border);
204
+ }
205
+ .cv-history-banner button {
206
+ border: none;
207
+ background: none;
208
+ color: var(--cv-accent, #4f46e5);
209
+ cursor: pointer;
210
+ font-size: 12px;
211
+ padding: 0;
212
+ }
213
+ .cv-body--history > * { pointer-events: none; }
148
214
  .cv-versions__nav {
149
215
  width: 24px;
150
216
  height: 24px;
@@ -212,16 +278,6 @@
212
278
  .cv-body--flush { padding: 0; overflow: hidden; }
213
279
  .cv-fallback { color: var(--cv-muted); font-size: 14px; }
214
280
 
215
- /* Browsing an old version: a preview, not an editing surface. Interaction is
216
- blocked wholesale — renderers patch by artifact id, so edits made "in the
217
- past" would silently overwrite the live version. */
218
- .cv-body--history > :not(.cv-history-note) { pointer-events: none; user-select: none; }
219
- .cv-history-note {
220
- position: sticky; top: 0; z-index: 5; margin-bottom: 12px;
221
- padding: 6px 12px; border-radius: 8px; font-size: 12.5px; font-weight: 600;
222
- color: var(--cv-muted); background: var(--cv-surface); border: 1px solid var(--cv-border);
223
- }
224
-
225
281
  /* --- inline editing (document / table / slides) ------------------------------- */
226
282
 
227
283
  .cv-edit-toolbar { display: flex; justify-content: flex-end; margin-bottom: 10px; }
@@ -414,23 +470,6 @@
414
470
  .cv-sheet-tools__filter { min-width: 96px; max-width: 160px; }
415
471
  .cv-sheet-tools__sort:focus, .cv-sheet-tools__filter:focus { outline: none; border-color: var(--cv-accent); }
416
472
  .cv-sheet-tools__hint { margin-left: auto; font-size: 11px; color: var(--cv-muted); }
417
-
418
- /* selection formatting cluster — reads as one tight group between separators */
419
- .cv-sheet-tools__fmt { display: inline-flex; align-items: center; gap: 3px; }
420
- .cv-sheet-tools__fmt button:disabled,
421
- .cv-sheet-tools button:disabled { opacity: 0.4; cursor: default; }
422
- .cv-sheet-tools button:disabled:hover { border-color: var(--cv-border); color: var(--cv-text); }
423
- .cv-sheet-tools__bold, .cv-sheet-tools__align { width: 28px; padding: 4px 0 !important; text-align: center; }
424
- .cv-sheet-tools__bold--on,
425
- .cv-sheet-tools__freeze--on {
426
- background: var(--cv-accent-weak) !important; border-color: var(--cv-accent) !important;
427
- color: var(--cv-accent) !important;
428
- }
429
- .cv-sheet-tools__color {
430
- width: 28px; height: 26px; padding: 2px; border: 1px solid var(--cv-border);
431
- border-radius: 6px; background: var(--cv-bg); cursor: pointer;
432
- }
433
- .cv-sheet-tools__color:disabled { opacity: 0.4; cursor: default; }
434
473
  .cv-sheet { position: relative; width: 100%; flex: 1; height: auto; min-height: 0; }
435
474
  .cv-sheet .fortune-container { height: 100% !important; width: 100% !important; }
436
475
  .cv-sheet--empty { display: grid; place-items: center; height: 200px; color: var(--cv-muted); font-size: 14px; }
@@ -640,31 +679,6 @@
640
679
  .cv-free__fmt input[type="number"] { width: 46px; height: 24px; border: 1px solid var(--cv-border); border-radius: 5px; background: var(--cv-bg); color: var(--cv-text); font-size: 12px; padding: 0 4px; }
641
680
  .cv-free__fmt input[type="color"] { width: 26px; height: 24px; padding: 0; border: 1px solid var(--cv-border); border-radius: 5px; background: none; cursor: pointer; }
642
681
 
643
- /* multi-select: marquee, selection state, floating action bar */
644
- .cv-free { outline: none; } /* the canvas is focusable for keyboard nudge/shortcuts */
645
- .cv-free:focus-visible { outline: 2px solid var(--cv-accent); outline-offset: -2px; border-radius: 4px; }
646
- .cv-free__marquee {
647
- position: absolute; z-index: 7; pointer-events: none;
648
- border: 1px solid var(--cv-accent);
649
- background: color-mix(in srgb, var(--cv-accent) 10%, transparent);
650
- border-radius: 2px;
651
- }
652
- .cv-free__el--selected { outline: 1.5px solid var(--cv-accent); outline-offset: 1px; }
653
- .cv-free__multibar {
654
- position: absolute; z-index: 8; transform: translate(-50%, calc(-100% - 8px));
655
- display: flex; align-items: center; gap: 2px;
656
- padding: 3px; background: var(--cv-bg); border: 1px solid var(--cv-border);
657
- border-radius: 8px; box-shadow: 0 6px 20px rgba(0, 0, 0, 0.16); cursor: default;
658
- }
659
- .cv-free__multibar--below { transform: translate(-50%, 8px); }
660
- .cv-free__multibar button {
661
- min-width: 24px; height: 24px; padding: 0 6px; border: none; border-radius: 5px;
662
- background: transparent; color: var(--cv-text); cursor: pointer; font-size: 12px; white-space: nowrap;
663
- }
664
- .cv-free__multibar button:hover { background: var(--cv-surface); }
665
- .cv-free__multibar button:disabled { opacity: 0.4; cursor: default; background: transparent; }
666
- .cv-free__multibar-del:hover { background: #fee2e2 !important; color: #dc2626; }
667
-
668
682
  /* toolbar: background swatch + present button */
669
683
  .cv-deck__bg input[type="color"] {
670
684
  width: 30px; height: 30px; padding: 0;
@@ -708,22 +722,6 @@
708
722
  background: rgba(255, 255, 255, 0.06); border: 1px solid rgba(255, 255, 255, 0.08);
709
723
  border-radius: 10px; padding: 12px 16px; cursor: default; white-space: pre-wrap;
710
724
  }
711
- /* Deck progress: a hairline track pinned to the bottom edge, plus a counter chip. */
712
- .cv-present__progress {
713
- position: absolute; left: 0; right: 0; bottom: 0; height: 3px;
714
- background: rgba(255, 255, 255, 0.08);
715
- }
716
- .cv-present__progress-fill {
717
- height: 100%; background: var(--cv-accent);
718
- transition: width 0.28s ease;
719
- }
720
- @media (prefers-reduced-motion: reduce) { .cv-present__progress-fill { transition: none; } }
721
- .cv-present__count {
722
- position: absolute; top: 16px; right: 20px;
723
- color: #9aa4b2; font-size: 13px; font-variant-numeric: tabular-nums;
724
- background: rgba(255, 255, 255, 0.06); border: 1px solid rgba(255, 255, 255, 0.08);
725
- border-radius: 999px; padding: 3px 10px; cursor: default;
726
- }
727
725
 
728
726
  /* in-place editing cues */
729
727
  .cv-word .cv-doc { cursor: text; }
@@ -1154,36 +1152,6 @@
1154
1152
  color: var(--cv-muted);
1155
1153
  }
1156
1154
 
1157
- /* --- PDF viewer ---------------------------------------------------------------- */
1158
-
1159
- /* Fills the flush body outright — `flex: 1` alone collapsed the iframe to its
1160
- content height (a thin strip of page) because `.cv-body` is a block, not a
1161
- flex container. */
1162
- .cv-pdf-panel { display: flex; flex-direction: column; height: 100%; min-height: 480px; }
1163
- .cv-pdf-tools {
1164
- display: flex; align-items: center; gap: 6px; padding: 6px 10px;
1165
- border-bottom: 1px solid var(--cv-border); background: var(--cv-bg); flex: 0 0 auto;
1166
- }
1167
- .cv-pdf-tools button {
1168
- font-size: 12px; font-weight: 600; padding: 4px 10px; border: 1px solid var(--cv-border);
1169
- border-radius: 6px; background: var(--cv-bg); color: var(--cv-text); cursor: pointer;
1170
- }
1171
- .cv-pdf-tools button:hover { border-color: var(--cv-accent); color: var(--cv-accent); }
1172
- .cv-pdf-tools__name {
1173
- font-size: 12px; font-weight: 600; color: var(--cv-muted);
1174
- overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 55%;
1175
- }
1176
- .cv-pdf-tools__spacer { flex: 1; }
1177
- .cv-pdf-tools__sep { width: 1px; height: 18px; background: var(--cv-border); margin: 0 2px; }
1178
- .cv-pdf-tools__zoom { display: inline-flex; align-items: center; gap: 3px; }
1179
- .cv-pdf-tools__zoom button { min-width: 26px; }
1180
- .cv-pdf-tools__zoom-label {
1181
- min-width: 52px; font-variant-numeric: tabular-nums; color: var(--cv-muted);
1182
- }
1183
- .cv-pdf { position: relative; flex: 1; min-height: 0; background: var(--cv-surface); }
1184
- .cv-pdf__frame { display: block; width: 100%; height: 100%; border: 0; }
1185
- .cv-pdf--empty { display: grid; place-items: center; min-height: 200px; color: var(--cv-muted); }
1186
-
1187
1155
  /* --- inline artifact card (for the transcript) -------------------------------- */
1188
1156
 
1189
1157
  .cv-card {
@@ -1243,6 +1211,63 @@
1243
1211
 
1244
1212
  /* --- responsive --------------------------------------------------------------- */
1245
1213
 
1214
+ /* --- file artifact (uploads shown as themselves) ------------------------------ */
1215
+
1216
+ .cv-file {
1217
+ display: flex;
1218
+ flex-direction: column;
1219
+ align-items: center;
1220
+ gap: 14px;
1221
+ max-width: 640px;
1222
+ margin: 0 auto;
1223
+ }
1224
+ .cv-file__image,
1225
+ .cv-file__cover {
1226
+ max-width: 100%;
1227
+ max-height: 60vh;
1228
+ border: 1px solid var(--cv-border);
1229
+ border-radius: 12px;
1230
+ background: #fff;
1231
+ }
1232
+ .cv-file__excerpt {
1233
+ width: 100%;
1234
+ max-height: 40vh;
1235
+ overflow: auto;
1236
+ margin: 0;
1237
+ padding: 14px 16px;
1238
+ border: 1px solid var(--cv-border);
1239
+ border-radius: 12px;
1240
+ background: var(--cv-bg);
1241
+ font: 12px/1.6 ui-monospace, Menlo, Consolas, monospace;
1242
+ white-space: pre-wrap;
1243
+ word-break: break-word;
1244
+ color: var(--cv-text);
1245
+ }
1246
+ .cv-file__card {
1247
+ display: flex;
1248
+ align-items: center;
1249
+ gap: 10px;
1250
+ width: 100%;
1251
+ padding: 10px 14px;
1252
+ border: 1px solid var(--cv-border);
1253
+ border-radius: 12px;
1254
+ background: var(--cv-bg);
1255
+ }
1256
+ .cv-file__icon { font-size: 24px; line-height: 1; }
1257
+ .cv-file__meta { display: flex; flex-direction: column; min-width: 0; flex: 1; }
1258
+ .cv-file__meta b { font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1259
+ .cv-file__facts { font-size: 11px; color: var(--cv-muted); }
1260
+ .cv-file__download {
1261
+ flex: 0 0 auto;
1262
+ padding: 6px 12px;
1263
+ border: 1px solid var(--cv-border);
1264
+ border-radius: 8px;
1265
+ font-size: 12px;
1266
+ color: var(--cv-text);
1267
+ text-decoration: none;
1268
+ }
1269
+ .cv-file__download:hover { border-color: var(--cv-accent); }
1270
+
1246
1271
  /* Tablet / narrow desktop: tighten paddings, let toolbars wrap. */
1247
1272
  @media (max-width: 900px) {
1248
1273
  .cv-body { padding: 16px; }