@agentforge-io/chat-sdk 2.0.24 → 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 +33 -0
- package/dist/react.js +384 -42
- package/package.json +1 -1
package/dist/react.d.ts
CHANGED
|
@@ -127,6 +127,39 @@ export interface ChatWidgetProps {
|
|
|
127
127
|
/** Input placeholder. Defaults to "Type a message…" — override when
|
|
128
128
|
* the persona speaks a different language. */
|
|
129
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;
|
|
130
163
|
}
|
|
131
164
|
/**
|
|
132
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,7 @@ 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, variant = 'card', greeting, personaName, inputPlaceholder, } = props;
|
|
230
|
+
const { token, apiBaseUrl, inline = false, position, browserSessionId, resumeConversationId, stream, className, style, onApprovalDecision, readOnlyApprovals = false, variant = 'card', greeting, personaName, shortcuts, onShortcutClick, inputPlaceholder, } = props;
|
|
185
231
|
const bare = variant === 'bare';
|
|
186
232
|
const [session, setSession] = (0, react_1.useState)(null);
|
|
187
233
|
const [status, setStatus] = (0, react_1.useState)('idle');
|
|
@@ -303,27 +349,45 @@ function ChatWidget(props) {
|
|
|
303
349
|
};
|
|
304
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 ? (
|
|
305
351
|
// eslint-disable-next-line @next/next/no-img-element
|
|
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.
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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, }) {
|
|
321
382
|
const kind = message.metadata?.kind;
|
|
322
383
|
if (kind === 'awaiting_approval') {
|
|
323
|
-
|
|
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 }));
|
|
324
388
|
}
|
|
325
389
|
if (kind === 'tool_blocked') {
|
|
326
|
-
return (0, jsx_runtime_1.jsx)(BlockedBubble, { message: message });
|
|
390
|
+
return wrapAssistantRow(bare, showAvatar, avatarTheme, avatarName, (0, jsx_runtime_1.jsx)(BlockedBubble, { message: message }));
|
|
327
391
|
}
|
|
328
392
|
const cls = `af-msg af-msg-${message.role}${message.role === 'assistant' && message.isStreaming
|
|
329
393
|
? message.content
|
|
@@ -332,17 +396,46 @@ function MessageBubble({ message, session, readOnly, onDecision, onContinue, })
|
|
|
332
396
|
: ''}`;
|
|
333
397
|
// Typing state (no content yet): render the three-dot indicator, no markdown.
|
|
334
398
|
if (message.role === 'assistant' && message.isStreaming && !message.content) {
|
|
335
|
-
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", {})] }) }));
|
|
336
400
|
}
|
|
337
401
|
if (message.role === 'assistant') {
|
|
338
|
-
return ((0, jsx_runtime_1.jsx)("div", { className: cls,
|
|
402
|
+
return wrapAssistantRow(bare, showAvatar, avatarTheme, avatarName, (0, jsx_runtime_1.jsx)("div", { className: cls,
|
|
339
403
|
// Output is sanitized by escapeHtml + a fixed tag whitelist in
|
|
340
404
|
// renderMarkdown — safe to inject as HTML.
|
|
341
405
|
dangerouslySetInnerHTML: { __html: renderMarkdown(message.content) } }));
|
|
342
406
|
}
|
|
343
407
|
// User & system messages stay as plain text — they're typed verbatim.
|
|
408
|
+
// No avatar column on the user side; they align right.
|
|
344
409
|
return (0, jsx_runtime_1.jsx)("div", { className: cls, children: message.content });
|
|
345
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
|
+
}
|
|
346
439
|
/**
|
|
347
440
|
* Awaiting-approval bubble. Renders the tool name + a countdown, and
|
|
348
441
|
* either the Approve/Deny buttons (when the host wired the handler)
|
|
@@ -517,6 +610,52 @@ const WIDGET_CSS = `
|
|
|
517
610
|
to { opacity: 1; transform: translateY(0); }
|
|
518
611
|
}
|
|
519
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); }
|
|
520
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; }
|
|
521
660
|
.af-input:focus { border-color: var(--af-primary); box-shadow: 0 0 0 3px var(--af-primary-soft); }
|
|
522
661
|
.af-input:disabled { background: var(--af-bubble-bg); cursor: not-allowed; }
|
|
@@ -567,6 +706,23 @@ const WIDGET_CSS = `
|
|
|
567
706
|
* --af-gap spacing between messages (default: 12px)
|
|
568
707
|
*/
|
|
569
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
|
+
}
|
|
570
726
|
.af-widget-root.af-variant-bare.af-inline .af-panel,
|
|
571
727
|
.af-widget-root.af-variant-bare .af-panel {
|
|
572
728
|
position: relative;
|
|
@@ -579,11 +735,20 @@ const WIDGET_CSS = `
|
|
|
579
735
|
overflow: visible;
|
|
580
736
|
display: flex;
|
|
581
737
|
flex-direction: column;
|
|
738
|
+
flex: 1;
|
|
739
|
+
min-height: 0;
|
|
582
740
|
}
|
|
583
741
|
.af-widget-root.af-variant-bare .af-messages {
|
|
584
742
|
background: transparent;
|
|
585
|
-
|
|
586
|
-
|
|
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;
|
|
587
752
|
/* Let the parent flexbox give us a min-height; we'll grow into it
|
|
588
753
|
and scroll internally when the transcript gets longer. */
|
|
589
754
|
min-height: 0;
|
|
@@ -613,18 +778,197 @@ const WIDGET_CSS = `
|
|
|
613
778
|
background: rgba(148, 163, 184, 0.2);
|
|
614
779
|
color: inherit;
|
|
615
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
|
+
}
|
|
616
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
|
+
}
|
|
617
915
|
.af-widget-root.af-variant-bare .af-input-row {
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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;
|
|
621
934
|
border-top: none;
|
|
622
|
-
|
|
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);
|
|
623
944
|
gap: 8px;
|
|
624
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
|
+
}
|
|
625
964
|
.af-widget-root.af-variant-bare .af-input {
|
|
626
965
|
background: var(--af-input-bg, rgba(148, 163, 184, 0.10));
|
|
627
|
-
|
|
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);
|
|
628
972
|
border: 1px solid var(--af-input-border, rgba(148, 163, 184, 0.25));
|
|
629
973
|
border-radius: var(--af-input-radius, 24px);
|
|
630
974
|
padding: 12px 16px;
|
|
@@ -632,13 +976,21 @@ const WIDGET_CSS = `
|
|
|
632
976
|
min-height: 48px;
|
|
633
977
|
line-height: 1.4;
|
|
634
978
|
}
|
|
979
|
+
.af-widget-root.af-variant-bare .af-input::placeholder {
|
|
980
|
+
color: var(--af-input-placeholder, currentColor);
|
|
981
|
+
opacity: 0.5;
|
|
982
|
+
}
|
|
635
983
|
.af-widget-root.af-variant-bare .af-input:focus {
|
|
636
984
|
border-color: var(--af-primary);
|
|
637
985
|
box-shadow: 0 0 0 3px var(--af-primary-soft);
|
|
638
986
|
}
|
|
639
987
|
.af-widget-root.af-variant-bare .af-input:disabled { background: rgba(148, 163, 184, 0.06); }
|
|
640
988
|
.af-widget-root.af-variant-bare .af-send {
|
|
641
|
-
|
|
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));
|
|
642
994
|
color: #fff;
|
|
643
995
|
border-radius: 50%;
|
|
644
996
|
width: 44px;
|
|
@@ -655,23 +1007,13 @@ const WIDGET_CSS = `
|
|
|
655
1007
|
}
|
|
656
1008
|
.af-widget-root.af-variant-bare .af-typing-dots span { background: currentColor; opacity: 0.5; }
|
|
657
1009
|
|
|
658
|
-
/*
|
|
659
|
-
*
|
|
660
|
-
*
|
|
661
|
-
* sticky behaviour scopes to the chat container, which is enough. */
|
|
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. */
|
|
662
1013
|
@media (max-width: 640px) {
|
|
663
1014
|
.af-widget-root.af-variant-bare .af-input-row {
|
|
664
|
-
position: sticky;
|
|
665
1015
|
bottom: env(safe-area-inset-bottom, 0);
|
|
666
1016
|
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
1017
|
}
|
|
676
1018
|
}
|
|
677
1019
|
`;
|
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.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",
|