1. 模型对比页面完善,包括bug修复,支持markdown展示

This commit is contained in:
2026-01-22 14:09:25 +08:00
parent f126139bbd
commit d7fa8583f7
6 changed files with 452 additions and 54 deletions

View File

@@ -11,6 +11,46 @@
.text-primary { color: #1890ff; }
:root { --primary: #1890ff; }
/* 卡片内容区域滚动条样式 */
.markdown-content {
overflow-y: auto;
max-height: calc(100vh - 280px);
scrollbar-width: thin;
scrollbar-color: #d1d5db transparent;
}
.markdown-content::-webkit-scrollbar {
width: 6px;
}
.markdown-content::-webkit-scrollbar-track {
background: transparent;
}
.markdown-content::-webkit-scrollbar-thumb {
background-color: #d1d5db;
border-radius: 3px;
}
/* Markdown 基础样式 */
.markdown-content h1 { font-size: 1.5em; font-weight: bold; margin: 0.5em 0; border-bottom: 1px solid #e5e7eb; padding-bottom: 0.3em; }
.markdown-content h2 { font-size: 1.25em; font-weight: bold; margin: 0.5em 0; border-bottom: 1px solid #e5e7eb; padding-bottom: 0.2em; }
.markdown-content h3 { font-size: 1.1em; font-weight: bold; margin: 0.4em 0; }
.markdown-content p { margin: 0.5em 0; line-height: 1.6; }
.markdown-content ul, .markdown-content ol { margin: 0.5em 0; padding-left: 2em; }
.markdown-content li { margin: 0.25em 0; }
.markdown-content code { background-color: #f3f4f6; padding: 0.2em 0.4em; border-radius: 4px; font-family: monospace; font-size: 0.9em; }
.markdown-content pre { background-color: #1f2937; color: #f9fafb; padding: 1em; border-radius: 8px; overflow-x: auto; margin: 0.5em 0; }
.markdown-content pre code { background: none; padding: 0; color: inherit; }
.markdown-content blockquote { border-left: 4px solid #1890ff; padding-left: 1em; margin: 0.5em 0; color: #6b7280; }
.markdown-content table { border-collapse: collapse; width: 100%; margin: 0.5em 0; }
.markdown-content th, .markdown-content td { border: 1px solid #d1d5db; padding: 0.5em 1em; text-align: left; }
.markdown-content th { background-color: #f9fafb; font-weight: bold; }
.markdown-content tr:nth-child(even) { background-color: #f9fafb; }
.markdown-content a { color: #1890ff; text-decoration: none; }
.markdown-content a:hover { text-decoration: underline; }
.markdown-content hr { border: none; border-top: 1px solid #e5e7eb; margin: 1em 0; }
.markdown-content strong { font-weight: bold; }
.markdown-content em { font-style: italic; }
.markdown-content del { text-decoration: line-through; color: #9ca3af; }
/* 流式输出光标 */
.typing-cursor::after {
content: '|';
@@ -49,6 +89,7 @@
</div>
<script>
console.log('>>> model-compare-result.html 脚本开始加载');
// 动态获取 API 基础地址
const getApiBase = () => {
const protocol = window.location.protocol;
@@ -73,6 +114,8 @@
let compareTaskId = null;
let taskName = '';
let userQuestion = '';
let chatResults = [];
let useLocalStorageResults = false; // 是否使用localStorage中的结果
// 模拟回复内容
const mockResponses = [
@@ -84,23 +127,90 @@
// 页面初始化
async function initPage() {
const urlParams = new URLSearchParams(window.location.search);
compareTaskId = urlParams.get('taskId');
taskName = urlParams.get('taskName') || '对比任务';
userQuestion = decodeURIComponent(urlParams.get('question') || '');
try {
const urlParams = new URLSearchParams(window.location.search);
compareTaskId = urlParams.get('taskId');
taskName = urlParams.get('taskName') || '对比任务';
userQuestion = decodeURIComponent(urlParams.get('question') || '');
const needRealData = urlParams.get('real') === '1';
// 设置用户提问
document.getElementById('questionText').textContent = userQuestion;
// 设置用户提问
const questionTextEl = document.getElementById('questionText');
if (questionTextEl) {
questionTextEl.textContent = userQuestion;
}
// 加载模型列表
await loadModels();
// 加载模型列表和任务数据
await Promise.all([loadModels(), loadCompareTask()]);
// 加载任务数据获取模型列表
await loadCompareTask();
// 初始化输出卡片(显示加载中状态)
initializeOutputCards(true);
// 初始化输出卡片并开始模拟流式输出
initializeOutputCards();
simulateStreaming();
// 读取 localStorage 结果
let shouldUseStoredResults = false;
const storedResults = localStorage.getItem('chatResults');
if (storedResults && !needRealData) {
try {
const parsed = JSON.parse(storedResults);
const isRecent = (Date.now() - parsed.timestamp) < 5 * 60 * 1000;
if (parsed.results && isRecent) {
chatResults = parsed.results;
shouldUseStoredResults = true;
}
} catch (e) {
console.error('解析localStorage失败:', e);
}
}
// 如果需要真实数据或没有缓存结果则调用API
if (needRealData || !shouldUseStoredResults) {
if (selectedModelIds.length > 0 && userQuestion) {
await fetchChatResults();
// 存储到 localStorage
localStorage.setItem('chatResults', JSON.stringify({
results: chatResults,
timestamp: Date.now()
}));
}
}
// 重新初始化卡片(移除加载状态)
initializeOutputCards(false);
// 开始流式输出
simulateStreaming();
} catch (err) {
console.error('初始化失败:', err);
const contentGrid = document.getElementById('outputGrid');
if (contentGrid) {
contentGrid.innerHTML = `<div class="text-red-500 p-8">初始化失败: ${err.message}</div>`;
}
}
}
// 调用批量对话 API 获取结果
async function fetchChatResults() {
try {
const response = await fetch(`${API_BASE}/model-chat/batch`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model_ids: selectedModelIds,
system_prompt: '',
user_question: userQuestion,
temperature: 0.7,
max_tokens: 2048
})
});
const result = await response.json();
if (result.code === 0 && result.data) {
chatResults = result.data;
}
} catch (error) {
console.error('调用API失败:', error);
}
}
// 加载对比任务数据
@@ -146,12 +256,11 @@
}
} catch (error) {
console.error('加载模型失败:', error);
allModels = mockModels;
}
}
// 初始化输出卡片
function initializeOutputCards() {
function initializeOutputCards(showLoading = false) {
const grid = document.getElementById('outputGrid');
const allAvailableModels = [...allModels, ...mockModels];
@@ -160,21 +269,26 @@
? selectedModelIds.slice(0, 4)
: [1, 2, 3, 4];
const statusText = showLoading ? '加载中...' : '等待中';
const statusIcon = showLoading ? 'fa-spinner fa-spin' : 'fa-clock-o';
const statusClass = showLoading ? 'text-primary' : 'text-gray-400';
const contentText = showLoading ? '<span class="text-gray-400">正在调用模型API...</span>' : '<span class="text-gray-300">模型即将开始生成回答...</span>';
grid.innerHTML = displayModelIds.map((modelId, index) => {
const model = allAvailableModels.find(m => m.id === modelId) || { name: `模型 ${modelId}` };
const colors = ['bg-blue-100 text-blue-700', 'bg-green-100 text-green-700', 'bg-purple-100 text-purple-700', 'bg-orange-100 text-orange-700'];
const colorClass = colors[index % colors.length];
return `
<div class="bg-white rounded-lg shadow-sm flex flex-col h-full" id="output-${modelId}">
<div class="flex items-center justify-between px-5 py-3 border-b border-gray-100">
<div class="flex items-center justify-between px-5 py-3 border-b border-gray-100 flex-shrink-0">
<span class="px-3 py-1 rounded text-sm font-medium ${colorClass}">${model.name}</span>
<span id="status-${modelId}" class="text-xs text-gray-400 flex items-center">
<i class="fa fa-clock-o mr-1"></i>
等待中
<span id="status-${modelId}" class="text-xs ${statusClass} flex items-center">
<i class="fa ${statusIcon} mr-1"></i>
${statusText}
</span>
</div>
<div id="content-${modelId}" class="flex-1 overflow-y-auto p-8 text-base text-gray-600 leading-relaxed min-h-[300px]">
<span class="text-gray-300">模型即将开始生成回答...</span>
<div id="content-${modelId}" class="markdown-content flex-1 p-8 text-base text-gray-600 leading-relaxed">
${contentText}
</div>
</div>
`;
@@ -204,6 +318,27 @@
});
}
// 获取模型对应的真实响应
function getRealResponse(modelId) {
if (chatResults.length === 0) return null;
const result = chatResults.find(r => r.model_id == modelId || r.modelId == modelId);
if (result && result.response && result.response.trim() !== '') {
return result.response;
}
return null;
}
// 获取错误信息
function getErrorMessage(modelId) {
if (chatResults.length === 0) return null;
const result = chatResults.find(r => r.model_id == modelId || r.modelId == modelId);
// 如果有 error 字段且 response 为空,返回错误信息
if (result && result.error && (!result.response || result.response.trim() === '')) {
return result.error;
}
return null;
}
// 流式输出单个模型
function streamModelResponse(modelId, responseIndex) {
const contentEl = document.getElementById(`content-${modelId}`);
@@ -218,24 +353,69 @@
contentEl.innerHTML = '';
contentEl.classList.add('typing-cursor');
const response = mockResponses[responseIndex % mockResponses.length];
// 获取响应内容(真实数据或模拟数据)
const realResponse = getRealResponse(modelId);
const errorMessage = getErrorMessage(modelId);
let response;
let isRealData = false;
if (realResponse) {
response = realResponse;
isRealData = true;
} else if (errorMessage) {
response = `[错误] ${errorMessage}`;
} else {
response = mockResponses[responseIndex % mockResponses.length];
}
// 对于错误信息直接显示,不进行 Markdown 渲染
const isError = errorMessage && !realResponse;
// 标记是否需要 Markdown 渲染(真实数据需要)
const needMarkdown = isRealData && !isError;
// 统计信息
const startTime = Date.now();
let firstCharTime = null;
let charIndex = 0;
const interval = setInterval(() => {
if (charIndex < response.length) {
contentEl.textContent = response.substring(0, charIndex + 1);
// 记录第一个字符输出的时间
if (charIndex === 0) {
firstCharTime = Date.now();
}
// 流式输出期间直接显示 Markdown 格式
const currentText = response.substring(0, charIndex + 1);
if (needMarkdown) {
contentEl.innerHTML = marked.parse(currentText);
} else {
contentEl.textContent = currentText;
}
charIndex++;
contentEl.scrollTop = contentEl.scrollHeight;
} else {
clearInterval(interval);
const idx = currentStreamingIntervals.indexOf(interval);
if (idx > -1) currentStreamingIntervals.splice(idx, 1);
statusEl.innerHTML = '<i class="fa fa-check-circle text-green-500 mr-1"></i> 完成';
statusEl.classList.remove('text-primary');
statusEl.classList.add('text-green-500');
contentEl.classList.remove('typing-cursor');
// 计算统计信息
const totalTime = (Date.now() - startTime) / 1000;
const firstCharLatency = firstCharTime ? ((firstCharTime - startTime) / 1000).toFixed(2) : 0;
const charCount = response.length;
const speed = totalTime > 0 ? (charCount / totalTime).toFixed(1) : 0;
// 根据状态更新UI
if (isError) {
statusEl.innerHTML = '<i class="fa fa-times-circle text-red-500 mr-1"></i> 失败';
statusEl.classList.remove('text-primary');
statusEl.classList.add('text-red-500');
} else {
statusEl.innerHTML = `<i class="fa fa-check-circle text-green-500 mr-1"></i> 完成 <span class="text-gray-400 ml-2">${speed} 字/秒 · ${totalTime.toFixed(1)}秒 · 首字 ${firstCharLatency}秒</span>`;
statusEl.classList.remove('text-primary');
statusEl.classList.add('text-green-500');
}
// 所有模型完成时启用按钮
if (currentStreamingIntervals.length === 0) {
const btn = document.getElementById('restartBtn');
@@ -245,7 +425,7 @@
}
}
}
}, 30 + Math.random() * 40);
}, needMarkdown ? 20 : 30 + Math.random() * 40);
currentStreamingIntervals.push(interval);
}
@@ -256,13 +436,10 @@
}
// 页面加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initPage);
} else {
initPage();
}
setTimeout(() => {
initPage().catch(err => console.error('初始化失败:', err));
}, 50);
// 直接绑定按钮点击事件(不依赖 DOMContentLoaded因为页面可能通过 fetch 加载)
const restartBtn = document.getElementById('restartBtn');
if (restartBtn) {
restartBtn.onclick = restartQuestion;