diff --git a/shatter-backend/docs/API_REFERENCE.md b/shatter-backend/docs/API_REFERENCE.md index 41d4dcd..a51ac2f 100644 --- a/shatter-backend/docs/API_REFERENCE.md +++ b/shatter-backend/docs/API_REFERENCE.md @@ -7,42 +7,55 @@ ## Table of Contents -- [General Information](#general-information) -- [Endpoint Summary](#endpoint-summary) -- [Authentication (`/api/auth`)](#authentication-apiauth) - - [POST /api/auth/signup](#post-apiauthsignup) - - [POST /api/auth/login](#post-apiauthlogin) - - [GET /api/auth/linkedin](#get-apiauthlinkedin) - - [GET /api/auth/linkedin/callback](#get-apiauthlinkedincallback) - - [POST /api/auth/exchange](#post-apiauthexchange) -- [Users (`/api/users`)](#users-apiusers) - - [GET /api/users](#get-apiusers) - - [POST /api/users](#post-apiusers) - - [GET /api/users/me](#get-apiusersme) - - [GET /api/users/:userId](#get-apiusersuserid) - - [GET /api/users/:userId/events](#get-apiusersuseриdevents) - - [PUT /api/users/:userId](#put-apiusersuserid) -- [Events (`/api/events`)](#events-apievents) - - [POST /api/events/createEvent](#post-apieventscreateevent) - - [GET /api/events/event/:joinCode](#get-apieventseventjoincode) - - [GET /api/events/:eventId](#get-apieventseventid) - - [PUT /api/events/:eventId/status](#put-apieventseventidstatus) - - [POST /api/events/:eventId/join/user](#post-apieventseventиdjoinuser) - - [POST /api/events/:eventId/join/guest](#post-apieventseventиdjoinguest) - - [GET /api/events/createdEvents/user/:userId](#get-apieventscreatedeventsuseriduserid) -- [Bingo (`/api/bingo`)](#bingo-apibingo) - - [POST /api/bingo/createBingo](#post-apibingocreatebingo) - - [GET /api/bingo/getBingo/:eventId](#get-apibingogetbingoeventid) - - [PUT /api/bingo/updateBingo](#put-apibingoupdatebingo) -- [Participant Connections (`/api/participantConnections`)](#participant-connections-apiparticipantconnections) - - [POST /api/participantConnections/](#post-apiparticipantconnections) - - [POST /api/participantConnections/by-emails](#post-apiparticipantconnectionsby-emails) - - [DELETE /api/participantConnections/delete](#delete-apiparticipantconnectionsdelete) - - [GET /api/participantConnections/getByParticipantAndEvent](#get-apiparticipantconnectionsgetbyparticipantandevent) - - [GET /api/participantConnections/getByUserEmailAndEvent](#get-apiparticipantconnectionsgetbyuseremailandevent) - - [GET /api/participantConnections/connected-users](#get-apiparticipantconnectionsconnected-users) -- [Planned Endpoints](#planned-endpoints-) -- [Quick Start Examples](#quick-start-examples) +- [Shatter Backend — API Reference](#shatter-backend--api-reference) + - [Table of Contents](#table-of-contents) + - [Endpoint Summary](#endpoint-summary) + - [General Information](#general-information) + - [Authentication](#authentication) + - [Response Format](#response-format) + - [Common Status Codes](#common-status-codes) + - [Authentication (`/api/auth`)](#authentication-apiauth) + - [POST `/api/auth/signup`](#post-apiauthsignup) + - [POST `/api/auth/login`](#post-apiauthlogin) + - [GET `/api/auth/linkedin`](#get-apiauthlinkedin) + - [GET `/api/auth/linkedin/callback`](#get-apiauthlinkedincallback) + - [POST `/api/auth/exchange`](#post-apiauthexchange) + - [Users (`/api/users`)](#users-apiusers) + - [GET `/api/users`](#get-apiusers) + - [POST `/api/users`](#post-apiusers) + - [GET `/api/users/me`](#get-apiusersme) + - [GET `/api/users/:userId`](#get-apiusersuserid) + - [GET `/api/users/:userId/events`](#get-apiusersuseridevents) + - [PUT `/api/users/:userId`](#put-apiusersuserid) + - [Events (`/api/events`)](#events-apievents) + - [POST `/api/events/createEvent`](#post-apieventscreateevent) + - [GET `/api/events/event/:joinCode`](#get-apieventseventjoincode) + - [GET `/api/events/:eventId`](#get-apieventseventid) + - [PUT `/api/events/:eventId/status`](#put-apieventseventidstatus) + - [POST `/api/events/:eventId/join/user`](#post-apieventseventidjoinuser) + - [POST `/api/events/:eventId/join/guest`](#post-apieventseventidjoinguest) + - [GET `/api/events/createdEvents/user/:userId`](#get-apieventscreatedeventsuseruserid) + - [Bingo (`/api/bingo`)](#bingo-apibingo) + - [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) + - [Participant Connections (`/api/participantConnections`)](#participant-connections-apiparticipantconnections) + - [POST `/api/participantConnections/`](#post-apiparticipantconnections) + - [POST `/api/participantConnections/by-emails`](#post-apiparticipantconnectionsby-emails) + - [DELETE `/api/participantConnections/delete`](#delete-apiparticipantconnectionsdelete) + - [GET `/api/participantConnections/getByParticipantAndEvent`](#get-apiparticipantconnectionsgetbyparticipantandevent) + - [GET `/api/participantConnections/getByUserEmailAndEvent`](#get-apiparticipantconnectionsgetbyuseremailandevent) + - [GET `/api/participantConnections/connected-users`](#get-apiparticipantconnectionsconnected-users) + - [Planned Endpoints ⏳](#planned-endpoints-) + - [Quick Start Examples](#quick-start-examples) + - [1. Sign up](#1-sign-up) + - [2. Log in](#2-log-in) + - [3. Create an event](#3-create-an-event) + - [4. Join the event (as authenticated user)](#4-join-the-event-as-authenticated-user) + - [5. Join the event (as guest)](#5-join-the-event-as-guest) + - [6. Create a bingo game for the event](#6-create-a-bingo-game-for-the-event) + - [7. Get the bingo game](#7-get-the-bingo-game) --- @@ -848,9 +861,9 @@ Create a bingo game for an event. | Field | Type | Required | Notes | |---------------|------------|----------|-------| -| `_eventId` | ObjectId | Yes | Must reference an existing event | -| `description` | string | No | | -| `grid` | string[][] | No | 2D array of strings | +| `_eventId` | ObjectId | Yes | Must reference an existing event | +| `description` | string | No | | +| `grid` | BingoTile\[\]\[\] | No | 2D array of `{ question: string, shortQuestion: string }` | **Success Response (201):** @@ -863,9 +876,16 @@ Create a bingo game for an event. "_eventId": "665a...", "description": "Networking Bingo", "grid": [ - ["Has a pet", "Speaks 3 languages", "Loves hiking"], - ["Works remotely", "Free space", "Plays guitar"], - ["From another country", "Has a blog", "Codes in Rust"] + [ + { "question": "Has a pet", "shortQuestion": "Has pet" }, + { "question": "Speaks 3 languages", "shortQuestion": "Speaks languages" }, + { "question": "Loves hiking outdoors", "shortQuestion": "Loves hiking" } + ], + [ + { "question": "Works remotely full-time", "shortQuestion": "Works remotely" }, + { "question": "Free space", "shortQuestion": "Free space" }, + { "question": "Plays guitar regularly", "shortQuestion": "Plays guitar" } + ] ] } } @@ -877,7 +897,7 @@ Create a bingo game for an event. |--------|-------| | 400 | `"_eventId is required"` | | 400 | `"_eventId must be a valid ObjectId"` | -| 400 | `"grid must be a 2D array of strings"` | +| 400 | `"grid must be a 2D array of { question: string, shortQuestion: string }"` | | 404 | `"Event not found"` | --- @@ -903,7 +923,12 @@ Get bingo by event ID (or bingo ID). "_id": "bingo_a1b2c3d4", "_eventId": "665a...", "description": "Networking Bingo", - "grid": [["Has a pet", "Speaks 3 languages", ...], ...] + "grid": [ + [ + { "question": "Has a pet", "shortQuestion": "Has pet" }, + { "question": "Speaks 3 languages", "shortQuestion": "Speaks languages" } + ] + ] } } ``` @@ -926,9 +951,9 @@ Update a bingo game. | Field | Type | Required | Notes | |---------------|------------|----------|-------| -| `id` | string | Yes | Bingo `_id` or event `_eventId` | -| `description` | string | No | | -| `grid` | string[][] | No | 2D array of strings | +| `id` | string | Yes | Bingo `_id` or event `_eventId` | +| `description` | string | No | | +| `grid` | BingoTile\[\]\[\] | No | 2D array of `{ question: string, shortQuestion: string }` | **Success Response (200):** @@ -945,7 +970,7 @@ Update a bingo game. |--------|-------| | 400 | `"id is required"` | | 400 | `"description must be a string"` | -| 400 | `"grid must be a 2D array of strings"` | +| 400 | `"grid must be a 2D array of { question: string, shortQuestion: string }"` | | 400 | `"Nothing to update: provide description and/or grid"` | | 404 | `"Bingo not found"` | @@ -953,6 +978,59 @@ Update a bingo game. --- +### POST `/api/bingo/generate` + +Generate an AI-powered bingo grid based on a given context. + +- **Auth:** 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) | + +**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", + "n_rows": 2, + "n_cols": 2 +} +``` + +**Example Response:** +``` +{ + "status": true, + "bingo_grid": [ + [ + { + "question": "Sketches architecture on a napkin", + "shortQuestion": "Napkin architecture" + }, + { + "question": "Shows a product demo on phone", + "shortQuestion": "Phone product demo" + } + ], + [ + { + "question": "Explains their open-source contribution", + "shortQuestion": "Open-source contribution" + }, + { + "question": "Asks 'What's your current stack?'", + "shortQuestion": "Current stack question" + } + ] + ] +} +``` + ## Participant Connections (`/api/participantConnections`) ### POST `/api/participantConnections/` diff --git a/shatter-backend/docs/DATABASE_SCHEMA.md b/shatter-backend/docs/DATABASE_SCHEMA.md index 21d9dc6..e68af11 100644 --- a/shatter-backend/docs/DATABASE_SCHEMA.md +++ b/shatter-backend/docs/DATABASE_SCHEMA.md @@ -177,7 +177,7 @@ | `_id` | String | Auto | Auto | Custom: `bingo_<8 random chars>` | | `_eventId` | ObjectId | Yes | — | Refs `Event` | | `description` | String | No | — | | -| `grid` | [[String]] | No | — | 2D array of strings | +| `grid` | [[{ question: String, shortQuestion: String }]] | No | — | 2D array of BingoTile objects | ### Pre-Save Hooks diff --git a/shatter-backend/src/ai/prompts/bingo_short_questions.txt b/shatter-backend/src/ai/prompts/bingo_short_questions.txt new file mode 100644 index 0000000..8b12320 --- /dev/null +++ b/shatter-backend/src/ai/prompts/bingo_short_questions.txt @@ -0,0 +1,2 @@ +You will generate the bingo grid with shortened version of the questions while maintaining the same structure. +Each entry should be turned into a max 3 word version of the question that describes what the question is about. diff --git a/shatter-backend/src/controllers/bingo_controller.ts b/shatter-backend/src/controllers/bingo_controller.ts index 70c16f2..9cb6cab 100644 --- a/shatter-backend/src/controllers/bingo_controller.ts +++ b/shatter-backend/src/controllers/bingo_controller.ts @@ -13,7 +13,6 @@ import "dotenv/config"; import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import { Console } from "node:console"; /** * POST /api/bingo @@ -46,18 +45,24 @@ export async function createBingo(req: Request, res: Response) { } if (grid !== undefined) { - const is2DStringArray = + const isValidGrid = Array.isArray(grid) && grid.every( (row: any) => Array.isArray(row) && - row.every((cell: any) => typeof cell === "string") + row.every( + (cell: any) => + cell && + typeof cell === "object" && + typeof cell.question === "string" && + typeof cell.shortQuestion === "string" + ) ); - if (!is2DStringArray) { + if (!isValidGrid) { return res.status(400).json({ success: false, - msg: "grid must be a 2D array of strings", + msg: "grid must be a 2D array of { question: string, shortQuestion: string }", }); } } @@ -98,11 +103,7 @@ export async function getBingo(req: Request, res: Response) { }); } - let bingo = await Bingo.findById(eventId); - - if (!bingo && Types.ObjectId.isValid(eventId)) { - bingo = await Bingo.findOne({ _eventId: eventId }); - } + let bingo = await Bingo.findOne({ _eventId: eventId }); if (!bingo) { return res.status(404).json({ @@ -149,18 +150,24 @@ export async function updateBingo(req: Request, res: Response) { } if (grid !== undefined) { - const is2DStringArray = + const isValidGrid = Array.isArray(grid) && grid.every( (row: any) => Array.isArray(row) && - row.every((cell: any) => typeof cell === "string") + row.every( + (cell: any) => + cell && + typeof cell === "object" && + typeof cell.question === "string" && + typeof cell.shortQuestion === "string" + ) ); - if (!is2DStringArray) { + if (!isValidGrid) { return res.status(400).json({ success: false, - msg: "grid must be a 2D array of strings", + msg: "grid must be a 2D array of { question: string, shortQuestion: string }", }); } @@ -198,8 +205,6 @@ export async function updateBingo(req: Request, res: Response) { function makeEmptyGrid(rows: number, cols: number): Record { - console.log(`Creating fallback grid ${rows}x${cols}`); - const grid: Record = {}; for (let r = 1; r <= rows; r++) { @@ -226,7 +231,6 @@ function buildSchema(rows: number, cols: number): z.ZodObject = {}; @@ -244,7 +248,6 @@ function buildShapeExample(rows: number, cols: number): string { * Calls Gemini to generate a bingo grid */ async function generateBingoGrid(n_rows: number, n_cols: number, context: string): Promise> { - const schema = buildSchema(n_rows, n_cols); const schemaJson = z.toJSONSchema(schema); @@ -267,9 +270,6 @@ async function generateBingoGrid(n_rows: number, n_cols: number, context: string const aiPrompt = new Prompt([basePrompt_structure, userContext, aiInstruction]) aiPrompt.generatePrompt(); const prompt = aiPrompt.getPrompt(); - console.log("----------- Final Prompt -----------"); - console.log(prompt); - console.log("------------------------------------"); try { @@ -283,16 +283,68 @@ async function generateBingoGrid(n_rows: number, n_cols: number, context: string }, }); - console.log("RAW RESPONSE:"); - console.log(response.text); + if (!response.text) { + throw new Error("Gemini returned empty response"); + } - const parsed = JSON.parse(response.text!); + const parsed = JSON.parse(response.text); const validated: Record = schema.parse(parsed); return validated; } catch (error) { - console.error("Generation failed:", error); - return makeEmptyGrid(n_rows, n_cols) + console.error("Generation failed:", error); + return makeEmptyGrid(n_rows, n_cols) + } +} + +async function generateBingoGrid_shortVersions(n_rows: number, n_cols: number, original_bingo_questions: string): Promise> { + + const schema = buildSchema(n_rows, n_cols); + const schemaJson = z.toJSONSchema(schema); + const example = buildShapeExample(n_rows, n_cols); + + 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 information about what to put inside the bingo squares. + `.trim(); + const original_bingo_questions_context = `These are the original bingo questions:\n${original_bingo_questions}`; + + const promptPath = path.resolve(__dirname, "../ai/prompts/bingo_short_questions.txt"); + const aiInstruction = fs.readFileSync(promptPath, "utf-8"); + + const aiPrompt = new Prompt([basePrompt_structure, original_bingo_questions_context, aiInstruction]) + aiPrompt.generatePrompt(); + const prompt = aiPrompt.getPrompt(); + + + try { + + const response = await ai.models.generateContent({ + model: "gemini-2.5-flash", + contents: prompt, + config: { + responseMimeType: "application/json", + responseJsonSchema: schemaJson, + temperature: 0.7, + }, + }); + + if (!response.text) { + throw new Error("Gemini returned empty response"); + } + + const parsed = JSON.parse(response.text); + const validated: Record = schema.parse(parsed); + return validated; + + } catch (error) { + console.error("Short version generation failed:", error); + return makeEmptyGrid(n_rows, n_cols) } } @@ -315,6 +367,15 @@ function process_ai_result(ai_result: Record) { return grid; } +function combine2DArrays(arr1: string[][], arr2: string[][]): { question: string; shortQuestion: string }[][] { + return arr1.map((row, i) => + row.map((val, j) => ({ + question: val, + shortQuestion: arr2[i]?.[j] || val, + })) + ); +} + /** * POST /api/bingo/generate * @@ -352,8 +413,11 @@ export async function generateBingo(req: Request, res: Response) { }); } - const aiResult = await generateBingoGrid(n_rows, n_cols, context); - const bingo_grid: string[][] = process_ai_result(aiResult); + const bingo_questions = await generateBingoGrid(n_rows, n_cols, context); + 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); return res.status(200).json({ status: true, diff --git a/shatter-backend/src/controllers/event_controller.ts b/shatter-backend/src/controllers/event_controller.ts index 1d9cf9c..efcb0bd 100644 --- a/shatter-backend/src/controllers/event_controller.ts +++ b/shatter-backend/src/controllers/event_controller.ts @@ -170,7 +170,8 @@ export async function getEventByJoinCode(req: Request, res: Response) { export async function joinEventAsUser(req: Request, res: Response) { try { const { name, userId } = req.body; - const { eventId } = req.params; + const rawEventId = req.params.eventId; + const eventId = Array.isArray(rawEventId) ? rawEventId[0] : rawEventId; if (!userId || !name || !eventId) return res.status(400).json({ @@ -279,7 +280,8 @@ export async function joinEventAsGuest(req: Request, res: Response) { organization?: string; title?: string; }; - const { eventId } = req.params; + const rawEventId = req.params.eventId; + const eventId = Array.isArray(rawEventId) ? rawEventId[0] : rawEventId; if (!name || !eventId) { return res.status(400).json({ diff --git a/shatter-backend/src/models/bingo_model.ts b/shatter-backend/src/models/bingo_model.ts index 9f5dd3b..9fda83e 100644 --- a/shatter-backend/src/models/bingo_model.ts +++ b/shatter-backend/src/models/bingo_model.ts @@ -1,10 +1,15 @@ import { Schema, model, Types, HydratedDocument } from "mongoose"; +export interface BingoTile { + question: string; + shortQuestion: string; +} + export interface IBingo { _id: string; _eventId: Types.ObjectId; description?: string; - grid?: string[][]; + grid?: BingoTile[][]; } export type BingoDocument = HydratedDocument; @@ -18,7 +23,12 @@ const bingoSchema = new Schema( required: true, }, description: { type: String }, - grid: { type: [[String]] }, + grid: { + type: [[{ + question: { type: String, required: true }, + shortQuestion: { type: String, required: true }, + }]], + }, }, { versionKey: false,