@agentforge-io/chat-sdk 2.0.22 → 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.
@@ -131,12 +131,10 @@ export interface ChatSessionOptions {
131
131
  token: string;
132
132
  /**
133
133
  * Base URL of the AgentForge API, including scheme. Trailing slash is
134
- * stripped. Resolution order:
135
- * 1. This option (when present).
136
- * 2. `window.AGENTFORGE_API_BASE_URL` (runtime override, useful when
137
- * embedding via a `<script>` tag without React props).
138
- * 3. The baked default that ships with the current SDK version.
139
- * Hosts embedding into their own site can leave this unset.
134
+ * stripped. Optional — when omitted the SDK uses the baked default
135
+ * that ships with the current version. Pass this only when self-
136
+ * hosting the backend or pointing the SDK at a non-production
137
+ * deployment (e.g. localhost during development).
140
138
  */
141
139
  apiBaseUrl?: string;
142
140
  /** Stable id for this end-user's browser. Persist it (localStorage etc.)
package/dist/react.d.ts CHANGED
@@ -64,8 +64,8 @@ export interface ChatWidgetProps {
64
64
  /**
65
65
  * AgentForge API origin. Optional — the SDK ships with a built-in
66
66
  * default that points at the hosted AgentForge deployment. Override
67
- * this when you self-host the backend. A `window.AGENTFORGE_API_BASE_URL`
68
- * global also overrides, useful for `<script>`-tag embeds.
67
+ * this only when self-hosting the backend or pointing at a non-
68
+ * production deployment.
69
69
  */
70
70
  apiBaseUrl?: string;
71
71
  /** Render inline (fills the parent) instead of as a floating bubble. */
@@ -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/dist/session.js CHANGED
@@ -391,19 +391,10 @@ function generateBrowserSessionId() {
391
391
  }
392
392
  /**
393
393
  * Build-time default API origin. Bumped together with the SDK when the
394
- * hosted AgentForge deployment moves to a new domain. Hosts that
395
- * self-host the backend override this via the `apiBaseUrl` constructor
396
- * option, or at runtime via `window.AGENTFORGE_API_BASE_URL`.
394
+ * hosted AgentForge deployment moves to a new domain. Self-hosted
395
+ * deployments override this via the `apiBaseUrl` constructor option.
397
396
  */
398
397
  const BAKED_API_BASE = 'https://api-agentforge.stupidmvp.com';
399
398
  function defaultApiBase() {
400
- // Runtime override for hosts that embed via a script tag and don't
401
- // want to touch React props. Set once on `window` before the widget
402
- // mounts and the SDK uses it for every transport call.
403
- if (typeof window !== 'undefined') {
404
- const override = window.AGENTFORGE_API_BASE_URL;
405
- if (override)
406
- return override;
407
- }
408
399
  return BAKED_API_BASE;
409
400
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentforge-io/chat-sdk",
3
- "version": "2.0.22",
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",