@agentforge-io/chat-sdk 2.0.20 → 2.0.22
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/dist/entities.d.ts +6 -2
- package/dist/react.d.ts +27 -19
- package/dist/react.js +12 -9
- package/dist/session.d.ts +7 -0
- package/dist/session.js +25 -9
- package/dist/transport.d.ts +9 -0
- package/dist/transport.js +20 -0
- package/package.json +1 -1
package/dist/entities.d.ts
CHANGED
|
@@ -131,8 +131,12 @@ export interface ChatSessionOptions {
|
|
|
131
131
|
token: string;
|
|
132
132
|
/**
|
|
133
133
|
* Base URL of the AgentForge API, including scheme. Trailing slash is
|
|
134
|
-
* stripped.
|
|
135
|
-
*
|
|
134
|
+
* stripped. Resolution order:
|
|
135
|
+
* 1. This option (when present).
|
|
136
|
+
* 2. `window.AGENTFORGE_API_BASE_URL` (runtime override, useful when
|
|
137
|
+
* embedding via a `<script>` tag without React props).
|
|
138
|
+
* 3. The baked default that ships with the current SDK version.
|
|
139
|
+
* Hosts embedding into their own site can leave this unset.
|
|
136
140
|
*/
|
|
137
141
|
apiBaseUrl?: string;
|
|
138
142
|
/** Stable id for this end-user's browser. Persist it (localStorage etc.)
|
package/dist/react.d.ts
CHANGED
|
@@ -18,22 +18,18 @@
|
|
|
18
18
|
import { type CSSProperties } from 'react';
|
|
19
19
|
import type { ChatTheme } from './entities';
|
|
20
20
|
/**
|
|
21
|
-
* Optional
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* `/public/chat/approvals/:id/{approve|deny}` with the visitor's
|
|
27
|
-
* `browserSessionId` for ownership verification.
|
|
28
|
-
*
|
|
29
|
-
* Throwing or rejecting from the handler keeps the bubble in its
|
|
30
|
-
* pending state and surfaces the error inline so the visitor can
|
|
31
|
-
* retry.
|
|
21
|
+
* Optional observer fired after the SDK resolves a tool-approval bubble.
|
|
22
|
+
* The SDK owns the network call (it already has `apiBaseUrl` and the
|
|
23
|
+
* `browserSessionId`); the host only gets notified for analytics or
|
|
24
|
+
* audit logging. The hook is fire-and-forget — throwing from it does
|
|
25
|
+
* not roll back the decision the server already committed.
|
|
32
26
|
*/
|
|
33
|
-
export type
|
|
27
|
+
export type ApprovalDecisionObserver = (args: {
|
|
34
28
|
approvalId: string;
|
|
35
29
|
action: 'approve' | 'deny';
|
|
36
|
-
|
|
30
|
+
ok: boolean;
|
|
31
|
+
error?: string;
|
|
32
|
+
}) => void;
|
|
37
33
|
/**
|
|
38
34
|
* Localizable copy for the approval / blocked bubbles. Pass via the
|
|
39
35
|
* `approvalCopy` prop on `<ChatWidget>` to drop the defaults (Spanish,
|
|
@@ -65,8 +61,12 @@ export interface ApprovalCopy {
|
|
|
65
61
|
export interface ChatWidgetProps {
|
|
66
62
|
/** Public chat token (`aft_*`) issued from the admin UI. */
|
|
67
63
|
token: string;
|
|
68
|
-
/**
|
|
69
|
-
*
|
|
64
|
+
/**
|
|
65
|
+
* AgentForge API origin. Optional — the SDK ships with a built-in
|
|
66
|
+
* default that points at the hosted AgentForge deployment. Override
|
|
67
|
+
* this when you self-host the backend. A `window.AGENTFORGE_API_BASE_URL`
|
|
68
|
+
* global also overrides, useful for `<script>`-tag embeds.
|
|
69
|
+
*/
|
|
70
70
|
apiBaseUrl?: string;
|
|
71
71
|
/** Render inline (fills the parent) instead of as a floating bubble. */
|
|
72
72
|
inline?: boolean;
|
|
@@ -84,11 +84,19 @@ export interface ChatWidgetProps {
|
|
|
84
84
|
/** Inline style on the root container. */
|
|
85
85
|
style?: CSSProperties;
|
|
86
86
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
87
|
+
* Optional observer called after the SDK resolves an approval bubble.
|
|
88
|
+
* The SDK handles the network call internally — pass this only if you
|
|
89
|
+
* want to record the decision for analytics. See
|
|
90
|
+
* `ApprovalDecisionObserver`.
|
|
91
|
+
*/
|
|
92
|
+
onApprovalDecision?: ApprovalDecisionObserver;
|
|
93
|
+
/**
|
|
94
|
+
* When `false` (the default), the Approve/Deny buttons render on
|
|
95
|
+
* `awaiting_approval` bubbles. Set to `true` to render bubbles as
|
|
96
|
+
* read-only — useful for transcripts/embeds where the visitor cannot
|
|
97
|
+
* legitimately decide.
|
|
90
98
|
*/
|
|
91
|
-
|
|
99
|
+
readOnlyApprovals?: boolean;
|
|
92
100
|
}
|
|
93
101
|
/**
|
|
94
102
|
* Drop-in chat widget. Owns its own `ChatSession` and re-renders on every
|
package/dist/react.js
CHANGED
|
@@ -181,7 +181,7 @@ function fallbackCopy(ctx) {
|
|
|
181
181
|
* SDK event. Consumers don't need to read `session.getState()` themselves.
|
|
182
182
|
*/
|
|
183
183
|
function ChatWidget(props) {
|
|
184
|
-
const { token, apiBaseUrl, inline = false, position, browserSessionId, resumeConversationId, stream, className, style,
|
|
184
|
+
const { token, apiBaseUrl, inline = false, position, browserSessionId, resumeConversationId, stream, className, style, onApprovalDecision, readOnlyApprovals = false, } = props;
|
|
185
185
|
const [session, setSession] = (0, react_1.useState)(null);
|
|
186
186
|
const [status, setStatus] = (0, react_1.useState)('idle');
|
|
187
187
|
const [agent, setAgent] = (0, react_1.useState)();
|
|
@@ -301,7 +301,7 @@ function ChatWidget(props) {
|
|
|
301
301
|
};
|
|
302
302
|
return ((0, jsx_runtime_1.jsxs)("div", { className: rootClass, style: rootStyle, "data-style-id": styleId, children: [!inline && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "af-toggle", "aria-label": open ? 'Close chat' : 'Open chat', "aria-expanded": open, onClick: () => setOpen((v) => !v), children: open ? (0, jsx_runtime_1.jsx)(CloseIcon, {}) : (0, jsx_runtime_1.jsx)(ChatIcon, {}) })), (0, jsx_runtime_1.jsxs)("div", { className: `af-panel ${open ? 'af-open' : ''}`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "af-header", children: [theme?.avatarUrl ? (
|
|
303
303
|
// eslint-disable-next-line @next/next/no-img-element
|
|
304
|
-
(0, jsx_runtime_1.jsx)("img", { className: "af-header-avatar", src: theme.avatarUrl, alt: "" })) : null, (0, jsx_runtime_1.jsxs)("div", { className: "af-header-info", children: [(0, jsx_runtime_1.jsx)("div", { className: "af-header-title", children: theme?.title ?? agent?.name ?? 'Chat' }), (0, jsx_runtime_1.jsx)("div", { className: "af-header-subtitle", children: status === 'loading' ? 'Loading…' : agent?.description ?? '' })] }), !inline && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "af-close", onClick: () => setOpen(false), "aria-label": "Close chat", children: (0, jsx_runtime_1.jsx)(CloseIcon, {}) }))] }), (0, jsx_runtime_1.jsx)("div", { className: "af-messages", ref: messagesRef, children: messages.map((m) => ((0, jsx_runtime_1.jsx)(MessageBubble, { message: m,
|
|
304
|
+
(0, jsx_runtime_1.jsx)("img", { className: "af-header-avatar", src: theme.avatarUrl, alt: "" })) : null, (0, jsx_runtime_1.jsxs)("div", { className: "af-header-info", children: [(0, jsx_runtime_1.jsx)("div", { className: "af-header-title", children: theme?.title ?? agent?.name ?? 'Chat' }), (0, jsx_runtime_1.jsx)("div", { className: "af-header-subtitle", children: status === 'loading' ? 'Loading…' : agent?.description ?? '' })] }), !inline && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "af-close", onClick: () => setOpen(false), "aria-label": "Close chat", children: (0, jsx_runtime_1.jsx)(CloseIcon, {}) }))] }), (0, jsx_runtime_1.jsx)("div", { className: "af-messages", ref: messagesRef, children: messages.map((m) => ((0, jsx_runtime_1.jsx)(MessageBubble, { message: m, session: session, readOnly: readOnlyApprovals, onDecision: onApprovalDecision, onContinue: () => {
|
|
305
305
|
// After a successful Approve, kick the next turn so the
|
|
306
306
|
// gate's fast-path consumes the approval and the tool
|
|
307
307
|
// actually runs. `silent: true` keeps the literal
|
|
@@ -315,10 +315,10 @@ function ChatWidget(props) {
|
|
|
315
315
|
}, 250);
|
|
316
316
|
} }, m.id))) }), lastError && status === 'error' && ((0, jsx_runtime_1.jsx)("div", { className: "af-error", children: lastError })), (0, jsx_runtime_1.jsxs)("div", { className: "af-input-row", children: [(0, jsx_runtime_1.jsx)("textarea", { className: "af-input", value: draft, onChange: (e) => setDraft(e.target.value), onKeyDown: onKeyDown, placeholder: "Type a message\u2026", rows: 1, disabled: status === 'ended' || status === 'loading' || status === 'idle' }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "af-send", onClick: handleSend, disabled: sendDisabled, "aria-label": "Send message", children: (0, jsx_runtime_1.jsx)(SendIcon, {}) })] }), (0, jsx_runtime_1.jsx)("div", { className: "af-footer", children: "Powered by AgentForge" })] })] }));
|
|
317
317
|
}
|
|
318
|
-
function MessageBubble({ message,
|
|
318
|
+
function MessageBubble({ message, session, readOnly, onDecision, onContinue, }) {
|
|
319
319
|
const kind = message.metadata?.kind;
|
|
320
320
|
if (kind === 'awaiting_approval') {
|
|
321
|
-
return ((0, jsx_runtime_1.jsx)(ApprovalBubble, { message: message,
|
|
321
|
+
return ((0, jsx_runtime_1.jsx)(ApprovalBubble, { message: message, session: session, readOnly: readOnly, onDecision: onDecision, onContinue: onContinue }));
|
|
322
322
|
}
|
|
323
323
|
if (kind === 'tool_blocked') {
|
|
324
324
|
return (0, jsx_runtime_1.jsx)(BlockedBubble, { message: message });
|
|
@@ -348,7 +348,7 @@ function MessageBubble({ message, onApprovalAction, onContinue, }) {
|
|
|
348
348
|
* so the chat auto-sends "continue" and the gate's fast-path consumes
|
|
349
349
|
* the row on the next turn.
|
|
350
350
|
*/
|
|
351
|
-
function ApprovalBubble({ message,
|
|
351
|
+
function ApprovalBubble({ message, session, readOnly, onDecision, onContinue, }) {
|
|
352
352
|
const meta = message.metadata;
|
|
353
353
|
const [busy, setBusy] = (0, react_1.useState)(null);
|
|
354
354
|
const [decided, setDecided] = (0, react_1.useState)(null);
|
|
@@ -361,24 +361,27 @@ function ApprovalBubble({ message, onAction, onContinue, }) {
|
|
|
361
361
|
if (!meta)
|
|
362
362
|
return null;
|
|
363
363
|
const act = async (action) => {
|
|
364
|
-
if (!
|
|
364
|
+
if (!session)
|
|
365
365
|
return;
|
|
366
366
|
setBusy(action);
|
|
367
367
|
setErr(null);
|
|
368
368
|
try {
|
|
369
|
-
await
|
|
369
|
+
await session.resolveApproval(meta.approvalId, action);
|
|
370
370
|
setDecided(action === 'approve' ? 'approved' : 'denied');
|
|
371
|
+
onDecision?.({ approvalId: meta.approvalId, action, ok: true });
|
|
371
372
|
if (action === 'approve')
|
|
372
373
|
onContinue();
|
|
373
374
|
}
|
|
374
375
|
catch (e) {
|
|
375
|
-
|
|
376
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
377
|
+
setErr(message);
|
|
378
|
+
onDecision?.({ approvalId: meta.approvalId, action, ok: false, error: message });
|
|
376
379
|
}
|
|
377
380
|
finally {
|
|
378
381
|
setBusy(null);
|
|
379
382
|
}
|
|
380
383
|
};
|
|
381
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className: "af-msg af-msg-approval", children: [(0, jsx_runtime_1.jsx)("div", { className: "af-approval-title", children: copy.title }), (0, jsx_runtime_1.jsx)("div", { className: "af-approval-body", children: copy.body }), remaining && !decided && ((0, jsx_runtime_1.jsx)("div", { className: "af-approval-meta", children: remaining })), decided && ((0, jsx_runtime_1.jsx)("div", { className: `af-approval-pill af-approval-pill-${decided}`, children: decided === 'approved' ? copy.approvedPill : copy.deniedPill })), !decided &&
|
|
384
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "af-msg af-msg-approval", children: [(0, jsx_runtime_1.jsx)("div", { className: "af-approval-title", children: copy.title }), (0, jsx_runtime_1.jsx)("div", { className: "af-approval-body", children: copy.body }), remaining && !decided && ((0, jsx_runtime_1.jsx)("div", { className: "af-approval-meta", children: remaining })), decided && ((0, jsx_runtime_1.jsx)("div", { className: `af-approval-pill af-approval-pill-${decided}`, children: decided === 'approved' ? copy.approvedPill : copy.deniedPill })), !decided && !readOnly && session && ((0, jsx_runtime_1.jsxs)("div", { className: "af-approval-actions", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "af-approval-btn af-approval-approve", disabled: busy !== null, onClick: () => act('approve'), children: busy === 'approve' ? copy.approveBusyLabel : copy.approveLabel }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "af-approval-btn af-approval-deny", disabled: busy !== null, onClick: () => act('deny'), children: busy === 'deny' ? copy.denyBusyLabel : copy.denyLabel })] })), !decided && readOnly && ((0, jsx_runtime_1.jsx)("div", { className: "af-approval-hint", children: copy.readOnlyHint })), err && (0, jsx_runtime_1.jsx)("div", { className: "af-approval-error", children: err })] }));
|
|
382
385
|
}
|
|
383
386
|
/**
|
|
384
387
|
* Terminal "tool blocked" bubble. No action — just informs the visitor
|
package/dist/session.d.ts
CHANGED
|
@@ -36,6 +36,13 @@ export declare class ChatSession {
|
|
|
36
36
|
* continue chatting.
|
|
37
37
|
*/
|
|
38
38
|
end(): Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* Resolve a pending tool-approval bubble. The SDK owns the transport
|
|
41
|
+
* call so hosts don't have to know the approval endpoint or assemble
|
|
42
|
+
* the `browserSessionId` payload. Errors propagate so the calling View
|
|
43
|
+
* layer can keep the bubble in pending state and show the message.
|
|
44
|
+
*/
|
|
45
|
+
resolveApproval(approvalId: string, action: 'approve' | 'deny'): Promise<void>;
|
|
39
46
|
/** Tear down. Future sends throw. Useful for SPA unmount. */
|
|
40
47
|
destroy(): void;
|
|
41
48
|
/**
|
package/dist/session.js
CHANGED
|
@@ -29,9 +29,6 @@ class ChatSession {
|
|
|
29
29
|
if (!opts.token)
|
|
30
30
|
throw new Error('ChatSession: token is required');
|
|
31
31
|
const apiBaseUrl = opts.apiBaseUrl ?? defaultApiBase();
|
|
32
|
-
if (!apiBaseUrl) {
|
|
33
|
-
throw new Error('ChatSession: apiBaseUrl is required outside the browser');
|
|
34
|
-
}
|
|
35
32
|
this.transport = new transport_1.HttpTransport(apiBaseUrl, opts.token);
|
|
36
33
|
this.stream = opts.stream ?? true;
|
|
37
34
|
this.browserSessionId = opts.browserSessionId ?? generateBrowserSessionId();
|
|
@@ -129,6 +126,15 @@ class ChatSession {
|
|
|
129
126
|
this.handleError(err);
|
|
130
127
|
}
|
|
131
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Resolve a pending tool-approval bubble. The SDK owns the transport
|
|
131
|
+
* call so hosts don't have to know the approval endpoint or assemble
|
|
132
|
+
* the `browserSessionId` payload. Errors propagate so the calling View
|
|
133
|
+
* layer can keep the bubble in pending state and show the message.
|
|
134
|
+
*/
|
|
135
|
+
async resolveApproval(approvalId, action) {
|
|
136
|
+
await this.transport.resolveApproval(approvalId, action, this.browserSessionId);
|
|
137
|
+
}
|
|
132
138
|
/** Tear down. Future sends throw. Useful for SPA unmount. */
|
|
133
139
|
destroy() {
|
|
134
140
|
this.listeners.clear();
|
|
@@ -383,11 +389,21 @@ function generateBrowserSessionId() {
|
|
|
383
389
|
return c.randomUUID();
|
|
384
390
|
return `${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
385
391
|
}
|
|
392
|
+
/**
|
|
393
|
+
* Build-time default API origin. Bumped together with the SDK when the
|
|
394
|
+
* hosted AgentForge deployment moves to a new domain. Hosts that
|
|
395
|
+
* self-host the backend override this via the `apiBaseUrl` constructor
|
|
396
|
+
* option, or at runtime via `window.AGENTFORGE_API_BASE_URL`.
|
|
397
|
+
*/
|
|
398
|
+
const BAKED_API_BASE = 'https://api-agentforge.stupidmvp.com';
|
|
386
399
|
function defaultApiBase() {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
//
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
400
|
+
// Runtime override for hosts that embed via a script tag and don't
|
|
401
|
+
// want to touch React props. Set once on `window` before the widget
|
|
402
|
+
// mounts and the SDK uses it for every transport call.
|
|
403
|
+
if (typeof window !== 'undefined') {
|
|
404
|
+
const override = window.AGENTFORGE_API_BASE_URL;
|
|
405
|
+
if (override)
|
|
406
|
+
return override;
|
|
407
|
+
}
|
|
408
|
+
return BAKED_API_BASE;
|
|
393
409
|
}
|
package/dist/transport.d.ts
CHANGED
|
@@ -83,6 +83,15 @@ export declare class HttpTransport {
|
|
|
83
83
|
metadata?: Record<string, unknown>;
|
|
84
84
|
}>;
|
|
85
85
|
} | null>;
|
|
86
|
+
/**
|
|
87
|
+
* Resolve a pending tool-approval bubble. The server verifies the row
|
|
88
|
+
* is owned by `browser:<browserSessionId>` before mutating it, so the
|
|
89
|
+
* SDK forwards the same browser session it uses for messaging. The
|
|
90
|
+
* route lives under `/public/chat/approvals/*` — siblings of this
|
|
91
|
+
* transport's `/public/chat/:token/*` routes — and does not need the
|
|
92
|
+
* widget token (the approvalId is the capability).
|
|
93
|
+
*/
|
|
94
|
+
resolveApproval(approvalId: string, action: 'approve' | 'deny', browserSessionId: string): Promise<void>;
|
|
86
95
|
endConversation(conversationId: string, browserSessionId: string): Promise<void>;
|
|
87
96
|
/** Parse the server's SSE response body into typed events. */
|
|
88
97
|
private readSse;
|
package/dist/transport.js
CHANGED
|
@@ -111,6 +111,26 @@ class HttpTransport {
|
|
|
111
111
|
})),
|
|
112
112
|
};
|
|
113
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Resolve a pending tool-approval bubble. The server verifies the row
|
|
116
|
+
* is owned by `browser:<browserSessionId>` before mutating it, so the
|
|
117
|
+
* SDK forwards the same browser session it uses for messaging. The
|
|
118
|
+
* route lives under `/public/chat/approvals/*` — siblings of this
|
|
119
|
+
* transport's `/public/chat/:token/*` routes — and does not need the
|
|
120
|
+
* widget token (the approvalId is the capability).
|
|
121
|
+
*/
|
|
122
|
+
async resolveApproval(approvalId, action, browserSessionId) {
|
|
123
|
+
const base = this.apiBaseUrl.replace(/\/$/, '');
|
|
124
|
+
const res = await fetch(`${base}/public/chat/approvals/${encodeURIComponent(approvalId)}/${action}`, {
|
|
125
|
+
method: 'POST',
|
|
126
|
+
headers: { 'Content-Type': 'application/json' },
|
|
127
|
+
body: JSON.stringify({ browserSessionId }),
|
|
128
|
+
});
|
|
129
|
+
if (!res.ok && res.status !== 204) {
|
|
130
|
+
const data = await safeJson(res);
|
|
131
|
+
throw makeTransportError(data?.message ?? `HTTP ${res.status}`, res.status, data?.error);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
114
134
|
async endConversation(conversationId, browserSessionId) {
|
|
115
135
|
const res = await fetch(this.url(`/conversations/${encodeURIComponent(conversationId)}/end`), {
|
|
116
136
|
method: 'POST',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentforge-io/chat-sdk",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.22",
|
|
4
4
|
"description": "Framework-free chat session SDK for AgentForge public chat tokens. Headless — no DOM. Drop into any frontend (React, Vue, Svelte, vanilla) and listen for events.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|