claude-artifact-framework 0.3.0 → 0.5.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/dist/index.esm.js CHANGED
@@ -124,6 +124,7 @@ function required() {
124
124
  var createElement = (...args) => required().createElement(...args);
125
125
  var useState = (...args) => required().useState(...args);
126
126
  var useEffect = (...args) => required().useEffect(...args);
127
+ var useRef = (...args) => required().useRef(...args);
127
128
  var Component = React && React.Component || class ReactMissing {
128
129
  render() {
129
130
  return required();
@@ -154,10 +155,13 @@ function debounce2(fn, ms) {
154
155
  timer = setTimeout(() => fn(...args), ms);
155
156
  };
156
157
  }
157
- function createAppState(defaults, { debounceMs = 400 } = {}) {
158
+ function createAppState(defaults, { debounceMs = 400, shared = false, pollMs = 2500 } = {}) {
159
+ const dataStore = shared ? storage.shared : storage;
158
160
  let data = structuredCloneish(defaults);
159
161
  let view = { ...EMPTY_VIEW };
160
162
  let hydrated = false;
163
+ let writes = 0;
164
+ let persistedWrites = 0;
161
165
  const listeners = /* @__PURE__ */ new Set();
162
166
  let queued = false;
163
167
  function notify() {
@@ -168,11 +172,30 @@ function createAppState(defaults, { debounceMs = 400 } = {}) {
168
172
  for (const fn of listeners) fn();
169
173
  });
170
174
  }
171
- const persistData = debounce2(() => storage.set(DATA_KEY, data), debounceMs);
175
+ const persistData = debounce2(() => {
176
+ const at = writes;
177
+ Promise.resolve(dataStore.set(DATA_KEY, data)).then(() => {
178
+ persistedWrites = Math.max(persistedWrites, at);
179
+ }).catch(() => {
180
+ });
181
+ }, debounceMs);
172
182
  const persistView = debounce2(() => storage.set(VIEW_KEY, view), debounceMs);
183
+ if (shared) {
184
+ setInterval(async () => {
185
+ if (!hydrated || writes > persistedWrites) return;
186
+ try {
187
+ const remote = await dataStore.get(DATA_KEY, void 0);
188
+ if (remote !== void 0 && JSON.stringify(remote) !== JSON.stringify(data)) {
189
+ data = mergeDefaults(defaults, remote);
190
+ notify();
191
+ }
192
+ } catch {
193
+ }
194
+ }, pollMs);
195
+ }
173
196
  const ready = (async () => {
174
197
  const [storedData, storedView] = await Promise.all([
175
- storage.get(DATA_KEY, void 0),
198
+ dataStore.get(DATA_KEY, void 0),
176
199
  storage.get(VIEW_KEY, void 0)
177
200
  ]);
178
201
  if (storedData !== void 0) data = mergeDefaults(defaults, storedData);
@@ -184,6 +207,7 @@ function createAppState(defaults, { debounceMs = 400 } = {}) {
184
207
  const result = fn(data);
185
208
  if (result !== void 0) data = result;
186
209
  data = { ...data };
210
+ writes++;
187
211
  persistData();
188
212
  notify();
189
213
  }
@@ -342,6 +366,11 @@ function themeCss(seed) {
342
366
  :root[data-theme="light"] { ${vars(p.light)} }
343
367
  `;
344
368
  }
369
+ function seriesColors(seed, n) {
370
+ const [h0, s0] = rgbToHsl(hexToRgb(seed || "#3b6ef6"));
371
+ const s = clamp(s0, 45, 70);
372
+ return Array.from({ length: n }, (_, i) => hsl((h0 + i * 137.5) % 360, s, 52));
373
+ }
345
374
 
346
375
  // src/blocks.js
347
376
  function formatValue(value, format) {
@@ -476,8 +505,8 @@ function OutputBlock({ spec, ctx }) {
476
505
  )
477
506
  );
478
507
  }
479
- function TextBlock({ spec }) {
480
- const body = typeof spec.text === "function" ? spec.text() : spec.text;
508
+ function TextBlock({ spec, ctx }) {
509
+ const body = typeof spec.text === "function" ? spec.text(ctx.data) : spec.text;
481
510
  return createElement(
482
511
  "div",
483
512
  { className: "caf-block caf-text" },
@@ -509,6 +538,161 @@ function ListBlock({ spec, ctx }) {
509
538
  )
510
539
  );
511
540
  }
541
+ var CHART_TYPES = ["bar", "line", "donut"];
542
+ function ChartBlock({ spec, ctx }) {
543
+ const c = typeof spec.chart === "function" ? spec.chart(ctx.data) : spec.chart;
544
+ const items = c && c.items || [];
545
+ if (!items.length) {
546
+ return createElement("div", { className: "caf-block caf-empty" }, spec.empty || "No data to chart yet.");
547
+ }
548
+ const type = c.type || "bar";
549
+ if (!CHART_TYPES.includes(type)) {
550
+ throw new Error(`unknown chart type "${type}". Valid types: ${CHART_TYPES.join(", ")}.`);
551
+ }
552
+ const colors = ctx.colors(items.length);
553
+ const body = type === "bar" ? barChart(items, c, colors) : type === "line" ? lineChart(items, c) : donutChart(items, c, colors);
554
+ return createElement(
555
+ "div",
556
+ { className: "caf-block caf-chart" },
557
+ spec.title ? createElement("h2", { className: "caf-block-title" }, spec.title) : null,
558
+ body
559
+ );
560
+ }
561
+ function barChart(items, c, colors) {
562
+ const max = Math.max(...items.map((it) => Number(it.value) || 0), 0) || 1;
563
+ return createElement(
564
+ "div",
565
+ { className: "caf-bars" },
566
+ items.map(
567
+ (it, i) => createElement(
568
+ "div",
569
+ { key: it.label ?? i, className: "caf-bar-row" },
570
+ createElement("span", { className: "caf-bar-label" }, it.label),
571
+ createElement(
572
+ "div",
573
+ { className: "caf-bar-track" },
574
+ createElement("div", {
575
+ className: "caf-bar-fill",
576
+ style: { width: `${Math.max(2, (Number(it.value) || 0) / max * 100)}%`, background: colors[i] }
577
+ })
578
+ ),
579
+ createElement("span", { className: "caf-bar-value" }, formatValue(it.value, c.format))
580
+ )
581
+ )
582
+ );
583
+ }
584
+ function lineChart(items, c) {
585
+ const values = items.map((it) => Number(it.value) || 0);
586
+ const min = Math.min(...values);
587
+ const max = Math.max(...values);
588
+ const span = max - min || 1;
589
+ const W = 100;
590
+ const H = 36;
591
+ const pts = values.map((v, i) => [
592
+ values.length === 1 ? W / 2 : i / (values.length - 1) * W,
593
+ H - 3 - (v - min) / span * (H - 6)
594
+ ]);
595
+ const last = pts[pts.length - 1];
596
+ return createElement(
597
+ "div",
598
+ { className: "caf-line" },
599
+ createElement(
600
+ "svg",
601
+ { viewBox: `0 0 ${W} ${H}`, className: "caf-line-svg", preserveAspectRatio: "none", "aria-hidden": "true" },
602
+ [0.25, 0.5, 0.75].map(
603
+ (f) => createElement("line", { key: f, x1: 0, x2: W, y1: H * f, y2: H * f, className: "caf-line-grid" })
604
+ ),
605
+ createElement("polyline", { points: pts.map((p) => p.join(",")).join(" "), className: "caf-line-path" }),
606
+ createElement("circle", { cx: last[0], cy: last[1], r: 1.6, className: "caf-line-dot" })
607
+ ),
608
+ createElement(
609
+ "div",
610
+ { className: "caf-line-meta" },
611
+ createElement("span", { className: "caf-hint" }, String(items[0].label ?? "")),
612
+ createElement("span", { className: "caf-line-last" }, formatValue(values[values.length - 1], c.format)),
613
+ createElement("span", { className: "caf-hint" }, String(items[items.length - 1].label ?? ""))
614
+ )
615
+ );
616
+ }
617
+ function donutChart(items, c, colors) {
618
+ const total = items.reduce((s, it) => s + (Number(it.value) || 0), 0) || 1;
619
+ const R = 15.9155;
620
+ let offset = 25;
621
+ const segments = items.map((it, i) => {
622
+ const pct = (Number(it.value) || 0) / total * 100;
623
+ const seg = createElement("circle", {
624
+ key: it.label ?? i,
625
+ cx: 21,
626
+ cy: 21,
627
+ r: R,
628
+ className: "caf-donut-seg",
629
+ stroke: colors[i],
630
+ strokeDasharray: `${pct} ${100 - pct}`,
631
+ strokeDashoffset: offset
632
+ });
633
+ offset -= pct;
634
+ return seg;
635
+ });
636
+ return createElement(
637
+ "div",
638
+ { className: "caf-donut" },
639
+ createElement("svg", { viewBox: "0 0 42 42", className: "caf-donut-svg", "aria-hidden": "true" }, segments),
640
+ createElement(
641
+ "div",
642
+ { className: "caf-donut-legend" },
643
+ items.map(
644
+ (it, i) => createElement(
645
+ "div",
646
+ { key: it.label ?? i, className: "caf-legend-row" },
647
+ createElement("span", { className: "caf-legend-swatch", style: { background: colors[i] } }),
648
+ createElement("span", { className: "caf-legend-label" }, it.label),
649
+ createElement("span", { className: "caf-legend-value" }, formatValue(it.value, c.format))
650
+ )
651
+ )
652
+ )
653
+ );
654
+ }
655
+ function TimerBlock({ spec, ctx }) {
656
+ const t = spec.timer || {};
657
+ const total = Math.max(1, Math.round((t.minutes || 0) * 60 + (t.seconds || 0)) || 300);
658
+ const [left, setLeft] = useState(total);
659
+ const [running, setRunning] = useState(false);
660
+ useEffect(() => {
661
+ if (!running) return;
662
+ const id = setInterval(() => setLeft((prev) => Math.max(0, prev - 1)), 1e3);
663
+ return () => clearInterval(id);
664
+ }, [running]);
665
+ useEffect(() => {
666
+ if (left === 0 && running) {
667
+ setRunning(false);
668
+ if (typeof t.onDone === "function") ctx.update((d) => t.onDone(d));
669
+ }
670
+ }, [left, running]);
671
+ const mm = String(Math.floor(left / 60)).padStart(2, "0");
672
+ const ss = String(left % 60).padStart(2, "0");
673
+ const done = left === 0;
674
+ return createElement(
675
+ "div",
676
+ { className: "caf-block caf-timer" },
677
+ spec.title ? createElement("h2", { className: "caf-block-title" }, spec.title) : null,
678
+ t.label ? createElement("span", { className: "caf-hint" }, t.label) : null,
679
+ createElement("div", { className: done ? "caf-timer-time caf-timer-done" : "caf-timer-time" }, done ? t.doneText || "Done!" : `${mm}:${ss}`),
680
+ createElement("div", { className: "caf-bar-track" }, createElement("div", { className: "caf-bar-fill caf-timer-fill", style: { width: `${left / total * 100}%` } })),
681
+ createElement(
682
+ "div",
683
+ { className: "caf-timer-controls" },
684
+ createElement(
685
+ "button",
686
+ { type: "button", className: "caf-btn caf-btn-primary", disabled: done, onClick: () => setRunning((r) => !r) },
687
+ running ? "Pause" : "Start"
688
+ ),
689
+ createElement("button", { type: "button", className: "caf-btn", onClick: () => {
690
+ setRunning(false);
691
+ setLeft(total);
692
+ } }, "Reset")
693
+ )
694
+ );
695
+ }
512
696
  function CustomBlock({ spec, ctx }) {
513
697
  const rendered = spec.custom(ctx);
514
698
  if (typeof rendered === "string") {
@@ -521,11 +705,28 @@ var BLOCKS = {
521
705
  output: OutputBlock,
522
706
  text: TextBlock,
523
707
  list: ListBlock,
708
+ chart: ChartBlock,
709
+ timer: TimerBlock,
524
710
  custom: CustomBlock
525
711
  };
526
712
  var BLOCK_NAMES = Object.keys(BLOCKS);
527
713
 
528
714
  // src/records.js
715
+ function computedEntries(spec) {
716
+ return Object.entries(spec.compute || {}).map(([k, v]) => [k, typeof v === "function" ? { value: v } : v]);
717
+ }
718
+ function withComputed(spec, rec) {
719
+ if (!spec.compute) return rec;
720
+ const out = { ...rec };
721
+ for (const [key, c] of computedEntries(spec)) {
722
+ try {
723
+ out[key] = c.value(rec);
724
+ } catch {
725
+ out[key] = void 0;
726
+ }
727
+ }
728
+ return out;
729
+ }
529
730
  function recordDefaults(fields) {
530
731
  const rec = {};
531
732
  for (const f of fields) {
@@ -565,8 +766,16 @@ function Summary({ spec, records }) {
565
766
  }
566
767
  function ListScreen({ spec, ctx }) {
567
768
  const label = spec.label || "item";
568
- let records = ctx.data.records;
769
+ const [query, setQuery] = useState("");
770
+ let records = ctx.data.records.map((r) => withComputed(spec, r));
569
771
  if (spec.sort) records = records.slice().sort(spec.sort);
772
+ const total = records.length;
773
+ const q = query.trim().toLowerCase();
774
+ if (spec.search && q) {
775
+ records = records.filter(
776
+ (r) => spec.fields.some((f) => String(r[f.key] ?? "").toLowerCase().includes(q))
777
+ );
778
+ }
570
779
  function add() {
571
780
  const rec = { id: makeId(), ...recordDefaults(spec.fields) };
572
781
  ctx.update((d) => {
@@ -580,9 +789,21 @@ function ListScreen({ spec, ctx }) {
580
789
  createElement(
581
790
  "div",
582
791
  { className: "caf-toolbar" },
583
- createElement("span", { className: "caf-count" }, `${records.length} ${label}${records.length === 1 ? "" : "s"}`),
792
+ createElement(
793
+ "span",
794
+ { className: "caf-count" },
795
+ spec.search && q ? `${records.length} of ${total}` : `${total} ${label}${total === 1 ? "" : "s"}`
796
+ ),
584
797
  createElement("button", { type: "button", className: "caf-btn caf-btn-primary", onClick: add }, `Add ${label}`)
585
798
  ),
799
+ spec.search ? createElement("input", {
800
+ className: "caf-search",
801
+ value: query,
802
+ placeholder: `Search ${label}s\u2026`,
803
+ "aria-label": "Search",
804
+ onChange: (e) => setQuery(e.target.value)
805
+ }) : null,
806
+ spec.search && q && records.length === 0 && total > 0 ? createElement("div", { className: "caf-empty" }, `Nothing matches "${query}".`) : null,
586
807
  records.length === 0 ? createElement("div", { className: "caf-empty" }, spec.empty || `No ${label}s yet. Add the first one.`) : records.map(
587
808
  (rec) => createElement(
588
809
  "button",
@@ -625,7 +846,20 @@ function DetailScreen({ spec, ctx, record }) {
625
846
  if (rec) rec[field.key] = v;
626
847
  })
627
848
  })
628
- )
849
+ ),
850
+ spec.compute ? createElement(
851
+ "div",
852
+ { className: "caf-output-grid caf-computed" },
853
+ computedEntries(spec).map(([key, c]) => {
854
+ const live = withComputed(spec, record);
855
+ return createElement(
856
+ "div",
857
+ { key, className: "caf-stat" },
858
+ createElement("span", { className: "caf-stat-label" }, c.label || key),
859
+ createElement("span", { className: "caf-stat-value" }, formatValue(live[key], c.format))
860
+ );
861
+ })
862
+ ) : null
629
863
  );
630
864
  }
631
865
  function RecordsLayout({ spec, ctx }) {
@@ -639,21 +873,365 @@ function RecordsLayout({ spec, ctx }) {
639
873
  return createElement(
640
874
  "div",
641
875
  { className: "caf-panel caf-panel-single" },
642
- createElement(Summary, { spec: rspec, records: ctx.data.records }),
876
+ createElement(Summary, { spec: rspec, records: ctx.data.records.map((r) => withComputed(rspec, r)) }),
643
877
  createElement(ListScreen, { spec: rspec, ctx })
644
878
  );
645
879
  }
646
880
 
881
+ // src/steps.js
882
+ function stepErrors(step, data) {
883
+ return (step.fields || []).some((f) => validateField(f, data[f.key]) !== null);
884
+ }
885
+ function ResultScreen({ step, ctx }) {
886
+ const items = step.result(ctx.data) || [];
887
+ return createElement(
888
+ "div",
889
+ { className: "caf-block caf-output" },
890
+ step.title ? createElement("h2", { className: "caf-block-title" }, step.title) : null,
891
+ createElement(
892
+ "div",
893
+ { className: "caf-output-grid" },
894
+ items.map(
895
+ (item, i) => createElement(
896
+ "div",
897
+ { key: item.label ?? i, className: item.big ? "caf-stat caf-stat-big" : "caf-stat" },
898
+ createElement("span", { className: "caf-stat-label" }, item.label),
899
+ createElement("span", { className: "caf-stat-value" }, formatValue(item.value, item.format)),
900
+ item.hint ? createElement("small", { className: "caf-hint" }, item.hint) : null
901
+ )
902
+ )
903
+ ),
904
+ createElement(
905
+ "div",
906
+ { className: "caf-step-nav" },
907
+ createElement("button", { type: "button", className: "caf-btn", onClick: () => ctx.setStep(0) }, "Start over")
908
+ )
909
+ );
910
+ }
911
+ function StepsLayout({ spec, ctx }) {
912
+ const steps = spec.steps;
913
+ const i = Math.min(Math.max(ctx.view.step || 0, 0), steps.length - 1);
914
+ const step = steps[i];
915
+ const isResult = typeof step.result === "function";
916
+ const isLast = i === steps.length - 1;
917
+ return createElement(
918
+ "div",
919
+ { className: "caf-panel caf-panel-single" },
920
+ createElement(
921
+ "div",
922
+ { className: "caf-steps-progress" },
923
+ createElement("span", { className: "caf-hint" }, isResult ? "Result" : `Step ${i + 1} of ${steps.length}`),
924
+ createElement(
925
+ "div",
926
+ { className: "caf-bar-track" },
927
+ createElement("div", { className: "caf-bar-fill caf-steps-fill", style: { width: `${(i + 1) / steps.length * 100}%` } })
928
+ )
929
+ ),
930
+ isResult ? createElement(ResultScreen, { step, ctx }) : createElement(
931
+ "div",
932
+ { className: "caf-block caf-fields" },
933
+ step.title ? createElement("h2", { className: "caf-block-title" }, step.title) : null,
934
+ step.text ? createElement("p", { className: "caf-step-text" }, typeof step.text === "function" ? step.text(ctx.data) : step.text) : null,
935
+ (step.fields || []).map(
936
+ (field) => createElement(Field, {
937
+ key: field.key,
938
+ field,
939
+ value: ctx.data[field.key],
940
+ onChange: (v) => ctx.update((d) => {
941
+ d[field.key] = v;
942
+ })
943
+ })
944
+ ),
945
+ createElement(
946
+ "div",
947
+ { className: "caf-step-nav" },
948
+ i > 0 ? createElement("button", { type: "button", className: "caf-btn", onClick: () => ctx.setStep(i - 1) }, "\u2039 Back") : createElement("span"),
949
+ !isLast ? createElement(
950
+ "button",
951
+ {
952
+ type: "button",
953
+ className: "caf-btn caf-btn-primary",
954
+ disabled: stepErrors(step, ctx.data),
955
+ onClick: () => ctx.setStep(i + 1)
956
+ },
957
+ "Next \u203A"
958
+ ) : null
959
+ )
960
+ )
961
+ );
962
+ }
963
+
964
+ // src/chat.js
965
+ var CHAT_KEYS = ["system", "greeting", "placeholder", "tools", "model", "maxTokens"];
966
+ var DEFAULT_MODEL = "claude-sonnet-4-6";
967
+ var MAX_ROUNDS = 8;
968
+ async function streamCall({ model, maxTokens, system, messages, onText }) {
969
+ const res = await fetch("https://api.anthropic.com/v1/messages", {
970
+ method: "POST",
971
+ headers: { "Content-Type": "application/json" },
972
+ body: JSON.stringify({ model, max_tokens: maxTokens, system, messages, stream: true })
973
+ });
974
+ if (!res.ok || !res.body) {
975
+ let detail = "";
976
+ try {
977
+ const b = await res.json();
978
+ detail = b && b.error && b.error.message || JSON.stringify(b);
979
+ } catch {
980
+ detail = await res.text().catch(() => "");
981
+ }
982
+ throw new Error(`HTTP ${res.status}${detail ? ": " + detail : ""}`);
983
+ }
984
+ const reader = res.body.getReader();
985
+ const decoder = new TextDecoder();
986
+ let full = "";
987
+ let buf = "";
988
+ for (; ; ) {
989
+ const { done, value } = await reader.read();
990
+ if (done) break;
991
+ buf += decoder.decode(value, { stream: true });
992
+ const lines = buf.split("\n");
993
+ buf = lines.pop();
994
+ for (const line of lines) {
995
+ if (!line.startsWith("data: ")) continue;
996
+ try {
997
+ const j = JSON.parse(line.slice(6));
998
+ if (j.type === "content_block_delta" && j.delta && j.delta.text) {
999
+ full += j.delta.text;
1000
+ if (onText) onText(full);
1001
+ }
1002
+ } catch {
1003
+ }
1004
+ }
1005
+ }
1006
+ return full;
1007
+ }
1008
+ function parseToolCalls(text) {
1009
+ const match = text.match(/```tool_call\s*\n?([\s\S]*?)```/);
1010
+ if (!match) return null;
1011
+ try {
1012
+ const calls = JSON.parse(match[1]);
1013
+ return Array.isArray(calls) && calls.length ? calls : null;
1014
+ } catch {
1015
+ return null;
1016
+ }
1017
+ }
1018
+ function stripToolCallBlock(text) {
1019
+ return text.replace(/```tool_call\s*\n?[\s\S]*?```/, "").trim();
1020
+ }
1021
+ function toolProtocol(tools) {
1022
+ const list = Object.entries(tools).map(([name, t]) => `- ${name}: ${t.description}
1023
+ Input: ${JSON.stringify(t.input || {})}`).join("\n");
1024
+ return `
1025
+
1026
+ You have tools available:
1027
+ ${list}
1028
+
1029
+ 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:
1030
+
1031
+ \`\`\`tool_call
1032
+ [{ "name": "tool_name", "input": { "param": "value" } }]
1033
+ \`\`\`
1034
+
1035
+ 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.`;
1036
+ }
1037
+ function runTools(calls, tools, ctx) {
1038
+ return calls.map((call) => {
1039
+ const tool = tools[call.name];
1040
+ if (!tool) {
1041
+ return { name: call.name, error: `unknown tool "${call.name}". Valid tools: ${Object.keys(tools).join(", ")}` };
1042
+ }
1043
+ try {
1044
+ const result = tool.run(call.input || {}, ctx);
1045
+ return { name: call.name, result: result === void 0 ? { ok: true } : result };
1046
+ } catch (e) {
1047
+ return { name: call.name, error: String(e && e.message || e) };
1048
+ }
1049
+ });
1050
+ }
1051
+ function Bubble({ m }) {
1052
+ if (m.role === "tools") {
1053
+ return createElement("div", { className: "caf-msg-tools" }, `used: ${m.tools.join(", ")}`);
1054
+ }
1055
+ if (m.role === "error") {
1056
+ return createElement(
1057
+ "div",
1058
+ { className: "caf-msg caf-msg-error" },
1059
+ createElement("strong", null, "The call failed"),
1060
+ createElement("span", null, m.content),
1061
+ createElement("span", { className: "caf-hint" }, "Your message is kept \u2014 send again to retry.")
1062
+ );
1063
+ }
1064
+ return createElement("div", { className: m.role === "user" ? "caf-msg caf-msg-user" : "caf-msg caf-msg-assistant" }, m.content);
1065
+ }
1066
+ function ChatLayout({ spec, ctx }) {
1067
+ const c = spec.chat;
1068
+ const [draft, setDraft] = useState("");
1069
+ const [busy, setBusy] = useState(false);
1070
+ const [live, setLive] = useState(null);
1071
+ const endRef = useRef(null);
1072
+ const chat = ctx.data.chat || { api: [], log: [] };
1073
+ const log = chat.log || [];
1074
+ useEffect(() => {
1075
+ if (endRef.current) endRef.current.scrollIntoView({ block: "end" });
1076
+ }, [log.length, live]);
1077
+ async function send() {
1078
+ const text = draft.trim();
1079
+ if (!text || busy) return;
1080
+ setDraft("");
1081
+ setBusy(true);
1082
+ let api = [...chat.api || [], { role: "user", content: text }];
1083
+ let logNow = [...log, { role: "user", content: text }];
1084
+ const commit = () => ctx.update((d) => {
1085
+ d.chat = { api, log: logNow };
1086
+ });
1087
+ commit();
1088
+ try {
1089
+ const system = (typeof c.system === "function" ? c.system(ctx.data) : c.system || "") + (c.tools ? toolProtocol(c.tools) : "");
1090
+ for (let round = 0; round < MAX_ROUNDS; round++) {
1091
+ const raw = await streamCall({
1092
+ model: c.model || DEFAULT_MODEL,
1093
+ maxTokens: c.maxTokens || 2e3,
1094
+ system,
1095
+ messages: api,
1096
+ onText: (t) => setLive(stripToolCallBlock(t))
1097
+ });
1098
+ api = [...api, { role: "assistant", content: raw }];
1099
+ const shown = stripToolCallBlock(raw);
1100
+ const calls = c.tools ? parseToolCalls(raw) : null;
1101
+ if (!calls) {
1102
+ logNow = [...logNow, { role: "assistant", content: shown || raw }];
1103
+ commit();
1104
+ break;
1105
+ }
1106
+ const results = runTools(calls, c.tools, ctx);
1107
+ if (shown) logNow = [...logNow, { role: "assistant", content: shown }];
1108
+ logNow = [...logNow, { role: "tools", tools: calls.map((x) => x.name) }];
1109
+ api = [
1110
+ ...api,
1111
+ {
1112
+ role: "user",
1113
+ content: `[TOOL_RESULT]
1114
+ ${JSON.stringify(results)}
1115
+ [/TOOL_RESULT]
1116
+
1117
+ Continue with this information. If you have everything you need, answer in natural language without further tool_call.`
1118
+ }
1119
+ ];
1120
+ commit();
1121
+ setLive(null);
1122
+ }
1123
+ } catch (e) {
1124
+ logNow = [...logNow, { role: "error", content: String(e && e.message || e) }];
1125
+ commit();
1126
+ } finally {
1127
+ setLive(null);
1128
+ setBusy(false);
1129
+ }
1130
+ }
1131
+ return createElement(
1132
+ "div",
1133
+ { className: "caf-panel caf-panel-single" },
1134
+ createElement(
1135
+ "div",
1136
+ { className: "caf-block caf-chat-log" },
1137
+ c.greeting ? createElement("div", { className: "caf-msg caf-msg-assistant" }, c.greeting) : null,
1138
+ log.map((m, i) => createElement(Bubble, { key: i, m })),
1139
+ live !== null ? createElement("div", { className: "caf-msg caf-msg-assistant caf-msg-live" }, live || "\u2026") : null,
1140
+ busy && live === null ? createElement("div", { className: "caf-msg-tools" }, "thinking\u2026") : null,
1141
+ createElement("div", { ref: endRef })
1142
+ ),
1143
+ createElement(
1144
+ "form",
1145
+ {
1146
+ className: "caf-chat-input",
1147
+ onSubmit: (e) => {
1148
+ e.preventDefault();
1149
+ send();
1150
+ }
1151
+ },
1152
+ createElement("input", {
1153
+ value: draft,
1154
+ placeholder: c.placeholder || "Message\u2026",
1155
+ disabled: busy,
1156
+ onChange: (e) => setDraft(e.target.value),
1157
+ "aria-label": "Message"
1158
+ }),
1159
+ createElement("button", { type: "submit", className: "caf-btn caf-btn-primary", disabled: busy || !draft.trim() }, "Send")
1160
+ )
1161
+ );
1162
+ }
1163
+
647
1164
  // src/app.js
648
- var LAYOUTS = ["panel", "records"];
1165
+ var LAYOUTS = ["panel", "records", "steps", "chat"];
649
1166
  var RESERVED = ["title", "empty", "wide", "onSelect", "subtitle"];
1167
+ var TOP_KEYS = ["title", "subtitle", "theme", "layout", "blocks", "records", "steps", "chat", "data", "shared"];
650
1168
  function validate(spec) {
1169
+ const unknownTop = Object.keys(spec).filter((k) => !TOP_KEYS.includes(k));
1170
+ if (unknownTop.length) {
1171
+ throw new Error(
1172
+ `claude-artifact-framework: unknown top-level keys (${unknownTop.join(", ")}). Valid keys: ${TOP_KEYS.join(", ")}.`
1173
+ );
1174
+ }
651
1175
  const layout = spec.layout || "panel";
652
1176
  if (!LAYOUTS.includes(layout)) {
653
1177
  throw new Error(
654
1178
  `claude-artifact-framework: unknown layout "${layout}". Valid layouts: ${LAYOUTS.join(", ")}.`
655
1179
  );
656
1180
  }
1181
+ if (layout === "steps") {
1182
+ const steps = spec.steps;
1183
+ if (!Array.isArray(steps) || steps.length === 0) {
1184
+ throw new Error('claude-artifact-framework: layout "steps" needs `steps: [...]` with at least one step.');
1185
+ }
1186
+ const STEP_KEYS = ["title", "text", "fields", "result"];
1187
+ const seen = /* @__PURE__ */ new Set();
1188
+ steps.forEach((st, i) => {
1189
+ if (!st || typeof st !== "object") {
1190
+ throw new Error(`claude-artifact-framework: steps[${i}] must be an object.`);
1191
+ }
1192
+ const unknown = Object.keys(st).filter((k) => !STEP_KEYS.includes(k));
1193
+ if (unknown.length) {
1194
+ throw new Error(
1195
+ `claude-artifact-framework: steps[${i}] has unknown keys (${unknown.join(", ")}). Valid keys: ${STEP_KEYS.join(", ")}.`
1196
+ );
1197
+ }
1198
+ if (!st.fields && !st.text && !st.result) {
1199
+ throw new Error(`claude-artifact-framework: steps[${i}] needs \`fields\`, \`text\`, or \`result\`.`);
1200
+ }
1201
+ for (const f of st.fields || []) {
1202
+ if (!f || !f.key) throw new Error(`claude-artifact-framework: every field in steps[${i}] needs a \`key\`.`);
1203
+ if (seen.has(f.key)) {
1204
+ throw new Error(
1205
+ `claude-artifact-framework: field key "${f.key}" appears in more than one step \u2014 keys share one data pool and must be unique.`
1206
+ );
1207
+ }
1208
+ seen.add(f.key);
1209
+ }
1210
+ });
1211
+ return layout;
1212
+ }
1213
+ if (layout === "chat") {
1214
+ const c = spec.chat;
1215
+ if (!c || typeof c.system !== "string" && typeof c.system !== "function") {
1216
+ throw new Error(
1217
+ 'claude-artifact-framework: layout "chat" needs `chat: { system: "..." }` (a string or a function of data).'
1218
+ );
1219
+ }
1220
+ const unknown = Object.keys(c).filter((k) => !CHAT_KEYS.includes(k));
1221
+ if (unknown.length) {
1222
+ throw new Error(
1223
+ `claude-artifact-framework: chat has unknown keys (${unknown.join(", ")}). Valid keys: ${CHAT_KEYS.join(", ")}.`
1224
+ );
1225
+ }
1226
+ for (const [name, tool] of Object.entries(c.tools || {})) {
1227
+ if (!tool || typeof tool.description !== "string" || typeof tool.run !== "function") {
1228
+ throw new Error(
1229
+ `claude-artifact-framework: chat tool "${name}" needs a \`description\` string and a \`run(input, ctx)\` function.`
1230
+ );
1231
+ }
1232
+ }
1233
+ return layout;
1234
+ }
657
1235
  if (layout === "records") {
658
1236
  const r = spec.records;
659
1237
  if (!r || !Array.isArray(r.fields) || r.fields.length === 0) {
@@ -661,6 +1239,13 @@ function validate(spec) {
661
1239
  'claude-artifact-framework: layout "records" needs `records: { fields: [...] }` \u2014 the same field declarations the fields block uses.'
662
1240
  );
663
1241
  }
1242
+ const RECORDS_KEYS = ["label", "fields", "title", "subtitle", "sort", "summary", "empty", "search", "compute"];
1243
+ const unknownR = Object.keys(r).filter((k) => !RECORDS_KEYS.includes(k));
1244
+ if (unknownR.length) {
1245
+ throw new Error(
1246
+ `claude-artifact-framework: records has unknown keys (${unknownR.join(", ")}). Valid keys: ${RECORDS_KEYS.join(", ")}.`
1247
+ );
1248
+ }
664
1249
  const seen = /* @__PURE__ */ new Set();
665
1250
  for (const f of r.fields) {
666
1251
  if (!f || !f.key) throw new Error("claude-artifact-framework: every records field needs a `key`.");
@@ -668,6 +1253,18 @@ function validate(spec) {
668
1253
  if (seen.has(f.key)) throw new Error(`claude-artifact-framework: duplicate records field key "${f.key}".`);
669
1254
  seen.add(f.key);
670
1255
  }
1256
+ for (const [key, c] of Object.entries(r.compute || {})) {
1257
+ if (seen.has(key) || key === "id") {
1258
+ throw new Error(
1259
+ `claude-artifact-framework: compute key "${key}" collides with a field key \u2014 computed values are derived, never stored.`
1260
+ );
1261
+ }
1262
+ if (typeof c !== "function" && typeof (c && c.value) !== "function") {
1263
+ throw new Error(
1264
+ `claude-artifact-framework: compute "${key}" must be a function of the record, or { label, format, value: (r) => ... }.`
1265
+ );
1266
+ }
1267
+ }
671
1268
  return layout;
672
1269
  }
673
1270
  const blocks = spec.blocks || [];
@@ -700,7 +1297,10 @@ function defaultsFrom(spec) {
700
1297
  if ((spec.layout || "panel") === "records" && !Array.isArray(data.records)) {
701
1298
  data.records = [];
702
1299
  }
703
- for (const block of spec.blocks || []) {
1300
+ if ((spec.layout || "panel") === "chat" && !data.chat) {
1301
+ data.chat = { api: [], log: [] };
1302
+ }
1303
+ for (const block of [...spec.blocks || [], ...spec.steps || []]) {
704
1304
  for (const field of block.fields || []) {
705
1305
  if (field && field.key !== void 0 && !(field.key in data)) {
706
1306
  data[field.key] = field.value !== void 0 ? field.value : field.type === "check" ? false : "";
@@ -753,10 +1353,10 @@ function Panel({ spec, ctx }) {
753
1353
  )
754
1354
  );
755
1355
  }
756
- var LAYOUT_COMPONENTS = { panel: Panel, records: RecordsLayout };
1356
+ var LAYOUT_COMPONENTS = { panel: Panel, records: RecordsLayout, steps: StepsLayout, chat: ChatLayout };
757
1357
  function createApp(spec = {}) {
758
1358
  const layout = validate(spec);
759
- const state = createAppState(defaultsFrom(spec));
1359
+ const state = createAppState(defaultsFrom(spec), { shared: Boolean(spec.shared) });
760
1360
  const css = themeCss(spec.theme && spec.theme.seed) + BASE_CSS;
761
1361
  return function App() {
762
1362
  if (!hasReact()) {
@@ -776,7 +1376,9 @@ function createApp(spec = {}) {
776
1376
  view: state.getView(),
777
1377
  update: state.update,
778
1378
  go: state.go,
779
- back: state.back
1379
+ back: state.back,
1380
+ setStep: (n) => state.setView({ step: n }),
1381
+ colors: (n) => seriesColors(spec.theme && spec.theme.seed, n)
780
1382
  };
781
1383
  const Layout = LAYOUT_COMPONENTS[layout];
782
1384
  return createElement(
@@ -864,6 +1466,56 @@ var BASE_CSS = `
864
1466
  .caf-btn:focus-visible { outline: 2px solid var(--caf-accent); outline-offset: 1px; }
865
1467
  .caf-btn-primary { background: var(--caf-accent); border-color: var(--caf-accent); color: var(--caf-accentText); }
866
1468
  .caf-btn-danger { background: transparent; border-color: var(--caf-danger); color: var(--caf-danger); }
1469
+ .caf-bars { display: flex; flex-direction: column; gap: 0.55rem; }
1470
+ .caf-bar-row { display: grid; grid-template-columns: minmax(60px, 30%) 1fr auto; align-items: center; gap: 0.6rem; }
1471
+ .caf-bar-label { font-size: 0.82rem; color: var(--caf-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1472
+ .caf-bar-track { height: 8px; border-radius: 4px; background: var(--caf-sunken); overflow: hidden; }
1473
+ .caf-bar-fill { height: 100%; border-radius: 4px; background: var(--caf-accent); }
1474
+ .caf-bar-value { font-size: 0.82rem; font-weight: 600; font-variant-numeric: tabular-nums; }
1475
+ .caf-line-svg { width: 100%; height: 110px; display: block; }
1476
+ .caf-line-grid { stroke: var(--caf-border); stroke-width: 0.3; }
1477
+ .caf-line-path { fill: none; stroke: var(--caf-accent); stroke-width: 1.1; stroke-linejoin: round; stroke-linecap: round; }
1478
+ .caf-line-dot { fill: var(--caf-accent); }
1479
+ .caf-line-meta { display: flex; justify-content: space-between; align-items: baseline; gap: 0.6rem; }
1480
+ .caf-line-last { font-weight: 650; font-variant-numeric: tabular-nums; color: var(--caf-accent); }
1481
+ .caf-donut { display: flex; align-items: center; gap: 1.1rem; flex-wrap: wrap; }
1482
+ .caf-donut-svg { width: 110px; height: 110px; flex: none; transform: rotate(0.001deg); }
1483
+ .caf-donut-seg { fill: none; stroke-width: 6; }
1484
+ .caf-donut-legend { display: flex; flex-direction: column; gap: 0.4rem; min-width: 0; flex: 1; }
1485
+ .caf-legend-row { display: flex; align-items: center; gap: 0.5rem; font-size: 0.84rem; }
1486
+ .caf-legend-swatch { width: 10px; height: 10px; border-radius: 3px; flex: none; }
1487
+ .caf-legend-label { color: var(--caf-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1488
+ .caf-legend-value { margin-left: auto; font-weight: 600; font-variant-numeric: tabular-nums; }
1489
+ .caf-timer { align-items: center; text-align: center; }
1490
+ .caf-timer .caf-bar-track { width: 100%; }
1491
+ .caf-timer-time { font-size: 2.6rem; font-weight: 700; font-variant-numeric: tabular-nums; letter-spacing: 0.02em; }
1492
+ .caf-timer-done { color: var(--caf-accent); }
1493
+ .caf-timer-fill { transition: width 0.9s linear; }
1494
+ .caf-timer-controls { display: flex; gap: 0.6rem; }
1495
+ .caf-btn:disabled { opacity: 0.45; cursor: not-allowed; }
1496
+ .caf-steps-progress { display: flex; flex-direction: column; gap: 0.35rem; margin-bottom: 0.2rem; }
1497
+ .caf-steps-fill { transition: width 0.25s ease; }
1498
+ .caf-step-text { margin: 0; color: var(--caf-muted); font-size: 0.92rem; line-height: 1.55; }
1499
+ .caf-step-nav { display: flex; justify-content: space-between; gap: 0.6rem; margin-top: 0.4rem; }
1500
+ .caf-chat-log { min-height: 320px; max-height: 62vh; overflow-y: auto; gap: 0.55rem; }
1501
+ .caf-msg { max-width: 85%; padding: 0.55rem 0.75rem; border-radius: 12px; font-size: 0.92rem; line-height: 1.5; white-space: pre-wrap; word-break: break-word; }
1502
+ .caf-msg-user { align-self: flex-end; background: var(--caf-accent); color: var(--caf-accentText); border-bottom-right-radius: 4px; }
1503
+ .caf-msg-assistant { align-self: flex-start; background: var(--caf-sunken); border-bottom-left-radius: 4px; }
1504
+ .caf-msg-live::after { content: "\u258D"; opacity: 0.6; }
1505
+ .caf-msg-tools { align-self: flex-start; font-size: 0.74rem; color: var(--caf-muted); padding: 0.15rem 0.55rem; border: 1px solid var(--caf-border); border-radius: 999px; }
1506
+ .caf-msg-error { align-self: stretch; max-width: none; display: flex; flex-direction: column; gap: 0.2rem; background: transparent; border: 1px solid var(--caf-danger); }
1507
+ .caf-msg-error strong { color: var(--caf-danger); font-size: 0.85rem; }
1508
+ .caf-chat-input { display: flex; gap: 0.6rem; margin-top: 0.8rem; }
1509
+ .caf-chat-input input { flex: 1; font: inherit; font-size: 0.95rem; padding: 0.55rem 0.7rem; border-radius: 8px; border: 1px solid var(--caf-border); background: var(--caf-surface); color: var(--caf-text); }
1510
+ .caf-chat-input input:focus-visible { outline: 2px solid var(--caf-accent); outline-offset: 1px; }
1511
+ .caf-search {
1512
+ font: inherit; font-size: 0.9rem; width: 100%;
1513
+ padding: 0.5rem 0.65rem; margin: 0 0 0.3rem;
1514
+ border-radius: 7px; border: 1px solid var(--caf-border);
1515
+ background: var(--caf-sunken); color: var(--caf-text);
1516
+ }
1517
+ .caf-search:focus-visible { outline: 2px solid var(--caf-accent); outline-offset: 1px; }
1518
+ .caf-computed { border-top: 1px solid var(--caf-border); padding-top: 0.7rem; }
867
1519
  .caf-empty, .caf-loading { color: var(--caf-muted); font-size: 0.9rem; text-align: center; padding: 1.6rem 1rem; }
868
1520
  .caf-block-error { border-color: var(--caf-danger); gap: 0.4rem; }
869
1521
  .caf-block-error strong { font-size: 0.9rem; color: var(--caf-danger); }