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
1 change: 0 additions & 1 deletion src/app/api/local-coding/keys/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ export async function POST(req: NextRequest) {
.from("local_coding_api_keys")
.insert({
user_id: user.id,
api_key: apiKeyHash,
api_key_hash: apiKeyHash,
name,
})
Expand Down
43 changes: 31 additions & 12 deletions src/app/api/local-coding/sync/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,43 @@ function hashApiKey(key: string): string {

async function authenticateApiKey(apiKey: string): Promise<string | null> {
const keyHash = hashApiKey(apiKey);
const keyHashFilter = `api_key_hash.eq.${keyHash},api_key.eq.${keyHash}`;

const { data: keyRecord } = await supabaseAdmin
// Step 1: Try modern hashed lookup first
const { data: hashedRecord } = await supabaseAdmin
.from("local_coding_api_keys")
.select("user_id")
.or(keyHashFilter)
.single();

if (!keyRecord) {
return null;
.select("id, user_id")
.eq("api_key_hash", keyHash)
.maybeSingle();

if (hashedRecord) {
await supabaseAdmin
.from("local_coding_api_keys")
.update({ last_used_at: new Date().toISOString() })
.eq("id", hashedRecord.id);
return hashedRecord.user_id;
}

await supabaseAdmin
// Step 2: Fallback for legacy plaintext keys (api_key_hash IS NULL)
const { data: legacyRecord } = await supabaseAdmin
.from("local_coding_api_keys")
.update({ last_used_at: new Date().toISOString() })
.or(keyHashFilter);
.select("id, user_id, api_key")
.is("api_key_hash", null)
.eq("api_key", apiKey)
.maybeSingle();

if (legacyRecord) {
// Backfill hash — migrate legacy key to hash-only verification
await supabaseAdmin
.from("local_coding_api_keys")
.update({
api_key_hash: keyHash,
last_used_at: new Date().toISOString(),
})
.eq("id", legacyRecord.id);
return legacyRecord.user_id;
}

return keyRecord.user_id;
return null;
}

function validateDays(days: number): number {
Expand Down
160 changes: 160 additions & 0 deletions src/app/api/project-tutor/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";

const GROQ_API_URL = "https://api.groq.com/openai/v1/chat/completions";

async function callGroq(prompt: string): Promise<string> {
const apiKey = process.env.GROQ_API_KEY;
if (!apiKey) return "AI insights unavailable — GROQ_API_KEY not configured.";

const res = await fetch(GROQ_API_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
model: "llama3-8b-8192",
messages: [{ role: "user", content: prompt }],
max_tokens: 1024,
temperature: 0.7,
}),
});

if (!res.ok) throw new Error("Groq API error");
const data = await res.json();
return data.choices?.[0]?.message?.content ?? "No response generated.";
}

async function fetchRepoData(owner: string, repo: string) {
const token = process.env.GITHUB_TOKEN;
const headers: Record<string, string> = {
Accept: "application/vnd.github.v3+json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
};

const [repoRes, readmeRes, languagesRes] = await Promise.all([
fetch(`https://api.github.com/repos/${owner}/${repo}`, { headers }),
fetch(`https://api.github.com/repos/${owner}/${repo}/readme`, { headers }),
fetch(`https://api.github.com/repos/${owner}/${repo}/languages`, { headers }),
]);

const repoData = repoRes.ok ? await repoRes.json() : {};
const readmeData = readmeRes.ok ? await readmeRes.json() : {};
const languages = languagesRes.ok ? await languagesRes.json() : {};

let readmeContent = "";
if (readmeData.content) {
readmeContent = Buffer.from(readmeData.content, "base64").toString("utf-8").slice(0, 2000);
}

return { repoData, readmeContent, languages };
}

export async function POST(req: NextRequest) {
const session = await getServerSession(authOptions);
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });

try {
const { repoUrl, action, question } = await req.json();

if (!repoUrl) return NextResponse.json({ error: "repo URL required" }, { status: 400 });

const match = repoUrl.match(/github\.com\/([^/]+)\/([^/]+)/);
if (!match) return NextResponse.json({ error: "Invalid GitHub URL" }, { status: 400 });

const [, owner, repo] = match;
const { repoData, readmeContent, languages } = await fetchRepoData(owner, repo);

const techStack = Object.keys(languages).join(", ") || "Unknown";
const description = repoData.description || "No description";
const repoName = repoData.name || repo;
const topics = (repoData.topics || []).join(", ");

const context = `
Project: ${repoName}
Description: ${description}
Tech Stack: ${techStack}
Topics: ${topics}
Stars: ${repoData.stargazers_count ?? 0}
README (excerpt): ${readmeContent}
`.trim();

if (action === "analyze") {
const prompt = `You are an expert software engineer helping a student prepare for technical interviews.

Analyze this GitHub project and provide:
1. A brief project summary (2-3 sentences)
2. Key features (3-5 bullet points)
3. Tech stack breakdown and why each technology was likely chosen
4. Potential architectural decisions and tradeoffs
5. Common challenges in this type of project

Project context:
${context}

Format your response with clear headings using markdown.`;

const analysis = await callGroq(prompt);
return NextResponse.json({ analysis, techStack, description: repoData.description });
}

if (action === "questions") {
const prompt = `You are a senior software engineer conducting technical interviews.

Generate interview questions for this project at three difficulty levels:

Project context:
${context}

Generate exactly:
- 3 Easy questions (basic understanding, what/why questions)
- 3 Medium questions (implementation details, design decisions)
- 3 Advanced questions (scalability, edge cases, improvements)

Format as JSON:
{
"easy": ["question1", "question2", "question3"],
"medium": ["question1", "question2", "question3"],
"advanced": ["question1", "question2", "question3"]
}

Return ONLY the JSON, no other text.`;

const raw = await callGroq(prompt);
try {
const clean = raw.replace(/```json|```/g, "").trim();
const questions = JSON.parse(clean);
return NextResponse.json({ questions });
} catch {
return NextResponse.json({
questions: {
easy: ["What is the main purpose of this project?", "What technologies did you use?", "How do you run this project locally?"],
medium: ["How did you structure your codebase?", "What was the most challenging part?", "How did you handle errors?"],
advanced: ["How would you scale this?", "What would you do differently?", "How would you add authentication?"],
}
});
}
}

if (action === "chat") {
const prompt = `You are an AI tutor helping a student prepare for technical interviews about their project.

Project context:
${context}

Student question: ${question}

Give a clear, concise answer focused on interview preparation. Keep it under 200 words.`;

const answer = await callGroq(prompt);
return NextResponse.json({ answer });
}

return NextResponse.json({ error: "Invalid action" }, { status: 400 });
} catch (error) {
console.error("Project tutor error:", error);
return NextResponse.json({ error: "Something went wrong" }, { status: 500 });
}
}
11 changes: 11 additions & 0 deletions src/app/project-tutor/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { redirect } from "next/navigation";
import ProjectTutorClient from "@/components/ProjectTutorClient";

export default async function ProjectTutorPage() {
const session = await getServerSession(authOptions);
if (!session) redirect("/api/auth/signin/github?callbackUrl=/project-tutor");

return <ProjectTutorClient username={session.user?.name ?? ""} />;
}
Loading