added post-listening rating
This commit is contained in:
1
drizzle/0010_dual_overall_questions.sql
Normal file
1
drizzle/0010_dual_overall_questions.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE `overall_rating` ADD `temporal_value` real DEFAULT 0 NOT NULL;
|
||||
610
drizzle/meta/0010_snapshot.json
Normal file
610
drizzle/meta/0010_snapshot.json
Normal file
@@ -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": {}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="space-y-4">
|
||||
<h4 class="text-center text-lg font-medium text-gray-900">
|
||||
How well do you think the two performers instantiated the temporal relationship prescribed in the score?
|
||||
</h4>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
bind:value={temporalRating}
|
||||
on:input={handleRatingChange}
|
||||
disabled={isSubmitting}
|
||||
class="slider h-3 w-full cursor-pointer appearance-none rounded-lg bg-gray-200 {isSubmitting ? 'opacity-50' : ''}"
|
||||
/>
|
||||
<div class="flex justify-between text-sm text-gray-600">
|
||||
<span>Not well (0)</span>
|
||||
<span class="font-semibold text-indigo-600">Rating: {temporalRating}</span>
|
||||
<span>Perfectly (100)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<h4 class="text-center text-lg font-medium text-gray-900">
|
||||
How good was this performance overall?
|
||||
</h4>
|
||||
|
||||
<div class="space-y-4">
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,20 +165,30 @@ 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
|
||||
@@ -181,8 +198,12 @@ export async function load() {
|
||||
[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
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -181,7 +181,7 @@
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
|
||||
>
|
||||
Overall Rating
|
||||
Post-Listening Ratings
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
|
||||
@@ -203,8 +203,10 @@
|
||||
{@const minTimestamp = participantData.ratings.length > 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)}
|
||||
<tr>
|
||||
<td class="px-6 py-4 text-sm whitespace-nowrap text-gray-900">
|
||||
{participantData.name}
|
||||
@@ -238,10 +240,11 @@
|
||||
{/if}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm whitespace-nowrap text-gray-500">
|
||||
{#if participantData.overallRating !== undefined}
|
||||
<span class="inline-flex items-center rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800">
|
||||
{participantData.overallRating}/100
|
||||
</span>
|
||||
{#if overallResponses}
|
||||
<div class="flex flex-col text-xs text-gray-700">
|
||||
<span class="font-medium text-gray-900">Temporal: <span class="text-blue-700">{overallResponses.temporal ?? '-'} / 100</span></span>
|
||||
<span class="font-medium text-gray-900">Overall: <span class="text-indigo-700">{overallResponses.overall ?? '-'} / 100</span></span>
|
||||
</div>
|
||||
{:else}
|
||||
<span class="text-gray-400">-</span>
|
||||
{/if}
|
||||
|
||||
@@ -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,9 +98,10 @@ 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) {
|
||||
if (displayContinuousRating) {
|
||||
completedRatings = await db
|
||||
.select({
|
||||
audioFileId: rating.audioFileId
|
||||
@@ -107,21 +109,36 @@ export async function load({ url, cookies }) {
|
||||
.from(rating)
|
||||
.innerJoin(audioFile, and(
|
||||
eq(rating.audioFileId, audioFile.id),
|
||||
isNull(audioFile.deletedAt) // Only count ratings for active audio files
|
||||
isNull(audioFile.deletedAt)
|
||||
))
|
||||
.where(
|
||||
and(
|
||||
eq(rating.participantId, participantId),
|
||||
eq(rating.isCompleted, true),
|
||||
isNull(rating.deletedAt), // Only count active ratings
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<script>
|
||||
export let data;
|
||||
|
||||
const showContinuousRating = data?.displayContinuousRating ?? true;
|
||||
|
||||
</script>
|
||||
|
||||
<div class="min-h-screen bg-gray-50 py-8">
|
||||
@@ -11,8 +13,12 @@
|
||||
<p class="text-lg text-gray-600">Hello, {data.invite.participantName}!</p>
|
||||
{/if}
|
||||
<p class="mt-4 text-gray-600">
|
||||
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}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -50,8 +56,11 @@
|
||||
<h2 class="mb-4 text-xl font-semibold text-gray-900">Instructions</h2>
|
||||
<ul class="list-inside list-disc space-y-2 text-gray-700">
|
||||
<li>Click on any audio file below to start listening and rating</li>
|
||||
{#if showContinuousRating}
|
||||
<li>Use the slider to rate the audio while listening (move it as your opinion changes)</li>
|
||||
<li>Click "Submit Rating" when you're finished to save your rating</li>
|
||||
{:else}
|
||||
{/if}
|
||||
<li>If you've already rated a file, click "View" to see it again or submit a new rating to replace the old one</li>
|
||||
<li>You can come back later to rate remaining files</li>
|
||||
</ul>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
export let data;
|
||||
let audioApiUrl = '';
|
||||
$: audioApiUrl = `/api/audio/${data.audioFile.id}?token=${data.token}`;
|
||||
const showContinuousRating = data?.displayContinuousRating ?? true;
|
||||
const existingOverallRatings = data?.existingOverallRatings ?? [];
|
||||
let hasSubmittedOverallRating = existingOverallRatings.length > 0;
|
||||
|
||||
let audio;
|
||||
let isPlaying = false;
|
||||
@@ -235,6 +238,9 @@ $: audioApiUrl = `/api/audio/${data.audioFile.id}?token=${data.token}`;
|
||||
hasListenedToEnd = true;
|
||||
isAudioCompleted = true;
|
||||
maxReachedTime = duration;
|
||||
if (!showContinuousRating && !hasSubmittedOverallRating) {
|
||||
showOverallRatingModal = true;
|
||||
}
|
||||
}
|
||||
|
||||
function handleRatingChange() {
|
||||
@@ -290,13 +296,14 @@ $: audioApiUrl = `/api/audio/${data.audioFile.id}?token=${data.token}`;
|
||||
}
|
||||
|
||||
async function handleOverallRatingSubmit(event) {
|
||||
const { overallRating } = event.detail;
|
||||
const { overallRating, temporalRating } = event.detail;
|
||||
isSubmittingOverallRating = true;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('participantId', data.participantId);
|
||||
formData.append('audioFileId', data.audioFile.id);
|
||||
formData.append('overallRating', overallRating.toString());
|
||||
formData.append('temporalRating', temporalRating.toString());
|
||||
|
||||
try {
|
||||
const response = await fetch('?/saveOverallRating', {
|
||||
@@ -317,9 +324,15 @@ $: audioApiUrl = `/api/audio/${data.audioFile.id}?token=${data.token}`;
|
||||
alert('Error saving overall rating. Please try again.');
|
||||
} finally {
|
||||
isSubmittingOverallRating = false;
|
||||
hasSubmittedOverallRating = true;
|
||||
}
|
||||
}
|
||||
|
||||
function openOverallRatingModal() {
|
||||
if (!isAudioCompleted || isSubmittingOverallRating) return;
|
||||
showOverallRatingModal = true;
|
||||
}
|
||||
|
||||
function handleOverallRatingClose() {
|
||||
if (isSubmittingOverallRating) return; // Prevent closing while submitting
|
||||
showOverallRatingModal = false;
|
||||
@@ -365,7 +378,11 @@ $: audioApiUrl = `/api/audio/${data.audioFile.id}?token=${data.token}`;
|
||||
<div class="rounded-lg bg-white p-8 shadow">
|
||||
<h1 class="mb-2 text-2xl font-bold text-gray-900">{data.audioFile.filename}</h1>
|
||||
<p class="mb-8 text-gray-600">
|
||||
{#if showContinuousRating}
|
||||
Listen to the audio and move the slider to rate it continuously
|
||||
{:else}
|
||||
Listen closely to the entire audio clip. Continuous slider ratings are disabled; you'll answer two quick questions after listening.
|
||||
{/if}
|
||||
</p>
|
||||
|
||||
<!-- Audio Player -->
|
||||
@@ -478,6 +495,7 @@ $: audioApiUrl = `/api/audio/${data.audioFile.id}?token=${data.token}`;
|
||||
</div>
|
||||
|
||||
<!-- Rating Slider -->
|
||||
{#if showContinuousRating}
|
||||
<div class="space-y-6">
|
||||
{#if data.existingRatings && data.existingRatings.length > 0}
|
||||
<div class="rounded-md border border-amber-200 bg-amber-50 p-4">
|
||||
@@ -552,6 +570,37 @@ $: audioApiUrl = `/api/audio/${data.audioFile.id}?token=${data.token}`;
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mt-10 rounded-lg border border-blue-200 bg-blue-50 p-6 space-y-4">
|
||||
{#if existingOverallRatings.length > 0 || hasSubmittedOverallRating}
|
||||
<p class="text-sm text-blue-800 font-medium">
|
||||
You have already answered the required questions for this clip.
|
||||
</p>
|
||||
<a
|
||||
href="/participate?token={data.token}"
|
||||
class="inline-flex items-center justify-center rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700"
|
||||
>
|
||||
Return to Dashboard
|
||||
</a>
|
||||
{:else}
|
||||
<p class="text-sm text-blue-800 font-medium">
|
||||
After fully listening to the clip, please answer two quick questions.
|
||||
</p>
|
||||
<button
|
||||
on:click={openOverallRatingModal}
|
||||
disabled={!isAudioCompleted || isSubmittingOverallRating}
|
||||
class="inline-flex items-center justify-center rounded-md px-4 py-2 text-sm font-medium text-white {(!isAudioCompleted || isSubmittingOverallRating) ? 'bg-gray-500 cursor-not-allowed' : 'bg-indigo-600 hover:bg-indigo-700'}"
|
||||
>
|
||||
Answer Questions
|
||||
</button>
|
||||
{#if !isAudioCompleted}
|
||||
<p class="text-xs text-blue-700">
|
||||
Listen to the entire clip to enable the questions.
|
||||
</p>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user