Browse Source

esperimenti sugli eventi delle annotazioni

pull/1384/head
mauro 4 years ago
parent
commit
a0f0406454
3 changed files with 234 additions and 69 deletions
  1. 95
      frontend/components/tasks/sequenceLabeling/EntityItem.vue
  2. 195
      frontend/components/tasks/sequenceLabeling/EntityItemBox.vue
  3. 13
      frontend/pages/projects/_id/sequence-labeling/index.vue

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

@ -1,32 +1,36 @@
<template> <template>
<v-menu <v-menu
v-if="label"
v-model="showMenu"
offset-y
v-if="label"
v-model="showMenu"
offset-y
> >
<template v-slot:activator="{ on }"> <template v-slot:activator="{ on }">
<span :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 :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"/>
</span> </span>
</template> </template>
<v-list <v-list
dense
min-width="150"
max-height="400"
class="overflow-y-auto"
dense
min-width="150"
max-height="400"
class="overflow-y-auto"
> >
<v-list-item <v-list-item
v-for="(item, i) in labels"
:key="i"
v-shortkey.once="[item.suffixKey]"
@shortkey="update(item)"
@click="update(item)"
v-for="(item, i) in labels"
:key="i"
v-shortkey.once="[item.suffixKey]"
@shortkey="update(item)"
@click="update(item)"
> >
<v-list-item-content> <v-list-item-content>
<v-list-item-title v-text="item.text" />
<v-list-item-title v-text="item.text"/>
</v-list-item-content> </v-list-item-content>
<v-list-item-action> <v-list-item-action>
<v-list-item-action-text v-text="item.suffixKey" />
<v-list-item-action-text v-text="item.suffixKey"/>
</v-list-item-action> </v-list-item-action>
</v-list-item> </v-list-item>
</v-list> </v-list>
@ -35,10 +39,15 @@
</template> </template>
<script> <script>
import { idealColor } from '~/plugins/utils.js'
import {idealColor} from '~/plugins/utils.js'
export default { export default {
props: { props: {
spanid: {
type: Number,
default: 0,
required: true
},
content: { content: {
type: String, type: String,
default: '', default: '',
@ -59,6 +68,10 @@ export default {
}, },
newline: { newline: {
type: Boolean type: Boolean
},
selectedChunkId: {
type: Number,
default: -1
} }
}, },
data() { data() {
@ -78,6 +91,10 @@ export default {
}, },
remove() { remove() {
this.$emit('remove') this.$emit('remove')
},
onLinkClick() {
this.$emit('selectLinkSource');
this.showMenu = false;
} }
} }
} }
@ -87,46 +104,76 @@ export default {
.highlight.blue { .highlight.blue {
background: #edf4fa !important; background: #edf4fa !important;
} }
.highlight.bottom { .highlight.bottom {
display: block; display: block;
white-space: normal; white-space: normal;
} }
.highlight:first-child { .highlight:first-child {
margin-left: 0; margin-left: 0;
} }
.highlight { .highlight {
border: 2px solid; border: 2px solid;
margin: 4px 6px 4px 3px; margin: 4px 6px 4px 3px;
vertical-align: middle; vertical-align: middle;
box-shadow: 2px 4px 20px rgba(0,0,0,.1);
box-shadow: 2px 4px 20px rgba(0, 0, 0, .1);
position: relative; position: relative;
cursor: default; cursor: default;
min-width: 26px; min-width: 26px;
line-height: 22px; line-height: 22px;
display: flex; display: flex;
} }
.highlight .delete { .highlight .delete {
top:-15px;
left:-13px;
position:absolute;
top: -15px;
left: -13px;
position: absolute;
display: none; display: none;
} }
.highlight:hover .delete { .highlight:hover .delete {
display: block; display: block;
} }
.highlight .target-selector:before {
content: 'L';
}
.highlight .target-selector {
position: absolute;
top: -12px;
right: -11px;
width: 20px;
background: rgba(0, 0, 0, 0.54);
color: #ffffff;
border-radius: 30px;
cursor: pointer;
text-align: center;
}
.highlight .target-selector.active {
display: block;
background: #008ad6;
color: #ffffff;
}
.highlight__content { .highlight__content {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
padding: 2px 2px 0px 6px;
padding: 2px 2px 0 6px;
background: #ffffff;
} }
.highlight.bottom .highlight__content:after { .highlight.bottom .highlight__content:after {
content: " "; content: " ";
padding-right: 3px; padding-right: 3px;
} }
.highlight__label { .highlight__label {
line-height: 14px; line-height: 14px;
padding-top: 1px;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
display: flex; display: flex;
@ -138,6 +185,7 @@ export default {
user-select: none; user-select: none;
color: white; color: white;
} }
.highlight__label::after { .highlight__label::after {
content: attr(data-label); content: attr(data-label);
display: block; display: block;
@ -145,6 +193,7 @@ export default {
-webkit-font-smoothing: subpixel-antialiased; -webkit-font-smoothing: subpixel-antialiased;
letter-spacing: .1em; letter-spacing: .1em;
} }
.newline { .newline {
width: 100%; width: 100%;
} }

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

@ -1,50 +1,59 @@
<template> <template>
<div class="highlight-container highlight-container--bottom-labels" @click="open" @touchend="open">
<entity-item
v-for="(chunk, i) in chunks"
:key="i"
:content="chunk.text"
:newline="chunk.newline"
:label="chunk.label"
:color="chunk.color"
:labels="labels"
@remove="deleteAnnotation(chunk.id)"
@update="updateEntity($event.id, chunk.id)"
/>
<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"
: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>
<canvas id="connections">
</canvas>
</div>
</template> </template>
<script> <script>
import EntityItem from './EntityItem' import EntityItem from './EntityItem'
export default { export default {
components: { components: {
EntityItem EntityItem
@ -79,6 +88,16 @@ export default {
type: Function, type: Function,
default: () => ([]), default: () => ([]),
required: true required: true
},
selectedChunkId: {
type: Number,
default: -1,
required: true
},
selectLinkSource: {
type: Function,
default: () => ([]),
required: true
} }
}, },
data() { data() {
@ -111,7 +130,8 @@ export default {
id: entity.id, id: entity.id,
label: label.text, label: label.text,
color: label.backgroundColor, color: label.backgroundColor,
text: piece
text: piece,
selectedAsLinkSource: false
}) })
} }
// add the rest of text. // add the rest of text.
@ -126,6 +146,71 @@ export default {
return obj return obj
} }
}, },
updated() {
this.$nextTick(() => {
// SIMULATION ONLY
// svuota il canvas adeguandolo alla dimensione reale del <div> col testo
const parentPos = document.getElementById('connections-wrapper').getBoundingClientRect();
const canvas = document.getElementById('connections');
canvas.width = parentPos.width;
canvas.height = parentPos.height;
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;
}
}
});
// 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-' + two.id).getBoundingClientRect();
const x2 = (childPos.x + childPos.width / 2) - parentPos.x;
const y2 = (childPos.y + childPos.height / 2) - parentPos.y;
ctx.lineWidth = 3;
ctx.moveTo(x1, y1);
if (y1 === y2) {
ctx.strokeStyle = one.color;
ctx.lineTo(x1, y1 + 25);
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();
} 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();
}
}
});
},
methods: { methods: {
makeChunks(text) { makeChunks(text) {
const chunks = [] const chunks = []
@ -152,6 +237,7 @@ export default {
}) })
return chunks return chunks
}, },
show(e) { show(e) {
e.preventDefault() e.preventDefault()
this.showMenu = false this.showMenu = false
@ -161,6 +247,7 @@ export default {
this.showMenu = true this.showMenu = true
}) })
}, },
setSpanInfo() { setSpanInfo() {
let selection let selection
// Modern browsers. // Modern browsers.
@ -180,6 +267,7 @@ export default {
this.start = [...preSelectionRange.toString()].length this.start = [...preSelectionRange.toString()].length
this.end = this.start + [...range.toString()].length this.end = this.start + [...range.toString()].length
}, },
validateSpan() { validateSpan() {
if ((typeof this.start === 'undefined') || (typeof this.end === 'undefined')) { if ((typeof this.start === 'undefined') || (typeof this.end === 'undefined')) {
return false return false
@ -200,12 +288,14 @@ export default {
} }
return true return true
}, },
open(e) { open(e) {
this.setSpanInfo() this.setSpanInfo()
if (this.validateSpan()) { if (this.validateSpan()) {
this.show(e) this.show(e)
} }
}, },
assignLabel(labelId) { assignLabel(labelId) {
if (this.validateSpan()) { if (this.validateSpan()) {
this.addEntity(this.start, this.end, labelId) this.addEntity(this.start, this.end, labelId)
@ -222,14 +312,31 @@ export default {
.highlight-container.highlight-container--bottom-labels { .highlight-container.highlight-container--bottom-labels {
align-items: flex-start; align-items: flex-start;
} }
.highlight-container { .highlight-container {
line-height: 42px!important;
line-height: 50px !important;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
white-space: pre-wrap; white-space: pre-wrap;
cursor: default; cursor: default;
position: relative;
z-index: 1;
} }
.highlight-container.highlight-container--bottom-labels .highlight.bottom { .highlight-container.highlight-container--bottom-labels .highlight.bottom {
margin-top: 6px; margin-top: 6px;
} }
#connections-wrapper {
position: relative;
}
#connections-wrapper canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
}
</style> </style>

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

@ -17,7 +17,7 @@
class="d-flex d-sm-none" class="d-flex d-sm-none"
/> />
</template> </template>
<template v-slot:content>
<template v-slot:content >
<v-card> <v-card>
<v-card-text class="title"> <v-card-text class="title">
<entity-item-box <entity-item-box
@ -27,6 +27,8 @@
:delete-annotation="remove" :delete-annotation="remove"
:update-entity="update" :update-entity="update"
:add-entity="add" :add-entity="add"
:selected-chunk-id="selectedChunkId"
:select-link-source="selectLinkSource"
/> />
</v-card-text> </v-card-text>
</v-card> </v-card>
@ -77,7 +79,8 @@ export default {
docs: [], docs: [],
labels: [], labels: [],
project: {}, project: {},
enableAutoLabeling: false
enableAutoLabeling: false,
selectedChunkId: -1
} }
}, },
@ -148,6 +151,12 @@ export default {
const approved = !this.doc.isApproved const approved = !this.doc.isApproved
await this.$services.document.approve(this.projectId, this.doc.id, approved) await this.$services.document.approve(this.projectId, this.doc.id, approved)
await this.$fetch() await this.$fetch()
},
selectLinkSource(chunk) {
console.log(chunk.id);
console.log(this.selectedChunkId);
this.selectedChunkId = (this.selectedChunkId === chunk.id) ? -1 : chunk.id;
} }
}, },

Loading…
Cancel
Save