@campfire-interactive/ai-chat 0.0.0-stage → 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/README.md CHANGED
@@ -1,3 +1,76 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # `@campfire-interactive/ai-chat`
2
+
3
+ > **Private and unvetted until the pm exercise.** Do not adopt it in an app yet;
4
+ > see [CLAUDE.md](./CLAUDE.md).
5
+
6
+ The platform AI chat: an icon in the header opens a docked panel beside the
7
+ page. The chat knows which page is open, offers chips that fit it, answers with
8
+ typed blocks (prose, tables, charts, KPI tiles, links, buttons, questions,
9
+ citations), and proposes small changes as cards the user confirms. It talks to
10
+ the hub's `/api/v1/assistant/*` API directly with the user's token.
11
+
12
+ A live playground with a scripted hub is the showcase's **AI chat** page
13
+ (`npm run showcase`, then `#ai-chat`).
14
+
15
+ ## Entries
16
+
17
+ | Entry | What it holds |
18
+ |---|---|
19
+ | `.` | The wire-contract types and `createAssistantClient` (`chatStream`, `confirmWrite`, `listActions`). No UI. |
20
+ | `/host` | `AiChatProvider`, `AiChatSurface`, `AiChatButton`, `useAiChat`. |
21
+
22
+ Peers: `react` / `react-dom` 18 or 19, `lucide-react` (>= 0.460), `recharts`
23
+ 2.15+ or 3. **No CSS import:** the stylesheet (`cfi-ac-*`, in
24
+ `@layer components`) is injected by the bundle. Charts load lazily.
25
+
26
+ ## Wiring a host (pm)
27
+
28
+ ```tsx
29
+ import { AiChatProvider, AiChatSurface, AiChatButton } from '@campfire-interactive/ai-chat/host';
30
+
31
+ // App.tsx: above <Routes>, beside <PmHelp>.
32
+ <AiChatProvider
33
+ app="pm"
34
+ hubUrl={aiEnabled ? runtime.aiHubBaseUrl : undefined} // undefined = chat off
35
+ getToken={getAccessToken}
36
+ uploadFile={uploadToDms} // pm's dms-client upload, as WorkAttachments does; returns { documentId }
37
+ pageContext={usePmPageContext()} // rebuilt on every navigation
38
+ starters={PM_STARTERS} // { 'program.risks': ['Which risks are overdue?'], … }
39
+ navigate={(link) => (link.app === 'pm' ? navigate(link.path) : (window.location.href = getAppUrl(link.app) + link.path))}
40
+ >
41
+ <Routes>…</Routes>
42
+ </AiChatProvider>
43
+
44
+ // AppShell.tsx
45
+ <ShellHeader rightActions={<AiChatButton />} … />
46
+ <div className="flex flex-1 overflow-hidden">
47
+ <SideNav … />
48
+ <main className="min-w-0 flex-1 overflow-auto">…</main>
49
+ <CampfireHelpSurfaces />
50
+ <AiChatSurface />
51
+ </div>
52
+ ```
53
+
54
+ `AiChatButton` and `AiChatSurface` render nothing when the provider has no
55
+ `hubUrl`, so a host gates only the provider.
56
+
57
+ ## Behavior worth knowing
58
+
59
+ - **Session.** The thread lives in React state; navigation and closing the panel
60
+ keep it. A reload ends it. "Start over" clears the thread, the dropped files
61
+ and the session id.
62
+ - **History.** Each request sends the last 8 turns as text. Blocks are reduced
63
+ to one-line traces; a confirmed write appends
64
+ `Confirmed: <summary> (succeeded|failed)` to that answer.
65
+ - **Starters** are keyed by `pageContext.page` (`'*'` is the fallback).
66
+ `{placeholders}` fill from the page's entities by `type`, then from `filters`,
67
+ then `{title}`. A fully filled chip sends; otherwise it loads into the composer.
68
+ They show in the empty state, and again above the composer once the page
69
+ differs from where the last question was asked.
70
+ - **Files** are kept in memory under a `fileRef` and offered on every request
71
+ until attached. `uploadFile` runs only when the user confirms an attach card,
72
+ immediately before `confirmWrite({ token, documentId })`.
73
+ - **Writes.** Cancel, or any new message, discards every pending write. Nothing
74
+ is stored, so nothing needs cleaning up.
75
+ - **Panel open state and width** (360 to 900 px) are remembered per browser in
76
+ `localStorage` under `cfi.ai-chat.<app>.*`.
@@ -0,0 +1,210 @@
1
+ import {
2
+ formatTick,
3
+ formatValue
4
+ } from "./chunk-2MWBMQMX.js";
5
+
6
+ // src/blocks/ChartBlock.tsx
7
+ import {
8
+ Area,
9
+ AreaChart,
10
+ Bar,
11
+ BarChart,
12
+ CartesianGrid,
13
+ Cell,
14
+ Legend,
15
+ Line,
16
+ LineChart,
17
+ ReferenceLine,
18
+ ResponsiveContainer,
19
+ Scatter,
20
+ ScatterChart,
21
+ Tooltip,
22
+ XAxis,
23
+ YAxis
24
+ } from "recharts";
25
+ import { jsx, jsxs } from "react/jsx-runtime";
26
+ var palette = (i) => `var(--cfi-chart-${i % 8 + 1})`;
27
+ var HEIGHT = 260;
28
+ var LONG_LABEL = 10;
29
+ var LABEL_MAX = 24;
30
+ var shorten = (v) => {
31
+ const text = String(v ?? "");
32
+ return text.length > LABEL_MAX ? `${text.slice(0, LABEL_MAX - 1)}\u2026` : text;
33
+ };
34
+ var axisProps = {
35
+ tick: { fontSize: 11, fill: "var(--cfi-chart-label, #6b7280)" },
36
+ stroke: "var(--cfi-chart-axis, #d1d5db)"
37
+ };
38
+ var tooltipStyle = {
39
+ contentStyle: {
40
+ background: "var(--cfi-surface-raised, #fff)",
41
+ border: "1px solid var(--cfi-border, #e5e7eb)",
42
+ borderRadius: "var(--cfi-radius-md, 6px)",
43
+ fontSize: 12,
44
+ color: "var(--cfi-content, #111827)"
45
+ }
46
+ };
47
+ function ChartBlock({ block }) {
48
+ const fmt = (v) => formatValue(typeof v === "number" || typeof v === "string" ? v : null, block.valueFormat ?? "number");
49
+ const tick = (v) => formatTick(v, block.valueFormat);
50
+ const showLegend = block.series.length > 1;
51
+ const color = (i) => showLegend ? palette(i) : "var(--cfi-ac-action)";
52
+ const sideways = block.chartType === "bar" && block.data.some((row) => String(row[block.xKey] ?? "").length > LONG_LABEL);
53
+ const chartHeight = sideways ? Math.max(140, block.data.length * 36 + 56) : HEIGHT;
54
+ const allowDecimals = block.data.some((row) => block.series.some((s) => {
55
+ const v = row[s.key];
56
+ return typeof v === "number" && !Number.isInteger(v);
57
+ }));
58
+ const top = Math.max(0, ...block.data.flatMap((row) => block.series.map((s) => Number(row[s.key]) || 0)));
59
+ const tickCount = allowDecimals ? void 0 : Math.max(2, Math.min(5, Math.ceil(top) + 1));
60
+ const grid = /* @__PURE__ */ jsx(CartesianGrid, { strokeDasharray: "3 3", stroke: "var(--cfi-chart-grid, #e5e7eb)", vertical: false });
61
+ const crowded = block.data.length > 4;
62
+ const x = /* @__PURE__ */ jsx(
63
+ XAxis,
64
+ {
65
+ dataKey: block.xKey,
66
+ ...axisProps,
67
+ interval: 0,
68
+ angle: crowded ? -30 : 0,
69
+ textAnchor: crowded ? "end" : "middle",
70
+ height: crowded ? 64 : 32,
71
+ label: block.xLabel ? { value: block.xLabel, position: "insideBottom", offset: 0, fontSize: 11 } : void 0
72
+ }
73
+ );
74
+ const y = /* @__PURE__ */ jsx(
75
+ YAxis,
76
+ {
77
+ ...axisProps,
78
+ width: 56,
79
+ tickFormatter: tick,
80
+ allowDecimals,
81
+ tickCount,
82
+ label: block.yLabel ? { value: block.yLabel, angle: -90, position: "insideLeft", fontSize: 11 } : void 0
83
+ }
84
+ );
85
+ const tooltip = /* @__PURE__ */ jsx(Tooltip, { ...tooltipStyle, formatter: (v) => fmt(v) });
86
+ let chart;
87
+ switch (block.chartType) {
88
+ case "line":
89
+ chart = /* @__PURE__ */ jsxs(LineChart, { accessibilityLayer: false, data: block.data, margin: { top: 8, right: 12, bottom: 8, left: 0 }, children: [
90
+ grid,
91
+ x,
92
+ y,
93
+ tooltip,
94
+ showLegend && /* @__PURE__ */ jsx(Legend, { verticalAlign: "top", wrapperStyle: { fontSize: 12, paddingBottom: 4 } }),
95
+ block.series.map((s, i) => /* @__PURE__ */ jsx(Line, { type: "monotone", dataKey: s.key, name: s.label, stroke: color(i), strokeWidth: 2, dot: false, connectNulls: true }, s.key))
96
+ ] });
97
+ break;
98
+ case "area":
99
+ chart = /* @__PURE__ */ jsxs(AreaChart, { accessibilityLayer: false, data: block.data, margin: { top: 8, right: 12, bottom: 8, left: 0 }, children: [
100
+ grid,
101
+ x,
102
+ y,
103
+ tooltip,
104
+ showLegend && /* @__PURE__ */ jsx(Legend, { verticalAlign: "top", wrapperStyle: { fontSize: 12, paddingBottom: 4 } }),
105
+ block.series.map((s, i) => /* @__PURE__ */ jsx(Area, { type: "monotone", dataKey: s.key, name: s.label, stroke: color(i), fill: color(i), fillOpacity: 0.18, strokeWidth: 2, connectNulls: true }, s.key))
106
+ ] });
107
+ break;
108
+ case "scatter":
109
+ chart = /* @__PURE__ */ jsxs(ScatterChart, { accessibilityLayer: false, margin: { top: 8, right: 12, bottom: 8, left: 0 }, children: [
110
+ grid,
111
+ /* @__PURE__ */ jsx(XAxis, { type: "number", dataKey: block.xKey, name: block.xLabel ?? block.xKey, ...axisProps, tickFormatter: (v) => formatTick(v, void 0) }),
112
+ /* @__PURE__ */ jsx(YAxis, { type: "number", ...axisProps, width: 56, tickFormatter: tick }),
113
+ tooltip,
114
+ showLegend && /* @__PURE__ */ jsx(Legend, { verticalAlign: "top", wrapperStyle: { fontSize: 12, paddingBottom: 4 } }),
115
+ block.series.map((s, i) => /* @__PURE__ */ jsx(
116
+ Scatter,
117
+ {
118
+ name: s.label,
119
+ data: block.data.map((row) => ({ [block.xKey]: row[block.xKey], y: row[s.key] })),
120
+ dataKey: "y",
121
+ fill: color(i)
122
+ },
123
+ s.key
124
+ ))
125
+ ] });
126
+ break;
127
+ case "waterfall": {
128
+ const key = block.series[0]?.key;
129
+ let running = 0;
130
+ const shaped = block.data.map((row, i) => {
131
+ const value = Number(row[key ?? ""] ?? 0) || 0;
132
+ const isTotal = i === block.data.length - 1;
133
+ const base = isTotal ? Math.min(0, value) : Math.min(running, running + value);
134
+ if (!isTotal) running += value;
135
+ return {
136
+ [block.xKey]: row[block.xKey],
137
+ base,
138
+ bar: Math.abs(value),
139
+ raw: value,
140
+ fill: isTotal ? color(0) : value >= 0 ? "var(--cfi-color-success, #10b981)" : "var(--cfi-color-error, #ef4444)"
141
+ };
142
+ });
143
+ chart = /* @__PURE__ */ jsxs(BarChart, { accessibilityLayer: false, data: shaped, margin: { top: 8, right: 12, bottom: 8, left: 0 }, children: [
144
+ grid,
145
+ x,
146
+ y,
147
+ /* @__PURE__ */ jsx(
148
+ Tooltip,
149
+ {
150
+ ...tooltipStyle,
151
+ formatter: ((_v, name, item) => name === "base" ? [null, null] : [fmt(item?.payload?.raw ?? 0), block.series[0]?.label ?? ""])
152
+ }
153
+ ),
154
+ /* @__PURE__ */ jsx(ReferenceLine, { y: 0, stroke: "var(--cfi-chart-axis, #d1d5db)" }),
155
+ /* @__PURE__ */ jsx(Bar, { dataKey: "base", stackId: "w", fill: "transparent", isAnimationActive: false }),
156
+ /* @__PURE__ */ jsx(Bar, { dataKey: "bar", stackId: "w", radius: [3, 3, 0, 0], children: shaped.map((entry, i) => /* @__PURE__ */ jsx(Cell, { fill: entry.fill }, i)) })
157
+ ] });
158
+ break;
159
+ }
160
+ case "bar":
161
+ default:
162
+ if (sideways) {
163
+ chart = /* @__PURE__ */ jsxs(BarChart, { accessibilityLayer: false, data: block.data, layout: "vertical", margin: { top: 8, right: 16, bottom: 8, left: 0 }, children: [
164
+ /* @__PURE__ */ jsx(CartesianGrid, { strokeDasharray: "3 3", stroke: "var(--cfi-chart-grid, #e5e7eb)", horizontal: false }),
165
+ /* @__PURE__ */ jsx(
166
+ XAxis,
167
+ {
168
+ type: "number",
169
+ ...axisProps,
170
+ tickFormatter: tick,
171
+ allowDecimals,
172
+ tickCount,
173
+ label: block.yLabel ? { value: block.yLabel, position: "insideBottom", offset: -4, fontSize: 11 } : void 0
174
+ }
175
+ ),
176
+ /* @__PURE__ */ jsx(YAxis, { type: "category", dataKey: block.xKey, ...axisProps, width: 150, interval: 0, tickFormatter: shorten }),
177
+ tooltip,
178
+ showLegend && /* @__PURE__ */ jsx(Legend, { verticalAlign: "top", wrapperStyle: { fontSize: 12, paddingBottom: 4 } }),
179
+ block.series.map((s, i) => /* @__PURE__ */ jsx(Bar, { dataKey: s.key, name: s.label, fill: color(i), radius: [0, 3, 3, 0], maxBarSize: 28 }, s.key))
180
+ ] });
181
+ break;
182
+ }
183
+ chart = /* @__PURE__ */ jsxs(BarChart, { accessibilityLayer: false, data: block.data, margin: { top: 8, right: 12, bottom: 8, left: 0 }, children: [
184
+ grid,
185
+ x,
186
+ y,
187
+ tooltip,
188
+ showLegend && /* @__PURE__ */ jsx(Legend, { verticalAlign: "top", wrapperStyle: { fontSize: 12, paddingBottom: 4 } }),
189
+ block.series.map((s, i) => /* @__PURE__ */ jsx(Bar, { dataKey: s.key, name: s.label, fill: color(i), radius: [3, 3, 0, 0] }, s.key))
190
+ ] });
191
+ }
192
+ return /* @__PURE__ */ jsxs("figure", { className: "cfi-ac-block cfi-ac-chart", children: [
193
+ block.title && /* @__PURE__ */ jsx("figcaption", { className: "cfi-ac-block__title", children: block.title }),
194
+ /* @__PURE__ */ jsxs("table", { className: "cfi-ac-sr-only", children: [
195
+ /* @__PURE__ */ jsx("caption", { children: block.title ?? "Chart data" }),
196
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
197
+ /* @__PURE__ */ jsx("th", { scope: "col", children: block.xLabel ?? block.xKey }),
198
+ block.series.map((s) => /* @__PURE__ */ jsx("th", { scope: "col", children: s.label }, s.key))
199
+ ] }) }),
200
+ /* @__PURE__ */ jsx("tbody", { children: block.data.map((row, i) => /* @__PURE__ */ jsxs("tr", { children: [
201
+ /* @__PURE__ */ jsx("th", { scope: "row", children: String(row[block.xKey] ?? "") }),
202
+ block.series.map((s) => /* @__PURE__ */ jsx("td", { children: fmt(row[s.key]) }, s.key))
203
+ ] }, i)) })
204
+ ] }),
205
+ /* @__PURE__ */ jsx("div", { className: "cfi-ac-chart__frame", style: { height: chartHeight }, "aria-hidden": true, children: /* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: "100%", children: chart }) })
206
+ ] });
207
+ }
208
+ export {
209
+ ChartBlock as default
210
+ };
@@ -0,0 +1,91 @@
1
+ // src/blocks/format.ts
2
+ function formatValue(value, type, currency = "USD") {
3
+ if (value === null || value === void 0 || value === "") return "\u2014";
4
+ if (typeof value === "boolean") return value ? "Yes" : "No";
5
+ switch (type) {
6
+ case "number":
7
+ return typeof value === "number" ? value.toLocaleString(void 0, { maximumFractionDigits: 2 }) : String(value);
8
+ case "money":
9
+ if (typeof value !== "number") return String(value);
10
+ try {
11
+ return value.toLocaleString(void 0, {
12
+ style: "currency",
13
+ currency,
14
+ minimumFractionDigits: 2,
15
+ maximumFractionDigits: 4
16
+ });
17
+ } catch {
18
+ return value.toLocaleString(void 0, { minimumFractionDigits: 2, maximumFractionDigits: 4 });
19
+ }
20
+ case "percent":
21
+ return typeof value === "number" ? `${value.toLocaleString(void 0, { maximumFractionDigits: 1 })}%` : String(value);
22
+ case "date":
23
+ return formatDate(String(value));
24
+ default:
25
+ return String(value);
26
+ }
27
+ }
28
+ var DATE_ONLY = /^(\d{4})-(\d{2})-(\d{2})$/;
29
+ function formatDate(value) {
30
+ const m = DATE_ONLY.exec(value);
31
+ const date = m ? new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3])) : new Date(value);
32
+ if (Number.isNaN(date.getTime())) return value;
33
+ return date.toLocaleDateString(void 0, { year: "numeric", month: "short", day: "numeric" });
34
+ }
35
+ function formatTick(value, format, currency = "USD") {
36
+ if (typeof value !== "number") return String(value ?? "");
37
+ const compact = value.toLocaleString(void 0, { notation: "compact", maximumFractionDigits: 1 });
38
+ if (format === "percent") return `${compact}%`;
39
+ if (format === "money") {
40
+ try {
41
+ return value.toLocaleString(void 0, { style: "currency", currency, notation: "compact", maximumFractionDigits: 1 });
42
+ } catch {
43
+ return compact;
44
+ }
45
+ }
46
+ return compact;
47
+ }
48
+ function compareCells(a, b, type) {
49
+ const blankA = a === null || a === void 0 || a === "";
50
+ const blankB = b === null || b === void 0 || b === "";
51
+ if (blankA || blankB) return blankA === blankB ? 0 : blankA ? 1 : -1;
52
+ if (typeof a === "number" && typeof b === "number") return a - b;
53
+ if (type === "date") {
54
+ const ta = Date.parse(String(a));
55
+ const tb = Date.parse(String(b));
56
+ if (!Number.isNaN(ta) && !Number.isNaN(tb)) return ta - tb;
57
+ }
58
+ return String(a).localeCompare(String(b), void 0, { numeric: true, sensitivity: "base" });
59
+ }
60
+ function safeUrl(url) {
61
+ const trimmed = url.trim();
62
+ if (trimmed.startsWith("/") || trimmed.startsWith("#")) return trimmed;
63
+ try {
64
+ const parsed = new URL(trimmed);
65
+ return parsed.protocol === "https:" || parsed.protocol === "http:" ? trimmed : void 0;
66
+ } catch {
67
+ return void 0;
68
+ }
69
+ }
70
+ function statusLabel(value) {
71
+ const text = String(value ?? "");
72
+ if (!/^[a-z0-9]+([_-][a-z0-9]+)*$/.test(text)) return text;
73
+ const words = text.replace(/[_-]+/g, " ");
74
+ return words.charAt(0).toUpperCase() + words.slice(1);
75
+ }
76
+ function statusTone(value) {
77
+ const v = String(value ?? "").toLowerCase();
78
+ if (/(done|complete|closed|approved|on track|green|passed|resolved)/.test(v)) return "good";
79
+ if (/(overdue|blocked|late|red|failed|rejected|critical|off track)/.test(v)) return "bad";
80
+ if (/(at risk|amber|yellow|pending|waiting|in review|warning)/.test(v)) return "warn";
81
+ return "neutral";
82
+ }
83
+
84
+ export {
85
+ formatValue,
86
+ formatTick,
87
+ compareCells,
88
+ safeUrl,
89
+ statusLabel,
90
+ statusTone
91
+ };
@@ -0,0 +1,133 @@
1
+ // src/client/sse.ts
2
+ var EVENT_TYPES = /* @__PURE__ */ new Set(["text", "step", "block", "write_pending", "done", "error"]);
3
+ var SseParser = class {
4
+ constructor() {
5
+ this.buffer = "";
6
+ this.decoder = new TextDecoder();
7
+ }
8
+ /** Feed a chunk; returns every event completed by it. */
9
+ push(chunk) {
10
+ this.buffer += typeof chunk === "string" ? chunk : this.decoder.decode(chunk, { stream: true });
11
+ const events = [];
12
+ let newline = this.buffer.search(/\r\n|\n|\r/);
13
+ while (newline !== -1) {
14
+ if (this.buffer[newline] === "\r" && newline === this.buffer.length - 1) break;
15
+ const line = this.buffer.slice(0, newline);
16
+ const width = this.buffer.startsWith("\r\n", newline) ? 2 : 1;
17
+ this.buffer = this.buffer.slice(newline + width);
18
+ const event = parseLine(line);
19
+ if (event) events.push(event);
20
+ newline = this.buffer.search(/\r\n|\n|\r/);
21
+ }
22
+ return events;
23
+ }
24
+ /** The stream ended: parse whatever is left as a last line. */
25
+ flush() {
26
+ const rest = this.buffer + this.decoder.decode();
27
+ this.buffer = "";
28
+ const event = parseLine(rest.replace(/\r$/, ""));
29
+ return event ? [event] : [];
30
+ }
31
+ };
32
+ function parseLine(line) {
33
+ if (!line.startsWith("data:")) return null;
34
+ const payload = line.slice(5).replace(/^ /, "");
35
+ if (!payload) return null;
36
+ try {
37
+ const value = JSON.parse(payload);
38
+ if (value && typeof value === "object" && typeof value.type === "string" && EVENT_TYPES.has(value.type)) {
39
+ return value;
40
+ }
41
+ } catch {
42
+ }
43
+ return null;
44
+ }
45
+ async function* readEvents(body) {
46
+ const parser = new SseParser();
47
+ const reader = body.getReader();
48
+ let finished = false;
49
+ try {
50
+ for (; ; ) {
51
+ const { done, value } = await reader.read();
52
+ if (done) break;
53
+ for (const event of parser.push(value)) yield event;
54
+ }
55
+ finished = true;
56
+ for (const event of parser.flush()) yield event;
57
+ } finally {
58
+ if (!finished) await reader.cancel().catch(() => void 0);
59
+ reader.releaseLock();
60
+ }
61
+ }
62
+
63
+ // src/client/client.ts
64
+ var AssistantHttpError = class extends Error {
65
+ constructor(status, message) {
66
+ super(message);
67
+ this.status = status;
68
+ this.name = "AssistantHttpError";
69
+ }
70
+ };
71
+ function createAssistantClient({ hubUrl, getToken, fetch: fetchImpl }) {
72
+ const base = `${hubUrl.replace(/\/+$/, "")}/api/v1/assistant`;
73
+ const doFetch = (input, init) => (fetchImpl ?? globalThis.fetch)(input, init);
74
+ const headers = async (extra) => {
75
+ const token = await getToken();
76
+ return { ...token ? { Authorization: `Bearer ${token}` } : {}, ...extra };
77
+ };
78
+ const json = async (res) => {
79
+ if (!res.ok) throw await httpError(res);
80
+ return await res.json();
81
+ };
82
+ return {
83
+ async *chatStream(req, opts) {
84
+ const res = await doFetch(`${base}/chat/stream`, {
85
+ method: "POST",
86
+ headers: await headers({ "Content-Type": "application/json", Accept: "text/event-stream" }),
87
+ body: JSON.stringify(req),
88
+ signal: opts?.signal
89
+ });
90
+ if (!res.ok) throw await httpError(res);
91
+ if (!res.body) throw new AssistantHttpError(res.status, "The answer arrived empty.");
92
+ yield* readEvents(res.body);
93
+ },
94
+ async confirmWrite(req) {
95
+ const res = await doFetch(`${base}/writes/confirm`, {
96
+ method: "POST",
97
+ headers: await headers({ "Content-Type": "application/json" }),
98
+ body: JSON.stringify(req)
99
+ });
100
+ return json(res);
101
+ },
102
+ async listActions(query) {
103
+ const params = new URLSearchParams();
104
+ if (query.sessionId) params.set("session_id", query.sessionId);
105
+ if (query.entityId) params.set("entity_id", query.entityId);
106
+ if (query.userId) params.set("user_id", query.userId);
107
+ if (query.limit != null) params.set("limit", String(query.limit));
108
+ const qs = params.toString();
109
+ const res = await doFetch(`${base}/actions${qs ? `?${qs}` : ""}`, { headers: await headers() });
110
+ return json(res);
111
+ }
112
+ };
113
+ }
114
+ async function httpError(res) {
115
+ let message = "";
116
+ try {
117
+ const body = await res.json();
118
+ const candidate = body.message ?? body.detail ?? body.error;
119
+ if (typeof candidate === "string") message = candidate;
120
+ } catch {
121
+ }
122
+ if (!message) {
123
+ message = res.status === 401 || res.status === 403 ? "AI chat is not available for your account." : "AI chat could not answer right now. Try again in a moment.";
124
+ }
125
+ return new AssistantHttpError(res.status, message);
126
+ }
127
+
128
+ export {
129
+ SseParser,
130
+ readEvents,
131
+ AssistantHttpError,
132
+ createAssistantClient
133
+ };