feat: 实现业务视图页面
登录、模型调优、评测、推理、对比、模型管理、数据集、数据处理、工具、系统(硬件/日志/训练日志)等全部业务页面视图。
This commit is contained in:
225
frontend/src/views/compare/CompareResultView.vue
Normal file
225
frontend/src/views/compare/CompareResultView.vue
Normal file
@@ -0,0 +1,225 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import MarkdownView from '@/components/MarkdownView.vue'
|
||||
import {
|
||||
getCompare,
|
||||
chatWithPort,
|
||||
batchChat,
|
||||
} from '@/api/modules/compare'
|
||||
import { getModelByName } from '@/api/modules/model'
|
||||
import type { CompareTask, LoadedModel } from '@/types'
|
||||
|
||||
const route = useRoute()
|
||||
const taskId = route.query.taskId as string
|
||||
const question = decodeURIComponent(route.query.question as string || '')
|
||||
const systemPrompt = decodeURIComponent(route.query.systemPrompt as string || '')
|
||||
const temperature = Number(route.query.temperature || 0.7)
|
||||
const topP = Number(route.query.topP || 0.9)
|
||||
const topK = Number(route.query.topK || 40)
|
||||
const maxTokens = Number(route.query.maxTokens || 2048)
|
||||
|
||||
const taskName = route.query.taskName as string
|
||||
|
||||
interface ModelResult {
|
||||
name: string
|
||||
content: string
|
||||
displayContent: string
|
||||
status: 'loading' | 'done' | 'error'
|
||||
stats?: { charsPerSec?: number; totalTime?: number }
|
||||
}
|
||||
|
||||
const results = ref<ModelResult[]>([])
|
||||
const started = ref(false)
|
||||
|
||||
const loadedModels = ref<LoadedModel[]>([])
|
||||
|
||||
async function init() {
|
||||
if (started.value) return
|
||||
started.value = true
|
||||
try {
|
||||
const task: any = await getCompare(taskId)
|
||||
let models: LoadedModel[] = []
|
||||
if (task.load_status) {
|
||||
const ls = typeof task.load_status === 'string' ? JSON.parse(task.load_status) : task.load_status
|
||||
models = ls.loaded_models || []
|
||||
}
|
||||
loadedModels.value = models
|
||||
// 初始化结果占位
|
||||
results.value = models.map((m) => ({
|
||||
name: m.model_name || '模型',
|
||||
content: '',
|
||||
displayContent: '',
|
||||
status: 'loading',
|
||||
}))
|
||||
|
||||
// 并行推理
|
||||
await Promise.all(models.map((m, idx) => inferOne(m, idx)))
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
/** 单模型推理(带超时) */
|
||||
async function inferOne(model: LoadedModel, idx: number) {
|
||||
const startTime = Date.now()
|
||||
try {
|
||||
// 尝试通过端口代理调用
|
||||
const res: any = await Promise.race([
|
||||
chatWithPort({
|
||||
port: model.port,
|
||||
model_name: model.model_name,
|
||||
messages: [
|
||||
...(systemPrompt ? [{ role: 'system', content: systemPrompt }] : []),
|
||||
{ role: 'user', content: question },
|
||||
],
|
||||
temperature,
|
||||
top_p: topP,
|
||||
top_k: topK,
|
||||
max_tokens: maxTokens,
|
||||
}),
|
||||
new Promise((_, reject) => setTimeout(() => reject(new Error('推理超时')), 300000)),
|
||||
])
|
||||
|
||||
const content = res?.response || res?.content || res?.data || JSON.stringify(res)
|
||||
const totalTime = (Date.now() - startTime) / 1000
|
||||
results.value[idx].content = content
|
||||
results.value[idx].status = 'done'
|
||||
results.value[idx].stats = {
|
||||
totalTime,
|
||||
charsPerSec: totalTime > 0 ? (content.length / totalTime).toFixed(1) as unknown as number : 0,
|
||||
}
|
||||
// 模拟打字机效果
|
||||
typewriterDisplay(idx, content)
|
||||
} catch (e: any) {
|
||||
results.value[idx].content = '推理失败: ' + (e.message || '')
|
||||
results.value[idx].status = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/** 打字机效果逐字展示 */
|
||||
function typewriterDisplay(idx: number, content: string) {
|
||||
let pos = 0
|
||||
const interval = setInterval(() => {
|
||||
pos += 2
|
||||
results.value[idx].displayContent = content.slice(0, pos)
|
||||
if (pos >= content.length) {
|
||||
clearInterval(interval)
|
||||
results.value[idx].displayContent = content
|
||||
}
|
||||
}, 20)
|
||||
}
|
||||
|
||||
const allDone = computed(() => results.value.length > 0 && results.value.every((r) => r.status === 'done' || r.status === 'error'))
|
||||
|
||||
onMounted(init)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="compare-result">
|
||||
<div class="result-header">
|
||||
<h2>对比结果{{ taskName ? ` - ${taskName}` : '' }}</h2>
|
||||
<div class="header-actions">
|
||||
<el-button @click="$router.push('/model-inference')">返回列表</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 问题 -->
|
||||
<el-alert type="info" :closable="false" show-icon class="question-box">
|
||||
<template #title>
|
||||
<strong>问题:</strong>{{ question }}
|
||||
</template>
|
||||
</el-alert>
|
||||
|
||||
<!-- 模型结果网格 -->
|
||||
<div class="result-grid">
|
||||
<el-card v-for="(r, idx) in results" :key="idx" shadow="hover" class="result-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="model-name">{{ r.name }}</span>
|
||||
<el-tag v-if="r.status === 'loading'" type="warning" size="small">生成中...</el-tag>
|
||||
<el-tag v-else-if="r.status === 'done'" type="success" size="small">完成</el-tag>
|
||||
<el-tag v-else type="danger" size="small">失败</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="r.status === 'error'" class="error-text">{{ r.content }}</div>
|
||||
<MarkdownView v-else-if="r.displayContent" :content="r.displayContent" />
|
||||
<div v-else class="loading-text">
|
||||
<i class="fa fa-spinner fa-spin" /> 正在生成回答...
|
||||
</div>
|
||||
|
||||
<div v-if="r.stats" class="result-stats">
|
||||
<span>耗时 {{ r.stats.totalTime?.toFixed(1) }}s</span>
|
||||
<span>速度 {{ r.stats.charsPerSec }} 字/秒</span>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.compare-result {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.result-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
|
||||
h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.question-box {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.result-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.result-card {
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.model-name {
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-text,
|
||||
.error-text {
|
||||
color: #909399;
|
||||
min-height: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.result-stats {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #ebeef5;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user