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
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