AI_Resume_Scanner / static /index.html
majidali1256's picture
feat: add Microsoft Word (.docx/.doc) document parsing alongside PDF and TXT
8f21bc9
Raw
History Blame Contribute Delete
39.1 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MATCH.AI — AI Resume Screener Studio (Executive 3D Edition)</title>
<!-- Google Stitch Typography: Geist + JetBrains Mono -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style>
/* Google Stitch MCP "Obsidian Prism" (Dark Theme Default) */
:root {
--bg-page: #080f1e;
--surface-card: rgba(18, 26, 44, 0.82);
--surface-hover: rgba(26, 36, 60, 0.92);
--border-card: rgba(76, 215, 246, 0.28);
--border-hover: rgba(76, 215, 246, 0.65);
--primary-accent: #4cd7f6;
--secondary-accent: #d0bcff;
--success-color: #10b981;
--warning-color: #f59e0b;
--danger-color: #ef4444;
--text-main: #f1f5f9;
--text-muted: #94a3b8;
--header-bg: rgba(8, 15, 30, 0.88);
--input-bg: rgba(11, 19, 38, 0.75);
--card-shadow: 0 25px 60px rgba(0, 0, 0, 0.45), 0 0 20px rgba(76, 215, 246, 0.12);
--dropzone-border: rgba(76, 215, 246, 0.35);
}
/* Executive Aurora Light Theme (Bold Classic Borders & High Contrast) */
[data-theme="light"] {
--bg-page: #eaf0f8;
--surface-card: rgba(255, 255, 255, 0.96);
--surface-hover: #ffffff;
--border-card: rgba(2, 132, 199, 0.32);
--border-hover: #0284c7;
--primary-accent: #0284c7;
--secondary-accent: #6366f1;
--success-color: #059669;
--warning-color: #d97706;
--danger-color: #dc2626;
--text-main: #0f172a;
--text-muted: #475569;
--header-bg: rgba(234, 240, 248, 0.92);
--input-bg: #f8fafc;
--card-shadow: 0 20px 50px rgba(15, 23, 42, 0.11), 0 4px 15px rgba(2, 132, 199, 0.08);
--dropzone-border: rgba(2, 132, 199, 0.38);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
transition: background-color 0.35s ease, color 0.35s ease, border-color 0.35s ease, box-shadow 0.35s ease;
}
body {
font-family: 'Geist', -apple-system, sans-serif;
background-color: var(--bg-page);
color: var(--text-main);
min-height: 100vh;
overflow-x: hidden;
position: relative;
}
/* 3D Interactive Particle Network Canvas */
#bg-canvas {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 0;
pointer-events: none;
}
/* Sticky Executive Header */
header {
position: sticky;
top: 0;
z-index: 50;
background: var(--header-bg);
backdrop-filter: blur(28px);
border-bottom: 1.5px solid var(--border-card);
padding: 1rem 2.75rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
}
.header-inner {
max-width: 1380px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
}
.brand {
display: flex;
align-items: center;
gap: 0.9rem;
font-weight: 800;
font-size: 1.5rem;
letter-spacing: -0.025em;
color: var(--text-main);
text-decoration: none;
}
.brand-logo {
width: 44px;
height: 44px;
border-radius: 13px;
background: linear-gradient(135deg, var(--primary-accent), var(--secondary-accent));
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
font-size: 1.4rem;
box-shadow: 0 8px 24px rgba(2, 132, 199, 0.35);
transform: rotate(-5deg);
transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.brand:hover .brand-logo {
transform: rotate(10deg) scale(1.1);
}
.header-controls {
display: flex;
align-items: center;
gap: 1.1rem;
}
/* Executive Pill Switch Button */
.theme-switch-btn {
display: inline-flex;
align-items: center;
gap: 0.65rem;
padding: 0.55rem 1.25rem;
background: var(--surface-card);
border: 1.5px solid var(--border-card);
border-radius: 999px;
color: var(--text-main);
font-family: 'JetBrains Mono', monospace;
font-size: 0.84rem;
font-weight: 700;
cursor: pointer;
box-shadow: 0 4px 14px rgba(0,0,0,0.06);
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.theme-switch-btn:hover {
border-color: var(--border-hover);
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(2, 132, 199, 0.18);
}
.theme-switch-btn i {
font-size: 1rem;
color: var(--primary-accent);
}
.stitch-badge {
display: inline-flex;
align-items: center;
gap: 0.55rem;
padding: 0.5rem 1.15rem;
background: rgba(2, 132, 199, 0.1);
border: 1.5px solid var(--border-card);
border-radius: 999px;
font-family: 'JetBrains Mono', monospace;
font-size: 0.78rem;
font-weight: 700;
color: var(--primary-accent);
letter-spacing: 0.04em;
}
.stitch-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--primary-accent);
box-shadow: 0 0 10px var(--primary-accent);
animation: pulse-ring 2s infinite;
}
@keyframes pulse-ring {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.4); opacity: 0.45; }
}
/* Workspace Grid */
.main-container {
position: relative;
z-index: 10;
max-width: 1380px;
margin: 2.75rem auto;
padding: 0 1.5rem;
display: grid;
grid-template-columns: 485px 1fr;
gap: 2.4rem;
align-items: start;
}
@media (max-width: 1100px) {
.main-container {
grid-template-columns: 1fr;
}
}
/* Executive 3D Glassmorphism Cards with Bold Defined Borders */
.glass-card {
position: relative;
background: var(--surface-card);
border: 1.5px solid var(--border-card);
border-radius: 24px;
padding: 2.3rem;
backdrop-filter: blur(32px);
box-shadow: var(--card-shadow);
transform-style: preserve-3d;
transform: perspective(1200px);
transition: transform 0.12s ease-out, border-color 0.3s ease, box-shadow 0.3s ease;
overflow: hidden;
}
.glass-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 4px;
background: linear-gradient(90deg, var(--primary-accent), var(--secondary-accent));
opacity: 0.9;
}
.glass-card::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(
650px circle at var(--mouse-x, 50%) var(--mouse-y, 50%),
rgba(2, 132, 199, 0.08),
transparent 45%
);
pointer-events: none;
z-index: 1;
}
.glass-card:hover {
border-color: var(--border-hover);
}
.card-header {
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 1.4rem;
font-weight: 800;
margin-bottom: 0.35rem;
letter-spacing: -0.015em;
}
.card-sub {
font-size: 0.92rem;
color: var(--text-muted);
margin-bottom: 1.85rem;
line-height: 1.5;
}
/* Upload Dropzones */
.field-label {
display: flex;
justify-content: space-between;
font-family: 'JetBrains Mono', monospace;
font-size: 0.8rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-muted);
margin-bottom: 0.65rem;
}
.dropzone {
border: 2px dashed var(--dropzone-border);
border-radius: 18px;
padding: 1.9rem 1.35rem;
text-align: center;
cursor: pointer;
transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1);
background: var(--input-bg);
margin-bottom: 1.75rem;
}
.dropzone:hover {
border-color: var(--primary-accent);
background: rgba(2, 132, 199, 0.06);
transform: translateY(-3px);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.06);
}
.dropzone-icon {
font-size: 2.4rem;
background: linear-gradient(135deg, var(--primary-accent), var(--secondary-accent));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 0.7rem;
}
.dropzone h4 {
font-size: 1rem;
font-weight: 700;
margin-bottom: 0.25rem;
color: var(--text-main);
}
.dropzone p {
font-size: 0.83rem;
color: var(--text-muted);
}
.badge-selected {
margin-top: 0.85rem;
padding: 0.55rem 1.05rem;
background: rgba(16, 185, 129, 0.15);
border: 1.5px solid var(--success-color);
border-radius: 12px;
font-size: 0.85rem;
font-weight: 700;
color: var(--success-color);
display: none;
align-items: center;
justify-content: center;
gap: 0.55rem;
}
/* Tabs */
.tab-box {
display: flex;
gap: 0.5rem;
background: var(--input-bg);
padding: 0.4rem;
border-radius: 14px;
border: 1.5px solid var(--border-card);
margin-bottom: 1.1rem;
}
.tab-btn {
flex: 1;
padding: 0.65rem;
font-family: 'JetBrains Mono', monospace;
font-size: 0.83rem;
font-weight: 700;
border: none;
background: transparent;
color: var(--text-muted);
border-radius: 10px;
cursor: pointer;
transition: all 0.2s ease;
}
.tab-btn.active {
background: rgba(2, 132, 199, 0.15);
color: var(--primary-accent);
border: 1px solid var(--primary-accent);
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
textarea {
width: 100%;
height: 140px;
background: var(--input-bg);
border: 1.5px solid var(--border-card);
border-radius: 14px;
padding: 1.1rem;
color: var(--text-main);
font-family: inherit;
font-size: 0.92rem;
resize: vertical;
margin-bottom: 1.75rem;
}
textarea:focus {
outline: none;
border-color: var(--primary-accent);
box-shadow: 0 0 0 3px rgba(2, 132, 199, 0.15);
}
/* Action Buttons */
.btn-row {
display: flex;
gap: 1.1rem;
}
.btn {
flex: 1;
padding: 1rem 1.5rem;
border-radius: 14px;
font-weight: 800;
font-size: 0.96rem;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.65rem;
cursor: pointer;
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
border: none;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary-accent), var(--secondary-accent));
color: #ffffff;
box-shadow: 0 8px 25px rgba(2, 132, 199, 0.35);
}
[data-theme="dark"] .btn-primary {
color: #0b1326;
}
.btn-primary:hover {
transform: translateY(-3px);
box-shadow: 0 14px 32px rgba(2, 132, 199, 0.5);
}
.btn-secondary {
background: var(--surface-card);
color: var(--text-main);
border: 1.5px solid var(--border-card);
}
.btn-secondary:hover {
border-color: var(--primary-accent);
transform: translateY(-2px);
background: var(--surface-hover);
}
/* Right Panel Empty & Loader States */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 540px;
text-align: center;
padding: 3.5rem;
background: var(--surface-card);
border: 2px dashed var(--border-card);
border-radius: 24px;
box-shadow: var(--card-shadow);
}
.empty-icon {
width: 90px;
height: 90px;
border-radius: 50%;
background: rgba(2, 132, 199, 0.1);
border: 1.5px solid var(--border-card);
display: flex;
align-items: center;
justify-content: center;
font-size: 2.5rem;
color: var(--primary-accent);
margin-bottom: 1.6rem;
animation: float 4s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-12px); }
}
/* 3D Holographic Cube Loader */
.loader-box {
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 540px;
gap: 1.85rem;
background: var(--surface-card);
border: 1.5px solid var(--border-card);
border-radius: 24px;
box-shadow: var(--card-shadow);
}
.cube {
width: 68px;
height: 68px;
position: relative;
transform-style: preserve-3d;
animation: spinCube 3s infinite linear;
}
.cube-face {
position: absolute;
width: 68px;
height: 68px;
background: rgba(2, 132, 199, 0.18);
border: 2px solid var(--primary-accent);
box-shadow: 0 0 18px rgba(2, 132, 199, 0.35);
}
.front { transform: rotateY(0deg) translateZ(34px); }
.back { transform: rotateY(180deg) translateZ(34px); }
.right { transform: rotateY(90deg) translateZ(34px); }
.left { transform: rotateY(-90deg) translateZ(34px); }
.top { transform: rotateX(90deg) translateZ(34px); }
.bottom { transform: rotateX(-90deg) translateZ(34px); }
@keyframes spinCube {
to { transform: rotateX(360deg) rotateY(360deg); }
}
/* Results Card */
#results-dashboard {
display: none;
}
.score-banner {
display: flex;
align-items: center;
gap: 2.4rem;
background: linear-gradient(135deg, rgba(16, 185, 129, 0.12), rgba(2, 132, 199, 0.08));
border: 1.5px solid rgba(16, 185, 129, 0.35);
border-radius: 20px;
padding: 1.85rem;
margin-bottom: 2rem;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.05);
}
.gauge-wrap {
position: relative;
width: 120px;
height: 120px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
.score-center {
position: absolute;
display: flex;
flex-direction: column;
align-items: center;
}
.score-val {
font-size: 2.25rem;
font-weight: 800;
line-height: 1;
}
.score-lbl {
font-family: 'JetBrains Mono', monospace;
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.08em;
color: var(--text-muted);
margin-top: 3px;
}
.rationale-header {
font-size: 1.2rem;
font-weight: 800;
color: var(--text-main);
margin-bottom: 0.45rem;
}
.rationale-body {
font-size: 0.95rem;
line-height: 1.62;
color: var(--text-muted);
}
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
margin-bottom: 1.5rem;
}
@media (max-width: 850px) {
.grid-2 {
grid-template-columns: 1fr;
}
}
.section-box {
background: var(--input-bg);
border: 1.5px solid var(--border-card);
border-radius: 18px;
padding: 1.5rem;
}
.section-title {
display: flex;
align-items: center;
gap: 0.65rem;
font-size: 1.03rem;
font-weight: 800;
margin-bottom: 1.15rem;
}
.list-row {
display: flex;
align-items: flex-start;
gap: 0.7rem;
font-size: 0.9rem;
line-height: 1.58;
color: var(--text-main);
margin-bottom: 0.9rem;
padding-bottom: 0.9rem;
border-bottom: 1px solid var(--border-card);
}
.list-row:last-child {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
}
.footer-disclaimer {
font-size: 0.8rem;
color: var(--text-muted);
background: var(--input-bg);
border: 1.5px solid var(--border-card);
border-radius: 14px;
padding: 1.1rem 1.35rem;
line-height: 1.58;
margin-top: 1.5rem;
}
</style>
</head>
<body>
<!-- 3D Interactive Particle Network Canvas -->
<canvas id="bg-canvas"></canvas>
<!-- Executive Sticky Header -->
<header>
<div class="header-inner">
<a href="#" class="brand">
<div class="brand-logo">
<i class="fa-solid fa-cube"></i>
</div>
<span>MATCH.AI <small style="font-weight: 500; color: var(--primary-accent); font-size: 0.88rem;">Executive Studio</small></span>
</a>
<div class="header-controls">
<!-- Theme Switch Toggle Button -->
<button class="theme-switch-btn" id="theme-toggle" onclick="toggleTheme()" title="Switch Light / Dark Theme">
<i class="fa-solid fa-moon" id="theme-icon"></i>
<span id="theme-label">Dark Theme</span>
</button>
<div class="stitch-badge">
<span class="stitch-dot"></span>
Google Stitch • 3D Edition
</div>
</div>
</div>
</header>
<!-- Main Workspace -->
<main class="main-container">
<!-- Left Column: Document Scanner Deck -->
<div class="glass-card">
<div class="card-header">
<i class="fa-solid fa-wand-magic-sparkles" style="color: var(--primary-accent);"></i>
Document Screening Deck
</div>
<div class="card-sub">Upload candidate resume & job description</div>
<div class="field-label">
<span>Candidate Resume (.pdf / .docx / .txt)</span>
<span>Max 10MB</span>
</div>
<div class="dropzone" id="resume-dropzone" onclick="document.getElementById('resume-file').click()">
<i class="fa-solid fa-file-pdf dropzone-icon"></i>
<h4>Drag & Drop candidate resume</h4>
<p>Supports .pdf, Word (.docx), or .txt</p>
<input type="file" id="resume-file" accept=".pdf,.txt,.docx,.doc" style="display: none;">
<div class="badge-selected" id="resume-badge">
<i class="fa-solid fa-check"></i>
<span id="resume-name"></span>
</div>
</div>
<div class="field-label">
<span>Job Description</span>
</div>
<div class="tab-box">
<button class="tab-btn active" id="btn-file" onclick="setTab('file')">Upload File</button>
<button class="tab-btn" id="btn-text" onclick="setTab('text')">Paste Text</button>
</div>
<div id="view-jd-file">
<div class="dropzone" id="jd-dropzone" onclick="document.getElementById('jd-file').click()">
<i class="fa-solid fa-briefcase dropzone-icon"></i>
<h4>Drag & Drop Job Spec file</h4>
<p>Supports .pdf, Word (.docx), or .txt</p>
<input type="file" id="jd-file" accept=".pdf,.txt,.docx,.doc" style="display: none;">
<div class="badge-selected" id="jd-badge">
<i class="fa-solid fa-check"></i>
<span id="jd-name"></span>
</div>
</div>
</div>
<div id="view-jd-text" style="display: none;">
<textarea id="jd-textarea" placeholder="Paste the complete job description or requirements here..."></textarea>
</div>
<div class="btn-row">
<button class="btn btn-secondary" onclick="triggerDemo()">
<i class="fa-solid fa-bolt" style="color: var(--warning-color);"></i> 1-Click Demo
</button>
<button class="btn btn-primary" onclick="runAssessment()">
<i class="fa-solid fa-radar"></i> Run 3D Scan
</button>
</div>
</div>
<!-- Right Column: Interactive Results Dashboard -->
<div>
<!-- Empty State -->
<div class="empty-state" id="panel-empty">
<div class="empty-icon">
<i class="fa-solid fa-layer-group"></i>
</div>
<h3 style="font-size: 1.4rem; font-weight: 800; margin-bottom: 0.5rem; color: var(--text-main);">Awaiting Document Scan</h3>
<p style="color: var(--text-muted); max-width: 390px; line-height: 1.62; font-size: 0.94rem;">
Upload documents on the left or click <strong>1-Click Demo</strong> to experience the structured Pydantic AI assessment.
</p>
</div>
<!-- 3D Hologram Loader -->
<div class="loader-box" id="panel-loader">
<div class="cube">
<div class="cube-face front"></div>
<div class="cube-face back"></div>
<div class="cube-face right"></div>
<div class="cube-face left"></div>
<div class="cube-face top"></div>
<div class="cube-face bottom"></div>
</div>
<div style="text-align: center;">
<h3 style="font-size: 1.3rem; font-weight: 800; margin-bottom: 0.35rem; color: var(--text-main);">AI Assessment in Progress...</h3>
<p style="color: var(--text-muted); font-size: 0.9rem;">Validating against Pydantic schema & extracting evidence</p>
</div>
</div>
<!-- Results Dashboard -->
<div class="glass-card" id="results-dashboard">
<div class="score-banner">
<div class="gauge-wrap">
<svg width="120" height="120" viewBox="0 0 120 120" style="transform: rotate(-90deg);">
<circle cx="60" cy="60" r="48" stroke="rgba(128,128,128,0.18)" stroke-width="10" fill="none" />
<circle id="gauge-ring" cx="60" cy="60" r="48" stroke="#10b981" stroke-width="10" fill="none"
stroke-dasharray="301" stroke-dashoffset="301" stroke-linecap="round"
style="transition: stroke-dashoffset 1.5s cubic-bezier(0.34, 1.56, 0.64, 1), stroke 0.5s ease;" />
</svg>
<div class="score-center">
<span class="score-val" id="res-score">0</span>
<span class="score-lbl">MATCH</span>
</div>
</div>
<div>
<div class="rationale-header">Executive Evaluation Rationale</div>
<div class="rationale-body" id="res-rationale"></div>
</div>
</div>
<div class="grid-2">
<div class="section-box">
<div class="section-title" style="color: var(--success-color);">
<i class="fa-solid fa-circle-check"></i> Matched Qualifications
</div>
<div id="list-matched"></div>
</div>
<div class="section-box">
<div class="section-title" style="color: var(--warning-color);">
<i class="fa-solid fa-triangle-exclamation"></i> Identified Gaps
</div>
<div id="list-missing"></div>
</div>
</div>
<div class="section-box">
<div class="section-title" style="color: var(--primary-accent);">
<i class="fa-solid fa-lightbulb"></i> Actionable Presentation Suggestions
</div>
<div id="list-suggestions"></div>
</div>
<div class="footer-disclaimer">
<strong style="color: var(--text-main);">Technical Transparency Note:</strong> <span id="res-limitations"></span>
</div>
</div>
</div>
</main>
<script>
/* -----------------------------------------------------------
0. Theme Toggle Switcher (Light / Dark Mode)
----------------------------------------------------------- */
function initTheme() {
const saved = localStorage.getItem('match_ai_theme') || 'light';
applyTheme(saved);
}
function toggleTheme() {
const current = document.documentElement.getAttribute('data-theme') === 'light' ? 'light' : 'dark';
const next = current === 'dark' ? 'light' : 'dark';
applyTheme(next);
localStorage.setItem('match_ai_theme', next);
}
function applyTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
const icon = document.getElementById('theme-icon');
const label = document.getElementById('theme-label');
if (theme === 'light') {
icon.className = 'fa-solid fa-sun';
label.textContent = 'Light Theme';
} else {
icon.className = 'fa-solid fa-moon';
label.textContent = 'Dark Theme';
}
}
initTheme();
/* -----------------------------------------------------------
1. 3D Particle Network Canvas with Depth Scaling
----------------------------------------------------------- */
const canvas = document.getElementById('bg-canvas');
const ctx = canvas.getContext('2d');
let width, height;
let particles = [];
function resizeCanvas() {
width = canvas.width = window.innerWidth;
height = canvas.height = window.innerHeight;
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
class Particle {
constructor() {
this.x = Math.random() * width;
this.y = Math.random() * height;
this.z = Math.random() * 0.8 + 0.4; /* Depth factor */
this.vx = (Math.random() - 0.5) * 0.6 * this.z;
this.vy = (Math.random() - 0.5) * 0.6 * this.z;
this.radius = (Math.random() * 1.8 + 1.2) * this.z;
}
update() {
this.x += this.vx;
this.y += this.vy;
if (this.x < 0 || this.x > width) this.vx = -this.vx;
if (this.y < 0 || this.y > height) this.vy = -this.vy;
}
draw() {
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
const isLight = document.documentElement.getAttribute('data-theme') === 'light';
ctx.fillStyle = isLight
? `rgba(2, 132, 199, ${0.55 * this.z})`
: `rgba(76, 215, 246, ${0.6 * this.z})`;
ctx.fill();
}
}
function initParticles() {
particles = [];
const count = Math.floor((width * height) / 13000);
for (let i = 0; i < count; i++) {
particles.push(new Particle());
}
}
initParticles();
function animateCanvas() {
ctx.clearRect(0, 0, width, height);
const isLight = document.documentElement.getAttribute('data-theme') === 'light';
for (let i = 0; i < particles.length; i++) {
particles[i].update();
particles[i].draw();
for (let j = i + 1; j < particles.length; j++) {
const dx = particles[i].x - particles[j].x;
const dy = particles[i].y - particles[j].y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 120) {
ctx.beginPath();
ctx.moveTo(particles[i].x, particles[i].y);
ctx.lineTo(particles[j].x, particles[j].y);
ctx.strokeStyle = isLight
? `rgba(2, 132, 199, ${0.22 * (1 - dist / 120)})`
: `rgba(76, 215, 246, ${0.2 * (1 - dist / 120)})`;
ctx.lineWidth = 1;
ctx.stroke();
}
}
}
requestAnimationFrame(animateCanvas);
}
animateCanvas();
/* -----------------------------------------------------------
2. 3D Perspective Mouse Tilt
----------------------------------------------------------- */
document.querySelectorAll('.glass-card').forEach(card => {
card.addEventListener('mousemove', (e) => {
const rect = card.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const rotateX = (((y - rect.height / 2) / (rect.height / 2)) * -5.5).toFixed(2);
const rotateY = (((x - rect.width / 2) / (rect.width / 2)) * 5.5).toFixed(2);
card.style.transform = `perspective(1200px) rotateX(${rotateX}deg) rotateY(${rotateY}deg)`;
card.style.setProperty('--mouse-x', `${x}px`);
card.style.setProperty('--mouse-y', `${y}px`);
});
card.addEventListener('mouseleave', () => {
card.style.transform = `perspective(1200px) rotateX(0deg) rotateY(0deg)`;
});
});
/* -----------------------------------------------------------
3. UI Actions & Navigation Handlers
----------------------------------------------------------- */
let activeTab = 'file';
function setTab(tab) {
activeTab = tab;
document.getElementById('btn-file').classList.toggle('active', tab === 'file');
document.getElementById('btn-text').classList.toggle('active', tab === 'text');
document.getElementById('view-jd-file').style.display = tab === 'file' ? 'block' : 'none';
document.getElementById('view-jd-text').style.display = tab === 'text' ? 'block' : 'none';
}
document.getElementById('resume-file').addEventListener('change', (e) => {
if (e.target.files[0]) {
document.getElementById('resume-name').textContent = e.target.files[0].name;
document.getElementById('resume-badge').style.display = 'inline-flex';
}
});
document.getElementById('jd-file').addEventListener('change', (e) => {
if (e.target.files[0]) {
document.getElementById('jd-name').textContent = e.target.files[0].name;
document.getElementById('jd-badge').style.display = 'inline-flex';
}
});
function switchPanel(panel) {
document.getElementById('panel-empty').style.display = panel === 'empty' ? 'flex' : 'none';
document.getElementById('panel-loader').style.display = panel === 'loader' ? 'flex' : 'none';
document.getElementById('results-dashboard').style.display = panel === 'results' ? 'block' : 'none';
}
/* -----------------------------------------------------------
4. Render Results
----------------------------------------------------------- */
function renderAssessment(data) {
switchPanel('results');
const score = data.match_score;
document.getElementById('res-score').textContent = score;
document.getElementById('res-rationale').textContent = data.score_rationale;
const ring = document.getElementById('gauge-ring');
const circumference = 301;
const offset = circumference - (score / 100) * circumference;
let color = '#10b981';
if (score < 75) color = '#f59e0b';
if (score < 55) color = '#ef4444';
ring.style.stroke = color;
setTimeout(() => {
ring.style.strokeDashoffset = offset;
}, 100);
populateList('list-matched', data.matched_requirements, 'fa-check', '#10b981');
populateList('list-missing', data.missing_requirements, 'fa-exclamation-circle', '#f59e0b');
populateList('list-suggestions', data.suggestions, 'fa-arrow-right', 'var(--primary-accent)');
const lims = data.limitations && data.limitations.length > 0
? data.limitations.join(' • ')
: 'No document limitations noted.';
document.getElementById('res-limitations').textContent = lims;
}
function populateList(elementId, items, icon, color) {
const container = document.getElementById(elementId);
container.innerHTML = '';
if (!items || items.length === 0) {
container.innerHTML = `<div class="list-row" style="color: var(--text-muted);">None identified.</div>`;
return;
}
items.forEach(text => {
const row = document.createElement('div');
row.className = 'list-row';
row.innerHTML = `<i class="fa-solid ${icon}" style="color: ${color}; margin-top: 3px;"></i> <span>${text}</span>`;
container.appendChild(row);
});
}
async function triggerDemo() {
switchPanel('loader');
try {
const res = await fetch('/api/sample');
const data = await res.json();
renderAssessment(data);
} catch (err) {
alert('Demo execution error: ' + err.message);
switchPanel('empty');
}
}
async function runAssessment() {
const resumeFile = document.getElementById('resume-file').files[0];
if (!resumeFile) {
alert('Please select a candidate resume file (.pdf, .docx, or .txt)');
return;
}
const formData = new FormData();
formData.append('resume_file', resumeFile);
if (activeTab === 'file') {
const jdFile = document.getElementById('jd-file').files[0];
if (!jdFile) {
alert('Please select a job description file (.pdf, .docx, or .txt)');
return;
}
formData.append('jd_file', jdFile);
} else {
const jdText = document.getElementById('jd-textarea').value.trim();
if (!jdText) {
alert('Please paste the job description text.');
return;
}
formData.append('jd_text', jdText);
}
switchPanel('loader');
try {
const res = await fetch('/api/scan', {
method: 'POST',
body: formData
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.detail || 'Server error occurred');
}
const data = await res.json();
renderAssessment(data);
} catch (err) {
alert('Scan error: ' + err.message);
switchPanel('empty');
}
}
</script>
</body>
</html>