diff --git a/src/app/dashboard/[org]/[repo]/todos/Todo.tsx b/src/app/dashboard/[org]/[repo]/todos/Todo.tsx index 785f252..29bf818 100644 --- a/src/app/dashboard/[org]/[repo]/todos/Todo.tsx +++ b/src/app/dashboard/[org]/[repo]/todos/Todo.tsx @@ -4,6 +4,7 @@ import React, { useState, useEffect } from "react"; import { api } from "~/trpc/react"; import { trpcClient } from "~/trpc/client"; import { type Todo } from "~/server/api/routers/events"; +import { type Research } from "~/server/db/tables/research.table"; import { type Project } from "~/server/db/tables/projects.table"; import LoadingIndicator from "../components/LoadingIndicator"; import { MagnifyingGlassIcon } from "@heroicons/react/24/outline"; @@ -27,6 +28,7 @@ const Todo: React.FC = ({ org, repo, project }) => { const [searchQuery, setSearchQuery] = useState(""); const [isLoadingIssue, setIsLoadingIssue] = useState(false); const [filteredTodos, setFilteredTodos] = useState([]); + const [research, setResearch] = useState([]); const { data: todos, isLoading: isLoadingTodos, @@ -35,6 +37,14 @@ const Todo: React.FC = ({ org, repo, project }) => { projectId: project.id, }); + const { refetch: refetchResearch } = api.events.getResearch.useQuery( + { + todoId: selectedTodo?.id ?? 0, + issueId: selectedTodo?.issueId ?? 0, + }, + { enabled: false } + ); + // const { data: codebaseContext, isLoading: isLoadingCodebaseContext } = // api.codebaseContext.getAll.useQuery({ // org, @@ -70,7 +80,7 @@ const Todo: React.FC = ({ org, repo, project }) => { }; void fetchIssue(); - }, [selectedTodo, org, repo]); + useEffect(() => { // Filter todos based on searchQuery @@ -98,6 +108,19 @@ const Todo: React.FC = ({ org, repo, project }) => { setSelectedTodo(todo); } }; + const handleResearchComplete = async () => { + if (selectedTodo?.id && selectedTodo?.issueId) { + try { + const result = await refetchResearch(); + if (result.data) { + setResearch(result.data); + } + } catch (error) { + console.error("Error refetching research:", error); + } + } + }; + return (
@@ -147,6 +170,8 @@ const Todo: React.FC = ({ org, repo, project }) => { selectedTodo={selectedTodo} selectedIssue={selectedIssue} isLoadingIssue={isLoadingIssue} + research={research} + onResearchComplete={handleResearchComplete} org={org} repo={repo} /> diff --git a/src/app/dashboard/[org]/[repo]/todos/components/Issue.tsx b/src/app/dashboard/[org]/[repo]/todos/components/Issue.tsx index 7072c18..a413103 100644 --- a/src/app/dashboard/[org]/[repo]/todos/components/Issue.tsx +++ b/src/app/dashboard/[org]/[repo]/todos/components/Issue.tsx @@ -1,11 +1,12 @@ -import React, { useEffect } from "react"; +import React, { useEffect, useState } from "react"; import type Todo from "../Todo"; import { type Issue } from "../Todo"; import LoadingIndicator from "../../components/LoadingIndicator"; import { TodoStatus } from "~/server/db/enums"; import { api } from "~/trpc/react"; -import { useState } from "react"; import MarkdownRenderer from "../../components/MarkdownRenderer"; +import { Button } from "~/components/ui/button"; +import { Loader2 } from "lucide-react"; import { toast } from "react-toastify"; interface IssueProps { @@ -23,18 +24,27 @@ const Issue: React.FC = ({ org, repo, }) => { - console.log("selectedIssue", selectedIssue); - const { data: research, isLoading: isLoadingResearch } = - api.events.getResearch.useQuery({ + const { + data: research, + isLoading: isLoadingResearch, + refetch: refetchResearch, + } = api.events.getResearch.useQuery( + { todoId: selectedTodo.id, issueId: selectedTodo.issueId ?? 0, - }); + }, + { + enabled: !!selectedTodo.id && !!selectedTodo.issueId, + refetchOnWindowFocus: false, + }, + ); const [isEditingIssue, setIsEditingIssue] = useState(false); const [issueTitle, setIssueTitle] = useState(selectedIssue?.title ?? ""); const [issueBody, setIssueBody] = useState(selectedIssue?.body ?? ""); const [isEditingExit, setIsEditingExit] = useState(false); const [exitCriteria, setExitCriteria] = useState("[] Add exit criteria"); + const [isResearching, setIsResearching] = useState(false); useEffect(() => { setIssueTitle(selectedIssue?.title ?? ""); @@ -42,6 +52,7 @@ const Issue: React.FC = ({ }, [selectedIssue]); const { mutateAsync: updateIssue } = api.github.updateIssue.useMutation(); + const { mutateAsync: researchIssue } = api.todos.researchIssue.useMutation(); const handleSaveIssue = async () => { try { @@ -81,6 +92,22 @@ const Issue: React.FC = ({ } }; + const handleResearch = async () => { + setIsResearching(true); + try { + await researchIssue({ + issueId: selectedTodo.issueId ?? 0, + }); + await refetchResearch(); + toast.success("Research generated successfully!"); + } catch (error) { + console.error("Error generating research:", error); + toast.error("Failed to generate research."); + } finally { + setIsResearching(false); + } + }; + if (isLoadingIssue) { return ; } @@ -205,6 +232,16 @@ const Issue: React.FC = ({

Research

+ {isLoadingResearch ? ( ) : ( diff --git a/src/server/agent/research.ts b/src/server/agent/research.ts index c6d58a2..845f863 100644 --- a/src/server/agent/research.ts +++ b/src/server/agent/research.ts @@ -20,6 +20,7 @@ export enum ResearchAgentActionType { ResearchCodebase = "ResearchCodebase", ResearchInternet = "ResearchInternet", AskProjectOwner = "AskProjectOwner", + GenerateResearch = "GenerateResearch", ResearchComplete = "ResearchComplete", } @@ -86,6 +87,24 @@ const researchTools: OpenAI.ChatCompletionTool[] = [ }, }, }, + { + type: "function", + function: { + name: ResearchAgentActionType.GenerateResearch, + description: + "Generate research items for a given issue based on the gathered information.", + parameters: { + type: "object", + properties: { + issueId: { + type: "number", + description: "The ID of the issue to generate research for.", + }, + }, + required: ["issueId"], + }, + }, + }, { type: "function", function: { @@ -235,7 +254,7 @@ export const researchIssue = async function ( }; async function callFunction( - functionName: ResearchAgentActionType, + githubIssue: string | number, args: { query: string }, githubIssue: string, sourceMap: string, @@ -254,6 +273,8 @@ async function callFunction( case ResearchAgentActionType.ResearchInternet: return await researchInternet(args.query); case ResearchAgentActionType.AskProjectOwner: + case ResearchAgentActionType.GenerateResearch: + return await generateResearch(Number(githubIssue)); // just return the question for now return args.query; default: @@ -438,5 +459,25 @@ export async function researchInternet(query: string): Promise { "llama-3.1-sonar-large-128k-online", ); + async function generateResearch(issueId: number): Promise { + try { + const researchItems = await db.research.findMany({ + where: { issueId }, + }); + + const researchSummary = researchItems + .map( + (item) => + `Type: ${item.type}\nQuestion: ${item.question}\nAnswer: ${item.answer}`, + ) + .join("\n\n"); + + return `Generated research items for issue ${issueId}:\n\n${researchSummary}`; + } catch (error) { + console.error(`Error generating research for issue ${issueId}:`, error); + return `Failed to generate research for issue ${issueId}. Error: ${error.message}`; + } + } + return result ?? "No response from the AI model."; } diff --git a/src/server/api/routers/todos.ts b/src/server/api/routers/todos.ts index f1fd304..9e54072 100644 --- a/src/server/api/routers/todos.ts +++ b/src/server/api/routers/todos.ts @@ -1,7 +1,11 @@ import { z } from "zod"; import { db } from "~/server/db/db"; import { TodoStatus } from "~/server/db/enums"; -import { researchIssue } from "~/server/agent/research"; +import { + researchIssue, + ResearchAgentActionType, + callFunction, +} from "~/server/agent/research"; import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; import { type Todo } from "./events"; @@ -121,4 +125,26 @@ export const todoRouter = createTRPCRouter({ await db.todos.find(id).delete(); return { id }; }), + + researchIssue: protectedProcedure + .input( + z.object({ + issueId: z.number(), + }), + ) + .mutation(async ({ input: { issueId } }): Promise => { + try { + const todo = await db.todos.findOne({ issueId }); + if (!todo) { + throw new Error("Todo not found for the given issueId"); + } + await callFunction(ResearchAgentActionType.RESEARCH_ISSUE, { + description: todo.description, + todoId: todo.id, + issueId: issueId, + }); + } catch (error) { + throw new Error(`Failed to research issue: ${error.message}`); + } + }), }); diff --git a/src/server/db/tables/research.table.ts b/src/server/db/tables/research.table.ts index 005ab46..f9de91e 100644 --- a/src/server/db/tables/research.table.ts +++ b/src/server/db/tables/research.table.ts @@ -15,6 +15,7 @@ export class ResearchTable extends BaseTable { type: t.enum("research_type_values", RESEARCH_TYPE_VALUES), question: t.text(), answer: t.text(), + createdAt: t.timestamp().defaultNow(), ...t.timestamps(), })); } diff --git a/src/trpc/client.tsx b/src/trpc/client.tsx index f431b61..aa03c70 100644 --- a/src/trpc/client.tsx +++ b/src/trpc/client.tsx @@ -1,19 +1,14 @@ import { createTRPCClient, httpBatchLink } from "@trpc/client"; import SuperJSON from "superjson"; -import { type AppRouter } from "~/server/api/root"; +import type { AppRouter } from "~/server/api/root"; const trpcClient = createTRPCClient({ links: [ httpBatchLink({ - url: `${getBaseUrl()}/api/trpc`, - headers: () => { - const headers = new Headers(); - headers.set("x-trpc-source", "nextjs-react"); - return headers; - }, - transformer: SuperJSON, + url: "/api/trpc", }), ], + transformer: SuperJSON, }); function getBaseUrl() {