Compare commits
No commits in common. "2ca3de3c75426d5e3a13b56696528545d1a8c380" and "032e65629766cad31847c330078e5347e5719a19" have entirely different histories.
2ca3de3c75
...
032e656297
7 changed files with 20 additions and 493 deletions
|
|
@ -6,12 +6,10 @@ import {
|
||||||
type PartyAnalytics,
|
type PartyAnalytics,
|
||||||
type PartyQuestionMember,
|
type PartyQuestionMember,
|
||||||
pickQuestionCandidate,
|
pickQuestionCandidate,
|
||||||
selectQuestionSong,
|
|
||||||
} from "../question-utils";
|
} from "../question-utils";
|
||||||
import { buildSocialQuestion } from "../social-question-generator";
|
import { buildSocialQuestion } from "../social-question-generator";
|
||||||
|
|
||||||
type Db = typeof import("../../db").db;
|
type Db = typeof import("../../db").db;
|
||||||
type Song = NonNullable<Question["song"]>;
|
|
||||||
|
|
||||||
function makeChoiceQuestion(
|
function makeChoiceQuestion(
|
||||||
text: string,
|
text: string,
|
||||||
|
|
@ -63,39 +61,6 @@ function createFakeDb(trackReleaseDate: Date | null) {
|
||||||
} as unknown as Db;
|
} as unknown as Db;
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeSong(id: string, platformId: string, name: string): Song {
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
albumId: "album-1",
|
|
||||||
platform: "spotify",
|
|
||||||
platform_id: platformId,
|
|
||||||
name,
|
|
||||||
popularity: 1,
|
|
||||||
duration: 1,
|
|
||||||
explicit: false,
|
|
||||||
disc_number: 1,
|
|
||||||
track_number: 1,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function createSongFallbackDb(rows: Song[]) {
|
|
||||||
return {
|
|
||||||
query: {
|
|
||||||
topTrack: {
|
|
||||||
findMany: vi.fn(async () =>
|
|
||||||
rows.map((row, index) => ({
|
|
||||||
position: index + 1,
|
|
||||||
track: row,
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
track: {
|
|
||||||
findMany: vi.fn(async () => []),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as unknown as Db;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("question generation", () => {
|
describe("question generation", () => {
|
||||||
it("skips repeated question keys, subjects, and text", () => {
|
it("skips repeated question keys, subjects, and text", () => {
|
||||||
const history: QuizRound[] = [
|
const history: QuizRound[] = [
|
||||||
|
|
@ -265,79 +230,4 @@ describe("question generation", () => {
|
||||||
|
|
||||||
expect(question).toBeNull();
|
expect(question).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("selects a fresh party song when the current one was already used", async () => {
|
|
||||||
const db = createSongFallbackDb([
|
|
||||||
makeSong("track-1", "spotify:track:one", "One"),
|
|
||||||
makeSong("track-2", "spotify:track:two", "Two"),
|
|
||||||
]);
|
|
||||||
const question = {
|
|
||||||
type: "choice" as const,
|
|
||||||
text: "Which genre appears most in the party analytics?",
|
|
||||||
correct: 0,
|
|
||||||
startTimestamp: 1,
|
|
||||||
endTimestamp: 2,
|
|
||||||
points: 10,
|
|
||||||
options: ["A", "B"],
|
|
||||||
questionKey: "audio:genre:pop",
|
|
||||||
subjectKey: "genre:pop",
|
|
||||||
};
|
|
||||||
|
|
||||||
const song = await selectQuestionSong({
|
|
||||||
db,
|
|
||||||
analytics: null,
|
|
||||||
members: [{ userId: "a", name: "A" }],
|
|
||||||
history: [
|
|
||||||
{
|
|
||||||
questionIndex: 0,
|
|
||||||
question: {
|
|
||||||
...question,
|
|
||||||
song: makeSong("track-1", "spotify:track:one", "One"),
|
|
||||||
},
|
|
||||||
responses: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
question,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(song?.platform_id).toBe("spotify:track:two");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("keeps a song-target question on the same track", async () => {
|
|
||||||
const db = createSongFallbackDb([
|
|
||||||
makeSong("track-1", "spotify:track:one", "One"),
|
|
||||||
makeSong("track-2", "spotify:track:two", "Two"),
|
|
||||||
]);
|
|
||||||
const question = {
|
|
||||||
type: "choice" as const,
|
|
||||||
text: "What song is currently playing?",
|
|
||||||
correct: 0,
|
|
||||||
startTimestamp: 1,
|
|
||||||
endTimestamp: 2,
|
|
||||||
points: 10,
|
|
||||||
options: ["A", "B"],
|
|
||||||
questionKey: "audio:current-song:One",
|
|
||||||
subjectKey: "track:One",
|
|
||||||
hideSongTitle: true,
|
|
||||||
song: {
|
|
||||||
...makeSong("track-1", "spotify:track:one", "One"),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const song = await selectQuestionSong({
|
|
||||||
db,
|
|
||||||
analytics: null,
|
|
||||||
members: [{ userId: "a", name: "A" }],
|
|
||||||
history: [
|
|
||||||
{
|
|
||||||
questionIndex: 0,
|
|
||||||
question,
|
|
||||||
responses: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
question,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(song?.platform_id).toBe("spotify:track:one");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import type { QuizState } from "../../party-types";
|
import type { QuizState } from "../../party-types";
|
||||||
import * as audioQuestionGenerator from "../audio-question-generator";
|
|
||||||
|
|
||||||
vi.mock("../audio-question-generator", () => ({
|
vi.mock("../audio-question-generator", () => ({
|
||||||
buildAudioMetadataQuestion: vi.fn(async () => null),
|
buildAudioMetadataQuestion: vi.fn(async () => null),
|
||||||
|
|
@ -48,59 +47,4 @@ describe("generatePartyQuestion", () => {
|
||||||
|
|
||||||
expect(question).toBeNull();
|
expect(question).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("attaches a fallback song to generated questions", async () => {
|
|
||||||
vi.mocked(
|
|
||||||
audioQuestionGenerator.buildAudioMetadataQuestion,
|
|
||||||
).mockResolvedValueOnce({
|
|
||||||
type: "choice",
|
|
||||||
text: "Which genre appears most in the party analytics?",
|
|
||||||
correct: 0,
|
|
||||||
startTimestamp: 1,
|
|
||||||
endTimestamp: 2,
|
|
||||||
points: 10,
|
|
||||||
options: ["A", "B"],
|
|
||||||
questionKey: "audio:genre:pop",
|
|
||||||
subjectKey: "genre:pop",
|
|
||||||
});
|
|
||||||
|
|
||||||
const quizState = {
|
|
||||||
status: "running",
|
|
||||||
workflowId: null,
|
|
||||||
questionIndex: 0,
|
|
||||||
currentQuestion: null,
|
|
||||||
answers: {},
|
|
||||||
scores: {},
|
|
||||||
history: [],
|
|
||||||
} as QuizState;
|
|
||||||
|
|
||||||
const question = await generatePartyQuestion({
|
|
||||||
db: {
|
|
||||||
query: {
|
|
||||||
partyMember: {
|
|
||||||
findMany: vi.fn(async () => [{ userId: "a", user: { name: "A" } }]),
|
|
||||||
},
|
|
||||||
topTrack: {
|
|
||||||
findMany: vi.fn(async () => [
|
|
||||||
{
|
|
||||||
position: 1,
|
|
||||||
track: {
|
|
||||||
id: "track-1",
|
|
||||||
platform: "spotify",
|
|
||||||
platform_id: "spotify:track:one",
|
|
||||||
name: "One",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as never,
|
|
||||||
partyId: "party-1",
|
|
||||||
quizState,
|
|
||||||
analytics: null,
|
|
||||||
index: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(question?.song?.platform_id).toBe("spotify:track:one");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,8 @@ import type { db } from "../db";
|
||||||
import type { Question, QuizState } from "../party-types";
|
import type { Question, QuizState } from "../party-types";
|
||||||
import { buildAudioMetadataQuestion } from "./audio-question-generator";
|
import { buildAudioMetadataQuestion } from "./audio-question-generator";
|
||||||
import { buildNumericQuestion } from "./numeric-question-generator";
|
import { buildNumericQuestion } from "./numeric-question-generator";
|
||||||
import {
|
import type { PartyAnalytics } from "./question-utils";
|
||||||
fetchPartyMembers,
|
import { fetchPartyMembers } from "./question-utils";
|
||||||
type PartyAnalytics,
|
|
||||||
selectQuestionSong,
|
|
||||||
} from "./question-utils";
|
|
||||||
import { buildSocialQuestion } from "./social-question-generator";
|
import { buildSocialQuestion } from "./social-question-generator";
|
||||||
|
|
||||||
export type PartyQuestionType = "audio-metadata" | "social" | "numeric";
|
export type PartyQuestionType = "audio-metadata" | "social" | "numeric";
|
||||||
|
|
@ -39,44 +36,37 @@ export async function generatePartyQuestion({
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const type of typeOrder) {
|
for (const type of typeOrder) {
|
||||||
let question: Question | null = null;
|
|
||||||
if (type === "audio-metadata") {
|
if (type === "audio-metadata") {
|
||||||
question = await buildAudioMetadataQuestion(
|
const q = await buildAudioMetadataQuestion(
|
||||||
dbClient,
|
dbClient,
|
||||||
analytics,
|
analytics,
|
||||||
index,
|
index,
|
||||||
quizState.history,
|
quizState.history,
|
||||||
);
|
);
|
||||||
} else if (type === "social") {
|
if (q) return q;
|
||||||
question = await buildSocialQuestion(
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "social") {
|
||||||
|
const q = await buildSocialQuestion(
|
||||||
dbClient,
|
dbClient,
|
||||||
quizState,
|
quizState,
|
||||||
analytics,
|
analytics,
|
||||||
members,
|
members,
|
||||||
index,
|
index,
|
||||||
);
|
);
|
||||||
} else {
|
if (q) return q;
|
||||||
question = await buildNumericQuestion({
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const q = await buildNumericQuestion({
|
||||||
db: dbClient,
|
db: dbClient,
|
||||||
analytics,
|
analytics,
|
||||||
index,
|
index,
|
||||||
members,
|
members,
|
||||||
history: quizState.history,
|
history: quizState.history,
|
||||||
});
|
});
|
||||||
}
|
if (q) return q;
|
||||||
|
|
||||||
if (!question) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const song = await selectQuestionSong({
|
|
||||||
db: dbClient,
|
|
||||||
analytics,
|
|
||||||
members,
|
|
||||||
history: quizState.history,
|
|
||||||
question,
|
|
||||||
});
|
|
||||||
return song ? { ...question, song } : question;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import type { InferSelectModel } from "drizzle-orm";
|
import type { InferSelectModel } from "drizzle-orm";
|
||||||
import type { db as Db } from "../db";
|
import type { db as Db } from "../db";
|
||||||
import type { track as trackTable } from "../db/schema";
|
import type { track as trackTable } from "../db/schema";
|
||||||
import type { Question, QuizRound } from "../party-types";
|
import type { QuizRound } from "../party-types";
|
||||||
|
|
||||||
export type PartyQuestionMember = {
|
export type PartyQuestionMember = {
|
||||||
userId: string;
|
userId: string;
|
||||||
|
|
@ -262,228 +262,6 @@ export async function resolveQuestionSong(
|
||||||
return song;
|
return song;
|
||||||
}
|
}
|
||||||
|
|
||||||
type SongSelectionInput = {
|
|
||||||
db: typeof Db;
|
|
||||||
analytics: PartyAnalytics;
|
|
||||||
members: PartyQuestionMember[];
|
|
||||||
history: QuizRound[];
|
|
||||||
question: Question;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function selectQuestionSong({
|
|
||||||
db,
|
|
||||||
analytics,
|
|
||||||
members,
|
|
||||||
history,
|
|
||||||
question,
|
|
||||||
}: SongSelectionInput): Promise<QuestionSong | null> {
|
|
||||||
const keepSpecificSong = isSongTargetQuestion(question);
|
|
||||||
const usedPlatformIds = new Set(
|
|
||||||
history
|
|
||||||
.map((round) => round.question.song?.platform_id)
|
|
||||||
.filter((value): value is string => isUsableText(value)),
|
|
||||||
);
|
|
||||||
|
|
||||||
const candidates = await collectSongCandidates({
|
|
||||||
db,
|
|
||||||
analytics,
|
|
||||||
members,
|
|
||||||
question,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (candidates.length === 0) return question.song ?? null;
|
|
||||||
if (keepSpecificSong) return candidates[0] ?? question.song ?? null;
|
|
||||||
|
|
||||||
const freshCandidate = candidates.find(
|
|
||||||
(candidate) =>
|
|
||||||
isUsableText(candidate.platform_id) &&
|
|
||||||
!usedPlatformIds.has(candidate.platform_id),
|
|
||||||
);
|
|
||||||
return freshCandidate ?? candidates[0] ?? question.song ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function collectSongCandidates({
|
|
||||||
db,
|
|
||||||
analytics,
|
|
||||||
members,
|
|
||||||
question,
|
|
||||||
}: {
|
|
||||||
db: typeof Db;
|
|
||||||
analytics: PartyAnalytics;
|
|
||||||
members: PartyQuestionMember[];
|
|
||||||
question: Question;
|
|
||||||
}): Promise<QuestionSong[]> {
|
|
||||||
const candidates: QuestionSong[] = [];
|
|
||||||
const seen = new Set<string>();
|
|
||||||
const push = (song: QuestionSong | null | undefined) => {
|
|
||||||
if (!song || !isUsableText(song.platform_id)) return;
|
|
||||||
if (seen.has(song.platform_id)) return;
|
|
||||||
seen.add(song.platform_id);
|
|
||||||
candidates.push(song);
|
|
||||||
};
|
|
||||||
|
|
||||||
push(question.song);
|
|
||||||
|
|
||||||
const subjectSong = await resolveSongFromQuestionSubject(
|
|
||||||
db,
|
|
||||||
analytics,
|
|
||||||
question,
|
|
||||||
);
|
|
||||||
push(subjectSong);
|
|
||||||
|
|
||||||
const peopleSong = await resolveSongFromMentionedPeople(
|
|
||||||
db,
|
|
||||||
analytics,
|
|
||||||
question,
|
|
||||||
);
|
|
||||||
push(peopleSong);
|
|
||||||
|
|
||||||
const topClusterTracks = [...(analytics?.storyClusters?.[0]?.tracks ?? [])]
|
|
||||||
.filter((track) => isUsableText(track.name))
|
|
||||||
.sort((a, b) => getTrackScore(b) - getTrackScore(a));
|
|
||||||
|
|
||||||
for (const track of topClusterTracks) {
|
|
||||||
const song = await resolveQuestionSong(db, analytics, {
|
|
||||||
trackName: track.name,
|
|
||||||
artistNames: track.artists?.map((artist) => artist.name),
|
|
||||||
albumName: track.albumName,
|
|
||||||
});
|
|
||||||
push(song);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (members.length > 0) {
|
|
||||||
const topPartySongs = await fetchPartyTopSongs(db, members);
|
|
||||||
for (const song of topPartySongs) {
|
|
||||||
push(song);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return candidates;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function resolveSongFromQuestionSubject(
|
|
||||||
db: typeof Db,
|
|
||||||
analytics: PartyAnalytics,
|
|
||||||
question: Question,
|
|
||||||
): Promise<QuestionSong | null> {
|
|
||||||
const subjectKey = question.subjectKey ?? "";
|
|
||||||
if (subjectKey.startsWith("track:")) {
|
|
||||||
const trackName = subjectKey.slice("track:".length).trim();
|
|
||||||
if (!trackName) return null;
|
|
||||||
return resolveQuestionSong(db, analytics, { trackName });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (subjectKey.startsWith("artist:")) {
|
|
||||||
const artistName = subjectKey.slice("artist:".length).trim();
|
|
||||||
if (!artistName) return null;
|
|
||||||
return resolveQuestionSong(db, analytics, { artistNames: [artistName] });
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function resolveSongFromMentionedPeople(
|
|
||||||
db: typeof Db,
|
|
||||||
analytics: PartyAnalytics,
|
|
||||||
question: Question,
|
|
||||||
): Promise<QuestionSong | null> {
|
|
||||||
const subjectKey = question.subjectKey ?? "";
|
|
||||||
const userIds = subjectKey.startsWith("member:")
|
|
||||||
? [subjectKey.slice("member:".length).trim()].filter(Boolean)
|
|
||||||
: subjectKey.startsWith("pair:")
|
|
||||||
? subjectKey
|
|
||||||
.slice("pair:".length)
|
|
||||||
.split("|")
|
|
||||||
.map((value) => value.trim())
|
|
||||||
.filter(Boolean)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
if (userIds.length === 0) return null;
|
|
||||||
|
|
||||||
const tracks = [...(analytics?.storyClusters?.[0]?.tracks ?? [])]
|
|
||||||
.filter((track) => isUsableText(track.name))
|
|
||||||
.sort(
|
|
||||||
(a, b) =>
|
|
||||||
getMemberTrackScore(b, userIds) - getMemberTrackScore(a, userIds),
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const track of tracks) {
|
|
||||||
const song = await resolveQuestionSong(db, analytics, {
|
|
||||||
trackName: track.name,
|
|
||||||
artistNames: track.artists?.map((artist) => artist.name),
|
|
||||||
albumName: track.albumName,
|
|
||||||
});
|
|
||||||
if (song) return song;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchPartyTopSongs(
|
|
||||||
db: typeof Db,
|
|
||||||
members: PartyQuestionMember[],
|
|
||||||
): Promise<QuestionSong[]> {
|
|
||||||
const songs: QuestionSong[] = [];
|
|
||||||
const seen = new Set<string>();
|
|
||||||
|
|
||||||
for (const member of members) {
|
|
||||||
const rows = await db.query.topTrack.findMany({
|
|
||||||
where: {
|
|
||||||
userId: member.userId,
|
|
||||||
},
|
|
||||||
with: {
|
|
||||||
track: {
|
|
||||||
with: {
|
|
||||||
album: true,
|
|
||||||
artists: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
orderBy: {
|
|
||||||
position: "asc",
|
|
||||||
},
|
|
||||||
limit: 5,
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const row of rows) {
|
|
||||||
const song = row.track;
|
|
||||||
if (!song || !isUsableText(song.platform_id)) continue;
|
|
||||||
if (seen.has(song.platform_id)) continue;
|
|
||||||
seen.add(song.platform_id);
|
|
||||||
songs.push(song);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return songs;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isSongTargetQuestion(question: Question): boolean {
|
|
||||||
const key = question.questionKey?.toLowerCase() ?? "";
|
|
||||||
const text = question.text.toLowerCase();
|
|
||||||
return (
|
|
||||||
question.hideSongTitle === true ||
|
|
||||||
key.startsWith("audio:current-song:") ||
|
|
||||||
text.includes("what song") ||
|
|
||||||
text.includes("which song")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTrackScore(track: { memberScores?: { score: number }[] }): number {
|
|
||||||
return (track.memberScores ?? []).reduce(
|
|
||||||
(total, entry) => total + entry.score,
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMemberTrackScore(
|
|
||||||
track: { memberScores?: { userId: string; score: number }[] },
|
|
||||||
userIds: string[],
|
|
||||||
): number {
|
|
||||||
return (track.memberScores ?? []).reduce((total, entry) => {
|
|
||||||
return userIds.includes(entry.userId) ? total + entry.score : total;
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isUsableText(
|
export function isUsableText(
|
||||||
value: string | null | undefined,
|
value: string | null | undefined,
|
||||||
): value is string {
|
): value is string {
|
||||||
|
|
|
||||||
|
|
@ -75,55 +75,6 @@ export const quizRoutes = new Elysia()
|
||||||
},
|
},
|
||||||
{ auth: true },
|
{ auth: true },
|
||||||
)
|
)
|
||||||
.post(
|
|
||||||
"/restart",
|
|
||||||
async ({ user, set, params }) => {
|
|
||||||
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 currentParty = await db.query.party.findFirst({
|
|
||||||
where: {
|
|
||||||
id: params.partyId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!currentParty) {
|
|
||||||
set.status = 404;
|
|
||||||
return { error: "Party not found" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const quizData = currentParty.data as QuizState | null;
|
|
||||||
if (!quizData || quizData.status !== "results") {
|
|
||||||
set.status = 409;
|
|
||||||
return { error: "Quiz is not finished yet" };
|
|
||||||
}
|
|
||||||
|
|
||||||
await db
|
|
||||||
.update(party)
|
|
||||||
.set({
|
|
||||||
status: "started",
|
|
||||||
data: null,
|
|
||||||
lastUpdated: new Date(),
|
|
||||||
})
|
|
||||||
.where(eq(party.id, params.partyId));
|
|
||||||
|
|
||||||
const handle = await DBOS.startWorkflow(quizWf.restartQuiz, {
|
|
||||||
queueName: quizQueue.name,
|
|
||||||
enqueueOptions: { queuePartitionKey: params.partyId },
|
|
||||||
})(params.partyId);
|
|
||||||
|
|
||||||
const status = await getPartyStatus(params.partyId);
|
|
||||||
broadcastStatusToMembers(status);
|
|
||||||
|
|
||||||
return {
|
|
||||||
message: "Quiz restarted",
|
|
||||||
workflowId: handle.workflowID,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
{ auth: true },
|
|
||||||
)
|
|
||||||
.post(
|
.post(
|
||||||
"/response",
|
"/response",
|
||||||
async ({ user, body, params, set }) => {
|
async ({ user, body, params, set }) => {
|
||||||
|
|
@ -139,6 +90,8 @@ export const quizRoutes = new Elysia()
|
||||||
}
|
}
|
||||||
const quizData = party.data as QuizState | null;
|
const quizData = party.data as QuizState | null;
|
||||||
|
|
||||||
|
console.log("response quiz data", party, quizData);
|
||||||
|
|
||||||
if (!quizData || quizData.status !== "running") {
|
if (!quizData || quizData.status !== "running") {
|
||||||
set.status = 400;
|
set.status = 400;
|
||||||
return { error: "Quiz not running" };
|
return { error: "Quiz not running" };
|
||||||
|
|
|
||||||
|
|
@ -32,16 +32,6 @@ export class QuizWorkflow extends ConfiguredInstance {
|
||||||
|
|
||||||
@DBOS.workflow()
|
@DBOS.workflow()
|
||||||
async startQuiz(partyId: string): Promise<void> {
|
async startQuiz(partyId: string): Promise<void> {
|
||||||
await partyAnalysisWorkflow.analyzeParty(partyId);
|
|
||||||
await QuizWorkflow.runQuiz(partyId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@DBOS.workflow()
|
|
||||||
async restartQuiz(partyId: string): Promise<void> {
|
|
||||||
await QuizWorkflow.runQuiz(partyId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async runQuiz(partyId: string): Promise<void> {
|
|
||||||
const quizState: QuizState = {
|
const quizState: QuizState = {
|
||||||
status: "running",
|
status: "running",
|
||||||
workflowId: DBOS.workflowID ?? null,
|
workflowId: DBOS.workflowID ?? null,
|
||||||
|
|
@ -52,6 +42,8 @@ export class QuizWorkflow extends ConfiguredInstance {
|
||||||
history: [],
|
history: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
await partyAnalysisWorkflow.analyzeParty(partyId);
|
||||||
|
|
||||||
// Initialize quiz state
|
// Initialize quiz state
|
||||||
await QuizWorkflow.updatePartyData(partyId, quizState);
|
await QuizWorkflow.updatePartyData(partyId, quizState);
|
||||||
|
|
||||||
|
|
@ -150,6 +142,7 @@ export class QuizWorkflow extends ConfiguredInstance {
|
||||||
partyId: string,
|
partyId: string,
|
||||||
quizState: QuizState,
|
quizState: QuizState,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
console.log(partyId, quizState);
|
||||||
await updatePartyData(db, partyId, quizState);
|
await updatePartyData(db, partyId, quizState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -203,6 +196,7 @@ export class QuizWorkflow extends ConfiguredInstance {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const _scoringGroups = groups.slice(0, Math.max(0, groups.length - 1));
|
||||||
if (groups.length <= 1) {
|
if (groups.length <= 1) {
|
||||||
return round.responses.map((response) => [
|
return round.responses.map((response) => [
|
||||||
response.playerId,
|
response.playerId,
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,7 @@
|
||||||
import { useState } from "react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
import { Button } from "#/components/ui/button";
|
|
||||||
import { useParty } from "#/hooks/use-party";
|
import { useParty } from "#/hooks/use-party";
|
||||||
import { client } from "#/lib/eden";
|
|
||||||
|
|
||||||
export function Results() {
|
export function Results() {
|
||||||
const { party, members } = useParty();
|
const { party, members } = useParty();
|
||||||
const [isRestarting, setIsRestarting] = useState(false);
|
|
||||||
if (!party?.data) return null;
|
if (!party?.data) return null;
|
||||||
|
|
||||||
const leaderboard = members
|
const leaderboard = members
|
||||||
|
|
@ -24,22 +18,6 @@ export function Results() {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h2 className="text-2xl font-semibold text-foreground">Leaderboard</h2>
|
<h2 className="text-2xl font-semibold text-foreground">Leaderboard</h2>
|
||||||
<Button
|
|
||||||
disabled={isRestarting}
|
|
||||||
onClick={async () => {
|
|
||||||
if (isRestarting) return;
|
|
||||||
setIsRestarting(true);
|
|
||||||
try {
|
|
||||||
await client.api.party({ partyId: party.id }).quiz.restart.post();
|
|
||||||
} catch (error) {
|
|
||||||
toast((error as Error)?.message || "Failed to restart quiz.");
|
|
||||||
} finally {
|
|
||||||
setIsRestarting(false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isRestarting ? "Restarting..." : "Play again"}
|
|
||||||
</Button>
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{leaderboard.length === 0 ? (
|
{leaderboard.length === 0 ? (
|
||||||
<p className="text-sm text-muted-foreground">No scores yet.</p>
|
<p className="text-sm text-muted-foreground">No scores yet.</p>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue