@agentforge-io/chat-sdk 2.0.17 → 2.0.18
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 +23 -0
- package/dist/react.js +112 -3
- package/package.json +1 -1
package/dist/react.d.ts
CHANGED
|
@@ -17,6 +17,23 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { type CSSProperties } from 'react';
|
|
19
19
|
import type { ChatTheme } from './entities';
|
|
20
|
+
/**
|
|
21
|
+
* Optional callback invoked when the visitor clicks the Approve/Deny
|
|
22
|
+
* button on a tool-approval bubble. When omitted, the bubble renders
|
|
23
|
+
* as a read-only "waiting" hint — the visitor can't decide from inside
|
|
24
|
+
* the widget. When wired, the widget calls this with `{approvalId,
|
|
25
|
+
* action}` and the host implementation typically POSTs to
|
|
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.
|
|
32
|
+
*/
|
|
33
|
+
export type ApprovalActionHandler = (args: {
|
|
34
|
+
approvalId: string;
|
|
35
|
+
action: 'approve' | 'deny';
|
|
36
|
+
}) => Promise<void>;
|
|
20
37
|
export interface ChatWidgetProps {
|
|
21
38
|
/** Public chat token (`aft_*`) issued from the admin UI. */
|
|
22
39
|
token: string;
|
|
@@ -38,6 +55,12 @@ export interface ChatWidgetProps {
|
|
|
38
55
|
className?: string;
|
|
39
56
|
/** Inline style on the root container. */
|
|
40
57
|
style?: CSSProperties;
|
|
58
|
+
/**
|
|
59
|
+
* Handler for tool-approval bubble actions. See `ApprovalActionHandler`
|
|
60
|
+
* for the contract. Omit to render approval messages as read-only
|
|
61
|
+
* hints.
|
|
62
|
+
*/
|
|
63
|
+
onApprovalAction?: ApprovalActionHandler;
|
|
41
64
|
}
|
|
42
65
|
/**
|
|
43
66
|
* Drop-in chat widget. Owns its own `ChatSession` and re-renders on every
|
package/dist/react.js
CHANGED
|
@@ -158,7 +158,7 @@ function renderMarkdown(src) {
|
|
|
158
158
|
* SDK event. Consumers don't need to read `session.getState()` themselves.
|
|
159
159
|
*/
|
|
160
160
|
function ChatWidget(props) {
|
|
161
|
-
const { token, apiBaseUrl, inline = false, position, browserSessionId, resumeConversationId, stream, className, style, } = props;
|
|
161
|
+
const { token, apiBaseUrl, inline = false, position, browserSessionId, resumeConversationId, stream, className, style, onApprovalAction, } = props;
|
|
162
162
|
const [session, setSession] = (0, react_1.useState)(null);
|
|
163
163
|
const [status, setStatus] = (0, react_1.useState)('idle');
|
|
164
164
|
const [agent, setAgent] = (0, react_1.useState)();
|
|
@@ -278,9 +278,31 @@ function ChatWidget(props) {
|
|
|
278
278
|
};
|
|
279
279
|
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 ? (
|
|
280
280
|
// eslint-disable-next-line @next/next/no-img-element
|
|
281
|
-
(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
|
|
281
|
+
(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, onApprovalAction: onApprovalAction, onContinue: () => {
|
|
282
|
+
// After a successful Approve, kick the next turn so the
|
|
283
|
+
// gate's fast-path consumes the approval and the tool
|
|
284
|
+
// actually runs. Delay = let the "Approved" pill flash
|
|
285
|
+
// for a beat before the assistant bubble appears.
|
|
286
|
+
if (!session)
|
|
287
|
+
return;
|
|
288
|
+
setTimeout(() => {
|
|
289
|
+
void session.send('continue');
|
|
290
|
+
}, 250);
|
|
291
|
+
} }, 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" })] })] }));
|
|
282
292
|
}
|
|
283
|
-
function MessageBubble({ message }) {
|
|
293
|
+
function MessageBubble({ message, onApprovalAction, onContinue, }) {
|
|
294
|
+
// Structured metadata takes precedence over plain text. The server
|
|
295
|
+
// tags messages with `kind: 'awaiting_approval' | 'tool_blocked'`
|
|
296
|
+
// and the widget renders a dedicated bubble for each — unknown
|
|
297
|
+
// kinds fall through to the markdown renderer with the plain text
|
|
298
|
+
// body the server also persists.
|
|
299
|
+
const kind = message.metadata?.kind;
|
|
300
|
+
if (kind === 'awaiting_approval') {
|
|
301
|
+
return ((0, jsx_runtime_1.jsx)(ApprovalBubble, { message: message, onAction: onApprovalAction, onContinue: onContinue }));
|
|
302
|
+
}
|
|
303
|
+
if (kind === 'tool_blocked') {
|
|
304
|
+
return (0, jsx_runtime_1.jsx)(BlockedBubble, { message: message });
|
|
305
|
+
}
|
|
284
306
|
const cls = `af-msg af-msg-${message.role}${message.role === 'assistant' && message.isStreaming
|
|
285
307
|
? message.content
|
|
286
308
|
? ' af-msg-streaming'
|
|
@@ -299,6 +321,72 @@ function MessageBubble({ message }) {
|
|
|
299
321
|
// User & system messages stay as plain text — they're typed verbatim.
|
|
300
322
|
return (0, jsx_runtime_1.jsx)("div", { className: cls, children: message.content });
|
|
301
323
|
}
|
|
324
|
+
/**
|
|
325
|
+
* Awaiting-approval bubble. Renders the tool name + a countdown, and
|
|
326
|
+
* either the Approve/Deny buttons (when the host wired the handler)
|
|
327
|
+
* or a read-only hint. After a successful approve, fires `onContinue`
|
|
328
|
+
* so the chat auto-sends "continue" and the gate's fast-path consumes
|
|
329
|
+
* the row on the next turn.
|
|
330
|
+
*/
|
|
331
|
+
function ApprovalBubble({ message, onAction, onContinue, }) {
|
|
332
|
+
const meta = message.metadata;
|
|
333
|
+
const [busy, setBusy] = (0, react_1.useState)(null);
|
|
334
|
+
const [decided, setDecided] = (0, react_1.useState)(null);
|
|
335
|
+
const [err, setErr] = (0, react_1.useState)(null);
|
|
336
|
+
const remaining = useExpiresIn(meta?.expiresAt);
|
|
337
|
+
if (!meta)
|
|
338
|
+
return null;
|
|
339
|
+
const act = async (action) => {
|
|
340
|
+
if (!onAction)
|
|
341
|
+
return;
|
|
342
|
+
setBusy(action);
|
|
343
|
+
setErr(null);
|
|
344
|
+
try {
|
|
345
|
+
await onAction({ approvalId: meta.approvalId, action });
|
|
346
|
+
setDecided(action === 'approve' ? 'approved' : 'denied');
|
|
347
|
+
if (action === 'approve')
|
|
348
|
+
onContinue();
|
|
349
|
+
}
|
|
350
|
+
catch (e) {
|
|
351
|
+
setErr(e instanceof Error ? e.message : String(e));
|
|
352
|
+
}
|
|
353
|
+
finally {
|
|
354
|
+
setBusy(null);
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
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: "Awaiting approval" }), (0, jsx_runtime_1.jsxs)("div", { className: "af-approval-body", children: ["The agent wants to run", ' ', (0, jsx_runtime_1.jsx)("code", { className: "af-approval-tool", children: meta.toolName }), "."] }), 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' ? 'Approved — retrying…' : 'Denied' })), !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' ? 'Approving…' : 'Approve' }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "af-approval-btn af-approval-deny", disabled: busy !== null, onClick: () => act('deny'), children: busy === 'deny' ? 'Denying…' : 'Deny' })] })), !decided && !onAction && ((0, jsx_runtime_1.jsx)("div", { className: "af-approval-hint", children: "A workspace admin needs to review this." })), err && (0, jsx_runtime_1.jsx)("div", { className: "af-approval-error", children: err })] }));
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Terminal "tool blocked" bubble. No action — just informs the visitor
|
|
361
|
+
* that the tool the agent tried isn't available.
|
|
362
|
+
*/
|
|
363
|
+
function BlockedBubble({ message }) {
|
|
364
|
+
const meta = message.metadata;
|
|
365
|
+
if (!meta)
|
|
366
|
+
return null;
|
|
367
|
+
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: "Tool blocked" }), (0, jsx_runtime_1.jsxs)("div", { className: "af-blocked-body", children: [(0, jsx_runtime_1.jsx)("code", { className: "af-approval-tool", children: meta.toolName }), " is blocked by the workspace admin.", ' ', meta.reason ?? ''] })] }));
|
|
368
|
+
}
|
|
369
|
+
/** Live "expires in" countdown. Returns null after the deadline. */
|
|
370
|
+
function useExpiresIn(iso) {
|
|
371
|
+
const [, force] = (0, react_1.useState)(0);
|
|
372
|
+
(0, react_1.useEffect)(() => {
|
|
373
|
+
if (!iso)
|
|
374
|
+
return;
|
|
375
|
+
const t = setInterval(() => force((n) => n + 1), 1000);
|
|
376
|
+
return () => clearInterval(t);
|
|
377
|
+
}, [iso]);
|
|
378
|
+
if (!iso)
|
|
379
|
+
return null;
|
|
380
|
+
const ms = new Date(iso).getTime() - Date.now();
|
|
381
|
+
if (!Number.isFinite(ms) || ms <= 0)
|
|
382
|
+
return null;
|
|
383
|
+
const totalSeconds = Math.floor(ms / 1000);
|
|
384
|
+
const m = Math.floor(totalSeconds / 60);
|
|
385
|
+
const s = totalSeconds % 60;
|
|
386
|
+
if (m >= 60)
|
|
387
|
+
return `Expires in ${Math.floor(m / 60)}h ${m % 60}m`;
|
|
388
|
+
return `Expires in ${m}m ${s.toString().padStart(2, '0')}s`;
|
|
389
|
+
}
|
|
302
390
|
function ChatIcon() {
|
|
303
391
|
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" }) }));
|
|
304
392
|
}
|
|
@@ -402,4 +490,25 @@ const WIDGET_CSS = `
|
|
|
402
490
|
.af-send svg { width: 16px; height: 16px; }
|
|
403
491
|
.af-error { padding: 10px 16px; font-size: 12px; color: #b91c1c; background: #fef2f2; border-top: 1px solid #fee2e2; }
|
|
404
492
|
.af-footer { padding: 6px 12px; font-size: 10px; color: var(--af-muted); text-align: center; background: var(--af-bubble-bg); border-top: 1px solid var(--af-border); }
|
|
493
|
+
/* Approval and blocked bubbles. Amber for needs-decision, red for
|
|
494
|
+
* won't-happen. Same animation-in as the regular .af-msg so the
|
|
495
|
+
* transition feels uniform. */
|
|
496
|
+
.af-msg-approval { align-self: flex-start; max-width: 90%; padding: 12px 14px; border-radius: 14px; border-bottom-left-radius: 4px; font-size: 13px; line-height: 1.5; background: #fffbeb; border: 1px solid #fde68a; color: #7c2d12; animation: af-msg-in 220ms cubic-bezier(0.2, 0.8, 0.2, 1); }
|
|
497
|
+
.af-approval-title { font-weight: 600; font-size: 13px; margin-bottom: 4px; }
|
|
498
|
+
.af-approval-body { font-size: 13px; line-height: 1.5; }
|
|
499
|
+
.af-approval-tool { background: rgba(0,0,0,0.06); padding: 1px 6px; border-radius: 4px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
|
|
500
|
+
.af-approval-meta { font-size: 11px; margin-top: 6px; color: #92400e; }
|
|
501
|
+
.af-approval-actions { display: flex; gap: 8px; margin-top: 10px; }
|
|
502
|
+
.af-approval-btn { padding: 6px 12px; font-size: 12px; font-weight: 600; border-radius: 8px; cursor: pointer; transition: opacity 0.15s ease; }
|
|
503
|
+
.af-approval-btn:disabled { opacity: 0.6; cursor: not-allowed; }
|
|
504
|
+
.af-approval-approve { background: var(--af-primary); color: white; border: none; }
|
|
505
|
+
.af-approval-deny { background: white; color: #7c2d12; border: 1px solid #fde68a; }
|
|
506
|
+
.af-approval-pill { display: inline-block; margin-top: 8px; padding: 2px 8px; border-radius: 999px; font-size: 11px; font-weight: 600; }
|
|
507
|
+
.af-approval-pill-approved { background: #dcfce7; color: #166534; }
|
|
508
|
+
.af-approval-pill-denied { background: #fee2e2; color: #991b1b; }
|
|
509
|
+
.af-approval-hint { font-size: 12px; margin-top: 8px; color: #7c2d12; }
|
|
510
|
+
.af-approval-error { font-size: 11px; margin-top: 6px; color: #b91c1c; }
|
|
511
|
+
.af-msg-blocked { align-self: flex-start; max-width: 90%; padding: 10px 14px; border-radius: 14px; border-bottom-left-radius: 4px; font-size: 13px; line-height: 1.5; background: #fef2f2; border: 1px solid #fecaca; color: #7f1d1d; animation: af-msg-in 220ms cubic-bezier(0.2, 0.8, 0.2, 1); }
|
|
512
|
+
.af-blocked-title { font-weight: 600; font-size: 13px; margin-bottom: 4px; }
|
|
513
|
+
.af-blocked-body { font-size: 12px; line-height: 1.5; }
|
|
405
514
|
`;
|
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.18",
|
|
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",
|