@firstlovecenter/ai-chat 0.9.0 → 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 +26 -0
- package/dist/ui/index.cjs +65 -14
- package/dist/ui/index.cjs.map +1 -1
- package/dist/ui/index.d.cts +18 -2
- package/dist/ui/index.d.ts +18 -2
- package/dist/ui/index.js +65 -14
- package/dist/ui/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,32 @@ 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
|
+
|
|
20
|
+
## [0.9.1] — 2026-05-09
|
|
21
|
+
|
|
22
|
+
Lets a host mount multiple chat surfaces side-by-side without yanking users between shells.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- **`AiChatProps.basePath`** (`string`, optional, default `'/chat'`) — URL prefix the chat surface is mounted at. Sidebar `<Link>` entries resolve to `${basePath}/${sessionId}`, `+ New chat` pushes `${basePath}?new`, and the open-session router push uses the same prefix. Both `AiChat` and `VercelChat` honour it.
|
|
27
|
+
|
|
28
|
+
### Migration notes
|
|
29
|
+
|
|
30
|
+
- Pure addition — `basePath` defaults to `/chat`, so existing `/chat`-mounted surfaces work unchanged.
|
|
31
|
+
- Hosts that want a second surface (e.g. an admin AI page at `/admin/ai`) need to (a) pass `basePath="/admin/ai"`, (b) add a matching dynamic segment (`/admin/ai/[id]`) so reload/bookmark URLs resolve, and (c) wire a redirect-to-most-recent at the basePath that respects `?new`.
|
|
32
|
+
- **Tailwind v4 hosts**: the package ships no CSS, so its component classes need to be in your Tailwind content scan. Add `@source "../node_modules/@firstlovecenter/ai-chat";` to your `globals.css` (path relative to the CSS file). Without this, the in-chat sidebar's `absolute`/`translate-x-*`/`bg-sidebar` classes are stripped from the production bundle and the sidebar renders as a static flex column instead of an overlay.
|
|
33
|
+
|
|
8
34
|
## [0.9.0] — 2026-05-09
|
|
9
35
|
|
|
10
36
|
Chat session and message identifiers are now short URL-safe UIDs (12 chars) instead of auto-incrementing BIGINTs. URLs like `/chat/V1StGXR8_Z5j` don't leak per-user chat counts and are unguessable, which matters as soon as a session URL is ever shared.
|
package/dist/ui/index.cjs
CHANGED
|
@@ -678,7 +678,8 @@ function AiChat({
|
|
|
678
678
|
userFirstName,
|
|
679
679
|
scopeLabel,
|
|
680
680
|
initialProvider,
|
|
681
|
-
initialSessionId = null
|
|
681
|
+
initialSessionId = null,
|
|
682
|
+
basePath = "/chat"
|
|
682
683
|
}) {
|
|
683
684
|
const router = navigation.useRouter();
|
|
684
685
|
const [sessions, setSessions] = React.useState([]);
|
|
@@ -768,16 +769,16 @@ function AiChat({
|
|
|
768
769
|
}, []);
|
|
769
770
|
const syncUrl = React.useCallback(
|
|
770
771
|
(id) => {
|
|
771
|
-
router.push(id == null ?
|
|
772
|
+
router.push(id == null ? basePath : `${basePath}/${id}`);
|
|
772
773
|
},
|
|
773
|
-
[router]
|
|
774
|
+
[router, basePath]
|
|
774
775
|
);
|
|
775
776
|
const newChat = React.useCallback(() => {
|
|
776
777
|
setActiveSessionId(null);
|
|
777
778
|
setAnswers([]);
|
|
778
779
|
setQuestion("");
|
|
779
|
-
router.push(
|
|
780
|
-
}, [router]);
|
|
780
|
+
router.push(`${basePath}?new`);
|
|
781
|
+
}, [router, basePath]);
|
|
781
782
|
const changeProvider = React.useCallback(
|
|
782
783
|
async (next) => {
|
|
783
784
|
if (next === provider || providerSaving) return;
|
|
@@ -873,7 +874,9 @@ function AiChat({
|
|
|
873
874
|
const data = await create.json();
|
|
874
875
|
sessionId = data.session.id;
|
|
875
876
|
setActiveSessionId(sessionId);
|
|
876
|
-
|
|
877
|
+
if (typeof window !== "undefined") {
|
|
878
|
+
window.history.replaceState(null, "", `${basePath}/${sessionId}`);
|
|
879
|
+
}
|
|
877
880
|
setSessions((prev) => [
|
|
878
881
|
{ id: data.session.id, title: data.session.title, updatedAt: null },
|
|
879
882
|
...prev
|
|
@@ -935,6 +938,17 @@ function AiChat({
|
|
|
935
938
|
const events = buffer.split("\n\n");
|
|
936
939
|
buffer = events.pop() ?? "";
|
|
937
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
|
+
}
|
|
938
952
|
handleEvent(raw, setAnswers);
|
|
939
953
|
}
|
|
940
954
|
}
|
|
@@ -1030,7 +1044,7 @@ function AiChat({
|
|
|
1030
1044
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1031
1045
|
Link__default.default,
|
|
1032
1046
|
{
|
|
1033
|
-
href:
|
|
1047
|
+
href: `${basePath}/${s.id}`,
|
|
1034
1048
|
onClick: () => setSidebarOpen(false),
|
|
1035
1049
|
className: cn(
|
|
1036
1050
|
"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm",
|
|
@@ -1393,6 +1407,22 @@ function UserChip({ text }) {
|
|
|
1393
1407
|
)
|
|
1394
1408
|
] });
|
|
1395
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
|
+
}
|
|
1396
1426
|
function handleEvent(raw, setAnswers) {
|
|
1397
1427
|
const lines = raw.split("\n");
|
|
1398
1428
|
let event = "";
|
|
@@ -1513,7 +1543,8 @@ function VercelChat({
|
|
|
1513
1543
|
userFirstName,
|
|
1514
1544
|
scopeLabel,
|
|
1515
1545
|
initialProvider,
|
|
1516
|
-
initialSessionId = null
|
|
1546
|
+
initialSessionId = null,
|
|
1547
|
+
basePath = "/chat"
|
|
1517
1548
|
}) {
|
|
1518
1549
|
const router = navigation.useRouter();
|
|
1519
1550
|
const [sessions, setSessions] = React.useState([]);
|
|
@@ -1644,6 +1675,20 @@ function VercelChat({
|
|
|
1644
1675
|
cancelled = true;
|
|
1645
1676
|
};
|
|
1646
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]);
|
|
1647
1692
|
const answers = React.useMemo(() => {
|
|
1648
1693
|
const liveBlocks = [];
|
|
1649
1694
|
const liveErrors = [];
|
|
@@ -1744,9 +1789,9 @@ function VercelChat({
|
|
|
1744
1789
|
}, [answers.length]);
|
|
1745
1790
|
const syncUrl = React.useCallback(
|
|
1746
1791
|
(id) => {
|
|
1747
|
-
router.push(id == null ?
|
|
1792
|
+
router.push(id == null ? basePath : `${basePath}/${id}`);
|
|
1748
1793
|
},
|
|
1749
|
-
[router]
|
|
1794
|
+
[router, basePath]
|
|
1750
1795
|
);
|
|
1751
1796
|
const newChat = React.useCallback(() => {
|
|
1752
1797
|
setActiveSessionId(null);
|
|
@@ -1756,8 +1801,8 @@ function VercelChat({
|
|
|
1756
1801
|
setHydratedErrors({});
|
|
1757
1802
|
setStartedAt({});
|
|
1758
1803
|
setInput("");
|
|
1759
|
-
router.push(
|
|
1760
|
-
}, [setMessages, setInput, router]);
|
|
1804
|
+
router.push(`${basePath}?new`);
|
|
1805
|
+
}, [setMessages, setInput, router, basePath]);
|
|
1761
1806
|
const changeProvider = React.useCallback(
|
|
1762
1807
|
async (next) => {
|
|
1763
1808
|
if (next === provider || providerSaving) return;
|
|
@@ -1866,7 +1911,13 @@ function VercelChat({
|
|
|
1866
1911
|
const json = await create.json();
|
|
1867
1912
|
activeSessionIdRef.current = json.session.id;
|
|
1868
1913
|
setActiveSessionId(json.session.id);
|
|
1869
|
-
|
|
1914
|
+
if (typeof window !== "undefined") {
|
|
1915
|
+
window.history.replaceState(
|
|
1916
|
+
null,
|
|
1917
|
+
"",
|
|
1918
|
+
`${basePath}/${json.session.id}`
|
|
1919
|
+
);
|
|
1920
|
+
}
|
|
1870
1921
|
setSessions((prev) => [
|
|
1871
1922
|
{ id: json.session.id, title: json.session.title, updatedAt: null },
|
|
1872
1923
|
...prev
|
|
@@ -1987,7 +2038,7 @@ function VercelChat({
|
|
|
1987
2038
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1988
2039
|
Link__default.default,
|
|
1989
2040
|
{
|
|
1990
|
-
href:
|
|
2041
|
+
href: `${basePath}/${s.id}`,
|
|
1991
2042
|
onClick: () => setSidebarOpen(false),
|
|
1992
2043
|
className: cn(
|
|
1993
2044
|
"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm",
|