claude-artifact-framework 0.6.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/dist/index.esm.js CHANGED
@@ -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) {
@@ -403,7 +638,7 @@ function validateField(field, value) {
403
638
  }
404
639
  return null;
405
640
  }
406
- function Field({ field, value, onChange }) {
641
+ function Field({ field, value, onChange, data }) {
407
642
  const error = validateField(field, value);
408
643
  const id = `caf-f-${field.key}`;
409
644
  const numeric = field.type === "number" || field.type === "money" || field.type === "percent";
@@ -430,11 +665,12 @@ function Field({ field, value, onChange }) {
430
665
  if (field.type === "textarea") {
431
666
  control = createElement("textarea", { ...common, rows: field.rows || 3 });
432
667
  } else if (field.type === "select") {
668
+ const options = typeof field.options === "function" ? field.options(data) || [] : field.options || [];
433
669
  control = createElement(
434
670
  "select",
435
671
  common,
436
672
  createElement("option", { value: "" }, field.placeholder || "Select\u2026"),
437
- (field.options || []).map((opt) => {
673
+ options.map((opt) => {
438
674
  const val = typeof opt === "string" ? opt : opt.value;
439
675
  const label = typeof opt === "string" ? opt : opt.label;
440
676
  return createElement("option", { key: val, value: val }, label);
@@ -474,6 +710,7 @@ function FieldsBlock({ spec, ctx }) {
474
710
  (field) => createElement(Field, {
475
711
  key: field.key,
476
712
  field,
713
+ data: ctx.data,
477
714
  value: ctx.data[field.key],
478
715
  onChange: (v) => ctx.update((d) => {
479
716
  d[field.key] = v;
@@ -517,13 +754,15 @@ function TextBlock({ spec, ctx }) {
517
754
  }
518
755
  function ListBlock({ spec, ctx }) {
519
756
  const rows = (typeof spec.list === "function" ? spec.list(ctx.data) : spec.list) || [];
757
+ const heading = typeof spec.title === "string" ? spec.title : null;
758
+ const rowTitle2 = typeof spec.title === "function" ? spec.title : (r) => r.title ?? r.name ?? r.label ?? r.text ?? String(r);
520
759
  if (!rows.length) {
521
760
  return createElement("div", { className: "caf-block caf-empty" }, spec.empty || "Nothing here yet.");
522
761
  }
523
- const title = spec.title || ((r) => r.title ?? r.name ?? r.label ?? String(r));
524
762
  return createElement(
525
763
  "div",
526
764
  { className: "caf-block caf-list" },
765
+ heading ? createElement("h2", { className: "caf-block-title" }, heading) : null,
527
766
  rows.map(
528
767
  (row, i) => createElement(
529
768
  "button",
@@ -533,7 +772,7 @@ function ListBlock({ spec, ctx }) {
533
772
  type: "button",
534
773
  onClick: spec.onSelect ? () => spec.onSelect(row, ctx) : void 0
535
774
  },
536
- createElement("span", { className: "caf-row-title" }, title(row)),
775
+ createElement("span", { className: "caf-row-title" }, rowTitle2(row)),
537
776
  spec.subtitle ? createElement("span", { className: "caf-row-sub" }, spec.subtitle(row)) : null
538
777
  )
539
778
  )
@@ -734,6 +973,7 @@ var BLOCKS = {
734
973
  chart: ChartBlock,
735
974
  timer: TimerBlock,
736
975
  actions: ActionsBlock,
976
+ chat: ({ spec, ctx }) => createElement(ChatPanel, { cfg: spec.chat, ctx }),
737
977
  custom: CustomBlock
738
978
  };
739
979
  var BLOCK_NAMES = Object.keys(BLOCKS);
@@ -890,6 +1130,7 @@ function DetailScreen({ spec, ctx, record }) {
890
1130
  (field) => createElement(Field, {
891
1131
  key: field.key,
892
1132
  field,
1133
+ data: ctx.data,
893
1134
  value: record[field.key],
894
1135
  onChange: (v) => ctx.update((d) => {
895
1136
  const rec = d.records.find((r) => r.id === record.id);
@@ -950,6 +1191,7 @@ function ItemsEditor({ spec, ctx, record }) {
950
1191
  (field) => createElement(Field, {
951
1192
  key: field.key,
952
1193
  field,
1194
+ data: ctx.data,
953
1195
  value: item[field.key],
954
1196
  onChange: (v) => mutate((rec) => {
955
1197
  const it = rec.items.find((x) => x.id === item.id);
@@ -1047,6 +1289,7 @@ function StepsLayout({ spec, ctx }) {
1047
1289
  (field) => createElement(Field, {
1048
1290
  key: field.key,
1049
1291
  field,
1292
+ data: ctx.data,
1050
1293
  value: ctx.data[field.key],
1051
1294
  onChange: (v) => ctx.update((d) => {
1052
1295
  d[field.key] = v;
@@ -1072,242 +1315,10 @@ function StepsLayout({ spec, ctx }) {
1072
1315
  );
1073
1316
  }
1074
1317
 
1075
- // src/chat.js
1076
- var CHAT_KEYS = ["system", "greeting", "placeholder", "tools", "model", "maxTokens"];
1077
- var DEFAULT_MODEL = "claude-sonnet-4-6";
1078
- var MAX_ROUNDS = 8;
1079
- async function streamCall({ model, maxTokens, system, messages, onText }) {
1080
- const res = await fetch("https://api.anthropic.com/v1/messages", {
1081
- method: "POST",
1082
- headers: { "Content-Type": "application/json" },
1083
- body: JSON.stringify({ model, max_tokens: maxTokens, system, messages, stream: true })
1084
- });
1085
- if (!res.ok || !res.body) {
1086
- let detail = "";
1087
- try {
1088
- const b = await res.json();
1089
- detail = b && b.error && b.error.message || JSON.stringify(b);
1090
- } catch {
1091
- detail = await res.text().catch(() => "");
1092
- }
1093
- throw new Error(`HTTP ${res.status}${detail ? ": " + detail : ""}`);
1094
- }
1095
- const reader = res.body.getReader();
1096
- const decoder = new TextDecoder();
1097
- let full = "";
1098
- let buf = "";
1099
- for (; ; ) {
1100
- const { done, value } = await reader.read();
1101
- if (done) break;
1102
- buf += decoder.decode(value, { stream: true });
1103
- const lines = buf.split("\n");
1104
- buf = lines.pop();
1105
- for (const line of lines) {
1106
- if (!line.startsWith("data: ")) continue;
1107
- try {
1108
- const j = JSON.parse(line.slice(6));
1109
- if (j.type === "content_block_delta" && j.delta && j.delta.text) {
1110
- full += j.delta.text;
1111
- if (onText) onText(full);
1112
- }
1113
- } catch {
1114
- }
1115
- }
1116
- }
1117
- return full;
1118
- }
1119
- function parseToolCalls(text) {
1120
- const match = text.match(/```tool_call\s*\n?([\s\S]*?)```/);
1121
- if (!match) return null;
1122
- try {
1123
- const calls = JSON.parse(match[1]);
1124
- return Array.isArray(calls) && calls.length ? calls : null;
1125
- } catch {
1126
- return null;
1127
- }
1128
- }
1129
- function stripToolCallBlock(text) {
1130
- return text.replace(/```tool_call\s*\n?[\s\S]*?```/, "").trim();
1131
- }
1132
- function toolProtocol(tools) {
1133
- const list = Object.entries(tools).map(([name, t]) => `- ${name}: ${t.description}
1134
- Input: ${JSON.stringify(t.input || {})}`).join("\n");
1135
- return `
1136
-
1137
- You have tools available:
1138
- ${list}
1139
-
1140
- 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:
1141
-
1142
- \`\`\`tool_call
1143
- [{ "name": "tool_name", "input": { "param": "value" } }]
1144
- \`\`\`
1145
-
1146
- 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.`;
1147
- }
1148
- function runTools(calls, tools, ctx) {
1149
- return calls.map((call) => {
1150
- const tool = tools[call.name];
1151
- if (!tool) {
1152
- return { name: call.name, error: `unknown tool "${call.name}". Valid tools: ${Object.keys(tools).join(", ")}` };
1153
- }
1154
- try {
1155
- const result = tool.run(call.input || {}, ctx);
1156
- return { name: call.name, result: result === void 0 ? { ok: true } : result };
1157
- } catch (e) {
1158
- return { name: call.name, error: String(e && e.message || e) };
1159
- }
1160
- });
1161
- }
1162
- var escHtml = (t) => String(t).replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
1163
- function renderMarkdown(text) {
1164
- const codeBlocks = [];
1165
- let t = String(text).replace(/```[a-z]*\n?([\s\S]*?)```/g, (m, code) => {
1166
- codeBlocks.push(code.replace(/\n$/, ""));
1167
- return "\0" + (codeBlocks.length - 1) + "\0";
1168
- });
1169
- t = escHtml(t);
1170
- t = t.replace(/`([^`]+)`/g, "<code>$1</code>");
1171
- t = t.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
1172
- t = t.replace(/(^|[^*])\*([^*\n]+)\*/g, "$1<em>$2</em>");
1173
- t = t.replace(/^#{1,3} (.+)$/gm, '<strong class="caf-md-h">$1</strong>');
1174
- t = t.replace(/^[-•*] (.+)$/gm, '<span class="caf-md-li">$1</span>');
1175
- t = t.replace(/^\d+\. (.+)$/gm, '<span class="caf-md-li caf-md-num">$1</span>');
1176
- t = t.replace(/\n/g, "<br>");
1177
- t = t.replace(/\u0000(\d+)\u0000/g, (m, i) => '<pre class="caf-code">' + escHtml(codeBlocks[Number(i)]) + "</pre>");
1178
- return t;
1179
- }
1180
- function Bubble({ m }) {
1181
- if (m.role === "tools") {
1182
- return createElement("div", { className: "caf-msg-tools" }, `used: ${m.tools.join(", ")}`);
1183
- }
1184
- if (m.role === "error") {
1185
- return createElement(
1186
- "div",
1187
- { className: "caf-msg caf-msg-error" },
1188
- createElement("strong", null, "The call failed"),
1189
- createElement("span", null, m.content),
1190
- createElement("span", { className: "caf-hint" }, "Your message is kept \u2014 send again to retry.")
1191
- );
1192
- }
1193
- if (m.role === "user") return createElement("div", { className: "caf-msg caf-msg-user" }, m.content);
1194
- return createElement("div", {
1195
- className: "caf-msg caf-msg-assistant",
1196
- dangerouslySetInnerHTML: { __html: renderMarkdown(m.content) }
1197
- });
1198
- }
1199
- function ChatLayout({ spec, ctx }) {
1200
- const c = spec.chat;
1201
- const [draft, setDraft] = useState("");
1202
- const [busy, setBusy] = useState(false);
1203
- const [live, setLive] = useState(null);
1204
- const endRef = useRef(null);
1205
- const chat = ctx.data.chat || { api: [], log: [] };
1206
- const log = chat.log || [];
1207
- useEffect(() => {
1208
- if (endRef.current) endRef.current.scrollIntoView({ block: "end" });
1209
- }, [log.length, live]);
1210
- async function send() {
1211
- const text = draft.trim();
1212
- if (!text || busy) return;
1213
- setDraft("");
1214
- setBusy(true);
1215
- let api = [...chat.api || [], { role: "user", content: text }];
1216
- let logNow = [...log, { role: "user", content: text }];
1217
- const commit = () => ctx.update((d) => {
1218
- d.chat = { api, log: logNow };
1219
- });
1220
- commit();
1221
- try {
1222
- const system = (typeof c.system === "function" ? c.system(ctx.data) : c.system || "") + (c.tools ? toolProtocol(c.tools) : "");
1223
- for (let round = 0; round < MAX_ROUNDS; round++) {
1224
- const raw = await streamCall({
1225
- model: c.model || DEFAULT_MODEL,
1226
- maxTokens: c.maxTokens || 2e3,
1227
- system,
1228
- messages: api,
1229
- onText: (t) => setLive(stripToolCallBlock(t))
1230
- });
1231
- api = [...api, { role: "assistant", content: raw }];
1232
- const shown = stripToolCallBlock(raw);
1233
- const calls = c.tools ? parseToolCalls(raw) : null;
1234
- if (!calls) {
1235
- logNow = [...logNow, { role: "assistant", content: shown || raw }];
1236
- commit();
1237
- break;
1238
- }
1239
- const results = runTools(calls, c.tools, ctx);
1240
- if (shown) logNow = [...logNow, { role: "assistant", content: shown }];
1241
- logNow = [...logNow, { role: "tools", tools: calls.map((x) => x.name) }];
1242
- api = [
1243
- ...api,
1244
- {
1245
- role: "user",
1246
- content: `[TOOL_RESULT]
1247
- ${JSON.stringify(results)}
1248
- [/TOOL_RESULT]
1249
-
1250
- Continue with this information. If you have everything you need, answer in natural language without further tool_call.`
1251
- }
1252
- ];
1253
- commit();
1254
- setLive(null);
1255
- }
1256
- } catch (e) {
1257
- logNow = [...logNow, { role: "error", content: String(e && e.message || e) }];
1258
- commit();
1259
- } finally {
1260
- setLive(null);
1261
- setBusy(false);
1262
- }
1263
- }
1264
- return createElement(
1265
- "div",
1266
- { className: "caf-panel caf-panel-single" },
1267
- createElement(
1268
- "div",
1269
- { className: "caf-block caf-chat-log" },
1270
- c.greeting ? createElement("div", { className: "caf-msg caf-msg-assistant", dangerouslySetInnerHTML: { __html: renderMarkdown(c.greeting) } }) : null,
1271
- log.map((m, i) => createElement(Bubble, { key: i, m })),
1272
- live !== null ? createElement("div", { className: "caf-msg caf-msg-assistant caf-msg-live", dangerouslySetInnerHTML: { __html: renderMarkdown(live || "\u2026") } }) : null,
1273
- busy && live === null ? createElement("div", { className: "caf-msg-tools" }, "thinking\u2026") : null,
1274
- createElement("div", { ref: endRef })
1275
- ),
1276
- // Deliberately NOT a <form>: the artifact iframe's sandbox lacks
1277
- // allow-forms, so native form submission is blocked there — and on mobile
1278
- // the keyboard's send key triggers exactly that implicit submission. A
1279
- // plain div with onClick + Enter via onKeyDown never touches the native
1280
- // form machinery, so it can't be sandboxed away.
1281
- createElement(
1282
- "div",
1283
- { className: "caf-chat-input" },
1284
- createElement("input", {
1285
- value: draft,
1286
- placeholder: c.placeholder || "Message\u2026",
1287
- disabled: busy,
1288
- enterKeyHint: "send",
1289
- onChange: (e) => setDraft(e.target.value),
1290
- onKeyDown: (e) => {
1291
- if (e.key === "Enter") {
1292
- e.preventDefault();
1293
- send();
1294
- }
1295
- },
1296
- "aria-label": "Message"
1297
- }),
1298
- createElement(
1299
- "button",
1300
- { type: "button", className: "caf-btn caf-btn-primary", disabled: busy || !draft.trim(), onClick: send },
1301
- "Send"
1302
- )
1303
- )
1304
- );
1305
- }
1306
-
1307
1318
  // src/app.js
1308
- var LAYOUTS = ["panel", "records", "steps", "chat"];
1319
+ var LAYOUTS = ["panel", "records", "steps", "chat", "workspace"];
1309
1320
  var RESERVED = ["title", "empty", "wide", "onSelect", "subtitle"];
1310
- var TOP_KEYS = ["title", "subtitle", "theme", "layout", "blocks", "records", "steps", "chat", "data", "shared"];
1321
+ var TOP_KEYS = ["title", "subtitle", "theme", "layout", "blocks", "records", "steps", "chat", "data", "shared", "main", "sidebar"];
1311
1322
  function checkFields(fields, where) {
1312
1323
  for (const f of fields || []) {
1313
1324
  if (!f || !f.key) throw new Error(`claude-artifact-framework: every field in ${where} needs a \`key\`.`);
@@ -1316,6 +1327,31 @@ function checkFields(fields, where) {
1316
1327
  `claude-artifact-framework: field "${f.key}" in ${where} has unknown type "${f.type}". Valid types: ${FIELD_TYPES.join(", ")}.`
1317
1328
  );
1318
1329
  }
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
+ );
1334
+ }
1335
+ }
1336
+ }
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).`
1341
+ );
1342
+ }
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
+ );
1348
+ }
1349
+ for (const [name, tool] of Object.entries(c.tools || {})) {
1350
+ if (!tool || typeof tool.description !== "string" || typeof tool.run !== "function") {
1351
+ throw new Error(
1352
+ `claude-artifact-framework: chat tool "${name}" needs a \`description\` string and a \`run(input, ctx)\` function.`
1353
+ );
1354
+ }
1319
1355
  }
1320
1356
  }
1321
1357
  var ACTION_KEYS = ["label", "run", "when", "kind"];
@@ -1359,6 +1395,7 @@ function checkBlocks(blocks) {
1359
1395
  }
1360
1396
  checkFields(block.fields, `blocks[${i}]`);
1361
1397
  if (known[0] === "actions") checkActions(block.actions, `blocks[${i}].actions`);
1398
+ if (known[0] === "chat") checkChatConfig(block.chat, `blocks[${i}].chat`);
1362
1399
  });
1363
1400
  }
1364
1401
  function validate(spec) {
@@ -1407,25 +1444,28 @@ function validate(spec) {
1407
1444
  return layout;
1408
1445
  }
1409
1446
  if (layout === "chat") {
1410
- const c = spec.chat;
1411
- if (!c || typeof c.system !== "string" && typeof c.system !== "function") {
1447
+ checkChatConfig(spec.chat, "chat");
1448
+ return layout;
1449
+ }
1450
+ if (layout === "workspace") {
1451
+ if (!Array.isArray(spec.main) || spec.main.length === 0) {
1412
1452
  throw new Error(
1413
- 'claude-artifact-framework: layout "chat" needs `chat: { system: "..." }` (a string or a function of data).'
1453
+ 'claude-artifact-framework: layout "workspace" needs `main: [...]` (an array of blocks). `sidebar: [...]` is optional.'
1414
1454
  );
1415
1455
  }
1416
- const unknown = Object.keys(c).filter((k) => !CHAT_KEYS.includes(k));
1417
- if (unknown.length) {
1456
+ checkBlocks(spec.main);
1457
+ if (spec.sidebar !== void 0) {
1458
+ if (!Array.isArray(spec.sidebar)) {
1459
+ throw new Error("claude-artifact-framework: workspace `sidebar` must be an array of blocks.");
1460
+ }
1461
+ checkBlocks(spec.sidebar);
1462
+ }
1463
+ const chats = [...spec.main, ...spec.sidebar || []].filter((b) => b && b.chat);
1464
+ if (chats.length > 1) {
1418
1465
  throw new Error(
1419
- `claude-artifact-framework: chat has unknown keys (${unknown.join(", ")}). Valid keys: ${CHAT_KEYS.join(", ")}.`
1466
+ "claude-artifact-framework: only one chat block per app \u2014 the conversation persists under data.chat and there is exactly one."
1420
1467
  );
1421
1468
  }
1422
- for (const [name, tool] of Object.entries(c.tools || {})) {
1423
- if (!tool || typeof tool.description !== "string" || typeof tool.run !== "function") {
1424
- throw new Error(
1425
- `claude-artifact-framework: chat tool "${name}" needs a \`description\` string and a \`run(input, ctx)\` function.`
1426
- );
1427
- }
1428
- }
1429
1469
  return layout;
1430
1470
  }
1431
1471
  if (layout === "records") {
@@ -1492,10 +1532,12 @@ function defaultsFrom(spec) {
1492
1532
  if ((spec.layout || "panel") === "records" && !Array.isArray(data.records)) {
1493
1533
  data.records = [];
1494
1534
  }
1495
- if ((spec.layout || "panel") === "chat" && !data.chat) {
1535
+ const allBlocks = [...spec.blocks || [], ...spec.main || [], ...spec.sidebar || []];
1536
+ const hasChat = (spec.layout || "panel") === "chat" || allBlocks.some((b) => b && b.chat);
1537
+ if (hasChat && !data.chat) {
1496
1538
  data.chat = { api: [], log: [] };
1497
1539
  }
1498
- for (const block of [...spec.blocks || [], ...spec.steps || []]) {
1540
+ for (const block of [...allBlocks, ...spec.steps || []]) {
1499
1541
  for (const field of block.fields || []) {
1500
1542
  if (field && field.key !== void 0 && !(field.key in data)) {
1501
1543
  data[field.key] = field.value !== void 0 ? field.value : field.type === "check" ? false : "";
@@ -1567,7 +1609,16 @@ function RecordsWithBlocks({ spec, ctx }) {
1567
1609
  ) : null
1568
1610
  );
1569
1611
  }
1570
- var LAYOUT_COMPONENTS = { panel: Panel, records: RecordsWithBlocks, steps: StepsLayout, chat: ChatLayout };
1612
+ function WorkspaceLayout({ spec, ctx }) {
1613
+ const render = (blocks) => (blocks || []).map((block, i) => createElement("section", { key: i, className: "caf-ws-slot" }, createElement(Block, { block, ctx })));
1614
+ return createElement(
1615
+ "div",
1616
+ { className: "caf-workspace" },
1617
+ createElement("div", { className: "caf-ws-main" }, render(spec.main)),
1618
+ spec.sidebar && spec.sidebar.length ? createElement("div", { className: "caf-ws-side" }, render(spec.sidebar)) : null
1619
+ );
1620
+ }
1621
+ var LAYOUT_COMPONENTS = { panel: Panel, records: RecordsWithBlocks, steps: StepsLayout, chat: ChatLayout, workspace: WorkspaceLayout };
1571
1622
  function createApp(spec = {}) {
1572
1623
  const layout = validate(spec);
1573
1624
  const state = createAppState(defaultsFrom(spec), { shared: Boolean(spec.shared) });
@@ -1747,6 +1798,16 @@ var BASE_CSS = `
1747
1798
  .caf-md-h { display: block; margin-top: 0.3rem; }
1748
1799
  .caf-md-li { display: block; padding-left: 1rem; position: relative; }
1749
1800
  .caf-md-li::before { content: "\u2022"; position: absolute; left: 0.2rem; opacity: 0.6; }
1801
+ .caf-workspace { max-width: 1200px; margin: 0 auto; display: flex; gap: 1rem; align-items: flex-start; }
1802
+ .caf-ws-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1rem; }
1803
+ .caf-ws-side { width: 340px; flex: none; display: flex; flex-direction: column; gap: 1rem; position: sticky; top: 1rem; }
1804
+ .caf-ws-slot { min-width: 0; }
1805
+ .caf-chat-panel { display: flex; flex-direction: column; min-height: 0; }
1806
+ .caf-ws-side .caf-chat-log { max-height: calc(100vh - 220px); }
1807
+ @media (max-width: 900px) {
1808
+ .caf-workspace { flex-direction: column; align-items: stretch; }
1809
+ .caf-ws-side { width: 100%; position: static; }
1810
+ }
1750
1811
  .caf-empty, .caf-loading { color: var(--caf-muted); font-size: 0.9rem; text-align: center; padding: 1.6rem 1rem; }
1751
1812
  .caf-block-error { border-color: var(--caf-danger); gap: 0.4rem; }
1752
1813
  .caf-block-error strong { font-size: 0.9rem; color: var(--caf-danger); }