mirror of https://github.com/doccano/doccano.git
4 changed files with 450 additions and 316 deletions
Split View
Diff Options
-
64frontend/components/discrepancy/DiscrepancyList.vue
-
9frontend/components/layout/TheSideBar.vue
-
439frontend/pages/projects/_id/compare-annotations/index.vue
-
254frontend/pages/projects/_id/compare.vue
@ -0,0 +1,439 @@ |
|||
<template> |
|||
<v-card> |
|||
<v-card-title> |
|||
<h2>Compare Annotations</h2> |
|||
<v-spacer /> |
|||
</v-card-title> |
|||
|
|||
<v-card-text> |
|||
<!-- Filtro para seleção de usuários --> |
|||
<v-row class="mb-4"> |
|||
<v-col cols="12" md="6"> |
|||
<v-select |
|||
v-model="selectedUsers" |
|||
:items="availableUsers" |
|||
item-text="username" |
|||
item-value="id" |
|||
label="Select Users to Compare" |
|||
multiple |
|||
chips |
|||
small-chips |
|||
deletable-chips |
|||
outlined |
|||
:rules="[v => !v || v.length >= 2 || 'Select at least 2 users', v => !v || v.length <= 5 || 'Select maximum 5 users']" |
|||
> |
|||
<template #selection="{ item, index }"> |
|||
<v-chip |
|||
v-if="index < 3" |
|||
:key="item.id" |
|||
color="primary" |
|||
small |
|||
close |
|||
@click:close="removeUser(item.id)" |
|||
> |
|||
{{ item.username }} |
|||
</v-chip> |
|||
<span |
|||
v-if="index === 3" |
|||
:key="item.id" |
|||
class="grey--text caption" |
|||
> |
|||
(+{{ selectedUsers.length - 3 }} others) |
|||
</span> |
|||
</template> |
|||
</v-select> |
|||
</v-col> |
|||
<v-col cols="12" md="6"> |
|||
<v-text-field |
|||
v-model="search" |
|||
prepend-inner-icon="mdi-magnify" |
|||
label="Search text..." |
|||
outlined |
|||
hide-details |
|||
clearable |
|||
/> |
|||
</v-col> |
|||
</v-row> |
|||
|
|||
<!-- Tabela de comparação --> |
|||
<v-data-table |
|||
:items="processedItems" |
|||
:headers="dynamicHeaders" |
|||
:loading="isLoading" |
|||
:search="search" |
|||
loading-text="Loading comparison data..." |
|||
:no-data-text="noDataMessage" |
|||
:footer-props="{ |
|||
showFirstLastPage: true, |
|||
'items-per-page-options': [10, 25, 50, 100], |
|||
'items-per-page-text': 'Items per page:', |
|||
'page-text': '{0}-{1} of {2}' |
|||
}" |
|||
item-key="id" |
|||
:item-class="getRowClass" |
|||
> |
|||
<!-- Template para texto (primeira coluna) --> |
|||
<template #[`item.text`]="{ item }"> |
|||
<div class="text-truncate" style="max-width: 300px;"> |
|||
<v-tooltip bottom> |
|||
<template #activator="{ on, attrs }"> |
|||
<span v-bind="attrs" v-on="on"> |
|||
{{ truncateText(item.text, 100) }} |
|||
</span> |
|||
</template> |
|||
<span>{{ item.text }}</span> |
|||
</v-tooltip> |
|||
</div> |
|||
</template> |
|||
|
|||
<!-- Templates dinâmicos para colunas de usuários --> |
|||
<template v-for="user in selectedUsers" #[`item.user_${user}`]="{ item }"> |
|||
<div :key="`user_${user}`"> |
|||
<v-chip |
|||
v-for="label in item.userAnnotations[user] || []" |
|||
:key="`${user}_${label}`" |
|||
small |
|||
:color="getLabelColor(label)" |
|||
text-color="white" |
|||
class="ma-1" |
|||
> |
|||
{{ label }} |
|||
</v-chip> |
|||
<span v-if="!item.userAnnotations[user] || item.userAnnotations[user].length === 0" class="grey--text"> |
|||
No annotation |
|||
</span> |
|||
</div> |
|||
</template> |
|||
|
|||
<!-- Template para coluna de discrepância --> |
|||
<template #[`item.discrepancy`]="{ item }"> |
|||
<v-chip |
|||
:color="getDiscrepancyColor(item.discrepancyStatus)" |
|||
text-color="white" |
|||
small |
|||
> |
|||
<v-icon left small> |
|||
{{ getDiscrepancyIcon(item.discrepancyStatus) }} |
|||
</v-icon> |
|||
{{ getDiscrepancyText(item.discrepancyStatus) }} |
|||
</v-chip> |
|||
</template> |
|||
</v-data-table> |
|||
</v-card-text> |
|||
</v-card> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import Vue from 'vue' |
|||
import { mapGetters } from 'vuex' |
|||
|
|||
type UserAnnotation = { |
|||
id: number |
|||
username: string |
|||
} |
|||
|
|||
type ExampleItem = { |
|||
id: number |
|||
text: string |
|||
userAnnotations: { [userId: number]: string[] } |
|||
hasDiscrepancy: boolean |
|||
discrepancyStatus: 'agreement' | 'discrepancy' | 'no_annotations' |
|||
} |
|||
|
|||
export default Vue.extend({ |
|||
name: 'CompareAnnotations', |
|||
|
|||
layout: 'project', |
|||
middleware: ['check-auth', 'auth', 'setCurrentProject'], |
|||
|
|||
data() { |
|||
return { |
|||
isLoading: true, |
|||
selectedUsers: [] as number[], |
|||
availableUsers: [] as UserAnnotation[], |
|||
examples: [] as any[], |
|||
search: '' as string, |
|||
labelColors: {} as { [label: string]: string } |
|||
} |
|||
}, |
|||
|
|||
async fetch() { |
|||
this.isLoading = true |
|||
try { |
|||
const projectId = this.$route.params.id |
|||
|
|||
// Buscar membros do projeto |
|||
const membersResponse = await this.$repositories.member.list(projectId) |
|||
this.availableUsers = membersResponse.map((member: any) => ({ |
|||
id: member.user, |
|||
username: member.username |
|||
})) |
|||
|
|||
// Buscar exemplos com anotações |
|||
await this.loadExamples() |
|||
|
|||
// Auto-selecionar os primeiros 2 usuários se disponíveis (para facilitar teste) |
|||
if (this.availableUsers.length >= 2) { |
|||
this.selectedUsers = [this.availableUsers[0].id, this.availableUsers[1].id] |
|||
console.log('🎯 Auto-selected users:', this.selectedUsers) |
|||
} |
|||
|
|||
console.log('✅ Loaded', this.availableUsers.length, 'users and', this.examples.length, 'examples') |
|||
|
|||
} catch (error) { |
|||
console.error('Error loading comparison data:', error) |
|||
} finally { |
|||
this.isLoading = false |
|||
} |
|||
}, |
|||
|
|||
computed: { |
|||
...mapGetters('projects', ['project']), |
|||
|
|||
dynamicHeaders() { |
|||
const headers = [ |
|||
{ text: 'Text', value: 'text', sortable: true, width: '300px' } |
|||
] |
|||
|
|||
// Adicionar colunas para cada usuário selecionado |
|||
this.selectedUsers.forEach(userId => { |
|||
const user = this.availableUsers.find(u => u.id === userId) |
|||
if (user) { |
|||
headers.push({ |
|||
text: user.username, |
|||
value: `user_${userId}`, |
|||
sortable: false, |
|||
width: '200px' |
|||
}) |
|||
} |
|||
}) |
|||
|
|||
// Adicionar coluna de discrepância |
|||
if (this.selectedUsers.length >= 2) { |
|||
headers.push({ |
|||
text: 'Consensus', |
|||
value: 'discrepancy', |
|||
sortable: true, |
|||
width: '150px' |
|||
}) |
|||
} |
|||
|
|||
return headers |
|||
}, |
|||
|
|||
processedItems(): ExampleItem[] { |
|||
if (this.selectedUsers.length < 2) { |
|||
return [] |
|||
} |
|||
|
|||
const processed = this.examples |
|||
.map(example => { |
|||
const userAnnotations: { [userId: number]: string[] } = {} |
|||
|
|||
// Processar anotações por usuário |
|||
this.selectedUsers.forEach(userId => { |
|||
userAnnotations[userId] = this.getUserAnnotations(example, userId) |
|||
}) |
|||
|
|||
// Verificar se algum usuário selecionado fez anotações |
|||
const hasAnyAnnotations = this.selectedUsers.some(userId => |
|||
userAnnotations[userId] && userAnnotations[userId].length > 0 |
|||
) |
|||
|
|||
// Se nenhum usuário selecionado anotou, pular este exemplo |
|||
if (!hasAnyAnnotations) { |
|||
return null |
|||
} |
|||
|
|||
// Verificar discrepâncias e determinar status |
|||
const discrepancyResult = this.checkDiscrepancyWithStatus(userAnnotations) |
|||
|
|||
return { |
|||
id: example.id, |
|||
text: example.text, |
|||
userAnnotations, |
|||
hasDiscrepancy: discrepancyResult.hasDiscrepancy, |
|||
discrepancyStatus: discrepancyResult.status |
|||
} |
|||
}) |
|||
.filter(item => item !== null) as ExampleItem[] // Remover itens nulos |
|||
|
|||
console.log('✅ Processed', processed.length, 'examples with relevant annotations for', this.selectedUsers.length, 'users') |
|||
return processed |
|||
}, |
|||
|
|||
noDataMessage() { |
|||
if (this.selectedUsers.length < 2) { |
|||
return 'Please select at least 2 users to compare annotations' |
|||
} |
|||
if (this.examples.length === 0) { |
|||
return 'No examples found in this project' |
|||
} |
|||
return 'No examples found where the selected users have made annotations. Try selecting different users or check if they have annotated any examples.' |
|||
} |
|||
}, |
|||
|
|||
methods: { |
|||
async loadExamples() { |
|||
try { |
|||
const projectId = this.$route.params.id |
|||
|
|||
// Usar a mesma abordagem EXATA da página de discrepâncias |
|||
const response = await this.$repositories.example.list(projectId, { |
|||
limit: '1000', |
|||
offset: '0', |
|||
include_annotation: 'true' // CHAVE PARA INCLUIR ANOTAÇÕES! |
|||
}) |
|||
|
|||
// Processar da mesma forma que a página de discrepâncias |
|||
this.examples = response.items.map((item: any) => { |
|||
return { |
|||
id: item.id, |
|||
text: item.text, |
|||
assignments: item.assignments || [], |
|||
annotations: (item.annotations || []).map((a: any) => ({ |
|||
user: a.user ?? a.user_id ?? a.created_by, |
|||
label: a.label, |
|||
start_offset: a.start_offset, |
|||
end_offset: a.end_offset, |
|||
text: a.text, |
|||
type: a.type |
|||
})) |
|||
} |
|||
}) |
|||
|
|||
console.log('📄 Loaded', this.examples.length, 'examples with annotations') |
|||
|
|||
} catch (error) { |
|||
console.error('Error loading examples:', error) |
|||
this.examples = [] |
|||
} |
|||
}, |
|||
|
|||
getUserAnnotations(example: any, userId: number): string[] { |
|||
if (!example.annotations) { |
|||
return [] |
|||
} |
|||
|
|||
const userAnnotations = example.annotations |
|||
.filter((annotation: any) => { |
|||
const annotationUserId = annotation.user || annotation.user_id || annotation.created_by |
|||
return annotationUserId === userId |
|||
}) |
|||
.map((annotation: any) => annotation.label || 'Unlabeled') |
|||
.filter((label: string, index: number, self: string[]) => self.indexOf(label) === index) // Remove duplicates |
|||
|
|||
return userAnnotations |
|||
}, |
|||
|
|||
checkDiscrepancyWithStatus(userAnnotations: { [userId: number]: string[] }): { hasDiscrepancy: boolean, status: 'agreement' | 'discrepancy' | 'no_annotations' } { |
|||
const userIds = Object.keys(userAnnotations).map(id => parseInt(id)) |
|||
|
|||
// Verificar quantos usuários têm anotações |
|||
const usersWithAnnotations = userIds.filter(userId => |
|||
userAnnotations[userId] && userAnnotations[userId].length > 0 |
|||
) |
|||
|
|||
// Se menos de 2 usuários anotaram, é neutro |
|||
if (usersWithAnnotations.length < 2) { |
|||
return { hasDiscrepancy: false, status: 'no_annotations' } |
|||
} |
|||
|
|||
// Comparar as anotações dos usuários que anotaram |
|||
const firstUserAnnotations = userAnnotations[usersWithAnnotations[0]] |
|||
|
|||
for (let i = 1; i < usersWithAnnotations.length; i++) { |
|||
const currentUserAnnotations = userAnnotations[usersWithAnnotations[i]] |
|||
|
|||
// Verificar se as anotações são diferentes |
|||
if (!this.arraysEqual(firstUserAnnotations, currentUserAnnotations)) { |
|||
return { hasDiscrepancy: true, status: 'discrepancy' } |
|||
} |
|||
} |
|||
|
|||
return { hasDiscrepancy: false, status: 'agreement' } |
|||
}, |
|||
|
|||
arraysEqual(arr1: string[], arr2: string[]): boolean { |
|||
if (arr1.length !== arr2.length) return false |
|||
|
|||
const sorted1 = [...arr1].sort() |
|||
const sorted2 = [...arr2].sort() |
|||
|
|||
return sorted1.every((val, index) => val === sorted2[index]) |
|||
}, |
|||
|
|||
removeUser(userId: number) { |
|||
this.selectedUsers = this.selectedUsers.filter(id => id !== userId) |
|||
}, |
|||
|
|||
truncateText(text: string, length: number): string { |
|||
if (!text) return '' |
|||
return text.length > length ? text.substring(0, length) + '...' : text |
|||
}, |
|||
|
|||
getLabelColor(label: string): string { |
|||
if (!this.labelColors[label]) { |
|||
// Gerar uma cor consistente para cada label |
|||
const colors = ['primary', 'secondary', 'accent', 'info', 'warning', 'error', 'success'] |
|||
const hash = this.hashString(label) |
|||
this.labelColors[label] = colors[hash % colors.length] |
|||
} |
|||
return this.labelColors[label] |
|||
}, |
|||
|
|||
hashString(str: string): number { |
|||
let hash = 0 |
|||
for (let i = 0; i < str.length; i++) { |
|||
const char = str.charCodeAt(i) |
|||
hash = ((hash << 5) - hash) + char |
|||
hash = hash & hash // Convert to 32bit integer |
|||
} |
|||
return Math.abs(hash) |
|||
}, |
|||
|
|||
getRowClass(item: ExampleItem): string { |
|||
return item.hasDiscrepancy ? 'discrepancy-row' : '' |
|||
}, |
|||
|
|||
getDiscrepancyColor(status: 'agreement' | 'discrepancy' | 'no_annotations'): string { |
|||
switch (status) { |
|||
case 'agreement': return 'success' |
|||
case 'discrepancy': return 'error' |
|||
case 'no_annotations': return 'grey' |
|||
default: return 'grey' |
|||
} |
|||
}, |
|||
|
|||
getDiscrepancyIcon(status: 'agreement' | 'discrepancy' | 'no_annotations'): string { |
|||
switch (status) { |
|||
case 'agreement': return 'mdi-check-circle' |
|||
case 'discrepancy': return 'mdi-alert-circle' |
|||
case 'no_annotations': return 'mdi-minus-circle' |
|||
default: return 'mdi-minus-circle' |
|||
} |
|||
}, |
|||
|
|||
getDiscrepancyText(status: 'agreement' | 'discrepancy' | 'no_annotations'): string { |
|||
switch (status) { |
|||
case 'agreement': return 'Agreement' |
|||
case 'discrepancy': return 'Disagreement' |
|||
case 'no_annotations': return 'Insufficient Data' |
|||
default: return 'Unknown' |
|||
} |
|||
} |
|||
} |
|||
}) |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.discrepancy-row { |
|||
background-color: #fff3e0 !important; |
|||
} |
|||
|
|||
.text-truncate { |
|||
white-space: nowrap; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
} |
|||
</style> |
@ -1,254 +0,0 @@ |
|||
<template> |
|||
<v-card> |
|||
<v-card-title> |
|||
<h2>Comparação de Anotações</h2> |
|||
<v-spacer /> |
|||
</v-card-title> |
|||
<v-card-text> |
|||
<div v-if="isLoading">Carregando dados para comparação...</div> |
|||
<div v-else-if="!documentText">Não foi possível carregar o documento ou anotações.</div> |
|||
<div v-else> |
|||
<div class="mb-4"> |
|||
<h3>Legenda:</h3> |
|||
<div class="d-flex flex-wrap"> |
|||
<div |
|||
v-for="labelName in uniqueLabelsForLegend" |
|||
:key="labelName" |
|||
:style="{ backgroundColor: getLabelColor(labelName), color: 'black', padding: '4px 8px', margin: '4px', borderRadius: '4px' }" |
|||
> |
|||
{{ labelName }} |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<v-row> |
|||
<v-col cols="12" md="6"> |
|||
<h4>Anotações do {{ user1Name }}:</h4> |
|||
<div v-if="user1Annotations.length === 0">Nenhuma anotação encontrada para este usuário.</div> |
|||
<div v-else> |
|||
<AnnotatedText :text="documentText" :annotations="user1Annotations" :label-colors="labelColorsDict" /> |
|||
</div> |
|||
</v-col> |
|||
<v-col cols="12" md="6"> |
|||
<h4>Anotações do {{ user2Name }}:</h4> |
|||
<div v-if="user2Annotations.length === 0">Nenhuma anotação encontrada para este usuário.</div> |
|||
<div v-else> |
|||
<AnnotatedText :text="documentText" :annotations="user2Annotations" :label-colors="labelColorsDict" /> |
|||
</div> |
|||
</v-col> |
|||
</v-row> |
|||
</div> |
|||
</v-card-text> |
|||
</v-card> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import Vue from 'vue' |
|||
import { mapGetters } from 'vuex' |
|||
import AnnotatedText from '@/components/AnnotatedText.vue' |
|||
|
|||
// Tipos para anotações de span |
|||
type SpanAnnotation = { |
|||
id?: number |
|||
prob?: number |
|||
user: number |
|||
example?: number |
|||
created_at?: string |
|||
updated_at?: string |
|||
label: string; // Corrigido para string |
|||
start_offset: number |
|||
end_offset: number |
|||
} |
|||
|
|||
// Tipo para Label |
|||
// type Label = { |
|||
// id: number |
|||
// text: string |
|||
// shortcut: string | null |
|||
// background_color: string |
|||
// text_color: string |
|||
// } |
|||
|
|||
export default Vue.extend({ |
|||
name: 'AnnotationCompare', |
|||
|
|||
components: { |
|||
AnnotatedText |
|||
}, |
|||
|
|||
layout: 'project', |
|||
middleware: ['check-auth', 'auth', 'setCurrentProject'], |
|||
|
|||
data() { |
|||
return { |
|||
isLoading: true, |
|||
projectId: '' as string, |
|||
exampleId: '' as string, |
|||
user1Id: '' as string, |
|||
user2Id: '' as string, |
|||
documentText: '' as string, |
|||
user1Annotations: [] as SpanAnnotation[], |
|||
user2Annotations: [] as SpanAnnotation[], |
|||
user1Name: '' as string, |
|||
user2Name: '' as string, |
|||
discrepancyReport: null as string | null, |
|||
labelColors: {} as { [key: string]: string }, |
|||
} |
|||
}, |
|||
|
|||
async fetch() { |
|||
this.isLoading = true |
|||
try { |
|||
this.projectId = this.$route.params.id |
|||
this.exampleId = this.$route.query.exampleId as string |
|||
this.user1Id = this.$route.query.user1Id as string |
|||
this.user2Id = this.$route.query.user2Id as string |
|||
|
|||
if (!this.projectId || !this.exampleId || !this.user1Id || !this.user2Id) { |
|||
console.error('Parâmetros de rota ausentes.') |
|||
this.isLoading = false |
|||
return |
|||
} |
|||
|
|||
console.log('this.$repositories:', this.$repositories); |
|||
const [exampleResponse, membersResponse] = await Promise.all([ |
|||
this.$repositories.example.findById(this.projectId, parseInt(this.exampleId)), |
|||
this.$repositories.member.list(this.projectId), |
|||
]); |
|||
|
|||
this.documentText = exampleResponse.text |
|||
|
|||
// As anotações (spans) já vêm na resposta do documento |
|||
// Não é mais necessário mapear label ID para nome, pois já vem como string. |
|||
const initialUser1Annotations = exampleResponse.annotations.filter((annotation: any) => annotation.user.toString() === this.user1Id); |
|||
const initialUser2Annotations = exampleResponse.annotations.filter((annotation: any) => annotation.user.toString() === this.user2Id); |
|||
|
|||
// Marcar discrepâncias nos spans |
|||
const { markedAnnotations1, markedAnnotations2 } = this.markDiscrepantSpans( |
|||
initialUser1Annotations, |
|||
initialUser2Annotations |
|||
); |
|||
this.user1Annotations = markedAnnotations1; |
|||
this.user2Annotations = markedAnnotations2; |
|||
|
|||
const user1Member = membersResponse.find((m: any) => m.user.toString() === this.user1Id); |
|||
const user2Member = membersResponse.find((m: any) => m.user.toString() === this.user2Id); |
|||
this.user1Name = user1Member ? user1Member.username : `Usuário ${this.user1Id}`; |
|||
this.user2Name = user2Member ? user2Member.username : `Usuário ${this.user2Id}`; |
|||
|
|||
// this.discrepancyReport = this.compareAnnotations(this.user1Annotations, this.user2Annotations); // Removido: não exibe mais o relatório textual |
|||
|
|||
} catch (e) { |
|||
console.error('Erro ao buscar dados para comparação:', e) |
|||
} finally { |
|||
this.isLoading = false |
|||
} |
|||
}, |
|||
|
|||
computed: { |
|||
...mapGetters('projects', ['project']), |
|||
compareAnnotations(): (annotations1: any[], annotations2: any[]) => string | null { |
|||
return (annotations1: any[], annotations2: any[]) => { |
|||
const differences: string[] = [] |
|||
|
|||
const spans1 = annotations1.filter(a => a.start_offset !== undefined).sort((a, b) => a.start_offset - b.start_offset) |
|||
const spans2 = annotations2.filter(a => a.start_offset !== undefined).sort((a, b) => a.start_offset - b.start_offset) |
|||
|
|||
if (spans1.length !== spans2.length) { |
|||
differences.push(`Número diferente de spans: ${spans1.length} vs ${spans2.length}`) |
|||
} else { |
|||
for (let i = 0; i < spans1.length; i++) { |
|||
if (spans1[i].label !== spans2[i].label || |
|||
spans1[i].start_offset !== spans2[i].start_offset || |
|||
spans1[i].end_offset !== spans2[i].end_offset) { |
|||
differences.push(`Span diferente: "${spans1[i].start_offset}-${spans1[i].end_offset}:${spans1[i].label}" vs "${spans2[i].start_offset}-${spans2[i].end_offset}:${spans2[i].label}"`) |
|||
} |
|||
} |
|||
} |
|||
return differences.length > 0 ? differences.join('\n') : null |
|||
} |
|||
}, |
|||
uniqueLabelsForLegend(): string[] { |
|||
const allLabels = [ |
|||
...this.user1Annotations.map(ann => ann.label), |
|||
...this.user2Annotations.map(ann => ann.label), |
|||
]; |
|||
return Array.from(new Set(allLabels)); |
|||
}, |
|||
labelColorsDict(): { [key: string]: string } { |
|||
// Gera as cores de forma determinística para todos os labels únicos |
|||
// Cores mais claras/pastéis para melhor contraste com sublinhado vermelho |
|||
const colors = [ |
|||
'#FFB6C1', // Light Pink |
|||
'#87CEEB', // Sky Blue |
|||
'#98FB98', // Pale Green |
|||
'#F0E68C', // Khaki |
|||
'#DDA0DD', // Plum |
|||
'#AFEEEE', // Pale Turquoise |
|||
'#FFE4E1', // Misty Rose |
|||
'#E0FFFF', // Light Cyan |
|||
'#FFEFD5', // Papaya Whip |
|||
'#F5DEB3', // Wheat |
|||
'#D8BFD8', // Thistle |
|||
'#B0E0E6', // Powder Blue |
|||
'#FAFAD2', // Light Goldenrod Yellow |
|||
'#FFE4B5', // Moccasin |
|||
'#E6E6FA', // Lavender |
|||
'#F0FFFF', // Azure |
|||
'#FFF8DC', // Cornsilk |
|||
'#F5F5DC', // Beige |
|||
'#FFFACD', // Lemon Chiffon |
|||
'#F0F8FF', // Alice Blue |
|||
]; |
|||
const dict: { [key: string]: string } = {}; |
|||
this.uniqueLabelsForLegend.forEach(label => { |
|||
const hash = label.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0); |
|||
dict[label] = colors[hash % colors.length]; |
|||
}); |
|||
return dict; |
|||
}, |
|||
}, |
|||
methods: { |
|||
getLabelColor(label: string): string { |
|||
return this.labelColorsDict[label] || '#FFF'; |
|||
}, |
|||
markDiscrepantSpans( |
|||
annotations1: SpanAnnotation[], |
|||
annotations2: SpanAnnotation[] |
|||
): { markedAnnotations1: SpanAnnotation[]; markedAnnotations2: SpanAnnotation[] } { |
|||
const markedAnnotations1: SpanAnnotation[] = annotations1.map(ann => ({ ...ann, isDiscrepant: false })) |
|||
const markedAnnotations2: SpanAnnotation[] = annotations2.map(ann => ({ ...ann, isDiscrepant: false })) |
|||
|
|||
// Função auxiliar para verificar se uma anotação existe em uma lista |
|||
const existsInList = (annotation: SpanAnnotation, list: SpanAnnotation[]) => { |
|||
return list.some( |
|||
a => |
|||
a.start_offset === annotation.start_offset && |
|||
a.end_offset === annotation.end_offset && |
|||
a.label === annotation.label |
|||
) |
|||
} |
|||
|
|||
// Marcar discrepâncias na primeira lista |
|||
markedAnnotations1.forEach(ann1 => { |
|||
if (!existsInList(ann1, markedAnnotations2)) { |
|||
ann1.isDiscrepant = true |
|||
} |
|||
}) |
|||
|
|||
// Marcar discrepâncias na segunda lista |
|||
markedAnnotations2.forEach(ann2 => { |
|||
if (!existsInList(ann2, markedAnnotations1)) { |
|||
ann2.isDiscrepant = true |
|||
} |
|||
}) |
|||
|
|||
return { markedAnnotations1, markedAnnotations2 } |
|||
} |
|||
} |
|||
}) |
|||
</script> |
|||
|
|||
<style scoped> |
|||
/* Estilos futuros para a visualização lado a lado */ |
|||
</style> |
Write
Preview
Loading…
Cancel
Save