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