Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3b53449
Add settings for vetting
naglepuff Dec 22, 2025
49a6320
Model submission status of recording annotations
naglepuff Dec 23, 2025
8511dcd
Add endpoint to get current user
naglepuff Dec 23, 2025
5863198
Show recording submission status in table view
naglepuff Dec 23, 2025
27a3ef9
Don't filter out noise
naglepuff Dec 23, 2025
7e60817
Add endpoint to submit file-level annotations
naglepuff Dec 23, 2025
a2e2fb0
Allow submitting file annotations in interface
naglepuff Dec 23, 2025
12552e2
Squash migrations
naglepuff Dec 23, 2025
7b7cb7a
Indicate when a file has been reviewed
naglepuff Dec 23, 2025
3e72bd5
Format
naglepuff Dec 23, 2025
3f88c06
Show the current user's submitted label in sidebar
naglepuff Dec 23, 2025
b004a75
Disable deletion for non-admin vetters
naglepuff Dec 30, 2025
00e8bbb
Make 403 message more descriptive
naglepuff Dec 30, 2025
1dbc1b2
Show progress bar for submitted recordings
naglepuff Dec 29, 2025
20febf7
Enable showing/hiding submitted recordings
naglepuff Dec 29, 2025
6f5fc43
Toggle submitted recordings in sidebar
naglepuff Dec 29, 2025
43caf66
Fix submission bug
naglepuff Dec 30, 2025
bfd4730
Add button to go to next unreviewed file
naglepuff Dec 30, 2025
92d60be
Add function to get next unreviewed recording
naglepuff Dec 30, 2025
968291e
Navigate between unreviewed files
naglepuff Dec 30, 2025
4c5f9e4
Display a message when there are no files to review
naglepuff Dec 31, 2025
6be58ab
Reset selected annotation when recording changes
naglepuff Dec 31, 2025
9ae9c31
Merge branch 'vetting-workflow' into issue-278-next-unreviewed
naglepuff Dec 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions client/src/components/RecordingAnnotationEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export default defineComponent({
default: () => undefined,
},
},
emits: ['update:annotation', 'delete:annotation'],
emits: ['update:annotation', 'delete:annotation', 'submit:annotation'],
setup(props, { emit }) {
const { configuration, currentUser } = useState();
const speciesEdit: Ref<string[]> = ref( props.annotation?.species?.map((item) => item.species_code || item.common_name) || []);
Expand Down Expand Up @@ -105,8 +105,8 @@ export default defineComponent({

const submitAnnotation = async () => {
if (props.annotation && props.recordingId) {
await submitFileAnnotation(props.annotation.id);
emit('update:annotation');
const response = await submitFileAnnotation(props.annotation.id);
emit('submit:annotation', props.annotation, response.data.submitted);
}
};

Expand Down
20 changes: 18 additions & 2 deletions client/src/components/RecordingAnnotations.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import { computed, defineComponent, onMounted, PropType, Ref } from "vue";
import { computed, defineComponent, onMounted, PropType, Ref, watch } from "vue";
import { ref } from "vue";
import { FileAnnotation, getFileAnnotations, putFileAnnotation, Species, UpdateFileAnnotation } from "@api/api";
import RecordingAnnotationEditor from "./RecordingAnnotationEditor.vue";
Expand Down Expand Up @@ -38,7 +38,7 @@ export default defineComponent({
const annotations: Ref<FileAnnotation[]> = ref([]);
const detailsDialog = ref(false);
const detailRecordingId = ref(-1);
const { configuration, isNaBat, currentUser } = useState();
const { configuration, isNaBat, currentUser, markAnnotationSubmitted } = useState();

const setSelectedId = (annotation: FileAnnotation) => {
selectedAnnotation.value = annotation;
Expand All @@ -56,6 +56,11 @@ export default defineComponent({
}
};

watch(() => props.recordingId, async () => {
selectedAnnotation.value = null;
await loadFileAnnotations();
});

onMounted(async () => {
await loadFileAnnotations();
if (props.type === 'nabat') {
Expand Down Expand Up @@ -100,6 +105,15 @@ export default defineComponent({
}
};

function handleSubmitAnnotation(annotation: FileAnnotation, submitSuccess: boolean) {
if (submitSuccess) {
annotation.submitted = true;
// Also update submitted status on the recording object
// This forces recomputation of allRecordings
markAnnotationSubmitted(props.recordingId, annotation.id);
}
}

const loadDetails = async (id: number) => {
detailRecordingId.value = id;
detailsDialog.value = true;
Expand Down Expand Up @@ -143,6 +157,7 @@ export default defineComponent({
disableNaBatAnnotations,
currentNaBatUser,
userSubmittedAnnotationId,
handleSubmitAnnotation,
};
},
});
Expand Down Expand Up @@ -245,6 +260,7 @@ export default defineComponent({
class="mt-4"
@update:annotation="updatedAnnotation()"
@delete:annotation="updatedAnnotation(true)"
@submit:annotation="handleSubmitAnnotation"
/>
<v-dialog
v-model="detailsDialog"
Expand Down
17 changes: 11 additions & 6 deletions client/src/components/RecordingList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,17 @@ export default defineComponent({

<template>
<v-expansion-panels v-model="openPanel">
<v-checkbox
v-if="configuration.mark_annotations_completed_enabled"
v-model="showSubmittedRecordings"
label="Show submitted recordings"
hide-details
/>
<v-col v-if="configuration.mark_annotations_completed_enabled">
<v-row>
<v-col>
<v-checkbox
v-model="showSubmittedRecordings"
label="Show submitted recordings"
hide-details
/>
</v-col>
</v-row>
</v-col>
<v-expansion-panel>
<v-expansion-panel-title>My Recordings</v-expansion-panel-title>
<v-expansion-panel-text>
Expand Down
73 changes: 73 additions & 0 deletions client/src/use/useState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
SpectrogramAnnotation,
SpectrogramSequenceAnnotation,
RecordingTag,
FileAnnotation,
} from "../api/api";
import {
interpolateCividis,
Expand Down Expand Up @@ -47,6 +48,7 @@ const sequenceAnnotations: Ref<SpectrogramSequenceAnnotation[]> = ref([]);
const otherUserAnnotations: Ref<OtherUserAnnotations> = ref({});
const sharedList: Ref<Recording[]> = ref([]);
const recordingList: Ref<Recording[]> = ref([]);
const currentRecordingId: Ref<number | undefined> = ref(undefined);
const recordingTagList: Ref<RecordingTag[]> = ref([]);
const nextShared: Ref<Recording | false> = ref(false);
const scaledVals: Ref<{ x: number; y: number }> = ref({ x: 1, y: 1 });
Expand Down Expand Up @@ -217,7 +219,74 @@ export default function useState() {
}
});

function hasSubmittedAnnotation(recording: Recording): boolean {
return recording.fileAnnotations.some((annotation: FileAnnotation) => (
annotation.owner === currentUser.value && annotation.submitted
));
}

const allRecordings = computed(() => {
const recordings = recordingList.value.concat(sharedList.value);
return recordings.map((recording: Recording) => {
const isSubmitted = recording.fileAnnotations.some((annotation: FileAnnotation) => (
annotation.owner === currentUser.value && annotation.submitted
));
return {
...recording,
submitted: isSubmitted,
};
});
});

function markAnnotationSubmitted(recordingId: number, annotationId: number) {
const recording = allRecordings.value.find((recording: Recording) => recording.id === recordingId);
if (!recording) return;
const annotation = recording.fileAnnotations.find((annotation: FileAnnotation) => annotation.id === annotationId);
if (!annotation) return;
annotation.submitted = true;
}

const nextUnsubmittedRecordingId = computed(() => {
if (allRecordings.value.length === 0) {
return undefined;
}
const startingIndex = allRecordings.value.findIndex((recording: Recording) => recording.id === currentRecordingId.value) || 0;

for (let i = startingIndex + 1; i < allRecordings.value.length; i++) {
if (!hasSubmittedAnnotation(allRecordings.value[i])) {
return allRecordings.value[i].id;
}
}

for (let i = 0; i < startingIndex; i++) {
if (!hasSubmittedAnnotation(allRecordings.value[i])) {
return allRecordings.value[i].id;
}
}

return undefined;
});

const previousUnsubmittedRecordingId = computed(() =>{
if (allRecordings.value.length === 0) {
return undefined;
}
const startingIndex = allRecordings.value.findIndex((recording: Recording) => recording.id === currentRecordingId.value) || 0;

for (let i = startingIndex -1; i >= 0; i--) {
if (!hasSubmittedAnnotation(allRecordings.value[i])) {
return allRecordings.value[i].id;
}
}

for (let i = allRecordings.value.length - 1; i > startingIndex; i--) {
if (!hasSubmittedAnnotation(allRecordings.value[i])) {
return allRecordings.value[i].id;
}
}

return undefined;
});

return {
annotationState,
Expand Down Expand Up @@ -268,5 +337,9 @@ export default function useState() {
unsubmittedSharedRecordings,
myRecordingsDisplay,
sharedRecordingsDisplay,
nextUnsubmittedRecordingId,
previousUnsubmittedRecordingId,
markAnnotationSubmitted,
currentRecordingId,
};
}
80 changes: 80 additions & 0 deletions client/src/views/Spectrogram.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
ref,
watch,
} from "vue";
import { useRouter } from "vue-router";
import {
getSpecies,
getAnnotations,
Expand Down Expand Up @@ -70,7 +71,11 @@ export default defineComponent({
toggleDrawingBoundingBox,
fixedAxes,
toggleFixedAxes,
nextUnsubmittedRecordingId,
previousUnsubmittedRecordingId,
currentRecordingId,
} = useState();
const router = useRouter();
const images: Ref<HTMLImageElement[]> = ref([]);
const spectroInfo: Ref<SpectroInfo | undefined> = ref();
const speciesList: Ref<Species[]> = ref([]);
Expand Down Expand Up @@ -117,6 +122,7 @@ export default defineComponent({
const loading = ref(false);
const loadData = async () => {
loading.value = true;
currentRecordingId.value = parseInt(props.id);
loadedImage.value = false;
const response = compressed.value
? await getSpectrogramCompressed(props.id)
Expand Down Expand Up @@ -264,7 +270,16 @@ export default defineComponent({
viewCompressedOverlay.value = !viewCompressedOverlay.value;
};

function goToNextUnreviewed() {
router.push({path: `/recording/${nextUnsubmittedRecordingId.value}/spectrogram`, replace: true });
}

function goToPreviousUnreviewed() {
router.push({ path: `/recording/${previousUnsubmittedRecordingId.value}/spectrogram`, replace: true });
}

return {
configuration,
annotationState,
compressed,
loadedImage,
Expand Down Expand Up @@ -308,6 +323,10 @@ export default defineComponent({
colorScale,
scaledVals,
recordingInfo,
// Vetting
goToNextUnreviewed,
goToPreviousUnreviewed,
nextUnsubmittedRecordingId,
};
},
});
Expand Down Expand Up @@ -614,6 +633,67 @@ export default defineComponent({
</v-row>
</v-card-title>
<v-card-text class="pa-0">
<div
v-if="configuration.mark_annotations_completed_enabled"
>
<v-col>
<v-row>
<v-col>
<span class="text-h6">
Vetting Controls
</span>
<v-tooltip>
<template #activator="{ props }">
<v-icon
v-bind="props"
class="pb-2"
size="medium"
>
mdi-help-circle
</v-icon>
</template>
Navigate between unreviewed files
</v-tooltip>
</v-col>
</v-row>
<v-row v-if="nextUnsubmittedRecordingId">
<v-col>
<v-btn
flat
color="primary"
@click="goToPreviousUnreviewed"
>
Prev
<template #prepend>
<v-icon>mdi-arrow-left</v-icon>
</template>
</v-btn>
</v-col>
<v-spacer />
<v-col>
<v-btn
flat
color="primary"
@click="goToNextUnreviewed"
>
Next
<template #append>
<v-icon>mdi-arrow-right</v-icon>
</template>
</v-btn>
</v-col>
</v-row>
<v-row v-else>
<v-col>
There are no more files to review
</v-col>
</v-row>
<v-divider

class="my-2"
/>
</v-col>
</div>
<div v-if="sideTab === 'annotations'">
<annotation-list
:annotations="annotations"
Expand Down