numeric options
This commit is contained in:
parent
55c4493459
commit
3be0ed058b
8 changed files with 244 additions and 47 deletions
|
|
@ -26,18 +26,31 @@ export type PartySocketOutgoing =
|
||||||
| { type: "ping" }
|
| { type: "ping" }
|
||||||
| { type: "member_payload"; payload: unknown };
|
| { type: "member_payload"; payload: unknown };
|
||||||
|
|
||||||
export type Question = {
|
type BaseQuestion = {
|
||||||
text: string;
|
text: string;
|
||||||
options: string[];
|
|
||||||
correct: number;
|
correct: number;
|
||||||
startTimestamp: number;
|
startTimestamp: number;
|
||||||
endTimestamp: number;
|
endTimestamp: number;
|
||||||
points: number;
|
points: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Question =
|
||||||
|
| (BaseQuestion & {
|
||||||
|
type: "choice";
|
||||||
|
options: string[];
|
||||||
|
})
|
||||||
|
| (BaseQuestion & {
|
||||||
|
type: "numeric";
|
||||||
|
range: {
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
export type QuizResponse = {
|
export type QuizResponse = {
|
||||||
playerId: string;
|
playerId: string;
|
||||||
selected: number;
|
selected: number;
|
||||||
|
selectedValue?: number;
|
||||||
correct: boolean;
|
correct: boolean;
|
||||||
answeredAt: number;
|
answeredAt: number;
|
||||||
pointsGained: number;
|
pointsGained: number;
|
||||||
|
|
@ -59,6 +72,7 @@ export type QuizState = {
|
||||||
{
|
{
|
||||||
playerId: string;
|
playerId: string;
|
||||||
selected: number;
|
selected: number;
|
||||||
|
selectedValue?: number;
|
||||||
correct: boolean;
|
correct: boolean;
|
||||||
pointsGained: number;
|
pointsGained: number;
|
||||||
answeredAt: number;
|
answeredAt: number;
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,17 @@ export function buildAudioMetadataQuestion(
|
||||||
analytics: PartyAnalytics,
|
analytics: PartyAnalytics,
|
||||||
index: number,
|
index: number,
|
||||||
): Question {
|
): Question {
|
||||||
const questions: Array<Omit<Question, "startTimestamp" | "endTimestamp">> = [
|
type ChoiceQuestion = Extract<Question, { type: "choice" }>;
|
||||||
|
const questions: Array<Omit<ChoiceQuestion, "startTimestamp" | "endTimestamp">> = [
|
||||||
{
|
{
|
||||||
|
type: "choice",
|
||||||
text: "Which genre appears most in the party analytics?",
|
text: "Which genre appears most in the party analytics?",
|
||||||
options: [getTopGenreName(analytics), "Electronic", "Rock", "Jazz"],
|
options: [getTopGenreName(analytics), "Electronic", "Rock", "Jazz"],
|
||||||
correct: 0,
|
correct: 0,
|
||||||
points: 10,
|
points: 10,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
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: [
|
options: [
|
||||||
getTopArtistName(analytics),
|
getTopArtistName(analytics),
|
||||||
|
|
@ -30,6 +33,7 @@ export function buildAudioMetadataQuestion(
|
||||||
points: 10,
|
points: 10,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
type: "choice",
|
||||||
text: "Which track looks most shared across the party?",
|
text: "Which track looks most shared across the party?",
|
||||||
options: [getTopTrackName(analytics), "Track B", "Track C", "Track D"],
|
options: [getTopTrackName(analytics), "Track B", "Track C", "Track D"],
|
||||||
correct: 0,
|
correct: 0,
|
||||||
|
|
|
||||||
47
api/src/party/numeric-question-generator.ts
Normal file
47
api/src/party/numeric-question-generator.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import type { db } from "../db";
|
||||||
|
import type { Question } from "../party-types";
|
||||||
|
import type { PartyAnalytics } from "./question-utils";
|
||||||
|
import { buildQuestionWindow, getQuestionRange } from "./question-utils";
|
||||||
|
|
||||||
|
type NumericQuestion = Omit<Extract<Question, { type: "numeric" }>, "startTimestamp" | "endTimestamp">;
|
||||||
|
|
||||||
|
type BuildNumericQuestionInput = {
|
||||||
|
db: typeof db;
|
||||||
|
analytics: PartyAnalytics;
|
||||||
|
index: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function getAlbumReleaseYear({
|
||||||
|
db,
|
||||||
|
analytics,
|
||||||
|
index,
|
||||||
|
}: BuildNumericQuestionInput): Promise<NumericQuestion> {
|
||||||
|
const trackName = analytics?.storyClusters?.[0]?.tracks?.[0]?.name;
|
||||||
|
const track = trackName
|
||||||
|
? await db.query.track.findFirst({
|
||||||
|
where: {
|
||||||
|
name: trackName,
|
||||||
|
},
|
||||||
|
with: {
|
||||||
|
album: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
const correct =
|
||||||
|
track?.album?.release_date?.getFullYear() ??
|
||||||
|
new Date().getFullYear() - 1 - index;
|
||||||
|
const subject = track?.album?.name ?? track?.name ?? "the album";
|
||||||
|
return {
|
||||||
|
type: "numeric",
|
||||||
|
text: `What number best matches ${subject}?`,
|
||||||
|
correct,
|
||||||
|
range: getQuestionRange(correct, 5),
|
||||||
|
points: 10,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function buildNumericQuestion(
|
||||||
|
input: BuildNumericQuestionInput,
|
||||||
|
): Promise<Question> {
|
||||||
|
return buildQuestionWindow(await getAlbumReleaseYear(input));
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import type { db } from "../db";
|
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 type { PartyAnalytics } from "./question-utils";
|
import type { PartyAnalytics } from "./question-utils";
|
||||||
import { fetchPartyMembers } from "./question-utils";
|
import { fetchPartyMembers } from "./question-utils";
|
||||||
import { buildSocialQuestion } from "./social-question-generator";
|
import { buildSocialQuestion } from "./social-question-generator";
|
||||||
|
|
||||||
export type PartyQuestionType = "audio-metadata" | "social";
|
export type PartyQuestionType = "audio-metadata" | "social" | "numeric";
|
||||||
|
|
||||||
type GenerateQuestionInput = {
|
type GenerateQuestionInput = {
|
||||||
db: typeof db;
|
db: typeof db;
|
||||||
|
|
@ -23,8 +24,11 @@ export async function generatePartyQuestion({
|
||||||
index,
|
index,
|
||||||
}: GenerateQuestionInput): Promise<Question> {
|
}: GenerateQuestionInput): Promise<Question> {
|
||||||
const members = await fetchPartyMembers(dbClient, partyId);
|
const members = await fetchPartyMembers(dbClient, partyId);
|
||||||
const type: PartyQuestionType = index % 2 === 0 ? "audio-metadata" : "social";
|
const type: PartyQuestionType =
|
||||||
|
index === 2 ? "numeric" : index % 2 === 0 ? "audio-metadata" : "social";
|
||||||
return type === "audio-metadata"
|
return type === "audio-metadata"
|
||||||
? buildAudioMetadataQuestion(analytics, index)
|
? buildAudioMetadataQuestion(analytics, index)
|
||||||
|
: type === "numeric"
|
||||||
|
? buildNumericQuestion({ db: dbClient, analytics, index })
|
||||||
: buildSocialQuestion(quizState, analytics, members, index);
|
: buildSocialQuestion(quizState, analytics, members, index);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,29 @@ export function buildQuestionWindow<T extends object>(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getQuestionDistanceScore(
|
||||||
|
selectedValue: number,
|
||||||
|
correctValue: number,
|
||||||
|
maxDistance: number,
|
||||||
|
points: number,
|
||||||
|
): number {
|
||||||
|
if (!Number.isFinite(selectedValue)) return 0;
|
||||||
|
if (maxDistance <= 0) return selectedValue === correctValue ? points : 0;
|
||||||
|
const distance = Math.abs(selectedValue - correctValue);
|
||||||
|
const ratio = Math.max(0, 1 - distance / maxDistance);
|
||||||
|
return Math.round(points * ratio);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getQuestionRange(
|
||||||
|
correctValue: number,
|
||||||
|
tolerance: number,
|
||||||
|
): { min: number; max: number } {
|
||||||
|
return {
|
||||||
|
min: Math.floor(correctValue - tolerance),
|
||||||
|
max: Math.ceil(correctValue + tolerance),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function getTopGenreName(analytics: PartyAnalytics): string {
|
export function getTopGenreName(analytics: PartyAnalytics): string {
|
||||||
return analytics?.groupSummary?.mostSharedGenres?.[0]?.name ?? "Hip-Hop";
|
return analytics?.groupSummary?.mostSharedGenres?.[0]?.name ?? "Hip-Hop";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,24 +15,28 @@ export function buildSocialQuestion(
|
||||||
members: PartyQuestionMember[],
|
members: PartyQuestionMember[],
|
||||||
index: number,
|
index: number,
|
||||||
): Question {
|
): Question {
|
||||||
|
type ChoiceQuestion = Extract<Question, { type: "choice" }>;
|
||||||
const leader = getCurrentLeader(quizState, members);
|
const leader = getCurrentLeader(quizState, members);
|
||||||
const diverse = getMostDiverseMember(analytics, members);
|
const diverse = getMostDiverseMember(analytics, members);
|
||||||
const aligned = getMostAlignedMember(analytics, members);
|
const aligned = getMostAlignedMember(analytics, members);
|
||||||
|
|
||||||
const questions: Array<Omit<Question, "startTimestamp" | "endTimestamp">> = [
|
const questions: Array<Omit<ChoiceQuestion, "startTimestamp" | "endTimestamp">> = [
|
||||||
{
|
{
|
||||||
|
type: "choice",
|
||||||
text: "Who is leading the quiz right now?",
|
text: "Who is leading the quiz right now?",
|
||||||
options: buildMemberOptions(leader, members),
|
options: buildMemberOptions(leader, members),
|
||||||
correct: 0,
|
correct: 0,
|
||||||
points: 10,
|
points: 10,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
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: buildMemberOptions(diverse, members),
|
options: buildMemberOptions(diverse, members),
|
||||||
correct: 0,
|
correct: 0,
|
||||||
points: 10,
|
points: 10,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
type: "choice",
|
||||||
text: "Which member seems most aligned with the rest of the party?",
|
text: "Which member seems most aligned with the rest of the party?",
|
||||||
options: buildMemberOptions(aligned, members),
|
options: buildMemberOptions(aligned, members),
|
||||||
correct: 0,
|
correct: 0,
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { partyMember } from "../db/schema";
|
||||||
import { generatePartyQuestion } from "../party/question-generator";
|
import { generatePartyQuestion } from "../party/question-generator";
|
||||||
import type { PartyAnalytics } from "../party/question-utils";
|
import type { PartyAnalytics } from "../party/question-utils";
|
||||||
import { updatePartyData } from "../party/state";
|
import { updatePartyData } from "../party/state";
|
||||||
import type { QuizResponse, QuizRound, QuizState } from "../party-types";
|
import type { Question, QuizResponse, QuizRound, QuizState } from "../party-types";
|
||||||
|
|
||||||
const TOTAL_QUESTIONS = 5;
|
const TOTAL_QUESTIONS = 5;
|
||||||
|
|
||||||
|
|
@ -95,25 +95,28 @@ export class QuizWorkflow extends ConfiguredInstance {
|
||||||
|
|
||||||
receivedPlayers.add(response.playerId);
|
receivedPlayers.add(response.playerId);
|
||||||
const answeredAt = Date.now();
|
const answeredAt = Date.now();
|
||||||
const isCorrect = response.selected === question.correct;
|
const selectedValue = response.selected;
|
||||||
const pointsGained = isCorrect ? question.points : 0;
|
const isCorrect = selectedValue === question.correct;
|
||||||
const quizResponse: QuizResponse = {
|
const quizResponse: QuizResponse = {
|
||||||
...response,
|
...response,
|
||||||
|
selectedValue,
|
||||||
correct: isCorrect,
|
correct: isCorrect,
|
||||||
answeredAt,
|
answeredAt,
|
||||||
pointsGained,
|
pointsGained: 0,
|
||||||
};
|
};
|
||||||
quizState.answers[response.playerId] = quizResponse;
|
quizState.answers[response.playerId] = quizResponse;
|
||||||
round.responses.push(quizResponse);
|
round.responses.push(quizResponse);
|
||||||
|
|
||||||
if (isCorrect) {
|
await QuizWorkflow.updatePartyData(partyId, quizState);
|
||||||
quizState.scores[response.playerId] =
|
}
|
||||||
(quizState.scores[response.playerId] ?? 0) + pointsGained;
|
|
||||||
|
for (const [playerId, gained] of QuizWorkflow.scoreRound(round)) {
|
||||||
|
quizState.scores[playerId] =
|
||||||
|
(quizState.scores[playerId] ?? 0) + gained;
|
||||||
}
|
}
|
||||||
|
|
||||||
await QuizWorkflow.updatePartyData(partyId, quizState);
|
await QuizWorkflow.updatePartyData(partyId, quizState);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Quiz complete
|
// Quiz complete
|
||||||
quizState.status = "results";
|
quizState.status = "results";
|
||||||
|
|
@ -134,14 +137,7 @@ export class QuizWorkflow extends ConfiguredInstance {
|
||||||
partyId: string,
|
partyId: string,
|
||||||
quizState: QuizState,
|
quizState: QuizState,
|
||||||
index: number,
|
index: number,
|
||||||
): Promise<{
|
): Promise<Question> {
|
||||||
text: string;
|
|
||||||
options: string[];
|
|
||||||
correct: number;
|
|
||||||
startTimestamp: number;
|
|
||||||
endTimestamp: number;
|
|
||||||
points: number;
|
|
||||||
}> {
|
|
||||||
const partyRecord = await db.query.party.findFirst({
|
const partyRecord = await db.query.party.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: partyId,
|
id: partyId,
|
||||||
|
|
@ -157,6 +153,46 @@ export class QuizWorkflow extends ConfiguredInstance {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static scoreRound(round: QuizRound): Array<[string, number]> {
|
||||||
|
if (round.question.type !== "numeric") {
|
||||||
|
return round.responses.map((response): [string, number] => [
|
||||||
|
response.playerId,
|
||||||
|
response.correct ? round.question.points : 0,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ordered = round.responses
|
||||||
|
.map((response) => ({
|
||||||
|
response,
|
||||||
|
distance: Math.abs((response.selectedValue ?? response.selected) - round.question.correct),
|
||||||
|
}))
|
||||||
|
.sort((a, b) => a.distance - b.distance);
|
||||||
|
|
||||||
|
const groups: Array<{ distance: number; responses: QuizResponse[] }> = [];
|
||||||
|
for (const item of ordered) {
|
||||||
|
const group = groups.at(-1);
|
||||||
|
if (!group || group.distance !== item.distance) {
|
||||||
|
groups.push({ distance: item.distance, responses: [item.response] });
|
||||||
|
} else {
|
||||||
|
group.responses.push(item.response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const scoringGroups = groups.slice(0, Math.max(0, groups.length - 1));
|
||||||
|
if (groups.length <= 1) {
|
||||||
|
return round.responses.map((response) => [response.playerId, round.question.points]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups.flatMap((group, index) => {
|
||||||
|
const factor = (groups.length - index - 1) / (groups.length - 1);
|
||||||
|
const gained = Math.round(round.question.points * factor);
|
||||||
|
return group.responses.map((response): [string, number] => [
|
||||||
|
response.playerId,
|
||||||
|
gained,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@DBOS.step()
|
@DBOS.step()
|
||||||
private static async getPartyMembers(
|
private static async getPartyMembers(
|
||||||
partyId: string,
|
partyId: string,
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import {
|
||||||
ProgressValue,
|
ProgressValue,
|
||||||
} from "#/components/ui/progress";
|
} from "#/components/ui/progress";
|
||||||
import { Section, SectionTitle } from "#/components/ui/section";
|
import { Section, SectionTitle } from "#/components/ui/section";
|
||||||
|
import { Slider } from "#/components/ui/slider";
|
||||||
import { Spinner } from "#/components/ui/spinner";
|
import { Spinner } from "#/components/ui/spinner";
|
||||||
import { useParty } from "#/hooks/use-party";
|
import { useParty } from "#/hooks/use-party";
|
||||||
import { useUser } from "#/hooks/user";
|
import { useUser } from "#/hooks/user";
|
||||||
|
|
@ -35,6 +36,7 @@ export function Question() {
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const [now, setNow] = useState(() => Date.now());
|
const [now, setNow] = useState(() => Date.now());
|
||||||
const [selected, setSelected] = useState<number | null>(null);
|
const [selected, setSelected] = useState<number | null>(null);
|
||||||
|
const [selectedValue, setSelectedValue] = useState<number | null>(null);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const question = party?.data?.currentQuestion;
|
const question = party?.data?.currentQuestion;
|
||||||
|
|
||||||
|
|
@ -46,6 +48,12 @@ export function Question() {
|
||||||
return () => window.clearInterval(timer);
|
return () => window.clearInterval(timer);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!question) return;
|
||||||
|
setSelected(null);
|
||||||
|
setSelectedValue(question.type === "numeric" ? question.range.min : null);
|
||||||
|
}, [question]);
|
||||||
|
|
||||||
if (!question) return null;
|
if (!question) return null;
|
||||||
|
|
||||||
const partyId = party.id;
|
const partyId = party.id;
|
||||||
|
|
@ -54,6 +62,11 @@ export function Question() {
|
||||||
const hasResponded = user ? party.data.answers[user.id] != null : false;
|
const hasResponded = user ? party.data.answers[user.id] != null : false;
|
||||||
const currentSelection =
|
const currentSelection =
|
||||||
selected ?? (user ? (party.data.answers[user.id]?.selected ?? null) : null);
|
selected ?? (user ? (party.data.answers[user.id]?.selected ?? null) : null);
|
||||||
|
const currentNumericSelection =
|
||||||
|
selectedValue ??
|
||||||
|
(user ? (party.data.answers[user.id]?.selectedValue ?? null) : null);
|
||||||
|
const numericMin = question.type === "numeric" ? question.range.min : 0;
|
||||||
|
const numericMax = question.type === "numeric" ? question.range.max : 0;
|
||||||
const progressValue = Math.max(
|
const progressValue = Math.max(
|
||||||
0,
|
0,
|
||||||
Math.min(
|
Math.min(
|
||||||
|
|
@ -64,7 +77,7 @@ export function Question() {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
async function handleAnswer(optionIndex: number) {
|
async function handleChoiceAnswer(optionIndex: number) {
|
||||||
if (!partyId || hasResponded || isSubmitting) return;
|
if (!partyId || hasResponded || isSubmitting) return;
|
||||||
setSelected(optionIndex);
|
setSelected(optionIndex);
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
|
|
@ -77,6 +90,20 @@ export function Question() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleNumericAnswer() {
|
||||||
|
if (!partyId || hasResponded || isSubmitting) return;
|
||||||
|
const value = selectedValue;
|
||||||
|
if (value == null) return;
|
||||||
|
setIsSubmitting(true);
|
||||||
|
try {
|
||||||
|
await client.api.party({ partyId }).quiz.response.post({
|
||||||
|
selected: value,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Section>
|
<Section>
|
||||||
<SectionTitle>Question {party.data.questionIndex + 1}</SectionTitle>
|
<SectionTitle>Question {party.data.questionIndex + 1}</SectionTitle>
|
||||||
|
|
@ -102,7 +129,44 @@ export function Question() {
|
||||||
</ItemDescription>
|
</ItemDescription>
|
||||||
</ItemContent>
|
</ItemContent>
|
||||||
</Item>
|
</Item>
|
||||||
{question.options.map((option, index) => {
|
{question.type === "numeric" ? (
|
||||||
|
<Item variant="muted">
|
||||||
|
<ItemContent>
|
||||||
|
<ItemHeader>
|
||||||
|
<ItemTitle>Choose a value</ItemTitle>
|
||||||
|
<ItemDescription>
|
||||||
|
Closest guesses get more points
|
||||||
|
</ItemDescription>
|
||||||
|
</ItemHeader>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Slider
|
||||||
|
min={numericMin}
|
||||||
|
max={numericMax}
|
||||||
|
step={1}
|
||||||
|
value={
|
||||||
|
currentNumericSelection != null
|
||||||
|
? [currentNumericSelection]
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onValueChange={(value) => setSelectedValue(value[0] ?? null)}
|
||||||
|
/>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
Exact value:{" "}
|
||||||
|
<span className="font-medium text-foreground">
|
||||||
|
{currentNumericSelection ?? question.correct}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ItemContent>
|
||||||
|
<Button
|
||||||
|
disabled={hasResponded || isSubmitting || selectedValue == null}
|
||||||
|
onClick={() => void handleNumericAnswer()}
|
||||||
|
>
|
||||||
|
{isSubmitting ? <Spinner /> : "Answer"}
|
||||||
|
</Button>
|
||||||
|
</Item>
|
||||||
|
) : (
|
||||||
|
question.options?.map((option, index) => {
|
||||||
const isSelected = currentSelection === index;
|
const isSelected = currentSelection === index;
|
||||||
return (
|
return (
|
||||||
<Item key={option} variant={isSelected ? "outline" : "default"}>
|
<Item key={option} variant={isSelected ? "outline" : "default"}>
|
||||||
|
|
@ -117,13 +181,14 @@ export function Question() {
|
||||||
<Button
|
<Button
|
||||||
variant={isSelected ? "secondary" : "default"}
|
variant={isSelected ? "secondary" : "default"}
|
||||||
disabled={hasResponded || isSubmitting}
|
disabled={hasResponded || isSubmitting}
|
||||||
onClick={() => handleAnswer(index)}
|
onClick={() => handleChoiceAnswer(index)}
|
||||||
>
|
>
|
||||||
{isSubmitting && isSelected ? <Spinner /> : "Answer"}
|
{isSubmitting && isSelected ? <Spinner /> : "Answer"}
|
||||||
</Button>
|
</Button>
|
||||||
</Item>
|
</Item>
|
||||||
);
|
);
|
||||||
})}
|
})
|
||||||
|
)}
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Section>
|
</Section>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue