1"use client";
2
3
21import { useCallback, useEffect, useRef, useState } from "react";
22import { AiPreStreamError } from "@/lib/ai";
23import { sendDbExtract, sendDbGenerate, type DbAiResult } from "@/lib/ai-database";
24
25export type DbAiStreamPhase = "idle" | "streaming" | "done" | "error";
26
27export interface DbAiStreamError {
28 code: string;
29 message: string;
30 retryable?: boolean;
31
32 budget?: boolean;
33}
34
35export interface DbAiStreamState {
36 phase: DbAiStreamPhase;
37
38 result: DbAiResult | null;
39 error: DbAiStreamError | null;
40}
41
42export interface UseDbAiStreamParams {
43 workspaceId: string;
44 databaseId?: string;
45 signal?: AbortSignal;
46}
47
48export function useDbAiStream({ workspaceId }: UseDbAiStreamParams) {
49 const [phase, setPhase] = useState<DbAiStreamPhase>("idle");
50 const [result, setResult] = useState<DbAiResult | null>(null);
51 const [error, setError] = useState<DbAiStreamError | null>(null);
52 const abortRef = useRef<AbortController | null>(null);
53
54 const run = useCallback(
55 async (
56 call: (signal: AbortSignal) => Promise<DbAiResult>
57 ): Promise<DbAiResult | null> => {
58 setPhase("streaming");
59 setResult(null);
60 setError(null);
61 const controller = new AbortController();
62 abortRef.current = controller;
63 try {
64 const res = await call(controller.signal);
65 if (res.reason === "aborted") {
66 setPhase("idle");
67 return null;
68 }
69 if (res.reason !== "ok" || !res.done) {
70 setPhase("error");
71 setResult(res);
72 const ef = res.errorFrame;
73 setError(
74 ef
75 ? { code: ef.code, message: ef.message }
76 : { code: "AI_STREAM_ERROR", message: "The stream ended unexpectedly. Nothing was created." }
77 );
78 return null;
79 }
80 setResult(res);
81 setPhase("done");
82 return res;
83 } catch (err) {
84 if (err instanceof AiPreStreamError) {
85 setPhase("error");
86 setError({
87 code: err.code,
88 message: err.message,
89 retryable: err.retryAfterSeconds != null,
90 budget: err.code === "AI_BUDGET_EXCEEDED" || err.code === "AI_BUDGET_MONTHLY_EXCEEDED"
91 });
92 return null;
93 }
94 setPhase("error");
95 setError({ code: "NETWORK_ERROR", message: "Could not reach the server." });
96 return null;
97 } finally {
98 abortRef.current = null;
99 }
100 },
101 []
102 );
103
104 const runGenerate = useCallback(
105 (prompt: string, parentId?: string) =>
106 run((signal) => sendDbGenerate({ workspaceId, prompt, ...(parentId ? { parentId } : {}), signal })),
107 [run, workspaceId]
108 );
109
110 const runExtract = useCallback(
111 (databaseId: string, text: string) =>
112 run((signal) => sendDbExtract({ workspaceId, databaseId, text, signal })),
113 [run, workspaceId]
114 );
115
116 const abort = useCallback(() => {
117 abortRef.current?.abort();
118 abortRef.current = null;
119 }, []);
120
121
122 useEffect(() => () => abortRef.current?.abort(), []);
123
124 return { phase, result, error, runGenerate, runExtract, abort };
125}
126