HR-ATS-Portal/js/aiassistant.js

219 lines
14 KiB
JavaScript

/* ============================================================
aiassistant.js — ChatGPT-style AI Recruiter Assistant (UI only)
+ AI Studio (future modules gallery). API-ready, no AI wired.
============================================================ */
window.Views = window.Views || {};
window.AI = {};
// simulated (non-AI) canned responses keyed by intent — clearly a UI stub
AI._reply = function (prompt) {
const p = prompt.toLowerCase();
if (p.includes('rank')) {
const top = [...DB.candidates].sort((a, b) => b.aiScore - a.aiScore).slice(0, 5);
return `<p>Here are the top-ranked candidates by ATS match score:</p><ul>${top.map((c, i) => `<li><b>${i + 1}. ${c.name}</b> — ${c.aiScore}% match · ${c.jobTitle} · ${c.recommendation}</li>`).join('')}</ul><p class="text-muted">This is a UI preview. Connect an LLM endpoint to generate live rankings from resume + JD embeddings.</p>`;
}
if (p.includes('compare')) {
const two = DB.candidates.slice(0, 2);
return `<p>Comparing <b>${two[0].name}</b> vs <b>${two[1].name}</b>:</p><ul><li><b>Experience:</b> ${two[0].experience}y vs ${two[1].experience}y</li><li><b>ATS Score:</b> ${two[0].aiScore}% vs ${two[1].aiScore}%</li><li><b>Recommendation:</b> ${two[0].recommendation} vs ${two[1].recommendation}</li></ul><p><b>Suggested:</b> ${two[0].aiScore >= two[1].aiScore ? two[0].name : two[1].name} appears stronger on core criteria.</p>`;
}
if (p.includes('job description') || p.includes('jd')) {
return `<p><b>Senior Product Designer</b></p><p>We're looking for a Senior Product Designer to craft intuitive, delightful experiences across our platform. You'll own end-to-end design, from research to polished UI, and partner closely with product and engineering.</p><p><b>Responsibilities:</b> lead design for key initiatives, run user research, build and maintain design systems, mentor peers.</p><p><b>Requirements:</b> 5+ years product design, strong portfolio, fluency in Figma, systems thinking.</p>`;
}
if (p.includes('interview question')) {
return `<p>Here are role-specific interview questions:</p><ul><li>Walk me through how you'd design a system to handle 1M concurrent users.</li><li>Describe a technically challenging project and the tradeoffs you made.</li><li>How do you approach debugging a production incident under time pressure?</li><li>Tell me about a time you disagreed with a teammate on an approach.</li></ul>`;
}
if (p.includes('summar')) {
const c = DB.candidates[0];
return `<p><b>Resume summary — ${c.name}</b></p><p>${c.experience} years of experience, currently ${c.currentTitle} at ${c.currentCompany}. Strong in ${c.skills.slice(0, 3).join(', ')}. ATS match ${c.aiScore}% for ${c.jobTitle}. ${c.recommendation}.</p>`;
}
if (p.includes('email')) {
return `<p><b>Subject:</b> Interview Invitation — Next Steps</p><p>Hi [Candidate],</p><p>Thank you for applying. We were impressed by your background and would love to invite you to an interview. Please share your availability for this week.</p><p>Best regards,<br>Talent Team</p>`;
}
if (p.includes('offer letter')) {
return `<p><b>Offer Letter</b></p><p>Dear [Candidate], We are pleased to offer you the position of Product Manager at a base salary of $160,000, plus equity and benefits. This offer is contingent on standard background checks.</p><p>We're excited about the possibility of you joining the team.</p>`;
}
if (p.includes('skill gap')) {
return `<p><b>Skill Gap Analysis — Engineering pipeline</b></p><ul><li><span class="skill-pill skill-missing">Kubernetes</span> under-represented (only 22% of pipeline)</li><li><span class="skill-pill skill-missing">System Design</span> gap at senior level</li><li><span class="skill-pill skill-matched">React</span> well covered</li></ul><p>Consider sourcing candidates with cloud-native infra experience.</p>`;
}
if (p.includes('pipeline')) {
const total = DB.candidates.length;
return `<p><b>Pipeline health analysis</b></p><ul><li>${total} active candidates across 6 stages</li><li>Conversion Applied → Interview: ~28%</li><li>Bottleneck detected at <b>Assessment</b> stage (longest dwell time)</li><li>Offer acceptance trending at 82%</li></ul><p>Recommendation: accelerate assessment turnaround to improve velocity.</p>`;
}
if (p.includes('productivity') || p.includes('recruiter')) {
return `<p><b>Team productivity this month</b></p><ul><li>Top performer: ${[...DB.recruiters].sort((a, b) => b.hires - a.hires)[0].name}</li><li>Avg time-to-hire: 27 days (3 days faster than last month)</li><li>Interview completion rate: 91%</li></ul>`;
}
if (p.includes('recommend') || p.includes('suggest')) {
const c = [...DB.candidates].sort((a, b) => b.aiScore - a.aiScore)[0];
return `<p><b>Top recommendation:</b> ${c.name} (${c.aiScore}% match) for ${c.jobTitle}. Strong on ${c.matchedSkills.slice(0, 2).join(' & ')}. I'd prioritise scheduling a screen this week.</p>`;
}
return `<p>I can help with ranking candidates, comparing profiles, drafting JDs, interview questions, emails, offer letters, skill-gap and pipeline analysis, and more.</p><p class="text-muted">This is a fully-designed interface. Wire an AI endpoint (Claude / OpenAI) into <code>AI.send()</code> to make responses live.</p>`;
};
AI._chatHtml = function (compact) {
const promptsHtml = DB.aiPrompts.slice(0, compact ? 6 : 12).map(p =>
`<button class="prompt-chip" onclick="AI.usePrompt(this, ${JSON.stringify(p.prompt).replace(/"/g, '&quot;')})">${UI.icon(p.icon)} ${p.text}</button>`).join('');
return `
<div class="chat-wrap" ${compact ? 'style="height:100%"' : ''}>
<div class="chat-scroll" id="chatScroll">
<div class="ai-hero">
<div class="ai-logo">${UI.icon('sparkles')}</div>
<h2 style="font-size:${compact ? '18' : '22'}px;margin-bottom:6px">AI Recruiter Assistant</h2>
<p class="text-muted">Ask anything about your candidates, jobs, and pipeline</p>
</div>
<div style="display:flex;flex-wrap:wrap;gap:8px;justify-content:center;max-width:720px;margin:0 auto 10px">${promptsHtml}</div>
</div>
<div style="padding-top:12px">
<div class="chat-input-bar">
<textarea id="chatInput" rows="1" placeholder="Message AI Assistant…"></textarea>
<button class="chat-send" id="chatSend">${UI.icon('arrow-right')}</button>
</div>
<p class="text-muted text-sm" style="text-align:center;margin-top:8px">UI preview · responses are simulated. ${UI.icon('lock')} API-ready for backend integration.</p>
</div>
</div>`;
};
AI._bindChat = function () {
const input = document.getElementById('chatInput');
const send = document.getElementById('chatSend');
if (!input) return;
input.oninput = () => { input.style.height = 'auto'; input.style.height = Math.min(input.scrollHeight, 140) + 'px'; };
input.onkeydown = e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); AI.send(); } };
send.onclick = AI.send;
AI._started = false;
};
AI.usePrompt = function (btn, prompt) {
const input = document.getElementById('chatInput');
input.value = prompt;
AI.send();
};
AI.send = function () {
const input = document.getElementById('chatInput');
const scroll = document.getElementById('chatScroll');
const text = input.value.trim();
if (!text) return;
if (!AI._started) { scroll.innerHTML = ''; AI._started = true; }
// user message
scroll.insertAdjacentHTML('beforeend', `
<div class="chat-msg"><div class="chat-av user">${UI.icon('users')}</div>
<div class="chat-bubble"><div class="chat-role">You</div><div class="chat-text">${text.replace(/</g, '&lt;')}</div></div></div>`);
input.value = ''; input.style.height = 'auto';
scroll.scrollTop = scroll.scrollHeight;
// typing indicator
const typingId = 'typing_' + Math.random().toString(36).slice(2, 7);
scroll.insertAdjacentHTML('beforeend', `
<div class="chat-msg" id="${typingId}"><div class="chat-av ai">${UI.icon('sparkles')}</div>
<div class="chat-bubble"><div class="chat-role">AI Assistant</div><div class="chat-typing"><span></span><span></span><span></span></div></div></div>`);
scroll.scrollTop = scroll.scrollHeight;
setTimeout(() => {
const t = document.getElementById(typingId);
if (t) t.querySelector('.chat-bubble').innerHTML = `<div class="chat-role">AI Assistant</div><div class="chat-text">${AI._reply(text)}</div>`;
scroll.scrollTop = scroll.scrollHeight;
}, 850 + Math.random() * 500);
};
// ---------------- Full page ----------------
Views.aiassistant = function () {
const html = `
<div class="page">
<div class="page-head">
<div><h1 class="page-title">AI Assistant</h1><p class="page-sub">Your recruiting copilot — powered by AI (interface preview)</p></div>
<div class="page-head-actions">
<span class="integration-status pending"><span class="pulse"></span>Model endpoint · Not connected</span>
<button class="btn btn-secondary" onclick="AI.newChat()">${UI.icon('plus')} New Chat</button>
</div>
</div>
<div class="card"><div class="card-body" id="aiChatMount">${AI._chatHtml(false)}</div></div>
</div>`;
return { html, onMount() { AI._bindChat(); } };
};
AI.newChat = function () {
const mount = document.getElementById('aiChatMount');
if (mount) { mount.innerHTML = AI._chatHtml(false); AI._bindChat(); }
else { AI._dockOpen(true); }
};
// ---------------- Floating dock ----------------
AI._dockOpen = function (force) {
const dock = document.getElementById('aiDock');
const inner = document.getElementById('aiDockInner');
const willOpen = force || !dock.classList.contains('open');
if (willOpen) {
inner.innerHTML = `
<div class="card-head" style="border-radius:0"><div><h3>${UI.icon('sparkles')} AI Assistant</h3></div>
<div class="flex items-center gap-8">
<button class="btn btn-ghost btn-sm" onclick="Router.go('aiassistant');AI._dockClose()">Expand</button>
<button class="modal-close" onclick="AI._dockClose()">${UI.icon('x')}</button></div></div>
<div style="flex:1;padding:16px;overflow:hidden;display:flex" id="dockChatMount">${AI._chatHtml(true)}</div>`;
dock.classList.add('open');
AI._bindChat();
} else { AI._dockClose(); }
};
AI._dockClose = function () { document.getElementById('aiDock').classList.remove('open'); };
// ---------------- AI Studio (future modules) ----------------
Views.aistudio = function () {
const cards = DB.aiModules.map(m => `
<div class="card" style="cursor:pointer" onclick="AI.moduleDetail('${m.name.replace(/'/g, "\\'")}')">
<div class="card-body">
<div class="flex items-center" style="justify-content:space-between;margin-bottom:12px">
<span class="kpi-icn ${m.cls}" style="width:46px;height:46px;border-radius:13px">${UI.icon(m.icon)}</span>
${UI.badge(m.status, m.status === 'Beta' ? 'b-indigo' : 'b-gray')}
</div>
<div class="lr-title" style="font-size:15px">${m.name}</div>
<div class="lr-sub" style="margin-top:5px;line-height:1.5">${m.desc}</div>
<div style="margin-top:14px;color:var(--primary);font-weight:600;font-size:13px">${m.status === 'Beta' ? 'Try it' : 'Join waitlist'} ${UI.icon('arrow-right')}</div>
</div>
</div>`).join('');
const html = `
<div class="page">
<div class="page-head">
<div><h1 class="page-title">AI Studio</h1><p class="page-sub">Next-generation AI modules — designed and API-ready for backend integration</p></div>
<div class="page-head-actions"><span class="integration-status pending"><span class="pulse"></span>${DB.aiModules.filter(m => m.status === 'Beta').length} in Beta</span>
<button class="btn btn-primary" onclick="AI._dockOpen(true)">${UI.icon('sparkles')} Open Assistant</button></div>
</div>
<div class="card brand-hero mb-18">
<div class="card-body" style="display:flex;align-items:center;gap:20px;flex-wrap:wrap">
<div class="ai-logo" style="margin:0;width:56px;height:56px">${UI.icon('sparkles')}</div>
<div style="flex:1;min-width:220px"><h2 style="font-size:19px;margin-bottom:4px">Everything is API-ready</h2>
<p style="opacity:.88">Each module below ships with a complete, production-grade interface. Connect your model endpoint to activate them — no UI work required.</p></div>
<button class="btn btn-on-brand" onclick="UI.toast('Integration guide opened','info')">${UI.icon('external')} Integration Guide</button>
</div>
</div>
<div class="grid g-3">${cards}</div>
</div>`;
return { html };
};
AI.moduleDetail = function (name) {
const m = DB.aiModules.find(x => x.name === name);
UI.modal({
title: m.name, subtitle: m.status + ' · AI Module',
body: `<div class="flex items-center gap-16" style="margin-bottom:18px"><span class="kpi-icn ${m.cls}" style="width:56px;height:56px;border-radius:16px">${UI.icon(m.icon)}</span>
<div><div class="fw-600" style="font-size:16px">${m.name}</div><div class="text-muted">${m.desc}</div></div></div>
<div class="card" style="box-shadow:none;background:var(--bg-sunken)"><div class="card-body">
<div class="form-section-title" style="margin-top:0">API Contract (preview)</div>
<div class="resume-thumb" style="max-height:none">POST /api/ai/${m.name.toLowerCase().replace(/ /g, '-')}
{
"context": { "jobId": "JOB-1001", "candidateIds": [...] },
"options": { "model": "claude-opus", "stream": true }
}
→ 200 OK
{
"result": { ... },
"usage": { "tokens": 1240 }
}</div>
</div></div>
<p class="text-muted text-sm" style="margin-top:14px">${UI.icon('lock')} This feature's UI is complete. Backend wiring is the only remaining step.</p>`,
footer: `<button class="btn btn-secondary" onclick="UI.closeModal()">Close</button>
<button class="btn btn-primary" onclick="UI.closeModal();AI._dockOpen(true)">${UI.icon('sparkles')} Try in Assistant</button>`,
size: 'modal-lg'
});
};