Browse Source

discrepancias automaticas

pull/2433/head
laurarodrigues3 3 months ago
parent
commit
e49c39f197
12 changed files with 817 additions and 4 deletions
  1. 18
      backend/projects/migrations/0009_project_label_discrepancy_threshold.py
  2. 4
      backend/projects/models.py
  3. 1
      backend/projects/serializers.py
  4. 586
      frontend/components/discrepancy/AutomaticDiscrepancyList.vue
  5. 9
      frontend/components/layout/TheSideBar.vue
  6. 44
      frontend/components/project/DiscrepancyThresholdField.vue
  7. 3
      frontend/domain/models/project/project.ts
  8. 4
      frontend/mixins/databaseHealthMixin.js
  9. 135
      frontend/pages/projects/_id/automatic-discrepancies/index.vue
  10. 10
      frontend/pages/projects/create.vue
  11. 2
      frontend/repositories/project/apiProjectRepository.ts
  12. 5
      frontend/services/application/project/projectApplicationService.ts

18
backend/projects/migrations/0009_project_label_discrepancy_threshold.py

@ -0,0 +1,18 @@
# Generated by Django 4.2.15 on 2025-06-13 00:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("projects", "0008_project_allow_member_to_create_label_type_and_more"),
]
operations = [
migrations.AddField(
model_name="project",
name="label_discrepancy_threshold",
field=models.FloatField(default=0.0, help_text="Percentagem mínima de discrepância de labels (0-100)"),
),
]

4
backend/projects/models.py

@ -40,6 +40,10 @@ class Project(PolymorphicModel):
collaborative_annotation = models.BooleanField(default=False)
single_class_classification = models.BooleanField(default=False)
allow_member_to_create_label_type = models.BooleanField(default=False)
label_discrepancy_threshold = models.FloatField(
default=0.0,
help_text="Percentagem mínima de discrepância de labels (0-100)"
)
def add_admin(self):
admin_role = Role.objects.get(name=settings.ROLE_PROJECT_ADMIN)

1
backend/projects/serializers.py

@ -72,6 +72,7 @@ class ProjectSerializer(serializers.ModelSerializer):
"collaborative_annotation",
"single_class_classification",
"allow_member_to_create_label_type",
"label_discrepancy_threshold",
"is_text_project",
"tags",
]

586
frontend/components/discrepancy/AutomaticDiscrepancyList.vue

@ -0,0 +1,586 @@
<template>
<div class="container">
<v-data-table
:items="processedItems"
:headers="headers"
:loading="isLoading"
loading-text="Carregando..."
no-data-text="Nenhum dado disponível"
:footer-props="{
showFirstLastPage: true,
'items-per-page-options': [10, 50, 100],
'items-per-page-text': 'Itens por página:',
'page-text': '{0}-{1} de {2}'
}"
item-key="id"
@input="$emit('input', $event)"
>
<template #top>
<v-row class="mb-3">
<v-col cols="12" md="6">
<v-text-field
v-model="search"
:prepend-inner-icon="mdiMagnify"
label="Search"
single-line
hide-details
filled
/>
</v-col>
<v-col cols="12" md="6">
<v-select
v-model="selectedAnnotationFilter"
:items="annotationFilterOptions"
label="Filter by Text"
prepend-inner-icon="mdi-filter"
clearable
hide-details
filled
/>
</v-col>
</v-row>
</template>
<template #[`item.text`]="{ item }">
<span class="d-flex d-sm-none">{{ item.text.length > 50 ? item.text.substring(0, 50) + '...' : item.text }}</span>
<span class="d-none d-sm-flex">{{ item.text.length > 200 ? item.text.substring(0, 200) + '...' : item.text }}</span>
</template>
<template #[`item.annotations`]="{ item }">
<div style="white-space: pre-line;">{{ item.annotationsText }}</div>
</template>
<template #[`item.labelPercentages`]="{ item }">
<div v-if="item.labelPercentages && Object.keys(item.labelPercentages).length > 0">
<div v-for="(percentage, label) in item.labelPercentages" :key="label" class="automatic-label-percentage">
<v-badge
:content="percentage + '%'"
:color="getAutomaticLabelColor(String(label))"
overlap
class="mr-2 mb-1"
>
<v-chip
small
:color="getAutomaticLabelColor(String(label))"
outlined
class="font-weight-bold"
style="border-width: 2px;"
>
{{ label }}
</v-chip>
</v-badge>
</div>
</div>
<span v-else class="text--disabled font-italic">No data available</span>
</template>
<template #[`item.alignment`]="{ item }">
<div class="alignment-info">
<v-progress-linear
:value="item.alignmentPercentage"
height="12"
:color="getAlignmentColor(item.alignmentPercentage)"
class="mb-1"
rounded
></v-progress-linear>
<span class="percentage-text font-weight-medium">{{ item.alignmentPercentage }}%</span>
</div>
</template>
<template #[`item.status`]="{ item }">
<v-chip v-if="item.hasDiscrepancy" color="error" small>
Discrepant
</v-chip>
<v-chip v-else color="success" small>
Non Discrepant
</v-chip>
</template>
</v-data-table>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
import { mdiMagnify, mdiAlertCircle } from '@mdi/js'
import type { PropType } from 'vue'
type ExampleDTO = {
id: number
text: string
annotations: Array<{
user: number
label: string
start_offset: number
end_offset: number
}>
}
export default Vue.extend({
name: 'AutomaticDiscrepancyList',
props: {
isLoading: {
type: Boolean,
default: false,
required: true
},
items: {
type: Array as PropType<ExampleDTO[]>,
default: () => [],
required: true
},
members: {
type: Array as PropType<any[]>,
default: () => [],
required: true
},
databaseError: {
type: Boolean,
default: false,
required: false
}
},
data() {
return {
search: '',
mdiMagnify,
mdiAlertCircle,
memberNames: {} as { [key: number]: string },
selectedAnnotationFilter: null as string | null
}
},
computed: {
headers() {
return [
{ text: 'Text', value: 'text', sortable: true },
{ text: 'Label Percentage', value: 'labelPercentages', sortable: false },
{ text: 'Alignment', value: 'alignment', sortable: false },
{ text: 'Status', value: 'status', sortable: false }
]
},
projectId(): string {
return this.$route.params.id
},
annotationFilterOptions() {
const annotatedTexts = new Set<string>()
this.items.forEach(item => {
if (item.annotations && Array.isArray(item.annotations) && item.annotations.length > 0) {
annotatedTexts.add(item.text) // Valor completo para filtro
}
})
return Array.from(annotatedTexts).sort().map(text => ({
text: text.length > 80 ? text.substring(0, 80) + '...' : text, // Texto truncado para exibição
value: text // Texto completo para filtro
}))
},
processedItems() {
console.log('📦 Anotações recebidas:', JSON.stringify(this.items, null, 2))
const result: Array<{
id: number
text: string
annotationsText: string
hasDiscrepancy: boolean
discrepancyDetails: string
labelPercentages: { [label: string]: number }
alignmentPercentage: number
}> = []
// Agrupar anotações por documento (não por usuário e documento)
const processedDocIds = new Set<number>()
const annotationsByDoc: { [docId: number]: { [memberId: number]: any[] } } = {}
// Primeiro passo: agrupar todas as anotações por documento
this.items.forEach(item => {
if (item.annotations && Array.isArray(item.annotations)) {
if (!annotationsByDoc[item.id]) {
annotationsByDoc[item.id] = {}
}
item.annotations.forEach((annotation: any) => {
const userId = annotation.user ?? annotation.user_id ?? annotation.created_by
if (userId) {
if (!annotationsByDoc[item.id][userId]) {
annotationsByDoc[item.id][userId] = []
}
annotationsByDoc[item.id][userId].push(annotation)
}
})
}
})
// Segundo passo: processar cada documento apenas uma vez
Object.keys(annotationsByDoc).forEach(docIdStr => {
const docId = parseInt(docIdStr)
if (processedDocIds.has(docId)) return
processedDocIds.add(docId)
const annotations = annotationsByDoc[docId]
if (!annotations) return
const memberIds = Object.keys(annotations).map(id => parseInt(id))
if (memberIds.length <= 1) return
// Encontrar o documento correspondente
const item = this.items.find(i => i.id === docId)
if (!item) return
let discrepancyDetails = ''
let annotationsText = ''
for (let i = 0; i < memberIds.length; i++) {
const memberId = memberIds[i]
const memberAnnotations = annotations[memberId]
const memberName = this.memberNames[memberId] || `Usuário ${memberId}`
annotationsText += `${memberName}: ${this.formatAnnotations(memberAnnotations)}\n`
for (let j = i + 1; j < memberIds.length; j++) {
const otherMemberId = memberIds[j]
const otherAnnotations = annotations[otherMemberId]
const discrepancy = this.compareAnnotations(memberAnnotations, otherAnnotations)
if (discrepancy) {
const otherMemberName = this.memberNames[otherMemberId] || `Usuário ${otherMemberId}`
discrepancyDetails += `Discrepancy between ${memberName} and ${otherMemberName}:\n${discrepancy}\n\n`
}
}
}
if (this.search) {
const searchLower = this.search.toLowerCase()
if (!item.text.toLowerCase().includes(searchLower) &&
!annotationsText.toLowerCase().includes(searchLower)) return
}
// Filtrar por texto selecionado
if (this.selectedAnnotationFilter) {
if (item.text !== this.selectedAnnotationFilter) return
}
// Calcular porcentagens de labels
const labelCounts: { [label: string]: number } = {}
const totalAnnotations: { [label: string]: number } = {}
// Contar ocorrências de cada label
Object.values(annotations).forEach(memberAnnotations => {
memberAnnotations.forEach((annotation: any) => {
if (annotation.label) {
if (!labelCounts[annotation.label]) {
labelCounts[annotation.label] = 0
totalAnnotations[annotation.label] = 0
}
labelCounts[annotation.label]++
totalAnnotations[annotation.label]++
}
})
})
// Calcular porcentagens
const labelPercentages: { [label: string]: number } = {}
const totalLabels = Object.values(totalAnnotations).reduce((sum, count) => sum + count, 0)
if (totalLabels > 0) {
Object.keys(labelCounts).forEach(label => {
labelPercentages[label] = Math.round((labelCounts[label] / totalLabels) * 100)
})
}
// Calcular alignment baseado no threshold do projeto
const threshold = this.$store.getters['projects/project'].labelDiscrepancyThreshold || 0
// Calcular percentagem da label mais comum
let alignmentPercentage = 0
if (memberIds.length > 0) {
// Contar todas as labels de todos os anotadores
const labelCounts: { [label: string]: number } = {}
let totalLabels = 0
memberIds.forEach(memberId => {
const memberAnnotations = annotations[memberId]
if (memberAnnotations && memberAnnotations.length > 0) {
memberAnnotations.forEach((annotation: any) => {
if (annotation.label) {
labelCounts[annotation.label] = (labelCounts[annotation.label] || 0) + 1
totalLabels++
}
})
}
})
if (totalLabels > 0) {
// Encontrar a label mais frequente
let maxCount = 0
Object.values(labelCounts).forEach(count => {
if (count > maxCount) {
maxCount = count
}
})
// Calcular percentagem da label mais comum
alignmentPercentage = Math.round((maxCount / totalLabels) * 100)
}
}
// Determinar se é discrepant baseado no threshold
const isDiscrepant = alignmentPercentage < threshold
result.push({
id: docId,
text: item.text,
annotationsText,
hasDiscrepancy: isDiscrepant,
discrepancyDetails,
labelPercentages,
alignmentPercentage
})
})
return result
}
},
watch: {
items: {
handler() {
this.$nextTick(() => {
this.loadMemberNames()
})
},
deep: true,
immediate: true
}
},
mounted() {
this.loadMemberNames()
this.loadLabelsIfNeeded()
},
methods: {
formatAnnotations(annotations: any[]): string {
if (!annotations || annotations.length === 0) return 'Sem anotações'
return annotations.map(a => {
if (a.label !== undefined) return `Label: ${a.label}`
if (a.text !== undefined) return a.text
return JSON.stringify(a)
}).join(', ')
},
loadLabelsIfNeeded() {
const labels = this.$store.getters['labels/list']
if (!labels || labels.length === 0) {
this.$store.dispatch('labels/fetch', this.projectId).catch(error => {
console.warn('Erro ao carregar labels:', error)
})
}
},
getLabelName(labelId: string): string {
// Se você tiver um mapeamento global com os nomes das labels (ex: this.$store.getters['labels/list'])
const label = this.$store.getters['labels/list']?.find((l: any) => l.id.toString() === labelId)
return label ? label.text : labelId
},
getLabelColor(label: string): string {
// Gerar uma cor consistente baseada no nome do label
const colors = ['primary', 'secondary', 'success', 'info', 'warning', 'error', 'purple', 'indigo', 'cyan', 'teal', 'orange']
const hash = label.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)
return colors[hash % colors.length]
},
getAutomaticLabelColor(label: string): string {
// Paleta de cores primárias e fortes para automatic discrepancies
const automaticColors = ['blue darken-3', 'green darken-3', 'red darken-1', 'orange darken-2', 'purple darken-2', 'teal darken-2', 'indigo darken-2', 'brown darken-2', 'blue-grey darken-3', 'deep-orange darken-1']
const hash = label.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)
return automaticColors[hash % automaticColors.length]
},
getParticipationColor(rate: number): string {
if (rate < 30) return 'error'
if (rate < 60) return 'warning'
return 'success'
},
getAlignmentColor(alignmentPercentage: number): string {
const threshold = this.$store.getters['projects/project'].labelDiscrepancyThreshold || 0
if (alignmentPercentage >= threshold) return 'success'
if (alignmentPercentage >= threshold * 0.7) return 'warning'
return 'error'
},
truncate(value: string, length: number): string {
if (!value || typeof value !== 'string') return ''
return value.length > length ? value.slice(0, length) + '...' : value
},
compareAnnotations(annotations1: any[], annotations2: any[]): string | null {
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}"`)
}
}
}
const categories1 = annotations1.filter(a => a.label !== undefined && a.start_offset === undefined)
const categories2 = annotations2.filter(a => a.label !== undefined && a.start_offset === undefined)
if (categories1.length !== categories2.length) {
differences.push(`Número diferente de categorias: ${categories1.length} vs ${categories2.length}`)
} else {
const labels1 = new Set<string>(categories1.map(c => c.label))
const labels2 = new Set<string>(categories2.map(c => c.label))
if (labels1.size !== labels2.size) {
differences.push(`Número diferente de labels: ${labels1.size} vs ${labels2.size}`)
} else {
for (const label of labels1) {
if (!labels2.has(label)) {
differences.push(`Label diferente: "${label}" não encontrado no segundo conjunto`)
}
}
}
}
return differences.length > 0 ? differences.join('\n') : null
},
loadMemberNames() {
// Usar os dados dos members que já foram buscados na página principal
this.members.forEach((member: any) => {
this.$set(this.memberNames, member.id, member.username)
})
// Para qualquer member ID que não esteja na lista, usar um fallback
const memberIds = new Set<number>()
this.items.forEach(item => {
if (item.annotations && Array.isArray(item.annotations)) {
item.annotations.forEach((annotation: any) => {
if (annotation.user && !this.memberNames[annotation.user]) {
memberIds.add(annotation.user)
}
})
}
})
// Só buscar members que não estão na lista já carregada
memberIds.forEach(memberId => {
this.$set(this.memberNames, memberId, `Usuário ${memberId}`)
})
},
}
})
</script>
<style scoped>
.container {
padding-left: 20px;
padding-right: 20px;
margin-top: 10px;
}
.pulse-animation {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(255, 193, 7, 0.4);
}
70% {
box-shadow: 0 0 0 10px rgba(255, 193, 7, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(255, 193, 7, 0);
}
}
.discrepancy-tooltip {
max-width: 300px;
white-space: pre-line;
font-size: 14px;
}
.label-percentage {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.label-percentage .v-chip {
font-size: 11px;
padding: 0 8px;
height: 24px;
max-width: 120px;
white-space: nowrap;
}
.label-percentage .v-progress-linear {
width: 120px;
height: 10px;
border-radius: 4px;
background-color: #f0f0f0;
margin: 0;
}
.percentage-text {
font-size: 12px;
min-width: 32px;
text-align: right;
}
.alignment-info {
display: flex;
flex-direction: column;
align-items: flex-start;
min-width: 120px;
}
.automatic-label-percentage {
display: inline-block;
margin-right: 8px;
margin-bottom: 8px;
}
.automatic-label-percentage .v-badge {
position: relative;
}
.automatic-label-percentage .v-chip {
transition: all 0.3s ease;
}
.automatic-label-percentage .v-chip:hover {
transform: scale(1.05);
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}
</style>

9
frontend/components/layout/TheSideBar.vue

@ -40,7 +40,8 @@ import {
mdiPlayCircleOutline,
mdiChatOutline,
mdiAlertCircleOutline,
mdiEyeOutline
mdiEyeOutline,
mdiRobotOutline
} from '@mdi/js'
import { getLinkToAnnotationPage } from '~/presenter/linkToAnnotationPage'
@ -138,6 +139,12 @@ export default {
link: 'perspectives',
isVisible: true
},
{
icon: mdiRobotOutline, // ÍCONE DE DISCREPÂNCIAS AUTOMÁTICAS
text: 'Automatic Discrepancies',
link: 'automatic-discrepancies', // LEVA PARA /projects/:id/automatic-discrepancies
isVisible: this.isProjectAdmin
},
{
icon: mdiCog,
text: this.$t('settings.title'),

44
frontend/components/project/DiscrepancyThresholdField.vue

@ -0,0 +1,44 @@
<template>
<v-text-field
:value="value"
:label="label"
:rules="rules"
:outlined="outlined"
type="number"
min="0"
max="100"
step="0.1"
suffix="%"
persistent-hint
:hint="hint"
@input="$emit('input', $event === '' ? '' : parseFloat($event))"
/>
</template>
<script>
export default {
props: {
value: {
type: [Number, String],
default: ''
},
outlined: {
type: Boolean,
default: false
}
},
data() {
return {
label: 'Percentagem Mínima de Discrepância de Labels',
hint: 'Defina a percentagem mínima de discrepância para análise comparativa (0-100%)',
rules: [
v => (v !== null && v !== undefined && v !== '') || 'Percentagem é obrigatória',
v => (v === '' || v >= 0) || 'Percentagem deve ser pelo menos 0%',
v => (v === '' || v <= 100) || 'Percentagem não pode exceder 100%',
v => (v === '' || !isNaN(parseFloat(v))) || 'Valor deve ser um número válido'
]
}
}
}
</script>

3
frontend/domain/models/project/project.ts

@ -61,6 +61,7 @@ export class Project {
readonly _description: string,
readonly guideline: string,
readonly _projectType: string,
readonly labelDiscrepancyThreshold: number = 0,
readonly enableRandomOrder: boolean,
readonly enableSharingMode: boolean,
readonly exclusiveCategories: boolean,
@ -98,6 +99,7 @@ export class Project {
description: string,
guideline: string,
projectType: string,
labelDiscrepancyThreshold: number,
enableRandomOrder: boolean,
enableSharingMode: boolean,
exclusiveCategories: boolean,
@ -113,6 +115,7 @@ export class Project {
description,
guideline,
projectType,
labelDiscrepancyThreshold,
enableRandomOrder,
enableSharingMode,
exclusiveCategories,

4
frontend/mixins/databaseHealthMixin.js

@ -33,10 +33,10 @@ export const databaseHealthMixin = {
// Verificação inicial
this.checkDatabaseHealth()
// Verificação a cada 10 segundos
// Verificação a cada 3 segundos
this.healthCheckInterval = setInterval(() => {
this.checkDatabaseHealth()
}, 10000)
}, 3000)
},
stopHealthCheck() {

135
frontend/pages/projects/_id/automatic-discrepancies/index.vue

@ -0,0 +1,135 @@
<template>
<v-card>
<v-card-title>
<h2>Automatic Discrepancies</h2>
<v-spacer />
<v-card
color="gradient"
class="pa-3 ml-4 d-flex align-center"
style="background: linear-gradient(45deg, #667eea 0%, #764ba2 100%); border-radius: 12px; box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);"
elevation="0"
>
<v-icon color="white" class="mr-2">mdi-target</v-icon>
<div class="white--text">
<div class="text-caption font-weight-medium opacity-90">Threshold</div>
<div class="text-h6 font-weight-bold">{{ $store.getters['projects/project'].labelDiscrepancyThreshold }}%</div>
</div>
</v-card>
</v-card-title>
<v-card-text>
<!-- Mensagem de erro da base de dados -->
<v-alert
v-if="!isDatabaseHealthy"
type="error"
class="mb-4"
>
De momento, a base de dados não se encontra disponível. Por favor, tente mais tarde.
</v-alert>
<AutomaticDiscrepancyList
:items="items"
:is-loading="isDatabaseHealthy && isLoading"
:members="members"
:database-error="!isDatabaseHealthy"
/>
</v-card-text>
</v-card>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { mapGetters } from 'vuex'
import AutomaticDiscrepancyList from '../../../../components/discrepancy/AutomaticDiscrepancyList.vue'
import { databaseHealthMixin } from '../../../../mixins/databaseHealthMixin'
// Definindo o tipo localmente para evitar import quebrado
export type ExampleDTO = {
id: number
text: string
annotations: Array<{
user: number
label: string
start_offset: number
end_offset: number
}>
}
export type MemberItem = {
id: number
username: string
}
export default defineComponent({
name: 'AutomaticDiscrepancyPage',
components: {
AutomaticDiscrepancyList
},
mixins: [databaseHealthMixin],
layout: 'project',
middleware: ['check-auth', 'auth', 'setCurrentProject'],
data() {
return {
items: [] as ExampleDTO[],
members: [] as MemberItem[],
isLoading: false
}
},
computed: {
...mapGetters('projects', ['project']),
projectId(): string {
return this.$route.params.id
}
},
async fetch() {
this.isLoading = true
try {
const [examplesResponse, membersResponse] = await Promise.all([
this.$repositories.example.list(this.projectId, {
limit: '1000',
offset: '0',
include_annotation: 'true'
}),
this.$repositories.member.list(this.projectId)
])
this.items = examplesResponse.items.map((item: any) => {
return {
id: item.id,
text: item.text,
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
}))
}
})
this.members = membersResponse.map((member: any) => ({
id: member.id,
username: member.username
}))
console.log('📦 Anotações recebidas:', JSON.stringify(this.items, null, 2))
} catch (e) {
console.error('Erro ao buscar dados do projeto:', e)
} finally {
this.isLoading = false
}
},
mounted() {
if (this.$store.hasModule('projects')) {
this.$store.commit('projects/setPageTitle', 'Discrepâncias Automáticas entre Anotações')
}
}
})
</script>

10
frontend/pages/projects/create.vue

@ -6,6 +6,7 @@
<project-type-field v-model="editedItem.projectType" />
<project-name-field v-model="editedItem.name" outlined autofocus />
<project-description-field v-model="editedItem.description" outlined />
<discrepancy-threshold-field v-model="editedItem.labelDiscrepancyThreshold" outlined />
<tag-list v-model="editedItem.tags" outlined />
<v-checkbox
v-if="showExclusiveCategories"
@ -71,6 +72,7 @@ import Vue from 'vue'
import ProjectDescriptionField from '~/components/project/ProjectDescriptionField.vue'
import ProjectNameField from '~/components/project/ProjectNameField.vue'
import ProjectTypeField from '~/components/project/ProjectTypeField.vue'
import DiscrepancyThresholdField from '~/components/project/DiscrepancyThresholdField.vue'
import RandomOrderField from '~/components/project/RandomOrderField.vue'
import SharingModeField from '~/components/project/SharingModeField.vue'
import TagList from '~/components/project/TagList.vue'
@ -86,6 +88,7 @@ const initializeProject = () => {
name: '',
description: '',
projectType: DocumentClassification,
labelDiscrepancyThreshold: '',
enableRandomOrder: false,
enableSharingMode: false,
exclusiveCategories: false,
@ -103,6 +106,7 @@ export default Vue.extend({
ProjectTypeField,
ProjectNameField,
ProjectDescriptionField,
DiscrepancyThresholdField,
RandomOrderField,
SharingModeField,
TagList
@ -133,7 +137,11 @@ export default Vue.extend({
methods: {
async create() {
const project = await this.$services.project.create(this.editedItem)
const projectData = {
...this.editedItem,
labelDiscrepancyThreshold: parseFloat(this.editedItem.labelDiscrepancyThreshold) || 0
}
const project = await this.$services.project.create(projectData)
this.$router.push(`/projects/${project.id}`)
this.$nextTick(() => {
this.editedItem = initializeProject()

2
frontend/repositories/project/apiProjectRepository.ts

@ -31,6 +31,7 @@ function toModel(item: { [key: string]: any }): Project {
item.description,
item.guideline,
item.project_type,
item.label_discrepancy_threshold || 0,
item.random_order,
item.collaborative_annotation,
item.single_class_classification,
@ -54,6 +55,7 @@ function toPayload(item: Project): { [key: string]: any } {
description: item.description,
guideline: item.guideline,
project_type: item.projectType,
label_discrepancy_threshold: item.labelDiscrepancyThreshold,
random_order: item.enableRandomOrder,
collaborative_annotation: item.enableSharingMode,
single_class_classification: item.exclusiveCategories,

5
frontend/services/application/project/projectApplicationService.ts

@ -8,6 +8,7 @@ type ProjectFields = {
description: string
guideline: string
projectType: string
labelDiscrepancyThreshold: number
enableRandomOrder: boolean
enableSharingMode: boolean
exclusiveCategories: boolean
@ -46,6 +47,7 @@ export class ProjectApplicationService {
name,
description,
projectType,
labelDiscrepancyThreshold,
enableRandomOrder,
enableSharingMode,
exclusiveCategories,
@ -62,6 +64,7 @@ export class ProjectApplicationService {
description,
guideline,
projectType,
labelDiscrepancyThreshold,
enableRandomOrder,
enableSharingMode,
exclusiveCategories,
@ -84,6 +87,7 @@ export class ProjectApplicationService {
name,
description,
projectType,
labelDiscrepancyThreshold,
enableRandomOrder,
enableSharingMode,
exclusiveCategories,
@ -100,6 +104,7 @@ export class ProjectApplicationService {
description,
guideline,
projectType,
labelDiscrepancyThreshold,
enableRandomOrder,
enableSharingMode,
exclusiveCategories,

Loading…
Cancel
Save