claude-artifact-framework 0.7.0 → 0.7.1
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/README.md +43 -7
- package/dist/index.esm.js +299 -250
- package/dist/index.esm.js.map +3 -3
- package/dist/index.global.js +23 -13
- package/dist/index.global.js.map +4 -4
- package/package.json +1 -1
- package/src/app.js +68 -18
- package/src/blocks.js +2 -0
- package/src/chat.js +10 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-artifact-framework",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Utilities for building apps inside Claude Artifacts: platform storage, chat tool-calling loops, and other primitives verified against the real runtime.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.esm.js",
|
package/src/app.js
CHANGED
|
@@ -17,13 +17,13 @@ import { RecordsLayout } from "./records.js";
|
|
|
17
17
|
import { StepsLayout } from "./steps.js";
|
|
18
18
|
import { ChatLayout, CHAT_KEYS } from "./chat.js";
|
|
19
19
|
|
|
20
|
-
const LAYOUTS = ["panel", "records", "steps", "chat"];
|
|
20
|
+
const LAYOUTS = ["panel", "records", "steps", "chat", "workspace"];
|
|
21
21
|
const RESERVED = ["title", "empty", "wide", "onSelect", "subtitle"];
|
|
22
22
|
|
|
23
23
|
// Frontier models still invent identifiers a few percent of the time, so an
|
|
24
24
|
// unknown name has to fail loudly and name the alternatives rather than
|
|
25
25
|
// silently rendering nothing — a blank pane is indistinguishable from a bug.
|
|
26
|
-
const TOP_KEYS = ["title", "subtitle", "theme", "layout", "blocks", "records", "steps", "chat", "data", "shared"];
|
|
26
|
+
const TOP_KEYS = ["title", "subtitle", "theme", "layout", "blocks", "records", "steps", "chat", "data", "shared", "main", "sidebar"];
|
|
27
27
|
|
|
28
28
|
// An unknown field type used to fall through silently to a text input — the
|
|
29
29
|
// exact quiet degradation the framework exists to prevent (a blind-tested
|
|
@@ -44,6 +44,27 @@ function checkFields(fields, where) {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
function checkChatConfig(c, where) {
|
|
48
|
+
if (!c || (typeof c.system !== "string" && typeof c.system !== "function")) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`claude-artifact-framework: ${where} needs \`{ system: "..." }\` (a string or a function of data).`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
const unknown = Object.keys(c).filter((k) => !CHAT_KEYS.includes(k));
|
|
54
|
+
if (unknown.length) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`claude-artifact-framework: ${where} has unknown keys (${unknown.join(", ")}). Valid keys: ${CHAT_KEYS.join(", ")}.`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
for (const [name, tool] of Object.entries(c.tools || {})) {
|
|
60
|
+
if (!tool || typeof tool.description !== "string" || typeof tool.run !== "function") {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`claude-artifact-framework: chat tool "${name}" needs a \`description\` string and a \`run(input, ctx)\` function.`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
47
68
|
const ACTION_KEYS = ["label", "run", "when", "kind"];
|
|
48
69
|
|
|
49
70
|
function checkActions(actions, where) {
|
|
@@ -89,6 +110,7 @@ function checkBlocks(blocks) {
|
|
|
89
110
|
}
|
|
90
111
|
checkFields(block.fields, `blocks[${i}]`);
|
|
91
112
|
if (known[0] === "actions") checkActions(block.actions, `blocks[${i}].actions`);
|
|
113
|
+
if (known[0] === "chat") checkChatConfig(block.chat, `blocks[${i}].chat`);
|
|
92
114
|
});
|
|
93
115
|
}
|
|
94
116
|
|
|
@@ -138,25 +160,28 @@ function validate(spec) {
|
|
|
138
160
|
return layout;
|
|
139
161
|
}
|
|
140
162
|
if (layout === "chat") {
|
|
141
|
-
|
|
142
|
-
|
|
163
|
+
checkChatConfig(spec.chat, "chat");
|
|
164
|
+
return layout;
|
|
165
|
+
}
|
|
166
|
+
if (layout === "workspace") {
|
|
167
|
+
if (!Array.isArray(spec.main) || spec.main.length === 0) {
|
|
143
168
|
throw new Error(
|
|
144
|
-
'claude-artifact-framework: layout "
|
|
169
|
+
'claude-artifact-framework: layout "workspace" needs `main: [...]` (an array of blocks). `sidebar: [...]` is optional.'
|
|
145
170
|
);
|
|
146
171
|
}
|
|
147
|
-
|
|
148
|
-
if (
|
|
172
|
+
checkBlocks(spec.main);
|
|
173
|
+
if (spec.sidebar !== undefined) {
|
|
174
|
+
if (!Array.isArray(spec.sidebar)) {
|
|
175
|
+
throw new Error("claude-artifact-framework: workspace `sidebar` must be an array of blocks.");
|
|
176
|
+
}
|
|
177
|
+
checkBlocks(spec.sidebar);
|
|
178
|
+
}
|
|
179
|
+
const chats = [...spec.main, ...(spec.sidebar || [])].filter((b) => b && b.chat);
|
|
180
|
+
if (chats.length > 1) {
|
|
149
181
|
throw new Error(
|
|
150
|
-
|
|
182
|
+
"claude-artifact-framework: only one chat block per app — the conversation persists under data.chat and there is exactly one."
|
|
151
183
|
);
|
|
152
184
|
}
|
|
153
|
-
for (const [name, tool] of Object.entries(c.tools || {})) {
|
|
154
|
-
if (!tool || typeof tool.description !== "string" || typeof tool.run !== "function") {
|
|
155
|
-
throw new Error(
|
|
156
|
-
`claude-artifact-framework: chat tool "${name}" needs a \`description\` string and a \`run(input, ctx)\` function.`
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
185
|
return layout;
|
|
161
186
|
}
|
|
162
187
|
if (layout === "records") {
|
|
@@ -227,10 +252,12 @@ function defaultsFrom(spec) {
|
|
|
227
252
|
if ((spec.layout || "panel") === "records" && !Array.isArray(data.records)) {
|
|
228
253
|
data.records = [];
|
|
229
254
|
}
|
|
230
|
-
|
|
255
|
+
const allBlocks = [...(spec.blocks || []), ...(spec.main || []), ...(spec.sidebar || [])];
|
|
256
|
+
const hasChat = (spec.layout || "panel") === "chat" || allBlocks.some((b) => b && b.chat);
|
|
257
|
+
if (hasChat && !data.chat) {
|
|
231
258
|
data.chat = { api: [], log: [] };
|
|
232
259
|
}
|
|
233
|
-
for (const block of [...
|
|
260
|
+
for (const block of [...allBlocks, ...(spec.steps || [])]) {
|
|
234
261
|
for (const field of block.fields || []) {
|
|
235
262
|
if (field && field.key !== undefined && !(field.key in data)) {
|
|
236
263
|
data[field.key] = field.value !== undefined ? field.value : field.type === "check" ? false : "";
|
|
@@ -317,7 +344,20 @@ function RecordsWithBlocks({ spec, ctx }) {
|
|
|
317
344
|
);
|
|
318
345
|
}
|
|
319
346
|
|
|
320
|
-
|
|
347
|
+
// The "content + copilot" shape: a full-width main column and a sidebar.
|
|
348
|
+
// Stacks vertically on narrow screens (sidebar after main).
|
|
349
|
+
function WorkspaceLayout({ spec, ctx }) {
|
|
350
|
+
const render = (blocks) =>
|
|
351
|
+
(blocks || []).map((block, i) => h("section", { key: i, className: "caf-ws-slot" }, h(Block, { block, ctx })));
|
|
352
|
+
return h(
|
|
353
|
+
"div",
|
|
354
|
+
{ className: "caf-workspace" },
|
|
355
|
+
h("div", { className: "caf-ws-main" }, render(spec.main)),
|
|
356
|
+
spec.sidebar && spec.sidebar.length ? h("div", { className: "caf-ws-side" }, render(spec.sidebar)) : null
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const LAYOUT_COMPONENTS = { panel: Panel, records: RecordsWithBlocks, steps: StepsLayout, chat: ChatLayout, workspace: WorkspaceLayout };
|
|
321
361
|
|
|
322
362
|
export function createApp(spec = {}) {
|
|
323
363
|
const layout = validate(spec);
|
|
@@ -510,6 +550,16 @@ const BASE_CSS = `
|
|
|
510
550
|
.caf-md-h { display: block; margin-top: 0.3rem; }
|
|
511
551
|
.caf-md-li { display: block; padding-left: 1rem; position: relative; }
|
|
512
552
|
.caf-md-li::before { content: "•"; position: absolute; left: 0.2rem; opacity: 0.6; }
|
|
553
|
+
.caf-workspace { max-width: 1200px; margin: 0 auto; display: flex; gap: 1rem; align-items: flex-start; }
|
|
554
|
+
.caf-ws-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1rem; }
|
|
555
|
+
.caf-ws-side { width: 340px; flex: none; display: flex; flex-direction: column; gap: 1rem; position: sticky; top: 1rem; }
|
|
556
|
+
.caf-ws-slot { min-width: 0; }
|
|
557
|
+
.caf-chat-panel { display: flex; flex-direction: column; min-height: 0; }
|
|
558
|
+
.caf-ws-side .caf-chat-log { max-height: calc(100vh - 220px); }
|
|
559
|
+
@media (max-width: 900px) {
|
|
560
|
+
.caf-workspace { flex-direction: column; align-items: stretch; }
|
|
561
|
+
.caf-ws-side { width: 100%; position: static; }
|
|
562
|
+
}
|
|
513
563
|
.caf-empty, .caf-loading { color: var(--caf-muted); font-size: 0.9rem; text-align: center; padding: 1.6rem 1rem; }
|
|
514
564
|
.caf-block-error { border-color: var(--caf-danger); gap: 0.4rem; }
|
|
515
565
|
.caf-block-error strong { font-size: 0.9rem; color: var(--caf-danger); }
|
package/src/blocks.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// Bound lazily rather than snapshotted as `React.createElement`, because this
|
|
12
12
|
// runs at module load — before the host has had a chance to hand React over.
|
|
13
13
|
import { createElement as h, useState, useEffect } from "react";
|
|
14
|
+
import { ChatPanel } from "./chat.js";
|
|
14
15
|
|
|
15
16
|
const FIELD_TYPES = ["text", "textarea", "number", "money", "percent", "check", "date", "select"];
|
|
16
17
|
|
|
@@ -425,6 +426,7 @@ export const BLOCKS = {
|
|
|
425
426
|
chart: ChartBlock,
|
|
426
427
|
timer: TimerBlock,
|
|
427
428
|
actions: ActionsBlock,
|
|
429
|
+
chat: ({ spec, ctx }) => h(ChatPanel, { cfg: spec.chat, ctx }),
|
|
428
430
|
custom: CustomBlock,
|
|
429
431
|
};
|
|
430
432
|
|
package/src/chat.js
CHANGED
|
@@ -168,8 +168,11 @@ function Bubble({ m }) {
|
|
|
168
168
|
});
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
|
|
172
|
-
|
|
171
|
+
// The embeddable core: everything the chat layout does, as a single column
|
|
172
|
+
// that can live in a workspace sidebar (or any block slot) as well as fill
|
|
173
|
+
// the whole app. One conversation per app — it persists under data.chat.
|
|
174
|
+
export function ChatPanel({ cfg, ctx }) {
|
|
175
|
+
const c = cfg;
|
|
173
176
|
const [draft, setDraft] = useState("");
|
|
174
177
|
const [busy, setBusy] = useState(false);
|
|
175
178
|
const [live, setLive] = useState(null);
|
|
@@ -243,7 +246,7 @@ export function ChatLayout({ spec, ctx }) {
|
|
|
243
246
|
|
|
244
247
|
return h(
|
|
245
248
|
"div",
|
|
246
|
-
{ className: "caf-
|
|
249
|
+
{ className: "caf-chat-panel" },
|
|
247
250
|
h(
|
|
248
251
|
"div",
|
|
249
252
|
{ className: "caf-block caf-chat-log" },
|
|
@@ -287,3 +290,7 @@ export function ChatLayout({ spec, ctx }) {
|
|
|
287
290
|
)
|
|
288
291
|
);
|
|
289
292
|
}
|
|
293
|
+
|
|
294
|
+
export function ChatLayout({ spec, ctx }) {
|
|
295
|
+
return h("div", { className: "caf-panel caf-panel-single" }, h(ChatPanel, { cfg: spec.chat, ctx }));
|
|
296
|
+
}
|