Compare commits
8 Commits
v2.0.1
...
85dd206154
| Author | SHA1 | Date | |
|---|---|---|---|
| 85dd206154 | |||
| 1b9bb1090c | |||
| 2373040b04 | |||
| f0789d6bbc | |||
| e05233fb4f | |||
| 7fa143b5b0 | |||
| b573638bf8 | |||
| af997aa5c5 |
@@ -52,14 +52,30 @@
|
|||||||
.user-message-text { background: #f0f0f0; padding: 12px 16px; border-radius: 12px; font-size: 15px; }
|
.user-message-text { background: #f0f0f0; padding: 12px 16px; border-radius: 12px; font-size: 15px; }
|
||||||
|
|
||||||
/* 消息操作按钮 */
|
/* 消息操作按钮 */
|
||||||
.message-actions { display: flex; gap: 8px; margin-top: 8px; opacity: 0; transition: opacity 0.2s; }
|
.message-actions { display: flex; gap: 8px; margin-top: 8px; align-items: center; flex-wrap: wrap; }
|
||||||
.message-content:hover + .message-actions,
|
|
||||||
.message-actions:hover { opacity: 1; }
|
|
||||||
.action-btn { padding: 6px 12px; background: #f5f5f5; border: 1px solid #e0e0e0; border-radius: 6px; cursor: pointer; font-size: 12px; color: #666; display: flex; align-items: center; gap: 4px; transition: all 0.2s; }
|
.action-btn { padding: 6px 12px; background: #f5f5f5; border: 1px solid #e0e0e0; border-radius: 6px; cursor: pointer; font-size: 12px; color: #666; display: flex; align-items: center; gap: 4px; transition: all 0.2s; }
|
||||||
.action-btn:hover { background: #e8e8e8; border-color: #ccc; }
|
.action-btn:hover { background: #e8e8e8; border-color: #ccc; }
|
||||||
.action-btn.copied { color: #10a37f; border-color: #10a37f; background: #e8f5e9; }
|
.action-btn.copied { color: #10a37f; border-color: #10a37f; background: #e8f5e9; }
|
||||||
.action-btn.regenerate:hover { color: #667eea; border-color: #667eea; }
|
.action-btn.regenerate:hover { color: #667eea; border-color: #667eea; }
|
||||||
|
|
||||||
|
/* 版本切换控件 - 简洁版 */
|
||||||
|
.version-switcher { display: none; align-items: center; gap: 4px; margin-left: 4px; }
|
||||||
|
.version-switcher.show { display: flex; }
|
||||||
|
.version-arrow { width: 24px; height: 24px; border-radius: 4px; background: #f5f5f5; border: 1px solid #e0e0e0; cursor: pointer; display: flex; align-items: center; justify-content: center; color: #666; font-size: 14px; transition: all 0.2s; }
|
||||||
|
.version-arrow:hover { background: #e8e8e8; border-color: #667eea; color: #667eea; }
|
||||||
|
.version-arrow:disabled { opacity: 0.4; cursor: not-allowed; background: #f5f5f5; }
|
||||||
|
.version-label { font-size: 11px; color: #888; padding: 0 2px; }
|
||||||
|
|
||||||
|
/* 加载动画 */
|
||||||
|
.loading-indicator { display: flex; align-items: center; justify-content: center; gap: 8px; padding: 16px; color: #667eea; }
|
||||||
|
.loading-spinner { width: 20px; height: 20px; border: 2px solid #667eea; border-top-color: transparent; border-radius: 50%; animation: spin 1s linear infinite; }
|
||||||
|
@keyframes spin { to { transform: rotate(360deg); } }
|
||||||
|
|
||||||
|
/* 消息内容容器 */
|
||||||
|
.response-container { position: relative; }
|
||||||
|
.response-version { display: none; }
|
||||||
|
.response-version.active { display: block; }
|
||||||
|
|
||||||
/* 思考内容样式 */
|
/* 思考内容样式 */
|
||||||
.thinking-block { background: #f8f9fa; border-left: 3px solid #667eea; padding: 12px 16px; margin: 8px 0 16px 0; font-size: 14px; color: #666; border-radius: 4px; position: relative; }
|
.thinking-block { background: #f8f9fa; border-left: 3px solid #667eea; padding: 12px 16px; margin: 8px 0 16px 0; font-size: 14px; color: #666; border-radius: 4px; position: relative; }
|
||||||
.thinking-header { display: flex; align-items: center; justify-content: space-between; cursor: pointer; }
|
.thinking-header { display: flex; align-items: center; justify-content: space-between; cursor: pointer; }
|
||||||
@@ -173,6 +189,10 @@
|
|||||||
let agents = [];
|
let agents = [];
|
||||||
let quickPhrases = [];
|
let quickPhrases = [];
|
||||||
let lastUserMessage = null; // 存储最后一条用户消息,用于重新生成
|
let lastUserMessage = null; // 存储最后一条用户消息,用于重新生成
|
||||||
|
let isRegenerating = false; // 标志:正在重新生成,跳过用户消息显示
|
||||||
|
let regeneratingMessageId = null; // 正在重新生成的消息ID
|
||||||
|
let messageVersionCounter = 0; // 消息版本计数器
|
||||||
|
let messageVersions = {}; // 存储每个assistant消息的多个版本 { messageId: [{content, thinking}] }
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
loadAgents();
|
loadAgents();
|
||||||
@@ -242,9 +262,22 @@
|
|||||||
case 'stream_end': document.getElementById('sendBtn').disabled = false; break;
|
case 'stream_end': document.getElementById('sendBtn').disabled = false; break;
|
||||||
case 'user_message':
|
case 'user_message':
|
||||||
lastUserMessage = data.message.content; // 存储最后一条用户消息
|
lastUserMessage = data.message.content; // 存储最后一条用户消息
|
||||||
appendMessage('user', data.message.content);
|
if (!isRegenerating) {
|
||||||
|
appendMessage('user', data.message.content);
|
||||||
|
}
|
||||||
|
// 注意:不要在这里重置 isRegenerating,要等 assistant_message 处理后再重置
|
||||||
|
break;
|
||||||
|
case 'assistant_message':
|
||||||
|
if (isRegenerating && regeneratingMessageId) {
|
||||||
|
// 添加新版本到现有消息
|
||||||
|
addResponseVersion(regeneratingMessageId, data.message.content, data.message.thinking_content);
|
||||||
|
regeneratingMessageId = null;
|
||||||
|
isRegenerating = false; // 在这里重置标志
|
||||||
|
} else {
|
||||||
|
appendMessage('assistant', data.message.content, data.message.thinking_content, data.message.agent_name);
|
||||||
|
}
|
||||||
|
document.getElementById('sendBtn').disabled = false;
|
||||||
break;
|
break;
|
||||||
case 'assistant_message': appendMessage('assistant', data.message.content, data.message.thinking_content, data.message.agent_name); document.getElementById('sendBtn').disabled = false; break;
|
|
||||||
case 'error': showError(data.message); document.getElementById('sendBtn').disabled = false; break;
|
case 'error': showError(data.message); document.getElementById('sendBtn').disabled = false; break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -258,6 +291,17 @@
|
|||||||
div.className = `message ${role}`;
|
div.className = `message ${role}`;
|
||||||
|
|
||||||
const avatar = role === 'user' ? '👤' : '🤖';
|
const avatar = role === 'user' ? '👤' : '🤖';
|
||||||
|
let messageId = null;
|
||||||
|
|
||||||
|
if (role === 'assistant') {
|
||||||
|
// 为assistant消息生成唯一ID
|
||||||
|
messageId = `msg_${++messageVersionCounter}`;
|
||||||
|
div.id = messageId;
|
||||||
|
div.dataset.messageId = messageId;
|
||||||
|
// 初始化版本存储
|
||||||
|
messageVersions[messageId] = [{ content, thinking }];
|
||||||
|
}
|
||||||
|
|
||||||
let html = `<div class="message-avatar">${avatar}</div><div class="message-body">`;
|
let html = `<div class="message-avatar">${avatar}</div><div class="message-body">`;
|
||||||
|
|
||||||
// 思考内容
|
// 思考内容
|
||||||
@@ -271,10 +315,14 @@
|
|||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 消息内容
|
// 消息内容 - assistant使用版本容器
|
||||||
html += `<div class="message-content">`;
|
html += `<div class="message-content">`;
|
||||||
if (role === 'assistant') {
|
if (role === 'assistant') {
|
||||||
html += `<div class="markdown-body">${marked.parse(content)}</div>`;
|
html += `<div class="response-container" id="${messageId}_container">
|
||||||
|
<div class="response-version active" data-version="0">
|
||||||
|
<div class="markdown-body">${marked.parse(content)}</div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
} else {
|
} else {
|
||||||
html += `<div class="user-message-text">${escapeHtml(content)}</div>`;
|
html += `<div class="user-message-text">${escapeHtml(content)}</div>`;
|
||||||
}
|
}
|
||||||
@@ -285,7 +333,13 @@
|
|||||||
html += `<div class="message-actions">`;
|
html += `<div class="message-actions">`;
|
||||||
html += `<button class="action-btn" onclick="copyMessage(this)"><i class="ri-file-copy-line"></i> 复制</button>`;
|
html += `<button class="action-btn" onclick="copyMessage(this)"><i class="ri-file-copy-line"></i> 复制</button>`;
|
||||||
if (role === 'assistant') {
|
if (role === 'assistant') {
|
||||||
html += `<button class="action-btn regenerate" onclick="regenerateMessage()"><i class="ri-refresh-line"></i> 重新生成</button>`;
|
html += `<button class="action-btn regenerate" onclick="regenerateMessage('${messageId}')"><i class="ri-refresh-line"></i> 重新生成</button>`;
|
||||||
|
// 版本切换控件 - 放在重新生成按钮后面
|
||||||
|
html += `<span class="version-switcher" id="${messageId}_version_switcher">`;
|
||||||
|
html += `<button class="version-arrow" onclick="switchVersion('${messageId}', -1)" data-dir="prev"><i class="ri-arrow-left-s-line"></i></button>`;
|
||||||
|
html += `<span class="version-label" id="${messageId}_version_label">1/1</span>`;
|
||||||
|
html += `<button class="version-arrow" onclick="switchVersion('${messageId}', 1)" data-dir="next"><i class="ri-arrow-right-s-line"></i></button>`;
|
||||||
|
html += `</span>`;
|
||||||
}
|
}
|
||||||
html += `</div>`;
|
html += `</div>`;
|
||||||
|
|
||||||
@@ -300,6 +354,167 @@
|
|||||||
container.scrollTop = container.scrollHeight;
|
container.scrollTop = container.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加新版本到现有消息
|
||||||
|
function addResponseVersion(messageId, content, thinking) {
|
||||||
|
const versions = messageVersions[messageId];
|
||||||
|
if (!versions) return;
|
||||||
|
|
||||||
|
// 添加新版本
|
||||||
|
versions.push({ content, thinking });
|
||||||
|
const newVersionIndex = versions.length - 1;
|
||||||
|
|
||||||
|
// 更新容器
|
||||||
|
const container = document.getElementById(`${messageId}_container`);
|
||||||
|
if (container) {
|
||||||
|
// 先移除loading(如果有)
|
||||||
|
hideLoadingIndicator(messageId);
|
||||||
|
|
||||||
|
// 添加思考块(如果有)
|
||||||
|
const messageBody = container.closest('.message-body');
|
||||||
|
if (thinking && messageBody) {
|
||||||
|
// 先移除旧的思考块
|
||||||
|
const oldThinking = messageBody.querySelector('.thinking-block');
|
||||||
|
if (oldThinking) oldThinking.remove();
|
||||||
|
|
||||||
|
// 添加新的思考块
|
||||||
|
const thinkingHtml = `<div class="thinking-block">
|
||||||
|
<div class="thinking-header" onclick="toggleThinking(this)">
|
||||||
|
<span><i class="ri-lightbulb-line"></i> 思考过程</span>
|
||||||
|
<span class="thinking-toggle">点击展开</span>
|
||||||
|
</div>
|
||||||
|
<div class="thinking-content">${escapeHtml(thinking)}</div>
|
||||||
|
</div>`;
|
||||||
|
messageBody.insertAdjacentHTML('afterbegin', thinkingHtml);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 隐藏所有旧版本,显示最新版本
|
||||||
|
container.querySelectorAll('.response-version').forEach(v => v.classList.remove('active'));
|
||||||
|
const newVersionHtml = `<div class="response-version active" data-version="${newVersionIndex}">
|
||||||
|
<div class="markdown-body">${marked.parse(content)}</div>
|
||||||
|
</div>`;
|
||||||
|
container.insertAdjacentHTML('beforeend', newVersionHtml);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新复制源
|
||||||
|
const messageDiv = document.getElementById(messageId);
|
||||||
|
if (messageDiv) {
|
||||||
|
const copySource = messageDiv.querySelector('.copy-source');
|
||||||
|
if (copySource) copySource.value = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示版本切换控件并更新指示器
|
||||||
|
showVersionControls(messageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示loading动画
|
||||||
|
function showLoadingIndicator(messageId) {
|
||||||
|
const container = document.getElementById(`${messageId}_container`);
|
||||||
|
if (container) {
|
||||||
|
// 隐藏所有版本
|
||||||
|
container.querySelectorAll('.response-version').forEach(v => v.classList.remove('active'));
|
||||||
|
// 显示loading
|
||||||
|
const loadingHtml = `<div class="loading-indicator" id="${messageId}_loading">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
<span>正在生成...</span>
|
||||||
|
</div>`;
|
||||||
|
container.insertAdjacentHTML('beforeend', loadingHtml);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 隐藏loading动画
|
||||||
|
function hideLoadingIndicator(messageId) {
|
||||||
|
const loading = document.getElementById(`${messageId}_loading`);
|
||||||
|
if (loading) loading.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示版本切换控件
|
||||||
|
function showVersionControls(messageId) {
|
||||||
|
const switcher = document.getElementById(`${messageId}_version_switcher`);
|
||||||
|
const versions = messageVersions[messageId];
|
||||||
|
if (switcher && versions && versions.length > 1) {
|
||||||
|
switcher.classList.add('show');
|
||||||
|
// 新生成的版本是最后一个,切换到最新版本
|
||||||
|
const container = document.getElementById(`${messageId}_container`);
|
||||||
|
if (container) {
|
||||||
|
// 隐藏所有版本,显示最后一个
|
||||||
|
container.querySelectorAll('.response-version').forEach(v => v.classList.remove('active'));
|
||||||
|
const lastVersion = container.querySelector(`.response-version[data-version="${versions.length - 1}"]`);
|
||||||
|
if (lastVersion) lastVersion.classList.add('active');
|
||||||
|
}
|
||||||
|
updateVersionIndicator(messageId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新版本指示器
|
||||||
|
function updateVersionIndicator(messageId) {
|
||||||
|
const container = document.getElementById(`${messageId}_container`);
|
||||||
|
const label = document.getElementById(`${messageId}_version_label`);
|
||||||
|
const versions = messageVersions[messageId];
|
||||||
|
if (!container || !label || !versions) return;
|
||||||
|
|
||||||
|
// 找到当前激活的版本
|
||||||
|
const activeVersion = container.querySelector('.response-version.active');
|
||||||
|
const currentIndex = activeVersion ? parseInt(activeVersion.dataset.version) : 0;
|
||||||
|
|
||||||
|
label.textContent = `${currentIndex + 1}/${versions.length}`;
|
||||||
|
|
||||||
|
// 更新按钮状态
|
||||||
|
const switcher = document.getElementById(`${messageId}_version_switcher`);
|
||||||
|
if (switcher) {
|
||||||
|
const prevBtn = switcher.querySelector('[data-dir="prev"]');
|
||||||
|
const nextBtn = switcher.querySelector('[data-dir="next"]');
|
||||||
|
if (prevBtn) prevBtn.disabled = currentIndex === 0;
|
||||||
|
if (nextBtn) nextBtn.disabled = currentIndex === versions.length - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换版本
|
||||||
|
function switchVersion(messageId, direction) {
|
||||||
|
const container = document.getElementById(`${messageId}_container`);
|
||||||
|
const versions = messageVersions[messageId];
|
||||||
|
if (!container || !versions) return;
|
||||||
|
|
||||||
|
// 找到当前激活的版本
|
||||||
|
const activeVersion = container.querySelector('.response-version.active');
|
||||||
|
const currentIndex = activeVersion ? parseInt(activeVersion.dataset.version) : 0;
|
||||||
|
const newIndex = currentIndex + direction;
|
||||||
|
|
||||||
|
if (newIndex < 0 || newIndex >= versions.length) return;
|
||||||
|
|
||||||
|
// 切换显示
|
||||||
|
container.querySelectorAll('.response-version').forEach(v => {
|
||||||
|
v.classList.remove('active');
|
||||||
|
if (parseInt(v.dataset.version) === newIndex) v.classList.add('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新复制源
|
||||||
|
const messageDiv = document.getElementById(messageId);
|
||||||
|
if (messageDiv) {
|
||||||
|
const copySource = messageDiv.querySelector('.copy-source');
|
||||||
|
if (copySource) copySource.value = versions[newIndex].content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新思考块
|
||||||
|
const messageBody = container.closest('.message-body');
|
||||||
|
if (messageBody && versions[newIndex].thinking) {
|
||||||
|
// 移除旧的思考块
|
||||||
|
const oldThinking = messageBody.querySelector('.thinking-block');
|
||||||
|
if (oldThinking) oldThinking.remove();
|
||||||
|
|
||||||
|
// 添加新版本的思考块
|
||||||
|
const thinkingHtml = `<div class="thinking-block">
|
||||||
|
<div class="thinking-header" onclick="toggleThinking(this)">
|
||||||
|
<span><i class="ri-lightbulb-line"></i> 思考过程</span>
|
||||||
|
<span class="thinking-toggle">点击展开</span>
|
||||||
|
</div>
|
||||||
|
<div class="thinking-content">${escapeHtml(versions[newIndex].thinking)}</div>
|
||||||
|
</div>`;
|
||||||
|
messageBody.insertAdjacentHTML('afterbegin', thinkingHtml);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateVersionIndicator(messageId);
|
||||||
|
}
|
||||||
|
|
||||||
function toggleThinking(header) {
|
function toggleThinking(header) {
|
||||||
const block = header.parentElement;
|
const block = header.parentElement;
|
||||||
const content = block.querySelector('.thinking-content');
|
const content = block.querySelector('.thinking-content');
|
||||||
@@ -315,37 +530,58 @@
|
|||||||
|
|
||||||
function copyMessage(btn) {
|
function copyMessage(btn) {
|
||||||
// 从隐藏input获取原始内容
|
// 从隐藏input获取原始内容
|
||||||
const hiddenInput = btn.closest('.message-body').querySelector('.copy-source');
|
const actionsDiv = btn.parentElement;
|
||||||
|
const messageBody = actionsDiv.parentElement;
|
||||||
|
const hiddenInput = messageBody.querySelector('.copy-source');
|
||||||
if (!hiddenInput) {
|
if (!hiddenInput) {
|
||||||
console.error('找不到复制源');
|
console.error('找不到复制源');
|
||||||
|
btn.innerHTML = '<i class="ri-error-line"></i> 失败';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const text = hiddenInput.value;
|
const text = hiddenInput.value;
|
||||||
|
|
||||||
navigator.clipboard.writeText(text).then(() => {
|
// 使用传统复制方法(兼容性更好)
|
||||||
btn.innerHTML = '<i class="ri-check-line"></i> 已复制';
|
const textarea = document.createElement('textarea');
|
||||||
btn.classList.add('copied');
|
textarea.value = text;
|
||||||
setTimeout(() => {
|
textarea.style.position = 'fixed';
|
||||||
btn.innerHTML = '<i class="ri-file-copy-line"></i> 复制';
|
textarea.style.left = '-9999px';
|
||||||
btn.classList.remove('copied');
|
textarea.style.top = '0';
|
||||||
}, 2000);
|
document.body.appendChild(textarea);
|
||||||
}).catch(err => {
|
textarea.focus();
|
||||||
|
textarea.select();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const success = document.execCommand('copy');
|
||||||
|
if (success) {
|
||||||
|
btn.innerHTML = '<i class="ri-check-line"></i> 已复制';
|
||||||
|
btn.classList.add('copied');
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.innerHTML = '<i class="ri-file-copy-line"></i> 复制';
|
||||||
|
btn.classList.remove('copied');
|
||||||
|
}, 2000);
|
||||||
|
} else {
|
||||||
|
btn.innerHTML = '<i class="ri-error-line"></i> 失败';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
console.error('复制失败:', err);
|
console.error('复制失败:', err);
|
||||||
btn.innerHTML = '<i class="ri-error-line"></i> 失败';
|
btn.innerHTML = '<i class="ri-error-line"></i> 失败';
|
||||||
});
|
}
|
||||||
|
|
||||||
|
document.body.removeChild(textarea);
|
||||||
}
|
}
|
||||||
|
|
||||||
function regenerateMessage() {
|
function regenerateMessage(messageId) {
|
||||||
if (!lastUserMessage) {
|
if (!lastUserMessage) {
|
||||||
alert('没有可重新生成的消息');
|
alert('没有可重新生成的消息');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 移除最后一条助手消息
|
// 设置重新生成标志,避免再次显示用户消息
|
||||||
const container = document.getElementById('messagesContainer');
|
isRegenerating = true;
|
||||||
const messages = container.querySelectorAll('.message.assistant');
|
regeneratingMessageId = messageId;
|
||||||
if (messages.length > 0) {
|
|
||||||
messages[messages.length - 1].remove();
|
// 显示loading动画
|
||||||
}
|
showLoadingIndicator(messageId);
|
||||||
|
|
||||||
// 重新发送最后一条用户消息
|
// 重新发送最后一条用户消息
|
||||||
document.getElementById('sendBtn').disabled = true;
|
document.getElementById('sendBtn').disabled = true;
|
||||||
if (ws?.readyState === WebSocket.OPEN) {
|
if (ws?.readyState === WebSocket.OPEN) {
|
||||||
@@ -408,6 +644,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function createNewConversation() {
|
async function createNewConversation() {
|
||||||
|
// 检查是否已经是新建对话状态(无对话ID且无消息)
|
||||||
|
const container = document.getElementById('messagesContainer');
|
||||||
|
const hasMessages = container.querySelectorAll('.message').length > 0;
|
||||||
|
|
||||||
|
// 如果当前没有对话ID且没有消息,则不创建新对话
|
||||||
|
if (!currentConversationId && !hasMessages) {
|
||||||
|
return; // 已经是新建对话状态,无需创建
|
||||||
|
}
|
||||||
|
|
||||||
const res = await fetch('/api/conversations', { method: 'POST' });
|
const res = await fetch('/api/conversations', { method: 'POST' });
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
currentConversationId = data.id;
|
currentConversationId = data.id;
|
||||||
|
|||||||
Reference in New Issue
Block a user