@agentforge-io/chat-sdk 2.0.23 → 2.0.24

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 CHANGED
@@ -97,6 +97,36 @@ export interface ChatWidgetProps {
97
97
  * legitimately decide.
98
98
  */
99
99
  readOnlyApprovals?: boolean;
100
+ /**
101
+ * Chrome preset.
102
+ *
103
+ * - `'card'` (default): the historical look. Rounded card, header
104
+ * with the agent name, "Powered by AgentForge" footer, opaque
105
+ * background. Designed to drop into a customer's site as a
106
+ * visually distinct widget.
107
+ * - `'bare'`: no card, no header, no footer, transparent background.
108
+ * Lets the host page wrap the chat in its own template. The host
109
+ * drives the visible chrome via the surrounding layout + CSS
110
+ * variables (`--af-primary`, `--af-bubble-bg`, etc.). Use this on
111
+ * a dedicated agent page where the page IS the chat surface.
112
+ */
113
+ variant?: 'card' | 'bare';
114
+ /**
115
+ * Initial assistant message rendered before the visitor types. Client-only —
116
+ * we don't send it to the server, so it costs zero tokens. Use it for
117
+ * "Hey, I'm Fabian, how can I help you?"-style openings.
118
+ */
119
+ greeting?: string;
120
+ /**
121
+ * Display name for the agent in the header / typing indicators. When
122
+ * unset we fall back to the agent's configured `name`. The visitor sees
123
+ * "personaName" alongside the avatar so a single workspace can run
124
+ * multiple agents that feel like distinct people.
125
+ */
126
+ personaName?: string;
127
+ /** Input placeholder. Defaults to "Type a message…" — override when
128
+ * the persona speaks a different language. */
129
+ inputPlaceholder?: string;
100
130
  }
101
131
  /**
102
132
  * Drop-in chat widget. Owns its own `ChatSession` and re-renders on every
package/dist/react.js CHANGED
@@ -181,7 +181,8 @@ 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, onApprovalDecision, readOnlyApprovals = false, } = props;
184
+ const { token, apiBaseUrl, inline = false, position, browserSessionId, resumeConversationId, stream, className, style, onApprovalDecision, readOnlyApprovals = false, variant = 'card', greeting, personaName, inputPlaceholder, } = props;
185
+ const bare = variant === 'bare';
185
186
  const [session, setSession] = (0, react_1.useState)(null);
186
187
  const [status, setStatus] = (0, react_1.useState)('idle');
187
188
  const [agent, setAgent] = (0, react_1.useState)();
@@ -281,6 +282,7 @@ function ChatWidget(props) {
281
282
  'af-widget-root',
282
283
  `af-pos-${resolvedPosition}`,
283
284
  inline ? 'af-inline' : '',
285
+ bare ? 'af-variant-bare' : 'af-variant-card',
284
286
  className ?? '',
285
287
  ]
286
288
  .filter(Boolean)
@@ -299,21 +301,21 @@ function ChatWidget(props) {
299
301
  }
300
302
  : {}),
301
303
  };
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 ? (
304
+ 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: [!bare && ((0, jsx_runtime_1.jsxs)("div", { className: "af-header", children: [theme?.avatarUrl ? (
303
305
  // 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, session: session, readOnly: readOnlyApprovals, onDecision: onApprovalDecision, onContinue: () => {
305
- // After a successful Approve, kick the next turn so the
306
- // gate's fast-path consumes the approval and the tool
307
- // actually runs. `silent: true` keeps the literal
308
- // "continue" prompt out of the visible transcript —
309
- // the visitor just sees the assistant's next reply
310
- // appearing under the "Approved" pill.
311
- if (!session)
312
- return;
313
- setTimeout(() => {
314
- void session.send('continue', { silent: true });
315
- }, 250);
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" })] })] }));
306
+ (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: personaName ?? 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.jsxs)("div", { className: "af-messages", ref: messagesRef, children: [bare && greeting && messages.length === 0 && status !== 'loading' && ((0, jsx_runtime_1.jsx)("div", { className: "af-msg af-msg-assistant af-msg-greeting", dangerouslySetInnerHTML: { __html: renderMarkdown(greeting) } })), messages.map((m) => ((0, jsx_runtime_1.jsx)(MessageBubble, { message: m, session: session, readOnly: readOnlyApprovals, onDecision: onApprovalDecision, onContinue: () => {
307
+ // After a successful Approve, kick the next turn so the
308
+ // gate's fast-path consumes the approval and the tool
309
+ // actually runs. `silent: true` keeps the literal
310
+ // "continue" prompt out of the visible transcript —
311
+ // the visitor just sees the assistant's next reply
312
+ // appearing under the "Approved" pill.
313
+ if (!session)
314
+ return;
315
+ setTimeout(() => {
316
+ void session.send('continue', { silent: true });
317
+ }, 250);
318
+ } }, 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: inputPlaceholder ?? 'Type a message…', 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, {}) })] }), !bare && (0, jsx_runtime_1.jsx)("div", { className: "af-footer", children: "Powered by AgentForge" })] })] }));
317
319
  }
318
320
  function MessageBubble({ message, session, readOnly, onDecision, onContinue, }) {
319
321
  const kind = message.metadata?.kind;
@@ -546,4 +548,130 @@ const WIDGET_CSS = `
546
548
  .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); }
547
549
  .af-blocked-title { font-weight: 600; font-size: 13px; margin-bottom: 4px; }
548
550
  .af-blocked-body { font-size: 12px; line-height: 1.5; }
551
+
552
+ /* ─── variant="bare" ────────────────────────────────────────────────────
553
+ * Drops the card chrome and lets the host page own the surroundings.
554
+ * Inherits font-family and color from the parent so the chat blends
555
+ * into whatever wrapper the host renders (templates own those values
556
+ * via CSS variables on a parent div).
557
+ *
558
+ * Host-overridable variables — all optional, sensible defaults below:
559
+ * --af-primary accent color (send button, focus ring, links)
560
+ * --af-bubble-user-bg user message background (default: primary)
561
+ * --af-bubble-user-fg user message text (default: white)
562
+ * --af-bubble-agent-bg assistant message background (default: subtle)
563
+ * --af-bubble-agent-fg assistant message text (default: inherit)
564
+ * --af-bubble-radius bubble corner radius (default: 18px)
565
+ * --af-input-bg input background (default: subtle)
566
+ * --af-input-radius input corner radius (default: 24px — pill)
567
+ * --af-gap spacing between messages (default: 12px)
568
+ */
569
+ .af-widget-root.af-variant-bare { font-family: inherit; color: inherit; }
570
+ .af-widget-root.af-variant-bare.af-inline .af-panel,
571
+ .af-widget-root.af-variant-bare .af-panel {
572
+ position: relative;
573
+ width: 100%;
574
+ height: 100%;
575
+ max-height: none;
576
+ background: transparent;
577
+ border-radius: 0;
578
+ box-shadow: none;
579
+ overflow: visible;
580
+ display: flex;
581
+ flex-direction: column;
582
+ }
583
+ .af-widget-root.af-variant-bare .af-messages {
584
+ background: transparent;
585
+ padding: 16px 14px 12px;
586
+ gap: var(--af-gap, 12px);
587
+ /* Let the parent flexbox give us a min-height; we'll grow into it
588
+ and scroll internally when the transcript gets longer. */
589
+ min-height: 0;
590
+ }
591
+ .af-widget-root.af-variant-bare .af-msg {
592
+ font-size: 15px;
593
+ line-height: 1.5;
594
+ border-radius: var(--af-bubble-radius, 18px);
595
+ padding: 10px 14px;
596
+ max-width: 86%;
597
+ }
598
+ .af-widget-root.af-variant-bare .af-msg-user {
599
+ background: var(--af-bubble-user-bg, var(--af-primary));
600
+ color: var(--af-bubble-user-fg, #ffffff);
601
+ border-bottom-right-radius: 6px;
602
+ }
603
+ .af-widget-root.af-variant-bare .af-msg-assistant {
604
+ background: var(--af-bubble-agent-bg, rgba(148, 163, 184, 0.12));
605
+ color: var(--af-bubble-agent-fg, inherit);
606
+ border: none;
607
+ border-bottom-left-radius: 6px;
608
+ }
609
+ .af-widget-root.af-variant-bare .af-msg-assistant a {
610
+ color: var(--af-primary);
611
+ }
612
+ .af-widget-root.af-variant-bare .af-msg-assistant code {
613
+ background: rgba(148, 163, 184, 0.2);
614
+ color: inherit;
615
+ }
616
+ .af-widget-root.af-variant-bare .af-msg-greeting { animation: af-msg-in 320ms cubic-bezier(0.2, 0.8, 0.2, 1); }
617
+ .af-widget-root.af-variant-bare .af-input-row {
618
+ position: sticky;
619
+ bottom: 0;
620
+ padding: 10px 12px 14px;
621
+ border-top: none;
622
+ background: transparent;
623
+ gap: 8px;
624
+ }
625
+ .af-widget-root.af-variant-bare .af-input {
626
+ background: var(--af-input-bg, rgba(148, 163, 184, 0.10));
627
+ color: inherit;
628
+ border: 1px solid var(--af-input-border, rgba(148, 163, 184, 0.25));
629
+ border-radius: var(--af-input-radius, 24px);
630
+ padding: 12px 16px;
631
+ font-size: 15px;
632
+ min-height: 48px;
633
+ line-height: 1.4;
634
+ }
635
+ .af-widget-root.af-variant-bare .af-input:focus {
636
+ border-color: var(--af-primary);
637
+ box-shadow: 0 0 0 3px var(--af-primary-soft);
638
+ }
639
+ .af-widget-root.af-variant-bare .af-input:disabled { background: rgba(148, 163, 184, 0.06); }
640
+ .af-widget-root.af-variant-bare .af-send {
641
+ background: var(--af-primary);
642
+ color: #fff;
643
+ border-radius: 50%;
644
+ width: 44px;
645
+ height: 44px;
646
+ min-height: 44px;
647
+ padding: 0;
648
+ align-self: flex-end;
649
+ box-shadow: 0 6px 16px var(--af-primary-soft);
650
+ }
651
+ .af-widget-root.af-variant-bare .af-error {
652
+ background: transparent;
653
+ border-top: none;
654
+ padding: 6px 14px;
655
+ }
656
+ .af-widget-root.af-variant-bare .af-typing-dots span { background: currentColor; opacity: 0.5; }
657
+
658
+ /* Mobile-first sticky input: on narrow viewports the input docks to
659
+ * the viewport's bottom edge so the visitor never has to scroll to
660
+ * find it (same affordance as iMessage / WhatsApp). On desktop the
661
+ * sticky behaviour scopes to the chat container, which is enough. */
662
+ @media (max-width: 640px) {
663
+ .af-widget-root.af-variant-bare .af-input-row {
664
+ position: sticky;
665
+ bottom: env(safe-area-inset-bottom, 0);
666
+ padding-bottom: calc(14px + env(safe-area-inset-bottom, 0px));
667
+ background: var(--af-input-row-bg, rgba(255, 255, 255, 0.85));
668
+ backdrop-filter: saturate(140%) blur(8px);
669
+ -webkit-backdrop-filter: saturate(140%) blur(8px);
670
+ }
671
+ }
672
+ @media (max-width: 640px) and (prefers-color-scheme: dark) {
673
+ .af-widget-root.af-variant-bare .af-input-row {
674
+ background: var(--af-input-row-bg, rgba(15, 23, 42, 0.7));
675
+ }
676
+ }
549
677
  `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentforge-io/chat-sdk",
3
- "version": "2.0.23",
3
+ "version": "2.0.24",
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",