diff --git a/kits/repo-interview-prep/.gitignore b/kits/repo-interview-prep/.gitignore new file mode 100644 index 000000000..996999665 --- /dev/null +++ b/kits/repo-interview-prep/.gitignore @@ -0,0 +1,6 @@ +.lamatic/ +node_modules/ +.env +.env.local +next-env.d.ts + diff --git a/kits/repo-interview-prep/README.md b/kits/repo-interview-prep/README.md new file mode 100644 index 000000000..eadff70d0 --- /dev/null +++ b/kits/repo-interview-prep/README.md @@ -0,0 +1,167 @@ +# Repo Interview Prep + +Turn any public GitHub repository into a complete, code-specific interview preparation brief in seconds. + +Paste a repo URL. Get a 2-minute verbal pitch, 15 tailored follow-up questions with suggested answers, concepts to review, red flags in your code, and strengths to highlight — all grounded in what is actually in your project, not generic advice. + +--- + +## What It Does + +Most candidates struggle to talk about their own projects under pressure. They built the thing, but give vague answers when a senior engineer probes the architecture, trade-offs, or weak points. This kit reads your actual repo — the README, file structure, and tech stack — and generates a deeply technical prep brief tailored to your code. + +**Output includes:** +- `project_summary` — concise 2-3 sentence overview of what the project actually does +- `tech_stack` — list of detected technologies +- `complexity_level` — junior / mid / senior signal +- `pitch` — a memorizable 2-minute verbal pitch in first-person spoken English +- `follow_up_questions` — 15 questions an interviewer would ask, with the signal they're testing and a strong suggested answer +- `concepts_to_review` — what to study before the interview, and how deep to go +- `red_flags` — what a senior engineer will push back on, and how to address it +- `strengths_to_highlight` — what genuinely shows strong engineering judgment in your code +- `architecture` — Mermaid.js system architecture diagram, data flow summary, and design trade-offs +- `grill_me` — 5 aggressive technical questions targeting real flaws, with defensive strategies for each +- `production` — a production-readiness verdict with critical gaps and concrete quick-win fixes + +--- + +## Prerequisites + +| Requirement | Details | +|---|---| +| Firecrawl API Key | Free at [firecrawl.dev](https://firecrawl.dev) — 500 pages/month free | +| LLM Credential | Any capable model; recommended: `gemini-2.0-flash` or `gpt-4o-mini` | +| Public GitHub Repo | The target repository must be publicly accessible | + +--- + +## Setup + +### 1. Lamatic Flow + +1. **Add Firecrawl credential** in Lamatic Studio → Credentials → Firecrawl +2. **Select your LLM** in all four `Generate Text` nodes — configure model and credential +3. **Deploy the flow** + +### 2. Next.js Dashboard (optional local UI) + +```bash +cd apps +cp .env.example .env.local +# Fill in your values from Lamatic Studio → Settings → API Docs +npm install +npm run dev +``` + +Open [http://localhost:3000](http://localhost:3000). + +--- + +## Usage + +Send a POST request to the deployed flow endpoint: + +```json +{ + "github_repo_url": "https://github.com/your-username/your-repo", + "target_role": "SWE Intern", + "jd_text": "We are looking for a backend engineer with experience in distributed systems...", + "github_token": "" +} +``` + +| Field | Required | Description | +|---|---|---| +| `github_repo_url` | ✅ | Full GitHub repo URL | +| `target_role` | ❌ | Role you are interviewing for (improves question relevance) | +| `jd_text` | ❌ | Job description text (tailors questions to a specific role) | +| `github_token` | ❌ | GitHub personal access token (not required for public repos) | + +--- + +## Example Response + +```json +{ + "prep_brief": { + "project_summary": "ATLAS is a distributed AI orchestration platform...", + "tech_stack": ["Python", "FastAPI", "React", "Vite", "Docker", "Redis"], + "complexity_level": "mid", + "pitch": "For my project ATLAS, I built a distributed AI orchestration platform...", + "follow_up_questions": [ + { + "question": "How did you handle race conditions in the distributed queue?", + "why_they_ask": "Testing your understanding of concurrent state management.", + "suggested_answer": "I used Redis transactions (MULTI/EXEC) to ensure atomicity." + } + ], + "concepts_to_review": [ + { + "concept": "Distributed Locks", + "why_relevant": "Critical for the queue worker implementation.", + "depth_needed": "moderate" + } + ], + "red_flags": [ + { + "observation": "API lacks rate limiting.", + "how_to_address": "Acknowledge it was out of scope for MVP, but suggest Redis sliding window." + } + ], + "strengths_to_highlight": [ + "Clean separation of concerns between API and worker processes." + ] + }, + "architecture": { + "mermaid_diagram": "graph TD\n A[API] --> B[Worker]", + "flow_summary": "Requests enter via FastAPI, are queued in Redis...", + "tradeoffs": ["Chosen Redis over RabbitMQ for simplicity at the cost of durability"] + }, + "grill_me": { + "questions": [ + { + "question": "Your job registry is in-memory. How does this fail under horizontal scaling?", + "defensive_strategy": "I acknowledge this is an MVP trade-off. In production I would migrate state to Redis hashes..." + } + ] + }, + "production": { + "is_production_ready": false, + "critical_missing_features": ["No CI/CD pipeline", "Missing auth middleware"], + "quick_wins": ["Add a GitHub Actions workflow", "Add API key validation middleware"] + } +} +``` + +--- + +## Flow Architecture + +```text +API Trigger + → Code Node (parses GitHub URL into owner + repo) + → Firecrawl Node (scrapes repo page for README + file listing) + → Generate Text #1 (LLM generates prep_brief as JSON) + → Generate Text #2 (LLM generates architecture diagram + trade-offs) + → Generate Text #3 (LLM generates grill_me simulation questions) + → Generate Text #4 (LLM evaluates production readiness) + → API Response (returns all four sections) +``` + +--- + +## Troubleshooting + +| Issue | Fix | +|---|---| +| Output describes an "empty repo" | Firecrawl credential is missing or invalid — check credentials in Studio | +| Code node parse error | GitHub URL is malformed — use exact format: `https://github.com/owner/repo` | +| Output is not valid JSON | Switch to a stronger model (`gemini-1.5-pro` or `gpt-4o`) | +| Questions are too generic | Add `target_role` and `jd_text` to the request payload | +| `architecture`/`grill_me`/`production` is empty | Check that node IDs in the API Response mapping match your canvas node IDs | + +--- + +## Author + +Built by [Ganesh Bamalwa](mailto:ganeshbamalwa89@gmail.com) for the Lamatic AgentKit Challenge. diff --git a/kits/repo-interview-prep/agent.md b/kits/repo-interview-prep/agent.md new file mode 100644 index 000000000..c817f035e --- /dev/null +++ b/kits/repo-interview-prep/agent.md @@ -0,0 +1,152 @@ +# Repo Interview Prep + +## Overview + +**Repo Interview Prep** is a multi-agent Lamatic kit that turns any public GitHub repository into a complete, code-specific interview preparation suite. It scrapes the repository's README, file listing, and project description via Firecrawl, then fans out to four sequential LLM nodes — each specializing in a different dimension of the analysis — and returns a structured aggregate response. + +The output is grounded in what is actually in the repository — not generic interview advice. + +--- + +## Purpose + +Candidates routinely struggle to articulate their own projects in interviews. They built the thing, but under pressure they give vague answers or miss the deeper engineering signals an interviewer is probing for. This kit solves that by: + +1. **Reading the actual code context** via Firecrawl's GitHub page scraper +2. **Generating tailored questions** based on the real tech stack, architecture, and trade-offs present in the repo +3. **Writing suggested answers** that the candidate can personalize and rehearse +4. **Surfacing red flags honestly** — what an interviewer will push back on, and how to address it +5. **Drawing architecture diagrams** from the codebase using Mermaid.js +6. **Simulating aggressive technical grilling** with 5 targeted questions and defensive strategies +7. **Evaluating production readiness** with concrete quick-win improvement steps + +--- + +## Flows + +### `repo-interview-prep` + +| Property | Value | +|---|---| +| Trigger | API Request (GraphQL) | +| Inputs | `github_repo_url` (required), `target_role` (optional), `jd_text` (optional), `github_token` (optional) | +| Outputs | `prep_brief`, `architecture`, `grill_me`, `production` | + +**Node pipeline:** + +```text +API Trigger → Code Node → Firecrawl Node → LLM #1 (prep_brief) → LLM #2 (architecture) → LLM #3 (grill_me) → LLM #4 (production) → API Response +``` + +1. **API Trigger** — receives the GitHub repo URL and optional context (target role, job description) +2. **Code Node** — parses the URL to extract `owner`, `repo`, and constructs the full GitHub page URL +3. **Firecrawl Node** (`syncSingleScrape`) — scrapes the GitHub repository page and returns cleaned markdown +4. **Generate Text #1 (prep_brief)** — generates the core interview brief: pitch, questions, concepts, red flags +5. **Generate Text #2 (architecture)** — generates a Mermaid diagram, data flow summary, and trade-off list +6. **Generate Text #3 (grill_me)** — generates 5 aggressive technical questions with defensive strategies +7. **Generate Text #4 (production)** — evaluates production readiness and returns a gap analysis with quick wins +8. **API Response** — returns all four sections as a single JSON payload + +--- + +## Guardrails + +- All prompts treat scraped repository content as **untrusted external data** and explicitly instruct the model not to follow instructions found inside it, reducing prompt-injection risk from malicious READMEs +- Every LLM node instructs the model to return **raw JSON only** — no markdown fences, no preamble +- The `jsonrepair` library on the Next.js server action provides a fallback parse layer for truncated or slightly malformed LLM outputs +- The constitution (`@constitutions/default.md`) applies standard safety, PII, and tone guardrails + +--- + +## Integration Reference + +| Service | Purpose | Required | +|---|---|---| +| Firecrawl | Scrapes GitHub repository page for README and file listing | Yes — configure Firecrawl credentials in Lamatic | +| LLM Provider | Generates all four analysis sections | Yes — configure model in all four `Generate Text` nodes | +| GitHub Token | Not currently used; `firecrawlNode_808` does not pass it to any API call | No — optional, passed as `github_token` in request payload | + +--- + +## Environment Setup + +### Lamatic Flow + +1. **Firecrawl API Key** — sign up at [firecrawl.dev](https://firecrawl.dev) (free tier: 500 pages/month), add credential in Lamatic Studio +2. **LLM Model** — any capable chat model works; recommended: `gemini-2.0-flash` or `gpt-4o-mini` +3. Deploy the flow and copy the **Flow ID** and **Project API Key** from Lamatic Studio → Settings → API Docs + +### Next.js Dashboard + +```bash +cd apps +cp .env.example .env.local +# Fill in LAMATIC_PROJECT_ENDPOINT, LAMATIC_FLOW_ID, and LAMATIC_PROJECT_API_KEY +npm install +npm run dev +``` + +--- + +## Inputs + +| Field | Type | Required | Description | +|---|---|---|---| +| `github_repo_url` | `string` | Yes | Full GitHub URL, e.g. `https://github.com/username/repo` | +| `target_role` | `string` | No | Role the candidate is interviewing for, e.g. `SWE Intern` | +| `jd_text` | `string` | No | Job description text to tailor questions to a specific role | +| `github_token` | `string` | No | Personal access token for GitHub (placeholder, not currently used) | + +--- + +## Output Schema + +The API response contains four top-level keys: + +```json +{ + "prep_brief": { + "project_summary": "string", + "tech_stack": ["string"], + "complexity_level": "junior | mid | senior", + "pitch": "string", + "follow_up_questions": [ + { "question": "string", "why_they_ask": "string", "suggested_answer": "string" } + ], + "concepts_to_review": [ + { "concept": "string", "why_relevant": "string", "depth_needed": "surface | moderate | deep" } + ], + "red_flags": [ + { "observation": "string", "how_to_address": "string" } + ], + "strengths_to_highlight": ["string"] + }, + "architecture": { + "mermaid_diagram": "string", + "flow_summary": "string", + "tradeoffs": ["string"] + }, + "grill_me": { + "questions": [ + { "question": "string", "defensive_strategy": "string" } + ] + }, + "production": { + "is_production_ready": false, + "critical_missing_features": ["string"], + "quick_wins": ["string"] + } +} +``` + +--- + +## Common Failure Modes + +| Symptom | Likely Cause | Fix | +|---|---|---| +| `prep_brief` contains generic empty-repo advice | Firecrawl failed to scrape the GitHub page | Verify the Firecrawl credential is valid and the repo URL is public | +| Parse error on code node | Malformed GitHub URL passed in `github_repo_url` | Ensure URL follows `https://github.com/owner/repo` format | +| Section contains empty string | LLM node ID mismatch in API Response mapping | Check that `outputMapping` in the API Response node references correct node IDs | +| Output is not valid JSON | LLM prefixed the JSON with explanation text | Add stricter phrasing to the user prompt or switch to a stronger model | +| `architecture`/`grill_me`/`production` is empty | Rate limiting from parallel LLM calls | Ensure nodes are wired sequentially, not in parallel | diff --git a/kits/repo-interview-prep/apps/.env.example b/kits/repo-interview-prep/apps/.env.example new file mode 100644 index 000000000..ef2855db9 --- /dev/null +++ b/kits/repo-interview-prep/apps/.env.example @@ -0,0 +1,7 @@ +# Environment variables for Repo Interview Prep +# Copy this file to .env.local and fill in your values from Lamatic Studio → Settings → API Docs + +LAMATIC_PROJECT_ENDPOINT=https://your-project-endpoint.lamatic.workers.dev +LAMATIC_FLOW_ID=your-flow-id-uuid +LAMATIC_PROJECT_ID=your-project-id-uuid +LAMATIC_PROJECT_API_KEY=lt-your-api-key diff --git a/kits/repo-interview-prep/apps/.gitignore b/kits/repo-interview-prep/apps/.gitignore new file mode 100644 index 000000000..fe6ccfe71 --- /dev/null +++ b/kits/repo-interview-prep/apps/.gitignore @@ -0,0 +1,6 @@ +.env.local +.env*.local +node_modules/ +.next/ +out/ +dist/ diff --git a/kits/repo-interview-prep/apps/AGENTS.md b/kits/repo-interview-prep/apps/AGENTS.md new file mode 100644 index 000000000..643577dfa --- /dev/null +++ b/kits/repo-interview-prep/apps/AGENTS.md @@ -0,0 +1,9 @@ + + +# This is NOT the Next.js you know + +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. + +This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean. + + diff --git a/kits/repo-interview-prep/apps/CLAUDE.md b/kits/repo-interview-prep/apps/CLAUDE.md new file mode 100644 index 000000000..496bc0055 --- /dev/null +++ b/kits/repo-interview-prep/apps/CLAUDE.md @@ -0,0 +1,3 @@ +# Repo Interview Prep — Claude Instructions + +@AGENTS.md diff --git a/kits/repo-interview-prep/apps/actions/orchestrate.ts b/kits/repo-interview-prep/apps/actions/orchestrate.ts new file mode 100644 index 000000000..e0af1ec45 --- /dev/null +++ b/kits/repo-interview-prep/apps/actions/orchestrate.ts @@ -0,0 +1,183 @@ +"use server"; + +import { lamaticClient } from "@/lib/lamatic-client"; +import { config } from "../orchestrate"; +import type { PrepBrief, ArchitectureAnalysis, GrillQuestion, ProductionReadiness, RepoAnalysis } from "@/lib/types"; +import { z } from "zod"; + +// Helper to coerce LLM arrays-of-objects back into arrays-of-strings +const robustStringArray = z.preprocess((val: any) => { + if (Array.isArray(val)) { + return val.map((item: any) => { + if (typeof item === "string") return item; + if (typeof item === "object" && item !== null) { + const vals = Object.values(item); + if (vals.length > 0 && typeof vals[0] === "string") return vals[0]; + return JSON.stringify(item); + } + return String(item); + }); + } + return val; +}, z.array(z.string())); + +const RepoAnalysisSchema = z.object({ + prep_brief: z.object({ + project_summary: z.string(), + tech_stack: robustStringArray, + complexity_level: z.enum(["junior", "mid", "senior"]).catch("mid"), + pitch: z.string(), + follow_up_questions: z.array( + z.object({ + question: z.string(), + why_they_ask: z.string(), + suggested_answer: z.string() + }) + ), + concepts_to_review: z.array( + z.object({ + concept: z.string(), + why_relevant: z.string(), + depth_needed: z.enum(["surface", "moderate", "deep"]).catch("moderate") + }) + ), + red_flags: z.array( + z.object({ + observation: z.string(), + how_to_address: z.string() + }) + ).optional().default([]), + strengths_to_highlight: robustStringArray + }), + architecture: z.object({ + mermaid_diagram: z.string(), + flow_summary: z.string(), + tradeoffs: robustStringArray + }), + grill_me: z.object({ + questions: z.array( + z.object({ + question: z.string(), + defensive_strategy: z.string() + }) + ) + }), + production: z.object({ + is_production_ready: z.boolean(), + critical_missing_features: robustStringArray, + quick_wins: robustStringArray + }) +}); + +import { jsonrepair } from "jsonrepair"; + +// Helper to robustly parse JSON from string +function safeParse(raw: any, fallbackName: string): T { + if (!raw) { + throw new Error(`No ${fallbackName} found in response. Check workflow output configuration.`); + } + + try { + const jsonStr = typeof raw === "string" ? raw : JSON.stringify(raw); + const clean = jsonStr.replace(/^```json\s*/i, "").replace(/```\s*$/i, "").trim(); + + try { + return JSON.parse(clean) as T; + } catch (e) { + console.log(`[repo-interview-prep] Standard JSON parse failed for ${fallbackName}, attempting jsonrepair...`); + const repaired = jsonrepair(clean); + return JSON.parse(repaired) as T; + } + } catch (err) { + throw new Error(`Failed to parse ${fallbackName} as JSON, even after repair.`); + } +} + +export async function generatePrepBrief( + github_repo_url: string, + target_role: string, + jd_text: string +): Promise<{ success: boolean; data?: RepoAnalysis; error?: string }> { + try { + if (!process.env.LAMATIC_FLOW_ID) { + throw new Error( + "LAMATIC_FLOW_ID environment variable is not set. Please add it to your .env.local file." + ); + } + + const flows = config.flows; + const flow = flows.step1; + + if (!flow.workflowId) { + throw new Error("Workflow ID not found in config."); + } + + const inputs = { + github_repo_url, + target_role: target_role || "", + jd_text: jd_text || "", + github_token: "", + }; + + console.log("[repo-interview-prep] Executing flow:", flow.workflowId); + let resData = await lamaticClient.executeFlow(flow.workflowId, inputs); + console.log("[repo-interview-prep] Response status:", resData?.status); + + if (resData?.status === "error") { + throw new Error(`Lamatic workflow error: ${resData?.message}`); + } + + // Handle async polling if needed + if (resData?.result?.requestId && !resData?.result?.prep_brief) { + const requestId = resData.result.requestId; + console.log("[repo-interview-prep] Polling async result:", requestId); + resData = await lamaticClient.checkStatus(requestId, 2, 120); // extended timeout to 120s for 4 sequential LLMs + if (resData?.status === "error") { + throw new Error(`Async execution failed: ${resData?.message}`); + } + } + + // Prefer result.output (wrapper case) before result (flat case) + const resObj = + resData?.result?.output || + resData?.result || + (resData as any)?.data?.output?.result; + + if (!resObj) { + throw new Error("No result found in response payload."); + } + + // Helper to unwrap if the LLM nested the response (e.g. {"architecture": { ... }}) + function unwrap(obj: any, key: string) { + if (obj && typeof obj === "object" && obj[key] && Object.keys(obj).length === 1) { + return obj[key]; + } + return obj; + } + + // Parse all 4 sections + const prep_brief = unwrap(safeParse(resObj.prep_brief, "prep_brief"), "prep_brief"); + const architecture = unwrap(safeParse(resObj.architecture, "architecture"), "architecture"); + const grill_me = unwrap(safeParse(resObj.grill_me, "grill_me"), "grill_me"); + const production = unwrap(safeParse(resObj.production, "production"), "production"); + + console.log("[repo-interview-prep] Parsed architecture shape:", JSON.stringify(architecture).substring(0, 200)); + + // Complete schema validation before returning success + const parsedData = RepoAnalysisSchema.parse({ + prep_brief, + architecture, + grill_me, + production + }); + + return { + success: true, + data: parsedData as RepoAnalysis + }; + } catch (error) { + console.error("[repo-interview-prep] Error:", error); + const message = error instanceof Error ? error.message : "Unknown error occurred"; + return { success: false, error: message }; + } +} diff --git a/kits/repo-interview-prep/apps/app/globals.css b/kits/repo-interview-prep/apps/app/globals.css new file mode 100644 index 000000000..e450e2111 --- /dev/null +++ b/kits/repo-interview-prep/apps/app/globals.css @@ -0,0 +1,174 @@ +@import "tailwindcss"; + +:root { + --bg-gradient: radial-gradient(circle at 15% 50%, rgba(20, 15, 38, 1), rgba(9, 9, 11, 1) 40%, rgba(5, 5, 5, 1) 100%); + --surface: rgba(255, 255, 255, 0.03); + --surface-hover: rgba(255, 255, 255, 0.06); + --surface-2: rgba(255, 255, 255, 0.05); + --border: rgba(255, 255, 255, 0.08); + --border-subtle: rgba(255, 255, 255, 0.04); + --text: #ffffff; + --text-muted: #a1a1aa; + --text-subtle: #71717a; + + /* Luxurious accent: Gold/Amber / Violet */ + --accent: #d8b4fe; + --accent-light: #f3e8ff; + --accent-glow: rgba(216, 180, 254, 0.15); + + --green: #4ade80; + --green-bg: rgba(74, 222, 128, 0.1); + --amber: #fbbf24; + --amber-bg: rgba(251, 191, 36, 0.1); + --blue: #60a5fa; + --blue-bg: rgba(96, 165, 250, 0.1); + --purple-bg: rgba(216, 180, 254, 0.1); + --red: #f87171; + --red-bg: rgba(239, 68, 68, 0.08); + --red-border: rgba(239, 68, 68, 0.2); + + --glass-blur: blur(20px); +} + +* { box-sizing: border-box; margin: 0; padding: 0; } + +html, body { + min-height: 100vh; + background: #050505; + background-image: var(--bg-gradient); + background-attachment: fixed; + color: var(--text); +} + +body { font-family: var(--font-geist-sans), system-ui, sans-serif; } + +/* Scrollbar */ +::-webkit-scrollbar { width: 6px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 3px; } +::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.2); } + +/* Animations */ +@keyframes fadeIn { from { opacity: 0; transform: translateY(12px); filter: blur(4px); } to { opacity: 1; transform: translateY(0); filter: blur(0); } } +@keyframes spin { to { transform: rotate(360deg); } } +@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.6; } } +@keyframes gradient-shift { + 0% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } + 100% { background-position: 0% 50%; } +} + +.fade-in { animation: fadeIn 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards; } +.spin { animation: spin 1s linear infinite; } +.pulse { animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; } + +.gradient-text { + background: linear-gradient(135deg, #e879f9, #c084fc, #818cf8); + background-size: 200% 200%; + animation: gradient-shift 4s ease infinite; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.glow-border { + background: var(--surface); + border: 1px solid var(--border); + box-shadow: inset 0 1px 1px rgba(255,255,255,0.05), 0 4px 20px rgba(0,0,0,0.2); + backdrop-filter: var(--glass-blur); + -webkit-backdrop-filter: var(--glass-blur); + transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1); +} +.glow-border:focus-within { + border-color: rgba(216, 180, 254, 0.4); + box-shadow: inset 0 1px 1px rgba(255,255,255,0.05), 0 0 0 1px rgba(216, 180, 254, 0.4), 0 0 20px var(--accent-glow); + background: rgba(255, 255, 255, 0.05); +} + +.card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 16px; + backdrop-filter: var(--glass-blur); + -webkit-backdrop-filter: var(--glass-blur); + box-shadow: inset 0 1px 1px rgba(255,255,255,0.04), 0 8px 32px rgba(0,0,0,0.2); + transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.3s cubic-bezier(0.16, 1, 0.3, 1), background 0.3s ease; +} + +.card:hover { + background: var(--surface-hover); + box-shadow: inset 0 1px 1px rgba(255,255,255,0.06), 0 12px 40px rgba(0,0,0,0.3); +} + +.btn-primary { + background: linear-gradient(135deg, rgba(255,255,255,0.1), rgba(255,255,255,0.03)); + color: #fff; + border: 1px solid rgba(255,255,255,0.15); + border-radius: 12px; + padding: 12px 28px; + font-size: 14px; + font-weight: 600; + letter-spacing: 0.3px; + cursor: pointer; + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + box-shadow: inset 0 1px 1px rgba(255,255,255,0.2), 0 4px 15px rgba(0,0,0,0.2); + transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1); + display: flex; + align-items: center; + gap: 8px; + position: relative; + overflow: hidden; +} +.btn-primary::before { + content: ''; + position: absolute; + top: 0; left: -100%; width: 50%; height: 100%; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent); + transform: skewX(-20deg); + transition: all 0.5s ease; +} +.btn-primary:hover:not(:disabled) { + border-color: rgba(216, 180, 254, 0.5); + box-shadow: inset 0 1px 1px rgba(255,255,255,0.3), 0 0 20px var(--accent-glow); + transform: translateY(-2px); +} +.btn-primary:hover:not(:disabled)::before { + left: 150%; +} +.btn-primary:active:not(:disabled) { transform: scale(0.97) translateY(0); } +.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; } + +.badge { + display: inline-flex; + align-items: center; + padding: 4px 12px; + border-radius: 20px; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.5px; + text-transform: uppercase; + backdrop-filter: blur(8px); + box-shadow: inset 0 1px 1px rgba(255,255,255,0.1); +} +.badge-junior { background: var(--green-bg); color: var(--green); border: 1px solid rgba(74,222,128,0.2); } +.badge-mid { background: var(--blue-bg); color: var(--blue); border: 1px solid rgba(96,165,250,0.2); } +.badge-senior { background: var(--purple-bg); color: #d8b4fe; border: 1px solid rgba(216,180,254,0.3); } +.badge-surface { background: var(--surface-2); color: var(--text); border: 1px solid var(--border); } +.badge-moderate { background: var(--blue-bg); color: var(--blue); border: 1px solid rgba(96,165,250,0.2); } +.badge-deep { background: var(--purple-bg); color: #d8b4fe; border: 1px solid rgba(216,180,254,0.3); } + +nav button { + position: relative; + overflow: hidden; +} +nav button::after { + content: ''; + position: absolute; + bottom: 0; left: 0; width: 0%; height: 100%; + background: var(--purple-bg); + transition: width 0.3s cubic-bezier(0.16, 1, 0.3, 1); + z-index: -1; + border-radius: inherit; +} +nav button:hover::after { width: 100%; } diff --git a/kits/repo-interview-prep/apps/app/layout.tsx b/kits/repo-interview-prep/apps/app/layout.tsx new file mode 100644 index 000000000..6b7100619 --- /dev/null +++ b/kits/repo-interview-prep/apps/app/layout.tsx @@ -0,0 +1,24 @@ +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import "./globals.css"; + +const geist = Geist({ subsets: ["latin"], variable: "--font-geist-sans" }); +const geistMono = Geist_Mono({ subsets: ["latin"], variable: "--font-geist-mono" }); + +export const metadata: Metadata = { + title: "Repo Interview Prep — Lamatic AgentKit", + description: + "Turn any GitHub repository into a complete, code-specific interview preparation brief. Get a 2-minute pitch, 15 tailored questions with answers, concepts to review, and red flags — all grounded in your actual code.", +}; + +export default function RootLayout({ + children, +}: Readonly<{ children: React.ReactNode }>) { + return ( + + + {children} + + + ); +} diff --git a/kits/repo-interview-prep/apps/app/page.tsx b/kits/repo-interview-prep/apps/app/page.tsx new file mode 100644 index 000000000..2edb44eea --- /dev/null +++ b/kits/repo-interview-prep/apps/app/page.tsx @@ -0,0 +1,520 @@ +"use client"; + +import { useState } from "react"; +import { generatePrepBrief } from "@/actions/orchestrate"; +import type { PrepBrief, RepoAnalysis } from "@/lib/types"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { Search, Code2, ChevronRight, ChevronDown } from "lucide-react"; + +const InputSchema = z.object({ + repoUrl: z.string().min(1, "Repository URL is required"), + role: z.string().optional(), + jd: z.string().optional() +}); +type InputForm = z.infer; + +type Step = "input" | "loading" | "results"; + +const LOAD_STEPS = [ + "Parsing repository URL...", + "Scraping GitHub page...", + "Analyzing tech stack...", + "Drafting architecture diagrams...", + "Generating aggressive interview questions...", + "Checking production readiness...", + "Building your final prep brief...", +]; + +const complexityClass: Record = { + junior: "badge-junior", + mid: "badge-mid", + senior: "badge-senior", +}; + +const depthClass: Record = { + surface: "badge-surface", + moderate: "badge-moderate", + deep: "badge-deep", +}; + +export default function Page() { + const [step, setStep] = useState("input"); + const [showJd, setShowJd] = useState(false); + const [loadStep, setLoadStep] = useState(0); + const [analysis, setAnalysis] = useState(null); + const [error, setError] = useState(null); + const [activeSection, setActiveSection] = useState("summary"); + const [expandedQ, setExpandedQ] = useState(null); + const [copied, setCopied] = useState(false); + + const { register, handleSubmit, formState: { errors }, watch } = useForm({ + resolver: zodResolver(InputSchema), + defaultValues: { repoUrl: "", role: "", jd: "" } + }); + + const repoUrlVal = watch("repoUrl"); + const repoName = repoUrlVal ? repoUrlVal.replace("https://github.com/", "").replace(/\/$/, "") : ""; + + const brief = analysis?.prep_brief; + + async function onSubmit(data: InputForm) { + // Normalize: accept both "owner/repo" and full "https://github.com/owner/repo" + const raw = data.repoUrl.trim(); + const normalizedUrl = raw.startsWith("https://github.com/") + ? raw + : `https://github.com/${raw.replace(/^\//, "")}`; + if (!normalizedUrl) return; + setError(null); + setStep("loading"); + setLoadStep(0); + + // Fake progress animation + const interval = setInterval(() => { + setLoadStep((p) => (p < LOAD_STEPS.length - 1 ? p + 1 : p)); + }, 4500); // Slower because 4 sequential LLMs take ~30-40s + + const result = await generatePrepBrief(normalizedUrl, data.role || "", data.jd || ""); + clearInterval(interval); + + if (result.success && result.data) { + setAnalysis(result.data); + setStep("results"); + setActiveSection("summary"); + } else { + setError(result.error ?? "Unknown error"); + setStep("input"); + } + } + + function copyPitch() { + if (brief?.pitch) { + navigator.clipboard.writeText(brief.pitch); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } + } + + + const NAV = [ + { id: "summary", label: "Overview" }, + { id: "architecture", label: "Architecture" }, + { id: "grill", label: `Grill Me (${analysis?.grill_me?.questions?.length ?? 0})` }, + { id: "questions", label: `Q&A (${brief?.follow_up_questions?.length ?? 0})` }, + { id: "production", label: "Prod Readiness" }, + ]; + + // ─── INPUT ───────────────────────────────────────────────────── + if (step === "input") { + return ( +
+
+ {/* Brand */} +
+
+ + RepoPrep +
+

+ Turn your projects into
interview gold +

+

+ Paste a GitHub repo URL. Get a complete, code-specific prep brief in seconds. +

+
+ + {/* Form */} +
+
+ {/* URL input */} +
+ +
+ github.com/ + +
+ {errors.repoUrl &&

{errors.repoUrl.message}

} +
+ + {/* Target role */} +
+ +
+ +
+
+ + {/* JD toggle */} + + + {showJd && ( +
+ +
+