@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.
@@ -14,8 +14,15 @@ type AiChatProps$1 = {
14
14
  * conversation. `null` (or omitted) renders the empty new-chat state.
15
15
  */
16
16
  initialSessionId?: string | null;
17
+ /**
18
+ * URL prefix the chat is mounted at. Sidebar `<Link>`s and router
19
+ * pushes use this prefix so multiple chat surfaces (e.g. `/chat` and
20
+ * `/admin/ai`) can coexist without yanking users between shells.
21
+ * Defaults to `/chat`.
22
+ */
23
+ basePath?: string;
17
24
  };
18
- declare function AiChat({ userFirstName, scopeLabel, initialProvider, initialSessionId }: AiChatProps$1): react_jsx_runtime.JSX.Element;
25
+ declare function AiChat({ userFirstName, scopeLabel, initialProvider, initialSessionId, basePath }: AiChatProps$1): react_jsx_runtime.JSX.Element;
19
26
 
20
27
  type AiChatSessionSummary = {
21
28
  id: string;
@@ -35,9 +42,18 @@ type AiChatProps = {
35
42
  * empty "new chat" state.
36
43
  */
37
44
  initialSessionId?: string | null;
45
+ /**
46
+ * URL prefix the chat surface is mounted at. Sidebar links resolve to
47
+ * `${basePath}/${sessionId}`, "+ New chat" goes to `${basePath}?new`,
48
+ * and the open-session push uses the same prefix. Defaults to `/chat`.
49
+ * Set to e.g. `/admin/ai` when embedding the chat inside another shell
50
+ * — the host then needs a matching `[id]` dynamic segment under that
51
+ * path so reload/bookmark URLs resolve.
52
+ */
53
+ basePath?: string;
38
54
  };
39
55
 
40
- declare function VercelChat({ userFirstName, scopeLabel, initialProvider, initialSessionId }: AiChatProps): react_jsx_runtime.JSX.Element;
56
+ declare function VercelChat({ userFirstName, scopeLabel, initialProvider, initialSessionId, basePath }: AiChatProps): react_jsx_runtime.JSX.Element;
41
57
 
42
58
  type ChartSpec = {
43
59
  type: 'line' | 'bar' | 'stacked_bar' | 'pie';
@@ -14,8 +14,15 @@ type AiChatProps$1 = {
14
14
  * conversation. `null` (or omitted) renders the empty new-chat state.
15
15
  */
16
16
  initialSessionId?: string | null;
17
+ /**
18
+ * URL prefix the chat is mounted at. Sidebar `<Link>`s and router
19
+ * pushes use this prefix so multiple chat surfaces (e.g. `/chat` and
20
+ * `/admin/ai`) can coexist without yanking users between shells.
21
+ * Defaults to `/chat`.
22
+ */
23
+ basePath?: string;
17
24
  };
18
- declare function AiChat({ userFirstName, scopeLabel, initialProvider, initialSessionId }: AiChatProps$1): react_jsx_runtime.JSX.Element;
25
+ declare function AiChat({ userFirstName, scopeLabel, initialProvider, initialSessionId, basePath }: AiChatProps$1): react_jsx_runtime.JSX.Element;
19
26
 
20
27
  type AiChatSessionSummary = {
21
28
  id: string;
@@ -35,9 +42,18 @@ type AiChatProps = {
35
42
  * empty "new chat" state.
36
43
  */
37
44
  initialSessionId?: string | null;
45
+ /**
46
+ * URL prefix the chat surface is mounted at. Sidebar links resolve to
47
+ * `${basePath}/${sessionId}`, "+ New chat" goes to `${basePath}?new`,
48
+ * and the open-session push uses the same prefix. Defaults to `/chat`.
49
+ * Set to e.g. `/admin/ai` when embedding the chat inside another shell
50
+ * — the host then needs a matching `[id]` dynamic segment under that
51
+ * path so reload/bookmark URLs resolve.
52
+ */
53
+ basePath?: string;
38
54
  };
39
55
 
40
- declare function VercelChat({ userFirstName, scopeLabel, initialProvider, initialSessionId }: AiChatProps): react_jsx_runtime.JSX.Element;
56
+ declare function VercelChat({ userFirstName, scopeLabel, initialProvider, initialSessionId, basePath }: AiChatProps): react_jsx_runtime.JSX.Element;
41
57
 
42
58
  type ChartSpec = {
43
59
  type: 'line' | 'bar' | 'stacked_bar' | 'pie';
package/dist/ui/index.js CHANGED
@@ -654,7 +654,8 @@ function AiChat({
654
654
  userFirstName,
655
655
  scopeLabel,
656
656
  initialProvider,
657
- initialSessionId = null
657
+ initialSessionId = null,
658
+ basePath = "/chat"
658
659
  }) {
659
660
  const router = useRouter();
660
661
  const [sessions, setSessions] = useState([]);
@@ -744,16 +745,16 @@ function AiChat({
744
745
  }, []);
745
746
  const syncUrl = useCallback(
746
747
  (id) => {
747
- router.push(id == null ? "/chat" : `/chat/${id}`);
748
+ router.push(id == null ? basePath : `${basePath}/${id}`);
748
749
  },
749
- [router]
750
+ [router, basePath]
750
751
  );
751
752
  const newChat = useCallback(() => {
752
753
  setActiveSessionId(null);
753
754
  setAnswers([]);
754
755
  setQuestion("");
755
- router.push("/chat?new");
756
- }, [router]);
756
+ router.push(`${basePath}?new`);
757
+ }, [router, basePath]);
757
758
  const changeProvider = useCallback(
758
759
  async (next) => {
759
760
  if (next === provider || providerSaving) return;
@@ -849,7 +850,9 @@ function AiChat({
849
850
  const data = await create.json();
850
851
  sessionId = data.session.id;
851
852
  setActiveSessionId(sessionId);
852
- syncUrl(sessionId);
853
+ if (typeof window !== "undefined") {
854
+ window.history.replaceState(null, "", `${basePath}/${sessionId}`);
855
+ }
853
856
  setSessions((prev) => [
854
857
  { id: data.session.id, title: data.session.title, updatedAt: null },
855
858
  ...prev
@@ -911,6 +914,17 @@ function AiChat({
911
914
  const events = buffer.split("\n\n");
912
915
  buffer = events.pop() ?? "";
913
916
  for (const raw of events) {
917
+ const meta = parseMetaChatSessionId(raw);
918
+ if (meta != null) {
919
+ setActiveSessionId((prev) => prev ?? meta);
920
+ if (typeof window !== "undefined" && !window.location.pathname.endsWith(`/${meta}`)) {
921
+ window.history.replaceState(
922
+ null,
923
+ "",
924
+ `${basePath}/${meta}`
925
+ );
926
+ }
927
+ }
914
928
  handleEvent(raw, setAnswers);
915
929
  }
916
930
  }
@@ -1006,7 +1020,7 @@ function AiChat({
1006
1020
  /* @__PURE__ */ jsx(
1007
1021
  Link,
1008
1022
  {
1009
- href: `/chat/${s.id}`,
1023
+ href: `${basePath}/${s.id}`,
1010
1024
  onClick: () => setSidebarOpen(false),
1011
1025
  className: cn(
1012
1026
  "flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm",
@@ -1369,6 +1383,22 @@ function UserChip({ text }) {
1369
1383
  )
1370
1384
  ] });
1371
1385
  }
1386
+ function parseMetaChatSessionId(raw) {
1387
+ const lines = raw.split("\n");
1388
+ let event = "";
1389
+ let dataStr = "";
1390
+ for (const line of lines) {
1391
+ if (line.startsWith("event: ")) event = line.slice(7).trim();
1392
+ else if (line.startsWith("data: ")) dataStr += line.slice(6);
1393
+ }
1394
+ if (event !== "meta") return null;
1395
+ try {
1396
+ const parsed = JSON.parse(dataStr || "{}");
1397
+ return typeof parsed.chatSessionId === "string" && parsed.chatSessionId.length > 0 ? parsed.chatSessionId : null;
1398
+ } catch {
1399
+ return null;
1400
+ }
1401
+ }
1372
1402
  function handleEvent(raw, setAnswers) {
1373
1403
  const lines = raw.split("\n");
1374
1404
  let event = "";
@@ -1489,7 +1519,8 @@ function VercelChat({
1489
1519
  userFirstName,
1490
1520
  scopeLabel,
1491
1521
  initialProvider,
1492
- initialSessionId = null
1522
+ initialSessionId = null,
1523
+ basePath = "/chat"
1493
1524
  }) {
1494
1525
  const router = useRouter();
1495
1526
  const [sessions, setSessions] = useState([]);
@@ -1620,6 +1651,20 @@ function VercelChat({
1620
1651
  cancelled = true;
1621
1652
  };
1622
1653
  }, [initialSessionId, setMessages]);
1654
+ useEffect(() => {
1655
+ if (!Array.isArray(data)) return;
1656
+ for (const raw of data) {
1657
+ const part = asDataPart(raw);
1658
+ if (!part || part.type !== "meta") continue;
1659
+ const id = part.value.chatSessionId;
1660
+ if (!id) continue;
1661
+ setActiveSessionId((prev) => prev ?? id);
1662
+ if (typeof window !== "undefined" && !window.location.pathname.endsWith(`/${id}`)) {
1663
+ window.history.replaceState(null, "", `${basePath}/${id}`);
1664
+ }
1665
+ break;
1666
+ }
1667
+ }, [data, basePath]);
1623
1668
  const answers = useMemo(() => {
1624
1669
  const liveBlocks = [];
1625
1670
  const liveErrors = [];
@@ -1720,9 +1765,9 @@ function VercelChat({
1720
1765
  }, [answers.length]);
1721
1766
  const syncUrl = useCallback(
1722
1767
  (id) => {
1723
- router.push(id == null ? "/chat" : `/chat/${id}`);
1768
+ router.push(id == null ? basePath : `${basePath}/${id}`);
1724
1769
  },
1725
- [router]
1770
+ [router, basePath]
1726
1771
  );
1727
1772
  const newChat = useCallback(() => {
1728
1773
  setActiveSessionId(null);
@@ -1732,8 +1777,8 @@ function VercelChat({
1732
1777
  setHydratedErrors({});
1733
1778
  setStartedAt({});
1734
1779
  setInput("");
1735
- router.push("/chat?new");
1736
- }, [setMessages, setInput, router]);
1780
+ router.push(`${basePath}?new`);
1781
+ }, [setMessages, setInput, router, basePath]);
1737
1782
  const changeProvider = useCallback(
1738
1783
  async (next) => {
1739
1784
  if (next === provider || providerSaving) return;
@@ -1842,7 +1887,13 @@ function VercelChat({
1842
1887
  const json = await create.json();
1843
1888
  activeSessionIdRef.current = json.session.id;
1844
1889
  setActiveSessionId(json.session.id);
1845
- syncUrl(json.session.id);
1890
+ if (typeof window !== "undefined") {
1891
+ window.history.replaceState(
1892
+ null,
1893
+ "",
1894
+ `${basePath}/${json.session.id}`
1895
+ );
1896
+ }
1846
1897
  setSessions((prev) => [
1847
1898
  { id: json.session.id, title: json.session.title, updatedAt: null },
1848
1899
  ...prev
@@ -1963,7 +2014,7 @@ function VercelChat({
1963
2014
  /* @__PURE__ */ jsx(
1964
2015
  Link,
1965
2016
  {
1966
- href: `/chat/${s.id}`,
2017
+ href: `${basePath}/${s.id}`,
1967
2018
  onClick: () => setSidebarOpen(false),
1968
2019
  className: cn(
1969
2020
  "flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm",