Compare commits

..

No commits in common. "ac506795c68f2bb89322831d854058babc9724d7" and "e4459833f42b343cca27f1b8de1eada061c03605" have entirely different histories.

11 changed files with 124 additions and 191 deletions

View file

@ -86,8 +86,8 @@ export async function buildAudioMetadataQuestion(
question: { question: {
type: "choice", type: "choice",
text: "What song is currently playing?", text: "What song is currently playing?",
options: currentSongOptions.options, options: currentSongOptions,
correct: currentSongOptions.correct, correct: 0,
points: 10, points: 10,
song: topSong ?? undefined, song: topSong ?? undefined,
hideSongTitle: true, hideSongTitle: true,
@ -96,27 +96,27 @@ export async function buildAudioMetadataQuestion(
} }
} }
const genreNames = buildOrderedOptions(getMostSharedGenreNames(analytics), 4); const genreOptions = buildOrderedOptions(
if (genreNames) { getMostSharedGenreNames(analytics),
const topGenre = genreNames[0]; 4,
if (topGenre) { );
const genreOptions = buildOptionsWithCorrect(topGenre, genreNames, 4);
if (genreOptions) { if (genreOptions) {
const topGenre = genreOptions[0];
if (topGenre) {
questions.push({ questions.push({
key: `audio:genre:${topGenre}`, key: `audio:genre:${topGenre}`,
subjectKey: `genre:${topGenre}`, subjectKey: `genre:${topGenre}`,
question: { question: {
type: "choice", type: "choice",
text: "Which genre appears most in the party analytics?", text: "Which genre appears most in the party analytics?",
options: genreOptions.options, options: genreOptions,
correct: genreOptions.correct, correct: 0,
points: 10, points: 10,
song: topSong ?? undefined, song: topSong ?? undefined,
}, },
}); });
} }
} }
}
const topArtistEntities = getFairQuestionArtists(analytics, members, history); const topArtistEntities = getFairQuestionArtists(analytics, members, history);
const topArtists = topArtistEntities.map((artist) => artist.name); const topArtists = topArtistEntities.map((artist) => artist.name);
@ -135,8 +135,8 @@ export async function buildAudioMetadataQuestion(
question: { question: {
type: "choice", type: "choice",
text: "Which artist shows up most often in the shared audio data?", text: "Which artist shows up most often in the shared audio data?",
options: artistOptions.options, options: artistOptions,
correct: artistOptions.correct, correct: 0,
points: 10, points: 10,
song: topSong ?? undefined, song: topSong ?? undefined,
}, },
@ -161,8 +161,8 @@ export async function buildAudioMetadataQuestion(
getTrackFairness(topTrack, members, history).memberCount > 1 getTrackFairness(topTrack, members, history).memberCount > 1
? "Which track looks most shared across the party?" ? "Which track looks most shared across the party?"
: "Which track stands out in the party analytics?", : "Which track stands out in the party analytics?",
options: trackOptions.options, options: trackOptions,
correct: trackOptions.correct, correct: 0,
points: 10, points: 10,
song: topSong ?? undefined, song: topSong ?? undefined,
}, },
@ -184,8 +184,8 @@ export async function buildAudioMetadataQuestion(
question: { question: {
type: "choice", type: "choice",
text: `Which artist appears on "${topTrack.albumName}"?`, text: `Which artist appears on "${topTrack.albumName}"?`,
options: artistOptions.options, options: artistOptions,
correct: artistOptions.correct, correct: 0,
points: 10, points: 10,
song: topSong ?? undefined, song: topSong ?? undefined,
}, },
@ -226,8 +226,8 @@ export async function buildAudioMetadataQuestion(
question: { question: {
type: "choice", type: "choice",
text: "Which of these tracks came out first?", text: "Which of these tracks came out first?",
options: options.options, options,
correct: options.correct, correct: 0,
points: 10, points: 10,
song: topSong ?? undefined, song: topSong ?? undefined,
}, },
@ -250,8 +250,8 @@ export async function buildAudioMetadataQuestion(
question: { question: {
type: "choice", type: "choice",
text: "Which of these tracks came out most recently?", text: "Which of these tracks came out most recently?",
options: options.options, options,
correct: options.correct, correct: 0,
points: 10, points: 10,
song: topSong ?? undefined, song: topSong ?? undefined,
}, },
@ -294,8 +294,8 @@ export async function buildAudioMetadataQuestion(
question: { question: {
type: "choice", type: "choice",
text: `What's the longest track by ${artistName}?`, text: `What's the longest track by ${artistName}?`,
options: options.options, options,
correct: options.correct, correct: 0,
points: 10, points: 10,
song: topSong ?? undefined, song: topSong ?? undefined,
}, },
@ -326,8 +326,8 @@ export async function buildAudioMetadataQuestion(
question: { question: {
type: "choice", type: "choice",
text: `Who performs "${topTrack.name}"?`, text: `Who performs "${topTrack.name}"?`,
options: artistOptions.options, options: artistOptions,
correct: artistOptions.correct, correct: 0,
points: 10, points: 10,
song: trackSong ?? topSong ?? undefined, song: trackSong ?? topSong ?? undefined,
}, },
@ -348,9 +348,8 @@ export async function buildAudioMetadataQuestion(
question: { question: {
type: "choice", type: "choice",
text: `What is the name of this track by ${correctArtist}?`, text: `What is the name of this track by ${correctArtist}?`,
options: trackNameOptions.options, options: trackNameOptions,
correct: trackNameOptions.correct, correct: 0,
hideSongTitle: true,
points: 10, points: 10,
song: trackSong ?? topSong ?? undefined, song: trackSong ?? topSong ?? undefined,
}, },
@ -370,8 +369,8 @@ export async function buildAudioMetadataQuestion(
question: { question: {
type: "choice", type: "choice",
text: "Which song is this audio clip from?", text: "Which song is this audio clip from?",
options: alternateSongOptions.options, options: alternateSongOptions,
correct: alternateSongOptions.correct, correct: 0,
points: 10, points: 10,
song: topSong ?? undefined, song: topSong ?? undefined,
hideSongTitle: true, hideSongTitle: true,
@ -398,8 +397,8 @@ export async function buildAudioMetadataQuestion(
question: { question: {
type: "choice", type: "choice",
text: `"${topTrack.name}" appears on which album?`, text: `"${topTrack.name}" appears on which album?`,
options: albumOptions.options, options: albumOptions,
correct: albumOptions.correct, correct: 0,
points: 10, points: 10,
song: trackSong ?? topSong ?? undefined, song: trackSong ?? topSong ?? undefined,
}, },

View file

@ -745,28 +745,14 @@ export function buildOptionsWithCorrect(
correct: string, correct: string,
candidates: string[], candidates: string[],
desiredCount: number, desiredCount: number,
): { options: string[]; correct: number } | null { ): string[] | null {
if (!isUsableText(correct)) return null; if (!isUsableText(correct)) return null;
const options = uniqueStrings([ const options = uniqueStrings([
correct, correct,
...candidates.filter((c) => isUsableText(c) && c !== correct), ...candidates.filter((c) => isUsableText(c) && c !== correct),
]); ]);
const optionCount = getAvailableOptionCount(options.length, desiredCount); const optionCount = getAvailableOptionCount(options.length, desiredCount);
if (!optionCount) return null; return optionCount ? options.slice(0, optionCount) : null;
const shuffled = shuffleOptions(options.slice(0, optionCount));
return { options: shuffled, correct: shuffled.indexOf(correct) };
}
function shuffleOptions(options: string[]): string[] {
const shuffled = options.slice();
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
const value = shuffled[i];
if (value === undefined) continue;
shuffled[i] = shuffled[j] ?? value;
shuffled[j] = value;
}
return shuffled;
} }
export function pickRandom<T>(items: T[]): T | null { export function pickRandom<T>(items: T[]): T | null {

View file

@ -3,7 +3,6 @@ import type { Question, QuizState } from "../party-types";
import { import {
buildMemberOptions, buildMemberOptions,
buildMemberPairOptions, buildMemberPairOptions,
buildOptionsWithCorrect,
buildQuestionWindow, buildQuestionWindow,
getCurrentLeader, getCurrentLeader,
getFairQuestionTracks, getFairQuestionTracks,
@ -35,18 +34,15 @@ export async function buildSocialQuestion(
if (hasMultipleMembers && hasClearLeader(quizState)) { if (hasMultipleMembers && hasClearLeader(quizState)) {
const leader = getCurrentLeader(quizState, members); const leader = getCurrentLeader(quizState, members);
const options = buildMemberOptions(leader, members); const options = buildMemberOptions(leader, members);
const choices = options if (options) {
? buildOptionsWithCorrect(leader.name, options, options.length)
: null;
if (choices) {
questions.push({ questions.push({
key: "social:leader", key: "social:leader",
subjectKey: `member:${leader.userId}`, subjectKey: `member:${leader.userId}`,
question: { question: {
type: "choice", type: "choice",
text: "Who is leading the quiz right now?", text: "Who is leading the quiz right now?",
options: choices.options, options,
correct: choices.correct, correct: 0,
points: 10, points: 10,
song: topSong ?? undefined, song: topSong ?? undefined,
}, },
@ -58,18 +54,15 @@ export async function buildSocialQuestion(
const diverse = getMostDiverseMember(analytics, members); const diverse = getMostDiverseMember(analytics, members);
if (diverse) { if (diverse) {
const options = buildMemberOptions(diverse, members); const options = buildMemberOptions(diverse, members);
const choices = options if (options) {
? buildOptionsWithCorrect(diverse.name, options, options.length)
: null;
if (choices) {
questions.push({ questions.push({
key: "social:diverse", key: "social:diverse",
subjectKey: `member:${diverse.userId}`, subjectKey: `member:${diverse.userId}`,
question: { question: {
type: "choice", type: "choice",
text: "Who looks like the most diverse listener in the party?", text: "Who looks like the most diverse listener in the party?",
options: choices.options, options,
correct: choices.correct, correct: 0,
points: 10, points: 10,
song: topSong ?? undefined, song: topSong ?? undefined,
}, },
@ -105,10 +98,7 @@ export async function buildSocialQuestion(
albumName: topTrack.albumName, albumName: topTrack.albumName,
}); });
const options = buildMemberOptions(topListener, members); const options = buildMemberOptions(topListener, members);
const choices = options if (options) {
? buildOptionsWithCorrect(topListener.name, options, options.length)
: null;
if (choices) {
questions.push({ questions.push({
key: `social:track-listener:${topTrack.name}`, key: `social:track-listener:${topTrack.name}`,
subjectKey: `track:${topTrack.name}`, subjectKey: `track:${topTrack.name}`,
@ -116,8 +106,8 @@ export async function buildSocialQuestion(
question: { question: {
type: "choice", type: "choice",
text: `Who listens the most to "${topTrack.name}"?`, text: `Who listens the most to "${topTrack.name}"?`,
options: choices.options, options,
correct: choices.correct, correct: 0,
points: 10, points: 10,
song: trackSong ?? topSong ?? undefined, song: trackSong ?? topSong ?? undefined,
}, },
@ -133,18 +123,15 @@ export async function buildSocialQuestion(
if (memberA && memberB) { if (memberA && memberB) {
const correctPair = `${memberA.name} & ${memberB.name}`; const correctPair = `${memberA.name} & ${memberB.name}`;
const pairOptions = buildMemberPairOptions(members, correctPair); const pairOptions = buildMemberPairOptions(members, correctPair);
const pairChoices = pairOptions if (pairOptions) {
? buildOptionsWithCorrect(correctPair, pairOptions, pairOptions.length)
: null;
if (pairChoices) {
questions.push({ questions.push({
key: `social:pair:${memberA.userId}:${memberB.userId}`, key: `social:pair:${memberA.userId}:${memberB.userId}`,
subjectKey: `pair:${[memberA.userId, memberB.userId].sort().join("|")}`, subjectKey: `pair:${[memberA.userId, memberB.userId].sort().join("|")}`,
question: { question: {
type: "choice", type: "choice",
text: "Which two players share the most musical taste?", text: "Which two players share the most musical taste?",
options: pairChoices.options, options: pairOptions,
correct: pairChoices.correct, correct: 0,
points: 10, points: 10,
song: topSong ?? undefined, song: topSong ?? undefined,
}, },

View file

@ -1,7 +1,7 @@
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import type { db as Db } from "../db"; import type { db as Db } from "../db";
import { party } from "../db/schema"; import { party } from "../db/schema";
import type { PartySocketEvent, PartyStatus, QuizState } from "../party-types"; import type { PartySocketEvent, QuizState } from "../party-types";
import { publishDeviceEventForUser } from "../routes/device-socket"; import { publishDeviceEventForUser } from "../routes/device-socket";
import { pubsub } from "../routes/party-socket"; import { pubsub } from "../routes/party-socket";
@ -9,7 +9,6 @@ export async function updatePartyData(
db: typeof Db, db: typeof Db,
id: string, id: string,
data: QuizState, data: QuizState,
status?: PartyStatus,
) { ) {
const members = await db.query.partyMember.findMany({ const members = await db.query.partyMember.findMany({
where: { where: {
@ -25,29 +24,22 @@ export async function updatePartyData(
}, },
}); });
if (!partyObject) throw new Error("Missing party"); if (!partyObject) throw new Error("Missing party");
const updatedParty = {
...partyObject,
status: status ?? partyObject.status,
data,
};
await db
.update(party)
.set({
data,
...(status ? { status } : {}),
lastUpdated: new Date(),
})
.where(eq(party.id, id));
pubsub.publishPartyData(id, { pubsub.publishPartyData(id, {
type: "party_status", type: "party_status",
party: updatedParty, party: {
...partyObject,
data,
},
members, members,
}); });
const event: PartySocketEvent = { const event: PartySocketEvent = {
type: "party_status", type: "party_status",
party: updatedParty, party: {
...partyObject,
data,
},
members, members,
}; };
for (const member of members) { for (const member of members) {
@ -56,10 +48,20 @@ export async function updatePartyData(
`user:${member.userId}`, `user:${member.userId}`,
JSON.stringify({ JSON.stringify({
type: "party_status", type: "party_status",
party: updatedParty, party: {
...partyObject,
data,
},
members, members,
}), }),
); );
void publishDeviceEventForUser(member.userId, event); void publishDeviceEventForUser(member.userId, event);
} }
await db
.update(party)
.set({
data: data,
lastUpdated: new Date(),
})
.where(eq(party.id, id));
} }

View file

@ -73,15 +73,6 @@ function isDeviceQuizResponsePayload(
); );
} }
function isValidAnswer(quizData: QuizState, selected: number): boolean {
const question = quizData.currentQuestion;
if (!question) return false;
if (question.type === "choice") {
return selected >= 0 && selected < question.options.length;
}
return selected >= question.range.min && selected <= question.range.max;
}
function sendDeviceEvent(deviceId: string, event: DeviceProxyEvent) { function sendDeviceEvent(deviceId: string, event: DeviceProxyEvent) {
if (!devProxySocket) { if (!devProxySocket) {
console.log("[device-socket] no dev proxy for event", deviceId, event.type); console.log("[device-socket] no dev proxy for event", deviceId, event.type);
@ -244,13 +235,6 @@ async function forwardDevicePayload(deviceId: string, payload: unknown) {
}); });
return; return;
} }
if (!isValidAnswer(quizData, payload.QuizResponse)) {
sendDeviceEvent(deviceId, {
type: "error",
message: "Invalid answer.",
});
return;
}
await DBOS.send( await DBOS.send(
quizData.workflowId, quizData.workflowId,

View file

@ -4,7 +4,7 @@ import { betterAuthElysia } from "../auth";
import { db } from "../db"; import { db } from "../db";
import { getMemberRecord, getPartyStatus } from "../party-data"; import { getMemberRecord, getPartyStatus } from "../party-data";
import type { PartySocketEvent } from "../party-types"; import type { PartySocketEvent, QuizState } from "../party-types";
export function userTopic(userId: string) { export function userTopic(userId: string) {
return `user:${userId}`; return `user:${userId}`;
@ -63,6 +63,31 @@ async function subscribeWsToParty(
return nextPartyId; return nextPartyId;
} }
export async function broadcastQuizState(
ws: { publish: (topic: string, message: string) => void },
partyId: string,
) {
const partyRecord = await db.query.party.findFirst({
where: { id: partyId },
});
if (!partyRecord) return;
const quizData = ((partyRecord.data ?? {}) as Record<string, unknown>).quiz as
| QuizState
| undefined;
if (!quizData) return;
if (!quizData) return;
ws.publish(
partyTopic(partyId),
JSON.stringify({
type: "quiz_state",
quiz: quizData,
}),
);
}
export const topic = { export const topic = {
user: userTopic, user: userTopic,
party: partyTopic, party: partyTopic,
@ -93,6 +118,8 @@ export const partySocketApp = new Elysia()
); );
return; return;
} }
await broadcastQuizState(ws, subscribedPartyId);
}, },
message: async (ws, message) => { message: async (ws, message) => {
const data = ws.data; const data = ws.data;

View file

@ -26,15 +26,6 @@ function broadcastStatusToMembers(
const quizWf = new QuizWorkflow(); const quizWf = new QuizWorkflow();
function isValidAnswer(quizData: QuizState, selected: number): boolean {
const question = quizData.currentQuestion;
if (!question) return false;
if (question.type === "choice") {
return selected >= 0 && selected < question.options.length;
}
return selected >= question.range.min && selected <= question.range.max;
}
export const quizRoutes = new Elysia() export const quizRoutes = new Elysia()
.use(betterAuthElysia) .use(betterAuthElysia)
.group("/party/:partyId/quiz", (app) => .group("/party/:partyId/quiz", (app) =>
@ -60,6 +51,11 @@ export const quizRoutes = new Elysia()
return { error: "Quiz already running" }; return { error: "Quiz already running" };
} }
const handle = await DBOS.startWorkflow(quizWf.startQuiz, {
queueName: quizQueue.name,
enqueueOptions: { queuePartitionKey: params.partyId },
})(params.partyId);
await db await db
.update(party) .update(party)
.set({ .set({
@ -69,11 +65,6 @@ export const quizRoutes = new Elysia()
}) })
.where(eq(party.id, params.partyId)); .where(eq(party.id, params.partyId));
const handle = await DBOS.startWorkflow(quizWf.startQuiz, {
queueName: quizQueue.name,
enqueueOptions: { queuePartitionKey: params.partyId },
})(params.partyId);
const status = await getPartyStatus(params.partyId); const status = await getPartyStatus(params.partyId);
broadcastStatusToMembers(status); broadcastStatusToMembers(status);
@ -136,12 +127,6 @@ export const quizRoutes = new Elysia()
.post( .post(
"/response", "/response",
async ({ user, body, params, set }) => { async ({ user, body, params, set }) => {
const membership = await getMemberRecord(db, user.id);
if (!membership || membership.partyId !== params.partyId) {
set.status = 403;
return { error: "Not a member of this party" };
}
const party = await db.query.party.findFirst({ const party = await db.query.party.findFirst({
where: { where: {
id: params.partyId, id: params.partyId,
@ -163,25 +148,10 @@ export const quizRoutes = new Elysia()
set.status = 500; set.status = 500;
return { error: "Workflow ID not found" }; return { error: "Workflow ID not found" };
} }
if (
body.questionIndex !== undefined &&
body.questionIndex !== quizData.questionIndex
) {
set.status = 409;
return { error: "Stale question response" };
}
if (!isValidAnswer(quizData, body.selected)) {
set.status = 400;
return { error: "Invalid answer" };
}
await DBOS.send( await DBOS.send(
quizData.workflowId, quizData.workflowId,
{ { playerId: user.id, selected: body.selected },
playerId: user.id,
selected: body.selected,
questionIndex: body.questionIndex,
},
"quiz_responses", "quiz_responses",
); );
@ -191,7 +161,6 @@ export const quizRoutes = new Elysia()
auth: true, auth: true,
body: t.Object({ body: t.Object({
selected: t.Integer(), selected: t.Integer(),
questionIndex: t.Optional(t.Integer()),
}), }),
}, },
), ),

View file

@ -24,7 +24,6 @@ export const quizQueue = new WorkflowQueue("quiz_queue", {
type Response = { type Response = {
playerId: string; playerId: string;
selected: number; selected: number;
questionIndex?: number;
}; };
export class QuizWorkflow extends ConfiguredInstance { export class QuizWorkflow extends ConfiguredInstance {
@ -54,15 +53,15 @@ export class QuizWorkflow extends ConfiguredInstance {
history: [], history: [],
}; };
// Initialize quiz state
await QuizWorkflow.updatePartyData(partyId, quizState);
// Get party members to initialize scores // Get party members to initialize scores
let members = await QuizWorkflow.getPartyMembers(partyId); let members = await QuizWorkflow.getPartyMembers(partyId);
for (const member of members) { for (const member of members) {
quizState.scores[member.userId] = 0; quizState.scores[member.userId] = 0;
} }
// Initialize quiz state after scores are ready.
await QuizWorkflow.updatePartyData(partyId, quizState);
for (let i = 0; i < TOTAL_QUESTIONS; i++) { for (let i = 0; i < TOTAL_QUESTIONS; i++) {
quizState.status = "running"; quizState.status = "running";
quizState.questionIndex = i; quizState.questionIndex = i;
@ -97,7 +96,7 @@ export class QuizWorkflow extends ConfiguredInstance {
if (response === null) { if (response === null) {
// Timeout - fill in missing players with no answer // Timeout - fill in missing players with no answer
const now = await DBOS.now(); const now = Date.now();
if (now < question.endTimestamp) continue; if (now < question.endTimestamp) continue;
for (const memberId of memberIds) { for (const memberId of memberIds) {
if (!receivedPlayers.has(memberId)) { if (!receivedPlayers.has(memberId)) {
@ -117,17 +116,10 @@ export class QuizWorkflow extends ConfiguredInstance {
break; break;
} }
if (!memberIds.has(response.playerId)) continue;
if (
response.questionIndex !== undefined &&
response.questionIndex !== i
) {
continue;
}
if (receivedPlayers.has(response.playerId)) continue; if (receivedPlayers.has(response.playerId)) continue;
receivedPlayers.add(response.playerId); receivedPlayers.add(response.playerId);
const answeredAt = await DBOS.now(); const answeredAt = Date.now();
const selectedValue = response.selected; const selectedValue = response.selected;
const isCorrect = selectedValue === question.correct; const isCorrect = selectedValue === question.correct;
const quizResponse: QuizResponse = { const quizResponse: QuizResponse = {
@ -156,16 +148,15 @@ export class QuizWorkflow extends ConfiguredInstance {
// Quiz complete // Quiz complete
quizState.status = "results"; quizState.status = "results";
await QuizWorkflow.updatePartyData(partyId, quizState, "ended"); await QuizWorkflow.updatePartyData(partyId, quizState);
} }
@DBOS.step() @DBOS.step()
private static async updatePartyData( private static async updatePartyData(
partyId: string, partyId: string,
quizState: QuizState, quizState: QuizState,
status?: "created" | "started" | "ended",
): Promise<void> { ): Promise<void> {
await updatePartyData(db, partyId, quizState, status); await updatePartyData(db, partyId, quizState);
} }
@DBOS.step() @DBOS.step()
@ -224,9 +215,10 @@ export class QuizWorkflow extends ConfiguredInstance {
} }
if (groups.length <= 1) { if (groups.length <= 1) {
const onlyDistance = groups[0]?.distance ?? Number.POSITIVE_INFINITY; return ordered.map(({ response }) => [
const gained = onlyDistance === 0 ? round.question.points : 0; response.playerId,
return ordered.map(({ response }) => [response.playerId, gained]); round.question.points,
]);
} }
const scoredAnswers = groups.flatMap((group, index) => { const scoredAnswers = groups.flatMap((group, index) => {

View file

@ -55,11 +55,6 @@ export function Question() {
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const question = party?.data?.currentQuestion; const question = party?.data?.currentQuestion;
const questionStartTimestamp = question?.startTimestamp ?? null; const questionStartTimestamp = question?.startTimestamp ?? null;
const questionResetKey = question
? `${party?.data?.questionIndex ?? 0}:${question.startTimestamp}`
: null;
const numericInitialValue =
question?.type === "numeric" ? question.range.min : null;
const questionAnnouncement = question const questionAnnouncement = question
? getQuestionAnnouncement(question) ? getQuestionAnnouncement(question)
: null; : null;
@ -73,10 +68,10 @@ export function Question() {
}, []); }, []);
useEffect(() => { useEffect(() => {
if (!questionResetKey) return; if (!question) return;
setSelected(null); setSelected(null);
setSelectedValue(numericInitialValue); setSelectedValue(question.type === "numeric" ? question.range.min : null);
}, [questionResetKey, numericInitialValue]); }, [question]);
useEffect(() => { useEffect(() => {
if ( if (
@ -106,7 +101,6 @@ export function Question() {
); );
const partyId = party.id; const partyId = party.id;
const questionIndex = party.data.questionIndex;
const timeLeft = formatTimeLeft(question.endTimestamp - now); const timeLeft = formatTimeLeft(question.endTimestamp - now);
const answeredCount = Object.keys(party.data.answers).length; const answeredCount = Object.keys(party.data.answers).length;
const hasResponded = user ? party.data.answers[user.id] != null : false; const hasResponded = user ? party.data.answers[user.id] != null : false;
@ -134,7 +128,6 @@ export function Question() {
try { try {
await client.api.party({ partyId }).quiz.response.post({ await client.api.party({ partyId }).quiz.response.post({
selected: optionIndex, selected: optionIndex,
questionIndex,
}); });
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
@ -149,7 +142,6 @@ export function Question() {
try { try {
await client.api.party({ partyId }).quiz.response.post({ await client.api.party({ partyId }).quiz.response.post({
selected: value, selected: value,
questionIndex,
}); });
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);

View file

@ -50,12 +50,7 @@ export function usePartySocket({
}; };
ws.onmessage = (event) => { ws.onmessage = (event) => {
let parsed: PartySocketEvent; const parsed = JSON.parse(event.data) as PartySocketEvent;
try {
parsed = JSON.parse(event.data) as PartySocketEvent;
} catch {
return;
}
handlerRef.current?.(parsed); handlerRef.current?.(parsed);
}; };

View file

@ -23,7 +23,7 @@ const config = defineConfig({
}), }),
], ],
server: { server: {
allowedHosts: ["aura.rpi1.danbulant.cloud", "fern.rpi1.danbulant.cloud"], allowedHosts: ["aura.rpi1.danbulant.cloud"],
proxy: { proxy: {
"/api": { "/api": {
target: "http://localhost:4000", target: "http://localhost:4000",