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/README.md +134 -3
- package/dist/index.esm.js +666 -14
- package/dist/index.esm.js.map +3 -3
- package/dist/index.global.js +77 -9
- package/dist/index.global.js.map +4 -4
- package/package.json +1 -1
- package/src/app.js +143 -5
- package/src/appState.js +34 -3
- package/src/blocks.js +177 -3
- package/src/chat.js +248 -0
- package/src/records.js +64 -5
- package/src/steps.js +106 -0
- package/src/theme.js +9 -0
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(() =>
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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); }
|