<template> <div> <div class="_display:flex _margin-bottom:4"> <div class="_display:flex _align-items:center _margin-left:auto"> <p class="_margin-right:2">{{ $t('sort_by') }}:</p> <i-select v-model="sortBy" :options="sortOptions" label="label" idField="value" placeholder="Choose something.." @update:modelValue="onChange($event.value)" /> </div> </div> <i-card v-for="(item, i) in list" :key="i" class="_margin-bottom:5 shadow-3 border-gray-200"> <template #header> <div class="_display:flex _align-items:center"> <h3 class="_font-size:lg _font-weight:bold">{{ item.metadata.gt_workspace.label }}</h3> <span class="_font-weight:semibold _color:gray-60 _margin-left:2 _display:flex _align-items:center" :title="$t('number_of_pages')"> <Icon class="_margin-right:1/2" name="file-text" size="md"></Icon> {{ item.metadata.document_metadata.data_properties.number_of_pages }} </span> <span class="_display:flex _align-items:center _margin-left:auto _border-color:gray-40 _color:gray-50 _border-width:2 _border-radius _font-size:sm _padding-x:1" v-if="item.isFastest" > <Icon class="_margin-right:1" name="clock" size="md"></Icon> {{ $t('fastest') }} </span> </div> </template> <template #default> <div class="_display:flex _font-size:sm _padding-bottom:1 _color:gray-60"> <span class="_font-weight:semibold">{{ $t('publication_year') }}: </span> <span class="_margin-left:1/2"> {{ item.metadata.document_metadata.data_properties.publication_year }}</span> <span class="_margin-left:2 _font-weight:semibold">{{ $t('layout') }}: </span> <span class="_margin-left:1/2">{{ item.metadata.document_metadata.data_properties.layout }}</span> </div> <i-row class=""> <i-column xs="5"> <div class="_display:flex _align-items:flex-start _margin-top:1"> <div class="_font-weight:semibold _margin-top:1">{{ $t('workflow') }}:</div> <div class="_flex-grow:1 _margin-left:2"> <i-collapsible size="md" class="_font-size:sm _flex-grow:1"> <i-collapsible-item :title="item.metadata.ocr_workflow?.label || $t('unknown_workflow')"> <div class="_display:flex _flex-direction:column" v-if="item.metadata.workflow_steps"> <div class="_margin-bottom:2" v-for="({id, url, params }, i) in item.metadata.workflow_steps" :key="id"> <div class="_display:flex _align-items:center"> <span class="_margin-right:1">{{ i + 1 }}. </span> <a v-if="url" :href="url" target="_blank" :title="$t('external_repo_url')" class="_display:flex _align-items:center _flex-shrink:0"> <span class="_font-weight:semibold _font-size:md">{{ id }}</span> <Icon class="_margin-left:1/3" name="external-link"></Icon> </a></div> <div class="_display:flex _flex-wrap:wrap _align-items:flex-start _margin-top:1"> <div v-for="{ name, value } in params" :key="name" class="_margin-bottom:1 _margin-right:1 _display:flex _align-items:flex-start" style="line-height:1.2" > <span class="_border-top-left-radius _border-bottom-left-radius _background:gray-20 _color:gray-70 _font-weight:semibold _padding-x:1 _padding-y:1/2">{{name }}</span> <span class="_border-top-right-radius _border-bottom-right-radius _background:gray-10 _color:gray-70 _padding-x:1 _padding-y:1/2">{{value }}</span> </div> </div> </div> </div> <template v-else> <span class="_font-weight:bold">{{ $t('no_ocr_workflow') }}</span> </template> </i-collapsible-item> </i-collapsible> </div> </div> </i-column> <i-column xs="7" class="_margin-left:auto"> <i-row class="_align-items:flex-end" style="margin-top: -16px"> <i-column v-for="({ name, value }, i) in item.evaluations" :key="i" class="_display:flex _flex-direction:column _align-items:center _padding-x:1/2"> <span class="_font-weight:bold _font-size:xs _margin-bottom:1">{{ defs[name] ? defs[name].label : name }}</span> <i-badge size="lg" class="metric _cursor:pointer _padding-x:1" :class="getEvalColor(name, value)" :title="value"> {{ createReadableMetricValue(name, value) }} </i-badge> </i-column> </i-row> </i-column> </i-row> </template> </i-card> </div> </template> <script setup> import { ref, onMounted, watch } from "vue"; import { useI18n } from "vue-i18n"; import { store } from "@/helpers/store"; import { createReadableMetricValue, getEvalColor } from "@/helpers/utils"; import Icon from "@/components/Icon.vue"; const props = defineProps(['data', 'defs']); const list = ref([]); const evals = ref([]); const { t } = useI18n(); const sortOptions = ref([ { value: 'none', label: t('-') }, { value: 'wall_time_asc', label: t('wall_time_asc') }, { value: 'wall_time_desc', label: t('wall_time_desc') }, { value: 'cer_asc', label: t('cer_asc') }, { value: 'cer_desc', label: t('cer_desc') }, { value: 'cer_min_asc', label: t('cer_min_asc') }, { value: 'cer_min_desc', label: t('cer_min_desc') }, { value: 'cer_max_asc', label: t('cer_max_asc') }, { value: 'cer_max_desc', label: t('cer_max_desc') } ]); const sortBy = ref(sortOptions.value[0]); const onChange = (value) => { if (value === 'wall_time_asc') sortByWallTime('asc'); else if (value === 'wall_time_desc') sortByWallTime('desc'); else if (value === 'cer_asc') sortByCER('asc'); else if (value === 'cer_desc') sortByCER('desc'); else if (value === 'cer_min_asc') sortByCERMin('asc'); else if (value === 'cer_min_desc') sortByCERMin('desc'); else if (value === 'cer_max_asc') sortByCERMax('asc'); else if (value === 'cer_max_desc') sortByCERMax('desc'); }; const sortByWallTime = (order = 'asc') => { list.value.sort((a, b) => { const wallTimeA = a.evaluations.find(e => e.name === 'wall_time')?.value || 0; const wallTimeB = b.evaluations.find(e => e.name === 'wall_time')?.value || 0; if (order === 'asc') return wallTimeA > wallTimeB ? 1 : -1; if (order === 'desc') return wallTimeA < wallTimeB ? 1 : -1; return 0; }); }; const sortByCER = (order = 'asc') => { list.value.sort((a, b) => { const cerA = a.evaluations.find(e => e.name === 'cer')?.value || 0; const cerB = b.evaluations.find(e => e.name === 'cer')?.value || 0; if (order === 'asc') return cerA > cerB ? 1 : -1; if (order === 'desc') return cerA < cerB ? 1 : -1; return 0; }); }; const sortByCERMin = (order = 'asc') => { list.value.sort((a, b) => { const cerMinA = a.evaluations.find(e => e.name === 'cer_min_max')?.value[0] || 0; const cerMinB = b.evaluations.find(e => e.name === 'cer_min_max')?.value[0] || 0; if (order === 'asc') return cerMinA > cerMinB ? 1 : -1; if (order === 'desc') return cerMinA < cerMinB ? 1 : -1; return 0; }); }; const sortByCERMax = (order = 'asc') => { list.value.sort((a, b) => { const cerMaxA = a.evaluations.find(e => e.name === 'cer_min_max')?.value[1] || 0; const cerMaxB = b.evaluations.find(e => e.name === 'cer_min_max')?.value[1] || 0; if (order === 'asc') return cerMaxA > cerMaxB ? 1 : -1; if (order === 'desc') return cerMaxA < cerMaxB ? 1 : -1; return 0; }); }; const mapMetadata = ({ workflow_model = t('no_workflow_model'), document_metadata = { fonts: [] }, gt_workspace = null, ocr_workflow = null, workflow_steps = {} }) => { workflow_steps = workflow_steps .map(step => { const id = step.id; const params = Object.keys(step.params).map(paramKey => ({ name: paramKey, value: step.params[paramKey] })); return { id, url: getRepoUrl(id), params }; }); return { workflow_model, document_metadata, gt_workspace, ocr_workflow, workflow_steps }; }; const mapEvaluationResults = ({ document_wide = [] }) => { return Object.keys(document_wide).map(key => ({ name: key, value: document_wide[key] })); }; const setListData = (data) => { const fastest = data.reduce( (prev, curr) => prev.evaluation_results.document_wide.wall_time < curr.evaluation_results.document_wide.wall_time ? prev : curr ); list.value = data.map(({ eval_workflow_id, label, evaluation_results = {}, metadata }) => { return { label, metadata: mapMetadata(metadata), evaluations: mapEvaluationResults(evaluation_results), isFastest: fastest.eval_workflow_id === eval_workflow_id }; }); }; const getRepoUrl = (needleId) => { const repo = store.repos.find(({ ocrd_tool }) => { return ocrd_tool && ocrd_tool.tools[needleId]; }); if (!repo) return null; return repo.url; }; const setEvals = (data) => { evals.value = data && data.length > 0 && data[0].evaluation_results ? Object.keys(data[0].evaluation_results.document_wide) || [] : []; }; onMounted(() => { setEvals(props.data); setListData(props.data); }); watch(() => props.data, () => { setEvals(props.data); setListData(props.data); }); // watch(() => props.defs, () => { // if (props.defs['cer_standard_deviation']) { // props.defs['cer_standard_deviation'].label = 'CER (std. deviation)'; // } // }); </script> <style scoped lang="scss"> .metric { --border-radius: 8px; } .card { ----header--padding-left: 1rem; ----header--padding-right: 1rem; ----body--padding-top: 0.5rem; ----body--padding-bottom: 1.5rem; ----body--padding-left: 1rem; ----body--padding-right: 1rem; } </style>