Browse Source

- avanzamento creazione grafica relazioni

- tentativo di creazione pagine di gestione tipi di relazioni
pull/1384/head
mauro 3 years ago
parent
commit
b6eea92095
15 changed files with 984 additions and 243 deletions
  1. 1
      frontend/components/layout/TheSideBar.vue
  2. 32
      frontend/components/links/ActionMenu.vue
  3. 106
      frontend/components/links/FormCreate.vue
  4. 28
      frontend/components/links/FormDelete.vue
  5. 89
      frontend/components/links/LinksList.vue
  6. 111
      frontend/components/tasks/sequenceLabeling/EntityItem.vue
  7. 209
      frontend/components/tasks/sequenceLabeling/EntityItemBox.vue
  8. 81
      frontend/domain/models/links/link.ts
  9. 11
      frontend/domain/models/links/linksRepository.ts
  10. 169
      frontend/pages/projects/_id/links/index.vue
  11. 94
      frontend/pages/projects/_id/sequence-labeling/index.vue
  12. 202
      frontend/plugins/services.ts
  13. 45
      frontend/repositories/links/apiLinksRepository.ts
  14. 19
      frontend/services/application/links/linkData.ts
  15. 30
      frontend/services/application/links/linksApplicationService.ts

1
frontend/components/layout/TheSideBar.vue

@ -61,6 +61,7 @@ export default {
{ icon: 'mdi-home', text: this.$t('projectHome.home'), link: '', adminOnly: false },
{ icon: 'mdi-database', text: this.$t('dataset.dataset'), link: 'dataset', adminOnly: true },
{ icon: 'label', text: this.$t('labels.labels'), link: 'labels', adminOnly: true },
{ icon: 'label', text: 'Relations', link: 'links', adminOnly: true },
{ icon: 'person', text: this.$t('members.members'), link: 'members', adminOnly: true },
{ icon: 'mdi-comment-account-outline', text: 'Comments', link: 'comments', adminOnly: true },
{ icon: 'mdi-book-open-outline', text: this.$t('guideline.guideline'), link: 'guideline', adminOnly: true },

32
frontend/components/links/ActionMenu.vue

@ -0,0 +1,32 @@
<template>
<action-menu
:items="items"
:text="$t('dataset.actions')"
@create="$emit('create')"
@upload="$emit('upload')"
@download="$emit('download')"
/>
</template>
<script lang="ts">
import Vue from 'vue'
import ActionMenu from '~/components/utils/ActionMenu.vue'
export default Vue.extend({
components: {
ActionMenu
},
computed: {
items() {
return [
{
title: this.$t('links.createLink'),
icon: 'mdi-pencil',
event: 'create'
}
]
}
}
})
</script>

106
frontend/components/links/FormCreate.vue

@ -0,0 +1,106 @@
<template>
<base-card
:disabled="!valid"
:title="$t('labels.createLabel')"
:agree-text="$t('generic.save')"
:cancel-text="$t('generic.cancel')"
@agree="$emit('save')"
@cancel="$emit('cancel')"
>
<template #content>
<v-form v-model="valid">
<v-text-field
:value="text"
:label="$t('labels.labelName')"
:rules="[rules.required, rules.counter, rules.nameDuplicated]"
prepend-icon="label"
single-line
counter
autofocus
@input="updateValue('text', $event)"
/>
<v-select
:value="suffixKey"
:items="shortkeys"
:label="$t('labels.key')"
:rules="[rules.keyDuplicated]"
prepend-icon="mdi-keyboard"
@input="updateValue('suffixKey', $event)"
/>
<v-color-picker
:value="backgroundColor"
:rules="[rules.required]"
show-swatches
hide-mode-switch
width="800"
@input="updateValue('backgroundColor', $event)"
/>
</v-form>
</template>
</base-card>
</template>
<script lang="ts">
import Vue from 'vue'
import BaseCard from '@/components/utils/BaseCard.vue'
export default Vue.extend({
components: {
BaseCard
},
props: {
text: {
type: String,
default: '',
required: true
},
suffixKey: {
type: String,
default: null,
},
backgroundColor: {
type: String,
default: '#ffffff',
required: true
},
usedNames: {
type: Array,
default: () => [],
required: true
},
usedKeys: {
type: Array,
default: () => [],
required: true
}
},
data() {
return {
valid: false,
rules: {
required: (v: string) => !!v || 'Required',
// @ts-ignore
counter: (v: string) => (v && v.length <= 100) || this.$t('rules.labelNameRules').labelLessThan100Chars,
// @ts-ignore
nameDuplicated: (v: string) => (!this.usedNames.includes(v)) || this.$t('rules.labelNameRules').duplicated,
// @ts-ignore
keyDuplicated: (v: string) => !this.usedKeys.includes(v) || this.$t('rules.keyNameRules').duplicated,
}
}
},
computed: {
shortkeys() {
return '0123456789abcdefghijklmnopqrstuvwxyz'.split('')
}
},
methods: {
updateValue(key: string, value: string) {
this.$emit(`update:${key}`, value);
}
}
})
</script>

28
frontend/components/links/FormDelete.vue

@ -0,0 +1,28 @@
<template>
<confirm-form
:items="selected"
title="Delete Label"
:message="$t('labels.deleteMessage')"
item-key="text"
@ok="$emit('remove')"
@cancel="$emit('cancel')"
/>
</template>
<script lang="ts">
import Vue from 'vue'
import ConfirmForm from '@/components/utils/ConfirmForm.vue'
export default Vue.extend({
components: {
ConfirmForm
},
props: {
selected: {
type: Array,
default: () => []
}
}
})
</script>

89
frontend/components/links/LinksList.vue

@ -0,0 +1,89 @@
<template>
<v-data-table
:value="value"
:headers="headers"
:items="items"
:search="search"
:loading="isLoading"
:loading-text="$t('generic.loading')"
:no-data-text="$t('vuetify.noDataAvailable')"
:footer-props="{
'showFirstLastPage': true,
'items-per-page-options': [5, 10, 15, $t('generic.all')],
'items-per-page-text': $t('vuetify.itemsPerPageText'),
'page-text': $t('dataset.pageText')
}"
item-key="id"
show-select
@input="$emit('input', $event)"
>
<template v-slot:top>
<v-text-field
v-model="search"
prepend-inner-icon="search"
:label="$t('generic.search')"
single-line
hide-details
filled
/>
</template>
<template v-slot:[`item.backgroundColor`]="props">
<v-chip
:color="props.item.backgroundColor"
:text-color="$contrastColor(props.item.backgroundColor)"
>
{{ props.item.backgroundColor }}
</v-chip>
</template>
<template v-slot:[`item.actions`]="{ item }">
<v-icon
small
@click="$emit('edit', item)"
>
mdi-pencil
</v-icon>
</template>
</v-data-table>
</template>
<script lang="ts">
import Vue, { PropType } from 'vue'
import { LinkDTO } from '~/services/application/links/linkData'
export default Vue.extend({
props: {
isLoading: {
type: Boolean,
default: false,
required: true
},
items: {
type: Array as PropType<LinkDTO[]>,
default: () => [],
required: true
},
value: {
type: Array as PropType<LinkDTO[]>,
default: () => [],
required: true
}
},
data() {
return {
search: ''
}
},
computed: {
headers() {
return [
{ text: this.$t('generic.name'), value: 'text' },
{ text: this.$t('links.shortkey'), value: 'suffixKey' },
{ text: this.$t('links.color'), value: 'backgroundColor' },
{ text: 'Actions', value: 'actions', sortable: false },
]
}
}
})
</script>

111
frontend/components/tasks/sequenceLabeling/EntityItem.vue

@ -1,15 +1,17 @@
<template>
<v-menu
v-if="label"
v-if="label && !showLinksMenu"
v-model="showMenu"
offset-y
>
<template v-slot:activator="{ on }">
<span :id="'spn-' + spanid" :style="{ borderColor: color }" class="highlight bottom" v-on="on">
<span class="highlight__content">{{ content }}<v-icon class="delete" @click.stop="remove">mdi-close-circle</v-icon><span
:class="{ active: spanid === selectedChunkId }" class="iconify target-selector" data-icon="mdi-link-variant"
@click.stop="onLinkClick"></span></span><span :data-label="label" :style="{ backgroundColor: color, color: textColor }"
class="highlight__label"/>
v-if="!showMenu && sourceChunk.none" class="choose-link-type" @click.stop="selectSourceAndShowLinkTypes"></span><span
v-if="!showMenu && sourceChunk.id === spanid" class="active-link-source" @click.stop="abortNewLink"></span><span
v-if="selectedLinkType > -1 && sourceChunk.id && sourceChunk.id !== spanid" class="choose-target"
@click.stop="selectTarget"></span></span><span
:data-label="label" :style="{ backgroundColor: color, color: textColor }" class="highlight__label"/>
</span>
</template>
@ -35,6 +37,40 @@
</v-list-item>
</v-list>
</v-menu>
<v-menu
v-else-if="label && showLinksMenu"
v-model="showMenu"
offset-y
>
<template v-slot:activator="{ on }">
<span :id="'spn-' + spanid" :style="{ borderColor: color }" class="highlight bottom" v-on="on">
<span class="highlight__content">{{ content }}<v-icon class="delete" @click.stop="remove">mdi-close-circle</v-icon></span><span
:data-label="label" :style="{ backgroundColor: color, color: textColor }" class="highlight__label"/>
</span>
</template>
<v-list
dense
min-width="150"
max-height="400"
class="overflow-y-auto"
>
<v-list-item
v-for="(item, i) in labels"
:key="i"
@click="selectLinkType(item)"
>
<v-list-item-content>
<v-list-item-title v-text="'LINK - ' + item.text"/>
</v-list-item-content>
<v-list-item-action>
<v-list-item-action-text v-text="item.suffixKey"/>
</v-list-item-action>
</v-list-item>
</v-list>
</v-menu>
<span v-else :class="[newline ? 'newline' : '']">{{ content }}</span>
</template>
@ -69,32 +105,63 @@ export default {
newline: {
type: Boolean
},
selectedChunkId: {
sourceChunk: {
type: Object,
default: () => {
}
},
selectedLinkType: {
type: Number,
default: -1
default: -1,
required: true
}
},
data() {
return {
showMenu: false
showMenu: false,
showLinksMenu: false
}
},
computed: {
textColor() {
return idealColor(this.color)
}
},
methods: {
update(label) {
this.$emit('update', label)
this.showMenu = false
},
remove() {
this.$emit('remove')
},
onLinkClick() {
this.$emit('selectLinkSource');
selectSourceAndShowLinkTypes() {
this.showMenu = true;
this.showLinksMenu = true;
this.$emit('selectSource');
},
selectLinkType(type) {
this.showMenu = false;
this.showLinksMenu = false;
this.$emit('selectLinkType', type);
},
selectTarget() {
this.showMenu = false;
this.showLinksMenu = false;
this.$emit('selectTarget');
},
abortNewLink() {
this.showMenu = false;
this.showLinksMenu = false;
this.$emit('abortNewLink');
}
}
}
@ -137,11 +204,22 @@ export default {
display: block;
}
.highlight .target-selector:before {
content: 'L';
.highlight .choose-link-type:before {
content: 'R';
}
.highlight .active-link-source:before {
content: 'R';
}
.highlight .target-selector {
.highlight .choose-target:before {
content: '+';
}
.highlight .choose-link-type,
.highlight .active-link-source,
.highlight .choose-target {
display: none;
position: absolute;
top: -12px;
right: -11px;
@ -153,9 +231,14 @@ export default {
text-align: center;
}
.highlight .target-selector.active {
.highlight:hover .choose-link-type,
.highlight:hover .choose-target {
display: block;
}
.highlight .active-link-source {
display: block;
background: #008ad6;
background: #00a4cf;
color: #ffffff;
}

209
frontend/components/tasks/sequenceLabeling/EntityItemBox.vue

@ -1,54 +1,58 @@
<template>
<div id="connections-wrapper">
<div class="highlight-container highlight-container--bottom-labels" @click="open" @touchend="open">
<entity-item
v-for="(chunk, i) in chunks"
:key="i"
:spanid="chunk.id"
:content="chunk.text"
:newline="chunk.newline"
:label="chunk.label"
:color="chunk.color"
:labels="labels"
:selected-chunk-id="selectedChunkId"
@remove="deleteAnnotation(chunk.id)"
@update="updateEntity($event.id, chunk.id)"
@selectLinkSource="selectLinkSource(chunk)"
/>
<v-menu
v-model="showMenu"
:position-x="x"
:position-y="y"
absolute
offset-y
>
<v-list
dense
min-width="150"
max-height="400"
class="overflow-y-auto"
>
<v-list-item
v-for="(label, i) in labels"
:key="i"
v-shortkey="[label.suffixKey]"
@shortkey="assignLabel(label.id)"
@click="assignLabel(label.id)"
>
<v-list-item-content>
<v-list-item-title v-text="label.text"/>
</v-list-item-content>
<v-list-item-action>
<v-list-item-action-text v-text="label.suffixKey"/>
</v-list-item-action>
</v-list-item>
</v-list>
</v-menu>
</div>
<div id="connections-wrapper">
<div class="highlight-container highlight-container--bottom-labels" @click="open" @touchend="open">
<entity-item
v-for="(chunk, i) in chunks"
:key="i"
:spanid="chunk.id"
:content="chunk.text"
:newline="chunk.newline"
:label="chunk.label"
:color="chunk.color"
:labels="labels"
:source-chunk="sourceChunk"
:selected-link-type="selectedLinkType"
@remove="deleteAnnotation(chunk.id)"
@update="updateEntity($event.id, chunk.id)"
@selectSource="selectSource(chunk)"
@selectTarget="selectTarget(chunk)"
@selectLinkType="selectLinkType($event.id)"
@abortNewLink="abortNewLink()"
/>
<v-menu
v-model="showMenu"
:position-x="x"
:position-y="y"
absolute
offset-y
>
<v-list
dense
min-width="150"
max-height="400"
class="overflow-y-auto"
>
<v-list-item
v-for="(label, i) in labels"
:key="i"
v-shortkey="[label.suffixKey]"
@shortkey="assignLabel(label.id)"
@click="assignLabel(label.id)"
>
<v-list-item-content>
<v-list-item-title v-text="label.text"/>
</v-list-item-content>
<v-list-item-action>
<v-list-item-action-text v-text="label.suffixKey"/>
</v-list-item-action>
</v-list-item>
</v-list>
</v-menu>
</div>
<canvas id="connections">
</canvas>
</div>
<canvas id="connections">
</canvas>
</div>
</template>
<script>
@ -58,6 +62,7 @@ export default {
components: {
EntityItem
},
props: {
text: {
type: String,
@ -89,17 +94,39 @@ export default {
default: () => ([]),
required: true
},
selectedChunkId: {
sourceChunk: {
type: Object,
default: () => {
},
required: true
},
selectedLinkType: {
type: Number,
default: -1,
required: true
},
selectLinkSource: {
selectSource: {
type: Function,
default: () => ([]),
required: true
},
selectTarget: {
type: Function,
default: () => ([]),
required: true
},
selectLinkType: {
type: Function,
default: () => ([]),
required: true
},
abortNewLink: {
type: Function,
default: () => ([]),
required: true
}
},
data() {
return {
showMenu: false,
@ -109,6 +136,7 @@ export default {
end: 0
}
},
computed: {
sortedEntities() {
return this.entities.slice().sort((a, b) => a.startOffset - b.startOffset)
@ -131,7 +159,8 @@ export default {
label: label.text,
color: label.backgroundColor,
text: piece,
selectedAsLinkSource: false
selectedAsLinkSource: false,
links: []
})
}
// add the rest of text.
@ -146,6 +175,7 @@ export default {
return obj
}
},
updated() {
this.$nextTick(() => {
// SIMULATION ONLY
@ -158,59 +188,42 @@ export default {
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, parentPos.width, parentPos.height);
// simulo una relazione tra le prime due annotazioni
let one, two;
this.chunks.forEach(function(chunk) {
if (chunk.id) {
if (!one) {
one = chunk;
} else if (!two) {
two = chunk;
}
}
});
this.chunks.forEach(function(fromChunk) {
if (fromChunk.links) {
fromChunk.links.forEach(function(toChunk) {
let childPos = document.getElementById('spn-' + fromChunk.id).getBoundingClientRect();
const x1 = (childPos.x + childPos.width / 2) - parentPos.x;
const y1 = (childPos.y + childPos.height / 2) - parentPos.y;
// disegno la pseudo relazione nel canvas
if (one && two) {
let childPos = document.getElementById('spn-' + one.id).getBoundingClientRect();
const x1 = (childPos.x + childPos.width / 2) - parentPos.x;
const y1 = (childPos.y + childPos.height / 2) - parentPos.y;
childPos = document.getElementById('spn-' + toChunk.id).getBoundingClientRect();
const x2 = (childPos.x + childPos.width / 2) - parentPos.x;
const y2 = (childPos.y + childPos.height / 2) - parentPos.y;
childPos = document.getElementById('spn-' + two.id).getBoundingClientRect();
const x2 = (childPos.x + childPos.width / 2) - parentPos.x;
const y2 = (childPos.y + childPos.height / 2) - parentPos.y;
ctx.beginPath();
ctx.lineWidth = 3;
ctx.moveTo(x1, y1);
ctx.strokeStyle = fromChunk.color;
ctx.lineWidth = 3;
ctx.moveTo(x1, y1);
if (y1 === y2) {
ctx.lineTo(x1, y1 + 35);
ctx.stroke();
if (y1 === y2) {
ctx.strokeStyle = one.color;
ctx.lineTo(x1, y1 + 25);
ctx.stroke();
ctx.lineTo(x2, y1 + 35);
ctx.stroke();
const gradient = ctx.createLinearGradient(x1, y1 + 25, x2, y1 + 25);
gradient.addColorStop(0, one.color);
gradient.addColorStop(1, two.color);
ctx.strokeStyle = gradient;
ctx.lineTo(x2, y1 + 25);
ctx.stroke();
ctx.lineTo(x2, y2);
ctx.stroke();
ctx.lineTo(x2, y2);
ctx.stroke();
} else {
const gradient = ctx.createLinearGradient(x1, y1, x2, y2);
gradient.addColorStop(0, one.color);
gradient.addColorStop(1, two.color);
ctx.strokeStyle = gradient;
ctx.lineTo(x2, y2);
ctx.stroke();
} else {
ctx.lineTo(x2, y2);
ctx.stroke();
}
});
}
}
});
});
},
methods: {
makeChunks(text) {
const chunks = []
@ -290,6 +303,8 @@ export default {
},
open(e) {
this.$emit('abortNewLink');
this.setSpanInfo()
if (this.validateSpan()) {
this.show(e)
@ -314,7 +329,7 @@ export default {
}
.highlight-container {
line-height: 50px !important;
line-height: 70px !important;
display: flex;
flex-wrap: wrap;
white-space: pre-wrap;

81
frontend/domain/models/links/link.ts

@ -0,0 +1,81 @@
export class LinkItemList {
constructor(public LinkItems: LinkItem[]) {}
static valueOf(items: LinkItem[]): LinkItemList {
return new LinkItemList(items)
}
add(item: LinkItem) {
this.LinkItems.push(item)
}
update(item: LinkItem) {
const index = this.LinkItems.findIndex(label => label.id === item.id)
this.LinkItems.splice(index, 1, item)
}
delete(item: LinkItem) {
this.LinkItems = this.LinkItems.filter(label => label.id !== item.id)
}
bulkDelete(items: LinkItemList) {
const ids = items.ids()
this.LinkItems = this.LinkItems.filter(label => !ids.includes(label.id))
}
count(): Number {
return this.LinkItems.length
}
ids(): Number[]{
return this.LinkItems.map(item => item.id)
}
get nameList(): string[] {
return this.LinkItems.map(item => item.name)
}
get usedKeys(): string[] {
const items = this.LinkItems
.filter(item => item.suffixKey !== null)
.map(item => item.suffixKey) as string[]
return items
}
toArray(): Object[] {
return this.LinkItems.map(item => item.toObject())
}
}
export class LinkItem {
constructor(
public id: number,
public text: string,
public prefixKey: string | null,
public suffixKey: string | null,
public backgroundColor: string,
public textColor: string = '#ffffff'
) {}
static valueOf(
{ id, text, prefix_key, suffix_key, background_color, text_color }:
{ id: number, text: string, prefix_key: string, suffix_key: string, background_color: string, text_color: string }
): LinkItem {
return new LinkItem(id, text, prefix_key, suffix_key, background_color, text_color)
}
get name(): string {
return this.text
}
toObject(): Object {
return {
id: this.id,
text: this.text,
prefix_key: this.prefixKey,
suffix_key: this.suffixKey,
background_color: this.backgroundColor,
text_color: this.textColor
}
}
}

11
frontend/domain/models/links/linksRepository.ts

@ -0,0 +1,11 @@
import { LinkItem } from '~/domain/models/links/link'
export interface LinksRepository {
list(projectId: string): Promise<LinkItem[]>
create(projectId: string, item: LinkItem): Promise<LinkItem>
update(projectId: string, item: LinkItem): Promise<LinkItem>
bulkDelete(projectId: string, linkIds: number[]): Promise<void>
}

169
frontend/pages/projects/_id/links/index.vue

@ -0,0 +1,169 @@
<template>
<v-card>
<v-card-title>
<action-menu
@create="dialogCreate=true"
@upload="dialogUpload=true"
/>
<v-btn
class="text-capitalize ms-2"
:disabled="!canDelete"
outlined
@click.stop="dialogDelete=true"
>
{{ $t('generic.delete') }}
</v-btn>
<v-dialog v-model="dialogCreate">
<form-create
v-bind.sync="editedItem"
:used-keys="usedKeys"
:used-names="usedNames"
@cancel="close"
@save="save"
/>
</v-dialog>
<v-dialog v-model="dialogDelete">
<form-delete
:selected="selected"
@cancel="dialogDelete=false"
@remove="remove"
/>
</v-dialog>
</v-card-title>
<p>WORK IN PROGRESS... THIS WILL BECOME THE RELATIONS TYPE CRUD PAGE</p>
<links-list
v-model="selected"
:items="items"
:is-loading="isLoading"
@edit="editItem"
/>
</v-card>
</template>
<script lang="ts">
import Vue from 'vue'
import ActionMenu from '@/components/links/ActionMenu.vue'
import FormCreate from '@/components/links/FormCreate.vue'
import FormDelete from '@/components/links/FormDelete.vue'
import LinksList from '~/components/links/LinksList.vue'
import { LinkDTO } from '~/services/application/links/linkData'
export default Vue.extend({
layout: 'project',
components: {
ActionMenu,
FormCreate,
FormDelete,
LinksList
},
async fetch() {
this.isLoading = true
this.items = await this.$services.links.list(this.projectId)
this.isLoading = false
},
data() {
return {
dialogCreate: false,
dialogDelete: false,
dialogUpload: false,
editedIndex: -1,
editedItem: {
text: '',
prefixKey: null,
suffixKey: null,
backgroundColor: '#2196F3',
textColor: '#ffffff'
} as LinkDTO,
defaultItem: {
text: '',
prefixKey: null,
suffixKey: null,
backgroundColor: '#2196F3',
textColor: '#ffffff'
} as LinkDTO,
items: [] as LinkDTO[],
selected: [] as LinkDTO[],
isLoading: false,
errorMessage: ''
}
},
computed: {
canDelete(): boolean {
return this.selected.length > 0
},
projectId(): string {
return this.$route.params.id
},
usedNames(): string[] {
const item = this.items[this.editedIndex] // to remove myself
return this.items.filter(_ => _ !== item).map(item => item.text)
},
usedKeys(): string[] {
const item = this.items[this.editedIndex] // to remove myself
return this.items.filter(_ => _ !== item).map(item => item.suffixKey)
.filter(item => item !==null) as string[]
}
},
methods: {
async create() {
await this.$services.links.create(this.projectId, this.editedItem)
},
async update() {
await this.$services.links.update(this.projectId, this.editedItem)
},
save() {
if (this.editedIndex > -1) {
this.update()
} else {
this.create()
}
this.$fetch()
this.close()
},
close() {
this.dialogCreate = false
this.$nextTick(() => {
this.editedItem = Object.assign({}, this.defaultItem)
this.editedIndex = -1
})
},
async remove() {
await this.$services.links.bulkDelete(this.projectId, this.selected)
this.$fetch()
this.dialogDelete = false
this.selected = []
},
clearErrorMessage() {
this.errorMessage = ''
},
editItem(item: LinkDTO) {
this.editedIndex = this.items.indexOf(item)
this.editedItem = Object.assign({}, item)
this.dialogCreate = true
}
},
validate({ params }) {
return /^\d+$/.test(params.id)
}
})
</script>
<style scoped>
::v-deep .v-dialog {
width: 800px;
}
</style>

94
frontend/pages/projects/_id/sequence-labeling/index.vue

@ -2,39 +2,43 @@
<layout-text v-if="doc.id">
<template v-slot:header>
<toolbar-laptop
:doc-id="doc.id"
:enable-auto-labeling.sync="enableAutoLabeling"
:guideline-text="project.guideline"
:is-reviewd="doc.isApproved"
:show-approve-button="project.permitApprove"
:total="docs.count"
class="d-none d-sm-block"
@click:clear-label="clear"
@click:review="approve"
:doc-id="doc.id"
:enable-auto-labeling.sync="enableAutoLabeling"
:guideline-text="project.guideline"
:is-reviewd="doc.isApproved"
:show-approve-button="project.permitApprove"
:total="docs.count"
class="d-none d-sm-block"
@click:clear-label="clear"
@click:review="approve"
/>
<toolbar-mobile
:total="docs.count"
class="d-flex d-sm-none"
:total="docs.count"
class="d-flex d-sm-none"
/>
</template>
<template v-slot:content >
<template v-slot:content>
<v-card>
<v-card-text class="title">
<entity-item-box
:labels="labels"
:text="doc.text"
:entities="annotations"
:delete-annotation="remove"
:update-entity="update"
:add-entity="add"
:selected-chunk-id="selectedChunkId"
:select-link-source="selectLinkSource"
:labels="labels"
:text="doc.text"
:entities="annotations"
:delete-annotation="remove"
:update-entity="update"
:add-entity="add"
:source-chunk="sourceChunk"
:selected-link-type="selectedLinkType"
:select-source="selectSource"
:select-target="selectTarget"
:select-link-type="selectLinkType"
:abort-new-link="abortNewLink"
/>
</v-card-text>
</v-card>
</template>
<template v-slot:sidebar>
<list-metadata :metadata="JSON.parse(doc.meta)" />
<list-metadata :metadata="JSON.parse(doc.meta)"/>
</template>
</layout-text>
</template>
@ -60,11 +64,11 @@ export default {
async fetch() {
this.docs = await this.$services.document.fetchOne(
this.projectId,
this.$route.query.page,
this.$route.query.q,
this.$route.query.isChecked,
this.project.filterOption
this.projectId,
this.$route.query.page,
this.$route.query.q,
this.$route.query.isChecked,
this.project.filterOption
)
const doc = this.docs.items[0]
if (this.enableAutoLabeling) {
@ -80,7 +84,10 @@ export default {
labels: [],
project: {},
enableAutoLabeling: false,
selectedChunkId: -1
sourceChunk: {
none: true
},
selectedLinkType: -1
}
},
@ -116,20 +123,24 @@ export default {
methods: {
async list(docId) {
this.abortNewLink();
this.annotations = await this.$services.sequenceLabeling.list(this.projectId, docId)
},
async remove(id) {
this.abortNewLink();
await this.$services.sequenceLabeling.delete(this.projectId, this.doc.id, id)
await this.list(this.doc.id)
},
async add(startOffset, endOffset, labelId) {
this.abortNewLink();
await this.$services.sequenceLabeling.create(this.projectId, this.doc.id, labelId, startOffset, endOffset)
await this.list(this.doc.id)
},
async update(labelId, annotationId) {
this.abortNewLink();
await this.$services.sequenceLabeling.changeLabel(this.projectId, this.doc.id, annotationId, labelId)
await this.list(this.doc.id)
},
@ -153,19 +164,34 @@ export default {
await this.$fetch()
},
selectLinkSource(chunk) {
console.log(chunk.id);
console.log(this.selectedChunkId);
selectSource(chunk) {
this.sourceChunk = chunk;
},
if (this.selectedChunkId !== -1) {
console.log('aggiungi link [' + this.selectedChunkId + ', ' + chunk.id + ']');
selectTarget(chunk) {
// skips links duplicates
if (!chunk.links.find(ch => ch.id === this.sourceChunk.id)) {
this.sourceChunk.links.push({
id: chunk.id,
type: this.selectedLinkType
});
}
this.abortNewLink();
},
selectLinkType(type) {
this.selectedLinkType = type;
},
this.selectedChunkId = (this.selectedChunkId === chunk.id) ? -1 : chunk.id;
abortNewLink() {
this.sourceChunk = {
none: true
};
this.selectedLinkType = -1;
}
},
validate({ params, query }) {
validate({params, query}) {
return /^\d+$/.test(params.id) && /^\d+$/.test(query.page)
}
}

202
frontend/plugins/services.ts

@ -1,110 +1,116 @@
import { Plugin } from '@nuxt/types'
import { APISequenceLabelingRepository } from '~/repositories/tasks/sequenceLabeling/apiSequenceLabeling'
import { APISeq2seqRepository } from '~/repositories/tasks/seq2seq/apiSeq2seq'
import { APIConfigRepository } from '~/repositories/autoLabeling/config/apiConfigRepository'
import { APITemplateRepository } from '~/repositories/autoLabeling/template/apiTemplateRepository'
import { APIUserRepository } from '~/repositories/user/apiUserRepository'
import { APIStatisticsRepository } from '~/repositories/statistics/apiStatisticsRepository'
import { APIRoleRepository } from '~/repositories/role/apiRoleRepository'
import { APIProjectRepository } from '~/repositories/project/apiProjectRepository'
import { LocalStorageOptionRepository} from '~/repositories/option/apiOptionRepository'
import { APIMemberRepository } from '~/repositories/member/apiMemberRepository'
import { APILabelRepository } from '~/repositories/label/apiLabelRepository'
import { APIDocumentRepository } from '~/repositories/document/apiDocumentRepository'
import { APICommentRepository } from '~/repositories/comment/apiCommentRepository'
import { APIAuthRepository } from '~/repositories/auth/apiAuthRepository'
import { LabelApplicationService } from '~/services/application/label/labelApplicationService'
import { MemberApplicationService } from '~/services/application/member/memberApplicationService'
import { UserApplicationService } from '~/services/application/user/userApplicationService'
import { RoleApplicationService } from '~/services/application/role/roleApplicationService'
import { ProjectApplicationService } from '~/services/application/project/projectApplicationService'
import { CommentApplicationService } from '~/services/application/comment/commentApplicationService'
import { StatisticsApplicationService } from '~/services/application/statistics/statisticsApplicationService'
import { DocumentApplicationService } from '~/services/application/document/documentApplicationService'
import { OptionApplicationService } from '~/services/application/option/optionApplicationService'
import { SequenceLabelingApplicationService } from '~/services/application/tasks/sequenceLabeling/sequenceLabelingApplicationService'
import { Seq2seqApplicationService } from '~/services/application/tasks/seq2seq/seq2seqApplicationService'
import { ConfigApplicationService } from '~/services/application/autoLabeling/configApplicationService'
import { TemplateApplicationService } from '~/services/application/autoLabeling/templateApplicationService'
import { APITextClassificationRepository } from '~/repositories/tasks/textClassification/apiTextClassification'
import { TextClassificationApplicationService } from '~/services/application/tasks/textClassification/textClassificationApplicationService'
import { AuthApplicationService } from '~/services/application/auth/authApplicationService'
import {Plugin} from '@nuxt/types'
import {APISequenceLabelingRepository} from '~/repositories/tasks/sequenceLabeling/apiSequenceLabeling'
import {APISeq2seqRepository} from '~/repositories/tasks/seq2seq/apiSeq2seq'
import {APIConfigRepository} from '~/repositories/autoLabeling/config/apiConfigRepository'
import {APITemplateRepository} from '~/repositories/autoLabeling/template/apiTemplateRepository'
import {APIUserRepository} from '~/repositories/user/apiUserRepository'
import {APIStatisticsRepository} from '~/repositories/statistics/apiStatisticsRepository'
import {APIRoleRepository} from '~/repositories/role/apiRoleRepository'
import {APIProjectRepository} from '~/repositories/project/apiProjectRepository'
import {LocalStorageOptionRepository} from '~/repositories/option/apiOptionRepository'
import {APIMemberRepository} from '~/repositories/member/apiMemberRepository'
import {APILabelRepository} from '~/repositories/label/apiLabelRepository'
import {ApiLinksRepository} from "~/repositories/links/apiLinksRepository";
import {APIDocumentRepository} from '~/repositories/document/apiDocumentRepository'
import {APICommentRepository} from '~/repositories/comment/apiCommentRepository'
import {APIAuthRepository} from '~/repositories/auth/apiAuthRepository'
import {LabelApplicationService} from '~/services/application/label/labelApplicationService'
import {LinksApplicationService} from "~/services/application/links/linksApplicationService";
import {MemberApplicationService} from '~/services/application/member/memberApplicationService'
import {UserApplicationService} from '~/services/application/user/userApplicationService'
import {RoleApplicationService} from '~/services/application/role/roleApplicationService'
import {ProjectApplicationService} from '~/services/application/project/projectApplicationService'
import {CommentApplicationService} from '~/services/application/comment/commentApplicationService'
import {StatisticsApplicationService} from '~/services/application/statistics/statisticsApplicationService'
import {DocumentApplicationService} from '~/services/application/document/documentApplicationService'
import {OptionApplicationService} from '~/services/application/option/optionApplicationService'
import {SequenceLabelingApplicationService} from '~/services/application/tasks/sequenceLabeling/sequenceLabelingApplicationService'
import {Seq2seqApplicationService} from '~/services/application/tasks/seq2seq/seq2seqApplicationService'
import {ConfigApplicationService} from '~/services/application/autoLabeling/configApplicationService'
import {TemplateApplicationService} from '~/services/application/autoLabeling/templateApplicationService'
import {APITextClassificationRepository} from '~/repositories/tasks/textClassification/apiTextClassification'
import {TextClassificationApplicationService} from '~/services/application/tasks/textClassification/textClassificationApplicationService'
import {AuthApplicationService} from '~/services/application/auth/authApplicationService'
export interface Services {
label: LabelApplicationService,
member: MemberApplicationService,
user: UserApplicationService,
role: RoleApplicationService,
project: ProjectApplicationService,
comment: CommentApplicationService,
statistics: StatisticsApplicationService,
document: DocumentApplicationService,
textClassification: TextClassificationApplicationService,
sequenceLabeling: SequenceLabelingApplicationService,
seq2seq: Seq2seqApplicationService,
option: OptionApplicationService,
config: ConfigApplicationService,
template: TemplateApplicationService,
auth: AuthApplicationService
label: LabelApplicationService,
links: LinksApplicationService,
member: MemberApplicationService,
user: UserApplicationService,
role: RoleApplicationService,
project: ProjectApplicationService,
comment: CommentApplicationService,
statistics: StatisticsApplicationService,
document: DocumentApplicationService,
textClassification: TextClassificationApplicationService,
sequenceLabeling: SequenceLabelingApplicationService,
seq2seq: Seq2seqApplicationService,
option: OptionApplicationService,
config: ConfigApplicationService,
template: TemplateApplicationService,
auth: AuthApplicationService
}
declare module 'vue/types/vue' {
interface Vue {
readonly $services: Services
}
interface Vue {
readonly $services: Services
}
}
const plugin: Plugin = (context, inject) => {
const labelRepository = new APILabelRepository()
const memberRepository = new APIMemberRepository()
const userRepository = new APIUserRepository()
const roleRepository = new APIRoleRepository()
const projectRepository = new APIProjectRepository()
const commentRepository = new APICommentRepository()
const statisticsRepository = new APIStatisticsRepository()
const documentRepository = new APIDocumentRepository()
const textClassificationRepository = new APITextClassificationRepository()
const sequenceLabelingRepository = new APISequenceLabelingRepository()
const seq2seqRepository = new APISeq2seqRepository()
const optionRepository = new LocalStorageOptionRepository()
const configRepository = new APIConfigRepository()
const templateRepository = new APITemplateRepository()
const authRepository = new APIAuthRepository()
const labelRepository = new APILabelRepository()
const linksRepository = new ApiLinksRepository()
const memberRepository = new APIMemberRepository()
const userRepository = new APIUserRepository()
const roleRepository = new APIRoleRepository()
const projectRepository = new APIProjectRepository()
const commentRepository = new APICommentRepository()
const statisticsRepository = new APIStatisticsRepository()
const documentRepository = new APIDocumentRepository()
const textClassificationRepository = new APITextClassificationRepository()
const sequenceLabelingRepository = new APISequenceLabelingRepository()
const seq2seqRepository = new APISeq2seqRepository()
const optionRepository = new LocalStorageOptionRepository()
const configRepository = new APIConfigRepository()
const templateRepository = new APITemplateRepository()
const authRepository = new APIAuthRepository()
const label = new LabelApplicationService(labelRepository)
const member = new MemberApplicationService(memberRepository)
const user = new UserApplicationService(userRepository)
const role = new RoleApplicationService(roleRepository)
const project = new ProjectApplicationService(projectRepository)
const comment = new CommentApplicationService(commentRepository)
const statistics = new StatisticsApplicationService(statisticsRepository)
const document = new DocumentApplicationService(documentRepository)
const textClassification = new TextClassificationApplicationService(textClassificationRepository)
const sequenceLabeling = new SequenceLabelingApplicationService(sequenceLabelingRepository)
const seq2seq = new Seq2seqApplicationService(seq2seqRepository)
const option = new OptionApplicationService(optionRepository)
const config = new ConfigApplicationService(configRepository)
const template = new TemplateApplicationService(templateRepository)
const auth = new AuthApplicationService(authRepository)
const services: Services = {
label,
member,
user,
role,
project,
comment,
statistics,
document,
textClassification,
sequenceLabeling,
seq2seq,
option,
config,
template,
auth
}
inject('services', services)
const label = new LabelApplicationService(labelRepository)
const links = new LinksApplicationService(linksRepository)
const member = new MemberApplicationService(memberRepository)
const user = new UserApplicationService(userRepository)
const role = new RoleApplicationService(roleRepository)
const project = new ProjectApplicationService(projectRepository)
const comment = new CommentApplicationService(commentRepository)
const statistics = new StatisticsApplicationService(statisticsRepository)
const document = new DocumentApplicationService(documentRepository)
const textClassification = new TextClassificationApplicationService(textClassificationRepository)
const sequenceLabeling = new SequenceLabelingApplicationService(sequenceLabelingRepository)
const seq2seq = new Seq2seqApplicationService(seq2seqRepository)
const option = new OptionApplicationService(optionRepository)
const config = new ConfigApplicationService(configRepository)
const template = new TemplateApplicationService(templateRepository)
const auth = new AuthApplicationService(authRepository)
const services: Services = {
label,
links,
member,
user,
role,
project,
comment,
statistics,
document,
textClassification,
sequenceLabeling,
seq2seq,
option,
config,
template,
auth
}
inject('services', services)
}
export default plugin

45
frontend/repositories/links/apiLinksRepository.ts

@ -0,0 +1,45 @@
import ApiService from '@/services/api.service'
import { LinksRepository } from "~/domain/models/links/linksRepository";
import { LinkItem } from "~/domain/models/links/link";
export interface LinkItemResponse {
id: number,
text: string,
prefix_key: string,
suffix_key: string,
background_color: string,
text_color: string
}
export class ApiLinksRepository implements LinksRepository {
constructor(
private readonly request = ApiService
) {}
async list(projectId: string): Promise<LinkItem[]> {
const url = `/projects/${projectId}/relation_types`
const response = await this.request.get(url)
console.log(response);
const responseItems: LinkItemResponse[] = response.data
return responseItems.map(item => LinkItem.valueOf(item))
}
async create(projectId: string, item: LinkItem): Promise<LinkItem> {
const url = `/projects/${projectId}/relation_types`
const response = await this.request.post(url, item.toObject())
const responseItem: LinkItemResponse = response.data
return LinkItem.valueOf(responseItem)
}
async update(projectId: string, item: LinkItem): Promise<LinkItem> {
const url = `/projects/${projectId}/relation_types/${item.id}`
const response = await this.request.patch(url, item.toObject())
const responseItem: LinkItemResponse = response.data
return LinkItem.valueOf(responseItem)
}
async bulkDelete(projectId: string, linkIds: number[]): Promise<void> {
const url = `/projects/${projectId}/relation_types`
await this.request.delete(url, { ids: linkIds })
}
}

19
frontend/services/application/links/linkData.ts

@ -0,0 +1,19 @@
import { LinkItem } from '~/domain/models/links/link'
export class LinkDTO {
id: number
text: string
prefixKey: string | null
suffixKey: string | null
backgroundColor: string
textColor: string
constructor(item: LinkItem) {
this.id = item.id
this.text = item.text
this.prefixKey = item.prefixKey
this.suffixKey = item.suffixKey
this.backgroundColor = item.backgroundColor
this.textColor = '#ffffff'
}
}

30
frontend/services/application/links/linksApplicationService.ts

@ -0,0 +1,30 @@
import { LinkDTO } from './linkData';
import { LinksRepository } from '~/domain/models/links/linksRepository';
import { LinkItem } from '~/domain/models/links/link';
export class LinksApplicationService {
constructor(
private readonly repository: LinksRepository
) {}
public async list(id: string): Promise<LinkDTO[]> {
const items = await this.repository.list(id)
return items.map(item => new LinkDTO(item))
}
public create(projectId: string, item: LinkDTO): void {
const label = new LinkItem(0, item.text, item.prefixKey, item.suffixKey, item.backgroundColor, item.textColor)
this.repository.create(projectId, label)
}
public update(projectId: string, item: LinkDTO): void {
const label = new LinkItem(item.id, item.text, item.prefixKey, item.suffixKey, item.backgroundColor, item.textColor)
this.repository.update(projectId, label)
}
public bulkDelete(projectId: string, items: LinkDTO[]): Promise<void> {
const ids = items.map(item => item.id)
return this.repository.bulkDelete(projectId, ids)
}
}
Loading…
Cancel
Save