From d1fdfb140cc440664f77a24886172f9976a05a31 Mon Sep 17 00:00:00 2001 From: soryu Date: Tue, 28 Apr 2026 19:12:52 +0100 Subject: feat: revert broken directive PRs, re-implement Lexical document orchestrator (#98) * feat: soryu-co/soryu - makima: Revert broken directive PRs and verify clean build * feat: soryu-co/soryu - makima: Re-implement frontend: Lexical document editor with feature flag and base components * WIP: heartbeat checkpoint * feat: soryu-co/soryu - makima: Add contract blocks, expandable log rows, and interaction controls * WIP: heartbeat checkpoint * feat: soryu-co/soryu - makima: End-to-end build verification and integration polish --- frontend/package-lock.json | 451 ++++++-------- frontend/package.json | 6 +- frontend/src/components/ConfigModal.tsx | 35 +- frontend/src/components/VNInterface.tsx | 50 +- .../src/components/document/AutoSavePlugin.tsx | 140 ----- frontend/src/components/document/ContextMenu.css | 79 --- frontend/src/components/document/ContextMenu.tsx | 98 --- .../src/components/document/DirectiveFileTree.tsx | 166 ----- .../src/components/document/DocumentEditor.css | 246 -------- .../src/components/document/DocumentEditor.tsx | 236 ------- .../src/components/document/DocumentLayout.css | 363 ----------- .../src/components/document/DocumentLayout.tsx | 316 ---------- .../src/components/document/DocumentSettings.tsx | 76 --- frontend/src/components/document/EditorTheme.ts | 30 - frontend/src/components/document/Toast.css | 100 --- frontend/src/components/document/Toast.tsx | 97 --- frontend/src/components/document/index.ts | 1 + .../components/document/nodes/ContractBlock.css | 123 ---- .../document/nodes/ContractBlockComponent.tsx | 117 ---- .../document/nodes/ContractBlockNode.tsx | 106 ---- .../components/document/nodes/ContractLogFeed.css | 346 ----------- .../components/document/nodes/ContractLogFeed.tsx | 225 ------- .../src/components/document/nodes/StepLogFeed.tsx | 4 +- .../src/components/document/nodes/StepsDiagram.css | 683 --------------------- .../document/nodes/StepsDiagramComponent.tsx | 37 -- .../components/document/nodes/StepsDiagramNode.tsx | 91 --- frontend/src/main.tsx | 35 +- frontend/src/services/directiveApi.ts | 21 + frontend/src/services/taskWs.ts | 88 --- frontend/src/stores/index.ts | 58 +- frontend/tsconfig.tsbuildinfo | 2 +- .../20260427000000_create_user_settings.sql | 11 - makima/src/daemon/process/claude.rs | 2 - makima/src/daemon/storage/patch.rs | 1 - makima/src/daemon/task/manager.rs | 6 +- makima/src/daemon/tui/mod.rs | 1 + makima/src/db/models.rs | 12 +- makima/src/db/repository.rs | 20 +- makima/src/listen.rs | 2 - makima/src/llm/groq.rs | 1 - makima/src/orchestration/directive.rs | 99 +-- makima/src/server/handlers/listen.rs | 3 +- makima/src/server/handlers/mesh.rs | 2 +- makima/src/server/handlers/mesh_daemon.rs | 2 +- makima/src/server/handlers/mesh_supervisor.rs | 2 +- makima/src/server/handlers/mod.rs | 1 - makima/src/server/handlers/settings.rs | 196 ------ makima/src/server/mod.rs | 22 +- package-lock.json | 6 + 49 files changed, 385 insertions(+), 4430 deletions(-) delete mode 100644 frontend/src/components/document/AutoSavePlugin.tsx delete mode 100644 frontend/src/components/document/ContextMenu.css delete mode 100644 frontend/src/components/document/ContextMenu.tsx delete mode 100644 frontend/src/components/document/DirectiveFileTree.tsx delete mode 100644 frontend/src/components/document/DocumentEditor.css delete mode 100644 frontend/src/components/document/DocumentEditor.tsx delete mode 100644 frontend/src/components/document/DocumentLayout.css delete mode 100644 frontend/src/components/document/DocumentLayout.tsx delete mode 100644 frontend/src/components/document/DocumentSettings.tsx delete mode 100644 frontend/src/components/document/EditorTheme.ts delete mode 100644 frontend/src/components/document/Toast.css delete mode 100644 frontend/src/components/document/Toast.tsx delete mode 100644 frontend/src/components/document/nodes/ContractBlock.css delete mode 100644 frontend/src/components/document/nodes/ContractBlockComponent.tsx delete mode 100644 frontend/src/components/document/nodes/ContractBlockNode.tsx delete mode 100644 frontend/src/components/document/nodes/ContractLogFeed.css delete mode 100644 frontend/src/components/document/nodes/ContractLogFeed.tsx delete mode 100644 frontend/src/components/document/nodes/StepsDiagram.css delete mode 100644 frontend/src/components/document/nodes/StepsDiagramNode.tsx delete mode 100644 frontend/src/services/taskWs.ts delete mode 100644 makima/migrations/20260427000000_create_user_settings.sql delete mode 100644 makima/src/server/handlers/settings.rs create mode 100644 package-lock.json diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4c4bcbc..771bdff 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,17 +8,13 @@ "name": "pc98-vn", "version": "0.0.1", "dependencies": { - "@lexical/html": "^0.44.0", "@lexical/link": "^0.44.0", "@lexical/list": "^0.44.0", - "@lexical/markdown": "^0.44.0", "@lexical/react": "^0.44.0", "@lexical/rich-text": "^0.44.0", - "@lexical/selection": "^0.44.0", - "@lexical/utils": "^0.44.0", "@nanostores/react": "^1.0.0", "@types/three": "^0.180.0", - "lexical": "^0.44.0", + "lexical": "^0.21.0", "nanostores": "^1.0.1", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -265,6 +261,14 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", @@ -683,54 +687,6 @@ "node": ">=12" } }, - "node_modules/@floating-ui/core": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", - "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", - "dependencies": { - "@floating-ui/utils": "^0.2.11" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", - "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", - "dependencies": { - "@floating-ui/core": "^1.7.5", - "@floating-ui/utils": "^0.2.11" - } - }, - "node_modules/@floating-ui/react": { - "version": "0.27.19", - "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.19.tgz", - "integrity": "sha512-31B8h5mm8YxotlE7/AU/PhNAl8eWxAmjL/v2QOxroDNkTFLk3Uu82u63N3b6TXa4EGJeeZLVcd/9AlNlVqzeog==", - "dependencies": { - "@floating-ui/react-dom": "^2.1.8", - "@floating-ui/utils": "^0.2.11", - "tabbable": "^6.0.0" - }, - "peerDependencies": { - "react": ">=17.0.0", - "react-dom": ">=17.0.0" - } - }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", - "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", - "dependencies": { - "@floating-ui/dom": "^1.7.6" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", - "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==" - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -767,39 +723,38 @@ } }, "node_modules/@lexical/clipboard": { - "version": "0.44.0", - "resolved": "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.44.0.tgz", - "integrity": "sha512-nfmNIs7uENqlDI7cm2E4I1Yp8mDJGMhEQIrIV2rNWnL1oeHVXQ7yuYdyoPdcY1zuj/9nvkYBQYUEh0QiGwpETA==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.21.0.tgz", + "integrity": "sha512-3lNMlMeUob9fcnRXGVieV/lmPbmet/SVWckNTOwzfKrZ/YW5HiiyJrWviLRVf50dGXTbmBGt7K/2pfPYvWCHFA==", "dependencies": { - "@lexical/extension": "0.44.0", - "@lexical/html": "0.44.0", - "@lexical/list": "0.44.0", - "@lexical/selection": "0.44.0", - "@lexical/utils": "0.44.0", - "@types/trusted-types": "^2.0.7", - "lexical": "0.44.0" - } - }, - "node_modules/@lexical/code-core": { - "version": "0.44.0", - "resolved": "https://registry.npmjs.org/@lexical/code-core/-/code-core-0.44.0.tgz", - "integrity": "sha512-m57JyXTIvW1tsqw/Vuogk8jqWCZZIeFQbWybRc46ytR8ReDgzPRODpN8+dacIIeRH5yC5UC3lAa743mtdNkxqg==", + "@lexical/html": "0.21.0", + "@lexical/list": "0.21.0", + "@lexical/selection": "0.21.0", + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" + } + }, + "node_modules/@lexical/code": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@lexical/code/-/code-0.21.0.tgz", + "integrity": "sha512-E0DNSFu4I+LMn3ft+UT0Dbntc8ZKjIA0BJj6BDewm0qh3bir40YUf5DkI2lpiFNRF2OpcmmcIxakREeU6avqTA==", "dependencies": { - "@lexical/extension": "0.44.0", - "lexical": "0.44.0" + "@lexical/utils": "0.21.0", + "lexical": "0.21.0", + "prismjs": "^1.27.0" } }, "node_modules/@lexical/devtools-core": { - "version": "0.44.0", - "resolved": "https://registry.npmjs.org/@lexical/devtools-core/-/devtools-core-0.44.0.tgz", - "integrity": "sha512-X3uNG3P1vOsdzmEcy+7m9DxAcIVtVUZnvskmLqqLs6VluVVwH9xy7h1bPsvlDKvj1Nj73tWJ3TW0qXQWDTo5tw==", - "dependencies": { - "@lexical/html": "0.44.0", - "@lexical/link": "0.44.0", - "@lexical/mark": "0.44.0", - "@lexical/table": "0.44.0", - "@lexical/utils": "0.44.0", - "lexical": "0.44.0" + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@lexical/devtools-core/-/devtools-core-0.21.0.tgz", + "integrity": "sha512-csK41CmRLZbKNV5pT4fUn5RzdPjU5PoWR8EqaS9kiyayhDg2zEnuPtvUYWanLfCLH9A2oOfbEsGxjMctAySlJw==", + "dependencies": { + "@lexical/html": "0.21.0", + "@lexical/link": "0.21.0", + "@lexical/mark": "0.21.0", + "@lexical/table": "0.21.0", + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" }, "peerDependencies": { "react": ">=17.x", @@ -807,209 +762,196 @@ } }, "node_modules/@lexical/dragon": { - "version": "0.44.0", - "resolved": "https://registry.npmjs.org/@lexical/dragon/-/dragon-0.44.0.tgz", - "integrity": "sha512-RhlsjVDket9k1+YFEkDE0/7Qyrh2BI0vxBMzrWwPJTXX/4YFanYN9su8RSabkIukBBJ3QiNOOoC8FKK4Lkr4qg==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@lexical/dragon/-/dragon-0.21.0.tgz", + "integrity": "sha512-ahTCaOtRFNauEzplN1qVuPjyGAlDd+XcVM5FQCdxVh/1DvqmBxEJRVuCBqatzUUVb89jRBekYUcEdnY9iNjvEQ==", "dependencies": { - "@lexical/extension": "0.44.0", - "lexical": "0.44.0" - } - }, - "node_modules/@lexical/extension": { - "version": "0.44.0", - "resolved": "https://registry.npmjs.org/@lexical/extension/-/extension-0.44.0.tgz", - "integrity": "sha512-BsYtoc+0EU0pqcOpf/lIUDU6LQVO6zX2AawZoUWJzT3Wzfov23qsqZWvl2WGM9dnRTN5iISJL3Fl53bQVxiXxw==", - "dependencies": { - "@lexical/utils": "0.44.0", - "@preact/signals-core": "^1.14.1", - "lexical": "0.44.0" + "lexical": "0.21.0" } }, "node_modules/@lexical/hashtag": { - "version": "0.44.0", - "resolved": "https://registry.npmjs.org/@lexical/hashtag/-/hashtag-0.44.0.tgz", - "integrity": "sha512-0WATahDSqYKVTudQv3KpFbLeCpmrCpRptPFbjxOMckAX2MRpYlrExlqKfgfpri5BSQPtG49EPSGeNfSx/Faavw==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@lexical/hashtag/-/hashtag-0.21.0.tgz", + "integrity": "sha512-O4dxcZNq1Xm45HLoRifbGAYvQkg3qLoBc6ibmHnDqZL5mQDsufnH6QEKWfgDtrvp9++3iqsSC+TE7VzWIvA7ww==", "dependencies": { - "@lexical/text": "0.44.0", - "@lexical/utils": "0.44.0", - "lexical": "0.44.0" + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" } }, "node_modules/@lexical/history": { - "version": "0.44.0", - "resolved": "https://registry.npmjs.org/@lexical/history/-/history-0.44.0.tgz", - "integrity": "sha512-RGXcbFTgYL1GIWaReBI26mNSsJTfiA9EAtDY4LBeZ14NrIQhYNokKgNiOxq5Bn8xXrl2+mawQEqoMfgpWp/5YA==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@lexical/history/-/history-0.21.0.tgz", + "integrity": "sha512-Sv2sici2NnAfHYHYRSjjS139MDT8fHP6PlYM2hVr+17dOg7/fJl22VBLRgQ7/+jLtAPxQjID69jvaMlOvt4Oog==", "dependencies": { - "@lexical/extension": "0.44.0", - "@lexical/utils": "0.44.0", - "lexical": "0.44.0" + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" } }, "node_modules/@lexical/html": { - "version": "0.44.0", - "resolved": "https://registry.npmjs.org/@lexical/html/-/html-0.44.0.tgz", - "integrity": "sha512-5X6eGsgwtqPxABsuShUxF7ZfyB/U4GwSEyeonvwH1Vc/5Q2uQVjlB+FAYd+MNwWMHMh4d4+yZ3l70AtIuhr5eg==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@lexical/html/-/html-0.21.0.tgz", + "integrity": "sha512-UGahVsGz8OD7Ya39qwquE+JPStTxCw/uaQrnUNorCM7owtPidO2H+tsilAB3A1GK3ksFGdHeEjBjG0Gf7gOg+Q==", "dependencies": { - "@lexical/extension": "0.44.0", - "@lexical/selection": "0.44.0", - "@lexical/utils": "0.44.0", - "lexical": "0.44.0" + "@lexical/selection": "0.21.0", + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" } }, "node_modules/@lexical/link": { - "version": "0.44.0", - "resolved": "https://registry.npmjs.org/@lexical/link/-/link-0.44.0.tgz", - "integrity": "sha512-uvEqEol/mLEzGVQd8Rok9I48RgYPKokM/nsclI9nYcEdccVOM2Nri4ntoRwodhbccFLtjMPl8OBldwXbfc77tQ==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@lexical/link/-/link-0.21.0.tgz", + "integrity": "sha512-/coktIyRXg8rXz/7uxXsSEfSQYxPIx8CmignAXWYhcyYtCWA0fD2mhEhWwVvHH9ofNzvidclRPYKUnrmUm3z3Q==", "dependencies": { - "@lexical/extension": "0.44.0", - "@lexical/utils": "0.44.0", - "lexical": "0.44.0" + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" } }, "node_modules/@lexical/list": { - "version": "0.44.0", - "resolved": "https://registry.npmjs.org/@lexical/list/-/list-0.44.0.tgz", - "integrity": "sha512-ZTCWxDz1okPrC9FBXi1yV3W5fbQQeMUlFIcSVF9HibcVPmCsPa900IxthuiQbGiTycUyXDTOB3IUYRtlJNtpjw==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@lexical/list/-/list-0.21.0.tgz", + "integrity": "sha512-WItGlwwNJCS8b6SO1QPKzArShmD+OXQkLbhBcAh+EfpnkvmCW5T5LqY+OfIRmEN1dhDOnwqCY7mXkivWO8o5tw==", "dependencies": { - "@lexical/extension": "0.44.0", - "@lexical/utils": "0.44.0", - "lexical": "0.44.0" + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" } }, "node_modules/@lexical/mark": { - "version": "0.44.0", - "resolved": "https://registry.npmjs.org/@lexical/mark/-/mark-0.44.0.tgz", - "integrity": "sha512-bWMowllwe6BcgYMAkrsZx6Z+CX/72qCQpFKhlkR4ael92yOWSBkz68xp1wxxkSnQX9zoI1gYTeWBofVsSDKcsQ==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@lexical/mark/-/mark-0.21.0.tgz", + "integrity": "sha512-2x/LoHDYPOkZbKHz4qLFWsPywjRv9KggTOtmRazmaNRUG0FpkImJwUbbaKjWQXeESVGpzfL3qNFSAmCWthsc4g==", "dependencies": { - "@lexical/utils": "0.44.0", - "lexical": "0.44.0" + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" } }, "node_modules/@lexical/markdown": { - "version": "0.44.0", - "resolved": "https://registry.npmjs.org/@lexical/markdown/-/markdown-0.44.0.tgz", - "integrity": "sha512-DwlXdp85pYMo3exDF6W3iz8plpuP+RQ4Me4Iljm7O5aPDp0SSrIoZxyX4zS668mVAoz5HHj1Ka0kQkft8mq26Q==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@lexical/markdown/-/markdown-0.21.0.tgz", + "integrity": "sha512-XCQCyW5ujK0xR6evV8sF0hv/MRUA//kIrB2JiyF12tLQyjLRNEXO+0IKastWnMKSaDdJMKjzgd+4PiummYs7uA==", + "dependencies": { + "@lexical/code": "0.21.0", + "@lexical/link": "0.21.0", + "@lexical/list": "0.21.0", + "@lexical/rich-text": "0.21.0", + "@lexical/text": "0.21.0", + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" + } + }, + "node_modules/@lexical/offset": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@lexical/offset/-/offset-0.21.0.tgz", + "integrity": "sha512-UR0wHg+XXbq++6aeUPdU0K41xhUDBYzX+AeiqU9bZ7yoOq4grvKD8KBr5tARCSYTy0yvQnL1ddSO12TrP/98Lg==", "dependencies": { - "@lexical/code-core": "0.44.0", - "@lexical/link": "0.44.0", - "@lexical/list": "0.44.0", - "@lexical/rich-text": "0.44.0", - "@lexical/text": "0.44.0", - "@lexical/utils": "0.44.0", - "lexical": "0.44.0" + "lexical": "0.21.0" } }, "node_modules/@lexical/overflow": { - "version": "0.44.0", - "resolved": "https://registry.npmjs.org/@lexical/overflow/-/overflow-0.44.0.tgz", - "integrity": "sha512-5GYaYjSxn27pqHRfU+tQ2STF10wgJvI+MUnwTnUFSzy3dko1b+oV94K/Yx0TuEewPbwDibfoFA8CwqUvOLHAyw==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@lexical/overflow/-/overflow-0.21.0.tgz", + "integrity": "sha512-93P+d1mbvaJvZF8KK2pG22GuS2pHLtyC7N3GBfkbyAIb7TL/rYs47iR+eADJ4iNY680lylJ4Sl/AEnWvlY7hAg==", "dependencies": { - "lexical": "0.44.0" + "lexical": "0.21.0" } }, "node_modules/@lexical/plain-text": { - "version": "0.44.0", - "resolved": "https://registry.npmjs.org/@lexical/plain-text/-/plain-text-0.44.0.tgz", - "integrity": "sha512-bIV4Lljk0x70zFhkZIwzSPK5q3m9FpDisjGm2/3Q/chb+5BW3Tv8QJmqnpCiSO6S2KXO7gfSy81ZfkQ1dcd4EQ==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@lexical/plain-text/-/plain-text-0.21.0.tgz", + "integrity": "sha512-r4CsAknBD7qGYSE5fPdjpJ6EjfvzHbDtuCeKciL9muiswQhw4HeJrT1qb/QUIY+072uvXTgCgmjUmkbYnxKyPA==", "dependencies": { - "@lexical/clipboard": "0.44.0", - "@lexical/dragon": "0.44.0", - "@lexical/selection": "0.44.0", - "@lexical/utils": "0.44.0", - "lexical": "0.44.0" + "@lexical/clipboard": "0.21.0", + "@lexical/selection": "0.21.0", + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" } }, "node_modules/@lexical/react": { - "version": "0.44.0", - "resolved": "https://registry.npmjs.org/@lexical/react/-/react-0.44.0.tgz", - "integrity": "sha512-p/NQd/fMh3pXb1XqegE2ruvWDcUmfB12OidQ9nwtMtj5VfcUjQu2I+trUhgGRIADxSYxMWmw+8PPj5YSf4m5oA==", - "dependencies": { - "@floating-ui/react": "^0.27.16", - "@lexical/devtools-core": "0.44.0", - "@lexical/dragon": "0.44.0", - "@lexical/extension": "0.44.0", - "@lexical/hashtag": "0.44.0", - "@lexical/history": "0.44.0", - "@lexical/link": "0.44.0", - "@lexical/list": "0.44.0", - "@lexical/mark": "0.44.0", - "@lexical/markdown": "0.44.0", - "@lexical/overflow": "0.44.0", - "@lexical/plain-text": "0.44.0", - "@lexical/rich-text": "0.44.0", - "@lexical/table": "0.44.0", - "@lexical/text": "0.44.0", - "@lexical/utils": "0.44.0", - "@lexical/yjs": "0.44.0", - "lexical": "0.44.0", - "react-error-boundary": "^6.0.0" + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@lexical/react/-/react-0.21.0.tgz", + "integrity": "sha512-tKwx8EoNkBBKOZf8c10QfyDImH87+XUI1QDL8KXt+Lb8E4ho7g1jAjoEirNEn9gMBj33K4l2qVdbe3XmPAdpMQ==", + "dependencies": { + "@lexical/clipboard": "0.21.0", + "@lexical/code": "0.21.0", + "@lexical/devtools-core": "0.21.0", + "@lexical/dragon": "0.21.0", + "@lexical/hashtag": "0.21.0", + "@lexical/history": "0.21.0", + "@lexical/link": "0.21.0", + "@lexical/list": "0.21.0", + "@lexical/mark": "0.21.0", + "@lexical/markdown": "0.21.0", + "@lexical/overflow": "0.21.0", + "@lexical/plain-text": "0.21.0", + "@lexical/rich-text": "0.21.0", + "@lexical/selection": "0.21.0", + "@lexical/table": "0.21.0", + "@lexical/text": "0.21.0", + "@lexical/utils": "0.21.0", + "@lexical/yjs": "0.21.0", + "lexical": "0.21.0", + "react-error-boundary": "^3.1.4" }, "peerDependencies": { "react": ">=17.x", - "react-dom": ">=17.x", - "yjs": ">=13.5.22" - }, - "peerDependenciesMeta": { - "yjs": { - "optional": true - } + "react-dom": ">=17.x" } }, "node_modules/@lexical/rich-text": { - "version": "0.44.0", - "resolved": "https://registry.npmjs.org/@lexical/rich-text/-/rich-text-0.44.0.tgz", - "integrity": "sha512-IIdrutK5GY47ITjPlZB7KzUi9dBDwygsyFOwolnrYSL7m6TtGhAqrYiFg/YNOTT/nBzK3KQeCJRbnxpjJAVZtQ==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@lexical/rich-text/-/rich-text-0.21.0.tgz", + "integrity": "sha512-+pvEKUneEkGfWOSTl9jU58N9knePilMLxxOtppCAcgnaCdilOh3n5YyRppXhvmprUe0JaTseCMoik2LP51G/JA==", "dependencies": { - "@lexical/clipboard": "0.44.0", - "@lexical/dragon": "0.44.0", - "@lexical/selection": "0.44.0", - "@lexical/utils": "0.44.0", - "lexical": "0.44.0" + "@lexical/clipboard": "0.21.0", + "@lexical/selection": "0.21.0", + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" } }, "node_modules/@lexical/selection": { - "version": "0.44.0", - "resolved": "https://registry.npmjs.org/@lexical/selection/-/selection-0.44.0.tgz", - "integrity": "sha512-AEyeZJFFr5YRLeqVR+X0QAW19c4Fk4MFAQu52z2gxAyDGTj9xwVJxjfepVpfUp4P9K+sPtJ/yaqfMXH506ksSQ==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@lexical/selection/-/selection-0.21.0.tgz", + "integrity": "sha512-4u53bc8zlPPF0rnHjsGQExQ1St8NafsDd70/t1FMw7yvoMtUsKdH7+ap00esLkJOMv45unJD7UOzKRqU1X0sEA==", "dependencies": { - "lexical": "0.44.0" + "lexical": "0.21.0" } }, "node_modules/@lexical/table": { - "version": "0.44.0", - "resolved": "https://registry.npmjs.org/@lexical/table/-/table-0.44.0.tgz", - "integrity": "sha512-5Uq0O/fBCxcZp9y17fXUONY7dU9lVo/mB5JHy23laIiKzBKP5IzzTLMU9ikZTppIXbMNxYXd+R2pmy7PYTLyvw==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@lexical/table/-/table-0.21.0.tgz", + "integrity": "sha512-JhylAWcf4qKD4FmxMUt3YzH5zg2+baBr4+/haLZL7178hMvUzJwGIiWk+3hD3phzmW3WrP49uFXzM7DMSCkE8w==", "dependencies": { - "@lexical/clipboard": "0.44.0", - "@lexical/extension": "0.44.0", - "@lexical/utils": "0.44.0", - "lexical": "0.44.0" + "@lexical/clipboard": "0.21.0", + "@lexical/utils": "0.21.0", + "lexical": "0.21.0" } }, "node_modules/@lexical/text": { - "version": "0.44.0", - "resolved": "https://registry.npmjs.org/@lexical/text/-/text-0.44.0.tgz", - "integrity": "sha512-1XJD8ZbwaXljTl8k4+jjiopdhnYZm26IJw9Gv8+cIThVC0b6B3JZ/WxH97BMDcSloKvWHFkGiPztxRwNwA29Rw==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@lexical/text/-/text-0.21.0.tgz", + "integrity": "sha512-ceB4fhYejCoR8ID4uIs0sO/VyQoayRjrRWTIEMvOcQtwUkcyciKRhY0A7f2wVeq/MFStd+ajLLjy4WKYK5zUnA==", "dependencies": { - "lexical": "0.44.0" + "lexical": "0.21.0" } }, "node_modules/@lexical/utils": { - "version": "0.44.0", - "resolved": "https://registry.npmjs.org/@lexical/utils/-/utils-0.44.0.tgz", - "integrity": "sha512-/D2ptztNevfBJgtkj4uaiYBeRcvSy+1mQj6pNYaCFZIoPJIwl6H5fXwWAvpvr11vcQKP9DEEoXR+V4qkMOA+EA==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@lexical/utils/-/utils-0.21.0.tgz", + "integrity": "sha512-YzsNOAiLkCy6R3DuP18gtseDrzgx+30lFyqRvp5M7mckeYgQElwdfG5biNFDLv7BM9GjSzgU5Cunjycsx6Sjqg==", "dependencies": { - "@lexical/selection": "0.44.0", - "lexical": "0.44.0" + "@lexical/list": "0.21.0", + "@lexical/selection": "0.21.0", + "@lexical/table": "0.21.0", + "lexical": "0.21.0" } }, "node_modules/@lexical/yjs": { - "version": "0.44.0", - "resolved": "https://registry.npmjs.org/@lexical/yjs/-/yjs-0.44.0.tgz", - "integrity": "sha512-b3QTub9J/3LuwSSdooynb6GbMHBRyBT4xUbXzXqNPbDHgYe6CDrqf/uJIHRihIjAhOnPaHYqo9XUzitl++N1DQ==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@lexical/yjs/-/yjs-0.21.0.tgz", + "integrity": "sha512-AtPhC3pJ92CHz3dWoniSky7+MSK2WSd0xijc76I2qbTeXyeuFfYyhR6gWMg4knuY9Wz3vo9/+dXGdbQIPD8efw==", "dependencies": { - "@lexical/selection": "0.44.0", - "lexical": "0.44.0" + "@lexical/offset": "0.21.0", + "@lexical/selection": "0.21.0", + "lexical": "0.21.0" }, "peerDependencies": { "yjs": ">=13.5.22" @@ -1033,15 +975,6 @@ "react": ">=18.0.0" } }, - "node_modules/@preact/signals-core": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.14.1.tgz", - "integrity": "sha512-vxPpfXqrwUe9lpjqfYNjAF/0RF/eFGeLgdJzdmIIZjpOnTmGmAB4BjWone562mJGMRP4frU6iZ6ei3PDsu52Ng==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/preact" - } - }, "node_modules/@remix-run/router": { "version": "1.23.2", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", @@ -1422,11 +1355,6 @@ "meshoptimizer": "~0.22.0" } }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" - }, "node_modules/@types/webxr": { "version": "0.5.23", "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.23.tgz", @@ -1619,16 +1547,6 @@ "node": ">=6.9.0" } }, - "node_modules/isomorphic.js": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", - "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", - "peer": true, - "funding": { - "type": "GitHub Sponsors ❤", - "url": "https://github.com/sponsors/dmonad" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -1659,9 +1577,9 @@ } }, "node_modules/lexical": { - "version": "0.44.0", - "resolved": "https://registry.npmjs.org/lexical/-/lexical-0.44.0.tgz", - "integrity": "sha512-ReDUjRlFgkGoPWzvdjr7s16PUVpHATN+2NH2NiZs+PLlISTaIFFgKil2P467oP3Vg+XgmpDsUgmWZsFJTztYjg==" + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/lexical/-/lexical-0.21.0.tgz", + "integrity": "sha512-Dxc5SCG4kB+wF+Rh55ism3SuecOKeOtCtGHFGKd6pj2QKVojtjkxGTQPMt7//2z5rMSue4R+hmRM0pCEZflupA==" }, "node_modules/lib0": { "version": "0.2.117", @@ -1787,6 +1705,14 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "engines": { + "node": ">=6" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -1811,11 +1737,18 @@ } }, "node_modules/react-error-boundary": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-6.1.1.tgz", - "integrity": "sha512-BrYwPOdXi5mqkk5lw+Uvt0ThHx32rCt3BkukS4X23A2AIWDPSGX6iaWTc0y9TU/mHDA/6qOSGel+B2ERkOvD1w==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz", + "integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + }, "peerDependencies": { - "react": "^18.0.0 || ^19.0.0" + "react": ">=16.13.1" } }, "node_modules/react-refresh": { @@ -1924,11 +1857,6 @@ "node": ">=0.10.0" } }, - "node_modules/tabbable": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", - "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==" - }, "node_modules/three": { "version": "0.180.0", "resolved": "https://registry.npmjs.org/three/-/three-0.180.0.tgz", @@ -2047,23 +1975,6 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true - }, - "node_modules/yjs": { - "version": "13.6.30", - "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.30.tgz", - "integrity": "sha512-vv/9h42eCMC81ZHDFswuu/MKzkl/vyq1BhaNGfHyOonwlG4CJbQF4oiBBJPvfdeCt/PlVDWh7Nov9D34YY09uQ==", - "peer": true, - "dependencies": { - "lib0": "^0.2.99" - }, - "engines": { - "node": ">=16.0.0", - "npm": ">=8.0.0" - }, - "funding": { - "type": "GitHub Sponsors ❤", - "url": "https://github.com/sponsors/dmonad" - } } } } diff --git a/frontend/package.json b/frontend/package.json index 230a982..1ef66c3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,17 +9,13 @@ "preview": "vite preview" }, "dependencies": { - "@lexical/html": "^0.44.0", "@lexical/link": "^0.44.0", "@lexical/list": "^0.44.0", - "@lexical/markdown": "^0.44.0", "@lexical/react": "^0.44.0", "@lexical/rich-text": "^0.44.0", - "@lexical/selection": "^0.44.0", - "@lexical/utils": "^0.44.0", "@nanostores/react": "^1.0.0", "@types/three": "^0.180.0", - "lexical": "^0.44.0", + "lexical": "^0.21.0", "nanostores": "^1.0.1", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/frontend/src/components/ConfigModal.tsx b/frontend/src/components/ConfigModal.tsx index 9746e4e..e7b1f9f 100644 --- a/frontend/src/components/ConfigModal.tsx +++ b/frontend/src/components/ConfigModal.tsx @@ -1,7 +1,4 @@ import React from 'react' -import { useStore } from '@nanostores/react' -import { Link } from 'react-router-dom' -import { documentUiEnabledStore, setDocumentUiEnabled } from '../stores' type Props = { isOpen: boolean @@ -11,8 +8,6 @@ type Props = { } export const ConfigModal: React.FC = ({ isOpen, onClose, skipIntro, onSkipIntroChange }) => { - const documentUiEnabled = useStore(documentUiEnabledStore) - if (!isOpen) return null return ( @@ -20,9 +15,9 @@ export const ConfigModal: React.FC = ({ isOpen, onClose, skipIntro, onSki
e.stopPropagation()}>

Configuration

- +
- +
- -
- -
- Replace the directive management interface with an interactive document editor. This is a proof of concept. -
- {documentUiEnabled && ( - - Open Directives Editor {'\u2192'} - - )} -
- +
diff --git a/frontend/src/components/VNInterface.tsx b/frontend/src/components/VNInterface.tsx index 48b150a..0a77f39 100644 --- a/frontend/src/components/VNInterface.tsx +++ b/frontend/src/components/VNInterface.tsx @@ -9,11 +9,11 @@ import { showSettingsModalStore, isVisibleStore, yenBalanceStore, - documentUiEnabledStore, + documentEditorEnabledStore, toggleStandby, toggleShowChoices, updateTime, - setDocumentUiEnabled, + setDocumentEditorEnabled } from '../stores' interface VNInterfaceProps { @@ -28,7 +28,7 @@ export function VNInterface({ onLogout }: VNInterfaceProps) { const showSettingsModal = useStore(showSettingsModalStore) const isVisible = useStore(isVisibleStore) const yenBalance = useStore(yenBalanceStore) - const documentUiEnabled = useStore(documentUiEnabledStore) + const documentEditorEnabled = useStore(documentEditorEnabledStore) // Fade in effect on mount useEffect(() => { @@ -110,20 +110,20 @@ export function VNInterface({ onLogout }: VNInterfaceProps) { Daemons - {documentUiEnabled && ( +
+ + View: + Contracts + +
+ {documentEditorEnabled && (
- - View: + + Edit: Directives
)} -
- - {documentUiEnabled ? 'Legacy:' : 'View:'} - Contracts - -
@@ -209,28 +209,18 @@ export function VNInterface({ onLogout }: VNInterfaceProps) {
-

Experimental

+

Feature Flags

-
diff --git a/frontend/src/components/document/AutoSavePlugin.tsx b/frontend/src/components/document/AutoSavePlugin.tsx deleted file mode 100644 index d3d0eb5..0000000 --- a/frontend/src/components/document/AutoSavePlugin.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import { useEffect, useRef, useState, useCallback } from 'react'; -import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; -import { UNDO_COMMAND } from 'lexical'; - -const COUNTDOWN_DURATION_MS = 3000; -const TICK_INTERVAL_MS = 50; - -interface AutoSavePluginProps { - onAutoSave: (content: string) => void; - getContent: () => string; - enabled?: boolean; -} - -export default function AutoSavePlugin({ - onAutoSave, - getContent, - enabled = true, -}: AutoSavePluginProps) { - const [editor] = useLexicalComposerContext(); - const [countdown, setCountdown] = useState(null); - const timerRef = useRef | null>(null); - const startTimeRef = useRef(0); - const pendingContentRef = useRef(''); - const lastSavedContentRef = useRef(''); - - const clearTimer = useCallback(() => { - if (timerRef.current !== null) { - clearInterval(timerRef.current); - timerRef.current = null; - } - setCountdown(null); - }, []); - - const cancelCountdown = useCallback(() => { - clearTimer(); - }, [clearTimer]); - - const startCountdown = useCallback( - (content: string) => { - pendingContentRef.current = content; - clearTimer(); - - startTimeRef.current = Date.now(); - setCountdown(COUNTDOWN_DURATION_MS); - - timerRef.current = setInterval(() => { - const elapsed = Date.now() - startTimeRef.current; - const remaining = COUNTDOWN_DURATION_MS - elapsed; - - if (remaining <= 0) { - clearTimer(); - lastSavedContentRef.current = pendingContentRef.current; - onAutoSave(pendingContentRef.current); - } else { - setCountdown(remaining); - } - }, TICK_INTERVAL_MS); - }, - [clearTimer, onAutoSave] - ); - - // Listen for editor updates (content changes) - useEffect(() => { - if (!enabled) return; - - const unregister = editor.registerUpdateListener(({ editorState, dirtyElements, dirtyLeaves }) => { - // Only trigger on actual content changes - if (dirtyElements.size === 0 && dirtyLeaves.size === 0) return; - - const content = getContent(); - if (content !== lastSavedContentRef.current) { - startCountdown(content); - } - }); - - return unregister; - }, [editor, enabled, getContent, startCountdown]); - - // Listen for undo command to cancel countdown - useEffect(() => { - const unregister = editor.registerCommand( - UNDO_COMMAND, - () => { - cancelCountdown(); - return false; // Don't prevent the undo from executing - }, - 1 // COMMAND_PRIORITY_LOW - ); - - return unregister; - }, [editor, cancelCountdown]); - - // Listen for Escape key to cancel countdown - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Escape' && countdown !== null) { - e.preventDefault(); - cancelCountdown(); - } - }; - - document.addEventListener('keydown', handleKeyDown); - return () => document.removeEventListener('keydown', handleKeyDown); - }, [countdown, cancelCountdown]); - - // Cleanup on unmount - useEffect(() => { - return () => { - if (timerRef.current !== null) { - clearInterval(timerRef.current); - } - }; - }, []); - - if (countdown === null) return null; - - const progressPercent = (countdown / COUNTDOWN_DURATION_MS) * 100; - const secondsLeft = Math.ceil(countdown / 1000); - - return ( -
- - Saving in {secondsLeft}s... Esc to cancel - -
-
-
- -
- ); -} diff --git a/frontend/src/components/document/ContextMenu.css b/frontend/src/components/document/ContextMenu.css deleted file mode 100644 index 4eed119..0000000 --- a/frontend/src/components/document/ContextMenu.css +++ /dev/null @@ -1,79 +0,0 @@ -/* ============================================ - Custom Context Menu - ============================================ */ - -.ctx-menu { - position: fixed; - z-index: 10000; - min-width: 200px; - background: #ffffff; - border: 1px solid #e5e7eb; - border-radius: 10px; - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12), 0 2px 6px rgba(0, 0, 0, 0.06); - padding: 4px 0; - animation: ctxFadeIn 0.12s ease-out; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; -} - -/* Menu item */ -.ctx-menu-item { - display: flex; - align-items: center; - gap: 0.6rem; - width: 100%; - padding: 0.5rem 0.85rem; - border: none; - background: none; - cursor: pointer; - font-size: 0.875rem; - color: #1f2937; - text-align: left; - border-radius: 0; - transition: background 0.1s ease; -} - -.ctx-menu-item:hover:not(:disabled) { - background: #f3f4f6; -} - -.ctx-menu-item:active:not(:disabled) { - background: #e5e7eb; -} - -/* Disabled state */ -.ctx-menu-item-disabled, -.ctx-menu-item:disabled { - color: #9ca3af; - cursor: not-allowed; -} - -/* Icon */ -.ctx-menu-icon { - font-size: 1rem; - width: 1.25rem; - text-align: center; - flex-shrink: 0; -} - -.ctx-menu-label { - flex: 1; -} - -/* Divider */ -.ctx-menu-divider { - height: 1px; - background: #e5e7eb; - margin: 4px 0; -} - -/* Animation */ -@keyframes ctxFadeIn { - from { - opacity: 0; - transform: scale(0.96); - } - to { - opacity: 1; - transform: scale(1); - } -} diff --git a/frontend/src/components/document/ContextMenu.tsx b/frontend/src/components/document/ContextMenu.tsx deleted file mode 100644 index 5aed940..0000000 --- a/frontend/src/components/document/ContextMenu.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { useCallback, useEffect, useRef } from 'react'; -import './ContextMenu.css'; - -export interface ContextMenuAction { - label: string; - icon: string; - disabled?: boolean; - onClick: () => void; -} - -export interface ContextMenuProps { - x: number; - y: number; - actions: ContextMenuAction[]; - dividerAfter?: number[]; - onClose: () => void; -} - -export default function ContextMenu({ - x, - y, - actions, - dividerAfter = [], - onClose, -}: ContextMenuProps) { - const menuRef = useRef(null); - - // Adjust position so menu stays within viewport - const adjustedPosition = useCallback(() => { - const el = menuRef.current; - if (!el) return { left: x, top: y }; - const rect = el.getBoundingClientRect(); - const left = x + rect.width > window.innerWidth ? x - rect.width : x; - const top = y + rect.height > window.innerHeight ? y - rect.height : y; - return { left: Math.max(0, left), top: Math.max(0, top) }; - }, [x, y]); - - // Close on click outside - useEffect(() => { - const handler = (e: MouseEvent) => { - if (menuRef.current && !menuRef.current.contains(e.target as Node)) { - onClose(); - } - }; - // Use capture so we catch clicks before any other handler - document.addEventListener('mousedown', handler, true); - return () => document.removeEventListener('mousedown', handler, true); - }, [onClose]); - - // Close on Escape - useEffect(() => { - const handler = (e: KeyboardEvent) => { - if (e.key === 'Escape') onClose(); - }; - document.addEventListener('keydown', handler); - return () => document.removeEventListener('keydown', handler); - }, [onClose]); - - // After mount, adjust position - useEffect(() => { - const el = menuRef.current; - if (!el) return; - const pos = adjustedPosition(); - el.style.left = `${pos.left}px`; - el.style.top = `${pos.top}px`; - }, [adjustedPosition]); - - const dividerSet = new Set(dividerAfter); - - return ( -
- {actions.map((action, i) => ( -
- - {dividerSet.has(i) &&
} -
- ))} -
- ); -} diff --git a/frontend/src/components/document/DirectiveFileTree.tsx b/frontend/src/components/document/DirectiveFileTree.tsx deleted file mode 100644 index bacffe6..0000000 --- a/frontend/src/components/document/DirectiveFileTree.tsx +++ /dev/null @@ -1,166 +0,0 @@ -import React, { useEffect, useState } from 'react' -import { listDirectives, DirectiveSummary } from '../../services/directiveApi' - -interface DirectiveFileTreeProps { - selectedDirectiveId: string | null - onSelectDirective: (id: string) => void - onNewDirective: () => void -} - -interface GroupState { - [key: string]: boolean -} - -const STATUS_GROUPS = [ - { key: 'active', label: 'Active', defaultExpanded: true }, - { key: 'idle', label: 'Idle', defaultExpanded: true }, - { key: 'draft', label: 'Draft', defaultExpanded: false }, - { key: 'archived', label: 'Archived', defaultExpanded: false }, -] as const - -function statusColor(status: string): string { - switch (status.toLowerCase()) { - case 'active': - case 'running': - return '#4caf50' - case 'idle': - case 'paused': - return '#ffc107' - case 'draft': - case 'pending': - return '#9e9e9e' - case 'archived': - case 'failed': - return '#f44336' - default: - return '#9e9e9e' - } -} - -function groupDirectives(directives: DirectiveSummary[]): Record { - const groups: Record = { - active: [], - idle: [], - draft: [], - archived: [], - } - - for (const d of directives) { - const s = d.status.toLowerCase() - if (s === 'active' || s === 'running') { - groups.active.push(d) - } else if (s === 'idle' || s === 'paused') { - groups.idle.push(d) - } else if (s === 'draft' || s === 'pending') { - groups.draft.push(d) - } else { - groups.archived.push(d) - } - } - - return groups -} - -export function DirectiveFileTree({ selectedDirectiveId, onSelectDirective, onNewDirective }: DirectiveFileTreeProps) { - const [directives, setDirectives] = useState([]) - const [loading, setLoading] = useState(true) - const [error, setError] = useState(null) - const [expanded, setExpanded] = useState(() => { - const state: GroupState = {} - for (const g of STATUS_GROUPS) { - state[g.key] = g.defaultExpanded - } - return state - }) - - useEffect(() => { - async function load() { - try { - setLoading(true) - const data = await listDirectives() - setDirectives(data) - } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to load directives') - } finally { - setLoading(false) - } - } - load() - }, []) - - const toggleGroup = (key: string) => { - setExpanded(prev => ({ ...prev, [key]: !prev[key] })) - } - - const grouped = groupDirectives(directives) - - return ( -
-
- Directives - -
- - {loading &&
Loading...
} - {error &&
{error}
} - - {!loading && !error && ( -
- {STATUS_GROUPS.map(group => { - const items = grouped[group.key] - if (!items || items.length === 0) return null - - return ( -
- - - {expanded[group.key] && ( -
- {items.map(directive => ( - - ))} -
- )} -
- ) - })} -
- )} -
- ) -} diff --git a/frontend/src/components/document/DocumentEditor.css b/frontend/src/components/document/DocumentEditor.css deleted file mode 100644 index 0be1151..0000000 --- a/frontend/src/components/document/DocumentEditor.css +++ /dev/null @@ -1,246 +0,0 @@ -/* ============================================ - Document Editor - Clean, modern document UI - ============================================ */ - -.document-editor-container { - max-width: 800px; - margin: 0 auto; - padding: 2rem 1rem; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; - position: relative; -} - -/* ---- Lexical Root ---- */ -.doc-editor-root { - outline: none; - min-height: 400px; - padding: 1rem 0; - color: #1a1a2e; - line-height: 1.7; - font-size: 16px; -} - -/* ---- Headings ---- */ -.doc-editor-h1 { - font-size: 2.25rem; - font-weight: 700; - color: #0f0f23; - margin: 0 0 0.25rem 0; - padding: 0; - line-height: 1.3; - letter-spacing: -0.02em; - border: none; -} - -.doc-editor-h2 { - font-size: 1.5rem; - font-weight: 600; - color: #1a1a2e; - margin: 1.5rem 0 0.5rem 0; - line-height: 1.4; -} - -.doc-editor-h3 { - font-size: 1.2rem; - font-weight: 600; - color: #2a2a4a; - margin: 1.25rem 0 0.4rem 0; - line-height: 1.4; -} - -/* ---- Paragraphs ---- */ -.doc-editor-paragraph { - margin: 0.4rem 0; - padding: 0; - color: #374151; - line-height: 1.7; -} - -/* ---- Text Formatting ---- */ -.doc-editor-text-bold { - font-weight: 700; -} - -.doc-editor-text-italic { - font-style: italic; -} - -.doc-editor-text-underline { - text-decoration: underline; -} - -.doc-editor-text-strikethrough { - text-decoration: line-through; -} - -.doc-editor-text-code { - background: #f3f4f6; - border-radius: 3px; - padding: 0.15em 0.35em; - font-family: 'SF Mono', 'Fira Code', 'Fira Mono', Menlo, Consolas, monospace; - font-size: 0.9em; - color: #e11d48; -} - -/* ---- Lists ---- */ -.doc-editor-list-ul { - padding-left: 1.5rem; - margin: 0.5rem 0; - list-style-type: disc; -} - -.doc-editor-list-ol { - padding-left: 1.5rem; - margin: 0.5rem 0; - list-style-type: decimal; -} - -.doc-editor-listitem { - margin: 0.25rem 0; - color: #374151; -} - -.doc-editor-nested-listitem { - list-style-type: circle; -} - -/* ---- Links ---- */ -.doc-editor-link { - color: #2563eb; - text-decoration: underline; - cursor: pointer; -} - -.doc-editor-link:hover { - color: #1d4ed8; -} - -/* ---- Placeholder ---- */ -.doc-editor-placeholder { - color: #9ca3af; - position: absolute; - top: 1rem; - left: 0; - pointer-events: none; - font-size: 16px; - user-select: none; -} - -/* ---- Content Editable wrapper ---- */ -.doc-editor-input { - position: relative; -} - -.doc-editor-content-editable { - outline: none; - position: relative; -} - -/* ---- Divider between title and body ---- */ -.doc-editor-title-divider { - height: 1px; - background: #e5e7eb; - margin: 0.5rem 0 1rem 0; - border: none; -} - -/* ============================================ - Auto-Save Countdown Bar - ============================================ */ - -.autosave-bar { - position: sticky; - bottom: 0; - left: 0; - right: 0; - z-index: 50; - background: #fefce8; - border-top: 1px solid #fde68a; - padding: 0.5rem 1rem; - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; - font-size: 0.85rem; - color: #92400e; - transition: opacity 0.2s ease; -} - -.autosave-bar-hidden { - opacity: 0; - pointer-events: none; - height: 0; - padding: 0; - overflow: hidden; -} - -.autosave-bar-text { - display: flex; - align-items: center; - gap: 0.5rem; - white-space: nowrap; -} - -.autosave-bar-text kbd { - background: #fef3c7; - border: 1px solid #fde68a; - border-radius: 3px; - padding: 0.1em 0.4em; - font-size: 0.8em; - font-family: inherit; -} - -.autosave-bar-progress-track { - flex: 1; - height: 4px; - background: #fde68a; - border-radius: 2px; - overflow: hidden; - min-width: 80px; -} - -.autosave-bar-progress-fill { - height: 100%; - background: #f59e0b; - border-radius: 2px; - transition: width 0.1s linear; -} - -.autosave-bar-cancel { - background: none; - border: 1px solid #d97706; - border-radius: 4px; - color: #92400e; - padding: 0.2rem 0.6rem; - cursor: pointer; - font-size: 0.8rem; - white-space: nowrap; - transition: background 0.15s ease; -} - -.autosave-bar-cancel:hover { - background: #fef3c7; -} - -/* ============================================ - Responsive - ============================================ */ - -@media (max-width: 640px) { - .document-editor-container { - padding: 1rem 0.75rem; - } - - .doc-editor-h1 { - font-size: 1.75rem; - } - - .doc-editor-root { - font-size: 15px; - } - - .autosave-bar { - font-size: 0.78rem; - padding: 0.4rem 0.75rem; - } -} diff --git a/frontend/src/components/document/DocumentEditor.tsx b/frontend/src/components/document/DocumentEditor.tsx deleted file mode 100644 index 2ef37fe..0000000 --- a/frontend/src/components/document/DocumentEditor.tsx +++ /dev/null @@ -1,236 +0,0 @@ -import { useCallback, useRef, useState } from 'react'; -import { LexicalComposer } from '@lexical/react/LexicalComposer'; -import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'; -import { ContentEditable } from '@lexical/react/LexicalContentEditable'; -import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin'; -import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin'; -import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary'; -import { HeadingNode } from '@lexical/rich-text'; -import { ListNode, ListItemNode } from '@lexical/list'; -import { LinkNode } from '@lexical/link'; -import { - $getRoot, - $createParagraphNode, - $createTextNode, - type EditorState, - type LexicalEditor, -} from 'lexical'; -import { $createHeadingNode } from '@lexical/rich-text'; - -import { StepsDiagramNode, $isStepsDiagramNode, $createStepsDiagramNode } from './nodes/StepsDiagramNode'; -import { ContractBlockNode, $isContractBlockNode } from './nodes/ContractBlockNode'; -import editorTheme from './EditorTheme'; -import AutoSavePlugin from './AutoSavePlugin'; -import ContextMenu, { type ContextMenuAction } from './ContextMenu'; -import './DocumentEditor.css'; -import './nodes/StepsDiagram.css'; -import './nodes/ContractBlock.css'; - -interface DocumentEditorProps { - directiveId: string; - title: string; - goal: string; - status: string; - prBranch?: string | null; - onGoalChange?: (newGoal: string) => void; - onTitleChange?: (newTitle: string) => void; - onCleanup?: () => void; - onCreatePr?: () => void; - onPlanOrders?: () => void; - onTogglePause?: () => void; - readOnly?: boolean; -} - -function buildInitialEditorState(directiveId: string, title: string, goal: string) { - return () => { - const root = $getRoot(); - - // Title as H1 - const heading = $createHeadingNode('h1'); - heading.append($createTextNode(title)); - root.append(heading); - - // Goal as paragraph(s), split by newlines - const lines = goal.split('\n'); - for (const line of lines) { - const paragraph = $createParagraphNode(); - if (line.trim()) { - paragraph.append($createTextNode(line)); - } - root.append(paragraph); - } - - // Insert steps diagram node after the goal content - const stepsNode = $createStepsDiagramNode(directiveId); - root.append(stepsNode); - - // Add a trailing paragraph so the user can type below the diagram - const trailingParagraph = $createParagraphNode(); - root.append(trailingParagraph); - }; -} - -function onError(error: Error) { - console.error('[DocumentEditor] Lexical error:', error); -} - -export default function DocumentEditor({ - directiveId, - title, - goal, - status, - prBranch, - onGoalChange, - onTitleChange, - onCleanup, - onCreatePr, - onPlanOrders, - onTogglePause, - readOnly = false, -}: DocumentEditorProps) { - const editorRef = useRef(null); - const latestGoalRef = useRef(goal); - const latestTitleRef = useRef(title); - - // Context menu state - const [ctxMenu, setCtxMenu] = useState<{ x: number; y: number } | null>(null); - - const initialConfig = { - namespace: `DocumentEditor-${directiveId}`, - theme: editorTheme, - editorState: buildInitialEditorState(directiveId, title, goal), - nodes: [HeadingNode, ListNode, ListItemNode, LinkNode, StepsDiagramNode, ContractBlockNode], - onError, - editable: !readOnly, - }; - - const handleChange = useCallback( - (_editorState: EditorState, editor: LexicalEditor) => { - editorRef.current = editor; - - editor.getEditorState().read(() => { - const root = $getRoot(); - const children = root.getChildren(); - - let newTitle = ''; - const goalLines: string[] = []; - - for (let i = 0; i < children.length; i++) { - const child = children[i]; - // Skip decorator nodes (steps diagram, contract blocks) when extracting text - if ($isStepsDiagramNode(child)) continue; - if ($isContractBlockNode(child)) continue; - - const text = child.getTextContent(); - - if (i === 0 && child.getType() === 'heading') { - newTitle = text; - } else { - goalLines.push(text); - } - } - - const newGoal = goalLines.join('\n'); - - if (newTitle !== latestTitleRef.current) { - latestTitleRef.current = newTitle; - onTitleChange?.(newTitle); - } - - if (newGoal !== latestGoalRef.current) { - latestGoalRef.current = newGoal; - onGoalChange?.(newGoal); - } - }); - }, - [onGoalChange, onTitleChange] - ); - - const getContent = useCallback(() => { - return latestGoalRef.current; - }, []); - - const handleAutoSave = useCallback( - (content: string) => { - onGoalChange?.(content); - }, - [onGoalChange] - ); - - // Context menu handler - const handleContextMenu = useCallback( - (e: React.MouseEvent) => { - e.preventDefault(); - setCtxMenu({ x: e.clientX, y: e.clientY }); - }, - [] - ); - - const closeCtxMenu = useCallback(() => setCtxMenu(null), []); - - const isPaused = status === 'paused'; - const isIdle = status === 'idle'; - - const ctxActions: ContextMenuAction[] = [ - { - label: 'Clean Up', - icon: '\uD83E\uDDF9', // broom - disabled: !isIdle, - onClick: () => onCleanup?.(), - }, - { - label: 'Update PR', - icon: '\uD83D\uDD00', // shuffle - disabled: !prBranch, - onClick: () => onCreatePr?.(), - }, - { - label: 'Plan Orders', - icon: '\uD83D\uDCCB', // clipboard - onClick: () => onPlanOrders?.(), - }, - { - label: isPaused ? 'Resume Directive' : 'Pause Directive', - icon: isPaused ? '\u25B6' : '\u23F8', - onClick: () => onTogglePause?.(), - }, - ]; - - return ( -
- -
- - } - placeholder={ -
Start writing...
- } - ErrorBoundary={LexicalErrorBoundary} - /> -
- - - {!readOnly && ( - - )} -
- - {ctxMenu && ( - - )} -
- ); -} - diff --git a/frontend/src/components/document/DocumentLayout.css b/frontend/src/components/document/DocumentLayout.css deleted file mode 100644 index ae73e7a..0000000 --- a/frontend/src/components/document/DocumentLayout.css +++ /dev/null @@ -1,363 +0,0 @@ -/* Document Layout - Main container */ -.document-layout { - display: flex; - height: 100vh; - width: 100vw; - overflow: hidden; - background: #1a1a2e; - color: #e0e0e0; -} - -/* Sidebar */ -.document-sidebar { - flex-shrink: 0; - height: 100%; - overflow-y: auto; - overflow-x: hidden; - background: #16162a; - border-right: 1px solid #2a2a4a; -} - -/* Back link */ -.document-sidebar-back { - padding: 8px 12px; - border-bottom: 1px solid #2a2a4a; -} - -.document-back-link { - color: #9ca3af; - text-decoration: none; - font-size: 0.85rem; - display: flex; - align-items: center; - gap: 4px; - transition: color 0.15s; -} - -.document-back-link:hover { - color: #e0e0e0; -} - -/* Resize handle */ -.document-resize-handle { - width: 4px; - cursor: col-resize; - background: transparent; - flex-shrink: 0; - transition: background 0.15s; -} - -.document-resize-handle:hover, -.document-resize-handle:active { - background: #4a4a8a; -} - -/* Main content area */ -.document-main { - flex: 1; - display: flex; - flex-direction: column; - min-width: 0; - overflow: hidden; -} - -/* Top bar */ -.document-topbar { - display: flex; - align-items: center; - justify-content: space-between; - padding: 8px 20px; - border-bottom: 1px solid #2a2a4a; - background: #1e1e38; - flex-shrink: 0; -} - -.document-topbar-left { - display: flex; - align-items: center; - gap: 12px; - min-width: 0; -} - -.document-topbar-title { - font-size: 16px; - font-weight: 600; - margin: 0; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - color: #f0f0f0; -} - -.document-topbar-right { - display: flex; - align-items: center; - gap: 8px; -} - -.document-topbar-gear { - background: none; - border: none; - color: #888; - font-size: 20px; - cursor: pointer; - padding: 4px 8px; - border-radius: 4px; - transition: color 0.15s, background 0.15s; -} - -.document-topbar-gear:hover { - color: #fff; - background: rgba(255, 255, 255, 0.08); -} - -/* Status badge */ -.doc-status-badge { - display: inline-block; - padding: 2px 10px; - border-radius: 10px; - font-size: 11px; - font-weight: 600; - text-transform: uppercase; - color: #fff; - letter-spacing: 0.5px; -} - -/* Content area */ -.document-content { - flex: 1; - overflow-y: auto; - overflow-x: hidden; - display: flex; - flex-direction: column; - align-items: center; - scroll-behavior: smooth; - /* Ensure expanded log feeds don't break layout */ - min-height: 0; -} - -/* Placeholder / empty state */ -.document-placeholder { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 100%; - text-align: center; - color: #888; - padding: 40px; -} - -.document-placeholder-icon { - font-size: 48px; - margin-bottom: 16px; - opacity: 0.5; -} - -.document-placeholder h2 { - font-size: 20px; - font-weight: 500; - margin: 0 0 8px; - color: #aaa; -} - -.document-placeholder p { - font-size: 14px; - margin: 0; - max-width: 400px; - line-height: 1.5; -} - -.document-error { - color: #f44336; -} - -/* File Tree styles */ -.directive-file-tree { - display: flex; - flex-direction: column; - height: 100%; - font-size: 13px; -} - -.file-tree-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 12px 14px; - border-bottom: 1px solid #2a2a4a; - flex-shrink: 0; -} - -.file-tree-title { - font-size: 11px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 1px; - color: #888; -} - -.file-tree-new-btn { - background: none; - border: 1px solid #3a3a6a; - color: #aaa; - font-size: 16px; - width: 24px; - height: 24px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 4px; - cursor: pointer; - padding: 0; - line-height: 1; - transition: all 0.15s; -} - -.file-tree-new-btn:hover { - background: #3a3a6a; - color: #fff; - border-color: #5a5a9a; -} - -.file-tree-loading, -.file-tree-error { - padding: 16px; - color: #888; - font-size: 12px; - text-align: center; -} - -.file-tree-error { - color: #f44336; -} - -.file-tree-groups { - flex: 1; - overflow-y: auto; - padding: 4px 0; -} - -/* Group header */ -.file-tree-group { - margin-bottom: 2px; -} - -.file-tree-group-header { - display: flex; - align-items: center; - gap: 6px; - width: 100%; - padding: 6px 14px; - background: none; - border: none; - color: #999; - font-size: 11px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.5px; - cursor: pointer; - text-align: left; - transition: color 0.15s; -} - -.file-tree-group-header:hover { - color: #ccc; -} - -.file-tree-chevron { - font-size: 8px; - transition: transform 0.15s; - display: inline-block; -} - -.file-tree-chevron.expanded { - transform: rotate(90deg); -} - -.file-tree-group-count { - margin-left: auto; - color: #666; - font-size: 10px; -} - -.file-tree-group-label { - flex: 1; -} - -/* Tree items */ -.file-tree-items { - padding: 0; -} - -.file-tree-item { - display: flex; - align-items: center; - gap: 8px; - width: 100%; - padding: 5px 14px 5px 28px; - background: none; - border: none; - color: #ccc; - font-size: 13px; - cursor: pointer; - text-align: left; - transition: background 0.1s; - white-space: nowrap; - overflow: hidden; -} - -.file-tree-item:hover { - background: rgba(255, 255, 255, 0.05); -} - -.file-tree-item.selected { - background: rgba(100, 100, 200, 0.15); - color: #fff; -} - -.file-tree-status-dot { - width: 7px; - height: 7px; - border-radius: 50%; - flex-shrink: 0; -} - -.file-tree-doc-icon { - font-size: 14px; - flex-shrink: 0; - opacity: 0.7; -} - -.file-tree-item-title { - overflow: hidden; - text-overflow: ellipsis; - flex: 1; -} - -.file-tree-step-count { - margin-left: auto; - font-size: 10px; - color: #666; - background: rgba(255, 255, 255, 0.06); - border-radius: 8px; - padding: 1px 6px; - flex-shrink: 0; - white-space: nowrap; -} - -/* Responsive: mobile */ -@media (max-width: 768px) { - .document-sidebar { - position: absolute; - z-index: 100; - left: 0; - top: 0; - height: 100%; - box-shadow: 4px 0 20px rgba(0, 0, 0, 0.5); - } - - .document-resize-handle { - display: none; - } -} diff --git a/frontend/src/components/document/DocumentLayout.tsx b/frontend/src/components/document/DocumentLayout.tsx deleted file mode 100644 index 05f4190..0000000 --- a/frontend/src/components/document/DocumentLayout.tsx +++ /dev/null @@ -1,316 +0,0 @@ -import { useEffect, useState, useCallback, useRef, useMemo } from 'react' -import { useParams, useNavigate, Link } from 'react-router-dom' -import { DirectiveFileTree } from './DirectiveFileTree' -import DocumentEditor from './DocumentEditor' -import { ToastProvider, useToast } from './Toast' -import { - type DirectiveWithSteps, - type DirectiveStep, - getDirective, - getDirectiveSteps, - updateGoal, - updateDirective, - cleanupDirective, - createPr, - pickUpOrders, - pauseDirective, - startDirective, -} from '../../services/directiveApi' -import './DocumentLayout.css' - -function StatusBadge({ status }: { status: string }) { - const colors: Record = { - active: '#4caf50', - running: '#4caf50', - idle: '#ffc107', - paused: '#ffc107', - draft: '#9e9e9e', - pending: '#9e9e9e', - archived: '#f44336', - failed: '#f44336', - } - const color = colors[status.toLowerCase()] || '#9e9e9e' - - return ( - - {status} - - ) -} - -function DocumentLayoutInner() { - const { id: urlDirectiveId } = useParams<{ id: string }>() - const navigate = useNavigate() - const { addToast } = useToast() - - const [selectedId, setSelectedId] = useState(urlDirectiveId || null) - const [directive, setDirective] = useState(null) - const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) - const [sidebarWidth, setSidebarWidth] = useState(250) - const resizingRef = useRef(false) - const startXRef = useRef(0) - const startWidthRef = useRef(250) - const pollRef = useRef | null>(null) - - // Sync URL param on mount - useEffect(() => { - if (urlDirectiveId && urlDirectiveId !== selectedId) { - setSelectedId(urlDirectiveId) - } - }, [urlDirectiveId]) - - // Handle directive selection - update URL - const handleSelectDirective = useCallback((id: string) => { - setSelectedId(id) - navigate(`/directives/${id}`, { replace: true }) - }, [navigate]) - - // Load directive when selected - useEffect(() => { - if (!selectedId) { - setDirective(null) - return - } - - let cancelled = false - async function load() { - try { - setLoading(true) - setError(null) - const data = await getDirective(selectedId!) - if (!cancelled) setDirective(data) - } catch (err) { - if (!cancelled) { - const msg = err instanceof Error ? err.message : 'Failed to load directive' - setError(msg) - addToast(msg, 'error') - } - } finally { - if (!cancelled) setLoading(false) - } - } - load() - - return () => { cancelled = true } - }, [selectedId, addToast]) - - // Step polling (after goal update triggers supervisor) - const startStepPolling = useCallback(() => { - if (pollRef.current) clearInterval(pollRef.current) - pollRef.current = setInterval(async () => { - if (!selectedId) return - try { - const data = await getDirective(selectedId) - setDirective(data) - } catch { - // Silently fail for polling - } - }, 3000) - // Stop after 60 seconds - setTimeout(() => { - if (pollRef.current) { - clearInterval(pollRef.current) - pollRef.current = null - } - }, 60000) - }, [selectedId]) - - useEffect(() => { - return () => { - if (pollRef.current) clearInterval(pollRef.current) - } - }, []) - - // Auto-save goal changes - const handleGoalChange = useCallback(async (newGoal: string) => { - if (!selectedId) return - try { - const updated = await updateGoal(selectedId, newGoal) - setDirective(updated) - addToast('Goal saved', 'success') - startStepPolling() - } catch (err) { - addToast(`Failed to save goal: ${(err as Error).message}`, 'error') - } - }, [selectedId, addToast, startStepPolling]) - - const handleTitleChange = useCallback(async (newTitle: string) => { - if (!selectedId || !directive) return - try { - const updated = await updateDirective(selectedId, { - title: newTitle, - version: directive.version, - }) - setDirective(updated) - } catch (err) { - addToast(`Failed to update title: ${(err as Error).message}`, 'error') - } - }, [selectedId, directive, addToast]) - - const handleCleanup = useCallback(async () => { - if (!selectedId) return - try { - await cleanupDirective(selectedId) - addToast('Cleanup contract spawned', 'success') - startStepPolling() - } catch (err) { - addToast(`Cleanup failed: ${(err as Error).message}`, 'error') - } - }, [selectedId, addToast, startStepPolling]) - - const handleCreatePr = useCallback(async () => { - if (!selectedId) return - try { - await createPr(selectedId) - addToast('PR update triggered', 'success') - } catch (err) { - addToast(`PR update failed: ${(err as Error).message}`, 'error') - } - }, [selectedId, addToast]) - - const handlePlanOrders = useCallback(async () => { - if (!selectedId) return - try { - await pickUpOrders(selectedId) - addToast('Planning orders...', 'info') - startStepPolling() - } catch (err) { - addToast(`Plan orders failed: ${(err as Error).message}`, 'error') - } - }, [selectedId, addToast, startStepPolling]) - - const handleTogglePause = useCallback(async () => { - if (!selectedId || !directive) return - try { - if (directive.status === 'paused') { - const result = await startDirective(selectedId) - setDirective(result) - addToast('Directive resumed', 'success') - } else { - const updated = await pauseDirective(selectedId) - setDirective(updated) - addToast('Directive paused', 'info') - } - } catch (err) { - addToast(`Failed to toggle pause: ${(err as Error).message}`, 'error') - } - }, [selectedId, directive, addToast]) - - // Sidebar resize handlers - const handleMouseDown = useCallback((e: React.MouseEvent) => { - resizingRef.current = true - startXRef.current = e.clientX - startWidthRef.current = sidebarWidth - document.body.style.cursor = 'col-resize' - document.body.style.userSelect = 'none' - - const handleMouseMove = (e: MouseEvent) => { - if (!resizingRef.current) return - const diff = e.clientX - startXRef.current - const newWidth = Math.max(180, Math.min(500, startWidthRef.current + diff)) - setSidebarWidth(newWidth) - } - - const handleMouseUp = () => { - resizingRef.current = false - document.body.style.cursor = '' - document.body.style.userSelect = '' - document.removeEventListener('mousemove', handleMouseMove) - document.removeEventListener('mouseup', handleMouseUp) - } - - document.addEventListener('mousemove', handleMouseMove) - document.addEventListener('mouseup', handleMouseUp) - }, [sidebarWidth]) - - const handleNewDirective = useCallback(() => { - // Placeholder - will be implemented with full directive creation flow - console.log('New directive requested') - }, []) - - return ( -
- {/* Sidebar */} -
-
- - {'\u2190'} Back to Main - -
- -
- - {/* Resize handle */} -
- - {/* Main content */} -
- {directive && ( -
-
-

{directive.title || 'Untitled'}

- -
-
- -
-
- )} - -
- {loading && ( -
-

Loading directive...

-
- )} - - {error && ( -
-

Error: {error}

-
- )} - - {!loading && !error && !directive && ( -
-
{'\u{1F4DD}'}
-

No directive selected

-

Select a directive from the sidebar or create a new one to get started.

-
- )} - - {!loading && !error && directive && ( - - )} -
-
-
- ) -} - -// Wrapper that provides toast context -export default function DocumentLayout() { - return ( - - - - ) -} diff --git a/frontend/src/components/document/DocumentSettings.tsx b/frontend/src/components/document/DocumentSettings.tsx deleted file mode 100644 index b575b3d..0000000 --- a/frontend/src/components/document/DocumentSettings.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { useState, useCallback } from 'react' -import { upsertUserSetting } from '../../services/directiveApi' - -interface DocumentSettingsProps { - isOpen: boolean - onClose: () => void - enabled: boolean - onToggle: (enabled: boolean) => void -} - -export default function DocumentSettings({ - isOpen, - onClose, - enabled, - onToggle, -}: DocumentSettingsProps) { - const [saving, setSaving] = useState(false) - - const handleToggle = useCallback(async () => { - const newValue = !enabled - setSaving(true) - try { - // Update localStorage immediately for instant UI response - localStorage.setItem('document_ui_enabled', JSON.stringify(newValue)) - onToggle(newValue) - - // Persist to backend - await upsertUserSetting('document_ui_enabled', newValue) - } catch (err) { - console.error('Failed to save document UI setting:', err) - // Revert on failure - localStorage.setItem('document_ui_enabled', JSON.stringify(!newValue)) - onToggle(!newValue) - } finally { - setSaving(false) - } - }, [enabled, onToggle]) - - if (!isOpen) return null - - return ( -
-
e.stopPropagation()}> -
-

Document UI Settings

- -
- -
-
- -
- Replace the directive management interface with an interactive - document editor. This is a proof of concept. -
-
-
- -
- -
-
-
- ) -} diff --git a/frontend/src/components/document/EditorTheme.ts b/frontend/src/components/document/EditorTheme.ts deleted file mode 100644 index 5b336ad..0000000 --- a/frontend/src/components/document/EditorTheme.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { EditorThemeClasses } from 'lexical'; - -const editorTheme: EditorThemeClasses = { - root: 'doc-editor-root', - paragraph: 'doc-editor-paragraph', - heading: { - h1: 'doc-editor-h1', - h2: 'doc-editor-h2', - h3: 'doc-editor-h3', - }, - text: { - bold: 'doc-editor-text-bold', - italic: 'doc-editor-text-italic', - underline: 'doc-editor-text-underline', - strikethrough: 'doc-editor-text-strikethrough', - code: 'doc-editor-text-code', - }, - list: { - ul: 'doc-editor-list-ul', - ol: 'doc-editor-list-ol', - listitem: 'doc-editor-listitem', - nested: { - listitem: 'doc-editor-nested-listitem', - }, - }, - link: 'doc-editor-link', - placeholder: 'doc-editor-placeholder', -}; - -export default editorTheme; diff --git a/frontend/src/components/document/Toast.css b/frontend/src/components/document/Toast.css deleted file mode 100644 index e97304c..0000000 --- a/frontend/src/components/document/Toast.css +++ /dev/null @@ -1,100 +0,0 @@ -/* ============================================ - Toast Notifications - ============================================ */ - -.toast-container { - position: fixed; - bottom: 1.5rem; - right: 1.5rem; - z-index: 9999; - display: flex; - flex-direction: column-reverse; - gap: 0.5rem; - pointer-events: none; -} - -.toast-item { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.65rem 1rem; - border-radius: 8px; - font-size: 0.875rem; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12), 0 1px 3px rgba(0, 0, 0, 0.08); - pointer-events: auto; - max-width: 360px; - backdrop-filter: blur(6px); -} - -/* Types */ -.toast-success { - background: #ecfdf5; - color: #065f46; - border: 1px solid #a7f3d0; -} - -.toast-error { - background: #fef2f2; - color: #991b1b; - border: 1px solid #fecaca; -} - -.toast-info { - background: #eff6ff; - color: #1e40af; - border: 1px solid #bfdbfe; -} - -/* Icon */ -.toast-icon { - flex-shrink: 0; - font-size: 1rem; - line-height: 1; -} - -.toast-message { - line-height: 1.4; -} - -/* Animations */ -.toast-enter { - animation: toastSlideIn 0.25s ease-out forwards; -} - -.toast-exit { - animation: toastSlideOut 0.3s ease-in forwards; -} - -@keyframes toastSlideIn { - from { - opacity: 0; - transform: translateY(8px) scale(0.96); - } - to { - opacity: 1; - transform: translateY(0) scale(1); - } -} - -@keyframes toastSlideOut { - from { - opacity: 1; - transform: translateY(0) scale(1); - } - to { - opacity: 0; - transform: translateY(8px) scale(0.96); - } -} - -@media (max-width: 640px) { - .toast-container { - right: 0.75rem; - bottom: 0.75rem; - } - .toast-item { - max-width: calc(100vw - 1.5rem); - font-size: 0.8rem; - } -} diff --git a/frontend/src/components/document/Toast.tsx b/frontend/src/components/document/Toast.tsx deleted file mode 100644 index 653db8f..0000000 --- a/frontend/src/components/document/Toast.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { - createContext, - useCallback, - useContext, - useEffect, - useRef, - useState, - type ReactNode, -} from 'react'; -import './Toast.css'; - -// -- Types ------------------------------------------------------------------- - -export type ToastType = 'success' | 'error' | 'info'; - -interface ToastItem { - id: number; - message: string; - type: ToastType; -} - -interface ToastContextValue { - addToast: (message: string, type?: ToastType) => void; -} - -// -- Context ----------------------------------------------------------------- - -const ToastContext = createContext(null); - -export function useToast(): ToastContextValue { - const ctx = useContext(ToastContext); - if (!ctx) throw new Error('useToast must be used within a ToastProvider'); - return ctx; -} - -// -- Provider ---------------------------------------------------------------- - -const DISMISS_MS = 3000; - -export function ToastProvider({ children }: { children: ReactNode }) { - const [toasts, setToasts] = useState([]); - const nextId = useRef(0); - - const addToast = useCallback((message: string, type: ToastType = 'info') => { - const id = nextId.current++; - setToasts((prev) => [...prev, { id, message, type }]); - }, []); - - const removeToast = useCallback((id: number) => { - setToasts((prev) => prev.filter((t) => t.id !== id)); - }, []); - - return ( - - {children} -
- {toasts.map((t) => ( - - ))} -
-
- ); -} - -// -- Single toast ------------------------------------------------------------ - -function ToastItem({ - toast, - onDismiss, -}: { - toast: ToastItem; - onDismiss: (id: number) => void; -}) { - const [exiting, setExiting] = useState(false); - - useEffect(() => { - const timer = setTimeout(() => setExiting(true), DISMISS_MS - 300); - const remove = setTimeout(() => onDismiss(toast.id), DISMISS_MS); - return () => { - clearTimeout(timer); - clearTimeout(remove); - }; - }, [toast.id, onDismiss]); - - const icon = - toast.type === 'success' ? '\u2713' : toast.type === 'error' ? '\u2717' : '\u2139'; - - return ( -
- {icon} - {toast.message} -
- ); -} diff --git a/frontend/src/components/document/index.ts b/frontend/src/components/document/index.ts index 3217a1b..906c1dc 100644 --- a/frontend/src/components/document/index.ts +++ b/frontend/src/components/document/index.ts @@ -11,3 +11,4 @@ export { ContractBlockNode, $createContractBlockNode, $isContractBlockNode } fro export { StepsDiagramComponent } from './nodes/StepsDiagramComponent' export { ContractBlockComponent } from './nodes/ContractBlockComponent' export { StepLogFeed } from './nodes/StepLogFeed' +export { ContractLogFeed } from './nodes/ContractLogFeed' diff --git a/frontend/src/components/document/nodes/ContractBlock.css b/frontend/src/components/document/nodes/ContractBlock.css deleted file mode 100644 index 80edb74..0000000 --- a/frontend/src/components/document/nodes/ContractBlock.css +++ /dev/null @@ -1,123 +0,0 @@ -/* ============================================ - Contract Block - Inline contract reference - ============================================ */ - -.contract-block-wrapper { - margin: 1rem 0; - user-select: none; -} - -.contract-block { - background: #fafbff; - border: 1px solid #e2e5ef; - border-radius: 8px; - padding: 0.65rem 0.85rem; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; - font-size: 13px; - color: #374151; - transition: box-shadow 0.2s ease, border-color 0.2s ease; - animation: contractBlockAppear 0.25s ease-out both; -} - -@keyframes contractBlockAppear { - from { - opacity: 0; - transform: translateY(4px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.contract-block:hover { - border-color: #c7cce0; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); -} - -.contract-block--error { - border-color: #fecaca; - background: #fef2f2; -} - -/* Header */ -.contract-block-header { - display: flex; - align-items: center; - gap: 0.5rem; -} - -.contract-block-icon { - font-size: 1rem; - flex-shrink: 0; -} - -.contract-block-name { - font-weight: 600; - font-size: 0.88rem; - color: #1f2937; - flex: 1; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.contract-block-phase-badge { - font-size: 0.68rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.03em; - padding: 0.1rem 0.4rem; - border-radius: 8px; - white-space: nowrap; - flex-shrink: 0; -} - -.contract-block-status-dot { - width: 8px; - height: 8px; - border-radius: 50%; - flex-shrink: 0; -} - -/* Meta */ -.contract-block-meta { - margin-top: 0.3rem; - padding-left: 1.5rem; -} - -.contract-block-type { - font-size: 0.75rem; - color: #9ca3af; - font-style: italic; -} - -.contract-block-error-msg { - margin-top: 0.25rem; - font-size: 0.78rem; - color: #dc2626; - padding-left: 1.5rem; -} - -/* Loading state */ -.contract-block-loading { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.4rem 0; - color: #9ca3af; - font-size: 0.82rem; -} - -.contract-block-spinner { - width: 14px; - height: 14px; - border: 2px solid #e5e7eb; - border-top-color: #6b7280; - border-radius: 50%; - animation: spin 0.8s linear infinite; -} - -@keyframes spin { - to { transform: rotate(360deg); } -} diff --git a/frontend/src/components/document/nodes/ContractBlockComponent.tsx b/frontend/src/components/document/nodes/ContractBlockComponent.tsx deleted file mode 100644 index 0d9a25a..0000000 --- a/frontend/src/components/document/nodes/ContractBlockComponent.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import './ContractBlock.css'; - -interface ContractBlockComponentProps { - contractId: string; - contractName: string; -} - -interface ContractInfo { - id: string; - name: string; - status: string; - phase: string; - contract_type: string; -} - -const PHASE_COLORS: Record = { - planning: '#3b82f6', - execution: '#f59e0b', - review: '#8b5cf6', - completed: '#10b981', - failed: '#ef4444', -}; - -const STATUS_COLORS: Record = { - active: '#10b981', - running: '#10b981', - idle: '#f59e0b', - paused: '#f59e0b', - completed: '#10b981', - failed: '#ef4444', - archived: '#6b7280', -}; - -export function ContractBlockComponent({ contractId, contractName }: ContractBlockComponentProps) { - const [contract, setContract] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - let cancelled = false; - - async function fetchContract() { - try { - const response = await fetch(`/api/v1/contracts/${contractId}`); - if (!response.ok) throw new Error(`HTTP ${response.status}`); - const data = await response.json(); - if (!cancelled) { - setContract(data.contract || data); - setError(null); - } - } catch (err) { - if (!cancelled) { - setError(err instanceof Error ? err.message : 'Failed to load'); - } - } finally { - if (!cancelled) setLoading(false); - } - } - - fetchContract(); - return () => { cancelled = true; }; - }, [contractId]); - - if (loading) { - return ( -
-
-
- Loading contract... -
-
- ); - } - - if (error) { - return ( -
-
- 📦 - {contractName} -
-
Unable to load: {error}
-
- ); - } - - const phase = contract?.phase?.toLowerCase() || 'unknown'; - const status = contract?.status?.toLowerCase() || 'unknown'; - const phaseColor = PHASE_COLORS[phase] || '#6b7280'; - const statusColor = STATUS_COLORS[status] || '#6b7280'; - - return ( -
-
- 📦 - {contract?.name || contractName} - - {phase} - - -
- {contract?.contract_type && ( -
- {contract.contract_type} -
- )} -
- ); -} diff --git a/frontend/src/components/document/nodes/ContractBlockNode.tsx b/frontend/src/components/document/nodes/ContractBlockNode.tsx deleted file mode 100644 index 86e4c9d..0000000 --- a/frontend/src/components/document/nodes/ContractBlockNode.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { - DecoratorNode, - DOMExportOutput, - LexicalNode, - NodeKey, - SerializedLexicalNode, - Spread, -} from 'lexical'; -import React from 'react'; -import { ContractBlockComponent } from './ContractBlockComponent'; - -export type SerializedContractBlockNode = Spread< - { - contractId: string; - contractName: string; - }, - SerializedLexicalNode ->; - -export class ContractBlockNode extends DecoratorNode { - __contractId: string; - __contractName: string; - - static getType(): string { - return 'contract-block'; - } - - static clone(node: ContractBlockNode): ContractBlockNode { - return new ContractBlockNode(node.__contractId, node.__contractName, node.__key); - } - - constructor(contractId: string, contractName: string, key?: NodeKey) { - super(key); - this.__contractId = contractId; - this.__contractName = contractName; - } - - createDOM(): HTMLElement { - const div = document.createElement('div'); - div.className = 'contract-block-wrapper'; - return div; - } - - updateDOM(): boolean { - return false; - } - - decorate(): JSX.Element { - return ( - - ); - } - - exportJSON(): SerializedContractBlockNode { - return { - ...super.exportJSON(), - type: 'contract-block', - contractId: this.__contractId, - contractName: this.__contractName, - version: 1, - }; - } - - static importJSON(serializedNode: SerializedContractBlockNode): ContractBlockNode { - return $createContractBlockNode( - serializedNode.contractId, - serializedNode.contractName - ); - } - - isInline(): boolean { - return false; - } - - canInsertTextBefore(): boolean { - return false; - } - - canInsertTextAfter(): boolean { - return false; - } - - exportDOM(): DOMExportOutput { - const element = document.createElement('div'); - element.className = 'contract-block-wrapper'; - element.setAttribute('data-contract-id', this.__contractId); - element.textContent = `[Contract: ${this.__contractName}]`; - return { element }; - } -} - -export function $createContractBlockNode( - contractId: string, - contractName: string -): ContractBlockNode { - return new ContractBlockNode(contractId, contractName); -} - -export function $isContractBlockNode( - node: LexicalNode | null | undefined, -): node is ContractBlockNode { - return node instanceof ContractBlockNode; -} diff --git a/frontend/src/components/document/nodes/ContractLogFeed.css b/frontend/src/components/document/nodes/ContractLogFeed.css deleted file mode 100644 index b5dd15d..0000000 --- a/frontend/src/components/document/nodes/ContractLogFeed.css +++ /dev/null @@ -1,346 +0,0 @@ -/* ============================================ - Contract Log Feed - ============================================ */ - -.contract-log-feed { - display: flex; - flex-direction: column; - background: #1a1d23; - border: 1px solid #2d3039; - border-radius: 8px; - overflow: hidden; - margin-top: 0.5rem; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; - font-size: 13px; - max-height: 420px; - animation: logFeedSlideIn 0.25s ease-out; -} - -@keyframes logFeedSlideIn { - from { - opacity: 0; - transform: translateY(-6px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -/* ---- Header ---- */ -.contract-log-feed-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 0.5rem 0.75rem; - background: #22252b; - border-bottom: 1px solid #2d3039; -} - -.contract-log-feed-title { - display: flex; - align-items: center; - gap: 0.5rem; -} - -.contract-log-feed-name { - font-weight: 600; - font-size: 0.82rem; - color: #e5e7eb; -} - -.contract-log-feed-status { - font-size: 0.65rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.04em; - padding: 0.1rem 0.4rem; - border-radius: 8px; -} - -.contract-log-feed-status--running, -.contract-log-feed-status--starting { - background: rgba(245, 158, 11, 0.2); - color: #fbbf24; - animation: statusPulse 2s ease-in-out infinite; -} - -.contract-log-feed-status--completed { - background: rgba(16, 185, 129, 0.2); - color: #34d399; -} - -.contract-log-feed-status--failed { - background: rgba(239, 68, 68, 0.2); - color: #f87171; -} - -.contract-log-feed-status--pending, -.contract-log-feed-status--ready { - background: rgba(107, 114, 128, 0.2); - color: #9ca3af; -} - -.contract-log-feed-close { - background: none; - border: none; - color: #6b7280; - font-size: 1.1rem; - cursor: pointer; - padding: 0 0.25rem; - line-height: 1; - border-radius: 3px; - transition: color 0.15s, background 0.15s; -} - -.contract-log-feed-close:hover { - color: #e5e7eb; - background: rgba(255, 255, 255, 0.08); -} - -/* ---- Log Content ---- */ -.contract-log-feed-content { - flex: 1; - overflow-y: auto; - padding: 0.5rem 0.75rem; - min-height: 80px; - max-height: 240px; - scrollbar-width: thin; - scrollbar-color: #3a3f4b transparent; -} - -.contract-log-feed-content::-webkit-scrollbar { - width: 5px; -} - -.contract-log-feed-content::-webkit-scrollbar-thumb { - background: #3a3f4b; - border-radius: 3px; -} - -.contract-log-feed-empty { - color: #6b7280; - font-size: 0.82rem; - font-style: italic; - text-align: center; - padding: 1.5rem 0; -} - -/* ---- Log Entry ---- */ -.contract-log-entry { - display: flex; - gap: 0.5rem; - padding: 0.2rem 0; - line-height: 1.5; - animation: entryFadeIn 0.2s ease-out; -} - -@keyframes entryFadeIn { - from { opacity: 0; } - to { opacity: 1; } -} - -.contract-log-entry-time { - flex-shrink: 0; - font-size: 0.7rem; - color: #4b5563; - font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; - line-height: 1.65; -} - -.contract-log-entry-text { - color: #d1d5db; - white-space: pre-wrap; - word-break: break-word; - font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; - font-size: 0.78rem; -} - -.contract-log-entry--user .contract-log-entry-text { - color: #93c5fd; -} - -.contract-log-entry--user::before { - content: '>'; - color: #3b82f6; - font-weight: 700; - font-family: monospace; - flex-shrink: 0; - line-height: 1.5; -} - -.contract-log-entry--system .contract-log-entry-text { - color: #fbbf24; - font-style: italic; -} - -/* ---- Error ---- */ -.contract-log-feed-error { - padding: 0.4rem 0.75rem; - background: rgba(239, 68, 68, 0.12); - border-top: 1px solid rgba(239, 68, 68, 0.25); - color: #f87171; - font-size: 0.78rem; -} - -/* ---- Interaction Bar ---- */ -.contract-interaction-bar { - border-top: 1px solid #2d3039; - padding: 0.5rem 0.75rem; - background: #22252b; -} - -.contract-interaction-bar--disabled { - display: flex; - align-items: center; - justify-content: center; - padding: 0.6rem 0.75rem; -} - -.contract-interaction-disabled-text { - color: #6b7280; - font-size: 0.78rem; - font-style: italic; -} - -.contract-interaction-message-row { - display: flex; - align-items: flex-end; - gap: 0.4rem; - position: relative; -} - -.contract-message-input { - flex: 1; - background: #1a1d23; - border: 1px solid #3a3f4b; - border-radius: 6px; - color: #e5e7eb; - padding: 0.4rem 0.6rem; - font-size: 0.82rem; - font-family: inherit; - resize: none; - min-height: 32px; - max-height: 80px; - line-height: 1.4; - outline: none; - transition: border-color 0.15s; -} - -.contract-message-input::placeholder { - color: #4b5563; - font-size: 0.78rem; -} - -.contract-message-input:focus { - border-color: #3b82f6; -} - -.contract-message-input:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.contract-send-btn { - flex-shrink: 0; - background: #3b82f6; - color: #fff; - border: none; - border-radius: 6px; - padding: 0.4rem 0.85rem; - font-size: 0.8rem; - font-weight: 500; - cursor: pointer; - transition: background 0.15s, opacity 0.15s; - min-height: 32px; -} - -.contract-send-btn:hover:not(:disabled) { - background: #2563eb; -} - -.contract-send-btn:disabled { - opacity: 0.4; - cursor: not-allowed; -} - -.contract-sent-indicator { - position: absolute; - right: 0; - top: -1.4rem; - font-size: 0.7rem; - color: #34d399; - font-weight: 500; - animation: sentFlash 1.5s ease-out forwards; -} - -@keyframes sentFlash { - 0% { - opacity: 1; - transform: translateY(0); - } - 70% { - opacity: 1; - } - 100% { - opacity: 0; - transform: translateY(-4px); - } -} - -/* ---- Actions Row ---- */ -.contract-interaction-actions-row { - display: flex; - align-items: center; - gap: 0.5rem; - margin-top: 0.4rem; -} - -.contract-interrupt-btn { - background: transparent; - color: #ef4444; - border: 1px solid rgba(239, 68, 68, 0.3); - border-radius: 6px; - padding: 0.3rem 0.7rem; - font-size: 0.75rem; - font-weight: 500; - cursor: pointer; - transition: background 0.15s, border-color 0.15s, color 0.15s; -} - -.contract-interrupt-btn:hover:not(:disabled) { - background: rgba(239, 68, 68, 0.1); - border-color: rgba(239, 68, 68, 0.5); -} - -.contract-interrupt-btn--confirm { - background: rgba(239, 68, 68, 0.15); - border-color: #ef4444; - color: #f87171; - animation: confirmPulse 0.8s ease-in-out infinite; -} - -@keyframes confirmPulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.7; } -} - -.contract-interrupt-btn:disabled { - opacity: 0.4; - cursor: not-allowed; -} - -/* ---- Responsive ---- */ -@media (max-width: 640px) { - .contract-log-feed { - max-height: 360px; - } - - .contract-log-feed-content { - max-height: 180px; - } - - .contract-message-input::placeholder { - font-size: 0.72rem; - } -} diff --git a/frontend/src/components/document/nodes/ContractLogFeed.tsx b/frontend/src/components/document/nodes/ContractLogFeed.tsx deleted file mode 100644 index 79af91c..0000000 --- a/frontend/src/components/document/nodes/ContractLogFeed.tsx +++ /dev/null @@ -1,225 +0,0 @@ -import React, { useState, useRef, useEffect, useCallback } from 'react'; -import { - sendContractMessage, - interruptContract, - getContractOutput, -} from '../../../services/directiveApi'; -import './ContractLogFeed.css'; - -interface ContractLogFeedProps { - taskId: string; - contractName: string; - status: string; - onClose?: () => void; -} - -interface LogEntry { - id: string; - text: string; - type: 'output' | 'user' | 'system'; - timestamp: Date; -} - -const INTERACTIVE_STATUSES = ['running', 'starting']; - -export function ContractLogFeed({ taskId, contractName, status, onClose }: ContractLogFeedProps) { - const [logEntries, setLogEntries] = useState([]); - const [message, setMessage] = useState(''); - const [sending, setSending] = useState(false); - const [sentIndicator, setSentIndicator] = useState(false); - const [interruptConfirm, setInterruptConfirm] = useState(false); - const [interrupting, setInterrupting] = useState(false); - const [error, setError] = useState(null); - const logEndRef = useRef(null); - const textareaRef = useRef(null); - const pollRef = useRef | null>(null); - const lastOutputRef = useRef(''); - const entryIdRef = useRef(0); - - const isInteractive = INTERACTIVE_STATUSES.includes(status.toLowerCase()); - - const addLogEntry = useCallback((text: string, type: LogEntry['type']) => { - entryIdRef.current += 1; - setLogEntries(prev => [ - ...prev, - { id: `entry-${entryIdRef.current}`, text, type, timestamp: new Date() }, - ]); - }, []); - - // Poll for contract output - const fetchOutput = useCallback(async () => { - if (!taskId) return; - try { - const data = await getContractOutput(taskId); - const output = data.output || ''; - if (output && output !== lastOutputRef.current) { - // Find new content - const newContent = output.startsWith(lastOutputRef.current) - ? output.slice(lastOutputRef.current.length).trim() - : output.trim(); - lastOutputRef.current = output; - if (newContent) { - addLogEntry(newContent, 'output'); - } - } - } catch { - // Silently ignore fetch errors for output polling - } - }, [taskId, addLogEntry]); - - useEffect(() => { - fetchOutput(); - pollRef.current = setInterval(fetchOutput, 3000); - return () => { - if (pollRef.current) clearInterval(pollRef.current); - }; - }, [fetchOutput]); - - // Auto-scroll to bottom on new entries - useEffect(() => { - logEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - }, [logEntries]); - - // Reset interrupt confirm after timeout - useEffect(() => { - if (!interruptConfirm) return; - const timer = setTimeout(() => setInterruptConfirm(false), 3000); - return () => clearTimeout(timer); - }, [interruptConfirm]); - - const handleSendMessage = async () => { - const trimmed = message.trim(); - if (!trimmed || sending || !isInteractive) return; - - setSending(true); - setError(null); - try { - await sendContractMessage(taskId, trimmed); - addLogEntry(trimmed, 'user'); - setMessage(''); - setSentIndicator(true); - setTimeout(() => setSentIndicator(false), 1500); - } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to send message'); - } finally { - setSending(false); - } - }; - - const handleInterrupt = async () => { - if (!interruptConfirm) { - setInterruptConfirm(true); - return; - } - - setInterrupting(true); - setError(null); - setInterruptConfirm(false); - try { - await interruptContract(taskId); - addLogEntry('Contract interrupted by user', 'system'); - } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to interrupt contract'); - } finally { - setInterrupting(false); - } - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - handleSendMessage(); - } - }; - - const statusLower = status.toLowerCase(); - - return ( -
-
-
- {contractName} - - {status} - -
- {onClose && ( - - )} -
- -
- {logEntries.length === 0 && ( -
- {isInteractive ? 'Waiting for output...' : 'No output available.'} -
- )} - {logEntries.map(entry => ( -
- - {entry.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })} - - {entry.text} -
- ))} -
-
- - {error && ( -
{error}
- )} - - {isInteractive && ( -
-
-