diff --git a/frontend/ftchat/chat.js b/frontend/ftchat/chat.js
index 40722ab..b38c4ed 100644
--- a/frontend/ftchat/chat.js
+++ b/frontend/ftchat/chat.js
@@ -1,612 +1,491 @@
-/* ═══════════════════════════════════════════════════════════
- * 光湖 · 微调模型内测 · 前端逻辑
- * 守护: 铸渊 · ICE-GL-ZY001
- * 版权: 国作登字-2026-A-00037559
- * ═══════════════════════════════════════════════════════════ */
-
-(function () {
+/**
+ * 光湖·内测 · 前端逻辑
+ * 登录 + 聊天SSE流 + 自动翻转 + 工具调用
+ * 无框架 · 纯 Vanilla JS
+ */
+(function(){
'use strict';
// ─── 配置 ───
- var API_BASE = (function () {
- if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') {
- // 本地开发: 后端默认 PORT=3000 (src/index.js)
- return 'http://' + location.hostname + ':3000';
- }
- // 生产: 同域走 Nginx 反代
- return location.protocol + '//' + location.host;
- })();
-
- var TOKEN_KEY = 'ftchat_token';
- var EMAIL_KEY = 'ftchat_email';
- var USER_HASH_KEY = 'ftchat_user_hash';
- var SLOT_KEY = 'ftchat_slot';
+ const API_BASE = '/hli/ftchat';
+ const LS_PREFIX = 'ftchat_';
+ const SESSION_THRESHOLD = 50; // 超过N条自动翻转
// ─── 状态 ───
- var token = null;
- var email = null;
- var userHash = null;
- var slotIndex = null;
- var currentSessionId = null;
- var sessions = []; // 服务端列表(左侧栏)
- var localSessions = {}; // localStorage: sessionId → {messages, title}
- var isStreaming = false;
+ let state = {
+ token: null,
+ email: null,
+ userHash: null,
+ slotIndex: null,
+ messages: [],
+ currentStream: null, // AbortController
+ isSending: false,
+ isLoggedIn: false,
+ hasCodeSent: false,
+ currentEmail: ''
+ };
- // ─── DOM ───
- var $ = function (id) { return document.getElementById(id); };
- var loginScreen = $('loginScreen');
- var chatScreen = $('chatScreen');
- var emailInput = $('emailInput');
- var codeInput = $('codeInput');
- var sendCodeBtn = $('sendCodeBtn');
- var loginBtn = $('loginBtn');
- var loginForm = $('loginForm');
- var loginMsg = $('loginMsg');
- var slotsBanner = $('slotsBanner');
- var slotsText = $('slotsText');
- var sessionsList = $('sessionsList');
- var newChatBtn = $('newChatBtn');
- var sidebar = $('sidebar');
- var sidebarOverlay = $('sidebarOverlay');
- var sidebarToggle = $('sidebarToggle');
- var sidebarClose = $('sidebarClose');
- var chatBody = $('chatBody');
- var msgInput = $('msgInput');
- var sendBtn = $('sendBtn');
- var composer = $('composer');
- var logoutBtn = $('logoutBtn');
- var userBadge = $('userBadge');
- var slotBadgeEl = $('slotIndex');
- var metaBar = $('metaBar');
+ // ─── 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 loadStorage() {
- try {
- token = localStorage.getItem(TOKEN_KEY);
- email = localStorage.getItem(EMAIL_KEY);
- userHash = localStorage.getItem(USER_HASH_KEY);
- slotIndex = localStorage.getItem(SLOT_KEY);
- } catch (_e) { /* ignore */ }
- }
+ // ─── 星场背景 ───
+ function initStarfield() {
+ const canvas = document.getElementById('starfield');
+ if (!canvas) return;
+ const ctx = canvas.getContext('2d');
+ let stars = [];
+ let W, H;
- function persistSession() {
- try {
- localStorage.setItem(TOKEN_KEY, token);
- localStorage.setItem(EMAIL_KEY, email);
- localStorage.setItem(USER_HASH_KEY, userHash);
- localStorage.setItem(SLOT_KEY, String(slotIndex));
- } catch (_e) { /* ignore */ }
- }
-
- function clearSession() {
- token = email = userHash = slotIndex = null;
- try {
- localStorage.removeItem(TOKEN_KEY);
- localStorage.removeItem(EMAIL_KEY);
- localStorage.removeItem(USER_HASH_KEY);
- localStorage.removeItem(SLOT_KEY);
- } catch (_e) { /* ignore */ }
- }
-
- function localKey(suffix) {
- return 'ftchat_' + (userHash || 'anon') + '_' + suffix;
- }
-
- function loadLocalSessions() {
- try {
- var raw = localStorage.getItem(localKey('sessions'));
- localSessions = raw ? JSON.parse(raw) : {};
- } catch (_e) { localSessions = {}; }
- }
-
- function saveLocalSessions() {
- try {
- localStorage.setItem(localKey('sessions'), JSON.stringify(localSessions));
- } catch (_e) { /* quota or private mode */ }
- }
-
- function escapeHtml(s) {
- return String(s).replace(/[&<>"']/g, function (c) {
- return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
- });
- }
-
- // 使用 Web Crypto 生成随机字符 (避免 Math.random 在 ID 生成中的 CodeQL 警告)
- function cryptoRandomId(len) {
- var alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789';
- var out = '';
- if (window.crypto && window.crypto.getRandomValues) {
- var arr = new Uint32Array(len);
- window.crypto.getRandomValues(arr);
- for (var i = 0; i < len; i++) out += alphabet[arr[i] % alphabet.length];
- return out;
+ function resize() {
+ W = canvas.width = window.innerWidth;
+ H = canvas.height = window.innerHeight;
}
- // 兜底 (不应在现代浏览器触发)
- for (var j = 0; j < len; j++) out += alphabet[Math.floor(Math.random() * alphabet.length)]; // lgtm[js/insecure-randomness]
- return out;
- }
- function renderMarkdown(text) {
- if (typeof marked === 'undefined' || typeof DOMPurify === 'undefined') {
- // 降级: 转义 + 换行
- return escapeHtml(text).replace(/\n/g, '
');
- }
- try {
- marked.setOptions({ breaks: true, gfm: true });
- var html = marked.parse(text);
- return DOMPurify.sanitize(html, {
- FORBID_TAGS: ['style', 'script', 'iframe', 'object', 'embed'],
- FORBID_ATTR: ['onclick', 'onerror', 'onload', 'style']
- });
- } catch (_e) {
- return escapeHtml(text).replace(/\n/g, '
');
- }
- }
-
- function showLoginMsg(text, isSuccess) {
- loginMsg.textContent = text;
- if (isSuccess) loginMsg.classList.add('success');
- else loginMsg.classList.remove('success');
- }
-
- function setMetaBar(meta) {
- if (!meta) { metaBar.textContent = ''; return; }
- var parts = [];
- if (meta.time_anchor) parts.push('🕒 ' + meta.time_anchor);
- if (meta.prompt_source) parts.push('📖 ' + meta.prompt_source);
- if (meta.model_variant) parts.push('🎯 ' + (meta.model_variant === 'naipping' ? '情感线' : '系统线'));
- metaBar.textContent = parts.join(' · ');
- }
-
- // 字节级直连百炼后, 服务端不再下发 meta 帧 ——
- // 这条 meta 完全由前端本地生成, 仅用于 UI 展示, 不进入对话上下文。
- function localMeta(variant) {
- var now = new Date();
- var bj = now.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false });
- return {
- time_anchor: bj,
- prompt_source: '直连百炼',
- model_variant: variant === 'naipping' ? 'naipping' : 'system'
- };
- }
-
- // ─── API 调用 ───
- function api(path, opts) {
- opts = opts || {};
- var headers = Object.assign({ 'Content-Type': 'application/json' }, opts.headers || {});
- if (token) headers['Authorization'] = 'Bearer ' + token;
- return fetch(API_BASE + path, {
- method: opts.method || 'GET',
- headers: headers,
- body: opts.body ? JSON.stringify(opts.body) : undefined
- }).then(function (res) {
- return res.json().then(function (data) {
- if (!res.ok) {
- var err = new Error(data.message || 'HTTP ' + res.status);
- err.status = res.status;
- err.data = data;
- throw err;
- }
- return data;
- });
- });
- }
-
- // ─── 槽位查询 ───
- function refreshSlots() {
- fetch(API_BASE + '/hli/ftchat/status').then(function (r) { return r.json(); }).then(function (s) {
- if (typeof s.slots_taken === 'number') {
- slotsText.textContent = '当前席位: ' + s.slots_taken + ' / ' + s.slots_total +
- (s.slots_remaining === 0 ? ' (已满,请等待成员退出)' : ' (剩余 ' + s.slots_remaining + ' 席)');
+ 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
+ });
}
- }).catch(function () {
- slotsText.textContent = '席位状态查询失败';
- });
+ }
+
+ 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();
}
- // ─── 登录流程 ───
- sendCodeBtn.addEventListener('click', function () {
- var em = (emailInput.value || '').trim();
- if (!em) { showLoginMsg('请填写 QQ 邮箱'); return; }
- sendCodeBtn.disabled = true;
- sendCodeBtn.textContent = '发送中…';
- showLoginMsg('');
+ // ─── 登录逻辑 ───
+ function showStatus(msg, type) {
+ loginStatus.style.display = 'block';
+ loginStatus.className = 'login-status ' + type;
+ loginStatus.textContent = msg;
+ }
- api('/hli/ftchat/login', { method: 'POST', body: { email: em } })
- .then(function (resp) {
- showLoginMsg(resp.message || '验证码已发送', true);
- var seconds = 60;
- var timer = setInterval(function () {
- if (seconds <= 0) {
- clearInterval(timer);
- sendCodeBtn.disabled = false;
- sendCodeBtn.textContent = '获取验证码';
- } else {
- sendCodeBtn.textContent = seconds + 's 后重发';
- seconds--;
- }
- }, 1000);
+ 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(function (err) {
- sendCodeBtn.disabled = false;
- sendCodeBtn.textContent = '获取验证码';
- showLoginMsg(err.message || '发送失败');
- refreshSlots();
- });
- });
-
- loginForm.addEventListener('submit', function (ev) {
- ev.preventDefault();
- var em = (emailInput.value || '').trim();
- var code = (codeInput.value || '').trim();
- if (!em || !code) { showLoginMsg('请填写邮箱和验证码'); return; }
-
- loginBtn.disabled = true;
- loginBtn.textContent = '验证中…';
-
- api('/hli/ftchat/verify', { method: 'POST', body: { email: em, code: code } })
- .then(function (resp) {
- if (!resp.success) throw new Error(resp.message || '验证失败');
- token = resp.token;
- email = em;
- userHash = resp.user_hash;
- slotIndex = resp.slot_index;
- persistSession();
- showLoginMsg('登录成功,正在进入…', true);
- setTimeout(enterChat, 400);
- })
- .catch(function (err) {
- loginBtn.disabled = false;
- loginBtn.textContent = '进入对话';
- showLoginMsg(err.message || '验证失败');
- });
- });
-
- logoutBtn.addEventListener('click', function () {
- clearSession();
- location.reload();
- });
-
- // ─── 聊天进入 ───
- function enterChat() {
- loginScreen.hidden = true;
- chatScreen.hidden = false;
- // 防御性兜底: 直接用 inline style 切换显示, 避免 CDN/WebView 缓存
- // 让 [hidden] 的 CSS 修复未到达客户端时, 卡在登录页的退化
- try {
- loginScreen.style.display = 'none';
- chatScreen.style.display = '';
- } catch (_e) { /* ignore */ }
- userBadge.textContent = email || '匿名';
- userBadge.title = email || '';
- slotBadgeEl.textContent = '席位 ' + slotIndex + '/10';
-
- loadLocalSessions();
- refreshServerSessions();
-
- // 选最新的一个 session 或创建新会话
- var sessionIds = Object.keys(localSessions);
- if (sessionIds.length > 0) {
- // 选最新
- sessionIds.sort(function (a, b) {
- return (localSessions[b].updated_at || 0) - (localSessions[a].updated_at || 0);
- });
- switchSession(sessionIds[0]);
- } else {
- createNewSession(false);
- }
+ .catch(() => {});
}
- function refreshServerSessions() {
- api('/hli/ftchat/sessions').then(function (data) {
- sessions = data.sessions || [];
- renderSidebar();
- }).catch(function () { /* ignore */ });
- }
+ window.handleLogin = function() {
+ const email = emailInput.value.trim();
+ if (!email) { showStatus('请输入QQ邮箱', 'error'); return; }
- function renderSidebar() {
- sessionsList.innerHTML = '';
- // 合并本地 sessions 与服务端 sessions(按 session_id)
- var ids = Object.keys(localSessions);
- var serverIds = sessions.map(function (s) { return s.session_id; });
- serverIds.forEach(function (id) { if (ids.indexOf(id) === -1) ids.push(id); });
+ if (!state.hasCodeSent) {
+ // 发送验证码
+ loginBtn.disabled = true;
+ loginBtn.textContent = '发送中...';
+ state.currentEmail = email;
- // 按 updated_at 排序
- ids.sort(function (a, b) {
- var ta = (localSessions[a] && localSessions[a].updated_at) || 0;
- var tb = (localSessions[b] && localSessions[b].updated_at) || 0;
- var serverA = sessions.find(function (s) { return s.session_id === a; });
- var serverB = sessions.find(function (s) { return s.session_id === b; });
- if (serverA) ta = Math.max(ta, new Date(serverA.updated_at).getTime() || 0);
- if (serverB) tb = Math.max(tb, new Date(serverB.updated_at).getTime() || 0);
- return tb - ta;
- });
-
- if (ids.length === 0) {
- sessionsList.innerHTML = '
还没有对话
';
- return;
- }
-
- ids.forEach(function (id) {
- var local = localSessions[id];
- var server = sessions.find(function (s) { return s.session_id === id; });
- var title = (local && local.title) || (server && server.title) || '新对话';
- var ts = (local && local.updated_at) || (server && new Date(server.updated_at).getTime()) || 0;
-
- var btn = document.createElement('button');
- btn.type = 'button';
- btn.className = 'session-item' + (id === currentSessionId ? ' active' : '');
- btn.innerHTML =
- '' + escapeHtml(title) + '' +
- '' + formatTs(ts) + '';
- btn.addEventListener('click', function () {
- switchSession(id);
- if (window.innerWidth <= 768) closeSidebar();
- });
- sessionsList.appendChild(btn);
- });
- }
-
- function formatTs(ts) {
- if (!ts) return '';
- var d = new Date(ts);
- var now = new Date();
- if (d.toDateString() === now.toDateString()) {
- return d.getHours().toString().padStart(2, '0') + ':' + d.getMinutes().toString().padStart(2, '0');
- }
- return (d.getMonth() + 1) + '月' + d.getDate() + '日';
- }
-
- // ─── 会话管理 ───
- function switchSession(id) {
- currentSessionId = id;
- var s = localSessions[id];
- chatBody.innerHTML = '';
- metaBar.textContent = '';
- if (s && s.messages) {
- s.messages.forEach(function (m) {
- appendBubble(m.role === 'assistant' ? 'persona' : 'user', m.content);
- });
- } else {
- appendWelcome();
- }
- renderSidebar();
- scrollToBottom();
- }
-
- function appendWelcome() {
- var welcome = '你好,我是被微调的人格体。\n\n这是一个团队**内测频道**,你的对话会被记忆并跨会话延续。\n\n- 我会根据你给的真实时间锚点回应\n- 我使用 Markdown 排版(标题、列表、表格、代码块)\n- 试着问我任何东西吧~';
- appendBubble('persona', welcome);
- }
-
- function createNewSession(triggerCompress) {
- var prevId = currentSessionId;
- var prevMessages = (prevId && localSessions[prevId] && localSessions[prevId].messages) || [];
-
- function actuallyCreate(newId) {
- currentSessionId = newId || (Date.now().toString(36) + cryptoRandomId(8));
- localSessions[currentSessionId] = {
- title: '新对话',
- messages: [],
- updated_at: Date.now()
- };
- saveLocalSessions();
- switchSession(currentSessionId);
- refreshServerSessions();
- }
-
- if (triggerCompress && prevMessages.length >= 4) {
- // 调服务端压缩 + 拿新 session_id
- api('/hli/ftchat/sessions/new', {
+ fetch(API_BASE + '/login', {
method: 'POST',
- body: {
- previous_session_id: prevId,
- previous_messages: prevMessages.slice(-30)
+ 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');
}
- }).then(function (resp) {
- actuallyCreate(resp.session_id);
- if (resp.compressed) {
- // 在新对话里给个轻提示(不污染 messages 历史)
- var sysRow = document.createElement('div');
- sysRow.className = 'message-row system';
- sysRow.innerHTML = '🧠 已为你保留上次对话的母语印记,新对话开始时会自动注入
';
- chatBody.insertBefore(sysRow, chatBody.firstChild);
- }
- }).catch(function () {
- actuallyCreate();
+ })
+ .catch(err => {
+ loginBtn.disabled = false;
+ loginBtn.textContent = '发送验证码';
+ showStatus('网络错误: ' + err.message, 'error');
});
} else {
- actuallyCreate();
+ // 验证码
+ 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 = [];
}
}
- newChatBtn.addEventListener('click', function () { createNewSession(true); });
-
- // ─── 侧栏移动端 ───
- function openSidebar() { sidebar.classList.add('open'); sidebarOverlay.classList.add('show'); }
- function closeSidebar() { sidebar.classList.remove('open'); sidebarOverlay.classList.remove('show'); }
- sidebarToggle.addEventListener('click', openSidebar);
- sidebarClose.addEventListener('click', closeSidebar);
- sidebarOverlay.addEventListener('click', closeSidebar);
-
- // ─── 消息渲染 ───
- function appendBubble(role, content) {
- var row = document.createElement('div');
- row.className = 'message-row ' + role;
- var bubble = document.createElement('div');
- bubble.className = 'bubble ' + role;
- if (role === 'persona') {
- bubble.innerHTML = renderMarkdown(content);
- } else if (role === 'system') {
- bubble.textContent = content;
- } else {
- bubble.textContent = content;
+ 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 {}
+ }
}
- row.appendChild(bubble);
- chatBody.appendChild(row);
- scrollToBottom();
- return bubble;
}
- function appendStreamBubble() {
- var row = document.createElement('div');
- row.className = 'message-row persona';
- var bubble = document.createElement('div');
- bubble.className = 'bubble persona cursor-blink';
- bubble.textContent = '';
- row.appendChild(bubble);
- chatBody.appendChild(row);
- scrollToBottom();
- return bubble;
+ function addMessage(role, content) {
+ state.messages.push({ role: role, content: content });
+ saveConversation();
}
- function scrollToBottom() {
- chatBody.scrollTop = chatBody.scrollHeight;
- }
-
- // ─── 发送消息 (SSE) ───
- composer.addEventListener('submit', function (ev) {
- ev.preventDefault();
- sendMessage();
- });
-
- msgInput.addEventListener('keydown', function (ev) {
- if (ev.key === 'Enter' && !ev.shiftKey) {
- ev.preventDefault();
- sendMessage();
+ function renderMessages(scrollToBottom) {
+ chatMessages.innerHTML = '';
+ for (const msg of state.messages) {
+ appendMessageElement(msg.role, msg.content, false);
}
- });
+ if (scrollToBottom !== false) {
+ chatMessages.scrollTop = chatMessages.scrollHeight;
+ }
+ }
- msgInput.addEventListener('input', function () {
- msgInput.style.height = 'auto';
- msgInput.style.height = Math.min(msgInput.scrollHeight, 200) + 'px';
- });
+ 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 sendMessage() {
- if (isStreaming) return;
- var text = (msgInput.value || '').trim();
+ 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;
- msgInput.value = '';
- msgInput.style.height = 'auto';
- appendBubble('user', text);
+ chatInput.value = '';
+ chatInput.style.height = 'auto';
- var sess = localSessions[currentSessionId];
- if (!sess) {
- sess = { title: '新对话', messages: [], updated_at: Date.now() };
- localSessions[currentSessionId] = sess;
+ // Check auto-rotate
+ if (state.messages.length >= SESSION_THRESHOLD) {
+ rotateConversation();
}
- sess.messages.push({ role: 'user', content: text });
- if (sess.messages.length === 1) sess.title = text.slice(0, 30);
- sess.updated_at = Date.now();
- saveLocalSessions();
- renderSidebar();
- streamReply(sess.messages);
- }
+ // Add user message
+ addMessage('user', text);
+ appendMessageElement('user', text, true);
- function streamReply(history) {
- isStreaming = true;
+ // Send
+ state.isSending = true;
sendBtn.disabled = true;
- var bubble = appendStreamBubble();
- var fullText = '';
- // 字节级直连: 服务端不再下发 meta, 这里前端本地生成一次
- setMetaBar(localMeta('system'));
+ const controller = new AbortController();
+ state.currentStream = controller;
- fetch(API_BASE + '/hli/ftchat/chat', {
+ // 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 ' + token
+ 'Authorization': 'Bearer ' + state.token
},
body: JSON.stringify({
- session_id: currentSessionId,
- messages: history.slice(-50), // 最近 50 条上行
- model_variant: 'system'
- })
- }).then(function (res) {
- if (res.status === 401) {
- clearSession();
- location.reload();
- throw new Error('登录失效');
+ 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;
+ });
}
- if (!res.ok) {
- return res.json().then(function (d) { throw new Error(d.message || 'HTTP ' + res.status); });
- }
- // 直接解析百炼/OpenAI 兼容 SSE 格式: data: { choices: [{ delta: { content: "..." } }] }
- return readSse(res, function (event) {
- // 模型增量 token
- var delta = event && event.choices && event.choices[0] && event.choices[0].delta;
- var piece = delta && delta.content;
- if (piece) {
- fullText += piece;
- bubble.classList.remove('cursor-blink');
- bubble.innerHTML = renderMarkdown(fullText) + '';
- scrollToBottom();
- }
- // 百炼/我方异常时透传的 error 帧
- if (event && event.error) {
- var msg = (event.error && event.error.message) || event.message || '上游异常';
- bubble.innerHTML = '⚠️ ' + escapeHtml(msg) + '';
- }
- });
- }).then(function () {
- bubble.classList.remove('cursor-blink');
- if (fullText) {
- bubble.innerHTML = renderMarkdown(fullText);
- var sess = localSessions[currentSessionId];
- if (sess) {
- sess.messages.push({ role: 'assistant', content: fullText });
- sess.updated_at = Date.now();
- saveLocalSessions();
- }
- } else if (!bubble.innerHTML) {
- bubble.innerHTML = '(未收到有效回复)';
- }
- }).catch(function (err) {
- bubble.classList.remove('cursor-blink');
- bubble.innerHTML = '⚠️ ' + escapeHtml(err.message || '请求失败') + '';
- }).then(function () {
- isStreaming = false;
+
+ readStream();
+ })
+ .catch(err => {
+ typingDiv.remove();
+ state.isSending = false;
sendBtn.disabled = false;
- msgInput.focus();
+ 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);
}
- function readSse(res, onEvent) {
- var reader = res.body.getReader();
- var decoder = new TextDecoder();
- var buf = '';
- function pump() {
- return reader.read().then(function (chunk) {
- if (chunk.done) return;
- buf += decoder.decode(chunk.value, { stream: true });
- var lines = buf.split('\n');
- buf = lines.pop();
- for (var i = 0; i < lines.length; i++) {
- var line = lines[i].trim();
- if (!line.startsWith('data:')) continue;
- var payload = line.slice(5).trim();
- if (payload === '[DONE]') return;
- try {
- var parsed = JSON.parse(payload);
- onEvent(parsed);
- } catch (_e) { /* ignore malformed */ }
- }
- return pump();
- });
+ 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 = '/';
}
- return pump();
}
- // ─── 启动 ───
- loadStorage();
- if (token && userHash) {
- // 有效性由后端鉴权决定,先进入;过期会被 401 踢出
- enterChat();
- } else {
- refreshSlots();
- setInterval(refreshSlots, 30000);
- }
+ // Periodically update slot info
+ setInterval(updateSlotInfo, 30000);
+
+ // Keyboard shortcut: Escape to blur input
+ document.addEventListener('keydown', function(e) {
+ if (e.key === 'Escape' && document.activeElement === chatInput) {
+ chatInput.blur();
+ }
+ });
+
})();