claude-artifact-framework 0.7.0 → 0.7.2

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/dist/index.esm.js CHANGED
@@ -134,7 +134,7 @@ var Component = React && React.Component || class ReactMissing {
134
134
  // src/appState.js
135
135
  var DATA_KEY = "caf:data";
136
136
  var VIEW_KEY = "caf:view";
137
- var EMPTY_VIEW = { screen: null, arg: null, stack: [], step: 0, selected: null };
137
+ var EMPTY_VIEW = { screen: null, arg: null, stack: [], step: 0, selected: null, tabs: {}, collapsed: {} };
138
138
  function isPlainObject(v) {
139
139
  return v !== null && typeof v === "object" && !Array.isArray(v);
140
140
  }
@@ -372,6 +372,241 @@ function seriesColors(seed, n) {
372
372
  return Array.from({ length: n }, (_, i) => hsl((h0 + i * 137.5) % 360, s, 52));
373
373
  }
374
374
 
375
+ // src/chat.js
376
+ var CHAT_KEYS = ["system", "greeting", "placeholder", "tools", "model", "maxTokens"];
377
+ var DEFAULT_MODEL = "claude-sonnet-4-6";
378
+ var MAX_ROUNDS = 8;
379
+ async function streamCall({ model, maxTokens, system, messages, onText }) {
380
+ const res = await fetch("https://api.anthropic.com/v1/messages", {
381
+ method: "POST",
382
+ headers: { "Content-Type": "application/json" },
383
+ body: JSON.stringify({ model, max_tokens: maxTokens, system, messages, stream: true })
384
+ });
385
+ if (!res.ok || !res.body) {
386
+ let detail = "";
387
+ try {
388
+ const b = await res.json();
389
+ detail = b && b.error && b.error.message || JSON.stringify(b);
390
+ } catch {
391
+ detail = await res.text().catch(() => "");
392
+ }
393
+ throw new Error(`HTTP ${res.status}${detail ? ": " + detail : ""}`);
394
+ }
395
+ const reader = res.body.getReader();
396
+ const decoder = new TextDecoder();
397
+ let full = "";
398
+ let buf = "";
399
+ for (; ; ) {
400
+ const { done, value } = await reader.read();
401
+ if (done) break;
402
+ buf += decoder.decode(value, { stream: true });
403
+ const lines = buf.split("\n");
404
+ buf = lines.pop();
405
+ for (const line of lines) {
406
+ if (!line.startsWith("data: ")) continue;
407
+ try {
408
+ const j = JSON.parse(line.slice(6));
409
+ if (j.type === "content_block_delta" && j.delta && j.delta.text) {
410
+ full += j.delta.text;
411
+ if (onText) onText(full);
412
+ }
413
+ } catch {
414
+ }
415
+ }
416
+ }
417
+ return full;
418
+ }
419
+ function parseToolCalls(text) {
420
+ const match = text.match(/```tool_call\s*\n?([\s\S]*?)```/);
421
+ if (!match) return null;
422
+ try {
423
+ const calls = JSON.parse(match[1]);
424
+ return Array.isArray(calls) && calls.length ? calls : null;
425
+ } catch {
426
+ return null;
427
+ }
428
+ }
429
+ function stripToolCallBlock(text) {
430
+ return text.replace(/```tool_call\s*\n?[\s\S]*?```/, "").trim();
431
+ }
432
+ function toolProtocol(tools) {
433
+ const list = Object.entries(tools).map(([name, t]) => `- ${name}: ${t.description}
434
+ Input: ${JSON.stringify(t.input || {})}`).join("\n");
435
+ return `
436
+
437
+ You have tools available:
438
+ ${list}
439
+
440
+ This environment does not support native function calling. To use a tool, reply ONLY with a \`\`\`tool_call code block containing a JSON array, with no other text in that turn. Example:
441
+
442
+ \`\`\`tool_call
443
+ [{ "name": "tool_name", "input": { "param": "value" } }]
444
+ \`\`\`
445
+
446
+ You may include several calls in one array. NEVER invent a tool's result \u2014 emit the pure tool_call block and wait for the real result, which arrives in the next message tagged [TOOL_RESULT]. When you have everything you need, answer in natural language with no tool_call block.`;
447
+ }
448
+ function runTools(calls, tools, ctx) {
449
+ return calls.map((call) => {
450
+ const tool = tools[call.name];
451
+ if (!tool) {
452
+ return { name: call.name, error: `unknown tool "${call.name}". Valid tools: ${Object.keys(tools).join(", ")}` };
453
+ }
454
+ try {
455
+ const result = tool.run(call.input || {}, ctx);
456
+ return { name: call.name, result: result === void 0 ? { ok: true } : result };
457
+ } catch (e) {
458
+ return { name: call.name, error: String(e && e.message || e) };
459
+ }
460
+ });
461
+ }
462
+ var escHtml = (t) => String(t).replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
463
+ function renderMarkdown(text) {
464
+ const codeBlocks = [];
465
+ let t = String(text).replace(/```[a-z]*\n?([\s\S]*?)```/g, (m, code) => {
466
+ codeBlocks.push(code.replace(/\n$/, ""));
467
+ return "\0" + (codeBlocks.length - 1) + "\0";
468
+ });
469
+ t = escHtml(t);
470
+ t = t.replace(/`([^`]+)`/g, "<code>$1</code>");
471
+ t = t.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
472
+ t = t.replace(/(^|[^*])\*([^*\n]+)\*/g, "$1<em>$2</em>");
473
+ t = t.replace(/^#{1,3} (.+)$/gm, '<strong class="caf-md-h">$1</strong>');
474
+ t = t.replace(/^[-•*] (.+)$/gm, '<span class="caf-md-li">$1</span>');
475
+ t = t.replace(/^\d+\. (.+)$/gm, '<span class="caf-md-li caf-md-num">$1</span>');
476
+ t = t.replace(/\n/g, "<br>");
477
+ t = t.replace(/\u0000(\d+)\u0000/g, (m, i) => '<pre class="caf-code">' + escHtml(codeBlocks[Number(i)]) + "</pre>");
478
+ return t;
479
+ }
480
+ function Bubble({ m }) {
481
+ if (m.role === "tools") {
482
+ return createElement("div", { className: "caf-msg-tools" }, `used: ${m.tools.join(", ")}`);
483
+ }
484
+ if (m.role === "error") {
485
+ return createElement(
486
+ "div",
487
+ { className: "caf-msg caf-msg-error" },
488
+ createElement("strong", null, "The call failed"),
489
+ createElement("span", null, m.content),
490
+ createElement("span", { className: "caf-hint" }, "Your message is kept \u2014 send again to retry.")
491
+ );
492
+ }
493
+ if (m.role === "user") return createElement("div", { className: "caf-msg caf-msg-user" }, m.content);
494
+ return createElement("div", {
495
+ className: "caf-msg caf-msg-assistant",
496
+ dangerouslySetInnerHTML: { __html: renderMarkdown(m.content) }
497
+ });
498
+ }
499
+ function ChatPanel({ cfg, ctx }) {
500
+ const c = cfg;
501
+ const [draft, setDraft] = useState("");
502
+ const [busy, setBusy] = useState(false);
503
+ const [live, setLive] = useState(null);
504
+ const endRef = useRef(null);
505
+ const chat = ctx.data.chat || { api: [], log: [] };
506
+ const log = chat.log || [];
507
+ useEffect(() => {
508
+ if (endRef.current) endRef.current.scrollIntoView({ block: "end" });
509
+ }, [log.length, live]);
510
+ async function send() {
511
+ const text = draft.trim();
512
+ if (!text || busy) return;
513
+ setDraft("");
514
+ setBusy(true);
515
+ let api = [...chat.api || [], { role: "user", content: text }];
516
+ let logNow = [...log, { role: "user", content: text }];
517
+ const commit = () => ctx.update((d) => {
518
+ d.chat = { api, log: logNow };
519
+ });
520
+ commit();
521
+ try {
522
+ const system = (typeof c.system === "function" ? c.system(ctx.data) : c.system || "") + (c.tools ? toolProtocol(c.tools) : "");
523
+ for (let round = 0; round < MAX_ROUNDS; round++) {
524
+ const raw = await streamCall({
525
+ model: c.model || DEFAULT_MODEL,
526
+ maxTokens: c.maxTokens || 2e3,
527
+ system,
528
+ messages: api,
529
+ onText: (t) => setLive(stripToolCallBlock(t))
530
+ });
531
+ api = [...api, { role: "assistant", content: raw }];
532
+ const shown = stripToolCallBlock(raw);
533
+ const calls = c.tools ? parseToolCalls(raw) : null;
534
+ if (!calls) {
535
+ logNow = [...logNow, { role: "assistant", content: shown || raw }];
536
+ commit();
537
+ break;
538
+ }
539
+ const results = runTools(calls, c.tools, ctx);
540
+ if (shown) logNow = [...logNow, { role: "assistant", content: shown }];
541
+ logNow = [...logNow, { role: "tools", tools: calls.map((x) => x.name) }];
542
+ api = [
543
+ ...api,
544
+ {
545
+ role: "user",
546
+ content: `[TOOL_RESULT]
547
+ ${JSON.stringify(results)}
548
+ [/TOOL_RESULT]
549
+
550
+ Continue with this information. If you have everything you need, answer in natural language without further tool_call.`
551
+ }
552
+ ];
553
+ commit();
554
+ setLive(null);
555
+ }
556
+ } catch (e) {
557
+ logNow = [...logNow, { role: "error", content: String(e && e.message || e) }];
558
+ commit();
559
+ } finally {
560
+ setLive(null);
561
+ setBusy(false);
562
+ }
563
+ }
564
+ return createElement(
565
+ "div",
566
+ { className: "caf-chat-panel" },
567
+ createElement(
568
+ "div",
569
+ { className: "caf-block caf-chat-log" },
570
+ c.greeting ? createElement("div", { className: "caf-msg caf-msg-assistant", dangerouslySetInnerHTML: { __html: renderMarkdown(c.greeting) } }) : null,
571
+ log.map((m, i) => createElement(Bubble, { key: i, m })),
572
+ live !== null ? createElement("div", { className: "caf-msg caf-msg-assistant caf-msg-live", dangerouslySetInnerHTML: { __html: renderMarkdown(live || "\u2026") } }) : null,
573
+ busy && live === null ? createElement("div", { className: "caf-msg-tools" }, "thinking\u2026") : null,
574
+ createElement("div", { ref: endRef })
575
+ ),
576
+ // Deliberately NOT a <form>: the artifact iframe's sandbox lacks
577
+ // allow-forms, so native form submission is blocked there — and on mobile
578
+ // the keyboard's send key triggers exactly that implicit submission. A
579
+ // plain div with onClick + Enter via onKeyDown never touches the native
580
+ // form machinery, so it can't be sandboxed away.
581
+ createElement(
582
+ "div",
583
+ { className: "caf-chat-input" },
584
+ createElement("input", {
585
+ value: draft,
586
+ placeholder: c.placeholder || "Message\u2026",
587
+ disabled: busy,
588
+ enterKeyHint: "send",
589
+ onChange: (e) => setDraft(e.target.value),
590
+ onKeyDown: (e) => {
591
+ if (e.key === "Enter") {
592
+ e.preventDefault();
593
+ send();
594
+ }
595
+ },
596
+ "aria-label": "Message"
597
+ }),
598
+ createElement(
599
+ "button",
600
+ { type: "button", className: "caf-btn caf-btn-primary", disabled: busy || !draft.trim(), onClick: send },
601
+ "Send"
602
+ )
603
+ )
604
+ );
605
+ }
606
+ function ChatLayout({ spec, ctx }) {
607
+ return createElement("div", { className: "caf-panel caf-panel-single" }, createElement(ChatPanel, { cfg: spec.chat, ctx }));
608
+ }
609
+
375
610
  // src/blocks.js
376
611
  var FIELD_TYPES = ["text", "textarea", "number", "money", "percent", "check", "date", "select"];
377
612
  function formatValue(value, format) {
@@ -738,6 +973,7 @@ var BLOCKS = {
738
973
  chart: ChartBlock,
739
974
  timer: TimerBlock,
740
975
  actions: ActionsBlock,
976
+ chat: ({ spec, ctx }) => createElement(ChatPanel, { cfg: spec.chat, ctx }),
741
977
  custom: CustomBlock
742
978
  };
743
979
  var BLOCK_NAMES = Object.keys(BLOCKS);
@@ -1079,253 +1315,41 @@ function StepsLayout({ spec, ctx }) {
1079
1315
  );
1080
1316
  }
1081
1317
 
1082
- // src/chat.js
1083
- var CHAT_KEYS = ["system", "greeting", "placeholder", "tools", "model", "maxTokens"];
1084
- var DEFAULT_MODEL = "claude-sonnet-4-6";
1085
- var MAX_ROUNDS = 8;
1086
- async function streamCall({ model, maxTokens, system, messages, onText }) {
1087
- const res = await fetch("https://api.anthropic.com/v1/messages", {
1088
- method: "POST",
1089
- headers: { "Content-Type": "application/json" },
1090
- body: JSON.stringify({ model, max_tokens: maxTokens, system, messages, stream: true })
1091
- });
1092
- if (!res.ok || !res.body) {
1093
- let detail = "";
1094
- try {
1095
- const b = await res.json();
1096
- detail = b && b.error && b.error.message || JSON.stringify(b);
1097
- } catch {
1098
- detail = await res.text().catch(() => "");
1318
+ // src/app.js
1319
+ var LAYOUTS = ["panel", "records", "steps", "chat", "workspace"];
1320
+ var RESERVED = ["title", "empty", "wide", "onSelect", "subtitle", "when", "collapsible"];
1321
+ var TOP_KEYS = ["title", "subtitle", "theme", "layout", "blocks", "records", "steps", "chat", "data", "shared", "main", "sidebar"];
1322
+ function checkFields(fields, where) {
1323
+ for (const f of fields || []) {
1324
+ if (!f || !f.key) throw new Error(`claude-artifact-framework: every field in ${where} needs a \`key\`.`);
1325
+ if (f.type !== void 0 && !FIELD_TYPES.includes(f.type)) {
1326
+ throw new Error(
1327
+ `claude-artifact-framework: field "${f.key}" in ${where} has unknown type "${f.type}". Valid types: ${FIELD_TYPES.join(", ")}.`
1328
+ );
1099
1329
  }
1100
- throw new Error(`HTTP ${res.status}${detail ? ": " + detail : ""}`);
1101
- }
1102
- const reader = res.body.getReader();
1103
- const decoder = new TextDecoder();
1104
- let full = "";
1105
- let buf = "";
1106
- for (; ; ) {
1107
- const { done, value } = await reader.read();
1108
- if (done) break;
1109
- buf += decoder.decode(value, { stream: true });
1110
- const lines = buf.split("\n");
1111
- buf = lines.pop();
1112
- for (const line of lines) {
1113
- if (!line.startsWith("data: ")) continue;
1114
- try {
1115
- const j = JSON.parse(line.slice(6));
1116
- if (j.type === "content_block_delta" && j.delta && j.delta.text) {
1117
- full += j.delta.text;
1118
- if (onText) onText(full);
1119
- }
1120
- } catch {
1121
- }
1330
+ if (f.type === "select" && f.options !== void 0 && !Array.isArray(f.options) && typeof f.options !== "function") {
1331
+ throw new Error(
1332
+ `claude-artifact-framework: field "${f.key}" in ${where}: \`options\` must be an array or a function of data returning one.`
1333
+ );
1122
1334
  }
1123
1335
  }
1124
- return full;
1125
- }
1126
- function parseToolCalls(text) {
1127
- const match = text.match(/```tool_call\s*\n?([\s\S]*?)```/);
1128
- if (!match) return null;
1129
- try {
1130
- const calls = JSON.parse(match[1]);
1131
- return Array.isArray(calls) && calls.length ? calls : null;
1132
- } catch {
1133
- return null;
1134
- }
1135
- }
1136
- function stripToolCallBlock(text) {
1137
- return text.replace(/```tool_call\s*\n?[\s\S]*?```/, "").trim();
1138
- }
1139
- function toolProtocol(tools) {
1140
- const list = Object.entries(tools).map(([name, t]) => `- ${name}: ${t.description}
1141
- Input: ${JSON.stringify(t.input || {})}`).join("\n");
1142
- return `
1143
-
1144
- You have tools available:
1145
- ${list}
1146
-
1147
- This environment does not support native function calling. To use a tool, reply ONLY with a \`\`\`tool_call code block containing a JSON array, with no other text in that turn. Example:
1148
-
1149
- \`\`\`tool_call
1150
- [{ "name": "tool_name", "input": { "param": "value" } }]
1151
- \`\`\`
1152
-
1153
- You may include several calls in one array. NEVER invent a tool's result \u2014 emit the pure tool_call block and wait for the real result, which arrives in the next message tagged [TOOL_RESULT]. When you have everything you need, answer in natural language with no tool_call block.`;
1154
1336
  }
1155
- function runTools(calls, tools, ctx) {
1156
- return calls.map((call) => {
1157
- const tool = tools[call.name];
1158
- if (!tool) {
1159
- return { name: call.name, error: `unknown tool "${call.name}". Valid tools: ${Object.keys(tools).join(", ")}` };
1160
- }
1161
- try {
1162
- const result = tool.run(call.input || {}, ctx);
1163
- return { name: call.name, result: result === void 0 ? { ok: true } : result };
1164
- } catch (e) {
1165
- return { name: call.name, error: String(e && e.message || e) };
1166
- }
1167
- });
1168
- }
1169
- var escHtml = (t) => String(t).replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
1170
- function renderMarkdown(text) {
1171
- const codeBlocks = [];
1172
- let t = String(text).replace(/```[a-z]*\n?([\s\S]*?)```/g, (m, code) => {
1173
- codeBlocks.push(code.replace(/\n$/, ""));
1174
- return "\0" + (codeBlocks.length - 1) + "\0";
1175
- });
1176
- t = escHtml(t);
1177
- t = t.replace(/`([^`]+)`/g, "<code>$1</code>");
1178
- t = t.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
1179
- t = t.replace(/(^|[^*])\*([^*\n]+)\*/g, "$1<em>$2</em>");
1180
- t = t.replace(/^#{1,3} (.+)$/gm, '<strong class="caf-md-h">$1</strong>');
1181
- t = t.replace(/^[-•*] (.+)$/gm, '<span class="caf-md-li">$1</span>');
1182
- t = t.replace(/^\d+\. (.+)$/gm, '<span class="caf-md-li caf-md-num">$1</span>');
1183
- t = t.replace(/\n/g, "<br>");
1184
- t = t.replace(/\u0000(\d+)\u0000/g, (m, i) => '<pre class="caf-code">' + escHtml(codeBlocks[Number(i)]) + "</pre>");
1185
- return t;
1186
- }
1187
- function Bubble({ m }) {
1188
- if (m.role === "tools") {
1189
- return createElement("div", { className: "caf-msg-tools" }, `used: ${m.tools.join(", ")}`);
1190
- }
1191
- if (m.role === "error") {
1192
- return createElement(
1193
- "div",
1194
- { className: "caf-msg caf-msg-error" },
1195
- createElement("strong", null, "The call failed"),
1196
- createElement("span", null, m.content),
1197
- createElement("span", { className: "caf-hint" }, "Your message is kept \u2014 send again to retry.")
1337
+ function checkChatConfig(c, where) {
1338
+ if (!c || typeof c.system !== "string" && typeof c.system !== "function") {
1339
+ throw new Error(
1340
+ `claude-artifact-framework: ${where} needs \`{ system: "..." }\` (a string or a function of data).`
1198
1341
  );
1199
1342
  }
1200
- if (m.role === "user") return createElement("div", { className: "caf-msg caf-msg-user" }, m.content);
1201
- return createElement("div", {
1202
- className: "caf-msg caf-msg-assistant",
1203
- dangerouslySetInnerHTML: { __html: renderMarkdown(m.content) }
1204
- });
1205
- }
1206
- function ChatLayout({ spec, ctx }) {
1207
- const c = spec.chat;
1208
- const [draft, setDraft] = useState("");
1209
- const [busy, setBusy] = useState(false);
1210
- const [live, setLive] = useState(null);
1211
- const endRef = useRef(null);
1212
- const chat = ctx.data.chat || { api: [], log: [] };
1213
- const log = chat.log || [];
1214
- useEffect(() => {
1215
- if (endRef.current) endRef.current.scrollIntoView({ block: "end" });
1216
- }, [log.length, live]);
1217
- async function send() {
1218
- const text = draft.trim();
1219
- if (!text || busy) return;
1220
- setDraft("");
1221
- setBusy(true);
1222
- let api = [...chat.api || [], { role: "user", content: text }];
1223
- let logNow = [...log, { role: "user", content: text }];
1224
- const commit = () => ctx.update((d) => {
1225
- d.chat = { api, log: logNow };
1226
- });
1227
- commit();
1228
- try {
1229
- const system = (typeof c.system === "function" ? c.system(ctx.data) : c.system || "") + (c.tools ? toolProtocol(c.tools) : "");
1230
- for (let round = 0; round < MAX_ROUNDS; round++) {
1231
- const raw = await streamCall({
1232
- model: c.model || DEFAULT_MODEL,
1233
- maxTokens: c.maxTokens || 2e3,
1234
- system,
1235
- messages: api,
1236
- onText: (t) => setLive(stripToolCallBlock(t))
1237
- });
1238
- api = [...api, { role: "assistant", content: raw }];
1239
- const shown = stripToolCallBlock(raw);
1240
- const calls = c.tools ? parseToolCalls(raw) : null;
1241
- if (!calls) {
1242
- logNow = [...logNow, { role: "assistant", content: shown || raw }];
1243
- commit();
1244
- break;
1245
- }
1246
- const results = runTools(calls, c.tools, ctx);
1247
- if (shown) logNow = [...logNow, { role: "assistant", content: shown }];
1248
- logNow = [...logNow, { role: "tools", tools: calls.map((x) => x.name) }];
1249
- api = [
1250
- ...api,
1251
- {
1252
- role: "user",
1253
- content: `[TOOL_RESULT]
1254
- ${JSON.stringify(results)}
1255
- [/TOOL_RESULT]
1256
-
1257
- Continue with this information. If you have everything you need, answer in natural language without further tool_call.`
1258
- }
1259
- ];
1260
- commit();
1261
- setLive(null);
1262
- }
1263
- } catch (e) {
1264
- logNow = [...logNow, { role: "error", content: String(e && e.message || e) }];
1265
- commit();
1266
- } finally {
1267
- setLive(null);
1268
- setBusy(false);
1269
- }
1343
+ const unknown = Object.keys(c).filter((k) => !CHAT_KEYS.includes(k));
1344
+ if (unknown.length) {
1345
+ throw new Error(
1346
+ `claude-artifact-framework: ${where} has unknown keys (${unknown.join(", ")}). Valid keys: ${CHAT_KEYS.join(", ")}.`
1347
+ );
1270
1348
  }
1271
- return createElement(
1272
- "div",
1273
- { className: "caf-panel caf-panel-single" },
1274
- createElement(
1275
- "div",
1276
- { className: "caf-block caf-chat-log" },
1277
- c.greeting ? createElement("div", { className: "caf-msg caf-msg-assistant", dangerouslySetInnerHTML: { __html: renderMarkdown(c.greeting) } }) : null,
1278
- log.map((m, i) => createElement(Bubble, { key: i, m })),
1279
- live !== null ? createElement("div", { className: "caf-msg caf-msg-assistant caf-msg-live", dangerouslySetInnerHTML: { __html: renderMarkdown(live || "\u2026") } }) : null,
1280
- busy && live === null ? createElement("div", { className: "caf-msg-tools" }, "thinking\u2026") : null,
1281
- createElement("div", { ref: endRef })
1282
- ),
1283
- // Deliberately NOT a <form>: the artifact iframe's sandbox lacks
1284
- // allow-forms, so native form submission is blocked there — and on mobile
1285
- // the keyboard's send key triggers exactly that implicit submission. A
1286
- // plain div with onClick + Enter via onKeyDown never touches the native
1287
- // form machinery, so it can't be sandboxed away.
1288
- createElement(
1289
- "div",
1290
- { className: "caf-chat-input" },
1291
- createElement("input", {
1292
- value: draft,
1293
- placeholder: c.placeholder || "Message\u2026",
1294
- disabled: busy,
1295
- enterKeyHint: "send",
1296
- onChange: (e) => setDraft(e.target.value),
1297
- onKeyDown: (e) => {
1298
- if (e.key === "Enter") {
1299
- e.preventDefault();
1300
- send();
1301
- }
1302
- },
1303
- "aria-label": "Message"
1304
- }),
1305
- createElement(
1306
- "button",
1307
- { type: "button", className: "caf-btn caf-btn-primary", disabled: busy || !draft.trim(), onClick: send },
1308
- "Send"
1309
- )
1310
- )
1311
- );
1312
- }
1313
-
1314
- // src/app.js
1315
- var LAYOUTS = ["panel", "records", "steps", "chat"];
1316
- var RESERVED = ["title", "empty", "wide", "onSelect", "subtitle"];
1317
- var TOP_KEYS = ["title", "subtitle", "theme", "layout", "blocks", "records", "steps", "chat", "data", "shared"];
1318
- function checkFields(fields, where) {
1319
- for (const f of fields || []) {
1320
- if (!f || !f.key) throw new Error(`claude-artifact-framework: every field in ${where} needs a \`key\`.`);
1321
- if (f.type !== void 0 && !FIELD_TYPES.includes(f.type)) {
1322
- throw new Error(
1323
- `claude-artifact-framework: field "${f.key}" in ${where} has unknown type "${f.type}". Valid types: ${FIELD_TYPES.join(", ")}.`
1324
- );
1325
- }
1326
- if (f.type === "select" && f.options !== void 0 && !Array.isArray(f.options) && typeof f.options !== "function") {
1349
+ for (const [name, tool] of Object.entries(c.tools || {})) {
1350
+ if (!tool || typeof tool.description !== "string" || typeof tool.run !== "function") {
1327
1351
  throw new Error(
1328
- `claude-artifact-framework: field "${f.key}" in ${where}: \`options\` must be an array or a function of data returning one.`
1352
+ `claude-artifact-framework: chat tool "${name}" needs a \`description\` string and a \`run(input, ctx)\` function.`
1329
1353
  );
1330
1354
  }
1331
1355
  }
@@ -1356,7 +1380,7 @@ function checkBlocks(blocks) {
1356
1380
  throw new Error(`claude-artifact-framework: blocks[${i}] must be an object like { fields: [...] }.`);
1357
1381
  }
1358
1382
  const keys = Object.keys(block).filter((k) => !RESERVED.includes(k));
1359
- const known = keys.filter((k) => BLOCK_NAMES.includes(k));
1383
+ const known = keys.filter((k) => BLOCK_NAMES.includes(k) || k === "tabs");
1360
1384
  if (known.length === 0) {
1361
1385
  throw new Error(
1362
1386
  `claude-artifact-framework: blocks[${i}] has no recognised block type (found: ${keys.join(", ") || "nothing"}). Valid block types: ${BLOCK_NAMES.join(", ")}.`
@@ -1371,6 +1395,25 @@ function checkBlocks(blocks) {
1371
1395
  }
1372
1396
  checkFields(block.fields, `blocks[${i}]`);
1373
1397
  if (known[0] === "actions") checkActions(block.actions, `blocks[${i}].actions`);
1398
+ if (known[0] === "chat") checkChatConfig(block.chat, `blocks[${i}].chat`);
1399
+ if (known[0] === "tabs") {
1400
+ const tabs = block.tabs;
1401
+ if (!Array.isArray(tabs) || tabs.length === 0) {
1402
+ throw new Error(`claude-artifact-framework: blocks[${i}].tabs needs an array of { label, blocks: [...] }.`);
1403
+ }
1404
+ tabs.forEach((t, j) => {
1405
+ if (!t || typeof t.label !== "string" || !Array.isArray(t.blocks) || t.blocks.length === 0) {
1406
+ throw new Error(`claude-artifact-framework: tabs[${j}] needs a \`label\` string and a non-empty \`blocks\` array.`);
1407
+ }
1408
+ if (t.blocks.some((b) => b && b.tabs)) {
1409
+ throw new Error("claude-artifact-framework: tabs cannot nest inside tabs.");
1410
+ }
1411
+ checkBlocks(t.blocks);
1412
+ });
1413
+ }
1414
+ if (block.when !== void 0 && typeof block.when !== "function") {
1415
+ throw new Error(`claude-artifact-framework: blocks[${i}].when must be a function of data returning true/false.`);
1416
+ }
1374
1417
  });
1375
1418
  }
1376
1419
  function validate(spec) {
@@ -1419,25 +1462,28 @@ function validate(spec) {
1419
1462
  return layout;
1420
1463
  }
1421
1464
  if (layout === "chat") {
1422
- const c = spec.chat;
1423
- if (!c || typeof c.system !== "string" && typeof c.system !== "function") {
1465
+ checkChatConfig(spec.chat, "chat");
1466
+ return layout;
1467
+ }
1468
+ if (layout === "workspace") {
1469
+ if (!Array.isArray(spec.main) || spec.main.length === 0) {
1424
1470
  throw new Error(
1425
- 'claude-artifact-framework: layout "chat" needs `chat: { system: "..." }` (a string or a function of data).'
1471
+ 'claude-artifact-framework: layout "workspace" needs `main: [...]` (an array of blocks). `sidebar: [...]` is optional.'
1426
1472
  );
1427
1473
  }
1428
- const unknown = Object.keys(c).filter((k) => !CHAT_KEYS.includes(k));
1429
- if (unknown.length) {
1474
+ checkBlocks(spec.main);
1475
+ if (spec.sidebar !== void 0) {
1476
+ if (!Array.isArray(spec.sidebar)) {
1477
+ throw new Error("claude-artifact-framework: workspace `sidebar` must be an array of blocks.");
1478
+ }
1479
+ checkBlocks(spec.sidebar);
1480
+ }
1481
+ const chats = flattenBlocks([...spec.main, ...spec.sidebar || []]).filter((b) => b && b.chat);
1482
+ if (chats.length > 1) {
1430
1483
  throw new Error(
1431
- `claude-artifact-framework: chat has unknown keys (${unknown.join(", ")}). Valid keys: ${CHAT_KEYS.join(", ")}.`
1484
+ "claude-artifact-framework: only one chat block per app \u2014 the conversation persists under data.chat and there is exactly one."
1432
1485
  );
1433
1486
  }
1434
- for (const [name, tool] of Object.entries(c.tools || {})) {
1435
- if (!tool || typeof tool.description !== "string" || typeof tool.run !== "function") {
1436
- throw new Error(
1437
- `claude-artifact-framework: chat tool "${name}" needs a \`description\` string and a \`run(input, ctx)\` function.`
1438
- );
1439
- }
1440
- }
1441
1487
  return layout;
1442
1488
  }
1443
1489
  if (layout === "records") {
@@ -1499,15 +1545,25 @@ function validate(spec) {
1499
1545
  checkBlocks(spec.blocks || []);
1500
1546
  return layout;
1501
1547
  }
1548
+ function flattenBlocks(blocks) {
1549
+ const out = [];
1550
+ for (const b of blocks || []) {
1551
+ out.push(b);
1552
+ if (b && Array.isArray(b.tabs)) for (const t of b.tabs) out.push(...flattenBlocks(t.blocks));
1553
+ }
1554
+ return out;
1555
+ }
1502
1556
  function defaultsFrom(spec) {
1503
1557
  const data = { ...spec.data || {} };
1504
1558
  if ((spec.layout || "panel") === "records" && !Array.isArray(data.records)) {
1505
1559
  data.records = [];
1506
1560
  }
1507
- if ((spec.layout || "panel") === "chat" && !data.chat) {
1561
+ const allBlocks = flattenBlocks([...spec.blocks || [], ...spec.main || [], ...spec.sidebar || []]);
1562
+ const hasChat = (spec.layout || "panel") === "chat" || allBlocks.some((b) => b && b.chat);
1563
+ if (hasChat && !data.chat) {
1508
1564
  data.chat = { api: [], log: [] };
1509
1565
  }
1510
- for (const block of [...spec.blocks || [], ...spec.steps || []]) {
1566
+ for (const block of [...allBlocks, ...spec.steps || []]) {
1511
1567
  for (const field of block.fields || []) {
1512
1568
  if (field && field.key !== void 0 && !(field.key in data)) {
1513
1569
  data[field.key] = field.value !== void 0 ? field.value : field.type === "check" ? false : "";
@@ -1542,10 +1598,69 @@ function boundary(React2) {
1542
1598
  }
1543
1599
  return BlockBoundary;
1544
1600
  }
1601
+ function TabsBlock({ spec, ctx }) {
1602
+ const tabs = spec.tabs;
1603
+ const key = tabs.map((t) => t.label).join("|");
1604
+ const stored = ctx.view.tabs ? ctx.view.tabs[key] : void 0;
1605
+ const active = Math.min(stored === void 0 ? 0 : stored, tabs.length - 1);
1606
+ return createElement(
1607
+ "div",
1608
+ { className: "caf-tabs" },
1609
+ createElement(
1610
+ "div",
1611
+ { className: "caf-tabbar", role: "tablist" },
1612
+ tabs.map(
1613
+ (t, i) => createElement(
1614
+ "button",
1615
+ {
1616
+ key: t.label,
1617
+ type: "button",
1618
+ role: "tab",
1619
+ "aria-selected": i === active,
1620
+ className: i === active ? "caf-tab caf-tab-active" : "caf-tab",
1621
+ onClick: () => ctx.setTab(key, i)
1622
+ },
1623
+ t.label
1624
+ )
1625
+ )
1626
+ ),
1627
+ createElement(
1628
+ "div",
1629
+ { className: "caf-tabs-body" },
1630
+ tabs[active].blocks.map((b, i) => createElement(Block, { key: active + ":" + i, block: b, ctx }))
1631
+ )
1632
+ );
1633
+ }
1634
+ var ALL_BLOCKS = { ...BLOCKS, tabs: TabsBlock };
1635
+ var ALL_NAMES = [...BLOCK_NAMES, "tabs"];
1545
1636
  function Block({ block, ctx }) {
1546
- const name = Object.keys(block).find((k) => BLOCK_NAMES.includes(k));
1547
- const Component2 = BLOCKS[name];
1548
- return createElement(boundary(ctx.React), null, createElement(Component2, { spec: block, ctx }));
1637
+ if (typeof block.when === "function" && !block.when(ctx.data)) return null;
1638
+ const name = Object.keys(block).find((k) => ALL_NAMES.includes(k));
1639
+ const Component2 = ALL_BLOCKS[name];
1640
+ const inner = createElement(
1641
+ boundary(ctx.React),
1642
+ null,
1643
+ createElement(Component2, { spec: block.collapsible ? { ...block, title: void 0 } : block, ctx })
1644
+ );
1645
+ if (!block.collapsible) return inner;
1646
+ const key = typeof block.title === "string" && block.title ? block.title : name;
1647
+ const collapsed = ctx.view.collapsed && key in ctx.view.collapsed ? ctx.view.collapsed[key] : block.collapsible === "collapsed";
1648
+ return createElement(
1649
+ "div",
1650
+ { className: "caf-collapse" },
1651
+ createElement(
1652
+ "button",
1653
+ {
1654
+ type: "button",
1655
+ className: "caf-collapse-head",
1656
+ "aria-expanded": !collapsed,
1657
+ onClick: () => ctx.toggleCollapsed(key, !collapsed)
1658
+ },
1659
+ createElement("span", null, block.title || name),
1660
+ createElement("span", { className: collapsed ? "caf-chevron caf-chevron-closed" : "caf-chevron" }, "\u25BE")
1661
+ ),
1662
+ collapsed ? null : createElement("div", { className: "caf-collapse-body" }, inner)
1663
+ );
1549
1664
  }
1550
1665
  function Panel({ spec, ctx }) {
1551
1666
  return createElement(
@@ -1579,7 +1694,16 @@ function RecordsWithBlocks({ spec, ctx }) {
1579
1694
  ) : null
1580
1695
  );
1581
1696
  }
1582
- var LAYOUT_COMPONENTS = { panel: Panel, records: RecordsWithBlocks, steps: StepsLayout, chat: ChatLayout };
1697
+ function WorkspaceLayout({ spec, ctx }) {
1698
+ const render = (blocks) => (blocks || []).map((block, i) => createElement("section", { key: i, className: "caf-ws-slot" }, createElement(Block, { block, ctx })));
1699
+ return createElement(
1700
+ "div",
1701
+ { className: "caf-workspace" },
1702
+ createElement("div", { className: "caf-ws-main" }, render(spec.main)),
1703
+ spec.sidebar && spec.sidebar.length ? createElement("div", { className: "caf-ws-side" }, render(spec.sidebar)) : null
1704
+ );
1705
+ }
1706
+ var LAYOUT_COMPONENTS = { panel: Panel, records: RecordsWithBlocks, steps: StepsLayout, chat: ChatLayout, workspace: WorkspaceLayout };
1583
1707
  function createApp(spec = {}) {
1584
1708
  const layout = validate(spec);
1585
1709
  const state = createAppState(defaultsFrom(spec), { shared: Boolean(spec.shared) });
@@ -1604,6 +1728,8 @@ function createApp(spec = {}) {
1604
1728
  go: state.go,
1605
1729
  back: state.back,
1606
1730
  setStep: (n) => state.setView({ step: n }),
1731
+ setTab: (key, i) => state.setView({ tabs: { ...state.getView().tabs, [key]: i } }),
1732
+ toggleCollapsed: (key, val) => state.setView({ collapsed: { ...state.getView().collapsed, [key]: val } }),
1607
1733
  colors: (n) => seriesColors(spec.theme && spec.theme.seed, n)
1608
1734
  };
1609
1735
  const Layout = LAYOUT_COMPONENTS[layout];
@@ -1759,6 +1885,37 @@ var BASE_CSS = `
1759
1885
  .caf-md-h { display: block; margin-top: 0.3rem; }
1760
1886
  .caf-md-li { display: block; padding-left: 1rem; position: relative; }
1761
1887
  .caf-md-li::before { content: "\u2022"; position: absolute; left: 0.2rem; opacity: 0.6; }
1888
+ .caf-workspace { max-width: 1200px; margin: 0 auto; display: flex; gap: 1rem; align-items: flex-start; }
1889
+ .caf-ws-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1rem; }
1890
+ .caf-ws-side { width: 340px; flex: none; display: flex; flex-direction: column; gap: 1rem; position: sticky; top: 1rem; }
1891
+ .caf-ws-slot { min-width: 0; }
1892
+ .caf-chat-panel { display: flex; flex-direction: column; min-height: 0; }
1893
+ .caf-ws-side .caf-chat-log { max-height: calc(100vh - 220px); }
1894
+ @media (max-width: 900px) {
1895
+ .caf-workspace { flex-direction: column; align-items: stretch; }
1896
+ .caf-ws-side { width: 100%; position: static; }
1897
+ }
1898
+ .caf-tabs { display: flex; flex-direction: column; gap: 0.8rem; min-width: 0; }
1899
+ .caf-tabbar { display: flex; gap: 0.2rem; border-bottom: 1px solid var(--caf-border); overflow-x: auto; }
1900
+ .caf-tab {
1901
+ font: inherit; font-size: 0.85rem; font-weight: 600; cursor: pointer;
1902
+ padding: 0.45rem 0.8rem; border: none; background: none; color: var(--caf-muted);
1903
+ border-bottom: 2px solid transparent; margin-bottom: -1px; white-space: nowrap;
1904
+ }
1905
+ .caf-tab:hover { color: var(--caf-text); }
1906
+ .caf-tab:focus-visible { outline: 2px solid var(--caf-accent); outline-offset: -2px; }
1907
+ .caf-tab-active { color: var(--caf-accent); border-bottom-color: var(--caf-accent); }
1908
+ .caf-tabs-body { display: flex; flex-direction: column; gap: 0.8rem; }
1909
+ .caf-collapse { display: flex; flex-direction: column; gap: 0.5rem; min-width: 0; }
1910
+ .caf-collapse-head {
1911
+ font: inherit; font-size: 0.95rem; font-weight: 650; cursor: pointer;
1912
+ display: flex; align-items: center; justify-content: space-between; gap: 0.6rem;
1913
+ padding: 0.7rem 1rem; border: 1px solid var(--caf-border); border-radius: var(--caf-radius);
1914
+ background: var(--caf-surface); color: var(--caf-text); width: 100%; text-align: left;
1915
+ }
1916
+ .caf-collapse-head:focus-visible { outline: 2px solid var(--caf-accent); outline-offset: 1px; }
1917
+ .caf-chevron { transition: transform 0.15s ease; color: var(--caf-muted); }
1918
+ .caf-chevron-closed { transform: rotate(-90deg); }
1762
1919
  .caf-empty, .caf-loading { color: var(--caf-muted); font-size: 0.9rem; text-align: center; padding: 1.6rem 1rem; }
1763
1920
  .caf-block-error { border-color: var(--caf-danger); gap: 0.4rem; }
1764
1921
  .caf-block-error strong { font-size: 0.9rem; color: var(--caf-danger); }