@agentforge-io/chat-sdk 2.1.1 → 2.3.0
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/ChatDrawer.d.ts +91 -0
- package/dist/ChatDrawer.js +230 -0
- package/dist/entities.d.ts +11 -0
- package/dist/react.d.ts +30 -0
- package/dist/react.js +135 -18
- package/dist/session.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<ChatDrawer>` — standard bottom-sheet wrapper around `<ChatWidget>`.
|
|
3
|
+
*
|
|
4
|
+
* The host gives us:
|
|
5
|
+
* - `open` / `onOpenChange`: controlled visibility (host owns the route /
|
|
6
|
+
* URL sync, the SDK doesn't touch the URL).
|
|
7
|
+
* - All the `<ChatWidget>` props (token, apiBaseUrl, etc.): forwarded
|
|
8
|
+
* verbatim. The widget mounts INSIDE the drawer.
|
|
9
|
+
*
|
|
10
|
+
* What the drawer adds on top:
|
|
11
|
+
* - Mounts via React portal (`document.body`) so it overlays the page
|
|
12
|
+
* regardless of the host's stacking context. Lazy-mounted: the portal
|
|
13
|
+
* target is computed at first open so SSR stays clean.
|
|
14
|
+
* - Visually pinned to the bottom of the *visual viewport* (not the
|
|
15
|
+
* layout viewport). On iOS Safari and Android Chrome the on-screen
|
|
16
|
+
* keyboard shrinks `window.visualViewport.height`; we listen to
|
|
17
|
+
* `resize` / `scroll` on visualViewport and reflow the drawer's
|
|
18
|
+
* `height` + `bottom` so the composer never gets clipped by the
|
|
19
|
+
* keyboard.
|
|
20
|
+
* - Opens at a configurable snap fraction (default 0.98 = 98% of the
|
|
21
|
+
* visible viewport). The visitor sees a thin sliver of the page
|
|
22
|
+
* underneath, which keeps context and lets a tap-outside dismiss.
|
|
23
|
+
* - Drag-to-dismiss with vanilla touch events: drag the handle down
|
|
24
|
+
* past 30% of the panel height and the drawer closes. No external
|
|
25
|
+
* deps — the SDK stays portable.
|
|
26
|
+
* - Survives close+reopen: the widget inside is rendered once and kept
|
|
27
|
+
* alive (CSS `display:none` toggle when closed, NOT unmounted), so
|
|
28
|
+
* the chat session, transcript, and any in-flight tool calls stay
|
|
29
|
+
* intact when the visitor closes and reopens the drawer.
|
|
30
|
+
*
|
|
31
|
+
* What the drawer does NOT do (deliberately):
|
|
32
|
+
* - It doesn't sync with the URL. The host decides whether `?view=chat`,
|
|
33
|
+
* `/chat`, or any other route shape opens it.
|
|
34
|
+
* - It doesn't manage the chat session lifecycle. That's `<ChatWidget>`'s
|
|
35
|
+
* job. We just provide presentation.
|
|
36
|
+
* - It doesn't render a "fake composer" to trigger opening. The host
|
|
37
|
+
* decides what trigger UX makes sense (button, input pill, FAB, etc.)
|
|
38
|
+
* and calls `onOpenChange(true)`.
|
|
39
|
+
*/
|
|
40
|
+
import { type ReactNode } from 'react';
|
|
41
|
+
import { type ChatWidgetProps } from './react';
|
|
42
|
+
/**
|
|
43
|
+
* Drawer accepts the chat surface in two shapes:
|
|
44
|
+
*
|
|
45
|
+
* - `widgetProps`: pass the ChatWidget configuration and the drawer
|
|
46
|
+
* creates the widget internally. The standard / forward-looking
|
|
47
|
+
* API — most consumers should use this.
|
|
48
|
+
*
|
|
49
|
+
* - `chatSlot`: pass a pre-rendered React node (typically a
|
|
50
|
+
* `<ChatWidget>` instance the host wired up itself). Useful when
|
|
51
|
+
* the host already orchestrates the widget (multiple chat slots,
|
|
52
|
+
* custom decorators, legacy code) and just wants the drawer's
|
|
53
|
+
* positioning + drag UX on top.
|
|
54
|
+
*
|
|
55
|
+
* Exactly one of the two MUST be passed. Mutual exclusivity isn't
|
|
56
|
+
* encoded at the type level (TS unions with optional props get noisy)
|
|
57
|
+
* — the runtime asserts gracefully if neither is present.
|
|
58
|
+
*/
|
|
59
|
+
type ChatSurface = {
|
|
60
|
+
widgetProps: ChatWidgetProps;
|
|
61
|
+
chatSlot?: never;
|
|
62
|
+
} | {
|
|
63
|
+
chatSlot: ReactNode;
|
|
64
|
+
widgetProps?: never;
|
|
65
|
+
};
|
|
66
|
+
export type ChatDrawerProps = ChatSurface & {
|
|
67
|
+
/** Whether the drawer is visible. Controlled. */
|
|
68
|
+
open: boolean;
|
|
69
|
+
/** Fired when the drawer wants to close (drag-to-dismiss, tap on the
|
|
70
|
+
* backdrop, close button). Host is responsible for setting `open=false`. */
|
|
71
|
+
onOpenChange: (open: boolean) => void;
|
|
72
|
+
/** Snap fraction of the visible viewport, 0–1. Defaults to 0.98 (98% =
|
|
73
|
+
* the standard "near-fullscreen drawer that still hints at the page
|
|
74
|
+
* below" pattern). Lower numbers leave more of the page visible. */
|
|
75
|
+
snap?: number;
|
|
76
|
+
/** Display in the drawer header above the chat. Templates pass the
|
|
77
|
+
* agent / team identity here. When omitted, the drawer renders a
|
|
78
|
+
* bare drag handle only — useful for hosts that want a borderless
|
|
79
|
+
* panel. */
|
|
80
|
+
header?: ReactNode;
|
|
81
|
+
/** Backdrop click closes the drawer. Default true — set false if the
|
|
82
|
+
* host wants a "modal" feel where the only escape is the close button
|
|
83
|
+
* or the drag-down gesture. */
|
|
84
|
+
closeOnBackdropClick?: boolean;
|
|
85
|
+
/** Extra class on the drawer's root surface (the white card). Use it to
|
|
86
|
+
* add a custom shadow / border colour. The CSS vars on `--af-*` already
|
|
87
|
+
* let you re-theme the chat widget itself; this is for the SURROUND. */
|
|
88
|
+
drawerClassName?: string;
|
|
89
|
+
};
|
|
90
|
+
export declare function ChatDrawer(props: ChatDrawerProps): JSX.Element | null;
|
|
91
|
+
export {};
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ChatDrawer = ChatDrawer;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
/**
|
|
6
|
+
* `<ChatDrawer>` — standard bottom-sheet wrapper around `<ChatWidget>`.
|
|
7
|
+
*
|
|
8
|
+
* The host gives us:
|
|
9
|
+
* - `open` / `onOpenChange`: controlled visibility (host owns the route /
|
|
10
|
+
* URL sync, the SDK doesn't touch the URL).
|
|
11
|
+
* - All the `<ChatWidget>` props (token, apiBaseUrl, etc.): forwarded
|
|
12
|
+
* verbatim. The widget mounts INSIDE the drawer.
|
|
13
|
+
*
|
|
14
|
+
* What the drawer adds on top:
|
|
15
|
+
* - Mounts via React portal (`document.body`) so it overlays the page
|
|
16
|
+
* regardless of the host's stacking context. Lazy-mounted: the portal
|
|
17
|
+
* target is computed at first open so SSR stays clean.
|
|
18
|
+
* - Visually pinned to the bottom of the *visual viewport* (not the
|
|
19
|
+
* layout viewport). On iOS Safari and Android Chrome the on-screen
|
|
20
|
+
* keyboard shrinks `window.visualViewport.height`; we listen to
|
|
21
|
+
* `resize` / `scroll` on visualViewport and reflow the drawer's
|
|
22
|
+
* `height` + `bottom` so the composer never gets clipped by the
|
|
23
|
+
* keyboard.
|
|
24
|
+
* - Opens at a configurable snap fraction (default 0.98 = 98% of the
|
|
25
|
+
* visible viewport). The visitor sees a thin sliver of the page
|
|
26
|
+
* underneath, which keeps context and lets a tap-outside dismiss.
|
|
27
|
+
* - Drag-to-dismiss with vanilla touch events: drag the handle down
|
|
28
|
+
* past 30% of the panel height and the drawer closes. No external
|
|
29
|
+
* deps — the SDK stays portable.
|
|
30
|
+
* - Survives close+reopen: the widget inside is rendered once and kept
|
|
31
|
+
* alive (CSS `display:none` toggle when closed, NOT unmounted), so
|
|
32
|
+
* the chat session, transcript, and any in-flight tool calls stay
|
|
33
|
+
* intact when the visitor closes and reopens the drawer.
|
|
34
|
+
*
|
|
35
|
+
* What the drawer does NOT do (deliberately):
|
|
36
|
+
* - It doesn't sync with the URL. The host decides whether `?view=chat`,
|
|
37
|
+
* `/chat`, or any other route shape opens it.
|
|
38
|
+
* - It doesn't manage the chat session lifecycle. That's `<ChatWidget>`'s
|
|
39
|
+
* job. We just provide presentation.
|
|
40
|
+
* - It doesn't render a "fake composer" to trigger opening. The host
|
|
41
|
+
* decides what trigger UX makes sense (button, input pill, FAB, etc.)
|
|
42
|
+
* and calls `onOpenChange(true)`.
|
|
43
|
+
*/
|
|
44
|
+
const react_1 = require("react");
|
|
45
|
+
const react_dom_1 = require("react-dom");
|
|
46
|
+
const react_2 = require("./react");
|
|
47
|
+
function ChatDrawer(props) {
|
|
48
|
+
const { open, onOpenChange, snap = 0.98, header, closeOnBackdropClick = true, drawerClassName, widgetProps, chatSlot, } = props;
|
|
49
|
+
// We mount once and keep alive across close→reopen so the chat session
|
|
50
|
+
// doesn't get destroyed. After the FIRST open the panel stays in the
|
|
51
|
+
// DOM forever (toggled by `display:none`) — `hasOpened` gates the
|
|
52
|
+
// initial mount so SSR doesn't render an empty portal.
|
|
53
|
+
const [hasOpened, setHasOpened] = (0, react_1.useState)(open);
|
|
54
|
+
(0, react_1.useEffect)(() => {
|
|
55
|
+
if (open)
|
|
56
|
+
setHasOpened(true);
|
|
57
|
+
}, [open]);
|
|
58
|
+
// Visible viewport tracking. iOS Safari + Android Chrome shrink
|
|
59
|
+
// `visualViewport.height` when the on-screen keyboard pops up; the
|
|
60
|
+
// layout viewport stays the same. We pin the drawer's height + bottom
|
|
61
|
+
// to the visual viewport so the composer is always above the keyboard.
|
|
62
|
+
const [vv, setVv] = (0, react_1.useState)(() => ({
|
|
63
|
+
h: typeof window === 'undefined' ? 0 : window.innerHeight,
|
|
64
|
+
offsetTop: 0,
|
|
65
|
+
}));
|
|
66
|
+
(0, react_1.useEffect)(() => {
|
|
67
|
+
if (typeof window === 'undefined')
|
|
68
|
+
return;
|
|
69
|
+
const view = window.visualViewport;
|
|
70
|
+
if (!view)
|
|
71
|
+
return;
|
|
72
|
+
const update = () => setVv({ h: view.height, offsetTop: view.offsetTop });
|
|
73
|
+
update();
|
|
74
|
+
view.addEventListener('resize', update);
|
|
75
|
+
view.addEventListener('scroll', update);
|
|
76
|
+
return () => {
|
|
77
|
+
view.removeEventListener('resize', update);
|
|
78
|
+
view.removeEventListener('scroll', update);
|
|
79
|
+
};
|
|
80
|
+
}, []);
|
|
81
|
+
// Snap-point height: % of the visible viewport. Recomputed when vv
|
|
82
|
+
// changes so a keyboard popup keeps the drawer aligned.
|
|
83
|
+
const drawerHeight = Math.max(0, Math.round(vv.h * Math.min(Math.max(snap, 0.1), 1)));
|
|
84
|
+
// Drag-to-dismiss. Vanilla touch handlers — no library. We track
|
|
85
|
+
// pointerdown on the handle, follow movement on pointermove, and
|
|
86
|
+
// decide on pointerup whether the drag crossed the "dismiss" threshold
|
|
87
|
+
// (30% of the panel height).
|
|
88
|
+
const surfaceRef = (0, react_1.useRef)(null);
|
|
89
|
+
const dragStateRef = (0, react_1.useRef)(null);
|
|
90
|
+
const [dragOffset, setDragOffset] = (0, react_1.useState)(0);
|
|
91
|
+
const onHandlePointerDown = (0, react_1.useCallback)((e) => {
|
|
92
|
+
if (e.button !== 0 && e.pointerType !== 'touch')
|
|
93
|
+
return;
|
|
94
|
+
e.currentTarget.setPointerCapture?.(e.pointerId);
|
|
95
|
+
dragStateRef.current = {
|
|
96
|
+
startY: e.clientY,
|
|
97
|
+
startTime: Date.now(),
|
|
98
|
+
dragging: true,
|
|
99
|
+
};
|
|
100
|
+
}, []);
|
|
101
|
+
const onHandlePointerMove = (0, react_1.useCallback)((e) => {
|
|
102
|
+
const s = dragStateRef.current;
|
|
103
|
+
if (!s?.dragging)
|
|
104
|
+
return;
|
|
105
|
+
const delta = Math.max(0, e.clientY - s.startY);
|
|
106
|
+
setDragOffset(delta);
|
|
107
|
+
}, []);
|
|
108
|
+
const onHandlePointerUp = (0, react_1.useCallback)((e) => {
|
|
109
|
+
const s = dragStateRef.current;
|
|
110
|
+
if (!s?.dragging)
|
|
111
|
+
return;
|
|
112
|
+
dragStateRef.current = null;
|
|
113
|
+
try {
|
|
114
|
+
e.currentTarget.releasePointerCapture?.(e.pointerId);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Pointer was already released by some other code path; nothing
|
|
118
|
+
// to do here. Releasing a non-captured pointer throws — swallow.
|
|
119
|
+
}
|
|
120
|
+
const delta = Math.max(0, e.clientY - s.startY);
|
|
121
|
+
const dismissThreshold = drawerHeight * 0.3;
|
|
122
|
+
const elapsed = Date.now() - s.startTime;
|
|
123
|
+
// Dismiss on EITHER a long-drag (past the threshold) or a quick
|
|
124
|
+
// flick (small distance but high velocity). Velocity unit is
|
|
125
|
+
// px/ms; >0.5 is roughly the threshold iOS sheets use.
|
|
126
|
+
const velocity = elapsed > 0 ? delta / elapsed : 0;
|
|
127
|
+
if (delta > dismissThreshold || velocity > 0.5) {
|
|
128
|
+
onOpenChange(false);
|
|
129
|
+
}
|
|
130
|
+
setDragOffset(0);
|
|
131
|
+
}, [drawerHeight, onOpenChange]);
|
|
132
|
+
// Lock body scroll while the drawer is open so the page underneath
|
|
133
|
+
// doesn't move when the visitor scrolls inside the chat. Restored on
|
|
134
|
+
// close. Necessary on iOS Safari where the rubber-band scroll bleeds
|
|
135
|
+
// through to the body even with overflow:hidden on a child.
|
|
136
|
+
(0, react_1.useEffect)(() => {
|
|
137
|
+
if (typeof document === 'undefined')
|
|
138
|
+
return;
|
|
139
|
+
if (!open)
|
|
140
|
+
return;
|
|
141
|
+
const prev = document.body.style.overflow;
|
|
142
|
+
document.body.style.overflow = 'hidden';
|
|
143
|
+
return () => {
|
|
144
|
+
document.body.style.overflow = prev;
|
|
145
|
+
};
|
|
146
|
+
}, [open]);
|
|
147
|
+
// Escape closes the drawer. Keyboard-friendly even when focus is in
|
|
148
|
+
// the textarea — preventDefault on the input itself swallows Escape
|
|
149
|
+
// before it bubbles, so we use capture phase.
|
|
150
|
+
(0, react_1.useEffect)(() => {
|
|
151
|
+
if (!open)
|
|
152
|
+
return;
|
|
153
|
+
if (typeof window === 'undefined')
|
|
154
|
+
return;
|
|
155
|
+
const onKey = (e) => {
|
|
156
|
+
if (e.key === 'Escape')
|
|
157
|
+
onOpenChange(false);
|
|
158
|
+
};
|
|
159
|
+
window.addEventListener('keydown', onKey, true);
|
|
160
|
+
return () => window.removeEventListener('keydown', onKey, true);
|
|
161
|
+
}, [open, onOpenChange]);
|
|
162
|
+
// Lazy portal target. We render to `document.body` so the drawer
|
|
163
|
+
// overlays everything regardless of the consumer's stacking context.
|
|
164
|
+
const [portalEl, setPortalEl] = (0, react_1.useState)(null);
|
|
165
|
+
(0, react_1.useLayoutEffect)(() => {
|
|
166
|
+
if (typeof document === 'undefined')
|
|
167
|
+
return;
|
|
168
|
+
setPortalEl(document.body);
|
|
169
|
+
}, []);
|
|
170
|
+
// Stable id so the drag handle's `aria-controls` can point at the
|
|
171
|
+
// panel. Required for screen readers to announce the relationship.
|
|
172
|
+
const panelId = (0, react_1.useId)();
|
|
173
|
+
if (!portalEl)
|
|
174
|
+
return null;
|
|
175
|
+
if (!hasOpened)
|
|
176
|
+
return null;
|
|
177
|
+
// The translate3d ensures the drawer animates from below on first
|
|
178
|
+
// open (initial transform = 100%, becomes 0% after the open prop
|
|
179
|
+
// flips). When dragging we add the manual drag offset on top.
|
|
180
|
+
const translateY = open ? `${dragOffset}px` : `${drawerHeight}px`;
|
|
181
|
+
// Transition disabled WHILE dragging so the surface follows the
|
|
182
|
+
// finger 1:1. Re-enabled at the end of the drag (delta back to 0
|
|
183
|
+
// smoothly when below threshold).
|
|
184
|
+
const isDragging = dragStateRef.current?.dragging === true;
|
|
185
|
+
const surfaceStyle = {
|
|
186
|
+
height: `${drawerHeight}px`,
|
|
187
|
+
transform: `translate3d(0, ${translateY}, 0)`,
|
|
188
|
+
bottom: `${vv.offsetTop}px`,
|
|
189
|
+
transition: isDragging ? 'none' : 'transform 240ms cubic-bezier(.32,.72,0,1)',
|
|
190
|
+
willChange: 'transform',
|
|
191
|
+
};
|
|
192
|
+
return (0, react_dom_1.createPortal)((0, jsx_runtime_1.jsxs)("div", { className: "af-drawer-root", "data-state": open ? 'open' : 'closed', style: {
|
|
193
|
+
position: 'fixed',
|
|
194
|
+
inset: 0,
|
|
195
|
+
zIndex: 2147483600,
|
|
196
|
+
pointerEvents: open ? 'auto' : 'none',
|
|
197
|
+
}, children: [(0, jsx_runtime_1.jsx)("div", { className: "af-drawer-backdrop", onClick: closeOnBackdropClick ? () => onOpenChange(false) : undefined, style: {
|
|
198
|
+
position: 'absolute',
|
|
199
|
+
inset: 0,
|
|
200
|
+
backgroundColor: 'rgba(0, 0, 0, 0.4)',
|
|
201
|
+
opacity: open ? 1 : 0,
|
|
202
|
+
transition: 'opacity 200ms ease-out',
|
|
203
|
+
}, "aria-hidden": true }), (0, jsx_runtime_1.jsxs)("div", { ref: surfaceRef, id: panelId, role: "dialog", "aria-modal": "true", className: `af-drawer-surface ${drawerClassName ?? ''}`, style: {
|
|
204
|
+
position: 'absolute',
|
|
205
|
+
left: 0,
|
|
206
|
+
right: 0,
|
|
207
|
+
width: '100%',
|
|
208
|
+
backgroundColor: 'var(--af-bg, #ffffff)',
|
|
209
|
+
borderTopLeftRadius: '16px',
|
|
210
|
+
borderTopRightRadius: '16px',
|
|
211
|
+
boxShadow: '0 -8px 24px rgba(15, 23, 42, 0.15)',
|
|
212
|
+
display: 'flex',
|
|
213
|
+
flexDirection: 'column',
|
|
214
|
+
overflow: 'hidden',
|
|
215
|
+
...surfaceStyle,
|
|
216
|
+
}, children: [(0, jsx_runtime_1.jsx)("div", { onPointerDown: onHandlePointerDown, onPointerMove: onHandlePointerMove, onPointerUp: onHandlePointerUp, onPointerCancel: onHandlePointerUp, style: {
|
|
217
|
+
padding: '10px 0',
|
|
218
|
+
cursor: 'grab',
|
|
219
|
+
touchAction: 'none',
|
|
220
|
+
display: 'flex',
|
|
221
|
+
justifyContent: 'center',
|
|
222
|
+
flexShrink: 0,
|
|
223
|
+
}, "aria-label": "Drag to close", children: (0, jsx_runtime_1.jsx)("span", { style: {
|
|
224
|
+
width: '40px',
|
|
225
|
+
height: '4px',
|
|
226
|
+
borderRadius: '999px',
|
|
227
|
+
backgroundColor: 'var(--af-muted, rgba(100, 116, 139, 0.45))',
|
|
228
|
+
display: 'block',
|
|
229
|
+
} }) }), header, (0, jsx_runtime_1.jsx)("div", { style: { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }, children: chatSlot ?? (widgetProps ? ((0, jsx_runtime_1.jsx)(react_2.ChatWidget, { ...widgetProps, inline: true, variant: widgetProps.variant ?? 'bare' })) : null) })] })] }), portalEl);
|
|
230
|
+
}
|
package/dist/entities.d.ts
CHANGED
|
@@ -160,6 +160,17 @@ export type ChatEvent = {
|
|
|
160
160
|
type: 'error';
|
|
161
161
|
message: string;
|
|
162
162
|
code?: string;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Emitted exactly once per session, the moment the server hands us
|
|
166
|
+
* back the new conversation id after the visitor sent the first
|
|
167
|
+
* message. View layers use this to write the id into URL query
|
|
168
|
+
* (`?c=<id>`), localStorage, etc. so a refresh / drawer-reopen
|
|
169
|
+
* resumes the same conversation instead of starting fresh.
|
|
170
|
+
*/
|
|
171
|
+
| {
|
|
172
|
+
type: 'conversation_started';
|
|
173
|
+
conversationId: string;
|
|
163
174
|
} | {
|
|
164
175
|
type: 'destroyed';
|
|
165
176
|
};
|
package/dist/react.d.ts
CHANGED
|
@@ -77,6 +77,14 @@ export interface ChatWidgetProps {
|
|
|
77
77
|
browserSessionId?: string;
|
|
78
78
|
/** Existing conversation id to resume. */
|
|
79
79
|
resumeConversationId?: string;
|
|
80
|
+
/**
|
|
81
|
+
* Fired exactly once per session, when the server hands back the
|
|
82
|
+
* new conversation id after the visitor sends their first message.
|
|
83
|
+
* Hosts use this to write the id to URL query, localStorage, etc.
|
|
84
|
+
* so a refresh or drawer-reopen resumes the same conversation.
|
|
85
|
+
* Not called when resuming an existing conversation (the host
|
|
86
|
+
* already has the id in that case). */
|
|
87
|
+
onConversationStart?: (conversationId: string) => void;
|
|
80
88
|
/** Disable SSE streaming and use plain POSTs. */
|
|
81
89
|
stream?: boolean;
|
|
82
90
|
/** Extra class on the root container. */
|
|
@@ -151,6 +159,17 @@ export interface ChatWidgetProps {
|
|
|
151
159
|
* behaviour).
|
|
152
160
|
*/
|
|
153
161
|
members?: ChatTeamMember[];
|
|
162
|
+
/**
|
|
163
|
+
* Slot rendered inside the composer row, BEFORE the textarea. Hosts
|
|
164
|
+
* use this for affordances that scope or augment the next turn —
|
|
165
|
+
* e.g. a Team chat's member-picker chip (the "@<agent>" affordance),
|
|
166
|
+
* a tools menu à la Gemini, an attachment button.
|
|
167
|
+
*
|
|
168
|
+
* Kept as a generic React slot rather than a typed prop list so the
|
|
169
|
+
* SDK doesn't have to learn every product surface that wants to put
|
|
170
|
+
* something there.
|
|
171
|
+
*/
|
|
172
|
+
composerLeftSlot?: React.ReactNode;
|
|
154
173
|
/**
|
|
155
174
|
* Imperative handle. The widget fills this ref on mount with a
|
|
156
175
|
* small command object the host can call to drive the session
|
|
@@ -170,9 +189,20 @@ export interface ChatWidgetHandle {
|
|
|
170
189
|
* entirely. No-op if the session isn't ready yet (status ===
|
|
171
190
|
* 'idle' / 'loading'). */
|
|
172
191
|
sendNow(text: string): void;
|
|
192
|
+
/**
|
|
193
|
+
* Insert `text` into the composer's draft at the current cursor
|
|
194
|
+
* position (replacing any selection). Used by mention pickers,
|
|
195
|
+
* suggested-fragment chips, or slash-menu commands — the user
|
|
196
|
+
* sees the text appear in the input as if they had typed it and
|
|
197
|
+
* can keep editing before sending. Focus stays on the textarea
|
|
198
|
+
* and the cursor lands at the end of the inserted text.
|
|
199
|
+
*/
|
|
200
|
+
insertText(text: string): void;
|
|
173
201
|
}
|
|
174
202
|
/**
|
|
175
203
|
* Drop-in chat widget. Owns its own `ChatSession` and re-renders on every
|
|
176
204
|
* SDK event. Consumers don't need to read `session.getState()` themselves.
|
|
177
205
|
*/
|
|
178
206
|
export declare function ChatWidget(props: ChatWidgetProps): JSX.Element;
|
|
207
|
+
export { ChatDrawer } from './ChatDrawer';
|
|
208
|
+
export type { ChatDrawerProps } from './ChatDrawer';
|
package/dist/react.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ChatDrawer = void 0;
|
|
3
4
|
exports.ChatWidget = ChatWidget;
|
|
4
5
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
6
|
/**
|
|
@@ -227,7 +228,7 @@ function fallbackCopy(ctx) {
|
|
|
227
228
|
* SDK event. Consumers don't need to read `session.getState()` themselves.
|
|
228
229
|
*/
|
|
229
230
|
function ChatWidget(props) {
|
|
230
|
-
const { token, apiBaseUrl, inline = false, position, browserSessionId, resumeConversationId, stream, className, style, onApprovalDecision, readOnlyApprovals = false, variant = 'card', greeting, personaName, shortcuts, onShortcutClick, inputPlaceholder, members, } = props;
|
|
231
|
+
const { token, apiBaseUrl, inline = false, position, browserSessionId, resumeConversationId, onConversationStart, stream, className, style, onApprovalDecision, readOnlyApprovals = false, variant = 'card', greeting, personaName, shortcuts, onShortcutClick, inputPlaceholder, members, composerLeftSlot, handleRef, } = props;
|
|
231
232
|
// Build a lookup so MessageBubble can resolve actingAgentId → identity
|
|
232
233
|
// in O(1) per render without re-walking the members array. Stable
|
|
233
234
|
// identity per `members` prop change.
|
|
@@ -259,6 +260,14 @@ function ChatWidget(props) {
|
|
|
259
260
|
// ALWAYS — the user is mid-conversation, losing the cursor
|
|
260
261
|
// breaks the typing-rhythm).
|
|
261
262
|
const hasInteractedRef = (0, react_1.useRef)(false);
|
|
263
|
+
// Hold the latest `onConversationStart` in a ref so the session
|
|
264
|
+
// effect doesn't recreate the ChatSession every time the parent
|
|
265
|
+
// passes a new function identity (typical with inline arrow props).
|
|
266
|
+
// Recreating the session would wipe the transcript on every render.
|
|
267
|
+
const onConversationStartRef = (0, react_1.useRef)(onConversationStart);
|
|
268
|
+
(0, react_1.useEffect)(() => {
|
|
269
|
+
onConversationStartRef.current = onConversationStart;
|
|
270
|
+
}, [onConversationStart]);
|
|
262
271
|
// Auto-focus the composer once the session is ready.
|
|
263
272
|
//
|
|
264
273
|
// Landing (hasInteractedRef.current === false):
|
|
@@ -316,6 +325,10 @@ function ChatWidget(props) {
|
|
|
316
325
|
setLastError(evt.message);
|
|
317
326
|
return;
|
|
318
327
|
}
|
|
328
|
+
if (evt.type === 'conversation_started') {
|
|
329
|
+
onConversationStartRef.current?.(evt.conversationId);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
319
332
|
});
|
|
320
333
|
void s.start();
|
|
321
334
|
return () => {
|
|
@@ -379,6 +392,63 @@ function ChatWidget(props) {
|
|
|
379
392
|
handleSend();
|
|
380
393
|
}
|
|
381
394
|
}, [handleSend]);
|
|
395
|
+
// Insert `text` at the current cursor position of the composer
|
|
396
|
+
// textarea. Replaces any active selection, advances the cursor to
|
|
397
|
+
// the end of the inserted slice, and keeps focus on the input so
|
|
398
|
+
// the user can keep typing without a click. Falls back to "append
|
|
399
|
+
// at the end" when the textarea isn't mounted yet (e.g. the host
|
|
400
|
+
// calls insertText before the session has booted).
|
|
401
|
+
const insertText = (0, react_1.useCallback)((text) => {
|
|
402
|
+
if (!text)
|
|
403
|
+
return;
|
|
404
|
+
const el = inputRef.current;
|
|
405
|
+
if (!el) {
|
|
406
|
+
setDraft((prev) => prev + text);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const start = el.selectionStart ?? el.value.length;
|
|
410
|
+
const end = el.selectionEnd ?? el.value.length;
|
|
411
|
+
const before = el.value.slice(0, start);
|
|
412
|
+
const after = el.value.slice(end);
|
|
413
|
+
const next = before + text + after;
|
|
414
|
+
setDraft(next);
|
|
415
|
+
// Imperatively place the cursor after the commit. React's
|
|
416
|
+
// state update is async, so we wait one tick — by then the
|
|
417
|
+
// controlled value has flushed into the DOM and we can move
|
|
418
|
+
// the selection without it being clobbered.
|
|
419
|
+
queueMicrotask(() => {
|
|
420
|
+
const cursor = before.length + text.length;
|
|
421
|
+
el.focus({ preventScroll: true });
|
|
422
|
+
el.setSelectionRange(cursor, cursor);
|
|
423
|
+
});
|
|
424
|
+
}, []);
|
|
425
|
+
// Wire the imperative handle. The host's ref slot is filled in on
|
|
426
|
+
// every render so a late-binding consumer still gets the live
|
|
427
|
+
// closure (sendNow reads the latest session/state via the closure
|
|
428
|
+
// it captures here).
|
|
429
|
+
(0, react_1.useEffect)(() => {
|
|
430
|
+
if (!handleRef)
|
|
431
|
+
return;
|
|
432
|
+
handleRef.current = {
|
|
433
|
+
sendNow: (text) => {
|
|
434
|
+
if (!session)
|
|
435
|
+
return;
|
|
436
|
+
const trimmed = text.trim();
|
|
437
|
+
if (!trimmed)
|
|
438
|
+
return;
|
|
439
|
+
hasInteractedRef.current = true;
|
|
440
|
+
setDraft('');
|
|
441
|
+
void session.send(trimmed);
|
|
442
|
+
},
|
|
443
|
+
insertText,
|
|
444
|
+
};
|
|
445
|
+
return () => {
|
|
446
|
+
// Drop the handle on unmount so a stale ref can't fire send
|
|
447
|
+
// after the component is gone.
|
|
448
|
+
if (handleRef.current)
|
|
449
|
+
handleRef.current = null;
|
|
450
|
+
};
|
|
451
|
+
}, [handleRef, session, insertText]);
|
|
382
452
|
const sendDisabled = !session ||
|
|
383
453
|
status === 'idle' ||
|
|
384
454
|
status === 'loading' ||
|
|
@@ -436,7 +506,14 @@ function ChatWidget(props) {
|
|
|
436
506
|
? { ...theme, avatarUrl: member.avatarUrl }
|
|
437
507
|
: theme;
|
|
438
508
|
const bubbleAvatarName = member?.name ?? personaName ?? agent?.name;
|
|
439
|
-
|
|
509
|
+
// Seed for the deterministic-hue fallback avatar. Prefer
|
|
510
|
+
// the acting agent id (so each member in a team gets its
|
|
511
|
+
// own stable color) and fall back to the primary agent's
|
|
512
|
+
// slug for solo chats / orchestrator-self turns. The
|
|
513
|
+
// `agent` summary doesn't carry an id — slug is stable
|
|
514
|
+
// and unique, which is all hueFromSeed needs.
|
|
515
|
+
const bubbleAgentId = m.actingAgentId ?? agent?.slug;
|
|
516
|
+
return ((0, jsx_runtime_1.jsx)(MessageBubble, { message: m, session: session, readOnly: readOnlyApprovals, onDecision: onApprovalDecision, bare: bare, showAvatar: showAvatar, avatarTheme: bubbleAvatarTheme, avatarName: bubbleAvatarName, avatarAgentId: bubbleAgentId, speakerLabel: member?.name, onContinue: () => {
|
|
440
517
|
// After a successful Approve, kick the next turn so the
|
|
441
518
|
// gate's fast-path consumes the approval and the tool
|
|
442
519
|
// actually runs. `silent: true` keeps the literal
|
|
@@ -449,23 +526,23 @@ function ChatWidget(props) {
|
|
|
449
526
|
void session.send('continue', { silent: true });
|
|
450
527
|
}, 250);
|
|
451
528
|
} }, m.id));
|
|
452
|
-
}) }), lastError && status === 'error' && ((0, jsx_runtime_1.jsx)("div", { className: "af-error", children: lastError })), bare && greeting && messages.length === 0 && status !== 'loading' && ((0, jsx_runtime_1.jsx)("div", { className: "af-greeting-slot", children: (0, jsx_runtime_1.jsxs)("div", { className: "af-msg-row af-msg-row-assistant af-msg-row-greeting", 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) } })] }) })), 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: () => {
|
|
529
|
+
}) }), lastError && status === 'error' && ((0, jsx_runtime_1.jsx)("div", { className: "af-error", children: lastError })), bare && greeting && messages.length === 0 && status !== 'loading' && ((0, jsx_runtime_1.jsx)("div", { className: "af-greeting-slot", children: (0, jsx_runtime_1.jsxs)("div", { className: "af-msg-row af-msg-row-assistant af-msg-row-greeting", children: [(0, jsx_runtime_1.jsx)(AssistantAvatar, { theme: theme, name: personaName ?? agent?.name, agentId: agent?.slug, show: true }), (0, jsx_runtime_1.jsx)("div", { className: "af-msg af-msg-assistant af-msg-greeting", dangerouslySetInnerHTML: { __html: renderMarkdown(greeting) } })] }) })), 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: () => {
|
|
453
530
|
if (onShortcutClick)
|
|
454
531
|
onShortcutClick(text, i);
|
|
455
532
|
else
|
|
456
533
|
setDraft(text);
|
|
457
|
-
}, children: text }, `${i}-${text}`))) })), (0, jsx_runtime_1.jsxs)("div", { className: "af-input-row", children: [(0, jsx_runtime_1.jsx)("textarea", { ref: inputRef, 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" })] })] }));
|
|
534
|
+
}, children: text }, `${i}-${text}`))) })), (0, jsx_runtime_1.jsxs)("div", { className: "af-input-row", children: [composerLeftSlot && ((0, jsx_runtime_1.jsx)("div", { className: "af-input-left", children: composerLeftSlot })), (0, jsx_runtime_1.jsx)("textarea", { ref: inputRef, 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" })] })] }));
|
|
458
535
|
}
|
|
459
|
-
function MessageBubble({ message, session, readOnly, onDecision, onContinue, bare = false, showAvatar = false, avatarTheme, avatarName, speakerLabel, }) {
|
|
536
|
+
function MessageBubble({ message, session, readOnly, onDecision, onContinue, bare = false, showAvatar = false, avatarTheme, avatarName, avatarAgentId, speakerLabel, }) {
|
|
460
537
|
const kind = message.metadata?.kind;
|
|
461
538
|
if (kind === 'awaiting_approval') {
|
|
462
539
|
// Approval / blocked bubbles also count as "assistant-side" so we
|
|
463
540
|
// wrap them in the same row geometry — keeps the conversation
|
|
464
541
|
// aligned even when a tool dispatch interrupts the regular flow.
|
|
465
|
-
return wrapAssistantRow(bare, showAvatar, avatarTheme, avatarName, (0, jsx_runtime_1.jsx)(ApprovalBubble, { message: message, session: session, readOnly: readOnly, onDecision: onDecision, onContinue: onContinue }), speakerLabel);
|
|
542
|
+
return wrapAssistantRow(bare, showAvatar, avatarTheme, avatarName, avatarAgentId, (0, jsx_runtime_1.jsx)(ApprovalBubble, { message: message, session: session, readOnly: readOnly, onDecision: onDecision, onContinue: onContinue }), speakerLabel);
|
|
466
543
|
}
|
|
467
544
|
if (kind === 'tool_blocked') {
|
|
468
|
-
return wrapAssistantRow(bare, showAvatar, avatarTheme, avatarName, (0, jsx_runtime_1.jsx)(BlockedBubble, { message: message }), speakerLabel);
|
|
545
|
+
return wrapAssistantRow(bare, showAvatar, avatarTheme, avatarName, avatarAgentId, (0, jsx_runtime_1.jsx)(BlockedBubble, { message: message }), speakerLabel);
|
|
469
546
|
}
|
|
470
547
|
const cls = `af-msg af-msg-${message.role}${message.role === 'assistant' && message.isStreaming
|
|
471
548
|
? message.content
|
|
@@ -474,10 +551,10 @@ function MessageBubble({ message, session, readOnly, onDecision, onContinue, bar
|
|
|
474
551
|
: ''}`;
|
|
475
552
|
// Typing state (no content yet): render the three-dot indicator, no markdown.
|
|
476
553
|
if (message.role === 'assistant' && message.isStreaming && !message.content) {
|
|
477
|
-
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", {})] }) }), speakerLabel);
|
|
554
|
+
return wrapAssistantRow(bare, showAvatar, avatarTheme, avatarName, avatarAgentId, (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", {})] }) }), speakerLabel);
|
|
478
555
|
}
|
|
479
556
|
if (message.role === 'assistant') {
|
|
480
|
-
return wrapAssistantRow(bare, showAvatar, avatarTheme, avatarName, (0, jsx_runtime_1.jsx)("div", { className: cls,
|
|
557
|
+
return wrapAssistantRow(bare, showAvatar, avatarTheme, avatarName, avatarAgentId, (0, jsx_runtime_1.jsx)("div", { className: cls,
|
|
481
558
|
// Output is sanitized by escapeHtml + a fixed tag whitelist in
|
|
482
559
|
// renderMarkdown — safe to inject as HTML.
|
|
483
560
|
dangerouslySetInnerHTML: { __html: renderMarkdown(message.content) } }), speakerLabel);
|
|
@@ -491,22 +568,27 @@ function MessageBubble({ message, session, readOnly, onDecision, onContinue, bar
|
|
|
491
568
|
* avatar column. Card variant skips the wrapper entirely so its
|
|
492
569
|
* historical layout is unchanged.
|
|
493
570
|
*/
|
|
494
|
-
function wrapAssistantRow(bare, showAvatar, theme, name, child,
|
|
571
|
+
function wrapAssistantRow(bare, showAvatar, theme, name, agentId, child,
|
|
495
572
|
/** When set + this is the first bubble of a speaker run (showAvatar
|
|
496
573
|
* is true), render the speaker's display name just above the
|
|
497
574
|
* bubble. Used by Team chats so members are visually attributed. */
|
|
498
575
|
speakerLabel) {
|
|
499
576
|
if (!bare)
|
|
500
577
|
return child;
|
|
501
|
-
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 }), (0, jsx_runtime_1.jsxs)("div", { className: "af-msg-col", children: [showAvatar && speakerLabel ? ((0, jsx_runtime_1.jsx)("div", { className: "af-msg-speaker", children: speakerLabel })) : null, child] })] }));
|
|
578
|
+
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, agentId: agentId, show: showAvatar }), (0, jsx_runtime_1.jsxs)("div", { className: "af-msg-col", children: [showAvatar && speakerLabel ? ((0, jsx_runtime_1.jsx)("div", { className: "af-msg-speaker", children: speakerLabel })) : null, child] })] }));
|
|
502
579
|
}
|
|
503
580
|
/**
|
|
504
581
|
* Small circular avatar for the assistant-side column. Prefers the
|
|
505
|
-
* agent's avatarUrl when set; otherwise renders a
|
|
506
|
-
*
|
|
507
|
-
*
|
|
582
|
+
* agent's avatarUrl when set; otherwise renders a SOLID circle with
|
|
583
|
+
* the first letter of `name` over a hue derived from `agentId`. The
|
|
584
|
+
* hash → HSL mapping means every agent in a team gets a stable,
|
|
585
|
+
* distinguishable color across renders without us needing a palette
|
|
586
|
+
* table or per-agent config.
|
|
587
|
+
*
|
|
588
|
+
* The slot reserves space even when `show` is false so consecutive
|
|
589
|
+
* bubbles stay column-aligned.
|
|
508
590
|
*/
|
|
509
|
-
function AssistantAvatar({ theme, name, show, }) {
|
|
591
|
+
function AssistantAvatar({ theme, name, agentId, show, }) {
|
|
510
592
|
const initial = (name ?? 'A').trim().charAt(0).toUpperCase() || 'A';
|
|
511
593
|
if (!show) {
|
|
512
594
|
return (0, jsx_runtime_1.jsx)("div", { className: "af-msg-avatar af-msg-avatar-spacer", "aria-hidden": true });
|
|
@@ -516,7 +598,21 @@ function AssistantAvatar({ theme, name, show, }) {
|
|
|
516
598
|
// eslint-disable-next-line @next/next/no-img-element -- vanilla widget; consumer can override host
|
|
517
599
|
(0, jsx_runtime_1.jsx)("img", { className: "af-msg-avatar af-msg-avatar-img", src: theme.avatarUrl, alt: "", "aria-hidden": true }));
|
|
518
600
|
}
|
|
519
|
-
|
|
601
|
+
const seed = agentId || name || 'agent';
|
|
602
|
+
return ((0, jsx_runtime_1.jsx)("div", { className: "af-msg-avatar af-msg-avatar-fallback", "aria-hidden": true, style: { backgroundColor: hueFromSeed(seed) }, children: (0, jsx_runtime_1.jsx)("span", { children: initial }) }));
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Deterministic per-agent hue. Hash the seed into the HSL hue space
|
|
606
|
+
* so an agent's avatar color stays stable across renders and looks
|
|
607
|
+
* varied across a roster. Saturation/lightness are tuned for both
|
|
608
|
+
* light + dark chat surfaces: mid-saturation + mid-lightness reads
|
|
609
|
+
* legibly with white text on either background.
|
|
610
|
+
*/
|
|
611
|
+
function hueFromSeed(seed) {
|
|
612
|
+
let h = 0;
|
|
613
|
+
for (let i = 0; i < seed.length; i++)
|
|
614
|
+
h = (h * 31 + seed.charCodeAt(i)) >>> 0;
|
|
615
|
+
return `hsl(${h % 360} 60% 52%)`;
|
|
520
616
|
}
|
|
521
617
|
/**
|
|
522
618
|
* Awaiting-approval bubble. Renders the tool name + a countdown, and
|
|
@@ -692,6 +788,11 @@ const WIDGET_CSS = `
|
|
|
692
788
|
to { opacity: 1; transform: translateY(0); }
|
|
693
789
|
}
|
|
694
790
|
.af-input-row { padding: 12px; border-top: 1px solid var(--af-border); background: var(--af-bg); display: flex; gap: 8px; align-items: flex-end; }
|
|
791
|
+
/* Composer left slot — hosts use this for affordance buttons that
|
|
792
|
+
scope the next turn (member picker, tools menu, attachments).
|
|
793
|
+
align-items: center keeps a single-line chip vertically centered
|
|
794
|
+
against the auto-growing textarea. */
|
|
795
|
+
.af-input-left { display: flex; align-items: center; padding-bottom: 4px; flex-shrink: 0; }
|
|
695
796
|
.af-shortcut-row {
|
|
696
797
|
display: flex;
|
|
697
798
|
gap: 6px;
|
|
@@ -815,7 +916,13 @@ const WIDGET_CSS = `
|
|
|
815
916
|
background: transparent;
|
|
816
917
|
border-radius: 0;
|
|
817
918
|
box-shadow: none;
|
|
818
|
-
overflow:
|
|
919
|
+
/* overflow:hidden so a long transcript (markdown tables, long
|
|
920
|
+
lists) scrolls inside af-messages instead of pushing the
|
|
921
|
+
af-input-row past the bottom of the host envelope. Was 'visible'
|
|
922
|
+
before, which let a tall message break out of the host sized
|
|
923
|
+
container and shove the composer off-screen. The messages region
|
|
924
|
+
itself carries overflow-y:auto so internal scroll keeps working. */
|
|
925
|
+
overflow: hidden;
|
|
819
926
|
display: flex;
|
|
820
927
|
flex-direction: column;
|
|
821
928
|
flex: 1;
|
|
@@ -1048,7 +1155,11 @@ const WIDGET_CSS = `
|
|
|
1048
1155
|
font-size: 11.5px;
|
|
1049
1156
|
font-weight: 600;
|
|
1050
1157
|
letter-spacing: 0.01em;
|
|
1051
|
-
|
|
1158
|
+
/* SOLID fill — the React component sets the actual color via an
|
|
1159
|
+
* inline style.backgroundColor (deterministic hash of agentId).
|
|
1160
|
+
* No gradient here on purpose: the gradient was masking the
|
|
1161
|
+
* inline color and made every agent avatar look the same. */
|
|
1162
|
+
background-color: var(--af-primary, #8b5cf6);
|
|
1052
1163
|
}
|
|
1053
1164
|
@media (min-width: 768px) {
|
|
1054
1165
|
.af-widget-root.af-variant-bare .af-msg-avatar { width: 30px; height: 30px; }
|
|
@@ -1159,3 +1270,9 @@ const WIDGET_CSS = `
|
|
|
1159
1270
|
}
|
|
1160
1271
|
}
|
|
1161
1272
|
`;
|
|
1273
|
+
// ─── Re-exports ───────────────────────────────────────────────────────
|
|
1274
|
+
// Bottom-sheet wrapper around the widget. Hosts that need the full
|
|
1275
|
+
// drawer UX (98% snap, visualViewport-aware composer, drag-to-dismiss)
|
|
1276
|
+
// import this from `@agentforge-io/chat-sdk/react` next to ChatWidget.
|
|
1277
|
+
var ChatDrawer_1 = require("./ChatDrawer");
|
|
1278
|
+
Object.defineProperty(exports, "ChatDrawer", { enumerable: true, get: function () { return ChatDrawer_1.ChatDrawer; } });
|
package/dist/session.js
CHANGED
|
@@ -211,6 +211,7 @@ class ChatSession {
|
|
|
211
211
|
for await (const evt of generator) {
|
|
212
212
|
if (evt.kind === 'conversation') {
|
|
213
213
|
this.state.conversationId = evt.id;
|
|
214
|
+
this.emit({ type: 'conversation_started', conversationId: evt.id });
|
|
214
215
|
continue;
|
|
215
216
|
}
|
|
216
217
|
if (evt.kind === 'chunk') {
|
|
@@ -398,6 +399,7 @@ class ChatSession {
|
|
|
398
399
|
else {
|
|
399
400
|
const res = await this.transport.createConversation(text, this.browserSessionId);
|
|
400
401
|
this.state.conversationId = res.conversationId;
|
|
402
|
+
this.emit({ type: 'conversation_started', conversationId: res.conversationId });
|
|
401
403
|
content = res.content;
|
|
402
404
|
}
|
|
403
405
|
assistant.content = content;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentforge-io/chat-sdk",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
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",
|