Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(#821): token classifier show predictions in explore view #1009

Merged
merged 12 commits into from
Jan 26, 2022
8 changes: 4 additions & 4 deletions frontend/components/commons/results/ResultsList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -223,11 +223,11 @@ export default {
<style lang="scss">
.vue-recycle-scroller__item-wrapper {
box-sizing: content-box;
padding-bottom: 260px;
.fixed-header & {
padding-bottom: 260px;
.fixed-header & {
padding-bottom: 260px;
}
}
}
}
.vue-recycle-scroller__item-view {
box-sizing: border-box;
}
Expand Down
55 changes: 41 additions & 14 deletions frontend/components/token-classifier/results/EntityHighlight.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
-->

<template>
<span :class="['highlight', isText ? '' : 'highlight--block']">
<span
:class="['highlight', span.entity.origin, isText ? '' : 'highlight--block']"
>
<span
class="highlight__content"
@click="openTagSelector"
Expand All @@ -30,12 +32,17 @@
annotationEnabled ? 'highlight__tooltip--icon' : '',
]"
>
<span
class="highlight__tooltip__origin"
v-if="span.entity.origin === 'annotation'"
>Annot.</span
>
<span
>{{ span.entity.label }}
<svgicon
v-if="annotationEnabled"
width="12"
height="12"
width="8"
height="8"
name="cross"
@click="removeEntity"
></svgicon>
Expand Down Expand Up @@ -110,9 +117,10 @@ export default {
line-height: 1em;
position: relative;
cursor: default;
display: inline;
// display: inline-flex;
border-radius: 2px;
padding: 0;
margin-right: -3px;
&--block {
display: block;
.highlight__content:after {
Expand All @@ -126,10 +134,10 @@ export default {
&__label {
@include font-size(0px);
}
.highlight__content {
&__content {
display: inline;
}
.highlight__metadata {
&__metadata {
background: white;
display: block;
position: absolute;
Expand All @@ -150,14 +158,13 @@ export default {
right: 50%;
transform: translateX(50%);
}
.highlight__tooltip {
&__tooltip {
display: block;
position: absolute;
border-radius: 2px;
padding: 4px 9px 5px 9px;
opacity: 0;
z-index: -1;
bottom: 100%;
margin-bottom: 0.5em;
transition: opacity 0.5s ease, z-index 0.2s ease;
white-space: nowrap;
Expand All @@ -167,23 +174,43 @@ export default {
right: 50%;
transform: translateX(50%);
@include font-size(12px);
& > span {
display: block;
}
&__origin {
@include font-size(8px);
}
.annotation & {
bottom: 100%;
}
.prediction & {
top: calc(100% + 15px);
}
&--icon {
padding-right: 20px;
.svg-icon {
display: inline-block;
margin-left: 1em;
position: absolute;
top: 8px;
right: 8px;
cursor: pointer;
}
}
}
.highlight__tooltip:after {
&__tooltip:after {
margin: auto;
transform: translateY(10px);
@include triangle(bottom, 6px, 6px, auto);
position: absolute;
bottom: 5px;
right: 0;
left: 0;
.annotation & {
@include triangle(bottom, 6px, 6px, auto);
bottom: 5px;
transform: translateY(10px);
}
.prediction & {
@include triangle(top, 6px, 6px, auto);
top: -15px;
transform: translateY(10px);
}
}
&:hover .highlight__metadata {
opacity: 1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,37 +17,22 @@

<template>
<div class="record">
<div v-if="textSpans.length" ref="list" class="content__input">
<TextSpan
v-for="(token, i) in textSpans"
:key="i"
<div class="content">
<record-token-classification-annotation
:dataset="dataset"
:record="record"
:span-id="i"
:spans="textSpans"
v-if="annotationEnabled"
/>
<record-token-classification-exploration
:dataset="dataset"
:class="isSelected(i) ? 'selected' : ''"
@startSelection="onStartSelection"
@endSelection="onEndSelection"
@selectEntity="onSelectEntity"
@changeEntityLabel="onChangeEntityLabel"
@removeEntity="onRemoveEntity"
@reset="onReset"
:record="record"
v-else
/>
</div>
<div v-if="annotationEnabled" class="content__actions-buttons">
<re-button
v-if="record.status !== 'Validated'"
class="button-primary"
@click="onValidate(record)"
>{{ record.status === "Edited" ? "Save" : "Validate" }}</re-button
>
</div>
</div>
</template>

<script>
import { mapActions } from "vuex";

export default {
props: {
dataset: {
Expand All @@ -59,184 +44,11 @@ export default {
required: true,
},
},
data: function () {
return {
selectionStart: undefined,
selectionEnd: undefined,
};
},
computed: {
entities() {
let entities = [];
if (this.record.annotation) {
entities = this.record.annotation.entities;
} else if (this.record.prediction) {
entities = this.record.prediction.entities;
}
return entities;
},
agent() {
if (this.record.annotation) {
return this.record.annotation.agent;
}
if (this.record.prediction) {
return this.record.prediction.agent;
}
return undefined;
},
textSpans() {
// TODO Simplify !!!
const normalizedEntities = (entities, tokens) => {
const tokenForChar = (character, tokens) => {
const tokenIdx = tokens.findIndex(
(token) => token.start <= character && character < token.end
);
return tokenIdx >= 0 ? tokenIdx : undefined;
};
return entities.map((entity) => {
const start_token = tokenForChar(entity.start, tokens);
const end_token = tokenForChar(entity.end - 1, tokens);
return entity.start_token && entity.end_token
? entity
: { ...entity, start_token, end_token: end_token + 1 };
});
};

let idx = 0;
let textSpans = [];
const entities = normalizedEntities(
this.entities,
this.record.visualTokens
);
while (idx < this.record.visualTokens.length) {
const entity = entities.find(
(entity) => entity.start_token <= idx && idx < entity.end_token
);
if (entity) {
textSpans.push({
entity,
tokens: this.record.visualTokens.slice(
entity.start_token,
entity.end_token
),
start: entity.start,
end: entity.end,
agent: this.agent,
});
idx = entity.end_token;
} else {
const token = this.record.visualTokens[idx];
textSpans.push({
entity: undefined,
tokens: [token],
start: token.start,
end: token.end,
agent: this.agent,
});
idx++;
}
}
return textSpans;
},
annotationEnabled() {
return this.dataset.viewSettings.viewMode === "annotate";
},
},
methods: {
...mapActions({
updateRecords: "entities/datasets/updateDatasetRecords",
discard: "entities/datasets/discardAnnotations",
validate: "entities/datasets/validateAnnotations",
}),

onReset() {
this.selectionStart = undefined;
this.selectionEnd = undefined;
},
onStartSelection(spanId) {
this.selectionStart = spanId;
},
onEndSelection(spanId) {
this.selectionEnd = spanId;
},
updateRecordEntities(entities) {
this.updateRecords({
dataset: this.dataset,
records: [
{
...this.record,
selected: true,
status: "Edited",
annotation: {
entities,
agent: this.$auth.user.username,
},
},
],
});
this.onReset();
},
async onValidate(record) {
const emptyEntities = {
entities: [],
};
await this.validate({
dataset: this.dataset,
agent: this.$auth.user.username,
records: [
{
...record,
annotation: {
...(record.annotation || record.prediction || emptyEntities),
},
},
],
});
},
onSelectEntity(entity) {
const from = Math.min(this.selectionStart, this.selectionEnd);
const to = Math.max(this.selectionStart, this.selectionEnd);
const startToken = this.textSpans[from].tokens[0];
const endToken = this.textSpans[to].tokens.reverse()[0];

let entities = [...this.entities];
entities.push({
start: startToken.start,
end: endToken.end,
label: entity,
});
this.updateRecordEntities(entities);
},
onChangeEntityLabel(entity, newLabel) {
let entities = this.entities.map((ent) => {
return ent.start === entity.start &&
ent.end === entity.end &&
ent.label === entity.label
? { ...ent, label: newLabel }
: ent;
});
this.updateRecordEntities(entities);
},
onRemoveEntity(entity) {
const found = this.entities.findIndex(
(ent) =>
ent.start === entity.start &&
ent.end === entity.end &&
ent.label === entity.label
);
let entities = [...this.entities];
entities.splice(found, 1);
this.updateRecordEntities(entities);
},
isSelected(i) {
const init = Math.min(this.selectionStart, this.selectionEnd);
const end = Math.max(this.selectionStart, this.selectionEnd);
if (i >= init && i <= end) {
return true;
}
return false;
},
},
};
</script>

Expand All @@ -245,14 +57,15 @@ export default {
padding: 44px 20px 20px 20px;
display: block;
margin-bottom: 0; // white-space: pre-line;
white-space: pre-wrap;
@include font-size(16px);
line-height: 1.6em;
.list__item--annotation-mode & {
padding-left: 65px;
}
}
.content {
position: relative;
white-space: pre-wrap;
&__input {
padding-right: 200px;
}
Expand Down