mirror of https://github.com/doccano/doccano.git
7 changed files with 433 additions and 3 deletions
Split View
Diff Options
-
180app/server/static/bundle/demo_named_entity.js
-
214app/server/static/js/demo/demo_named_entity.js
-
2app/server/templates/base.html
-
30app/server/templates/demo/demo_named_entity.html
-
3app/server/urls.py
-
4app/server/views.py
-
3app/server/webpack.config.js
180
app/server/static/bundle/demo_named_entity.js
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,214 @@ |
|||
import Vue from 'vue'; |
|||
import annotationMixin from './demo_mixin'; |
|||
|
|||
Vue.use(require('vue-shortkey'), { |
|||
prevent: ['input', 'textarea'], |
|||
}); |
|||
|
|||
Vue.component('annotator', { |
|||
template: '<div @click="setSelectedRange">\ |
|||
<span v-for="r in chunks"\ |
|||
v-bind:class="{tag: id2label[r.label].text_color}"\ |
|||
v-bind:style="{ color: id2label[r.label].text_color, backgroundColor: id2label[r.label].background_color }"\ |
|||
>{{ text.slice(r.start_offset, r.end_offset) }}<button class="delete is-small"\ |
|||
v-if="id2label[r.label].text_color"\ |
|||
@click="removeLabel(r)"></button></span>\ |
|||
</div>', |
|||
props: { |
|||
labels: Array, // [{id: Integer, color: String, text: String}]
|
|||
text: String, |
|||
entityPositions: Array, // [{'startOffset': 10, 'endOffset': 15, 'label_id': 1}]
|
|||
}, |
|||
data() { |
|||
return { |
|||
startOffset: 0, |
|||
endOffset: 0, |
|||
}; |
|||
}, |
|||
|
|||
methods: { |
|||
setSelectedRange(e) { |
|||
let start; |
|||
let end; |
|||
if (window.getSelection) { |
|||
const range = window.getSelection().getRangeAt(0); |
|||
const preSelectionRange = range.cloneRange(); |
|||
preSelectionRange.selectNodeContents(this.$el); |
|||
preSelectionRange.setEnd(range.startContainer, range.startOffset); |
|||
start = preSelectionRange.toString().length; |
|||
end = start + range.toString().length; |
|||
} else if (document.selection && document.selection.type !== 'Control') { |
|||
const selectedTextRange = document.selection.createRange(); |
|||
const preSelectionTextRange = document.body.createTextRange(); |
|||
preSelectionTextRange.moveToElementText(this.$el); |
|||
preSelectionTextRange.setEndPoint('EndToStart', selectedTextRange); |
|||
start = preSelectionTextRange.text.length; |
|||
end = start + selectedTextRange.text.length; |
|||
} |
|||
this.startOffset = start; |
|||
this.endOffset = end; |
|||
console.log(start, end); |
|||
}, |
|||
|
|||
validRange() { |
|||
if (this.startOffset === this.endOffset) { |
|||
return false; |
|||
} |
|||
if (this.startOffset > this.text.length || this.endOffset > this.text.length) { |
|||
return false; |
|||
} |
|||
if (this.startOffset < 0 || this.endOffset < 0) { |
|||
return false; |
|||
} |
|||
return true; |
|||
}, |
|||
|
|||
resetRange() { |
|||
this.startOffset = 0; |
|||
this.endOffset = 0; |
|||
}, |
|||
|
|||
addLabel(labelId) { |
|||
if (this.validRange()) { |
|||
const label = { |
|||
start_offset: this.startOffset, |
|||
end_offset: this.endOffset, |
|||
label: labelId, |
|||
}; |
|||
this.$emit('add-label', label); |
|||
} |
|||
}, |
|||
|
|||
removeLabel(index) { |
|||
this.$emit('remove-label', index); |
|||
}, |
|||
|
|||
makeLabel(startOffset, endOffset) { |
|||
const label = { |
|||
id: 0, |
|||
label: -1, |
|||
start_offset: startOffset, |
|||
end_offset: endOffset, |
|||
}; |
|||
return label; |
|||
}, |
|||
}, |
|||
|
|||
watch: { |
|||
entityPositions() { |
|||
this.resetRange(); |
|||
}, |
|||
}, |
|||
|
|||
computed: { |
|||
sortedEntityPositions() { |
|||
this.entityPositions = this.entityPositions.sort((a, b) => a.start_offset - b.start_offset); |
|||
return this.entityPositions; |
|||
}, |
|||
|
|||
chunks() { |
|||
const res = []; |
|||
let left = 0; |
|||
for (let i = 0; i < this.sortedEntityPositions.length; i++) { |
|||
const e = this.sortedEntityPositions[i]; |
|||
const l = this.makeLabel(left, e.start_offset); |
|||
res.push(l); |
|||
res.push(e); |
|||
left = e.end_offset; |
|||
} |
|||
const l = this.makeLabel(left, this.text.length); |
|||
res.push(l); |
|||
|
|||
return res; |
|||
}, |
|||
|
|||
id2label() { |
|||
let id2label = {}; |
|||
// default value;
|
|||
id2label[-1] = { |
|||
text_color: '', |
|||
background_color: '', |
|||
}; |
|||
for (let i = 0; i < this.labels.length; i++) { |
|||
const label = this.labels[i]; |
|||
id2label[label.id] = label; |
|||
} |
|||
return id2label; |
|||
}, |
|||
}, |
|||
}); |
|||
|
|||
const vm = new Vue({ |
|||
el: '#mail-app', |
|||
delimiters: ['[[', ']]'], |
|||
mixins: [annotationMixin], |
|||
data: { |
|||
docs: [{ |
|||
id: 1, |
|||
text: 'This is a document for named entity recognition.', |
|||
}, |
|||
{ |
|||
id: 10, |
|||
text: 'This is a sentence.', |
|||
}, |
|||
{ |
|||
id: 11, |
|||
text: 'This is a sentence.', |
|||
}, |
|||
{ |
|||
id: 12, |
|||
text: 'This is a sentence.', |
|||
}, |
|||
{ |
|||
id: 13, |
|||
text: 'This is a sentence.', |
|||
}, |
|||
{ |
|||
id: 13, |
|||
text: 'This is a sentence.', |
|||
}, |
|||
], |
|||
labels: [ |
|||
{ |
|||
id: 1, |
|||
text: 'Negative', |
|||
shortcut: 'n', |
|||
background_color: '#ff0033', |
|||
text_color: '#ffffff', |
|||
}, |
|||
{ |
|||
id: 2, |
|||
text: 'Positive', |
|||
shortcut: 'p', |
|||
background_color: '#209cee', |
|||
text_color: '#ffffff', |
|||
}, |
|||
], |
|||
annotations: [ |
|||
[ |
|||
{ |
|||
id: 1, |
|||
prob: 0.0, |
|||
label: 1, |
|||
start_offset: 5, |
|||
end_offset: 10, |
|||
}, |
|||
], |
|||
[], |
|||
[], |
|||
[], |
|||
[], |
|||
[], |
|||
], |
|||
}, |
|||
|
|||
methods: { |
|||
annotate(labelId) { |
|||
this.$refs.annotator.addLabel(labelId); |
|||
}, |
|||
|
|||
addLabel(annotation) { |
|||
this.annotations[this.pageNumber].push(annotation); |
|||
}, |
|||
}, |
|||
}); |
@ -0,0 +1,30 @@ |
|||
{% extends "annotation/annotation_base.html" %} |
|||
{% load static %} |
|||
{% block annotation-area %} |
|||
<div class="card"> |
|||
<header class="card-header"> |
|||
<div class="card-header-title" style="padding:1.5rem;background-color:royalblue;"> |
|||
<div class="field is-grouped is-grouped-multiline"> |
|||
<div class="control" v-for="label in labels"> |
|||
<div class="tags has-addons"> |
|||
<a class="tag is-medium" v-bind:style="{ color: label.text_color, backgroundColor: label.background_color }" v-on:click="annotate(label.id)" |
|||
v-shortkey.once="[ label.shortcut ]" @shortkey="annotate(label.id)"> |
|||
[[ label.text ]] |
|||
</a> |
|||
<span class="tag is-medium">[[ label.shortcut ]]</span> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</header> |
|||
<div class="card-content"> |
|||
<div class="content" v-if="docs[pageNumber] && annotations[pageNumber]"> |
|||
<annotator ref="annotator" v-bind:labels="labels" v-bind:entity-positions="annotations[pageNumber]" v-bind:text="docs[pageNumber].text" |
|||
@remove-label="removeLabel" @add-label="addLabel"></annotator> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
{% endblock %} |
|||
{% block footer %} |
|||
<script src="{% static 'bundle/demo_named_entity.js' %}"></script> |
|||
{% endblock %} |
Write
Preview
Loading…
Cancel
Save