From 6f6225688118a69eb3efa02fba567271b47edf78 Mon Sep 17 00:00:00 2001
From: Shaheed Azaad
Date: Mon, 10 Nov 2025 23:40:41 +0100
Subject: [PATCH] added post-listening rating
---
drizzle/0010_dual_overall_questions.sql | 1 +
drizzle/meta/0010_snapshot.json | 610 ++++++++++++++++++
drizzle/meta/_journal.json | 9 +-
src/lib/components/OverallRatingModal.svelte | 34 +-
src/lib/server/db/schema.js | 1 +
src/routes/admin/ratings/+page.server.js | 35 +-
src/routes/admin/ratings/+page.svelte | 15 +-
src/routes/participate/+page.server.js | 62 +-
src/routes/participate/+page.svelte | 17 +-
.../participate/audio/[id]/+page.server.js | 20 +-
.../participate/audio/[id]/+page.svelte | 191 ++++--
11 files changed, 875 insertions(+), 120 deletions(-)
create mode 100644 drizzle/0010_dual_overall_questions.sql
create mode 100644 drizzle/meta/0010_snapshot.json
diff --git a/drizzle/0010_dual_overall_questions.sql b/drizzle/0010_dual_overall_questions.sql
new file mode 100644
index 0000000..b1e74ee
--- /dev/null
+++ b/drizzle/0010_dual_overall_questions.sql
@@ -0,0 +1 @@
+ALTER TABLE `overall_rating` ADD `temporal_value` real DEFAULT 0 NOT NULL;
diff --git a/drizzle/meta/0010_snapshot.json b/drizzle/meta/0010_snapshot.json
new file mode 100644
index 0000000..bb7070e
--- /dev/null
+++ b/drizzle/meta/0010_snapshot.json
@@ -0,0 +1,610 @@
+{
+ "version": "6",
+ "dialect": "sqlite",
+ "id": "c49d536a-0a0c-4c62-aed0-7b4fcb36d414",
+ "prevId": "662a64ad-8681-48f3-93fe-e0e07aaa5cb6",
+ "tables": {
+ "audio_file": {
+ "name": "audio_file",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "filename": {
+ "name": "filename",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "content_type": {
+ "name": "content_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "data": {
+ "name": "data",
+ "type": "blob",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'null'"
+ },
+ "s3_key": {
+ "name": "s3_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "duration": {
+ "name": "duration",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "file_size": {
+ "name": "file_size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "tags": {
+ "name": "tags",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'[]'"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "invite_link": {
+ "name": "invite_link",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "participant_name": {
+ "name": "participant_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "is_used": {
+ "name": "is_used",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "used_at": {
+ "name": "used_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "tags": {
+ "name": "tags",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'[]'"
+ }
+ },
+ "indexes": {
+ "invite_link_token_unique": {
+ "name": "invite_link_token_unique",
+ "columns": [
+ "token"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "overall_rating": {
+ "name": "overall_rating",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "participant_id": {
+ "name": "participant_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "audio_file_id": {
+ "name": "audio_file_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "temporal_value": {
+ "name": "temporal_value",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "value": {
+ "name": "value",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "overall_rating_participant_id_participant_id_fk": {
+ "name": "overall_rating_participant_id_participant_id_fk",
+ "tableFrom": "overall_rating",
+ "tableTo": "participant",
+ "columnsFrom": [
+ "participant_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "overall_rating_audio_file_id_audio_file_id_fk": {
+ "name": "overall_rating_audio_file_id_audio_file_id_fk",
+ "tableFrom": "overall_rating",
+ "tableTo": "audio_file",
+ "columnsFrom": [
+ "audio_file_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "participant": {
+ "name": "participant",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "invite_token": {
+ "name": "invite_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "session_id": {
+ "name": "session_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "participant_invite_token_invite_link_token_fk": {
+ "name": "participant_invite_token_invite_link_token_fk",
+ "tableFrom": "participant",
+ "tableTo": "invite_link",
+ "columnsFrom": [
+ "invite_token"
+ ],
+ "columnsTo": [
+ "token"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "participant_progress": {
+ "name": "participant_progress",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "participant_id": {
+ "name": "participant_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "audio_file_id": {
+ "name": "audio_file_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "is_completed": {
+ "name": "is_completed",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "last_position": {
+ "name": "last_position",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "max_reached_time": {
+ "name": "max_reached_time",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "participant_progress_participant_id_participant_id_fk": {
+ "name": "participant_progress_participant_id_participant_id_fk",
+ "tableFrom": "participant_progress",
+ "tableTo": "participant",
+ "columnsFrom": [
+ "participant_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "participant_progress_audio_file_id_audio_file_id_fk": {
+ "name": "participant_progress_audio_file_id_audio_file_id_fk",
+ "tableFrom": "participant_progress",
+ "tableTo": "audio_file",
+ "columnsFrom": [
+ "audio_file_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "rating": {
+ "name": "rating",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "participant_id": {
+ "name": "participant_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "audio_file_id": {
+ "name": "audio_file_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "timestamp": {
+ "name": "timestamp",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "value": {
+ "name": "value",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "is_completed": {
+ "name": "is_completed",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "timeseries_data": {
+ "name": "timeseries_data",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "rating_participant_id_participant_id_fk": {
+ "name": "rating_participant_id_participant_id_fk",
+ "tableFrom": "rating",
+ "tableTo": "participant",
+ "columnsFrom": [
+ "participant_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "rating_audio_file_id_audio_file_id_fk": {
+ "name": "rating_audio_file_id_audio_file_id_fk",
+ "tableFrom": "rating",
+ "tableTo": "audio_file",
+ "columnsFrom": [
+ "audio_file_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "session": {
+ "name": "session",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "session_user_id_user_id_fk": {
+ "name": "session_user_id_user_id_fk",
+ "tableFrom": "session",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "user": {
+ "name": "user",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "age": {
+ "name": "age",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "username": {
+ "name": "username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "password_hash": {
+ "name": "password_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "user_username_unique": {
+ "name": "user_username_unique",
+ "columns": [
+ "username"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ }
+ },
+ "views": {},
+ "enums": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "internal": {
+ "indexes": {}
+ }
+}
diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json
index 563f770..34f4cc9 100644
--- a/drizzle/meta/_journal.json
+++ b/drizzle/meta/_journal.json
@@ -71,6 +71,13 @@
"when": 1762809580759,
"tag": "0009_admin_tags",
"breakpoints": true
+ },
+ {
+ "idx": 10,
+ "version": "6",
+ "when": 1762812000000,
+ "tag": "0010_dual_overall_questions",
+ "breakpoints": true
}
]
-}
\ No newline at end of file
+}
diff --git a/src/lib/components/OverallRatingModal.svelte b/src/lib/components/OverallRatingModal.svelte
index 0061adb..6acaa19 100644
--- a/src/lib/components/OverallRatingModal.svelte
+++ b/src/lib/components/OverallRatingModal.svelte
@@ -8,6 +8,7 @@
const dispatch = createEventDispatcher();
+ let temporalRating = 50;
let overallRating = 50;
function handleClose() {
@@ -16,7 +17,7 @@
}
function handleSubmit() {
- dispatch('submit', { overallRating });
+ dispatch('submit', { overallRating, temporalRating });
}
function handleRatingChange() {
@@ -35,12 +36,31 @@
-
-
- How good was this performance overall?
-
-
+
+
+ How well do you think the two performers instantiated the temporal relationship prescribed in the score?
+
+
+
+ Not well (0)
+ Rating: {temporalRating}
+ Perfectly (100)
+
+
+
+
+
+ How good was this performance overall?
+
\ No newline at end of file
+
diff --git a/src/lib/server/db/schema.js b/src/lib/server/db/schema.js
index f5a8a59..f110d61 100644
--- a/src/lib/server/db/schema.js
+++ b/src/lib/server/db/schema.js
@@ -88,6 +88,7 @@ export const overallRating = sqliteTable('overall_rating', {
audioFileId: text('audio_file_id')
.notNull()
.references(() => audioFile.id),
+ temporalValue: real('temporal_value').notNull().default(0),
value: real('value').notNull(), // 0-100 rating value
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
diff --git a/src/routes/admin/ratings/+page.server.js b/src/routes/admin/ratings/+page.server.js
index 6c921d8..7e08964 100644
--- a/src/routes/admin/ratings/+page.server.js
+++ b/src/routes/admin/ratings/+page.server.js
@@ -62,7 +62,14 @@ export async function load() {
// Get overall ratings
const overallRatings = await db
.select({
- overallRating,
+ overallRating: {
+ id: overallRating.id,
+ participantId: overallRating.participantId,
+ audioFileId: overallRating.audioFileId,
+ value: overallRating.value,
+ temporalValue: overallRating.temporalValue,
+ createdAt: overallRating.createdAt
+ },
participant,
audioFile: {
id: audioFile.id,
@@ -158,31 +165,45 @@ export async function load() {
overallRatingsMap[audioId][participantId] = {
value: entry.overallRating.value,
+ temporalValue: entry.overallRating.temporalValue,
createdAt: entry.overallRating.createdAt,
participantName
};
// Ensure the participant exists in timeseriesData
if (timeseriesData[audioId] && timeseriesData[audioId].participants[participantId]) {
- timeseriesData[audioId].participants[participantId].overallRating = entry.overallRating.value;
+ timeseriesData[audioId].participants[participantId].overallResponses = {
+ temporal: entry.overallRating.temporalValue,
+ overall: entry.overallRating.value,
+ createdAt: entry.overallRating.createdAt
+ };
+ timeseriesData[audioId].participants[participantId].isCompleted = true;
} else if (timeseriesData[audioId]) {
// Participant has overall rating but no timeseries data yet
timeseriesData[audioId].participants[participantId] = {
name: participantName,
ratings: [],
- isCompleted: false,
- overallRating: entry.overallRating.value
+ isCompleted: true,
+ overallResponses: {
+ temporal: entry.overallRating.temporalValue,
+ overall: entry.overallRating.value,
+ createdAt: entry.overallRating.createdAt
+ }
};
} else {
// Neither audio nor participant exists in timeseriesData yet
- timeseriesData[audioId] = {
+ timeseriesData[audioId] = {
audioFile: entry.audioFile,
participants: {
[participantId]: {
name: participantName,
ratings: [],
- isCompleted: false,
- overallRating: entry.overallRating.value
+ isCompleted: true,
+ overallResponses: {
+ temporal: entry.overallRating.temporalValue,
+ overall: entry.overallRating.value,
+ createdAt: entry.overallRating.createdAt
+ }
}
}
};
diff --git a/src/routes/admin/ratings/+page.svelte b/src/routes/admin/ratings/+page.svelte
index cce49db..2d64930 100644
--- a/src/routes/admin/ratings/+page.svelte
+++ b/src/routes/admin/ratings/+page.svelte
@@ -181,7 +181,7 @@
- Overall Rating
+ Post-Listening Ratings
0 ? Math.min(...participantData.ratings.map(r => r.timestamp)) : 0}
{@const maxTimestamp = participantData.ratings.length > 0 ? Math.max(...participantData.ratings.map(r => r.timestamp)) : 0}
{@const durationCovered = maxTimestamp - minTimestamp}
- {@const lastUpdated = participantData.ratings.length > 0 ?
+ {@const lastTimeseriesUpdate = participantData.ratings.length > 0 ?
new Date(Math.max(...participantData.ratings.map(r => new Date(r.createdAt).getTime()))) : null}
+ {@const overallResponses = participantData.overallResponses}
+ {@const lastUpdated = lastTimeseriesUpdate ?? (overallResponses ? new Date(overallResponses.createdAt) : null)}
{participantData.name}
@@ -238,10 +240,11 @@
{/if}
- {#if participantData.overallRating !== undefined}
-
- {participantData.overallRating}/100
-
+ {#if overallResponses}
+
+ Temporal: {overallResponses.temporal ?? '-'} / 100
+ Overall: {overallResponses.overall ?? '-'} / 100
+
{:else}
-
{/if}
diff --git a/src/routes/participate/+page.server.js b/src/routes/participate/+page.server.js
index 7be59a2..8b7f1f3 100644
--- a/src/routes/participate/+page.server.js
+++ b/src/routes/participate/+page.server.js
@@ -1,8 +1,9 @@
import { error } from '@sveltejs/kit';
import { db } from '$lib/server/db/index.js';
-import { inviteLink, participant, audioFile, rating } from '$lib/server/db/schema.js';
+import { inviteLink, participant, audioFile, rating, overallRating } from '$lib/server/db/schema.js';
import { eq, isNull, and, inArray } from 'drizzle-orm';
import { parseStoredTags, matchesInviteTags } from '$lib/server/tag-utils.js';
+import { env } from '$env/dynamic/private';
export async function load({ url, cookies }) {
const token = url.searchParams.get('token');
@@ -97,31 +98,47 @@ export async function load({ url, cookies }) {
const allowedAudioIds = filteredAudio.map((file) => file.id);
- // Get completed ratings for this participant (only active, non-deleted ratings for active audio files)
+ const displayContinuousRating = env.DISPLAY_CONT_RATING !== 'false';
let completedRatings = [];
if (allowedAudioIds.length > 0) {
- completedRatings = await db
- .select({
- audioFileId: rating.audioFileId
- })
- .from(rating)
- .innerJoin(audioFile, and(
- eq(rating.audioFileId, audioFile.id),
- isNull(audioFile.deletedAt) // Only count ratings for active audio files
- ))
- .where(
- and(
- eq(rating.participantId, participantId),
- eq(rating.isCompleted, true),
- isNull(rating.deletedAt), // Only count active ratings
- inArray(rating.audioFileId, allowedAudioIds)
- )
- );
+ if (displayContinuousRating) {
+ completedRatings = await db
+ .select({
+ audioFileId: rating.audioFileId
+ })
+ .from(rating)
+ .innerJoin(audioFile, and(
+ eq(rating.audioFileId, audioFile.id),
+ isNull(audioFile.deletedAt)
+ ))
+ .where(
+ and(
+ eq(rating.participantId, participantId),
+ eq(rating.isCompleted, true),
+ isNull(rating.deletedAt),
+ inArray(rating.audioFileId, allowedAudioIds)
+ )
+ );
+ } else {
+ completedRatings = await db
+ .select({ audioFileId: overallRating.audioFileId })
+ .from(overallRating)
+ .innerJoin(audioFile, and(
+ eq(overallRating.audioFileId, audioFile.id),
+ isNull(audioFile.deletedAt)
+ ))
+ .where(
+ and(
+ eq(overallRating.participantId, participantId),
+ isNull(overallRating.deletedAt),
+ inArray(overallRating.audioFileId, allowedAudioIds)
+ )
+ );
+ }
}
- const completedAudioIds = new Set(completedRatings.map(r => r.audioFileId));
+ const completedAudioIds = new Set(completedRatings.map((r) => r.audioFileId));
- // Add completion status to audio files
const audioFilesWithStatus = filteredAudio.map(file => ({
...file,
isCompleted: completedAudioIds.has(file.id)
@@ -133,6 +150,7 @@ export async function load({ url, cookies }) {
audioFiles: audioFilesWithStatus,
token,
completedCount: completedRatings.length,
- totalCount: filteredAudio.length
+ totalCount: filteredAudio.length,
+ displayContinuousRating
};
}
diff --git a/src/routes/participate/+page.svelte b/src/routes/participate/+page.svelte
index 7ab47d2..8cfed5b 100644
--- a/src/routes/participate/+page.svelte
+++ b/src/routes/participate/+page.svelte
@@ -1,6 +1,8 @@
@@ -11,8 +13,12 @@
Hello, {data.invite.participantName}!
{/if}
- Thank you for participating in our study. You will listen to audio files and rate them using
- a slider. Click "Submit Rating" when you're finished rating each file.
+ Thank you for participating in our study. You will listen to audio files
+ {#if showContinuousRating}
+ and rate them using a slider. Click "Submit Rating" when you're finished rating each file.
+ {:else}
+ and rate them.
+ {/if}
@@ -50,8 +56,11 @@
Instructions
Click on any audio file below to start listening and rating
- Use the slider to rate the audio while listening (move it as your opinion changes)
- Click "Submit Rating" when you're finished to save your rating
+ {#if showContinuousRating}
+ Use the slider to rate the audio while listening (move it as your opinion changes)
+ Click "Submit Rating" when you're finished to save your rating
+ {:else}
+ {/if}
If you've already rated a file, click "View" to see it again or submit a new rating to replace the old one
You can come back later to rate remaining files
diff --git a/src/routes/participate/audio/[id]/+page.server.js b/src/routes/participate/audio/[id]/+page.server.js
index c128c19..48f89af 100644
--- a/src/routes/participate/audio/[id]/+page.server.js
+++ b/src/routes/participate/audio/[id]/+page.server.js
@@ -3,6 +3,7 @@ import { db } from '$lib/server/db/index.js';
import { audioFile, rating, participantProgress, participant, overallRating, inviteLink } from '$lib/server/db/schema.js';
import { eq, and, isNull } from 'drizzle-orm';
import { parseStoredTags, matchesInviteTags } from '$lib/server/tag-utils.js';
+import { env } from '$env/dynamic/private';
async function ensureParticipantAccess(participantId, audioFileId) {
const participantRecords = await db
@@ -73,6 +74,8 @@ export async function load({ params, url, cookies }) {
redirect(302, `/participate?token=${token}`);
}
+ const displayContinuousRating = env.DISPLAY_CONT_RATING !== 'false';
+
const inviteRecords = await db
.select({ tags: inviteLink.tags })
.from(inviteLink)
@@ -157,6 +160,15 @@ export async function load({ params, url, cookies }) {
isNull(rating.deletedAt)
));
+ const existingOverallRatings = await db
+ .select()
+ .from(overallRating)
+ .where(and(
+ eq(overallRating.participantId, participantId),
+ eq(overallRating.audioFileId, audioId),
+ isNull(overallRating.deletedAt)
+ ));
+
return {
audioFile: {
id: audioRecord.id,
@@ -169,7 +181,9 @@ export async function load({ params, url, cookies }) {
participantId,
token,
progress,
- existingRatings
+ existingRatings,
+ existingOverallRatings,
+ displayContinuousRating
};
}
@@ -357,8 +371,9 @@ export const actions = {
const participantId = data.get('participantId');
const audioFileId = data.get('audioFileId');
const overallRatingValue = parseFloat(data.get('overallRating'));
+ const temporalRatingValue = parseFloat(data.get('temporalRating'));
- if (!participantId || !audioFileId || isNaN(overallRatingValue)) {
+ if (!participantId || !audioFileId || isNaN(overallRatingValue) || isNaN(temporalRatingValue)) {
return { error: 'Invalid overall rating data' };
}
@@ -384,6 +399,7 @@ export const actions = {
id: overallRatingId,
participantId,
audioFileId,
+ temporalValue: temporalRatingValue,
value: overallRatingValue,
createdAt: now,
updatedAt: now,
diff --git a/src/routes/participate/audio/[id]/+page.svelte b/src/routes/participate/audio/[id]/+page.svelte
index 18c044e..98c912f 100644
--- a/src/routes/participate/audio/[id]/+page.svelte
+++ b/src/routes/participate/audio/[id]/+page.svelte
@@ -1,10 +1,13 @@