@agentforge-io/chat-sdk 2.4.0-dev.0 → 2.4.0-dev.10
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 +78 -43
- package/dist/ChatDrawer.js +252 -131
- package/dist/react.d.ts +7 -0
- package/dist/react.js +43 -21
- package/dist/session.d.ts +3 -1
- package/dist/session.js +72 -7
- package/package.json +3 -9
package/dist/ChatDrawer.d.ts
CHANGED
|
@@ -1,40 +1,60 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `<ChatDrawer>` — standard
|
|
2
|
+
* `<ChatDrawer>` — standard bottom-sheet wrapper around `<ChatWidget>`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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.
|
|
6
9
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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.
|
|
12
30
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* install it. Most hosts already have Vaul (Radix design system).
|
|
22
|
-
*
|
|
23
|
-
* Why fullscreen (not snap points): the operator decision was that a
|
|
24
|
-
* conversation should occupy the whole device. No drag-to-dismiss,
|
|
25
|
-
* no peek of the page behind. Close via the explicit back button in
|
|
26
|
-
* the header. This is the "modal" feel — predictable, no accidental
|
|
27
|
-
* dismissals from a fast scroll.
|
|
28
|
-
*
|
|
29
|
-
* Why we still don't manage the chat session: the SDK's `ChatWidget`
|
|
30
|
-
* owns that, this component is purely the surround.
|
|
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)`.
|
|
31
39
|
*/
|
|
32
40
|
import { type CSSProperties, type ReactNode } from 'react';
|
|
33
41
|
import { type ChatWidgetProps } from './react';
|
|
34
42
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
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.
|
|
38
58
|
*/
|
|
39
59
|
type ChatSurface = {
|
|
40
60
|
widgetProps: ChatWidgetProps;
|
|
@@ -44,24 +64,39 @@ type ChatSurface = {
|
|
|
44
64
|
widgetProps?: never;
|
|
45
65
|
};
|
|
46
66
|
export type ChatDrawerProps = ChatSurface & {
|
|
47
|
-
/**
|
|
67
|
+
/** Whether the drawer is visible. Controlled. */
|
|
48
68
|
open: boolean;
|
|
49
|
-
/** Fired when the drawer wants to close (
|
|
50
|
-
*
|
|
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`. */
|
|
51
71
|
onOpenChange: (open: boolean) => void;
|
|
52
|
-
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
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. */
|
|
55
80
|
header?: ReactNode;
|
|
56
|
-
/**
|
|
57
|
-
* the
|
|
58
|
-
|
|
59
|
-
|
|
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. */
|
|
60
88
|
drawerClassName?: string;
|
|
61
|
-
/**
|
|
62
|
-
*
|
|
63
|
-
* the
|
|
64
|
-
*
|
|
89
|
+
/** Inline style applied to the drawer surface — typically the host's
|
|
90
|
+
* bundle of `--af-bg`, `--af-fg`, `--af-bubble-*`, etc. CSS vars.
|
|
91
|
+
* Because the drawer renders into a portal (`document.body`), any
|
|
92
|
+
* vars declared on the host's page wrapper DON'T cascade in. The
|
|
93
|
+
* host re-declares them here so the chat surface inside the portal
|
|
94
|
+
* picks up the same theme.
|
|
95
|
+
*
|
|
96
|
+
* We default the SURFACE BG to `var(--af-bg, …)` so passing
|
|
97
|
+
* `--af-bg` in `surfaceStyle` themes the drawer's background.
|
|
98
|
+
* When omitted the drawer falls back to white in light mode and
|
|
99
|
+
* the system default elsewhere — passable but not theme-perfect. */
|
|
65
100
|
surfaceStyle?: CSSProperties & Record<string, string>;
|
|
66
101
|
};
|
|
67
102
|
export declare function ChatDrawer(props: ChatDrawerProps): JSX.Element | null;
|
package/dist/ChatDrawer.js
CHANGED
|
@@ -3,59 +3,143 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.ChatDrawer = ChatDrawer;
|
|
4
4
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
5
|
/**
|
|
6
|
-
* `<ChatDrawer>` — standard
|
|
6
|
+
* `<ChatDrawer>` — standard bottom-sheet wrapper around `<ChatWidget>`.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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.
|
|
10
13
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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.
|
|
16
34
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* install it. Most hosts already have Vaul (Radix design system).
|
|
26
|
-
*
|
|
27
|
-
* Why fullscreen (not snap points): the operator decision was that a
|
|
28
|
-
* conversation should occupy the whole device. No drag-to-dismiss,
|
|
29
|
-
* no peek of the page behind. Close via the explicit back button in
|
|
30
|
-
* the header. This is the "modal" feel — predictable, no accidental
|
|
31
|
-
* dismissals from a fast scroll.
|
|
32
|
-
*
|
|
33
|
-
* Why we still don't manage the chat session: the SDK's `ChatWidget`
|
|
34
|
-
* owns that, this component is purely the surround.
|
|
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)`.
|
|
35
43
|
*/
|
|
36
44
|
const react_1 = require("react");
|
|
45
|
+
const react_dom_1 = require("react-dom");
|
|
37
46
|
const react_2 = require("./react");
|
|
38
|
-
// Lazy import of Vaul. We can't `import { Drawer } from 'vaul'` at
|
|
39
|
-
// the top of the file because vaul is an OPTIONAL peerDependency
|
|
40
|
-
// and the package shouldn't crash at import-time when the host
|
|
41
|
-
// hasn't installed it. We resolve at module evaluation but inside a
|
|
42
|
-
// try/catch so the missing-vaul case is just a runtime "please
|
|
43
|
-
// install vaul" warning instead of a build break.
|
|
44
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
45
|
-
let VaulDrawer = null;
|
|
46
|
-
try {
|
|
47
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
48
|
-
VaulDrawer = require('vaul').Drawer;
|
|
49
|
-
}
|
|
50
|
-
catch {
|
|
51
|
-
// vaul not installed — render a fallback in the component.
|
|
52
|
-
}
|
|
53
47
|
function ChatDrawer(props) {
|
|
54
|
-
const { open, onOpenChange, header,
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
48
|
+
const { open, onOpenChange, snap = 1, header, closeOnBackdropClick = true, drawerClassName, surfaceStyle: surfaceStyleProp, widgetProps, chatSlot, } = props;
|
|
49
|
+
// `fullscreen` mode: snap=1 (the default). The drawer occupies the
|
|
50
|
+
// entire visible viewport. We disable drag-to-dismiss, the backdrop,
|
|
51
|
+
// the rounded top corners, and the drag handle in this mode — they
|
|
52
|
+
// exist purely for the bottom-sheet form factor (snap<1) where the
|
|
53
|
+
// page peeks behind the surface. With snap=1 there's nothing behind
|
|
54
|
+
// and no affordance to drag against.
|
|
55
|
+
const fullscreen = snap >= 1;
|
|
56
|
+
// We mount once and keep alive across close→reopen so the chat session
|
|
57
|
+
// doesn't get destroyed. After the FIRST open the panel stays in the
|
|
58
|
+
// DOM forever (toggled by `display:none`) — `hasOpened` gates the
|
|
59
|
+
// initial mount so SSR doesn't render an empty portal.
|
|
60
|
+
const [hasOpened, setHasOpened] = (0, react_1.useState)(open);
|
|
61
|
+
(0, react_1.useEffect)(() => {
|
|
62
|
+
if (open)
|
|
63
|
+
setHasOpened(true);
|
|
64
|
+
}, [open]);
|
|
65
|
+
// Visible viewport tracking. iOS Safari + Android Chrome shrink
|
|
66
|
+
// `visualViewport.height` when the on-screen keyboard pops up; the
|
|
67
|
+
// layout viewport stays the same. We pin the drawer's height + bottom
|
|
68
|
+
// to the visual viewport so the composer is always above the keyboard.
|
|
69
|
+
const [vv, setVv] = (0, react_1.useState)(() => ({
|
|
70
|
+
h: typeof window === 'undefined' ? 0 : window.innerHeight,
|
|
71
|
+
offsetTop: 0,
|
|
72
|
+
}));
|
|
73
|
+
(0, react_1.useEffect)(() => {
|
|
74
|
+
if (typeof window === 'undefined')
|
|
75
|
+
return;
|
|
76
|
+
const view = window.visualViewport;
|
|
77
|
+
if (!view)
|
|
78
|
+
return;
|
|
79
|
+
const update = () => setVv({ h: view.height, offsetTop: view.offsetTop });
|
|
80
|
+
update();
|
|
81
|
+
view.addEventListener('resize', update);
|
|
82
|
+
view.addEventListener('scroll', update);
|
|
83
|
+
return () => {
|
|
84
|
+
view.removeEventListener('resize', update);
|
|
85
|
+
view.removeEventListener('scroll', update);
|
|
86
|
+
};
|
|
87
|
+
}, []);
|
|
88
|
+
// Snap-point height: % of the visible viewport. Recomputed when vv
|
|
89
|
+
// changes so a keyboard popup keeps the drawer aligned.
|
|
90
|
+
const drawerHeight = Math.max(0, Math.round(vv.h * Math.min(Math.max(snap, 0.1), 1)));
|
|
91
|
+
// Drag-to-dismiss. Vanilla touch handlers — no library. We track
|
|
92
|
+
// pointerdown on the handle, follow movement on pointermove, and
|
|
93
|
+
// decide on pointerup whether the drag crossed the "dismiss" threshold
|
|
94
|
+
// (30% of the panel height).
|
|
95
|
+
const surfaceRef = (0, react_1.useRef)(null);
|
|
96
|
+
const dragStateRef = (0, react_1.useRef)(null);
|
|
97
|
+
const [dragOffset, setDragOffset] = (0, react_1.useState)(0);
|
|
98
|
+
const onHandlePointerDown = (0, react_1.useCallback)((e) => {
|
|
99
|
+
if (e.button !== 0 && e.pointerType !== 'touch')
|
|
100
|
+
return;
|
|
101
|
+
e.currentTarget.setPointerCapture?.(e.pointerId);
|
|
102
|
+
dragStateRef.current = {
|
|
103
|
+
startY: e.clientY,
|
|
104
|
+
startTime: Date.now(),
|
|
105
|
+
dragging: true,
|
|
106
|
+
};
|
|
107
|
+
}, []);
|
|
108
|
+
const onHandlePointerMove = (0, react_1.useCallback)((e) => {
|
|
109
|
+
const s = dragStateRef.current;
|
|
110
|
+
if (!s?.dragging)
|
|
111
|
+
return;
|
|
112
|
+
const delta = Math.max(0, e.clientY - s.startY);
|
|
113
|
+
setDragOffset(delta);
|
|
114
|
+
}, []);
|
|
115
|
+
const onHandlePointerUp = (0, react_1.useCallback)((e) => {
|
|
116
|
+
const s = dragStateRef.current;
|
|
117
|
+
if (!s?.dragging)
|
|
118
|
+
return;
|
|
119
|
+
dragStateRef.current = null;
|
|
120
|
+
try {
|
|
121
|
+
e.currentTarget.releasePointerCapture?.(e.pointerId);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// Pointer was already released by some other code path; nothing
|
|
125
|
+
// to do here. Releasing a non-captured pointer throws — swallow.
|
|
126
|
+
}
|
|
127
|
+
const delta = Math.max(0, e.clientY - s.startY);
|
|
128
|
+
const dismissThreshold = drawerHeight * 0.3;
|
|
129
|
+
const elapsed = Date.now() - s.startTime;
|
|
130
|
+
// Dismiss on EITHER a long-drag (past the threshold) or a quick
|
|
131
|
+
// flick (small distance but high velocity). Velocity unit is
|
|
132
|
+
// px/ms; >0.5 is roughly the threshold iOS sheets use.
|
|
133
|
+
const velocity = elapsed > 0 ? delta / elapsed : 0;
|
|
134
|
+
if (delta > dismissThreshold || velocity > 0.5) {
|
|
135
|
+
onOpenChange(false);
|
|
136
|
+
}
|
|
137
|
+
setDragOffset(0);
|
|
138
|
+
}, [drawerHeight, onOpenChange]);
|
|
139
|
+
// Lock body scroll while the drawer is open so the page underneath
|
|
140
|
+
// doesn't move when the visitor scrolls inside the chat. Restored on
|
|
141
|
+
// close. Necessary on iOS Safari where the rubber-band scroll bleeds
|
|
142
|
+
// through to the body even with overflow:hidden on a child.
|
|
59
143
|
(0, react_1.useEffect)(() => {
|
|
60
144
|
if (typeof document === 'undefined')
|
|
61
145
|
return;
|
|
@@ -67,90 +151,127 @@ function ChatDrawer(props) {
|
|
|
67
151
|
document.body.style.overflow = prev;
|
|
68
152
|
};
|
|
69
153
|
}, [open]);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (open
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
154
|
+
// Escape closes the drawer. Keyboard-friendly even when focus is in
|
|
155
|
+
// the textarea — preventDefault on the input itself swallows Escape
|
|
156
|
+
// before it bubbles, so we use capture phase.
|
|
157
|
+
(0, react_1.useEffect)(() => {
|
|
158
|
+
if (!open)
|
|
159
|
+
return;
|
|
160
|
+
if (typeof window === 'undefined')
|
|
161
|
+
return;
|
|
162
|
+
const onKey = (e) => {
|
|
163
|
+
if (e.key === 'Escape')
|
|
164
|
+
onOpenChange(false);
|
|
165
|
+
};
|
|
166
|
+
window.addEventListener('keydown', onKey, true);
|
|
167
|
+
return () => window.removeEventListener('keydown', onKey, true);
|
|
168
|
+
}, [open, onOpenChange]);
|
|
169
|
+
// Lazy portal target. We render to `document.body` so the drawer
|
|
170
|
+
// overlays everything regardless of the consumer's stacking context.
|
|
171
|
+
const [portalEl, setPortalEl] = (0, react_1.useState)(null);
|
|
172
|
+
(0, react_1.useLayoutEffect)(() => {
|
|
173
|
+
if (typeof document === 'undefined')
|
|
174
|
+
return;
|
|
175
|
+
setPortalEl(document.body);
|
|
176
|
+
}, []);
|
|
177
|
+
// Stable id so the drag handle's `aria-controls` can point at the
|
|
178
|
+
// panel. Required for screen readers to announce the relationship.
|
|
179
|
+
const panelId = (0, react_1.useId)();
|
|
180
|
+
if (!portalEl)
|
|
79
181
|
return null;
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
182
|
+
if (!hasOpened)
|
|
183
|
+
return null;
|
|
184
|
+
// The translate3d ensures the drawer animates from below on first
|
|
185
|
+
// open (initial transform = 100%, becomes 0% after the open prop
|
|
186
|
+
// flips). When dragging we add the manual drag offset on top.
|
|
187
|
+
const translateY = open ? `${dragOffset}px` : `${drawerHeight}px`;
|
|
188
|
+
// Transition disabled WHILE dragging so the surface follows the
|
|
189
|
+
// finger 1:1. Re-enabled at the end of the drag (delta back to 0
|
|
190
|
+
// smoothly when below threshold).
|
|
191
|
+
const isDragging = dragStateRef.current?.dragging === true;
|
|
192
|
+
// Motion / sizing portion of the surface style, kept separate from
|
|
193
|
+
// the host-supplied theming so a re-render driven by drag offset
|
|
194
|
+
// doesn't smash the host's CSS vars.
|
|
195
|
+
//
|
|
196
|
+
// Two distinct positioning modes:
|
|
197
|
+
//
|
|
198
|
+
// **fullscreen (snap=1)**: pin the surface to the VISUAL viewport
|
|
199
|
+
// box. `top = visualViewport.offsetTop` + `height = visualViewport.h`
|
|
200
|
+
// means the drawer sits exactly on the area the user can see, with
|
|
201
|
+
// the bottom edge flush against the on-screen keyboard (when it's up)
|
|
202
|
+
// or the bottom of the screen (when it's down). No layout viewport
|
|
203
|
+
// gymnastics needed.
|
|
204
|
+
//
|
|
205
|
+
// **bottom-sheet (snap<1)**: anchor the surface to the bottom of the
|
|
206
|
+
// visual viewport. `bottom = visualViewport.offsetTop` is the legacy
|
|
207
|
+
// path that handles the rubber-band scroll case on iOS Safari.
|
|
208
|
+
const surfaceMotionStyle = fullscreen
|
|
209
|
+
? {
|
|
210
|
+
top: `${vv.offsetTop}px`,
|
|
211
|
+
height: `${vv.h}px`,
|
|
212
|
+
transform: `translate3d(0, ${translateY}, 0)`,
|
|
213
|
+
transition: isDragging
|
|
214
|
+
? 'none'
|
|
215
|
+
: 'transform 240ms cubic-bezier(.32,.72,0,1)',
|
|
216
|
+
willChange: 'transform',
|
|
217
|
+
}
|
|
218
|
+
: {
|
|
219
|
+
height: `${drawerHeight}px`,
|
|
220
|
+
transform: `translate3d(0, ${translateY}, 0)`,
|
|
221
|
+
bottom: `${vv.offsetTop}px`,
|
|
222
|
+
transition: isDragging
|
|
223
|
+
? 'none'
|
|
224
|
+
: 'transform 240ms cubic-bezier(.32,.72,0,1)',
|
|
225
|
+
willChange: 'transform',
|
|
226
|
+
};
|
|
227
|
+
return (0, react_dom_1.createPortal)((0, jsx_runtime_1.jsxs)("div", { className: "af-drawer-root", "data-state": open ? 'open' : 'closed', style: {
|
|
228
|
+
position: 'fixed',
|
|
229
|
+
inset: 0,
|
|
230
|
+
zIndex: 2147483600,
|
|
231
|
+
pointerEvents: open ? 'auto' : 'none',
|
|
232
|
+
}, children: [!fullscreen && ((0, jsx_runtime_1.jsx)("div", { className: "af-drawer-backdrop", onClick: closeOnBackdropClick ? () => onOpenChange(false) : undefined, style: {
|
|
233
|
+
position: 'absolute',
|
|
234
|
+
inset: 0,
|
|
235
|
+
backgroundColor: 'rgba(0, 0, 0, 0.4)',
|
|
236
|
+
opacity: open ? 1 : 0,
|
|
237
|
+
transition: 'opacity 200ms ease-out',
|
|
238
|
+
}, "aria-hidden": true })), (0, jsx_runtime_1.jsxs)("div", { ref: surfaceRef, id: panelId, role: "dialog", "aria-modal": "true", className: `af-drawer-surface ${drawerClassName ?? ''}`, style: {
|
|
239
|
+
position: 'absolute',
|
|
240
|
+
left: 0,
|
|
241
|
+
right: 0,
|
|
242
|
+
width: '100%',
|
|
243
|
+
// Host supplies the CSS vars via `surfaceStyle`; the
|
|
244
|
+
// backgroundColor resolves from `--af-bg` (which the host
|
|
245
|
+
// declares in that same prop). When `surfaceStyle` is
|
|
246
|
+
// omitted we fall back to white — passable for an unthemed
|
|
247
|
+
// demo, wrong for a themed embed; passing surfaceStyle is
|
|
248
|
+
// the correct path.
|
|
249
|
+
backgroundColor: 'var(--af-bg, #ffffff)',
|
|
250
|
+
color: 'var(--af-fg, inherit)',
|
|
251
|
+
// Bottom-sheet visuals only — fullscreen mode is flat.
|
|
252
|
+
borderTopLeftRadius: fullscreen ? 0 : '16px',
|
|
253
|
+
borderTopRightRadius: fullscreen ? 0 : '16px',
|
|
254
|
+
boxShadow: fullscreen ? 'none' : '0 -8px 24px rgba(15, 23, 42, 0.25)',
|
|
255
|
+
display: 'flex',
|
|
256
|
+
flexDirection: 'column',
|
|
257
|
+
overflow: 'hidden',
|
|
258
|
+
// Host-supplied theme vars come FIRST so the motion
|
|
259
|
+
// properties below override any conflicting `transform` /
|
|
260
|
+
// `height` declarations a careless host might pass.
|
|
261
|
+
...surfaceStyleProp,
|
|
262
|
+
...surfaceMotionStyle,
|
|
263
|
+
}, children: [!fullscreen && ((0, jsx_runtime_1.jsx)("div", { onPointerDown: onHandlePointerDown, onPointerMove: onHandlePointerMove, onPointerUp: onHandlePointerUp, onPointerCancel: onHandlePointerUp, style: {
|
|
264
|
+
padding: '10px 0',
|
|
265
|
+
cursor: 'grab',
|
|
266
|
+
touchAction: 'none',
|
|
267
|
+
display: 'flex',
|
|
268
|
+
justifyContent: 'center',
|
|
269
|
+
flexShrink: 0,
|
|
270
|
+
}, "aria-label": "Drag to close", children: (0, jsx_runtime_1.jsx)("span", { style: {
|
|
271
|
+
width: '40px',
|
|
272
|
+
height: '4px',
|
|
273
|
+
borderRadius: '999px',
|
|
274
|
+
backgroundColor: 'var(--af-muted, rgba(100, 116, 139, 0.45))',
|
|
275
|
+
display: 'block',
|
|
276
|
+
} }) })), 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);
|
|
156
277
|
}
|
package/dist/react.d.ts
CHANGED
|
@@ -198,6 +198,13 @@ export interface ChatWidgetHandle {
|
|
|
198
198
|
* and the cursor lands at the end of the inserted text.
|
|
199
199
|
*/
|
|
200
200
|
insertText(text: string): void;
|
|
201
|
+
/**
|
|
202
|
+
* Move keyboard focus to the composer textarea. Use when the
|
|
203
|
+
* host UI surfaces the widget late (e.g. opening a drawer) and
|
|
204
|
+
* wants the on-screen keyboard up immediately. Safe no-op when
|
|
205
|
+
* the textarea hasn't mounted yet or is disabled.
|
|
206
|
+
*/
|
|
207
|
+
focus(): void;
|
|
201
208
|
}
|
|
202
209
|
/**
|
|
203
210
|
* Drop-in chat widget. Owns its own `ChatSession` and re-renders on every
|
package/dist/react.js
CHANGED
|
@@ -268,28 +268,28 @@ function ChatWidget(props) {
|
|
|
268
268
|
(0, react_1.useEffect)(() => {
|
|
269
269
|
onConversationStartRef.current = onConversationStart;
|
|
270
270
|
}, [onConversationStart]);
|
|
271
|
-
// Auto-focus the composer
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
|
|
281
|
-
// they're in conversation mode, their next thought is the
|
|
282
|
-
// next message, the keyboard should stay alive.
|
|
271
|
+
// Auto-focus the composer ONCE, the first time the session
|
|
272
|
+
// reaches `ready` AND the textarea is enabled. After that,
|
|
273
|
+
// focus is preserved by the composer's `onPointerDown
|
|
274
|
+
// preventDefault` (the send button never steals it) and by NOT
|
|
275
|
+
// firing focus() on every status change. That used to cause a
|
|
276
|
+
// visible "blink": status flips `ready → sending → streaming →
|
|
277
|
+
// ready` on every send, and a status-watching focus effect
|
|
278
|
+
// would refocus AFTER the keyboard had already started
|
|
279
|
+
// collapsing, producing the jitter.
|
|
280
|
+
const focusedOnceRef = (0, react_1.useRef)(false);
|
|
283
281
|
(0, react_1.useEffect)(() => {
|
|
282
|
+
if (focusedOnceRef.current)
|
|
283
|
+
return;
|
|
284
284
|
if (status !== 'ready')
|
|
285
285
|
return;
|
|
286
286
|
if (typeof window === 'undefined')
|
|
287
287
|
return;
|
|
288
|
-
|
|
289
|
-
|
|
288
|
+
const el = inputRef.current;
|
|
289
|
+
if (!el || el.disabled)
|
|
290
290
|
return;
|
|
291
|
-
}
|
|
292
|
-
|
|
291
|
+
el.focus({ preventScroll: true });
|
|
292
|
+
focusedOnceRef.current = true;
|
|
293
293
|
}, [status]);
|
|
294
294
|
// ── Session lifecycle. Recreate when token / apiBaseUrl changes. ──────
|
|
295
295
|
(0, react_1.useEffect)(() => {
|
|
@@ -378,12 +378,13 @@ function ChatWidget(props) {
|
|
|
378
378
|
const text = draft.trim();
|
|
379
379
|
if (!text)
|
|
380
380
|
return;
|
|
381
|
-
// Mark engagement BEFORE the status flip so the auto-focus
|
|
382
|
-
// effect (which watches `status`) sees the flag when it
|
|
383
|
-
// re-runs after `ready` returns. Subsequent renders will
|
|
384
|
-
// refocus the textarea on every send completion.
|
|
385
381
|
hasInteractedRef.current = true;
|
|
386
382
|
setDraft('');
|
|
383
|
+
// session.send awaits start() internally, so it's safe to fire
|
|
384
|
+
// even before the initial agent/theme fetch resolves. The send
|
|
385
|
+
// button's `onPointerDown preventDefault` keeps the textarea
|
|
386
|
+
// focused through tap/click, so we don't manually re-focus
|
|
387
|
+
// here (manual focus during the status flip caused a blink).
|
|
387
388
|
void session.send(text);
|
|
388
389
|
}, [session, draft]);
|
|
389
390
|
const onKeyDown = (0, react_1.useCallback)((e) => {
|
|
@@ -441,6 +442,12 @@ function ChatWidget(props) {
|
|
|
441
442
|
void session.send(trimmed);
|
|
442
443
|
},
|
|
443
444
|
insertText,
|
|
445
|
+
focus: () => {
|
|
446
|
+
const el = inputRef.current;
|
|
447
|
+
if (!el || el.disabled)
|
|
448
|
+
return;
|
|
449
|
+
el.focus({ preventScroll: true });
|
|
450
|
+
},
|
|
444
451
|
};
|
|
445
452
|
return () => {
|
|
446
453
|
// Drop the handle on unmount so a stale ref can't fire send
|
|
@@ -531,7 +538,22 @@ function ChatWidget(props) {
|
|
|
531
538
|
onShortcutClick(text, i);
|
|
532
539
|
else
|
|
533
540
|
setDraft(text);
|
|
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,
|
|
541
|
+
}, 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,
|
|
542
|
+
// The textarea stays editable while the agent is
|
|
543
|
+
// streaming so the visitor can compose their next
|
|
544
|
+
// message without waiting. Only block when the
|
|
545
|
+
// conversation has actually ended OR before the
|
|
546
|
+
// session is ready at all.
|
|
547
|
+
disabled: status === 'ended' || status === 'loading' || status === 'idle' }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "af-send",
|
|
548
|
+
// `onPointerDown preventDefault` is the single cross-
|
|
549
|
+
// device guarantee that the button never steals focus
|
|
550
|
+
// from the textarea. PointerEvents cover touch, mouse,
|
|
551
|
+
// AND pen — `onMouseDown` alone misses mobile touch.
|
|
552
|
+
// Without this, the focus shifts to the button just
|
|
553
|
+
// before `handleSend` runs, the button then disables
|
|
554
|
+
// (sendDisabled flips true on status change), focus
|
|
555
|
+
// jumps to <body>, and the on-screen keyboard collapses.
|
|
556
|
+
onPointerDown: (e) => e.preventDefault(), 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" })] })] }));
|
|
535
557
|
}
|
|
536
558
|
function MessageBubble({ message, session, readOnly, onDecision, onContinue, bare = false, showAvatar = false, avatarTheme, avatarName, avatarAgentId, speakerLabel, }) {
|
|
537
559
|
const kind = message.metadata?.kind;
|
package/dist/session.d.ts
CHANGED
|
@@ -23,12 +23,13 @@ export declare class ChatSession {
|
|
|
23
23
|
private readonly resumeId?;
|
|
24
24
|
private listeners;
|
|
25
25
|
private state;
|
|
26
|
-
private
|
|
26
|
+
private startPromise;
|
|
27
27
|
constructor(opts: ChatSessionOptions);
|
|
28
28
|
/** Returns an unsubscribe function. Listeners are called synchronously. */
|
|
29
29
|
onEvent(listener: Listener): () => void;
|
|
30
30
|
getState(): ChatSessionState;
|
|
31
31
|
start(): Promise<void>;
|
|
32
|
+
private runStart;
|
|
32
33
|
/**
|
|
33
34
|
* Mark the active conversation as ended on the server and lock the
|
|
34
35
|
* session locally. After this, sends throw — the consumer should drop
|
|
@@ -68,6 +69,7 @@ export declare class ChatSession {
|
|
|
68
69
|
private removeMessage;
|
|
69
70
|
private setStatus;
|
|
70
71
|
private emitStateChange;
|
|
72
|
+
private debug;
|
|
71
73
|
private handleError;
|
|
72
74
|
private emit;
|
|
73
75
|
}
|
package/dist/session.js
CHANGED
|
@@ -25,7 +25,13 @@ class ChatSession {
|
|
|
25
25
|
status: 'idle',
|
|
26
26
|
messages: [],
|
|
27
27
|
};
|
|
28
|
-
|
|
28
|
+
// Tracks the in-flight (or completed) `start()` invocation.
|
|
29
|
+
// Used so concurrent callers — typically a React effect that
|
|
30
|
+
// fires `void s.start()` AND a user that taps Send before
|
|
31
|
+
// start resolved — both await the SAME promise instead of
|
|
32
|
+
// either firing two starts or skipping the wait entirely.
|
|
33
|
+
// Resolves once status reaches `ready`/`ended` (or throws).
|
|
34
|
+
this.startPromise = null;
|
|
29
35
|
if (!opts.token)
|
|
30
36
|
throw new Error('ChatSession: token is required');
|
|
31
37
|
const apiBaseUrl = opts.apiBaseUrl ?? defaultApiBase();
|
|
@@ -49,9 +55,14 @@ class ChatSession {
|
|
|
49
55
|
}
|
|
50
56
|
// ─── Lifecycle ──────────────────────────────────────────────────────────
|
|
51
57
|
async start() {
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
this.
|
|
58
|
+
// Idempotent: if another caller already kicked off start(),
|
|
59
|
+
// join their promise. Critical for the React + send race.
|
|
60
|
+
if (this.startPromise)
|
|
61
|
+
return this.startPromise;
|
|
62
|
+
this.startPromise = this.runStart();
|
|
63
|
+
return this.startPromise;
|
|
64
|
+
}
|
|
65
|
+
async runStart() {
|
|
55
66
|
this.setStatus('loading');
|
|
56
67
|
try {
|
|
57
68
|
const { agent, theme } = await this.transport.getAgent();
|
|
@@ -141,7 +152,7 @@ class ChatSession {
|
|
|
141
152
|
this.emit({ type: 'destroyed' });
|
|
142
153
|
// Drop everything — a destroyed session shouldn't be reused.
|
|
143
154
|
this.state = { status: 'idle', messages: [] };
|
|
144
|
-
this.
|
|
155
|
+
this.startPromise = null;
|
|
145
156
|
}
|
|
146
157
|
// ─── User actions ───────────────────────────────────────────────────────
|
|
147
158
|
/**
|
|
@@ -161,11 +172,16 @@ class ChatSession {
|
|
|
161
172
|
const trimmed = text.trim();
|
|
162
173
|
if (!trimmed)
|
|
163
174
|
return '';
|
|
175
|
+
this.debug('send() called', { text: trimmed, status: this.state.status });
|
|
164
176
|
if (this.state.status === 'ended') {
|
|
165
177
|
throw new Error('Conversation has ended. Start a fresh chat to continue.');
|
|
166
178
|
}
|
|
167
|
-
|
|
168
|
-
|
|
179
|
+
// Always await start — start() is idempotent and resolves
|
|
180
|
+
// immediately if it already completed. This closes the race
|
|
181
|
+
// where the user taps Send while the initial agent/theme
|
|
182
|
+
// fetch is still in flight and conversationId is unset.
|
|
183
|
+
await this.start();
|
|
184
|
+
this.debug('send() start awaited, status=' + this.state.status);
|
|
169
185
|
if (!opts?.silent) {
|
|
170
186
|
this.appendMessage({
|
|
171
187
|
id: makeMessageId('u'),
|
|
@@ -189,6 +205,7 @@ class ChatSession {
|
|
|
189
205
|
}
|
|
190
206
|
// ─── Internals ──────────────────────────────────────────────────────────
|
|
191
207
|
async runStream(text, assistant) {
|
|
208
|
+
this.debug('runStream start', { hasConvId: !!this.state.conversationId });
|
|
192
209
|
this.setStatus('sending');
|
|
193
210
|
// The "active" message is the one we're currently appending
|
|
194
211
|
// text_deltas into. For non-team sessions it's always the initial
|
|
@@ -207,8 +224,11 @@ class ChatSession {
|
|
|
207
224
|
const generator = this.state.conversationId
|
|
208
225
|
? this.transport.streamSendMessage(this.state.conversationId, text, this.browserSessionId)
|
|
209
226
|
: this.transport.streamCreateConversation(text, this.browserSessionId);
|
|
227
|
+
this.debug('runStream got generator, awaiting events');
|
|
210
228
|
let sawAnyChunk = false;
|
|
229
|
+
let chunkCount = 0;
|
|
211
230
|
for await (const evt of generator) {
|
|
231
|
+
this.debug('SSE evt', evt.kind, chunkCount++);
|
|
212
232
|
if (evt.kind === 'conversation') {
|
|
213
233
|
this.state.conversationId = evt.id;
|
|
214
234
|
this.emit({ type: 'conversation_started', conversationId: evt.id });
|
|
@@ -384,6 +404,7 @@ class ChatSession {
|
|
|
384
404
|
cleanup(assistant);
|
|
385
405
|
if (active !== assistant)
|
|
386
406
|
cleanup(active);
|
|
407
|
+
this.debug('runStream THREW', err instanceof Error ? err.message : String(err));
|
|
387
408
|
this.handleError(err);
|
|
388
409
|
return assistant.content;
|
|
389
410
|
}
|
|
@@ -444,6 +465,50 @@ class ChatSession {
|
|
|
444
465
|
emitStateChange() {
|
|
445
466
|
this.emit({ type: 'state', state: this.getState() });
|
|
446
467
|
}
|
|
468
|
+
// Lightweight debug logger that BOTH logs to console AND appends
|
|
469
|
+
// to a visible on-screen overlay so mobile QA without remote
|
|
470
|
+
// devtools can still see what's happening. Enable by running
|
|
471
|
+
// `localStorage.setItem('af-chat-debug', '1')` in the browser
|
|
472
|
+
// console (or via the URL `?af-debug=1` once mounted) and
|
|
473
|
+
// reloading. Disabled by default so production ships clean.
|
|
474
|
+
debug(...args) {
|
|
475
|
+
try {
|
|
476
|
+
if (typeof window === 'undefined')
|
|
477
|
+
return;
|
|
478
|
+
const ls = window.localStorage;
|
|
479
|
+
let enabled = ls?.getItem('af-chat-debug') === '1';
|
|
480
|
+
if (!enabled && window.location?.search?.includes('af-debug=1')) {
|
|
481
|
+
ls?.setItem('af-chat-debug', '1');
|
|
482
|
+
enabled = true;
|
|
483
|
+
}
|
|
484
|
+
if (!enabled)
|
|
485
|
+
return;
|
|
486
|
+
const msg = args
|
|
487
|
+
.map((a) => (typeof a === 'string' ? a : JSON.stringify(a)))
|
|
488
|
+
.join(' ');
|
|
489
|
+
// eslint-disable-next-line no-console
|
|
490
|
+
console.log('[chat-sdk]', msg);
|
|
491
|
+
// Visible overlay (single shared element across all sessions).
|
|
492
|
+
let panel = document.getElementById('af-debug-panel');
|
|
493
|
+
if (!panel) {
|
|
494
|
+
panel = document.createElement('div');
|
|
495
|
+
panel.id = 'af-debug-panel';
|
|
496
|
+
panel.style.cssText =
|
|
497
|
+
'position:fixed;top:0;left:0;right:0;max-height:38vh;overflow:auto;' +
|
|
498
|
+
'background:rgba(0,0,0,0.85);color:#7CFC00;font:11px/1.3 monospace;' +
|
|
499
|
+
'padding:6px 8px;z-index:2147483647;pointer-events:auto;' +
|
|
500
|
+
'white-space:pre-wrap;word-break:break-all;';
|
|
501
|
+
document.body.appendChild(panel);
|
|
502
|
+
}
|
|
503
|
+
const line = document.createElement('div');
|
|
504
|
+
line.textContent = `${new Date().toISOString().slice(11, 23)} ${msg}`;
|
|
505
|
+
panel.appendChild(line);
|
|
506
|
+
panel.scrollTop = panel.scrollHeight;
|
|
507
|
+
}
|
|
508
|
+
catch {
|
|
509
|
+
/* localStorage can throw in private mode — ignore */
|
|
510
|
+
}
|
|
511
|
+
}
|
|
447
512
|
handleError(err) {
|
|
448
513
|
const message = err instanceof Error ? err.message : String(err);
|
|
449
514
|
const code = err && typeof err === 'object' && 'code' in err
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentforge-io/chat-sdk",
|
|
3
|
-
"version": "2.4.0-dev.
|
|
3
|
+
"version": "2.4.0-dev.10",
|
|
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",
|
|
@@ -24,23 +24,17 @@
|
|
|
24
24
|
"clean": "rm -rf dist *.tgz"
|
|
25
25
|
},
|
|
26
26
|
"peerDependencies": {
|
|
27
|
-
"react": ">=18.0.0"
|
|
28
|
-
"vaul": ">=1.0.0"
|
|
27
|
+
"react": ">=18.0.0"
|
|
29
28
|
},
|
|
30
29
|
"peerDependenciesMeta": {
|
|
31
30
|
"react": {
|
|
32
31
|
"optional": true
|
|
33
|
-
},
|
|
34
|
-
"vaul": {
|
|
35
|
-
"optional": true
|
|
36
32
|
}
|
|
37
33
|
},
|
|
38
34
|
"devDependencies": {
|
|
39
35
|
"@types/node": "^20.0.0",
|
|
40
36
|
"@types/react": "^18.3.28",
|
|
41
37
|
"react": "^18.3.1",
|
|
42
|
-
"
|
|
43
|
-
"typescript": "^5.0.0",
|
|
44
|
-
"vaul": "^1.1.2"
|
|
38
|
+
"typescript": "^5.0.0"
|
|
45
39
|
}
|
|
46
40
|
}
|