Skip to content

Commit

Permalink
feat: updated question results ui
Browse files Browse the repository at this point in the history
  • Loading branch information
maxwiseman committed Dec 30, 2024
1 parent 672c7e1 commit b18897c
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 139 deletions.
20 changes: 14 additions & 6 deletions apps/multiplayer-server/src/message-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,10 @@ export async function handleIncomingMessage(

if (
Object.values(currentChannelData.users).length ===
Object.values(currentChannelData.gameState.answers).length
Object.values(currentChannelData.gameState.answers).length &&
!Object.values(currentChannelData.gameState.answers)
.map((a) => a.correct)
.includes("grading")
) {
console.log(
"Everyone has answered!",
Expand All @@ -96,15 +99,15 @@ export async function handleIncomingMessage(
if (msg.answer.toLowerCase() === correctAnswer?.letter.toLowerCase()) {
// send back true
handleAllAnswers({
correct: true,
correct: "correct",
time: answerTime,
answer: msg.answer,
});
return;
} else {
// send back false
handleAllAnswers({
correct: false,
correct: "incorrect",
time: answerTime,
answer: msg.answer,
});
Expand All @@ -115,12 +118,17 @@ export async function handleIncomingMessage(
if (question.answer.toLowerCase().includes(msg.answer.toLowerCase())) {
// send back true
handleAllAnswers({
correct: true,
correct: "correct",
time: answerTime,
answer: msg.answer,
});
return;
}
handleAllAnswers({
correct: "grading",
time: answerTime,
answer: msg.answer,
});
const data = await generateObject({
model: openai("gpt-4o-mini", { structuredOutputs: true }),
prompt: `Your job is to determine whether or not a participant's answer to a question is correct or not. If they are wrong, give a SHORT, neutral explanation as if you are talking to the participant. THE ANSWER IS CORRECT EVEN IF THEY MADE A SPELLING OR GRAMMAR ERROR! The question information is as follows:
Expand All @@ -139,7 +147,7 @@ export async function handleIncomingMessage(
}),
});
handleAllAnswers({
correct: data.object.correct,
correct: data.object.correct ? "correct" : "incorrect",
time: answerTime,
answer: msg.answer,
});
Expand Down Expand Up @@ -173,7 +181,7 @@ async function nextQuestion(
...serverQuestionSchema.parse(nextQuestionData),
asked: new Date(),
questionTime: 10,
qNumber: 1,
qNumber: lastQuestionData ? lastQuestionData.qNumber + 1 : 1,
},
answers: {},
};
Expand Down
2 changes: 1 addition & 1 deletion apps/multiplayer-server/src/schema/from-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const clientQuestionStateSchema = z.object({
z.object({
time: z.coerce.date(),
answer: z.string(),
correct: z.boolean(),
correct: z.enum(["correct", "incorrect", "grading"]),
}),
)
.optional(),
Expand Down
2 changes: 1 addition & 1 deletion apps/multiplayer-server/src/schema/from-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { lobbyStateSchema, serverQuestionSchema, userSchema } from "./shared";
export const serverAnswerSchema = z.object({
time: z.date(),
answer: z.string(),
correct: z.boolean(),
correct: z.enum(["incorrect", "correct", "grading"]),
});
export const serverQuestionStateSchema = z.object({
stage: z.literal("question"),
Expand Down
79 changes: 79 additions & 0 deletions apps/nextjs/src/app/_components/stages/question.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"use client";

import { useState } from "react";
import { IconCheck, IconX } from "@tabler/icons-react";
import { useSelector } from "@xstate/store/react";

import { cn } from "@scibo/ui";
import { Spinner } from "@scibo/ui/spinner";

import { QuizQuestion } from "../quiz/question";
import websocketStore from "../websocket/xstate";

export function Question() {
const state = useSelector(websocketStore, (state) => state.context.state);
const [answered, setAnswered] = useState<boolean[]>([]);

if (state.stage !== "question") return null;

return (
<div className="max-w-[60rem]">
{answered[state.question.qNumber] === true ? (
<div className="flex w-[30rem] flex-col gap-4">
<Leaderboard />
</div>
) : (
<QuizQuestion
onSubmit={(val) => {
websocketStore.send({
type: "sendMessage",
message: { type: "answerQuestion", answer: val },
});
const answeredCopy = [...answered];
answeredCopy[state.question.qNumber] = true;
setAnswered(answeredCopy);
}}
question={state.question}
/>
)}
</div>
);
}

export function Leaderboard() {
const state = useSelector(websocketStore, (state) => state.context.state);
const users = useSelector(websocketStore, (state) => state.context.users);
const self = useSelector(
websocketStore,
(state) => state.context.currentUser,
);

if (state.stage !== "question") return null;

return Object.keys(users).map((uId) => {
const answer = state.answers?.[uId]?.correct;
let icon: React.ReactNode = <></>;
switch (answer) {
case "correct":
icon = <IconCheck className="h-6 w-6" />;
break;
case "incorrect":
icon = <IconX className="h-6 w-6" />;
break;
default:
icon = <Spinner className="h-6 w-6" />;
break;
}

return (
<div
className={cn(
"flex w-full items-center gap-4 rounded-md bg-muted px-4 py-2 text-2xl",
{ "text-muted-foreground": uId !== self?.id },
)}
>
{icon} {users[uId]?.username}
</div>
);
});
}
12 changes: 12 additions & 0 deletions apps/nextjs/src/app/_components/websocket/xstate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,18 @@ const websocketStore = createStore({
return { status: "connecting" } as Partial<websocketContext>;
},
disconnect: (context) => {
websocketStore.send({
type: "updateStatus",
...{
currentUser: null,
users: {},
messageHistory: [],
error: null,
status: "disconnected",
state: { stage: "lobby" },
socket: null,
},
});
context.socket?.close(1000, "User disconnected");
return {};
},
Expand Down
134 changes: 3 additions & 131 deletions apps/nextjs/src/app/test/page.tsx
Original file line number Diff line number Diff line change
@@ -1,153 +1,25 @@
"use client";

import { useEffect, useState } from "react";
import { useSelector } from "@xstate/store/react";
import { AnimatePresence, motion } from "motion/react";

import { blurTransition } from "../_components/blur-transition";
import { Orb } from "../_components/quiz/orb";
import { QuizQuestion } from "../_components/quiz/question";
import { SpinText } from "../_components/spin-text";
import { Lobby } from "../_components/stages/lobby";
import { Question } from "../_components/stages/question";
import websocketStore from "../_components/websocket/xstate";

export default function Page() {
const state = useSelector(websocketStore, (state) => state.context.state);
const status = useSelector(websocketStore, (state) => state.context.status);
const users = useSelector(websocketStore, (state) => state.context.users);
const [answered, setAnswered] = useState<boolean[]>([]);

// useEffect(() => {
// setAnswered(false);
// }, [state.question?.qNumber]);

if (state.stage === "lobby" || status !== "connected")
return (
<div className="flex h-full w-full items-center justify-center">
<div className="flex h-full w-full items-center justify-center p-4">
<Lobby />
</div>
);
if (state.stage === "question")
return (
<div className="flex h-full min-h-max w-full items-center justify-center p-4">
<div className="max-w-[60rem]">
{answered[state.question.qNumber] === true ? (
<>
<Orb />
<div className="fixed bottom-8 left-1/2 w-max -translate-x-1/2 cursor-default text-muted-foreground/50">
<AnimatePresence>
<motion.div
key={"waitingFor"}
layout
layoutId="waitingFor"
className="inline"
>
Waiting for:{" "}
</motion.div>
{Object.keys(users).map((uId) =>
Object.keys(state.answers ?? {}).includes(uId) ? (
""
) : (
<motion.div
className="inline"
layout
layoutId={users[uId]?.id}
key={users[uId]?.id}
{...blurTransition}
>
{users[uId]?.username + " "}
</motion.div>
),
)}
</AnimatePresence>
</div>
</>
) : (
<QuizQuestion
onSubmit={(val) => {
websocketStore.send({
type: "sendMessage",
message: { type: "answerQuestion", answer: val },
});
const answeredCopy = [...answered];
answeredCopy[state.question.qNumber] = true;
setAnswered(answeredCopy);
}}
question={state.question}
/>
)}
</div>
<Question />
</div>
);
}

// export default function Page() {
// const status = useSelector(websocketStore, (state) => state.context.status);
// const users = useSelector(websocketStore, (state) => state.context.users);
// const messages = useSelector(
// websocketStore,
// (state) => state.context.messageHistory,
// );

// if (status != "connected") {
// return (
// <div className="flex h-full w-full flex-col items-center justify-center">
// <Button
// onClick={() => {
// websocketStore.send({
// type: "connect",
// username: `Tester${Math.floor(1000 + Math.random() * 9000)}`,
// userId: `Tester${Math.floor(1000 + Math.random() * 9000)}`,
// room: "group-chat",
// });
// }}
// loading={status === "connecting"}
// >
// Connect
// </Button>
// </div>
// );
// }

// return (
// <div className="flex h-full w-full flex-col items-center justify-center gap-4">
// {/* <Quiz
// // debug={process.env.NODE_ENV === "development"}
// // questions={exampleData}
// /> */}
// <form
// onSubmit={(e) => {
// e.preventDefault();
// websocketStore.send({
// type: "sendMessage",
// message: {
// type: "message",
// content: (
// e.currentTarget.elements.namedItem(
// "text-box",
// ) as HTMLInputElement
// ).value,
// },
// });
// (
// e.currentTarget.elements.namedItem("text-box") as HTMLInputElement
// ).value = "";
// }}
// className="flex w-96 gap-2"
// >
// <Input id="text-box" placeholder="Type something..." />
// <Button type="submit" size="icon" className="aspect-square">
// <IconArrowUp />
// </Button>
// </form>
// <div>{users.map((user) => user.username)}</div>
// <div>
// {messages.map((msg) => (
// <div>
// {msg.user.username}: {msg.content}
// </div>
// ))}
// </div>
// </div>
// );
// }

0 comments on commit b18897c

Please sign in to comment.