diff --git a/package-lock.json b/package-lock.json index c07de6c..bad8aa9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "autoprefixer": "^10.4.21", "chart.js": "^4.5.0", "chartjs-plugin-zoom": "^2.2.0", + "firebase": "^12.0.0", "lucide-react": "^0.522.0", "postcss": "^8.5.6", "react": "^19.1.0", @@ -1097,6 +1098,645 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@firebase/ai": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@firebase/ai/-/ai-2.0.0.tgz", + "integrity": "sha512-N/aSHjqOpU+KkYU3piMkbcuxzvqsOvxflLUXBAkYAPAz8wjE2Ye3BQDgKHEYuhMmEWqj6LFgEBUN8wwc6dfMTw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/analytics": { + "version": "0.10.18", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.18.tgz", + "integrity": "sha512-iN7IgLvM06iFk8BeFoWqvVpRFW3Z70f+Qe2PfCJ7vPIgLPjHXDE774DhCT5Y2/ZU/ZbXPDPD60x/XPWEoZLNdg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/analytics-compat": { + "version": "0.2.24", + "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.24.tgz", + "integrity": "sha512-jE+kJnPG86XSqGQGhXXYt1tpTbCTED8OQJ/PQ90SEw14CuxRxx/H+lFbWA1rlFtFSsTCptAJtgyRBwr/f00vsw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/analytics": "0.10.18", + "@firebase/analytics-types": "0.8.3", + "@firebase/component": "0.7.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/analytics-types": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.3.tgz", + "integrity": "sha512-VrIp/d8iq2g501qO46uGz3hjbDb8xzYMrbu8Tp0ovzIzrvJZ2fvmj649gTjge/b7cCCcjT0H37g1gVtlNhnkbg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.0.tgz", + "integrity": "sha512-APIAeKvRNFWKJLjIL8wLDjh7u8g6ZjaeVmItyqSjCdEkJj14UuVlus74D8ofsOMWh45HEwxwkd96GYbi+CImEg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/app-check": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.11.0.tgz", + "integrity": "sha512-XAvALQayUMBJo58U/rxW02IhsesaxxfWVmVkauZvGEz3vOAjMEQnzFlyblqkc2iAaO82uJ2ZVyZv9XzPfxjJ6w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/app-check-compat": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.4.0.tgz", + "integrity": "sha512-UfK2Q8RJNjYM/8MFORltZRG9lJj11k0nW84rrffiKvcJxLf1jf6IEjCIkCamykHE73C6BwqhVfhIBs69GXQV0g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check": "0.11.0", + "@firebase/app-check-types": "0.5.3", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.3.tgz", + "integrity": "sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-check-types": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.3.tgz", + "integrity": "sha512-hyl5rKSj0QmwPdsAxrI5x1otDlByQ7bvNvVt8G/XPO2CSwE++rmSVf3VEhaeOR4J8ZFaF0Z0NDSmLejPweZ3ng==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-compat": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.0.tgz", + "integrity": "sha512-nUnNpOeRj0KZzVzHsyuyrmZKKHfykZ8mn40FtG28DeSTWeM5b/2P242Va4bmQpJsy5y32vfv50+jvdckrpzy7Q==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app": "0.14.0", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/app-types": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz", + "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/auth": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.11.0.tgz", + "integrity": "sha512-5j7+ua93X+IRcJ1oMDTClTo85l7Xe40WSkoJ+shzPrX7OISlVWLdE1mKC57PSD+/LfAbdhJmvKixINBw2ESK6w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@react-native-async-storage/async-storage": "^1.18.1" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/@firebase/auth-compat": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.6.0.tgz", + "integrity": "sha512-J0lGSxXlG/lYVi45wbpPhcWiWUMXevY4fvLZsN1GHh+po7TZVng+figdHBVhFheaiipU8HZyc7ljw1jNojM2nw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth": "1.11.0", + "@firebase/auth-types": "0.13.0", + "@firebase/component": "0.7.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.4.tgz", + "integrity": "sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/auth-types": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.13.0.tgz", + "integrity": "sha512-S/PuIjni0AQRLF+l9ck0YpsMOdE8GO2KU6ubmBB7P+7TJUCQDa3R1dlgYm9UzGbbePMZsp0xzB93f2b/CgxMOg==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/component": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.7.0.tgz", + "integrity": "sha512-wR9En2A+WESUHexjmRHkqtaVH94WLNKt6rmeqZhSLBybg4Wyf0Umk04SZsS6sBq4102ZsDBFwoqMqJYj2IoDSg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/data-connect": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@firebase/data-connect/-/data-connect-0.3.11.tgz", + "integrity": "sha512-G258eLzAD6im9Bsw+Qm1Z+P4x0PGNQ45yeUuuqe5M9B1rn0RJvvsQCRHXgE52Z+n9+WX1OJd/crcuunvOGc7Vw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/database": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.1.0.tgz", + "integrity": "sha512-gM6MJFae3pTyNLoc9VcJNuaUDej0ctdjn3cVtILo3D5lpp0dmUHHLFN/pUKe7ImyeB1KAvRlEYxvIHNF04Filg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.1.0.tgz", + "integrity": "sha512-8nYc43RqxScsePVd1qe1xxvWNf0OBnbwHxmXJ7MHSuuTVYFO3eLyLW3PiCKJ9fHnmIz4p4LbieXwz+qtr9PZDg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/database": "1.1.0", + "@firebase/database-types": "1.0.16", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.16.tgz", + "integrity": "sha512-xkQLQfU5De7+SPhEGAXFBnDryUWhhlFXelEg2YeZOQMCdoe7dL64DDAd77SQsR+6uoXIZY5MB4y/inCs4GTfcw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-types": "0.9.3", + "@firebase/util": "1.13.0" + } + }, + "node_modules/@firebase/firestore": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.9.0.tgz", + "integrity": "sha512-5zl0+/h1GvlCSLt06RMwqFsd7uqRtnNZt4sW99k2rKRd6k/ECObIWlEnvthm2cuOSnUmwZknFqtmd1qyYSLUuQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "@firebase/webchannel-wrapper": "1.0.4", + "@grpc/grpc-js": "~1.9.0", + "@grpc/proto-loader": "^0.7.8", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/firestore-compat": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.4.0.tgz", + "integrity": "sha512-4O7v4VFeSEwAZtLjsaj33YrMHMRjplOIYC2CiYsF6o/MboOhrhe01VrTt8iY9Y5EwjRHuRz4pS6jMBT8LfQYJA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/firestore": "4.9.0", + "@firebase/firestore-types": "3.0.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/firestore-types": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-3.0.3.tgz", + "integrity": "sha512-hD2jGdiWRxB/eZWF89xcK9gF8wvENDJkzpVFb4aGkzfEaKxVRD1kjz1t1Wj8VZEp2LCB53Yx1zD8mrhQu87R6Q==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/functions": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.13.0.tgz", + "integrity": "sha512-2/LH5xIbD8aaLOWSFHAwwAybgSzHIM0dB5oVOL0zZnxFG1LctX2bc1NIAaPk1T+Zo9aVkLKUlB5fTXTkVUQprQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.7.0", + "@firebase/messaging-interop-types": "0.2.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/functions-compat": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.4.0.tgz", + "integrity": "sha512-VPgtvoGFywWbQqtvgJnVWIDFSHV1WE6Hmyi5EGI+P+56EskiGkmnw6lEqc/MEUfGpPGdvmc4I9XMU81uj766/g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/functions": "0.13.0", + "@firebase/functions-types": "0.6.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/functions-types": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.3.tgz", + "integrity": "sha512-EZoDKQLUHFKNx6VLipQwrSMh01A1SaL3Wg6Hpi//x6/fJ6Ee4hrAeswK99I5Ht8roiniKHw4iO0B1Oxj5I4plg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/installations": { + "version": "0.6.19", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.19.tgz", + "integrity": "sha512-nGDmiwKLI1lerhwfwSHvMR9RZuIH5/8E3kgUWnVRqqL7kGVSktjLTWEMva7oh5yxQ3zXfIlIwJwMcaM5bK5j8Q==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/util": "1.13.0", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/installations-compat": { + "version": "0.2.19", + "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.19.tgz", + "integrity": "sha512-khfzIY3EI5LePePo7vT19/VEIH1E3iYsHknI/6ek9T8QCozAZshWT9CjlwOzZrKvTHMeNcbpo/VSOSIWDSjWdQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", + "@firebase/installations-types": "0.5.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/installations-types": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.3.tgz", + "integrity": "sha512-2FJI7gkLqIE0iYsNQ1P751lO3hER+Umykel+TkLwHj6plzWVxqvfclPUZhcKFVQObqloEBTmpi2Ozn7EkCABAA==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/logger": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.5.0.tgz", + "integrity": "sha512-cGskaAvkrnh42b3BA3doDWeBmuHFO/Mx5A83rbRDYakPjO9bJtRL3dX7javzc2Rr/JHZf4HlterTW2lUkfeN4g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/messaging": { + "version": "0.12.23", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.23.tgz", + "integrity": "sha512-cfuzv47XxqW4HH/OcR5rM+AlQd1xL/VhuaeW/wzMW1LFrsFcTn0GND/hak1vkQc2th8UisBcrkVcQAnOnKwYxg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", + "@firebase/messaging-interop-types": "0.2.3", + "@firebase/util": "1.13.0", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/messaging-compat": { + "version": "0.2.23", + "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.23.tgz", + "integrity": "sha512-SN857v/kBUvlQ9X/UjAqBoQ2FEaL1ZozpnmL1ByTe57iXkmnVVFm9KqAsTfmf+OEwWI4kJJe9NObtN/w22lUgg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/messaging": "0.12.23", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/messaging-interop-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.3.tgz", + "integrity": "sha512-xfzFaJpzcmtDjycpDeCUj0Ge10ATFi/VHVIvEEjDNc3hodVBQADZ7BWQU7CuFpjSHE+eLuBI13z5F/9xOoGX8Q==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/performance": { + "version": "0.7.8", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.7.8.tgz", + "integrity": "sha512-k6xfNM/CdTl4RaV4gT/lH53NU+wP33JiN0pUeNBzGVNvfXZ3HbCkoISE3M/XaiOwHgded1l6XfLHa4zHgm0Wyg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0", + "web-vitals": "^4.2.4" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/performance-compat": { + "version": "0.2.21", + "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.21.tgz", + "integrity": "sha512-OQfYRsIQiEf9ez1SOMLb5TRevBHNIyA2x1GI1H10lZ432W96AK5r4LTM+SNApg84dxOuHt6RWSQWY7TPWffKXg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/performance": "0.7.8", + "@firebase/performance-types": "0.2.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/performance-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.3.tgz", + "integrity": "sha512-IgkyTz6QZVPAq8GSkLYJvwSLr3LS9+V6vNPQr0x4YozZJiLF5jYixj0amDtATf1X0EtYHqoPO48a9ija8GocxQ==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/remote-config": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.6.6.tgz", + "integrity": "sha512-Yelp5xd8hM4NO1G1SuWrIk4h5K42mNwC98eWZ9YLVu6Z0S6hFk1mxotAdCRmH2luH8FASlYgLLq6OQLZ4nbnCA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/remote-config-compat": { + "version": "0.2.19", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.19.tgz", + "integrity": "sha512-y7PZAb0l5+5oIgLJr88TNSelxuASGlXyAKj+3pUc4fDuRIdPNBoONMHaIUa9rlffBR5dErmaD2wUBJ7Z1a513Q==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/remote-config": "0.6.6", + "@firebase/remote-config-types": "0.4.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/remote-config-types": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.4.0.tgz", + "integrity": "sha512-7p3mRE/ldCNYt8fmWMQ/MSGRmXYlJ15Rvs9Rk17t8p0WwZDbeK7eRmoI1tvCPaDzn9Oqh+yD6Lw+sGLsLg4kKg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/storage": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.14.0.tgz", + "integrity": "sha512-xWWbb15o6/pWEw8H01UQ1dC5U3rf8QTAzOChYyCpafV6Xki7KVp3Yaw2nSklUwHEziSWE9KoZJS7iYeyqWnYFA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/storage-compat": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.4.0.tgz", + "integrity": "sha512-vDzhgGczr1OfcOy285YAPur5pWDEvD67w4thyeCUh6Ys0izN9fNYtA1MJERmNBfqjqu0lg0FM5GLbw0Il21M+g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/storage": "0.14.0", + "@firebase/storage-types": "0.8.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/storage-types": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.3.tgz", + "integrity": "sha512-+Muk7g9uwngTpd8xn9OdF/D48uiQ7I1Fae7ULsWPuKoCH3HU7bfFPhxtJYzyhjdniowhuDpQcfPmuNRAqZEfvg==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/util": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.13.0.tgz", + "integrity": "sha512-0AZUyYUfpMNcztR5l09izHwXkZpghLgCUaAGjtMwXnCg3bj4ml5VgiwqOMOxJ+Nw4qN/zJAaOQBcJ7KGkWStqQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/webchannel-wrapper": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.4.tgz", + "integrity": "sha512-6m8+P+dE/RPl4OPzjTxcTbQ0rGeRyeTvAi9KwIffBVCiAMKrfXfLZaqD1F+m8t4B5/Q5aHsMozOgirkH1F5oMQ==", + "license": "Apache-2.0" + }, + "node_modules/@grpc/grpc-js": { + "version": "1.9.15", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.15.tgz", + "integrity": "sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.7.8", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1302,6 +1942,70 @@ "node": ">=14" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.19", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.19.tgz", @@ -1835,6 +2539,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.0.tgz", + "integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.10.0" + } + }, "node_modules/@types/react": { "version": "19.1.8", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", @@ -2439,6 +3152,78 @@ "node": ">= 6" } }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3098,6 +3883,18 @@ "reusify": "^1.0.4" } }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -3140,6 +3937,42 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/firebase": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-12.0.0.tgz", + "integrity": "sha512-KV+OrMJpi2uXlqL2zaCcXb7YuQbY/gMIWT1hf8hKeTW1bSumWaHT5qfmn0WTpHwKQa3QEVOtZR2ta9EchcmYuw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/ai": "2.0.0", + "@firebase/analytics": "0.10.18", + "@firebase/analytics-compat": "0.2.24", + "@firebase/app": "0.14.0", + "@firebase/app-check": "0.11.0", + "@firebase/app-check-compat": "0.4.0", + "@firebase/app-compat": "0.5.0", + "@firebase/app-types": "0.9.3", + "@firebase/auth": "1.11.0", + "@firebase/auth-compat": "0.6.0", + "@firebase/data-connect": "0.3.11", + "@firebase/database": "1.1.0", + "@firebase/database-compat": "2.1.0", + "@firebase/firestore": "4.9.0", + "@firebase/firestore-compat": "0.4.0", + "@firebase/functions": "0.13.0", + "@firebase/functions-compat": "0.4.0", + "@firebase/installations": "0.6.19", + "@firebase/installations-compat": "0.2.19", + "@firebase/messaging": "0.12.23", + "@firebase/messaging-compat": "0.2.23", + "@firebase/performance": "0.7.8", + "@firebase/performance-compat": "0.2.21", + "@firebase/remote-config": "0.6.6", + "@firebase/remote-config-compat": "0.2.19", + "@firebase/storage": "0.14.0", + "@firebase/storage-compat": "0.4.0", + "@firebase/util": "1.13.0" + } + }, "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", @@ -3247,6 +4080,15 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-func-name": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", @@ -3471,6 +4313,12 @@ "dev": true, "license": "MIT" }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "license": "MIT" + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -3522,6 +4370,12 @@ "node": ">=0.10.0" } }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "license": "ISC" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3948,6 +4802,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -3955,6 +4815,12 @@ "dev": true, "license": "MIT" }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/loupe": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", @@ -4721,6 +5587,30 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/protobufjs": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.3.tgz", + "integrity": "sha512-sildjKwVqOI2kmFDiXQ6aEB0fjYTafpEvIBs8tOR8qI4spuL9OPROLVu2qZqi/xgCfsHIwVqlaF8JBjWFHnKbw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/psl": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", @@ -4854,6 +5744,15 @@ "node": ">=8" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -4971,6 +5870,26 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -5493,6 +6412,12 @@ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", "license": "Apache-2.0" }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -5523,6 +6448,12 @@ "dev": true, "license": "MIT" }, + "node_modules/undici-types": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "license": "MIT" + }, "node_modules/universalify": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", @@ -6775,6 +7706,12 @@ "node": ">=18" } }, + "node_modules/web-vitals": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", + "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==", + "license": "Apache-2.0" + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -6785,6 +7722,29 @@ "node": ">=12" } }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/whatwg-encoding": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", @@ -6998,6 +7958,15 @@ "dev": true, "license": "MIT" }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -7017,6 +7986,74 @@ "node": ">= 14.6" } }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index b8b0f28..1429ab2 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "autoprefixer": "^10.4.21", "chart.js": "^4.5.0", "chartjs-plugin-zoom": "^2.2.0", + "firebase": "^12.0.0", "lucide-react": "^0.522.0", "postcss": "^8.5.6", "react": "^19.1.0", diff --git a/src/App.jsx b/src/App.jsx index fce2a15..f9e683e 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,4 +1,29 @@ import React, { useState, useCallback, useEffect } from 'react'; + +// Helper to load state from localStorage +const loadState = () => { + try { + const serializedState = localStorage.getItem('workspaceV1'); + if (serializedState === null) return undefined; + const state = JSON.parse(serializedState); + // Files content is not persisted, just their configs and names. + // User will be prompted to re-upload if they want to see the charts. + if (state.uploadedFiles) { + state.uploadedFiles = state.uploadedFiles.map(file => ({ + ...file, + file: null, + content: null, + data: null, + })); + } + return state; + } catch (err) { + console.warn("Could not load state from localStorage", err); + return undefined; + } +}; + +const persistedState = loadState(); import { FileUpload } from './components/FileUpload'; import { RegexControls } from './components/RegexControls'; import { FileList } from './components/FileList'; @@ -8,12 +33,15 @@ import { Header } from './components/Header'; import { FileConfigModal } from './components/FileConfigModal'; import { PanelLeftClose, PanelLeftOpen } from 'lucide-react'; import { mergeFilesWithReplacement } from './utils/mergeFiles.js'; +import { signInWithGoogle, doSignOut, onAuthChange } from './services/authService.js'; +import { saveWorkspace, loadWorkspace } from './services/workspaceService.js'; function App() { - const [uploadedFiles, setUploadedFiles] = useState([]); + const [currentUser, setCurrentUser] = useState(null); + const [uploadedFiles, setUploadedFiles] = useState(persistedState?.uploadedFiles || []); // 全局解析配置状态 - const [globalParsingConfig, setGlobalParsingConfig] = useState({ + const [globalParsingConfig, setGlobalParsingConfig] = useState(persistedState?.globalParsingConfig || { metrics: [ { name: 'Loss', @@ -30,16 +58,107 @@ function App() { ] }); - const [compareMode, setCompareMode] = useState('normal'); - const [relativeBaseline, setRelativeBaseline] = useState(0.002); - const [absoluteBaseline, setAbsoluteBaseline] = useState(0.005); + const [compareMode, setCompareMode] = useState(persistedState?.compareMode || 'normal'); + const [relativeBaseline, setRelativeBaseline] = useState(persistedState?.relativeBaseline || 0.002); + const [absoluteBaseline, setAbsoluteBaseline] = useState(persistedState?.absoluteBaseline || 0.005); const [configModalOpen, setConfigModalOpen] = useState(false); const [configFile, setConfigFile] = useState(null); const [globalDragOver, setGlobalDragOver] = useState(false); const [, setDragCounter] = useState(0); - const [xRange, setXRange] = useState({ min: undefined, max: undefined }); + const [xRange, setXRange] = useState(persistedState?.xRange || { min: undefined, max: undefined }); const [maxStep, setMaxStep] = useState(0); - const [sidebarVisible, setSidebarVisible] = useState(true); + const [sidebarVisible, setSidebarVisible] = useState(persistedState?.sidebarVisible !== undefined ? persistedState.sidebarVisible : true); + + const applyWorkspace = (workspace) => { + if (!workspace) return; + + // Clear local storage if we are loading from the cloud + localStorage.removeItem('workspaceV1'); + + const files = workspace.uploadedFiles || []; + // Ensure files are in the correct format (without content) + setUploadedFiles(files.map(f => ({ ...f, file: null, content: null, data: null }))); + + setGlobalParsingConfig(workspace.globalParsingConfig || { metrics: [] }); + setCompareMode(workspace.compareMode || 'normal'); + setRelativeBaseline(workspace.relativeBaseline || 0.002); + setAbsoluteBaseline(workspace.absoluteBaseline || 0.005); + setXRange(workspace.xRange || { min: undefined, max: undefined }); + setSidebarVisible(workspace.sidebarVisible !== undefined ? workspace.sidebarVisible : true); + }; + + // Effect for saving state + useEffect(() => { + const stateToSave = { + uploadedFiles: uploadedFiles.map(f => ({ + id: f.id, + name: f.name, + enabled: f.enabled, + config: f.config, + })), + globalParsingConfig, + compareMode, + relativeBaseline, + absoluteBaseline, + xRange, + sidebarVisible, + }; + + if (currentUser) { + saveWorkspace(currentUser.uid, stateToSave) + .catch(err => console.warn("Could not save workspace to Firestore", err)); + } else { + try { + const serializedState = JSON.stringify(stateToSave); + localStorage.setItem('workspaceV1', serializedState); + } catch (err) { + console.warn("Could not save state to localStorage", err); + } + } + }, [ + currentUser, + uploadedFiles, + globalParsingConfig, + compareMode, + relativeBaseline, + absoluteBaseline, + xRange, + sidebarVisible + ]); + + // Effect for handling auth changes and loading data + useEffect(() => { + const unsubscribe = onAuthChange(async (user) => { + if (user) { + const workspace = await loadWorkspace(user.uid); + console.log("User signed in. Workspace from cloud:", workspace); + if (workspace) { + applyWorkspace(workspace); + } + setCurrentUser(user); + } else { + console.log("User signed out."); + setCurrentUser(null); + } + }); + return () => unsubscribe(); + }, []); + + const handleLogin = async () => { + try { + await signInWithGoogle(); + } catch (error) { + console.error("Error signing in with Google", error); + } + }; + + const handleLogout = async () => { + try { + await doSignOut(); + } catch (error) { + console.error("Error signing out", error); + } + }; const handleFilesUploaded = useCallback((files) => { const filesWithDefaults = files.map(file => ({ @@ -300,6 +419,22 @@ function App() { GitHub + {currentUser ? ( + + ) : ( + + )} diff --git a/src/components/RegexControls.jsx b/src/components/RegexControls.jsx index 971cce9..b51f83a 100644 --- a/src/components/RegexControls.jsx +++ b/src/components/RegexControls.jsx @@ -1,201 +1,15 @@ import React, { useState, useEffect, useCallback } from 'react'; -import { Settings, Zap, Eye, ChevronDown, ChevronUp, Target, Code, ZoomIn } from 'lucide-react'; +import { Settings, Zap, Eye, ZoomIn } from 'lucide-react'; import { METRIC_PRESETS } from '../metricPresets.js'; - -// 匹配模式枚举 -const MATCH_MODES = { - KEYWORD: 'keyword', - REGEX: 'regex' -}; - -// 模式配置 -const MODE_CONFIG = { - [MATCH_MODES.KEYWORD]: { - name: '关键词匹配', - icon: Target, - description: '输入关键词,自动查找并提取数值', - example: '输入 "loss" 匹配 "loss: 0.123"' - }, - [MATCH_MODES.REGEX]: { - name: '正则表达式', - icon: Code, - description: '使用正则表达式进行高级匹配', - example: 'loss:\\s*([\\d.eE+-]+)' - } -}; +import { extractorPlugins, extractorPluginOrder } from '../plugins/pluginRegistry.js'; // 根据配置生成友好的标题 function getMetricTitle(metric, index) { if (metric.name && metric.name.trim()) return metric.name.trim(); - if (metric.keyword) return metric.keyword.replace(/[::]/g, '').trim(); - if (metric.regex) { - const sanitized = metric.regex.replace(/[^a-zA-Z0-9_]/g, '').trim(); - return sanitized || `Metric ${index + 1}`; - } + // Fallback title if name is empty return `Metric ${index + 1}`; } -// 数值提取器类 -export class ValueExtractor { - // 关键词匹配 - static extractByKeyword(content, keyword) { - const results = []; - const lines = content.split('\n'); - - // 数值正则:支持各种数值格式,包括科学计数法 - const numberRegex = /[+-]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/; - - lines.forEach((line, lineIndex) => { - // 查找关键词(忽略大小写) - const keywordIndex = line.toLowerCase().indexOf(keyword.toLowerCase()); - if (keywordIndex !== -1) { - // 从关键词后开始查找第一个数字 - const afterKeyword = line.substring(keywordIndex + keyword.length); - const numberMatch = afterKeyword.match(numberRegex); - - if (numberMatch) { - const value = parseFloat(numberMatch[0]); - if (!isNaN(value)) { - results.push({ - value, - line: lineIndex + 1, - text: line.trim(), - format: 'Keyword Match' - }); - } - } - } - }); - - return results; - } - - // 列位置匹配 - static extractByColumn(content, columnIndex, separator = ' ') { - const results = []; - const lines = content.split('\n'); - - lines.forEach((line, lineIndex) => { - if (line.trim()) { - const columns = separator === ' ' - ? line.trim().split(/\s+/) - : line.split(separator); - - if (columns.length > columnIndex) { - const value = parseFloat(columns[columnIndex]); - if (!isNaN(value)) { - results.push({ - value, - line: lineIndex + 1, - text: line.trim() - }); - } - } - } - }); - - return results; - } - - // 智能解析 - static extractBySmart(content, type = 'loss') { - const results = []; - const lines = content.split('\n'); - - // 智能关键词列表 - const keywords = type === 'loss' - ? ['loss', 'training_loss', 'train_loss', 'val_loss', 'validation_loss'] - : ['grad_norm', 'gradient_norm', 'gnorm', 'grad norm', 'gradient norm', 'global_norm']; - - lines.forEach((line, lineIndex) => { - // 尝试JSON解析 - try { - const jsonMatch = line.match(/\{.*\}/); - if (jsonMatch) { - const obj = JSON.parse(jsonMatch[0]); - for (const keyword of keywords) { - if (obj[keyword] !== undefined) { - const value = parseFloat(obj[keyword]); - if (!isNaN(value)) { - results.push({ - value, - line: lineIndex + 1, - text: line.trim(), - format: 'JSON' - }); - return; - } - } - } - } - } catch { - // 不是JSON,继续其他格式 - } - - // 尝试键值对格式和特殊格式 - for (const keyword of keywords) { - const patterns = [ - // 标准键值对格式 - new RegExp(`${keyword}\\s*[:=]\\s*([\\d.eE+-]+)`, 'i'), - new RegExp(`"${keyword}"\\s*:\\s*([\\d.eE+-]+)`, 'i'), - new RegExp(`${keyword}\\s+([\\d.eE+-]+)`, 'i'), - // MindFormers特殊格式:global_norm: [1.6887678] - new RegExp(`${keyword}\\s*:\\s*\\[([\\d.eE+-]+)\\]`, 'i'), - // 其他可能的数组格式 - new RegExp(`${keyword}\\s*:\\s*\\[\\s*([\\d.eE+-]+)\\s*\\]`, 'i') - ]; - - for (const pattern of patterns) { - const match = line.match(pattern); - if (match) { - const value = parseFloat(match[1]); - if (!isNaN(value)) { - results.push({ - value, - line: lineIndex + 1, - text: line.trim(), - format: keyword.includes('global_norm') ? 'MindFormers' : 'Key-Value' - }); - return; - } - } - } - } - }); - - return results; - } - - // 正则表达式匹配(原有功能) - static extractByRegex(content, regex) { - const results = []; - const lines = content.split('\n'); - - try { - const regexObj = new RegExp(regex, 'gi'); - lines.forEach((line, lineIndex) => { - const matches = [...line.matchAll(regexObj)]; - matches.forEach(match => { - if (match[1]) { - const value = parseFloat(match[1]); - if (!isNaN(value)) { - results.push({ - value, - line: lineIndex + 1, - text: line.trim() - }); - } - } - }); - }); - } catch { - // 无效正则表达式 - } - - return results; - } -} - export function RegexControls({ globalParsingConfig, onGlobalParsingConfigChange, @@ -207,26 +21,22 @@ export function RegexControls({ const [showPreview, setShowPreview] = useState(false); const [previewResults, setPreviewResults] = useState({}); - // 提取数值的通用函数 - const extractValues = useCallback((content, mode, config) => { - switch (mode) { - case MATCH_MODES.KEYWORD: - return ValueExtractor.extractByKeyword(content, config.keyword); - case MATCH_MODES.REGEX: - return ValueExtractor.extractByRegex(content, config.regex); - default: - return []; + // 提取数值的通用函数 - 现在是插件分发器 + const extractValues = useCallback((content, metricConfig) => { + const plugin = extractorPlugins[metricConfig.mode]; + if (plugin) { + return plugin.extract(content, metricConfig); } + return []; }, []); // 预览匹配结果 const previewMatches = useCallback(() => { const results = {}; - uploadedFiles.forEach(file => { if (file.content) { globalParsingConfig.metrics.forEach((cfg, idx) => { - const matches = extractValues(file.content, cfg.mode, cfg); + const matches = extractValues(file.content, cfg); const key = getMetricTitle(cfg, idx); if (!results[key]) results[key] = []; results[key].push({ @@ -235,48 +45,38 @@ export function RegexControls({ examples: matches.slice(0, 3).map(m => ({ value: m.value, line: m.line, - text: m.text, - format: m.format + text: m.text })) }); }); } }); - setPreviewResults(results); }, [uploadedFiles, globalParsingConfig, extractValues]); - // 智能推荐最佳配置 + // 智能推荐最佳配置 (This could also be a plugin in the future) const smartRecommend = useCallback(() => { if (uploadedFiles.length === 0) return; - + // For simplicity, this logic remains here, but demonstrates where another plugin type could be used. + // This implementation just finds the most common "loss" keyword. const allContent = uploadedFiles.map(f => f.content).join('\n'); + let bestKeyword = 'loss'; + let maxCount = 0; - const newMetrics = globalParsingConfig.metrics.map(m => ({ ...m })); + ['loss', 'training_loss', 'train_loss', 'val_loss'].forEach(keyword => { + const tempConfig = { mode: 'keyword', keyword }; + const matches = extractorPlugins.keyword.extract(allContent, tempConfig); + if (matches.length > maxCount) { + maxCount = matches.length; + bestKeyword = keyword; + } + }); + const newMetrics = [...globalParsingConfig.metrics]; if (newMetrics[0]) { - let maxCount = 0; - ['loss', 'training_loss', 'train_loss'].forEach(keyword => { - const matches = ValueExtractor.extractByKeyword(allContent, keyword); - if (matches.length > maxCount) { - maxCount = matches.length; - newMetrics[0] = { ...newMetrics[0], mode: MATCH_MODES.KEYWORD, keyword }; - } - }); - } - - if (newMetrics[1]) { - let maxCount = 0; - ['grad_norm', 'gradient_norm', 'gnorm', 'global_norm'].forEach(keyword => { - const matches = ValueExtractor.extractByKeyword(allContent, keyword); - if (matches.length > maxCount) { - maxCount = matches.length; - newMetrics[1] = { ...newMetrics[1], mode: MATCH_MODES.KEYWORD, keyword }; - } - }); + newMetrics[0] = { ...newMetrics[0], mode: 'keyword', keyword: bestKeyword }; + onGlobalParsingConfigChange({ metrics: newMetrics }); } - - onGlobalParsingConfigChange({ metrics: newMetrics }); }, [uploadedFiles, globalParsingConfig, onGlobalParsingConfigChange]); // 当配置变化时更新预览 @@ -289,7 +89,15 @@ export function RegexControls({ // 处理配置变化 const handleMetricChange = (index, field, value) => { const newMetrics = [...globalParsingConfig.metrics]; - newMetrics[index] = { ...newMetrics[index], [field]: value }; + const oldMetric = newMetrics[index]; + newMetrics[index] = { ...oldMetric, [field]: value }; + + // If changing mode, we should preserve common fields but might want to reset specific ones. + if (field === 'mode') { + newMetrics[index].keyword = oldMetric.keyword || ''; + newMetrics[index].regex = oldMetric.regex || ''; + } + onGlobalParsingConfigChange({ metrics: newMetrics }); }; @@ -297,8 +105,8 @@ export function RegexControls({ const newMetrics = [ ...globalParsingConfig.metrics, { - name: `metric${globalParsingConfig.metrics.length + 1}`, - mode: 'keyword', + name: `Metric ${globalParsingConfig.metrics.length + 1}`, + mode: 'keyword', // Default mode keyword: '', regex: '' } @@ -314,8 +122,17 @@ export function RegexControls({ const applyPreset = (index, presetLabel) => { const preset = METRIC_PRESETS.find(p => p.label === presetLabel); if (!preset) return; + + // Create a new metric object from the preset + const newMetricFromPreset = { + name: preset.name, + mode: preset.mode, + keyword: preset.keyword || '', + regex: preset.regex || '', + }; + const newMetrics = [...globalParsingConfig.metrics]; - newMetrics[index] = { ...newMetrics[index], ...preset }; + newMetrics[index] = newMetricFromPreset; onGlobalParsingConfigChange({ metrics: newMetrics }); }; @@ -325,88 +142,35 @@ export function RegexControls({ }; // 渲染配置项的函数 - const renderConfigPanel = (type, config, onConfigChange, index) => { - const ModeIcon = MODE_CONFIG[config.mode].icon; + const renderConfigPanel = (metricConfig, onConfigChange) => { + const selectedPlugin = extractorPlugins[metricConfig.mode]; + const ConfigComponent = selectedPlugin ? selectedPlugin.ConfigUI : null; return (
-
- - onConfigChange('name', e.target.value)} - className="w-full px-2 py-1 text-sm border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 focus:outline-none" - /> - -
{/* 模式选择 */}
-

- - {MODE_CONFIG[config.mode].description} -

{/* 根据模式显示不同的配置项 */} - {config.mode === MATCH_MODES.KEYWORD && ( -
- - onConfigChange('keyword', e.target.value)} - className="w-full px-2 py-1 text-sm border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 focus:outline-none" - placeholder="keyword" - /> -

- 支持模糊匹配,如 "loss" 可匹配 "training_loss" -

-
- )} - - {config.mode === MATCH_MODES.REGEX && ( -
- - onConfigChange('regex', e.target.value)} - className="w-full px-2 py-1 text-sm border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 focus:outline-none font-mono" - placeholder="value:\\s*([\\d.eE+-]+)" - /> -

- 使用捕获组 () 来提取数值 -

-
- )} + {ConfigComponent && }
); }; @@ -415,25 +179,17 @@ export function RegexControls({
-
-
{uploadedFiles.length > 0 && ( @@ -441,7 +197,7 @@ export function RegexControls({ @@ -450,52 +206,68 @@ export function RegexControls({
{globalParsingConfig.metrics.map((cfg, idx) => ( -
- -

- - {getMetricTitle(cfg, idx)} 解析配置 -

- {renderConfigPanel(`metric-${idx}`, cfg, (field, value) => handleMetricChange(idx, field, value), idx)} +
+
+ + handleMetricChange(idx, 'name', e.target.value)} + placeholder={getMetricTitle(cfg, idx)} + className="w-full px-2 py-1 text-sm border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 focus:outline-none" + /> + +
+
+
+ {renderConfigPanel(cfg, (field, value) => handleMetricChange(idx, field, value))}
))} -
+
-
handleXRangeChange('min', e.target.value)} className="w-full px-2 py-1 text-xs border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 focus:outline-none" /> - handleXRangeChange('max', e.target.value)} className="w-full px-2 py-1 text-xs border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 focus:outline-none" /> @@ -503,39 +275,38 @@ export function RegexControls({ onClick={() => onXRangeChange({ min: undefined, max: undefined })} className="px-2 py-1 text-xs bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 whitespace-nowrap" > - 复位 + 复位 (Reset)

- 在图表上按住 Shift 键并拖动鼠标可选择范围,或直接输入数值。 + 在图表上按住 Shift 键并拖动鼠标可选择范围。

{/* 预览结果 */} {showPreview && uploadedFiles.length > 0 && (
-

匹配预览

+

匹配预览 (Preview)

{Object.entries(previewResults).map(([key, results]) => ( results.map((result, idx) => (
{key} - {result.fileName} - ({result.count} 个匹配) + ({result.count} matches)
- {result.examples.length > 0 && ( + {result.examples.length > 0 ? (
{result.examples.map((example, exIdx) => (
{example.value} - (第{example.line}行) - {example.format && ( - [{example.format}] - )} + (line {example.line})
{example.text}
))}
+ ) : ( +

No matches found in this file.

)}
)) @@ -543,19 +314,6 @@ export function RegexControls({
)} - -
-

🎯 增强解析功能:

-
    -
  • 关键词匹配:简单输入关键词,自动提取数值(默认模式)
  • -
  • 正则表达式:高级用户可使用复杂模式
  • -
  • 智能推荐:一键获得最佳解析配置
  • -
-
); diff --git a/src/components/__tests__/valueExtractor.test.js b/src/components/__tests__/valueExtractor.test.js deleted file mode 100644 index db286dd..0000000 --- a/src/components/__tests__/valueExtractor.test.js +++ /dev/null @@ -1,25 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { ValueExtractor } from '../RegexControls.jsx'; - -const sampleContent = `loss: 0.123\nstep2 loss 0.234\n{"loss": 0.345, "global_norm": 1.23}`; - -describe('ValueExtractor', () => { - it('extracts values by keyword', () => { - const results = ValueExtractor.extractByKeyword(sampleContent, 'loss:'); - expect(results.length).toBe(1); - expect(results[0].value).toBeCloseTo(0.123); - }); - - it('extracts values by regex', () => { - const results = ValueExtractor.extractByRegex(sampleContent, 'loss[:\\s]+([\\d.]+)'); - expect(results.length).toBe(2); - expect(results[1].value).toBeCloseTo(0.234); - }); - - it('extracts values by smart detection', () => { - const gradContent = 'global_norm: [1.5]'; - const results = ValueExtractor.extractBySmart(gradContent, 'grad_norm'); - expect(results.length).toBe(1); - expect(results[0].value).toBeCloseTo(1.5); - }); -}); diff --git a/src/firebase.js b/src/firebase.js new file mode 100644 index 0000000..42a4d24 --- /dev/null +++ b/src/firebase.js @@ -0,0 +1,28 @@ +// Import the functions you need from the SDKs you need +import { initializeApp } from "firebase/app"; +import { getAuth } from "firebase/auth"; +import { getFirestore } from "firebase/firestore"; + +// TODO: Add SDKs for Firebase products that you want to use +// https://firebase.google.com/docs/web/setup#available-libraries + +// --- IMPORTANT --- +// Replace this with your own Firebase project configuration. +// You can get this from the Firebase console. +const firebaseConfig = { + apiKey: "AIzaSyXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + authDomain: "your-project-id.firebaseapp.com", + projectId: "your-project-id", + storageBucket: "your-project-id.appspot.com", + messagingSenderId: "123456789012", + appId: "1:123456789012:web:XXXXXXXXXXXXXXXXXXXXXX" +}; + +// Initialize Firebase +const app = initializeApp(firebaseConfig); + +// Export the necessary Firebase services +export const auth = getAuth(app); +export const db = getFirestore(app); + +export default app; diff --git a/src/metricPresets.js b/src/metricPresets.js index 8f0bd8d..187e380 100644 --- a/src/metricPresets.js +++ b/src/metricPresets.js @@ -1,6 +1,16 @@ export const METRIC_PRESETS = [ - { label: 'Loss', name: 'Loss', mode: 'keyword', keyword: 'loss:' }, - { label: 'Grad Norm', name: 'Grad Norm', mode: 'keyword', keyword: 'norm:' }, - { label: 'Accuracy', name: 'Accuracy', mode: 'keyword', keyword: 'acc:' }, - { label: 'Learning Rate', name: 'Learning Rate', mode: 'keyword', keyword: 'lr:' } + { label: 'Generic/Loss', name: 'Loss', mode: 'keyword', keyword: 'loss:' }, + { label: 'Generic/Grad Norm', name: 'Grad Norm', mode: 'keyword', keyword: 'norm:' }, + { label: 'Generic/Accuracy', name: 'Accuracy', mode: 'keyword', keyword: 'acc:' }, + { label: 'Generic/Learning Rate', name: 'Learning Rate', mode: 'keyword', keyword: 'lr:' }, + + { label: 'PyTorch/Train Loss', name: 'Train Loss', mode: 'keyword', keyword: 'train_loss' }, + { label: 'PyTorch/Val Loss', name: 'Val Loss', mode: 'keyword', keyword: 'val_loss' }, + { label: 'PyTorch/Val Acc', name: 'Val Acc', mode: 'keyword', keyword: 'val_acc' }, + + { label: 'HuggingFace/Eval Loss', name: 'Eval Loss', mode: 'keyword', keyword: 'eval_loss' }, + { label: 'HuggingFace/Eval Accuracy', name: 'Eval Accuracy', mode: 'keyword', keyword: 'eval_accuracy' }, + + { label: 'Keras/MAE', name: 'MAE', mode: 'keyword', keyword: 'mae:' }, + { label: 'Keras/MSE', name: 'MSE', mode: 'keyword', keyword: 'mse:' }, ]; diff --git a/src/plugins/extractors/keywordExtractor.js b/src/plugins/extractors/keywordExtractor.js new file mode 100644 index 0000000..fc77628 --- /dev/null +++ b/src/plugins/extractors/keywordExtractor.js @@ -0,0 +1,72 @@ +import React from 'react'; + +/** + * Extracts numerical values from text content based on a keyword search. + * It looks for the first number appearing after a case-insensitive keyword match on each line. + * @param {string} content - The text content to parse. + * @param {object} config - The configuration for this extractor. + * @param {string} config.keyword - The keyword to search for. + * @returns {Array} An array of objects, each containing the extracted value and its line number. + */ +function extract(content, config) { + if (!config || !config.keyword) return []; + + const results = []; + const lines = content.split('\n'); + const keyword = config.keyword.toLowerCase(); + const numberRegex = /[+-]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/; + + lines.forEach((line, lineIndex) => { + const keywordIndex = line.toLowerCase().indexOf(keyword); + if (keywordIndex !== -1) { + const afterKeyword = line.substring(keywordIndex + keyword.length); + const numberMatch = afterKeyword.match(numberRegex); + + if (numberMatch) { + const value = parseFloat(numberMatch[0]); + if (!isNaN(value)) { + results.push({ + value, + line: lineIndex + 1, + text: line.trim() + }); + } + } + } + }); + + return results; +} + +/** + * A React component that renders the configuration UI for the keyword extractor. + * @param {object} props - The component props. + * @param {object} props.config - The current configuration object for the metric. + * @param {function} props.onConfigChange - A callback function to update the configuration. + */ +function ConfigUI({ config, onConfigChange }) { + return ( +
+ + onConfigChange('keyword', e.target.value)} + className="w-full px-2 py-1 text-sm border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 focus:outline-none" + placeholder="e.g., loss:" + /> +

+ 支持模糊匹配,如 "loss" 可匹配 "training_loss"。 +

+
+ ); +} + +export const keywordExtractorPlugin = { + name: 'keyword', + displayName: '关键词匹配 (Keyword)', + extract, + ConfigUI, +}; diff --git a/src/plugins/extractors/regexExtractor.js b/src/plugins/extractors/regexExtractor.js new file mode 100644 index 0000000..cddb9d4 --- /dev/null +++ b/src/plugins/extractors/regexExtractor.js @@ -0,0 +1,73 @@ +import React from 'react'; + +/** + * Extracts numerical values from text content using a regular expression. + * The regex must contain at least one capture group `()`, and the first capture group will be used as the extracted value. + * @param {string} content - The text content to parse. + * @param {object} config - The configuration for this extractor. + * @param {string} config.regex - The regular expression string. + * @returns {Array} An array of objects, each containing the extracted value and its line number. + */ +function extract(content, config) { + if (!config || !config.regex) return []; + + const results = []; + const lines = content.split('\n'); + + try { + const regexObj = new RegExp(config.regex, 'gi'); + lines.forEach((line, lineIndex) => { + const matches = [...line.matchAll(regexObj)]; + matches.forEach(match => { + if (match[1]) { + const value = parseFloat(match[1]); + if (!isNaN(value)) { + results.push({ + value, + line: lineIndex + 1, + text: line.trim() + }); + } + } + }); + }); + } catch (e) { + // Invalid regex, return no results + console.warn("Invalid regex provided:", config.regex, e); + } + + return results; +} + +/** + * A React component that renders the configuration UI for the regex extractor. + * @param {object} props - The component props. + * @param {object} props.config - The current configuration object for the metric. + * @param {function} props.onConfigChange - A callback function to update the configuration. + */ +function ConfigUI({ config, onConfigChange }) { + return ( +
+ + onConfigChange('regex', e.target.value)} + className="w-full px-2 py-1 text-sm border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 focus:outline-none font-mono" + placeholder="value:\\s*([\\d.eE+-]+)" + /> +

+ 使用捕获组 `()` 来提取数值。 +

+
+ ); +} + +export const regexExtractorPlugin = { + name: 'regex', + displayName: '正则表达式 (Regex)', + extract, + ConfigUI, +}; diff --git a/src/plugins/pluginRegistry.js b/src/plugins/pluginRegistry.js new file mode 100644 index 0000000..30ace09 --- /dev/null +++ b/src/plugins/pluginRegistry.js @@ -0,0 +1,20 @@ +import { keywordExtractorPlugin } from './extractors/keywordExtractor.js'; +import { regexExtractorPlugin } from './extractors/regexExtractor.js'; + +/** + * A registry of all available data extractor plugins. + * The application will use this registry to discover and use the plugins. + * The key is the plugin's unique `name`, and the value is the plugin object itself. + */ +export const extractorPlugins = { + [keywordExtractorPlugin.name]: keywordExtractorPlugin, + [regexExtractorPlugin.name]: regexExtractorPlugin, +}; + +/** + * An ordered list of plugin names, defining the order they appear in the UI. + */ +export const extractorPluginOrder = [ + keywordExtractorPlugin.name, + regexExtractorPlugin.name, +]; diff --git a/src/services/authService.js b/src/services/authService.js new file mode 100644 index 0000000..301e5c4 --- /dev/null +++ b/src/services/authService.js @@ -0,0 +1,35 @@ +import { + GoogleAuthProvider, + signInWithPopup, + signOut, + onAuthStateChanged +} from "firebase/auth"; +import { auth } from "../firebase"; + +const provider = new GoogleAuthProvider(); + +/** + * Initiates the Google Sign-In popup flow. + * @returns {Promise} A promise that resolves with the user's credentials. + */ +export const signInWithGoogle = () => { + return signInWithPopup(auth, provider); +}; + +/** + * Signs the current user out. + * @returns {Promise} A promise that resolves when the sign-out is complete. + */ +export const doSignOut = () => { + return signOut(auth); +}; + +/** + * Subscribes to changes in the user's authentication state. + * @param {function} callback - The function to call when the auth state changes. + * It receives the user object (or null) as an argument. + * @returns {function} An unsubscribe function. + */ +export const onAuthChange = (callback) => { + return onAuthStateChanged(auth, callback); +}; diff --git a/src/services/workspaceService.js b/src/services/workspaceService.js new file mode 100644 index 0000000..71e2594 --- /dev/null +++ b/src/services/workspaceService.js @@ -0,0 +1,30 @@ +import { db } from '../firebase'; +import { doc, setDoc, getDoc } from 'firebase/firestore'; + +/** + * Saves a user's workspace data to Firestore. + * @param {string} userId - The ID of the user. + * @param {object} workspaceData - The workspace data object to save. + * @returns {Promise} + */ +export const saveWorkspace = (userId, workspaceData) => { + const workspaceRef = doc(db, 'workspaces', userId); + return setDoc(workspaceRef, workspaceData, { merge: true }); +}; + +/** + * Retrieves a user's workspace data from Firestore. + * @param {string} userId - The ID of the user. + * @returns {Promise} A promise that resolves with the workspace data, or null if it doesn't exist. + */ +export const loadWorkspace = async (userId) => { + const workspaceRef = doc(db, 'workspaces', userId); + const docSnap = await getDoc(workspaceRef); + + if (docSnap.exists()) { + return docSnap.data(); + } else { + console.log("No such workspace!"); + return null; + } +};