From 50376f71a832e2f0e597dbc7573bcb50f2ed2067 Mon Sep 17 00:00:00 2001 From: "Ishan S. Patel" Date: Wed, 17 Sep 2025 12:03:14 -0400 Subject: [PATCH] common code --- SearchFrontend/search_ui/.prettierrc | 4 + SearchFrontend/search_ui/package-lock.json | 1214 ++++++++++++++++- SearchFrontend/search_ui/package.json | 19 +- SearchFrontend/search_ui/src/App.css | 116 +- SearchFrontend/search_ui/src/App.jsx | 289 +++- .../src/components/CompactDateRangePicker.jsx | 28 + .../src/components/DateRangePicker.jsx | 77 ++ .../src/components/EmbedTimeline.css | 28 - .../src/components/EmbedTimeline.jsx | 778 ++++++----- .../src/components/StatusDisplay.jsx | 79 ++ .../search_ui/src/components/VideoPlayer.jsx | 72 + SearchFrontend/search_ui/src/index.css | 3 +- SearchFrontend/search_ui/src/main.jsx | 4 +- SearchFrontend/search_ui/vite.config.js | 10 +- SearchInterface.code-workspace | 10 + SearchScratch/test_recreate_cache.py | 13 + SearchScratch/test_seek.py | 182 +++ SearchUtil | 1 - VectorService/util/.gitignore | 162 +++ VectorService/util/CommonCode.code-workspace | 11 + VectorService/util/embed_scores.py | 339 +++++ VectorService/vector_service.py | 96 ++ 22 files changed, 3145 insertions(+), 390 deletions(-) create mode 100644 SearchFrontend/search_ui/.prettierrc create mode 100644 SearchFrontend/search_ui/src/components/CompactDateRangePicker.jsx create mode 100644 SearchFrontend/search_ui/src/components/DateRangePicker.jsx create mode 100644 SearchFrontend/search_ui/src/components/StatusDisplay.jsx create mode 100644 SearchFrontend/search_ui/src/components/VideoPlayer.jsx create mode 100644 SearchInterface.code-workspace create mode 100644 SearchScratch/test_recreate_cache.py create mode 100644 SearchScratch/test_seek.py delete mode 160000 SearchUtil create mode 100644 VectorService/util/.gitignore create mode 100644 VectorService/util/CommonCode.code-workspace create mode 100644 VectorService/util/embed_scores.py create mode 100644 VectorService/vector_service.py diff --git a/SearchFrontend/search_ui/.prettierrc b/SearchFrontend/search_ui/.prettierrc new file mode 100644 index 0000000..222861c --- /dev/null +++ b/SearchFrontend/search_ui/.prettierrc @@ -0,0 +1,4 @@ +{ + "tabWidth": 2, + "useTabs": false +} diff --git a/SearchFrontend/search_ui/package-lock.json b/SearchFrontend/search_ui/package-lock.json index b6b1fdb..f85c2bf 100644 --- a/SearchFrontend/search_ui/package-lock.json +++ b/SearchFrontend/search_ui/package-lock.json @@ -4,6 +4,211 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "requires": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + } + }, + "@babel/generator": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "requires": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + } + }, + "@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==" + }, + "@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "requires": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + } + }, + "@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==" + }, + "@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==" + }, + "@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "requires": { + "@babel/types": "^7.28.4" + } + }, + "@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==" + }, + "@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "requires": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + } + }, + "@babel/traverse": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "requires": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" + } + }, + "@babel/types": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "requires": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + } + }, + "@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "requires": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "requires": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==" + }, + "@emotion/is-prop-valid": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", + "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", + "requires": { + "@emotion/memoize": "^0.9.0" + } + }, + "@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==" + }, + "@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "requires": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + } + }, + "@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "requires": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==" + }, + "@emotion/styled": { + "version": "11.14.1", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", + "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", + "requires": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/is-prop-valid": "^1.3.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2" + } + }, + "@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==" + }, + "@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==" + }, + "@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==" + }, + "@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==" + }, "@esbuild/aix-ppc64": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", @@ -282,6 +487,61 @@ "levn": "^0.4.1" } }, + "@fingerprintjs/fingerprintjs": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@fingerprintjs/fingerprintjs/-/fingerprintjs-3.4.2.tgz", + "integrity": "sha512-3Ncze6JsJpB7BpYhqIgvBpfvEX1jsEKrad5hQBpyRQxtoAp6hx3+R46zqfsuQG4D9egQZ+xftQ0u4LPFMB7Wmg==", + "requires": { + "tslib": "^2.4.1" + }, + "dependencies": { + "tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + } + } + }, + "@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==", + "requires": { + "@floating-ui/utils": "^0.2.10" + } + }, + "@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "requires": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "@floating-ui/react": { + "version": "0.27.16", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.16.tgz", + "integrity": "sha512-9O8N4SeG2z++TSM8QA/KTeKFBVCNEz/AGS7gWPJf6KFRzmRWixFRnCnkPHRDwSVZW6QPDO6uT0P2SpWNKCc9/g==", + "requires": { + "@floating-ui/react-dom": "^2.1.6", + "@floating-ui/utils": "^0.2.10", + "tabbable": "^6.0.0" + } + }, + "@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==", + "requires": { + "@floating-ui/dom": "^1.7.4" + } + }, + "@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==" + }, "@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -318,6 +578,218 @@ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true }, + "@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "requires": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==" + }, + "@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" + }, + "@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "@juggle/resize-observer": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz", + "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==" + }, + "@lit-labs/ssr-dom-shim": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.4.0.tgz", + "integrity": "sha512-ficsEARKnmmW5njugNYKipTm4SFnbik7CXtoencDZzmzo/dQ+2Q0bgkzJuoJP20Aj0F+izzJjOqsnkd6F/o1bw==" + }, + "@lit/reactive-element": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.3.tgz", + "integrity": "sha512-QuTgnG52Poic7uM1AN5yJ09QMe0O28e10XzSvWDz02TJiiKee4stsiownEIadWm8nYzyDAyT+gKzUoZmiWQtsQ==", + "requires": { + "@lit-labs/ssr-dom-shim": "^1.0.0" + } + }, + "@mui/core-downloads-tracker": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.2.tgz", + "integrity": "sha512-AOyfHjyDKVPGJJFtxOlept3EYEdLoar/RvssBTWVAvDJGIE676dLi2oT/Kx+FoVXFoA/JdV7DEMq/BVWV3KHRw==" + }, + "@mui/material": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.2.tgz", + "integrity": "sha512-qXvbnawQhqUVfH1LMgMaiytP+ZpGoYhnGl7yYq2x57GYzcFL/iPzSZ3L30tlbwEjSVKNYcbiKO8tANR1tadjUg==", + "requires": { + "@babel/runtime": "^7.28.3", + "@mui/core-downloads-tracker": "^7.3.2", + "@mui/system": "^7.3.2", + "@mui/types": "^7.4.6", + "@mui/utils": "^7.3.2", + "@popperjs/core": "^2.11.8", + "@types/react-transition-group": "^4.4.12", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "prop-types": "^15.8.1", + "react-is": "^19.1.1", + "react-transition-group": "^4.4.5" + }, + "dependencies": { + "react-is": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.1.tgz", + "integrity": "sha512-tr41fA15Vn8p4X9ntI+yCyeGSf1TlYaY5vlTZfQmeLBrFo3psOPX6HhTDnFNL9uj3EhP0KAQ80cugCl4b4BERA==" + } + } + }, + "@mui/private-theming": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.3.2.tgz", + "integrity": "sha512-ha7mFoOyZGJr75xeiO9lugS3joRROjc8tG1u4P50dH0KR7bwhHznVMcYg7MouochUy0OxooJm/OOSpJ7gKcMvg==", + "requires": { + "@babel/runtime": "^7.28.3", + "@mui/utils": "^7.3.2", + "prop-types": "^15.8.1" + } + }, + "@mui/styled-engine": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.3.2.tgz", + "integrity": "sha512-PkJzW+mTaek4e0nPYZ6qLnW5RGa0KN+eRTf5FA2nc7cFZTeM+qebmGibaTLrgQBy3UpcpemaqfzToBNkzuxqew==", + "requires": { + "@babel/runtime": "^7.28.3", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + } + }, + "@mui/system": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.2.tgz", + "integrity": "sha512-9d8JEvZW+H6cVkaZ+FK56R53vkJe3HsTpcjMUtH8v1xK6Y1TjzHdZ7Jck02mGXJsE6MQGWVs3ogRHTQmS9Q/rA==", + "requires": { + "@babel/runtime": "^7.28.3", + "@mui/private-theming": "^7.3.2", + "@mui/styled-engine": "^7.3.2", + "@mui/types": "^7.4.6", + "@mui/utils": "^7.3.2", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + } + }, + "@mui/types": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.6.tgz", + "integrity": "sha512-NVBbIw+4CDMMppNamVxyTccNv0WxtDb7motWDlMeSC8Oy95saj1TIZMGynPpFLePt3yOD8TskzumeqORCgRGWw==", + "requires": { + "@babel/runtime": "^7.28.3" + } + }, + "@mui/utils": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.2.tgz", + "integrity": "sha512-4DMWQGenOdLnM3y/SdFQFwKsCLM+mqxzvoWp9+x2XdEzXapkznauHLiXtSohHs/mc0+5/9UACt1GdugCX2te5g==", + "requires": { + "@babel/runtime": "^7.28.3", + "@mui/types": "^7.4.6", + "@types/prop-types": "^15.7.15", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.1.1" + }, + "dependencies": { + "react-is": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.1.tgz", + "integrity": "sha512-tr41fA15Vn8p4X9ntI+yCyeGSf1TlYaY5vlTZfQmeLBrFo3psOPX6HhTDnFNL9uj3EhP0KAQ80cugCl4b4BERA==" + } + } + }, + "@mui/x-date-pickers": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-8.11.2.tgz", + "integrity": "sha512-izosRFdlo0Aq4nrQ2klOQBLB+yCX3bIlErF/gxZfaXK/kb8NToweZjhHdiyy+hr+VrxK0A71AWI6LkPyfG2WCg==", + "requires": { + "@babel/runtime": "^7.28.2", + "@mui/utils": "^7.3.2", + "@mui/x-internals": "8.11.2", + "@types/react-transition-group": "^4.4.12", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + } + }, + "@mui/x-date-pickers-pro": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers-pro/-/x-date-pickers-pro-8.11.2.tgz", + "integrity": "sha512-VqYq2JlIDd7HPjBUeI9B6netsgafdY1mrM2Q5uXJ8az/WUHnj2KMma7/4NwdE+POyDMtBGRB8GKVGv6C576ToA==", + "requires": { + "@babel/runtime": "^7.28.2", + "@mui/utils": "^7.3.2", + "@mui/x-date-pickers": "8.11.2", + "@mui/x-internals": "8.11.2", + "@mui/x-license": "8.11.2", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + } + }, + "@mui/x-internals": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.11.2.tgz", + "integrity": "sha512-3BFZ0Njgih+eWQBzSsdKEkRMlHtKRGFWz+/CGUrSBb5IApO0apkUSvG4v5augNYASsjksqWOXVlds7Wwznd0Lg==", + "requires": { + "@babel/runtime": "^7.28.2", + "@mui/utils": "^7.3.2", + "reselect": "^5.1.1", + "use-sync-external-store": "^1.5.0" + } + }, + "@mui/x-license": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/@mui/x-license/-/x-license-8.11.2.tgz", + "integrity": "sha512-Z6iRkz5PV1DPkGaSrnNHAFVJ4CAXIll5SNYWG1DZG5wgDWF2T8xqAh9oJbFSdvBaBoonHWiCFHl2AVDrKIhEFg==", + "requires": { + "@babel/runtime": "^7.28.2", + "@mui/utils": "^7.3.2", + "@mui/x-internals": "8.11.2", + "@mui/x-telemetry": "8.11.2" + } + }, + "@mui/x-telemetry": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/@mui/x-telemetry/-/x-telemetry-8.11.2.tgz", + "integrity": "sha512-rRuDcZkIIhUMvEzcu66tyrBGaBEAxYy5K99wbNZVla/FGRJvQVKzubEev4EX51HEtuTphUha0BHvAUYTYu5UMw==", + "requires": { + "@babel/runtime": "^7.28.2", + "@fingerprintjs/fingerprintjs": "^3.4.2", + "ci-info": "^4.3.0", + "conf": "^11.0.2", + "is-docker": "^3.0.0", + "node-machine-id": "^1.1.12" + } + }, + "@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==" + }, "@rolldown/pluginutils": { "version": "1.0.0-beta.32", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.32.tgz", @@ -464,6 +936,20 @@ "dev": true, "optional": true }, + "@rsuite/icon-font": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@rsuite/icon-font/-/icon-font-4.1.0.tgz", + "integrity": "sha512-q0Y+uQCVvzhD6lFeAFrvCDd1lTjZfM6MIaBjre3lSW1w586VWbuFnhTiqos3v9HIMlUpm3aAsxd3SuM6gYaqqQ==" + }, + "@rsuite/icons": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@rsuite/icons/-/icons-1.3.2.tgz", + "integrity": "sha512-3sOdZpPcVePwTCoWcHHIEg4rsjWp9ehwjC84SjvgyqNyEf/BJNVDm0+jDAGGbhOF0HiWYyfU10LwrwnDSAYltA==", + "requires": { + "@rsuite/icon-font": "^4.1.0", + "classnames": "^2.2.5" + } + }, "@swc/core": { "version": "1.13.5", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.5.tgz", @@ -581,11 +1067,25 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "@types/lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==" + }, + "@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" + }, + "@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==" + }, "@types/react": { "version": "19.1.12", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.12.tgz", "integrity": "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==", - "dev": true, "requires": { "csstype": "^3.0.2" } @@ -596,6 +1096,24 @@ "integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==", "dev": true }, + "@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==" + }, + "@types/react-window": { + "version": "1.8.8", + "resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.8.tgz", + "integrity": "sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==", + "requires": { + "@types/react": "*" + } + }, + "@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" + }, "@vitejs/plugin-react-swc": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-4.0.1.tgz", @@ -606,6 +1124,23 @@ "@swc/core": "^1.13.2" } }, + "@wojtekmaj/date-utils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@wojtekmaj/date-utils/-/date-utils-2.0.2.tgz", + "integrity": "sha512-Do66mSlSNifFFuo3l9gNKfRMSFi26CRuQMsDJuuKO/ekrDWuTTtE4ZQxoFCUOG+NgxnpSeBq/k5TY8ZseEzLpA==" + }, + "@wojtekmaj/react-daterange-picker": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@wojtekmaj/react-daterange-picker/-/react-daterange-picker-7.0.0.tgz", + "integrity": "sha512-ieNpzf85kqnvBUvyu6Fqdt7WKHZgXW2aidXHeBNaGozSjtWk4lMCoxke3f6TFA3iYpH3xR173mrTGRI8kyyqwA==", + "requires": { + "clsx": "^2.0.0", + "make-event-props": "^2.0.0", + "react-calendar": "^6.0.0", + "react-date-picker": "^12.0.1", + "react-fit": "^3.0.0" + } + }, "acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -630,6 +1165,32 @@ "uri-js": "^4.2.2" } }, + "ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "requires": { + "ajv": "^8.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "requires": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + } + } + }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -645,6 +1206,25 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "atomically": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/atomically/-/atomically-2.0.3.tgz", + "integrity": "sha512-kU6FmrwZ3Lx7/7y3hPS5QnbJfaohcIul5fGqf7ok+4KklIEk9tJ0C2IQPdacSbVUWv6zVHXEBWoWd6NrVMT7Cw==", + "requires": { + "stubborn-fs": "^1.2.5", + "when-exit": "^2.1.1" + } + }, + "babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "requires": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + } + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -664,8 +1244,7 @@ "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" }, "chalk": { "version": "4.1.2", @@ -677,6 +1256,21 @@ "supports-color": "^7.1.0" } }, + "ci-info": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", + "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==" + }, + "classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, + "clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==" + }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -703,6 +1297,56 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "conf": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/conf/-/conf-11.0.2.tgz", + "integrity": "sha512-jjyhlQ0ew/iwmtwsS2RaB6s8DBifcE2GYBEaw2SJDUY/slJJbNfY4GlDVzOs/ff8cM/Wua5CikqXgbFl5eu85A==", + "requires": { + "ajv": "^8.12.0", + "ajv-formats": "^2.1.1", + "atomically": "^2.0.0", + "debounce-fn": "^5.1.2", + "dot-prop": "^7.2.0", + "env-paths": "^3.0.0", + "json-schema-typed": "^8.0.1", + "semver": "^7.3.8" + }, + "dependencies": { + "ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "requires": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + } + } + }, + "convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + }, + "cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + } + }, "cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -717,8 +1361,7 @@ "csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "d3": { "version": "7.9.0", @@ -1016,11 +1659,28 @@ "d3-transition": "2 - 3" } }, + "date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==" + }, + "dayjs": { + "version": "1.11.18", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz", + "integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==" + }, + "debounce-fn": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-5.1.2.tgz", + "integrity": "sha512-Sr4SdOZ4vw6eQDvPYNxHogvrxmCIld/VenC5JbNrFwMiwd7lY/Z18ZFfo+EWNG4DD9nFlAujWAo/wGuOPHmy5A==", + "requires": { + "mimic-fn": "^4.0.0" + } + }, "debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, "requires": { "ms": "^2.1.3" } @@ -1039,6 +1699,36 @@ "robust-predicates": "^3.0.2" } }, + "detect-element-overflow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/detect-element-overflow/-/detect-element-overflow-2.0.0.tgz", + "integrity": "sha512-DETEHRGdIdQowMR1Fb5hG4WXUJhlLId/IRQgAi5GVqCnHwnb0SKf9V/teyEVDX0+iClPeOPiNvlE6fSiECz65w==" + }, + "dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "requires": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "dom-lib": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/dom-lib/-/dom-lib-3.3.2.tgz", + "integrity": "sha512-ux0wcf6lggOCcJ6O3Q3mewbCOM/CL9f6+NXmxaWsF0/AKCvFNbfdmmqNnMG7cMVupCr9VeFEYWspSAD9WT/6gA==", + "requires": { + "@babel/runtime": "^7.20.0" + } + }, + "dot-prop": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-7.2.0.tgz", + "integrity": "sha512-Ol/IPXUARn9CSbkrdV4VJo7uCy1I3VuSiWCaFSg+8BdUOzF9n3jefIpcgAydvUZbTdEBZs2vEiTiS9m61ssiDA==", + "requires": { + "type-fest": "^2.11.2" + } + }, "echarts": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz", @@ -1057,6 +1747,19 @@ "size-sensor": "^1.0.1" } }, + "env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==" + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "requires": { + "is-arrayish": "^0.2.1" + } + }, "esbuild": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", @@ -1094,8 +1797,7 @@ "escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" }, "eslint": { "version": "9.34.0", @@ -1226,6 +1928,11 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==" + }, "fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -1241,6 +1948,11 @@ "flat-cache": "^4.0.0" } }, + "find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + }, "find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -1267,6 +1979,22 @@ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true }, + "flex-layout-system": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/flex-layout-system/-/flex-layout-system-2.0.3.tgz", + "integrity": "sha512-sJGPXir3VoSvBvT+2oyGJIbU4LBo7uRZWJCWFpjncNUwEUeNdH99IN+TM698RSSLrYd8EkcOJiCT3XBh1FcxVg==", + "requires": { + "lit": "^2.7.4" + } + }, + "flexboxgrid2": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/flexboxgrid2/-/flexboxgrid2-7.2.1.tgz", + "integrity": "sha512-O2bO5ZcBXnFy7cYmyt/CKb6CuwzNuUPxWJt8WOiaot8SetE9zyUahXGTSpKDm3+CTYQ5YeEMPeunMdjcxKJz4w==", + "requires": { + "normalize.css": "^7.0.0" + } + }, "fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1274,6 +2002,19 @@ "dev": true, "optional": true }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" + }, + "get-user-locale": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-user-locale/-/get-user-locale-3.0.0.tgz", + "integrity": "sha512-iJfHSmdYV39UUBw7Jq6GJzeJxUr4U+S03qdhVuDsR9gCEnfbqLy9gYDJFBJQL1riqolFUKQvx36mEkp2iGgJ3g==", + "requires": { + "memoize": "^10.0.0" + } + }, "glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -1295,6 +2036,22 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "requires": { + "function-bind": "^1.1.2" + } + }, + "hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "requires": { + "react-is": "^16.7.0" + } + }, "iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -1313,7 +2070,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, "requires": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -1330,6 +2086,24 @@ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==" }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "requires": { + "hasown": "^2.0.2" + } + }, + "is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==" + }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1351,6 +2125,11 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, "js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -1360,18 +2139,33 @@ "argparse": "^2.0.1" } }, + "jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==" + }, "json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, + "json-schema-typed": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.1.tgz", + "integrity": "sha512-XQmWYj2Sm4kn4WeTYvmpKEbyPsL7nBsb647c7pMe6l02/yx2+Jfc4dT6UZkEXnIUb5LhD55r2HPsJ1milQ4rDg==" + }, "json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -1405,6 +2199,39 @@ "type-check": "~0.4.0" } }, + "lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "lit": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/lit/-/lit-2.8.0.tgz", + "integrity": "sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA==", + "requires": { + "@lit/reactive-element": "^1.6.0", + "lit-element": "^3.3.0", + "lit-html": "^2.8.0" + } + }, + "lit-element": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.3.3.tgz", + "integrity": "sha512-XbeRxmTHubXENkV4h8RIPyr8lXc+Ff28rkcQzw3G6up2xg5E8Zu1IgOWIwBLEQsu3cOVFqdYwiVi0hv0SlpqUA==", + "requires": { + "@lit-labs/ssr-dom-shim": "^1.1.0", + "@lit/reactive-element": "^1.3.0", + "lit-html": "^2.8.0" + } + }, + "lit-html": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.8.0.tgz", + "integrity": "sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==", + "requires": { + "@types/trusted-types": "^2.0.2" + } + }, "locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -1414,6 +2241,11 @@ "p-locate": "^5.0.0" } }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "lodash-es": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", @@ -1425,6 +2257,42 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "make-event-props": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/make-event-props/-/make-event-props-2.0.0.tgz", + "integrity": "sha512-G/hncXrl4Qt7mauJEXSg3AcdYzmpkIITTNl5I+rH9sog5Yw0kK6vseJjCaPfOXqOqQuPUP89Rkhfz5kPS8ijtw==" + }, + "memoize": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/memoize/-/memoize-10.1.0.tgz", + "integrity": "sha512-MMbFhJzh4Jlg/poq1si90XRlTZRDHVqdlz2mPyGJ6kqMpyHUyVpDd5gpFAvVehW64+RA1eKE9Yt8aSLY7w2Kgg==", + "requires": { + "mimic-function": "^5.0.1" + } + }, + "memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + }, + "mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==" + }, + "mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==" + }, "minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -1437,8 +2305,7 @@ "ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "nanoid": { "version": "3.3.11", @@ -1452,6 +2319,21 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node-machine-id": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/node-machine-id/-/node-machine-id-1.1.12.tgz", + "integrity": "sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==" + }, + "normalize.css": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/normalize.css/-/normalize.css-7.0.0.tgz", + "integrity": "sha512-LYaFZxj2Q1Q9e1VJ0f6laG46Rt5s9URhKyckNaA2vZnL/0gwQHWhM7ALQkp3WBQKM5sXRLQ5Ehrfkp+E/ZiCRg==" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + }, "optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -1488,11 +2370,21 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "requires": { "callsites": "^3.0.0" } }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -1505,11 +2397,20 @@ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" + }, "picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" }, "picomatch": { "version": "4.0.3", @@ -1534,6 +2435,16 @@ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, + "prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -1545,6 +2456,52 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==" }, + "react-calendar": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/react-calendar/-/react-calendar-6.0.0.tgz", + "integrity": "sha512-6wqaki3Us0DNDjZDr0DYIzhSFprNoy4FdPT9Pjy5aD2hJJVjtJwmdMT9VmrTUo949nlk35BOxehThxX62RkuRQ==", + "requires": { + "@wojtekmaj/date-utils": "^2.0.2", + "clsx": "^2.0.0", + "get-user-locale": "^3.0.0", + "warning": "^4.0.0" + } + }, + "react-date-picker": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/react-date-picker/-/react-date-picker-12.0.1.tgz", + "integrity": "sha512-VHXOIBt5/Bxa+ICMENjfWjGvllvXxd+lzGB2iH9fOc7+BfcetI/aI8MZuiIKofmNWq2oAdD+fg4MsZkKmOBjig==", + "requires": { + "@wojtekmaj/date-utils": "^2.0.2", + "clsx": "^2.0.0", + "get-user-locale": "^3.0.0", + "make-event-props": "^2.0.0", + "react-calendar": "^6.0.0", + "react-fit": "^3.0.0", + "update-input-width": "^1.4.0" + } + }, + "react-date-range": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/react-date-range/-/react-date-range-2.0.1.tgz", + "integrity": "sha512-jwKYc9zcjYMg2hWbPht+6BF2wjGG5DkRVNJLRXn2Y0B/QCOOnvQX6YXziZVujVADWmgsBaoQnILdmzYw+Bwh0g==", + "requires": { + "classnames": "^2.2.6", + "prop-types": "^15.7.2", + "react-list": "^0.8.13", + "shallow-equal": "^1.2.1" + } + }, + "react-datepicker": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-8.7.0.tgz", + "integrity": "sha512-r5OJbiLWc3YiVNy69Kau07/aVgVGsFVMA6+nlqCV7vyQ8q0FUOnJ+wAI4CgVxHejG3i5djAEiebrF8/Eip4rIw==", + "requires": { + "@floating-ui/react": "^0.27.15", + "clsx": "^2.1.1", + "date-fns": "^4.1.0" + } + }, "react-dom": { "version": "19.1.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", @@ -1553,11 +2510,106 @@ "scheduler": "^0.26.0" } }, + "react-fit": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/react-fit/-/react-fit-3.0.0.tgz", + "integrity": "sha512-3jrm0k4TZS7n6fZXgCu5k8ARx5w+YbrzpcFMop/iScOjnA22xQVjrVOOJD8YyqRM1j0focbRBrBEJXyydzF/Iw==", + "requires": { + "detect-element-overflow": "^2.0.0", + "warning": "^4.0.0" + } + }, + "react-flexbox-grid": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/react-flexbox-grid/-/react-flexbox-grid-2.1.2.tgz", + "integrity": "sha512-lj1oVnIJ7TY3W6tPjFUxlUYd1DLFxEg8RiX3HAYVvreE3O9HU9n2390CFoPQ+qk1E+5MXa2t/mLMafFLAa8+7Q==", + "requires": { + "flexboxgrid2": "^7.2.0", + "prop-types": "^15.5.8" + } + }, + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, + "react-list": { + "version": "0.8.18", + "resolved": "https://registry.npmjs.org/react-list/-/react-list-0.8.18.tgz", + "integrity": "sha512-1OSdDvzuKuwDJvQNuhXxxL+jTmmdtKg1i6KtYgxI9XR98kbOql1FcSGP+Lcvo91fk3cYng+Z6YkC6X9HRJwxfw==" + }, + "react-split-pane": { + "version": "0.1.92", + "resolved": "https://registry.npmjs.org/react-split-pane/-/react-split-pane-0.1.92.tgz", + "integrity": "sha512-GfXP1xSzLMcLJI5BM36Vh7GgZBpy+U/X0no+VM3fxayv+p1Jly5HpMofZJraeaMl73b3hvlr+N9zJKvLB/uz9w==", + "requires": { + "prop-types": "^15.7.2", + "react-lifecycles-compat": "^3.0.4", + "react-style-proptype": "^3.2.2" + } + }, + "react-style-proptype": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-style-proptype/-/react-style-proptype-3.2.2.tgz", + "integrity": "sha512-ywYLSjNkxKHiZOqNlso9PZByNEY+FTyh3C+7uuziK0xFXu9xzdyfHwg4S9iyiRRoPCR4k2LqaBBsWVmSBwCWYQ==", + "requires": { + "prop-types": "^15.5.4" + } + }, + "react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "requires": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + } + }, + "react-use-set": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/react-use-set/-/react-use-set-1.0.0.tgz", + "integrity": "sha512-6BBbOcWc/tOKuwd9gDtdunvOr/g40S0SkCBYvrSJvpI0upzNlHmLoeDvylnoP8PrjQXItClAFxseVGGhEkk7kw==" + }, + "react-window": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.11.tgz", + "integrity": "sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ==", + "requires": { + "@babel/runtime": "^7.0.0", + "memoize-one": ">=3.1.1 <6" + } + }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" + }, + "reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==" + }, + "resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "requires": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, "resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" }, "robust-predicates": { "version": "3.0.2", @@ -1594,6 +2646,50 @@ "fsevents": "~2.3.2" } }, + "rsuite": { + "version": "5.83.3", + "resolved": "https://registry.npmjs.org/rsuite/-/rsuite-5.83.3.tgz", + "integrity": "sha512-L9iNrFTSDyXzSSJCu5tEK1sv9dE2gT4H6b5AQBbdIzdADqYr3DhBJLnOW5WthqSnCc40hupoPUcI9wUrnGbjZQ==", + "requires": { + "@babel/runtime": "^7.20.1", + "@juggle/resize-observer": "^3.4.0", + "@rsuite/icons": "^1.3.2", + "@types/lodash": "^4.17.15", + "@types/prop-types": "^15.7.5", + "@types/react-window": "^1.8.5", + "classnames": "^2.3.1", + "date-fns": "^2.29.3", + "dom-lib": "^3.3.1", + "lodash": "^4.17.21", + "prop-types": "^15.8.1", + "react-use-set": "^1.0.0", + "react-window": "^1.8.8", + "rsuite-table": "^5.19.2", + "schema-typed": "^2.4.2" + }, + "dependencies": { + "date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "requires": { + "@babel/runtime": "^7.21.0" + } + } + } + }, + "rsuite-table": { + "version": "5.19.2", + "resolved": "https://registry.npmjs.org/rsuite-table/-/rsuite-table-5.19.2.tgz", + "integrity": "sha512-0mnAuvTlDjNGo3FTWqIMdlCP2+gx8NJiMYJnGvOoYMt/kcxRsWzayQRrywc2cvnHTEOjMIQFi2uHYfie0irAHg==", + "requires": { + "@babel/runtime": "^7.12.5", + "@juggle/resize-observer": "^3.3.1", + "classnames": "^2.3.1", + "dom-lib": "^3.3.1", + "lodash": "^4.17.21" + } + }, "rw": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", @@ -1609,6 +2705,24 @@ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==" }, + "schema-typed": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/schema-typed/-/schema-typed-2.4.2.tgz", + "integrity": "sha512-4eYZiheiPps+I7JEKrhm/S8OIPncXqY0lKQbvI/Agn9QMJUQ3cgfFZ2spy4Ta9Qr3xLYB3/qj4wGbsNcVwEO/w==", + "requires": { + "lodash": "^4.17.21" + } + }, + "semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==" + }, + "shallow-equal": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz", + "integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==" + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -1629,6 +2743,11 @@ "resolved": "https://registry.npmjs.org/size-sensor/-/size-sensor-1.0.2.tgz", "integrity": "sha512-2NCmWxY7A9pYKGXNBfteo4hy14gWu47rg5692peVMst6lQLPKrVjhY+UTEsPI5ceFRJSl3gVgMYaUi/hKuaiKw==" }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==" + }, "source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -1641,6 +2760,16 @@ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, + "stubborn-fs": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-1.2.5.tgz", + "integrity": "sha512-H2N9c26eXjzL/S/K+i/RHHcFanE74dptvvjM8iwzwbVcWY/zjBbgRqF3K0DY4+OD+uTTASTBvDoxPDaPN02D7g==" + }, + "stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -1650,6 +2779,11 @@ "has-flag": "^4.0.0" } }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" + }, "svg-text-fit": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/svg-text-fit/-/svg-text-fit-1.2.3.tgz", @@ -1669,6 +2803,11 @@ "kapsule": "^1.16" } }, + "tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" + }, "timelines-chart": { "version": "2.14.2", "resolved": "https://registry.npmjs.org/timelines-chart/-/timelines-chart-2.14.2.tgz", @@ -1717,6 +2856,26 @@ "prelude-ls": "^1.2.1" } }, + "type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==" + }, + "update-input-width": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/update-input-width/-/update-input-width-1.4.2.tgz", + "integrity": "sha512-/p0XLhrQQQ4bMWD7bL9duYObwYCO1qGr8R19xcMmoMSmXuQ7/1//veUnCObQ7/iW6E2pGS6rFkS4TfH4ur7e/g==" + }, + "uplot": { + "version": "1.6.32", + "resolved": "https://registry.npmjs.org/uplot/-/uplot-1.6.32.tgz", + "integrity": "sha512-KIMVnG68zvu5XXUbC4LQEPnhwOxBuLyW1AHtpm6IKTXImkbLgkMy+jabjLgSLMasNuGGzQm/ep3tOkyTxpiQIw==" + }, + "uplot-react": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/uplot-react/-/uplot-react-1.2.4.tgz", + "integrity": "sha512-mDe/mqD9KtXeHDR8llSJaUFpDcEJvYpHNS+cyUhJ2qvkbT9GPKod1BVXG+hNegRqYiV1ldsFBlI5+OKSi/yPNA==" + }, "uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -1726,6 +2885,11 @@ "punycode": "^2.1.0" } }, + "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", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==" + }, "vite": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.3.tgz", @@ -1741,6 +2905,19 @@ "tinyglobby": "^0.2.14" } }, + "warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "requires": { + "loose-envify": "^1.0.0" + } + }, + "when-exit": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.4.tgz", + "integrity": "sha512-4rnvd3A1t16PWzrBUcSDZqcAmsUIy4minDXT/CZ8F2mVDgd65i4Aalimgz1aQkRGU0iH5eT5+6Rx2TK8o443Pg==" + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -1756,6 +2933,11 @@ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true }, + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" + }, "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/SearchFrontend/search_ui/package.json b/SearchFrontend/search_ui/package.json index 2dce901..993bd53 100644 --- a/SearchFrontend/search_ui/package.json +++ b/SearchFrontend/search_ui/package.json @@ -10,12 +10,29 @@ "preview": "vite preview" }, "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@mui/material": "^7.3.2", + "@mui/x-date-pickers": "^8.11.2", + "@mui/x-date-pickers-pro": "^8.11.2", + "@wojtekmaj/react-daterange-picker": "^7.0.0", "d3": "^7.9.0", + "date-fns": "^4.1.0", + "dayjs": "^1.11.18", "echarts": "^6.0.0", "echarts-for-react": "^3.0.2", + "flex-layout-system": "^2.0.3", "react": "^19.1.1", + "react-calendar": "^6.0.0", + "react-date-range": "^2.0.1", + "react-datepicker": "^8.7.0", "react-dom": "^19.1.1", - "timelines-chart": "^2.14.2" + "react-flexbox-grid": "^2.1.2", + "react-split-pane": "^0.1.92", + "rsuite": "^5.83.3", + "timelines-chart": "^2.14.2", + "uplot": "^1.6.32", + "uplot-react": "^1.2.4" }, "devDependencies": { "@eslint/js": "^9.33.0", diff --git a/SearchFrontend/search_ui/src/App.css b/SearchFrontend/search_ui/src/App.css index 027945e..6077332 100644 --- a/SearchFrontend/search_ui/src/App.css +++ b/SearchFrontend/search_ui/src/App.css @@ -1,6 +1,114 @@ +/* Root container */ #root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; + width: 100vw; + max-width: 100vw; + margin: 0; + padding: 0; text-align: center; -} \ No newline at end of file + background: #181a20; +} + +/* Video.js player */ +.video-js-mod { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + object-fit: contain; + background: #000; +} + +.vjs-tech { + object-fit: contain; +} + +/* Main app layout */ +.app-container { + display: flex; + flex-direction: column; + height: 100vh; + width: 100vw; + margin: 0; + padding: 0; + gap: 12px; + /* background: #181a20; */ +} + + +.flex-group { + display: flex; + flex-direction: column; /* or 'row' if you want horizontal grouping */ + flex: 1 1 0; + min-width: 0; +} +/* Section containers */ +.section-box-horiz { + overflow: visible; + flex-direction: row; + display: flex; + align-items: center; + justify-content: center; +} + +/* Section containers */ +.section-box { + flex: 0 0 5%; + overflow: visible; + /* background: #23272f; */ + /* padding: 0; + box-sizing: border-box; + border-radius: 10px; + margin: 0 16px; + box-shadow: 0 2px 8px rgba(0,0,0,0.10); */ + display: flex; + align-items: center; + justify-content: center; +} + +.timeline-container { + flex: 0 0 24%; + overflow: visible; + background: #20232a; + padding: 0; + box-sizing: border-box; + border-radius: 10px; + margin: 0 16px; + box-shadow: 0 2px 8px rgba(0,0,0,0.10); + display: flex; + align-items: center; + justify-content: center; +} + +.section-box:last-of-type { + flex: 1 1 68%; + overflow: hidden; + background: #23272f; + padding: 0; + box-sizing: border-box; + border-radius: 10px; + margin: 0 16px 16px 16px; + box-shadow: 0 2px 8px rgba(0,0,0,0.10); + display: flex; + align-items: center; + justify-content: center; +} + +/* Responsive tweaks */ +@media (max-width: 600px) { + .app-container { + gap: 6px; + } + .section-box, + .timeline-container, + .section-box:last-of-type { + margin: 0 4px; + border-radius: 6px; + padding: 0; + } + .date-range-selector { + max-width: 98vw; + padding: 12px 8px; + border-radius: 8px; + } +} diff --git a/SearchFrontend/search_ui/src/App.jsx b/SearchFrontend/search_ui/src/App.jsx index 373ceb1..25b91b7 100644 --- a/SearchFrontend/search_ui/src/App.jsx +++ b/SearchFrontend/search_ui/src/App.jsx @@ -1,14 +1,289 @@ -import React from 'react'; -import EmbedTimeline from './components/EmbedTimeline'; -import './App.css'; -import data_results from "./util/embed_results_web.json" +"use client"; +import React, { useState, useEffect, useRef, useCallback } from "react"; +import EmbedTimeline from "./components/EmbedTimeline"; +import VideoPlayer from "./components/VideoPlayer"; +// import ModernDateRangeSelector from './components/ModernDateRangeSelector'; +import CompactDateRangePicker from "./components/CompactDateRangePicker"; +import CustomDateRangePicker from "./components/DateRangePicker"; +import "./App.css"; +import StatusesDisplayHUD from "./components/StatusDisplay"; function App() { + const original_data = useRef(null); + const chartRef = useRef(null); + const [dataResults, setDataResults] = useState(null); + const [statusMessages, setStatusMessages] = useState([]); + const [markerTime, setMarkerTime] = useState(0); + const playerRef = useRef(null); + const playerInstanceRef = useRef(null); + // State for the values + window.chartRef = chartRef; + window.playerRef = playerRef; + window.playerInstanceRef = playerInstanceRef; + // Slider states + + const [sliderMin, setSliderMin] = useState(0.0); + const [sliderMax, setSliderMax] = useState(1.0); + // Date range states + // + + const [startRange, setStartRange] = useState( + new Date(new Date().getTime() - 7 * 24 * 60 * 60 * 1000) + ); + const [endRange, setEndRange] = useState(new Date()); + // const [endRange, setEndRange] = useState(new Date(new Date().getTime() - 6 * 24 * 60 * 60 * 1000)); + const [queryText, setQueryText] = useState("A clouded leopard and a human"); + const [sliderValue, setSliderValue] = useState(0); + + // State to track last submitted values + const [lastSubmitted, setLastSubmitted] = useState({ + startRange, + endRange, + sliderValue, + queryText, + }); + + // Check if any value has changed + const hasChanged = + startRange !== lastSubmitted.startRange || + endRange !== lastSubmitted.endRange || + sliderValue !== lastSubmitted.sliderValue || + queryText !== lastSubmitted.queryText; + + // Function to resubmit fetch + const handleResubmit = () => { + // Start streaming status updates + fetch("api/return_status") + .then((response) => { + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ""; // Accumulate partial text + + function read() { + reader.read().then(({ done, value }) => { + if (done) { + if (buffer) { + // console.log("Status:", buffer); // Log any remaining text + } + setStatusMessages([]); + // console.log("Status stream finished"); + + return; + } + // Decode only the new chunk + buffer += decoder.decode(value, { stream: true }); + + // If your server sends lines, split and log only complete lines: + let lines = buffer.split("\n"); + buffer = lines.pop(); // Save incomplete line for next chunk + + for (const line of lines) { + if (line.trim()) { + // console.log("Status:", line); + console.log(line) + setStatusMessages((msgs) => [...msgs, JSON.parse(line)]); + } + } + + read(); + }); + } + read(); + }) + .catch((error) => { + console.error("Error while streaming status:", error); + }); + + const params = new URLSearchParams(); + params.append("startRange", startRange.toISOString()); + params.append("endRange", endRange.toISOString()); + params.append("threshold", 0.0); + params.append("query", queryText); + setDataResults({ videos: [], breaks: [] }); + + fetch("api/videos.json?" + params.toString()) + .then((res) => res.json()) + .then((data) => { + const max_value = Math.max( + ...data["videos"].map((vid) => vid["embed_scores"]["score"][1]) + ); + setSliderMax(max_value); + original_data.current = data; + window.original_data = original_data; + setDataResults(data); + }); + + setLastSubmitted({ startRange, endRange, sliderValue, queryText }); + }; + + function updateDataAndValue(newValue) { + const floatValue = parseFloat(newValue); + setSliderValue(floatValue); + var newData = JSON.parse(JSON.stringify(original_data.current)); + newData["videos"] = newData["videos"].filter( + (vid) => vid["embed_scores"]["score"][1] >= floatValue + ); + setDataResults(newData); + } + + function setMarkerValueNonReactive(inputValue) { + let chart = chartRef.current.getEchartsInstance(); + let options = chart.getOption(); + let mappers = options["mappers"]; + + let vv = { + xAxis: mappers["real_to_virtual"](new Date(inputValue)), + lineStyle: { type: "solid", color: "#FF0000", width: 2 }, + label: { + show: false, + formatter: "Break", + position: "bottom", + color: "#888", + fontSize: 10, + }, + }; + + let markLine = { + symbol: ["none", "none"], + data: [vv], + lineStyle: { type: "dashed", color: "#FF0000", width: 2 }, + silent: true, + animation: false, + }; + + // if ("markLine" in options["series"][1]) { + if (false) { + let vv_new = { + xAxis: mappers["real_to_virtual"](new Date(inputValue)), + }; + let markLine_new = { + data: [vv_new], + }; + + chart.setOption( + { + series: [{}, { markLine: { data: [vv_new] } }], + }, + false, + ["series.markLine"] + ); + } else { + chart.setOption( + { + series: [{}, { markLine: markLine }], + }, + false, + ["series.markLine"] + ); + } + } + + // Memoize the timeline click handler + const handleTimelineClick = useCallback( + (path, timeoffset) => { + console.log("Timeline clicked:", path, timeoffset); + + if (playerRef.current && playerInstanceRef.current) { + console.log("Seeking video player to:", path, timeoffset); + playerInstanceRef.current.src({ + src: "api/" + path, + type: "video/mp4", + }); + playerInstanceRef.current.on("loadedmetadata", () => { + playerInstanceRef.current.currentTime(timeoffset); + }); + } + }, + [] // Empty dependency array since it only uses playerRef + ); + + useEffect(() => { + const params = new URLSearchParams(window.location.search); // id=123 + + if (params.get("test_mode") == "true") { + setStartRange(new Date(new Date().getTime() - 2 * 24 * 60 * 60 * 1000)); + setEndRange(new Date(new Date().getTime() - 1 * 24 * 60 * 60 * 1000)); + } + handleResubmit(); + }, []); return ( -
-

Embed Timeline Visualization

- +
+
+
+ +
+
+ setQueryText(e.target.value)} + style={{ + marginLeft: "16px", + marginRight: "16px", + padding: "8px", + borderRadius: "4px", + border: "1px solid #343a40", + color: "#fff", // Text white + backgroundColor: "#23272f", // Optional: dark background for contrast + }} + /> +
+
+ +
+
+ updateDataAndValue(e.target.value)} + style={{ + width: "120px", + color: "#fff", // Text white + backgroundColor: "#23272f", // Optional: dark background for contrast + }} + /> +
+
+ + {sliderValue.toFixed(2)} + +
+
+ +
+
+
+ +
+ +
+ +
+
+ +
); } diff --git a/SearchFrontend/search_ui/src/components/CompactDateRangePicker.jsx b/SearchFrontend/search_ui/src/components/CompactDateRangePicker.jsx new file mode 100644 index 0000000..6c2fc21 --- /dev/null +++ b/SearchFrontend/search_ui/src/components/CompactDateRangePicker.jsx @@ -0,0 +1,28 @@ +import React, { useState } from "react"; +import DatePicker from "react-datepicker"; +import "react-datepicker/dist/react-datepicker.css"; + +export default function CompactDateRangePicker({ startDate, endDate, setStartDate, setEndDate}) { + // const [startDate, setStartDate] = useState(null); + // const [endDate, setEndDate] = useState(null); + console.log(startDate) + console.log(endDate) + console.log(setStartDate) + console.log(setEndDate) + return ( + { + setStartDate(start); + setEndDate(end); + if (end && onChange) onChange({ startDate: start, endDate: end }); + }} + isClearable + maxDate={new Date()} + placeholderText="Select date range" + withPortal + /> + ); +} \ No newline at end of file diff --git a/SearchFrontend/search_ui/src/components/DateRangePicker.jsx b/SearchFrontend/search_ui/src/components/DateRangePicker.jsx new file mode 100644 index 0000000..3bb45da --- /dev/null +++ b/SearchFrontend/search_ui/src/components/DateRangePicker.jsx @@ -0,0 +1,77 @@ +import React, { useState, useRef, useEffect } from "react"; + +import "react-date-range/dist/styles.css"; // main css file +import "react-date-range/dist/theme/default.css"; // theme css file +import { DateRange } from "react-date-range"; + +export default function CustomDateRangePicker({ startDate, endDate, setStartRange, setEndRange }) { + const minDate = new Date("2025-07-01") + const maxDate = new Date() + const [showCalendar, setShowCalendar] = useState(false); + const calendarRef = useRef(null); + + // Create range object for react-date-range + const range = [{ + startDate: startDate, + endDate: endDate, + key: 'selection' + }]; + + const handleSelect = (ranges) => { + const { startDate: newStart, endDate: newEnd } = ranges.selection; + + setStartRange(newStart); + setEndRange(newEnd); + + if ( + newStart && + newEnd && + newStart.getTime() !== newEnd.getTime() + ) { + setShowCalendar(false); + } + }; + + // Hide calendar when clicking outside + useEffect(() => { + const handleClickOutside = (event) => { + if ( + calendarRef.current && + !calendarRef.current.contains(event.target) + ) { + setShowCalendar(false); + } + }; + + if (showCalendar) { + document.addEventListener("mousedown", handleClickOutside); + } else { + document.removeEventListener("mousedown", handleClickOutside); + } + + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [showCalendar]); + + return ( +
+ + + {showCalendar && ( +
+ +
+ )} +
+ ); +} \ No newline at end of file diff --git a/SearchFrontend/search_ui/src/components/EmbedTimeline.css b/SearchFrontend/search_ui/src/components/EmbedTimeline.css index 447a9c6..e69de29 100644 --- a/SearchFrontend/search_ui/src/components/EmbedTimeline.css +++ b/SearchFrontend/search_ui/src/components/EmbedTimeline.css @@ -1,28 +0,0 @@ -.time-block { - opacity: 0.5; - stroke-width: 1; - fill: #4CAF50; - stroke: #2E7D32; -} - -.score-line { - fill: none; - stroke-width: 2; - stroke: #4CAF50; -} - -.score-dot { - r: 4; - stroke: white; - stroke-width: 1; - fill: #4CAF50; -} - -.axis { - font-size: 12px; -} -.grid-line { - stroke: #e0e0e0; - stroke-dasharray: 2,2; - opacity: 0.7; -} diff --git a/SearchFrontend/search_ui/src/components/EmbedTimeline.jsx b/SearchFrontend/search_ui/src/components/EmbedTimeline.jsx index 53731c0..31a7d04 100644 --- a/SearchFrontend/search_ui/src/components/EmbedTimeline.jsx +++ b/SearchFrontend/search_ui/src/components/EmbedTimeline.jsx @@ -1,361 +1,481 @@ -import React, { useRef, useEffect, useState } from 'react'; -import ReactECharts from 'echarts-for-react'; -// import './EmbedTimeline.css'; +import React, { useRef, useEffect } from "react"; +import ReactECharts from "echarts-for-react"; +const EmbedTimeline = React.memo(function EmbedTimeline({ + chartRef, + data_in, + onTimelineClick, + markerTime, +}) { + // --- Early return if loading --- + if (!data_in) return
Loading....
; -export default function EmbedTimeline({ data_in }) { + // --- Constants --- + const BREAK_GAP = 0; + console.log("REDRAW"); + const timeFormatOptions = { + withSeconds: { + month: "short", + day: "numeric", + hour: "numeric", + minute: "2-digit", + second: "numeric", + hour12: true, + }, + edges: { + month: "short", + day: "numeric", + hour: "numeric", + minute: "2-digit", + hour12: true, + }, + within: { + hour: "numeric", + minute: "2-digit", + hour12: true, + }, + }; - var result = [] + const mean = (data) => { + if (data.length < 1) { + return; + } + return data.reduce((prev, current) => prev + current) / data.length; + }; + function prepareVideoData(videos) { + let new_data = []; + videos.forEach((item) => { + let start_time = new Date(1000 * item["start_time"]); + if ("embed_scores" in item) { + var mean_val = item["embed_scores"]["time"] / 2; + // var max_score = Math.max(...item['embed_scores']['score']) + var max_score = item["embed_scores"]["score"][1]; + var max_score_time = new Date( + start_time.getTime() + 1000 * item["embed_scores"]["score"][3] + ); + var new_time = new Date(start_time.getTime() + 1000 * 2 * mean_val); + new_data.push([ + new Date(start_time.getTime()), + new_time, + max_score, + max_score_time, + ]); + // new_data.push([new_time, item['embed_scores']['score'][idx]]); - for (let idx_outer = 0; idx_outer < data_in.length; idx_outer++) { - let item = data_in[idx_outer] - let start_time = Date.parse(item["start_time"]) - var new_data = []; - if ('embed_scores' in item) { - for (let idx = 0; idx < item['embed_scores']['time'].length; idx++) { - var new_time = 1000 * item['embed_scores']['time'][idx] + start_time + // Math.max.apply(Math, item['embed_scores']['time'].map(function(o) { return o.y; })) + // item['embed_scores']['time'].forEach((sec, idx) => { + // let new_time = new Date(start_time.getTime() + 1000 * sec); - new_data.push([new_time, item['embed_scores']['score'][idx]]) - } - } - result.push(new_data) + // new_data.push([new_time, item['embed_scores']['score'][idx]]); + // }); + } + }); + // Remove duplicates and sort + return Array.from(new Set(new_data.map(JSON.stringify)), JSON.parse).sort( + (a, b) => new Date(a[0]) - new Date(b[0]) + ); + } + + function calculateBreaks(videos) { + const breaks = []; + if (videos.length < 3) { + return breaks; + } + let t_diff = videos.at(-1)["end_time"] - videos[0]["start_time"]; + + for (let i = 0; i < videos.length - 1; i++) { + let end_now = videos[i]["end_time"]; + let start_next = videos[i + 1]["start_time"]; + if (start_next - end_now > 60 * 60) { + // still in unix timestamp. break only if spaces of 60 minutes + breaks.push([end_now, start_next]); + } } - function reframe_data(item, idx) { + return breaks; + } + + function fillNulls(data) { + const with_nulls = []; + for (let i = 0; i < data.length; i++) { + with_nulls.push(data[i]); + if (i < data.length - 1) { + const curr_time = new Date(data[i][0]).getTime(); + const next_time = new Date(data[i + 1][0]).getTime(); + if (next_time - curr_time > 1000) { + // with_nulls.push([new Date(curr_time + 1), null]); + // with_nulls.push([new Date(curr_time + 1), 0]); + } + } + } + return with_nulls; + } + + function prepareBreaks(breaksRaw) { + return breaksRaw.map(([start, end]) => ({ + start: new Date(1000 * start), + end: new Date(1000 * end), + gap: BREAK_GAP, + isExpanded: false, + })); + } + function buildVirtualTimeMapper(breaks) { + const sortedBreaks = breaks.slice().sort((a, b) => a.start - b.start); + return function (realDate) { + let offset = 0; + let realMs = realDate.getTime(); + for (const br of sortedBreaks) { + if (realMs >= br.end.getTime()) { + offset += br.end.getTime() - br.start.getTime(); + } else if (realMs > br.start.getTime()) { + offset += realMs - br.start.getTime(); + break; + } + } + return realMs - offset; + }; + } + + function mapVirtualToRealTime(virtualMs, breaks, virtualTime) { + let realMs = virtualMs; + for (const br of breaks) { + const breakStartVirtual = virtualTime(br.start); + const breakDuration = br.end.getTime() - br.start.getTime(); + if (virtualMs >= breakStartVirtual) { + realMs += breakDuration; + } + } + return realMs; + } + + function buildSeries(item, idx) { + const data = item.map(function (item, index) { + return { + value: item, + }; + }); + + console.log(data) + + return { + type: "custom", + renderItem: function (params, api) { + var yValue = api.value(2); + var start = api.coord([api.value(0), yValue]); + var size = api.size([api.value(1) - api.value(0), yValue]); + var style = api.style(); + var maxTime = api.coord([api.value(3), yValue]); return { - type: 'line', - symbol: 'none', - smooth: true, - lineStyle: { - normal: { - color: 'green', - width: 1, - } + type: "group", + children: [ + { + type: "rect", + shape: { + x: start[0], + y: start[1], + width: size[0], + height: size[1], + }, + style: { fill: "#00F0003F" }, }, - data: item - } - } - const series_out = result.map(reframe_data) + { + type: "circle", + shape: { cx: maxTime[0], cy: maxTime[1], r: 1 }, + style: { fill: "#00F0003F" }, + }, + ], + }; + }, + symbol: "none", + smooth: true, + large: true, + lineStyle: { normal: { color: "green", width: 1 } }, + // data: item.map(d => [d[0], d[1], d[2], d[3]]), + data: data, + // sampling: 'lttb', + triggerLineEvent: true, + z: 11, + }; + } - const option = { - xAxis: { - type: 'time', - boundaryGap: false - }, - yAxis: { - type: 'value' - }, - dataZoom: [ + function buildInvisibleHitBoxSeries(item, idx) { + const data = item.map(function (item, index) { + return { + value: item, + }; + }); + + return { + type: "custom", + renderItem: function (params, api) { + var yValue = api.value(2); + var start = api.coord([api.value(0), yValue]); + var size = api.size([api.value(1) - api.value(0), yValue]); + var style = api.style(); + + var maxTime = api.coord([api.value(3), yValue]); + return { + type: "group", + children: [ { - type: 'inside', - start: 0, - end: 100 + type: "rect", + shape: { + x: start[0], + y: start[1], + width: size[0], + height: size[1], + }, + style: { fill: "#00F0003F" }, }, { - start: 0, - end: 100 - } - ], - series: series_out + type: "circle", + shape: { cx: maxTime[0], cy: maxTime[1], r: 1 }, + style: { fill: "#00F0003F" }, + }, + ], + }; + }, + symbol: "none", + smooth: true, + // large: true, + lineStyle: { normal: { color: "green", width: 1 } }, + // data: item.map(d => [d[0], d[1], d[2], d[3]]), + data: data, + // sampling: 'lttb', + triggerLineEvent: true, + z: 11, }; - return ( -
- -
- ); -} + // return { + // type: 'line', + // symbol: 'none', + // smooth: true, + // large: true, + // lineStyle: { width: 100, opacity: 0 }, + // data: item.map(d => [d[0], d[1]]), + // sampling: 'lttb', + // triggerLineEvent: true, + // z: 10 + // }; + } + function buildBlankSeries() { + return { + type: "line", + symbol: "none", + lineStyle: { width: 100, opacity: 0 }, + data: [], + z: 4, + }; + } + // --- Data Processing --- + const videoData = prepareVideoData(data_in["videos"]); + const withNulls = videoData; + data_in["calc_breaks"] = calculateBreaks(data_in["videos"]); + // const withNulls = fillNulls(videoData); + const breaks = prepareBreaks(data_in["calc_breaks"]); + const virtualTime = buildVirtualTimeMapper(breaks); -// const EmbedTimeline = ({ data }) => { -// const containerRef = useRef(null); + const breaks_split = data_in["calc_breaks"].flat(1).map(function (x) { + return x * 1000; + }); + // if (videoData.length > 2) { + // breaks_split.unshift(new Date(videoData[0][0]).getTime()) + // breaks_split.push(new Date(videoData.at(-1)[0]).getTime()) + // } + const paired_splits = []; + for (let i = 0; i < breaks_split.length; i += 2) { + paired_splits.push([ + breaks_split[i], + breaks_split[i + 1], + breaks_split[i] / 2 + breaks_split[i + 1] / 2, + ]); + } + const split_centers = paired_splits.map((d) => new Date(d[2])); + const splitCenterVirtualTimes = split_centers.map((d) => virtualTime(d)); + const splitCenterLabels = split_centers.map((d) => + new Date(d).toLocaleTimeString("en-US", timeFormatOptions.edges) + ); -// useEffect(() => { -// if (!containerRef.current) return; + const splitCenterMarkLines = splitCenterVirtualTimes.map((vt, i) => ({ + xAxis: vt, + // make the line invisible + lineStyle: { width: 0, color: "transparent" }, + // show the precomputed text + label: { + show: true, + formatter: splitCenterLabels[i], + position: "end", // try other values if overlap; 'end', 'insideStartTop', etc. + color: "#FFFFFF", + fontSize: 11, + rotate: 90, + }, + })); -// var myChart = ReactECharts.init(containerRef); + const virtualData = withNulls.map( + ([realStartTime, realEndTime, value, realMaxTime]) => [ + virtualTime(new Date(realStartTime)), + virtualTime(new Date(realEndTime)), + value, + virtualTime(new Date(realMaxTime)), + ] + ); + const result = [virtualData]; + const ymax = Math.max(...virtualData.map((d) => d[2])); + // --- Series --- + const seriesNormal = result.map(buildSeries); + // const seriesInvisible = result.map(buildInvisibleHitBoxSeries); + const series_out = [].concat(seriesNormal, buildBlankSeries()); -// // Specify the configuration items and data for the chart -// var option = { -// title: { -// text: 'ECharts Getting Started Example' -// }, -// tooltip: {}, -// legend: { -// data: ['sales'] -// }, -// xAxis: { -// data: ['Shirts', 'Cardigans', 'Chiffons', 'Pants', 'Heels', 'Socks'] -// }, -// yAxis: {}, -// series: [ -// { -// name: 'sales', -// type: 'bar', -// data: [5, 20, 36, 10, 10, 20] -// } -// ] -// }; + // --- Break MarkLines --- + const breakMarkLines = breaks.map((br) => ({ + xAxis: virtualTime(br.start), + lineStyle: { type: "dashed", color: "#888", width: 2 }, + label: { + show: true, + formatter: "Break", + position: "bottom", + color: "#888", + fontSize: 10, + }, + })); -// // Display the chart using the configuration items and data just specified. -// myChart.setOption(option); -// }); -// return ( -//
-//
-// ) -// } -// export default EmbedTimeline; + // Attach break mark lines to the first series + if (seriesNormal[0]) { + seriesNormal[0].markLine = { + symbol: ["none", "none"], + data: [...(breakMarkLines || []), ...(splitCenterMarkLines || [])], + lineStyle: { type: "dashed", color: "#888", width: 2 }, + label: { show: true, position: "bottom", color: "#888", fontSize: 10 }, + }; + } + // --- Axis & Chart Option --- + const virtual_x_min = virtualData.length > 0 ? virtualData[0][0] : 0; + const virtual_x_max = + virtualData.length > 0 ? virtualData[virtualData.length - 1][0] : 1; + const option = { + animation: false, + // progressive: 0, // Disable progressive rendering + progressiveThreshold: 100000 , // Disable progressive threshold + mappers: { + virtual_to_real: mapVirtualToRealTime, + real_to_virtual: virtualTime, + }, + response: true, + grid: { + top: 30, // Remove top padding + left: 10, + right: 20, + bottom: 60, + containLabel: true, + }, + dataZoom: [ + { + type: "slider", + show: true, + xAxisIndex: [0], + startValue: virtual_x_min, + endValue: virtual_x_max, + filterMode: 'weakFilter', + }, + { + type: "inside", + xAxisIndex: [0], + startValue: virtual_x_min, + endValue: virtual_x_max, + filterMode: 'weakFilter', + }, + ], + xAxis: { + type: "value", + min: virtual_x_min, + max: virtual_x_max, + splitLine: { show: false }, + axisLabel: { + formatter: function (virtualMs) { + let range = virtual_x_max - virtual_x_min; + if ( + chartRef && + chartRef.current && + chartRef.current.getEchartsInstance + ) { + const chart = chartRef.current.getEchartsInstance(); + const dz = chart.getOption().dataZoom?.[0]; + if ( + dz && + dz.startValue !== undefined && + dz.endValue !== undefined + ) { + range = dz.endValue - dz.startValue; + } + } + const realTime = mapVirtualToRealTime(virtualMs, breaks, virtualTime); + if (realTime) { + const useSeconds = range < 5 * 60 * 1000; + const fmt = useSeconds + ? timeFormatOptions.withSeconds + : timeFormatOptions.edges; + return new Date(realTime).toLocaleTimeString("en-US", fmt); + } + return ""; + }, + }, + }, + yAxis: { + type: "value", + min: 0.0, + max: ymax, + splitLine: { show: false }, + }, + series: series_out.map((s) => ({ + ...s, + animation: false, // Disable animation for each series + animationDuration: 0, + })), + }; -// const EmbedTimelineF = ({ data }) => { -// const svgRef = useRef(null); -// const containerRef = useRef(null); -// const [showLabels, setShowLabels] = useState(true); -// const [zoomLevel, setZoomLevel] = useState(1); + // --- Chart Event Handlers --- + async function onChartClick(params, echarts) { + const nativeEvent = params.event.event; + const pixel = [nativeEvent.offsetX, nativeEvent.offsetY]; + const dataCoord = echarts.convertFromPixel({ seriesIndex: 0 }, pixel); -// useEffect(() => { -// if (!svgRef.current) return; + const res = await fetch("/api/events/click", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + timestamp: + mapVirtualToRealTime(dataCoord[0], breaks, virtualTime) / 1000, + }), + }); + if (!res.ok) throw new Error(`HTTP error: ${res.status}`); + const { path, timeoffset } = await res.json(); + if (onTimelineClick) + onTimelineClick(path, virtualTime(new Date(timeoffset))); + } -// // Clear any existing SVG content -// d3.select(svgRef.current).selectAll("*").remove(); - -// // Parse dates and prepare data -// const parseTime = d3.timeParse("%Y-%m-%dT%H:%M:%S.%f"); -// const parseTimeAlt = d3.timeParse("%Y-%m-%dT%H:%M:%S"); - -// const parseDate = (dateStr) => { -// return parseTime(dateStr) || parseTimeAlt(dateStr); -// }; - -// const processedData = data.map((d, i) => ({ -// id: i, -// startTime: parseDate(d.start_time), -// endTime: parseDate(d.end_time), -// scores: d.embed_scores.time.map((time, j) => ({ -// time: parseDate(time), -// score: d.embed_scores.score[j] -// })) -// })); - -// // Set up dimensions -// const margin = { top: 50, right: 50, bottom: 50, left: 50 }; -// const plotHeight = 300; -// const blockHeight = 30; -// const baseWidth = 1000; -// const width = baseWidth * zoomLevel - margin.left - margin.right; - -// // Find overall time range -// const allTimes = processedData.flatMap(d => [d.startTime, d.endTime, ...d.scores.map(s => s.time)]); -// const timeExtent = d3.extent(allTimes); - -// // Add some padding to time range -// const timePadding = (timeExtent[1] - timeExtent[0]) * 0.02; -// timeExtent[0] = new Date(timeExtent[0].getTime() - timePadding); -// timeExtent[1] = new Date(timeExtent[1].getTime() + timePadding); - -// // Find score range -// const allScores = processedData.flatMap(d => d.scores.map(s => s.score)); -// const scoreExtent = d3.extent(allScores); -// const scorePadding = (scoreExtent[1] - scoreExtent[0]) * 0.1; -// scoreExtent[0] -= scorePadding; -// scoreExtent[1] += scorePadding; - -// // Create main SVG -// const svg = d3.select(svgRef.current) -// .attr("width", width + margin.left + margin.right) -// .attr("height", plotHeight + margin.top + margin.bottom); - -// const mainGroup = svg.append("g") -// .attr("transform", `translate(${margin.left}, ${margin.top})`); - -// // Create scales -// const xScale = d3.scaleTime() -// .domain(timeExtent) -// .range([0, width]); - -// const yScoreScale = d3.scaleLinear() -// .domain(scoreExtent) -// .range([plotHeight * 0.6, 0]); - -// // Zoom functionality -// const zoom = d3.zoom() -// .scaleExtent([1, 20]) -// .on("zoom", (event) => { -// // Update zoom transform -// const newTransform = event.transform; - -// // Update x-scale with zoom -// const newXScale = newTransform.rescaleX(xScale); - -// // Function to update visualization elements -// const updateVisualization = () => { -// // Update score lines -// mainGroup.selectAll(".score-line") -// .attr("d", d3.line() -// .x(s => newXScale(s.time)) -// .y(s => yScoreScale(s.score)) -// ); - -// // Update score dots -// mainGroup.selectAll(".score-dot") -// .attr("cx", s => newXScale(s.time)); - -// // Update time blocks -// mainGroup.selectAll(".time-block") -// .attr("x", d => newXScale(d.startTime)) -// .attr("width", d => Math.max(2, newXScale(d.endTime) - newXScale(d.startTime))); - -// // Update time labels if visible -// if (showLabels) { -// mainGroup.selectAll(".block-start-label") -// .attr("x", d => newXScale(d.startTime)); -// mainGroup.selectAll(".block-end-label") -// .attr("x", d => newXScale(d.endTime)); -// } - -// // Update x-axis -// mainGroup.select(".x-axis").call( -// d3.axisBottom(newXScale) -// .ticks(8) -// .tickFormat(d3.timeFormat("%H:%M:%S")) -// ); -// }; - -// // Apply updates -// updateVisualization(); -// }); - -// // Add zoom behavior -// svg.call(zoom); - -// // Create line generator -// const line = d3.line() -// .x(d => xScale(d.time)) -// .y(d => yScoreScale(d.score)) -// .curve(d3.curveMonotoneX); - -// // Add grid lines -// const yTicks = yScoreScale.ticks(6); -// mainGroup.selectAll(".grid-line-y") -// .data(yTicks) -// .enter() -// .append("line") -// .attr("class", "grid-line") -// .attr("x1", 0) -// .attr("x2", width) -// .attr("y1", d => yScoreScale(d)) -// .attr("y2", d => yScoreScale(d)); - -// // Add score lines and dots -// processedData.forEach((d, i) => { -// // Score line -// mainGroup.append("path") -// .datum(d.scores) -// .attr("class", `score-line`) -// .attr("d", line); - -// // Score dots -// mainGroup.selectAll(`.score-dot-group-${i}`) -// .data(d.scores) -// .enter() -// .append("circle") -// .attr("class", `score-dot`) -// .attr("cx", s => xScale(s.time)) -// .attr("cy", s => yScoreScale(s.score)); - -// // Time blocks with full data for zoom tracking -// mainGroup.append("rect") -// .datum(d) -// .attr("class", `time-block`) -// .attr("x", xScale(d.startTime)) -// .attr("y", plotHeight * 0.7) -// .attr("width", Math.max(2, xScale(d.endTime) - xScale(d.startTime))) -// .attr("height", blockHeight); - -// // Conditional labels -// if (showLabels) { -// mainGroup.append("text") -// .datum(d) -// .attr("class", "block-start-label") -// .attr("x", xScale(d.startTime)) -// .attr("y", plotHeight * 0.7 + blockHeight + 15) -// .attr("text-anchor", "start") -// .style("font-size", "10px") -// .text(d.startTime.toLocaleTimeString()); - -// mainGroup.append("text") -// .datum(d) -// .attr("class", "block-end-label") -// .attr("x", xScale(d.endTime)) -// .attr("y", plotHeight * 0.7 + blockHeight + 15) -// .attr("text-anchor", "end") -// .style("font-size", "10px") -// .text(d.endTime.toLocaleTimeString()); -// } -// }); - -// // Y-axis for scores -// const yAxis = d3.axisLeft(yScoreScale) -// .ticks(6) -// .tickFormat(d3.format(".4f")); - -// mainGroup.append("g") -// .attr("class", "axis y-axis") -// .call(yAxis); - -// // Y-axis label -// mainGroup.append("text") -// .attr("transform", "rotate(-90)") -// .attr("y", -40) -// .attr("x", -plotHeight / 2) -// .style("text-anchor", "middle") -// .text("Embed Score"); - -// // X-axis -// const xAxis = d3.axisBottom(xScale) -// .ticks(8) -// .tickFormat(d3.timeFormat("%H:%M:%S")); - -// mainGroup.append("g") -// .attr("class", "axis x-axis") -// .attr("transform", `translate(0, ${plotHeight})`) -// .call(xAxis); - -// }, [data, showLabels, zoomLevel]); - -// // Zoom control handler -// const handleZoom = (factor) => { -// const currentZoom = zoomLevel; -// const newZoom = Math.max(1, Math.min(20, currentZoom * factor)); -// setZoomLevel(newZoom); -// }; - -// // Reset zoom -// const handleResetZoom = () => { -// setZoomLevel(1); -// }; - -// return ( -//
-//
-// -// -// -// -//
-//
-// -//
-//
-// ); -// }; + function onChartReady(echarts) { + // Chart is ready + } + const onEvents = { click: onChartClick }; + window.chartRef2 = chartRef; + // --- Render --- + return ( + + ); +}); +export default EmbedTimeline; diff --git a/SearchFrontend/search_ui/src/components/StatusDisplay.jsx b/SearchFrontend/search_ui/src/components/StatusDisplay.jsx new file mode 100644 index 0000000..26b29db --- /dev/null +++ b/SearchFrontend/search_ui/src/components/StatusDisplay.jsx @@ -0,0 +1,79 @@ + + + + + + + +export default function StatusesDisplayHUD({ statusMessages }) { + + + const msg = {}; + + statusMessages.forEach((m) => { + let when_key = 'other' + if (m['task'] == 'SCHEDULED') + m['when'].forEach(( w ) => { msg[w] = 'Scheduled' }) + else { + if ('when' in m) + when_key = m['when'] + msg[when_key] = m['task'] + } + + + + }); + + return ( +
+ {Object.entries(msg).map(([when, messages], idx) => ( + + ))} +
+ ); +} + + +export function StatusDisplay({when, message }) { + let msg_show = '' + + msg_show = when + ': ' + message + + + + return ( +
+ {msg_show} +
+ ); + +} + + + + +//
+// {statusMessages.map((msg, idx) => ( +//
{msg}
+// ))} +//
diff --git a/SearchFrontend/search_ui/src/components/VideoPlayer.jsx b/SearchFrontend/search_ui/src/components/VideoPlayer.jsx new file mode 100644 index 0000000..382edae --- /dev/null +++ b/SearchFrontend/search_ui/src/components/VideoPlayer.jsx @@ -0,0 +1,72 @@ +import React, { useRef, useEffect, forwardRef, useImperativeHandle } from "react"; +import videojs from "video.js"; +import "video.js/dist/video-js.css"; + +const VideoPlayer = function VideoPlayer({videoRef, playerInstanceRef, setMarkerTimeFunc}) { + + + + useEffect(() => { + // Prevent double init in StrictMode + if (!playerInstanceRef.current && videoRef.current) { + playerInstanceRef.current = videojs(videoRef.current, { + controls: true, + preload: "auto", + autoplay: true, + }); + + playerInstanceRef.current.on('timeupdate', async function (event) { + + const res = await fetch('api/events/video_step', { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ timestamp: this.currentTime() }), + }); + if (!res.ok) throw new Error(`HTTP error: ${res.status}`); + const { path, timeoffset, do_update, absolute_time} = await res.json(); + setMarkerTimeFunc(1000*absolute_time) + if (do_update) { + playerInstanceRef.current.src({ src: 'api/' + path, type: "video/mp4" }); + + // Seek after metadata is loaded + playerInstanceRef.current.on("loadedmetadata", () => { + playerInstanceRef.current.currentTime(timeoffset); + }); + } + + + } + ) + } + + return () => { + if (playerInstanceRef.current) { + playerInstanceRef.current.dispose(); + playerInstanceRef.current = null; + } + }; + }, []); + + + return ( +
+
+
+
+ ); +}; + +export default VideoPlayer; diff --git a/SearchFrontend/search_ui/src/index.css b/SearchFrontend/search_ui/src/index.css index 08a3ac9..d44f8ea 100644 --- a/SearchFrontend/search_ui/src/index.css +++ b/SearchFrontend/search_ui/src/index.css @@ -3,10 +3,11 @@ line-height: 1.5; font-weight: 400; - color-scheme: light dark; + color-scheme: light dark ; color: rgba(255, 255, 255, 0.87); background-color: #242424; + font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; diff --git a/SearchFrontend/search_ui/src/main.jsx b/SearchFrontend/search_ui/src/main.jsx index b9a1a6d..af4624b 100644 --- a/SearchFrontend/search_ui/src/main.jsx +++ b/SearchFrontend/search_ui/src/main.jsx @@ -4,7 +4,7 @@ import './index.css' import App from './App.jsx' createRoot(document.getElementById('root')).render( - + // - , + // , ) diff --git a/SearchFrontend/search_ui/vite.config.js b/SearchFrontend/search_ui/vite.config.js index 3518ceb..c4b563d 100644 --- a/SearchFrontend/search_ui/vite.config.js +++ b/SearchFrontend/search_ui/vite.config.js @@ -4,5 +4,13 @@ import react from '@vitejs/plugin-react-swc' // https://vite.dev/config/ export default defineConfig({ plugins: [react()], - server: {'host':'0.0.0.0'} + server: {'host':'0.0.0.0', + 'proxy':{ + '/api': { + target: 'http://192.168.1.242:5003', + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api/, ''), + } + } + } }) diff --git a/SearchInterface.code-workspace b/SearchInterface.code-workspace new file mode 100644 index 0000000..7e17fe3 --- /dev/null +++ b/SearchInterface.code-workspace @@ -0,0 +1,10 @@ +{ + "folders": [ + { + "path": "." + }, + { + "path": "../../../Seafile/Designs/Code/Python/CommonCode" + } + ] +} \ No newline at end of file diff --git a/SearchScratch/test_recreate_cache.py b/SearchScratch/test_recreate_cache.py new file mode 100644 index 0000000..7f8be44 --- /dev/null +++ b/SearchScratch/test_recreate_cache.py @@ -0,0 +1,13 @@ +import sys, os +sys.path.append("/home/thebears/Web/Nuggets/SearchInterface/SearchUtil") +sys.path.append("/home/thebears/Web/Nuggets/SearchInterface/VectorService/util") +import embed_scores as ES + +cd = '/srv/ftp_tcc/leopards1/2025/09/13/' +o = ES.calculate_embedding_score_in_folder(cd, 0.1, query='Two cats'); + +# %% +from CommonCode.video_meta import FTPVideo +f='/srv/ftp_tcc/leopards1/2025/09/13/Leopards1_00_20250913135952.mp4' +c = FTPVideo(f) +c.embeddings diff --git a/SearchScratch/test_seek.py b/SearchScratch/test_seek.py new file mode 100644 index 0000000..5260029 --- /dev/null +++ b/SearchScratch/test_seek.py @@ -0,0 +1,182 @@ +# %% +import sys, os +sys.path.append("/home/thebears/Web/Nuggets/SearchInterface/SearchUtil") +sys.path.append("/home/thebears/Web/Nuggets/SearchInterface/VectorService/util") +import embed_scores as ES +# %% +query = 'Cat and human' +c_dir = '/srv/ftp_tcc/leopards1/2025/09/08' +threshold=0.10 + +results = ES.calculate_embedding_score_in_folder(c_dir, threshold, query) +print(len(results['videos'])) + +# %% +c_dir = '/srv/ftp_tcc/leopards1/2025/09/08' +query_vector = None +og_dir = c_dir + +if query_vector is None: + query_vector = ES.get_query_vector(query) + +candidate_dirs = list() +candidate_dirs.append(og_dir) +candidate_dirs.append(og_dir.replace('/srv/ftp_tcc','/mnt/hdd_24tb_1/videos/ftp')) +candidate_dirs.append(og_dir.replace('/srv/ftp','/mnt/hdd_24tb_1/videos/ftp')) + +c_dir = None +for candidate in candidate_dirs: + if os.path.exists(candidate): + c_dir = candidate + break +if c_dir is None: +# return [] + pass +from embed_scores import * +redis_key = 'helllo' +vec_cache_str = md5(query_vector).hexdigest() +cache_file_loc = os.path.join(c_dir, 'embedding_scores@'+str(threshold)+'@'+vec_cache_str+'.pkl') + + + +vec_rep = get_vector_representation(c_dir, redis_key = redis_key) +query_scores = get_scores_embedding_c_dir(c_dir, tuple(query_vector.tolist()[0])) + +video_json_info = list() +idces_keep = np.where(query_scores > threshold)[0] + +video_id = vec_rep['idces'][idces_keep] +videos_that_match = np.unique(video_id) + +id_extract_video_level = np.where(np.isin(vec_rep['idces'], videos_that_match))[0] + +idces_split = np.where(np.diff(vec_rep['idces'][id_extract_video_level]) !=0)[0] + 1 +subset_timestampsF = np.split(vec_rep['timestamps'][id_extract_video_level], idces_split) + + + +for idx, subset_t in enumerate(subset_timestampsF): + if len(subset_t) == 0: + continue + + min_t = min(subset_t) + max_t = max(subset_t) + print(idx, max_t - min_t) + idces_curr = np.where(np.logical_and(vec_rep['timestamps'] > min_t , vec_rep['timestamps'] < max_t))[0] + if len(idces_curr) == 0: + continue + + unq_vids = np.unique(vec_rep['idces'][idces_curr]) + subset_idx = np.where(np.isin(vec_rep['idces'],unq_vids))[0] + + subset_idces = vec_rep['idces'][subset_idx] + subset_timestamps = vec_rep['timestamps'][subset_idx] + subset_scores = query_scores[subset_idx] + idx_split = np.where(np.diff(vec_rep['idces'][subset_idx]) !=0)[0]+1 + + split_idces = np.split(subset_idces, idx_split) + split_timestamps = np.split(subset_timestamps, idx_split) + split_scores = np.split(subset_scores, idx_split) + split_files = [vec_rep['source_files'][x[0]] for x in split_idces] + + for s_file, s_scores, s_tstamps, s_idces in zip(split_files, split_scores, split_timestamps, split_idces): + start_time = float(min(s_tstamps)) + end_time = float(max(s_tstamps)) + + frame_time = (s_tstamps - start_time).tolist() + embed_scores = s_scores.tolist() + + c_data = {'file_name': str(s_file), 'start_time':start_time, 'end_time':end_time, 'embed_scores':{'time':frame_time, 'score':embed_scores}} + video_json_info.append(c_data) + + +print(len(video_json_info)) + +# %% +query = 'A cat and a human' +c_dirs = ['/mnt/hdd_24tb_1/videos/ftp/leopards2/2025/08/26','/srv/ftp_tcc/leopards1/2025/08/27','/srv/ftp_tcc/leopards1/2025/08/28','/srv/ftp_tcc/leopards1/2025/08/29'] + +threshold = 0.10 +folder_scores = ES.calculate_embedding_score_in_folders( tuple(c_dirs), threshold = threshold, query = query ) +folder_scores['breaks'] = ES.add_breaks_between_videos(folder_scores) +# %% +target_tstamp = 1756332686.5805347 + + +matching_file = None +for video_file in folder_scores['videos']: + start_time = video_file['start_time'] + end_time = video_file['end_time'] + + if target_tstamp > start_time and target_tstamp < end_time: + matching_file = video_file + +if matching_file is not None: + fname = video_file['file_name'] + offset = target_tstamp - start_time +pelse: + fname = 'None Found' + offset = -1 + +web_name = os.path.basename(fname) +# %% + + + + + +import embed_scores as ES + +result = ES.get_matching_file_for_tstamp(target_tstamp + 500, folder_scores) +print(result) +# %% + +import requests +folder_scores = requests.get('http://192.168.1.242:5004/videos.json').json() +print(len( + +# %% +folder_scores = requests.get('http://192.168.1.242:5004/videos.json', params={'threshold':0.09}).json() +print(len(folder_scores['videos'])) +# %% + +new_folder_scores = folder_scores.copy() +import lttb +min_rows = 15 +factor = 0.1 +for x in new_folder_scores['videos']: + data = np.asarray( [x['embed_scores']['time'], x['embed_scores']['score']]) + amt = max(min_rows, int(factor*data.shape[1])) + + if data.shape[1] > amt: + sampled = lttb.downsample(data.T, amt) + else: + sampled = data.T + + time = sampled[:,0].tolist() + scores = sampled[:,1].tolist() + + +# %% + +import pickle +cache_file_loc = '/srv/ftp_tcc/leopards1/2025/09/09/embedding_scores@0.1@de376b3b6e90315477571ef6e82e841c.pkl' +c_dir = os.path.dirname(cache_file_loc) + + + +# %% +with open(cache_file_loc,'rb') as f: + video_json_info = pickle.load(f) + + +files_in_cache = {os.path.splitext(os.path.basename(x['file_name']))[0] for x in video_json_info} +lsd_dir = os.listdir(c_dir) +files_on_disk = {x.split('.')[0] for x in lsd_dir if x.endswith('oclip_embeds.npz')} +print(len(files_on_disk), len(files_in_cache)) + + + +p# %% +import embed_scores as ES +a_mov = '/srv/ftp_tcc/leopards1/2025/09/09/Leopards1_00_20250909045221.mp4' diff --git a/SearchUtil b/SearchUtil deleted file mode 160000 index ac52bd4..0000000 --- a/SearchUtil +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ac52bd43fb861acfc7c58f6967aaf291ce9029c3 diff --git a/VectorService/util/.gitignore b/VectorService/util/.gitignore new file mode 100644 index 0000000..c94a45f --- /dev/null +++ b/VectorService/util/.gitignore @@ -0,0 +1,162 @@ + +### Python +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/VectorService/util/CommonCode.code-workspace b/VectorService/util/CommonCode.code-workspace new file mode 100644 index 0000000..7426e00 --- /dev/null +++ b/VectorService/util/CommonCode.code-workspace @@ -0,0 +1,11 @@ +{ + "folders": [ + { + "path": "../../../../../Seafile/Designs/Code/Python/CommonCode" + }, + { + "path": "../.." + } + ], + "settings": {} +} \ No newline at end of file diff --git a/VectorService/util/embed_scores.py b/VectorService/util/embed_scores.py new file mode 100644 index 0000000..bccc141 --- /dev/null +++ b/VectorService/util/embed_scores.py @@ -0,0 +1,339 @@ +from CommonCode.video_meta import FTPVideo +from CommonCode.settings import get_logger +import logging +import json +import datetime as dt +import functools +import requests +import numpy as np +from pqdm.processes import pqdm +from multiprocessing import Pool +import os +import lttb +import pickle +import redis +from hashlib import md5 + + +r = redis.Redis(host='localhost', port=6379, db=15) + +logger = get_logger(__name__,'/var/log/vector_search_logs/util_embed_scores', stdout=True, systemd=False, level = logging.INFO) + + +def get_matching_file_for_tstamp(target_tstamp, folder_scores): + matching_file = None + for video_file in folder_scores['videos']: + start_time = video_file['start_time'] + end_time = video_file['end_time'] + + if target_tstamp > start_time and target_tstamp < end_time: + matching_file = video_file + + if matching_file is not None: + fname = matching_file['file_name'] + offset = target_tstamp - matching_file['start_time'] + else: + fname = 'None Found' + offset = -1 + + web_name = 'media/'+os.path.basename(fname) + return dict(full_path = fname, path=web_name, timeoffset = offset) + + +def get_vec_rep_file_loc(c_dir): + vec_rep_file = os.path.join(c_dir, 'vec_rep.npz') + return vec_rep_file + +def get_vector_representation(c_dir, force_compute = False, redis_key = 'compute_log'): + message = {'task':'VECTOR_CALC_IN_FOLDER_START', 'when': str(c_dir), 'time': dt.datetime.now().timestamp()} + r.rpush(redis_key, json.dumps(message)) + + vec_rep_file = get_vec_rep_file_loc(c_dir) + if os.path.exists(vec_rep_file) and not force_compute: + try: + result = dict(np.load(vec_rep_file)) + message = {'task':'VECTOR_CALC_IN_FOLDER_DONE', 'when': str(c_dir), 'time': dt.datetime.now().timestamp(), 'precomputed':True} + r.rpush(redis_key, json.dumps(message)) + return result + except: + os.remove(vec_rep_file) + + + ff = list() + for root, dirs, files in os.walk(c_dir): + for f in files: + if f.endswith('.mp4') and '_reduced' not in f: + ff.append(os.path.join(root, f)) + + videos = list() + for x in ff: + cvid = FTPVideo(x) + videos.append(FTPVideo(x)) + + sorted_videos = sorted(videos) + + all_cat = list() + all_idx = list() + all_source = list() + all_tstamps = list() + enu = 0 + for idx, x in enumerate(sorted_videos): + + try: + hh = x.embeddings + except Exception as e: + hh = None + + if hh is not None: + n_emb = FTPVideo.vec_norm(hh['embeds']) + all_cat.append(n_emb) + all_idx.append( enu * np.ones(n_emb.shape[0], dtype=np.int64) ) + all_source.append(x.real_path) + all_tstamps.append( [x.timestamp() for x in hh['frame_time']]) + enu +=1 + + message = {'task':'VECTOR_CALC_IN_FOLDER_BUMP', 'progress': idx+1, 'how_many': len(sorted_videos), 'time': dt.datetime.now().timestamp()} + r.rpush(redis_key, json.dumps(message)) + + if len(all_cat) == 0: + return [] + all_embeds = np.vstack(all_cat) + all_embeds = FTPVideo.vec_norm(all_embeds) + all_idces = np.hstack(all_idx) + all_times = np.hstack(all_tstamps) + + np.savez(vec_rep_file, embeds = all_embeds, idces= all_idces, timestamps = all_times, source_files = all_source) + message = {'task':'VECTOR_CALC_IN_FOLDER_DONE', 'when': str(c_dir), 'time': dt.datetime.now().timestamp()} + return dict( embeds = all_embeds, idces= all_idces, timestamps = all_times, source_files = all_source) + + + + + +def get_scores_embedding_c_dir(c_dir, query_vector, redis_key = 'compute_log'): + vec_rep = get_vector_representation(c_dir, redis_key=redis_key) + query_scores = (query_vector @ vec_rep['embeds'].T).squeeze() + return query_scores + +@functools.lru_cache +def get_query_vector(query): + vec_form = requests.get('http://192.168.1.242:53004/encode',params={'query':query}).json()['vector'][0] + vec_search = np.asarray(vec_form) + query_vector = FTPVideo.vec_norm(vec_search[None,:]) + return query_vector + + + +def calculate_embedding_score_in_folders(c_dirs, threshold, query = None, query_vector = None, redis_key = 'compute_log'): + result_list = list() + query_vector = None + if query_vector is None: + query_vector = get_query_vector(query) + + +# kwargs = [{'c_dir':x, 'threshold':threshold, 'query': query} for x in c_dirs] + args = [(x, threshold, query, None, logger, redis_key) for x in c_dirs] + + logger.info(f"CALCULATING FOR {args}") + with Pool(processes=8) as pool: + out = pool.starmap(calculate_embedding_score_in_folder, args) + logger.info(f"DONE CALCULATING FOR {args}") + + for x in out: + try: + result_list.extend(x['videos']) + except Exception as e: + print(e, x) + + + + return {'videos':result_list} + + +def collapse_scores_to_maxmin_avg(folder_scores): + + result = list() + for c_data in folder_scores['videos']: + new_d = c_data.copy() + + scores = new_d['embed_scores']['score'] + max_score = max(scores) + min_score = min(scores) + max_score_idx = scores.index(max_score) + min_score_idx = scores.index(min_score) + max_score_time = new_d['embed_scores']['time'][max_score_idx] + min_score_time = new_d['embed_scores']['time'][min_score_idx] + new_d['embed_scores']['score'] = [min_score, max_score, max_score_time, min_score_time] + new_d['embed_scores']['time'] = max(new_d['embed_scores']['time']) + result.append(new_d) + + return result + # c_data = {'file_name': str(s_file), 'start_time':start_time, 'end_time':end_time, 'embed_scores':{'time':frame_time, 'score':embed_scores}} + # video_json_info.append(c_data) + + # to_write = {'source_files': vec_rep['source_files'], 'videos': video_json_info} + # with open(cache_file_loc, 'wb') as f: + # logger.info(f"WRITING EMBEDDING SCORE TO CACHE {cache_file_loc}") + # pickle.dump(to_write, f) + + +def calculate_embedding_score_in_folder(og_dir, threshold, query = None, query_vector = None, logger = logger, redis_key = 'compute_log'): + message = {'task':'SCORE_CALC_IN_FOLDER_START', 'when': str(og_dir), 'time': dt.datetime.now().timestamp()} + r.rpush(redis_key, json.dumps(message)) + + if query_vector is None: + query_vector = get_query_vector(query) + + candidate_dirs = list() + candidate_dirs.append(og_dir) + candidate_dirs.append(og_dir.replace('/srv/ftp_tcc','/mnt/hdd_24tb_1/videos/ftp')) + candidate_dirs.append(og_dir.replace('/srv/ftp','/mnt/hdd_24tb_1/videos/ftp')) + + c_dir = None + for candidate in candidate_dirs: + if os.path.exists(candidate): + c_dir = candidate + break + if c_dir is None: + return [] + + vec_cache_str = md5(query_vector).hexdigest() + cache_file_loc = os.path.join(c_dir, 'embedding_scores@'+str(threshold)+'@'+vec_cache_str+'.pkl') + + if os.path.exists(cache_file_loc): + logger.info(f"TRYING TO LOAD CACHE {cache_file_loc}") + try: + + with open(cache_file_loc, 'rb') as f: + video_json_info = pickle.load(f) + files_in_cache = {os.path.splitext(os.path.basename(x))[0] for x in video_json_info.get('source_files',[])} + lsd_dir = os.listdir(c_dir) + files_on_disk = {x.split(".")[0] for x in lsd_dir if x.endswith('oclip_embeds.npz')} + + + if files_on_disk == files_in_cache: + logger.info(f"LOADED EMBEDDING SCORE FROM CACHE {cache_file_loc}") + message = {'task':'SCORE_CALC_IN_FOLDER_DONE', 'when': str(c_dir), 'time': dt.datetime.now().timestamp(), 'precomputed': True} + r.rpush(redis_key, json.dumps(message)) + return video_json_info + else: + logger.info(f"CACHE FILE IS OLD, DELETING VEC REP FILE AND RECREATING {cache_file_loc}") + os.remove( get_vec_rep_file_loc(c_dir)) + except Exception as e: + logger.info(f"CACHE FILE IS CORRUPT, RECREATING {cache_file_loc} {e}") + os.remove(cache_file_loc) + + pass + + + vec_rep = get_vector_representation(c_dir, redis_key = redis_key) + query_scores = get_scores_embedding_c_dir(c_dir, tuple(query_vector.tolist()[0]), redis_key = redis_key) + + video_json_info = list() + idces_keep = np.where(query_scores > threshold)[0] + + video_id = vec_rep['idces'][idces_keep] + videos_that_match = np.unique(video_id) + + # subset_timestampsF = list() + # for s in videos_that_match: + # idces_entry = np.where(vec_rep['idces'] == s)[0] + # min_idces = idces_entry[0] + # max_idces = idces_entry[-1] + # subset_timestampsF.append( [ vec_rep['timestamps'][min_idces], vec_rep['timestamps'][max_idces]]) + + id_extract_video_level = np.where(np.isin(vec_rep['idces'], videos_that_match))[0] + idces_split = np.where(np.diff(vec_rep['idces'][id_extract_video_level]) !=0)[0] + 1 + subset_timestampsF = np.split(vec_rep['timestamps'][id_extract_video_level], idces_split) + + + + + for subset_t in subset_timestampsF: + if len(subset_t) == 0: + continue + + min_t = min(subset_t) + max_t = max(subset_t) + idces_curr = np.where(np.logical_and(vec_rep['timestamps'] > min_t , vec_rep['timestamps'] < max_t))[0] + if len(idces_curr) == 0: + continue + + unq_vids = np.unique(vec_rep['idces'][idces_curr]) + subset_idx = np.where(np.isin(vec_rep['idces'],unq_vids))[0] + + subset_idces = vec_rep['idces'][subset_idx] + subset_timestamps = vec_rep['timestamps'][subset_idx] + subset_scores = query_scores[subset_idx] + idx_split = np.where(np.diff(vec_rep['idces'][subset_idx]) !=0)[0]+1 + + split_idces = np.split(subset_idces, idx_split) + split_timestamps = np.split(subset_timestamps, idx_split) + split_scores = np.split(subset_scores, idx_split) + split_files = [vec_rep['source_files'][x[0]] for x in split_idces] + + for s_file, s_scores, s_tstamps, s_idces in zip(split_files, split_scores, split_timestamps, split_idces): + start_time = float(min(s_tstamps)) + end_time = float(max(s_tstamps)) + + frame_time = (s_tstamps - start_time).tolist() + embed_scores = s_scores.tolist() + + c_data = {'file_name': str(s_file), 'start_time':start_time, 'end_time':end_time, 'embed_scores':{'time':frame_time, 'score':embed_scores}} + video_json_info.append(c_data) + + message = {'task':'SCORE_CALC_IN_FOLDER_DONE', 'when': str(c_dir), 'time': dt.datetime.now().timestamp()} + r.rpush(redis_key, json.dumps(message)) + to_write = {'source_files': vec_rep['source_files'], 'videos': video_json_info} + with open(cache_file_loc, 'wb') as f: + logger.info(f"WRITING EMBEDDING SCORE TO CACHE {cache_file_loc}") + pickle.dump(to_write, f) + + return to_write + + +def get_matching_file_given_filename(web_name, folder_scores): + file_name = None + for x in folder_scores['videos']: + if x['file_name'].endswith(web_name): + file_name = x['file_name'] + + + candidate_files = list() + candidate_files.append(file_name) + candidate_files.append(file_name.replace('/srv/ftp_tcc','/mnt/hdd_24tb_1/videos/ftp')) + candidate_files.append(file_name.replace('/srv/ftp','/mnt/hdd_24tb_1/videos/ftp')) + + file_name = None + for candidate in candidate_files: + if os.path.exists(candidate): + file_name = candidate + break + + + + + return file_name + + + +#c_dirs = ['/mnt/hdd_24tb_1/videos/ftp/leopards2/2025/08/26','/srv/ftp_tcc/leopards1/2025/08/27','/srv/ftp_tcc/leopards1/2025/08/28','/srv/ftp_tcc/leopards1/2025/08/29'] +#op = calculate_embedding_score_in_folders( tuple(c_dirs), 0.10, query = 'A cat and human') + +def add_breaks_between_videos(op, threshold_to_split_seconds = 30*60): # 30 minutes): + ranges = list() + for vids in op['videos']: + ranges.append( (vids['start_time'], vids['end_time']) ) + + breaks = list() + for idx in range(len(ranges)-1): + current_range = ranges[idx] + next_range = ranges[idx+1] + + end_now = current_range[1] + start_next = next_range[0] + + if (start_next - end_now) > threshold_to_split_seconds: + breaks.append((end_now, start_next)) + + return breaks diff --git a/VectorService/vector_service.py b/VectorService/vector_service.py new file mode 100644 index 0000000..4475fa6 --- /dev/null +++ b/VectorService/vector_service.py @@ -0,0 +1,96 @@ +from typing import Union, Optional, List +from pydantic import BaseModel +from fastapi import FastAPI, Request, Depends +from CommonCode.settings import get_logger +import logging +from fastapi.responses import StreamingResponse +import os +import sys +import json +import time +from util import embed_scores as ES +from fastapi_server_session import SessionManager, RedisSessionInterface, Session +import redis +from datetime import timedelta + +app = FastAPI() + +session_manager = SessionManager( + interface=RedisSessionInterface(redis.from_url("redis://localhost")) +) + +logger = get_logger(__name__,'/var/log/vector_search_logs/main_embed_scores', stdout=True, systemd=False, level = logging.INFO) +r = redis.Redis(host='localhost', port=6379, db=15) + + +class VideosPostRequest(BaseModel): + query: str = "A cat and a human", + threshold: float = 0.10, + c_dirs: Optional[List[str]] = None, + task_id: str = 'compute_log' + +@app.post("/videos.json") +async def videos_json( + vpr: VideosPostRequest, + session: Session = Depends(session_manager.use_session), +): + + query = vpr.query + threshold = vpr.threshold + c_dirs = vpr.c_dirs + task_id = vpr.task_id + if c_dirs is None: + c_dirs = [ + # "/mnt/hdd_24tb_1/videos/ftp/leopards2/2025/08/26", + # "/srv/ftp_tcc/leopards1/2025/08/27", + # "/srv/ftp_tcc/leopards1/2025/08/28", + # "/srv/ftp_tcc/leopards1/2025/08/29", + # "/srv/ftp_tcc/leopards1/2025/08/30", + # "/srv/ftp_tcc/leopards1/2025/08/31", + # "/srv/ftp_tcc/leopards1/2025/09/01", + # "/srv/ftp_tcc/leopards1/2025/09/02", + # "/srv/ftp_tcc/leopards1/2025/09/03", + # "/srv/ftp_tcc/leopards1/2025/09/04", + # "/srv/ftp_tcc/leopards1/2025/09/05", + # "/srv/ftp_tcc/leopards1/2025/09/06", + # "/srv/ftp_tcc/leopards1/2025/09/07", + "/srv/ftp_tcc/leopards1/2025/09/08", + "/srv/ftp_tcc/leopards1/2025/09/09", + "/srv/ftp_tcc/leopards1/2025/09/10", + "/srv/ftp_tcc/leopards1/2025/09/11", + ] + + + print(','.join([str(x) for x in c_dirs])) + message = {'task':'SCHEDULED','when':[str(x) for x in c_dirs], 'time':time.time()} + r.rpush(task_id, json.dumps(message)) + + + for x in c_dirs: + message = {'task':'QUEUEING', 'when': str(x), 'time': time.time()} + r.rpush(task_id, json.dumps(message)) + + folder_scores = ES.calculate_embedding_score_in_folders( + tuple(c_dirs), threshold=threshold, query=query, redis_key = task_id) + # if p_hits != ES.calculate_embedding_score_in_folders.cache_info().hits: + # logger.info("FROM CACHE") + # else:pp + # logger.info("COMPUTED FROM SCRATCH") + + + + folder_scores["breaks"] = ES.add_breaks_between_videos(folder_scores) + folder_scores['videos'] = ES.collapse_scores_to_maxmin_avg(folder_scores) + + + session["folder_scores"] = folder_scores + return folder_scores + + +class ClickEvent(BaseModel): + timestamp: float + + +class ClickResponse(BaseModel): + path: str + timeoffset: float