/** * 光湖·内测 · 前端逻辑 * 登录 + 聊天SSE流 + 自动翻转 + 工具调用 * 无框架 · 纯 Vanilla JS */ (function(){ 'use strict'; // ─── 配置 ─── const API_BASE = '/hli/ftchat'; const LS_PREFIX = 'ftchat_'; const SESSION_THRESHOLD = 50; // 超过N条自动翻转 // ─── 状态 ─── let state = { token: null, email: null, userHash: null, slotIndex: null, messages: [], currentStream: null, // AbortController isSending: false, isLoggedIn: false, hasCodeSent: false, currentEmail: '' }; // ─── DOM 引用 ─── const $ = (id) => document.getElementById(id); const loginScreen = $('loginScreen'); const chatScreen = $('chatScreen'); const loginForm = $('loginForm'); const emailInput = $('emailInput'); const codeInput = $('codeInput'); const codeGroup = $('codeGroup'); const loginBtn = $('loginBtn'); const loginStatus = $('loginStatus'); const slotInfo = $('slotInfo'); const chatMessages = $('chatMessages'); const chatInput = $('chatInput'); const sendBtn = $('sendBtn'); const userEmail = $('userEmail'); // ─── 星场背景 ─── function initStarfield() { const canvas = document.getElementById('starfield'); if (!canvas) return; const ctx = canvas.getContext('2d'); let stars = []; let W, H; function resize() { W = canvas.width = window.innerWidth; H = canvas.height = window.innerHeight; } function createStars() { stars = []; const count = Math.min(Math.floor(W * H / 8000), 120); for (let i = 0; i < count; i++) { stars.push({ x: Math.random() * W, y: Math.random() * H, r: Math.random() * 1.5 + 0.5, a: Math.random() * 0.8 + 0.2, speed: Math.random() * 0.005 + 0.002 }); } } function draw() { ctx.clearRect(0, 0, W, H); const t = Date.now(); for (const s of stars) { const twinkle = Math.sin(t * s.speed) * 0.3 + 0.7; ctx.beginPath(); ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2); ctx.fillStyle = 'rgba(200, 200, 255, ' + (s.a * twinkle) + ')'; ctx.fill(); } requestAnimationFrame(draw); } window.addEventListener('resize', () => { resize(); createStars(); }); resize(); createStars(); draw(); } // ─── 登录逻辑 ─── function showStatus(msg, type) { loginStatus.style.display = 'block'; loginStatus.className = 'login-status ' + type; loginStatus.textContent = msg; } function updateSlotInfo() { fetch(API_BASE + '/status') .then(r => r.json()) .then(d => { if (d.slots_remaining !== undefined) { slotInfo.textContent = '剩余内测席位: ' + d.slots_remaining + ' / ' + d.slots_total; } }) .catch(() => {}); } window.handleLogin = function() { const email = emailInput.value.trim(); if (!email) { showStatus('请输入QQ邮箱', 'error'); return; } if (!state.hasCodeSent) { // 发送验证码 loginBtn.disabled = true; loginBtn.textContent = '发送中...'; state.currentEmail = email; fetch(API_BASE + '/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: email }) }) .then(r => r.json()) .then(d => { loginBtn.disabled = false; if (d.success) { state.hasCodeSent = true; codeGroup.style.display = 'block'; codeInput.focus(); loginBtn.textContent = '登录'; showStatus('验证码已发送(开发模式: 请查看服务器日志)', 'info'); emailInput.readOnly = true; } else { loginBtn.textContent = '发送验证码'; showStatus(d.message || '发送失败', 'error'); } }) .catch(err => { loginBtn.disabled = false; loginBtn.textContent = '发送验证码'; showStatus('网络错误: ' + err.message, 'error'); }); } else { // 验证码 const code = codeInput.value.trim(); if (!code || code.length !== 6) { showStatus('请输入6位验证码', 'error'); return; } loginBtn.disabled = true; loginBtn.textContent = '验证中...'; fetch(API_BASE + '/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: state.currentEmail, code: code }) }) .then(r => r.json()) .then(d => { loginBtn.disabled = false; if (d.success) { onLoginSuccess(d); } else { loginBtn.textContent = '登录'; showStatus(d.message || '验证失败', 'error'); codeInput.value = ''; codeInput.focus(); } }) .catch(err => { loginBtn.disabled = false; loginBtn.textContent = '登录'; showStatus('网络错误: ' + err.message, 'error'); }); } }; function onLoginSuccess(data) { state.token = data.token; state.userHash = data.user_hash; state.email = state.currentEmail || data.email; state.slotIndex = data.slot_index; state.isLoggedIn = true; localStorage.setItem(LS_PREFIX + 'token', data.token); localStorage.setItem(LS_PREFIX + 'email', state.email); localStorage.setItem(LS_PREFIX + 'hash', data.user_hash); userEmail.textContent = state.email; loginScreen.classList.remove('active'); chatScreen.classList.add('active'); loadConversation(); chatInput.focus(); } function logout() { state.token = null; state.isLoggedIn = false; state.hasCodeSent = false; state.messages = []; state.currentEmail = ''; state.email = null; localStorage.removeItem(LS_PREFIX + 'token'); localStorage.removeItem(LS_PREFIX + 'email'); localStorage.removeItem(LS_PREFIX + 'hash'); emailInput.value = ''; emailInput.readOnly = false; codeInput.value = ''; codeGroup.style.display = 'none'; loginBtn.textContent = '发送验证码'; loginBtn.disabled = false; loginStatus.style.display = 'none'; chatScreen.classList.remove('active'); loginScreen.classList.add('active'); } window.logout = logout; function restoreSession() { const token = localStorage.getItem(LS_PREFIX + 'token'); const email = localStorage.getItem(LS_PREFIX + 'email'); const hash = localStorage.getItem(LS_PREFIX + 'hash'); if (token && email && hash) { state.token = token; state.email = email; state.userHash = hash; state.isLoggedIn = true; userEmail.textContent = email; loginScreen.classList.remove('active'); chatScreen.classList.add('active'); loadConversation(); chatInput.focus(); return true; } return false; } // ─── 聊天逻辑 ─── function getStorageKey() { return LS_PREFIX + state.userHash + '_messages'; } function loadConversation() { try { const saved = localStorage.getItem(getStorageKey()); if (saved) { state.messages = JSON.parse(saved); renderMessages(); } else { state.messages = []; chatMessages.innerHTML = '
开始新对话 · 母模型已就绪
'; } } catch (e) { state.messages = []; } } function saveConversation() { try { localStorage.setItem(getStorageKey(), JSON.stringify(state.messages)); } catch (e) { // Storage full - trim history if (e.name === 'QuotaExceededError') { state.messages = state.messages.slice(-100); try { localStorage.setItem(getStorageKey(), JSON.stringify(state.messages)); } catch {} } } } function addMessage(role, content) { state.messages.push({ role: role, content: content }); saveConversation(); } function renderMessages(scrollToBottom) { chatMessages.innerHTML = ''; for (const msg of state.messages) { appendMessageElement(msg.role, msg.content, false); } if (scrollToBottom !== false) { chatMessages.scrollTop = chatMessages.scrollHeight; } } function appendMessageElement(role, content, isNew) { const div = document.createElement('div'); div.className = 'message ' + role; const contentDiv = document.createElement('div'); contentDiv.className = 'message-content'; contentDiv.textContent = content; div.appendChild(contentDiv); chatMessages.appendChild(div); if (isNew) chatMessages.scrollTop = chatMessages.scrollHeight; } function appendStreamingMessage(role) { const div = document.createElement('div'); div.className = 'message ' + role; const contentDiv = document.createElement('div'); contentDiv.className = 'message-content cursor-blink'; contentDiv.textContent = ''; div.appendChild(contentDiv); chatMessages.appendChild(div); chatMessages.scrollTop = chatMessages.scrollHeight; return contentDiv; } // ─── 发送消息 ─── window.sendMessage = function() { if (state.isSending) return; const text = chatInput.value.trim(); if (!text) return; chatInput.value = ''; chatInput.style.height = 'auto'; // Check auto-rotate if (state.messages.length >= SESSION_THRESHOLD) { rotateConversation(); } // Add user message addMessage('user', text); appendMessageElement('user', text, true); // Send state.isSending = true; sendBtn.disabled = true; const controller = new AbortController(); state.currentStream = controller; // Show typing indicator const typingDiv = document.createElement('div'); typingDiv.className = 'message assistant'; typingDiv.innerHTML = '
'; chatMessages.appendChild(typingDiv); chatMessages.scrollTop = chatMessages.scrollHeight; // Format messages for API - NO system prompt const apiMessages = state.messages.map(m => ({ role: m.role, content: m.content })); fetch(API_BASE + '/chat', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + state.token }, body: JSON.stringify({ messages: apiMessages, max_tokens: 1024, temperature: 0.7 }), signal: controller.signal }) .then(response => { // Remove typing indicator typingDiv.remove(); const contentDiv = appendStreamingMessage('assistant'); let fullContent = ''; const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; function readStream() { reader.read().then(({ done, value }) => { if (done) { contentDiv.classList.remove('cursor-blink'); state.isSending = false; sendBtn.disabled = false; state.currentStream = null; if (fullContent) { addMessage('assistant', fullContent); } return; } 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) continue; try { const parsed = JSON.parse(trimmed); if (parsed.delta) { fullContent += parsed.delta; contentDiv.textContent = fullContent; chatMessages.scrollTop = chatMessages.scrollHeight; } if (parsed.error) { fullContent += '\n[错误: ' + parsed.error + ']'; contentDiv.textContent = fullContent; } } catch (e) { // Non-JSON line (e.g. [DONE]) if (trimmed === 'data: [DONE]') { // Stream complete } } } readStream(); }).catch(err => { if (err.name !== 'AbortError') { contentDiv.textContent = '请求中断: ' + err.message; } contentDiv.classList.remove('cursor-blink'); state.isSending = false; sendBtn.disabled = false; state.currentStream = null; }); } readStream(); }) .catch(err => { typingDiv.remove(); state.isSending = false; sendBtn.disabled = false; state.currentStream = null; if (err.name !== 'AbortError') { const errorDiv = appendStreamingMessage('assistant'); errorDiv.textContent = '连接失败: ' + err.message; errorDiv.classList.remove('cursor-blink'); } }); }; function rotateConversation() { // Save current and start new state.messages = []; saveConversation(); renderMessages(); addMessage('system', '[对话已自动翻转,继续当前上下文]'); appendMessageElement('system-note', '—— 对话已自动翻转,继续当前上下文 ——', true); } window.newConversation = function() { if (state.messages.length > 0) { state.messages = []; saveConversation(); renderMessages(); chatMessages.innerHTML = '
新对话 · 母模型已就绪
'; } }; window.handleInputKeydown = function(e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }; // Auto-resize textarea chatInput.addEventListener('input', function() { this.style.height = 'auto'; this.style.height = Math.min(this.scrollHeight, 120) + 'px'; }); // ─── 初始化 ─── initStarfield(); updateSlotInfo(); if (!restoreSession()) { // Try to check if token in URL params const params = new URLSearchParams(window.location.search); const tokenParam = params.get('token'); if (tokenParam) { localStorage.setItem(LS_PREFIX + 'token', tokenParam); // Force reload to pick up the token window.location.href = '/'; } } // Periodically update slot info setInterval(updateSlotInfo, 30000); chatInput.addEventListener('keydown', function(e) { if (e.key === 'Escape' && document.activeElement === chatInput) { chatInput.blur(); } }); })();