1"use client";
2
3
19import { ApiError } from "./api";
20import { apiFetch } from "./api";
21import { AiPreStreamError, parseSseFrames } from "./ai";
22
23
24
25
26export type DbAiSseFrame =
27 | { type: "delta"; text: string }
28 | { type: "tool_call"; id: string; name: string; arguments: Record<string, unknown> }
29 | { type: "usage"; tokensIn: number; tokensOut: number }
30 | { type: "error"; code: string; message: string; reason?: string; retryable?: boolean }
31 | {
32 type: "done";
33 assistantMessageId?: string;
34 reason?: "ok" | "aborted" | "error";
35 databaseId?: string;
36 propertyIds?: string[];
37 recordIds?: string[];
38 ignoredColumns?: string[];
39 };
40
41export interface DbAiResult {
42 reason: "ok" | "aborted" | "error";
43
44 done?: DbAiSseFrame;
45 usage?: { tokensIn: number; tokensOut: number };
46
47 errorFrame?: { code: string; message: string; reason?: string };
48}
49
50
51
52async function streamDb(
53 path: string,
54 body: Record<string, unknown>,
55 signal: AbortSignal | undefined
56): Promise<DbAiResult> {
57 const { authStore } = await import("./auth-store");
58 const { API_BASE } = await import("./constants");
59 const token = authStore.accessToken;
60
61 const res = await fetch(`${API_BASE}${path}`, {
62 method: "POST",
63 headers: {
64 "Content-Type": "application/json",
65 Accept: "text/event-stream",
66 ...(token ? { Authorization: `Bearer ${token}` } : {})
67 },
68 body: JSON.stringify(body),
69 signal,
70 credentials: "include"
71 });
72
73 if (!res.ok || !res.body) {
74 let err: ApiError;
75 try {
76 const json = (await res.json()) as { error?: { code: string; message: string; details?: unknown } };
77 err = new ApiError(json.error?.code ?? "UNKNOWN", res.status, json.error?.message ?? "Request failed", json.error?.details);
78 } catch {
79 err = new ApiError("UNKNOWN", res.status, "Request failed");
80 }
81 throw new AiPreStreamError(err);
82 }
83
84 const reader = res.body.getReader();
85 const decoder = new TextDecoder();
86 let buf = "";
87 let reason: "ok" | "aborted" | "error" = "error";
88 let done: DbAiSseFrame | undefined;
89 let usage: { tokensIn: number; tokensOut: number } | undefined;
90 let errorFrame: DbAiResult["errorFrame"];
91
92 const handle = (frame: DbAiSseFrame): void => {
93 if (frame.type === "usage") usage = { tokensIn: frame.tokensIn, tokensOut: frame.tokensOut };
94 else if (frame.type === "done") {
95 done = frame;
96 reason = frame.reason === "aborted" ? "aborted" : frame.reason === "error" ? "error" : "ok";
97 } else if (frame.type === "error") {
98 reason = "error";
99 errorFrame = { code: frame.code, message: frame.message, ...(frame.reason ? { reason: frame.reason } : {}) };
100 }
101 };
102
103 try {
104 for (;;) {
105 const chunk = await reader.read();
106 if (chunk.done) break;
107 buf += decoder.decode(chunk.value, { stream: true });
108
109
110
111 const { frames, rest } = parseSseFrames(buf) as unknown as { frames: DbAiSseFrame[]; rest: string };
112 buf = rest;
113 for (const f of frames) handle(f);
114 }
115 const { frames } = parseSseFrames(buf + "\n\n") as unknown as { frames: DbAiSseFrame[]; rest: string };
116 for (const f of frames) handle(f);
117 } catch (err) {
118 if (err instanceof Error && err.name === "AbortError") reason = "aborted";
119 else throw err;
120 } finally {
121 try {
122 reader.releaseLock();
123 } catch {
124
125 }
126 }
127 return { reason, ...(done ? { done } : {}), ...(usage ? { usage } : {}), ...(errorFrame ? { errorFrame } : {}) };
128}
129
130
131
132export async function sendDbGenerate(params: {
133 workspaceId: string;
134 prompt: string;
135 parentId?: string;
136 signal?: AbortSignal;
137}): Promise<DbAiResult> {
138 const body: Record<string, unknown> = { prompt: params.prompt };
139 if (params.parentId) body.parentId = params.parentId;
140 return streamDb(`/workspaces/${params.workspaceId}/ai/databases/generate`, body, params.signal);
141}
142
143
144
145export async function sendDbExtract(params: {
146 workspaceId: string;
147 databaseId: string;
148 text: string;
149 signal?: AbortSignal;
150}): Promise<DbAiResult> {
151 return streamDb(
152 `/workspaces/${params.workspaceId}/ai/databases/extract`,
153 { databaseId: params.databaseId, text: params.text },
154 params.signal
155 );
156}
157
158
159
160
161export interface DbExtractPreview {
162 databaseId: string;
163 isTable: boolean;
164 columns: string[];
165 mapped: Array<{ column: string; propertyId: string }>;
166 ignored: string[];
167 rowCount: number;
168}
169
170
171export interface DbFormulaSample {
172 recordId: string;
173 value: unknown;
174 error?: string;
175}
176
177
178export interface DbFormulaResult {
179 formulaId: string;
180 propertyId: string;
181 expression: string;
182 returnType: string;
183 explanation?: string;
184 samples: DbFormulaSample[];
185 tokensIn: number;
186 tokensOut: number;
187}
188
189export const dbAiApi = {
190
191 previewExtract(
192 workspaceId: string,
193 body: { databaseId: string; text: string },
194 signal?: AbortSignal
195 ): Promise<DbExtractPreview> {
196 return apiFetch<DbExtractPreview>(`/workspaces/${workspaceId}/ai/databases/extract/preview`, {
197 method: "POST",
198 body,
199 signal
200 });
201 },
202
204 generateFormula(
205 workspaceId: string,
206 body: { databaseId: string; description: string; name?: string },
207 signal?: AbortSignal
208 ): Promise<DbFormulaResult> {
209 return apiFetch<DbFormulaResult>(`/workspaces/${workspaceId}/ai/databases/formulas`, {
210 method: "POST",
211 body,
212 signal
213 });
214 }
215};
216
217
218
219
220export function displayStoredValue(
221 value: unknown,
222 options?: Array<{ id: string; name: string }>
223): string {
224 if (value == null) return "—";
225 if (typeof value === "string") return value;
226 if (typeof value === "number" || typeof value === "boolean") return String(value);
227 const v = value as Record<string, unknown>;
228 if (typeof v.text === "string") return v.text;
229 if (v.value !== undefined) return String(v.value);
230 if (v.optionId !== undefined) {
231
232 const ref = v.optionId;
233 const opt = options?.find((o) => o.id === ref) ?? options?.find((o) => o.name === ref);
234 return opt?.name ?? String(ref);
235 }
236 if (v.start !== undefined) return String(v.start);
237 return JSON.stringify(value);
238}
239