Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules
dist
.vercel
.env
.env.local
*.local
78 changes: 78 additions & 0 deletions api/analyze.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import type { VercelRequest, VercelResponse } from "@vercel/node";

export default async function handler(req: VercelRequest, res: VercelResponse) {
if (req.method !== "POST") {
return res.status(405).json({ error: "Method not allowed" });
}

const apiKey = process.env.ANTHROPIC_API_KEY;
if (!apiKey) {
return res.status(501).json({ error: "AI analysis not configured" });
}

const { files } = req.body;
if (!files || !Array.isArray(files)) {
return res.status(400).json({ error: "Missing files" });
}

// Build a summary of the diff
const diffSummary = files
.map(
(f: { filename: string; status: string; additions: number; deletions: number; patch?: string }) =>
`File: ${f.filename} (${f.status}, +${f.additions} -${f.deletions})\n${f.patch ? f.patch.slice(0, 2000) : "(no patch)"}`,
)
.join("\n\n");

try {
const response = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": apiKey,
"anthropic-version": "2023-06-01",
},
body: JSON.stringify({
model: "claude-haiku-4-5-20251001",
max_tokens: 1024,
messages: [
{
role: "user",
content: `Analyze this PR diff and identify the affected areas that a reviewer should test. Group by feature area, route, or component.

Return JSON only (no markdown fences), in this format:
[{"title": "Area name", "description": "What changed and what to test", "files": ["file1.ts", "file2.ts"]}]

Diff:
${diffSummary}`,
},
],
}),
});

if (!response.ok) {
return res.status(502).json({ error: "AI API call failed" });
}

const data = await response.json();
const text = data.content?.[0]?.text || "[]";

let areas;
try {
areas = JSON.parse(text);
} catch {
return res.status(502).json({ error: "Failed to parse AI response" });
}

return res.json({
areas: areas.map((a: { title: string; description: string; files: string[] }, i: number) => ({
id: `area-${i}`,
title: a.title,
description: a.description,
files: a.files,
checked: false,
})),
});
} catch (e) {
return res.status(500).json({ error: "Analysis failed" });
}
}
44 changes: 44 additions & 0 deletions api/auth/callback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { VercelRequest, VercelResponse } from "@vercel/node";

export default async function handler(req: VercelRequest, res: VercelResponse) {
const { code } = req.query;

if (!code || typeof code !== "string") {
return res.status(400).json({ error: "Missing code parameter" });
}

const clientId = process.env.GITHUB_CLIENT_ID;
const clientSecret = process.env.GITHUB_CLIENT_SECRET;

if (!clientId || !clientSecret) {
return res.status(500).json({ error: "GitHub OAuth not configured" });
}

try {
const tokenRes = await fetch("https://github.com/login/oauth/access_token", {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({
client_id: clientId,
client_secret: clientSecret,
code,
}),
});

const data = await tokenRes.json();

if (data.error) {
return res.status(400).json({ error: data.error_description || data.error });
}

// Redirect back to the app with the token
const redirectUrl = new URL("/", `https://${req.headers.host}`);
redirectUrl.searchParams.set("token", data.access_token);
return res.redirect(302, redirectUrl.toString());
} catch (e) {
return res.status(500).json({ error: "OAuth exchange failed" });
}
}
12 changes: 12 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Preview PR</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
Loading