Skip to content
27 changes: 26 additions & 1 deletion src/app/dashboard/[org]/[repo]/todos/Todo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -27,6 +28,7 @@ const Todo: React.FC<TodoProps> = ({ org, repo, project }) => {
const [searchQuery, setSearchQuery] = useState("");
const [isLoadingIssue, setIsLoadingIssue] = useState(false);
const [filteredTodos, setFilteredTodos] = useState<Todo[]>([]);
const [research, setResearch] = useState<Research[]>([]);
const {
data: todos,
isLoading: isLoadingTodos,
Expand All @@ -35,6 +37,14 @@ const Todo: React.FC<TodoProps> = ({ 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,
Expand Down Expand Up @@ -70,7 +80,7 @@ const Todo: React.FC<TodoProps> = ({ org, repo, project }) => {
};

void fetchIssue();
}, [selectedTodo, org, repo]);


useEffect(() => {
// Filter todos based on searchQuery
Expand Down Expand Up @@ -98,6 +108,19 @@ const Todo: React.FC<TodoProps> = ({ 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 (
<div className="flex h-full w-full flex-col overflow-clip rounded-md dark:bg-gray-900 lg:flex-row">
Expand Down Expand Up @@ -147,6 +170,8 @@ const Todo: React.FC<TodoProps> = ({ org, repo, project }) => {
selectedTodo={selectedTodo}
selectedIssue={selectedIssue}
isLoadingIssue={isLoadingIssue}
research={research}
onResearchComplete={handleResearchComplete}
org={org}
repo={repo}
/>
Expand Down
49 changes: 43 additions & 6 deletions src/app/dashboard/[org]/[repo]/todos/components/Issue.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -23,25 +24,35 @@ const Issue: React.FC<IssueProps> = ({
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 ?? "");
setIssueBody(selectedIssue?.body ?? "");
}, [selectedIssue]);

const { mutateAsync: updateIssue } = api.github.updateIssue.useMutation();
const { mutateAsync: researchIssue } = api.todos.researchIssue.useMutation();

const handleSaveIssue = async () => {
try {
Expand Down Expand Up @@ -81,6 +92,22 @@ const Issue: React.FC<IssueProps> = ({
}
};

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 <LoadingIndicator />;
}
Expand Down Expand Up @@ -205,6 +232,16 @@ const Issue: React.FC<IssueProps> = ({
<h3 className="mb-3 text-lg font-semibold text-slate-700 dark:text-slate-300">
Research
</h3>
<Button
onClick={handleResearch}
disabled={isResearching}
className="mb-4"
>
{isResearching ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : null}
{isResearching ? "Researching..." : "Research"}
</Button>
{isLoadingResearch ? (
<LoadingIndicator />
) : (
Expand Down
43 changes: 42 additions & 1 deletion src/server/agent/research.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export enum ResearchAgentActionType {
ResearchCodebase = "ResearchCodebase",
ResearchInternet = "ResearchInternet",
AskProjectOwner = "AskProjectOwner",
GenerateResearch = "GenerateResearch",
ResearchComplete = "ResearchComplete",
}

Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -235,7 +254,7 @@ export const researchIssue = async function (
};

async function callFunction(
functionName: ResearchAgentActionType,
githubIssue: string | number,
args: { query: string },
githubIssue: string,
sourceMap: string,
Expand All @@ -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:
Expand Down Expand Up @@ -438,5 +459,25 @@ export async function researchInternet(query: string): Promise<string> {
"llama-3.1-sonar-large-128k-online",
);

async function generateResearch(issueId: number): Promise<string> {
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.";
}
28 changes: 27 additions & 1 deletion src/server/api/routers/todos.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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<void> => {
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}`);
}
}),
});
1 change: 1 addition & 0 deletions src/server/db/tables/research.table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}));
}
11 changes: 3 additions & 8 deletions src/trpc/client.tsx
Original file line number Diff line number Diff line change
@@ -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<AppRouter>({
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() {
Expand Down