diff --git a/client/package-lock.json b/client/package-lock.json index 5e0e074..950e6e8 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -9,8 +9,13 @@ "version": "0.0.0", "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", "@tanstack/react-table": "^8.21.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -962,6 +967,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 +1096,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 +1158,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 +1259,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 +1344,126 @@ } } }, + "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", + "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 +1542,113 @@ } } }, + "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-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", @@ -1301,6 +1697,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 +1783,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 +1834,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 +2761,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 +3172,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 +3579,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 +4410,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 +4495,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 +4892,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 +4925,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..2307521 100644 --- a/client/package.json +++ b/client/package.json @@ -11,8 +11,13 @@ }, "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", "@tanstack/react-table": "^8.21.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/client/src/assets/MindBenchAI.png b/client/src/assets/MindBenchAI.png new file mode 100644 index 0000000..22d0050 Binary files /dev/null and b/client/src/assets/MindBenchAI.png differ 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 */} +
+
+ 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/Leaderboard.tsx b/client/src/components/Leaderboard.tsx index 7d0784f..09c0ce3 100644 --- a/client/src/components/Leaderboard.tsx +++ b/client/src/components/Leaderboard.tsx @@ -1,29 +1,24 @@ -import "../styles/Leaderboard.css"; -import { useState, useMemo, useRef, useEffect, MouseEvent, KeyboardEvent } from "react"; +import { useState, useMemo, useEffect } from "react"; import type { ChangeEvent } from "react"; import { useReactTable, getCoreRowModel, + getSortedRowModel, getFilteredRowModel, flexRender, ColumnDef, SortingState, - ColumnFiltersState, - Row, } from "@tanstack/react-table"; -import { - getLatestVersions, - modelVersions, - filterVersions, - systemPrompts, - messagePrompts, - modelFamilies -} from "../data/leaderboardData"; - -interface InfoBubbleProps { - title: string; - content: string; -} +import { ChevronDown, ChevronRight, Search, Info } from "lucide-react"; +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"; + +const API_BASE = import.meta.env.VITE_API_URL ?? 'http://localhost:5001/api'; interface ModelVersion { id: string; @@ -79,55 +74,15 @@ 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}
-
- )} -
- ); +interface Prompt { + id: string; + name: string; + content: string; } 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()); @@ -138,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); @@ -148,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, @@ -194,29 +214,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]); + }, [modelVersions, temperatureFilter, topPFilter, systemPromptFilter, messagePromptFilter, modelFamilyFilter]); const data = useMemo(() => { const rows: TableRow[] = []; @@ -263,18 +267,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,45 +288,61 @@ export default function Leaderboard() { const isOpen = expandedModels.has(row.original.id); return ( ); }, }, { accessorKey: "modelFamily", - header: () => "Model Family", - cell: (info) => info.getValue() + header: () =>
Model Family
, + cell: ({ row, getValue }) => { + if (!row.original.isMainRow) return null; + return {String(getValue())}; + } }, { accessorKey: "model", - header: () => "Model", - cell: (info) => info.getValue() + header: () =>
Model
, + cell: ({ row, getValue }) => { + if (!row.original.isMainRow) return null; + return String(getValue()); + } }, { accessorKey: "version", - header: () => "Version", - cell: (info) => info.getValue(), - size: 100 + header: () =>
Version
, + 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 +353,18 @@ export default function Leaderboard() { { accessorKey: "A_pharm", header: () => ( -
+
A-Pharm - + + + + + +

RMSE — lower is better. Pharmacology subset performance

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

RMSE — lower is better. Math & reasoning subset performance

+
+
), cell: ({ getValue }) => { @@ -371,14 +401,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(); @@ -390,292 +418,419 @@ 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 */} +
+
+

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

+
+
+
+
+
+ A-Pharm + + + + + +

RMSE — lower is better. Pharmacology subset performance

+
+
+
- {flexRender(cell.column.columnDef.cell, cell.getContext())} - +
+ 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/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/Resources.tsx b/client/src/components/Resources.tsx index 42323c7..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,10 +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); @@ -125,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(() => { @@ -151,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": @@ -164,22 +273,96 @@ 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 */} -
-
-
-

Benchmark Studies & Resources

-

- Explore the original research papers and documentation for various mental health and medical benchmarks. - Each study provides detailed methodology, datasets, and evaluation criteria used to assess language model - performance in mental health contexts. -

-
+
+
+

Benchmark Studies & Resources

+

+ Explore the original research papers and documentation for various mental health and medical benchmarks +

+ {/* Tabs */} +
+
+
+ + +
+
+ + {activeTab === 'studies' && ( +
{/* Content */}
{/* Filters */} @@ -346,8 +529,121 @@ export default function Resources() { )}
)} +
+
+ )} + + {activeTab === 'articles' && ( +
+
+ {/* 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.

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

Ready to Compare Models?

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/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/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/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/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 }; 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/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/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 c96bf4b..27ff8d3 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 */ @@ -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/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/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..f05a130 100644 --- a/server/src/routes/CurrentVersion/index.ts +++ b/server/src/routes/CurrentVersion/index.ts @@ -9,12 +9,20 @@ import { import { getResourceBenchmarks, getResourceBenchmarkById, + getResourceArticles, + getResourceArticleById, } from '../../controllers/CurrentVersion/User/Resource.controller'; import { getUpdates, getSuggestions, getTeamMembers, } from '../../controllers/CurrentVersion/User/Community.controller'; +import { + getLeaderboardData, + getSystemPrompts, + getMessagePrompts, + getModelFamilies, +} from '../../controllers/CurrentVersion/User/Benchmark.controller'; const router: Router = express.Router(); @@ -30,9 +38,19 @@ 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); 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;