// ── Chat Widget JavaScript ── // NOTE: Uses BASE_PATH from main.js (loaded before this file on index.html) let chatSessionId = sessionStorage.getItem('chat_session'); if (!chatSessionId) { chatSessionId = 'sess-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10); sessionStorage.setItem('chat_session', chatSessionId); } let chatOpen = false; let isSending = false; let historyLoaded = false; let usedQuestions = 0; let maxQuestions = 10; let limitReached = false; let isAnonymous = false; document.addEventListener('DOMContentLoaded', () => { const fab = document.getElementById('chat-fab'); const panel = document.getElementById('chat-panel'); const closeBtn = document.getElementById('chat-close'); const sendBtn = document.getElementById('chat-send'); const input = document.getElementById('chat-input'); const fileInput = document.getElementById('chat-file'); const uploadBtn = document.getElementById('chat-upload'); const removeFileBtn = document.getElementById('remove-file'); loadChatConfig(); fab.addEventListener('click', () => { chatOpen = !chatOpen; panel.classList.toggle('open', chatOpen); if (chatOpen && !historyLoaded) { loadChatHistory(); historyLoaded = true; } if (chatOpen) input.focus(); }); closeBtn.addEventListener('click', () => { chatOpen = false; panel.classList.remove('open'); }); sendBtn.addEventListener('click', sendMessage); input.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }); uploadBtn.addEventListener('click', () => fileInput.click()); fileInput.addEventListener('change', () => { const preview = document.getElementById('file-preview'); const nameEl = document.getElementById('file-name'); if (fileInput.files.length > 0) { nameEl.textContent = fileInput.files[0].name; preview.classList.add('show'); } }); removeFileBtn.addEventListener('click', () => { fileInput.value = ''; document.getElementById('file-preview').classList.remove('show'); }); }); async function loadChatConfig() { try { const resp = await fetch(BASE_PATH + '/api/chat/config'); if (resp.ok) { const data = await resp.json(); maxQuestions = data.max_questions || 10; usedQuestions = data.used_questions || 0; isAnonymous = !!data.anonymous; limitReached = usedQuestions >= maxQuestions; // For anonymous users, also check from session storage if (isAnonymous) { const storedUsed = parseInt(sessionStorage.getItem('anon_used_q') || '0'); usedQuestions = storedUsed; limitReached = usedQuestions >= maxQuestions; } } } catch (e) { /* use defaults */ } const greetingEl = document.getElementById('greeting-msg'); if (greetingEl) { if (isAnonymous) { greetingEl.textContent = `您好!我是AI职业助手。您当前为匿名访问,可提问${maxQuestions}次。如需更多次数,请联系管理员获取访问令牌。`; } else { greetingEl.textContent = `您好!我是AI职业助手,可以介绍候选人的背景、技能和经历,也支持上传JD进行匹配分析。您可提问${maxQuestions}次。`; } } updateCounter(); updateInputState(); } async function loadChatHistory() { try { const resp = await fetch(BASE_PATH + `/api/chat/history/${chatSessionId}`); if (!resp.ok) return; const history = await resp.json(); for (const msg of history) { appendBubble(msg.role, msg.content); } scrollToBottom(); } catch (e) { console.error('Failed to load history:', e); } } async function sendMessage() { if (isSending || limitReached) return; const input = document.getElementById('chat-input'); const fileInput = document.getElementById('chat-file'); const message = input.value.trim(); const file = fileInput.files[0] || null; if (!message && !file) return; // Show user message let displayMsg = message; if (file) { displayMsg = message ? `${message}\n[附件: ${file.name}]` : `[附件: ${file.name}]`; } appendBubble('user', displayMsg); usedQuestions++; if (isAnonymous) { sessionStorage.setItem('anon_used_q', String(usedQuestions)); } updateCounter(); // Clear inputs input.value = ''; fileInput.value = ''; document.getElementById('file-preview').classList.remove('show'); // Show typing indicator const typing = document.getElementById('typing-indicator'); typing.classList.add('show'); scrollToBottom(); isSending = true; try { const formData = new FormData(); formData.append('session_id', chatSessionId); formData.append('message', message); if (file) formData.append('file', file); const response = await fetch(BASE_PATH + '/api/chat', { method: 'POST', body: formData, }); typing.classList.remove('show'); if (!response.ok) { appendBubble('assistant', '抱歉,出现了错误,请稍后再试。'); isSending = false; return; } const bubble = appendBubble('assistant', ''); const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; let fullText = ''; while (true) { const { value, done } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop(); for (const line of lines) { const trimmed = line.trim(); if (!trimmed.startsWith('data:')) continue; const dataStr = trimmed.slice(5).trim(); if (!dataStr) continue; try { const data = JSON.parse(dataStr); if (data.limit_reached) limitReached = true; if (data.done) continue; if (data.content) { fullText += data.content; bubble.textContent = fullText; scrollToBottom(); } } catch (e) { /* skip */ } } } // Check if limit reached after this question if (usedQuestions >= maxQuestions) { limitReached = true; } updateInputState(); } catch (e) { document.getElementById('typing-indicator').classList.remove('show'); appendBubble('assistant', '网络异常,请稍后再试。'); } isSending = false; } function updateCounter() { let counter = document.getElementById('question-counter'); if (!counter) { counter = document.createElement('div'); counter.id = 'question-counter'; counter.style.cssText = 'text-align:center;font-size:12px;color:#94a3b8;padding:4px 0;border-bottom:1px solid #e2e8f0;'; const header = document.querySelector('.chat-header'); if (header) header.parentNode.insertBefore(counter, header.nextSibling); } const remaining = maxQuestions - usedQuestions; const label = isAnonymous ? '匿名提问剩余' : '剩余提问次数'; counter.innerHTML = `${label}: ${remaining < 0 ? 0 : remaining} / ${maxQuestions}`; if (isAnonymous) { counter.innerHTML += ` 输入令牌`; } counter.style.color = remaining <= 2 ? '#ef4444' : '#94a3b8'; } function updateInputState() { const input = document.getElementById('chat-input'); const sendBtn = document.getElementById('chat-send'); const uploadBtn = document.getElementById('chat-upload'); if (limitReached) { input.disabled = true; input.placeholder = isAnonymous ? '匿名次数已用完,请获取令牌继续' : '次数已用完,请联系管理员'; sendBtn.disabled = true; sendBtn.style.opacity = '0.5'; uploadBtn.disabled = true; uploadBtn.style.opacity = '0.5'; } } async function enterToken() { const token = prompt('请输入访问令牌 (Access Token):'); if (!token || !token.trim()) return; try { const resp = await fetch(BASE_PATH + '/api/verify-token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: token.trim() }), }); if (resp.ok) { window.location.reload(); } else { alert('无效或已过期的访问令牌'); } } catch (e) { alert('网络错误,请稍后重试'); } } function appendBubble(role, text) { const container = document.getElementById('chat-messages'); const bubble = document.createElement('div'); bubble.className = `message-bubble message-${role}`; bubble.textContent = text; container.appendChild(bubble); scrollToBottom(); return bubble; } function scrollToBottom() { const container = document.getElementById('chat-messages'); container.scrollTop = container.scrollHeight; }