diff --git a/.gitignore b/.gitignore index feae4364..befc8b3b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,31 +1,24 @@ -*/__pycache__ -__pycache__ -*.so - -hosts.json -hosts*.json -nodes.json - -# hide direnv stuff -.direnv/ - -build/ -dist/ - -*.xcuserstate -.DS_Store -*/.DS_Store - -# for the gitingest enthusiasts +# gitingest digest.txt -# Rust +# python +**/__pycache__ + +# nix +.direnv/ + + +# xcode / macos +*.xcuserstate +**/.DS_Store + + +# rust target/ -## These are backup files generated by rustfmt **/*.rs.bk -## MSVC Windows builds of rustc generate these, which store debugging information *.pdb -## Generated by cargo mutants -## Contains mutation testing data -**/mutants.out*/ +# svelte +dashboard/build/ +dashboard/node_modules/ +dashboard/.svelte-kit/ diff --git a/dashboard/index.html b/dashboard/index.html deleted file mode 100644 index 896b79a9..00000000 --- a/dashboard/index.html +++ /dev/null @@ -1,3343 +0,0 @@ - - - - - - EXO - - - - - - - - - -
-
- -
-
-

EXO logo

-

Fetching data...

-
-
- -
-
- - - - -
- × -

Node Details

-
-
- -
-
- - - - \ No newline at end of file diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json new file mode 100644 index 00000000..e075d621 --- /dev/null +++ b/dashboard/package-lock.json @@ -0,0 +1,3058 @@ +{ + "name": "exo-dashboard", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "exo-dashboard", + "version": "1.0.0", + "dependencies": { + "highlight.js": "^11.11.1", + "mode-watcher": "^1.1.0" + }, + "devDependencies": { + "@sveltejs/adapter-static": "^3.0.10", + "@sveltejs/kit": "^2.48.4", + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "@tailwindcss/vite": "^4.0.0", + "@types/d3": "^7.4.3", + "@types/node": "^22", + "d3": "^7.9.0", + "svelte": "^5.0.0", + "svelte-check": "^4.0.0", + "tailwindcss": "^4.0.0", + "tw-animate-css": "^1.3.5", + "typescript": "^5.0.0", + "vite": "^6.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.8.tgz", + "integrity": "sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/adapter-static": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.10.tgz", + "integrity": "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@sveltejs/kit": "^2.0.0" + } + }, + "node_modules/@sveltejs/kit": { + "version": "2.49.0", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.49.0.tgz", + "integrity": "sha512-oH8tXw7EZnie8FdOWYrF7Yn4IKrqTFHhXvl8YxXxbKwTMcD/5NNCryUSEXRk2ZR4ojnub0P8rNrsVGHXWqIDtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/cookie": "^0.6.0", + "acorn": "^8.14.1", + "cookie": "^0.6.0", + "devalue": "^5.3.2", + "esm-env": "^1.2.2", + "kleur": "^4.1.5", + "magic-string": "^0.30.5", + "mrmime": "^2.0.0", + "sade": "^1.8.1", + "set-cookie-parser": "^2.6.0", + "sirv": "^3.0.0" + }, + "bin": { + "svelte-kit": "svelte-kit.js" + }, + "engines": { + "node": ">=18.13" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", + "svelte": "^4.0.0 || ^5.0.0-next.0", + "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + } + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.1.1.tgz", + "integrity": "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", + "debug": "^4.4.1", + "deepmerge": "^4.3.1", + "kleur": "^4.1.5", + "magic-string": "^0.30.17", + "vitefu": "^1.0.6" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "svelte": "^5.0.0", + "vite": "^6.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz", + "integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.7" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "svelte": "^5.0.0", + "vite": "^6.0.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.17.tgz", + "integrity": "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.17" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.17.tgz", + "integrity": "sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.17", + "@tailwindcss/oxide-darwin-arm64": "4.1.17", + "@tailwindcss/oxide-darwin-x64": "4.1.17", + "@tailwindcss/oxide-freebsd-x64": "4.1.17", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.17", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.17", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.17", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.17", + "@tailwindcss/oxide-linux-x64-musl": "4.1.17", + "@tailwindcss/oxide-wasm32-wasi": "4.1.17", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.17", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.17" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.17.tgz", + "integrity": "sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.17.tgz", + "integrity": "sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.17.tgz", + "integrity": "sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.17.tgz", + "integrity": "sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.17.tgz", + "integrity": "sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.17.tgz", + "integrity": "sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.17.tgz", + "integrity": "sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.17.tgz", + "integrity": "sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.17.tgz", + "integrity": "sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.17.tgz", + "integrity": "sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.6.0", + "@emnapi/runtime": "^1.6.0", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.0.7", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.17.tgz", + "integrity": "sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.17.tgz", + "integrity": "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.17.tgz", + "integrity": "sha512-4+9w8ZHOiGnpcGI6z1TVVfWaX/koK7fKeSYF3qlYg2xpBtbteP2ddBxiarL+HVgfSJGeK5RIxRQmKm4rTJJAwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.17", + "@tailwindcss/oxide": "4.1.17", + "tailwindcss": "4.1.17" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz", + "integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dev": true, + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "dev": true, + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.5.0.tgz", + "integrity": "sha512-69sM5yrHfFLJt0AZ9QqZXGCPfJ7fQjvpln3Rq5+PS03LD32Ost1Q9N+eEnaQwGRIriKkMImXD56ocjQmfjbV3w==", + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "license": "MIT" + }, + "node_modules/esrap": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.1.tgz", + "integrity": "sha512-GiYWG34AN/4CUyaWAgunGt0Rxvr1PTMlGC0vvEov/uOQYWne2bpN03Um+k8jT+q3op33mKouP2zeJ6OlM+qeUg==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mode-watcher": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mode-watcher/-/mode-watcher-1.1.0.tgz", + "integrity": "sha512-mUT9RRGPDYenk59qJauN1rhsIMKBmWA3xMF+uRwE8MW/tjhaDSCCARqkSuDTq8vr4/2KcAxIGVjACxTjdk5C3g==", + "license": "MIT", + "dependencies": { + "runed": "^0.25.0", + "svelte-toolbelt": "^0.7.1" + }, + "peerDependencies": { + "svelte": "^5.27.0" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/runed": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/runed/-/runed-0.25.0.tgz", + "integrity": "sha512-7+ma4AG9FT2sWQEA0Egf6mb7PBT2vHyuHail1ie8ropfSjvZGtEAx8YTmUjv/APCsdRRxEVvArNjALk9zFSOrg==", + "funding": [ + "https://github.com/sponsors/huntabyte", + "https://github.com/sponsors/tglide" + ], + "dependencies": { + "esm-env": "^1.0.0" + }, + "peerDependencies": { + "svelte": "^5.7.0" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "dev": true, + "license": "MIT" + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, + "node_modules/svelte": { + "version": "5.45.3", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.45.3.tgz", + "integrity": "sha512-ngKXNhNvwPzF43QqEhDOue7TQTrG09em1sd4HBxVF0Wr2gopAmdEWan+rgbdgK4fhBtSOTJO8bYU4chUG7VXZQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/estree": "^1.0.5", + "acorn": "^8.12.1", + "aria-query": "^5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "devalue": "^5.5.0", + "esm-env": "^1.2.1", + "esrap": "^2.2.0", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/svelte-check": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.4.tgz", + "integrity": "sha512-DVWvxhBrDsd+0hHWKfjP99lsSXASeOhHJYyuKOFYJcP7ThfSCKgjVarE8XfuMWpS5JV3AlDf+iK1YGGo2TACdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "chokidar": "^4.0.1", + "fdir": "^6.2.0", + "picocolors": "^1.0.0", + "sade": "^1.7.4" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": ">=5.0.0" + } + }, + "node_modules/svelte-toolbelt": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.7.1.tgz", + "integrity": "sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ==", + "funding": [ + "https://github.com/sponsors/huntabyte" + ], + "dependencies": { + "clsx": "^2.1.1", + "runed": "^0.23.2", + "style-to-object": "^1.0.8" + }, + "engines": { + "node": ">=18", + "pnpm": ">=8.7.0" + }, + "peerDependencies": { + "svelte": "^5.0.0" + } + }, + "node_modules/svelte-toolbelt/node_modules/runed": { + "version": "0.23.4", + "resolved": "https://registry.npmjs.org/runed/-/runed-0.23.4.tgz", + "integrity": "sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA==", + "funding": [ + "https://github.com/sponsors/huntabyte", + "https://github.com/sponsors/tglide" + ], + "dependencies": { + "esm-env": "^1.0.0" + }, + "peerDependencies": { + "svelte": "^5.7.0" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", + "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tw-animate-css": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", + "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz", + "integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==", + "dev": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/zimmerframe": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", + "license": "MIT" + } + } +} diff --git a/dashboard/package.json b/dashboard/package.json new file mode 100644 index 00000000..c9c27630 --- /dev/null +++ b/dashboard/package.json @@ -0,0 +1,33 @@ +{ + "name": "exo-dashboard", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json" + }, + "devDependencies": { + "@sveltejs/adapter-static": "^3.0.10", + "@sveltejs/kit": "^2.48.4", + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "@tailwindcss/vite": "^4.0.0", + "@types/d3": "^7.4.3", + "@types/node": "^22", + "d3": "^7.9.0", + "svelte": "^5.0.0", + "svelte-check": "^4.0.0", + "tailwindcss": "^4.0.0", + "tw-animate-css": "^1.3.5", + "typescript": "^5.0.0", + "vite": "^6.0.0" + }, + "dependencies": { + "highlight.js": "^11.11.1", + "mode-watcher": "^1.1.0" + } +} + diff --git a/dashboard/src/app.css b/dashboard/src/app.css new file mode 100644 index 00000000..fc532578 --- /dev/null +++ b/dashboard/src/app.css @@ -0,0 +1,322 @@ +@import 'tailwindcss'; +@import 'tw-animate-css'; + +@custom-variant dark (&:is(.dark *)); + +:root { + /* EXO Brand Colors - Command Center Theme (neutral dark greys) */ + --exo-black: oklch(0.12 0 0); + --exo-dark-gray: oklch(0.16 0 0); + --exo-medium-gray: oklch(0.22 0 0); + --exo-light-gray: oklch(0.6 0 0); + --exo-yellow: oklch(0.85 0.18 85); + --exo-yellow-darker: oklch(0.7 0.16 85); + --exo-yellow-glow: oklch(0.9 0.2 85); + + /* Gotham-inspired accent colors */ + --exo-grid: oklch(0.25 0 0); + --exo-scanline: oklch(0.15 0 0); + --exo-glow-yellow: 0 0 20px oklch(0.85 0.18 85 / 0.3); + --exo-glow-yellow-strong: 0 0 40px oklch(0.85 0.18 85 / 0.5); + + /* Theme Variables */ + --radius: 0.375rem; + --background: var(--exo-black); + --foreground: oklch(0.9 0 0); + --card: var(--exo-dark-gray); + --card-foreground: oklch(0.9 0 0); + --popover: var(--exo-dark-gray); + --popover-foreground: oklch(0.9 0 0); + --primary: var(--exo-yellow); + --primary-foreground: var(--exo-black); + --secondary: var(--exo-medium-gray); + --secondary-foreground: oklch(0.9 0 0); + --muted: var(--exo-medium-gray); + --muted-foreground: var(--exo-light-gray); + --accent: var(--exo-medium-gray); + --accent-foreground: oklch(0.9 0 0); + --destructive: oklch(0.6 0.25 25); + --border: oklch(0.22 0 0); + --input: oklch(0.22 0 0); + --ring: var(--exo-yellow); +} + +@theme inline { + --radius-sm: calc(var(--radius) - 2px); + --radius-md: var(--radius); + --radius-lg: calc(var(--radius) + 2px); + --radius-xl: calc(var(--radius) + 4px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + + /* Custom EXO colors */ + --color-exo-yellow: var(--exo-yellow); + --color-exo-yellow-darker: var(--exo-yellow-darker); + --color-exo-black: var(--exo-black); + --color-exo-dark-gray: var(--exo-dark-gray); + --color-exo-medium-gray: var(--exo-medium-gray); + --color-exo-light-gray: var(--exo-light-gray); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + html, body { + @apply bg-background text-foreground; + font-family: 'SF Mono', 'Fira Code', 'Monaco', 'Consolas', 'Liberation Mono', monospace; + letter-spacing: 0.02em; + } +} + +@layer utilities { + .scrollbar-hide { + &::-webkit-scrollbar { + display: none; + } + -ms-overflow-style: none; + scrollbar-width: none; + } + + /* CRT Scanline effect */ + .scanlines { + position: relative; + &::before { + content: ''; + position: absolute; + inset: 0; + background: repeating-linear-gradient( + 0deg, + transparent, + transparent 2px, + oklch(0 0 0 / 0.03) 2px, + oklch(0 0 0 / 0.03) 4px + ); + pointer-events: none; + z-index: 100; + } + } + + /* Command panel styling */ + .command-panel { + background: linear-gradient( + 180deg, + oklch(0.16 0 0 / 0.95) 0%, + oklch(0.12 0 0 / 0.98) 100% + ); + border: 1px solid oklch(0.25 0 0); + box-shadow: + inset 0 1px 0 oklch(1 0 0 / 0.03), + 0 4px 20px oklch(0 0 0 / 0.5); + } + + /* Glow text */ + .glow-text { + text-shadow: + 0 0 10px oklch(0.85 0.18 85 / 0.5), + 0 0 20px oklch(0.85 0.18 85 / 0.3), + 0 0 40px oklch(0.85 0.18 85 / 0.1); + } + + /* Status indicator pulse */ + .status-pulse { + animation: statusPulse 2s ease-in-out infinite; + } + + /* Grid background */ + .grid-bg { + background-image: + linear-gradient(oklch(0.2 0 0 / 0.3) 1px, transparent 1px), + linear-gradient(90deg, oklch(0.2 0 0 / 0.3) 1px, transparent 1px); + background-size: 40px 40px; + } +} + +/* Animations */ +@keyframes flowAnimation { + from { + stroke-dashoffset: 0; + } + to { + stroke-dashoffset: -16; + } +} + +@keyframes statusPulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +} + +@keyframes radarSweep { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@keyframes glowPulse { + 0%, 100% { + box-shadow: 0 0 5px oklch(0.85 0.18 85 / 0.3), 0 0 10px oklch(0.85 0.18 85 / 0.1); + } + 50% { + box-shadow: 0 0 15px oklch(0.85 0.18 85 / 0.5), 0 0 30px oklch(0.85 0.18 85 / 0.2); + } +} + +@keyframes dataPulse { + 0%, 100% { + opacity: 0.6; + } + 50% { + opacity: 1; + } +} + +.graph-link { + stroke: oklch(0.85 0.18 85 / 0.4); + stroke-width: 1.5px; + stroke-dasharray: 8, 8; + animation: flowAnimation 1s linear infinite; + filter: drop-shadow(0 0 3px oklch(0.85 0.18 85 / 0.5)); +} + +.graph-link-active { + stroke: oklch(0.85 0.18 85 / 0.8); + stroke-width: 2px; + filter: drop-shadow(0 0 6px oklch(0.85 0.18 85 / 0.8)); +} + +/* CRT Screen effect for topology */ +.crt-screen { + position: relative; + border-radius: 50%; + background: radial-gradient( + ellipse at center, + oklch(0.16 0 0) 0%, + oklch(0.12 0 0) 50%, + oklch(0.09 0 0) 100% + ); + box-shadow: + inset 0 0 100px oklch(0 0 0 / 0.5), + 0 0 50px oklch(0.85 0.18 85 / 0.1); +} + +/* Data readout styling */ +.data-readout { + font-family: 'SF Mono', 'Fira Code', monospace; + font-size: 11px; + letter-spacing: 0.05em; + text-transform: uppercase; +} + +/* Terminal cursor blink */ +.cursor-blink { + animation: cursorBlink 1s step-end infinite; +} + +@keyframes cursorBlink { + 0%, 100% { opacity: 1; } + 50% { opacity: 0; } +} + +/* Custom scrollbar for command center */ +::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +::-webkit-scrollbar-track { + background: oklch(0.1 0 0); +} + +::-webkit-scrollbar-thumb { + background: oklch(0.3 0 0); + border-radius: 3px; +} + +::-webkit-scrollbar-thumb:hover { + background: oklch(0.85 0.18 85 / 0.5); +} + +/* Remove focus outline/border for inputs */ +input:focus, textarea:focus { + outline: none; + box-shadow: none; +} + +/* Shooting Stars Animation */ +.shooting-stars { + position: fixed; + inset: 0; + overflow: hidden; + pointer-events: none; + z-index: 0; +} + +.shooting-star { + position: absolute; + width: 3px; + height: 3px; + background: oklch(0.85 0.18 85 / 1); + border-radius: 50%; + box-shadow: 0 0 6px oklch(0.85 0.18 85 / 0.8); + animation: shootingStar var(--duration, 3s) linear infinite; + animation-delay: var(--delay, 0s); + opacity: 0; +} + +.shooting-star::before { + content: ''; + position: absolute; + width: 80px; + height: 2px; + background: linear-gradient(90deg, oklch(0.85 0.18 85 / 0), oklch(0.85 0.18 85 / 0.6)); + transform: rotate(45deg); + transform-origin: right center; + top: 0; + right: 2px; +} + +@keyframes shootingStar { + 0% { + opacity: 0; + transform: translate(0, 0); + } + 0.5% { + opacity: 1; + } + 2.5% { + opacity: 0.8; + transform: translate(300px, 300px); + } + 3.5% { + opacity: 0; + transform: translate(400px, 400px); + } + 100% { + opacity: 0; + transform: translate(400px, 400px); + } +} diff --git a/dashboard/src/app.d.ts b/dashboard/src/app.d.ts new file mode 100644 index 00000000..b111beb0 --- /dev/null +++ b/dashboard/src/app.d.ts @@ -0,0 +1,14 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; + diff --git a/dashboard/src/app.html b/dashboard/src/app.html new file mode 100644 index 00000000..a974a968 --- /dev/null +++ b/dashboard/src/app.html @@ -0,0 +1,14 @@ + + + + + + + EXO + %sveltekit.head% + + +
%sveltekit.body%
+ + + diff --git a/dashboard/src/lib/components/ChatAttachments.svelte b/dashboard/src/lib/components/ChatAttachments.svelte new file mode 100644 index 00000000..f56e23e3 --- /dev/null +++ b/dashboard/src/lib/components/ChatAttachments.svelte @@ -0,0 +1,75 @@ + + +{#if files.length > 0} +
+ {#each files as file (file.id)} +
+ + {#if file.preview && getFileCategory(file.type, file.name) === 'image'} + {file.name} + {:else} + {getFileIcon(file)} + {/if} + + +
+ + {truncateName(file.name)} + + + {formatFileSize(file.size)} + +
+ + + {#if !readonly && onRemove} + + {/if} +
+ {/each} +
+{/if} + diff --git a/dashboard/src/lib/components/ChatForm.svelte b/dashboard/src/lib/components/ChatForm.svelte new file mode 100644 index 00000000..95d023c3 --- /dev/null +++ b/dashboard/src/lib/components/ChatForm.svelte @@ -0,0 +1,398 @@ + + + + + +
{ e.preventDefault(); handleSubmit(); }} + class="w-full {className}" + ondragover={handleDragOver} + ondragleave={handleDragLeave} + ondrop={handleDrop} +> +
+ +
+ + + {#if isDragOver} +
+
+ DROP FILES HERE +
+
+ {/if} + + + {#if showModelSelector && availableModels().length > 0} +
+
+ MODEL: + +
+ +
+ + + +
+
+ + {#if isModelDropdownOpen} + + + + +
+
+ {#each availableModels() as model} + + {/each} +
+
+ {/if} +
+ + {#if currentTtft !== null || currentTps !== null} +
+ {#if currentTtft !== null} + + TTFT {currentTtft.toFixed(1)}ms + + {/if} + {#if currentTps !== null} + + TPS {currentTps.toFixed(1)} tok/s + ({(1000 / currentTps).toFixed(1)} ms/tok) + + {/if} +
+ {/if} +
+ {/if} + + + {#if uploadedFiles.length > 0} +
+ +
+ {/if} + + +
+ + + + + + + + + +
+ + +
+
+ + {#if showHelperText} +

+ ENTER + TO SEND + | + SHIFT+ENTER + NEW LINE + | + DRAG & DROP OR PASTE FILES +

+ {/if} +
diff --git a/dashboard/src/lib/components/ChatMessages.svelte b/dashboard/src/lib/components/ChatMessages.svelte new file mode 100644 index 00000000..baaf43f7 --- /dev/null +++ b/dashboard/src/lib/components/ChatMessages.svelte @@ -0,0 +1,462 @@ + + +
+ {#each messageList as message (message.id)} +
+
+ {#if message.role === 'assistant'} + +
+
+ EXO + {formatTimestamp(message.timestamp)} + {#if message.ttftMs || message.tps} + + {#if message.ttftMs}TTFT {message.ttftMs.toFixed(0)}ms{/if}{#if message.ttftMs && message.tps}{/if}{#if message.tps}{message.tps.toFixed(1)} tok/s{/if} + + {/if} +
+ {:else} + +
+ {formatTimestamp(message.timestamp)} + QUERY +
+
+ {/if} + + {#if deleteConfirmId === message.id} + +
+

Delete this message{message.role === 'user' ? ' and all responses after it' : ''}?

+
+ + +
+
+ {:else if editingMessageId === message.id} + +
+ +
+ + +
+
+ {:else} +
+ + {#if message.role === 'user'} + +
+ + {#if message.attachments && message.attachments.length > 0} +
+ {#each message.attachments as attachment} +
+ {#if attachment.type === 'image' && attachment.preview} + {attachment.name} + {:else} + {getAttachmentIcon(attachment)} + {/if} + {truncateName(attachment.name)} +
+ {/each} +
+ {/if} + + {#if message.content} +
+ {message.content} +
+ {/if} +
+ {:else} + +
+ {#if message.thinking && message.thinking.trim().length > 0} +
+ + {#if isThinkingExpanded(message.id)} +
+ {message.thinking.trim()} +
+ {/if} +
+ {/if} +
+ {message.content || (loading ? response : '')} + {#if loading && !message.content} + + {/if} +
+
+ {/if} +
+ + +
+ + + + + {#if message.role === 'user'} + + {/if} + + + {#if message.role === 'assistant' && isLastAssistantMessage(message.id) && !loading} + + {/if} + + + +
+ {/if} +
+
+ {/each} + + {#if messageList.length === 0} +
+
+
+
+
+
+

AWAITING INPUT

+

ENTER A QUERY TO BEGIN

+
+ {/if} + + +
+
diff --git a/dashboard/src/lib/components/ChatSidebar.svelte b/dashboard/src/lib/components/ChatSidebar.svelte new file mode 100644 index 00000000..87e06059 --- /dev/null +++ b/dashboard/src/lib/components/ChatSidebar.svelte @@ -0,0 +1,430 @@ + + + + diff --git a/dashboard/src/lib/components/HeaderNav.svelte b/dashboard/src/lib/components/HeaderNav.svelte new file mode 100644 index 00000000..4ec770d6 --- /dev/null +++ b/dashboard/src/lib/components/HeaderNav.svelte @@ -0,0 +1,57 @@ + + +
+ + + + +
+ {#if showHome} + + {/if} + + + + + + + Downloads + +
+
diff --git a/dashboard/src/lib/components/ModelCard.svelte b/dashboard/src/lib/components/ModelCard.svelte new file mode 100644 index 00000000..ee5f07ab --- /dev/null +++ b/dashboard/src/lib/components/ModelCard.svelte @@ -0,0 +1,660 @@ + + +
+ +
+
+
+
+ +
+ +
+
+
+
+ {model.name || model.id} +
+ {#if huggingFaceModelId} + + + + + + + + {/if} + {#if tags.length > 0} +
+ {#each tags as tag} + + {tag} + + {/each} +
+ {/if} +
+ {#if model.name && model.name !== model.id} +
+ {model.id} +
+ {/if} +
+
+
+ {estimatedMemory}GB +
+
+
+ + +
+ + {sharding} + + + {runtime === 'MlxRing' ? 'MLX Ring' : runtime === 'MlxIbv' || runtime === 'MlxJaccl' ? 'MLX RDMA' : runtime} + +
+ + + {#if placementPreview().nodes.length > 0} + {@const preview = placementPreview()} +
+ +
+ + + + + + + + + + + + + + + + + + + + + {#if preview.nodes.length > 1} + {#each preview.nodes as node, i} + {#each preview.nodes.slice(i + 1) as node2} + + {/each} + {/each} + {/if} + + {#each preview.nodes as node} + + + {#if node.deviceType === 'macbook'} + + + + + + + + + + {#if node.modelUsageGB > 0 && node.isUsed} + + {/if} + + + + {:else if node.deviceType === 'studio'} + + + + + + + + + {#if node.modelUsageGB > 0 && node.isUsed} + + {/if} + + {:else if node.deviceType === 'mini'} + + + + + + + + + {#if node.modelUsageGB > 0 && node.isUsed} + + {/if} + + {:else} + + + + + {/if} + + + 90 ? '#f87171' : '#FFD700') : '#4B5563'} + > + {node.newPercent.toFixed(0)}% + + + {/each} + +
+ {/if} + + + +
+
+ + diff --git a/dashboard/src/lib/components/TopologyGraph.svelte b/dashboard/src/lib/components/TopologyGraph.svelte new file mode 100644 index 00000000..e45ca080 --- /dev/null +++ b/dashboard/src/lib/components/TopologyGraph.svelte @@ -0,0 +1,971 @@ + + + + + diff --git a/dashboard/src/lib/components/index.ts b/dashboard/src/lib/components/index.ts new file mode 100644 index 00000000..bd750839 --- /dev/null +++ b/dashboard/src/lib/components/index.ts @@ -0,0 +1,7 @@ +export { default as TopologyGraph } from './TopologyGraph.svelte'; +export { default as ChatForm } from './ChatForm.svelte'; +export { default as ChatMessages } from './ChatMessages.svelte'; +export { default as ChatAttachments } from './ChatAttachments.svelte'; +export { default as ChatSidebar } from './ChatSidebar.svelte'; +export { default as ModelCard } from './ModelCard.svelte'; + diff --git a/dashboard/src/lib/stores/app.svelte.ts b/dashboard/src/lib/stores/app.svelte.ts new file mode 100644 index 00000000..ffeb1aa1 --- /dev/null +++ b/dashboard/src/lib/stores/app.svelte.ts @@ -0,0 +1,1395 @@ +/** + * AppStore - Central state management for the EXO dashboard + * + * Manages: + * - Chat state (whether a conversation has started) + * - Topology data from the EXO server + * - UI state for the topology/chat transition + */ + +import { browser } from '$app/environment'; + +// UUID generation fallback for browsers without crypto.randomUUID +function generateUUID(): string { + if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { + return crypto.randomUUID(); + } + // Fallback implementation + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + const r = Math.random() * 16 | 0; + const v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); +} + +export interface NodeInfo { + system_info?: { + model_id?: string; + chip?: string; + memory?: number; + }; + network_interfaces?: Array<{ + name?: string; + addresses?: string[]; + }>; + ip_to_interface?: Record; + macmon_info?: { + memory?: { + ram_usage: number; + ram_total: number; + }; + temp?: { + gpu_temp_avg: number; + }; + gpu_usage?: [number, number]; + sys_power?: number; + }; + last_macmon_update: number; + friendly_name?: string; +} + +export interface TopologyEdge { + source: string; + target: string; + sendBackIp?: string; + sendBackInterface?: string; +} + +export interface TopologyData { + nodes: Record; + edges: TopologyEdge[]; +} + +export interface Instance { + shardAssignments?: { + modelId?: string; + runnerToShard?: Record; + nodeToRunner?: Record; + }; +} + +interface RawNodeProfile { + modelId?: string; + chipId?: string; + friendlyName?: string; + networkInterfaces?: Array<{ + name?: string; + ipAddress?: string; + addresses?: Array<{ address?: string } | string>; + ipv4?: string; + ipv6?: string; + ipAddresses?: string[]; + ips?: string[]; + }>; + memory?: { + ramTotal?: { inBytes: number }; + ramAvailable?: { inBytes: number }; + swapTotal?: { inBytes: number }; + swapAvailable?: { inBytes: number }; + }; + system?: { + gpuUsage?: number; + temp?: number; + sysPower?: number; + }; +} + +interface RawTopologyNode { + nodeId: string; + nodeProfile: RawNodeProfile; +} + +interface RawTopologyConnection { + localNodeId: string; + sendBackNodeId: string; + sendBackMultiaddr?: { multiaddr?: string; address?: string; ip_address?: string } | string; +} + +interface RawTopology { + nodes: RawTopologyNode[]; + connections?: RawTopologyConnection[]; +} + +type RawNodeProfiles = Record; + +export interface DownloadProgress { + totalBytes: number; + downloadedBytes: number; + speed: number; + etaMs: number; + percentage: number; + completedFiles: number; + totalFiles: number; + files: Array<{ + name: string; + totalBytes: number; + downloadedBytes: number; + speed: number; + etaMs: number; + percentage: number; + }>; +} + +export interface ModelDownloadStatus { + isDownloading: boolean; + progress: DownloadProgress | null; + nodeDetails: Array<{ + nodeId: string; + nodeName: string; + progress: DownloadProgress; + }>; +} + +// Placement preview from the API +export interface PlacementPreview { + model_id: string; + sharding: 'Pipeline' | 'Tensor'; + instance_meta: 'MlxRing' | 'MlxIbv' | 'MlxJaccl'; + instance: unknown | null; + memory_delta_by_node: Record | null; + error: string | null; +} + +export interface PlacementPreviewResponse { + previews: PlacementPreview[]; +} + +interface RawStateResponse { + topology?: RawTopology; + instances?: Record; + runners?: Record; + downloads?: Record; + nodeProfiles?: RawNodeProfiles; +} + +export interface MessageAttachment { + type: 'image' | 'text' | 'file'; + name: string; + content?: string; + preview?: string; + mimeType?: string; +} + +export interface Message { + id: string; + role: 'user' | 'assistant' | 'system'; + content: string; + timestamp: number; + thinking?: string; + attachments?: MessageAttachment[]; + ttftMs?: number; // Time to first token in ms (for assistant messages) + tps?: number; // Tokens per second (for assistant messages) +} + +export interface Conversation { + id: string; + name: string; + messages: Message[]; + createdAt: number; + updatedAt: number; + modelId: string | null; + sharding: string | null; + instanceType: string | null; +} + +const STORAGE_KEY = 'exo-conversations'; + +function transformTopology(raw: RawTopology, profiles?: RawNodeProfiles): TopologyData { + const nodes: Record = {}; + const edges: TopologyEdge[] = []; + + for (const node of raw.nodes || []) { + const mergedProfile = profiles?.[node.nodeId]; + const profile = { ...(node.nodeProfile ?? {}), ...(mergedProfile ?? {}) }; + const ramTotal = profile?.memory?.ramTotal?.inBytes ?? 0; + const ramAvailable = profile?.memory?.ramAvailable?.inBytes ?? 0; + const ramUsage = Math.max(ramTotal - ramAvailable, 0); + + const networkInterfaces = (profile?.networkInterfaces || []).map((iface) => { + const addresses: string[] = []; + if (iface.ipAddress && typeof iface.ipAddress === 'string') { + addresses.push(iface.ipAddress); + } + if (Array.isArray(iface.addresses)) { + for (const addr of iface.addresses) { + if (typeof addr === 'string') addresses.push(addr); + else if (addr && typeof addr === 'object' && addr.address) addresses.push(addr.address); + } + } + if (Array.isArray(iface.ipAddresses)) { + addresses.push(...iface.ipAddresses.filter((a): a is string => typeof a === 'string')); + } + if (Array.isArray(iface.ips)) { + addresses.push(...iface.ips.filter((a): a is string => typeof a === 'string')); + } + if (iface.ipv4 && typeof iface.ipv4 === 'string') addresses.push(iface.ipv4); + if (iface.ipv6 && typeof iface.ipv6 === 'string') addresses.push(iface.ipv6); + + return { + name: iface.name, + addresses: Array.from(new Set(addresses)) + }; + }); + + const ipToInterface: Record = {}; + for (const iface of networkInterfaces) { + for (const addr of iface.addresses || []) { + ipToInterface[addr] = iface.name ?? ''; + } + } + + nodes[node.nodeId] = { + system_info: { + model_id: profile?.modelId ?? 'Unknown', + chip: profile?.chipId, + memory: ramTotal + }, + network_interfaces: networkInterfaces, + ip_to_interface: ipToInterface, + macmon_info: { + memory: { + ram_usage: ramUsage, + ram_total: ramTotal + }, + temp: profile?.system?.temp !== undefined ? { gpu_temp_avg: profile.system.temp } : undefined, + gpu_usage: profile?.system?.gpuUsage !== undefined ? [0, profile.system.gpuUsage] : undefined, + sys_power: profile?.system?.sysPower + }, + last_macmon_update: Date.now() / 1000, + friendly_name: profile?.friendlyName + }; + } + + for (const conn of raw.connections || []) { + if (!conn.localNodeId || !conn.sendBackNodeId) continue; + if (conn.localNodeId === conn.sendBackNodeId) continue; + if (!nodes[conn.localNodeId] || !nodes[conn.sendBackNodeId]) continue; + + let sendBackIp: string | undefined; + if (conn.sendBackMultiaddr) { + const multi = conn.sendBackMultiaddr; + if (typeof multi === 'string') { + sendBackIp = extractIpFromMultiaddr(multi); + } else { + sendBackIp = multi.ip_address || extractIpFromMultiaddr(multi.multiaddr) || extractIpFromMultiaddr(multi.address); + } + } + + edges.push({ + source: conn.localNodeId, + target: conn.sendBackNodeId, + sendBackIp + }); + } + + return { nodes, edges }; +} + +function extractIpFromMultiaddr(ma?: string): string | undefined { + if (!ma) return undefined; + const parts = ma.split('/'); + const ip4Idx = parts.indexOf('ip4'); + const ip6Idx = parts.indexOf('ip6'); + const idx = ip4Idx >= 0 ? ip4Idx : ip6Idx; + if (idx >= 0 && parts.length > idx + 1) { + return parts[idx + 1]; + } + return undefined; +} + +class AppStore { + // Conversation state + conversations = $state([]); + activeConversationId = $state(null); + + // Chat state + hasStartedChat = $state(false); + messages = $state([]); + currentResponse = $state(''); + isLoading = $state(false); + + // Performance metrics + ttftMs = $state(null); // Time to first token in ms + tps = $state(null); // Tokens per second + totalTokens = $state(0); // Total tokens in current response + + // Topology state + topologyData = $state(null); + instances = $state>({}); + runners = $state>({}); + downloads = $state>({}); + placementPreviews = $state([]); + selectedPreviewModelId = $state(null); + isLoadingPreviews = $state(false); + lastUpdate = $state(null); + + // UI state + isTopologyMinimized = $state(false); + isSidebarOpen = $state(false); // Hidden by default, shown when in chat mode + debugMode = $state(false); + + private fetchInterval: ReturnType | null = null; + private previewsInterval: ReturnType | null = null; + private lastConversationPersistTs = 0; + + constructor() { + if (browser) { + this.startPolling(); + this.loadConversationsFromStorage(); + this.loadDebugModeFromStorage(); + } + } + + /** + * Load conversations from localStorage + */ + private loadConversationsFromStorage() { + try { + const stored = localStorage.getItem(STORAGE_KEY); + if (stored) { + const parsed = JSON.parse(stored) as Array>; + this.conversations = parsed.map((conversation) => ({ + id: conversation.id ?? generateUUID(), + name: conversation.name ?? 'Chat', + messages: conversation.messages ?? [], + createdAt: conversation.createdAt ?? Date.now(), + updatedAt: conversation.updatedAt ?? Date.now(), + modelId: conversation.modelId ?? null, + sharding: conversation.sharding ?? null, + instanceType: conversation.instanceType ?? null + })); + } + } catch (error) { + console.error('Failed to load conversations:', error); + } + } + + /** + * Save conversations to localStorage + */ + private saveConversationsToStorage() { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(this.conversations)); + } catch (error) { + console.error('Failed to save conversations:', error); + } + } + + private loadDebugModeFromStorage() { + try { + const stored = localStorage.getItem('exo-debug-mode'); + if (stored !== null) { + this.debugMode = stored === 'true'; + } + } catch (error) { + console.error('Failed to load debug mode:', error); + } + } + + private saveDebugModeToStorage() { + try { + localStorage.setItem('exo-debug-mode', this.debugMode ? 'true' : 'false'); + } catch (error) { + console.error('Failed to save debug mode:', error); + } + } + + /** + * Create a new conversation + */ + createConversation(name?: string): string { + const id = generateUUID(); + const now = Date.now(); + + // Try to derive model and strategy immediately from selected model or running instances + let derivedModelId = this.selectedChatModel || null; + let derivedInstanceType: string | null = null; + let derivedSharding: string | null = null; + + // If no selected model, fall back to the first running instance + if (!derivedModelId) { + const firstInstance = Object.values(this.instances)[0]; + if (firstInstance) { + const candidateModel = this.extractInstanceModelId(firstInstance); + derivedModelId = candidateModel ?? null; + const details = this.describeInstance(firstInstance); + derivedInstanceType = details.instanceType; + derivedSharding = details.sharding; + } + } else { + // If selected model is set, attempt to get its details from instances + for (const [, instanceWrapper] of Object.entries(this.instances)) { + const candidateModelId = this.extractInstanceModelId(instanceWrapper); + if (candidateModelId === derivedModelId) { + const details = this.describeInstance(instanceWrapper); + derivedInstanceType = details.instanceType; + derivedSharding = details.sharding; + break; + } + } + } + + const conversation: Conversation = { + id, + name: name || `Chat ${new Date(now).toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}`, + messages: [], + createdAt: now, + updatedAt: now, + modelId: derivedModelId, + sharding: derivedSharding, + instanceType: derivedInstanceType + }; + + this.conversations.unshift(conversation); + this.activeConversationId = id; + this.messages = []; + this.hasStartedChat = true; + this.isTopologyMinimized = true; + this.isSidebarOpen = true; // Auto-open sidebar when chatting + + this.saveConversationsToStorage(); + return id; + } + + /** + * Load a conversation by ID + */ + loadConversation(id: string): boolean { + const conversation = this.conversations.find(c => c.id === id); + if (!conversation) return false; + + this.activeConversationId = id; + this.messages = [...conversation.messages]; + this.hasStartedChat = true; + this.isTopologyMinimized = true; + this.isSidebarOpen = true; // Auto-open sidebar when chatting + this.refreshConversationModelFromInstances(); + + return true; + } + + /** + * Delete a conversation by ID + */ + deleteConversation(id: string) { + this.conversations = this.conversations.filter(c => c.id !== id); + + if (this.activeConversationId === id) { + this.activeConversationId = null; + this.messages = []; + this.hasStartedChat = false; + this.isTopologyMinimized = false; + } + + this.saveConversationsToStorage(); + } + + /** + * Delete all conversations + */ + deleteAllConversations() { + this.conversations = []; + this.activeConversationId = null; + this.messages = []; + this.hasStartedChat = false; + this.isTopologyMinimized = false; + this.saveConversationsToStorage(); + } + + /** + * Rename a conversation + */ + renameConversation(id: string, newName: string) { + const conversation = this.conversations.find(c => c.id === id); + if (conversation) { + conversation.name = newName; + conversation.updatedAt = Date.now(); + this.saveConversationsToStorage(); + } + } + + private getTaggedValue(obj: unknown): [string | null, unknown] { + if (!obj || typeof obj !== 'object') return [null, null]; + const keys = Object.keys(obj as Record); + if (keys.length === 1) { + return [keys[0], (obj as Record)[keys[0]]]; + } + return [null, null]; + } + + private extractInstanceModelId(instanceWrapped: unknown): string | null { + const [, instance] = this.getTaggedValue(instanceWrapped); + if (!instance || typeof instance !== 'object') return null; + const inst = instance as { shardAssignments?: { modelId?: string } }; + return inst.shardAssignments?.modelId ?? null; + } + + private describeInstance(instanceWrapped: unknown): { sharding: string | null; instanceType: string | null } { + const [instanceTag, instance] = this.getTaggedValue(instanceWrapped); + if (!instance || typeof instance !== 'object') { + return { sharding: null, instanceType: null }; + } + + let instanceType: string | null = null; + if (instanceTag === 'MlxRingInstance') instanceType = 'MLX Ring'; + else if (instanceTag === 'MlxIbvInstance' || instanceTag === 'MlxJacclInstance') instanceType = 'MLX RDMA'; + + let sharding: string | null = null; + const inst = instance as { shardAssignments?: { runnerToShard?: Record } }; + const runnerToShard = inst.shardAssignments?.runnerToShard || {}; + const firstShardWrapped = Object.values(runnerToShard)[0]; + if (firstShardWrapped) { + const [shardTag] = this.getTaggedValue(firstShardWrapped); + if (shardTag === 'PipelineShardMetadata') sharding = 'Pipeline'; + else if (shardTag === 'TensorShardMetadata') sharding = 'Tensor'; + else if (shardTag === 'PrefillDecodeShardMetadata') sharding = 'Prefill/Decode'; + } + + return { sharding, instanceType }; + } + + private buildConversationModelInfo(modelId: string): { modelId: string; sharding: string | null; instanceType: string | null } { + let sharding: string | null = null; + let instanceType: string | null = null; + + for (const [, instanceWrapper] of Object.entries(this.instances)) { + const candidateModelId = this.extractInstanceModelId(instanceWrapper); + if (candidateModelId === modelId) { + const details = this.describeInstance(instanceWrapper); + sharding = details.sharding; + instanceType = details.instanceType; + break; + } + } + + return { modelId, sharding, instanceType }; + } + + private applyConversationModelInfo(info: { modelId: string; sharding: string | null; instanceType: string | null }) { + if (!this.activeConversationId) return; + const conversation = this.conversations.find(c => c.id === this.activeConversationId); + if (!conversation) return; + + // Keep the first known modelId stable; only backfill if missing + if (!conversation.modelId) { + conversation.modelId = info.modelId; + } + conversation.sharding = info.sharding; + conversation.instanceType = info.instanceType; + this.saveConversationsToStorage(); + } + + private getModelTail(modelId: string): string { + const parts = modelId.split('/'); + return (parts[parts.length - 1] || modelId).toLowerCase(); + } + + private isBetterModelId(currentId: string | null, candidateId: string | null): boolean { + if (!candidateId) return false; + if (!currentId) return true; + const currentTail = this.getModelTail(currentId); + const candidateTail = this.getModelTail(candidateId); + return candidateTail.length > currentTail.length && candidateTail.startsWith(currentTail); + } + + private refreshConversationModelFromInstances() { + if (!this.activeConversationId) return; + const conversation = this.conversations.find(c => c.id === this.activeConversationId); + if (!conversation) return; + + // Prefer stored model; do not replace it once set. Only backfill when missing. + let modelId = conversation.modelId; + + // If missing, try the selected model + if (!modelId && this.selectedChatModel) { + modelId = this.selectedChatModel; + } + + // If still missing, fall back to first instance model + if (!modelId) { + const firstInstance = Object.values(this.instances)[0]; + if (firstInstance) { + modelId = this.extractInstanceModelId(firstInstance); + } + } + + if (!modelId) return; + + // If a more specific instance modelId is available (e.g., adds "-4bit"), prefer it + let preferredModelId = modelId; + for (const [, instanceWrapper] of Object.entries(this.instances)) { + const candidate = this.extractInstanceModelId(instanceWrapper); + if (!candidate) continue; + if (candidate === preferredModelId) { + break; + } + if (this.isBetterModelId(preferredModelId, candidate)) { + preferredModelId = candidate; + } + } + + if (this.isBetterModelId(conversation.modelId, preferredModelId)) { + conversation.modelId = preferredModelId; + } + + const info = this.buildConversationModelInfo(preferredModelId); + const hasNewInfo = Boolean(info.sharding || info.instanceType || !conversation.modelId); + if (hasNewInfo) { + this.applyConversationModelInfo(info); + } + } + + getDebugMode(): boolean { + return this.debugMode; + } + + /** + * Update the active conversation with current messages + */ + private updateActiveConversation() { + if (!this.activeConversationId) return; + + const conversation = this.conversations.find(c => c.id === this.activeConversationId); + if (conversation) { + conversation.messages = [...this.messages]; + conversation.updatedAt = Date.now(); + + // Auto-generate name from first user message if still has default name + if (conversation.name.startsWith('Chat ')) { + const firstUserMsg = conversation.messages.find(m => m.role === 'user' && m.content.trim()); + if (firstUserMsg) { + // Clean up the content - remove file context markers and whitespace + let content = firstUserMsg.content + .replace(/\[File:.*?\][\s\S]*?```[\s\S]*?```/g, '') // Remove file attachments + .trim(); + + if (content) { + const preview = content.slice(0, 50); + conversation.name = preview.length < content.length ? preview + '...' : preview; + } + } + } + + this.saveConversationsToStorage(); + } + } + + private persistActiveConversation(throttleMs = 400) { + const now = Date.now(); + if (now - this.lastConversationPersistTs < throttleMs) return; + this.lastConversationPersistTs = now; + this.updateActiveConversation(); + } + + /** + * Toggle sidebar visibility + */ + toggleSidebar() { + this.isSidebarOpen = !this.isSidebarOpen; + } + + setDebugMode(enabled: boolean) { + this.debugMode = enabled; + this.saveDebugModeToStorage(); + } + + toggleDebugMode() { + this.debugMode = !this.debugMode; + this.saveDebugModeToStorage(); + } + + startPolling() { + this.fetchState(); + this.fetchInterval = setInterval(() => this.fetchState(), 1000); + } + + stopPolling() { + if (this.fetchInterval) { + clearInterval(this.fetchInterval); + this.fetchInterval = null; + } + this.stopPreviewsPolling(); + } + + async fetchState() { + try { + const response = await fetch('/state'); + if (!response.ok) { + throw new Error(`Failed to fetch state: ${response.status}`); + } + const data: RawStateResponse = await response.json(); + + if (data.topology) { + this.topologyData = transformTopology(data.topology, data.nodeProfiles); + } + if (data.instances) { + this.instances = data.instances; + this.refreshConversationModelFromInstances(); + } + if (data.runners) { + this.runners = data.runners; + } + if (data.downloads) { + this.downloads = data.downloads; + } + this.lastUpdate = Date.now(); + } catch (error) { + console.error('Error fetching state:', error); + } + } + + async fetchPlacementPreviews(modelId: string, showLoading = true) { + if (!modelId) return; + + if (showLoading) { + this.isLoadingPreviews = true; + } + this.selectedPreviewModelId = modelId; + + try { + const response = await fetch(`/instance/previews?model_id=${encodeURIComponent(modelId)}`); + if (!response.ok) { + throw new Error(`Failed to fetch placement previews: ${response.status}`); + } + const data: PlacementPreviewResponse = await response.json(); + this.placementPreviews = data.previews; + } catch (error) { + console.error('Error fetching placement previews:', error); + this.placementPreviews = []; + } finally { + if (showLoading) { + this.isLoadingPreviews = false; + } + } + } + + startPreviewsPolling(modelId: string) { + // Stop any existing preview polling + this.stopPreviewsPolling(); + + // Fetch immediately + this.fetchPlacementPreviews(modelId); + + // Then poll every 15 seconds (don't show loading spinner for subsequent fetches) + this.previewsInterval = setInterval(() => { + if (this.selectedPreviewModelId) { + this.fetchPlacementPreviews(this.selectedPreviewModelId, false); + } + }, 15000); + } + + stopPreviewsPolling() { + if (this.previewsInterval) { + clearInterval(this.previewsInterval); + this.previewsInterval = null; + } + } + + selectPreviewModel(modelId: string | null) { + if (modelId) { + this.startPreviewsPolling(modelId); + } else { + this.stopPreviewsPolling(); + this.selectedPreviewModelId = null; + this.placementPreviews = []; + } + } + + /** + * Starts a chat conversation - triggers the topology minimization animation + * Creates a new conversation if none is active + */ + startChat() { + if (!this.activeConversationId) { + this.createConversation(); + } else { + this.hasStartedChat = true; + this.isSidebarOpen = true; // Auto-open sidebar when chatting + // Small delay before minimizing for a nice visual effect + setTimeout(() => { + this.isTopologyMinimized = true; + }, 100); + } + } + + /** + * Add a message to the conversation + */ + addMessage(role: 'user' | 'assistant', content: string) { + const message: Message = { + id: generateUUID(), + role, + content, + timestamp: Date.now() + }; + this.messages.push(message); + return message; + } + + /** + * Delete a message and all subsequent messages + */ + deleteMessage(messageId: string) { + const messageIndex = this.messages.findIndex(m => m.id === messageId); + if (messageIndex === -1) return; + + // Remove this message and all subsequent messages + this.messages = this.messages.slice(0, messageIndex); + this.updateActiveConversation(); + } + + /** + * Edit a user message content (does not regenerate response) + */ + editMessage(messageId: string, newContent: string) { + const message = this.messages.find(m => m.id === messageId); + if (!message) return; + + message.content = newContent; + message.timestamp = Date.now(); + this.updateActiveConversation(); + } + + /** + * Edit a user message and regenerate the response + */ + async editAndRegenerate(messageId: string, newContent: string): Promise { + const messageIndex = this.messages.findIndex(m => m.id === messageId); + if (messageIndex === -1) return; + + const message = this.messages[messageIndex]; + if (message.role !== 'user') return; + + // Update the message content + message.content = newContent; + message.timestamp = Date.now(); + + // Remove all messages after this one (including the assistant response) + this.messages = this.messages.slice(0, messageIndex + 1); + + // Regenerate the response + await this.regenerateLastResponse(); + } + + /** + * Regenerate the last assistant response + */ + async regenerateLastResponse(): Promise { + if (this.isLoading) return; + + // Find the last user message + let lastUserIndex = -1; + for (let i = this.messages.length - 1; i >= 0; i--) { + if (this.messages[i].role === 'user') { + lastUserIndex = i; + break; + } + } + + if (lastUserIndex === -1) return; + + const lastUserMessage = this.messages[lastUserIndex]; + + // Remove any messages after the user message + this.messages = this.messages.slice(0, lastUserIndex + 1); + + // Resend the message to get a new response + this.isLoading = true; + this.currentResponse = ''; + + // Create placeholder for assistant message + const assistantMessage = this.addMessage('assistant', ''); + + try { + const systemPrompt = { + role: 'system' as const, + content: 'You are a helpful AI assistant. Respond directly and concisely. Do not show your reasoning or thought process.' + }; + + const apiMessages = [ + systemPrompt, + ...this.messages.slice(0, -1).map((m) => { + return { role: m.role, content: m.content }; + }) + ]; + + // Determine which model to use + let modelToUse = this.selectedChatModel; + if (!modelToUse) { + const firstInstanceKey = Object.keys(this.instances)[0]; + if (firstInstanceKey) { + const instance = this.instances[firstInstanceKey] as Record | undefined; + if (instance) { + const keys = Object.keys(instance); + if (keys.length === 1) { + const inst = instance[keys[0]] as { shardAssignments?: { modelId?: string } } | undefined; + modelToUse = inst?.shardAssignments?.modelId || ''; + } + } + } + } + + if (!modelToUse) { + assistantMessage.content = 'Error: No model available. Please launch an instance first.'; + this.isLoading = false; + this.updateActiveConversation(); + return; + } + + const response = await fetch('/v1/chat/completions', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + model: modelToUse, + messages: apiMessages, + stream: true + }) + }); + + if (!response.ok) { + const errorText = await response.text(); + assistantMessage.content = `Error: ${response.status} - ${errorText}`; + this.isLoading = false; + this.updateActiveConversation(); + return; + } + + const reader = response.body?.getReader(); + if (!reader) { + assistantMessage.content = 'Error: No response stream available'; + this.isLoading = false; + this.updateActiveConversation(); + return; + } + + const decoder = new TextDecoder(); + let fullContent = ''; + let partialLine = ''; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + const chunk = decoder.decode(value, { stream: true }); + const lines = (partialLine + chunk).split('\n'); + partialLine = lines.pop() || ''; + + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed || trimmed === 'data: [DONE]') continue; + + if (trimmed.startsWith('data: ')) { + try { + const json = JSON.parse(trimmed.slice(6)); + const delta = json.choices?.[0]?.delta?.content; + if (delta) { + fullContent += delta; + const { displayContent } = this.stripThinkingTags(fullContent); + this.currentResponse = displayContent; + assistantMessage.content = displayContent; + } + } catch { + // Skip malformed JSON + } + } + } + } + + const { displayContent } = this.stripThinkingTags(fullContent); + assistantMessage.content = displayContent; + this.currentResponse = ''; + this.updateActiveConversation(); + + } catch (error) { + assistantMessage.content = `Error: ${error instanceof Error ? error.message : 'Unknown error'}`; + this.updateActiveConversation(); + } finally { + this.isLoading = false; + } + } + + /** + * Selected model for chat (can be set by the UI) + */ + selectedChatModel = $state(''); + + /** + * Set the model to use for chat + */ + setSelectedModel(modelId: string) { + this.selectedChatModel = modelId; + // Clear stats when model changes + this.ttftMs = null; + this.tps = null; + } + + /** + * Strip thinking tags from content for display. + * Handles both complete ... blocks and in-progress ... blocks during streaming. + */ + private stripThinkingTags(content: string): { displayContent: string; thinkingContent: string } { + const extracted: string[] = []; + let displayContent = content; + + // Extract complete ... blocks + const completeBlockRegex = /([\s\S]*?)<\/think>/gi; + let match: RegExpExecArray | null; + while ((match = completeBlockRegex.exec(content)) !== null) { + const inner = match[1]?.trim(); + if (inner) extracted.push(inner); + } + displayContent = displayContent.replace(completeBlockRegex, ''); + + // Handle in-progress thinking block (has but no closing yet) + const openTagIndex = displayContent.lastIndexOf(''); + if (openTagIndex !== -1) { + const inProgressThinking = displayContent.slice(openTagIndex + 7).trim(); + if (inProgressThinking) { + extracted.push(inProgressThinking); + } + displayContent = displayContent.slice(0, openTagIndex); + } + + return { displayContent: displayContent.trim(), thinkingContent: extracted.join('\n\n') }; + } + + /** + * Send a message to the LLM and stream the response + */ + async sendMessage(content: string, files?: { id: string; name: string; type: string; textContent?: string; preview?: string }[]): Promise { + if ((!content.trim() && (!files || files.length === 0)) || this.isLoading) return; + + if (!this.hasStartedChat) { + this.startChat(); + } + + this.isLoading = true; + this.currentResponse = ''; + this.ttftMs = null; + this.tps = null; + this.totalTokens = 0; + + // Build attachments from files + const attachments: MessageAttachment[] = []; + let fileContext = ''; + + if (files && files.length > 0) { + for (const file of files) { + const isImage = file.type.startsWith('image/'); + + if (isImage && file.preview) { + attachments.push({ + type: 'image', + name: file.name, + preview: file.preview, + mimeType: file.type + }); + } else if (file.textContent) { + attachments.push({ + type: 'text', + name: file.name, + content: file.textContent, + mimeType: file.type + }); + // Add text file content to the message context + fileContext += `\n\n[File: ${file.name}]\n\`\`\`\n${file.textContent}\n\`\`\``; + } else { + attachments.push({ + type: 'file', + name: file.name, + mimeType: file.type + }); + } + } + } + + // Combine content with file context + const fullContent = content + fileContext; + + // Add user message with attachments + const userMessage: Message = { + id: generateUUID(), + role: 'user', + content: content, // Store original content for display + timestamp: Date.now(), + attachments: attachments.length > 0 ? attachments : undefined + }; + this.messages.push(userMessage); + + // Create placeholder for assistant message + const assistantMessage = this.addMessage('assistant', ''); + this.updateActiveConversation(); + + try { + // Build the messages array for the API with system prompt + const systemPrompt = { + role: 'system' as const, + content: 'You are a helpful AI assistant. Respond directly and concisely. Do not show your reasoning or thought process. When files are shared with you, analyze them and respond helpfully.' + }; + + // Build API messages - include file content for text files + const apiMessages = [ + systemPrompt, + ...this.messages.slice(0, -1).map((m) => { + // Build content including any text file attachments + let msgContent = m.content; + + // Add text attachments as context + if (m.attachments) { + for (const attachment of m.attachments) { + if (attachment.type === 'text' && attachment.content) { + msgContent += `\n\n[File: ${attachment.name}]\n\`\`\`\n${attachment.content}\n\`\`\``; + } + } + } + + return { + role: m.role, + content: msgContent + }; + }) + ]; + + // Determine the model to use - prefer selectedChatModel, otherwise try to get from instances + let modelToUse = this.selectedChatModel; + if (!modelToUse) { + // Try to get model from first running instance + for (const [, instanceWrapper] of Object.entries(this.instances)) { + if (instanceWrapper && typeof instanceWrapper === 'object') { + const keys = Object.keys(instanceWrapper as Record); + if (keys.length === 1) { + const instance = (instanceWrapper as Record)[keys[0]] as { shardAssignments?: { modelId?: string } }; + if (instance?.shardAssignments?.modelId) { + modelToUse = instance.shardAssignments.modelId; + break; + } + } + } + } + } + + if (!modelToUse) { + throw new Error('No model selected and no running instances available. Please launch an instance first.'); + } + + const conversationModelInfo = this.buildConversationModelInfo(modelToUse); + this.applyConversationModelInfo(conversationModelInfo); + + // Start timing for TTFT measurement + const requestStartTime = performance.now(); + let firstTokenTime: number | null = null; + let tokenCount = 0; + + const response = await fetch('/v1/chat/completions', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + model: modelToUse, + messages: apiMessages, + temperature: 0.7, + stream: true + }) + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`API error: ${response.status} - ${errorText}`); + } + + const reader = response.body?.getReader(); + if (!reader) { + throw new Error('No response body'); + } + + const decoder = new TextDecoder(); + let fullContent = ''; + let buffer = ''; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + + // Process complete lines + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; // Keep incomplete line in buffer + + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed) continue; + + if (trimmed.startsWith('data: ')) { + const data = trimmed.slice(6); + if (data === '[DONE]') continue; + + try { + const parsed = JSON.parse(data); + const tokenContent = parsed.choices?.[0]?.delta?.content; + if (tokenContent) { + // Track first token for TTFT + if (firstTokenTime === null) { + firstTokenTime = performance.now(); + this.ttftMs = firstTokenTime - requestStartTime; + } + + // Count tokens (each SSE chunk is typically one token) + tokenCount += 1; + this.totalTokens = tokenCount; + + // Update real-time TPS during streaming + if (firstTokenTime !== null && tokenCount > 1) { + const elapsed = performance.now() - firstTokenTime; + this.tps = (tokenCount / elapsed) * 1000; + } + + fullContent += tokenContent; + + // Strip thinking tags for display and extract thinking content + const { displayContent, thinkingContent } = this.stripThinkingTags(fullContent); + this.currentResponse = displayContent; + + // Update the assistant message in place + const idx = this.messages.findIndex(m => m.id === assistantMessage.id); + if (idx !== -1) { + this.messages[idx].content = displayContent; + this.messages[idx].thinking = thinkingContent || undefined; + } + this.persistActiveConversation(); + } + } catch { + // Skip invalid JSON lines + } + } + } + } + + // Process any remaining buffer + if (buffer.trim()) { + const trimmed = buffer.trim(); + if (trimmed.startsWith('data: ') && trimmed.slice(6) !== '[DONE]') { + try { + const parsed = JSON.parse(trimmed.slice(6)); + const tokenContent = parsed.choices?.[0]?.delta?.content; + if (tokenContent) { + fullContent += tokenContent; + this.persistActiveConversation(); + } + } catch { + // Skip + } + } + } + + // Calculate final TPS + if (firstTokenTime !== null && tokenCount > 1) { + const totalGenerationTime = performance.now() - firstTokenTime; + this.tps = (tokenCount / totalGenerationTime) * 1000; // tokens per second + } + + // Final cleanup of the message + const { displayContent, thinkingContent } = this.stripThinkingTags(fullContent); + const idx = this.messages.findIndex(m => m.id === assistantMessage.id); + if (idx !== -1) { + this.messages[idx].content = displayContent; + this.messages[idx].thinking = thinkingContent || undefined; + // Store performance metrics on the message + if (this.ttftMs !== null) { + this.messages[idx].ttftMs = this.ttftMs; + } + if (this.tps !== null) { + this.messages[idx].tps = this.tps; + } + } + this.persistActiveConversation(); + + } catch (error) { + console.error('Error sending message:', error); + // Update the assistant message with error + const idx = this.messages.findIndex(m => m.id === assistantMessage.id); + if (idx !== -1) { + this.messages[idx].content = `Error: ${error instanceof Error ? error.message : 'Failed to get response'}`; + } + this.persistActiveConversation(); + } finally { + this.isLoading = false; + this.currentResponse = ''; + this.updateActiveConversation(); + } + } + + /** + * Clear current chat and go back to welcome state + */ + clearChat() { + this.activeConversationId = null; + this.messages = []; + this.hasStartedChat = false; + this.isTopologyMinimized = false; + this.currentResponse = ''; + // Clear performance stats + this.ttftMs = null; + this.tps = null; + } + + /** + * Get the active conversation + */ + getActiveConversation(): Conversation | null { + if (!this.activeConversationId) return null; + return this.conversations.find(c => c.id === this.activeConversationId) || null; + } +} + +export const appStore = new AppStore(); + +// Reactive exports +export const hasStartedChat = () => appStore.hasStartedChat; +export const messages = () => appStore.messages; +export const currentResponse = () => appStore.currentResponse; +export const isLoading = () => appStore.isLoading; +export const ttftMs = () => appStore.ttftMs; +export const tps = () => appStore.tps; +export const totalTokens = () => appStore.totalTokens; +export const topologyData = () => appStore.topologyData; +export const instances = () => appStore.instances; +export const runners = () => appStore.runners; +export const downloads = () => appStore.downloads; +export const placementPreviews = () => appStore.placementPreviews; +export const selectedPreviewModelId = () => appStore.selectedPreviewModelId; +export const isLoadingPreviews = () => appStore.isLoadingPreviews; +export const lastUpdate = () => appStore.lastUpdate; +export const isTopologyMinimized = () => appStore.isTopologyMinimized; +export const selectedChatModel = () => appStore.selectedChatModel; +export const debugMode = () => appStore.getDebugMode(); + +// Actions +export const startChat = () => appStore.startChat(); +export const sendMessage = (content: string, files?: { id: string; name: string; type: string; textContent?: string; preview?: string }[]) => appStore.sendMessage(content, files); +export const clearChat = () => appStore.clearChat(); +export const setSelectedChatModel = (modelId: string) => appStore.setSelectedModel(modelId); +export const selectPreviewModel = (modelId: string | null) => appStore.selectPreviewModel(modelId); +export const deleteMessage = (messageId: string) => appStore.deleteMessage(messageId); +export const editMessage = (messageId: string, newContent: string) => appStore.editMessage(messageId, newContent); +export const editAndRegenerate = (messageId: string, newContent: string) => appStore.editAndRegenerate(messageId, newContent); +export const regenerateLastResponse = () => appStore.regenerateLastResponse(); + +// Conversation actions +export const conversations = () => appStore.conversations; +export const activeConversationId = () => appStore.activeConversationId; +export const createConversation = (name?: string) => appStore.createConversation(name); +export const loadConversation = (id: string) => appStore.loadConversation(id); +export const deleteConversation = (id: string) => appStore.deleteConversation(id); +export const deleteAllConversations = () => appStore.deleteAllConversations(); +export const renameConversation = (id: string, name: string) => appStore.renameConversation(id, name); +export const getActiveConversation = () => appStore.getActiveConversation(); + +// Sidebar actions +export const isSidebarOpen = () => appStore.isSidebarOpen; +export const toggleSidebar = () => appStore.toggleSidebar(); +export const toggleDebugMode = () => appStore.toggleDebugMode(); +export const setDebugMode = (enabled: boolean) => appStore.setDebugMode(enabled); +export const refreshState = () => appStore.fetchState(); + diff --git a/dashboard/src/lib/types/files.ts b/dashboard/src/lib/types/files.ts new file mode 100644 index 00000000..b92e269e --- /dev/null +++ b/dashboard/src/lib/types/files.ts @@ -0,0 +1,169 @@ +/** + * File attachment types for the chat interface + */ + +export interface ChatUploadedFile { + id: string; + name: string; + size: number; + type: string; + file: File; + preview?: string; + textContent?: string; +} + +export interface ChatAttachment { + type: 'image' | 'text' | 'pdf' | 'audio'; + name: string; + content?: string; + base64Url?: string; + mimeType?: string; +} + +export type FileCategory = 'image' | 'text' | 'pdf' | 'audio' | 'unknown'; + +export const IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg']; +export const IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml']; + +export const TEXT_EXTENSIONS = [ + '.txt', '.md', '.json', '.xml', '.yaml', '.yml', '.csv', '.log', + '.js', '.ts', '.jsx', '.tsx', '.py', '.java', '.cpp', '.c', '.h', + '.css', '.html', '.htm', '.sql', '.sh', '.bat', '.rs', '.go', + '.rb', '.php', '.swift', '.kt', '.scala', '.r', '.dart', '.vue', '.svelte' +]; +export const TEXT_MIME_TYPES = [ + 'text/plain', 'text/markdown', 'text/csv', 'text/html', 'text/css', + 'application/json', 'application/xml', 'text/xml', 'application/javascript', + 'text/javascript', 'application/typescript' +]; + +export const PDF_EXTENSIONS = ['.pdf']; +export const PDF_MIME_TYPES = ['application/pdf']; + +export const AUDIO_EXTENSIONS = ['.mp3', '.wav', '.ogg', '.m4a']; +export const AUDIO_MIME_TYPES = ['audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/mp4']; + +/** + * Get file category based on MIME type and extension + */ +export function getFileCategory(mimeType: string, fileName: string): FileCategory { + const extension = fileName.toLowerCase().slice(fileName.lastIndexOf('.')); + + if (IMAGE_MIME_TYPES.includes(mimeType) || IMAGE_EXTENSIONS.includes(extension)) { + return 'image'; + } + if (PDF_MIME_TYPES.includes(mimeType) || PDF_EXTENSIONS.includes(extension)) { + return 'pdf'; + } + if (AUDIO_MIME_TYPES.includes(mimeType) || AUDIO_EXTENSIONS.includes(extension)) { + return 'audio'; + } + if (TEXT_MIME_TYPES.includes(mimeType) || TEXT_EXTENSIONS.includes(extension) || mimeType.startsWith('text/')) { + return 'text'; + } + return 'unknown'; +} + +/** + * Get accept string for file input based on categories + */ +export function getAcceptString(categories: FileCategory[]): string { + const accepts: string[] = []; + + for (const category of categories) { + switch (category) { + case 'image': + accepts.push(...IMAGE_EXTENSIONS, ...IMAGE_MIME_TYPES); + break; + case 'text': + accepts.push(...TEXT_EXTENSIONS, ...TEXT_MIME_TYPES); + break; + case 'pdf': + accepts.push(...PDF_EXTENSIONS, ...PDF_MIME_TYPES); + break; + case 'audio': + accepts.push(...AUDIO_EXTENSIONS, ...AUDIO_MIME_TYPES); + break; + } + } + + return accepts.join(','); +} + +/** + * Format file size for display + */ +export function formatFileSize(bytes: number): string { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; +} + +/** + * Read file as data URL (base64) + */ +export function readFileAsDataURL(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result as string); + reader.onerror = () => reject(reader.error); + reader.readAsDataURL(file); + }); +} + +/** + * Read file as text + */ +export function readFileAsText(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result as string); + reader.onerror = () => reject(reader.error); + reader.readAsText(file); + }); +} + +/** + * Process uploaded files into ChatUploadedFile format + */ +export async function processUploadedFiles(files: File[]): Promise { + const results: ChatUploadedFile[] = []; + + for (const file of files) { + const id = Date.now().toString() + Math.random().toString(36).substring(2, 9); + const category = getFileCategory(file.type, file.name); + + const base: ChatUploadedFile = { + id, + name: file.name, + size: file.size, + type: file.type, + file + }; + + try { + if (category === 'image') { + const preview = await readFileAsDataURL(file); + results.push({ ...base, preview }); + } else if (category === 'text' || category === 'unknown') { + const textContent = await readFileAsText(file); + results.push({ ...base, textContent }); + } else if (category === 'pdf') { + results.push(base); + } else if (category === 'audio') { + const preview = await readFileAsDataURL(file); + results.push({ ...base, preview }); + } else { + results.push(base); + } + } catch (error) { + console.error('Error processing file:', file.name, error); + results.push(base); + } + } + + return results; +} + diff --git a/dashboard/src/routes/+layout.svelte b/dashboard/src/routes/+layout.svelte new file mode 100644 index 00000000..7e75b676 --- /dev/null +++ b/dashboard/src/routes/+layout.svelte @@ -0,0 +1,15 @@ + + + + EXO + + + +
+ {@render children?.()} +
+ diff --git a/dashboard/src/routes/+page.svelte b/dashboard/src/routes/+page.svelte new file mode 100644 index 00000000..082d1138 --- /dev/null +++ b/dashboard/src/routes/+page.svelte @@ -0,0 +1,1840 @@ + + + + + +
+ +
+ + +
+
+
+
+
+ + + + +
+ +
+ +
+ + {#if !chatStarted} + +
+ + +
+ + +
+ + + +
+ + +
+
+ +
+
+
+ + + + +
+ {:else} + +
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+ + + {#if minimized} + + {/if} +
+ {/if} +
+ +
diff --git a/dashboard/src/routes/downloads/+page.svelte b/dashboard/src/routes/downloads/+page.svelte new file mode 100644 index 00000000..81e29ed9 --- /dev/null +++ b/dashboard/src/routes/downloads/+page.svelte @@ -0,0 +1,441 @@ + + +
+ +
+
+
+

Downloads

+

Overview of models on each node

+
+
+ +
+ Last update: {lastUpdateTs ? new Date(lastUpdateTs).toLocaleTimeString() : 'n/a'} +
+
+
+ + {#if !hasDownloads} +
+
No downloads found. Start a model download to see progress here.
+
+ Download keys detected: {downloadKeys.length === 0 ? 'none' : downloadKeys.join(', ')} +
+
+ {:else} +
+ {#each downloadOverview as node} +
+
+
+
{node.nodeName}
+
{node.nodeId}
+
+
+ {node.models.filter(m => m.status === 'completed').length} /{node.models.length} models +
+
+ + {#each node.models as model} + {@const key = `${node.nodeId}|${model.modelId}`} + {@const pct = clampPercent(model.percentage)} + {@const gradient = getBarGradient(pct)} + {@const isExpanded = expanded.has(key)} +
+
+
+
{model.prettyName ?? model.modelId}
+
+ {model.modelId} +
+
+ {formatBytes(model.downloadedBytes)} / {formatBytes(model.totalBytes)} +
+
+
+ + {pct.toFixed(1)}% + + +
+
+ +
+
+
+ +
+ {model.status === 'completed' ? 'Completed' : `${formatSpeed(model.speed)} • ETA ${formatEta(model.etaMs)}`} + {#if model.status !== 'completed'} + {model.files.length} file{model.files.length === 1 ? '' : 's'} + {/if} +
+ + {#if isExpanded} +
+ {#if model.files.length === 0} +
No file details reported.
+ {:else} + {#each model.files as f} + {@const fpct = clampPercent(f.percentage)} + {@const fgradient = getBarGradient(fpct)} +
+
+ {f.name} + {fpct.toFixed(1)}% +
+
+
+
+
+ {formatBytes(f.downloadedBytes)} / {formatBytes(f.totalBytes)} + {formatSpeed(f.speed)} • ETA {formatEta(f.etaMs)} +
+
+ {/each} + {/if} +
+ {/if} +
+ {/each} +
+ {/each} +
+ {/if} + +
+
+ + diff --git a/dashboard/static/exo-logo.png b/dashboard/static/exo-logo.png new file mode 100644 index 00000000..199bcfdd Binary files /dev/null and b/dashboard/static/exo-logo.png differ diff --git a/dashboard/static/favicon.ico b/dashboard/static/favicon.ico new file mode 100644 index 00000000..c0ae2099 Binary files /dev/null and b/dashboard/static/favicon.ico differ diff --git a/dashboard/svelte.config.js b/dashboard/svelte.config.js new file mode 100644 index 00000000..991b07b0 --- /dev/null +++ b/dashboard/svelte.config.js @@ -0,0 +1,28 @@ +import adapter from '@sveltejs/adapter-static'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + preprocess: [vitePreprocess()], + + kit: { + paths: { + relative: true + }, + router: { type: 'hash' }, + adapter: adapter({ + pages: 'build', + assets: 'build', + fallback: 'index.html', + precompress: false, + strict: true + }), + alias: { + $lib: 'src/lib', + $components: 'src/lib/components' + } + } +}; + +export default config; + diff --git a/dashboard/tsconfig.json b/dashboard/tsconfig.json new file mode 100644 index 00000000..51db996c --- /dev/null +++ b/dashboard/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } +} + diff --git a/dashboard/vite.config.ts b/dashboard/vite.config.ts new file mode 100644 index 00000000..4d22f688 --- /dev/null +++ b/dashboard/vite.config.ts @@ -0,0 +1,16 @@ +import tailwindcss from '@tailwindcss/vite'; +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [tailwindcss(), sveltekit()], + server: { + proxy: { + '/v1': 'http://localhost:8000', + '/state': 'http://localhost:8000', + '/models': 'http://localhost:8000', + '/instance': 'http://localhost:8000' + } + } +}); + diff --git a/flake.nix b/flake.nix index c6141754..9d3ade75 100644 --- a/flake.nix +++ b/flake.nix @@ -81,6 +81,9 @@ # NIX nixpkgs-fmt + # SVELTE + nodejs + # MISC just jq @@ -96,7 +99,6 @@ shellHook = '' # PYTHON - export DASHBOARD_DIR="$(git rev-parse --show-toplevel)/dashboard" export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:${pkgs.python313}/lib" echo echo "🍎🍎 Run 'just ' to get started" diff --git a/justfile b/justfile index 6f4e67e9..0a82d616 100644 --- a/justfile +++ b/justfile @@ -20,7 +20,19 @@ rust-rebuild: cargo run --bin stub_gen just sync-clean +build-dashboard: + #!/usr/bin/env bash + cd dashboard + npm install + npm run build + +package: + uv run pyinstaller packaging/pyinstaller/exo.spec + clean: rm -rf **/__pycache__ rm -rf target/ rm -rf .venv + rm -rf dashboard/node_modules + rm -rf dashboard/.svelte-kit + rm -rf dashboard/build diff --git a/src/exo/master/api.py b/src/exo/master/api.py index 172ae5c1..ffbf3fde 100644 --- a/src/exo/master/api.py +++ b/src/exo/master/api.py @@ -1,4 +1,3 @@ -import os import time from collections.abc import AsyncGenerator from typing import cast @@ -15,6 +14,7 @@ from hypercorn.config import Config from hypercorn.typing import ASGIFramework from loguru import logger +from exo.master.placement import place_instance as get_instance_placements from exo.shared.apply import apply from exo.shared.election import ElectionMessage from exo.shared.logging import InterceptLogger @@ -23,11 +23,14 @@ from exo.shared.models.model_meta import get_model_meta from exo.shared.types.api import ( ChatCompletionMessage, ChatCompletionResponse, + CreateInstanceParams, CreateInstanceResponse, - CreateInstanceTaskParams, DeleteInstanceResponse, ModelList, ModelListModel, + PlaceInstanceParams, + PlacementPreview, + PlacementPreviewResponse, StreamingChoiceResponse, ) from exo.shared.types.chunks import TokenChunk @@ -37,17 +40,20 @@ from exo.shared.types.commands import ( CreateInstance, DeleteInstance, ForwarderCommand, + PlaceInstance, TaskFinished, ) from exo.shared.types.common import CommandId, NodeId, SessionId from exo.shared.types.events import ChunkGenerated, Event, ForwarderEvent, IndexedEvent from exo.shared.types.memory import Memory -from exo.shared.types.models import ModelMetadata +from exo.shared.types.models import ModelId, ModelMetadata from exo.shared.types.state import State from exo.shared.types.tasks import ChatCompletionTaskParams -from exo.shared.types.worker.instances import Instance, InstanceId +from exo.shared.types.worker.instances import Instance, InstanceId, InstanceMeta +from exo.shared.types.worker.shards import Sharding from exo.utils.banner import print_startup_banner from exo.utils.channels import Receiver, Sender, channel +from exo.utils.dashboard_path import find_dashboard from exo.utils.event_buffer import OrderedBuffer HIDE_THINKING = False @@ -91,7 +97,8 @@ class API: # This lets us pause the API if an election is running election_receiver: Receiver[ElectionMessage], ) -> None: - self._state = State() + self.state = State() + self._event_log: list[Event] = [] self.command_sender = command_sender self.global_event_receiver = global_event_receiver self.election_receiver = election_receiver @@ -111,12 +118,7 @@ class API: self.app.mount( "/", StaticFiles( - directory=os.environ.get( - "DASHBOARD_DIR", - os.path.abspath( - os.path.join(os.path.dirname(__file__), "../../../dashboard") - ), - ), + directory=find_dashboard(), html=True, ), name="dashboard", @@ -127,7 +129,7 @@ class API: def reset(self, new_session_id: SessionId, result_clock: int): logger.info("Resetting API State") - self._state = State() + self.state = State() self.session_id = new_session_id self.event_buffer = OrderedBuffer[Event]() self._chat_completion_queues = {} @@ -150,51 +152,194 @@ class API: ) def _setup_routes(self) -> None: + self.app.get("/node_id")(lambda: self.node_id) self.app.post("/instance")(self.create_instance) + self.app.post("/place_instance")(self.place_instance) + self.app.get("/instance/placement")(self.get_placement) + self.app.get("/instance/previews")(self.get_placement_previews) self.app.get("/instance/{instance_id}")(self.get_instance) self.app.delete("/instance/{instance_id}")(self.delete_instance) self.app.get("/models")(self.get_models) self.app.get("/v1/models")(self.get_models) self.app.post("/v1/chat/completions")(self.chat_completions) - self.app.get("/state")(self.state) + self.app.get("/state")(lambda: self.state) + self.app.get("/events")(lambda: self._event_log) - async def state(self) -> State: - return self._state - - async def create_instance( - self, payload: CreateInstanceTaskParams - ) -> CreateInstanceResponse: - model_meta = await resolve_model_meta(payload.model_id) - required_memory = model_meta.storage_size - available_memory = self._calculate_total_available_memory() - - if required_memory > available_memory: - raise HTTPException( - status_code=400, - detail=f"Insufficient memory to create instance. Required: {required_memory.in_gb:.1f}GB, Available: {available_memory.in_gb:.1f}GB", - ) - - command = CreateInstance( - model_meta=model_meta, + async def place_instance(self, payload: PlaceInstanceParams): + command = PlaceInstance( + model_meta=await resolve_model_meta(payload.model_id), + sharding=payload.sharding, instance_meta=payload.instance_meta, min_nodes=payload.min_nodes, - sharding=payload.sharding, ) await self._send(command) return CreateInstanceResponse( message="Command received.", command_id=command.command_id, - model_meta=model_meta, ) + async def create_instance( + self, payload: CreateInstanceParams + ) -> CreateInstanceResponse: + command = CreateInstance(instance=payload.instance) + await self._send(command) + + return CreateInstanceResponse( + message="Command received.", + command_id=command.command_id, + ) + + async def get_placement( + self, + model_id: str, + sharding: Sharding = Sharding.Pipeline, + instance_meta: InstanceMeta = InstanceMeta.MlxRing, + min_nodes: int = 1, + ) -> Instance: + model_meta = await resolve_model_meta(model_id) + + try: + placements = get_instance_placements( + PlaceInstance( + model_meta=model_meta, + sharding=sharding, + instance_meta=instance_meta, + min_nodes=min_nodes, + ), + topology=self.state.topology, + current_instances=self.state.instances, + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + + current_ids = set(self.state.instances.keys()) + new_ids = [ + instance_id for instance_id in placements if instance_id not in current_ids + ] + if len(new_ids) != 1: + raise HTTPException( + status_code=500, + detail="Expected exactly one new instance from placement", + ) + + return placements[new_ids[0]] + + async def get_placement_previews( + self, model_id: ModelId + ) -> PlacementPreviewResponse: + seen: set[tuple[ModelId, Sharding, InstanceMeta, int]] = set() + previews: list[PlacementPreview] = [] + if len(list(self.state.topology.list_nodes())) == 0: + return PlacementPreviewResponse(previews=[]) + + cards = [card for card in MODEL_CARDS.values() if card.short_id == model_id] + if not cards: + raise HTTPException(status_code=404, detail=f"Model {model_id} not found") + + instance_combinations: list[tuple[Sharding, InstanceMeta, int]] = [] + for sharding in (Sharding.Pipeline, Sharding.Tensor): + for instance_meta in (InstanceMeta.MlxRing, InstanceMeta.MlxJaccl): + instance_combinations.extend( + [ + (sharding, instance_meta, i) + for i in range( + 1, len(list(self.state.topology.list_nodes())) + 1 + ) + ] + ) + # TODO: PDD + # instance_combinations.append((Sharding.PrefillDecodeDisaggregation, InstanceMeta.MlxRing, 1)) + + for card in cards: + model_meta = card.metadata + for sharding, instance_meta, min_nodes in instance_combinations: + try: + placements = get_instance_placements( + PlaceInstance( + model_meta=model_meta, + sharding=sharding, + instance_meta=instance_meta, + min_nodes=min_nodes, + ), + topology=self.state.topology, + current_instances=self.state.instances, + ) + except ValueError as exc: + if (card.model_id, sharding, instance_meta, 0) not in seen: + previews.append( + PlacementPreview( + model_id=card.model_id, + sharding=sharding, + instance_meta=instance_meta, + instance=None, + error=str(exc), + ) + ) + seen.add((card.model_id, sharding, instance_meta, 0)) + continue + + current_ids = set(self.state.instances.keys()) + new_instances = [ + instance + for instance_id, instance in placements.items() + if instance_id not in current_ids + ] + + if len(new_instances) != 1: + if (card.model_id, sharding, instance_meta, 0) not in seen: + previews.append( + PlacementPreview( + model_id=card.model_id, + sharding=sharding, + instance_meta=instance_meta, + instance=None, + error="Expected exactly one new instance from placement", + ) + ) + seen.add((card.model_id, sharding, instance_meta, 0)) + continue + + instance = new_instances[0] + shard_assignments = instance.shard_assignments + node_ids = list(shard_assignments.node_to_runner.keys()) + + memory_delta_by_node: dict[str, int] = {} + if node_ids: + total_bytes = model_meta.storage_size.in_bytes + per_node = total_bytes // len(node_ids) + remainder = total_bytes % len(node_ids) + for index, node_id in enumerate(sorted(node_ids, key=str)): + extra = 1 if index < remainder else 0 + memory_delta_by_node[str(node_id)] = per_node + extra + + if ( + card.model_id, + sharding, + instance_meta, + len(node_ids), + ) not in seen: + previews.append( + PlacementPreview( + model_id=card.model_id, + sharding=sharding, + instance_meta=instance_meta, + instance=instance, + memory_delta_by_node=memory_delta_by_node or None, + error=None, + ) + ) + seen.add((card.model_id, sharding, instance_meta, len(node_ids))) + + return PlacementPreviewResponse(previews=previews) + def get_instance(self, instance_id: InstanceId) -> Instance: - if instance_id not in self._state.instances: + if instance_id not in self.state.instances: raise HTTPException(status_code=404, detail="Instance not found") - return self._state.instances[instance_id] + return self.state.instances[instance_id] async def delete_instance(self, instance_id: InstanceId) -> DeleteInstanceResponse: - if instance_id not in self._state.instances: + if instance_id not in self.state.instances: raise HTTPException(status_code=404, detail="Instance not found") command = DeleteInstance( @@ -261,7 +406,7 @@ class API: if not any( instance.shard_assignments.model_id == payload.model - for instance in self._state.instances.values() + for instance in self.state.instances.values() ): await self._trigger_notify_user_to_download_model(payload.model) raise HTTPException( @@ -281,7 +426,7 @@ class API: """Calculate total available memory across all nodes in bytes.""" total_available = Memory() - for node in self._state.topology.list_nodes(): + for node in self.state.topology.list_nodes(): if node.node_profile is not None: total_available += node.node_profile.memory.ram_available @@ -313,7 +458,7 @@ class API: async with create_task_group() as tg: self._tg = tg logger.info("Starting API") - tg.start_soon(self._apply_state) + tg.start_soon(self._applystate) tg.start_soon(self._pause_on_new_election) print_startup_banner(self.port) await serve( @@ -325,14 +470,15 @@ class API: self.command_sender.close() self.global_event_receiver.close() - async def _apply_state(self): + async def _applystate(self): with self.global_event_receiver as events: async for f_event in events: if f_event.origin != self.session_id.master_node_id: continue self.event_buffer.ingest(f_event.origin_idx, f_event.event) for idx, event in self.event_buffer.drain_indexed(): - self._state = apply(self._state, IndexedEvent(event=event, idx=idx)) + self._event_log.append(event) + self.state = apply(self.state, IndexedEvent(event=event, idx=idx)) if ( isinstance(event, ChunkGenerated) and event.command_id in self._chat_completion_queues diff --git a/src/exo/master/main.py b/src/exo/master/main.py index 149bfbd2..55b72d7d 100644 --- a/src/exo/master/main.py +++ b/src/exo/master/main.py @@ -5,9 +5,10 @@ from anyio.abc import TaskGroup from loguru import logger from exo.master.placement import ( - get_instance_placements_after_create, - get_instance_placements_after_delete, + add_instance_to_placements, + delete_instance, get_transition_events, + place_instance, ) from exo.shared.apply import apply from exo.shared.types.commands import ( @@ -15,6 +16,7 @@ from exo.shared.types.commands import ( CreateInstance, DeleteInstance, ForwarderCommand, + PlaceInstance, RequestEventLog, TaskFinished, TestCommand, @@ -148,19 +150,26 @@ class Master: self.command_task_mapping[command.command_id] = task_id case DeleteInstance(): - placement = get_instance_placements_after_delete( - command, self.state.instances + placement = delete_instance(command, self.state.instances) + transition_events = get_transition_events( + self.state.instances, placement + ) + generated_events.extend(transition_events) + case PlaceInstance(): + placement = place_instance( + command, + self.state.topology, + self.state.instances, ) transition_events = get_transition_events( self.state.instances, placement ) generated_events.extend(transition_events) case CreateInstance(): - placement = get_instance_placements_after_create( + placement = add_instance_to_placements( command, self.state.topology, self.state.instances, - tb_only=self.tb_only, ) transition_events = get_transition_events( self.state.instances, placement diff --git a/src/exo/master/placement.py b/src/exo/master/placement.py index c0862c10..f3856f93 100644 --- a/src/exo/master/placement.py +++ b/src/exo/master/placement.py @@ -17,6 +17,7 @@ from exo.shared.topology import Topology from exo.shared.types.commands import ( CreateInstance, DeleteInstance, + PlaceInstance, ) from exo.shared.types.common import Host from exo.shared.types.events import Event, InstanceCreated, InstanceDeleted @@ -35,12 +36,20 @@ def random_ephemeral_port() -> int: return random.randint(49152, 65535) -def get_instance_placements_after_create( +def add_instance_to_placements( command: CreateInstance, topology: Topology, current_instances: Mapping[InstanceId, Instance], - *, - tb_only: bool = False, +) -> Mapping[InstanceId, Instance]: + # TODO: validate against topology + + return {**current_instances, command.instance.instance_id: command.instance} + + +def place_instance( + command: PlaceInstance, + topology: Topology, + current_instances: Mapping[InstanceId, Instance], ) -> dict[InstanceId, Instance]: all_nodes = list(topology.list_nodes()) @@ -64,9 +73,7 @@ def get_instance_placements_after_create( if topology.get_subgraph_from_nodes(cycle).is_thunderbolt_cycle(cycle) ] - if tb_only and smallest_tb_cycles == []: - raise ValueError("No TB cycles found with sufficient memory") - elif smallest_tb_cycles != []: + if smallest_tb_cycles != []: smallest_cycles = smallest_tb_cycles cycles_with_leaf_nodes: list[list[NodeInfo]] = [ @@ -138,7 +145,7 @@ def get_instance_placements_after_create( return target_instances -def get_instance_placements_after_delete( +def delete_instance( command: DeleteInstance, current_instances: Mapping[InstanceId, Instance], ) -> dict[InstanceId, Instance]: diff --git a/src/exo/master/tests/test_master.py b/src/exo/master/tests/test_master.py index 948bcb1f..c2111baf 100644 --- a/src/exo/master/tests/test_master.py +++ b/src/exo/master/tests/test_master.py @@ -11,8 +11,8 @@ from exo.shared.types.api import ChatCompletionMessage, ChatCompletionTaskParams from exo.shared.types.commands import ( ChatCompletion, CommandId, - CreateInstance, ForwarderCommand, + PlaceInstance, ) from exo.shared.types.common import NodeId, SessionId from exo.shared.types.events import ( @@ -117,7 +117,7 @@ async def test_master(): ForwarderCommand( origin=node_id, command=( - CreateInstance( + PlaceInstance( command_id=CommandId(), model_meta=ModelMetadata( model_id=ModelId("llama-3.2-1b"), diff --git a/src/exo/master/tests/test_placement.py b/src/exo/master/tests/test_placement.py index 1bfdf4e2..c688e8ff 100644 --- a/src/exo/master/tests/test_placement.py +++ b/src/exo/master/tests/test_placement.py @@ -4,11 +4,11 @@ import pytest from loguru import logger from exo.master.placement import ( - get_instance_placements_after_create, get_transition_events, + place_instance, ) from exo.shared.topology import Topology -from exo.shared.types.commands import CreateInstance +from exo.shared.types.commands import PlaceInstance from exo.shared.types.common import CommandId, NodeId from exo.shared.types.events import InstanceCreated, InstanceDeleted from exo.shared.types.memory import Memory @@ -52,8 +52,8 @@ def model_meta() -> ModelMetadata: ) -def create_instance_command(model_meta: ModelMetadata) -> CreateInstance: - return CreateInstance( +def place_instance_command(model_meta: ModelMetadata) -> PlaceInstance: + return PlaceInstance( command_id=CommandId(), model_meta=model_meta, sharding=Sharding.Pipeline, @@ -85,7 +85,7 @@ def test_get_instance_placements_create_instance( available_memory ) # make it exactly fit across all nodes - cic = create_instance_command(model_meta) + cic = place_instance_command(model_meta) node_id_a = NodeId() node_id_b = NodeId() node_id_c = NodeId() @@ -97,7 +97,7 @@ def test_get_instance_placements_create_instance( topology.add_connection(create_connection(node_id_c, node_id_a)) # act - placements = get_instance_placements_after_create(cic, topology, {}) + placements = place_instance(cic, topology, {}) # assert assert len(placements) == 1 @@ -129,7 +129,7 @@ def test_get_instance_placements_one_node_exact_fit( topology = Topology() node_id = NodeId() topology.add_node(create_node(1000 * 1024, node_id)) - cic = create_instance_command( + cic = place_instance_command( ModelMetadata( model_id=ModelId("test-model"), storage_size=Memory.from_kb(1000), @@ -137,7 +137,7 @@ def test_get_instance_placements_one_node_exact_fit( n_layers=10, ), ) - placements = get_instance_placements_after_create(cic, topology, {}) + placements = place_instance(cic, topology, {}) assert len(placements) == 1 instance_id = list(placements.keys())[0] @@ -154,7 +154,7 @@ def test_get_instance_placements_one_node_fits_with_extra_memory( topology = Topology() node_id = NodeId() topology.add_node(create_node(1001 * 1024, node_id)) - cic = create_instance_command( + cic = place_instance_command( ModelMetadata( model_id=ModelId("test-model"), storage_size=Memory.from_kb(1000), @@ -162,7 +162,7 @@ def test_get_instance_placements_one_node_fits_with_extra_memory( n_layers=10, ), ) - placements = get_instance_placements_after_create(cic, topology, {}) + placements = place_instance(cic, topology, {}) assert len(placements) == 1 instance_id = list(placements.keys())[0] @@ -179,7 +179,7 @@ def test_get_instance_placements_one_node_not_fit( topology = Topology() node_id = NodeId() topology.add_node(create_node(1000 * 1024, node_id)) - cic = create_instance_command( + cic = place_instance_command( model_meta=ModelMetadata( model_id=ModelId("test-model"), storage_size=Memory.from_kb(1001), @@ -189,7 +189,7 @@ def test_get_instance_placements_one_node_not_fit( ) with pytest.raises(ValueError, match="No cycles found with sufficient memory"): - get_instance_placements_after_create(cic, topology, {}) + place_instance(cic, topology, {}) def test_get_transition_events_no_change(instance: Instance): @@ -292,12 +292,12 @@ def test_placement_prioritizes_leaf_cycle_with_less_memory( topology.add_connection(create_connection(node_id_e, node_id_y)) topology.add_connection(create_connection(node_id_f, node_id_z)) - cic = create_instance_command( + cic = place_instance_command( model_meta=model_meta, ) # Act - placements = get_instance_placements_after_create(cic, topology, {}) + placements = place_instance(cic, topology, {}) # Assert the chosen cycle is A-B-C (contains at least one leaf node), even though # D-E-F has more total memory. @@ -420,7 +420,7 @@ def test_tensor_rdma_backend_connectivity_matrix( topology.add_connection(conn_c_b) topology.add_connection(conn_a_c) - cic = CreateInstance( + cic = PlaceInstance( sharding=Sharding.Tensor, instance_meta=InstanceMeta.MlxJaccl, command_id=CommandId(), @@ -428,7 +428,7 @@ def test_tensor_rdma_backend_connectivity_matrix( min_nodes=1, ) - placements = get_instance_placements_after_create(cic, topology, {}) + placements = place_instance(cic, topology, {}) assert len(placements) == 1 instance_id = list(placements.keys())[0] diff --git a/src/exo/shared/models/model_cards.py b/src/exo/shared/models/model_cards.py index 6368a72d..17f00e4c 100644 --- a/src/exo/shared/models/model_cards.py +++ b/src/exo/shared/models/model_cards.py @@ -5,7 +5,7 @@ from exo.utils.pydantic_ext import CamelCaseModel class ModelCard(CamelCaseModel): short_id: str - model_id: str + model_id: ModelId name: str description: str tags: list[str] @@ -40,35 +40,63 @@ MODEL_CARDS: dict[str, ModelCard] = { # n_layers=61, # ), # ), - "deepseek-v3.1": ModelCard( - short_id="deepseek-v3.1", - model_id="mlx-community/DeepSeek-V3.1-8bit", - name="DeepSeek V3.1 (8-bit)", - description="""DeepSeek V3.1 is a large language model trained on the DeepSeek V3.1 dataset.""", - tags=[], - metadata=ModelMetadata( - model_id=ModelId("mlx-community/DeepSeek-V3.1-8bit"), - pretty_name="DeepSeek V3.1 (8-bit)", - storage_size=Memory.from_kb(754706307), - n_layers=61, - ), - ), - "deepseek-v3.1:4bit": ModelCard( - short_id="deepseek-v3.1:4bit", - model_id="mlx-community/DeepSeek-V3.1-4bit", + "deepseek-v3.1-4bit": ModelCard( + short_id="deepseek-v3.1-4bit", + model_id=ModelId("mlx-community/DeepSeek-V3.1-4bit"), name="DeepSeek V3.1 (4-bit)", description="""DeepSeek V3.1 is a large language model trained on the DeepSeek V3.1 dataset.""", tags=[], metadata=ModelMetadata( model_id=ModelId("mlx-community/DeepSeek-V3.1-4bit"), pretty_name="DeepSeek V3.1 (4-bit)", - storage_size=Memory.from_kb(754706307 // 2), # TODO !!!!! + storage_size=Memory.from_gb(378), n_layers=61, ), ), + "deepseek-v3.1-8bit": ModelCard( + short_id="deepseek-v3.1-8bit", + model_id=ModelId("mlx-community/DeepSeek-V3.1-8bit"), + name="DeepSeek V3.1 (8-bit)", + description="""DeepSeek V3.1 is a large language model trained on the DeepSeek V3.1 dataset.""", + tags=[], + metadata=ModelMetadata( + model_id=ModelId("mlx-community/DeepSeek-V3.1-8bit"), + pretty_name="DeepSeek V3.1 (8-bit)", + storage_size=Memory.from_gb(713), + n_layers=61, + ), + ), + # "deepseek-v3.2": ModelCard( + # short_id="deepseek-v3.2", + # model_id=ModelId("mlx-community/DeepSeek-V3.2-8bit"), + # name="DeepSeek V3.2 (8-bit)", + # description="""DeepSeek V3.2 is a large language model trained on the DeepSeek V3.2 dataset.""", + # tags=[], + # metadata=ModelMetadata( + # model_id=ModelId("mlx-community/DeepSeek-V3.2-8bit"), + # pretty_name="DeepSeek V3.2 (8-bit)", + # storage_size=Memory.from_kb(754706307), + # n_layers=61, + # hidden_size=7168, + # ), + # ), + # "deepseek-v3.2-4bit": ModelCard( + # short_id="deepseek-v3.2-4bit", + # model_id=ModelId("mlx-community/DeepSeek-V3.2-4bit"), + # name="DeepSeek V3.2 (4-bit)", + # description="""DeepSeek V3.2 is a large language model trained on the DeepSeek V3.2 dataset.""", + # tags=[], + # metadata=ModelMetadata( + # model_id=ModelId("mlx-community/DeepSeek-V3.2-4bit"), + # pretty_name="DeepSeek V3.2 (4-bit)", + # storage_size=Memory.from_kb(754706307 // 2), # TODO !!!!! + # n_layers=61, + # hidden_size=7168, + # ), + # ), # deepseek r1 - # "deepseek-r1-0528:4bit": ModelCard( - # short_id="deepseek-r1-0528:4bit", + # "deepseek-r1-0528-4bit": ModelCard( + # short_id="deepseek-r1-0528-4bit", # model_id="mlx-community/DeepSeek-R1-0528-4bit", # name="DeepSeek-R1-0528 (4-bit)", # description="""DeepSeek R1 is a large language model trained on the DeepSeek R1 dataset.""", @@ -78,6 +106,7 @@ MODEL_CARDS: dict[str, ModelCard] = { # pretty_name="DeepSeek R1 671B (4-bit)", # storage_size=Memory.from_kb(409706307), # n_layers=61, + # hidden_size=7168, # ), # ), # "deepseek-r1-0528": ModelCard( @@ -91,226 +120,279 @@ MODEL_CARDS: dict[str, ModelCard] = { # pretty_name="DeepSeek R1 671B (8-bit)", # storage_size=Memory.from_bytes(754998771712), # n_layers=61, + # . hidden_size=7168, # ), # ), # kimi k2 "kimi-k2-instruct-4bit": ModelCard( short_id="kimi-k2-instruct-4bit", - model_id="mlx-community/Kimi-K2-Instruct-4bit", + model_id=ModelId("mlx-community/Kimi-K2-Instruct-4bit"), name="Kimi K2 Instruct (4-bit)", description="""Kimi K2 is a large language model trained on the Kimi K2 dataset.""", tags=[], metadata=ModelMetadata( model_id=ModelId("mlx-community/Kimi-K2-Instruct-4bit"), pretty_name="Kimi K2 Instruct (4-bit)", - storage_size=Memory.from_bytes(577597603840), + storage_size=Memory.from_gb(578), n_layers=61, ), ), "kimi-k2-thinking": ModelCard( short_id="kimi-k2-thinking", - model_id="mlx-community/Kimi-K2-Thinking", - name="Kimi K2 Thinking", + model_id=ModelId("mlx-community/Kimi-K2-Thinking"), + name="Kimi K2 Thinking (4-bit)", description="""Kimi K2 Thinking is the latest, most capable version of open-source thinking model.""", tags=[], metadata=ModelMetadata( model_id=ModelId("mlx-community/Kimi-K2-Thinking"), - pretty_name="Kimi K2 Thinking", - storage_size=Memory.from_bytes(577597603840), + pretty_name="Kimi K2 Thinking (4-bit)", + storage_size=Memory.from_gb(658), n_layers=61, ), ), # llama-3.1 "llama-3.1-8b": ModelCard( short_id="llama-3.1-8b", - model_id="mlx-community/Meta-Llama-3.1-8B-Instruct-4bit", - name="Llama 3.1 8B", + model_id=ModelId("mlx-community/Meta-Llama-3.1-8B-Instruct-4bit"), + name="Llama 3.1 8B (4-bit)", description="""Llama 3.1 is a large language model trained on the Llama 3.1 dataset.""", tags=[], metadata=ModelMetadata( model_id=ModelId("mlx-community/Meta-Llama-3.1-8B-Instruct-4bit"), - pretty_name="Llama 3.1 8B", - storage_size=Memory.from_kb(4411528), + pretty_name="Llama 3.1 8B (4-bit)", + storage_size=Memory.from_mb(4423), n_layers=32, ), ), "llama-3.1-70b": ModelCard( short_id="llama-3.1-70b", - model_id="mlx-community/Meta-Llama-3.1-70B-Instruct-4bit", - name="Llama 3.1 70B", + model_id=ModelId("mlx-community/Meta-Llama-3.1-70B-Instruct-4bit"), + name="Llama 3.1 70B (4-bit)", description="""Llama 3.1 is a large language model trained on the Llama 3.1 dataset.""", tags=[], metadata=ModelMetadata( model_id=ModelId("mlx-community/Meta-Llama-3.1-70B-Instruct-4bit"), - pretty_name="Llama 3.1 70B", - storage_size=Memory.from_kb(38758160), + pretty_name="Llama 3.1 70B (4-bit)", + storage_size=Memory.from_mb(38769), n_layers=80, ), ), # llama-3.2 "llama-3.2-1b": ModelCard( short_id="llama-3.2-1b", - model_id="mlx-community/Llama-3.2-1B-Instruct-4bit", - name="Llama 3.2 1B", + model_id=ModelId("mlx-community/Llama-3.2-1B-Instruct-4bit"), + name="Llama 3.2 1B (4-bit)", description="""Llama 3.2 is a large language model trained on the Llama 3.2 dataset.""", tags=[], metadata=ModelMetadata( model_id=ModelId("mlx-community/Llama-3.2-1B-Instruct-4bit"), - pretty_name="Llama 3.2 1B", - storage_size=Memory.from_kb(678948), + pretty_name="Llama 3.2 1B (4-bit)", + storage_size=Memory.from_mb(696), n_layers=16, ), ), "llama-3.2-3b": ModelCard( short_id="llama-3.2-3b", - model_id="mlx-community/Llama-3.2-3B-Instruct-4bit", - name="Llama 3.2 3B", + model_id=ModelId("mlx-community/Llama-3.2-3B-Instruct-4bit"), + name="Llama 3.2 3B (4-bit)", description="""Llama 3.2 is a large language model trained on the Llama 3.2 dataset.""", tags=[], metadata=ModelMetadata( model_id=ModelId("mlx-community/Llama-3.2-3B-Instruct-4bit"), - pretty_name="Llama 3.2 3B", - storage_size=Memory.from_kb(1765062), + pretty_name="Llama 3.2 3B (4-bit)", + storage_size=Memory.from_mb(1777), + n_layers=28, + ), + ), + "llama-3.2-3b-8bit": ModelCard( + short_id="llama-3.2-3b-8bit", + model_id=ModelId("mlx-community/Llama-3.2-3B-Instruct-8bit"), + name="Llama 3.2 3B (8-bit)", + description="""Llama 3.2 is a large language model trained on the Llama 3.2 dataset.""", + tags=[], + metadata=ModelMetadata( + model_id=ModelId("mlx-community/Llama-3.2-3B-Instruct-8bit"), + pretty_name="Llama 3.2 3B (8-bit)", + storage_size=Memory.from_mb(3339), n_layers=28, ), ), # llama-3.3 "llama-3.3-70b": ModelCard( short_id="llama-3.3-70b", - model_id="mlx-community/Llama-3.3-70B-Instruct-4bit", + model_id=ModelId("mlx-community/Llama-3.3-70B-Instruct-4bit"), name="Llama 3.3 70B (4-bit)", description="""The Meta Llama 3.3 multilingual large language model (LLM) is an instruction tuned generative model in 70B (text in/text out)""", tags=[], metadata=ModelMetadata( model_id=ModelId("mlx-community/Llama-3.3-70B-Instruct-4bit"), pretty_name="Llama 3.3 70B", - storage_size=Memory.from_kb(38758160), + storage_size=Memory.from_mb(38769), n_layers=80, ), ), "llama-3.3-70b-8bit": ModelCard( short_id="llama-3.3-70b-8bit", - model_id="mlx-community/Llama-3.3-70B-Instruct-8bit", + model_id=ModelId("mlx-community/Llama-3.3-70B-Instruct-8bit"), name="Llama 3.3 70B (8-bit)", description="""The Meta Llama 3.3 multilingual large language model (LLM) is an instruction tuned generative model in 70B (text in/text out)""", tags=[], metadata=ModelMetadata( model_id=ModelId("mlx-community/Llama-3.3-70B-Instruct-8bit"), pretty_name="Llama 3.3 70B (8-bit)", - storage_size=Memory.from_kb(77516320), + storage_size=Memory.from_mb(73242), n_layers=80, ), ), "llama-3.3-70b-fp16": ModelCard( short_id="llama-3.3-70b-fp16", - model_id="mlx-community/llama-3.3-70b-instruct-fp16", + model_id=ModelId("mlx-community/llama-3.3-70b-instruct-fp16"), name="Llama 3.3 70B (FP16)", description="""The Meta Llama 3.3 multilingual large language model (LLM) is an instruction tuned generative model in 70B (text in/text out)""", tags=[], metadata=ModelMetadata( model_id=ModelId("mlx-community/llama-3.3-70b-instruct-fp16"), pretty_name="Llama 3.3 70B (FP16)", - storage_size=Memory.from_kb(155032640), + storage_size=Memory.from_mb(137695), n_layers=80, ), ), # phi-3 "phi-3-mini": ModelCard( short_id="phi-3-mini", - model_id="mlx-community/Phi-3-mini-128k-instruct-4bit", - name="Phi 3 Mini 128k", + model_id=ModelId("mlx-community/Phi-3-mini-128k-instruct-4bit"), + name="Phi 3 Mini 128k (4-bit)", description="""Phi 3 Mini is a large language model trained on the Phi 3 Mini dataset.""", tags=[], metadata=ModelMetadata( model_id=ModelId("mlx-community/Phi-3-mini-128k-instruct-4bit"), - pretty_name="Phi 3 Mini 128k", - storage_size=Memory.from_kb(2099262), + pretty_name="Phi 3 Mini 128k (4-bit)", + storage_size=Memory.from_mb(2099), n_layers=32, ), ), - # "phi-3-mini:128k": ModelCard( - # short_id="phi-3-mini:128k", - # model_id="mlx-community/Phi-3-mini-128k-instruct-4bit", - # name="Phi 3 Mini 128k", - # description="""Phi 3 Mini is a large language model trained on the Phi 3 Mini dataset.""", - # tags=[], - # metadata=ModelMetadata( - # model_id=ModelId("mlx-community/Phi-3-mini-128k-instruct-4bit"), - # pretty_name="Phi 3 Mini 128k", - # storage_size=Memory.from_kb(2099262), - # n_layers=32, - # ), - # ), # qwen3 "qwen3-0.6b": ModelCard( short_id="qwen3-0.6b", - model_id="mlx-community/Qwen3-0.6B-4bit", - name="Qwen3 0.6B", + model_id=ModelId("mlx-community/Qwen3-0.6B-4bit"), + name="Qwen3 0.6B (4-bit)", description="""Qwen3 0.6B is a large language model trained on the Qwen3 0.6B dataset.""", tags=[], metadata=ModelMetadata( model_id=ModelId("mlx-community/Qwen3-0.6B-4bit"), - pretty_name="Qwen3 0.6B", - storage_size=Memory.from_kb(327512), + pretty_name="Qwen3 0.6B (4-bit)", + storage_size=Memory.from_mb(327), + n_layers=28, + ), + ), + "qwen3-0.6b-8bit": ModelCard( + short_id="qwen3-0.6b-8bit", + model_id=ModelId("mlx-community/Qwen3-0.6B-8bit"), + name="Qwen3 0.6B (8-bit)", + description="""Qwen3 0.6B is a large language model trained on the Qwen3 0.6B dataset.""", + tags=[], + metadata=ModelMetadata( + model_id=ModelId("mlx-community/Qwen3-0.6B-8bit"), + pretty_name="Qwen3 0.6B (8-bit)", + storage_size=Memory.from_mb(666), n_layers=28, ), ), "qwen3-30b": ModelCard( short_id="qwen3-30b", - model_id="mlx-community/Qwen3-30B-A3B-4bit", - name="Qwen3 30B (Active 3B)", + model_id=ModelId("mlx-community/Qwen3-30B-A3B-4bit"), + name="Qwen3 30B A3B (4-bit)", description="""Qwen3 30B is a large language model trained on the Qwen3 30B dataset.""", tags=[], metadata=ModelMetadata( model_id=ModelId("mlx-community/Qwen3-30B-A3B-4bit"), - pretty_name="Qwen3 30B (Active 3B)", - storage_size=Memory.from_kb(16772092), + pretty_name="Qwen3 30B A3B (4-bit)", + storage_size=Memory.from_mb(16797), n_layers=48, ), ), - # "qwen3-235b-a22b": ModelCard( - # short_id="qwen3-235b-a22b", - # model_id="mlx-community/Qwen3-235B-A22B-4bit", - # name="Qwen3 235B, Active 22B (4-bit)", - # description="""Qwen3 235B (Active 22B) is a large language model trained on the Qwen3 235B dataset.""", - # tags=[], - # metadata=ModelMetadata( - # model_id=ModelId("mlx-community/Qwen3-235B-A22B-4bit"), - # pretty_name="Qwen3 235B, Active 22B (4-bit)", - # storage_size=Memory.from_kb(123207680), - # n_layers=94, - # ), - # ), + "qwen3-30b-8bit": ModelCard( + short_id="qwen3-30b-8bit", + model_id=ModelId("mlx-community/Qwen3-30B-A3B-8bit"), + name="Qwen3 30B A3B (8-bit)", + description="""Qwen3 30B is a large language model trained on the Qwen3 30B dataset.""", + tags=[], + metadata=ModelMetadata( + model_id=ModelId("mlx-community/Qwen3-30B-A3B-8bit"), + pretty_name="Qwen3 30B A3B (8-bit)", + storage_size=Memory.from_mb(31738), + n_layers=48, + ), + ), + "qwen3-235b-a22b-4bit": ModelCard( + short_id="qwen3-235b-a22b-4bit", + model_id=ModelId("mlx-community/Qwen3-235B-A22B-Instruct-2507-4bit"), + name="Qwen3 235B A22B (4-bit)", + description="""Qwen3 235B (Active 22B) is a large language model trained on the Qwen3 235B dataset.""", + tags=[], + metadata=ModelMetadata( + model_id=ModelId("mlx-community/Qwen3-235B-A22B-Instruct-2507-4bit"), + pretty_name="Qwen3 235B A22B (4-bit)", + storage_size=Memory.from_gb(132), + n_layers=94, + ), + ), "qwen3-235b-a22b-8bit": ModelCard( short_id="qwen3-235b-a22b-8bit", - model_id="mlx-community/Qwen3-235B-A22B-Instruct-2507-8bit", - name="Qwen3 235B, Active 22B (8-bit)", + model_id=ModelId("mlx-community/Qwen3-235B-A22B-Instruct-2507-8bit"), + name="Qwen3 235B A22B (8-bit)", description="""Qwen3 235B (Active 22B) is a large language model trained on the Qwen3 235B dataset.""", tags=[], metadata=ModelMetadata( model_id=ModelId("mlx-community/Qwen3-235B-A22B-Instruct-2507-8bit"), - pretty_name="Qwen3 235B, Active 22B (8-bit)", - storage_size=Memory.from_kb(246415360), + pretty_name="Qwen3 235B A22B (8-bit)", + storage_size=Memory.from_gb(250), n_layers=94, ), ), + "qwen3-coder-480b-a35b-4bit": ModelCard( + short_id="qwen3-coder-480b-a35b-4bit", + model_id=ModelId("mlx-community/Qwen3-Coder-480B-A35B-Instruct-4bit"), + name="Qwen3 Coder 480B A35B (4-bit)", + description="""Qwen3 Coder 480B (Active 35B) is a large language model trained on the Qwen3 Coder 480B dataset.""", + tags=[], + metadata=ModelMetadata( + model_id=ModelId("mlx-community/Qwen3-Coder-480B-A35B-Instruct-4bit"), + pretty_name="Qwen3 Coder 480B A35B (4-bit)", + storage_size=Memory.from_gb(270), + n_layers=62, + ), + ), + "qwen3-coder-480b-a35b-8bit": ModelCard( + short_id="qwen3-coder-480b-a35b-8bit", + model_id=ModelId("mlx-community/Qwen3-Coder-480B-A35B-Instruct-8bit"), + name="Qwen3 Coder 480B A35B (8-bit)", + description="""Qwen3 Coder 480B (Active 35B) is a large language model trained on the Qwen3 Coder 480B dataset.""", + tags=[], + metadata=ModelMetadata( + model_id=ModelId("mlx-community/Qwen3-Coder-480B-A35B-Instruct-8bit"), + pretty_name="Qwen3 Coder 480B A35B (8-bit)", + storage_size=Memory.from_gb(540), + n_layers=62, + ), + ), # granite "granite-3.3-2b": ModelCard( short_id="granite-3.3-2b", - model_id="mlx-community/granite-3.3-2b-instruct-fp16", - name="Granite 3.3 2B", + model_id=ModelId("mlx-community/granite-3.3-2b-instruct-fp16"), + name="Granite 3.3 2B (FP16)", description="""Granite-3.3-2B-Instruct is a 2-billion parameter 128K context length language model fine-tuned for improved reasoning and instruction-following capabilities.""", tags=[], metadata=ModelMetadata( model_id=ModelId("mlx-community/granite-3.3-2b-instruct-fp16"), - pretty_name="Granite 3.3 2B", - storage_size=Memory.from_kb(4948320), + pretty_name="Granite 3.3 2B (FP16)", + storage_size=Memory.from_mb(4951), n_layers=40, ), ), # "granite-3.3-8b": ModelCard( # short_id="granite-3.3-8b", - # model_id="mlx-community/granite-3.3-8b-instruct-fp16", + # model_id=ModelId("mlx-community/granite-3.3-8b-instruct-fp16"), # name="Granite 3.3 8B", # description="""Granite-3.3-8B-Instruct is a 8-billion parameter 128K context length language model fine-tuned for improved reasoning and instruction-following capabilities.""", # tags=[], @@ -335,4 +417,35 @@ MODEL_CARDS: dict[str, ModelCard] = { # n_layers=30, # ), # ), + # gpt-oss + # "gpt-oss-120b-MXFP4-Q8": ModelCard( + # short_id="gpt-oss-120b-MXFP4-Q8", + # model_id=ModelId("mlx-community/gpt-oss-120b-MXFP4-Q8"), + # name="GPT-OSS 120B (MXFP4-Q8, MLX)", + # description="""OpenAI's GPT-OSS 120B is a 117B-parameter Mixture-of-Experts model designed for high-reasoning and general-purpose use; this variant is a 4-bit MLX conversion for Apple Silicon.""", + # tags=[], + # metadata=ModelMetadata( + # model_id=ModelId("mlx-community/gpt-oss-120b-MXFP4-Q8"), + # pretty_name="GPT-OSS 120B (MXFP4-Q8, MLX)", + # storage_size=Memory.from_kb(68_996_301), + # n_layers=36, + # hidden_size=2880, + # supports_tensor=True, + # ), + # ), + # "gpt-oss-20b-4bit": ModelCard( + # short_id="gpt-oss-20b-4bit", + # model_id=ModelId("mlx-community/gpt-oss-20b-MXFP4-Q4"), + # name="GPT-OSS 20B (MXFP4-Q4, MLX)", + # description="""OpenAI's GPT-OSS 20B is a medium-sized MoE model for lower-latency and local or specialized use cases; this MLX variant uses MXFP4 4-bit quantization.""", + # tags=[], + # metadata=ModelMetadata( + # model_id=ModelId("mlx-community/gpt-oss-20b-MXFP4-Q4"), + # pretty_name="GPT-OSS 20B (MXFP4-Q4, MLX)", + # storage_size=Memory.from_kb(11_744_051), + # n_layers=24, + # hidden_size=2880, + # supports_tensor=True, + # ), + # ), } diff --git a/src/exo/shared/types/api.py b/src/exo/shared/types/api.py index 56def4dc..30b01e3e 100644 --- a/src/exo/shared/types/api.py +++ b/src/exo/shared/types/api.py @@ -1,11 +1,12 @@ import time from typing import Any, Literal -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, field_validator +from pydantic_core import PydanticUseDefault from exo.shared.types.common import CommandId -from exo.shared.types.models import ModelMetadata -from exo.shared.types.worker.instances import InstanceId, InstanceMeta +from exo.shared.types.models import ModelId +from exo.shared.types.worker.instances import Instance, InstanceId, InstanceMeta from exo.shared.types.worker.shards import Sharding FinishReason = Literal[ @@ -24,6 +25,8 @@ class ModelListModel(BaseModel): description: str = Field(default="") context_length: int = Field(default=0) tags: list[str] = Field(default=[]) + storage_size_megabytes: int = Field(default=0) + supports_tensor: bool = Field(default=False) class ModelList(BaseModel): @@ -132,13 +135,37 @@ class ChatCompletionTaskParams(BaseModel): user: str | None = None -class CreateInstanceTaskParams(BaseModel): - # TODO: in future the user could specify a specific Instance, not just a model_id +class PlaceInstanceParams(BaseModel): model_id: str sharding: Sharding = Sharding.Pipeline instance_meta: InstanceMeta = InstanceMeta.MlxRing min_nodes: int = 1 + @field_validator("sharding", "instance_meta", mode="plain") + @classmethod + def use_default(cls, v: object): + if not v or not isinstance(v, (Sharding, InstanceMeta)): + raise PydanticUseDefault() + return v + + +class CreateInstanceParams(BaseModel): + instance: Instance + + +class PlacementPreview(BaseModel): + model_id: ModelId + sharding: Sharding + instance_meta: InstanceMeta + instance: Instance | None = None + # Keys are NodeId strings, values are additional bytes that would be used on that node + memory_delta_by_node: dict[str, int] | None = None + error: str | None = None + + +class PlacementPreviewResponse(BaseModel): + previews: list[PlacementPreview] + class DeleteInstanceTaskParams(BaseModel): instance_id: str @@ -147,7 +174,6 @@ class DeleteInstanceTaskParams(BaseModel): class CreateInstanceResponse(BaseModel): message: str command_id: CommandId - model_meta: ModelMetadata class DeleteInstanceResponse(BaseModel): diff --git a/src/exo/shared/types/commands.py b/src/exo/shared/types/commands.py index 0a584ff5..5d8a5026 100644 --- a/src/exo/shared/types/commands.py +++ b/src/exo/shared/types/commands.py @@ -3,7 +3,7 @@ from pydantic import Field from exo.shared.types.api import ChatCompletionTaskParams from exo.shared.types.common import CommandId, NodeId from exo.shared.types.models import ModelMetadata -from exo.shared.types.worker.instances import InstanceId, InstanceMeta +from exo.shared.types.worker.instances import Instance, InstanceId, InstanceMeta from exo.shared.types.worker.shards import Sharding from exo.utils.pydantic_ext import CamelCaseModel, TaggedModel @@ -20,13 +20,17 @@ class ChatCompletion(BaseCommand): request_params: ChatCompletionTaskParams -class CreateInstance(BaseCommand): +class PlaceInstance(BaseCommand): model_meta: ModelMetadata sharding: Sharding instance_meta: InstanceMeta min_nodes: int +class CreateInstance(BaseCommand): + instance: Instance + + class DeleteInstance(BaseCommand): instance_id: InstanceId @@ -43,6 +47,7 @@ Command = ( TestCommand | RequestEventLog | ChatCompletion + | PlaceInstance | CreateInstance | DeleteInstance | TaskFinished diff --git a/src/exo/shared/types/memory.py b/src/exo/shared/types/memory.py index 562c3c87..b97fb345 100644 --- a/src/exo/shared/types/memory.py +++ b/src/exo/shared/types/memory.py @@ -47,6 +47,11 @@ class Memory(CamelCaseModel): """Construct a new Memory object from a number of megabytes""" return cls(in_bytes=round(val * (1024**2))) + @classmethod + def from_gb(cls, val: float) -> Self: + """Construct a new Memory object from a number of megabytes""" + return cls(in_bytes=round(val * (1024**3))) + @property def in_gb(self) -> float: """The approximate gigabytes this memory represents.""" diff --git a/src/exo/utils/dashboard_path.py b/src/exo/utils/dashboard_path.py new file mode 100644 index 00000000..b9e6990c --- /dev/null +++ b/src/exo/utils/dashboard_path.py @@ -0,0 +1,45 @@ +import os +import sys +from pathlib import Path +from typing import cast + + +def find_dashboard() -> Path: + dashboard = ( + _find_dashboard_in_env() + or _find_dashboard_in_repo() + or _find_dashboard_in_bundle() + ) + if not dashboard: + raise FileNotFoundError( + "Unable to locate dashboard assets. Export DASHBOARD_DIR or rebuild the binary." + ) + return dashboard + + +def _find_dashboard_in_env() -> Path | None: + env = os.environ.get("DASHBOARD_DIR") + if not env: + return None + resolved_env = Path(env).expanduser().resolve() + + return resolved_env + + +def _find_dashboard_in_repo() -> Path | None: + current_module = Path(__file__).resolve() + for parent in current_module.parents: + build = parent / "dashboard" / "build" + if build.is_dir() and (build / "index.html").exists(): + return build + return None + + +def _find_dashboard_in_bundle() -> Path | None: + frozen_root = cast(str | None, getattr(sys, "_MEIPASS", None)) + if frozen_root is None: + return None + candidate = Path(frozen_root) / "dashboard" + if candidate.is_dir(): + return candidate + return None