feat(interface): frontend updates and Nexus branding
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "0.0.0",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "web",
|
||||
"version": "0.0.0",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4"
|
||||
@@ -24,13 +24,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
|
||||
"integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
|
||||
"integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": "^7.28.5",
|
||||
"@babel/helper-validator-identifier": "^7.29.7",
|
||||
"js-tokens": "^4.0.0",
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
@@ -39,9 +39,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/compat-data": {
|
||||
"version": "7.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
|
||||
"integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
|
||||
"integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -49,21 +49,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/core": {
|
||||
"version": "7.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
|
||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
|
||||
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
"@babel/helper-compilation-targets": "^7.28.6",
|
||||
"@babel/helper-module-transforms": "^7.28.6",
|
||||
"@babel/helpers": "^7.28.6",
|
||||
"@babel/parser": "^7.29.0",
|
||||
"@babel/template": "^7.28.6",
|
||||
"@babel/traverse": "^7.29.0",
|
||||
"@babel/types": "^7.29.0",
|
||||
"@babel/code-frame": "^7.29.7",
|
||||
"@babel/generator": "^7.29.7",
|
||||
"@babel/helper-compilation-targets": "^7.29.7",
|
||||
"@babel/helper-module-transforms": "^7.29.7",
|
||||
"@babel/helpers": "^7.29.7",
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@babel/template": "^7.29.7",
|
||||
"@babel/traverse": "^7.29.7",
|
||||
"@babel/types": "^7.29.7",
|
||||
"@jridgewell/remapping": "^2.3.5",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"debug": "^4.1.0",
|
||||
@@ -80,14 +80,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/generator": {
|
||||
"version": "7.29.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
|
||||
"integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
|
||||
"integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.29.0",
|
||||
"@babel/types": "^7.29.0",
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@babel/types": "^7.29.7",
|
||||
"@jridgewell/gen-mapping": "^0.3.12",
|
||||
"@jridgewell/trace-mapping": "^0.3.28",
|
||||
"jsesc": "^3.0.2"
|
||||
@@ -97,14 +97,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-compilation-targets": {
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
|
||||
"integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
|
||||
"integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/compat-data": "^7.28.6",
|
||||
"@babel/helper-validator-option": "^7.27.1",
|
||||
"@babel/compat-data": "^7.29.7",
|
||||
"@babel/helper-validator-option": "^7.29.7",
|
||||
"browserslist": "^4.24.0",
|
||||
"lru-cache": "^5.1.1",
|
||||
"semver": "^6.3.1"
|
||||
@@ -114,9 +114,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-globals": {
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
|
||||
"integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
|
||||
"integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -124,29 +124,29 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-module-imports": {
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
|
||||
"integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
|
||||
"integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/traverse": "^7.28.6",
|
||||
"@babel/types": "^7.28.6"
|
||||
"@babel/traverse": "^7.29.7",
|
||||
"@babel/types": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-module-transforms": {
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
|
||||
"integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
|
||||
"integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-imports": "^7.28.6",
|
||||
"@babel/helper-validator-identifier": "^7.28.5",
|
||||
"@babel/traverse": "^7.28.6"
|
||||
"@babel/helper-module-imports": "^7.29.7",
|
||||
"@babel/helper-validator-identifier": "^7.29.7",
|
||||
"@babel/traverse": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -156,9 +156,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
||||
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
|
||||
"integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -166,9 +166,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
|
||||
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
|
||||
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -176,9 +176,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-option": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
|
||||
"integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
|
||||
"integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -186,27 +186,27 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helpers": {
|
||||
"version": "7.29.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
|
||||
"integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
|
||||
"integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/template": "^7.28.6",
|
||||
"@babel/types": "^7.29.0"
|
||||
"@babel/template": "^7.29.7",
|
||||
"@babel/types": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.29.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
|
||||
"integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
|
||||
"integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.29.0"
|
||||
"@babel/types": "^7.29.7"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
@@ -216,33 +216,33 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
||||
"integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
|
||||
"integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.28.6",
|
||||
"@babel/parser": "^7.28.6",
|
||||
"@babel/types": "^7.28.6"
|
||||
"@babel/code-frame": "^7.29.7",
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@babel/types": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/traverse": {
|
||||
"version": "7.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
|
||||
"integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
|
||||
"integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
"@babel/helper-globals": "^7.28.0",
|
||||
"@babel/parser": "^7.29.0",
|
||||
"@babel/template": "^7.28.6",
|
||||
"@babel/types": "^7.29.0",
|
||||
"@babel/code-frame": "^7.29.7",
|
||||
"@babel/generator": "^7.29.7",
|
||||
"@babel/helper-globals": "^7.29.7",
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@babel/template": "^7.29.7",
|
||||
"@babel/types": "^7.29.7",
|
||||
"debug": "^4.3.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -250,23 +250,23 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
|
||||
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
|
||||
"integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.28.5"
|
||||
"@babel/helper-string-parser": "^7.29.7",
|
||||
"@babel/helper-validator-identifier": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
|
||||
"integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
||||
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -276,9 +276,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz",
|
||||
"integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
||||
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -557,14 +557,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
|
||||
"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",
|
||||
"integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@tybys/wasm-util": "^0.10.1"
|
||||
"@tybys/wasm-util": "^0.10.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -576,9 +576,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-project/types": {
|
||||
"version": "0.124.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz",
|
||||
"integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==",
|
||||
"version": "0.133.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz",
|
||||
"integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@@ -586,9 +586,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-android-arm64": {
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
|
||||
"integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -603,9 +603,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz",
|
||||
"integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -620,9 +620,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-x64": {
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz",
|
||||
"integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -637,9 +637,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz",
|
||||
"integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -654,9 +654,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz",
|
||||
"integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -671,9 +671,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz",
|
||||
"integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -688,9 +688,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz",
|
||||
"integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -705,9 +705,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz",
|
||||
"integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -722,9 +722,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz",
|
||||
"integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -739,9 +739,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz",
|
||||
"integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -756,9 +756,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz",
|
||||
"integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -773,9 +773,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz",
|
||||
"integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -790,9 +790,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-wasm32-wasi": {
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz",
|
||||
"integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
@@ -800,18 +800,18 @@
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "1.9.2",
|
||||
"@emnapi/runtime": "1.9.2",
|
||||
"@napi-rs/wasm-runtime": "^1.1.3"
|
||||
"@emnapi/core": "1.10.0",
|
||||
"@emnapi/runtime": "1.10.0",
|
||||
"@napi-rs/wasm-runtime": "^1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz",
|
||||
"integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -826,9 +826,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz",
|
||||
"integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -850,9 +850,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
||||
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
|
||||
"version": "0.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
|
||||
"integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -991,9 +991,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.10.19",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.19.tgz",
|
||||
"integrity": "sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g==",
|
||||
"version": "2.10.38",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.38.tgz",
|
||||
"integrity": "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
@@ -1059,9 +1059,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001788",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz",
|
||||
"integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==",
|
||||
"version": "1.0.30001799",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz",
|
||||
"integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -1188,9 +1188,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.336",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.336.tgz",
|
||||
"integrity": "sha512-AbH9q9J455r/nLmdNZes0G0ZKcRX73FicwowalLs6ijwOmCJSRRrLX63lcAlzy9ux3dWK1w1+1nsBJEWN11hcQ==",
|
||||
"version": "1.5.375",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.375.tgz",
|
||||
"integrity": "sha512-ZWP5eB4BVPW/ZYo9252hQZHZ5XavtsTgpbhcmMmRwymavC5AsLWQWBPaKMeNd2LW0KGby5HPXvj7+sr4ta5j/Q==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
@@ -1644,10 +1644,20 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz",
|
||||
"integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/puzrin"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/nodeca"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
@@ -2042,9 +2052,9 @@
|
||||
"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==",
|
||||
"version": "3.3.12",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
||||
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -2068,11 +2078,14 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.37",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz",
|
||||
"integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==",
|
||||
"version": "2.0.48",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.48.tgz",
|
||||
"integrity": "sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/optionator": {
|
||||
"version": "0.9.4",
|
||||
@@ -2178,9 +2191,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.10",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
|
||||
"integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
|
||||
"version": "8.5.15",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
||||
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -2198,7 +2211,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"nanoid": "^3.3.12",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
@@ -2258,14 +2271,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rolldown": {
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
|
||||
"integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@oxc-project/types": "=0.124.0",
|
||||
"@rolldown/pluginutils": "1.0.0-rc.15"
|
||||
"@oxc-project/types": "=0.133.0",
|
||||
"@rolldown/pluginutils": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"rolldown": "bin/cli.mjs"
|
||||
@@ -2274,27 +2287,27 @@
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rolldown/binding-android-arm64": "1.0.0-rc.15",
|
||||
"@rolldown/binding-darwin-arm64": "1.0.0-rc.15",
|
||||
"@rolldown/binding-darwin-x64": "1.0.0-rc.15",
|
||||
"@rolldown/binding-freebsd-x64": "1.0.0-rc.15",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15",
|
||||
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15",
|
||||
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15",
|
||||
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.15",
|
||||
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.15",
|
||||
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.15",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15",
|
||||
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15"
|
||||
"@rolldown/binding-android-arm64": "1.0.3",
|
||||
"@rolldown/binding-darwin-arm64": "1.0.3",
|
||||
"@rolldown/binding-darwin-x64": "1.0.3",
|
||||
"@rolldown/binding-freebsd-x64": "1.0.3",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.3",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.0.3",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.0.3",
|
||||
"@rolldown/binding-linux-ppc64-gnu": "1.0.3",
|
||||
"@rolldown/binding-linux-s390x-gnu": "1.0.3",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.0.3",
|
||||
"@rolldown/binding-linux-x64-musl": "1.0.3",
|
||||
"@rolldown/binding-openharmony-arm64": "1.0.3",
|
||||
"@rolldown/binding-wasm32-wasi": "1.0.3",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.0.3",
|
||||
"@rolldown/binding-win32-x64-msvc": "1.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/rolldown/node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==",
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
|
||||
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -2374,9 +2387,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.16",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
|
||||
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
|
||||
"version": "0.2.17",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2453,17 +2466,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "8.0.8",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz",
|
||||
"integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==",
|
||||
"version": "8.0.16",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
|
||||
"integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lightningcss": "^1.32.0",
|
||||
"picomatch": "^4.0.4",
|
||||
"postcss": "^8.5.8",
|
||||
"rolldown": "1.0.0-rc.15",
|
||||
"tinyglobby": "^0.2.15"
|
||||
"postcss": "^8.5.15",
|
||||
"rolldown": "1.0.3",
|
||||
"tinyglobby": "^0.2.17"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
@@ -2479,7 +2492,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": "^20.19.0 || >=22.12.0",
|
||||
"@vitejs/devtools": "^0.1.0",
|
||||
"@vitejs/devtools": "^0.1.18",
|
||||
"esbuild": "^0.27.0 || ^0.28.0",
|
||||
"jiti": ">=1.21.0",
|
||||
"less": "^4.0.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "web",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 185 KiB After Width: | Height: | Size: 185 KiB |
@@ -19,10 +19,12 @@ function timeAgo(epochSeconds) {
|
||||
function App() {
|
||||
const [currentPage, setCurrentPage] = useState("chatbot");
|
||||
const [status, setStatus] = useState("Checking Synapse...");
|
||||
const [version, setVersion] = useState("");
|
||||
const [ollamaStatus, setOllamaStatus] = useState("checking");
|
||||
const [showStatusTooltip, setShowStatusTooltip] = useState(false);
|
||||
|
||||
const [activeConversationId, setActiveConversationId] = useState(() => crypto.randomUUID());
|
||||
const [isModelPulling, setIsModelPulling] = useState(false);
|
||||
const [conversations, setConversations] = useState([]);
|
||||
const [search, setSearch] = useState("");
|
||||
const [hoveredConvId, setHoveredConvId] = useState(null);
|
||||
@@ -33,6 +35,7 @@ function App() {
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
setStatus(d.status ?? "Unknown");
|
||||
setVersion(d.version ?? "");
|
||||
setOllamaStatus(d.ollama ?? "unavailable");
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -91,6 +94,25 @@ function App() {
|
||||
setCurrentPage("chatbot");
|
||||
};
|
||||
|
||||
const renameConversation = async (e, conv) => {
|
||||
e.stopPropagation();
|
||||
const current = conv.title || conv.preview || "";
|
||||
const next = window.prompt("Rename conversation:", current);
|
||||
if (next == null) return;
|
||||
const title = next.trim();
|
||||
if (!title || title === current) return;
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/conversations/${conv.id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ title }),
|
||||
});
|
||||
if (response.ok) loadConversations(search);
|
||||
} catch (error) {
|
||||
console.error("Failed to rename conversation:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteConversation = async (e, conversationId) => {
|
||||
e.stopPropagation();
|
||||
if (!window.confirm("Delete this conversation?")) return;
|
||||
@@ -108,9 +130,9 @@ function App() {
|
||||
};
|
||||
|
||||
const navItems = [
|
||||
{ key: "chatbot", label: "💬 Chatbot" },
|
||||
{ key: "playbook", label: "📖 Playbook" },
|
||||
{ key: "models", label: "🤖 Models" },
|
||||
{ key: "chatbot", label: "💬 Chat" },
|
||||
{ key: "playbook", label: "📖 Playbooks" },
|
||||
{ key: "models", label: "🤖 Models", badge: isModelPulling },
|
||||
{ key: "memory", label: "🧠 Memory" },
|
||||
{ key: "settings", label: "⚙️ Settings" },
|
||||
];
|
||||
@@ -120,6 +142,7 @@ function App() {
|
||||
{/* Sidebar */}
|
||||
<aside style={{
|
||||
width: "260px",
|
||||
flexShrink: 0,
|
||||
background: "#0a0a0a",
|
||||
borderRight: "1px solid #333",
|
||||
display: "flex",
|
||||
@@ -138,6 +161,9 @@ function App() {
|
||||
}}>
|
||||
<img src="/n small.png" alt="Logo" style={{ width: "48px", height: "48px", objectFit: "contain", borderRadius: "8px" }} />
|
||||
<h1 style={{ fontSize: "1rem", margin: 0, color: "#007acc", textAlign: "center" }}>NexusOS</h1>
|
||||
{version && (
|
||||
<span style={{ fontSize: "0.7rem", color: "#666", marginTop: "-0.5rem", letterSpacing: "0.02em" }}>v{version}</span>
|
||||
)}
|
||||
|
||||
{/* Status Indicator */}
|
||||
<div style={{ position: "relative" }}>
|
||||
@@ -197,9 +223,22 @@ function App() {
|
||||
fontSize: "0.85rem",
|
||||
textAlign: "left",
|
||||
transition: "all 0.15s",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
{item.badge && (
|
||||
<span style={{
|
||||
width: "8px",
|
||||
height: "8px",
|
||||
borderRadius: "50%",
|
||||
background: "#28a745",
|
||||
flexShrink: 0,
|
||||
animation: "pulse 1.2s ease-in-out infinite",
|
||||
}} />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
@@ -284,13 +323,30 @@ function App() {
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}>
|
||||
{conv.preview || "Conversation"}
|
||||
{conv.title || conv.preview || "Conversation"}
|
||||
</div>
|
||||
<div style={{ fontSize: "0.7rem", color: "#666", marginTop: "0.1rem" }}>
|
||||
{timeAgo(conv.timestamp)}
|
||||
</div>
|
||||
</div>
|
||||
{isHover && (
|
||||
<>
|
||||
<button
|
||||
onClick={(e) => renameConversation(e, conv)}
|
||||
title="Rename"
|
||||
style={{
|
||||
padding: "0.15rem 0.4rem",
|
||||
background: "transparent",
|
||||
color: "#888",
|
||||
border: "1px solid #333",
|
||||
borderRadius: "4px",
|
||||
cursor: "pointer",
|
||||
fontSize: "0.7rem",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
✎
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => deleteConversation(e, conv.id)}
|
||||
title="Delete"
|
||||
@@ -307,6 +363,7 @@ function App() {
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -317,19 +374,26 @@ function App() {
|
||||
</aside>
|
||||
|
||||
{/* Main Content */}
|
||||
<main style={{ flexGrow: 1, padding: "1.5rem", display: "flex", flexDirection: "column", overflowY: "auto", minHeight: 0 }}>
|
||||
{currentPage === "chatbot" ? (
|
||||
<main style={{ flexGrow: 1, padding: "1.5rem", display: "flex", flexDirection: "column", overflowY: "auto", minHeight: 0, minWidth: 0 }}>
|
||||
{/* Models stays mounted so active downloads survive page navigation */}
|
||||
<div style={{
|
||||
display: currentPage === "models" ? "flex" : "none",
|
||||
flexDirection: "column",
|
||||
flexGrow: 1,
|
||||
minHeight: 0,
|
||||
}}>
|
||||
<Models onPullStateChange={setIsModelPulling} />
|
||||
</div>
|
||||
{currentPage === "chatbot" && (
|
||||
<Chatbot
|
||||
status={status}
|
||||
conversationId={activeConversationId}
|
||||
setConversationId={setActiveConversationId}
|
||||
onConversationChanged={() => loadConversations(search)}
|
||||
/>
|
||||
)
|
||||
: currentPage === "playbook" ? <Playbook status={status} />
|
||||
: currentPage === "models" ? <Models status={status} />
|
||||
: currentPage === "memory" ? <Memory />
|
||||
: <Settings />}
|
||||
)}
|
||||
{currentPage === "playbook" && <Playbook />}
|
||||
{currentPage === "memory" && <Memory />}
|
||||
{currentPage === "settings" && <Settings />}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useState, useRef, useEffect } from "react";
|
||||
import { API_BASE } from "./config";
|
||||
import { Markdown } from "./Markdown";
|
||||
|
||||
export function Chatbot({ status, conversationId, setConversationId, onConversationChanged }) {
|
||||
export function Chatbot({ conversationId, setConversationId, onConversationChanged }) {
|
||||
const [messages, setMessages] = useState([]);
|
||||
const [input, setInput] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -67,7 +67,7 @@ export function Chatbot({ status, conversationId, setConversationId, onConversat
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ model }),
|
||||
});
|
||||
} catch {}
|
||||
} catch { /* persisting the model choice is best-effort */ }
|
||||
};
|
||||
|
||||
const startNewChat = () => {
|
||||
@@ -145,7 +145,18 @@ export function Chatbot({ status, conversationId, setConversationId, onConversat
|
||||
if (!payload.trim()) { pendingEventType = null; continue; }
|
||||
|
||||
if (pendingEventType === "meta") {
|
||||
try { setLastStats(JSON.parse(payload)); } catch { /* ignore */ }
|
||||
try {
|
||||
const stats = JSON.parse(payload);
|
||||
setLastStats(stats);
|
||||
// Tag this message with the model that answered
|
||||
if (stats.model) {
|
||||
setMessages(prev => {
|
||||
const updated = [...prev];
|
||||
updated[assistantIndex] = { ...updated[assistantIndex], model: stats.model };
|
||||
return updated;
|
||||
});
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
pendingEventType = null;
|
||||
continue;
|
||||
}
|
||||
@@ -158,6 +169,18 @@ export function Chatbot({ status, conversationId, setConversationId, onConversat
|
||||
pendingEventType = null;
|
||||
continue;
|
||||
}
|
||||
if (pendingEventType === "done") {
|
||||
// Answer is complete; re-enable input while the backend finishes
|
||||
// slow post-processing (title, memory) on the still-open stream.
|
||||
setLoading(false);
|
||||
pendingEventType = null;
|
||||
continue;
|
||||
}
|
||||
if (pendingEventType === "title") {
|
||||
if (onConversationChanged) onConversationChanged();
|
||||
pendingEventType = null;
|
||||
continue;
|
||||
}
|
||||
if (pendingEventType === "error") {
|
||||
try {
|
||||
const err = JSON.parse(payload);
|
||||
@@ -171,11 +194,14 @@ export function Chatbot({ status, conversationId, setConversationId, onConversat
|
||||
}
|
||||
pendingEventType = null;
|
||||
|
||||
let token = payload;
|
||||
try { token = JSON.parse(payload); } catch { /* plain text fallback */ }
|
||||
|
||||
setMessages(prev => {
|
||||
const updated = [...prev];
|
||||
updated[assistantIndex] = {
|
||||
...updated[assistantIndex],
|
||||
content: (updated[assistantIndex].content || "") + payload,
|
||||
content: (updated[assistantIndex].content || "") + token,
|
||||
};
|
||||
return updated;
|
||||
});
|
||||
@@ -368,12 +394,14 @@ export function Chatbot({ status, conversationId, setConversationId, onConversat
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
maxWidth: "70%",
|
||||
maxWidth: msg.role === "user" ? "70%" : "90%",
|
||||
width: msg.role === "assistant" ? "100%" : undefined,
|
||||
padding: "0.9rem",
|
||||
borderRadius: "10px",
|
||||
background: msg.role === "user" ? "#007acc" : "#262626",
|
||||
color: "#eee",
|
||||
wordWrap: "break-word",
|
||||
boxSizing: "border-box",
|
||||
}}
|
||||
>
|
||||
{msg.role === "user"
|
||||
|
||||
@@ -1,43 +1,118 @@
|
||||
import { useState } from "react";
|
||||
|
||||
// Parse content into an array of {type, value, lang, streaming} blocks.
|
||||
// Handles:
|
||||
// - ```lang\ncode``` (well-formed)
|
||||
// - ```code``` (no language, no newline — model formatting bug)
|
||||
// - ```lang\ncode (unclosed — streaming in progress)
|
||||
function parseBlocks(content) {
|
||||
const blocks = [];
|
||||
let i = 0;
|
||||
|
||||
while (i < content.length) {
|
||||
const fenceStart = content.indexOf("```", i);
|
||||
|
||||
if (fenceStart === -1) {
|
||||
if (i < content.length) blocks.push({ type: "text", value: content.slice(i) });
|
||||
break;
|
||||
}
|
||||
|
||||
if (fenceStart > i) blocks.push({ type: "text", value: content.slice(i, fenceStart) });
|
||||
|
||||
let j = fenceStart + 3;
|
||||
let lang = "";
|
||||
|
||||
// Language specifier is valid only when word-chars are followed by a newline.
|
||||
// If there's no newline (e.g. ```pythonprint(...)) treat everything as code.
|
||||
const langMatch = content.slice(j).match(/^(\w+)(\r?\n)/);
|
||||
if (langMatch) {
|
||||
lang = langMatch[1];
|
||||
j += langMatch[0].length;
|
||||
} else if (/^\r?\n/.test(content.slice(j))) {
|
||||
j += content[j] === "\r" ? 2 : 1;
|
||||
}
|
||||
// else: no newline — leave j as-is, lang stays ""
|
||||
|
||||
const closeIdx = content.indexOf("```", j);
|
||||
if (closeIdx === -1) {
|
||||
blocks.push({ type: "code", lang, value: content.slice(j), streaming: true });
|
||||
i = content.length;
|
||||
} else {
|
||||
blocks.push({ type: "code", lang, value: content.slice(j, closeIdx).replace(/\n$/, ""), streaming: false });
|
||||
i = closeIdx + 3;
|
||||
}
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
export function Markdown({ content }) {
|
||||
if (!content) return null;
|
||||
|
||||
const blocks = [];
|
||||
const codeBlockRe = /```(\w*)\n?([\s\S]*?)```/g;
|
||||
let last = 0;
|
||||
let match;
|
||||
|
||||
while ((match = codeBlockRe.exec(content)) !== null) {
|
||||
if (match.index > last) {
|
||||
blocks.push({ type: "text", value: content.slice(last, match.index) });
|
||||
}
|
||||
blocks.push({ type: "code", lang: match[1], value: match[2] });
|
||||
last = match.index + match[0].length;
|
||||
}
|
||||
if (last < content.length) {
|
||||
blocks.push({ type: "text", value: content.slice(last) });
|
||||
}
|
||||
|
||||
const blocks = parseBlocks(content);
|
||||
return (
|
||||
<div style={{ lineHeight: "1.6" }}>
|
||||
{blocks.map((block, i) =>
|
||||
block.type === "code" ? (
|
||||
<pre key={i} style={{
|
||||
block.type === "code"
|
||||
? <CodeBlock key={i} lang={block.lang} value={block.value} streaming={block.streaming} />
|
||||
: <TextBlock key={i} text={block.value} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CodeBlock({ lang, value, streaming }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const copy = () => {
|
||||
navigator.clipboard.writeText(value.trimEnd()).then(() => {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1500);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
background: "#0d0d0d",
|
||||
border: "1px solid #2a2a2a",
|
||||
borderRadius: "6px",
|
||||
margin: "0.5rem 0",
|
||||
overflow: "hidden",
|
||||
}}>
|
||||
<div style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: "0.3rem 0.75rem",
|
||||
background: "#161616",
|
||||
borderBottom: "1px solid #2a2a2a",
|
||||
}}>
|
||||
<span style={{ fontSize: "0.75rem", color: "#666", fontFamily: "monospace" }}>
|
||||
{lang || "code"}
|
||||
{streaming && <span style={{ color: "#444", marginLeft: "0.5rem" }}>●</span>}
|
||||
</span>
|
||||
{!streaming && (
|
||||
<button onClick={copy} style={{
|
||||
background: "transparent",
|
||||
border: "none",
|
||||
color: copied ? "#4caf50" : "#555",
|
||||
cursor: "pointer",
|
||||
fontSize: "0.75rem",
|
||||
padding: "0.1rem 0.3rem",
|
||||
}}>
|
||||
{copied ? "Copied!" : "Copy"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<pre style={{
|
||||
padding: "0.75rem 1rem",
|
||||
overflowX: "auto",
|
||||
fontSize: "0.85rem",
|
||||
lineHeight: "1.5",
|
||||
margin: "0.5rem 0",
|
||||
margin: 0,
|
||||
fontFamily: "monospace",
|
||||
}}>
|
||||
<code>{block.value.trimEnd()}</code>
|
||||
<code>{value.trimEnd()}</code>
|
||||
</pre>
|
||||
) : (
|
||||
<TextBlock key={i} text={block.value} />
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -50,7 +125,6 @@ function TextBlock({ text }) {
|
||||
while (i < lines.length) {
|
||||
const line = lines[i];
|
||||
|
||||
// Heading
|
||||
const hMatch = line.match(/^(#{1,3})\s+(.+)/);
|
||||
if (hMatch) {
|
||||
const level = hMatch[1].length;
|
||||
@@ -60,11 +134,9 @@ function TextBlock({ text }) {
|
||||
{inlineMarkdown(hMatch[2])}
|
||||
</div>
|
||||
);
|
||||
i++;
|
||||
continue;
|
||||
i++; continue;
|
||||
}
|
||||
|
||||
// Unordered list
|
||||
if (/^[-*]\s+/.test(line)) {
|
||||
const items = [];
|
||||
while (i < lines.length && /^[-*]\s+/.test(lines[i])) {
|
||||
@@ -75,7 +147,6 @@ function TextBlock({ text }) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ordered list
|
||||
if (/^\d+\.\s+/.test(line)) {
|
||||
const items = [];
|
||||
while (i < lines.length && /^\d+\.\s+/.test(lines[i])) {
|
||||
@@ -86,21 +157,16 @@ function TextBlock({ text }) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Horizontal rule
|
||||
if (/^---+$/.test(line.trim())) {
|
||||
elements.push(<hr key={i} style={{ border: "none", borderTop: "1px solid #333", margin: "0.5rem 0" }} />);
|
||||
i++;
|
||||
continue;
|
||||
i++; continue;
|
||||
}
|
||||
|
||||
// Empty line → small gap
|
||||
if (line.trim() === "") {
|
||||
elements.push(<div key={i} style={{ height: "0.4rem" }} />);
|
||||
i++;
|
||||
continue;
|
||||
i++; continue;
|
||||
}
|
||||
|
||||
// Regular paragraph line
|
||||
elements.push(<div key={i}>{inlineMarkdown(line)}</div>);
|
||||
i++;
|
||||
}
|
||||
@@ -116,7 +182,6 @@ function inlineMarkdown(text) {
|
||||
|
||||
while ((m = re.exec(text)) !== null) {
|
||||
if (m.index > last) parts.push(text.slice(last, m.index));
|
||||
|
||||
if (m[2] !== undefined) {
|
||||
parts.push(<strong key={m.index}>{m[2]}</strong>);
|
||||
} else if (m[3] !== undefined) {
|
||||
|
||||
@@ -13,6 +13,28 @@ function groupBySection(items) {
|
||||
return map;
|
||||
}
|
||||
|
||||
function indentLevelOf(text) {
|
||||
const m = text.match(/^( *)/);
|
||||
return Math.floor((m ? m[0].length : 0) / 2);
|
||||
}
|
||||
|
||||
// Collapse a section's items into top-level "groups": each group is a parent
|
||||
// (indent 0) plus its consecutive indented children.
|
||||
function computeGroups(sectionItems) {
|
||||
const groups = [];
|
||||
const groupIndexById = {};
|
||||
for (const item of sectionItems) {
|
||||
const lvl = indentLevelOf(item.text);
|
||||
if (lvl === 0 || groups.length === 0) {
|
||||
groups.push([item]);
|
||||
} else {
|
||||
groups[groups.length - 1].push(item);
|
||||
}
|
||||
groupIndexById[item.id] = groups.length - 1;
|
||||
}
|
||||
return { groups, groupIndexById };
|
||||
}
|
||||
|
||||
export function Memory() {
|
||||
const [items, setItems] = useState([]);
|
||||
const [editing, setEditing] = useState(null); // { id, section, text } or null
|
||||
@@ -20,6 +42,8 @@ export function Memory() {
|
||||
const [newSection, setNewSection] = useState("");
|
||||
const [showNewSection, setShowNewSection] = useState(false);
|
||||
const [message, setMessage] = useState("");
|
||||
const [draggingId, setDraggingId] = useState(null);
|
||||
const [dragOver, setDragOver] = useState(null); // { id, position: "before"|"after" }
|
||||
|
||||
const loadItems = useCallback(async () => {
|
||||
try {
|
||||
@@ -36,22 +60,30 @@ export function Memory() {
|
||||
const flash = (msg) => { setMessage(msg); setTimeout(() => setMessage(""), 3000); };
|
||||
|
||||
const saveNew = async (section, text) => {
|
||||
if (!text.trim()) return;
|
||||
// Split on newlines so a pasted list becomes one memory per line (leading
|
||||
// spaces kept for nesting). Direct write — no LLM curator, saves every line.
|
||||
const lines = text.split("\n").map(l => l.replace(/\s+$/, "")).filter(l => l.trim());
|
||||
if (!lines.length) return;
|
||||
let ok = 0;
|
||||
for (const line of lines) {
|
||||
const res = await fetch(`${API_BASE}/memory`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ section, text: text.trim() }),
|
||||
body: JSON.stringify({ section, text: line }),
|
||||
});
|
||||
if (res.ok) { loadItems(); setAdding(null); flash("Saved."); }
|
||||
if (res.ok) ok++;
|
||||
}
|
||||
if (ok) { loadItems(); setAdding(null); flash(ok === 1 ? "Saved." : `Saved ${ok} items.`); }
|
||||
else flash("Save failed.");
|
||||
};
|
||||
|
||||
const saveEdit = async (id, section, text) => {
|
||||
if (!text.trim()) return;
|
||||
const cleaned = text.replace(/\s+$/, "");
|
||||
if (!cleaned.trim()) return;
|
||||
const res = await fetch(`${API_BASE}/memory/${id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ section, text: text.trim() }),
|
||||
body: JSON.stringify({ section, text: cleaned }),
|
||||
});
|
||||
if (res.ok) { loadItems(); setEditing(null); flash("Updated."); }
|
||||
else flash("Update failed.");
|
||||
@@ -64,6 +96,154 @@ export function Memory() {
|
||||
else flash("Delete failed.");
|
||||
};
|
||||
|
||||
const reorderSection = async (section, ids) => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/memory/reorder`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ section, ids }),
|
||||
});
|
||||
const result = await res.json();
|
||||
if (!res.ok || result.ok === false) {
|
||||
throw new Error("Memory reorder failed");
|
||||
}
|
||||
loadItems();
|
||||
} catch (err) {
|
||||
setMessage(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
const moveItem = (id, direction) => {
|
||||
const item = items.find(i => i.id === id);
|
||||
if (!item) return;
|
||||
const section = item.section || "General";
|
||||
const sectionItems = items.filter(i => (i.section || "General") === section);
|
||||
const { groups, groupIndexById } = computeGroups(sectionItems);
|
||||
const lvl = indentLevelOf(item.text);
|
||||
|
||||
if (lvl === 0) {
|
||||
// Swap whole group with previous/next group
|
||||
const gIdx = groupIndexById[id];
|
||||
const tIdx = direction === "up" ? gIdx - 1 : gIdx + 1;
|
||||
if (tIdx < 0 || tIdx >= groups.length) return;
|
||||
const newGroups = [...groups];
|
||||
[newGroups[gIdx], newGroups[tIdx]] = [newGroups[tIdx], newGroups[gIdx]];
|
||||
reorderSection(section, newGroups.flat().map(i => i.id));
|
||||
} else {
|
||||
// Swap child with previous/next sibling within same group; never past the parent
|
||||
const gIdx = groupIndexById[id];
|
||||
const group = groups[gIdx];
|
||||
const localIdx = group.findIndex(i => i.id === id);
|
||||
const tLocal = direction === "up" ? localIdx - 1 : localIdx + 1;
|
||||
if (tLocal <= 0 || tLocal >= group.length) return; // tLocal === 0 is the parent
|
||||
const newGroup = [...group];
|
||||
[newGroup[localIdx], newGroup[tLocal]] = [newGroup[tLocal], newGroup[localIdx]];
|
||||
const newGroups = [...groups];
|
||||
newGroups[gIdx] = newGroup;
|
||||
reorderSection(section, newGroups.flat().map(i => i.id));
|
||||
}
|
||||
};
|
||||
|
||||
const onItemDragStart = (e, item) => {
|
||||
setDraggingId(item.id);
|
||||
e.dataTransfer.effectAllowed = "move";
|
||||
try { e.dataTransfer.setData("text/plain", item.id); } catch { /* some browsers restrict */ }
|
||||
};
|
||||
|
||||
const onItemDragOver = (e, targetItem, section) => {
|
||||
if (!draggingId || draggingId === targetItem.id) return;
|
||||
const dragged = items.find(i => i.id === draggingId);
|
||||
if (!dragged || (dragged.section || "General") !== section) return;
|
||||
|
||||
const sectionItems = items.filter(i => (i.section || "General") === section);
|
||||
const { groups, groupIndexById } = computeGroups(sectionItems);
|
||||
const draggedLevel = indentLevelOf(dragged.text);
|
||||
const targetGroupIdx = groupIndexById[targetItem.id];
|
||||
const draggedGroupIdx = groupIndexById[dragged.id];
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
const inUpperHalf = e.clientY < rect.top + rect.height / 2;
|
||||
|
||||
if (draggedLevel === 0) {
|
||||
// Top-level drag: snap to group boundaries; ignore hover over own group
|
||||
if (targetGroupIdx === draggedGroupIdx) {
|
||||
if (dragOver) setDragOver(null);
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = "move";
|
||||
const targetGroup = groups[targetGroupIdx];
|
||||
const isTargetParent = targetItem.id === targetGroup[0].id;
|
||||
const position = (isTargetParent && inUpperHalf) ? "before" : "after";
|
||||
if (dragOver?.kind !== "group" || dragOver?.section !== section || dragOver?.groupIdx !== targetGroupIdx || dragOver?.position !== position) {
|
||||
setDragOver({ kind: "group", section, groupIdx: targetGroupIdx, position });
|
||||
}
|
||||
} else {
|
||||
// Child drag: confined to its own parent group
|
||||
if (targetGroupIdx !== draggedGroupIdx) {
|
||||
if (dragOver) setDragOver(null);
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = "move";
|
||||
const targetGroup = groups[targetGroupIdx];
|
||||
const isTargetParent = targetItem.id === targetGroup[0].id;
|
||||
let position = inUpperHalf ? "before" : "after";
|
||||
if (isTargetParent) position = "after"; // child can't go before its parent
|
||||
if (dragOver?.kind !== "item" || dragOver?.section !== section || dragOver?.id !== targetItem.id || dragOver?.position !== position) {
|
||||
setDragOver({ kind: "item", section, id: targetItem.id, position });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onItemDrop = (e, targetItem, section) => {
|
||||
e.preventDefault();
|
||||
const dragged = items.find(i => i.id === draggingId);
|
||||
if (!dragged || dragged.id === targetItem.id || (dragged.section || "General") !== section || !dragOver) {
|
||||
setDraggingId(null);
|
||||
setDragOver(null);
|
||||
return;
|
||||
}
|
||||
const sectionItems = items.filter(i => (i.section || "General") === section);
|
||||
const { groups, groupIndexById } = computeGroups(sectionItems);
|
||||
const draggedLevel = indentLevelOf(dragged.text);
|
||||
|
||||
let block, anchorId, after;
|
||||
if (draggedLevel === 0 && dragOver.kind === "group") {
|
||||
block = groups[groupIndexById[dragged.id]];
|
||||
const targetGroup = groups[dragOver.groupIdx];
|
||||
anchorId = dragOver.position === "before" ? targetGroup[0].id : targetGroup[targetGroup.length - 1].id;
|
||||
after = dragOver.position === "after";
|
||||
} else if (draggedLevel > 0 && dragOver.kind === "item") {
|
||||
block = [dragged];
|
||||
anchorId = dragOver.id;
|
||||
after = dragOver.position === "after";
|
||||
} else {
|
||||
setDraggingId(null);
|
||||
setDragOver(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const blockIds = new Set(block.map(b => b.id));
|
||||
const remainder = sectionItems.filter(i => !blockIds.has(i.id));
|
||||
let insertIdx = remainder.findIndex(i => i.id === anchorId);
|
||||
if (insertIdx < 0) {
|
||||
setDraggingId(null);
|
||||
setDragOver(null);
|
||||
return;
|
||||
}
|
||||
if (after) insertIdx += 1;
|
||||
const newOrder = [...remainder.slice(0, insertIdx), ...block, ...remainder.slice(insertIdx)];
|
||||
|
||||
setDraggingId(null);
|
||||
setDragOver(null);
|
||||
reorderSection(section, newOrder.map(i => i.id));
|
||||
};
|
||||
|
||||
const onItemDragEnd = () => {
|
||||
setDraggingId(null);
|
||||
setDragOver(null);
|
||||
};
|
||||
|
||||
const addSection = () => {
|
||||
const sec = newSection.trim();
|
||||
if (!sec) return;
|
||||
@@ -98,7 +278,7 @@ export function Memory() {
|
||||
});
|
||||
|
||||
return (
|
||||
<div style={{ flexGrow: 1, maxWidth: "860px" }}>
|
||||
<div style={{ flexGrow: 1 }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "1.25rem" }}>
|
||||
<div>
|
||||
<h2 style={{ margin: 0, fontSize: "1.1rem" }}>Memory</h2>
|
||||
@@ -148,9 +328,49 @@ export function Memory() {
|
||||
</div>
|
||||
|
||||
{/* Bullet items */}
|
||||
{(() => {
|
||||
const sectionItems = grouped[section] || [];
|
||||
const { groups, groupIndexById } = computeGroups(sectionItems);
|
||||
return (
|
||||
<div style={{ paddingLeft: "1rem", borderLeft: "2px solid #222" }}>
|
||||
{(grouped[section] || []).map(item => (
|
||||
<div key={item.id} style={{ marginBottom: "0.4rem" }}>
|
||||
{sectionItems.map((item) => {
|
||||
const indentMatch = item.text.match(/^( *)(.*)$/s);
|
||||
const indentLevel = indentMatch ? Math.floor(indentMatch[1].length / 2) : 0;
|
||||
const displayText = indentMatch ? indentMatch[2] : item.text;
|
||||
const gIdx = groupIndexById[item.id];
|
||||
const group = groups[gIdx];
|
||||
const localIdx = group.findIndex(i => i.id === item.id);
|
||||
const isParentRow = localIdx === 0;
|
||||
const isLastInGroup = localIdx === group.length - 1;
|
||||
const arrowUpDisabled = indentLevel === 0 ? gIdx === 0 : localIdx <= 1;
|
||||
const arrowDownDisabled = indentLevel === 0 ? gIdx === groups.length - 1 : isLastInGroup;
|
||||
const isDragging = draggingId === item.id;
|
||||
let showLineBefore = false, showLineAfter = false;
|
||||
if (dragOver && dragOver.section === section) {
|
||||
if (dragOver.kind === "group" && dragOver.groupIdx === gIdx) {
|
||||
if (dragOver.position === "before" && isParentRow) showLineBefore = true;
|
||||
if (dragOver.position === "after" && isLastInGroup) showLineAfter = true;
|
||||
} else if (dragOver.kind === "item" && dragOver.id === item.id) {
|
||||
if (dragOver.position === "before") showLineBefore = true;
|
||||
else showLineAfter = true;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
draggable={!editing}
|
||||
onDragStart={(e) => onItemDragStart(e, item)}
|
||||
onDragOver={(e) => onItemDragOver(e, item, section)}
|
||||
onDrop={(e) => onItemDrop(e, item, section)}
|
||||
onDragEnd={onItemDragEnd}
|
||||
style={{
|
||||
marginBottom: "0.4rem",
|
||||
paddingLeft: `${indentLevel * 1.5}rem`,
|
||||
borderTop: showLineBefore ? "2px solid #4db8ff" : "2px solid transparent",
|
||||
borderBottom: showLineAfter ? "2px solid #4db8ff" : "2px solid transparent",
|
||||
opacity: isDragging ? 0.4 : 1,
|
||||
}}
|
||||
>
|
||||
{editing?.id === item.id ? (
|
||||
<EditRow
|
||||
initial={editing.text}
|
||||
@@ -160,17 +380,29 @@ export function Memory() {
|
||||
btnStyle={btnStyle}
|
||||
/>
|
||||
) : (
|
||||
<div style={{ display: "flex", alignItems: "flex-start", gap: "0.5rem", group: true }}>
|
||||
<div
|
||||
onDoubleClick={() => setEditing({ id: item.id, section, text: item.text })}
|
||||
title="Drag to reorder · double-click to edit"
|
||||
style={{ display: "flex", alignItems: "flex-start", gap: "0.5rem", cursor: "grab" }}
|
||||
>
|
||||
<span style={{ color: "#aaa", marginTop: "0.1rem", flexShrink: 0 }}>-</span>
|
||||
<span style={{ color: "#ddd", fontSize: "0.9rem", flexGrow: 1 }}>{item.text}</span>
|
||||
<span style={{ color: "#ddd", fontSize: "0.9rem", flexGrow: 1 }}>{displayText}</span>
|
||||
<div style={{ display: "flex", gap: "0.3rem", flexShrink: 0, opacity: 0.4 }}
|
||||
onMouseEnter={e => e.currentTarget.style.opacity = 1}
|
||||
onMouseLeave={e => e.currentTarget.style.opacity = 0.4}
|
||||
>
|
||||
<button
|
||||
onClick={() => setEditing({ id: item.id, section, text: item.text })}
|
||||
style={{ ...btnStyle("#333"), fontSize: "0.72rem", padding: "0.2rem 0.5rem" }}
|
||||
>edit</button>
|
||||
onClick={() => moveItem(item.id, "up")}
|
||||
disabled={arrowUpDisabled}
|
||||
title="Move up"
|
||||
style={{ ...btnStyle("#333"), fontSize: "0.72rem", padding: "0.2rem 0.4rem", opacity: arrowUpDisabled ? 0.3 : 1, cursor: arrowUpDisabled ? "default" : "pointer" }}
|
||||
>▲</button>
|
||||
<button
|
||||
onClick={() => moveItem(item.id, "down")}
|
||||
disabled={arrowDownDisabled}
|
||||
title="Move down"
|
||||
style={{ ...btnStyle("#333"), fontSize: "0.72rem", padding: "0.2rem 0.4rem", opacity: arrowDownDisabled ? 0.3 : 1, cursor: arrowDownDisabled ? "default" : "pointer" }}
|
||||
>▼</button>
|
||||
<button
|
||||
onClick={() => deleteItem(item.id)}
|
||||
style={{ ...btnStyle("#5a1a1a"), fontSize: "0.72rem", padding: "0.2rem 0.5rem" }}
|
||||
@@ -179,7 +411,8 @@ export function Memory() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Inline add form */}
|
||||
{adding?.section === section && (
|
||||
@@ -191,6 +424,8 @@ export function Memory() {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -203,19 +438,34 @@ export function Memory() {
|
||||
);
|
||||
}
|
||||
|
||||
function handleIndentKey(e, setText) {
|
||||
if (e.key !== "Tab") return false;
|
||||
e.preventDefault();
|
||||
if (e.shiftKey) {
|
||||
setText(t => t.startsWith(" ") ? t.slice(2) : t);
|
||||
} else {
|
||||
setText(t => " " + t);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function AddRow({ onSave, onCancel, inputStyle, btnStyle }) {
|
||||
const [text, setText] = useState("");
|
||||
return (
|
||||
<div style={{ display: "flex", gap: "0.5rem", alignItems: "center", marginTop: "0.4rem" }}>
|
||||
<span style={{ color: "#555", flexShrink: 0 }}>-</span>
|
||||
<input
|
||||
<textarea
|
||||
autoFocus
|
||||
type="text"
|
||||
placeholder="New item..."
|
||||
rows={1}
|
||||
placeholder="New item… (paste multiple lines to add several at once)"
|
||||
value={text}
|
||||
onChange={e => setText(e.target.value)}
|
||||
onKeyDown={e => { if (e.key === "Enter") onSave(text); if (e.key === "Escape") onCancel(); }}
|
||||
style={{ ...inputStyle }}
|
||||
onKeyDown={e => {
|
||||
if (handleIndentKey(e, setText)) return;
|
||||
if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); onSave(text); }
|
||||
if (e.key === "Escape") onCancel();
|
||||
}}
|
||||
style={{ ...inputStyle, resize: "vertical", fontFamily: "inherit", minHeight: "2.1rem" }}
|
||||
/>
|
||||
<button onClick={() => onSave(text)} style={btnStyle()}>Save</button>
|
||||
<button onClick={onCancel} style={btnStyle("#444")}>✕</button>
|
||||
@@ -233,7 +483,11 @@ function EditRow({ initial, onSave, onCancel, inputStyle, btnStyle }) {
|
||||
type="text"
|
||||
value={text}
|
||||
onChange={e => setText(e.target.value)}
|
||||
onKeyDown={e => { if (e.key === "Enter") onSave(text); if (e.key === "Escape") onCancel(); }}
|
||||
onKeyDown={e => {
|
||||
if (handleIndentKey(e, setText)) return;
|
||||
if (e.key === "Enter") onSave(text);
|
||||
if (e.key === "Escape") onCancel();
|
||||
}}
|
||||
style={{ ...inputStyle }}
|
||||
/>
|
||||
<button onClick={() => onSave(text)} style={btnStyle()}>Save</button>
|
||||
|
||||
@@ -1,28 +1,25 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { API_BASE } from "./config";
|
||||
|
||||
export function Models({ status }) {
|
||||
export function Models({ onPullStateChange }) {
|
||||
const [models, setModels] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [pulling, setPulling] = useState(false);
|
||||
const [modelName, setModelName] = useState("");
|
||||
const [pullProgress, setPullProgress] = useState("");
|
||||
const [ollamaStatus, setOllamaStatus] = useState("checking");
|
||||
|
||||
const checkOllamaStatus = async () => {
|
||||
const checkOllamaStatus = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/ollama/status`);
|
||||
const data = await response.json();
|
||||
setOllamaStatus(data.status);
|
||||
return data.status === "running";
|
||||
} catch {
|
||||
setOllamaStatus("unavailable");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const loadModels = async () => {
|
||||
const loadModels = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
try {
|
||||
@@ -35,14 +32,15 @@ export function Models({ status }) {
|
||||
const response = await fetch(`${API_BASE}/models/details`);
|
||||
if (!response.ok) throw new Error("Failed to fetch models");
|
||||
const data = await response.json();
|
||||
setModels(data.models || []);
|
||||
// Embedding models (nomic-embed-text) are infrastructure — hide them.
|
||||
setModels((data.models || []).filter((m) => !m.name.toLowerCase().includes("embed")));
|
||||
} catch (err) {
|
||||
setError(`Error loading models: ${err.message}`);
|
||||
setModels([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}, [checkOllamaStatus]);
|
||||
|
||||
const pullModel = async () => {
|
||||
if (!modelName.trim()) {
|
||||
@@ -51,6 +49,7 @@ export function Models({ status }) {
|
||||
}
|
||||
|
||||
setPulling(true);
|
||||
onPullStateChange?.(true);
|
||||
setError("");
|
||||
setPullProgress("");
|
||||
|
||||
@@ -99,6 +98,7 @@ export function Models({ status }) {
|
||||
setError(`Error pulling model: ${err.message}`);
|
||||
} finally {
|
||||
setPulling(false);
|
||||
onPullStateChange?.(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -123,8 +123,9 @@ export function Models({ status }) {
|
||||
useEffect(() => {
|
||||
loadModels();
|
||||
const interval = setInterval(loadModels, 30000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
}, [loadModels]);
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", height: "100%", flexGrow: 1 }}>
|
||||
@@ -214,7 +215,7 @@ export function Models({ status }) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Models List */}
|
||||
{/* Local Models List */}
|
||||
<div style={{ flexGrow: 1, overflowY: "auto", paddingRight: "0.5rem" }}>
|
||||
<h3 style={{ margin: "0 0 1rem 0", fontSize: "0.95rem", color: "#bbb" }}>
|
||||
Available Models ({models.length})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState, useCallback, useRef } from "react";
|
||||
import { API_BASE } from "./config";
|
||||
|
||||
export function Playbook({ status }) {
|
||||
export function Playbook() {
|
||||
const [playbooks, setPlaybooks] = useState([]);
|
||||
const [selectedId, setSelectedId] = useState(null);
|
||||
const [form, setForm] = useState({ title: "", goal: "", instructions: "", tags: "" });
|
||||
@@ -29,6 +29,7 @@ export function Playbook({ status }) {
|
||||
}
|
||||
}, []);
|
||||
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- async fetch on mount; setPlaybooks runs after await, not a synchronous cascading render
|
||||
useEffect(() => { loadPlaybooks(); }, [loadPlaybooks]);
|
||||
|
||||
const safeString = (v) => (v == null ? "" : String(v));
|
||||
@@ -184,7 +185,6 @@ export function Playbook({ status }) {
|
||||
// Optimistic UI update — reorder within filtered set, keep unfiltered playbooks intact
|
||||
setPlaybooks(prev => {
|
||||
const filteredSet = new Set(currentOrder);
|
||||
const unfiltered = prev.filter(p => !filteredSet.has(String(p.id)));
|
||||
const filteredMap = Object.fromEntries(prev.filter(p => filteredSet.has(String(p.id))).map(p => [String(p.id), p]));
|
||||
const reorderedFiltered = newFilteredOrder.map(id => filteredMap[id]).filter(Boolean);
|
||||
// Merge: place unfiltered items back in their original relative positions
|
||||
|
||||
@@ -6,6 +6,10 @@ const DEFAULTS = {
|
||||
temperature: 0.7,
|
||||
system_prompt: "",
|
||||
timeout: 120,
|
||||
gpu_offload: -1, // -1 = Auto; 0–100 = percent of layers forced onto the GPU
|
||||
memory_model: "mistral:latest", // curator model for memory extraction
|
||||
memory_gpu_offload: 0, // curator offload; 0 = all CPU/RAM (safe on small GPUs)
|
||||
memory_merge_threshold: 0.80, // 0 = off; else update near-duplicate facts in place
|
||||
};
|
||||
|
||||
export function Settings() {
|
||||
@@ -13,11 +17,25 @@ export function Settings() {
|
||||
const [saved, setSaved] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
// Icon branding state
|
||||
const [iconApps, setIconApps] = useState([]);
|
||||
const [iconFilter, setIconFilter] = useState("");
|
||||
const [iconFrac, setIconFrac] = useState(0.60);
|
||||
const [iconRing, setIconRing] = useState(false);
|
||||
const [selectedApps, setSelectedApps] = useState([]);
|
||||
const [brandQueue, setBrandQueue] = useState([]);
|
||||
const [isBranding, setIsBranding] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${API_BASE}/settings`)
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(d => { if (d) setForm(f => ({ ...f, ...d })); })
|
||||
.catch(() => {});
|
||||
|
||||
fetch(`${API_BASE}/icons/apps`)
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(d => { if (d) setIconApps(d.apps || []); })
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
const update = (key, value) => setForm(f => ({ ...f, [key]: value }));
|
||||
@@ -41,6 +59,73 @@ export function Settings() {
|
||||
|
||||
const reset = () => setForm(DEFAULTS);
|
||||
|
||||
// Icon branding logic
|
||||
const toggleAppSelect = (app) => {
|
||||
setSelectedApps(prev => {
|
||||
const exists = prev.find(a => a.icon_name === app.icon_name);
|
||||
if (exists) return prev.filter(a => a.icon_name !== app.icon_name);
|
||||
return [...prev, app];
|
||||
});
|
||||
};
|
||||
|
||||
const removeFromQueue = (icon_name) => {
|
||||
setSelectedApps(prev => prev.filter(a => a.icon_name !== icon_name));
|
||||
};
|
||||
|
||||
const brandAll = async () => {
|
||||
if (!selectedApps.length || isBranding) return;
|
||||
setIsBranding(true);
|
||||
const queue = selectedApps.map(a => ({ ...a, status: "pending" }));
|
||||
setBrandQueue(queue);
|
||||
|
||||
for (let i = 0; i < queue.length; i++) {
|
||||
const app = queue[i];
|
||||
setBrandQueue(prev => prev.map((a, idx) => idx === i ? { ...a, status: "running" } : a));
|
||||
try {
|
||||
const r = await fetch(`${API_BASE}/icons/brand`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
src_path: app.icon_path,
|
||||
output_name: app.icon_name,
|
||||
frac: iconFrac,
|
||||
nexus_ring: iconRing,
|
||||
reload: false,
|
||||
}),
|
||||
});
|
||||
if (!r.ok) throw new Error(await r.text());
|
||||
setBrandQueue(prev => prev.map((a, idx) => idx === i ? { ...a, status: "done" } : a));
|
||||
} catch (e) {
|
||||
setBrandQueue(prev => prev.map((a, idx) => idx === i ? { ...a, status: "error", err: e.message } : a));
|
||||
}
|
||||
}
|
||||
|
||||
// Apply cache once after all are done
|
||||
await fetch(`${API_BASE}/icons/apply`, { method: "POST" }).catch(() => {});
|
||||
|
||||
// Mark already_branded in the app list
|
||||
setIconApps(prev => prev.map(a => {
|
||||
if (selectedApps.find(s => s.icon_name === a.icon_name)) {
|
||||
return { ...a, already_branded: true };
|
||||
}
|
||||
return a;
|
||||
}));
|
||||
|
||||
setIsBranding(false);
|
||||
};
|
||||
|
||||
const filteredApps = iconApps.filter(a =>
|
||||
a.name.toLowerCase().includes(iconFilter.toLowerCase())
|
||||
);
|
||||
|
||||
const statusIcon = (status) => {
|
||||
if (status === "pending") return <span style={{ color: "#666" }}>○</span>;
|
||||
if (status === "running") return <span style={{ color: "#8cc63f" }}>⟳</span>;
|
||||
if (status === "done") return <span style={{ color: "#8cc63f" }}>✓</span>;
|
||||
if (status === "error") return <span style={{ color: "#f44336" }}>✗</span>;
|
||||
return null;
|
||||
};
|
||||
|
||||
const inputStyle = {
|
||||
width: "100%",
|
||||
padding: "0.6rem 0.75rem",
|
||||
@@ -69,8 +154,10 @@ export function Settings() {
|
||||
marginBottom: "1rem",
|
||||
};
|
||||
|
||||
const displayQueue = isBranding ? brandQueue : selectedApps;
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: "640px" }}>
|
||||
<div style={{ maxWidth: "720px" }}>
|
||||
<h2 style={{ margin: "0 0 1.25rem", fontSize: "1.1rem", color: "#eee" }}>Settings</h2>
|
||||
|
||||
{/* Generation */}
|
||||
@@ -99,6 +186,140 @@ export function Settings() {
|
||||
<span>2.0 — creative</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: "1.25rem" }}>
|
||||
<label style={labelStyle}>
|
||||
GPU Offload
|
||||
<span style={{ float: "right", color: "#b040c0", fontWeight: 600, textTransform: "none", letterSpacing: 0 }}>
|
||||
{form.gpu_offload < 0 ? "Auto" : `${form.gpu_offload}%`}
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
step="5"
|
||||
value={form.gpu_offload < 0 ? 100 : form.gpu_offload}
|
||||
disabled={form.gpu_offload < 0}
|
||||
onChange={e => update("gpu_offload", parseInt(e.target.value))}
|
||||
style={{ width: "100%", accentColor: "#b040c0", opacity: form.gpu_offload < 0 ? 0.4 : 1 }}
|
||||
/>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", fontSize: "0.72rem", color: "#555", marginTop: "0.2rem" }}>
|
||||
<span>0% — all CPU</span>
|
||||
<span>100% — all GPU</span>
|
||||
</div>
|
||||
<label style={{ display: "flex", alignItems: "center", gap: "0.5rem", cursor: "pointer", userSelect: "none", marginTop: "0.6rem" }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={form.gpu_offload < 0}
|
||||
onChange={e => update("gpu_offload", e.target.checked ? -1 : 50)}
|
||||
style={{ accentColor: "#8cc63f", width: "16px", height: "16px" }}
|
||||
/>
|
||||
<span style={{ fontSize: "0.82rem", color: form.gpu_offload < 0 ? "#8cc63f" : "#888" }}>
|
||||
Auto — fit as many layers as VRAM allows (recommended)
|
||||
</span>
|
||||
</label>
|
||||
<p style={{ margin: "0.4rem 0 0", fontSize: "0.72rem", color: "#555" }}>
|
||||
Lower values push more of the model onto the CPU — slower, but frees VRAM and reduces GPU load. Layer count is resolved per model.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Memory Curator */}
|
||||
<div style={sectionStyle}>
|
||||
<h3 style={{ margin: "0 0 1rem", fontSize: "0.95rem", color: "#bbb" }}>Memory Curator</h3>
|
||||
<p style={{ margin: "0 0 1rem", fontSize: "0.75rem", color: "#555" }}>
|
||||
The model that reviews each exchange and saves new facts to memory, in the background after every reply.
|
||||
</p>
|
||||
|
||||
<div style={{ marginBottom: "1rem" }}>
|
||||
<label style={labelStyle}>Curator Model</label>
|
||||
<input
|
||||
type="text"
|
||||
value={form.memory_model}
|
||||
onChange={e => update("memory_model", e.target.value)}
|
||||
placeholder="mistral:latest (blank = same model as chat)"
|
||||
style={inputStyle}
|
||||
/>
|
||||
<p style={{ margin: "0.35rem 0 0", fontSize: "0.75rem", color: "#555" }}>
|
||||
A dedicated model like <code>mistral:latest</code> extracts facts more reliably than a small chat model.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={labelStyle}>
|
||||
Curator GPU Offload
|
||||
<span style={{ float: "right", color: "#b040c0", fontWeight: 600, textTransform: "none", letterSpacing: 0 }}>
|
||||
{form.memory_gpu_offload < 0 ? "Auto" : `${form.memory_gpu_offload}%`}
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
step="5"
|
||||
value={form.memory_gpu_offload < 0 ? 100 : form.memory_gpu_offload}
|
||||
disabled={form.memory_gpu_offload < 0}
|
||||
onChange={e => update("memory_gpu_offload", parseInt(e.target.value))}
|
||||
style={{ width: "100%", accentColor: "#b040c0", opacity: form.memory_gpu_offload < 0 ? 0.4 : 1 }}
|
||||
/>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", fontSize: "0.72rem", color: "#555", marginTop: "0.2rem" }}>
|
||||
<span>0% — all CPU/RAM</span>
|
||||
<span>100% — all GPU</span>
|
||||
</div>
|
||||
<label style={{ display: "flex", alignItems: "center", gap: "0.5rem", cursor: "pointer", userSelect: "none", marginTop: "0.6rem" }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={form.memory_gpu_offload < 0}
|
||||
onChange={e => update("memory_gpu_offload", e.target.checked ? -1 : 0)}
|
||||
style={{ accentColor: "#8cc63f", width: "16px", height: "16px" }}
|
||||
/>
|
||||
<span style={{ fontSize: "0.82rem", color: form.memory_gpu_offload < 0 ? "#8cc63f" : "#888" }}>
|
||||
Auto — let Ollama fit layers to spare VRAM
|
||||
</span>
|
||||
</label>
|
||||
<p style={{ margin: "0.4rem 0 0", fontSize: "0.72rem", color: "#555" }}>
|
||||
Keep at <strong>0% (all CPU/RAM)</strong> on a small GPU so the curator doesn't evict the chat model — on a ~4 GB GPU that thrash makes extraction time out and silently save nothing. Raise it only if you have spare VRAM.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: "1.25rem" }}>
|
||||
<label style={{ display: "flex", alignItems: "center", gap: "0.5rem", cursor: "pointer", userSelect: "none" }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={form.memory_merge_threshold > 0}
|
||||
onChange={e => update("memory_merge_threshold", e.target.checked ? 0.80 : 0)}
|
||||
style={{ accentColor: "#b040c0", width: "16px", height: "16px" }}
|
||||
/>
|
||||
<span style={{ fontSize: "0.85rem", color: "#ccc", fontWeight: 600 }}>Merge similar facts (edit in place)</span>
|
||||
</label>
|
||||
<p style={{ margin: "0.4rem 0 0.7rem", fontSize: "0.72rem", color: "#555" }}>
|
||||
When a newly extracted fact closely matches an existing one, update that entry instead of adding a duplicate. Uses embeddings.
|
||||
</p>
|
||||
{form.memory_merge_threshold > 0 && (
|
||||
<>
|
||||
<label style={labelStyle}>
|
||||
Merge threshold
|
||||
<span style={{ float: "right", color: "#b040c0", fontWeight: 600, textTransform: "none", letterSpacing: 0 }}>
|
||||
{Number(form.memory_merge_threshold).toFixed(2)}
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0.60"
|
||||
max="0.98"
|
||||
step="0.01"
|
||||
value={form.memory_merge_threshold}
|
||||
onChange={e => update("memory_merge_threshold", parseFloat(e.target.value))}
|
||||
style={{ width: "100%", accentColor: "#b040c0" }}
|
||||
/>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", fontSize: "0.72rem", color: "#555", marginTop: "0.2rem" }}>
|
||||
<span>0.60 — merges more</span>
|
||||
<span>0.98 — only near-identical</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Behaviour */}
|
||||
@@ -143,6 +364,165 @@ export function Settings() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Icon Branding */}
|
||||
<div style={sectionStyle}>
|
||||
<h3 style={{ margin: "0 0 0.75rem", fontSize: "0.95rem", color: "#bbb" }}>App Icon Branding</h3>
|
||||
<p style={{ margin: "0 0 1rem", fontSize: "0.78rem", color: "#555" }}>
|
||||
Select apps on the left to composite their icon onto the NexusOS tile.
|
||||
</p>
|
||||
|
||||
{/* Controls row */}
|
||||
<div style={{ display: "flex", gap: "1.5rem", marginBottom: "1rem", alignItems: "center", flexWrap: "wrap" }}>
|
||||
<div style={{ flex: "1 1 180px" }}>
|
||||
<label style={labelStyle}>
|
||||
Logo size
|
||||
<span style={{ float: "right", color: "#b040c0", fontWeight: 600, textTransform: "none", letterSpacing: 0 }}>
|
||||
{Math.round(iconFrac * 100)}%
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0.30"
|
||||
max="0.85"
|
||||
step="0.05"
|
||||
value={iconFrac}
|
||||
onChange={e => setIconFrac(parseFloat(e.target.value))}
|
||||
style={{ width: "100%", accentColor: "#b040c0" }}
|
||||
/>
|
||||
</div>
|
||||
<label style={{ display: "flex", alignItems: "center", gap: "0.5rem", cursor: "pointer", userSelect: "none" }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={iconRing}
|
||||
onChange={e => setIconRing(e.target.checked)}
|
||||
style={{ accentColor: "#8cc63f", width: "16px", height: "16px" }}
|
||||
/>
|
||||
<span style={{ fontSize: "0.85rem", color: iconRing ? "#8cc63f" : "#888" }}>Nexus ring</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Shuttle */}
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.75rem" }}>
|
||||
{/* Left: app list */}
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "0.5rem" }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Filter apps…"
|
||||
value={iconFilter}
|
||||
onChange={e => setIconFilter(e.target.value)}
|
||||
style={{ ...inputStyle, padding: "0.4rem 0.6rem", fontSize: "0.82rem" }}
|
||||
/>
|
||||
<div style={{ height: "280px", overflowY: "auto", background: "#0d0d0d", border: "1px solid #2a2a2a", borderRadius: "8px" }}>
|
||||
{filteredApps.length === 0 && (
|
||||
<div style={{ padding: "1rem", color: "#444", fontSize: "0.82rem", textAlign: "center" }}>
|
||||
{iconApps.length === 0 ? "Loading…" : "No apps match"}
|
||||
</div>
|
||||
)}
|
||||
{filteredApps.map(app => {
|
||||
const isSelected = !!selectedApps.find(a => a.icon_name === app.icon_name);
|
||||
return (
|
||||
<div
|
||||
key={app.icon_name}
|
||||
onClick={() => !isBranding && !isSelected && app.icon_path && toggleAppSelect(app)}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "0.5rem",
|
||||
padding: "0.35rem 0.6rem",
|
||||
cursor: isBranding || isSelected || !app.icon_path ? "default" : "pointer",
|
||||
background: isSelected ? "rgba(176,64,192,0.15)" : "transparent",
|
||||
borderLeft: isSelected ? "2px solid #b040c0" : "2px solid transparent",
|
||||
opacity: !app.icon_path ? 0.35 : 1,
|
||||
}}
|
||||
onMouseEnter={e => { if (!isSelected && app.icon_path && !isBranding) e.currentTarget.style.background = "rgba(255,255,255,0.04)"; }}
|
||||
onMouseLeave={e => { if (!isSelected) e.currentTarget.style.background = "transparent"; }}
|
||||
>
|
||||
{app.icon_path ? (
|
||||
<img
|
||||
src={`${API_BASE}/icons/image?path=${encodeURIComponent(app.icon_path)}`}
|
||||
alt=""
|
||||
style={{ width: "22px", height: "22px", objectFit: "contain", flexShrink: 0 }}
|
||||
onError={e => { e.target.style.display = "none"; }}
|
||||
/>
|
||||
) : (
|
||||
<div style={{ width: "22px", height: "22px", background: "#1a1a1a", borderRadius: "4px", flexShrink: 0 }} />
|
||||
)}
|
||||
<span style={{ fontSize: "0.82rem", color: "#ccc", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
{app.name}
|
||||
</span>
|
||||
{app.already_branded && (
|
||||
<span style={{ marginLeft: "auto", color: "#8cc63f", fontSize: "0.72rem", flexShrink: 0 }}>✓</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: queue */}
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "0.5rem" }}>
|
||||
<div style={{ fontSize: "0.8rem", color: "#555", padding: "0.4rem 0", minHeight: "2rem" }}>
|
||||
{isBranding ? `Branding ${brandQueue.length} app${brandQueue.length !== 1 ? "s" : ""}…` : `${selectedApps.length} selected`}
|
||||
</div>
|
||||
<div style={{ height: "280px", overflowY: "auto", background: "#0d0d0d", border: "1px solid #2a2a2a", borderRadius: "8px" }}>
|
||||
{displayQueue.length === 0 && (
|
||||
<div style={{ padding: "1rem", color: "#333", fontSize: "0.82rem", textAlign: "center" }}>
|
||||
Click apps to add →
|
||||
</div>
|
||||
)}
|
||||
{displayQueue.map(app => (
|
||||
<div
|
||||
key={app.icon_name}
|
||||
style={{ display: "flex", alignItems: "center", gap: "0.5rem", padding: "0.35rem 0.6rem" }}
|
||||
>
|
||||
{isBranding ? (
|
||||
<span style={{ fontSize: "1rem", width: "18px", textAlign: "center", flexShrink: 0 }}>
|
||||
{statusIcon(app.status)}
|
||||
</span>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => removeFromQueue(app.icon_name)}
|
||||
style={{ background: "transparent", border: "none", color: "#555", cursor: "pointer", fontSize: "0.9rem", padding: "0 2px", lineHeight: 1, flexShrink: 0 }}
|
||||
onMouseEnter={e => e.target.style.color = "#f44336"}
|
||||
onMouseLeave={e => e.target.style.color = "#555"}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
<span style={{ fontSize: "0.82rem", color: isBranding && app.status === "done" ? "#8cc63f" : "#ccc", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
{app.name}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Brand button */}
|
||||
<div style={{ marginTop: "0.75rem", display: "flex", alignItems: "center", gap: "0.75rem" }}>
|
||||
<button
|
||||
onClick={brandAll}
|
||||
disabled={!selectedApps.length || isBranding}
|
||||
style={{
|
||||
padding: "0.6rem 1.25rem",
|
||||
background: selectedApps.length && !isBranding ? "#5e0066" : "#1a1a1a",
|
||||
color: selectedApps.length && !isBranding ? "#eee" : "#444",
|
||||
border: `1px solid ${selectedApps.length && !isBranding ? "#b040c0" : "#2a2a2a"}`,
|
||||
borderRadius: "8px",
|
||||
cursor: selectedApps.length && !isBranding ? "pointer" : "default",
|
||||
fontSize: "0.88rem",
|
||||
fontWeight: 500,
|
||||
transition: "background 0.15s",
|
||||
}}
|
||||
>
|
||||
{isBranding ? "Branding…" : `Brand ${selectedApps.length || ""} App${selectedApps.length !== 1 ? "s" : ""}`}
|
||||
</button>
|
||||
{!isBranding && brandQueue.length > 0 && brandQueue.every(a => a.status === "done") && (
|
||||
<span style={{ color: "#8cc63f", fontSize: "0.85rem" }}>Done — panel reloaded</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div style={{ display: "flex", gap: "0.75rem", alignItems: "center" }}>
|
||||
<button
|
||||
|
||||
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 8.5 KiB |
@@ -1,3 +1,8 @@
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.3; }
|
||||
}
|
||||
|
||||
:root {
|
||||
--text: #6b6375;
|
||||
--text-h: #08060d;
|
||||
@@ -55,11 +60,8 @@ body {
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 1126px;
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
border-inline: 1px solid var(--border);
|
||||
min-height: 100svh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -4,4 +4,7 @@ import react from '@vitejs/plugin-react'
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
// host: true binds 0.0.0.0 so the Windows browser can reach the dev server
|
||||
// through WSL2 (loopback-only binding isn't reliably forwarded out of WSL).
|
||||
server: { host: true },
|
||||
})
|
||||
|
||||