@frontal-labs/react 0.0.0-stage → 0.2.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 +190 -2
- package/dist/index.cjs +257 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +233 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/use-agent-run.d.ts +39 -0
- package/dist/use-agent-run.d.ts.map +1 -0
- package/dist/use-chat.d.ts +56 -0
- package/dist/use-chat.d.ts.map +1 -0
- package/dist/use-workflow-run.d.ts +31 -0
- package/dist/use-workflow-run.d.ts.map +1 -0
- package/package.json +84 -4
- package/src/index.ts +27 -0
- package/src/use-agent-run.ts +123 -0
- package/src/use-chat.ts +185 -0
- package/src/use-workflow-run.ts +94 -0
package/README.md
CHANGED
|
@@ -1,3 +1,191 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @frontal-labs/react
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
React hooks for the Frontal SDK. Thin, transport-agnostic, no framework
|
|
4
|
+
lock-in: `useChat` talks to any route that returns
|
|
5
|
+
`toUIMessageStreamResponse(...)`; the other hooks take objects from
|
|
6
|
+
`new Frontal(...)`.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
bun add @frontal-labs/sdk @frontal-labs/react
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Peer dependency: `react >= 18`.
|
|
15
|
+
|
|
16
|
+
## Chat in two files (Next.js App Router)
|
|
17
|
+
|
|
18
|
+
**`app/api/chat/route.ts`** — the server side. `toUIMessageStreamResponse`
|
|
19
|
+
turns `ai.streamText` into a streaming `Response`; model errors arrive as
|
|
20
|
+
frames the UI can render and retry, not as broken connections.
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { Frontal, toUIMessageStreamResponse } from "@frontal-labs/sdk";
|
|
24
|
+
|
|
25
|
+
const f = new Frontal({ apiKey: process.env.FRONTAL_API_KEY! });
|
|
26
|
+
|
|
27
|
+
export async function POST(req: Request) {
|
|
28
|
+
const { messages } = (await req.json()) as {
|
|
29
|
+
messages: Array<{ role: string; content: string }>;
|
|
30
|
+
};
|
|
31
|
+
// Never trust roles from the browser: keep the system prompt server-side.
|
|
32
|
+
const history = messages
|
|
33
|
+
.filter((m) => m.role === "user" || m.role === "assistant")
|
|
34
|
+
.map((m) => ({ role: m.role as "user" | "assistant", content: m.content }));
|
|
35
|
+
|
|
36
|
+
return toUIMessageStreamResponse(
|
|
37
|
+
f.ai.streamText({
|
|
38
|
+
model: "claude-sonnet-5",
|
|
39
|
+
prompt: [{ role: "system", content: "You are a helpful assistant." }, ...history],
|
|
40
|
+
streamRetries: 1,
|
|
41
|
+
})
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**`app/page.tsx`** — the client side.
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
"use client";
|
|
50
|
+
import { useChat } from "@frontal-labs/react";
|
|
51
|
+
import { useState } from "react";
|
|
52
|
+
|
|
53
|
+
export default function Chat() {
|
|
54
|
+
const { messages, send, status, error, retry, stop } = useChat({ api: "/api/chat" });
|
|
55
|
+
const [input, setInput] = useState("");
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<main>
|
|
59
|
+
{messages.map((m) => (
|
|
60
|
+
<div key={m.id}>
|
|
61
|
+
<b>{m.role}:</b>{" "}
|
|
62
|
+
{m.parts.map((p, i) =>
|
|
63
|
+
p.type === "text" ? (
|
|
64
|
+
<span key={i}>{p.text}</span>
|
|
65
|
+
) : p.type === "tool-call" ? (
|
|
66
|
+
<code key={i}>{p.toolName}({JSON.stringify(p.input)})</code>
|
|
67
|
+
) : (
|
|
68
|
+
<em key={i}>{p.error.message}{p.error.retryable ? " (retryable)" : ""}</em>
|
|
69
|
+
)
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
72
|
+
))}
|
|
73
|
+
{error?.retryable && <button onClick={() => retry()}>Retry</button>}
|
|
74
|
+
<form
|
|
75
|
+
onSubmit={(e) => {
|
|
76
|
+
e.preventDefault();
|
|
77
|
+
void send(input);
|
|
78
|
+
setInput("");
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
<input value={input} onChange={(e) => setInput(e.target.value)} />
|
|
82
|
+
<button disabled={status === "streaming"}>Send</button>
|
|
83
|
+
{status === "streaming" && <button type="button" onClick={stop}>Stop</button>}
|
|
84
|
+
</form>
|
|
85
|
+
</main>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
That is the whole chatbot. A copy-paste version lives in
|
|
91
|
+
[`templates/chatbot`](../../templates/chatbot).
|
|
92
|
+
|
|
93
|
+
## Hooks
|
|
94
|
+
|
|
95
|
+
### `useChat(options)`
|
|
96
|
+
|
|
97
|
+
| Option | Purpose |
|
|
98
|
+
| --- | --- |
|
|
99
|
+
| `api` | Route URL (default `/api/chat`) **or** a `(messages, { signal }) => Promise<Response>` transport |
|
|
100
|
+
| `id`, `body`, `headers` | Sent with every request |
|
|
101
|
+
| `fetch` | Custom fetch (tests, auth) |
|
|
102
|
+
| `onError(err)` | Every error frame (`err.code`, `err.retryable`, `err.fix`) |
|
|
103
|
+
| `onFinish(message)` | When the assistant message completes |
|
|
104
|
+
|
|
105
|
+
Returns `{ messages, status, error, send, stop, retry, setMessages }`.
|
|
106
|
+
`messages[i].parts` is a discriminated union of `text`, `tool-call`
|
|
107
|
+
(`state: "pending" | "done" | "error"`) and `error`.
|
|
108
|
+
|
|
109
|
+
### `useAgentRun(agent, options)`
|
|
110
|
+
|
|
111
|
+
Agents are event-driven, not chat: `trigger(event, payload)` starts a run and
|
|
112
|
+
streams it. With a `stateSchema` on the accessor, `state` is typed.
|
|
113
|
+
|
|
114
|
+
```tsx
|
|
115
|
+
import { Frontal } from "@frontal-labs/sdk";
|
|
116
|
+
import { useAgentRun } from "@frontal-labs/react";
|
|
117
|
+
import { z } from "zod";
|
|
118
|
+
|
|
119
|
+
const f = new Frontal({ apiKey: process.env.NEXT_PUBLIC_FRONTAL_API_KEY! });
|
|
120
|
+
const triage = f.agents.use("agt_ticket_triager", {
|
|
121
|
+
stateSchema: z.object({ tier: z.string(), score: z.number() }),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
export function TriagePanel() {
|
|
125
|
+
const { trigger, events, state, status, error } = useAgentRun(triage);
|
|
126
|
+
return (
|
|
127
|
+
<div>
|
|
128
|
+
<button onClick={() => trigger("support.ticket.created", { ticketId: "t_987" })}>
|
|
129
|
+
Run
|
|
130
|
+
</button>
|
|
131
|
+
<p>{status}{state ? ` — tier ${state.tier} (${state.score})` : ""}</p>
|
|
132
|
+
{error && <p>{error.code}: {error.fix ?? error.message}</p>}
|
|
133
|
+
<ul>{events.map((e, i) => <li key={i}>{e.type === "event" ? e.event : e.type}</li>)}</ul>
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### `useWorkflowApprovals(workflows, options)`
|
|
140
|
+
|
|
141
|
+
The human side of an agent's `approveWhen` contract.
|
|
142
|
+
|
|
143
|
+
```tsx
|
|
144
|
+
import { Frontal } from "@frontal-labs/sdk";
|
|
145
|
+
import { useWorkflowApprovals } from "@frontal-labs/react";
|
|
146
|
+
|
|
147
|
+
const f = new Frontal({ apiKey: process.env.NEXT_PUBLIC_FRONTAL_API_KEY! });
|
|
148
|
+
|
|
149
|
+
export function Inbox() {
|
|
150
|
+
const { approvals, approve, reject, loading } = useWorkflowApprovals(f.workflows, {
|
|
151
|
+
pollIntervalMs: 5_000,
|
|
152
|
+
});
|
|
153
|
+
if (loading && approvals.length === 0) return <p>Loading…</p>;
|
|
154
|
+
return (
|
|
155
|
+
<ul>
|
|
156
|
+
{approvals.map((a) => (
|
|
157
|
+
<li key={a.id}>
|
|
158
|
+
{a.stepId}
|
|
159
|
+
<button onClick={() => approve(a.id, "LGTM")}>Approve</button>
|
|
160
|
+
<button onClick={() => reject(a.id)}>Reject</button>
|
|
161
|
+
</li>
|
|
162
|
+
))}
|
|
163
|
+
</ul>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Testing
|
|
169
|
+
|
|
170
|
+
Everything runs against mocks — no gateway, no key:
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
import { AISdk, toUIMessageStreamResponse } from "@frontal-labs/ai";
|
|
174
|
+
import { createTestClient, mockLanguageModel } from "@frontal-labs/testing";
|
|
175
|
+
|
|
176
|
+
const model = mockLanguageModel({ doGenerate: () => ({ text: "Hello!" }) });
|
|
177
|
+
const ai = new AISdk(createTestClient(model.routes).client.httpClient);
|
|
178
|
+
|
|
179
|
+
// A fake /api/chat for useChat({ fetch })
|
|
180
|
+
const fetchImpl: typeof fetch = async () =>
|
|
181
|
+
toUIMessageStreamResponse(ai.streamText({ model: "m", prompt: "hi" }));
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
See `tests/` in this package for `renderHook` examples.
|
|
185
|
+
|
|
186
|
+
## Environment Variables
|
|
187
|
+
|
|
188
|
+
| Variable | Required | Description |
|
|
189
|
+
| --- | --- | --- |
|
|
190
|
+
| `FRONTAL_API_KEY` | Server routes | API key (`frt_...`); never ship it to the browser |
|
|
191
|
+
| `NEXT_PUBLIC_FRONTAL_API_KEY` | Client hooks | A browser-scoped key, if your deployment issues them |
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/index.ts
|
|
22
|
+
var index_exports = {};
|
|
23
|
+
__export(index_exports, {
|
|
24
|
+
useAgentRun: () => useAgentRun,
|
|
25
|
+
useChat: () => useChat,
|
|
26
|
+
useWorkflowApprovals: () => useWorkflowApprovals
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(index_exports);
|
|
29
|
+
|
|
30
|
+
// src/use-agent-run.ts
|
|
31
|
+
var import_react = require("react");
|
|
32
|
+
function useAgentRun(agent, options = {}) {
|
|
33
|
+
const [run, setRun] = (0, import_react.useState)(void 0);
|
|
34
|
+
const [events, setEvents] = (0, import_react.useState)([]);
|
|
35
|
+
const [state, setState] = (0, import_react.useState)(void 0);
|
|
36
|
+
const [status, setStatus] = (0, import_react.useState)("idle");
|
|
37
|
+
const [error, setError] = (0, import_react.useState)(void 0);
|
|
38
|
+
const abortRef = (0, import_react.useRef)(void 0);
|
|
39
|
+
const watch = (0, import_react.useCallback)(
|
|
40
|
+
async (runId) => {
|
|
41
|
+
abortRef.current?.abort();
|
|
42
|
+
const controller = new AbortController();
|
|
43
|
+
abortRef.current = controller;
|
|
44
|
+
setEvents([]);
|
|
45
|
+
setError(void 0);
|
|
46
|
+
setStatus("streaming");
|
|
47
|
+
let failed = false;
|
|
48
|
+
for await (const part of agent.watch(runId, {
|
|
49
|
+
signal: controller.signal
|
|
50
|
+
})) {
|
|
51
|
+
setEvents((prev) => [...prev, part]);
|
|
52
|
+
if (part.type === "abort") continue;
|
|
53
|
+
if (part.type === "state") setState(part.state);
|
|
54
|
+
if (part.type === "error") {
|
|
55
|
+
failed = true;
|
|
56
|
+
setError(part.error);
|
|
57
|
+
options.onError?.(part.error);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (abortRef.current === controller) {
|
|
61
|
+
abortRef.current = void 0;
|
|
62
|
+
setStatus(failed ? "error" : "completed");
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
[agent, options.onError]
|
|
66
|
+
);
|
|
67
|
+
const trigger = (0, import_react.useCallback)(
|
|
68
|
+
async (event, payload = {}) => {
|
|
69
|
+
setStatus("starting");
|
|
70
|
+
setError(void 0);
|
|
71
|
+
try {
|
|
72
|
+
const started = await agent.message(event, payload);
|
|
73
|
+
setRun(started);
|
|
74
|
+
await watch(started.id);
|
|
75
|
+
return started;
|
|
76
|
+
} catch (err) {
|
|
77
|
+
const e = err;
|
|
78
|
+
setError(e);
|
|
79
|
+
options.onError?.(e);
|
|
80
|
+
setStatus("error");
|
|
81
|
+
return void 0;
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
[agent, options.onError, watch]
|
|
85
|
+
);
|
|
86
|
+
const stop = (0, import_react.useCallback)(() => {
|
|
87
|
+
abortRef.current?.abort();
|
|
88
|
+
abortRef.current = void 0;
|
|
89
|
+
setStatus("idle");
|
|
90
|
+
}, []);
|
|
91
|
+
return { run, events, state, status, error, trigger, watch, stop };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/use-chat.ts
|
|
95
|
+
var import_ai = require("@frontal-labs/ai");
|
|
96
|
+
var import_react2 = require("react");
|
|
97
|
+
function toWireMessages(messages) {
|
|
98
|
+
return messages.map((m) => ({ role: m.role, content: m.text }));
|
|
99
|
+
}
|
|
100
|
+
function useChat(options = {}) {
|
|
101
|
+
const {
|
|
102
|
+
api = "/api/chat",
|
|
103
|
+
id,
|
|
104
|
+
initialMessages = [],
|
|
105
|
+
body,
|
|
106
|
+
headers,
|
|
107
|
+
fetch: fetchImpl,
|
|
108
|
+
onError,
|
|
109
|
+
onFinish
|
|
110
|
+
} = options;
|
|
111
|
+
const [messages, setMessages] = (0, import_react2.useState)(initialMessages);
|
|
112
|
+
const [status, setStatus] = (0, import_react2.useState)("idle");
|
|
113
|
+
const [error, setError] = (0, import_react2.useState)(void 0);
|
|
114
|
+
const abortRef = (0, import_react2.useRef)(void 0);
|
|
115
|
+
const messagesRef = (0, import_react2.useRef)(messages);
|
|
116
|
+
messagesRef.current = messages;
|
|
117
|
+
const extrasRef = (0, import_react2.useRef)({ body, headers });
|
|
118
|
+
extrasRef.current = { body, headers };
|
|
119
|
+
const transport = (0, import_react2.useMemo)(() => {
|
|
120
|
+
if (typeof api === "function") return api;
|
|
121
|
+
return async (msgs, init) => (fetchImpl ?? fetch)(api, {
|
|
122
|
+
method: "POST",
|
|
123
|
+
headers: {
|
|
124
|
+
"content-type": "application/json",
|
|
125
|
+
...extrasRef.current.headers
|
|
126
|
+
},
|
|
127
|
+
body: JSON.stringify({
|
|
128
|
+
id,
|
|
129
|
+
messages: toWireMessages(msgs),
|
|
130
|
+
...extrasRef.current.body
|
|
131
|
+
}),
|
|
132
|
+
signal: init.signal
|
|
133
|
+
});
|
|
134
|
+
}, [api, fetchImpl, id]);
|
|
135
|
+
const run = (0, import_react2.useCallback)(
|
|
136
|
+
async (history) => {
|
|
137
|
+
abortRef.current?.abort();
|
|
138
|
+
const controller = new AbortController();
|
|
139
|
+
abortRef.current = controller;
|
|
140
|
+
setError(void 0);
|
|
141
|
+
setStatus("submitted");
|
|
142
|
+
let assistant = (0, import_ai.createUIMessage)("assistant");
|
|
143
|
+
setMessages([...history, assistant]);
|
|
144
|
+
try {
|
|
145
|
+
const response = await transport(history, {
|
|
146
|
+
signal: controller.signal
|
|
147
|
+
});
|
|
148
|
+
setStatus("streaming");
|
|
149
|
+
for await (const frame of (0, import_ai.parseUIMessageStream)(response, {
|
|
150
|
+
signal: controller.signal
|
|
151
|
+
})) {
|
|
152
|
+
if (frame.type === "error") {
|
|
153
|
+
setError(frame.error);
|
|
154
|
+
onError?.(frame.error);
|
|
155
|
+
}
|
|
156
|
+
assistant = (0, import_ai.applyUIFrame)(assistant, frame);
|
|
157
|
+
const snapshot = assistant;
|
|
158
|
+
setMessages([...history, snapshot]);
|
|
159
|
+
}
|
|
160
|
+
} catch (err) {
|
|
161
|
+
const e = {
|
|
162
|
+
code: "TRANSPORT_ERROR",
|
|
163
|
+
message: err instanceof Error ? err.message : String(err),
|
|
164
|
+
retryable: true
|
|
165
|
+
};
|
|
166
|
+
setError(e);
|
|
167
|
+
onError?.(e);
|
|
168
|
+
assistant = (0, import_ai.applyUIFrame)(assistant, { type: "error", error: e });
|
|
169
|
+
setMessages([...history, assistant]);
|
|
170
|
+
} finally {
|
|
171
|
+
if (abortRef.current === controller) abortRef.current = void 0;
|
|
172
|
+
}
|
|
173
|
+
setStatus(
|
|
174
|
+
assistant.parts.some((p) => p.type === "error") ? "error" : "idle"
|
|
175
|
+
);
|
|
176
|
+
onFinish?.(assistant);
|
|
177
|
+
},
|
|
178
|
+
[onError, onFinish, transport]
|
|
179
|
+
);
|
|
180
|
+
const send = (0, import_react2.useCallback)(
|
|
181
|
+
async (text) => {
|
|
182
|
+
const history = [...messagesRef.current, (0, import_ai.createUIMessage)("user", text)];
|
|
183
|
+
await run(history);
|
|
184
|
+
},
|
|
185
|
+
[run]
|
|
186
|
+
);
|
|
187
|
+
const retry = (0, import_react2.useCallback)(async () => {
|
|
188
|
+
const current = messagesRef.current;
|
|
189
|
+
const lastUser = [...current].reverse().findIndex((m) => m.role === "user");
|
|
190
|
+
if (lastUser === -1) return;
|
|
191
|
+
const cut = current.length - lastUser;
|
|
192
|
+
await run(current.slice(0, cut));
|
|
193
|
+
}, [run]);
|
|
194
|
+
const stop = (0, import_react2.useCallback)(() => {
|
|
195
|
+
abortRef.current?.abort();
|
|
196
|
+
abortRef.current = void 0;
|
|
197
|
+
}, []);
|
|
198
|
+
return { messages, status, error, send, stop, retry, setMessages };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/use-workflow-run.ts
|
|
202
|
+
var import_react3 = require("react");
|
|
203
|
+
function useWorkflowApprovals(workflows, options = {}) {
|
|
204
|
+
const { status = "pending", pollIntervalMs = 0, enabled = true } = options;
|
|
205
|
+
const [approvals, setApprovals] = (0, import_react3.useState)([]);
|
|
206
|
+
const [loading, setLoading] = (0, import_react3.useState)(false);
|
|
207
|
+
const [error, setError] = (0, import_react3.useState)(void 0);
|
|
208
|
+
const refresh = (0, import_react3.useCallback)(async () => {
|
|
209
|
+
setLoading(true);
|
|
210
|
+
try {
|
|
211
|
+
const page = await workflows.approvals.list({ status });
|
|
212
|
+
setApprovals(page.data);
|
|
213
|
+
setError(void 0);
|
|
214
|
+
} catch (err) {
|
|
215
|
+
setError(err);
|
|
216
|
+
} finally {
|
|
217
|
+
setLoading(false);
|
|
218
|
+
}
|
|
219
|
+
}, [status, workflows]);
|
|
220
|
+
(0, import_react3.useEffect)(() => {
|
|
221
|
+
if (!enabled) return;
|
|
222
|
+
void refresh();
|
|
223
|
+
if (!pollIntervalMs) return;
|
|
224
|
+
const timer = setInterval(() => void refresh(), pollIntervalMs);
|
|
225
|
+
return () => clearInterval(timer);
|
|
226
|
+
}, [enabled, pollIntervalMs, refresh]);
|
|
227
|
+
const act = (0, import_react3.useCallback)(
|
|
228
|
+
async (fn, id) => {
|
|
229
|
+
try {
|
|
230
|
+
const updated = await fn();
|
|
231
|
+
setApprovals(
|
|
232
|
+
(prev) => prev.map((a) => a.id === id ? updated : a).filter((a) => a.status === status)
|
|
233
|
+
);
|
|
234
|
+
return updated;
|
|
235
|
+
} catch (err) {
|
|
236
|
+
setError(err);
|
|
237
|
+
return void 0;
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
[status]
|
|
241
|
+
);
|
|
242
|
+
const approve = (0, import_react3.useCallback)(
|
|
243
|
+
(id, comment) => act(() => workflows.approvals.approve(id, comment), id),
|
|
244
|
+
[act, workflows]
|
|
245
|
+
);
|
|
246
|
+
const reject = (0, import_react3.useCallback)(
|
|
247
|
+
(id, comment) => act(() => workflows.approvals.reject(id, comment), id),
|
|
248
|
+
[act, workflows]
|
|
249
|
+
);
|
|
250
|
+
return { approvals, loading, error, refresh, approve, reject };
|
|
251
|
+
}
|
|
252
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
253
|
+
0 && (module.exports = {
|
|
254
|
+
useAgentRun,
|
|
255
|
+
useChat,
|
|
256
|
+
useWorkflowApprovals
|
|
257
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @frontal-labs/react
|
|
3
|
+
*
|
|
4
|
+
* React hooks for the Frontal SDK. Transport-agnostic: `useChat` talks to
|
|
5
|
+
* any route that returns `toUIMessageStreamResponse(...)`; the other hooks
|
|
6
|
+
* take SDK objects from `new Frontal(...)`.
|
|
7
|
+
*/
|
|
8
|
+
export type { UIError, UIMessage, UIMessagePart } from "@frontal-labs/ai";
|
|
9
|
+
export type { AgentRunStatus, UseAgentRunOptions, UseAgentRunResult, } from "./use-agent-run";
|
|
10
|
+
export { useAgentRun } from "./use-agent-run";
|
|
11
|
+
export type { ChatStatus, ChatTransport, UseChatOptions, UseChatResult, } from "./use-chat";
|
|
12
|
+
export { useChat } from "./use-chat";
|
|
13
|
+
export type { UseWorkflowApprovalsOptions, UseWorkflowApprovalsResult, } from "./use-workflow-run";
|
|
14
|
+
export { useWorkflowApprovals } from "./use-workflow-run";
|
|
15
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAC1E,YAAY,EACV,cAAc,EACd,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,YAAY,EACV,UAAU,EACV,aAAa,EACb,cAAc,EACd,aAAa,GACd,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACrC,YAAY,EACV,2BAA2B,EAC3B,0BAA0B,GAC3B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/use-agent-run.ts
|
|
4
|
+
import { useCallback, useRef, useState } from "react";
|
|
5
|
+
function useAgentRun(agent, options = {}) {
|
|
6
|
+
const [run, setRun] = useState(void 0);
|
|
7
|
+
const [events, setEvents] = useState([]);
|
|
8
|
+
const [state, setState] = useState(void 0);
|
|
9
|
+
const [status, setStatus] = useState("idle");
|
|
10
|
+
const [error, setError] = useState(void 0);
|
|
11
|
+
const abortRef = useRef(void 0);
|
|
12
|
+
const watch = useCallback(
|
|
13
|
+
async (runId) => {
|
|
14
|
+
abortRef.current?.abort();
|
|
15
|
+
const controller = new AbortController();
|
|
16
|
+
abortRef.current = controller;
|
|
17
|
+
setEvents([]);
|
|
18
|
+
setError(void 0);
|
|
19
|
+
setStatus("streaming");
|
|
20
|
+
let failed = false;
|
|
21
|
+
for await (const part of agent.watch(runId, {
|
|
22
|
+
signal: controller.signal
|
|
23
|
+
})) {
|
|
24
|
+
setEvents((prev) => [...prev, part]);
|
|
25
|
+
if (part.type === "abort") continue;
|
|
26
|
+
if (part.type === "state") setState(part.state);
|
|
27
|
+
if (part.type === "error") {
|
|
28
|
+
failed = true;
|
|
29
|
+
setError(part.error);
|
|
30
|
+
options.onError?.(part.error);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (abortRef.current === controller) {
|
|
34
|
+
abortRef.current = void 0;
|
|
35
|
+
setStatus(failed ? "error" : "completed");
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
[agent, options.onError]
|
|
39
|
+
);
|
|
40
|
+
const trigger = useCallback(
|
|
41
|
+
async (event, payload = {}) => {
|
|
42
|
+
setStatus("starting");
|
|
43
|
+
setError(void 0);
|
|
44
|
+
try {
|
|
45
|
+
const started = await agent.message(event, payload);
|
|
46
|
+
setRun(started);
|
|
47
|
+
await watch(started.id);
|
|
48
|
+
return started;
|
|
49
|
+
} catch (err) {
|
|
50
|
+
const e = err;
|
|
51
|
+
setError(e);
|
|
52
|
+
options.onError?.(e);
|
|
53
|
+
setStatus("error");
|
|
54
|
+
return void 0;
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
[agent, options.onError, watch]
|
|
58
|
+
);
|
|
59
|
+
const stop = useCallback(() => {
|
|
60
|
+
abortRef.current?.abort();
|
|
61
|
+
abortRef.current = void 0;
|
|
62
|
+
setStatus("idle");
|
|
63
|
+
}, []);
|
|
64
|
+
return { run, events, state, status, error, trigger, watch, stop };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/use-chat.ts
|
|
68
|
+
import {
|
|
69
|
+
applyUIFrame,
|
|
70
|
+
createUIMessage,
|
|
71
|
+
parseUIMessageStream
|
|
72
|
+
} from "@frontal-labs/ai";
|
|
73
|
+
import { useCallback as useCallback2, useMemo, useRef as useRef2, useState as useState2 } from "react";
|
|
74
|
+
function toWireMessages(messages) {
|
|
75
|
+
return messages.map((m) => ({ role: m.role, content: m.text }));
|
|
76
|
+
}
|
|
77
|
+
function useChat(options = {}) {
|
|
78
|
+
const {
|
|
79
|
+
api = "/api/chat",
|
|
80
|
+
id,
|
|
81
|
+
initialMessages = [],
|
|
82
|
+
body,
|
|
83
|
+
headers,
|
|
84
|
+
fetch: fetchImpl,
|
|
85
|
+
onError,
|
|
86
|
+
onFinish
|
|
87
|
+
} = options;
|
|
88
|
+
const [messages, setMessages] = useState2(initialMessages);
|
|
89
|
+
const [status, setStatus] = useState2("idle");
|
|
90
|
+
const [error, setError] = useState2(void 0);
|
|
91
|
+
const abortRef = useRef2(void 0);
|
|
92
|
+
const messagesRef = useRef2(messages);
|
|
93
|
+
messagesRef.current = messages;
|
|
94
|
+
const extrasRef = useRef2({ body, headers });
|
|
95
|
+
extrasRef.current = { body, headers };
|
|
96
|
+
const transport = useMemo(() => {
|
|
97
|
+
if (typeof api === "function") return api;
|
|
98
|
+
return async (msgs, init) => (fetchImpl ?? fetch)(api, {
|
|
99
|
+
method: "POST",
|
|
100
|
+
headers: {
|
|
101
|
+
"content-type": "application/json",
|
|
102
|
+
...extrasRef.current.headers
|
|
103
|
+
},
|
|
104
|
+
body: JSON.stringify({
|
|
105
|
+
id,
|
|
106
|
+
messages: toWireMessages(msgs),
|
|
107
|
+
...extrasRef.current.body
|
|
108
|
+
}),
|
|
109
|
+
signal: init.signal
|
|
110
|
+
});
|
|
111
|
+
}, [api, fetchImpl, id]);
|
|
112
|
+
const run = useCallback2(
|
|
113
|
+
async (history) => {
|
|
114
|
+
abortRef.current?.abort();
|
|
115
|
+
const controller = new AbortController();
|
|
116
|
+
abortRef.current = controller;
|
|
117
|
+
setError(void 0);
|
|
118
|
+
setStatus("submitted");
|
|
119
|
+
let assistant = createUIMessage("assistant");
|
|
120
|
+
setMessages([...history, assistant]);
|
|
121
|
+
try {
|
|
122
|
+
const response = await transport(history, {
|
|
123
|
+
signal: controller.signal
|
|
124
|
+
});
|
|
125
|
+
setStatus("streaming");
|
|
126
|
+
for await (const frame of parseUIMessageStream(response, {
|
|
127
|
+
signal: controller.signal
|
|
128
|
+
})) {
|
|
129
|
+
if (frame.type === "error") {
|
|
130
|
+
setError(frame.error);
|
|
131
|
+
onError?.(frame.error);
|
|
132
|
+
}
|
|
133
|
+
assistant = applyUIFrame(assistant, frame);
|
|
134
|
+
const snapshot = assistant;
|
|
135
|
+
setMessages([...history, snapshot]);
|
|
136
|
+
}
|
|
137
|
+
} catch (err) {
|
|
138
|
+
const e = {
|
|
139
|
+
code: "TRANSPORT_ERROR",
|
|
140
|
+
message: err instanceof Error ? err.message : String(err),
|
|
141
|
+
retryable: true
|
|
142
|
+
};
|
|
143
|
+
setError(e);
|
|
144
|
+
onError?.(e);
|
|
145
|
+
assistant = applyUIFrame(assistant, { type: "error", error: e });
|
|
146
|
+
setMessages([...history, assistant]);
|
|
147
|
+
} finally {
|
|
148
|
+
if (abortRef.current === controller) abortRef.current = void 0;
|
|
149
|
+
}
|
|
150
|
+
setStatus(
|
|
151
|
+
assistant.parts.some((p) => p.type === "error") ? "error" : "idle"
|
|
152
|
+
);
|
|
153
|
+
onFinish?.(assistant);
|
|
154
|
+
},
|
|
155
|
+
[onError, onFinish, transport]
|
|
156
|
+
);
|
|
157
|
+
const send = useCallback2(
|
|
158
|
+
async (text) => {
|
|
159
|
+
const history = [...messagesRef.current, createUIMessage("user", text)];
|
|
160
|
+
await run(history);
|
|
161
|
+
},
|
|
162
|
+
[run]
|
|
163
|
+
);
|
|
164
|
+
const retry = useCallback2(async () => {
|
|
165
|
+
const current = messagesRef.current;
|
|
166
|
+
const lastUser = [...current].reverse().findIndex((m) => m.role === "user");
|
|
167
|
+
if (lastUser === -1) return;
|
|
168
|
+
const cut = current.length - lastUser;
|
|
169
|
+
await run(current.slice(0, cut));
|
|
170
|
+
}, [run]);
|
|
171
|
+
const stop = useCallback2(() => {
|
|
172
|
+
abortRef.current?.abort();
|
|
173
|
+
abortRef.current = void 0;
|
|
174
|
+
}, []);
|
|
175
|
+
return { messages, status, error, send, stop, retry, setMessages };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// src/use-workflow-run.ts
|
|
179
|
+
import { useCallback as useCallback3, useEffect, useState as useState3 } from "react";
|
|
180
|
+
function useWorkflowApprovals(workflows, options = {}) {
|
|
181
|
+
const { status = "pending", pollIntervalMs = 0, enabled = true } = options;
|
|
182
|
+
const [approvals, setApprovals] = useState3([]);
|
|
183
|
+
const [loading, setLoading] = useState3(false);
|
|
184
|
+
const [error, setError] = useState3(void 0);
|
|
185
|
+
const refresh = useCallback3(async () => {
|
|
186
|
+
setLoading(true);
|
|
187
|
+
try {
|
|
188
|
+
const page = await workflows.approvals.list({ status });
|
|
189
|
+
setApprovals(page.data);
|
|
190
|
+
setError(void 0);
|
|
191
|
+
} catch (err) {
|
|
192
|
+
setError(err);
|
|
193
|
+
} finally {
|
|
194
|
+
setLoading(false);
|
|
195
|
+
}
|
|
196
|
+
}, [status, workflows]);
|
|
197
|
+
useEffect(() => {
|
|
198
|
+
if (!enabled) return;
|
|
199
|
+
void refresh();
|
|
200
|
+
if (!pollIntervalMs) return;
|
|
201
|
+
const timer = setInterval(() => void refresh(), pollIntervalMs);
|
|
202
|
+
return () => clearInterval(timer);
|
|
203
|
+
}, [enabled, pollIntervalMs, refresh]);
|
|
204
|
+
const act = useCallback3(
|
|
205
|
+
async (fn, id) => {
|
|
206
|
+
try {
|
|
207
|
+
const updated = await fn();
|
|
208
|
+
setApprovals(
|
|
209
|
+
(prev) => prev.map((a) => a.id === id ? updated : a).filter((a) => a.status === status)
|
|
210
|
+
);
|
|
211
|
+
return updated;
|
|
212
|
+
} catch (err) {
|
|
213
|
+
setError(err);
|
|
214
|
+
return void 0;
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
[status]
|
|
218
|
+
);
|
|
219
|
+
const approve = useCallback3(
|
|
220
|
+
(id, comment) => act(() => workflows.approvals.approve(id, comment), id),
|
|
221
|
+
[act, workflows]
|
|
222
|
+
);
|
|
223
|
+
const reject = useCallback3(
|
|
224
|
+
(id, comment) => act(() => workflows.approvals.reject(id, comment), id),
|
|
225
|
+
[act, workflows]
|
|
226
|
+
);
|
|
227
|
+
return { approvals, loading, error, refresh, approve, reject };
|
|
228
|
+
}
|
|
229
|
+
export {
|
|
230
|
+
useAgentRun,
|
|
231
|
+
useChat,
|
|
232
|
+
useWorkflowApprovals
|
|
233
|
+
};
|