From ef48af60e0beb6e666e9e797ef5955a8a4216f9c Mon Sep 17 00:00:00 2001 From: Hironsan Date: Mon, 23 Sep 2019 15:53:03 +0900 Subject: [PATCH] Implement CRUD for sequence labeling --- frontend/api/routes/docs.js | 46 ++++ .../components/containers/EntityItemBox.vue | 61 +++++ .../containers/EntityItemContainer.vue | 122 ++++++++-- frontend/components/molecules/EntityItem.vue | 112 +++++---- .../components/organisms/EntityItemBox.vue | 212 ++++++++++++++++++ .../pages/projects/_id/annotation/index.vue | 138 +----------- frontend/services/annotation.service.js | 25 +++ frontend/store/documents.js | 51 ++++- 8 files changed, 574 insertions(+), 193 deletions(-) create mode 100644 frontend/components/containers/EntityItemBox.vue create mode 100644 frontend/components/organisms/EntityItemBox.vue create mode 100644 frontend/services/annotation.service.js diff --git a/frontend/api/routes/docs.js b/frontend/api/routes/docs.js index 999c9c09..7cda7a64 100644 --- a/frontend/api/routes/docs.js +++ b/frontend/api/routes/docs.js @@ -81,4 +81,50 @@ router.delete('/:docId', (req, res, next) => { res.status(404).json({ detail: 'Not found.' }) } }) + +// Add an annotation. +router.post('/:docId/annotations', (req, res, next) => { + const doc = db.find(item => item.id === parseInt(req.params.docId)) + if (doc) { + const annotation = { + id: Math.floor(Math.random() * 10000), + label: req.body.label, + start_offset: req.body.start_offset, + end_offset: req.body.end_offset, + user: 1, + document: parseInt(req.params.docId) + } + doc.annotations.push(annotation) + res.json(annotation) + } else { + res.status(404).json({ detail: 'Not found.' }) + } +}) + +// Delete an annotation. +router.delete('/:docId/annotations/:annotationId', (req, res, next) => { + const doc = db.find(item => item.id === parseInt(req.params.docId)) + const docIndex = db.findIndex(item => item.id === parseInt(req.params.docId)) + if (doc) { + const annotation = doc.annotations.find(item => item.id === parseInt(req.params.annotationId)) + doc.annotations = doc.annotations.filter(item => item.id !== parseInt(req.params.annotationId)) + db[docIndex] = doc + res.json(annotation) + } else { + res.status(404).json({ detail: 'Not found.' }) + } +}) + +// Update an annotation. +router.patch('/:docId/annotations/:annotationId', (req, res, next) => { + const docIndex = db.findIndex(item => item.id === parseInt(req.params.docId)) + if (docIndex !== -1) { + const doc = db[docIndex] + const annotationIndex = doc.annotations.findIndex(item => item.id === parseInt(req.params.annotationId)) + Object.assign(db[docIndex].annotations[annotationIndex], req.body) + res.json(db[docIndex].annotations[annotationIndex]) + } else { + res.status(404).json({ detail: 'Not found.' }) + } +}) module.exports = router diff --git a/frontend/components/containers/EntityItemBox.vue b/frontend/components/containers/EntityItemBox.vue new file mode 100644 index 00000000..41dda266 --- /dev/null +++ b/frontend/components/containers/EntityItemBox.vue @@ -0,0 +1,61 @@ + + + diff --git a/frontend/components/containers/EntityItemContainer.vue b/frontend/components/containers/EntityItemContainer.vue index 1b2ab339..93c15e56 100644 --- a/frontend/components/containers/EntityItemContainer.vue +++ b/frontend/components/containers/EntityItemContainer.vue @@ -1,6 +1,40 @@ @@ -28,6 +62,15 @@ export default { required: true } }, + data() { + return { + showMenu: false, + x: 0, + y: 0, + start: 0, + end: 0 + } + }, computed: { sortedEntities() { return this.entities.slice().sort((a, b) => a.start_offset - b.start_offset) @@ -72,7 +115,19 @@ export default { } }, methods: { - open() { + show(e) { + e.preventDefault() + this.showMenu = false + this.x = e.clientX + this.y = e.clientY + this.$nextTick(() => { + this.showMenu = true + }) + }, + add() { + + }, + getSpanInfo() { let selection // Modern browsers. if (window.getSelection) { @@ -80,16 +135,43 @@ export default { } else if (document.selection) { selection = document.selection } - // If something is selected. - if (selection.rangeCount > 0) { - const range = selection.getRangeAt(0) - const preSelectionRange = range.cloneRange() - preSelectionRange.selectNodeContents(this.$el) - preSelectionRange.setEnd(range.startContainer, range.startOffset) - const start = [...preSelectionRange.toString()].length - const end = start + [...range.toString()].length - alert(start + ' ' + end) - return end + // If nothing is selected. + if (selection.rangeCount <= 0) { + return + } + const range = selection.getRangeAt(0) + const preSelectionRange = range.cloneRange() + preSelectionRange.selectNodeContents(this.$el) + preSelectionRange.setEnd(range.startContainer, range.startOffset) + const start = [...preSelectionRange.toString()].length + const end = start + [...range.toString()].length + return { start, end } + }, + validateSpan(span, entities) { + if (!span) { + return false + } + if (span.start === span.end) { + return false + } + for (const entity of this.entities) { + if ((entity.start_offset <= span.start) && (span.start < entity.end_offset)) { + return false + } + if ((entity.start_offset < span.end) && (span.end <= entity.end_offset)) { + return false + } + if ((span.start < entity.start_offset) && (entity.end_offset < span.end)) { + return false + } + } + return true + }, + open(e) { + const span = this.getSpanInfo() + const isValid = this.validateSpan(span, this.entities) + if (isValid) { + this.show(e) } } } @@ -98,16 +180,16 @@ export default { diff --git a/frontend/components/molecules/EntityItem.vue b/frontend/components/molecules/EntityItem.vue index 360c7bd4..912c6a2c 100644 --- a/frontend/components/molecules/EntityItem.vue +++ b/frontend/components/molecules/EntityItem.vue @@ -1,7 +1,34 @@ @@ -22,6 +49,16 @@ export default { color: { type: String, default: '#64FFDA' + }, + labels: { + type: Array, + default: () => [], + required: true + } + }, + data() { + return { + showMenu: false } }, computed: { @@ -30,11 +67,11 @@ export default { } }, methods: { - open() { - alert('hello') + update(label) { + this.$emit('update', label) }, remove() { - alert('remove') + this.$emit('remove') } } } @@ -42,26 +79,25 @@ export default { diff --git a/frontend/pages/projects/_id/annotation/index.vue b/frontend/pages/projects/_id/annotation/index.vue index 5d6f29f7..90cbc783 100644 --- a/frontend/pages/projects/_id/annotation/index.vue +++ b/frontend/pages/projects/_id/annotation/index.vue @@ -11,7 +11,7 @@ - + @@ -22,23 +22,19 @@ diff --git a/frontend/services/annotation.service.js b/frontend/services/annotation.service.js new file mode 100644 index 00000000..c24addaf --- /dev/null +++ b/frontend/services/annotation.service.js @@ -0,0 +1,25 @@ +import ApiService from '@/services/api.service' + +class AnnotationService { + constructor() { + this.request = new ApiService() + } + + getAnnotationList(projectId, docId) { + return this.request.get(`/projects/${projectId}/docs/${docId}/annotations`) + } + + addAnnotation(projectId, docId, payload) { + return this.request.post(`/projects/${projectId}/docs/${docId}/annotations`, payload) + } + + deleteAnnotation(projectId, docId, annotationId) { + return this.request.delete(`/projects/${projectId}/docs/${docId}/annotations/${annotationId}`) + } + + updateAnnotation(projectId, docId, annotationId, payload) { + return this.request.patch(`/projects/${projectId}/docs/${docId}/annotations/${annotationId}`, payload) + } +} + +export default new AnnotationService() diff --git a/frontend/store/documents.js b/frontend/store/documents.js index 7f9a9fb9..f42b8312 100644 --- a/frontend/store/documents.js +++ b/frontend/store/documents.js @@ -1,4 +1,5 @@ import DocumentService from '@/services/document.service' +import AnnotationService from '@/services/annotation.service' import CSVParser from '@/services/parsers/csv.service' export const state = () => ({ @@ -6,7 +7,8 @@ export const state = () => ({ selected: [], loading: false, selectedFormat: null, - parsed: {} + parsed: {}, + current: 0 }) export const getters = { @@ -52,6 +54,9 @@ export const getters = { } else { return [] } + }, + currentDoc(state) { + return state.items[state.current] } } @@ -81,6 +86,16 @@ export const mutations = { parseFile(state, text) { const parser = new CSVParser() state.parsed = parser.parse(text) + }, + addAnnotation(state, payload) { + state.items[state.current].annotations.push(payload) + }, + deleteAnnotation(state, annotationId) { + state.items[state.current].annotations = state.items[state.current].annotations.filter(item => item.id !== annotationId) + }, + updateAnnotation(state, payload) { + const item = state.items[state.current].annotations.find(item => item.id === payload.id) + Object.assign(item, payload) } } @@ -128,6 +143,10 @@ export const actions = { } commit('resetSelected') }, + nextPage({ commit }) { + }, + prevPage({ commit }) { + }, parseFile({ commit }, data) { const reader = new FileReader() reader.readAsText(data, 'UTF-8') @@ -137,5 +156,35 @@ export const actions = { reader.onerror = (e) => { alert(e) } + }, + addAnnotation({ commit, state }, payload) { + const documentId = state.items[state.current].id + AnnotationService.addAnnotation(payload.projectId, documentId, payload) + .then((response) => { + commit('addAnnotation', response) + }) + .catch((error) => { + alert(error) + }) + }, + updateAnnotation({ commit, state }, payload) { + const documentId = state.items[state.current].id + AnnotationService.updateAnnotation(payload.projectId, documentId, payload.annotationId, payload) + .then((response) => { + commit('updateAnnotation', response) + }) + .catch((error) => { + alert(error) + }) + }, + deleteAnnotation({ commit, state }, payload) { + const documentId = state.items[state.current].id + AnnotationService.deleteAnnotation(payload.projectId, documentId, payload.annotationId) + .then((response) => { + commit('deleteAnnotation', payload.annotationId) + }) + .catch((error) => { + alert(error) + }) } }