Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 9 additions & 7 deletions bats_ai/core/views/nabat/nabat_recording.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,8 +231,6 @@ def generate_nabat_recording(

@router.get('/{id}/spectrogram', auth=admin_auth)
def get_spectrogram(request: HttpRequest, id: int):
if not request.user.is_authenticated and not request.user.is_superuser:
return JsonResponse({'error': 'Permission denied'}, status=403)
try:
nabat_recording = NABatRecording.objects.get(pk=id)
except NABatRecording.DoesNotExist:
Expand Down Expand Up @@ -432,7 +430,9 @@ def get_recording_annotation_details(request: HttpRequest, id: int, apiToken: st

@router.put('recording-annotation', auth=None, response={200: str})
def create_recording_annotation(request: HttpRequest, data: NABatCreateRecordingAnnotationSchema):
email_or_response = get_email_if_authorized(request, data.apiToken, recording_pk=id)
email_or_response = get_email_if_authorized(
request, data.apiToken, recording_pk=data.recordingId
)
if isinstance(email_or_response, JsonResponse):
return email_or_response
user_email = email_or_response # safe to use
Expand Down Expand Up @@ -487,11 +487,12 @@ def create_recording_annotation(request: HttpRequest, data: NABatCreateRecording
def update_recording_annotation(
request: HttpRequest, id: int, data: NABatCreateRecordingAnnotationSchema
):
email_or_response = get_email_if_authorized(request, data.apiToken, recording_pk=id)
email_or_response = get_email_if_authorized(
request, data.apiToken, recording_pk=data.recordingId
)
if isinstance(email_or_response, JsonResponse):
return email_or_response
user_email = email_or_response # safe to use

try:
annotation = NABatRecordingAnnotation.objects.get(pk=id, user_email=user_email)
# Check permission
Expand Down Expand Up @@ -539,9 +540,10 @@ def update_recording_annotation(
return JsonResponse({'error': 'One or more species IDs not found.'}, 404)


# TODO: Determine if this will be implemented for NABat
@router.delete('recording-annotation/{id}', auth=None, response={200: str})
def delete_recording_annotation(request: HttpRequest, id: int, apiToken: str):
email_or_response = get_email_if_authorized(request, apiToken, recording_pk=id)
def delete_recording_annotation(request: HttpRequest, id: int, apiToken: str, recordingId: str):
email_or_response = get_email_if_authorized(request, apiToken, recording_pk=recordingId)
if isinstance(email_or_response, JsonResponse):
return email_or_response
user_email = email_or_response # safe to use
Expand Down
4 changes: 2 additions & 2 deletions client/src/api/NABatApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ async function patchNABatFileAnnotation(fileAnnotationId: number, fileAnnotation
return axiosInstance.patch<{ message: string, id: number }>(`nabat/recording/recording-annotation/${fileAnnotationId}`, { ...fileAnnotation });
}

async function deleteNABatFileAnnotation(fileAnnotationId: number, apiToken?: string) {
return axiosInstance.delete<{ message: string, id: number }>(`nabat/recording/recording-annotation/${fileAnnotationId}`, { params: { apiToken } });
async function deleteNABatFileAnnotation(fileAnnotationId: number, apiToken?: string, recordingId?: number) {
return axiosInstance.delete<{ message: string, id: number }>(`nabat/recording/recording-annotation/${fileAnnotationId}`, { params: { apiToken, recordingId } });
}

export interface RecordingListItem {
Expand Down
4 changes: 4 additions & 0 deletions client/src/components/ColorSchemeSelect.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ defineProps({
type: Number,
default: 150,
},
maxWidth: {
type: Number,
default: 150,
},
returnObject: {
type: Boolean,
default: true,
Expand Down
5 changes: 3 additions & 2 deletions client/src/components/RecordingAnnotationEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export default defineComponent({

const deleteAnnotation = async () => {
if (props.annotation && props.recordingId) {
props.type === 'nabat' ? await deleteNABatFileAnnotation(props.annotation.id, props.apiToken) : await deleteFileAnnotation(props.annotation.id,);
props.type === 'nabat' ? await deleteNABatFileAnnotation(props.annotation.id, props.apiToken, props.recordingId) : await deleteFileAnnotation(props.annotation.id,);
emit('delete:annotation');
}
};
Expand All @@ -119,7 +119,7 @@ export default defineComponent({
Edit Annotations
<v-spacer />
<v-btn
v-if="type !== 'nabat' || (annotation?.owner && type === 'nabat')"
v-if="type !== 'nabat'"
size="x-small"
color="error"
class="mt-1"
Expand All @@ -141,6 +141,7 @@ export default defineComponent({
<v-autocomplete
v-if="type !== 'nabat'"
v-model="speciesEdit"
autocomplete="off"
multiple
closable-chips
chips
Expand Down
26 changes: 23 additions & 3 deletions client/src/components/RecordingAnnotations.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import RecordingAnnotationEditor from "./RecordingAnnotationEditor.vue";
import { getNABatRecordingFileAnnotations, putNABatFileAnnotation } from "@api/NABatApi";
import RecordingAnnotationDetails from "./RecordingAnnotationDetails.vue";
import useState from "@use/useState";
import { decodeJWT } from "../use/useJWTToken";
export default defineComponent({
name: "AnnotationList",
components: {
Expand Down Expand Up @@ -43,6 +44,10 @@ export default defineComponent({
selectedAnnotation.value = annotation;
};

const currentNaBatUser: Ref<string | null> = ref(null);



const loadFileAnnotations = async () => {
if (props.type === 'nabat') {
annotations.value = (await getNABatRecordingFileAnnotations(props.recordingId, props.apiToken)).data;
Expand All @@ -51,7 +56,20 @@ export default defineComponent({
}
};

onMounted(() => loadFileAnnotations());
onMounted(async () => {
await loadFileAnnotations();
if (props.type === 'nabat') {
const decoded = decodeJWT(props.apiToken);
if (decoded['email']) {
currentNaBatUser.value = decoded['email'];
const foundItem = annotations.value.find((item) => item.owner === currentNaBatUser.value);
if (foundItem) {
setSelectedId(foundItem);
}
}
}

});

const addAnnotation = async () => {
const newAnnotation: UpdateFileAnnotation & { apiToken?: string } = {
Expand Down Expand Up @@ -91,11 +109,11 @@ export default defineComponent({
const isAdmin = computed(() => configuration.value.is_admin);

const disableNaBatAnnotations = computed(() => {
const nonAIAnnotations = annotations.value.filter((item) => item.owner);
const currentUserAnnotations = annotations.value.filter((item) => item.owner === currentNaBatUser.value);
if (isAdmin.value && props.type === 'nabat' && !props.apiToken) {
return true;
}
return (nonAIAnnotations.length > 0 && props.type === 'nabat');
return ( currentUserAnnotations.length > 0 && props.type === 'nabat');
});

return {
Expand All @@ -109,6 +127,7 @@ export default defineComponent({
detailsDialog,
detailRecordingId,
disableNaBatAnnotations,
currentNaBatUser,
};
},
});
Expand Down Expand Up @@ -137,6 +156,7 @@ export default defineComponent({
:id="`annotation-${annotation.id}`"
:key="annotation.id"
:class="{ selected: annotation.id === selectedAnnotation?.id }"
:disabled="type === 'nabat' && disableNaBatAnnotations && annotation.owner !== currentNaBatUser"
class="annotation-item"
@click="setSelectedId(annotation)"
>
Expand Down
38 changes: 19 additions & 19 deletions client/src/use/useJWTToken.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
import { ref, watch } from 'vue';
import { ref, watch } from "vue";

interface UseJWTTokenOptions {
token: string;
warningSeconds: number;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function decodeJWT(token: string): any | null {
try {
const payload = token.split(".")[1];
const decoded = atob(payload.replace(/-/g, "+").replace(/_/g, "/"));
return JSON.parse(decoded);
} catch (error) {
console.error("Failed to decode JWT:", error);
return null;
}
}

export function useJWTToken(options: UseJWTTokenOptions) {
const { token, warningSeconds } = options;
const storageKey = 'jwt-expiration';
const storageKey = "jwt-expiration";
const exp = ref<number | null>(null);
const shouldWarn = ref(false);
let warningTimeout: ReturnType<typeof setTimeout> | null = null;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function decodeJWT(token: string): any | null {
try {
const payload = token.split('.')[1];
const decoded = atob(payload.replace(/-/g, '+').replace(/_/g, '/'));
return JSON.parse(decoded);
} catch (error) {
console.error('Failed to decode JWT:', error);
return null;
}
}

function setupWarning(expiration: number) {
const now = Math.floor(Date.now() / 1000);
const secondsUntilWarning = expiration - now - warningSeconds;
Expand Down Expand Up @@ -51,11 +51,11 @@ export function useJWTToken(options: UseJWTTokenOptions) {
if (stored) {
try {
const data = JSON.parse(stored);
if (typeof data.expiration === 'number') {
if (typeof data.expiration === "number") {
return data.expiration;
}
} catch (error) {
console.error('Failed to parse stored expiration:', error);
console.error("Failed to parse stored expiration:", error);
}
}
return null;
Expand All @@ -66,12 +66,12 @@ export function useJWTToken(options: UseJWTTokenOptions) {
return;
}
const decoded = decodeJWT(token);
if (decoded && typeof decoded.exp === 'number') {
if (decoded && typeof decoded.exp === "number") {
exp.value = decoded.exp;
persistExpiration(decoded.exp);
setupWarning(decoded.exp);
} else {
console.warn('Token does not have a valid exp field.');
console.warn("Token does not have a valid exp field.");
const persisted = loadPersistedExpiration();
if (persisted) {
exp.value = persisted;
Expand Down Expand Up @@ -101,4 +101,4 @@ export function useJWTToken(options: UseJWTTokenOptions) {
shouldWarn,
clear,
};
}
}
Loading