1. 模型对比页面完善,包括bug修复,支持markdown展示
This commit is contained in:
@@ -3,9 +3,11 @@ API 路由包
|
||||
"""
|
||||
from .datasets import datasets_bp
|
||||
from .model_manage import model_manage_bp
|
||||
from .model_chat import model_chat_bp
|
||||
|
||||
# 注册所有蓝图
|
||||
def register_blueprints(app):
|
||||
"""注册所有蓝图"""
|
||||
app.register_blueprint(datasets_bp)
|
||||
app.register_blueprint(model_manage_bp)
|
||||
app.register_blueprint(model_chat_bp)
|
||||
|
||||
210
src/api/model_chat.py
Normal file
210
src/api/model_chat.py
Normal file
@@ -0,0 +1,210 @@
|
||||
"""
|
||||
模型对话 API 路由
|
||||
"""
|
||||
import os
|
||||
import pymysql
|
||||
import yaml
|
||||
import requests
|
||||
import concurrent.futures
|
||||
from flask import Blueprint, request, jsonify
|
||||
|
||||
# 获取项目根目录
|
||||
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
# 创建蓝图
|
||||
model_chat_bp = Blueprint('model_chat', __name__, url_prefix='/api/model-chat')
|
||||
|
||||
|
||||
def get_db_connection():
|
||||
"""获取数据库连接"""
|
||||
CONFIG_PATH = os.path.join(PROJECT_ROOT, 'config.yaml')
|
||||
with open(CONFIG_PATH, 'r', encoding='utf-8') as f:
|
||||
CONFIG = yaml.safe_load(f)
|
||||
db_config = CONFIG['database']
|
||||
return pymysql.connect(
|
||||
host=db_config['host'],
|
||||
port=db_config['port'],
|
||||
user=db_config['username'],
|
||||
password=db_config['password'],
|
||||
database=db_config['name'],
|
||||
charset=db_config.get('charset', 'utf8mb4'),
|
||||
cursorclass=pymysql.cursors.DictCursor
|
||||
)
|
||||
|
||||
|
||||
def generic_get_by_id(table_name, id_val):
|
||||
"""按ID查询"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(f"SELECT * FROM {table_name} WHERE id = %s", (id_val,))
|
||||
result = cursor.fetchone()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
return result
|
||||
|
||||
|
||||
def call_api_model(model_config, messages, temperature, max_tokens):
|
||||
"""调用API模型(OpenAI兼容格式)"""
|
||||
api_url = model_config.get('api_url')
|
||||
api_key = model_config.get('api_key')
|
||||
model_name = model_config.get('model_name', '')
|
||||
|
||||
# 构造OpenAI兼容的完整URL
|
||||
# 支持: https://api.openai.com/v1/chat/completions 或 https://api.example.com/v1
|
||||
# 如果URL已经包含 /chat/completions 则直接使用,否则追加
|
||||
if '/chat/completions' in api_url:
|
||||
full_url = api_url
|
||||
else:
|
||||
# 去掉末尾的斜杠,然后追加 /chat/completions
|
||||
full_url = api_url.rstrip('/') + '/chat/completions'
|
||||
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': f'Bearer {api_key}'
|
||||
}
|
||||
|
||||
payload = {
|
||||
'model': model_name,
|
||||
'messages': messages,
|
||||
'temperature': temperature,
|
||||
'max_tokens': max_tokens
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(full_url, headers=headers, json=payload, timeout=120)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
if 'choices' in result and len(result['choices']) > 0:
|
||||
return {
|
||||
'success': True,
|
||||
'content': result['choices'][0]['message'].get('content', '')
|
||||
}
|
||||
return {'success': False, 'error': 'API返回格式异常'}
|
||||
except requests.exceptions.RequestException as e:
|
||||
return {'success': False, 'error': str(e)}
|
||||
|
||||
|
||||
def call_local_model(model_config, messages, temperature, max_tokens):
|
||||
"""调用本地模型(通过vLLM OpenAI兼容API)"""
|
||||
api_url = model_config.get('path') # 本地模型path字段存储API地址
|
||||
model_name = model_config.get('model_name', '')
|
||||
|
||||
if not api_url:
|
||||
return {'success': False, 'error': '本地模型API地址未配置'}
|
||||
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
|
||||
payload = {
|
||||
'model': model_name,
|
||||
'messages': messages,
|
||||
'temperature': temperature,
|
||||
'max_tokens': max_tokens
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(api_url, headers=headers, json=payload, timeout=120)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
if 'choices' in result and len(result['choices']) > 0:
|
||||
return {
|
||||
'success': True,
|
||||
'content': result['choices'][0]['message'].get('content', '')
|
||||
}
|
||||
return {'success': False, 'error': 'API返回格式异常'}
|
||||
except requests.exceptions.RequestException as e:
|
||||
return {'success': False, 'error': str(e)}
|
||||
|
||||
|
||||
@model_chat_bp.route('', methods=['POST'])
|
||||
def model_chat():
|
||||
"""模型对话接口"""
|
||||
data = request.json
|
||||
model_id = data.get('model_id')
|
||||
system_prompt = data.get('system_prompt', '')
|
||||
user_question = data.get('user_question')
|
||||
temperature = data.get('temperature', 0.7)
|
||||
max_tokens = data.get('max_tokens', 2048)
|
||||
|
||||
if not model_id:
|
||||
return jsonify({'code': 1, 'message': '缺少模型ID'})
|
||||
if not user_question:
|
||||
return jsonify({'code': 1, 'message': '缺少用户提问'})
|
||||
|
||||
# 获取模型配置
|
||||
model = generic_get_by_id('model_manage', model_id)
|
||||
if not model:
|
||||
return jsonify({'code': 1, 'message': '模型不存在'})
|
||||
|
||||
# 构建消息
|
||||
messages = []
|
||||
if system_prompt:
|
||||
messages.append({'role': 'system', 'content': system_prompt})
|
||||
messages.append({'role': 'user', 'content': user_question})
|
||||
|
||||
# 根据模型类型调用
|
||||
if model.get('model_source') == 'api':
|
||||
result = call_api_model(model, messages, temperature, max_tokens)
|
||||
else:
|
||||
result = call_local_model(model, messages, temperature, max_tokens)
|
||||
|
||||
if result.get('success'):
|
||||
return jsonify({
|
||||
'code': 0,
|
||||
'data': {
|
||||
'model_id': model_id,
|
||||
'model_name': model.get('name'),
|
||||
'response': result['content']
|
||||
}
|
||||
})
|
||||
else:
|
||||
return jsonify({'code': 1, 'message': result.get('error', '调用失败')})
|
||||
|
||||
|
||||
@model_chat_bp.route('/batch', methods=['POST'])
|
||||
def model_chat_batch():
|
||||
"""批量模型对话接口(并发调用多个模型)"""
|
||||
data = request.json
|
||||
model_ids = data.get('model_ids', [])
|
||||
system_prompt = data.get('system_prompt', '')
|
||||
user_question = data.get('user_question')
|
||||
temperature = data.get('temperature', 0.7)
|
||||
max_tokens = data.get('max_tokens', 2048)
|
||||
|
||||
if not model_ids:
|
||||
return jsonify({'code': 1, 'message': '缺少模型ID列表'})
|
||||
if not user_question:
|
||||
return jsonify({'code': 1, 'message': '缺少用户提问'})
|
||||
|
||||
def call_single_model(model_id):
|
||||
model = generic_get_by_id('model_manage', model_id)
|
||||
if not model:
|
||||
return {'model_id': model_id, 'success': False, 'error': '模型不存在'}
|
||||
|
||||
messages = []
|
||||
if system_prompt:
|
||||
messages.append({'role': 'system', 'content': system_prompt})
|
||||
messages.append({'role': 'user', 'content': user_question})
|
||||
|
||||
if model.get('model_source') == 'api':
|
||||
result = call_api_model(model, messages, temperature, max_tokens)
|
||||
else:
|
||||
result = call_local_model(model, messages, temperature, max_tokens)
|
||||
|
||||
return {
|
||||
'model_id': model_id,
|
||||
'model_name': model.get('name'),
|
||||
'success': result.get('success', False),
|
||||
'response': result.get('content', ''),
|
||||
'error': result.get('error', '')
|
||||
}
|
||||
|
||||
# 并发调用所有模型
|
||||
results = []
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=min(len(model_ids), 4)) as executor:
|
||||
future_to_model = {executor.submit(call_single_model, mid): mid for mid in model_ids}
|
||||
for future in concurrent.futures.as_completed(future_to_model):
|
||||
results.append(future.result())
|
||||
|
||||
return jsonify({'code': 0, 'data': results})
|
||||
@@ -5,6 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>模型调优 - 远光软件微调平台</title>
|
||||
<script src="../lib/tailwindcss/tailwind.js"></script>
|
||||
<script src="../lib/marked.min.js"></script>
|
||||
<link href="../lib/font-awesome/css/font-awesome.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.sidebar-section-title {
|
||||
@@ -568,8 +569,6 @@
|
||||
// 保存到 sessionStorage(当前会话)
|
||||
sessionStorage.setItem('lastPage', defaultPage);
|
||||
|
||||
console.log('Page init - URL param:', pageParam, 'saved:', sessionStorage.getItem('lastPage'), 'loading:', defaultPage);
|
||||
|
||||
loadPage(defaultPage);
|
||||
|
||||
// 更新侧边栏高亮状态
|
||||
@@ -667,15 +666,15 @@
|
||||
try {
|
||||
// 渲染页面
|
||||
if (config.isExternalPage) {
|
||||
// 外部页面,直接通过 fetch 加载 HTML
|
||||
const response = await fetch(`${pageName}.html`);
|
||||
// 外部页面,直接通过 fetch 加载 HTML(添加时间戳禁用缓存)
|
||||
const response = await fetch(`${pageName}.html?t=${Date.now()}`);
|
||||
if (response.ok) {
|
||||
const html = await response.text();
|
||||
// 提取脚本内容并执行
|
||||
const scriptMatch = html.match(/<script>([\s\S]*?)<\/script>/);
|
||||
// 只提取内联脚本内容(没有src属性的script标签)
|
||||
const scriptMatch = html.match(/<script\b(?![^>]*\bsrc)[^>]*>([\s\S]*?)<\/script>/);
|
||||
const scriptContent = scriptMatch ? scriptMatch[1] : '';
|
||||
// 移除脚本标签后插入HTML
|
||||
const htmlWithoutScript = html.replace(/<script>[\s\S]*?<\/script>/g, '');
|
||||
// 移除所有script标签后插入HTML
|
||||
const htmlWithoutScript = html.replace(/<script\b[^>]*>[\s\S]*?<\/script>/g, '');
|
||||
|
||||
// 如果有创建配置,添加 Tab 切换和创建按钮
|
||||
let headerHtml = '';
|
||||
@@ -710,7 +709,7 @@
|
||||
|
||||
container.innerHTML = headerHtml + htmlWithoutScript;
|
||||
// 执行脚本
|
||||
if (scriptContent) {
|
||||
if (scriptContent && scriptContent.trim()) {
|
||||
try {
|
||||
eval(scriptContent);
|
||||
} catch (e) {
|
||||
|
||||
@@ -230,6 +230,7 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
console.log('>>> model-compare-chat.html 脚本开始加载');
|
||||
// 动态获取 API 基础地址
|
||||
const getApiBase = () => {
|
||||
const protocol = window.location.protocol;
|
||||
@@ -263,6 +264,9 @@
|
||||
|
||||
// 页面初始化函数
|
||||
async function initPage() {
|
||||
console.log('>>> chat initPage 开始执行');
|
||||
console.log('document.readyState:', document.readyState);
|
||||
|
||||
try {
|
||||
// 获取URL参数
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
@@ -281,11 +285,10 @@
|
||||
}
|
||||
|
||||
// 立即初始化(支持通过 fetch 加载的页面)
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initPage);
|
||||
} else {
|
||||
initPage();
|
||||
}
|
||||
// 延迟执行,确保 DOM 完全准备好
|
||||
setTimeout(() => {
|
||||
initPage().catch(err => console.error('initPage 执行失败:', err));
|
||||
}, 50);
|
||||
|
||||
// 加载对比任务数据
|
||||
async function loadCompareTask() {
|
||||
@@ -406,7 +409,7 @@
|
||||
if (startBtn) startBtn.addEventListener('click', startGeneration);
|
||||
}
|
||||
|
||||
// 开始生成回答 - 跳转到结果页面
|
||||
// 开始生成回答 - 直接跳转到结果页面,由结果页面调用API
|
||||
function startGeneration() {
|
||||
const userQuestion = document.getElementById('userQuestion').value.trim();
|
||||
|
||||
@@ -419,10 +422,11 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// 跳转到结果页面
|
||||
// 直接跳转到结果页面,不在这里等待API结果
|
||||
// 结果页面会自动调用API并显示加载状态
|
||||
const taskName = compareTaskData?.model_name || '对比任务';
|
||||
const encodedQuestion = encodeURIComponent(userQuestion);
|
||||
window.location.href = `main.html?page=model-compare-result&taskId=${compareTaskId}&taskName=${encodeURIComponent(taskName)}&question=${encodedQuestion}`;
|
||||
window.location.href = `main.html?page=model-compare-result&taskId=${compareTaskId}&taskName=${encodeURIComponent(taskName)}&question=${encodedQuestion}&real=1`;
|
||||
}
|
||||
|
||||
// 返回
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -238,7 +238,7 @@
|
||||
<span class="text-sm">本地模型</span>
|
||||
</label>
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input type="radio" name="model_source" value="online" class="mr-2" onchange="toggleModelSource('online')">
|
||||
<input type="radio" name="model_source" value="api" class="mr-2" onchange="toggleModelSource('api')">
|
||||
<span class="text-sm">在线模型</span>
|
||||
</label>
|
||||
</div>
|
||||
@@ -405,8 +405,11 @@
|
||||
document.querySelector('input[name="name"]').value = model.name || '';
|
||||
document.querySelector('select[name="type"]').value = model.type || 'LLM';
|
||||
|
||||
// 设置模型来源
|
||||
const modelSource = model.model_source || 'local';
|
||||
// 设置模型来源(兼容旧的 'online' 值,转换为新的 'api' 值)
|
||||
let modelSource = model.model_source || 'local';
|
||||
if (modelSource === 'online') {
|
||||
modelSource = 'api'; // 兼容旧数据
|
||||
}
|
||||
document.querySelectorAll('input[name="model_source"]').forEach(radio => {
|
||||
radio.checked = radio.value === modelSource;
|
||||
});
|
||||
@@ -446,6 +449,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 为了兼容性,保留 online 作为别名
|
||||
window.toggleModelSource = toggleModelSource;
|
||||
|
||||
// 提交表单
|
||||
async function submitForm() {
|
||||
console.log('submitForm called');
|
||||
|
||||
Reference in New Issue
Block a user