@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.
@@ -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