@firstlovecenter/ai-chat 0.6.1 → 0.8.0

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,7 @@
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';
4
5
  import { PanelLeftClose, Plus, Pencil, Trash2, Menu, Sparkles, Loader2, ChevronDown, Check, ArrowUp, Copy, RotateCcw, ChevronUp } from 'lucide-react';
5
6
  import { DropdownMenu as DropdownMenu$1 } from 'radix-ui';
6
7
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
@@ -651,10 +652,14 @@ var PROVIDER_DESCRIPTIONS = {
651
652
  function AiChat({
652
653
  userFirstName,
653
654
  scopeLabel,
654
- initialProvider
655
+ initialProvider,
656
+ initialSessionId = null
655
657
  }) {
658
+ const router = useRouter();
656
659
  const [sessions, setSessions] = useState([]);
657
- const [activeSessionId, setActiveSessionId] = useState(null);
660
+ const [activeSessionId, setActiveSessionId] = useState(
661
+ initialSessionId
662
+ );
658
663
  const [answers, setAnswers] = useState([]);
659
664
  const [pending, setPending] = useState(false);
660
665
  const [question, setQuestion] = useState("");
@@ -668,7 +673,6 @@ function AiChat({
668
673
  const textareaRef = useRef(null);
669
674
  const lastAnswerRef = useRef(null);
670
675
  const prevAnswersLen = useRef(0);
671
- const autoOpenedRef = useRef(false);
672
676
  useLayoutEffect(() => {
673
677
  const el = textareaRef.current;
674
678
  if (!el) return;
@@ -684,33 +688,7 @@ function AiChat({
684
688
  if (!res.ok) return;
685
689
  const data = await res.json();
686
690
  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
- }
691
+ setSessions(data.sessions ?? []);
714
692
  } catch {
715
693
  }
716
694
  }
@@ -719,6 +697,35 @@ function AiChat({
719
697
  cancelled = true;
720
698
  };
721
699
  }, []);
700
+ useEffect(() => {
701
+ if (initialSessionId == null) {
702
+ setAnswers([]);
703
+ return;
704
+ }
705
+ let cancelled = false;
706
+ async function load(id) {
707
+ setLoadingSession(true);
708
+ try {
709
+ const res = await fetch(`/api/chat/sessions/${id}`, { cache: "no-store" });
710
+ if (cancelled) return;
711
+ if (!res.ok) {
712
+ setAnswers([]);
713
+ return;
714
+ }
715
+ const data = await res.json();
716
+ if (cancelled) return;
717
+ setAnswers(messagesToAnswers(data.messages ?? []));
718
+ } catch {
719
+ if (!cancelled) setAnswers([]);
720
+ } finally {
721
+ if (!cancelled) setLoadingSession(false);
722
+ }
723
+ }
724
+ void load(initialSessionId);
725
+ return () => {
726
+ cancelled = true;
727
+ };
728
+ }, [initialSessionId]);
722
729
  useEffect(() => {
723
730
  if (answers.length > prevAnswersLen.current && lastAnswerRef.current) {
724
731
  lastAnswerRef.current.scrollIntoView({ behavior: "smooth", block: "start" });
@@ -734,11 +741,18 @@ function AiChat({
734
741
  } catch {
735
742
  }
736
743
  }, []);
744
+ const syncUrl = useCallback(
745
+ (id) => {
746
+ router.push(id == null ? "/chat" : `/chat/${id}`);
747
+ },
748
+ [router]
749
+ );
737
750
  const newChat = useCallback(() => {
738
751
  setActiveSessionId(null);
739
752
  setAnswers([]);
740
753
  setQuestion("");
741
- }, []);
754
+ syncUrl(null);
755
+ }, [syncUrl]);
742
756
  const changeProvider = useCallback(
743
757
  async (next) => {
744
758
  if (next === provider || providerSaving) return;
@@ -761,23 +775,27 @@ function AiChat({
761
775
  },
762
776
  [provider, providerSaving]
763
777
  );
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) {
778
+ const openSession = useCallback(
779
+ async (id) => {
780
+ setLoadingSession(true);
781
+ setActiveSessionId(id);
782
+ syncUrl(id);
783
+ try {
784
+ const res = await fetch(`/api/chat/sessions/${id}`, { cache: "no-store" });
785
+ if (!res.ok) {
786
+ setAnswers([]);
787
+ return;
788
+ }
789
+ const data = await res.json();
790
+ setAnswers(messagesToAnswers(data.messages ?? []));
791
+ } catch {
770
792
  setAnswers([]);
771
- return;
793
+ } finally {
794
+ setLoadingSession(false);
772
795
  }
773
- const data = await res.json();
774
- setAnswers(messagesToAnswers(data.messages ?? []));
775
- } catch {
776
- setAnswers([]);
777
- } finally {
778
- setLoadingSession(false);
779
- }
780
- }, []);
796
+ },
797
+ [syncUrl]
798
+ );
781
799
  const persistTitle = useCallback(
782
800
  async (id, title) => {
783
801
  const trimmed = title.trim();
@@ -807,9 +825,10 @@ function AiChat({
807
825
  if (activeSessionId === id) {
808
826
  setActiveSessionId(null);
809
827
  setAnswers([]);
828
+ syncUrl(null);
810
829
  }
811
830
  },
812
- [activeSessionId]
831
+ [activeSessionId, syncUrl]
813
832
  );
814
833
  const submit = useCallback(
815
834
  async (q) => {
@@ -829,6 +848,7 @@ function AiChat({
829
848
  const data = await create.json();
830
849
  sessionId = data.session.id;
831
850
  setActiveSessionId(sessionId);
851
+ syncUrl(sessionId);
832
852
  setSessions((prev) => [
833
853
  { id: data.session.id, title: data.session.title, updatedAt: null },
834
854
  ...prev
@@ -909,7 +929,7 @@ function AiChat({
909
929
  void refreshSessions();
910
930
  }
911
931
  },
912
- [activeSessionId, pending, refreshSessions]
932
+ [activeSessionId, pending, refreshSessions, syncUrl]
913
933
  );
914
934
  const heroVisible = answers.length === 0 && !loadingSession;
915
935
  const greeting = useMemo(
@@ -1470,10 +1490,14 @@ function asDataPart(v) {
1470
1490
  function VercelChat({
1471
1491
  userFirstName,
1472
1492
  scopeLabel,
1473
- initialProvider
1493
+ initialProvider,
1494
+ initialSessionId = null
1474
1495
  }) {
1496
+ const router = useRouter();
1475
1497
  const [sessions, setSessions] = useState([]);
1476
- const [activeSessionId, setActiveSessionId] = useState(null);
1498
+ const [activeSessionId, setActiveSessionId] = useState(
1499
+ initialSessionId
1500
+ );
1477
1501
  const [sidebarOpen, setSidebarOpen] = useState(false);
1478
1502
  const [loadingSession, setLoadingSession] = useState(false);
1479
1503
  const [provider, setProvider] = useState(initialProvider);
@@ -1487,7 +1511,6 @@ function VercelChat({
1487
1511
  const textareaRef = useRef(null);
1488
1512
  const lastAnswerRef = useRef(null);
1489
1513
  const prevAnswersLen = useRef(0);
1490
- const autoOpenedRef = useRef(false);
1491
1514
  const activeSessionIdRef = useRef(activeSessionId);
1492
1515
  const providerRef = useRef(provider);
1493
1516
  useEffect(() => {
@@ -1543,48 +1566,7 @@ function VercelChat({
1543
1566
  if (!res.ok) return;
1544
1567
  const json = await res.json();
1545
1568
  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
- }
1569
+ setSessions(json.sessions ?? []);
1588
1570
  } catch {
1589
1571
  }
1590
1572
  }
@@ -1592,7 +1574,54 @@ function VercelChat({
1592
1574
  return () => {
1593
1575
  cancelled = true;
1594
1576
  };
1595
- }, [setMessages]);
1577
+ }, []);
1578
+ useEffect(() => {
1579
+ if (initialSessionId == null) {
1580
+ setMessages([]);
1581
+ setHydratedBlocks({});
1582
+ setHydratedProse({});
1583
+ setHydratedErrors({});
1584
+ return;
1585
+ }
1586
+ let cancelled = false;
1587
+ async function load(id) {
1588
+ setLoadingSession(true);
1589
+ try {
1590
+ const res = await fetch(`/api/chat/sessions/${id}`, { cache: "no-store" });
1591
+ if (cancelled) return;
1592
+ if (!res.ok) {
1593
+ setMessages([]);
1594
+ setHydratedBlocks({});
1595
+ setHydratedProse({});
1596
+ setHydratedErrors({});
1597
+ return;
1598
+ }
1599
+ const json = await res.json();
1600
+ if (cancelled) return;
1601
+ const { uiMessages, blocksMap, proseMap, errorsMap } = storedToUseChat(
1602
+ json.messages ?? []
1603
+ );
1604
+ setMessages(uiMessages);
1605
+ setHydratedBlocks(blocksMap);
1606
+ setHydratedProse(proseMap);
1607
+ setHydratedErrors(errorsMap);
1608
+ setStartedAt({});
1609
+ } catch {
1610
+ if (!cancelled) {
1611
+ setMessages([]);
1612
+ setHydratedBlocks({});
1613
+ setHydratedProse({});
1614
+ setHydratedErrors({});
1615
+ }
1616
+ } finally {
1617
+ if (!cancelled) setLoadingSession(false);
1618
+ }
1619
+ }
1620
+ void load(initialSessionId);
1621
+ return () => {
1622
+ cancelled = true;
1623
+ };
1624
+ }, [initialSessionId, setMessages]);
1596
1625
  const answers = useMemo(() => {
1597
1626
  const liveBlocks = [];
1598
1627
  const liveErrors = [];
@@ -1691,6 +1720,12 @@ function VercelChat({
1691
1720
  }
1692
1721
  prevAnswersLen.current = answers.length;
1693
1722
  }, [answers.length]);
1723
+ const syncUrl = useCallback(
1724
+ (id) => {
1725
+ router.push(id == null ? "/chat" : `/chat/${id}`);
1726
+ },
1727
+ [router]
1728
+ );
1694
1729
  const newChat = useCallback(() => {
1695
1730
  setActiveSessionId(null);
1696
1731
  setMessages([]);
@@ -1699,7 +1734,8 @@ function VercelChat({
1699
1734
  setHydratedErrors({});
1700
1735
  setStartedAt({});
1701
1736
  setInput("");
1702
- }, [setMessages, setInput]);
1737
+ syncUrl(null);
1738
+ }, [setMessages, setInput, syncUrl]);
1703
1739
  const changeProvider = useCallback(
1704
1740
  async (next) => {
1705
1741
  if (next === provider || providerSaving) return;
@@ -1726,6 +1762,7 @@ function VercelChat({
1726
1762
  async (id) => {
1727
1763
  setLoadingSession(true);
1728
1764
  setActiveSessionId(id);
1765
+ syncUrl(id);
1729
1766
  try {
1730
1767
  const res = await fetch(`/api/chat/sessions/${id}`, { cache: "no-store" });
1731
1768
  if (!res.ok) {
@@ -1753,7 +1790,7 @@ function VercelChat({
1753
1790
  setLoadingSession(false);
1754
1791
  }
1755
1792
  },
1756
- [setMessages]
1793
+ [setMessages, syncUrl]
1757
1794
  );
1758
1795
  const persistTitle = useCallback(
1759
1796
  async (id, title) => {
@@ -1787,9 +1824,10 @@ function VercelChat({
1787
1824
  setHydratedBlocks({});
1788
1825
  setHydratedProse({});
1789
1826
  setHydratedErrors({});
1827
+ syncUrl(null);
1790
1828
  }
1791
1829
  },
1792
- [activeSessionId, setMessages]
1830
+ [activeSessionId, setMessages, syncUrl]
1793
1831
  );
1794
1832
  const submitForm = useCallback(
1795
1833
  async (e) => {
@@ -1806,6 +1844,7 @@ function VercelChat({
1806
1844
  const json = await create.json();
1807
1845
  activeSessionIdRef.current = json.session.id;
1808
1846
  setActiveSessionId(json.session.id);
1847
+ syncUrl(json.session.id);
1809
1848
  setSessions((prev) => [
1810
1849
  { id: json.session.id, title: json.session.title, updatedAt: null },
1811
1850
  ...prev
@@ -1816,7 +1855,7 @@ function VercelChat({
1816
1855
  }
1817
1856
  handleSubmit(e);
1818
1857
  },
1819
- [input, status, handleSubmit]
1858
+ [input, status, handleSubmit, syncUrl]
1820
1859
  );
1821
1860
  useEffect(() => {
1822
1861
  setStartedAt((prev) => {