Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,109 @@ describe('EvaluationDetail', () => {
model: 'testmodel'
})
})

it('filters evaluation table data by model search query correctly', async () => {
wrapper.vm.tableDataResult = [
{ model: 'Qwen-7B', dataset: { 'mmlu': [{ metric: 'acc', score: 75.2 }] } },
{ model: 'Llama-3-8B', dataset: { 'mmlu': [{ metric: 'acc', score: 68.4 }] } },
{ model: 'Mistral-7B', dataset: { 'mmlu': [{ metric: 'acc', score: 65.0 }] } }
]
await wrapper.vm.$nextTick()
expect(wrapper.vm.filteredTableData).toHaveLength(3)

wrapper.vm.searchQuery = 'llama'
await wrapper.vm.$nextTick()
expect(wrapper.vm.filteredTableData).toHaveLength(1)
expect(wrapper.vm.filteredTableData[0].model).toBe('Llama-3-8B')

wrapper.vm.searchQuery = 'nonexistent'
await wrapper.vm.$nextTick()
expect(wrapper.vm.filteredTableData).toHaveLength(0)

wrapper.vm.searchQuery = ''
await wrapper.vm.$nextTick()
expect(wrapper.vm.filteredTableData).toHaveLength(3)
})

it('computes availableDatasets correctly across multiple models', async () => {
wrapper.vm.tableDataResult = [
{
model: 'model1',
dataset: {
'mmlu': [{ metric: 'acc', score: 80 }],
'gsm8k': [{ metric: 'acc', score: 70 }]
}
},
{
model: 'model2',
dataset: {
'mmlu': [{ metric: 'acc', score: 85 }],
'humaneval': [{ metric: 'pass@1', score: 60 }]
}
}
]
await wrapper.vm.$nextTick()

const dsMap = wrapper.vm.availableDatasets
expect(Object.keys(dsMap)).toEqual(['mmlu', 'gsm8k', 'humaneval'])
expect(dsMap['mmlu']).toEqual([{ metric: 'acc', score: 80 }])
expect(dsMap['humaneval']).toEqual([{ metric: 'pass@1', score: 60 }])
})

it('exports evaluation matrix to CSV with UTF-8 BOM and correct headers', async () => {
wrapper.vm.tableDataResult = [
{
model: 'Qwen-7B, "Special"',
dataset: {
'mmlu': [{ metric: 'accuracy', score: 82.5 }]
}
}
]
wrapper.vm.activeName = 'all'
wrapper.vm.evaluation = { task_name: 'test_eval' }
await wrapper.vm.$nextTick()

let capturedBlobParts = null
const originalBlob = global.Blob
global.Blob = class MockBlob extends originalBlob {
constructor(parts, options) {
super(parts, options)
capturedBlobParts = parts
}
}

let downloadLink = null
const originalCreateObjectURL = window.URL.createObjectURL
const originalRevokeObjectURL = window.URL.revokeObjectURL
window.URL.createObjectURL = vi.fn(() => 'blob:mock-url')
window.URL.revokeObjectURL = vi.fn()

const clickSpy = vi.fn()
const originalCreateElement = document.createElement.bind(document)
vi.spyOn(document, 'createElement').mockImplementation((tag) => {
const el = originalCreateElement(tag)
if (tag === 'a') {
el.click = clickSpy
downloadLink = el
}
return el
})

wrapper.vm.exportCsv()

expect(window.URL.createObjectURL).toHaveBeenCalled()
expect(clickSpy).toHaveBeenCalled()
expect(downloadLink.getAttribute('download')).toBe('test_eval_all_scores.csv')

const text = capturedBlobParts[0]
expect(text.startsWith('\uFEFF')).toBe(true)
expect(text).includes('mmlu - accuracy')
expect(text).includes('"Qwen-7B, ""Special"""')
expect(text).includes('82.5')

global.Blob = originalBlob
window.URL.createObjectURL = originalCreateObjectURL
window.URL.revokeObjectURL = originalRevokeObjectURL
vi.restoreAllMocks()
})
})
133 changes: 128 additions & 5 deletions frontend/src/components/evaluations/EvaluationDetail.vue
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
>
<span :class="dataset.url ? 'dataset-item-valid' : 'dataset-item-invalid'">
<el-tooltip
:disabled="dataset.url"
:disabled="!!dataset.url"
class="datasetTooltip"
effect="dark"
:content="$t('evaluation.detail.invalidDataset')"
Expand Down Expand Up @@ -138,15 +138,38 @@
:label="tab.label"
:name="tab.name"
>
<div class="flex items-center justify-between mt-6 mb-3 flex-wrap gap-4">
<div class="flex items-center gap-3">
<el-input
v-model="searchQuery"
:placeholder="$t('evaluation.detail.searchPlaceholder')"
clearable
:prefix-icon="Search"
class="w-72"
/>
<span v-if="searchQuery" class="text-xs text-gray-500">
{{ $t('evaluation.detail.showingModels', { filtered: filteredTableData.length, total: tableDataResult.length }) }}
</span>
</div>
<CsgButton
class="btn btn-secondary-gray btn-sm flex items-center gap-1.5"
:name="$t('evaluation.detail.exportCsv')"
svgName="download"
svgPosition="left"
:disabled="!tableDataResult.length"
@click="exportCsv"
/>
</div>

<el-table
v-loading="loading"
:data="tableDataResult"
:data="filteredTableData"
:border="true"
header-row-class-name="evaluation-table-header-row"
header-cell-class-name="evaluation-table-header-cell"
row-class-name="evaluation-table-row"
cell-class-name="evaluation-table-row-cell"
class="evaluation-table mt-6 mb-10 rounded-xl"
class="evaluation-table mt-3 mb-10 rounded-xl"
>
<el-table-column
prop="model"
Expand All @@ -156,14 +179,14 @@
fixed="left"
/>
<el-table-column
v-for="(datasetData, datasetName) in tableDataResult[0]?.dataset || {}" :key="datasetName"
v-for="(datasetData, datasetName) in availableDatasets" :key="datasetName"
:label="datasetName"
header-align="center"
align="center"
>
<el-table-column
v-for="(metricItem, index) in datasetData"
:key="`${datasetName}-${metricItem}-${index}`"
:key="`${datasetName}-${metricItem.metric || metricItem}-${index}`"
:label="metricItem.metric"
:min-width="dynamicMinWidth + 'px'"
class-name="evaluation-metric-column"
Expand All @@ -180,6 +203,9 @@
</el-table-column>
<template #empty>
<span v-if="error">{{ error }}</span>
<span v-else-if="searchQuery && !filteredTableData.length">
{{ $t('evaluation.detail.noMatchingModels') }}
</span>
<span v-else>{{ $t('all.noData') }}</span>
</template>
</el-table>
Expand Down Expand Up @@ -223,9 +249,11 @@
import { ref, onMounted, computed, nextTick, onBeforeUnmount } from 'vue'
import { useI18n } from 'vue-i18n'
import { ElMessage } from 'element-plus'
import { Search } from '@element-plus/icons-vue'
import useFetchApi from '@/packs/useFetchApi'
import { formatDate } from '@/packs/datetimeUtils'
import SvgIcon from '@/components/shared/SvgIcon.vue';
import CsgButton from '@/components/shared/CsgButton.vue';

const props = defineProps({
evaluationId: {
Expand All @@ -245,6 +273,7 @@
const categories = ref([])
const evaluationSucceed = ref(true)
const isCustomDataset = ref(false)
const searchQuery = ref('')

const groupedDatasets = computed(() => {
if(isCustomDataset.value) {
Expand Down Expand Up @@ -351,6 +380,100 @@
refreshTableData(tab.props.name)
}

const filteredTableData = computed(() => {
if (!searchQuery.value?.trim()) {
return tableDataResult.value
}
const q = searchQuery.value.trim().toLowerCase()
return tableDataResult.value.filter((row) => {
return row.model && row.model.toLowerCase().includes(q)
})
})

const availableDatasets = computed(() => {
if (!tableDataResult.value?.length) return {}
const map = {}
for (const row of tableDataResult.value) {
if (row.dataset) {
for (const [dsName, metrics] of Object.entries(row.dataset)) {
if (!map[dsName]) {
map[dsName] = []
}
if (Array.isArray(metrics)) {
for (const m of metrics) {
if (!map[dsName].some((existing) => existing.metric === m.metric)) {
map[dsName].push(m)
}
}
}
}
}
}
return map
})

const exportCsv = () => {
if (!tableDataResult.value?.length) return

const headers = [t('evaluation.detail.model')]
const columnRefs = []

const dsMap = availableDatasets.value
for (const [datasetName, metrics] of Object.entries(dsMap)) {
for (const metricItem of metrics) {
const headerTitle =
activeName.value === 'all'
? `${datasetName} - ${metricItem.metric}`
: metricItem.metric
headers.push(headerTitle)
columnRefs.push({ datasetName, metric: metricItem.metric })
}
}

const escapeCsv = (val) => {
if (val === null || val === undefined) return '""'
const str = String(val)
if (
str.includes(',') ||
str.includes('"') ||
str.includes('\n') ||
str.includes('\r')
) {
return `"${str.replace(/"/g, '""')}"`
}
return `"${str}"`
}

const rows = [headers.map(escapeCsv).join(',')]

const dataToExport =
filteredTableData.value.length ? filteredTableData.value : tableDataResult.value
for (const row of dataToExport) {
const rowValues = [escapeCsv(row.model)]
for (const col of columnRefs) {
const score =
(row.dataset?.[col.datasetName] || []).find(
(m) => m.metric === col.metric
)?.score ?? 0
rowValues.push(escapeCsv(score))
}
rows.push(rowValues.join(','))
}

const csvContent = '\uFEFF' + rows.join('\r\n')
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
const taskName = evaluation.value.task_name || 'evaluation'
const tabName = activeName.value || 'all'
link.setAttribute('href', url)
link.setAttribute('download', `${taskName}_${tabName}_scores.csv`)
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
}

const fetchTags = async () => {
const { data } = await useFetchApi(
'/tags?scope=dataset&category=evaluation'
Expand Down
8 changes: 6 additions & 2 deletions frontend/src/locales/en_js/evaluation.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const evaluation = {
customDatasetTip: "Select any available dataset from the platform for configuration",
customDatasetDocumentation: "Documentation",
dataset: "Evaluation Dataset",
customDataset: "Custom Dataset",
customdataset: "Custom Dataset",
resourceType: "Resource Type",
sharedResource: "Shared Resource",
sharedResourceTip: "Shared resources may cause congestion, queueing, and longer wait times.",
Expand Down Expand Up @@ -45,7 +45,11 @@ export const evaluation = {
evaluationFailed: "Evaluation Failed",
return: "Return",
recreate: "Recreate",
invalidDataset: "The dataset has been deleted or is invalid"
invalidDataset: "The dataset has been deleted or is invalid",
searchPlaceholder: "Filter by model name...",
exportCsv: "Export CSV",
showingModels: "Showing {filtered} of {total} models",
noMatchingModels: "No matching models found"
},
list: {
title: "Model Evaluation",
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/locales/zh_hant_js/evaluation.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,11 @@ export const evaluation = {
evaluationFailed: "評測失敗",
return: "返回",
recreate: "重新創建",
invalidDataset: "數據集已刪除或失效"
invalidDataset: "數據集已刪除或失效",
searchPlaceholder: "依模型名稱過濾...",
exportCsv: "匯出 CSV",
showingModels: "顯示 {filtered} / {total} 個模型",
noMatchingModels: "未找到匹配的模型"
},
list: {
title: "模型評測",
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/locales/zh_js/evaluation.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,11 @@ export const evaluation = {
evaluationFailed: "评测失败",
return: "返回",
recreate: "重新创建",
invalidDataset: "数据集已删除或失效"
invalidDataset: "数据集已删除或失效",
searchPlaceholder: "按模型名称过滤...",
exportCsv: "导出 CSV",
showingModels: "显示 {filtered} / {total} 个模型",
noMatchingModels: "未找到匹配的模型"
},
list: {
title: "模型评测",
Expand Down