# Project export: ElevatR

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: CruzHacks 2025
- Tagline: Smart Skills. Smarter Careers.
- Devpost: https://devpost.com/software/elevatr
- GitHub: https://github.com/osheengupta/CruzHacks-ElevatR
- Video: https://www.youtube.com/embed/r8vr1rf-6KI?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Result: winner ([Sponsor - Vectara])
- Team: 1 GitHub contributor(s) — Osheen Gupta (9 commits)

## Devpost submission (written by the team)

No Devpost description available.

## README (from the GitHub repository)

# ElevatR - AI-Powered Career Development Platform

A Chrome extension that helps students track job skills from descriptions and get personalized project recommendations to enhance their resumes.

## Features

- **Job Description Scraping**: Automatically extracts information from job postings
- **Skill Tracking**: Identifies and categorizes technical and soft skills from job descriptions
- **Resume Analysis**: Compares your resume against job requirements to identify skill gaps
- **Project Recommendations**: Suggests personalized projects to build missing skills
- **Skills Dashboard**: Visualizes skill frequency across saved jobs

## Architecture

This project consists of two main components:

1. **Chrome Extension**: Frontend interface for saving job descriptions and viewing analysis
2. **CrewAI Backend**: Multi-agent system that processes job descriptions, analyzes resumes, and recommends projects

### AI Agents

The backend uses CrewAI to implement three specialized AI agents:

- **Job Description Analyzer**: Extracts skills and requirements from job descriptions
- **Resume Analyzer**: Identifies skills in resumes and compares to job requirements
- **Project Recommender**: Suggests personalized projects based on skill gaps

## Setup

### Chrome Extension

1. Clone this repository
2. Open Chrome and navigate to `chrome://extensions/`
3. Enable "Developer mode" (toggle in the top-right)
4. Click "Load unpacked" and select the repository folder
5. The extension should appear in your toolbar

### Backend

1. Navigate to the backend directory:
```bash
cd backend
```

2. Install the required Python packages:
```bash
pip install -r requirements.txt
```

3. Set up your environment variables:
   - Copy the `.env.example` file to `.env`
   - Add your Gemini API key to the `.env` file

4. Run the API server:
```bash
python run_api.py
```

The API will be available at `http://localhost:8000`.

## Usage

1. Browse job listings on sites like LinkedIn, Indeed, or Glassdoor
2. Click the ElevatR extension icon and select "Save Job Description"
3. The extension will extract skills and save the job information
4. Click "View Dashboard" to see your saved jobs and skill analysis
5. Upload your resume to identify skill gaps
6. Get personalized project recommendations based on your skill gaps

## Technologies Used

- **Frontend**: HTML, CSS, JavaScript, Chart.js
- **Backend**: Python, FastAPI, CrewAI, LangChain
- **AI**: 
  - Google Gemini API (primary for chat and resume analysis)
  - Vectara (for interview question generation and document search)
- **Storage**: Chrome Extension Storage API

## Project Structure

- `/images` - Extension icons
- `/backend` - CrewAI backend service
  - `/agents` - AI agent definitions
  - `/api` - FastAPI server
- Root files - Chrome extension files (manifest.json, popup.html, etc.)

## CruzHacks Submission

This project was created for CruzHacks, focusing on helping students improve their job prospects by identifying and addressing skill gaps through targeted project work.

## License

MIT


## Detected evidence (automated analysis)

Indexed codebase: 32 recognized source files, 442 KB.
- CrewAI (technology) — detected in the code
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- LangChain (technology) — detected in the code
- OpenAI (technology) — detected in the code
- Python (language) — detected in the code
- Google Gemini (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (40 of 40)

```
.gitignore
backend/agents/agents.py
backend/agents/crew.py
backend/agents/tasks.py
backend/api/main.py
backend/README.md
backend/requirements.txt
backend/run_api.py
backend/utils/__init__.py
backend/utils/vectara_utils.py
backend/utils/vectara_utils.py.new
background.js
button-test.html
content.js
dashboard-fixed.js
dashboard.css
dashboard.html
dashboard.js
dashboard.js.backup
dashboard.js.original
debug-dashboard.html
emergency-fix.js
fix-buttons.js
fixed-dashboard.js
fixed-recommendations.js
index-fixed.html
interview-prep.css
interview-prep.js
manifest.json
popup.css
popup.html
popup.js
README.md
speech-utils.js
test-dashboard.html
test-resume-request.json
test-resume.html
test-resume.txt
test-upload.html
utils.js
```

### Dependencies

- backend/requirements.txt: crewai@>=0.28.0, fastapi@>=0.104.1, langchain@>=0.0.335, openai@>=1.3.0, pydantic@>=2.4.2, python-dotenv@>=1.0.0, uvicorn@>=0.24.0

### Recent commits (newest first)

- Rename application from JobSkillTracker to ElevatR
- Update README: Rename to ElevatR and remove OpenAI mentions
- Update README to include Gemini API and Vectara in technologies
- Implement Gemini API for resume analysis and chat functionality
- Fix dashboard issues and improve job display functionality
- Merge pull request #1 from osheengupta/merged-features
- Merge resume-analysis-features with firecrawl-integration
- Add resume analysis and AI learning assistant features
- Integrate Firecrawl for improved job description extraction with fallback mechanism
- Initial commit: JobSkillTracker Chrome extension with CrewAI backend

## Key source files (fetched from GitHub, selected and truncated for size)

### backend/requirements.txt

```
crewai>=0.28.0
langchain>=0.0.335
openai>=1.3.0
python-dotenv>=1.0.0
fastapi>=0.104.1
uvicorn>=0.24.0
pydantic>=2.4.2

```

### popup.html

```html
<!DOCTYPE html>
<html>
<head>
  <title>ElevatR</title>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="popup.css">
</head>
<body>
  <div class="container">
    <h1>ElevatR</h1>
    
    <div id="initial-view">
      <p>Found a job posting? Save it to track required skills!</p>
      <button id="saveJobBtn" class="primary-btn">Save Job Description</button>
      <button id="viewDashboardBtn" class="secondary-btn">View Dashboard</button>
    </div>
    
    <div id="saving-view" class="hidden">
      <div class="loader"></div>
      <p>Analyzing job description...</p>
    </div>
    
    <div id="success-view" class="hidden">
      <div class="success-icon">✓</div>
      <h2>Job Saved!</h2>
      <div id="job-preview">
        <h3 id="job-title">Job Title</h3>
        <p id="company-name">Company Name</p>
        <div class="skills-preview">
          <h4>Top Skills Detected:</h4>
          <ul id="skills-list"></ul>
        </div>
      </div>
      <button id="viewAllJobsBtn" class="primary-btn">View All Saved Jobs</button>
    </div>
    
    <div id="error-view" class="hidden">
      <div class="error-icon">!</div>
      <h2>Error</h2>
      <p id="error-message">Could not process job description.</p>
      <button id="tryAgainBtn" class="primary-btn">Try Again</button>
    </div>
  </div>
  
  <script src="utils.js"></script>
  <script src="popup.js"></script>
</body>
</html>

```

### utils.js

```javascript
// Function to get default skills based on job title
function getDefaultSkillsForJobTitle(jobTitle) {
  // Convert job title to lowercase for easier matching
  const title = (jobTitle || '').toLowerCase();
  
  // Define skill sets for common job categories
  const skillSets = {
    developer: ["JavaScript", "HTML", "CSS", "Git", "Problem Solving"],
    engineer: ["Problem Solving", "Analytical Thinking", "Teamwork", "Communication"],
    software: ["Java", "Python", "SQL", "Data Structures", "Algorithms"],
    web: ["HTML", "CSS", "JavaScript", "React", "Node.js"],
    frontend: ["HTML", "CSS", "JavaScript", "React", "UI/UX"],
    backend: ["Node.js", "Python", "Java", "SQL", "API Design"],
    fullstack: ["JavaScript", "React", "Node.js", "SQL", "Git"],
    data: ["Python", "SQL", "Data Analysis", "Statistics", "Machine Learning"],
    product: ["Product Management", "User Research", "Agile", "Communication", "Problem Solving"],
    manager: ["Leadership", "Communication", "Project Management", "Strategic Thinking", "Team Building"],
    designer: ["UI/UX", "Figma", "Adobe Creative Suite", "Visual Design", "User Research"],
    marketing: ["Content Creation", "Social Media", "SEO", "Analytics", "Communication"],
    sales: ["Negotiation", "Communication", "CRM", "Relationship Building", "Problem Solving"],
    analyst: ["Data Analysis", "Excel", "SQL", "Problem Solving", "Communication"]
  };
  
  // Check which category the job title falls into
  let matchedSkills = [];
  
  // Try to find matching skills based on keywords in the job title
  for (const category in skillSets) {
    if (title.includes(category)) {
      matchedSkills = matchedSkills.concat(skillSets[category]);
    }
  }
  
  // Remove duplicates
  matchedSkills = [...new Set(matchedSkills)];
  
  // If no specific skills matched, return a generic set of skills
  if (matchedSkills.length === 0) {
    return ["Communication", "Problem Solving", "Teamwork", "Adaptability", "Time Management"];
  }
  
  return matchedSkills;
}

```

### popup.css

```css
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}

body {
  width: 350px;
  min-height: 300px;
  background-color: #f9f9f9;
}

.container {
  padding: 16px;
}

h1 {
  color: #2c3e50;
  font-size: 20px;
  margin-bottom: 16px;
  text-align: center;
}

h2 {
  color: #2c3e50;
  font-size: 18px;
  margin-bottom: 12px;
  text-align: center;
}

h3 {
  color: #2c3e50;
  font-size: 16px;
  margin-bottom: 4px;
}

h4 {
  color: #2c3e50;
  font-size: 14px;
  margin-bottom: 8px;
}

p {
  color: #7f8c8d;
  font-size: 14px;
  line-height: 1.4;
  margin-bottom: 16px;
}

.primary-btn, .secondary-btn {
  display: block;
  width: 100%;
  padding: 10px;
  border: none;
  border-radius: 4px;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  margin-bottom: 10px;
  transition: background-color 0.2s;
}

.primary-btn {
  background-color: #3498db;
  color: white;
}

.primary-btn:hover {
  background-color: #2980b9;
}

.secondary-btn {
  background-color: #ecf0f1;
  color: #2c3e50;
}

.secondary-btn:hover {
  background-color: #bdc3c7;
}

.hidden {
  display: none;
}

.loader {
  border: 4px solid #f3f3f3;
  border-top: 4px solid #3498db;
  border-radius: 50%;
  width: 40px;
  height: 40px;
  animation: spin 2s linear infinite;
  margin: 20px auto;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

.success-icon, .error-icon {
  font-size: 48px;
  text-align: center;
  margin: 10px 0;
}

.success-icon {
  color: #2ecc71;
}

.error-icon {
  color: #e74c3c;
}

#job-preview {
  background-color: white;
  border-radius: 4px;
  padding: 12px;
  margin: 16px 0;
  box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}

#company-name {
  color: #7f8c8d;
  font-size: 14px;
  margin-bottom: 8px;
}

.skills-preview {
  margin-top: 8px;
}

#skills-list {
  list-style-type: none;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

#skills-list li {
  background-color: #e1f0fa;
  color: #3498db;
  padding: 4px 8px;
  border-radius: 12px;
  font-size: 12px;
}

```

### test-upload.html

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Test File Upload</title>
  <style>
    body {
      font-family: Arial, sans-serif;
      max-width: 800px;
      margin: 0 auto;
      padding: 20px;
    }
    button {
      padding: 10px 15px;
      background-color: #4CAF50;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      margin-top: 20px;
    }
    input[type="file"] {
      display: none;
    }
  </style>
</head>
<body>
  <h1>Test File Upload</h1>
  
  <button id="uploadBtn">Upload File</button>
  <input type="file" id="fileInput" accept=".pdf,.doc,.docx,.txt">
  
  <div id="result" style="margin-top: 20px;"></div>
  
  <script>
    document.addEventListener('DOMContentLoaded', function() {
      const uploadBtn = document.getElementById('uploadBtn');
      const fileInput = document.getElementById('fileInput');
      const result = document.getElementById('result');
      
      console.log('Upload button exists:', !!uploadBtn);
      console.log('File input exists:', !!fileInput);
      
      uploadBtn.addEventListener('click', function() {
        console.log('Upload button clicked');
        fileInput.click();
      });
      
      fileInput.addEventListener('change', function(event) {
        console.log('File input change event triggered');
        const file = event.target.files[0];
        if (!file) {
          console.log('No file selected');
          result.textContent = 'No file selected';
          return;
        }
        
        console.log('File selected:', file.name, file.type);
        result.textContent = `File selected: ${file.name} (${file.type})`;
        
        // Read file content
        const reader = new FileReader();
        reader.onload = function(e) {
          console.log('File read successfully, length:', e.target.result.length);
          const preview = e.target.result.substring(0, 100) + '...';
          const preElement = document.createElement('pre');
          preElement.textContent = `Preview: ${preview}`;
          result.appendChild(preElement);
        };
        reader.readAsText(file);
      });
    });
  </script>
</body>
</html>

```

### interview-prep.css

```css
/* Interview Preparation Modal styles */
.interview-prep-modal {
  max-width: 800px !important;
  width: 90% !important;
  height: 80vh;
  margin: 5% auto;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.interview-setup-section,
.interview-session-section,
.interview-summary-section {
  padding: 16px;
  overflow-y: auto;
  height: 100%;
}

.form-group {
  margin-bottom: 20px;
}

.form-group label {
  display: block;
  margin-bottom: 8px;
  font-weight: 500;
}

#jobDescriptionInput {
  width: 100%;
  height: 120px;
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
  resize: vertical;
}

.difficulty-options {
  display: flex;
  gap: 10px;
}

.difficulty-btn {
  background-color: #f5f5f5;
  border: 1px solid #ddd;
  border-radius: 4px;
  padding: 8px 16px;
  cursor: pointer;
  transition: all 0.2s;
}

.difficulty-btn.active {
  background-color: #2c3e50;
  color: white;
}

#interviewFocus {
  width: 100%;
  padding: 8px;
  border: 1px solid #ddd;
  border-radius: 4px;
}

.interview-conversation {
  height: calc(100% - 140px);
  overflow-y: auto;
  border: 1px solid #eee;
  border-radius: 8px;
  padding: 16px;
  margin-bottom: 16px;
  background-color: #f9f9f9;
}

.interview-messages {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.interview-message {
  padding: 12px;
  border-radius: 8px;
  max-width: 80%;
}

.interviewer-message {
  background-color: #e1f5fe;
  align-self: flex-start;
}

.user-message {
  background-color: #e8f5e9;
  align-self: flex-end;
}

.interview-controls {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.mic-btn {
  background-color: #f44336;
  color: white;
  border: none;
  border-radius: 50px;
  padding: 12px 20px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  margin: 0 auto;
  transition: all 0.2s;
}

.mic-btn:hover {
  background-color: #d32f2f;
}

.mic-btn.listening {
  background-color: #4CAF50;
  animation: pulse 1.5s infinite;
}

@keyframes pulse {
  0% {
    box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.7);
  }
  70% {
    box-shadow: 0 0 0 10px rgba(76, 175, 80, 0);
  }
  100% {
    box-shadow: 0 0 0 0 rgba(76, 175, 80, 0);
  }
}

.speech-status {
  text-align: center;
  font-style: italic;
  color: #666;
  margin: 8px 0;
}

#manualAnswerInput {
  width: 100%;
  height: 80px;
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
  resize: vertical;
}

.interview-feedback {
  padding: 16px;
  background-color: #f5f5f5;
  border-radius: 8px;
  margin-bottom: 16px;
}

.interview-strengths,
.interview-weaknesses {
  margin-bottom: 20px;
}

#strengthsList,
#weaknessesList {
  padding-left: 24px;
}

#strengthsList li,
#weaknessesList li {
  margin-bottom: 8px;
}

.close-interview-modal {
  float: right;
  font-size: 24px;
  font-weight: bold;
  cursor: pointer;
}

.close-interview-modal:hover {
  color: #555;
}

.hidden {
  display: none !important;
}

```

### test-dashboard.html

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Test Dashboard</title>
    <style>
        body { font-family: Arial, sans-serif; padding: 20px; }
        .result { margin: 10px 0; padding: 10px; border: 1px solid #ddd; }
        .success { color: green; }
        .error { color: red; }
    </style>
</head>
<body>
    <h1>Dashboard Test</h1>
    <div id="results"></div>

    <script>
        const resultsDiv = document.getElementById('results');
        const API_BASE_URL = "http://localhost:8001";

        function addResult(message, isSuccess = true) {
            const div = document.createElement('div');
            div.className = `result ${isSuccess ? 'success' : 'error'}`;
            div.textContent = message;
            resultsDiv.appendChild(div);
        }

        // Test API connection
        fetch(`${API_BASE_URL}/openapi.json`)
            .then(response => {
                if (response.ok) {
                    addResult("✅ API connection successful");
                } else {
                    addResult(`❌ API connection failed: ${response.status}`, false);
                }
            })
            .catch(error => {
                addResult(`❌ API connection error: ${error.message}`, false);
            });

        // Test chat endpoint
        fetch(`${API_BASE_URL}/chat`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                message: "Hello",
                chat_history: [],
                mode: "general"
            })
        })
        .then(response => {
            if (response.ok) {
                addResult("✅ Chat endpoint working");
                return response.json();
            } else {
                addResult(`❌ Chat endpoint failed: ${response.status}`, false);
                throw new Error(`Status: ${response.status}`);
            }
        })
        .then(data => {
            console.log("Chat response:", data);
        })
        .catch(error => {
            addResult(`❌ Chat error: ${error.message}`, false);
        });

        // Test resume analysis endpoint
        fetch(`${API_BASE_URL}/analyze-resume`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                resume_text: "Test resume content",
                extracted_job_skills: {
                    technical_skills: ["Python", "JavaScript"],
                    soft_skills: ["Communication"]
                }
            })
        })
        .then(response => {
            if (response.ok) {
                addResult("✅ Resume analysis endpoint working");
                return response.json();
            } else {
                addResult(`❌ Resume analysis endpoint failed: ${response.status}`, false);
                throw new Error(`Status: ${response.status}`);
            }
        })
        .then(data => {
            console.log("Resume analysis response:", data);
        })
        .catch(error => {
            addResult(`❌ Resume analysis error: ${error.message}`, false);
        });
    </script>
</body>
</html>

```

### speech-utils.js

```javascript
// Speech Utilities for ElevatR
// This module provides text-to-speech and speech recognition utilities

// Text-to-Speech Configuration
const speechConfig = {
  voice: null,          // Will be set during initialization
  pitch: 1.0,           // Default pitch
  rate: 1.0,            // Default rate
  volume: 1.0,          // Default volume
  voices: [],           // Available voices
  speaking: false,      // Flag to track if currently speaking
  preferredVoice: 'en-US',  // Preferred voice language
  interviewerVoice: null,   // Voice for the interviewer
  cancelSpeech: false   // Flag to cancel ongoing speech
};

// Initialize Text-to-Speech
function initSpeech() {
  return new Promise((resolve) => {
    // Get available voices
    speechConfig.voices = window.speechSynthesis.getVoices();
    
    // If voices are already available
    if (speechConfig.voices.length > 0) {
      setupVoices();
      resolve(true);
    } else {
      // Wait for voices to be loaded
      window.speechSynthesis.onvoiceschanged = () => {
        speechConfig.voices = window.speechSynthesis.getVoices();
        setupVoices();
        resolve(true);
      };
      
      // Fallback in case voices don't load
      setTimeout(() => {
        if (speechConfig.voices.length === 0) {
          console.warn('No voices available for speech synthesis');
          resolve(false);
        }
      }, 1000);
    }
  });
}

// Set up voices for the interview
function setupVoices() {
  // Filter for English voices
  const englishVoices = speechConfig.voices.filter(voice => 
    voice.lang.includes('en-') || voice.lang.includes('en_')
  );
  
  if (englishVoices.length > 0) {
    // Try to find a professional sounding voice for the interviewer
    // Prefer voices like "Google UK English Male" or similar professional voices
    const professionalVoices = englishVoices.filter(voice => 
      voice.name.includes('Male') && 
      (voice.name.includes('UK') || voice.name.includes('US'))
    );
    
    if (professionalVoices.length > 0) {
      speechConfig.interviewerVoice = professionalVoices[0];
    } else {
      // Fallback to any English voice
      speechConfig.interviewerVoice = englishVoices[0];
    }
    
    console.log(`Selected interviewer voice: ${speechConfig.interviewerVoice.name}`);
  } else {
    console.warn('No English voices found for speech synthesis');
  }
}

// Speak text with the interviewer voice
function speakText(text, onEndCallback = null) {
  return new Promise((resolve, reject) => {
    // Reset cancel flag
    speechConfig.cancelSpeech = false;
    
    // Check if speech synthesis is available
    if (!window.speechSynthesis) {
      console.error('Speech synthesis not supported in this browser');
      reject('Speech synthesis not supported');
      return;
    }
    
    // Cancel any ongoing speech
    window.speechSynthesis.cancel();
    
    // Create utterance
    const utterance = new SpeechSynthesisUtterance(text);
    
    // Set voice and properties
    if (speechConfig.interviewerVoice) {
      utterance.voice = speechConfig.interviewerVoice;
    }
    
    utterance.pitch = speechConfig.pitch;
    utterance.rate = speechConfig.rate;
    utterance.volume = speechConfig.volume;
    
    // Set event handlers
    utterance.onstart = () => {
      speechConfig.speaking = true;
      console.log('Started speaking');
    };
    
    utterance.onend = () => {
      speechConfig.speaking = false;
      console.log('Finished speaking');
      if (onEndCallback && typeof onEndCallback === 'function') {
        onEndCallback();
      }
      resolve();
    };
    
    utterance.onerror = (event) => {
      speechConfig.speaking = false;
      console.error('Speech synthesis error:', event);
      reject(event);
    };
    
    // Start speaking
    window.speechSynthesis.speak(utterance);
  });
}

// Stop speaking
function stopSpeaking() {
  speechConfig.cancelSpeech = true;
  window.speechSynthesis.cancel();
  speechConfig.speaking = false;
}

// Check if speech synthesis is supported
function isSpeechSynthesisSupported() {
  return 'speechSynthesis' in window;
}

// Check if speech recognition is supported
function isSpeechRecognitionSupported() {
  return ('webkitSpeechRecognition' in window) || ('SpeechRecognition' in window);
}

// Export functions
window.speechUtils = {
  initSpeech,
  speakText,
  stopSpeaking,
  isSpeechSynthesisSupported,
  isSpeechRecognitionSupported,
  speechConfig
};

```

### button-test.html

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Button Test</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
        }
        .button {
            padding: 10px 15px;
            background-color: #4285f4;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            margin: 5px;
        }
        .button:hover {
            background-color: #3367d6;
        }
        .message {
            margin: 20px 0;
            padding: 10px;
            border-radius: 4px;
            background-color: #f1f1f1;
        }
        #output {
            margin-top: 20px;
            padding: 10px;
            border: 1px solid #ddd;
            min-height: 100px;
        }
        .active {
            background-color: #34a853;
        }
        input {
            padding: 8px;
            width: 80%;
            margin-right: 10px;
        }
    </style>
</head>
<body>
    <h1>Button Test</h1>
    <p>This page tests basic button functionality to isolate any issues.</p>
    
    <div>
        <h2>Chat Mode</h2>
        <button id="generalBtn" class="button active">General Chat</button>
        <button id="paperBtn" class="button">Paper Summary</button>
        <button id="roadmapBtn" class="button">Skill Roadmap</button>
    </div>
    
    <div>
        <h2>Message Input</h2>
        <input type="text" id="messageInput" placeholder="Type a message...">
        <button id="sendBtn" class="button">Send</button>
    </div>
    
    <div id="output">
        <p>Button clicks will show here...</p>
    </div>
    
    <script>
        document.addEventListener('DOMContentLoaded', function() {
            // Get DOM elements
            const generalBtn = document.getElementById('generalBtn');
            const paperBtn = document.getElementById('paperBtn');
            const roadmapBtn = document.getElementById('roadmapBtn');
            const messageInput = document.getElementById('messageInput');
            const sendBtn = document.getElementById('sendBtn');
            const output = document.getElementById('output');
            
            // Log elements to check if they exist
            console.log('Elements:', {
                generalBtn, paperBtn, roadmapBtn, messageInput, sendBtn, output
            });
            
            // Function to log messages to the output div and console
            function logMessage(message) {
                const p = document.createElement('p');
                p.textContent = message;
                output.appendChild(p);
                console.log(message);
            }
            
            // Function to set active button
            function setActiveButton(activeBtn) {
                // Remove active class from all buttons
                generalBtn.classList.remove('active');
                paperBtn.classList.remove('active');
                roadmapBtn.classList.remove('active');
                
                // Add active class to clicked button
                activeBtn.classList.add('active');
            }
            
            // Button click event handlers
            generalBtn.addEventListener('click', function() {
                logMessage('General Chat button clicked');
                setActiveButton(generalBtn);
            });
            
            paperBtn.addEventListener('click', function() {
                logMessage('Paper Summary button clicked');
                setActiveButton(paperBtn);
            });
            
            roadmapBtn.addEventListener('click', function() {
                logMessage('Skill Roadmap button clicked');
                setActiveButton(roadmapBtn);
            });
            
            // Send button event handler
            sendBtn.addEventListener('click', function() {
                const message = messageInput.value.trim();
                if (message) {
                    logMessage('Message sent: ' + message);
                    messageInput.value = '';
                } else {
                    logMessage('Please enter a message');
                }
            });
            
            // Enter key event handler
            messageInput.addEventListener('keypress', function(e) {
                if (e.key === 'Enter') {
                    const message = messageInput.value.trim();
                    if (message) {
                        logMessage('Message sent (Enter key): ' + message);
                        messageInput.value = '';
                    } else {
                        logMessage('Please enter a message');
                    }
                }
            });
            
            // Log initial state
            logMessage('Test page loaded successfully');
        });
    </script>
</body>
</html>

```

### content.js

```javascript
// Listen for messages from the popup
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
  if (request.action === "extractJobDescription") {
    try {
      // Extract job data from the current page
      const jobData = extractJobData();
      sendResponse({jobData: jobData});
    } catch (error) {
      sendResponse({error: error.message});
    }
  }
  return true; // Required for async response
});

// Function to extract job data from the page
function extractJobData() {
  // Get the current URL
  const url = window.location.href;
  
  // Get the page title (often contains job title)
  const pageTitle = document.title;
  
  // Try to extract job title more specifically
  let jobTitle = pageTitle;
  
  // Try to extract company name
  let companyName = "";
  
  // Extract the job description directly as a fallback for Firecrawl
  let description = "";
  
  // Site-specific selectors for popular job sites
  if (url.includes("linkedin.com")) {
    // LinkedIn selectors
    const titleElement = document.querySelector(".job-details-jobs-unified-top-card__job-title");
    const companyElement = document.querySelector(".job-details-jobs-unified-top-card__company-name");
    const descriptionElement = document.querySelector(".jobs-description-content__text");
    
    if (titleElement) jobTitle = titleElement.textContent.trim();
    if (companyElement) companyName = companyElement.textContent.trim();
    if (descriptionElement) description = descriptionElement.textContent.trim();
  } 
  else if (url.includes("indeed.com")) {
    // Indeed selectors
    const titleElement = document.querySelector(".jobsearch-JobInfoHeader-title");
    const companyElement = document.querySelector("[data-company-name]");
    const descriptionElement = document.querySelector("#jobDescriptionText");
    
    if (titleElement) jobTitle = titleElement.textContent.trim();
    if (companyElement) companyName = companyElement.textContent.trim();
    if (descriptionElement) description = descriptionElement.textContent.trim();
  }
  else if (url.includes("glassdoor.com")) {
    // Glassdoor selectors
    const titleElement = document.querySelector(".job-title");
    const companyElement = document.querySelector(".employer-name");
    const descriptionElement = document.querySelector(".jobDescriptionContent");
    
    if (titleElement) jobTitle = titleElement.textContent.trim();
    if (companyElement) companyName = companyElement.textContent.trim();
    if (descriptionElement) description = descriptionElement.textContent.trim();
  }
  else if (url.includes("google.com") && url.includes("careers")) {
    // Google Careers selectors
    const titleElement = document.querySelector("h1, h2");
    companyName = "Google"; // Hardcoded since it's Google Careers
    
    // For Google Careers, try to get the main content
    const descriptionElement = document.querySelector("main") || 
                              document.querySelector(".content-wrapper") || 
                              document.querySelector(".article-content");
    
    if (titleElement) jobTitle = titleElement.textContent.trim();
    if (descriptionElement) description = descriptionElement.innerText;
    
    // Extract skills from required skills section if available
    const skillsElements = document.querySelectorAll(".skills-list li, .requirements-list li");
    if (skillsElements.length > 0) {
      description += "\n\nRequired Skills:\n";
      skillsElements.forEach(skill => {
        description += "- " + skill.textContent.trim() + "\n";
      });
    }
  }
  else if (url.includes("deloitte.com")) {
    // Deloitte selectors
    const titleElement = document.querySelector("h1") || document.querySelector("h2");
    companyName = "Deloitte"; // Hardcoded since it's the Deloitte website
    
    // For Deloitte, try to get the main content
    const mainContent = document.querySelector("main") || 
                        document.querySelector(".content-wrapper") || 
                        document.querySelector(".article-content");
    
    if (titleElement) jobTitle = titleElement.textContent.trim();
    if (mainContent) description = mainContent.innerText;
    
    // If we still don't have a description, get all paragraph content
    if (!description) {
      const paragraphs = Array.from(document.querySelectorAll("p"));
      description = paragraphs.map(p => p.innerText).join("\n\n");
    }
  }
  
  // If no specific description container found, get the main content
  if (!description) {
    // Try common content containers
    const descriptionElement = document.querySelector(".job-description") || 
                              document.querySelector("#job-description") ||
                              document.querySelector("[data-automation='jobDescriptionText']") ||
                              document.querySelector(".description__text") ||
                              document.querySelector("main") || 
                              document.querySelector("article") || 
                              document.querySelector(".content");
    
    if (descriptionElement) {
      description = descriptionElement.innerText;
    } else {
      // Last resort: get a portion of the body text
      const bodyText = document.body.innerText;
      // Take the first 5000 characters as a reasonable description size
      description = bodyText.substring(0, 5000);
    }
  }
  
  // Make sure we have at least some description
  if (!description || description.trim() === "") {
    // Absolute last resort: get visible text from the page
    description = getVisibleText(document.body);
  }
  
  // Return the extracted data
  return {
    url: url,
    title: jobTitle,
    company: companyName,
    description: description,
    useFirecrawl: true  // Still try Firecrawl first, but we have a fallback now
  };
}

// Helper function to get visible text from an element
function getVisibleText(element) {
  let text = "";
  
  // 
[truncated — 462 more characters]
```

[18 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]