"use client"; /** * TASK-0124 — "Generate column with formula" dialog. * * Flow: the user types a description of the column → the dialog calls the * formula endpoint (JSON, non-streamed — `dbAiApi.generateFormula`). The * engine VALIDATES the candidate expression (parse + type-check + evaluate * on the database's own first ≤5 rows) BEFORE saving; the response carries * the validated `expression` + `returnType` + the `samples` (recordId → * value/error) so the UI previews the evaluated values. * * NOTE on "Save": the server SAVES the formula as part of the endpoint (the * expression is engine-validated before the write, so the UI never has to * confirm an unvalidated expression — the preview IS the saved formula). The * dialog therefore shows the expression + the sample evaluation table and * closes; the new FORMULA column appears in the database (the query cache is * invalidated). A `422 formula_invalid` (after the server's retry-once) shows * the parse error and the formula is NOT written. * * Pinned testids (tests/e2e/specs/ai-database.spec.ts — do not rename): * db-formula the dialog * db-formula-desc the description textarea * db-formula-generate the Generate button * db-formula-expression the validated expression * db-formula-table the sample evaluation table * db-formula-done the success summary * db-ai-error-banner / db-ai-loading (shared chrome) */ import { useCallback, useEffect, useRef, useState } from "react"; import { useQueryClient } from "@tanstack/react-query"; import { CheckCheck, Loader2, Sigma } from "lucide-react"; import { dbAiApi, displayStoredValue, type DbFormulaResult } from "@/lib/ai-database"; import { ApiError } from "@/lib/api"; import { recordTitle, type DatabaseProperty, type RecordDto } from "@/hooks/use-database"; import { DbAiDialog, DbAiErrorBanner, DbAiLoading } from "@/components/ai/db-ai-dialog"; export interface AiFormulaDialogProps { open: boolean; workspaceId: string; databaseId: string; /** The database's properties (to resolve the FORMULA column + display). */ properties: DatabaseProperty[]; /** The loaded record rows (for the sample table's row labels). */ records?: RecordDto[]; onClose: () => void; } const MAX_DESC_CHARS = 500; export function AiFormulaDialog({ open, workspaceId, databaseId, properties, records, onClose }: AiFormulaDialogProps) { const qc = useQueryClient(); const [description, setDescription] = useState(""); const [loading, setLoading] = useState(false); const [error, setError] = useState<{ code: string; message: string; parseError?: string } | null>(null); const [result, setResult] = useState(null); const lastDesc = useRef(""); useEffect(() => { if (open) { setDescription(""); setResult(null); setError(null); setLoading(false); } }, [open]); const recordById = useRef(new Map()); recordById.current = new Map((records ?? []).map((r) => [r.id, { title: recordTitle(r, properties) }])); const propertyById = useRef(new Map()); propertyById.current = new Map(properties.map((p) => [p.id, p])); const invalid = description.trim().length === 0; const run = useCallback( async (desc: string) => { lastDesc.current = desc; setLoading(true); setError(null); setResult(null); try { const res = await dbAiApi.generateFormula(workspaceId, { databaseId, description: desc }); setResult(res); void qc.invalidateQueries({ queryKey: ["database", databaseId] }); } catch (err) { if (err instanceof ApiError) { const detail = (err.details as Array> | undefined)?.[0]; setError({ code: err.code, message: err.message, ...(typeof detail?.parseError === "string" ? { parseError: detail.parseError } : {}) }); } else { setError({ code: "NETWORK_ERROR", message: "Could not reach the server." }); } } finally { setLoading(false); } }, [workspaceId, databaseId, qc] ); const generate = useCallback(() => { void run(description.trim()); }, [run, description]); const retry = useCallback(() => { void run(lastDesc.current); }, [run]); const close = useCallback(() => { onClose(); }, [onClose]); const formulaProp = result ? propertyById.current.get(result.propertyId) : undefined; const budget = error?.code === "AI_BUDGET_EXCEEDED" || error?.code === "AI_BUDGET_MONTHLY_EXCEEDED"; const invalidStructure = error?.code === "UNPROCESSABLE_ENTITY"; const live = loading ? "AI is generating a formula" : result ? `Formula generated: ${result.expression}` : error ? `Error: ${error.message}` : ""; return ( {result ? "Formula validated and saved" : "Validated by the formula engine before it is saved"} } >