diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 00000000..3d517e63
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,13 @@
+repos:
+ - repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: v0.15.2
+ hooks:
+ - id: ruff-check
+ args: [--fix]
+ - id: ruff-format
+
+ - repo: https://github.com/hoxbro/ty-pre-commit
+ rev: v0.0.17
+ hooks:
+ - id: ty-check
+ args: [--ignore, unresolved-import]
diff --git a/viewer_new/.gitignore b/viewer_new/.gitignore
new file mode 100644
index 00000000..a547bf36
--- /dev/null
+++ b/viewer_new/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/viewer_new/README.md b/viewer_new/README.md
new file mode 100644
index 00000000..975cbbb6
--- /dev/null
+++ b/viewer_new/README.md
@@ -0,0 +1,19 @@
+# `viewer_new`
+
+Standalone Bun + Vite + React viewer for DeepRetro.
+
+## Commands
+
+```bash
+bun install
+bun run dev
+bun run build
+bun run typecheck
+bun run test
+```
+
+## Notes
+
+- The legacy `viewer/` app is intentionally untouched.
+- `bun run sync:config` copies the root `config/advanced_settings.json` into `public/advanced-settings.json`.
+- Runtime backend configuration lives in `public/runtime-config.json`.
diff --git a/viewer_new/bun.lock b/viewer_new/bun.lock
new file mode 100644
index 00000000..64f2d510
--- /dev/null
+++ b/viewer_new/bun.lock
@@ -0,0 +1,766 @@
+{
+ "lockfileVersion": 1,
+ "configVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "viewer_new",
+ "dependencies": {
+ "@codemirror/lang-json": "^6.0.2",
+ "@rdkit/rdkit": "^2025.3.4-1.0.0",
+ "@tanstack/react-query": "^5.90.21",
+ "@uiw/react-codemirror": "^4.25.7",
+ "@xyflow/react": "^12.10.1",
+ "elkjs": "^0.11.1",
+ "lucide-react": "^0.577.0",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0",
+ "zod": "^4.3.6",
+ "zustand": "^5.0.11",
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.1",
+ "@testing-library/jest-dom": "^6.9.1",
+ "@testing-library/react": "^16.3.2",
+ "@testing-library/user-event": "^14.6.1",
+ "@types/node": "^25.3.5",
+ "@types/react": "^19.2.7",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^5.1.1",
+ "eslint": "^9.39.1",
+ "eslint-plugin-react-hooks": "^7.0.1",
+ "eslint-plugin-react-refresh": "^0.4.24",
+ "globals": "^16.5.0",
+ "jsdom": "^28.1.0",
+ "typescript": "~5.9.3",
+ "typescript-eslint": "^8.48.0",
+ "vite": "^7.3.1",
+ "vitest": "^4.0.18",
+ },
+ },
+ },
+ "packages": {
+ "@acemir/cssom": ["@acemir/cssom@0.9.31", "", {}, "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA=="],
+
+ "@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="],
+
+ "@asamuzakjp/css-color": ["@asamuzakjp/css-color@5.0.1", "", { "dependencies": { "@csstools/css-calc": "^3.1.1", "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "lru-cache": "^11.2.6" } }, "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw=="],
+
+ "@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@6.8.1", "", { "dependencies": { "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.1.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.2.6" } }, "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ=="],
+
+ "@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="],
+
+ "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
+
+ "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],
+
+ "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="],
+
+ "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
+
+ "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
+
+ "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
+
+ "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
+
+ "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
+
+ "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="],
+
+ "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
+
+ "@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="],
+
+ "@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
+
+ "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],
+
+ "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
+
+ "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="],
+
+ "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
+
+ "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
+
+ "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
+ "@bramus/specificity": ["@bramus/specificity@2.4.2", "", { "dependencies": { "css-tree": "^3.0.0" }, "bin": { "specificity": "bin/cli.js" } }, "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw=="],
+
+ "@codemirror/autocomplete": ["@codemirror/autocomplete@6.20.1", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-1cvg3Vz1dSSToCNlJfRA2WSI4ht3K+WplO0UMOgmUYPivCyy2oueZY6Lx7M9wThm7SDUBViRmuT+OG/i8+ON9A=="],
+
+ "@codemirror/commands": ["@codemirror/commands@6.10.2", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.4.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" } }, "sha512-vvX1fsih9HledO1c9zdotZYUZnE4xV0m6i3m25s5DIfXofuprk6cRcLUZvSk3CASUbwjQX21tOGbkY2BH8TpnQ=="],
+
+ "@codemirror/lang-json": ["@codemirror/lang-json@6.0.2", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@lezer/json": "^1.0.0" } }, "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ=="],
+
+ "@codemirror/language": ["@codemirror/language@6.12.2", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", "@lezer/common": "^1.5.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0", "style-mod": "^4.0.0" } }, "sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg=="],
+
+ "@codemirror/lint": ["@codemirror/lint@6.9.5", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.35.0", "crelt": "^1.0.5" } }, "sha512-GElsbU9G7QT9xXhpUg1zWGmftA/7jamh+7+ydKRuT0ORpWS3wOSP0yT1FOlIZa7mIJjpVPipErsyvVqB9cfTFA=="],
+
+ "@codemirror/search": ["@codemirror/search@6.6.0", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.37.0", "crelt": "^1.0.5" } }, "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw=="],
+
+ "@codemirror/state": ["@codemirror/state@6.5.4", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw=="],
+
+ "@codemirror/theme-one-dark": ["@codemirror/theme-one-dark@6.1.3", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/highlight": "^1.0.0" } }, "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA=="],
+
+ "@codemirror/view": ["@codemirror/view@6.39.16", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-m6S22fFpKtOWhq8HuhzsI1WzUP/hB9THbDj0Tl5KX4gbO6Y91hwBl7Yky33NdvB6IffuRFiBxf1R8kJMyXmA4Q=="],
+
+ "@csstools/color-helpers": ["@csstools/color-helpers@6.0.2", "", {}, "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q=="],
+
+ "@csstools/css-calc": ["@csstools/css-calc@3.1.1", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ=="],
+
+ "@csstools/css-color-parser": ["@csstools/css-color-parser@4.0.2", "", { "dependencies": { "@csstools/color-helpers": "^6.0.2", "@csstools/css-calc": "^3.1.1" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw=="],
+
+ "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@4.0.0", "", { "peerDependencies": { "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w=="],
+
+ "@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.1.0", "", {}, "sha512-H4tuz2nhWgNKLt1inYpoVCfbJbMwX/lQKp3g69rrrIMIYlFD9+zTykOKhNR8uGrAmbS/kT9n6hTFkmDkxLgeTA=="],
+
+ "@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="],
+
+ "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],
+
+ "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="],
+
+ "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="],
+
+ "@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="],
+
+ "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="],
+
+ "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="],
+
+ "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="],
+
+ "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="],
+
+ "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="],
+
+ "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="],
+
+ "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="],
+
+ "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="],
+
+ "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="],
+
+ "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="],
+
+ "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="],
+
+ "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="],
+
+ "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="],
+
+ "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="],
+
+ "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="],
+
+ "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="],
+
+ "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="],
+
+ "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="],
+
+ "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="],
+
+ "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="],
+
+ "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="],
+
+ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="],
+
+ "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
+
+ "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
+
+ "@eslint/config-array": ["@eslint/config-array@0.21.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.5" } }, "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw=="],
+
+ "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="],
+
+ "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
+
+ "@eslint/eslintrc": ["@eslint/eslintrc@3.3.5", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg=="],
+
+ "@eslint/js": ["@eslint/js@9.39.4", "", {}, "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw=="],
+
+ "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="],
+
+ "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
+
+ "@exodus/bytes": ["@exodus/bytes@1.15.0", "", { "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" }, "optionalPeers": ["@noble/hashes"] }, "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ=="],
+
+ "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
+
+ "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="],
+
+ "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
+
+ "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
+
+ "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
+
+ "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
+
+ "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
+
+ "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
+
+ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
+
+ "@lezer/common": ["@lezer/common@1.5.1", "", {}, "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw=="],
+
+ "@lezer/highlight": ["@lezer/highlight@1.2.3", "", { "dependencies": { "@lezer/common": "^1.3.0" } }, "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g=="],
+
+ "@lezer/json": ["@lezer/json@1.0.3", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ=="],
+
+ "@lezer/lr": ["@lezer/lr@1.4.8", "", { "dependencies": { "@lezer/common": "^1.0.0" } }, "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA=="],
+
+ "@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.2", "", {}, "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="],
+
+ "@rdkit/rdkit": ["@rdkit/rdkit@2025.3.4-1.0.0", "", {}, "sha512-MJzNoeW2SWt2KCdIibfY213uLWyHZjFfS2ntJ/HuYNHdu6dEiim7jb6ZMU9wnt9Oovkc85BHMuupxkufIMvftQ=="],
+
+ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="],
+
+ "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="],
+
+ "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="],
+
+ "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="],
+
+ "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="],
+
+ "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="],
+
+ "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="],
+
+ "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="],
+
+ "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="],
+
+ "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="],
+
+ "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="],
+
+ "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="],
+
+ "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="],
+
+ "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="],
+
+ "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="],
+
+ "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="],
+
+ "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="],
+
+ "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="],
+
+ "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="],
+
+ "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="],
+
+ "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="],
+
+ "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="],
+
+ "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="],
+
+ "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="],
+
+ "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="],
+
+ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="],
+
+ "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
+
+ "@tanstack/query-core": ["@tanstack/query-core@5.90.20", "", {}, "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg=="],
+
+ "@tanstack/react-query": ["@tanstack/react-query@5.90.21", "", { "dependencies": { "@tanstack/query-core": "5.90.20" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg=="],
+
+ "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="],
+
+ "@testing-library/jest-dom": ["@testing-library/jest-dom@6.9.1", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "picocolors": "^1.1.1", "redent": "^3.0.0" } }, "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA=="],
+
+ "@testing-library/react": ["@testing-library/react@16.3.2", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g=="],
+
+ "@testing-library/user-event": ["@testing-library/user-event@14.6.1", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw=="],
+
+ "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
+
+ "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
+
+ "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
+
+ "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
+
+ "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
+
+ "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
+
+ "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="],
+
+ "@types/d3-drag": ["@types/d3-drag@3.0.7", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ=="],
+
+ "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="],
+
+ "@types/d3-selection": ["@types/d3-selection@3.0.11", "", {}, "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w=="],
+
+ "@types/d3-transition": ["@types/d3-transition@3.0.9", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg=="],
+
+ "@types/d3-zoom": ["@types/d3-zoom@3.0.8", "", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw=="],
+
+ "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
+
+ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
+
+ "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
+
+ "@types/node": ["@types/node@25.3.5", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA=="],
+
+ "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
+
+ "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
+
+ "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.56.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/type-utils": "8.56.1", "@typescript-eslint/utils": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.56.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A=="],
+
+ "@typescript-eslint/parser": ["@typescript-eslint/parser@8.56.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg=="],
+
+ "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.56.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.56.1", "@typescript-eslint/types": "^8.56.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ=="],
+
+ "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1" } }, "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w=="],
+
+ "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.56.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ=="],
+
+ "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/utils": "8.56.1", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg=="],
+
+ "@typescript-eslint/types": ["@typescript-eslint/types@8.56.1", "", {}, "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw=="],
+
+ "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.56.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.56.1", "@typescript-eslint/tsconfig-utils": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg=="],
+
+ "@typescript-eslint/utils": ["@typescript-eslint/utils@8.56.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA=="],
+
+ "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw=="],
+
+ "@uiw/codemirror-extensions-basic-setup": ["@uiw/codemirror-extensions-basic-setup@4.25.7", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/commands": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/lint": "^6.0.0", "@codemirror/search": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0" } }, "sha512-tPV/AGjF4yM22D5mnyH7EuYBkWO05wF5Y4x3lmQJo6LuHmhjh0RQsVDjqeIgNOkXT3UO9OdkL4dzxw465/JZVg=="],
+
+ "@uiw/react-codemirror": ["@uiw/react-codemirror@4.25.7", "", { "dependencies": { "@babel/runtime": "^7.18.6", "@codemirror/commands": "^6.1.0", "@codemirror/state": "^6.1.1", "@codemirror/theme-one-dark": "^6.0.0", "@uiw/codemirror-extensions-basic-setup": "4.25.7", "codemirror": "^6.0.0" }, "peerDependencies": { "@codemirror/view": ">=6.0.0", "react": ">=17.0.0", "react-dom": ">=17.0.0" } }, "sha512-s/EbEe0dFANWEgfLbfdIrrOGv0R7M1XhkKG3ShroBeH6uP9pVNQy81YHOLRCSVcytTp9zAWRNfXR/+XxZTvV7w=="],
+
+ "@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.4", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA=="],
+
+ "@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="],
+
+ "@vitest/mocker": ["@vitest/mocker@4.0.18", "", { "dependencies": { "@vitest/spy": "4.0.18", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ=="],
+
+ "@vitest/pretty-format": ["@vitest/pretty-format@4.0.18", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw=="],
+
+ "@vitest/runner": ["@vitest/runner@4.0.18", "", { "dependencies": { "@vitest/utils": "4.0.18", "pathe": "^2.0.3" } }, "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw=="],
+
+ "@vitest/snapshot": ["@vitest/snapshot@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA=="],
+
+ "@vitest/spy": ["@vitest/spy@4.0.18", "", {}, "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw=="],
+
+ "@vitest/utils": ["@vitest/utils@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "tinyrainbow": "^3.0.3" } }, "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA=="],
+
+ "@xyflow/react": ["@xyflow/react@12.10.1", "", { "dependencies": { "@xyflow/system": "0.0.75", "classcat": "^5.0.3", "zustand": "^4.4.0" }, "peerDependencies": { "react": ">=17", "react-dom": ">=17" } }, "sha512-5eSWtIK/+rkldOuFbOOz44CRgQRjtS9v5nufk77DV+XBnfCGL9HAQ8PG00o2ZYKqkEU/Ak6wrKC95Tu+2zuK3Q=="],
+
+ "@xyflow/system": ["@xyflow/system@0.0.75", "", { "dependencies": { "@types/d3-drag": "^3.0.7", "@types/d3-interpolate": "^3.0.4", "@types/d3-selection": "^3.0.10", "@types/d3-transition": "^3.0.8", "@types/d3-zoom": "^3.0.8", "d3-drag": "^3.0.0", "d3-interpolate": "^3.0.1", "d3-selection": "^3.0.0", "d3-zoom": "^3.0.0" } }, "sha512-iXs+AGFLi8w/VlAoc/iSxk+CxfT6o64Uw/k0CKASOPqjqz6E0rb5jFZgJtXGZCpfQI6OQpu5EnumP5fGxQheaQ=="],
+
+ "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
+
+ "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
+
+ "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
+
+ "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
+
+ "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+
+ "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
+
+ "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
+
+ "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
+
+ "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
+
+ "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
+
+ "baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="],
+
+ "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="],
+
+ "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
+
+ "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
+
+ "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
+
+ "caniuse-lite": ["caniuse-lite@1.0.30001777", "", {}, "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ=="],
+
+ "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
+
+ "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
+
+ "classcat": ["classcat@5.0.5", "", {}, "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w=="],
+
+ "codemirror": ["codemirror@6.0.2", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/commands": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/lint": "^6.0.0", "@codemirror/search": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0" } }, "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw=="],
+
+ "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
+
+ "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
+
+ "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
+
+ "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
+
+ "crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="],
+
+ "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
+
+ "css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="],
+
+ "css.escape": ["css.escape@1.5.1", "", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="],
+
+ "cssstyle": ["cssstyle@6.2.0", "", { "dependencies": { "@asamuzakjp/css-color": "^5.0.1", "@csstools/css-syntax-patches-for-csstree": "^1.0.28", "css-tree": "^3.1.0", "lru-cache": "^11.2.6" } }, "sha512-Fm5NvhYathRnXNVndkUsCCuR63DCLVVwGOOwQw782coXFi5HhkXdu289l59HlXZBawsyNccXfWRYvLzcDCdDig=="],
+
+ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
+
+ "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="],
+
+ "d3-dispatch": ["d3-dispatch@3.0.1", "", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="],
+
+ "d3-drag": ["d3-drag@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-selection": "3" } }, "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg=="],
+
+ "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="],
+
+ "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="],
+
+ "d3-selection": ["d3-selection@3.0.0", "", {}, "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="],
+
+ "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="],
+
+ "d3-transition": ["d3-transition@3.0.1", "", { "dependencies": { "d3-color": "1 - 3", "d3-dispatch": "1 - 3", "d3-ease": "1 - 3", "d3-interpolate": "1 - 3", "d3-timer": "1 - 3" }, "peerDependencies": { "d3-selection": "2 - 3" } }, "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w=="],
+
+ "d3-zoom": ["d3-zoom@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="],
+
+ "data-urls": ["data-urls@7.0.0", "", { "dependencies": { "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.0" } }, "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA=="],
+
+ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
+
+ "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="],
+
+ "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
+
+ "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
+
+ "dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="],
+
+ "electron-to-chromium": ["electron-to-chromium@1.5.307", "", {}, "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg=="],
+
+ "elkjs": ["elkjs@0.11.1", "", {}, "sha512-zxxR9k+rx5ktMwT/FwyLdPCrq7xN6e4VGGHH8hA01vVYKjTFik7nHOxBnAYtrgYUB1RpAiLvA1/U2YraWxyKKg=="],
+
+ "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
+
+ "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
+
+ "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],
+
+ "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
+
+ "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
+
+ "eslint": ["eslint@9.39.4", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="],
+
+ "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.0.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA=="],
+
+ "eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.26", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ=="],
+
+ "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
+
+ "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
+
+ "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
+
+ "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
+
+ "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
+
+ "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
+
+ "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
+
+ "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
+
+ "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
+
+ "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
+
+ "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
+
+ "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
+
+ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
+
+ "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
+
+ "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
+
+ "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
+
+ "flatted": ["flatted@3.3.4", "", {}, "sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA=="],
+
+ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
+
+ "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
+
+ "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
+
+ "globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="],
+
+ "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
+
+ "hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="],
+
+ "hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="],
+
+ "html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="],
+
+ "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
+
+ "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
+
+ "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
+
+ "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
+
+ "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
+
+ "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="],
+
+ "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
+
+ "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
+
+ "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="],
+
+ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
+
+ "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
+
+ "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
+
+ "jsdom": ["jsdom@28.1.0", "", { "dependencies": { "@acemir/cssom": "^0.9.31", "@asamuzakjp/dom-selector": "^6.8.1", "@bramus/specificity": "^2.4.2", "@exodus/bytes": "^1.11.0", "cssstyle": "^6.0.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", "parse5": "^8.0.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.0", "undici": "^7.21.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.0", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug=="],
+
+ "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
+
+ "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
+
+ "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
+
+ "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
+
+ "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
+
+ "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
+
+ "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
+
+ "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
+
+ "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
+
+ "lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="],
+
+ "lucide-react": ["lucide-react@0.577.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A=="],
+
+ "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
+
+ "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
+
+ "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="],
+
+ "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="],
+
+ "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
+
+ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+
+ "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
+
+ "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
+
+ "node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="],
+
+ "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
+
+ "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
+
+ "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
+
+ "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
+
+ "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
+
+ "parse5": ["parse5@8.0.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA=="],
+
+ "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
+
+ "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
+
+ "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
+
+ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
+
+ "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
+
+ "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
+
+ "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
+
+ "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
+
+ "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
+
+ "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
+
+ "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
+
+ "react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
+
+ "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="],
+
+ "redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="],
+
+ "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
+
+ "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
+
+ "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="],
+
+ "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="],
+
+ "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
+
+ "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
+
+ "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
+
+ "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
+
+ "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
+
+ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+
+ "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
+
+ "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
+
+ "strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="],
+
+ "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
+
+ "style-mod": ["style-mod@4.1.3", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="],
+
+ "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
+
+ "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="],
+
+ "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
+
+ "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
+
+ "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
+
+ "tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="],
+
+ "tldts": ["tldts@7.0.25", "", { "dependencies": { "tldts-core": "^7.0.25" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-keinCnPbwXEUG3ilrWQZU+CqcTTzHq9m2HhoUP2l7Xmi8l1LuijAXLpAJ5zRW+ifKTNscs4NdCkfkDCBYm352w=="],
+
+ "tldts-core": ["tldts-core@7.0.25", "", {}, "sha512-ZjCZK0rppSBu7rjHYDYsEaMOIbbT+nWF57hKkv4IUmZWBNrBWBOjIElc0mKRgLM8bm7x/BBlof6t2gi/Oq/Asw=="],
+
+ "tough-cookie": ["tough-cookie@6.0.0", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w=="],
+
+ "tr46": ["tr46@6.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="],
+
+ "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="],
+
+ "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
+
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
+
+ "typescript-eslint": ["typescript-eslint@8.56.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.56.1", "@typescript-eslint/parser": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/utils": "8.56.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ=="],
+
+ "undici": ["undici@7.22.0", "", {}, "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg=="],
+
+ "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
+
+ "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
+
+ "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
+
+ "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
+
+ "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
+
+ "vitest": ["vitest@4.0.18", "", { "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", "@vitest/pretty-format": "4.0.18", "@vitest/runner": "4.0.18", "@vitest/snapshot": "4.0.18", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.18", "@vitest/browser-preview": "4.0.18", "@vitest/browser-webdriverio": "4.0.18", "@vitest/ui": "4.0.18", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ=="],
+
+ "w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="],
+
+ "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="],
+
+ "webidl-conversions": ["webidl-conversions@8.0.1", "", {}, "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ=="],
+
+ "whatwg-mimetype": ["whatwg-mimetype@5.0.0", "", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="],
+
+ "whatwg-url": ["whatwg-url@16.0.1", "", { "dependencies": { "@exodus/bytes": "^1.11.0", "tr46": "^6.0.0", "webidl-conversions": "^8.0.1" } }, "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw=="],
+
+ "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
+
+ "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
+
+ "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
+
+ "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="],
+
+ "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="],
+
+ "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
+
+ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
+
+ "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
+
+ "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="],
+
+ "zustand": ["zustand@5.0.11", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg=="],
+
+ "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
+
+ "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
+
+ "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
+
+ "@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="],
+
+ "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="],
+
+ "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
+
+ "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
+
+ "@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
+
+ "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
+
+ "@xyflow/react/zustand": ["zustand@4.5.7", "", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "@types/react": ">=16.8", "immer": ">=9.0.6", "react": ">=16.8" }, "optionalPeers": ["@types/react", "immer", "react"] }, "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw=="],
+
+ "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
+
+ "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="],
+
+ "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
+ }
+}
diff --git a/viewer_new/eslint.config.js b/viewer_new/eslint.config.js
new file mode 100644
index 00000000..5e6b472f
--- /dev/null
+++ b/viewer_new/eslint.config.js
@@ -0,0 +1,23 @@
+import js from '@eslint/js'
+import globals from 'globals'
+import reactHooks from 'eslint-plugin-react-hooks'
+import reactRefresh from 'eslint-plugin-react-refresh'
+import tseslint from 'typescript-eslint'
+import { defineConfig, globalIgnores } from 'eslint/config'
+
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ js.configs.recommended,
+ tseslint.configs.recommended,
+ reactHooks.configs.flat.recommended,
+ reactRefresh.configs.vite,
+ ],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser,
+ },
+ },
+])
diff --git a/viewer_new/index.html b/viewer_new/index.html
new file mode 100644
index 00000000..f86e1550
--- /dev/null
+++ b/viewer_new/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ DeepRetro Viewer
+
+
+
+
+
+
diff --git a/viewer_new/package.json b/viewer_new/package.json
new file mode 100644
index 00000000..7076c88c
--- /dev/null
+++ b/viewer_new/package.json
@@ -0,0 +1,51 @@
+{
+ "name": "viewer_new",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "sync:config": "node ./scripts/sync-public-config.ts",
+ "predev": "node ./scripts/sync-public-config.ts",
+ "prebuild": "node ./scripts/sync-public-config.ts",
+ "pretest": "node ./scripts/sync-public-config.ts",
+ "pretypecheck": "node ./scripts/sync-public-config.ts",
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "typecheck": "tsc -b --pretty false",
+ "test": "vitest run",
+ "lint": "eslint .",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@codemirror/lang-json": "^6.0.2",
+ "@rdkit/rdkit": "^2025.3.4-1.0.0",
+ "@tanstack/react-query": "^5.90.21",
+ "@uiw/react-codemirror": "^4.25.7",
+ "@xyflow/react": "^12.10.1",
+ "elkjs": "^0.11.1",
+ "lucide-react": "^0.577.0",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0",
+ "zod": "^4.3.6",
+ "zustand": "^5.0.11"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.1",
+ "@testing-library/jest-dom": "^6.9.1",
+ "@testing-library/react": "^16.3.2",
+ "@testing-library/user-event": "^14.6.1",
+ "@types/node": "^25.3.5",
+ "@types/react": "^19.2.7",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^5.1.1",
+ "eslint": "^9.39.1",
+ "eslint-plugin-react-hooks": "^7.0.1",
+ "eslint-plugin-react-refresh": "^0.4.24",
+ "globals": "^16.5.0",
+ "jsdom": "^28.1.0",
+ "typescript": "~5.9.3",
+ "typescript-eslint": "^8.48.0",
+ "vite": "^7.3.1",
+ "vitest": "^4.0.18"
+ }
+}
diff --git a/viewer_new/public/advanced-settings.json b/viewer_new/public/advanced-settings.json
new file mode 100644
index 00000000..9bb51651
--- /dev/null
+++ b/viewer_new/public/advanced-settings.json
@@ -0,0 +1,59 @@
+{
+ "llm_models": {
+ "claude3": {
+ "internal_name": "claude-3-opus-20240229",
+ "display_name": "Claude 3 Opus",
+ "supports_advanced_prompt": true,
+ "supports_stability_check": true,
+ "supports_hallucination_check": true,
+ "supports_protecting_group_feature": false
+ },
+ "claude37": {
+ "internal_name": "anthropic/claude-3-7-sonnet-20250219",
+ "display_name": "Claude 3.7 Sonnet",
+ "supports_advanced_prompt": true,
+ "supports_stability_check": true,
+ "supports_hallucination_check": true,
+ "supports_protecting_group_feature": false
+ },
+ "deepseek": {
+ "internal_name": "fireworks_ai/accounts/fireworks/models/deepseek-r1",
+ "display_name": "DeepSeek",
+ "supports_advanced_prompt": true,
+ "supports_stability_check": true,
+ "supports_hallucination_check": true,
+ "supports_protecting_group_feature": false
+ },
+ "claude4opus": {
+ "internal_name": "anthropic/claude-opus-4-20250514",
+ "display_name": "Claude 4 Opus",
+ "supports_advanced_prompt": true,
+ "supports_stability_check": true,
+ "supports_hallucination_check": true,
+ "supports_protecting_group_feature": false
+ },
+ "claude4sonnet": {
+ "internal_name": "anthropic/claude-sonnet-4-20250514",
+ "display_name": "Claude 4 Sonnet",
+ "supports_advanced_prompt": true,
+ "supports_stability_check": true,
+ "supports_hallucination_check": true,
+ "supports_protecting_group_feature": false
+ }
+ },
+ "az_models": {
+ "USPTO": { "display_name": "USPTO" },
+ "Pistachio_25": { "display_name": "Pistachio (25)" },
+ "Pistachio_50": { "display_name": "Pistachio (50)" },
+ "Pistachio_100": { "display_name": "Pistachio (100)" },
+ "Pistachio_100+": { "display_name": "Pistachio (100+)" }
+ },
+ "defaults": {
+ "model_type": "claude4sonnet",
+ "advanced_prompt": false,
+ "model_version": "USPTO",
+ "stability_flag": true,
+ "hallucination_check": true,
+ "use_protecting_group_feature": false
+ }
+}
\ No newline at end of file
diff --git a/viewer_new/public/runtime-config.json b/viewer_new/public/runtime-config.json
new file mode 100644
index 00000000..9acef2bb
--- /dev/null
+++ b/viewer_new/public/runtime-config.json
@@ -0,0 +1,24 @@
+{
+ "instances": [
+ {
+ "id": "instance1",
+ "label": "Pathway 1",
+ "baseUrl": "http://localhost:5000",
+ "defaults": {
+ "model_type": "claude4sonnet",
+ "advanced_prompt": true,
+ "model_version": "Pistachio_100+",
+ "stability_flag": true,
+ "hallucination_check": true,
+ "use_protecting_group_feature": false
+ }
+ }
+ ],
+ "endpoints": {
+ "retrosynthesis": "/api/retrosynthesis",
+ "rerun": "/api/rerun_retrosynthesis",
+ "partialRerun": "/api/partial_rerun",
+ "saveEdited": "/api/save_edited_result",
+ "health": "/api/health"
+ }
+}
diff --git a/viewer_new/scripts/sync-public-config.ts b/viewer_new/scripts/sync-public-config.ts
new file mode 100644
index 00000000..c0b21457
--- /dev/null
+++ b/viewer_new/scripts/sync-public-config.ts
@@ -0,0 +1,12 @@
+import { copyFile, mkdir } from "node:fs/promises";
+import { dirname, resolve } from "node:path";
+import { fileURLToPath } from "node:url";
+
+const scriptDir = dirname(fileURLToPath(import.meta.url));
+const source = resolve(scriptDir, "../../config/advanced_settings.json");
+const target = resolve(scriptDir, "../public/advanced-settings.json");
+
+await mkdir(dirname(target), { recursive: true });
+await copyFile(source, target);
+
+console.log(`Synced advanced settings to ${target}`);
diff --git a/viewer_new/src/api/flaskClient.ts b/viewer_new/src/api/flaskClient.ts
new file mode 100644
index 00000000..b2b791f0
--- /dev/null
+++ b/viewer_new/src/api/flaskClient.ts
@@ -0,0 +1,77 @@
+import type {
+ AnalysisRequest,
+ PathwayResult,
+ ViewerRuntimeConfig,
+} from "../types/viewer";
+
+type InstanceEndpointConfig = {
+ baseUrl: string;
+ endpoints: ViewerRuntimeConfig["endpoints"];
+ apiKey: string;
+};
+
+async function requestJson(
+ url: string,
+ init: RequestInit,
+): Promise {
+ const response = await fetch(url, init);
+ const data = (await response.json().catch(() => ({}))) as T & {
+ error?: string;
+ };
+
+ if (!response.ok) {
+ throw new Error(data.error || `Request failed with ${response.status}`);
+ }
+
+ return data;
+}
+
+function withHeaders(apiKey: string) {
+ return {
+ "Content-Type": "application/json",
+ "X-API-KEY": apiKey,
+ };
+}
+
+export function createFlaskClient(config: InstanceEndpointConfig) {
+ const buildUrl = (path: string) => `${config.baseUrl}${path}`;
+
+ return {
+ async retrosynthesis(body: AnalysisRequest) {
+ return requestJson(buildUrl(config.endpoints.retrosynthesis), {
+ method: "POST",
+ headers: withHeaders(config.apiKey),
+ body: JSON.stringify(body),
+ });
+ },
+ async rerun(body: AnalysisRequest) {
+ return requestJson(buildUrl(config.endpoints.rerun), {
+ method: "POST",
+ headers: withHeaders(config.apiKey),
+ body: JSON.stringify(body),
+ });
+ },
+ async partialRerun(body: AnalysisRequest & { steps: string }) {
+ return requestJson(buildUrl(config.endpoints.partialRerun), {
+ method: "POST",
+ headers: withHeaders(config.apiKey),
+ body: JSON.stringify(body),
+ });
+ },
+ async saveEditedResult(smiles: string, result: PathwayResult) {
+ return requestJson<{ message: string }>(buildUrl(config.endpoints.saveEdited), {
+ method: "POST",
+ headers: withHeaders(config.apiKey),
+ body: JSON.stringify({ smiles, result }),
+ });
+ },
+ async health() {
+ return requestJson<{ status: string }>(buildUrl(config.endpoints.health), {
+ method: "GET",
+ headers: {
+ "X-API-KEY": config.apiKey,
+ },
+ });
+ },
+ };
+}
diff --git a/viewer_new/src/app/App.tsx b/viewer_new/src/app/App.tsx
new file mode 100644
index 00000000..13a7b550
--- /dev/null
+++ b/viewer_new/src/app/App.tsx
@@ -0,0 +1,392 @@
+import { useMemo, useState } from "react";
+import { Download, KeyRound, Play, RefreshCcw, Save, Sparkles, Upload } from "lucide-react";
+
+import { GraphCanvas } from "../features/graph/GraphCanvas";
+import { InspectorPanel } from "../features/inspector/InspectorPanel";
+import { RunSidebar } from "../features/runs/RunSidebar";
+import { UploadPanel } from "../features/upload/UploadPanel";
+import { EditorDrawer } from "../features/editor/EditorDrawer";
+import { useBootstrapViewer, useViewerActions } from "../features/runs/useViewerActions";
+import { useViewerStore } from "../features/runs/store";
+import { canPartialRerunStep, getStep } from "../lib/pathway";
+import type { HealthStatusMap } from "../types/viewer";
+
+function summarizeHealthStates(states: HealthStatusMap) {
+ const values = Object.values(states);
+ if (!values.length) {
+ return "Health unknown";
+ }
+ if (values.every((value) => value.state === "healthy")) {
+ return "All backends healthy";
+ }
+ if (values.some((value) => value.state === "healthy")) {
+ return "Backend health degraded";
+ }
+ return "Backends unavailable";
+}
+
+export function App() {
+ useBootstrapViewer();
+
+ const runtimeConfig = useViewerStore((state) => state.runtimeConfig);
+ const advancedSettings = useViewerStore((state) => state.advancedSettings);
+ const health = useViewerStore((state) => state.health);
+ const runs = useViewerStore((state) => state.runs);
+ const instanceSettings = useViewerStore((state) => state.instanceSettings);
+ const mode = useViewerStore((state) => state.mode);
+ const apiKey = useViewerStore((state) => state.apiKey);
+ const currentSmiles = useViewerStore((state) => state.currentSmiles);
+ const activeRunKey = useViewerStore((state) => state.activeRunKey);
+ const selectedStepId = useViewerStore((state) => state.selectedStepId);
+ const editorOpen = useViewerStore((state) => state.editorOpen);
+ const setMode = useViewerStore((state) => state.setMode);
+ const setApiKey = useViewerStore((state) => state.setApiKey);
+ const setCurrentSmiles = useViewerStore((state) => state.setCurrentSmiles);
+ const setActiveRun = useViewerStore((state) => state.setActiveRun);
+ const setSelectedStep = useViewerStore((state) => state.setSelectedStep);
+ const updateInstanceSetting = useViewerStore((state) => state.updateInstanceSetting);
+ const setEditorOpen = useViewerStore((state) => state.setEditorOpen);
+
+ const [feedback, setFeedback] = useState("");
+ const [error, setError] = useState("");
+
+ const {
+ activeRun,
+ isAnalyzing,
+ isSaving,
+ isPartialRerunning,
+ analyzeAll,
+ loadUploadedResult,
+ applyStructuredStepEdit,
+ applyRawJsonEdit,
+ saveRunEdits,
+ partialRerun,
+ } = useViewerActions();
+
+ const handleUploadedFiles = (files: Array<{ fileName: string; input: unknown }>) => {
+ const loadedRunKeys = files.map(({ fileName, input }) =>
+ loadUploadedResult(fileName, input),
+ );
+ const latestRunKey = loadedRunKeys[loadedRunKeys.length - 1];
+ if (latestRunKey) {
+ setActiveRun(latestRunKey);
+ }
+ setFeedback(
+ `Loaded ${files.length} file${files.length === 1 ? "" : "s"} into the new viewer.`,
+ );
+ };
+
+ const selectedStep = useMemo(
+ () =>
+ activeRun?.rawResult && selectedStepId
+ ? getStep(activeRun.rawResult, selectedStepId)
+ : undefined,
+ [activeRun?.rawResult, selectedStepId],
+ );
+
+ const partialRerunState = canPartialRerunStep(selectedStep);
+ const saveDisabled = !activeRun || activeRun.source !== "api" || !activeRun.dirty || isSaving;
+ const healthSummary = summarizeHealthStates(health);
+
+ if (!runtimeConfig || !advancedSettings) {
+ return (
+
+
+
DeepRetro
+
Loading `viewer_new`
+
Fetching runtime config, advanced settings, and backend capabilities.
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
{
+ try {
+ setError("");
+ setFeedback("");
+ handleUploadedFiles(files);
+ } catch (nextError) {
+ setError(nextError instanceof Error ? nextError.message : "Upload failed.");
+ }
+ }}
+ onUpdateSetting={updateInstanceSetting}
+ />
+
+
+ {mode === "upload" ? (
+ {
+ try {
+ setError("");
+ setFeedback("");
+ handleUploadedFiles(files);
+ } catch (nextError) {
+ setError(nextError instanceof Error ? nextError.message : "Upload failed.");
+ }
+ }}
+ />
+ ) : null}
+
+
+
+
+
Pathway graph
+
{activeRun?.label ?? "No run selected"}
+
+ {activeRun?.dirty ?
local edits pending : null}
+
+
+ setSelectedStep(stepId)}
+ onEditStep={(stepId) => {
+ setSelectedStep(stepId);
+ setEditorOpen(true);
+ }}
+ onPartialRerunStep={async (stepId) => {
+ if (!activeRunKey) {
+ return;
+ }
+
+ try {
+ setError("");
+ setFeedback("");
+ await partialRerun(activeRunKey, stepId);
+ setFeedback(`Partial rerun completed from step ${stepId}.`);
+ } catch (nextError) {
+ setError(
+ nextError instanceof Error ? nextError.message : "Partial rerun failed.",
+ );
+ }
+ }}
+ />
+
+
+
+ setEditorOpen(true)}
+ onPartialRerun={async () => {
+ if (!activeRunKey || !selectedStepId || !partialRerunState.enabled) {
+ setError(partialRerunState.reason || "Partial rerun is not available.");
+ return;
+ }
+
+ try {
+ setError("");
+ setFeedback("");
+ await partialRerun(activeRunKey, selectedStepId);
+ setFeedback(`Partial rerun completed from step ${selectedStepId}.`);
+ } catch (nextError) {
+ setError(
+ nextError instanceof Error ? nextError.message : "Partial rerun failed.",
+ );
+ }
+ }}
+ onSaveEdits={async () => {
+ if (!activeRunKey) {
+ return;
+ }
+ try {
+ setError("");
+ setFeedback("");
+ await saveRunEdits(activeRunKey);
+ setFeedback("Edits saved to the backend.");
+ } catch (nextError) {
+ setError(nextError instanceof Error ? nextError.message : "Save failed.");
+ }
+ }}
+ saveDisabled={saveDisabled}
+ />
+
+
+ setEditorOpen(false)}
+ onApplyStructured={(stepId, nextStep) => {
+ if (!activeRunKey) {
+ return;
+ }
+ applyStructuredStepEdit(activeRunKey, stepId, nextStep);
+ setFeedback(`Applied structured changes to step ${stepId}.`);
+ }}
+ onApplyRaw={(input) => {
+ if (!activeRunKey) {
+ return;
+ }
+ applyRawJsonEdit(activeRunKey, input);
+ setFeedback("Applied raw pathway JSON changes.");
+ }}
+ />
+
+ {(isPartialRerunning || isSaving) && (
+
+
+
Working
+
{isPartialRerunning ? "Running partial synthesis" : "Saving pathway edits"}
+
The new viewer is waiting for the backend to finish.
+
+
+ )}
+
+ );
+}
diff --git a/viewer_new/src/config/loaders.ts b/viewer_new/src/config/loaders.ts
new file mode 100644
index 00000000..77df98ce
--- /dev/null
+++ b/viewer_new/src/config/loaders.ts
@@ -0,0 +1,20 @@
+import type { AdvancedSettingsConfig, ViewerRuntimeConfig } from "../types/viewer";
+import { advancedSettingsSchema, runtimeConfigSchema } from "../lib/schemas";
+
+async function fetchJson(path: string): Promise {
+ const response = await fetch(path);
+ if (!response.ok) {
+ throw new Error(`Failed to load ${path}: ${response.status}`);
+ }
+ return response.json() as Promise;
+}
+
+export async function loadRuntimeConfig() {
+ const data = await fetchJson("/runtime-config.json");
+ return runtimeConfigSchema.parse(data);
+}
+
+export async function loadAdvancedSettings() {
+ const data = await fetchJson("/advanced-settings.json");
+ return advancedSettingsSchema.parse(data);
+}
diff --git a/viewer_new/src/config/storage.ts b/viewer_new/src/config/storage.ts
new file mode 100644
index 00000000..4df0d6b1
--- /dev/null
+++ b/viewer_new/src/config/storage.ts
@@ -0,0 +1,34 @@
+const keys = {
+ apiKey: "deepretro.viewer.apiKey",
+ lastSmiles: "deepretro.viewer.lastSmiles",
+ activeRun: "deepretro.viewer.activeRun",
+} as const;
+
+function read(key: string) {
+ if (typeof window === "undefined") {
+ return "";
+ }
+ return window.localStorage.getItem(key) ?? "";
+}
+
+function write(key: string, value: string) {
+ if (typeof window === "undefined") {
+ return;
+ }
+
+ if (value) {
+ window.localStorage.setItem(key, value);
+ } else {
+ window.localStorage.removeItem(key);
+ }
+}
+
+export const storage = {
+ keys,
+ getApiKey: () => read(keys.apiKey),
+ setApiKey: (value: string) => write(keys.apiKey, value),
+ getLastSmiles: () => read(keys.lastSmiles),
+ setLastSmiles: (value: string) => write(keys.lastSmiles, value),
+ getActiveRun: () => read(keys.activeRun),
+ setActiveRun: (value: string) => write(keys.activeRun, value),
+};
diff --git a/viewer_new/src/features/editor/EditorDrawer.tsx b/viewer_new/src/features/editor/EditorDrawer.tsx
new file mode 100644
index 00000000..779d3fce
--- /dev/null
+++ b/viewer_new/src/features/editor/EditorDrawer.tsx
@@ -0,0 +1,531 @@
+import { useEffect, useMemo, useState } from "react";
+import CodeMirror from "@uiw/react-codemirror";
+import { json as jsonLanguage } from "@codemirror/lang-json";
+import { X } from "lucide-react";
+
+import { getStep, safeJsonParse, stringifyResult } from "../../lib/pathway";
+import type { MoleculeRecord, PathwayStep, ViewerRun } from "../../types/viewer";
+
+type EditorDrawerProps = {
+ open: boolean;
+ run?: ViewerRun;
+ stepId?: string;
+ onClose: () => void;
+ onApplyStructured: (stepId: string, nextStep: PathwayStep) => void;
+ onApplyRaw: (input: unknown) => void;
+};
+
+function cloneStep(step: PathwayStep | undefined): PathwayStep {
+ if (!step) {
+ return {
+ step: "",
+ products: [],
+ reactants: [],
+ reagents: [],
+ conditions: {},
+ reactionmetrics: [{}],
+ };
+ }
+
+ return JSON.parse(JSON.stringify(step)) as PathwayStep;
+}
+
+function makeBlankMolecule(): MoleculeRecord {
+ return {
+ smiles: "",
+ product_metadata: {},
+ };
+}
+
+function serializeConditionValue(value: unknown) {
+ if (typeof value === "string") {
+ return value;
+ }
+ if (value === undefined) {
+ return "";
+ }
+ return JSON.stringify(value);
+}
+
+function parseConditionValue(value: string) {
+ const trimmed = value.trim();
+ if (!trimmed) {
+ return "";
+ }
+
+ const shouldParseAsJson =
+ trimmed.startsWith("{") ||
+ trimmed.startsWith("[") ||
+ trimmed.startsWith('"') ||
+ trimmed === "true" ||
+ trimmed === "false" ||
+ trimmed === "null" ||
+ /^-?\d+(\.\d+)?$/.test(trimmed);
+
+ if (!shouldParseAsJson) {
+ return value;
+ }
+
+ const parsed = safeJsonParse(trimmed);
+ if (parsed.data !== undefined) {
+ return parsed.data;
+ }
+
+ return value;
+}
+
+function ConditionEditor({
+ conditions,
+ onChange,
+}: {
+ conditions: PathwayStep["conditions"];
+ onChange: (next: PathwayStep["conditions"]) => void;
+}) {
+ const entries = Array.isArray(conditions)
+ ? []
+ : Object.entries(conditions ?? {}).map(([key, value]) => ({
+ key,
+ value: serializeConditionValue(value),
+ }));
+
+ const updateEntries = (
+ nextEntries: Array<{
+ key: string;
+ value: string;
+ }>,
+ ) => {
+ const nextConditions: Record = {};
+ nextEntries.forEach((entry) => {
+ if (!entry.key.trim()) {
+ return;
+ }
+ nextConditions[entry.key] = parseConditionValue(entry.value);
+ });
+ onChange(nextConditions);
+ };
+
+ return (
+
+
+
Conditions
+
+
+ {Array.isArray(conditions) ? (
+
+ This step stores conditions as an array. Switch to raw JSON to edit that payload safely.
+
+ ) : null}
+
+
+ );
+}
+
+function MoleculeEditor({
+ label,
+ molecules,
+ metadataKey,
+ onChange,
+}: {
+ label: string;
+ molecules: MoleculeRecord[];
+ metadataKey: "product_metadata" | "reactant_metadata" | "reagent_metadata";
+ onChange: (next: MoleculeRecord[]) => void;
+}) {
+ return (
+
+
+
{label}
+
+
+
+ {molecules.map((molecule, index) => {
+ const metadata = molecule[metadataKey] ?? {};
+ return (
+
+
+
+
+
+
+
+ );
+ })}
+
+
+ );
+}
+
+export function EditorDrawer({
+ open,
+ run,
+ stepId,
+ onClose,
+ onApplyStructured,
+ onApplyRaw,
+}: EditorDrawerProps) {
+ const selectedStep = useMemo(
+ () => (run?.rawResult && stepId ? getStep(run.rawResult, stepId) : undefined),
+ [run?.rawResult, stepId],
+ );
+ const [tab, setTab] = useState<"structured" | "raw">("structured");
+ const [draft, setDraft] = useState(cloneStep(selectedStep));
+ const [rawText, setRawText] = useState(stringifyResult(run?.rawResult));
+ const [error, setError] = useState("");
+
+ useEffect(() => {
+ setDraft(cloneStep(selectedStep));
+ }, [selectedStep]);
+
+ useEffect(() => {
+ setRawText(stringifyResult(run?.rawResult));
+ }, [run?.rawResult]);
+
+ if (!open || !run || !stepId) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+}
diff --git a/viewer_new/src/features/graph/GraphCanvas.tsx b/viewer_new/src/features/graph/GraphCanvas.tsx
new file mode 100644
index 00000000..0ddcec09
--- /dev/null
+++ b/viewer_new/src/features/graph/GraphCanvas.tsx
@@ -0,0 +1,278 @@
+import { useEffect, useMemo, useRef, useState } from "react";
+import {
+ Background,
+ Controls,
+ type Edge,
+ type FitViewOptions,
+ MiniMap,
+ ReactFlow,
+ ReactFlowProvider,
+ useReactFlow,
+ type NodeTypes,
+} from "@xyflow/react";
+import { Expand, Search } from "lucide-react";
+
+import "@xyflow/react/dist/style.css";
+
+import { buildFlowGraph, type StepFlowNode } from "../../lib/layout";
+import { StepNode } from "./StepNode";
+import type { ViewerRun } from "../../types/viewer";
+
+const nodeTypes = {
+ step: StepNode,
+} satisfies NodeTypes;
+
+type GraphCanvasProps = {
+ run?: ViewerRun;
+ selectedStepId?: string;
+ onSelectStep: (stepId: string) => void;
+ onEditStep: (stepId: string) => void;
+ onPartialRerunStep: (stepId: string) => void;
+};
+
+function GraphCanvasInner({
+ run,
+ selectedStepId,
+ onSelectStep,
+ onEditStep,
+ onPartialRerunStep,
+}: GraphCanvasProps) {
+ const [nodes, setNodes] = useState([]);
+ const [edges, setEdges] = useState([]);
+ const [search, setSearch] = useState("");
+ const shellRef = useRef(null);
+ const handlersRef = useRef({
+ onSelectStep,
+ onEditStep,
+ onPartialRerunStep,
+ });
+ const lastFitSignatureRef = useRef("");
+ const reactFlow = useReactFlow();
+
+ useEffect(() => {
+ handlersRef.current = {
+ onSelectStep,
+ onEditStep,
+ onPartialRerunStep,
+ };
+ }, [onEditStep, onPartialRerunStep, onSelectStep]);
+
+ const searchQuery = search.trim().toLowerCase();
+
+ const matchesSearch = useMemo(() => {
+ return (stepNode: NonNullable["nodes"][number]) => {
+ if (!searchQuery) {
+ return true;
+ }
+
+ const haystacks = [
+ stepNode.title,
+ ...stepNode.products.map((molecule) => `${molecule.label} ${molecule.smiles}`),
+ ...stepNode.reactants.map((molecule) => `${molecule.label} ${molecule.smiles}`),
+ ...stepNode.reagents.map((molecule) => `${molecule.label} ${molecule.smiles}`),
+ ];
+
+ return haystacks.some((value) => value.toLowerCase().includes(searchQuery));
+ };
+ }, [searchQuery]);
+
+ const visibleNodeIds = useMemo(() => {
+ if (!searchQuery) {
+ return new Set(nodes.map((node) => node.id));
+ }
+
+ return new Set(
+ nodes
+ .filter((node) => matchesSearch(node.data.stepNode))
+ .map((node) => node.id),
+ );
+ }, [matchesSearch, nodes, searchQuery]);
+
+ useEffect(() => {
+ let cancelled = false;
+
+ if (!run?.graph) {
+ setNodes([]);
+ setEdges([]);
+ return () => {
+ cancelled = true;
+ };
+ }
+
+ buildFlowGraph(run.graph, {
+ onInspect: (stepId) => handlersRef.current.onSelectStep(stepId),
+ onEdit: (stepId) => handlersRef.current.onEditStep(stepId),
+ onPartialRerun: (stepId) => handlersRef.current.onPartialRerunStep(stepId),
+ matchesSearch: () => true,
+ }).then((flow) => {
+ if (!cancelled) {
+ setNodes(flow.nodes);
+ setEdges(flow.edges);
+ }
+ });
+
+ return () => {
+ cancelled = true;
+ };
+ }, [run?.graph]);
+
+ useEffect(() => {
+ if (!nodes.length) {
+ return;
+ }
+
+ const signature = `${run?.key ?? "no-run"}:${searchQuery}:${Array.from(visibleNodeIds)
+ .sort()
+ .join(",")}`;
+ if (signature === lastFitSignatureRef.current) {
+ return;
+ }
+ lastFitSignatureRef.current = signature;
+
+ const targetNodes = searchQuery
+ ? nodes.filter((node) => visibleNodeIds.has(node.id))
+ : nodes;
+
+ const timeout = window.setTimeout(() => {
+ const options: FitViewOptions = {
+ duration: 350,
+ padding: searchQuery ? 0.08 : 0.14,
+ includeHiddenNodes: true,
+ };
+ if (searchQuery) {
+ options.minZoom = 0.82;
+ }
+ if (targetNodes.length) {
+ options.nodes = targetNodes;
+ }
+ reactFlow.fitView(options);
+ }, 40);
+
+ return () => {
+ window.clearTimeout(timeout);
+ };
+ }, [nodes, reactFlow, searchQuery, visibleNodeIds]);
+
+ useEffect(() => {
+ if (!searchQuery) {
+ return;
+ }
+ const firstMatch = nodes.find((node) => visibleNodeIds.has(node.id));
+ if (firstMatch) {
+ onSelectStep(firstMatch.data.stepNode.stepId);
+ }
+ }, [nodes, onSelectStep, searchQuery, visibleNodeIds]);
+
+ if (!run) {
+ return (
+
+
No pathway selected
+
Run an analysis or upload a pathway JSON to populate the canvas.
+
+ );
+ }
+
+ if (run.status === "error") {
+ return (
+
+
Pathway request failed
+
{run.error}
+
+ );
+ }
+
+ if (!run.graph) {
+ return (
+
+
Waiting for graph data
+
The active run has not produced a pathway graph yet.
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+ ({
+ ...node,
+ hidden: !visibleNodeIds.has(node.id),
+ selected: node.data.stepNode.stepId === selectedStepId,
+ data: {
+ ...node.data,
+ highlighted: visibleNodeIds.has(node.id),
+ },
+ }))}
+ edges={edges.map((edge) => ({
+ ...edge,
+ hidden:
+ !visibleNodeIds.has(edge.source) || !visibleNodeIds.has(edge.target),
+ }))}
+ nodeTypes={nodeTypes}
+ minZoom={0.35}
+ maxZoom={2.2}
+ defaultEdgeOptions={{
+ style: {
+ stroke: "#ffffff",
+ strokeWidth: 4,
+ },
+ }}
+ onNodeClick={(_, node) => onSelectStep(node.data.stepNode.stepId)}
+ proOptions={{ hideAttribution: true }}
+ >
+
+ node.id === "step-0" ? "#7ad2ac" : node.selected ? "#ff8663" : "#8fa7ca"
+ }
+ />
+
+
+
+
+
+ );
+}
+
+export function GraphCanvas(props: GraphCanvasProps) {
+ return (
+
+
+
+ );
+}
diff --git a/viewer_new/src/features/graph/MoleculePreview.tsx b/viewer_new/src/features/graph/MoleculePreview.tsx
new file mode 100644
index 00000000..2b654614
--- /dev/null
+++ b/viewer_new/src/features/graph/MoleculePreview.tsx
@@ -0,0 +1,77 @@
+import { useEffect, useState } from "react";
+
+import { renderMoleculeSvg } from "../../lib/rdkit";
+
+type MoleculePreviewProps = {
+ smiles: string;
+ label: string;
+ width?: number;
+ height?: number;
+ compact?: boolean;
+};
+
+export function MoleculePreview({
+ smiles,
+ label,
+ width = 180,
+ height = 120,
+ compact = false,
+}: MoleculePreviewProps) {
+ const [svg, setSvg] = useState("");
+ const [error, setError] = useState("");
+
+ useEffect(() => {
+ let cancelled = false;
+ setSvg("");
+ setError("");
+
+ if (!smiles) {
+ setError("Missing SMILES");
+ return () => {
+ cancelled = true;
+ };
+ }
+
+ renderMoleculeSvg(smiles, width, height)
+ .then((nextSvg) => {
+ if (!cancelled) {
+ setSvg(nextSvg);
+ }
+ })
+ .catch((nextError) => {
+ if (!cancelled) {
+ setError(nextError instanceof Error ? nextError.message : "Render failed");
+ }
+ });
+
+ return () => {
+ cancelled = true;
+ };
+ }, [height, smiles, width]);
+
+ if (error) {
+ return (
+
+ {label}
+ {smiles}
+ {error}
+
+ );
+ }
+
+ if (!svg) {
+ return (
+
+ Rendering...
+
+ );
+ }
+
+ return (
+
+ );
+}
diff --git a/viewer_new/src/features/graph/StepNode.tsx b/viewer_new/src/features/graph/StepNode.tsx
new file mode 100644
index 00000000..4bc64841
--- /dev/null
+++ b/viewer_new/src/features/graph/StepNode.tsx
@@ -0,0 +1,126 @@
+import { Handle, Position, type NodeProps } from "@xyflow/react";
+import { Beaker, FlaskConical, Pencil, RefreshCcw } from "lucide-react";
+
+import { formatConfidence } from "../../lib/pathway";
+import { MoleculePreview } from "./MoleculePreview";
+import type { StepFlowNode } from "../../lib/layout";
+
+export function StepNode({
+ data,
+ selected,
+}: NodeProps) {
+ const stepNode = data.stepNode;
+ const product = stepNode.products[0];
+ const primaryFormula = product?.formula ?? product?.label;
+
+ return (
+ data.onInspect(stepNode.stepId)}
+ role="button"
+ tabIndex={0}
+ style={{
+ width: `${data.cardWidth}px`,
+ minHeight: `${data.cardHeight}px`,
+ }}
+ >
+
+
+
+ {product ? (
+
+
+
+ ) : (
+
No product data available
+ )}
+
+
+
+
+
{stepNode.isVirtualRoot ? "Target" : "Synthesis step"}
+
{stepNode.isVirtualRoot ? "Target" : `Step ${stepNode.stepId}`}
+
+
+ {stepNode.metrics?.confidenceestimate !== undefined ? (
+
+
+ {formatConfidence(stepNode.metrics.confidenceestimate)}
+
+ ) : null}
+ {stepNode.metrics?.scalabilityindex !== undefined ? (
+
+
+ Scale {stepNode.metrics.scalabilityindex}
+
+ ) : null}
+
+
+
+
+ {primaryFormula ? (
+ {primaryFormula}
+ ) : null}
+ {stepNode.products.length} product
+ {stepNode.reactants.length} reactants
+ {stepNode.reagents.length ? (
+ {stepNode.reagents.length} reagents
+ ) : null}
+
+
+ {!stepNode.isVirtualRoot ? (
+
+
+
+
+
+ ) : null}
+
+
+ );
+}
diff --git a/viewer_new/src/features/inspector/InspectorPanel.tsx b/viewer_new/src/features/inspector/InspectorPanel.tsx
new file mode 100644
index 00000000..260519e1
--- /dev/null
+++ b/viewer_new/src/features/inspector/InspectorPanel.tsx
@@ -0,0 +1,256 @@
+import { ChevronDown, Pencil, RefreshCcw, Save } from "lucide-react";
+
+import {
+ canPartialRerunStep,
+ formatConfidence,
+ getStep,
+ stringifyResult,
+} from "../../lib/pathway";
+import { MoleculePreview } from "../graph/MoleculePreview";
+import type { ViewerRun } from "../../types/viewer";
+
+type InspectorPanelProps = {
+ run?: ViewerRun;
+ selectedStepId?: string;
+ onSelectStep: (stepId: string) => void;
+ onEdit: () => void;
+ onPartialRerun: () => void;
+ onSaveEdits: () => void;
+ saveDisabled: boolean;
+};
+
+function MoleculeSection({
+ title,
+ molecules,
+}: {
+ title: string;
+ molecules: NonNullable["nodes"][number]["products"]>;
+}) {
+ if (!molecules.length) {
+ return null;
+ }
+
+ return (
+
+
+
{title}
+ {molecules.length}
+
+
+ {molecules.map((molecule) => (
+
+
+ {molecule.label}
+ {molecule.smiles}
+
+ {molecule.formula ? {molecule.formula} : null}
+ {typeof molecule.mass === "number" ? (
+ {molecule.mass.toFixed(1)} g/mol
+ ) : null}
+
+
+ ))}
+
+
+ );
+}
+
+function ConditionCards({
+ conditions,
+}: {
+ conditions: Record | unknown[] | undefined;
+}) {
+ if (!conditions || Array.isArray(conditions)) {
+ return (
+
+
+ Conditions
+ {conditions ? "Unstructured payload" : "Not provided"}
+
+
+ );
+ }
+
+ const entries = Object.entries(conditions);
+ if (!entries.length) {
+ return (
+
+
+ Conditions
+ Not provided
+
+
+ );
+ }
+
+ return (
+
+ {entries.map(([key, value]) => (
+
+ {key.replaceAll("_", " ")}
+ {typeof value === "string" ? value : JSON.stringify(value)}
+
+ ))}
+
+ );
+}
+
+export function InspectorPanel({
+ run,
+ selectedStepId,
+ onSelectStep,
+ onEdit,
+ onPartialRerun,
+ onSaveEdits,
+ saveDisabled,
+}: InspectorPanelProps) {
+ if (!run?.graph) {
+ return (
+
+ );
+ }
+
+ const activeStepId = selectedStepId ?? run.graph.virtualRoot.stepId;
+ const stepNode = run.graph.nodes.find((node) => node.stepId === activeStepId);
+ const rawStep = getStep(run.rawResult, activeStepId);
+ const rerunState = canPartialRerunStep(rawStep);
+
+ return (
+
+ );
+}
diff --git a/viewer_new/src/features/runs/RunSidebar.tsx b/viewer_new/src/features/runs/RunSidebar.tsx
new file mode 100644
index 00000000..9ed9055c
--- /dev/null
+++ b/viewer_new/src/features/runs/RunSidebar.tsx
@@ -0,0 +1,285 @@
+import { useId } from "react";
+import { Activity, CheckCircle2, CircleAlert, LoaderCircle, Upload } from "lucide-react";
+
+import type {
+ AdvancedSettingsConfig,
+ HealthStatusMap,
+ InstanceRequestSettings,
+ ViewerRun,
+ ViewerRuntimeConfig,
+} from "../../types/viewer";
+
+type RunSidebarProps = {
+ runtimeConfig?: ViewerRuntimeConfig;
+ advancedSettings?: AdvancedSettingsConfig;
+ health: HealthStatusMap;
+ instanceSettings: Record;
+ runs: Record;
+ activeRunKey?: string;
+ onRunSelect: (runKey: string) => void;
+ onUploadFiles: (files: Array<{ fileName: string; input: unknown }>) => void;
+ onUpdateSetting: (
+ instanceId: string,
+ patch: Partial,
+ ) => void;
+};
+
+function StatusIcon({ run }: { run?: ViewerRun }) {
+ if (!run) {
+ return ;
+ }
+ if (run.status === "pending") {
+ return ;
+ }
+ if (run.status === "success") {
+ return ;
+ }
+ if (run.status === "error") {
+ return ;
+ }
+ return ;
+}
+
+function ToggleRow({
+ label,
+ checked,
+ disabled,
+ onChange,
+}: {
+ label: string;
+ checked: boolean;
+ disabled?: boolean;
+ onChange: (checked: boolean) => void;
+}) {
+ return (
+
+ );
+}
+
+export function RunSidebar({
+ runtimeConfig,
+ advancedSettings,
+ health,
+ instanceSettings,
+ runs,
+ activeRunKey,
+ onRunSelect,
+ onUploadFiles,
+ onUpdateSetting,
+}: RunSidebarProps) {
+ const uploadInputId = useId();
+ const uploadedRuns = Object.values(runs).filter((run) => run.source === "file");
+
+ return (
+
+ );
+}
diff --git a/viewer_new/src/features/runs/store.ts b/viewer_new/src/features/runs/store.ts
new file mode 100644
index 00000000..640ec24f
--- /dev/null
+++ b/viewer_new/src/features/runs/store.ts
@@ -0,0 +1,96 @@
+import { create } from "zustand";
+
+import { storage } from "../../config/storage";
+import type {
+ AdvancedSettingsConfig,
+ HealthStatusMap,
+ InstanceRequestSettings,
+ ViewerMode,
+ ViewerRun,
+ ViewerRuntimeConfig,
+ ViewerStoreState,
+} from "../../types/viewer";
+
+type ViewerActions = {
+ bootstrap: (payload: {
+ runtimeConfig: ViewerRuntimeConfig;
+ advancedSettings: AdvancedSettingsConfig;
+ }) => void;
+ setMode: (mode: ViewerMode) => void;
+ setApiKey: (apiKey: string) => void;
+ setCurrentSmiles: (smiles: string) => void;
+ setHealth: (health: HealthStatusMap) => void;
+ setActiveRun: (runKey?: string) => void;
+ setSelectedStep: (stepId?: string) => void;
+ setRuns: (
+ updater: (runs: Record) => Record,
+ ) => void;
+ updateInstanceSetting: (
+ instanceId: string,
+ patch: Partial,
+ ) => void;
+ setEditorOpen: (open: boolean) => void;
+};
+
+type ViewerStore = ViewerStoreState & ViewerActions;
+
+export const useViewerStore = create((set, get) => ({
+ mode: "analyze",
+ apiKey: storage.getApiKey(),
+ currentSmiles: storage.getLastSmiles(),
+ activeRunKey: storage.getActiveRun() || undefined,
+ selectedStepId: undefined,
+ runs: {},
+ instanceSettings: {},
+ health: {},
+ editorOpen: false,
+ bootstrap: ({ runtimeConfig, advancedSettings }) => {
+ const existing = get().instanceSettings;
+ const nextSettings = Object.fromEntries(
+ runtimeConfig.instances.map((instance) => [
+ instance.id,
+ existing[instance.id] ?? instance.defaults,
+ ]),
+ );
+
+ set({
+ runtimeConfig,
+ advancedSettings,
+ instanceSettings: nextSettings,
+ });
+ },
+ setMode: (mode) => set({ mode }),
+ setApiKey: (apiKey) => {
+ storage.setApiKey(apiKey);
+ set({ apiKey });
+ },
+ setCurrentSmiles: (smiles) => {
+ storage.setLastSmiles(smiles);
+ set({ currentSmiles: smiles });
+ },
+ setHealth: (health) => set({ health }),
+ setActiveRun: (runKey) => {
+ storage.setActiveRun(runKey ?? "");
+ const nextRun = runKey ? get().runs[runKey] : undefined;
+ const nextStepId =
+ nextRun?.graph?.nodes.find((node) => !node.isVirtualRoot)?.stepId ??
+ nextRun?.graph?.virtualRoot.stepId;
+ set({
+ activeRunKey: runKey,
+ selectedStepId: nextStepId,
+ });
+ },
+ setSelectedStep: (stepId) => set({ selectedStepId: stepId }),
+ setRuns: (updater) => set((state) => ({ runs: updater(state.runs) })),
+ updateInstanceSetting: (instanceId, patch) =>
+ set((state) => ({
+ instanceSettings: {
+ ...state.instanceSettings,
+ [instanceId]: {
+ ...state.instanceSettings[instanceId],
+ ...patch,
+ },
+ },
+ })),
+ setEditorOpen: (editorOpen) => set({ editorOpen }),
+}));
diff --git a/viewer_new/src/features/runs/useViewerActions.ts b/viewer_new/src/features/runs/useViewerActions.ts
new file mode 100644
index 00000000..810745da
--- /dev/null
+++ b/viewer_new/src/features/runs/useViewerActions.ts
@@ -0,0 +1,464 @@
+import { useCallback, useEffect, useRef } from "react";
+import { useMutation } from "@tanstack/react-query";
+
+import { loadAdvancedSettings, loadRuntimeConfig } from "../../config/loaders";
+import { createFlaskClient } from "../../api/flaskClient";
+import { storage } from "../../config/storage";
+import {
+ buildPathwayGraph,
+ canPartialRerunStep,
+ getStep,
+ inferTopLevelSmiles,
+ updateStepInResult,
+ validatePathwayResult,
+ validatePathwayStep,
+} from "../../lib/pathway";
+import { useViewerStore } from "./store";
+import type {
+ AnalysisRequest,
+ HealthStatusMap,
+ ViewerRun,
+} from "../../types/viewer";
+
+function createRun(
+ key: string,
+ label: string,
+ status: ViewerRun["status"],
+ source: ViewerRun["source"],
+ payload: Partial,
+): ViewerRun {
+ return {
+ key,
+ label,
+ source,
+ status,
+ dirty: false,
+ lastUpdatedAt: Date.now(),
+ ...payload,
+ };
+}
+
+export function useBootstrapViewer() {
+ const bootstrap = useViewerStore((state) => state.bootstrap);
+
+ useEffect(() => {
+ let cancelled = false;
+
+ Promise.all([loadRuntimeConfig(), loadAdvancedSettings()])
+ .then(([runtimeConfig, advancedSettings]) => {
+ if (!cancelled) {
+ bootstrap({ runtimeConfig, advancedSettings });
+ }
+ })
+ .catch((error) => {
+ console.error("Failed to bootstrap viewer:", error);
+ });
+
+ return () => {
+ cancelled = true;
+ };
+ }, [bootstrap]);
+}
+
+export function useViewerActions() {
+ const runtimeConfig = useViewerStore((state) => state.runtimeConfig);
+ const instanceSettings = useViewerStore((state) => state.instanceSettings);
+ const apiKey = useViewerStore((state) => state.apiKey);
+ const runs = useViewerStore((state) => state.runs);
+ const activeRunKey = useViewerStore((state) => state.activeRunKey);
+ const setRuns = useViewerStore((state) => state.setRuns);
+ const setHealth = useViewerStore((state) => state.setHealth);
+ const setActiveRun = useViewerStore((state) => state.setActiveRun);
+ const setSelectedStep = useViewerStore((state) => state.setSelectedStep);
+ const healthAttemptRef = useRef(0);
+ const healthTimeoutRef = useRef(null);
+
+ const runHealthCheck = useCallback(async () => {
+ if (!runtimeConfig) {
+ return {} as HealthStatusMap;
+ }
+
+ const statusMap: HealthStatusMap = {};
+
+ if (!apiKey) {
+ for (const instance of runtimeConfig.instances) {
+ statusMap[instance.id] = {
+ state: "unauthorized",
+ message: "API key required",
+ };
+ }
+ setHealth(statusMap);
+ return statusMap;
+ }
+
+ await Promise.all(
+ runtimeConfig.instances.map(async (instance) => {
+ const client = createFlaskClient({
+ baseUrl: instance.baseUrl,
+ endpoints: runtimeConfig.endpoints,
+ apiKey,
+ });
+
+ try {
+ await client.health();
+ statusMap[instance.id] = {
+ state: "healthy",
+ message: "Healthy",
+ };
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Health check failed";
+ statusMap[instance.id] = {
+ state: message.toLowerCase().includes("unauthorized")
+ ? "unauthorized"
+ : "error",
+ message,
+ };
+ }
+ }),
+ );
+
+ setHealth(statusMap);
+ return statusMap;
+ }, [apiKey, runtimeConfig, setHealth]);
+
+ const healthMutation = useMutation({
+ mutationFn: runHealthCheck,
+ });
+
+ const analyzeMutation = useMutation({
+ mutationFn: async ({
+ smiles,
+ rerun,
+ }: {
+ smiles: string;
+ rerun: boolean;
+ }) => {
+ if (!runtimeConfig) {
+ throw new Error("Runtime config has not loaded yet.");
+ }
+
+ if (!apiKey) {
+ throw new Error("An API key is required before running the viewer.");
+ }
+
+ const pendingRuns = runtimeConfig.instances.reduce>(
+ (accumulator, instance) => {
+ const key = `api:${instance.id}`;
+ accumulator[key] = createRun(key, instance.label, "pending", "api", {
+ instanceId: instance.id,
+ request: {
+ smiles,
+ ...instanceSettings[instance.id],
+ },
+ });
+ return accumulator;
+ },
+ {},
+ );
+
+ setRuns((previous) => ({
+ ...previous,
+ ...pendingRuns,
+ }));
+
+ const results = await Promise.all(
+ runtimeConfig.instances.map(async (instance) => {
+ const request: AnalysisRequest = {
+ smiles,
+ ...instanceSettings[instance.id],
+ };
+
+ const startedAt = performance.now();
+ const client = createFlaskClient({
+ baseUrl: instance.baseUrl,
+ endpoints: runtimeConfig.endpoints,
+ apiKey,
+ });
+
+ try {
+ const rawResult = rerun
+ ? await client.rerun(request)
+ : await client.retrosynthesis(request);
+ return {
+ key: `api:${instance.id}`,
+ run: createRun(`api:${instance.id}`, instance.label, "success", "api", {
+ instanceId: instance.id,
+ request,
+ rawResult,
+ graph: buildPathwayGraph(rawResult),
+ durationMs: Math.round(performance.now() - startedAt),
+ }),
+ };
+ } catch (error) {
+ return {
+ key: `api:${instance.id}`,
+ run: createRun(`api:${instance.id}`, instance.label, "error", "api", {
+ instanceId: instance.id,
+ request,
+ error: error instanceof Error ? error.message : "Request failed",
+ durationMs: Math.round(performance.now() - startedAt),
+ }),
+ };
+ }
+ }),
+ );
+
+ return { smiles, results };
+ },
+ onSuccess: ({ smiles, results }) => {
+ storage.setLastSmiles(smiles);
+ setRuns((previous) => {
+ const nextRuns = { ...previous };
+ for (const { key, run } of results) {
+ nextRuns[key] = run;
+ }
+ return nextRuns;
+ });
+
+ const firstSuccessfulRun = results.find((item) => item.run.status === "success");
+ setActiveRun(firstSuccessfulRun?.key ?? results[0]?.key);
+ setSelectedStep("0");
+ void healthMutation.mutateAsync();
+ },
+ });
+
+ const saveMutation = useMutation({
+ mutationFn: async (runKey: string) => {
+ if (!runtimeConfig) {
+ throw new Error("Runtime config has not loaded yet.");
+ }
+
+ const run = runs[runKey];
+ if (!run?.instanceId || !run.rawResult || !run.request) {
+ throw new Error("Only API runs with loaded results can be saved.");
+ }
+
+ const instance = runtimeConfig.instances.find(
+ (candidate) => candidate.id === run.instanceId,
+ );
+ if (!instance) {
+ throw new Error("Missing backend instance configuration.");
+ }
+
+ const client = createFlaskClient({
+ baseUrl: instance.baseUrl,
+ endpoints: runtimeConfig.endpoints,
+ apiKey,
+ });
+
+ const smiles = run.request.smiles || inferTopLevelSmiles(run.rawResult);
+ await client.saveEditedResult(smiles, run.rawResult);
+ return runKey;
+ },
+ onSuccess: (runKey) => {
+ setRuns((previous) => ({
+ ...previous,
+ [runKey]: {
+ ...previous[runKey],
+ dirty: false,
+ lastUpdatedAt: Date.now(),
+ },
+ }));
+ },
+ });
+
+ const partialRerunMutation = useMutation({
+ mutationFn: async ({
+ runKey,
+ stepId,
+ }: {
+ runKey: string;
+ stepId: string;
+ }) => {
+ if (!runtimeConfig) {
+ throw new Error("Runtime config has not loaded yet.");
+ }
+
+ const run = runs[runKey];
+ if (!run?.instanceId || !run.request || !run.rawResult) {
+ throw new Error("Select an API run before triggering partial rerun.");
+ }
+
+ const step = getStep(run.rawResult, stepId);
+ const eligibility = canPartialRerunStep(step);
+ if (!eligibility.enabled) {
+ throw new Error(eligibility.reason);
+ }
+
+ const instance = runtimeConfig.instances.find(
+ (candidate) => candidate.id === run.instanceId,
+ );
+ if (!instance) {
+ throw new Error("Missing backend instance configuration.");
+ }
+
+ const client = createFlaskClient({
+ baseUrl: instance.baseUrl,
+ endpoints: runtimeConfig.endpoints,
+ apiKey,
+ });
+
+ const request = {
+ smiles: run.request.smiles,
+ steps: stepId,
+ ...instanceSettings[run.instanceId],
+ };
+
+ const rawResult = await client.partialRerun(request);
+ return { runKey, rawResult };
+ },
+ onSuccess: ({ runKey, rawResult }) => {
+ const currentRun = runs[runKey];
+ setRuns((previous) => ({
+ ...previous,
+ [runKey]: {
+ ...previous[runKey],
+ rawResult,
+ graph: buildPathwayGraph(rawResult),
+ dirty: false,
+ error: undefined,
+ status: "success",
+ lastUpdatedAt: Date.now(),
+ request: currentRun?.request
+ ? {
+ ...currentRun.request,
+ smiles: currentRun.request.smiles,
+ }
+ : currentRun?.request,
+ },
+ }));
+ setActiveRun(runKey);
+ setSelectedStep("0");
+ },
+ });
+
+ useEffect(() => {
+ if (!runtimeConfig) {
+ return;
+ }
+
+ if (healthTimeoutRef.current) {
+ window.clearTimeout(healthTimeoutRef.current);
+ healthTimeoutRef.current = null;
+ }
+
+ if (!apiKey) {
+ void runHealthCheck();
+ return;
+ }
+
+ let cancelled = false;
+
+ const scheduleNext = (delayMs: number) => {
+ if (cancelled) {
+ return;
+ }
+
+ healthTimeoutRef.current = window.setTimeout(async () => {
+ const statusMap = await runHealthCheck();
+ const hasRecoverableError = Object.values(statusMap).some(
+ (status) => status.state === "error",
+ );
+
+ healthAttemptRef.current = hasRecoverableError
+ ? healthAttemptRef.current + 1
+ : 0;
+
+ const nextDelay = hasRecoverableError
+ ? Math.min(30_000, 1_000 * 2 ** healthAttemptRef.current)
+ : 15_000;
+
+ scheduleNext(nextDelay);
+ }, delayMs);
+ };
+
+ healthAttemptRef.current = 0;
+ scheduleNext(0);
+
+ return () => {
+ cancelled = true;
+ if (healthTimeoutRef.current) {
+ window.clearTimeout(healthTimeoutRef.current);
+ healthTimeoutRef.current = null;
+ }
+ };
+ }, [apiKey, runHealthCheck, runtimeConfig]);
+
+ return {
+ runtimeConfig,
+ activeRun: activeRunKey ? runs[activeRunKey] : undefined,
+ activeRunKey,
+ isAnalyzing: analyzeMutation.isPending,
+ isSaving: saveMutation.isPending,
+ isPartialRerunning: partialRerunMutation.isPending,
+ refreshHealth: () => healthMutation.mutateAsync(),
+ analyzeAll: (smiles: string, rerun = false) =>
+ analyzeMutation.mutateAsync({ smiles, rerun }),
+ loadUploadedResult: (fileName: string, input: unknown) => {
+ const validation = validatePathwayResult(input);
+ if (!validation.data) {
+ throw new Error(validation.issues.map((issue) => issue.message).join("\n"));
+ }
+
+ const key = `file:${fileName}`;
+ const run = createRun(key, fileName, "success", "file", {
+ rawResult: validation.data,
+ graph: buildPathwayGraph(validation.data),
+ });
+
+ setRuns((previous) => ({
+ ...previous,
+ [key]: run,
+ }));
+ setActiveRun(key);
+ setSelectedStep("0");
+ return key;
+ },
+ applyStructuredStepEdit: (runKey: string, stepId: string, nextStep: unknown) => {
+ const validation = validatePathwayStep(nextStep);
+ if (!validation.data) {
+ throw new Error(validation.issues.map((issue) => issue.message).join("\n"));
+ }
+
+ const run = runs[runKey];
+ if (!run?.rawResult) {
+ throw new Error("No run data is loaded.");
+ }
+
+ const rawResult = updateStepInResult(run.rawResult, stepId, validation.data);
+ const graph = buildPathwayGraph(rawResult);
+
+ setRuns((previous) => ({
+ ...previous,
+ [runKey]: {
+ ...previous[runKey],
+ rawResult,
+ graph,
+ dirty: true,
+ lastUpdatedAt: Date.now(),
+ },
+ }));
+ },
+ applyRawJsonEdit: (runKey: string, input: unknown) => {
+ const validation = validatePathwayResult(input);
+ if (!validation.data) {
+ throw new Error(validation.issues.map((issue) => issue.message).join("\n"));
+ }
+
+ setRuns((previous) => ({
+ ...previous,
+ [runKey]: {
+ ...previous[runKey],
+ rawResult: validation.data,
+ graph: buildPathwayGraph(validation.data),
+ dirty: true,
+ lastUpdatedAt: Date.now(),
+ },
+ }));
+ setSelectedStep("0");
+ },
+ saveRunEdits: (runKey: string) => saveMutation.mutateAsync(runKey),
+ partialRerun: (runKey: string, stepId: string) =>
+ partialRerunMutation.mutateAsync({ runKey, stepId }),
+ };
+}
diff --git a/viewer_new/src/features/upload/UploadPanel.test.tsx b/viewer_new/src/features/upload/UploadPanel.test.tsx
new file mode 100644
index 00000000..a34ba602
--- /dev/null
+++ b/viewer_new/src/features/upload/UploadPanel.test.tsx
@@ -0,0 +1,52 @@
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { describe, expect, test, vi } from "vitest";
+
+import { UploadPanel } from "./UploadPanel";
+
+describe("UploadPanel", () => {
+ test("parses a valid JSON upload and forwards it to the callback", async () => {
+ const onLoad = vi.fn();
+ const user = userEvent.setup();
+
+ render(
+ {
+ files.forEach(({ fileName, input }) => onLoad(fileName, input));
+ }}
+ />,
+ );
+
+ const input = screen.getByLabelText(/choose one or more `.json` pathway exports/i);
+ const file = new File([JSON.stringify({ steps: [] })], "pathway.json", {
+ type: "application/json",
+ });
+
+ await user.upload(input, file);
+
+ expect(onLoad).toHaveBeenCalledWith("pathway.json", { steps: [] });
+ });
+
+ test("surfaces a parsing error for malformed JSON", async () => {
+ const onLoad = vi.fn();
+ const user = userEvent.setup();
+
+ render(
+ {
+ files.forEach(({ fileName, input }) => onLoad(fileName, input));
+ }}
+ />,
+ );
+
+ const input = screen.getByLabelText(/choose one or more `.json` pathway exports/i);
+ const file = new File(["not-json"], "broken.json", {
+ type: "application/json",
+ });
+
+ await user.upload(input, file);
+
+ expect(onLoad).not.toHaveBeenCalled();
+ expect(screen.getByText(/unexpected token/i)).toBeInTheDocument();
+ });
+});
diff --git a/viewer_new/src/features/upload/UploadPanel.tsx b/viewer_new/src/features/upload/UploadPanel.tsx
new file mode 100644
index 00000000..5f036625
--- /dev/null
+++ b/viewer_new/src/features/upload/UploadPanel.tsx
@@ -0,0 +1,60 @@
+import { useId, useState } from "react";
+import { FileUp } from "lucide-react";
+
+type UploadPanelProps = {
+ onLoadFiles: (files: Array<{ fileName: string; input: unknown }>) => void;
+};
+
+export function UploadPanel({ onLoadFiles }: UploadPanelProps) {
+ const inputId = useId();
+ const [error, setError] = useState("");
+
+ return (
+
+
+
+
Upload mode
+
Inspect existing pathway JSON
+
+
+
+ {
+ const files = Array.from(event.target.files ?? []);
+ if (!files.length) {
+ return;
+ }
+
+ try {
+ setError("");
+ const parsedFiles = await Promise.all(
+ files.map(async (file) => ({
+ fileName: file.name,
+ input: JSON.parse(await file.text()),
+ })),
+ );
+ onLoadFiles(parsedFiles);
+ } catch (nextError) {
+ setError(
+ nextError instanceof Error ? nextError.message : "Failed to load the file.",
+ );
+ } finally {
+ event.target.value = "";
+ }
+ }}
+ />
+ {error ? {error}
: null}
+
+ );
+}
diff --git a/viewer_new/src/lib/layout.ts b/viewer_new/src/lib/layout.ts
new file mode 100644
index 00000000..036c3004
--- /dev/null
+++ b/viewer_new/src/lib/layout.ts
@@ -0,0 +1,227 @@
+import ELK from "elkjs/lib/elk.bundled.js";
+import type { Edge, Node } from "@xyflow/react";
+import { MarkerType, Position } from "@xyflow/react";
+
+import type {
+ NormalizedPathwayGraph,
+ NormalizedStepNode,
+} from "../types/viewer";
+
+export type StepFlowNode = Node<{
+ stepNode: NormalizedStepNode;
+ highlighted: boolean;
+ cardWidth: number;
+ cardHeight: number;
+ previewWidth: number;
+ previewHeight: number;
+ onInspect: (stepId: string) => void;
+ onEdit: (stepId: string) => void;
+ onPartialRerun: (stepId: string) => void;
+}>;
+
+const elk = new ELK();
+
+function getMoleculeSizing(stepNode: NormalizedStepNode) {
+ const smiles = stepNode.products[0]?.smiles ?? "";
+ const atomCount = (smiles.match(/[A-Z][a-z]?/g) ?? []).length;
+ const complexity = Math.max(smiles.length, atomCount * 4);
+
+ if (stepNode.isVirtualRoot) {
+ if (complexity > 110) {
+ return {
+ cardWidth: 500,
+ cardHeight: 460,
+ previewWidth: 420,
+ previewHeight: 380,
+ };
+ }
+
+ return {
+ cardWidth: 430,
+ cardHeight: 390,
+ previewWidth: 360,
+ previewHeight: 320,
+ };
+ }
+
+ if (complexity > 110) {
+ return {
+ cardWidth: 430,
+ cardHeight: 390,
+ previewWidth: 360,
+ previewHeight: 320,
+ };
+ }
+
+ if (complexity > 75) {
+ return {
+ cardWidth: 370,
+ cardHeight: 340,
+ previewWidth: 310,
+ previewHeight: 275,
+ };
+ }
+
+ return {
+ cardWidth: stepNode.isVirtualRoot ? 430 : 290,
+ cardHeight: stepNode.isVirtualRoot ? 390 : 290,
+ previewWidth: stepNode.isVirtualRoot ? 360 : 235,
+ previewHeight: stepNode.isVirtualRoot ? 320 : 210,
+ };
+}
+
+function toFlowEdges(graph: NormalizedPathwayGraph) {
+ return graph.edges.map((edge) => ({
+ ...edge,
+ type: "smoothstep" as const,
+ animated: false,
+ markerEnd: {
+ type: MarkerType.ArrowClosed,
+ width: 24,
+ height: 24,
+ color: "#ffffff",
+ },
+ style: {
+ stroke: "#ffffff",
+ strokeOpacity: 1,
+ strokeWidth: 4,
+ filter: "drop-shadow(0 0 10px rgba(255, 255, 255, 0.55))",
+ },
+ }));
+}
+
+function buildFallbackFlowGraph(
+ graph: NormalizedPathwayGraph,
+ options: {
+ onInspect: (stepId: string) => void;
+ onEdit: (stepId: string) => void;
+ onPartialRerun: (stepId: string) => void;
+ matchesSearch: (stepNode: NormalizedStepNode) => boolean;
+ },
+) {
+ const depthMap = new Map([["step-0", 0]]);
+ const queue = ["step-0"];
+
+ while (queue.length) {
+ const currentId = queue.shift()!;
+ const currentDepth = depthMap.get(currentId) ?? 0;
+ for (const edge of graph.edges) {
+ if (edge.source !== currentId) {
+ continue;
+ }
+ if (!depthMap.has(edge.target)) {
+ depthMap.set(edge.target, currentDepth + 1);
+ queue.push(edge.target);
+ }
+ }
+ }
+
+ const rowsByDepth = new Map();
+
+ return {
+ nodes: graph.nodes.map((stepNode) => {
+ const depth = depthMap.get(stepNode.id) ?? 0;
+ const row = rowsByDepth.get(depth) ?? 0;
+ rowsByDepth.set(depth, row + 1);
+ const sizing = getMoleculeSizing(stepNode);
+
+ return {
+ id: stepNode.id,
+ type: "step",
+ position: {
+ x: depth * 420,
+ y: row * 280,
+ },
+ sourcePosition: Position.Right,
+ targetPosition: Position.Left,
+ data: {
+ stepNode,
+ highlighted: options.matchesSearch(stepNode),
+ ...sizing,
+ onInspect: options.onInspect,
+ onEdit: options.onEdit,
+ onPartialRerun: options.onPartialRerun,
+ },
+ draggable: false,
+ };
+ }),
+ edges: toFlowEdges(graph),
+ };
+}
+
+export async function buildFlowGraph(
+ graph: NormalizedPathwayGraph,
+ options: {
+ onInspect: (stepId: string) => void;
+ onEdit: (stepId: string) => void;
+ onPartialRerun: (stepId: string) => void;
+ matchesSearch: (stepNode: NormalizedStepNode) => boolean;
+ },
+): Promise<{
+ nodes: StepFlowNode[];
+ edges: Edge[];
+}> {
+ try {
+ const elkGraph = await elk.layout({
+ id: "deepretro-pathway",
+ layoutOptions: {
+ "elk.algorithm": "layered",
+ "elk.direction": "RIGHT",
+ "elk.layered.spacing.nodeNodeBetweenLayers": "280",
+ "elk.spacing.nodeNode": "210",
+ "elk.padding": "[top=72,left=72,bottom=72,right=140]",
+ },
+ children: graph.nodes.map((stepNode) => {
+ const sizing = getMoleculeSizing(stepNode);
+ return {
+ id: stepNode.id,
+ width: sizing.cardWidth,
+ height: sizing.cardHeight,
+ };
+ }),
+ edges: graph.edges.map((edge) => ({
+ id: edge.id,
+ sources: [edge.source],
+ targets: [edge.target],
+ })),
+ });
+
+ const layoutNodes = new Map(
+ (elkGraph.children ?? []).map((child) => [child.id, child]),
+ );
+
+ if (!layoutNodes.size) {
+ return buildFallbackFlowGraph(graph, options);
+ }
+
+ return {
+ nodes: graph.nodes.map((stepNode) => {
+ const layoutNode = layoutNodes.get(stepNode.id);
+ const sizing = getMoleculeSizing(stepNode);
+ return {
+ id: stepNode.id,
+ type: "step",
+ position: {
+ x: layoutNode?.x ?? 0,
+ y: layoutNode?.y ?? 0,
+ },
+ sourcePosition: Position.Right,
+ targetPosition: Position.Left,
+ data: {
+ stepNode,
+ highlighted: options.matchesSearch(stepNode),
+ ...sizing,
+ onInspect: options.onInspect,
+ onEdit: options.onEdit,
+ onPartialRerun: options.onPartialRerun,
+ },
+ draggable: false,
+ };
+ }),
+ edges: toFlowEdges(graph),
+ };
+ } catch (error) {
+ console.error("ELK layout failed, using fallback placement.", error);
+ return buildFallbackFlowGraph(graph, options);
+ }
+}
diff --git a/viewer_new/src/lib/pathway.test.ts b/viewer_new/src/lib/pathway.test.ts
new file mode 100644
index 00000000..d2b3c03d
--- /dev/null
+++ b/viewer_new/src/lib/pathway.test.ts
@@ -0,0 +1,68 @@
+import { describe, expect, test } from "vitest";
+
+import {
+ buildPathwayGraph,
+ canPartialRerunStep,
+ updateStepInResult,
+ validatePathwayResult,
+} from "./pathway";
+import type { PathwayResult } from "../types/viewer";
+
+const sampleResult: PathwayResult = {
+ dependencies: {
+ 1: ["2"],
+ 2: [],
+ },
+ steps: [
+ {
+ step: "1",
+ reactants: [{ smiles: "CCO", reactant_metadata: { chemical_formula: "C2H6O" } }],
+ products: [{ smiles: "CC=O", product_metadata: { chemical_formula: "C2H4O" } }],
+ reactionmetrics: [{ confidenceestimate: 0.88, scalabilityindex: "7" }],
+ conditions: { solvent: "MeOH" },
+ },
+ {
+ step: "2",
+ reactants: [{ smiles: "O", reactant_metadata: { chemical_formula: "H2O" } }],
+ products: [{ smiles: "CCO", product_metadata: { chemical_formula: "C2H6O" } }],
+ },
+ ],
+};
+
+describe("pathway utilities", () => {
+ test("builds a graph with a virtual root", () => {
+ const graph = buildPathwayGraph(sampleResult);
+
+ expect(graph.virtualRoot.stepId).toBe("0");
+ expect(graph.nodes).toHaveLength(3);
+ expect(graph.edges.map((edge) => edge.id)).toContain("edge-0-1");
+ expect(graph.edges.map((edge) => edge.id)).toContain("edge-1-2");
+ });
+
+ test("updates a step in place without mutating other steps", () => {
+ const next = updateStepInResult(sampleResult, "2", {
+ ...sampleResult.steps[1],
+ products: [{ smiles: "CO", product_metadata: { chemical_formula: "CH4O" } }],
+ });
+
+ expect(next.steps[1].products?.[0]?.smiles).toBe("CO");
+ expect(sampleResult.steps[1].products?.[0]?.smiles).toBe("CCO");
+ });
+
+ test("validates partial rerun eligibility from product count", () => {
+ expect(canPartialRerunStep(sampleResult.steps[0]).enabled).toBe(true);
+ expect(
+ canPartialRerunStep({
+ ...sampleResult.steps[0],
+ products: [],
+ }).enabled,
+ ).toBe(false);
+ });
+
+ test("returns validation issues for malformed pathway payloads", () => {
+ const validation = validatePathwayResult({ steps: {} });
+
+ expect(validation.data).toBeUndefined();
+ expect(validation.issues.length).toBeGreaterThan(0);
+ });
+});
diff --git a/viewer_new/src/lib/pathway.ts b/viewer_new/src/lib/pathway.ts
new file mode 100644
index 00000000..c9eef714
--- /dev/null
+++ b/viewer_new/src/lib/pathway.ts
@@ -0,0 +1,293 @@
+import { ZodError } from "zod";
+
+import { pathwayResultSchema, pathwayStepSchema } from "./schemas";
+import type {
+ MoleculeMetadata,
+ MoleculeRecord,
+ NormalizedMolecule,
+ NormalizedPathwayGraph,
+ NormalizedStepNode,
+ PathwayResult,
+ PathwayStep,
+ ValidationIssue,
+} from "../types/viewer";
+
+const VIRTUAL_ROOT_STEP = "0";
+
+function getMetadata(
+ molecule: MoleculeRecord,
+ role: "product" | "reactant" | "reagent",
+): MoleculeMetadata | undefined {
+ if (role === "product") {
+ return molecule.product_metadata;
+ }
+ if (role === "reactant") {
+ return molecule.reactant_metadata;
+ }
+ return molecule.reagent_metadata;
+}
+
+function normalizeMolecules(
+ molecules: MoleculeRecord[] | undefined,
+ role: "product" | "reactant" | "reagent",
+ stepId: string,
+): NormalizedMolecule[] {
+ return (molecules ?? []).map((molecule, index) => {
+ const metadata = getMetadata(molecule, role);
+ const label = metadata?.name || metadata?.chemical_formula || molecule.smiles;
+
+ return {
+ key: `${stepId}-${role}-${index}`,
+ role,
+ smiles: molecule.smiles,
+ metadata,
+ label,
+ formula: metadata?.chemical_formula,
+ mass: metadata?.mass,
+ };
+ });
+}
+
+function normalizeDependencyMap(
+ result: PathwayResult,
+): Record {
+ const dependencyMap: Record = {};
+ for (const [parent, children] of Object.entries(result.dependencies ?? {})) {
+ dependencyMap[String(parent)] = (children ?? []).map((child) => String(child));
+ }
+
+ if (result.steps.some((step) => String(step.step) === "1")) {
+ dependencyMap[VIRTUAL_ROOT_STEP] = ["1"];
+ } else {
+ dependencyMap[VIRTUAL_ROOT_STEP] = [];
+ }
+
+ return dependencyMap;
+}
+
+function buildParentMap(
+ dependencyMap: Record,
+): Record {
+ const parentMap: Record = {};
+
+ for (const [parent, children] of Object.entries(dependencyMap)) {
+ for (const child of children) {
+ if (!parentMap[child]) {
+ parentMap[child] = [];
+ }
+ parentMap[child].push(parent);
+ }
+ }
+
+ return parentMap;
+}
+
+function createVirtualRootNode(result: PathwayResult): NormalizedStepNode {
+ const primaryProduct = result.steps[0]?.products?.[0];
+ const products = primaryProduct
+ ? normalizeMolecules([primaryProduct], "product", VIRTUAL_ROOT_STEP)
+ : [];
+
+ return {
+ id: `step-${VIRTUAL_ROOT_STEP}`,
+ stepId: VIRTUAL_ROOT_STEP,
+ title: "Target Molecule",
+ isVirtualRoot: true,
+ products,
+ reactants: [],
+ reagents: [],
+ childIds: [],
+ parentIds: [],
+ conditions: result.steps[0]?.conditions,
+ metrics: result.steps[0]?.reactionmetrics?.[0],
+ };
+}
+
+export function buildPathwayGraph(result: PathwayResult): NormalizedPathwayGraph {
+ const parsed = pathwayResultSchema.parse(result);
+ const dependencyMap = normalizeDependencyMap(parsed);
+ const parentMap = buildParentMap(dependencyMap);
+ const stepMap = Object.fromEntries(parsed.steps.map((step) => [String(step.step), step]));
+ const virtualRoot = createVirtualRootNode(parsed);
+
+ const nodes: NormalizedStepNode[] = [virtualRoot];
+ const edges: NormalizedPathwayGraph["edges"] = [];
+
+ const sortedSteps = [...parsed.steps].sort(
+ (left, right) => Number(left.step) - Number(right.step),
+ );
+
+ for (const step of sortedSteps) {
+ const stepId = String(step.step);
+ nodes.push({
+ id: `step-${stepId}`,
+ stepId,
+ title: `Step ${stepId}`,
+ isVirtualRoot: false,
+ rawStep: step,
+ products: normalizeMolecules(step.products, "product", stepId),
+ reactants: normalizeMolecules(step.reactants, "reactant", stepId),
+ reagents: normalizeMolecules(step.reagents, "reagent", stepId),
+ metrics: step.reactionmetrics?.[0],
+ conditions: step.conditions,
+ childIds: dependencyMap[stepId] ?? [],
+ parentIds: parentMap[stepId] ?? [],
+ });
+ }
+
+ nodes[0].childIds = dependencyMap[VIRTUAL_ROOT_STEP] ?? [];
+ const validNodeIds = new Set(nodes.map((node) => node.id));
+
+ for (const [parent, children] of Object.entries(dependencyMap)) {
+ for (const child of children) {
+ if (parent === child) {
+ continue;
+ }
+ const source = `step-${parent}`;
+ const target = `step-${child}`;
+ if (!validNodeIds.has(source) || !validNodeIds.has(target)) {
+ continue;
+ }
+ edges.push({
+ id: `edge-${parent}-${child}`,
+ source,
+ target,
+ });
+ }
+ }
+
+ return {
+ stepMap,
+ dependencyMap,
+ nodes,
+ edges,
+ virtualRoot,
+ };
+}
+
+export function validatePathwayResult(input: unknown) {
+ try {
+ return {
+ data: pathwayResultSchema.parse(input),
+ issues: [] as ValidationIssue[],
+ };
+ } catch (error) {
+ if (error instanceof ZodError) {
+ return {
+ data: undefined,
+ issues: error.issues.map((issue) => ({
+ path: issue.path.join("."),
+ message: issue.message,
+ })),
+ };
+ }
+ return {
+ data: undefined,
+ issues: [{ path: "", message: "Unknown validation error" }],
+ };
+ }
+}
+
+export function validatePathwayStep(input: unknown) {
+ try {
+ return {
+ data: pathwayStepSchema.parse(input),
+ issues: [] as ValidationIssue[],
+ };
+ } catch (error) {
+ if (error instanceof ZodError) {
+ return {
+ data: undefined,
+ issues: error.issues.map((issue) => ({
+ path: issue.path.join("."),
+ message: issue.message,
+ })),
+ };
+ }
+ return {
+ data: undefined,
+ issues: [{ path: "", message: "Unknown validation error" }],
+ };
+ }
+}
+
+export function getStep(result: PathwayResult | undefined, stepId: string) {
+ return result?.steps.find((step) => String(step.step) === stepId);
+}
+
+export function updateStepInResult(
+ result: PathwayResult,
+ stepId: string,
+ nextStep: PathwayStep,
+): PathwayResult {
+ return {
+ ...result,
+ steps: result.steps.map((step) =>
+ String(step.step) === stepId ? nextStep : step,
+ ),
+ };
+}
+
+export function canPartialRerunStep(step: PathwayStep | undefined) {
+ if (!step) {
+ return {
+ enabled: false,
+ reason: "Select a valid step first.",
+ };
+ }
+
+ const products = step.products ?? [];
+ if (products.length !== 1) {
+ return {
+ enabled: false,
+ reason: "Partial rerun requires exactly one product SMILES on the selected step.",
+ };
+ }
+
+ if (!products[0]?.smiles) {
+ return {
+ enabled: false,
+ reason: "The selected step is missing a product SMILES string.",
+ };
+ }
+
+ return {
+ enabled: true,
+ reason: "",
+ };
+}
+
+export function prettyValidationIssues(issues: ValidationIssue[]) {
+ return issues.map((issue) =>
+ issue.path ? `${issue.path}: ${issue.message}` : issue.message,
+ );
+}
+
+export function inferTopLevelSmiles(result: PathwayResult | undefined) {
+ return result?.steps[0]?.products?.[0]?.smiles ?? "";
+}
+
+export function formatConfidence(value: number | undefined) {
+ if (typeof value !== "number" || Number.isNaN(value)) {
+ return "n/a";
+ }
+ return `${Math.round(value * 100)}%`;
+}
+
+export function safeJsonParse(text: string) {
+ try {
+ return {
+ data: JSON.parse(text) as unknown,
+ error: "",
+ };
+ } catch (error) {
+ return {
+ data: undefined,
+ error: error instanceof Error ? error.message : "Invalid JSON",
+ };
+ }
+}
+
+export function stringifyResult(result: PathwayResult | undefined) {
+ return JSON.stringify(result ?? { steps: [], dependencies: {} }, null, 2);
+}
diff --git a/viewer_new/src/lib/rdkit.ts b/viewer_new/src/lib/rdkit.ts
new file mode 100644
index 00000000..6a449ab1
--- /dev/null
+++ b/viewer_new/src/lib/rdkit.ts
@@ -0,0 +1,51 @@
+import rdkitLoaderModule, { type RDKitLoader, type RDKitModule } from "@rdkit/rdkit";
+import rdkitWasm from "@rdkit/rdkit/dist/RDKit_minimal.wasm?url";
+
+const svgCache = new Map();
+
+let rdkitPromise: Promise | null = null;
+
+const initRDKitModule = (
+ (rdkitLoaderModule as unknown as { default?: RDKitLoader }).default ??
+ (rdkitLoaderModule as unknown as RDKitLoader)
+) as RDKitLoader;
+
+export async function getRDKit() {
+ if (!rdkitPromise) {
+ rdkitPromise = initRDKitModule({
+ locateFile: () => rdkitWasm,
+ });
+ }
+ return rdkitPromise;
+}
+
+export async function renderMoleculeSvg(
+ smiles: string,
+ width = 220,
+ height = 140,
+) {
+ const cacheKey = `${smiles}:${width}:${height}`;
+ const cached = svgCache.get(cacheKey);
+ if (cached) {
+ return cached;
+ }
+
+ const RDKit = await getRDKit();
+ const molecule = RDKit.get_mol(smiles);
+
+ if (!molecule || !molecule.is_valid()) {
+ molecule?.delete();
+ throw new Error(`Unable to render SMILES: ${smiles}`);
+ }
+
+ try {
+ if (!molecule.has_coords()) {
+ molecule.set_new_coords(true);
+ }
+ const svg = molecule.get_svg(width, height);
+ svgCache.set(cacheKey, svg);
+ return svg;
+ } finally {
+ molecule.delete();
+ }
+}
diff --git a/viewer_new/src/lib/schemas.ts b/viewer_new/src/lib/schemas.ts
new file mode 100644
index 00000000..18631718
--- /dev/null
+++ b/viewer_new/src/lib/schemas.ts
@@ -0,0 +1,96 @@
+import { z } from "zod";
+
+export const moleculeMetadataSchema = z
+ .object({
+ name: z.string().optional(),
+ chemical_formula: z.string().optional(),
+ mass: z.number().optional(),
+ smiles: z.string().optional(),
+ inchi: z.string().optional(),
+ })
+ .catchall(z.unknown());
+
+export const moleculeRecordSchema = z
+ .object({
+ smiles: z.string(),
+ reactant_metadata: moleculeMetadataSchema.optional(),
+ reagent_metadata: moleculeMetadataSchema.optional(),
+ product_metadata: moleculeMetadataSchema.optional(),
+ })
+ .catchall(z.unknown());
+
+export const reactionMetricsSchema = z
+ .object({
+ scalabilityindex: z.union([z.string(), z.number()]).optional(),
+ confidenceestimate: z.number().optional(),
+ closestliterature: z.string().optional(),
+ })
+ .catchall(z.unknown());
+
+export const pathwayStepSchema = z
+ .object({
+ step: z.union([z.string(), z.number()]).transform((value) => String(value)),
+ reactants: z.array(moleculeRecordSchema).optional(),
+ reagents: z.array(moleculeRecordSchema).optional(),
+ products: z.array(moleculeRecordSchema).optional(),
+ conditions: z.union([z.record(z.string(), z.unknown()), z.array(z.unknown())]).optional(),
+ reactionmetrics: z.array(reactionMetricsSchema).optional(),
+ })
+ .catchall(z.unknown());
+
+export const pathwayResultSchema = z
+ .object({
+ dependencies: z
+ .record(z.string(), z.array(z.union([z.string(), z.number()])))
+ .optional(),
+ steps: z.array(pathwayStepSchema),
+ })
+ .catchall(z.unknown());
+
+export const runtimeConfigSchema = z.object({
+ instances: z.array(
+ z.object({
+ id: z.string(),
+ label: z.string(),
+ baseUrl: z.string(),
+ defaults: z.object({
+ model_type: z.string(),
+ model_version: z.string(),
+ advanced_prompt: z.boolean(),
+ stability_flag: z.boolean(),
+ hallucination_check: z.boolean(),
+ use_protecting_group_feature: z.boolean().default(false),
+ }),
+ }),
+ ),
+ endpoints: z.object({
+ retrosynthesis: z.string(),
+ rerun: z.string(),
+ partialRerun: z.string(),
+ saveEdited: z.string(),
+ health: z.string(),
+ }),
+});
+
+export const advancedSettingsSchema = z.object({
+ llm_models: z.record(
+ z.string(),
+ z.object({
+ internal_name: z.string(),
+ display_name: z.string(),
+ supports_advanced_prompt: z.boolean(),
+ supports_stability_check: z.boolean(),
+ supports_hallucination_check: z.boolean(),
+ supports_protecting_group_feature: z.boolean().optional(),
+ }),
+ ),
+ az_models: z.record(z.string(), z.object({ display_name: z.string() })),
+ defaults: z.object({
+ model_type: z.string(),
+ model_version: z.string(),
+ advanced_prompt: z.boolean(),
+ stability_flag: z.boolean(),
+ hallucination_check: z.boolean(),
+ use_protecting_group_feature: z.boolean().default(false),
+ }),
+});
diff --git a/viewer_new/src/main.tsx b/viewer_new/src/main.tsx
new file mode 100644
index 00000000..5f5daad8
--- /dev/null
+++ b/viewer_new/src/main.tsx
@@ -0,0 +1,16 @@
+import React from "react";
+import ReactDOM from "react-dom/client";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+
+import { App } from "./app/App";
+import "./styles/global.css";
+
+const queryClient = new QueryClient();
+
+ReactDOM.createRoot(document.getElementById("root")!).render(
+
+
+
+
+ ,
+);
diff --git a/viewer_new/src/styles/global.css b/viewer_new/src/styles/global.css
new file mode 100644
index 00000000..26c0bbc7
--- /dev/null
+++ b/viewer_new/src/styles/global.css
@@ -0,0 +1,977 @@
+:root {
+ color-scheme: light;
+ --bg: #f3f7fb;
+ --panel: rgba(255, 255, 255, 0.88);
+ --panel-strong: #ffffff;
+ --panel-border: rgba(31, 45, 61, 0.1);
+ --text: #132033;
+ --text-muted: #5b6f86;
+ --brand: #13395b;
+ --brand-soft: #d8e6f5;
+ --accent: #ff8663;
+ --accent-soft: #ffe0d6;
+ --success: #1d8f63;
+ --warning: #c86a1b;
+ --error: #cf4b4b;
+ --shadow: 0 16px 48px rgba(19, 57, 91, 0.08);
+ --radius: 20px;
+ font-family:
+ "IBM Plex Sans",
+ "Space Grotesk",
+ "Avenir Next",
+ sans-serif;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ min-height: 100vh;
+ color: var(--text);
+ background:
+ radial-gradient(circle at top left, rgba(122, 210, 172, 0.25), transparent 28%),
+ radial-gradient(circle at top right, rgba(255, 134, 99, 0.18), transparent 22%),
+ linear-gradient(180deg, #f5f9fc 0%, #edf3f8 100%);
+}
+
+button,
+input,
+select,
+textarea {
+ font: inherit;
+}
+
+button {
+ border: 0;
+}
+
+#root {
+ min-height: 100vh;
+}
+
+.app-shell {
+ min-height: 100vh;
+ padding: 24px;
+}
+
+.boot-screen,
+.busy-overlay {
+ position: fixed;
+ inset: 0;
+ display: grid;
+ place-items: center;
+}
+
+.busy-overlay {
+ background: rgba(19, 32, 51, 0.24);
+ backdrop-filter: blur(10px);
+}
+
+.boot-card,
+.busy-card {
+ width: min(520px, calc(100vw - 32px));
+}
+
+.topbar {
+ display: grid;
+ gap: 20px;
+ padding: 24px;
+ border: 1px solid var(--panel-border);
+ border-radius: 32px;
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.94), rgba(255, 255, 255, 0.78));
+ box-shadow: var(--shadow);
+}
+
+.brand,
+.topbar__controls,
+.topbar__status,
+.panel__header,
+.section-heading,
+.instance-card__header,
+.step-node__header,
+.step-node__actions,
+.inspector-actions,
+.canvas-toolbar,
+.badge-row {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.brand-mark {
+ display: grid;
+ place-items: center;
+ width: 44px;
+ height: 44px;
+ border-radius: 14px;
+ background: linear-gradient(135deg, var(--brand), #285f8f);
+ color: white;
+}
+
+.brand h1,
+.panel h2,
+.panel h3,
+.step-node h3,
+.editor-drawer h2 {
+ margin: 0;
+}
+
+.brand p,
+.panel p,
+.instance-card p,
+.step-node p {
+ margin: 0;
+}
+
+.eyebrow {
+ color: var(--text-muted);
+ text-transform: uppercase;
+ letter-spacing: 0.18em;
+ font-size: 0.7rem;
+ font-weight: 700;
+}
+
+.topbar__controls {
+ flex-wrap: wrap;
+}
+
+.topbar__status {
+ flex-wrap: wrap;
+}
+
+.segmented-control,
+.tab-row {
+ display: inline-flex;
+ padding: 4px;
+ border-radius: 999px;
+ background: rgba(19, 57, 91, 0.07);
+}
+
+.segmented-control button,
+.tab-row button {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ padding: 10px 14px;
+ border-radius: 999px;
+ background: transparent;
+ color: var(--text-muted);
+ cursor: pointer;
+}
+
+.segmented-control button.active,
+.tab-row button.active {
+ background: white;
+ color: var(--brand);
+ box-shadow: 0 6px 16px rgba(19, 57, 91, 0.1);
+}
+
+.primary-button,
+.ghost-button,
+.instance-card__header,
+.uploaded {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ border-radius: 14px;
+ padding: 11px 16px;
+ cursor: pointer;
+ transition:
+ transform 120ms ease,
+ background 120ms ease,
+ border-color 120ms ease;
+}
+
+.primary-button {
+ color: white;
+ background: linear-gradient(135deg, var(--brand), #246194);
+}
+
+.ghost-button,
+.instance-card__header,
+.uploaded {
+ color: var(--text);
+ background: rgba(255, 255, 255, 0.72);
+ border: 1px solid rgba(19, 57, 91, 0.1);
+}
+
+.ghost-button:hover,
+.primary-button:hover,
+.instance-card__header:hover,
+.uploaded:hover {
+ transform: translateY(-1px);
+}
+
+.ghost-button:disabled,
+.primary-button:disabled {
+ cursor: not-allowed;
+ opacity: 0.45;
+ transform: none;
+}
+
+.ghost-button.danger {
+ color: var(--error);
+}
+
+.upload-trigger {
+ white-space: nowrap;
+}
+
+.api-key-field,
+.smiles-field,
+.search-field,
+.form-grid label,
+.editor-drawer label,
+.editor-molecule-row label {
+ display: grid;
+ gap: 8px;
+}
+
+.api-key-field,
+.search-field {
+ position: relative;
+}
+
+.api-key-field svg,
+.search-field svg {
+ position: absolute;
+ top: 12px;
+ left: 12px;
+ color: var(--text-muted);
+}
+
+.api-key-field input,
+.search-field input {
+ padding-left: 38px;
+}
+
+.smiles-field {
+ min-width: min(420px, 100%);
+ flex: 1;
+}
+
+input,
+select,
+textarea {
+ width: 100%;
+ padding: 12px 14px;
+ border-radius: 14px;
+ border: 1px solid rgba(19, 57, 91, 0.14);
+ background: rgba(255, 255, 255, 0.8);
+ color: var(--text);
+}
+
+textarea {
+ resize: vertical;
+}
+
+.workspace {
+ display: grid;
+ grid-template-columns: 360px minmax(0, 1fr) 380px;
+ gap: 20px;
+ margin-top: 20px;
+}
+
+.sidebar,
+.main-column,
+.inspector {
+ min-height: 0;
+}
+
+.main-column {
+ display: grid;
+ gap: 20px;
+}
+
+.panel {
+ border: 1px solid var(--panel-border);
+ border-radius: var(--radius);
+ background: var(--panel);
+ box-shadow: var(--shadow);
+ backdrop-filter: blur(10px);
+}
+
+.sidebar-panel,
+.inspector-panel,
+.upload-panel,
+.graph-panel {
+ padding: 18px;
+}
+
+.stack {
+ display: grid;
+ gap: 16px;
+}
+
+.instance-card {
+ display: grid;
+ gap: 14px;
+ padding: 16px;
+ border-radius: 20px;
+ border: 1px solid rgba(19, 57, 91, 0.1);
+ background: rgba(255, 255, 255, 0.56);
+}
+
+.instance-card.active,
+.uploaded.active {
+ border-color: rgba(255, 134, 99, 0.48);
+ box-shadow: 0 0 0 3px rgba(255, 134, 99, 0.12);
+}
+
+.instance-card__header {
+ justify-content: space-between;
+ padding: 0;
+ text-align: left;
+}
+
+.instance-card__meta,
+.toggle-group {
+ display: grid;
+ gap: 8px;
+}
+
+.form-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 12px;
+}
+
+.form-grid .full-span {
+ grid-column: 1 / -1;
+}
+
+.toggle-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ color: var(--text);
+ padding: 10px 12px;
+ border-radius: 16px;
+ background: rgba(19, 57, 91, 0.04);
+ border: 1px solid rgba(19, 57, 91, 0.08);
+ cursor: pointer;
+}
+
+.toggle-row.disabled {
+ color: var(--text-muted);
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+.toggle-row__label {
+ font-weight: 600;
+}
+
+.toggle-switch__input {
+ position: absolute;
+ opacity: 0;
+ pointer-events: none;
+}
+
+.toggle-switch {
+ position: relative;
+ width: 48px;
+ height: 28px;
+ border-radius: 999px;
+ background: rgba(19, 57, 91, 0.16);
+ transition: background 140ms ease;
+}
+
+.toggle-switch__thumb {
+ position: absolute;
+ top: 3px;
+ left: 3px;
+ width: 22px;
+ height: 22px;
+ border-radius: 50%;
+ background: white;
+ box-shadow: 0 8px 16px rgba(19, 57, 91, 0.18);
+ transition: transform 140ms ease;
+}
+
+.toggle-switch__input:checked + .toggle-switch {
+ background: linear-gradient(135deg, var(--brand), #2d79b8);
+}
+
+.toggle-switch__input:checked + .toggle-switch .toggle-switch__thumb {
+ transform: translateX(20px);
+}
+
+.toggle-switch__input:focus-visible + .toggle-switch {
+ outline: 2px solid rgba(36, 97, 148, 0.35);
+ outline-offset: 2px;
+}
+
+.status-pill,
+.health-pill,
+.badge,
+.molecule-chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ width: fit-content;
+ padding: 6px 10px;
+ border-radius: 999px;
+ font-size: 0.82rem;
+ font-weight: 600;
+}
+
+.status-pill {
+ background: rgba(19, 57, 91, 0.08);
+}
+
+.status-pill.success,
+.health-pill.healthy {
+ color: var(--success);
+ background: rgba(29, 143, 99, 0.12);
+}
+
+.status-pill.error,
+.health-pill.error,
+.health-pill.unauthorized {
+ color: var(--error);
+ background: rgba(207, 75, 75, 0.12);
+}
+
+.status-pill.dirty {
+ color: var(--warning);
+ background: rgba(200, 106, 27, 0.12);
+}
+
+.status-pill.pending {
+ color: var(--brand);
+}
+
+.badge {
+ color: var(--brand);
+ background: var(--brand-soft);
+}
+
+.badge.subtle,
+.molecule-chip {
+ color: var(--text-muted);
+ background: rgba(19, 57, 91, 0.08);
+}
+
+.molecule-chip.more {
+ color: var(--accent);
+ background: var(--accent-soft);
+}
+
+.graph-panel {
+ min-height: 68vh;
+ display: grid;
+ gap: 14px;
+}
+
+.canvas-shell {
+ position: relative;
+ min-height: 60vh;
+ border-radius: 20px;
+ overflow: hidden;
+ background:
+ linear-gradient(180deg, rgba(14, 31, 47, 0.98), rgba(20, 40, 61, 0.98));
+}
+
+.canvas-flow {
+ height: min(72vh, 880px);
+}
+
+.canvas-toolbar {
+ justify-content: space-between;
+ padding: 12px;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
+ background: rgba(9, 19, 29, 0.44);
+}
+
+.canvas-toolbar .ghost-button {
+ color: white;
+ border-color: rgba(255, 255, 255, 0.15);
+ background: rgba(255, 255, 255, 0.08);
+}
+
+.canvas-empty {
+ min-height: 60vh;
+ display: grid;
+ place-items: center;
+ text-align: center;
+ color: white;
+ border-radius: 20px;
+ background: linear-gradient(180deg, rgba(14, 31, 47, 0.98), rgba(20, 40, 61, 0.98));
+}
+
+.canvas-empty.error {
+ color: #ffd8d8;
+}
+
+.step-node {
+ position: relative;
+ display: grid;
+ place-items: center;
+ padding: 14px;
+ border-radius: 28px;
+ border: 1px solid rgba(255, 255, 255, 0.14);
+ background:
+ radial-gradient(circle at top, rgba(255, 255, 255, 0.92), rgba(244, 249, 255, 0.96)),
+ linear-gradient(180deg, rgba(250, 252, 255, 0.98), rgba(245, 248, 252, 0.96));
+ box-shadow: 0 12px 24px rgba(10, 16, 24, 0.12);
+ overflow: hidden;
+}
+
+.step-node__handle {
+ width: 1px;
+ height: 1px;
+ opacity: 0;
+ border: 0;
+ background: transparent;
+}
+
+.step-node.root {
+ background: linear-gradient(180deg, rgba(221, 250, 236, 0.98), rgba(241, 255, 248, 0.98));
+}
+
+.step-node.selected {
+ border-color: rgba(255, 134, 99, 0.58);
+ box-shadow: 0 12px 24px rgba(255, 134, 99, 0.12);
+}
+
+.step-node.muted {
+ opacity: 0.4;
+}
+
+.step-node__product {
+ display: grid;
+ width: 100%;
+ place-items: center;
+}
+
+.step-node__product.solo .molecule-svg,
+.step-node__product.solo .molecule-loading,
+.step-node__product.solo .molecule-fallback {
+ width: 100%;
+ min-height: 220px;
+}
+
+.step-node__placeholder,
+.molecule-loading,
+.molecule-fallback {
+ display: grid;
+ place-items: center;
+ min-height: 110px;
+ padding: 12px;
+ text-align: center;
+ border-radius: 16px;
+ background: rgba(19, 57, 91, 0.06);
+ color: var(--text-muted);
+}
+
+.molecule-loading.compact,
+.molecule-fallback.compact {
+ min-height: 90px;
+}
+
+.molecule-svg {
+ display: grid;
+ place-items: center;
+ overflow: hidden;
+ border-radius: 16px;
+ background: white;
+ padding: 8px;
+}
+
+.molecule-svg.compact {
+ min-height: 88px;
+}
+
+.molecule-svg svg {
+ display: block;
+ width: auto;
+ height: auto;
+ max-width: 100%;
+ max-height: 100%;
+ shape-rendering: geometricPrecision;
+ text-rendering: geometricPrecision;
+}
+
+.molecule-chip-group {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.step-node__overlay {
+ position: absolute;
+ inset: 0;
+ display: grid;
+ grid-template-rows: auto auto 1fr;
+ align-content: start;
+ gap: 16px;
+ padding: 18px;
+ background:
+ radial-gradient(circle at top right, rgba(122, 210, 172, 0.22), transparent 34%),
+ radial-gradient(circle at bottom left, rgba(255, 134, 99, 0.18), transparent 32%),
+ linear-gradient(180deg, rgba(18, 47, 79, 0.86), rgba(12, 33, 56, 0.9));
+ color: #f8fbff;
+ opacity: 0;
+ transform: translateY(8px);
+ transition:
+ opacity 140ms ease,
+ transform 140ms ease;
+}
+
+.step-node:hover .step-node__overlay,
+.step-node:focus-visible .step-node__overlay,
+.step-node:focus-within .step-node__overlay {
+ opacity: 1;
+ transform: translateY(0);
+}
+
+.step-node__overlay-header {
+ display: grid;
+ gap: 10px;
+}
+
+.step-node__overlay .badge-row {
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.step-node__overlay .eyebrow,
+.step-node__overlay .badge,
+.step-node__overlay .badge.subtle,
+.step-node__overlay .molecule-chip {
+ color: white;
+}
+
+.step-node__overlay .badge {
+ background: rgba(255, 255, 255, 0.16);
+}
+
+.step-node__overlay .badge.subtle,
+.step-node__overlay .molecule-chip {
+ background: rgba(255, 255, 255, 0.1);
+}
+
+.step-node__overlay-meta {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+}
+
+.step-node__actions {
+ align-self: end;
+ justify-content: flex-start;
+ flex-wrap: wrap;
+ gap: 12px;
+ margin-top: 8px;
+}
+
+.step-node__actions .ghost-button {
+ min-height: 40px;
+ padding: 10px 14px;
+ color: #f8fbff;
+ background: rgba(255, 255, 255, 0.12);
+ border-color: rgba(255, 255, 255, 0.24);
+ box-shadow: none;
+}
+
+.step-node__actions .ghost-button:hover {
+ background: rgba(255, 255, 255, 0.18);
+}
+
+.react-flow__edges {
+ z-index: 2;
+}
+
+.react-flow__edge-path {
+ stroke: white;
+ stroke-width: 4;
+ stroke-opacity: 1;
+}
+
+.react-flow__arrowhead path,
+.react-flow__marker path {
+ fill: white;
+ stroke: white;
+}
+
+.inspector {
+ display: grid;
+}
+
+.inspector-panel {
+ height: calc(100vh - 180px);
+ overflow: auto;
+}
+
+.inspector-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 12px;
+}
+
+.metric-card {
+ display: grid;
+ gap: 8px;
+ padding: 14px;
+ border-radius: 16px;
+ background: rgba(19, 57, 91, 0.06);
+}
+
+.metric-card span {
+ color: var(--text-muted);
+ font-size: 0.82rem;
+}
+
+.inspector-section,
+.editor-section {
+ display: grid;
+ gap: 12px;
+ margin-top: 18px;
+}
+
+.inspector-molecules,
+.editor-molecule-list,
+.editor-condition-list {
+ display: grid;
+ gap: 12px;
+}
+
+.inspector-molecule,
+.editor-molecule-row,
+.editor-condition-row {
+ display: grid;
+ gap: 12px;
+ padding: 14px;
+ border-radius: 16px;
+ background: rgba(255, 255, 255, 0.72);
+ border: 1px solid rgba(19, 57, 91, 0.08);
+}
+
+.editor-condition-row {
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto;
+ align-items: end;
+}
+
+.empty-note {
+ padding: 14px;
+ border-radius: 16px;
+ background: rgba(19, 57, 91, 0.04);
+ color: var(--text-muted);
+}
+
+.inspector-molecule p {
+ overflow-wrap: anywhere;
+}
+
+.conditions-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 12px;
+}
+
+.condition-card {
+ display: grid;
+ gap: 8px;
+ padding: 14px;
+ border-radius: 16px;
+ background: rgba(19, 57, 91, 0.06);
+}
+
+.condition-card span {
+ color: var(--text-muted);
+ font-size: 0.82rem;
+ text-transform: capitalize;
+}
+
+.condition-card strong {
+ overflow-wrap: anywhere;
+}
+
+.action-chip {
+ cursor: pointer;
+ border: 1px solid transparent;
+}
+
+.action-chip.active {
+ color: var(--accent);
+ border-color: rgba(255, 134, 99, 0.4);
+ background: rgba(255, 224, 214, 0.92);
+}
+
+.json-details {
+ margin-top: 18px;
+ border: 1px solid rgba(19, 57, 91, 0.08);
+ border-radius: 16px;
+ background: rgba(255, 255, 255, 0.48);
+}
+
+.json-details summary {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ padding: 14px 16px;
+ cursor: pointer;
+ list-style: none;
+ font-weight: 600;
+}
+
+.json-details summary::-webkit-details-marker {
+ display: none;
+}
+
+.json-details[open] summary svg {
+ transform: rotate(180deg);
+}
+
+.json-details pre {
+ margin: 0 14px 14px;
+}
+
+.json-block {
+ overflow: auto;
+ padding: 12px;
+ border-radius: 16px;
+ background: #0e1f2f;
+ color: #dbe6f2;
+ font-family:
+ "SFMono-Regular",
+ "IBM Plex Mono",
+ monospace;
+ font-size: 0.84rem;
+ line-height: 1.5;
+}
+
+.json-block.tall {
+ max-height: 260px;
+}
+
+.upload-dropzone {
+ display: grid;
+ place-items: center;
+ gap: 10px;
+ min-height: 180px;
+ margin-top: 16px;
+ border: 1px dashed rgba(19, 57, 91, 0.2);
+ border-radius: 20px;
+ background:
+ linear-gradient(180deg, rgba(255, 255, 255, 0.6), rgba(241, 247, 253, 0.72));
+ text-align: center;
+ cursor: pointer;
+}
+
+.editor-drawer__backdrop {
+ position: fixed;
+ inset: 0;
+ z-index: 40;
+ display: flex;
+ justify-content: flex-end;
+ background: rgba(9, 19, 29, 0.32);
+ backdrop-filter: blur(10px);
+}
+
+.editor-drawer {
+ width: min(760px, 100vw);
+ height: 100vh;
+ padding: 20px;
+ overflow: auto;
+ background: #f7fbff;
+ box-shadow: -20px 0 40px rgba(9, 19, 29, 0.2);
+}
+
+.editor-drawer__content {
+ display: grid;
+ gap: 16px;
+ margin-top: 16px;
+}
+
+.editor-drawer__footer {
+ display: flex;
+ justify-content: flex-end;
+}
+
+.inline-error,
+.inline-warning {
+ padding: 12px 14px;
+ border-radius: 14px;
+ font-size: 0.92rem;
+}
+
+.inline-error {
+ color: var(--error);
+ background: rgba(207, 75, 75, 0.12);
+}
+
+.inline-warning {
+ color: var(--warning);
+ background: rgba(200, 106, 27, 0.12);
+}
+
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+}
+
+.spin {
+ animation: spin 0.9s linear infinite;
+}
+
+@keyframes spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (max-width: 1360px) {
+ .workspace {
+ grid-template-columns: 320px minmax(0, 1fr);
+ }
+
+ .inspector {
+ grid-column: 1 / -1;
+ }
+
+ .inspector-panel {
+ height: auto;
+ }
+}
+
+@media (max-width: 960px) {
+ .app-shell {
+ padding: 16px;
+ }
+
+ .workspace {
+ grid-template-columns: 1fr;
+ }
+
+ .topbar {
+ padding: 18px;
+ }
+
+ .graph-panel {
+ min-height: 56vh;
+ }
+
+ .step-node__product {
+ grid-template-columns: 1fr;
+ }
+
+ .form-grid,
+ .inspector-grid,
+ .conditions-grid {
+ grid-template-columns: 1fr;
+ }
+}
diff --git a/viewer_new/src/test/setup.ts b/viewer_new/src/test/setup.ts
new file mode 100644
index 00000000..f149f27a
--- /dev/null
+++ b/viewer_new/src/test/setup.ts
@@ -0,0 +1 @@
+import "@testing-library/jest-dom/vitest";
diff --git a/viewer_new/src/types/viewer.ts b/viewer_new/src/types/viewer.ts
new file mode 100644
index 00000000..f95eeaa9
--- /dev/null
+++ b/viewer_new/src/types/viewer.ts
@@ -0,0 +1,173 @@
+export type ToggleableModelFlags = {
+ advanced_prompt: boolean;
+ stability_flag: boolean;
+ hallucination_check: boolean;
+ use_protecting_group_feature: boolean;
+};
+
+export type InstanceDefaults = ToggleableModelFlags & {
+ model_type: string;
+ model_version: string;
+};
+
+export type ViewerRuntimeConfig = {
+ instances: Array<{
+ id: string;
+ label: string;
+ baseUrl: string;
+ defaults: InstanceDefaults;
+ }>;
+ endpoints: {
+ retrosynthesis: string;
+ rerun: string;
+ partialRerun: string;
+ saveEdited: string;
+ health: string;
+ };
+};
+
+export type AdvancedSettingsConfig = {
+ llm_models: Record<
+ string,
+ {
+ internal_name: string;
+ display_name: string;
+ supports_advanced_prompt: boolean;
+ supports_stability_check: boolean;
+ supports_hallucination_check: boolean;
+ supports_protecting_group_feature?: boolean;
+ }
+ >;
+ az_models: Record;
+ defaults: InstanceDefaults;
+};
+
+export type MoleculeMetadata = {
+ name?: string;
+ chemical_formula?: string;
+ mass?: number;
+ smiles?: string;
+ inchi?: string;
+ [key: string]: unknown;
+};
+
+export type MoleculeRecord = {
+ smiles: string;
+ reactant_metadata?: MoleculeMetadata;
+ reagent_metadata?: MoleculeMetadata;
+ product_metadata?: MoleculeMetadata;
+ [key: string]: unknown;
+};
+
+export type ReactionMetrics = {
+ scalabilityindex?: string | number;
+ confidenceestimate?: number;
+ closestliterature?: string;
+ [key: string]: unknown;
+};
+
+export type PathwayStep = {
+ step: string;
+ reactants?: MoleculeRecord[];
+ reagents?: MoleculeRecord[];
+ products?: MoleculeRecord[];
+ conditions?: Record | unknown[];
+ reactionmetrics?: ReactionMetrics[];
+ [key: string]: unknown;
+};
+
+export type PathwayResult = {
+ dependencies?: Record>;
+ steps: PathwayStep[];
+ [key: string]: unknown;
+};
+
+export type InstanceRequestSettings = InstanceDefaults;
+
+export type AnalysisRequest = InstanceRequestSettings & {
+ smiles: string;
+};
+
+export type ViewerMode = "analyze" | "upload";
+export type ViewerRunSource = "api" | "file";
+export type ViewerRunStatus = "idle" | "pending" | "success" | "error";
+export type HealthState = "unknown" | "healthy" | "unauthorized" | "error";
+
+export type ValidationIssue = {
+ path: string;
+ message: string;
+};
+
+export type NormalizedMolecule = {
+ key: string;
+ role: "product" | "reactant" | "reagent";
+ smiles: string;
+ metadata?: MoleculeMetadata;
+ label: string;
+ formula?: string;
+ mass?: number;
+};
+
+export type NormalizedStepNode = {
+ id: string;
+ stepId: string;
+ title: string;
+ isVirtualRoot: boolean;
+ rawStep?: PathwayStep;
+ products: NormalizedMolecule[];
+ reactants: NormalizedMolecule[];
+ reagents: NormalizedMolecule[];
+ metrics?: ReactionMetrics;
+ conditions?: Record | unknown[];
+ childIds: string[];
+ parentIds: string[];
+};
+
+export type NormalizedPathwayGraph = {
+ stepMap: Record;
+ dependencyMap: Record;
+ nodes: NormalizedStepNode[];
+ edges: Array<{
+ id: string;
+ source: string;
+ target: string;
+ }>;
+ virtualRoot: NormalizedStepNode;
+};
+
+export type ViewerRun = {
+ key: string;
+ label: string;
+ source: ViewerRunSource;
+ status: ViewerRunStatus;
+ instanceId?: string;
+ request?: AnalysisRequest;
+ rawResult?: PathwayResult;
+ graph?: NormalizedPathwayGraph;
+ error?: string;
+ durationMs?: number;
+ dirty: boolean;
+ lastUpdatedAt: number;
+};
+
+export type HealthStatusMap = Record<
+ string,
+ {
+ state: HealthState;
+ message: string;
+ }
+>;
+
+export type ViewerStoreState = {
+ runtimeConfig?: ViewerRuntimeConfig;
+ advancedSettings?: AdvancedSettingsConfig;
+ mode: ViewerMode;
+ apiKey: string;
+ currentSmiles: string;
+ activeRunKey?: string;
+ selectedStepId?: string;
+ runs: Record;
+ instanceSettings: Record;
+ health: HealthStatusMap;
+ editorOpen: boolean;
+};
diff --git a/viewer_new/tsconfig.app.json b/viewer_new/tsconfig.app.json
new file mode 100644
index 00000000..a9b5a59c
--- /dev/null
+++ b/viewer_new/tsconfig.app.json
@@ -0,0 +1,28 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "types": ["vite/client"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["src"]
+}
diff --git a/viewer_new/tsconfig.json b/viewer_new/tsconfig.json
new file mode 100644
index 00000000..1ffef600
--- /dev/null
+++ b/viewer_new/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/viewer_new/tsconfig.node.json b/viewer_new/tsconfig.node.json
new file mode 100644
index 00000000..8a67f62f
--- /dev/null
+++ b/viewer_new/tsconfig.node.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "ES2023",
+ "lib": ["ES2023"],
+ "module": "ESNext",
+ "types": ["node"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/viewer_new/vite.config.ts b/viewer_new/vite.config.ts
new file mode 100644
index 00000000..bbfb6a83
--- /dev/null
+++ b/viewer_new/vite.config.ts
@@ -0,0 +1,12 @@
+import react from "@vitejs/plugin-react";
+import { defineConfig } from "vitest/config";
+
+export default defineConfig({
+ plugins: [react()],
+ test: {
+ environment: "jsdom",
+ globals: true,
+ setupFiles: "./src/test/setup.ts",
+ css: true,
+ },
+});