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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
/.pnp
.pnp.js

.pnpm-store

# testing
/coverage

Expand Down
3 changes: 3 additions & 0 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ const config: NextConfig = {
middlewareClientMaxBodySize: "128mb",
},

// Transpile mermaid for client-side Mermaid diagram rendering
transpilePackages: ["mermaid"],

eslint: { ignoreDuringBuilds: true },
typescript: { ignoreBuildErrors: true },

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
"lru-cache": "^11.2.6",
"lucide-react": "^0.487.0",
"mammoth": "^1.11.0",
"mermaid": "^11.12.2",
"marked": "^17.0.3",
"motion": "^12.29.2",
"neo4j-driver": "^6.0.1",
Expand Down
785 changes: 783 additions & 2 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

192 changes: 192 additions & 0 deletions src/app/api/repo-explainer/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import { NextResponse } from "next/server";
import { auth } from "@clerk/nextjs/server";
import { z } from "zod";
import {
parseGitHubUrl,
getRepoContext,
} from "~/lib/repo-explainer";
import { explainRepoWithLlm } from "~/lib/repo-explainer/llm";
import { extractMermaidCode, extractSummary } from "~/lib/repo-explainer/prompts";
import type { RepoInfo, RepoExplanationRequest } from "~/lib/repo-explainer/types";

export const runtime = "nodejs";
export const maxDuration = 120;

const RepoExplainerRequestSchema = z.object({
url: z.string().min(1, "GitHub URL is required"),
instructions: z.string().optional(),
});

async function validateRepoAccess(
owner: string,
repo: string,
githubToken?: string | null,
): Promise<string | null> {
try {
const res = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
method: "GET",
headers: {
Accept: "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
...(githubToken ? { Authorization: `Bearer ${githubToken}` } : {}),
},
cache: "no-store",
});
if (res.status === 404) {
return githubToken
? `Repository '${owner}/${repo}' not found or token does not have access.`
: `Repository '${owner}/${repo}' not found or is private. Add a GitHub token with access for private repos.`;
}
if (res.status === 403) {
return githubToken
? `Access forbidden for '${owner}/${repo}'. Check token permissions and repo access.`
: `Repository '${owner}/${repo}' is private or access is forbidden. Add a GitHub token.`;
}
if (res.status === 401) {
return "Invalid GitHub token.";
}
if (res.status === 429) {
return "Too many requests to GitHub. Please try again later.";
}
if (!res.ok) {
return `Error accessing repository '${owner}/${repo}' (HTTP ${res.status}).`;
}
return null;
} catch (error) {
console.error("[repo-explainer] validateRepoAccess error:", error);
return "Could not validate repository on GitHub. Please try again.";
}
}

export async function POST(request: Request) {
try {
const { userId } = await auth();
if (!userId) {
return NextResponse.json(
{ success: false, message: "Unauthorized" },
{ status: 401 },
);
}

const body = (await request.json()) as unknown;
const parsed = RepoExplainerRequestSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{
success: false,
message: "Invalid input",
errors: parsed.error.flatten(),
},
{ status: 400 },
);
}

const { url, instructions } = parsed.data as RepoExplanationRequest;

const parsedUrl = parseGitHubUrl(url);
if (!parsedUrl) {
return NextResponse.json(
{
success: false,
message:
"Invalid GitHub URL format. Please use: https://github.com/owner/repo or owner/repo",
},
{ status: 400 },
);
}

const { owner, repo } = parsedUrl;
const githubToken =
request.headers.get("X-GitHub-Token") ||
process.env.GITHUB_TOKEN ||
null;

const validationError = await validateRepoAccess(owner, repo, githubToken);
if (validationError) {
return NextResponse.json(
{
success: false,
message: validationError,
},
{ status: 400 },
);
}

const repoInfo: RepoInfo = { owner, repoName: repo };

const statusStages: string[] = [];
const statusCallback = (stage: string) => {
statusStages.push(stage);
};

const contextResult = await getRepoContext(
repoInfo,
null,
githubToken,
statusCallback,
);
if (!contextResult.success) {
return NextResponse.json(
{
success: false,
message: contextResult.error ?? "Failed to fetch repository context",
},
{ status: 500 },
);
}

const explanationResult = await explainRepoWithLlm(
repoInfo,
contextResult.context,
instructions,
statusCallback,
);

if (!explanationResult.success) {
return NextResponse.json(
{
success: false,
message: explanationResult.error ?? "Failed to generate explanation",
},
{ status: 500 },
);
}

const timestamp = new Date().toISOString();
const repoFullName = `${owner}/${repo}`;
const summary = extractSummary(explanationResult.explanation);
const mermaidCode = extractMermaidCode(explanationResult.explanation);

return NextResponse.json(
{
success: true,
data: {
explanation: explanationResult.explanation,
repo: repoFullName,
summary,
mermaidCode,
umlJson: {
format: "mermaid",
repo: repoFullName,
summary,
diagram: mermaidCode,
generatedAt: timestamp,
},
timestamp,
},
},
{ status: 200 },
);
} catch (error) {
console.error("[repo-explainer] POST error:", error);
return NextResponse.json(
{
success: false,
message: "An error occurred while explaining the repository",
error: error instanceof Error ? error.message : "Unknown error",
},
{ status: 500 },
);
}
}

3 changes: 3 additions & 0 deletions src/app/employer/documents/components/EmployerDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import type { DocumentType, CategoryGroup } from "../types";
import type { ViewMode } from "../types";
import { getDocumentDisplayType } from "../types/document";
import { DISPLAY_TYPE_ICONS } from "./DocumentViewer";
import { RepoExplainerPanel } from "./RepoExplainerPanel";

interface EmployerDashboardProps {
documents: DocumentType[];
Expand Down Expand Up @@ -477,6 +478,8 @@ export function EmployerDashboard({
)}
</div>
</div>

<RepoExplainerPanel />
</div>
</div>
);
Expand Down
Loading