@firstlovecenter/ai-chat 0.7.0 → 0.8.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.
@@ -8,8 +8,14 @@ type AiChatProps$1 = {
8
8
  scopeLabel: string;
9
9
  /** User's stored narrative provider, surfaced as the model picker default. */
10
10
  initialProvider: NarrativeProvider;
11
+ /**
12
+ * Session to open on mount. The host resolves this from the URL
13
+ * (`/chat/[id]`) so reload/bookmark/multi-tab restore the exact
14
+ * conversation. `null` (or omitted) renders the empty new-chat state.
15
+ */
16
+ initialSessionId?: number | null;
11
17
  };
12
- declare function AiChat({ userFirstName, scopeLabel, initialProvider }: AiChatProps$1): react_jsx_runtime.JSX.Element;
18
+ declare function AiChat({ userFirstName, scopeLabel, initialProvider, initialSessionId }: AiChatProps$1): react_jsx_runtime.JSX.Element;
13
19
 
14
20
  type AiChatSessionSummary = {
15
21
  id: number;
@@ -22,9 +28,16 @@ type AiChatProps = {
22
28
  scopeLabel: string;
23
29
  /** User's stored narrative provider, surfaced as the model picker default. */
24
30
  initialProvider: 'claude' | 'grok' | 'gemini';
31
+ /**
32
+ * Session to open on mount. The host resolves this from the URL
33
+ * (`/chat/[id]`) so reload/bookmark/multi-tab restore the exact
34
+ * conversation the user was viewing. Pass `null` (or omit) for the
35
+ * empty "new chat" state.
36
+ */
37
+ initialSessionId?: number | null;
25
38
  };
26
39
 
27
- declare function VercelChat({ userFirstName, scopeLabel, initialProvider }: AiChatProps): react_jsx_runtime.JSX.Element;
40
+ declare function VercelChat({ userFirstName, scopeLabel, initialProvider, initialSessionId }: AiChatProps): react_jsx_runtime.JSX.Element;
28
41
 
29
42
  type ChartSpec = {
30
43
  type: 'line' | 'bar' | 'stacked_bar' | 'pie';
@@ -8,8 +8,14 @@ type AiChatProps$1 = {
8
8
  scopeLabel: string;
9
9
  /** User's stored narrative provider, surfaced as the model picker default. */
10
10
  initialProvider: NarrativeProvider;
11
+ /**
12
+ * Session to open on mount. The host resolves this from the URL
13
+ * (`/chat/[id]`) so reload/bookmark/multi-tab restore the exact
14
+ * conversation. `null` (or omitted) renders the empty new-chat state.
15
+ */
16
+ initialSessionId?: number | null;
11
17
  };
12
- declare function AiChat({ userFirstName, scopeLabel, initialProvider }: AiChatProps$1): react_jsx_runtime.JSX.Element;
18
+ declare function AiChat({ userFirstName, scopeLabel, initialProvider, initialSessionId }: AiChatProps$1): react_jsx_runtime.JSX.Element;
13
19
 
14
20
  type AiChatSessionSummary = {
15
21
  id: number;
@@ -22,9 +28,16 @@ type AiChatProps = {
22
28
  scopeLabel: string;
23
29
  /** User's stored narrative provider, surfaced as the model picker default. */
24
30
  initialProvider: 'claude' | 'grok' | 'gemini';
31
+ /**
32
+ * Session to open on mount. The host resolves this from the URL
33
+ * (`/chat/[id]`) so reload/bookmark/multi-tab restore the exact
34
+ * conversation the user was viewing. Pass `null` (or omit) for the
35
+ * empty "new chat" state.
36
+ */
37
+ initialSessionId?: number | null;
25
38
  };
26
39
 
27
- declare function VercelChat({ userFirstName, scopeLabel, initialProvider }: AiChatProps): react_jsx_runtime.JSX.Element;
40
+ declare function VercelChat({ userFirstName, scopeLabel, initialProvider, initialSessionId }: AiChatProps): react_jsx_runtime.JSX.Element;
28
41
 
29
42
  type ChartSpec = {
30
43
  type: 'line' | 'bar' | 'stacked_bar' | 'pie';
package/dist/ui/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  'use client';
2
2
  import * as React from 'react';
3
3
  import { useMemo, useState, useRef, useLayoutEffect, useEffect, useCallback } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ import Link from 'next/link';
4
6
  import { PanelLeftClose, Plus, Pencil, Trash2, Menu, Sparkles, Loader2, ChevronDown, Check, ArrowUp, Copy, RotateCcw, ChevronUp } from 'lucide-react';
5
7
  import { DropdownMenu as DropdownMenu$1 } from 'radix-ui';
6
8
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
@@ -651,10 +653,14 @@ var PROVIDER_DESCRIPTIONS = {
651
653
  function AiChat({
652
654
  userFirstName,
653
655
  scopeLabel,
654
- initialProvider
656
+ initialProvider,
657
+ initialSessionId = null
655
658
  }) {
659
+ const router = useRouter();
656
660
  const [sessions, setSessions] = useState([]);
657
- const [activeSessionId, setActiveSessionId] = useState(null);
661
+ const [activeSessionId, setActiveSessionId] = useState(
662
+ initialSessionId
663
+ );
658
664
  const [answers, setAnswers] = useState([]);
659
665
  const [pending, setPending] = useState(false);
660
666
  const [question, setQuestion] = useState("");
@@ -668,7 +674,6 @@ function AiChat({
668
674
  const textareaRef = useRef(null);
669
675
  const lastAnswerRef = useRef(null);
670
676
  const prevAnswersLen = useRef(0);
671
- const autoOpenedRef = useRef(false);
672
677
  useLayoutEffect(() => {
673
678
  const el = textareaRef.current;
674
679
  if (!el) return;
@@ -684,33 +689,7 @@ function AiChat({
684
689
  if (!res.ok) return;
685
690
  const data = await res.json();
686
691
  if (cancelled) return;
687
- const list = data.sessions ?? [];
688
- setSessions(list);
689
- if (!autoOpenedRef.current && list.length > 0) {
690
- autoOpenedRef.current = true;
691
- const mostRecentId = list[0].id;
692
- setLoadingSession(true);
693
- setActiveSessionId(mostRecentId);
694
- try {
695
- const sres = await fetch(`/api/chat/sessions/${mostRecentId}`, {
696
- cache: "no-store"
697
- });
698
- if (cancelled) return;
699
- if (!sres.ok) {
700
- setAnswers([]);
701
- return;
702
- }
703
- const sdata = await sres.json();
704
- if (cancelled) return;
705
- setAnswers(messagesToAnswers(sdata.messages ?? []));
706
- } catch {
707
- if (!cancelled) setAnswers([]);
708
- } finally {
709
- if (!cancelled) setLoadingSession(false);
710
- }
711
- } else if (!autoOpenedRef.current) {
712
- autoOpenedRef.current = true;
713
- }
692
+ setSessions(data.sessions ?? []);
714
693
  } catch {
715
694
  }
716
695
  }
@@ -719,6 +698,35 @@ function AiChat({
719
698
  cancelled = true;
720
699
  };
721
700
  }, []);
701
+ useEffect(() => {
702
+ if (initialSessionId == null) {
703
+ setAnswers([]);
704
+ return;
705
+ }
706
+ let cancelled = false;
707
+ async function load(id) {
708
+ setLoadingSession(true);
709
+ try {
710
+ const res = await fetch(`/api/chat/sessions/${id}`, { cache: "no-store" });
711
+ if (cancelled) return;
712
+ if (!res.ok) {
713
+ setAnswers([]);
714
+ return;
715
+ }
716
+ const data = await res.json();
717
+ if (cancelled) return;
718
+ setAnswers(messagesToAnswers(data.messages ?? []));
719
+ } catch {
720
+ if (!cancelled) setAnswers([]);
721
+ } finally {
722
+ if (!cancelled) setLoadingSession(false);
723
+ }
724
+ }
725
+ void load(initialSessionId);
726
+ return () => {
727
+ cancelled = true;
728
+ };
729
+ }, [initialSessionId]);
722
730
  useEffect(() => {
723
731
  if (answers.length > prevAnswersLen.current && lastAnswerRef.current) {
724
732
  lastAnswerRef.current.scrollIntoView({ behavior: "smooth", block: "start" });
@@ -734,11 +742,18 @@ function AiChat({
734
742
  } catch {
735
743
  }
736
744
  }, []);
745
+ const syncUrl = useCallback(
746
+ (id) => {
747
+ router.push(id == null ? "/chat" : `/chat/${id}`);
748
+ },
749
+ [router]
750
+ );
737
751
  const newChat = useCallback(() => {
738
752
  setActiveSessionId(null);
739
753
  setAnswers([]);
740
754
  setQuestion("");
741
- }, []);
755
+ router.push("/chat?new");
756
+ }, [router]);
742
757
  const changeProvider = useCallback(
743
758
  async (next) => {
744
759
  if (next === provider || providerSaving) return;
@@ -761,23 +776,27 @@ function AiChat({
761
776
  },
762
777
  [provider, providerSaving]
763
778
  );
764
- const openSession = useCallback(async (id) => {
765
- setLoadingSession(true);
766
- setActiveSessionId(id);
767
- try {
768
- const res = await fetch(`/api/chat/sessions/${id}`, { cache: "no-store" });
769
- if (!res.ok) {
779
+ useCallback(
780
+ async (id) => {
781
+ setLoadingSession(true);
782
+ setActiveSessionId(id);
783
+ syncUrl(id);
784
+ try {
785
+ const res = await fetch(`/api/chat/sessions/${id}`, { cache: "no-store" });
786
+ if (!res.ok) {
787
+ setAnswers([]);
788
+ return;
789
+ }
790
+ const data = await res.json();
791
+ setAnswers(messagesToAnswers(data.messages ?? []));
792
+ } catch {
770
793
  setAnswers([]);
771
- return;
794
+ } finally {
795
+ setLoadingSession(false);
772
796
  }
773
- const data = await res.json();
774
- setAnswers(messagesToAnswers(data.messages ?? []));
775
- } catch {
776
- setAnswers([]);
777
- } finally {
778
- setLoadingSession(false);
779
- }
780
- }, []);
797
+ },
798
+ [syncUrl]
799
+ );
781
800
  const persistTitle = useCallback(
782
801
  async (id, title) => {
783
802
  const trimmed = title.trim();
@@ -807,9 +826,10 @@ function AiChat({
807
826
  if (activeSessionId === id) {
808
827
  setActiveSessionId(null);
809
828
  setAnswers([]);
829
+ syncUrl(null);
810
830
  }
811
831
  },
812
- [activeSessionId]
832
+ [activeSessionId, syncUrl]
813
833
  );
814
834
  const submit = useCallback(
815
835
  async (q) => {
@@ -829,6 +849,7 @@ function AiChat({
829
849
  const data = await create.json();
830
850
  sessionId = data.session.id;
831
851
  setActiveSessionId(sessionId);
852
+ syncUrl(sessionId);
832
853
  setSessions((prev) => [
833
854
  { id: data.session.id, title: data.session.title, updatedAt: null },
834
855
  ...prev
@@ -909,7 +930,7 @@ function AiChat({
909
930
  void refreshSessions();
910
931
  }
911
932
  },
912
- [activeSessionId, pending, refreshSessions]
933
+ [activeSessionId, pending, refreshSessions, syncUrl]
913
934
  );
914
935
  const heroVisible = answers.length === 0 && !loadingSession;
915
936
  const greeting = useMemo(
@@ -983,13 +1004,10 @@ function AiChat({
983
1004
  }
984
1005
  return /* @__PURE__ */ jsxs("li", { className: "group relative", children: [
985
1006
  /* @__PURE__ */ jsx(
986
- "button",
1007
+ Link,
987
1008
  {
988
- type: "button",
989
- onClick: () => {
990
- void openSession(s.id);
991
- setSidebarOpen(false);
992
- },
1009
+ href: `/chat/${s.id}`,
1010
+ onClick: () => setSidebarOpen(false),
993
1011
  className: cn(
994
1012
  "flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm",
995
1013
  active ? "bg-sidebar-accent text-sidebar-accent-foreground" : "text-sidebar-foreground/80 hover:bg-sidebar-accent/60 hover:text-sidebar-foreground"
@@ -1470,10 +1488,14 @@ function asDataPart(v) {
1470
1488
  function VercelChat({
1471
1489
  userFirstName,
1472
1490
  scopeLabel,
1473
- initialProvider
1491
+ initialProvider,
1492
+ initialSessionId = null
1474
1493
  }) {
1494
+ const router = useRouter();
1475
1495
  const [sessions, setSessions] = useState([]);
1476
- const [activeSessionId, setActiveSessionId] = useState(null);
1496
+ const [activeSessionId, setActiveSessionId] = useState(
1497
+ initialSessionId
1498
+ );
1477
1499
  const [sidebarOpen, setSidebarOpen] = useState(false);
1478
1500
  const [loadingSession, setLoadingSession] = useState(false);
1479
1501
  const [provider, setProvider] = useState(initialProvider);
@@ -1487,7 +1509,6 @@ function VercelChat({
1487
1509
  const textareaRef = useRef(null);
1488
1510
  const lastAnswerRef = useRef(null);
1489
1511
  const prevAnswersLen = useRef(0);
1490
- const autoOpenedRef = useRef(false);
1491
1512
  const activeSessionIdRef = useRef(activeSessionId);
1492
1513
  const providerRef = useRef(provider);
1493
1514
  useEffect(() => {
@@ -1543,48 +1564,7 @@ function VercelChat({
1543
1564
  if (!res.ok) return;
1544
1565
  const json = await res.json();
1545
1566
  if (cancelled) return;
1546
- const list = json.sessions ?? [];
1547
- setSessions(list);
1548
- if (!autoOpenedRef.current && list.length > 0) {
1549
- autoOpenedRef.current = true;
1550
- const mostRecentId = list[0].id;
1551
- setLoadingSession(true);
1552
- setActiveSessionId(mostRecentId);
1553
- try {
1554
- const sres = await fetch(`/api/chat/sessions/${mostRecentId}`, {
1555
- cache: "no-store"
1556
- });
1557
- if (cancelled) return;
1558
- if (!sres.ok) {
1559
- setMessages([]);
1560
- setHydratedBlocks({});
1561
- setHydratedProse({});
1562
- setHydratedErrors({});
1563
- return;
1564
- }
1565
- const sjson = await sres.json();
1566
- if (cancelled) return;
1567
- const { uiMessages, blocksMap, proseMap, errorsMap } = storedToUseChat(
1568
- sjson.messages ?? []
1569
- );
1570
- setMessages(uiMessages);
1571
- setHydratedBlocks(blocksMap);
1572
- setHydratedProse(proseMap);
1573
- setHydratedErrors(errorsMap);
1574
- setStartedAt({});
1575
- } catch {
1576
- if (!cancelled) {
1577
- setMessages([]);
1578
- setHydratedBlocks({});
1579
- setHydratedProse({});
1580
- setHydratedErrors({});
1581
- }
1582
- } finally {
1583
- if (!cancelled) setLoadingSession(false);
1584
- }
1585
- } else if (!autoOpenedRef.current) {
1586
- autoOpenedRef.current = true;
1587
- }
1567
+ setSessions(json.sessions ?? []);
1588
1568
  } catch {
1589
1569
  }
1590
1570
  }
@@ -1592,7 +1572,54 @@ function VercelChat({
1592
1572
  return () => {
1593
1573
  cancelled = true;
1594
1574
  };
1595
- }, [setMessages]);
1575
+ }, []);
1576
+ useEffect(() => {
1577
+ if (initialSessionId == null) {
1578
+ setMessages([]);
1579
+ setHydratedBlocks({});
1580
+ setHydratedProse({});
1581
+ setHydratedErrors({});
1582
+ return;
1583
+ }
1584
+ let cancelled = false;
1585
+ async function load(id) {
1586
+ setLoadingSession(true);
1587
+ try {
1588
+ const res = await fetch(`/api/chat/sessions/${id}`, { cache: "no-store" });
1589
+ if (cancelled) return;
1590
+ if (!res.ok) {
1591
+ setMessages([]);
1592
+ setHydratedBlocks({});
1593
+ setHydratedProse({});
1594
+ setHydratedErrors({});
1595
+ return;
1596
+ }
1597
+ const json = await res.json();
1598
+ if (cancelled) return;
1599
+ const { uiMessages, blocksMap, proseMap, errorsMap } = storedToUseChat(
1600
+ json.messages ?? []
1601
+ );
1602
+ setMessages(uiMessages);
1603
+ setHydratedBlocks(blocksMap);
1604
+ setHydratedProse(proseMap);
1605
+ setHydratedErrors(errorsMap);
1606
+ setStartedAt({});
1607
+ } catch {
1608
+ if (!cancelled) {
1609
+ setMessages([]);
1610
+ setHydratedBlocks({});
1611
+ setHydratedProse({});
1612
+ setHydratedErrors({});
1613
+ }
1614
+ } finally {
1615
+ if (!cancelled) setLoadingSession(false);
1616
+ }
1617
+ }
1618
+ void load(initialSessionId);
1619
+ return () => {
1620
+ cancelled = true;
1621
+ };
1622
+ }, [initialSessionId, setMessages]);
1596
1623
  const answers = useMemo(() => {
1597
1624
  const liveBlocks = [];
1598
1625
  const liveErrors = [];
@@ -1691,6 +1718,12 @@ function VercelChat({
1691
1718
  }
1692
1719
  prevAnswersLen.current = answers.length;
1693
1720
  }, [answers.length]);
1721
+ const syncUrl = useCallback(
1722
+ (id) => {
1723
+ router.push(id == null ? "/chat" : `/chat/${id}`);
1724
+ },
1725
+ [router]
1726
+ );
1694
1727
  const newChat = useCallback(() => {
1695
1728
  setActiveSessionId(null);
1696
1729
  setMessages([]);
@@ -1699,7 +1732,8 @@ function VercelChat({
1699
1732
  setHydratedErrors({});
1700
1733
  setStartedAt({});
1701
1734
  setInput("");
1702
- }, [setMessages, setInput]);
1735
+ router.push("/chat?new");
1736
+ }, [setMessages, setInput, router]);
1703
1737
  const changeProvider = useCallback(
1704
1738
  async (next) => {
1705
1739
  if (next === provider || providerSaving) return;
@@ -1722,10 +1756,11 @@ function VercelChat({
1722
1756
  },
1723
1757
  [provider, providerSaving]
1724
1758
  );
1725
- const openSession = useCallback(
1759
+ useCallback(
1726
1760
  async (id) => {
1727
1761
  setLoadingSession(true);
1728
1762
  setActiveSessionId(id);
1763
+ syncUrl(id);
1729
1764
  try {
1730
1765
  const res = await fetch(`/api/chat/sessions/${id}`, { cache: "no-store" });
1731
1766
  if (!res.ok) {
@@ -1753,7 +1788,7 @@ function VercelChat({
1753
1788
  setLoadingSession(false);
1754
1789
  }
1755
1790
  },
1756
- [setMessages]
1791
+ [setMessages, syncUrl]
1757
1792
  );
1758
1793
  const persistTitle = useCallback(
1759
1794
  async (id, title) => {
@@ -1787,9 +1822,10 @@ function VercelChat({
1787
1822
  setHydratedBlocks({});
1788
1823
  setHydratedProse({});
1789
1824
  setHydratedErrors({});
1825
+ syncUrl(null);
1790
1826
  }
1791
1827
  },
1792
- [activeSessionId, setMessages]
1828
+ [activeSessionId, setMessages, syncUrl]
1793
1829
  );
1794
1830
  const submitForm = useCallback(
1795
1831
  async (e) => {
@@ -1806,6 +1842,7 @@ function VercelChat({
1806
1842
  const json = await create.json();
1807
1843
  activeSessionIdRef.current = json.session.id;
1808
1844
  setActiveSessionId(json.session.id);
1845
+ syncUrl(json.session.id);
1809
1846
  setSessions((prev) => [
1810
1847
  { id: json.session.id, title: json.session.title, updatedAt: null },
1811
1848
  ...prev
@@ -1816,7 +1853,7 @@ function VercelChat({
1816
1853
  }
1817
1854
  handleSubmit(e);
1818
1855
  },
1819
- [input, status, handleSubmit]
1856
+ [input, status, handleSubmit, syncUrl]
1820
1857
  );
1821
1858
  useEffect(() => {
1822
1859
  setStartedAt((prev) => {
@@ -1924,13 +1961,10 @@ function VercelChat({
1924
1961
  }
1925
1962
  return /* @__PURE__ */ jsxs("li", { className: "group relative", children: [
1926
1963
  /* @__PURE__ */ jsx(
1927
- "button",
1964
+ Link,
1928
1965
  {
1929
- type: "button",
1930
- onClick: () => {
1931
- void openSession(s.id);
1932
- setSidebarOpen(false);
1933
- },
1966
+ href: `/chat/${s.id}`,
1967
+ onClick: () => setSidebarOpen(false),
1934
1968
  className: cn(
1935
1969
  "flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm",
1936
1970
  active ? "bg-sidebar-accent text-sidebar-accent-foreground" : "text-sidebar-foreground/80 hover:bg-sidebar-accent/60 hover:text-sidebar-foreground"