{"repo": "0lin0-c/mymem", "files": {"frontend/js/main.js": {"34fba0151a8a51b1035f15788505978262306780": {"t": 1785746699.2779644, "n": 21148, "h": "888b542a45a4ae0f", "c": "// 初始化页面逻辑\ndocument.addEventListener('DOMContentLoaded', function () {\n const page = location.pathname.split('/').pop().replace('.html', '') || 'index';\n\n if (page === 'register') {\n initOnboardingPage();\n } else if (page === 'chat') {\n initChatPage();\n } else if (page === 'index' || page === '') {\n initLoginPage();\n } else if (page === 'settings') {\n initSettingsPage();\n }\n});\n\n// ========== 设置页面 ==========\nfunction initSettingsPage() {\n const userId = localStorage.getItem('userId');\n if (!userId) {\n // 未登录,显示提示\n document.getElementById('llmSettingsForm').innerHTML = `\n
\n

请先登录后再配置 LLM

\n 去登录\n
\n `;\n return;\n }\n\n const form = document.getElementById('llmSettingsForm');\n const warmingUp = document.getElementById('warming_up');\n const successMsg = document.getElementById('success_msg');\n\n // 根据提供商自动填充常用 URL\n const providerSelect = document.getElementById('llm_provider');\n const baseUrlInput = document.getElementById('llm_base_url');\n const modelInput = document.getElementById('llm_model');\n\n const defaultUrls = {\n 'openai': 'https://api.openai.com/v1',\n 'deepseek': 'https://api.deepseek.com/v1',\n 'qwen': 'https://dashscope.aliyuncs.com/compatible-mode/v1',\n 'glm': 'https://open.bigmodel.cn/api/paas/v4',\n 'anthropic': '',\n 'custom': '',\n };\n\n const defaultModels = {\n 'openai': 'gpt-4o',\n 'deepseek': 'deepseek-chat',\n 'qwen': 'qwen-plus',\n 'glm': 'glm-4',\n 'anthropic': 'claude-sonnet-4-20250514',\n 'custom': '',\n };\n\n providerSelect.addEventListener('change', function() {\n const provider = this.value;\n baseUrlInput.placeholder = defaultUrls[provider] || '请输入 API Base URL';\n modelInput.placeholder = defaultModels[provider] || '请输入模型名称';\n });\n\n form.addEventListener('submit', async function(e) {\n e.preventDefault();\n\n const provider = providerSelect.value;\n const baseUrl = baseUrlInput.value.trim();\n const apiKey = document.getElementById('llm_api_key').value.trim();\n const model = modelInput.value.trim();\n\n // 校验\n document.querySelectorAll('.error-msg').forEach(el => el.textContent = '');\n\n if (!apiKey) {\n showError('llm_api_key', '请输入 API Key');\n return;\n }\n if (!model) {\n showError('llm_model', '请输入模型名称');\n return;\n }\n\n // 显示预热中\n form.classList.add('hidden');\n warmingUp.classList.remove('hidden');\n\n try {\n const result = await api.updateLLMSettings({\n user_id: userId,\n llm_provider: provider,\n llm_api_key: apiKey,\n llm_base_url: baseUrl || null,\n llm_model: model,\n });\n\n if (result.success) {\n // 等待预热完成(轮询状态)\n let attempts = 0;\n const maxAttempts = 30; // 最多等待 30 秒\n\n const checkWarmup = async () => {\n attempts++;\n const status = await api.getLLMSettings(userId);\n\n if (status.warmed_up) {\n warmingUp.classList.add('hidden');\n successMsg.classList.remove('hidden');\n } else if (attempts < maxAttempts) {\n setTimeout(checkWarmup, 1000);\n } else {\n warmingUp.classList.add('hidden');\n form.classList.remove('hidden');\n alert('预热超时,请检查配置后重试');\n }\n };\n\n setTimeout(checkWarmup, 1000);\n } else {\n warmingUp.classList.add('hidden');\n form.classList.remove('hidden');\n alert('保存失败:' + result.message);\n }\n } catch (error) {\n warmingUp.classList.add('hidden');\n form.classList.remove('hidden');\n alert('保存失败:' + error.message);\n }\n });\n}\n\n// ========== 登录页面 ==========\nfunction initLoginPage() {\n const loginForm = document.getElementById('loginForm');\n const usernameInput = document.getElementById('username');\n const passwordInput = document.getElementById('password');\n\n loginForm.addEventListener('submit', async function (e) {\n e.preventDefault();\n\n const username = usernameInput.value.trim();\n const password = passwordInput.value;\n\n // 清空错误提示\n document.querySelectorAll('.error-msg').forEach(el => el.textContent = '');\n\n // 校验\n let valid = true;\n if (!username) {\n showError('username', '请输入用户名');\n valid = false;\n }\n if (!password) {\n showError('password', '请输入密码');\n valid = false;\n }\n\n if (!valid) return;\n\n try {\n const result = await api.login(username, password);\n\n if (result.success) {\n // 保存用户信息到 localStorage\n localStorage.setItem('userId', result.user_id);\n localStorage.setItem('username', result.username);\n if (result.ai_name) {\n localStorage.setItem('aiName', result.ai_name);\n }\n\n // 登录成功,跳转聊天页面\n window.location.href = 'chat.html';\n } else {\n showError('username', result.message);\n }\n } catch (error) {\n showError('username', error.message || '登录失败,请重试');\n }\n });\n}\n\n// ========== 初始化页面 ==========\nfunction initOnboardingPage() {\n const step1 = document.getElementById('step1');\n const step2 = document.getElementById('step2');\n const loading = document.getElementById('loading');\n const identitySelect = document.getElementById('identity_type');\n const identityDetailContainer = document.getElementById('identity_detail_container');\n\n // 身份详情模板\n const identityDetailTemplates = {\n student: `\n
\n \n \n
\n
\n \n \n
\n `,\n worker: `\n
\n \n \n
\n
\n \n \n
\n `,\n teacher: `\n
\n \n \n
\n
\n \n \n
\n `,\n freelancer: `\n
\n \n \n
\n `,\n other: `\n
\n \n \n
\n `,\n };\n\n // 身份类型变化时动态展示详情字段\n identitySelect.addEventListener('change', function () {\n const type = this.value;\n identityDetailContainer.innerHTML = type ? (identityDetailTemplates[type] || '') : '';\n\n // 学生身份:监听学段变化,控制专业方向显示\n if (type === 'student') {\n const educationStageSelect = document.getElementById('education_stage');\n const majorGroup = document.getElementById('major_group');\n\n if (educationStageSelect && majorGroup) {\n educationStageSelect.addEventListener('change', function() {\n const stage = this.value;\n // 只有大学(college)和研究生(graduate)才显示专业方向\n if (stage === 'college' || stage === 'graduate') {\n majorGroup.style.display = 'block';\n } else {\n majorGroup.style.display = 'none';\n // 清空专业方向输入\n const majorInput = document.getElementById('major');\n if (majorInput) majorInput.value = '';\n }\n });\n }\n }\n });\n\n // 下一步按钮\n document.getElementById('nextBtn').addEventListener('click', function () {\n if (validateStep1()) {\n step1.classList.add('hidden');\n step2.classList.remove('hidden');\n }\n });\n\n // 上一步按钮\n document.getElementById('prevBtn').addEventListener('click', function () {\n step2.classList.add('hidden');\n step1.classList.remove('hidden');\n });\n\n // 提交按钮\n document.getElementById('submitBtn').addEventListener('click', async function () {\n if (validateStep2()) {\n const data = collectFormData();\n\n step2.classList.add('hidden');\n loading.classList.remove('hidden');\n\n try {\n const result = await api.onboarding(data);\n\n if (result.success) {\n // 保存用户信息到 localStorage\n localStorage.setItem('userId', result.user_id);\n localStorage.setItem('username', data.username);\n localStorage.setItem('aiName', data.ai_customization.ai_name);\n\n // 跳转到聊天页面\n window.location.href = 'chat.html';\n } else {\n alert('初始化失败:' + result.message);\n loading.classList.add('hidden');\n step2.classList.remove('hidden');\n }\n } catch (error) {\n alert('初始化失败:' + error.message);\n console.error(error);\n loading.classList.add('hidden');\n step2.classList.remove('hidden');\n }\n }\n });\n}\n\n// 校验第一步\nfunction validateStep1() {\n const username = document.getElementById('username').value.trim();\n const password = document.getElementById('password').value;\n const identityType = document.getElementById('identity_type').value;\n let valid = true;\n\n // 清空错误提示\n document.querySelectorAll('#step1 .error-msg').forEach(el => el.textContent = '');\n\n if (!username) {\n showError('username', '请输入您的昵称');\n valid = false;\n } else if (username.length > 50) {\n showError('username', '昵称最多 50 个字符');\n valid = false;\n }\n\n if (!password) {\n showError('password', '请设置密码');\n valid = false;\n }\n\n if (!identityType) {\n showError('identity_type', '请选择您的身份类型');\n valid = false;\n }\n\n return valid;\n}\n\n// 校验第二步\nfunction validateStep2() {\n const aiName = document.getElementById('ai_name').value.trim();\n const aiRole = document.getElementById('ai_role').value;\n let valid = true;\n\n document.querySelectorAll('#step2 .error-msg').forEach(el => el.textContent = '');\n\n if (!aiName) {\n showError('ai_name', '请给 AI 助手起个名字');\n valid = false;\n } else if (aiName.length > 20) {\n showError('ai_name', '名字最多 20 个字符');\n valid = false;\n }\n\n if (!aiRole) {\n showError('ai_role', '请选择 AI 助手的身份');\n valid = false;\n }\n\n return valid;\n}\n\n// 显示错误提示\nfunction showError(fieldId, message) {\n const field = document.getElementById(fieldId);\n const errorEl = field.parentElement.querySelector('.error-msg');\n if (errorEl) {\n errorEl.textContent = message;\n }\n}\n\n// 收集表单数据\nfunction collectFormData() {\n const identityType = document.getElementById('identity_type').value;\n const identityDetail = {};\n\n // 根据身份类型收集详情\n if (identityType === 'student') {\n const educationStage = document.getElementById('education_stage')?.value;\n const major = document.getElementById('major')?.value.trim();\n if (educationStage) identityDetail.education_stage = educationStage;\n if (major) identityDetail.major = major;\n } else if (identityType === 'worker') {\n const industry = document.getElementById('industry')?.value.trim();\n const jobTitle = document.getElementById('job_title')?.value.trim();\n if (industry) identityDetail.industry = industry;\n if (jobTitle) identityDetail.job_title = jobTitle;\n } else if (identityType === 'teacher') {\n const subject = document.getElementById('subject')?.value.trim();\n const teachingStage = document.getElementById('teaching_stage')?.value.trim();\n if (subject) identityDetail.subject = subject;\n if (teachingStage) identityDetail.teaching_stage = teachingStage;\n } else if (identityType === 'freelancer') {\n const field = document.getElementById('field')?.value.trim();\n if (field) identityDetail.field = field;\n } else if (identityType === 'other') {\n const description = document.getElementById('description')?.value.trim();\n if (description) identityDetail.description = description;\n }\n\n // 收集使用场景(第一步的复选框)\n const useCases = [];\n document.querySelectorAll('#step1 .checkbox-group input[type=\"checkbox\"]:checked').forEach(cb => {\n useCases.push(cb.value);\n });\n\n // 收集性格特点(第二步的复选框)\n const personality = [];\n document.querySelectorAll('#step2 .checkbox-group input[type=\"checkbox\"]:checked').forEach(cb => {\n personality.push(cb.value);\n });\n\n return {\n username: document.getElementById('username').value.trim(),\n password: document.getElementById('password').value,\n identity_type: identityType,\n identity_detail: Object.keys(identityDetail).length > 0 ? identityDetail : null,\n use_cases: useCases,\n interests: [], // 暂时留空,表单中没有这个\n ai_customization: {\n ai_name: document.getElementById('ai_name').value.trim(),\n ai_role: document.getElementById('ai_role').value,\n personality: personality,\n communication_style: document.getElementById('communication_style').value,\n },\n };\n}\n\n// ========== 聊天页面 ==========\nfunction initChatPage() {\n const userId = localStorage.getItem('userId');\n const aiName = localStorage.getItem('aiName') || 'AI 助手';\n const username = localStorage.getItem('username') || '用户';\n\n if (!userId) {\n window.location.href = 'index.html';\n return;\n }\n\n // 配置 marked + highlight.js\n if (typeof marked !== 'undefined') {\n marked.setOptions({\n highlight: function(code, lang) {\n if (typeof hljs !== 'undefined') {\n if (lang && hljs.getLanguage(lang)) {\n return hljs.highlight(code, { language: lang }).value;\n }\n return hljs.highlightAuto(code).value;\n }\n return code;\n },\n breaks: true\n });\n }\n\n // 显示 AI 名字\n document.getElementById('ai_name_display').textContent = aiName;\n\n const chatMessages = document.getElementById('chat_messages');\n const userInput = document.getElementById('user_input');\n const sendBtn = document.getElementById('sendBtn');\n const logoutBtn = document.getElementById('logoutBtn');\n\n // 退出登录\n if (logoutBtn) {\n logoutBtn.addEventListener('click', async function() {\n const sessionId = localStorage.getItem('sessionId');\n try {\n // 调用后端登出 API\n await api.logout(userId, sessionId);\n } catch (error) {\n console.error('登出 API 调用失败:', error);\n }\n // 清除本地存储\n localStorage.removeItem('userId');\n localStorage.removeItem('username');\n localStorage.removeItem('aiName');\n localStorage.removeItem('sessionId');\n window.location.href = 'index.html';\n });\n }\n\n // 发送消息\n async function sendMessage() {\n const message = userInput.value.trim();\n if (!message) return;\n\n // 显示用户消息\n appendMessage('user', message);\n userInput.value = '';\n\n // 创建 AI 消息容器(带思考动画)\n const aiMessageEl = appendMessage('ai', '', true);\n let fullResponse = '';\n\n try {\n await api.chat(\n userId,\n message,\n // onChunk - 流式接收内容\n (chunk) => {\n fullResponse += chunk;\n // 流式渲染 Markdown\n renderAIMarkdown(aiMessageEl, fullResponse);\n chatMessages.scrollTop = chatMessages.scrollHeight;\n },\n // onDone - 完成\n (data) => {\n console.log('对话完成:', data);\n // 移除思考动画\n removeThinkingAnimation(aiMessageEl);\n }\n );\n } catch (error) {\n console.error(error);\n removeThinkingAnimation(aiMessageEl);\n aiMessageEl.querySelector('.message-content').textContent = '网络错误,请重试';\n }\n }\n\n sendBtn.addEventListener('click', sendMessage);\n userInput.addEventListener('keypress', function (e) {\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n sendMessage();\n }\n });\n}\n\n// 渲染 AI 消息的 Markdown\nfunction renderAIMarkdown(messageEl, content) {\n if (typeof marked !== 'undefined') {\n const html = marked.parse(content);\n messageEl.querySelector('.message-content').innerHTML = html;\n // 高亮代码块\n if (typeof hljs !== 'undefined') {\n messageEl.querySelectorAll('pre code').forEach((block) => {\n hljs.highlightElement(block);\n });\n }\n }\n}\n\n// 移除思考动画\nfunction removeThinkingAnimation(messageEl) {\n const thinkingEl = messageEl.querySelector('.thinking-indicator');\n if (thinkingEl) {\n thinkingEl.remove();\n }\n}\n\n// 添加消息到聊天窗口\nfunction appendMessage(role, content, showThinking = false) {\n const chatMessages = document.getElementById('chat_messages');\n const messageDiv = document.createElement('div');\n messageDiv.className = `message ${role}`;\n\n // 获取头像显示文字\n const aiName = localStorage.getItem('aiName') || 'AI';\n const username = localStorage.getItem('username') || 'U';\n const avatarText = role === 'ai' ? aiName.charAt(0).toUpperCase() : username.charAt(0).toUpperCase();\n\n // 格式化时间\n const time = new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });\n\n if (role === 'ai') {\n // AI 消息\n let thinkingHtml = '';\n if (showThinking || content === '') {\n // 思考动画放在 message-body 外面,定位到气泡右上角\n thinkingHtml = `\n
\n \n \n \n
\n `;\n }\n\n let contentHtml = '';\n if (content) {\n const renderedContent = typeof marked !== 'undefined' ? marked.parse(content) : escapeHtml(content);\n contentHtml = `
${renderedContent}
`;\n } else {\n contentHtml = '
';\n }\n\n messageDiv.innerHTML = `\n
${avatarText}
\n
\n ${thinkingHtml}\n
\n ${contentHtml}\n ${time}\n
\n
\n `;\n } else {\n // 用户消息保持纯文本\n messageDiv.innerHTML = `\n
${avatarText}
\n
\n
${escapeHtml(content)}
\n ${time}\n
\n `;\n }\n\n chatMessages.appendChild(messageDiv);\n chatMessages.scrollTop = chatMessages.scrollHeight;\n return messageDiv;\n}\n\n// HTML 转义\nfunction escapeHtml(text) {\n const div = document.createElement('div');\n div.textContent = text;\n return div.innerHTML;\n}\n"}, "31942754f3ccb553c9f7ff643c341469b7672d13": {"t": 1785766983.1830359, "n": 21148, "h": "888b542a45a4ae0f", "c": "// 初始化页面逻辑\ndocument.addEventListener('DOMContentLoaded', function () {\n const page = location.pathname.split('/').pop().replace('.html', '') || 'index';\n\n if (page === 'register') {\n initOnboardingPage();\n } else if (page === 'chat') {\n initChatPage();\n } else if (page === 'index' || page === '') {\n initLoginPage();\n } else if (page === 'settings') {\n initSettingsPage();\n }\n});\n\n// ========== 设置页面 ==========\nfunction initSettingsPage() {\n const userId = localStorage.getItem('userId');\n if (!userId) {\n // 未登录,显示提示\n document.getElementById('llmSettingsForm').innerHTML = `\n
\n

请先登录后再配置 LLM

\n 去登录\n
\n `;\n return;\n }\n\n const form = document.getElementById('llmSettingsForm');\n const warmingUp = document.getElementById('warming_up');\n const successMsg = document.getElementById('success_msg');\n\n // 根据提供商自动填充常用 URL\n const providerSelect = document.getElementById('llm_provider');\n const baseUrlInput = document.getElementById('llm_base_url');\n const modelInput = document.getElementById('llm_model');\n\n const defaultUrls = {\n 'openai': 'https://api.openai.com/v1',\n 'deepseek': 'https://api.deepseek.com/v1',\n 'qwen': 'https://dashscope.aliyuncs.com/compatible-mode/v1',\n 'glm': 'https://open.bigmodel.cn/api/paas/v4',\n 'anthropic': '',\n 'custom': '',\n };\n\n const defaultModels = {\n 'openai': 'gpt-4o',\n 'deepseek': 'deepseek-chat',\n 'qwen': 'qwen-plus',\n 'glm': 'glm-4',\n 'anthropic': 'claude-sonnet-4-20250514',\n 'custom': '',\n };\n\n providerSelect.addEventListener('change', function() {\n const provider = this.value;\n baseUrlInput.placeholder = defaultUrls[provider] || '请输入 API Base URL';\n modelInput.placeholder = defaultModels[provider] || '请输入模型名称';\n });\n\n form.addEventListener('submit', async function(e) {\n e.preventDefault();\n\n const provider = providerSelect.value;\n const baseUrl = baseUrlInput.value.trim();\n const apiKey = document.getElementById('llm_api_key').value.trim();\n const model = modelInput.value.trim();\n\n // 校验\n document.querySelectorAll('.error-msg').forEach(el => el.textContent = '');\n\n if (!apiKey) {\n showError('llm_api_key', '请输入 API Key');\n return;\n }\n if (!model) {\n showError('llm_model', '请输入模型名称');\n return;\n }\n\n // 显示预热中\n form.classList.add('hidden');\n warmingUp.classList.remove('hidden');\n\n try {\n const result = await api.updateLLMSettings({\n user_id: userId,\n llm_provider: provider,\n llm_api_key: apiKey,\n llm_base_url: baseUrl || null,\n llm_model: model,\n });\n\n if (result.success) {\n // 等待预热完成(轮询状态)\n let attempts = 0;\n const maxAttempts = 30; // 最多等待 30 秒\n\n const checkWarmup = async () => {\n attempts++;\n const status = await api.getLLMSettings(userId);\n\n if (status.warmed_up) {\n warmingUp.classList.add('hidden');\n successMsg.classList.remove('hidden');\n } else if (attempts < maxAttempts) {\n setTimeout(checkWarmup, 1000);\n } else {\n warmingUp.classList.add('hidden');\n form.classList.remove('hidden');\n alert('预热超时,请检查配置后重试');\n }\n };\n\n setTimeout(checkWarmup, 1000);\n } else {\n warmingUp.classList.add('hidden');\n form.classList.remove('hidden');\n alert('保存失败:' + result.message);\n }\n } catch (error) {\n warmingUp.classList.add('hidden');\n form.classList.remove('hidden');\n alert('保存失败:' + error.message);\n }\n });\n}\n\n// ========== 登录页面 ==========\nfunction initLoginPage() {\n const loginForm = document.getElementById('loginForm');\n const usernameInput = document.getElementById('username');\n const passwordInput = document.getElementById('password');\n\n loginForm.addEventListener('submit', async function (e) {\n e.preventDefault();\n\n const username = usernameInput.value.trim();\n const password = passwordInput.value;\n\n // 清空错误提示\n document.querySelectorAll('.error-msg').forEach(el => el.textContent = '');\n\n // 校验\n let valid = true;\n if (!username) {\n showError('username', '请输入用户名');\n valid = false;\n }\n if (!password) {\n showError('password', '请输入密码');\n valid = false;\n }\n\n if (!valid) return;\n\n try {\n const result = await api.login(username, password);\n\n if (result.success) {\n // 保存用户信息到 localStorage\n localStorage.setItem('userId', result.user_id);\n localStorage.setItem('username', result.username);\n if (result.ai_name) {\n localStorage.setItem('aiName', result.ai_name);\n }\n\n // 登录成功,跳转聊天页面\n window.location.href = 'chat.html';\n } else {\n showError('username', result.message);\n }\n } catch (error) {\n showError('username', error.message || '登录失败,请重试');\n }\n });\n}\n\n// ========== 初始化页面 ==========\nfunction initOnboardingPage() {\n const step1 = document.getElementById('step1');\n const step2 = document.getElementById('step2');\n const loading = document.getElementById('loading');\n const identitySelect = document.getElementById('identity_type');\n const identityDetailContainer = document.getElementById('identity_detail_container');\n\n // 身份详情模板\n const identityDetailTemplates = {\n student: `\n
\n \n \n
\n
\n \n \n
\n `,\n worker: `\n
\n \n \n
\n
\n \n \n
\n `,\n teacher: `\n
\n \n \n
\n
\n \n \n
\n `,\n freelancer: `\n
\n \n \n
\n `,\n other: `\n
\n \n \n
\n `,\n };\n\n // 身份类型变化时动态展示详情字段\n identitySelect.addEventListener('change', function () {\n const type = this.value;\n identityDetailContainer.innerHTML = type ? (identityDetailTemplates[type] || '') : '';\n\n // 学生身份:监听学段变化,控制专业方向显示\n if (type === 'student') {\n const educationStageSelect = document.getElementById('education_stage');\n const majorGroup = document.getElementById('major_group');\n\n if (educationStageSelect && majorGroup) {\n educationStageSelect.addEventListener('change', function() {\n const stage = this.value;\n // 只有大学(college)和研究生(graduate)才显示专业方向\n if (stage === 'college' || stage === 'graduate') {\n majorGroup.style.display = 'block';\n } else {\n majorGroup.style.display = 'none';\n // 清空专业方向输入\n const majorInput = document.getElementById('major');\n if (majorInput) majorInput.value = '';\n }\n });\n }\n }\n });\n\n // 下一步按钮\n document.getElementById('nextBtn').addEventListener('click', function () {\n if (validateStep1()) {\n step1.classList.add('hidden');\n step2.classList.remove('hidden');\n }\n });\n\n // 上一步按钮\n document.getElementById('prevBtn').addEventListener('click', function () {\n step2.classList.add('hidden');\n step1.classList.remove('hidden');\n });\n\n // 提交按钮\n document.getElementById('submitBtn').addEventListener('click', async function () {\n if (validateStep2()) {\n const data = collectFormData();\n\n step2.classList.add('hidden');\n loading.classList.remove('hidden');\n\n try {\n const result = await api.onboarding(data);\n\n if (result.success) {\n // 保存用户信息到 localStorage\n localStorage.setItem('userId', result.user_id);\n localStorage.setItem('username', data.username);\n localStorage.setItem('aiName', data.ai_customization.ai_name);\n\n // 跳转到聊天页面\n window.location.href = 'chat.html';\n } else {\n alert('初始化失败:' + result.message);\n loading.classList.add('hidden');\n step2.classList.remove('hidden');\n }\n } catch (error) {\n alert('初始化失败:' + error.message);\n console.error(error);\n loading.classList.add('hidden');\n step2.classList.remove('hidden');\n }\n }\n });\n}\n\n// 校验第一步\nfunction validateStep1() {\n const username = document.getElementById('username').value.trim();\n const password = document.getElementById('password').value;\n const identityType = document.getElementById('identity_type').value;\n let valid = true;\n\n // 清空错误提示\n document.querySelectorAll('#step1 .error-msg').forEach(el => el.textContent = '');\n\n if (!username) {\n showError('username', '请输入您的昵称');\n valid = false;\n } else if (username.length > 50) {\n showError('username', '昵称最多 50 个字符');\n valid = false;\n }\n\n if (!password) {\n showError('password', '请设置密码');\n valid = false;\n }\n\n if (!identityType) {\n showError('identity_type', '请选择您的身份类型');\n valid = false;\n }\n\n return valid;\n}\n\n// 校验第二步\nfunction validateStep2() {\n const aiName = document.getElementById('ai_name').value.trim();\n const aiRole = document.getElementById('ai_role').value;\n let valid = true;\n\n document.querySelectorAll('#step2 .error-msg').forEach(el => el.textContent = '');\n\n if (!aiName) {\n showError('ai_name', '请给 AI 助手起个名字');\n valid = false;\n } else if (aiName.length > 20) {\n showError('ai_name', '名字最多 20 个字符');\n valid = false;\n }\n\n if (!aiRole) {\n showError('ai_role', '请选择 AI 助手的身份');\n valid = false;\n }\n\n return valid;\n}\n\n// 显示错误提示\nfunction showError(fieldId, message) {\n const field = document.getElementById(fieldId);\n const errorEl = field.parentElement.querySelector('.error-msg');\n if (errorEl) {\n errorEl.textContent = message;\n }\n}\n\n// 收集表单数据\nfunction collectFormData() {\n const identityType = document.getElementById('identity_type').value;\n const identityDetail = {};\n\n // 根据身份类型收集详情\n if (identityType === 'student') {\n const educationStage = document.getElementById('education_stage')?.value;\n const major = document.getElementById('major')?.value.trim();\n if (educationStage) identityDetail.education_stage = educationStage;\n if (major) identityDetail.major = major;\n } else if (identityType === 'worker') {\n const industry = document.getElementById('industry')?.value.trim();\n const jobTitle = document.getElementById('job_title')?.value.trim();\n if (industry) identityDetail.industry = industry;\n if (jobTitle) identityDetail.job_title = jobTitle;\n } else if (identityType === 'teacher') {\n const subject = document.getElementById('subject')?.value.trim();\n const teachingStage = document.getElementById('teaching_stage')?.value.trim();\n if (subject) identityDetail.subject = subject;\n if (teachingStage) identityDetail.teaching_stage = teachingStage;\n } else if (identityType === 'freelancer') {\n const field = document.getElementById('field')?.value.trim();\n if (field) identityDetail.field = field;\n } else if (identityType === 'other') {\n const description = document.getElementById('description')?.value.trim();\n if (description) identityDetail.description = description;\n }\n\n // 收集使用场景(第一步的复选框)\n const useCases = [];\n document.querySelectorAll('#step1 .checkbox-group input[type=\"checkbox\"]:checked').forEach(cb => {\n useCases.push(cb.value);\n });\n\n // 收集性格特点(第二步的复选框)\n const personality = [];\n document.querySelectorAll('#step2 .checkbox-group input[type=\"checkbox\"]:checked').forEach(cb => {\n personality.push(cb.value);\n });\n\n return {\n username: document.getElementById('username').value.trim(),\n password: document.getElementById('password').value,\n identity_type: identityType,\n identity_detail: Object.keys(identityDetail).length > 0 ? identityDetail : null,\n use_cases: useCases,\n interests: [], // 暂时留空,表单中没有这个\n ai_customization: {\n ai_name: document.getElementById('ai_name').value.trim(),\n ai_role: document.getElementById('ai_role').value,\n personality: personality,\n communication_style: document.getElementById('communication_style').value,\n },\n };\n}\n\n// ========== 聊天页面 ==========\nfunction initChatPage() {\n const userId = localStorage.getItem('userId');\n const aiName = localStorage.getItem('aiName') || 'AI 助手';\n const username = localStorage.getItem('username') || '用户';\n\n if (!userId) {\n window.location.href = 'index.html';\n return;\n }\n\n // 配置 marked + highlight.js\n if (typeof marked !== 'undefined') {\n marked.setOptions({\n highlight: function(code, lang) {\n if (typeof hljs !== 'undefined') {\n if (lang && hljs.getLanguage(lang)) {\n return hljs.highlight(code, { language: lang }).value;\n }\n return hljs.highlightAuto(code).value;\n }\n return code;\n },\n breaks: true\n });\n }\n\n // 显示 AI 名字\n document.getElementById('ai_name_display').textContent = aiName;\n\n const chatMessages = document.getElementById('chat_messages');\n const userInput = document.getElementById('user_input');\n const sendBtn = document.getElementById('sendBtn');\n const logoutBtn = document.getElementById('logoutBtn');\n\n // 退出登录\n if (logoutBtn) {\n logoutBtn.addEventListener('click', async function() {\n const sessionId = localStorage.getItem('sessionId');\n try {\n // 调用后端登出 API\n await api.logout(userId, sessionId);\n } catch (error) {\n console.error('登出 API 调用失败:', error);\n }\n // 清除本地存储\n localStorage.removeItem('userId');\n localStorage.removeItem('username');\n localStorage.removeItem('aiName');\n localStorage.removeItem('sessionId');\n window.location.href = 'index.html';\n });\n }\n\n // 发送消息\n async function sendMessage() {\n const message = userInput.value.trim();\n if (!message) return;\n\n // 显示用户消息\n appendMessage('user', message);\n userInput.value = '';\n\n // 创建 AI 消息容器(带思考动画)\n const aiMessageEl = appendMessage('ai', '', true);\n let fullResponse = '';\n\n try {\n await api.chat(\n userId,\n message,\n // onChunk - 流式接收内容\n (chunk) => {\n fullResponse += chunk;\n // 流式渲染 Markdown\n renderAIMarkdown(aiMessageEl, fullResponse);\n chatMessages.scrollTop = chatMessages.scrollHeight;\n },\n // onDone - 完成\n (data) => {\n console.log('对话完成:', data);\n // 移除思考动画\n removeThinkingAnimation(aiMessageEl);\n }\n );\n } catch (error) {\n console.error(error);\n removeThinkingAnimation(aiMessageEl);\n aiMessageEl.querySelector('.message-content').textContent = '网络错误,请重试';\n }\n }\n\n sendBtn.addEventListener('click', sendMessage);\n userInput.addEventListener('keypress', function (e) {\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n sendMessage();\n }\n });\n}\n\n// 渲染 AI 消息的 Markdown\nfunction renderAIMarkdown(messageEl, content) {\n if (typeof marked !== 'undefined') {\n const html = marked.parse(content);\n messageEl.querySelector('.message-content').innerHTML = html;\n // 高亮代码块\n if (typeof hljs !== 'undefined') {\n messageEl.querySelectorAll('pre code').forEach((block) => {\n hljs.highlightElement(block);\n });\n }\n }\n}\n\n// 移除思考动画\nfunction removeThinkingAnimation(messageEl) {\n const thinkingEl = messageEl.querySelector('.thinking-indicator');\n if (thinkingEl) {\n thinkingEl.remove();\n }\n}\n\n// 添加消息到聊天窗口\nfunction appendMessage(role, content, showThinking = false) {\n const chatMessages = document.getElementById('chat_messages');\n const messageDiv = document.createElement('div');\n messageDiv.className = `message ${role}`;\n\n // 获取头像显示文字\n const aiName = localStorage.getItem('aiName') || 'AI';\n const username = localStorage.getItem('username') || 'U';\n const avatarText = role === 'ai' ? aiName.charAt(0).toUpperCase() : username.charAt(0).toUpperCase();\n\n // 格式化时间\n const time = new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });\n\n if (role === 'ai') {\n // AI 消息\n let thinkingHtml = '';\n if (showThinking || content === '') {\n // 思考动画放在 message-body 外面,定位到气泡右上角\n thinkingHtml = `\n
\n \n \n \n
\n `;\n }\n\n let contentHtml = '';\n if (content) {\n const renderedContent = typeof marked !== 'undefined' ? marked.parse(content) : escapeHtml(content);\n contentHtml = `
${renderedContent}
`;\n } else {\n contentHtml = '
';\n }\n\n messageDiv.innerHTML = `\n
${avatarText}
\n
\n ${thinkingHtml}\n
\n ${contentHtml}\n ${time}\n
\n
\n `;\n } else {\n // 用户消息保持纯文本\n messageDiv.innerHTML = `\n
${avatarText}
\n
\n
${escapeHtml(content)}
\n ${time}\n
\n `;\n }\n\n chatMessages.appendChild(messageDiv);\n chatMessages.scrollTop = chatMessages.scrollHeight;\n return messageDiv;\n}\n\n// HTML 转义\nfunction escapeHtml(text) {\n const div = document.createElement('div');\n div.textContent = text;\n return div.innerHTML;\n}\n"}}}}