@agentforge-io/chat-react 2.0.15 → 2.0.17

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.
@@ -1,4 +1,19 @@
1
1
  import { type CSSProperties } from 'react';
2
+ /**
3
+ * Optional handler the host wires when the chat panel is embedded
4
+ * inside an admin surface (e.g. the dashboard's agent playground).
5
+ *
6
+ * When provided, an `awaiting_approval` bubble renders Approve/Deny
7
+ * buttons that call this function — the host implementation typically
8
+ * POSTs to `/admin/tenants/:tid/approvals/:id/{approve,deny}` and then
9
+ * resolves so the panel knows to auto-continue. When omitted (e.g.
10
+ * a public-chat embed where the visitor isn't an admin), the bubble
11
+ * renders a read-only "waiting" state with a link to `/approvals`.
12
+ */
13
+ export type ApprovalActionHandler = (args: {
14
+ approvalId: string;
15
+ action: 'approve' | 'deny';
16
+ }) => Promise<void>;
2
17
  export interface ChatPanelProps {
3
18
  /** Override the header title. Falls back to theme.title, then agent.name. */
4
19
  title?: string;
@@ -11,6 +26,9 @@ export interface ChatPanelProps {
11
26
  * the height. */
12
27
  style?: CSSProperties;
13
28
  className?: string;
29
+ /** Wire an admin-side handler to render Approve/Deny buttons on
30
+ * `awaiting_approval` bubbles. See `ApprovalActionHandler`. */
31
+ onApprovalAction?: ApprovalActionHandler;
14
32
  }
15
33
  /**
16
34
  * Reference rendering of a chat panel. Pure React, no portal, no fixed
@@ -18,4 +36,4 @@ export interface ChatPanelProps {
18
36
  * (modal, sidebar, page section). For the floating launcher pattern, see
19
37
  * the script-tag widget at /widget.js.
20
38
  */
21
- export declare function ChatPanel({ title, placeholder, subtitle, style, className, }: ChatPanelProps): import("react/jsx-runtime").JSX.Element;
39
+ export declare function ChatPanel({ title, placeholder, subtitle, style, className, onApprovalAction, }: ChatPanelProps): import("react/jsx-runtime").JSX.Element;
package/dist/ChatPanel.js CHANGED
@@ -11,7 +11,7 @@ const hooks_1 = require("./hooks");
11
11
  * (modal, sidebar, page section). For the floating launcher pattern, see
12
12
  * the script-tag widget at /widget.js.
13
13
  */
14
- function ChatPanel({ title, placeholder = 'Type a message…', subtitle, style, className, }) {
14
+ function ChatPanel({ title, placeholder = 'Type a message…', subtitle, style, className, onApprovalAction, }) {
15
15
  const { agent, theme } = (0, hooks_1.useAgent)();
16
16
  const { messages, status, send, isBusy, error } = (0, hooks_1.useChat)();
17
17
  const resolvedTitle = title ?? theme?.title ?? agent?.name ?? 'Chat';
@@ -81,7 +81,20 @@ function ChatPanel({ title, placeholder = 'Type a message…', subtitle, style,
81
81
  display: 'flex',
82
82
  flexDirection: 'column',
83
83
  gap: 10,
84
- }, children: messages.map((m) => ((0, jsx_runtime_1.jsx)(Bubble, { role: m.role, streaming: m.isStreaming, primary: primary, children: m.content }, m.id))) }), error && ((0, jsx_runtime_1.jsx)("div", { style: {
84
+ }, children: messages.map((m) => {
85
+ // Branch on structured metadata before falling back to the
86
+ // plain-text bubble. Unknown kinds also fall through — the
87
+ // body still carries the human-readable line the server
88
+ // generated.
89
+ const kind = m.metadata?.kind;
90
+ if (kind === 'awaiting_approval') {
91
+ return ((0, jsx_runtime_1.jsx)(ApprovalCardBubble, { message: m, primary: primary, onAction: onApprovalAction, onContinue: () => send('continue') }, m.id));
92
+ }
93
+ if (kind === 'tool_blocked') {
94
+ return ((0, jsx_runtime_1.jsx)(BlockedCardBubble, { message: m }, m.id));
95
+ }
96
+ return ((0, jsx_runtime_1.jsx)(Bubble, { role: m.role, streaming: m.isStreaming, primary: primary, children: m.content }, m.id));
97
+ }) }), error && ((0, jsx_runtime_1.jsx)("div", { style: {
85
98
  padding: '10px 16px',
86
99
  fontSize: 12,
87
100
  color: '#b91c1c',
@@ -183,3 +196,137 @@ function hexToRgba(hex, alpha) {
183
196
  const b = parseInt(h.slice(4, 6), 16);
184
197
  return `rgba(${r},${g},${b},${alpha})`;
185
198
  }
199
+ /**
200
+ * Bubble variant for `metadata.kind === 'awaiting_approval'`. Shows the
201
+ * tool name + a countdown, and either Approve/Deny buttons (when the
202
+ * host wired `onApprovalAction`) or a read-only hint pointing the user
203
+ * to `/approvals`. After a successful Approve, calls `onContinue` so
204
+ * the chat sends a follow-up turn that consumes the approval through
205
+ * the gate's fast path.
206
+ */
207
+ function ApprovalCardBubble({ message, primary, onAction, onContinue, }) {
208
+ const meta = message.metadata;
209
+ const [busy, setBusy] = (0, react_1.useState)(null);
210
+ const [decided, setDecided] = (0, react_1.useState)(null);
211
+ const [err, setErr] = (0, react_1.useState)(null);
212
+ const remaining = useExpiresIn(meta?.expiresAt);
213
+ if (!meta)
214
+ return null;
215
+ const act = async (action) => {
216
+ if (!onAction)
217
+ return;
218
+ setBusy(action);
219
+ setErr(null);
220
+ try {
221
+ await onAction({ approvalId: meta.approvalId, action });
222
+ setDecided(action === 'approve' ? 'approved' : 'denied');
223
+ if (action === 'approve') {
224
+ // Small delay so the user sees the "Approved" pill flash
225
+ // before the next turn starts streaming on top.
226
+ setTimeout(onContinue, 250);
227
+ }
228
+ }
229
+ catch (e) {
230
+ setErr(e instanceof Error ? e.message : String(e));
231
+ }
232
+ finally {
233
+ setBusy(null);
234
+ }
235
+ };
236
+ return ((0, jsx_runtime_1.jsxs)("div", { style: {
237
+ alignSelf: 'flex-start',
238
+ maxWidth: '90%',
239
+ padding: '12px 14px',
240
+ borderRadius: 14,
241
+ borderBottomLeftRadius: 4,
242
+ fontSize: 14,
243
+ background: '#fffbeb',
244
+ border: '1px solid #fde68a',
245
+ color: '#7c2d12',
246
+ }, children: [(0, jsx_runtime_1.jsx)("div", { style: { fontWeight: 600, fontSize: 13, marginBottom: 4 }, children: "Awaiting approval" }), (0, jsx_runtime_1.jsxs)("div", { style: { fontSize: 13, lineHeight: 1.5 }, children: ["The agent wants to run", ' ', (0, jsx_runtime_1.jsx)("code", { style: {
247
+ background: 'rgba(0,0,0,0.06)',
248
+ padding: '1px 6px',
249
+ borderRadius: 4,
250
+ fontSize: 12,
251
+ }, children: meta.toolName }), "."] }), remaining && !decided && ((0, jsx_runtime_1.jsx)("div", { style: { fontSize: 11, marginTop: 6, color: '#92400e' }, children: remaining })), decided && ((0, jsx_runtime_1.jsx)("div", { style: {
252
+ display: 'inline-block',
253
+ marginTop: 8,
254
+ padding: '2px 8px',
255
+ borderRadius: 999,
256
+ fontSize: 11,
257
+ fontWeight: 600,
258
+ background: decided === 'approved' ? '#dcfce7' : '#fee2e2',
259
+ color: decided === 'approved' ? '#166534' : '#991b1b',
260
+ }, children: decided === 'approved' ? 'Approved — retrying…' : 'Denied' })), !decided && onAction && ((0, jsx_runtime_1.jsxs)("div", { style: { display: 'flex', gap: 8, marginTop: 10 }, children: [(0, jsx_runtime_1.jsx)("button", { type: "button", disabled: busy !== null, onClick: () => act('approve'), style: {
261
+ background: primary,
262
+ color: 'white',
263
+ border: 'none',
264
+ borderRadius: 8,
265
+ padding: '6px 12px',
266
+ fontSize: 12,
267
+ fontWeight: 600,
268
+ cursor: busy === null ? 'pointer' : 'not-allowed',
269
+ opacity: busy === null ? 1 : 0.6,
270
+ }, children: busy === 'approve' ? 'Approving…' : 'Approve' }), (0, jsx_runtime_1.jsx)("button", { type: "button", disabled: busy !== null, onClick: () => act('deny'), style: {
271
+ background: 'white',
272
+ color: '#7c2d12',
273
+ border: '1px solid #fde68a',
274
+ borderRadius: 8,
275
+ padding: '6px 12px',
276
+ fontSize: 12,
277
+ fontWeight: 600,
278
+ cursor: busy === null ? 'pointer' : 'not-allowed',
279
+ opacity: busy === null ? 1 : 0.6,
280
+ }, children: busy === 'deny' ? 'Denying…' : 'Deny' })] })), !decided && !onAction && ((0, jsx_runtime_1.jsxs)("div", { style: { fontSize: 12, marginTop: 8, color: '#7c2d12' }, children: ["A workspace admin needs to review this. Visit", ' ', (0, jsx_runtime_1.jsx)("code", { style: { fontSize: 12 }, children: "/approvals" }), " in the dashboard."] })), err && ((0, jsx_runtime_1.jsx)("div", { style: { fontSize: 11, marginTop: 6, color: '#b91c1c' }, children: err }))] }));
281
+ }
282
+ /**
283
+ * Bubble variant for `metadata.kind === 'tool_blocked'`. Terminal —
284
+ * no action affordance. Renders the reason if the server supplied
285
+ * one, otherwise a generic line.
286
+ */
287
+ function BlockedCardBubble({ message }) {
288
+ const meta = message.metadata;
289
+ if (!meta)
290
+ return null;
291
+ return ((0, jsx_runtime_1.jsxs)("div", { style: {
292
+ alignSelf: 'flex-start',
293
+ maxWidth: '90%',
294
+ padding: '10px 14px',
295
+ borderRadius: 14,
296
+ borderBottomLeftRadius: 4,
297
+ fontSize: 13,
298
+ background: '#fef2f2',
299
+ border: '1px solid #fecaca',
300
+ color: '#7f1d1d',
301
+ }, children: [(0, jsx_runtime_1.jsx)("div", { style: { fontWeight: 600, fontSize: 13, marginBottom: 4 }, children: "Tool blocked" }), (0, jsx_runtime_1.jsxs)("div", { style: { fontSize: 12, lineHeight: 1.5 }, children: [(0, jsx_runtime_1.jsx)("code", { style: {
302
+ background: 'rgba(0,0,0,0.06)',
303
+ padding: '1px 6px',
304
+ borderRadius: 4,
305
+ fontSize: 12,
306
+ }, children: meta.toolName }), ' ', "is blocked by the workspace admin. ", meta.reason ?? ''] })] }));
307
+ }
308
+ /**
309
+ * Live countdown for an ISO timestamp. Returns null when the deadline
310
+ * has passed — caller renders nothing in that case. Tick interval 1s
311
+ * to match the `/approvals` page's badge.
312
+ */
313
+ function useExpiresIn(iso) {
314
+ const [, force] = (0, react_1.useState)(0);
315
+ (0, react_1.useEffect)(() => {
316
+ if (!iso)
317
+ return;
318
+ const t = setInterval(() => force((n) => n + 1), 1000);
319
+ return () => clearInterval(t);
320
+ }, [iso]);
321
+ if (!iso)
322
+ return null;
323
+ const ms = new Date(iso).getTime() - Date.now();
324
+ if (!Number.isFinite(ms) || ms <= 0)
325
+ return null;
326
+ const totalSeconds = Math.floor(ms / 1000);
327
+ const m = Math.floor(totalSeconds / 60);
328
+ const s = totalSeconds % 60;
329
+ if (m >= 60)
330
+ return `Expires in ${Math.floor(m / 60)}h ${m % 60}m`;
331
+ return `Expires in ${m}m ${s.toString().padStart(2, '0')}s`;
332
+ }
package/dist/index.d.ts CHANGED
@@ -26,5 +26,5 @@ export { AgentChatProvider, useAgentChatContext, useChatSessionState } from './p
26
26
  export type { AgentChatProviderProps, ChatUserContext, } from './provider';
27
27
  export { useChat, useAgent } from './hooks';
28
28
  export { ChatPanel } from './ChatPanel';
29
- export type { ChatPanelProps } from './ChatPanel';
30
- export type { ChatAgentSummary, ChatEvent, ChatMessage, ChatRole, ChatSessionStatus, ChatTheme, } from '@agentforge-io/chat-sdk';
29
+ export type { ChatPanelProps, ApprovalActionHandler } from './ChatPanel';
30
+ export type { ChatAgentSummary, ChatEvent, ChatMessage, ChatMessageMetadata, ChatRole, ChatSessionStatus, ChatTheme, } from '@agentforge-io/chat-sdk';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentforge-io/chat-react",
3
- "version": "2.0.15",
3
+ "version": "2.0.17",
4
4
  "description": "React + Next.js adapter for @agentforge-io/chat-sdk. Provider, hooks, and a reference ChatPanel component.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -16,10 +16,10 @@
16
16
  "peerDependencies": {
17
17
  "react": "^18.0.0 || ^19.0.0",
18
18
  "react-dom": "^18.0.0 || ^19.0.0",
19
- "@agentforge-io/chat-sdk": "^2.0.15"
19
+ "@agentforge-io/chat-sdk": "^2.0.17"
20
20
  },
21
21
  "dependencies": {
22
- "@agentforge-io/chat-sdk": "^2.0.15"
22
+ "@agentforge-io/chat-sdk": "^2.0.17"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@types/node": "^20.0.0",