2026-07-10 16:45:55 +08:00
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { computed, ref } from 'vue'
|
2026-07-24 16:30:29 +08:00
|
|
|
|
import type { PreviewItem, ResultItem } from './types'
|
2026-07-10 16:45:55 +08:00
|
|
|
|
|
|
|
|
|
|
const props = defineProps<{
|
|
|
|
|
|
items: ResultItem[]
|
2026-07-24 16:30:29 +08:00
|
|
|
|
previewItems: PreviewItem[]
|
2026-07-10 16:45:55 +08:00
|
|
|
|
selectedId: string | null
|
|
|
|
|
|
}>()
|
|
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
|
'update:selectedId': [value: string]
|
|
|
|
|
|
'update:field': [id: string, field: 'instruction' | 'input' | 'output', value: string]
|
|
|
|
|
|
'restore:item': [id: string]
|
|
|
|
|
|
}>()
|
|
|
|
|
|
|
|
|
|
|
|
const search = ref('')
|
|
|
|
|
|
const invalidOnly = ref(false)
|
|
|
|
|
|
const selectedItem = computed(() => props.items.find((item) => item.id === props.selectedId) ?? props.items[0])
|
|
|
|
|
|
const selectedIndex = computed(() => props.items.findIndex((item) => item.id === selectedItem.value?.id))
|
2026-07-24 16:30:29 +08:00
|
|
|
|
const selectedSource = computed(() => {
|
|
|
|
|
|
const previewItemId = selectedItem.value?.previewItemId
|
|
|
|
|
|
if (!previewItemId) return null
|
|
|
|
|
|
return props.previewItems.find((item) => item.id === previewItemId) ?? null
|
|
|
|
|
|
})
|
|
|
|
|
|
const selectedSourceContent = computed(() => (
|
|
|
|
|
|
selectedSource.value?.editedContent.trim()
|
|
|
|
|
|
|| selectedSource.value?.originalContent.trim()
|
|
|
|
|
|
|| ''
|
|
|
|
|
|
))
|
|
|
|
|
|
const selectedSourceWasPreprocessed = computed(() => Boolean(
|
|
|
|
|
|
selectedSource.value
|
|
|
|
|
|
&& selectedSource.value.editedContent.trim()
|
|
|
|
|
|
&& selectedSource.value.editedContent !== selectedSource.value.originalContent
|
|
|
|
|
|
))
|
|
|
|
|
|
const selectedSourceMeta = computed(() => {
|
|
|
|
|
|
const source = selectedSource.value
|
|
|
|
|
|
if (!source) return ''
|
|
|
|
|
|
const parts: string[] = []
|
|
|
|
|
|
if (source.sourceStartLine != null) {
|
|
|
|
|
|
parts.push(
|
|
|
|
|
|
source.sourceEndLine != null && source.sourceEndLine !== source.sourceStartLine
|
|
|
|
|
|
? `第 ${source.sourceStartLine}–${source.sourceEndLine} 行`
|
|
|
|
|
|
: `第 ${source.sourceStartLine} 行`,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (source.tokenCount > 0) parts.push(`${source.tokenCount} Token`)
|
|
|
|
|
|
return parts.join(' · ')
|
|
|
|
|
|
})
|
2026-07-10 16:45:55 +08:00
|
|
|
|
|
|
|
|
|
|
const filteredItems = computed(() => props.items.filter((item, index) => {
|
|
|
|
|
|
const keyword = search.value.trim().toLowerCase()
|
|
|
|
|
|
const matchesSearch = !keyword
|
|
|
|
|
|
|| item.instruction.toLowerCase().includes(keyword)
|
|
|
|
|
|
|| item.output.toLowerCase().includes(keyword)
|
|
|
|
|
|
|| String(index + 1).includes(keyword)
|
|
|
|
|
|
return matchesSearch && (!invalidOnly.value || item.status === 'invalid')
|
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
|
|
function selectRelative(offset: number) {
|
|
|
|
|
|
if (!props.items.length) return
|
|
|
|
|
|
const nextIndex = Math.min(Math.max(selectedIndex.value + offset, 0), props.items.length - 1)
|
|
|
|
|
|
emit('update:selectedId', props.items[nextIndex].id)
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<section class="result-step">
|
|
|
|
|
|
<div class="result-workspace">
|
|
|
|
|
|
<aside class="result-list-pane">
|
|
|
|
|
|
<div class="pane-header"><strong>生成结果</strong><span>共 {{ items.length }} 条</span></div>
|
|
|
|
|
|
<div class="result-toolbar">
|
|
|
|
|
|
<el-input v-model="search" clearable size="small" placeholder="搜索结果">
|
|
|
|
|
|
<template #prefix><i class="fa fa-search" /></template>
|
|
|
|
|
|
</el-input>
|
|
|
|
|
|
<el-checkbox v-model="invalidOnly">仅看错误</el-checkbox>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="result-list">
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-for="item in filteredItems"
|
|
|
|
|
|
:key="item.id"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="result-item"
|
|
|
|
|
|
:class="{ 'is-active': item.id === selectedItem?.id }"
|
|
|
|
|
|
@click="emit('update:selectedId', item.id)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span class="result-index">#{{ String(items.findIndex((entry) => entry.id === item.id) + 1).padStart(3, '0') }}</span>
|
|
|
|
|
|
<span class="result-copy">
|
|
|
|
|
|
<strong>{{ item.instruction || '未填写指令' }}</strong>
|
|
|
|
|
|
<small>{{ item.output || '未填写输出' }}</small>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<i class="fa" :class="item.status === 'invalid' ? 'fa-exclamation-circle is-error' : 'fa-check-circle is-valid'" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</aside>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="selectedItem" class="result-editor-pane">
|
|
|
|
|
|
<div class="pane-header">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<strong>结果 #{{ String(selectedIndex + 1).padStart(3, '0') }}</strong>
|
|
|
|
|
|
<span v-if="selectedItem.status === 'modified'" class="modified-label">已修改</span>
|
2026-07-23 15:10:13 +08:00
|
|
|
|
<el-tag v-if="selectedItem.split" size="small" effect="plain">{{ selectedItem.split }}</el-tag>
|
|
|
|
|
|
<el-tag
|
|
|
|
|
|
v-if="selectedItem.qualityScore != null"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
:type="selectedItem.qualityScore >= 80 ? 'success' : selectedItem.qualityScore >= 60 ? 'warning' : 'danger'"
|
|
|
|
|
|
>质量 {{ selectedItem.qualityScore.toFixed(1) }}</el-tag>
|
2026-07-10 16:45:55 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<el-button link @click="emit('restore:item', selectedItem.id)"><i class="fa fa-undo" /> 恢复生成结果</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-07-24 16:30:29 +08:00
|
|
|
|
<article class="source-reference" aria-labelledby="source-reference-title">
|
|
|
|
|
|
<div class="source-reference-heading">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<strong id="source-reference-title">原文参照</strong>
|
|
|
|
|
|
<span v-if="selectedSourceMeta">{{ selectedSourceMeta }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<el-tag v-if="selectedSourceWasPreprocessed" size="small" effect="plain">已智能预处理</el-tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<pre v-if="selectedSourceContent">{{ selectedSourceContent }}</pre>
|
|
|
|
|
|
<p v-else>当前结果没有关联到可用的原文切片。</p>
|
|
|
|
|
|
<small v-if="selectedSourceWasPreprocessed">这里展示的是实际送入模型的预处理后原文,便于核对问题和答案是否有依据。</small>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
|
2026-07-10 16:45:55 +08:00
|
|
|
|
<div class="field-editor">
|
|
|
|
|
|
<label>Instruction <em>必填</em></label>
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
:model-value="selectedItem.instruction"
|
|
|
|
|
|
type="textarea"
|
|
|
|
|
|
:rows="3"
|
|
|
|
|
|
@update:model-value="emit('update:field', selectedItem.id, 'instruction', $event)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="field-editor">
|
|
|
|
|
|
<label>Input <span>选填</span></label>
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
:model-value="selectedItem.input"
|
|
|
|
|
|
type="textarea"
|
|
|
|
|
|
:rows="2"
|
|
|
|
|
|
@update:model-value="emit('update:field', selectedItem.id, 'input', $event)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="field-editor">
|
|
|
|
|
|
<label>Output <em>必填</em></label>
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
:model-value="selectedItem.output"
|
|
|
|
|
|
type="textarea"
|
|
|
|
|
|
:rows="7"
|
|
|
|
|
|
@update:model-value="emit('update:field', selectedItem.id, 'output', $event)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-if="selectedItem.error" class="validation-error">
|
|
|
|
|
|
<i class="fa fa-exclamation-circle" /> {{ selectedItem.error }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else class="validation-success">
|
|
|
|
|
|
<i class="fa fa-check-circle" /> 字段校验通过
|
|
|
|
|
|
</div>
|
2026-07-23 15:10:13 +08:00
|
|
|
|
<div v-if="selectedItem.qualityFlags?.length" class="quality-flags">
|
|
|
|
|
|
<el-tag
|
|
|
|
|
|
v-for="flag in selectedItem.qualityFlags"
|
|
|
|
|
|
:key="flag"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
type="warning"
|
|
|
|
|
|
effect="plain"
|
|
|
|
|
|
>{{ flag }}</el-tag>
|
|
|
|
|
|
</div>
|
2026-07-10 16:45:55 +08:00
|
|
|
|
<div class="editor-pagination">
|
|
|
|
|
|
<el-button :disabled="selectedIndex <= 0" @click="selectRelative(-1)">上一条</el-button>
|
|
|
|
|
|
<span>{{ selectedIndex + 1 }} / {{ items.length }}</span>
|
|
|
|
|
|
<el-button :disabled="selectedIndex >= items.length - 1" @click="selectRelative(1)">下一条</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
|
|
|
.result-step {
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.result-workspace {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: minmax(280px, 34fr) minmax(480px, 66fr);
|
|
|
|
|
|
min-height: clamp(420px, calc(100vh - 500px), 590px);
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
border: 1px solid #e2e5ec;
|
|
|
|
|
|
border-radius: 9px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.result-list-pane {
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
border-right: 1px solid #e5e8ee;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.pane-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
min-height: 52px;
|
|
|
|
|
|
padding: 0 15px;
|
|
|
|
|
|
color: #344054;
|
2026-07-13 11:47:33 +08:00
|
|
|
|
background: #fff;
|
2026-07-10 16:45:55 +08:00
|
|
|
|
border-bottom: 1px solid #e8ebf0;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
|
|
|
|
|
|
> div {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
> span,
|
|
|
|
|
|
.modified-label {
|
|
|
|
|
|
color: #8a93a3;
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.result-toolbar {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: minmax(0, 1fr) auto;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
padding: 10px;
|
|
|
|
|
|
border-bottom: 1px solid #edf0f5;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.result-list {
|
|
|
|
|
|
height: 476px;
|
|
|
|
|
|
padding: 7px;
|
|
|
|
|
|
overflow: auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.result-item {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: 46px minmax(0, 1fr) 18px;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
min-height: 62px;
|
|
|
|
|
|
padding: 9px;
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border: 1px solid transparent;
|
|
|
|
|
|
border-bottom-color: #edf0f5;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover,
|
|
|
|
|
|
&.is-active {
|
|
|
|
|
|
background: #f7f6ff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&.is-active {
|
|
|
|
|
|
border-color: #5b50f2;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.result-index {
|
|
|
|
|
|
color: #667085;
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.result-copy {
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
|
|
|
|
|
|
strong,
|
|
|
|
|
|
small {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
strong {
|
|
|
|
|
|
color: #344054;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
small {
|
|
|
|
|
|
margin-top: 5px;
|
|
|
|
|
|
color: #98a2b3;
|
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.is-valid {
|
|
|
|
|
|
color: #2ca66a;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.is-error {
|
|
|
|
|
|
color: #d97706;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.result-editor-pane {
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-24 16:30:29 +08:00
|
|
|
|
.source-reference {
|
|
|
|
|
|
margin: 14px 18px 2px;
|
|
|
|
|
|
padding: 12px 14px;
|
|
|
|
|
|
border: 1px solid #dfe4ec;
|
|
|
|
|
|
border-radius: 7px;
|
|
|
|
|
|
background: #f8fafc;
|
|
|
|
|
|
|
|
|
|
|
|
pre {
|
|
|
|
|
|
max-height: 168px;
|
|
|
|
|
|
margin: 10px 0 0;
|
|
|
|
|
|
overflow: auto;
|
|
|
|
|
|
color: #344054;
|
|
|
|
|
|
font-family: inherit;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
line-height: 1.7;
|
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
|
word-break: break-word;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
p {
|
|
|
|
|
|
margin: 10px 0 0;
|
|
|
|
|
|
color: #98a2b3;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
> small {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
|
color: #8a93a3;
|
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.source-reference-heading {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
|
|
|
|
|
|
> div {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: baseline;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
strong {
|
|
|
|
|
|
color: #344054;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
span {
|
|
|
|
|
|
color: #8a93a3;
|
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-10 16:45:55 +08:00
|
|
|
|
.field-editor {
|
|
|
|
|
|
padding: 13px 18px 0;
|
|
|
|
|
|
|
|
|
|
|
|
label {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
margin-bottom: 7px;
|
|
|
|
|
|
color: #344054;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
font-weight: 650;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
em {
|
|
|
|
|
|
color: #e05252;
|
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
|
font-style: normal;
|
|
|
|
|
|
font-weight: 400;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
span {
|
|
|
|
|
|
color: #98a2b3;
|
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
|
font-weight: 400;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.validation-error,
|
|
|
|
|
|
.validation-success {
|
|
|
|
|
|
margin: 12px 18px 0;
|
|
|
|
|
|
padding: 9px 11px;
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-23 15:10:13 +08:00
|
|
|
|
.quality-flags {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-10 16:45:55 +08:00
|
|
|
|
.validation-error {
|
|
|
|
|
|
color: #b45309;
|
|
|
|
|
|
background: #fff7e8;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.validation-success {
|
|
|
|
|
|
color: #25895c;
|
|
|
|
|
|
background: #edf9f3;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.editor-pagination {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
padding: 12px 18px;
|
|
|
|
|
|
|
|
|
|
|
|
span {
|
|
|
|
|
|
color: #8a93a3;
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 900px) {
|
|
|
|
|
|
.result-workspace {
|
|
|
|
|
|
grid-template-columns: minmax(0, 1fr);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.result-list-pane {
|
|
|
|
|
|
border-right: 0;
|
|
|
|
|
|
border-bottom: 1px solid #e5e8ee;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.result-list {
|
|
|
|
|
|
height: 260px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|