diff --git a/.env.example b/.env.example index f35a2f7..613d642 100644 --- a/.env.example +++ b/.env.example @@ -11,13 +11,16 @@ GEMINI_API_KEY=your-gemini-api-key-here # GitHub Personal Access Token (for higher rate limits) GITHUB_TOKEN=your-github-token-here -# ===== ENVIRONMENT & SERVER ===== - -# Environment selection: development, staging, production, testing +# Environment & Server ENVIRONMENT=development - -# Server Configuration -HOST=127.0.0.1 +FLASK_HOST=127.0.0.1 +FLASK_PORT=5001 +PORT=5001 + +# Database (MongoDB) +# Default for local Docker: mongodb://localhost:27017/mployable +MONGODB_URI=mongodb://localhost:27017/mployable +MONGODB_DB_NAME=mployable PORT=5000 DEBUG=false WORKERS=1 diff --git a/README.md b/README.md new file mode 100644 index 0000000..6ae2972 --- /dev/null +++ b/README.md @@ -0,0 +1,100 @@ +# 🚀 Mployable - AI-Powered Resume Builder + +**Mployable** is a sophisticated full-stack platform designed to automate the creation of hyper-tailored resumes. By leveraging LLM-based analysis and semantic matching, Mployable bridges the gap between your technical profile (GitHub/LinkedIn) and specific job requirements. + +## ✨ Latest Features + +- **Modern Full-Stack Architecture**: React 19.2 frontend with Framer Motion animations and a robust Flask backend. +- **Semantic Tech-Stack Matching**: Intelligent selection of GitHub repositories that best align with job descriptions using skill scoring. +- **Hybrid Profile Sourcing**: + - **GitHub**: Automatic repository scraping and language analysis. + - **LinkedIn**: Structured data import via LinkedIn Data Export ZIP. +- **MongoDB Persistence**: Full database integration for user profiles, resume versioning, job application tracking, and intelligence caching. +- **Gemini AI Integration**: Advanced job description analysis to identify required skills and experience levels. +- **Professional LaTeX Generation**: Dynamic compilation of resume content into high-quality, ATS-friendly PDFs. + +## 🛠️ Technology Stack + +### Frontend +- **React 19.2**: Utilizing modern hooks and patterns. +- **Vite**: Ultra-fast build tool and development server. +- **Tailwind CSS**: Utility-first CSS framework for clean, responsive UI. +- **Framer Motion**: Smooth, high-performance web animations. +- **Lucide React**: Clean and consistent iconography. + +### Backend +- **Python / Flask**: Scalable RESTful API service. +- **MongoDB**: Document-oriented database for flexible data storage. +- **Google Gemini API**: state-of-the-art LLM for intelligent text analysis. +- **PyGithub & BeautifulSoup**: Specialized scraping tools for profile data. +- **LaTeX (pdflatex)**: Industry-standard typesetting for professional PDFs. + +### DevOps +- **Docker & Docker Compose**: Containerized environment for consistent deployment. +- **Nginx**: High-performance web server and reverse proxy. + +## 📋 Prerequisites + +- **Python 3.10+** +- **Node.js 18+** & npm +- **MongoDB**: Local instance or MongoDB Atlas URI. +- **LaTeX Distribution**: TeX Live (Linux/macOS) or MiKTeX (Windows). +- **Google Gemini API Key**: [Get one here](https://aistudio.google.com/app/apikey). + +## 🚀 Getting Started + +### 1. Clone & Setup Environments + +```bash +git clone https://github.com/HrishikeshUchake/Mployable.git +cd Mployable +cp .env.example .env +``` + +### 2. Backend Installation + +```bash +# It is recommended to use a virtual environment +python -m venv venv +source venv/bin/activate # venv\Scripts\activate on Windows +pip install -r requirements.txt +``` + +### 3. Frontend Installation + +```bash +cd frontend +npm install +cd .. +``` + +### 4. Running the Application + +**Option A: Manual Start** + +```bash +# Terminal 1: Backend (Port 5001) +python app.py + +# Terminal 2: Frontend (Port 5173) +cd frontend +npm run dev +``` + +**Option B: Docker (Recommended)** + +```bash +docker-compose up --build +``` + +## 📖 How it Works + +1. **Profile Connection**: Enter your GitHub username. Mployable fetches your repos, languages, and contributions. +2. **Contextual Input**: Provide your LinkedIn Data Export for professional experience. +3. **Job Analysis**: Paste a job description. The AI extracts key requirements, tech stack, and experience levels. +4. **Smart Selection**: Mployable's semantic matcher identifies which of your GitHub projects most closely match the job's tech stack. +5. **PDF Generation**: A customized LaTeX template is filled with the matched data and compiled into a downloadable PDF. + +--- + +Built with ❤️ for developers who want to stand out. diff --git a/analyzer/interview_generator.py b/analyzer/interview_generator.py new file mode 100644 index 0000000..76197b3 --- /dev/null +++ b/analyzer/interview_generator.py @@ -0,0 +1,154 @@ +""" +Interview Preparation Generator using Google Gemini + +Uses Google Gemini AI to analyze job requirements and generate +tailored interview preparation tips and sample questions. +""" + +import google.generativeai as genai +import os +import json +from typing import Dict, List +from config.config import get_config + + +class InterviewGenerator: + def __init__(self): + """Initialize interview generator with Gemini API""" + config = get_config() + api_key = os.getenv("GEMINI_API_KEY") + if not api_key: + print("Warning: GEMINI_API_KEY not found in environment variables") + self.model = None + else: + genai.configure(api_key=api_key) + try: + self.model = genai.GenerativeModel(config.GEMINI_MODEL) + except Exception: + self.model = genai.GenerativeModel("gemini-pro") + + def generate(self, job_requirements: Dict) -> Dict: + """ + Generate interview tips and sample questions based on job requirements + + Args: + job_requirements: Dictionary containing analyzed job requirements + + Returns: + Dictionary containing structured interview prep content + """ + if not self.model: + print("InterviewGenerator: No model initialized, using basic prep") + return self._generate_basic_prep(job_requirements) + + try: + # Prepare prompt for Gemini + prompt = self._create_prompt(job_requirements) + print(f"InterviewGenerator: Generating content with Gemini...") + + # Generate content using Gemini + response = self.model.generate_content(prompt) + + if not response or not response.text: + print("InterviewGenerator: Empty response from Gemini") + return self._generate_basic_prep(job_requirements) + + # Parse the response + prep_content = self._parse_gemini_response(response.text) + print("InterviewGenerator: Successfully generated interview prep") + + return prep_content + + except Exception as e: + print(f"Error generating interview prep with Gemini: {e}") + return self._generate_basic_prep(job_requirements) + + def _create_prompt(self, job_requirements: Dict) -> str: + """Create prompt for Gemini API""" + skills = job_requirements.get("skills", {}) + experience = job_requirements.get("experience", "Not specified") + education = job_requirements.get("education", "Not specified") + sections = job_requirements.get("sections", {}) + + # Flatten skills for prompt + all_skills = [] + for category, skill_list in skills.items(): + all_skills.extend(skill_list) + + skills_text = ", ".join(all_skills) if all_skills else "General technical skills" + + prompt = f""" + Act as an expert technical recruiter and interview coach. + Based on the following job requirements, generate a comprehensive interview preparation guide. + + JOB REQUIREMENTS: + - Required Skills: {skills_text} + - Experience Level: {experience} + - Education: {education} + + ADDITIONAL JOB DETAILS: + {json.dumps(sections, indent=2)} + + TASKS: + 1. Provide 5-7 customized Interview Preparation Tips specific to this role and these technologies. + 2. Generate 5 Technical Questions based on the required skills. + 3. Generate 3 Behavioral Questions tailored to the responsibilities of this role. + 4. Generate 2 "Situational" or "Scenario-based" Questions. + 5. Provide a brief "Winning Strategy" for this specific role. + + FORMAT: + Return ONLY a JSON object with this exact structure: + {{ + "tips": ["tip 1", "tip 2", ...], + "technical_questions": [ + {{"question": "...", "context": "Focus on..."}} + ], + "behavioral_questions": [ + {{"question": "...", "context": "Focus on..."}} + ], + "situational_questions": [ + {{"question": "...", "context": "Focus on..."}} + ], + "winning_strategy": "..." + }} + """ + return prompt + + def _parse_gemini_response(self, response_text: str) -> Dict: + """Parse Gemini JSON response""" + try: + # Find the JSON part in the response + json_start = response_text.find('{') + json_end = response_text.rfind('}') + 1 + if json_start >= 0 and json_end > 0: + json_str = response_text[json_start:json_end] + return json.loads(json_str) + else: + raise ValueError("No JSON found in response") + except Exception as e: + print(f"Error parsing Gemini response: {e}") + return self._generate_basic_prep({}) + + def _generate_basic_prep(self, job_requirements: Dict) -> Dict: + """Basic fallback interview prep without AI""" + return { + "tips": [ + "Research the company's recent projects and values.", + "Review the core technologies mentioned in the job description.", + "Prepare examples using the STAR method (Situation, Task, Action, Result).", + "Practice explaining your technical decisions and trade-offs.", + "Have 2-3 thoughtful questions ready for the interviewer." + ], + "technical_questions": [ + {"question": "Can you walk us through a challenging technical problem you solved recently?", "context": "Focus on your problem-solving process and final outcome."}, + {"question": "What is your experience with the core tech stack mentioned in this role?", "context": "Be specific about libraries and frameworks you've used."} + ], + "behavioral_questions": [ + {"question": "Tell me about a time you had a conflict with a teammate. How did you handle it?", "context": "Focus on collaboration and professionalism."}, + {"question": "Where do you see your technical skills evolving in the next 2 years?", "context": "Show growth mindset and alignment with company goals."} + ], + "situational_questions": [ + {"question": "If you are given a task with a tight deadline and unclear requirements, what would you do?", "context": "Focus on communication and prioritization."} + ], + "winning_strategy": "Be authentic, demonstrate how your unique skills solve the company's specific problems, and show enthusiasm for the role." + } diff --git a/app.py b/app.py index 0646c65..df5eb26 100644 --- a/app.py +++ b/app.py @@ -16,6 +16,7 @@ from scrapers.github_scraper import GitHubScraper from scrapers.linkedin_scraper import LinkedInScraper from analyzer.job_analyzer import JobAnalyzer +from analyzer.interview_generator import InterviewGenerator from generator.resume_generator import ResumeGenerator from generator.latex_compiler import LaTeXCompiler @@ -29,6 +30,7 @@ github_scraper = GitHubScraper() linkedin_scraper = LinkedInScraper() job_analyzer = JobAnalyzer() +interview_generator = InterviewGenerator() resume_generator = ResumeGenerator() latex_compiler = LaTeXCompiler() @@ -251,6 +253,7 @@ def index(): @app.route("/api/generate-resume", methods=["POST"]) +@app.route("/api/v1/generate-resume", methods=["POST"]) def generate_resume(): """ Main endpoint to generate a tailored resume @@ -337,6 +340,31 @@ def generate_resume(): return jsonify({"error": str(e)}), 500 +@app.route("/api/interview-prep", methods=["POST"]) +@app.route("/api/v1/interview-prep", methods=["POST"]) +def interview_prep(): + """ + Endpoint to generate interview preparation tips and questions + """ + try: + data = request.get_json(silent=True) or {} + job_description = data.get("job_description", "").strip() + + if not job_description: + return jsonify({"error": "Job description is required"}), 400 + + # Analyze job description + job_requirements = job_analyzer.analyze(job_description) + + # Generate interview prep + prep_data = interview_generator.generate(job_requirements) + + return jsonify(prep_data) + + except Exception as e: + return jsonify({"error": str(e)}), 500 + + @app.route("/health") def health(): """Health check endpoint""" diff --git a/app_production.py b/app_production.py index ccd8e4c..cdc5602 100644 --- a/app_production.py +++ b/app_production.py @@ -43,6 +43,7 @@ from scrapers.github_scraper import GitHubScraper from scrapers.linkedin_scraper import LinkedInScraper from analyzer.job_analyzer import JobAnalyzer +from analyzer.interview_generator import InterviewGenerator from generator.resume_generator import ResumeGenerator from generator.latex_compiler import LaTeXCompiler @@ -74,6 +75,7 @@ github_scraper = GitHubScraper() linkedin_scraper = LinkedInScraper() job_analyzer = JobAnalyzer() + interview_generator = InterviewGenerator() resume_generator = ResumeGenerator() latex_compiler = LaTeXCompiler() logger.info("All components initialized successfully") @@ -552,6 +554,46 @@ def generate_resume(): ) +@app.route("/api/v1/interview-prep", methods=["POST"]) +def interview_prep(): + """ + Endpoint to generate interview preparation tips and questions + """ + try: + logger.info(f"[{request.request_id}] Interview prep request") + data = request.json + if not data: + raise ValidationError("Request body is required") + + job_description = data.get("job_description", "").strip() + + if not job_description: + raise ValidationError("Job description is required") + + # Analyze job description + job_requirements = job_analyzer.analyze(job_description) + + # Generate interview prep + prep_data = interview_generator.generate(job_requirements) + + return jsonify(prep_data) + + except ValidationError as e: + return jsonify({"error": "VALIDATION_ERROR", "message": str(e)}), 400 + except Exception as e: + logger.error(f"Interview prep generation failed: {e}", exc_info=True) + return ( + jsonify( + { + "error": "INTERNAL_ERROR", + "message": "An unexpected error occurred during interview prep generation", + "status": 500, + } + ), + 500, + ) + + @app.route("/health", methods=["GET"]) def health(): """Health check endpoint""" diff --git a/config/config.py b/config/config.py index b89fc04..0094973 100644 --- a/config/config.py +++ b/config/config.py @@ -47,7 +47,7 @@ class Config: SESSION_COOKIE_SAMESITE = "Lax" # CORS - CORS_ORIGINS = ["http://localhost:3000", "http://localhost:5000"] + CORS_ORIGINS = ["http://localhost:3000", "http://localhost:5000", "http://localhost:5173"] # API Rate Limiting RATE_LIMIT_ENABLED = True diff --git a/docs/PROJECT_INDEX.md b/docs/PROJECT_INDEX.md index 14ef9cf..c43f910 100644 --- a/docs/PROJECT_INDEX.md +++ b/docs/PROJECT_INDEX.md @@ -33,6 +33,13 @@ ``` Mployable/ │ +├── 📂 frontend/ # Modern React Frontend +│ ├── src/ # React application source +│ ├── public/ # Static assets +│ ├── package.json # Node.js dependencies +│ ├── vite.config.ts # Vite configuration +│ └── tsconfig.json # TypeScript configuration +│ ├── 📂 config/ # Configuration & Error Handling │ ├── __init__.py │ ├── config.py # Environment-specific configs @@ -78,17 +85,14 @@ Mployable/ ├── 📂 temp/ # Temporary Files (runtime) ├── 📂 uploads/ # User Uploads (runtime) │ -├── 🐍 Python Files -│ ├── app.py # Original demo Flask app -│ ├── app_production.py # Production Flask app -│ ├── demo.py # CLI demonstration -│ ├── test_app.py # Original tests -│ └── requirements.txt # Python dependencies -│ -├── 🐳 Docker Files -│ ├── Dockerfile # Multi-stage build -│ ├── docker-compose.yml # Services orchestration -│ └── nginx.conf # Reverse proxy config +├── � README.md # Primary project entry point (root) +├── 📄 app.py # Original demo Flask app +├── 📄 app_production.py # Production-ready Flask app +├── 📄 requirements.txt # Python dependencies +├── 📄 pytest.ini # PyTest configuration +├── 📄 docker-compose.yml # Services orchestration +├── 📄 Dockerfile # Container image definition +└── 📄 nginx.conf # Reverse proxy configuration │ ├── 📚 Documentation Files │ ├── README.md # Project overview diff --git a/docs/README.md b/docs/README.md index 2f80d41..6635577 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,27 +1,32 @@ # 🚀 Mployable - AI-Powered Resume Builder -**Mployable** is an intelligent resume builder designed for hackathons and job applications. It scans your GitHub and LinkedIn profiles, analyzes job descriptions, and generates tailored resumes as professional PDFs using LaTeX. +**Mployable** is a sophisticated full-stack platform designed to automate the creation of hyper-tailored resumes. By leveraging LLM-based analysis and semantic matching, Mployable bridges the gap between your technical profile (GitHub/LinkedIn) and specific job requirements. ## ✨ Features -- **Profile Scraping**: Automatically extracts data from GitHub and LinkedIn profiles -- **Smart Analysis**: Uses Google Gemini AI to analyze job descriptions and match requirements -- **Tailored Resumes**: Generates customized resumes highlighting your most relevant experience -- **Professional PDFs**: Compiles beautiful LaTeX-formatted resumes -- **Web Interface**: Simple, user-friendly web interface -- **Fast & Efficient**: Get your tailored resume in seconds +- **Modern Full-Stack Architecture**: React 19.2 frontend with Framer Motion animations and a robust Flask backend. +- **Semantic Tech-Stack Matching**: Intelligent selection of GitHub repositories that best align with job descriptions using skill scoring. +- **Hybrid Profile Sourcing**: + - **GitHub**: Automatic repository scraping and language analysis. + - **LinkedIn**: Structured data import via LinkedIn Data Export ZIP. +- **MongoDB Persistence**: Full database integration for user profiles, resume versioning, job application tracking, and intelligence caching. +- **Gemini AI Integration**: Advanced job description analysis to identify required skills and experience levels. +- **Professional LaTeX Generation**: Dynamic compilation of resume content into high-quality, ATS-friendly PDFs. ## 🛠️ Technology Stack -- **Backend**: Python, Flask +- **Frontend**: React 19.2, TypeScript, Tailwind CSS, Framer Motion +- **Backend**: Python, Flask (running on port 5001) +- **Database**: MongoDB - **AI**: Google Gemini API -- **Profile Scraping**: PyGithub, BeautifulSoup - **PDF Generation**: LaTeX (pdflatex) -- **Frontend**: HTML, CSS, JavaScript +- **Deployment**: Docker, Docker Compose, Nginx ## 📋 Prerequisites -- Python 3.8 or higher +- Python 3.10 or higher +- Node.js 18 or higher +- MongoDB instance (local or Atlas) - LaTeX distribution (TeX Live, MiKTeX, or MacTeX) - Google Gemini API key - (Optional) GitHub Personal Access Token for higher API rate limits @@ -37,27 +42,17 @@ cd Mployable ### 2. Install Dependencies +**Backend:** ```bash pip install -r requirements.txt ``` -### 3. Install LaTeX (if not already installed) - -**Ubuntu/Debian:** -```bash -sudo apt-get update -sudo apt-get install texlive-latex-base texlive-latex-extra -``` - -**macOS:** +**Frontend:** ```bash -brew install --cask mactex +cd frontend && npm install && cd .. ``` -**Windows:** -Download and install [MiKTeX](https://miktex.org/download) - -### 4. Set Up Environment Variables +### 3. Set Up Environment Variables Create a `.env` file in the root directory: @@ -69,9 +64,26 @@ Edit `.env` and add your API keys: ``` GEMINI_API_KEY=your_gemini_api_key_here +MONGODB_URI=your_mongodb_uri_here GITHUB_TOKEN=your_github_token_here # Optional ``` +### 4. Run the Application + +**Manual Start:** +```bash +# Terminal 1 +python app.py + +# Terminal 2 +cd frontend && npm run dev +``` + +**Using Docker:** +```bash +docker-compose up --build +``` + **Get a Gemini API Key:** 1. Go to [Google AI Studio](https://makersuite.google.com/app/apikey) 2. Sign in with your Google account diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 877b7ae..83b4aeb 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,5 @@ -import { useState, useActionState, useTransition } from 'react' -import { Rocket, Github, Linkedin, Briefcase, Download, Loader2, AlertCircle, CheckCircle2, Info } from 'lucide-react' +import { useState, useActionState, useTransition, useCallback, useRef, useEffect } from 'react' +import { Rocket, Github, Linkedin, Briefcase, Download, Loader2, AlertCircle, CheckCircle2, Info, Lightbulb, MessageSquare, Brain, Trophy, ChevronRight, Copy, Check, ChevronDown, ChevronUp } from 'lucide-react' import { motion, AnimatePresence } from 'framer-motion' import axios from 'axios' @@ -10,6 +10,13 @@ interface ActionState { type: 'idle' | 'loading' | 'success' | 'error' message: string downloadUrl?: string + interviewPrep?: { + tips: string[]; + technical_questions: { question: string; context: string }[]; + behavioral_questions: { question: string; context: string }[]; + situational_questions: { question: string; context: string }[]; + winning_strategy: string; + } } async function generateResumeAction(prevState: ActionState, formData: FormData): Promise { @@ -26,18 +33,20 @@ async function generateResumeAction(prevState: ActionState, formData: FormData): } try { - // Try v1 first, fallback to root API + // Stage 1: Generate Resume (Main task) let response; try { response = await axios.post(`${API_BASE_URL}/api/v1/generate-resume`, formData, { responseType: 'blob', headers: { 'Content-Type': 'multipart/form-data' }, + timeout: 60000 // 60s timeout }) } catch (e: any) { if (e.response?.status === 404) { response = await axios.post(`${API_BASE_URL}/api/generate-resume`, formData, { responseType: 'blob', headers: { 'Content-Type': 'multipart/form-data' }, + timeout: 60000 }) } else { throw e; @@ -54,11 +63,41 @@ async function generateResumeAction(prevState: ActionState, formData: FormData): document.body.appendChild(link) link.click() link.parentNode?.removeChild(link) + + // Stage 2: Fetch Interview Prep (Sequential to avoid race conditions/timeouts) + let interviewPrep = null; + try { + console.log('Fetching interview prep...'); + let prepRes; + try { + prepRes = await axios.post(`${API_BASE_URL}/api/v1/interview-prep`, + { job_description: jobDescription }, + { timeout: 35000 } + ); + } catch (e: any) { + console.warn('V1 interview prep failed, trying fallback...', e.message); + // Fallback if 404 OR if it's a network/CORS error (which might happen if preflight fails on V1) + if (!e.response || e.response.status === 404) { + prepRes = await axios.post(`${API_BASE_URL}/api/interview-prep`, + { job_description: jobDescription }, + { timeout: 35000 } + ); + } else { + throw e; + } + } + interviewPrep = prepRes.data; + console.log('Interview prep loaded successfully'); + } catch (err: any) { + console.error('Interview prep fetch failed:', err.message || err); + // We don't throw here so the user still sees the success for the resume + } return { type: 'success', - message: 'Resume generated successfully! Your download should start automatically.', - downloadUrl: url + message: 'Resume generated! Your download should start automatically.', + downloadUrl: url, + interviewPrep } } catch (error: any) { console.error('Error generating resume:', error) @@ -84,14 +123,93 @@ async function generateResumeAction(prevState: ActionState, formData: FormData): } } +function QuestionCard({ question, context, index }: { question: string; context: string; index: number }) { + const [isOpen, setIsOpen] = useState(false) + + return ( + setIsOpen(!isOpen)} + > +
+
+ + {index + 1} + +

+ {question} +

+
+
+ {isOpen ? ( + + ) : ( + + )} +
+
+ + + {isOpen && ( + +
+
+
+ +
+
+

Coach Context

+

+ {context} +

+
+
+
+
+ )} +
+
+ ) +} + function App() { const [state, formAction, isPending] = useActionState(generateResumeAction, { type: 'idle', message: '', }) - // Local state for file name display + // Local state for UI feedback const [fileName, setFileName] = useState('') + + // Dynamic loading message + const [loadingStep, setLoadingStep] = useState(0) + + useEffect(() => { + if (isPending) { + const interval = setInterval(() => { + setLoadingStep(s => (s + 1) % 4) + }, 3000) + return () => clearInterval(interval) + } else { + setLoadingStep(0) + } + }, [isPending]) + + const loadingMessages = [ + "Analyzing your technical DNA...", + "Matching professional path to job requirements...", + "Scanning project impact and metrics...", + "Preparing your personalized interview coach..." + ] return (
@@ -220,6 +338,26 @@ function App() {

{state.message}

+ + {state.type === 'success' && ( +
+ {state.interviewPrep ? ( + + ) : ( + + +

Interview Prep Unavailable

+

+ We couldn't generate your preparation plan this time, but your tailored resume is ready! +

+
+ )} +
+ )} )} @@ -303,9 +441,17 @@ function App() {

Forging Success

-

- Analyzing your technical DNA and professional path to match the job requirements perfectly... -

+ + + {loadingMessages[loadingStep]} + +
@@ -320,6 +466,181 @@ function App() { ) } +function InterviewPrepPlan({ data }: { data: NonNullable }) { + const [copied, setCopied] = useState(false) + + // Defensive values + const tips = Array.isArray(data?.tips) ? data.tips : [] + const techQs = Array.isArray(data?.technical_questions) ? data.technical_questions : [] + const behavioralQs = Array.isArray(data?.behavioral_questions) ? data.behavioral_questions : [] + const situationalQs = Array.isArray(data?.situational_questions) ? data.situational_questions : [] + const winningStrategy = data?.winning_strategy || 'Be prepared and stay confident.' + + const copyToClipboard = () => { + try { + const text = ` +INTERVIEW PREP PLAN +------------------- +WINNING STRATEGY: +${winningStrategy} + +PREP TIPS: +${tips.map(t => `- ${t}`).join('\n')} + +TECHNICAL QUESTIONS: +${techQs.map((q, i) => `${i+1}. ${q?.question}\n Context: ${q?.context}`).join('\n\n')} + +BEHAVIORAL QUESTIONS: +${behavioralQs.map((q, i) => `${i+1}. ${q?.question}\n Context: ${q?.context}`).join('\n\n')} +`.trim() + + navigator.clipboard.writeText(text) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } catch (err) { + console.error('Failed to copy text:', err) + } + } + + return ( + +
+
+
+ +

Interview Game Plan

+
+ +
+ +
+ {/* Winning Strategy */} +
+
+

+ Winning Strategy +

+

+ "{winningStrategy}" +

+
+ +
+ {/* Tips Section */} +
+

+ Actionable Tips +

+
+ {tips.length > 0 ? tips.map((tip, i) => ( + +
+

{tip}

+ + )) :

No specific tips generated.

} +
+
+ + {/* Quick Stats or Info */} +
+
+ +
+

Preparation Scope

+
+
+ Technical Depth + High +
+
+ Behavioral Focus + Strategic +
+
+ Complexity + Level 4 +
+
+
+
+ + {/* Questions Sections */} +
+ {techQs.length > 0 && ( + } + questions={techQs} + /> + )} + + {behavioralQs.length > 0 && ( + } + questions={behavioralQs} + /> + )} + + {situationalQs.length > 0 && ( + } + questions={situationalQs} + /> + )} +
+
+
+ + ) +} + +function PrepSection({ title, subtitle, icon, questions }: { title: string; subtitle: string; icon: React.ReactNode; questions: { question: string; context: string }[] }) { + return ( +
+
+
+ {icon} +
+
+

{title}

+

{subtitle}

+
+
+
+ {questions.map((q, i) => ( + + ))} +
+
+ ) +} + function LoadingProgress({ label, progress }: { label: string; progress: number }) { return (