@firstlovecenter/ai-chat 0.2.3 → 0.6.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.
package/dist/ui/index.js CHANGED
@@ -6,6 +6,7 @@ import { DropdownMenu as DropdownMenu$1 } from 'radix-ui';
6
6
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
7
7
  import * as RechartsPrimitive from 'recharts';
8
8
  import { PieChart, Pie, Cell, Label, BarChart, CartesianGrid, XAxis, YAxis, Bar, LineChart, Line } from 'recharts';
9
+ import { useChat } from '@ai-sdk/react';
9
10
 
10
11
  // src/ui/_shared/cn.ts
11
12
  function cn(...args) {
@@ -78,6 +79,92 @@ function DropdownMenuItem({
78
79
  }
79
80
  );
80
81
  }
82
+ function SidebarTitleEditor({
83
+ initial,
84
+ onSave,
85
+ onCancel
86
+ }) {
87
+ const [draft, setDraft] = useState(initial);
88
+ const commit = () => {
89
+ const trimmed = draft.trim();
90
+ if (trimmed && trimmed !== initial) onSave(trimmed);
91
+ else onCancel();
92
+ };
93
+ return /* @__PURE__ */ jsx(
94
+ "input",
95
+ {
96
+ autoFocus: true,
97
+ value: draft,
98
+ onChange: (e) => setDraft(e.target.value),
99
+ onFocus: (e) => e.currentTarget.select(),
100
+ onClick: (e) => e.stopPropagation(),
101
+ onBlur: commit,
102
+ onKeyDown: (e) => {
103
+ if (e.key === "Enter") {
104
+ e.preventDefault();
105
+ commit();
106
+ } else if (e.key === "Escape") {
107
+ e.preventDefault();
108
+ onCancel();
109
+ }
110
+ },
111
+ maxLength: 200,
112
+ className: "w-full rounded-md border border-sidebar-border bg-sidebar-accent/40 px-2 py-1.5 text-sm text-sidebar-foreground focus:outline-none focus:ring-1 focus:ring-ring"
113
+ }
114
+ );
115
+ }
116
+ function EditableTitle({
117
+ title,
118
+ onSave
119
+ }) {
120
+ const [editing, setEditing] = useState(false);
121
+ const [draft, setDraft] = useState(title);
122
+ useEffect(() => {
123
+ if (!editing) setDraft(title);
124
+ }, [title, editing]);
125
+ if (!editing) {
126
+ return /* @__PURE__ */ jsx(
127
+ "button",
128
+ {
129
+ type: "button",
130
+ onClick: () => {
131
+ setDraft(title);
132
+ setEditing(true);
133
+ },
134
+ className: "w-64 max-w-full min-w-0 truncate rounded px-2 py-0.5 text-left hover:bg-accent hover:text-foreground",
135
+ title: "Click to rename",
136
+ children: title
137
+ }
138
+ );
139
+ }
140
+ const commit = () => {
141
+ const trimmed = draft.trim();
142
+ setEditing(false);
143
+ if (trimmed && trimmed !== title) onSave(trimmed);
144
+ };
145
+ return /* @__PURE__ */ jsx(
146
+ "input",
147
+ {
148
+ autoFocus: true,
149
+ value: draft,
150
+ onChange: (e) => setDraft(e.target.value),
151
+ onFocus: (e) => e.currentTarget.select(),
152
+ onBlur: commit,
153
+ onKeyDown: (e) => {
154
+ if (e.key === "Enter") {
155
+ e.preventDefault();
156
+ commit();
157
+ } else if (e.key === "Escape") {
158
+ e.preventDefault();
159
+ setEditing(false);
160
+ setDraft(title);
161
+ }
162
+ },
163
+ className: "w-64 max-w-full min-w-0 rounded border border-border bg-background px-2 py-0.5 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-ring",
164
+ maxLength: 200
165
+ }
166
+ );
167
+ }
81
168
  function ChartCard({
82
169
  title,
83
170
  subtitle,
@@ -576,6 +663,7 @@ function AiChat({
576
663
  const textareaRef = useRef(null);
577
664
  const lastAnswerRef = useRef(null);
578
665
  const prevAnswersLen = useRef(0);
666
+ const autoOpenedRef = useRef(false);
579
667
  useLayoutEffect(() => {
580
668
  const el = textareaRef.current;
581
669
  if (!el) return;
@@ -590,7 +678,34 @@ function AiChat({
590
678
  const res = await fetch("/api/chat/sessions", { cache: "no-store" });
591
679
  if (!res.ok) return;
592
680
  const data = await res.json();
593
- if (!cancelled) setSessions(data.sessions ?? []);
681
+ if (cancelled) return;
682
+ const list = data.sessions ?? [];
683
+ setSessions(list);
684
+ if (!autoOpenedRef.current && list.length > 0) {
685
+ autoOpenedRef.current = true;
686
+ const mostRecentId = list[0].id;
687
+ setLoadingSession(true);
688
+ setActiveSessionId(mostRecentId);
689
+ try {
690
+ const sres = await fetch(`/api/chat/sessions/${mostRecentId}`, {
691
+ cache: "no-store"
692
+ });
693
+ if (cancelled) return;
694
+ if (!sres.ok) {
695
+ setAnswers([]);
696
+ return;
697
+ }
698
+ const sdata = await sres.json();
699
+ if (cancelled) return;
700
+ setAnswers(messagesToAnswers(sdata.messages ?? []));
701
+ } catch {
702
+ if (!cancelled) setAnswers([]);
703
+ } finally {
704
+ if (!cancelled) setLoadingSession(false);
705
+ }
706
+ } else if (!autoOpenedRef.current) {
707
+ autoOpenedRef.current = true;
708
+ }
594
709
  } catch {
595
710
  }
596
711
  }
@@ -1158,92 +1273,6 @@ function AnswerView({
1158
1273
  }
1159
1274
  );
1160
1275
  }
1161
- function SidebarTitleEditor({
1162
- initial,
1163
- onSave,
1164
- onCancel
1165
- }) {
1166
- const [draft, setDraft] = useState(initial);
1167
- const commit = () => {
1168
- const trimmed = draft.trim();
1169
- if (trimmed && trimmed !== initial) onSave(trimmed);
1170
- else onCancel();
1171
- };
1172
- return /* @__PURE__ */ jsx(
1173
- "input",
1174
- {
1175
- autoFocus: true,
1176
- value: draft,
1177
- onChange: (e) => setDraft(e.target.value),
1178
- onFocus: (e) => e.currentTarget.select(),
1179
- onClick: (e) => e.stopPropagation(),
1180
- onBlur: commit,
1181
- onKeyDown: (e) => {
1182
- if (e.key === "Enter") {
1183
- e.preventDefault();
1184
- commit();
1185
- } else if (e.key === "Escape") {
1186
- e.preventDefault();
1187
- onCancel();
1188
- }
1189
- },
1190
- maxLength: 200,
1191
- className: "w-full rounded-md border border-sidebar-border bg-sidebar-accent/40 px-2 py-1.5 text-sm text-sidebar-foreground focus:outline-none focus:ring-1 focus:ring-ring"
1192
- }
1193
- );
1194
- }
1195
- function EditableTitle({
1196
- title,
1197
- onSave
1198
- }) {
1199
- const [editing, setEditing] = useState(false);
1200
- const [draft, setDraft] = useState(title);
1201
- useEffect(() => {
1202
- if (!editing) setDraft(title);
1203
- }, [title, editing]);
1204
- if (!editing) {
1205
- return /* @__PURE__ */ jsx(
1206
- "button",
1207
- {
1208
- type: "button",
1209
- onClick: () => {
1210
- setDraft(title);
1211
- setEditing(true);
1212
- },
1213
- className: "w-64 max-w-full min-w-0 truncate rounded px-2 py-0.5 text-left hover:bg-accent hover:text-foreground",
1214
- title: "Click to rename",
1215
- children: title
1216
- }
1217
- );
1218
- }
1219
- const commit = () => {
1220
- const trimmed = draft.trim();
1221
- setEditing(false);
1222
- if (trimmed && trimmed !== title) onSave(trimmed);
1223
- };
1224
- return /* @__PURE__ */ jsx(
1225
- "input",
1226
- {
1227
- autoFocus: true,
1228
- value: draft,
1229
- onChange: (e) => setDraft(e.target.value),
1230
- onFocus: (e) => e.currentTarget.select(),
1231
- onBlur: commit,
1232
- onKeyDown: (e) => {
1233
- if (e.key === "Enter") {
1234
- e.preventDefault();
1235
- commit();
1236
- } else if (e.key === "Escape") {
1237
- e.preventDefault();
1238
- setEditing(false);
1239
- setDraft(title);
1240
- }
1241
- },
1242
- className: "w-64 max-w-full min-w-0 rounded border border-border bg-background px-2 py-0.5 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-ring",
1243
- maxLength: 200
1244
- }
1245
- );
1246
- }
1247
1276
  function blocksToPlainText(blocks) {
1248
1277
  return blocks.map((b) => {
1249
1278
  if (b.kind === "paragraph_brief") {
@@ -1408,7 +1437,912 @@ function messagesToAnswers(messages) {
1408
1437
  }
1409
1438
  return out;
1410
1439
  }
1440
+ function formatDuration2(ms) {
1441
+ if (ms < 0) ms = 0;
1442
+ const totalSec = Math.round(ms / 1e3);
1443
+ if (totalSec < 60) return `${totalSec}s`;
1444
+ const m = Math.floor(totalSec / 60);
1445
+ const s = totalSec % 60;
1446
+ return s === 0 ? `${m}m` : `${m}m ${s}s`;
1447
+ }
1448
+ var PROVIDER_LABELS2 = {
1449
+ claude: "Claude",
1450
+ grok: "Grok",
1451
+ gemini: "Gemini"
1452
+ };
1453
+ var TEXTAREA_MAX_PX2 = 176;
1454
+ var PROVIDER_DESCRIPTIONS2 = {
1455
+ claude: "Anthropic Claude on Vertex AI",
1456
+ grok: "xAI Grok on Vertex AI",
1457
+ gemini: "Google Gemini on Vertex AI"
1458
+ };
1459
+ function asDataPart(v) {
1460
+ if (typeof v !== "object" || v === null || Array.isArray(v)) return null;
1461
+ const t = v.type;
1462
+ if (typeof t !== "string") return null;
1463
+ return v;
1464
+ }
1465
+ function VercelChat({
1466
+ userFirstName,
1467
+ scopeLabel,
1468
+ initialProvider
1469
+ }) {
1470
+ const [sessions, setSessions] = useState([]);
1471
+ const [activeSessionId, setActiveSessionId] = useState(null);
1472
+ const [sidebarOpen, setSidebarOpen] = useState(false);
1473
+ const [loadingSession, setLoadingSession] = useState(false);
1474
+ const [provider, setProvider] = useState(initialProvider);
1475
+ const [providerSaving, setProviderSaving] = useState(false);
1476
+ const [editingSidebarId, setEditingSidebarId] = useState(null);
1477
+ const [hydratedBlocks, setHydratedBlocks] = useState({});
1478
+ const [hydratedProse, setHydratedProse] = useState({});
1479
+ const [hydratedErrors, setHydratedErrors] = useState({});
1480
+ const [startedAt, setStartedAt] = useState({});
1481
+ const threadRef = useRef(null);
1482
+ const textareaRef = useRef(null);
1483
+ const lastAnswerRef = useRef(null);
1484
+ const prevAnswersLen = useRef(0);
1485
+ const autoOpenedRef = useRef(false);
1486
+ const activeSessionIdRef = useRef(activeSessionId);
1487
+ const providerRef = useRef(provider);
1488
+ useEffect(() => {
1489
+ activeSessionIdRef.current = activeSessionId;
1490
+ }, [activeSessionId]);
1491
+ useEffect(() => {
1492
+ providerRef.current = provider;
1493
+ }, [provider]);
1494
+ const refreshSessions = useCallback(async () => {
1495
+ try {
1496
+ const res = await fetch("/api/chat/sessions", { cache: "no-store" });
1497
+ if (!res.ok) return;
1498
+ const data2 = await res.json();
1499
+ setSessions(data2.sessions ?? []);
1500
+ } catch {
1501
+ }
1502
+ }, []);
1503
+ const {
1504
+ messages,
1505
+ input,
1506
+ handleInputChange,
1507
+ handleSubmit,
1508
+ status,
1509
+ setMessages,
1510
+ setInput,
1511
+ data
1512
+ } = useChat({
1513
+ api: "/api/agent/vercel",
1514
+ // `body` here is captured at hook init and stale-closes over
1515
+ // `activeSessionId`/`provider`. Real per-submit body comes through
1516
+ // `experimental_prepareRequestBody` below, which reads from refs.
1517
+ experimental_prepareRequestBody: ({ messages: msgs }) => ({
1518
+ messages: msgs,
1519
+ chatSessionId: activeSessionIdRef.current,
1520
+ model: providerRef.current
1521
+ }),
1522
+ onFinish: () => {
1523
+ void refreshSessions();
1524
+ }
1525
+ });
1526
+ useLayoutEffect(() => {
1527
+ const el = textareaRef.current;
1528
+ if (!el) return;
1529
+ el.style.height = "0px";
1530
+ const next = Math.min(el.scrollHeight, TEXTAREA_MAX_PX2);
1531
+ el.style.height = `${next}px`;
1532
+ }, [input]);
1533
+ useEffect(() => {
1534
+ let cancelled = false;
1535
+ async function load() {
1536
+ try {
1537
+ const res = await fetch("/api/chat/sessions", { cache: "no-store" });
1538
+ if (!res.ok) return;
1539
+ const json = await res.json();
1540
+ if (cancelled) return;
1541
+ const list = json.sessions ?? [];
1542
+ setSessions(list);
1543
+ if (!autoOpenedRef.current && list.length > 0) {
1544
+ autoOpenedRef.current = true;
1545
+ const mostRecentId = list[0].id;
1546
+ setLoadingSession(true);
1547
+ setActiveSessionId(mostRecentId);
1548
+ try {
1549
+ const sres = await fetch(`/api/chat/sessions/${mostRecentId}`, {
1550
+ cache: "no-store"
1551
+ });
1552
+ if (cancelled) return;
1553
+ if (!sres.ok) {
1554
+ setMessages([]);
1555
+ setHydratedBlocks({});
1556
+ setHydratedProse({});
1557
+ setHydratedErrors({});
1558
+ return;
1559
+ }
1560
+ const sjson = await sres.json();
1561
+ if (cancelled) return;
1562
+ const { uiMessages, blocksMap, proseMap, errorsMap } = storedToUseChat(
1563
+ sjson.messages ?? []
1564
+ );
1565
+ setMessages(uiMessages);
1566
+ setHydratedBlocks(blocksMap);
1567
+ setHydratedProse(proseMap);
1568
+ setHydratedErrors(errorsMap);
1569
+ setStartedAt({});
1570
+ } catch {
1571
+ if (!cancelled) {
1572
+ setMessages([]);
1573
+ setHydratedBlocks({});
1574
+ setHydratedProse({});
1575
+ setHydratedErrors({});
1576
+ }
1577
+ } finally {
1578
+ if (!cancelled) setLoadingSession(false);
1579
+ }
1580
+ } else if (!autoOpenedRef.current) {
1581
+ autoOpenedRef.current = true;
1582
+ }
1583
+ } catch {
1584
+ }
1585
+ }
1586
+ void load();
1587
+ return () => {
1588
+ cancelled = true;
1589
+ };
1590
+ }, [setMessages]);
1591
+ const answers = useMemo(() => {
1592
+ const liveBlocks = [];
1593
+ const liveErrors = [];
1594
+ if (Array.isArray(data)) {
1595
+ for (const raw of data) {
1596
+ const part = asDataPart(raw);
1597
+ if (!part) continue;
1598
+ if (part.type === "block") liveBlocks.push(part.value);
1599
+ else if (part.type === "error") liveErrors.push(part.value);
1600
+ }
1601
+ }
1602
+ const out = [];
1603
+ let pendingQuestion = null;
1604
+ let liveAssistantSeen = 0;
1605
+ const liveTurns = [];
1606
+ {
1607
+ let current = [];
1608
+ let lastIdx = -1;
1609
+ for (const b of liveBlocks) {
1610
+ if (b.index <= lastIdx && current.length > 0) {
1611
+ liveTurns.push(current);
1612
+ current = [];
1613
+ }
1614
+ current.push(b);
1615
+ lastIdx = b.index;
1616
+ }
1617
+ if (current.length > 0) liveTurns.push(current);
1618
+ }
1619
+ for (const m of messages) {
1620
+ if (m.role === "user") {
1621
+ pendingQuestion = m.content;
1622
+ continue;
1623
+ }
1624
+ if (m.role !== "assistant") continue;
1625
+ const question = pendingQuestion ?? "";
1626
+ pendingQuestion = null;
1627
+ const hydrated = hydratedBlocks[m.id];
1628
+ const hydratedProseStr = hydratedProse[m.id];
1629
+ const hydratedError = hydratedErrors[m.id];
1630
+ let blocks = [];
1631
+ let prose = "";
1632
+ let error;
1633
+ if (hydrated != null) {
1634
+ blocks = hydrated;
1635
+ prose = hydratedProseStr ?? "";
1636
+ error = hydratedError;
1637
+ } else {
1638
+ const run = liveTurns[liveAssistantSeen] ?? [];
1639
+ const acc = [];
1640
+ for (const raw of run) {
1641
+ acc[raw.index] = sanitiseBlock(raw);
1642
+ }
1643
+ blocks = acc;
1644
+ prose = m.content ?? "";
1645
+ error = liveErrors[liveAssistantSeen];
1646
+ liveAssistantSeen += 1;
1647
+ }
1648
+ if (prose) {
1649
+ const firstPb = blocks.findIndex((b) => b && b.kind === "paragraph_brief");
1650
+ if (firstPb >= 0) {
1651
+ const target = blocks[firstPb];
1652
+ if (target.kind === "paragraph_brief") {
1653
+ blocks[firstPb] = { ...target, prose };
1654
+ }
1655
+ }
1656
+ }
1657
+ const isLive = hydrated == null;
1658
+ const turnDone = !isLive || status === "ready" || status === "error";
1659
+ const startedTs = startedAt[m.id];
1660
+ out.push({
1661
+ question,
1662
+ blocks: blocks.filter(Boolean),
1663
+ prose,
1664
+ done: turnDone,
1665
+ error,
1666
+ startedAt: startedTs,
1667
+ durationMs: turnDone && startedTs != null ? Date.now() - startedTs : void 0
1668
+ });
1669
+ }
1670
+ if (pendingQuestion != null) {
1671
+ const lastUser = [...messages].reverse().find((m) => m.role === "user");
1672
+ const ts = lastUser ? startedAt[lastUser.id] : void 0;
1673
+ out.push({
1674
+ question: pendingQuestion,
1675
+ blocks: [],
1676
+ prose: "",
1677
+ done: false,
1678
+ startedAt: ts
1679
+ });
1680
+ }
1681
+ return out;
1682
+ }, [messages, data, hydratedBlocks, hydratedProse, hydratedErrors, status, startedAt]);
1683
+ useEffect(() => {
1684
+ if (answers.length > prevAnswersLen.current && lastAnswerRef.current) {
1685
+ lastAnswerRef.current.scrollIntoView({ behavior: "smooth", block: "start" });
1686
+ }
1687
+ prevAnswersLen.current = answers.length;
1688
+ }, [answers.length]);
1689
+ const newChat = useCallback(() => {
1690
+ setActiveSessionId(null);
1691
+ setMessages([]);
1692
+ setHydratedBlocks({});
1693
+ setHydratedProse({});
1694
+ setHydratedErrors({});
1695
+ setStartedAt({});
1696
+ setInput("");
1697
+ }, [setMessages, setInput]);
1698
+ const changeProvider = useCallback(
1699
+ async (next) => {
1700
+ if (next === provider || providerSaving) return;
1701
+ setProvider(next);
1702
+ setProviderSaving(true);
1703
+ try {
1704
+ const res = await fetch("/api/settings/me", {
1705
+ method: "PATCH",
1706
+ headers: { "Content-Type": "application/json" },
1707
+ body: JSON.stringify({ narrative_provider: next })
1708
+ });
1709
+ if (!res.ok) {
1710
+ setProvider((curr) => curr === next ? provider : curr);
1711
+ }
1712
+ } catch {
1713
+ setProvider((curr) => curr === next ? provider : curr);
1714
+ } finally {
1715
+ setProviderSaving(false);
1716
+ }
1717
+ },
1718
+ [provider, providerSaving]
1719
+ );
1720
+ const openSession = useCallback(
1721
+ async (id) => {
1722
+ setLoadingSession(true);
1723
+ setActiveSessionId(id);
1724
+ try {
1725
+ const res = await fetch(`/api/chat/sessions/${id}`, { cache: "no-store" });
1726
+ if (!res.ok) {
1727
+ setMessages([]);
1728
+ setHydratedBlocks({});
1729
+ setHydratedProse({});
1730
+ setHydratedErrors({});
1731
+ return;
1732
+ }
1733
+ const json = await res.json();
1734
+ const { uiMessages, blocksMap, proseMap, errorsMap } = storedToUseChat(
1735
+ json.messages ?? []
1736
+ );
1737
+ setMessages(uiMessages);
1738
+ setHydratedBlocks(blocksMap);
1739
+ setHydratedProse(proseMap);
1740
+ setHydratedErrors(errorsMap);
1741
+ setStartedAt({});
1742
+ } catch {
1743
+ setMessages([]);
1744
+ setHydratedBlocks({});
1745
+ setHydratedProse({});
1746
+ setHydratedErrors({});
1747
+ } finally {
1748
+ setLoadingSession(false);
1749
+ }
1750
+ },
1751
+ [setMessages]
1752
+ );
1753
+ const persistTitle = useCallback(
1754
+ async (id, title) => {
1755
+ const trimmed = title.trim();
1756
+ if (!trimmed) return;
1757
+ const res = await fetch(`/api/chat/sessions/${id}`, {
1758
+ method: "PATCH",
1759
+ headers: { "Content-Type": "application/json" },
1760
+ body: JSON.stringify({ title: trimmed })
1761
+ });
1762
+ if (res.ok) {
1763
+ setSessions(
1764
+ (prev) => prev.map((s) => s.id === id ? { ...s, title: trimmed } : s)
1765
+ );
1766
+ }
1767
+ },
1768
+ []
1769
+ );
1770
+ const startSidebarRename = useCallback((id) => {
1771
+ setEditingSidebarId(id);
1772
+ }, []);
1773
+ const deleteSession = useCallback(
1774
+ async (id) => {
1775
+ if (!window.confirm("Delete this chat? This cannot be undone.")) return;
1776
+ const res = await fetch(`/api/chat/sessions/${id}`, { method: "DELETE" });
1777
+ if (!res.ok) return;
1778
+ setSessions((prev) => prev.filter((s) => s.id !== id));
1779
+ if (activeSessionId === id) {
1780
+ setActiveSessionId(null);
1781
+ setMessages([]);
1782
+ setHydratedBlocks({});
1783
+ setHydratedProse({});
1784
+ setHydratedErrors({});
1785
+ }
1786
+ },
1787
+ [activeSessionId, setMessages]
1788
+ );
1789
+ const submitForm = useCallback(
1790
+ async (e) => {
1791
+ const trimmed = input.trim();
1792
+ if (!trimmed || status !== "ready") return;
1793
+ if (activeSessionIdRef.current == null) {
1794
+ try {
1795
+ const create = await fetch("/api/chat/sessions", {
1796
+ method: "POST",
1797
+ headers: { "Content-Type": "application/json" },
1798
+ body: JSON.stringify({})
1799
+ });
1800
+ if (create.ok) {
1801
+ const json = await create.json();
1802
+ activeSessionIdRef.current = json.session.id;
1803
+ setActiveSessionId(json.session.id);
1804
+ setSessions((prev) => [
1805
+ { id: json.session.id, title: json.session.title, updatedAt: null },
1806
+ ...prev
1807
+ ]);
1808
+ }
1809
+ } catch {
1810
+ }
1811
+ }
1812
+ handleSubmit(e);
1813
+ },
1814
+ [input, status, handleSubmit]
1815
+ );
1816
+ useEffect(() => {
1817
+ setStartedAt((prev) => {
1818
+ let changed = false;
1819
+ const next = { ...prev };
1820
+ for (const m of messages) {
1821
+ if (m.role === "user" && next[m.id] == null) {
1822
+ next[m.id] = Date.now();
1823
+ changed = true;
1824
+ }
1825
+ if (m.role === "assistant" && next[m.id] == null) {
1826
+ const idx = messages.findIndex((mm) => mm.id === m.id);
1827
+ for (let i = idx - 1; i >= 0; i -= 1) {
1828
+ const prior = messages[i];
1829
+ if (prior.role === "user" && next[prior.id] != null) {
1830
+ next[m.id] = next[prior.id];
1831
+ changed = true;
1832
+ break;
1833
+ }
1834
+ }
1835
+ }
1836
+ }
1837
+ return changed ? next : prev;
1838
+ });
1839
+ }, [messages]);
1840
+ const heroVisible = answers.length === 0 && !loadingSession;
1841
+ const greeting = useMemo(
1842
+ () => `Hi ${userFirstName || "there"}${userFirstName.endsWith("s") ? "" : ""},`,
1843
+ [userFirstName]
1844
+ );
1845
+ const activeTitle = activeSessionId ? sessions.find((s) => s.id === activeSessionId)?.title ?? "Chat" : "New chat";
1846
+ const retry = useCallback(
1847
+ (q) => {
1848
+ setInput(q);
1849
+ window.setTimeout(() => {
1850
+ const form = textareaRef.current?.form;
1851
+ if (form) form.requestSubmit();
1852
+ }, 0);
1853
+ },
1854
+ [setInput]
1855
+ );
1856
+ return /* @__PURE__ */ jsxs("div", { className: "relative flex h-full min-h-0 w-full overflow-hidden rounded-lg border border-border bg-background", children: [
1857
+ /* @__PURE__ */ jsx(
1858
+ "div",
1859
+ {
1860
+ onClick: () => setSidebarOpen(false),
1861
+ "aria-hidden": "true",
1862
+ className: cn(
1863
+ "absolute inset-0 z-10 bg-background/60 backdrop-blur-sm transition-opacity duration-200",
1864
+ sidebarOpen ? "opacity-100" : "pointer-events-none opacity-0"
1865
+ )
1866
+ }
1867
+ ),
1868
+ /* @__PURE__ */ jsxs(
1869
+ "aside",
1870
+ {
1871
+ inert: !sidebarOpen,
1872
+ className: cn(
1873
+ "absolute inset-y-0 left-0 z-20 flex w-72 max-w-[85vw] flex-col border-r border-border bg-sidebar text-sidebar-foreground shadow-lg transition-transform duration-200 ease-out",
1874
+ sidebarOpen ? "translate-x-0" : "-translate-x-full"
1875
+ ),
1876
+ children: [
1877
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border-b border-sidebar-border p-2", children: [
1878
+ /* @__PURE__ */ jsx(
1879
+ "button",
1880
+ {
1881
+ type: "button",
1882
+ onClick: () => setSidebarOpen(false),
1883
+ className: "inline-flex size-8 items-center justify-center rounded-md text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-foreground",
1884
+ "aria-label": "Close sidebar",
1885
+ children: /* @__PURE__ */ jsx(PanelLeftClose, { className: "size-4" })
1886
+ }
1887
+ ),
1888
+ /* @__PURE__ */ jsxs(
1889
+ "button",
1890
+ {
1891
+ type: "button",
1892
+ onClick: () => {
1893
+ newChat();
1894
+ setSidebarOpen(false);
1895
+ },
1896
+ className: "ml-auto inline-flex items-center gap-1.5 rounded-full border border-sidebar-border bg-sidebar-accent px-3 py-1.5 text-xs font-medium text-sidebar-accent-foreground hover:bg-sidebar-accent/80",
1897
+ children: [
1898
+ /* @__PURE__ */ jsx(Plus, { className: "size-3.5" }),
1899
+ "New chat"
1900
+ ]
1901
+ }
1902
+ )
1903
+ ] }),
1904
+ /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto p-2", children: sessions.length === 0 ? /* @__PURE__ */ jsx("p", { className: "px-2 py-3 text-xs text-sidebar-foreground/60", children: "No chats yet. Ask something below to start." }) : /* @__PURE__ */ jsx("ul", { className: "flex flex-col gap-0.5", children: sessions.map((s) => {
1905
+ const active = s.id === activeSessionId;
1906
+ const editing = s.id === editingSidebarId;
1907
+ if (editing) {
1908
+ return /* @__PURE__ */ jsx("li", { className: "px-1 py-1", children: /* @__PURE__ */ jsx(
1909
+ SidebarTitleEditor,
1910
+ {
1911
+ initial: s.title,
1912
+ onCancel: () => setEditingSidebarId(null),
1913
+ onSave: (next) => {
1914
+ setEditingSidebarId(null);
1915
+ void persistTitle(s.id, next);
1916
+ }
1917
+ }
1918
+ ) }, s.id);
1919
+ }
1920
+ return /* @__PURE__ */ jsxs("li", { className: "group relative", children: [
1921
+ /* @__PURE__ */ jsx(
1922
+ "button",
1923
+ {
1924
+ type: "button",
1925
+ onClick: () => {
1926
+ void openSession(s.id);
1927
+ setSidebarOpen(false);
1928
+ },
1929
+ className: cn(
1930
+ "flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm",
1931
+ active ? "bg-sidebar-accent text-sidebar-accent-foreground" : "text-sidebar-foreground/80 hover:bg-sidebar-accent/60 hover:text-sidebar-foreground"
1932
+ ),
1933
+ title: s.title,
1934
+ children: /* @__PURE__ */ jsx("span", { className: "truncate", children: s.title })
1935
+ }
1936
+ ),
1937
+ /* @__PURE__ */ jsxs("div", { className: "absolute right-1 top-1/2 hidden -translate-y-1/2 items-center gap-0.5 group-hover:flex", children: [
1938
+ /* @__PURE__ */ jsx(
1939
+ "button",
1940
+ {
1941
+ type: "button",
1942
+ onClick: (e) => {
1943
+ e.stopPropagation();
1944
+ startSidebarRename(s.id);
1945
+ },
1946
+ "aria-label": "Rename",
1947
+ className: "inline-flex size-6 items-center justify-center rounded text-sidebar-foreground/60 hover:bg-sidebar hover:text-sidebar-foreground",
1948
+ children: /* @__PURE__ */ jsx(Pencil, { className: "size-3" })
1949
+ }
1950
+ ),
1951
+ /* @__PURE__ */ jsx(
1952
+ "button",
1953
+ {
1954
+ type: "button",
1955
+ onClick: (e) => {
1956
+ e.stopPropagation();
1957
+ void deleteSession(s.id);
1958
+ },
1959
+ "aria-label": "Delete",
1960
+ className: "inline-flex size-6 items-center justify-center rounded text-sidebar-foreground/60 hover:bg-sidebar hover:text-destructive",
1961
+ children: /* @__PURE__ */ jsx(Trash2, { className: "size-3" })
1962
+ }
1963
+ )
1964
+ ] })
1965
+ ] }, s.id);
1966
+ }) }) })
1967
+ ]
1968
+ }
1969
+ ),
1970
+ /* @__PURE__ */ jsxs("section", { className: "relative flex flex-1 flex-col overflow-hidden", children: [
1971
+ /* @__PURE__ */ jsxs("header", { className: "flex items-center gap-2 px-3 py-3 text-sm text-muted-foreground", children: [
1972
+ /* @__PURE__ */ jsx(
1973
+ "button",
1974
+ {
1975
+ type: "button",
1976
+ onClick: () => setSidebarOpen(true),
1977
+ "aria-label": "Open chat history",
1978
+ className: "inline-flex size-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground",
1979
+ children: /* @__PURE__ */ jsx(Menu, { className: "size-4" })
1980
+ }
1981
+ ),
1982
+ /* @__PURE__ */ jsx(Sparkles, { className: "size-4 text-primary" }),
1983
+ activeSessionId != null ? /* @__PURE__ */ jsx(
1984
+ EditableTitle,
1985
+ {
1986
+ title: activeTitle,
1987
+ onSave: (next) => void persistTitle(activeSessionId, next)
1988
+ },
1989
+ activeSessionId
1990
+ ) : /* @__PURE__ */ jsx("span", { className: "truncate", children: activeTitle })
1991
+ ] }),
1992
+ /* @__PURE__ */ jsx(
1993
+ "div",
1994
+ {
1995
+ ref: threadRef,
1996
+ className: "min-h-0 flex-1 overflow-y-auto px-4 pb-6 pt-2",
1997
+ children: loadingSession ? /* @__PURE__ */ jsxs("div", { className: "flex h-full items-center justify-center text-sm text-muted-foreground", children: [
1998
+ /* @__PURE__ */ jsx(Loader2, { className: "mr-2 size-4 animate-spin" }),
1999
+ " Loading conversation\u2026"
2000
+ ] }) : heroVisible ? /* @__PURE__ */ jsxs("div", { className: "flex h-full flex-col items-center justify-center text-center", children: [
2001
+ /* @__PURE__ */ jsx("h1", { className: "text-2xl font-medium tracking-tight text-foreground sm:text-3xl", children: greeting }),
2002
+ /* @__PURE__ */ jsx("p", { className: "mt-2 text-2xl font-light tracking-tight text-muted-foreground sm:text-3xl", children: "What's on your mind?" })
2003
+ ] }) : /* @__PURE__ */ jsx("div", { className: "mx-auto flex w-full max-w-3xl flex-col gap-6", children: answers.map((a, idx) => /* @__PURE__ */ jsx(
2004
+ AnswerView2,
2005
+ {
2006
+ answer: a,
2007
+ onRetry: () => retry(a.question),
2008
+ forwardRef: idx === answers.length - 1 ? lastAnswerRef : void 0,
2009
+ isLast: idx === answers.length - 1
2010
+ },
2011
+ idx
2012
+ )) })
2013
+ }
2014
+ ),
2015
+ /* @__PURE__ */ jsx("div", { className: "shrink-0 px-4 pb-4 pt-2", children: /* @__PURE__ */ jsxs(
2016
+ "form",
2017
+ {
2018
+ className: "mx-auto w-full max-w-3xl",
2019
+ onSubmit: (e) => {
2020
+ e.preventDefault();
2021
+ void submitForm(e);
2022
+ },
2023
+ children: [
2024
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 rounded-3xl border border-border bg-background px-4 py-3 shadow-sm", children: [
2025
+ /* @__PURE__ */ jsx(
2026
+ "textarea",
2027
+ {
2028
+ ref: textareaRef,
2029
+ value: input,
2030
+ onChange: handleInputChange,
2031
+ onKeyDown: (e) => {
2032
+ if (e.key === "Enter" && !e.shiftKey) {
2033
+ e.preventDefault();
2034
+ e.currentTarget.form?.requestSubmit();
2035
+ }
2036
+ },
2037
+ placeholder: "Ask AI...",
2038
+ rows: 1,
2039
+ disabled: status !== "ready",
2040
+ style: { maxHeight: TEXTAREA_MAX_PX2 },
2041
+ className: "w-full resize-none overflow-y-auto border-0 bg-transparent py-1 text-sm leading-6 text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-0 disabled:opacity-60"
2042
+ }
2043
+ ),
2044
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
2045
+ /* @__PURE__ */ jsxs(
2046
+ "span",
2047
+ {
2048
+ className: "inline-flex max-w-[60%] items-center gap-1.5 truncate rounded-full border border-border bg-muted/40 px-2.5 py-1 text-xs font-medium text-muted-foreground",
2049
+ title: `Scope: ${scopeLabel}`,
2050
+ children: [
2051
+ "Scope: ",
2052
+ /* @__PURE__ */ jsx("span", { className: "truncate text-foreground", children: scopeLabel })
2053
+ ]
2054
+ }
2055
+ ),
2056
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
2057
+ /* @__PURE__ */ jsxs(DropdownMenu, { children: [
2058
+ /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
2059
+ "button",
2060
+ {
2061
+ type: "button",
2062
+ className: "inline-flex items-center gap-1 rounded-full px-2.5 py-1 text-xs font-medium text-muted-foreground hover:bg-accent hover:text-foreground",
2063
+ "aria-label": "Choose model",
2064
+ children: [
2065
+ PROVIDER_LABELS2[provider],
2066
+ /* @__PURE__ */ jsx(ChevronDown, { className: "size-3.5" })
2067
+ ]
2068
+ }
2069
+ ) }),
2070
+ /* @__PURE__ */ jsx(DropdownMenuContent, { align: "end", className: "w-56", children: ["claude", "grok", "gemini"].map((p) => /* @__PURE__ */ jsxs(
2071
+ DropdownMenuItem,
2072
+ {
2073
+ onSelect: () => void changeProvider(p),
2074
+ className: "flex items-start gap-2",
2075
+ children: [
2076
+ /* @__PURE__ */ jsx(
2077
+ Check,
2078
+ {
2079
+ className: cn(
2080
+ "mt-0.5 size-4",
2081
+ provider === p ? "opacity-100" : "opacity-0"
2082
+ )
2083
+ }
2084
+ ),
2085
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
2086
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: PROVIDER_LABELS2[p] }),
2087
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: PROVIDER_DESCRIPTIONS2[p] })
2088
+ ] })
2089
+ ]
2090
+ },
2091
+ p
2092
+ )) })
2093
+ ] }),
2094
+ /* @__PURE__ */ jsx(
2095
+ "button",
2096
+ {
2097
+ type: "submit",
2098
+ disabled: status !== "ready" || !input.trim(),
2099
+ "aria-label": "Send",
2100
+ className: "inline-flex size-8 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground transition-colors enabled:hover:bg-primary/90 disabled:opacity-40",
2101
+ children: status === "streaming" || status === "submitted" ? /* @__PURE__ */ jsx(Loader2, { className: "size-4 animate-spin" }) : /* @__PURE__ */ jsx(ArrowUp, { className: "size-4" })
2102
+ }
2103
+ )
2104
+ ] })
2105
+ ] })
2106
+ ] }),
2107
+ /* @__PURE__ */ jsxs("p", { className: "mt-2 text-center text-xs text-muted-foreground", children: [
2108
+ PROVIDER_LABELS2[provider],
2109
+ " is AI and can make mistakes."
2110
+ ] })
2111
+ ]
2112
+ }
2113
+ ) })
2114
+ ] })
2115
+ ] });
2116
+ }
2117
+ function AnswerView2({
2118
+ answer,
2119
+ onRetry,
2120
+ forwardRef,
2121
+ isLast
2122
+ }) {
2123
+ const [copied, setCopied] = useState(false);
2124
+ const handleCopy = useCallback(async () => {
2125
+ const blockText = blocksToPlainText2(answer.blocks);
2126
+ const text = blockText || (answer.error ? `${answer.error.code}: ${answer.error.message}` : "");
2127
+ if (!text) return;
2128
+ try {
2129
+ await navigator.clipboard.writeText(text);
2130
+ setCopied(true);
2131
+ window.setTimeout(() => setCopied(false), 1500);
2132
+ } catch {
2133
+ }
2134
+ }, [answer.blocks, answer.error]);
2135
+ const showActions = answer.done && (answer.blocks.length > 0 || answer.error != null);
2136
+ const isThinking = !answer.done && answer.blocks.length === 0 && !answer.error;
2137
+ const [, forceTick] = useState(0);
2138
+ useEffect(() => {
2139
+ if (answer.done || answer.startedAt == null) return;
2140
+ const id = window.setInterval(() => forceTick((n) => n + 1), 1e3);
2141
+ return () => window.clearInterval(id);
2142
+ }, [answer.done, answer.startedAt]);
2143
+ const liveElapsed = answer.startedAt != null ? Date.now() - answer.startedAt : null;
2144
+ const finalDuration = answer.durationMs;
2145
+ return /* @__PURE__ */ jsxs(
2146
+ "div",
2147
+ {
2148
+ ref: forwardRef,
2149
+ className: cn(
2150
+ "flex flex-col gap-4 scroll-mt-2",
2151
+ // Reserve space below the latest turn so the AI response has a
2152
+ // full screen to fill without bouncing the user. Calc subtracts
2153
+ // the chat header (~3rem) and the input pill area (~10rem) from
2154
+ // the dynamic viewport height.
2155
+ isLast && "min-h-[calc(100dvh-13rem)]"
2156
+ ),
2157
+ children: [
2158
+ /* @__PURE__ */ jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx(UserChip2, { text: answer.question }) }),
2159
+ isThinking ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
2160
+ /* @__PURE__ */ jsx("div", { className: "flex size-7 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary", children: /* @__PURE__ */ jsx(Sparkles, { className: "size-4" }) }),
2161
+ /* @__PURE__ */ jsxs("p", { className: "flex items-center text-sm text-muted-foreground", children: [
2162
+ /* @__PURE__ */ jsx(Loader2, { className: "mr-1 inline size-3.5 animate-spin" }),
2163
+ "Thinking\u2026",
2164
+ liveElapsed != null && liveElapsed >= 1e3 && /* @__PURE__ */ jsxs("span", { className: "ml-2 text-xs tabular-nums", children: [
2165
+ "(",
2166
+ formatDuration2(liveElapsed),
2167
+ ")"
2168
+ ] })
2169
+ ] })
2170
+ ] }) : /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
2171
+ /* @__PURE__ */ jsx("div", { className: "mt-0.5 flex size-7 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary", children: /* @__PURE__ */ jsx(Sparkles, { className: "size-4" }) }),
2172
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-3", children: [
2173
+ /* @__PURE__ */ jsx(AnswerBlocks, { blocks: answer.blocks }),
2174
+ answer.error && /* @__PURE__ */ jsxs("div", { className: "wrap-break-word whitespace-pre-wrap rounded border border-amber-300 bg-amber-50 p-3 text-sm text-amber-900 dark:border-amber-800 dark:bg-amber-950/30 dark:text-amber-100", children: [
2175
+ answer.error.code,
2176
+ ": ",
2177
+ answer.error.message
2178
+ ] }),
2179
+ showActions && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
2180
+ (answer.blocks.length > 0 || answer.error != null) && /* @__PURE__ */ jsx(
2181
+ "button",
2182
+ {
2183
+ type: "button",
2184
+ onClick: handleCopy,
2185
+ "aria-label": answer.blocks.length === 0 && answer.error ? "Copy error" : "Copy response",
2186
+ title: copied ? "Copied" : "Copy",
2187
+ className: "inline-flex size-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground",
2188
+ children: copied ? /* @__PURE__ */ jsx(Check, { className: "size-4" }) : /* @__PURE__ */ jsx(Copy, { className: "size-4" })
2189
+ }
2190
+ ),
2191
+ /* @__PURE__ */ jsx(
2192
+ "button",
2193
+ {
2194
+ type: "button",
2195
+ onClick: onRetry,
2196
+ "aria-label": "Retry",
2197
+ title: "Retry",
2198
+ className: "inline-flex size-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground",
2199
+ children: /* @__PURE__ */ jsx(RotateCcw, { className: "size-4" })
2200
+ }
2201
+ ),
2202
+ finalDuration != null && finalDuration >= 1e3 && /* @__PURE__ */ jsx(
2203
+ "span",
2204
+ {
2205
+ className: "ml-1 text-xs text-muted-foreground tabular-nums",
2206
+ title: "Time taken to generate this response",
2207
+ children: formatDuration2(finalDuration)
2208
+ }
2209
+ )
2210
+ ] })
2211
+ ] })
2212
+ ] })
2213
+ ]
2214
+ }
2215
+ );
2216
+ }
2217
+ function blocksToPlainText2(blocks) {
2218
+ return blocks.map((b) => {
2219
+ if (b.kind === "paragraph_brief") {
2220
+ return b.prose || b.key_facts.join(". ");
2221
+ }
2222
+ if (b.kind === "list") {
2223
+ const lines = b.items.map(
2224
+ (it, i) => b.style === "numbered" ? `${i + 1}. ${it}` : `- ${it}`
2225
+ );
2226
+ return [b.title, ...lines].filter(Boolean).join("\n");
2227
+ }
2228
+ if (b.kind === "chart") {
2229
+ return `${b.title} (chart)`;
2230
+ }
2231
+ if (b.kind === "table") {
2232
+ const header = b.columns.join(" ");
2233
+ const rows = b.rows.map(
2234
+ (r) => r.map((c) => c == null ? "\u2014" : String(c)).join(" ")
2235
+ );
2236
+ return [b.title, header, ...rows].join("\n");
2237
+ }
2238
+ if (b.kind === "callout") return b.text;
2239
+ return "";
2240
+ }).filter(Boolean).join("\n\n");
2241
+ }
2242
+ function UserChip2({ text }) {
2243
+ const [expanded, setExpanded] = useState(false);
2244
+ const [overflowing, setOverflowing] = useState(false);
2245
+ const ref = useRef(null);
2246
+ useLayoutEffect(() => {
2247
+ const el = ref.current;
2248
+ if (!el || expanded) return;
2249
+ setOverflowing(el.scrollHeight > el.clientHeight + 1);
2250
+ }, [text, expanded]);
2251
+ return /* @__PURE__ */ jsxs("div", { className: "max-w-[85%] rounded-2xl bg-muted px-4 py-2 text-sm text-foreground", children: [
2252
+ /* @__PURE__ */ jsxs("div", { className: "relative", children: [
2253
+ /* @__PURE__ */ jsx(
2254
+ "div",
2255
+ {
2256
+ ref,
2257
+ className: cn(
2258
+ "whitespace-pre-wrap wrap-break-word leading-6",
2259
+ // 3 lines × leading-6 (24px) = 72px → max-h-18.
2260
+ !expanded && "max-h-18 overflow-hidden"
2261
+ ),
2262
+ children: text
2263
+ }
2264
+ ),
2265
+ !expanded && overflowing && /* @__PURE__ */ jsx(
2266
+ "div",
2267
+ {
2268
+ "aria-hidden": "true",
2269
+ className: "pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-linear-to-t from-muted to-transparent"
2270
+ }
2271
+ )
2272
+ ] }),
2273
+ (overflowing || expanded) && /* @__PURE__ */ jsx(
2274
+ "button",
2275
+ {
2276
+ type: "button",
2277
+ onClick: () => setExpanded((v) => !v),
2278
+ className: "mt-1 inline-flex items-center gap-1 text-xs font-medium text-muted-foreground hover:text-foreground",
2279
+ children: expanded ? /* @__PURE__ */ jsxs(Fragment, { children: [
2280
+ /* @__PURE__ */ jsx(ChevronUp, { className: "size-3.5" }),
2281
+ "Show less"
2282
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
2283
+ /* @__PURE__ */ jsx(ChevronDown, { className: "size-3.5" }),
2284
+ "Show more"
2285
+ ] })
2286
+ }
2287
+ )
2288
+ ] });
2289
+ }
2290
+ function storedToUseChat(stored) {
2291
+ const uiMessages = [];
2292
+ const blocksMap = {};
2293
+ const proseMap = {};
2294
+ const errorsMap = {};
2295
+ for (const m of stored) {
2296
+ if (m.role === "user") {
2297
+ const id = `user-${m.id}`;
2298
+ const question = m.question ?? "";
2299
+ uiMessages.push({ id, role: "user", content: question });
2300
+ continue;
2301
+ }
2302
+ if (m.role === "assistant") {
2303
+ const id = `assistant-${m.id}`;
2304
+ const blocks = [];
2305
+ const stored2 = m.blocks ?? [];
2306
+ stored2.forEach((b, i) => {
2307
+ const sanitised = sanitiseBlock({ ...b});
2308
+ if (sanitised.kind === "paragraph_brief") {
2309
+ sanitised.prose = m.prose?.[String(i)] ?? "";
2310
+ }
2311
+ blocks[i] = sanitised;
2312
+ });
2313
+ const joinedProse = Object.values(m.prose ?? {}).filter(Boolean).join("\n\n");
2314
+ uiMessages.push({
2315
+ id,
2316
+ role: "assistant",
2317
+ content: joinedProse
2318
+ });
2319
+ blocksMap[id] = blocks;
2320
+ if (joinedProse) proseMap[id] = joinedProse;
2321
+ if (m.errorJson) errorsMap[id] = m.errorJson;
2322
+ }
2323
+ }
2324
+ return { uiMessages, blocksMap, proseMap, errorsMap };
2325
+ }
2326
+
2327
+ // src/ui/index.tsx
2328
+ var chatInterfaces = [
2329
+ {
2330
+ id: "custom",
2331
+ label: "Custom",
2332
+ description: "Bespoke SSE chat with structured blocks and prose narrator pass.",
2333
+ Component: AiChat
2334
+ },
2335
+ {
2336
+ id: "vercel",
2337
+ label: "Vercel",
2338
+ description: "Vercel AI SDK chat with native tool loop and data-stream protocol.",
2339
+ Component: VercelChat
2340
+ }
2341
+ ];
2342
+ function getChatInterface(id) {
2343
+ return chatInterfaces.find((c) => c.id === id) ?? chatInterfaces[0];
2344
+ }
1411
2345
 
1412
- export { AiChat, AnswerBlocks, sanitiseBlock };
2346
+ export { AiChat, AnswerBlocks, VercelChat, chatInterfaces, getChatInterface, sanitiseBlock };
1413
2347
  //# sourceMappingURL=index.js.map
1414
2348
  //# sourceMappingURL=index.js.map