skalara-core/components/generate-project.tsx
Christopher Arraya 788b952127 initial commit
2023-11-18 21:09:24 -05:00

544 lines
16 KiB
TypeScript

"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>
);
}