mirror of
https://github.com/SkalaraAI/skalara-core.git
synced 2025-04-09 23:20:15 -04:00
544 lines
16 KiB
TypeScript
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>
|
|
);
|
|
}
|