@drewsepsi/nextpi 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/BUILD_ID +1 -0
- package/.next/app-path-routes-manifest.json +13 -0
- package/.next/build/chunks/33f32_323ea524._.js +6766 -0
- package/.next/build/chunks/33f32_323ea524._.js.map +47 -0
- package/.next/build/chunks/[root-of-the-server]__6ead02f9._.js +500 -0
- package/.next/build/chunks/[root-of-the-server]__6ead02f9._.js.map +11 -0
- package/.next/build/chunks/[root-of-the-server]__777e9116._.js +206 -0
- package/.next/build/chunks/[root-of-the-server]__777e9116._.js.map +8 -0
- package/.next/build/chunks/[turbopack-node]_transforms_postcss_ts_597188b4._.js +13 -0
- package/.next/build/chunks/[turbopack-node]_transforms_postcss_ts_597188b4._.js.map +5 -0
- package/.next/build/chunks/[turbopack]_runtime.js +795 -0
- package/.next/build/chunks/[turbopack]_runtime.js.map +10 -0
- package/.next/build/package.json +1 -0
- package/.next/build/postcss.js +6 -0
- package/.next/build/postcss.js.map +5 -0
- package/.next/build-manifest.json +19 -0
- package/.next/cache/.previewinfo +1 -0
- package/.next/cache/.rscinfo +1 -0
- package/.next/cache/.tsbuildinfo +1 -0
- package/.next/diagnostics/build-diagnostics.json +6 -0
- package/.next/diagnostics/framework.json +1 -0
- package/.next/export-marker.json +6 -0
- package/.next/fallback-build-manifest.json +12 -0
- package/.next/images-manifest.json +67 -0
- package/.next/next-minimal-server.js.nft.json +1 -0
- package/.next/next-server.js.nft.json +1 -0
- package/.next/package.json +1 -0
- package/.next/prerender-manifest.json +114 -0
- package/.next/required-server-files.js +324 -0
- package/.next/required-server-files.json +324 -0
- package/.next/routes-manifest.json +113 -0
- package/.next/server/app/_global-error/page/app-paths-manifest.json +3 -0
- package/.next/server/app/_global-error/page/build-manifest.json +16 -0
- package/.next/server/app/_global-error/page/next-font-manifest.json +6 -0
- package/.next/server/app/_global-error/page/react-loadable-manifest.json +1 -0
- package/.next/server/app/_global-error/page/server-reference-manifest.json +4 -0
- package/.next/server/app/_global-error/page.js +11 -0
- package/.next/server/app/_global-error/page.js.map +5 -0
- package/.next/server/app/_global-error/page.js.nft.json +1 -0
- package/.next/server/app/_global-error/page_client-reference-manifest.js +2 -0
- package/.next/server/app/_global-error.html +2 -0
- package/.next/server/app/_global-error.meta +15 -0
- package/.next/server/app/_global-error.rsc +13 -0
- package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +5 -0
- package/.next/server/app/_global-error.segments/_full.segment.rsc +13 -0
- package/.next/server/app/_global-error.segments/_head.segment.rsc +6 -0
- package/.next/server/app/_global-error.segments/_index.segment.rsc +4 -0
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -0
- package/.next/server/app/_not-found/page/app-paths-manifest.json +3 -0
- package/.next/server/app/_not-found/page/build-manifest.json +16 -0
- package/.next/server/app/_not-found/page/next-font-manifest.json +11 -0
- package/.next/server/app/_not-found/page/react-loadable-manifest.json +1 -0
- package/.next/server/app/_not-found/page/server-reference-manifest.json +4 -0
- package/.next/server/app/_not-found/page.js +14 -0
- package/.next/server/app/_not-found/page.js.map +5 -0
- package/.next/server/app/_not-found/page.js.nft.json +1 -0
- package/.next/server/app/_not-found/page_client-reference-manifest.js +2 -0
- package/.next/server/app/_not-found.html +1 -0
- package/.next/server/app/_not-found.meta +16 -0
- package/.next/server/app/_not-found.rsc +15 -0
- package/.next/server/app/_not-found.segments/_full.segment.rsc +15 -0
- package/.next/server/app/_not-found.segments/_head.segment.rsc +6 -0
- package/.next/server/app/_not-found.segments/_index.segment.rsc +6 -0
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +5 -0
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +4 -0
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -0
- package/.next/server/app/api/chat/route/app-paths-manifest.json +3 -0
- package/.next/server/app/api/chat/route/build-manifest.json +11 -0
- package/.next/server/app/api/chat/route/server-reference-manifest.json +4 -0
- package/.next/server/app/api/chat/route.js +7 -0
- package/.next/server/app/api/chat/route.js.map +5 -0
- package/.next/server/app/api/chat/route.js.nft.json +1 -0
- package/.next/server/app/api/chat/route_client-reference-manifest.js +2 -0
- package/.next/server/app/api/config/route/app-paths-manifest.json +3 -0
- package/.next/server/app/api/config/route/build-manifest.json +11 -0
- package/.next/server/app/api/config/route/server-reference-manifest.json +4 -0
- package/.next/server/app/api/config/route.js +7 -0
- package/.next/server/app/api/config/route.js.map +5 -0
- package/.next/server/app/api/config/route.js.nft.json +1 -0
- package/.next/server/app/api/config/route_client-reference-manifest.js +2 -0
- package/.next/server/app/api/files/[...path]/route/app-paths-manifest.json +3 -0
- package/.next/server/app/api/files/[...path]/route/build-manifest.json +11 -0
- package/.next/server/app/api/files/[...path]/route/server-reference-manifest.json +4 -0
- package/.next/server/app/api/files/[...path]/route.js +7 -0
- package/.next/server/app/api/files/[...path]/route.js.map +5 -0
- package/.next/server/app/api/files/[...path]/route.js.nft.json +1 -0
- package/.next/server/app/api/files/[...path]/route_client-reference-manifest.js +2 -0
- package/.next/server/app/api/files/route/app-paths-manifest.json +3 -0
- package/.next/server/app/api/files/route/build-manifest.json +11 -0
- package/.next/server/app/api/files/route/server-reference-manifest.json +4 -0
- package/.next/server/app/api/files/route.js +7 -0
- package/.next/server/app/api/files/route.js.map +5 -0
- package/.next/server/app/api/files/route.js.nft.json +1 -0
- package/.next/server/app/api/files/route_client-reference-manifest.js +2 -0
- package/.next/server/app/api/messages/route/app-paths-manifest.json +3 -0
- package/.next/server/app/api/messages/route/build-manifest.json +11 -0
- package/.next/server/app/api/messages/route/server-reference-manifest.json +4 -0
- package/.next/server/app/api/messages/route.js +7 -0
- package/.next/server/app/api/messages/route.js.map +5 -0
- package/.next/server/app/api/messages/route.js.nft.json +1 -0
- package/.next/server/app/api/messages/route_client-reference-manifest.js +2 -0
- package/.next/server/app/api/session/route/app-paths-manifest.json +3 -0
- package/.next/server/app/api/session/route/build-manifest.json +11 -0
- package/.next/server/app/api/session/route/server-reference-manifest.json +4 -0
- package/.next/server/app/api/session/route.js +7 -0
- package/.next/server/app/api/session/route.js.map +5 -0
- package/.next/server/app/api/session/route.js.nft.json +1 -0
- package/.next/server/app/api/session/route_client-reference-manifest.js +2 -0
- package/.next/server/app/api/stream/route/app-paths-manifest.json +3 -0
- package/.next/server/app/api/stream/route/build-manifest.json +11 -0
- package/.next/server/app/api/stream/route/server-reference-manifest.json +4 -0
- package/.next/server/app/api/stream/route.js +6 -0
- package/.next/server/app/api/stream/route.js.map +5 -0
- package/.next/server/app/api/stream/route.js.nft.json +1 -0
- package/.next/server/app/api/stream/route_client-reference-manifest.js +2 -0
- package/.next/server/app/favicon.ico/route/app-paths-manifest.json +3 -0
- package/.next/server/app/favicon.ico/route/build-manifest.json +11 -0
- package/.next/server/app/favicon.ico/route.js +8 -0
- package/.next/server/app/favicon.ico/route.js.map +5 -0
- package/.next/server/app/favicon.ico/route.js.nft.json +1 -0
- package/.next/server/app/favicon.ico.body +0 -0
- package/.next/server/app/favicon.ico.meta +1 -0
- package/.next/server/app/index.html +1 -0
- package/.next/server/app/index.meta +14 -0
- package/.next/server/app/index.rsc +21 -0
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +9 -0
- package/.next/server/app/index.segments/_full.segment.rsc +21 -0
- package/.next/server/app/index.segments/_head.segment.rsc +6 -0
- package/.next/server/app/index.segments/_index.segment.rsc +6 -0
- package/.next/server/app/index.segments/_tree.segment.rsc +4 -0
- package/.next/server/app/page/app-paths-manifest.json +3 -0
- package/.next/server/app/page/build-manifest.json +16 -0
- package/.next/server/app/page/next-font-manifest.json +11 -0
- package/.next/server/app/page/react-loadable-manifest.json +1 -0
- package/.next/server/app/page/server-reference-manifest.json +4 -0
- package/.next/server/app/page.js +16 -0
- package/.next/server/app/page.js.map +5 -0
- package/.next/server/app/page.js.nft.json +1 -0
- package/.next/server/app/page_client-reference-manifest.js +2 -0
- package/.next/server/app-paths-manifest.json +13 -0
- package/.next/server/chunks/33f32_next_b78429d5._.js +17 -0
- package/.next/server/chunks/33f32_next_b78429d5._.js.map +1 -0
- package/.next/server/chunks/33f32_next_dist_esm_build_templates_app-route_5a539e27.js +3 -0
- package/.next/server/chunks/33f32_next_dist_esm_build_templates_app-route_5a539e27.js.map +1 -0
- package/.next/server/chunks/[externals]_next_dist_b89b5a39._.js +3 -0
- package/.next/server/chunks/[externals]_next_dist_b89b5a39._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__2554f0b0._.js +17 -0
- package/.next/server/chunks/[root-of-the-server]__2554f0b0._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__5e671cad._.js +7 -0
- package/.next/server/chunks/[root-of-the-server]__5e671cad._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__629f5815._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__629f5815._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__633f9ccd._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__633f9ccd._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__65cdeb73._.js +17 -0
- package/.next/server/chunks/[root-of-the-server]__65cdeb73._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__bc57ef5b._.js +21 -0
- package/.next/server/chunks/[root-of-the-server]__bc57ef5b._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__be62c218._.js +17 -0
- package/.next/server/chunks/[root-of-the-server]__be62c218._.js.map +1 -0
- package/.next/server/chunks/[root-of-the-server]__e87dbf93._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__e87dbf93._.js.map +1 -0
- package/.next/server/chunks/[turbopack]_runtime.js +795 -0
- package/.next/server/chunks/[turbopack]_runtime.js.map +10 -0
- package/.next/server/chunks/c45a0_nextpi__next-internal_server_app_api_files_[___path]_route_actions_16a66e62.js +3 -0
- package/.next/server/chunks/c45a0_nextpi__next-internal_server_app_api_files_[___path]_route_actions_16a66e62.js.map +1 -0
- package/.next/server/chunks/picode_nextpi__next-internal_server_app_api_chat_route_actions_e342cdbf.js +3 -0
- package/.next/server/chunks/picode_nextpi__next-internal_server_app_api_chat_route_actions_e342cdbf.js.map +1 -0
- package/.next/server/chunks/picode_nextpi__next-internal_server_app_api_config_route_actions_04d1272c.js +3 -0
- package/.next/server/chunks/picode_nextpi__next-internal_server_app_api_config_route_actions_04d1272c.js.map +1 -0
- package/.next/server/chunks/picode_nextpi__next-internal_server_app_api_files_route_actions_a8035013.js +3 -0
- package/.next/server/chunks/picode_nextpi__next-internal_server_app_api_files_route_actions_a8035013.js.map +1 -0
- package/.next/server/chunks/picode_nextpi__next-internal_server_app_api_messages_route_actions_4f52ac4b.js +3 -0
- package/.next/server/chunks/picode_nextpi__next-internal_server_app_api_messages_route_actions_4f52ac4b.js.map +1 -0
- package/.next/server/chunks/picode_nextpi__next-internal_server_app_api_session_route_actions_ff684c69.js +3 -0
- package/.next/server/chunks/picode_nextpi__next-internal_server_app_api_session_route_actions_ff684c69.js.map +1 -0
- package/.next/server/chunks/picode_nextpi__next-internal_server_app_api_stream_route_actions_7a0f6dba.js +3 -0
- package/.next/server/chunks/picode_nextpi__next-internal_server_app_api_stream_route_actions_7a0f6dba.js.map +1 -0
- package/.next/server/chunks/picode_nextpi__next-internal_server_app_favicon_ico_route_actions_2526216f.js +3 -0
- package/.next/server/chunks/picode_nextpi__next-internal_server_app_favicon_ico_route_actions_2526216f.js.map +1 -0
- package/.next/server/chunks/ssr/33f32_next_dist_580d3573._.js +6 -0
- package/.next/server/chunks/ssr/33f32_next_dist_580d3573._.js.map +1 -0
- package/.next/server/chunks/ssr/33f32_next_dist_85b0ccc0._.js +4 -0
- package/.next/server/chunks/ssr/33f32_next_dist_85b0ccc0._.js.map +1 -0
- package/.next/server/chunks/ssr/33f32_next_dist_client_components_a8ffe8d4._.js +3 -0
- package/.next/server/chunks/ssr/33f32_next_dist_client_components_a8ffe8d4._.js.map +1 -0
- package/.next/server/chunks/ssr/33f32_next_dist_client_components_builtin_forbidden_ad97660b.js +3 -0
- package/.next/server/chunks/ssr/33f32_next_dist_client_components_builtin_forbidden_ad97660b.js.map +1 -0
- package/.next/server/chunks/ssr/33f32_next_dist_client_components_builtin_global-error_ab28468b.js +3 -0
- package/.next/server/chunks/ssr/33f32_next_dist_client_components_builtin_global-error_ab28468b.js.map +1 -0
- package/.next/server/chunks/ssr/33f32_next_dist_client_components_builtin_unauthorized_edd1fe08.js +3 -0
- package/.next/server/chunks/ssr/33f32_next_dist_client_components_builtin_unauthorized_edd1fe08.js.map +1 -0
- package/.next/server/chunks/ssr/33f32_next_dist_esm_build_templates_app-page_5ef96b51.js +4 -0
- package/.next/server/chunks/ssr/33f32_next_dist_esm_build_templates_app-page_5ef96b51.js.map +1 -0
- package/.next/server/chunks/ssr/33f32_next_dist_server_route-modules_app-page_vendored_ssr_react-dom_33aeae20.js +3 -0
- package/.next/server/chunks/ssr/33f32_next_dist_server_route-modules_app-page_vendored_ssr_react-dom_33aeae20.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__028106ef._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__028106ef._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0dc9a9e1._.js +4 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0dc9a9e1._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__15f57d5d._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__15f57d5d._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__26f2451b._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__26f2451b._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__42ddc417._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__42ddc417._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__6823e2d4._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__6823e2d4._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__6bf5dd5d._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__6bf5dd5d._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__7b7d477c._.js +10 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__7b7d477c._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__85c46341._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__85c46341._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__99c61367._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__99c61367._.js.map +1 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__a0bdeac8._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__a0bdeac8._.js.map +1 -0
- package/.next/server/chunks/ssr/[turbopack]_runtime.js +795 -0
- package/.next/server/chunks/ssr/[turbopack]_runtime.js.map +10 -0
- package/.next/server/chunks/ssr/_091ac666._.js +3 -0
- package/.next/server/chunks/ssr/_091ac666._.js.map +1 -0
- package/.next/server/chunks/ssr/picode_nextpi_1e69e558._.js +4 -0
- package/.next/server/chunks/ssr/picode_nextpi_1e69e558._.js.map +1 -0
- package/.next/server/chunks/ssr/picode_nextpi_4fadcd01._.js +3 -0
- package/.next/server/chunks/ssr/picode_nextpi_4fadcd01._.js.map +1 -0
- package/.next/server/chunks/ssr/picode_nextpi__next-internal_server_app__global-error_page_actions_91eb1cf1.js +3 -0
- package/.next/server/chunks/ssr/picode_nextpi__next-internal_server_app__global-error_page_actions_91eb1cf1.js.map +1 -0
- package/.next/server/chunks/ssr/picode_nextpi__next-internal_server_app__not-found_page_actions_0a31df41.js +3 -0
- package/.next/server/chunks/ssr/picode_nextpi__next-internal_server_app__not-found_page_actions_0a31df41.js.map +1 -0
- package/.next/server/chunks/ssr/picode_nextpi__next-internal_server_app_page_actions_b14d985f.js +3 -0
- package/.next/server/chunks/ssr/picode_nextpi__next-internal_server_app_page_actions_b14d985f.js.map +1 -0
- package/.next/server/chunks/ssr/picode_nextpi_app_77d1bd95._.js +3 -0
- package/.next/server/chunks/ssr/picode_nextpi_app_77d1bd95._.js.map +1 -0
- package/.next/server/chunks/ssr/picode_nextpi_app_page_tsx_cebd7814._.js +3 -0
- package/.next/server/chunks/ssr/picode_nextpi_app_page_tsx_cebd7814._.js.map +1 -0
- package/.next/server/functions-config-manifest.json +4 -0
- package/.next/server/interception-route-rewrite-manifest.js +1 -0
- package/.next/server/middleware-build-manifest.js +20 -0
- package/.next/server/middleware-manifest.json +6 -0
- package/.next/server/next-font-manifest.js +1 -0
- package/.next/server/next-font-manifest.json +15 -0
- package/.next/server/pages/404.html +1 -0
- package/.next/server/pages/500.html +2 -0
- package/.next/server/pages-manifest.json +4 -0
- package/.next/server/server-reference-manifest.js +1 -0
- package/.next/server/server-reference-manifest.json +5 -0
- package/.next/static/PdRtiPZ_ExBLCzmKGXOBT/_buildManifest.js +11 -0
- package/.next/static/PdRtiPZ_ExBLCzmKGXOBT/_clientMiddlewareManifest.json +1 -0
- package/.next/static/PdRtiPZ_ExBLCzmKGXOBT/_ssgManifest.js +1 -0
- package/.next/static/chunks/0ab43a45e07af0f9.js +1 -0
- package/.next/static/chunks/265e06106152dcbb.js +1 -0
- package/.next/static/chunks/65d5124f3edd1b84.js +1 -0
- package/.next/static/chunks/76111d9b2044b643.js +1 -0
- package/.next/static/chunks/81dfdd74986c8afe.css +3 -0
- package/.next/static/chunks/93b85e5de0842835.js +5 -0
- package/.next/static/chunks/a6dad97d9634a72d.js +1 -0
- package/.next/static/chunks/a6dad97d9634a72d.js.map +1 -0
- package/.next/static/chunks/cace1b477872431f.js +1 -0
- package/.next/static/chunks/turbopack-775e95d747b6e667.js +4 -0
- package/.next/static/media/4fa387ec64143e14-s.c1fdd6c2.woff2 +0 -0
- package/.next/static/media/7178b3e590c64307-s.b97b3418.woff2 +0 -0
- package/.next/static/media/797e433ab948586e-s.p.dbea232f.woff2 +0 -0
- package/.next/static/media/8a480f0b521d4e75-s.8e0177b5.woff2 +0 -0
- package/.next/static/media/bbc41e54d2fcbd21-s.799d8ef8.woff2 +0 -0
- package/.next/static/media/caa3a2e1cccd8315-s.p.853070df.woff2 +0 -0
- package/.next/static/media/favicon.0b3bf435.ico +0 -0
- package/.next/trace +1 -0
- package/.next/trace-build +1 -0
- package/.next/turbopack +0 -0
- package/.next/types/routes.d.ts +79 -0
- package/.next/types/validator.ts +133 -0
- package/README.md +50 -0
- package/app/api/chat/route.ts +28 -0
- package/app/api/config/route.ts +25 -0
- package/app/api/files/[...path]/route.ts +55 -0
- package/app/api/files/route.ts +62 -0
- package/app/api/messages/route.ts +26 -0
- package/app/api/session/route.ts +55 -0
- package/app/api/stream/route.ts +76 -0
- package/app/favicon.ico +0 -0
- package/app/globals.css +1143 -0
- package/app/layout.tsx +46 -0
- package/app/page.tsx +1643 -0
- package/bin/cli.js +85 -0
- package/components/.gitkeep +0 -0
- package/components/theme-provider.tsx +71 -0
- package/components/ui/button.tsx +67 -0
- package/components/ui/file-tree.tsx +560 -0
- package/lib/.gitkeep +0 -0
- package/lib/agent.ts +105 -0
- package/lib/config.ts +50 -0
- package/lib/root-path.ts +15 -0
- package/lib/session-singleton.ts +48 -0
- package/lib/utils.ts +6 -0
- package/package.json +72 -0
- package/public/.gitkeep +0 -0
- package/src/agent.ts +82 -0
- package/src/cli.ts +107 -0
- package/src/tools.ts +71 -0
- package/src/tui.ts +238 -0
- package/src/web-server.ts +280 -0
- package/tsconfig.json +34 -0
package/app/page.tsx
ADDED
|
@@ -0,0 +1,1643 @@
|
|
|
1
|
+
// Styling updated to use globals.css — visual changes only
|
|
2
|
+
'use client';
|
|
3
|
+
|
|
4
|
+
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
5
|
+
import {
|
|
6
|
+
Search,
|
|
7
|
+
RefreshCw,
|
|
8
|
+
Send,
|
|
9
|
+
Terminal,
|
|
10
|
+
Trash2,
|
|
11
|
+
ChevronUp,
|
|
12
|
+
ChevronDown,
|
|
13
|
+
Activity,
|
|
14
|
+
Folder,
|
|
15
|
+
FolderOpen,
|
|
16
|
+
Brain,
|
|
17
|
+
User,
|
|
18
|
+
Bot,
|
|
19
|
+
Wrench,
|
|
20
|
+
FileText,
|
|
21
|
+
Save,
|
|
22
|
+
X,
|
|
23
|
+
FilePlus,
|
|
24
|
+
RotateCcw,
|
|
25
|
+
AlertTriangle,
|
|
26
|
+
Undo,
|
|
27
|
+
ChevronsDownUp,
|
|
28
|
+
ChevronsUpDown,
|
|
29
|
+
Play,
|
|
30
|
+
Square,
|
|
31
|
+
Globe,
|
|
32
|
+
ExternalLink,
|
|
33
|
+
HelpCircle,
|
|
34
|
+
Command,
|
|
35
|
+
Settings,
|
|
36
|
+
} from 'lucide-react';
|
|
37
|
+
import { Tree, type TreeViewElement } from '@/components/ui/file-tree';
|
|
38
|
+
|
|
39
|
+
// Types
|
|
40
|
+
interface Message {
|
|
41
|
+
id: string;
|
|
42
|
+
content: string;
|
|
43
|
+
thinking?: string;
|
|
44
|
+
isUser: boolean;
|
|
45
|
+
isStreaming?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface Activity {
|
|
49
|
+
id: string;
|
|
50
|
+
name: string;
|
|
51
|
+
type: string;
|
|
52
|
+
status: 'running' | 'success' | 'error';
|
|
53
|
+
details?: string;
|
|
54
|
+
time: string;
|
|
55
|
+
elapsed?: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface TerminalLine {
|
|
59
|
+
id: string;
|
|
60
|
+
content: string;
|
|
61
|
+
type: 'info' | 'command' | 'output' | 'error' | 'success';
|
|
62
|
+
timestamp: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface SessionInfo {
|
|
66
|
+
model: string;
|
|
67
|
+
messages: number;
|
|
68
|
+
tokens: number;
|
|
69
|
+
tools: string[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Markdown renderer
|
|
73
|
+
function renderMarkdown(text: string): string {
|
|
74
|
+
let html = text
|
|
75
|
+
.replace(/&/g, '&')
|
|
76
|
+
.replace(/</g, '<')
|
|
77
|
+
.replace(/>/g, '>');
|
|
78
|
+
|
|
79
|
+
// Code blocks
|
|
80
|
+
html = html.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => {
|
|
81
|
+
return `<pre><code class="language-${lang || 'text'}">${code.trim()}</code></pre>`;
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Inline code
|
|
85
|
+
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
86
|
+
|
|
87
|
+
// Headers
|
|
88
|
+
html = html.replace(/^### (.*$)/gm, '<h3>$1</h3>');
|
|
89
|
+
html = html.replace(/^## (.*$)/gm, '<h2>$1</h2>');
|
|
90
|
+
html = html.replace(/^# (.*$)/gm, '<h1>$1</h1>');
|
|
91
|
+
|
|
92
|
+
// Bold and italic
|
|
93
|
+
html = html.replace(/\*\*\*(.*?)\*\*\*/g, '<strong><em>$1</em></strong>');
|
|
94
|
+
html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
|
95
|
+
html = html.replace(/\*(.*?)\*/g, '<em>$1</em>');
|
|
96
|
+
|
|
97
|
+
// Links
|
|
98
|
+
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
|
|
99
|
+
|
|
100
|
+
// Line breaks
|
|
101
|
+
html = html.replace(/\n\n/g, '</p><p>');
|
|
102
|
+
html = html.replace(/\n/g, '<br>');
|
|
103
|
+
|
|
104
|
+
if (!html.startsWith('<')) {
|
|
105
|
+
html = `<p>${html}</p>`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return html;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const MODELS = [
|
|
112
|
+
{ id: 'openrouter/free', name: 'OpenRouter Free (Auto)', description: 'Fast and reliable, uses free models.' },
|
|
113
|
+
{ id: 'anthropic/claude-3.5-sonnet', name: 'Claude 3.5 Sonnet', description: 'Very capable, balanced speed & intelligence.' },
|
|
114
|
+
{ id: 'anthropic/claude-3-opus', name: 'Claude 3 Opus', description: 'Most powerful Claude model.' },
|
|
115
|
+
{ id: 'openai/gpt-4o', name: 'GPT-4o', description: 'Flagship OpenAI model.' },
|
|
116
|
+
{ id: 'openai/gpt-4o-mini', name: 'GPT-4o Mini', description: 'Fastest OpenAI model.' },
|
|
117
|
+
{ id: 'google/gemini-2.0-flash-exp:free', name: 'Gemini 2.0 Flash', description: "Google's fastest large model (Free Preview)." },
|
|
118
|
+
{ id: 'google/gemini-pro-1.5', name: 'Gemini 1.5 Pro', description: "Google's most capable model." },
|
|
119
|
+
{ id: 'meta-llama/llama-3.2-90b-vision-instruct', name: 'Llama 3.2 90B', description: "Meta's latest high-performing open model." },
|
|
120
|
+
{ id: 'qwen/qwen-2.5-72b-instruct', name: 'Qwen 2.5 72B', description: 'Powerful instruction-tuned model from Alibaba.' },
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
export default function Home() {
|
|
124
|
+
// State
|
|
125
|
+
const [messages, setMessages] = useState<Message[]>([]);
|
|
126
|
+
const [input, setInput] = useState('');
|
|
127
|
+
const [isStreaming, setIsStreaming] = useState(false);
|
|
128
|
+
const [currentMessage, setCurrentMessage] = useState('');
|
|
129
|
+
const [currentThinking, setCurrentThinking] = useState('');
|
|
130
|
+
const [activities, setActivities] = useState<Activity[]>([]);
|
|
131
|
+
const [terminalLines, setTerminalLines] = useState<TerminalLine[]>([]);
|
|
132
|
+
const [isTerminalExpanded, setIsTerminalExpanded] = useState(false);
|
|
133
|
+
const [activeTab, setActiveTab] = useState<'activity' | 'files'>('activity');
|
|
134
|
+
const [connectionStatus, setConnectionStatus] = useState<'connecting' | 'connected' | 'error'>('connecting');
|
|
135
|
+
const [sessionInfo, setSessionInfo] = useState<SessionInfo | null>(null);
|
|
136
|
+
const [showThinking, setShowThinking] = useState(true);
|
|
137
|
+
const [fileElements, setFileElements] = useState<TreeViewElement[]>([]);
|
|
138
|
+
const [expandedFolders, setExpandedFolders] = useState<string[]>([]);
|
|
139
|
+
const [selectedFileId, setSelectedFileId] = useState<string | undefined>(undefined);
|
|
140
|
+
const isInitialLoad = useRef(true);
|
|
141
|
+
|
|
142
|
+
// File Editor State
|
|
143
|
+
const [editingFile, setEditingFile] = useState<{ id: string, name: string } | null>(null);
|
|
144
|
+
const [editorContent, setEditorContent] = useState('');
|
|
145
|
+
const [originalContent, setOriginalContent] = useState('');
|
|
146
|
+
const [isEditorOpen, setIsEditorOpen] = useState(false);
|
|
147
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
148
|
+
const [isDeleting, setIsDeleting] = useState(false);
|
|
149
|
+
const [isDeleteConfirmOpen, setIsDeleteConfirmOpen] = useState(false);
|
|
150
|
+
const [isNewFileModalOpen, setIsNewFileModalOpen] = useState(false);
|
|
151
|
+
const [newFileName, setNewFileName] = useState('');
|
|
152
|
+
const [targetDirectory, setTargetDirectory] = useState<string>('');
|
|
153
|
+
const [contextMenu, setContextMenu] = useState<{ x: number, y: number, folderId: string } | null>(null);
|
|
154
|
+
const [isModelSelectorOpen, setIsModelSelectorOpen] = useState(false);
|
|
155
|
+
|
|
156
|
+
// Preview State
|
|
157
|
+
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
|
|
158
|
+
const [previewUrl, setPreviewUrl] = useState('http://localhost:3000');
|
|
159
|
+
|
|
160
|
+
// Settings & Config State
|
|
161
|
+
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
|
162
|
+
const [configInfo, setConfigInfo] = useState<{ hasKey: boolean, maskedKey: string | null }>({ hasKey: false, maskedKey: null });
|
|
163
|
+
const [tempApiKey, setTempApiKey] = useState('');
|
|
164
|
+
const [isSavingConfig, setIsSavingConfig] = useState(false);
|
|
165
|
+
const [isHelpOpen, setIsHelpOpen] = useState(false);
|
|
166
|
+
|
|
167
|
+
// Refs
|
|
168
|
+
const chatRef = useRef<HTMLDivElement>(null);
|
|
169
|
+
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
170
|
+
const eventSourceRef = useRef<EventSource | null>(null);
|
|
171
|
+
const activityTimers = useRef<Map<string, number>>(new Map());
|
|
172
|
+
const accumulatedMessageRef = useRef<string>('');
|
|
173
|
+
const accumulatedThinkingRef = useRef<string>('');
|
|
174
|
+
|
|
175
|
+
// Scroll to bottom of chat
|
|
176
|
+
const scrollToBottom = useCallback(() => {
|
|
177
|
+
if (chatRef.current) {
|
|
178
|
+
chatRef.current.scrollTop = chatRef.current.scrollHeight;
|
|
179
|
+
}
|
|
180
|
+
}, []);
|
|
181
|
+
|
|
182
|
+
// Add terminal line
|
|
183
|
+
const addTerminalLine = useCallback((content: string, type: TerminalLine['type'] = 'info') => {
|
|
184
|
+
const timestamp = new Date().toLocaleTimeString('en-US', {
|
|
185
|
+
hour12: false,
|
|
186
|
+
hour: '2-digit',
|
|
187
|
+
minute: '2-digit',
|
|
188
|
+
second: '2-digit'
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
setTerminalLines(prev => [...prev, {
|
|
192
|
+
id: `term-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
|
|
193
|
+
content,
|
|
194
|
+
type,
|
|
195
|
+
timestamp
|
|
196
|
+
}]);
|
|
197
|
+
}, []);
|
|
198
|
+
|
|
199
|
+
// Add activity
|
|
200
|
+
const addActivity = useCallback((name: string, type: string, details?: string, explicitId?: string) => {
|
|
201
|
+
// Use explicitId (e.g. tool name) when provided so tool_end can find it by the same key
|
|
202
|
+
const id = explicitId ?? `activity-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
203
|
+
const time = new Date().toLocaleTimeString('en-US', {
|
|
204
|
+
hour12: false,
|
|
205
|
+
hour: '2-digit',
|
|
206
|
+
minute: '2-digit'
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const activity: Activity = {
|
|
210
|
+
id,
|
|
211
|
+
name,
|
|
212
|
+
type,
|
|
213
|
+
status: 'running',
|
|
214
|
+
details,
|
|
215
|
+
time,
|
|
216
|
+
elapsed: 0
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
setActivities(prev => [activity, ...prev]);
|
|
220
|
+
|
|
221
|
+
// Start elapsed timer
|
|
222
|
+
const startTime = Date.now();
|
|
223
|
+
const timer = window.setInterval(() => {
|
|
224
|
+
setActivities(prev => prev.map(a =>
|
|
225
|
+
a.id === id ? { ...a, elapsed: Math.round((Date.now() - startTime) / 100) / 10 } : a
|
|
226
|
+
));
|
|
227
|
+
}, 100);
|
|
228
|
+
|
|
229
|
+
activityTimers.current.set(id, timer);
|
|
230
|
+
return id;
|
|
231
|
+
}, []);
|
|
232
|
+
|
|
233
|
+
// Get all folders for selection
|
|
234
|
+
const getAllFolders = useCallback((elements: TreeViewElement[]): { id: string, name: string }[] => {
|
|
235
|
+
const folders: { id: string, name: string }[] = [{ id: '', name: 'Project Root' }];
|
|
236
|
+
|
|
237
|
+
const scan = (items: TreeViewElement[]) => {
|
|
238
|
+
for (const item of items) {
|
|
239
|
+
if (item.type === 'folder') {
|
|
240
|
+
folders.push({ id: item.id, name: item.id });
|
|
241
|
+
if (item.children) scan(item.children);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
scan(elements);
|
|
247
|
+
return folders;
|
|
248
|
+
}, []);
|
|
249
|
+
|
|
250
|
+
const handleAddNewFile = useCallback((dirId: string = '') => {
|
|
251
|
+
setTargetDirectory(dirId);
|
|
252
|
+
setNewFileName('');
|
|
253
|
+
setIsNewFileModalOpen(true);
|
|
254
|
+
setContextMenu(null);
|
|
255
|
+
}, []);
|
|
256
|
+
const handleContextMenu = useCallback((e: React.MouseEvent, folderId: string) => {
|
|
257
|
+
e.preventDefault();
|
|
258
|
+
setContextMenu({ x: e.clientX, y: e.clientY, folderId });
|
|
259
|
+
}, []);
|
|
260
|
+
|
|
261
|
+
// Fetch config on mount
|
|
262
|
+
useEffect(() => {
|
|
263
|
+
fetch('/api/config')
|
|
264
|
+
.then(res => res.json())
|
|
265
|
+
.then(data => {
|
|
266
|
+
setConfigInfo(data);
|
|
267
|
+
if (!data.hasKey) {
|
|
268
|
+
setIsSettingsOpen(true);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
}, []);
|
|
272
|
+
|
|
273
|
+
const handleSaveConfig = async () => {
|
|
274
|
+
if (!tempApiKey.trim()) return;
|
|
275
|
+
setIsSavingConfig(true);
|
|
276
|
+
try {
|
|
277
|
+
const res = await fetch('/api/config', {
|
|
278
|
+
method: 'POST',
|
|
279
|
+
headers: { 'Content-Type': 'application/json' },
|
|
280
|
+
body: JSON.stringify({ apiKey: tempApiKey })
|
|
281
|
+
});
|
|
282
|
+
if (res.ok) {
|
|
283
|
+
setTempApiKey('');
|
|
284
|
+
const data = await (await fetch('/api/config')).json();
|
|
285
|
+
setConfigInfo(data);
|
|
286
|
+
setIsSettingsOpen(false);
|
|
287
|
+
addTerminalLine('API configuration updated successfully', 'success');
|
|
288
|
+
}
|
|
289
|
+
} catch (err) {
|
|
290
|
+
addTerminalLine('Failed to save API configuration', 'error');
|
|
291
|
+
} finally {
|
|
292
|
+
setIsSavingConfig(false);
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
useEffect(() => {
|
|
297
|
+
const handleClickOutside = () => setContextMenu(null);
|
|
298
|
+
window.addEventListener('click', handleClickOutside);
|
|
299
|
+
return () => window.removeEventListener('click', handleClickOutside);
|
|
300
|
+
}, []);
|
|
301
|
+
|
|
302
|
+
const expandAll = useCallback(() => {
|
|
303
|
+
const getAllFolderIds = (elements: TreeViewElement[]): string[] => {
|
|
304
|
+
const ids: string[] = [];
|
|
305
|
+
for (const el of elements) {
|
|
306
|
+
if (el.type === 'folder') {
|
|
307
|
+
ids.push(el.id);
|
|
308
|
+
if (el.children) {
|
|
309
|
+
ids.push(...getAllFolderIds(el.children));
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return ids;
|
|
314
|
+
};
|
|
315
|
+
setExpandedFolders(getAllFolderIds(fileElements));
|
|
316
|
+
}, [fileElements]);
|
|
317
|
+
|
|
318
|
+
const collapseAll = useCallback(() => {
|
|
319
|
+
setExpandedFolders([]);
|
|
320
|
+
}, []);
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
const toggleAllFolders = useCallback(() => {
|
|
324
|
+
if (expandedFolders.length > 0) {
|
|
325
|
+
collapseAll();
|
|
326
|
+
} else {
|
|
327
|
+
expandAll();
|
|
328
|
+
}
|
|
329
|
+
}, [expandedFolders.length, expandAll, collapseAll]);
|
|
330
|
+
|
|
331
|
+
// Complete activity
|
|
332
|
+
const completeActivity = useCallback((id: string, success: boolean) => {
|
|
333
|
+
// Clear timer
|
|
334
|
+
const timer = activityTimers.current.get(id);
|
|
335
|
+
if (timer) {
|
|
336
|
+
clearInterval(timer);
|
|
337
|
+
activityTimers.current.delete(id);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
setActivities(prev => prev.map(a =>
|
|
341
|
+
a.id === id ? { ...a, status: success ? 'success' : 'error' } : a
|
|
342
|
+
));
|
|
343
|
+
}, []);
|
|
344
|
+
|
|
345
|
+
// Load chat history
|
|
346
|
+
const loadMessages = useCallback(async () => {
|
|
347
|
+
try {
|
|
348
|
+
const response = await fetch('/api/messages');
|
|
349
|
+
if (response.ok) {
|
|
350
|
+
const data = await response.json();
|
|
351
|
+
if (data.messages && data.messages.length > 0) {
|
|
352
|
+
setMessages(data.messages);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
} catch (err) {
|
|
356
|
+
console.error('Failed to load messages:', err);
|
|
357
|
+
}
|
|
358
|
+
}, []);
|
|
359
|
+
|
|
360
|
+
// Load session info
|
|
361
|
+
const loadSessionInfo = useCallback(async () => {
|
|
362
|
+
try {
|
|
363
|
+
const response = await fetch('/api/session');
|
|
364
|
+
if (response.ok) {
|
|
365
|
+
const data = await response.json();
|
|
366
|
+
setSessionInfo(data);
|
|
367
|
+
}
|
|
368
|
+
} catch (err) {
|
|
369
|
+
console.error('Failed to load session info:', err);
|
|
370
|
+
}
|
|
371
|
+
}, []);
|
|
372
|
+
|
|
373
|
+
// Load file tree
|
|
374
|
+
const loadFileTree = useCallback(async () => {
|
|
375
|
+
try {
|
|
376
|
+
const response = await fetch('/api/files');
|
|
377
|
+
if (response.ok) {
|
|
378
|
+
const data = await response.json();
|
|
379
|
+
setFileElements(data.elements);
|
|
380
|
+
// Only auto-expand if no folders are currently expanded and it's the initial load
|
|
381
|
+
if (isInitialLoad.current && expandedFolders.length === 0) {
|
|
382
|
+
const getIdsToExpand = (elements: TreeViewElement[], depth = 0): string[] => {
|
|
383
|
+
if (depth >= 2) return [];
|
|
384
|
+
const ids: string[] = [];
|
|
385
|
+
for (const el of elements) {
|
|
386
|
+
if (el.type === 'folder') {
|
|
387
|
+
ids.push(el.id);
|
|
388
|
+
if (el.children) {
|
|
389
|
+
ids.push(...getIdsToExpand(el.children, depth + 1));
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return ids;
|
|
394
|
+
};
|
|
395
|
+
setExpandedFolders(getIdsToExpand(data.elements));
|
|
396
|
+
isInitialLoad.current = false;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
} catch (err) {
|
|
400
|
+
console.error('Failed to load file tree:', err);
|
|
401
|
+
}
|
|
402
|
+
}, []);
|
|
403
|
+
|
|
404
|
+
// Handle file operations
|
|
405
|
+
const handleFileSelect = useCallback(async (id: string | undefined) => {
|
|
406
|
+
if (!id) return;
|
|
407
|
+
|
|
408
|
+
// Find the element to see if it's a file
|
|
409
|
+
const findElement = (elements: TreeViewElement[], targetId: string): TreeViewElement | null => {
|
|
410
|
+
for (const el of elements) {
|
|
411
|
+
if (el.id === targetId) return el;
|
|
412
|
+
if (el.children) {
|
|
413
|
+
const found = findElement(el.children, targetId);
|
|
414
|
+
if (found) return found;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return null;
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
const element = findElement(fileElements, id);
|
|
421
|
+
if (!element || element.type !== 'file') return;
|
|
422
|
+
|
|
423
|
+
try {
|
|
424
|
+
// The id is the relative path
|
|
425
|
+
const pathSegments = id.replace(/\\/g, '/').split('/');
|
|
426
|
+
const response = await fetch(`/api/files/${pathSegments.join('/')}`);
|
|
427
|
+
if (response.ok) {
|
|
428
|
+
const data = await response.json();
|
|
429
|
+
setEditingFile({ id, name: element.name });
|
|
430
|
+
setEditorContent(data.content);
|
|
431
|
+
setOriginalContent(data.content);
|
|
432
|
+
setIsEditorOpen(true);
|
|
433
|
+
} else {
|
|
434
|
+
addTerminalLine(`Failed to load file: ${id}`, 'error');
|
|
435
|
+
}
|
|
436
|
+
} catch (err) {
|
|
437
|
+
console.error('Error loading file:', err);
|
|
438
|
+
addTerminalLine(`Error loading file: ${id}`, 'error');
|
|
439
|
+
}
|
|
440
|
+
}, [fileElements, addTerminalLine]);
|
|
441
|
+
|
|
442
|
+
const saveFile = async () => {
|
|
443
|
+
if (!editingFile) return;
|
|
444
|
+
setIsSaving(true);
|
|
445
|
+
try {
|
|
446
|
+
const pathSegments = editingFile.id.replace(/\\/g, '/').split('/');
|
|
447
|
+
const response = await fetch(`/api/files/${pathSegments.join('/')}`, {
|
|
448
|
+
method: 'PUT',
|
|
449
|
+
headers: { 'Content-Type': 'application/json' },
|
|
450
|
+
body: JSON.stringify({ content: editorContent })
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
if (response.ok) {
|
|
454
|
+
addTerminalLine(`File saved: ${editingFile.name}`, 'info');
|
|
455
|
+
setOriginalContent(editorContent);
|
|
456
|
+
setIsEditorOpen(false);
|
|
457
|
+
loadFileTree();
|
|
458
|
+
} else {
|
|
459
|
+
const data = await response.json();
|
|
460
|
+
addTerminalLine(`Failed to save file: ${data.error}`, 'error');
|
|
461
|
+
}
|
|
462
|
+
} catch (err) {
|
|
463
|
+
addTerminalLine(`Error saving file: ${(err as Error).message}`, 'error');
|
|
464
|
+
} finally {
|
|
465
|
+
setIsSaving(false);
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
const deleteFile = async () => {
|
|
470
|
+
if (!editingFile) return;
|
|
471
|
+
|
|
472
|
+
setIsDeleting(true);
|
|
473
|
+
try {
|
|
474
|
+
const pathSegments = editingFile.id.replace(/\\/g, '/').split('/');
|
|
475
|
+
const response = await fetch(`/api/files/${pathSegments.join('/')}`, {
|
|
476
|
+
method: 'DELETE'
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
if (response.ok) {
|
|
480
|
+
addTerminalLine(`File deleted: ${editingFile.name}`, 'info');
|
|
481
|
+
setIsEditorOpen(false);
|
|
482
|
+
setIsDeleteConfirmOpen(false);
|
|
483
|
+
loadFileTree();
|
|
484
|
+
} else {
|
|
485
|
+
const data = await response.json();
|
|
486
|
+
addTerminalLine(`Failed to delete file: ${data.error}`, 'error');
|
|
487
|
+
}
|
|
488
|
+
} catch (err) {
|
|
489
|
+
addTerminalLine(`Error deleting file: ${(err as Error).message}`, 'error');
|
|
490
|
+
} finally {
|
|
491
|
+
setIsDeleting(false);
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
const handleExpandedChange = useCallback((items: string[] | undefined) => {
|
|
496
|
+
setExpandedFolders(items || []);
|
|
497
|
+
}, []);
|
|
498
|
+
|
|
499
|
+
const handleSelectedIdChange = useCallback((id: string | undefined) => {
|
|
500
|
+
setSelectedFileId(id);
|
|
501
|
+
if (id) {
|
|
502
|
+
handleFileSelect(id);
|
|
503
|
+
}
|
|
504
|
+
}, [handleFileSelect]);
|
|
505
|
+
|
|
506
|
+
// Connect to SSE
|
|
507
|
+
useEffect(() => {
|
|
508
|
+
const connectEventSource = () => {
|
|
509
|
+
if (eventSourceRef.current) {
|
|
510
|
+
eventSourceRef.current.close();
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const eventSource = new EventSource('/api/stream');
|
|
514
|
+
eventSourceRef.current = eventSource;
|
|
515
|
+
|
|
516
|
+
eventSource.onopen = () => {
|
|
517
|
+
setConnectionStatus('connected');
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
eventSource.onmessage = (e) => {
|
|
521
|
+
const data = JSON.parse(e.data);
|
|
522
|
+
|
|
523
|
+
switch (data.type) {
|
|
524
|
+
case 'start':
|
|
525
|
+
setIsStreaming(true);
|
|
526
|
+
accumulatedMessageRef.current = '';
|
|
527
|
+
accumulatedThinkingRef.current = '';
|
|
528
|
+
setCurrentMessage('');
|
|
529
|
+
setCurrentThinking('');
|
|
530
|
+
addTerminalLine('Agent started responding', 'info');
|
|
531
|
+
break;
|
|
532
|
+
|
|
533
|
+
case 'thinking':
|
|
534
|
+
accumulatedThinkingRef.current += data.content;
|
|
535
|
+
setCurrentThinking(accumulatedThinkingRef.current);
|
|
536
|
+
scrollToBottom();
|
|
537
|
+
break;
|
|
538
|
+
|
|
539
|
+
case 'content':
|
|
540
|
+
accumulatedMessageRef.current += data.content;
|
|
541
|
+
setCurrentMessage(accumulatedMessageRef.current);
|
|
542
|
+
scrollToBottom();
|
|
543
|
+
break;
|
|
544
|
+
|
|
545
|
+
case 'tool_start':
|
|
546
|
+
// Use tool name as the activity ID so tool_end can look it up
|
|
547
|
+
addActivity(data.tool, 'tool', JSON.stringify(data.args).slice(0, 80), data.tool);
|
|
548
|
+
addTerminalLine(`$ ${data.tool}: ${JSON.stringify(data.args)}`, 'command');
|
|
549
|
+
break;
|
|
550
|
+
|
|
551
|
+
case 'tool_end':
|
|
552
|
+
completeActivity(data.tool, !data.isError);
|
|
553
|
+
if (data.output) {
|
|
554
|
+
addTerminalLine(data.output.slice(0, 500), 'output');
|
|
555
|
+
}
|
|
556
|
+
if (data.error) {
|
|
557
|
+
addTerminalLine(`Error: ${data.error}`, 'error');
|
|
558
|
+
}
|
|
559
|
+
break;
|
|
560
|
+
|
|
561
|
+
case 'end':
|
|
562
|
+
setIsStreaming(false);
|
|
563
|
+
// Move accumulated content to a local variable before clearing
|
|
564
|
+
const finalContent = accumulatedMessageRef.current;
|
|
565
|
+
const finalThinking = accumulatedThinkingRef.current;
|
|
566
|
+
|
|
567
|
+
if (finalContent || finalThinking) {
|
|
568
|
+
setMessages(prev => [...prev, {
|
|
569
|
+
id: `msg-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
|
|
570
|
+
content: finalContent,
|
|
571
|
+
thinking: finalThinking,
|
|
572
|
+
isUser: false
|
|
573
|
+
}]);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Clear refs and state
|
|
577
|
+
accumulatedMessageRef.current = '';
|
|
578
|
+
accumulatedThinkingRef.current = '';
|
|
579
|
+
setCurrentMessage('');
|
|
580
|
+
setCurrentThinking('');
|
|
581
|
+
|
|
582
|
+
addTerminalLine('Agent finished', 'info');
|
|
583
|
+
loadSessionInfo();
|
|
584
|
+
// Also sync from backend to be absolutely sure we have the full history
|
|
585
|
+
setTimeout(loadMessages, 500);
|
|
586
|
+
break;
|
|
587
|
+
|
|
588
|
+
case 'compacting':
|
|
589
|
+
addActivity('context-compact', 'tool', 'Compacting conversation');
|
|
590
|
+
addTerminalLine('Compacting context...', 'info');
|
|
591
|
+
break;
|
|
592
|
+
|
|
593
|
+
case 'retry':
|
|
594
|
+
addActivity(`Retry ${data.attempt}/${data.maxAttempts}`, 'retry', `Delay: ${data.delayMs}ms`);
|
|
595
|
+
addTerminalLine(`Retrying attempt ${data.attempt}/${data.maxAttempts}`, 'info');
|
|
596
|
+
break;
|
|
597
|
+
|
|
598
|
+
case 'connected':
|
|
599
|
+
setConnectionStatus('connected');
|
|
600
|
+
break;
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
eventSource.onerror = () => {
|
|
605
|
+
setConnectionStatus('error');
|
|
606
|
+
eventSource.close();
|
|
607
|
+
setTimeout(connectEventSource, 3000);
|
|
608
|
+
};
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
connectEventSource();
|
|
612
|
+
loadSessionInfo();
|
|
613
|
+
loadMessages(); // Load existing history
|
|
614
|
+
loadFileTree();
|
|
615
|
+
|
|
616
|
+
return () => {
|
|
617
|
+
eventSourceRef.current?.close();
|
|
618
|
+
// Clear all timers
|
|
619
|
+
activityTimers.current.forEach(timer => clearInterval(timer));
|
|
620
|
+
};
|
|
621
|
+
}, [addActivity, addTerminalLine, completeActivity, loadFileTree, loadSessionInfo, loadMessages]);
|
|
622
|
+
|
|
623
|
+
// Send message
|
|
624
|
+
const sendMessage = async () => {
|
|
625
|
+
if (!input.trim() || isStreaming) return;
|
|
626
|
+
|
|
627
|
+
const userMessage = input.trim();
|
|
628
|
+
setInput('');
|
|
629
|
+
|
|
630
|
+
// Slash command handling
|
|
631
|
+
if (userMessage.startsWith('/')) {
|
|
632
|
+
const command = userMessage.split(' ')[0].toLowerCase();
|
|
633
|
+
|
|
634
|
+
if (command === '/clear' || command === '/new') {
|
|
635
|
+
addTerminalLine(`Executing command: ${command}`, 'info');
|
|
636
|
+
await resetSession();
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (command === '/model') {
|
|
641
|
+
setIsModelSelectorOpen(true);
|
|
642
|
+
addTerminalLine('Opening model selector...', 'info');
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// If unrecognized slash command, maybe just inform the user or ignore
|
|
647
|
+
addTerminalLine(`Unknown command: ${command}`, 'error');
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Add user message
|
|
652
|
+
setMessages(prev => [...prev, {
|
|
653
|
+
id: `msg-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
|
|
654
|
+
content: userMessage,
|
|
655
|
+
isUser: true
|
|
656
|
+
}]);
|
|
657
|
+
|
|
658
|
+
setIsStreaming(true);
|
|
659
|
+
|
|
660
|
+
try {
|
|
661
|
+
const response = await fetch('/api/chat', {
|
|
662
|
+
method: 'POST',
|
|
663
|
+
headers: { 'Content-Type': 'application/json' },
|
|
664
|
+
body: JSON.stringify({ message: userMessage })
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
if (!response.ok) {
|
|
668
|
+
throw new Error('Failed to send message');
|
|
669
|
+
}
|
|
670
|
+
} catch (err: unknown) {
|
|
671
|
+
setIsStreaming(false);
|
|
672
|
+
addTerminalLine(`Error: ${(err as Error).message}`, 'error');
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
// Select model
|
|
677
|
+
const selectModel = async (modelId: string) => {
|
|
678
|
+
setIsModelSelectorOpen(false);
|
|
679
|
+
addTerminalLine(`Switching to model: ${modelId}`, 'info');
|
|
680
|
+
|
|
681
|
+
try {
|
|
682
|
+
const response = await fetch('/api/session', {
|
|
683
|
+
method: 'POST',
|
|
684
|
+
headers: { 'Content-Type': 'application/json' },
|
|
685
|
+
body: JSON.stringify({ modelId })
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
if (response.ok) {
|
|
689
|
+
setMessages([]);
|
|
690
|
+
setCurrentMessage('');
|
|
691
|
+
setActivities([]);
|
|
692
|
+
setTerminalLines([]);
|
|
693
|
+
addTerminalLine(`Switched to ${modelId}`, 'info');
|
|
694
|
+
loadSessionInfo();
|
|
695
|
+
} else {
|
|
696
|
+
addTerminalLine(`Failed to switch model: ${response.statusText}`, 'error');
|
|
697
|
+
}
|
|
698
|
+
} catch (err) {
|
|
699
|
+
console.error('Failed to switch model:', err);
|
|
700
|
+
addTerminalLine('Error switching model', 'error');
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
// Reset session
|
|
705
|
+
const resetSession = async () => {
|
|
706
|
+
try {
|
|
707
|
+
const response = await fetch('/api/session', { method: 'POST' });
|
|
708
|
+
if (response.ok) {
|
|
709
|
+
setMessages([]);
|
|
710
|
+
setCurrentMessage('');
|
|
711
|
+
setActivities([]);
|
|
712
|
+
setTerminalLines([]);
|
|
713
|
+
addTerminalLine('Session reset', 'info');
|
|
714
|
+
loadSessionInfo();
|
|
715
|
+
}
|
|
716
|
+
} catch (err) {
|
|
717
|
+
console.error('Failed to reset session:', err);
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
// Handle input keydown
|
|
722
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
723
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
724
|
+
e.preventDefault();
|
|
725
|
+
sendMessage();
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
// Get activity icon
|
|
730
|
+
const getActivityIcon = (type: string) => {
|
|
731
|
+
switch (type) {
|
|
732
|
+
case 'tool': return Wrench;
|
|
733
|
+
case 'retry': return RefreshCw;
|
|
734
|
+
default: return Activity;
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
return (
|
|
739
|
+
<div className="app-wrapper">
|
|
740
|
+
{/* Sidebar */}
|
|
741
|
+
<aside className="sidebar" role="complementary" aria-label="Activity and file browser">
|
|
742
|
+
{/* Connection Status */}
|
|
743
|
+
<div className="connection-status">
|
|
744
|
+
<span
|
|
745
|
+
className={`status-indicator ${connectionStatus}`}
|
|
746
|
+
role="status"
|
|
747
|
+
aria-label={`Connection: ${connectionStatus}`}
|
|
748
|
+
></span>
|
|
749
|
+
<span className="status-text">
|
|
750
|
+
{connectionStatus === 'connected' ? 'Connected' :
|
|
751
|
+
connectionStatus === 'connecting' ? 'Connecting…' : 'Disconnected'}
|
|
752
|
+
</span>
|
|
753
|
+
</div>
|
|
754
|
+
|
|
755
|
+
{/* Tabs */}
|
|
756
|
+
<div className="sidebar-tabs" role="tablist" aria-label="Sidebar panels">
|
|
757
|
+
<button
|
|
758
|
+
className={`sidebar-tab ${activeTab === 'activity' ? 'active' : ''}`}
|
|
759
|
+
onClick={() => setActiveTab('activity')}
|
|
760
|
+
role="tab"
|
|
761
|
+
aria-selected={activeTab === 'activity'}
|
|
762
|
+
aria-controls="panel-activity"
|
|
763
|
+
>
|
|
764
|
+
<Activity size={14} />
|
|
765
|
+
<span>Activity</span>
|
|
766
|
+
</button>
|
|
767
|
+
<button
|
|
768
|
+
className={`sidebar-tab ${activeTab === 'files' ? 'active' : ''}`}
|
|
769
|
+
onClick={() => setActiveTab('files')}
|
|
770
|
+
role="tab"
|
|
771
|
+
aria-selected={activeTab === 'files'}
|
|
772
|
+
aria-controls="panel-files"
|
|
773
|
+
>
|
|
774
|
+
<Folder size={14} />
|
|
775
|
+
<span>Files</span>
|
|
776
|
+
</button>
|
|
777
|
+
</div>
|
|
778
|
+
|
|
779
|
+
{/* Activity Panel */}
|
|
780
|
+
{activeTab === 'activity' && (
|
|
781
|
+
<div className="sidebar-panel active" id="panel-activity" role="tabpanel">
|
|
782
|
+
<div className="panel-header">
|
|
783
|
+
<h3>Activity Log</h3>
|
|
784
|
+
<button
|
|
785
|
+
className="btn-icon"
|
|
786
|
+
onClick={() => setActivities([])}
|
|
787
|
+
aria-label="Clear activity log"
|
|
788
|
+
>
|
|
789
|
+
<Trash2 size={14} />
|
|
790
|
+
</button>
|
|
791
|
+
</div>
|
|
792
|
+
<div className="activity-log">
|
|
793
|
+
{activities.length === 0 ? (
|
|
794
|
+
<div className="activity-empty">
|
|
795
|
+
{/* Accessible empty state */}
|
|
796
|
+
<span>Waiting for activity…</span>
|
|
797
|
+
</div>
|
|
798
|
+
) : (
|
|
799
|
+
activities.map(activity => {
|
|
800
|
+
const Icon = getActivityIcon(activity.type);
|
|
801
|
+
return (
|
|
802
|
+
<div key={activity.id} className="activity-item">
|
|
803
|
+
<div className="activity-item-header">
|
|
804
|
+
<Icon
|
|
805
|
+
size={14}
|
|
806
|
+
className={`activity-item-icon ${activity.status === 'running' ? 'running' : ''}`}
|
|
807
|
+
/>
|
|
808
|
+
<span className="activity-item-name">{activity.name}</span>
|
|
809
|
+
<span className={`activity-item-status ${activity.status}`}>
|
|
810
|
+
{activity.status === 'running'
|
|
811
|
+
? `${activity.elapsed?.toFixed(1)}s`
|
|
812
|
+
: activity.status}
|
|
813
|
+
</span>
|
|
814
|
+
</div>
|
|
815
|
+
{activity.details && (
|
|
816
|
+
<div className="activity-item-details">{activity.details}</div>
|
|
817
|
+
)}
|
|
818
|
+
<div className="activity-item-time">{activity.time}</div>
|
|
819
|
+
</div>
|
|
820
|
+
);
|
|
821
|
+
})
|
|
822
|
+
)}
|
|
823
|
+
</div>
|
|
824
|
+
</div>
|
|
825
|
+
)}
|
|
826
|
+
|
|
827
|
+
{/* Files Panel */}
|
|
828
|
+
{activeTab === 'files' && (
|
|
829
|
+
<div className="sidebar-panel active flex flex-col" id="panel-files" role="tabpanel">
|
|
830
|
+
<div className="panel-header">
|
|
831
|
+
<div className="flex items-center gap-2">
|
|
832
|
+
<FolderOpen size={14} className="text-muted-foreground" />
|
|
833
|
+
<h3>Files</h3>
|
|
834
|
+
</div>
|
|
835
|
+
<div className="flex items-center gap-1">
|
|
836
|
+
<button
|
|
837
|
+
className="btn-icon h-8 w-8 bg-muted/50 text-muted-foreground hover:bg-muted hover:text-foreground transition-all duration-200"
|
|
838
|
+
onClick={toggleAllFolders}
|
|
839
|
+
title={expandedFolders.length > 0 ? "Collapse All" : "Expand All"}
|
|
840
|
+
>
|
|
841
|
+
{expandedFolders.length > 0 ? <ChevronsDownUp size={16} /> : <ChevronsUpDown size={16} />}
|
|
842
|
+
</button>
|
|
843
|
+
<button
|
|
844
|
+
className="btn-icon h-8 w-8 bg-primary/10 text-primary hover:bg-primary hover:text-primary-foreground transition-all duration-200 shadow-sm"
|
|
845
|
+
onClick={() => {
|
|
846
|
+
setNewFileName('');
|
|
847
|
+
setTargetDirectory('');
|
|
848
|
+
setIsNewFileModalOpen(true);
|
|
849
|
+
}}
|
|
850
|
+
title="New File"
|
|
851
|
+
>
|
|
852
|
+
<FilePlus size={18} />
|
|
853
|
+
</button>
|
|
854
|
+
<span className="text-xs text-muted-foreground ml-1">
|
|
855
|
+
{fileElements.length} {fileElements.length === 1 ? 'item' : 'items'}
|
|
856
|
+
</span>
|
|
857
|
+
</div>
|
|
858
|
+
</div>
|
|
859
|
+
<div className="file-browser flex-1 overflow-hidden px-2 py-2">
|
|
860
|
+
{fileElements.length === 0 ? (
|
|
861
|
+
<div className="flex items-center justify-center h-full text-muted-foreground text-sm">
|
|
862
|
+
Loading…
|
|
863
|
+
</div>
|
|
864
|
+
) : (
|
|
865
|
+
<Tree
|
|
866
|
+
elements={fileElements}
|
|
867
|
+
initialExpandedItems={expandedFolders}
|
|
868
|
+
initialSelectedId={selectedFileId}
|
|
869
|
+
onExpandedItemsChange={handleExpandedChange}
|
|
870
|
+
onSelectedIdChange={handleSelectedIdChange}
|
|
871
|
+
onFolderContextMenu={handleContextMenu}
|
|
872
|
+
className="h-full"
|
|
873
|
+
/>
|
|
874
|
+
)}
|
|
875
|
+
</div>
|
|
876
|
+
</div>
|
|
877
|
+
)}
|
|
878
|
+
</aside>
|
|
879
|
+
|
|
880
|
+
{/* Main Content */}
|
|
881
|
+
<div className="app">
|
|
882
|
+
{/* Header */}
|
|
883
|
+
<header className="header">
|
|
884
|
+
<div className="header-brand">
|
|
885
|
+
<Search className="header-icon" size={18} />
|
|
886
|
+
<h1 className="header-title">NextPi</h1>
|
|
887
|
+
</div>
|
|
888
|
+
|
|
889
|
+
<div className="header-meta">
|
|
890
|
+
<div className="header-info">
|
|
891
|
+
<span><strong>Model</strong> {sessionInfo?.model || '—'}</span>
|
|
892
|
+
<span><strong>Messages</strong> {sessionInfo?.messages || 0}</span>
|
|
893
|
+
<span><strong>Tokens</strong> ~{sessionInfo?.tokens || 0}</span>
|
|
894
|
+
</div>
|
|
895
|
+
|
|
896
|
+
<label className="thinking-toggle">
|
|
897
|
+
<input
|
|
898
|
+
type="checkbox"
|
|
899
|
+
checked={showThinking}
|
|
900
|
+
onChange={(e) => setShowThinking(e.target.checked)}
|
|
901
|
+
/>
|
|
902
|
+
<Brain size={14} />
|
|
903
|
+
<span>Thinking</span>
|
|
904
|
+
</label>
|
|
905
|
+
|
|
906
|
+
<button
|
|
907
|
+
className={`thinking-toggle ${isSettingsOpen ? 'bg-accent text-foreground' : ''}`}
|
|
908
|
+
onClick={() => setIsSettingsOpen(true)}
|
|
909
|
+
>
|
|
910
|
+
<Settings size={14} />
|
|
911
|
+
<span>Settings</span>
|
|
912
|
+
</button>
|
|
913
|
+
|
|
914
|
+
<button
|
|
915
|
+
className={`thinking-toggle ${isHelpOpen ? 'bg-accent text-foreground' : ''}`}
|
|
916
|
+
onClick={() => setIsHelpOpen(true)}
|
|
917
|
+
>
|
|
918
|
+
<HelpCircle size={14} />
|
|
919
|
+
<span>Help</span>
|
|
920
|
+
</button>
|
|
921
|
+
|
|
922
|
+
<button className="btn btn-secondary" onClick={resetSession} aria-label="Start new session">
|
|
923
|
+
<RefreshCw size={14} />
|
|
924
|
+
<span>New Session</span>
|
|
925
|
+
</button>
|
|
926
|
+
|
|
927
|
+
<div className="w-px h-6 bg-border mx-1" />
|
|
928
|
+
|
|
929
|
+
<button
|
|
930
|
+
className={`btn ${isPreviewOpen ? 'bg-destructive/10 text-destructive hover:bg-destructive/20 border-destructive/20' : 'btn-primary'} h-9 px-4 transition-all duration-300 shadow-sm`}
|
|
931
|
+
onClick={() => setIsPreviewOpen(!isPreviewOpen)}
|
|
932
|
+
aria-label={isPreviewOpen ? "Stop App" : "Run App"}
|
|
933
|
+
>
|
|
934
|
+
{isPreviewOpen ? (
|
|
935
|
+
<>
|
|
936
|
+
<div className="size-2 rounded-full bg-destructive animate-pulse mr-2 shadow-[0_0_10px_rgba(239,68,68,0.6)]" />
|
|
937
|
+
<Square size={14} className="mr-2 fill-current opacity-70" />
|
|
938
|
+
<span>Stop App</span>
|
|
939
|
+
</>
|
|
940
|
+
) : (
|
|
941
|
+
<>
|
|
942
|
+
<Play size={14} className="mr-2 fill-current" />
|
|
943
|
+
<span>Run App</span>
|
|
944
|
+
</>
|
|
945
|
+
)}
|
|
946
|
+
</button>
|
|
947
|
+
</div>
|
|
948
|
+
</header>
|
|
949
|
+
|
|
950
|
+
<div className="app-content-area">
|
|
951
|
+
<div className="chat-section">
|
|
952
|
+
<main className="chat-container" ref={chatRef} aria-label="Chat messages" aria-live="polite">
|
|
953
|
+
{messages.length === 0 && !isStreaming && (
|
|
954
|
+
<div className="empty-state">
|
|
955
|
+
<Bot className="empty-state-icon" size={48} />
|
|
956
|
+
<p className="empty-state-title">How can I help you today?</p>
|
|
957
|
+
<p className="empty-state-hint">
|
|
958
|
+
Ask me to research topics, search the web, fetch documentation,
|
|
959
|
+
or help with coding tasks. Use <kbd>Shift+Enter</kbd> for new lines.
|
|
960
|
+
</p>
|
|
961
|
+
</div>
|
|
962
|
+
)}
|
|
963
|
+
|
|
964
|
+
{messages.map(msg => (
|
|
965
|
+
<div key={msg.id} className={`message message-${msg.isUser ? 'user' : 'assistant'}`}>
|
|
966
|
+
<div className="message-header">
|
|
967
|
+
{msg.isUser ? <User size={14} /> : <Bot size={14} />}
|
|
968
|
+
<span>{msg.isUser ? 'You' : 'Assistant'}</span>
|
|
969
|
+
</div>
|
|
970
|
+
<div className="message-bubble">
|
|
971
|
+
{showThinking && msg.thinking && (
|
|
972
|
+
<div className="thinking-block">
|
|
973
|
+
<div className="thinking-header">
|
|
974
|
+
<Brain size={12} className="text-muted-foreground" />
|
|
975
|
+
<span>Reasoning</span>
|
|
976
|
+
</div>
|
|
977
|
+
<div className="thinking-content italic text-muted-foreground/80 leading-relaxed text-sm">
|
|
978
|
+
{msg.thinking}
|
|
979
|
+
</div>
|
|
980
|
+
</div>
|
|
981
|
+
)}
|
|
982
|
+
<div
|
|
983
|
+
className="message-content"
|
|
984
|
+
dangerouslySetInnerHTML={{
|
|
985
|
+
__html: msg.isUser
|
|
986
|
+
? msg.content.replace(/</g, '<').replace(/>/g, '>')
|
|
987
|
+
: renderMarkdown(msg.content)
|
|
988
|
+
}}
|
|
989
|
+
/>
|
|
990
|
+
</div>
|
|
991
|
+
</div>
|
|
992
|
+
))}
|
|
993
|
+
|
|
994
|
+
{(isStreaming && (currentMessage || currentThinking)) && (
|
|
995
|
+
<div className="message message-assistant">
|
|
996
|
+
<div className="message-header">
|
|
997
|
+
<Bot size={14} />
|
|
998
|
+
<span>NextPi</span>
|
|
999
|
+
</div>
|
|
1000
|
+
<div className="message-bubble">
|
|
1001
|
+
{showThinking && currentThinking && (
|
|
1002
|
+
<div className="thinking-block">
|
|
1003
|
+
<div className="thinking-header">
|
|
1004
|
+
<Brain size={12} className="text-muted-foreground" />
|
|
1005
|
+
<span>Reasoning</span>
|
|
1006
|
+
</div>
|
|
1007
|
+
<div className="thinking-content italic text-muted-foreground/80 leading-relaxed text-sm">
|
|
1008
|
+
{currentThinking}
|
|
1009
|
+
</div>
|
|
1010
|
+
</div>
|
|
1011
|
+
)}
|
|
1012
|
+
{currentMessage ? (
|
|
1013
|
+
<div
|
|
1014
|
+
className="message-content"
|
|
1015
|
+
dangerouslySetInnerHTML={{ __html: renderMarkdown(currentMessage) }}
|
|
1016
|
+
/>
|
|
1017
|
+
) : (
|
|
1018
|
+
<div className="flex gap-2 text-sm text-muted-foreground animate-pulse mt-2 ml-1 items-center">
|
|
1019
|
+
<div className="size-1.5 rounded-full bg-current" />
|
|
1020
|
+
Generating...
|
|
1021
|
+
</div>
|
|
1022
|
+
)}
|
|
1023
|
+
</div>
|
|
1024
|
+
</div>
|
|
1025
|
+
)}
|
|
1026
|
+
|
|
1027
|
+
{isStreaming && !currentMessage && !currentThinking && (
|
|
1028
|
+
<div className="flex gap-2 text-sm text-muted-foreground animate-pulse mt-2 ml-4 mb-4 items-center">
|
|
1029
|
+
<div className="size-1.5 rounded-full bg-current" />
|
|
1030
|
+
Thinking...
|
|
1031
|
+
</div>
|
|
1032
|
+
)}
|
|
1033
|
+
</main>
|
|
1034
|
+
|
|
1035
|
+
{/* Terminal Panel */}
|
|
1036
|
+
<div className={`terminal-panel ${isTerminalExpanded ? 'expanded' : 'collapsed'}`}>
|
|
1037
|
+
<div
|
|
1038
|
+
className="terminal-header"
|
|
1039
|
+
onClick={() => setIsTerminalExpanded(!isTerminalExpanded)}
|
|
1040
|
+
role="button"
|
|
1041
|
+
tabIndex={0}
|
|
1042
|
+
aria-expanded={isTerminalExpanded}
|
|
1043
|
+
aria-controls="terminal-content"
|
|
1044
|
+
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setIsTerminalExpanded(!isTerminalExpanded); } }}
|
|
1045
|
+
>
|
|
1046
|
+
<div className="terminal-title">
|
|
1047
|
+
<Terminal size={14} />
|
|
1048
|
+
<span>Terminal Output</span>
|
|
1049
|
+
{terminalLines.length > 0 && (
|
|
1050
|
+
<span className="terminal-badge">{terminalLines.length}</span>
|
|
1051
|
+
)}
|
|
1052
|
+
</div>
|
|
1053
|
+
<div className="terminal-controls">
|
|
1054
|
+
<button
|
|
1055
|
+
className="btn-icon"
|
|
1056
|
+
onClick={(e) => { e.stopPropagation(); setTerminalLines([]); }}
|
|
1057
|
+
aria-label="Clear terminal"
|
|
1058
|
+
>
|
|
1059
|
+
<Trash2 size={14} />
|
|
1060
|
+
</button>
|
|
1061
|
+
<button className="btn-icon" aria-label={isTerminalExpanded ? 'Collapse terminal' : 'Expand terminal'}>
|
|
1062
|
+
{isTerminalExpanded ? <ChevronDown size={14} /> : <ChevronUp size={14} />}
|
|
1063
|
+
</button>
|
|
1064
|
+
</div>
|
|
1065
|
+
</div>
|
|
1066
|
+
<div className="terminal-content" id="terminal-content">
|
|
1067
|
+
<div className="terminal-lines">
|
|
1068
|
+
{terminalLines.length === 0 ? (
|
|
1069
|
+
<div className="terminal-line terminal-line-info">
|
|
1070
|
+
Terminal ready. Agent commands will appear here.
|
|
1071
|
+
</div>
|
|
1072
|
+
) : (
|
|
1073
|
+
terminalLines.map(line => (
|
|
1074
|
+
<div key={line.id} className={`terminal-line terminal-line-${line.type}`}>
|
|
1075
|
+
<span className="terminal-line-timestamp">[{line.timestamp}]</span>
|
|
1076
|
+
{line.content}
|
|
1077
|
+
</div>
|
|
1078
|
+
))
|
|
1079
|
+
)}
|
|
1080
|
+
</div>
|
|
1081
|
+
</div>
|
|
1082
|
+
</div>
|
|
1083
|
+
|
|
1084
|
+
{/* Input */}
|
|
1085
|
+
<footer className="input-area relative">
|
|
1086
|
+
{input.trim().startsWith('/') && (
|
|
1087
|
+
|
|
1088
|
+
<div className="absolute bottom-full left-4 mb-2 w-48 bg-card border rounded-lg shadow-xl overflow-hidden animate-in slide-in-from-bottom-2 duration-200 z-50">
|
|
1089
|
+
<div className="p-2 border-b bg-muted/50 text-[10px] font-bold text-muted-foreground uppercase tracking-wider">
|
|
1090
|
+
Available Commands
|
|
1091
|
+
</div>
|
|
1092
|
+
<div className="p-1">
|
|
1093
|
+
{[
|
|
1094
|
+
{ cmd: '/clear', desc: 'Clear chat & reset' },
|
|
1095
|
+
{ cmd: '/new', desc: 'Start new session' },
|
|
1096
|
+
{ cmd: '/model', desc: 'Select AI model' },
|
|
1097
|
+
].map((s) => (
|
|
1098
|
+
<button
|
|
1099
|
+
key={s.cmd}
|
|
1100
|
+
className="w-full flex flex-col items-start px-3 py-1.5 hover:bg-muted rounded-md transition-colors text-left"
|
|
1101
|
+
onClick={async () => {
|
|
1102
|
+
if (s.cmd === '/model') {
|
|
1103
|
+
setIsModelSelectorOpen(true);
|
|
1104
|
+
setInput('');
|
|
1105
|
+
} else if (s.cmd === '/clear' || s.cmd === '/new') {
|
|
1106
|
+
setInput('');
|
|
1107
|
+
await resetSession();
|
|
1108
|
+
} else {
|
|
1109
|
+
setInput(s.cmd);
|
|
1110
|
+
inputRef.current?.focus();
|
|
1111
|
+
}
|
|
1112
|
+
}}
|
|
1113
|
+
|
|
1114
|
+
>
|
|
1115
|
+
<span className="text-sm font-mono font-semibold text-primary">{s.cmd}</span>
|
|
1116
|
+
<span className="text-[10px] text-muted-foreground">{s.desc}</span>
|
|
1117
|
+
</button>
|
|
1118
|
+
))}
|
|
1119
|
+
</div>
|
|
1120
|
+
</div>
|
|
1121
|
+
)}
|
|
1122
|
+
<div className="input-wrapper">
|
|
1123
|
+
<div className="flex items-end gap-3.5">
|
|
1124
|
+
<textarea
|
|
1125
|
+
ref={inputRef}
|
|
1126
|
+
className="message-input"
|
|
1127
|
+
placeholder="Ask anything or type / for commands…"
|
|
1128
|
+
rows={1}
|
|
1129
|
+
value={input}
|
|
1130
|
+
onChange={(e) => setInput(e.target.value)}
|
|
1131
|
+
onKeyDown={handleKeyDown}
|
|
1132
|
+
disabled={isStreaming}
|
|
1133
|
+
aria-label="Message input"
|
|
1134
|
+
/>
|
|
1135
|
+
<button
|
|
1136
|
+
className="btn-send mb-0.5"
|
|
1137
|
+
onClick={sendMessage}
|
|
1138
|
+
disabled={!input.trim() || isStreaming}
|
|
1139
|
+
aria-label="Send message"
|
|
1140
|
+
>
|
|
1141
|
+
<Send size={18} />
|
|
1142
|
+
</button>
|
|
1143
|
+
</div>
|
|
1144
|
+
<span className="input-hint">Enter to send · Shift+Enter for new line</span>
|
|
1145
|
+
</div>
|
|
1146
|
+
</footer>
|
|
1147
|
+
</div>
|
|
1148
|
+
|
|
1149
|
+
{isPreviewOpen && (
|
|
1150
|
+
<aside className="preview-section">
|
|
1151
|
+
<div className="preview-header">
|
|
1152
|
+
<div className="preview-address-bar">
|
|
1153
|
+
<Globe size={14} />
|
|
1154
|
+
<input
|
|
1155
|
+
type="text"
|
|
1156
|
+
value={previewUrl}
|
|
1157
|
+
onChange={(e) => setPreviewUrl(e.target.value)}
|
|
1158
|
+
onKeyDown={(e) => {
|
|
1159
|
+
if (e.key === 'Enter') {
|
|
1160
|
+
const url = previewUrl;
|
|
1161
|
+
setPreviewUrl('');
|
|
1162
|
+
setTimeout(() => setPreviewUrl(url), 10);
|
|
1163
|
+
}
|
|
1164
|
+
}}
|
|
1165
|
+
/>
|
|
1166
|
+
<button
|
|
1167
|
+
className="btn-icon h-7 w-7"
|
|
1168
|
+
onClick={() => {
|
|
1169
|
+
const url = previewUrl;
|
|
1170
|
+
setPreviewUrl('');
|
|
1171
|
+
setTimeout(() => setPreviewUrl(url), 10);
|
|
1172
|
+
}}
|
|
1173
|
+
title="Reload Preview"
|
|
1174
|
+
>
|
|
1175
|
+
<RefreshCw size={12} />
|
|
1176
|
+
</button>
|
|
1177
|
+
</div>
|
|
1178
|
+
<a
|
|
1179
|
+
href={previewUrl}
|
|
1180
|
+
target="_blank"
|
|
1181
|
+
rel="noopener noreferrer"
|
|
1182
|
+
className="btn-icon h-8 w-8 text-muted-foreground hover:text-foreground"
|
|
1183
|
+
title="Open in new tab"
|
|
1184
|
+
>
|
|
1185
|
+
<ExternalLink size={16} />
|
|
1186
|
+
</a>
|
|
1187
|
+
<button
|
|
1188
|
+
className="btn-icon h-8 w-8 hover:bg-destructive/10 hover:text-destructive"
|
|
1189
|
+
onClick={() => setIsPreviewOpen(false)}
|
|
1190
|
+
title="Close Preview"
|
|
1191
|
+
>
|
|
1192
|
+
<X size={18} />
|
|
1193
|
+
</button>
|
|
1194
|
+
</div>
|
|
1195
|
+
<div className="preview-content">
|
|
1196
|
+
<iframe
|
|
1197
|
+
src={previewUrl}
|
|
1198
|
+
title="App Preview"
|
|
1199
|
+
sandbox="allow-forms allow-modals allow-pointer-lock allow-popups allow-presentation allow-same-origin allow-scripts"
|
|
1200
|
+
/>
|
|
1201
|
+
</div>
|
|
1202
|
+
</aside>
|
|
1203
|
+
)}
|
|
1204
|
+
</div>
|
|
1205
|
+
</div>
|
|
1206
|
+
|
|
1207
|
+
{/* File Editor Modal */}
|
|
1208
|
+
{isEditorOpen && (
|
|
1209
|
+
<div
|
|
1210
|
+
className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm animate-in fade-in duration-200"
|
|
1211
|
+
onClick={(e) => { if (e.target === e.currentTarget) setIsEditorOpen(false); }}
|
|
1212
|
+
>
|
|
1213
|
+
<div className="bg-card w-full max-w-4xl h-[80vh] border rounded-xl shadow-2xl flex flex-col overflow-hidden animate-in zoom-in-95 duration-200">
|
|
1214
|
+
{/* Modal Header */}
|
|
1215
|
+
<div className="px-6 py-4 border-b flex items-center justify-between bg-muted/30">
|
|
1216
|
+
<div className="flex items-center gap-3">
|
|
1217
|
+
<FileText className="text-sky-400" size={18} />
|
|
1218
|
+
<div>
|
|
1219
|
+
<h2 className="text-lg font-semibold leading-none">{editingFile?.name}</h2>
|
|
1220
|
+
<p className="text-xs text-muted-foreground mt-1">{editingFile?.id}</p>
|
|
1221
|
+
</div>
|
|
1222
|
+
</div>
|
|
1223
|
+
<div className="flex items-center gap-2">
|
|
1224
|
+
<button
|
|
1225
|
+
className="btn-icon text-muted-foreground hover:bg-destructive/10 hover:text-destructive transition-colors mr-2"
|
|
1226
|
+
onClick={() => setIsDeleteConfirmOpen(true)}
|
|
1227
|
+
title="Delete File"
|
|
1228
|
+
>
|
|
1229
|
+
<Trash2 size={18} />
|
|
1230
|
+
</button>
|
|
1231
|
+
<div className="w-px h-6 bg-border mr-1" />
|
|
1232
|
+
{editorContent !== originalContent && (
|
|
1233
|
+
<button
|
|
1234
|
+
className="btn btn-secondary h-9 px-3 animate-in fade-in slide-in-from-right-2 duration-200"
|
|
1235
|
+
onClick={() => setEditorContent(originalContent)}
|
|
1236
|
+
disabled={isSaving || isDeleting}
|
|
1237
|
+
>
|
|
1238
|
+
<RotateCcw size={14} className="mr-2" />
|
|
1239
|
+
<span>Revert</span>
|
|
1240
|
+
</button>
|
|
1241
|
+
)}
|
|
1242
|
+
<button
|
|
1243
|
+
className="btn btn-primary h-9 px-4"
|
|
1244
|
+
onClick={saveFile}
|
|
1245
|
+
disabled={isSaving || isDeleting}
|
|
1246
|
+
>
|
|
1247
|
+
{isSaving ? <RefreshCw size={14} className="mr-2 animate-spin" /> : <Save size={14} className="mr-2" />}
|
|
1248
|
+
<span>Save Changes</span>
|
|
1249
|
+
</button>
|
|
1250
|
+
<div className="w-px h-6 bg-border mx-1" />
|
|
1251
|
+
<button
|
|
1252
|
+
className="btn-icon hover:bg-muted"
|
|
1253
|
+
onClick={() => setIsEditorOpen(false)}
|
|
1254
|
+
>
|
|
1255
|
+
<X size={20} />
|
|
1256
|
+
</button>
|
|
1257
|
+
</div>
|
|
1258
|
+
</div>
|
|
1259
|
+
|
|
1260
|
+
{/* Modal Content */}
|
|
1261
|
+
<div className="flex-1 overflow-hidden p-4 bg-zinc-950">
|
|
1262
|
+
<textarea
|
|
1263
|
+
className="w-full h-full bg-transparent text-zinc-300 font-mono text-sm resize-none focus:outline-none scrollbar-thin p-2"
|
|
1264
|
+
spellCheck={false}
|
|
1265
|
+
value={editorContent}
|
|
1266
|
+
onChange={(e) => setEditorContent(e.target.value)}
|
|
1267
|
+
placeholder="File is empty..."
|
|
1268
|
+
/>
|
|
1269
|
+
</div>
|
|
1270
|
+
|
|
1271
|
+
{/* Modal Footer */}
|
|
1272
|
+
<div className="px-6 py-3 border-t bg-muted/30 flex justify-between items-center text-xs text-muted-foreground font-mono">
|
|
1273
|
+
<div>
|
|
1274
|
+
{editorContent.length} characters · {editorContent.split('\n').length} lines
|
|
1275
|
+
</div>
|
|
1276
|
+
<div>
|
|
1277
|
+
UTF-8
|
|
1278
|
+
</div>
|
|
1279
|
+
</div>
|
|
1280
|
+
</div>
|
|
1281
|
+
</div>
|
|
1282
|
+
)}
|
|
1283
|
+
|
|
1284
|
+
{/* Delete Confirmation Modal */}
|
|
1285
|
+
{isDeleteConfirmOpen && (
|
|
1286
|
+
<div
|
|
1287
|
+
className="fixed inset-0 z-[60] flex items-center justify-center bg-background/60 backdrop-blur-md animate-in fade-in duration-200"
|
|
1288
|
+
onClick={(e) => { if (e.target === e.currentTarget) setIsDeleteConfirmOpen(false); }}
|
|
1289
|
+
>
|
|
1290
|
+
<div className="bg-card w-full max-w-sm border rounded-xl shadow-2xl p-6 animate-in zoom-in-95 duration-200">
|
|
1291
|
+
<div className="flex items-center gap-3 mb-4 text-destructive">
|
|
1292
|
+
<div className="p-2 bg-destructive/10 rounded-lg">
|
|
1293
|
+
<AlertTriangle size={20} />
|
|
1294
|
+
</div>
|
|
1295
|
+
<h3 className="text-lg font-semibold">Delete File?</h3>
|
|
1296
|
+
</div>
|
|
1297
|
+
<p className="text-sm text-muted-foreground mb-6 leading-relaxed">
|
|
1298
|
+
Are you sure you want to delete <span className="font-mono text-foreground font-medium">{editingFile?.name}</span>? This action is permanent and cannot be undone.
|
|
1299
|
+
</p>
|
|
1300
|
+
<div className="flex justify-end gap-3">
|
|
1301
|
+
<button
|
|
1302
|
+
className="btn btn-secondary h-9 px-4"
|
|
1303
|
+
onClick={() => setIsDeleteConfirmOpen(false)}
|
|
1304
|
+
disabled={isDeleting}
|
|
1305
|
+
>
|
|
1306
|
+
Cancel
|
|
1307
|
+
</button>
|
|
1308
|
+
<button
|
|
1309
|
+
className="btn bg-destructive text-destructive-foreground hover:bg-destructive/90 h-9 px-4 shadow-sm"
|
|
1310
|
+
onClick={deleteFile}
|
|
1311
|
+
disabled={isDeleting}
|
|
1312
|
+
>
|
|
1313
|
+
{isDeleting ? <RefreshCw size={14} className="mr-2 animate-spin" /> : <Trash2 size={14} className="mr-2" />}
|
|
1314
|
+
Delete Forever
|
|
1315
|
+
</button>
|
|
1316
|
+
</div>
|
|
1317
|
+
</div>
|
|
1318
|
+
</div>
|
|
1319
|
+
)}
|
|
1320
|
+
|
|
1321
|
+
{/* New File Modal */}
|
|
1322
|
+
{isNewFileModalOpen && (
|
|
1323
|
+
<div
|
|
1324
|
+
className="fixed inset-0 z-[70] flex items-center justify-center bg-background/60 backdrop-blur-md animate-in fade-in duration-200"
|
|
1325
|
+
onClick={(e) => { if (e.target === e.currentTarget) setIsNewFileModalOpen(false); }}
|
|
1326
|
+
>
|
|
1327
|
+
<div className="bg-card w-full max-w-sm border rounded-xl shadow-2xl p-6 animate-in zoom-in-95 duration-200">
|
|
1328
|
+
<div className="flex items-center gap-3 mb-6">
|
|
1329
|
+
<div className="p-2 bg-primary/10 rounded-lg text-primary">
|
|
1330
|
+
<FilePlus size={20} />
|
|
1331
|
+
</div>
|
|
1332
|
+
<h3 className="text-lg font-semibold text-foreground">Create New File</h3>
|
|
1333
|
+
</div>
|
|
1334
|
+
|
|
1335
|
+
<div className="space-y-6">
|
|
1336
|
+
<div className="space-y-2">
|
|
1337
|
+
<label className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
1338
|
+
Target Directory
|
|
1339
|
+
</label>
|
|
1340
|
+
<div className="grid grid-cols-1 gap-2 max-h-40 overflow-y-auto pr-2 scrollbar-thin">
|
|
1341
|
+
{getAllFolders(fileElements).map((folder) => (
|
|
1342
|
+
<button
|
|
1343
|
+
key={folder.id}
|
|
1344
|
+
onClick={() => setTargetDirectory(folder.id)}
|
|
1345
|
+
className={`flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-all ${targetDirectory === folder.id
|
|
1346
|
+
? 'bg-primary text-primary-foreground shadow-sm'
|
|
1347
|
+
: 'bg-muted/30 text-muted-foreground hover:bg-muted hover:text-foreground'
|
|
1348
|
+
}`}
|
|
1349
|
+
>
|
|
1350
|
+
<Folder size={14} className={targetDirectory === folder.id ? 'text-primary-foreground' : 'text-amber-500/70'} />
|
|
1351
|
+
<span className="truncate">{folder.name}</span>
|
|
1352
|
+
</button>
|
|
1353
|
+
))}
|
|
1354
|
+
</div>
|
|
1355
|
+
</div>
|
|
1356
|
+
|
|
1357
|
+
<div className="space-y-2">
|
|
1358
|
+
<label htmlFor="filename" className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
1359
|
+
File Name
|
|
1360
|
+
</label>
|
|
1361
|
+
<input
|
|
1362
|
+
id="filename"
|
|
1363
|
+
autoFocus
|
|
1364
|
+
type="text"
|
|
1365
|
+
className="w-full bg-muted/50 border border-border rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary/50 transition-all font-mono"
|
|
1366
|
+
placeholder="e.g. index.ts"
|
|
1367
|
+
value={newFileName}
|
|
1368
|
+
onChange={(e) => setNewFileName(e.target.value)}
|
|
1369
|
+
onKeyDown={(e) => {
|
|
1370
|
+
if (e.key === 'Enter' && newFileName.trim()) {
|
|
1371
|
+
const fullPath = targetDirectory ? `${targetDirectory}/${newFileName.trim()}` : newFileName.trim();
|
|
1372
|
+
setEditingFile({ id: fullPath, name: newFileName.trim() });
|
|
1373
|
+
setEditorContent('');
|
|
1374
|
+
setIsEditorOpen(true);
|
|
1375
|
+
setIsNewFileModalOpen(false);
|
|
1376
|
+
} else if (e.key === 'Escape') {
|
|
1377
|
+
setIsNewFileModalOpen(false);
|
|
1378
|
+
}
|
|
1379
|
+
}}
|
|
1380
|
+
/>
|
|
1381
|
+
</div>
|
|
1382
|
+
</div>
|
|
1383
|
+
|
|
1384
|
+
<div className="flex justify-end gap-3 mt-8">
|
|
1385
|
+
<button
|
|
1386
|
+
className="btn btn-secondary h-10 px-4"
|
|
1387
|
+
onClick={() => setIsNewFileModalOpen(false)}
|
|
1388
|
+
>
|
|
1389
|
+
Cancel
|
|
1390
|
+
</button>
|
|
1391
|
+
<button
|
|
1392
|
+
className="btn btn-primary h-10 px-6"
|
|
1393
|
+
disabled={!newFileName.trim()}
|
|
1394
|
+
onClick={() => {
|
|
1395
|
+
const fullPath = targetDirectory ? `${targetDirectory}/${newFileName.trim()}` : newFileName.trim();
|
|
1396
|
+
setEditingFile({ id: fullPath, name: newFileName.trim() });
|
|
1397
|
+
setEditorContent('');
|
|
1398
|
+
setIsEditorOpen(true);
|
|
1399
|
+
setIsNewFileModalOpen(false);
|
|
1400
|
+
}}
|
|
1401
|
+
>
|
|
1402
|
+
Create File
|
|
1403
|
+
</button>
|
|
1404
|
+
</div>
|
|
1405
|
+
</div>
|
|
1406
|
+
</div>
|
|
1407
|
+
)}
|
|
1408
|
+
|
|
1409
|
+
{/* Model Selector Modal */}
|
|
1410
|
+
{isModelSelectorOpen && (
|
|
1411
|
+
<div
|
|
1412
|
+
className="fixed inset-0 z-[80] flex items-center justify-center bg-background/60 backdrop-blur-md animate-in fade-in duration-200"
|
|
1413
|
+
onClick={(e) => { if (e.target === e.currentTarget) setIsModelSelectorOpen(false); }}
|
|
1414
|
+
>
|
|
1415
|
+
<div className="bg-card w-full max-w-md border rounded-xl shadow-2xl p-6 animate-in zoom-in-95 duration-200">
|
|
1416
|
+
<div className="flex items-center justify-between mb-6">
|
|
1417
|
+
<div className="flex items-center gap-3">
|
|
1418
|
+
<div className="p-2 bg-primary/10 rounded-lg text-primary">
|
|
1419
|
+
<Bot size={20} />
|
|
1420
|
+
</div>
|
|
1421
|
+
<h3 className="text-lg font-semibold text-foreground">Select AI Model</h3>
|
|
1422
|
+
</div>
|
|
1423
|
+
<button
|
|
1424
|
+
className="btn-icon hover:bg-muted"
|
|
1425
|
+
onClick={() => setIsModelSelectorOpen(false)}
|
|
1426
|
+
>
|
|
1427
|
+
<X size={20} />
|
|
1428
|
+
</button>
|
|
1429
|
+
</div>
|
|
1430
|
+
|
|
1431
|
+
<div className="grid grid-cols-1 gap-2 max-h-[60vh] overflow-y-auto pr-2 scrollbar-thin">
|
|
1432
|
+
{MODELS.map((model) => (
|
|
1433
|
+
<button
|
|
1434
|
+
key={model.id}
|
|
1435
|
+
onClick={() => selectModel(model.id)}
|
|
1436
|
+
className={`flex flex-col gap-1 px-4 py-3 rounded-lg text-left transition-all border ${sessionInfo?.model === model.id
|
|
1437
|
+
? 'bg-primary/5 border-primary shadow-sm'
|
|
1438
|
+
: 'bg-muted/30 border-transparent hover:bg-muted hover:border-border'
|
|
1439
|
+
}`}
|
|
1440
|
+
>
|
|
1441
|
+
<div className="flex items-center justify-between">
|
|
1442
|
+
<span className={`font-medium ${sessionInfo?.model === model.id ? 'text-primary' : 'text-foreground'}`}>
|
|
1443
|
+
{model.name}
|
|
1444
|
+
</span>
|
|
1445
|
+
{sessionInfo?.model === model.id && (
|
|
1446
|
+
<span className="text-[10px] bg-primary text-primary-foreground px-1.5 py-0.5 rounded-full font-bold uppercase tracking-wider">
|
|
1447
|
+
Active
|
|
1448
|
+
</span>
|
|
1449
|
+
)}
|
|
1450
|
+
</div>
|
|
1451
|
+
<span className="text-xs text-muted-foreground leading-relaxed">
|
|
1452
|
+
{model.description}
|
|
1453
|
+
</span>
|
|
1454
|
+
</button>
|
|
1455
|
+
))}
|
|
1456
|
+
</div>
|
|
1457
|
+
|
|
1458
|
+
<div className="mt-6 pt-6 border-t flex justify-center">
|
|
1459
|
+
<p className="text-[10px] text-muted-foreground uppercase tracking-widest font-semibold flex items-center gap-2">
|
|
1460
|
+
<RefreshCw size={10} />
|
|
1461
|
+
Session will be reset on model change
|
|
1462
|
+
</p>
|
|
1463
|
+
</div>
|
|
1464
|
+
</div>
|
|
1465
|
+
</div>
|
|
1466
|
+
)}
|
|
1467
|
+
|
|
1468
|
+
{/* Context Menu */}
|
|
1469
|
+
|
|
1470
|
+
{contextMenu && (
|
|
1471
|
+
<div
|
|
1472
|
+
className="fixed z-[100] bg-card border rounded-xl shadow-2xl py-1.5 min-w-44 animate-in fade-in zoom-in-95 duration-100"
|
|
1473
|
+
style={{ top: contextMenu.y, left: contextMenu.x }}
|
|
1474
|
+
onClick={(e) => e.stopPropagation()}
|
|
1475
|
+
>
|
|
1476
|
+
<button
|
|
1477
|
+
className="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-foreground hover:bg-primary hover:text-primary-foreground transition-colors text-left"
|
|
1478
|
+
onClick={() => handleAddNewFile(contextMenu.folderId)}
|
|
1479
|
+
>
|
|
1480
|
+
<FilePlus size={15} />
|
|
1481
|
+
<span>Add File here</span>
|
|
1482
|
+
</button>
|
|
1483
|
+
<div className="h-px bg-border my-1 mx-2" />
|
|
1484
|
+
<button
|
|
1485
|
+
className="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-muted-foreground/60 transition-colors text-left cursor-not-allowed"
|
|
1486
|
+
disabled
|
|
1487
|
+
>
|
|
1488
|
+
<Folder size={15} />
|
|
1489
|
+
<span>New Folder (soon)</span>
|
|
1490
|
+
</button>
|
|
1491
|
+
</div>
|
|
1492
|
+
)}
|
|
1493
|
+
|
|
1494
|
+
{/* Settings Modal */}
|
|
1495
|
+
{isSettingsOpen && (
|
|
1496
|
+
<div className="fixed inset-0 z-[150] flex items-center justify-center p-4 bg-background/80 backdrop-blur-sm animate-in fade-in duration-200">
|
|
1497
|
+
<div className="bg-card w-full max-w-md border rounded-2xl shadow-2xl overflow-hidden animate-in zoom-in-95 duration-200">
|
|
1498
|
+
<div className="p-6">
|
|
1499
|
+
<div className="flex items-center gap-3 mb-6">
|
|
1500
|
+
<div className="w-10 h-10 rounded-xl bg-primary/10 text-primary flex items-center justify-center">
|
|
1501
|
+
<Settings size={22} />
|
|
1502
|
+
</div>
|
|
1503
|
+
<div>
|
|
1504
|
+
<h2 className="text-xl font-semibold tracking-tight">Settings</h2>
|
|
1505
|
+
<p className="text-sm text-muted-foreground line-clamp-1">Configure your NextPi workspace</p>
|
|
1506
|
+
</div>
|
|
1507
|
+
</div>
|
|
1508
|
+
|
|
1509
|
+
<div className="space-y-6">
|
|
1510
|
+
<div className="space-y-2.5">
|
|
1511
|
+
<div className="flex items-center justify-between">
|
|
1512
|
+
<label className="text-sm font-medium">OpenRouter API Key</label>
|
|
1513
|
+
{configInfo.hasKey && (
|
|
1514
|
+
<span className="text-[10px] uppercase tracking-wider font-bold text-emerald-500 bg-emerald-500/10 px-1.5 py-0.5 rounded">Active</span>
|
|
1515
|
+
)}
|
|
1516
|
+
</div>
|
|
1517
|
+
<input
|
|
1518
|
+
type="password"
|
|
1519
|
+
className="w-full px-3.5 py-2.5 bg-background border rounded-xl outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all text-sm"
|
|
1520
|
+
placeholder={configInfo.hasKey ? `Keys ends in ${configInfo.maskedKey?.slice(-4)}` : "sk-or-v1-..."}
|
|
1521
|
+
value={tempApiKey}
|
|
1522
|
+
onChange={(e) => setTempApiKey(e.target.value)}
|
|
1523
|
+
/>
|
|
1524
|
+
<p className="text-[11px] text-muted-foreground leading-relaxed">
|
|
1525
|
+
Your key is stored locally in <code className="bg-muted px-1 rounded">~/.nextpi/config.json</code>.
|
|
1526
|
+
It is never shared with anyone except OpenRouter.
|
|
1527
|
+
</p>
|
|
1528
|
+
</div>
|
|
1529
|
+
|
|
1530
|
+
<div className="flex items-center gap-3 pt-2">
|
|
1531
|
+
<button
|
|
1532
|
+
className="flex-1 btn btn-primary py-2.5 justify-center"
|
|
1533
|
+
onClick={handleSaveConfig}
|
|
1534
|
+
disabled={!tempApiKey.trim() || isSavingConfig}
|
|
1535
|
+
>
|
|
1536
|
+
{isSavingConfig ? 'Saving...' : 'Save Configuration'}
|
|
1537
|
+
</button>
|
|
1538
|
+
{configInfo.hasKey && (
|
|
1539
|
+
<button
|
|
1540
|
+
className="btn btn-secondary py-2.5 px-4"
|
|
1541
|
+
onClick={() => {
|
|
1542
|
+
setIsSettingsOpen(false);
|
|
1543
|
+
setTempApiKey('');
|
|
1544
|
+
}}
|
|
1545
|
+
>
|
|
1546
|
+
Close
|
|
1547
|
+
</button>
|
|
1548
|
+
)}
|
|
1549
|
+
</div>
|
|
1550
|
+
</div>
|
|
1551
|
+
</div>
|
|
1552
|
+
|
|
1553
|
+
<div className="px-6 py-4 bg-muted/50 border-t flex items-center justify-between">
|
|
1554
|
+
<span className="text-[11px] text-muted-foreground">NextPi v0.1.0</span>
|
|
1555
|
+
<a
|
|
1556
|
+
href="https://openrouter.ai/keys"
|
|
1557
|
+
target="_blank"
|
|
1558
|
+
rel="noreferrer"
|
|
1559
|
+
className="text-[11px] text-primary hover:underline flex items-center gap-1"
|
|
1560
|
+
>
|
|
1561
|
+
Get a key <ExternalLink size={10} />
|
|
1562
|
+
</a>
|
|
1563
|
+
</div>
|
|
1564
|
+
</div>
|
|
1565
|
+
</div>
|
|
1566
|
+
)}
|
|
1567
|
+
|
|
1568
|
+
{/* Help Modal */}
|
|
1569
|
+
{isHelpOpen && (
|
|
1570
|
+
<div className="fixed inset-0 z-[150] flex items-center justify-center p-4 bg-background/80 backdrop-blur-sm animate-in fade-in duration-200">
|
|
1571
|
+
<div className="bg-card w-full max-w-lg border rounded-2xl shadow-2xl overflow-hidden animate-in zoom-in-95 duration-200">
|
|
1572
|
+
<div className="p-6">
|
|
1573
|
+
<div className="flex items-center justify-between mb-6">
|
|
1574
|
+
<div className="flex items-center gap-3">
|
|
1575
|
+
<div className="w-10 h-10 rounded-xl bg-primary/10 text-primary flex items-center justify-center">
|
|
1576
|
+
<HelpCircle size={22} />
|
|
1577
|
+
</div>
|
|
1578
|
+
<div>
|
|
1579
|
+
<h2 className="text-xl font-semibold tracking-tight">Quick Guide</h2>
|
|
1580
|
+
<p className="text-sm text-muted-foreground">Master the NextPi workspace</p>
|
|
1581
|
+
</div>
|
|
1582
|
+
</div>
|
|
1583
|
+
<button onClick={() => setIsHelpOpen(false)} className="btn-icon">
|
|
1584
|
+
<X size={20} />
|
|
1585
|
+
</button>
|
|
1586
|
+
</div>
|
|
1587
|
+
|
|
1588
|
+
<div className="space-y-6">
|
|
1589
|
+
{/* Keyboard Shortcuts */}
|
|
1590
|
+
<div className="space-y-3">
|
|
1591
|
+
<h3 className="text-sm font-bold uppercase tracking-widest text-muted-foreground/50 flex items-center gap-2">
|
|
1592
|
+
<Command size={14} /> Keyboard Shortcuts
|
|
1593
|
+
</h3>
|
|
1594
|
+
<div className="grid grid-cols-2 gap-2">
|
|
1595
|
+
{[
|
|
1596
|
+
{ key: 'Enter', desc: 'Send Message' },
|
|
1597
|
+
{ key: 'Shift + Enter', desc: 'New Line' },
|
|
1598
|
+
{ key: '/', desc: 'Quick Commands' },
|
|
1599
|
+
{ key: 'Esc', desc: 'Close Modals' },
|
|
1600
|
+
].map(s => (
|
|
1601
|
+
<div key={s.key} className="flex items-center justify-between p-2.5 bg-muted/30 rounded-xl border border-border/50">
|
|
1602
|
+
<span className="text-xs text-muted-foreground">{s.desc}</span>
|
|
1603
|
+
<kbd className="px-1.5 py-0.5 bg-background border rounded text-[10px] font-bold shadow-sm">{s.key}</kbd>
|
|
1604
|
+
</div>
|
|
1605
|
+
))}
|
|
1606
|
+
</div>
|
|
1607
|
+
</div>
|
|
1608
|
+
|
|
1609
|
+
{/* Slash Commands */}
|
|
1610
|
+
<div className="space-y-3">
|
|
1611
|
+
<h3 className="text-sm font-bold uppercase tracking-widest text-muted-foreground/50 flex items-center gap-2">
|
|
1612
|
+
<Play size={14} /> Core Concepts
|
|
1613
|
+
</h3>
|
|
1614
|
+
<div className="space-y-2">
|
|
1615
|
+
<div className="p-3 bg-primary/[0.03] border border-primary/10 rounded-xl">
|
|
1616
|
+
<p className="text-xs leading-relaxed text-foreground/80">
|
|
1617
|
+
<strong>The Agent </strong> is aware of your entire project directory. It can create files, run terminal commands, and research your codebase automatically.
|
|
1618
|
+
</p>
|
|
1619
|
+
</div>
|
|
1620
|
+
<div className="p-3 bg-primary/[0.03] border border-primary/10 rounded-xl">
|
|
1621
|
+
<p className="text-xs leading-relaxed text-foreground/80">
|
|
1622
|
+
<strong>Thinking Phase </strong> shows the agent's internal reasoning. Toggle it in the header if you want a cleaner view or need to understand its "logic."
|
|
1623
|
+
</p>
|
|
1624
|
+
</div>
|
|
1625
|
+
</div>
|
|
1626
|
+
</div>
|
|
1627
|
+
</div>
|
|
1628
|
+
|
|
1629
|
+
<div className="mt-8 flex justify-center">
|
|
1630
|
+
<button
|
|
1631
|
+
className="btn btn-primary px-8 py-2.5 rounded-full"
|
|
1632
|
+
onClick={() => setIsHelpOpen(false)}
|
|
1633
|
+
>
|
|
1634
|
+
Got it!
|
|
1635
|
+
</button>
|
|
1636
|
+
</div>
|
|
1637
|
+
</div>
|
|
1638
|
+
</div>
|
|
1639
|
+
</div>
|
|
1640
|
+
)}
|
|
1641
|
+
</div>
|
|
1642
|
+
);
|
|
1643
|
+
}
|