@agentforge-io/chat-sdk 2.1.0 → 2.1.1
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.js +45 -6
- package/package.json +1 -1
package/dist/react.js
CHANGED
|
@@ -250,17 +250,36 @@ function ChatWidget(props) {
|
|
|
250
250
|
const messagesRef = (0, react_1.useRef)(null);
|
|
251
251
|
const inputRef = (0, react_1.useRef)(null);
|
|
252
252
|
const styleId = (0, react_1.useId)();
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
//
|
|
253
|
+
// Track whether the user has already engaged with the widget. We
|
|
254
|
+
// start at false (just landed on the page) and flip to true the
|
|
255
|
+
// moment `handleSend` fires. The auto-focus effect below uses this
|
|
256
|
+
// to decide whether the impending `status = 'ready'` is the first
|
|
257
|
+
// landing (skip focus on touch — opening the keyboard unprompted
|
|
258
|
+
// is hostile) or a return from a send-streaming cycle (re-focus
|
|
259
|
+
// ALWAYS — the user is mid-conversation, losing the cursor
|
|
260
|
+
// breaks the typing-rhythm).
|
|
261
|
+
const hasInteractedRef = (0, react_1.useRef)(false);
|
|
262
|
+
// Auto-focus the composer once the session is ready.
|
|
263
|
+
//
|
|
264
|
+
// Landing (hasInteractedRef.current === false):
|
|
265
|
+
// • pointer:fine → focus (desktop user expects ready-to-type)
|
|
266
|
+
// • pointer:coarse → skip (iOS/Android opening the keyboard
|
|
267
|
+
// before the visitor reads anything
|
|
268
|
+
// is jarring)
|
|
269
|
+
//
|
|
270
|
+
// Returning from a send (hasInteractedRef.current === true):
|
|
271
|
+
// • always focus, including touch — the user just hit Send,
|
|
272
|
+
// they're in conversation mode, their next thought is the
|
|
273
|
+
// next message, the keyboard should stay alive.
|
|
257
274
|
(0, react_1.useEffect)(() => {
|
|
258
275
|
if (status !== 'ready')
|
|
259
276
|
return;
|
|
260
277
|
if (typeof window === 'undefined')
|
|
261
278
|
return;
|
|
262
|
-
if (!
|
|
279
|
+
if (!hasInteractedRef.current &&
|
|
280
|
+
!window.matchMedia('(pointer: fine)').matches) {
|
|
263
281
|
return;
|
|
282
|
+
}
|
|
264
283
|
inputRef.current?.focus({ preventScroll: true });
|
|
265
284
|
}, [status]);
|
|
266
285
|
// ── Session lifecycle. Recreate when token / apiBaseUrl changes. ──────
|
|
@@ -306,11 +325,26 @@ function ChatWidget(props) {
|
|
|
306
325
|
};
|
|
307
326
|
}, [token, apiBaseUrl, browserSessionId, resumeConversationId, stream]);
|
|
308
327
|
// Auto-scroll on new tokens.
|
|
328
|
+
//
|
|
329
|
+
// We defer the scroll into a requestAnimationFrame so the DOM has
|
|
330
|
+
// actually grown by the time we read `scrollHeight`. Without that
|
|
331
|
+
// tick, a stream of small text_deltas can leave the scroll lagging
|
|
332
|
+
// 1–2 chunks behind because the effect runs synchronously after
|
|
333
|
+
// the React commit but BEFORE the browser paints the new rows.
|
|
334
|
+
// Result: the user sees the latest sentence half-cut at the bottom.
|
|
335
|
+
//
|
|
336
|
+
// `behavior: 'smooth'` doesn't help much during a fast stream (each
|
|
337
|
+
// rAF queues a new smooth-scroll that cancels the previous one) but
|
|
338
|
+
// it does make the FINAL settle look fluid — and that's the part
|
|
339
|
+
// the user notices when the stream stops.
|
|
309
340
|
(0, react_1.useEffect)(() => {
|
|
310
341
|
const el = messagesRef.current;
|
|
311
342
|
if (!el)
|
|
312
343
|
return;
|
|
313
|
-
|
|
344
|
+
const raf = requestAnimationFrame(() => {
|
|
345
|
+
el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' });
|
|
346
|
+
});
|
|
347
|
+
return () => cancelAnimationFrame(raf);
|
|
314
348
|
}, [messages]);
|
|
315
349
|
// Inject the widget stylesheet exactly once per page. We key the <style>
|
|
316
350
|
// tag by a fixed id so multiple widget mounts share it.
|
|
@@ -331,6 +365,11 @@ function ChatWidget(props) {
|
|
|
331
365
|
const text = draft.trim();
|
|
332
366
|
if (!text)
|
|
333
367
|
return;
|
|
368
|
+
// Mark engagement BEFORE the status flip so the auto-focus
|
|
369
|
+
// effect (which watches `status`) sees the flag when it
|
|
370
|
+
// re-runs after `ready` returns. Subsequent renders will
|
|
371
|
+
// refocus the textarea on every send completion.
|
|
372
|
+
hasInteractedRef.current = true;
|
|
334
373
|
setDraft('');
|
|
335
374
|
void session.send(text);
|
|
336
375
|
}, [session, draft]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentforge-io/chat-sdk",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.1",
|
|
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",
|