@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 +63 -0
- package/dist/react.js +492 -22
- package/package.json +1 -1
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.
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|