@firstlovecenter/ai-chat 0.1.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/LICENSE +21 -0
- package/README.md +53 -0
- package/dist/drizzle/index.cjs +199 -0
- package/dist/drizzle/index.cjs.map +1 -0
- package/dist/drizzle/index.d.cts +361 -0
- package/dist/drizzle/index.d.ts +361 -0
- package/dist/drizzle/index.js +194 -0
- package/dist/drizzle/index.js.map +1 -0
- package/dist/prisma/index.cjs +163 -0
- package/dist/prisma/index.cjs.map +1 -0
- package/dist/prisma/index.d.cts +163 -0
- package/dist/prisma/index.d.ts +163 -0
- package/dist/prisma/index.js +160 -0
- package/dist/prisma/index.js.map +1 -0
- package/dist/server/index.cjs +1465 -0
- package/dist/server/index.cjs.map +1 -0
- package/dist/server/index.d.cts +487 -0
- package/dist/server/index.d.ts +487 -0
- package/dist/server/index.js +1450 -0
- package/dist/server/index.js.map +1 -0
- package/dist/types-DNwFvL-C.d.cts +268 -0
- package/dist/types-DNwFvL-C.d.ts +268 -0
- package/dist/ui/index.cjs +1388 -0
- package/dist/ui/index.cjs.map +1 -0
- package/dist/ui/index.d.cts +89 -0
- package/dist/ui/index.d.ts +89 -0
- package/dist/ui/index.js +1365 -0
- package/dist/ui/index.js.map +1 -0
- package/package.json +112 -0
- package/prisma/chat-models.prisma +61 -0
|
@@ -0,0 +1,1388 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var React = require('react');
|
|
5
|
+
var lucideReact = require('lucide-react');
|
|
6
|
+
var radixUi = require('radix-ui');
|
|
7
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
8
|
+
var RechartsPrimitive = require('recharts');
|
|
9
|
+
|
|
10
|
+
function _interopNamespace(e) {
|
|
11
|
+
if (e && e.__esModule) return e;
|
|
12
|
+
var n = Object.create(null);
|
|
13
|
+
if (e) {
|
|
14
|
+
Object.keys(e).forEach(function (k) {
|
|
15
|
+
if (k !== 'default') {
|
|
16
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
17
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
18
|
+
enumerable: true,
|
|
19
|
+
get: function () { return e[k]; }
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
n.default = e;
|
|
25
|
+
return Object.freeze(n);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
var React__namespace = /*#__PURE__*/_interopNamespace(React);
|
|
29
|
+
var RechartsPrimitive__namespace = /*#__PURE__*/_interopNamespace(RechartsPrimitive);
|
|
30
|
+
|
|
31
|
+
// src/ui/_shared/cn.ts
|
|
32
|
+
function cn(...args) {
|
|
33
|
+
const out = [];
|
|
34
|
+
for (const a of args) {
|
|
35
|
+
if (!a) continue;
|
|
36
|
+
if (typeof a === "string") {
|
|
37
|
+
out.push(a);
|
|
38
|
+
} else if (Array.isArray(a)) {
|
|
39
|
+
const inner = cn(...a);
|
|
40
|
+
if (inner) out.push(inner);
|
|
41
|
+
} else if (typeof a === "object") {
|
|
42
|
+
for (const [k, v] of Object.entries(a)) {
|
|
43
|
+
if (v) out.push(k);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return out.join(" ");
|
|
48
|
+
}
|
|
49
|
+
function DropdownMenu({
|
|
50
|
+
...props
|
|
51
|
+
}) {
|
|
52
|
+
return /* @__PURE__ */ jsxRuntime.jsx(radixUi.DropdownMenu.Root, { "data-slot": "dropdown-menu", ...props });
|
|
53
|
+
}
|
|
54
|
+
function DropdownMenuTrigger({
|
|
55
|
+
...props
|
|
56
|
+
}) {
|
|
57
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
58
|
+
radixUi.DropdownMenu.Trigger,
|
|
59
|
+
{
|
|
60
|
+
"data-slot": "dropdown-menu-trigger",
|
|
61
|
+
...props
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
function DropdownMenuContent({
|
|
66
|
+
className,
|
|
67
|
+
align = "start",
|
|
68
|
+
sideOffset = 4,
|
|
69
|
+
...props
|
|
70
|
+
}) {
|
|
71
|
+
return /* @__PURE__ */ jsxRuntime.jsx(radixUi.DropdownMenu.Portal, { children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
72
|
+
radixUi.DropdownMenu.Content,
|
|
73
|
+
{
|
|
74
|
+
"data-slot": "dropdown-menu-content",
|
|
75
|
+
sideOffset,
|
|
76
|
+
align,
|
|
77
|
+
className: cn("z-50 max-h-(--radix-dropdown-menu-content-available-height) w-(--radix-dropdown-menu-trigger-width) min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:overflow-hidden data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className),
|
|
78
|
+
...props
|
|
79
|
+
}
|
|
80
|
+
) });
|
|
81
|
+
}
|
|
82
|
+
function DropdownMenuItem({
|
|
83
|
+
className,
|
|
84
|
+
inset,
|
|
85
|
+
variant = "default",
|
|
86
|
+
...props
|
|
87
|
+
}) {
|
|
88
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
89
|
+
radixUi.DropdownMenu.Item,
|
|
90
|
+
{
|
|
91
|
+
"data-slot": "dropdown-menu-item",
|
|
92
|
+
"data-inset": inset,
|
|
93
|
+
"data-variant": variant,
|
|
94
|
+
className: cn(
|
|
95
|
+
"group/dropdown-menu-item relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive",
|
|
96
|
+
className
|
|
97
|
+
),
|
|
98
|
+
...props
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
function ChartCard({
|
|
103
|
+
title,
|
|
104
|
+
subtitle,
|
|
105
|
+
children,
|
|
106
|
+
className
|
|
107
|
+
}) {
|
|
108
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
109
|
+
"div",
|
|
110
|
+
{
|
|
111
|
+
className: cn(
|
|
112
|
+
"flex flex-col rounded-xl border border-border bg-card text-card-foreground shadow-sm",
|
|
113
|
+
className
|
|
114
|
+
),
|
|
115
|
+
children: [
|
|
116
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1.5 p-6", children: [
|
|
117
|
+
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "font-semibold leading-none tracking-tight", children: title }),
|
|
118
|
+
subtitle ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: subtitle }) : null
|
|
119
|
+
] }),
|
|
120
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-6 pt-0", children })
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
var THEMES = { light: "", dark: ".dark" };
|
|
126
|
+
var INITIAL_DIMENSION = { width: 320, height: 200 };
|
|
127
|
+
var ChartContext = React__namespace.createContext(null);
|
|
128
|
+
function useChart() {
|
|
129
|
+
const context = React__namespace.useContext(ChartContext);
|
|
130
|
+
if (!context) {
|
|
131
|
+
throw new Error("useChart must be used within a <ChartContainer />");
|
|
132
|
+
}
|
|
133
|
+
return context;
|
|
134
|
+
}
|
|
135
|
+
function ChartContainer({
|
|
136
|
+
id,
|
|
137
|
+
className,
|
|
138
|
+
children,
|
|
139
|
+
config,
|
|
140
|
+
initialDimension = INITIAL_DIMENSION,
|
|
141
|
+
...props
|
|
142
|
+
}) {
|
|
143
|
+
const uniqueId = React__namespace.useId();
|
|
144
|
+
const chartId = `chart-${id ?? uniqueId.replace(/:/g, "")}`;
|
|
145
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ChartContext.Provider, { value: { config }, children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
146
|
+
"div",
|
|
147
|
+
{
|
|
148
|
+
"data-slot": "chart",
|
|
149
|
+
"data-chart": chartId,
|
|
150
|
+
className: cn(
|
|
151
|
+
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
|
|
152
|
+
className
|
|
153
|
+
),
|
|
154
|
+
...props,
|
|
155
|
+
children: [
|
|
156
|
+
/* @__PURE__ */ jsxRuntime.jsx(ChartStyle, { id: chartId, config }),
|
|
157
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
158
|
+
RechartsPrimitive__namespace.ResponsiveContainer,
|
|
159
|
+
{
|
|
160
|
+
initialDimension,
|
|
161
|
+
children
|
|
162
|
+
}
|
|
163
|
+
)
|
|
164
|
+
]
|
|
165
|
+
}
|
|
166
|
+
) });
|
|
167
|
+
}
|
|
168
|
+
var ChartStyle = ({ id, config }) => {
|
|
169
|
+
const colorConfig = Object.entries(config).filter(
|
|
170
|
+
([, config2]) => config2.theme ?? config2.color
|
|
171
|
+
);
|
|
172
|
+
if (!colorConfig.length) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
176
|
+
"style",
|
|
177
|
+
{
|
|
178
|
+
dangerouslySetInnerHTML: {
|
|
179
|
+
__html: Object.entries(THEMES).map(
|
|
180
|
+
([theme, prefix]) => `
|
|
181
|
+
${prefix} [data-chart=${id}] {
|
|
182
|
+
${colorConfig.map(([key, itemConfig]) => {
|
|
183
|
+
const color = itemConfig.theme?.[theme] ?? itemConfig.color;
|
|
184
|
+
return color ? ` --color-${key}: ${color};` : null;
|
|
185
|
+
}).join("\n")}
|
|
186
|
+
}
|
|
187
|
+
`
|
|
188
|
+
).join("\n")
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
);
|
|
192
|
+
};
|
|
193
|
+
var ChartTooltip = RechartsPrimitive__namespace.Tooltip;
|
|
194
|
+
function ChartTooltipContent({
|
|
195
|
+
active,
|
|
196
|
+
payload,
|
|
197
|
+
className,
|
|
198
|
+
indicator = "dot",
|
|
199
|
+
hideLabel = false,
|
|
200
|
+
hideIndicator = false,
|
|
201
|
+
label,
|
|
202
|
+
labelFormatter,
|
|
203
|
+
labelClassName,
|
|
204
|
+
formatter,
|
|
205
|
+
color,
|
|
206
|
+
nameKey,
|
|
207
|
+
labelKey
|
|
208
|
+
}) {
|
|
209
|
+
const { config } = useChart();
|
|
210
|
+
const tooltipLabel = React__namespace.useMemo(() => {
|
|
211
|
+
if (hideLabel || !payload?.length) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
const [item] = payload;
|
|
215
|
+
const key = `${labelKey ?? item?.dataKey ?? item?.name ?? "value"}`;
|
|
216
|
+
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
|
217
|
+
const value = !labelKey && typeof label === "string" ? config[label]?.label ?? label : itemConfig?.label;
|
|
218
|
+
if (labelFormatter) {
|
|
219
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("font-medium", labelClassName), children: labelFormatter(value, payload) });
|
|
220
|
+
}
|
|
221
|
+
if (!value) {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("font-medium", labelClassName), children: value });
|
|
225
|
+
}, [
|
|
226
|
+
label,
|
|
227
|
+
labelFormatter,
|
|
228
|
+
payload,
|
|
229
|
+
hideLabel,
|
|
230
|
+
labelClassName,
|
|
231
|
+
config,
|
|
232
|
+
labelKey
|
|
233
|
+
]);
|
|
234
|
+
if (!active || !payload?.length) {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
const nestLabel = payload.length === 1 && indicator !== "dot";
|
|
238
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
239
|
+
"div",
|
|
240
|
+
{
|
|
241
|
+
className: cn(
|
|
242
|
+
"grid min-w-32 items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
|
|
243
|
+
className
|
|
244
|
+
),
|
|
245
|
+
children: [
|
|
246
|
+
!nestLabel ? tooltipLabel : null,
|
|
247
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid gap-1.5", children: payload.filter((item) => item.type !== "none").map((item, index) => {
|
|
248
|
+
const key = `${nameKey ?? item.name ?? item.dataKey ?? "value"}`;
|
|
249
|
+
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
|
250
|
+
const indicatorColor = color ?? item.payload?.fill ?? item.color;
|
|
251
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
252
|
+
"div",
|
|
253
|
+
{
|
|
254
|
+
className: cn(
|
|
255
|
+
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
|
|
256
|
+
indicator === "dot" && "items-center"
|
|
257
|
+
),
|
|
258
|
+
children: formatter && item?.value !== void 0 && item.name ? formatter(item.value, item.name, item, index, item.payload) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
259
|
+
itemConfig?.icon ? /* @__PURE__ */ jsxRuntime.jsx(itemConfig.icon, {}) : !hideIndicator && /* @__PURE__ */ jsxRuntime.jsx(
|
|
260
|
+
"div",
|
|
261
|
+
{
|
|
262
|
+
className: cn(
|
|
263
|
+
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
|
|
264
|
+
{
|
|
265
|
+
"h-2.5 w-2.5": indicator === "dot",
|
|
266
|
+
"w-1": indicator === "line",
|
|
267
|
+
"w-0 border-[1.5px] border-dashed bg-transparent": indicator === "dashed",
|
|
268
|
+
"my-0.5": nestLabel && indicator === "dashed"
|
|
269
|
+
}
|
|
270
|
+
),
|
|
271
|
+
style: {
|
|
272
|
+
"--color-bg": indicatorColor,
|
|
273
|
+
"--color-border": indicatorColor
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
),
|
|
277
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
278
|
+
"div",
|
|
279
|
+
{
|
|
280
|
+
className: cn(
|
|
281
|
+
"flex flex-1 justify-between leading-none",
|
|
282
|
+
nestLabel ? "items-end" : "items-center"
|
|
283
|
+
),
|
|
284
|
+
children: [
|
|
285
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-1.5", children: [
|
|
286
|
+
nestLabel ? tooltipLabel : null,
|
|
287
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: itemConfig?.label ?? item.name })
|
|
288
|
+
] }),
|
|
289
|
+
item.value != null && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono font-medium text-foreground tabular-nums", children: typeof item.value === "number" ? item.value.toLocaleString() : String(item.value) })
|
|
290
|
+
]
|
|
291
|
+
}
|
|
292
|
+
)
|
|
293
|
+
] })
|
|
294
|
+
},
|
|
295
|
+
index
|
|
296
|
+
);
|
|
297
|
+
}) })
|
|
298
|
+
]
|
|
299
|
+
}
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
var ChartLegend = RechartsPrimitive__namespace.Legend;
|
|
303
|
+
function ChartLegendContent({
|
|
304
|
+
className,
|
|
305
|
+
hideIcon = false,
|
|
306
|
+
payload,
|
|
307
|
+
verticalAlign = "bottom",
|
|
308
|
+
nameKey
|
|
309
|
+
}) {
|
|
310
|
+
const { config } = useChart();
|
|
311
|
+
if (!payload?.length) {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
315
|
+
"div",
|
|
316
|
+
{
|
|
317
|
+
className: cn(
|
|
318
|
+
"flex items-center justify-center gap-4",
|
|
319
|
+
verticalAlign === "top" ? "pb-3" : "pt-3",
|
|
320
|
+
className
|
|
321
|
+
),
|
|
322
|
+
children: payload.filter((item) => item.type !== "none").map((item, index) => {
|
|
323
|
+
const key = `${nameKey ?? item.dataKey ?? "value"}`;
|
|
324
|
+
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
|
325
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
326
|
+
"div",
|
|
327
|
+
{
|
|
328
|
+
className: cn(
|
|
329
|
+
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
|
|
330
|
+
),
|
|
331
|
+
children: [
|
|
332
|
+
itemConfig?.icon && !hideIcon ? /* @__PURE__ */ jsxRuntime.jsx(itemConfig.icon, {}) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
333
|
+
"div",
|
|
334
|
+
{
|
|
335
|
+
className: "h-2 w-2 shrink-0 rounded-[2px]",
|
|
336
|
+
style: {
|
|
337
|
+
backgroundColor: item.color
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
),
|
|
341
|
+
itemConfig?.label
|
|
342
|
+
]
|
|
343
|
+
},
|
|
344
|
+
index
|
|
345
|
+
);
|
|
346
|
+
})
|
|
347
|
+
}
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
function getPayloadConfigFromPayload(config, payload, key) {
|
|
351
|
+
if (typeof payload !== "object" || payload === null) {
|
|
352
|
+
return void 0;
|
|
353
|
+
}
|
|
354
|
+
const payloadPayload = "payload" in payload && typeof payload.payload === "object" && payload.payload !== null ? payload.payload : void 0;
|
|
355
|
+
let configLabelKey = key;
|
|
356
|
+
if (key in payload && typeof payload[key] === "string") {
|
|
357
|
+
configLabelKey = payload[key];
|
|
358
|
+
} else if (payloadPayload && key in payloadPayload && typeof payloadPayload[key] === "string") {
|
|
359
|
+
configLabelKey = payloadPayload[key];
|
|
360
|
+
}
|
|
361
|
+
return configLabelKey in config ? config[configLabelKey] : config[key];
|
|
362
|
+
}
|
|
363
|
+
var AI_CHART_PALETTE = [
|
|
364
|
+
"var(--chart-1)",
|
|
365
|
+
"var(--chart-2)",
|
|
366
|
+
"var(--chart-3)",
|
|
367
|
+
"var(--chart-4)",
|
|
368
|
+
"var(--chart-5)"
|
|
369
|
+
];
|
|
370
|
+
function aiChartConfig(keys) {
|
|
371
|
+
const out = {};
|
|
372
|
+
keys.forEach((key, idx) => {
|
|
373
|
+
out[key] = {
|
|
374
|
+
label: key,
|
|
375
|
+
color: AI_CHART_PALETTE[idx % AI_CHART_PALETTE.length]
|
|
376
|
+
};
|
|
377
|
+
});
|
|
378
|
+
return out;
|
|
379
|
+
}
|
|
380
|
+
function sanitiseBlock(input) {
|
|
381
|
+
if (input.kind === "paragraph_brief") {
|
|
382
|
+
return {
|
|
383
|
+
kind: "paragraph_brief",
|
|
384
|
+
topic: input.topic,
|
|
385
|
+
key_facts: input.key_facts,
|
|
386
|
+
prose: ""
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
if (input.kind === "list") {
|
|
390
|
+
return { kind: "list", style: input.style, items: input.items, title: input.title };
|
|
391
|
+
}
|
|
392
|
+
if (input.kind === "chart") {
|
|
393
|
+
return { kind: "chart", title: input.title, spec: input.spec, data: input.data };
|
|
394
|
+
}
|
|
395
|
+
if (input.kind === "table") {
|
|
396
|
+
return {
|
|
397
|
+
kind: "table",
|
|
398
|
+
title: input.title,
|
|
399
|
+
columns: input.columns,
|
|
400
|
+
rows: input.rows
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
return { kind: "callout", tone: input.tone, text: input.text };
|
|
404
|
+
}
|
|
405
|
+
function AnswerBlocks({ blocks }) {
|
|
406
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-3", children: blocks.map((b, i) => /* @__PURE__ */ jsxRuntime.jsx(BlockView, { block: b }, i)) });
|
|
407
|
+
}
|
|
408
|
+
function BlockView({ block }) {
|
|
409
|
+
if (block.kind === "paragraph_brief") {
|
|
410
|
+
return /* @__PURE__ */ jsxRuntime.jsx("p", { className: "whitespace-pre-wrap text-sm leading-6 text-foreground", children: block.prose || /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-muted-foreground", children: [
|
|
411
|
+
block.key_facts.join(". "),
|
|
412
|
+
"\u2026"
|
|
413
|
+
] }) });
|
|
414
|
+
}
|
|
415
|
+
if (block.kind === "list") {
|
|
416
|
+
const Tag = block.style === "numbered" ? "ol" : "ul";
|
|
417
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
418
|
+
block.title && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mb-1 text-sm font-medium", children: block.title }),
|
|
419
|
+
/* @__PURE__ */ jsxRuntime.jsx(Tag, { className: block.style === "numbered" ? "list-decimal pl-6" : "list-disc pl-6", children: block.items.map((it, i) => /* @__PURE__ */ jsxRuntime.jsx("li", { className: "text-sm leading-6", children: it }, i)) })
|
|
420
|
+
] });
|
|
421
|
+
}
|
|
422
|
+
if (block.kind === "chart") {
|
|
423
|
+
return /* @__PURE__ */ jsxRuntime.jsx(AiChartBlock, { block });
|
|
424
|
+
}
|
|
425
|
+
if (block.kind === "table") {
|
|
426
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "overflow-x-auto rounded border border-border", children: [
|
|
427
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "border-b border-border px-3 py-2 text-sm font-medium", children: block.title }),
|
|
428
|
+
/* @__PURE__ */ jsxRuntime.jsxs("table", { className: "min-w-full text-sm", children: [
|
|
429
|
+
/* @__PURE__ */ jsxRuntime.jsx("thead", { className: "bg-muted/40", children: /* @__PURE__ */ jsxRuntime.jsx("tr", { children: block.columns.map((c) => /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-3 py-2 text-left font-medium text-muted-foreground", children: c }, c)) }) }),
|
|
430
|
+
/* @__PURE__ */ jsxRuntime.jsx("tbody", { children: block.rows.map((row, i) => /* @__PURE__ */ jsxRuntime.jsx("tr", { className: "border-t border-border", children: row.map((cell, j) => /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-3 py-2 text-foreground", children: cell == null ? "\u2014" : String(cell) }, j)) }, i)) })
|
|
431
|
+
] })
|
|
432
|
+
] });
|
|
433
|
+
}
|
|
434
|
+
if (block.kind === "callout") {
|
|
435
|
+
const cls = block.tone === "warn" ? "border-amber-300 dark:border-amber-800 bg-amber-50 dark:bg-amber-950/30 text-amber-900 dark:text-amber-100" : block.tone === "success" ? "border-emerald-300 dark:border-emerald-800 bg-emerald-50 dark:bg-emerald-950/30 text-emerald-900 dark:text-emerald-100" : "border-sky-300 bg-sky-50 text-sky-900";
|
|
436
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: `rounded border p-3 text-sm ${cls}`, children: block.text });
|
|
437
|
+
}
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
function AiChartBlock({ block }) {
|
|
441
|
+
const yKeys = Array.isArray(block.spec.y) ? block.spec.y : [block.spec.y];
|
|
442
|
+
if (block.spec.type === "pie") {
|
|
443
|
+
return /* @__PURE__ */ jsxRuntime.jsx(AiPieChart, { block, valueKey: yKeys[0] });
|
|
444
|
+
}
|
|
445
|
+
if (block.spec.type === "bar" || block.spec.type === "stacked_bar") {
|
|
446
|
+
return /* @__PURE__ */ jsxRuntime.jsx(AiBarChart, { block, yKeys, stacked: block.spec.type === "stacked_bar" });
|
|
447
|
+
}
|
|
448
|
+
return /* @__PURE__ */ jsxRuntime.jsx(AiLineChart, { block, yKeys });
|
|
449
|
+
}
|
|
450
|
+
function AiPieChart({
|
|
451
|
+
block,
|
|
452
|
+
valueKey
|
|
453
|
+
}) {
|
|
454
|
+
const config = React.useMemo(
|
|
455
|
+
() => aiChartConfig(block.data.map((row) => String(row[block.spec.x] ?? ""))),
|
|
456
|
+
[block.data, block.spec.x]
|
|
457
|
+
);
|
|
458
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: block.title, children: /* @__PURE__ */ jsxRuntime.jsx(ChartContainer, { config, className: "h-72 w-full", children: /* @__PURE__ */ jsxRuntime.jsxs(RechartsPrimitive.PieChart, { children: [
|
|
459
|
+
/* @__PURE__ */ jsxRuntime.jsx(ChartTooltip, { content: /* @__PURE__ */ jsxRuntime.jsx(ChartTooltipContent, { hideLabel: true }) }),
|
|
460
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
461
|
+
RechartsPrimitive.Pie,
|
|
462
|
+
{
|
|
463
|
+
data: block.data,
|
|
464
|
+
dataKey: valueKey,
|
|
465
|
+
nameKey: block.spec.x,
|
|
466
|
+
innerRadius: 60,
|
|
467
|
+
strokeWidth: 2,
|
|
468
|
+
children: [
|
|
469
|
+
block.data.map((row, idx) => {
|
|
470
|
+
const sliceKey = String(row[block.spec.x] ?? "");
|
|
471
|
+
return /* @__PURE__ */ jsxRuntime.jsx(RechartsPrimitive.Cell, { fill: `var(--color-${sliceKey})` }, sliceKey || idx);
|
|
472
|
+
}),
|
|
473
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
474
|
+
RechartsPrimitive.Label,
|
|
475
|
+
{
|
|
476
|
+
content: ({ viewBox }) => {
|
|
477
|
+
if (!viewBox || !("cx" in viewBox)) return null;
|
|
478
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
479
|
+
"text",
|
|
480
|
+
{
|
|
481
|
+
x: viewBox.cx,
|
|
482
|
+
y: viewBox.cy,
|
|
483
|
+
textAnchor: "middle",
|
|
484
|
+
dominantBaseline: "middle",
|
|
485
|
+
className: "fill-foreground text-sm font-medium",
|
|
486
|
+
children: valueKey
|
|
487
|
+
}
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
)
|
|
492
|
+
]
|
|
493
|
+
}
|
|
494
|
+
),
|
|
495
|
+
/* @__PURE__ */ jsxRuntime.jsx(ChartLegend, { content: /* @__PURE__ */ jsxRuntime.jsx(ChartLegendContent, { nameKey: block.spec.x }) })
|
|
496
|
+
] }) }) });
|
|
497
|
+
}
|
|
498
|
+
function AiBarChart({
|
|
499
|
+
block,
|
|
500
|
+
yKeys,
|
|
501
|
+
stacked
|
|
502
|
+
}) {
|
|
503
|
+
const config = React.useMemo(() => aiChartConfig(yKeys), [yKeys]);
|
|
504
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: block.title, children: /* @__PURE__ */ jsxRuntime.jsx(ChartContainer, { config, className: "h-72 w-full", children: /* @__PURE__ */ jsxRuntime.jsxs(RechartsPrimitive.BarChart, { data: block.data, margin: { left: 12, right: 12, top: 4 }, children: [
|
|
505
|
+
/* @__PURE__ */ jsxRuntime.jsx(RechartsPrimitive.CartesianGrid, { vertical: false, strokeDasharray: "3 3" }),
|
|
506
|
+
/* @__PURE__ */ jsxRuntime.jsx(RechartsPrimitive.XAxis, { dataKey: block.spec.x, tickLine: false, axisLine: false, tickMargin: 8 }),
|
|
507
|
+
/* @__PURE__ */ jsxRuntime.jsx(RechartsPrimitive.YAxis, { tickLine: false, axisLine: false, tickMargin: 8, width: 64 }),
|
|
508
|
+
/* @__PURE__ */ jsxRuntime.jsx(ChartTooltip, { content: /* @__PURE__ */ jsxRuntime.jsx(ChartTooltipContent, {}) }),
|
|
509
|
+
/* @__PURE__ */ jsxRuntime.jsx(ChartLegend, { content: /* @__PURE__ */ jsxRuntime.jsx(ChartLegendContent, {}) }),
|
|
510
|
+
yKeys.map((k) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
511
|
+
RechartsPrimitive.Bar,
|
|
512
|
+
{
|
|
513
|
+
dataKey: k,
|
|
514
|
+
stackId: stacked ? "stack" : void 0,
|
|
515
|
+
fill: `var(--color-${k})`,
|
|
516
|
+
radius: stacked ? [2, 2, 0, 0] : [4, 4, 0, 0]
|
|
517
|
+
},
|
|
518
|
+
k
|
|
519
|
+
))
|
|
520
|
+
] }) }) });
|
|
521
|
+
}
|
|
522
|
+
function AiLineChart({
|
|
523
|
+
block,
|
|
524
|
+
yKeys
|
|
525
|
+
}) {
|
|
526
|
+
const config = React.useMemo(() => aiChartConfig(yKeys), [yKeys]);
|
|
527
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: block.title, children: /* @__PURE__ */ jsxRuntime.jsx(ChartContainer, { config, className: "h-72 w-full", children: /* @__PURE__ */ jsxRuntime.jsxs(RechartsPrimitive.LineChart, { data: block.data, margin: { left: 12, right: 12, top: 4 }, children: [
|
|
528
|
+
/* @__PURE__ */ jsxRuntime.jsx(RechartsPrimitive.CartesianGrid, { vertical: false, strokeDasharray: "3 3" }),
|
|
529
|
+
/* @__PURE__ */ jsxRuntime.jsx(RechartsPrimitive.XAxis, { dataKey: block.spec.x, tickLine: false, axisLine: false, tickMargin: 8 }),
|
|
530
|
+
/* @__PURE__ */ jsxRuntime.jsx(RechartsPrimitive.YAxis, { tickLine: false, axisLine: false, tickMargin: 8, width: 64 }),
|
|
531
|
+
/* @__PURE__ */ jsxRuntime.jsx(ChartTooltip, { content: /* @__PURE__ */ jsxRuntime.jsx(ChartTooltipContent, {}) }),
|
|
532
|
+
/* @__PURE__ */ jsxRuntime.jsx(ChartLegend, { content: /* @__PURE__ */ jsxRuntime.jsx(ChartLegendContent, {}) }),
|
|
533
|
+
yKeys.map((k) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
534
|
+
RechartsPrimitive.Line,
|
|
535
|
+
{
|
|
536
|
+
type: "monotone",
|
|
537
|
+
dataKey: k,
|
|
538
|
+
stroke: `var(--color-${k})`,
|
|
539
|
+
strokeWidth: 2,
|
|
540
|
+
dot: false,
|
|
541
|
+
activeDot: { r: 4 }
|
|
542
|
+
},
|
|
543
|
+
k
|
|
544
|
+
))
|
|
545
|
+
] }) }) });
|
|
546
|
+
}
|
|
547
|
+
var PROVIDER_LABELS = {
|
|
548
|
+
claude: "Claude",
|
|
549
|
+
grok: "Grok",
|
|
550
|
+
gemini: "Gemini"
|
|
551
|
+
};
|
|
552
|
+
var TEXTAREA_MAX_PX = 176;
|
|
553
|
+
var PROVIDER_DESCRIPTIONS = {
|
|
554
|
+
claude: "Anthropic Claude on Vertex AI",
|
|
555
|
+
grok: "xAI Grok on Vertex AI",
|
|
556
|
+
gemini: "Google Gemini on Vertex AI"
|
|
557
|
+
};
|
|
558
|
+
function AiChat({
|
|
559
|
+
userFirstName,
|
|
560
|
+
scopeLabel,
|
|
561
|
+
initialProvider
|
|
562
|
+
}) {
|
|
563
|
+
const [sessions, setSessions] = React.useState([]);
|
|
564
|
+
const [activeSessionId, setActiveSessionId] = React.useState(null);
|
|
565
|
+
const [answers, setAnswers] = React.useState([]);
|
|
566
|
+
const [pending, setPending] = React.useState(false);
|
|
567
|
+
const [question, setQuestion] = React.useState("");
|
|
568
|
+
const [sidebarOpen, setSidebarOpen] = React.useState(false);
|
|
569
|
+
const [loadingSession, setLoadingSession] = React.useState(false);
|
|
570
|
+
const [provider, setProvider] = React.useState(initialProvider);
|
|
571
|
+
const [providerSaving, setProviderSaving] = React.useState(false);
|
|
572
|
+
const [editingSidebarId, setEditingSidebarId] = React.useState(null);
|
|
573
|
+
const abortRef = React.useRef(null);
|
|
574
|
+
const threadRef = React.useRef(null);
|
|
575
|
+
const textareaRef = React.useRef(null);
|
|
576
|
+
const lastAnswerRef = React.useRef(null);
|
|
577
|
+
const prevAnswersLen = React.useRef(0);
|
|
578
|
+
React.useLayoutEffect(() => {
|
|
579
|
+
const el = textareaRef.current;
|
|
580
|
+
if (!el) return;
|
|
581
|
+
el.style.height = "0px";
|
|
582
|
+
const next = Math.min(el.scrollHeight, TEXTAREA_MAX_PX);
|
|
583
|
+
el.style.height = `${next}px`;
|
|
584
|
+
}, [question]);
|
|
585
|
+
React.useEffect(() => {
|
|
586
|
+
let cancelled = false;
|
|
587
|
+
async function load() {
|
|
588
|
+
try {
|
|
589
|
+
const res = await fetch("/api/chat/sessions", { cache: "no-store" });
|
|
590
|
+
if (!res.ok) return;
|
|
591
|
+
const data = await res.json();
|
|
592
|
+
if (!cancelled) setSessions(data.sessions ?? []);
|
|
593
|
+
} catch {
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
void load();
|
|
597
|
+
return () => {
|
|
598
|
+
cancelled = true;
|
|
599
|
+
};
|
|
600
|
+
}, []);
|
|
601
|
+
React.useEffect(() => {
|
|
602
|
+
if (answers.length > prevAnswersLen.current && lastAnswerRef.current) {
|
|
603
|
+
lastAnswerRef.current.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
604
|
+
}
|
|
605
|
+
prevAnswersLen.current = answers.length;
|
|
606
|
+
}, [answers.length]);
|
|
607
|
+
const refreshSessions = React.useCallback(async () => {
|
|
608
|
+
try {
|
|
609
|
+
const res = await fetch("/api/chat/sessions", { cache: "no-store" });
|
|
610
|
+
if (!res.ok) return;
|
|
611
|
+
const data = await res.json();
|
|
612
|
+
setSessions(data.sessions ?? []);
|
|
613
|
+
} catch {
|
|
614
|
+
}
|
|
615
|
+
}, []);
|
|
616
|
+
const newChat = React.useCallback(() => {
|
|
617
|
+
setActiveSessionId(null);
|
|
618
|
+
setAnswers([]);
|
|
619
|
+
setQuestion("");
|
|
620
|
+
}, []);
|
|
621
|
+
const changeProvider = React.useCallback(
|
|
622
|
+
async (next) => {
|
|
623
|
+
if (next === provider || providerSaving) return;
|
|
624
|
+
setProvider(next);
|
|
625
|
+
setProviderSaving(true);
|
|
626
|
+
try {
|
|
627
|
+
const res = await fetch("/api/settings/me", {
|
|
628
|
+
method: "PATCH",
|
|
629
|
+
headers: { "Content-Type": "application/json" },
|
|
630
|
+
body: JSON.stringify({ narrative_provider: next })
|
|
631
|
+
});
|
|
632
|
+
if (!res.ok) {
|
|
633
|
+
setProvider((curr) => curr === next ? provider : curr);
|
|
634
|
+
}
|
|
635
|
+
} catch {
|
|
636
|
+
setProvider((curr) => curr === next ? provider : curr);
|
|
637
|
+
} finally {
|
|
638
|
+
setProviderSaving(false);
|
|
639
|
+
}
|
|
640
|
+
},
|
|
641
|
+
[provider, providerSaving]
|
|
642
|
+
);
|
|
643
|
+
const openSession = React.useCallback(async (id) => {
|
|
644
|
+
setLoadingSession(true);
|
|
645
|
+
setActiveSessionId(id);
|
|
646
|
+
try {
|
|
647
|
+
const res = await fetch(`/api/chat/sessions/${id}`, { cache: "no-store" });
|
|
648
|
+
if (!res.ok) {
|
|
649
|
+
setAnswers([]);
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
const data = await res.json();
|
|
653
|
+
setAnswers(messagesToAnswers(data.messages ?? []));
|
|
654
|
+
} catch {
|
|
655
|
+
setAnswers([]);
|
|
656
|
+
} finally {
|
|
657
|
+
setLoadingSession(false);
|
|
658
|
+
}
|
|
659
|
+
}, []);
|
|
660
|
+
const persistTitle = React.useCallback(
|
|
661
|
+
async (id, title) => {
|
|
662
|
+
const trimmed = title.trim();
|
|
663
|
+
if (!trimmed) return;
|
|
664
|
+
const res = await fetch(`/api/chat/sessions/${id}`, {
|
|
665
|
+
method: "PATCH",
|
|
666
|
+
headers: { "Content-Type": "application/json" },
|
|
667
|
+
body: JSON.stringify({ title: trimmed })
|
|
668
|
+
});
|
|
669
|
+
if (res.ok) {
|
|
670
|
+
setSessions(
|
|
671
|
+
(prev) => prev.map((s) => s.id === id ? { ...s, title: trimmed } : s)
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
},
|
|
675
|
+
[]
|
|
676
|
+
);
|
|
677
|
+
const startSidebarRename = React.useCallback((id) => {
|
|
678
|
+
setEditingSidebarId(id);
|
|
679
|
+
}, []);
|
|
680
|
+
const deleteSession = React.useCallback(
|
|
681
|
+
async (id) => {
|
|
682
|
+
if (!window.confirm("Delete this chat? This cannot be undone.")) return;
|
|
683
|
+
const res = await fetch(`/api/chat/sessions/${id}`, { method: "DELETE" });
|
|
684
|
+
if (!res.ok) return;
|
|
685
|
+
setSessions((prev) => prev.filter((s) => s.id !== id));
|
|
686
|
+
if (activeSessionId === id) {
|
|
687
|
+
setActiveSessionId(null);
|
|
688
|
+
setAnswers([]);
|
|
689
|
+
}
|
|
690
|
+
},
|
|
691
|
+
[activeSessionId]
|
|
692
|
+
);
|
|
693
|
+
const submit = React.useCallback(
|
|
694
|
+
async (q) => {
|
|
695
|
+
const trimmed = q.trim();
|
|
696
|
+
if (!trimmed || pending) return;
|
|
697
|
+
setPending(true);
|
|
698
|
+
setQuestion("");
|
|
699
|
+
let sessionId = activeSessionId;
|
|
700
|
+
if (sessionId == null) {
|
|
701
|
+
try {
|
|
702
|
+
const create = await fetch("/api/chat/sessions", {
|
|
703
|
+
method: "POST",
|
|
704
|
+
headers: { "Content-Type": "application/json" },
|
|
705
|
+
body: JSON.stringify({})
|
|
706
|
+
});
|
|
707
|
+
if (create.ok) {
|
|
708
|
+
const data = await create.json();
|
|
709
|
+
sessionId = data.session.id;
|
|
710
|
+
setActiveSessionId(sessionId);
|
|
711
|
+
setSessions((prev) => [
|
|
712
|
+
{ id: data.session.id, title: data.session.title, updatedAt: null },
|
|
713
|
+
...prev
|
|
714
|
+
]);
|
|
715
|
+
}
|
|
716
|
+
} catch {
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
setAnswers((prev) => [
|
|
720
|
+
...prev,
|
|
721
|
+
{ question: trimmed, blocks: [], done: false }
|
|
722
|
+
]);
|
|
723
|
+
const ac = new AbortController();
|
|
724
|
+
abortRef.current = ac;
|
|
725
|
+
let res;
|
|
726
|
+
try {
|
|
727
|
+
res = await fetch("/api/agent", {
|
|
728
|
+
method: "POST",
|
|
729
|
+
headers: { "Content-Type": "application/json" },
|
|
730
|
+
body: JSON.stringify({
|
|
731
|
+
question: trimmed,
|
|
732
|
+
chatSessionId: sessionId
|
|
733
|
+
}),
|
|
734
|
+
signal: ac.signal
|
|
735
|
+
});
|
|
736
|
+
} catch (e) {
|
|
737
|
+
const message = e.message ?? "Request failed";
|
|
738
|
+
setAnswers(
|
|
739
|
+
(prev) => updateLast(prev, (a) => ({
|
|
740
|
+
...a,
|
|
741
|
+
done: true,
|
|
742
|
+
error: { code: "NETWORK", message }
|
|
743
|
+
}))
|
|
744
|
+
);
|
|
745
|
+
setPending(false);
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
if (!res.body) {
|
|
749
|
+
setAnswers(
|
|
750
|
+
(prev) => updateLast(prev, (a) => ({
|
|
751
|
+
...a,
|
|
752
|
+
done: true,
|
|
753
|
+
error: { code: "NO_BODY", message: "No response stream." }
|
|
754
|
+
}))
|
|
755
|
+
);
|
|
756
|
+
setPending(false);
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
const reader = res.body.getReader();
|
|
760
|
+
const decoder = new TextDecoder();
|
|
761
|
+
let buffer = "";
|
|
762
|
+
try {
|
|
763
|
+
while (true) {
|
|
764
|
+
const { value, done } = await reader.read();
|
|
765
|
+
if (done) break;
|
|
766
|
+
buffer += decoder.decode(value, { stream: true });
|
|
767
|
+
const events = buffer.split("\n\n");
|
|
768
|
+
buffer = events.pop() ?? "";
|
|
769
|
+
for (const raw of events) {
|
|
770
|
+
handleEvent(raw, setAnswers);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
} catch (e) {
|
|
774
|
+
const message = e.message ?? "Stream interrupted";
|
|
775
|
+
setAnswers(
|
|
776
|
+
(prev) => updateLast(prev, (a) => ({
|
|
777
|
+
...a,
|
|
778
|
+
done: true,
|
|
779
|
+
error: { code: "STREAM", message }
|
|
780
|
+
}))
|
|
781
|
+
);
|
|
782
|
+
} finally {
|
|
783
|
+
setPending(false);
|
|
784
|
+
abortRef.current = null;
|
|
785
|
+
void refreshSessions();
|
|
786
|
+
}
|
|
787
|
+
},
|
|
788
|
+
[activeSessionId, pending, refreshSessions]
|
|
789
|
+
);
|
|
790
|
+
const heroVisible = answers.length === 0 && !loadingSession;
|
|
791
|
+
const greeting = React.useMemo(
|
|
792
|
+
() => `Hi ${userFirstName || "there"}${userFirstName.endsWith("s") ? "" : ""},`,
|
|
793
|
+
[userFirstName]
|
|
794
|
+
);
|
|
795
|
+
const activeTitle = activeSessionId ? sessions.find((s) => s.id === activeSessionId)?.title ?? "Chat" : "New chat";
|
|
796
|
+
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: [
|
|
797
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
798
|
+
"div",
|
|
799
|
+
{
|
|
800
|
+
onClick: () => setSidebarOpen(false),
|
|
801
|
+
"aria-hidden": "true",
|
|
802
|
+
className: cn(
|
|
803
|
+
"absolute inset-0 z-10 bg-background/60 backdrop-blur-sm transition-opacity duration-200",
|
|
804
|
+
sidebarOpen ? "opacity-100" : "pointer-events-none opacity-0"
|
|
805
|
+
)
|
|
806
|
+
}
|
|
807
|
+
),
|
|
808
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
809
|
+
"aside",
|
|
810
|
+
{
|
|
811
|
+
"aria-hidden": !sidebarOpen,
|
|
812
|
+
className: cn(
|
|
813
|
+
"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",
|
|
814
|
+
sidebarOpen ? "translate-x-0" : "-translate-x-full"
|
|
815
|
+
),
|
|
816
|
+
children: [
|
|
817
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 border-b border-sidebar-border p-2", children: [
|
|
818
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
819
|
+
"button",
|
|
820
|
+
{
|
|
821
|
+
type: "button",
|
|
822
|
+
onClick: () => setSidebarOpen(false),
|
|
823
|
+
className: "inline-flex size-8 items-center justify-center rounded-md text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-foreground",
|
|
824
|
+
"aria-label": "Close sidebar",
|
|
825
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.PanelLeftClose, { className: "size-4" })
|
|
826
|
+
}
|
|
827
|
+
),
|
|
828
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
829
|
+
"button",
|
|
830
|
+
{
|
|
831
|
+
type: "button",
|
|
832
|
+
onClick: () => {
|
|
833
|
+
newChat();
|
|
834
|
+
setSidebarOpen(false);
|
|
835
|
+
},
|
|
836
|
+
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",
|
|
837
|
+
children: [
|
|
838
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "size-3.5" }),
|
|
839
|
+
"New chat"
|
|
840
|
+
]
|
|
841
|
+
}
|
|
842
|
+
)
|
|
843
|
+
] }),
|
|
844
|
+
/* @__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) => {
|
|
845
|
+
const active = s.id === activeSessionId;
|
|
846
|
+
const editing = s.id === editingSidebarId;
|
|
847
|
+
if (editing) {
|
|
848
|
+
return /* @__PURE__ */ jsxRuntime.jsx("li", { className: "px-1 py-1", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
849
|
+
SidebarTitleEditor,
|
|
850
|
+
{
|
|
851
|
+
initial: s.title,
|
|
852
|
+
onCancel: () => setEditingSidebarId(null),
|
|
853
|
+
onSave: (next) => {
|
|
854
|
+
setEditingSidebarId(null);
|
|
855
|
+
void persistTitle(s.id, next);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
) }, s.id);
|
|
859
|
+
}
|
|
860
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "group relative", children: [
|
|
861
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
862
|
+
"button",
|
|
863
|
+
{
|
|
864
|
+
type: "button",
|
|
865
|
+
onClick: () => {
|
|
866
|
+
void openSession(s.id);
|
|
867
|
+
setSidebarOpen(false);
|
|
868
|
+
},
|
|
869
|
+
className: cn(
|
|
870
|
+
"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm",
|
|
871
|
+
active ? "bg-sidebar-accent text-sidebar-accent-foreground" : "text-sidebar-foreground/80 hover:bg-sidebar-accent/60 hover:text-sidebar-foreground"
|
|
872
|
+
),
|
|
873
|
+
title: s.title,
|
|
874
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: s.title })
|
|
875
|
+
}
|
|
876
|
+
),
|
|
877
|
+
/* @__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: [
|
|
878
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
879
|
+
"button",
|
|
880
|
+
{
|
|
881
|
+
type: "button",
|
|
882
|
+
onClick: (e) => {
|
|
883
|
+
e.stopPropagation();
|
|
884
|
+
startSidebarRename(s.id);
|
|
885
|
+
},
|
|
886
|
+
"aria-label": "Rename",
|
|
887
|
+
className: "inline-flex size-6 items-center justify-center rounded text-sidebar-foreground/60 hover:bg-sidebar hover:text-sidebar-foreground",
|
|
888
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Pencil, { className: "size-3" })
|
|
889
|
+
}
|
|
890
|
+
),
|
|
891
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
892
|
+
"button",
|
|
893
|
+
{
|
|
894
|
+
type: "button",
|
|
895
|
+
onClick: (e) => {
|
|
896
|
+
e.stopPropagation();
|
|
897
|
+
void deleteSession(s.id);
|
|
898
|
+
},
|
|
899
|
+
"aria-label": "Delete",
|
|
900
|
+
className: "inline-flex size-6 items-center justify-center rounded text-sidebar-foreground/60 hover:bg-sidebar hover:text-destructive",
|
|
901
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2, { className: "size-3" })
|
|
902
|
+
}
|
|
903
|
+
)
|
|
904
|
+
] })
|
|
905
|
+
] }, s.id);
|
|
906
|
+
}) }) })
|
|
907
|
+
]
|
|
908
|
+
}
|
|
909
|
+
),
|
|
910
|
+
/* @__PURE__ */ jsxRuntime.jsxs("section", { className: "relative flex flex-1 flex-col overflow-hidden", children: [
|
|
911
|
+
/* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex items-center gap-2 px-3 py-3 text-sm text-muted-foreground", children: [
|
|
912
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
913
|
+
"button",
|
|
914
|
+
{
|
|
915
|
+
type: "button",
|
|
916
|
+
onClick: () => setSidebarOpen(true),
|
|
917
|
+
"aria-label": "Open chat history",
|
|
918
|
+
className: "inline-flex size-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground",
|
|
919
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Menu, { className: "size-4" })
|
|
920
|
+
}
|
|
921
|
+
),
|
|
922
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Sparkles, { className: "size-4 text-primary" }),
|
|
923
|
+
activeSessionId != null ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
924
|
+
EditableTitle,
|
|
925
|
+
{
|
|
926
|
+
title: activeTitle,
|
|
927
|
+
onSave: (next) => void persistTitle(activeSessionId, next)
|
|
928
|
+
},
|
|
929
|
+
activeSessionId
|
|
930
|
+
) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: activeTitle })
|
|
931
|
+
] }),
|
|
932
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
933
|
+
"div",
|
|
934
|
+
{
|
|
935
|
+
ref: threadRef,
|
|
936
|
+
className: "min-h-0 flex-1 overflow-y-auto px-4 pb-6 pt-2",
|
|
937
|
+
children: loadingSession ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex h-full items-center justify-center text-sm text-muted-foreground", children: [
|
|
938
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "mr-2 size-4 animate-spin" }),
|
|
939
|
+
" Loading conversation\u2026"
|
|
940
|
+
] }) : heroVisible ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex h-full flex-col items-center justify-center text-center", children: [
|
|
941
|
+
/* @__PURE__ */ jsxRuntime.jsx("h1", { className: "text-2xl font-medium tracking-tight text-foreground sm:text-3xl", children: greeting }),
|
|
942
|
+
/* @__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?" })
|
|
943
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto flex w-full max-w-3xl flex-col gap-6", children: [
|
|
944
|
+
answers.map((a, idx) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
945
|
+
AnswerView,
|
|
946
|
+
{
|
|
947
|
+
answer: a,
|
|
948
|
+
onRetry: () => void submit(a.question),
|
|
949
|
+
forwardRef: idx === answers.length - 1 ? lastAnswerRef : void 0,
|
|
950
|
+
isLast: idx === answers.length - 1
|
|
951
|
+
},
|
|
952
|
+
idx
|
|
953
|
+
)),
|
|
954
|
+
pending && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-sm text-muted-foreground", children: [
|
|
955
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "mr-1 inline size-3.5 animate-spin" }),
|
|
956
|
+
"Thinking\u2026"
|
|
957
|
+
] })
|
|
958
|
+
] })
|
|
959
|
+
}
|
|
960
|
+
),
|
|
961
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "shrink-0 px-4 pb-4 pt-2", children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
962
|
+
"form",
|
|
963
|
+
{
|
|
964
|
+
className: "mx-auto w-full max-w-3xl",
|
|
965
|
+
onSubmit: (e) => {
|
|
966
|
+
e.preventDefault();
|
|
967
|
+
void submit(question);
|
|
968
|
+
},
|
|
969
|
+
children: [
|
|
970
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-2 rounded-3xl border border-border bg-background px-4 py-3 shadow-sm", children: [
|
|
971
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
972
|
+
"textarea",
|
|
973
|
+
{
|
|
974
|
+
ref: textareaRef,
|
|
975
|
+
value: question,
|
|
976
|
+
onChange: (e) => setQuestion(e.target.value),
|
|
977
|
+
onKeyDown: (e) => {
|
|
978
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
979
|
+
e.preventDefault();
|
|
980
|
+
void submit(question);
|
|
981
|
+
}
|
|
982
|
+
},
|
|
983
|
+
placeholder: "Ask AI...",
|
|
984
|
+
rows: 1,
|
|
985
|
+
disabled: pending,
|
|
986
|
+
style: { maxHeight: TEXTAREA_MAX_PX },
|
|
987
|
+
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"
|
|
988
|
+
}
|
|
989
|
+
),
|
|
990
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-2", children: [
|
|
991
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
992
|
+
"span",
|
|
993
|
+
{
|
|
994
|
+
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",
|
|
995
|
+
title: `Scope: ${scopeLabel}`,
|
|
996
|
+
children: [
|
|
997
|
+
"Scope: ",
|
|
998
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate text-foreground", children: scopeLabel })
|
|
999
|
+
]
|
|
1000
|
+
}
|
|
1001
|
+
),
|
|
1002
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
1003
|
+
/* @__PURE__ */ jsxRuntime.jsxs(DropdownMenu, { children: [
|
|
1004
|
+
/* @__PURE__ */ jsxRuntime.jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1005
|
+
"button",
|
|
1006
|
+
{
|
|
1007
|
+
type: "button",
|
|
1008
|
+
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",
|
|
1009
|
+
"aria-label": "Choose model",
|
|
1010
|
+
children: [
|
|
1011
|
+
PROVIDER_LABELS[provider],
|
|
1012
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronDown, { className: "size-3.5" })
|
|
1013
|
+
]
|
|
1014
|
+
}
|
|
1015
|
+
) }),
|
|
1016
|
+
/* @__PURE__ */ jsxRuntime.jsx(DropdownMenuContent, { align: "end", className: "w-56", children: ["claude", "grok", "gemini"].map((p) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1017
|
+
DropdownMenuItem,
|
|
1018
|
+
{
|
|
1019
|
+
onSelect: () => void changeProvider(p),
|
|
1020
|
+
className: "flex items-start gap-2",
|
|
1021
|
+
children: [
|
|
1022
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1023
|
+
lucideReact.Check,
|
|
1024
|
+
{
|
|
1025
|
+
className: cn(
|
|
1026
|
+
"mt-0.5 size-4",
|
|
1027
|
+
provider === p ? "opacity-100" : "opacity-0"
|
|
1028
|
+
)
|
|
1029
|
+
}
|
|
1030
|
+
),
|
|
1031
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col", children: [
|
|
1032
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium", children: PROVIDER_LABELS[p] }),
|
|
1033
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground", children: PROVIDER_DESCRIPTIONS[p] })
|
|
1034
|
+
] })
|
|
1035
|
+
]
|
|
1036
|
+
},
|
|
1037
|
+
p
|
|
1038
|
+
)) })
|
|
1039
|
+
] }),
|
|
1040
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1041
|
+
"button",
|
|
1042
|
+
{
|
|
1043
|
+
type: "submit",
|
|
1044
|
+
disabled: pending || !question.trim(),
|
|
1045
|
+
"aria-label": "Send",
|
|
1046
|
+
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",
|
|
1047
|
+
children: pending ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "size-4 animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ArrowUp, { className: "size-4" })
|
|
1048
|
+
}
|
|
1049
|
+
)
|
|
1050
|
+
] })
|
|
1051
|
+
] })
|
|
1052
|
+
] }),
|
|
1053
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "mt-2 text-center text-xs text-muted-foreground", children: [
|
|
1054
|
+
PROVIDER_LABELS[provider],
|
|
1055
|
+
" is AI and can make mistakes."
|
|
1056
|
+
] })
|
|
1057
|
+
]
|
|
1058
|
+
}
|
|
1059
|
+
) })
|
|
1060
|
+
] })
|
|
1061
|
+
] });
|
|
1062
|
+
}
|
|
1063
|
+
function AnswerView({
|
|
1064
|
+
answer,
|
|
1065
|
+
onRetry,
|
|
1066
|
+
forwardRef,
|
|
1067
|
+
isLast
|
|
1068
|
+
}) {
|
|
1069
|
+
const [copied, setCopied] = React.useState(false);
|
|
1070
|
+
const handleCopy = React.useCallback(async () => {
|
|
1071
|
+
const blockText = blocksToPlainText(answer.blocks);
|
|
1072
|
+
const text = blockText || (answer.error ? `${answer.error.code}: ${answer.error.message}` : "");
|
|
1073
|
+
if (!text) return;
|
|
1074
|
+
try {
|
|
1075
|
+
await navigator.clipboard.writeText(text);
|
|
1076
|
+
setCopied(true);
|
|
1077
|
+
window.setTimeout(() => setCopied(false), 1500);
|
|
1078
|
+
} catch {
|
|
1079
|
+
}
|
|
1080
|
+
}, [answer.blocks, answer.error]);
|
|
1081
|
+
const showActions = answer.done && (answer.blocks.length > 0 || answer.error != null);
|
|
1082
|
+
const isThinking = !answer.done && answer.blocks.length === 0 && !answer.error;
|
|
1083
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1084
|
+
"div",
|
|
1085
|
+
{
|
|
1086
|
+
ref: forwardRef,
|
|
1087
|
+
className: cn(
|
|
1088
|
+
"flex flex-col gap-4 scroll-mt-2",
|
|
1089
|
+
// Reserve space below the latest turn so the AI response has a
|
|
1090
|
+
// full screen to fill without bouncing the user. Calc subtracts
|
|
1091
|
+
// the chat header (~3rem) and the input pill area (~10rem) from
|
|
1092
|
+
// the dynamic viewport height.
|
|
1093
|
+
isLast && "min-h-[calc(100dvh-13rem)]"
|
|
1094
|
+
),
|
|
1095
|
+
children: [
|
|
1096
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsxRuntime.jsx(UserChip, { text: answer.question }) }),
|
|
1097
|
+
isThinking ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
|
|
1098
|
+
/* @__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" }) }),
|
|
1099
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: "Thinking\u2026" })
|
|
1100
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-3", children: [
|
|
1101
|
+
/* @__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" }) }),
|
|
1102
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-3", children: [
|
|
1103
|
+
/* @__PURE__ */ jsxRuntime.jsx(AnswerBlocks, { blocks: answer.blocks }),
|
|
1104
|
+
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: [
|
|
1105
|
+
answer.error.code,
|
|
1106
|
+
": ",
|
|
1107
|
+
answer.error.message
|
|
1108
|
+
] }),
|
|
1109
|
+
showActions && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
|
|
1110
|
+
(answer.blocks.length > 0 || answer.error != null) && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1111
|
+
"button",
|
|
1112
|
+
{
|
|
1113
|
+
type: "button",
|
|
1114
|
+
onClick: handleCopy,
|
|
1115
|
+
"aria-label": answer.blocks.length === 0 && answer.error ? "Copy error" : "Copy response",
|
|
1116
|
+
title: copied ? "Copied" : "Copy",
|
|
1117
|
+
className: "inline-flex size-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground",
|
|
1118
|
+
children: copied ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { className: "size-4" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Copy, { className: "size-4" })
|
|
1119
|
+
}
|
|
1120
|
+
),
|
|
1121
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1122
|
+
"button",
|
|
1123
|
+
{
|
|
1124
|
+
type: "button",
|
|
1125
|
+
onClick: onRetry,
|
|
1126
|
+
"aria-label": "Retry",
|
|
1127
|
+
title: "Retry",
|
|
1128
|
+
className: "inline-flex size-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground",
|
|
1129
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RotateCcw, { className: "size-4" })
|
|
1130
|
+
}
|
|
1131
|
+
)
|
|
1132
|
+
] })
|
|
1133
|
+
] })
|
|
1134
|
+
] })
|
|
1135
|
+
]
|
|
1136
|
+
}
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
function SidebarTitleEditor({
|
|
1140
|
+
initial,
|
|
1141
|
+
onSave,
|
|
1142
|
+
onCancel
|
|
1143
|
+
}) {
|
|
1144
|
+
const [draft, setDraft] = React.useState(initial);
|
|
1145
|
+
const commit = () => {
|
|
1146
|
+
const trimmed = draft.trim();
|
|
1147
|
+
if (trimmed && trimmed !== initial) onSave(trimmed);
|
|
1148
|
+
else onCancel();
|
|
1149
|
+
};
|
|
1150
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1151
|
+
"input",
|
|
1152
|
+
{
|
|
1153
|
+
autoFocus: true,
|
|
1154
|
+
value: draft,
|
|
1155
|
+
onChange: (e) => setDraft(e.target.value),
|
|
1156
|
+
onFocus: (e) => e.currentTarget.select(),
|
|
1157
|
+
onClick: (e) => e.stopPropagation(),
|
|
1158
|
+
onBlur: commit,
|
|
1159
|
+
onKeyDown: (e) => {
|
|
1160
|
+
if (e.key === "Enter") {
|
|
1161
|
+
e.preventDefault();
|
|
1162
|
+
commit();
|
|
1163
|
+
} else if (e.key === "Escape") {
|
|
1164
|
+
e.preventDefault();
|
|
1165
|
+
onCancel();
|
|
1166
|
+
}
|
|
1167
|
+
},
|
|
1168
|
+
maxLength: 200,
|
|
1169
|
+
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"
|
|
1170
|
+
}
|
|
1171
|
+
);
|
|
1172
|
+
}
|
|
1173
|
+
function EditableTitle({
|
|
1174
|
+
title,
|
|
1175
|
+
onSave
|
|
1176
|
+
}) {
|
|
1177
|
+
const [editing, setEditing] = React.useState(false);
|
|
1178
|
+
const [draft, setDraft] = React.useState(title);
|
|
1179
|
+
React.useEffect(() => {
|
|
1180
|
+
if (!editing) setDraft(title);
|
|
1181
|
+
}, [title, editing]);
|
|
1182
|
+
if (!editing) {
|
|
1183
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1184
|
+
"button",
|
|
1185
|
+
{
|
|
1186
|
+
type: "button",
|
|
1187
|
+
onClick: () => {
|
|
1188
|
+
setDraft(title);
|
|
1189
|
+
setEditing(true);
|
|
1190
|
+
},
|
|
1191
|
+
className: "w-64 max-w-full min-w-0 truncate rounded px-2 py-0.5 text-left hover:bg-accent hover:text-foreground",
|
|
1192
|
+
title: "Click to rename",
|
|
1193
|
+
children: title
|
|
1194
|
+
}
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
const commit = () => {
|
|
1198
|
+
const trimmed = draft.trim();
|
|
1199
|
+
setEditing(false);
|
|
1200
|
+
if (trimmed && trimmed !== title) onSave(trimmed);
|
|
1201
|
+
};
|
|
1202
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1203
|
+
"input",
|
|
1204
|
+
{
|
|
1205
|
+
autoFocus: true,
|
|
1206
|
+
value: draft,
|
|
1207
|
+
onChange: (e) => setDraft(e.target.value),
|
|
1208
|
+
onFocus: (e) => e.currentTarget.select(),
|
|
1209
|
+
onBlur: commit,
|
|
1210
|
+
onKeyDown: (e) => {
|
|
1211
|
+
if (e.key === "Enter") {
|
|
1212
|
+
e.preventDefault();
|
|
1213
|
+
commit();
|
|
1214
|
+
} else if (e.key === "Escape") {
|
|
1215
|
+
e.preventDefault();
|
|
1216
|
+
setEditing(false);
|
|
1217
|
+
setDraft(title);
|
|
1218
|
+
}
|
|
1219
|
+
},
|
|
1220
|
+
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",
|
|
1221
|
+
maxLength: 200
|
|
1222
|
+
}
|
|
1223
|
+
);
|
|
1224
|
+
}
|
|
1225
|
+
function blocksToPlainText(blocks) {
|
|
1226
|
+
return blocks.map((b) => {
|
|
1227
|
+
if (b.kind === "paragraph_brief") {
|
|
1228
|
+
return b.prose || b.key_facts.join(". ");
|
|
1229
|
+
}
|
|
1230
|
+
if (b.kind === "list") {
|
|
1231
|
+
const lines = b.items.map(
|
|
1232
|
+
(it, i) => b.style === "numbered" ? `${i + 1}. ${it}` : `- ${it}`
|
|
1233
|
+
);
|
|
1234
|
+
return [b.title, ...lines].filter(Boolean).join("\n");
|
|
1235
|
+
}
|
|
1236
|
+
if (b.kind === "chart") {
|
|
1237
|
+
return `${b.title} (chart)`;
|
|
1238
|
+
}
|
|
1239
|
+
if (b.kind === "table") {
|
|
1240
|
+
const header = b.columns.join(" ");
|
|
1241
|
+
const rows = b.rows.map(
|
|
1242
|
+
(r) => r.map((c) => c == null ? "\u2014" : String(c)).join(" ")
|
|
1243
|
+
);
|
|
1244
|
+
return [b.title, header, ...rows].join("\n");
|
|
1245
|
+
}
|
|
1246
|
+
if (b.kind === "callout") return b.text;
|
|
1247
|
+
return "";
|
|
1248
|
+
}).filter(Boolean).join("\n\n");
|
|
1249
|
+
}
|
|
1250
|
+
function UserChip({ text }) {
|
|
1251
|
+
const [expanded, setExpanded] = React.useState(false);
|
|
1252
|
+
const [overflowing, setOverflowing] = React.useState(false);
|
|
1253
|
+
const ref = React.useRef(null);
|
|
1254
|
+
React.useLayoutEffect(() => {
|
|
1255
|
+
const el = ref.current;
|
|
1256
|
+
if (!el || expanded) return;
|
|
1257
|
+
setOverflowing(el.scrollHeight > el.clientHeight + 1);
|
|
1258
|
+
}, [text, expanded]);
|
|
1259
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "max-w-[85%] rounded-2xl bg-muted px-4 py-2 text-sm text-foreground", children: [
|
|
1260
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
|
|
1261
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1262
|
+
"div",
|
|
1263
|
+
{
|
|
1264
|
+
ref,
|
|
1265
|
+
className: cn(
|
|
1266
|
+
"whitespace-pre-wrap wrap-break-word leading-6",
|
|
1267
|
+
// 3 lines × leading-6 (24px) = 72px → max-h-18.
|
|
1268
|
+
!expanded && "max-h-18 overflow-hidden"
|
|
1269
|
+
),
|
|
1270
|
+
children: text
|
|
1271
|
+
}
|
|
1272
|
+
),
|
|
1273
|
+
!expanded && overflowing && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1274
|
+
"div",
|
|
1275
|
+
{
|
|
1276
|
+
"aria-hidden": "true",
|
|
1277
|
+
className: "pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-linear-to-t from-muted to-transparent"
|
|
1278
|
+
}
|
|
1279
|
+
)
|
|
1280
|
+
] }),
|
|
1281
|
+
(overflowing || expanded) && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1282
|
+
"button",
|
|
1283
|
+
{
|
|
1284
|
+
type: "button",
|
|
1285
|
+
onClick: () => setExpanded((v) => !v),
|
|
1286
|
+
className: "mt-1 inline-flex items-center gap-1 text-xs font-medium text-muted-foreground hover:text-foreground",
|
|
1287
|
+
children: expanded ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1288
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronUp, { className: "size-3.5" }),
|
|
1289
|
+
"Show less"
|
|
1290
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1291
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronDown, { className: "size-3.5" }),
|
|
1292
|
+
"Show more"
|
|
1293
|
+
] })
|
|
1294
|
+
}
|
|
1295
|
+
)
|
|
1296
|
+
] });
|
|
1297
|
+
}
|
|
1298
|
+
function handleEvent(raw, setAnswers) {
|
|
1299
|
+
const lines = raw.split("\n");
|
|
1300
|
+
let event = "";
|
|
1301
|
+
let dataStr = "";
|
|
1302
|
+
for (const line of lines) {
|
|
1303
|
+
if (line.startsWith("event: ")) event = line.slice(7).trim();
|
|
1304
|
+
else if (line.startsWith("data: ")) dataStr += line.slice(6);
|
|
1305
|
+
}
|
|
1306
|
+
if (!event) return;
|
|
1307
|
+
let data = {};
|
|
1308
|
+
try {
|
|
1309
|
+
data = JSON.parse(dataStr || "{}");
|
|
1310
|
+
} catch {
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
if (event === "block") {
|
|
1314
|
+
const blockData = data;
|
|
1315
|
+
const block = sanitiseBlock(blockData);
|
|
1316
|
+
setAnswers(
|
|
1317
|
+
(prev) => updateLast(prev, (a) => {
|
|
1318
|
+
const blocks = [...a.blocks];
|
|
1319
|
+
blocks[blockData.index] = block;
|
|
1320
|
+
return { ...a, blocks };
|
|
1321
|
+
})
|
|
1322
|
+
);
|
|
1323
|
+
} else if (event === "prose") {
|
|
1324
|
+
const { block_index, delta } = data;
|
|
1325
|
+
setAnswers(
|
|
1326
|
+
(prev) => updateLast(prev, (a) => {
|
|
1327
|
+
const blocks = [...a.blocks];
|
|
1328
|
+
const target = blocks[block_index];
|
|
1329
|
+
if (target && target.kind === "paragraph_brief") {
|
|
1330
|
+
blocks[block_index] = { ...target, prose: target.prose + delta };
|
|
1331
|
+
}
|
|
1332
|
+
return { ...a, blocks };
|
|
1333
|
+
})
|
|
1334
|
+
);
|
|
1335
|
+
} else if (event === "done") {
|
|
1336
|
+
setAnswers((prev) => updateLast(prev, (a) => ({ ...a, done: true })));
|
|
1337
|
+
} else if (event === "error") {
|
|
1338
|
+
setAnswers(
|
|
1339
|
+
(prev) => updateLast(prev, (a) => ({
|
|
1340
|
+
...a,
|
|
1341
|
+
error: data
|
|
1342
|
+
}))
|
|
1343
|
+
);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
function updateLast(prev, updater) {
|
|
1347
|
+
if (prev.length === 0) return prev;
|
|
1348
|
+
const next = [...prev];
|
|
1349
|
+
next[next.length - 1] = updater(next[next.length - 1]);
|
|
1350
|
+
return next;
|
|
1351
|
+
}
|
|
1352
|
+
function messagesToAnswers(messages) {
|
|
1353
|
+
const out = [];
|
|
1354
|
+
let pendingUser = null;
|
|
1355
|
+
for (const m of messages) {
|
|
1356
|
+
if (m.role === "user") {
|
|
1357
|
+
pendingUser = m.question ?? "";
|
|
1358
|
+
} else if (m.role === "assistant") {
|
|
1359
|
+
const question = pendingUser ?? "";
|
|
1360
|
+
pendingUser = null;
|
|
1361
|
+
const blocks = [];
|
|
1362
|
+
const stored = m.blocks ?? [];
|
|
1363
|
+
stored.forEach((b, i) => {
|
|
1364
|
+
const sanitised = sanitiseBlock({ ...b});
|
|
1365
|
+
if (sanitised.kind === "paragraph_brief") {
|
|
1366
|
+
sanitised.prose = m.prose?.[String(i)] ?? "";
|
|
1367
|
+
}
|
|
1368
|
+
blocks[i] = sanitised;
|
|
1369
|
+
});
|
|
1370
|
+
out.push({
|
|
1371
|
+
question,
|
|
1372
|
+
blocks,
|
|
1373
|
+
done: true,
|
|
1374
|
+
error: m.errorJson ?? void 0
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
if (pendingUser !== null) {
|
|
1379
|
+
out.push({ question: pendingUser, blocks: [], done: true });
|
|
1380
|
+
}
|
|
1381
|
+
return out;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
exports.AiChat = AiChat;
|
|
1385
|
+
exports.AnswerBlocks = AnswerBlocks;
|
|
1386
|
+
exports.sanitiseBlock = sanitiseBlock;
|
|
1387
|
+
//# sourceMappingURL=index.cjs.map
|
|
1388
|
+
//# sourceMappingURL=index.cjs.map
|