refactor: 推理对比超时与打字机展示改进
抽取 withTimeout 替代 Promise.race 超时控制,对比结果新增打字机逐字渲染与清理,推理聊天参数与列表类型同步收敛。
This commit is contained in:
@@ -1,14 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ref, computed, onBeforeUnmount, 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'
|
||||
import { getCompare, chatWithPort } from '@/api/modules/compare'
|
||||
import type { LoadedModel } from '@/types'
|
||||
|
||||
const route = useRoute()
|
||||
const taskId = route.query.taskId as string
|
||||
@@ -25,6 +20,7 @@ interface ModelResult {
|
||||
name: string
|
||||
content: string
|
||||
displayContent: string
|
||||
isTyping: boolean
|
||||
status: 'loading' | 'done' | 'error'
|
||||
stats?: { charsPerSec?: number; totalTime?: number }
|
||||
}
|
||||
@@ -33,6 +29,7 @@ const results = ref<ModelResult[]>([])
|
||||
const started = ref(false)
|
||||
|
||||
const loadedModels = ref<LoadedModel[]>([])
|
||||
const typewriterTimers = new Set<ReturnType<typeof setInterval>>()
|
||||
|
||||
async function init() {
|
||||
if (started.value) return
|
||||
@@ -50,6 +47,7 @@ async function init() {
|
||||
name: m.model_name || '模型',
|
||||
content: '',
|
||||
displayContent: '',
|
||||
isTyping: false,
|
||||
status: 'loading',
|
||||
}))
|
||||
|
||||
@@ -65,7 +63,7 @@ async function inferOne(model: LoadedModel, idx: number) {
|
||||
const startTime = Date.now()
|
||||
try {
|
||||
// 尝试通过端口代理调用
|
||||
const res: any = await Promise.race([
|
||||
const res: any = await withTimeout(
|
||||
chatWithPort({
|
||||
port: model.port,
|
||||
model_name: model.model_name,
|
||||
@@ -78,8 +76,8 @@ async function inferOne(model: LoadedModel, idx: number) {
|
||||
top_k: topK,
|
||||
max_tokens: maxTokens,
|
||||
}),
|
||||
new Promise((_, reject) => setTimeout(() => reject(new Error('推理超时')), 300000)),
|
||||
])
|
||||
300000,
|
||||
)
|
||||
|
||||
const content = res?.response || res?.content || res?.data || JSON.stringify(res)
|
||||
const totalTime = (Date.now() - startTime) / 1000
|
||||
@@ -87,7 +85,7 @@ async function inferOne(model: LoadedModel, idx: number) {
|
||||
results.value[idx].status = 'done'
|
||||
results.value[idx].stats = {
|
||||
totalTime,
|
||||
charsPerSec: totalTime > 0 ? (content.length / totalTime).toFixed(1) as unknown as number : 0,
|
||||
charsPerSec: totalTime > 0 ? Number((content.length / totalTime).toFixed(1)) : 0,
|
||||
}
|
||||
// 模拟打字机效果
|
||||
typewriterDisplay(idx, content)
|
||||
@@ -97,22 +95,46 @@ async function inferOne(model: LoadedModel, idx: number) {
|
||||
}
|
||||
}
|
||||
|
||||
async function withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
|
||||
let timeoutId: ReturnType<typeof setTimeout> | null = null
|
||||
try {
|
||||
return await Promise.race([
|
||||
promise,
|
||||
new Promise<T>((_, reject) => {
|
||||
timeoutId = setTimeout(() => reject(new Error('推理超时')), timeoutMs)
|
||||
}),
|
||||
])
|
||||
} finally {
|
||||
if (timeoutId) clearTimeout(timeoutId)
|
||||
}
|
||||
}
|
||||
|
||||
/** 打字机效果逐字展示 */
|
||||
function typewriterDisplay(idx: number, content: string) {
|
||||
let pos = 0
|
||||
results.value[idx].isTyping = true
|
||||
// 将更新次数控制在约 30 次,避免长回答逐字触发 Markdown 全文解析。
|
||||
const step = Math.max(2, Math.ceil(content.length / 30))
|
||||
const interval = setInterval(() => {
|
||||
pos += 2
|
||||
pos += step
|
||||
results.value[idx].displayContent = content.slice(0, pos)
|
||||
if (pos >= content.length) {
|
||||
clearInterval(interval)
|
||||
typewriterTimers.delete(interval)
|
||||
results.value[idx].displayContent = content
|
||||
results.value[idx].isTyping = false
|
||||
}
|
||||
}, 20)
|
||||
}, 50)
|
||||
typewriterTimers.add(interval)
|
||||
}
|
||||
|
||||
const allDone = computed(() => results.value.length > 0 && results.value.every((r) => r.status === 'done' || r.status === 'error'))
|
||||
|
||||
onMounted(init)
|
||||
onBeforeUnmount(() => {
|
||||
typewriterTimers.forEach(clearInterval)
|
||||
typewriterTimers.clear()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -144,6 +166,7 @@ onMounted(init)
|
||||
</template>
|
||||
|
||||
<div v-if="r.status === 'error'" class="error-text">{{ r.content }}</div>
|
||||
<div v-else-if="r.isTyping" class="streaming-text">{{ r.displayContent }}</div>
|
||||
<MarkdownView v-else-if="r.displayContent" :content="r.displayContent" />
|
||||
<div v-else class="loading-text">
|
||||
<i class="fa fa-spinner fa-spin" /> 正在生成回答...
|
||||
@@ -212,6 +235,13 @@ onMounted(init)
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.streaming-text {
|
||||
min-height: 80px;
|
||||
line-height: 1.7;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.result-stats {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
|
||||
Reference in New Issue
Block a user