// ── Admin Panel JavaScript ──
const BASE_PATH = window.BASE_PATH || '';
function authHeaders() {
return { 'Authorization': 'Bearer ' + localStorage.getItem('admin_token') };
}
async function apiGet(url) {
const resp = await fetch(BASE_PATH + url, { headers: authHeaders() });
if (resp.status === 401) { window.location.href = BASE_PATH + '/admin/login'; return null; }
return resp.json();
}
async function apiPost(url, data) {
const resp = await fetch(BASE_PATH + url, {
method: 'POST', headers: { ...authHeaders(), 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (resp.status === 401) { window.location.href = BASE_PATH + '/admin/login'; return null; }
return resp.json();
}
async function apiPut(url, data) {
const resp = await fetch(BASE_PATH + url, {
method: 'PUT', headers: { ...authHeaders(), 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (resp.status === 401) { window.location.href = BASE_PATH + '/admin/login'; return null; }
return resp.json();
}
async function apiDelete(url) {
const resp = await fetch(BASE_PATH + url, { method: 'DELETE', headers: authHeaders() });
if (resp.status === 401) { window.location.href = BASE_PATH + '/admin/login'; return null; }
return resp.json();
}
function showToast(msg, type = 'success') {
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.textContent = msg;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
}
function openModal(id) { document.getElementById(id).classList.add('open'); }
function closeModal(id) { document.getElementById(id).classList.remove('open'); }
// ── Page Detection & Init ──
document.addEventListener('DOMContentLoaded', () => {
const path = window.location.pathname;
if (path.includes('/admin/dashboard')) loadDashboard();
else if (path.includes('/admin/profile')) loadProfile();
else if (path.includes('/admin/education')) loadEducation();
else if (path.includes('/admin/experience')) loadExperience();
else if (path.includes('/admin/skills')) loadSkills();
else if (path.includes('/admin/tokens')) loadTokens();
else if (path.includes('/admin/messages')) loadMessages();
else if (path.includes('/admin/llm-config')) loadLLMConfig();
// Highlight active nav
document.querySelectorAll('.admin-sidebar nav a').forEach(a => {
if (a.getAttribute('href') === path) a.classList.add('active');
});
// Load unread badge on all admin pages
loadUnreadBadge();
});
function adminLogout() {
localStorage.removeItem('admin_token');
window.location.href = BASE_PATH + '/admin/login';
}
// ── Dashboard ──
async function loadDashboard() {
const [skills, edu, exp, tokens] = await Promise.all([
apiGet('/api/admin/skills'),
apiGet('/api/admin/education'),
apiGet('/api/admin/experience'),
apiGet('/api/admin/tokens'),
]);
document.getElementById('stat-skills').textContent = skills ? skills.length : 0;
document.getElementById('stat-education').textContent = edu ? edu.length : 0;
document.getElementById('stat-experience').textContent = exp ? exp.length : 0;
document.getElementById('stat-tokens').textContent = tokens ? tokens.filter(t => t.is_active).length : 0;
}
// ── Profile ──
async function loadProfile() {
const data = await apiGet('/api/admin/profile');
if (!data) return;
['name','phone','location','birthday','party','education_level','email','self_summary'].forEach(f => {
const el = document.getElementById('field-' + f);
if (el) el.value = data[f] || '';
});
if (data.photo_url) {
document.getElementById('photo-preview').innerHTML = `
`;
}
}
async function saveProfile() {
const fields = {};
['name','phone','location','birthday','party','education_level','email','self_summary'].forEach(f => {
const el = document.getElementById('field-' + f);
if (el) fields[f] = el.value;
});
const result = await apiPut('/api/admin/profile', fields);
if (result) showToast('Profile saved');
}
async function uploadPhoto() {
const input = document.getElementById('photo-input');
if (!input.files[0]) return;
const formData = new FormData();
formData.append('file', input.files[0]);
const resp = await fetch(BASE_PATH + '/api/admin/profile/photo', {
method: 'POST', headers: authHeaders(), body: formData,
});
if (resp.status === 401) { window.location.href = BASE_PATH + '/admin/login'; return; }
const data = await resp.json();
if (data.photo_url) {
document.getElementById('photo-preview').innerHTML = `
`;
showToast('Photo uploaded');
}
}
// ── Education CRUD ──
let educationList = [];
async function loadEducation() {
educationList = await apiGet('/api/admin/education') || [];
renderEducation();
}
function renderEducation() {
const tbody = document.getElementById('education-tbody');
tbody.innerHTML = educationList.map(e => `
| ${e.start_date} - ${e.end_date} |
${e.school} |
${e.major} |
${e.degree} |
|
`).join('');
}
function showEducationModal(id = null) {
const item = id ? educationList.find(e => e.id === id) : {};
document.getElementById('edu-id').value = id || '';
['start_date','end_date','school','major','degree','details','sort_order'].forEach(f => {
document.getElementById('edu-' + f).value = (item && item[f]) || '';
});
openModal('education-modal');
}
function editEducation(id) { showEducationModal(id); }
async function saveEducation() {
const id = document.getElementById('edu-id').value;
const data = {};
['start_date','end_date','school','major','degree','details','sort_order'].forEach(f => {
const val = document.getElementById('edu-' + f).value;
data[f] = f === 'sort_order' ? parseInt(val) || 0 : val;
});
if (id) await apiPut(`/api/admin/education/${id}`, data);
else await apiPost('/api/admin/education', data);
closeModal('education-modal');
showToast('Saved');
loadEducation();
}
async function deleteEducation(id) {
if (!confirm('Delete this entry?')) return;
await apiDelete(`/api/admin/education/${id}`);
showToast('Deleted');
loadEducation();
}
// ── Experience CRUD ──
let experienceList = [];
async function loadExperience() {
experienceList = await apiGet('/api/admin/experience') || [];
renderExperience();
}
function renderExperience() {
const tbody = document.getElementById('experience-tbody');
tbody.innerHTML = experienceList.map(e => `
| ${e.start_date} - ${e.end_date} |
${e.company} |
${e.position} |
|
`).join('');
}
function showExperienceModal(id = null) {
const item = id ? experienceList.find(e => e.id === id) : {};
document.getElementById('exp-id').value = id || '';
['start_date','end_date','company','position','company_intro','responsibilities','achievements','sort_order'].forEach(f => {
document.getElementById('exp-' + f).value = (item && item[f]) || '';
});
openModal('experience-modal');
}
function editExperience(id) { showExperienceModal(id); }
async function saveExperience() {
const id = document.getElementById('exp-id').value;
const data = {};
['start_date','end_date','company','position','company_intro','responsibilities','achievements','sort_order'].forEach(f => {
const val = document.getElementById('exp-' + f).value;
data[f] = f === 'sort_order' ? parseInt(val) || 0 : val;
});
if (id) await apiPut(`/api/admin/experience/${id}`, data);
else await apiPost('/api/admin/experience', data);
closeModal('experience-modal');
showToast('Saved');
loadExperience();
}
async function deleteExperience(id) {
if (!confirm('Delete this entry?')) return;
await apiDelete(`/api/admin/experience/${id}`);
showToast('Deleted');
loadExperience();
}
// ── Skills CRUD ──
let skillsList = [];
async function loadSkills() {
skillsList = await apiGet('/api/admin/skills') || [];
renderSkills();
}
function renderSkills() {
const tbody = document.getElementById('skills-tbody');
tbody.innerHTML = skillsList.map(s => `
| ${s.category} |
${s.content.substring(0, 60)}${s.content.length > 60 ? '...' : ''} |
|
`).join('');
}
function showSkillModal(id = null) {
const item = id ? skillsList.find(s => s.id === id) : {};
document.getElementById('skill-id').value = id || '';
['category','content','sort_order'].forEach(f => {
document.getElementById('skill-' + f).value = (item && item[f]) || '';
});
openModal('skill-modal');
}
function editSkill(id) { showSkillModal(id); }
async function saveSkill() {
const id = document.getElementById('skill-id').value;
const data = {};
['category','content','sort_order'].forEach(f => {
const val = document.getElementById('skill-' + f).value;
data[f] = f === 'sort_order' ? parseInt(val) || 0 : val;
});
if (id) await apiPut(`/api/admin/skills/${id}`, data);
else await apiPost('/api/admin/skills', data);
closeModal('skill-modal');
showToast('Saved');
loadSkills();
}
async function deleteSkill(id) {
if (!confirm('Delete this entry?')) return;
await apiDelete(`/api/admin/skills/${id}`);
showToast('Deleted');
loadSkills();
}
// ── Access Tokens ──
let tokensList = [];
async function loadTokens() {
tokensList = await apiGet('/api/admin/tokens') || [];
renderTokens();
}
function renderTokens() {
const tbody = document.getElementById('tokens-tbody');
tbody.innerHTML = tokensList.map(t => `
${t.token.substring(0, 12)}... |
${t.note || '-'} |
${t.used_questions || 0} / ${t.max_questions || 10} |
${t.is_active ? 'Active' : 'Inactive'} |
${t.created_at ? new Date(t.created_at).toLocaleString() : '-'} |
|
`).join('');
}
async function generateToken() {
const note = prompt('Token note (optional):') || '';
const result = await apiPost('/api/admin/tokens', { note });
if (result && result.token) {
showToast('Token created: ' + result.token);
navigator.clipboard.writeText(result.token).catch(() => {});
loadTokens();
}
}
function copyToken(token) {
navigator.clipboard.writeText(token).then(() => showToast('Copied!')).catch(() => showToast('Copy failed', 'error'));
}
function editToken(id) {
const t = tokensList.find(x => x.id === id);
if (!t) return;
document.getElementById('token-edit-id').value = id;
document.getElementById('token-edit-note').value = t.note || '';
document.getElementById('token-edit-max').value = t.max_questions || 10;
document.getElementById('token-edit-used').value = t.used_questions || 0;
document.getElementById('token-edit-active').value = t.is_active ? 'true' : 'false';
openModal('token-modal');
}
async function saveToken() {
const id = document.getElementById('token-edit-id').value;
const data = {
note: document.getElementById('token-edit-note').value,
max_questions: parseInt(document.getElementById('token-edit-max').value) || 10,
used_questions: parseInt(document.getElementById('token-edit-used').value) || 0,
is_active: document.getElementById('token-edit-active').value === 'true',
};
await apiPut(`/api/admin/tokens/${id}`, data);
closeModal('token-modal');
showToast('Token updated');
loadTokens();
}
async function deleteToken(id) {
if (!confirm('Delete this token?')) return;
await apiDelete(`/api/admin/tokens/${id}`);
showToast('Deleted');
loadTokens();
}
// ── LLM Config ──
async function loadLLMConfig() {
const data = await apiGet('/api/admin/llm-config');
if (!data) return;
['api_url','api_key','model_name','system_prompt'].forEach(f => {
const el = document.getElementById('llm-' + f);
if (el) el.value = data[f] || '';
});
const mt = document.getElementById('llm-max_tokens');
if (mt) mt.value = data.max_tokens || 2048;
const mq = document.getElementById('llm-max_questions');
if (mq) mq.value = data.max_questions_per_session || 10;
const temp = document.getElementById('llm-temperature');
if (temp) {
temp.value = data.temperature || 0.7;
document.getElementById('temp-value').textContent = temp.value;
}
}
function setPreset(name) {
const presets = {
'qwen': { url: 'https://dashscope.aliyuncs.com/compatible-mode', model: 'qwen-plus' },
'deepseek': { url: 'https://api.deepseek.com', model: 'deepseek-chat' },
'openai': { url: 'https://api.openai.com', model: 'gpt-4o' },
'zhipu': { url: 'https://open.bigmodel.cn/api/paas', model: 'glm-4-flash' },
};
const p = presets[name];
if (p) {
document.getElementById('llm-api_url').value = p.url;
document.getElementById('llm-model_name').value = p.model;
}
}
async function saveLLMConfig() {
const data = {
api_url: document.getElementById('llm-api_url').value,
api_key: document.getElementById('llm-api_key').value,
model_name: document.getElementById('llm-model_name').value,
max_tokens: parseInt(document.getElementById('llm-max_tokens').value) || 2048,
temperature: parseFloat(document.getElementById('llm-temperature').value) || 0.7,
max_questions_per_session: parseInt(document.getElementById('llm-max_questions').value) || 10,
system_prompt: document.getElementById('llm-system_prompt').value,
};
const result = await apiPut('/api/admin/llm-config', data);
if (result) showToast('LLM config saved');
}
async function testLLMConnection() {
// Auto-save before testing
await saveLLMConfig();
showToast('Testing connection...');
try {
const result = await apiPost('/api/admin/llm-config/test', {});
if (result && result.result) {
showToast('Response: ' + result.result.substring(0, 100), 'success');
} else {
showToast('No response from LLM', 'error');
}
} catch (e) {
showToast('Test failed: ' + e.message, 'error');
}
}
// ── Recruiter Messages ──
async function loadUnreadBadge() {
try {
const data = await apiGet('/api/admin/messages/unread-count');
if (!data) return;
const badges = document.querySelectorAll('.nav-badge');
badges.forEach(badge => {
if (data.count > 0) {
badge.textContent = data.count;
badge.style.display = 'inline-block';
} else {
badge.style.display = 'none';
}
});
} catch (e) { /* ignore */ }
}
let messagesList = [];
async function loadMessages() {
messagesList = await apiGet('/api/admin/messages') || [];
renderMessages();
}
function renderMessages() {
const container = document.getElementById('messages-list');
const noMsg = document.getElementById('no-messages');
if (!messagesList.length) {
container.innerHTML = '';
noMsg.style.display = 'block';
return;
}
noMsg.style.display = 'none';
container.innerHTML = messagesList.map(m => `
${m.visitor_label || 'Unknown'}
${m.intent ? `${m.intent}` : ''}
${!m.is_read ? 'NEW' : ''}
${m.created_at ? new Date(m.created_at).toLocaleString() : ''}
${m.company ? `
Company: ${m.company}
` : ''}
${m.contact ? `
Contact: ${m.contact}
` : ''}
${m.summary ? `
${m.summary}
` : ''}
${!m.is_read ? `` : ''}
`).join('');
}
async function markRead(id) {
await apiPut(`/api/admin/messages/${id}/read`, {});
loadMessages();
loadUnreadBadge();
}
async function markAllRead() {
await apiPut('/api/admin/messages/read-all', {});
showToast('All marked as read');
loadMessages();
loadUnreadBadge();
}
async function deleteMessage(id) {
if (!confirm('Delete this message?')) return;
await apiDelete(`/api/admin/messages/${id}`);
showToast('Deleted');
loadMessages();
loadUnreadBadge();
}