Files
ai-chat-app/www/app.js
hubian c8cea4c1a1 feat: 登录注册系统 + 游客使用限制
功能:
- 登录/注册界面(本地存储用户数据)
- 我的页面显示用户状态、登录/退出按钮
- 游客配额显示(对话会话/消息/智能体消息)

游客限制:
- 每天最多1个对话会话
- 每天最多20条对话消息
- 只能使用通用助手智能体
- 每天最多20条智能体消息
- 登录用户无限制

限制提示:
- 达到限制弹出提示对话框
- 提示登录解锁全部功能
2026-04-27 11:06:20 +08:00

3653 lines
122 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// AI助手 - 前端应用
// 使用智谱 GLM-4.5-Air 模型(流式输出 + 多对话管理)
const CONFIG = {
apiUrl: 'https://open.bigmodel.cn/api/paas/v4/chat/completions',
apiKey: '2259e33a1357460abe17919aaf81e73d.K44a8LPQTmFM5PKm',
model: 'glm-4.5-air',
maxTokens: 2048,
// Tavily Search API
tavilyApiUrl: 'https://api.tavily.com/search',
tavilyApiKey: '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'
};
// 数据结构
let conversations = []; // 对话列表
let currentConversation = null; // 当前对话
let isLoading = false;
// 当前页面状态
let currentPage = 'chats'; // chats | agents | profile
// 系统智能体数据(完整列表)
let systemAgents = [
// 热门智能体
{ id: 'assistant', name: '通用助手', avatar: '🤖', category: 'hot', desc: '能回答各类问题,帮助写作、分析、解答疑惑', systemPrompt: '你是一个智能助手,能够回答各类问题,帮助用户解决问题。', heat: 9500 },
{ id: 'writer', name: '写作助手', avatar: '✍️', category: 'hot', desc: '专注于文章写作、文案创作、内容润色', systemPrompt: '你是一个专业的写作助手,擅长各类文章写作、文案创作和内容润色。', heat: 8800 },
{ id: 'coder', name: '编程助手', avatar: '👨‍💻', category: 'hot', desc: '精通编程语言,解答技术问题,生成代码', systemPrompt: '你是一个专业的编程助手,精通各类编程语言,能够解答技术问题并生成高质量代码。', heat: 8500 },
{ id: 'translator', name: '翻译助手', avatar: '🌐', category: 'hot', desc: '多语言翻译,精准表达,文化适配', systemPrompt: '你是一个专业的翻译助手,精通多语言翻译,能够精准表达并适配文化差异。', heat: 7200 },
// 工作助手
{ id: 'assistant-work', name: '工作助手', avatar: '💼', category: 'work', desc: '职场问题解答,工作效率提升', systemPrompt: '你是一个工作助手,帮助解决职场问题,提升工作效率。', heat: 5000 },
{ id: 'ppt', name: 'PPT助手', avatar: '📊', category: 'work', desc: 'PPT内容生成结构优化设计建议', systemPrompt: '你是一个PPT助手擅长PPT内容生成、结构优化和设计建议。', heat: 4500 },
{ id: 'excel', name: 'Excel助手', avatar: '📈', category: 'work', desc: 'Excel公式、数据分析、表格优化', systemPrompt: '你是一个Excel助手精通Excel公式、数据分析和表格优化。', heat: 4000 },
// 学习助手
{ id: 'teacher', name: '学习助手', avatar: '📚', category: 'study', desc: '知识讲解,学习方法,考试辅导', systemPrompt: '你是一个学习助手,擅长知识讲解、学习方法指导和考试辅导。', heat: 5500 },
{ id: 'english', name: '英语助手', avatar: '🔤', category: 'study', desc: '英语学习,语法纠正,口语练习', systemPrompt: '你是一个英语助手,帮助英语学习、语法纠正和口语练习。', heat: 5000 },
{ id: 'math', name: '数学助手', avatar: '🔢', category: 'study', desc: '数学解题,公式推导,概念讲解', systemPrompt: '你是一个数学助手,擅长数学解题、公式推导和概念讲解。', heat: 4500 },
// 生活助手
{ id: 'health', name: '健康助手', avatar: '🏥', category: 'life', desc: '健康咨询,养生建议,运动指导', systemPrompt: '你是一个健康助手,提供健康咨询、养生建议和运动指导。', heat: 3500 },
{ id: 'travel', name: '旅行助手', avatar: '✈️', category: 'life', desc: '旅行规划,景点推荐,美食指南', systemPrompt: '你是一个旅行助手,擅长旅行规划、景点推荐和美食指南。', heat: 3200 },
{ id: 'food', name: '美食助手', avatar: '🍳', category: 'life', desc: '菜谱推荐,烹饪技巧,营养搭配', systemPrompt: '你是一个美食助手,提供菜谱推荐、烹饪技巧和营养搭配建议。', heat: 3000 },
];
// 用户智能体界面显示的智能体(默认显示热门智能体)
let agents = [];
// 用户添加的智能体(按类别分组)
let myAgents = {
hot: ['assistant', 'writer', 'coder', 'translator'],
work: ['assistant-work', 'ppt', 'excel'],
study: ['teacher', 'english', 'math'],
life: ['health', 'travel', 'food']
};
// 收藏的智能体列表
let favoriteAgents = [];
// 置顶的智能体列表(按类别)
let pinnedAgents = {
hot: [],
work: [],
study: [],
life: []
};
let currentAgent = null; // 当前选中的智能体
// 用户状态
let currentUser = null; // 当前登录用户 { username, password, registeredAt }
// 每日使用统计(未登录用户)
let dailyUsage = {
date: null, // 日期 YYYY-MM-DD
chatSessions: 0, // 对话会话数
chatMessages: 0, // 对话消息数
agentUsed: null, // 使用的智能体ID只能用一个
agentMessages: 0 // 智能体消息数
};
// 未登录用户限制
const GUEST_LIMITS = {
maxChatSessionsPerDay: 1, // 每天最多1个对话会话
maxChatMessagesPerDay: 20, // 每天最多20条对话消息
maxAgentPerDay: 1, // 每天只能用1个智能体通用助手
maxAgentMessagesPerDay: 20 // 每天最多20条智能体消息
};
// 获取今日日期字符串
function getTodayDate() {
const today = new Date();
return `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
}
// 检查并重置每日使用统计
function checkDailyUsage() {
const today = getTodayDate();
if (dailyUsage.date !== today) {
// 新的一天,重置统计
dailyUsage = {
date: today,
chatSessions: 0,
chatMessages: 0,
agentUsed: null,
agentMessages: 0
};
saveDailyUsage();
}
}
// 保存用户状态
function saveCurrentUser() {
if (currentUser) {
localStorage.setItem('currentUser', JSON.stringify(currentUser));
} else {
localStorage.removeItem('currentUser');
}
}
// 保存每日使用统计
function saveDailyUsage() {
localStorage.setItem('dailyUsage', JSON.stringify(dailyUsage));
}
// 检查是否可以创建新对话(未登录用户)
function canCreateNewChat() {
if (currentUser) return true; // 登录用户无限制
checkDailyUsage();
return dailyUsage.chatSessions < GUEST_LIMITS.maxChatSessionsPerDay;
}
// 检查是否可以发送对话消息(未登录用户)
function canSendChatMessage() {
if (currentUser) return true; // 登录用户无限制
checkDailyUsage();
return dailyUsage.chatMessages < GUEST_LIMITS.maxChatMessagesPerDay;
}
// 检查是否可以使用智能体(未登录用户)
function canUseAgent(agentId) {
if (currentUser) return true; // 登录用户无限制
checkDailyUsage();
// 未登录用户只能使用通用助手
if (agentId !== 'assistant') {
return { allowed: false, reason: 'guest_agent_only' };
}
// 如果今天还没用智能体,可以用
if (!dailyUsage.agentUsed) {
return { allowed: true };
}
// 如果已经用了其他智能体,不能换
if (dailyUsage.agentUsed !== agentId) {
return { allowed: false, reason: 'guest_one_agent_only' };
}
return { allowed: true };
}
// 检查是否可以发送智能体消息(未登录用户)
function canSendAgentMessage() {
if (currentUser) return true; // 登录用户无限制
checkDailyUsage();
return dailyUsage.agentMessages < GUEST_LIMITS.maxAgentMessagesPerDay;
}
// 增加对话会话计数
function incrementChatSession() {
if (!currentUser) {
checkDailyUsage();
dailyUsage.chatSessions++;
saveDailyUsage();
}
}
// 增加对话消息计数
function incrementChatMessage() {
if (!currentUser) {
checkDailyUsage();
dailyUsage.chatMessages++;
saveDailyUsage();
}
}
// 设置使用的智能体(未登录用户)
function setAgentUsed(agentId) {
if (!currentUser) {
checkDailyUsage();
if (!dailyUsage.agentUsed) {
dailyUsage.agentUsed = agentId;
saveDailyUsage();
}
}
}
// 增加智能体消息计数
function incrementAgentMessage() {
if (!currentUser) {
checkDailyUsage();
dailyUsage.agentMessages++;
saveDailyUsage();
}
}
// 获取剩余配额提示
function getRemainingQuota() {
if (currentUser) return null; // 登录用户无限制
checkDailyUsage();
const chatSessionRemain = GUEST_LIMITS.maxChatSessionsPerDay - dailyUsage.chatSessions;
const chatMsgRemain = GUEST_LIMITS.maxChatMessagesPerDay - dailyUsage.chatMessages;
const agentMsgRemain = GUEST_LIMITS.maxAgentMessagesPerDay - dailyUsage.agentMessages;
return {
chatSessionRemain,
chatMsgRemain,
agentMsgRemain,
agentUsed: dailyUsage.agentUsed
};
}
// 获取使用智能体的对话列表(按时间倒序)
function getAgentConversationHistory(limit = 5) {
return conversations
.filter(conv => conv.agentId) // 筛选有智能体的对话
.sort((a, b) => b.updatedAt - a.updatedAt) // 按更新时间倒序
.slice(0, limit)
.map(conv => {
const agent = systemAgents.find(a => a.id === conv.agentId);
return {
...conv,
agent: agent
};
});
}
// 功能开关
let enableThinking = false; // 深度思考
let enableSearch = false; // 联网搜索
let autoScrollEnabled = true; // 自动滚动(用户滚动后可关闭)
// DOM 元素(初始为 null在 openConversation 时重新获取)
let appContainer = null;
let messagesContainer = null;
let messagesDiv = null;
let userInput = null;
let sendBtn = null;
let welcome = null;
let thinkingBtn = null;
let searchBtn = null;
// 初始化
document.addEventListener('DOMContentLoaded', () => {
// 初始化 appContainer
appContainer = document.getElementById('app');
// 从本地存储加载对话列表
const saved = localStorage.getItem('conversations');
if (saved) {
conversations = JSON.parse(saved);
}
// 兼容旧数据格式chat_history
const oldHistory = localStorage.getItem('chat_history');
if (oldHistory && conversations.length === 0) {
const oldMessages = JSON.parse(oldHistory);
if (oldMessages.length > 0) {
// 转换旧数据为新格式
const convertedConv = {
id: Date.now().toString(),
title: oldMessages[0].content.slice(0, 30) + (oldMessages[0].content.length > 30 ? '...' : ''),
messages: oldMessages,
createdAt: Date.now(),
updatedAt: Date.now()
};
conversations.push(convertedConv);
saveConversations();
localStorage.removeItem('chat_history'); // 清理旧数据
}
}
// 加载用户智能体数据
const savedMyAgents = localStorage.getItem('myAgents');
if (savedMyAgents) {
myAgents = JSON.parse(savedMyAgents);
}
// 加载收藏的智能体
const savedFavoriteAgents = localStorage.getItem('favoriteAgents');
if (savedFavoriteAgents) {
favoriteAgents = JSON.parse(savedFavoriteAgents);
}
// 加载置顶的智能体
const savedPinnedAgents = localStorage.getItem('pinnedAgents');
if (savedPinnedAgents) {
pinnedAgents = JSON.parse(savedPinnedAgents);
}
// 加载用户登录状态
const savedUser = localStorage.getItem('currentUser');
if (savedUser) {
currentUser = JSON.parse(savedUser);
}
// 加载每日使用统计
const savedDailyUsage = localStorage.getItem('dailyUsage');
if (savedDailyUsage) {
dailyUsage = JSON.parse(savedDailyUsage);
}
// 检查并重置每日使用统计
checkDailyUsage();
// 根据用户配置更新显示的智能体
updateAgentsDisplay();
// 加载当前页面状态
const savedPage = localStorage.getItem('currentPage');
if (savedPage) {
currentPage = savedPage;
}
// 显示主页
showMainPage();
});
// 根据用户配置更新显示的智能体
function updateAgentsDisplay() {
agents = [];
Object.keys(myAgents).forEach(category => {
myAgents[category].forEach(agentId => {
const agent = systemAgents.find(a => a.id === agentId);
if (agent) {
agents.push({
...agent,
is_pinned: pinnedAgents[category]?.includes(agentId) || false,
is_favorite: favoriteAgents.includes(agentId)
});
}
});
});
}
// 保存用户智能体配置
function saveMyAgents() {
localStorage.setItem('myAgents', JSON.stringify(myAgents));
updateAgentsDisplay();
}
// 保存收藏的智能体
function saveFavoriteAgents() {
localStorage.setItem('favoriteAgents', JSON.stringify(favoriteAgents));
}
// 保存置顶的智能体
function savePinnedAgents() {
localStorage.setItem('pinnedAgents', JSON.stringify(pinnedAgents));
}
// ==================== 主页(底部导航栏) ====================
function showMainPage() {
currentConversation = null;
currentAgent = null;
const mainHtml = `
<div class="main-page">
<div class="main-content" id="mainContent">
${renderCurrentPage()}
</div>
<!-- 底部导航栏 -->
<nav class="bottom-nav">
<div class="nav-item ${currentPage === 'chats' ? 'active' : ''}" data-page="chats">
<svg viewBox="0 0 24 24" width="24" height="24"><path fill="currentColor" d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6l-2 2V4h16v12z"/></svg>
<span>对话</span>
</div>
<div class="nav-item ${currentPage === 'agents' ? 'active' : ''}" data-page="agents">
<svg viewBox="0 0 24 24" width="24" height="24"><path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z"/></svg>
<span>智能体</span>
</div>
<div class="nav-item ${currentPage === 'profile' ? 'active' : ''}" data-page="profile">
<svg viewBox="0 0 24 24" width="24" height="24"><path fill="currentColor" d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>
<span>我的</span>
</div>
</nav>
</div>
`;
appContainer.innerHTML = mainHtml;
// 绑定底部导航事件
document.querySelectorAll('.nav-item').forEach(item => {
item.addEventListener('click', () => {
const page = item.getAttribute('data-page');
switchPage(page);
});
});
// 根据当前页面绑定其他事件
bindPageEvents();
}
// 渲染当前页面内容
function renderCurrentPage() {
switch (currentPage) {
case 'chats':
return renderChatsPage();
case 'agents':
return renderAgentsPage();
case 'profile':
return renderProfilePage();
default:
return renderChatsPage();
}
}
// 切换页面
function switchPage(page) {
currentPage = page;
localStorage.setItem('currentPage', page);
// 更新导航栏状态
document.querySelectorAll('.nav-item').forEach(item => {
item.classList.toggle('active', item.getAttribute('data-page') === page);
});
// 更新内容区
const mainContent = document.getElementById('mainContent');
if (mainContent) {
mainContent.innerHTML = renderCurrentPage();
}
// 绑定页面事件
bindPageEvents();
}
// 绑定页面事件
function bindPageEvents() {
switch (currentPage) {
case 'chats':
bindChatsPageEvents();
break;
case 'agents':
bindAgentsPageEvents();
break;
case 'profile':
bindProfilePageEvents();
break;
}
}
// ==================== 对话页面 ====================
function renderChatsPage() {
// 只显示没有智能体的普通对话
const normalConversations = conversations.filter(conv => !conv.agentId);
return `
<div class="chats-page">
<header class="chats-header">
<h1>对话</h1>
<div class="chats-header-actions">
<button class="chats-header-btn" id="searchToggleBtn" title="搜索">
<svg viewBox="0 0 24 24" width="20" height="20"><path fill="currentColor" d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 7 9.5 7 14 9.01 14 9.5 11.99 14 9.5 14z"/></svg>
</button>
<button class="chats-header-btn" id="newChatBtn" title="新建对话">
<svg viewBox="0 0 24 24" width="20" height="20"><path fill="currentColor" d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
</button>
</div>
</header>
<div class="chats-content">
<div class="conversation-list" id="conversationList">
${normalConversations.length === 0
? '<div class="empty-list">暂无对话记录<br><br>点击右上角 + 开始新对话</div>'
: sortNormalConversations(normalConversations).map(conv => `
<div class="conversation-item ${conv.is_pinned ? 'pinned' : ''}" data-id="${conv.id}">
${conv.is_pinned ? '<span class="pin-icon">📌</span>' : ''}
<div class="conv-title">${escapeHtml(conv.title)}</div>
<div class="conv-meta">${conv.messages.length} 条消息 · ${formatTime(conv.updatedAt)}</div>
</div>
`).join('')
}
</div>
</div>
<!-- 搜索栏 -->
<div class="search-bar" id="searchBar">
<div class="search-input-wrapper">
<svg viewBox="0 0 24 24" width="20" height="20"><path fill="currentColor" d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 7 9.5 7 14 9.01 14 9.5 11.99 14 9.5 14z"/></svg>
<input type="text" id="searchInput" placeholder="搜索对话标题或内容...">
<button class="search-close-btn" id="searchCloseBtn">
<svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/></svg>
</button>
</div>
<div class="search-results" id="searchResults"></div>
</div>
</div>
`;
}
function bindChatsPageEvents() {
const newChatBtn = document.getElementById('newChatBtn');
if (newChatBtn) {
newChatBtn.addEventListener('click', createNewConversation);
}
// 搜索功能
const searchToggleBtn = document.getElementById('searchToggleBtn');
const searchBar = document.getElementById('searchBar');
const searchInput = document.getElementById('searchInput');
const searchCloseBtn = document.getElementById('searchCloseBtn');
const searchResults = document.getElementById('searchResults');
if (searchToggleBtn) {
searchToggleBtn.addEventListener('click', () => {
if (searchBar) {
searchBar.classList.add('show');
if (searchInput) {
searchInput.focus();
}
}
});
}
if (searchCloseBtn) {
searchCloseBtn.addEventListener('click', () => {
hideSearchBarInChats();
});
}
if (searchInput) {
searchInput.addEventListener('input', (e) => {
const keyword = e.target.value.trim();
if (keyword) {
searchConversations(keyword);
} else {
if (searchResults) searchResults.innerHTML = '';
}
});
searchInput.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
hideSearchBarInChats();
}
});
}
if (searchResults) {
searchResults.addEventListener('click', (e) => {
const item = e.target.closest('.search-result-item');
if (item) {
const id = item.getAttribute('data-id');
hideSearchBarInChats();
openConversation(id);
}
});
}
function hideSearchBarInChats() {
if (searchBar) {
searchBar.classList.remove('show');
}
if (searchInput) {
searchInput.value = '';
}
if (searchResults) {
searchResults.innerHTML = '';
}
}
const conversationList = document.getElementById('conversationList');
if (conversationList) {
conversationList.addEventListener('click', (e) => {
const item = e.target.closest('.conversation-item');
if (item) {
const id = item.getAttribute('data-id');
openConversation(id);
}
});
// 长按事件
setupLongPressEvents(conversationList);
}
}
// ==================== 智能体页面 ====================
function renderAgentsPage() {
// 根据用户配置筛选智能体
const hotAgents = agents.filter(a => a.category === 'hot');
const workAgents = agents.filter(a => a.category === 'work');
const studyAgents = agents.filter(a => a.category === 'study');
const lifeAgents = agents.filter(a => a.category === 'life');
// 获取使用智能体的对话历史最多5个
const recentAgentConvos = getAgentConversationHistory(5);
const totalAgentConvos = conversations.filter(c => c.agentId).length;
// 是否有收藏的智能体
const hasFavorites = favoriteAgents.length > 0;
return `
<div class="agents-page">
<header class="agents-header">
<h1>智能体</h1>
<div class="agents-header-actions">
<button class="agents-header-btn" id="agentDiscoverBtn" title="发现智能体">
<svg viewBox="0 0 24 24" width="20" height="20"><path fill="currentColor" d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 7 9.5 7 14 9.01 14 9.5 11.99 14 9.5 14z"/></svg>
</button>
<button class="agents-header-btn favorite-btn ${hasFavorites ? 'has-favorites' : ''}" id="agentFavoriteBtn" title="收藏夹">
${hasFavorites ? '★' : '☆'}
</button>
</div>
</header>
<div class="agents-content">
<!-- 最近使用 -->
${recentAgentConvos.length > 0 ? `
<div class="agents-section">
<div class="section-title-row">
<div class="section-title">🕐 最近使用</div>
${totalAgentConvos > 5 ? `
<div class="section-more" id="showAllRecentBtn">更多&gt;&gt;</div>
` : ''}
</div>
<div class="recent-agents-list">
${recentAgentConvos.map(conv => `
<div class="recent-agent-item" data-conv-id="${conv.id}">
<div class="recent-agent-left">
<span class="recent-agent-avatar">${conv.agent ? conv.agent.avatar : '🤖'}</span>
<span class="recent-agent-name">${conv.title}</span>
</div>
<div class="recent-agent-right">
<span class="recent-agent-agent-name">${conv.agent ? conv.agent.name : '未知智能体'}</span>
<span class="recent-agent-time">${formatTime(conv.updatedAt)}</span>
</div>
</div>
`).join('')}
</div>
</div>
` : ''}
<!-- 热门智能体 -->
${hotAgents.length > 0 ? `
<div class="agents-section">
<div class="section-title">🔥 热门智能体</div>
<div class="agents-grid">
${hotAgents.map(agent => renderAgentCard(agent)).join('')}
</div>
</div>
` : ''}
<!-- 工作助手 -->
${workAgents.length > 0 ? `
<div class="agents-section">
<div class="section-title">💼 工作助手</div>
<div class="agents-grid">
${workAgents.map(agent => renderAgentCard(agent)).join('')}
</div>
</div>
` : ''}
<!-- 学习助手 -->
${studyAgents.length > 0 ? `
<div class="agents-section">
<div class="section-title">📚 学习助手</div>
<div class="agents-grid">
${studyAgents.map(agent => renderAgentCard(agent)).join('')}
</div>
</div>
` : ''}
<!-- 生活助手 -->
${lifeAgents.length > 0 ? `
<div class="agents-section">
<div class="section-title">🏠 生活助手</div>
<div class="agents-grid">
${lifeAgents.map(agent => renderAgentCard(agent)).join('')}
</div>
</div>
` : ''}
</div>
<!-- 智能体操作菜单 -->
<div class="agent-action-menu" id="agentActionMenu">
<div class="agent-action-menu-content">
<div class="agent-action-item" data-action="pin">
<svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M16 12V4h1V2H7v2h1v8l-2 2v2h5.2v6h1.6v-6H18v-2l-2-2z"/></svg>
<span id="agentPinText">置顶</span>
</div>
<div class="agent-action-item" data-action="favorite">
<svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/></svg>
<span id="agentFavoriteText">收藏</span>
</div>
<div class="agent-action-item delete-action" data-action="remove">
<svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
<span>移除</span>
</div>
</div>
</div>
</div>
`;
}
// 渲染智能体卡片
function renderAgentCard(agent) {
const pinnedClass = agent.is_pinned ? 'pinned' : '';
const favoriteClass = agent.is_favorite ? 'favorite' : '';
return `
<div class="agent-card ${pinnedClass} ${favoriteClass}" data-id="${agent.id}">
${agent.is_pinned ? '<span class="agent-pin-icon">📌</span>' : ''}
${agent.is_favorite ? '<span class="agent-fav-icon">⭐</span>' : ''}
<div class="agent-avatar">${agent.avatar}</div>
<div class="agent-name">${agent.name}</div>
<div class="agent-desc">${agent.desc}</div>
</div>
`;
}
function bindAgentsPageEvents() {
// 发现智能体按钮
const agentDiscoverBtn = document.getElementById('agentDiscoverBtn');
if (agentDiscoverBtn) {
agentDiscoverBtn.addEventListener('click', () => {
showAgentDiscoverPage();
});
}
// 收藏夹按钮
const agentFavoriteBtn = document.getElementById('agentFavoriteBtn');
if (agentFavoriteBtn) {
agentFavoriteBtn.addEventListener('click', () => {
showAgentFavoritePage();
});
}
// 智能体卡片点击(新建对话)
document.querySelectorAll('.agent-card').forEach(card => {
card.addEventListener('click', (e) => {
// 如果是长按后的点击,不触发
if (card.classList.contains('long-pressed')) {
card.classList.remove('long-pressed');
return;
}
const id = card.getAttribute('data-id');
openAgent(id);
});
// 长按事件
setupAgentLongPress(card);
});
// 最近使用对话点击(打开已有对话)
document.querySelectorAll('.recent-agent-item').forEach(item => {
item.addEventListener('click', () => {
const convId = item.getAttribute('data-conv-id');
if (convId) {
openConversation(convId);
}
});
});
// 查看全部历史使用
const showAllRecentBtn = document.getElementById('showAllRecentBtn');
if (showAllRecentBtn) {
showAllRecentBtn.addEventListener('click', () => {
showAgentHistoryPage();
});
}
// 智能体操作菜单事件
const agentActionMenu = document.getElementById('agentActionMenu');
if (agentActionMenu) {
agentActionMenu.addEventListener('click', (e) => {
const item = e.target.closest('.agent-action-item');
if (item && currentActionAgentId) {
const action = item.getAttribute('data-action');
handleAgentAction(action, currentActionAgentId);
hideAgentActionMenu();
}
});
// 点击其他地方关闭菜单
document.addEventListener('click', (e) => {
if (agentActionMenu.classList.contains('show') && !agentActionMenu.contains(e.target)) {
hideAgentActionMenu();
}
});
}
}
// 当前操作的智能体ID
let currentActionAgentId = null;
// 智能体长按事件
function setupAgentLongPress(card) {
let longPressTimer = null;
card.addEventListener('touchstart', (e) => {
longPressTimer = setTimeout(() => {
currentActionAgentId = card.getAttribute('data-id');
card.classList.add('long-pressed');
showAgentActionMenu(currentActionAgentId);
}, 500);
});
card.addEventListener('touchend', () => {
if (longPressTimer) {
clearTimeout(longPressTimer);
longPressTimer = null;
}
});
card.addEventListener('touchmove', () => {
if (longPressTimer) {
clearTimeout(longPressTimer);
longPressTimer = null;
}
});
// 鼠标长按PC端
card.addEventListener('mousedown', (e) => {
longPressTimer = setTimeout(() => {
currentActionAgentId = card.getAttribute('data-id');
card.classList.add('long-pressed');
showAgentActionMenu(currentActionAgentId);
}, 500);
});
card.addEventListener('mouseup', () => {
if (longPressTimer) {
clearTimeout(longPressTimer);
longPressTimer = null;
}
});
card.addEventListener('mouseleave', () => {
if (longPressTimer) {
clearTimeout(longPressTimer);
longPressTimer = null;
}
});
}
// 显示智能体操作菜单
function showAgentActionMenu(agentId) {
const agentActionMenu = document.getElementById('agentActionMenu');
const agent = systemAgents.find(a => a.id === agentId);
const displayAgent = agents.find(a => a.id === agentId);
if (!agentActionMenu || !displayAgent) return;
// 更新按钮文字
const pinText = document.getElementById('agentPinText');
const favoriteText = document.getElementById('agentFavoriteText');
if (pinText) {
pinText.textContent = displayAgent.is_pinned ? '取消置顶' : '置顶';
}
if (favoriteText) {
favoriteText.textContent = displayAgent.is_favorite ? '取消收藏' : '收藏';
}
agentActionMenu.classList.add('show');
}
// 隐藏智能体操作菜单
function hideAgentActionMenu() {
const agentActionMenu = document.getElementById('agentActionMenu');
if (agentActionMenu) {
agentActionMenu.classList.remove('show');
}
currentActionAgentId = null;
}
// 处理智能体操作
function handleAgentAction(action, agentId) {
const agent = systemAgents.find(a => a.id === agentId);
if (!agent) return;
switch (action) {
case 'pin':
toggleAgentPin(agentId);
break;
case 'favorite':
toggleAgentFavorite(agentId);
break;
case 'remove':
removeAgentFromMyAgents(agentId);
break;
}
// 刷新页面
switchPage('agents');
}
// 置顶/取消置顶智能体
function toggleAgentPin(agentId) {
const agent = agents.find(a => a.id === agentId);
if (!agent) return;
const category = agent.category;
if (pinnedAgents[category]?.includes(agentId)) {
// 取消置顶
pinnedAgents[category] = pinnedAgents[category].filter(id => id !== agentId);
agent.is_pinned = false;
showToast('已取消置顶');
} else {
// 置顶
if (!pinnedAgents[category]) {
pinnedAgents[category] = [];
}
pinnedAgents[category].push(agentId);
agent.is_pinned = true;
showToast('已置顶');
}
savePinnedAgents();
saveMyAgents(); // 更新显示
}
// 收藏/取消收藏智能体
function toggleAgentFavorite(agentId) {
const agent = agents.find(a => a.id === agentId);
if (!agent) return;
if (favoriteAgents.includes(agentId)) {
// 取消收藏
favoriteAgents = favoriteAgents.filter(id => id !== agentId);
agent.is_favorite = false;
showToast('已取消收藏');
} else {
// 收藏
favoriteAgents.push(agentId);
agent.is_favorite = true;
showToast('已收藏');
}
saveFavoriteAgents();
saveMyAgents(); // 更新显示
}
// 从用户智能体列表移除
function removeAgentFromMyAgents(agentId) {
const agent = systemAgents.find(a => a.id === agentId);
if (!agent) return;
const category = agent.category;
// 从对应类别移除
if (myAgents[category]) {
myAgents[category] = myAgents[category].filter(id => id !== agentId);
}
// 同时取消置顶
if (pinnedAgents[category]) {
pinnedAgents[category] = pinnedAgents[category].filter(id => id !== agentId);
}
// 同时取消收藏
if (favoriteAgents.includes(agentId)) {
favoriteAgents = favoriteAgents.filter(id => id !== agentId);
}
saveMyAgents();
savePinnedAgents();
saveFavoriteAgents();
showToast('已移除');
}
// ==================== 智能体发现页面 ====================
function showAgentDiscoverPage() {
// 按热度排序所有系统智能体
const sortedAgents = [...systemAgents].sort((a, b) => b.heat - a.heat);
// 分类显示
const hotAgents = systemAgents.filter(a => a.category === 'hot');
const workAgents = systemAgents.filter(a => a.category === 'work');
const studyAgents = systemAgents.filter(a => a.category === 'study');
const lifeAgents = systemAgents.filter(a => a.category === 'life');
const discoverHtml = `
<div class="agent-discover-page">
<header class="discover-header">
<button class="back-btn-white" id="discoverBackBtn">
<svg viewBox="0 0 24 24" width="24" height="24"><path fill="currentColor" d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
</button>
<h1>发现智能体</h1>
</header>
<div class="discover-content">
<!-- 搜索栏 -->
<div class="discover-search">
<svg viewBox="0 0 24 24" width="20" height="20"><path fill="currentColor" d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 7 9.5 7 14 9.01 14 9.5 11.99 14 9.5 14z"/></svg>
<input type="text" id="discoverSearchInput" placeholder="搜索智能体名称或描述...">
</div>
<!-- 热门智能体 -->
<div class="discover-section">
<div class="discover-section-title">🔥 热门智能体</div>
<div class="discover-grid" id="discoverHotGrid">
${hotAgents.map(agent => renderDiscoverCard(agent)).join('')}
</div>
</div>
<!-- 工作助手 -->
<div class="discover-section">
<div class="discover-section-title">💼 工作助手</div>
<div class="discover-grid" id="discoverWorkGrid">
${workAgents.map(agent => renderDiscoverCard(agent)).join('')}
</div>
</div>
<!-- 学习助手 -->
<div class="discover-section">
<div class="discover-section-title">📚 学习助手</div>
<div class="discover-grid" id="discoverStudyGrid">
${studyAgents.map(agent => renderDiscoverCard(agent)).join('')}
</div>
</div>
<!-- 生活助手 -->
<div class="discover-section">
<div class="discover-section-title">🏠 生活助手</div>
<div class="discover-grid" id="discoverLifeGrid">
${lifeAgents.map(agent => renderDiscoverCard(agent)).join('')}
</div>
</div>
</div>
</div>
`;
appContainer.innerHTML = discoverHtml;
// 绑定返回按钮
const backBtn = document.getElementById('discoverBackBtn');
if (backBtn) {
backBtn.addEventListener('click', () => {
showMainPage();
});
}
// 绑定搜索
const searchInput = document.getElementById('discoverSearchInput');
if (searchInput) {
searchInput.addEventListener('input', (e) => {
const keyword = e.target.value.trim().toLowerCase();
filterDiscoverAgents(keyword);
});
}
// 绑定添加/收藏按钮
bindDiscoverCardEvents();
}
// 渲染发现页面智能体卡片
function renderDiscoverCard(agent) {
const isAdded = myAgents[agent.category]?.includes(agent.id);
const isFavorite = favoriteAgents.includes(agent.id);
// 格式化热度显示
let heatDisplay = '';
if (agent.heat >= 9000) {
heatDisplay = '🔥🔥🔥';
} else if (agent.heat >= 6000) {
heatDisplay = '🔥🔥';
} else if (agent.heat >= 3000) {
heatDisplay = '🔥';
} else {
heatDisplay = `${Math.floor(agent.heat / 1000)}k`;
}
return `
<div class="discover-card" data-id="${agent.id}">
<div class="discover-card-header">
<div class="discover-avatar">${agent.avatar}</div>
<div class="discover-card-info">
<div class="discover-name">${agent.name}</div>
<div class="discover-meta">
<span class="discover-category">${getCategoryLabel(agent.category)}</span>
<span class="discover-heat">${heatDisplay}</span>
</div>
</div>
</div>
<div class="discover-desc">${agent.desc}</div>
<div class="discover-actions">
<button class="discover-action-btn add-btn ${isAdded ? 'added' : ''}" data-id="${agent.id}" data-action="add">
${isAdded ? '已添加' : '+ 添加'}
</button>
<button class="discover-action-btn favorite-btn ${isFavorite ? 'favorited' : ''}" data-id="${agent.id}" data-action="favorite">
${isFavorite ? '★ 已收藏' : '☆ 收藏'}
</button>
</div>
</div>
`;
}
// 获取类别标签
function getCategoryLabel(category) {
const labels = {
hot: '热门',
work: '工作',
study: '学习',
life: '生活'
};
return labels[category] || category;
}
// 搜索过滤发现页面智能体
function filterDiscoverAgents(keyword) {
if (!keyword) {
// 显示所有
showDiscoverSection('hot', systemAgents.filter(a => a.category === 'hot'));
showDiscoverSection('work', systemAgents.filter(a => a.category === 'work'));
showDiscoverSection('study', systemAgents.filter(a => a.category === 'study'));
showDiscoverSection('life', systemAgents.filter(a => a.category === 'life'));
return;
}
// 搜索所有智能体
const filtered = systemAgents.filter(agent =>
agent.name.toLowerCase().includes(keyword) ||
agent.desc.toLowerCase().includes(keyword)
);
// 显示搜索结果
showDiscoverSection('hot', filtered.filter(a => a.category === 'hot'));
showDiscoverSection('work', filtered.filter(a => a.category === 'work'));
showDiscoverSection('study', filtered.filter(a => a.category === 'study'));
showDiscoverSection('life', filtered.filter(a => a.category === 'life'));
}
// 显示发现页面某个类别的智能体
function showDiscoverSection(category, agents) {
const gridId = `discover${category.charAt(0).toUpperCase() + category.slice(1)}Grid`;
const grid = document.getElementById(gridId);
if (grid) {
grid.innerHTML = agents.map(agent => renderDiscoverCard(agent)).join('');
bindDiscoverCardEvents();
}
}
// 绑定发现页面卡片按钮事件
function bindDiscoverCardEvents() {
document.querySelectorAll('.discover-action-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const agentId = btn.getAttribute('data-id');
const action = btn.getAttribute('data-action');
if (action === 'add') {
addAgentToMyAgents(agentId);
btn.classList.toggle('added');
btn.textContent = btn.classList.contains('added') ? '已添加' : '+ 添加';
} else if (action === 'favorite') {
toggleAgentFavoriteFromDiscover(agentId);
btn.classList.toggle('favorited');
btn.textContent = btn.classList.contains('favorited') ? '★ 已收藏' : '☆ 收藏';
}
});
});
}
// 添加智能体到用户列表
function addAgentToMyAgents(agentId) {
const agent = systemAgents.find(a => a.id === agentId);
if (!agent) return;
// 检查是否已添加
if (myAgents[agent.category]?.includes(agentId)) {
showToast('已添加过该智能体');
return;
}
// 添加到对应类别
if (!myAgents[agent.category]) {
myAgents[agent.category] = [];
}
myAgents[agent.category].push(agentId);
saveMyAgents();
showToast(`已添加 ${agent.name}`);
}
// 从发现页面收藏智能体
function toggleAgentFavoriteFromDiscover(agentId) {
const agent = systemAgents.find(a => a.id === agentId);
if (!agent) return;
if (favoriteAgents.includes(agentId)) {
favoriteAgents = favoriteAgents.filter(id => id !== agentId);
showToast('已取消收藏');
} else {
favoriteAgents.push(agentId);
showToast('已收藏');
// 如果没有添加到用户列表,自动添加
if (!myAgents[agent.category]?.includes(agentId)) {
addAgentToMyAgents(agentId);
}
}
saveFavoriteAgents();
}
// ==================== 智能体收藏夹页面 ====================
function showAgentFavoritePage() {
// 获取收藏的智能体详情
const favoriteAgentsList = favoriteAgents.map(id => {
const agent = systemAgents.find(a => a.id === id);
return agent ? { ...agent, is_favorite: true } : null;
}).filter(a => a);
const favoriteHtml = `
<div class="agent-favorite-page">
<header class="favorite-header">
<button class="back-btn-white" id="favoriteBackBtn">
<svg viewBox="0 0 24 24" width="24" height="24"><path fill="currentColor" d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
</button>
<h1>收藏夹</h1>
</header>
<div class="favorite-content">
${favoriteAgentsList.length === 0
? '<div class="empty-favorites"><div class="empty-icon">⭐</div><p>暂无收藏的智能体</p><p class="empty-tip">点击右上角搜索按钮发现更多智能体</p></div>'
: `
<div class="favorite-grid">
${favoriteAgentsList.map(agent => `
<div class="favorite-agent-card" data-id="${agent.id}">
<div class="favorite-agent-avatar">${agent.avatar}</div>
<div class="favorite-agent-name">${agent.name}</div>
<div class="favorite-agent-desc">${agent.desc}</div>
<button class="favorite-agent-unfav" data-id="${agent.id}" title="取消收藏">☆</button>
</div>
`).join('')}
</div>
`}
</div>
</div>
`;
appContainer.innerHTML = favoriteHtml;
// 绑定返回按钮
const backBtn = document.getElementById('favoriteBackBtn');
if (backBtn) {
backBtn.addEventListener('click', () => {
showMainPage();
});
}
// 绑定智能体卡片点击(进入对话)
document.querySelectorAll('.favorite-agent-card').forEach(card => {
card.addEventListener('click', (e) => {
// 如果点击的是取消收藏按钮,不进入对话
if (e.target.classList.contains('favorite-agent-unfav')) {
return;
}
const agentId = card.getAttribute('data-id');
openAgent(agentId);
});
});
// 绑定取消收藏按钮
document.querySelectorAll('.favorite-agent-unfav').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const agentId = btn.getAttribute('data-id');
toggleAgentFavorite(agentId);
// 刷新收藏夹页面
showAgentFavoritePage();
});
});
}
// ==================== 我的页面 ====================
function renderProfilePage() {
const quota = getRemainingQuota();
return `
<div class="profile-page">
<header class="page-header">
<h1>我的</h1>
</header>
<div class="profile-content">
<div class="profile-card">
<div class="profile-avatar">${currentUser ? '👤' : '👤'}</div>
<div class="profile-name">${currentUser ? currentUser.username : '游客'}</div>
${currentUser
? `<div class="profile-status">已登录</div>
<button class="logout-btn" id="logoutBtn">退出登录</button>`
: `<div class="profile-status guest">未登录</div>
<div class="profile-login-buttons">
<button class="login-btn" id="loginBtn">登录</button>
<button class="register-btn" id="registerBtn">注册</button>
<button class="skip-btn" id="skipBtn">跳过,继续使用</button>
</div>`
}
</div>
${!currentUser && quota ? `
<div class="quota-card">
<div class="quota-title">今日使用配额(游客)</div>
<div class="quota-items">
<div class="quota-item">
<span class="quota-label">对话会话</span>
<span class="quota-value">${quota.chatSessionRemain}/${GUEST_LIMITS.maxChatSessionsPerDay}</span>
</div>
<div class="quota-item">
<span class="quota-label">对话消息</span>
<span class="quota-value">${quota.chatMsgRemain}/${GUEST_LIMITS.maxChatMessagesPerDay}</span>
</div>
<div class="quota-item">
<span class="quota-label">智能体消息</span>
<span class="quota-value">${quota.agentMsgRemain}/${GUEST_LIMITS.maxAgentMessagesPerDay}</span>
</div>
<div class="quota-item">
<span class="quota-label">可用智能体</span>
<span class="quota-value">仅通用助手 🤖</span>
</div>
</div>
<div class="quota-tip">登录后解锁全部功能</div>
</div>
` : ''}
<div class="profile-section">
<div class="profile-item" id="aboutBtn">
<svg viewBox="0 0 24 24" width="20" height="20"><path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/></svg>
<span>关于</span>
</div>
<div class="profile-item" id="clearDataBtn">
<svg viewBox="0 0 24 24" width="20" height="20"><path fill="currentColor" d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
<span>清除数据</span>
</div>
</div>
<div class="profile-footer">
<p>AI助手 v3.3.0</p>
<p>基于智谱 GLM-4.5-Air</p>
</div>
</div>
</div>
`;
}
function bindProfilePageEvents() {
// 登录按钮
const loginBtn = document.getElementById('loginBtn');
if (loginBtn) {
loginBtn.addEventListener('click', () => {
showLoginPage();
});
}
// 注册按钮
const registerBtn = document.getElementById('registerBtn');
if (registerBtn) {
registerBtn.addEventListener('click', () => {
showRegisterPage();
});
}
// 跳过按钮(游客模式)
const skipBtn = document.getElementById('skipBtn');
if (skipBtn) {
skipBtn.addEventListener('click', () => {
showToast('以游客模式继续使用');
switchPage('chats');
});
}
// 退出登录按钮
const logoutBtn = document.getElementById('logoutBtn');
if (logoutBtn) {
logoutBtn.addEventListener('click', () => {
if (confirm('确定要退出登录吗?')) {
currentUser = null;
saveCurrentUser();
showToast('已退出登录');
switchPage('profile');
}
});
}
// 清除数据按钮
const clearDataBtn = document.getElementById('clearDataBtn');
if (clearDataBtn) {
clearDataBtn.addEventListener('click', () => {
if (confirm('确定要清除所有数据吗?这将删除所有对话记录和账户信息。')) {
localStorage.clear();
conversations = [];
currentUser = null;
dailyUsage = { date: null, chatSessions: 0, chatMessages: 0, agentUsed: null, agentMessages: 0 };
showToast('数据已清除');
showMainPage();
}
});
}
}
// ==================== 登录页面 ====================
function showLoginPage() {
const loginHtml = `
<div class="auth-page">
<header class="auth-header">
<button class="back-btn-white" id="loginBackBtn">
<svg viewBox="0 0 24 24" width="24" height="24"><path fill="currentColor" d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
</button>
<h1>登录</h1>
</header>
<div class="auth-content">
<div class="auth-form">
<div class="auth-input-group">
<label>用户名</label>
<input type="text" id="loginUsername" placeholder="请输入用户名" autocomplete="username">
</div>
<div class="auth-input-group">
<label>密码</label>
<input type="password" id="loginPassword" placeholder="请输入密码" autocomplete="current-password">
</div>
<button class="auth-submit-btn" id="loginSubmitBtn">登录</button>
</div>
<div class="auth-footer">
<p>还没有账号?<span class="auth-link" id="goToRegister">立即注册</span></p>
</div>
</div>
</div>
`;
appContainer.innerHTML = loginHtml;
// 绑定事件
const backBtn = document.getElementById('loginBackBtn');
if (backBtn) {
backBtn.addEventListener('click', () => {
showMainPage();
});
}
const submitBtn = document.getElementById('loginSubmitBtn');
if (submitBtn) {
submitBtn.addEventListener('click', handleLogin);
}
const goToRegister = document.getElementById('goToRegister');
if (goToRegister) {
goToRegister.addEventListener('click', () => {
showRegisterPage();
});
}
// 回车登录
document.getElementById('loginPassword')?.addEventListener('keydown', (e) => {
if (e.key === 'Enter') handleLogin();
});
}
function handleLogin() {
const username = document.getElementById('loginUsername')?.value.trim();
const password = document.getElementById('loginPassword')?.value;
if (!username) {
showToast('请输入用户名');
return;
}
if (!password) {
showToast('请输入密码');
return;
}
// 检查本地存储的用户
const users = JSON.parse(localStorage.getItem('registeredUsers') || '[]');
const user = users.find(u => u.username === username && u.password === password);
if (user) {
currentUser = { username: user.username, registeredAt: user.registeredAt };
saveCurrentUser();
showToast('登录成功');
showMainPage();
} else {
showToast('用户名或密码错误');
}
}
// ==================== 注册页面 ====================
function showRegisterPage() {
const registerHtml = `
<div class="auth-page">
<header class="auth-header">
<button class="back-btn-white" id="registerBackBtn">
<svg viewBox="0 0 24 24" width="24" height="24"><path fill="currentColor" d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
</button>
<h1>注册</h1>
</header>
<div class="auth-content">
<div class="auth-form">
<div class="auth-input-group">
<label>用户名</label>
<input type="text" id="registerUsername" placeholder="请输入用户名3-20字符" autocomplete="username">
</div>
<div class="auth-input-group">
<label>密码</label>
<input type="password" id="registerPassword" placeholder="请输入密码6-20字符" autocomplete="new-password">
</div>
<div class="auth-input-group">
<label>确认密码</label>
<input type="password" id="registerPasswordConfirm" placeholder="请再次输入密码" autocomplete="new-password">
</div>
<button class="auth-submit-btn" id="registerSubmitBtn">注册</button>
</div>
<div class="auth-footer">
<p>已有账号?<span class="auth-link" id="goToLogin">立即登录</span></p>
</div>
</div>
</div>
`;
appContainer.innerHTML = registerHtml;
// 绑定事件
const backBtn = document.getElementById('registerBackBtn');
if (backBtn) {
backBtn.addEventListener('click', () => {
showMainPage();
});
}
const submitBtn = document.getElementById('registerSubmitBtn');
if (submitBtn) {
submitBtn.addEventListener('click', handleRegister);
}
const goToLogin = document.getElementById('goToLogin');
if (goToLogin) {
goToLogin.addEventListener('click', () => {
showLoginPage();
});
}
// 回车注册
document.getElementById('registerPasswordConfirm')?.addEventListener('keydown', (e) => {
if (e.key === 'Enter') handleRegister();
});
}
function handleRegister() {
const username = document.getElementById('registerUsername')?.value.trim();
const password = document.getElementById('registerPassword')?.value;
const passwordConfirm = document.getElementById('registerPasswordConfirm')?.value;
if (!username || username.length < 3 || username.length > 20) {
showToast('用户名需要3-20个字符');
return;
}
if (!password || password.length < 6 || password.length > 20) {
showToast('密码需要6-20个字符');
return;
}
if (password !== passwordConfirm) {
showToast('两次密码不一致');
return;
}
// 检查用户名是否已存在
const users = JSON.parse(localStorage.getItem('registeredUsers') || '[]');
if (users.find(u => u.username === username)) {
showToast('用户名已存在');
return;
}
// 注册新用户
const newUser = {
username,
password,
registeredAt: Date.now()
};
users.push(newUser);
localStorage.setItem('registeredUsers', JSON.stringify(users));
// 自动登录
currentUser = { username: newUser.username, registeredAt: newUser.registeredAt };
saveCurrentUser();
showToast('注册成功');
showMainPage();
}
// ==================== 限制提示 ====================
function showLimitDialog(type) {
let title = '';
let message = '';
let showLoginBtn = true;
switch (type) {
case 'chat_session':
title = '对话会话已达上限';
message = `游客每天只能创建 ${GUEST_LIMITS.maxChatSessionsPerDay} 个对话会话。\n登录后即可无限使用。`;
break;
case 'chat_message':
title = '对话消息已达上限';
message = `游客每天最多发送 ${GUEST_LIMITS.maxChatMessagesPerDay} 条对话消息。\n登录后即可无限使用。`;
break;
case 'agent_type':
title = '智能体限制';
message = `游客只能使用「通用助手」智能体。\n登录后即可使用全部智能体。`;
break;
case 'agent_change':
title = '智能体切换限制';
message = `游客每天只能使用一个智能体。\n您今天已使用过智能体,明天才能更换。\n登录后即可自由切换智能体。`;
break;
case 'agent_message':
title = '智能体消息已达上限';
message = `游客每天最多发送 ${GUEST_LIMITS.maxAgentMessagesPerDay} 条智能体消息。\n登录后即可无限使用。`;
break;
default:
title = '使用限制';
message = '登录后解锁全部功能。';
}
const dialogHtml = `
<div class="limit-dialog" id="limitDialog">
<div class="limit-dialog-content">
<div class="limit-dialog-icon">⚠️</div>
<div class="limit-dialog-title">${title}</div>
<div class="limit-dialog-message">${message}</div>
<div class="limit-dialog-actions">
${showLoginBtn ? '<button class="limit-dialog-btn login" id="limitLoginBtn">登录</button>' : ''}
<button class="limit-dialog-btn cancel" id="limitCancelBtn">知道了</button>
</div>
</div>
</div>
`;
// 添加到页面
const existingDialog = document.getElementById('limitDialog');
if (existingDialog) existingDialog.remove();
document.body.insertAdjacentHTML('beforeend', dialogHtml);
// 绑定事件
const loginBtn = document.getElementById('limitLoginBtn');
if (loginBtn) {
loginBtn.addEventListener('click', () => {
document.getElementById('limitDialog')?.remove();
showLoginPage();
});
}
const cancelBtn = document.getElementById('limitCancelBtn');
if (cancelBtn) {
cancelBtn.addEventListener('click', () => {
document.getElementById('limitDialog')?.remove();
});
}
// 点击背景关闭
document.getElementById('limitDialog')?.addEventListener('click', (e) => {
if (e.target.id === 'limitDialog') {
document.getElementById('limitDialog')?.remove();
}
});
}
// 查看全部历史使用
function showAgentHistoryPage() {
const allAgentConvos = getAgentConversationHistory(100); // 获取所有
const historyHtml = `
<div class="agent-history-page">
<header class="page-header">
<button class="back-btn-white" id="historyBackBtn">
<svg viewBox="0 0 24 24" width="24" height="24"><path fill="currentColor" d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
</button>
<h1>历史使用</h1>
</header>
<div class="agent-history-content">
${allAgentConvos.length === 0
? '<div class="empty-list">暂无历史使用记录</div>'
: allAgentConvos.map(conv => `
<div class="agent-history-item" data-conv-id="${conv.id}">
<div class="agent-history-left">
<span class="agent-history-avatar">${conv.agent ? conv.agent.avatar : '🤖'}</span>
<span class="agent-history-name">${conv.title}</span>
</div>
<div class="agent-history-right">
<span class="agent-history-agent">${conv.agent ? conv.agent.name : '未知智能体'}</span>
<span class="agent-history-time">${formatTime(conv.updatedAt)}</span>
</div>
</div>
`).join('')
}
</div>
</div>
`;
appContainer.innerHTML = historyHtml;
// 绑定返回按钮
const backBtn = document.getElementById('historyBackBtn');
if (backBtn) {
backBtn.addEventListener('click', () => {
showMainPage();
});
}
// 绑定对话点击(打开已有对话)
document.querySelectorAll('.agent-history-item').forEach(item => {
item.addEventListener('click', () => {
const convId = item.getAttribute('data-conv-id');
if (convId) {
openConversation(convId);
}
});
});
}
// 打开智能体对话
function openAgent(agentId) {
currentAgent = systemAgents.find(a => a.id === agentId);
if (!currentAgent) return;
// 检查未登录用户的智能体限制
if (!currentUser) {
const check = canUseAgent(agentId);
if (!check.allowed) {
if (check.reason === 'guest_agent_only') {
showLimitDialog('agent_type');
return;
} else if (check.reason === 'guest_one_agent_only') {
showLimitDialog('agent_change');
return;
}
}
// 记录使用的智能体
setAgentUsed(agentId);
}
// 创建新对话并设置智能体
const newConv = {
id: Date.now().toString(),
title: currentAgent.name,
messages: [],
agentId: agentId,
createdAt: Date.now(),
updatedAt: Date.now()
};
conversations.unshift(newConv);
saveConversations();
currentConversation = newConv;
// 显示对话界面
showAgentChatPage();
}
// 显示智能体对话界面
function showAgentChatPage() {
if (!currentAgent || !currentConversation) return;
const chatHtml = `
<div class="chat-page">
<header class="chat-header">
<button class="back-btn" id="backBtn">
<svg viewBox="0 0 24 24" width="24" height="24"><path fill="currentColor" d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
</button>
<div class="header-info">
<div class="agent-avatar-header">${currentAgent.avatar}</div>
<div class="header-text">
<h1>${currentAgent.name}</h1>
<p class="agent-desc-header">${currentAgent.desc}</p>
</div>
</div>
</header>
<div class="messages-container" id="messagesContainer">
<div class="welcome" id="welcome">
<div class="welcome-icon">${currentAgent.avatar}</div>
<h2>${currentAgent.name}</h2>
<p>${currentAgent.desc}</p>
<p class="welcome-tip">有什么可以帮你的吗?</p>
</div>
<div class="messages" id="messages"></div>
</div>
<!-- 功能开关栏 -->
<div class="feature-bar" id="featureBar">
<div class="feature-left">
<button class="feature-btn thinking-btn" id="thinkingBtn">
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>
<span>深度思考</span>
</button>
<button class="feature-btn search-btn" id="searchBtn">
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 7 9.5 7 14 9.01 14 9.5 11.99 14 9.5 14z"/></svg>
<span>联网搜索</span>
</button>
</div>
<div class="feature-right">
<button class="feature-btn nav-btn" id="scrollTopBtn">
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z"/></svg>
</button>
<button class="feature-btn nav-btn" id="scrollBottomBtn">
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6z"/></svg>
</button>
</div>
</div>
<div class="input-area">
<button class="attach-btn" id="attachBtn" title="上传文件">
<svg viewBox="0 0 24 24" width="24" height="24"><path fill="currentColor" d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
</button>
<textarea id="userInput" placeholder="输入消息..." rows="1"></textarea>
<button class="send-btn" id="sendBtn">
<svg viewBox="0 0 24 24" width="24" height="24"><path fill="currentColor" d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
</button>
</div>
<!-- 上传选项弹窗 -->
<div class="attach-panel" id="attachPanel">
<div class="attach-panel-content">
<div class="attach-item" data-type="image">
<div class="attach-icon">📷</div>
<div class="attach-label">上传图片</div>
</div>
<div class="attach-item" data-type="file">
<div class="attach-icon">📄</div>
<div class="attach-label">上传文件</div>
</div>
</div>
</div>
<input type="file" id="imageInput" accept="image/*" style="display:none">
<input type="file" id="fileInput" accept=".txt,.md,.pdf,.doc,.docx,.json,.csv" style="display:none">
<!-- 搜索栏 -->
<div class="search-bar" id="searchBar">
<div class="search-input-wrapper">
<svg viewBox="0 0 24 24" width="20" height="20"><path fill="currentColor" d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 7 9.5 7 14 9.01 14 9.5 11.99 14 9.5 14z"/></svg>
<input type="text" id="searchInput" placeholder="搜索对话标题或内容...">
<button class="search-close-btn" id="searchCloseBtn">
<svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/></svg>
</button>
</div>
<div class="search-results" id="searchResults"></div>
</div>
</div>
`;
appContainer.innerHTML = chatHtml;
// 重新获取 DOM 元素
messagesContainer = document.getElementById('messagesContainer');
messagesDiv = document.getElementById('messages');
userInput = document.getElementById('userInput');
sendBtn = document.getElementById('sendBtn');
welcome = document.getElementById('welcome');
thinkingBtn = document.getElementById('thinkingBtn');
searchBtn = document.getElementById('searchBtn');
// 重置自动滚动状态
autoScrollEnabled = true;
// 设置滚动监听
setupScrollListener();
// 绑定事件
const backBtn = document.getElementById('backBtn');
if (backBtn) {
backBtn.addEventListener('click', () => {
showMainPage();
});
}
// 绑定功能开关按钮事件
if (thinkingBtn) {
thinkingBtn.addEventListener('click', () => {
enableThinking = !enableThinking;
thinkingBtn.classList.toggle('active', enableThinking);
});
}
if (searchBtn) {
searchBtn.addEventListener('click', () => {
enableSearch = !enableSearch;
searchBtn.classList.toggle('active', enableSearch);
});
}
// 绑定输入事件
userInput.addEventListener('keydown', handleKeyDown);
userInput.addEventListener('input', (e) => autoResize(e.target));
sendBtn.addEventListener('click', sendMessage);
// 绑定置顶置底按钮事件
const scrollTopBtn = document.getElementById('scrollTopBtn');
const scrollBottomBtn = document.getElementById('scrollBottomBtn');
if (scrollTopBtn) {
scrollTopBtn.addEventListener('click', () => {
if (messagesContainer) {
messagesContainer.scrollTo({
top: 0,
behavior: 'smooth'
});
}
});
}
if (scrollBottomBtn) {
scrollBottomBtn.addEventListener('click', () => {
if (messagesContainer) {
messagesContainer.scrollTo({
top: messagesContainer.scrollHeight,
behavior: 'smooth'
});
}
});
}
// 绑定上传按钮事件
const attachBtn = document.getElementById('attachBtn');
const attachPanel = document.getElementById('attachPanel');
const imageInput = document.getElementById('imageInput');
const fileInput = document.getElementById('fileInput');
if (attachBtn) {
attachBtn.addEventListener('click', (e) => {
e.stopPropagation();
attachPanel.classList.toggle('show');
});
}
document.addEventListener('click', (e) => {
if (attachPanel && attachPanel.classList.contains('show') &&
!attachPanel.contains(e.target) && !attachBtn.contains(e.target)) {
attachPanel.classList.remove('show');
}
});
attachPanel.querySelectorAll('.attach-item').forEach(item => {
item.addEventListener('click', () => {
const type = item.getAttribute('data-type');
attachPanel.classList.remove('show');
if (type === 'image') {
imageInput.click();
} else if (type === 'file') {
fileInput.click();
}
});
});
imageInput.addEventListener('change', handleImageUpload);
fileInput.addEventListener('change', handleFileUpload);
// 渲染消息
renderMessages();
userInput.focus();
}
// 设置长按事件
function setupLongPressEvents(container) {
let longPressTimer = null;
let currentActionConvId = null;
container.addEventListener('touchstart', (e) => {
const item = e.target.closest('.conversation-item');
if (item) {
longPressTimer = setTimeout(() => {
currentActionConvId = item.getAttribute('data-id');
showActionMenu(currentActionConvId);
}, 500);
}
});
container.addEventListener('touchend', () => {
if (longPressTimer) {
clearTimeout(longPressTimer);
longPressTimer = null;
}
});
container.addEventListener('touchmove', () => {
if (longPressTimer) {
clearTimeout(longPressTimer);
longPressTimer = null;
}
});
container.addEventListener('mousedown', (e) => {
const item = e.target.closest('.conversation-item');
if (item) {
longPressTimer = setTimeout(() => {
currentActionConvId = item.getAttribute('data-id');
showActionMenu(currentActionConvId);
}, 500);
}
});
container.addEventListener('mouseup', () => {
if (longPressTimer) {
clearTimeout(longPressTimer);
longPressTimer = null;
}
});
container.addEventListener('mouseleave', () => {
if (longPressTimer) {
clearTimeout(longPressTimer);
longPressTimer = null;
}
});
}
function showConversationList() {
currentConversation = null;
// 渲染对话列表
const listHtml = `
<div class="conversation-list-page">
<header class="list-header">
<div class="header-title">
<span class="logo">🤖</span>
<h1>AI助手</h1>
</div>
<div class="header-actions">
<button class="header-btn search-toggle-btn" id="searchToggleBtn" title="搜索">
<svg viewBox="0 0 24 24" width="20" height="20"><path fill="currentColor" d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 7 9.5 7 14 9.01 14 9.5 11.99 14 9.5 14z"/></svg>
</button>
<button class="header-btn new-chat-btn-header" id="newChatBtn" title="新建对话">
<svg viewBox="0 0 24 24" width="20" height="20"><path fill="currentColor" d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
</button>
</div>
</header>
<div class="list-content">
<div class="conversation-list" id="conversationList">
${conversations.length === 0
? '<div class="empty-list">暂无对话记录</div>'
: sortConversations().map(conv => `
<div class="conversation-item ${conv.is_pinned ? 'pinned' : ''}" data-id="${conv.id}">
${conv.is_pinned ? '<span class="pin-icon">📌</span>' : ''}
<div class="conv-title">${escapeHtml(conv.title)}</div>
<div class="conv-meta">${conv.messages.length} 条消息 · ${formatTime(conv.updatedAt)}</div>
</div>
`).join('')
}
</div>
</div>
<!-- 操作菜单 -->
<div class="action-menu" id="actionMenu">
<div class="action-menu-content">
<div class="action-menu-item" data-action="rename">
<svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg>
<span>重命名</span>
</div>
<div class="action-menu-item" data-action="share">
<svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.35 2.92 3 2.92s3-1.31 3-2.92c0-1.61-1.35-2.92-3-2.92z"/></svg>
<span>分享</span>
</div>
<div class="action-menu-item" data-action="pin">
<svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M16 12V4h1V2H7v2h1v8l-2 2v2h5.2v6h1.6v-6H18v-2l-2-2z"/></svg>
<span id="pinText">置顶</span>
</div>
<div class="action-menu-item delete-action" data-action="delete">
<svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
<span>删除</span>
</div>
</div>
</div>
<!-- 搜索栏 -->
<div class="search-bar" id="searchBar">
<div class="search-input-wrapper">
<svg viewBox="0 0 24 24" width="20" height="20"><path fill="currentColor" d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 7 9.5 7 14 9.01 14 9.5 11.99 14 9.5 14z"/></svg>
<input type="text" id="searchInput" placeholder="搜索对话标题或内容...">
<button class="search-close-btn" id="searchCloseBtn">
<svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/></svg>
</button>
</div>
<div class="search-results" id="searchResults"></div>
</div>
</div>
</div>
`;
appContainer.innerHTML = listHtml;
// 绑定事件
const newChatBtn = document.getElementById('newChatBtn');
if (newChatBtn) {
newChatBtn.addEventListener('click', createNewConversation);
}
// 搜索功能
const searchToggleBtn = document.getElementById('searchToggleBtn');
const searchBar = document.getElementById('searchBar');
const searchInput = document.getElementById('searchInput');
const searchCloseBtn = document.getElementById('searchCloseBtn');
const searchResults = document.getElementById('searchResults');
if (searchToggleBtn) {
searchToggleBtn.addEventListener('click', () => {
if (searchBar) {
searchBar.classList.add('show');
if (searchInput) {
searchInput.focus();
}
}
});
}
if (searchCloseBtn) {
searchCloseBtn.addEventListener('click', () => {
hideSearchBar();
});
}
if (searchInput) {
searchInput.addEventListener('input', (e) => {
const keyword = e.target.value.trim();
if (keyword) {
searchConversations(keyword);
} else {
if (searchResults) searchResults.innerHTML = '';
}
});
searchInput.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
hideSearchBar();
}
});
}
// 点击搜索结果
if (searchResults) {
searchResults.addEventListener('click', (e) => {
const item = e.target.closest('.search-result-item');
if (item) {
const id = item.getAttribute('data-id');
hideSearchBar();
openConversation(id);
}
});
}
function hideSearchBar() {
if (searchBar) {
searchBar.classList.remove('show');
}
if (searchInput) {
searchInput.value = '';
}
if (searchResults) {
searchResults.innerHTML = '';
}
}
const conversationList = document.getElementById('conversationList');
const actionMenu = document.getElementById('actionMenu');
let longPressTimer = null;
let currentActionConvId = null;
if (conversationList) {
// 点击事件
conversationList.addEventListener('click', (e) => {
const item = e.target.closest('.conversation-item');
if (item) {
const id = item.getAttribute('data-id');
openConversation(id);
}
});
// 长按事件
conversationList.addEventListener('touchstart', (e) => {
const item = e.target.closest('.conversation-item');
if (item) {
longPressTimer = setTimeout(() => {
currentActionConvId = item.getAttribute('data-id');
showActionMenu(currentActionConvId);
}, 500); // 500ms长按
}
});
conversationList.addEventListener('touchend', () => {
if (longPressTimer) {
clearTimeout(longPressTimer);
longPressTimer = null;
}
});
conversationList.addEventListener('touchmove', () => {
if (longPressTimer) {
clearTimeout(longPressTimer);
longPressTimer = null;
}
});
// 鼠标长按PC端
conversationList.addEventListener('mousedown', (e) => {
const item = e.target.closest('.conversation-item');
if (item) {
longPressTimer = setTimeout(() => {
currentActionConvId = item.getAttribute('data-id');
showActionMenu(currentActionConvId);
}, 500);
}
});
conversationList.addEventListener('mouseup', () => {
if (longPressTimer) {
clearTimeout(longPressTimer);
longPressTimer = null;
}
});
conversationList.addEventListener('mouseleave', () => {
if (longPressTimer) {
clearTimeout(longPressTimer);
longPressTimer = null;
}
});
}
// 操作菜单事件
if (actionMenu) {
actionMenu.addEventListener('click', (e) => {
const item = e.target.closest('.action-menu-item');
if (item && currentActionConvId) {
const action = item.getAttribute('data-action');
handleActionMenuAction(action, currentActionConvId);
hideActionMenu();
}
});
// 点击其他地方关闭菜单
document.addEventListener('click', (e) => {
if (actionMenu.classList.contains('show') && !actionMenu.contains(e.target)) {
hideActionMenu();
}
});
}
}
// 排序对话(置顶在前)
function sortConversations() {
return [...conversations].sort((a, b) => {
// 置顶优先
if (a.is_pinned && !b.is_pinned) return -1;
if (!a.is_pinned && b.is_pinned) return 1;
// 然后按更新时间
return b.updatedAt - a.updatedAt;
});
}
// 排序普通对话(没有智能体的)
function sortNormalConversations(convList) {
return [...convList].sort((a, b) => {
// 置顶优先
if (a.is_pinned && !b.is_pinned) return -1;
if (!a.is_pinned && b.is_pinned) return 1;
// 然后按更新时间
return b.updatedAt - a.updatedAt;
});
}
// 搜索普通对话(不包含智能体对话)
function searchConversations(keyword) {
const searchResults = document.getElementById('searchResults');
if (!searchResults) return;
keyword = keyword.toLowerCase();
// 只搜索没有智能体的普通对话
const normalConversations = conversations.filter(conv => !conv.agentId);
// 搜索标题和消息内容
const results = normalConversations.filter(conv => {
// 搜索标题
if (conv.title.toLowerCase().includes(keyword)) return true;
// 搜索消息内容
if (conv.messages.some(m => m.content.toLowerCase().includes(keyword))) return true;
return false;
});
if (results.length === 0) {
searchResults.innerHTML = '<div class="search-empty">未找到相关对话</div>';
return;
}
searchResults.innerHTML = results.map(conv => {
// 找到匹配的消息片段
let matchSnippet = '';
const matchedMsg = conv.messages.find(m => m.content.toLowerCase().includes(keyword));
if (matchedMsg) {
const content = matchedMsg.content;
const idx = content.toLowerCase().indexOf(keyword);
const start = Math.max(0, idx - 30);
const end = Math.min(content.length, idx + keyword.length + 30);
matchSnippet = (start > 0 ? '...' : '') + content.slice(start, end) + (end < content.length ? '...' : '');
}
return `
<div class="search-result-item ${conv.is_pinned ? 'pinned' : ''}" data-id="${conv.id}">
${conv.is_pinned ? '<span class="pin-icon">📌</span>' : ''}
<div class="search-result-title">${escapeHtml(conv.title)}</div>
${matchSnippet ? `<div class="search-result-snippet">${escapeHtml(matchSnippet)}</div>` : ''}
<div class="search-result-meta">${conv.messages.length} 条消息 · ${formatTime(conv.updatedAt)}</div>
</div>
`;
}).join('');
}
// 显示操作菜单
function showActionMenu(convId) {
const actionMenu = document.getElementById('actionMenu');
const conv = conversations.find(c => c.id === convId);
if (!actionMenu || !conv) return;
// 更新置顶按钮文字
const pinText = document.getElementById('pinText');
if (pinText) {
pinText.textContent = conv.is_pinned ? '取消置顶' : '置顶';
}
// 显示菜单
actionMenu.classList.add('show');
}
// 隐藏操作菜单
function hideActionMenu() {
const actionMenu = document.getElementById('actionMenu');
if (actionMenu) {
actionMenu.classList.remove('show');
}
}
// 处理操作菜单动作
function handleActionMenuAction(action, convId) {
const conv = conversations.find(c => c.id === convId);
if (!conv) return;
switch (action) {
case 'rename':
renameConversation(convId);
break;
case 'share':
shareConversation(convId);
break;
case 'pin':
togglePinConversation(convId);
break;
case 'delete':
deleteConversation(convId);
break;
}
}
// 重命名对话
function renameConversation(convId) {
const conv = conversations.find(c => c.id === convId);
if (!conv) return;
const newTitle = prompt('请输入新的对话标题:', conv.title);
if (newTitle && newTitle.trim() && newTitle !== conv.title) {
conv.title = newTitle.trim();
conv.updatedAt = Date.now();
saveConversations();
showConversationList();
showToast('已重命名');
}
}
// 分享对话
function shareConversation(convId) {
const conv = conversations.find(c => c.id === convId);
if (!conv) return;
// 构建分享内容
const shareContent = `${conv.title}\n\n${conv.messages.map(m =>
`${m.role === 'user' ? '👤 用户' : '🤖 AI'}: ${m.content}`
).join('\n\n')}`;
// 复制到剪贴板
try {
const textarea = document.createElement('textarea');
textarea.value = shareContent;
textarea.style.position = 'fixed';
textarea.style.top = '0';
textarea.style.left = '0';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
showToast('对话已复制到剪贴板');
} catch (err) {
showToast('分享失败');
}
}
// 置顶/取消置顶对话
function togglePinConversation(convId) {
const conv = conversations.find(c => c.id === convId);
if (!conv) return;
conv.is_pinned = !conv.is_pinned;
conv.updatedAt = Date.now();
saveConversations();
showConversationList();
showToast(conv.is_pinned ? '已置顶' : '已取消置顶');
}
// 创建新对话
function createNewConversation() {
// 检查未登录用户的对话限制
if (!currentUser && !canCreateNewChat()) {
showLimitDialog('chat_session');
return;
}
const newConv = {
id: Date.now().toString(),
title: '新对话',
messages: [],
createdAt: Date.now(),
updatedAt: Date.now()
};
conversations.unshift(newConv);
saveConversations();
// 增加对话会话计数(未登录用户)
incrementChatSession();
openConversation(newConv.id);
}
// 打开对话
function openConversation(id) {
currentConversation = conversations.find(c => c.id === id);
if (!currentConversation) {
showConversationList();
return;
}
// 渲染对话页面
const chatHtml = `
<div id="chatPage">
<header class="header">
<button class="back-btn" id="backBtn">
<svg viewBox="0 0 24 24" width="24" height="24"><path fill="currentColor" d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
</button>
<div class="header-title">
<span class="logo">🤖</span>
<h1>${escapeHtml(currentConversation.title)}</h1>
</div>
<button class="clear-btn" id="clearBtn" title="清空对话">
<svg viewBox="0 0 24 24" width="20" height="20"><path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/></svg>
</button>
</header>
<div class="messages-container" id="messagesContainer">
<div class="welcome" id="welcome" style="${currentConversation.messages.length > 0 ? 'display:none' : ''}">
<div class="welcome-icon">👋</div>
<h2>你好我是AI助手</h2>
<p>有什么可以帮助你的吗?</p>
<div class="quick-actions">
<button class="quick-btn" data-text="介绍一下你自己">介绍一下你自己</button>
<button class="quick-btn" data-text="帮我写一段代码">帮我写代码</button>
<button class="quick-btn" data-text="解释一个概念">解释概念</button>
</div>
</div>
<div class="messages" id="messages"></div>
</div>
<!-- 功能开关栏 -->
<div class="feature-bar">
<div class="feature-left">
<button class="feature-btn thinking-btn ${enableThinking ? 'active' : ''}" id="thinkingBtn">
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>
<span>深度思考</span>
</button>
<button class="feature-btn search-btn ${enableSearch ? 'active' : ''}" id="searchBtn">
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 7 9.5 7 14 9.01 14 9.5 11.99 14 9.5 14z"/></svg>
<span>联网搜索</span>
</button>
</div>
<div class="feature-right">
<button class="feature-btn nav-btn" id="scrollTopBtn" title="回到顶部">
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z"/></svg>
</button>
<button class="feature-btn nav-btn" id="scrollBottomBtn" title="回到底部">
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6z"/></svg>
</button>
</div>
</div>
<div class="input-area">
<button class="attach-btn" id="attachBtn" title="上传文件">
<svg viewBox="0 0 24 24" width="24" height="24"><path fill="currentColor" d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
</button>
<textarea
id="userInput"
placeholder="输入消息..."
rows="1"
></textarea>
<button class="send-btn" id="sendBtn">
<svg viewBox="0 0 24 24" width="24" height="24"><path fill="currentColor" d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
</button>
</div>
<!-- 上传选项弹窗 -->
<div class="attach-panel" id="attachPanel">
<div class="attach-panel-content">
<div class="attach-item" data-type="image">
<div class="attach-icon">📷</div>
<div class="attach-label">上传图片</div>
</div>
<div class="attach-item" data-type="file">
<div class="attach-icon">📄</div>
<div class="attach-label">上传文件</div>
</div>
</div>
</div>
<input type="file" id="imageInput" accept="image/*" style="display:none">
<input type="file" id="fileInput" accept=".txt,.md,.pdf,.doc,.docx,.json,.csv" style="display:none">
</div>
`;
appContainer.innerHTML = chatHtml;
// 重新获取 DOM 元素
messagesContainer = document.getElementById('messagesContainer');
messagesDiv = document.getElementById('messages');
userInput = document.getElementById('userInput');
sendBtn = document.getElementById('sendBtn');
welcome = document.getElementById('welcome');
thinkingBtn = document.getElementById('thinkingBtn');
searchBtn = document.getElementById('searchBtn');
// 重置自动滚动状态
autoScrollEnabled = true;
// 设置滚动监听
setupScrollListener();
// 绑定按钮事件
const backBtn = document.getElementById('backBtn');
if (backBtn) backBtn.addEventListener('click', showConversationList);
const clearBtn = document.getElementById('clearBtn');
if (clearBtn) clearBtn.addEventListener('click', clearCurrentChat);
// 绑定功能开关按钮事件
if (thinkingBtn) {
thinkingBtn.addEventListener('click', () => {
enableThinking = !enableThinking;
thinkingBtn.classList.toggle('active', enableThinking);
});
}
if (searchBtn) {
searchBtn.addEventListener('click', () => {
enableSearch = !enableSearch;
searchBtn.classList.toggle('active', enableSearch);
});
}
// 绑定置顶置底按钮事件
const scrollTopBtn = document.getElementById('scrollTopBtn');
const scrollBottomBtn = document.getElementById('scrollBottomBtn');
if (scrollTopBtn) {
scrollTopBtn.addEventListener('click', () => {
if (messagesContainer) {
messagesContainer.scrollTo({
top: 0,
behavior: 'smooth'
});
}
});
}
if (scrollBottomBtn) {
scrollBottomBtn.addEventListener('click', () => {
if (messagesContainer) {
messagesContainer.scrollTo({
top: messagesContainer.scrollHeight,
behavior: 'smooth'
});
}
});
}
// 绑定输入事件
userInput.addEventListener('keydown', handleKeyDown);
userInput.addEventListener('input', (e) => autoResize(e.target));
sendBtn.addEventListener('click', sendMessage);
// 绑定上传按钮事件
const attachBtn = document.getElementById('attachBtn');
const attachPanel = document.getElementById('attachPanel');
const imageInput = document.getElementById('imageInput');
const fileInput = document.getElementById('fileInput');
if (attachBtn) {
attachBtn.addEventListener('click', (e) => {
e.stopPropagation(); // 阻止冒泡到 document
attachPanel.classList.toggle('show');
});
}
// 点击其他地方关闭面板
document.addEventListener('click', (e) => {
if (attachPanel && attachPanel.classList.contains('show') &&
!attachPanel.contains(e.target) && !attachBtn.contains(e.target)) {
attachPanel.classList.remove('show');
}
});
// 上传选项点击
attachPanel.querySelectorAll('.attach-item').forEach(item => {
item.addEventListener('click', () => {
const type = item.getAttribute('data-type');
attachPanel.classList.remove('show');
if (type === 'image') {
imageInput.click();
} else if (type === 'file') {
fileInput.click();
}
});
});
// 图片上传处理
imageInput.addEventListener('change', handleImageUpload);
// 文件上传处理
fileInput.addEventListener('change', handleFileUpload);
// 绑定快捷按钮事件
document.querySelectorAll('.quick-btn').forEach(btn => {
btn.addEventListener('click', () => {
const text = btn.getAttribute('data-text');
userInput.value = text;
sendMessage();
});
});
// 渲染消息
renderMessages();
userInput.focus();
}
// 删除对话
function deleteConversation(id) {
if (!confirm('确定要删除这个对话吗?')) return;
conversations = conversations.filter(c => c.id !== id);
saveConversations();
showConversationList();
}
// ==================== 对话页面 ====================
// 自动调整输入框高度
function autoResize(textarea) {
textarea.style.height = 'auto';
textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
}
// 处理键盘事件
function handleKeyDown(event) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
sendMessage();
}
}
// 发送快捷消息
function sendQuickMessage(text) {
userInput.value = text;
sendMessage();
}
// 发送消息(流式输出)
async function sendMessage() {
if (!currentConversation) return;
const text = userInput.value.trim();
if (!text || isLoading) return;
// 检查未登录用户的消息限制
if (!currentUser) {
if (currentConversation.agentId) {
// 智能体对话
if (!canSendAgentMessage()) {
showLimitDialog('agent_message');
return;
}
} else {
// 普通对话
if (!canSendChatMessage()) {
showLimitDialog('chat_message');
return;
}
}
}
// 隐藏欢迎界面
welcome.style.display = 'none';
// 添加用户消息
currentConversation.messages.push({ role: 'user', content: text });
// 更新对话标题(第一条用户消息)
if (currentConversation.title === '新对话') {
currentConversation.title = text.slice(0, 30) + (text.length > 30 ? '...' : '');
// 更新标题显示
const titleEl = document.querySelector('.header h1');
if (titleEl) {
titleEl.textContent = currentConversation.title;
}
}
currentConversation.updatedAt = Date.now();
saveConversations();
// 增加消息计数(未登录用户)
if (!currentUser) {
if (currentConversation.agentId) {
incrementAgentMessage();
} else {
incrementChatMessage();
}
}
// 发送新消息时启用自动滚动
autoScrollEnabled = true;
renderMessages();
userInput.value = '';
autoResize(userInput);
// 调用流式生成
await streamGenerate(currentConversation.messages.length - 1);
}
// 流式生成 AI 回复
async function streamGenerate(userMsgIndex) {
isLoading = true;
sendBtn.disabled = true;
const aiMessageIndex = currentConversation.messages.length;
const userMessage = currentConversation.messages[userMsgIndex];
// 如果开启联网搜索,先执行搜索
let searchResults = null;
if (enableSearch && userMessage.role === 'user') {
searchResults = await performSearch(userMessage.content);
}
// 只有开启深度思考时才添加 thinking 字段,开启搜索时添加 search_results 字段
currentConversation.messages.push({
role: 'assistant',
content: '',
...(enableThinking ? { thinking: '' } : {}),
...(searchResults ? { search_results: searchResults } : {})
});
renderMessages();
const lastMessageEl = messagesDiv.lastElementChild;
const contentEl = lastMessageEl.querySelector('.message-content');
const thinkingEl = lastMessageEl.querySelector('.thinking-content');
// 深度思考模式:思考块默认展开
if (enableThinking && thinkingEl) {
const thinkingBlock = lastMessageEl.querySelector('.thinking-block');
if (thinkingBlock) thinkingBlock.classList.add('expanded');
thinkingEl.innerHTML = '<span class="streaming-cursor">思考中...</span>';
}
contentEl.innerHTML = '<span class="streaming-cursor">▌</span>';
// 显示停止生成按钮
showStopGenerateBtn();
try {
// 构建消息数组
let messagesToSend = currentConversation.messages.slice(0, aiMessageIndex).map(m => ({
role: m.role,
content: m.content
}));
// 如果有搜索结果,将搜索内容添加到消息中
if (searchResults) {
const searchContext = formatSearchResultsForLLM(searchResults);
messagesToSend.push({
role: 'system',
content: `以下是搜索结果,请根据这些信息回答用户问题:\n\n${searchContext}`
});
}
// 构建请求体 - 统一使用 glm-4.5-air通过 thinking 参数控制
const requestBody = {
model: CONFIG.model,
messages: messagesToSend,
max_tokens: CONFIG.maxTokens,
stream: true,
thinking: {
type: enableThinking ? 'enabled' : 'disabled'
}
};
const response = await fetch(CONFIG.apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${CONFIG.apiKey}`
},
body: JSON.stringify(requestBody)
});
if (!response.ok) {
throw new Error(`API 错误: ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let thinkingOutputStarted = false; // 正式内容是否开始输出
let abortController = new AbortController(); // 用于中断流
// 绑定停止按钮事件
const stopBtn = document.getElementById('stopGenerateBtn');
if (stopBtn) {
stopBtn.onclick = () => {
abortController.abort();
isLoading = false;
sendBtn.disabled = false;
hideStopGenerateBtn();
// 更新最终内容
if (thinkingEl && enableThinking && currentConversation.messages[aiMessageIndex].thinking) {
thinkingEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].thinking);
}
contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content);
currentConversation.updatedAt = Date.now();
saveConversations();
renderMessages();
};
}
while (true) {
if (abortController.signal.aborted) break; // 检查是否已停止
const { done, value } = 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) {
if (line.startsWith('data: ')) {
const jsonStr = line.slice(6).trim();
if (jsonStr === '[DONE]') continue;
try {
const data = JSON.parse(jsonStr);
const delta = data.choices?.[0]?.delta;
if (delta) {
// 只有开启深度思考时才处理思考内容
if (enableThinking && (delta.reasoning_content || delta.thinking)) {
const thinkingChunk = delta.reasoning_content || delta.thinking;
currentConversation.messages[aiMessageIndex].thinking += thinkingChunk;
if (thinkingEl) {
thinkingEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].thinking) + '<span class="streaming-cursor">▌</span>';
}
// 确保思考块展开
const thinkingBlock = lastMessageEl.querySelector('.thinking-block');
if (thinkingBlock && !thinkingBlock.classList.contains('expanded')) {
thinkingBlock.classList.add('expanded');
}
scrollToBottom();
}
// 处理正式回复内容
if (delta.content) {
// 如果开启深度思考且开始输出正式内容,说明思考完成,立即折叠思考块
if (enableThinking && !thinkingOutputStarted && currentConversation.messages[aiMessageIndex].thinking) {
thinkingOutputStarted = true;
// 折叠思考内容
const thinkingBlock = lastMessageEl.querySelector('.thinking-block');
if (thinkingBlock) thinkingBlock.classList.remove('expanded');
if (thinkingEl) thinkingEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].thinking);
}
currentConversation.messages[aiMessageIndex].content += delta.content;
contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content) + '<span class="streaming-cursor">▌</span>';
scrollToBottom();
}
}
} catch (e) {}
}
}
}
// 最终渲染
if (thinkingEl && enableThinking && currentConversation.messages[aiMessageIndex].thinking) {
thinkingEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].thinking);
}
contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content);
} catch (error) {
console.error('Error:', error);
currentConversation.messages[aiMessageIndex].content = `抱歉,出现了错误:${error.message}\n\n请检查网络连接后重试。`;
contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content);
} finally {
isLoading = false;
sendBtn.disabled = false;
hideStopGenerateBtn();
currentConversation.updatedAt = Date.now();
saveConversations();
renderMessages();
// 自动总结标题第一次对话和每隔5次对话
const totalMessages = currentConversation.messages.length;
// 第一次对话(用户+AI=2条或每5次对话10条
if (totalMessages === 2 || totalMessages % 10 === 0) {
await generateConversationTitle();
}
}
}
// 执行 Tavily 搜索
async function performSearch(query) {
try {
const response = await fetch(CONFIG.tavilyApiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${CONFIG.tavilyApiKey}`
},
body: JSON.stringify({
query: query,
max_results: 10,
include_raw_content: false
})
});
if (!response.ok) {
console.error('搜索失败:', response.status);
return null;
}
const data = await response.json();
return data.results || [];
} catch (error) {
console.error('搜索错误:', error);
return null;
}
}
// 格式化搜索结果给 LLM
function formatSearchResultsForLLM(results) {
if (!results || results.length === 0) return '无搜索结果';
return results.map((r, i) =>
`${i + 1}. 【${r.title}\n来源: ${r.url}\n摘要: ${r.content || '无摘要'}\n`
).join('\n');
}
// 显示停止生成按钮
function showStopGenerateBtn() {
// 检查是否已存在
if (document.getElementById('stopGenerateBtn')) return;
const stopBtn = document.createElement('button');
stopBtn.id = 'stopGenerateBtn';
stopBtn.className = 'stop-generate-btn';
stopBtn.innerHTML = `
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M6 6h12v12H6z"/></svg>
<span>停止生成</span>
`;
// 插入到消息容器底部
if (messagesContainer) {
messagesContainer.appendChild(stopBtn);
}
}
// 隐藏停止生成按钮
function hideStopGenerateBtn() {
const stopBtn = document.getElementById('stopGenerateBtn');
if (stopBtn) {
stopBtn.remove();
}
}
// 生成对话标题
async function generateConversationTitle() {
if (!currentConversation) return;
console.log('开始生成标题,当前消息数:', currentConversation.messages.length);
// 构建对话摘要
const conversationText = currentConversation.messages.map(m =>
`${m.role === 'user' ? '用户' : 'AI'}: ${m.content.slice(0, 200)}`
).join('\n');
const titlePrompt = `请用不超过20个字总结以下对话的主题只输出标题不要其他内容
${conversationText}`;
try {
const response = await fetch(CONFIG.apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${CONFIG.apiKey}`
},
body: JSON.stringify({
model: CONFIG.model,
messages: [{ role: 'user', content: titlePrompt }],
max_tokens: 100,
thinking: { type: 'disabled' } // 禁用思考模式,直接输出标题
})
});
console.log('标题API响应状态:', response.status);
if (response.ok) {
const data = await response.json();
console.log('标题API响应数据:', data);
const newTitle = data.choices?.[0]?.message?.content?.trim();
console.log('生成的标题:', newTitle);
if (newTitle && newTitle.length > 0) {
// 去掉可能的引号和多余符号
const cleanTitle = newTitle.replace(/^["'"']+|["'"']+$/g, '').trim();
if (cleanTitle.length > 0 && cleanTitle.length <= 30) {
currentConversation.title = cleanTitle;
currentConversation.updatedAt = Date.now();
saveConversations();
console.log('标题已保存:', cleanTitle);
// 更新页面标题显示
const titleEl = document.querySelector('.header h1');
if (titleEl) {
titleEl.textContent = cleanTitle;
}
// 更新侧边栏标题(如果在对话列表页面)
const convItem = document.querySelector(`.conversation-item[data-id="${currentConversation.id}"] .conv-title`);
if (convItem) {
convItem.textContent = cleanTitle;
}
showToast('标题已更新');
}
}
} else {
const errorText = await response.text();
console.error('标题API错误:', errorText);
}
} catch (error) {
console.error('生成标题失败:', error);
}
}
// 重新生成 AI 回复
async function regenerate(index) {
if (!currentConversation || isLoading || index < 1) return;
const userMsgIndex = index - 1;
if (currentConversation.messages[userMsgIndex].role !== 'user') return;
currentConversation.messages.splice(index, 1);
currentConversation.updatedAt = Date.now();
saveConversations();
await streamGenerate(userMsgIndex);
}
// 删除消息
function deleteMessage(index) {
if (!currentConversation || isLoading) return;
const msg = currentConversation.messages[index];
if (msg.role === 'assistant') {
if (index > 0 && currentConversation.messages[index - 1].role === 'user') {
currentConversation.messages.splice(index - 1, 2);
} else {
currentConversation.messages.splice(index, 1);
}
} else {
if (index < currentConversation.messages.length - 1 && currentConversation.messages[index + 1].role === 'assistant') {
currentConversation.messages.splice(index, 2);
} else {
currentConversation.messages.splice(index, 1);
}
}
currentConversation.updatedAt = Date.now();
saveConversations();
renderMessages();
if (currentConversation.messages.length === 0) {
welcome.style.display = 'block';
}
}
// 复制消息(复制原文)
function copyMessage(index) {
if (!currentConversation) return;
const msg = currentConversation.messages[index];
// 如果是图片消息,复制图片描述或提示
let content = msg.content;
if (msg.image && content === '[图片]') {
content = '[图片: ' + (msg.imageName || '未命名') + ']';
}
// HTTP 环境下 navigator.clipboard 不工作,优先使用 fallback
try {
const textarea = document.createElement('textarea');
textarea.value = content;
textarea.style.position = 'fixed';
textarea.style.top = '0';
textarea.style.left = '0';
textarea.style.opacity = '0';
textarea.style.pointerEvents = 'none';
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
const success = document.execCommand('copy');
document.body.removeChild(textarea);
if (success) {
showToast('已复制到剪贴板');
} else {
showToast('复制失败,请手动复制');
}
} catch (err) {
console.error('复制失败:', err);
showToast('复制失败,请手动复制');
}
}
// 清空当前对话
function clearCurrentChat() {
if (!currentConversation) return;
if (confirm('确定要清空当前对话吗?')) {
currentConversation.messages = [];
currentConversation.updatedAt = Date.now();
saveConversations();
renderMessages();
welcome.style.display = 'block';
}
}
// 渲染消息
function renderMessages() {
if (!currentConversation) return;
// 根据消息数量显示/隐藏欢迎界面
if (welcome) {
welcome.style.display = currentConversation.messages.length > 0 ? 'none' : 'block';
}
messagesDiv.innerHTML = currentConversation.messages.map((msg, index) => {
const isUser = msg.role === 'user';
const avatar = isUser ? '👤' : '🤖';
// 处理消息内容(支持图片)
let contentHtml = '';
if (msg.image) {
// 图片消息
contentHtml = `<div class="message-image"><img src="${msg.image}" alt="${msg.imageName || '图片'}"></div>`;
if (msg.content && msg.content !== '[图片]') {
contentHtml += `<div class="message-text">${renderMarkdown(msg.content)}</div>`;
}
} else {
contentHtml = renderMarkdown(msg.content);
}
// 思考内容块仅AI消息
let thinkingHtml = '';
if (!isUser && 'thinking' in msg) {
// 判断是否是当前正在生成的消息有thinking字段且正在加载
const isGenerating = index === currentConversation.messages.length - 1 && isLoading && enableThinking;
// 思考进行中且没有正式内容时展开
const expandedClass = isGenerating && !msg.content ? 'expanded' : '';
thinkingHtml = `
<div class="thinking-block ${expandedClass}" onclick="toggleThinking(this)">
<div class="thinking-header">
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>
<span>思考过程</span>
<svg class="thinking-arrow" viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M7 10l5 5 5-5z"/></svg>
</div>
<div class="thinking-content">${renderMarkdown(msg.thinking || '思考中...')}</div>
</div>`;
}
// 搜索结果块仅AI消息放在思考块前面
let searchHtml = '';
if (!isUser && msg.search_results && msg.search_results.length > 0) {
searchHtml = `
<div class="search-results-block" onclick="toggleSearchResults(this)">
<div class="search-results-header">
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 7 9.5 7 14 9.01 14 9.5 11.99 14 9.5 14z"/></svg>
<span>搜索结果 (${msg.search_results.length})</span>
<svg class="search-results-arrow" viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M7 10l5 5 5-5z"/></svg>
</div>
<div class="search-results-content">
${msg.search_results.map((r, i) => `
<div class="search-result-link">
<span class="search-result-num">${i + 1}</span>
<a href="${r.url}" target="_blank" rel="noopener">${escapeHtml(r.title)}</a>
</div>
`).join('')}
</div>
</div>`;
}
const copyIcon = `<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>`;
const actions = isUser
? `<div class="message-actions">
<button class="action-btn copy-btn" data-index="${index}" title="复制">${copyIcon}</button>
<button class="action-btn delete-btn" data-index="${index}" title="删除">
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
</button>
</div>`
: `<div class="message-actions">
<button class="action-btn copy-btn" data-index="${index}" title="复制">${copyIcon}</button>
<button class="action-btn regenerate-btn" data-index="${index}" title="重新生成">
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
</button>
<button class="action-btn delete-btn" data-index="${index}" title="删除">
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
</button>
</div>`;
return `
<div class="message ${msg.role}" data-index="${index}">
<div class="message-avatar">${avatar}</div>
<div class="message-body">
${searchHtml}
${thinkingHtml}
<div class="message-content">${contentHtml}</div>
${actions}
</div>
</div>
`;
}).join('');
// 绑定消息操作按钮事件(事件委托)
messagesDiv.querySelectorAll('.copy-btn').forEach(btn => {
btn.addEventListener('click', () => copyMessage(parseInt(btn.dataset.index)));
});
messagesDiv.querySelectorAll('.regenerate-btn').forEach(btn => {
btn.addEventListener('click', () => regenerate(parseInt(btn.dataset.index)));
});
messagesDiv.querySelectorAll('.delete-btn').forEach(btn => {
btn.addEventListener('click', () => deleteMessage(parseInt(btn.dataset.index)));
});
scrollToBottom();
}
// 折叠/展开思考内容
function toggleThinking(block) {
block.classList.toggle('expanded');
}
// 折叠/展开搜索结果
function toggleSearchResults(block) {
block.classList.toggle('expanded');
}
// ==================== 工具函数 ====================
// 渲染 Markdown
function renderMarkdown(text) {
if (!text) return '';
marked.setOptions({
breaks: true,
gfm: true
});
return marked.parse(text);
}
// 滚动到底部(智能滚动:只在用户已在底部时自动滚动)
function scrollToBottom() {
if (messagesContainer) {
// 判断用户是否在底部附近距离底部100px以内
const isNearBottom = messagesContainer.scrollHeight - messagesContainer.scrollTop - messagesContainer.clientHeight < 100;
if (isNearBottom || autoScrollEnabled) {
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
}
}
// 监听用户滚动行为
function setupScrollListener() {
if (messagesContainer) {
messagesContainer.addEventListener('scroll', () => {
// 判断用户是否在底部附近
const isNearBottom = messagesContainer.scrollHeight - messagesContainer.scrollTop - messagesContainer.clientHeight < 100;
if (isNearBottom) {
autoScrollEnabled = true;
} else {
// 用户往上滚动,停止自动滚动
autoScrollEnabled = false;
}
});
}
}
// 保存对话列表
function saveConversations() {
localStorage.setItem('conversations', JSON.stringify(conversations));
}
// 显示提示
function showToast(message) {
const toast = document.createElement('div');
toast.className = 'toast';
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => toast.classList.add('show'), 10);
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => document.body.removeChild(toast), 300);
}, 2000);
}
// HTML转义
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 格式化时间
function formatTime(timestamp) {
const date = new Date(timestamp);
const now = new Date();
const diff = now - date;
if (diff < 60000) return '刚刚';
if (diff < 3600000) return Math.floor(diff / 60000) + '分钟前';
if (diff < 86400000) return Math.floor(diff / 3600000) + '小时前';
if (diff < 604800000) return Math.floor(diff / 86400000) + '天前';
return date.toLocaleDateString('zh-CN');
}
// PWA 注册
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('sw.js').catch(() => {});
}
// ==================== 文件上传处理 ====================
// 处理图片上传
async function handleImageUpload(e) {
const file = e.target.files[0];
if (!file) return;
// 读取图片为base64
const reader = new FileReader();
reader.onload = async (event) => {
const base64 = event.target.result;
// 添加用户消息(显示图片)
currentConversation.messages.push({
role: 'user',
content: '[图片]',
image: base64,
imageName: file.name
});
currentConversation.updatedAt = Date.now();
saveConversations();
renderMessages();
// 隐藏欢迎界面
if (welcome) welcome.style.display = 'none';
// 调用AI生成
await streamGenerateWithImage(base64, file.name);
};
reader.readAsDataURL(file);
// 清空input以便再次选择同一文件
e.target.value = '';
}
// 处理文件上传
async function handleFileUpload(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = async (event) => {
const content = event.target.result;
const fileName = file.name;
// 添加用户消息
currentConversation.messages.push({
role: 'user',
content: `[文件: ${fileName}]\\n\\n${content.slice(0, 500)}${content.length > 500 ? '...' : ''}`
});
currentConversation.updatedAt = Date.now();
saveConversations();
renderMessages();
if (welcome) welcome.style.display = 'none';
// 调用AI生成
await streamGenerateWithFile(content, fileName);
};
// 根据文件类型读取
if (file.name.endsWith('.pdf') || file.name.endsWith('.doc') || file.name.endsWith('.docx')) {
// PDF/Word文件暂时只显示文件名
showToast('PDF/Word文件暂不支持解析请上传文本文件');
e.target.value = '';
return;
}
reader.readAsText(file);
e.target.value = '';
}
// 带图片的流式生成
async function streamGenerateWithImage(base64, imageName) {
isLoading = true;
sendBtn.disabled = true;
const aiMessageIndex = currentConversation.messages.length;
currentConversation.messages.push({ role: 'assistant', content: '' });
renderMessages();
const lastMessageEl = messagesDiv.lastElementChild;
const contentEl = lastMessageEl.querySelector('.message-content');
contentEl.innerHTML = '<span class="streaming-cursor">▌</span>';
try {
// 构建多模态消息
const messages = currentConversation.messages.slice(0, aiMessageIndex).map(m => {
if (m.image) {
return {
role: m.role,
content: [
{ type: 'image_url', image_url: { url: m.image } },
{ type: 'text', text: '请分析这张图片' }
]
};
}
return { role: m.role, content: m.content };
});
const response = await fetch(CONFIG.apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${CONFIG.apiKey}`
},
body: JSON.stringify({
model: 'glm-4v-flash', // 视觉模型
messages: messages,
max_tokens: CONFIG.maxTokens,
stream: true
})
});
if (!response.ok) {
throw new Error(`API错误: ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = 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) {
if (line.startsWith('data: ')) {
const jsonStr = line.slice(6).trim();
if (jsonStr === '[DONE]') continue;
try {
const data = JSON.parse(jsonStr);
if (data.choices && data.choices[0]?.delta?.content) {
currentConversation.messages[aiMessageIndex].content += data.choices[0].delta.content;
contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content) + '<span class="streaming-cursor">▌</span>';
scrollToBottom();
}
} catch (err) {}
}
}
}
contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content);
} catch (error) {
console.error('Error:', error);
currentConversation.messages[aiMessageIndex].content = `抱歉,图片分析失败:${error.message}`;
contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content);
} finally {
isLoading = false;
sendBtn.disabled = false;
currentConversation.updatedAt = Date.now();
saveConversations();
renderMessages();
}
}
// 带文件的流式生成
async function streamGenerateWithFile(content, fileName) {
isLoading = true;
sendBtn.disabled = true;
const aiMessageIndex = currentConversation.messages.length;
currentConversation.messages.push({ role: 'assistant', content: '' });
renderMessages();
const lastMessageEl = messagesDiv.lastElementChild;
const contentEl = lastMessageEl.querySelector('.message-content');
contentEl.innerHTML = '<span class="streaming-cursor">▌</span>';
try {
const messages = currentConversation.messages.slice(0, aiMessageIndex).map(m => ({
role: m.role,
content: m.content
}));
// 添加文件内容作为系统提示
messages.unshift({
role: 'system',
content: `以下是用户上传的文件内容,请根据内容回答问题:\\n文件名${fileName}\\n内容\\n${content}`
});
const response = await fetch(CONFIG.apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${CONFIG.apiKey}`
},
body: JSON.stringify({
model: CONFIG.model,
messages: messages,
max_tokens: CONFIG.maxTokens,
stream: true
})
});
if (!response.ok) {
throw new Error(`API错误: ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = 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) {
if (line.startsWith('data: ')) {
const jsonStr = line.slice(6).trim();
if (jsonStr === '[DONE]') continue;
try {
const data = JSON.parse(jsonStr);
if (data.choices && data.choices[0]?.delta?.content) {
currentConversation.messages[aiMessageIndex].content += data.choices[0].delta.content;
contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content) + '<span class="streaming-cursor">▌</span>';
scrollToBottom();
}
} catch (err) {}
}
}
}
contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content);
} catch (error) {
console.error('Error:', error);
currentConversation.messages[aiMessageIndex].content = `抱歉,文件处理失败:${error.message}`;
contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content);
} finally {
isLoading = false;
sendBtn.disabled = false;
currentConversation.updatedAt = Date.now();
saveConversations();
renderMessages();
}
}