1"use client";
2
3
21import { useCallback, useEffect, useRef, useState } from "react";
22import { useRouter } from "next/navigation";
23import { useQueryClient } from "@tanstack/react-query";
24import { CheckCheck, Loader2, Sparkles } from "lucide-react";
25import { useDbAiStream } from "@/hooks/use-db-ai-stream";
26import { DbAiDialog, DbAiErrorBanner, DbAiLoading } from "@/components/ai/db-ai-dialog";
27
28export interface AiGenerateDialogProps {
29 open: boolean;
30 workspaceId: string;
31
32 workspaceSlug?: string;
33 onClose: () => void;
34}
35
36export function AiGenerateDialog({ open, workspaceId, workspaceSlug, onClose }: AiGenerateDialogProps) {
37 const qc = useQueryClient();
38 const router = useRouter();
39 const [prompt, setPrompt] = useState("");
40 const { phase, result, error, runGenerate } = useDbAiStream({ workspaceId });
41 const lastPrompt = useRef("");
42
43
44 useEffect(() => {
45 if (open) setPrompt("");
46 }, [open]);
47
48 const invalid = prompt.trim().length === 0;
49
50 const generate = useCallback(() => {
51 lastPrompt.current = prompt.trim();
52 void runGenerate(prompt.trim());
53 }, [prompt, runGenerate]);
54
55 const retry = useCallback(() => {
56 void runGenerate(lastPrompt.current);
57 }, [runGenerate]);
58
59 const close = useCallback(() => {
60 onClose();
61 }, [onClose]);
62
63 const done = phase === "done" && result?.done;
64 const databaseId = done ? (result!.done as { databaseId?: string }).databaseId : undefined;
65 const propCount = done ? (result!.done as { propertyIds?: string[] }).propertyIds?.length ?? 0 : 0;
66 const recordCount = done ? (result!.done as { recordIds?: string[] }).recordIds?.length ?? 0 : 0;
67
68 const invalidateTree = useCallback(() => {
69 void qc.invalidateQueries({ queryKey: ["workspaces", workspaceId, "page-tree"] });
70 }, [qc, workspaceId]);
71
72
73 useEffect(() => {
74 if (phase === "done" && databaseId) invalidateTree();
75 }, [phase, databaseId, invalidateTree]);
76
77 const budget = error?.budget;
78 const live =
79 phase === "streaming"
80 ? "AI is generating a database"
81 : done
82 ? `Database generated: ${propCount} properties, ${recordCount} sample records`
83 : error
84 ? `Error: ${error.message}`
85 : "";
86
87 return (
88 <DbAiDialog
89 open={open}
90 title="Generate database"
91 testId="db-ai-generate"
92 onClose={close}
93 live={live}
94 footer={
95 <>
96 <span className="text-[11px] text-muted-foreground">
97 Creates the database with AI-suggested properties + sample records.
98 </span>
99 <button
100 type="button"
101 data-testid="db-ai-generate-btn"
102 onClick={generate}
103 disabled={invalid || phase === "streaming"}
104 className="inline-flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:opacity-90 disabled:opacity-40"
105 >
106 {phase === "streaming" ? (
107 <Loader2 className="size-3.5 animate-spin" aria-hidden />
108 ) : (
109 <Sparkles className="size-3.5" aria-hidden />
110 )}
111 Generate
112 </button>
113 </>
114 }
115 >
116 <label className="block text-xs font-medium text-muted-foreground" htmlFor="db-ai-generate-prompt">
117 Describe the database to create
118 </label>
119 <textarea
120 id="db-ai-generate-prompt"
121 data-testid="db-ai-generate-prompt"
122 value={prompt}
123 onChange={(e) => setPrompt(e.target.value)}
124 onKeyDown={(e) => {
125 if (e.key === "Enter" && (e.metaKey || e.ctrlKey) && !invalid && phase !== "streaming") {
126 e.preventDefault();
127 generate();
128 }
129 }}
130 rows={4}
131 placeholder={'e.g. "a bug tracker with title, state, due date and assignee"'}
132 maxLength={2000}
133 className="w-full resize-none rounded-md border border-border bg-background px-3 py-2 text-sm outline-none focus:border-primary"
134 />
135
136 {phase === "streaming" ? <DbAiLoading text="Generating database…" /> : null}
137
138 {phase === "error" && error ? (
139 <DbAiErrorBanner
140 code={error.code}
141 message={error.message}
142 budget={budget}
143 retryable={error.retryable}
144 onRetry={retry}
145 />
146 ) : null}
147
148 {done ? (
149 <div
150 data-testid="db-ai-generate-done"
151 role="status"
152 className="flex items-center gap-2 rounded-md border border-green-300 bg-green-50 px-3 py-2 text-xs text-green-900 dark:border-green-800 dark:bg-green-950/40 dark:text-green-200"
153 >
154 <CheckCheck className="size-3.5 shrink-0" aria-hidden />
155 <span className="min-w-0 flex-1">
156 Database created with {propCount} properties and {recordCount} sample record{recordCount === 1 ? "" : "s"}. It now
157 appears in the sidebar.
158 </span>
159 {workspaceSlug && databaseId ? (
160 <button
161 type="button"
162 onClick={() => {
163 onClose();
164 router.push(`/w/${workspaceSlug}/databases/${databaseId}`);
165 }}
166 className="shrink-0 rounded border border-green-400 px-2 py-1 font-medium hover:bg-green-100 dark:hover:bg-green-900/40"
167 >
168 Open
169 </button>
170 ) : null}
171 </div>
172 ) : null}
173 </DbAiDialog>
174 );
175}
176