/**
* Memora — Intelligent Career & Memory Engine
* Client Application Logic
*/
// ============================================================================
// State Management
// ============================================================================
const state = {
userId: localStorage.getItem("memora_user_id") || `user-${Math.random().toString(36).substring(2, 10)}`,
conversationId: localStorage.getItem("memora_conv_id") || null,
isRecording: false,
mediaRecorder: null,
audioChunks: [],
selectedImageFile: null,
recordingStartTime: null,
recordingTimerInterval: null,
memories: [],
activeMemoryFilter: "all",
resumes: [],
activeResumeId: null,
jobs: [],
dueJobs: [],
activeJobFilter: "all",
activeJobSort: "newest",
lastMatchResult: null,
lastAtsResult: null,
suggestedEdits: [],
lastInsightsResult: null,
outreachParsedJD: null,
outreachSelectedRole: null,
outreachDraft: null,
gmailConnected: false
};
// Persist user ID immediately
localStorage.setItem("memora_user_id", state.userId);
// ============================================================================
// DOM Element Cache
// ============================================================================
const dom = {
// Sidebar & Navigation
userIdDisplay: document.getElementById("user-id-display"),
convIdDisplay: document.getElementById("conv-id-display"),
btnNewChat: document.getElementById("btn-new-chat"),
btnSwitchUser: document.getElementById("btn-switch-user"),
serverStatus: document.getElementById("server-status"),
navItems: document.querySelectorAll(".nav-item"),
tabPanes: document.querySelectorAll(".tab-pane"),
currentTabTitle: document.getElementById("current-tab-title"),
currentTabSubtitle: document.getElementById("current-tab-subtitle"),
memoryCountBadge: document.getElementById("memory-count-badge"),
jobsDueBadge: document.getElementById("jobs-due-badge"),
// Chat
chatMessages: document.getElementById("chat-messages"),
chatForm: document.getElementById("chat-form"),
chatInput: document.getElementById("chat-input"),
chatSuggestions: document.getElementById("chat-suggestion-chips"),
btnVoiceRecord: document.getElementById("btn-voice-record"),
voiceRecordingBar: document.getElementById("voice-recording-bar"),
recordingTimer: document.getElementById("recording-timer"),
btnStopVoice: document.getElementById("btn-stop-voice"),
btnAttachImage: document.getElementById("btn-attach-image"),
imageFileInput: document.getElementById("image-file-input"),
mediaPreview: document.getElementById("media-preview"),
previewContent: document.getElementById("preview-content"),
btnCancelMedia: document.getElementById("btn-cancel-media"),
// Memory Vault
memoriesGrid: document.getElementById("memories-grid"),
btnRefreshMemories: document.getElementById("btn-refresh-memories"),
memorySearchInput: document.getElementById("memory-search-input"),
memoryFilterPills: document.getElementById("memory-filter-pills"),
countAllMemories: document.getElementById("count-all-memories"),
countSemanticMemories: document.getElementById("count-semantic-memories"),
countEpisodicMemories: document.getElementById("count-episodic-memories"),
// Matcher
matcherJobUrl: document.getElementById("matcher-job-url"),
btnFetchJob: document.getElementById("btn-fetch-job"),
fetchBtnText: document.getElementById("fetch-btn-text"),
matcherBotWarning: document.getElementById("matcher-bot-warning"),
matcherCompany: document.getElementById("matcher-company"),
matcherRole: document.getElementById("matcher-role"),
matcherJdText: document.getElementById("matcher-jd-text"),
resumeSelectPicker: document.getElementById("resume-select-picker"),
btnDeleteActiveResume: document.getElementById("btn-delete-active-resume"),
resumeDropzone: document.getElementById("resume-dropzone"),
resumeFileInput: document.getElementById("resume-file-input"),
resumeTitleInput: document.getElementById("resume-title-input"),
matcherResumeText: document.getElementById("matcher-resume-text"),
btnSaveResume: document.getElementById("btn-save-resume"),
btnRunMatch: document.getElementById("btn-run-match"),
matcherEmptyState: document.getElementById("matcher-empty-state"),
matcherLoadingState: document.getElementById("matcher-loading-state"),
matcherResultsView: document.getElementById("matcher-results-view"),
matchScoreVal: document.getElementById("match-score-val"),
gaugeCircle: document.getElementById("gauge-circle"),
matchVerdictPill: document.getElementById("match-verdict-pill"),
matchTargetRole: document.getElementById("match-target-role"),
matchTargetCompany: document.getElementById("match-target-company"),
matchSummaryText: document.getElementById("match-summary-text"),
matchStrengthsList: document.getElementById("match-strengths-list"),
matchExperienceFit: document.getElementById("match-experience-fit"),
skillsMatchedTags: document.getElementById("skills-matched-tags"),
skillsMissingTags: document.getElementById("skills-missing-tags"),
matchRecommendationsList: document.getElementById("match-recommendations-list"),
matchPitchText: document.getElementById("match-pitch-text"),
btnCopyPitch: document.getElementById("btn-copy-pitch"),
btnAddMatchedToTracker: document.getElementById("btn-add-matched-to-tracker"),
// Suggested Resume Edits
btnGenerateEdits: document.getElementById("btn-generate-edits"),
btnGenerateEditsText: document.getElementById("btn-generate-edits-text"),
editsLoadingState: document.getElementById("edits-loading-state"),
editsEmptyState: document.getElementById("edits-empty-state"),
editsListContainer: document.getElementById("edits-list-container"),
// ATS Checker
atsResumeSelectPicker: document.getElementById("ats-resume-select-picker"),
atsResumeDropzone: document.getElementById("ats-resume-dropzone"),
atsResumeFileInput: document.getElementById("ats-resume-file-input"),
atsResumeText: document.getElementById("ats-resume-text"),
btnRunAtsCheck: document.getElementById("btn-run-ats-check"),
atsEmptyState: document.getElementById("ats-empty-state"),
atsLoadingState: document.getElementById("ats-loading-state"),
atsResultsView: document.getElementById("ats-results-view"),
atsScoreVal: document.getElementById("ats-score-val"),
atsGaugeCircle: document.getElementById("ats-gauge-circle"),
atsVerdictPill: document.getElementById("ats-verdict-pill"),
atsL1Val: document.getElementById("ats-l1-val"),
atsL2Val: document.getElementById("ats-l2-val"),
atsRuleChecksList: document.getElementById("ats-rule-checks-list"),
atsMlChecksList: document.getElementById("ats-ml-checks-list"),
atsRecommendationsList: document.getElementById("ats-recommendations-list"),
// Authenticity Checker
authJobUrl: document.getElementById("auth-job-url"),
btnAuthFetchJob: document.getElementById("btn-auth-fetch-job"),
authFetchBtnText: document.getElementById("auth-fetch-btn-text"),
authBotWarning: document.getElementById("auth-bot-warning"),
authCompanyInput: document.getElementById("auth-company-input"),
authRoleInput: document.getElementById("auth-role-input"),
authJobText: document.getElementById("auth-job-text"),
btnRunAuthenticity: document.getElementById("btn-run-authenticity"),
authBtnLabel: document.getElementById("auth-btn-label"),
btnAuthPresetSafe: document.getElementById("btn-auth-preset-safe"),
btnAuthPresetScam: document.getElementById("btn-auth-preset-scam"),
btnMatcherCheckAuth: document.getElementById("btn-matcher-check-auth"),
authEmptyState: document.getElementById("auth-empty-state"),
authLoadingState: document.getElementById("auth-loading-state"),
authLoadingTitle: document.getElementById("auth-loading-title"),
authLoadingSubtitle: document.getElementById("auth-loading-subtitle"),
authResultsView: document.getElementById("auth-results-view"),
authRiskHero: document.getElementById("auth-risk-hero"),
authRiskPill: document.getElementById("auth-risk-pill"),
authRiskScore: document.getElementById("auth-risk-score"),
authRiskSummary: document.getElementById("auth-risk-summary"),
authL1Counts: document.getElementById("auth-l1-counts"),
authLayer1List: document.getElementById("auth-layer1-list"),
authLayer2Content: document.getElementById("auth-layer2-content"),
authLayer3Content: document.getElementById("auth-layer3-content"),
// Company Insights
insightCompanyInput: document.getElementById("insight-company-input"),
insightUrlInput: document.getElementById("insight-url-input"),
insightRoleInput: document.getElementById("insight-role-input"),
insightAboutInput: document.getElementById("insight-about-input"),
btnRunInsights: document.getElementById("btn-run-insights"),
insightBtnLabel: document.getElementById("insight-btn-label"),
btnInsightPresetStripe: document.getElementById("btn-insight-preset-stripe"),
btnInsightPresetStartup: document.getElementById("btn-insight-preset-startup"),
btnInsightPresetConflict: document.getElementById("btn-insight-preset-conflict"),
btnMatcherInspectInsights: document.getElementById("btn-matcher-inspect-insights"),
insightEmptyState: document.getElementById("insight-empty-state"),
insightLoadingState: document.getElementById("insight-loading-state"),
insightResultsView: document.getElementById("insight-results-view"),
insightUrlMismatchAlert: document.getElementById("insight-url-mismatch-alert"),
insightUrlMismatchText: document.getElementById("insight-url-mismatch-text"),
insightLowDataAlert: document.getElementById("insight-low-data-alert"),
insightLowDataText: document.getElementById("insight-low-data-text"),
insightCompanyTitle: document.getElementById("insight-company-title"),
insightIndustryBadge: document.getElementById("insight-industry-badge"),
insightSizeLabel: document.getElementById("insight-size-label"),
insightSizeConfidence: document.getElementById("insight-size-confidence"),
insightDisagreementAlert: document.getElementById("insight-disagreement-alert"),
insightAllScoresList: document.getElementById("insight-all-scores-list"),
insightEmployeesVal: document.getElementById("insight-employees-val"),
insightFoundedVal: document.getElementById("insight-founded-val"),
insightSentimentSummary: document.getElementById("insight-sentiment-summary"),
insightSentimentPosBar: document.getElementById("insight-sentiment-pos-bar"),
insightSentimentNegBar: document.getElementById("insight-sentiment-neg-bar"),
insightSentimentBarTrack: document.getElementById("insight-sentiment-bar-track"),
insightSignalStrengthNote: document.getElementById("insight-signal-strength-note"),
insightPraisedList: document.getElementById("insight-praised-list"),
insightCriticizedList: document.getElementById("insight-criticized-list"),
insightFocusAreasTags: document.getElementById("insight-focus-areas-tags"),
insightProcessStagesList: document.getElementById("insight-process-stages-list"),
insightPrepTipsList: document.getElementById("insight-prep-tips-list"),
// Jobs Tracker
jobsAlertBanner: document.getElementById("jobs-alert-banner"),
jobsAlertText: document.getElementById("jobs-alert-text"),
jobsList: document.getElementById("jobs-list"),
jobSearchInput: document.getElementById("job-search-input"),
jobStatusFilter: document.getElementById("job-status-filter"),
jobSortSelect: document.getElementById("job-sort-select"),
btnOpenJobModal: document.getElementById("btn-open-job-modal"),
jobModal: document.getElementById("job-modal"),
btnCloseModal: document.getElementById("btn-close-modal"),
btnCancelModal: document.getElementById("btn-cancel-modal"),
jobForm: document.getElementById("job-form"),
// Stats
statTotalJobs: document.getElementById("stat-total-jobs"),
statActiveJobs: document.getElementById("stat-active-jobs"),
statOfferJobs: document.getElementById("stat-offer-jobs"),
statRejectedJobs: document.getElementById("stat-rejected-jobs"),
statDueJobs: document.getElementById("stat-due-jobs"),
// Modals
profileModal: document.getElementById("profile-modal"),
btnCloseProfileModal: document.getElementById("btn-close-profile-modal"),
profileCurrentId: document.getElementById("profile-current-id"),
btnCopyUserId: document.getElementById("btn-copy-user-id"),
profileSwitchId: document.getElementById("profile-switch-id"),
btnApplySwitchUser: document.getElementById("btn-apply-switch-user"),
btnCreateNewUser: document.getElementById("btn-create-new-user"),
editMemoryModal: document.getElementById("edit-memory-modal"),
btnCloseEditMemory: document.getElementById("btn-close-edit-memory"),
btnCancelEditMemory: document.getElementById("btn-cancel-edit-memory"),
editMemoryForm: document.getElementById("edit-memory-form"),
editMemoryId: document.getElementById("edit-memory-id"),
editMemoryContent: document.getElementById("edit-memory-content"),
editMemoryImportance: document.getElementById("edit-memory-importance"),
btnOpenSettings: document.getElementById("btn-open-settings"),
settingsModal: document.getElementById("settings-modal"),
btnCloseSettings: document.getElementById("btn-close-settings"),
btnCancelSettings: document.getElementById("btn-cancel-settings"),
settingsForm: document.getElementById("settings-form"),
settingsLlmProvider: document.getElementById("settings-llm-provider"),
settingsGroqKey: document.getElementById("settings-groq-key"),
settingsAnthropicKey: document.getElementById("settings-anthropic-key"),
groqKeyGroup: document.getElementById("groq-key-group"),
anthropicKeyGroup: document.getElementById("anthropic-key-group"),
// Apply via Email (Outreach)
outreachHeaderAuthPill: document.getElementById("outreach-header-auth-pill"),
outreachAuthIndicatorDot: document.getElementById("outreach-auth-indicator-dot"),
outreachHeaderAuthText: document.getElementById("outreach-header-auth-text"),
outreachStep1Indicator: document.getElementById("outreach-step1-indicator"),
outreachStep2Indicator: document.getElementById("outreach-step2-indicator"),
outreachStep3Indicator: document.getElementById("outreach-step3-indicator"),
outreachStep4Indicator: document.getElementById("outreach-step4-indicator"),
btnOutreachPresetRayo: document.getElementById("btn-outreach-preset-rayo"),
outreachResumeSelectPicker: document.getElementById("outreach-resume-select-picker"),
outreachUrlInput: document.getElementById("outreach-url-input"),
btnOutreachFetchUrl: document.getElementById("btn-outreach-fetch-url"),
outreachJdText: document.getElementById("outreach-jd-text"),
btnParseOutreach: document.getElementById("btn-parse-outreach"),
outreachStep2Panel: document.getElementById("outreach-step2-panel"),
outreachStep2Empty: document.getElementById("outreach-step2-empty"),
outreachStep2Content: document.getElementById("outreach-step2-content"),
outreachFieldCompany: document.getElementById("outreach-field-company"),
outreachFieldEmail: document.getElementById("outreach-field-email"),
outreachEmailMissingNote: document.getElementById("outreach-email-missing-note"),
outreachFieldPhone: document.getElementById("outreach-field-phone"),
outreachFieldLocation: document.getElementById("outreach-field-location"),
outreachFieldExp: document.getElementById("outreach-field-exp"),
outreachFieldDays: document.getElementById("outreach-field-days"),
outreachRoleRecommendationBox: document.getElementById("outreach-role-recommendation-box"),
outreachRoleRecommendationText: document.getElementById("outreach-role-recommendation-text"),
outreachRoleOptionsContainer: document.getElementById("outreach-role-options-container"),
outreachApplicantName: document.getElementById("outreach-applicant-name"),
outreachCustomInstructions: document.getElementById("outreach-custom-instructions"),
btnDraftOutreach: document.getElementById("btn-draft-outreach"),
outreachStep3Empty: document.getElementById("outreach-step3-empty"),
outreachStep3Loading: document.getElementById("outreach-step3-loading"),
outreachStep3Content: document.getElementById("outreach-step3-content"),
outreachEmailSubject: document.getElementById("outreach-email-subject"),
outreachEmailBody: document.getElementById("outreach-email-body"),
outreachClicheStatusTag: document.getElementById("outreach-cliche-status-tag"),
outreachFabricationAuditBox: document.getElementById("outreach-fabrication-audit-box"),
outreachFabricationSummaryTag: document.getElementById("outreach-fabrication-summary-tag"),
outreachFabricationFlagsList: document.getElementById("outreach-fabrication-flags-list"),
outreachAiDetectorBox: document.getElementById("outreach-ai-detector-box"),
outreachDetectorScoreText: document.getElementById("outreach-detector-score-text"),
outreachEmailRecipientConfirm: document.getElementById("outreach-email-recipient-confirm"),
outreachAuthCardDot: document.getElementById("outreach-auth-card-dot"),
outreachAuthCardStatus: document.getElementById("outreach-auth-card-status"),
outreachAuthCardDesc: document.getElementById("outreach-auth-card-desc"),
btnOutreachConnectGmail: document.getElementById("btn-outreach-connect-gmail"),
btnOutreachDisconnectGmail: document.getElementById("btn-outreach-disconnect-gmail"),
outreachAttachResumeCheckbox: document.getElementById("outreach-attach-resume-checkbox"),
outreachLogTrackerCheckbox: document.getElementById("outreach-log-tracker-checkbox"),
btnSendOutreach: document.getElementById("btn-send-outreach"),
outreachSendResultCard: document.getElementById("outreach-send-result-card"),
outreachResultMsgId: document.getElementById("outreach-result-msg-id"),
outreachResultTimestamp: document.getElementById("outreach-result-timestamp"),
outreachResultRecipient: document.getElementById("outreach-result-recipient"),
btnOutreachGotoTracker: document.getElementById("btn-outreach-goto-tracker"),
btnMatcherApplyEmail: document.getElementById("btn-matcher-apply-email"),
// Toast
toastContainer: document.getElementById("toast-container")
};
// ============================================================================
// Initialization
// ============================================================================
document.addEventListener("DOMContentLoaded", () => {
renderUserInfo();
setupNavigation();
setupProfileModal();
setupChatHandlers();
setupVoiceAndMedia();
setupMemoryVaultHandlers();
setupMatcherHandlers();
setupSuggestedEditsHandlers();
setupAtsCheckerHandlers();
setupAuthenticityHandlers();
setupCompanyInsightsHandlers();
setupOutreachHandlers();
setupJobHandlers();
setupSettingsHandlers();
checkHealth();
// Load initial data
loadMemories();
loadResumes();
loadJobs();
loadSettings();
checkGmailStatus();
// Poll health and due follow-ups periodically
setInterval(checkHealth, 30000);
});
function renderUserInfo() {
if (dom.userIdDisplay) dom.userIdDisplay.textContent = state.userId;
if (dom.convIdDisplay) dom.convIdDisplay.textContent = state.conversationId ? state.conversationId.substring(0, 8) + '...' : "(new)";
if (dom.profileCurrentId) dom.profileCurrentId.value = state.userId;
const sidebarUserId = document.getElementById("sidebar-user-id");
if (sidebarUserId) sidebarUserId.textContent = state.userId.substring(0, 10);
}
// ============================================================================
// Navigation & Tabs
// ============================================================================
function setupNavigation() {
dom.navItems.forEach(item => {
item.addEventListener("click", () => {
const tabId = item.getAttribute("data-tab");
switchTab(tabId);
});
});
if (dom.btnNewChat) {
dom.btnNewChat.addEventListener("click", () => {
state.conversationId = null;
localStorage.removeItem("memora_conv_id");
renderUserInfo();
resetChatView();
showToast("Started new terminal session", "info");
});
}
}
function switchTab(tabId) {
dom.navItems.forEach(nav => {
const isActive = nav.getAttribute("data-tab") === tabId;
nav.classList.toggle("active", isActive);
});
dom.tabPanes.forEach(pane => {
pane.classList.toggle("active", pane.id === tabId);
});
const tabTitles = {
"tab-chat": { title: "TERMINAL_SESSION", subtitle: "// System initialized. Active memory extraction online." },
"tab-memory": { title: "MEMORY_VAULT_SYS", subtitle: "// Persistent structured extractions. Synthesized knowledge base." },
"tab-matcher": { title: "RESUME_MATCHER_SYS", subtitle: "// Deep comparative technical fit & outreach composition" },
"tab-ats": { title: "ATS_PARSEABILITY_AUDIT", subtitle: "// Rule-based structure validation + HuggingFace BERT entity signals" },
"tab-authenticity": { title: "JOB_AUTHENTICITY_AUDIT", subtitle: "// Hybrid 3-Layer fraud risk detection & RAG reasoning engine" },
"tab-insights": { title: "COMPANY_INSIGHTS_SYS", subtitle: "// Multi-layer size classification, industry tagging & culture sentiment" },
"tab-outreach": { title: "APPLY_VIA_EMAIL_SYS", subtitle: "// Informal JD extraction, resume-grounded drafting, fabrication audit & Gmail sender" },
"tab-jobs": { title: "JOB_TRACKER_SYS", subtitle: "// Monitoring active engagements & follow-up timelines" },
"tab-docs": { title: "API_SWAGGER_EXPLORER", subtitle: "// FastAPI OpenAPI interactive documentation" }
};
if (tabTitles[tabId]) {
if (dom.currentTabTitle) dom.currentTabTitle.textContent = tabTitles[tabId].title;
if (dom.currentTabSubtitle) dom.currentTabSubtitle.textContent = tabTitles[tabId].subtitle;
}
if (tabId === "tab-memory") {
loadMemories();
} else if (tabId === "tab-jobs") {
loadJobs();
} else if (tabId === "tab-matcher" || tabId === "tab-ats" || tabId === "tab-outreach") {
loadResumes();
if (tabId === "tab-outreach") {
checkGmailStatus();
}
}
}
// ============================================================================
// User Profile & Session Modal
// ============================================================================
function setupProfileModal() {
if (dom.btnSwitchUser) {
dom.btnSwitchUser.addEventListener("click", () => {
dom.profileCurrentId.value = state.userId;
dom.profileModal.classList.remove("hidden");
});
}
if (dom.btnCloseProfileModal) {
dom.btnCloseProfileModal.addEventListener("click", () => {
dom.profileModal.classList.add("hidden");
});
}
if (dom.btnCopyUserId) {
dom.btnCopyUserId.addEventListener("click", () => {
navigator.clipboard.writeText(state.userId);
showToast("User ID copied to clipboard!", "success");
});
}
if (dom.btnApplySwitchUser) {
dom.btnApplySwitchUser.addEventListener("click", () => {
const newId = dom.profileSwitchId.value.trim();
if (!newId) {
showToast("Please enter a valid user ID", "error");
return;
}
state.userId = newId;
state.conversationId = null;
localStorage.setItem("memora_user_id", newId);
localStorage.removeItem("memora_conv_id");
dom.profileModal.classList.add("hidden");
renderUserInfo();
resetChatView();
loadMemories();
loadResumes();
loadJobs();
showToast(`Switched profile to ${newId}`, "success");
});
}
if (dom.btnCreateNewUser) {
dom.btnCreateNewUser.addEventListener("click", () => {
const newId = `user-${Math.random().toString(36).substring(2, 10)}`;
state.userId = newId;
state.conversationId = null;
localStorage.setItem("memora_user_id", newId);
localStorage.removeItem("memora_conv_id");
dom.profileModal.classList.add("hidden");
renderUserInfo();
resetChatView();
loadMemories();
loadResumes();
loadJobs();
showToast(`Created new profile ${newId}`, "success");
});
}
}
// ============================================================================
// AI Assistant & Dynamic Chips
// ============================================================================
function setupChatHandlers() {
dom.chatForm.addEventListener("submit", async (e) => {
e.preventDefault();
const text = dom.chatInput.value.trim();
if (!text && !state.selectedImageFile) return;
let messageToSend = text;
dom.chatInput.value = "";
// If an image was attached, caption it first
if (state.selectedImageFile) {
const caption = await handleImageCaptioning(state.selectedImageFile);
if (caption) {
messageToSend = messageToSend
? `${messageToSend}\n[Attached image: "${caption}"]`
: `[Attached image: "${caption}"]`;
}
clearMediaPreview();
}
if (!messageToSend.trim()) return;
appendMessage("user", messageToSend);
const typingId = showTypingIndicator();
try {
const resp = await fetch("/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
user_external_id: state.userId,
conversation_id: state.conversationId,
message: messageToSend
})
});
removeTypingIndicator(typingId);
if (!resp.ok) {
const errorData = await resp.json().catch(() => ({}));
throw new Error(errorData.detail || `Server returned ${resp.status}`);
}
const data = await resp.json();
state.conversationId = data.conversation_id;
localStorage.setItem("memora_conv_id", data.conversation_id);
renderUserInfo();
appendMessage("assistant", data.reply);
// Auto-refresh memories in background after chat turn
setTimeout(loadMemories, 2500);
} catch (err) {
removeTypingIndicator(typingId);
appendMessage("assistant", `⚠️ Error: ${err.message}. You can configure your Groq API key in Settings.`);
showToast(err.message, "error");
}
});
}
function useSuggestion(text) {
dom.chatInput.value = text;
dom.chatInput.focus();
}
window.useSuggestion = useSuggestion;
function updateDynamicSuggestionChips(memories) {
if (!dom.chatSuggestions) return;
if (!memories || memories.length === 0) {
dom.chatSuggestions.innerHTML = `
💼 Set career profile
🦀 Set preferences
🔍 Test memory retrieval
`;
return;
}
const chips = [
`🎯 Suggest matching roles `,
`🧠 What do you know about me? `,
`📝 Draft personal pitch `
];
dom.chatSuggestions.innerHTML = chips.join("");
}
function appendMessage(role, content) {
const msgEl = document.createElement("div");
msgEl.className = `w-full my-4 flex ${role === 'user' ? 'justify-end' : 'justify-start'}`;
const now = new Date();
const timeStr = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
if (role === "user") {
msgEl.innerHTML = `
person
OPERATOR
${timeStr}
${escapeHtml(content).replace(/\n/g, ' ')}
`;
} else {
msgEl.innerHTML = `
smart_toy
malloc()_core
${timeStr}
${escapeHtml(content).replace(/\n/g, ' ')}
`;
}
dom.chatMessages.appendChild(msgEl);
dom.chatMessages.scrollTop = dom.chatMessages.scrollHeight;
}
function showTypingIndicator() {
const id = `typing-${Date.now()}`;
const el = document.createElement("div");
el.id = id;
el.className = "w-full my-4 flex justify-start";
el.innerHTML = `
smart_toy
malloc()_core
Processing...
> Querying Memory Vault & Processing Input...
`;
dom.chatMessages.appendChild(el);
dom.chatMessages.scrollTop = dom.chatMessages.scrollHeight;
return id;
}
function removeTypingIndicator(id) {
const el = document.getElementById(id);
if (el) el.remove();
}
function resetChatView() {
dom.chatMessages.innerHTML = `
smart_toy
malloc() Assistant
Continuous Memory
Hello! I'm your AI career partner with persistent long-term memory. Talk with me, record voice messages, or upload documents. Everything you share is securely retained and indexed.
Suggested Quick Prompts
💼 Set career profile
🦀 Set preferences
🔍 Test memory retrieval
`;
}
// ============================================================================
// Voice & Media Handlers
// ============================================================================
function setupVoiceAndMedia() {
dom.btnVoiceRecord.addEventListener("click", toggleVoiceRecording);
dom.btnStopVoice.addEventListener("click", toggleVoiceRecording);
dom.btnAttachImage.addEventListener("click", () => dom.imageFileInput.click());
dom.imageFileInput.addEventListener("change", (e) => {
if (e.target.files.length > 0) {
handleImageSelected(e.target.files[0]);
}
});
dom.btnCancelMedia.addEventListener("click", clearMediaPreview);
}
async function toggleVoiceRecording() {
if (state.isRecording) {
// Stop recording
state.isRecording = false;
dom.btnVoiceRecord.classList.remove("recording");
dom.voiceRecordingBar.classList.add("hidden");
clearInterval(state.recordingTimerInterval);
if (state.mediaRecorder && state.mediaRecorder.state !== "inactive") {
state.mediaRecorder.stop();
}
} else {
// Start recording
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
state.audioChunks = [];
state.mediaRecorder = new MediaRecorder(stream);
state.mediaRecorder.ondataavailable = (e) => {
if (e.data.size > 0) state.audioChunks.push(e.data);
};
state.mediaRecorder.onstop = async () => {
const audioBlob = new Blob(state.audioChunks, { type: "audio/wav" });
stream.getTracks().forEach(t => t.stop());
await processAudioTranscription(audioBlob);
};
state.mediaRecorder.start();
state.isRecording = true;
dom.btnVoiceRecord.classList.add("recording");
dom.voiceRecordingBar.classList.remove("hidden");
state.recordingStartTime = Date.now();
dom.recordingTimer.textContent = "0:00";
state.recordingTimerInterval = setInterval(() => {
const sec = Math.floor((Date.now() - state.recordingStartTime) / 1000);
const mins = Math.floor(sec / 60);
const remSec = sec % 60;
dom.recordingTimer.textContent = `${mins}:${remSec < 10 ? '0' : ''}${remSec}`;
}, 1000);
showToast("Recording audio... Speak now!", "info");
} catch (err) {
showToast(`Microphone access error: ${err.message}`, "error");
}
}
}
async function processAudioTranscription(blob) {
showToast("Transcribing voice with Whisper...", "info");
const formData = new FormData();
formData.append("file", blob, "recording.wav");
try {
const resp = await fetch("/media/transcribe", { method: "POST", body: formData });
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.detail || "Transcription failed");
}
const data = await resp.json();
if (data.text) {
dom.chatInput.value = data.text;
dom.chatInput.focus();
showToast("Voice transcribed successfully!", "success");
}
} catch (err) {
showToast(`Whisper transcription error: ${err.message}`, "error");
}
}
function handleImageSelected(file) {
state.selectedImageFile = file;
dom.previewContent.innerHTML = `
🖼️ Image: ${escapeHtml(file.name)} (${(file.size / 1024).toFixed(1)} KB)
`;
dom.mediaPreview.classList.remove("hidden");
}
function clearMediaPreview() {
state.selectedImageFile = null;
dom.imageFileInput.value = "";
dom.mediaPreview.classList.add("hidden");
dom.previewContent.innerHTML = "";
}
async function handleImageCaptioning(file) {
showToast("Analyzing image with BLIP...", "info");
const formData = new FormData();
formData.append("file", file);
try {
const resp = await fetch("/media/caption", { method: "POST", body: formData });
if (!resp.ok) {
return `Image file: ${file.name}`;
}
const data = await resp.json();
return data.caption || `Image file: ${file.name}`;
} catch {
return `Image file: ${file.name}`;
}
}
// ============================================================================
// Memory Vault & Management
// ============================================================================
function setupMemoryVaultHandlers() {
dom.btnRefreshMemories.addEventListener("click", () => {
loadMemories();
showToast("Memory Vault synced!", "info");
});
dom.memorySearchInput.addEventListener("input", filterAndRenderMemories);
if (dom.memoryFilterPills) {
dom.memoryFilterPills.addEventListener("click", (e) => {
const pill = e.target.closest(".pill");
if (!pill) return;
dom.memoryFilterPills.querySelectorAll(".pill").forEach(p => p.classList.remove("active"));
pill.classList.add("active");
state.activeMemoryFilter = pill.getAttribute("data-filter");
filterAndRenderMemories();
});
}
// Edit Memory Modal Handlers
if (dom.btnCloseEditMemory) {
dom.btnCloseEditMemory.addEventListener("click", () => dom.editMemoryModal.classList.add("hidden"));
}
if (dom.btnCancelEditMemory) {
dom.btnCancelEditMemory.addEventListener("click", () => dom.editMemoryModal.classList.add("hidden"));
}
if (dom.editMemoryForm) {
dom.editMemoryForm.addEventListener("submit", async (e) => {
e.preventDefault();
const id = dom.editMemoryId.value;
const content = dom.editMemoryContent.value.trim();
const importance = parseFloat(dom.editMemoryImportance.value) || 0.5;
try {
const resp = await fetch(`/memories/${id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content, importance })
});
if (!resp.ok) throw new Error("Failed to update memory");
dom.editMemoryModal.classList.add("hidden");
showToast("Memory updated successfully!", "success");
loadMemories();
} catch (err) {
showToast(err.message, "error");
}
});
}
}
async function loadMemories() {
try {
const resp = await fetch(`/memories/${state.userId}`);
if (!resp.ok) return;
const data = await resp.json();
state.memories = data;
if (dom.memoryCountBadge) dom.memoryCountBadge.textContent = data.length;
const sidebarVaultCount = document.getElementById("sidebar-vault-count");
if (sidebarVaultCount) sidebarVaultCount.textContent = data.length;
const semantic = data.filter(m => m.memory_type === "semantic").length;
const episodic = data.filter(m => m.memory_type === "episodic").length;
if (dom.countAllMemories) dom.countAllMemories.textContent = data.length;
if (dom.countSemanticMemories) dom.countSemanticMemories.textContent = semantic;
if (dom.countEpisodicMemories) dom.countEpisodicMemories.textContent = episodic;
filterAndRenderMemories();
updateDynamicSuggestionChips(data);
} catch (err) {
console.error("Failed to load memories:", err);
}
}
function filterAndRenderMemories() {
const query = (dom.memorySearchInput ? dom.memorySearchInput.value : "").toLowerCase().trim();
const filter = state.activeMemoryFilter;
let list = state.memories;
if (filter !== "all") {
list = list.filter(m => m.memory_type === filter);
}
if (query) {
list = list.filter(m => m.content.toLowerCase().includes(query));
}
if (!list.length) {
dom.memoriesGrid.innerHTML = `
psychology
${state.memories.length === 0 ? "NO_MEMORIES_EXTRACTED_YET" : "NO_MATCHING_RECORDS"}
${state.memories.length === 0 ? "The Memory Vault is currently empty. As you interact with the AI Assistant or upload resumes, structured entities will be automatically indexed here." : "Try adjusting your search query or filter tags."}
terminal
INITIATE_CONVERSATION
memory
SYSTEM_STATUS
EXTRACTION_ENGINE
ONLINE
VECTOR_INDEX
${state.memories.length}_ENTITIES
SYNC_LATENCY
12ms
schema
EXPECTED_SCHEMA
{
"entity_type": "Skill",
"value": "Python, FastAPI",
"confidence": 0.98
}
`;
return;
}
dom.memoriesGrid.innerHTML = list.map(m => `
${escapeHtml(m.content)}
Importance: ${Math.round((m.importance || 0.5) * 100)}%
${(m.created_at || '').substring(0, 10)}
`).join("");
}
window.openEditMemoryModal = function(id) {
const memory = state.memories.find(m => m.id === id);
if (!memory) return;
dom.editMemoryId.value = memory.id;
dom.editMemoryContent.value = memory.content;
dom.editMemoryImportance.value = memory.importance || 0.5;
dom.editMemoryModal.classList.remove("hidden");
};
window.deleteMemory = async function(id) {
if (!confirm("Are you sure you want to remove this memory fact?")) return;
try {
const resp = await fetch(`/memories/${id}`, { method: "DELETE" });
if (!resp.ok) throw new Error("Failed to delete memory");
showToast("Memory removed", "info");
loadMemories();
} catch (err) {
showToast(err.message, "error");
}
};
// ============================================================================
// Multi-Resume Management & Job Matcher
// ============================================================================
function setupMatcherHandlers() {
// 1. Fetch Job from URL
dom.btnFetchJob.addEventListener("click", async () => {
const url = dom.matcherJobUrl.value.trim();
if (!url) {
showToast("Please enter a valid job posting URL", "error");
dom.matcherJobUrl.focus();
return;
}
dom.fetchBtnText.textContent = "⏳ Fetching...";
dom.btnFetchJob.disabled = true;
dom.matcherBotWarning.classList.add("hidden");
try {
const resp = await fetch("/matcher/fetch-job", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url: url })
});
if (!resp.ok) throw new Error("Failed to fetch job details");
const data = await resp.json();
if (data.company && !dom.matcherCompany.value) dom.matcherCompany.value = data.company;
if (data.role_title && !dom.matcherRole.value) dom.matcherRole.value = data.role_title;
if (data.description) {
dom.matcherJdText.value = data.description;
}
if (!data.success) {
dom.matcherBotWarning.classList.remove("hidden");
showToast("Job page requires login. Please paste description text below.", "warning");
} else {
showToast("Job extracted successfully!", "success");
}
} catch (err) {
dom.matcherBotWarning.classList.remove("hidden");
showToast(`Fetch notice: ${err.message}. Please paste job description manually.`, "warning");
} finally {
dom.fetchBtnText.textContent = "⚡ Fetch Job";
dom.btnFetchJob.disabled = false;
}
});
// 2. Multi-Resume Selector & Drag and Drop
dom.resumeSelectPicker.addEventListener("change", () => {
const selectedId = dom.resumeSelectPicker.value;
if (selectedId === "new") {
state.activeResumeId = null;
dom.matcherResumeText.value = "";
dom.resumeTitleInput.value = "";
dom.btnDeleteActiveResume.classList.add("hidden");
} else {
const resume = state.resumes.find(r => r.id === selectedId);
if (resume) {
state.activeResumeId = resume.id;
dom.matcherResumeText.value = resume.resume_text;
dom.resumeTitleInput.value = resume.title;
dom.btnDeleteActiveResume.classList.remove("hidden");
}
}
});
dom.btnDeleteActiveResume.addEventListener("click", async () => {
if (!state.activeResumeId) return;
if (!confirm("Are you sure you want to delete this saved resume profile?")) return;
try {
const resp = await fetch(`/matcher/resumes/${state.activeResumeId}`, { method: "DELETE" });
if (!resp.ok) throw new Error("Failed to delete resume");
showToast("Resume profile deleted", "info");
loadResumes();
} catch (err) {
showToast(err.message, "error");
}
});
dom.resumeDropzone.addEventListener("click", () => dom.resumeFileInput.click());
dom.resumeDropzone.addEventListener("dragover", (e) => {
e.preventDefault();
dom.resumeDropzone.style.borderColor = "var(--accent-primary)";
});
dom.resumeDropzone.addEventListener("dragleave", () => {
dom.resumeDropzone.style.borderColor = "var(--border-color)";
});
dom.resumeDropzone.addEventListener("drop", async (e) => {
e.preventDefault();
dom.resumeDropzone.style.borderColor = "var(--border-color)";
if (e.dataTransfer.files.length > 0) {
await handleResumeFileUpload(e.dataTransfer.files[0]);
}
});
dom.resumeFileInput.addEventListener("change", async (e) => {
if (e.target.files.length > 0) {
await handleResumeFileUpload(e.target.files[0]);
}
});
// 3. Save Resume Button
dom.btnSaveResume.addEventListener("click", async () => {
const text = dom.matcherResumeText.value.trim();
const title = dom.resumeTitleInput.value.trim() || "Master Resume";
if (!text) {
showToast("Resume text is empty", "error");
return;
}
try {
const resp = await fetch("/matcher/save-resume", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
user_external_id: state.userId,
title: title,
resume_text: text,
file_name: "manual_entry.txt"
})
});
if (!resp.ok) throw new Error("Failed to save resume");
showToast(`Saved resume profile "${title}"!`, "success");
loadResumes();
} catch (err) {
showToast(err.message, "error");
}
});
// 4. Run Match & Score Analysis
dom.btnRunMatch.addEventListener("click", async () => {
const jdText = dom.matcherJdText.value.trim();
const resumeText = dom.matcherResumeText.value.trim();
if (!jdText) {
showToast("Please provide a job posting URL or description", "error");
dom.matcherJdText.focus();
return;
}
if (!resumeText) {
showToast("Please upload your resume or paste resume text", "error");
dom.matcherResumeText.focus();
return;
}
// Show loading state
dom.matcherEmptyState.classList.add("hidden");
dom.matcherResultsView.classList.add("hidden");
dom.matcherLoadingState.classList.remove("hidden");
dom.btnRunMatch.disabled = true;
try {
const resp = await fetch("/matcher/analyze", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
user_external_id: state.userId,
resume_text: resumeText,
job_description: jdText,
job_url: dom.matcherJobUrl.value.trim() || null,
company: dom.matcherCompany.value.trim() || null,
role_title: dom.matcherRole.value.trim() || null
})
});
if (!resp.ok) {
const errorData = await resp.json().catch(() => ({}));
throw new Error(errorData.detail || "Analysis failed");
}
const result = await resp.json();
state.lastMatchResult = result;
renderMatchResults(result);
showToast(`Match analysis complete: ${result.match_score}% fit!`, "success");
// Also trigger Suggested Edits generation in the edits panel
fetchSuggestedEdits(false);
} catch (err) {
showToast(`Matching notice: ${err.message}`, "error");
dom.matcherLoadingState.classList.add("hidden");
dom.matcherEmptyState.classList.remove("hidden");
} finally {
dom.btnRunMatch.disabled = false;
}
});
// 5. Copy Pitch Button
dom.btnCopyPitch.addEventListener("click", () => {
if (state.lastMatchResult && state.lastMatchResult.tailored_pitch) {
navigator.clipboard.writeText(state.lastMatchResult.tailored_pitch);
showToast("Outreach pitch copied to clipboard!", "success");
}
});
// 6. Add Matched Job to Tracker
dom.btnAddMatchedToTracker.addEventListener("click", async () => {
if (!state.lastMatchResult) return;
const res = state.lastMatchResult;
const company = res.company || dom.matcherCompany.value.trim() || "Target Company";
const role = res.role_title || dom.matcherRole.value.trim() || "Target Role";
const notes = `Key Strengths:\n- ${(res.key_strengths || []).join('\n- ')}`;
try {
const resp = await fetch("/jobs", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
user_external_id: state.userId,
company: company,
role_title: role,
job_url: dom.matcherJobUrl.value.trim() || null,
match_score: res.match_score,
tailored_pitch: res.tailored_pitch,
follow_up_days: 7,
notes: notes
})
});
if (!resp.ok) throw new Error("Failed to add to Job Tracker");
showToast(`Added ${company} — ${role} to Job Tracker!`, "success");
loadJobs();
switchTab("tab-jobs");
} catch (err) {
showToast(err.message, "error");
}
});
}
// ============================================================================
// Suggested Resume Edits Handlers
// ============================================================================
function setupSuggestedEditsHandlers() {
if (dom.btnGenerateEdits) {
dom.btnGenerateEdits.addEventListener("click", () => {
fetchSuggestedEdits(true);
});
}
}
async function fetchSuggestedEdits(showToastNotification = true) {
const resumeText = dom.matcherResumeText.value.trim();
const jdText = dom.matcherJdText.value.trim();
if (!jdText) {
if (showToastNotification) showToast("Please fetch or paste a target job description", "error");
dom.matcherJdText.focus();
return;
}
if (!resumeText) {
if (showToastNotification) showToast("Please provide or upload your resume text", "error");
dom.matcherResumeText.focus();
return;
}
if (dom.editsEmptyState) dom.editsEmptyState.classList.add("hidden");
if (dom.editsListContainer) dom.editsListContainer.classList.add("hidden");
if (dom.editsLoadingState) dom.editsLoadingState.classList.remove("hidden");
if (dom.btnGenerateEdits) dom.btnGenerateEdits.disabled = true;
if (dom.btnGenerateEditsText) dom.btnGenerateEditsText.textContent = "SYNTHESIZING...";
try {
const resp = await fetch("/matcher/suggest-edits", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
user_external_id: state.userId,
resume_text: resumeText,
job_description: jdText,
company: dom.matcherCompany.value.trim() || null,
role_title: dom.matcherRole.value.trim() || null
})
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.detail || "Failed to generate edit suggestions");
}
const data = await resp.json();
state.suggestedEdits = data.suggestions || [];
renderSuggestedEdits(state.suggestedEdits);
if (showToastNotification) {
if (state.suggestedEdits.length > 0) {
showToast(`Generated ${state.suggestedEdits.length} tailored edit suggestions!`, "success");
} else {
showToast("Resume is already well aligned with the job description!", "info");
}
}
} catch (err) {
showToast(err.message, "error");
if (dom.editsLoadingState) dom.editsLoadingState.classList.add("hidden");
if (dom.editsEmptyState) dom.editsEmptyState.classList.remove("hidden");
} finally {
if (dom.btnGenerateEdits) dom.btnGenerateEdits.disabled = false;
if (dom.btnGenerateEditsText) dom.btnGenerateEditsText.textContent = "GENERATE EDITS";
}
}
function renderSuggestedEdits(suggestions) {
if (!dom.editsListContainer) return;
if (dom.editsLoadingState) dom.editsLoadingState.classList.add("hidden");
if (!suggestions || suggestions.length === 0) {
if (dom.editsEmptyState) {
dom.editsEmptyState.innerHTML = `
task_alt
No pending edits! All relevant skills and experience bullets are well framed.
`;
dom.editsEmptyState.classList.remove("hidden");
}
dom.editsListContainer.classList.add("hidden");
return;
}
if (dom.editsEmptyState) dom.editsEmptyState.classList.add("hidden");
dom.editsListContainer.classList.remove("hidden");
dom.editsListContainer.innerHTML = suggestions.map((s, idx) => {
const isRewrite = s.type === "bullet_rewrite";
const typeLabel = isRewrite ? "BULLET REWRITE" : "KEYWORD GAP";
const badgeClass = isRewrite ? "edit-type-rewrite" : "edit-type-keyword";
return `
${s.original ? `
${escapeHtml(s.original)}
` : ''}
${escapeHtml(s.suggested)}
💡 ${escapeHtml(s.reason)}
${s.flagged_for_review && s.warning_message ? `
info
⚠️ ${escapeHtml(s.warning_message)}
` : ''}
Dismiss
check
Accept Edit
`;
}).join("");
}
window.acceptSuggestion = function(index) {
const s = state.suggestedEdits[index];
if (!s) return;
const currentResume = dom.matcherResumeText.value;
if (s.type === "bullet_rewrite" && s.original) {
if (currentResume.includes(s.original)) {
dom.matcherResumeText.value = currentResume.replace(s.original, s.suggested);
showToast("Applied rewrite to resume editor!", "success");
} else {
dom.matcherResumeText.value += `\n- ${s.suggested}`;
showToast("Appended optimized bullet to resume editor!", "info");
}
} else if (s.type === "missing_keyword") {
const skillMatch = s.suggested.match(/Add '([^']+)'/i);
const skillToAdd = skillMatch ? skillMatch[1] : s.suggested;
const skillsHeaderRegex = /(SKILLS[\s\S]*?:)/i;
if (skillsHeaderRegex.test(currentResume)) {
dom.matcherResumeText.value = currentResume.replace(skillsHeaderRegex, `$1\n- ${skillToAdd}`);
showToast(`Added '${skillToAdd}' to Skills section!`, "success");
} else {
dom.matcherResumeText.value = `SKILLS:\n- ${skillToAdd}\n\n` + currentResume;
showToast(`Added '${skillToAdd}' to resume!`, "success");
}
} else {
dom.matcherResumeText.value += `\n- ${s.suggested}`;
showToast("Applied edit suggestion to resume!", "success");
}
// Remove accepted suggestion from list
state.suggestedEdits.splice(index, 1);
renderSuggestedEdits(state.suggestedEdits);
};
window.dismissSuggestion = function(index) {
state.suggestedEdits.splice(index, 1);
renderSuggestedEdits(state.suggestedEdits);
showToast("Dismissed suggestion", "info");
};
// ============================================================================
// ATS Checker Handlers
// ============================================================================
function setupAtsCheckerHandlers() {
if (!dom.btnRunAtsCheck) return;
// Picker selection sync
if (dom.atsResumeSelectPicker) {
dom.atsResumeSelectPicker.addEventListener("change", () => {
const selectedId = dom.atsResumeSelectPicker.value;
if (selectedId === "new") {
dom.atsResumeText.value = "";
} else {
const resume = state.resumes.find(r => r.id === selectedId);
if (resume) {
dom.atsResumeText.value = resume.resume_text;
}
}
});
}
// Dropzone drag-and-drop
if (dom.atsResumeDropzone) {
dom.atsResumeDropzone.addEventListener("click", () => dom.atsResumeFileInput.click());
dom.atsResumeDropzone.addEventListener("dragover", (e) => {
e.preventDefault();
dom.atsResumeDropzone.style.borderColor = "var(--accent-primary)";
});
dom.atsResumeDropzone.addEventListener("dragleave", () => {
dom.atsResumeDropzone.style.borderColor = "var(--border-color)";
});
dom.atsResumeDropzone.addEventListener("drop", async (e) => {
e.preventDefault();
dom.atsResumeDropzone.style.borderColor = "var(--border-color)";
if (e.dataTransfer.files.length > 0) {
await handleAtsFileUpload(e.dataTransfer.files[0]);
}
});
}
if (dom.atsResumeFileInput) {
dom.atsResumeFileInput.addEventListener("change", async (e) => {
if (e.target.files.length > 0) {
await handleAtsFileUpload(e.target.files[0]);
}
});
}
// Run ATS Check Button
dom.btnRunAtsCheck.addEventListener("click", async () => {
const text = dom.atsResumeText.value.trim();
if (!text) {
showToast("Please select a resume profile or upload a resume file", "error");
dom.atsResumeText.focus();
return;
}
// Show loading state
dom.atsEmptyState.classList.add("hidden");
dom.atsResultsView.classList.add("hidden");
dom.atsLoadingState.classList.remove("hidden");
dom.btnRunAtsCheck.disabled = true;
try {
const resp = await fetch("/ats/check", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
user_external_id: state.userId,
resume_text: text,
file_name: "resume.pdf"
})
});
if (!resp.ok) {
const errorData = await resp.json().catch(() => ({}));
throw new Error(errorData.detail || "ATS check failed");
}
const result = await resp.json();
state.lastAtsResult = result;
renderAtsResults(result);
showToast(`ATS audit complete: ${result.overall_score}% (${result.label})`, "success");
} catch (err) {
showToast(`ATS audit notice: ${err.message}`, "error");
dom.atsLoadingState.classList.add("hidden");
dom.atsEmptyState.classList.remove("hidden");
} finally {
dom.btnRunAtsCheck.disabled = false;
}
});
}
async function handleAtsFileUpload(file) {
showToast(`Parsing resume (${file.name})...`, "info");
const formData = new FormData();
formData.append("user_external_id", state.userId);
formData.append("title", file.name.replace(/\.[^/.]+$/, ""));
formData.append("file", file);
try {
const resp = await fetch("/matcher/upload-resume", {
method: "POST",
body: formData
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.detail || "Failed to parse resume");
}
const data = await resp.json();
dom.atsResumeText.value = data.resume_text;
showToast(`Extracted ${data.char_count} characters from resume!`, "success");
loadResumes();
} catch (err) {
showToast(err.message, "error");
} finally {
dom.atsResumeFileInput.value = "";
}
}
function renderAtsResults(res) {
dom.atsLoadingState.classList.add("hidden");
dom.atsEmptyState.classList.add("hidden");
dom.atsResultsView.classList.remove("hidden");
// Overall Score & Gauge
dom.atsScoreVal.innerHTML = `${res.overall_score}% `;
if (dom.atsGaugeCircle) {
dom.atsGaugeCircle.className = "gauge-circle";
if (res.overall_score >= 80) dom.atsGaugeCircle.classList.add("score-strong");
else if (res.overall_score >= 65) dom.atsGaugeCircle.classList.add("score-good");
else if (res.overall_score >= 50) dom.atsGaugeCircle.classList.add("score-moderate");
else dom.atsGaugeCircle.classList.add("score-low");
}
// Verdict Pill
dom.atsVerdictPill.textContent = res.label.toUpperCase();
dom.atsVerdictPill.className = "px-4 py-1.5 border border-secondary text-secondary font-label-mono text-[11px] uppercase tracking-widest glow-secondary bg-secondary/10";
if (res.overall_score < 50) {
dom.atsVerdictPill.className = "px-4 py-1.5 border border-error text-error font-label-mono text-[11px] uppercase tracking-widest glow-error bg-error/10";
}
// Sub-scores
dom.atsL1Val.textContent = `${res.layer1_score}%`;
dom.atsL2Val.textContent = res.layer2_score != null ? `${res.layer2_score}%` : "Rule-Based Active";
// Layer 1: Rule-Based Checks List
dom.atsRuleChecksList.innerHTML = (res.rule_based_checks || []).map(c => {
const badgeCls = c.status === "pass" ? "status-pass" : c.status === "warn" ? "status-warn" : "status-fail";
const badgeText = c.status === "pass" ? "✓ PASS" : c.status === "warn" ? "⚠️ WARN" : "✗ FAIL";
const title = formatCheckTitle(c.name);
return `
${escapeHtml(title)}
${escapeHtml(c.detail)}
${badgeText}
`;
}).join("");
// Layer 2: ML Signals List
dom.atsMlChecksList.innerHTML = (res.ml_checks || []).map(c => {
const badgeCls = c.status === "pass" ? "status-pass" : c.status === "warn" ? "status-warn" : c.status === "fail" ? "status-fail" : "status-unavailable";
const badgeText = c.status === "pass" ? "✓ PASS" : c.status === "warn" ? "⚠️ WARN" : c.status === "fail" ? "✗ FAIL" : "⚙️ UNAVAILABLE";
const title = formatCheckTitle(c.name);
return `
${escapeHtml(title)} ${c.model ? `(${escapeHtml(c.model)}) ` : ''}
${escapeHtml(c.detail)}
${badgeText}
`;
}).join("");
// Recommendations Checklist
dom.atsRecommendationsList.innerHTML = (res.recommendations || []).map(r => `
${escapeHtml(r)}
`).join("");
}
function formatCheckTitle(name) {
const map = {
"file_format": "File Format Compatibility",
"filename_conventions": "Filename Hygiene & Conventions",
"standard_sections": "Standard ATS Section Headers",
"contact_info": "Contact Information Detectability",
"multi_column_tables": "Single-Column Layout & Tables",
"resume_length": "Resume Length & Word Count",
"bullet_point_density": "Bullet Point Formatting & Scannability",
"skills_extractable": "BERT Technical Skills Extraction",
"designation_extractable": "BERT Job Designation Extraction",
"role_coherence": "Role Classification & Domain Coherence",
"contact_email_domain": "Contact Email & Domain Verification",
"messaging_channel_screening": "Recruitment & Messaging Channels",
"upfront_fees_and_equipment": "Upfront Fees & Equipment Demands",
"unrealistic_pay_and_urgency": "Compensation & Pressure Phrasing",
"url_security_and_domain": "URL Protocol & Shortener Hygiene",
"posting_structural_quality": "Structural Hygiene & Formatting"
};
return map[name] || name.replace(/_/g, " ").replace(/\b\w/g, l => l.toUpperCase());
}
// ============================================================================
// Job Authenticity Checker
// ============================================================================
const AUTH_PRESET_SAFE = {
company: "Stripe",
role_title: "Senior Backend Engineer - Platform",
url: "https://stripe.com/jobs/senior-backend-engineer",
text: `Senior Backend Engineer - Core Payments Platform\nStripe (careers@stripe.com)\nLocation: San Francisco, CA (Hybrid)\n\nAbout the Role:\nWe are seeking an experienced Backend Engineer to scale our distributed core transaction systems. You will build highly available APIs, design resilient PostgreSQL architectures, and collaborate across global engineering teams.\n\nQualifications:\n- 5+ years of experience with Python, Go, or Ruby in production systems.\n- Deep understanding of distributed transactions, event-driven architectures, and database scalability.\n- Strong communication skills and a passion for engineering craft.\n\nCompensation & Benefits:\nBase Salary: $175,000 - $215,000 + Equity + Full Health/Dental/Vision + 401(k) Matching.`
};
const AUTH_PRESET_SCAM = {
company: "Apex Global Remote",
role_title: "Work From Home Data Entry Specialist",
url: "http://bit.ly/urgent-remote-entry-392",
text: `URGENT WORK FROM HOME DATA ENTRY ASSISTANT - EARN $4500 A WEEK NO EXPERIENCE NEEDED!!!!\n\nWe are urgently hiring 20 remote data assistants immediately today. No interview or background required.\n\nTo apply, you MUST message our HR Manager on Telegram: @hiring_officer_apex or email apex-hiring-desk@gmail.com.\n\nWe will mail you a $3200 equipment check upfront to purchase certified home office hardware from our approved vendor. You must send your bank account routing details for initial direct deposit setup.\n\nA $45 administrative onboarding processing fee is required upfront before starting.`
};
function setupAuthenticityHandlers() {
if (!dom.btnRunAuthenticity) return;
// Preset buttons
if (dom.btnAuthPresetSafe) {
dom.btnAuthPresetSafe.addEventListener("click", () => {
dom.authCompanyInput.value = AUTH_PRESET_SAFE.company;
dom.authRoleInput.value = AUTH_PRESET_SAFE.role_title;
dom.authJobUrl.value = AUTH_PRESET_SAFE.url;
dom.authJobText.value = AUTH_PRESET_SAFE.text;
showToast("Loaded Legitimate Tech Role preset", "info");
});
}
if (dom.btnAuthPresetScam) {
dom.btnAuthPresetScam.addEventListener("click", () => {
dom.authCompanyInput.value = AUTH_PRESET_SCAM.company;
dom.authRoleInput.value = AUTH_PRESET_SCAM.role_title;
dom.authJobUrl.value = AUTH_PRESET_SCAM.url;
dom.authJobText.value = AUTH_PRESET_SCAM.text;
showToast("Loaded Known Scam Pattern preset", "info");
});
}
// Cross-tab button from Resume Matcher
if (dom.btnMatcherCheckAuth) {
dom.btnMatcherCheckAuth.addEventListener("click", () => {
const url = dom.matcherJobUrl.value.trim();
const text = dom.matcherJobText.value.trim();
const comp = dom.matcherCompany.value.trim();
const role = dom.matcherRole.value.trim();
if (!url && !text) {
showToast("Please enter a Job URL or paste description text first", "warning");
return;
}
dom.authJobUrl.value = url;
dom.authJobText.value = text;
if (comp) dom.authCompanyInput.value = comp;
if (role) dom.authRoleInput.value = role;
switchTab("tab-authenticity");
runAuthenticityAudit();
});
}
// URL Fetching
if (dom.btnAuthFetchJob) {
dom.btnAuthFetchJob.addEventListener("click", async () => {
const url = (dom.authJobUrl.value || "").trim();
if (!url) {
showToast("Please enter a valid Job URL", "warning");
return;
}
dom.authFetchBtnText.textContent = "⌛ FETCHING...";
dom.btnAuthFetchJob.disabled = true;
if (dom.authBotWarning) dom.authBotWarning.classList.add("hidden");
try {
const resp = await fetch("/matcher/fetch-job", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url: url })
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.detail || "Failed to fetch job posting");
}
const data = await resp.json();
if (data.company && !dom.authCompanyInput.value) dom.authCompanyInput.value = data.company;
if (data.role_title && !dom.authRoleInput.value) dom.authRoleInput.value = data.role_title;
if (data.description) dom.authJobText.value = data.description;
if (!data.success && dom.authBotWarning) {
dom.authBotWarning.classList.remove("hidden");
dom.authBotWarning.textContent = `⚠️ ${data.message}`;
showToast("Protected site. Please verify or paste description manually.", "warning");
} else {
showToast(`Extracted ${data.description ? data.description.length : 0} characters!`, "success");
}
} catch (err) {
showToast(err.message, "error");
if (dom.authBotWarning) {
dom.authBotWarning.classList.remove("hidden");
dom.authBotWarning.textContent = `⚠️ ${err.message}`;
}
} finally {
dom.authFetchBtnText.textContent = "⚡ FETCH";
dom.btnAuthFetchJob.disabled = false;
}
});
}
// Run Audit
dom.btnRunAuthenticity.addEventListener("click", runAuthenticityAudit);
}
async function runAuthenticityAudit() {
const url = (dom.authJobUrl.value || "").trim();
const jobText = (dom.authJobText.value || "").trim();
const company = (dom.authCompanyInput.value || "").trim();
const roleTitle = (dom.authRoleInput.value || "").trim();
if (!url && !jobText) {
showToast("Please enter a Job URL or paste the job description text", "warning");
return;
}
dom.authEmptyState.classList.add("hidden");
dom.authResultsView.classList.add("hidden");
dom.authLoadingState.classList.remove("hidden");
dom.btnRunAuthenticity.disabled = true;
dom.authBtnLabel.textContent = "AUDITING_LEGITIMACY...";
try {
const payload = {
url: url || null,
job_text: jobText || null,
company: company || null,
role_title: roleTitle || null
};
const resp = await fetch("/authenticity/check", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.detail || "Authenticity audit failed");
}
const data = await resp.json();
renderAuthenticityReport(data);
showToast(`Authenticity assessment complete: ${data.verdict_label}`, data.risk_level === "high" ? "error" : data.risk_level === "medium" ? "warning" : "success");
} catch (err) {
showToast(err.message, "error");
dom.authLoadingState.classList.add("hidden");
dom.authEmptyState.classList.remove("hidden");
} finally {
dom.btnRunAuthenticity.disabled = false;
dom.authBtnLabel.textContent = "RUN AUTHENTICITY AUDIT";
}
}
function renderAuthenticityReport(res) {
dom.authLoadingState.classList.add("hidden");
dom.authEmptyState.classList.add("hidden");
dom.authResultsView.classList.remove("hidden");
// Hero Card Styling
dom.authRiskHero.className = "bg-surface-container border p-6 tech-card flex flex-col items-center justify-center text-center gap-3";
if (res.risk_level === "high") {
dom.authRiskHero.classList.add("risk-high-hero", "glow-error");
} else if (res.risk_level === "medium") {
dom.authRiskHero.classList.add("risk-medium-hero");
} else {
dom.authRiskHero.classList.add("risk-low-hero", "glow-secondary");
}
dom.authRiskPill.textContent = res.verdict_label.toUpperCase();
dom.authRiskScore.innerHTML = `${res.risk_score}/ 100 RISK INDEX `;
dom.authRiskSummary.textContent = res.summary;
// Layer 1: Rule Heuristics
const l1Checks = res.layer1_heuristics || [];
dom.authL1Counts.textContent = `${l1Checks.length} checks evaluated (${res.red_flags_count} flags, ${res.warnings_count} warnings)`;
dom.authLayer1List.innerHTML = l1Checks.map(c => {
const badgeCls = c.status === "pass" ? "status-pass" : c.status === "warn" ? "status-warn" : c.status === "fail" ? "status-fail" : "status-unavailable";
const badgeText = c.status === "pass" ? "✓ PASS" : c.status === "warn" ? "⚠️ WARN" : c.status === "fail" ? "✗ FAIL" : "⚙️ UNAVAILABLE";
const title = formatCheckTitle(c.name);
return `
${escapeHtml(title)}
${escapeHtml(c.detail)}
${c.evidence ? `Matched Evidence: "${escapeHtml(c.evidence)}" ` : ''}
${badgeText}
`;
}).join("");
// Layer 2: BERT ML Classifier
const l2 = res.layer2_classifier || {};
if (l2.status === "completed") {
const isFake = l2.predicted_label === "Fake Job";
const badgeCls = isFake ? "status-fail" : "status-pass";
const confPct = Math.round((l2.confidence || 0) * 100);
dom.authLayer2Content.innerHTML = `
Model: ${escapeHtml(l2.model_name)}
${escapeHtml(l2.detail)}
${escapeHtml(l2.predicted_label)}
${confPct}% conf
Documented Model Caveats: ${escapeHtml(l2.limitations_note || '')}
`;
} else {
dom.authLayer2Content.innerHTML = `
ML Classifier Signal
${escapeHtml(l2.detail || 'Model evaluation unavailable. Fallback to Layer 1 & 3.')}
⚙️ UNAVAILABLE
`;
}
// Layer 3: RAG Reasoning
const l3 = res.layer3_llm_reasoning || {};
if (l3.status === "completed") {
const patterns = l3.matched_patterns || [];
const patternCards = patterns.length > 0
? patterns.map(p => `
⚠️ ${escapeHtml(p.pattern)}
Evidence Quote: "${escapeHtml(p.evidence)}"
${escapeHtml(p.explanation)}
`).join("")
: `
✓ No known fraud patterns matched in knowledge base retrieval. Posting text appears consistent with standard hiring.
`;
dom.authLayer3Content.innerHTML = `
Retrieved Fraud Knowledge Patterns: ${l3.retrieved_patterns_count} categories evaluated
LLM Risk: ${(l3.risk_level || 'low').toUpperCase()}
${escapeHtml(l3.reasoning_summary)}
${patternCards}
`;
} else {
dom.authLayer3Content.innerHTML = `
RAG LLM Reasoning Layer
${escapeHtml(l3.reasoning_summary || 'LLM reasoning layer unavailable. Decision based on Layer 1 & 2 signals.')}
⚙️ UNAVAILABLE
`;
}
}
// ============================================================================
// Company Insights Handlers & Rendering
// ============================================================================
const INSIGHT_PRESET_STRIPE = {
company: "Stripe",
url: "https://stripe.com",
role: "Staff Backend Engineer",
about: "Stripe is a financial infrastructure platform for businesses. Millions of companies—from the world’s largest enterprises to the most ambitious startups—use Stripe to accept payments, grow their revenue, and accelerate new business opportunities. Founded in 2010 with over 7,000 employees globally."
};
const INSIGHT_PRESET_STARTUP = {
company: "Venture Stealth Robotics",
url: "https://stealthrobotics.ai",
role: "Senior Motion Planning Engineer",
about: "Venture Stealth Robotics is an early stage seed funded robotics startup developing autonomous manipulation software. Founded in 2023 by ex-DeepMind researchers, we are a fast-moving team of 14 engineers based in San Francisco building next-generation warehouse logistics robots."
};
const INSIGHT_PRESET_CONFLICT = {
company: "MegaGlobal Enterprises",
url: "https://megaglobal.com",
role: "Lead Platform Architect",
about: "MegaGlobal is an early stage stealth startup. Founded in 1994, MegaGlobal employs over 50,000 engineers and operations staff across 40 countries, processing billions in enterprise commerce annually."
};
function setupCompanyInsightsHandlers() {
if (!dom.btnRunInsights) return;
// Preset buttons
if (dom.btnInsightPresetStripe) {
dom.btnInsightPresetStripe.addEventListener("click", () => {
dom.insightCompanyInput.value = INSIGHT_PRESET_STRIPE.company;
dom.insightUrlInput.value = INSIGHT_PRESET_STRIPE.url;
dom.insightRoleInput.value = INSIGHT_PRESET_STRIPE.role;
dom.insightAboutInput.value = INSIGHT_PRESET_STRIPE.about;
fetchCompanyInsights();
});
}
if (dom.btnInsightPresetStartup) {
dom.btnInsightPresetStartup.addEventListener("click", () => {
dom.insightCompanyInput.value = INSIGHT_PRESET_STARTUP.company;
dom.insightUrlInput.value = INSIGHT_PRESET_STARTUP.url;
dom.insightRoleInput.value = INSIGHT_PRESET_STARTUP.role;
dom.insightAboutInput.value = INSIGHT_PRESET_STARTUP.about;
fetchCompanyInsights();
});
}
if (dom.btnInsightPresetConflict) {
dom.btnInsightPresetConflict.addEventListener("click", () => {
dom.insightCompanyInput.value = INSIGHT_PRESET_CONFLICT.company;
dom.insightUrlInput.value = INSIGHT_PRESET_CONFLICT.url;
dom.insightRoleInput.value = INSIGHT_PRESET_CONFLICT.role;
dom.insightAboutInput.value = INSIGHT_PRESET_CONFLICT.about;
fetchCompanyInsights();
});
}
// If user edits company name manually, clear stale preset URLs that do not match
if (dom.insightCompanyInput) {
dom.insightCompanyInput.addEventListener("input", () => {
const comp = dom.insightCompanyInput.value.toLowerCase().trim();
const currUrl = (dom.insightUrlInput.value || "").toLowerCase().trim();
if (currUrl && !currUrl.includes(comp) && (currUrl.includes("stripe.com") || currUrl.includes("stealthrobotics.ai") || currUrl.includes("megaglobal.com"))) {
dom.insightUrlInput.value = "";
}
});
}
// Inspect insights from matcher result card
if (dom.btnMatcherInspectInsights) {
dom.btnMatcherInspectInsights.addEventListener("click", () => {
const comp = dom.matcherCompany.value.trim() || (state.lastMatchResult && state.lastMatchResult.company) || "Target Company";
const role = dom.matcherRole.value.trim() || (state.lastMatchResult && state.lastMatchResult.role_title) || "";
const jd = dom.matcherJdText.value.trim() || "";
const url = dom.matcherJobUrl.value.trim() || "";
dom.insightCompanyInput.value = comp;
dom.insightRoleInput.value = role;
dom.insightAboutInput.value = jd;
dom.insightUrlInput.value = url;
switchTab("tab-insights");
fetchCompanyInsights();
});
}
// Run Insights Button
dom.btnRunInsights.addEventListener("click", () => {
fetchCompanyInsights();
});
}
async function fetchCompanyInsights() {
const company = (dom.insightCompanyInput.value || "").trim();
const url = (dom.insightUrlInput.value || "").trim();
const role = (dom.insightRoleInput.value || "").trim();
const about = (dom.insightAboutInput.value || "").trim();
if (!company) {
showToast("Please enter a company name to analyze", "warning");
dom.insightCompanyInput.focus();
return;
}
dom.insightEmptyState.classList.add("hidden");
dom.insightResultsView.classList.add("hidden");
dom.insightLoadingState.classList.remove("hidden");
dom.btnRunInsights.disabled = true;
dom.insightBtnLabel.textContent = "SYNTHESIZING_INTELLIGENCE...";
try {
const payload = {
company: company,
company_url: url || null,
role_title: role || null,
about_text: about || null,
user_external_id: state.userId
};
const resp = await fetch("/insights/analyze", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.detail || "Failed to generate company insights");
}
const data = await resp.json();
state.lastInsightsResult = data;
renderCompanyInsights(data);
showToast(`Company profile generated for ${data.company}!`, "success");
} catch (err) {
showToast(err.message, "error");
dom.insightLoadingState.classList.add("hidden");
dom.insightEmptyState.classList.remove("hidden");
} finally {
dom.btnRunInsights.disabled = false;
dom.insightBtnLabel.textContent = "GENERATE COMPANY INTELLIGENCE";
}
}
function renderCompanyInsights(data) {
dom.insightLoadingState.classList.add("hidden");
dom.insightEmptyState.classList.add("hidden");
dom.insightResultsView.classList.remove("hidden");
// URL Mismatch Alert Banner
const mismatchWarn = data.url_mismatch_warning || (data.classification && data.classification.url_mismatch_warning);
if (mismatchWarn && dom.insightUrlMismatchAlert) {
dom.insightUrlMismatchText.textContent = mismatchWarn;
dom.insightUrlMismatchAlert.classList.remove("hidden");
} else if (dom.insightUrlMismatchAlert) {
dom.insightUrlMismatchAlert.classList.add("hidden");
}
// Classification Card
dom.insightCompanyTitle.textContent = data.company;
const cls = data.classification || {};
dom.insightSizeLabel.textContent = cls.label || "Company";
const confPct = Math.round((cls.confidence || 0) * 100);
dom.insightSizeConfidence.textContent = `Confidence: ${confPct}%`;
if (cls.industry) {
const indConfPct = cls.industry_confidence ? ` (${Math.round(cls.industry_confidence * 100)}%)` : '';
dom.insightIndustryBadge.textContent = `${cls.industry}${indConfPct}`;
dom.insightIndustryBadge.classList.remove("hidden");
} else {
dom.insightIndustryBadge.classList.add("hidden");
}
// Low Confidence Data Quality Alert
if (cls.low_confidence_data && dom.insightLowDataAlert) {
dom.insightLowDataText.textContent = cls.data_quality_note || "Limited grounding data available — workforce headcount and founding year could not be verified.";
dom.insightLowDataAlert.classList.remove("hidden");
} else if (dom.insightLowDataAlert) {
dom.insightLowDataAlert.classList.add("hidden");
}
// Disagreement Alert
if (cls.disagreement_flag && dom.insightDisagreementAlert) {
dom.insightDisagreementAlert.classList.remove("hidden");
} else if (dom.insightDisagreementAlert) {
dom.insightDisagreementAlert.classList.add("hidden");
}
// Zero-Shot Score Breakdown
const scores = cls.all_scores || {};
const scoreEntries = Object.entries(scores);
if (scoreEntries.length > 0) {
dom.insightAllScoresList.innerHTML = scoreEntries.map(([label, score]) => {
const pct = Math.round(score * 100);
const isTop = Math.abs(score - (cls.confidence || 0)) < 0.05;
return `
${escapeHtml(label)}
${pct}%
`;
}).join("");
} else {
dom.insightAllScoresList.innerHTML = `Deterministic heuristic classification active. `;
}
// Extracted Facts
const facts = cls.facts || {};
dom.insightEmployeesVal.textContent = facts.employee_count_estimate || "unknown";
dom.insightFoundedVal.textContent = facts.founded_year ? facts.founded_year.toString() : "unknown";
// Culture & Sentiment
const culture = data.culture_synthesis || {};
const sent = culture.sentiment_breakdown || { positive_count: 0, negative_count: 0, total: 0 };
const totalSent = sent.total || (sent.positive_count + sent.negative_count) || 1;
const posPct = Math.round((sent.positive_count / totalSent) * 100);
const negPct = 100 - posPct;
dom.insightSentimentSummary.textContent = `${sent.positive_count} POSITIVE / ${sent.negative_count} NEGATIVE (${posPct}% POSITIVE)`;
dom.insightSentimentPosBar.style.width = `${posPct}%`;
dom.insightSentimentNegBar.style.width = `${negPct}%`;
// Signal Strength & Note
if (dom.insightSignalStrengthNote) {
if (sent.signal_strength_note) {
dom.insightSignalStrengthNote.textContent = sent.signal_strength_note;
dom.insightSignalStrengthNote.classList.remove("hidden");
} else {
dom.insightSignalStrengthNote.classList.add("hidden");
}
}
if (dom.insightSentimentBarTrack) {
if (sent.signal_strength === "low") {
dom.insightSentimentBarTrack.classList.add("opacity-60");
} else {
dom.insightSentimentBarTrack.classList.remove("opacity-60");
}
}
// Praised Aspects
const praised = culture.praised_aspects || [];
dom.insightPraisedList.innerHTML = praised.map(p => `
•
${escapeHtml(p)}
`).join("");
// Criticized Aspects
const criticized = culture.criticized_aspects || [];
dom.insightCriticizedList.innerHTML = criticized.map(c => `
•
${escapeHtml(c)}
`).join("");
// Interview Intelligence
const interview = data.interview_insights || {};
const focusAreas = interview.focus_areas || [];
dom.insightFocusAreasTags.innerHTML = focusAreas.map(f => `
${escapeHtml(f)}
`).join("");
const stages = interview.process_stages || [];
dom.insightProcessStagesList.innerHTML = stages.map(s => `
${escapeHtml(s)}
`).join("");
const tips = interview.prep_tips || [];
dom.insightPrepTipsList.innerHTML = tips.map(t => `
${escapeHtml(t)}
`).join("");
}
// ============================================================================
// Apply via Email (Outreach) Handlers & Rendering
// ============================================================================
const OUTREACH_RAYO_EXAMPLE = `🏢 Company: Rayo Innovations Pvt. Ltd.
💼 Open Positions:
* Web Designer
* Graphic Designer
* Android Developer
* iOS Developer
🎯 Experience: 0–1 Year / Freshers
📍 Location: Shyamal, Ahmedabad
🗓️ Working Days: 5 Days a Week
📧 Apply Via: hr@rayoinnovations.com
📞 Contact: +91 8799493952`;
function setOutreachStep(stepNum) {
const steps = [
dom.outreachStep1Indicator,
dom.outreachStep2Indicator,
dom.outreachStep3Indicator,
dom.outreachStep4Indicator
];
steps.forEach((el, idx) => {
if (!el) return;
const currentStep = idx + 1;
el.classList.remove("step-indicator-active", "step-indicator-done");
if (currentStep === stepNum) {
el.classList.add("step-indicator-active");
} else if (currentStep < stepNum) {
el.classList.add("step-indicator-done");
}
});
}
function setupOutreachHandlers() {
if (!dom.btnParseOutreach) return;
// Preset example button
if (dom.btnOutreachPresetRayo) {
dom.btnOutreachPresetRayo.addEventListener("click", () => {
dom.outreachJdText.value = OUTREACH_RAYO_EXAMPLE;
showToast("Loaded Rayo Innovations example posting", "info");
parseOutreachJD();
});
}
// Fetch URL button
if (dom.btnOutreachFetchUrl) {
dom.btnOutreachFetchUrl.addEventListener("click", async () => {
const url = (dom.outreachUrlInput.value || "").trim();
if (!url) {
showToast("Please enter a valid job URL to fetch", "warning");
return;
}
showToast("Fetching job description from URL...", "info");
parseOutreachJD(url);
});
}
// Parse button
dom.btnParseOutreach.addEventListener("click", () => {
parseOutreachJD();
});
// Draft button
if (dom.btnDraftOutreach) {
dom.btnDraftOutreach.addEventListener("click", () => {
draftOutreachEmail();
});
}
// Connect Gmail button
if (dom.btnOutreachConnectGmail) {
dom.btnOutreachConnectGmail.addEventListener("click", async () => {
try {
const resp = await fetch(`/api/outreach/gmail/connect?user_external_id=${encodeURIComponent(state.userId)}`);
const data = await resp.json();
if (data.configured && data.auth_url) {
const authWin = window.open(data.auth_url, "Google OAuth", "width=550,height=650");
if (!authWin) {
window.location.href = data.auth_url;
}
} else {
showToast(
"Google OAuth is running in local mode. Please configure GOOGLE_CLIENT_ID in .env or settings for live OAuth consent.",
"warning"
);
}
} catch (err) {
showToast(`OAuth connection error: ${err.message}`, "error");
}
});
}
// Disconnect Gmail button
if (dom.btnOutreachDisconnectGmail) {
dom.btnOutreachDisconnectGmail.addEventListener("click", async () => {
try {
const resp = await fetch(`/api/outreach/gmail/disconnect?user_external_id=${encodeURIComponent(state.userId)}`, {
method: "POST"
});
if (resp.ok) {
showToast("Gmail access disconnected successfully", "info");
checkGmailStatus();
}
} catch (err) {
showToast(`Disconnect error: ${err.message}`, "error");
}
});
}
// Explicit Send button
if (dom.btnSendOutreach) {
dom.btnSendOutreach.addEventListener("click", () => {
sendOutreachApplication();
});
}
// View in Job Tracker shortcut
if (dom.btnOutreachGotoTracker) {
dom.btnOutreachGotoTracker.addEventListener("click", () => {
switchTab("tab-jobs");
});
}
// Matcher shortcut
if (dom.btnMatcherApplyEmail) {
dom.btnMatcherApplyEmail.addEventListener("click", () => {
const comp = dom.matcherCompany.value.trim() || (state.lastMatchResult && state.lastMatchResult.company) || "";
const role = dom.matcherRole.value.trim() || (state.lastMatchResult && state.lastMatchResult.role_title) || "";
const jd = dom.matcherJdText.value.trim() || "";
const url = dom.matcherJobUrl.value.trim() || "";
let formattedText = jd;
if (comp || role) {
formattedText = `🏢 Company: ${comp || 'Company'}\n💼 Open Positions:\n* ${role || 'Role'}\n\n${jd}`.trim();
}
dom.outreachJdText.value = formattedText;
dom.outreachUrlInput.value = url;
switchTab("tab-outreach");
showToast("Transferred matched JD to Apply via Email", "info");
parseOutreachJD();
});
}
// Listen for OAuth completion popup message
window.addEventListener("message", (event) => {
if (event.data && event.data.type === "GMAIL_CONNECTED") {
checkGmailStatus();
showToast(`Gmail account connected: ${event.data.email || ''}`, "success");
}
});
}
async function parseOutreachJD(urlParam = null) {
const text = (dom.outreachJdText.value || "").trim();
const url = (urlParam || dom.outreachUrlInput.value || "").trim();
const resumeSelectVal = dom.outreachResumeSelectPicker ? dom.outreachResumeSelectPicker.value : "active";
const resumeId = resumeSelectVal !== "active" ? resumeSelectVal : (state.activeResumeId || null);
if (!text && !url) {
showToast("Please paste job posting text or provide a JD URL", "warning");
return;
}
dom.btnParseOutreach.disabled = true;
dom.btnParseOutreach.innerHTML = ` PARSING JOB DETAILS... `;
try {
const resp = await fetch("/api/outreach/parse-jd", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
text: text || null,
url: url || null,
user_external_id: state.userId,
resume_id: resumeId
})
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.detail || "Failed to parse job posting");
}
const data = await resp.json();
const parsed = data.parsed || {};
state.outreachParsedJD = parsed;
// Populate Step 2 Fields
dom.outreachFieldCompany.value = parsed.company_name || "";
dom.outreachFieldEmail.value = parsed.hr_email || "";
dom.outreachFieldPhone.value = parsed.contact_phone || "";
dom.outreachFieldLocation.value = parsed.location || "";
dom.outreachFieldExp.value = parsed.experience_required || "";
dom.outreachFieldDays.value = parsed.working_days || "";
// Missing email warning indicator
if (!parsed.hr_email) {
dom.outreachEmailMissingNote.classList.remove("hidden");
} else {
dom.outreachEmailMissingNote.classList.add("hidden");
}
// Role fit recommendation banner
if (parsed.recommended_role) {
dom.outreachRoleRecommendationBox.classList.remove("hidden");
dom.outreachRoleRecommendationText.textContent = parsed.recommendation_reason || `Closest match based on your resume: ${parsed.recommended_role}`;
} else {
dom.outreachRoleRecommendationBox.classList.add("hidden");
}
// Render positions radio list
const positions = parsed.open_positions && parsed.open_positions.length > 0
? parsed.open_positions
: [(parsed.company_name ? `${parsed.company_name} Role` : "Software Engineer")];
state.outreachSelectedRole = parsed.recommended_role || positions[0];
dom.outreachRoleOptionsContainer.innerHTML = positions.map((pos, idx) => {
const isSelected = pos === state.outreachSelectedRole;
const isRecommended = pos === parsed.recommended_role;
return `
${escapeHtml(pos)}
${isRecommended ? 'RECOMMENDED FIT ' : ''}
`;
}).join("");
// Attach click listeners to role cards
const roleCards = dom.outreachRoleOptionsContainer.querySelectorAll(".role-option-card");
roleCards.forEach(card => {
card.addEventListener("click", () => {
const radio = card.querySelector("input[type='radio']");
if (radio) radio.checked = true;
roleCards.forEach(c => c.classList.remove("selected"));
card.classList.add("selected");
state.outreachSelectedRole = card.getAttribute("data-role");
});
});
// Reveal Step 2
dom.outreachStep2Empty.classList.add("hidden");
dom.outreachStep2Content.classList.remove("hidden");
setOutreachStep(2);
showToast(`Extracted ${positions.length} position(s) from posting`, "success");
} catch (err) {
showToast(err.message, "error");
} finally {
dom.btnParseOutreach.disabled = false;
dom.btnParseOutreach.innerHTML = `document_scanner PARSE JOB POSTING DETAILS `;
}
}
async function draftOutreachEmail() {
const role = state.outreachSelectedRole || dom.outreachFieldCompany.value || "Role";
const resumeSelectVal = dom.outreachResumeSelectPicker ? dom.outreachResumeSelectPicker.value : "active";
const resumeId = resumeSelectVal !== "active" ? resumeSelectVal : (state.activeResumeId || null);
const parsed_jd = {
company_name: (dom.outreachFieldCompany.value || "").trim(),
hr_email: (dom.outreachFieldEmail.value || "").trim() || null,
contact_phone: (dom.outreachFieldPhone.value || "").trim() || null,
location: (dom.outreachFieldLocation.value || "").trim() || null,
experience_required: (dom.outreachFieldExp.value || "").trim() || null,
working_days: (dom.outreachFieldDays.value || "").trim() || null,
open_positions: state.outreachParsedJD?.open_positions || [role],
raw_text: dom.outreachJdText.value || ""
};
dom.btnDraftOutreach.disabled = true;
dom.outreachStep3Empty.classList.add("hidden");
dom.outreachStep3Content.classList.add("hidden");
dom.outreachStep3Loading.classList.remove("hidden");
try {
const resp = await fetch("/api/outreach/draft-email", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
user_external_id: state.userId,
resume_id: resumeId,
selected_role: role,
parsed_jd: parsed_jd,
applicant_name: dom.outreachApplicantName.value.trim() || undefined,
custom_instructions: dom.outreachCustomInstructions.value.trim() || undefined
})
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.detail || "Failed to draft application email");
}
const data = await resp.json();
state.outreachDraft = data;
// Populate Step 3 inputs
dom.outreachEmailSubject.value = data.subject || `Application for ${role}`;
dom.outreachEmailBody.value = data.body || "";
dom.outreachEmailRecipientConfirm.value = data.recipient || dom.outreachFieldEmail.value || "";
// Cliché check banner
if (data.cliches_detected && data.cliches_detected.length > 0) {
dom.outreachClicheStatusTag.textContent = `⚠️ ${data.cliches_detected.length} CLICHÉ(S) REVIEWED`;
dom.outreachClicheStatusTag.className = "cyber-badge cyber-badge-alert text-[10px]";
} else {
dom.outreachClicheStatusTag.textContent = "✓ ZERO CLICHÉS DETECTED";
dom.outreachClicheStatusTag.className = "cyber-badge cyber-badge-cyan text-[10px]";
}
// Fabrication Flags
const flags = data.flagged_fabrications || [];
if (flags.length > 0) {
dom.outreachFabricationSummaryTag.textContent = `⚠️ ${flags.length} UNGROUNDED TERM(S) FLAGGED`;
dom.outreachFabricationSummaryTag.className = "text-[10px] text-error font-bold";
dom.outreachFabricationFlagsList.innerHTML = flags.map(f => `
⚠️ '${escapeHtml(f.term)}'
UNGROUNDED IN RESUME
${escapeHtml(f.reason)}
"${escapeHtml(f.snippet)}"
`).join("");
} else {
dom.outreachFabricationSummaryTag.textContent = "✓ 100% GROUNDED IN RESUME";
dom.outreachFabricationSummaryTag.className = "text-[10px] text-success font-bold";
dom.outreachFabricationFlagsList.innerHTML = `
All technical tools, skills, and qualifications cited are verified against your uploaded resume.
`;
}
// Secondary AI Detector Signal
if (data.ai_detector_score !== null && data.ai_detector_score !== undefined) {
dom.outreachDetectorScoreText.textContent = `Secondary AI Signal: ${Math.round(data.ai_detector_score * 100)}%`;
} else {
dom.outreachDetectorScoreText.textContent = "Secondary AI Signal: Offline Heuristic Active";
}
// Reveal Step 3 & activate Step 4
dom.outreachStep3Loading.classList.add("hidden");
dom.outreachStep3Content.classList.remove("hidden");
setOutreachStep(3);
showToast("Application email drafted successfully!", "success");
} catch (err) {
dom.outreachStep3Loading.classList.add("hidden");
dom.outreachStep3Empty.classList.remove("hidden");
showToast(err.message, "error");
} finally {
dom.btnDraftOutreach.disabled = false;
}
}
async function checkGmailStatus() {
try {
const resp = await fetch(`/api/outreach/gmail/status?user_external_id=${encodeURIComponent(state.userId)}`);
if (!resp.ok) return;
const data = await resp.json();
state.gmailConnected = data.connected;
if (data.connected) {
if (dom.outreachAuthCardDot) dom.outreachAuthCardDot.className = "w-2.5 h-2.5 rounded-full bg-success";
if (dom.outreachAuthCardStatus) dom.outreachAuthCardStatus.textContent = `Connected as ${data.email || 'Authenticated User'}`;
if (dom.outreachAuthCardDesc) dom.outreachAuthCardDesc.textContent = "Scope: https://www.googleapis.com/auth/gmail.send (Send-only access active).";
if (dom.btnOutreachConnectGmail) dom.btnOutreachConnectGmail.classList.add("hidden");
if (dom.btnOutreachDisconnectGmail) dom.btnOutreachDisconnectGmail.classList.remove("hidden");
if (dom.outreachAuthIndicatorDot) dom.outreachAuthIndicatorDot.className = "w-2 h-2 rounded-full bg-success";
if (dom.outreachHeaderAuthText) {
dom.outreachHeaderAuthText.textContent = `GMAIL_ONLINE (${(data.email || '').split('@')[0] || 'ACTIVE'})`;
dom.outreachHeaderAuthText.className = "text-success font-bold";
}
} else {
if (dom.outreachAuthCardDot) dom.outreachAuthCardDot.className = "w-2.5 h-2.5 rounded-full bg-warning";
if (dom.outreachAuthCardStatus) dom.outreachAuthCardStatus.textContent = "Gmail Not Connected";
if (dom.outreachAuthCardDesc) dom.outreachAuthCardDesc.textContent = "Requires one-time OAuth authorization (https://www.googleapis.com/auth/gmail.send).";
if (dom.btnOutreachConnectGmail) dom.btnOutreachConnectGmail.classList.remove("hidden");
if (dom.btnOutreachDisconnectGmail) dom.btnOutreachDisconnectGmail.classList.add("hidden");
if (dom.outreachAuthIndicatorDot) dom.outreachAuthIndicatorDot.className = "w-2 h-2 rounded-full bg-on-surface-variant";
if (dom.outreachHeaderAuthText) {
dom.outreachHeaderAuthText.textContent = "GMAIL_OFFLINE";
dom.outreachHeaderAuthText.className = "text-on-surface-variant";
}
}
} catch (err) {
console.error("Failed to check Gmail status:", err);
}
}
async function sendOutreachApplication() {
const recipient = (dom.outreachEmailRecipientConfirm.value || "").trim();
const subject = (dom.outreachEmailSubject.value || "").trim();
const body = (dom.outreachEmailBody.value || "").trim();
if (!recipient || !recipient.includes("@")) {
showToast("Please enter a valid recipient HR email address", "warning");
dom.outreachEmailRecipientConfirm.focus();
return;
}
if (!subject) {
showToast("Email subject cannot be empty", "warning");
dom.outreachEmailSubject.focus();
return;
}
if (!body) {
showToast("Email body cannot be empty", "warning");
dom.outreachEmailBody.focus();
return;
}
if (!state.gmailConnected) {
showToast("Please click 'CONNECT GMAIL' to authorize your Gmail account before sending", "warning");
return;
}
dom.btnSendOutreach.disabled = true;
dom.btnSendOutreach.innerHTML = ` TRANSMITTING VIA GMAIL API... `;
try {
const resp = await fetch("/api/outreach/send", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
user_external_id: state.userId,
to_email: recipient,
subject: subject,
body: body,
company_name: (dom.outreachFieldCompany.value || "").trim() || undefined,
role_title: state.outreachSelectedRole || undefined,
attach_resume_id: dom.outreachAttachResumeCheckbox.checked ? (state.activeResumeId || null) : null,
log_to_tracker: dom.outreachLogTrackerCheckbox.checked
})
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.detail || "Failed to send email via Gmail API");
}
const data = await resp.json();
// Show success card
dom.outreachSendResultCard.classList.remove("hidden");
dom.outreachResultMsgId.textContent = data.message_id || `msg-${Date.now()}`;
dom.outreachResultTimestamp.textContent = new Date(data.timestamp).toLocaleString();
dom.outreachResultRecipient.textContent = data.recipient;
setOutreachStep(4);
showToast("Application email sent successfully via Gmail!", "success");
if (data.job_application_id) {
loadJobs();
}
} catch (err) {
showToast(err.message, "error");
} finally {
dom.btnSendOutreach.disabled = false;
dom.btnSendOutreach.innerHTML = `send SEND APPLICATION VIA GMAIL `;
}
}
async function handleResumeFileUpload(file) {
showToast(`Parsing resume (${file.name})...`, "info");
const formData = new FormData();
formData.append("user_external_id", state.userId);
formData.append("title", file.name.replace(/\.[^/.]+$/, ""));
formData.append("file", file);
try {
const resp = await fetch("/matcher/upload-resume", {
method: "POST",
body: formData
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.detail || "Failed to parse resume");
}
const data = await resp.json();
dom.matcherResumeText.value = data.resume_text;
dom.resumeTitleInput.value = data.title || file.name;
if (dom.atsResumeText) dom.atsResumeText.value = data.resume_text;
showToast(`Parsed ${data.char_count} characters from resume!`, "success");
loadResumes();
} catch (err) {
showToast(err.message, "error");
} finally {
dom.resumeFileInput.value = "";
}
}
async function loadResumes() {
try {
const resp = await fetch(`/matcher/resumes/${state.userId}`);
if (!resp.ok) return;
const resumes = await resp.json();
state.resumes = resumes;
const optionsHtml = `+ Upload / Enter New Resume ` +
resumes.map(r => `
${escapeHtml(r.title)} (${(r.resume_text || '').length} chars)
`).join("");
dom.resumeSelectPicker.innerHTML = optionsHtml;
if (dom.atsResumeSelectPicker) {
dom.atsResumeSelectPicker.innerHTML = optionsHtml;
}
if (dom.outreachResumeSelectPicker) {
dom.outreachResumeSelectPicker.innerHTML = `Active Selected Resume ` +
resumes.map(r => `
${escapeHtml(r.title)} (${(r.resume_text || '').length} chars)
`).join("");
}
if (resumes.length > 0 && !state.activeResumeId) {
state.activeResumeId = resumes[0].id;
dom.resumeSelectPicker.value = resumes[0].id;
dom.matcherResumeText.value = resumes[0].resume_text;
dom.resumeTitleInput.value = resumes[0].title;
if (dom.atsResumeText && !dom.atsResumeText.value) {
dom.atsResumeText.value = resumes[0].resume_text;
}
dom.btnDeleteActiveResume.classList.remove("hidden");
} else if (!resumes.length) {
dom.btnDeleteActiveResume.classList.add("hidden");
}
} catch (err) {
console.error("Failed to load resumes:", err);
}
}
function renderMatchResults(res) {
dom.matcherLoadingState.classList.add("hidden");
dom.matcherEmptyState.classList.add("hidden");
dom.matcherResultsView.classList.remove("hidden");
// Score Number & Class
dom.matchScoreVal.innerHTML = `${res.match_score}% `;
if (dom.gaugeCircle) {
dom.gaugeCircle.className = "gauge-circle";
if (res.match_score >= 80) dom.gaugeCircle.classList.add("score-strong");
else if (res.match_score >= 65) dom.gaugeCircle.classList.add("score-good");
else if (res.match_score >= 50) dom.gaugeCircle.classList.add("score-moderate");
else dom.gaugeCircle.classList.add("score-low");
}
// Verdict Pill
dom.matchVerdictPill.textContent = res.verdict.toUpperCase();
dom.matchVerdictPill.className = "px-4 py-1.5 border border-secondary text-secondary font-label-mono text-[11px] uppercase tracking-widest glow-secondary bg-secondary/10";
if (res.match_score < 50) {
dom.matchVerdictPill.className = "px-4 py-1.5 border border-error text-error font-label-mono text-[11px] uppercase tracking-widest glow-error bg-error/10";
}
// Meta & Summary
dom.matchTargetRole.textContent = res.role_title || "Job Position";
dom.matchTargetCompany.textContent = res.company || "Target Company";
dom.matchSummaryText.textContent = res.summary;
// Strengths
dom.matchStrengthsList.innerHTML = (res.key_strengths || [])
.map(s => `${escapeHtml(s)} `)
.join("");
// Experience fit
dom.matchExperienceFit.innerHTML = `Experience Fit: ${escapeHtml(res.experience_fit || 'Matches criteria')}`;
// Matched Skills Tags
dom.skillsMatchedTags.innerHTML = (res.skills_matched || []).length > 0
? res.skills_matched.map(s => `✓ ${escapeHtml(s)} `).join("")
: 'No direct skill matches identified ';
// Missing Skills Tags
dom.skillsMissingTags.innerHTML = (res.missing_skills || []).length > 0
? res.missing_skills.map(s => `✗ ${escapeHtml(s)} `).join("")
: 'No critical skill gaps detected! ';
// Recommendations
dom.matchRecommendationsList.innerHTML = (res.recommendations || [])
.map(r => `${escapeHtml(r)} `)
.join("");
// Tailored Pitch
dom.matchPitchText.textContent = `"${res.tailored_pitch || 'I am excited to apply for this role...'}"`;
}
// ============================================================================
// Job Application Tracker
// ============================================================================
function setupJobHandlers() {
dom.btnOpenJobModal.addEventListener("click", () => dom.jobModal.classList.remove("hidden"));
dom.btnCloseModal.addEventListener("click", () => dom.jobModal.classList.add("hidden"));
dom.btnCancelModal.addEventListener("click", () => dom.jobModal.classList.add("hidden"));
dom.jobSearchInput.addEventListener("input", filterAndRenderJobs);
dom.jobStatusFilter.addEventListener("change", (e) => {
state.activeJobFilter = e.target.value;
filterAndRenderJobs();
});
dom.jobSortSelect.addEventListener("change", (e) => {
state.activeJobSort = e.target.value;
filterAndRenderJobs();
});
dom.jobForm.addEventListener("submit", async (e) => {
e.preventDefault();
const company = document.getElementById("job-company").value.trim();
const role = document.getElementById("job-role").value.trim();
const url = document.getElementById("job-url").value.trim();
const followup = parseInt(document.getElementById("job-followup").value, 10) || 7;
const notes = document.getElementById("job-notes").value.trim();
try {
const resp = await fetch("/jobs", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
user_external_id: state.userId,
company: company,
role_title: role,
job_url: url || null,
follow_up_days: followup,
notes: notes || null
})
});
if (!resp.ok) throw new Error("Failed to create job application");
dom.jobModal.classList.add("hidden");
dom.jobForm.reset();
showToast(`Logged application for ${company}`, "success");
loadJobs();
} catch (err) {
showToast(err.message, "error");
}
});
}
async function loadJobs() {
try {
const [jobsResp, dueResp] = await Promise.all([
fetch(`/jobs/${state.userId}`),
fetch(`/jobs/${state.userId}/due-followups`)
]);
const jobs = jobsResp.ok ? await jobsResp.json() : [];
const dueJobs = dueResp.ok ? await dueResp.json() : [];
state.jobs = jobs;
state.dueJobs = dueJobs;
dom.statTotalJobs.textContent = jobs.length;
dom.statActiveJobs.textContent = jobs.filter(j => ["applied", "interview", "no_response"].includes(j.status)).length;
dom.statOfferJobs.textContent = jobs.filter(j => j.status === "offer").length;
if (dom.statRejectedJobs) dom.statRejectedJobs.textContent = jobs.filter(j => j.status === "rejected").length;
dom.statDueJobs.textContent = dueJobs.length;
if (dueJobs.length > 0) {
dom.jobsDueBadge.textContent = dueJobs.length;
dom.jobsDueBadge.classList.remove("hidden");
dom.jobsAlertBanner.classList.remove("hidden");
dom.jobsAlertText.innerHTML = `${dueJobs.length} application(s) due for follow-up: ` +
dueJobs.map(j => `${escapeHtml(j.company)} (${escapeHtml(j.role_title)})`).join(", ");
} else {
dom.jobsDueBadge.classList.add("hidden");
dom.jobsAlertBanner.classList.add("hidden");
}
filterAndRenderJobs();
} catch (err) {
console.error("Failed to load jobs:", err);
}
}
function filterAndRenderJobs() {
const query = (dom.jobSearchInput.value || "").toLowerCase().trim();
const filter = state.activeJobFilter;
const sort = state.activeJobSort;
let list = [...state.jobs];
// 1. Filter by Status
if (filter === "due") {
const dueIds = new Set(state.dueJobs.map(d => d.id));
list = list.filter(j => dueIds.has(j.id));
} else if (filter !== "all") {
list = list.filter(j => j.status === filter);
}
// 2. Search query
if (query) {
list = list.filter(j =>
(j.company || "").toLowerCase().includes(query) ||
(j.role_title || "").toLowerCase().includes(query) ||
(j.notes || "").toLowerCase().includes(query)
);
}
// 3. Sorting
if (sort === "newest") {
list.sort((a, b) => new Date(b.applied_date || 0) - new Date(a.applied_date || 0));
} else if (sort === "oldest") {
list.sort((a, b) => new Date(a.applied_date || 0) - new Date(b.applied_date || 0));
} else if (sort === "followup") {
list.sort((a, b) => new Date(a.follow_up_date || 0) - new Date(b.follow_up_date || 0));
} else if (sort === "score") {
list.sort((a, b) => (b.match_score || 0) - (a.match_score || 0));
} else if (sort === "company") {
list.sort((a, b) => (a.company || "").localeCompare(b.company || ""));
}
if (!list.length) {
dom.jobsList.innerHTML = `
work_off
${state.jobs.length === 0 ? "NO_APPLICATIONS_LOGGED_YET" : "NO_MATCHING_APPLICATIONS"}
${state.jobs.length === 0 ? "Click 'LOG_NEW_APPLICATION' above or use the Resume Matcher to track your first job." : "Try adjusting your search query or status filter."}
`;
return;
}
const statuses = [
{ key: "applied", label: "APPLIED" },
{ key: "interview", label: "INTERVIEWING" },
{ key: "offer", label: "OFFER" },
{ key: "rejected", label: "REJECTED" },
{ key: "no_response", label: "NO RESPONSE" }
];
dom.jobsList.innerHTML = list.map((j, idx) => {
let scoreBadge = '';
if (j.match_score != null && j.match_score > 0) {
const cls = j.match_score >= 80 ? 'job-score-strong' : j.match_score >= 65 ? 'job-score-good' : j.match_score >= 50 ? 'job-score-moderate' : 'job-score-low';
scoreBadge = `🎯 ${Math.round(j.match_score)}% FIT `;
}
let statusPill = '';
if (j.status === 'offer') {
statusPill = ` OFFER_RECEIVED `;
} else if (j.status === 'interview') {
statusPill = ` INTERVIEW_ACTIVE `;
} else if (j.status === 'applied') {
statusPill = ` APPLIED `;
} else if (j.status === 'rejected') {
statusPill = ` REJECTED `;
} else {
statusPill = ` AWAITING `;
}
const entityCode = `01${(idx + 1).toString().padStart(3, '0')}`;
return `
${j.tailored_pitch ? `
${escapeHtml(j.tailored_pitch)}
` : ''}
${j.notes ? `
${escapeHtml(j.notes).replace(/\n/g, ' ')}
` : ''}
`;
}).join("");
}
window.copyJobPitch = function(jobId) {
const el = document.getElementById(`pitch-text-${jobId}`);
if (el) {
navigator.clipboard.writeText(el.innerText);
showToast("Pitch copied to clipboard!", "success");
}
};
window.updateJobStatus = async function(jobId, newStatus) {
const cardEl = document.getElementById(`job-card-${jobId}`);
if (cardEl) {
cardEl.classList.remove("animate-flash-red", "animate-flash-green", "animate-flash-magenta", "job-card-offer", "job-card-interview", "job-card-rejected", "job-card-applied");
void cardEl.offsetWidth; // Trigger DOM reflow for restart
if (newStatus === "rejected") {
cardEl.classList.add("job-card-rejected");
showToast("Status updated to Rejected", "error");
} else if (newStatus === "offer") {
cardEl.classList.add("job-card-offer");
showToast("🎉 Status updated to Offer!", "success");
} else if (newStatus === "interview") {
cardEl.classList.add("job-card-interview");
showToast("Status updated to Interviewing", "info");
} else {
cardEl.classList.add("job-card-applied");
showToast(`Status updated to ${newStatus.replace('_', ' ')}`, "info");
}
}
try {
const resp = await fetch(`/jobs/${jobId}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ status: newStatus })
});
if (!resp.ok) throw new Error("Failed to update status");
setTimeout(loadJobs, 400);
} catch (err) {
showToast(err.message, "error");
}
};
window.deleteJob = async function(jobId) {
if (!confirm("Are you sure you want to delete this job application?")) return;
try {
const resp = await fetch(`/jobs/${jobId}`, { method: "DELETE" });
if (!resp.ok) throw new Error("Failed to delete job application");
showToast("Application deleted", "info");
loadJobs();
} catch (err) {
showToast(err.message, "error");
}
};
// ============================================================================
// AI Model & API Key Settings Handlers
// ============================================================================
function setupSettingsHandlers() {
if (dom.btnOpenSettings) {
dom.btnOpenSettings.addEventListener("click", () => {
loadSettings();
dom.settingsModal.classList.remove("hidden");
});
}
if (dom.btnCloseSettings) {
dom.btnCloseSettings.addEventListener("click", () => dom.settingsModal.classList.add("hidden"));
}
if (dom.btnCancelSettings) {
dom.btnCancelSettings.addEventListener("click", () => dom.settingsModal.classList.add("hidden"));
}
if (dom.settingsLlmProvider) {
dom.settingsLlmProvider.addEventListener("change", () => {
const provider = dom.settingsLlmProvider.value;
if (provider === "groq") {
dom.groqKeyGroup.classList.remove("hidden");
dom.anthropicKeyGroup.classList.add("hidden");
} else {
dom.groqKeyGroup.classList.add("hidden");
dom.anthropicKeyGroup.classList.remove("hidden");
}
});
}
if (dom.settingsForm) {
dom.settingsForm.addEventListener("submit", async (e) => {
e.preventDefault();
const provider = dom.settingsLlmProvider.value;
const groqKey = dom.settingsGroqKey.value.trim();
const anthropicKey = dom.settingsAnthropicKey.value.trim();
try {
const resp = await fetch("/api/settings", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
llm_provider: provider,
groq_api_key: groqKey || undefined,
anthropic_api_key: anthropicKey || undefined
})
});
if (!resp.ok) throw new Error("Failed to save settings");
dom.settingsModal.classList.add("hidden");
showToast("AI API Key saved successfully!", "success");
} catch (err) {
showToast(`Settings error: ${err.message}`, "error");
}
});
}
}
async function loadSettings() {
try {
const resp = await fetch("/api/settings");
if (!resp.ok) return;
const data = await resp.json();
if (dom.settingsLlmProvider) dom.settingsLlmProvider.value = data.llm_provider || "groq";
if (dom.settingsGroqKey) {
dom.settingsGroqKey.placeholder = data.groq_api_key_configured
? "•••••••••••• (API Key Active & Configured)"
: "Enter your Groq key (e.g. gsk_...)";
}
if (dom.settingsAnthropicKey) {
dom.settingsAnthropicKey.placeholder = data.anthropic_api_key_configured
? "•••••••••••• (API Key Active & Configured)"
: "Enter your Anthropic key (sk-ant-...)";
}
if (dom.settingsLlmProvider) {
if (data.llm_provider === "anthropic") {
dom.groqKeyGroup.classList.add("hidden");
dom.anthropicKeyGroup.classList.remove("hidden");
} else {
dom.groqKeyGroup.classList.remove("hidden");
dom.anthropicKeyGroup.classList.add("hidden");
}
}
} catch (err) {
console.error("Failed to load settings:", err);
}
}
// ============================================================================
// Utilities & Health Check
// ============================================================================
async function checkHealth() {
try {
const resp = await fetch("/health");
if (resp.ok) {
dom.serverStatus.classList.add("online");
dom.serverStatus.title = "FastAPI backend online";
} else {
dom.serverStatus.classList.remove("online");
dom.serverStatus.title = "FastAPI backend error";
}
} catch {
dom.serverStatus.classList.remove("online");
dom.serverStatus.title = "Backend unreachable";
}
}
function showToast(message, type = "info") {
const toast = document.createElement("div");
toast.className = `toast toast-${type}`;
const icon = type === "success" ? "✅" : type === "error" ? "❌" : type === "warning" ? "⚠️" : "💡";
toast.innerHTML = `${icon} ${escapeHtml(message)} `;
dom.toastContainer.appendChild(toast);
setTimeout(() => {
toast.style.opacity = "0";
setTimeout(() => toast.remove(), 300);
}, 4500);
}
function escapeHtml(text) {
if (!text) return "";
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}