@agentforge-io/chat-sdk 2.0.23 → 2.0.25

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,69 @@ 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;
130
+ /**
131
+ * One-click conversation starters rendered as chips ABOVE the input
132
+ * (and only while the conversation is empty — they fade away after
133
+ * the first turn). Click pre-fills the input with the chip text so
134
+ * the visitor can review / edit before sending. Use this for
135
+ * persona-specific openings ("Schedule a call", "Get a quote") on
136
+ * the same surface that owns the chat. */
137
+ shortcuts?: string[];
138
+ /**
139
+ * Called when the visitor clicks a shortcut chip. When omitted the
140
+ * default behaviour is to populate the input with the chip text.
141
+ * Provide this hook to customise — e.g. send immediately, log the
142
+ * click, or open an external link instead. */
143
+ onShortcutClick?: (text: string, index: number) => void;
144
+ /**
145
+ * Imperative handle. The widget fills this ref on mount with a
146
+ * small command object the host can call to drive the session
147
+ * without owning it. Today exposes `sendNow(text)` — sends the
148
+ * given text as a user turn, bypassing the textarea/draft state.
149
+ * Use this for chips, suggested replies, programmatic kicks, etc.
150
+ *
151
+ * `null` while the session is still booting. Becomes available
152
+ * once `agent_loaded` has fired internally.
153
+ */
154
+ handleRef?: React.MutableRefObject<ChatWidgetHandle | null>;
155
+ }
156
+ /** Imperative surface exposed via ChatWidgetProps.handleRef. Narrow
157
+ * on purpose — hosts get a verb, not the whole session. */
158
+ export interface ChatWidgetHandle {
159
+ /** Send `text` immediately as a user turn. Skips the textarea
160
+ * entirely. No-op if the session isn't ready yet (status ===
161
+ * 'idle' / 'loading'). */
162
+ sendNow(text: string): void;
100
163
  }
101
164
  /**
102
165
  * Drop-in chat widget. Owns its own `ChatSession` and re-renders on every
package/dist/react.js CHANGED
@@ -133,6 +133,47 @@ function renderMarkdown(src) {
133
133
  out.push(`<blockquote>${renderInline(qbuf.join(' '))}</blockquote>`);
134
134
  continue;
135
135
  }
136
+ // GFM-style table: a row of `| col | col |`, then a separator row
137
+ // `|---|---|` (any number of dashes / optional :), then body rows.
138
+ // Detection looks two lines ahead so a stray "| pipe |" sentence
139
+ // in prose doesn't get promoted to a table.
140
+ if (/^\s*\|.*\|\s*$/.test(line) &&
141
+ i + 1 < lines.length &&
142
+ /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(lines[i + 1])) {
143
+ const splitRow = (raw) => {
144
+ // Trim outer pipes then split, preserving empty cells. GFM
145
+ // tolerates escaped pipes (\|) inside cells — we don't, the
146
+ // server-side markdown the agent emits never includes them.
147
+ const trimmed = raw.trim().replace(/^\|/, '').replace(/\|$/, '');
148
+ return trimmed.split('|').map((c) => c.trim());
149
+ };
150
+ const header = splitRow(line);
151
+ i += 2; // skip header + separator
152
+ const bodyRows = [];
153
+ while (i < lines.length && /^\s*\|.*\|\s*$/.test(lines[i])) {
154
+ bodyRows.push(splitRow(lines[i]));
155
+ i++;
156
+ }
157
+ // We stamp each <td> with a `data-label` attribute so the
158
+ // mobile stylesheet can show a label before the cell value
159
+ // when the table collapses to a stacked layout. Cheap on the
160
+ // desktop side (the CSS just hides the label) and gives mobile
161
+ // a real card-per-row read instead of a horizontal scroll.
162
+ const thead = '<thead><tr>' +
163
+ header.map((c) => `<th>${renderInline(c)}</th>`).join('') +
164
+ '</tr></thead>';
165
+ const tbody = '<tbody>' +
166
+ bodyRows
167
+ .map((r) => '<tr>' +
168
+ r
169
+ .map((c, idx) => `<td data-label="${escapeHtml(header[idx] ?? '')}">${renderInline(c)}</td>`)
170
+ .join('') +
171
+ '</tr>')
172
+ .join('') +
173
+ '</tbody>';
174
+ out.push(`<table>${thead}${tbody}</table>`);
175
+ continue;
176
+ }
136
177
  if (/^\s*$/.test(line)) {
137
178
  i++;
138
179
  continue;
@@ -145,7 +186,12 @@ function renderMarkdown(src) {
145
186
  !/^\s*[-*]\s+/.test(lines[i]) &&
146
187
  !/^\s*\d+\.\s+/.test(lines[i]) &&
147
188
  !/^\s*>\s?/.test(lines[i]) &&
148
- !/^\s*(---+|\*\*\*+|___+)\s*$/.test(lines[i])) {
189
+ !/^\s*(---+|\*\*\*+|___+)\s*$/.test(lines[i]) &&
190
+ // Stop on a likely table start (header + separator next line) so
191
+ // the paragraph collector doesn't swallow the table rows.
192
+ !(/^\s*\|.*\|\s*$/.test(lines[i]) &&
193
+ i + 1 < lines.length &&
194
+ /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(lines[i + 1]))) {
149
195
  pbuf.push(lines[i]);
150
196
  i++;
151
197
  }
@@ -181,7 +227,8 @@ function fallbackCopy(ctx) {
181
227
  * SDK event. Consumers don't need to read `session.getState()` themselves.
182
228
  */
183
229
  function ChatWidget(props) {
184
- const { token, apiBaseUrl, inline = false, position, browserSessionId, resumeConversationId, stream, className, style, onApprovalDecision, readOnlyApprovals = false, } = props;
230
+ const { token, apiBaseUrl, inline = false, position, browserSessionId, resumeConversationId, stream, className, style, onApprovalDecision, readOnlyApprovals = false, variant = 'card', greeting, personaName, shortcuts, onShortcutClick, inputPlaceholder, } = props;
231
+ const bare = variant === 'bare';
185
232
  const [session, setSession] = (0, react_1.useState)(null);
186
233
  const [status, setStatus] = (0, react_1.useState)('idle');
187
234
  const [agent, setAgent] = (0, react_1.useState)();
@@ -281,6 +328,7 @@ function ChatWidget(props) {
281
328
  'af-widget-root',
282
329
  `af-pos-${resolvedPosition}`,
283
330
  inline ? 'af-inline' : '',
331
+ bare ? 'af-variant-bare' : 'af-variant-card',
284
332
  className ?? '',
285
333
  ]
286
334
  .filter(Boolean)
@@ -299,29 +347,47 @@ function ChatWidget(props) {
299
347
  }
300
348
  : {}),
301
349
  };
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 ? (
350
+ 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
351
  // 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" })] })] }));
317
- }
318
- function MessageBubble({ message, session, readOnly, onDecision, onContinue, }) {
352
+ (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.jsxs)("div", { className: "af-msg-row af-msg-row-assistant", children: [(0, jsx_runtime_1.jsx)(AssistantAvatar, { theme: theme, name: personaName ?? agent?.name, show: true }), (0, jsx_runtime_1.jsx)("div", { className: "af-msg af-msg-assistant af-msg-greeting", dangerouslySetInnerHTML: { __html: renderMarkdown(greeting) } })] })), messages.map((m, i) => {
353
+ // Show the avatar only on the FIRST bubble in a run of
354
+ // assistant messages. Consecutive assistant turns get a
355
+ // transparent placeholder so the column stays aligned.
356
+ // User/system bubbles never get an avatar.
357
+ const prev = messages[i - 1];
358
+ const isAssistant = m.role === 'assistant';
359
+ const prevWasAssistant = prev?.role === 'assistant';
360
+ const showAvatar = bare && isAssistant && !prevWasAssistant;
361
+ return ((0, jsx_runtime_1.jsx)(MessageBubble, { message: m, session: session, readOnly: readOnlyApprovals, onDecision: onApprovalDecision, bare: bare, showAvatar: showAvatar, avatarTheme: theme, avatarName: personaName ?? agent?.name, onContinue: () => {
362
+ // After a successful Approve, kick the next turn so the
363
+ // gate's fast-path consumes the approval and the tool
364
+ // actually runs. `silent: true` keeps the literal
365
+ // "continue" prompt out of the visible transcript —
366
+ // the visitor just sees the assistant's next reply
367
+ // appearing under the "Approved" pill.
368
+ if (!session)
369
+ return;
370
+ setTimeout(() => {
371
+ void session.send('continue', { silent: true });
372
+ }, 250);
373
+ } }, m.id));
374
+ })] }), lastError && status === 'error' && ((0, jsx_runtime_1.jsx)("div", { className: "af-error", children: lastError })), shortcuts && shortcuts.length > 0 && messages.length === 0 && ((0, jsx_runtime_1.jsx)("div", { className: "af-shortcut-row", children: shortcuts.map((text, i) => ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "af-shortcut", onClick: () => {
375
+ if (onShortcutClick)
376
+ onShortcutClick(text, i);
377
+ else
378
+ setDraft(text);
379
+ }, children: text }, `${i}-${text}`))) })), (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" })] })] }));
380
+ }
381
+ function MessageBubble({ message, session, readOnly, onDecision, onContinue, bare = false, showAvatar = false, avatarTheme, avatarName, }) {
319
382
  const kind = message.metadata?.kind;
320
383
  if (kind === 'awaiting_approval') {
321
- return ((0, jsx_runtime_1.jsx)(ApprovalBubble, { message: message, session: session, readOnly: readOnly, onDecision: onDecision, onContinue: onContinue }));
384
+ // Approval / blocked bubbles also count as "assistant-side" so we
385
+ // wrap them in the same row geometry — keeps the conversation
386
+ // aligned even when a tool dispatch interrupts the regular flow.
387
+ return wrapAssistantRow(bare, showAvatar, avatarTheme, avatarName, (0, jsx_runtime_1.jsx)(ApprovalBubble, { message: message, session: session, readOnly: readOnly, onDecision: onDecision, onContinue: onContinue }));
322
388
  }
323
389
  if (kind === 'tool_blocked') {
324
- return (0, jsx_runtime_1.jsx)(BlockedBubble, { message: message });
390
+ return wrapAssistantRow(bare, showAvatar, avatarTheme, avatarName, (0, jsx_runtime_1.jsx)(BlockedBubble, { message: message }));
325
391
  }
326
392
  const cls = `af-msg af-msg-${message.role}${message.role === 'assistant' && message.isStreaming
327
393
  ? message.content
@@ -330,17 +396,46 @@ function MessageBubble({ message, session, readOnly, onDecision, onContinue, })
330
396
  : ''}`;
331
397
  // Typing state (no content yet): render the three-dot indicator, no markdown.
332
398
  if (message.role === 'assistant' && message.isStreaming && !message.content) {
333
- return ((0, jsx_runtime_1.jsx)("div", { className: cls, children: (0, jsx_runtime_1.jsxs)("span", { className: "af-typing-dots", "aria-label": "Assistant is typing", children: [(0, jsx_runtime_1.jsx)("span", {}), " ", (0, jsx_runtime_1.jsx)("span", {}), " ", (0, jsx_runtime_1.jsx)("span", {})] }) }));
399
+ return wrapAssistantRow(bare, showAvatar, avatarTheme, avatarName, (0, jsx_runtime_1.jsx)("div", { className: cls, children: (0, jsx_runtime_1.jsxs)("span", { className: "af-typing-dots", "aria-label": "Assistant is typing", children: [(0, jsx_runtime_1.jsx)("span", {}), " ", (0, jsx_runtime_1.jsx)("span", {}), " ", (0, jsx_runtime_1.jsx)("span", {})] }) }));
334
400
  }
335
401
  if (message.role === 'assistant') {
336
- return ((0, jsx_runtime_1.jsx)("div", { className: cls,
402
+ return wrapAssistantRow(bare, showAvatar, avatarTheme, avatarName, (0, jsx_runtime_1.jsx)("div", { className: cls,
337
403
  // Output is sanitized by escapeHtml + a fixed tag whitelist in
338
404
  // renderMarkdown — safe to inject as HTML.
339
405
  dangerouslySetInnerHTML: { __html: renderMarkdown(message.content) } }));
340
406
  }
341
407
  // User & system messages stay as plain text — they're typed verbatim.
408
+ // No avatar column on the user side; they align right.
342
409
  return (0, jsx_runtime_1.jsx)("div", { className: cls, children: message.content });
343
410
  }
411
+ /**
412
+ * Wrap an assistant-side bubble in the row geometry that reserves an
413
+ * avatar column. Card variant skips the wrapper entirely so its
414
+ * historical layout is unchanged.
415
+ */
416
+ function wrapAssistantRow(bare, showAvatar, theme, name, child) {
417
+ if (!bare)
418
+ return child;
419
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "af-msg-row af-msg-row-assistant", children: [(0, jsx_runtime_1.jsx)(AssistantAvatar, { theme: theme, name: name, show: showAvatar }), child] }));
420
+ }
421
+ /**
422
+ * Small circular avatar for the assistant-side column. Prefers the
423
+ * agent's avatarUrl when set; otherwise renders a gradient circle
424
+ * with the first letter of `name`. The slot reserves space even when
425
+ * `show` is false so consecutive bubbles stay column-aligned.
426
+ */
427
+ function AssistantAvatar({ theme, name, show, }) {
428
+ const initial = (name ?? 'A').trim().charAt(0).toUpperCase() || 'A';
429
+ if (!show) {
430
+ return (0, jsx_runtime_1.jsx)("div", { className: "af-msg-avatar af-msg-avatar-spacer", "aria-hidden": true });
431
+ }
432
+ if (theme?.avatarUrl) {
433
+ return (
434
+ // eslint-disable-next-line @next/next/no-img-element -- vanilla widget; consumer can override host
435
+ (0, jsx_runtime_1.jsx)("img", { className: "af-msg-avatar af-msg-avatar-img", src: theme.avatarUrl, alt: "", "aria-hidden": true }));
436
+ }
437
+ return ((0, jsx_runtime_1.jsx)("div", { className: "af-msg-avatar af-msg-avatar-fallback", "aria-hidden": true, children: (0, jsx_runtime_1.jsx)("span", { children: initial }) }));
438
+ }
344
439
  /**
345
440
  * Awaiting-approval bubble. Renders the tool name + a countdown, and
346
441
  * either the Approve/Deny buttons (when the host wired the handler)
@@ -515,6 +610,52 @@ const WIDGET_CSS = `
515
610
  to { opacity: 1; transform: translateY(0); }
516
611
  }
517
612
  .af-input-row { padding: 12px; border-top: 1px solid var(--af-border); background: var(--af-bg); display: flex; gap: 8px; align-items: flex-end; }
613
+ .af-shortcut-row {
614
+ display: flex;
615
+ gap: 6px;
616
+ padding: 10px 12px 6px 12px;
617
+ overflow-x: auto;
618
+ scrollbar-width: none;
619
+ background: var(--af-bg);
620
+ animation: af-msg-in 320ms cubic-bezier(0.2, 0.8, 0.2, 1);
621
+ }
622
+ .af-shortcut-row::-webkit-scrollbar { display: none; }
623
+ /* When the shortcut row sits directly above the composer, drop the
624
+ composer's top padding + border so the two read as a single
625
+ stacked block. Without this the 12px input-row padding + 1px
626
+ border-top create a visible gutter between the chips and the
627
+ textarea. */
628
+ .af-shortcut-row + .af-input-row {
629
+ padding-top: 4px;
630
+ border-top: 0;
631
+ }
632
+ .af-shortcut {
633
+ flex-shrink: 0;
634
+ display: inline-flex;
635
+ align-items: center;
636
+ gap: 6px;
637
+ padding: 6px 12px;
638
+ font-size: 12.5px;
639
+ font-weight: 500;
640
+ font-family: inherit;
641
+ line-height: 1.2;
642
+ color: var(--af-fg);
643
+ background: var(--af-input-bg, rgba(148, 163, 184, 0.10));
644
+ border: 1px solid var(--af-input-border, rgba(148, 163, 184, 0.22));
645
+ border-radius: 999px;
646
+ cursor: pointer;
647
+ transition: background-color .15s ease, border-color .15s ease, transform .15s ease;
648
+ white-space: nowrap;
649
+ max-width: 280px;
650
+ overflow: hidden;
651
+ text-overflow: ellipsis;
652
+ }
653
+ .af-shortcut:hover {
654
+ background: var(--af-primary-soft, rgba(99, 102, 241, 0.10));
655
+ border-color: var(--af-primary, #6366f1);
656
+ transform: translateY(-1px);
657
+ }
658
+ .af-shortcut:active { transform: translateY(0); }
518
659
  .af-input { flex: 1; border: 1px solid var(--af-border); border-radius: 10px; padding: 10px 12px; font-size: 14px; font-family: inherit; outline: none; resize: none; min-height: 40px; max-height: 120px; color: var(--af-fg); background: var(--af-bg); transition: border-color .15s ease, box-shadow .15s ease; }
519
660
  .af-input:focus { border-color: var(--af-primary); box-shadow: 0 0 0 3px var(--af-primary-soft); }
520
661
  .af-input:disabled { background: var(--af-bubble-bg); cursor: not-allowed; }
@@ -546,4 +687,333 @@ const WIDGET_CSS = `
546
687
  .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
688
  .af-blocked-title { font-weight: 600; font-size: 13px; margin-bottom: 4px; }
548
689
  .af-blocked-body { font-size: 12px; line-height: 1.5; }
690
+
691
+ /* ─── variant="bare" ────────────────────────────────────────────────────
692
+ * Drops the card chrome and lets the host page own the surroundings.
693
+ * Inherits font-family and color from the parent so the chat blends
694
+ * into whatever wrapper the host renders (templates own those values
695
+ * via CSS variables on a parent div).
696
+ *
697
+ * Host-overridable variables — all optional, sensible defaults below:
698
+ * --af-primary accent color (send button, focus ring, links)
699
+ * --af-bubble-user-bg user message background (default: primary)
700
+ * --af-bubble-user-fg user message text (default: white)
701
+ * --af-bubble-agent-bg assistant message background (default: subtle)
702
+ * --af-bubble-agent-fg assistant message text (default: inherit)
703
+ * --af-bubble-radius bubble corner radius (default: 18px)
704
+ * --af-input-bg input background (default: subtle)
705
+ * --af-input-radius input corner radius (default: 24px — pill)
706
+ * --af-gap spacing between messages (default: 12px)
707
+ */
708
+ .af-widget-root.af-variant-bare { font-family: inherit; color: inherit; }
709
+ /* Make the bare-inline root fill its flex parent so the panel +
710
+ * its sticky input dock to the parent's bottom edge instead of
711
+ * collapsing to content height. Required for hosts that nest the
712
+ * widget inside a constrained drawer / column.
713
+ *
714
+ * width:100% is mandatory: the base af-widget-root ships
715
+ * position:fixed which leaves the root with intrinsic-content
716
+ * width by default. af-inline only resets position, not the width
717
+ * — so without this the bare-inline widget collapses to the width
718
+ * of its tallest child (usually the input pill). */
719
+ .af-widget-root.af-variant-bare.af-inline {
720
+ display: flex;
721
+ flex-direction: column;
722
+ width: 100%;
723
+ height: 100%;
724
+ min-height: 0;
725
+ }
726
+ .af-widget-root.af-variant-bare.af-inline .af-panel,
727
+ .af-widget-root.af-variant-bare .af-panel {
728
+ position: relative;
729
+ width: 100%;
730
+ height: 100%;
731
+ max-height: none;
732
+ background: transparent;
733
+ border-radius: 0;
734
+ box-shadow: none;
735
+ overflow: visible;
736
+ display: flex;
737
+ flex-direction: column;
738
+ flex: 1;
739
+ min-height: 0;
740
+ }
741
+ .af-widget-root.af-variant-bare .af-messages {
742
+ background: transparent;
743
+ /* Vertical only. Side gutters belong to the host so messages can
744
+ align with whatever surrounding column the page renders.
745
+ Bottom padding creates breathing room between the last bubble
746
+ and the composer pill — without it the assistant's reply
747
+ visually collides with the input border. */
748
+ padding: 6px 0 14px;
749
+ gap: var(--af-gap, 8px);
750
+ width: 100%;
751
+ box-sizing: border-box;
752
+ /* Let the parent flexbox give us a min-height; we'll grow into it
753
+ and scroll internally when the transcript gets longer. */
754
+ min-height: 0;
755
+ }
756
+ .af-widget-root.af-variant-bare .af-msg {
757
+ font-size: 15px;
758
+ line-height: 1.5;
759
+ border-radius: var(--af-bubble-radius, 18px);
760
+ padding: 10px 14px;
761
+ max-width: 86%;
762
+ }
763
+ .af-widget-root.af-variant-bare .af-msg-user {
764
+ background: var(--af-bubble-user-bg, var(--af-primary));
765
+ color: var(--af-bubble-user-fg, #ffffff);
766
+ border-bottom-right-radius: 6px;
767
+ }
768
+ .af-widget-root.af-variant-bare .af-msg-assistant {
769
+ background: var(--af-bubble-agent-bg, rgba(148, 163, 184, 0.12));
770
+ color: var(--af-bubble-agent-fg, inherit);
771
+ border: none;
772
+ border-bottom-left-radius: 6px;
773
+ }
774
+ .af-widget-root.af-variant-bare .af-msg-assistant a {
775
+ color: var(--af-primary);
776
+ }
777
+ .af-widget-root.af-variant-bare .af-msg-assistant code {
778
+ background: rgba(148, 163, 184, 0.2);
779
+ color: inherit;
780
+ }
781
+ /* Tables inside an assistant bubble. We use @media on viewport
782
+ (not @container) because the bubble row uses max-width 92% and
783
+ any container-type:inline-size ancestor breaks the
784
+ percentage-against-flex-parent sizing, collapsing the bubble to
785
+ one character wide. Trade-off: the editor preview iframe (which
786
+ is 420px inside a desktop browser) wont show the stacked layout
787
+ — fine, the preview is for design checks and the layout
788
+ activates correctly on a real phone. */
789
+ .af-widget-root.af-variant-bare .af-msg-assistant table {
790
+ display: table;
791
+ width: 100%;
792
+ max-width: 100%;
793
+ margin: 6px 0;
794
+ font-size: 13px;
795
+ border-collapse: collapse;
796
+ table-layout: auto;
797
+ }
798
+ .af-widget-root.af-variant-bare .af-msg-assistant thead th {
799
+ text-align: left;
800
+ font-weight: 600;
801
+ padding: 6px 10px;
802
+ border-bottom: 1px solid rgba(148, 163, 184, 0.35);
803
+ white-space: nowrap;
804
+ }
805
+ .af-widget-root.af-variant-bare .af-msg-assistant tbody td {
806
+ padding: 5px 10px;
807
+ border-bottom: 1px solid rgba(148, 163, 184, 0.15);
808
+ vertical-align: top;
809
+ }
810
+ .af-widget-root.af-variant-bare .af-msg-assistant tbody tr:last-child td {
811
+ border-bottom: none;
812
+ }
813
+
814
+ /* Stacked card layout for tables on narrow viewports. Each row
815
+ becomes a small card; each cell shows its column label inline
816
+ via the data-label attribute we stamped on render. No horizontal
817
+ scroll, no truncation. Activates on real mobile (the editor's
818
+ preview iframe stays in the desktop look since it sits inside a
819
+ wide browser viewport — that's intentional). */
820
+ @media (max-width: 480px) {
821
+ .af-widget-root.af-variant-bare .af-msg-assistant table,
822
+ .af-widget-root.af-variant-bare .af-msg-assistant thead,
823
+ .af-widget-root.af-variant-bare .af-msg-assistant tbody,
824
+ .af-widget-root.af-variant-bare .af-msg-assistant tr,
825
+ .af-widget-root.af-variant-bare .af-msg-assistant td,
826
+ .af-widget-root.af-variant-bare .af-msg-assistant th {
827
+ display: block;
828
+ width: 100%;
829
+ }
830
+ .af-widget-root.af-variant-bare .af-msg-assistant thead {
831
+ position: absolute;
832
+ left: -9999px;
833
+ }
834
+ .af-widget-root.af-variant-bare .af-msg-assistant tbody tr {
835
+ border: 1px solid rgba(148, 163, 184, 0.18);
836
+ border-radius: 10px;
837
+ padding: 8px 10px;
838
+ margin: 6px 0;
839
+ }
840
+ .af-widget-root.af-variant-bare .af-msg-assistant tbody td {
841
+ border-bottom: none;
842
+ padding: 3px 0;
843
+ display: flex;
844
+ gap: 8px;
845
+ align-items: baseline;
846
+ flex-wrap: wrap;
847
+ }
848
+ .af-widget-root.af-variant-bare .af-msg-assistant tbody td::before {
849
+ content: attr(data-label);
850
+ flex-shrink: 0;
851
+ min-width: 72px;
852
+ font-weight: 600;
853
+ opacity: 0.65;
854
+ font-size: 11px;
855
+ text-transform: uppercase;
856
+ letter-spacing: 0.04em;
857
+ }
858
+ .af-widget-root.af-variant-bare .af-msg-assistant tbody td[data-label='']::before {
859
+ display: none;
860
+ }
861
+ }
862
+ .af-widget-root.af-variant-bare .af-msg-greeting { animation: af-msg-in 320ms cubic-bezier(0.2, 0.8, 0.2, 1); }
863
+
864
+ /* Slack-style assistant rows: avatar column on the left, bubble on
865
+ * the right. The row IS the alignment container — align-self targets
866
+ * the row, not the bubble, so user/assistant rows still push opposite
867
+ * edges. */
868
+ .af-widget-root.af-variant-bare .af-msg-row {
869
+ display: flex;
870
+ align-items: flex-end;
871
+ gap: 8px;
872
+ max-width: 92%;
873
+ }
874
+ .af-widget-root.af-variant-bare .af-msg-row-assistant {
875
+ align-self: flex-start;
876
+ }
877
+ .af-widget-root.af-variant-bare .af-msg-row .af-msg {
878
+ /* The bubble is now a flex child of the row. Reset the max-width
879
+ * the row already enforces — otherwise we'd stack two caps. */
880
+ max-width: 100%;
881
+ align-self: flex-end;
882
+ }
883
+ .af-widget-root.af-variant-bare .af-msg-avatar {
884
+ width: 26px;
885
+ height: 26px;
886
+ flex-shrink: 0;
887
+ border-radius: 50%;
888
+ overflow: hidden;
889
+ /* Stick to the bubble's bottom edge so multi-line replies still
890
+ * show the avatar next to the last line — same as iMessage. */
891
+ align-self: flex-end;
892
+ margin-bottom: 2px;
893
+ }
894
+ .af-widget-root.af-variant-bare .af-msg-avatar-spacer {
895
+ background: transparent;
896
+ }
897
+ .af-widget-root.af-variant-bare .af-msg-avatar-img {
898
+ object-fit: cover;
899
+ display: block;
900
+ }
901
+ .af-widget-root.af-variant-bare .af-msg-avatar-fallback {
902
+ display: flex;
903
+ align-items: center;
904
+ justify-content: center;
905
+ color: #fff;
906
+ font-size: 11.5px;
907
+ font-weight: 600;
908
+ letter-spacing: 0.01em;
909
+ background-image: linear-gradient(135deg, var(--af-primary, #8b5cf6), color-mix(in srgb, var(--af-primary, #8b5cf6) 60%, #6366f1));
910
+ }
911
+ @media (min-width: 768px) {
912
+ .af-widget-root.af-variant-bare .af-msg-avatar { width: 30px; height: 30px; }
913
+ .af-widget-root.af-variant-bare .af-msg-avatar-fallback { font-size: 13px; }
914
+ }
915
+ .af-widget-root.af-variant-bare .af-input-row {
916
+ /* Plain flex item. af-messages carries flex:1 so it eats the
917
+ remaining space and naturally pushes the input row to the
918
+ bottom of the panel. Sticky positioning across host containers
919
+ was unreliable (different ancestors had different overflow
920
+ semantics); flex layout is deterministic.
921
+ width:100% + box-sizing:border-box so the row fills its
922
+ parent cross-axis instead of shrinking to its children
923
+ intrinsic width — without these, certain ancestor combos
924
+ (Radix portals, Sheet content) left the row offset from the
925
+ edges and pushed the send button out of view. */
926
+ flex-shrink: 0;
927
+ width: 100%;
928
+ box-sizing: border-box;
929
+ /* Vertical only — horizontal padding lives on the host wrapper so
930
+ the input pill can stretch edge-to-edge when the host wants
931
+ (e.g. a sheet drawer). Templates that want side gutters add
932
+ their own padding above the widget. */
933
+ padding: 10px 0 14px;
934
+ border-top: none;
935
+ /* Soft fade so messages scroll behind the input without bleeding
936
+ through. Host can override via --af-input-row-bg (e.g. solid
937
+ color) for templates that want a hard band instead. */
938
+ background: var(
939
+ --af-input-row-bg,
940
+ linear-gradient(to top, rgba(255, 255, 255, 0.92) 60%, rgba(255, 255, 255, 0))
941
+ );
942
+ backdrop-filter: saturate(140%) blur(8px);
943
+ -webkit-backdrop-filter: saturate(140%) blur(8px);
944
+ gap: 8px;
945
+ }
946
+ @media (prefers-color-scheme: dark) {
947
+ .af-widget-root.af-variant-bare .af-input-row {
948
+ background: var(
949
+ --af-input-row-bg,
950
+ linear-gradient(to top, rgba(15, 23, 42, 0.85) 60%, rgba(15, 23, 42, 0))
951
+ );
952
+ }
953
+ }
954
+ /* In the bare variant the shortcut row also lives directly above
955
+ the composer. Tighten the gap so the chips read as a continuous
956
+ stack with the input. */
957
+ .af-widget-root.af-variant-bare .af-shortcut-row {
958
+ padding: 0 12px 6px;
959
+ background: transparent;
960
+ }
961
+ .af-widget-root.af-variant-bare .af-shortcut-row + .af-input-row {
962
+ padding-top: 2px;
963
+ }
964
+ .af-widget-root.af-variant-bare .af-input {
965
+ background: var(--af-input-bg, rgba(148, 163, 184, 0.10));
966
+ /* Foreground is overridable. Defaults to currentColor so the
967
+ input inherits the page text color (light text in dark mode,
968
+ dark text in light mode) instead of getting frozen at the
969
+ bg's "neutral" value, which was causing low-contrast text on
970
+ dark backgrounds when the host overrode --af-input-bg. */
971
+ color: var(--af-input-fg, currentColor);
972
+ border: 1px solid var(--af-input-border, rgba(148, 163, 184, 0.25));
973
+ border-radius: var(--af-input-radius, 24px);
974
+ padding: 12px 16px;
975
+ font-size: 15px;
976
+ min-height: 48px;
977
+ line-height: 1.4;
978
+ }
979
+ .af-widget-root.af-variant-bare .af-input::placeholder {
980
+ color: var(--af-input-placeholder, currentColor);
981
+ opacity: 0.5;
982
+ }
983
+ .af-widget-root.af-variant-bare .af-input:focus {
984
+ border-color: var(--af-primary);
985
+ box-shadow: 0 0 0 3px var(--af-primary-soft);
986
+ }
987
+ .af-widget-root.af-variant-bare .af-input:disabled { background: rgba(148, 163, 184, 0.06); }
988
+ .af-widget-root.af-variant-bare .af-send {
989
+ /* Host overrides via --af-send-bg; otherwise the primary color
990
+ drives the send button. Same fallback pattern used elsewhere
991
+ so templates can opt into different palettes per surface
992
+ without redeclaring the primary. */
993
+ background: var(--af-send-bg, var(--af-primary));
994
+ color: #fff;
995
+ border-radius: 50%;
996
+ width: 44px;
997
+ height: 44px;
998
+ min-height: 44px;
999
+ padding: 0;
1000
+ align-self: flex-end;
1001
+ box-shadow: 0 6px 16px var(--af-primary-soft);
1002
+ }
1003
+ .af-widget-root.af-variant-bare .af-error {
1004
+ background: transparent;
1005
+ border-top: none;
1006
+ padding: 6px 14px;
1007
+ }
1008
+ .af-widget-root.af-variant-bare .af-typing-dots span { background: currentColor; opacity: 0.5; }
1009
+
1010
+ /* On narrow viewports add safe-area padding so the input clears the
1011
+ * home indicator on iPhones. Base sticky + backdrop styles above
1012
+ * apply at every viewport — no override needed. */
1013
+ @media (max-width: 640px) {
1014
+ .af-widget-root.af-variant-bare .af-input-row {
1015
+ bottom: env(safe-area-inset-bottom, 0);
1016
+ padding-bottom: calc(14px + env(safe-area-inset-bottom, 0px));
1017
+ }
1018
+ }
549
1019
  `;
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.25",
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",