@agentforge-io/chat-sdk 2.0.19 → 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/entities.d.ts +20 -6
- package/dist/react.d.ts +31 -41
- package/dist/react.js +53 -47
- package/dist/session.d.ts +7 -0
- package/dist/session.js +16 -10
- package/dist/transport.d.ts +9 -0
- package/dist/transport.js +20 -0
- package/package.json +1 -1
package/dist/entities.d.ts
CHANGED
|
@@ -19,25 +19,39 @@ export type ChatRole = 'user' | 'assistant' | 'system';
|
|
|
19
19
|
* Future kinds extend this union without breaking back-compat: the
|
|
20
20
|
* View checks `kind` and falls back to plain text when it's unknown.
|
|
21
21
|
*/
|
|
22
|
+
export interface ChatApprovalCopy {
|
|
23
|
+
title: string;
|
|
24
|
+
body: string;
|
|
25
|
+
approveLabel: string;
|
|
26
|
+
approveBusyLabel: string;
|
|
27
|
+
denyLabel: string;
|
|
28
|
+
denyBusyLabel: string;
|
|
29
|
+
approvedPill: string;
|
|
30
|
+
deniedPill: string;
|
|
31
|
+
readOnlyHint: string;
|
|
32
|
+
blockedTitle: string;
|
|
33
|
+
blockedBody: string;
|
|
34
|
+
expiresPrefix: string;
|
|
35
|
+
}
|
|
22
36
|
export type ChatMessageMetadata = {
|
|
23
37
|
kind: 'awaiting_approval';
|
|
24
38
|
approvalId: string;
|
|
25
39
|
toolName: string;
|
|
26
40
|
expiresAt: string;
|
|
27
|
-
/** Friendly connector display name surfaced by the server when
|
|
28
|
-
* available (e.g. `'Granola'`). Lets the View render a
|
|
29
|
-
* natural-language card without the host having to map slugs
|
|
30
|
-
* to product names. */
|
|
31
41
|
connectorName?: string;
|
|
32
|
-
/** First line of the tool's description from the connector
|
|
33
|
-
* definition. Used as a one-line "what will happen" hint. */
|
|
34
42
|
toolDescription?: string;
|
|
43
|
+
/** Server-authored microcopy for the approval bubble. Generated
|
|
44
|
+
* on every pause by an AI copywriter on the backend so language
|
|
45
|
+
* and tone match the live conversation. View layers render it
|
|
46
|
+
* verbatim; when missing, fall back to a minimal meta render. */
|
|
47
|
+
copy?: ChatApprovalCopy;
|
|
35
48
|
} | {
|
|
36
49
|
kind: 'tool_blocked';
|
|
37
50
|
toolName: string;
|
|
38
51
|
reason?: string;
|
|
39
52
|
connectorName?: string;
|
|
40
53
|
toolDescription?: string;
|
|
54
|
+
copy?: ChatApprovalCopy;
|
|
41
55
|
} | {
|
|
42
56
|
kind: string;
|
|
43
57
|
[k: string]: unknown;
|
package/dist/react.d.ts
CHANGED
|
@@ -18,57 +18,45 @@
|
|
|
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,
|
|
40
|
-
*
|
|
36
|
+
* Server-authored approval microcopy. The bubble renders whatever the
|
|
37
|
+
* server places in `metadata.copy` — title, body, button labels, etc.
|
|
38
|
+
* That copy is generated by the AgentForge backend on every pause via a
|
|
39
|
+
* dedicated Haiku call, so its language and tone always match the
|
|
40
|
+
* conversation the user is having. There is no client-side dictionary
|
|
41
|
+
* to keep in sync.
|
|
41
42
|
*
|
|
42
|
-
*
|
|
43
|
-
* with — typically a friendly connector name like `'Granola'`. When
|
|
44
|
-
* the server didn't supply it, the function is called with `undefined`
|
|
45
|
-
* and the implementation should pick a sensible generic line. The
|
|
46
|
-
* widget itself never references the raw tool slug in copy.
|
|
43
|
+
* The shape mirrors `ApprovalCopyBundle` in `@agentforge-io/core`.
|
|
47
44
|
*/
|
|
48
45
|
export interface ApprovalCopy {
|
|
49
46
|
title: string;
|
|
50
|
-
body:
|
|
51
|
-
connectorName?: string;
|
|
52
|
-
toolDescription?: string;
|
|
53
|
-
}) => string;
|
|
47
|
+
body: string;
|
|
54
48
|
approveLabel: string;
|
|
55
49
|
approveBusyLabel: string;
|
|
56
50
|
denyLabel: string;
|
|
57
51
|
denyBusyLabel: string;
|
|
58
52
|
approvedPill: string;
|
|
59
53
|
deniedPill: string;
|
|
60
|
-
/** Shown when the host didn't wire `onApprovalAction` — the visitor
|
|
61
|
-
* can't decide from inside the widget so we tell them where to go. */
|
|
62
54
|
readOnlyHint: string;
|
|
63
|
-
/** Used by the `expires in …` countdown. Receives minutes/seconds
|
|
64
|
-
* and returns the formatted string. */
|
|
65
|
-
expiresIn: (mins: number, secs: number) => string;
|
|
66
|
-
/** Block bubble title + body. */
|
|
67
55
|
blockedTitle: string;
|
|
68
|
-
blockedBody:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
56
|
+
blockedBody: string;
|
|
57
|
+
/** Prefix shown before the countdown, e.g. "Expires in". The seconds
|
|
58
|
+
* are appended client-side because they tick locally. */
|
|
59
|
+
expiresPrefix: string;
|
|
72
60
|
}
|
|
73
61
|
export interface ChatWidgetProps {
|
|
74
62
|
/** Public chat token (`aft_*`) issued from the admin UI. */
|
|
@@ -92,17 +80,19 @@ export interface ChatWidgetProps {
|
|
|
92
80
|
/** Inline style on the root container. */
|
|
93
81
|
style?: CSSProperties;
|
|
94
82
|
/**
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
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`.
|
|
98
87
|
*/
|
|
99
|
-
|
|
88
|
+
onApprovalDecision?: ApprovalDecisionObserver;
|
|
100
89
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
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.
|
|
104
94
|
*/
|
|
105
|
-
|
|
95
|
+
readOnlyApprovals?: boolean;
|
|
106
96
|
}
|
|
107
97
|
/**
|
|
108
98
|
* Drop-in chat widget. Owns its own `ChatSession` and re-renders on every
|
package/dist/react.js
CHANGED
|
@@ -154,37 +154,34 @@ function renderMarkdown(src) {
|
|
|
154
154
|
return out.join('');
|
|
155
155
|
}
|
|
156
156
|
/**
|
|
157
|
-
*
|
|
158
|
-
*
|
|
157
|
+
* Minimal fallback used when the server didn't supply `metadata.copy`
|
|
158
|
+
* (older backend, copywriter timeout, missing API key, etc.). Intentionally
|
|
159
|
+
* terse and meta-only — the goal is to avoid lying about language. The
|
|
160
|
+
* server is the source of truth for natural copy.
|
|
159
161
|
*/
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
:
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
:
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
(connectorName
|
|
178
|
-
? `No puedo usar ${connectorName} en esta cuenta.`
|
|
179
|
-
: 'No puedo usar esa herramienta en esta cuenta.'),
|
|
180
|
-
};
|
|
162
|
+
function fallbackCopy(ctx) {
|
|
163
|
+
const target = ctx.connectorName ?? ctx.toolName ?? '';
|
|
164
|
+
return {
|
|
165
|
+
title: target || 'Permission required',
|
|
166
|
+
body: target ? `${target} → ✓ / ✗` : 'Permission required',
|
|
167
|
+
approveLabel: '✓',
|
|
168
|
+
approveBusyLabel: '…',
|
|
169
|
+
denyLabel: '✗',
|
|
170
|
+
denyBusyLabel: '…',
|
|
171
|
+
approvedPill: '✓',
|
|
172
|
+
deniedPill: '✗',
|
|
173
|
+
readOnlyHint: '',
|
|
174
|
+
blockedTitle: target || 'Blocked',
|
|
175
|
+
blockedBody: ctx.reason ?? (target ? `${target} blocked` : 'Blocked'),
|
|
176
|
+
expiresPrefix: '⏱',
|
|
177
|
+
};
|
|
178
|
+
}
|
|
181
179
|
/**
|
|
182
180
|
* Drop-in chat widget. Owns its own `ChatSession` and re-renders on every
|
|
183
181
|
* SDK event. Consumers don't need to read `session.getState()` themselves.
|
|
184
182
|
*/
|
|
185
183
|
function ChatWidget(props) {
|
|
186
|
-
const { token, apiBaseUrl, inline = false, position, browserSessionId, resumeConversationId, stream, className, style,
|
|
187
|
-
const copy = { ...DEFAULT_APPROVAL_COPY, ...(approvalCopy ?? {}) };
|
|
184
|
+
const { token, apiBaseUrl, inline = false, position, browserSessionId, resumeConversationId, stream, className, style, onApprovalDecision, readOnlyApprovals = false, } = props;
|
|
188
185
|
const [session, setSession] = (0, react_1.useState)(null);
|
|
189
186
|
const [status, setStatus] = (0, react_1.useState)('idle');
|
|
190
187
|
const [agent, setAgent] = (0, react_1.useState)();
|
|
@@ -304,7 +301,7 @@ function ChatWidget(props) {
|
|
|
304
301
|
};
|
|
305
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 ? (
|
|
306
303
|
// eslint-disable-next-line @next/next/no-img-element
|
|
307
|
-
(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: () => {
|
|
308
305
|
// After a successful Approve, kick the next turn so the
|
|
309
306
|
// gate's fast-path consumes the approval and the tool
|
|
310
307
|
// actually runs. `silent: true` keeps the literal
|
|
@@ -318,13 +315,13 @@ function ChatWidget(props) {
|
|
|
318
315
|
}, 250);
|
|
319
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" })] })] }));
|
|
320
317
|
}
|
|
321
|
-
function MessageBubble({ message,
|
|
318
|
+
function MessageBubble({ message, session, readOnly, onDecision, onContinue, }) {
|
|
322
319
|
const kind = message.metadata?.kind;
|
|
323
320
|
if (kind === 'awaiting_approval') {
|
|
324
|
-
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 }));
|
|
325
322
|
}
|
|
326
323
|
if (kind === 'tool_blocked') {
|
|
327
|
-
return (0, jsx_runtime_1.jsx)(BlockedBubble, { message: message
|
|
324
|
+
return (0, jsx_runtime_1.jsx)(BlockedBubble, { message: message });
|
|
328
325
|
}
|
|
329
326
|
const cls = `af-msg af-msg-${message.role}${message.role === 'assistant' && message.isStreaming
|
|
330
327
|
? message.content
|
|
@@ -351,54 +348,60 @@ function MessageBubble({ message, copy, onApprovalAction, onContinue, }) {
|
|
|
351
348
|
* so the chat auto-sends "continue" and the gate's fast-path consumes
|
|
352
349
|
* the row on the next turn.
|
|
353
350
|
*/
|
|
354
|
-
function ApprovalBubble({ message,
|
|
351
|
+
function ApprovalBubble({ message, session, readOnly, onDecision, onContinue, }) {
|
|
355
352
|
const meta = message.metadata;
|
|
356
353
|
const [busy, setBusy] = (0, react_1.useState)(null);
|
|
357
354
|
const [decided, setDecided] = (0, react_1.useState)(null);
|
|
358
355
|
const [err, setErr] = (0, react_1.useState)(null);
|
|
359
|
-
const
|
|
356
|
+
const copy = meta?.copy ?? fallbackCopy({
|
|
357
|
+
connectorName: meta?.connectorName,
|
|
358
|
+
toolName: meta?.toolName,
|
|
359
|
+
});
|
|
360
|
+
const remaining = useExpiresIn(meta?.expiresAt, copy.expiresPrefix);
|
|
360
361
|
if (!meta)
|
|
361
362
|
return null;
|
|
362
363
|
const act = async (action) => {
|
|
363
|
-
if (!
|
|
364
|
+
if (!session)
|
|
364
365
|
return;
|
|
365
366
|
setBusy(action);
|
|
366
367
|
setErr(null);
|
|
367
368
|
try {
|
|
368
|
-
await
|
|
369
|
+
await session.resolveApproval(meta.approvalId, action);
|
|
369
370
|
setDecided(action === 'approve' ? 'approved' : 'denied');
|
|
371
|
+
onDecision?.({ approvalId: meta.approvalId, action, ok: true });
|
|
370
372
|
if (action === 'approve')
|
|
371
373
|
onContinue();
|
|
372
374
|
}
|
|
373
375
|
catch (e) {
|
|
374
|
-
|
|
376
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
377
|
+
setErr(message);
|
|
378
|
+
onDecision?.({ approvalId: meta.approvalId, action, ok: false, error: message });
|
|
375
379
|
}
|
|
376
380
|
finally {
|
|
377
381
|
setBusy(null);
|
|
378
382
|
}
|
|
379
383
|
};
|
|
380
|
-
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({
|
|
381
|
-
connectorName: meta.connectorName,
|
|
382
|
-
toolDescription: meta.toolDescription,
|
|
383
|
-
}) }), 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 && onAction && ((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 && !onAction && ((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 })] }));
|
|
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 })] }));
|
|
384
385
|
}
|
|
385
386
|
/**
|
|
386
387
|
* Terminal "tool blocked" bubble. No action — just informs the visitor
|
|
387
388
|
* that the tool the agent tried isn't available.
|
|
388
389
|
*/
|
|
389
|
-
function BlockedBubble({ message,
|
|
390
|
+
function BlockedBubble({ message, }) {
|
|
390
391
|
const meta = message.metadata;
|
|
391
392
|
if (!meta)
|
|
392
393
|
return null;
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
394
|
+
const copy = meta.copy ?? fallbackCopy({
|
|
395
|
+
connectorName: meta.connectorName,
|
|
396
|
+
toolName: meta.toolName,
|
|
397
|
+
reason: meta.reason,
|
|
398
|
+
});
|
|
399
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "af-msg af-msg-blocked", children: [(0, jsx_runtime_1.jsx)("div", { className: "af-blocked-title", children: copy.blockedTitle }), (0, jsx_runtime_1.jsx)("div", { className: "af-blocked-body", children: copy.blockedBody })] }));
|
|
397
400
|
}
|
|
398
401
|
/** Live "expires in" countdown. Returns null after the deadline.
|
|
399
|
-
* The
|
|
400
|
-
* the hook
|
|
401
|
-
function useExpiresIn(iso,
|
|
402
|
+
* The prefix label is server-authored so the language matches the
|
|
403
|
+
* rest of the bubble; the hook just appends the localised digits. */
|
|
404
|
+
function useExpiresIn(iso, prefix) {
|
|
402
405
|
const [, force] = (0, react_1.useState)(0);
|
|
403
406
|
(0, react_1.useEffect)(() => {
|
|
404
407
|
if (!iso)
|
|
@@ -414,7 +417,10 @@ function useExpiresIn(iso, format) {
|
|
|
414
417
|
const totalSeconds = Math.floor(ms / 1000);
|
|
415
418
|
const m = Math.floor(totalSeconds / 60);
|
|
416
419
|
const s = totalSeconds % 60;
|
|
417
|
-
|
|
420
|
+
const tail = m >= 60
|
|
421
|
+
? `${Math.floor(m / 60)}h ${m % 60}m`
|
|
422
|
+
: `${m}m ${s.toString().padStart(2, '0')}s`;
|
|
423
|
+
return `${prefix} ${tail}`.trim();
|
|
418
424
|
}
|
|
419
425
|
function ChatIcon() {
|
|
420
426
|
return ((0, jsx_runtime_1.jsx)("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", "aria-hidden": true, children: (0, jsx_runtime_1.jsx)("path", { d: "M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" }) }));
|
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();
|
|
@@ -214,12 +223,12 @@ class ChatSession {
|
|
|
214
223
|
expiresAt: c.expiresAt,
|
|
215
224
|
connectorName: c.connectorName,
|
|
216
225
|
toolDescription: c.toolDescription,
|
|
226
|
+
copy: c.copy,
|
|
217
227
|
};
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
: `Necesito tu permiso para continuar.`);
|
|
228
|
+
// Plain-text fallback for legacy clients that don't render
|
|
229
|
+
// metadata.kind. Capable widgets ignore `content` and read
|
|
230
|
+
// `metadata.copy` directly.
|
|
231
|
+
assistant.content = full || c.copy?.body || (c.connectorName ?? '');
|
|
223
232
|
this.updateMessage(assistant);
|
|
224
233
|
continue;
|
|
225
234
|
}
|
|
@@ -231,13 +240,10 @@ class ChatSession {
|
|
|
231
240
|
reason: c.reason,
|
|
232
241
|
connectorName: c.connectorName,
|
|
233
242
|
toolDescription: c.toolDescription,
|
|
243
|
+
copy: c.copy,
|
|
234
244
|
};
|
|
235
245
|
assistant.content =
|
|
236
|
-
full ||
|
|
237
|
-
c.reason ||
|
|
238
|
-
(c.connectorName
|
|
239
|
-
? `No puedo usar ${c.connectorName} en esta cuenta.`
|
|
240
|
-
: `No puedo usar esa herramienta en esta cuenta.`);
|
|
246
|
+
full || c.copy?.blockedBody || c.reason || (c.connectorName ?? '');
|
|
241
247
|
this.updateMessage(assistant);
|
|
242
248
|
continue;
|
|
243
249
|
}
|
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",
|