claude-artifact-framework 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -9
- package/dist/index.esm.js +315 -254
- package/dist/index.esm.js.map +4 -4
- package/dist/index.global.js +23 -13
- package/dist/index.global.js.map +4 -4
- package/package.json +1 -1
- package/src/app.js +73 -18
- package/src/blocks.js +15 -4
- package/src/chat.js +10 -3
- package/src/records.js +2 -0
- package/src/steps.js +1 -0
package/dist/index.esm.js
CHANGED
|
@@ -372,6 +372,241 @@ function seriesColors(seed, n) {
|
|
|
372
372
|
return Array.from({ length: n }, (_, i) => hsl((h0 + i * 137.5) % 360, s, 52));
|
|
373
373
|
}
|
|
374
374
|
|
|
375
|
+
// src/chat.js
|
|
376
|
+
var CHAT_KEYS = ["system", "greeting", "placeholder", "tools", "model", "maxTokens"];
|
|
377
|
+
var DEFAULT_MODEL = "claude-sonnet-4-6";
|
|
378
|
+
var MAX_ROUNDS = 8;
|
|
379
|
+
async function streamCall({ model, maxTokens, system, messages, onText }) {
|
|
380
|
+
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
381
|
+
method: "POST",
|
|
382
|
+
headers: { "Content-Type": "application/json" },
|
|
383
|
+
body: JSON.stringify({ model, max_tokens: maxTokens, system, messages, stream: true })
|
|
384
|
+
});
|
|
385
|
+
if (!res.ok || !res.body) {
|
|
386
|
+
let detail = "";
|
|
387
|
+
try {
|
|
388
|
+
const b = await res.json();
|
|
389
|
+
detail = b && b.error && b.error.message || JSON.stringify(b);
|
|
390
|
+
} catch {
|
|
391
|
+
detail = await res.text().catch(() => "");
|
|
392
|
+
}
|
|
393
|
+
throw new Error(`HTTP ${res.status}${detail ? ": " + detail : ""}`);
|
|
394
|
+
}
|
|
395
|
+
const reader = res.body.getReader();
|
|
396
|
+
const decoder = new TextDecoder();
|
|
397
|
+
let full = "";
|
|
398
|
+
let buf = "";
|
|
399
|
+
for (; ; ) {
|
|
400
|
+
const { done, value } = await reader.read();
|
|
401
|
+
if (done) break;
|
|
402
|
+
buf += decoder.decode(value, { stream: true });
|
|
403
|
+
const lines = buf.split("\n");
|
|
404
|
+
buf = lines.pop();
|
|
405
|
+
for (const line of lines) {
|
|
406
|
+
if (!line.startsWith("data: ")) continue;
|
|
407
|
+
try {
|
|
408
|
+
const j = JSON.parse(line.slice(6));
|
|
409
|
+
if (j.type === "content_block_delta" && j.delta && j.delta.text) {
|
|
410
|
+
full += j.delta.text;
|
|
411
|
+
if (onText) onText(full);
|
|
412
|
+
}
|
|
413
|
+
} catch {
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return full;
|
|
418
|
+
}
|
|
419
|
+
function parseToolCalls(text) {
|
|
420
|
+
const match = text.match(/```tool_call\s*\n?([\s\S]*?)```/);
|
|
421
|
+
if (!match) return null;
|
|
422
|
+
try {
|
|
423
|
+
const calls = JSON.parse(match[1]);
|
|
424
|
+
return Array.isArray(calls) && calls.length ? calls : null;
|
|
425
|
+
} catch {
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
function stripToolCallBlock(text) {
|
|
430
|
+
return text.replace(/```tool_call\s*\n?[\s\S]*?```/, "").trim();
|
|
431
|
+
}
|
|
432
|
+
function toolProtocol(tools) {
|
|
433
|
+
const list = Object.entries(tools).map(([name, t]) => `- ${name}: ${t.description}
|
|
434
|
+
Input: ${JSON.stringify(t.input || {})}`).join("\n");
|
|
435
|
+
return `
|
|
436
|
+
|
|
437
|
+
You have tools available:
|
|
438
|
+
${list}
|
|
439
|
+
|
|
440
|
+
This environment does not support native function calling. To use a tool, reply ONLY with a \`\`\`tool_call code block containing a JSON array, with no other text in that turn. Example:
|
|
441
|
+
|
|
442
|
+
\`\`\`tool_call
|
|
443
|
+
[{ "name": "tool_name", "input": { "param": "value" } }]
|
|
444
|
+
\`\`\`
|
|
445
|
+
|
|
446
|
+
You may include several calls in one array. NEVER invent a tool's result \u2014 emit the pure tool_call block and wait for the real result, which arrives in the next message tagged [TOOL_RESULT]. When you have everything you need, answer in natural language with no tool_call block.`;
|
|
447
|
+
}
|
|
448
|
+
function runTools(calls, tools, ctx) {
|
|
449
|
+
return calls.map((call) => {
|
|
450
|
+
const tool = tools[call.name];
|
|
451
|
+
if (!tool) {
|
|
452
|
+
return { name: call.name, error: `unknown tool "${call.name}". Valid tools: ${Object.keys(tools).join(", ")}` };
|
|
453
|
+
}
|
|
454
|
+
try {
|
|
455
|
+
const result = tool.run(call.input || {}, ctx);
|
|
456
|
+
return { name: call.name, result: result === void 0 ? { ok: true } : result };
|
|
457
|
+
} catch (e) {
|
|
458
|
+
return { name: call.name, error: String(e && e.message || e) };
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
var escHtml = (t) => String(t).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
463
|
+
function renderMarkdown(text) {
|
|
464
|
+
const codeBlocks = [];
|
|
465
|
+
let t = String(text).replace(/```[a-z]*\n?([\s\S]*?)```/g, (m, code) => {
|
|
466
|
+
codeBlocks.push(code.replace(/\n$/, ""));
|
|
467
|
+
return "\0" + (codeBlocks.length - 1) + "\0";
|
|
468
|
+
});
|
|
469
|
+
t = escHtml(t);
|
|
470
|
+
t = t.replace(/`([^`]+)`/g, "<code>$1</code>");
|
|
471
|
+
t = t.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
|
472
|
+
t = t.replace(/(^|[^*])\*([^*\n]+)\*/g, "$1<em>$2</em>");
|
|
473
|
+
t = t.replace(/^#{1,3} (.+)$/gm, '<strong class="caf-md-h">$1</strong>');
|
|
474
|
+
t = t.replace(/^[-•*] (.+)$/gm, '<span class="caf-md-li">$1</span>');
|
|
475
|
+
t = t.replace(/^\d+\. (.+)$/gm, '<span class="caf-md-li caf-md-num">$1</span>');
|
|
476
|
+
t = t.replace(/\n/g, "<br>");
|
|
477
|
+
t = t.replace(/\u0000(\d+)\u0000/g, (m, i) => '<pre class="caf-code">' + escHtml(codeBlocks[Number(i)]) + "</pre>");
|
|
478
|
+
return t;
|
|
479
|
+
}
|
|
480
|
+
function Bubble({ m }) {
|
|
481
|
+
if (m.role === "tools") {
|
|
482
|
+
return createElement("div", { className: "caf-msg-tools" }, `used: ${m.tools.join(", ")}`);
|
|
483
|
+
}
|
|
484
|
+
if (m.role === "error") {
|
|
485
|
+
return createElement(
|
|
486
|
+
"div",
|
|
487
|
+
{ className: "caf-msg caf-msg-error" },
|
|
488
|
+
createElement("strong", null, "The call failed"),
|
|
489
|
+
createElement("span", null, m.content),
|
|
490
|
+
createElement("span", { className: "caf-hint" }, "Your message is kept \u2014 send again to retry.")
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
if (m.role === "user") return createElement("div", { className: "caf-msg caf-msg-user" }, m.content);
|
|
494
|
+
return createElement("div", {
|
|
495
|
+
className: "caf-msg caf-msg-assistant",
|
|
496
|
+
dangerouslySetInnerHTML: { __html: renderMarkdown(m.content) }
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
function ChatPanel({ cfg, ctx }) {
|
|
500
|
+
const c = cfg;
|
|
501
|
+
const [draft, setDraft] = useState("");
|
|
502
|
+
const [busy, setBusy] = useState(false);
|
|
503
|
+
const [live, setLive] = useState(null);
|
|
504
|
+
const endRef = useRef(null);
|
|
505
|
+
const chat = ctx.data.chat || { api: [], log: [] };
|
|
506
|
+
const log = chat.log || [];
|
|
507
|
+
useEffect(() => {
|
|
508
|
+
if (endRef.current) endRef.current.scrollIntoView({ block: "end" });
|
|
509
|
+
}, [log.length, live]);
|
|
510
|
+
async function send() {
|
|
511
|
+
const text = draft.trim();
|
|
512
|
+
if (!text || busy) return;
|
|
513
|
+
setDraft("");
|
|
514
|
+
setBusy(true);
|
|
515
|
+
let api = [...chat.api || [], { role: "user", content: text }];
|
|
516
|
+
let logNow = [...log, { role: "user", content: text }];
|
|
517
|
+
const commit = () => ctx.update((d) => {
|
|
518
|
+
d.chat = { api, log: logNow };
|
|
519
|
+
});
|
|
520
|
+
commit();
|
|
521
|
+
try {
|
|
522
|
+
const system = (typeof c.system === "function" ? c.system(ctx.data) : c.system || "") + (c.tools ? toolProtocol(c.tools) : "");
|
|
523
|
+
for (let round = 0; round < MAX_ROUNDS; round++) {
|
|
524
|
+
const raw = await streamCall({
|
|
525
|
+
model: c.model || DEFAULT_MODEL,
|
|
526
|
+
maxTokens: c.maxTokens || 2e3,
|
|
527
|
+
system,
|
|
528
|
+
messages: api,
|
|
529
|
+
onText: (t) => setLive(stripToolCallBlock(t))
|
|
530
|
+
});
|
|
531
|
+
api = [...api, { role: "assistant", content: raw }];
|
|
532
|
+
const shown = stripToolCallBlock(raw);
|
|
533
|
+
const calls = c.tools ? parseToolCalls(raw) : null;
|
|
534
|
+
if (!calls) {
|
|
535
|
+
logNow = [...logNow, { role: "assistant", content: shown || raw }];
|
|
536
|
+
commit();
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
const results = runTools(calls, c.tools, ctx);
|
|
540
|
+
if (shown) logNow = [...logNow, { role: "assistant", content: shown }];
|
|
541
|
+
logNow = [...logNow, { role: "tools", tools: calls.map((x) => x.name) }];
|
|
542
|
+
api = [
|
|
543
|
+
...api,
|
|
544
|
+
{
|
|
545
|
+
role: "user",
|
|
546
|
+
content: `[TOOL_RESULT]
|
|
547
|
+
${JSON.stringify(results)}
|
|
548
|
+
[/TOOL_RESULT]
|
|
549
|
+
|
|
550
|
+
Continue with this information. If you have everything you need, answer in natural language without further tool_call.`
|
|
551
|
+
}
|
|
552
|
+
];
|
|
553
|
+
commit();
|
|
554
|
+
setLive(null);
|
|
555
|
+
}
|
|
556
|
+
} catch (e) {
|
|
557
|
+
logNow = [...logNow, { role: "error", content: String(e && e.message || e) }];
|
|
558
|
+
commit();
|
|
559
|
+
} finally {
|
|
560
|
+
setLive(null);
|
|
561
|
+
setBusy(false);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return createElement(
|
|
565
|
+
"div",
|
|
566
|
+
{ className: "caf-chat-panel" },
|
|
567
|
+
createElement(
|
|
568
|
+
"div",
|
|
569
|
+
{ className: "caf-block caf-chat-log" },
|
|
570
|
+
c.greeting ? createElement("div", { className: "caf-msg caf-msg-assistant", dangerouslySetInnerHTML: { __html: renderMarkdown(c.greeting) } }) : null,
|
|
571
|
+
log.map((m, i) => createElement(Bubble, { key: i, m })),
|
|
572
|
+
live !== null ? createElement("div", { className: "caf-msg caf-msg-assistant caf-msg-live", dangerouslySetInnerHTML: { __html: renderMarkdown(live || "\u2026") } }) : null,
|
|
573
|
+
busy && live === null ? createElement("div", { className: "caf-msg-tools" }, "thinking\u2026") : null,
|
|
574
|
+
createElement("div", { ref: endRef })
|
|
575
|
+
),
|
|
576
|
+
// Deliberately NOT a <form>: the artifact iframe's sandbox lacks
|
|
577
|
+
// allow-forms, so native form submission is blocked there — and on mobile
|
|
578
|
+
// the keyboard's send key triggers exactly that implicit submission. A
|
|
579
|
+
// plain div with onClick + Enter via onKeyDown never touches the native
|
|
580
|
+
// form machinery, so it can't be sandboxed away.
|
|
581
|
+
createElement(
|
|
582
|
+
"div",
|
|
583
|
+
{ className: "caf-chat-input" },
|
|
584
|
+
createElement("input", {
|
|
585
|
+
value: draft,
|
|
586
|
+
placeholder: c.placeholder || "Message\u2026",
|
|
587
|
+
disabled: busy,
|
|
588
|
+
enterKeyHint: "send",
|
|
589
|
+
onChange: (e) => setDraft(e.target.value),
|
|
590
|
+
onKeyDown: (e) => {
|
|
591
|
+
if (e.key === "Enter") {
|
|
592
|
+
e.preventDefault();
|
|
593
|
+
send();
|
|
594
|
+
}
|
|
595
|
+
},
|
|
596
|
+
"aria-label": "Message"
|
|
597
|
+
}),
|
|
598
|
+
createElement(
|
|
599
|
+
"button",
|
|
600
|
+
{ type: "button", className: "caf-btn caf-btn-primary", disabled: busy || !draft.trim(), onClick: send },
|
|
601
|
+
"Send"
|
|
602
|
+
)
|
|
603
|
+
)
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
function ChatLayout({ spec, ctx }) {
|
|
607
|
+
return createElement("div", { className: "caf-panel caf-panel-single" }, createElement(ChatPanel, { cfg: spec.chat, ctx }));
|
|
608
|
+
}
|
|
609
|
+
|
|
375
610
|
// src/blocks.js
|
|
376
611
|
var FIELD_TYPES = ["text", "textarea", "number", "money", "percent", "check", "date", "select"];
|
|
377
612
|
function formatValue(value, format) {
|
|
@@ -403,7 +638,7 @@ function validateField(field, value) {
|
|
|
403
638
|
}
|
|
404
639
|
return null;
|
|
405
640
|
}
|
|
406
|
-
function Field({ field, value, onChange }) {
|
|
641
|
+
function Field({ field, value, onChange, data }) {
|
|
407
642
|
const error = validateField(field, value);
|
|
408
643
|
const id = `caf-f-${field.key}`;
|
|
409
644
|
const numeric = field.type === "number" || field.type === "money" || field.type === "percent";
|
|
@@ -430,11 +665,12 @@ function Field({ field, value, onChange }) {
|
|
|
430
665
|
if (field.type === "textarea") {
|
|
431
666
|
control = createElement("textarea", { ...common, rows: field.rows || 3 });
|
|
432
667
|
} else if (field.type === "select") {
|
|
668
|
+
const options = typeof field.options === "function" ? field.options(data) || [] : field.options || [];
|
|
433
669
|
control = createElement(
|
|
434
670
|
"select",
|
|
435
671
|
common,
|
|
436
672
|
createElement("option", { value: "" }, field.placeholder || "Select\u2026"),
|
|
437
|
-
|
|
673
|
+
options.map((opt) => {
|
|
438
674
|
const val = typeof opt === "string" ? opt : opt.value;
|
|
439
675
|
const label = typeof opt === "string" ? opt : opt.label;
|
|
440
676
|
return createElement("option", { key: val, value: val }, label);
|
|
@@ -474,6 +710,7 @@ function FieldsBlock({ spec, ctx }) {
|
|
|
474
710
|
(field) => createElement(Field, {
|
|
475
711
|
key: field.key,
|
|
476
712
|
field,
|
|
713
|
+
data: ctx.data,
|
|
477
714
|
value: ctx.data[field.key],
|
|
478
715
|
onChange: (v) => ctx.update((d) => {
|
|
479
716
|
d[field.key] = v;
|
|
@@ -517,13 +754,15 @@ function TextBlock({ spec, ctx }) {
|
|
|
517
754
|
}
|
|
518
755
|
function ListBlock({ spec, ctx }) {
|
|
519
756
|
const rows = (typeof spec.list === "function" ? spec.list(ctx.data) : spec.list) || [];
|
|
757
|
+
const heading = typeof spec.title === "string" ? spec.title : null;
|
|
758
|
+
const rowTitle2 = typeof spec.title === "function" ? spec.title : (r) => r.title ?? r.name ?? r.label ?? r.text ?? String(r);
|
|
520
759
|
if (!rows.length) {
|
|
521
760
|
return createElement("div", { className: "caf-block caf-empty" }, spec.empty || "Nothing here yet.");
|
|
522
761
|
}
|
|
523
|
-
const title = spec.title || ((r) => r.title ?? r.name ?? r.label ?? String(r));
|
|
524
762
|
return createElement(
|
|
525
763
|
"div",
|
|
526
764
|
{ className: "caf-block caf-list" },
|
|
765
|
+
heading ? createElement("h2", { className: "caf-block-title" }, heading) : null,
|
|
527
766
|
rows.map(
|
|
528
767
|
(row, i) => createElement(
|
|
529
768
|
"button",
|
|
@@ -533,7 +772,7 @@ function ListBlock({ spec, ctx }) {
|
|
|
533
772
|
type: "button",
|
|
534
773
|
onClick: spec.onSelect ? () => spec.onSelect(row, ctx) : void 0
|
|
535
774
|
},
|
|
536
|
-
createElement("span", { className: "caf-row-title" },
|
|
775
|
+
createElement("span", { className: "caf-row-title" }, rowTitle2(row)),
|
|
537
776
|
spec.subtitle ? createElement("span", { className: "caf-row-sub" }, spec.subtitle(row)) : null
|
|
538
777
|
)
|
|
539
778
|
)
|
|
@@ -734,6 +973,7 @@ var BLOCKS = {
|
|
|
734
973
|
chart: ChartBlock,
|
|
735
974
|
timer: TimerBlock,
|
|
736
975
|
actions: ActionsBlock,
|
|
976
|
+
chat: ({ spec, ctx }) => createElement(ChatPanel, { cfg: spec.chat, ctx }),
|
|
737
977
|
custom: CustomBlock
|
|
738
978
|
};
|
|
739
979
|
var BLOCK_NAMES = Object.keys(BLOCKS);
|
|
@@ -890,6 +1130,7 @@ function DetailScreen({ spec, ctx, record }) {
|
|
|
890
1130
|
(field) => createElement(Field, {
|
|
891
1131
|
key: field.key,
|
|
892
1132
|
field,
|
|
1133
|
+
data: ctx.data,
|
|
893
1134
|
value: record[field.key],
|
|
894
1135
|
onChange: (v) => ctx.update((d) => {
|
|
895
1136
|
const rec = d.records.find((r) => r.id === record.id);
|
|
@@ -950,6 +1191,7 @@ function ItemsEditor({ spec, ctx, record }) {
|
|
|
950
1191
|
(field) => createElement(Field, {
|
|
951
1192
|
key: field.key,
|
|
952
1193
|
field,
|
|
1194
|
+
data: ctx.data,
|
|
953
1195
|
value: item[field.key],
|
|
954
1196
|
onChange: (v) => mutate((rec) => {
|
|
955
1197
|
const it = rec.items.find((x) => x.id === item.id);
|
|
@@ -1047,6 +1289,7 @@ function StepsLayout({ spec, ctx }) {
|
|
|
1047
1289
|
(field) => createElement(Field, {
|
|
1048
1290
|
key: field.key,
|
|
1049
1291
|
field,
|
|
1292
|
+
data: ctx.data,
|
|
1050
1293
|
value: ctx.data[field.key],
|
|
1051
1294
|
onChange: (v) => ctx.update((d) => {
|
|
1052
1295
|
d[field.key] = v;
|
|
@@ -1072,242 +1315,10 @@ function StepsLayout({ spec, ctx }) {
|
|
|
1072
1315
|
);
|
|
1073
1316
|
}
|
|
1074
1317
|
|
|
1075
|
-
// src/chat.js
|
|
1076
|
-
var CHAT_KEYS = ["system", "greeting", "placeholder", "tools", "model", "maxTokens"];
|
|
1077
|
-
var DEFAULT_MODEL = "claude-sonnet-4-6";
|
|
1078
|
-
var MAX_ROUNDS = 8;
|
|
1079
|
-
async function streamCall({ model, maxTokens, system, messages, onText }) {
|
|
1080
|
-
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
1081
|
-
method: "POST",
|
|
1082
|
-
headers: { "Content-Type": "application/json" },
|
|
1083
|
-
body: JSON.stringify({ model, max_tokens: maxTokens, system, messages, stream: true })
|
|
1084
|
-
});
|
|
1085
|
-
if (!res.ok || !res.body) {
|
|
1086
|
-
let detail = "";
|
|
1087
|
-
try {
|
|
1088
|
-
const b = await res.json();
|
|
1089
|
-
detail = b && b.error && b.error.message || JSON.stringify(b);
|
|
1090
|
-
} catch {
|
|
1091
|
-
detail = await res.text().catch(() => "");
|
|
1092
|
-
}
|
|
1093
|
-
throw new Error(`HTTP ${res.status}${detail ? ": " + detail : ""}`);
|
|
1094
|
-
}
|
|
1095
|
-
const reader = res.body.getReader();
|
|
1096
|
-
const decoder = new TextDecoder();
|
|
1097
|
-
let full = "";
|
|
1098
|
-
let buf = "";
|
|
1099
|
-
for (; ; ) {
|
|
1100
|
-
const { done, value } = await reader.read();
|
|
1101
|
-
if (done) break;
|
|
1102
|
-
buf += decoder.decode(value, { stream: true });
|
|
1103
|
-
const lines = buf.split("\n");
|
|
1104
|
-
buf = lines.pop();
|
|
1105
|
-
for (const line of lines) {
|
|
1106
|
-
if (!line.startsWith("data: ")) continue;
|
|
1107
|
-
try {
|
|
1108
|
-
const j = JSON.parse(line.slice(6));
|
|
1109
|
-
if (j.type === "content_block_delta" && j.delta && j.delta.text) {
|
|
1110
|
-
full += j.delta.text;
|
|
1111
|
-
if (onText) onText(full);
|
|
1112
|
-
}
|
|
1113
|
-
} catch {
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
|
-
}
|
|
1117
|
-
return full;
|
|
1118
|
-
}
|
|
1119
|
-
function parseToolCalls(text) {
|
|
1120
|
-
const match = text.match(/```tool_call\s*\n?([\s\S]*?)```/);
|
|
1121
|
-
if (!match) return null;
|
|
1122
|
-
try {
|
|
1123
|
-
const calls = JSON.parse(match[1]);
|
|
1124
|
-
return Array.isArray(calls) && calls.length ? calls : null;
|
|
1125
|
-
} catch {
|
|
1126
|
-
return null;
|
|
1127
|
-
}
|
|
1128
|
-
}
|
|
1129
|
-
function stripToolCallBlock(text) {
|
|
1130
|
-
return text.replace(/```tool_call\s*\n?[\s\S]*?```/, "").trim();
|
|
1131
|
-
}
|
|
1132
|
-
function toolProtocol(tools) {
|
|
1133
|
-
const list = Object.entries(tools).map(([name, t]) => `- ${name}: ${t.description}
|
|
1134
|
-
Input: ${JSON.stringify(t.input || {})}`).join("\n");
|
|
1135
|
-
return `
|
|
1136
|
-
|
|
1137
|
-
You have tools available:
|
|
1138
|
-
${list}
|
|
1139
|
-
|
|
1140
|
-
This environment does not support native function calling. To use a tool, reply ONLY with a \`\`\`tool_call code block containing a JSON array, with no other text in that turn. Example:
|
|
1141
|
-
|
|
1142
|
-
\`\`\`tool_call
|
|
1143
|
-
[{ "name": "tool_name", "input": { "param": "value" } }]
|
|
1144
|
-
\`\`\`
|
|
1145
|
-
|
|
1146
|
-
You may include several calls in one array. NEVER invent a tool's result \u2014 emit the pure tool_call block and wait for the real result, which arrives in the next message tagged [TOOL_RESULT]. When you have everything you need, answer in natural language with no tool_call block.`;
|
|
1147
|
-
}
|
|
1148
|
-
function runTools(calls, tools, ctx) {
|
|
1149
|
-
return calls.map((call) => {
|
|
1150
|
-
const tool = tools[call.name];
|
|
1151
|
-
if (!tool) {
|
|
1152
|
-
return { name: call.name, error: `unknown tool "${call.name}". Valid tools: ${Object.keys(tools).join(", ")}` };
|
|
1153
|
-
}
|
|
1154
|
-
try {
|
|
1155
|
-
const result = tool.run(call.input || {}, ctx);
|
|
1156
|
-
return { name: call.name, result: result === void 0 ? { ok: true } : result };
|
|
1157
|
-
} catch (e) {
|
|
1158
|
-
return { name: call.name, error: String(e && e.message || e) };
|
|
1159
|
-
}
|
|
1160
|
-
});
|
|
1161
|
-
}
|
|
1162
|
-
var escHtml = (t) => String(t).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
1163
|
-
function renderMarkdown(text) {
|
|
1164
|
-
const codeBlocks = [];
|
|
1165
|
-
let t = String(text).replace(/```[a-z]*\n?([\s\S]*?)```/g, (m, code) => {
|
|
1166
|
-
codeBlocks.push(code.replace(/\n$/, ""));
|
|
1167
|
-
return "\0" + (codeBlocks.length - 1) + "\0";
|
|
1168
|
-
});
|
|
1169
|
-
t = escHtml(t);
|
|
1170
|
-
t = t.replace(/`([^`]+)`/g, "<code>$1</code>");
|
|
1171
|
-
t = t.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
|
1172
|
-
t = t.replace(/(^|[^*])\*([^*\n]+)\*/g, "$1<em>$2</em>");
|
|
1173
|
-
t = t.replace(/^#{1,3} (.+)$/gm, '<strong class="caf-md-h">$1</strong>');
|
|
1174
|
-
t = t.replace(/^[-•*] (.+)$/gm, '<span class="caf-md-li">$1</span>');
|
|
1175
|
-
t = t.replace(/^\d+\. (.+)$/gm, '<span class="caf-md-li caf-md-num">$1</span>');
|
|
1176
|
-
t = t.replace(/\n/g, "<br>");
|
|
1177
|
-
t = t.replace(/\u0000(\d+)\u0000/g, (m, i) => '<pre class="caf-code">' + escHtml(codeBlocks[Number(i)]) + "</pre>");
|
|
1178
|
-
return t;
|
|
1179
|
-
}
|
|
1180
|
-
function Bubble({ m }) {
|
|
1181
|
-
if (m.role === "tools") {
|
|
1182
|
-
return createElement("div", { className: "caf-msg-tools" }, `used: ${m.tools.join(", ")}`);
|
|
1183
|
-
}
|
|
1184
|
-
if (m.role === "error") {
|
|
1185
|
-
return createElement(
|
|
1186
|
-
"div",
|
|
1187
|
-
{ className: "caf-msg caf-msg-error" },
|
|
1188
|
-
createElement("strong", null, "The call failed"),
|
|
1189
|
-
createElement("span", null, m.content),
|
|
1190
|
-
createElement("span", { className: "caf-hint" }, "Your message is kept \u2014 send again to retry.")
|
|
1191
|
-
);
|
|
1192
|
-
}
|
|
1193
|
-
if (m.role === "user") return createElement("div", { className: "caf-msg caf-msg-user" }, m.content);
|
|
1194
|
-
return createElement("div", {
|
|
1195
|
-
className: "caf-msg caf-msg-assistant",
|
|
1196
|
-
dangerouslySetInnerHTML: { __html: renderMarkdown(m.content) }
|
|
1197
|
-
});
|
|
1198
|
-
}
|
|
1199
|
-
function ChatLayout({ spec, ctx }) {
|
|
1200
|
-
const c = spec.chat;
|
|
1201
|
-
const [draft, setDraft] = useState("");
|
|
1202
|
-
const [busy, setBusy] = useState(false);
|
|
1203
|
-
const [live, setLive] = useState(null);
|
|
1204
|
-
const endRef = useRef(null);
|
|
1205
|
-
const chat = ctx.data.chat || { api: [], log: [] };
|
|
1206
|
-
const log = chat.log || [];
|
|
1207
|
-
useEffect(() => {
|
|
1208
|
-
if (endRef.current) endRef.current.scrollIntoView({ block: "end" });
|
|
1209
|
-
}, [log.length, live]);
|
|
1210
|
-
async function send() {
|
|
1211
|
-
const text = draft.trim();
|
|
1212
|
-
if (!text || busy) return;
|
|
1213
|
-
setDraft("");
|
|
1214
|
-
setBusy(true);
|
|
1215
|
-
let api = [...chat.api || [], { role: "user", content: text }];
|
|
1216
|
-
let logNow = [...log, { role: "user", content: text }];
|
|
1217
|
-
const commit = () => ctx.update((d) => {
|
|
1218
|
-
d.chat = { api, log: logNow };
|
|
1219
|
-
});
|
|
1220
|
-
commit();
|
|
1221
|
-
try {
|
|
1222
|
-
const system = (typeof c.system === "function" ? c.system(ctx.data) : c.system || "") + (c.tools ? toolProtocol(c.tools) : "");
|
|
1223
|
-
for (let round = 0; round < MAX_ROUNDS; round++) {
|
|
1224
|
-
const raw = await streamCall({
|
|
1225
|
-
model: c.model || DEFAULT_MODEL,
|
|
1226
|
-
maxTokens: c.maxTokens || 2e3,
|
|
1227
|
-
system,
|
|
1228
|
-
messages: api,
|
|
1229
|
-
onText: (t) => setLive(stripToolCallBlock(t))
|
|
1230
|
-
});
|
|
1231
|
-
api = [...api, { role: "assistant", content: raw }];
|
|
1232
|
-
const shown = stripToolCallBlock(raw);
|
|
1233
|
-
const calls = c.tools ? parseToolCalls(raw) : null;
|
|
1234
|
-
if (!calls) {
|
|
1235
|
-
logNow = [...logNow, { role: "assistant", content: shown || raw }];
|
|
1236
|
-
commit();
|
|
1237
|
-
break;
|
|
1238
|
-
}
|
|
1239
|
-
const results = runTools(calls, c.tools, ctx);
|
|
1240
|
-
if (shown) logNow = [...logNow, { role: "assistant", content: shown }];
|
|
1241
|
-
logNow = [...logNow, { role: "tools", tools: calls.map((x) => x.name) }];
|
|
1242
|
-
api = [
|
|
1243
|
-
...api,
|
|
1244
|
-
{
|
|
1245
|
-
role: "user",
|
|
1246
|
-
content: `[TOOL_RESULT]
|
|
1247
|
-
${JSON.stringify(results)}
|
|
1248
|
-
[/TOOL_RESULT]
|
|
1249
|
-
|
|
1250
|
-
Continue with this information. If you have everything you need, answer in natural language without further tool_call.`
|
|
1251
|
-
}
|
|
1252
|
-
];
|
|
1253
|
-
commit();
|
|
1254
|
-
setLive(null);
|
|
1255
|
-
}
|
|
1256
|
-
} catch (e) {
|
|
1257
|
-
logNow = [...logNow, { role: "error", content: String(e && e.message || e) }];
|
|
1258
|
-
commit();
|
|
1259
|
-
} finally {
|
|
1260
|
-
setLive(null);
|
|
1261
|
-
setBusy(false);
|
|
1262
|
-
}
|
|
1263
|
-
}
|
|
1264
|
-
return createElement(
|
|
1265
|
-
"div",
|
|
1266
|
-
{ className: "caf-panel caf-panel-single" },
|
|
1267
|
-
createElement(
|
|
1268
|
-
"div",
|
|
1269
|
-
{ className: "caf-block caf-chat-log" },
|
|
1270
|
-
c.greeting ? createElement("div", { className: "caf-msg caf-msg-assistant", dangerouslySetInnerHTML: { __html: renderMarkdown(c.greeting) } }) : null,
|
|
1271
|
-
log.map((m, i) => createElement(Bubble, { key: i, m })),
|
|
1272
|
-
live !== null ? createElement("div", { className: "caf-msg caf-msg-assistant caf-msg-live", dangerouslySetInnerHTML: { __html: renderMarkdown(live || "\u2026") } }) : null,
|
|
1273
|
-
busy && live === null ? createElement("div", { className: "caf-msg-tools" }, "thinking\u2026") : null,
|
|
1274
|
-
createElement("div", { ref: endRef })
|
|
1275
|
-
),
|
|
1276
|
-
// Deliberately NOT a <form>: the artifact iframe's sandbox lacks
|
|
1277
|
-
// allow-forms, so native form submission is blocked there — and on mobile
|
|
1278
|
-
// the keyboard's send key triggers exactly that implicit submission. A
|
|
1279
|
-
// plain div with onClick + Enter via onKeyDown never touches the native
|
|
1280
|
-
// form machinery, so it can't be sandboxed away.
|
|
1281
|
-
createElement(
|
|
1282
|
-
"div",
|
|
1283
|
-
{ className: "caf-chat-input" },
|
|
1284
|
-
createElement("input", {
|
|
1285
|
-
value: draft,
|
|
1286
|
-
placeholder: c.placeholder || "Message\u2026",
|
|
1287
|
-
disabled: busy,
|
|
1288
|
-
enterKeyHint: "send",
|
|
1289
|
-
onChange: (e) => setDraft(e.target.value),
|
|
1290
|
-
onKeyDown: (e) => {
|
|
1291
|
-
if (e.key === "Enter") {
|
|
1292
|
-
e.preventDefault();
|
|
1293
|
-
send();
|
|
1294
|
-
}
|
|
1295
|
-
},
|
|
1296
|
-
"aria-label": "Message"
|
|
1297
|
-
}),
|
|
1298
|
-
createElement(
|
|
1299
|
-
"button",
|
|
1300
|
-
{ type: "button", className: "caf-btn caf-btn-primary", disabled: busy || !draft.trim(), onClick: send },
|
|
1301
|
-
"Send"
|
|
1302
|
-
)
|
|
1303
|
-
)
|
|
1304
|
-
);
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
1318
|
// src/app.js
|
|
1308
|
-
var LAYOUTS = ["panel", "records", "steps", "chat"];
|
|
1319
|
+
var LAYOUTS = ["panel", "records", "steps", "chat", "workspace"];
|
|
1309
1320
|
var RESERVED = ["title", "empty", "wide", "onSelect", "subtitle"];
|
|
1310
|
-
var TOP_KEYS = ["title", "subtitle", "theme", "layout", "blocks", "records", "steps", "chat", "data", "shared"];
|
|
1321
|
+
var TOP_KEYS = ["title", "subtitle", "theme", "layout", "blocks", "records", "steps", "chat", "data", "shared", "main", "sidebar"];
|
|
1311
1322
|
function checkFields(fields, where) {
|
|
1312
1323
|
for (const f of fields || []) {
|
|
1313
1324
|
if (!f || !f.key) throw new Error(`claude-artifact-framework: every field in ${where} needs a \`key\`.`);
|
|
@@ -1316,6 +1327,31 @@ function checkFields(fields, where) {
|
|
|
1316
1327
|
`claude-artifact-framework: field "${f.key}" in ${where} has unknown type "${f.type}". Valid types: ${FIELD_TYPES.join(", ")}.`
|
|
1317
1328
|
);
|
|
1318
1329
|
}
|
|
1330
|
+
if (f.type === "select" && f.options !== void 0 && !Array.isArray(f.options) && typeof f.options !== "function") {
|
|
1331
|
+
throw new Error(
|
|
1332
|
+
`claude-artifact-framework: field "${f.key}" in ${where}: \`options\` must be an array or a function of data returning one.`
|
|
1333
|
+
);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
function checkChatConfig(c, where) {
|
|
1338
|
+
if (!c || typeof c.system !== "string" && typeof c.system !== "function") {
|
|
1339
|
+
throw new Error(
|
|
1340
|
+
`claude-artifact-framework: ${where} needs \`{ system: "..." }\` (a string or a function of data).`
|
|
1341
|
+
);
|
|
1342
|
+
}
|
|
1343
|
+
const unknown = Object.keys(c).filter((k) => !CHAT_KEYS.includes(k));
|
|
1344
|
+
if (unknown.length) {
|
|
1345
|
+
throw new Error(
|
|
1346
|
+
`claude-artifact-framework: ${where} has unknown keys (${unknown.join(", ")}). Valid keys: ${CHAT_KEYS.join(", ")}.`
|
|
1347
|
+
);
|
|
1348
|
+
}
|
|
1349
|
+
for (const [name, tool] of Object.entries(c.tools || {})) {
|
|
1350
|
+
if (!tool || typeof tool.description !== "string" || typeof tool.run !== "function") {
|
|
1351
|
+
throw new Error(
|
|
1352
|
+
`claude-artifact-framework: chat tool "${name}" needs a \`description\` string and a \`run(input, ctx)\` function.`
|
|
1353
|
+
);
|
|
1354
|
+
}
|
|
1319
1355
|
}
|
|
1320
1356
|
}
|
|
1321
1357
|
var ACTION_KEYS = ["label", "run", "when", "kind"];
|
|
@@ -1359,6 +1395,7 @@ function checkBlocks(blocks) {
|
|
|
1359
1395
|
}
|
|
1360
1396
|
checkFields(block.fields, `blocks[${i}]`);
|
|
1361
1397
|
if (known[0] === "actions") checkActions(block.actions, `blocks[${i}].actions`);
|
|
1398
|
+
if (known[0] === "chat") checkChatConfig(block.chat, `blocks[${i}].chat`);
|
|
1362
1399
|
});
|
|
1363
1400
|
}
|
|
1364
1401
|
function validate(spec) {
|
|
@@ -1407,25 +1444,28 @@ function validate(spec) {
|
|
|
1407
1444
|
return layout;
|
|
1408
1445
|
}
|
|
1409
1446
|
if (layout === "chat") {
|
|
1410
|
-
|
|
1411
|
-
|
|
1447
|
+
checkChatConfig(spec.chat, "chat");
|
|
1448
|
+
return layout;
|
|
1449
|
+
}
|
|
1450
|
+
if (layout === "workspace") {
|
|
1451
|
+
if (!Array.isArray(spec.main) || spec.main.length === 0) {
|
|
1412
1452
|
throw new Error(
|
|
1413
|
-
'claude-artifact-framework: layout "
|
|
1453
|
+
'claude-artifact-framework: layout "workspace" needs `main: [...]` (an array of blocks). `sidebar: [...]` is optional.'
|
|
1414
1454
|
);
|
|
1415
1455
|
}
|
|
1416
|
-
|
|
1417
|
-
if (
|
|
1456
|
+
checkBlocks(spec.main);
|
|
1457
|
+
if (spec.sidebar !== void 0) {
|
|
1458
|
+
if (!Array.isArray(spec.sidebar)) {
|
|
1459
|
+
throw new Error("claude-artifact-framework: workspace `sidebar` must be an array of blocks.");
|
|
1460
|
+
}
|
|
1461
|
+
checkBlocks(spec.sidebar);
|
|
1462
|
+
}
|
|
1463
|
+
const chats = [...spec.main, ...spec.sidebar || []].filter((b) => b && b.chat);
|
|
1464
|
+
if (chats.length > 1) {
|
|
1418
1465
|
throw new Error(
|
|
1419
|
-
|
|
1466
|
+
"claude-artifact-framework: only one chat block per app \u2014 the conversation persists under data.chat and there is exactly one."
|
|
1420
1467
|
);
|
|
1421
1468
|
}
|
|
1422
|
-
for (const [name, tool] of Object.entries(c.tools || {})) {
|
|
1423
|
-
if (!tool || typeof tool.description !== "string" || typeof tool.run !== "function") {
|
|
1424
|
-
throw new Error(
|
|
1425
|
-
`claude-artifact-framework: chat tool "${name}" needs a \`description\` string and a \`run(input, ctx)\` function.`
|
|
1426
|
-
);
|
|
1427
|
-
}
|
|
1428
|
-
}
|
|
1429
1469
|
return layout;
|
|
1430
1470
|
}
|
|
1431
1471
|
if (layout === "records") {
|
|
@@ -1492,10 +1532,12 @@ function defaultsFrom(spec) {
|
|
|
1492
1532
|
if ((spec.layout || "panel") === "records" && !Array.isArray(data.records)) {
|
|
1493
1533
|
data.records = [];
|
|
1494
1534
|
}
|
|
1495
|
-
|
|
1535
|
+
const allBlocks = [...spec.blocks || [], ...spec.main || [], ...spec.sidebar || []];
|
|
1536
|
+
const hasChat = (spec.layout || "panel") === "chat" || allBlocks.some((b) => b && b.chat);
|
|
1537
|
+
if (hasChat && !data.chat) {
|
|
1496
1538
|
data.chat = { api: [], log: [] };
|
|
1497
1539
|
}
|
|
1498
|
-
for (const block of [...
|
|
1540
|
+
for (const block of [...allBlocks, ...spec.steps || []]) {
|
|
1499
1541
|
for (const field of block.fields || []) {
|
|
1500
1542
|
if (field && field.key !== void 0 && !(field.key in data)) {
|
|
1501
1543
|
data[field.key] = field.value !== void 0 ? field.value : field.type === "check" ? false : "";
|
|
@@ -1567,7 +1609,16 @@ function RecordsWithBlocks({ spec, ctx }) {
|
|
|
1567
1609
|
) : null
|
|
1568
1610
|
);
|
|
1569
1611
|
}
|
|
1570
|
-
|
|
1612
|
+
function WorkspaceLayout({ spec, ctx }) {
|
|
1613
|
+
const render = (blocks) => (blocks || []).map((block, i) => createElement("section", { key: i, className: "caf-ws-slot" }, createElement(Block, { block, ctx })));
|
|
1614
|
+
return createElement(
|
|
1615
|
+
"div",
|
|
1616
|
+
{ className: "caf-workspace" },
|
|
1617
|
+
createElement("div", { className: "caf-ws-main" }, render(spec.main)),
|
|
1618
|
+
spec.sidebar && spec.sidebar.length ? createElement("div", { className: "caf-ws-side" }, render(spec.sidebar)) : null
|
|
1619
|
+
);
|
|
1620
|
+
}
|
|
1621
|
+
var LAYOUT_COMPONENTS = { panel: Panel, records: RecordsWithBlocks, steps: StepsLayout, chat: ChatLayout, workspace: WorkspaceLayout };
|
|
1571
1622
|
function createApp(spec = {}) {
|
|
1572
1623
|
const layout = validate(spec);
|
|
1573
1624
|
const state = createAppState(defaultsFrom(spec), { shared: Boolean(spec.shared) });
|
|
@@ -1747,6 +1798,16 @@ var BASE_CSS = `
|
|
|
1747
1798
|
.caf-md-h { display: block; margin-top: 0.3rem; }
|
|
1748
1799
|
.caf-md-li { display: block; padding-left: 1rem; position: relative; }
|
|
1749
1800
|
.caf-md-li::before { content: "\u2022"; position: absolute; left: 0.2rem; opacity: 0.6; }
|
|
1801
|
+
.caf-workspace { max-width: 1200px; margin: 0 auto; display: flex; gap: 1rem; align-items: flex-start; }
|
|
1802
|
+
.caf-ws-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1rem; }
|
|
1803
|
+
.caf-ws-side { width: 340px; flex: none; display: flex; flex-direction: column; gap: 1rem; position: sticky; top: 1rem; }
|
|
1804
|
+
.caf-ws-slot { min-width: 0; }
|
|
1805
|
+
.caf-chat-panel { display: flex; flex-direction: column; min-height: 0; }
|
|
1806
|
+
.caf-ws-side .caf-chat-log { max-height: calc(100vh - 220px); }
|
|
1807
|
+
@media (max-width: 900px) {
|
|
1808
|
+
.caf-workspace { flex-direction: column; align-items: stretch; }
|
|
1809
|
+
.caf-ws-side { width: 100%; position: static; }
|
|
1810
|
+
}
|
|
1750
1811
|
.caf-empty, .caf-loading { color: var(--caf-muted); font-size: 0.9rem; text-align: center; padding: 1.6rem 1rem; }
|
|
1751
1812
|
.caf-block-error { border-color: var(--caf-danger); gap: 0.4rem; }
|
|
1752
1813
|
.caf-block-error strong { font-size: 0.9rem; color: var(--caf-danger); }
|