From bd73051b90aa83f1c9ce23f2acc1f97fee3aad16 Mon Sep 17 00:00:00 2001 From: rxmox Date: Mon, 6 Apr 2026 13:30:54 -0600 Subject: [PATCH 1/6] Add bingo leaderboard API with live Pusher updates Implement GET /api/events/:eventId/leaderboard and PUT /api/events/:eventId/leaderboard/score endpoints for the web organizer dashboard. Leaderboard shows participant name, profile photo, connections count (computed from ParticipantConnection records), lines completed, and completion status. Scores update via Pusher leaderboard-updated event for live refresh. - Add linesCompleted and completed fields to Participant model - Create leaderboard controller with getLeaderboard and updateScore - Trigger leaderboard-updated Pusher event on score changes and connection create/delete - Update API reference, database schema, and real-time events documentation --- shatter-backend/docs/API_REFERENCE.md | 88 +++++++++ shatter-backend/docs/DATABASE_SCHEMA.md | 10 +- shatter-backend/docs/REALTIME_EVENTS_GUIDE.md | 35 ++++ .../src/controllers/leaderboard_controller.ts | 177 ++++++++++++++++++ .../participant_connections_controller.ts | 7 + .../src/models/participant_model.ts | 4 + shatter-backend/src/routes/event_routes.ts | 3 + 7 files changed, 320 insertions(+), 4 deletions(-) create mode 100644 shatter-backend/src/controllers/leaderboard_controller.ts diff --git a/shatter-backend/docs/API_REFERENCE.md b/shatter-backend/docs/API_REFERENCE.md index 082a2ed..97f64ac 100644 --- a/shatter-backend/docs/API_REFERENCE.md +++ b/shatter-backend/docs/API_REFERENCE.md @@ -39,6 +39,8 @@ - [DELETE `/api/events/:eventId`](#delete-apieventseventid) - [GET `/api/events/createdEvents/user/:userId`](#get-apieventscreatedeventsuseruserid) - [PUT `/api/events/:eventId`](#put-apieventseventid) + - [GET `/api/events/:eventId/leaderboard`](#get-apieventseventidleaderboard) + - [PUT `/api/events/:eventId/leaderboard/score`](#put-apieventseventidleaderboardscore) - [Bingo (`/api/bingo`)](#bingo-apibingo) - [POST `/api/bingo/createBingo`](#post-apibingocreatebingo) - [GET `/api/bingo/getBingo/:eventId`](#get-apibingogetbingoeventid) @@ -90,6 +92,8 @@ Quick reference of all implemented endpoints. See detailed sections below for re | POST | `/api/events/:eventId/leave` | Protected | Leave event as participant | | DELETE | `/api/events/:eventId` | Protected | Delete/cancel event (host-only) | | GET | `/api/events/createdEvents/user/:userId` | Protected | Get events created by user | +| GET | `/api/events/:eventId/leaderboard` | Protected | Get bingo leaderboard for event | +| PUT | `/api/events/:eventId/leaderboard/score` | Protected | Update own bingo score | | POST | `/api/bingo/createBingo` | Protected | Create bingo game for event | | GET | `/api/bingo/getBingo/:eventId` | Public | Get bingo by event ID | | PUT | `/api/bingo/updateBingo` | Protected | Update bingo game | @@ -1068,6 +1072,90 @@ Update an existing event's basic information (host only). Only the fields provid --- +### GET `/api/events/:eventId/leaderboard` + +Get the bingo leaderboard for an event. Returns participants sorted by completion status and lines completed. Connections count is computed from ParticipantConnection records (unique connected partners). + +- **Auth:** Protected + +**URL Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `eventId` | ObjectId | Yes | The event ID | + +**Response (200):** + +```json +[ + { + "participantId": "abc123", + "name": "Jane Doe", + "profilePhoto": "https://example.com/photo.jpg", + "connectionsCount": 5, + "linesCompleted": 3, + "completed": true + } +] +``` + +**Sort order:** `completed` desc (completed first), then `linesCompleted` desc, then `connectionsCount` desc. + +**Fields:** + +| Field | Type | Description | +|-------|------|-------------| +| `participantId` | string | Participant document ID | +| `name` | string | User's display name | +| `profilePhoto` | string \| null | User's profile photo URL | +| `connectionsCount` | number | Number of unique people connected with (from ParticipantConnection records) | +| `linesCompleted` | number | Completed bingo lines (vertical, horizontal, diagonal) | +| `completed` | boolean | Whether the entire bingo sheet is filled | + +--- + +### PUT `/api/events/:eventId/leaderboard/score` + +Update the authenticated user's bingo score for an event. Triggers a Pusher `leaderboard-updated` event on channel `event-{eventId}`. + +- **Auth:** Protected + +**URL Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `eventId` | ObjectId | Yes | The event ID | + +**Request Body:** + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `linesCompleted` | number | No | Number of completed bingo lines | +| `completed` | boolean | No | Whether the entire bingo sheet is filled | + +At least one field must be provided. + +**Response (200):** + +```json +{ + "participantId": "abc123", + "name": "Jane Doe", + "linesCompleted": 3, + "completed": false, + "connectionsCount": 2 +} +``` + +**Error Responses:** + +| Status | Description | +|--------|-------------| +| 400 | Invalid eventId or no valid fields provided | +| 404 | Participant not found for this event | + +--- + ## Bingo (`/api/bingo`) ### POST `/api/bingo/createBingo` diff --git a/shatter-backend/docs/DATABASE_SCHEMA.md b/shatter-backend/docs/DATABASE_SCHEMA.md index e68af11..4835720 100644 --- a/shatter-backend/docs/DATABASE_SCHEMA.md +++ b/shatter-backend/docs/DATABASE_SCHEMA.md @@ -145,10 +145,12 @@ | Field | Type | Required | Default | Notes | |-----------|----------|----------|---------|-------| -| `_id` | ObjectId | Auto | Auto | | -| `userId` | ObjectId | No | `null` | Refs `User`. Nullable for legacy reasons | -| `name` | String | Yes | — | Display name in the event | -| `eventId` | ObjectId | Yes | — | Refs `Event` | +| `_id` | ObjectId | Auto | Auto | | +| `userId` | ObjectId | No | `null` | Refs `User`. Nullable for legacy reasons | +| `name` | String | Yes | — | Display name in the event | +| `eventId` | ObjectId | Yes | — | Refs `Event` | +| `linesCompleted` | Number | No | `0` | Completed bingo lines (vertical, horizontal, diagonal) | +| `completed` | Boolean | No | `false` | Whether the entire bingo sheet is filled | ### Indexes diff --git a/shatter-backend/docs/REALTIME_EVENTS_GUIDE.md b/shatter-backend/docs/REALTIME_EVENTS_GUIDE.md index 76a4d52..6c4aa5c 100644 --- a/shatter-backend/docs/REALTIME_EVENTS_GUIDE.md +++ b/shatter-backend/docs/REALTIME_EVENTS_GUIDE.md @@ -15,6 +15,7 @@ - [`event-ended`](#event-ended) - [`participant-left`](#participant-left) - [`event-deleted`](#event-deleted) + - [`leaderboard-updated`](#leaderboard-updated) - [Planned Events](#planned-events-) - [`bingo-achieved`](#bingo-achieved) - [Client Integration Examples](#client-integration-examples) @@ -210,6 +211,40 @@ Each event has its own channel. Subscribe when a user enters an event, unsubscri --- +### `leaderboard-updated` + +**Channel:** `event-{eventId}` + +Triggered when a participant updates their bingo score via `PUT /api/events/:eventId/leaderboard/score`. + +**Payload:** + +```json +{ + "participantId": "666b...", + "name": "Jane Doe", + "linesCompleted": 3, + "connectionsCount": 5, + "completed": false +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `participantId` | string | The participant who updated their score | +| `name` | string | Participant's display name | +| `linesCompleted` | number | Completed bingo lines (vertical, horizontal, diagonal) | +| `connectionsCount` | number | Unique connected partners count | +| `completed` | boolean | Whether the entire bingo sheet is filled | + +**Sources:** +- `src/controllers/leaderboard_controller.ts` → `updateScore()` (score changes) +- `src/controllers/participant_connections_controller.ts` → `createParticipantConnection()`, `createParticipantConnectionByEmails()`, `deleteParticipantConnection()` (connection changes — payload is `{}`) + +**Recommended client action:** Re-fetch the full leaderboard via `GET /api/events/:eventId/leaderboard` to get the latest sorted data. + +--- + ## Planned Events ⏳ These events are **not yet implemented**. Do not depend on them. diff --git a/shatter-backend/src/controllers/leaderboard_controller.ts b/shatter-backend/src/controllers/leaderboard_controller.ts new file mode 100644 index 0000000..3305169 --- /dev/null +++ b/shatter-backend/src/controllers/leaderboard_controller.ts @@ -0,0 +1,177 @@ +import { Request, Response } from "express"; +import { Types } from "mongoose"; +import { Participant } from "../models/participant_model.js"; +import { ParticipantConnection } from "../models/participant_connection_model.js"; +import { pusher } from "../utils/pusher_websocket.js"; + +/** + * GET /api/events/:eventId/leaderboard + * + * Returns the bingo leaderboard for an event, sorted by completion status + * and lines completed. Connections count is computed from ParticipantConnection + * records (unique connected partners per participant). + * + * @returns 200 - Sorted leaderboard array + * @returns 400 - Invalid eventId + * @returns 500 - Internal server error + */ +export async function getLeaderboard(req: Request, res: Response) { + try { + const eventId = req.params.eventId as string; + + if (!Types.ObjectId.isValid(eventId)) { + return res.status(400).json({ error: "Invalid eventId" }); + } + + const eventObjectId = new Types.ObjectId(eventId); + + // Fetch participants and connections in parallel + const [participants, connections] = await Promise.all([ + Participant.find({ eventId: eventObjectId }) + .select("name linesCompleted completed userId") + .populate("userId", "name profilePhoto") + .lean(), + ParticipantConnection.find({ _eventId: eventObjectId }) + .select("primaryParticipantId secondaryParticipantId") + .lean(), + ]); + + // Build connections count: unique connected partners per participant + const connectionsMap = new Map>(); + + for (const conn of connections) { + const primaryId = conn.primaryParticipantId.toString(); + const secondaryId = conn.secondaryParticipantId.toString(); + + if (!connectionsMap.has(primaryId)) { + connectionsMap.set(primaryId, new Set()); + } + if (!connectionsMap.has(secondaryId)) { + connectionsMap.set(secondaryId, new Set()); + } + + connectionsMap.get(primaryId)!.add(secondaryId); + connectionsMap.get(secondaryId)!.add(primaryId); + } + + // Build leaderboard entries + const leaderboard = participants.map((p) => { + const participantId = (p._id as Types.ObjectId).toString(); + const user = p.userId as { name?: string; profilePhoto?: string } | null; + + return { + participantId, + name: user?.name || p.name, + profilePhoto: user?.profilePhoto || null, + connectionsCount: connectionsMap.get(participantId)?.size || 0, + linesCompleted: p.linesCompleted || 0, + completed: p.completed || false, + }; + }); + + // Sort: completed first, then by linesCompleted desc, then connectionsCount desc + leaderboard.sort((a, b) => { + if (a.completed !== b.completed) return a.completed ? -1 : 1; + if (a.linesCompleted !== b.linesCompleted) + return b.linesCompleted - a.linesCompleted; + return b.connectionsCount - a.connectionsCount; + }); + + return res.status(200).json(leaderboard); + } catch (_error) { + return res.status(500).json({ error: "Internal server error" }); + } +} + +/** + * PUT /api/events/:eventId/leaderboard/score + * + * Updates the authenticated user's bingo score for an event. + * Triggers a Pusher event for live leaderboard updates. + * + * @param req.body.linesCompleted - Number of completed lines (optional) + * @param req.body.completed - Whether the entire bingo sheet is filled (optional) + * + * @returns 200 - Updated score fields + * @returns 400 - Invalid eventId or no valid fields provided + * @returns 404 - Participant not found for this event + * @returns 500 - Internal server error + */ +export async function updateScore(req: Request, res: Response) { + try { + const eventId = req.params.eventId as string; + const userId = req.user?.userId; + + if (!Types.ObjectId.isValid(eventId)) { + return res.status(400).json({ error: "Invalid eventId" }); + } + + const { linesCompleted, completed } = req.body as { + linesCompleted?: number; + completed?: boolean; + }; + + // Build update object with only provided fields + const update: Record = {}; + if (typeof linesCompleted === "number") update.linesCompleted = linesCompleted; + if (typeof completed === "boolean") update.completed = completed; + + if (Object.keys(update).length === 0) { + return res.status(400).json({ + error: "At least one of linesCompleted or completed must be provided", + }); + } + + const participant = await Participant.findOneAndUpdate( + { userId, eventId }, + { $set: update }, + { new: true } + ).select("name linesCompleted completed"); + + if (!participant) { + return res.status(404).json({ + error: "Participant not found for this event", + }); + } + + // Compute connections count for the Pusher payload + const participantId = (participant._id as Types.ObjectId).toString(); + const connections = await ParticipantConnection.find({ + _eventId: new Types.ObjectId(eventId), + $or: [ + { primaryParticipantId: participant._id }, + { secondaryParticipantId: participant._id }, + ], + }) + .select("primaryParticipantId secondaryParticipantId") + .lean(); + + const uniquePartners = new Set(); + for (const conn of connections) { + const otherId = + conn.primaryParticipantId.toString() === participantId + ? conn.secondaryParticipantId.toString() + : conn.primaryParticipantId.toString(); + uniquePartners.add(otherId); + } + + // Trigger live update + await pusher.trigger(`event-${eventId}`, "leaderboard-updated", { + participantId, + name: participant.name, + linesCompleted: participant.linesCompleted, + connectionsCount: uniquePartners.size, + completed: participant.completed, + }); + + return res.status(200).json({ + participantId, + name: participant.name, + linesCompleted: participant.linesCompleted, + completed: participant.completed, + connectionsCount: uniquePartners.size, + }); + } catch (_error) { + return res.status(500).json({ error: "Internal server error" }); + } +} diff --git a/shatter-backend/src/controllers/participant_connections_controller.ts b/shatter-backend/src/controllers/participant_connections_controller.ts index d2ee3e6..08653b1 100644 --- a/shatter-backend/src/controllers/participant_connections_controller.ts +++ b/shatter-backend/src/controllers/participant_connections_controller.ts @@ -7,6 +7,7 @@ import { check_req_fields } from "../utils/requests_utils.js"; import { User } from "../models/user_model.js"; import { Participant } from "../models/participant_model.js"; import { ParticipantConnection } from "../models/participant_connection_model.js"; +import { pusher } from "../utils/pusher_websocket.js"; /** * POST /api/participantConnections @@ -104,6 +105,8 @@ export async function createParticipantConnection(req: Request, res: Response) { description, }); + await pusher.trigger(`event-${_eventId}`, "leaderboard-updated", {}); + return res.status(201).json(newConnection); } catch (_error) { return res.status(500).json({ error: "Internal server error" }); @@ -229,6 +232,8 @@ export async function createParticipantConnectionByEmails( description, }); + await pusher.trigger(`event-${_eventId}`, "leaderboard-updated", {}); + return res.status(201).json(newConnection); } catch (_error) { return res.status(500).json({ error: "Internal server error" }); @@ -271,6 +276,8 @@ export async function deleteParticipantConnection(req: Request, res: Response) { .json({ error: "ParticipantConnection not found for this event" }); } + await pusher.trigger(`event-${eventId}`, "leaderboard-updated", {}); + return res.status(200).json({ message: "ParticipantConnection deleted successfully", deletedConnection: deleted, diff --git a/shatter-backend/src/models/participant_model.ts b/shatter-backend/src/models/participant_model.ts index f26a574..0e45b94 100644 --- a/shatter-backend/src/models/participant_model.ts +++ b/shatter-backend/src/models/participant_model.ts @@ -4,12 +4,16 @@ export interface IParticipant extends Document { userId: Schema.Types.ObjectId | null; name: string; eventId: Schema.Types.ObjectId; + linesCompleted: number; + completed: boolean; } const ParticipantSchema = new Schema({ userId: { type: Schema.Types.ObjectId, ref: "User", default: null }, name: { type: String, required: true }, eventId: { type: Schema.Types.ObjectId, ref: "Event", required: true }, + linesCompleted: { type: Number, default: 0 }, + completed: { type: Boolean, default: false }, }); ParticipantSchema.index( diff --git a/shatter-backend/src/routes/event_routes.ts b/shatter-backend/src/routes/event_routes.ts index 6d3e447..ee4558e 100644 --- a/shatter-backend/src/routes/event_routes.ts +++ b/shatter-backend/src/routes/event_routes.ts @@ -1,5 +1,6 @@ import { Router } from 'express'; import { createEvent, getEventByJoinCode, getEventById, joinEventAsUser, joinEventAsGuest, getEventsByUserId, updateEventStatus, leaveEvent, deleteEvent, updateEvent } from '../controllers/event_controller.js'; +import { getLeaderboard, updateScore } from '../controllers/leaderboard_controller.js'; import { authMiddleware } from '../middleware/auth_middleware.js'; const router = Router(); @@ -15,5 +16,7 @@ router.post("/:eventId/leave", authMiddleware, leaveEvent); router.delete("/:eventId", authMiddleware, deleteEvent); router.get("/createdEvents/user/:userId", authMiddleware, getEventsByUserId); router.put("/:eventId", authMiddleware, updateEvent); +router.get("/:eventId/leaderboard", authMiddleware, getLeaderboard); +router.put("/:eventId/leaderboard/score", authMiddleware, updateScore); export default router; From 57af1f7b7b96705209537d5512b78f4f93c62a98 Mon Sep 17 00:00:00 2001 From: Inko-z Date: Thu, 30 Apr 2026 13:18:41 -0600 Subject: [PATCH 2/6] Created New bingo Route to generate single questions, added route in the bingo routes Co-authored-by: Copilot --- .../src/controllers/bingo_controller.ts | 189 +++++++++++++++++- shatter-backend/src/routes/bingo_routes.ts | 5 +- 2 files changed, 187 insertions(+), 7 deletions(-) diff --git a/shatter-backend/src/controllers/bingo_controller.ts b/shatter-backend/src/controllers/bingo_controller.ts index 0a96455..75daab1 100644 --- a/shatter-backend/src/controllers/bingo_controller.ts +++ b/shatter-backend/src/controllers/bingo_controller.ts @@ -362,12 +362,6 @@ async function generateBingoGrid( `.trim(); const userContext = `Additional context for bingo content:\n${context}`; - // const promptPath = new URL( - // "../ai/prompts/bingo_short_questions.txt", - // import.meta.url, - // ); - // const aiInstruction = fs.readFileSync(promptPath, "utf-8"); - const aiPrompt = new Prompt([ basePrompt_structure, userContext, @@ -549,3 +543,186 @@ export async function generateBingo(req: Request, res: Response) { }); } } + +function buildSingleBingoQuestionSchema() { + return z.object({ + question: z + .string() + .min(1) + .describe("The full generated bingo question"), + shortQuestion: z + .string() + .min(1) + .describe("A max 3 word short version of the question"), + }); +} + +async function generateSingleBingoQuestionWithGemini({ + event_description, + tags, + bingo_grid, + bingo_question_target, +}: { + event_description: string; + tags: string[]; + bingo_grid: string[][]; + bingo_question_target: string; +}): Promise<{ question: string; shortQuestion: string }> { + const schema = buildSingleBingoQuestionSchema(); + const schemaJson = z.toJSONSchema(schema); + + const basePrompt = ` +Generate one new bingo question for an event bingo game. + +Return JSON exactly matching this structure: +{ + "question": "Full bingo question here", + "shortQuestion": "Max 3 words" +} + +Rules: +- Return ONLY valid JSON. +- Generate exactly one new question. +- The new question must fit the event context. +- The new question must be different from every existing question in the bingo grid. +- The target question is being regenerated, but the replacement does NOT need to be about the same topic. +- The replacement should be entirely new while still fitting the event. +- The shortQuestion must be max 3 words. +- The shortQuestion should describe what the full question is about. +- Avoid generic networking questions. +- Prefer observable, realistic, event-specific bingo moments. +`.trim(); + + const eventContext = ` +Event description: +${event_description} + +Tags / attendee roles: +${tags.length > 0 ? tags.join(", ") : "No tags provided"} + +Existing bingo grid questions: +${JSON.stringify(bingo_grid, null, 2)} + +Target question to replace: +${bingo_question_target} +`.trim(); + + const aiPrompt = new Prompt([ + basePrompt, + eventContext, + aiShortVersionInstruction, + ]); + + aiPrompt.generatePrompt(); + const prompt = aiPrompt.getPrompt(); + + const response = await ai.models.generateContent({ + model: "gemini-2.5-flash", + contents: prompt, + config: { + responseMimeType: "application/json", + responseJsonSchema: schemaJson, + temperature: 0.8, + }, + }); + + if (!response.text) { + throw new Error("Gemini returned empty response"); + } + + const parsed = JSON.parse(response.text); + const validated = schema.parse(parsed); + + return validated; +} + + +/** + * POST /api/bingo/generate/single + * + * Generate one AI bingo question. + * + * @param req.body.event_description - Context for bingo content (required) - string + * @param req.body.tags - Tags for the type/roles of people attending the event - string[] can be empty + * @param req.body.bingo_grid - Existing bingo grid of full questions - string[][] + * @param req.body.bingo_question_target - Target question to regenerate - string + * + * @returns 200 with generated bingo question and short version + * @returns 400 if validation fails + */ +export async function generateSingleBingoQuestion(req: Request, res: Response) { + try { + const { + event_description, + tags, + bingo_grid, + bingo_question_target, + } = req.body; + + if ( + !event_description || + typeof event_description !== "string" || + event_description.trim().length === 0 + ) { + return res.status(400).json({ + status: false, + msg: "event_description is required and must be a non-empty string", + }); + } + + if ( + tags === undefined || + !Array.isArray(tags) || + !tags.every((tag: any) => typeof tag === "string") + ) { + return res.status(400).json({ + status: false, + msg: "tags is required and must be an array of strings", + }); + } + + if ( + !Array.isArray(bingo_grid) || + bingo_grid.length === 0 || + !bingo_grid.every( + (row: any) => + Array.isArray(row) && + row.every((cell: any) => typeof cell === "string"), + ) + ) { + return res.status(400).json({ + status: false, + msg: "bingo_grid is required and must be a 2D array of strings", + }); + } + + if ( + !bingo_question_target || + typeof bingo_question_target !== "string" || + bingo_question_target.trim().length === 0 + ) { + return res.status(400).json({ + status: false, + msg: "bingo_question_target is required and must be a non-empty string", + }); + } + + const generatedQuestion = await generateSingleBingoQuestionWithGemini({ + event_description: event_description.trim(), + tags, + bingo_grid, + bingo_question_target: bingo_question_target.trim(), + }); + + return res.status(200).json({ + status: true, + question: generatedQuestion.question, + shortQuestion: generatedQuestion.shortQuestion, + }); + } catch (err: any) { + return res.status(500).json({ + status: false, + error: err.message, + }); + } +} diff --git a/shatter-backend/src/routes/bingo_routes.ts b/shatter-backend/src/routes/bingo_routes.ts index a811f78..b5e9d0c 100644 --- a/shatter-backend/src/routes/bingo_routes.ts +++ b/shatter-backend/src/routes/bingo_routes.ts @@ -1,5 +1,5 @@ import { Router } from 'express'; -import { createBingo, getBingo, updateBingo, generateBingo} from '../controllers/bingo_controller.js'; +import { createBingo, getBingo, updateBingo, generateBingo, generateSingleBingoQuestion} from '../controllers/bingo_controller.js'; import { authMiddleware } from '../middleware/auth_middleware.js'; const router = Router(); @@ -15,5 +15,8 @@ router.put("/updateBingo", authMiddleware, updateBingo); // POST /api/bingo/generateBingo - generate bingo using AI for an event router.post("/generateBingo", authMiddleware, generateBingo); +// POST /api/bingo/generate/single - generate a single AI bingo question +router.post("/generateBingo/singlequestion", authMiddleware, generateSingleBingoQuestion); + export default router; From f0ce3db00b3db0a67cbe527e7d7df3aeb24e69ac Mon Sep 17 00:00:00 2001 From: Inko-z Date: Thu, 30 Apr 2026 13:52:31 -0600 Subject: [PATCH 3/6] added documentation for the API Routes --- shatter-backend/docs/API_REFERENCE.md | 90 ++++++++++++++++++++++++++- 1 file changed, 88 insertions(+), 2 deletions(-) diff --git a/shatter-backend/docs/API_REFERENCE.md b/shatter-backend/docs/API_REFERENCE.md index 97f64ac..17e0243 100644 --- a/shatter-backend/docs/API_REFERENCE.md +++ b/shatter-backend/docs/API_REFERENCE.md @@ -45,7 +45,8 @@ - [POST `/api/bingo/createBingo`](#post-apibingocreatebingo) - [GET `/api/bingo/getBingo/:eventId`](#get-apibingogetbingoeventid) - [PUT `/api/bingo/updateBingo`](#put-apibingoupdatebingo) - - [POST `/api/bingo/generate`](#post-apibingogenerate) + - [POST `/api/bingo/generateBingo`](#post-apibingogeneratebingo) + - [POST `/api/bingo/generateBingo/single`](#post-apibingogeneratebingosingle) - [Participant Connections (`/api/participantConnections`)](#participant-connections-apiparticipantconnections) - [POST `/api/participantConnections/`](#post-apiparticipantconnections) - [POST `/api/participantConnections/by-emails`](#post-apiparticipantconnectionsby-emails) @@ -1285,7 +1286,7 @@ Update a bingo game. --- -### POST `/api/bingo/generate` +### POST `/api/bingo/generateBingo` Generate an AI-powered bingo grid based on a given context. @@ -1338,6 +1339,91 @@ Generate an AI-powered bingo grid based on a given context. } ``` +### POST `/api/bingo/generateBingo/single` + +Generate one new AI-powered bingo question to replace a target question in an existing bingo grid. + +The New generated question should be different from the existing questions in the bingo grid while still matching the provided event context. + +- **Auth:** Protected + +**Request Body:** + +| Field | Type | Required | Notes | +|---|---|---|---| +| `event_description` | string | Yes | Event context used to generate the new bingo question. Cannot be empty | +| `tags` | string[] | Yes | Types or roles of people attending the event. Can be an empty array | +| `bingo_grid` | string[][] | Yes | Existing bingo grid containing the full question strings | +| `bingo_question_target` | string | Yes | The question intended to be regenerated/replaced | + +**Example Request:** + +```json +{ + "event_description": "Software engineer networking event where developers meet, discuss tech stacks, exchange ideas, talk about startups, open source, AI, and career opportunities", + "tags": ["software engineers", "startup founders", "product managers", "designers"], + "bingo_grid": [ + [ + "Sketches architecture on a napkin", + "Shows a product demo on phone" + ], + [ + "Explains their open-source contribution", + "Asks 'What's your current stack?'" + ] + ], + "bingo_question_target": "Explains their open-source contribution" +} +``` + +**Example Response:** + +```json +{ + "status": true, + "question": "Shows a side project they built over the weekend", + "shortQuestion": "Weekend side project" +} +``` + +**Validation Errors:** + +Missing or invalid `event_description`: + +```json +{ + "status": false, + "msg": "event_description is required and must be a non-empty string" +} +``` + +Missing or invalid `tags`: + +```json +{ + "status": false, + "msg": "tags is required and must be an array of strings" +} +``` + +Missing or invalid `bingo_grid`: + +```json +{ + "status": false, + "msg": "bingo_grid is required and must be a 2D array of strings" +} +``` + +Missing or invalid `bingo_question_target`: + +```json +{ + "status": false, + "msg": "bingo_question_target is required and must be a non-empty string" +} +``` + ## Participant Connections (`/api/participantConnections`) ### POST `/api/participantConnections/` From d0a3597219a6c9f7ce6da11dd5925ea4ec454835 Mon Sep 17 00:00:00 2001 From: Inko-z Date: Thu, 30 Apr 2026 13:54:28 -0600 Subject: [PATCH 4/6] Updated documentation --- shatter-backend/docs/API_REFERENCE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shatter-backend/docs/API_REFERENCE.md b/shatter-backend/docs/API_REFERENCE.md index 17e0243..392744c 100644 --- a/shatter-backend/docs/API_REFERENCE.md +++ b/shatter-backend/docs/API_REFERENCE.md @@ -1290,7 +1290,7 @@ Update a bingo game. Generate an AI-powered bingo grid based on a given context. -- **Auth:** Protected +- **Auth:** Not Protected **Request Body:** @@ -1345,7 +1345,7 @@ Generate one new AI-powered bingo question to replace a target question in an ex The New generated question should be different from the existing questions in the bingo grid while still matching the provided event context. -- **Auth:** Protected +- **Auth:** Not Protected **Request Body:** From 6f3f93a437155741437ba7a608bffdcec9e24619 Mon Sep 17 00:00:00 2001 From: Inko-z Date: Thu, 30 Apr 2026 14:25:31 -0600 Subject: [PATCH 5/6] Updated generateBingo route to split context param into 2 separate ones, event_description and tags. --- .../src/controllers/bingo_controller.ts | 65 +++++++++++++++---- 1 file changed, 52 insertions(+), 13 deletions(-) diff --git a/shatter-backend/src/controllers/bingo_controller.ts b/shatter-backend/src/controllers/bingo_controller.ts index 75daab1..3be6f07 100644 --- a/shatter-backend/src/controllers/bingo_controller.ts +++ b/shatter-backend/src/controllers/bingo_controller.ts @@ -342,31 +342,43 @@ function buildShapeExample(rows: number, cols: number): string { async function generateBingoGrid( n_rows: number, n_cols: number, - context: string, + event_description: string, + tags: string[], ): Promise> { const schema = buildSchema(n_rows, n_cols); const schemaJson = z.toJSONSchema(schema); const example = buildShapeExample(n_rows, n_cols); - const { GoogleGenAI } = await import("@google/genai"); - const basePrompt_structure = `Generate a ${n_rows}x${n_cols} bingo board. Return JSON exactly matching this structure: ${example} + Rules: - Keys must be row1, row2, row3, etc. - Each row must contain ${n_cols} strings. - Return ONLY valid JSON. - - You will be provided with additional context to inspire the content of the bingo squares. Use that context to generate relevant bingo square phrases. + - Return ONLY valid JSON. + - Each entry should be a full bingo question or bingo square phrase. + - Every entry must fit the provided event description. + - Use the tags as guidance for the types of professionals, roles, departments, or specializations attending the event. + - Tags can be empty. If no tags are provided, rely only on the event description. + - Avoid duplicate or near-duplicate entries. + - Avoid generic networking items unless they are made specific to the event context. `.trim(); - const userContext = `Additional context for bingo content:\n${context}`; + + const eventContext = ` +Event description: +${event_description} + +Tags / attendee professional types: +${tags.length > 0 ? tags.join(", ") : "No tags provided"} +`.trim(); const aiPrompt = new Prompt([ basePrompt_structure, - userContext, + eventContext, aiShortVersionInstruction, ]); + aiPrompt.generatePrompt(); const prompt = aiPrompt.getPrompt(); @@ -486,7 +498,8 @@ function combine2DArrays( * * Generate an AI bingo grid. * - * @param req.body.context - Context for bingo content (required) + * @param req.body.event_description - General event context for bingo content (required) + * @param req.body.tags - Types of professionals, roles, departments, or specializations attending the event (required, can be empty) * @param req.body.n_rows - Number of grid rows (1-5) * @param req.body.n_cols - Number of grid columns (1-5) * @@ -495,12 +508,27 @@ function combine2DArrays( */ export async function generateBingo(req: Request, res: Response) { try { - const { context, n_rows, n_cols } = req.body; + const { event_description, tags, n_rows, n_cols } = req.body; + + if ( + !event_description || + typeof event_description !== "string" || + event_description.trim().length === 0 + ) { + return res.status(400).json({ + status: false, + msg: "event_description is required and must be a non-empty string", + }); + } - if (!context) { + if ( + tags === undefined || + !Array.isArray(tags) || + !tags.every((tag: any) => typeof tag === "string") + ) { return res.status(400).json({ status: false, - msg: "context is required", + msg: "tags is required and must be an array of strings", }); } @@ -518,15 +546,26 @@ export async function generateBingo(req: Request, res: Response) { }); } - const bingo_questions = await generateBingoGrid(n_rows, n_cols, context); + const cleanedTags = tags.map((tag: string) => tag.trim()).filter(Boolean); + + const bingo_questions = await generateBingoGrid( + n_rows, + n_cols, + event_description.trim(), + cleanedTags, + ); + const bingo_short_versions = await generateBingoGrid_shortVersions( n_rows, n_cols, JSON.stringify(bingo_questions), ); + const bingo_grid_questions: string[][] = process_ai_result(bingo_questions); + const bingo_grid_short_versions: string[][] = process_ai_result(bingo_short_versions); + const bingo_grid = combine2DArrays( bingo_grid_questions, bingo_grid_short_versions, From bf93aed97ff00efc6e5fa4abcfd52e12f3819e43 Mon Sep 17 00:00:00 2001 From: Inko-z Date: Thu, 30 Apr 2026 14:29:00 -0600 Subject: [PATCH 6/6] Updated Documentation for generating full bingo game api route --- shatter-backend/docs/API_REFERENCE.md | 48 ++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/shatter-backend/docs/API_REFERENCE.md b/shatter-backend/docs/API_REFERENCE.md index 392744c..926f107 100644 --- a/shatter-backend/docs/API_REFERENCE.md +++ b/shatter-backend/docs/API_REFERENCE.md @@ -1288,30 +1288,33 @@ Update a bingo game. ### POST `/api/bingo/generateBingo` -Generate an AI-powered bingo grid based on a given context. +Generate an AI-powered bingo grid based on a given event description and attendee tags. - **Auth:** Not Protected **Request Body:** -| Field | Type | Required | Notes | -|-----------|--------|----------|-------| -| `context` | string | Yes | Context used to generate bingo content | -| `n_rows` | number | Yes | Number of rows (1–5) | -| `n_cols` | number | Yes | Number of columns (1–5) | +| Field | Type | Required | Notes | +|---|---|---|---| +| `event_description` | string | Yes | General information about what the event is about. Cannot be empty | +| `tags` | string[] | Yes | List of professional types, roles, job titles, specializations, or departments attending the event. Can be an empty array | +| `n_rows` | number | Yes | Number of rows (1–5) | +| `n_cols` | number | Yes | Number of columns (1–5) | **Example Request:** ```json { - "context": "Software engineer networking event where developers meet, discuss tech stacks, exchange ideas, talk about startups, open source, AI, and career opportunities", + "event_description": "Software engineer networking event where developers meet, discuss tech stacks, exchange ideas, talk about startups, open source, AI, and career opportunities", + "tags": ["software engineers", "frontend developers", "backend developers", "startup founders", "product managers"], "n_rows": 2, "n_cols": 2 } ``` **Example Response:** -``` + +```json { "status": true, "bingo_grid": [ @@ -1339,6 +1342,35 @@ Generate an AI-powered bingo grid based on a given context. } ``` +**Validation Errors:** + +Missing or invalid `event_description`: + +```json +{ + "status": false, + "msg": "event_description is required and must be a non-empty string" +} +``` + +Missing or invalid `tags`: + +```json +{ + "status": false, + "msg": "tags is required and must be an array of strings" +} +``` + +Missing or invalid `n_rows` or `n_cols`: + +```json +{ + "status": false, + "msg": "n_rows and n_cols must be numbers where 0 < value <= 5" +} +``` + ### POST `/api/bingo/generateBingo/single` Generate one new AI-powered bingo question to replace a target question in an existing bingo grid.