Browse Source

Merge branch 'master' of https://github.com/chakki-works/doccano into authentification

pull/250/head
Guillim 5 years ago
parent
commit
7298650a2a
20 changed files with 348 additions and 701 deletions
  1. 9
      app/api/views.py
  2. 2
      app/server/static/components/annotation.pug
  3. 8
      app/server/static/components/annotationMixin.js
  4. 158
      app/server/static/components/demo/demo_annotator.vue
  5. 137
      app/server/static/components/demo/demo_api.js
  6. 264
      app/server/static/components/demo/demo_data.js
  7. 139
      app/server/static/components/demo/demo_mixin.js
  8. 62
      app/server/static/components/demo/demo_named_entity.vue
  9. 83
      app/server/static/components/demo/demo_text_classification.vue
  10. 96
      app/server/static/components/demo/demo_translation.vue
  11. 2
      app/server/static/components/filter.js
  12. 3
      app/server/static/components/http.js
  13. 2
      app/server/static/components/label.vue
  14. 14
      app/server/static/components/projects.vue
  15. 4
      app/server/static/components/uploadMixin.js
  16. 11
      app/server/static/package-lock.json
  17. 1
      app/server/static/package.json
  18. 18
      app/server/static/pages/demo_named_entity.js
  19. 18
      app/server/static/pages/demo_text_classification.js
  20. 18
      app/server/static/pages/demo_translation.js

9
app/api/views.py

@ -42,7 +42,6 @@ class Features(APIView):
class ProjectList(generics.ListCreateAPIView):
queryset = Project.objects.all()
serializer_class = ProjectPolymorphicSerializer
pagination_class = None
permission_classes = (IsAuthenticated, IsAdminUserAndWriteOnly)
@ -109,14 +108,13 @@ class ApproveLabelsAPI(APIView):
class LabelList(generics.ListCreateAPIView):
queryset = Label.objects.all()
serializer_class = LabelSerializer
pagination_class = None
permission_classes = (IsAuthenticated, IsProjectUser, IsAdminUserAndWriteOnly)
def get_queryset(self):
queryset = self.queryset.filter(project=self.kwargs['project_id'])
return queryset
project = get_object_or_404(Project, pk=self.kwargs['project_id'])
return project.labels
def perform_create(self, serializer):
project = get_object_or_404(Project, pk=self.kwargs['project_id'])
@ -131,7 +129,6 @@ class LabelDetail(generics.RetrieveUpdateDestroyAPIView):
class DocumentList(generics.ListCreateAPIView):
queryset = Document.objects.all()
serializer_class = DocumentSerializer
filter_backends = (DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter)
search_fields = ('text', )
@ -143,7 +140,7 @@ class DocumentList(generics.ListCreateAPIView):
def get_queryset(self):
project = get_object_or_404(Project, pk=self.kwargs['project_id'])
queryset = self.queryset.filter(project=project)
queryset = project.documents
if project.randomize_document_order:
queryset = queryset.annotate(sort_id=F('id') % self.request.user.id).order_by('sort_id')

2
app/server/static/components/annotation.pug

@ -145,6 +145,8 @@ div.columns(v-cloak="")
i.fas.fa-chevron-left
span Prev
span.button.level-center.is-static {{ offset + pageNumber + 1 }} / {{ count }}
a.button.level-right(
v-shortkey="{ next1: ['ctrl', 'n'], next2: ['arrowdown'], next3: ['arrowright'] }"
v-on:click="nextPage"

8
app/server/static/components/annotationMixin.js

@ -1,9 +1,7 @@
import * as marked from 'marked';
import VueJsonPretty from 'vue-json-pretty';
import isEmpty from 'lodash.isempty';
import HTTP, { rootUrl, newHttpClient } from './http';
const httpClient = newHttpClient();
import HTTP, { defaultHttpClient } from './http';
const getOffsetFromUrl = (url) => {
const offsetMatch = url.match(/[?#].*offset=(\d+)/);
@ -124,7 +122,7 @@ export default {
},
replaceNull(shortcut) {
if (shortcut === null) {
if (shortcut == null) {
shortcut = '';
}
shortcut = shortcut.split(' ');
@ -176,7 +174,7 @@ export default {
HTTP.get().then((response) => {
this.guideline = response.data.guideline;
});
httpClient.get(`${rootUrl}/v1/me`).then((response) => {
defaultHttpClient.get('/v1/me').then((response) => {
this.isSuperuser = response.data.is_superuser;
});
this.submit();

158
app/server/static/components/demo/demo_annotator.vue

@ -1,158 +0,0 @@
<template lang="pug">
div(v-on: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.delete.is-small(v-if="id2label[r.label].text_color", v-on:click="removeLabel(r)")
</template>
<script>
export default {
props: {
labels: {
type: Array, // [{id: Integer, color: String, text: String}]
default: () => [],
},
text: {
type: String,
default: '',
},
entityPositions: {
type: Array, // [{'startOffset': 10, 'endOffset': 15, 'label_id': 1}]
default: () => [],
},
},
data: () => ({
startOffset: 0,
endOffset: 0,
}),
computed: {
sortedEntityPositions() {
/* eslint-disable vue/no-side-effects-in-computed-properties */
this.entityPositions = this.entityPositions.sort((a, b) => a.start_offset - b.start_offset);
return this.entityPositions;
/* eslint-enable vue/no-side-effects-in-computed-properties */
},
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() {
const 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;
},
},
watch: {
entityPositions() {
this.resetRange();
},
},
methods: {
setSelectedRange() {
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); // eslint-disable-line no-console
},
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;
}
for (let i = 0; i < this.entityPositions.length; i++) {
const e = this.entityPositions[i];
if ((e.start_offset <= this.startOffset) && (this.startOffset <= e.end_offset)) {
return false;
}
if ((e.start_offset <= this.endOffset) && (this.endOffset <= e.end_offset)) {
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;
},
},
};
</script>

137
app/server/static/components/demo/demo_api.js

@ -0,0 +1,137 @@
import MockAdatper from 'axios-mock-adapter';
import HTTP, { defaultHttpClient } from '../http';
function newId() {
return Number((Math.random() * 100000).toFixed(0));
}
function parseOffset(url) {
const offset = url.match(/offset=(\d+)/);
return offset ? Number(offset[1]) : 0;
}
function parseDocId(url) {
return Number(url.split('/')[5]);
}
function parseAnnotationId(url) {
return Number(url.split('/')[7]);
}
export default class DemoApi {
constructor(data, labelField) {
this.data = data;
this.labelField = labelField;
this.mocks = [new MockAdatper(HTTP), new MockAdatper(defaultHttpClient)];
this.pageSize = 5;
}
getMe() {
return [200, this.data.me];
}
getLabels() {
return [200, this.data.labels];
}
getStatistics() {
return [200, {
total: this.data.docs.length,
remaining: this.data.docs.filter(doc => doc.annotations.length === 0).length,
}];
}
getDocs(config) {
const offset = parseOffset(config.url);
return [200, {
results: this.data.docs.slice(Math.max(offset - 1, 0), this.pageSize),
count: this.data.docs.length,
next: offset + this.pageSize <= this.data.docs.length
? config.url.replace(`offset=${offset}`, `offset=${offset + this.pageSize}`)
: null,
previous: offset - this.pageSize >= 0
? config.url.replace(`offset=${offset}`, `offset=${offset - this.pageSize}`)
: null,
}];
}
getProject() {
return [200, this.data.project];
}
postAnnotations(config) {
const docId = parseDocId(config.url);
const body = JSON.parse(config.data);
const doc = this.data.docs.find(_ => _.id === docId);
if (!doc) {
return [404, {}];
}
let annotation = doc.annotations.find(_ => _[this.labelField] === body[this.labelField]);
if (!annotation) {
annotation = { id: newId(), ...body };
doc.annotations.push(annotation);
}
return [200, annotation];
}
deleteAnnotations(config) {
const docId = parseDocId(config.url);
const annotationId = parseAnnotationId(config.url);
const doc = this.data.docs.find(_ => _.id === docId);
if (!doc) {
return [404, {}];
}
doc.annotations = doc.annotations.filter(el => el.id !== annotationId);
return [200, {}];
}
start() {
this.mocks.forEach((mock) => {
mock.onGet(/\/v1\/.*/g).reply((config) => {
if (config.url.endsWith('/me')) {
return this.getMe();
}
if (config.url.endsWith('/labels')) {
return this.getLabels();
}
if (config.url.endsWith('/statistics')) {
return this.getStatistics();
}
if (config.url.indexOf('/docs') !== -1) {
return this.getDocs(config);
}
return this.getProject();
});
mock.onPost(/\/v1\/.*/g).reply((config) => {
if (config.url.endsWith('/annotations')) {
return this.postAnnotations(config);
}
return [404, {}];
});
mock.onDelete(/\/v1\/.*/g).reply((config) => {
if (config.url.indexOf('/annotations') !== -1) {
return this.deleteAnnotations(config);
}
return [404, {}];
});
});
}
stop() {
this.mocks.forEach(mock => mock.reset());
}
}

264
app/server/static/components/demo/demo_data.js

@ -1,264 +1,274 @@
export const demoNamedEntity = {
project: {
guideline: '',
},
me: {
is_superuser: false,
},
docs: [
{
id: 1,
text: 'Barack Hussein Obama II (born August 4, 1961) is an American attorney and politician who served as the 44th President of the United States from January 20, 2009, to January 20, 2017. A member of the Democratic Party, he was the first African American to serve as president. He was previously a United States Senator from Illinois and a member of the Illinois State Senate.',
annotations: [
{
id: 16,
prob: 0.0,
label: 1,
start_offset: 0,
end_offset: 23,
},
{
id: 19,
prob: 0.0,
label: 2,
start_offset: 121,
end_offset: 138,
},
{
id: 27,
prob: 0.0,
label: 2,
start_offset: 321,
end_offset: 329,
},
{
id: 22,
prob: 0.0,
label: 3,
start_offset: 199,
end_offset: 215,
},
{
id: 28,
prob: 0.0,
label: 3,
start_offset: 350,
end_offset: 371,
},
{
id: 17,
prob: 0.0,
label: 5,
start_offset: 30,
end_offset: 44,
},
{
id: 20,
prob: 0.0,
label: 5,
start_offset: 144,
end_offset: 160,
},
{
id: 21,
prob: 0.0,
label: 5,
start_offset: 165,
end_offset: 181,
},
{
id: 18,
prob: 0.0,
label: 6,
start_offset: 52,
end_offset: 60,
},
{
id: 24,
prob: 0.0,
label: 6,
start_offset: 234,
end_offset: 250,
},
{
id: 26,
prob: 0.0,
label: 6,
start_offset: 294,
end_offset: 315,
},
],
},
{
id: 10,
text: 'The White House is the official residence and workplace of the President of the United States. It is located at 1600 Pennsylvania Avenue NW in Washington, D.C. and has been the residence of every U.S. President since John Adams in 1800. The term is often used as a metonym for the president and his advisers.',
annotations: [],
},
{
id: 11,
text: "The Democratic Party is one of the two major contemporary political parties in the United States, along with the Republican Party. Tracing its heritage back to Thomas Jefferson and James Madison's Democratic-Republican Party, the modern-day Democratic Party was founded around 1828 by supporters of Andrew Jackson, making it the world's oldest active political party.",
annotations: [],
},
{
id: 12,
text: "Stanford University (officially Leland Stanford Junior University, colloquially the Farm) is a private research university in Stanford, California. Stanford is known for its academic strength, wealth, proximity to Silicon Valley, and ranking as one of the world's top universities.",
annotations: [],
},
{
id: 13,
text: 'Donald John Trump (born June 14, 1946) is the 45th and current President of the United States. Before entering politics, he was a businessman and television personality.',
annotations: [],
},
{
id: 14,
text: "Silicon Valley (abbreviated as SV) is a region in the southern San Francisco Bay Area of Northern California, referring to the Santa Clara Valley, which serves as the global center for high technology, venture capital, innovation, and social media. San Jose is the Valley's largest city, the 3rd-largest in California, and the 10th-largest in the United States. Other major SV cities include Palo Alto, Santa Clara, Mountain View, and Sunnyvale. The San Jose Metropolitan Area has the third highest GDP per capita in the world (after Zurich, Switzerland and Oslo, Norway), according to the Brookings Institution.",
annotations: [],
},
],
labels: [
{
id: 1,
text: 'Person',
shortcut: 'p',
suffix_key: 'p',
background_color: '#209cee',
text_color: '#ffffff',
},
{
id: 2,
text: 'Loc',
shortcut: 'l',
suffix_key: 'l',
background_color: '#ffcc00',
text_color: '#333333',
},
{
id: 3,
text: 'Org',
shortcut: 'o',
suffix_key: 'o',
background_color: '#333333',
text_color: '#ffffff',
},
{
id: 4,
text: 'Event',
shortcut: 'e',
suffix_key: 'e',
background_color: '#33cc99',
text_color: '#ffffff',
},
{
id: 5,
text: 'Date',
shortcut: 'd',
suffix_key: 'd',
background_color: '#ff3333',
text_color: '#ffffff',
},
{
id: 6,
text: 'Other',
shortcut: 'z',
suffix_key: 'z',
background_color: '#9933ff',
text_color: '#ffffff',
},
],
annotations: [
[
{
id: 16,
prob: 0.0,
label: 1,
start_offset: 0,
end_offset: 23,
},
{
id: 19,
prob: 0.0,
label: 2,
start_offset: 121,
end_offset: 138,
},
{
id: 27,
prob: 0.0,
label: 2,
start_offset: 321,
end_offset: 329,
},
{
id: 22,
prob: 0.0,
label: 3,
start_offset: 199,
end_offset: 215,
},
{
id: 28,
prob: 0.0,
label: 3,
start_offset: 350,
end_offset: 371,
},
{
id: 17,
prob: 0.0,
label: 5,
start_offset: 30,
end_offset: 44,
},
{
id: 20,
prob: 0.0,
label: 5,
start_offset: 144,
end_offset: 160,
},
{
id: 21,
prob: 0.0,
label: 5,
start_offset: 165,
end_offset: 181,
},
{
id: 18,
prob: 0.0,
label: 6,
start_offset: 52,
end_offset: 60,
},
{
id: 24,
prob: 0.0,
label: 6,
start_offset: 234,
end_offset: 250,
},
{
id: 26,
prob: 0.0,
label: 6,
start_offset: 294,
end_offset: 315,
},
],
[],
[],
[],
[],
[],
],
};
export const demoTextClassification = {
me: {
is_superuser: false,
},
project: {
guideline: 'Insert text to provide labeling instructions to annotator here...',
},
docs: [
{
id: 1,
text: 'Fair drama/love story movie that focuses on the lives of blue collar people finding new life thru new love.The acting here is good but the film fails in cinematography,screenplay,directing and editing.The story/script is only average at best.This film will be enjoyed by Fonda and De Niro fans and by people who love middle age love stories where in the coartship is on a more wiser and cautious level.It would also be interesting for people who are interested on the subject matter regarding illiteracy.......',
annotations: [
{
id: 1,
label: 2,
},
],
},
{
id: 10,
text: "If you like adult comedy cartoons, like South Park, then this is nearly a similar format about the small adventures of three teenage girls at Bromwell High. Keisha, Natella and Latrina have given exploding sweets and behaved like bitches, I think Keisha is a good leader. There are also small stories going on with the teachers of the school. There's the idiotic principal, Mr. Bip, the nervous Maths teacher and many others. The cast is also fantastic, Lenny Henry's Gina Yashere, EastEnders Chrissie Watts, Tracy-Ann Oberman, Smack The Pony's Doon Mackichan, Dead Ringers' Mark Perry and Blunder's Nina Conti. I didn't know this came from Canada, but it is very good. Very good!",
annotations: [],
},
{
id: 11,
text: "I came in in the middle of this film so I had no idea about any credits or even its title till I looked it up here, where I see that it has received a mixed reception by your commentators. I'm on the positive side regarding this film but one thing really caught my attention as I watched: the beautiful and sensitive score written in a Coplandesque Americana style. My surprise was great when I discovered the score to have been written by none other than John Williams himself. True he has written sensitive and poignant scores such as Schindler's List but one usually associates his name with such bombasticities as Star Wars. But in my opinion what Williams has written for this movie surpasses anything I've ever heard of his for tenderness, sensitivity and beauty, fully in keeping with the tender and lovely plot of the movie. And another recent score of his, for Catch Me if You Can, shows still more wit and sophistication. As to Stanley and Iris, I like education movies like How Green was my Valley and Konrack, that one with John Voigt and his young African American charges in South Carolina, and Danny deVito's Renaissance Man, etc. They tell a necessary story of intellectual and spiritual awakening, a story which can't be told often enough. This one is an excellent addition to that genre.",
annotations: [],
},
{
id: 12,
text: "Story of a man who has unnatural feelings for a pig. Starts out with a opening scene that is a terrific example of absurd comedy. A formal orchestra audience is turned into an insane, violent mob by the crazy chantings of it's singers. Unfortunately it stays absurd the WHOLE time with no general narrative eventually making it just too off putting. Even those from the era should be turned off. The cryptic dialogue would make Shakespeare seem easy to a third grader. On a technical level it's better than you might think with some good cinematography by future great Vilmos Zsigmond. Future stars Sally Kirkland and Frederic Forrest can be seen briefly.",
annotations: [],
},
{
id: 13,
text: "Robert DeNiro plays the most unbelievably intelligent illiterate of all time. This movie is so wasteful of talent, it is truly disgusting. The script is unbelievable. The dialog is unbelievable. Jane Fonda's character is a caricature of herself, and not a funny one. The movie moves at a snail's pace, is photographed in an ill-advised manner, and is insufferably preachy. It also plugs in every cliche in the book. Swoozie Kurtz is excellent in a supporting role, but so what?<br /><br />Equally annoying is this new IMDB rule of requiring ten lines for every review. When a movie is this worthless, it doesn't require ten lines of text to let other readers know that it is a waste of time and tape. Avoid this movie.",
annotations: [],
},
{
id: 14,
text: 'From the beginning of the movie, it gives the feeling the director is trying to portray something, what I mean to say that instead of the story dictating the style in which the movie should be made, he has gone in the opposite way, he had a type of move that he wanted to make, and wrote a story to suite it. And he has failed in it very badly. I guess he was trying to make a stylish movie. Any way I think this movie is a total waste of time and effort. In the credit of the director, he knows the media that he is working with, what I am trying to say is I have seen worst movies than this. Here at least the director knows to maintain the continuity in the movie. And the actors also have given a decent performance.',
annotations: [],
},
],
labels: [
{
id: 1,
text: 'Negative',
shortcut: 'n',
suffix_key: 'n',
background_color: '#ff0033',
text_color: '#ffffff',
},
{
id: 2,
text: 'Positive',
shortcut: 'p',
suffix_key: 'p',
background_color: '#209cee',
text_color: '#ffffff',
},
],
annotations: [
[
{
id: 1,
label: 2,
},
],
[],
[],
[],
[],
[],
],
};
export const demoTranslation = {
newTodo: '',
editedTodo: null,
project: {
guideline: '',
},
me: {
is_superuser: false,
},
docs: [
{
id: 1,
text: 'If it had not been for his help, I would have failed.',
annotations: [
{
id: 1,
text: "S'il ne m'avait pas aidé, j'aurais échoué.",
},
{
id: 2,
text: "S'il ne m'avait pas aidée, j'aurais échoué.",
},
],
},
{
id: 10,
text: 'According to this magazine, my favorite actress will marry a jazz musician next spring.',
annotations: [],
},
{
id: 11,
text: "It's not always possible to eat well when you are traveling in this part of the world.",
annotations: [],
},
{
id: 12,
text: "It's still early. We should all just chill for a bit.",
annotations: [],
},
{
id: 13,
text: "She got a master's degree three years ago.",
annotations: [],
},
{
id: 13,
id: 14,
text: 'We adopted an alternative method.',
annotations: [],
},
],
annotations: [
[
{
id: 1,
text: "S'il ne m'avait pas aidé, j'aurais échoué.",
},
{
id: 2,
text: "S'il ne m'avait pas aidée, j'aurais échoué.",
},
],
[],
[],
[],
[],
[],
],
};

139
app/server/static/components/demo/demo_mixin.js

@ -1,139 +0,0 @@
import * as marked from 'marked';
const annotationMixin = {
data() {
return {
pageNumber: 0,
docs: [],
annotations: [],
labels: [],
tmp_docs: [],
tmp_annotations: [],
guideline: 'You can see annotation guideline here.',
searchQuery: '',
picked: 'all',
annotationId: 100,
isMetadataActive: false,
isAnnotationGuidelineActive: false,
};
},
methods: {
nextPage() {
this.pageNumber = Math.min(this.pageNumber + 1, this.docs.length - 1);
},
prevPage() {
this.pageNumber = Math.max(this.pageNumber - 1, 0);
},
search() {
this.docs = [];
this.annotations = [];
for (let i = 0; i < this.tmp_docs.length; i++) {
if (this.tmp_docs[i].text.indexOf(this.searchQuery) !== -1) {
if (this.picked === 'all') {
this.docs.push(this.tmp_docs[i]);
this.annotations.push(this.tmp_annotations[i]);
}
if (this.picked === 'active') {
if (this.tmp_annotations[i].length === 0) {
this.docs.push(this.tmp_docs[i]);
this.annotations.push(this.tmp_annotations[i]);
}
}
if (this.picked === 'completed') {
if (this.tmp_annotations[i].length !== 0) {
this.docs.push(this.tmp_docs[i]);
this.annotations.push(this.tmp_annotations[i]);
}
}
}
}
},
getState() {
if (this.picked === 'all') {
return '';
}
if (this.picked === 'active') {
return 'true';
}
return 'false';
},
submit() {
this.search();
this.pageNumber = 0;
},
removeLabel(annotation) {
const index = this.annotations[this.pageNumber].indexOf(annotation);
this.annotations[this.pageNumber].splice(index, 1);
},
},
watch: {
picked() {
this.submit();
},
},
created() {
this.tmp_docs = this.docs;
this.tmp_annotations = this.annotations;
},
computed: {
total() {
return this.tmp_docs.length;
},
count() {
return this.docs.length;
},
compiledMarkdown() {
return marked(this.guideline, {
sanitize: true,
});
},
remaining() {
let cnt = 0;
for (let i = 0; i < this.tmp_annotations.length; i++) {
if (this.tmp_annotations[i].length === 0) {
cnt += 1;
}
}
return cnt;
},
achievement() {
const done = this.total - this.remaining;
const percentage = Math.round(done / this.total * 100);
return this.total > 0 ? percentage : 0;
},
id2label() {
const id2label = {};
for (let i = 0; i < this.labels.length; i++) {
const label = this.labels[i];
id2label[label.id] = label;
}
return id2label;
},
progressColor() {
if (this.achievement < 30) {
return 'is-danger';
}
if (this.achievement < 70) {
return 'is-warning';
}
return 'is-primary';
},
},
};
export default annotationMixin;

62
app/server/static/components/demo/demo_named_entity.vue

@ -1,62 +0,0 @@
<template lang="pug">
extends ../annotation.pug
block annotation-area
div.card
header.card-header
div.card-header-title.has-background-royalblue
div.field.is-grouped.is-grouped-multiline
div.control(v-for="label in labels")
div.tags.has-addons
a.tag.is-medium(
v-shortkey.once="[ label.shortcut ]"
v-bind:style="{ \
color: label.text_color, \
backgroundColor: label.background_color \
}"
v-on:click="annotate(label.id)"
v-on:shortkey="annotate(label.id)"
) {{ label.text }}
span.tag.is-medium {{ label.shortcut }}
div.card-content
div.content(v-if="docs[pageNumber] && annotations[pageNumber]")
annotator(
v-bind:labels="labels"
v-bind:entity-positions="annotations[pageNumber]"
v-bind:text="docs[pageNumber].text"
v-on:remove-label="removeLabel"
v-on:add-label="addLabel"
ref="annotator"
)
</template>
<style scoped>
.card-header-title {
padding: 1.5rem;
}
</style>
<script>
import { demoNamedEntity } from './demo_data';
import annotationMixin from './demo_mixin';
import Annotator from './demo_annotator.vue';
export default {
components: { Annotator },
mixins: [annotationMixin],
data: () => ({ ...demoNamedEntity }),
methods: {
annotate(labelId) {
this.$refs.annotator.addLabel(labelId);
},
addLabel(annotation) {
this.annotations[this.pageNumber].push(annotation);
},
},
};
</script>

83
app/server/static/components/demo/demo_text_classification.vue

@ -1,83 +0,0 @@
<template lang="pug">
extends ../annotation.pug
block annotation-area
div.card
header.card-header
div.card-header-title.has-background-royalblue
div.field.is-grouped.is-grouped-multiline
div.control(v-for="label in labels")
div.tags.has-addons
a.tag.is-medium(
v-shortkey.once="[ label.shortcut ]"
v-bind:style="{ \
color: label.text_color, \
backgroundColor: label.background_color \
}"
v-on:click="addLabel(label)"
v-on:shortkey="addLabel(label)"
) {{ label.text }}
span.tag.is-medium {{ label.shortcut }}
div.card-content
div.field.is-grouped.is-grouped-multiline
div.control(v-for="annotation in annotations[pageNumber]")
div.tags.has-addons
span.tag.is-medium(
v-bind:style="{ \
color: id2label[annotation.label].text_color, \
backgroundColor: id2label[annotation.label].background_color \
}"
) {{ id2label[annotation.label].text }}
button.delete.is-small(v-on:click="removeLabel(annotation)")
hr
div.content(v-if="docs[pageNumber]") {{ docs[pageNumber].text }}
</template>
<style scoped>
.card-header-title {
padding: 1.5rem;
}
hr {
margin: 0.8rem 0;
}
</style>
<script>
import annotationMixin from './demo_mixin';
import { demoTextClassification } from './demo_data';
export default {
mixins: [annotationMixin],
data: () => ({ ...demoTextClassification }),
methods: {
isIn(label) {
for (let i = 0; i < this.annotations[this.pageNumber].length; i++) {
const a = this.annotations[this.pageNumber][i];
if (a.label === label.id) {
return a;
}
}
return false;
},
addLabel(label) {
const a = this.isIn(label);
if (a) {
this.removeLabel(a);
} else {
const annotation = {
id: this.annotationId++,
label: label.id,
};
this.annotations[this.pageNumber].push(annotation);
console.log(this.annotations); // eslint-disable-line no-console
}
},
},
};
</script>

96
app/server/static/components/demo/demo_translation.vue

@ -1,96 +0,0 @@
<template lang="pug">
extends ../annotation.pug
block annotation-area
div
div.card.has-text-weight-bold.has-text-white.has-background-royalblue
div.card-content
div.content(v-if="docs[pageNumber]") {{ docs[pageNumber].text }}
section.todoapp
header.header
input.textarea.new-todo(
v-model="newTodo"
v-on:keyup.enter="addTodo"
type="text"
placeholder="What is your response?"
)
section.main(v-cloak="")
ul.todo-list
li.todo(
v-for="todo in annotations[pageNumber]"
v-bind:key="todo.id"
v-bind:class="{ \
editing: todo == editedTodo \
}"
)
div.view
label(v-on:dblclick="editTodo(todo)") {{ todo.text }}
button.delete.destroy.is-large(v-on:click="removeTodo(todo)")
input.textarea.edit(
v-model="todo.text"
v-todo-focus="todo == editedTodo"
v-on:blur="doneEdit(todo)"
v-on:keyup.enter="doneEdit(todo)"
v-on:keyup.esc="cancelEdit(todo)"
type="text"
)
</template>
<script>
import annotationMixin from './demo_mixin';
import todoFocus from '../directives';
import { demoTranslation } from './demo_data';
export default {
directives: { todoFocus },
mixins: [annotationMixin],
data: () => ({ ...demoTranslation }),
methods: {
addTodo() {
const value = this.newTodo && this.newTodo.trim();
if (!value) {
return;
}
const payload = {
text: value,
id: this.annotationId++,
};
this.annotations[this.pageNumber].push(payload);
this.newTodo = '';
},
removeTodo(todo) {
const index = this.annotations[this.pageNumber].indexOf(todo);
this.annotations[this.pageNumber].splice(index, 1);
},
editTodo(todo) {
this.beforeEditCache = todo.text;
this.editedTodo = todo;
},
doneEdit(todo) {
if (!this.editedTodo) {
return;
}
this.editedTodo = null;
todo.text = todo.text.trim();
if (!todo.text) {
this.removeTodo(todo);
}
},
cancelEdit(todo) {
this.editedTodo = null;
todo.text = this.beforeEditCache;
},
},
};
</script>

2
app/server/static/components/filter.js

@ -1,5 +1,5 @@
export function simpleShortcut(shortcut) {
let simplified = shortcut === null ? '' : shortcut;
let simplified = shortcut == null ? '' : shortcut;
simplified = simplified.replace('ctrl', 'C');
simplified = simplified.replace('shift', 'S');
simplified = simplified.split(' ').join('-');

3
app/server/static/components/http.js

@ -7,6 +7,5 @@ const HTTP = axios.create({
baseURL: `/v1/${baseUrl}`,
});
export const rootUrl = window.location.href.split('/').slice(0, 3).join('/');
export const newHttpClient = axios.create;
export const defaultHttpClient = axios.create();
export default HTTP;

2
app/server/static/components/label.vue

@ -16,7 +16,7 @@
backgroundColor: newLabel.background_color \
}") {{ newLabel.text }}
span.tag is-medium
span.tag.is-medium
kbd {{ shortcutKey(newLabel) | simpleShortcut }}
div.column

14
app/server/static/components/projects.vue

@ -120,9 +120,7 @@
<script>
import { title, daysAgo } from './filter';
import { rootUrl, newHttpClient } from './http';
const httpClient = newHttpClient();
import { defaultHttpClient } from './http';
export default {
filters: { title, daysAgo },
@ -152,8 +150,8 @@ export default {
created() {
Promise.all([
httpClient.get(`${rootUrl}/v1/projects`),
httpClient.get(`${rootUrl}/v1/me`),
defaultHttpClient.get('/v1/projects'),
defaultHttpClient.get('/v1/me'),
]).then(([projects, me]) => {
this.items = projects.data;
this.username = me.data.username;
@ -163,7 +161,7 @@ export default {
methods: {
deleteProject() {
httpClient.delete(`${rootUrl}/v1/projects/${this.project.id}`).then(() => {
defaultHttpClient.delete(`/v1/projects/${this.project.id}`).then(() => {
this.isDelete = false;
const index = this.items.indexOf(this.project);
this.items.splice(index, 1);
@ -197,9 +195,9 @@ export default {
guideline: 'Please write annotation guideline.',
resourcetype: this.resourceType(),
};
httpClient.post(`${rootUrl}/v1/projects`, payload)
defaultHttpClient.post('/v1/projects', payload)
.then((response) => {
window.location = `${rootUrl}/projects/${response.data.id}/docs/create`;
window.location = `/projects/${response.data.id}/docs/create`;
})
.catch((error) => {
this.projectTypeError = '';

4
app/server/static/components/uploadMixin.js

@ -1,6 +1,6 @@
import hljs from 'highlight.js/lib/highlight';
import hljsLanguages from './hljsLanguages';
import HTTP, { newHttpClient } from './http';
import HTTP, { defaultHttpClient } from './http';
import Messages from './messages.vue';
hljsLanguages.forEach((languageName) => {
@ -27,7 +27,7 @@ export default {
},
created() {
newHttpClient().get('/v1/features').then((response) => {
defaultHttpClient.get('/v1/features').then((response) => {
this.canUploadFromCloud = response.data.cloud_upload;
});
},

11
app/server/static/package-lock.json

@ -673,6 +673,14 @@
"is-buffer": "1.1.6"
}
},
"axios-mock-adapter": {
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.17.0.tgz",
"integrity": "sha512-q3efmwJUOO4g+wsLNSk9Ps1UlJoF3fQ3FSEe4uEEhkRtu7SoiAVPj8R3Hc/WP55MBTVFzaDP9QkdJhdVhP8A1Q==",
"requires": {
"deep-equal": "1.0.1"
}
},
"babel-code-frame": {
"version": "6.26.0",
"resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz",
@ -1836,8 +1844,7 @@
"deep-equal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz",
"integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=",
"dev": true
"integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU="
},
"deep-extend": {
"version": "0.6.0",

1
app/server/static/package.json

@ -15,6 +15,7 @@
"license": "ISC",
"dependencies": {
"axios": "^0.18.0",
"axios-mock-adapter": "^1.17.0",
"chart.js": "^2.7.2",
"highlight.js": "^9.12.0",
"lodash.isempty": "^4.4.0",

18
app/server/static/pages/demo_named_entity.js

@ -1,14 +1,26 @@
import Vue from 'vue';
import DemoNamedEntity from '../components/demo/demo_named_entity.vue';
import { demoNamedEntity as data } from '../components/demo/demo_data';
import DemoApi from '../components/demo/demo_api';
import SequenceLabeling from '../components/sequence_labeling.vue';
Vue.use(require('vue-shortkey'), {
prevent: ['input', 'textarea'],
});
const demoApi = new DemoApi(data, 'start_offset');
new Vue({
el: '#mail-app',
components: { DemoNamedEntity },
components: { SequenceLabeling },
beforeCreate() {
demoApi.start();
},
beforeDestroy() {
demoApi.stop();
},
template: '<DemoNamedEntity />',
template: '<SequenceLabeling />',
});

18
app/server/static/pages/demo_text_classification.js

@ -1,14 +1,26 @@
import Vue from 'vue';
import DemoTextClassification from '../components/demo/demo_text_classification.vue';
import { demoTextClassification as data } from '../components/demo/demo_data';
import DemoApi from '../components/demo/demo_api';
import DocumentClassification from '../components/document_classification.vue';
Vue.use(require('vue-shortkey'), {
prevent: ['input', 'textarea'],
});
const demoApi = new DemoApi(data, 'label');
new Vue({
el: '#mail-app',
components: { DemoTextClassification },
components: { DocumentClassification },
beforeCreate() {
demoApi.start();
},
beforeDestroy() {
demoApi.stop();
},
template: '<DemoTextClassification />',
template: '<DocumentClassification />',
});

18
app/server/static/pages/demo_translation.js

@ -1,12 +1,24 @@
import Vue from 'vue';
import DemoTranslation from '../components/demo/demo_translation.vue';
import { demoTranslation as data } from '../components/demo/demo_data';
import DemoApi from '../components/demo/demo_api';
import Seq2Seq from '../components/seq2seq.vue';
Vue.use(require('vue-shortkey'));
const demoApi = new DemoApi(data, 'text');
new Vue({
el: '#mail-app',
components: { DemoTranslation },
components: { Seq2Seq },
beforeCreate() {
demoApi.start();
},
beforeDestroy() {
demoApi.stop();
},
template: '<DemoTranslation />',
template: '<Seq2Seq />',
});
Loading…
Cancel
Save