From bcf3f113ad67b689cfccb963d05fd2fd80d64f97 Mon Sep 17 00:00:00 2001 From: Christopher Arraya Date: Sun, 5 Nov 2023 07:34:37 -0500 Subject: [PATCH] i'm done --- app/chat/[taskID]/route.ts | 46 +++ app/chat/add/route.ts | 48 +++ app/chat/route.ts | 54 +++ .../p/[projectID]/features/add/route.ts | 60 +++ .../p/[projectID]/features/gen/route.ts | 83 ++++ .../{feature => features}/route.ts | 18 +- .../[workspaceID]/p/[projectID]/not-found.tsx | 7 + app/w/[workspaceID]/p/[projectID]/page.tsx | 90 ++++- .../[workspaceID]/p/[projectID]/task/route.ts | 85 ---- .../p/[projectID]/tasks/add/route.ts | 69 ++++ .../p/[projectID]/tasks/gen/route.ts | 91 +++++ app/w/[workspaceID]/p/gen/route.ts | 70 ++++ app/w/[workspaceID]/p/route.ts | 12 +- app/w/[workspaceID]/page.tsx | 64 +++ components/chat.tsx | 128 ++++++ components/create-feature.tsx | 6 +- components/create-project.tsx | 288 +++++++++----- components/create-task.tsx | 2 +- components/create-workspace.tsx | 6 + components/feature-card.tsx | 103 +++++ components/feature-list.tsx | 16 +- components/generate-features.tsx | 7 - components/generate-project.tsx | 375 ++++++++++++++++++ components/generate-task.tsx | 67 ++++ components/generate-tasks.tsx | 120 ------ components/task-list.tsx | 65 ++- .../data-table-column-header.tsx | 96 +++++ .../data-table-faceted-filter.tsx | 146 +++++++ .../data-table-floating-bar.tsx | 0 .../task-table-legacy/data-table-loading.tsx | 77 ++++ .../data-table-pagination.tsx | 107 +++++ .../task-table-legacy/data-table-toolbar.tsx | 78 ++++ .../data-table-view-options.tsx | 61 +++ components/task-table-legacy/data-table.tsx | 300 ++++++++++++++ components/task-table/columns.tsx | 177 +++++++++ .../task-table/data-table-column-header.tsx | 45 +-- .../task-table/data-table-faceted-filter.tsx | 27 +- .../task-table/data-table-pagination.tsx | 144 ++++--- .../task-table/data-table-row-actions.tsx | 67 ++++ components/task-table/data-table-toolbar.tsx | 73 ++-- .../task-table/data-table-view-options.tsx | 8 +- components/task-table/data-table.tsx | 194 +-------- components/task-table/data.tsx | 56 +++ components/task-table/schema.tsx | 15 + components/ui/sheet.tsx | 54 +-- lib/prompts.ts | 82 ++++ package.json | 2 + pnpm-lock.yaml | 334 +++++++++++++++- prisma/schema.prisma | 32 +- types/index.d.ts | 47 +++ types/supabase.ts | 43 +- 51 files changed, 3510 insertions(+), 735 deletions(-) create mode 100644 app/chat/[taskID]/route.ts create mode 100644 app/chat/add/route.ts create mode 100644 app/chat/route.ts create mode 100644 app/w/[workspaceID]/p/[projectID]/features/add/route.ts create mode 100644 app/w/[workspaceID]/p/[projectID]/features/gen/route.ts rename app/w/[workspaceID]/p/[projectID]/{feature => features}/route.ts (75%) create mode 100644 app/w/[workspaceID]/p/[projectID]/not-found.tsx delete mode 100644 app/w/[workspaceID]/p/[projectID]/task/route.ts create mode 100644 app/w/[workspaceID]/p/[projectID]/tasks/add/route.ts create mode 100644 app/w/[workspaceID]/p/[projectID]/tasks/gen/route.ts create mode 100644 app/w/[workspaceID]/p/gen/route.ts create mode 100644 components/feature-card.tsx delete mode 100644 components/generate-features.tsx create mode 100644 components/generate-project.tsx create mode 100644 components/generate-task.tsx delete mode 100644 components/generate-tasks.tsx create mode 100644 components/task-table-legacy/data-table-column-header.tsx create mode 100644 components/task-table-legacy/data-table-faceted-filter.tsx rename components/{task-table => task-table-legacy}/data-table-floating-bar.tsx (100%) create mode 100644 components/task-table-legacy/data-table-loading.tsx create mode 100644 components/task-table-legacy/data-table-pagination.tsx create mode 100644 components/task-table-legacy/data-table-toolbar.tsx create mode 100644 components/task-table-legacy/data-table-view-options.tsx create mode 100644 components/task-table-legacy/data-table.tsx create mode 100644 components/task-table/columns.tsx create mode 100644 components/task-table/data-table-row-actions.tsx create mode 100644 components/task-table/data.tsx create mode 100644 components/task-table/schema.tsx create mode 100644 lib/prompts.ts create mode 100644 types/index.d.ts diff --git a/app/chat/[taskID]/route.ts b/app/chat/[taskID]/route.ts new file mode 100644 index 0000000..2164d50 --- /dev/null +++ b/app/chat/[taskID]/route.ts @@ -0,0 +1,46 @@ +import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs"; +import { cookies } from "next/headers"; +import { NextResponse, NextRequest } from "next/server"; +import { Database } from "@/types/supabase"; +import { prisma } from "@/lib/prisma"; + +async function getSession(supabase: any) { + const { + data: { session }, + } = await supabase.auth.getSession(); + return session; +} + +export async function GET( + req: NextRequest, + { params: { taskID } }: { params: { taskID: string } } +) { + const supabase = createRouteHandlerClient({ cookies }); + const session = await getSession(supabase); + + if (!session) return NextResponse.redirect("/auth"); + + try { + const messages = await prisma.message.findMany({ + where: { + task_id: BigInt(taskID), + }, + orderBy: { + created_at: "asc", + }, + }); + + console.log(messages); + + const res = messages.map((message) => ({ + ...message, + id: String(message.id), + task_id: String(message.task_id), + })); + + return NextResponse.json({ messages: res }, { status: 200 }); + } catch (err) { + console.error(err); + return NextResponse.json({ error: err }, { status: 500 }); + } +} diff --git a/app/chat/add/route.ts b/app/chat/add/route.ts new file mode 100644 index 0000000..48e1a78 --- /dev/null +++ b/app/chat/add/route.ts @@ -0,0 +1,48 @@ +import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs"; +import { cookies } from "next/headers"; +import { NextResponse, NextRequest } from "next/server"; +import { Database } from "@/types/supabase"; +import { prisma } from "@/lib/prisma"; + +async function getSession(supabase: any) { + const { + data: { session }, + } = await supabase.auth.getSession(); + return session; +} + +export async function POST(req: NextRequest) { + const supabase = createRouteHandlerClient({ cookies }); + const session = await getSession(supabase); + + if (!session) return NextResponse.redirect("/auth"); + + try { + const formData = await req.json(); + const role = String(formData.role); + const message = String(formData.content); + const task_id = String(formData.task_id); + console.log("TASK_ID IN CHATADD ===> ", task_id); + + console.log(role, message); + + const res = await prisma.message.create({ + data: { + content: message, + role: role, + task_id: BigInt(task_id), + }, + }); + + const res_data = { + ...res, + id: String(res.id), + task_id: String(res.task_id), + }; + + return NextResponse.json({ message: res_data }, { status: 200 }); + } catch (err) { + console.error(err); + return NextResponse.json({ error: err }, { status: 500 }); + } +} diff --git a/app/chat/route.ts b/app/chat/route.ts new file mode 100644 index 0000000..f5c7690 --- /dev/null +++ b/app/chat/route.ts @@ -0,0 +1,54 @@ +// app/api/chat/route.ts + +import OpenAI from "openai"; +import { OpenAIStream, StreamingTextResponse } from "ai"; + +// Optional, but recommended: run on the edge runtime. +// See https://vercel.com/docs/concepts/functions/edge-functions +export const runtime = "edge"; + +const openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY!, +}); + +export async function POST(req: Request) { + // Extract the `messages` from the body of the request + const { messages, ...body } = await req.json(); + + // console.log("BODY===>", body); + + const prompt = `You are a chatbot that helps users with questions specific to project tasks. +- Project Details: + - Name: ${body.projectInfo.name} + - Description: ${body.projectInfo.description} + - Tech Stack: ${body.projectInfo.stack.join(", ")} + +- Feature Context: + - Name: ${body.featureInfo.name} + - Description: ${body.featureInfo.description} + +- Task Context: + - Name: ${body.taskInfo.task_name} + - Description: ${body.taskInfo.task_description} + +OPERATION GUIDELINES: + +1. Provide information and answer questions specifically related to the project, feature, or task context provided. +2. Do not give generic answers; tailor responses based on the given context.`; + + messages.unshift({ role: "system", content: prompt }); + console.log("MESSAGES ===>", messages); + + // Request the OpenAI API for the response based on the prompt + const response = await openai.chat.completions.create({ + model: "gpt-3.5-turbo-16k", + stream: true, + messages: messages, + }); + + // Convert the response into a friendly text-stream + const stream = OpenAIStream(response); + + // Respond with the stream + return new StreamingTextResponse(stream); +} diff --git a/app/w/[workspaceID]/p/[projectID]/features/add/route.ts b/app/w/[workspaceID]/p/[projectID]/features/add/route.ts new file mode 100644 index 0000000..5950734 --- /dev/null +++ b/app/w/[workspaceID]/p/[projectID]/features/add/route.ts @@ -0,0 +1,60 @@ +import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs"; +import { cookies } from "next/headers"; +import { NextResponse, NextRequest } from "next/server"; +import { Database } from "@/types/supabase"; +import { prisma } from "@/lib/prisma"; + +async function getSession(supabase: any) { + const { + data: { session }, + } = await supabase.auth.getSession(); + return session; +} + +export async function POST( + req: NextRequest, + { + params: { workspaceID, projectID }, + }: { params: { workspaceID: string; projectID: string } } +) { + const supabase = createRouteHandlerClient({ cookies }); + const session = await getSession(supabase); + + if (!session) return NextResponse.redirect("/auth"); + + try { + const formData = await req.json(); + const features = Array(formData)[0]; + + console.log("FORM DATA ===>", formData); + + let res_data: any[] = []; + for (const feature of features) { + const name = String(feature.name); + const description = String(feature.description); + + const feature_data = await prisma.feature.create({ + data: { + name, + description, + project_id: BigInt(projectID), + }, + }); + + const feature_data_res = { + ...feature_data, + id: String(feature_data.id), + project_id: String(feature_data.project_id), + }; + console.log("feature_data ===>", feature_data); + console.log("created feature:", feature_data_res); + res_data.push(feature_data_res); + } + + console.log(`GET DATA TO PASSBACK ${res_data}`); + return NextResponse.json({ features: res_data }, { status: 200 }); + } catch (err) { + console.log(err); + return NextResponse.json({ error: err }, { status: 500 }); + } +} diff --git a/app/w/[workspaceID]/p/[projectID]/features/gen/route.ts b/app/w/[workspaceID]/p/[projectID]/features/gen/route.ts new file mode 100644 index 0000000..1269be9 --- /dev/null +++ b/app/w/[workspaceID]/p/[projectID]/features/gen/route.ts @@ -0,0 +1,83 @@ +import { generateFeatures } from "@/lib/prompts"; +import { v4 as uuidv4 } from "uuid"; + +import { NextRequest, NextResponse } from "next/server"; +import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs"; +import { cookies } from "next/headers"; +import { Database } from "@/types/supabase"; +import { prisma } from "@/lib/prisma"; +import { OpenAIEmbeddings } from "langchain/embeddings/openai"; + +import { completion } from "zod-gpt"; +import * as z from "zod"; +import { OpenAIChatApi } from "llm-api"; + +const MIN_FEATURES_PER_PROJECT = 6; +const MAX_FEATURES_PER_PROJECT = 12; +const OPENAI_MODEL = "gpt-3.5-turbo-16k"; + +const openai = new OpenAIChatApi( + { + apiKey: process.env.OPENAI_API_KEY!, + }, + { + model: OPENAI_MODEL, + } +); + +async function getSession(supabase: any) { + const { + data: { session }, + } = await supabase.auth.getSession(); + return session; +} + +export async function POST(req: NextRequest) { + const supabase = createRouteHandlerClient({ cookies }); + const session = await getSession(supabase); + + if (!session) return NextResponse.redirect("/auth"); + + try { + const formData = await req.json(); + const project_name = String(formData.project_name); + const project_description = String(formData.project_description); + const project_stack = formData.tech_stack; + + // console.log("SERVER FORM DATA ===>", project_stack); + + const feature_gen_prompt = generateFeatures( + project_name, + project_description, + project_stack + ); + + const res = await completion(openai, feature_gen_prompt, { + schema: z.object({ + features: z + .array( + z.object({ + name: z.string().describe("The name of the feature"), + description: z + .string() + .describe("The description of the feature"), + }) + ) + .min(MIN_FEATURES_PER_PROJECT) + .max(MAX_FEATURES_PER_PROJECT), + }), + }); + + const featuresWithUUID = res.data.features.map((feature) => ({ + ...feature, + uid: uuidv4(), // Add a UUID to each feature object + })); + + console.log("SERVER RES ===>", featuresWithUUID); + + return NextResponse.json({ features: featuresWithUUID }, { status: 200 }); + } catch (err) { + console.log(err); + return NextResponse.json({ error: err }, { status: 500 }); + } +} diff --git a/app/w/[workspaceID]/p/[projectID]/feature/route.ts b/app/w/[workspaceID]/p/[projectID]/features/route.ts similarity index 75% rename from app/w/[workspaceID]/p/[projectID]/feature/route.ts rename to app/w/[workspaceID]/p/[projectID]/features/route.ts index f55977a..d4e6175 100644 --- a/app/w/[workspaceID]/p/[projectID]/feature/route.ts +++ b/app/w/[workspaceID]/p/[projectID]/features/route.ts @@ -23,25 +23,21 @@ export async function POST( if (!session) return NextResponse.redirect("/auth"); - const formData = await req.json(); - const name = String(formData.name); - const description = String(formData.description); - - const feature = await prisma.feature.create({ - data: { - name, - description, + const features = await prisma.feature.findMany({ + where: { project_id: BigInt(projectID), }, }); - const res = { + console.log("SERVER FEATURES ===> ", features); + + const res = features.map((feature) => ({ ...feature, id: String(feature.id), project_id: String(feature.project_id), - }; + })); - return NextResponse.json({ project: res }, { status: 200 }); + return NextResponse.json({ features: res }, { status: 200 }); } catch (err) { console.log(err); return NextResponse.json({ error: err }, { status: 500 }); diff --git a/app/w/[workspaceID]/p/[projectID]/not-found.tsx b/app/w/[workspaceID]/p/[projectID]/not-found.tsx new file mode 100644 index 0000000..0247954 --- /dev/null +++ b/app/w/[workspaceID]/p/[projectID]/not-found.tsx @@ -0,0 +1,7 @@ +export default function NotFound() { + return ( +
+

Not Found

+
+ ); +} diff --git a/app/w/[workspaceID]/p/[projectID]/page.tsx b/app/w/[workspaceID]/p/[projectID]/page.tsx index c9951d0..0da0c12 100644 --- a/app/w/[workspaceID]/p/[projectID]/page.tsx +++ b/app/w/[workspaceID]/p/[projectID]/page.tsx @@ -1,17 +1,101 @@ import { FeatureList } from "@/components/feature-list"; import { TaskList } from "@/components/task-list"; -export default function Project({ +import { createServerComponentClient } from "@supabase/auth-helpers-nextjs"; +import { Database } from "@/types/supabase"; +import { cookies } from "next/headers"; +import { redirect } from "next/navigation"; +import Link from "next/link"; +import { prisma } from "@/lib/prisma"; +import { CreateFeature } from "@/components/create-feature"; +import { NextResponse } from "next/server"; + +async function getSession(supabase: any) { + const { + data: { session }, + } = await supabase.auth.getSession(); + return session; +} + +async function getProject(projectID: string) { + const supabase = createServerComponentClient({ cookies }); + const session = await getSession(supabase); + + if (!session) redirect("/auth"); + + try { + const project = await prisma.project.findUnique({ + where: { + id: BigInt(projectID), + }, + }); + + if (!project) return undefined; + + const res = { + ...project, + id: String(project.id), + workspace_id: String(project.workspace_id), + }; + + return res; + } catch (err) { + console.error(err); + return; + } +} + +export default async function Project({ params: { projectID, workspaceID }, }: { params: { projectID: string; workspaceID: string }; }) { + const project = await getProject(projectID); + const supabase = createServerComponentClient({ cookies }); + const session = await getSession(supabase); + if (!project) + return ( +
+

Project not found.

+
+ ); + + if ( + !(await prisma.profile_project.findFirst({ + where: { + profile_id: session.user.id, + project_id: BigInt(projectID), + }, + })) + ) { + return ( +
+

You are not a member of this project.

+
+ ); + } + return (

Project: {projectID}

Workspace: {workspaceID}

+

Project Name: {project?.name}

+

Project Description: {project?.description}

+

Tech Stack: {project?.stack.join(", ")}

- - + +
); } diff --git a/app/w/[workspaceID]/p/[projectID]/task/route.ts b/app/w/[workspaceID]/p/[projectID]/task/route.ts deleted file mode 100644 index e86acc1..0000000 --- a/app/w/[workspaceID]/p/[projectID]/task/route.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs"; -import { cookies } from "next/headers"; -import { NextResponse, NextRequest } from "next/server"; -import { Database } from "@/types/supabase"; -import { prisma } from "@/lib/prisma"; -import { OpenAIEmbeddings } from "langchain/embeddings/openai"; -import { SupabaseVectorStore } from "langchain/vectorstores/supabase"; - -async function getSession(supabase: any) { - const { - data: { session }, - } = await supabase.auth.getSession(); - return session; -} - -export async function POST( - req: NextRequest, - { - params: { workspaceID, projectID }, - }: { params: { workspaceID: string; projectID: string } } -) { - try { - const supabase = createRouteHandlerClient({ cookies }); - const session = await getSession(supabase); - - if (!session) return NextResponse.redirect("/auth"); - - const formData = await req.json(); - - const name = String(formData.name); - const description = String(formData.description); - const featureID = - formData.featureID != undefined ? String(formData.featureID) : null; - - const task = await prisma.task.create({ - data: { - name, - description, - project_id: BigInt(projectID), - feature_id: featureID != null ? BigInt(featureID) : null, - }, - }); - - await prisma.profile_task.create({ - data: { profile_id: session.user.id, task_id: task.id }, - }); - - const embeddings = new OpenAIEmbeddings({ - openAIApiKey: process.env.OPENAI_API_KEY!, - batchSize: 512, - }); - - const task_prompt = `Task Name: ${task.name}\nTask Description: ${task.description}\nTask Feature: ${task.feature_id}\nTask Project: ${task.project_id}`; - const embedding = await embeddings.embedQuery(task_prompt); - - const { data, error } = await supabase.from("documents").insert({ - content: task_prompt, - metadata: { - workspace_id: workspaceID, - project_id: projectID, - feature_id: featureID, - }, - embedding: JSON.stringify(embedding), - }); - - if (error) { - console.log(error); - return NextResponse.json({ error: error }, { status: 500 }); - } - - console.log(data); - - const res = { - ...task, - id: String(task.id), - project_id: String(task.project_id), - feature_id: task.feature_id ? String(task.feature_id) : null, - }; - - return NextResponse.json({ project: res }, { status: 200 }); - } catch (err) { - console.log(err); - return NextResponse.json({ error: err }, { status: 500 }); - } -} diff --git a/app/w/[workspaceID]/p/[projectID]/tasks/add/route.ts b/app/w/[workspaceID]/p/[projectID]/tasks/add/route.ts new file mode 100644 index 0000000..26c8537 --- /dev/null +++ b/app/w/[workspaceID]/p/[projectID]/tasks/add/route.ts @@ -0,0 +1,69 @@ +import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs"; +import { cookies } from "next/headers"; +import { NextResponse, NextRequest } from "next/server"; +import { Database } from "@/types/supabase"; +import { prisma } from "@/lib/prisma"; +import { OpenAIEmbeddings } from "langchain/embeddings/openai"; +import { SupabaseVectorStore } from "langchain/vectorstores/supabase"; + +async function getSession(supabase: any) { + const { + data: { session }, + } = await supabase.auth.getSession(); + return session; +} + +export async function POST( + req: NextRequest, + { + params: { workspaceID, projectID }, + }: { params: { workspaceID: string; projectID: string } } +) { + const supabase = createRouteHandlerClient({ cookies }); + const session = await getSession(supabase); + + if (!session) return NextResponse.redirect("/auth"); + + try { + const formData = await req.json(); + const tasks: any[] = Array(formData); + + console.log("TASKS FORM DATA ===>", formData); + + let res_data: any[] = []; + for (const task of formData) { + console.log("CURRENT TASK ===>", task); + const name = String(task.name); + const description = String(task.description); + const featureID = + task.feature_id != undefined ? String(task.feature_id) : null; + + const task_data = await prisma.task.create({ + data: { + name, + description, + project_id: BigInt(projectID), + feature_id: featureID != null ? BigInt(featureID) : null, + }, + }); + await prisma.profile_task.create({ + data: { profile_id: session.user.id, task_id: task_data.id }, + }); + + const task_data_res = { + ...task_data, + id: String(task_data.id), + project_id: String(task_data.project_id), + feature_id: String(task_data.feature_id), + }; + + res_data.push(task_data_res); + } + + console.log(res_data); + return NextResponse.json({ feature: res_data }, { status: 200 }); + } catch (err) { + console.error(err); + return NextResponse.json({ error: err }, { status: 500 }); + } +} diff --git a/app/w/[workspaceID]/p/[projectID]/tasks/gen/route.ts b/app/w/[workspaceID]/p/[projectID]/tasks/gen/route.ts new file mode 100644 index 0000000..0217dd9 --- /dev/null +++ b/app/w/[workspaceID]/p/[projectID]/tasks/gen/route.ts @@ -0,0 +1,91 @@ +import { NextRequest, NextResponse } from "next/server"; +import { v4 as uuidv4 } from "uuid"; +import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs"; +import { cookies } from "next/headers"; +import { Database } from "@/types/supabase"; +import { prisma } from "@/lib/prisma"; + +import { completion } from "zod-gpt"; +import * as z from "zod"; +import { OpenAIChatApi } from "llm-api"; + +import { generateTasks } from "@/lib/prompts"; + +const MIN_TASKS_PER_FEATURE = 6; +const MAX_TASKS_PER_FEATURE = 12; +const OPENAI_MODEL = "gpt-3.5-turbo-16k"; + +const openai = new OpenAIChatApi( + { + apiKey: process.env.OPENAI_API_KEY!, + }, + { + model: OPENAI_MODEL, + } +); + +async function getSession(supabase: any) { + const { + data: { session }, + } = await supabase.auth.getSession(); + return session; +} + +export async function POST(req: NextRequest) { + const supabase = createRouteHandlerClient({ cookies }); + const session = await getSession(supabase); + + if (!session) return NextResponse.redirect("/auth"); + + try { + const formData = await req.json(); + const project_name = String(formData.project_name); + const project_description = String(formData.project_description); + const project_stack = formData.tech_stack; + const related_features = formData.related_features; + const feature = formData.feature; + + console.log("GEN TASKS DATA ===>", formData); + + const task_gen_prompt = generateTasks( + project_name, + project_description, + project_stack, + related_features, + feature + ); + + const res = await completion(openai, task_gen_prompt, { + schema: z.object({ + tasks: z + .array( + z.object({ + name: z.string().describe("The task name"), + description: z.string().describe("The task description"), + priority: z + .enum(["low", "medium", "high"]) + .describe("The task priority"), + order: z + .number() + .describe("The order in which the task should be implemented"), + }) + ) + .min(MIN_TASKS_PER_FEATURE) + .max(MAX_TASKS_PER_FEATURE), + }), + }); + + const tasksWithUUID = res.data.tasks.map((task) => ({ + ...task, + uid: uuidv4(), + feature_id: feature.id, + })); + + console.log("TASKS RAW RES ===>", tasksWithUUID); + + return NextResponse.json({ tasks: tasksWithUUID }, { status: 200 }); + } catch (err) { + console.error(err); + return NextResponse.json({ error: err }, { status: 500 }); + } +} diff --git a/app/w/[workspaceID]/p/gen/route.ts b/app/w/[workspaceID]/p/gen/route.ts new file mode 100644 index 0000000..4527801 --- /dev/null +++ b/app/w/[workspaceID]/p/gen/route.ts @@ -0,0 +1,70 @@ +import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs"; +import { cookies } from "next/headers"; +import { NextResponse, NextRequest } from "next/server"; +import { Database } from "@/types/supabase"; +import { prisma } from "@/lib/prisma"; + +import { completion } from "zod-gpt"; +import * as z from "zod"; +import { OpenAIChatApi } from "llm-api"; +import { generateProjectQuestions } from "@/lib/prompts"; + +const OPENAI_MODEL = "gpt-3.5-turbo-16k"; +const MIN_QUESTIONS = 1; +const MAX_QUESTIONS = 3; + +const openai = new OpenAIChatApi( + { + apiKey: process.env.OPENAI_API_KEY!, + }, + { + model: OPENAI_MODEL, + } +); + +async function getSession(supabase: any) { + const { + data: { session }, + } = await supabase.auth.getSession(); + return session; +} + +export async function POST( + req: NextRequest, + { params: { workspaceID } }: { params: { workspaceID: string } } +) { + try { + const supabase = createRouteHandlerClient({ cookies }); + const session = await getSession(supabase); + + if (!session) return NextResponse.redirect("/auth"); + + const formData = await req.json(); + const name = String(formData.name); + const description = String(formData.description); + const stack = formData.stack; + + const questionPrompt = generateProjectQuestions(name, description, stack); + console.log("questionPrompt ", questionPrompt); + const res = await completion(openai, questionPrompt, { + schema: z.object({ + questions: z + .array( + z.string().describe("A question to ask the user about the project") + ) + .min(MIN_QUESTIONS) + .max(MAX_QUESTIONS), + }), + }); + + console.log("questions ", res.data.questions); + + return NextResponse.json( + { questions: res.data.questions }, + { status: 200 } + ); + } catch (err) { + console.log(err); + return NextResponse.json({ error: err }, { status: 500 }); + } +} diff --git a/app/w/[workspaceID]/p/route.ts b/app/w/[workspaceID]/p/route.ts index 67ca95a..df16a5c 100644 --- a/app/w/[workspaceID]/p/route.ts +++ b/app/w/[workspaceID]/p/route.ts @@ -22,9 +22,19 @@ export async function POST( if (!session) return NextResponse.redirect("/auth"); const formData = await req.json(); + const questions = formData.questions; + const questionsText = formData.questionsText; + + const extraContext: string = `Also we asked the following questions about the project: ${questions + .map( + (q: any, i: number) => + `Question: ${questionsText[i]} Answer: ${q.question}` + ) + .join("\n")}`; + const name = String(formData.name); - const description = String(formData.description); const stack = formData.stack; + const description = String(formData.description) + extraContext; const project = await prisma.project.create({ data: { diff --git a/app/w/[workspaceID]/page.tsx b/app/w/[workspaceID]/page.tsx index e3d8322..603a92c 100644 --- a/app/w/[workspaceID]/page.tsx +++ b/app/w/[workspaceID]/page.tsx @@ -1,10 +1,74 @@ import { Sidebar } from "@/components/sidebar"; +import { createServerComponentClient } from "@supabase/auth-helpers-nextjs"; +import { Database } from "@/types/supabase"; +import { cookies } from "next/headers"; +import { redirect } from "next/navigation"; +import { prisma } from "@/lib/prisma"; +async function getSession(supabase: any) { + const { + data: { session }, + } = await supabase.auth.getSession(); + return session; +} + +async function getWorkspace(workspaceID: string) { + const supabase = createServerComponentClient({ cookies }); + const session = await getSession(supabase); + + if (!session) redirect("/auth"); + + try { + const workspace = await prisma.workspace.findUnique({ + where: { + id: BigInt(workspaceID), + }, + }); + + if (!workspace) return undefined; + + const res = { + ...workspace, + id: String(workspace.id), + }; + + return res; + } catch (err) { + console.error(err); + return; + } +} export default async function Workspace({ params: { workspaceID }, }: { params: { workspaceID: string }; }) { + const workspace = await getWorkspace(workspaceID); + const supabase = createServerComponentClient({ cookies }); + const session = await getSession(supabase); + + if (!workspace) + return ( +
+

Workspace not found.

+
+ ); + + if ( + !(await prisma.profile_workspace.findFirst({ + where: { + profile_id: session.user.id, + workspace_id: BigInt(workspaceID), + }, + })) + ) { + return ( +
+

You are not a member of this workspace.

+
+ ); + } + return (

Workspace: {workspaceID}

diff --git a/components/chat.tsx b/components/chat.tsx index e69de29..9f089c0 100644 --- a/components/chat.tsx +++ b/components/chat.tsx @@ -0,0 +1,128 @@ +"use client"; +import { useEffect, useState } from "react"; +import { Send } from "lucide-react"; + +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardFooter, + CardHeader, +} from "@/components/ui/card"; +import { ScrollArea } from "@/components/ui/scroll-area"; + +import { Input } from "@/components/ui/input"; + +import { useChat } from "ai/react"; + +export default function Chat({ projectInfo, featureInfo, taskInfo }: any) { + // console.log("projectInfo", projectInfo); + // console.log("featureInfo", featureInfo); + // console.log("taskInfo", taskInfo); + const { messages, input, handleInputChange, handleSubmit, setMessages } = + useChat({ + body: { + projectInfo, + featureInfo, + taskInfo, + }, + api: "/chat", + onResponse: async (res) => { + try { + const task_id = taskInfo.task_id; + const data = await fetch(`/chat/add`, { + method: "POST", + body: JSON.stringify({ + content: input, + role: "user", + task_id: task_id, + }), + }); + } catch (err) { + console.error(err); + } + }, + onFinish: async (message) => { + try { + const data = await fetch(`/chat/add`, { + method: "POST", + body: JSON.stringify({ + content: message.content, + role: "assistant", + task_id: taskInfo.task_id, + }), + }); + } catch (err) { + console.error(err); + } + }, + }); + + useEffect(() => { + async function getMessages() { + try { + const res = await fetch(`/chat/${taskInfo.task_id}`); + const data = await res.json(); + console.log("CLIENT RAW RES ===>", data); + setMessages(data.messages); + return data; + } catch (err) { + console.error(err); + } + } + getMessages(); + }, [setMessages, taskInfo.task_id]); + + const inputLength = input.trim().length; + + return ( + <> + + +

Chat with this task.

+
+ + + +
+ {messages.map((message, index) => ( +
+ {message.content} +
+ ))} +
+
+
+ + +
+ + +
+
+
+ + ); +} diff --git a/components/create-feature.tsx b/components/create-feature.tsx index df574d3..d65e5b5 100644 --- a/components/create-feature.tsx +++ b/components/create-feature.tsx @@ -14,6 +14,7 @@ import { } from "@/components/ui/form"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; +import { useRouter } from "next/navigation"; const formSchema = z.object({ name: z.string().min(2, { @@ -41,10 +42,11 @@ export function CreateFeature({ description: "", }, }); + const router = useRouter(); async function onSubmit(values: z.infer) { try { - const res = await fetch(`/w/${workspaceID}/p/${projectID}/feature`, { + const res = await fetch(`/w/${workspaceID}/p/${projectID}/features/add`, { method: "POST", body: JSON.stringify(values), }); @@ -53,6 +55,8 @@ export function CreateFeature({ if (!res.ok) throw new Error("Something went wrong."); + router.refresh(); + return res; } catch (err) { console.error(err); diff --git a/components/create-project.tsx b/components/create-project.tsx index ecf0f9a..94748b0 100644 --- a/components/create-project.tsx +++ b/components/create-project.tsx @@ -1,7 +1,7 @@ "use client"; import * as z from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useForm } from "react-hook-form"; +import { useForm, useFieldArray } from "react-hook-form"; import { useState } from "react"; import { useRouter } from "next/navigation"; @@ -26,7 +26,8 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { X } from "lucide-react"; -import { set } from "date-fns"; +import { cn } from "@/lib/utils"; +import { Textarea } from "./ui/textarea"; const formSchema = z.object({ name: z.string().min(2, { @@ -38,40 +39,77 @@ const formSchema = z.object({ stack: z.array(z.string()).min(1, { message: "Project tech stack must have at least one item.", }), + questions: z.array( + z.object({ + question: z.string().optional(), + }) + ), }); export function CreateProject({ workspaceID }: { workspaceID: string }) { const form = useForm>({ resolver: zodResolver(formSchema), + mode: "all", defaultValues: { name: "", description: "", stack: [], + questions: [], }, }); - const { setValue } = form; + const { setValue, formState } = form; const [stackInput, setStackInput] = useState(""); - const [isLoading, setIsLoading] = useState(false); + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [formStep, setFormStep] = useState(0); + const [questions, setQuestions] = useState([]); // TODO: [] as Question[] const router = useRouter(); + const { fields, append, remove } = useFieldArray({ + name: "questions", + control: form.control, + }); async function onSubmit(values: z.infer) { try { - setIsLoading(true); const res = await fetch(`/w/${workspaceID}/p`, { method: "POST", - body: JSON.stringify(values), + body: JSON.stringify({ + ...values, + questionsText: questions, + }), }); + const data = await res.json(); + console.log("===>", res); if (!res.ok) throw new Error("Something went wrong."); - router.refresh(); - setIsLoading(false); + router.push(`/w/${workspaceID}/p/${data.project.id}`); + setIsDialogOpen(false); + return res; + } catch (err) { + console.error(err); + } + } + + async function generateQuestions() { + try { + const res = await fetch(`/w/${workspaceID}/p/gen`, { + method: "POST", + body: JSON.stringify(form.getValues()), + }); + + if (!res.ok) throw new Error("Something went wrong."); + + const data = await res.json(); + + console.log("===>", data.questions); + setQuestions(data.questions); + append(data.questions); + return res; } catch (err) { - setIsLoading(false); console.error(err); } } @@ -85,93 +123,163 @@ export function CreateProject({ workspaceID }: { workspaceID: string }) { }; return ( - + - - Create a Project - - Give your project a name, description, and tech stack. - - + {formStep == 0 && ( + + Create a Project + + Give your project a name, description, and tech stack. + + + )} + {formStep == 1 && ( + + Extra Questions + + We've like to know some more about the specifics of your + project, feel free to answer any of the following additional + questions. + + + )}
- - ( - - Name - - - - This is your project name. - - + +
+ ( + + Name + + + + + This is your project name. + + + + )} + /> + ( + + Description + + + + + This is your project description. + + + + )} + /> + ( + + Tech Stack + + setStackInput(e.target.value)} + onKeyDown={keyHandler} + /> + + + This is your project tech stack. + +
+ {form.getValues("stack").map((stack) => ( + + {stack} + + setValue( + "stack", + form + .getValues("stack") + .filter((s) => s !== stack) + ) + } + /> + + ))} +
+ +
+ )} + /> + +
+
+ {questions.length == 0 && ( +

+ Generating questions... +

)} - /> - ( - - Description - - - - - This is your project description. - - - - )} - /> - ( - - Tech Stack - - setStackInput(e.target.value)} - onKeyDown={keyHandler} - /> - - - This is your project tech stack. - -
- {form.getValues("stack").map((stack) => ( - - {stack} - - setValue( - "stack", - form.getValues("stack").filter((s) => s !== stack) - ) - } - /> - - ))} -
- -
- )} - /> - + {fields.map((field, index) => ( + ( + + Question {index + 1} + + + {questions[index]} + + + +