feat: Add OpenClaw module with dedicated frontend page, backend API, and documentation.
This commit is contained in:
Generated
+375
@@ -8,6 +8,13 @@
|
||||
"name": "foxy-dev-team-dashboard",
|
||||
"version": "2.0.0",
|
||||
"dependencies": {
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
"@codemirror/lang-markdown": "^6.5.0",
|
||||
"@codemirror/lang-yaml": "^6.1.2",
|
||||
"@codemirror/state": "^6.6.0",
|
||||
"@codemirror/theme-one-dark": "^6.1.3",
|
||||
"@codemirror/view": "^6.40.0",
|
||||
"@uiw/react-codemirror": "^4.25.8",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.1.0"
|
||||
@@ -256,6 +263,15 @@
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
|
||||
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
||||
@@ -304,6 +320,184 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/autocomplete": {
|
||||
"version": "6.20.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.1.tgz",
|
||||
"integrity": "sha512-1cvg3Vz1dSSToCNlJfRA2WSI4ht3K+WplO0UMOgmUYPivCyy2oueZY6Lx7M9wThm7SDUBViRmuT+OG/i8+ON9A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.17.0",
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/commands": {
|
||||
"version": "6.10.3",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.3.tgz",
|
||||
"integrity": "sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.6.0",
|
||||
"@codemirror/view": "^6.27.0",
|
||||
"@lezer/common": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-css": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz",
|
||||
"integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@lezer/common": "^1.0.2",
|
||||
"@lezer/css": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-html": {
|
||||
"version": "6.4.11",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz",
|
||||
"integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/lang-css": "^6.0.0",
|
||||
"@codemirror/lang-javascript": "^6.0.0",
|
||||
"@codemirror/language": "^6.4.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.17.0",
|
||||
"@lezer/common": "^1.0.0",
|
||||
"@lezer/css": "^1.1.0",
|
||||
"@lezer/html": "^1.3.12"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-javascript": {
|
||||
"version": "6.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.5.tgz",
|
||||
"integrity": "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/language": "^6.6.0",
|
||||
"@codemirror/lint": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.17.0",
|
||||
"@lezer/common": "^1.0.0",
|
||||
"@lezer/javascript": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-json": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz",
|
||||
"integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@lezer/json": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-markdown": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.5.0.tgz",
|
||||
"integrity": "sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.7.1",
|
||||
"@codemirror/lang-html": "^6.0.0",
|
||||
"@codemirror/language": "^6.3.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"@lezer/common": "^1.2.1",
|
||||
"@lezer/markdown": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lang-yaml": {
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lang-yaml/-/lang-yaml-6.1.2.tgz",
|
||||
"integrity": "sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.2.0",
|
||||
"@lezer/lr": "^1.0.0",
|
||||
"@lezer/yaml": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/language": {
|
||||
"version": "6.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.2.tgz",
|
||||
"integrity": "sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.23.0",
|
||||
"@lezer/common": "^1.5.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0",
|
||||
"style-mod": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/lint": {
|
||||
"version": "6.9.5",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.5.tgz",
|
||||
"integrity": "sha512-GElsbU9G7QT9xXhpUg1zWGmftA/7jamh+7+ydKRuT0ORpWS3wOSP0yT1FOlIZa7mIJjpVPipErsyvVqB9cfTFA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.35.0",
|
||||
"crelt": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/search": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.6.0.tgz",
|
||||
"integrity": "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.37.0",
|
||||
"crelt": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/state": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.6.0.tgz",
|
||||
"integrity": "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@marijn/find-cluster-break": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/theme-one-dark": {
|
||||
"version": "6.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz",
|
||||
"integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"@lezer/highlight": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/view": {
|
||||
"version": "6.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.40.0.tgz",
|
||||
"integrity": "sha512-WA0zdU7xfF10+5I3HhUUq3kqOx3KjqmtQ9lqZjfK7jtYk4G72YW9rezcSywpaUMCWOMlq+6E0pO1IWg1TNIhtg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.6.0",
|
||||
"crelt": "^1.0.6",
|
||||
"style-mod": "^4.1.0",
|
||||
"w3c-keyname": "^2.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
|
||||
@@ -796,6 +990,101 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/common": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.1.tgz",
|
||||
"integrity": "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@lezer/css": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.1.tgz",
|
||||
"integrity": "sha512-PYAKeUVBo3HFThruRyp/iK91SwiZJnzXh8QzkQlwijB5y+N5iB28+iLk78o2zmKqqV0uolNhCwFqB8LA7b0Svg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/highlight": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
|
||||
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/html": {
|
||||
"version": "1.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.13.tgz",
|
||||
"integrity": "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/javascript": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz",
|
||||
"integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.1.3",
|
||||
"@lezer/lr": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/json": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz",
|
||||
"integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/lr": {
|
||||
"version": "1.4.8",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.8.tgz",
|
||||
"integrity": "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/markdown": {
|
||||
"version": "1.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.6.3.tgz",
|
||||
"integrity": "sha512-jpGm5Ps+XErS+xA4urw7ogEGkeZOahVQF21Z6oECF0sj+2liwZopd2+I8uH5I/vZsRuuze3OxBREIANLf6KKUw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.5.0",
|
||||
"@lezer/highlight": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/yaml": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/yaml/-/yaml-1.0.4.tgz",
|
||||
"integrity": "sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.2.0",
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@marijn/find-cluster-break": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
|
||||
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-beta.27",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
|
||||
@@ -1497,6 +1786,59 @@
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@uiw/codemirror-extensions-basic-setup": {
|
||||
"version": "4.25.8",
|
||||
"resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.25.8.tgz",
|
||||
"integrity": "sha512-9Rr+liiBmK4xzZHszL+twNRJApthqmITBwDP3emNTtTrkBFN4gHlqfp+nodKmoVt1+bUH1qQCtyqt+7dbDTHiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/commands": "^6.0.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/lint": "^6.0.0",
|
||||
"@codemirror/search": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://jaywcjlove.github.io/#/sponsor"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@codemirror/autocomplete": ">=6.0.0",
|
||||
"@codemirror/commands": ">=6.0.0",
|
||||
"@codemirror/language": ">=6.0.0",
|
||||
"@codemirror/lint": ">=6.0.0",
|
||||
"@codemirror/search": ">=6.0.0",
|
||||
"@codemirror/state": ">=6.0.0",
|
||||
"@codemirror/view": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@uiw/react-codemirror": {
|
||||
"version": "4.25.8",
|
||||
"resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.25.8.tgz",
|
||||
"integrity": "sha512-A0aLOuJZm2yJ+U9GlMFwxwFciztjd5LhcAG4SMqFxdD58wH+sCQXuY4UU5J2hqgS390qAlShtUgREvJPUonbuQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.6",
|
||||
"@codemirror/commands": "^6.1.0",
|
||||
"@codemirror/state": "^6.1.1",
|
||||
"@codemirror/theme-one-dark": "^6.0.0",
|
||||
"@uiw/codemirror-extensions-basic-setup": "4.25.8",
|
||||
"codemirror": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://jaywcjlove.github.io/#/sponsor"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/runtime": ">=7.11.0",
|
||||
"@codemirror/state": ">=6.0.0",
|
||||
"@codemirror/theme-one-dark": ">=6.0.0",
|
||||
"@codemirror/view": ">=6.0.0",
|
||||
"codemirror": ">=6.0.0",
|
||||
"react": ">=17.0.0",
|
||||
"react-dom": ">=17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitejs/plugin-react": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
|
||||
@@ -1586,6 +1928,21 @@
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/codemirror": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz",
|
||||
"integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/commands": "^6.0.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/lint": "^6.0.0",
|
||||
"@codemirror/search": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/convert-source-map": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
@@ -1606,6 +1963,12 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/crelt": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
||||
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
@@ -2316,6 +2679,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/style-mod": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
|
||||
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz",
|
||||
@@ -2474,6 +2843,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-keyname": {
|
||||
"version": "2.2.8",
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
|
||||
+10
-3
@@ -9,17 +9,24 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
"@codemirror/lang-markdown": "^6.5.0",
|
||||
"@codemirror/lang-yaml": "^6.1.2",
|
||||
"@codemirror/state": "^6.6.0",
|
||||
"@codemirror/theme-one-dark": "^6.1.3",
|
||||
"@codemirror/view": "^6.40.0",
|
||||
"@uiw/react-codemirror": "^4.25.8",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"typescript": "~5.7.0",
|
||||
"vite": "^6.0.0",
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"tailwindcss": "^4.0.0"
|
||||
"vite": "^6.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,6 +173,8 @@ export interface OpenClawAgent {
|
||||
system_prompt?: string;
|
||||
system_prompt_preview?: string;
|
||||
file_count?: number;
|
||||
has_workspace?: boolean;
|
||||
identity_files?: { name: string; size: number }[];
|
||||
}
|
||||
|
||||
export interface OpenClawSkill {
|
||||
@@ -209,6 +211,41 @@ export interface FileTreeNode {
|
||||
children?: FileTreeNode[];
|
||||
}
|
||||
|
||||
export interface ReadFileResult {
|
||||
content: string | null;
|
||||
pretty?: string | null;
|
||||
binary: boolean;
|
||||
path: string;
|
||||
size: number;
|
||||
language: string;
|
||||
modified?: string;
|
||||
}
|
||||
|
||||
export interface WriteFileResult {
|
||||
message: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface IdentityFile {
|
||||
name: string;
|
||||
size: number;
|
||||
modified?: string | null;
|
||||
}
|
||||
|
||||
export interface AgentFilesResult {
|
||||
files: IdentityFile[];
|
||||
other_files: IdentityFile[];
|
||||
workspace: string;
|
||||
exists: boolean;
|
||||
}
|
||||
|
||||
export interface AgentFileResult {
|
||||
content: string;
|
||||
filename: string;
|
||||
size: number;
|
||||
language: string;
|
||||
}
|
||||
|
||||
// ─── HTTP Helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
async function request<T>(path: string, options?: RequestInit): Promise<T> {
|
||||
@@ -291,15 +328,31 @@ export const api = {
|
||||
|
||||
// OpenClaw
|
||||
openclawStatus: () => request<OpenClawStatus>('/api/openclaw/status'),
|
||||
openclawConfig: () => request<{ config: Record<string, unknown> | null; error?: string }>('/api/openclaw/config'),
|
||||
openclawConfig: () => request<{ config: Record<string, unknown> }>('/api/openclaw/config'),
|
||||
openclawAgents: () => request<{ agents: OpenClawAgent[]; count: number }>('/api/openclaw/agents'),
|
||||
openclawDeleteAgent: (name: string) => request<{ message: string }>(`/api/openclaw/agents/${encodeURIComponent(name)}`, { method: 'DELETE' }),
|
||||
openclawAgentFiles: (name: string) => request<AgentFilesResult>(`/api/openclaw/agents/${encodeURIComponent(name)}/files`),
|
||||
openclawReadAgentFile: (name: string, filename: string) =>
|
||||
request<AgentFileResult>(`/api/openclaw/agents/${encodeURIComponent(name)}/file?filename=${encodeURIComponent(filename)}`),
|
||||
openclawWriteAgentFile: (name: string, filename: string, content: string) =>
|
||||
request<WriteFileResult>(`/api/openclaw/agents/${encodeURIComponent(name)}/file?filename=${encodeURIComponent(filename)}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ content }),
|
||||
}),
|
||||
openclawSkills: () => request<{ skills: OpenClawSkill[]; count: number }>('/api/openclaw/skills'),
|
||||
openclawLogs: (params?: { lines?: number; level?: string }) => {
|
||||
const qs = params ? '?' + new URLSearchParams(
|
||||
Object.fromEntries(Object.entries(params).filter(([, v]) => v != null).map(([k, v]) => [k, String(v)]))
|
||||
).toString() : '';
|
||||
return request<OpenClawLogs>(`/api/openclaw/logs${qs}`);
|
||||
openclawLogs: (params: { lines?: number; level?: string }) => {
|
||||
const qs = new URLSearchParams();
|
||||
if (params.lines) qs.set('lines', params.lines.toString());
|
||||
if (params.level) qs.set('level', params.level);
|
||||
return request<OpenClawLogs>(`/api/openclaw/logs?${qs.toString()}`);
|
||||
},
|
||||
openclawModels: () => request<OpenClawModels>('/api/openclaw/models'),
|
||||
openclawFilesystem: () => request<{ root: string; tree: FileTreeNode[] }>('/api/openclaw/filesystem'),
|
||||
openclawReadFile: (path: string) =>
|
||||
request<ReadFileResult>(`/api/openclaw/file?path=${encodeURIComponent(path)}`),
|
||||
openclawWriteFile: (path: string, content: string) =>
|
||||
request<WriteFileResult>(`/api/openclaw/file?path=${encodeURIComponent(path)}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ content }),
|
||||
}),
|
||||
};
|
||||
|
||||
+360
-66
@@ -1,7 +1,12 @@
|
||||
import { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import CodeMirror from '@uiw/react-codemirror';
|
||||
import { json } from '@codemirror/lang-json';
|
||||
import { markdown } from '@codemirror/lang-markdown';
|
||||
import { yaml } from '@codemirror/lang-yaml';
|
||||
import { oneDark } from '@codemirror/theme-one-dark';
|
||||
import type {
|
||||
OpenClawStatus, OpenClawAgent, OpenClawSkill, OpenClawLogs,
|
||||
OpenClawModels, FileTreeNode,
|
||||
OpenClawModels, FileTreeNode, ReadFileResult
|
||||
} from '../api/client';
|
||||
import { api } from '../api/client';
|
||||
|
||||
@@ -11,6 +16,7 @@ import { api } from '../api/client';
|
||||
|
||||
const TABS = [
|
||||
{ id: 'status', label: 'Status', icon: '🔋' },
|
||||
{ id: 'filesystem', label: 'Arborescence', icon: '📂' },
|
||||
{ id: 'config', label: 'Configuration', icon: '⚙️' },
|
||||
{ id: 'agents', label: 'Agents', icon: '🤖' },
|
||||
{ id: 'skills', label: 'Skills', icon: '🧩' },
|
||||
@@ -93,6 +99,7 @@ export default function OpenClawPage() {
|
||||
{/* Tab Content */}
|
||||
<div className="animate-fade-in">
|
||||
{activeTab === 'status' && <StatusTab status={status} />}
|
||||
{activeTab === 'filesystem' && <FilesystemTab />}
|
||||
{activeTab === 'config' && <ConfigTab />}
|
||||
{activeTab === 'agents' && <AgentsTab />}
|
||||
{activeTab === 'skills' && <SkillsTab />}
|
||||
@@ -109,15 +116,6 @@ export default function OpenClawPage() {
|
||||
// ═════════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function StatusTab({ status }: { status: OpenClawStatus | null }) {
|
||||
const [tree, setTree] = useState<FileTreeNode[]>([]);
|
||||
const [treeLoading, setTreeLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
api.openclawFilesystem()
|
||||
.then(d => { setTree(d.tree); setTreeLoading(false); })
|
||||
.catch(() => setTreeLoading(false));
|
||||
}, []);
|
||||
|
||||
if (!status) return <EmptyState message="Impossible de charger le status" />;
|
||||
|
||||
const metrics = [
|
||||
@@ -172,24 +170,6 @@ function StatusTab({ status }: { status: OpenClawStatus | null }) {
|
||||
<MetricCard key={m.label} {...m} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Filesystem tree */}
|
||||
<div className="glass-card p-6">
|
||||
<h2 className="text-lg font-bold text-white mb-4 flex items-center gap-2">
|
||||
<span>📂</span> Arborescence FOXY_HOME
|
||||
</h2>
|
||||
{treeLoading ? (
|
||||
<div className="text-gray-500 text-sm animate-pulse">Chargement de l'arborescence…</div>
|
||||
) : tree.length === 0 ? (
|
||||
<p className="text-gray-500 text-sm">Aucun fichier trouvé</p>
|
||||
) : (
|
||||
<div className="font-mono text-xs max-h-80 overflow-y-auto space-y-0.5">
|
||||
{tree.map(node => (
|
||||
<TreeNode key={node.path} node={node} depth={0} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -217,7 +197,7 @@ function MetricCard({ label, value, accent, icon }: { label: string; value: stri
|
||||
}
|
||||
|
||||
|
||||
function TreeNode({ node, depth }: { node: FileTreeNode; depth: number }) {
|
||||
function TreeNode({ node, depth, onSelect }: { node: FileTreeNode; depth: number, onSelect?: (n: FileTreeNode) => void }) {
|
||||
const [open, setOpen] = useState(depth < 1);
|
||||
const isDir = node.type === 'directory';
|
||||
const indent = depth * 20;
|
||||
@@ -225,11 +205,14 @@ function TreeNode({ node, depth }: { node: FileTreeNode; depth: number }) {
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className={`flex items-center gap-2 py-1 px-2 rounded-lg transition-colors ${
|
||||
isDir ? 'hover:bg-surface-700/50 cursor-pointer' : 'text-gray-400'
|
||||
className={`flex items-center gap-2 py-1 px-2 rounded-lg transition-colors cursor-pointer ${
|
||||
isDir ? 'hover:bg-surface-700/50' : 'hover:bg-surface-700/50 text-gray-400'
|
||||
}`}
|
||||
style={{ paddingLeft: `${indent + 8}px` }}
|
||||
onClick={() => isDir && setOpen(!open)}
|
||||
onClick={() => {
|
||||
if (isDir) setOpen(!open);
|
||||
else onSelect?.(node);
|
||||
}}
|
||||
>
|
||||
{isDir ? (
|
||||
<span className="text-yellow-400 w-4 text-center">{open ? '📂' : '📁'}</span>
|
||||
@@ -242,7 +225,7 @@ function TreeNode({ node, depth }: { node: FileTreeNode; depth: number }) {
|
||||
)}
|
||||
</div>
|
||||
{isDir && open && node.children?.map(child => (
|
||||
<TreeNode key={child.path} node={child} depth={depth + 1} />
|
||||
<TreeNode key={child.path} node={child} depth={depth + 1} onSelect={onSelect} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
@@ -356,22 +339,83 @@ function AgentsTab() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [expandedAgent, setExpandedAgent] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const [deleting, setDeleting] = useState<string | null>(null);
|
||||
const [activeFile, setActiveFile] = useState<{agent: string, file: string} | null>(null);
|
||||
const [fileContent, setFileContent] = useState('');
|
||||
const [fileLoading, setFileLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const loadAgents = () => {
|
||||
setLoading(true);
|
||||
api.openclawAgents()
|
||||
.then(d => { setAgents(d.agents); setLoading(false); })
|
||||
.catch(() => setLoading(false));
|
||||
}, []);
|
||||
};
|
||||
|
||||
if (loading) return <LoadingSpinner />;
|
||||
useEffect(() => { loadAgents(); }, []);
|
||||
|
||||
const handleDelete = async (e: React.MouseEvent, name: string) => {
|
||||
e.stopPropagation();
|
||||
if (!confirm(`Voulez-vous vraiment supprimer l'agent "${name}" ?\n\n⚠️ Cette action supprimera définitivement son dossier de configuration.`)) return;
|
||||
setDeleting(name);
|
||||
try {
|
||||
await api.openclawDeleteAgent(name);
|
||||
if (expandedAgent === name) setExpandedAgent(null);
|
||||
loadAgents();
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
alert('Erreur lors de la suppression: ' + String(err));
|
||||
} finally {
|
||||
setDeleting(null);
|
||||
}
|
||||
};
|
||||
|
||||
const openFile = async (agentName: string, filename: string) => {
|
||||
if (activeFile?.agent === agentName && activeFile?.file === filename) {
|
||||
setActiveFile(null); // toggle close
|
||||
return;
|
||||
}
|
||||
setActiveFile({ agent: agentName, file: filename });
|
||||
setFileLoading(true);
|
||||
try {
|
||||
const data = await api.openclawReadAgentFile(agentName, filename);
|
||||
setFileContent(data.content || '');
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
setFileContent('Erreur de chargement du fichier.');
|
||||
} finally {
|
||||
setFileLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const saveFile = async () => {
|
||||
if (!activeFile) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
await api.openclawWriteAgentFile(activeFile.agent, activeFile.file, fileContent);
|
||||
loadAgents(); // refresh file sizes
|
||||
setActiveFile(null); // close the editor after saving
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
alert('Erreur lors de la sauvegarde: ' + String(err));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading && agents.length === 0) return <LoadingSpinner />;
|
||||
if (agents.length === 0) return <EmptyState message="Aucun agent trouvé dans le répertoire agents/" />;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Agent count header */}
|
||||
<div className="glass-card p-4 flex items-center gap-3">
|
||||
<span className="text-lg">🤖</span>
|
||||
<span className="text-white font-semibold">{agents.length} agents</span>
|
||||
<span className="text-gray-500 text-sm">détectés dans le répertoire OpenClaw</span>
|
||||
<div className="glass-card p-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-lg">🤖</span>
|
||||
<span className="text-white font-semibold">{agents.length} agents</span>
|
||||
<span className="text-gray-500 text-sm hidden sm:inline">détectés dans le répertoire OpenClaw</span>
|
||||
</div>
|
||||
<button onClick={loadAgents} className="text-gray-400 hover:text-white transition-colors" title="Actualiser">🔄</button>
|
||||
</div>
|
||||
|
||||
{/* Agent grid */}
|
||||
@@ -384,55 +428,122 @@ function AgentsTab() {
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className="p-5 cursor-pointer hover:bg-glass-hover transition-colors"
|
||||
onClick={() => setExpandedAgent(expandedAgent === agent.name ? null : agent.name)}
|
||||
className={`p-5 cursor-pointer hover:bg-glass-hover transition-colors ${deleting === agent.name ? 'opacity-50 pointer-events-none' : ''}`}
|
||||
onClick={() => { setExpandedAgent(expandedAgent === agent.name ? null : agent.name); setActiveFile(null); }}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-amber-500/20 to-orange-600/10 border border-amber-500/20 flex items-center justify-center text-xl">
|
||||
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-amber-500/20 to-orange-600/10 border border-amber-500/20 flex items-center justify-center text-xl shrink-0">
|
||||
🤖
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-white font-semibold text-sm">{agent.name}</h3>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<div className="min-w-0">
|
||||
<h3 className="text-white font-semibold text-sm truncate pr-2">{agent.name}</h3>
|
||||
<div className="flex items-center gap-2 mt-1 flex-wrap">
|
||||
<span className="badge bg-surface-600 text-gray-300 text-[10px]">{agent.type}</span>
|
||||
{agent.model && (
|
||||
<span className="badge bg-purple-500/15 text-purple-300 text-[10px]">{agent.model}</span>
|
||||
<span className="badge bg-purple-500/15 text-purple-300 text-[10px] truncate max-w-[150px]">{agent.model}</span>
|
||||
)}
|
||||
{agent.file_count != null && (
|
||||
<span className="text-[10px] text-gray-600">{agent.file_count} fichiers</span>
|
||||
{agent.has_workspace && (
|
||||
<span className="badge bg-green-500/15 text-green-300 text-[10px]" title="A un dossier workspace défini">ws ✓</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-gray-600 text-sm">{expandedAgent === agent.name ? '▲' : '▼'}</span>
|
||||
</div>
|
||||
|
||||
{/* Config files */}
|
||||
{agent.config_files && agent.config_files.length > 0 && (
|
||||
<div className="flex gap-2 mt-3 flex-wrap">
|
||||
{agent.config_files.map(f => (
|
||||
<span key={f} className="px-2 py-0.5 rounded-md bg-surface-700 text-gray-400 text-[10px] font-mono">{f}</span>
|
||||
))}
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<button
|
||||
onClick={(e) => handleDelete(e, agent.name)}
|
||||
className="p-1.5 rounded-lg text-gray-500 hover:text-red-400 hover:bg-red-400/10 transition-colors"
|
||||
title="Supprimer l'agent"
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
<span className="text-gray-600 text-sm ml-2 w-4 text-center">
|
||||
{expandedAgent === agent.name ? '▲' : '▼'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expanded details */}
|
||||
{expandedAgent === agent.name && (
|
||||
<div className="border-t border-glass-border p-5 bg-surface-900/40 space-y-4">
|
||||
{agent.system_prompt && (
|
||||
<div className="border-t border-glass-border p-5 bg-surface-900/40 space-y-6">
|
||||
|
||||
{/* Agent Identity Files Manager */}
|
||||
{agent.has_workspace && agent.identity_files && agent.identity_files.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-xs text-gray-500 uppercase tracking-wider mb-2">System Prompt</h4>
|
||||
<div className="p-3 rounded-xl bg-surface-800 text-xs text-gray-300 font-mono whitespace-pre-wrap max-h-48 overflow-y-auto leading-relaxed">
|
||||
<h4 className="text-xs text-gray-400 font-bold uppercase tracking-wider mb-3 flex items-center gap-2">
|
||||
<span>🧠</span> Fichiers d'identité
|
||||
</h4>
|
||||
<div className="flex gap-2 flex-wrap mb-3">
|
||||
{agent.identity_files.map(file => {
|
||||
const isActive = activeFile?.agent === agent.name && activeFile?.file === file.name;
|
||||
return (
|
||||
<button
|
||||
key={file.name}
|
||||
onClick={() => openFile(agent.name, file.name)}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-mono transition-all ${
|
||||
isActive
|
||||
? 'bg-gradient-to-r from-purple-600 to-indigo-600 text-white shadow-[0_0_10px_rgba(147,51,234,0.3)]'
|
||||
: file.size > 0
|
||||
? 'bg-surface-800 text-gray-300 border border-surface-600 hover:border-purple-500/50 hover:bg-surface-700'
|
||||
: 'bg-surface-800 text-gray-600 border border-surface-700/50 opacity-60 hover:opacity-100 hover:text-gray-400'
|
||||
}`}
|
||||
>
|
||||
{file.name}
|
||||
{file.size > 0 && <span className="ml-1.5 opacity-60 text-[9px]">{formatSize(file.size)}</span>}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Identity File Editor */}
|
||||
{activeFile?.agent === agent.name && (
|
||||
<div className="rounded-xl border border-glass-border bg-[#282c34] overflow-hidden shadow-inner mt-4 animate-fade-in relative">
|
||||
<div className="flex items-center justify-between px-3 py-2 bg-surface-900/80 border-b border-glass-border">
|
||||
<span className="text-xs font-mono text-purple-300">workspace/{agent.name}/{activeFile.file}</span>
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => setActiveFile(null)} className="px-2 py-1 rounded text-[10px] text-gray-400 hover:bg-surface-700 hover:text-white transition-colors">Fermer</button>
|
||||
<button onClick={saveFile} disabled={saving} className="px-3 py-1 rounded bg-purple-600/80 hover:bg-purple-500 text-white text-[10px] font-medium transition-colors">
|
||||
{saving ? '...' : 'Sauvegarder'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{fileLoading ? (
|
||||
<div className="p-8 text-center text-gray-500 text-sm animate-pulse">Chargement…</div>
|
||||
) : (
|
||||
<div className="max-h-80 overflow-y-auto custom-scrollbar [&_.cm-editor]:bg-transparent [&_.cm-scroller]:font-mono [&_.cm-scroller]:text-xs">
|
||||
<CodeMirror
|
||||
value={fileContent}
|
||||
theme={oneDark}
|
||||
extensions={[markdown()]}
|
||||
onChange={(val) => setFileContent(val)}
|
||||
basicSetup={{ lineNumbers: true, foldGutter: false, highlightActiveLine: true, tabSize: 2 }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* System Prompt (Read-only overlay for old config style) */}
|
||||
{agent.system_prompt && !activeFile && (
|
||||
<div>
|
||||
<h4 className="text-xs text-gray-400 font-bold uppercase tracking-wider mb-2">Instructions principales</h4>
|
||||
<div className="px-4 py-3 rounded-xl bg-surface-800/80 text-xs text-gray-300 font-mono whitespace-pre-wrap max-h-48 overflow-y-auto leading-relaxed custom-scrollbar border border-surface-700/50">
|
||||
{agent.system_prompt}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{agent.config && (
|
||||
|
||||
{/* Configuration (Read-only) */}
|
||||
{agent.config && !activeFile && (
|
||||
<div>
|
||||
<h4 className="text-xs text-gray-500 uppercase tracking-wider mb-2">Configuration complète</h4>
|
||||
<pre className="p-3 rounded-xl bg-surface-800 text-xs text-gray-300 font-mono whitespace-pre-wrap max-h-64 overflow-y-auto leading-relaxed">
|
||||
<h4 className="text-xs text-gray-400 font-bold uppercase tracking-wider mb-2 flex justify-between items-center">
|
||||
<span>Configuration JSON/YAML</span>
|
||||
<span className="font-mono lowercase text-[10px] text-gray-500">{agent.config_files?.join(', ')}</span>
|
||||
</h4>
|
||||
<pre className="px-4 py-3 rounded-xl bg-surface-800/80 text-[11px] text-gray-400 font-mono whitespace-pre-wrap max-h-64 overflow-y-auto leading-relaxed custom-scrollbar border border-surface-700/50">
|
||||
{JSON.stringify(agent.config, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
@@ -815,3 +926,186 @@ function EmptyState({ message }: { message: string }) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════════════
|
||||
// Tab 7 — Filesystem (Arborescence)
|
||||
// ═════════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function FilesystemTab() {
|
||||
const [tree, setTree] = useState<FileTreeNode[]>([]);
|
||||
const [treeLoading, setTreeLoading] = useState(true);
|
||||
const [selectedFile, setSelectedFile] = useState<ReadFileResult | null>(null);
|
||||
const [fileLoading, setFileLoading] = useState(false);
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
const [editContent, setEditContent] = useState('');
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadTree();
|
||||
}, []);
|
||||
|
||||
const loadTree = () => {
|
||||
setTreeLoading(true);
|
||||
api.openclawFilesystem()
|
||||
.then(d => { setTree(d.tree); setTreeLoading(false); })
|
||||
.catch(() => setTreeLoading(false));
|
||||
};
|
||||
|
||||
const handleSelectFile = async (node: FileTreeNode) => {
|
||||
if (node.type !== 'file') return;
|
||||
setFileLoading(true);
|
||||
setEditMode(false);
|
||||
try {
|
||||
const data = await api.openclawReadFile(node.path);
|
||||
setSelectedFile(data);
|
||||
setEditContent(data.pretty || data.content || '');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
setFileLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!selectedFile) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
await api.openclawWriteFile(selectedFile.path, editContent);
|
||||
const data = await api.openclawReadFile(selectedFile.path);
|
||||
setSelectedFile(data);
|
||||
setEditMode(false);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getExtensions = () => {
|
||||
if (!selectedFile) return [];
|
||||
const lang = selectedFile.language;
|
||||
if (lang === 'json') return [json()];
|
||||
if (lang === 'markdown') return [markdown()];
|
||||
if (lang === 'yaml') return [yaml()];
|
||||
return [];
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex gap-6 h-[calc(100vh-250px)] min-h-[600px]">
|
||||
{/* Sidebar Tree */}
|
||||
<div className="glass-card w-1/3 p-4 flex flex-col border border-glass-border">
|
||||
<div className="flex items-center justify-between mb-4 px-2">
|
||||
<h2 className="text-lg font-bold text-white flex items-center gap-2">
|
||||
<span>📂</span> Arborescence
|
||||
</h2>
|
||||
<button onClick={loadTree} className="p-1.5 rounded-lg text-gray-400 hover:text-white hover:bg-surface-700 transition-colors" title="Actualiser">🔄</button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto font-mono text-[13px] pr-2 custom-scrollbar">
|
||||
{treeLoading ? (
|
||||
<div className="text-gray-500 animate-pulse px-2">Chargement de l'arborescence…</div>
|
||||
) : tree.length === 0 ? (
|
||||
<p className="text-gray-500 px-2">Aucun fichier trouvé</p>
|
||||
) : (
|
||||
<div className="space-y-0.5">
|
||||
{tree.map(node => (
|
||||
<TreeNode key={node.path} node={node} depth={0} onSelect={handleSelectFile} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Editor Panel */}
|
||||
<div className="glass-card w-2/3 flex flex-col overflow-hidden border border-glass-border shadow-[0_8px_30px_rgb(0,0,0,0.5)]">
|
||||
{fileLoading ? (
|
||||
<div className="flex-1 flex items-center justify-center bg-surface-900/40">
|
||||
<div className="text-gray-400 flex flex-col items-center gap-3">
|
||||
<span className="text-3xl animate-spin-slow">⏳</span>
|
||||
<span className="text-sm tracking-wide">Ouverture du fichier…</span>
|
||||
</div>
|
||||
</div>
|
||||
) : !selectedFile ? (
|
||||
<div className="flex-1 flex items-center justify-center text-gray-500 flex-col gap-4 bg-surface-900/40">
|
||||
<span className="text-5xl opacity-30 grayscale">📄</span>
|
||||
<p className="tracking-wide">Sélectionnez un fichier pour le visualiser</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Editor Header */}
|
||||
<div className="p-4 bg-surface-800 border-b border-glass-border flex items-center justify-between z-10 shrink-0">
|
||||
<div className="min-w-0 pr-4">
|
||||
<h3 className="text-white font-medium flex items-center gap-2 text-base truncate">
|
||||
<span className="text-xl">📄</span> {selectedFile.path.split('/').pop()}
|
||||
</h3>
|
||||
<div className="flex items-center gap-3 mt-1 text-[11px] text-gray-400 font-mono">
|
||||
<span className="truncate max-w-[200px] md:max-w-xs" title={selectedFile.path}>{selectedFile.path}</span>
|
||||
<span className="opacity-50">•</span>
|
||||
<span>{formatSize(selectedFile.size)}</span>
|
||||
<span className="opacity-50">•</span>
|
||||
<span className="uppercase text-purple-300 bg-purple-500/10 px-1.5 py-0.5 rounded">{selectedFile.language}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 shrink-0">
|
||||
{!selectedFile.binary && (
|
||||
<>
|
||||
{editMode ? (
|
||||
<>
|
||||
<button onClick={() => setEditMode(false)} disabled={saving} className="px-3.5 py-1.5 rounded-lg bg-surface-700 text-gray-300 text-sm hover:bg-surface-600 transition-colors">
|
||||
Annuler
|
||||
</button>
|
||||
<button onClick={handleSave} disabled={saving} className={`px-4 py-1.5 rounded-lg bg-gradient-to-r from-emerald-600 to-teal-600 text-white text-sm font-medium hover:from-emerald-500 hover:to-teal-500 transition-all shadow-[0_0_12px_rgba(16,185,129,0.3)] flex items-center gap-2 ${saving ? 'opacity-50 cursor-not-allowed scale-95' : 'hover:scale-105'}`}>
|
||||
{saving ? 'Sauvegarde…' : '💾 Sauvegarder'}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button onClick={() => { setEditMode(true); setEditContent(selectedFile.pretty || selectedFile.content || ''); }} className="px-4 py-1.5 rounded-lg bg-gradient-to-r from-purple-600 to-indigo-600 text-white text-sm font-medium hover:from-purple-500 hover:to-indigo-500 transition-all shadow-[0_0_12px_rgba(147,51,234,0.3)] hover:scale-105 flex items-center gap-2">
|
||||
<span>✏️</span> Modifier
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Editor Body */}
|
||||
<div className="flex-1 overflow-hidden relative bg-[#282c34]">
|
||||
{selectedFile.binary ? (
|
||||
<div className="absolute inset-0 flex items-center justify-center text-gray-500 bg-surface-900/60">
|
||||
<div className="text-center">
|
||||
<div className="text-5xl mb-3 opacity-30">📦</div>
|
||||
<p className="tracking-wide">Fichier binaire. Aperçu non disponible.</p>
|
||||
</div>
|
||||
</div>
|
||||
) : editMode ? (
|
||||
<div className="h-full w-full absolute inset-0 [&_.cm-editor]:h-full [&_.cm-scroller]:h-full [&_.cm-scroller]:font-mono [&_.cm-scroller]:text-[13px]">
|
||||
<CodeMirror
|
||||
value={editContent}
|
||||
height="100%"
|
||||
theme={oneDark}
|
||||
extensions={getExtensions()}
|
||||
onChange={(val) => setEditContent(val)}
|
||||
basicSetup={{
|
||||
lineNumbers: true,
|
||||
foldGutter: true,
|
||||
highlightActiveLine: true,
|
||||
tabSize: 2,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-full overflow-y-auto w-full custom-scrollbar absolute inset-0">
|
||||
<div className="p-4 min-h-full">
|
||||
<pre className="text-[13px] text-[#abb2bf] font-mono whitespace-pre-wrap leading-relaxed">
|
||||
{selectedFile.pretty || selectedFile.content}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user