@firstlovecenter/ai-chat 0.9.1 → 0.9.2
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/CHANGELOG.md +12 -0
- package/dist/ui/index.cjs +51 -2
- package/dist/ui/index.cjs.map +1 -1
- package/dist/ui/index.js +51 -2
- package/dist/ui/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,18 @@ All notable changes to `@firstlovecenter/ai-chat` are documented here.
|
|
|
5
5
|
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.9.2] — 2026-05-09
|
|
9
|
+
|
|
10
|
+
Fixes a streaming-vs-navigation race that caused the first turn of a brand-new conversation to vanish on reload.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Race condition on lazy-create-on-submit**. When the user sent the first message in a fresh conversation, the client used `router.push('/chat/<uid>')` to update the URL. App-Router navigation **unmounted the chat component mid-streaming**, so the in-flight SSE / Vercel-AI data stream was dropped on the floor. The newly-mounted page would then re-hydrate from `/api/chat/sessions/<uid>` *before* the assistant message had finished persisting — so reload showed an empty thread. Now the URL is updated via `window.history.replaceState` (shallow update, no remount), and the streaming connection survives. Affects both `AiChat` and `VercelChat`.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **Defensive `meta` event handling**. The custom-SSE route emits `event: meta` carrying the persistent `chatSessionId` as its first frame; the Vercel route emits the same as a `{ type: 'meta' }` data part. Both shells now sync `activeSessionId` and the URL from that event as a backstop, so even if the lazy-create POST silently fails the client still picks up the server-resolved session id.
|
|
19
|
+
|
|
8
20
|
## [0.9.1] — 2026-05-09
|
|
9
21
|
|
|
10
22
|
Lets a host mount multiple chat surfaces side-by-side without yanking users between shells.
|
package/dist/ui/index.cjs
CHANGED
|
@@ -874,7 +874,9 @@ function AiChat({
|
|
|
874
874
|
const data = await create.json();
|
|
875
875
|
sessionId = data.session.id;
|
|
876
876
|
setActiveSessionId(sessionId);
|
|
877
|
-
|
|
877
|
+
if (typeof window !== "undefined") {
|
|
878
|
+
window.history.replaceState(null, "", `${basePath}/${sessionId}`);
|
|
879
|
+
}
|
|
878
880
|
setSessions((prev) => [
|
|
879
881
|
{ id: data.session.id, title: data.session.title, updatedAt: null },
|
|
880
882
|
...prev
|
|
@@ -936,6 +938,17 @@ function AiChat({
|
|
|
936
938
|
const events = buffer.split("\n\n");
|
|
937
939
|
buffer = events.pop() ?? "";
|
|
938
940
|
for (const raw of events) {
|
|
941
|
+
const meta = parseMetaChatSessionId(raw);
|
|
942
|
+
if (meta != null) {
|
|
943
|
+
setActiveSessionId((prev) => prev ?? meta);
|
|
944
|
+
if (typeof window !== "undefined" && !window.location.pathname.endsWith(`/${meta}`)) {
|
|
945
|
+
window.history.replaceState(
|
|
946
|
+
null,
|
|
947
|
+
"",
|
|
948
|
+
`${basePath}/${meta}`
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
939
952
|
handleEvent(raw, setAnswers);
|
|
940
953
|
}
|
|
941
954
|
}
|
|
@@ -1394,6 +1407,22 @@ function UserChip({ text }) {
|
|
|
1394
1407
|
)
|
|
1395
1408
|
] });
|
|
1396
1409
|
}
|
|
1410
|
+
function parseMetaChatSessionId(raw) {
|
|
1411
|
+
const lines = raw.split("\n");
|
|
1412
|
+
let event = "";
|
|
1413
|
+
let dataStr = "";
|
|
1414
|
+
for (const line of lines) {
|
|
1415
|
+
if (line.startsWith("event: ")) event = line.slice(7).trim();
|
|
1416
|
+
else if (line.startsWith("data: ")) dataStr += line.slice(6);
|
|
1417
|
+
}
|
|
1418
|
+
if (event !== "meta") return null;
|
|
1419
|
+
try {
|
|
1420
|
+
const parsed = JSON.parse(dataStr || "{}");
|
|
1421
|
+
return typeof parsed.chatSessionId === "string" && parsed.chatSessionId.length > 0 ? parsed.chatSessionId : null;
|
|
1422
|
+
} catch {
|
|
1423
|
+
return null;
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1397
1426
|
function handleEvent(raw, setAnswers) {
|
|
1398
1427
|
const lines = raw.split("\n");
|
|
1399
1428
|
let event = "";
|
|
@@ -1646,6 +1675,20 @@ function VercelChat({
|
|
|
1646
1675
|
cancelled = true;
|
|
1647
1676
|
};
|
|
1648
1677
|
}, [initialSessionId, setMessages]);
|
|
1678
|
+
React.useEffect(() => {
|
|
1679
|
+
if (!Array.isArray(data)) return;
|
|
1680
|
+
for (const raw of data) {
|
|
1681
|
+
const part = asDataPart(raw);
|
|
1682
|
+
if (!part || part.type !== "meta") continue;
|
|
1683
|
+
const id = part.value.chatSessionId;
|
|
1684
|
+
if (!id) continue;
|
|
1685
|
+
setActiveSessionId((prev) => prev ?? id);
|
|
1686
|
+
if (typeof window !== "undefined" && !window.location.pathname.endsWith(`/${id}`)) {
|
|
1687
|
+
window.history.replaceState(null, "", `${basePath}/${id}`);
|
|
1688
|
+
}
|
|
1689
|
+
break;
|
|
1690
|
+
}
|
|
1691
|
+
}, [data, basePath]);
|
|
1649
1692
|
const answers = React.useMemo(() => {
|
|
1650
1693
|
const liveBlocks = [];
|
|
1651
1694
|
const liveErrors = [];
|
|
@@ -1868,7 +1911,13 @@ function VercelChat({
|
|
|
1868
1911
|
const json = await create.json();
|
|
1869
1912
|
activeSessionIdRef.current = json.session.id;
|
|
1870
1913
|
setActiveSessionId(json.session.id);
|
|
1871
|
-
|
|
1914
|
+
if (typeof window !== "undefined") {
|
|
1915
|
+
window.history.replaceState(
|
|
1916
|
+
null,
|
|
1917
|
+
"",
|
|
1918
|
+
`${basePath}/${json.session.id}`
|
|
1919
|
+
);
|
|
1920
|
+
}
|
|
1872
1921
|
setSessions((prev) => [
|
|
1873
1922
|
{ id: json.session.id, title: json.session.title, updatedAt: null },
|
|
1874
1923
|
...prev
|