@ai-gui/plugin-artifact 0.4.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.
- package/CHANGELOG.md +18 -0
- package/LICENSE +21 -0
- package/README.md +68 -0
- package/dist/index.cjs +1027 -0
- package/dist/index.d.cts +103 -0
- package/dist/index.d.ts +103 -0
- package/dist/index.js +1010 -0
- package/package.json +60 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1010 @@
|
|
|
1
|
+
//#region src/index.ts
|
|
2
|
+
const CREATE_KEYS = new Set([
|
|
3
|
+
"version",
|
|
4
|
+
"operationId",
|
|
5
|
+
"artifact"
|
|
6
|
+
]);
|
|
7
|
+
const ARTIFACT_KEYS = new Set([
|
|
8
|
+
"id",
|
|
9
|
+
"title",
|
|
10
|
+
"filename",
|
|
11
|
+
"kind",
|
|
12
|
+
"language",
|
|
13
|
+
"content"
|
|
14
|
+
]);
|
|
15
|
+
const UPDATE_KEYS = new Set([
|
|
16
|
+
"version",
|
|
17
|
+
"operationId",
|
|
18
|
+
"id",
|
|
19
|
+
"baseRevision",
|
|
20
|
+
"content",
|
|
21
|
+
"title",
|
|
22
|
+
"filename",
|
|
23
|
+
"language"
|
|
24
|
+
]);
|
|
25
|
+
const RECORD_KEYS = new Set([
|
|
26
|
+
"id",
|
|
27
|
+
"title",
|
|
28
|
+
"filename",
|
|
29
|
+
"kind",
|
|
30
|
+
"language",
|
|
31
|
+
"content",
|
|
32
|
+
"revision"
|
|
33
|
+
]);
|
|
34
|
+
const SNAPSHOT_KEYS = new Set([
|
|
35
|
+
"version",
|
|
36
|
+
"records",
|
|
37
|
+
"receipts"
|
|
38
|
+
]);
|
|
39
|
+
const RECEIPT_SNAPSHOT_KEYS = new Set([
|
|
40
|
+
"operationId",
|
|
41
|
+
"canonical",
|
|
42
|
+
"receipt"
|
|
43
|
+
]);
|
|
44
|
+
const RECEIPT_KEYS = new Set([
|
|
45
|
+
"operationId",
|
|
46
|
+
"command",
|
|
47
|
+
"record"
|
|
48
|
+
]);
|
|
49
|
+
const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
50
|
+
const SAFE_OPERATION_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
51
|
+
const KINDS = new Set([
|
|
52
|
+
"text",
|
|
53
|
+
"code",
|
|
54
|
+
"markdown",
|
|
55
|
+
"json"
|
|
56
|
+
]);
|
|
57
|
+
const DANGEROUS_KEYS = new Set([
|
|
58
|
+
"__proto__",
|
|
59
|
+
"prototype",
|
|
60
|
+
"constructor"
|
|
61
|
+
]);
|
|
62
|
+
const MAX_SOURCE_BYTES = 1024 * 1024;
|
|
63
|
+
const DEFAULT_MAX_ARTIFACTS = 100;
|
|
64
|
+
const DEFAULT_MAX_ARTIFACT_BYTES = 512 * 1024;
|
|
65
|
+
const DEFAULT_MAX_TOTAL_BYTES = 4 * 1024 * 1024;
|
|
66
|
+
const MAX_TITLE_LENGTH = 256;
|
|
67
|
+
const MAX_FILENAME_LENGTH = 180;
|
|
68
|
+
const MAX_LANGUAGE_LENGTH = 64;
|
|
69
|
+
const MAX_JSON_PREVIEW_CHARS = 256 * 1024;
|
|
70
|
+
var ArtifactError = class extends Error {};
|
|
71
|
+
var ArtifactValidationError = class extends ArtifactError {};
|
|
72
|
+
var ArtifactLimitError = class extends ArtifactError {};
|
|
73
|
+
var ArtifactNotFoundError = class extends ArtifactError {};
|
|
74
|
+
var ArtifactConflictError = class extends ArtifactError {};
|
|
75
|
+
var ArtifactOperationConflictError = class extends ArtifactError {};
|
|
76
|
+
var ArtifactSnapshotError = class extends ArtifactError {};
|
|
77
|
+
var ArtifactStore = class {
|
|
78
|
+
records = new Map();
|
|
79
|
+
receipts = new Map();
|
|
80
|
+
listeners = new Map();
|
|
81
|
+
allListeners = new Set();
|
|
82
|
+
epoch = 0;
|
|
83
|
+
limits;
|
|
84
|
+
constructor(options = {}) {
|
|
85
|
+
assertPlainOptions(options);
|
|
86
|
+
this.limits = {
|
|
87
|
+
maxArtifacts: readPositiveLimit(options.maxArtifacts, DEFAULT_MAX_ARTIFACTS, "maxArtifacts", DEFAULT_MAX_ARTIFACTS),
|
|
88
|
+
maxArtifactBytes: readPositiveLimit(options.maxArtifactBytes, DEFAULT_MAX_ARTIFACT_BYTES, "maxArtifactBytes", DEFAULT_MAX_ARTIFACT_BYTES),
|
|
89
|
+
maxTotalBytes: readPositiveLimit(options.maxTotalBytes, DEFAULT_MAX_TOTAL_BYTES, "maxTotalBytes", DEFAULT_MAX_TOTAL_BYTES)
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
create(command) {
|
|
93
|
+
const parsed = validateCreateValue(command);
|
|
94
|
+
if (!parsed.valid) throw new ArtifactValidationError(parsed.issues.join(" "));
|
|
95
|
+
const canonical = canonicalCreate(parsed.data);
|
|
96
|
+
const prior = this.checkReceipt(parsed.data.operationId, canonical);
|
|
97
|
+
if (prior) return prior;
|
|
98
|
+
if (this.records.has(parsed.data.artifact.id)) throw new ArtifactConflictError("Artifact already exists.");
|
|
99
|
+
const record = freezeRecord({
|
|
100
|
+
...parsed.data.artifact,
|
|
101
|
+
revision: 0
|
|
102
|
+
});
|
|
103
|
+
this.assertCapacity(record);
|
|
104
|
+
this.records.set(record.id, record);
|
|
105
|
+
const receipt = freezeReceipt({
|
|
106
|
+
operationId: parsed.data.operationId,
|
|
107
|
+
command: "create",
|
|
108
|
+
record
|
|
109
|
+
});
|
|
110
|
+
this.receipts.set(receipt.operationId, {
|
|
111
|
+
canonical,
|
|
112
|
+
receipt
|
|
113
|
+
});
|
|
114
|
+
this.mutated(record.id);
|
|
115
|
+
return receipt;
|
|
116
|
+
}
|
|
117
|
+
update(command) {
|
|
118
|
+
const parsed = validateUpdateValue(command);
|
|
119
|
+
if (!parsed.valid) throw new ArtifactValidationError(parsed.issues.join(" "));
|
|
120
|
+
const canonical = canonicalUpdate(parsed.data);
|
|
121
|
+
const prior = this.checkReceipt(parsed.data.operationId, canonical);
|
|
122
|
+
if (prior) return prior;
|
|
123
|
+
const current = this.records.get(parsed.data.id);
|
|
124
|
+
if (!current) throw new ArtifactNotFoundError("Artifact does not exist.");
|
|
125
|
+
if (current.revision !== parsed.data.baseRevision) throw new ArtifactConflictError("Artifact revision conflict.");
|
|
126
|
+
if (parsed.data.language !== void 0 && current.kind !== "code") throw new ArtifactValidationError("Language can be updated only for code artifacts.");
|
|
127
|
+
const record = freezeRecord({
|
|
128
|
+
...current,
|
|
129
|
+
content: parsed.data.content,
|
|
130
|
+
title: parsed.data.title ?? current.title,
|
|
131
|
+
filename: parsed.data.filename ?? current.filename,
|
|
132
|
+
...current.language === void 0 ? {} : { language: current.language },
|
|
133
|
+
...parsed.data.language === void 0 ? {} : { language: parsed.data.language },
|
|
134
|
+
revision: current.revision + 1
|
|
135
|
+
});
|
|
136
|
+
this.assertCapacity(record, current.id);
|
|
137
|
+
this.records.set(record.id, record);
|
|
138
|
+
const receipt = freezeReceipt({
|
|
139
|
+
operationId: parsed.data.operationId,
|
|
140
|
+
command: "update",
|
|
141
|
+
record
|
|
142
|
+
});
|
|
143
|
+
this.receipts.set(receipt.operationId, {
|
|
144
|
+
canonical,
|
|
145
|
+
receipt
|
|
146
|
+
});
|
|
147
|
+
this.mutated(record.id);
|
|
148
|
+
return receipt;
|
|
149
|
+
}
|
|
150
|
+
get(id) {
|
|
151
|
+
return this.records.get(id);
|
|
152
|
+
}
|
|
153
|
+
list() {
|
|
154
|
+
return Object.freeze([...this.records.values()]);
|
|
155
|
+
}
|
|
156
|
+
subscribe(id, listener) {
|
|
157
|
+
if (typeof listener !== "function") throw new TypeError("Artifact listener must be a function.");
|
|
158
|
+
let listeners = this.listeners.get(id);
|
|
159
|
+
if (!listeners) this.listeners.set(id, listeners = new Set());
|
|
160
|
+
listeners.add(listener);
|
|
161
|
+
let active = true;
|
|
162
|
+
return () => {
|
|
163
|
+
if (!active) return;
|
|
164
|
+
active = false;
|
|
165
|
+
listeners?.delete(listener);
|
|
166
|
+
if (listeners?.size === 0) this.listeners.delete(id);
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
subscribeAll(listener) {
|
|
170
|
+
if (typeof listener !== "function") throw new TypeError("Artifact listener must be a function.");
|
|
171
|
+
this.allListeners.add(listener);
|
|
172
|
+
let active = true;
|
|
173
|
+
return () => {
|
|
174
|
+
if (!active) return;
|
|
175
|
+
active = false;
|
|
176
|
+
this.allListeners.delete(listener);
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
delete(id) {
|
|
180
|
+
if (!this.records.delete(id)) return false;
|
|
181
|
+
this.mutated(id);
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
clear() {
|
|
185
|
+
if (this.records.size === 0 && this.receipts.size === 0) return;
|
|
186
|
+
const ids = [...this.records.keys()];
|
|
187
|
+
this.records.clear();
|
|
188
|
+
this.receipts.clear();
|
|
189
|
+
this.epoch++;
|
|
190
|
+
for (const id of ids) this.notifyOne(id);
|
|
191
|
+
this.notifyAll();
|
|
192
|
+
}
|
|
193
|
+
snapshot() {
|
|
194
|
+
return deepFreeze({
|
|
195
|
+
version: 1,
|
|
196
|
+
records: [...this.records.values()].map((record) => ({ ...record })),
|
|
197
|
+
receipts: [...this.receipts.entries()].map(([operationId, entry]) => ({
|
|
198
|
+
operationId,
|
|
199
|
+
canonical: entry.canonical,
|
|
200
|
+
receipt: {
|
|
201
|
+
...entry.receipt,
|
|
202
|
+
record: { ...entry.receipt.record }
|
|
203
|
+
}
|
|
204
|
+
}))
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
restore(snapshot) {
|
|
208
|
+
let restored;
|
|
209
|
+
try {
|
|
210
|
+
restored = validateSnapshot(snapshot, this.limits);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
throw new ArtifactSnapshotError(error instanceof Error ? error.message : "Invalid artifact snapshot.");
|
|
213
|
+
}
|
|
214
|
+
const oldIds = new Set(this.records.keys());
|
|
215
|
+
this.records = restored.records;
|
|
216
|
+
this.receipts = restored.receipts;
|
|
217
|
+
this.epoch++;
|
|
218
|
+
for (const id of this.records.keys()) oldIds.add(id);
|
|
219
|
+
for (const id of oldIds) this.notifyOne(id);
|
|
220
|
+
this.notifyAll();
|
|
221
|
+
}
|
|
222
|
+
captureMutationEpoch() {
|
|
223
|
+
return this.epoch;
|
|
224
|
+
}
|
|
225
|
+
checkReceipt(operationId, canonical) {
|
|
226
|
+
const prior = this.receipts.get(operationId);
|
|
227
|
+
if (!prior) return void 0;
|
|
228
|
+
if (prior.canonical !== canonical) throw new ArtifactOperationConflictError("Operation ID was already used for another command.");
|
|
229
|
+
return prior.receipt;
|
|
230
|
+
}
|
|
231
|
+
assertCapacity(next, replacingId) {
|
|
232
|
+
if (!replacingId && this.records.size >= this.limits.maxArtifacts) throw new ArtifactLimitError("Artifact count limit exceeded.");
|
|
233
|
+
const bytes = utf8Bytes(next.content);
|
|
234
|
+
if (bytes > this.limits.maxArtifactBytes) throw new ArtifactLimitError("Artifact size limit exceeded.");
|
|
235
|
+
let total = bytes;
|
|
236
|
+
for (const record of this.records.values()) if (record.id !== replacingId) total += utf8Bytes(record.content);
|
|
237
|
+
if (total > this.limits.maxTotalBytes) throw new ArtifactLimitError("Artifact total size limit exceeded.");
|
|
238
|
+
}
|
|
239
|
+
mutated(id) {
|
|
240
|
+
this.epoch++;
|
|
241
|
+
this.notifyOne(id);
|
|
242
|
+
this.notifyAll();
|
|
243
|
+
}
|
|
244
|
+
notifyOne(id) {
|
|
245
|
+
const record = this.records.get(id);
|
|
246
|
+
for (const listener of [...this.listeners.get(id) ?? []]) safeCall(listener, record);
|
|
247
|
+
}
|
|
248
|
+
notifyAll() {
|
|
249
|
+
const records = this.list();
|
|
250
|
+
for (const listener of [...this.allListeners]) safeCall(listener, records);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
function parseArtifactCreate(source) {
|
|
254
|
+
return parseCommand(source, validateCreateValue, "Artifact create command");
|
|
255
|
+
}
|
|
256
|
+
function parseArtifactUpdate(source) {
|
|
257
|
+
return parseCommand(source, validateUpdateValue, "Artifact update command");
|
|
258
|
+
}
|
|
259
|
+
function serializeArtifactCreate(command) {
|
|
260
|
+
const parsed = validateCreateValue(command);
|
|
261
|
+
if (!parsed.valid) throw new ArtifactValidationError(parsed.issues.join(" "));
|
|
262
|
+
return `\`\`\`artifact-create\n${JSON.stringify(parsed.data, null, 2)}\n\`\`\``;
|
|
263
|
+
}
|
|
264
|
+
function serializeArtifactUpdate(command) {
|
|
265
|
+
const parsed = validateUpdateValue(command);
|
|
266
|
+
if (!parsed.valid) throw new ArtifactValidationError(parsed.issues.join(" "));
|
|
267
|
+
return `\`\`\`artifact-update\n${JSON.stringify(parsed.data, null, 2)}\n\`\`\``;
|
|
268
|
+
}
|
|
269
|
+
function artifactPromptSpec(store) {
|
|
270
|
+
const records = store?.list() ?? [];
|
|
271
|
+
const current = records.length === 0 ? "Current artifacts: none." : `Current artifacts:\n${records.map((record) => `- ${record.id}: ${record.kind}, revision ${record.revision}, title ${JSON.stringify(record.title)}`).join("\n")}`;
|
|
272
|
+
return [
|
|
273
|
+
"Artifacts use only fenced JSON commands named exactly ```artifact-create and ```artifact-update.",
|
|
274
|
+
"Create schema: {version:1, operationId, artifact:{id,title,filename,kind:text|code|markdown|json,language?,content}}. Do not send a create revision.",
|
|
275
|
+
"Update schema: {version:1, operationId,id,baseRevision,content,title?,filename?,language?}. Updates replace full content; kind is immutable. There is no model delete command.",
|
|
276
|
+
"Use a new safe operationId for each intended mutation. Never claim success; the application commits commands only after validation.",
|
|
277
|
+
"Never emit HTML, scripts, executable URLs, actions, components, network requests, or other fence types. Markdown artifact content may contain Markdown and HTTPS links, but raw HTML never executes.",
|
|
278
|
+
current
|
|
279
|
+
].join("\n");
|
|
280
|
+
}
|
|
281
|
+
function artifact(options = {}) {
|
|
282
|
+
if (!isPlainObject(options)) throw new TypeError("artifact() options must be a plain object.");
|
|
283
|
+
rejectDangerousObjectKeys(options, "artifact options");
|
|
284
|
+
for (const key of Object.keys(options)) if (key !== "store") throw new TypeError(`Unknown artifact option: ${key}`);
|
|
285
|
+
const store = options.store ?? new ArtifactStore();
|
|
286
|
+
if (!(store instanceof ArtifactStore)) throw new TypeError("artifact store must be an ArtifactStore.");
|
|
287
|
+
const status = new WeakMap();
|
|
288
|
+
const outputs = new WeakMap();
|
|
289
|
+
let anchor;
|
|
290
|
+
const render = (node) => {
|
|
291
|
+
const cached = outputs.get(node);
|
|
292
|
+
if (cached) return cached;
|
|
293
|
+
const state = status.get(node) ?? (node.complete ? "rejected" : "loading");
|
|
294
|
+
let output;
|
|
295
|
+
if (node === anchor && state === "accepted") output = {
|
|
296
|
+
kind: "mount",
|
|
297
|
+
mount: (host) => mountArtifactWorkspace(host, store)
|
|
298
|
+
};
|
|
299
|
+
else if (state === "loading") output = {
|
|
300
|
+
kind: "element",
|
|
301
|
+
tag: "div",
|
|
302
|
+
props: {
|
|
303
|
+
"data-aigui-block-loading": "",
|
|
304
|
+
"data-block-type": node.type
|
|
305
|
+
},
|
|
306
|
+
children: []
|
|
307
|
+
};
|
|
308
|
+
else output = statusOutput(state === "accepted");
|
|
309
|
+
outputs.set(node, output);
|
|
310
|
+
return output;
|
|
311
|
+
};
|
|
312
|
+
return {
|
|
313
|
+
name: "artifact",
|
|
314
|
+
nodeRenderers: {
|
|
315
|
+
"artifact-create": render,
|
|
316
|
+
"artifact-update": render
|
|
317
|
+
},
|
|
318
|
+
onASTCommit: (nodes, context) => {
|
|
319
|
+
anchor = void 0;
|
|
320
|
+
for (const node of nodes) {
|
|
321
|
+
if (node.type !== "artifact-create" && node.type !== "artifact-update") continue;
|
|
322
|
+
if (!node.complete) {
|
|
323
|
+
status.set(node, "loading");
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
try {
|
|
327
|
+
if (node.type === "artifact-create") {
|
|
328
|
+
const parsed = parseArtifactCreate(node.content ?? "");
|
|
329
|
+
if (!parsed.valid) throw new ArtifactValidationError("Invalid command.");
|
|
330
|
+
store.create(parsed.data);
|
|
331
|
+
status.set(node, "accepted");
|
|
332
|
+
if (!anchor) anchor = node;
|
|
333
|
+
} else {
|
|
334
|
+
const parsed = parseArtifactUpdate(node.content ?? "");
|
|
335
|
+
if (!parsed.valid) throw new ArtifactValidationError("Invalid command.");
|
|
336
|
+
store.update(parsed.data);
|
|
337
|
+
status.set(node, "accepted");
|
|
338
|
+
}
|
|
339
|
+
context.emitDebug("artifact-command-committed", {
|
|
340
|
+
nodeType: node.type,
|
|
341
|
+
nodeKey: node.key
|
|
342
|
+
});
|
|
343
|
+
} catch (error) {
|
|
344
|
+
status.set(node, "rejected");
|
|
345
|
+
context.emitDebug("artifact-command-rejected", {
|
|
346
|
+
nodeType: node.type,
|
|
347
|
+
nodeKey: node.key,
|
|
348
|
+
errorName: error instanceof Error ? error.name : "Error"
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
promptSpec: () => artifactPromptSpec(store),
|
|
354
|
+
css: artifactCss
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
function mountArtifactWorkspace(host, store) {
|
|
358
|
+
if (!host || typeof host.replaceChildren !== "function") throw new TypeError("Artifact workspace requires an HTMLElement host.");
|
|
359
|
+
if (!(store instanceof ArtifactStore)) throw new TypeError("Artifact workspace requires an ArtifactStore.");
|
|
360
|
+
let selectedId;
|
|
361
|
+
let activeTab = "preview";
|
|
362
|
+
let disposed = false;
|
|
363
|
+
const objectUrls = new Set();
|
|
364
|
+
const timers = new Set();
|
|
365
|
+
const render = () => {
|
|
366
|
+
if (disposed) return;
|
|
367
|
+
const records = store.list();
|
|
368
|
+
if (!selectedId || !records.some((record) => record.id === selectedId)) selectedId = records[0]?.id;
|
|
369
|
+
const selected = selectedId ? store.get(selectedId) : void 0;
|
|
370
|
+
const root = element("section", {
|
|
371
|
+
"data-aigui-artifact-workspace": "",
|
|
372
|
+
"aria-label": "Artifact workspace"
|
|
373
|
+
});
|
|
374
|
+
const sidebar = element("nav", {
|
|
375
|
+
"data-artifact-list": "",
|
|
376
|
+
"aria-label": "Artifacts"
|
|
377
|
+
});
|
|
378
|
+
const list = element("ul");
|
|
379
|
+
for (const record of records) {
|
|
380
|
+
const item = element("li");
|
|
381
|
+
const button = element("button", {
|
|
382
|
+
type: "button",
|
|
383
|
+
"data-artifact-id": record.id,
|
|
384
|
+
"data-artifact-selected": String(record.id === selectedId),
|
|
385
|
+
"aria-current": record.id === selectedId ? "true" : "false"
|
|
386
|
+
}, record.title);
|
|
387
|
+
button.addEventListener("click", () => {
|
|
388
|
+
selectedId = record.id;
|
|
389
|
+
render();
|
|
390
|
+
});
|
|
391
|
+
item.append(button);
|
|
392
|
+
list.append(item);
|
|
393
|
+
}
|
|
394
|
+
sidebar.append(list);
|
|
395
|
+
const main = element("div", { "data-artifact-main": "" });
|
|
396
|
+
if (selected) {
|
|
397
|
+
const header = element("header", { "data-artifact-header": "" });
|
|
398
|
+
const identity = element("div", { "data-artifact-identity": "" });
|
|
399
|
+
identity.append(element("h2", void 0, selected.title));
|
|
400
|
+
identity.append(element("p", { "data-artifact-meta": "" }, `${selected.filename} · ${selected.kind} · revision ${selected.revision}`));
|
|
401
|
+
header.append(identity);
|
|
402
|
+
const actions = element("div", { "data-artifact-actions": "" });
|
|
403
|
+
const copy = element("button", {
|
|
404
|
+
type: "button",
|
|
405
|
+
"data-artifact-copy": ""
|
|
406
|
+
}, "Copy");
|
|
407
|
+
copy.addEventListener("click", () => {
|
|
408
|
+
copyText(selected.content);
|
|
409
|
+
});
|
|
410
|
+
const download = element("button", {
|
|
411
|
+
type: "button",
|
|
412
|
+
"data-artifact-download": ""
|
|
413
|
+
}, "Download");
|
|
414
|
+
download.addEventListener("click", () => downloadRecord(selected, objectUrls, timers));
|
|
415
|
+
actions.append(copy, download);
|
|
416
|
+
header.append(actions);
|
|
417
|
+
main.append(header);
|
|
418
|
+
const tabs = element("div", {
|
|
419
|
+
role: "tablist",
|
|
420
|
+
"aria-label": "Artifact view",
|
|
421
|
+
"data-artifact-tabs": ""
|
|
422
|
+
});
|
|
423
|
+
const previewTab = tabButton("preview", activeTab, () => {
|
|
424
|
+
activeTab = "preview";
|
|
425
|
+
render();
|
|
426
|
+
});
|
|
427
|
+
const sourceTab = tabButton("source", activeTab, () => {
|
|
428
|
+
activeTab = "source";
|
|
429
|
+
render();
|
|
430
|
+
});
|
|
431
|
+
const onTabKey = (event) => {
|
|
432
|
+
if (event.key !== "ArrowLeft" && event.key !== "ArrowRight" && event.key !== "Home" && event.key !== "End") return;
|
|
433
|
+
event.preventDefault();
|
|
434
|
+
activeTab = event.key === "ArrowLeft" || event.key === "Home" ? "preview" : "source";
|
|
435
|
+
render();
|
|
436
|
+
host.querySelector(`[data-artifact-tab="${activeTab}"]`)?.focus();
|
|
437
|
+
};
|
|
438
|
+
previewTab.addEventListener("keydown", onTabKey);
|
|
439
|
+
sourceTab.addEventListener("keydown", onTabKey);
|
|
440
|
+
tabs.append(previewTab, sourceTab);
|
|
441
|
+
main.append(tabs);
|
|
442
|
+
const panel = element("div", {
|
|
443
|
+
role: "tabpanel",
|
|
444
|
+
"data-artifact-panel": "",
|
|
445
|
+
tabindex: "0"
|
|
446
|
+
});
|
|
447
|
+
if (activeTab === "source") panel.append(sourceView(selected.content));
|
|
448
|
+
else panel.append(previewRecord(selected));
|
|
449
|
+
main.append(panel);
|
|
450
|
+
} else main.append(element("p", { "data-artifact-empty": "" }, "No artifacts available."));
|
|
451
|
+
root.append(sidebar, main);
|
|
452
|
+
host.replaceChildren(root);
|
|
453
|
+
};
|
|
454
|
+
const unsubscribe = store.subscribeAll(render);
|
|
455
|
+
render();
|
|
456
|
+
return () => {
|
|
457
|
+
if (disposed) return;
|
|
458
|
+
disposed = true;
|
|
459
|
+
unsubscribe();
|
|
460
|
+
for (const timer of timers) clearTimeout(timer);
|
|
461
|
+
timers.clear();
|
|
462
|
+
for (const url of objectUrls) URL.revokeObjectURL(url);
|
|
463
|
+
objectUrls.clear();
|
|
464
|
+
host.replaceChildren();
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
const artifactCss = `
|
|
468
|
+
[data-aigui-artifact-workspace]{display:grid;grid-template-columns:minmax(10rem,15rem) minmax(0,1fr);border:1px solid #d7dce2;border-radius:.75rem;overflow:hidden;background:#fff;color:#17202a;min-height:20rem}
|
|
469
|
+
[data-artifact-list]{border-right:1px solid #d7dce2;padding:.75rem;overflow:auto}[data-artifact-list] ul{list-style:none;margin:0;padding:0;display:grid;gap:.25rem}
|
|
470
|
+
[data-artifact-list] button{width:100%;text-align:left;border:0;border-radius:.4rem;padding:.55rem .65rem;background:transparent;color:inherit}[data-artifact-selected="true"]{background:#eef3f8!important;font-weight:600}
|
|
471
|
+
[data-artifact-main]{min-width:0;padding:1rem}[data-artifact-header]{display:flex;align-items:center;justify-content:space-between;gap:1rem}[data-artifact-header] h2{margin:0;font-size:1rem;overflow-wrap:anywhere}
|
|
472
|
+
[data-artifact-meta]{margin:.25rem 0 0;color:#66707a;font:12px/1.4 ui-monospace,monospace;overflow-wrap:anywhere}
|
|
473
|
+
[data-artifact-actions],[data-artifact-tabs]{display:flex;gap:.5rem}[data-artifact-tabs]{margin:.85rem 0;border-bottom:1px solid #d7dce2}[data-artifact-tabs] button{border:0;background:transparent;padding:.5rem .2rem}[data-artifact-tabs] [aria-selected="true"]{border-bottom:2px solid currentColor;font-weight:600}
|
|
474
|
+
[data-artifact-preview],[data-artifact-source]{white-space:pre-wrap;overflow-wrap:anywhere;max-width:100%;overflow:auto}[data-artifact-preview] pre,[data-artifact-source]{padding:.8rem;border-radius:.5rem;background:#f5f7f9}
|
|
475
|
+
@media(max-width:640px){[data-aigui-artifact-workspace]{grid-template-columns:1fr}[data-artifact-list]{border-right:0;border-bottom:1px solid #d7dce2;max-height:9rem}[data-artifact-header]{align-items:flex-start;flex-direction:column}}
|
|
476
|
+
`.trim();
|
|
477
|
+
function parseCommand(source, validate, label) {
|
|
478
|
+
if (typeof source !== "string") return {
|
|
479
|
+
valid: false,
|
|
480
|
+
issues: [`${label} must be JSON text.`]
|
|
481
|
+
};
|
|
482
|
+
if (utf8Bytes(source) > MAX_SOURCE_BYTES) return {
|
|
483
|
+
valid: false,
|
|
484
|
+
issues: [`${label} is too large.`]
|
|
485
|
+
};
|
|
486
|
+
let value;
|
|
487
|
+
try {
|
|
488
|
+
value = JSON.parse(source);
|
|
489
|
+
} catch {
|
|
490
|
+
return {
|
|
491
|
+
valid: false,
|
|
492
|
+
issues: [`${label} must be valid JSON.`]
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
return validate(value);
|
|
496
|
+
}
|
|
497
|
+
function validateCreateValue(value) {
|
|
498
|
+
const issues = [];
|
|
499
|
+
if (!isPlainObject(value)) return {
|
|
500
|
+
valid: false,
|
|
501
|
+
issues: ["$ must be a plain object."]
|
|
502
|
+
};
|
|
503
|
+
inspectObject(value, CREATE_KEYS, "$", issues);
|
|
504
|
+
if (value.version !== 1) issues.push("$.version must equal 1.");
|
|
505
|
+
const operationId = readSafeString(value.operationId, "$.operationId", issues, SAFE_OPERATION_ID, 128);
|
|
506
|
+
const artifact$1 = validateArtifact(value.artifact, "$.artifact", issues);
|
|
507
|
+
if (issues.length || !operationId || !artifact$1) return {
|
|
508
|
+
valid: false,
|
|
509
|
+
issues
|
|
510
|
+
};
|
|
511
|
+
return {
|
|
512
|
+
valid: true,
|
|
513
|
+
data: {
|
|
514
|
+
version: 1,
|
|
515
|
+
operationId,
|
|
516
|
+
artifact: artifact$1
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
function validateUpdateValue(value) {
|
|
521
|
+
const issues = [];
|
|
522
|
+
if (!isPlainObject(value)) return {
|
|
523
|
+
valid: false,
|
|
524
|
+
issues: ["$ must be a plain object."]
|
|
525
|
+
};
|
|
526
|
+
inspectObject(value, UPDATE_KEYS, "$", issues);
|
|
527
|
+
if (value.version !== 1) issues.push("$.version must equal 1.");
|
|
528
|
+
const operationId = readSafeString(value.operationId, "$.operationId", issues, SAFE_OPERATION_ID, 128);
|
|
529
|
+
const id = readSafeString(value.id, "$.id", issues, SAFE_ID, 128);
|
|
530
|
+
const baseRevision = readRevision(value.baseRevision, "$.baseRevision", issues);
|
|
531
|
+
const content = readString(value.content, "$.content", issues);
|
|
532
|
+
const title = value.title === void 0 ? void 0 : readBoundedString(value.title, "$.title", issues, MAX_TITLE_LENGTH);
|
|
533
|
+
const filename = value.filename === void 0 ? void 0 : readFilename(value.filename, "$.filename", issues);
|
|
534
|
+
const language = value.language === void 0 ? void 0 : readLanguage(value.language, "$.language", issues);
|
|
535
|
+
if (issues.length || !operationId || !id || baseRevision === void 0 || content === void 0) return {
|
|
536
|
+
valid: false,
|
|
537
|
+
issues
|
|
538
|
+
};
|
|
539
|
+
return {
|
|
540
|
+
valid: true,
|
|
541
|
+
data: {
|
|
542
|
+
version: 1,
|
|
543
|
+
operationId,
|
|
544
|
+
id,
|
|
545
|
+
baseRevision,
|
|
546
|
+
content,
|
|
547
|
+
...title === void 0 ? {} : { title },
|
|
548
|
+
...filename === void 0 ? {} : { filename },
|
|
549
|
+
...language === void 0 ? {} : { language }
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
function validateArtifact(value, path, issues) {
|
|
554
|
+
if (!isPlainObject(value)) {
|
|
555
|
+
issues.push(`${path} must be a plain object.`);
|
|
556
|
+
return void 0;
|
|
557
|
+
}
|
|
558
|
+
inspectObject(value, ARTIFACT_KEYS, path, issues);
|
|
559
|
+
const id = readSafeString(value.id, `${path}.id`, issues, SAFE_ID, 128);
|
|
560
|
+
const title = readBoundedString(value.title, `${path}.title`, issues, MAX_TITLE_LENGTH);
|
|
561
|
+
const filename = readFilename(value.filename, `${path}.filename`, issues);
|
|
562
|
+
const kindValue = readBoundedString(value.kind, `${path}.kind`, issues, 16);
|
|
563
|
+
const kind = kindValue && KINDS.has(kindValue) ? kindValue : void 0;
|
|
564
|
+
if (kindValue && !kind) issues.push(`${path}.kind is not supported.`);
|
|
565
|
+
const language = value.language === void 0 ? void 0 : readLanguage(value.language, `${path}.language`, issues);
|
|
566
|
+
const content = readString(value.content, `${path}.content`, issues);
|
|
567
|
+
if (language !== void 0 && kind !== "code") issues.push(`${path}.language is supported only for code artifacts.`);
|
|
568
|
+
if (!id || title === void 0 || !filename || !kind || content === void 0) return void 0;
|
|
569
|
+
return {
|
|
570
|
+
id,
|
|
571
|
+
title,
|
|
572
|
+
filename,
|
|
573
|
+
kind,
|
|
574
|
+
...language === void 0 ? {} : { language },
|
|
575
|
+
content
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
function validateRecord(value, path, issues) {
|
|
579
|
+
if (!isPlainObject(value)) {
|
|
580
|
+
issues.push(`${path} must be a plain object.`);
|
|
581
|
+
return void 0;
|
|
582
|
+
}
|
|
583
|
+
inspectObject(value, RECORD_KEYS, path, issues);
|
|
584
|
+
const artifact$1 = validateArtifactKeys(value, path, issues);
|
|
585
|
+
const revision = readRevision(value.revision, `${path}.revision`, issues);
|
|
586
|
+
if (!artifact$1 || revision === void 0) return void 0;
|
|
587
|
+
return freezeRecord({
|
|
588
|
+
...artifact$1,
|
|
589
|
+
revision
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
function validateArtifactKeys(value, path, issues) {
|
|
593
|
+
const id = readSafeString(value.id, `${path}.id`, issues, SAFE_ID, 128);
|
|
594
|
+
const title = readBoundedString(value.title, `${path}.title`, issues, MAX_TITLE_LENGTH);
|
|
595
|
+
const filename = readFilename(value.filename, `${path}.filename`, issues);
|
|
596
|
+
const kindValue = readBoundedString(value.kind, `${path}.kind`, issues, 16);
|
|
597
|
+
const kind = kindValue && KINDS.has(kindValue) ? kindValue : void 0;
|
|
598
|
+
if (kindValue && !kind) issues.push(`${path}.kind is not supported.`);
|
|
599
|
+
const language = value.language === void 0 ? void 0 : readLanguage(value.language, `${path}.language`, issues);
|
|
600
|
+
const content = readString(value.content, `${path}.content`, issues);
|
|
601
|
+
if (language !== void 0 && kind !== "code") issues.push(`${path}.language is supported only for code artifacts.`);
|
|
602
|
+
if (!id || title === void 0 || !filename || !kind || content === void 0) return void 0;
|
|
603
|
+
return {
|
|
604
|
+
id,
|
|
605
|
+
title,
|
|
606
|
+
filename,
|
|
607
|
+
kind,
|
|
608
|
+
...language === void 0 ? {} : { language },
|
|
609
|
+
content
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
function validateSnapshot(snapshot, limits) {
|
|
613
|
+
assertAcyclicPlainJSON(snapshot);
|
|
614
|
+
if (!isPlainObject(snapshot)) throw new Error("Snapshot must be a plain object.");
|
|
615
|
+
const issues = [];
|
|
616
|
+
inspectObject(snapshot, SNAPSHOT_KEYS, "$", issues);
|
|
617
|
+
if (snapshot.version !== 1) issues.push("$.version must equal 1.");
|
|
618
|
+
if (!Array.isArray(snapshot.records)) issues.push("$.records must be an array.");
|
|
619
|
+
if (!Array.isArray(snapshot.receipts)) issues.push("$.receipts must be an array.");
|
|
620
|
+
const records = new Map();
|
|
621
|
+
let total = 0;
|
|
622
|
+
if (Array.isArray(snapshot.records)) {
|
|
623
|
+
if (snapshot.records.length > limits.maxArtifacts) issues.push("Snapshot artifact count limit exceeded.");
|
|
624
|
+
snapshot.records.forEach((value, index) => {
|
|
625
|
+
const record = validateRecord(value, `$.records[${index}]`, issues);
|
|
626
|
+
if (!record) return;
|
|
627
|
+
if (records.has(record.id)) issues.push(`Duplicate artifact ID at $.records[${index}].`);
|
|
628
|
+
const bytes = utf8Bytes(record.content);
|
|
629
|
+
if (bytes > limits.maxArtifactBytes) issues.push(`Artifact size limit exceeded at $.records[${index}].`);
|
|
630
|
+
total += bytes;
|
|
631
|
+
records.set(record.id, record);
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
if (total > limits.maxTotalBytes) issues.push("Snapshot total size limit exceeded.");
|
|
635
|
+
const receipts = new Map();
|
|
636
|
+
if (Array.isArray(snapshot.receipts)) snapshot.receipts.forEach((value, index) => {
|
|
637
|
+
const path = `$.receipts[${index}]`;
|
|
638
|
+
if (!isPlainObject(value)) {
|
|
639
|
+
issues.push(`${path} must be a plain object.`);
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
inspectObject(value, RECEIPT_SNAPSHOT_KEYS, path, issues);
|
|
643
|
+
const operationId = readSafeString(value.operationId, `${path}.operationId`, issues, SAFE_OPERATION_ID, 128);
|
|
644
|
+
const canonical = readString(value.canonical, `${path}.canonical`, issues);
|
|
645
|
+
const receipt = validateReceipt(value.receipt, `${path}.receipt`, issues);
|
|
646
|
+
if (!operationId || canonical === void 0 || !receipt) return;
|
|
647
|
+
if (receipt.operationId !== operationId) issues.push(`${path} operation IDs must match.`);
|
|
648
|
+
if (receipts.has(operationId)) issues.push(`${path}.operationId must be unique.`);
|
|
649
|
+
let parsedCanonical;
|
|
650
|
+
try {
|
|
651
|
+
const parsed = JSON.parse(canonical);
|
|
652
|
+
parsedCanonical = receipt.command === "create" ? validateCreateValue(parsed) : validateUpdateValue(parsed);
|
|
653
|
+
} catch {
|
|
654
|
+
parsedCanonical = {
|
|
655
|
+
valid: false,
|
|
656
|
+
issues: ["Invalid canonical command."]
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
if (!parsedCanonical.valid || (receipt.command === "create" ? canonicalCreate(parsedCanonical.data) : canonicalUpdate(parsedCanonical.data)) !== canonical) issues.push(`${path}.canonical is invalid.`);
|
|
660
|
+
else if (!receiptMatchesCommand(receipt, parsedCanonical.data)) issues.push(`${path}.receipt does not match its canonical command.`);
|
|
661
|
+
receipts.set(operationId, {
|
|
662
|
+
canonical,
|
|
663
|
+
receipt
|
|
664
|
+
});
|
|
665
|
+
});
|
|
666
|
+
if (issues.length) throw new Error(issues.join(" "));
|
|
667
|
+
return {
|
|
668
|
+
records,
|
|
669
|
+
receipts
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
function validateReceipt(value, path, issues) {
|
|
673
|
+
if (!isPlainObject(value)) {
|
|
674
|
+
issues.push(`${path} must be a plain object.`);
|
|
675
|
+
return void 0;
|
|
676
|
+
}
|
|
677
|
+
inspectObject(value, RECEIPT_KEYS, path, issues);
|
|
678
|
+
const operationId = readSafeString(value.operationId, `${path}.operationId`, issues, SAFE_OPERATION_ID, 128);
|
|
679
|
+
const command = value.command === "create" || value.command === "update" ? value.command : void 0;
|
|
680
|
+
if (!command) issues.push(`${path}.command is invalid.`);
|
|
681
|
+
const record = validateRecord(value.record, `${path}.record`, issues);
|
|
682
|
+
if (!operationId || !command || !record) return void 0;
|
|
683
|
+
return freezeReceipt({
|
|
684
|
+
operationId,
|
|
685
|
+
command,
|
|
686
|
+
record
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
function canonicalCreate(command) {
|
|
690
|
+
return JSON.stringify({
|
|
691
|
+
version: 1,
|
|
692
|
+
operationId: command.operationId,
|
|
693
|
+
artifact: {
|
|
694
|
+
id: command.artifact.id,
|
|
695
|
+
title: command.artifact.title,
|
|
696
|
+
filename: command.artifact.filename,
|
|
697
|
+
kind: command.artifact.kind,
|
|
698
|
+
...command.artifact.language === void 0 ? {} : { language: command.artifact.language },
|
|
699
|
+
content: command.artifact.content
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
function canonicalUpdate(command) {
|
|
704
|
+
return JSON.stringify({
|
|
705
|
+
version: 1,
|
|
706
|
+
operationId: command.operationId,
|
|
707
|
+
id: command.id,
|
|
708
|
+
baseRevision: command.baseRevision,
|
|
709
|
+
content: command.content,
|
|
710
|
+
...command.title === void 0 ? {} : { title: command.title },
|
|
711
|
+
...command.filename === void 0 ? {} : { filename: command.filename },
|
|
712
|
+
...command.language === void 0 ? {} : { language: command.language }
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
function receiptMatchesCommand(receipt, command) {
|
|
716
|
+
if (receipt.operationId !== command.operationId) return false;
|
|
717
|
+
if (receipt.command === "create") {
|
|
718
|
+
if (!("artifact" in command) || receipt.record.revision !== 0) return false;
|
|
719
|
+
return sameArtifact(receipt.record, command.artifact);
|
|
720
|
+
}
|
|
721
|
+
if (!("id" in command)) return false;
|
|
722
|
+
if (receipt.record.id !== command.id || receipt.record.revision !== command.baseRevision + 1 || receipt.record.content !== command.content) return false;
|
|
723
|
+
if (command.title !== void 0 && receipt.record.title !== command.title) return false;
|
|
724
|
+
if (command.filename !== void 0 && receipt.record.filename !== command.filename) return false;
|
|
725
|
+
return command.language === void 0 || receipt.record.language === command.language;
|
|
726
|
+
}
|
|
727
|
+
function sameArtifact(record, artifact$1) {
|
|
728
|
+
return record.id === artifact$1.id && record.title === artifact$1.title && record.filename === artifact$1.filename && record.kind === artifact$1.kind && record.language === artifact$1.language && record.content === artifact$1.content;
|
|
729
|
+
}
|
|
730
|
+
function statusOutput(success) {
|
|
731
|
+
return {
|
|
732
|
+
kind: "element",
|
|
733
|
+
tag: "div",
|
|
734
|
+
props: {
|
|
735
|
+
"data-aigui-artifact-command": success ? "accepted" : "rejected",
|
|
736
|
+
role: "status"
|
|
737
|
+
},
|
|
738
|
+
children: [{
|
|
739
|
+
kind: "element",
|
|
740
|
+
tag: "span",
|
|
741
|
+
children: [{
|
|
742
|
+
kind: "html",
|
|
743
|
+
html: success ? "Artifact command accepted." : "Artifact command could not be applied."
|
|
744
|
+
}]
|
|
745
|
+
}]
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
function previewRecord(record) {
|
|
749
|
+
const container = element("div", {
|
|
750
|
+
"data-artifact-preview": "",
|
|
751
|
+
"data-artifact-kind": record.kind
|
|
752
|
+
});
|
|
753
|
+
if (record.kind === "markdown") container.append(renderMarkdown(record.content));
|
|
754
|
+
else if (record.kind === "json") container.append(renderJson(record.content));
|
|
755
|
+
else {
|
|
756
|
+
const pre = element("pre");
|
|
757
|
+
const code = element("code", record.kind === "code" && record.language ? { "data-language": record.language } : void 0);
|
|
758
|
+
code.textContent = record.content;
|
|
759
|
+
pre.append(code);
|
|
760
|
+
container.append(pre);
|
|
761
|
+
}
|
|
762
|
+
return container;
|
|
763
|
+
}
|
|
764
|
+
function sourceView(content) {
|
|
765
|
+
const pre = element("pre", { "data-artifact-source": "" });
|
|
766
|
+
const code = element("code");
|
|
767
|
+
code.textContent = content;
|
|
768
|
+
pre.append(code);
|
|
769
|
+
return pre;
|
|
770
|
+
}
|
|
771
|
+
function renderJson(content) {
|
|
772
|
+
const pre = element("pre");
|
|
773
|
+
const code = element("code");
|
|
774
|
+
try {
|
|
775
|
+
code.textContent = JSON.stringify(JSON.parse(content), null, 2);
|
|
776
|
+
} catch {
|
|
777
|
+
code.textContent = content;
|
|
778
|
+
}
|
|
779
|
+
pre.append(code);
|
|
780
|
+
return pre;
|
|
781
|
+
}
|
|
782
|
+
function renderMarkdown(content) {
|
|
783
|
+
const root = element("div", { "data-artifact-markdown": "" });
|
|
784
|
+
const lines = content.split(/\r\n|\r|\n/);
|
|
785
|
+
let paragraph = [];
|
|
786
|
+
let code;
|
|
787
|
+
const flushParagraph = () => {
|
|
788
|
+
if (!paragraph.length) return;
|
|
789
|
+
const p = element("p");
|
|
790
|
+
appendInlineMarkdown(p, paragraph.join(" "));
|
|
791
|
+
root.append(p);
|
|
792
|
+
paragraph = [];
|
|
793
|
+
};
|
|
794
|
+
const flushCode = () => {
|
|
795
|
+
if (!code) return;
|
|
796
|
+
const pre = element("pre");
|
|
797
|
+
const child = element("code");
|
|
798
|
+
child.textContent = code.join("\n");
|
|
799
|
+
pre.append(child);
|
|
800
|
+
root.append(pre);
|
|
801
|
+
code = void 0;
|
|
802
|
+
};
|
|
803
|
+
for (const line of lines) {
|
|
804
|
+
if (line.startsWith("```")) {
|
|
805
|
+
if (code) flushCode();
|
|
806
|
+
else {
|
|
807
|
+
flushParagraph();
|
|
808
|
+
code = [];
|
|
809
|
+
}
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
if (code) {
|
|
813
|
+
code.push(line);
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
const heading = /^(#{1,6})\s+(.*)$/.exec(line);
|
|
817
|
+
if (heading) {
|
|
818
|
+
flushParagraph();
|
|
819
|
+
const h = element(`h${heading[1].length}`);
|
|
820
|
+
appendInlineMarkdown(h, heading[2]);
|
|
821
|
+
root.append(h);
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
if (/^\s*[-*+]\s+/.test(line)) {
|
|
825
|
+
flushParagraph();
|
|
826
|
+
const ul = root.lastElementChild?.tagName === "UL" ? root.lastElementChild : element("ul");
|
|
827
|
+
if (!ul.parentNode) root.append(ul);
|
|
828
|
+
const li = element("li");
|
|
829
|
+
appendInlineMarkdown(li, line.replace(/^\s*[-*+]\s+/, ""));
|
|
830
|
+
ul.append(li);
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
if (line.trim() === "") {
|
|
834
|
+
flushParagraph();
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
paragraph.push(line);
|
|
838
|
+
}
|
|
839
|
+
flushParagraph();
|
|
840
|
+
flushCode();
|
|
841
|
+
return root;
|
|
842
|
+
}
|
|
843
|
+
function appendInlineMarkdown(parent, text) {
|
|
844
|
+
const pattern = /\[([^\]\n]{1,512})\]\(([^)\s]{1,2048})\)/g;
|
|
845
|
+
let offset = 0;
|
|
846
|
+
for (const match of text.matchAll(pattern)) {
|
|
847
|
+
const index = match.index ?? 0;
|
|
848
|
+
parent.append(document.createTextNode(text.slice(offset, index)));
|
|
849
|
+
const href = safeHttpsUrl(match[2]);
|
|
850
|
+
if (href) {
|
|
851
|
+
const link = element("a", {
|
|
852
|
+
href,
|
|
853
|
+
rel: "noreferrer noopener",
|
|
854
|
+
target: "_blank"
|
|
855
|
+
}, match[1]);
|
|
856
|
+
parent.append(link);
|
|
857
|
+
} else parent.append(document.createTextNode(match[0]));
|
|
858
|
+
offset = index + match[0].length;
|
|
859
|
+
}
|
|
860
|
+
parent.append(document.createTextNode(text.slice(offset)));
|
|
861
|
+
}
|
|
862
|
+
function safeHttpsUrl(value) {
|
|
863
|
+
try {
|
|
864
|
+
const url = new URL(value);
|
|
865
|
+
return url.protocol === "https:" && !url.username && !url.password ? url.href : void 0;
|
|
866
|
+
} catch {
|
|
867
|
+
return void 0;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
function tabButton(tab, active, activate) {
|
|
871
|
+
const button = element("button", {
|
|
872
|
+
type: "button",
|
|
873
|
+
role: "tab",
|
|
874
|
+
"data-artifact-tab": tab,
|
|
875
|
+
"aria-selected": String(tab === active),
|
|
876
|
+
tabindex: tab === active ? "0" : "-1"
|
|
877
|
+
}, tab === "preview" ? "Preview" : "Source");
|
|
878
|
+
button.addEventListener("click", activate);
|
|
879
|
+
return button;
|
|
880
|
+
}
|
|
881
|
+
async function copyText(content) {
|
|
882
|
+
try {
|
|
883
|
+
await navigator.clipboard?.writeText(content);
|
|
884
|
+
} catch {}
|
|
885
|
+
}
|
|
886
|
+
function downloadRecord(record, urls, timers) {
|
|
887
|
+
const blob = new Blob([record.content], { type: artifactMime(record) });
|
|
888
|
+
const url = URL.createObjectURL(blob);
|
|
889
|
+
urls.add(url);
|
|
890
|
+
const link = document.createElement("a");
|
|
891
|
+
link.href = url;
|
|
892
|
+
link.download = record.filename;
|
|
893
|
+
link.rel = "noopener";
|
|
894
|
+
link.click();
|
|
895
|
+
const timer = setTimeout(() => {
|
|
896
|
+
timers.delete(timer);
|
|
897
|
+
urls.delete(url);
|
|
898
|
+
URL.revokeObjectURL(url);
|
|
899
|
+
}, 0);
|
|
900
|
+
timers.add(timer);
|
|
901
|
+
}
|
|
902
|
+
function artifactMime(record) {
|
|
903
|
+
if (record.kind === "markdown") return "text/markdown;charset=utf-8";
|
|
904
|
+
if (record.kind === "json") return "application/json;charset=utf-8";
|
|
905
|
+
if (record.kind === "code") return "text/plain;charset=utf-8";
|
|
906
|
+
return "text/plain;charset=utf-8";
|
|
907
|
+
}
|
|
908
|
+
function element(tag, attrs, text) {
|
|
909
|
+
const node = document.createElement(tag);
|
|
910
|
+
for (const [key, value] of Object.entries(attrs ?? {})) node.setAttribute(key, value);
|
|
911
|
+
if (text !== void 0) node.textContent = text;
|
|
912
|
+
return node;
|
|
913
|
+
}
|
|
914
|
+
function inspectObject(value, allowed, path, issues) {
|
|
915
|
+
for (const key of Object.keys(value)) if (DANGEROUS_KEYS.has(key)) issues.push(`${path} contains a dangerous key.`);
|
|
916
|
+
else if (!allowed.has(key)) issues.push(`${path}.${key} is not allowed.`);
|
|
917
|
+
}
|
|
918
|
+
function readSafeString(value, path, issues, pattern, max) {
|
|
919
|
+
const string = readBoundedString(value, path, issues, max);
|
|
920
|
+
if (string !== void 0 && !pattern.test(string)) issues.push(`${path} is not safe.`);
|
|
921
|
+
return string;
|
|
922
|
+
}
|
|
923
|
+
function readBoundedString(value, path, issues, max) {
|
|
924
|
+
const string = readString(value, path, issues);
|
|
925
|
+
if (string !== void 0 && (string.length === 0 || string.length > max)) issues.push(`${path} must contain 1 to ${max} characters.`);
|
|
926
|
+
return string;
|
|
927
|
+
}
|
|
928
|
+
function readString(value, path, issues) {
|
|
929
|
+
if (typeof value !== "string") {
|
|
930
|
+
issues.push(`${path} must be a string.`);
|
|
931
|
+
return void 0;
|
|
932
|
+
}
|
|
933
|
+
return value;
|
|
934
|
+
}
|
|
935
|
+
function readFilename(value, path, issues) {
|
|
936
|
+
const filename = readBoundedString(value, path, issues, MAX_FILENAME_LENGTH);
|
|
937
|
+
if (filename !== void 0 && (filename === "." || filename === ".." || /[\\/\u0000-\u001f\u007f]/.test(filename))) issues.push(`${path} is not a safe filename.`);
|
|
938
|
+
return filename;
|
|
939
|
+
}
|
|
940
|
+
function readLanguage(value, path, issues) {
|
|
941
|
+
const language = readBoundedString(value, path, issues, MAX_LANGUAGE_LENGTH);
|
|
942
|
+
if (language !== void 0 && !/^[A-Za-z0-9][A-Za-z0-9_+.-]{0,63}$/.test(language)) issues.push(`${path} is not a safe language name.`);
|
|
943
|
+
return language;
|
|
944
|
+
}
|
|
945
|
+
function readRevision(value, path, issues) {
|
|
946
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
947
|
+
issues.push(`${path} must be a non-negative safe integer.`);
|
|
948
|
+
return void 0;
|
|
949
|
+
}
|
|
950
|
+
return value;
|
|
951
|
+
}
|
|
952
|
+
function isPlainObject(value) {
|
|
953
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
954
|
+
const proto = Object.getPrototypeOf(value);
|
|
955
|
+
return proto === Object.prototype || proto === null;
|
|
956
|
+
}
|
|
957
|
+
function assertPlainOptions(options) {
|
|
958
|
+
if (!isPlainObject(options)) throw new TypeError("ArtifactStore options must be a plain object.");
|
|
959
|
+
rejectDangerousObjectKeys(options, "ArtifactStore options");
|
|
960
|
+
for (const key of Object.keys(options)) if (key !== "maxArtifacts" && key !== "maxArtifactBytes" && key !== "maxTotalBytes") throw new TypeError(`Unknown ArtifactStore option: ${key}`);
|
|
961
|
+
}
|
|
962
|
+
function rejectDangerousObjectKeys(value, label) {
|
|
963
|
+
for (const key of Object.keys(value)) if (DANGEROUS_KEYS.has(key)) throw new TypeError(`${label} contains a dangerous key.`);
|
|
964
|
+
}
|
|
965
|
+
function readPositiveLimit(value, fallback, name, maximum) {
|
|
966
|
+
if (value === void 0) return fallback;
|
|
967
|
+
if (!Number.isSafeInteger(value) || value <= 0 || value > maximum) throw new TypeError(`${name} must be a positive safe integer no greater than ${maximum}.`);
|
|
968
|
+
return value;
|
|
969
|
+
}
|
|
970
|
+
function assertAcyclicPlainJSON(value, seen = new Set()) {
|
|
971
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return;
|
|
972
|
+
if (typeof value === "number") {
|
|
973
|
+
if (!Number.isFinite(value)) throw new Error("Snapshot contains a non-finite number.");
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
if (typeof value !== "object") throw new Error("Snapshot contains an unsupported value.");
|
|
977
|
+
if (seen.has(value)) throw new Error("Snapshot contains a cycle.");
|
|
978
|
+
seen.add(value);
|
|
979
|
+
if (Array.isArray(value)) for (const item of value) assertAcyclicPlainJSON(item, seen);
|
|
980
|
+
else {
|
|
981
|
+
if (!isPlainObject(value)) throw new Error("Snapshot contains a class instance.");
|
|
982
|
+
rejectDangerousObjectKeys(value, "Snapshot");
|
|
983
|
+
for (const item of Object.values(value)) assertAcyclicPlainJSON(item, seen);
|
|
984
|
+
}
|
|
985
|
+
seen.delete(value);
|
|
986
|
+
}
|
|
987
|
+
function utf8Bytes(value) {
|
|
988
|
+
return new TextEncoder().encode(value).byteLength;
|
|
989
|
+
}
|
|
990
|
+
function freezeRecord(record) {
|
|
991
|
+
return Object.freeze(record);
|
|
992
|
+
}
|
|
993
|
+
function freezeReceipt(receipt) {
|
|
994
|
+
return Object.freeze(receipt);
|
|
995
|
+
}
|
|
996
|
+
function deepFreeze(value) {
|
|
997
|
+
if (value && typeof value === "object") {
|
|
998
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
999
|
+
Object.freeze(value);
|
|
1000
|
+
}
|
|
1001
|
+
return value;
|
|
1002
|
+
}
|
|
1003
|
+
function safeCall(listener, value) {
|
|
1004
|
+
try {
|
|
1005
|
+
listener(value);
|
|
1006
|
+
} catch {}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
//#endregion
|
|
1010
|
+
export { ArtifactConflictError, ArtifactError, ArtifactLimitError, ArtifactNotFoundError, ArtifactOperationConflictError, ArtifactSnapshotError, ArtifactStore, ArtifactValidationError, artifact, artifactCss, artifactPromptSpec, mountArtifactWorkspace, parseArtifactCreate, parseArtifactUpdate, serializeArtifactCreate, serializeArtifactUpdate };
|