11"use client" ;
22
3- import { useState , useTransition } from "react" ;
3+ import { useState , useTransition , useEffect } from "react" ;
44import { useRouter } from "next/navigation" ;
55import { zodResolver } from "@hookform/resolvers/zod" ;
66import { useForm , useFieldArray } from "react-hook-form" ;
77import { z } from "zod" ;
8+ import { experimental_useObject as useObject } from "@ai-sdk/react" ;
89import { Button } from "@propsto/ui/atoms/button" ;
910import { Input } from "@propsto/ui/atoms/input" ;
1011import { Textarea } from "@propsto/ui/atoms/text-area" ;
@@ -33,8 +34,9 @@ import {
3334 SelectValue ,
3435} from "@propsto/ui/atoms/select" ;
3536import { FeedbackType , FieldType } from "@prisma/client" ;
36- import { Plus , Trash2 , Sparkles , GripVertical } from "lucide-react" ;
37- import { generateFormAction , createTemplateAction } from "./actions" ;
37+ import { Plus , Trash2 , Sparkles , GripVertical , Loader2 } from "lucide-react" ;
38+ import { createTemplateAction } from "./actions" ;
39+ import { FieldTypeSchema } from "@propsto/forms" ;
3840
3941const fieldSchema = z . object ( {
4042 label : z . string ( ) . min ( 1 , "Label is required" ) ,
@@ -78,12 +80,34 @@ const fieldTypeOptions = [
7880 { value : "DATE" , label : "Date" } ,
7981] ;
8082
83+ // Schema for AI object streaming
84+ const aiFormSchema = z . object ( {
85+ name : z . string ( ) . describe ( "A concise, descriptive name for the form" ) ,
86+ description : z
87+ . string ( )
88+ . optional ( )
89+ . describe ( "Brief description of the form's purpose" ) ,
90+ fields : z
91+ . array (
92+ z . object ( {
93+ label : z . string ( ) . describe ( "The question or field label" ) ,
94+ type : FieldTypeSchema . describe ( "The field type" ) ,
95+ required : z . boolean ( ) . describe ( "Whether this field is required" ) ,
96+ options : z
97+ . array ( z . string ( ) )
98+ . optional ( )
99+ . describe ( "Options for SELECT/RADIO fields" ) ,
100+ placeholder : z . string ( ) . optional ( ) . describe ( "Placeholder text hint" ) ,
101+ helpText : z . string ( ) . optional ( ) . describe ( "Help text for the field" ) ,
102+ } )
103+ )
104+ . describe ( "Form fields in order" ) ,
105+ } ) ;
106+
81107export function CreateTemplateForm ( ) : React . JSX . Element {
82108 const router = useRouter ( ) ;
83109 const [ isPending , startTransition ] = useTransition ( ) ;
84- const [ isGenerating , setIsGenerating ] = useState ( false ) ;
85110 const [ aiPrompt , setAiPrompt ] = useState ( "" ) ;
86- const [ aiError , setAiError ] = useState < string | null > ( null ) ;
87111
88112 const form = useForm < FormValues > ( {
89113 resolver : zodResolver ( formSchema ) ,
@@ -101,6 +125,52 @@ export function CreateTemplateForm(): React.JSX.Element {
101125 name : "fields" ,
102126 } ) ;
103127
128+ // Streaming AI form generation
129+ const {
130+ object : aiForm ,
131+ submit : submitGeneration ,
132+ isLoading : isGenerating ,
133+ error : aiError ,
134+ } = useObject ( {
135+ api : "/api/generate-form" ,
136+ schema : aiFormSchema ,
137+ onFinish : ( { object } ) => {
138+ if ( object ) {
139+ // Apply final form values
140+ form . setValue ( "name" , object . name ) ;
141+ if ( object . description ) {
142+ form . setValue ( "description" , object . description ) ;
143+ }
144+ if ( object . fields ) {
145+ replace (
146+ object . fields . map ( ( f , i ) => ( {
147+ label : f . label ,
148+ type : f . type as FieldType ,
149+ required : f . required ,
150+ options : f . options ,
151+ placeholder : f . placeholder ,
152+ helpText : f . helpText ,
153+ order : i ,
154+ } ) )
155+ ) ;
156+ }
157+ setAiPrompt ( "" ) ;
158+ }
159+ } ,
160+ } ) ;
161+
162+ // Update form progressively as AI streams
163+ useEffect ( ( ) => {
164+ if ( aiForm ) {
165+ if ( aiForm . name ) {
166+ form . setValue ( "name" , aiForm . name ) ;
167+ }
168+ if ( aiForm . description ) {
169+ form . setValue ( "description" , aiForm . description ) ;
170+ }
171+ }
172+ } , [ aiForm , form ] ) ;
173+
104174 function addField ( ) : void {
105175 append ( {
106176 label : "" ,
@@ -110,36 +180,9 @@ export function CreateTemplateForm(): React.JSX.Element {
110180 } ) ;
111181 }
112182
113- async function handleGenerate ( ) : Promise < void > {
114- if ( ! aiPrompt . trim ( ) ) return ;
115-
116- setIsGenerating ( true ) ;
117- setAiError ( null ) ;
118-
119- const result = await generateFormAction ( { prompt : aiPrompt } ) ;
120-
121- if ( result . success && result . form ) {
122- form . setValue ( "name" , result . form . name ) ;
123- if ( result . form . description ) {
124- form . setValue ( "description" , result . form . description ) ;
125- }
126- replace (
127- result . form . fields . map ( ( f , i ) => ( {
128- label : f . label ,
129- type : f . type as FieldType ,
130- required : f . required ?? false ,
131- options : f . options ,
132- placeholder : f . placeholder ,
133- helpText : f . helpText ,
134- order : i ,
135- } ) ) ,
136- ) ;
137- setAiPrompt ( "" ) ;
138- } else {
139- setAiError ( result . error ?? "Failed to generate form" ) ;
140- }
141-
142- setIsGenerating ( false ) ;
183+ function handleGenerate ( ) : void {
184+ if ( ! aiPrompt . trim ( ) || isGenerating ) return ;
185+ submitGeneration ( { prompt : aiPrompt } ) ;
143186 }
144187
145188 function onSubmit ( values : FormValues ) : void {
@@ -191,7 +234,10 @@ export function CreateTemplateForm(): React.JSX.Element {
191234 disabled = { isGenerating || ! aiPrompt . trim ( ) }
192235 >
193236 { isGenerating ? (
194- < > Generating...</ >
237+ < >
238+ < Loader2 className = "mr-2 size-4 animate-spin" />
239+ Generating...
240+ </ >
195241 ) : (
196242 < >
197243 < Sparkles className = "mr-2 size-4" />
@@ -200,7 +246,9 @@ export function CreateTemplateForm(): React.JSX.Element {
200246 ) }
201247 </ Button >
202248 { aiError && (
203- < span className = "text-sm text-destructive" > { aiError } </ span >
249+ < span className = "text-sm text-destructive" >
250+ { aiError . message || "Failed to generate form" }
251+ </ span >
204252 ) }
205253 </ div >
206254 </ CardContent >
0 commit comments