1、添加复制消息功能;2 资源加载 优化,全使用本地资源

This commit is contained in:
李振民 2025-06-25 17:48:05 +08:00
parent fab9003b98
commit 4dcf449c99
9 changed files with 405 additions and 17 deletions

View File

@ -30,8 +30,8 @@ class I18nManager:
def __init__(self):
self._current_language = None
self._translations = {}
self._supported_languages = ["zh-TW", "en", "zh-CN"]
self._fallback_language = "en"
self._supported_languages = ["zh-CN", "zh-TW", "en"]
self._fallback_language = "zh-CN"
self._config_file = self._get_config_file_path()
self._locales_dir = Path(__file__).parent / "web" / "locales"
@ -139,7 +139,7 @@ class I18nManager:
def get_current_language(self) -> str:
"""獲取當前語言"""
return self._current_language or "zh-TW"
return self._current_language or "zh-CN"
def set_language(self, language: str) -> bool:
"""設定語言"""

View File

@ -753,7 +753,7 @@
border: 1px solid var(--border-color);
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
max-width: 500px;
max-width: 750px;
width: 90%;
max-height: 80vh;
overflow: hidden;
@ -854,11 +854,149 @@
.detail-value.summary {
background: var(--bg-secondary);
padding: 8px 12px;
padding: 0;
border-radius: 6px;
border-left: 3px solid var(--accent-color);
line-height: 1.4;
margin-top: 4px;
position: relative;
}
.summary-actions {
position: absolute;
top: 8px;
right: 8px;
z-index: 10;
}
.btn-copy-summary {
background: var(--accent-color);
color: white;
border: none;
border-radius: 4px;
padding: 4px 8px;
font-size: 12px;
cursor: pointer;
transition: all var(--transition-fast) ease;
opacity: 0.8;
}
.btn-copy-summary:hover {
opacity: 1;
background: #1976d2;
transform: scale(1.05);
}
.summary-content {
padding: 8px 12px;
padding-right: 50px; /* 为复制按钮留出空间 */
}
/* 为会话详情弹窗中的摘要内容应用与反馈页面相同的 Markdown 样式 */
.detail-value.summary .summary-content h1,
.detail-value.summary .summary-content h2,
.detail-value.summary .summary-content h3,
.detail-value.summary .summary-content h4,
.detail-value.summary .summary-content h5,
.detail-value.summary .summary-content h6 {
color: var(--text-primary);
margin: 16px 0 8px 0;
font-weight: 600;
}
.detail-value.summary .summary-content h1 { font-size: 20px; }
.detail-value.summary .summary-content h2 { font-size: 18px; }
.detail-value.summary .summary-content h3 { font-size: 16px; }
.detail-value.summary .summary-content h4 { font-size: 14px; }
.detail-value.summary .summary-content h5 { font-size: 13px; }
.detail-value.summary .summary-content h6 { font-size: 12px; }
.detail-value.summary .summary-content p {
margin: 8px 0;
line-height: 1.6;
}
.detail-value.summary .summary-content strong {
font-weight: 600;
color: var(--text-primary);
}
.detail-value.summary .summary-content em {
font-style: italic;
color: var(--text-primary);
}
.detail-value.summary .summary-content code {
background: var(--bg-tertiary);
padding: 2px 4px;
border-radius: 3px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 11px;
color: var(--accent-color);
}
.detail-value.summary .summary-content pre {
background: var(--bg-tertiary);
padding: 12px;
border-radius: 6px;
overflow-x: auto;
margin: 12px 0;
border-left: 3px solid var(--accent-color);
}
.detail-value.summary .summary-content pre code {
background: none;
padding: 0;
color: var(--text-primary);
}
.detail-value.summary .summary-content ul,
.detail-value.summary .summary-content ol {
margin: 8px 0;
padding-left: 20px;
}
.detail-value.summary .summary-content li {
margin: 4px 0;
line-height: 1.5;
}
.detail-value.summary .summary-content blockquote {
border-left: 4px solid var(--accent-color);
margin: 12px 0;
padding: 8px 16px;
background: rgba(0, 122, 204, 0.05);
font-style: italic;
}
/* 复制提示样式 */
.copy-toast {
position: fixed;
top: 20px;
right: 20px;
padding: 12px 20px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
color: white;
z-index: 10000;
opacity: 0;
transform: translateX(100px);
transition: all 0.3s ease;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.copy-toast.show {
opacity: 1;
transform: translateX(0);
}
.copy-toast-success {
background: var(--success-color);
}
.copy-toast-error {
background: var(--error-color);
}
.modal-footer {
@ -924,6 +1062,36 @@
background: var(--bg-secondary);
}
.message-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.message-header .btn-copy-message {
background: var(--accent-color);
color: white;
border: none;
border-radius: 3px;
padding: 2px 6px;
font-size: 10px;
cursor: pointer;
transition: all var(--transition-fast) ease;
opacity: 0.7;
margin-left: 8px;
}
.message-header .btn-copy-message:hover {
opacity: 1;
background: #1976d2;
transform: scale(1.05);
}
.user-message-item:hover .btn-copy-message {
opacity: 0.9;
}
.message-header {
display: flex;
justify-content: space-between;

View File

@ -369,7 +369,6 @@ body {
/* 容器樣式 */
.container {
max-width: 1200px;
width: 100%;
margin: 0 auto;
padding: 20px;

View File

@ -8,7 +8,7 @@
class I18nManager {
constructor() {
this.currentLanguage = 'zh-TW';
this.currentLanguage = 'zh-CN';
this.translations = {};
this.loadingPromise = null;
}

View File

@ -45,6 +45,9 @@
console.log('🔍 顯示會話詳情:', sessionData.session_id);
// 存储当前会话数据,供复制功能使用
this.currentSessionData = sessionData;
// 關閉現有彈窗
this.closeModal();
@ -166,7 +169,12 @@
</div>
<div class="detail-row">
<span class="detail-label">${i18n ? i18n.t('sessionManagement.aiSummary') : 'AI 摘要'}:</span>
<div class="detail-value summary">${this.escapeHtml(details.summary)}</div>
<div class="detail-value summary">
<div class="summary-actions">
<button class="btn-copy-summary" title="复制源码" aria-label="复制源码">📋</button>
</div>
<div class="summary-content">${this.renderMarkdownSafely(details.summary)}</div>
</div>
</div>
${this.createUserMessagesSection(details)}
</div>
@ -241,11 +249,12 @@
}
messagesHtml += `
<div class="user-message-item">
<div class="user-message-item" data-message-index="${index}">
<div class="message-header">
<span class="message-index">#${index + 1}</span>
<span class="message-time">${timestamp}</span>
<span class="message-method">${submissionMethod}</span>
<button class="btn-copy-message" title="复制消息内容" aria-label="复制消息内容" data-message-content="${this.escapeHtml(message.content)}">📋</button>
</div>
${contentHtml}
</div>
@ -310,6 +319,24 @@
};
document.addEventListener('keydown', this.keydownHandler);
}
// 复制摘要按钮
const copyBtn = this.currentModal.querySelector('.btn-copy-summary');
if (copyBtn) {
DOMUtils.addEventListener(copyBtn, 'click', function() {
self.copySummaryToClipboard();
});
}
// 复制用户消息按钮
const copyMessageBtns = this.currentModal.querySelectorAll('.btn-copy-message');
copyMessageBtns.forEach(function(btn) {
DOMUtils.addEventListener(btn, 'click', function(e) {
e.stopPropagation(); // 防止事件冒泡
const messageContent = btn.getAttribute('data-message-content');
self.copyMessageToClipboard(messageContent);
});
});
};
/**
@ -361,6 +388,192 @@
return div.innerHTML;
};
/**
* 安全地渲染 Markdown 內容
*/
SessionDetailsModal.prototype.renderMarkdownSafely = function(content) {
if (!content) return '';
try {
// 检查 marked 和 DOMPurify 是否可用
if (typeof window.marked === 'undefined' || typeof window.DOMPurify === 'undefined') {
console.warn('⚠️ Markdown 库未载入,使用纯文字显示');
return this.escapeHtml(content);
}
// 使用 marked 解析 Markdown
const htmlContent = window.marked.parse(content);
// 使用 DOMPurify 清理 HTML
const cleanHtml = window.DOMPurify.sanitize(htmlContent, {
ALLOWED_TAGS: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'br', 'strong', 'em', 'code', 'pre', 'ul', 'ol', 'li', 'blockquote', 'a', 'hr', 'del', 's', 'table', 'thead', 'tbody', 'tr', 'td', 'th'],
ALLOWED_ATTR: ['href', 'title', 'class', 'align', 'style'],
ALLOW_DATA_ATTR: false
});
return cleanHtml;
} catch (error) {
console.error('❌ Markdown 渲染失败:', error);
return this.escapeHtml(content);
}
};
/**
* 复制摘要内容到剪贴板
*/
SessionDetailsModal.prototype.copySummaryToClipboard = function() {
const self = this; // 定义 self 变量
try {
// 获取原始摘要内容Markdown 源码)
const summaryContent = this.currentSessionData && this.currentSessionData.summary ?
this.currentSessionData.summary : '';
if (!summaryContent) {
console.warn('⚠️ 没有摘要内容可复制');
return;
}
// 传统复制方法
const fallbackCopyTextToClipboard = function(text) {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
const successful = document.execCommand('copy');
if (successful) {
console.log('✅ 摘要内容已复制到剪贴板(传统方法)');
self.showToast('✅ 摘要已复制到剪贴板', 'success');
} else {
console.error('❌ 复制失败(传统方法)');
self.showToast('❌ 复制失败,请手动复制', 'error');
}
} catch (err) {
console.error('❌ 复制失败:', err);
self.showToast('❌ 复制失败,请手动复制', 'error');
} finally {
document.body.removeChild(textArea);
}
};
// 使用现代 Clipboard API
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(summaryContent).then(function() {
console.log('✅ 摘要内容已复制到剪贴板');
self.showToast('✅ 摘要已复制到剪贴板', 'success');
}).catch(function(err) {
console.error('❌ 复制失败:', err);
// 降级到传统方法
fallbackCopyTextToClipboard(summaryContent);
});
} else {
// 降级到传统方法
fallbackCopyTextToClipboard(summaryContent);
}
} catch (error) {
console.error('❌ 复制摘要时发生错误:', error);
this.showToast('❌ 复制失败,请手动复制', 'error');
}
};
/**
* 复制用户消息内容到剪贴板
*/
SessionDetailsModal.prototype.copyMessageToClipboard = function(messageContent) {
if (!messageContent) {
console.warn('⚠️ 没有消息内容可复制');
return;
}
const self = this;
try {
// 使用现代 Clipboard API
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(messageContent).then(function() {
console.log('✅ 用户消息已复制到剪贴板');
self.showToast('✅ 消息已复制到剪贴板', 'success');
}).catch(function(err) {
console.error('❌ 复制失败:', err);
// 降级到传统方法
fallbackCopyTextToClipboard(messageContent);
});
} else {
// 降级到传统方法
fallbackCopyTextToClipboard(messageContent);
}
// 传统复制方法
const fallbackCopyTextToClipboard = function(text) {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
const successful = document.execCommand('copy');
if (successful) {
console.log('✅ 用户消息已复制到剪贴板(传统方法)');
self.showToast('✅ 消息已复制到剪贴板', 'success');
} else {
console.error('❌ 复制失败(传统方法)');
self.showToast('❌ 复制失败,请手动复制', 'error');
}
} catch (err) {
console.error('❌ 复制失败:', err);
self.showToast('❌ 复制失败,请手动复制', 'error');
} finally {
document.body.removeChild(textArea);
}
};
} catch (error) {
console.error('❌ 复制用户消息时发生错误:', error);
this.showToast('❌ 复制失败,请手动复制', 'error');
}
};
/**
* 显示提示消息
*/
SessionDetailsModal.prototype.showToast = function(message, type) {
// 创建提示元素
const toast = document.createElement('div');
toast.className = 'copy-toast copy-toast-' + type;
toast.textContent = message;
// 添加到弹窗中
if (this.currentModal) {
this.currentModal.appendChild(toast);
// 显示动画
setTimeout(function() {
toast.classList.add('show');
}, 10);
// 自动隐藏
setTimeout(function() {
toast.classList.remove('show');
setTimeout(function() {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 300);
}, 2000);
}
};
/**
* 檢查是否有彈窗開啟
*/

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="zh-TW" id="html-root">
<html lang="zh-CN" id="html-root">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@ -735,8 +735,8 @@
</div>
<div class="language-selector-dropdown">
<select id="settingsLanguageSelect" class="language-setting-select">
<option value="zh-TW" data-i18n="languages.zh-TW">繁體中文</option>
<option value="zh-CN" data-i18n="languages.zh-CN">简体中文</option>
<option value="zh-TW" data-i18n="languages.zh-TW">繁體中文</option>
<option value="en" data-i18n="languages.en">English</option>
</select>
</div>
@ -1051,9 +1051,9 @@
</div>
<!-- WebSocket 和 JavaScript -->
<!-- Markdown 支援庫 -->
<script src="https://cdn.jsdelivr.net/npm/marked@14.1.3/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.2.2/dist/purify.min.js"></script>
<!-- Markdown 支援庫 - 本地版本 -->
<script src="/static/js/vendor/marked.min.js"></script>
<script src="/static/js/vendor/purify.min.js"></script>
<script src="/static/js/i18n.js?v=2025010510"></script>
<!-- 載入所有模組 -->
<!-- 核心模組(最先載入) -->

View File

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="zh-TW" id="html-root">
<html lang="zh-CN" id="html-root">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@ -129,7 +129,6 @@
/* 主容器 - 有會話時顯示 */
.main-container {
display: none;
max-width: 1200px;
width: 100%;
margin: 0 auto;
padding: 20px;