Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9098e6b4f7 | |||
| fb09560259 | |||
| 83bdab3205 | |||
| 610ee8f409 | |||
| 8451f04302 | |||
| c6bdfb15cc | |||
| ac5335ac4f | |||
| 99000f6f04 |
113
www/app.js
113
www/app.js
@@ -19,6 +19,7 @@ let isLoading = false;
|
|||||||
// 功能开关
|
// 功能开关
|
||||||
let enableThinking = false; // 深度思考
|
let enableThinking = false; // 深度思考
|
||||||
let enableSearch = false; // 联网搜索
|
let enableSearch = false; // 联网搜索
|
||||||
|
let autoScrollEnabled = true; // 自动滚动(用户滚动后可关闭)
|
||||||
|
|
||||||
// DOM 元素(初始为 null,在 openConversation 时重新获取)
|
// DOM 元素(初始为 null,在 openConversation 时重新获取)
|
||||||
let appContainer = null;
|
let appContainer = null;
|
||||||
@@ -575,6 +576,12 @@ function openConversation(id) {
|
|||||||
thinkingBtn = document.getElementById('thinkingBtn');
|
thinkingBtn = document.getElementById('thinkingBtn');
|
||||||
searchBtn = document.getElementById('searchBtn');
|
searchBtn = document.getElementById('searchBtn');
|
||||||
|
|
||||||
|
// 重置自动滚动状态
|
||||||
|
autoScrollEnabled = true;
|
||||||
|
|
||||||
|
// 设置滚动监听
|
||||||
|
setupScrollListener();
|
||||||
|
|
||||||
// 绑定按钮事件
|
// 绑定按钮事件
|
||||||
const backBtn = document.getElementById('backBtn');
|
const backBtn = document.getElementById('backBtn');
|
||||||
if (backBtn) backBtn.addEventListener('click', showConversationList);
|
if (backBtn) backBtn.addEventListener('click', showConversationList);
|
||||||
@@ -587,11 +594,6 @@ function openConversation(id) {
|
|||||||
thinkingBtn.addEventListener('click', () => {
|
thinkingBtn.addEventListener('click', () => {
|
||||||
enableThinking = !enableThinking;
|
enableThinking = !enableThinking;
|
||||||
thinkingBtn.classList.toggle('active', enableThinking);
|
thinkingBtn.classList.toggle('active', enableThinking);
|
||||||
// 如果开启深度思考,关闭联网搜索(智谱思考模型不支持联网)
|
|
||||||
if (enableThinking && enableSearch) {
|
|
||||||
enableSearch = false;
|
|
||||||
searchBtn.classList.remove('active');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -599,11 +601,6 @@ function openConversation(id) {
|
|||||||
searchBtn.addEventListener('click', () => {
|
searchBtn.addEventListener('click', () => {
|
||||||
enableSearch = !enableSearch;
|
enableSearch = !enableSearch;
|
||||||
searchBtn.classList.toggle('active', enableSearch);
|
searchBtn.classList.toggle('active', enableSearch);
|
||||||
// 如果开启联网搜索,关闭深度思考
|
|
||||||
if (enableSearch && enableThinking) {
|
|
||||||
enableThinking = false;
|
|
||||||
thinkingBtn.classList.remove('active');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -749,6 +746,9 @@ async function sendMessage() {
|
|||||||
currentConversation.updatedAt = Date.now();
|
currentConversation.updatedAt = Date.now();
|
||||||
saveConversations();
|
saveConversations();
|
||||||
|
|
||||||
|
// 发送新消息时启用自动滚动
|
||||||
|
autoScrollEnabled = true;
|
||||||
|
|
||||||
renderMessages();
|
renderMessages();
|
||||||
userInput.value = '';
|
userInput.value = '';
|
||||||
autoResize(userInput);
|
autoResize(userInput);
|
||||||
@@ -888,12 +888,17 @@ async function streamGenerate(userMsgIndex) {
|
|||||||
if (thinkingEl) {
|
if (thinkingEl) {
|
||||||
thinkingEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].thinking) + '<span class="streaming-cursor">▌</span>';
|
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();
|
scrollToBottom();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理正式回复内容
|
// 处理正式回复内容
|
||||||
if (delta.content) {
|
if (delta.content) {
|
||||||
// 如果开启深度思考且开始输出正式内容,折叠思考块
|
// 如果开启深度思考且开始输出正式内容,说明思考完成,立即折叠思考块
|
||||||
if (enableThinking && !thinkingOutputStarted && currentConversation.messages[aiMessageIndex].thinking) {
|
if (enableThinking && !thinkingOutputStarted && currentConversation.messages[aiMessageIndex].thinking) {
|
||||||
thinkingOutputStarted = true;
|
thinkingOutputStarted = true;
|
||||||
// 折叠思考内容
|
// 折叠思考内容
|
||||||
@@ -932,7 +937,8 @@ async function streamGenerate(userMsgIndex) {
|
|||||||
|
|
||||||
// 自动总结标题:第一次对话和每隔5次对话
|
// 自动总结标题:第一次对话和每隔5次对话
|
||||||
const totalMessages = currentConversation.messages.length;
|
const totalMessages = currentConversation.messages.length;
|
||||||
if (totalMessages === 1 || totalMessages % 5 === 0) {
|
// 第一次对话(用户+AI=2条)或每5次对话(10条)
|
||||||
|
if (totalMessages === 2 || totalMessages % 10 === 0) {
|
||||||
await generateConversationTitle();
|
await generateConversationTitle();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -949,7 +955,7 @@ async function performSearch(query) {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
query: query,
|
query: query,
|
||||||
max_results: 5,
|
max_results: 10,
|
||||||
include_raw_content: false
|
include_raw_content: false
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
@@ -1007,12 +1013,14 @@ function hideStopGenerateBtn() {
|
|||||||
async function generateConversationTitle() {
|
async function generateConversationTitle() {
|
||||||
if (!currentConversation) return;
|
if (!currentConversation) return;
|
||||||
|
|
||||||
|
console.log('开始生成标题,当前消息数:', currentConversation.messages.length);
|
||||||
|
|
||||||
// 构建对话摘要
|
// 构建对话摘要
|
||||||
const conversationText = currentConversation.messages.map(m =>
|
const conversationText = currentConversation.messages.map(m =>
|
||||||
`${m.role === 'user' ? '用户' : 'AI'}: ${m.content.slice(0, 200)}`
|
`${m.role === 'user' ? '用户' : 'AI'}: ${m.content.slice(0, 200)}`
|
||||||
).join('\n');
|
).join('\n');
|
||||||
|
|
||||||
const titlePrompt = `请用不超过10个字总结以下对话的主题,只输出标题,不要其他内容:
|
const titlePrompt = `请用不超过20个字总结以下对话的主题,只输出标题,不要其他内容:
|
||||||
${conversationText}`;
|
${conversationText}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1025,24 +1033,46 @@ ${conversationText}`;
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: CONFIG.model,
|
model: CONFIG.model,
|
||||||
messages: [{ role: 'user', content: titlePrompt }],
|
messages: [{ role: 'user', content: titlePrompt }],
|
||||||
max_tokens: 50
|
max_tokens: 100,
|
||||||
|
thinking: { type: 'disabled' } // 禁用思考模式,直接输出标题
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('标题API响应状态:', response.status);
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
console.log('标题API响应数据:', data);
|
||||||
const newTitle = data.choices?.[0]?.message?.content?.trim();
|
const newTitle = data.choices?.[0]?.message?.content?.trim();
|
||||||
if (newTitle && newTitle.length > 0 && newTitle.length <= 15) {
|
console.log('生成的标题:', newTitle);
|
||||||
currentConversation.title = newTitle;
|
|
||||||
currentConversation.updatedAt = Date.now();
|
|
||||||
saveConversations();
|
|
||||||
|
|
||||||
// 更新页面标题显示
|
if (newTitle && newTitle.length > 0) {
|
||||||
const titleEl = document.querySelector('.header h1');
|
// 去掉可能的引号和多余符号
|
||||||
if (titleEl) {
|
const cleanTitle = newTitle.replace(/^["'"']+|["'"']+$/g, '').trim();
|
||||||
titleEl.textContent = newTitle;
|
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) {
|
} catch (error) {
|
||||||
console.error('生成标题失败:', error);
|
console.error('生成标题失败:', error);
|
||||||
@@ -1170,15 +1200,20 @@ function renderMessages() {
|
|||||||
|
|
||||||
// 思考内容块(仅AI消息)
|
// 思考内容块(仅AI消息)
|
||||||
let thinkingHtml = '';
|
let thinkingHtml = '';
|
||||||
if (!isUser && msg.thinking) {
|
if (!isUser && 'thinking' in msg) {
|
||||||
|
// 判断是否是当前正在生成的消息(有thinking字段且正在加载)
|
||||||
|
const isGenerating = index === currentConversation.messages.length - 1 && isLoading && enableThinking;
|
||||||
|
// 思考进行中且没有正式内容时展开
|
||||||
|
const expandedClass = isGenerating && !msg.content ? 'expanded' : '';
|
||||||
|
|
||||||
thinkingHtml = `
|
thinkingHtml = `
|
||||||
<div class="thinking-block" onclick="toggleThinking(this)">
|
<div class="thinking-block ${expandedClass}" onclick="toggleThinking(this)">
|
||||||
<div class="thinking-header">
|
<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>
|
<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>
|
<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>
|
<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>
|
||||||
<div class="thinking-content">${renderMarkdown(msg.thinking)}</div>
|
<div class="thinking-content">${renderMarkdown(msg.thinking || '思考中...')}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1273,10 +1308,32 @@ function renderMarkdown(text) {
|
|||||||
return marked.parse(text);
|
return marked.parse(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 滚动到底部
|
// 滚动到底部(智能滚动:只在用户已在底部时自动滚动)
|
||||||
function scrollToBottom() {
|
function scrollToBottom() {
|
||||||
if (messagesContainer) {
|
if (messagesContainer) {
|
||||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
// 判断用户是否在底部附近(距离底部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;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,12 +8,12 @@
|
|||||||
<meta http-equiv="Pragma" content="no-cache">
|
<meta http-equiv="Pragma" content="no-cache">
|
||||||
<meta http-equiv="Expires" content="0">
|
<meta http-equiv="Expires" content="0">
|
||||||
<title>AI助手</title>
|
<title>AI助手</title>
|
||||||
<link rel="stylesheet" href="style.css?v=2.7.0">
|
<link rel="stylesheet" href="style.css?v=2.8.0">
|
||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="manifest.json">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script src="marked.min.js?v=2.7.0"></script>
|
<script src="marked.min.js?v=2.8.0"></script>
|
||||||
<script src="app.js?v=2.7.0"></script>
|
<script src="app.js?v=2.8.0"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user