From b0bb2e82042c72edb1cf591806b919f125257f76 Mon Sep 17 00:00:00 2001 From: Tyler Hill Date: Tue, 11 Nov 2025 20:03:40 -0600 Subject: [PATCH 01/31] Route with comments --- src/app/api/rmpSummary/route.ts | 69 +++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 src/app/api/rmpSummary/route.ts diff --git a/src/app/api/rmpSummary/route.ts b/src/app/api/rmpSummary/route.ts new file mode 100644 index 00000000..1b927b3c --- /dev/null +++ b/src/app/api/rmpSummary/route.ts @@ -0,0 +1,69 @@ +import { NextResponse } from 'next/server'; + +export async function GET(request: Request) { + const API_URL = process.env.NEBULA_API_KEY; + if (typeof API_URL !== 'string') { + return NextResponse.json( + { message: 'error', data: 'API URL is undefined' }, + { status: 500 }, + ); + } + const API_KEY = process.env.NEBULA_API_KEY; + if (typeof API_KEY !== 'string') { + return NextResponse.json( + { message: 'error', data: 'API key is undefined' }, + { status: 500 }, + ); + } + const API_STORAGE_BUCKET = process.env.NEBULA_API_KEY; + if (typeof API_STORAGE_BUCKET !== 'string') { + return NextResponse.json( + { message: 'error', data: 'API storage bucket is undefined' }, + { status: 500 }, + ); + } + const API_STORAGE_KEY = process.env.NEBULA_API_KEY; + if (typeof API_STORAGE_KEY !== 'string') { + return NextResponse.json( + { message: 'error', data: 'API storage key is undefined' }, + { status: 500 }, + ); + } + + const { searchParams } = new URL(request.url); + const prefix = searchParams.get('prefix'); + const number = searchParams.get('number'); + const profFirst = searchParams.get('profFirst'); + const profLast = searchParams.get('profLast'); + + if ( + typeof prefix !== 'string' || + typeof number !== 'string' || + typeof profFirst !== 'string' || + typeof profLast !== 'string' + ) { + return NextResponse.json( + { message: 'error', data: 'Incorrect query parameters' }, + { status: 400 }, + ); + } + + // Check cache + const filename = prefix + number + profFirst + profLast + '.txt'; + const headers = { + 'x-api-key': API_KEY, + 'x-storage-key': API_KEY, + }; + const cache = await fetch(API_URL + 'storage/' + API_STORAGE_BUCKET + "/" + filename, { headers }) + + // Fetch RMP + + + // AI + + + // Cache + + + // Return +} From 67fb79f09fc4eaa7aabc25efc98f0636aa69c304 Mon Sep 17 00:00:00 2001 From: davis118 Date: Thu, 13 Nov 2025 13:48:54 -0600 Subject: [PATCH 02/31] RMP fetch + Gemini processing --- package-lock.json | 416 ++++++++++++++++++++++++++++++-- package.json | 1 + src/app/api/rmpSummary/route.ts | 99 +++++++- src/modules/fetchRmp.ts | 26 +- 4 files changed, 510 insertions(+), 32 deletions(-) diff --git a/package-lock.json b/package-lock.json index 46b90d2e..b3d6c4bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", + "@google/genai": "^1.29.1", "@mui/icons-material": "^7.0.2", "@mui/material": "^7.0.2", "@mui/material-nextjs": "^7.0.2", @@ -1851,6 +1852,183 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@google/genai": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.29.1.tgz", + "integrity": "sha512-Buywpq0A6xf9cOdhiWCi5KUiDBbZkjCH5xbl+xxNQRItoYQgd31p0OKyn5cUnT0YNzC/pAmszqXoOc7kncqfFQ==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.20.1" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/@google/genai/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@google/genai/node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@google/genai/node_modules/gaxios": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", + "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@google/genai/node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@google/genai/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@google/genai/node_modules/google-auth-library": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz", + "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.0.0", + "gcp-metadata": "^8.0.0", + "google-logging-utils": "^1.0.0", + "gtoken": "^8.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@google/genai/node_modules/gtoken": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", + "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", + "license": "MIT", + "dependencies": { + "gaxios": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@google/genai/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@google/genai/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@google/genai/node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@google/genai/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "dev": true, @@ -1931,6 +2109,96 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "dev": true, @@ -3954,6 +4222,16 @@ "node": ">=14" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@popperjs/core": { "version": "2.11.8", "license": "MIT", @@ -6132,7 +6410,6 @@ }, "node_modules/ansi-regex": { "version": "5.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7366,7 +7643,6 @@ }, "node_modules/cross-spawn": { "version": "7.0.6", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -7476,7 +7752,6 @@ }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">= 12" @@ -7804,6 +8079,12 @@ "dev": true, "license": "MIT" }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "license": "Apache-2.0", @@ -7822,7 +8103,6 @@ }, "node_modules/emoji-regex": { "version": "9.2.2", - "dev": true, "license": "MIT" }, "node_modules/encodeurl": { @@ -8872,7 +9152,6 @@ }, "node_modules/fetch-blob": { "version": "3.2.0", - "dev": true, "funding": [ { "type": "github", @@ -9207,7 +9486,6 @@ }, "node_modules/formdata-polyfill": { "version": "4.0.10", - "dev": true, "license": "MIT", "dependencies": { "fetch-blob": "^3.1.2" @@ -9599,6 +9877,15 @@ "node": ">=14" } }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/gopd": { "version": "1.2.0", "dev": true, @@ -10320,7 +10607,6 @@ }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -10721,6 +11007,21 @@ "node": ">= 0.4" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/javascript-stringify": { "version": "2.1.0", "dev": true, @@ -11811,7 +12112,6 @@ }, "node_modules/node-domexception": { "version": "1.0.0", - "dev": true, "funding": [ { "type": "github", @@ -11829,7 +12129,6 @@ }, "node_modules/node-fetch": { "version": "3.3.2", - "dev": true, "license": "MIT", "dependencies": { "data-uri-to-buffer": "^4.0.0", @@ -12210,6 +12509,12 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, "node_modules/param-case": { "version": "3.0.4", "dev": true, @@ -12323,7 +12628,6 @@ }, "node_modules/path-key": { "version": "3.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -14655,7 +14959,6 @@ }, "node_modules/shebang-command": { "version": "2.0.0", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -14666,7 +14969,6 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -14897,7 +15199,6 @@ }, "node_modules/string-width": { "version": "4.2.3", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -14908,9 +15209,29 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/string-width/node_modules/emoji-regex": { "version": "8.0.0", - "dev": true, "license": "MIT" }, "node_modules/string.prototype.includes": { @@ -15029,7 +15350,19 @@ }, "node_modules/strip-ansi": { "version": "6.0.1", - "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -15941,7 +16274,6 @@ }, "node_modules/web-streams-polyfill": { "version": "3.3.3", - "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -16276,6 +16608,57 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, "node_modules/wrap-ansi/node_modules/ansi-styles": { "version": "4.3.0", "dev": true, @@ -16313,7 +16696,6 @@ }, "node_modules/ws": { "version": "8.18.2", - "dev": true, "license": "MIT", "engines": { "node": ">=10.0.0" diff --git a/package.json b/package.json index 68d0434e..04e8e177 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", + "@google/genai": "^1.29.1", "@mui/icons-material": "^7.0.2", "@mui/material": "^7.0.2", "@mui/material-nextjs": "^7.0.2", diff --git a/src/app/api/rmpSummary/route.ts b/src/app/api/rmpSummary/route.ts index 1b927b3c..3ccbc5c3 100644 --- a/src/app/api/rmpSummary/route.ts +++ b/src/app/api/rmpSummary/route.ts @@ -1,7 +1,10 @@ import { NextResponse } from 'next/server'; +import { GoogleGenAI } from '@google/genai'; +import fetchRmp from '@/modules/fetchRmp'; +import type { SearchQuery } from '@/types/SearchQuery'; export async function GET(request: Request) { - const API_URL = process.env.NEBULA_API_KEY; + const API_URL = process.env.NEBULA_API_URL; if (typeof API_URL !== 'string') { return NextResponse.json( { message: 'error', data: 'API URL is undefined' }, @@ -15,14 +18,14 @@ export async function GET(request: Request) { { status: 500 }, ); } - const API_STORAGE_BUCKET = process.env.NEBULA_API_KEY; + const API_STORAGE_BUCKET = process.env.NEBULA_API_STORAGE_BUCKET; if (typeof API_STORAGE_BUCKET !== 'string') { return NextResponse.json( { message: 'error', data: 'API storage bucket is undefined' }, { status: 500 }, ); } - const API_STORAGE_KEY = process.env.NEBULA_API_KEY; + const API_STORAGE_KEY = process.env.NEBULA_API_STORAGE_KEY; if (typeof API_STORAGE_KEY !== 'string') { return NextResponse.json( { message: 'error', data: 'API storage key is undefined' }, @@ -35,7 +38,7 @@ export async function GET(request: Request) { const number = searchParams.get('number'); const profFirst = searchParams.get('profFirst'); const profLast = searchParams.get('profLast'); - + const useCache = searchParams.get('useCache'); if ( typeof prefix !== 'string' || typeof number !== 'string' || @@ -50,20 +53,96 @@ export async function GET(request: Request) { // Check cache const filename = prefix + number + profFirst + profLast + '.txt'; + const url = API_URL + 'storage/' + API_STORAGE_BUCKET + '/' + filename; const headers = { 'x-api-key': API_KEY, - 'x-storage-key': API_KEY, + 'x-storage-key': API_STORAGE_KEY, + 'Content-Type': 'application/json', }; - const cache = await fetch(API_URL + 'storage/' + API_STORAGE_BUCKET + "/" + filename, { headers }) - + if (typeof useCache === 'string' && useCache === 'true') { + const cache = await fetch(url, { headers }); + if (cache.ok) { + const cacheData = await cache.json(); + // Cache is valid for 30 days + if ( + new Date(cacheData.data.updated) > + new Date(Date.now() - 1000 * 60 * 60 * 24 * 30) + ) { + const mediaData = await fetch(cacheData.data.media_link, { headers }); + if (mediaData.ok) { + return NextResponse.json( + { message: 'success', data: await mediaData.json() }, + { status: 200 }, + ); + } + } + } + } // Fetch RMP + const searchQuery: SearchQuery = { + prefix: prefix, + number: number, + profFirst: profFirst, + profLast: profLast, + }; + const rmp = await fetchRmp(searchQuery, true); + if (!rmp?.ratings) { + return NextResponse.json( + { message: 'error', data: 'No ratings found' }, + { status: 500 }, + ); + } // AI + const prompt = ` + You are a helpful assistant that summarizes the reviews of a professor. + The reviews are in the following JSON format: + ${JSON.stringify(rmp?.ratings)} + Please summarize the reviews in a concise and informative manner, + synthesizing the most important and relevant information for a student + to know about the professor. + The summary should be in plain-text (no markdown), and should be no more than 100 words. + `; + const GEMINI_SERVICE_ACCOUNT = process.env.GEMINI_SERVICE_ACCOUNT; + if (typeof GEMINI_SERVICE_ACCOUNT !== 'string') { + return NextResponse.json( + { message: 'error', data: 'GEMINI_SERVICE_ACCOUNT is undefined' }, + { status: 500 }, + ); + } + const serviceAccount = JSON.parse(GEMINI_SERVICE_ACCOUNT); + const geminiClient = new GoogleGenAI({ + vertexai: true, + project: serviceAccount.project_id, + googleAuthOptions: { + credentials: { + client_email: serviceAccount.client_email, + private_key: serviceAccount.private_key, + }, + }, + }); + const response = await geminiClient.models.generateContent({ + model: 'gemini-2.5-flash', + contents: prompt, + }); + // Cache response + const cacheResponse = await fetch(url, { + method: 'POST', + headers: headers, + body: JSON.stringify({ data: response.text }), + }); - // Cache - - + if (!cacheResponse.ok) { + return NextResponse.json( + { message: 'error', data: 'Failed to cache response' }, + { status: 500 }, + ); + } // Return + return NextResponse.json( + { message: 'success', data: response.text }, + { status: 200 }, + ); } diff --git a/src/modules/fetchRmp.ts b/src/modules/fetchRmp.ts index 2744a734..0621af83 100644 --- a/src/modules/fetchRmp.ts +++ b/src/modules/fetchRmp.ts @@ -13,7 +13,7 @@ const HEADERS = { }; const OVERWRITES = professor_to_alias as { [key: string]: string }; -function buildProfessorSearchQuery(names: string[]) { +function buildProfessorSearchQuery(names: string[], reviews: boolean) { // Generate the query string with N aliased queries const queries = names .map((_, i) => { @@ -34,7 +34,18 @@ function buildProfessorSearchQuery(names: string[]) { wouldTakeAgainPercent teacherRatingTags { tagName tagCount } ratingsDistribution { total r1 r2 r3 r4 r5 } - } + ${ + reviews && + `ratings (first: 100) { + edges { + node { + comment + } + } + } + } + ` + } } } } @@ -71,8 +82,8 @@ function buildProfessorSearchQuery(names: string[]) { }; } -function getGraphQlUrlProp(names: string[]) { - const query = buildProfessorSearchQuery(names); +function getGraphQlUrlProp(names: string[], reviews: boolean) { + const query = buildProfessorSearchQuery(names, reviews); return { method: 'POST', headers: HEADERS, @@ -107,6 +118,9 @@ export interface RMP { r5: number; total: number; }; + ratings?: { + edges: { node: { comment: string } }[]; + }; } type TeacherSearchResponse = { @@ -134,6 +148,7 @@ function checkProfData( export default async function fetchRmp( query: SearchQuery, + reviews: boolean = false, ): Promise { if ( typeof query.profFirst !== 'string' || @@ -154,6 +169,7 @@ export default async function fetchRmp( // Create fetch object for professor const graphQlUrlProp = getGraphQlUrlProp( aliasName ? [name, aliasName] : [name], + reviews, ); // Fetch professor info by name with graphQL @@ -193,4 +209,4 @@ export default async function fetchRmp( } } return maxRatingsProfessor.node; -} +} \ No newline at end of file From d8aafec7c75a4f922ffad1f4662c91a6f4a14fbf Mon Sep 17 00:00:00 2001 From: davis118 Date: Thu, 13 Nov 2025 14:06:34 -0600 Subject: [PATCH 03/31] fix unrelated lint warnings. how'd they end up here? --- src/app/Home.tsx | 8 ++++++-- src/components/search/SearchBar/SearchBar.tsx | 15 ++++++--------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/app/Home.tsx b/src/app/Home.tsx index e40caa5f..9ab30942 100644 --- a/src/app/Home.tsx +++ b/src/app/Home.tsx @@ -63,15 +63,19 @@ export default function Home(props: Props) { rel="noreferrer" className="bg-royal dark:bg-cornflower-300 text-cornflower-50 dark:text-haiti py-3 px-5 rounded transition hover:scale-[1.01] text-center flex gap-2 items-center mr-auto" > - - Spring 2026 courses are now on Trends! diff --git a/src/components/search/SearchBar/SearchBar.tsx b/src/components/search/SearchBar/SearchBar.tsx index 2031d697..06e85e5f 100644 --- a/src/components/search/SearchBar/SearchBar.tsx +++ b/src/components/search/SearchBar/SearchBar.tsx @@ -165,21 +165,18 @@ export default function SearchBar(props: Props) { 'ex. MKT 3320', 'ex. ANGM 3305 Robert Manriquez', ]; - const [searchBarHintIndex, setSearchBarHintIndex] = useState( - Math.floor(Math.random() * searchBarHints.length), - ); + // Initialize with 0 to avoid hydration mismatch (Math.random() differs on server/client) + const [searchBarHintIndex, setSearchBarHintIndex] = useState(0); - function changeHint() { + useEffect(() => { setSearchBarHintIndex(Math.floor(Math.random() * searchBarHints.length)); - } - useEffect(() => { const interval = setInterval(() => { - changeHint(); + setSearchBarHintIndex(Math.floor(Math.random() * searchBarHints.length)); }, 7000); return () => clearInterval(interval); // Cleanup when component unmounts - }, []); // run on mount + }, [searchBarHints.length]); // Only recreate if hints array changes // updateValue -> onSelect_internal -> updateQueries - clicking enter on an autocomplete suggestion in topMenu Searchbar // updateValue -> onSelect_internal -> onSelect (custom function) - clicking enter on an autocomplete suggestion in home page SearchBar @@ -212,7 +209,7 @@ export default function SearchBar(props: Props) { } function onSelect(newValue: SearchQuery[]) { - changeHint(); + setSearchBarHintIndex(Math.floor(Math.random() * searchBarHints.length)); if (searchTerms == newValue.map((el) => searchQueryLabel(el)).join(',')) // do not initiate a new search when the searchTerms haven't changed return; From 740e6473a6e7a698d110351ec57ac0e0ed363f89 Mon Sep 17 00:00:00 2001 From: davis118 Date: Thu, 13 Nov 2025 14:14:40 -0600 Subject: [PATCH 04/31] prettier --- src/modules/fetchRmp.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/fetchRmp.ts b/src/modules/fetchRmp.ts index 0621af83..0f16cabd 100644 --- a/src/modules/fetchRmp.ts +++ b/src/modules/fetchRmp.ts @@ -209,4 +209,4 @@ export default async function fetchRmp( } } return maxRatingsProfessor.node; -} \ No newline at end of file +} From b0b6dfde51318f33cf6ba462b6c38a184e9dd91b Mon Sep 17 00:00:00 2001 From: davis118 Date: Thu, 13 Nov 2025 14:49:39 -0600 Subject: [PATCH 05/31] cache by prof name (cost) --- src/app/api/rmpSummary/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/api/rmpSummary/route.ts b/src/app/api/rmpSummary/route.ts index 3ccbc5c3..b1d33fc4 100644 --- a/src/app/api/rmpSummary/route.ts +++ b/src/app/api/rmpSummary/route.ts @@ -52,7 +52,7 @@ export async function GET(request: Request) { } // Check cache - const filename = prefix + number + profFirst + profLast + '.txt'; + const filename = profFirst + profLast + '.txt'; const url = API_URL + 'storage/' + API_STORAGE_BUCKET + '/' + filename; const headers = { 'x-api-key': API_KEY, From 852d9c3add72ea8474113ad793239cb82ee99f19 Mon Sep 17 00:00:00 2001 From: davis118 Date: Sun, 23 Nov 2025 01:22:59 -0600 Subject: [PATCH 06/31] remove useCache; add next.js cache; simplify SearchQuery --- src/app/api/rmpSummary/route.ts | 35 ++++++++++++++------------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/src/app/api/rmpSummary/route.ts b/src/app/api/rmpSummary/route.ts index b1d33fc4..245c1cd1 100644 --- a/src/app/api/rmpSummary/route.ts +++ b/src/app/api/rmpSummary/route.ts @@ -38,7 +38,6 @@ export async function GET(request: Request) { const number = searchParams.get('number'); const profFirst = searchParams.get('profFirst'); const profLast = searchParams.get('profLast'); - const useCache = searchParams.get('useCache'); if ( typeof prefix !== 'string' || typeof number !== 'string' || @@ -57,31 +56,27 @@ export async function GET(request: Request) { const headers = { 'x-api-key': API_KEY, 'x-storage-key': API_STORAGE_KEY, - 'Content-Type': 'application/json', }; - if (typeof useCache === 'string' && useCache === 'true') { - const cache = await fetch(url, { headers }); - if (cache.ok) { - const cacheData = await cache.json(); - // Cache is valid for 30 days - if ( - new Date(cacheData.data.updated) > - new Date(Date.now() - 1000 * 60 * 60 * 24 * 30) - ) { - const mediaData = await fetch(cacheData.data.media_link, { headers }); - if (mediaData.ok) { - return NextResponse.json( - { message: 'success', data: await mediaData.json() }, - { status: 200 }, - ); - } + const cache = await fetch(url, { headers, next: { revalidate: 3600 } }); + if (cache.ok) { + const cacheData = await cache.json(); + // Cache is valid for 30 days + if ( + new Date(cacheData.data.updated) > + new Date(Date.now() - 1000 * 60 * 60 * 24 * 30) + ) { + const mediaData = await fetch(cacheData.data.media_link, { headers }); + if (mediaData.ok) { + return NextResponse.json( + { message: 'success', data: await mediaData.json() }, + { status: 200 }, + ); } } } + // Fetch RMP const searchQuery: SearchQuery = { - prefix: prefix, - number: number, profFirst: profFirst, profLast: profLast, }; From 39718b4fba42b3fa391576727bb384e2d678e878 Mon Sep 17 00:00:00 2001 From: Tyler Hill Date: Sun, 23 Nov 2025 16:30:10 -0600 Subject: [PATCH 07/31] Tweak prompt and switch to Gemini 2.5 Flash Lite --- src/app/api/rmpSummary/route.ts | 38 ++++++++++++++++----------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/app/api/rmpSummary/route.ts b/src/app/api/rmpSummary/route.ts index 245c1cd1..bea55154 100644 --- a/src/app/api/rmpSummary/route.ts +++ b/src/app/api/rmpSummary/route.ts @@ -34,16 +34,9 @@ export async function GET(request: Request) { } const { searchParams } = new URL(request.url); - const prefix = searchParams.get('prefix'); - const number = searchParams.get('number'); const profFirst = searchParams.get('profFirst'); const profLast = searchParams.get('profLast'); - if ( - typeof prefix !== 'string' || - typeof number !== 'string' || - typeof profFirst !== 'string' || - typeof profLast !== 'string' - ) { + if (typeof profFirst !== 'string' || typeof profLast !== 'string') { return NextResponse.json( { message: 'error', data: 'Incorrect query parameters' }, { status: 400 }, @@ -88,17 +81,23 @@ export async function GET(request: Request) { { status: 500 }, ); } + if (rmp.ratings.edges.length < 5) { + return NextResponse.json( + { message: 'error', data: 'Not enough ratings for a summary' }, + { status: 500 }, + ); + } // AI - const prompt = ` - You are a helpful assistant that summarizes the reviews of a professor. - The reviews are in the following JSON format: - ${JSON.stringify(rmp?.ratings)} - Please summarize the reviews in a concise and informative manner, - synthesizing the most important and relevant information for a student - to know about the professor. - The summary should be in plain-text (no markdown), and should be no more than 100 words. - `; + const prompt = `Summarize the Rate My Professors reviews of professor ${profFirst} ${profLast}: + +${rmp.ratings.edges.map((rating) => rating.node.comment.replaceAll('\n', ' ').slice(0, 500)).join('\n')} + +Summary requirements: +- Summarize the reviews in a concise and informative manner, synthesizing the most important and relevant information. +- Be respectful but honest. +- Respond in plain-text (no markdown), with no more than 100 words. +`; const GEMINI_SERVICE_ACCOUNT = process.env.GEMINI_SERVICE_ACCOUNT; if (typeof GEMINI_SERVICE_ACCOUNT !== 'string') { return NextResponse.json( @@ -118,11 +117,11 @@ export async function GET(request: Request) { }, }); const response = await geminiClient.models.generateContent({ - model: 'gemini-2.5-flash', + model: 'gemini-2.5-flash-lite', contents: prompt, }); - // Cache response + // Cache response const cacheResponse = await fetch(url, { method: 'POST', headers: headers, @@ -135,6 +134,7 @@ export async function GET(request: Request) { { status: 500 }, ); } + // Return return NextResponse.json( { message: 'success', data: response.text }, From 56632d64672f9b736ee896e927c961d7088a2f6a Mon Sep 17 00:00:00 2001 From: davis118 Date: Sun, 23 Nov 2025 17:16:01 -0600 Subject: [PATCH 08/31] Revert "fix unrelated lint warnings. how'd they end up here?" they were moved to different PR --- src/app/Home.tsx | 8 ++------ src/components/search/SearchBar/SearchBar.tsx | 15 +++++++++------ 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/app/Home.tsx b/src/app/Home.tsx index 9ab30942..e40caa5f 100644 --- a/src/app/Home.tsx +++ b/src/app/Home.tsx @@ -63,19 +63,15 @@ export default function Home(props: Props) { rel="noreferrer" className="bg-royal dark:bg-cornflower-300 text-cornflower-50 dark:text-haiti py-3 px-5 rounded transition hover:scale-[1.01] text-center flex gap-2 items-center mr-auto" > - - Spring 2026 courses are now on Trends! diff --git a/src/components/search/SearchBar/SearchBar.tsx b/src/components/search/SearchBar/SearchBar.tsx index 06e85e5f..2031d697 100644 --- a/src/components/search/SearchBar/SearchBar.tsx +++ b/src/components/search/SearchBar/SearchBar.tsx @@ -165,18 +165,21 @@ export default function SearchBar(props: Props) { 'ex. MKT 3320', 'ex. ANGM 3305 Robert Manriquez', ]; - // Initialize with 0 to avoid hydration mismatch (Math.random() differs on server/client) - const [searchBarHintIndex, setSearchBarHintIndex] = useState(0); + const [searchBarHintIndex, setSearchBarHintIndex] = useState( + Math.floor(Math.random() * searchBarHints.length), + ); - useEffect(() => { + function changeHint() { setSearchBarHintIndex(Math.floor(Math.random() * searchBarHints.length)); + } + useEffect(() => { const interval = setInterval(() => { - setSearchBarHintIndex(Math.floor(Math.random() * searchBarHints.length)); + changeHint(); }, 7000); return () => clearInterval(interval); // Cleanup when component unmounts - }, [searchBarHints.length]); // Only recreate if hints array changes + }, []); // run on mount // updateValue -> onSelect_internal -> updateQueries - clicking enter on an autocomplete suggestion in topMenu Searchbar // updateValue -> onSelect_internal -> onSelect (custom function) - clicking enter on an autocomplete suggestion in home page SearchBar @@ -209,7 +212,7 @@ export default function SearchBar(props: Props) { } function onSelect(newValue: SearchQuery[]) { - setSearchBarHintIndex(Math.floor(Math.random() * searchBarHints.length)); + changeHint(); if (searchTerms == newValue.map((el) => searchQueryLabel(el)).join(',')) // do not initiate a new search when the searchTerms haven't changed return; From 4db3672d374bc9c87d1fd7c67008d23689a25712 Mon Sep 17 00:00:00 2001 From: barkat-10 Date: Mon, 1 Dec 2025 02:16:20 -0600 Subject: [PATCH 09/31] Placeholder for AI generated Syllabus Summary Feature 550 --- .../common/SingleProfInfo/SingleProfInfo.tsx | 117 ++++++++++++++++-- 1 file changed, 109 insertions(+), 8 deletions(-) diff --git a/src/components/common/SingleProfInfo/SingleProfInfo.tsx b/src/components/common/SingleProfInfo/SingleProfInfo.tsx index a1c88984..dbc9f4bd 100644 --- a/src/components/common/SingleProfInfo/SingleProfInfo.tsx +++ b/src/components/common/SingleProfInfo/SingleProfInfo.tsx @@ -4,7 +4,7 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import { Chip, Collapse, Grid, IconButton, Skeleton } from '@mui/material'; import Link from 'next/link'; import React, { useState } from 'react'; - +import ChevronRightIcon from '@mui/icons-material/ChevronRight'; import type { RMP } from '@/modules/fetchRmp'; export function LoadingSingleProfInfo() { @@ -73,6 +73,7 @@ type Props = { export default function SingleProfInfo({ rmp }: Props) { const [showMore, setShowMore] = useState(false); + const [showSyllabus, setShowSyllabus] = useState(false); if (rmp.numRatings == 0) { return ( @@ -165,13 +166,113 @@ export default function SingleProfInfo({ rmp }: Props) { )} - - Visit Rate My Professors - +
+ + Visit Rate My Professors + + + +
+ +
+

Syllabus Grading Summary

+
+ + {/* Outer flex row: tables + AI summary */} +
+ {/* Tables wrapper */} +
+ {/* Weighting Table */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Weighting + %
Attendance5%
Class Quiz20%
Projects20%
Midterm25%
Final30%
+ + {/* Grade Scale Table */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
GradeScale
A90-100
B80-89.9
C70-79.9
D60-69.9
F0-59.9
+
+ + {/* AI Summary / Placeholder */} +
+

+ Regular lecture attendance is mandatory. Attendance will be + taken randomly at some lectures. Students who fail to follow + the class material regularly are inviting scholastic + difficulty. +

+
+
+
+
); From 8b242b3d0c2f2fc911e7f40b1d8460e4a4c3ef1b Mon Sep 17 00:00:00 2001 From: barkat-10 Date: Mon, 1 Dec 2025 13:33:37 -0600 Subject: [PATCH 10/31] Feature 550: used props rather than static placeholders --- .../common/SingleProfInfo/SingleProfInfo.tsx | 68 ++++++------------- .../ProfessorOverview/ProfessorOverview.tsx | 24 ++++++- .../PlannerCoursesTable/PlannerCard.tsx | 22 +++++- .../SearchResultsTable/SearchResultsTable.tsx | 22 +++++- 4 files changed, 86 insertions(+), 50 deletions(-) diff --git a/src/components/common/SingleProfInfo/SingleProfInfo.tsx b/src/components/common/SingleProfInfo/SingleProfInfo.tsx index dbc9f4bd..f50f25a9 100644 --- a/src/components/common/SingleProfInfo/SingleProfInfo.tsx +++ b/src/components/common/SingleProfInfo/SingleProfInfo.tsx @@ -67,11 +67,18 @@ export function LoadingSingleProfInfo() { ); } +type SyllabusData = { + weighting: { label: string; value: string }[]; + grading: { grade: string; range: string }[]; + summary: string; +}; + type Props = { rmp: RMP; + syllabus: SyllabusData; }; -export default function SingleProfInfo({ rmp }: Props) { +export default function SingleProfInfo({ rmp, syllabus }: Props) { const [showMore, setShowMore] = useState(false); const [showSyllabus, setShowSyllabus] = useState(false); @@ -202,26 +209,12 @@ export default function SingleProfInfo({ rmp }: Props) { - - Attendance - 5% - - - Class Quiz - 20% - - - Projects - 20% - - - Midterm - 25% - - - Final - 30% - + {syllabus.weighting.map((row, idx) => ( + + {row.label} + {row.value} + + ))} @@ -234,26 +227,12 @@ export default function SingleProfInfo({ rmp }: Props) { - - A - 90-100 - - - B - 80-89.9 - - - C - 70-79.9 - - - D - 60-69.9 - - - F - 0-59.9 - + {syllabus.grading.map((row, idx) => ( + + {row.grade} + {row.range} + + ))} @@ -263,12 +242,7 @@ export default function SingleProfInfo({ rmp }: Props) { id="ai-summary" className="text-sm flex items-center flex-1 min-h-[100px]" > -

- Regular lecture attendance is mandatory. Attendance will be - taken randomly at some lectures. Students who fail to follow - the class material regularly are inviting scholastic - difficulty. -

+

{syllabus.summary}

diff --git a/src/components/overview/ProfessorOverview/ProfessorOverview.tsx b/src/components/overview/ProfessorOverview/ProfessorOverview.tsx index 404fe63f..a047c3c4 100644 --- a/src/components/overview/ProfessorOverview/ProfessorOverview.tsx +++ b/src/components/overview/ProfessorOverview/ProfessorOverview.tsx @@ -126,7 +126,29 @@ export default function ProfessorOverview({ grades={grades} filteredGrades={calculateGrades(grades)} /> - {rmp && } + {rmp && ( + + )} ); } diff --git a/src/components/planner/PlannerCoursesTable/PlannerCard.tsx b/src/components/planner/PlannerCoursesTable/PlannerCard.tsx index 91039d17..4e7f2f65 100644 --- a/src/components/planner/PlannerCoursesTable/PlannerCard.tsx +++ b/src/components/planner/PlannerCoursesTable/PlannerCard.tsx @@ -832,7 +832,27 @@ export default function PlannerCard(props: PlannerCardProps) { {(latestMatchedSections.type === 'professor' || latestMatchedSections.type === 'combo') && latestMatchedSections.RMP && ( - + )} diff --git a/src/components/search/SearchResultsTable/SearchResultsTable.tsx b/src/components/search/SearchResultsTable/SearchResultsTable.tsx index 0d861860..baaf7fec 100644 --- a/src/components/search/SearchResultsTable/SearchResultsTable.tsx +++ b/src/components/search/SearchResultsTable/SearchResultsTable.tsx @@ -376,7 +376,27 @@ function Row({ filteredGrades={filteredGrades} /> {searchResult.type !== 'course' && searchResult.RMP && ( - + )} From df46223639f1cda8383d624b137636353f2a2bde Mon Sep 17 00:00:00 2001 From: Tyler Hill Date: Sun, 14 Dec 2025 15:53:34 -0600 Subject: [PATCH 11/31] Fix curly brackets for search results --- src/modules/fetchRmp.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/modules/fetchRmp.ts b/src/modules/fetchRmp.ts index 0f16cabd..961a4e9c 100644 --- a/src/modules/fetchRmp.ts +++ b/src/modules/fetchRmp.ts @@ -35,17 +35,18 @@ function buildProfessorSearchQuery(names: string[], reviews: boolean) { teacherRatingTags { tagName tagCount } ratingsDistribution { total r1 r2 r3 r4 r5 } ${ - reviews && - `ratings (first: 100) { + reviews + ? `ratings (first: 100) { edges { node { comment } } } + ` + : '' } - ` - } + } } } } From 3125e049b235e401c35b71b32f5e7e456ae2450f Mon Sep 17 00:00:00 2001 From: Tyler Hill Date: Sun, 14 Dec 2025 16:41:08 -0600 Subject: [PATCH 12/31] Fix extra data in cached results --- src/app/api/rmpSummary/route.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/api/rmpSummary/route.ts b/src/app/api/rmpSummary/route.ts index bea55154..17d8c7df 100644 --- a/src/app/api/rmpSummary/route.ts +++ b/src/app/api/rmpSummary/route.ts @@ -61,7 +61,7 @@ export async function GET(request: Request) { const mediaData = await fetch(cacheData.data.media_link, { headers }); if (mediaData.ok) { return NextResponse.json( - { message: 'success', data: await mediaData.json() }, + { message: 'success', data: await mediaData.text() }, { status: 200 }, ); } @@ -125,7 +125,7 @@ Summary requirements: const cacheResponse = await fetch(url, { method: 'POST', headers: headers, - body: JSON.stringify({ data: response.text }), + body: response.text, }); if (!cacheResponse.ok) { From c1d9738d3ff8665125ae02b6cbef8cc1a225139b Mon Sep 17 00:00:00 2001 From: Tyler Hill Date: Sun, 14 Dec 2025 16:41:19 -0600 Subject: [PATCH 13/31] Tweak prompt --- src/app/api/rmpSummary/route.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/app/api/rmpSummary/route.ts b/src/app/api/rmpSummary/route.ts index 17d8c7df..406e5174 100644 --- a/src/app/api/rmpSummary/route.ts +++ b/src/app/api/rmpSummary/route.ts @@ -94,9 +94,10 @@ export async function GET(request: Request) { ${rmp.ratings.edges.map((rating) => rating.node.comment.replaceAll('\n', ' ').slice(0, 500)).join('\n')} Summary requirements: -- Summarize the reviews in a concise and informative manner, synthesizing the most important and relevant information. -- Be respectful but honest. -- Respond in plain-text (no markdown), with no more than 100 words. +- Summarize the reviews in a concise and informative manner. +- Focus on the structure of the class, exams, projects, homeworks, and assignments. +- Be respectful but honest, like a student writing to a peer. +- Respond in plain-text (no markdown), in 30 words. `; const GEMINI_SERVICE_ACCOUNT = process.env.GEMINI_SERVICE_ACCOUNT; if (typeof GEMINI_SERVICE_ACCOUNT !== 'string') { From 06caee55eb545b3be66f35d802b060a9e3e2a9d1 Mon Sep 17 00:00:00 2001 From: Tyler Hill Date: Sun, 14 Dec 2025 16:42:54 -0600 Subject: [PATCH 14/31] Frontend UI --- .../common/RmpSummary/RmpSummary.tsx | 78 +++++++++++++++++++ .../common/SingleProfInfo/SingleProfInfo.tsx | 16 +++- .../ProfessorOverview/ProfessorOverview.tsx | 2 +- .../PlannerCoursesTable/PlannerCard.tsx | 6 +- .../SearchResultsTable/SearchResultsTable.tsx | 6 +- 5 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 src/components/common/RmpSummary/RmpSummary.tsx diff --git a/src/components/common/RmpSummary/RmpSummary.tsx b/src/components/common/RmpSummary/RmpSummary.tsx new file mode 100644 index 00000000..4a7472aa --- /dev/null +++ b/src/components/common/RmpSummary/RmpSummary.tsx @@ -0,0 +1,78 @@ +'use client'; + +import { Skeleton, Typography } from '@mui/material'; +import React, { useEffect, useState } from 'react'; + +import type { SearchQuery } from '@/types/SearchQuery'; + +export function LoadingRmpSummary() { + return ( + <> + + + + + + AI REVIEW SUMMARY + + + ); +} + +type Props = { + open: boolean; + searchQuery: SearchQuery; +}; + +export default function RmpSummary({ open, searchQuery }: Props) { + const [state, setState] = useState<'closed' | 'loading' | 'error' | 'done'>( + 'closed', + ); + const [summary, setSummary] = useState(null); + + useEffect(() => { + if (open && state === 'closed') { + setState('loading'); + const params = new URLSearchParams(); + if (searchQuery.profFirst) + params.append('profFirst', searchQuery.profFirst); + if (searchQuery.profLast) params.append('profLast', searchQuery.profLast); + fetch(`/api/rmpSummary?${params.toString()}`, { + method: 'GET', + next: { revalidate: 3600 }, + }) + .then((res) => res.json()) + .then((data) => { + if (data.message !== 'success') { + setState('error'); + return; + } + setState('done'); + setSummary(data.data); + }); + } + }, [open, state, searchQuery.profFirst, searchQuery.profLast]); + + if (state === 'error') { + return

Problem loading AI review summary.

; + } + + if (!summary) { + return ; + } + + return ( + <> +

{summary}

+ + AI REVIEW SUMMARY + + + ); +} diff --git a/src/components/common/SingleProfInfo/SingleProfInfo.tsx b/src/components/common/SingleProfInfo/SingleProfInfo.tsx index a1c88984..6001cb22 100644 --- a/src/components/common/SingleProfInfo/SingleProfInfo.tsx +++ b/src/components/common/SingleProfInfo/SingleProfInfo.tsx @@ -5,7 +5,11 @@ import { Chip, Collapse, Grid, IconButton, Skeleton } from '@mui/material'; import Link from 'next/link'; import React, { useState } from 'react'; +import type { SearchQuery } from '@/types/SearchQuery'; import type { RMP } from '@/modules/fetchRmp'; +import RmpSummary, { + LoadingRmpSummary, +} from '@/components/common/RmpSummary/RmpSummary'; export function LoadingSingleProfInfo() { const loadingTags = [ @@ -58,6 +62,10 @@ export function LoadingSingleProfInfo() { + + + +

Visit Rate My Professors

@@ -68,10 +76,12 @@ export function LoadingSingleProfInfo() { } type Props = { + open: boolean; + searchQuery: SearchQuery; rmp: RMP; }; -export default function SingleProfInfo({ rmp }: Props) { +export default function SingleProfInfo({ open, searchQuery, rmp }: Props) { const [showMore, setShowMore] = useState(false); if (rmp.numRatings == 0) { @@ -164,6 +174,10 @@ export default function SingleProfInfo({ rmp }: Props) {
)} + + + + - {rmp && } + {rmp && } ); } diff --git a/src/components/planner/PlannerCoursesTable/PlannerCard.tsx b/src/components/planner/PlannerCoursesTable/PlannerCard.tsx index 91039d17..c3369640 100644 --- a/src/components/planner/PlannerCoursesTable/PlannerCard.tsx +++ b/src/components/planner/PlannerCoursesTable/PlannerCard.tsx @@ -832,7 +832,11 @@ export default function PlannerCard(props: PlannerCardProps) { {(latestMatchedSections.type === 'professor' || latestMatchedSections.type === 'combo') && latestMatchedSections.RMP && ( - + )} diff --git a/src/components/search/SearchResultsTable/SearchResultsTable.tsx b/src/components/search/SearchResultsTable/SearchResultsTable.tsx index 0d861860..001d1bcd 100644 --- a/src/components/search/SearchResultsTable/SearchResultsTable.tsx +++ b/src/components/search/SearchResultsTable/SearchResultsTable.tsx @@ -376,7 +376,11 @@ function Row({ filteredGrades={filteredGrades} /> {searchResult.type !== 'course' && searchResult.RMP && ( - + )} From ca84b55a96cc9b8648cf8581be9ac2e4eca19f9f Mon Sep 17 00:00:00 2001 From: Tyler Hill Date: Mon, 15 Dec 2025 23:01:00 -0600 Subject: [PATCH 15/31] Force refetch summary when search query changes --- src/components/common/RmpSummary/RmpSummary.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/common/RmpSummary/RmpSummary.tsx b/src/components/common/RmpSummary/RmpSummary.tsx index 4a7472aa..2d7162ca 100644 --- a/src/components/common/RmpSummary/RmpSummary.tsx +++ b/src/components/common/RmpSummary/RmpSummary.tsx @@ -1,9 +1,9 @@ 'use client'; import { Skeleton, Typography } from '@mui/material'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; -import type { SearchQuery } from '@/types/SearchQuery'; +import { type SearchQuery, searchQueryEqual } from '@/types/SearchQuery'; export function LoadingRmpSummary() { return ( @@ -28,12 +28,18 @@ type Props = { }; export default function RmpSummary({ open, searchQuery }: Props) { + const searchQueryRef = useRef(searchQuery); const [state, setState] = useState<'closed' | 'loading' | 'error' | 'done'>( 'closed', ); const [summary, setSummary] = useState(null); useEffect(() => { + if (!searchQueryEqual(searchQueryRef.current, searchQuery)) { + searchQueryRef.current = searchQuery; + setState('closed'); + setSummary(null); + } if (open && state === 'closed') { setState('loading'); const params = new URLSearchParams(); @@ -54,7 +60,7 @@ export default function RmpSummary({ open, searchQuery }: Props) { setSummary(data.data); }); } - }, [open, state, searchQuery.profFirst, searchQuery.profLast]); + }, [open, state, searchQuery]); if (state === 'error') { return

Problem loading AI review summary.

; From 3706b9a3565c9aa8114c51a5fbcc1ee10e76c59e Mon Sep 17 00:00:00 2001 From: Shurgbee <81319757+shurgbee@users.noreply.github.com> Date: Sun, 21 Dec 2025 16:32:02 -0600 Subject: [PATCH 16/31] Remove next cache validation --- src/app/api/rmpSummary/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/api/rmpSummary/route.ts b/src/app/api/rmpSummary/route.ts index 406e5174..52ba36af 100644 --- a/src/app/api/rmpSummary/route.ts +++ b/src/app/api/rmpSummary/route.ts @@ -50,7 +50,7 @@ export async function GET(request: Request) { 'x-api-key': API_KEY, 'x-storage-key': API_STORAGE_KEY, }; - const cache = await fetch(url, { headers, next: { revalidate: 3600 } }); + const cache = await fetch(url, { headers}); if (cache.ok) { const cacheData = await cache.json(); // Cache is valid for 30 days From 9f1f6aaeed34e90199ed2120ddfbfb3eb8ebe00a Mon Sep 17 00:00:00 2001 From: Shurgbee <81319757+shurgbee@users.noreply.github.com> Date: Tue, 23 Dec 2025 12:56:05 -0600 Subject: [PATCH 17/31] Added Summary Tooltip and removed media data headers --- src/app/api/rmpSummary/route.ts | 4 ++-- src/components/common/RmpSummary/RmpSummary.tsx | 17 +++++++++++------ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/app/api/rmpSummary/route.ts b/src/app/api/rmpSummary/route.ts index 52ba36af..5103db09 100644 --- a/src/app/api/rmpSummary/route.ts +++ b/src/app/api/rmpSummary/route.ts @@ -50,7 +50,7 @@ export async function GET(request: Request) { 'x-api-key': API_KEY, 'x-storage-key': API_STORAGE_KEY, }; - const cache = await fetch(url, { headers}); + const cache = await fetch(url, { headers }); if (cache.ok) { const cacheData = await cache.json(); // Cache is valid for 30 days @@ -58,7 +58,7 @@ export async function GET(request: Request) { new Date(cacheData.data.updated) > new Date(Date.now() - 1000 * 60 * 60 * 24 * 30) ) { - const mediaData = await fetch(cacheData.data.media_link, { headers }); + const mediaData = await fetch(cacheData.data.media_link); if (mediaData.ok) { return NextResponse.json( { message: 'success', data: await mediaData.text() }, diff --git a/src/components/common/RmpSummary/RmpSummary.tsx b/src/components/common/RmpSummary/RmpSummary.tsx index 2d7162ca..c6330fb8 100644 --- a/src/components/common/RmpSummary/RmpSummary.tsx +++ b/src/components/common/RmpSummary/RmpSummary.tsx @@ -1,6 +1,6 @@ 'use client'; -import { Skeleton, Typography } from '@mui/material'; +import { Skeleton, Tooltip, Typography } from '@mui/material'; import React, { useEffect, useRef, useState } from 'react'; import { type SearchQuery, searchQueryEqual } from '@/types/SearchQuery'; @@ -73,12 +73,17 @@ export default function RmpSummary({ open, searchQuery }: Props) { return ( <>

{summary}

- - AI REVIEW SUMMARY - + + AI REVIEW SUMMARY + + ); } From 0eecd704ade9c09f3a79c98db878b9db042e7539 Mon Sep 17 00:00:00 2001 From: Shurgbee Date: Thu, 1 Jan 2026 14:32:46 -0600 Subject: [PATCH 18/31] run format --- src/app/api/rmpSummary/route.ts | 4 ++-- src/components/common/RmpSummary/RmpSummary.tsx | 3 +-- .../common/SingleProfInfo/SingleProfInfo.tsx | 12 +++++------- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/app/api/rmpSummary/route.ts b/src/app/api/rmpSummary/route.ts index 5103db09..e5c780d9 100644 --- a/src/app/api/rmpSummary/route.ts +++ b/src/app/api/rmpSummary/route.ts @@ -1,7 +1,7 @@ -import { NextResponse } from 'next/server'; -import { GoogleGenAI } from '@google/genai'; import fetchRmp from '@/modules/fetchRmp'; import type { SearchQuery } from '@/types/SearchQuery'; +import { GoogleGenAI } from '@google/genai'; +import { NextResponse } from 'next/server'; export async function GET(request: Request) { const API_URL = process.env.NEBULA_API_URL; diff --git a/src/components/common/RmpSummary/RmpSummary.tsx b/src/components/common/RmpSummary/RmpSummary.tsx index c6330fb8..22676e51 100644 --- a/src/components/common/RmpSummary/RmpSummary.tsx +++ b/src/components/common/RmpSummary/RmpSummary.tsx @@ -1,10 +1,9 @@ 'use client'; +import { searchQueryEqual, type SearchQuery } from '@/types/SearchQuery'; import { Skeleton, Tooltip, Typography } from '@mui/material'; import React, { useEffect, useRef, useState } from 'react'; -import { type SearchQuery, searchQueryEqual } from '@/types/SearchQuery'; - export function LoadingRmpSummary() { return ( <> diff --git a/src/components/common/SingleProfInfo/SingleProfInfo.tsx b/src/components/common/SingleProfInfo/SingleProfInfo.tsx index 4766ac17..e36be189 100644 --- a/src/components/common/SingleProfInfo/SingleProfInfo.tsx +++ b/src/components/common/SingleProfInfo/SingleProfInfo.tsx @@ -1,17 +1,15 @@ 'use client'; -import type { RMP } from '@/modules/fetchRmp'; +import RmpSummary, { + LoadingRmpSummary, +} from '@/components/common/RmpSummary/RmpSummary'; +import type { RMP, RMP } from '@/modules/fetchRmp'; +import type { SearchQuery } from '@/types/SearchQuery'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import { Chip, Collapse, Grid, IconButton, Skeleton } from '@mui/material'; import Link from 'next/link'; import React, { useState } from 'react'; -import type { SearchQuery } from '@/types/SearchQuery'; -import type { RMP } from '@/modules/fetchRmp'; -import RmpSummary, { - LoadingRmpSummary, -} from '@/components/common/RmpSummary/RmpSummary'; - export function LoadingSingleProfInfo() { const loadingTags = [ 'Group projects (54)', From 576c2fb06be9661591c5d484c8e98a6c215c247c Mon Sep 17 00:00:00 2001 From: egsch Date: Sat, 3 Jan 2026 00:27:26 -0700 Subject: [PATCH 19/31] fix import in SingleProfInput --- src/components/common/SingleProfInfo/SingleProfInfo.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/common/SingleProfInfo/SingleProfInfo.tsx b/src/components/common/SingleProfInfo/SingleProfInfo.tsx index e36be189..1728098a 100644 --- a/src/components/common/SingleProfInfo/SingleProfInfo.tsx +++ b/src/components/common/SingleProfInfo/SingleProfInfo.tsx @@ -3,7 +3,7 @@ import RmpSummary, { LoadingRmpSummary, } from '@/components/common/RmpSummary/RmpSummary'; -import type { RMP, RMP } from '@/modules/fetchRmp'; +import type { RMP } from '@/modules/fetchRmp'; import type { SearchQuery } from '@/types/SearchQuery'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import { Chip, Collapse, Grid, IconButton, Skeleton } from '@mui/material'; From 665563cede3bab66f759efc30bc84335735d03b2 Mon Sep 17 00:00:00 2001 From: AbhiramTadepalli Date: Thu, 8 Jan 2026 10:26:57 -0600 Subject: [PATCH 20/31] copy over rmpSummary route for syllabusSummary --- src/app/api/syllabusSummary/route.ts | 144 +++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 src/app/api/syllabusSummary/route.ts diff --git a/src/app/api/syllabusSummary/route.ts b/src/app/api/syllabusSummary/route.ts new file mode 100644 index 00000000..da940982 --- /dev/null +++ b/src/app/api/syllabusSummary/route.ts @@ -0,0 +1,144 @@ +import fetchRmp from '@/modules/fetchRmp'; +import type { SearchQuery } from '@/types/SearchQuery'; +import { GoogleGenAI } from '@google/genai'; +import { NextResponse } from 'next/server'; + +export async function GET(request: Request) { + const API_URL = process.env.NEBULA_API_URL; + if (typeof API_URL !== 'string') { + return NextResponse.json( + { message: 'error', data: 'API URL is undefined' }, + { status: 500 }, + ); + } + const API_KEY = process.env.NEBULA_API_KEY; + if (typeof API_KEY !== 'string') { + return NextResponse.json( + { message: 'error', data: 'API key is undefined' }, + { status: 500 }, + ); + } + const API_STORAGE_BUCKET = process.env.NEBULA_API_STORAGE_BUCKET; + if (typeof API_STORAGE_BUCKET !== 'string') { + return NextResponse.json( + { message: 'error', data: 'API storage bucket is undefined' }, + { status: 500 }, + ); + } + const API_STORAGE_KEY = process.env.NEBULA_API_STORAGE_KEY; + if (typeof API_STORAGE_KEY !== 'string') { + return NextResponse.json( + { message: 'error', data: 'API storage key is undefined' }, + { status: 500 }, + ); + } + + const { searchParams } = new URL(request.url); + const profFirst = searchParams.get('profFirst'); + const profLast = searchParams.get('profLast'); + if (typeof profFirst !== 'string' || typeof profLast !== 'string') { + return NextResponse.json( + { message: 'error', data: 'Incorrect query parameters' }, + { status: 400 }, + ); + } + + // Check cache + const filename = profFirst + profLast + '.txt'; + const url = API_URL + 'storage/' + API_STORAGE_BUCKET + '/' + filename; + const headers = { + 'x-api-key': API_KEY, + 'x-storage-key': API_STORAGE_KEY, + }; + const cache = await fetch(url, { headers }); + if (cache.ok) { + const cacheData = await cache.json(); + // Cache is valid for 30 days + if ( + new Date(cacheData.data.updated) > + new Date(Date.now() - 1000 * 60 * 60 * 24 * 30) + ) { + const mediaData = await fetch(cacheData.data.media_link); + if (mediaData.ok) { + return NextResponse.json( + { message: 'success', data: await mediaData.text() }, + { status: 200 }, + ); + } + } + } + + // Fetch RMP + const searchQuery: SearchQuery = { + profFirst: profFirst, + profLast: profLast, + }; + const rmp = await fetchRmp(searchQuery, true); + + if (!rmp?.ratings) { + return NextResponse.json( + { message: 'error', data: 'No ratings found' }, + { status: 500 }, + ); + } + if (rmp.ratings.edges.length < 5) { + return NextResponse.json( + { message: 'error', data: 'Not enough ratings for a summary' }, + { status: 500 }, + ); + } + + // AI + const prompt = `Summarize the Rate My Professors reviews of professor ${profFirst} ${profLast}: + +${rmp.ratings.edges.map((rating) => rating.node.comment.replaceAll('\n', ' ').slice(0, 500)).join('\n')} + +Summary requirements: +- Summarize the reviews in a concise and informative manner. +- Focus on the structure of the class, exams, projects, homeworks, and assignments. +- Be respectful but honest, like a student writing to a peer. +- Respond in plain-text (no markdown), in 30 words. +`; + const GEMINI_SERVICE_ACCOUNT = process.env.GEMINI_SERVICE_ACCOUNT; + if (typeof GEMINI_SERVICE_ACCOUNT !== 'string') { + return NextResponse.json( + { message: 'error', data: 'GEMINI_SERVICE_ACCOUNT is undefined' }, + { status: 500 }, + ); + } + const serviceAccount = JSON.parse(GEMINI_SERVICE_ACCOUNT); + const geminiClient = new GoogleGenAI({ + vertexai: true, + project: serviceAccount.project_id, + googleAuthOptions: { + credentials: { + client_email: serviceAccount.client_email, + private_key: serviceAccount.private_key, + }, + }, + }); + const response = await geminiClient.models.generateContent({ + model: 'gemini-2.5-flash-lite', + contents: prompt, + }); + + // Cache response + const cacheResponse = await fetch(url, { + method: 'POST', + headers: headers, + body: response.text, + }); + + if (!cacheResponse.ok) { + return NextResponse.json( + { message: 'error', data: 'Failed to cache response' }, + { status: 500 }, + ); + } + + // Return + return NextResponse.json( + { message: 'success', data: response.text }, + { status: 200 }, + ); +} \ No newline at end of file From d8037af58a451cc014581386996780a133dc170f Mon Sep 17 00:00:00 2001 From: AbhiramTadepalli Date: Thu, 8 Jan 2026 10:27:14 -0600 Subject: [PATCH 21/31] use syllabus_uri param --- src/app/api/syllabusSummary/route.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/app/api/syllabusSummary/route.ts b/src/app/api/syllabusSummary/route.ts index da940982..30ed9feb 100644 --- a/src/app/api/syllabusSummary/route.ts +++ b/src/app/api/syllabusSummary/route.ts @@ -34,9 +34,8 @@ export async function GET(request: Request) { } const { searchParams } = new URL(request.url); - const profFirst = searchParams.get('profFirst'); - const profLast = searchParams.get('profLast'); - if (typeof profFirst !== 'string' || typeof profLast !== 'string') { + const syllabus_uri = searchParams.get('syllabus_uri'); + if (typeof syllabus_uri !== 'string') { return NextResponse.json( { message: 'error', data: 'Incorrect query parameters' }, { status: 400 }, @@ -44,7 +43,7 @@ export async function GET(request: Request) { } // Check cache - const filename = profFirst + profLast + '.txt'; + const filename = syllabus_uri + '.txt'; const url = API_URL + 'storage/' + API_STORAGE_BUCKET + '/' + filename; const headers = { 'x-api-key': API_KEY, From 22bb4df19358d43476fd725bf05780bfe2ecb6b2 Mon Sep 17 00:00:00 2001 From: AbhiramTadepalli Date: Thu, 8 Jan 2026 19:57:06 -0600 Subject: [PATCH 22/31] build syllabus gemini pipeline --- src/app/api/syllabusSummary/route.ts | 95 ++++++++++++++++++---------- 1 file changed, 63 insertions(+), 32 deletions(-) diff --git a/src/app/api/syllabusSummary/route.ts b/src/app/api/syllabusSummary/route.ts index 30ed9feb..cf3911a8 100644 --- a/src/app/api/syllabusSummary/route.ts +++ b/src/app/api/syllabusSummary/route.ts @@ -3,6 +3,37 @@ import type { SearchQuery } from '@/types/SearchQuery'; import { GoogleGenAI } from '@google/genai'; import { NextResponse } from 'next/server'; +const syllabusResponseSchema = { + type: 'OBJECT', + properties: { + summary: { + type: 'STRING', + description: 'A direct, no-fluff summary of the course content and professor style.', + }, + grade_weights: { + type: 'ARRAY', + items: { + type: 'OBJECT', + properties: { + category: { type: 'STRING', description: 'e.g., Attendance, Midterm' }, + percentage: { type: 'STRING', description: 'e.g., 5%, 20%' }, + }, + }, + }, + grade_scale: { + type: 'ARRAY', + items: { + type: 'OBJECT', + properties: { + grade: { type: 'STRING', description: 'e.g., A, B' }, + scale: { type: 'STRING', description: 'e.g., 90-100, 80-89.9' }, + }, + }, + }, + }, + required: ['summary', 'grade_weights', 'grade_scale'], +}; + export async function GET(request: Request) { const API_URL = process.env.NEBULA_API_URL; if (typeof API_URL !== 'string') { @@ -57,7 +88,7 @@ export async function GET(request: Request) { new Date(cacheData.data.updated) > new Date(Date.now() - 1000 * 60 * 60 * 24 * 30) ) { - const mediaData = await fetch(cacheData.data.media_link); + const mediaData = await fetch(cacheData.data.media_link); //TODO: what is media_link? if (mediaData.ok) { return NextResponse.json( { message: 'success', data: await mediaData.text() }, @@ -67,37 +98,18 @@ export async function GET(request: Request) { } } - // Fetch RMP - const searchQuery: SearchQuery = { - profFirst: profFirst, - profLast: profLast, - }; - const rmp = await fetchRmp(searchQuery, true); + // Fetch Syllabus from URI + const syllabus = await fetch(syllabus_uri); - if (!rmp?.ratings) { - return NextResponse.json( - { message: 'error', data: 'No ratings found' }, - { status: 500 }, - ); - } - if (rmp.ratings.edges.length < 5) { - return NextResponse.json( - { message: 'error', data: 'Not enough ratings for a summary' }, - { status: 500 }, - ); - } - - // AI - const prompt = `Summarize the Rate My Professors reviews of professor ${profFirst} ${profLast}: + if (!syllabus.ok) { + return NextResponse.json({ error: 'Failed to fetch Syllabus from URI' }, { status: 500 }); + } -${rmp.ratings.edges.map((rating) => rating.node.comment.replaceAll('\n', ' ').slice(0, 500)).join('\n')} + const arrayBuffer = await syllabus.arrayBuffer(); + const pdfBase64 = Buffer.from(arrayBuffer).toString('base64'); -Summary requirements: -- Summarize the reviews in a concise and informative manner. -- Focus on the structure of the class, exams, projects, homeworks, and assignments. -- Be respectful but honest, like a student writing to a peer. -- Respond in plain-text (no markdown), in 30 words. -`; + // AI + const prompt = ``; const GEMINI_SERVICE_ACCOUNT = process.env.GEMINI_SERVICE_ACCOUNT; if (typeof GEMINI_SERVICE_ACCOUNT !== 'string') { return NextResponse.json( @@ -117,9 +129,28 @@ Summary requirements: }, }); const response = await geminiClient.models.generateContent({ - model: 'gemini-2.5-flash-lite', - contents: prompt, - }); + model: 'gemini-1.5-flash', + config: { + responseMimeType: 'application/json', + responseSchema: syllabusResponseSchema, + }, + contents: [ + { + role: 'user', + parts: [ + { + inlineData: { + mimeType: 'application/pdf', + data: pdfBase64, + }, + }, + { + text: 'Extract the grading weights and grade scale exactly as shown in the tables. Provide a concise summary of the course.', + }, + ], + }, + ], + }); // Cache response const cacheResponse = await fetch(url, { From 1bc5da58906a778c3d507449555c37e33c1dc099 Mon Sep 17 00:00:00 2001 From: AbhiramTadepalli Date: Thu, 8 Jan 2026 20:28:18 -0600 Subject: [PATCH 23/31] use 2.5-flash-lite model for now --- src/app/api/syllabusSummary/route.ts | 80 ++++++++++++++-------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/src/app/api/syllabusSummary/route.ts b/src/app/api/syllabusSummary/route.ts index cf3911a8..9d4d37e3 100644 --- a/src/app/api/syllabusSummary/route.ts +++ b/src/app/api/syllabusSummary/route.ts @@ -73,30 +73,30 @@ export async function GET(request: Request) { ); } - // Check cache - const filename = syllabus_uri + '.txt'; - const url = API_URL + 'storage/' + API_STORAGE_BUCKET + '/' + filename; - const headers = { - 'x-api-key': API_KEY, - 'x-storage-key': API_STORAGE_KEY, - }; - const cache = await fetch(url, { headers }); - if (cache.ok) { - const cacheData = await cache.json(); - // Cache is valid for 30 days - if ( - new Date(cacheData.data.updated) > - new Date(Date.now() - 1000 * 60 * 60 * 24 * 30) - ) { - const mediaData = await fetch(cacheData.data.media_link); //TODO: what is media_link? - if (mediaData.ok) { - return NextResponse.json( - { message: 'success', data: await mediaData.text() }, - { status: 200 }, - ); - } - } - } +// // Check cache +// const filename = syllabus_uri + '.txt'; +// const url = API_URL + 'storage/' + API_STORAGE_BUCKET + '/' + filename; +// const headers = { +// 'x-api-key': API_KEY, +// 'x-storage-key': API_STORAGE_KEY, +// }; +// const cache = await fetch(url, { headers }); +// if (cache.ok) { +// const cacheData = await cache.json(); +// // Cache is valid for 30 days +// if ( +// new Date(cacheData.data.updated) > +// new Date(Date.now() - 1000 * 60 * 60 * 24 * 30) +// ) { +// const mediaData = await fetch(cacheData.data.media_link); //TODO: what is media_link? +// if (mediaData.ok) { +// return NextResponse.json( +// { message: 'success', data: await mediaData.text() }, +// { status: 200 }, +// ); +// } +// } +// } // Fetch Syllabus from URI const syllabus = await fetch(syllabus_uri); @@ -129,7 +129,7 @@ export async function GET(request: Request) { }, }); const response = await geminiClient.models.generateContent({ - model: 'gemini-1.5-flash', + model: 'gemini-2.5-flash-lite', config: { responseMimeType: 'application/json', responseSchema: syllabusResponseSchema, @@ -145,30 +145,30 @@ export async function GET(request: Request) { }, }, { - text: 'Extract the grading weights and grade scale exactly as shown in the tables. Provide a concise summary of the course.', + text: 'Extract the grading weights and grade scale exactly as shown in the tables. Provide a concise summary of the course for students.', }, ], }, ], }); - // Cache response - const cacheResponse = await fetch(url, { - method: 'POST', - headers: headers, - body: response.text, - }); - - if (!cacheResponse.ok) { - return NextResponse.json( - { message: 'error', data: 'Failed to cache response' }, - { status: 500 }, - ); - } +// // Cache response +// const cacheResponse = await fetch(url, { +// method: 'POST', +// headers: headers, +// body: response.text, +// }); +// if (!cacheResponse.ok) { +// return NextResponse.json( +// { message: 'error', data: 'Failed to cache response' }, +// { status: 500 }, +// ); +// } + const responseData = JSON.parse(response.text ?? ''); // Return return NextResponse.json( - { message: 'success', data: response.text }, + { message: 'success', data: responseData }, { status: 200 }, ); } \ No newline at end of file From 1d8b3837f205a5a71d2195df85262e091d2bead3 Mon Sep 17 00:00:00 2001 From: AbhiramTadepalli Date: Thu, 8 Jan 2026 20:32:39 -0600 Subject: [PATCH 24/31] try to make prompt better --- src/app/api/syllabusSummary/route.ts | 146 ++++++++++++++------------- 1 file changed, 75 insertions(+), 71 deletions(-) diff --git a/src/app/api/syllabusSummary/route.ts b/src/app/api/syllabusSummary/route.ts index 9d4d37e3..53e036ad 100644 --- a/src/app/api/syllabusSummary/route.ts +++ b/src/app/api/syllabusSummary/route.ts @@ -1,5 +1,3 @@ -import fetchRmp from '@/modules/fetchRmp'; -import type { SearchQuery } from '@/types/SearchQuery'; import { GoogleGenAI } from '@google/genai'; import { NextResponse } from 'next/server'; @@ -8,14 +6,18 @@ const syllabusResponseSchema = { properties: { summary: { type: 'STRING', - description: 'A direct, no-fluff summary of the course content and professor style.', + description: + 'A direct, no-fluff summary of the course structure & professor style.', }, grade_weights: { type: 'ARRAY', items: { type: 'OBJECT', properties: { - category: { type: 'STRING', description: 'e.g., Attendance, Midterm' }, + category: { + type: 'STRING', + description: 'e.g., Attendance, Midterm', + }, percentage: { type: 'STRING', description: 'e.g., 5%, 20%' }, }, }, @@ -73,43 +75,45 @@ export async function GET(request: Request) { ); } -// // Check cache -// const filename = syllabus_uri + '.txt'; -// const url = API_URL + 'storage/' + API_STORAGE_BUCKET + '/' + filename; -// const headers = { -// 'x-api-key': API_KEY, -// 'x-storage-key': API_STORAGE_KEY, -// }; -// const cache = await fetch(url, { headers }); -// if (cache.ok) { -// const cacheData = await cache.json(); -// // Cache is valid for 30 days -// if ( -// new Date(cacheData.data.updated) > -// new Date(Date.now() - 1000 * 60 * 60 * 24 * 30) -// ) { -// const mediaData = await fetch(cacheData.data.media_link); //TODO: what is media_link? -// if (mediaData.ok) { -// return NextResponse.json( -// { message: 'success', data: await mediaData.text() }, -// { status: 200 }, -// ); -// } -// } -// } + // // Check cache + // const filename = syllabus_uri + '.txt'; + // const url = API_URL + 'storage/' + API_STORAGE_BUCKET + '/' + filename; + // const headers = { + // 'x-api-key': API_KEY, + // 'x-storage-key': API_STORAGE_KEY, + // }; + // const cache = await fetch(url, { headers }); + // if (cache.ok) { + // const cacheData = await cache.json(); + // // Cache is valid for 30 days + // if ( + // new Date(cacheData.data.updated) > + // new Date(Date.now() - 1000 * 60 * 60 * 24 * 30) + // ) { + // const mediaData = await fetch(cacheData.data.media_link); //TODO: what is media_link? + // if (mediaData.ok) { + // return NextResponse.json( + // { message: 'success', data: await mediaData.text() }, + // { status: 200 }, + // ); + // } + // } + // } - // Fetch Syllabus from URI - const syllabus = await fetch(syllabus_uri); + // Fetch Syllabus from URI + const syllabus = await fetch(syllabus_uri); - if (!syllabus.ok) { - return NextResponse.json({ error: 'Failed to fetch Syllabus from URI' }, { status: 500 }); - } + if (!syllabus.ok) { + return NextResponse.json( + { error: 'Failed to fetch Syllabus from URI' }, + { status: 500 }, + ); + } - const arrayBuffer = await syllabus.arrayBuffer(); - const pdfBase64 = Buffer.from(arrayBuffer).toString('base64'); + const arrayBuffer = await syllabus.arrayBuffer(); + const pdfBase64 = Buffer.from(arrayBuffer).toString('base64'); // AI - const prompt = ``; const GEMINI_SERVICE_ACCOUNT = process.env.GEMINI_SERVICE_ACCOUNT; if (typeof GEMINI_SERVICE_ACCOUNT !== 'string') { return NextResponse.json( @@ -129,46 +133,46 @@ export async function GET(request: Request) { }, }); const response = await geminiClient.models.generateContent({ - model: 'gemini-2.5-flash-lite', - config: { - responseMimeType: 'application/json', - responseSchema: syllabusResponseSchema, - }, - contents: [ - { - role: 'user', - parts: [ - { - inlineData: { - mimeType: 'application/pdf', - data: pdfBase64, - }, - }, - { - text: 'Extract the grading weights and grade scale exactly as shown in the tables. Provide a concise summary of the course for students.', + model: 'gemini-2.5-flash-lite', + config: { + responseMimeType: 'application/json', + responseSchema: syllabusResponseSchema, + }, + contents: [ + { + role: 'user', + parts: [ + { + inlineData: { + mimeType: 'application/pdf', + data: pdfBase64, }, - ], - }, - ], - }); + }, + { + text: 'Extract the grading weights and grade scale exactly as shown in the tables. Provide a concise summary of the course structure & professor style for students.', + }, + ], + }, + ], + }); -// // Cache response -// const cacheResponse = await fetch(url, { -// method: 'POST', -// headers: headers, -// body: response.text, -// }); + // // Cache response + // const cacheResponse = await fetch(url, { + // method: 'POST', + // headers: headers, + // body: response.text, + // }); -// if (!cacheResponse.ok) { -// return NextResponse.json( -// { message: 'error', data: 'Failed to cache response' }, -// { status: 500 }, -// ); -// } - const responseData = JSON.parse(response.text ?? ''); + // if (!cacheResponse.ok) { + // return NextResponse.json( + // { message: 'error', data: 'Failed to cache response' }, + // { status: 500 }, + // ); + // } + const responseData = JSON.parse(response.text ?? ''); // Return return NextResponse.json( { message: 'success', data: responseData }, { status: 200 }, ); -} \ No newline at end of file +} From 3e8fedfe185badc9cc636768e2ae5385086ac0f1 Mon Sep 17 00:00:00 2001 From: AbhiramTadepalli Date: Thu, 8 Jan 2026 21:46:41 -0600 Subject: [PATCH 25/31] format --- src/components/common/SingleProfInfo/SingleProfInfo.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/common/SingleProfInfo/SingleProfInfo.tsx b/src/components/common/SingleProfInfo/SingleProfInfo.tsx index a7ce288d..49fa368e 100644 --- a/src/components/common/SingleProfInfo/SingleProfInfo.tsx +++ b/src/components/common/SingleProfInfo/SingleProfInfo.tsx @@ -5,11 +5,11 @@ import RmpSummary, { } from '@/components/common/RmpSummary/RmpSummary'; import type { RMP } from '@/modules/fetchRmp'; import type { SearchQuery } from '@/types/SearchQuery'; +import ChevronRightIcon from '@mui/icons-material/ChevronRight'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import { Chip, Collapse, Grid, IconButton, Skeleton } from '@mui/material'; import Link from 'next/link'; import React, { useState } from 'react'; -import ChevronRightIcon from '@mui/icons-material/ChevronRight'; export function LoadingSingleProfInfo() { const loadingTags = [ @@ -88,7 +88,12 @@ type Props = { syllabus: SyllabusData; }; -export default function SingleProfInfo({ open, searchQuery, rmp, syllabus }: Props) { +export default function SingleProfInfo({ + open, + searchQuery, + rmp, + syllabus, +}: Props) { const [showMore, setShowMore] = useState(false); const [showSyllabus, setShowSyllabus] = useState(false); From 5084b040ee8a3a4e6954702a0beb7aaed0cd128f Mon Sep 17 00:00:00 2001 From: AbhiramTadepalli Date: Fri, 9 Jan 2026 22:01:17 -0600 Subject: [PATCH 26/31] Create SyllabusSummary component and link the api to the frontend --- src/app/api/syllabusSummary/route.ts | 6 +- .../common/SingleProfInfo/SingleProfInfo.tsx | 77 ++------- .../SyllabusSummary/SyllabusSummary.tsx | 152 ++++++++++++++++++ .../ProfessorOverview/ProfessorOverview.tsx | 26 +-- .../PlannerCoursesTable/PlannerCard.tsx | 30 ++-- .../SearchResultsTable/SearchResultsTable.tsx | 30 ++-- 6 files changed, 191 insertions(+), 130 deletions(-) create mode 100644 src/components/common/SyllabusSummary/SyllabusSummary.tsx diff --git a/src/app/api/syllabusSummary/route.ts b/src/app/api/syllabusSummary/route.ts index 53e036ad..0d5f8aca 100644 --- a/src/app/api/syllabusSummary/route.ts +++ b/src/app/api/syllabusSummary/route.ts @@ -22,18 +22,18 @@ const syllabusResponseSchema = { }, }, }, - grade_scale: { + letter_grade_scale: { type: 'ARRAY', items: { type: 'OBJECT', properties: { grade: { type: 'STRING', description: 'e.g., A, B' }, - scale: { type: 'STRING', description: 'e.g., 90-100, 80-89.9' }, + range: { type: 'STRING', description: 'e.g., 90-100, 80-89.9' }, }, }, }, }, - required: ['summary', 'grade_weights', 'grade_scale'], + required: ['summary', 'grade_weights', 'letter_grade_scale'], }; export async function GET(request: Request) { diff --git a/src/components/common/SingleProfInfo/SingleProfInfo.tsx b/src/components/common/SingleProfInfo/SingleProfInfo.tsx index 49fa368e..10c244ed 100644 --- a/src/components/common/SingleProfInfo/SingleProfInfo.tsx +++ b/src/components/common/SingleProfInfo/SingleProfInfo.tsx @@ -3,6 +3,7 @@ import RmpSummary, { LoadingRmpSummary, } from '@/components/common/RmpSummary/RmpSummary'; +import SyllabusSummary from '@/components/common/SyllabusSummary/SyllabusSummary'; import type { RMP } from '@/modules/fetchRmp'; import type { SearchQuery } from '@/types/SearchQuery'; import ChevronRightIcon from '@mui/icons-material/ChevronRight'; @@ -75,24 +76,18 @@ export function LoadingSingleProfInfo() { ); } -type SyllabusData = { - weighting: { label: string; value: string }[]; - grading: { grade: string; range: string }[]; - summary: string; -}; - type Props = { open: boolean; searchQuery: SearchQuery; rmp: RMP; - syllabus: SyllabusData; + syllabus_uri?: string | null; }; export default function SingleProfInfo({ open, searchQuery, rmp, - syllabus, + syllabus_uri, }: Props) { const [showMore, setShowMore] = useState(false); const [showSyllabus, setShowSyllabus] = useState(false); @@ -208,64 +203,14 @@ export default function SingleProfInfo({ /> - -
-

Syllabus Grading Summary

-
- - {/* Outer flex row: tables + AI summary */} -
- {/* Tables wrapper */} -
- {/* Weighting Table */} - - - - - - - - - {syllabus.weighting.map((row, idx) => ( - - - - - ))} - -
- Weighting - %
{row.label}{row.value}
- - {/* Grade Scale Table */} - - - - - - - - - {syllabus.grading.map((row, idx) => ( - - - - - ))} - -
GradeScale
{row.grade}{row.range}
-
- - {/* AI Summary / Placeholder */} -
-

{syllabus.summary}

-
-
-
-
+ {syllabus_uri && ( + + )}
); diff --git a/src/components/common/SyllabusSummary/SyllabusSummary.tsx b/src/components/common/SyllabusSummary/SyllabusSummary.tsx new file mode 100644 index 00000000..f0e47fc6 --- /dev/null +++ b/src/components/common/SyllabusSummary/SyllabusSummary.tsx @@ -0,0 +1,152 @@ +'use client'; + +import { searchQueryEqual, type SearchQuery } from '@/types/SearchQuery'; +import { Collapse, Link, Skeleton, Typography } from '@mui/material'; +import React, { useEffect, useRef, useState } from 'react'; + +export function LoadingSyllabusSummary() { + return ( + <> + + + + + + AI REVIEW SUMMARY + + + ); +} + +type SyllabusData = { + grade_weights: { category: string; percentage: string }[]; + letter_grade_scale: { grade: string; range: string }[]; + summary: string; +}; + +type Props = { + open: boolean; + searchQuery: SearchQuery; + syllabus_uri: string; + showSyllabus: boolean; +}; + +export default function SyllabusSummary({ + open, + searchQuery, + syllabus_uri, + showSyllabus, +}: Props) { + const searchQueryRef = useRef(searchQuery); + const [state, setState] = useState<'closed' | 'loading' | 'error' | 'done'>( + 'closed', + ); + const [syllabus, setSyllabus] = useState(null); + + useEffect(() => { + if (!searchQueryEqual(searchQueryRef.current, searchQuery)) { + searchQueryRef.current = searchQuery; + setState('closed'); + setSyllabus(null); + } + if (open && state === 'closed') { + setState('loading'); + const params = new URLSearchParams(); + if (syllabus_uri) params.append('syllabus_uri', syllabus_uri); + fetch(`/api/syllabusSummary?${params.toString()}`, { + method: 'GET', + next: { revalidate: 3600 }, + }) + .then((res) => res.json()) + .then((data) => { + if (data.message !== 'success') { + setState('error'); + return; + } + setState('done'); + setSyllabus(data.data); + }); + } + }, [open, state, searchQuery]); + + if (state === 'error') { + return

Problem loading AI review summary.

; + } + + if (!syllabus) { + return ; + } + + return ( + <> + +
+

Syllabus Grading Summary

+
+ + {/* Outer flex row: tables + AI summary */} +
+ {/* Tables wrapper */} +
+ {/* Weighting Table */} + + + + + + + + + {syllabus.grade_weights.map((row, idx) => ( + + + + + ))} + +
+ Weighting + %
{row.category}{row.percentage}
+ + {/* Grade Scale Table */} + + + + + + + + + {syllabus.letter_grade_scale.map((row, idx) => ( + + + + + ))} + +
GradeScale
{row.grade}{row.range}
+
+ + {/* AI Summary / Placeholder */} +
+

{syllabus.summary}

+ + View Syllabus + +
+
+
+
+ + ); +} diff --git a/src/components/overview/ProfessorOverview/ProfessorOverview.tsx b/src/components/overview/ProfessorOverview/ProfessorOverview.tsx index 8d4cde14..8621b010 100644 --- a/src/components/overview/ProfessorOverview/ProfessorOverview.tsx +++ b/src/components/overview/ProfessorOverview/ProfessorOverview.tsx @@ -125,31 +125,7 @@ export default function ProfessorOverview({ grades={grades} filteredGrades={calculateGrades(grades)} /> - {rmp && ( - - )} + {rmp && } ); } diff --git a/src/components/planner/PlannerCoursesTable/PlannerCard.tsx b/src/components/planner/PlannerCoursesTable/PlannerCard.tsx index bb610a91..01f22403 100644 --- a/src/components/planner/PlannerCoursesTable/PlannerCard.tsx +++ b/src/components/planner/PlannerCoursesTable/PlannerCard.tsx @@ -833,26 +833,20 @@ export default function PlannerCard(props: PlannerCardProps) { latestMatchedSections.RMP && ( + !!s.syllabus_uri && !!s.academic_session?.start_date, + ) + .sort( + (a, b) => + new Date(b.academic_session.start_date).getTime() - + new Date(a.academic_session.start_date).getTime(), + )[0]?.syllabus_uri || null + } /> )} diff --git a/src/components/search/SearchResultsTable/SearchResultsTable.tsx b/src/components/search/SearchResultsTable/SearchResultsTable.tsx index 939e6c88..4d2628aa 100644 --- a/src/components/search/SearchResultsTable/SearchResultsTable.tsx +++ b/src/components/search/SearchResultsTable/SearchResultsTable.tsx @@ -377,27 +377,21 @@ function Row({ /> {searchResult.type !== 'course' && searchResult.RMP && ( + !!s.syllabus_uri && !!s.academic_session?.start_date, + ) + .sort( + (a, b) => + new Date(b.academic_session.start_date).getTime() - + new Date(a.academic_session.start_date).getTime(), + )[0]?.syllabus_uri || null + } /> )} From d23b1c41a1161924b09b37d8bf200d10cb6b9f48 Mon Sep 17 00:00:00 2001 From: barkat-10 Date: Sat, 10 Jan 2026 13:01:17 -0600 Subject: [PATCH 27/31] Prompt changed. Grade scale not available message added. --- src/app/api/syllabusSummary/route.ts | 20 +++++++++- .../SyllabusSummary/SyllabusSummary.tsx | 37 +++++++++++-------- 2 files changed, 41 insertions(+), 16 deletions(-) diff --git a/src/app/api/syllabusSummary/route.ts b/src/app/api/syllabusSummary/route.ts index 0d5f8aca..04fac40a 100644 --- a/src/app/api/syllabusSummary/route.ts +++ b/src/app/api/syllabusSummary/route.ts @@ -149,7 +149,25 @@ export async function GET(request: Request) { }, }, { - text: 'Extract the grading weights and grade scale exactly as shown in the tables. Provide a concise summary of the course structure & professor style for students.', + // text: 'Extract the grading weights and grade scale exactly as shown in the tables. Provide a concise summary of the course structure & professor style for students.', + text: `Extract the grading weights and grade scale exactly as shown in the tables. + + Then write a concise, student-focused summary that helps a student decide + whether this professor is a good fit for them. + + Focus on: + - workload intensity (exam-heavy vs assignment-heavy) + - number and type of exams(comprehensive or not, mcqs or subjective etc) + - group vs individual work + - strictness on deadlines and academic policies + - teaching style (theoretical vs practical, lecture-heavy vs interactive) + - overall stress level for an average student + + Avoid course topic descriptions unless they affect workload or difficulty. + Be direct and practical. + + ` + }, ], }, diff --git a/src/components/common/SyllabusSummary/SyllabusSummary.tsx b/src/components/common/SyllabusSummary/SyllabusSummary.tsx index f0e47fc6..1b95b0db 100644 --- a/src/components/common/SyllabusSummary/SyllabusSummary.tsx +++ b/src/components/common/SyllabusSummary/SyllabusSummary.tsx @@ -112,22 +112,29 @@ export default function SyllabusSummary({ {/* Grade Scale Table */} - - - - - - - - - {syllabus.letter_grade_scale.map((row, idx) => ( - - - + {syllabus.letter_grade_scale != null && + syllabus.letter_grade_scale.length > 0 ? ( +
GradeScale
{row.grade}{row.range}
+ + + + - ))} - -
GradeScale
+ + + {syllabus.letter_grade_scale.map((row, idx) => ( + + {row.grade} + {row.range} + + ))} + + + ) : ( +
+

Grade scale not available

+
+ )} {/* AI Summary / Placeholder */} From ea250a425de27c33f7ba1bc6931f195710e69669 Mon Sep 17 00:00:00 2001 From: AbhiramTadepalli Date: Sat, 10 Jan 2026 22:37:20 -0600 Subject: [PATCH 28/31] just remove non-existant fields & lint --- src/app/api/syllabusSummary/route.ts | 7 +- .../SyllabusSummary/SyllabusSummary.tsx | 87 ++++++++++--------- 2 files changed, 50 insertions(+), 44 deletions(-) diff --git a/src/app/api/syllabusSummary/route.ts b/src/app/api/syllabusSummary/route.ts index 04fac40a..e65877ab 100644 --- a/src/app/api/syllabusSummary/route.ts +++ b/src/app/api/syllabusSummary/route.ts @@ -149,8 +149,8 @@ export async function GET(request: Request) { }, }, { - // text: 'Extract the grading weights and grade scale exactly as shown in the tables. Provide a concise summary of the course structure & professor style for students.', - text: `Extract the grading weights and grade scale exactly as shown in the tables. + // text: 'Extract the grading weights and grade scale exactly as shown in the tables. Provide a concise summary of the course structure & professor style for students.', + text: `Extract the grading weights and grade scale exactly as shown in the tables. Then write a concise, student-focused summary that helps a student decide whether this professor is a good fit for them. @@ -166,8 +166,7 @@ export async function GET(request: Request) { Avoid course topic descriptions unless they affect workload or difficulty. Be direct and practical. - ` - + `, }, ], }, diff --git a/src/components/common/SyllabusSummary/SyllabusSummary.tsx b/src/components/common/SyllabusSummary/SyllabusSummary.tsx index 1b95b0db..51428897 100644 --- a/src/components/common/SyllabusSummary/SyllabusSummary.tsx +++ b/src/components/common/SyllabusSummary/SyllabusSummary.tsx @@ -92,49 +92,52 @@ export default function SyllabusSummary({ {/* Tables wrapper */}
{/* Weighting Table */} - - - - - - - - - {syllabus.grade_weights.map((row, idx) => ( - - - - - ))} - -
- Weighting - %
{row.category}{row.percentage}
+ {syllabus.grade_weights != null && + syllabus.grade_weights.length > 0 && ( + + + + + + + + + {syllabus.grade_weights.map((row, idx) => ( + + + + + ))} + +
+ Weighting + %
{row.category}{row.percentage}
+ )} {/* Grade Scale Table */} {syllabus.letter_grade_scale != null && - syllabus.letter_grade_scale.length > 0 ? ( - - - - - - - - - {syllabus.letter_grade_scale.map((row, idx) => ( - - - + syllabus.letter_grade_scale.length > 0 && ( +
GradeScale
{row.grade}{row.range}
+ + + + - ))} - -
+ Grade + + Scale +
- ) : ( -
-

Grade scale not available

-
- )} + + + {syllabus.letter_grade_scale.map((row, idx) => ( + + {row.grade} + {row.range} + + ))} + + + )}
{/* AI Summary / Placeholder */} @@ -142,7 +145,11 @@ export default function SyllabusSummary({ id="ai-summary" className="text-sm flex flex-col items-center flex-1 min-h-[100px]" > -

{syllabus.summary}

+ {syllabus.summary != null ? ( +

{syllabus.summary}

+ ) : ( +

Could not summarize the syllabus

+ )} Date: Sat, 10 Jan 2026 22:47:56 -0600 Subject: [PATCH 29/31] loading state ui should be inside the view syllabus summary --- .../SyllabusSummary/SyllabusSummary.tsx | 146 +++++++++--------- 1 file changed, 73 insertions(+), 73 deletions(-) diff --git a/src/components/common/SyllabusSummary/SyllabusSummary.tsx b/src/components/common/SyllabusSummary/SyllabusSummary.tsx index 51428897..c30c103d 100644 --- a/src/components/common/SyllabusSummary/SyllabusSummary.tsx +++ b/src/components/common/SyllabusSummary/SyllabusSummary.tsx @@ -76,90 +76,90 @@ export default function SyllabusSummary({ return

Problem loading AI review summary.

; } - if (!syllabus) { - return ; - } - return ( <> -
-

Syllabus Grading Summary

-
+ {!syllabus ? ( + + ) : ( +
+

Syllabus Grading Summary

+
- {/* Outer flex row: tables + AI summary */} -
- {/* Tables wrapper */} -
- {/* Weighting Table */} - {syllabus.grade_weights != null && - syllabus.grade_weights.length > 0 && ( - - - - - - - - - {syllabus.grade_weights.map((row, idx) => ( - - - + {/* Outer flex row: tables + AI summary */} +
+ {/* Tables wrapper */} +
+ {/* Weighting Table */} + {syllabus.grade_weights != null && + syllabus.grade_weights.length > 0 && ( +
- Weighting - %
{row.category}{row.percentage}
+ + + + - ))} - -
+ Weighting + %
- )} + + + {syllabus.grade_weights.map((row, idx) => ( + + {row.category} + {row.percentage} + + ))} + + + )} - {/* Grade Scale Table */} - {syllabus.letter_grade_scale != null && - syllabus.letter_grade_scale.length > 0 && ( - - - - - - - - - {syllabus.letter_grade_scale.map((row, idx) => ( - - - + {/* Grade Scale Table */} + {syllabus.letter_grade_scale != null && + syllabus.letter_grade_scale.length > 0 && ( +
- Grade - - Scale -
{row.grade}{row.range}
+ + + + - ))} - -
+ Grade + + Scale +
- )} -
+ + + {syllabus.letter_grade_scale.map((row, idx) => ( + + {row.grade} + {row.range} + + ))} + + + )} +
- {/* AI Summary / Placeholder */} -
- {syllabus.summary != null ? ( -

{syllabus.summary}

- ) : ( -

Could not summarize the syllabus

- )} - - View Syllabus - + {syllabus.summary != null ? ( +

{syllabus.summary}

+ ) : ( +

Could not summarize the syllabus

+ )} + + View Syllabus + +
- + )}
); From 1cb43132f59aea6988cb847a1ac8fb8b1d52f8ff Mon Sep 17 00:00:00 2001 From: barkat-10 Date: Sun, 11 Jan 2026 16:22:45 -0600 Subject: [PATCH 30/31] Prompt specifications changed. --- src/app/api/syllabusSummary/route.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/app/api/syllabusSummary/route.ts b/src/app/api/syllabusSummary/route.ts index e65877ab..282af57c 100644 --- a/src/app/api/syllabusSummary/route.ts +++ b/src/app/api/syllabusSummary/route.ts @@ -161,12 +161,13 @@ export async function GET(request: Request) { - group vs individual work - strictness on deadlines and academic policies - teaching style (theoretical vs practical, lecture-heavy vs interactive) - - overall stress level for an average student + Avoid course topic descriptions unless they affect workload or difficulty. Be direct and practical. - - `, + No need to write about academic honesty policies + Keep the word limit to around 30-40 words + `, }, ], }, From bc2e5fe10f4289658be184535a4d5239079b1d2f Mon Sep 17 00:00:00 2001 From: barkat-10 Date: Fri, 16 Jan 2026 16:08:44 -0600 Subject: [PATCH 31/31] Different versions of Prompts for testing and opinions --- src/app/api/syllabusSummary/route.ts | 77 ++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 4 deletions(-) diff --git a/src/app/api/syllabusSummary/route.ts b/src/app/api/syllabusSummary/route.ts index 282af57c..2a0eca49 100644 --- a/src/app/api/syllabusSummary/route.ts +++ b/src/app/api/syllabusSummary/route.ts @@ -149,10 +149,79 @@ export async function GET(request: Request) { }, }, { - // text: 'Extract the grading weights and grade scale exactly as shown in the tables. Provide a concise summary of the course structure & professor style for students.', - text: `Extract the grading weights and grade scale exactly as shown in the tables. + text: + // Original - 'Extract the grading weights and grade scale exactly as shown in the tables. Provide a concise summary of the course structure & professor style for students.', - Then write a concise, student-focused summary that helps a student decide + /* ` POLISHED VERSION THE ORIGINAL + Extract the grading weights and grade scale exactly as shown in the tables. + + Then write a concise, student-focused summary only in bullet points, and not a paragraph that helps a student decide + whether this professor is a good fit for them. + + Focus on: + - workload intensity (exam-heavy vs assignment-heavy) + - number and type of exams(comprehensive or not, mcqs or subjective etc) + - group vs individual work + - strictness on deadlines and academic policies + - teaching style (theoretical vs practical, lecture-heavy vs interactive) + + + Avoid course topic descriptions unless they affect workload or difficulty. + Be direct and practical. + No need to write about academic honesty policies + Keep the word limit to around 30-40 words + `, */ + + /* ` RATE MY PROFF STYLE + Extract the grading weights and grade scale exactly as shown in the tables. + + Then write a short, student-style summary that sounds like a realistic + RateMyProfessor review. + + Focus on: + - how heavy the workload feels + - how frequent the exams are, and how stressful they can be based on their weightage + - whether group work is common or avoidable + - how strict deadlines feel in practice + + + Use casual, plain language. + Avoid formal or academic wording. + Do not use bullets or headings. + No need to talk about Grading scale, until its not standard + Write 2-3 natural sentences (30-40 words). + `, */ + + + + /*` (SNAPSHOT VERSION - NEEDS TO BE PROPERLY FORMATTED!) + Extract the grading weights and grade scale exactly as shown in the tables. + Write a concise student-focused summary " Snapshot" using 4-5 labeled lines that helps a student decide + whether this professor is a good fit for them. + + Focus on: + - workload intensity (exam-heavy vs assignment-heavy) + - number and type of exams(comprehensive or not, mcqs or subjective etc) + - group vs individual work + - strictness on deadlines and academic policies + - teaching style (theoretical vs practical, lecture-heavy vs interactive) + + + Avoid course topic descriptions unless they affect workload or difficulty. + Be direct and practical. + No need to write about academic honesty policies or make up exam policies + Each line must follow the format: + Label: short description + Do NOT use paragraphs or bullet points. + + + `*/ + + + /* ` (BULLET POINTS - NEED TO BE FORMATTED PROPERLY) + Extract the grading weights and grade scale exactly as shown in the tables. + + Then write a concise, student-focused summary only in bullet points, and not a paragraph that helps a student decide whether this professor is a good fit for them. Focus on: @@ -167,7 +236,7 @@ export async function GET(request: Request) { Be direct and practical. No need to write about academic honesty policies Keep the word limit to around 30-40 words - `, + `, */ }, ], },