Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
395 changes: 321 additions & 74 deletions bun.lock

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@ai-sdk/google": "^1.2.13",
"@ai-sdk/openai": "^3.0.18",
"@google/generative-ai": "^0.24.0",
"@hookform/resolvers": "^5.0.1",
"@langchain/community": "^0.3.41",
Expand Down Expand Up @@ -64,7 +64,7 @@
"@types/react-syntax-highlighter": "^15.5.13",
"@uiw/react-md-editor": "^4.0.5",
"@uploadcare/upload-client": "^6.14.3",
"ai": "^4.3.9",
"ai": "^6.0.49",
"assemblyai": "^4.12.2",
"axios": "^1.8.4",
"better-auth": "^1.3.4",
Expand All @@ -76,6 +76,7 @@
"embla-carousel-react": "^8.6.0",
"firebase": "^11.6.1",
"ignore": "^7.0.3",
"inngest": "^3.49.3",
"input-otp": "^1.4.2",
"lucide-react": "^0.501.0",
"motion": "^12.9.4",
Expand All @@ -100,7 +101,7 @@
"tw-animate-css": "^1.2.5",
"usehooks-ts": "^3.1.1",
"vaul": "^1.1.2",
"zod": "^3.24.3"
"zod": "^4.3.6"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
Expand Down
12 changes: 12 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ model Project {
githubUrl String
githubToken String?
deletedAt DateTime?
indexStatus IndexStatus @default(PENDING)
indexProgress Int @default(0)
totalFiles Int?
processedFiles Int @default(0)
indexError String?
userToProject UserToProject[]
commit Commit[]
SourceCodeEmbedding SourceCodeEmbedding[]
Expand Down Expand Up @@ -180,3 +185,10 @@ enum MeetingStatus {
PROCESSING
COMPLETED
}

enum IndexStatus {
PENDING
PROCESSING
READY
FAILED
}
108 changes: 76 additions & 32 deletions src/app/(protected)/create/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import useRefetch from "@/hooks/useRefetch";
import { api } from "@/trpc/react";
import { Info } from "lucide-react";

import { Info, Loader2 } from "lucide-react";
import Image from "next/image";
import React from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { useRouter } from "next/navigation";

type FormInput = {
repoUrl: string;
projectName: string;
Expand All @@ -17,40 +18,63 @@ type FormInput = {

const CreatePage = () => {
const { register, handleSubmit, reset } = useForm<FormInput>();
const router = useRouter();
const checkCredits = api.project.checkCredits.useMutation();
const { mutateAsync, isPending, data } =
api.project.createProject.useMutation();
const { mutateAsync, isPending } = api.project.createProject.useMutation();
const refetch = useRefetch();

async function onSubmit(data: FormInput) {
if (!checkCredits.data) {
await mutateAsync({
githubUrl: data.repoUrl,
name: data.projectName,
githubToken: data.githubToke,
});
if (data) {
toast.success("Project created successfully!");
// First click - check credits
checkCredits.mutate(
{
githubUrl: data.repoUrl,
githubToken: data.githubToke,
},
{
onSuccess: (result) => {
if (result.fileCount > result.credits) {
toast.error("Not enough credits!");
}
},
},
);
} else {
// Second click - create project
try {
const project = await mutateAsync({
githubUrl: data.repoUrl,
name: data.projectName,
githubToken: data.githubToke,
});

toast.success(
"Project created! Repository indexing is running in the background.",
);
reset();
checkCredits.reset();
refetch();
} else {

// Redirect to dashboard after a short delay
setTimeout(() => {
router.push("/dashboard");
}, 1500);
} catch (error) {
toast.error("Error creating project!");
}
} else {
checkCredits.mutate({
githubUrl: data.repoUrl,
githubToken: data.githubToke,
});
}
}

const hasEnoughCredits = checkCredits.data?.fileCount
? checkCredits.data.fileCount <= checkCredits.data.credits
: false;

const canCreate = !!checkCredits.data && hasEnoughCredits;

return (
<div className="flex h-full items-center justify-center gap-12">
<img
src={
"https://img.freepik.com/free-vector/cloud-computing-concept_53876-64621.jpg?t=st=1745953922~exp=1745957522~hmac=6ed60f56b56f146e8a74b4eee14dd24b1ada46cfc5ed77f99701197100bb6cfc&w=1060"
}
src="https://img.freepik.com/free-vector/cloud-computing-concept_53876-64621.jpg?t=st=1745953922~exp=1745957522~hmac=6ed60f56b56f146e8a74b4eee14dd24b1ada46cfc5ed77f99701197100bb6cfc&w=1060"
alt="logo"
className="h-56 w-auto"
/>
Expand All @@ -62,52 +86,72 @@ const CreatePage = () => {
<p className="text-muted-foreground text-sm">
Enter the URL of your repository to link it to DevSage.
</p>
<div className="h-4"></div>
<div className="h-4" />
<div>
<form action="" onSubmit={handleSubmit(onSubmit)}>
<form onSubmit={handleSubmit(onSubmit)}>
<Input
required
{...register("projectName", { required: true })}
placeholder="Project Name"
/>
<div className="h-2"></div>
<div className="h-2" />
<Input
required
{...register("repoUrl", { required: true })}
type="url"
placeholder="Github URL"
/>
<div className="h-2"></div>
<div className="h-2" />
<Input
{...register("githubToke")}
placeholder="Github Token (optional)"
/>
{!!checkCredits.data && (
<>
<div className="mt-4 rounded-md border border-orange-200 bg-orange-50 px-4 py-2 text-orange-700">
<div className="mt-4 rounded-md border border-blue-200 bg-blue-50 px-4 py-2 text-blue-700">
<div className="flex items-center gap-2">
<Info className="size-4" />
<p className="text-sm">
You will be charged{" "}
<strong>{checkCredits.data.credits}</strong> credits for
this repository.
This repository has{" "}
<strong>{checkCredits.data.fileCount}</strong> files.
</p>
</div>
<p className="text-sm text-blue-600">
You have <strong>{checkCredits.data.fileCount}</strong>{" "}
You have <strong>{checkCredits.data.credits}</strong>{" "}
credits remaining.
</p>
{!hasEnoughCredits && (
<p className="text-sm text-red-600 font-medium">
Not enough credits!
</p>
)}
</div>
</>
)}
<div className="h-4"></div>
<div className="h-4" />
<Button
type="submit"
disabled={
isPending || !!checkCredits.isPending || hasEnoughCredits
isPending ||
checkCredits.isPending ||
(checkCredits.data && !hasEnoughCredits)
}
>
{!!checkCredits.data ? "Create project" : "Check credit"}
{isPending ? (
<>
<Loader2 className="mr-2 size-4 animate-spin" />
Creating...
</>
) : checkCredits.isPending ? (
<>
<Loader2 className="mr-2 size-4 animate-spin" />
Checking...
</>
) : canCreate ? (
"Create Project"
) : (
"Check Credits"
)}
</Button>
</form>
</div>
Expand Down
101 changes: 60 additions & 41 deletions src/app/(protected)/dashboard/actions.ts
Original file line number Diff line number Diff line change
@@ -1,67 +1,86 @@
"use server";

import { streamText } from "ai";
import { createStreamableValue } from "ai/rsc";
import { createGoogleGenerativeAI } from "@ai-sdk/google";
import { generateEmbedding } from "@/lib/gemini";
import { generateEmbedding, openrouter } from "@/lib/gemini";
import { db } from "@/server/db";
const google = createGoogleGenerativeAI({
apiKey: process.env.GEMINI_API_KEY,
});

export async function askQuestion(question: string, projectId: string) {
const stream = createStreamableValue();
// 1️⃣ Create embedding
const queryVector = await generateEmbedding(question);
const vectorQuery = `[${queryVector.join(",")}]`;

const result = (await db.$queryRaw`
SELECT "fileName","sourceCode","summary",1-("summaryEmbedding"<=> ${vectorQuery}::vector) AS similarity
// 2️⃣ Vector similarity search (pgvector)
const result = await db.$queryRaw<
{
fileName: string;
sourceCode: string;
summary: string;
similarity: number;
}[]
>`
SELECT
"fileName",
"sourceCode",
"summary",
1 - ("summaryEmbedding" <=> ${vectorQuery}::vector) AS similarity
FROM "SourceCodeEmbedding"
WHERE 1-("summaryEmbedding"<=> ${vectorQuery}::vector)> 0.5
AND "projectId" = ${projectId}
WHERE
1 - ("summaryEmbedding" <=> ${vectorQuery}::vector) > 0.5
AND "projectId" = ${projectId}
ORDER BY similarity DESC
LIMIT 10
`) as { fileName: string; sourceCode: string; summary: string }[];

let context = "";
for (const doc of result) {
context += `source: ${doc.sourceCode}\ncode content: ${doc.sourceCode}\n summary of the file ${doc.summary}\n\n`;
}
`;

(async () => {
const { textStream } = streamText({
model: google("gemini-1.5-flash"),
prompt: `
You are an AI code assistant designed to help a technical intern who is new to the codebase.
// 3️⃣ Build context
const context = result
.map(
(doc) => `
FILE: ${doc.fileName}

You have expert knowledge, are helpful, articulate, friendly, and thoughtful. Your responses should be clear, accurate, and detailed. You explain concepts step-by-step, using markdown syntax with relevant code snippets, analogies, or links when appropriate.
CODE:
${doc.sourceCode}

When a <Context>...</Context> block is provided, use it as the primary source to answer questions specifically related to the codebase. If the answer is not fully covered by the context, supplement it with your broader knowledge to provide the best possible help.
SUMMARY:
${doc.summary}
`
)
.join("\n\n---\n\n");

If neither the context nor your general knowledge allows you to answer confidently, then respond:
**"I'm sorry, but I don't have the answer to that question."**
// 4️⃣ Stream response (LATEST SDK WAY)
const streamResult = await streamText({
model: openrouter("google/gemini-2.0-flash-exp:free"),
temperature: 0.2,
messages: [
{
role: "system",
content: `
You are an AI code assistant helping a technical intern understand a codebase.

Your goal is to be a helpful, inspiring, and empowering assistant for someone learning a complex codebase from scratch.

START CONTEXT BLOCK
Rules:
- Be clear and structured
- Explain step-by-step
- Use the provided context as the primary source
- If you cannot answer confidently, say:
"I'm sorry, but I don't have the answer to that question."
`,
},
{
role: "user",
content: `
<Context>
${context}
</Context>
END CONTEXT BLOCK

START QUESTION
<Question>
${question}
END QUESTION

`,
});
for await (const delta of textStream) {
stream.update(delta);
}
stream.done();
})();
</Question>
`,
},
],
});

return {
output: stream.value,
stream: streamResult.textStream,
fileReferences: result,
};
}
Loading