diff --git a/package-lock.json b/package-lock.json index 9eaacf4c..86b31b92 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", "@ianvs/prettier-plugin-sort-imports": "^4.7.0", "@mui/icons-material": "^7.0.2", "@mui/material": "^7.0.2", @@ -1858,6 +1859,183 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@google/genai": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.33.0.tgz", + "integrity": "sha512-ThUjFZ1N0DU88peFjnQkb8K198EWaW2RmmnDShFQ+O+xkIH9itjpRe358x3L/b4X/A7dimkvq63oz49Vbh7Cog==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.24.0" + }, + "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.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "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, @@ -1984,6 +2162,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, @@ -4018,6 +4286,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", @@ -6204,7 +6482,6 @@ }, "node_modules/ansi-regex": { "version": "5.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7444,7 +7721,6 @@ }, "node_modules/cross-spawn": { "version": "7.0.6", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -7554,7 +7830,6 @@ }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">= 12" @@ -7882,6 +8157,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", @@ -7900,7 +8181,6 @@ }, "node_modules/emoji-regex": { "version": "9.2.2", - "dev": true, "license": "MIT" }, "node_modules/encodeurl": { @@ -8951,7 +9231,6 @@ }, "node_modules/fetch-blob": { "version": "3.2.0", - "dev": true, "funding": [ { "type": "github", @@ -9286,7 +9565,6 @@ }, "node_modules/formdata-polyfill": { "version": "4.0.10", - "dev": true, "license": "MIT", "dependencies": { "fetch-blob": "^3.1.2" @@ -9678,6 +9956,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, @@ -10399,7 +10686,6 @@ }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -10800,6 +11086,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, @@ -11897,7 +12198,6 @@ }, "node_modules/node-domexception": { "version": "1.0.0", - "dev": true, "funding": [ { "type": "github", @@ -11915,7 +12215,6 @@ }, "node_modules/node-fetch": { "version": "3.3.2", - "dev": true, "license": "MIT", "dependencies": { "data-uri-to-buffer": "^4.0.0", @@ -12300,6 +12599,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, @@ -12413,7 +12718,6 @@ }, "node_modules/path-key": { "version": "3.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -14744,7 +15048,6 @@ }, "node_modules/shebang-command": { "version": "2.0.0", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -14755,7 +15058,6 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -14986,7 +15288,6 @@ }, "node_modules/string-width": { "version": "4.2.3", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -14997,9 +15298,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": { @@ -15118,7 +15439,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" @@ -16030,7 +16363,6 @@ }, "node_modules/web-streams-polyfill": { "version": "3.3.3", - "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -16365,6 +16697,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, @@ -16402,7 +16785,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 d0980d5a..92c30189 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", "@ianvs/prettier-plugin-sort-imports": "^4.7.0", "@mui/icons-material": "^7.0.2", "@mui/material": "^7.0.2", diff --git a/src/app/Home.tsx b/src/app/Home.tsx index 77a507be..7afb284d 100644 --- a/src/app/Home.tsx +++ b/src/app/Home.tsx @@ -62,19 +62,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/app/api/rmpSummary/route.ts b/src/app/api/rmpSummary/route.ts new file mode 100644 index 00000000..e5c780d9 --- /dev/null +++ b/src/app/api/rmpSummary/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 }, + ); +} diff --git a/src/app/api/syllabusSummary/route.ts b/src/app/api/syllabusSummary/route.ts new file mode 100644 index 00000000..2a0eca49 --- /dev/null +++ b/src/app/api/syllabusSummary/route.ts @@ -0,0 +1,265 @@ +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 structure & 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%' }, + }, + }, + }, + letter_grade_scale: { + type: 'ARRAY', + items: { + type: 'OBJECT', + properties: { + grade: { type: 'STRING', description: 'e.g., A, B' }, + range: { type: 'STRING', description: 'e.g., 90-100, 80-89.9' }, + }, + }, + }, + }, + required: ['summary', 'grade_weights', 'letter_grade_scale'], +}; + +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 syllabus_uri = searchParams.get('syllabus_uri'); + if (typeof syllabus_uri !== 'string') { + return NextResponse.json( + { message: 'error', data: 'Incorrect query parameters' }, + { status: 400 }, + ); + } + + // // 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); + + 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'); + + // AI + 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', + config: { + responseMimeType: 'application/json', + responseSchema: syllabusResponseSchema, + }, + contents: [ + { + role: 'user', + parts: [ + { + inlineData: { + mimeType: 'application/pdf', + data: pdfBase64, + }, + }, + { + 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.', + + /* ` 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: + - 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 + `, */ + }, + ], + }, + ], + }); + + // // 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: responseData }, + { status: 200 }, + ); +} diff --git a/src/components/common/RmpSummary/RmpSummary.tsx b/src/components/common/RmpSummary/RmpSummary.tsx new file mode 100644 index 00000000..22676e51 --- /dev/null +++ b/src/components/common/RmpSummary/RmpSummary.tsx @@ -0,0 +1,88 @@ +'use client'; + +import { searchQueryEqual, type SearchQuery } from '@/types/SearchQuery'; +import { Skeleton, Tooltip, Typography } from '@mui/material'; +import React, { useEffect, useRef, useState } from 'react'; + +export function LoadingRmpSummary() { + return ( + <> + + + + + + AI REVIEW SUMMARY + + + ); +} + +type Props = { + open: boolean; + searchQuery: SearchQuery; +}; + +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(); + 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]); + + 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 7a151139..10c244ed 100644 --- a/src/components/common/SingleProfInfo/SingleProfInfo.tsx +++ b/src/components/common/SingleProfInfo/SingleProfInfo.tsx @@ -1,6 +1,12 @@ 'use client'; +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'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import { Chip, Collapse, Grid, IconButton, Skeleton } from '@mui/material'; import Link from 'next/link'; @@ -57,6 +63,10 @@ export function LoadingSingleProfInfo() { + + + +

Visit Rate My Professors

@@ -67,11 +77,20 @@ export function LoadingSingleProfInfo() { } type Props = { + open: boolean; + searchQuery: SearchQuery; rmp: RMP; + syllabus_uri?: string | null; }; -export default function SingleProfInfo({ rmp }: Props) { +export default function SingleProfInfo({ + open, + searchQuery, + rmp, + syllabus_uri, +}: Props) { const [showMore, setShowMore] = useState(false); + const [showSyllabus, setShowSyllabus] = useState(false); if (rmp.numRatings == 0) { return ( @@ -164,13 +183,34 @@ export default function SingleProfInfo({ rmp }: Props) { )} - - Visit Rate My Professors - + + + + +
+ + Visit Rate My Professors + + + +
+ {syllabus_uri && ( + + )}
); diff --git a/src/components/common/SyllabusSummary/SyllabusSummary.tsx b/src/components/common/SyllabusSummary/SyllabusSummary.tsx new file mode 100644 index 00000000..c30c103d --- /dev/null +++ b/src/components/common/SyllabusSummary/SyllabusSummary.tsx @@ -0,0 +1,166 @@ +'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.

; + } + + return ( + <> + + {!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) => ( + + + + + ))} + +
+ 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) => ( + + + + + ))} + +
+ Grade + + Scale +
{row.grade}{row.range}
+ )} +
+ + {/* AI Summary / Placeholder */} +
+ {syllabus.summary != null ? ( +

{syllabus.summary}

+ ) : ( +

Could not summarize the syllabus

+ )} + + View Syllabus + +
+
+
+ )} +
+ + ); +} diff --git a/src/components/overview/ProfessorOverview/ProfessorOverview.tsx b/src/components/overview/ProfessorOverview/ProfessorOverview.tsx index ddb40ce1..8621b010 100644 --- a/src/components/overview/ProfessorOverview/ProfessorOverview.tsx +++ b/src/components/overview/ProfessorOverview/ProfessorOverview.tsx @@ -125,7 +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 7012779d..01f22403 100644 --- a/src/components/planner/PlannerCoursesTable/PlannerCard.tsx +++ b/src/components/planner/PlannerCoursesTable/PlannerCard.tsx @@ -831,7 +831,23 @@ export default function PlannerCard(props: PlannerCardProps) { {(latestMatchedSections.type === 'professor' || latestMatchedSections.type === 'combo') && 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/SearchBar/SearchBar.tsx b/src/components/search/SearchBar/SearchBar.tsx index fa655492..77b50044 100644 --- a/src/components/search/SearchBar/SearchBar.tsx +++ b/src/components/search/SearchBar/SearchBar.tsx @@ -164,18 +164,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 @@ -208,7 +211,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; diff --git a/src/components/search/SearchResultsTable/SearchResultsTable.tsx b/src/components/search/SearchResultsTable/SearchResultsTable.tsx index c67f8cc7..4d2628aa 100644 --- a/src/components/search/SearchResultsTable/SearchResultsTable.tsx +++ b/src/components/search/SearchResultsTable/SearchResultsTable.tsx @@ -376,7 +376,23 @@ function Row({ filteredGrades={filteredGrades} /> {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 + } + /> )} diff --git a/src/modules/fetchRmp.ts b/src/modules/fetchRmp.ts index 2744a734..961a4e9c 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,6 +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 +83,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 +119,9 @@ export interface RMP { r5: number; total: number; }; + ratings?: { + edges: { node: { comment: string } }[]; + }; } type TeacherSearchResponse = { @@ -134,6 +149,7 @@ function checkProfData( export default async function fetchRmp( query: SearchQuery, + reviews: boolean = false, ): Promise { if ( typeof query.profFirst !== 'string' || @@ -154,6 +170,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