diff options
Diffstat (limited to 'frontend')
| -rw-r--r-- | frontend/index.html | 2 | ||||
| -rw-r--r-- | frontend/package-lock.json | 305 | ||||
| -rw-r--r-- | frontend/package.json | 5 | ||||
| -rw-r--r-- | frontend/src/components/LandingPage.tsx | 262 | ||||
| -rw-r--r-- | frontend/src/components/VNInterface.tsx | 28 | ||||
| -rw-r--r-- | frontend/src/components/document/index.ts | 14 | ||||
| -rw-r--r-- | frontend/src/components/document/nodes/StepLogFeed.tsx | 277 | ||||
| -rw-r--r-- | frontend/src/components/document/nodes/StepsDiagramComponent.tsx | 239 | ||||
| -rw-r--r-- | frontend/src/main.tsx | 10 | ||||
| -rw-r--r-- | frontend/src/services/directiveApi.ts | 185 | ||||
| -rw-r--r-- | frontend/src/stores/index.ts | 37 | ||||
| -rw-r--r-- | frontend/src/styles/mobile.css | 108 | ||||
| -rw-r--r-- | frontend/src/styles/pc98.css | 323 | ||||
| -rw-r--r-- | frontend/tsconfig.tsbuildinfo | 2 |
14 files changed, 180 insertions, 1617 deletions
diff --git a/frontend/index.html b/frontend/index.html index 0014d94..095310b 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -6,7 +6,7 @@ <title>soryu.co</title> <link rel="icon" type="image/png" href="/logo/crane-logo-transparent.png" /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> - <link href="https://fonts.googleapis.com/css2?family=DotGothic16&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet"> + <link href="https://fonts.googleapis.com/css2?family=DotGothic16&display=swap" rel="stylesheet"> </head> <body> <div id="root"></div> diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 771bdff..230ed07 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,13 +8,8 @@ "name": "pc98-vn", "version": "0.0.1", "dependencies": { - "@lexical/link": "^0.44.0", - "@lexical/list": "^0.44.0", - "@lexical/react": "^0.44.0", - "@lexical/rich-text": "^0.44.0", "@nanostores/react": "^1.0.0", "@types/three": "^0.180.0", - "lexical": "^0.21.0", "nanostores": "^1.0.1", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -71,6 +66,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "dev": true, + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -261,14 +257,6 @@ "@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", @@ -722,241 +710,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@lexical/clipboard": { - "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/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/utils": "0.21.0", - "lexical": "0.21.0", - "prismjs": "^1.27.0" - } - }, - "node_modules/@lexical/devtools-core": { - "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", - "react-dom": ">=17.x" - } - }, - "node_modules/@lexical/dragon": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@lexical/dragon/-/dragon-0.21.0.tgz", - "integrity": "sha512-ahTCaOtRFNauEzplN1qVuPjyGAlDd+XcVM5FQCdxVh/1DvqmBxEJRVuCBqatzUUVb89jRBekYUcEdnY9iNjvEQ==", - "dependencies": { - "lexical": "0.21.0" - } - }, - "node_modules/@lexical/hashtag": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@lexical/hashtag/-/hashtag-0.21.0.tgz", - "integrity": "sha512-O4dxcZNq1Xm45HLoRifbGAYvQkg3qLoBc6ibmHnDqZL5mQDsufnH6QEKWfgDtrvp9++3iqsSC+TE7VzWIvA7ww==", - "dependencies": { - "@lexical/utils": "0.21.0", - "lexical": "0.21.0" - } - }, - "node_modules/@lexical/history": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@lexical/history/-/history-0.21.0.tgz", - "integrity": "sha512-Sv2sici2NnAfHYHYRSjjS139MDT8fHP6PlYM2hVr+17dOg7/fJl22VBLRgQ7/+jLtAPxQjID69jvaMlOvt4Oog==", - "dependencies": { - "@lexical/utils": "0.21.0", - "lexical": "0.21.0" - } - }, - "node_modules/@lexical/html": { - "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/selection": "0.21.0", - "@lexical/utils": "0.21.0", - "lexical": "0.21.0" - } - }, - "node_modules/@lexical/link": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@lexical/link/-/link-0.21.0.tgz", - "integrity": "sha512-/coktIyRXg8rXz/7uxXsSEfSQYxPIx8CmignAXWYhcyYtCWA0fD2mhEhWwVvHH9ofNzvidclRPYKUnrmUm3z3Q==", - "dependencies": { - "@lexical/utils": "0.21.0", - "lexical": "0.21.0" - } - }, - "node_modules/@lexical/list": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@lexical/list/-/list-0.21.0.tgz", - "integrity": "sha512-WItGlwwNJCS8b6SO1QPKzArShmD+OXQkLbhBcAh+EfpnkvmCW5T5LqY+OfIRmEN1dhDOnwqCY7mXkivWO8o5tw==", - "dependencies": { - "@lexical/utils": "0.21.0", - "lexical": "0.21.0" - } - }, - "node_modules/@lexical/mark": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@lexical/mark/-/mark-0.21.0.tgz", - "integrity": "sha512-2x/LoHDYPOkZbKHz4qLFWsPywjRv9KggTOtmRazmaNRUG0FpkImJwUbbaKjWQXeESVGpzfL3qNFSAmCWthsc4g==", - "dependencies": { - "@lexical/utils": "0.21.0", - "lexical": "0.21.0" - } - }, - "node_modules/@lexical/markdown": { - "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": "0.21.0" - } - }, - "node_modules/@lexical/overflow": { - "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.21.0" - } - }, - "node_modules/@lexical/plain-text": { - "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.21.0", - "@lexical/selection": "0.21.0", - "@lexical/utils": "0.21.0", - "lexical": "0.21.0" - } - }, - "node_modules/@lexical/react": { - "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" - } - }, - "node_modules/@lexical/rich-text": { - "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.21.0", - "@lexical/selection": "0.21.0", - "@lexical/utils": "0.21.0", - "lexical": "0.21.0" - } - }, - "node_modules/@lexical/selection": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@lexical/selection/-/selection-0.21.0.tgz", - "integrity": "sha512-4u53bc8zlPPF0rnHjsGQExQ1St8NafsDd70/t1FMw7yvoMtUsKdH7+ap00esLkJOMv45unJD7UOzKRqU1X0sEA==", - "dependencies": { - "lexical": "0.21.0" - } - }, - "node_modules/@lexical/table": { - "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.21.0", - "@lexical/utils": "0.21.0", - "lexical": "0.21.0" - } - }, - "node_modules/@lexical/text": { - "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.21.0" - } - }, - "node_modules/@lexical/utils": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@lexical/utils/-/utils-0.21.0.tgz", - "integrity": "sha512-YzsNOAiLkCy6R3DuP18gtseDrzgx+30lFyqRvp5M7mckeYgQElwdfG5biNFDLv7BM9GjSzgU5Cunjycsx6Sjqg==", - "dependencies": { - "@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.21.0", - "resolved": "https://registry.npmjs.org/@lexical/yjs/-/yjs-0.21.0.tgz", - "integrity": "sha512-AtPhC3pJ92CHz3dWoniSky7+MSK2WSd0xijc76I2qbTeXyeuFfYyhR6gWMg4knuY9Wz3vo9/+dXGdbQIPD8efw==", - "dependencies": { - "@lexical/offset": "0.21.0", - "@lexical/selection": "0.21.0", - "lexical": "0.21.0" - }, - "peerDependencies": { - "yjs": ">=13.5.22" - } - }, "node_modules/@nanostores/react": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@nanostores/react/-/react-1.0.0.tgz", @@ -1307,6 +1060,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz", "integrity": "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==", "dev": true, + "peer": true, "dependencies": { "undici-types": "~7.10.0" } @@ -1322,6 +1076,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.24.tgz", "integrity": "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==", "dev": true, + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -1404,6 +1159,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001737", "electron-to-chromium": "^1.5.211", @@ -1576,32 +1332,6 @@ "node": ">=6" } }, - "node_modules/lexical": { - "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", - "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.117.tgz", - "integrity": "sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==", - "peer": true, - "dependencies": { - "isomorphic.js": "^0.2.4" - }, - "bin": { - "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js", - "0gentesthtml": "bin/gentesthtml.js", - "0serve": "bin/0serve.js" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "type": "GitHub Sponsors ❤", - "url": "https://github.com/sponsors/dmonad" - } - }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -1661,6 +1391,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "engines": { "node": "^20.0.0 || >=22.0.0" } @@ -1705,18 +1436,11 @@ "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", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -1728,6 +1452,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -1736,21 +1461,6 @@ "react": "^18.3.1" } }, - "node_modules/react-error-boundary": { - "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": ">=16.13.1" - } - }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -1916,6 +1626,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", "dev": true, + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", diff --git a/frontend/package.json b/frontend/package.json index 1ef66c3..197c3d8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,13 +9,8 @@ "preview": "vite preview" }, "dependencies": { - "@lexical/link": "^0.44.0", - "@lexical/list": "^0.44.0", - "@lexical/react": "^0.44.0", - "@lexical/rich-text": "^0.44.0", "@nanostores/react": "^1.0.0", "@types/three": "^0.180.0", - "lexical": "^0.21.0", "nanostores": "^1.0.1", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/frontend/src/components/LandingPage.tsx b/frontend/src/components/LandingPage.tsx index e7579b5..f126d6f 100644 --- a/frontend/src/components/LandingPage.tsx +++ b/frontend/src/components/LandingPage.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react' +import React, { useState, useEffect, useRef } from 'react' import { LoadingScreen } from './LoadingScreen' import { HeartLogo } from './HeartLogo' @@ -9,7 +9,12 @@ interface LandingPageProps { export function LandingPage({ onLogin }: LandingPageProps) { const [loading, setLoading] = useState(false) const [showLanding, setShowLanding] = useState(false) + const [isStandby, setIsStandby] = useState(true) // false = LIVE, true = STDBY + const [velocity, setVelocity] = useState(0) + const [energy, setEnergy] = useState(0) + const [ramped, setRamped] = useState(false) const [pendingAction, setPendingAction] = useState<null | 'makimaRedirect'>(null) + const [activePanel, setActivePanel] = useState<null | 'mission' | 'makima'>(null) // Fade-in landing page content after mount useEffect(() => { @@ -17,6 +22,62 @@ export function LandingPage({ onLogin }: LandingPageProps) { return () => clearTimeout(timer) }, []) + // Ramp up stats, then keep them fluctuating near max + useEffect(() => { + const VELOCITY_MAX = 603 + const ENERGY_MAX = 32 + + let rampInterval: number | undefined + let fluctuateInterval: number | undefined + + // Ramp-up for ~2 seconds + const rampDurationMs = 2000 + const tickMs = 30 + const vStep = VELOCITY_MAX / (rampDurationMs / tickMs) + const eStep = ENERGY_MAX / (rampDurationMs / tickMs) + + rampInterval = window.setInterval(() => { + setVelocity((v) => { + const next = v + vStep + return next >= VELOCITY_MAX ? VELOCITY_MAX : next + }) + setEnergy((e) => { + const next = e + eStep + return next >= ENERGY_MAX ? ENERGY_MAX : next + }) + }, tickMs) + + const stopRamp = window.setTimeout(() => { + if (rampInterval) window.clearInterval(rampInterval) + setVelocity(VELOCITY_MAX) + setEnergy(ENERGY_MAX) + setRamped(true) + + // Fluctuate near the top + fluctuateInterval = window.setInterval(() => { + setVelocity((v) => { + const min = VELOCITY_MAX - 18 + const max = VELOCITY_MAX + const delta = (Math.random() - 0.5) * 6 // ±3 + const next = Math.max(min, Math.min(max, v + delta)) + return next + }) + setEnergy((e) => { + const min = ENERGY_MAX - 2 + const max = ENERGY_MAX + const delta = (Math.random() - 0.5) * 0.25 // ±0.125 + const next = Math.max(min, Math.min(max, e + delta)) + return next + }) + }, 220) + }, rampDurationMs + 60) + + return () => { + if (rampInterval) window.clearInterval(rampInterval) + if (fluctuateInterval) window.clearInterval(fluctuateInterval) + } + }, []) + const handleLoadingComplete = () => { if (pendingAction === 'makimaRedirect') { window.location.assign('https://makima.jp') @@ -30,129 +91,144 @@ export function LandingPage({ onLogin }: LandingPageProps) { setLoading(true) } - const scrollTo = (id: string) => { - document.getElementById(id)?.scrollIntoView({ behavior: 'smooth' }) + const handleMission = () => { + setActivePanel((mode) => (mode === 'mission' ? null : 'mission')) + } + + const handleMakimaPanel = () => { + setActivePanel((mode) => (mode === 'makima' ? null : 'makima')) } return ( <div> {loading && <LoadingScreen onComplete={handleLoadingComplete} />} - {/* Professional floating header */} {!loading && ( - <div className={`pro-header ${showLanding ? 'fade-in' : 'hidden'}`}> - <div className="pro-header-content"> - <div className="pro-header-left"> + <div className={`floating-header ${showLanding ? 'fade-in' : 'hidden'}`}> + <div className="header-content"> + <div className="brand"> <img src="/logo/crane-logo-transparent.png" alt="Soryu" - height={36} - className="pro-crane-logo" + height={40} + className="brand-mark" onError={(e) => { const img = e.currentTarget as HTMLImageElement img.onerror = null img.src = '/logo/crane-logo.png' }} /> - <span className="pro-company-name">SORYU</span> </div> - - <div className="pro-header-center"> + <div className="header-center"> <HeartLogo size="header-no-rays" className="header-heart" /> </div> - <nav className="pro-header-nav"> - <button className="pro-nav-link" onClick={() => scrollTo('pro-mission')}> - Mission - </button> - <button className="pro-nav-link" onClick={() => scrollTo('pro-makima')}> - Makima - </button> - <button className="pro-nav-link pro-nav-login" onClick={handleLogin}> - Login - </button> - </nav> + <div className="system-info"> + <div className="info-item"> + <span className="info-label">System:</span> + <span + className="info-value live-status clickable" + onClick={() => setIsStandby(!isStandby)} + title="Click to toggle between LIVE and STANDBY" + > + <span className={`status-dot ${isStandby ? 'standby' : 'live'}`}></span> + {isStandby ? 'STDBY' : 'LIVE'} + </span> + </div> + <div className="info-item"> + <span className="info-label">Version:</span> + <span className="info-value">v1.0.0</span> + </div> + </div> </div> </div> )} - {/* Main professional landing layout */} - <div className={`pro-landing ${showLanding && !loading ? 'fade-in' : 'hidden'}`}> - - {/* Hero section */} - <section className="pro-hero"> - <div className="pro-hero-inner"> - <div className="pro-hero-tagline-jp">そりゅう</div> - <h1 className="pro-hero-headline"> - Real‑Time Systems for<br />Mission‑Critical Observability - </h1> - <p className="pro-hero-sub"> - Low‑latency streaming infrastructure that turns live data into reliable, secure insight. - </p> - <div className="pro-hero-cta"> - <button className="pro-btn-primary" onClick={() => scrollTo('pro-mission')}> - Learn More - </button> - <button className="pro-btn-secondary" onClick={handleLogin}> - <span className="pro-btn-icon">▶</span> Launch Makima - </button> - </div> - </div> - </section> - - {/* Content grid: Mission + Makima cards */} - <section className="pro-content-grid"> - <div className="pro-card" id="pro-mission"> - <div className="pro-card-header"> - <h2 className="pro-card-title">Mission</h2> - <div className="pro-card-accent" /> + <div className={`modern-landing-page manga-style ${showLanding && !loading ? 'fade-in' : 'hidden'}`}> + {/* Background GIF fills the main body */} + <div className="background-only" aria-hidden="true"> + <img src="/background-animation.gif" alt="" className="background-gif" /> + </div> + + {/* Minimal overlay: masthead, issue badge, and CTA */} + <div className={`taisho-cover ${activePanel ? 'mission-mode' : ''}`}> + <div className="cover-backdrop" aria-hidden="true" /> + <div className="cover-content"> + {/* Masthead + Issue badge (kept) */} + <div className="masthead"> + <div className="masthead-vertical"> + <span className="jp">そりゅう</span> + <span className="en">SORYU</span> + </div> + <div className="issue-badge"><span className="led-heart" aria-hidden="true"></span>かはいい Vol.01</div> </div> - <div className="pro-card-body"> - <h3 className="pro-card-subtitle"> - Building real‑time systems for mission-critical observability and surveillance - </h3> - <p className="pro-card-text"> - We deliver low‑latency streaming & infrastructure that turns live data into - reliable, secure insight. Target selection, monitoring and full end to end observability - to make vital decisions where it matters most. - </p> + + {/* Hero area becomes Mission content when in mission mode */} + {activePanel === 'mission' ? ( + <div className="mission-screen" role="region" aria-label="Mission"> + <h1 className="mission-headline">Building real‑time systems for mission-critical observability and surveillance </h1> + <img src="/PC98Doukuusei.webp" alt="Mission montage" className="mission-image" /> + <p className="mission-paragraph"> + We deliver low‑latency streaming & infrastructure that turns live data into + reliable, secure insight. Target selection, monitoring and full end to end observability + to make vital decisions where it matters most. + </p> + </div> + ) : activePanel === 'makima' ? ( + <div className="mission-screen makima-screen" role="region" aria-label="Makima"> + <h1 className="mission-headline makima-headline">Mesh Orchestration Platform</h1> + <span className="makima-badge">Control System</span> + <img src="/logo/makima-logo.svg" alt="Makima mesh logo" className="mission-image makima-logo" /> + <p className="mission-paragraph"> + Makima is a control system for orchestrating distributed daemon meshes, + coordinating concurrent execution across distinct domains. + </p> + <p className="mission-paragraph"> + Unified command interface for spawning, monitoring, and directing + worker daemons. Real-time task coordination with overlay management. + </p> + </div> + ) : ( + <div className="hero" /> + )} + + {/* CTA row spanning full width: left Mission/MAKIMA, right Login */} + <div className="cta-area"> + <div className="cta-left"> + <button className="taisho-cta" onClick={handleMission}> + <span className="cta-text">{activePanel === 'mission' ? 'Close' : 'Mission'}</span> + </button> + <button className="taisho-cta" onClick={handleMakimaPanel}> + <span className="cta-text">{activePanel === 'makima' ? 'Close' : 'MAKIMA'}</span> + </button> + </div> + <div className="cta-right"> + <button className="taisho-cta" onClick={handleLogin}> + <span className="cta-icon">▶</span> + <span className="cta-text">Login</span> + </button> + </div> </div> </div> + </div> - <div className="pro-card" id="pro-makima"> - <div className="pro-card-header"> - <h2 className="pro-card-title">Makima</h2> - <span className="pro-card-badge">Control System</span> - <div className="pro-card-accent" /> - </div> - <div className="pro-card-body"> - <img - src="/logo/makima-logo.svg" - alt="Makima mesh logo" - className="pro-makima-logo" - /> - <h3 className="pro-card-subtitle">Mesh Orchestration Platform</h3> - <p className="pro-card-text"> - Makima is a control system for orchestrating distributed daemon meshes, - coordinating concurrent execution across distinct domains. - </p> - <p className="pro-card-text"> - Unified command interface for spawning, monitoring, and directing - worker daemons. Real-time task coordination with overlay management. - </p> + {/* Bottom stats: Velocity + Energy only (hidden in mission mode) */} + {!activePanel && ( + <div className="bottom-stats"> + <div className="rf-stats"> + <div className="rf-stat"> + <span className="label">Velocity</span> + <span className="value">{Math.round(velocity)} km/h</span> + </div> + <div className="rf-stat"> + <span className="label">Energy</span> + <span className="value">{energy.toFixed(1)} MJ</span> + </div> </div> </div> - </section> - - {/* Footer */} - <footer className="pro-footer"> - <div className="pro-footer-inner"> - <span className="pro-footer-brand">SORYU</span> - <span className="pro-footer-sep">—</span> - <span className="pro-footer-text">Real‑time systems & infrastructure</span> - </div> - </footer> + )} </div> + {/* MissionDrawer removed in favor of mission screen transformation */} </div> ) } diff --git a/frontend/src/components/VNInterface.tsx b/frontend/src/components/VNInterface.tsx index 0a77f39..318a9b9 100644 --- a/frontend/src/components/VNInterface.tsx +++ b/frontend/src/components/VNInterface.tsx @@ -9,11 +9,9 @@ import { showSettingsModalStore, isVisibleStore, yenBalanceStore, - documentEditorEnabledStore, toggleStandby, toggleShowChoices, - updateTime, - setDocumentEditorEnabled + updateTime } from '../stores' interface VNInterfaceProps { @@ -28,7 +26,6 @@ export function VNInterface({ onLogout }: VNInterfaceProps) { const showSettingsModal = useStore(showSettingsModalStore) const isVisible = useStore(isVisibleStore) const yenBalance = useStore(yenBalanceStore) - const documentEditorEnabled = useStore(documentEditorEnabledStore) // Fade in effect on mount useEffect(() => { @@ -116,14 +113,6 @@ export function VNInterface({ onLogout }: VNInterfaceProps) { <span className="info-value">Contracts</span> </Link> </div> - {documentEditorEnabled && ( - <div className="status-item"> - <Link to="/directives" style={{ color: '#66ccff', textDecoration: 'none', display: 'flex', alignItems: 'center', gap: '4px' }}> - <span className="info-label">Edit:</span> - <span className="info-value">Directives</span> - </Link> - </div> - )} </div> </div> </div> @@ -209,21 +198,6 @@ export function VNInterface({ onLogout }: VNInterfaceProps) { </div> </div> <div className="settings-section"> - <h3>Feature Flags</h3> - <div className="setting-item"> - <label> - <input - type="checkbox" - checked={documentEditorEnabled} - onChange={(e) => setDocumentEditorEnabled(e.target.checked)} - /> Document Editor (Directives) - </label> - <div style={{ fontSize: '0.8em', opacity: 0.7, marginTop: '4px' }}> - Enable the directive document editor interface - </div> - </div> - </div> - <div className="settings-section"> <h3>Audio</h3> <div className="setting-item"> <label>Master Volume</label> diff --git a/frontend/src/components/document/index.ts b/frontend/src/components/document/index.ts deleted file mode 100644 index 906c1dc..0000000 --- a/frontend/src/components/document/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -export { default as DocumentLayout } from './DocumentLayout' -export { default as DocumentEditor } from './DocumentEditor' -export { DirectiveFileTree } from './DirectiveFileTree' -export { default as DocumentSettings } from './DocumentSettings' - -// Lexical Nodes -export { StepsDiagramNode, $createStepsDiagramNode, $isStepsDiagramNode } from './nodes/StepsDiagramNode' -export { ContractBlockNode, $createContractBlockNode, $isContractBlockNode } from './nodes/ContractBlockNode' - -// Sub-components -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/StepLogFeed.tsx b/frontend/src/components/document/nodes/StepLogFeed.tsx deleted file mode 100644 index 2f2f553..0000000 --- a/frontend/src/components/document/nodes/StepLogFeed.tsx +++ /dev/null @@ -1,277 +0,0 @@ -import React, { useEffect, useRef, useState, useCallback } from 'react'; - -interface StepLogFeedProps { - taskId: string; - stepName: string; - stepStatus: string; - onCollapse: () => void; -} - -interface LogEntry { - timestamp: string; - content: string; - type: 'stdout' | 'stderr' | 'system' | 'user'; -} - -/** - * Live log feed for an expanded step row. - * Connects via WebSocket to stream task output and allows - * sending messages (comments) and interrupting the task. - */ -export function StepLogFeed({ taskId, stepName, stepStatus, onCollapse }: StepLogFeedProps) { - const [logs, setLogs] = useState<LogEntry[]>([]); - const [message, setMessage] = useState(''); - const [sending, setSending] = useState(false); - const [connected, setConnected] = useState(false); - const [error, setError] = useState<string | null>(null); - const logsEndRef = useRef<HTMLDivElement>(null); - const wsRef = useRef<WebSocket | null>(null); - const logContainerRef = useRef<HTMLDivElement>(null); - const inputRef = useRef<HTMLInputElement>(null); - - const isActive = ['running', 'starting'].includes(stepStatus.toLowerCase()); - - // Auto-scroll to bottom when new logs arrive - useEffect(() => { - logsEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - }, [logs]); - - // Connect to WebSocket for live streaming - useEffect(() => { - if (!taskId) return; - - const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - const wsUrl = `${protocol}//${window.location.host}/api/v1/mesh/tasks/subscribe`; - - let ws: WebSocket; - let reconnectTimer: ReturnType<typeof setTimeout>; - let shouldReconnect = true; - - function connect() { - try { - ws = new WebSocket(wsUrl); - wsRef.current = ws; - - ws.addEventListener('open', () => { - setConnected(true); - setError(null); - // Subscribe to this specific task - ws.send(JSON.stringify({ type: 'subscribe', taskId })); - }); - - ws.addEventListener('message', (evt) => { - try { - const data = JSON.parse(evt.data); - // Handle different message formats from the backend - if (data.taskId === taskId || data.task_id === taskId) { - const entry: LogEntry = { - timestamp: data.timestamp || new Date().toISOString(), - content: data.content || data.output || data.message || JSON.stringify(data), - type: data.type || data.stream || 'stdout', - }; - setLogs(prev => [...prev, entry]); - } - } catch { - // Non-JSON message, treat as raw log - setLogs(prev => [...prev, { - timestamp: new Date().toISOString(), - content: evt.data, - type: 'stdout', - }]); - } - }); - - ws.addEventListener('close', () => { - setConnected(false); - wsRef.current = null; - if (shouldReconnect && isActive) { - reconnectTimer = setTimeout(connect, 3000); - } - }); - - ws.addEventListener('error', () => { - setConnected(false); - setError('WebSocket connection failed'); - }); - } catch (err) { - setError('Failed to connect to log stream'); - } - } - - connect(); - - return () => { - shouldReconnect = false; - clearTimeout(reconnectTimer); - if (wsRef.current) { - wsRef.current.close(); - wsRef.current = null; - } - }; - }, [taskId, isActive]); - - // Keyboard shortcut: Escape to collapse - useEffect(() => { - const handler = (e: KeyboardEvent) => { - if (e.key === 'Escape') { - onCollapse(); - } - }; - document.addEventListener('keydown', handler); - return () => document.removeEventListener('keydown', handler); - }, [onCollapse]); - - // Send a message/comment to the task - const handleSendMessage = useCallback(async () => { - if (!message.trim() || !taskId || sending) return; - - setSending(true); - try { - const response = await fetch(`/api/v1/mesh/tasks/${taskId}/message`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ message: message.trim() }), - }); - - if (!response.ok) { - const body = await response.json().catch(() => ({ message: response.statusText })); - throw new Error(body.message || body.error || `HTTP ${response.status}`); - } - - // Add as a user message in the log - setLogs(prev => [...prev, { - timestamp: new Date().toISOString(), - content: message.trim(), - type: 'user', - }]); - setMessage(''); - inputRef.current?.focus(); - } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to send message'); - } finally { - setSending(false); - } - }, [message, taskId, sending]); - - const handleKeyDown = useCallback((e: React.KeyboardEvent) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - handleSendMessage(); - } - // Prevent Escape from bubbling when input is focused - if (e.key === 'Escape') { - e.stopPropagation(); - inputRef.current?.blur(); - } - }, [handleSendMessage]); - - // Interrupt the running task - const handleInterrupt = useCallback(async () => { - if (!taskId) return; - try { - // Send a special interrupt message - const response = await fetch(`/api/v1/mesh/tasks/${taskId}/message`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ message: '/interrupt' }), - }); - if (!response.ok) { - throw new Error(`HTTP ${response.status}`); - } - setLogs(prev => [...prev, { - timestamp: new Date().toISOString(), - content: 'Interrupt signal sent', - type: 'system', - }]); - } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to interrupt'); - } - }, [taskId]); - - const formatTimestamp = (ts: string) => { - try { - return new Date(ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); - } catch { - return ''; - } - }; - - return ( - <div className="step-log-feed"> - {/* Header */} - <div className="step-log-feed-header"> - <div className="step-log-feed-header-left"> - <span className="step-log-feed-title">{stepName} - Logs</span> - <span className={`step-log-feed-status ${connected ? 'connected' : 'disconnected'}`}> - {connected ? 'Live' : 'Disconnected'} - </span> - </div> - <div className="step-log-feed-header-right"> - {isActive && ( - <button - className="step-log-feed-interrupt-btn" - onClick={handleInterrupt} - title="Interrupt this contract" - > - ⏹ Interrupt - </button> - )} - <button - className="step-log-feed-collapse-btn" - onClick={onCollapse} - title="Collapse (Esc)" - > - ✕ - </button> - </div> - </div> - - {/* Log content */} - <div className="step-log-feed-content" ref={logContainerRef}> - {logs.length === 0 && !error && ( - <div className="step-log-feed-empty"> - {isActive - ? 'Waiting for log output...' - : 'No logs available for this step.'} - </div> - )} - - {error && ( - <div className="step-log-feed-error">{error}</div> - )} - - {logs.map((entry, idx) => ( - <div key={idx} className={`step-log-entry step-log-entry--${entry.type}`}> - <span className="step-log-entry-time">{formatTimestamp(entry.timestamp)}</span> - <span className="step-log-entry-content">{entry.content}</span> - </div> - ))} - <div ref={logsEndRef} /> - </div> - - {/* Message input (comment/interrupt controls) */} - {isActive && ( - <div className="step-log-feed-input"> - <input - ref={inputRef} - type="text" - className="step-log-feed-input-field" - placeholder="Send a message to this contract..." - value={message} - onChange={(e) => setMessage(e.target.value)} - onKeyDown={handleKeyDown} - disabled={sending} - /> - <button - className="step-log-feed-send-btn" - onClick={handleSendMessage} - disabled={!message.trim() || sending} - title="Send message (Enter)" - > - {sending ? '...' : '➤'} - </button> - </div> - )} - </div> - ); -} diff --git a/frontend/src/components/document/nodes/StepsDiagramComponent.tsx b/frontend/src/components/document/nodes/StepsDiagramComponent.tsx deleted file mode 100644 index ac1cb83..0000000 --- a/frontend/src/components/document/nodes/StepsDiagramComponent.tsx +++ /dev/null @@ -1,239 +0,0 @@ -import React, { useEffect, useState, useRef, useCallback } from 'react'; -import { getDirective, DirectiveStep, DirectiveWithSteps } from '../../../services/directiveApi'; -import { StepLogFeed } from './StepLogFeed'; -import './StepsDiagram.css'; - -interface StepsDiagramComponentProps { - directiveId: string; - onExpandContract?: (step: DirectiveStep) => void; -} - -type StepStatus = 'pending' | 'ready' | 'running' | 'completed' | 'failed' | 'skipped'; - -const STATUS_LABELS: Record<string, string> = { - pending: 'Queued', - ready: 'Ready', - running: 'Executing', - completed: 'Fulfilled', - failed: 'Failed', - skipped: 'Skipped', -}; - -function formatTime(dateStr: string): string { - if (!dateStr) return ''; - const d = new Date(dateStr); - return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); -} - -interface StepCardProps { - step: DirectiveStep; - isExpanded: boolean; - onToggleExpand: () => void; - onCollapse: () => void; -} - -function StepCard({ step, isExpanded, onToggleExpand, onCollapse }: StepCardProps) { - const status = (step.status || 'pending').toLowerCase() as StepStatus; - const hasTask = !!step.taskId || !!step.contractId; - const canExpand = hasTask && ['running', 'completed', 'failed'].includes(status); - - return ( - <div className={`steps-diagram-card steps-diagram-card--${status} ${isExpanded ? 'steps-diagram-card--expanded' : ''}`}> - <div - className={`steps-diagram-card-header ${canExpand ? 'steps-diagram-card-header--clickable' : ''}`} - onClick={canExpand ? onToggleExpand : undefined} - role={canExpand ? 'button' : undefined} - tabIndex={canExpand ? 0 : undefined} - onKeyDown={canExpand ? (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onToggleExpand(); } } : undefined} - > - <span className="steps-diagram-card-name">{step.name}</span> - <div className="steps-diagram-card-header-right"> - <span className={`steps-diagram-status-badge steps-diagram-status-badge--${status}`}> - {STATUS_LABELS[status] || status} - </span> - {canExpand && ( - <span className={`steps-diagram-expand-icon ${isExpanded ? 'expanded' : ''}`}> - ▶ - </span> - )} - </div> - </div> - {step.description && !isExpanded && ( - <p className="steps-diagram-card-desc">{step.description}</p> - )} - <div className="steps-diagram-card-footer"> - <span className="steps-diagram-card-index">#{step.orderIndex}</span> - {status === 'running' && ( - <span className="steps-diagram-card-progress">In progress...</span> - )} - {status === 'completed' && step.completedAt && ( - <span className="steps-diagram-card-time"> - Completed {formatTime(step.completedAt)} - </span> - )} - </div> - - {/* Expandable log feed */} - {isExpanded && hasTask && ( - <StepLogFeed - taskId={step.taskId || step.contractId} - stepName={step.name} - stepStatus={status} - onCollapse={onCollapse} - /> - )} - </div> - ); -} - -export function StepsDiagramComponent({ directiveId, onExpandContract }: StepsDiagramComponentProps) { - const [steps, setSteps] = useState<DirectiveStep[]>([]); - const [directiveStatus, setDirectiveStatus] = useState<string>(''); - const [loading, setLoading] = useState(true); - const [error, setError] = useState<string | null>(null); - const [expandedStepId, setExpandedStepId] = useState<string | null>(null); - const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null); - const prevStepCountRef = useRef(0); - - const fetchSteps = useCallback(async () => { - try { - const data: DirectiveWithSteps = await getDirective(directiveId); - setSteps(data.steps || []); - setDirectiveStatus(data.status || ''); - setError(null); - } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to load contracts'); - } finally { - setLoading(false); - } - }, [directiveId]); - - useEffect(() => { - fetchSteps(); - intervalRef.current = setInterval(fetchSteps, 5000); - return () => { - if (intervalRef.current) clearInterval(intervalRef.current); - }; - }, [fetchSteps]); - - // Track when new steps appear for animation - useEffect(() => { - prevStepCountRef.current = steps.length; - }, [steps.length]); - - // Keyboard shortcut: Escape to collapse expanded step - useEffect(() => { - const handler = (e: KeyboardEvent) => { - if (e.key === 'Escape' && expandedStepId) { - setExpandedStepId(null); - } - }; - document.addEventListener('keydown', handler); - return () => document.removeEventListener('keydown', handler); - }, [expandedStepId]); - - const toggleExpand = useCallback((stepId: string) => { - setExpandedStepId(prev => prev === stepId ? null : stepId); - }, []); - - const collapseExpanded = useCallback(() => { - setExpandedStepId(null); - }, []); - - const completedCount = steps.filter(s => s.status?.toLowerCase() === 'completed').length; - const totalCount = steps.length; - const isActive = ['active', 'running', 'planning'].includes(directiveStatus.toLowerCase()); - const isBuilding = isActive && steps.length === 0; - - // Group steps by orderIndex - const groupedSteps: Map<number, DirectiveStep[]> = new Map(); - const sortedSteps = [...steps].sort((a, b) => a.orderIndex - b.orderIndex); - for (const step of sortedSteps) { - const idx = step.orderIndex; - if (!groupedSteps.has(idx)) groupedSteps.set(idx, []); - groupedSteps.get(idx)!.push(step); - } - const orderGroups = Array.from(groupedSteps.entries()).sort((a, b) => a[0] - b[0]); - - if (loading) { - return ( - <div className="steps-diagram" contentEditable={false}> - <div className="steps-diagram-header"> - <span className="steps-diagram-header-title">Contract Steps</span> - <span className="steps-diagram-header-author">Authored by Makima</span> - </div> - <div className="steps-diagram-loading"> - <div className="steps-diagram-spinner" /> - <span>Loading contracts...</span> - </div> - </div> - ); - } - - if (error) { - return ( - <div className="steps-diagram" contentEditable={false}> - <div className="steps-diagram-header"> - <span className="steps-diagram-header-title">Contract Steps</span> - <span className="steps-diagram-header-author">Authored by Makima</span> - </div> - <div className="steps-diagram-error">Failed to load contracts: {error}</div> - </div> - ); - } - - return ( - <div className={`steps-diagram ${expandedStepId ? 'steps-diagram--has-expanded' : ''}`} contentEditable={false}> - <div className="steps-diagram-header"> - <div className="steps-diagram-header-left"> - <span className="steps-diagram-header-title">Contract Steps</span> - {totalCount > 0 && ( - <span className="steps-diagram-header-count"> - {completedCount}/{totalCount} fulfilled - </span> - )} - </div> - <span className="steps-diagram-header-author">Authored by Makima</span> - </div> - - {isBuilding && ( - <div className="steps-diagram-planning"> - <div className="steps-diagram-planning-dots"> - <span /><span /><span /> - </div> - <span>Makima is drafting contracts...</span> - </div> - )} - - {totalCount === 0 && !isBuilding && ( - <div className="steps-diagram-empty">No contract steps defined yet.</div> - )} - - {totalCount > 0 && ( - <div className="steps-diagram-dag"> - {orderGroups.map(([orderIndex, groupSteps], groupIdx) => ( - <React.Fragment key={orderIndex}> - {groupIdx > 0 && ( - <div className="steps-diagram-arrow"> - <div className="steps-diagram-arrow-line" /> - <div className="steps-diagram-arrow-head" /> - </div> - )} - <div className="steps-diagram-group"> - {groupSteps.map((step) => ( - <StepCard - key={step.id} - step={step} - isExpanded={expandedStepId === step.id} - onToggleExpand={() => toggleExpand(step.id)} - onCollapse={collapseExpanded} - /> - ))} - </div> - </React.Fragment> - ))} - </div> - )} - </div> - ); -} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 7688159..04b8cde 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -14,8 +14,6 @@ import './styles/mobile.css' // - /contracts - List all contracts // - /contracts/:id - View contract details with tabs (including Files tab) // - /contracts/:contractId/files/:fileId - View a specific file within contract context -// - /directives - Document editor layout (file tree sidebar) -// - /directives/:id - Document editor for a specific directive // // Note: Standalone file routes (/files, /files/:id) have been removed. // Files are now only accessible through their parent contract. @@ -38,14 +36,6 @@ const router = createBrowserRouter([ element: <FileDetail />, }, { - path: '/directives', - element: <DocumentLayout />, - }, - { - path: '/directives/:id', - element: <DocumentLayout />, - }, - { path: '/daemons', element: <DaemonList />, }, diff --git a/frontend/src/services/directiveApi.ts b/frontend/src/services/directiveApi.ts deleted file mode 100644 index fcb6636..0000000 --- a/frontend/src/services/directiveApi.ts +++ /dev/null @@ -1,185 +0,0 @@ -// API service for directive operations - -export interface DirectiveStepCounts { - pending: number - ready: number - running: number - completed: number - failed: number - skipped: number -} - -export interface DirectiveSummary { - id: string - title: string - goal: string - status: string - repositoryUrl: string - prUrl: string - prBranch: string - createdAt: string - updatedAt: string - goalUpdatedAt: string - stepCounts: DirectiveStepCounts - version?: number - pr_branch?: string | null -} - -export interface DirectiveStep { - id: string - directiveId: string - directive_id?: string - name: string - title?: string - description: string | null - taskPlan: string - dependsOn: string[] - status: string - contractId: string - /** @deprecated Use contractId instead */ - taskId: string - orderIndex: number - sort_order?: number - completedAt: string -} - -export interface DirectiveWithSteps extends DirectiveSummary { - steps: DirectiveStep[] - reconcileMode: string -} - -// Alias for compatibility with context-menu branch types -export type Directive = DirectiveSummary - -async function apiFetch(path: string, options?: RequestInit): Promise<Response> { - const response = await fetch(path, { - ...options, - headers: { - 'Content-Type': 'application/json', - ...options?.headers, - }, - }) - if (!response.ok) { - const body = await response.json().catch(() => ({ message: response.statusText })) - throw new Error(body.message ?? body.error ?? `API error ${response.status}: ${response.statusText}`) - } - return response -} - -export async function listDirectives(): Promise<DirectiveSummary[]> { - const response = await apiFetch('/api/v1/directives') - const data = await response.json() - return data.directives || [] -} - -export async function getDirective(id: string): Promise<DirectiveWithSteps> { - const response = await apiFetch(`/api/v1/directives/${id}`) - return response.json() -} - -export async function getDirectiveSteps(id: string): Promise<DirectiveStep[]> { - const response = await apiFetch(`/api/v1/directives/${id}/steps`) - return response.json() -} - -export async function updateGoal(id: string, goal: string): Promise<DirectiveWithSteps> { - const response = await apiFetch(`/api/v1/directives/${id}/goal`, { - method: 'PUT', - body: JSON.stringify({ goal }), - }) - return response.json() -} - -export async function updateDirective( - id: string, - updates: { title?: string; goal?: string; version?: number }, -): Promise<DirectiveWithSteps> { - const response = await apiFetch(`/api/v1/directives/${id}`, { - method: 'PUT', - body: JSON.stringify(updates), - }) - return response.json() -} - -export async function cleanupDirective(id: string): Promise<void> { - await apiFetch(`/api/v1/directives/${id}/cleanup`, { method: 'POST' }) -} - -export async function createPr(id: string): Promise<{ prUrl: string }> { - const response = await apiFetch(`/api/v1/directives/${id}/create-pr`, { method: 'POST' }) - return response.json() -} - -export async function pickUpOrders(id: string): Promise<void> { - await apiFetch(`/api/v1/directives/${id}/pick-up-orders`, { method: 'POST' }) -} - -export async function startDirective(id: string): Promise<DirectiveWithSteps> { - const response = await apiFetch(`/api/v1/directives/${id}/start`, { method: 'POST' }) - return response.json() -} - -export async function pauseDirective(id: string): Promise<DirectiveWithSteps> { - const response = await apiFetch(`/api/v1/directives/${id}/pause`, { method: 'POST' }) - return response.json() -} - -export async function getUserSetting(key: string): Promise<any> { - const response = await apiFetch(`/api/v1/user-settings/${key}`) - return response.json() -} - -export async function upsertUserSetting(key: string, value: any): Promise<void> { - await apiFetch('/api/v1/user-settings', { - method: 'PUT', - body: JSON.stringify({ key, value }), - }) -} - -// ---- Task control APIs ---- - -export async function sendTaskMessage(taskId: string, message: string): Promise<void> { - await apiFetch(`/api/v1/mesh/tasks/${taskId}/message`, { - method: 'POST', - body: JSON.stringify({ message }), - }) -} - -export async function stopTask(taskId: string): Promise<void> { - await apiFetch(`/api/v1/mesh/tasks/${taskId}/stop`, { - method: 'POST', - }) -} - -export async function continueTask(taskId: string): Promise<void> { - await apiFetch(`/api/v1/mesh/tasks/${taskId}/continue`, { - method: 'POST', - }) -} - -export async function startTask(taskId: string): Promise<void> { - await apiFetch(`/api/v1/mesh/tasks/${taskId}/start`, { - method: 'POST', - }) -} - -// ---- Contract interaction APIs ---- - -export async function sendContractMessage(taskId: string, message: string): Promise<void> { - await apiFetch(`/api/v1/mesh/tasks/${taskId}/message`, { - method: 'POST', - body: JSON.stringify({ message }), - }) -} - -export async function interruptContract(taskId: string): Promise<void> { - await apiFetch(`/api/v1/mesh/tasks/${taskId}/message`, { - method: 'POST', - body: JSON.stringify({ message: '/interrupt' }), - }) -} - -export async function getContractOutput(taskId: string): Promise<{ output: string }> { - const response = await apiFetch(`/api/v1/mesh/tasks/${taskId}/output`) - return response.json() -} diff --git a/frontend/src/stores/index.ts b/frontend/src/stores/index.ts index 1853f4f..58f461c 100644 --- a/frontend/src/stores/index.ts +++ b/frontend/src/stores/index.ts @@ -1,29 +1,5 @@ import { atom } from 'nanostores' import { ChatMessage, Choice } from '../types' -import { upsertUserSetting } from '../services/directiveApi' - -// Document UI feature flag -export const documentUiEnabledStore = atom<boolean>( - (() => { - try { - const saved = localStorage.getItem('document_ui_enabled') - return saved === 'true' - } catch { - return false - } - })() -) - -export const setDocumentUiEnabled = async (enabled: boolean) => { - documentUiEnabledStore.set(enabled) - localStorage.setItem('document_ui_enabled', enabled.toString()) - // Persist to backend (best-effort) - try { - await upsertUserSetting('document_ui_enabled', enabled) - } catch (err) { - console.error('Failed to persist document UI setting:', err) - } -} // Authentication state export const isLoggedInStore = atom<boolean>(false) @@ -53,14 +29,6 @@ export const nameStore = atom<string>('???') export const backgroundStore = atom<string>('/__gaogao__56242cbde8f18ac64522e410bad04e68_waifu2x_art_noise2.png') export const locationStore = atom<string>('Tokyo') export const configModalOpenStore = atom<boolean>(false) - -// Feature flags -export const documentEditorEnabledStore = atom<boolean>( - (() => { - const saved = localStorage.getItem('documentEditorEnabled') - return saved === 'true' - })() -) export const skipIntroStore = atom<boolean>( (() => { const saved = localStorage.getItem('skipIntro') @@ -118,11 +86,6 @@ export const setSkipIntro = (skip: boolean) => { localStorage.setItem('skipIntro', skip.toString()) } -export const setDocumentEditorEnabled = (enabled: boolean) => { - documentEditorEnabledStore.set(enabled) - localStorage.setItem('documentEditorEnabled', enabled.toString()) -} - export const setLoadingComplete = (complete: boolean) => { loadingCompleteStore.set(complete) if (complete) { diff --git a/frontend/src/styles/mobile.css b/frontend/src/styles/mobile.css index 7d1f282..c1c524d 100644 --- a/frontend/src/styles/mobile.css +++ b/frontend/src/styles/mobile.css @@ -51,112 +51,4 @@ .makima-inline-icon { width: 16px; height: 16px; margin-left: 6px; } .makima-list { gap: 4px; } .makima-list li { font-size: 12px; line-height: 1.45; } - - /* ================== Professional Landing Page – Mobile ================== */ - - /* Compact header */ - .pro-header-content { - padding: 0.45rem 1rem; - } - - .pro-company-name { - font-size: 0.85rem; - letter-spacing: 0.12em; - } - - .pro-crane-logo { - height: 28px; - } - - /* Hide center heart on small screens to save space */ - .pro-header-center { - display: none; - } - - /* Full-width nav buttons */ - .pro-header-nav { - gap: 0.15rem; - } - - .pro-nav-link { - font-size: 0.7rem; - padding: 0.3rem 0.5rem; - } - - .pro-nav-login { - margin-left: 0.25rem; - } - - /* Hero – reduce heading size and padding */ - .pro-hero { - min-height: 85vh; - padding: 5rem 1.25rem 3rem; - } - - .pro-hero-headline { - font-size: clamp(1.4rem, 5.5vw, 2rem); - } - - .pro-hero-sub { - font-size: 0.9rem; - margin-bottom: 2rem; - } - - .pro-hero-tagline-jp { - font-size: 0.75rem; - letter-spacing: 0.35em; - } - - .pro-hero-cta { - flex-direction: column; - align-items: stretch; - } - - .pro-btn-primary, - .pro-btn-secondary { - width: 100%; - text-align: center; - justify-content: center; - } - - /* Stack cards to single column */ - .pro-content-grid { - grid-template-columns: 1fr; - padding: 0 1rem 3rem; - gap: 1.5rem; - } - - .pro-card-header { - padding: 1rem 1.25rem 0.6rem; - } - - .pro-card-body { - padding: 0.6rem 1.25rem 1.25rem; - } - - .pro-card-title { - font-size: 1rem; - } - - .pro-card-subtitle { - font-size: 0.88rem; - } - - .pro-card-text { - font-size: 0.8rem; - } - - .pro-makima-logo { - width: 48px; - height: 48px; - } - - /* Footer compact */ - .pro-footer { - padding: 1.25rem 1rem; - } - - .pro-footer-inner { - font-size: 0.7rem; - } } diff --git a/frontend/src/styles/pc98.css b/frontend/src/styles/pc98.css index e591cdc..7ec0d1c 100644 --- a/frontend/src/styles/pc98.css +++ b/frontend/src/styles/pc98.css @@ -4680,326 +4680,3 @@ button:focus-visible { .capacity-fill.high { background: #ffcc66; } .capacity-fill.full { background: #ff4466; } .refresh-indicator { font-size: 11px; color: rgba(255, 255, 255, 0.3); margin-left: auto; } - -/* ================== Professional Landing Page ================== */ - -/* ── Header ── */ - -.pro-header { - position: fixed; - top: 0; - left: 0; - right: 0; - z-index: 100; - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - background: rgba(10, 14, 28, 0.85); - border-bottom: 1px solid rgba(0, 200, 255, 0.15); -} - -.pro-header-content { - max-width: 1100px; - margin: 0 auto; - display: flex; - align-items: center; - justify-content: space-between; - padding: 0.6rem 1.5rem; -} - -.pro-header-left { - display: flex; - align-items: center; - gap: 0.65rem; -} - -.pro-crane-logo { - display: block; -} - -.pro-company-name { - font-family: var(--font-family, 'IBM Plex Mono', 'MS Gothic', monospace); - font-size: 1rem; - font-weight: 600; - letter-spacing: 0.18em; - color: #e0e8f0; -} - -.pro-header-center { - display: flex; - align-items: center; - justify-content: center; -} - -.pro-header-nav { - display: flex; - align-items: center; - gap: 0.25rem; -} - -.pro-nav-link { - background: none; - border: 1px solid transparent; - color: #8fa8c8; - font-family: var(--font-family, 'IBM Plex Mono', 'MS Gothic', monospace); - font-size: 0.8rem; - letter-spacing: 0.08em; - padding: 0.35rem 0.75rem; - cursor: pointer; - border-radius: 3px; - transition: color 0.2s, border-color 0.2s; -} - -.pro-nav-link:hover { - color: #00c8ff; - border-color: rgba(0, 200, 255, 0.3); -} - -.pro-nav-login { - color: #00c8ff; - border-color: rgba(0, 200, 255, 0.35); - margin-left: 0.5rem; -} - -.pro-nav-login:hover { - background: rgba(0, 200, 255, 0.08); - border-color: #00c8ff; -} - -/* ── Landing Container ── */ - -.pro-landing { - min-height: 100vh; - background: #080c1a; - color: #c8d4e0; - font-family: var(--font-family, 'IBM Plex Mono', 'MS Gothic', monospace); - overflow-x: hidden; -} - -/* ── Hero ── */ - -.pro-hero { - display: flex; - align-items: center; - justify-content: center; - min-height: 100vh; - padding: 6rem 1.5rem 4rem; - text-align: center; - position: relative; -} - -.pro-hero::before { - content: ''; - position: absolute; - inset: 0; - background: - radial-gradient(ellipse 60% 50% at 50% 40%, rgba(0, 100, 180, 0.08) 0%, transparent 100%), - radial-gradient(ellipse 40% 30% at 50% 60%, rgba(0, 200, 255, 0.04) 0%, transparent 100%); - pointer-events: none; -} - -.pro-hero-inner { - position: relative; - max-width: 700px; -} - -.pro-hero-tagline-jp { - font-size: 0.85rem; - letter-spacing: 0.5em; - color: rgba(0, 200, 255, 0.45); - margin-bottom: 1.5rem; - text-transform: uppercase; -} - -.pro-hero-headline { - font-size: clamp(1.8rem, 4vw, 2.8rem); - font-weight: 600; - line-height: 1.25; - color: #e8f0fa; - margin: 0 0 1.25rem; - letter-spacing: -0.01em; -} - -.pro-hero-sub { - font-size: 1rem; - line-height: 1.7; - color: #8fa8c8; - margin: 0 auto 2.5rem; - max-width: 520px; -} - -.pro-hero-cta { - display: flex; - gap: 1rem; - justify-content: center; - flex-wrap: wrap; -} - -.pro-btn-primary, -.pro-btn-secondary { - font-family: var(--font-family, 'IBM Plex Mono', 'MS Gothic', monospace); - font-size: 0.85rem; - letter-spacing: 0.06em; - padding: 0.65rem 1.5rem; - border-radius: 3px; - cursor: pointer; - transition: all 0.2s; -} - -.pro-btn-primary { - background: rgba(0, 200, 255, 0.1); - color: #00c8ff; - border: 1px solid rgba(0, 200, 255, 0.35); -} - -.pro-btn-primary:hover { - background: rgba(0, 200, 255, 0.18); - border-color: #00c8ff; -} - -.pro-btn-secondary { - background: transparent; - color: #8fa8c8; - border: 1px solid rgba(140, 170, 200, 0.25); -} - -.pro-btn-secondary:hover { - color: #e0e8f0; - border-color: rgba(140, 170, 200, 0.5); -} - -.pro-btn-icon { - margin-right: 0.35rem; - font-size: 0.75rem; -} - -/* ── Content Grid ── */ - -.pro-content-grid { - max-width: 1100px; - margin: 0 auto; - padding: 0 1.5rem 5rem; - display: grid; - grid-template-columns: repeat(auto-fit, minmax(min(100%, 440px), 1fr)); - gap: 2rem; -} - -.pro-card { - background: rgba(12, 18, 36, 0.7); - border: 1px solid rgba(0, 200, 255, 0.12); - border-radius: 4px; - overflow: hidden; - scroll-margin-top: 5rem; - transition: border-color 0.3s ease, box-shadow 0.3s ease; -} - -.pro-card:hover { - border-color: rgba(0, 200, 255, 0.28); - box-shadow: 0 0 20px rgba(0, 200, 255, 0.06); -} - -.pro-card-header { - padding: 1.25rem 1.5rem 0.75rem; - display: flex; - align-items: center; - gap: 0.75rem; - flex-wrap: wrap; -} - -.pro-card-title { - font-size: 1.1rem; - font-weight: 600; - letter-spacing: 0.12em; - color: #e0e8f0; - margin: 0; - text-transform: uppercase; -} - -.pro-card-badge { - font-size: 0.65rem; - letter-spacing: 0.1em; - text-transform: uppercase; - padding: 0.2rem 0.5rem; - background: rgba(0, 200, 255, 0.1); - border: 1px solid rgba(0, 200, 255, 0.25); - border-radius: 2px; - color: #00c8ff; -} - -.pro-card-accent { - flex: 1; - height: 1px; - background: linear-gradient(90deg, rgba(0, 200, 255, 0.3), transparent); -} - -.pro-card-body { - padding: 0.75rem 1.5rem 1.5rem; -} - -.pro-card-subtitle { - font-size: 0.95rem; - font-weight: 500; - color: #c0d0e0; - margin: 0 0 0.75rem; - line-height: 1.5; -} - -.pro-card-text { - font-size: 0.85rem; - line-height: 1.7; - color: #7a90a8; - margin: 0 0 0.6rem; -} - -.pro-card-text:last-child { - margin-bottom: 0; -} - -.pro-makima-logo { - display: block; - width: 64px; - height: 64px; - margin-bottom: 1rem; - opacity: 0.75; -} - -/* ── Footer ── */ - -.pro-footer { - border-top: 1px solid rgba(0, 200, 255, 0.1); - padding: 1.5rem; - text-align: center; -} - -.pro-footer-inner { - font-size: 0.75rem; - color: #4a6080; - letter-spacing: 0.08em; -} - -.pro-footer-brand { - letter-spacing: 0.18em; - color: #5a7898; -} - -.pro-footer-sep { - margin: 0 0.5rem; - opacity: 0.4; -} - -.pro-footer-text { - color: #4a6080; -} - -/* ── Shared transitions ── */ - -.pro-landing.hidden, -.pro-header.hidden { - opacity: 0; - pointer-events: none; -} - -.pro-landing.fade-in, -.pro-header.fade-in { - opacity: 1; - transition: opacity 0.8s ease; -} diff --git a/frontend/tsconfig.tsbuildinfo b/frontend/tsconfig.tsbuildinfo index b4f8fee..79408dc 100644 --- a/frontend/tsconfig.tsbuildinfo +++ b/frontend/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/main.tsx","./src/types.ts","./src/components/bottombar.tsx","./src/components/choicemenu.tsx","./src/components/cityscapebackground.tsx","./src/components/configmodal.tsx","./src/components/contractdetail.tsx","./src/components/contractlist.tsx","./src/components/daemondetail.tsx","./src/components/daemonlist.tsx","./src/components/dialoguebox.tsx","./src/components/filedetail.tsx","./src/components/heartlogo.tsx","./src/components/landingpage.tsx","./src/components/loadingscreen.tsx","./src/components/origamidragonlogo.tsx","./src/components/topbar.tsx","./src/components/vnapp.tsx","./src/components/vninterface.tsx","./src/components/vnviewport.tsx","./src/components/document/autosaveplugin.tsx","./src/components/document/contextmenu.tsx","./src/components/document/directivefiletree.tsx","./src/components/document/documenteditor.tsx","./src/components/document/documentlayout.tsx","./src/components/document/documentsettings.tsx","./src/components/document/editortheme.ts","./src/components/document/toast.tsx","./src/components/document/index.ts","./src/components/document/nodes/contractblockcomponent.tsx","./src/components/document/nodes/contractblocknode.tsx","./src/components/document/nodes/contractlogfeed.tsx","./src/components/document/nodes/steplogfeed.tsx","./src/components/document/nodes/stepsdiagramcomponent.tsx","./src/components/document/nodes/stepsdiagramnode.tsx","./src/services/directiveapi.ts","./src/services/taskws.ts","./src/services/ws.ts","./src/stores/index.ts"],"version":"5.9.2"}
\ No newline at end of file +{"root":["./src/app.tsx","./src/main.tsx","./src/types.ts","./src/components/bottombar.tsx","./src/components/choicemenu.tsx","./src/components/cityscapebackground.tsx","./src/components/configmodal.tsx","./src/components/contractdetail.tsx","./src/components/contractlist.tsx","./src/components/dialoguebox.tsx","./src/components/filedetail.tsx","./src/components/heartlogo.tsx","./src/components/landingpage.tsx","./src/components/loadingscreen.tsx","./src/components/origamidragonlogo.tsx","./src/components/topbar.tsx","./src/components/vnapp.tsx","./src/components/vninterface.tsx","./src/components/vnviewport.tsx","./src/services/ws.ts","./src/stores/index.ts"],"version":"5.9.2"}
\ No newline at end of file |
