"use client"; import { useState, useEffect } from "react"; import { useToast } from "@/components/ui/use-toast"; import { useRouter, useParams } from "next/navigation"; import { v4 as uuidv4 } from "uuid"; import * as z from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm, useFieldArray } from "react-hook-form"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { ReloadIcon, TrashIcon, Pencil2Icon, PlusIcon, } from "@radix-ui/react-icons"; import { Textarea } from "@/components/ui/textarea"; import { Card, CardContent, CardFooter, CardHeader, CardTitle, } from "@/components/ui/card"; import { ScrollArea } from "@/components/ui/scroll-area"; import { FeatureCard } from "./feature-card"; import { Input } from "./ui/input"; const DIALOG_PAGES = { WELCOME: 0, QUESTIONS: 1, GENERATE_FEATURES: 2, GENERATED_FEATURES: 3, GENERATE_TASKS: 4, GENERATING_TASKS: 5, GENERATED_TASKS: 6, }; const questionsFormSchema = z.object({ questions: z.array( z.object({ answer: z.string().optional(), }) ), }); type Question = { question: string; answer: string; }; type Feature = { uid: string; name: string; description: string; }; export function GenerateProject({ project_name, project_description, project_stack, }: { project_name: string; project_description: string; project_stack: string; }) { const [questions, setQuestions] = useState<string[]>([]); const [features, setFeatures] = useState<Feature[]>([]); const [isAddingFeature, setIsAddingFeature] = useState(false); const [newFeatureName, setNewFeatureName] = useState(""); const [newFeatureDescription, setNewFeatureDescription] = useState(""); const [tasks, setTasks] = useState([]); const [qa, setQA] = useState<Question[]>([]); const [dialogOpen, setDialogOpen] = useState(false); const [loading, setLoading] = useState(false); const [dialogPage, setDialogPage] = useState(DIALOG_PAGES.WELCOME); const { toast } = useToast(); const params = useParams(); const router = useRouter(); const form = useForm<z.infer<typeof questionsFormSchema>>({ resolver: zodResolver(questionsFormSchema), mode: "all", defaultValues: { questions: [], }, }); const { fields, append, remove } = useFieldArray({ name: "questions", control: form.control, }); useEffect(() => { async function fetchFeatures() { try { console.log(params.projectID); const res = await fetch( `/w/${params.workspaceID}/p/${params.projectID}/features` ); const data = await res.json(); return data.features; } catch (err) { console.error(err); toast({ variant: "destructive", title: "Failed to fetch features.", description: `${err}`, }); } } fetchFeatures().then((features) => { if (features === undefined || features.length === 0) setDialogOpen(true); setFeatures(features); }); }, [params.projectID, params.workspaceID, toast]); async function generateQuestions() { setLoading(true); try { const res = await fetch( `/w/${params.workspaceID}/p/${params.projectID}/gen` ); const data = await res.json(); console.log(data.questions); append(data.questions); setQuestions(data.questions); setDialogPage(DIALOG_PAGES.QUESTIONS); setLoading(false); return data.questions; } catch (err) { console.error(err); } setLoading(false); } async function generateFeatures() { setLoading(true); try { const res = await fetch( `/w/${params.workspaceID}/p/${params.projectID}/features/gen`, { method: "POST", body: JSON.stringify({ project_name, project_description, project_stack, qa, }), } ); const data = await res.json(); console.log(data.features); setFeatures(data.features); setDialogPage(DIALOG_PAGES.GENERATED_FEATURES); setLoading(false); return data.features; } catch (err) { console.error(err); } setLoading(false); } async function generateTasks() { setLoading(true); try { const res = await fetch( `/w/${params.workspaceID}/p/${params.projectID}/features/gen/tasks`, { method: "POST", body: JSON.stringify({ project_name, project_description, project_stack, }), } ); const data = await res.json(); console.log(data.tasks); setTasks(data.tasks); await addTasks(data.tasks); setDialogPage(DIALOG_PAGES.GENERATED_TASKS); setLoading(false); return data.tasks; } catch (err) { console.error(err); } setLoading(false); } async function addQuestions(values: z.infer<typeof questionsFormSchema>) { setLoading(true); try { const answer_arr = values.questions.map((q) => q.answer); const q_data: Question[] = answer_arr.map((a, i) => ({ question: questions[i], answer: String(a), })); console.log(q_data); const res = await fetch( `/w/${params.workspaceID}/p/${params.projectID}/gen`, { method: "PUT", body: JSON.stringify(q_data), } ); console.log(res); if (!res.ok) throw new Error(res.statusText); if (res.ok) { setQA(q_data); setDialogPage(DIALOG_PAGES.GENERATE_FEATURES); } } catch (err) { console.error(err); } setLoading(false); } async function addFeatures() { setLoading(true); try { const res = await fetch( `/w/${params.workspaceID}/p/${params.projectID}/features/add`, { method: "POST", body: JSON.stringify({ features: features.map((feature) => ({ name: feature.name, description: feature.description, project_id: params.projectID, })), }), } ); if (!res.ok) throw new Error(res.statusText); const project_features = await fetch( `/w/${params.workspaceID}/p/${params.projectID}/features` ); const data = await project_features.json(); const deps = await fetch( `/w/${params.workspaceID}/p/${params.projectID}/features/gen/deps`, { method: "POST", body: JSON.stringify({ project_name, project_description, project_stack, features: data.features, }), } ); const deps_data = await deps.json(); console.log(deps_data); if (res.ok) { setDialogPage(DIALOG_PAGES.GENERATE_TASKS); } } catch (err) { console.error(err); } setLoading(false); } async function addTasks(tasks: any[]) { try { const res = await fetch( `/w/${params.workspaceID}/p/${params.projectID}/tasks/add`, { method: "POST", body: JSON.stringify({ tasks, }), } ); if (!res.ok) throw new Error(res.statusText); } catch (err) { console.error(err); } } return ( <Dialog open={dialogOpen} onOpenChange={setDialogOpen}> {dialogPage == DIALOG_PAGES.WELCOME && ( <DialogContent> <DialogHeader> <DialogTitle>Welcome to your project!</DialogTitle> <DialogDescription> Skalara would like to learn more about your project. This will help in the rest of the project generation process. </DialogDescription> </DialogHeader> <DialogFooter> {!loading ? ( <Button onClick={generateQuestions}>Generate Questions</Button> ) : ( <Button disabled> <ReloadIcon className="mr-2 h-4 w-4 animate-spin" /> Generating Questions... </Button> )} </DialogFooter> </DialogContent> )} {dialogPage == DIALOG_PAGES.QUESTIONS && ( <DialogContent> <DialogHeader> <DialogTitle>Project Questions</DialogTitle> <DialogDescription> Answer any questions that you feel would help Skalara learn more about your project goals. All questions are optional. </DialogDescription> </DialogHeader> <Form {...form}> <form onSubmit={form.handleSubmit(addQuestions)} className="flex flex-col space-y-4" > {fields.map((field, index) => ( <FormField control={form.control} key={field.id} name={`questions.${index}.answer`} render={({ field }) => ( <FormItem> <FormLabel>Question {index + 1} </FormLabel> <FormDescription> <span className="text-gray-400"> {questions[index]} </span> </FormDescription> <FormControl> <Textarea {...field} /> </FormControl> <FormMessage /> </FormItem> )} /> ))} <DialogFooter> {!loading ? ( <Button type="submit">Submit</Button> ) : ( <Button disabled> <ReloadIcon className="mr-2 h-4 w-4 animate-spin" /> Loading... </Button> )} </DialogFooter> </form> </Form> </DialogContent> )} {dialogPage == DIALOG_PAGES.GENERATE_FEATURES && ( <DialogContent> <DialogHeader> <DialogTitle>Generate Features</DialogTitle> <DialogDescription> Skalara will now generate features for your project based on what you have provided so far. </DialogDescription> </DialogHeader> <DialogFooter> {!loading ? ( <Button onClick={generateFeatures}>Generate Features</Button> ) : ( <Button disabled> <ReloadIcon className="mr-2 h-4 w-4 animate-spin" /> Generating Features... </Button> )} </DialogFooter> </DialogContent> )} {dialogPage == DIALOG_PAGES.GENERATED_FEATURES && ( <DialogContent> <DialogHeader> <DialogTitle>Generated Features</DialogTitle> <DialogDescription> Skalara has generated the following features for your project: </DialogDescription> </DialogHeader> <ScrollArea className="max-h-[500px]"> <div className="grid grid-cols-2 gap-4"> {features.map( ( feature: { uid: string; name: string; description: string }, i ) => ( <FeatureCard feature={feature} features={features} setFeatures={setFeatures} key={feature.uid} /> ) )} {!isAddingFeature ? ( <Card className="shadow-none w-full h-[150px] flex justify-center items-center border-2 border-primary border-dashed"> <CardContent className="p-0 flex flex-col items-center gap-1"> <Button onClick={() => setIsAddingFeature(true)}> <PlusIcon className="mr-2" /> <h1>Create Feature</h1> </Button> </CardContent> </Card> ) : ( <Card> <CardHeader> <CardTitle> <Input type="text" value={newFeatureName} placeholder="Feature Name" onChange={(e) => setNewFeatureName(e.target.value)} /> </CardTitle> </CardHeader> <CardContent> <Textarea value={newFeatureDescription} placeholder="Feature Description" onChange={(e) => setNewFeatureDescription(e.target.value)} /> </CardContent> <CardFooter className="flex flex-row space-x-2"> <Button variant="ghost" onClick={() => { setIsAddingFeature(false); setNewFeatureName(""); setNewFeatureDescription(""); }} > Cancel </Button> <Button onClick={() => { setFeatures([ ...features, { name: newFeatureName, description: newFeatureDescription, uid: uuidv4(), }, ]); setNewFeatureName(""); setNewFeatureDescription(""); setIsAddingFeature(false); }} > Create </Button> </CardFooter> </Card> )} </div> </ScrollArea> <DialogFooter> {!loading ? ( <Button onClick={addFeatures}>Add Features to Project</Button> ) : ( <Button disabled> <ReloadIcon className="mr-2 h-4 w-4 animate-spin" /> Adding Features... </Button> )} </DialogFooter> </DialogContent> )} {dialogPage == DIALOG_PAGES.GENERATE_TASKS && ( <DialogContent> <DialogHeader> <DialogTitle>Generate Tasks</DialogTitle> <DialogDescription> Skalara will now generate tasks for your project based on what you have provided so far. </DialogDescription> </DialogHeader> <DialogFooter> {!loading ? ( <Button onClick={generateTasks}>Generate Tasks</Button> ) : ( <Button disabled> <ReloadIcon className="mr-2 h-4 w-4 animate-spin" /> Generating Tasks... </Button> )} </DialogFooter> </DialogContent> )} {dialogPage == DIALOG_PAGES.GENERATED_TASKS && ( <DialogContent> <DialogHeader> <DialogTitle>Tasks Generated</DialogTitle> <DialogDescription> Skalara has generated tasks for your project. </DialogDescription> </DialogHeader> <DialogFooter> <Button onClick={() => { setDialogOpen(false); setDialogPage(DIALOG_PAGES.WELCOME); router.refresh(); }} > Close </Button> </DialogFooter> </DialogContent> )} </Dialog> ); }