feat(interface): frontend updates and Nexus branding
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "web",
|
"name": "web",
|
||||||
"version": "0.0.0",
|
"version": "1.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "web",
|
"name": "web",
|
||||||
"version": "0.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4"
|
"react-dom": "^19.2.4"
|
||||||
@@ -24,13 +24,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
"version": "7.29.0",
|
"version": "7.29.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
|
||||||
"integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
|
"integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-validator-identifier": "^7.28.5",
|
"@babel/helper-validator-identifier": "^7.29.7",
|
||||||
"js-tokens": "^4.0.0",
|
"js-tokens": "^4.0.0",
|
||||||
"picocolors": "^1.1.1"
|
"picocolors": "^1.1.1"
|
||||||
},
|
},
|
||||||
@@ -39,9 +39,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/compat-data": {
|
"node_modules/@babel/compat-data": {
|
||||||
"version": "7.29.0",
|
"version": "7.29.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
|
||||||
"integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
|
"integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -49,21 +49,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/core": {
|
"node_modules/@babel/core": {
|
||||||
"version": "7.29.0",
|
"version": "7.29.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
|
||||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.29.0",
|
"@babel/code-frame": "^7.29.7",
|
||||||
"@babel/generator": "^7.29.0",
|
"@babel/generator": "^7.29.7",
|
||||||
"@babel/helper-compilation-targets": "^7.28.6",
|
"@babel/helper-compilation-targets": "^7.29.7",
|
||||||
"@babel/helper-module-transforms": "^7.28.6",
|
"@babel/helper-module-transforms": "^7.29.7",
|
||||||
"@babel/helpers": "^7.28.6",
|
"@babel/helpers": "^7.29.7",
|
||||||
"@babel/parser": "^7.29.0",
|
"@babel/parser": "^7.29.7",
|
||||||
"@babel/template": "^7.28.6",
|
"@babel/template": "^7.29.7",
|
||||||
"@babel/traverse": "^7.29.0",
|
"@babel/traverse": "^7.29.7",
|
||||||
"@babel/types": "^7.29.0",
|
"@babel/types": "^7.29.7",
|
||||||
"@jridgewell/remapping": "^2.3.5",
|
"@jridgewell/remapping": "^2.3.5",
|
||||||
"convert-source-map": "^2.0.0",
|
"convert-source-map": "^2.0.0",
|
||||||
"debug": "^4.1.0",
|
"debug": "^4.1.0",
|
||||||
@@ -80,14 +80,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/generator": {
|
"node_modules/@babel/generator": {
|
||||||
"version": "7.29.1",
|
"version": "7.29.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
|
||||||
"integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
|
"integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "^7.29.0",
|
"@babel/parser": "^7.29.7",
|
||||||
"@babel/types": "^7.29.0",
|
"@babel/types": "^7.29.7",
|
||||||
"@jridgewell/gen-mapping": "^0.3.12",
|
"@jridgewell/gen-mapping": "^0.3.12",
|
||||||
"@jridgewell/trace-mapping": "^0.3.28",
|
"@jridgewell/trace-mapping": "^0.3.28",
|
||||||
"jsesc": "^3.0.2"
|
"jsesc": "^3.0.2"
|
||||||
@@ -97,14 +97,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-compilation-targets": {
|
"node_modules/@babel/helper-compilation-targets": {
|
||||||
"version": "7.28.6",
|
"version": "7.29.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
|
||||||
"integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
|
"integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/compat-data": "^7.28.6",
|
"@babel/compat-data": "^7.29.7",
|
||||||
"@babel/helper-validator-option": "^7.27.1",
|
"@babel/helper-validator-option": "^7.29.7",
|
||||||
"browserslist": "^4.24.0",
|
"browserslist": "^4.24.0",
|
||||||
"lru-cache": "^5.1.1",
|
"lru-cache": "^5.1.1",
|
||||||
"semver": "^6.3.1"
|
"semver": "^6.3.1"
|
||||||
@@ -114,9 +114,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-globals": {
|
"node_modules/@babel/helper-globals": {
|
||||||
"version": "7.28.0",
|
"version": "7.29.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
|
||||||
"integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
|
"integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -124,29 +124,29 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-module-imports": {
|
"node_modules/@babel/helper-module-imports": {
|
||||||
"version": "7.28.6",
|
"version": "7.29.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
|
||||||
"integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
|
"integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/traverse": "^7.28.6",
|
"@babel/traverse": "^7.29.7",
|
||||||
"@babel/types": "^7.28.6"
|
"@babel/types": "^7.29.7"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-module-transforms": {
|
"node_modules/@babel/helper-module-transforms": {
|
||||||
"version": "7.28.6",
|
"version": "7.29.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
|
||||||
"integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
|
"integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-module-imports": "^7.28.6",
|
"@babel/helper-module-imports": "^7.29.7",
|
||||||
"@babel/helper-validator-identifier": "^7.28.5",
|
"@babel/helper-validator-identifier": "^7.29.7",
|
||||||
"@babel/traverse": "^7.28.6"
|
"@babel/traverse": "^7.29.7"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
@@ -156,9 +156,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-string-parser": {
|
"node_modules/@babel/helper-string-parser": {
|
||||||
"version": "7.27.1",
|
"version": "7.29.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
|
||||||
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
"integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -166,9 +166,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-validator-identifier": {
|
"node_modules/@babel/helper-validator-identifier": {
|
||||||
"version": "7.28.5",
|
"version": "7.29.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
|
||||||
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
|
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -176,9 +176,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-validator-option": {
|
"node_modules/@babel/helper-validator-option": {
|
||||||
"version": "7.27.1",
|
"version": "7.29.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
|
||||||
"integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
|
"integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -186,27 +186,27 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helpers": {
|
"node_modules/@babel/helpers": {
|
||||||
"version": "7.29.2",
|
"version": "7.29.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
|
||||||
"integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
|
"integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/template": "^7.28.6",
|
"@babel/template": "^7.29.7",
|
||||||
"@babel/types": "^7.29.0"
|
"@babel/types": "^7.29.7"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/parser": {
|
"node_modules/@babel/parser": {
|
||||||
"version": "7.29.2",
|
"version": "7.29.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
|
||||||
"integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
|
"integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/types": "^7.29.0"
|
"@babel/types": "^7.29.7"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"parser": "bin/babel-parser.js"
|
"parser": "bin/babel-parser.js"
|
||||||
@@ -216,33 +216,33 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/template": {
|
"node_modules/@babel/template": {
|
||||||
"version": "7.28.6",
|
"version": "7.29.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
|
||||||
"integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
|
"integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.28.6",
|
"@babel/code-frame": "^7.29.7",
|
||||||
"@babel/parser": "^7.28.6",
|
"@babel/parser": "^7.29.7",
|
||||||
"@babel/types": "^7.28.6"
|
"@babel/types": "^7.29.7"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/traverse": {
|
"node_modules/@babel/traverse": {
|
||||||
"version": "7.29.0",
|
"version": "7.29.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
|
||||||
"integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
|
"integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.29.0",
|
"@babel/code-frame": "^7.29.7",
|
||||||
"@babel/generator": "^7.29.0",
|
"@babel/generator": "^7.29.7",
|
||||||
"@babel/helper-globals": "^7.28.0",
|
"@babel/helper-globals": "^7.29.7",
|
||||||
"@babel/parser": "^7.29.0",
|
"@babel/parser": "^7.29.7",
|
||||||
"@babel/template": "^7.28.6",
|
"@babel/template": "^7.29.7",
|
||||||
"@babel/types": "^7.29.0",
|
"@babel/types": "^7.29.7",
|
||||||
"debug": "^4.3.1"
|
"debug": "^4.3.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -250,23 +250,23 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/types": {
|
"node_modules/@babel/types": {
|
||||||
"version": "7.29.0",
|
"version": "7.29.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
|
||||||
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
|
"integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-string-parser": "^7.27.1",
|
"@babel/helper-string-parser": "^7.29.7",
|
||||||
"@babel/helper-validator-identifier": "^7.28.5"
|
"@babel/helper-validator-identifier": "^7.29.7"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/core": {
|
"node_modules/@emnapi/core": {
|
||||||
"version": "1.9.2",
|
"version": "1.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
||||||
"integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
|
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -276,9 +276,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/runtime": {
|
"node_modules/@emnapi/runtime": {
|
||||||
"version": "1.9.2",
|
"version": "1.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
||||||
"integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
|
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -557,14 +557,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@napi-rs/wasm-runtime": {
|
"node_modules/@napi-rs/wasm-runtime": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",
|
||||||
"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
|
"integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tybys/wasm-util": "^0.10.1"
|
"@tybys/wasm-util": "^0.10.2"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -576,9 +576,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@oxc-project/types": {
|
"node_modules/@oxc-project/types": {
|
||||||
"version": "0.124.0",
|
"version": "0.133.0",
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz",
|
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz",
|
||||||
"integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==",
|
"integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
@@ -586,9 +586,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-android-arm64": {
|
"node_modules/@rolldown/binding-android-arm64": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
|
||||||
"integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==",
|
"integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -603,9 +603,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz",
|
||||||
"integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==",
|
"integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -620,9 +620,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-darwin-x64": {
|
"node_modules/@rolldown/binding-darwin-x64": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz",
|
||||||
"integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==",
|
"integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -637,9 +637,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz",
|
||||||
"integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==",
|
"integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -654,9 +654,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz",
|
||||||
"integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==",
|
"integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -671,9 +671,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz",
|
||||||
"integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==",
|
"integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -688,9 +688,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz",
|
||||||
"integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==",
|
"integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -705,9 +705,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz",
|
||||||
"integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==",
|
"integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
@@ -722,9 +722,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz",
|
||||||
"integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==",
|
"integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
@@ -739,9 +739,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz",
|
||||||
"integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==",
|
"integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -756,9 +756,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz",
|
||||||
"integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==",
|
"integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -773,9 +773,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz",
|
||||||
"integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==",
|
"integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -790,9 +790,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-wasm32-wasi": {
|
"node_modules/@rolldown/binding-wasm32-wasi": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz",
|
||||||
"integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==",
|
"integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"wasm32"
|
"wasm32"
|
||||||
],
|
],
|
||||||
@@ -800,18 +800,18 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emnapi/core": "1.9.2",
|
"@emnapi/core": "1.10.0",
|
||||||
"@emnapi/runtime": "1.9.2",
|
"@emnapi/runtime": "1.10.0",
|
||||||
"@napi-rs/wasm-runtime": "^1.1.3"
|
"@napi-rs/wasm-runtime": "^1.1.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz",
|
||||||
"integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==",
|
"integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -826,9 +826,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz",
|
||||||
"integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==",
|
"integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -850,9 +850,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@tybys/wasm-util": {
|
"node_modules/@tybys/wasm-util": {
|
||||||
"version": "0.10.1",
|
"version": "0.10.2",
|
||||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
|
||||||
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
|
"integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -991,9 +991,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.10.19",
|
"version": "2.10.38",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.19.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.38.tgz",
|
||||||
"integrity": "sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g==",
|
"integrity": "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -1059,9 +1059,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001788",
|
"version": "1.0.30001799",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz",
|
||||||
"integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==",
|
"integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -1188,9 +1188,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.336",
|
"version": "1.5.375",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.336.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.375.tgz",
|
||||||
"integrity": "sha512-AbH9q9J455r/nLmdNZes0G0ZKcRX73FicwowalLs6ijwOmCJSRRrLX63lcAlzy9ux3dWK1w1+1nsBJEWN11hcQ==",
|
"integrity": "sha512-ZWP5eB4BVPW/ZYo9252hQZHZ5XavtsTgpbhcmMmRwymavC5AsLWQWBPaKMeNd2LW0KGby5HPXvj7+sr4ta5j/Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
@@ -1644,10 +1644,20 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/js-yaml": {
|
"node_modules/js-yaml": {
|
||||||
"version": "4.1.1",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz",
|
||||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
"integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/puzrin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/nodeca"
|
||||||
|
}
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"argparse": "^2.0.1"
|
"argparse": "^2.0.1"
|
||||||
@@ -2042,9 +2052,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.11",
|
"version": "3.3.12",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
||||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -2068,11 +2078,14 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/node-releases": {
|
"node_modules/node-releases": {
|
||||||
"version": "2.0.37",
|
"version": "2.0.48",
|
||||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz",
|
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.48.tgz",
|
||||||
"integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==",
|
"integrity": "sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/optionator": {
|
"node_modules/optionator": {
|
||||||
"version": "0.9.4",
|
"version": "0.9.4",
|
||||||
@@ -2178,9 +2191,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.10",
|
"version": "8.5.15",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
||||||
"integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
|
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -2198,7 +2211,7 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.12",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"source-map-js": "^1.2.1"
|
"source-map-js": "^1.2.1"
|
||||||
},
|
},
|
||||||
@@ -2258,14 +2271,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rolldown": {
|
"node_modules/rolldown": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
|
||||||
"integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==",
|
"integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oxc-project/types": "=0.124.0",
|
"@oxc-project/types": "=0.133.0",
|
||||||
"@rolldown/pluginutils": "1.0.0-rc.15"
|
"@rolldown/pluginutils": "^1.0.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"rolldown": "bin/cli.mjs"
|
"rolldown": "bin/cli.mjs"
|
||||||
@@ -2274,27 +2287,27 @@
|
|||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@rolldown/binding-android-arm64": "1.0.0-rc.15",
|
"@rolldown/binding-android-arm64": "1.0.3",
|
||||||
"@rolldown/binding-darwin-arm64": "1.0.0-rc.15",
|
"@rolldown/binding-darwin-arm64": "1.0.3",
|
||||||
"@rolldown/binding-darwin-x64": "1.0.0-rc.15",
|
"@rolldown/binding-darwin-x64": "1.0.3",
|
||||||
"@rolldown/binding-freebsd-x64": "1.0.0-rc.15",
|
"@rolldown/binding-freebsd-x64": "1.0.3",
|
||||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15",
|
"@rolldown/binding-linux-arm-gnueabihf": "1.0.3",
|
||||||
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15",
|
"@rolldown/binding-linux-arm64-gnu": "1.0.3",
|
||||||
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15",
|
"@rolldown/binding-linux-arm64-musl": "1.0.3",
|
||||||
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15",
|
"@rolldown/binding-linux-ppc64-gnu": "1.0.3",
|
||||||
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15",
|
"@rolldown/binding-linux-s390x-gnu": "1.0.3",
|
||||||
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15",
|
"@rolldown/binding-linux-x64-gnu": "1.0.3",
|
||||||
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.15",
|
"@rolldown/binding-linux-x64-musl": "1.0.3",
|
||||||
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.15",
|
"@rolldown/binding-openharmony-arm64": "1.0.3",
|
||||||
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.15",
|
"@rolldown/binding-wasm32-wasi": "1.0.3",
|
||||||
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15",
|
"@rolldown/binding-win32-arm64-msvc": "1.0.3",
|
||||||
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15"
|
"@rolldown/binding-win32-x64-msvc": "1.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rolldown/node_modules/@rolldown/pluginutils": {
|
"node_modules/rolldown/node_modules/@rolldown/pluginutils": {
|
||||||
"version": "1.0.0-rc.15",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
|
||||||
"integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==",
|
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -2374,9 +2387,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.16",
|
"version": "0.2.17",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||||
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
|
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2453,17 +2466,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "8.0.8",
|
"version": "8.0.16",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
|
||||||
"integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==",
|
"integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lightningcss": "^1.32.0",
|
"lightningcss": "^1.32.0",
|
||||||
"picomatch": "^4.0.4",
|
"picomatch": "^4.0.4",
|
||||||
"postcss": "^8.5.8",
|
"postcss": "^8.5.15",
|
||||||
"rolldown": "1.0.0-rc.15",
|
"rolldown": "1.0.3",
|
||||||
"tinyglobby": "^0.2.15"
|
"tinyglobby": "^0.2.17"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"vite": "bin/vite.js"
|
"vite": "bin/vite.js"
|
||||||
@@ -2479,7 +2492,7 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/node": "^20.19.0 || >=22.12.0",
|
"@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",
|
"esbuild": "^0.27.0 || ^0.28.0",
|
||||||
"jiti": ">=1.21.0",
|
"jiti": ">=1.21.0",
|
||||||
"less": "^4.0.0",
|
"less": "^4.0.0",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "web",
|
"name": "web",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"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() {
|
function App() {
|
||||||
const [currentPage, setCurrentPage] = useState("chatbot");
|
const [currentPage, setCurrentPage] = useState("chatbot");
|
||||||
const [status, setStatus] = useState("Checking Synapse...");
|
const [status, setStatus] = useState("Checking Synapse...");
|
||||||
|
const [version, setVersion] = useState("");
|
||||||
const [ollamaStatus, setOllamaStatus] = useState("checking");
|
const [ollamaStatus, setOllamaStatus] = useState("checking");
|
||||||
const [showStatusTooltip, setShowStatusTooltip] = useState(false);
|
const [showStatusTooltip, setShowStatusTooltip] = useState(false);
|
||||||
|
|
||||||
const [activeConversationId, setActiveConversationId] = useState(() => crypto.randomUUID());
|
const [activeConversationId, setActiveConversationId] = useState(() => crypto.randomUUID());
|
||||||
|
const [isModelPulling, setIsModelPulling] = useState(false);
|
||||||
const [conversations, setConversations] = useState([]);
|
const [conversations, setConversations] = useState([]);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [hoveredConvId, setHoveredConvId] = useState(null);
|
const [hoveredConvId, setHoveredConvId] = useState(null);
|
||||||
@@ -33,6 +35,7 @@ function App() {
|
|||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(d => {
|
.then(d => {
|
||||||
setStatus(d.status ?? "Unknown");
|
setStatus(d.status ?? "Unknown");
|
||||||
|
setVersion(d.version ?? "");
|
||||||
setOllamaStatus(d.ollama ?? "unavailable");
|
setOllamaStatus(d.ollama ?? "unavailable");
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@@ -91,6 +94,25 @@ function App() {
|
|||||||
setCurrentPage("chatbot");
|
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) => {
|
const deleteConversation = async (e, conversationId) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (!window.confirm("Delete this conversation?")) return;
|
if (!window.confirm("Delete this conversation?")) return;
|
||||||
@@ -108,9 +130,9 @@ function App() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ key: "chatbot", label: "💬 Chatbot" },
|
{ key: "chatbot", label: "💬 Chat" },
|
||||||
{ key: "playbook", label: "📖 Playbook" },
|
{ key: "playbook", label: "📖 Playbooks" },
|
||||||
{ key: "models", label: "🤖 Models" },
|
{ key: "models", label: "🤖 Models", badge: isModelPulling },
|
||||||
{ key: "memory", label: "🧠 Memory" },
|
{ key: "memory", label: "🧠 Memory" },
|
||||||
{ key: "settings", label: "⚙️ Settings" },
|
{ key: "settings", label: "⚙️ Settings" },
|
||||||
];
|
];
|
||||||
@@ -120,6 +142,7 @@ function App() {
|
|||||||
{/* Sidebar */}
|
{/* Sidebar */}
|
||||||
<aside style={{
|
<aside style={{
|
||||||
width: "260px",
|
width: "260px",
|
||||||
|
flexShrink: 0,
|
||||||
background: "#0a0a0a",
|
background: "#0a0a0a",
|
||||||
borderRight: "1px solid #333",
|
borderRight: "1px solid #333",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@@ -138,6 +161,9 @@ function App() {
|
|||||||
}}>
|
}}>
|
||||||
<img src="/n small.png" alt="Logo" style={{ width: "48px", height: "48px", objectFit: "contain", borderRadius: "8px" }} />
|
<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>
|
<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 */}
|
{/* Status Indicator */}
|
||||||
<div style={{ position: "relative" }}>
|
<div style={{ position: "relative" }}>
|
||||||
@@ -197,9 +223,22 @@ function App() {
|
|||||||
fontSize: "0.85rem",
|
fontSize: "0.85rem",
|
||||||
textAlign: "left",
|
textAlign: "left",
|
||||||
transition: "all 0.15s",
|
transition: "all 0.15s",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{item.label}
|
{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>
|
</button>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
@@ -284,13 +323,30 @@ function App() {
|
|||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
textOverflow: "ellipsis",
|
textOverflow: "ellipsis",
|
||||||
}}>
|
}}>
|
||||||
{conv.preview || "Conversation"}
|
{conv.title || conv.preview || "Conversation"}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: "0.7rem", color: "#666", marginTop: "0.1rem" }}>
|
<div style={{ fontSize: "0.7rem", color: "#666", marginTop: "0.1rem" }}>
|
||||||
{timeAgo(conv.timestamp)}
|
{timeAgo(conv.timestamp)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{isHover && (
|
{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
|
<button
|
||||||
onClick={(e) => deleteConversation(e, conv.id)}
|
onClick={(e) => deleteConversation(e, conv.id)}
|
||||||
title="Delete"
|
title="Delete"
|
||||||
@@ -307,6 +363,7 @@ function App() {
|
|||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -317,19 +374,26 @@ function App() {
|
|||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<main style={{ flexGrow: 1, padding: "1.5rem", display: "flex", flexDirection: "column", overflowY: "auto", minHeight: 0 }}>
|
<main style={{ flexGrow: 1, padding: "1.5rem", display: "flex", flexDirection: "column", overflowY: "auto", minHeight: 0, minWidth: 0 }}>
|
||||||
{currentPage === "chatbot" ? (
|
{/* 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
|
<Chatbot
|
||||||
status={status}
|
|
||||||
conversationId={activeConversationId}
|
conversationId={activeConversationId}
|
||||||
setConversationId={setActiveConversationId}
|
setConversationId={setActiveConversationId}
|
||||||
onConversationChanged={() => loadConversations(search)}
|
onConversationChanged={() => loadConversations(search)}
|
||||||
/>
|
/>
|
||||||
)
|
)}
|
||||||
: currentPage === "playbook" ? <Playbook status={status} />
|
{currentPage === "playbook" && <Playbook />}
|
||||||
: currentPage === "models" ? <Models status={status} />
|
{currentPage === "memory" && <Memory />}
|
||||||
: currentPage === "memory" ? <Memory />
|
{currentPage === "settings" && <Settings />}
|
||||||
: <Settings />}
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useState, useRef, useEffect } from "react";
|
|||||||
import { API_BASE } from "./config";
|
import { API_BASE } from "./config";
|
||||||
import { Markdown } from "./Markdown";
|
import { Markdown } from "./Markdown";
|
||||||
|
|
||||||
export function Chatbot({ status, conversationId, setConversationId, onConversationChanged }) {
|
export function Chatbot({ conversationId, setConversationId, onConversationChanged }) {
|
||||||
const [messages, setMessages] = useState([]);
|
const [messages, setMessages] = useState([]);
|
||||||
const [input, setInput] = useState("");
|
const [input, setInput] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -67,7 +67,7 @@ export function Chatbot({ status, conversationId, setConversationId, onConversat
|
|||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ model }),
|
body: JSON.stringify({ model }),
|
||||||
});
|
});
|
||||||
} catch {}
|
} catch { /* persisting the model choice is best-effort */ }
|
||||||
};
|
};
|
||||||
|
|
||||||
const startNewChat = () => {
|
const startNewChat = () => {
|
||||||
@@ -145,7 +145,18 @@ export function Chatbot({ status, conversationId, setConversationId, onConversat
|
|||||||
if (!payload.trim()) { pendingEventType = null; continue; }
|
if (!payload.trim()) { pendingEventType = null; continue; }
|
||||||
|
|
||||||
if (pendingEventType === "meta") {
|
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;
|
pendingEventType = null;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -158,6 +169,18 @@ export function Chatbot({ status, conversationId, setConversationId, onConversat
|
|||||||
pendingEventType = null;
|
pendingEventType = null;
|
||||||
continue;
|
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") {
|
if (pendingEventType === "error") {
|
||||||
try {
|
try {
|
||||||
const err = JSON.parse(payload);
|
const err = JSON.parse(payload);
|
||||||
@@ -171,11 +194,14 @@ export function Chatbot({ status, conversationId, setConversationId, onConversat
|
|||||||
}
|
}
|
||||||
pendingEventType = null;
|
pendingEventType = null;
|
||||||
|
|
||||||
|
let token = payload;
|
||||||
|
try { token = JSON.parse(payload); } catch { /* plain text fallback */ }
|
||||||
|
|
||||||
setMessages(prev => {
|
setMessages(prev => {
|
||||||
const updated = [...prev];
|
const updated = [...prev];
|
||||||
updated[assistantIndex] = {
|
updated[assistantIndex] = {
|
||||||
...updated[assistantIndex],
|
...updated[assistantIndex],
|
||||||
content: (updated[assistantIndex].content || "") + payload,
|
content: (updated[assistantIndex].content || "") + token,
|
||||||
};
|
};
|
||||||
return updated;
|
return updated;
|
||||||
});
|
});
|
||||||
@@ -368,12 +394,14 @@ export function Chatbot({ status, conversationId, setConversationId, onConversat
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
maxWidth: "70%",
|
maxWidth: msg.role === "user" ? "70%" : "90%",
|
||||||
|
width: msg.role === "assistant" ? "100%" : undefined,
|
||||||
padding: "0.9rem",
|
padding: "0.9rem",
|
||||||
borderRadius: "10px",
|
borderRadius: "10px",
|
||||||
background: msg.role === "user" ? "#007acc" : "#262626",
|
background: msg.role === "user" ? "#007acc" : "#262626",
|
||||||
color: "#eee",
|
color: "#eee",
|
||||||
wordWrap: "break-word",
|
wordWrap: "break-word",
|
||||||
|
boxSizing: "border-box",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{msg.role === "user"
|
{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 }) {
|
export function Markdown({ content }) {
|
||||||
if (!content) return null;
|
if (!content) return null;
|
||||||
|
const blocks = parseBlocks(content);
|
||||||
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) });
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ lineHeight: "1.6" }}>
|
<div style={{ lineHeight: "1.6" }}>
|
||||||
{blocks.map((block, i) =>
|
{blocks.map((block, i) =>
|
||||||
block.type === "code" ? (
|
block.type === "code"
|
||||||
<pre key={i} style={{
|
? <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",
|
background: "#0d0d0d",
|
||||||
border: "1px solid #2a2a2a",
|
border: "1px solid #2a2a2a",
|
||||||
borderRadius: "6px",
|
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",
|
padding: "0.75rem 1rem",
|
||||||
overflowX: "auto",
|
overflowX: "auto",
|
||||||
fontSize: "0.85rem",
|
fontSize: "0.85rem",
|
||||||
lineHeight: "1.5",
|
lineHeight: "1.5",
|
||||||
margin: "0.5rem 0",
|
margin: 0,
|
||||||
fontFamily: "monospace",
|
fontFamily: "monospace",
|
||||||
}}>
|
}}>
|
||||||
<code>{block.value.trimEnd()}</code>
|
<code>{value.trimEnd()}</code>
|
||||||
</pre>
|
</pre>
|
||||||
) : (
|
|
||||||
<TextBlock key={i} text={block.value} />
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -50,7 +125,6 @@ function TextBlock({ text }) {
|
|||||||
while (i < lines.length) {
|
while (i < lines.length) {
|
||||||
const line = lines[i];
|
const line = lines[i];
|
||||||
|
|
||||||
// Heading
|
|
||||||
const hMatch = line.match(/^(#{1,3})\s+(.+)/);
|
const hMatch = line.match(/^(#{1,3})\s+(.+)/);
|
||||||
if (hMatch) {
|
if (hMatch) {
|
||||||
const level = hMatch[1].length;
|
const level = hMatch[1].length;
|
||||||
@@ -60,11 +134,9 @@ function TextBlock({ text }) {
|
|||||||
{inlineMarkdown(hMatch[2])}
|
{inlineMarkdown(hMatch[2])}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
i++;
|
i++; continue;
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unordered list
|
|
||||||
if (/^[-*]\s+/.test(line)) {
|
if (/^[-*]\s+/.test(line)) {
|
||||||
const items = [];
|
const items = [];
|
||||||
while (i < lines.length && /^[-*]\s+/.test(lines[i])) {
|
while (i < lines.length && /^[-*]\s+/.test(lines[i])) {
|
||||||
@@ -75,7 +147,6 @@ function TextBlock({ text }) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ordered list
|
|
||||||
if (/^\d+\.\s+/.test(line)) {
|
if (/^\d+\.\s+/.test(line)) {
|
||||||
const items = [];
|
const items = [];
|
||||||
while (i < lines.length && /^\d+\.\s+/.test(lines[i])) {
|
while (i < lines.length && /^\d+\.\s+/.test(lines[i])) {
|
||||||
@@ -86,21 +157,16 @@ function TextBlock({ text }) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Horizontal rule
|
|
||||||
if (/^---+$/.test(line.trim())) {
|
if (/^---+$/.test(line.trim())) {
|
||||||
elements.push(<hr key={i} style={{ border: "none", borderTop: "1px solid #333", margin: "0.5rem 0" }} />);
|
elements.push(<hr key={i} style={{ border: "none", borderTop: "1px solid #333", margin: "0.5rem 0" }} />);
|
||||||
i++;
|
i++; continue;
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Empty line → small gap
|
|
||||||
if (line.trim() === "") {
|
if (line.trim() === "") {
|
||||||
elements.push(<div key={i} style={{ height: "0.4rem" }} />);
|
elements.push(<div key={i} style={{ height: "0.4rem" }} />);
|
||||||
i++;
|
i++; continue;
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Regular paragraph line
|
|
||||||
elements.push(<div key={i}>{inlineMarkdown(line)}</div>);
|
elements.push(<div key={i}>{inlineMarkdown(line)}</div>);
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
@@ -116,7 +182,6 @@ function inlineMarkdown(text) {
|
|||||||
|
|
||||||
while ((m = re.exec(text)) !== null) {
|
while ((m = re.exec(text)) !== null) {
|
||||||
if (m.index > last) parts.push(text.slice(last, m.index));
|
if (m.index > last) parts.push(text.slice(last, m.index));
|
||||||
|
|
||||||
if (m[2] !== undefined) {
|
if (m[2] !== undefined) {
|
||||||
parts.push(<strong key={m.index}>{m[2]}</strong>);
|
parts.push(<strong key={m.index}>{m[2]}</strong>);
|
||||||
} else if (m[3] !== undefined) {
|
} else if (m[3] !== undefined) {
|
||||||
|
|||||||
@@ -13,6 +13,28 @@ function groupBySection(items) {
|
|||||||
return map;
|
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() {
|
export function Memory() {
|
||||||
const [items, setItems] = useState([]);
|
const [items, setItems] = useState([]);
|
||||||
const [editing, setEditing] = useState(null); // { id, section, text } or null
|
const [editing, setEditing] = useState(null); // { id, section, text } or null
|
||||||
@@ -20,6 +42,8 @@ export function Memory() {
|
|||||||
const [newSection, setNewSection] = useState("");
|
const [newSection, setNewSection] = useState("");
|
||||||
const [showNewSection, setShowNewSection] = useState(false);
|
const [showNewSection, setShowNewSection] = useState(false);
|
||||||
const [message, setMessage] = useState("");
|
const [message, setMessage] = useState("");
|
||||||
|
const [draggingId, setDraggingId] = useState(null);
|
||||||
|
const [dragOver, setDragOver] = useState(null); // { id, position: "before"|"after" }
|
||||||
|
|
||||||
const loadItems = useCallback(async () => {
|
const loadItems = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -36,22 +60,30 @@ export function Memory() {
|
|||||||
const flash = (msg) => { setMessage(msg); setTimeout(() => setMessage(""), 3000); };
|
const flash = (msg) => { setMessage(msg); setTimeout(() => setMessage(""), 3000); };
|
||||||
|
|
||||||
const saveNew = async (section, text) => {
|
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`, {
|
const res = await fetch(`${API_BASE}/memory`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
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.");
|
else flash("Save failed.");
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveEdit = async (id, section, text) => {
|
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}`, {
|
const res = await fetch(`${API_BASE}/memory/${id}`, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
headers: { "Content-Type": "application/json" },
|
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."); }
|
if (res.ok) { loadItems(); setEditing(null); flash("Updated."); }
|
||||||
else flash("Update failed.");
|
else flash("Update failed.");
|
||||||
@@ -64,6 +96,154 @@ export function Memory() {
|
|||||||
else flash("Delete failed.");
|
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 addSection = () => {
|
||||||
const sec = newSection.trim();
|
const sec = newSection.trim();
|
||||||
if (!sec) return;
|
if (!sec) return;
|
||||||
@@ -98,7 +278,7 @@ export function Memory() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ flexGrow: 1, maxWidth: "860px" }}>
|
<div style={{ flexGrow: 1 }}>
|
||||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "1.25rem" }}>
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "1.25rem" }}>
|
||||||
<div>
|
<div>
|
||||||
<h2 style={{ margin: 0, fontSize: "1.1rem" }}>Memory</h2>
|
<h2 style={{ margin: 0, fontSize: "1.1rem" }}>Memory</h2>
|
||||||
@@ -148,9 +328,49 @@ export function Memory() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bullet items */}
|
{/* Bullet items */}
|
||||||
|
{(() => {
|
||||||
|
const sectionItems = grouped[section] || [];
|
||||||
|
const { groups, groupIndexById } = computeGroups(sectionItems);
|
||||||
|
return (
|
||||||
<div style={{ paddingLeft: "1rem", borderLeft: "2px solid #222" }}>
|
<div style={{ paddingLeft: "1rem", borderLeft: "2px solid #222" }}>
|
||||||
{(grouped[section] || []).map(item => (
|
{sectionItems.map((item) => {
|
||||||
<div key={item.id} style={{ marginBottom: "0.4rem" }}>
|
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 ? (
|
{editing?.id === item.id ? (
|
||||||
<EditRow
|
<EditRow
|
||||||
initial={editing.text}
|
initial={editing.text}
|
||||||
@@ -160,17 +380,29 @@ export function Memory() {
|
|||||||
btnStyle={btnStyle}
|
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: "#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 }}
|
<div style={{ display: "flex", gap: "0.3rem", flexShrink: 0, opacity: 0.4 }}
|
||||||
onMouseEnter={e => e.currentTarget.style.opacity = 1}
|
onMouseEnter={e => e.currentTarget.style.opacity = 1}
|
||||||
onMouseLeave={e => e.currentTarget.style.opacity = 0.4}
|
onMouseLeave={e => e.currentTarget.style.opacity = 0.4}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onClick={() => setEditing({ id: item.id, section, text: item.text })}
|
onClick={() => moveItem(item.id, "up")}
|
||||||
style={{ ...btnStyle("#333"), fontSize: "0.72rem", padding: "0.2rem 0.5rem" }}
|
disabled={arrowUpDisabled}
|
||||||
>edit</button>
|
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
|
<button
|
||||||
onClick={() => deleteItem(item.id)}
|
onClick={() => deleteItem(item.id)}
|
||||||
style={{ ...btnStyle("#5a1a1a"), fontSize: "0.72rem", padding: "0.2rem 0.5rem" }}
|
style={{ ...btnStyle("#5a1a1a"), fontSize: "0.72rem", padding: "0.2rem 0.5rem" }}
|
||||||
@@ -179,7 +411,8 @@ export function Memory() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
{/* Inline add form */}
|
{/* Inline add form */}
|
||||||
{adding?.section === section && (
|
{adding?.section === section && (
|
||||||
@@ -191,6 +424,8 @@ export function Memory() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
</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 }) {
|
function AddRow({ onSave, onCancel, inputStyle, btnStyle }) {
|
||||||
const [text, setText] = useState("");
|
const [text, setText] = useState("");
|
||||||
return (
|
return (
|
||||||
<div style={{ display: "flex", gap: "0.5rem", alignItems: "center", marginTop: "0.4rem" }}>
|
<div style={{ display: "flex", gap: "0.5rem", alignItems: "center", marginTop: "0.4rem" }}>
|
||||||
<span style={{ color: "#555", flexShrink: 0 }}>-</span>
|
<span style={{ color: "#555", flexShrink: 0 }}>-</span>
|
||||||
<input
|
<textarea
|
||||||
autoFocus
|
autoFocus
|
||||||
type="text"
|
rows={1}
|
||||||
placeholder="New item..."
|
placeholder="New item… (paste multiple lines to add several at once)"
|
||||||
value={text}
|
value={text}
|
||||||
onChange={e => setText(e.target.value)}
|
onChange={e => setText(e.target.value)}
|
||||||
onKeyDown={e => { if (e.key === "Enter") onSave(text); if (e.key === "Escape") onCancel(); }}
|
onKeyDown={e => {
|
||||||
style={{ ...inputStyle }}
|
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={() => onSave(text)} style={btnStyle()}>Save</button>
|
||||||
<button onClick={onCancel} style={btnStyle("#444")}>✕</button>
|
<button onClick={onCancel} style={btnStyle("#444")}>✕</button>
|
||||||
@@ -233,7 +483,11 @@ function EditRow({ initial, onSave, onCancel, inputStyle, btnStyle }) {
|
|||||||
type="text"
|
type="text"
|
||||||
value={text}
|
value={text}
|
||||||
onChange={e => setText(e.target.value)}
|
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 }}
|
style={{ ...inputStyle }}
|
||||||
/>
|
/>
|
||||||
<button onClick={() => onSave(text)} style={btnStyle()}>Save</button>
|
<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";
|
import { API_BASE } from "./config";
|
||||||
|
|
||||||
export function Models({ status }) {
|
export function Models({ onPullStateChange }) {
|
||||||
const [models, setModels] = useState([]);
|
const [models, setModels] = useState([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [pulling, setPulling] = useState(false);
|
const [pulling, setPulling] = useState(false);
|
||||||
const [modelName, setModelName] = useState("");
|
const [modelName, setModelName] = useState("");
|
||||||
const [pullProgress, setPullProgress] = useState("");
|
const [pullProgress, setPullProgress] = useState("");
|
||||||
const [ollamaStatus, setOllamaStatus] = useState("checking");
|
|
||||||
|
|
||||||
const checkOllamaStatus = async () => {
|
const checkOllamaStatus = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE}/ollama/status`);
|
const response = await fetch(`${API_BASE}/ollama/status`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
setOllamaStatus(data.status);
|
|
||||||
return data.status === "running";
|
return data.status === "running";
|
||||||
} catch {
|
} catch {
|
||||||
setOllamaStatus("unavailable");
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const loadModels = async () => {
|
const loadModels = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError("");
|
setError("");
|
||||||
try {
|
try {
|
||||||
@@ -35,14 +32,15 @@ export function Models({ status }) {
|
|||||||
const response = await fetch(`${API_BASE}/models/details`);
|
const response = await fetch(`${API_BASE}/models/details`);
|
||||||
if (!response.ok) throw new Error("Failed to fetch models");
|
if (!response.ok) throw new Error("Failed to fetch models");
|
||||||
const data = await response.json();
|
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) {
|
} catch (err) {
|
||||||
setError(`Error loading models: ${err.message}`);
|
setError(`Error loading models: ${err.message}`);
|
||||||
setModels([]);
|
setModels([]);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, [checkOllamaStatus]);
|
||||||
|
|
||||||
const pullModel = async () => {
|
const pullModel = async () => {
|
||||||
if (!modelName.trim()) {
|
if (!modelName.trim()) {
|
||||||
@@ -51,6 +49,7 @@ export function Models({ status }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setPulling(true);
|
setPulling(true);
|
||||||
|
onPullStateChange?.(true);
|
||||||
setError("");
|
setError("");
|
||||||
setPullProgress("");
|
setPullProgress("");
|
||||||
|
|
||||||
@@ -99,6 +98,7 @@ export function Models({ status }) {
|
|||||||
setError(`Error pulling model: ${err.message}`);
|
setError(`Error pulling model: ${err.message}`);
|
||||||
} finally {
|
} finally {
|
||||||
setPulling(false);
|
setPulling(false);
|
||||||
|
onPullStateChange?.(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -123,8 +123,9 @@ export function Models({ status }) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadModels();
|
loadModels();
|
||||||
const interval = setInterval(loadModels, 30000);
|
const interval = setInterval(loadModels, 30000);
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, []);
|
}, [loadModels]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: "flex", flexDirection: "column", height: "100%", flexGrow: 1 }}>
|
<div style={{ display: "flex", flexDirection: "column", height: "100%", flexGrow: 1 }}>
|
||||||
@@ -214,7 +215,7 @@ export function Models({ status }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Models List */}
|
{/* Local Models List */}
|
||||||
<div style={{ flexGrow: 1, overflowY: "auto", paddingRight: "0.5rem" }}>
|
<div style={{ flexGrow: 1, overflowY: "auto", paddingRight: "0.5rem" }}>
|
||||||
<h3 style={{ margin: "0 0 1rem 0", fontSize: "0.95rem", color: "#bbb" }}>
|
<h3 style={{ margin: "0 0 1rem 0", fontSize: "0.95rem", color: "#bbb" }}>
|
||||||
Available Models ({models.length})
|
Available Models ({models.length})
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useState, useCallback, useRef } from "react";
|
import { useEffect, useState, useCallback, useRef } from "react";
|
||||||
import { API_BASE } from "./config";
|
import { API_BASE } from "./config";
|
||||||
|
|
||||||
export function Playbook({ status }) {
|
export function Playbook() {
|
||||||
const [playbooks, setPlaybooks] = useState([]);
|
const [playbooks, setPlaybooks] = useState([]);
|
||||||
const [selectedId, setSelectedId] = useState(null);
|
const [selectedId, setSelectedId] = useState(null);
|
||||||
const [form, setForm] = useState({ title: "", goal: "", instructions: "", tags: "" });
|
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]);
|
useEffect(() => { loadPlaybooks(); }, [loadPlaybooks]);
|
||||||
|
|
||||||
const safeString = (v) => (v == null ? "" : String(v));
|
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
|
// Optimistic UI update — reorder within filtered set, keep unfiltered playbooks intact
|
||||||
setPlaybooks(prev => {
|
setPlaybooks(prev => {
|
||||||
const filteredSet = new Set(currentOrder);
|
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 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);
|
const reorderedFiltered = newFilteredOrder.map(id => filteredMap[id]).filter(Boolean);
|
||||||
// Merge: place unfiltered items back in their original relative positions
|
// Merge: place unfiltered items back in their original relative positions
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ const DEFAULTS = {
|
|||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
system_prompt: "",
|
system_prompt: "",
|
||||||
timeout: 120,
|
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() {
|
export function Settings() {
|
||||||
@@ -13,11 +17,25 @@ export function Settings() {
|
|||||||
const [saved, setSaved] = useState(false);
|
const [saved, setSaved] = useState(false);
|
||||||
const [error, setError] = useState("");
|
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(() => {
|
useEffect(() => {
|
||||||
fetch(`${API_BASE}/settings`)
|
fetch(`${API_BASE}/settings`)
|
||||||
.then(r => r.ok ? r.json() : null)
|
.then(r => r.ok ? r.json() : null)
|
||||||
.then(d => { if (d) setForm(f => ({ ...f, ...d })); })
|
.then(d => { if (d) setForm(f => ({ ...f, ...d })); })
|
||||||
.catch(() => {});
|
.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 }));
|
const update = (key, value) => setForm(f => ({ ...f, [key]: value }));
|
||||||
@@ -41,6 +59,73 @@ export function Settings() {
|
|||||||
|
|
||||||
const reset = () => setForm(DEFAULTS);
|
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 = {
|
const inputStyle = {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
padding: "0.6rem 0.75rem",
|
padding: "0.6rem 0.75rem",
|
||||||
@@ -69,8 +154,10 @@ export function Settings() {
|
|||||||
marginBottom: "1rem",
|
marginBottom: "1rem",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const displayQueue = isBranding ? brandQueue : selectedApps;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ maxWidth: "640px" }}>
|
<div style={{ maxWidth: "720px" }}>
|
||||||
<h2 style={{ margin: "0 0 1.25rem", fontSize: "1.1rem", color: "#eee" }}>Settings</h2>
|
<h2 style={{ margin: "0 0 1.25rem", fontSize: "1.1rem", color: "#eee" }}>Settings</h2>
|
||||||
|
|
||||||
{/* Generation */}
|
{/* Generation */}
|
||||||
@@ -99,6 +186,140 @@ export function Settings() {
|
|||||||
<span>2.0 — creative</span>
|
<span>2.0 — creative</span>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
{/* Behaviour */}
|
{/* Behaviour */}
|
||||||
@@ -143,6 +364,165 @@ export function Settings() {
|
|||||||
</div>
|
</div>
|
||||||
</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 */}
|
{/* Actions */}
|
||||||
<div style={{ display: "flex", gap: "0.75rem", alignItems: "center" }}>
|
<div style={{ display: "flex", gap: "0.75rem", alignItems: "center" }}>
|
||||||
<button
|
<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 {
|
:root {
|
||||||
--text: #6b6375;
|
--text: #6b6375;
|
||||||
--text-h: #08060d;
|
--text-h: #08060d;
|
||||||
@@ -55,11 +60,8 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#root {
|
#root {
|
||||||
width: 1126px;
|
width: 100%;
|
||||||
max-width: 100%;
|
|
||||||
margin: 0 auto;
|
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border-inline: 1px solid var(--border);
|
|
||||||
min-height: 100svh;
|
min-height: 100svh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -4,4 +4,7 @@ import react from '@vitejs/plugin-react'
|
|||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
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 },
|
||||||
})
|
})
|
||||||
|
|||||||