From 0f8536cbb754d0ed3b2126741f01fb414d2ea1e3 Mon Sep 17 00:00:00 2001 From: khaihtruong Date: Mon, 3 Nov 2025 10:30:14 -0500 Subject: [PATCH 1/5] leaderboard redesign --- client/package-lock.json | 537 ++++++++++++++++++ client/package.json | 3 + client/src/components/Leaderboard.tsx | 765 +++++++++++++------------- client/src/components/ui/checkbox.tsx | 32 ++ client/src/components/ui/input.tsx | 21 + client/src/components/ui/select.tsx | 189 +++++++ client/src/components/ui/tabs.tsx | 5 +- client/src/components/ui/tooltip.tsx | 61 ++ 8 files changed, 1239 insertions(+), 374 deletions(-) create mode 100644 client/src/components/ui/checkbox.tsx create mode 100644 client/src/components/ui/input.tsx create mode 100644 client/src/components/ui/select.tsx create mode 100644 client/src/components/ui/tooltip.tsx diff --git a/client/package-lock.json b/client/package-lock.json index 5e0e074..5e42134 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -9,8 +9,11 @@ "version": "0.0.0", "dependencies": { "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-table": "^8.21.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -962,6 +965,44 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.4" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1053,12 +1094,41 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, "node_modules/@radix-ui/primitive": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", "license": "MIT" }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-avatar": { "version": "1.1.10", "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", @@ -1086,6 +1156,36 @@ } } }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-collection": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", @@ -1157,6 +1257,73 @@ } } }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-id": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", @@ -1175,6 +1342,62 @@ } } }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-presence": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", @@ -1253,6 +1476,49 @@ } } }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", @@ -1301,6 +1567,40 @@ } } }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", @@ -1353,6 +1653,24 @@ } } }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-is-hydrated": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", @@ -1386,6 +1704,86 @@ } } }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, "node_modules/@reduxjs/toolkit": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.0.tgz", @@ -2233,6 +2631,18 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2632,6 +3042,12 @@ "node": ">=8" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.214", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.214.tgz", @@ -3033,6 +3449,15 @@ "node": ">=6.9.0" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -3855,6 +4280,53 @@ "node": ">=0.10.0" } }, + "node_modules/react-remove-scroll": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", + "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-router": { "version": "7.8.2", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.8.2.tgz", @@ -3893,6 +4365,28 @@ "react-dom": ">=18" } }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/recharts": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.2.1.tgz", @@ -4268,6 +4762,27 @@ "punycode": "^2.1.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/use-debounce": { "version": "10.0.6", "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.0.6.tgz", @@ -4280,6 +4795,28 @@ "react": "*" } }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/use-sync-external-store": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", diff --git a/client/package.json b/client/package.json index a0aec84..4c04d42 100644 --- a/client/package.json +++ b/client/package.json @@ -11,8 +11,11 @@ }, "dependencies": { "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-table": "^8.21.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/client/src/components/Leaderboard.tsx b/client/src/components/Leaderboard.tsx index 7d0784f..2b64cb2 100644 --- a/client/src/components/Leaderboard.tsx +++ b/client/src/components/Leaderboard.tsx @@ -1,29 +1,29 @@ -import "../styles/Leaderboard.css"; -import { useState, useMemo, useRef, useEffect, MouseEvent, KeyboardEvent } from "react"; +import { useState, useMemo } from "react"; import type { ChangeEvent } from "react"; import { useReactTable, getCoreRowModel, + getSortedRowModel, getFilteredRowModel, flexRender, ColumnDef, SortingState, - ColumnFiltersState, - Row, } from "@tanstack/react-table"; +import { ChevronDown, ChevronRight, Search, Info } from "lucide-react"; import { - getLatestVersions, modelVersions, filterVersions, systemPrompts, messagePrompts, modelFamilies } from "../data/leaderboardData"; - -interface InfoBubbleProps { - title: string; - content: string; -} +import { Input } from "./ui/input"; +import { Checkbox } from "./ui/checkbox"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"; +import { Button } from "./ui/button"; +import { Card } from "./ui/card"; +import { Badge } from "./ui/badge"; +import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; interface ModelVersion { id: string; @@ -79,55 +79,9 @@ interface FilterParams { modelFamilies?: string[]; } -function InfoBubble({ title, content }: InfoBubbleProps) { - const [open, setOpen] = useState(false); - const wrapRef = useRef(null); - - useEffect(() => { - function handleDocClick(e: Event): void { - if (wrapRef.current && !wrapRef.current.contains(e.target as Node)) { - setOpen(false); - } - } - document.addEventListener("click", handleDocClick); - return () => document.removeEventListener("click", handleDocClick); - }, []); - - return ( - - { - e.stopPropagation(); - setOpen((v) => !v); - }} - onKeyDown={(e: KeyboardEvent) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - setOpen((v) => !v); - } - }} - > - ! - - {open && ( -
-
{title}
-
{content}
-
- )} -
- ); -} - export default function Leaderboard() { const [activeTab, setActiveTab] = useState("models"); const [sorting, setSorting] = useState([]); - const [columnFilters, setColumnFilters] = useState([]); const [globalFilter, setGlobalFilter] = useState(""); const [expandedModels, setExpandedModels] = useState>(() => new Set()); const [selectedVersions, setSelectedVersions] = useState>(() => new Set()); @@ -194,29 +148,13 @@ export default function Leaderboard() { SIRI_2: latest.SIRI_2, A_pharm: latest.A_pharm, A_mamh: latest.A_mamh, - hasVersions: data.versions.length >= 1, + hasVersions: data.versions.length > 1, versions: data.versions }; }); - if (sorting.length > 0) { - const { id: sortColumn, desc } = sorting[0]; - mainRows.sort((a, b) => { - const aVal = a[sortColumn as keyof MainRow]; - const bVal = b[sortColumn as keyof MainRow]; - - if (typeof aVal === "number" && typeof bVal === "number") { - return desc ? bVal - aVal : aVal - bVal; - } - - const aStr = String(aVal || "").toLowerCase(); - const bStr = String(bVal || "").toLowerCase(); - return desc ? bStr.localeCompare(aStr) : aStr.localeCompare(bStr); - }); - } - return mainRows; - }, [temperatureFilter, topPFilter, systemPromptFilter, messagePromptFilter, modelFamilyFilter, sorting]); + }, [temperatureFilter, topPFilter, systemPromptFilter, messagePromptFilter, modelFamilyFilter]); const data = useMemo(() => { const rows: TableRow[] = []; @@ -263,18 +201,16 @@ export default function Leaderboard() { { id: "expander", header: () => "", - size: 30, + size: 40, enableSorting: false, - enableColumnFilter: false, cell: ({ row }) => { if (!row.original.isMainRow) { const isSelected = selectedVersions.has(row.original.id); return ( -
- + toggleVersion(row.original.id)} + onCheckedChange={() => toggleVersion(row.original.id)} onClick={(e) => e.stopPropagation()} />
@@ -286,16 +222,20 @@ export default function Leaderboard() { const isOpen = expandedModels.has(row.original.id); return ( ); }, @@ -303,28 +243,34 @@ export default function Leaderboard() { { accessorKey: "modelFamily", header: () => "Model Family", - cell: (info) => info.getValue() + cell: (info) => {String(info.getValue())} }, { accessorKey: "model", header: () => "Model", - cell: (info) => info.getValue() + cell: (info) => String(info.getValue()) }, { accessorKey: "version", header: () => "Version", - cell: (info) => info.getValue(), - size: 100 + cell: (info) => String(info.getValue()), + size: 120 }, { accessorKey: "SIRI_2", header: () => ( -
+
SIRI-2 - + + + + + +

RMSE — lower is better. Error across SIRI-2 benchmark

+
+
), cell: ({ getValue }) => { @@ -335,12 +281,18 @@ export default function Leaderboard() { { accessorKey: "A_pharm", header: () => ( -
+
A-Pharm - + + + + + +

RMSE — lower is better. Pharmacology subset performance

+
+
), cell: ({ getValue }) => { @@ -351,12 +303,18 @@ export default function Leaderboard() { { accessorKey: "A_mamh", header: () => ( -
+
A-MaMH - + + + + + +

RMSE — lower is better. Math & reasoning subset performance

+
+
), cell: ({ getValue }) => { @@ -371,14 +329,12 @@ export default function Leaderboard() { const table = useReactTable({ data, columns, - state: { sorting, columnFilters, globalFilter }, + state: { sorting, globalFilter }, onSortingChange: setSorting, - onColumnFiltersChange: setColumnFilters, onGlobalFilterChange: setGlobalFilter, getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel(), - enableSorting: true, - manualSorting: true, globalFilterFn: (row, _colId, filterValue) => { if (!filterValue) return true; const q = String(filterValue).toLowerCase(); @@ -395,287 +351,356 @@ export default function Leaderboard() { ); return ( -
-
- - +
+ {/* Header Section */} +
+
+

Model Leaderboard

+

+ Compare AI model performance across mental health benchmarks +

+
- {activeTab === "models" && ( -
- - -
-
- - - {table.getHeaderGroups().map((hg) => ( - - {hg.headers.map((header) => { - const canSort = header.column.getCanSort(); - const sortDir = header.column.getIsSorted(); - return ( -
- {flexRender(header.column.columnDef.header, header.getContext())} - {canSort && sortDir && ( - - {sortDir === "asc" ? "▲" : "▼"} - - )} + + + )} + + {activeTab === 'comparison' && ( +
+
+ + {selectedVersions.size === 0 ? ( +
+

No models selected for comparison

+

Select models from the Models tab to compare them

+
+ ) : ( +
+
+

Version Comparison

+ +
+
+ + + + + + + + - ); - })} - - ))} - - - - {table.getRowModel().rows.map((row) => { - const isMainRow = row.original.isMainRow; - const isVersionRow = !isMainRow; - const isSelected = isVersionRow && selectedVersions.has(row.original.id); - - return ( - toggleVersion(row.original.id) : undefined} - > - {row.getVisibleCells().map((cell) => ( - + + + + + + {comparisonRows.map((v) => ( + + + + + + + + + ))} - - ); - })} - -
KeepModel FamilyModelVersion +
+ SIRI-2 + + + + + +

RMSE — lower is better. Error across SIRI-2 benchmark

+
+
+
- {flexRender(cell.column.columnDef.cell, cell.getContext())} - +
+ A-Pharm + + + + + +

RMSE — lower is better. Pharmacology subset performance

+
+
+
+
+
+ A-MaMH + + + + + +

RMSE — lower is better. Math & reasoning subset performance

+
+
+
+
+ toggleVersion(v.id)} + /> + {v.modelFamily}{v.model}{v.version}{typeof v.SIRI_2 === "number" ? v.SIRI_2.toFixed(3) : v.SIRI_2}{typeof v.A_pharm === "number" ? v.A_pharm.toFixed(3) : v.A_pharm}{typeof v.A_mamh === "number" ? v.A_mamh.toFixed(3) : v.A_mamh}
- - {table.getRowModel().rows.length === 0 && ( -
No results match the current filters.
+ +
+
+
)} -
+
-
- )} - - {activeTab === "versions" && ( -
-
-
-

Version Comparison

- {selectedVersions.size > 0 && ( - - )} -
- - {comparisonRows.length ? ( -
- - - - - - - - - - - - - - {comparisonRows.map((v) => ( - - - - - - - - - - ))} - -
KeepModel FamilyModelVersion -
- SIRI-2 - -
-
-
- A-Pharm - -
-
-
- A-MaMH - -
-
- toggleVersion(v.id)} - /> - {v.modelFamily}{v.model}{v.version}{typeof v.SIRI_2 === "number" ? v.SIRI_2.toFixed(3) : v.SIRI_2}{typeof v.A_pharm === "number" ? v.A_pharm.toFixed(3) : v.A_pharm}{typeof v.A_mamh === "number" ? v.A_mamh.toFixed(3) : v.A_mamh}
-
- ) : ( -
No versions selected. Select versions from the Models tab to compare.
- )}
-
- )} + )} +
); } diff --git a/client/src/components/ui/checkbox.tsx b/client/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..4aea072 --- /dev/null +++ b/client/src/components/ui/checkbox.tsx @@ -0,0 +1,32 @@ +"use client"; + +import * as React from "react"; +import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; +import { Check } from "lucide-react"; + +import { cn } from "../../lib/utils"; + +function Checkbox({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + + ); +} + +export { Checkbox }; diff --git a/client/src/components/ui/input.tsx b/client/src/components/ui/input.tsx new file mode 100644 index 0000000..44ce3fc --- /dev/null +++ b/client/src/components/ui/input.tsx @@ -0,0 +1,21 @@ +import * as React from "react"; + +import { cn } from "../../lib/utils"; + +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + + ); +} + +export { Input }; diff --git a/client/src/components/ui/select.tsx b/client/src/components/ui/select.tsx new file mode 100644 index 0000000..c6081bf --- /dev/null +++ b/client/src/components/ui/select.tsx @@ -0,0 +1,189 @@ +"use client"; + +import * as React from "react"; +import * as SelectPrimitive from "@radix-ui/react-select"; +import { + Check, + ChevronDown, + ChevronUp, +} from "lucide-react"; + +import { cn } from "../../lib/utils"; + +function Select({ + ...props +}: React.ComponentProps) { + return ; +} + +function SelectGroup({ + ...props +}: React.ComponentProps) { + return ; +} + +function SelectValue({ + ...props +}: React.ComponentProps) { + return ; +} + +function SelectTrigger({ + className, + size = "default", + children, + ...props +}: React.ComponentProps & { + size?: "sm" | "default"; +}) { + return ( + + {children} + + + + + ); +} + +function SelectContent({ + className, + children, + position = "popper", + ...props +}: React.ComponentProps) { + return ( + + + + + {children} + + + + + ); +} + +function SelectLabel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function SelectItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} + +function SelectSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function SelectScrollUpButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +function SelectScrollDownButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +export { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, +}; diff --git a/client/src/components/ui/tabs.tsx b/client/src/components/ui/tabs.tsx index 144a850..174ccfe 100644 --- a/client/src/components/ui/tabs.tsx +++ b/client/src/components/ui/tabs.tsx @@ -23,10 +23,7 @@ function TabsList({ return ( ); diff --git a/client/src/components/ui/tooltip.tsx b/client/src/components/ui/tooltip.tsx new file mode 100644 index 0000000..f4ac3d4 --- /dev/null +++ b/client/src/components/ui/tooltip.tsx @@ -0,0 +1,61 @@ +"use client"; + +import * as React from "react"; +import * as TooltipPrimitive from "@radix-ui/react-tooltip"; + +import { cn } from "../../lib/utils"; + +function TooltipProvider({ + delayDuration = 0, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function Tooltip({ + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +function TooltipTrigger({ + ...props +}: React.ComponentProps) { + return ; +} + +function TooltipContent({ + className, + sideOffset = 0, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + + ); +} + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; From 2fcd20ebf2715d7fd53924f073589ff001e4129f Mon Sep 17 00:00:00 2001 From: khaihtruong Date: Mon, 3 Nov 2025 11:17:50 -0500 Subject: [PATCH 2/5] alignment fix --- client/src/components/Leaderboard.tsx | 63 +++++++++++++++------------ 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/client/src/components/Leaderboard.tsx b/client/src/components/Leaderboard.tsx index 2b64cb2..5bbe403 100644 --- a/client/src/components/Leaderboard.tsx +++ b/client/src/components/Leaderboard.tsx @@ -242,24 +242,30 @@ export default function Leaderboard() { }, { accessorKey: "modelFamily", - header: () => "Model Family", - cell: (info) => {String(info.getValue())} + header: () =>
Model Family
, + cell: ({ row, getValue }) => { + if (!row.original.isMainRow) return null; + return {String(getValue())}; + } }, { accessorKey: "model", - header: () => "Model", - cell: (info) => String(info.getValue()) + header: () =>
Model
, + cell: ({ row, getValue }) => { + if (!row.original.isMainRow) return null; + return String(getValue()); + } }, { accessorKey: "version", - header: () => "Version", + header: () =>
Version
, cell: (info) => String(info.getValue()), size: 120 }, { accessorKey: "SIRI_2", header: () => ( -
+
SIRI-2 @@ -281,7 +287,7 @@ export default function Leaderboard() { { accessorKey: "A_pharm", header: () => ( -
+
A-Pharm @@ -303,7 +309,7 @@ export default function Leaderboard() { { accessorKey: "A_mamh", header: () => ( -
+
A-MaMH @@ -529,30 +535,26 @@ export default function Leaderboard() { {hg.headers.map((header) => { const canSort = header.column.getCanSort(); const sortDir = header.column.getIsSorted(); + const isFirstColumn = header.id === 'expander'; + const alignment = isFirstColumn ? 'text-left' : 'text-center'; + return ( -
- {flexRender(header.column.columnDef.header, header.getContext())} - {canSort && sortDir && ( - - {sortDir === "asc" ? "▲" : "▼"} - - )} -
+ {flexRender(header.column.columnDef.header, header.getContext())} + {canSort && sortDir && ( + + {sortDir === "asc" ? "▲" : "▼"} + + )} ); })} @@ -579,11 +581,16 @@ export default function Leaderboard() { }} onClick={isVersionRow ? () => toggleVersion(row.original.id) : undefined} > - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} + {row.getVisibleCells().map((cell) => { + const isFirstColumn = cell.column.id === 'expander'; + const alignment = isFirstColumn ? 'text-left' : 'text-center'; + + return ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ); + })} ); })} From a3ca5e86854e162d8a0bf98b3f01f57f29ea22a8 Mon Sep 17 00:00:00 2001 From: khaihtruong Date: Tue, 4 Nov 2025 10:35:11 -0500 Subject: [PATCH 3/5] change tab naming --- client/src/components/Community.tsx | 14 ++-- client/src/components/Leaderboard.tsx | 53 +++++++++++---- client/src/components/Resources.tsx | 94 +++++++++++++++++++++++---- client/src/styles/Resources.css | 2 +- 4 files changed, 134 insertions(+), 29 deletions(-) diff --git a/client/src/components/Community.tsx b/client/src/components/Community.tsx index 1107f7a..beca3b2 100644 --- a/client/src/components/Community.tsx +++ b/client/src/components/Community.tsx @@ -10,18 +10,20 @@ export default function Community() { const [activeTab, setActiveTab] = useState('news'); return ( -
-
-
-

+
+
+
+

Community Hub

-

+

Stay updated with the latest news, share suggestions, and meet the team

+
- {/* Simple tabs navigation */} + {/* Simple tabs navigation */} +
-
+
+ +
+
+ + {activeTab === 'studies' && ( +
{/* Content */}
{/* Filters */} @@ -346,8 +402,24 @@ export default function Resources() { )}
)} +
+
+ )} + + {activeTab === 'articles' && ( +
+
+
+

Articles coming soon

+

We're curating a collection of articles about mental health AI benchmarks

+
+
+
+ )} +
- {/* Footer CTA */} + {/* Footer CTA */} +

Ready to Compare Models?

diff --git a/client/src/styles/Resources.css b/client/src/styles/Resources.css index c96bf4b..4a8604d 100644 --- a/client/src/styles/Resources.css +++ b/client/src/styles/Resources.css @@ -42,7 +42,7 @@ /* Content */ .resources-content { - padding: 48px 24px; + padding: 24px 24px; } /* Filters */ From da18d3c60d2ad3c6a0aed76dcf03f7f4dc4920ab Mon Sep 17 00:00:00 2001 From: khaihtruong Date: Tue, 4 Nov 2025 16:33:53 -0500 Subject: [PATCH 4/5] added api for articles tab --- client/src/assets/MindBenchAI.png | Bin 0 -> 56212 bytes client/src/components/HomePage.tsx | 12 +- client/src/components/Resources.tsx | 238 +++++++++++++++++- client/src/config/api.ts | 1 + client/src/styles/HomePage.css | 27 +- client/src/styles/NavBar.css | 11 +- client/src/styles/Resources.css | 16 ++ .../User/Resource.controller.ts | 172 +++++++++++++ server/src/routes/CurrentVersion/index.ts | 6 + 9 files changed, 465 insertions(+), 18 deletions(-) create mode 100644 client/src/assets/MindBenchAI.png diff --git a/client/src/assets/MindBenchAI.png b/client/src/assets/MindBenchAI.png new file mode 100644 index 0000000000000000000000000000000000000000..22d00505d0054de8761d20aef6c4d07f2ac2c49d GIT binary patch literal 56212 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

A$;WGRBUb#a{Ia4jOJ-a(EfV6{(#3w|!cOC(>T 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

+
+ MindBench AI Logo +
MindBench AI
+

Benchmark Leading Language Models

@@ -150,7 +155,12 @@ export default function HomePage() {

Start comparing language models and find the perfect fit for your needs.

- +
); diff --git a/client/src/components/Resources.tsx b/client/src/components/Resources.tsx index fe36088..474b491 100644 --- a/client/src/components/Resources.tsx +++ b/client/src/components/Resources.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, ChangeEvent, useMemo, useCallback } from "react"; -import { Github, FileText, Globe, Link } from "lucide-react"; +import { Github, FileText, Globe, Link, ExternalLink } from "lucide-react"; import "../styles/Resources.css"; import { API_ENDPOINTS } from "../config/api"; @@ -41,6 +41,28 @@ interface ResourceBenchmarkAPI { updated_at: string | null; } +interface ResourceArticleAPI { + id: string; + title: string; + author: string | null; + publication_date: string | null; + publisher: string | null; + url: string; + summary: string | null; + image_url: string | null; + image_storage_path: string | null; + article_type: string | null; + language: string | null; + read_time_minutes: number | null; + is_published: boolean; + is_featured: boolean; + published_at: string | null; + metadata: unknown; + tags: ResourceTag[]; + created_at: string; + updated_at: string | null; +} + // Frontend display type interface BenchmarkStudy { id: string; @@ -60,6 +82,18 @@ interface BenchmarkStudy { }; } +interface Article { + id: string; + title: string; + author: string; + year: string; + publisher: string; + summary: string; + articleType: string; + readTime: number; + url: string; +} + // Helper function to map backend data to frontend display format const mapBenchmarkToStudy = (benchmark: ResourceBenchmarkAPI): BenchmarkStudy => { // Extract year from first_released @@ -90,11 +124,31 @@ const mapBenchmarkToStudy = (benchmark: ResourceBenchmarkAPI): BenchmarkStudy => }; }; +// Helper function to map article data to frontend display format +const mapArticleToDisplay = (article: ResourceArticleAPI): Article => { + const year = article.publication_date + ? new Date(article.publication_date).getFullYear().toString() + : 'N/A'; + + return { + id: article.id, + title: article.title, + author: article.author || 'Unknown', + year, + publisher: article.publisher || 'Unknown Publisher', + summary: article.summary || '', + articleType: article.article_type || 'general', + readTime: article.read_time_minutes || 5, + url: article.url, + }; +}; + export default function Resources() { const [activeTab, setActiveTab] = useState<'studies' | 'articles'>('studies'); const [searchQuery, setSearchQuery] = useState(""); const [categoryFilter, setCategoryFilter] = useState("all"); const [benchmarkStudies, setBenchmarkStudies] = useState([]); + const [articles, setArticles] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -126,10 +180,42 @@ export default function Resources() { } }, []); - // Fetch on mount + // Fetch articles from API + const fetchArticles = useCallback(async () => { + try { + setLoading(true); + setError(null); + + const response = await fetch(API_ENDPOINTS.articles); + + if (!response.ok) { + throw new Error(`Failed to fetch articles: ${response.statusText}`); + } + + const data = await response.json(); + + if (data.success && Array.isArray(data.data)) { + const mappedArticles = data.data.map(mapArticleToDisplay); + setArticles(mappedArticles); + } else { + throw new Error('Invalid response format from API'); + } + } catch (err) { + console.error('Error fetching articles:', err); + setError(err instanceof Error ? err.message : 'Failed to load articles'); + } finally { + setLoading(false); + } + }, []); + + // Fetch on mount based on active tab useEffect(() => { - fetchBenchmarks(); - }, [fetchBenchmarks]); + if (activeTab === 'studies') { + fetchBenchmarks(); + } else { + fetchArticles(); + } + }, [activeTab, fetchBenchmarks, fetchArticles]); // Extract unique categories dynamically from fetched data const availableCategories = useMemo(() => { @@ -152,6 +238,28 @@ export default function Resources() { }); }, [benchmarkStudies, searchQuery, categoryFilter]); + // Memoize filtered articles for performance + const filteredArticles = useMemo(() => { + return articles.filter((article) => { + const matchesSearch = + searchQuery === "" || + article.title.toLowerCase().includes(searchQuery.toLowerCase()) || + article.author.toLowerCase().includes(searchQuery.toLowerCase()) || + article.summary.toLowerCase().includes(searchQuery.toLowerCase()) || + article.publisher.toLowerCase().includes(searchQuery.toLowerCase()); + + const matchesCategory = categoryFilter === "all" || article.articleType === categoryFilter; + + return matchesSearch && matchesCategory; + }); + }, [articles, searchQuery, categoryFilter]); + + // Extract unique article types dynamically from fetched data + const availableArticleTypes = useMemo(() => { + const types = new Set(articles.map(article => article.articleType)); + return Array.from(types).sort(); + }, [articles]); + const getCategoryColor = (category: string): string => { switch (category) { case "Mental Health": @@ -165,6 +273,25 @@ export default function Resources() { } }; + const getArticleTypeColor = (type: string): string => { + switch (type) { + case "research_summary": + return "category-mental-health"; + case "news": + return "category-medical"; + case "opinion": + return "category-psychology"; + case "interview": + return "category-interview"; + default: + return ""; + } + }; + + const formatArticleType = (type: string): string => { + return type.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); + }; + return (
{/* Header */} @@ -409,10 +536,107 @@ export default function Resources() { {activeTab === 'articles' && (
-
-

Articles coming soon

-

We're curating a collection of articles about mental health AI benchmarks

+ {/* Filters */} +
+
+ ) => setSearchQuery(e.target.value)} + className="resources-search-input" + aria-label="Search articles" + /> +
+
+ + {/* Loading State */} + {loading && ( +
+

Loading articles...

+
+ )} + + {/* Error State */} + {error && !loading && ( +
+

Error: {error}

+ +
+ )} + + {/* Results Count */} + {!loading && !error && ( +
+ Showing {filteredArticles.length} {filteredArticles.length === 1 ? "article" : "articles"} +
+ )} + + {/* Articles Grid */} + {!loading && !error && ( +
+ {filteredArticles.map((article) => ( + +
+
+

{article.title}

+ + {formatArticleType(article.articleType)} + +
+
+ {article.author} + • {article.publisher} + • {article.year} + • {article.readTime} min read +
+
+ +
+

{article.summary}

+
+ +
+
+
+ + Read Article +
+
+
+
+ ))} + + {/* No Results */} + {filteredArticles.length === 0 && ( +
+

No articles found matching your criteria.

+
+ )} +
+ )}
)} diff --git a/client/src/config/api.ts b/client/src/config/api.ts index 6cef213..8ed08aa 100644 --- a/client/src/config/api.ts +++ b/client/src/config/api.ts @@ -18,6 +18,7 @@ export const API_ENDPOINTS = { conversationalProfiles: (testName: string) => `${API_URL}/current/conversational-profiles/${testName}`, iriProfiles: `${API_URL}/current/iri/profiles`, benchmarks: `${API_URL}/current/resources/benchmarks`, + articles: `${API_URL}/current/resources/articles`, // Community endpoints communityUpdates: `${API_URL}/current/community/updates`, diff --git a/client/src/styles/HomePage.css b/client/src/styles/HomePage.css index f7363f8..657e1fc 100644 --- a/client/src/styles/HomePage.css +++ b/client/src/styles/HomePage.css @@ -9,7 +9,8 @@ .dashboard-home-page section { max-width: 1100px; - margin: 0 auto; + margin-left: auto; + margin-right: auto; } .hero { @@ -20,6 +21,26 @@ gap: 1.75rem; } +.hero-brand-container { + display: flex; + align-items: center; + gap: 1rem; + justify-content: center; +} + +.hero-logo { + width: 60px; + height: 60px; + object-fit: contain; +} + +.hero-brand { + font-size: 2rem; + font-weight: 700; + color: #1f2343; + letter-spacing: -0.01em; +} + .hero-pill { display: inline-flex; align-items: center; @@ -128,7 +149,7 @@ } .feature-section { - margin-top: 6rem; + margin-top: 8rem; text-align: center; } @@ -200,7 +221,7 @@ } .cta-section { - margin-top: 6rem; + margin-top: 8rem; background: linear-gradient(135deg, #f1f4ff 0%, #ffffff 50%); border-radius: 2rem; padding: 3rem 1.5rem; diff --git a/client/src/styles/NavBar.css b/client/src/styles/NavBar.css index b5c8889..ce0c882 100644 --- a/client/src/styles/NavBar.css +++ b/client/src/styles/NavBar.css @@ -42,7 +42,7 @@ } .navbar-brand:hover { - background: #333333; + color: #9ca3af; } .navbar-menu { @@ -59,7 +59,7 @@ .navbar-item { padding: 16px 20px; - color: #d1d5db; + color: #9ca3af; text-decoration: none; transition: all 0.2s; border-bottom: 3px solid transparent; @@ -73,7 +73,6 @@ } .navbar-item:hover { - background: #333333; color: white; } @@ -91,7 +90,7 @@ .navbar-dropdown-trigger { padding: 16px 20px; - color: #d1d5db; + color: #9ca3af; cursor: pointer; transition: all 0.2s; display: block; @@ -104,13 +103,12 @@ } .navbar-dropdown:hover .navbar-dropdown-trigger { - background: #333333; color: white; } .navbar-dropdown-trigger-link { padding: 16px 20px; - color: #d1d5db; + color: #9ca3af; text-decoration: none; transition: all 0.2s; display: block; @@ -123,7 +121,6 @@ } .navbar-dropdown:hover .navbar-dropdown-trigger-link { - background: #333333; color: white; } diff --git a/client/src/styles/Resources.css b/client/src/styles/Resources.css index 4a8604d..27ff8d3 100644 --- a/client/src/styles/Resources.css +++ b/client/src/styles/Resources.css @@ -176,6 +176,16 @@ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); } +.resources-article-card { + cursor: pointer; + display: block; +} + +.resources-article-card:hover { + transform: translateY(-2px); + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); +} + .resources-study-header { padding: 20px; } @@ -223,6 +233,12 @@ border-color: rgba(34, 197, 94, 0.2); } +.category-interview { + background: rgba(245, 158, 11, 0.1); + color: #d97706; + border-color: rgba(245, 158, 11, 0.2); +} + .resources-study-fullname { font-size: 13px; color: #64748b; diff --git a/server/src/controllers/CurrentVersion/User/Resource.controller.ts b/server/src/controllers/CurrentVersion/User/Resource.controller.ts index 66e42fc..7615f87 100644 --- a/server/src/controllers/CurrentVersion/User/Resource.controller.ts +++ b/server/src/controllers/CurrentVersion/User/Resource.controller.ts @@ -191,3 +191,175 @@ export const getResourceBenchmarkById = async ( next(error); } }; + +/** + * GET /api/current/resources/articles + * Fetch resource articles with optional filtering + * Query params: + * - articleType: research_summary | news | opinion | interview | other + * - isFeatured: true | false + * - isPublished: true | false (default: true) + * - publisher: string + */ +export const getResourceArticles = async ( + req: Request, + res: Response, + next: NextFunction +): Promise => { + try { + const articleType = req.query.articleType as string | undefined; + const isFeatured = req.query.isFeatured === 'true'; + const isPublished = req.query.isPublished !== 'false'; // Default to true + const publisher = req.query.publisher as string | undefined; + + const whereClause: any = { + isPublished, + }; + + // Filter by article type if provided + if (articleType) { + whereClause.articleType = articleType; + } + + // Filter by featured status if explicitly requested + if (req.query.isFeatured !== undefined) { + whereClause.isFeatured = isFeatured; + } + + // Filter by publisher if provided + if (publisher) { + whereClause.publisher = { + contains: publisher, + mode: 'insensitive', + }; + } + + const articles = await prisma.resourceArticle.findMany({ + where: whereClause, + include: { + tags: { + include: { + tag: true, + }, + }, + }, + orderBy: [ + { isFeatured: 'desc' }, + { publicationDate: 'desc' }, + { title: 'asc' }, + ], + }); + + // Transform to client-friendly format + const transformedArticles = articles.map((article) => ({ + id: article.id, + title: article.title, + author: article.author, + publication_date: article.publicationDate + ? article.publicationDate.toISOString() + : null, + publisher: article.publisher, + url: article.url, + summary: article.summary, + image_url: article.imageUrl, + image_storage_path: article.imageStoragePath, + article_type: article.articleType, + language: article.language, + read_time_minutes: article.readTimeMinutes, + is_published: article.isPublished, + is_featured: article.isFeatured, + published_at: article.publishedAt ? article.publishedAt.toISOString() : null, + metadata: article.metadata, + tags: article.tags.map((tagLink) => ({ + id: tagLink.tag.id, + name: tagLink.tag.name, + slug: tagLink.tag.slug, + category: tagLink.tag.category, + color: tagLink.tag.color, + icon: tagLink.tag.icon, + })), + created_at: article.createdAt.toISOString(), + updated_at: article.updatedAt ? article.updatedAt.toISOString() : null, + })); + + res.json({ + success: true, + data: transformedArticles, + count: transformedArticles.length, + }); + } catch (error) { + next(error); + } +}; + +/** + * GET /api/current/resources/articles/:id + * Fetch a single resource article by ID + */ +export const getResourceArticleById = async ( + req: Request, + res: Response, + next: NextFunction +): Promise => { + try { + const { id } = req.params; + + const article = await prisma.resourceArticle.findUnique({ + where: { id }, + include: { + tags: { + include: { + tag: true, + }, + }, + }, + }); + + if (!article) { + res.status(404).json({ + success: false, + error: 'Article not found', + }); + return; + } + + // Transform to client-friendly format + const transformedArticle = { + id: article.id, + title: article.title, + author: article.author, + publication_date: article.publicationDate + ? article.publicationDate.toISOString() + : null, + publisher: article.publisher, + url: article.url, + summary: article.summary, + image_url: article.imageUrl, + image_storage_path: article.imageStoragePath, + article_type: article.articleType, + language: article.language, + read_time_minutes: article.readTimeMinutes, + is_published: article.isPublished, + is_featured: article.isFeatured, + published_at: article.publishedAt ? article.publishedAt.toISOString() : null, + metadata: article.metadata, + tags: article.tags.map((tagLink) => ({ + id: tagLink.tag.id, + name: tagLink.tag.name, + slug: tagLink.tag.slug, + category: tagLink.tag.category, + color: tagLink.tag.color, + icon: tagLink.tag.icon, + })), + created_at: article.createdAt.toISOString(), + updated_at: article.updatedAt ? article.updatedAt.toISOString() : null, + }; + + res.json({ + success: true, + data: transformedArticle, + }); + } catch (error) { + next(error); + } +}; diff --git a/server/src/routes/CurrentVersion/index.ts b/server/src/routes/CurrentVersion/index.ts index f872cca..c6dc2fe 100644 --- a/server/src/routes/CurrentVersion/index.ts +++ b/server/src/routes/CurrentVersion/index.ts @@ -9,6 +9,8 @@ import { import { getResourceBenchmarks, getResourceBenchmarkById, + getResourceArticles, + getResourceArticleById, } from '../../controllers/CurrentVersion/User/Resource.controller'; import { getUpdates, @@ -30,6 +32,10 @@ router.get('/iri/profiles', optionalAuth, getIRIProfiles); router.get('/resources/benchmarks', optionalAuth, getResourceBenchmarks); router.get('/resources/benchmarks/:id', optionalAuth, getResourceBenchmarkById); +// Resource Article routes +router.get('/resources/articles', optionalAuth, getResourceArticles); +router.get('/resources/articles/:id', optionalAuth, getResourceArticleById); + // Community routes router.get('/community/updates', optionalAuth, getUpdates); router.get('/community/suggestions', optionalAuth, getSuggestions); From 7524139b65a2b7564d6c05c31c23c0825caf3549 Mon Sep 17 00:00:00 2001 From: khaihtruong Date: Wed, 26 Nov 2025 10:15:02 -0500 Subject: [PATCH 5/5] implemented leaderboard api --- client/package-lock.json | 130 +++++ client/package.json | 2 + client/src/components/Leaderboard.tsx | 148 ++++- client/src/components/Login.tsx | 201 ++++--- client/src/components/Register.tsx | 280 +++++---- client/src/components/ui/label.tsx | 24 + client/src/components/ui/separator.tsx | 29 + client/src/styles/Auth.css | 9 + client/src/styles/index.css | 14 +- .../migration.sql | 530 ++++++++++++++++++ server/prisma/seed.js | 26 +- .../prisma/seeds/benchmarkScoreAggregates.js | 143 +++++ server/prisma/seeds/benchmarking.js | 64 +++ .../User/Benchmark.controller.ts | 283 ++++++++++ server/src/routes/CurrentVersion/index.ts | 12 + 15 files changed, 1688 insertions(+), 207 deletions(-) create mode 100644 client/src/components/ui/label.tsx create mode 100644 client/src/components/ui/separator.tsx create mode 100644 server/prisma/schema/migrations/20251117145720_add_missing_tables/migration.sql create mode 100644 server/prisma/seeds/benchmarkScoreAggregates.js create mode 100644 server/src/controllers/CurrentVersion/User/Benchmark.controller.ts diff --git a/client/package-lock.json b/client/package-lock.json index 5e42134..950e6e8 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -10,7 +10,9 @@ "dependencies": { "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", @@ -1342,6 +1344,70 @@ } } }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz", + "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popper": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", @@ -1519,6 +1585,70 @@ } } }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz", + "integrity": "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", diff --git a/client/package.json b/client/package.json index 4c04d42..2307521 100644 --- a/client/package.json +++ b/client/package.json @@ -12,7 +12,9 @@ "dependencies": { "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", diff --git a/client/src/components/Leaderboard.tsx b/client/src/components/Leaderboard.tsx index ac8fbf4..09c0ce3 100644 --- a/client/src/components/Leaderboard.tsx +++ b/client/src/components/Leaderboard.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo } from "react"; +import { useState, useMemo, useEffect } from "react"; import type { ChangeEvent } from "react"; import { useReactTable, @@ -10,13 +10,6 @@ import { SortingState, } from "@tanstack/react-table"; import { ChevronDown, ChevronRight, Search, Info } from "lucide-react"; -import { - modelVersions, - filterVersions, - systemPrompts, - messagePrompts, - modelFamilies -} from "../data/leaderboardData"; import { Input } from "./ui/input"; import { Checkbox } from "./ui/checkbox"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"; @@ -25,6 +18,8 @@ import { Card } from "./ui/card"; import { Badge } from "./ui/badge"; import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; +const API_BASE = import.meta.env.VITE_API_URL ?? 'http://localhost:5001/api'; + interface ModelVersion { id: string; modelFamily: string; @@ -79,6 +74,12 @@ interface FilterParams { modelFamilies?: string[]; } +interface Prompt { + id: string; + name: string; + content: string; +} + export default function Leaderboard() { const [activeTab, setActiveTab] = useState("models"); const [sorting, setSorting] = useState([]); @@ -92,6 +93,14 @@ export default function Leaderboard() { const [messagePromptFilter, setMessagePromptFilter] = useState(""); const [modelFamilyFilter, setModelFamilyFilter] = useState([]); + // API data state + const [modelVersions, setModelVersions] = useState([]); + const [systemPrompts, setSystemPrompts] = useState([]); + const [messagePrompts, setMessagePrompts] = useState([]); + const [modelFamilies, setModelFamilies] = useState>({}); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const toggleVersion = (versionId: string): void => { setSelectedVersions((prev) => { const next = new Set(prev); @@ -102,6 +111,63 @@ export default function Leaderboard() { const clearAllSelected = (): void => setSelectedVersions(new Set()); + // Fetch data from API + useEffect(() => { + async function fetchLeaderboardData(): Promise { + try { + setLoading(true); + setError(null); + + const [ + leaderboardRes, + systemPromptsRes, + messagePromptsRes, + modelFamiliesRes + ] = await Promise.all([ + fetch(`${API_BASE}/current/leaderboard`), + fetch(`${API_BASE}/current/leaderboard/system-prompts`), + fetch(`${API_BASE}/current/leaderboard/message-prompts`), + fetch(`${API_BASE}/current/leaderboard/model-families`), + ]); + + if (!leaderboardRes.ok || !systemPromptsRes.ok || !messagePromptsRes.ok || !modelFamiliesRes.ok) { + throw new Error('Failed to fetch leaderboard data'); + } + + const [leaderboardData, systemPromptsData, messagePromptsData, modelFamiliesData] = await Promise.all([ + leaderboardRes.json(), + systemPromptsRes.json(), + messagePromptsRes.json(), + modelFamiliesRes.json(), + ]); + + setModelVersions(leaderboardData.data || []); + setSystemPrompts(systemPromptsData.data || []); + setMessagePrompts(messagePromptsData.data || []); + setModelFamilies(modelFamiliesData.data || {}); + } catch (err) { + console.error('Error fetching leaderboard data:', err); + setError(err instanceof Error ? err.message : 'Failed to load leaderboard data'); + } finally { + setLoading(false); + } + } + + fetchLeaderboardData(); + }, []); + + // Helper function to filter versions (replaces imported filterVersions) + const filterVersions = (versions: ModelVersion[], filters: FilterParams): ModelVersion[] => { + return versions.filter(v => { + if (filters.temperature !== undefined && v.temperature !== filters.temperature) return false; + if (filters.top_p !== undefined && v.top_p !== filters.top_p) return false; + if (filters.system_prompt_id && v.system_prompt_id !== filters.system_prompt_id) return false; + if (filters.message_prompt_id && v.message_prompt_id !== filters.message_prompt_id) return false; + if (filters.modelFamilies && !filters.modelFamilies.includes(v.modelFamily)) return false; + return true; + }); + }; + const sortedMainRows = useMemo(() => { const filters: FilterParams = { temperature: temperatureFilter ? parseFloat(temperatureFilter) : undefined, @@ -154,7 +220,7 @@ export default function Leaderboard() { }); return mainRows; - }, [temperatureFilter, topPFilter, systemPromptFilter, messagePromptFilter, modelFamilyFilter]); + }, [modelVersions, temperatureFilter, topPFilter, systemPromptFilter, messagePromptFilter, modelFamilyFilter]); const data = useMemo(() => { const rows: TableRow[] = []; @@ -352,10 +418,36 @@ export default function Leaderboard() { }); const comparisonRows = useMemo( - () => (modelVersions as ModelVersion[]).filter((v) => selectedVersions.has(v.id)), - [selectedVersions] + () => modelVersions.filter((v) => selectedVersions.has(v.id)), + [selectedVersions, modelVersions] ); + // Loading state + if (loading) { + return ( +
+
+
+

Loading leaderboard data...

+
+
+ ); + } + + // Error state + if (error) { + return ( +
+
+
⚠️
+

Failed to Load Leaderboard

+

{error}

+ +
+
+ ); + } + return (
{/* Header Section */} @@ -661,12 +753,12 @@ export default function Leaderboard() { - - - - - + + + + - - {comparisonRows.map((v) => ( - - - - - - - + + + + + + ))} diff --git a/client/src/components/Login.tsx b/client/src/components/Login.tsx index 0021713..00b2554 100644 --- a/client/src/components/Login.tsx +++ b/client/src/components/Login.tsx @@ -1,7 +1,11 @@ import { useState, FormEvent, ChangeEvent } from 'react'; import { Link, useNavigate, useLocation } from 'react-router-dom'; import { useAuth } from '../contexts/AuthContext'; -import '../styles/Auth.css'; +import { Button } from './ui/button'; +import { Input } from './ui/input'; +import { Label } from './ui/label'; +import { Card } from './ui/card'; +import { Separator } from './ui/separator'; interface FormData { email: string; @@ -84,79 +88,144 @@ export default function Login() { } }; + const handleOAuthLogin = () => { + // TODO: Implement Google OAuth login + console.log('Google OAuth not yet implemented'); + setErrors({ submit: 'Google login coming soon!' }); + }; + return ( -
-
-
-

Sign In

-

Welcome back to MindBenchAI

+
+ {/* Sign In Form */} +
+
+

Welcome Back

+

Sign in to your account to continue

-
- {errors.submit && ( -
{errors.submit}
- )} - -
- - - {errors.email && {errors.email}} + + + {errors.submit && ( +
+

{errors.submit}

+
+ )} + +
+ + + {errors.email && ( +

{errors.email}

+ )} +
+ +
+
+ + + Forgot password? + +
+ + {errors.password && ( +

{errors.password}

+ )} +
+ + + + +
+
+
+ +
+
+ Or continue with +
+
+ +
+ +
-
- - - {errors.password && {errors.password}} +
+

+ Don't have an account?{' '} + + Sign up + +

- - - -
-

- - Forgot your password? - -

-

- Don't have an account?{' '} - Sign up -

-
- -
-

Demo Accounts

-
- Researcher: researcher@mindbench.ai / TestPassword123! -
-
- User: user@mindbench.ai / TestPassword123! + {/* Demo Credentials */} +
+

Demo Accounts

+
+
+ Researcher: researcher@mindbench.ai / TestPassword123! +
+
+ User: user@mindbench.ai / TestPassword123! +
+
-
+
); diff --git a/client/src/components/Register.tsx b/client/src/components/Register.tsx index dca1d1e..d0a20ca 100644 --- a/client/src/components/Register.tsx +++ b/client/src/components/Register.tsx @@ -1,7 +1,11 @@ import { useState, FormEvent, ChangeEvent } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { useAuth } from '../contexts/AuthContext'; -import '../styles/Auth.css'; +import { Button } from './ui/button'; +import { Input } from './ui/input'; +import { Label } from './ui/label'; +import { Card } from './ui/card'; +import { Separator } from './ui/separator'; interface FormData { email: string; @@ -118,126 +122,200 @@ export default function Register() { } }; + const handleOAuthRegister = () => { + // TODO: Implement Google OAuth registration + console.log('Google OAuth not yet implemented'); + setErrors({ submit: 'Google registration coming soon!' }); + }; + return ( -
-
-
-

Create Account

-

Join MindBenchAI today

+
+ {/* Sign Up Form */} +
+
+

Create Account

+

Get started with MindBench.ai today

-
- {errors.submit && ( -
{errors.submit}
- )} + + + {errors.submit && ( +
+

{errors.submit}

+
+ )} + +
+
+ + + {errors.firstName && ( +

{errors.firstName}

+ )} +
-
-
- - + + + {errors.lastName && ( +

{errors.lastName}

+ )} +
+
+ +
+ + - {errors.firstName && {errors.firstName}} + {errors.username && ( +

{errors.username}

+ )}
-
- - + + - {errors.lastName && {errors.lastName}} + {errors.email && ( +

{errors.email}

+ )}
-
-
- - - {errors.username && {errors.username}} -
+
+ + + {!errors.password && ( +

Must be at least 8 characters with uppercase, lowercase, number, and special character

+ )} + {errors.password && ( +

{errors.password}

+ )} +
-
- - - {errors.email && {errors.email}} -
+
+ + + {errors.confirmPassword && ( +

{errors.confirmPassword}

+ )} +
-
- - - {errors.password && {errors.password}} -
+ + -
- - - {errors.confirmPassword && {errors.confirmPassword}} +
+
+
+ +
+
+ Or continue with +
+
+ +
+ +
- - - -
-

- Already have an account?{' '} - Sign in -

-
+
+

+ Already have an account?{' '} + + Sign in + +

+
+
); diff --git a/client/src/components/ui/label.tsx b/client/src/components/ui/label.tsx new file mode 100644 index 0000000..c23cb40 --- /dev/null +++ b/client/src/components/ui/label.tsx @@ -0,0 +1,24 @@ +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "../../lib/utils" + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" +) + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)) +Label.displayName = LabelPrimitive.Root.displayName + +export { Label } diff --git a/client/src/components/ui/separator.tsx b/client/src/components/ui/separator.tsx new file mode 100644 index 0000000..ffdf7a1 --- /dev/null +++ b/client/src/components/ui/separator.tsx @@ -0,0 +1,29 @@ +import * as React from "react" +import * as SeparatorPrimitive from "@radix-ui/react-separator" + +import { cn } from "../../lib/utils" + +const Separator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>( + ( + { className, orientation = "horizontal", decorative = true, ...props }, + ref + ) => ( + + ) +) +Separator.displayName = SeparatorPrimitive.Root.displayName + +export { Separator } diff --git a/client/src/styles/Auth.css b/client/src/styles/Auth.css index 01784f9..517b52f 100644 --- a/client/src/styles/Auth.css +++ b/client/src/styles/Auth.css @@ -86,6 +86,15 @@ cursor: not-allowed; } +/* Override browser autofill styles */ +.form-group input:-webkit-autofill, +.form-group input:-webkit-autofill:hover, +.form-group input:-webkit-autofill:focus { + -webkit-box-shadow: 0 0 0 1000px var(--panel) inset !important; + -webkit-text-fill-color: var(--text) !important; + transition: background-color 5000s ease-in-out 0s; +} + .field-error { color: #ef4444; font-size: 0.875rem; diff --git a/client/src/styles/index.css b/client/src/styles/index.css index 2c5278d..5400c90 100644 --- a/client/src/styles/index.css +++ b/client/src/styles/index.css @@ -46,8 +46,8 @@ --accent-foreground: #030213; --destructive: #d4183d; --destructive-foreground: #fff; - --border: #0000001a; - --input: transparent; + --border: #e5e7eb; + --input: #f3f3f5; --input-background: #f3f3f5; --switch-background: #cbced4; --font-weight-medium: 500; @@ -175,4 +175,14 @@ html { line-height: 1.5; } } + + /* Override browser autofill styles to ensure consistent input backgrounds */ + input:-webkit-autofill, + input:-webkit-autofill:hover, + input:-webkit-autofill:focus, + input:-webkit-autofill:active { + -webkit-box-shadow: 0 0 0 1000px var(--input-background) inset !important; + -webkit-text-fill-color: var(--foreground) !important; + transition: background-color 5000s ease-in-out 0s; + } } diff --git a/server/prisma/schema/migrations/20251117145720_add_missing_tables/migration.sql b/server/prisma/schema/migrations/20251117145720_add_missing_tables/migration.sql new file mode 100644 index 0000000..5a02220 --- /dev/null +++ b/server/prisma/schema/migrations/20251117145720_add_missing_tables/migration.sql @@ -0,0 +1,530 @@ +/* + Warnings: + + - The `status` column on the `suggestions` table would be dropped and recreated. This will lead to data loss if there is data in the column. + - You are about to drop the column `display_order` on the `team_members` table. All the data in the column will be lost. + - You are about to drop the column `tag` on the `updates` table. All the data in the column will be lost. + - You are about to drop the `response_profile_answers` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `response_profile_questions` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `response_profile_test` table. If the table is not empty, all the data it contains will be lost. + - A unique constraint covering the columns `[entity_type,model_version_id,tool_configuration_id]` on the table `evaluation_entities` will be added. If there are existing duplicate values, this will fail. + - A unique constraint covering the columns `[slug]` on the table `updates` will be added. If there are existing duplicate values, this will fail. + - Made the column `model_family_id` on table `models` required. This step will fail if there are existing NULL values in that column. + - Added the required column `category` to the `updates` table without a default value. This is not possible if the table is not empty. + +*/ +-- CreateEnum +CREATE TYPE "BenchmarkType" AS ENUM ('multiple_choice', 'open_ended', 'conversation', 'mixed', 'other'); + +-- CreateEnum +CREATE TYPE "UpdateCategory" AS ENUM ('feature', 'bug_fix', 'improvement', 'announcement', 'research', 'community'); + +-- CreateEnum +CREATE TYPE "SuggestionStatus" AS ENUM ('open_vote', 'under_review', 'planned', 'in_progress', 'completed', 'declined', 'duplicate'); + +-- CreateEnum +CREATE TYPE "SuggestionCategory" AS ENUM ('feature', 'benchmark', 'model', 'ui_ux', 'documentation', 'bug', 'other'); + +-- AlterEnum +ALTER TYPE "EntityType" ADD VALUE 'both'; + +-- DropForeignKey +ALTER TABLE "public"."models" DROP CONSTRAINT "models_model_family_id_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."response_profile_answers" DROP CONSTRAINT "response_profile_answers_approved_by_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."response_profile_answers" DROP CONSTRAINT "response_profile_answers_created_by_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."response_profile_answers" DROP CONSTRAINT "response_profile_answers_evaluation_entity_id_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."response_profile_answers" DROP CONSTRAINT "response_profile_answers_question_id_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."response_profile_answers" DROP CONSTRAINT "response_profile_answers_review_assignment_id_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."response_profile_answers" DROP CONSTRAINT "response_profile_answers_reviewer_id_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."response_profile_questions" DROP CONSTRAINT "response_profile_questions_created_by_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."response_profile_questions" DROP CONSTRAINT "response_profile_questions_test_id_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."response_profile_questions" DROP CONSTRAINT "response_profile_questions_updated_by_fkey"; + +-- DropIndex +DROP INDEX "public"."team_members_display_order_idx"; + +-- DropIndex +DROP INDEX "public"."updates_tag_idx"; + +-- AlterTable +ALTER TABLE "models" ALTER COLUMN "model_family_id" SET NOT NULL; + +-- AlterTable +ALTER TABLE "suggestions" ADD COLUMN "category" "SuggestionCategory", +ADD COLUMN "closed_at" TIMESTAMP(3), +ADD COLUMN "closed_reason" TEXT, +ADD COLUMN "implemented_at" TIMESTAMP(3), +ADD COLUMN "priority" INTEGER, +ADD COLUMN "related_issue_url" TEXT, +ADD COLUMN "reviewed_at" TIMESTAMP(3), +ADD COLUMN "reviewed_by" TEXT, +DROP COLUMN "status", +ADD COLUMN "status" "SuggestionStatus" NOT NULL DEFAULT 'open_vote'; + +-- AlterTable +ALTER TABLE "team_members" DROP COLUMN "display_order", +ADD COLUMN "end_date" DATE, +ADD COLUMN "expertise" JSONB, +ADD COLUMN "image_storage_path" TEXT, +ADD COLUMN "social_links" JSONB, +ADD COLUMN "sort_order" INTEGER, +ADD COLUMN "start_date" DATE; + +-- AlterTable +ALTER TABLE "update_reactions" ADD COLUMN "updated_at" TIMESTAMP(3); + +-- AlterTable +ALTER TABLE "updates" DROP COLUMN "tag", +ADD COLUMN "category" "UpdateCategory" NOT NULL, +ADD COLUMN "image_storage_path" TEXT, +ADD COLUMN "is_featured" BOOLEAN NOT NULL DEFAULT false, +ADD COLUMN "published_at" TIMESTAMP(3), +ADD COLUMN "slug" TEXT; + +-- DropTable +DROP TABLE "public"."response_profile_answers"; + +-- DropTable +DROP TABLE "public"."response_profile_questions"; + +-- DropTable +DROP TABLE "public"."response_profile_test"; + +-- CreateTable +CREATE TABLE "conversational_profile_test" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "test_type" TEXT NOT NULL, + "version" INTEGER NOT NULL DEFAULT 1, + "is_validated" BOOLEAN NOT NULL DEFAULT false, + "is_public" BOOLEAN NOT NULL DEFAULT false, + "scale_min" INTEGER NOT NULL DEFAULT 0, + "scale_max" INTEGER NOT NULL DEFAULT 5, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "created_by" TEXT, + "updated_at" TIMESTAMP(3), + "updated_by" TEXT, + + CONSTRAINT "conversational_profile_test_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "conversational_profile_questions" ( + "id" TEXT NOT NULL, + "test_id" TEXT, + "question_type" "QuestionType" NOT NULL, + "question_key" TEXT NOT NULL, + "question_text" TEXT NOT NULL, + "category" TEXT NOT NULL, + "subcategory" TEXT, + "display_order" INTEGER NOT NULL, + "is_active" BOOLEAN NOT NULL DEFAULT true, + "is_displayed" BOOLEAN NOT NULL DEFAULT true, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "created_by" TEXT, + "updated_at" TIMESTAMP(3), + "updated_by" TEXT, + + CONSTRAINT "conversational_profile_questions_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "conversational_profile_answers" ( + "id" TEXT NOT NULL, + "question_id" TEXT NOT NULL, + "entity_type" "EntityType" NOT NULL, + "entity_id" TEXT NOT NULL, + "evaluation_entity_id" TEXT NOT NULL, + "boolean_value" BOOLEAN, + "numeric_value" DECIMAL(65,30), + "text_value" TEXT, + "list_value" TEXT, + "notes" TEXT, + "reviewer_id" TEXT NOT NULL, + "review_assignment_id" TEXT, + "is_approved" BOOLEAN NOT NULL DEFAULT false, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "created_by" TEXT, + "approved_at" TIMESTAMP(3), + "approved_by" TEXT, + + CONSTRAINT "conversational_profile_answers_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "resource_benchmarks" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "benchmark_type" "BenchmarkType" NOT NULL, + "format" TEXT, + "image_url" TEXT, + "image_storage_path" TEXT, + "links" JSONB, + "first_released" DATE, + "organization" TEXT, + "language" TEXT DEFAULT 'en', + "question_count" INTEGER, + "is_active" BOOLEAN NOT NULL DEFAULT true, + "is_featured" BOOLEAN NOT NULL DEFAULT false, + "metadata" JSONB, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "created_by" TEXT, + "updated_at" TIMESTAMP(3), + "updated_by" TEXT, + + CONSTRAINT "resource_benchmarks_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "resource_articles" ( + "id" TEXT NOT NULL, + "title" TEXT NOT NULL, + "author" TEXT, + "publication_date" DATE, + "publisher" TEXT, + "url" TEXT NOT NULL, + "summary" TEXT, + "image_url" TEXT, + "image_storage_path" TEXT, + "article_type" TEXT, + "language" TEXT DEFAULT 'en', + "read_time_minutes" INTEGER, + "is_published" BOOLEAN NOT NULL DEFAULT false, + "is_featured" BOOLEAN NOT NULL DEFAULT false, + "published_at" TIMESTAMP(3), + "metadata" JSONB, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "created_by" TEXT, + "updated_at" TIMESTAMP(3), + "updated_by" TEXT, + + CONSTRAINT "resource_articles_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "resource_papers" ( + "id" TEXT NOT NULL, + "title" TEXT NOT NULL, + "authors" JSONB NOT NULL, + "publication_date" DATE, + "publication" TEXT, + "venue" TEXT, + "arxiv_id" TEXT, + "doi" TEXT, + "url" TEXT, + "pdf_url" TEXT, + "abstract" TEXT, + "image_url" TEXT, + "image_storage_path" TEXT, + "citation_count" INTEGER, + "paper_type" TEXT, + "is_preprint" BOOLEAN NOT NULL DEFAULT false, + "is_peer_reviewed" BOOLEAN NOT NULL DEFAULT false, + "is_published" BOOLEAN NOT NULL DEFAULT false, + "is_featured" BOOLEAN NOT NULL DEFAULT false, + "citation" JSONB, + "metadata" JSONB, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "created_by" TEXT, + "updated_at" TIMESTAMP(3), + "updated_by" TEXT, + + CONSTRAINT "resource_papers_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "resource_tags" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "slug" TEXT NOT NULL, + "description" TEXT, + "category" TEXT, + "color" TEXT, + "icon" TEXT, + "sort_order" INTEGER, + "is_active" BOOLEAN NOT NULL DEFAULT true, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "created_by" TEXT, + "updated_at" TIMESTAMP(3), + "updated_by" TEXT, + + CONSTRAINT "resource_tags_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "resource_benchmark_tag_link" ( + "id" TEXT NOT NULL, + "benchmark_id" TEXT NOT NULL, + "tag_id" TEXT NOT NULL, + + CONSTRAINT "resource_benchmark_tag_link_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "resource_article_tag_link" ( + "id" TEXT NOT NULL, + "article_id" TEXT NOT NULL, + "tag_id" TEXT NOT NULL, + + CONSTRAINT "resource_article_tag_link_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "resource_paper_tag_link" ( + "id" TEXT NOT NULL, + "paper_id" TEXT NOT NULL, + "tag_id" TEXT NOT NULL, + + CONSTRAINT "resource_paper_tag_link_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "conversational_profile_test_name_key" ON "conversational_profile_test"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "conversational_profile_questions_question_text_question_key_key" ON "conversational_profile_questions"("question_text", "question_key"); + +-- CreateIndex +CREATE INDEX "conversational_profile_answers_question_id_is_approved_crea_idx" ON "conversational_profile_answers"("question_id", "is_approved", "created_at"); + +-- CreateIndex +CREATE INDEX "conversational_profile_answers_evaluation_entity_id_idx" ON "conversational_profile_answers"("evaluation_entity_id"); + +-- CreateIndex +CREATE INDEX "conversational_profile_answers_review_assignment_id_idx" ON "conversational_profile_answers"("review_assignment_id"); + +-- CreateIndex +CREATE INDEX "conversational_profile_answers_is_approved_created_at_idx" ON "conversational_profile_answers"("is_approved", "created_at"); + +-- CreateIndex +CREATE UNIQUE INDEX "conversational_profile_answers_question_id_evaluation_entit_key" ON "conversational_profile_answers"("question_id", "evaluation_entity_id"); + +-- CreateIndex +CREATE INDEX "resource_benchmarks_benchmark_type_idx" ON "resource_benchmarks"("benchmark_type"); + +-- CreateIndex +CREATE INDEX "resource_benchmarks_is_active_idx" ON "resource_benchmarks"("is_active"); + +-- CreateIndex +CREATE INDEX "resource_benchmarks_is_featured_idx" ON "resource_benchmarks"("is_featured"); + +-- CreateIndex +CREATE INDEX "resource_benchmarks_first_released_idx" ON "resource_benchmarks"("first_released"); + +-- CreateIndex +CREATE INDEX "resource_articles_publication_date_idx" ON "resource_articles"("publication_date"); + +-- CreateIndex +CREATE INDEX "resource_articles_publisher_idx" ON "resource_articles"("publisher"); + +-- CreateIndex +CREATE INDEX "resource_articles_article_type_idx" ON "resource_articles"("article_type"); + +-- CreateIndex +CREATE INDEX "resource_articles_is_published_idx" ON "resource_articles"("is_published"); + +-- CreateIndex +CREATE INDEX "resource_articles_is_featured_idx" ON "resource_articles"("is_featured"); + +-- CreateIndex +CREATE INDEX "resource_articles_published_at_idx" ON "resource_articles"("published_at"); + +-- CreateIndex +CREATE INDEX "resource_papers_arxiv_id_idx" ON "resource_papers"("arxiv_id"); + +-- CreateIndex +CREATE INDEX "resource_papers_doi_idx" ON "resource_papers"("doi"); + +-- CreateIndex +CREATE INDEX "resource_papers_publication_date_idx" ON "resource_papers"("publication_date"); + +-- CreateIndex +CREATE INDEX "resource_papers_paper_type_idx" ON "resource_papers"("paper_type"); + +-- CreateIndex +CREATE INDEX "resource_papers_is_preprint_idx" ON "resource_papers"("is_preprint"); + +-- CreateIndex +CREATE INDEX "resource_papers_is_peer_reviewed_idx" ON "resource_papers"("is_peer_reviewed"); + +-- CreateIndex +CREATE INDEX "resource_papers_is_published_idx" ON "resource_papers"("is_published"); + +-- CreateIndex +CREATE INDEX "resource_papers_is_featured_idx" ON "resource_papers"("is_featured"); + +-- CreateIndex +CREATE UNIQUE INDEX "resource_tags_name_key" ON "resource_tags"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "resource_tags_slug_key" ON "resource_tags"("slug"); + +-- CreateIndex +CREATE INDEX "resource_tags_category_idx" ON "resource_tags"("category"); + +-- CreateIndex +CREATE INDEX "resource_tags_is_active_idx" ON "resource_tags"("is_active"); + +-- CreateIndex +CREATE INDEX "resource_tags_sort_order_idx" ON "resource_tags"("sort_order"); + +-- CreateIndex +CREATE INDEX "resource_benchmark_tag_link_benchmark_id_idx" ON "resource_benchmark_tag_link"("benchmark_id"); + +-- CreateIndex +CREATE INDEX "resource_benchmark_tag_link_tag_id_idx" ON "resource_benchmark_tag_link"("tag_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "resource_benchmark_tag_link_benchmark_id_tag_id_key" ON "resource_benchmark_tag_link"("benchmark_id", "tag_id"); + +-- CreateIndex +CREATE INDEX "resource_article_tag_link_article_id_idx" ON "resource_article_tag_link"("article_id"); + +-- CreateIndex +CREATE INDEX "resource_article_tag_link_tag_id_idx" ON "resource_article_tag_link"("tag_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "resource_article_tag_link_article_id_tag_id_key" ON "resource_article_tag_link"("article_id", "tag_id"); + +-- CreateIndex +CREATE INDEX "resource_paper_tag_link_paper_id_idx" ON "resource_paper_tag_link"("paper_id"); + +-- CreateIndex +CREATE INDEX "resource_paper_tag_link_tag_id_idx" ON "resource_paper_tag_link"("tag_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "resource_paper_tag_link_paper_id_tag_id_key" ON "resource_paper_tag_link"("paper_id", "tag_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "evaluation_entities_entity_type_model_version_id_tool_confi_key" ON "evaluation_entities"("entity_type", "model_version_id", "tool_configuration_id"); + +-- CreateIndex +CREATE INDEX "suggestions_category_idx" ON "suggestions"("category"); + +-- CreateIndex +CREATE INDEX "suggestions_status_idx" ON "suggestions"("status"); + +-- CreateIndex +CREATE INDEX "suggestions_priority_idx" ON "suggestions"("priority"); + +-- CreateIndex +CREATE INDEX "suggestions_reviewed_at_idx" ON "suggestions"("reviewed_at"); + +-- CreateIndex +CREATE INDEX "suggestions_implemented_at_idx" ON "suggestions"("implemented_at"); + +-- CreateIndex +CREATE INDEX "team_members_sort_order_idx" ON "team_members"("sort_order"); + +-- CreateIndex +CREATE INDEX "team_members_start_date_idx" ON "team_members"("start_date"); + +-- CreateIndex +CREATE INDEX "team_members_end_date_idx" ON "team_members"("end_date"); + +-- CreateIndex +CREATE UNIQUE INDEX "updates_slug_key" ON "updates"("slug"); + +-- CreateIndex +CREATE INDEX "updates_category_idx" ON "updates"("category"); + +-- CreateIndex +CREATE INDEX "updates_is_featured_idx" ON "updates"("is_featured"); + +-- CreateIndex +CREATE INDEX "updates_published_at_idx" ON "updates"("published_at"); + +-- AddForeignKey +ALTER TABLE "suggestions" ADD CONSTRAINT "suggestions_reviewed_by_fkey" FOREIGN KEY ("reviewed_by") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "conversational_profile_questions" ADD CONSTRAINT "conversational_profile_questions_test_id_fkey" FOREIGN KEY ("test_id") REFERENCES "conversational_profile_test"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "conversational_profile_questions" ADD CONSTRAINT "conversational_profile_questions_created_by_fkey" FOREIGN KEY ("created_by") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "conversational_profile_questions" ADD CONSTRAINT "conversational_profile_questions_updated_by_fkey" FOREIGN KEY ("updated_by") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "conversational_profile_answers" ADD CONSTRAINT "conversational_profile_answers_question_id_fkey" FOREIGN KEY ("question_id") REFERENCES "conversational_profile_questions"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "conversational_profile_answers" ADD CONSTRAINT "conversational_profile_answers_reviewer_id_fkey" FOREIGN KEY ("reviewer_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "conversational_profile_answers" ADD CONSTRAINT "conversational_profile_answers_approved_by_fkey" FOREIGN KEY ("approved_by") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "conversational_profile_answers" ADD CONSTRAINT "conversational_profile_answers_created_by_fkey" FOREIGN KEY ("created_by") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "conversational_profile_answers" ADD CONSTRAINT "conversational_profile_answers_review_assignment_id_fkey" FOREIGN KEY ("review_assignment_id") REFERENCES "profile_review_assignments"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "conversational_profile_answers" ADD CONSTRAINT "conversational_profile_answers_evaluation_entity_id_fkey" FOREIGN KEY ("evaluation_entity_id") REFERENCES "evaluation_entities"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "models" ADD CONSTRAINT "models_model_family_id_fkey" FOREIGN KEY ("model_family_id") REFERENCES "model_families"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "resource_benchmarks" ADD CONSTRAINT "resource_benchmarks_created_by_fkey" FOREIGN KEY ("created_by") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "resource_benchmarks" ADD CONSTRAINT "resource_benchmarks_updated_by_fkey" FOREIGN KEY ("updated_by") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "resource_articles" ADD CONSTRAINT "resource_articles_created_by_fkey" FOREIGN KEY ("created_by") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "resource_articles" ADD CONSTRAINT "resource_articles_updated_by_fkey" FOREIGN KEY ("updated_by") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "resource_papers" ADD CONSTRAINT "resource_papers_created_by_fkey" FOREIGN KEY ("created_by") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "resource_papers" ADD CONSTRAINT "resource_papers_updated_by_fkey" FOREIGN KEY ("updated_by") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "resource_tags" ADD CONSTRAINT "resource_tags_created_by_fkey" FOREIGN KEY ("created_by") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "resource_tags" ADD CONSTRAINT "resource_tags_updated_by_fkey" FOREIGN KEY ("updated_by") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "resource_benchmark_tag_link" ADD CONSTRAINT "resource_benchmark_tag_link_benchmark_id_fkey" FOREIGN KEY ("benchmark_id") REFERENCES "resource_benchmarks"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "resource_benchmark_tag_link" ADD CONSTRAINT "resource_benchmark_tag_link_tag_id_fkey" FOREIGN KEY ("tag_id") REFERENCES "resource_tags"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "resource_article_tag_link" ADD CONSTRAINT "resource_article_tag_link_article_id_fkey" FOREIGN KEY ("article_id") REFERENCES "resource_articles"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "resource_article_tag_link" ADD CONSTRAINT "resource_article_tag_link_tag_id_fkey" FOREIGN KEY ("tag_id") REFERENCES "resource_tags"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "resource_paper_tag_link" ADD CONSTRAINT "resource_paper_tag_link_paper_id_fkey" FOREIGN KEY ("paper_id") REFERENCES "resource_papers"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "resource_paper_tag_link" ADD CONSTRAINT "resource_paper_tag_link_tag_id_fkey" FOREIGN KEY ("tag_id") REFERENCES "resource_tags"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/server/prisma/seed.js b/server/prisma/seed.js index a1eb54c..c1fe9f3 100644 --- a/server/prisma/seed.js +++ b/server/prisma/seed.js @@ -8,12 +8,13 @@ const seedCSIConversationalProfile = require('./seeds/csiConversationalProfile') const seedUsers = require('./seeds/users'); const seedResources = require('./seeds/resources'); const seedCommunity = require('./seeds/community'); -// const seedBenchmarking = require('./seeds/benchmarking'); -// const seedExperiments = require('./seeds/experiments'); -// const seedHyperparameters = require('./seeds/hyperparameters'); -// const seedResults = require('./seeds/results'); -// const seedSystem = require('./seeds/system'); -// const seedRatings = require('./seeds/ratings'); +const seedBenchmarking = require('./seeds/benchmarking'); +const seedBenchmarkScoreAggregates = require('./seeds/benchmarkScoreAggregates'); +const seedExperiments = require('./seeds/experiments'); +const seedHyperparameters = require('./seeds/hyperparameters'); +const seedResults = require('./seeds/results'); +const seedSystem = require('./seeds/system'); +const seedRatings = require('./seeds/ratings'); const prisma = new PrismaClient(); @@ -53,10 +54,15 @@ async function main() { regularUser: users.regularUser, }); - // const benchmarking = await seedBenchmarking(prisma, { - // researcherUser: users.researcherUser, - // }); + const benchmarking = await seedBenchmarking(prisma, { + researcherUser: users.researcherUser, + }); + + await seedBenchmarkScoreAggregates(prisma, { + benchmarking, + }); + // Temporarily commented out due to model version lookup issues // const experimentContexts = await seedExperiments(prisma, { // researcherUser: users.researcherUser, // professionalUser: users.professionalUser, @@ -86,7 +92,7 @@ async function main() { // benchmarking, // }); - console.log('Database seed completed successfully (core models + tech profiles + resources + community).'); + console.log('Database seed completed successfully!'); } main() diff --git a/server/prisma/seeds/benchmarkScoreAggregates.js b/server/prisma/seeds/benchmarkScoreAggregates.js new file mode 100644 index 0000000..a918990 --- /dev/null +++ b/server/prisma/seeds/benchmarkScoreAggregates.js @@ -0,0 +1,143 @@ +const crypto = require('crypto'); + +// Model version data from leaderboardData.js +const leaderboardMockData = [ + // GPT-4o versions + { modelFamily: 'GPT', model: 'GPT-4o', version: '20250915', temperature: 0.7, top_p: 0.95, max_tokens: 1000, system_prompt_id: 'default', message_prompt_id: 'standard', SIRI_2: 1.245, A_pharm: 0.92, A_mamh: 1.08 }, + { modelFamily: 'GPT', model: 'GPT-4o', version: '20250815', temperature: 0.7, top_p: 0.95, max_tokens: 1000, system_prompt_id: 'default', message_prompt_id: 'standard', SIRI_2: 1.279, A_pharm: 0.95, A_mamh: 1.12 }, + { modelFamily: 'GPT', model: 'GPT-4o', version: '20250701', temperature: 0.7, top_p: 0.95, max_tokens: 1000, system_prompt_id: 'default', message_prompt_id: 'standard', SIRI_2: 1.312, A_pharm: 0.98, A_mamh: 1.15 }, + { modelFamily: 'GPT', model: 'GPT-4o', version: '20250601', temperature: 0.3, top_p: 0.9, max_tokens: 1000, system_prompt_id: 'clinical', message_prompt_id: 'standard', SIRI_2: 1.198, A_pharm: 0.89, A_mamh: 1.03 }, + + // GPT-3.5 Turbo versions + { modelFamily: 'GPT', model: 'GPT-3.5 Turbo', version: '20250815', temperature: 0.7, top_p: 0.95, max_tokens: 1000, system_prompt_id: 'default', message_prompt_id: 'standard', SIRI_2: 1.737, A_pharm: 1.42, A_mamh: 1.58 }, + { modelFamily: 'GPT', model: 'GPT-3.5 Turbo', version: '20250701', temperature: 0.7, top_p: 0.95, max_tokens: 1000, system_prompt_id: 'default', message_prompt_id: 'standard', SIRI_2: 1.782, A_pharm: 1.45, A_mamh: 1.62 }, + + // Claude Opus versions + { modelFamily: 'Claude', model: 'Claude Opus 4.1', version: '20250901', temperature: 0.7, top_p: 0.95, max_tokens: 1000, system_prompt_id: 'default', message_prompt_id: 'standard', SIRI_2: 0.876, A_pharm: 0.79, A_mamh: 0.89 }, + { modelFamily: 'Claude', model: 'Claude Opus 4.1', version: '20250815', temperature: 0.7, top_p: 0.95, max_tokens: 1000, system_prompt_id: 'default', message_prompt_id: 'standard', SIRI_2: 0.899, A_pharm: 0.82, A_mamh: 0.94 }, + { modelFamily: 'Claude', model: 'Claude Opus 4.1', version: '20250701', temperature: 0.7, top_p: 0.95, max_tokens: 1000, system_prompt_id: 'default', message_prompt_id: 'standard', SIRI_2: 0.923, A_pharm: 0.84, A_mamh: 0.97 }, + { modelFamily: 'Claude', model: 'Claude Opus 4.1', version: '20250601', temperature: 0.3, top_p: 0.9, max_tokens: 1000, system_prompt_id: 'clinical', message_prompt_id: 'standard', SIRI_2: 0.845, A_pharm: 0.76, A_mamh: 0.85 }, + + // Claude Sonnet versions + { modelFamily: 'Claude', model: 'Claude Sonnet 4', version: '20250915', temperature: 0.7, top_p: 0.95, max_tokens: 1000, system_prompt_id: 'default', message_prompt_id: 'standard', SIRI_2: 0.865, A_pharm: 0.83, A_mamh: 0.88 }, + { modelFamily: 'Claude', model: 'Claude Sonnet 4', version: '20250815', temperature: 0.7, top_p: 0.95, max_tokens: 1000, system_prompt_id: 'default', message_prompt_id: 'standard', SIRI_2: 0.888, A_pharm: 0.85, A_mamh: 0.91 }, + { modelFamily: 'Claude', model: 'Claude Sonnet 4', version: '20250601', temperature: 0.7, top_p: 0.95, max_tokens: 1000, system_prompt_id: 'default', message_prompt_id: 'standard', SIRI_2: 0.912, A_pharm: 0.87, A_mamh: 0.93 }, + + // Claude 3.5 Sonnet versions + { modelFamily: 'Claude', model: 'Claude 3.5 Sonnet', version: '20241022', temperature: 0.7, top_p: 0.95, max_tokens: 1000, system_prompt_id: 'default', message_prompt_id: 'standard', SIRI_2: 1.075, A_pharm: 1.08, A_mamh: 1.28 }, + { modelFamily: 'Claude', model: 'Claude 3.5 Sonnet', version: '20240620', temperature: 0.7, top_p: 0.95, max_tokens: 1000, system_prompt_id: 'default', message_prompt_id: 'standard', SIRI_2: 1.123, A_pharm: 1.11, A_mamh: 1.32 }, + + // Gemini versions + { modelFamily: 'Gemini', model: 'Gemini 2.5 Pro', version: '20250915', temperature: 0.7, top_p: 0.95, max_tokens: 1000, system_prompt_id: 'default', message_prompt_id: 'standard', SIRI_2: 1.023, A_pharm: 1.19, A_mamh: 1.31 }, + { modelFamily: 'Gemini', model: 'Gemini 2.5 Pro', version: '20250815', temperature: 0.7, top_p: 0.95, max_tokens: 1000, system_prompt_id: 'default', message_prompt_id: 'standard', SIRI_2: 1.045, A_pharm: 1.23, A_mamh: 1.35 }, + { modelFamily: 'Gemini', model: 'Gemini 2.5 Pro', version: '20250701', temperature: 0.7, top_p: 0.95, max_tokens: 1000, system_prompt_id: 'default', message_prompt_id: 'standard', SIRI_2: 1.067, A_pharm: 1.26, A_mamh: 1.38 }, + + // Gemini Flash versions + { modelFamily: 'Gemini', model: 'Gemini 2.0 Flash', version: '20250815', temperature: 0.7, top_p: 0.95, max_tokens: 1000, system_prompt_id: 'default', message_prompt_id: 'standard', SIRI_2: 1.201, A_pharm: 1.18, A_mamh: 1.29 }, + { modelFamily: 'Gemini', model: 'Gemini 2.0 Flash', version: '20250701', temperature: 0.7, top_p: 0.95, max_tokens: 1000, system_prompt_id: 'default', message_prompt_id: 'standard', SIRI_2: 1.234, A_pharm: 1.21, A_mamh: 1.32 }, + + // DeepSeek versions + { modelFamily: 'DeepSeek', model: 'DeepSeek-V3', version: '20250915', temperature: 0.7, top_p: 0.95, max_tokens: 1000, system_prompt_id: 'default', message_prompt_id: 'standard', SIRI_2: 1.342, A_pharm: 1.28, A_mamh: 1.45 }, + { modelFamily: 'DeepSeek', model: 'DeepSeek-V3', version: '20250701', temperature: 0.7, top_p: 0.95, max_tokens: 1000, system_prompt_id: 'default', message_prompt_id: 'standard', SIRI_2: 1.378, A_pharm: 1.31, A_mamh: 1.48 }, + + // GPT-4o Mini versions + { modelFamily: 'GPT', model: 'GPT-4o Mini', version: '20250815', temperature: 0.7, top_p: 0.95, max_tokens: 1000, system_prompt_id: 'default', message_prompt_id: 'standard', SIRI_2: 1.455, A_pharm: 1.38, A_mamh: 1.52 }, +]; + +function generateHyperparameterHash(config) { + const sortedConfig = JSON.stringify(config, Object.keys(config).sort()); + return crypto.createHash('md5').update(sortedConfig).digest('hex').substring(0, 16); +} + +module.exports = async function seedBenchmarkScoreAggregates(prisma, { benchmarking }) { + const { siriScale, aPharmScale, aMamhScale, promptMap } = benchmarking; + + console.log('Starting to seed BenchmarkScoreAggregates...'); + + let aggregatesCreated = 0; + let skipped = 0; + + for (const entry of leaderboardMockData) { + try { + // Look up model version by family, model name, and version + const modelVersion = await prisma.modelVersion.findFirst({ + where: { + version: entry.version, + model: { + name: entry.model, + modelFamily: { + name: entry.modelFamily, + }, + }, + }, + include: { + model: { + include: { + modelFamily: true, + }, + }, + }, + }); + + if (!modelVersion) { + console.warn(`Model version not found: ${entry.modelFamily} ${entry.model} ${entry.version}`); + skipped++; + continue; + } + + // Build hyperparameter config + const hyperparameterConfig = { + temperature: entry.temperature, + top_p: entry.top_p, + max_tokens: entry.max_tokens, + }; + const hyperparameterHash = generateHyperparameterHash(hyperparameterConfig); + + // Get prompt IDs + const systemPromptId = promptMap[entry.system_prompt_id]; + const messagePromptId = promptMap[entry.message_prompt_id]; + + // Create 3 score aggregates (one for each scale) + const scales = [ + { scale: siriScale, scoreKey: 'SIRI_2' }, + { scale: aPharmScale, scoreKey: 'A_pharm' }, + { scale: aMamhScale, scoreKey: 'A_mamh' }, + ]; + + for (const { scale, scoreKey } of scales) { + const rmseScore = entry[scoreKey]; + + if (rmseScore !== undefined) { + await prisma.benchmarkScoreAggregate.create({ + data: { + modelVersionId: modelVersion.id, + scaleId: scale.id, + hyperparameterHash, + hyperparameterConfig, + systemPromptId, + messagePromptId, + runCount: 5, // Mock: 5 runs per configuration + modelMean: rmseScore, + modelStd: rmseScore * 0.05, // Mock: 5% standard deviation + modelMin: rmseScore * 0.95, + modelMax: rmseScore * 1.05, + modelMedian: rmseScore, + expertConsensusMean: 7.5, // Mock expert consensus + rmseVsExperts: rmseScore, + firstRun: new Date('2025-01-01'), + lastRun: new Date('2025-01-15'), + experimentCount: 1, + lastRefreshed: new Date(), + }, + }); + aggregatesCreated++; + } + } + } catch (error) { + console.error(`Error creating aggregate for ${entry.modelFamily} ${entry.model} ${entry.version}:`, error.message); + skipped++; + } + } + + console.log(`Created ${aggregatesCreated} benchmark score aggregates (${skipped} skipped)`); +}; diff --git a/server/prisma/seeds/benchmarking.js b/server/prisma/seeds/benchmarking.js index 66ab13a..a2531bd 100644 --- a/server/prisma/seeds/benchmarking.js +++ b/server/prisma/seeds/benchmarking.js @@ -150,6 +150,69 @@ module.exports = async function seedBenchmarking(prisma, { researcherUser }) { console.log('Created sample benchmark questions for all scales'); + // Seed BenchmarkPrompts (system and message prompts) + const defaultSystemPrompt = await prisma.benchmarkPrompt.create({ + data: { + name: 'Default System Prompt', + promptType: 'system', + content: 'You are a helpful assistant.', + createdBy: researcherUser.id, + }, + }); + + const clinicalSystemPrompt = await prisma.benchmarkPrompt.create({ + data: { + name: 'Clinical System Prompt', + promptType: 'system', + content: 'You are a mental health support assistant.', + createdBy: researcherUser.id, + }, + }); + + const empatheticSystemPrompt = await prisma.benchmarkPrompt.create({ + data: { + name: 'Empathetic System Prompt', + promptType: 'system', + content: 'You are a compassionate listener.', + createdBy: researcherUser.id, + }, + }); + + const standardMessagePrompt = await prisma.benchmarkPrompt.create({ + data: { + name: 'Standard Format', + promptType: 'message', + content: 'Direct question format', + createdBy: researcherUser.id, + }, + }); + + const contextualMessagePrompt = await prisma.benchmarkPrompt.create({ + data: { + name: 'Contextual Format', + promptType: 'message', + content: 'Question with context', + createdBy: researcherUser.id, + }, + }); + + console.log('Created benchmark prompts:', + defaultSystemPrompt.name, + clinicalSystemPrompt.name, + empatheticSystemPrompt.name, + standardMessagePrompt.name, + contextualMessagePrompt.name + ); + + // Create prompt ID mapping for lookup + const promptMap = { + 'default': defaultSystemPrompt.id, + 'clinical': clinicalSystemPrompt.id, + 'empathetic': empatheticSystemPrompt.id, + 'standard': standardMessagePrompt.id, + 'contextual': contextualMessagePrompt.id, + }; + return { siriScale, aPharmScale, @@ -157,5 +220,6 @@ module.exports = async function seedBenchmarking(prisma, { researcherUser }) { aPharmQuestion, aMamhQuestion, siriQuestion, + promptMap, }; }; diff --git a/server/src/controllers/CurrentVersion/User/Benchmark.controller.ts b/server/src/controllers/CurrentVersion/User/Benchmark.controller.ts new file mode 100644 index 0000000..6350b8c --- /dev/null +++ b/server/src/controllers/CurrentVersion/User/Benchmark.controller.ts @@ -0,0 +1,283 @@ +import { Request, Response, NextFunction } from 'express'; +import { PrismaClient } from '../../../../prisma/generated/prisma'; + +const prisma = new PrismaClient(); + +interface HyperparameterConfig { + temperature?: number; + top_p?: number; + max_tokens?: number; + [key: string]: any; +} + +interface LeaderboardEntry { + id: string; + modelFamily: string; + model: string; + version: string; + temperature?: number; + top_p?: number; + max_tokens?: number; + system_prompt_id?: string; + message_prompt_id?: string; + SIRI_2?: number; + A_pharm?: number; + A_mamh?: number; +} + +/** + * Get leaderboard data with aggregated benchmark scores + * Returns ALL model versions (not just latest) to support frontend expand/collapse + * Supports filtering by model family, temperature, top_p, system/message prompts + */ +export const getLeaderboardData = async ( + req: Request, + res: Response, + next: NextFunction +): Promise => { + try { + // Extract filter parameters from query + const { + modelFamily, + temperature, + top_p, + system_prompt_id, + message_prompt_id, + } = req.query; + + // Build where clause for filters + const whereClause: any = {}; + + // Apply model family filter if provided + if (modelFamily) { + whereClause.modelVersion = { + model: { + modelFamily: { + name: modelFamily as string, + }, + }, + }; + } + + // Apply hyperparameter filters if provided + if (temperature || top_p) { + const hyperparameterFilters: any = {}; + if (temperature) { + hyperparameterFilters.path = ['temperature']; + hyperparameterFilters.equals = parseFloat(temperature as string); + } + // Note: Filtering on multiple JSON fields simultaneously is complex in Prisma + // We'll apply these filters in-memory after fetching + } + + // Apply prompt filters + if (system_prompt_id) { + whereClause.systemPromptId = system_prompt_id as string; + } + if (message_prompt_id) { + whereClause.messagePromptId = message_prompt_id as string; + } + + // Fetch all benchmark score aggregates with related data + const aggregates = await prisma.benchmarkScoreAggregate.findMany({ + where: whereClause, + include: { + modelVersion: { + include: { + model: { + include: { + modelFamily: true, + }, + }, + }, + }, + scale: true, + systemPrompt: true, + messagePrompt: true, + }, + orderBy: [ + { modelVersion: { model: { modelFamily: { name: 'asc' } } } }, + { modelVersion: { model: { name: 'asc' } } }, + { modelVersion: { version: 'desc' } }, + ], + }); + + // Group aggregates by unique configuration + // Key: modelVersionId + hyperparameterHash + systemPromptId + messagePromptId + const groupedData = new Map(); + + for (const aggregate of aggregates) { + const modelVersion = aggregate.modelVersion; + const model = modelVersion.model; + const modelFamilyName = model.modelFamily?.name || 'Unknown'; + const scaleName = aggregate.scale.name; + + // Extract hyperparameters from config JSON + const hyperparameterConfig = (aggregate.hyperparameterConfig as HyperparameterConfig) || {}; + const temp = hyperparameterConfig.temperature; + const topP = hyperparameterConfig.top_p; + const maxTokens = hyperparameterConfig.max_tokens; + + // Apply in-memory filters for hyperparameters + if (temperature && temp !== parseFloat(temperature as string)) continue; + if (top_p && topP !== parseFloat(top_p as string)) continue; + + // Create unique key for grouping + const groupKey = `${modelVersion.id}-${aggregate.hyperparameterHash || 'default'}-${aggregate.systemPromptId || 'none'}-${aggregate.messagePromptId || 'none'}`; + + // Initialize entry if it doesn't exist + if (!groupedData.has(groupKey)) { + groupedData.set(groupKey, { + id: groupKey, + modelFamily: modelFamilyName, + model: model.name, + version: modelVersion.version, + temperature: temp, + top_p: topP, + max_tokens: maxTokens, + system_prompt_id: aggregate.systemPromptId || undefined, + message_prompt_id: aggregate.messagePromptId || undefined, + }); + } + + const entry = groupedData.get(groupKey)!; + + // Map scale names to leaderboard column names + // Scale names from seed: "SIRI-2", "A-Pharm", "A-MaMH" + // Frontend expects: SIRI_2, A_pharm, A_mamh + const scaleMapping: Record = { + 'SIRI-2': 'SIRI_2', + 'A-Pharm': 'A_pharm', + 'A-MaMH': 'A_mamh', + }; + + const mappedKey = scaleMapping[scaleName]; + if (mappedKey) { + entry[mappedKey] = aggregate.rmseVsExperts ?? undefined; + } + } + + // Convert map to array + const leaderboardData = Array.from(groupedData.values()); + + res.json({ + success: true, + data: leaderboardData, + count: leaderboardData.length, + }); + } catch (error) { + console.error('Error fetching leaderboard data:', error); + next(error); + } +}; + +/** + * Get available system prompts for filtering + * Returns prompts in format: { id, name, content } + */ +export const getSystemPrompts = async ( + req: Request, + res: Response, + next: NextFunction +): Promise => { + try { + const prompts = await prisma.benchmarkPrompt.findMany({ + where: { + promptType: 'system', + }, + select: { + id: true, + name: true, + content: true, + }, + orderBy: { + name: 'asc', + }, + }); + + res.json({ + success: true, + data: prompts, + }); + } catch (error) { + console.error('Error fetching system prompts:', error); + next(error); + } +}; + +/** + * Get available message prompts for filtering + * Returns prompts in format: { id, name, content } + */ +export const getMessagePrompts = async ( + req: Request, + res: Response, + next: NextFunction +): Promise => { + try { + const prompts = await prisma.benchmarkPrompt.findMany({ + where: { + promptType: 'message', + }, + select: { + id: true, + name: true, + content: true, + }, + orderBy: { + name: 'asc', + }, + }); + + res.json({ + success: true, + data: prompts, + }); + } catch (error) { + console.error('Error fetching message prompts:', error); + next(error); + } +}; + +/** + * Get available model families with their models + * Returns in format compatible with frontend expectations + */ +export const getModelFamilies = async ( + req: Request, + res: Response, + next: NextFunction +): Promise => { + try { + const families = await prisma.modelFamily.findMany({ + include: { + models: { + select: { + id: true, + name: true, + }, + orderBy: { + name: 'asc', + }, + }, + }, + orderBy: { + name: 'asc', + }, + }); + + // Transform to object format: { 'GPT': ['GPT-4o', 'GPT-3.5 Turbo'], ... } + const familiesObject: Record = {}; + families.forEach(family => { + familiesObject[family.name] = family.models.map(model => model.name); + }); + + res.json({ + success: true, + data: familiesObject, + }); + } catch (error) { + console.error('Error fetching model families:', error); + next(error); + } +}; diff --git a/server/src/routes/CurrentVersion/index.ts b/server/src/routes/CurrentVersion/index.ts index c6dc2fe..f05a130 100644 --- a/server/src/routes/CurrentVersion/index.ts +++ b/server/src/routes/CurrentVersion/index.ts @@ -17,6 +17,12 @@ import { getSuggestions, getTeamMembers, } from '../../controllers/CurrentVersion/User/Community.controller'; +import { + getLeaderboardData, + getSystemPrompts, + getMessagePrompts, + getModelFamilies, +} from '../../controllers/CurrentVersion/User/Benchmark.controller'; const router: Router = express.Router(); @@ -41,4 +47,10 @@ router.get('/community/updates', optionalAuth, getUpdates); router.get('/community/suggestions', optionalAuth, getSuggestions); router.get('/community/team-members', optionalAuth, getTeamMembers); +// Benchmark/Leaderboard routes +router.get('/leaderboard', optionalAuth, getLeaderboardData); +router.get('/leaderboard/system-prompts', optionalAuth, getSystemPrompts); +router.get('/leaderboard/message-prompts', optionalAuth, getMessagePrompts); +router.get('/leaderboard/model-families', optionalAuth, getModelFamilies); + export default router;
KeepModel FamilyModelVersion -
+
KeepModel FamilyModelVersion +
SIRI-2 @@ -680,8 +772,8 @@ export default function Leaderboard() {
-
+
+
A-Pharm @@ -695,8 +787,8 @@ export default function Leaderboard() {
-
+
+
A-MaMH @@ -715,18 +807,18 @@ export default function Leaderboard() {
+ toggleVersion(v.id)} /> {v.modelFamily}{v.model}{v.version}{typeof v.SIRI_2 === "number" ? v.SIRI_2.toFixed(3) : v.SIRI_2}{typeof v.A_pharm === "number" ? v.A_pharm.toFixed(3) : v.A_pharm}{typeof v.A_mamh === "number" ? v.A_mamh.toFixed(3) : v.A_mamh}{v.modelFamily}{v.model}{v.version}{typeof v.SIRI_2 === "number" ? v.SIRI_2.toFixed(3) : v.SIRI_2}{typeof v.A_pharm === "number" ? v.A_pharm.toFixed(3) : v.A_pharm}{typeof v.A_mamh === "number" ? v.A_mamh.toFixed(3) : v.A_mamh}