@camelai/agent-runtime-react 0.0.0 → 0.10.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/LICENSE +21 -0
- package/README.md +51 -1
- package/dist/index.d.ts +100 -0
- package/dist/index.js +187 -0
- package/dist/shadcn.css +39 -0
- package/dist/styles.css +209 -0
- package/dist/ui/composer.d.ts +20 -0
- package/dist/ui/composer.js +49 -0
- package/dist/ui/context.d.ts +87 -0
- package/dist/ui/context.js +46 -0
- package/dist/ui/file.d.ts +9 -0
- package/dist/ui/file.js +25 -0
- package/dist/ui/index.d.ts +50 -0
- package/dist/ui/index.js +68 -0
- package/dist/ui/markdown.d.ts +10 -0
- package/dist/ui/markdown.js +71 -0
- package/dist/ui/messages.d.ts +23 -0
- package/dist/ui/messages.js +137 -0
- package/dist/ui/tool.d.ts +13 -0
- package/dist/ui/tool.js +127 -0
- package/package.json +48 -1
package/dist/ui/tool.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { memo, useId, useState } from "react";
|
|
3
|
+
import { useAgent, useToolRendererFor } from "../index.js";
|
|
4
|
+
import { useUI } from "./context.js";
|
|
5
|
+
/** A tool's name without its source's prefix (`shop__get_price` is `get_price`). */
|
|
6
|
+
const localName = (name) => name.includes("__") ? name.slice(name.lastIndexOf("__") + 2) : name;
|
|
7
|
+
const clip = (text, max = 4000) => text.length > max ? `${text.slice(0, max)}…` : text;
|
|
8
|
+
const pretty = (value) => { try {
|
|
9
|
+
return JSON.stringify(value, null, 2);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return String(value);
|
|
13
|
+
} };
|
|
14
|
+
/** A tool call: its renderer (generative UI) if it has one, what it waits on, or the default card. */
|
|
15
|
+
export const ToolView = memo(function ToolView({ part }) {
|
|
16
|
+
const ui = useUI();
|
|
17
|
+
const chat = useAgent();
|
|
18
|
+
const exact = useToolRendererFor(part.name, ui.tools);
|
|
19
|
+
const local = useToolRendererFor(localName(part.name), ui.tools);
|
|
20
|
+
const Renderer = exact ?? local;
|
|
21
|
+
if (Renderer) {
|
|
22
|
+
const answer = (value) => part.input ? chat.answer(part.input, value) : Promise.reject(new Error(`${part.name} is not waiting for an answer`));
|
|
23
|
+
return _jsx(Renderer, { part: part, name: part.name, args: part.args, state: part.state, result: part.result, progress: part.progress, input: part.input, answer: answer });
|
|
24
|
+
}
|
|
25
|
+
if (part.input)
|
|
26
|
+
return _jsx(ui.components.InputCard, { input: part.input });
|
|
27
|
+
return _jsx(ui.components.ToolFallback, { part: part });
|
|
28
|
+
});
|
|
29
|
+
/** The default card for a tool call: what it was called with, its progress, and its result. */
|
|
30
|
+
export function ToolCard({ part }) {
|
|
31
|
+
const { labels } = useUI();
|
|
32
|
+
const name = localName(part.name);
|
|
33
|
+
const label = part.state === "done" ? labels.toolDone(name) : part.state === "error" ? labels.toolFailed(name) : part.state === "input_required" ? labels.toolWaiting(name) : labels.toolRunning(name);
|
|
34
|
+
const code = typeof part.args.code === "string" ? part.args.code : null;
|
|
35
|
+
const progress = part.progress;
|
|
36
|
+
return (_jsxs("details", { className: "agent-chat__tool", "data-state": part.state, children: [_jsxs("summary", { children: [_jsx("span", { className: "agent-chat__tool-icon", "aria-hidden": "true" }), _jsx("span", { className: "agent-chat__tool-label", children: label }), progress?.message && _jsx("span", { className: "agent-chat__tool-progress-text", children: progress.message })] }), _jsxs("div", { className: "agent-chat__tool-body", children: [progress?.total ? _jsx("progress", { max: progress.total, value: progress.progress ?? 0, "aria-label": label }) : null, _jsxs("div", { className: "agent-chat__tool-section", children: [_jsx("div", { className: "agent-chat__tool-heading", children: labels.arguments }), _jsx("pre", { children: _jsx("code", { children: code ?? pretty(part.args) }) })] }), progress?.text && part.state !== "done" && _jsx("pre", { className: "agent-chat__tool-output", children: _jsx("code", { children: clip(progress.text) }) }), part.result && (_jsxs("div", { className: "agent-chat__tool-section", children: [_jsx("div", { className: "agent-chat__tool-heading", children: labels.result }), _jsx("pre", { children: _jsx("code", { children: clip(part.result.data !== undefined && typeof part.result.data === "object" ? pretty(part.result.data) : part.result.text) }) })] }))] })] }));
|
|
37
|
+
}
|
|
38
|
+
/** What the agent waits on: an approval, questions, a form, or a page to visit. */
|
|
39
|
+
export const InputCard = memo(function InputCard({ input }) {
|
|
40
|
+
const chat = useAgent();
|
|
41
|
+
const { labels } = useUI();
|
|
42
|
+
const busy = input.answering;
|
|
43
|
+
const answer = (value) => { void chat.answer(input, value); };
|
|
44
|
+
const body = (() => {
|
|
45
|
+
switch (input.kind) {
|
|
46
|
+
case "approval": {
|
|
47
|
+
// The call's arguments, or where they are long, the start of their JSON.
|
|
48
|
+
const shown = input.detail.argumentsPreview ?? (input.detail.arguments ? JSON.stringify(input.detail.arguments, null, 2) : undefined);
|
|
49
|
+
return (_jsxs(_Fragment, { children: [_jsx("p", { className: "agent-chat__input-message", children: input.message }), shown && _jsx("pre", { children: _jsx("code", { children: clip(shown, 2000) }) }), _jsxs("div", { className: "agent-chat__input-actions", children: [_jsx("button", { type: "button", className: "agent-chat__button agent-chat__button--primary", disabled: busy, onClick: () => answer(true), children: labels.approve }), _jsx("button", { type: "button", className: "agent-chat__button", disabled: busy, onClick: () => answer(false), children: labels.deny })] })] }));
|
|
50
|
+
}
|
|
51
|
+
case "question": return _jsx(Questions, { input: input, questions: (input.detail.questions ?? []), onAnswer: answer });
|
|
52
|
+
case "form": return _jsx(Form, { input: input, onAnswer: answer });
|
|
53
|
+
case "url": {
|
|
54
|
+
const detail = input.detail;
|
|
55
|
+
return (_jsxs(_Fragment, { children: [_jsx("p", { className: "agent-chat__input-message", children: input.message }), _jsxs("div", { className: "agent-chat__input-actions", children: [detail.url && /^https:\/\//.test(detail.url) && _jsxs("a", { className: "agent-chat__button agent-chat__button--primary", href: detail.url, target: "_blank", rel: "noopener noreferrer", children: [labels.open, detail.origin ? ` ${detail.origin}` : ""] }), _jsx("button", { type: "button", className: "agent-chat__button", disabled: busy, onClick: () => answer(true), children: labels.done }), _jsx("button", { type: "button", className: "agent-chat__button", disabled: busy, onClick: () => answer(false), children: labels.cancel })] })] }));
|
|
56
|
+
}
|
|
57
|
+
default: return _jsx("p", { className: "agent-chat__input-message", children: input.message });
|
|
58
|
+
}
|
|
59
|
+
})();
|
|
60
|
+
return (_jsxs("div", { className: "agent-chat__input", "data-kind": input.kind, "aria-busy": busy || undefined, children: [body, input.error && _jsx("p", { className: "agent-chat__input-error", role: "alert", children: input.error.message })] }));
|
|
61
|
+
});
|
|
62
|
+
function Questions({ input, questions, onAnswer }) {
|
|
63
|
+
const { labels } = useUI();
|
|
64
|
+
const id = useId();
|
|
65
|
+
const [chosen, setChosen] = useState({});
|
|
66
|
+
const [other, setOther] = useState({});
|
|
67
|
+
const valueOf = (question) => {
|
|
68
|
+
const own = other[question.question]?.trim();
|
|
69
|
+
const picked = (chosen[question.question] ?? []).filter(label => label !== OTHER);
|
|
70
|
+
if (question.multiSelect)
|
|
71
|
+
return [...picked, ...own && chosen[question.question]?.includes(OTHER) ? [own] : []];
|
|
72
|
+
if (chosen[question.question]?.[0] === OTHER || !question.options?.length)
|
|
73
|
+
return own || undefined;
|
|
74
|
+
return picked[0];
|
|
75
|
+
};
|
|
76
|
+
const complete = questions.every(question => { const value = valueOf(question); return Array.isArray(value) ? value.length > 0 : !!value; });
|
|
77
|
+
const submit = (event) => {
|
|
78
|
+
event.preventDefault();
|
|
79
|
+
if (!complete)
|
|
80
|
+
return;
|
|
81
|
+
onAnswer(Object.fromEntries(questions.map(question => [question.question, valueOf(question)])));
|
|
82
|
+
};
|
|
83
|
+
const pick = (question, label, checked) => setChosen(current => {
|
|
84
|
+
const now = current[question.question] ?? [];
|
|
85
|
+
return { ...current, [question.question]: question.multiSelect ? checked ? [...now, label] : now.filter(item => item !== label) : [label] };
|
|
86
|
+
});
|
|
87
|
+
return (_jsxs("form", { onSubmit: submit, className: "agent-chat__questions", children: [questions.map((question, index) => {
|
|
88
|
+
const name = `${id}-${index}`;
|
|
89
|
+
const options = question.options ?? [];
|
|
90
|
+
const otherChosen = chosen[question.question]?.includes(OTHER) || !options.length;
|
|
91
|
+
return (_jsxs("fieldset", { disabled: input.answering, children: [_jsx("legend", { children: question.header ? _jsxs(_Fragment, { children: [_jsx("span", { className: "agent-chat__question-header", children: question.header }), " ", question.question] }) : question.question }), options.map(option => (_jsxs("label", { className: "agent-chat__option", children: [_jsx("input", { type: question.multiSelect ? "checkbox" : "radio", name: name, value: option.label, checked: chosen[question.question]?.includes(option.label) ?? false, onChange: event => pick(question, option.label, event.currentTarget.checked) }), _jsxs("span", { children: [option.label, option.description && _jsx("span", { className: "agent-chat__option-description", children: option.description })] })] }, option.label))), question.allowOther !== false && options.length > 0 && (_jsxs("label", { className: "agent-chat__option", children: [_jsx("input", { type: question.multiSelect ? "checkbox" : "radio", name: name, value: OTHER, checked: chosen[question.question]?.includes(OTHER) ?? false, onChange: event => pick(question, OTHER, event.currentTarget.checked) }), _jsx("span", { children: labels.other })] })), otherChosen && (question.allowOther !== false || !options.length) && (_jsx("input", { type: "text", className: "agent-chat__text-input", "aria-label": options.length ? `${labels.other}: ${question.question}` : question.question, value: other[question.question] ?? "", onChange: event => { const value = event.currentTarget.value; setOther(current => ({ ...current, [question.question]: value })); } }))] }, question.question));
|
|
92
|
+
}), _jsx("div", { className: "agent-chat__input-actions", children: _jsx("button", { type: "submit", className: "agent-chat__button agent-chat__button--primary", disabled: !complete || input.answering, children: labels.submit }) })] }));
|
|
93
|
+
}
|
|
94
|
+
const OTHER = "\u0000other";
|
|
95
|
+
function Form({ input, onAnswer }) {
|
|
96
|
+
const { labels } = useUI();
|
|
97
|
+
const id = useId();
|
|
98
|
+
const schema = (input.detail.requestedSchema ?? {});
|
|
99
|
+
const fields = Object.entries(schema.properties ?? {});
|
|
100
|
+
const [values, setValues] = useState(() => Object.fromEntries(fields.flatMap(([name, field]) => field.default !== undefined ? [[name, field.default]] : field.type === "boolean" ? [[name, false]] : [])));
|
|
101
|
+
const set = (name, value) => setValues(current => ({ ...current, [name]: value }));
|
|
102
|
+
const submit = (event) => {
|
|
103
|
+
event.preventDefault();
|
|
104
|
+
const out = {};
|
|
105
|
+
for (const [name, field] of fields) {
|
|
106
|
+
const value = values[name];
|
|
107
|
+
if (value === undefined || value === "")
|
|
108
|
+
continue;
|
|
109
|
+
out[name] = field.type === "number" || field.type === "integer" ? Number(value) : value;
|
|
110
|
+
}
|
|
111
|
+
onAnswer(out);
|
|
112
|
+
};
|
|
113
|
+
return (_jsxs("form", { onSubmit: submit, className: "agent-chat__form", children: [_jsx("p", { className: "agent-chat__input-message", children: input.message }), _jsx("fieldset", { disabled: input.answering, children: fields.map(([name, field]) => {
|
|
114
|
+
const fieldId = `${id}-${name}`;
|
|
115
|
+
const required = schema.required?.includes(name);
|
|
116
|
+
const title = field.title ?? name;
|
|
117
|
+
const described = field.description ? `${fieldId}-description` : undefined;
|
|
118
|
+
let control;
|
|
119
|
+
if (field.type === "boolean")
|
|
120
|
+
control = _jsx("input", { id: fieldId, type: "checkbox", checked: !!values[name], onChange: event => set(name, event.currentTarget.checked), "aria-describedby": described });
|
|
121
|
+
else if (field.enum)
|
|
122
|
+
control = (_jsxs("select", { id: fieldId, required: required, value: String(values[name] ?? ""), onChange: event => set(name, event.currentTarget.value), "aria-describedby": described, children: [_jsx("option", { value: "", disabled: true, children: "\u2014" }), field.enum.map(option => _jsx("option", { value: String(option), children: String(option) }, String(option)))] }));
|
|
123
|
+
else
|
|
124
|
+
control = _jsx("input", { id: fieldId, className: "agent-chat__text-input", required: required, "aria-describedby": described, type: field.type === "number" || field.type === "integer" ? "number" : field.format === "email" ? "email" : field.format === "uri" ? "url" : field.format === "date" ? "date" : "text", step: field.type === "integer" ? 1 : undefined, value: String(values[name] ?? ""), onChange: event => set(name, event.currentTarget.value) });
|
|
125
|
+
return (_jsxs("div", { className: "agent-chat__field", children: [_jsxs("label", { htmlFor: fieldId, children: [title, required ? " *" : ""] }), control, field.description && _jsx("span", { id: described, className: "agent-chat__field-description", children: field.description })] }, name));
|
|
126
|
+
}) }), _jsxs("div", { className: "agent-chat__input-actions", children: [_jsx("button", { type: "submit", className: "agent-chat__button agent-chat__button--primary", disabled: input.answering, children: labels.submit }), _jsx("button", { type: "button", className: "agent-chat__button", disabled: input.answering, onClick: () => onAnswer({ action: "decline" }), children: labels.skip })] })] }));
|
|
127
|
+
}
|
package/package.json
CHANGED
|
@@ -1 +1,48 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
|
+
"name": "@camelai/agent-runtime-react",
|
|
3
|
+
"version": "0.10.0",
|
|
4
|
+
"description": "React hooks and components for chatting with agents on the camelAI agent runtime.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/qaml-ai/agent-runtime.git",
|
|
10
|
+
"directory": "packages/react"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://agents.camelai.dev",
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"registry": "https://registry.npmjs.org",
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"sideEffects": [
|
|
18
|
+
"*.css"
|
|
19
|
+
],
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"default": "./dist/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./ui": {
|
|
26
|
+
"types": "./dist/ui/index.d.ts",
|
|
27
|
+
"default": "./dist/ui/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./styles.css": "./dist/styles.css",
|
|
30
|
+
"./shadcn.css": "./dist/shadcn.css"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"README.md",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json && cp src/ui/styles.css src/ui/shadcn.css dist/",
|
|
39
|
+
"prepublishOnly": "npm run build"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@camelai/agent-runtime": "0.10.0"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"react": "^18.2.0 || ^19.0.0",
|
|
46
|
+
"react-dom": "^18.2.0 || ^19.0.0"
|
|
47
|
+
}
|
|
48
|
+
}
|