@agentforge-io/chat-sdk 2.0.20 → 2.0.21
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/react.d.ts +21 -17
- package/dist/react.js +12 -9
- package/dist/session.d.ts +7 -0
- package/dist/session.js +9 -0
- package/dist/transport.d.ts +9 -0
- package/dist/transport.js +20 -0
- package/package.json +1 -1
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,
|
|
@@ -84,11 +80,19 @@ export interface ChatWidgetProps {
|
|
|
84
80
|
/** Inline style on the root container. */
|
|
85
81
|
style?: CSSProperties;
|
|
86
82
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
83
|
+
* Optional observer called after the SDK resolves an approval bubble.
|
|
84
|
+
* The SDK handles the network call internally — pass this only if you
|
|
85
|
+
* want to record the decision for analytics. See
|
|
86
|
+
* `ApprovalDecisionObserver`.
|
|
87
|
+
*/
|
|
88
|
+
onApprovalDecision?: ApprovalDecisionObserver;
|
|
89
|
+
/**
|
|
90
|
+
* When `false` (the default), the Approve/Deny buttons render on
|
|
91
|
+
* `awaiting_approval` bubbles. Set to `true` to render bubbles as
|
|
92
|
+
* read-only — useful for transcripts/embeds where the visitor cannot
|
|
93
|
+
* legitimately decide.
|
|
90
94
|
*/
|
|
91
|
-
|
|
95
|
+
readOnlyApprovals?: boolean;
|
|
92
96
|
}
|
|
93
97
|
/**
|
|
94
98
|
* 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
|
@@ -129,6 +129,15 @@ class ChatSession {
|
|
|
129
129
|
this.handleError(err);
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Resolve a pending tool-approval bubble. The SDK owns the transport
|
|
134
|
+
* call so hosts don't have to know the approval endpoint or assemble
|
|
135
|
+
* the `browserSessionId` payload. Errors propagate so the calling View
|
|
136
|
+
* layer can keep the bubble in pending state and show the message.
|
|
137
|
+
*/
|
|
138
|
+
async resolveApproval(approvalId, action) {
|
|
139
|
+
await this.transport.resolveApproval(approvalId, action, this.browserSessionId);
|
|
140
|
+
}
|
|
132
141
|
/** Tear down. Future sends throw. Useful for SPA unmount. */
|
|
133
142
|
destroy() {
|
|
134
143
|
this.listeners.clear();
|
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.21",
|
|
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",
|