mcp / tech_radar /ui /web_app.html
Anish Dahiya
Deploy TechRadar-MCP Gradio App to Hugging Face Space
cca012f
Raw
History Blame Contribute Delete
36.9 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TechRadar MCP — Universal Tech Hiring Intelligence</title>
<!-- Google Fonts: Bebas Neue & Poppins -->
<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=Bebas+Neue&family=Poppins:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<!-- Chart.js for Market Analytics -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
:root {
--bg-dark: #090514;
--bg-card: rgba(20, 14, 45, 0.65);
--border-glass: rgba(0, 229, 255, 0.25);
--border-glow: #00e5ff;
--accent-cyan: #00e5ff;
--accent-purple: #9d4edd;
--accent-pink: #e94057;
--text-main: #f0f4f8;
--text-muted: #a0aec0;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: var(--bg-dark);
color: var(--text-main);
font-family: 'Poppins', sans-serif;
overflow-x: hidden;
min-height: 100vh;
}
/* Canvas 3D Cyber Wave Grid Background */
#cyber-canvas {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: -1;
pointer-events: none;
}
/* Main Wrapper */
.app-container {
max-width: 1350px;
margin: 0 auto;
padding: 20px 30px 80px 30px;
}
/* Navbar */
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
margin-bottom: 30px;
}
.brand-logo {
display: flex;
align-items: center;
gap: 12px;
font-family: 'Bebas Neue', sans-serif;
font-size: 2.2rem;
letter-spacing: 2px;
background: linear-gradient(90deg, #00e5ff, #9d4edd, #e94057);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.mcp-status-pill {
background: rgba(0, 229, 255, 0.1);
border: 1px solid var(--accent-cyan);
color: var(--accent-cyan);
padding: 6px 16px;
border-radius: 50px;
font-size: 0.85rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
box-shadow: 0 0 15px rgba(0, 229, 255, 0.3);
}
.status-dot {
width: 8px;
height: 8px;
background-color: var(--accent-cyan);
border-radius: 50%;
animation: pulse-dot 1.5s infinite;
}
@keyframes pulse-dot {
0% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(0, 229, 255, 0.7); }
70% { transform: scale(1); box-shadow: 0 0 0 8px rgba(0, 229, 255, 0); }
100% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(0, 229, 255, 0); }
}
/* Hero Banner Section (Inspired by reference 5718062.jpg) */
.hero-banner {
position: relative;
background: linear-gradient(135deg, rgba(20, 14, 50, 0.8) 0%, rgba(10, 6, 30, 0.9) 100%);
border: 1px solid var(--border-glass);
backdrop-filter: blur(20px);
border-radius: 24px;
padding: 45px 50px;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 40px;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.6), inset 0 1px 1px rgba(255, 255, 255, 0.15);
overflow: hidden;
}
.hero-banner::before {
content: '';
position: absolute;
top: -50%;
right: -10%;
width: 400px;
height: 400px;
background: radial-gradient(circle, rgba(157, 78, 221, 0.3) 0%, transparent 70%);
pointer-events: none;
}
.hero-text {
max-width: 650px;
z-index: 2;
}
.hero-tag {
text-transform: uppercase;
font-size: 0.9rem;
letter-spacing: 3px;
color: var(--accent-cyan);
font-weight: 700;
margin-bottom: 10px;
}
.hero-title {
font-family: 'Bebas Neue', sans-serif;
font-size: 4rem;
line-height: 1;
letter-spacing: 3px;
margin-bottom: 15px;
background: linear-gradient(90deg, #ffffff, #e2e8f0);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-shadow: 0 0 30px rgba(0, 229, 255, 0.3);
}
.hero-subtitle {
font-size: 1.05rem;
color: var(--text-muted);
margin-bottom: 25px;
line-height: 1.6;
}
.btn-gradient {
background: linear-gradient(90deg, var(--accent-cyan), var(--accent-purple));
color: white;
padding: 14px 34px;
border-radius: 50px;
font-size: 0.95rem;
font-weight: 700;
border: none;
cursor: pointer;
text-transform: uppercase;
letter-spacing: 1px;
transition: all 0.3s ease;
box-shadow: 0 0 25px rgba(0, 229, 255, 0.4);
display: inline-flex;
align-items: center;
gap: 10px;
}
.btn-gradient:hover {
transform: translateY(-3px) scale(1.02);
box-shadow: 0 0 35px rgba(157, 78, 221, 0.6);
}
/* 3D Visual Orb / Graphics */
.hero-graphic {
position: relative;
width: 320px;
height: 220px;
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
}
.orb-ring {
position: absolute;
border-radius: 50%;
border: 2px dashed rgba(0, 229, 255, 0.4);
animation: spin 20s linear infinite;
}
.orb-ring-1 { width: 220px; height: 220px; border-color: rgba(0, 229, 255, 0.5); }
.orb-ring-2 { width: 170px; height: 170px; border-color: rgba(157, 78, 221, 0.6); animation-direction: reverse; animation-duration: 15s; }
.orb-core {
width: 110px;
height: 110px;
border-radius: 50%;
background: radial-gradient(circle at 30% 30%, #00e5ff, #9d4edd 70%, #090514);
box-shadow: 0 0 50px rgba(0, 229, 255, 0.6);
display: flex;
align-items: center;
justify-content: center;
font-family: 'Bebas Neue', sans-serif;
font-size: 2rem;
color: white;
text-shadow: 0 0 10px rgba(255, 255, 255, 0.8);
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* Controls & Filter Bar */
.filter-section {
background: var(--bg-card);
backdrop-filter: blur(15px);
border: 1px solid var(--border-glass);
border-radius: 16px;
padding: 20px 25px;
margin-bottom: 30px;
display: flex;
flex-wrap: wrap;
gap: 20px;
align-items: center;
justify-content: space-between;
}
.filter-group {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.filter-label {
font-size: 0.85rem;
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 1px;
}
.pill-btn {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
color: var(--text-main);
padding: 8px 16px;
border-radius: 30px;
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
transition: all 0.25s ease;
}
.pill-btn:hover, .pill-btn.active {
background: linear-gradient(90deg, rgba(0, 229, 255, 0.2), rgba(157, 78, 221, 0.2));
border-color: var(--accent-cyan);
color: white;
box-shadow: 0 0 15px rgba(0, 229, 255, 0.3);
}
.search-input {
background: rgba(10, 6, 25, 0.8);
border: 1px solid var(--border-glass);
color: white;
padding: 10px 20px;
border-radius: 30px;
font-size: 0.9rem;
width: 280px;
outline: none;
transition: all 0.3s ease;
}
.search-input:focus {
border-color: var(--accent-cyan);
box-shadow: 0 0 20px rgba(0, 229, 255, 0.4);
}
/* Navigation Tabs */
.tab-menu {
display: flex;
gap: 15px;
margin-bottom: 30px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
padding-bottom: 10px;
}
.tab-item {
font-family: 'Bebas Neue', sans-serif;
font-size: 1.5rem;
letter-spacing: 1.5px;
color: var(--text-muted);
cursor: pointer;
padding: 8px 16px;
border-radius: 8px;
transition: all 0.3s ease;
}
.tab-item:hover, .tab-item.active {
color: var(--accent-cyan);
background: rgba(0, 229, 255, 0.08);
text-shadow: 0 0 10px rgba(0, 229, 255, 0.5);
}
/* Tab Content Panes */
.tab-pane {
display: none;
}
.tab-pane.active {
display: block;
}
/* Job Grid & 3D Glass Cards */
.job-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
gap: 25px;
}
.job-card-3d {
background: var(--bg-card);
backdrop-filter: blur(20px);
border: 1px solid var(--border-glass);
border-radius: 20px;
padding: 25px;
transition: transform 0.3s ease, box-shadow 0.3s ease, border-color 0.3s ease;
position: relative;
overflow: hidden;
transform-style: preserve-3d;
perspective: 1000px;
}
.job-card-3d:hover {
transform: translateY(-8px) rotateX(2deg) rotateY(-2deg);
border-color: var(--accent-cyan);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.6), 0 0 30px rgba(0, 229, 255, 0.3);
}
.job-card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
}
.job-title {
font-family: 'Poppins', sans-serif;
font-size: 1.2rem;
font-weight: 700;
color: #ffffff;
line-height: 1.3;
}
.badge-location {
background: rgba(0, 229, 255, 0.15);
border: 1px solid rgba(0, 229, 255, 0.4);
color: var(--accent-cyan);
padding: 4px 10px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
white-space: nowrap;
}
.job-company {
font-size: 0.9rem;
color: var(--text-muted);
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
}
.job-salary {
font-size: 1rem;
font-weight: 700;
color: #00ff9d;
margin-bottom: 12px;
}
.job-desc {
font-size: 0.85rem;
color: #cbd5e0;
line-height: 1.5;
margin-bottom: 15px;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.stack-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 15px;
}
.stack-tag {
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.12);
color: var(--text-main);
padding: 3px 8px;
border-radius: 6px;
font-size: 0.75rem;
}
.job-card-footer {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.75rem;
color: var(--text-muted);
border-top: 1px solid rgba(255, 255, 255, 0.08);
padding-top: 12px;
}
/* ATS & Skill Gap Section */
.analyzer-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
}
.analyzer-box {
background: var(--bg-card);
backdrop-filter: blur(20px);
border: 1px solid var(--border-glass);
border-radius: 20px;
padding: 30px;
}
.form-label {
font-size: 0.9rem;
font-weight: 600;
color: var(--text-muted);
margin-bottom: 8px;
display: block;
}
.form-textarea {
width: 100%;
height: 180px;
background: rgba(10, 6, 25, 0.8);
border: 1px solid var(--border-glass);
border-radius: 12px;
padding: 15px;
color: white;
font-family: 'Poppins', sans-serif;
font-size: 0.85rem;
outline: none;
resize: vertical;
margin-bottom: 15px;
}
.form-select {
width: 100%;
background: rgba(10, 6, 25, 0.8);
border: 1px solid var(--border-glass);
border-radius: 10px;
padding: 12px;
color: white;
font-family: 'Poppins', sans-serif;
font-size: 0.85rem;
outline: none;
margin-bottom: 20px;
}
.score-circle {
width: 110px;
height: 110px;
border-radius: 50%;
border: 4px solid var(--accent-cyan);
box-shadow: 0 0 25px rgba(0, 229, 255, 0.5);
display: flex;
align-items: center;
justify-content: center;
font-family: 'Bebas Neue', sans-serif;
font-size: 2.5rem;
color: var(--accent-cyan);
margin: 0 auto 20px auto;
}
.diff-card {
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 204, 0, 0.3);
border-radius: 10px;
padding: 12px 15px;
margin-bottom: 12px;
font-size: 0.85rem;
}
.diff-original { color: #eb5757; margin-bottom: 4px; }
.diff-tailored { color: #27ae60; font-weight: 600; }
/* JSON Output Box */
.json-box {
background: #05030a;
border: 1px solid var(--border-glass);
border-radius: 12px;
padding: 15px;
font-family: 'Courier New', Courier, monospace;
font-size: 0.85rem;
color: #00e5ff;
max-height: 400px;
overflow-y: auto;
white-space: pre-wrap;
}
</style>
</head>
<body>
<!-- 3D Cyber Mesh Canvas Background -->
<canvas id="cyber-canvas"></canvas>
<div class="app-container">
<!-- Top Navbar -->
<div class="navbar">
<div class="brand-logo">⚡ TECHRADAR MCP</div>
<div class="mcp-status-pill">
<span class="status-dot"></span> FASTMCP SERVER ACTIVE (STDIO/SSE)
</div>
</div>
<!-- Hero Banner (3D Glassmorphism, matching 5718062.jpg) -->
<div class="hero-banner">
<div class="hero-text">
<div class="hero-tag">MODEL CONTEXT PROTOCOL ECOSYSTEM</div>
<h1 class="hero-title">UNIVERSAL TECH HIRING INTELLIGENCE</h1>
<p class="hero-subtitle">
Autonomous AI job market intelligence, AST skill-gap evaluation, and ATS resume patching across Bengaluru, Pune, Hyderabad, Gurgaon, Mumbai & Remote tech hubs.
</p>
<button class="btn-gradient" onclick="switchTab('radar')">
🚀 EXPLORE TECH RADAR
</button>
</div>
<div class="hero-graphic">
<div class="orb-ring orb-ring-1"></div>
<div class="orb-ring orb-ring-2"></div>
<div class="orb-core">MCP AI</div>
</div>
</div>
<!-- Filter Controls -->
<div class="filter-section">
<div class="filter-group">
<span class="filter-label">Domain:</span>
<button class="pill-btn active" onclick="filterDomain('All', this)">ALL DOMAINS</button>
<button class="pill-btn" onclick="filterDomain('Backend Engineering', this)">BACKEND</button>
<button class="pill-btn" onclick="filterDomain('Frontend Engineering', this)">FRONTEND</button>
<button class="pill-btn" onclick="filterDomain('Full Stack Engineering', this)">FULL STACK</button>
<button class="pill-btn" onclick="filterDomain('Cloud & DevOps', this)">DEVOPS</button>
<button class="pill-btn" onclick="filterDomain('AI/ML & GenAI', this)">AI / GENAI</button>
</div>
<div class="filter-group">
<span class="filter-label">City:</span>
<button class="pill-btn active" onclick="filterCity('All', this)">ALL CITIES</button>
<button class="pill-btn" onclick="filterCity('Bengaluru', this)">BLR</button>
<button class="pill-btn" onclick="filterCity('Pune', this)">PUNE</button>
<button class="pill-btn" onclick="filterCity('Hyderabad', this)">HYD</button>
<button class="pill-btn" onclick="filterCity('Remote', this)">REMOTE</button>
</div>
<input type="text" id="searchInput" class="search-input" placeholder="🔍 Search Go, React, vLLM..." onkeyup="handleSearch()">
</div>
<!-- Navigation Tabs -->
<div class="tab-menu">
<div class="tab-item active" onclick="switchTab('radar', this)">📡 UNIVERSAL TECH RADAR</div>
<div class="tab-item" onclick="switchTab('analytics', this)">📊 MARKET ANALYTICS</div>
<div class="tab-item" onclick="switchTab('ats', this)">🎯 ATS SKILL GAP EVALUATOR</div>
<div class="tab-item" onclick="switchTab('mcp', this)">🧪 FASTMCP SERVER TESTER</div>
</div>
<!-- TAB 1: RADAR GRID -->
<div id="tab-radar" class="tab-pane active">
<div class="job-grid" id="jobGrid">
<!-- Rendered dynamically via JS -->
</div>
</div>
<!-- TAB 2: ANALYTICS -->
<div id="tab-analytics" class="tab-pane">
<div class="analyzer-container">
<div class="analyzer-box">
<h3 style="font-family: 'Bebas Neue', sans-serif; font-size: 1.8rem; margin-bottom: 15px; color: var(--accent-cyan);">🔥 TOP DEMANDED TECH FRAMEWORKS</h3>
<canvas id="skillsChart" height="220"></canvas>
</div>
<div class="analyzer-box">
<h3 style="font-family: 'Bebas Neue', sans-serif; font-size: 1.8rem; margin-bottom: 15px; color: var(--accent-purple);">🏢 MAJOR TECH HIRING HUBS</h3>
<canvas id="hubsChart" height="220"></canvas>
</div>
</div>
</div>
<!-- TAB 3: ATS ANALYZER -->
<div id="tab-ats" class="tab-pane">
<div class="analyzer-container">
<div class="analyzer-box">
<h3 style="font-family: 'Bebas Neue', sans-serif; font-size: 1.8rem; margin-bottom: 15px;">INPUT CANDIDATE PROFILE</h3>
<label class="form-label">Paste Resume Text:</label>
<textarea id="resumeInput" class="form-textarea">Senior Backend Engineer with 4 years experience building Go microservices, REST APIs, and Docker containers in Bengaluru. Seeking Senior Go or Distributed Systems roles.</textarea>
<label class="form-label">Select Target Job Opening:</label>
<select id="jobSelect" class="form-select"></select>
<button class="btn-gradient" style="width: 100%; justify-content: center;" onclick="runAtsEvaluation()">
🚀 EVALUATE ATS MATCH & SKILL GAP
</button>
</div>
<div class="analyzer-box" id="atsResultBox">
<h3 style="font-family: 'Bebas Neue', sans-serif; font-size: 1.8rem; margin-bottom: 15px; text-align: center;">EVALUATION RESULT</h3>
<div class="score-circle" id="matchScore">--</div>
<h4 style="font-size: 0.95rem; margin-bottom: 5px; color: #00ff9d;">✅ Matched Skills:</h4>
<p id="matchedSkills" style="font-size: 0.85rem; color: var(--text-muted); margin-bottom: 15px;">-</p>
<h4 style="font-size: 0.95rem; margin-bottom: 5px; color: #eb5757;">❌ Missing Skills (Dealbreakers):</h4>
<p id="missingSkills" style="font-size: 0.85rem; color: var(--text-muted); margin-bottom: 20px;">-</p>
<h4 style="font-size: 0.95rem; margin-bottom: 10px; color: var(--accent-cyan);">📝 ATS Tailored Resume Bullets:</h4>
<div id="tailoredBullets"></div>
</div>
</div>
</div>
<!-- TAB 4: MCP TESTER -->
<div id="tab-mcp" class="tab-pane">
<div class="analyzer-container">
<div class="analyzer-box">
<h3 style="font-family: 'Bebas Neue', sans-serif; font-size: 1.8rem; margin-bottom: 15px;">SELECT FASTMCP TOOL</h3>
<select id="mcpToolSelect" class="form-select" onchange="updateMcpInputs()">
<option value="search_tech_jobs">search_tech_jobs</option>
<option value="analyze_skill_gap">analyze_skill_gap</option>
<option value="generate_tailored_resume_patch">generate_tailored_resume_patch</option>
<option value="get_market_insights">get_market_insights</option>
<option value="generate_interview_prep_kit">generate_interview_prep_kit</option>
</select>
<div id="mcpInputFields"></div>
<button class="btn-gradient" style="width: 100%; justify-content: center;" onclick="executeMcpTool()">
⚡ EXECUTE FASTMCP TOOL
</button>
</div>
<div class="analyzer-box">
<h3 style="font-family: 'Bebas Neue', sans-serif; font-size: 1.8rem; margin-bottom: 15px; color: var(--accent-cyan);">JSON-RPC TOOL OUTPUT</h3>
<div class="json-box" id="mcpJsonOutput">// Execute an MCP tool to view response</div>
</div>
</div>
</div>
</div>
<!-- Scripts for 3D Mesh Canvas & App Logic -->
<script>
// 1. Interactive 3D Cyber Dot Mesh Canvas (Matching 5718062.jpg)
const canvas = document.getElementById('cyber-canvas');
const ctx = canvas.getContext('2d');
let width = canvas.width = window.innerWidth;
let height = canvas.height = window.innerHeight;
window.addEventListener('resize', () => {
width = canvas.width = window.innerWidth;
height = canvas.height = window.innerHeight;
});
const dots = [];
const spacing = 45;
for (let x = 0; x < width + spacing; x += spacing) {
for (let y = 0; y < height + spacing; y += spacing) {
dots.push({
baseX: x,
baseY: y,
x: x,
y: y,
angle: Math.random() * Math.PI * 2,
speed: 0.02 + Math.random() * 0.02
});
}
}
let mouseX = width / 2;
let mouseY = height / 2;
window.addEventListener('mousemove', (e) => {
mouseX = e.clientX;
mouseY = e.clientY;
});
function animateCanvas() {
ctx.clearRect(0, 0, width, height);
dots.forEach(dot => {
dot.angle += dot.speed;
const dist = Math.hypot(mouseX - dot.baseX, mouseY - dot.baseY);
const maxDist = 200;
let offset = Math.sin(dot.angle) * 4;
if (dist < maxDist) {
offset += (1 - dist / maxDist) * 15;
}
ctx.fillStyle = dist < 150 ? 'rgba(0, 229, 255, 0.4)' : 'rgba(157, 78, 221, 0.15)';
ctx.beginPath();
ctx.arc(dot.baseX, dot.baseY + offset, dist < 150 ? 2 : 1.2, 0, Math.PI * 2);
ctx.fill();
});
requestAnimationFrame(animateCanvas);
}
animateCanvas();
// 2. State & API Fetching Logic
let allJobs = [];
let currentDomain = 'All';
let currentCity = 'All';
async function fetchJobs() {
try {
const res = await fetch('/api/jobs');
const data = await res.json();
allJobs = data.jobs || [];
renderJobs();
populateJobSelect();
initCharts();
} catch (err) {
console.error("Failed to load jobs:", err);
}
}
function renderJobs() {
const grid = document.getElementById('jobGrid');
grid.innerHTML = '';
const filtered = allJobs.filter(j => {
const domainMatch = currentDomain === 'All' || j.tech_domain === currentDomain;
const cityMatch = currentCity === 'All' || j.city.toLowerCase() === currentCity.toLowerCase();
return domainMatch && cityMatch;
});
if (filtered.length === 0) {
grid.innerHTML = `<div style="grid-column: 1/-1; text-align: center; color: var(--text-muted); padding: 40px;">No matching tech roles found for selected filters.</div>`;
return;
}
filtered.forEach(job => {
const card = document.createElement('div');
card.className = 'job-card-3d';
card.innerHTML = `
<div class="job-card-header">
<h3 class="job-title">${job.title}</h3>
<span class="badge-location">📍 ${job.city}</span>
</div>
<div class="job-company">🏢 ${job.company} &bull; <span style="color: var(--accent-cyan);">${job.tech_domain}</span></div>
<div class="job-salary">💰 ₹${job.salary_min_lpa}L - ₹${job.salary_max_lpa}L PA &nbsp;|&nbsp; ⏳ ${job.experience_min_years}-${job.experience_max_years} Yrs</div>
<p class="job-desc">${job.requirements}</p>
<div class="stack-tags">
${job.tech_stack.map(s => `<span class="stack-tag">${s}</span>`).join('')}
</div>
<div class="job-card-footer">
<span>Job ID: <code>${job.id}</code></span>
<span style="color: #00ff9d;">🌐 ${job.work_mode}</span>
</div>
`;
// 3D Card Hover Perspective Effect
card.addEventListener('mousemove', (e) => {
const rect = card.getBoundingClientRect();
const x = e.clientX - rect.left - rect.width / 2;
const y = e.clientY - rect.top - rect.height / 2;
card.style.transform = `perspective(1000px) rotateX(${-y / 15}deg) rotateY(${x / 15}deg) translateY(-5px)`;
});
card.addEventListener('mouseleave', () => {
card.style.transform = `perspective(1000px) rotateX(0deg) rotateY(0deg) translateY(0)`;
});
grid.appendChild(card);
});
}
function filterDomain(domain, el) {
currentDomain = domain;
document.querySelectorAll('.filter-group:nth-child(1) .pill-btn').forEach(b => b.classList.remove('active'));
el.classList.add('active');
renderJobs();
}
function filterCity(city, el) {
currentCity = city;
document.querySelectorAll('.filter-group:nth-child(2) .pill-btn').forEach(b => b.classList.remove('active'));
el.classList.add('active');
renderJobs();
}
function handleSearch() {
const query = document.getElementById('searchInput').value.toLowerCase();
const cards = document.querySelectorAll('.job-card-3d');
cards.forEach(card => {
const text = card.innerText.toLowerCase();
card.style.display = text.includes(query) ? 'block' : 'none';
});
}
function switchTab(tabId, el) {
document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.tab-item').forEach(t => t.classList.remove('active'));
document.getElementById(`tab-${tabId}`).classList.add('active');
if (el) el.classList.add('active');
}
function populateJobSelect() {
const sel = document.getElementById('jobSelect');
sel.innerHTML = allJobs.map(j => `<option value="${j.id}">${j.id}${j.title} at ${j.company} (${j.city})</option>`).join('');
}
async function runAtsEvaluation() {
const resumeText = document.getElementById('resumeInput').value;
const targetJobId = document.getElementById('jobSelect').value;
try {
const res = await fetch('/api/mcp/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tool: 'analyze_skill_gap', args: { resume_text: resumeText, target_job_id: targetJobId } })
});
const data = await res.json();
const report = JSON.parse(data.result);
document.getElementById('matchScore').innerText = `${report.match_percentage}%`;
document.getElementById('matchedSkills').innerText = report.matched_skills.join(', ') || 'None';
document.getElementById('missingSkills').innerText = report.missing_skills.join(', ') || 'None';
// Fetch Patch
const patchRes = await fetch('/api/mcp/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tool: 'generate_tailored_resume_patch', args: { resume_text: resumeText, target_job_id: targetJobId } })
});
const patchData = await patchRes.json();
const patch = JSON.parse(patchData.result);
document.getElementById('tailoredBullets').innerHTML = patch.tailored_bullets.map(b => `
<div class="diff-card">
<div class="diff-original">Original: ${b.original}</div>
<div class="diff-tailored">Tailored: ${b.tailored}</div>
</div>
`).join('');
} catch (err) {
console.error("Evaluation failed:", err);
}
}
function updateMcpInputs() {
const tool = document.getElementById('mcpToolSelect').value;
const fields = document.getElementById('mcpInputFields');
if (tool === 'search_tech_jobs') {
fields.innerHTML = `
<label class="form-label">domain:</label><input id="arg_domain" class="search-input" style="width:100%; margin-bottom:10px;" value="Backend Engineering">
<label class="form-label">city:</label><input id="arg_city" class="search-input" style="width:100%; margin-bottom:10px;" value="Bengaluru">
<label class="form-label">query:</label><input id="arg_query" class="search-input" style="width:100%; margin-bottom:15px;" value="Go">
`;
} else {
fields.innerHTML = `
<label class="form-label">target_job_id:</label><input id="arg_job_id" class="search-input" style="width:100%; margin-bottom:15px;" value="${allJobs[0]?.id || 'BLR-BACKEND-101'}">
`;
}
}
async function executeMcpTool() {
const tool = document.getElementById('mcpToolSelect').value;
let args = {};
if (tool === 'search_tech_jobs') {
args = {
domain: document.getElementById('arg_domain').value,
city: document.getElementById('arg_city').value,
query: document.getElementById('arg_query').value
};
} else {
args = {
target_job_id: document.getElementById('arg_job_id').value,
resume_text: "Senior backend developer experienced in Go, Docker, AWS."
};
}
try {
const res = await fetch('/api/mcp/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tool: tool, args: args })
});
const data = await res.json();
document.getElementById('mcpJsonOutput').innerText = JSON.stringify(JSON.parse(data.result), null, 2);
} catch (err) {
document.getElementById('mcpJsonOutput').innerText = "Error executing tool: " + err;
}
}
function initCharts() {
const skillsCtx = document.getElementById('skillsChart').getContext('2d');
new Chart(skillsCtx, {
type: 'bar',
data: {
labels: ['Go', 'React', 'Kubernetes', 'Python', 'FastMCP', 'Spark'],
datasets: [{
label: 'Skill Demand %',
data: [85, 78, 72, 90, 65, 55],
backgroundColor: 'rgba(0, 229, 255, 0.6)',
borderColor: '#00e5ff',
borderWidth: 1
}]
},
options: {
responsive: true,
plugins: { legend: { display: false } },
scales: { y: { ticks: { color: '#a0aec0' } }, x: { ticks: { color: '#a0aec0' } } }
}
});
const hubsCtx = document.getElementById('hubsChart').getContext('2d');
new Chart(hubsCtx, {
type: 'doughnut',
data: {
labels: ['Bengaluru', 'Pune', 'Hyderabad', 'Gurgaon', 'Remote'],
datasets: [{
data: [40, 25, 20, 10, 5],
backgroundColor: ['#00e5ff', '#9d4edd', '#e94057', '#00ff9d', '#ffcc00']
}]
},
options: {
responsive: true,
plugins: { legend: { labels: { color: '#a0aec0' } } }
}
});
}
// Initialize
fetchJobs();
updateMcpInputs();
</script>
</body>
</html>