AgentOp
Agent Template

Resume Optimizer

The Resume Optimizer is a fully browser-based AI agent that rewrites your resume to beat Applicant Tracking Systems (ATS) and land more interviews. Upload your existing resume as a PDF, DOCX, or plain text file — the agent extracts the text using pypdf and python-docx running via Pyodide (Python compiled to WebAssembly) — then paste the job description you are targeting. The agent analyzes both documents locally and delivers four results simultaneously. First, an ATS compatibility score (0–100) with a breakdown across keyword coverage, formatting compliance, action verb usage, and achievement quantification. Second, a keyword analysis panel showing every required skill and term from the job description as either matched (present in your resume) or missing (absent but needed). Third, a set of issues and recommendations ranked as critical, warning, or informational — covering specific bullet points to rewrite, missing sections, and suggested power verbs for your industry and role level. Fourth, a full optimized resume with rewritten bullet points, added keywords, and ATS-safe formatting applied. The three-panel interface lets you view the original, the optimized version, and a direct side-by-side comparison. When you are satisfied, export the optimized resume as a PDF (via jsPDF), DOCX, or plain text with one click. The template supports seven target industries — Technology, Finance, Healthcare, Marketing, Engineering, Consulting, and Other — and four role levels: Entry, Mid, Senior, and Executive. Because the entire pipeline — PDF text extraction, keyword matching, ATS scoring, and LLM rewriting — runs inside the browser, no resume data is ever uploaded to an external server. Compatible with GPT-4o, Claude 3.5 Sonnet, or a fully local WebLLM model for complete offline privacy.

resume career ats job-search optimization cv-optimizer ats-score keyword-matching job-application pdf-resume resume-rewriter browser-ai pyodide cover-letter interview-prep
ozzo Feb 28, 2026 1 use

Preview Mode

This is a preview with sample data. The template uses placeholders like which will be replaced with actual agent data.

About This Template

Resume Optimizer is a browser-executable AI agent template built on AgentOp. It runs entirely in the browser using Python (via Pyodide) and can be deployed without a server — just download the generated HTML file and open it locally or host it anywhere.

Topics resume career ats job-search optimization cv-optimizer ats-score keyword-matching job-application pdf-resume resume-rewriter browser-ai pyodide cover-letter interview-prep
Template Preview

Template Metadata

Slug
resume-optimizer
Created By
ozzo
Created
Feb 28, 2026
Usage Count
1

Tags

resume career ats job-search optimization cv-optimizer ats-score keyword-matching job-application pdf-resume resume-rewriter browser-ai pyodide cover-letter interview-prep

Code Statistics

HTML Lines
278
CSS Lines
714
JS Lines
997
Python Lines
259

Source Code

<!DOCTYPE html>
  <html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <title>{{ agent_name }} - Resume Optimizer</title>

    <style>
      {{ css_code }}
    </style>

    <!-- Conditional Script Imports -->
    {% if needs_pyodide %}
    <script src="https://cdn.jsdelivr.net/pyodide/v{{ pyodide_version }}/full/pyodide.js"></script>
    {% endif %}

    <!-- jsPDF for PDF export -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>

    <script>
      const PROVIDER = "{{ embedded_provider }}";
      const API_KEY = "{{ embedded_api_key }}";
      const AGENT_CONFIG = {{ default_config|safe }};
      const NEEDS_PYODIDE = {{ needs_pyodide|lower }};
      const PYODIDE_VERSION = "{{ pyodide_version }}";
    </script>
  </head>
  <body>
  <div class="resume-optimizer-dashboard">
    <!-- Left Panel: Input & Settings -->
    <div class="left-panel">
      <div class="panel-header">
        <div class="icon-badge">📄</div>
        <div>
          <h2>Resume Optimizer</h2>
          <p class="tagline">Transform your resume into an ATS-winning document</p>
        </div>
      </div>

      <!-- Resume Upload Section -->
      <div class="section">
        <h3>📄 Upload Your Resume</h3>
        <div class="upload-area" id="resume-upload-area">
          <div class="upload-content">
            <div class="upload-icon">📤</div>
            <p class="upload-text">Drag & drop your resume<br>or click to browse</p>
            <p class="upload-hint">Supported: PDF, DOCX, TXT • Max 10 MB</p>
          </div>
          <input type="file" id="resume-file-input" accept=".pdf,.docx,.txt" hidden>
        </div>

        <button class="secondary-btn" id="paste-resume-btn" onclick="toggleResumeTextarea()">
          📋 Or Paste Resume Text
        </button>

        <textarea id="resume-text-input" 
                  class="text-input hidden" 
                  placeholder="Paste your resume text here..."
                  rows="6"></textarea>
      </div>

      <!-- Job Description Section -->
      <div class="section">
        <h3>🎯 Target Job Description</h3>
        <textarea id="job-description-input" 
                  class="text-input" 
                  placeholder="Paste the job posting you're applying for..."
                  rows="8"></textarea>
        <div class="char-count" id="jd-char-count">0 / 5000 characters</div>

        <div class="button-group">
          <button class="secondary-btn" onclick="loadSampleJob()">📂 Load Sample</button>
        </div>
      </div>

      <!-- Settings Section -->
      <div class="section">
        <h3>⚙️ Optimization Settings</h3>

        <label class="form-label">Target Industry</label>
        <select id="industry-select" class="select-input">
          <option value="technology">Technology</option>
          <option value="finance">Finance</option>
          <option value="healthcare">Healthcare</option>
          <option value="marketing">Marketing & Sales</option>
          <option value="engineering">Engineering</option>
          <option value="consulting">Consulting</option>
          <option value="other">Other</option>
        </select>

        <label class="form-label">Target Role Level</label>
        <div class="radio-group">
          <label class="radio-label">
            <input type="radio" name="level" value="entry" checked>
            <span>Entry-level</span>
          </label>
          <label class="radio-label">
            <input type="radio" name="level" value="mid">
            <span>Mid-level</span>
          </label>
          <label class="radio-label">
            <input type="radio" name="level" value="senior">
            <span>Senior</span>
          </label>
          <label class="radio-label">
            <input type="radio" name="level" value="executive">
            <span>Executive</span>
          </label>
        </div>

        <label class="form-label">Optimization Options</label>
        <div class="checkbox-group">
          <label class="checkbox-label">
            <input type="checkbox" checked>
            <span>Extract keywords from job</span>
          </label>
          <label class="checkbox-label">
            <input type="checkbox" checked>
            <span>Optimize bullet points</span>
          </label>
          <label class="checkbox-label">
            <input type="checkbox" checked>
            <span>Fix ATS formatting</span>
          </label>
          <label class="checkbox-label">
            <input type="checkbox" checked>
            <span>Add missing keywords</span>
          </label>
          <label class="checkbox-label">
            <input type="checkbox" checked>
            <span>Highlight achievements</span>
          </label>
          <label class="checkbox-label">
            <input type="checkbox" checked>
            <span>Suggest action verbs</span>
          </label>
        </div>

        <button class="primary-btn" id="optimize-btn" onclick="optimizeResume()">
          <span class="btn-icon">🚀</span>
          <span>Optimize Resume</span>
        </button>
      </div>
    </div>

    <!-- Center Panel: Preview & Editor -->
    <div class="center-panel">
      <div class="panel-header">
        <h2>Resume Preview</h2>
        <div class="view-toggle">
          <button class="toggle-btn active" data-view="original" onclick="switchView('original')">Original</button>
          <button class="toggle-btn" data-view="optimized" onclick="switchView('optimized')">Optimized</button>
          <button class="toggle-btn" data-view="comparison" onclick="switchView('comparison')">Compare</button>
        </div>
      </div>

      <div class="preview-container" id="preview-container">
        <div class="welcome-state" id="welcome-state">
          <div class="welcome-icon">📄</div>
          <h3>Ready to Optimize</h3>
          <p>Upload your resume and paste a job description to get started</p>
          <div class="feature-list">
            <div class="feature-item">✨ AI-Powered Analysis</div>
            <div class="feature-item">📊 ATS Compatibility Score</div>
            <div class="feature-item">🎯 Keyword Optimization</div>
            <div class="feature-item">💡 Smart Recommendations</div>
          </div>
        </div>

        <!-- Original View -->
        <div class="resume-view hidden" id="original-view">
          <div class="resume-document">
            <div id="original-content"></div>
          </div>
        </div>

        <!-- Optimized View -->
        <div class="resume-view hidden" id="optimized-view">
          <div class="resume-document">
            <div id="optimized-content"></div>
          </div>
        </div>

        <!-- Comparison View -->
        <div class="comparison-view hidden" id="comparison-view">
          <div class="comparison-split">
            <div class="comparison-side">
              <div class="comparison-label">Original</div>
              <div class="resume-document">
                <div id="comparison-original"></div>
              </div>
            </div>
            <div class="comparison-divider"></div>
            <div class="comparison-side">
              <div class="comparison-label">Optimized</div>
              <div class="resume-document">
                <div id="comparison-optimized"></div>
              </div>
            </div>
          </div>
        </div>
      </div>

      <!-- Export Section -->
      <div class="export-section hidden" id="export-section">
        <h3>📤 Export & Download</h3>
        <div class="button-group">
          <button class="primary-btn" onclick="downloadResume('pdf')">
            📄 Download as PDF
          </button>
          <button class="primary-btn" onclick="downloadResume('docx')">
            📋 Download as DOCX
          </button>
          <button class="secondary-btn" onclick="downloadResume('txt')">
            📝 Download as TXT
          </button>
          <button class="secondary-btn" onclick="copyToClipboard()">
            📋 Copy to Clipboard
          </button>
        </div>
      </div>
    </div>

    <!-- Right Panel: Analysis -->
    <div class="right-panel">
      <!-- ATS Score -->
      <div class="analysis-card">
        <h3>🎯 ATS Compatibility Score</h3>
        <div id="ats-score-display" class="score-display">
          <div class="score-placeholder">
            <div class="placeholder-icon">📊</div>
            <p>Upload resume to see score</p>
          </div>
        </div>
      </div>

      <!-- Keyword Analysis -->
      <div class="analysis-card">
        <h3>🔑 Keyword Analysis</h3>
        <div id="keyword-analysis-display" class="keyword-display">
          <div class="score-placeholder">
            <div class="placeholder-icon">🔍</div>
            <p>Analyzing keywords...</p>
          </div>
        </div>
      </div>

      <!-- Issues & Recommendations -->
      <div class="analysis-card">
        <h3>⚠️ Issues & Recommendations</h3>
        <div id="recommendations-display" class="recommendations-display">
          <div class="score-placeholder">
            <div class="placeholder-icon">💡</div>
            <p>Recommendations will appear here</p>
          </div>
        </div>
      </div>
    </div>

    <!-- Loading Overlay -->
    <div class="loading-overlay hidden" id="loading-overlay">
      <div class="loading-spinner"></div>
      <p class="loading-text">Analyzing your resume...</p>
    </div>
  </div>

  <script>
  {{ js_code }}
  </script>

  <!-- Hidden Python code -->
  <script type="text/python" id="python-code">
  {{ python_code }}
  </script>

  </body>
  </html>
:root {
  --primary-blue: #1E40AF;
  --accent-green: #10B981;
  --bg-gray: #F3F4F6;
  --text-dark: #1F2937;
  --text-muted: #6B7280;
  --border: #E5E7EB;
  --white: #FFFFFF;
  --red: #EF4444;
  --yellow: #FBBF24;
  --success: #10B981;
  --shadow: 0 1px 3px rgba(0,0,0,0.1);
  --shadow-lg: 0 10px 25px rgba(0,0,0,0.1);
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', 'Poppins', sans-serif;
  background: linear-gradient(135deg, #EFF6FF 0%, #F9FAFB 100%);
  color: var(--text-dark);
  line-height: 1.6;
}

.resume-optimizer-dashboard {
  max-width: 1800px;
  margin: 0 auto;
  padding: 1.5rem;
  display: grid;
  grid-template-columns: 380px 1fr 380px;
  gap: 1.5rem;
  min-height: 100vh;
}

/* Panel Styles */
.left-panel, .center-panel, .right-panel {
  background: var(--white);
  border-radius: 12px;
  padding: 1.5rem;
  box-shadow: var(--shadow);
  border: 1px solid var(--border);
  height: fit-content;
}

.center-panel {
  min-height: 600px;
}

.right-panel {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

/* Panel Headers */
.panel-header {
  display: flex;
  align-items: center;
  gap: 1rem;
  margin-bottom: 1.5rem;
  padding-bottom: 1rem;
  border-bottom: 2px solid var(--border);
}

.icon-badge {
  width: 48px;
  height: 48px;
  background: linear-gradient(135deg, var(--primary-blue), #3B82F6);
  border-radius: 12px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.5rem;
}

.panel-header h2 {
  margin: 0;
  font-size: 1.25rem;
  color: var(--text-dark);
  font-weight: 700;
}

.tagline {
  margin: 0;
  font-size: 0.875rem;
  color: var(--text-muted);
}

/* Sections */
.section {
  margin-bottom: 1.5rem;
  padding-bottom: 1.5rem;
  border-bottom: 1px solid var(--border);
}

.section:last-child {
  border-bottom: none;
}

.section h3 {
  margin: 0 0 1rem 0;
  font-size: 1rem;
  font-weight: 600;
  color: var(--text-dark);
}

/* Upload Area */
.upload-area {
  border: 2px dashed var(--border);
  border-radius: 8px;
  padding: 2rem;
  text-align: center;
  cursor: pointer;
  transition: all 0.3s ease;
  margin-bottom: 1rem;
  background: var(--bg-gray);
}

.upload-area:hover, .upload-area.drag-over {
  border-color: var(--primary-blue);
  background: #EFF6FF;
}

.upload-area.has-file {
  border-color: var(--accent-green);
  background: #F0FDF4;
}

.upload-icon {
  font-size: 2.5rem;
  margin-bottom: 0.5rem;
}

.upload-text {
  margin: 0.5rem 0;
  font-weight: 500;
  color: var(--text-dark);
}

.upload-hint {
  margin: 0;
  font-size: 0.75rem;
  color: var(--text-muted);
}

/* Form Elements */
.text-input {
  width: 100%;
  padding: 0.75rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  font-family: inherit;
  font-size: 0.875rem;
  resize: vertical;
  transition: all 0.2s;
}

.text-input:focus {
  outline: none;
  border-color: var(--primary-blue);
  box-shadow: 0 0 0 3px rgba(30, 64, 175, 0.1);
}

.select-input {
  width: 100%;
  padding: 0.75rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  font-family: inherit;
  font-size: 0.875rem;
  background: var(--white);
  cursor: pointer;
  transition: all 0.2s;
  margin-bottom: 1rem;
}

.select-input:focus {
  outline: none;
  border-color: var(--primary-blue);
  box-shadow: 0 0 0 3px rgba(30, 64, 175, 0.1);
}

.form-label {
  display: block;
  font-size: 0.875rem;
  font-weight: 500;
  margin-bottom: 0.5rem;
  color: var(--text-dark);
}

.char-count {
  font-size: 0.75rem;
  color: var(--text-muted);
  text-align: right;
  margin-top: 0.25rem;
}

/* Radio & Checkbox Groups */
.radio-group, .checkbox-group {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  margin-bottom: 1rem;
}

.radio-label, .checkbox-label {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  cursor: pointer;
  font-size: 0.875rem;
}

.radio-label input, .checkbox-label input {
  cursor: pointer;
}

/* Buttons */
.primary-btn, .secondary-btn {
  padding: 0.75rem 1.25rem;
  border-radius: 8px;
  font-weight: 600;
  font-size: 0.875rem;
  cursor: pointer;
  transition: all 0.2s;
  border: none;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  width: 100%;
}

.primary-btn {
  background: var(--primary-blue);
  color: var(--white);
}

.primary-btn:hover:not(:disabled) {
  background: #1E3A8A;
  transform: translateY(-1px);
  box-shadow: var(--shadow-lg);
}

.secondary-btn {
  background: var(--bg-gray);
  color: var(--text-dark);
  border: 1px solid var(--border);
}

.secondary-btn:hover:not(:disabled) {
  background: #E5E7EB;
}

.primary-btn:disabled, .secondary-btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.btn-icon {
  font-size: 1.125rem;
}

.button-group {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
}

.button-group button {
  flex: 1;
}

/* View Toggle */
.view-toggle {
  display: flex;
  gap: 0.5rem;
  margin-left: auto;
}

.toggle-btn {
  padding: 0.5rem 1rem;
  border: 1px solid var(--border);
  background: var(--white);
  border-radius: 6px;
  font-size: 0.875rem;
  cursor: pointer;
  transition: all 0.2s;
  font-weight: 500;
}

.toggle-btn:hover {
  background: var(--bg-gray);
}

.toggle-btn.active {
  background: var(--primary-blue);
  color: var(--white);
  border-color: var(--primary-blue);
}

/* Preview Container */
.preview-container {
  min-height: 500px;
  background: var(--bg-gray);
  border-radius: 8px;
  padding: 1.5rem;
  position: relative;
}

/* Welcome State */
.welcome-state {
  text-align: center;
  padding: 3rem 1rem;
}

.welcome-icon {
  font-size: 4rem;
  margin-bottom: 1rem;
}

.welcome-state h3 {
  font-size: 1.5rem;
  margin: 0 0 0.5rem 0;
  color: var(--text-dark);
}

.welcome-state p {
  color: var(--text-muted);
  margin: 0 0 2rem 0;
}

.feature-list {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 1rem;
  max-width: 400px;
  margin: 0 auto;
}

.feature-item {
  background: var(--white);
  padding: 0.75rem;
  border-radius: 8px;
  font-size: 0.875rem;
  box-shadow: var(--shadow);
}

/* Resume Views */
.resume-view, .comparison-view {
  animation: fadeIn 0.3s ease;
}

@keyframes fadeIn {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: translateY(0); }
}

.resume-document {
  background: var(--white);
  border-radius: 8px;
  padding: 2rem;
  box-shadow: var(--shadow);
  min-height: 400px;
  font-size: 0.875rem;
  line-height: 1.6;
}

.resume-document iframe {
  width: 100%;
  height: 900px;
  border: 0;
  border-radius: 8px;
  background: var(--white);
}

.resume-pre {
  white-space: pre-wrap;
  word-break: break-word;
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  font-size: 0.9rem;
  line-height: 1.6;
  color: var(--text-dark);
}

/* Comparison View */
.comparison-split {
  display: flex;
  flex-direction: column;
  gap: 2rem;
}

.comparison-divider {
  height: 2px;
  background: linear-gradient(to right, transparent, var(--border), transparent);
  margin: 1rem 0;
  border-radius: 1px;
}

.comparison-label {
  font-weight: 700;
  margin-bottom: 1rem;
  color: var(--primary-blue);
  font-size: 1.1rem;
  padding: 0.5rem 0.75rem;
  background: linear-gradient(135deg, #EFF6FF, #F0F9FF);
  border-left: 4px solid var(--primary-blue);
  border-radius: 4px;
}

/* Export Section */
.export-section {
  margin-top: 1rem;
  padding-top: 1rem;
  border-top: 2px solid var(--border);
}

.export-section h3 {
  font-size: 1rem;
  margin: 0 0 1rem 0;
}

/* Analysis Cards */
.analysis-card {
  background: var(--white);
  border-radius: 12px;
  padding: 1.25rem;
  box-shadow: var(--shadow);
  border: 1px solid var(--border);
}

.analysis-card h3 {
  margin: 0 0 1rem 0;
  font-size: 0.95rem;
  font-weight: 600;
  color: var(--text-dark);
}

/* Score Display */
.score-display, .keyword-display, .recommendations-display {
  min-height: 150px;
}

.score-placeholder {
  text-align: center;
  padding: 2rem 1rem;
}

.placeholder-icon {
  font-size: 2.5rem;
  margin-bottom: 0.5rem;
}

.score-placeholder p {
  margin: 0;
  color: var(--text-muted);
  font-size: 0.875rem;
}

/* Score Visualization */
.score-comparison {
  display: flex;
  justify-content: space-around;
  align-items: center;
  margin: 1.5rem 0;
}

.score-item {
  text-align: center;
}

.score-label {
  font-size: 0.75rem;
  color: var(--text-muted);
  text-transform: uppercase;
  margin-bottom: 0.5rem;
}

.score-value {
  font-size: 2rem;
  font-weight: 700;
  color: var(--primary-blue);
}

.score-arrow {
  font-size: 2rem;
  color: var(--accent-green);
}

.score-breakdown {
  background: var(--bg-gray);
  border-radius: 8px;
  padding: 1rem;
  margin-top: 1rem;
}

.score-breakdown-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 0.75rem;
  font-size: 0.875rem;
}

.score-breakdown-item:last-child {
  margin-bottom: 0;
}

.score-bar {
  flex: 1;
  height: 8px;
  background: #E5E7EB;
  border-radius: 4px;
  margin: 0 0.75rem;
  overflow: hidden;
}

.score-bar-fill {
  height: 100%;
  background: var(--accent-green);
  border-radius: 4px;
  transition: width 0.5s ease;
}

.score-badge {
  padding: 0.25rem 0.5rem;
  border-radius: 4px;
  font-size: 0.75rem;
  font-weight: 600;
}

.badge-green {
  background: #D1FAE5;
  color: #065F46;
}

.badge-yellow {
  background: #FEF3C7;
  color: #92400E;
}

.badge-red {
  background: #FEE2E2;
  color: #991B1B;
}

/* Keyword Analysis */
.keyword-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 0.5rem;
  margin-bottom: 1rem;
}

.keyword-tag {
  padding: 0.5rem;
  border-radius: 6px;
  font-size: 0.75rem;
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.keyword-tag.matched {
  background: #D1FAE5;
  color: #065F46;
}

.keyword-tag.missing {
  background: #FEE2E2;
  color: #991B1B;
}

.keyword-density {
  background: var(--bg-gray);
  padding: 1rem;
  border-radius: 8px;
  margin-top: 1rem;
  font-size: 0.875rem;
}

/* Recommendations */
.recommendation-item {
  background: var(--bg-gray);
  border-left: 3px solid var(--border);
  padding: 1rem;
  border-radius: 6px;
  margin-bottom: 0.75rem;
  font-size: 0.875rem;
}

.recommendation-item:last-child {
  margin-bottom: 0;
}

.recommendation-item.critical {
  border-left-color: var(--red);
  background: #FEF2F2;
}

.recommendation-item.warning {
  border-left-color: var(--yellow);
  background: #FFFBEB;
}

.recommendation-item.info {
  border-left-color: var(--primary-blue);
  background: #EFF6FF;
}

.recommendation-header {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.5rem;
  font-weight: 600;
}

.recommendation-text {
  color: var(--text-dark);
  line-height: 1.5;
}

/* Loading Overlay */
.loading-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  z-index: 1000;
  backdrop-filter: blur(4px);
}

.loading-spinner {
  width: 50px;
  height: 50px;
  border: 4px solid rgba(255, 255, 255, 0.3);
  border-top: 4px solid var(--white);
  border-radius: 50%;
  animation: spin 1s linear infinite;
}

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

.loading-text {
  color: var(--white);
  margin-top: 1rem;
  font-size: 1rem;
  font-weight: 500;
}

/* Utility Classes */
.hidden {
  display: none !important;
}

/* Responsive Design */
@media (max-width: 1400px) {
  .resume-optimizer-dashboard {
    grid-template-columns: 350px 1fr 350px;
    gap: 1rem;
  }
}

@media (max-width: 1200px) {
  .resume-optimizer-dashboard {
    grid-template-columns: 1fr;
  }
  
  .right-panel {
    order: -1;
  }
  
  .comparison-split {
    grid-template-columns: 1fr;
  }
  
  .comparison-divider {
    height: 2px;
    width: 100%;
  }
}

@media (max-width: 768px) {
  .resume-optimizer-dashboard {
    padding: 1rem;
  }
  
  .feature-list {
    grid-template-columns: 1fr;
  }
  
  .keyword-grid {
    grid-template-columns: 1fr;
  }
  
  .button-group {
    flex-direction: column;
  }
  
  .view-toggle {
    flex-wrap: wrap;
  }
}
<script>
// ========================
// Global state
// ========================
let resumeText = '';
let jobDescription = '';
let optimizedResume = '';
let analysisData = null;

// Uploaded file state for proper PDF preview
let uploadedResumeFileType = null;
let uploadedResumePdfUrl = null;
let uploadedResumePdfDataUrl = null;

// ========================
// Text normalization helpers
// ========================
function stripControlCharsKeepNewlines(text) {
  if (!text) return '';
  return String(text).replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
}

function fixCommonSpacing(text) {
  if (!text) return '';
  let t = String(text);

  // Sentence ends: "word.word" -> "word. Word"
  t = t.replace(/([a-z])\.([A-Z])/g, '$1. $2');

  // Commas/semicolons: "word,word" -> "word, word"
  t = t.replace(/([a-zA-Z]),([a-zA-Z])/g, '$1, $2');

  // Colons: "Word:Word" -> "Word: Word"
  t = t.replace(/([a-zA-Z]):([a-zA-Z])/g, '$1: $2');

  // Dashes: "likeGDB" -> "like GDB"
  t = t.replace(/([a-zA-Z])(\d)/g, '$1 $2');

  // Normalize NBSP and collapse spaces
  t = t.replace(/\u00A0/g, ' ').replace(/ {2,}/g, ' ');

  return t;
}

function collapseSpacedLettersInLine(line) {
  if (!line) return '';
  const parts = line.split(' ').filter(Boolean);
  if (parts.length < 8) return line;

  const singleCharParts = parts.filter(p => p.length === 1);
  const ratio = singleCharParts.length / parts.length;
  if (ratio < 0.75) return line;

  let joined = '';
  let buffer = '';
  for (const p of parts) {
    if (p.length === 1) {
      buffer += p;
    } else {
      if (buffer) {
        joined += buffer + ' ';
        buffer = '';
      }
      joined += p + ' ';
    }
  }
  if (buffer) joined += buffer;
  joined = joined.trim();

  // "July2022" -> "July 2022"
  joined = joined.replace(
    /(Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|Jul(?:y)?|Aug(?:ust)?|Sep(?:tember)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)(\d{4})/gi,
    '$1 $2'
  );
  // "2020February" -> "2020 February"
  joined = joined.replace(/(\d{4})([A-Za-z]+)/g, '$1 $2');

  return joined;
}

function normalizeExtractedResumeText(rawText) {
  if (!rawText) return '';
  let text = String(rawText);

  // Strip control characters (keep newlines)
  text = stripControlCharsKeepNewlines(text);

  // Common PDF bullet chars
  text = text
    .replace(/•/g, '• ')
    .replace(/●/g, '● ')
    .replace(/\u00b7/g, '• ')
    .replace(/\u2022/g, '• ');

  // Fix some specific spacing / artifacts if needed later
  text = text.replace(/githublinkedin\.com/gi, 'github linkedin.com');

  // Collapse spaced letters line by line
  const lines = text.split('\n').map((l) => collapseSpacedLettersInLine(l));
  text = lines.join('\n');

  text = fixCommonSpacing(text);
  return text.trim();
}

function normalizeOptimizedResumeText(rawText) {
  if (!rawText) return '';
  let text = String(rawText);
  text = stripControlCharsKeepNewlines(text);
  text = fixCommonSpacing(text);
  return text.trim();
}

// ========================
// File upload handlers
// ========================
function setupFileUpload() {
  const uploadArea = document.getElementById('resume-upload-area');
  const fileInput = document.getElementById('resume-file-input');
  if (!uploadArea || !fileInput) return;

  uploadArea.addEventListener('click', () => fileInput.click());

  uploadArea.addEventListener('dragover', (e) => {
    e.preventDefault();
    uploadArea.classList.add('drag-over');
  });

  uploadArea.addEventListener('dragleave', () => {
    uploadArea.classList.remove('drag-over');
  });

  uploadArea.addEventListener('drop', (e) => {
    e.preventDefault();
    uploadArea.classList.remove('drag-over');
    const files = e.dataTransfer.files;
    if (files && files.length > 0) {
      handleFileUpload(files[0]);
    }
  });

  fileInput.addEventListener('change', (e) => {
    if (e.target.files && e.target.files.length > 0) {
      handleFileUpload(e.target.files[0]);
    }
  });
}

async function handleFileUpload(file) {
  const uploadArea = document.getElementById('resume-upload-area');
  if (!uploadArea) return;

  const maxSize = 10 * 1024 * 1024; // 10MB
  if (file.size > maxSize) {
    alert('File size exceeds 10 MB limit');
    return;
  }

  const allowedTypes = [
    'application/pdf',
    'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
    'application/msword',
    'text/plain',
  ];
  if (!allowedTypes.includes(file.type)) {
    alert('Unsupported file type. Please upload PDF, DOCX, DOC, or TXT.');
    return;
  }

  uploadArea.innerHTML = `
    <div class="upload-content">
      <div class="upload-icon">⏳</div>
      <p class="upload-text">Processing ${file.name}...</p>
      <p class="upload-hint">Please wait while we extract the text</p>
    </div>
  `;

  try {
    // Clear previous PDF preview URL
    if (uploadedResumePdfUrl) {
      try { URL.revokeObjectURL(uploadedResumePdfUrl); } catch (e) {}
      uploadedResumePdfUrl = null;
    }
    uploadedResumeFileType = file.type;
    uploadedResumePdfDataUrl = null;

    if (file.type === 'application/pdf') {
      const isFileOrigin = window.location.protocol === 'file:';
      if (isFileOrigin) {
        // Use DataURL for local file
        uploadedResumePdfUrl = null;
        uploadedResumePdfDataUrl = await new Promise((resolve, reject) => {
          const r = new FileReader();
          r.onload = () => resolve(r.result);
          r.onerror = () => reject(new Error('Failed to read PDF for preview'));
          r.readAsDataURL(file);
        });
      } else {
        uploadedResumePdfUrl = URL.createObjectURL(file);
      }
    }

    const text = await extractTextFromFile(file);
    resumeText = normalizeExtractedResumeText(text);

    uploadArea.classList.add('has-file');
    uploadArea.innerHTML = `
      <div class="upload-content">
        <div class="upload-icon">✅</div>
        <p class="upload-text">${file.name}</p>
        <p class="upload-hint">${formatFileSize(file.size)} • Click to change</p>
      </div>
    `;

    updateOriginalView(resumeText);
  } catch (error) {
    console.error('File upload error:', error);
    uploadArea.innerHTML = `
      <div class="upload-content">
        <div class="upload-icon">⚠️</div>
        <p class="upload-text">Error processing file</p>
        <p class="upload-hint">${error.message}</p>
      </div>
    `;
  }
}

async function extractTextFromFile(file) {
  if (file.type === 'text/plain') {
    return await file.text();
  }

  // DOCX/DOC via python-docx
  if (
    file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ||
    file.type === 'application/msword'
  ) {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onload = async () => {
        try {
          const arrayBuffer = reader.result;
          const bytes = new Uint8Array(arrayBuffer);

          if (!window.pyodide) {
            reject(new Error('Pyodide not initialized yet. Please wait and try again.'));
            return;
          }

          try {
            await window.pyodide.loadPackage(['micropip']);
            await window.pyodide.runPythonAsync(`
import micropip
try:
    await micropip.install('python-docx')
except Exception as e:
    print('python-docx install error or already installed:', e)
            `);
          } catch (e) {
            console.log('python-docx already installed or error:', e);
          }

          window.pyodide.globals.set('docx_bytes', bytes);
          const extractedText = await window.pyodide.runPythonAsync(`
import io
from docx import Document

docx_file = io.BytesIO(bytes(docx_bytes))
doc = Document(docx_file)
text_content = [p.text for p in doc.paragraphs if p.text.strip()]
"\\n".join(text_content)
          `);

          if (!extractedText || !String(extractedText).trim()) {
            reject(new Error('Could not extract text from document. The file may be empty or corrupted.'));
            return;
          }

          resolve(extractedText);
        } catch (err) {
          console.error('DOCX extraction error:', err);
          reject(new Error('Failed to extract document text: ' + err.message));
        }
      };
      reader.onerror = () => reject(new Error('Failed to read document for extraction'));
      reader.readAsArrayBuffer(file);
    });
  }

  // PDF via pypdf
  if (file.type === 'application/pdf') {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onload = async () => {
        try {
          const arrayBuffer = reader.result;
          const bytes = new Uint8Array(arrayBuffer);

          if (!window.pyodide) {
            reject(new Error('Pyodide not initialized yet. Please wait and try again.'));
            return;
          }

          try {
            await window.pyodide.loadPackage(['micropip']);
            await window.pyodide.runPythonAsync(`
import micropip
try:
    await micropip.install('pypdf>=4.0.0')
except Exception as e:
    print('pypdf install error or already installed:', e)
            `);
          } catch (e) {
            console.log('pypdf already installed or error:', e);
          }

          window.pyodide.globals.set('pdf_bytes', bytes);
          const extractedText = await window.pyodide.runPythonAsync(`
import io
from pypdf import PdfReader

pdf_file = io.BytesIO(bytes(pdf_bytes))
reader = PdfReader(pdf_file)
text_content = []
for page in reader.pages:
    try:
        page_text = page.extract_text(extraction_mode="layout")
    except TypeError:
        page_text = page.extract_text()
    except Exception:
        page_text = page.extract_text()
    if page_text:
        text_content.append(page_text)
"\\n".join(text_content)
          `);

          if (!extractedText || !String(extractedText).trim()) {
            reject(new Error('Could not extract text from PDF. The file may be scanned or image-based.'));
            return;
          }

          resolve(extractedText);
        } catch (err) {
          console.error('PDF extraction error:', err);
          reject(new Error('Failed to extract PDF text: ' + err.message));
        }
      };
      reader.onerror = () => reject(new Error('Failed to read PDF for extraction'));
      reader.readAsArrayBuffer(file);
    });
  }

  throw new Error('Unsupported file type');
}

// ========================
// Misc helpers
// ========================
function escapeHtml(text) {
  if (text == null) return '';
  return String(text)
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;');
}

function formatFileSize(bytes) {
  if (bytes === 0) return '0 Bytes';
  const k = 1024;
  const sizes = ['Bytes', 'KB', 'MB', 'GB'];
  const i = Math.floor(Math.log(bytes) / Math.log(k));
  return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
}

// ========================
// Text inputs & view switching
// ========================
function toggleResumeTextarea() {
  const textarea = document.getElementById('resume-text-input');
  const btn = document.getElementById('paste-resume-btn');
  if (!textarea || !btn) return;

  if (textarea.classList.contains('hidden')) {
    textarea.classList.remove('hidden');
    btn.textContent = 'Use File Upload Instead';
  } else {
    textarea.classList.add('hidden');
    btn.textContent = '📋 Or Paste Resume Text';
  }
}

function setupTextInputs() {
  const resumeTextarea = document.getElementById('resume-text-input');
  const jobDescTextarea = document.getElementById('job-description-input');
  const charCount = document.getElementById('jd-char-count');

  if (resumeTextarea) {
    resumeTextarea.addEventListener('input', (e) => {
      resumeText = e.target.value;
      updateOriginalView(resumeText);
    });
  }

  if (jobDescTextarea && charCount) {
    jobDescTextarea.addEventListener('input', (e) => {
      jobDescription = e.target.value;
      const count = jobDescription.length;
      charCount.textContent = `${count} / 5000 characters`;
      charCount.style.color = count > 5000 ? 'var(--red)' : 'var(--text-muted)';
    });
  }
}

function switchView(viewName) {
  document.querySelectorAll('.toggle-btn').forEach((btn) => {
    btn.classList.remove('active');
    if (btn.dataset.view === viewName) {
      btn.classList.add('active');
    }
  });

  const views = ['welcome-state', 'original-view', 'optimized-view', 'comparison-view'];
  views.forEach((v) => {
    const el = document.getElementById(v);
    if (!el) return;
    el.classList.add('hidden');
  });

  if (viewName === 'original') {
    document.getElementById('original-view')?.classList.remove('hidden');
  } else if (viewName === 'optimized') {
    document.getElementById('optimized-view')?.classList.remove('hidden');
  } else if (viewName === 'comparison') {
    document.getElementById('comparison-view')?.classList.remove('hidden');
  }
}

function updateOriginalView(text) {
  const welcomeState = document.getElementById('welcome-state');
  const originalView = document.getElementById('original-view');
  const originalContent = document.getElementById('original-content');
  const comparisonOriginal = document.getElementById('comparison-original');

  if (!originalContent || !comparisonOriginal) return;

  if (text && text.trim()) {
    welcomeState?.classList.add('hidden');
    originalView?.classList.remove('hidden');

    const pdfSrc = uploadedResumePdfDataUrl || uploadedResumePdfUrl;
    if (uploadedResumeFileType === 'application/pdf' && pdfSrc) {
      const pdfIframe = `<iframe src="${pdfSrc}" title="Resume PDF Preview"></iframe>`;
      originalContent.innerHTML = pdfIframe;
      comparisonOriginal.innerHTML = pdfIframe;
    } else {
      const formatted = formatResumeText(text);
      originalContent.innerHTML = formatted;
      comparisonOriginal.innerHTML = formatted;
    }

    switchView('original');
  }
}

function formatResumeText(text) {
  return `<pre class="resume-pre">${escapeHtml(text)}</pre>`;
}

// ========================
// Optimization (existing code, unchanged)
// ========================
async function optimizeResume() {
  if (!resumeText || !resumeText.trim()) {
    alert('Please upload or paste your resume first');
    return;
  }

  if (!jobDescription.trim()) {
    alert('Please paste the job description');
    return;
  }

  // For local provider, optimization is driven by the in-browser WebLLM agent.
  // The model MUST be loaded first via the model selector.
  if (!window.agentManager || !window.agentManager.isLoaded || !window.agentManager.wllama) {
    alert('Please load an AI model first (top bar) before optimizing.');
    return;
  }

  const optimizeBtn = document.getElementById('optimize-btn');
  const loadingOverlay = document.getElementById('loading-overlay');

  optimizeBtn.disabled = true;
  optimizeBtn.innerHTML = '<span class="btn-icon">⏳</span><span>Optimizing...</span>';
  loadingOverlay.classList.remove('hidden');

  try {
    // Get settings
    const industry = document.getElementById('industry-select').value;
    const level = document.querySelector('input[name="level"]:checked').value;

    // Use cleaned text for best results (PDF extraction can introduce artifacts)
    const resumeForOptimization = normalizeExtractedResumeText(resumeText);

    // Route through the unified agent pipeline so WebLLM decides tool calls.
    // We instruct the model to call optimize_resume (Python tool) and then
    // synthesize a STRICT JSON response for the UI.
    const query =
      `You are a resume optimization assistant.\n\n` +
      `You have access to Python tools. You MUST call the tool optimize_resume exactly once to compute ATS/keyword analysis and recommendations.\n` +
      `After the tool returns, you MUST respond with ONLY a valid JSON object (no markdown, no backticks, no extra text).\n\n` +
      `JSON schema required:\n` +
      `{\n` +
      `  "optimized_resume": string,\n` +
      `  "ats_score": {"before": number, "after": number, "breakdown": object},\n` +
      `  "keyword_analysis": {"matched": array, "missing": array, "total": number, "density": number},\n` +
      `  "recommendations": [{"type": "critical"|"warning"|"info", "title": string, "text": string}]\n` +
      `}\n\n` +
      `Input parameters for optimize_resume:\n` +
      `- resume_text: ${JSON.stringify(resumeForOptimization)}\n` +
      `- job_description: ${JSON.stringify(jobDescription)}\n` +
      `- industry: ${JSON.stringify(industry)}\n` +
      `- experience_level: ${JSON.stringify(level)}\n\n` +
      `Optimization requirements for optimized_resume:\n` +
      `- Keep the original structure and headings\n` +
      `- Strengthen action verbs and bullet points\n` +
      `- Incorporate missing keywords naturally (do not keyword-stuff)\n` +
      `- Keep it ATS-friendly and professional\n` +
      `- Remove duplicated lines/headings (e.g., repeated name/contact)\n` +
      `- Fix spacing/line breaks; do not leave dangling words on their own line\n` +
      `- Use consistent bullet formatting and avoid incomplete bullets\n` +
      `- Replace placeholders/typos (e.g., 'Matched') with correct, meaningful wording\n`;

    const parseMaybeJson = (text) => {
      try {
        return JSON.parse(text);
      } catch (_) {
        const start = text.indexOf('{');
        const end = text.lastIndexOf('}');
        if (start >= 0 && end > start) {
          const slice = text.slice(start, end + 1);
          return JSON.parse(slice);
        }
        throw new Error('Model did not return valid JSON');
      }
    };

    // Call the unified Python entrypoint (async) via Pyodide
    window.pyodide.globals.set('user_query', query);
    const resultText = await window.pyodide.runPythonAsync(
      `await process_user_query(user_query)`
    );

    // Parse model result (must be JSON with the schema above)
    const data = parseMaybeJson(resultText);

    if (data && typeof data.optimized_resume === 'string') {
      data.optimized_resume = normalizeOptimizedResumeText(data.optimized_resume);
    }

    optimizedResume = data.optimized_resume;
    analysisData = data;

    // Update views
    updateOptimizedView(data);
    updateAnalysisPanel(data);

    // Show export section
    document.getElementById('export-section').classList.remove('hidden');

    // Switch to comparison view
    switchView('comparison');
  } catch (error) {
    console.error('Optimization error:', error);
    alert('Error optimizing resume: ' + error.message);
  } finally {
    optimizeBtn.disabled = false;
    optimizeBtn.innerHTML = '<span class="btn-icon">🚀</span><span>Optimize Resume</span>';
    loadingOverlay.classList.add('hidden');
  }
}

// ========================
// Analysis panel updates
// ========================
function updateOptimizedView(data) {
  const optimizedContent = document.getElementById('optimized-content');
  const comparisonOptimized = document.getElementById('comparison-optimized');
  if (!optimizedContent || !comparisonOptimized || !data || !data.optimized_resume) return;

  const formatted = formatResumeText(normalizeOptimizedResumeText(data.optimized_resume));
  optimizedContent.innerHTML = formatted;
  comparisonOptimized.innerHTML = formatted;
}

function updateAnalysisPanel(data) {
  if (!data) return;
  updateATSScore(data.ats_score || {});
  updateKeywordAnalysis(data.keyword_analysis || {});
  updateRecommendations(data.recommendations || []);
}

function updateATSScore(scoreData) {
  const scoreDisplay = document.getElementById('ats-score-display');
  if (!scoreDisplay || !scoreData) return;

  const before = scoreData.before ?? 0;
  const after = scoreData.after ?? 0;
  const breakdown = scoreData.breakdown || {};

  scoreDisplay.innerHTML = `
    <div class="score-comparison">
      <div class="score-item">
        <div class="score-label">Before</div>
        <div class="score-value" style="color: var(--yellow);">${before}</div>
      </div>
      <div class="score-arrow">➡️</div>
      <div class="score-item">
        <div class="score-label">After</div>
        <div class="score-value" style="color: var(--accent-green);">${after}</div>
      </div>
    </div>
    <div class="score-breakdown">
      <h4 style="margin: 0 0 0.75rem 0; font-size: 0.875rem;">Score Breakdown</h4>
      ${Object.entries(breakdown)
        .map(([key, value]) => {
          const val = Number(value) || 0;
          const badgeClass =
            val >= 80 ? 'badge-green' : val >= 60 ? 'badge-yellow' : 'badge-red';
          return `
            <div class="score-breakdown-item">
              <span>${escapeHtml(key)}</span>
              <div class="score-bar">
                <div class="score-bar-fill" style="width: ${val}%;"></div>
              </div>
              <span class="score-badge ${badgeClass}">${val}</span>
            </div>
          `;
        })
        .join('')}
    </div>
  `;
}

function updateKeywordAnalysis(keywordData) {
  const keywordDisplay = document.getElementById('keyword-analysis-display');
  if (!keywordDisplay || !keywordData) return;

  const matched = keywordData.matched || [];
  const missing = keywordData.missing || [];
  const total = keywordData.total ?? matched.length + missing.length;
  const density = keywordData.density ?? 0;

  const densityLabel =
    density >= 2 && density <= 3 ? 'Optimal (2–3%)' : 'Needs adjustment';

  keywordDisplay.innerHTML = `
    <p style="margin: 0 0 1rem 0; font-size: 0.875rem;">
      <strong>Matched Keywords</strong>: ${matched.length}/${total}
    </p>
    <div class="keyword-grid">
      ${matched
        .map(
          (kw) =>
            `<div class="keyword-tag matched"><span>✅</span><span>${escapeHtml(
              kw
            )}</span></div>`
        )
        .join('')}
      ${missing
        .map(
          (kw) =>
            `<div class="keyword-tag missing"><span>➕</span><span>${escapeHtml(
              kw
            )}</span></div>`
        )
        .join('')}
    </div>
    <div class="keyword-density">
      <strong>Keyword Density</strong>: ${density.toFixed(2)}%
      <br />
      <span style="color: var(--text-muted); font-size: 0.875rem;">${densityLabel}</span>
    </div>
  `;
}

function updateRecommendations(recommendations) {
  const recDisplay = document.getElementById('recommendations-display');
  if (!recDisplay || !Array.isArray(recommendations)) return;

  recDisplay.innerHTML = recommendations
    .map((rec) => {
      const type = rec.type || 'info';
      const title = rec.title || '';
      const text = rec.text || '';
      return `
        <div class="recommendation-item ${type}">
          <div class="recommendation-header">
            <span>${type === 'critical' ? '❌' : type === 'warning' ? '⚠️' : '💡'}</span>
            <span>${escapeHtml(title)}</span>
          </div>
          <div class="recommendation-text">${escapeHtml(text)}</div>
        </div>
      `;
    })
    .join('');
}

// ========================
// Export & clipboard
// ========================
async function downloadResume(format) {
  if (!optimizedResume || !optimizedResume.trim()) {
    alert('Please optimize your resume first');
    return;
  }

  if (format === 'pdf') {
    await downloadAsPDF();
    return;
  }
  if (format === 'docx') {
    await downloadAsDOCX();
    return;
  }

  const blob = new Blob([optimizedResume], { type: 'text/plain' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = 'optimized_resume.txt';
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
  URL.revokeObjectURL(url);
}

async function downloadAsPDF() {
  try {
    const { jsPDF } = window.jspdf;
    const doc = new jsPDF({ unit: 'mm', format: 'a4' });

    const pageWidth = doc.internal.pageSize.getWidth();
    const pageHeight = doc.internal.pageSize.getHeight();
    const margin = 15;
    const maxLineWidth = pageWidth - margin * 2;
    const lineHeight = 6;
    let yPosition = margin;

    doc.setFont('helvetica', 'normal');
    doc.setFontSize(10);

    const lines = optimizedResume.split('\n');

    let fontName = 'helvetica';
    try {
      const fontUrlReg =
        'https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.66/fonts/Roboto/Roboto-Regular.ttf';
      const fontUrlBold =
        'https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.66/fonts/Roboto/Roboto-Medium.ttf';

      const [bufReg, bufBold] = await Promise.all([
        fetch(fontUrlReg).then((res) => res.arrayBuffer()),
        fetch(fontUrlBold).then((res) => res.arrayBuffer()),
      ]);

      const toBase64 = (buffer) => {
        let binary = '';
        const bytes = new Uint8Array(buffer);
        const len = bytes.byteLength;
        const chunk = 8192;
        for (let i = 0; i < len; i += chunk) {
          binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
        }
        return window.btoa(binary);
      };

      doc.addFileToVFS('Roboto-Regular.ttf', toBase64(bufReg));
      doc.addFileToVFS('Roboto-Bold.ttf', toBase64(bufBold));
      doc.addFont('Roboto-Regular.ttf', 'Roboto', 'normal');
      doc.addFont('Roboto-Bold.ttf', 'Roboto', 'bold');
      fontName = 'Roboto';
      doc.setFont(fontName, 'normal');
    } catch (e) {
      console.warn('Could not load Unicode font, falling back to Helvetica:', e);
    }

    for (let i = 0; i < lines.length; i++) {
      const line = lines[i];
      const trimmedLine = line.trim();

      if (!trimmedLine) {
        yPosition += lineHeight * 0.5;
        continue;
      }

      // Name/title at top
      if (i < 3 && trimmedLine.length < 50 && /[A-Z]/.test(trimmedLine)) {
        doc.setFontSize(16);
        doc.setFont(fontName, 'bold');
        if (yPosition + lineHeight > pageHeight - margin) {
          doc.addPage();
          yPosition = margin;
        }
        const textLines = doc.splitTextToSize(trimmedLine, maxLineWidth);
        textLines.forEach((tl, idx) => {
          doc.text(tl, margin, yPosition + idx * lineHeight);
        });
        yPosition += textLines.length * lineHeight + 2;
        doc.setFontSize(10);
        doc.setFont(fontName, 'normal');
        continue;
      }

      // Section headings
      if (
        (trimmedLine === trimmedLine.toUpperCase() && trimmedLine.length < 60 && trimmedLine.length > 2) ||
        trimmedLine.endsWith(':')
      ) {
        yPosition += lineHeight * 0.5;
        doc.setFontSize(12);
        doc.setFont(fontName, 'bold');
        doc.setTextColor(30, 64, 175);
        if (yPosition + lineHeight > pageHeight - margin) {
          doc.addPage();
          yPosition = margin;
        }
        const textLines = doc.splitTextToSize(trimmedLine, maxLineWidth);
        textLines.forEach((tl, idx) => {
          doc.text(tl, margin, yPosition + idx * lineHeight);
        });
        yPosition += textLines.length * lineHeight + 2;
        doc.setFontSize(10);
        doc.setFont(fontName, 'normal');
        doc.setTextColor(0, 0, 0);
        continue;
      }

      // Bullet points
      if (/^[•●▪■◆%-]\s/.test(trimmedLine)) {
        doc.setFont(fontName, 'normal');
        if (yPosition + lineHeight > pageHeight - margin) {
          doc.addPage();
          yPosition = margin;
        }
        const textLines = doc.splitTextToSize(trimmedLine, maxLineWidth - 5);
        textLines.forEach((tl, idx) => {
          doc.text(tl, margin + 5, yPosition + idx * lineHeight);
        });
        yPosition += textLines.length * lineHeight;
        continue;
      }

      // Regular text
      doc.setFont(fontName, 'normal');
      if (yPosition + lineHeight > pageHeight - margin) {
        doc.addPage();
        yPosition = margin;
      }
      const textLines = doc.splitTextToSize(trimmedLine, maxLineWidth);
      textLines.forEach((tl, idx) => {
        doc.text(tl, margin, yPosition + idx * lineHeight);
      });
      yPosition += textLines.length * lineHeight;
    }

    doc.save('optimized_resume.pdf');
  } catch (error) {
    console.error('PDF export error:', error);
    alert('Failed to create PDF: ' + error.message + '. Downloading as TXT instead.');
    downloadResume('txt');
  }
}

async function downloadAsDOCX() {
  try {
    if (!window.pyodide) {
      alert('Please wait for the system to initialize.');
      return;
    }

    try {
      await window.pyodide.loadPackage(['micropip']);
      await window.pyodide.runPythonAsync(`
import micropip
try:
    await micropip.install('python-docx')
except Exception as e:
    print('python-docx install error or already installed:', e)
      `);
    } catch (e) {
      console.log('python-docx already installed or error:', e);
    }

    window.pyodide.globals.set('resume_text', optimizedResume);
    const docxBytes = await window.pyodide.runPythonAsync(`
import io
from docx import Document
from docx.shared import Pt, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH

doc = Document()
lines = resume_text.split('\\n')
for line in lines:
    line = line.strip()
    if not line:
        continue
    if line == line.upper() and len(line) < 50 and len(line) > 3:
        heading = doc.add_heading(line, level=2)
        heading.runs[0].font.color.rgb = RGBColor(30, 64, 175)
    elif line.startswith(('•', '●', '▪', '-', '–')):
        p = doc.add_paragraph(line, style='List Bullet')
        p.paragraph_format.left_indent = Pt(20)
    else:
        p = doc.add_paragraph(line)
        p.paragraph_format.space_after = Pt(6)
buf = io.BytesIO()
doc.save(buf)
buf.seek(0)
bytes(buf.read())
    `);

    const uint8Array = new Uint8Array(docxBytes.toJs());
    const blob = new Blob([uint8Array], {
      type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
    });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = 'optimized_resume.docx';
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
  } catch (error) {
    console.error('DOCX export error:', error);
    alert('Failed to create DOCX. Downloading as TXT instead.');
    downloadResume('txt');
  }
}

function copyToClipboard() {
  if (!optimizedResume) {
    alert('Please optimize your resume first');
    return;
  }

  navigator.clipboard
    .writeText(optimizedResume)
    .then(() => alert('✅ Copied to clipboard!'))
    .catch((err) => {
      console.error('Copy failed:', err);
      alert('Failed to copy to clipboard');
    });
}

// ========================
// Sample job & init
// ========================
function loadSampleJob() {
  const textarea = document.getElementById('job-description-input');
  if (!textarea) return;
  textarea.value = `Senior Full-Stack Engineer

We are looking for an experienced Full-Stack Engineer to join our growing team. You will be responsible for designing, developing, and maintaining scalable web applications.

Requirements:
• 5+ years of software development experience
• Strong proficiency in JavaScript, Python, and SQL
• Experience with AWS, Docker, and Kubernetes
• Proven track record of building microservices architecture
• Excellent problem-solving and communication skills
• Experience with Agile/Scrum methodologies

Nice to have:
• Experience with React and Node.js
• Knowledge of CI/CD pipelines
• Experience leading technical teams
• Bachelor's degree in Computer Science or related field`;
  textarea.dispatchEvent(new Event('input'));
}

document.addEventListener('DOMContentLoaded', () => {
  setupFileUpload();
  setupTextInputs();
});
// Expose functions used by inline HTML handlers on the global window object
window.toggleResumeTextarea = toggleResumeTextarea;
window.optimizeResume = optimizeResume;
window.downloadResume = downloadResume;
window.loadSampleJob = loadSampleJob;
window.copyToClipboard = copyToClipboard;
window.switchView = switchView;

</script>
import json
import inspect
import sys
from typing import Any, Dict, List, Optional

# Global tool function cache
toolfunctions: Optional[List[Any]] = None


def pythontypetojsontype(py_type: Any) -> str:
    """Map Python types to JSON schema types."""
    import inspect as _inspect
    origin = getattr(py_type, "__origin__", None)

    if origin is list or origin is tuple:
        return "array"
    if origin is dict:
        return "object"

    if py_type in (int, float):
        return "number"
    if py_type is bool:
        return "boolean"
    if py_type is str:
        return "string"

    # Fallback
    return "string"


def extractfunctionschema(func) -> Dict[str, Any]:
    """Build an OpenAI-style tool schema from a Python function."""
    import inspect as _inspect

    sig = _inspect.signature(func)
    params = {}
    required = []

    for name, param in sig.parameters.items():
        if name == "self":
            continue

        ann = param.annotation if param.annotation is not _inspect._empty else str
        param_type = pythontypetojsontype(ann)

        param_schema: Dict[str, Any] = {"type": param_type}

        if param.default is not _inspect._empty:
            param_schema["default"] = param.default
        else:
            required.append(name)

        params[name] = param_schema

    doc = _inspect.getdoc(func) or ""
    parts = doc.split("\n\n", 1)
    description = parts[0].strip() if parts else ""

    return {
        "type": "function",
        "function": {
            "name": func.__name__,
            "description": description,
            "parameters": {
                "type": "object",
                "properties": params,
                "required": required,
            },
        },
    }


def gettoolschemas() -> str:
    """
    Export all tool function schemas in OpenAI format.

    Used by the wllama JavaScript bridge to discover available tools.
    Returns a JSON string.
    """
    global toolfunctions

    # Explicitly expose NO tools by setting toolfunctions = []
    if toolfunctions is None:
        currentmodule = sys.modules[__name__]

        # Infrastructure / non‑tool names that must never be exposed as tools
        INFRANAMES = {
            "gettoolschemas",
            "processuserquery",
            "processuserquerywllama",
            "process_user_query",
            "pythontypetojsontype",
            "extractfunctionschema",
        }

        toolfunctions = []
        for name, obj in inspect.getmembers(currentmodule):
            if inspect.isfunction(obj) and not name.startswith("_"):
                if obj.__module__ == __name__ and name not in INFRANAMES:
                    toolfunctions.append(obj)

    schemas: List[Dict[str, Any]] = []
    for func in toolfunctions:
        try:
            functionschema = extractfunctionschema(func)
            schemas.append(functionschema)
        except Exception as e:
            print(f"Warning: Failed to extract schema for {func.__name__}: {e}")

    return json.dumps(schemas, indent=2)


async def processuserquerywllama(query: str, customprompt: str) -> str:
    """
    Process query using JavaScript wllama agent infrastructure.

    This calls window.processWllamaQuery from JS and returns the model text.
    """
    from js import processWllamaQuery

    try:
        result = await processWllamaQuery(query, customprompt)
        return str(result)
    except Exception as e:
        import traceback

        traceback.print_exc()
        return f"Error: {str(e)}. Make sure wllama model is loaded."


async def processuserquery(query: str) -> str:
    """
    Unified query processor for ALL providers (OpenAI, Anthropic, Local).

    - provider == "local": use the browser wllama + tools pipeline.
    - provider in {"openai", "anthropic"}: use Python LangChain with tools.
    """
    provider = globals().get("PROVIDER", "openai")

    # LOCAL: wllama path (no LangChain in Python)
    if provider == "local":
        try:
            # Custom prompt is optional; empty string lets JS decide system prompt.
            result = await processuserquerywllama(query, "")
            return result
        except Exception as e:
            print(f"WebLLM/Wllama Error: {str(e)}")
            import traceback

            traceback.print_exc()
            return f"Error using local model: {str(e)}"

    # REMOTE PROVIDERS (OpenAI / Anthropic) via LangChain
    try:
        from langchain_core.messages import HumanMessage, ToolMessage
        from langchain_core.tools import tool

        apikey = globals().get("CURRENT_API_KEY")
        if not apikey:
            return "Please enter an API key to use AI-powered features."

        if provider == "openai":
            from langchain_openai import ChatOpenAI

            llm = ChatOpenAI(model="gpt-3.5-turbo", api_key=apikey, temperature=0.7)
        elif provider == "anthropic":
            from langchain_anthropic import ChatAnthropic

            llm = ChatAnthropic(model="claude-3-haiku-20240307", api_key=apikey, temperature=0.7)
        else:
            return f"Unknown provider: {provider}"

        # Auto-discover tool functions (same filter as gettoolschemas)
        global toolfunctions
        if toolfunctions is None:
            currentmodule = sys.modules[__name__]
            INFRANAMES = {
                "gettoolschemas",
                "processuserquery",
                "processuserquerywllama",
                "process_user_query",
                "pythontypetojsontype",
                "extractfunctionschema",
            }
            toolfunctions = []
            for name, obj in inspect.getmembers(currentmodule):
                if inspect.isfunction(obj) and not name.startswith("_"):
                    if obj.__module__ == __name__ and name not in INFRANAMES:
                        toolfunctions.append(obj)

        # If no tools, simple chat path
        if not toolfunctions:
            response = llm.invoke([HumanMessage(content=query)])
            return response.content

        tools = [tool(func) for func in toolfunctions]
        llmwithtools = llm.bind_tools(tools)

        messages: List[Any] = [HumanMessage(content=query)]
        maxiterations = 3

        for _ in range(maxiterations):
            response = llmwithtools.invoke(messages)
            messages.append(response)

            tool_calls = getattr(response, "tool_calls", None)
            if not tool_calls:
                # No more tools: final answer in response.content
                break

            # Map tool names to Python callables
            toolmap = {func.__name__: func for func in toolfunctions}

            for tool_call in tool_calls:
                toolname = tool_call["name"]
                toolargs = tool_call["args"]

                if toolname in toolmap:
                    try:
                        result = toolmap[toolname](**toolargs)
                        messages.append(
                            ToolMessage(
                                content=str(result),
                                tool_call_id=tool_call["id"],
                            )
                        )
                    except Exception as e:
                        messages.append(
                            ToolMessage(
                                content=f"Error executing tool {toolname}: {str(e)}",
                                tool_call_id=tool_call["id"],
                            )
                        )
                else:
                    messages.append(
                        ToolMessage(
                            content=f"Unknown tool {toolname}",
                            tool_call_id=tool_call["id"],
                        )
                    )

        finalmessage = messages[-1]
        if hasattr(finalmessage, "content"):
            return finalmessage.content
        return str(finalmessage)

    except Exception as e:
        import traceback

        traceback.print_exc()
        return f"Error processing query: {str(e)}"


# Optional alias so older JS that calls process_user_query still works
async def process_user_query(query: str) -> str:
    return await processuserquery(query)


print("Unified agent initialized (provider={})".format(globals().get("PROVIDER", "openai")))