claude-artifact-framework 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +119 -3
- package/dist/index.esm.js +412 -35
- package/dist/index.esm.js.map +3 -3
- package/dist/index.global.js +47 -9
- package/dist/index.global.js.map +4 -4
- package/package.json +1 -1
- package/src/app.js +150 -31
- package/src/appState.js +34 -3
- package/src/chat.js +248 -0
- package/src/records.js +64 -5
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
|
}
|
|
@@ -349,6 +373,7 @@ function seriesColors(seed, n) {
|
|
|
349
373
|
}
|
|
350
374
|
|
|
351
375
|
// src/blocks.js
|
|
376
|
+
var FIELD_TYPES = ["text", "textarea", "number", "money", "percent", "check", "date", "select"];
|
|
352
377
|
function formatValue(value, format) {
|
|
353
378
|
if (value === null || value === void 0 || value === "") return "\u2014";
|
|
354
379
|
const n = Number(value);
|
|
@@ -688,6 +713,21 @@ var BLOCKS = {
|
|
|
688
713
|
var BLOCK_NAMES = Object.keys(BLOCKS);
|
|
689
714
|
|
|
690
715
|
// src/records.js
|
|
716
|
+
function computedEntries(spec) {
|
|
717
|
+
return Object.entries(spec.compute || {}).map(([k, v]) => [k, typeof v === "function" ? { value: v } : v]);
|
|
718
|
+
}
|
|
719
|
+
function withComputed(spec, rec) {
|
|
720
|
+
if (!spec.compute) return rec;
|
|
721
|
+
const out = { ...rec };
|
|
722
|
+
for (const [key, c] of computedEntries(spec)) {
|
|
723
|
+
try {
|
|
724
|
+
out[key] = c.value(rec);
|
|
725
|
+
} catch {
|
|
726
|
+
out[key] = void 0;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
return out;
|
|
730
|
+
}
|
|
691
731
|
function recordDefaults(fields) {
|
|
692
732
|
const rec = {};
|
|
693
733
|
for (const f of fields) {
|
|
@@ -727,8 +767,16 @@ function Summary({ spec, records }) {
|
|
|
727
767
|
}
|
|
728
768
|
function ListScreen({ spec, ctx }) {
|
|
729
769
|
const label = spec.label || "item";
|
|
730
|
-
|
|
770
|
+
const [query, setQuery] = useState("");
|
|
771
|
+
let records = ctx.data.records.map((r) => withComputed(spec, r));
|
|
731
772
|
if (spec.sort) records = records.slice().sort(spec.sort);
|
|
773
|
+
const total = records.length;
|
|
774
|
+
const q = query.trim().toLowerCase();
|
|
775
|
+
if (spec.search && q) {
|
|
776
|
+
records = records.filter(
|
|
777
|
+
(r) => spec.fields.some((f) => String(r[f.key] ?? "").toLowerCase().includes(q))
|
|
778
|
+
);
|
|
779
|
+
}
|
|
732
780
|
function add() {
|
|
733
781
|
const rec = { id: makeId(), ...recordDefaults(spec.fields) };
|
|
734
782
|
ctx.update((d) => {
|
|
@@ -742,9 +790,21 @@ function ListScreen({ spec, ctx }) {
|
|
|
742
790
|
createElement(
|
|
743
791
|
"div",
|
|
744
792
|
{ className: "caf-toolbar" },
|
|
745
|
-
createElement(
|
|
793
|
+
createElement(
|
|
794
|
+
"span",
|
|
795
|
+
{ className: "caf-count" },
|
|
796
|
+
spec.search && q ? `${records.length} of ${total}` : `${total} ${label}${total === 1 ? "" : "s"}`
|
|
797
|
+
),
|
|
746
798
|
createElement("button", { type: "button", className: "caf-btn caf-btn-primary", onClick: add }, `Add ${label}`)
|
|
747
799
|
),
|
|
800
|
+
spec.search ? createElement("input", {
|
|
801
|
+
className: "caf-search",
|
|
802
|
+
value: query,
|
|
803
|
+
placeholder: `Search ${label}s\u2026`,
|
|
804
|
+
"aria-label": "Search",
|
|
805
|
+
onChange: (e) => setQuery(e.target.value)
|
|
806
|
+
}) : null,
|
|
807
|
+
spec.search && q && records.length === 0 && total > 0 ? createElement("div", { className: "caf-empty" }, `Nothing matches "${query}".`) : null,
|
|
748
808
|
records.length === 0 ? createElement("div", { className: "caf-empty" }, spec.empty || `No ${label}s yet. Add the first one.`) : records.map(
|
|
749
809
|
(rec) => createElement(
|
|
750
810
|
"button",
|
|
@@ -787,7 +847,20 @@ function DetailScreen({ spec, ctx, record }) {
|
|
|
787
847
|
if (rec) rec[field.key] = v;
|
|
788
848
|
})
|
|
789
849
|
})
|
|
790
|
-
)
|
|
850
|
+
),
|
|
851
|
+
spec.compute ? createElement(
|
|
852
|
+
"div",
|
|
853
|
+
{ className: "caf-output-grid caf-computed" },
|
|
854
|
+
computedEntries(spec).map(([key, c]) => {
|
|
855
|
+
const live = withComputed(spec, record);
|
|
856
|
+
return createElement(
|
|
857
|
+
"div",
|
|
858
|
+
{ key, className: "caf-stat" },
|
|
859
|
+
createElement("span", { className: "caf-stat-label" }, c.label || key),
|
|
860
|
+
createElement("span", { className: "caf-stat-value" }, formatValue(live[key], c.format))
|
|
861
|
+
);
|
|
862
|
+
})
|
|
863
|
+
) : null
|
|
791
864
|
);
|
|
792
865
|
}
|
|
793
866
|
function RecordsLayout({ spec, ctx }) {
|
|
@@ -801,7 +874,7 @@ function RecordsLayout({ spec, ctx }) {
|
|
|
801
874
|
return createElement(
|
|
802
875
|
"div",
|
|
803
876
|
{ className: "caf-panel caf-panel-single" },
|
|
804
|
-
createElement(Summary, { spec: rspec, records: ctx.data.records }),
|
|
877
|
+
createElement(Summary, { spec: rspec, records: ctx.data.records.map((r) => withComputed(rspec, r)) }),
|
|
805
878
|
createElement(ListScreen, { spec: rspec, ctx })
|
|
806
879
|
);
|
|
807
880
|
}
|
|
@@ -889,10 +962,252 @@ function StepsLayout({ spec, ctx }) {
|
|
|
889
962
|
);
|
|
890
963
|
}
|
|
891
964
|
|
|
965
|
+
// src/chat.js
|
|
966
|
+
var CHAT_KEYS = ["system", "greeting", "placeholder", "tools", "model", "maxTokens"];
|
|
967
|
+
var DEFAULT_MODEL = "claude-sonnet-4-6";
|
|
968
|
+
var MAX_ROUNDS = 8;
|
|
969
|
+
async function streamCall({ model, maxTokens, system, messages, onText }) {
|
|
970
|
+
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
971
|
+
method: "POST",
|
|
972
|
+
headers: { "Content-Type": "application/json" },
|
|
973
|
+
body: JSON.stringify({ model, max_tokens: maxTokens, system, messages, stream: true })
|
|
974
|
+
});
|
|
975
|
+
if (!res.ok || !res.body) {
|
|
976
|
+
let detail = "";
|
|
977
|
+
try {
|
|
978
|
+
const b = await res.json();
|
|
979
|
+
detail = b && b.error && b.error.message || JSON.stringify(b);
|
|
980
|
+
} catch {
|
|
981
|
+
detail = await res.text().catch(() => "");
|
|
982
|
+
}
|
|
983
|
+
throw new Error(`HTTP ${res.status}${detail ? ": " + detail : ""}`);
|
|
984
|
+
}
|
|
985
|
+
const reader = res.body.getReader();
|
|
986
|
+
const decoder = new TextDecoder();
|
|
987
|
+
let full = "";
|
|
988
|
+
let buf = "";
|
|
989
|
+
for (; ; ) {
|
|
990
|
+
const { done, value } = await reader.read();
|
|
991
|
+
if (done) break;
|
|
992
|
+
buf += decoder.decode(value, { stream: true });
|
|
993
|
+
const lines = buf.split("\n");
|
|
994
|
+
buf = lines.pop();
|
|
995
|
+
for (const line of lines) {
|
|
996
|
+
if (!line.startsWith("data: ")) continue;
|
|
997
|
+
try {
|
|
998
|
+
const j = JSON.parse(line.slice(6));
|
|
999
|
+
if (j.type === "content_block_delta" && j.delta && j.delta.text) {
|
|
1000
|
+
full += j.delta.text;
|
|
1001
|
+
if (onText) onText(full);
|
|
1002
|
+
}
|
|
1003
|
+
} catch {
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
return full;
|
|
1008
|
+
}
|
|
1009
|
+
function parseToolCalls(text) {
|
|
1010
|
+
const match = text.match(/```tool_call\s*\n?([\s\S]*?)```/);
|
|
1011
|
+
if (!match) return null;
|
|
1012
|
+
try {
|
|
1013
|
+
const calls = JSON.parse(match[1]);
|
|
1014
|
+
return Array.isArray(calls) && calls.length ? calls : null;
|
|
1015
|
+
} catch {
|
|
1016
|
+
return null;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
function stripToolCallBlock(text) {
|
|
1020
|
+
return text.replace(/```tool_call\s*\n?[\s\S]*?```/, "").trim();
|
|
1021
|
+
}
|
|
1022
|
+
function toolProtocol(tools) {
|
|
1023
|
+
const list = Object.entries(tools).map(([name, t]) => `- ${name}: ${t.description}
|
|
1024
|
+
Input: ${JSON.stringify(t.input || {})}`).join("\n");
|
|
1025
|
+
return `
|
|
1026
|
+
|
|
1027
|
+
You have tools available:
|
|
1028
|
+
${list}
|
|
1029
|
+
|
|
1030
|
+
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:
|
|
1031
|
+
|
|
1032
|
+
\`\`\`tool_call
|
|
1033
|
+
[{ "name": "tool_name", "input": { "param": "value" } }]
|
|
1034
|
+
\`\`\`
|
|
1035
|
+
|
|
1036
|
+
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.`;
|
|
1037
|
+
}
|
|
1038
|
+
function runTools(calls, tools, ctx) {
|
|
1039
|
+
return calls.map((call) => {
|
|
1040
|
+
const tool = tools[call.name];
|
|
1041
|
+
if (!tool) {
|
|
1042
|
+
return { name: call.name, error: `unknown tool "${call.name}". Valid tools: ${Object.keys(tools).join(", ")}` };
|
|
1043
|
+
}
|
|
1044
|
+
try {
|
|
1045
|
+
const result = tool.run(call.input || {}, ctx);
|
|
1046
|
+
return { name: call.name, result: result === void 0 ? { ok: true } : result };
|
|
1047
|
+
} catch (e) {
|
|
1048
|
+
return { name: call.name, error: String(e && e.message || e) };
|
|
1049
|
+
}
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
function Bubble({ m }) {
|
|
1053
|
+
if (m.role === "tools") {
|
|
1054
|
+
return createElement("div", { className: "caf-msg-tools" }, `used: ${m.tools.join(", ")}`);
|
|
1055
|
+
}
|
|
1056
|
+
if (m.role === "error") {
|
|
1057
|
+
return createElement(
|
|
1058
|
+
"div",
|
|
1059
|
+
{ className: "caf-msg caf-msg-error" },
|
|
1060
|
+
createElement("strong", null, "The call failed"),
|
|
1061
|
+
createElement("span", null, m.content),
|
|
1062
|
+
createElement("span", { className: "caf-hint" }, "Your message is kept \u2014 send again to retry.")
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
1065
|
+
return createElement("div", { className: m.role === "user" ? "caf-msg caf-msg-user" : "caf-msg caf-msg-assistant" }, m.content);
|
|
1066
|
+
}
|
|
1067
|
+
function ChatLayout({ spec, ctx }) {
|
|
1068
|
+
const c = spec.chat;
|
|
1069
|
+
const [draft, setDraft] = useState("");
|
|
1070
|
+
const [busy, setBusy] = useState(false);
|
|
1071
|
+
const [live, setLive] = useState(null);
|
|
1072
|
+
const endRef = useRef(null);
|
|
1073
|
+
const chat = ctx.data.chat || { api: [], log: [] };
|
|
1074
|
+
const log = chat.log || [];
|
|
1075
|
+
useEffect(() => {
|
|
1076
|
+
if (endRef.current) endRef.current.scrollIntoView({ block: "end" });
|
|
1077
|
+
}, [log.length, live]);
|
|
1078
|
+
async function send() {
|
|
1079
|
+
const text = draft.trim();
|
|
1080
|
+
if (!text || busy) return;
|
|
1081
|
+
setDraft("");
|
|
1082
|
+
setBusy(true);
|
|
1083
|
+
let api = [...chat.api || [], { role: "user", content: text }];
|
|
1084
|
+
let logNow = [...log, { role: "user", content: text }];
|
|
1085
|
+
const commit = () => ctx.update((d) => {
|
|
1086
|
+
d.chat = { api, log: logNow };
|
|
1087
|
+
});
|
|
1088
|
+
commit();
|
|
1089
|
+
try {
|
|
1090
|
+
const system = (typeof c.system === "function" ? c.system(ctx.data) : c.system || "") + (c.tools ? toolProtocol(c.tools) : "");
|
|
1091
|
+
for (let round = 0; round < MAX_ROUNDS; round++) {
|
|
1092
|
+
const raw = await streamCall({
|
|
1093
|
+
model: c.model || DEFAULT_MODEL,
|
|
1094
|
+
maxTokens: c.maxTokens || 2e3,
|
|
1095
|
+
system,
|
|
1096
|
+
messages: api,
|
|
1097
|
+
onText: (t) => setLive(stripToolCallBlock(t))
|
|
1098
|
+
});
|
|
1099
|
+
api = [...api, { role: "assistant", content: raw }];
|
|
1100
|
+
const shown = stripToolCallBlock(raw);
|
|
1101
|
+
const calls = c.tools ? parseToolCalls(raw) : null;
|
|
1102
|
+
if (!calls) {
|
|
1103
|
+
logNow = [...logNow, { role: "assistant", content: shown || raw }];
|
|
1104
|
+
commit();
|
|
1105
|
+
break;
|
|
1106
|
+
}
|
|
1107
|
+
const results = runTools(calls, c.tools, ctx);
|
|
1108
|
+
if (shown) logNow = [...logNow, { role: "assistant", content: shown }];
|
|
1109
|
+
logNow = [...logNow, { role: "tools", tools: calls.map((x) => x.name) }];
|
|
1110
|
+
api = [
|
|
1111
|
+
...api,
|
|
1112
|
+
{
|
|
1113
|
+
role: "user",
|
|
1114
|
+
content: `[TOOL_RESULT]
|
|
1115
|
+
${JSON.stringify(results)}
|
|
1116
|
+
[/TOOL_RESULT]
|
|
1117
|
+
|
|
1118
|
+
Continue with this information. If you have everything you need, answer in natural language without further tool_call.`
|
|
1119
|
+
}
|
|
1120
|
+
];
|
|
1121
|
+
commit();
|
|
1122
|
+
setLive(null);
|
|
1123
|
+
}
|
|
1124
|
+
} catch (e) {
|
|
1125
|
+
logNow = [...logNow, { role: "error", content: String(e && e.message || e) }];
|
|
1126
|
+
commit();
|
|
1127
|
+
} finally {
|
|
1128
|
+
setLive(null);
|
|
1129
|
+
setBusy(false);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
return createElement(
|
|
1133
|
+
"div",
|
|
1134
|
+
{ className: "caf-panel caf-panel-single" },
|
|
1135
|
+
createElement(
|
|
1136
|
+
"div",
|
|
1137
|
+
{ className: "caf-block caf-chat-log" },
|
|
1138
|
+
c.greeting ? createElement("div", { className: "caf-msg caf-msg-assistant" }, c.greeting) : null,
|
|
1139
|
+
log.map((m, i) => createElement(Bubble, { key: i, m })),
|
|
1140
|
+
live !== null ? createElement("div", { className: "caf-msg caf-msg-assistant caf-msg-live" }, live || "\u2026") : null,
|
|
1141
|
+
busy && live === null ? createElement("div", { className: "caf-msg-tools" }, "thinking\u2026") : null,
|
|
1142
|
+
createElement("div", { ref: endRef })
|
|
1143
|
+
),
|
|
1144
|
+
createElement(
|
|
1145
|
+
"form",
|
|
1146
|
+
{
|
|
1147
|
+
className: "caf-chat-input",
|
|
1148
|
+
onSubmit: (e) => {
|
|
1149
|
+
e.preventDefault();
|
|
1150
|
+
send();
|
|
1151
|
+
}
|
|
1152
|
+
},
|
|
1153
|
+
createElement("input", {
|
|
1154
|
+
value: draft,
|
|
1155
|
+
placeholder: c.placeholder || "Message\u2026",
|
|
1156
|
+
disabled: busy,
|
|
1157
|
+
onChange: (e) => setDraft(e.target.value),
|
|
1158
|
+
"aria-label": "Message"
|
|
1159
|
+
}),
|
|
1160
|
+
createElement("button", { type: "submit", className: "caf-btn caf-btn-primary", disabled: busy || !draft.trim() }, "Send")
|
|
1161
|
+
)
|
|
1162
|
+
);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
892
1165
|
// src/app.js
|
|
893
|
-
var LAYOUTS = ["panel", "records", "steps"];
|
|
1166
|
+
var LAYOUTS = ["panel", "records", "steps", "chat"];
|
|
894
1167
|
var RESERVED = ["title", "empty", "wide", "onSelect", "subtitle"];
|
|
1168
|
+
var TOP_KEYS = ["title", "subtitle", "theme", "layout", "blocks", "records", "steps", "chat", "data", "shared"];
|
|
1169
|
+
function checkFields(fields, where) {
|
|
1170
|
+
for (const f of fields || []) {
|
|
1171
|
+
if (!f || !f.key) throw new Error(`claude-artifact-framework: every field in ${where} needs a \`key\`.`);
|
|
1172
|
+
if (f.type !== void 0 && !FIELD_TYPES.includes(f.type)) {
|
|
1173
|
+
throw new Error(
|
|
1174
|
+
`claude-artifact-framework: field "${f.key}" in ${where} has unknown type "${f.type}". Valid types: ${FIELD_TYPES.join(", ")}.`
|
|
1175
|
+
);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
function checkBlocks(blocks) {
|
|
1180
|
+
if (!Array.isArray(blocks)) {
|
|
1181
|
+
throw new Error("claude-artifact-framework: `blocks` must be an array.");
|
|
1182
|
+
}
|
|
1183
|
+
blocks.forEach((block, i) => {
|
|
1184
|
+
if (!block || typeof block !== "object") {
|
|
1185
|
+
throw new Error(`claude-artifact-framework: blocks[${i}] must be an object like { fields: [...] }.`);
|
|
1186
|
+
}
|
|
1187
|
+
const keys = Object.keys(block).filter((k) => !RESERVED.includes(k));
|
|
1188
|
+
const known = keys.filter((k) => BLOCK_NAMES.includes(k));
|
|
1189
|
+
if (known.length === 0) {
|
|
1190
|
+
throw new Error(
|
|
1191
|
+
`claude-artifact-framework: blocks[${i}] has no recognised block type (found: ${keys.join(", ") || "nothing"}). Valid block types: ${BLOCK_NAMES.join(", ")}.`
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1194
|
+
if (known.length > 1) {
|
|
1195
|
+
throw new Error(
|
|
1196
|
+
`claude-artifact-framework: blocks[${i}] declares more than one block type (${known.join(
|
|
1197
|
+
", "
|
|
1198
|
+
)}). Split them into separate entries of \`blocks\`.`
|
|
1199
|
+
);
|
|
1200
|
+
}
|
|
1201
|
+
checkFields(block.fields, `blocks[${i}]`);
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
895
1204
|
function validate(spec) {
|
|
1205
|
+
const unknownTop = Object.keys(spec).filter((k) => !TOP_KEYS.includes(k));
|
|
1206
|
+
if (unknownTop.length) {
|
|
1207
|
+
throw new Error(
|
|
1208
|
+
`claude-artifact-framework: unknown top-level keys (${unknownTop.join(", ")}). Valid keys: ${TOP_KEYS.join(", ")}.`
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
896
1211
|
const layout = spec.layout || "panel";
|
|
897
1212
|
if (!LAYOUTS.includes(layout)) {
|
|
898
1213
|
throw new Error(
|
|
@@ -919,8 +1234,8 @@ function validate(spec) {
|
|
|
919
1234
|
if (!st.fields && !st.text && !st.result) {
|
|
920
1235
|
throw new Error(`claude-artifact-framework: steps[${i}] needs \`fields\`, \`text\`, or \`result\`.`);
|
|
921
1236
|
}
|
|
1237
|
+
checkFields(st.fields, `steps[${i}]`);
|
|
922
1238
|
for (const f of st.fields || []) {
|
|
923
|
-
if (!f || !f.key) throw new Error(`claude-artifact-framework: every field in steps[${i}] needs a \`key\`.`);
|
|
924
1239
|
if (seen.has(f.key)) {
|
|
925
1240
|
throw new Error(
|
|
926
1241
|
`claude-artifact-framework: field key "${f.key}" appears in more than one step \u2014 keys share one data pool and must be unique.`
|
|
@@ -931,6 +1246,28 @@ function validate(spec) {
|
|
|
931
1246
|
});
|
|
932
1247
|
return layout;
|
|
933
1248
|
}
|
|
1249
|
+
if (layout === "chat") {
|
|
1250
|
+
const c = spec.chat;
|
|
1251
|
+
if (!c || typeof c.system !== "string" && typeof c.system !== "function") {
|
|
1252
|
+
throw new Error(
|
|
1253
|
+
'claude-artifact-framework: layout "chat" needs `chat: { system: "..." }` (a string or a function of data).'
|
|
1254
|
+
);
|
|
1255
|
+
}
|
|
1256
|
+
const unknown = Object.keys(c).filter((k) => !CHAT_KEYS.includes(k));
|
|
1257
|
+
if (unknown.length) {
|
|
1258
|
+
throw new Error(
|
|
1259
|
+
`claude-artifact-framework: chat has unknown keys (${unknown.join(", ")}). Valid keys: ${CHAT_KEYS.join(", ")}.`
|
|
1260
|
+
);
|
|
1261
|
+
}
|
|
1262
|
+
for (const [name, tool] of Object.entries(c.tools || {})) {
|
|
1263
|
+
if (!tool || typeof tool.description !== "string" || typeof tool.run !== "function") {
|
|
1264
|
+
throw new Error(
|
|
1265
|
+
`claude-artifact-framework: chat tool "${name}" needs a \`description\` string and a \`run(input, ctx)\` function.`
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
return layout;
|
|
1270
|
+
}
|
|
934
1271
|
if (layout === "records") {
|
|
935
1272
|
const r = spec.records;
|
|
936
1273
|
if (!r || !Array.isArray(r.fields) || r.fields.length === 0) {
|
|
@@ -938,38 +1275,36 @@ function validate(spec) {
|
|
|
938
1275
|
'claude-artifact-framework: layout "records" needs `records: { fields: [...] }` \u2014 the same field declarations the fields block uses.'
|
|
939
1276
|
);
|
|
940
1277
|
}
|
|
1278
|
+
const RECORDS_KEYS = ["label", "fields", "title", "subtitle", "sort", "summary", "empty", "search", "compute"];
|
|
1279
|
+
const unknownR = Object.keys(r).filter((k) => !RECORDS_KEYS.includes(k));
|
|
1280
|
+
if (unknownR.length) {
|
|
1281
|
+
throw new Error(
|
|
1282
|
+
`claude-artifact-framework: records has unknown keys (${unknownR.join(", ")}). Valid keys: ${RECORDS_KEYS.join(", ")}.`
|
|
1283
|
+
);
|
|
1284
|
+
}
|
|
1285
|
+
checkFields(r.fields, "records");
|
|
941
1286
|
const seen = /* @__PURE__ */ new Set();
|
|
942
1287
|
for (const f of r.fields) {
|
|
943
|
-
if (!f || !f.key) throw new Error("claude-artifact-framework: every records field needs a `key`.");
|
|
944
1288
|
if (f.key === "id") throw new Error('claude-artifact-framework: "id" is reserved on records \u2014 the framework assigns it.');
|
|
945
1289
|
if (seen.has(f.key)) throw new Error(`claude-artifact-framework: duplicate records field key "${f.key}".`);
|
|
946
1290
|
seen.add(f.key);
|
|
947
1291
|
}
|
|
1292
|
+
if (spec.blocks) checkBlocks(spec.blocks);
|
|
1293
|
+
for (const [key, c] of Object.entries(r.compute || {})) {
|
|
1294
|
+
if (seen.has(key) || key === "id") {
|
|
1295
|
+
throw new Error(
|
|
1296
|
+
`claude-artifact-framework: compute key "${key}" collides with a field key \u2014 computed values are derived, never stored.`
|
|
1297
|
+
);
|
|
1298
|
+
}
|
|
1299
|
+
if (typeof c !== "function" && typeof (c && c.value) !== "function") {
|
|
1300
|
+
throw new Error(
|
|
1301
|
+
`claude-artifact-framework: compute "${key}" must be a function of the record, or { label, format, value: (r) => ... }.`
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
948
1305
|
return layout;
|
|
949
1306
|
}
|
|
950
|
-
|
|
951
|
-
if (!Array.isArray(blocks)) {
|
|
952
|
-
throw new Error("claude-artifact-framework: `blocks` must be an array.");
|
|
953
|
-
}
|
|
954
|
-
blocks.forEach((block, i) => {
|
|
955
|
-
if (!block || typeof block !== "object") {
|
|
956
|
-
throw new Error(`claude-artifact-framework: blocks[${i}] must be an object like { fields: [...] }.`);
|
|
957
|
-
}
|
|
958
|
-
const keys = Object.keys(block).filter((k) => !RESERVED.includes(k));
|
|
959
|
-
const known = keys.filter((k) => BLOCK_NAMES.includes(k));
|
|
960
|
-
if (known.length === 0) {
|
|
961
|
-
throw new Error(
|
|
962
|
-
`claude-artifact-framework: blocks[${i}] has no recognised block type (found: ${keys.join(", ") || "nothing"}). Valid block types: ${BLOCK_NAMES.join(", ")}.`
|
|
963
|
-
);
|
|
964
|
-
}
|
|
965
|
-
if (known.length > 1) {
|
|
966
|
-
throw new Error(
|
|
967
|
-
`claude-artifact-framework: blocks[${i}] declares more than one block type (${known.join(
|
|
968
|
-
", "
|
|
969
|
-
)}). Split them into separate entries of \`blocks\`.`
|
|
970
|
-
);
|
|
971
|
-
}
|
|
972
|
-
});
|
|
1307
|
+
checkBlocks(spec.blocks || []);
|
|
973
1308
|
return layout;
|
|
974
1309
|
}
|
|
975
1310
|
function defaultsFrom(spec) {
|
|
@@ -977,6 +1312,9 @@ function defaultsFrom(spec) {
|
|
|
977
1312
|
if ((spec.layout || "panel") === "records" && !Array.isArray(data.records)) {
|
|
978
1313
|
data.records = [];
|
|
979
1314
|
}
|
|
1315
|
+
if ((spec.layout || "panel") === "chat" && !data.chat) {
|
|
1316
|
+
data.chat = { api: [], log: [] };
|
|
1317
|
+
}
|
|
980
1318
|
for (const block of [...spec.blocks || [], ...spec.steps || []]) {
|
|
981
1319
|
for (const field of block.fields || []) {
|
|
982
1320
|
if (field && field.key !== void 0 && !(field.key in data)) {
|
|
@@ -1030,10 +1368,29 @@ function Panel({ spec, ctx }) {
|
|
|
1030
1368
|
)
|
|
1031
1369
|
);
|
|
1032
1370
|
}
|
|
1033
|
-
|
|
1371
|
+
function RecordsWithBlocks({ spec, ctx }) {
|
|
1372
|
+
const detailOpen = ctx.view.screen === "record" && (ctx.data.records || []).some((r) => r.id === ctx.view.arg);
|
|
1373
|
+
return createElement(
|
|
1374
|
+
"div",
|
|
1375
|
+
null,
|
|
1376
|
+
createElement(RecordsLayout, { spec, ctx }),
|
|
1377
|
+
!detailOpen && Array.isArray(spec.blocks) && spec.blocks.length ? createElement(
|
|
1378
|
+
"div",
|
|
1379
|
+
{ className: "caf-panel caf-records-blocks" },
|
|
1380
|
+
spec.blocks.map(
|
|
1381
|
+
(block, i) => createElement(
|
|
1382
|
+
"section",
|
|
1383
|
+
{ key: i, className: block.wide ? "caf-slot caf-slot-wide" : "caf-slot" },
|
|
1384
|
+
createElement(Block, { block, ctx })
|
|
1385
|
+
)
|
|
1386
|
+
)
|
|
1387
|
+
) : null
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1390
|
+
var LAYOUT_COMPONENTS = { panel: Panel, records: RecordsWithBlocks, steps: StepsLayout, chat: ChatLayout };
|
|
1034
1391
|
function createApp(spec = {}) {
|
|
1035
1392
|
const layout = validate(spec);
|
|
1036
|
-
const state = createAppState(defaultsFrom(spec));
|
|
1393
|
+
const state = createAppState(defaultsFrom(spec), { shared: Boolean(spec.shared) });
|
|
1037
1394
|
const css = themeCss(spec.theme && spec.theme.seed) + BASE_CSS;
|
|
1038
1395
|
return function App() {
|
|
1039
1396
|
if (!hasReact()) {
|
|
@@ -1174,6 +1531,26 @@ var BASE_CSS = `
|
|
|
1174
1531
|
.caf-steps-fill { transition: width 0.25s ease; }
|
|
1175
1532
|
.caf-step-text { margin: 0; color: var(--caf-muted); font-size: 0.92rem; line-height: 1.55; }
|
|
1176
1533
|
.caf-step-nav { display: flex; justify-content: space-between; gap: 0.6rem; margin-top: 0.4rem; }
|
|
1534
|
+
.caf-chat-log { min-height: 320px; max-height: 62vh; overflow-y: auto; gap: 0.55rem; }
|
|
1535
|
+
.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; }
|
|
1536
|
+
.caf-msg-user { align-self: flex-end; background: var(--caf-accent); color: var(--caf-accentText); border-bottom-right-radius: 4px; }
|
|
1537
|
+
.caf-msg-assistant { align-self: flex-start; background: var(--caf-sunken); border-bottom-left-radius: 4px; }
|
|
1538
|
+
.caf-msg-live::after { content: "\u258D"; opacity: 0.6; }
|
|
1539
|
+
.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; }
|
|
1540
|
+
.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); }
|
|
1541
|
+
.caf-msg-error strong { color: var(--caf-danger); font-size: 0.85rem; }
|
|
1542
|
+
.caf-chat-input { display: flex; gap: 0.6rem; margin-top: 0.8rem; }
|
|
1543
|
+
.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); }
|
|
1544
|
+
.caf-chat-input input:focus-visible { outline: 2px solid var(--caf-accent); outline-offset: 1px; }
|
|
1545
|
+
.caf-search {
|
|
1546
|
+
font: inherit; font-size: 0.9rem; width: 100%;
|
|
1547
|
+
padding: 0.5rem 0.65rem; margin: 0 0 0.3rem;
|
|
1548
|
+
border-radius: 7px; border: 1px solid var(--caf-border);
|
|
1549
|
+
background: var(--caf-sunken); color: var(--caf-text);
|
|
1550
|
+
}
|
|
1551
|
+
.caf-search:focus-visible { outline: 2px solid var(--caf-accent); outline-offset: 1px; }
|
|
1552
|
+
.caf-computed { border-top: 1px solid var(--caf-border); padding-top: 0.7rem; }
|
|
1553
|
+
.caf-records-blocks { margin-top: 1rem; }
|
|
1177
1554
|
.caf-empty, .caf-loading { color: var(--caf-muted); font-size: 0.9rem; text-align: center; padding: 1.6rem 1rem; }
|
|
1178
1555
|
.caf-block-error { border-color: var(--caf-danger); gap: 0.4rem; }
|
|
1179
1556
|
.caf-block-error strong { font-size: 0.9rem; color: var(--caf-danger); }
|