claude-artifact-framework 0.4.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 +82 -3
- package/dist/index.esm.js +352 -10
- package/dist/index.esm.js.map +3 -3
- package/dist/index.global.js +46 -9
- package/dist/index.global.js.map +4 -4
- package/package.json +1 -1
- package/src/app.js +75 -3
- package/src/appState.js +34 -3
- package/src/chat.js +248 -0
- package/src/records.js +64 -5
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ mirrors npm packages automatically.
|
|
|
12
12
|
### As a global script (`<script>` tag)
|
|
13
13
|
|
|
14
14
|
```html
|
|
15
|
-
<script src="https://cdn.jsdelivr.net/npm/claude-artifact-framework@0.
|
|
15
|
+
<script src="https://cdn.jsdelivr.net/npm/claude-artifact-framework@0.5.0/dist/index.global.js"></script>
|
|
16
16
|
<script>
|
|
17
17
|
const { storage, createStore, createPersistedStore, createSharedStore } = ArtifactKit;
|
|
18
18
|
</script>
|
|
@@ -27,11 +27,11 @@ mirrors npm packages automatically.
|
|
|
27
27
|
createStore,
|
|
28
28
|
createPersistedStore,
|
|
29
29
|
createSharedStore,
|
|
30
|
-
} from "https://cdn.jsdelivr.net/npm/claude-artifact-framework@0.
|
|
30
|
+
} from "https://cdn.jsdelivr.net/npm/claude-artifact-framework@0.5.0/dist/index.esm.js";
|
|
31
31
|
</script>
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
Pin a version (`@0.
|
|
34
|
+
Pin a version (`@0.5.0`) for reproducible artifacts, or drop the version
|
|
35
35
|
(`claude-artifact-framework/dist/...`) to always get the latest release.
|
|
36
36
|
|
|
37
37
|
### Inside a React artifact
|
|
@@ -175,6 +175,85 @@ blocks: [
|
|
|
175
175
|
`text` blocks also accept a function of data — `{ text: (d) => "Hola " + d.nombre }` —
|
|
176
176
|
with the same contract as `output`.
|
|
177
177
|
|
|
178
|
+
### The `chat` layout — apps that talk to Claude
|
|
179
|
+
|
|
180
|
+
Tutors, advisors, generators, roleplay. Every piece of the network contract is
|
|
181
|
+
copied from code proven inside the real artifact runtime: the artifact fetches
|
|
182
|
+
`https://api.anthropic.com/v1/messages` directly with **no API key** (the
|
|
183
|
+
runtime proxies the call and injects auth), `stream: true` works, and tools
|
|
184
|
+
are prompted (the proxy does not support native function calling).
|
|
185
|
+
|
|
186
|
+
The author declares the system prompt and, optionally, tools that operate on
|
|
187
|
+
the app's data — a chat that can drive the app. The framework owns message
|
|
188
|
+
state, live streaming, the busy state, the tool loop, and errors as visible
|
|
189
|
+
bubbles. The conversation persists and survives reload.
|
|
190
|
+
|
|
191
|
+
```js
|
|
192
|
+
ArtifactKit.createApp({
|
|
193
|
+
title: "Asistente de gastos",
|
|
194
|
+
theme: { seed: "#0f766e" },
|
|
195
|
+
layout: "chat",
|
|
196
|
+
chat: {
|
|
197
|
+
system: (d) => "Eres un asistente de gastos. Total actual: " + (d.total || 0),
|
|
198
|
+
greeting: "¡Hola! Contame tus gastos.",
|
|
199
|
+
tools: {
|
|
200
|
+
agregar_gasto: {
|
|
201
|
+
description: "Agrega un gasto",
|
|
202
|
+
input: { concepto: "string", monto: "number" },
|
|
203
|
+
run: (input, ctx) => {
|
|
204
|
+
ctx.update((d) => { d.total = (d.total || 0) + Number(input.monto); });
|
|
205
|
+
return { ok: true };
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
`system` is a string or a function of data. Tool `run(input, ctx)` returns a
|
|
214
|
+
JSON-serializable result that is fed back to the model; an unknown tool name
|
|
215
|
+
is answered in-band so the model corrects itself. Optional: `greeting`,
|
|
216
|
+
`placeholder`, `model` (default `claude-sonnet-4-6`, proven through the
|
|
217
|
+
proxy), `maxTokens`.
|
|
218
|
+
|
|
219
|
+
### Search, computed fields, and shared data
|
|
220
|
+
|
|
221
|
+
Three record-app multipliers, all declarative:
|
|
222
|
+
|
|
223
|
+
```js
|
|
224
|
+
ArtifactKit.createApp({
|
|
225
|
+
title: "Inventario del club",
|
|
226
|
+
layout: "records",
|
|
227
|
+
shared: true, // one copy of the data for every viewer
|
|
228
|
+
records: {
|
|
229
|
+
search: true, // search box over all field values
|
|
230
|
+
fields: [
|
|
231
|
+
{ key: "nombre", type: "text", required: true },
|
|
232
|
+
{ key: "cantidad", type: "number", value: 1, min: 0 },
|
|
233
|
+
{ key: "precio", type: "money", value: 0, min: 0 },
|
|
234
|
+
],
|
|
235
|
+
compute: { // derived per record, never stored
|
|
236
|
+
subtotal: { label: "Subtotal", format: "money", value: (r) => r.cantidad * r.precio },
|
|
237
|
+
},
|
|
238
|
+
summary: (all) => [
|
|
239
|
+
{ label: "Valor total", value: all.reduce((s, r) => s + r.subtotal, 0), format: "money", big: true },
|
|
240
|
+
],
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
- `search: true` adds a filter box over every field's value; the count shows
|
|
246
|
+
"n of total" while filtering.
|
|
247
|
+
- `compute` entries are functions of the record (or `{ label, format, value }`).
|
|
248
|
+
Computed values appear read-only in the detail screen and are available to
|
|
249
|
+
`title` / `subtitle` / `sort` / `summary` as if they were fields. A compute
|
|
250
|
+
key that collides with a field key throws.
|
|
251
|
+
- `shared: true` (top level) stores the app's data in the shared scope: every
|
|
252
|
+
viewer of the published artifact converges on the same data via polling.
|
|
253
|
+
Conflict handling is last-write-wins; a poll never overwrites local edits
|
|
254
|
+
that haven't been persisted yet. View state (which screen is open) stays
|
|
255
|
+
personal per viewer.
|
|
256
|
+
|
|
178
257
|
## `storage`
|
|
179
258
|
|
|
180
259
|
Thin wrapper over `window.storage`, the persistence API Claude injects into a
|
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
|
}
|
|
@@ -688,6 +712,21 @@ var BLOCKS = {
|
|
|
688
712
|
var BLOCK_NAMES = Object.keys(BLOCKS);
|
|
689
713
|
|
|
690
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
|
+
}
|
|
691
730
|
function recordDefaults(fields) {
|
|
692
731
|
const rec = {};
|
|
693
732
|
for (const f of fields) {
|
|
@@ -727,8 +766,16 @@ function Summary({ spec, records }) {
|
|
|
727
766
|
}
|
|
728
767
|
function ListScreen({ spec, ctx }) {
|
|
729
768
|
const label = spec.label || "item";
|
|
730
|
-
|
|
769
|
+
const [query, setQuery] = useState("");
|
|
770
|
+
let records = ctx.data.records.map((r) => withComputed(spec, r));
|
|
731
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
|
+
}
|
|
732
779
|
function add() {
|
|
733
780
|
const rec = { id: makeId(), ...recordDefaults(spec.fields) };
|
|
734
781
|
ctx.update((d) => {
|
|
@@ -742,9 +789,21 @@ function ListScreen({ spec, ctx }) {
|
|
|
742
789
|
createElement(
|
|
743
790
|
"div",
|
|
744
791
|
{ className: "caf-toolbar" },
|
|
745
|
-
createElement(
|
|
792
|
+
createElement(
|
|
793
|
+
"span",
|
|
794
|
+
{ className: "caf-count" },
|
|
795
|
+
spec.search && q ? `${records.length} of ${total}` : `${total} ${label}${total === 1 ? "" : "s"}`
|
|
796
|
+
),
|
|
746
797
|
createElement("button", { type: "button", className: "caf-btn caf-btn-primary", onClick: add }, `Add ${label}`)
|
|
747
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,
|
|
748
807
|
records.length === 0 ? createElement("div", { className: "caf-empty" }, spec.empty || `No ${label}s yet. Add the first one.`) : records.map(
|
|
749
808
|
(rec) => createElement(
|
|
750
809
|
"button",
|
|
@@ -787,7 +846,20 @@ function DetailScreen({ spec, ctx, record }) {
|
|
|
787
846
|
if (rec) rec[field.key] = v;
|
|
788
847
|
})
|
|
789
848
|
})
|
|
790
|
-
)
|
|
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
|
|
791
863
|
);
|
|
792
864
|
}
|
|
793
865
|
function RecordsLayout({ spec, ctx }) {
|
|
@@ -801,7 +873,7 @@ function RecordsLayout({ spec, ctx }) {
|
|
|
801
873
|
return createElement(
|
|
802
874
|
"div",
|
|
803
875
|
{ className: "caf-panel caf-panel-single" },
|
|
804
|
-
createElement(Summary, { spec: rspec, records: ctx.data.records }),
|
|
876
|
+
createElement(Summary, { spec: rspec, records: ctx.data.records.map((r) => withComputed(rspec, r)) }),
|
|
805
877
|
createElement(ListScreen, { spec: rspec, ctx })
|
|
806
878
|
);
|
|
807
879
|
}
|
|
@@ -889,10 +961,217 @@ function StepsLayout({ spec, ctx }) {
|
|
|
889
961
|
);
|
|
890
962
|
}
|
|
891
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
|
+
|
|
892
1164
|
// src/app.js
|
|
893
|
-
var LAYOUTS = ["panel", "records", "steps"];
|
|
1165
|
+
var LAYOUTS = ["panel", "records", "steps", "chat"];
|
|
894
1166
|
var RESERVED = ["title", "empty", "wide", "onSelect", "subtitle"];
|
|
1167
|
+
var TOP_KEYS = ["title", "subtitle", "theme", "layout", "blocks", "records", "steps", "chat", "data", "shared"];
|
|
895
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
|
+
}
|
|
896
1175
|
const layout = spec.layout || "panel";
|
|
897
1176
|
if (!LAYOUTS.includes(layout)) {
|
|
898
1177
|
throw new Error(
|
|
@@ -931,6 +1210,28 @@ function validate(spec) {
|
|
|
931
1210
|
});
|
|
932
1211
|
return layout;
|
|
933
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
|
+
}
|
|
934
1235
|
if (layout === "records") {
|
|
935
1236
|
const r = spec.records;
|
|
936
1237
|
if (!r || !Array.isArray(r.fields) || r.fields.length === 0) {
|
|
@@ -938,6 +1239,13 @@ function validate(spec) {
|
|
|
938
1239
|
'claude-artifact-framework: layout "records" needs `records: { fields: [...] }` \u2014 the same field declarations the fields block uses.'
|
|
939
1240
|
);
|
|
940
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
|
+
}
|
|
941
1249
|
const seen = /* @__PURE__ */ new Set();
|
|
942
1250
|
for (const f of r.fields) {
|
|
943
1251
|
if (!f || !f.key) throw new Error("claude-artifact-framework: every records field needs a `key`.");
|
|
@@ -945,6 +1253,18 @@ function validate(spec) {
|
|
|
945
1253
|
if (seen.has(f.key)) throw new Error(`claude-artifact-framework: duplicate records field key "${f.key}".`);
|
|
946
1254
|
seen.add(f.key);
|
|
947
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
|
+
}
|
|
948
1268
|
return layout;
|
|
949
1269
|
}
|
|
950
1270
|
const blocks = spec.blocks || [];
|
|
@@ -977,6 +1297,9 @@ function defaultsFrom(spec) {
|
|
|
977
1297
|
if ((spec.layout || "panel") === "records" && !Array.isArray(data.records)) {
|
|
978
1298
|
data.records = [];
|
|
979
1299
|
}
|
|
1300
|
+
if ((spec.layout || "panel") === "chat" && !data.chat) {
|
|
1301
|
+
data.chat = { api: [], log: [] };
|
|
1302
|
+
}
|
|
980
1303
|
for (const block of [...spec.blocks || [], ...spec.steps || []]) {
|
|
981
1304
|
for (const field of block.fields || []) {
|
|
982
1305
|
if (field && field.key !== void 0 && !(field.key in data)) {
|
|
@@ -1030,10 +1353,10 @@ function Panel({ spec, ctx }) {
|
|
|
1030
1353
|
)
|
|
1031
1354
|
);
|
|
1032
1355
|
}
|
|
1033
|
-
var LAYOUT_COMPONENTS = { panel: Panel, records: RecordsLayout, steps: StepsLayout };
|
|
1356
|
+
var LAYOUT_COMPONENTS = { panel: Panel, records: RecordsLayout, steps: StepsLayout, chat: ChatLayout };
|
|
1034
1357
|
function createApp(spec = {}) {
|
|
1035
1358
|
const layout = validate(spec);
|
|
1036
|
-
const state = createAppState(defaultsFrom(spec));
|
|
1359
|
+
const state = createAppState(defaultsFrom(spec), { shared: Boolean(spec.shared) });
|
|
1037
1360
|
const css = themeCss(spec.theme && spec.theme.seed) + BASE_CSS;
|
|
1038
1361
|
return function App() {
|
|
1039
1362
|
if (!hasReact()) {
|
|
@@ -1174,6 +1497,25 @@ var BASE_CSS = `
|
|
|
1174
1497
|
.caf-steps-fill { transition: width 0.25s ease; }
|
|
1175
1498
|
.caf-step-text { margin: 0; color: var(--caf-muted); font-size: 0.92rem; line-height: 1.55; }
|
|
1176
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; }
|
|
1177
1519
|
.caf-empty, .caf-loading { color: var(--caf-muted); font-size: 0.9rem; text-align: center; padding: 1.6rem 1rem; }
|
|
1178
1520
|
.caf-block-error { border-color: var(--caf-danger); gap: 0.4rem; }
|
|
1179
1521
|
.caf-block-error strong { font-size: 0.9rem; color: var(--caf-danger); }
|