Agent Template

ATS Resume Optimizer Agent Template

The ATS Resume Optimizer Agent helps you tailor your resume to specific job postings and stand out in applicant tracking systems. Paste the job description, upload your resume, and let the agent: Parse your resume and the job description, then extract role-specific keywords. Compute an ATS compatibility score with a breakdown across skills, experience, formatting, and keyword match. Rewrite bullet points to be more outcome‑oriented, quantified, and aligned with the role level you select. Suggest missing skills or keywords that appear in the job posting but not in your resume. Highlight critical issues like vague bullets, missing dates, or formatting problems that may confuse ATS parsers. The agent never fabricates experience or qualifications you don’t have. It works purely with the content you provide and makes conservative, explainable edits you can accept or further refine.

resume analyse optimizer ats CV job

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

ATS Resume Optimizer Agent Template 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 analyse optimizer ats CV job
Template Preview

Template Metadata

Slug
ats-resume-optimizer-agent-template
Created By
ozzo
Created
Jul 13, 2026
Usage Count
0

Tags

resume analyse optimizer ats CV job

Code Statistics

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

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>
"""
Unified Agent Template - Works with OpenAI, Anthropic, and Local WebLLM
Pure Python tools with conditional LangChain wrapping based on provider.

This template enables ONE codebase for all three providers:
- OpenAI: Uses LangChain with ChatOpenAI
- Anthropic: Uses LangChain with ChatAnthropic  
- Local WebLLM: Routes to JavaScript LangChain.js bridge (NO Python LangChain)

Key features:
- Pure Python tool functions (no decorators at definition time)
- Schema extraction via get_tool_schemas() for WebLLM JavaScript bridge
- Conditional LangChain imports only when needed (cloud providers)
- Runtime tool wrapping with tool() function for cloud providers
"""

import json
import inspect
from typing import get_type_hints

# Provider injected from generator context (openai|anthropic|local)

# ============================================================================
# Schema Extraction Helpers (for all providers)
# ============================================================================

def _python_type_to_json_type(python_type):
    """Convert Python type to JSON schema type."""
    type_mapping = {
        'str': 'string',
        'int': 'integer',
        'float': 'number',
        'bool': 'boolean',
        'list': 'array',
        'dict': 'object',
    }
    type_name = python_type.__name__ if hasattr(python_type, '__name__') else str(python_type)
    return type_mapping.get(type_name, 'string')


def _extract_function_schema(func):
    """
    Extract OpenAI function schema from a Python function.
    Works for all providers - no LangChain dependency.
    
    Extracts:
    - Function name
    - Description from docstring
    - Parameters with types and descriptions
    - Required vs optional parameters
    
    Args:
        func: Python function with type hints and docstring
    
    Returns:
        dict: OpenAI function schema format
    """
    sig = inspect.signature(func)
    
    # Get type hints
    try:
        hints = get_type_hints(func)
    except:
        hints = {}
    
    # Parse docstring
    doc = inspect.getdoc(func) or ""
    description = doc.split('\n\n')[0] if doc else f"Execute {func.__name__}"
    
    # Build parameters schema
    parameters = {
        "type": "object",
        "properties": {},
        "required": []
    }
    
    # Extract parameter descriptions from docstring Args section
    param_descriptions = {}
    if "Args:" in doc:
        args_section = doc.split("Args:")[1].split("Returns:")[0] if "Returns:" in doc else doc.split("Args:")[1]
        for line in args_section.split('\n'):
            line = line.strip()
            if ':' in line:
                param_name = line.split(':')[0].strip()
                param_desc = line.split(':', 1)[1].strip()
                param_descriptions[param_name] = param_desc
    
    # Process each parameter
    for param_name, param in sig.parameters.items():
        if param_name in ['self', 'cls']:
            continue
        
        param_type = hints.get(param_name, param.annotation)
        if param_type == inspect.Parameter.empty:
            param_type = str
        
        json_type = _python_type_to_json_type(param_type)
        
        prop_schema = {
            "type": json_type,
            "description": param_descriptions.get(param_name, f"The {param_name} parameter")
        }
        
        parameters["properties"][param_name] = prop_schema
        
        # Mark as required if no default value
        if param.default == inspect.Parameter.empty:
            parameters["required"].append(param_name)
    
    return {
        "name": func.__name__,
        "description": description,
        "parameters": parameters
    }

# Resume Optimizer AI Agent
# Analyzes resumes against job descriptions and provides optimization recommendations

import json
import re
from typing import Dict, List, Any

# Global state
api_key = ""

# Import LangChain
try:
    from langchain_core.messages import HumanMessage, SystemMessage
    from langchain_openai import ChatOpenAI
    langchain_available = True
    print("[OK] LangChain imports successful")
except ImportError as e:
    langchain_available = False
    print(f"[WARNING] LangChain not available: {e}")

def extract_keywords_from_job(job_description: str) -> Dict[str, List[str]]:
    """Extract required skills and keywords from job description"""
    
    # Common keyword patterns
    skill_patterns = [
        r'(?i)(?:experience with|proficiency in|knowledge of|familiar with|skilled in)\s+([\w\s,/+#-]+)',
        r'(?i)(?:required|must have|should have):\s*([\w\s,/+#-]+)',
        r'(?i)(?:skills?|technologies?|tools?|languages?):\s*([\w\s,/+#-]+)'
    ]
    
    keywords = {
        'critical': [],
        'high': [],
        'medium': [],
        'nice_to_have': []
    }
    
    # Extract from "Requirements" section
    requirements_section = re.search(r'(?i)requirements?:(.*?)(?:nice to have|$)', job_description, re.DOTALL)
    if requirements_section:
        req_text = requirements_section.group(1)
        # Extract bullet points
        bullets = re.findall(r'[•●▪️-]\s*(.+)', req_text)
        
        for bullet in bullets:
            # Extract skills/technologies
            words = re.findall(r'\b[A-Z][\w+#.-]+\b', bullet)
            if words:
                if any(term in bullet.lower() for term in ['required', 'must', 'essential']):
                    keywords['critical'].extend(words)
                elif any(term in bullet.lower() for term in ['experience', 'years', 'proven']):
                    keywords['high'].extend(words)
                else:
                    keywords['medium'].extend(words)
    
    # Extract from "Nice to have" section
    nice_to_have = re.search(r'(?i)nice to have:(.*?)$', job_description, re.DOTALL)
    if nice_to_have:
        nice_text = nice_to_have.group(1)
        words = re.findall(r'\b[A-Z][\w+#.-]+\b', nice_text)
        keywords['nice_to_have'].extend(words)
    
    # Remove duplicates
    for category in keywords:
        keywords[category] = list(set(keywords[category]))
    
    return keywords

def extract_resume_keywords(resume_text: str) -> List[str]:
    """Extract technical keywords from resume"""
    
    # Common technical terms pattern
    tech_keywords = re.findall(r'\b[A-Z][\w+#.-]+\b', resume_text)
    
    # Filter out common words
    common_words = {'The', 'And', 'For', 'With', 'This', 'That', 'From', 'Have', 'Been', 'Were', 'Will'}
    filtered = [kw for kw in tech_keywords if kw not in common_words]
    
    return list(set(filtered))

def calculate_keyword_density(resume_text: str, keywords: List[str]) -> float:
    """Calculate keyword density percentage"""
    
    total_words = len(resume_text.split())
    keyword_count = sum(resume_text.lower().count(kw.lower()) for kw in keywords)
    
    if total_words == 0:
        return 0.0
    
    density = (keyword_count / total_words) * 100
    return round(density, 2)

def match_keywords(resume_keywords: List[str], job_keywords: Dict[str, List[str]]) -> Dict[str, List[str]]:
    """Match resume keywords against job requirements"""
    
    resume_lower = [kw.lower() for kw in resume_keywords]
    all_job_keywords = []
    for category in job_keywords.values():
        all_job_keywords.extend([kw.lower() for kw in category])
    
    matched = [kw for kw in resume_keywords if kw.lower() in all_job_keywords]
    missing = [kw for kw in all_job_keywords if kw not in resume_lower]
    
    return {
        'matched': matched,
        'missing': list(set(missing))
    }

def calculate_ats_score(resume_text: str, job_description: str, matched_keywords: Dict[str, List[str]]) -> Dict[str, Any]:
    """Calculate ATS compatibility score"""
    
    # Extract keywords
    job_keywords = extract_keywords_from_job(job_description)
    resume_keywords = extract_resume_keywords(resume_text)
    
    # Count total required keywords
    total_critical = len(job_keywords['critical'])
    total_high = len(job_keywords['high'])
    
    # Count matched keywords
    matched_critical = sum(1 for kw in job_keywords['critical'] if kw.lower() in [k.lower() for k in matched_keywords['matched']])
    matched_high = sum(1 for kw in job_keywords['high'] if kw.lower() in [k.lower() for k in matched_keywords['matched']])
    
    # Calculate keyword score (0-100)
    if total_critical + total_high > 0:
        keyword_score = ((matched_critical * 2 + matched_high) / (total_critical * 2 + total_high)) * 100
    else:
        keyword_score = 50
    
    # Format score (simple heuristics)
    format_score = 95  # Assume good formatting
    if len(resume_text.split('\n')) < 10:
        format_score -= 20
    
    # Content score (check for quantifiable achievements)
    achievement_count = len(re.findall(r'\d+%|\d+\+|\$\d+|\d+x', resume_text))
    content_score = min(100, 60 + (achievement_count * 5))
    
    # Experience score
    years_mentioned = len(re.findall(r'\d+\+?\s*years?', resume_text, re.IGNORECASE))
    experience_score = min(100, 70 + (years_mentioned * 10))
    
    # Overall score (weighted average)
    overall_before = int(
        keyword_score * 0.4 +
        format_score * 0.2 +
        content_score * 0.2 +
        experience_score * 0.2
    )
    
    # Optimized score (assume +25% improvement)
    overall_after = min(100, overall_before + 25)
    
    return {
        'before': overall_before,
        'after': overall_after,
        'breakdown': {
            'Keywords': int(keyword_score),
            'Formatting': format_score,
            'Content': content_score,
            'Experience': experience_score
        }
    }

def optimize_bullet_points(text: str) -> List[Dict[str, str]]:
    """Identify weak bullet points and suggest improvements"""
    
    weak_verbs = ['worked', 'did', 'was', 'responsible for', 'helped', 'assisted']
    strong_verbs = ['architected', 'engineered', 'led', 'developed', 'implemented', 'designed', 'optimized']
    
    improvements = []
    
    # Find bullet points
    bullets = re.findall(r'[•●▪️-]\s*(.+)', text)
    
    for bullet in bullets:
        bullet_lower = bullet.lower()
        
        # Check for weak verbs
        for weak in weak_verbs:
            if weak in bullet_lower:
                improvements.append({
                    'type': 'weak_verb',
                    'original': bullet,
                    'issue': f'Weak action verb: "{weak}"',
                    'suggestion': f'Use stronger verbs like: {", ".join(strong_verbs[:3])}'
                })
                break
        
        # Check for lack of quantification
        if not re.search(r'\d+', bullet):
            improvements.append({
                'type': 'no_metrics',
                'original': bullet,
                'issue': 'Missing quantifiable results',
                'suggestion': 'Add numbers, percentages, or specific outcomes'
            })
    
    return improvements

def optimize_resume(resume_text: str, job_description: str, industry: str, experience_level: str) -> str:
    """Main optimization function"""
    
    print(f"[OPTIMIZE] Starting optimization for {industry} - {experience_level}")
    
    # Extract keywords
    job_keywords = extract_keywords_from_job(job_description)
    resume_keywords = extract_resume_keywords(resume_text)
    
    print(f"[OPTIMIZE] Found {len(resume_keywords)} keywords in resume")
    print(f"[OPTIMIZE] Found {sum(len(v) for v in job_keywords.values())} keywords in job description")
    
    # Match keywords
    matched = match_keywords(resume_keywords, job_keywords)
    
    # Calculate scores
    ats_score = calculate_ats_score(resume_text, job_description, matched)
    
    # Keyword density
    all_job_kw = []
    for v in job_keywords.values():
        all_job_kw.extend(v)
    density = calculate_keyword_density(resume_text, all_job_kw)
    
    # Identify improvements
    bullet_improvements = optimize_bullet_points(resume_text)
    
    # Generate recommendations
    recommendations = []
    
    # Critical: Missing keywords
    if len(matched['missing']) > 0:
        recommendations.append({
            'type': 'critical',
            'title': 'Missing Required Keywords',
            'text': f"Add these keywords from job description: {', '.join(matched['missing'][:5])}"
        })
    
    # Warnings: Bullet point improvements
    for improvement in bullet_improvements[:3]:
        recommendations.append({
            'type': 'warning',
            'title': improvement['issue'],
            'text': f"Original: \"{improvement['original']}\" - {improvement['suggestion']}"
        })
    
    # Pro tips
    recommendations.append({
        'type': 'info',
        'title': '💡 Pro Tip',
        'text': 'Start each bullet point with a strong action verb and include quantifiable results'
    })
    
    # IMPORTANT (Local/WebLLM mode):
    # The in-browser LLM is responsible for generating the rewritten resume.
    # This tool returns analysis + recommendations that the LLM uses.
    print("[OPTIMIZE] Returning analysis for WebLLM to synthesize optimized resume")
    optimized_text = resume_text
    
    # Prepare result
    result = {
        'optimized_resume': optimized_text,
        'ats_score': ats_score,
        'keyword_analysis': {
            'matched': matched['matched'],
            'missing': matched['missing'],
            'total': len(matched['matched']) + len(matched['missing']),
            'density': density
        },
        'recommendations': recommendations
    }
    
    return json.dumps(result)

print("[INIT] Resume Optimizer ready")
print("[INIT] Upload your resume and paste a job description to begin")

# ============================================================================
# Tool Schema Export (for WebLLM JavaScript bridge)
# ============================================================================

def get_tool_schemas() -> str:
    """
    Export all tool function schemas in OpenAI format.
    Used by WebLLM JavaScript bridge to discover available tools.
    
    This function is called by PyodideToolBridge in JavaScript to:
    1. Discover what tools are available
    2. Get their schemas for LLM binding
    3. Enable function calling in WebLLM
    
    Returns:
        str: JSON string with OpenAI-format tool schemas
    """
    # List all your tool functions here
    # Example: tool_functions = [example_tool, another_tool]
    tool_functions = [
        # TODO: Add your tool functions here
        # For Data Analysis Agent:
        # load_csv_data, get_data_summary, get_column_info, get_value_counts, create_chart, get_correlation_analysis
    ]
    
    # Auto-discovery: if tool_functions is empty, try to find functions defined in this module
    # that are not private (start with _) and not imported
    if not tool_functions:
        import inspect
        import sys
        current_module = sys.modules[__name__]
        for name, obj in inspect.getmembers(current_module):
            if inspect.isfunction(obj) and not name.startswith('_'):
                # Filter out imported functions and infrastructure functions
                if obj.__module__ == __name__ and name not in ['get_tool_schemas', 'process_user_query', 'process_user_query_webllm']:
                    tool_functions.append(obj)
    
    schemas = []
    for func in tool_functions:
        try:
            function_schema = _extract_function_schema(func)
            openai_schema = {
                "type": "function",
                "function": function_schema
            }
            schemas.append(openai_schema)
        except Exception as e:
            print(f"Warning: Failed to extract schema for {func.__name__}: {e}")
    
    return json.dumps(schemas, indent=2)


# ============================================================================
# Unified Query Processing
# ============================================================================

async def process_user_query(query: str) -> str:
    """
    Unified query processor - works for ALL providers (OpenAI, Anthropic, Local).
    
    Routes to appropriate backend based on PROVIDER global variable:
    - 'local': Routes to JavaScript WebLLM agent (NO Python LangChain)
    - 'openai': Uses Python LangChain with ChatOpenAI
    - 'anthropic': Uses Python LangChain with ChatAnthropic
    
    Args:
        query: User's natural language query
    
    Returns:
        str: Response from the agent
    """
    provider = globals().get('PROVIDER', 'openai')
    
    if provider == 'local':
        # ====================================================================
        # WebLLM Local Mode: Use JavaScript LangChain.js bridge
        # ====================================================================
        # No Python LangChain imports needed here
        # All inference happens in JavaScript with WebLLM + LangChain.js
        # Tools are executed in Python via PyodideToolBridge
        
        try:
            # Route to JavaScript WebLLM agent
            # This function is defined in the HTML template and bridges to JS
            result = await process_user_query_webllm(query)
            return result
        except Exception as e:
            print(f"[WebLLM Error] {str(e)}")
            import traceback
            traceback.print_exc()
            return f"❌ Error using WebLLM: {str(e)}"
    
    else:
        # ====================================================================
        # Cloud Providers (OpenAI/Anthropic): Use Python LangChain
        # ====================================================================
        # Import LangChain components ONLY for cloud providers
        # This keeps the bundle smaller for local mode
        
        try:
            from langchain_core.tools import tool
            from langchain_core.messages import HumanMessage, AIMessage, ToolMessage
            
            # Get API key from globals (set by HTML template)
            api_key = globals().get('current_api_key', '')
            if not api_key:
                return "⚠️ Please enter an API key to use AI-powered features."
            
            # Import and initialize provider-specific LLM
            if provider == 'openai':
                from langchain_openai import ChatOpenAI
                llm = ChatOpenAI(
                    model="gpt-3.5-turbo",
                    api_key=api_key,
                    temperature=0.7
                )
            elif provider == 'anthropic':
                from langchain_anthropic import ChatAnthropic
                llm = ChatAnthropic(
                    model="gpt-3.5-turbo",
                    api_key=api_key,
                    temperature=0.7
                )
            else:
                return f"❌ Unknown provider: {provider}"
            
            # Get tool functions list using auto-discovery (same as get_tool_schemas)
            tool_functions = []
            
            # Auto-discover tool functions from current module
            import sys
            current_module = sys.modules[__name__]
            for name, obj in inspect.getmembers(current_module):
                if inspect.isfunction(obj) and not name.startswith('_'):
                    # Filter out imported functions and infrastructure functions
                    if obj.__module__ == __name__ and name not in ['get_tool_schemas', 'process_user_query', 'process_user_query_webllm', '_python_type_to_json_type', '_extract_function_schema']:
                        tool_functions.append(obj)
            
            if not tool_functions:
                # No tools defined - simple conversation mode
                response = llm.invoke([HumanMessage(content=query)])
                return response.content
            
            # Wrap tools with @tool decorator at runtime
            # This is the key: tool() is called as a FUNCTION, not decorator
            tools = [tool(func) for func in tool_functions]
            llm_with_tools = llm.bind_tools(tools)
            
            # Tool calling loop with message history
            messages = [HumanMessage(content=query)]
            
            max_iterations = 3  # Prevent infinite loops
            for iteration in range(max_iterations):
                response = llm_with_tools.invoke(messages)
                messages.append(response)
                
                # Check if LLM made any tool calls
                if not response.tool_calls:
                    break  # No more tools to call, we're done
                
                # Execute each tool call
                for tool_call in response.tool_calls:
                    tool_name = tool_call['name']
                    tool_args = tool_call['args']
                    
                    # Build tool name -> function mapping
                    tool_map = {func.__name__: func for func in tool_functions}
                    
                    if tool_name in tool_map:
                        try:
                            # Execute the tool
                            result = tool_map[tool_name](**tool_args)
                            messages.append(ToolMessage(
                                content=str(result),
                                tool_call_id=tool_call['id']
                            ))
                        except Exception as e:
                            # Tool execution failed
                            messages.append(ToolMessage(
                                content=f"❌ Error executing {tool_name}: {str(e)}",
                                tool_call_id=tool_call['id']
                            ))
                    else:
                        # Unknown tool requested
                        messages.append(ToolMessage(
                            content=f"❌ Unknown tool: {tool_name}",
                            tool_call_id=tool_call['id']
                        ))
            
            # Return final response content
            final_message = messages[-1]
            if hasattr(final_message, 'content'):
                return final_message.content
            else:
                return str(final_message)
                
        except Exception as e:
            import traceback
            traceback.print_exc()
            return f"❌ Error processing query: {str(e)}"


# ============================================================================
# Initialization Message
# ============================================================================

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