From 125927e0e8ef524fdc055230adbef5ef3e164b58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Mon, 20 Jul 2026 15:03:23 +0800 Subject: [PATCH] Ai game creator app home sidebar (#86) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ![shotmd-1784186268.jpg](/attachments/c828773c-4f1a-4654-b76d-c944b82ec023) ![shotmd-1784186877.jpg](/attachments/f1782ba3-245a-40c1-8caa-cbd7e4cce74b) ![shotmd-1784186948-compressed.webp](/attachments/f3d02ac3-db7a-439b-9a84-7aceefad9048) TODO: 补充说明书链接 Reviewed-on: http://genarrative-station/git/GenarrativeAI/Genarrative/pulls/86 Co-authored-by: 王德宇 Co-committed-by: 王德宇 --- apps/ai-game-creator-shell/package.json | 5 + .../src-tauri/Cargo.lock | 328 +++++- .../src-tauri/Cargo.toml | 3 +- .../src-tauri/capabilities/main.json | 14 + .../src-tauri/src/main.rs | 1 + apps/ai-game-creator-shell/src/App.tsx | 682 +++++------ .../src/services/clientApi.ts | 36 +- .../src/stores/useHomeDraftStore.ts | 44 - .../src/stores/useWalletStore.ts | 100 ++ apps/ai-game-creator-shell/src/styles.css | 266 +---- .../components/RichInputArea/attachment.tsx | 55 + .../RichInputArea/attachmentNode.tsx | 84 ++ .../home/components/RichInputArea/index.tsx | 245 ++++ .../RichInputArea/richTextToPrompt.tsx | 86 ++ .../src/view/home/index.tsx | 219 ++-- .../src/view/home/useHomeDraftStore.ts | 41 + .../src/view/home/useHomeShowcase.ts | 2 +- .../ai-game-creator-shell/src/view/layout.tsx | 341 ++++++ .../tests/appSurface.test.ts | 474 ++++++-- .../tests/walletStore.test.ts | 182 +++ .../shared-memory/decision-log.md | 8 + .../shared-memory/development-workflow.md | 2 + ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 5 +- package-lock.json | 1039 +++++++++++++++++ package.json | 5 + .../index.test.tsx} | 4 +- .../index.tsx} | 10 +- .../PlatformProfileRechargeModal/index.css | 92 ++ .../index.test.tsx | 5 +- .../PlatformProfileRechargeModal/index.tsx | 330 ++++-- .../index.css | 46 + .../index.test.tsx | 133 +++ .../index.tsx | 130 +++ .../PlatformProfileWalletLedgerModal/model.ts | 108 ++ packages/shared/src/contracts/runtime.ts | 1 + .../shared/src/icons/taonier-product-ip.png | Bin 0 -> 132336 bytes packages/shared/src/index.ts | 2 + packages/shared/src/utils/format.ts | 5 + scripts/dev.mjs | 3 +- .../ImageCanvasEditorView.test.tsx | 2 +- .../image-editor/ImageCanvasEditorView.tsx | 4 +- .../image-editor/ImageCanvasTopbarView.tsx | 2 +- .../PlatformProfileWalletLedgerModal.test.tsx | 58 - .../PlatformProfileWalletLedgerModal.tsx | 140 --- .../usePlatformProfileCenterController.ts | 11 +- src/components/rpg-entry/RpgEntryHomeView.tsx | 8 +- .../rpgEntryProfileFundsViewModel.test.ts | 100 -- .../rpgEntryProfileFundsViewModel.ts | 87 +- src/index.css | 8 - 49 files changed, 4194 insertions(+), 1362 deletions(-) create mode 100644 apps/ai-game-creator-shell/src-tauri/capabilities/main.json delete mode 100644 apps/ai-game-creator-shell/src/stores/useHomeDraftStore.ts create mode 100644 apps/ai-game-creator-shell/src/stores/useWalletStore.ts create mode 100644 apps/ai-game-creator-shell/src/view/home/components/RichInputArea/attachment.tsx create mode 100644 apps/ai-game-creator-shell/src/view/home/components/RichInputArea/attachmentNode.tsx create mode 100644 apps/ai-game-creator-shell/src/view/home/components/RichInputArea/index.tsx create mode 100644 apps/ai-game-creator-shell/src/view/home/components/RichInputArea/richTextToPrompt.tsx create mode 100644 apps/ai-game-creator-shell/src/view/home/useHomeDraftStore.ts create mode 100644 apps/ai-game-creator-shell/src/view/layout.tsx create mode 100644 apps/ai-game-creator-shell/tests/walletStore.test.ts rename packages/shared/src/components/{PlatformMudPointWalletEntry.test.tsx => PlatformMudPointWalletEntry/index.test.tsx} (97%) rename packages/shared/src/components/{PlatformMudPointWalletEntry.tsx => PlatformMudPointWalletEntry/index.tsx} (96%) create mode 100644 packages/shared/src/components/PlatformProfileRechargeModal/index.css rename src/components/platform-entry/PlatformProfileRechargeModal.test.tsx => packages/shared/src/components/PlatformProfileRechargeModal/index.test.tsx (98%) rename src/components/platform-entry/PlatformProfileRechargeModal.tsx => packages/shared/src/components/PlatformProfileRechargeModal/index.tsx (53%) create mode 100644 packages/shared/src/components/PlatformProfileWalletLedgerModal/index.css create mode 100644 packages/shared/src/components/PlatformProfileWalletLedgerModal/index.test.tsx create mode 100644 packages/shared/src/components/PlatformProfileWalletLedgerModal/index.tsx create mode 100644 packages/shared/src/components/PlatformProfileWalletLedgerModal/model.ts create mode 100644 packages/shared/src/icons/taonier-product-ip.png delete mode 100644 src/components/platform-entry/PlatformProfileWalletLedgerModal.test.tsx delete mode 100644 src/components/platform-entry/PlatformProfileWalletLedgerModal.tsx diff --git a/apps/ai-game-creator-shell/package.json b/apps/ai-game-creator-shell/package.json index d475dd99e..94fae6d80 100644 --- a/apps/ai-game-creator-shell/package.json +++ b/apps/ai-game-creator-shell/package.json @@ -24,7 +24,12 @@ "typecheck": "node ../../node_modules/typescript/bin/tsc -p tsconfig.json --noEmit && node scripts/check-config.mjs" }, "dependencies": { + "@lexical/react": "^0.47.0", + "@lexical/utils": "^0.47.0", + "@tauri-apps/plugin-clipboard-manager": "2.3.2", + "@tauri-apps/plugin-opener": "~2", "@vitejs/plugin-react": "^5.0.4", + "lexical": "^0.47.0", "lucide-react": "^0.546.0", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/apps/ai-game-creator-shell/src-tauri/Cargo.lock b/apps/ai-game-creator-shell/src-tauri/Cargo.lock index b5f866ba7..3535677cb 100644 --- a/apps/ai-game-creator-shell/src-tauri/Cargo.lock +++ b/apps/ai-game-creator-shell/src-tauri/Cargo.lock @@ -65,6 +65,27 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "arboard" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +dependencies = [ + "clipboard-win", + "image", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "parking_lot", + "percent-encoding", + "windows-sys 0.60.2", + "wl-clipboard-rs", + "x11rb", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -378,6 +399,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.12.0" @@ -588,6 +615,15 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + [[package]] name = "combine" version = "4.6.7" @@ -700,6 +736,12 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.7" @@ -947,7 +989,7 @@ checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" dependencies = [ "bit-set", "cssparser", - "foldhash", + "foldhash 0.2.0", "html5ever", "precomputed-hash", "selectors", @@ -1100,6 +1142,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + [[package]] name = "euclid" version = "0.20.14" @@ -1136,6 +1184,12 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +[[package]] +name = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" + [[package]] name = "fdeflate" version = "0.3.7" @@ -1172,6 +1226,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "flate2" version = "1.1.9" @@ -1188,6 +1248,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foldhash" version = "0.2.0" @@ -1473,6 +1539,7 @@ dependencies = [ "similar", "tauri", "tauri-build", + "tauri-plugin-clipboard-manager", "tauri-plugin-dialog", "tauri-plugin-opener", "tempfile", @@ -1493,6 +1560,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix", + "windows-link 0.2.1", +] + [[package]] name = "getrandom" version = "0.2.17" @@ -1694,12 +1771,32 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + [[package]] name = "hashbrown" version = "0.17.1" @@ -2046,6 +2143,20 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png 0.18.1", + "tiff", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -2345,7 +2456,7 @@ dependencies = [ "itoa", "log", "md-5", - "nom", + "nom 7.1.3", "rangemap", "time", "weezl", @@ -2430,6 +2541,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "muda" version = "0.19.3" @@ -2532,6 +2653,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "num-conv" version = "0.2.2" @@ -2589,6 +2719,7 @@ dependencies = [ "block2", "objc2", "objc2-core-foundation", + "objc2-core-graphics", "objc2-foundation", ] @@ -2842,6 +2973,16 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "os_pipe" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "pango" version = "0.18.3" @@ -2923,6 +3064,17 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap 2.14.0", +] + [[package]] name = "phf" version = "0.13.1" @@ -3214,6 +3366,18 @@ dependencies = [ "windows 0.62.2", ] +[[package]] +name = "pxfm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quick-xml" version = "0.39.4" @@ -4387,6 +4551,21 @@ dependencies = [ "walkdir", ] +[[package]] +name = "tauri-plugin-clipboard-manager" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "206dc20af4ed210748ba945c2774e60fd0acd52b9a73a028402caf809e9b6ecf" +dependencies = [ + "arboard", + "log", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", +] + [[package]] name = "tauri-plugin-dialog" version = "2.7.1" @@ -4614,6 +4793,20 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + [[package]] name = "time" version = "0.3.51" @@ -4949,6 +5142,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "tree_magic_mini" +version = "3.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6" +dependencies = [ + "memchr", + "nom 8.0.0", + "petgraph", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -5280,6 +5484,76 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wayland-backend" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" +dependencies = [ + "bitflags 2.13.0", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" +dependencies = [ + "bitflags 2.13.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" +dependencies = [ + "bitflags 2.13.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "pkg-config", +] + [[package]] name = "web-sys" version = "0.3.102" @@ -6018,6 +6292,24 @@ version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +[[package]] +name = "wl-clipboard-rs" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9651471a32e87d96ef3a127715382b2d11cc7c8bb9822ded8a7cc94072eb0a3" +dependencies = [ + "libc", + "log", + "os_pipe", + "rustix", + "thiserror 2.0.18", + "tree_magic_mini", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-wlr", +] + [[package]] name = "writeable" version = "0.6.3" @@ -6089,6 +6381,23 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "gethostname", + "rustix", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + [[package]] name = "yoke" version = "0.8.3" @@ -6288,6 +6597,21 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] + [[package]] name = "zvariant" version = "5.12.0" diff --git a/apps/ai-game-creator-shell/src-tauri/Cargo.toml b/apps/ai-game-creator-shell/src-tauri/Cargo.toml index 76848bd20..875582d4e 100644 --- a/apps/ai-game-creator-shell/src-tauri/Cargo.toml +++ b/apps/ai-game-creator-shell/src-tauri/Cargo.toml @@ -25,12 +25,13 @@ reqwest = { version = "0.12", default-features = false, features = ["json", "nat shared-contracts = { path = "../../../server-rs/crates/shared-contracts", default-features = false } tauri = { version = "2.11.2", features = [] } tauri-plugin-dialog = "2.7.1" -tauri-plugin-opener = "2.5.4" +tauri-plugin-opener = "2" tempfile = "3" tokio = { version = "1", features = ["io-util", "macros", "process", "rt-multi-thread", "sync", "time"] } url = "2" unicode-normalization = "0.1" zip = { version = "2", default-features = false, features = ["deflate"] } +tauri-plugin-clipboard-manager = "2.3.2" [target.'cfg(unix)'.dependencies] libc = "0.2" diff --git a/apps/ai-game-creator-shell/src-tauri/capabilities/main.json b/apps/ai-game-creator-shell/src-tauri/capabilities/main.json new file mode 100644 index 000000000..7f4653d2f --- /dev/null +++ b/apps/ai-game-creator-shell/src-tauri/capabilities/main.json @@ -0,0 +1,14 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "main", + "description": "AI 游戏创作主窗口允许读取系统剪贴板图片,用于粘贴素材附件。", + "windows": ["client"], + "permissions": [ + "clipboard-manager:allow-read-image", + "clipboard-manager:allow-read-text", + "core:image:allow-rgba", + "core:image:allow-size", + "core:resources:allow-close", + "opener:default" + ] +} diff --git a/apps/ai-game-creator-shell/src-tauri/src/main.rs b/apps/ai-game-creator-shell/src-tauri/src/main.rs index 1392549b6..ee948084f 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/main.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/main.rs @@ -1648,6 +1648,7 @@ fn main() { tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_clipboard_manager::init()) .manage(game_creator_preview_registry()) .setup(|app| { configure_game_creator_runtime_config_dir(app.handle())?; diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index 960f3f863..4c28bed3a 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -1,7 +1,27 @@ +/* eslint-disable react-refresh/only-export-components -- Testable pure helpers currently share this legacy app module. */ + +import { + Archive, + ChevronDown, + ChevronUp, + FileText, + FolderKanban, + Gamepad2, + GitFork, + Image, + Pause, + Pencil, + Play, + Plus, + Send, + Target, + Trash2, + Zap, +} from 'lucide-react'; import { - Fragment, type ChangeEvent, type FormEvent, + Fragment, type KeyboardEvent as ReactKeyboardEvent, type MouseEvent as ReactMouseEvent, type ReactNode, @@ -11,44 +31,15 @@ import { useRef, useState, } from 'react'; -import { - Archive, - Bell, - BookOpen, - ChevronDown, - ChevronUp, - CircleHelp, - Contact, - FileText, - FolderKanban, - Gamepad2, - GitFork, - Home, - Image, - LogOut, - Pause, - Pencil, - Play, - Plus, - Send, - Settings, - Target, - Trash2, - User, - Zap, -} from 'lucide-react'; -import HomeView, { - type HomeDraft, - type HomeAgentMode, - type HomeAgentModeItem, - type HomeAttachmentDraft, -} from './view/home'; -import { useLauncherHomeDraftStore } from './stores/useHomeDraftStore'; -import { - getClientProfileDashboard, - getClientProfileWalletLedger, -} from './services/clientApi'; +import { + PlatformMudPointWalletEntry, +} from '../../../packages/shared/src/components/PlatformMudPointWalletEntry'; +import { + PlatformProfileRechargeModal, + type PlatformProfileRechargeNativePaymentState, +} from '../../../packages/shared/src/components/PlatformProfileRechargeModal'; +import { PlatformProfileWalletLedgerModal } from '../../../packages/shared/src/components/PlatformProfileWalletLedgerModal'; import type { AuthEntryResponse, AuthMeResponse, @@ -58,15 +49,6 @@ import type { AuthUser, LogoutResponse, } from '../../../packages/shared/src/contracts/auth'; -import { - API_RESPONSE_ENVELOPE_HEADER, - API_RESPONSE_ENVELOPE_VERSION, - unwrapApiResponse, -} from '../../../packages/shared/src/http'; -import type { - ProfileDashboardSummary, - ProfileWalletLedgerEntry, -} from '../../../packages/shared/src/contracts/runtime'; import { createGameCreationAppManifest, createGameCreationAppSeedTasks, @@ -77,21 +59,46 @@ import { GAME_CREATION_APP_COMMANDS, GAME_CREATION_APP_LIMITED_RUN_COMMANDS, type GameCreationAgentCapabilityDescriptor, - type GameCreationAppAgentGroup, - type GameCreationAppCommandDescriptor, - type GameCreationAppLimitedRunCommandDescriptor, type GameCreationAgentRepairRouteTrace, type GameCreationAgentRunStep, - type GameCreationAgentToolCallTrace, type GameCreationAgentRunTrace, + type GameCreationAgentToolCallTrace, + type GameCreationAppAgentGroup, type GameCreationAppAssetSourceKind, + type GameCreationAppCommandDescriptor, + type GameCreationAppLimitedRunCommandDescriptor, type GameCreationAppManifest, type GameCreationAppPermission, type GameCreationAppPreviewStatus, - type GameCreationAppTaskStatus, type GameCreationAppTaskState, + type GameCreationAppTaskStatus, selectGameCreationAppReadyTasks, } from '../../../packages/shared/src/contracts/gameCreationApp'; +import type { + ProfileRechargeCenterResponse, + ProfileRechargeProduct, + ProfileWalletLedgerResponse, +} from '../../../packages/shared/src/contracts/runtime'; +import { + API_RESPONSE_ENVELOPE_HEADER, + API_RESPONSE_ENVELOPE_VERSION, + unwrapApiResponse, +} from '../../../packages/shared/src/http'; +import { + confirmClientWechatProfileRechargeOrder, + createClientProfileRechargeOrder, + getClientProfileRechargeCenter, + getClientProfileWalletLedger, +} from './services/clientApi'; +import { useWalletStore } from './stores/useWalletStore'; +import HomeView, { + type HomeAgentMode, + type HomeAgentModeItem, + type HomeAttachmentDraft, + type HomeDraft, +} from './view/home'; +import { useLauncherHomeDraftStore } from './view/home/useHomeDraftStore'; +import { type LauncherView, Sidebar } from './view/layout'; const seedManifest = createGameCreationAppManifest( 'local-project-draft', @@ -99,6 +106,10 @@ const seedManifest = createGameCreationAppManifest( ); const defaultProjectPath = '/tmp/genarrative-ai-game-draft'; +type RechargeContent = Omit< + ProfileRechargeCenterResponse, + 'walletBalance' | 'mudPointBalance' +>; const RECENT_WORKSPACES_STORAGE_KEY = 'genarrative-ai-game-creator.recent-workspaces.v1'; const AGENT_RUN_HISTORY_MAX_COUNT = 100; @@ -115,14 +126,6 @@ const launcherNotifications: Array<{ detail: string; actionLabel: string; }> = []; -type LauncherView = - | 'home' - | 'projects' - | 'agent-chat' - | 'guide' - | 'contact' - | 'news' - | 'project-development'; type AgentChatInteractionMode = 'run' | 'chat' | 'goal'; type AgentBackgroundSubmitMode = 'steer' | 'queue'; type AgentChatReplyPhase = @@ -3806,34 +3809,6 @@ const homeAgentModeLabels: Record = { doc: '做方案', }; -function formatMudPoints(value: number | null | undefined) { - if (typeof value !== 'number' || !Number.isFinite(value)) { - return '--'; - } - return Math.max(0, Math.floor(value)).toLocaleString('zh-CN'); -} - -function formatLedgerAmount(entry: ProfileWalletLedgerEntry) { - const amount = Math.trunc(entry.amountDelta); - return `${amount >= 0 ? '+' : ''}${amount}`; -} - -function formatLocalDateTime(value: string | number | null | undefined) { - if (value === null || value === undefined || value === '') { - return '未知时间'; - } - const date = new Date(value); - if (Number.isNaN(date.getTime())) { - return '未知时间'; - } - return date.toLocaleString('zh-CN', { - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - }); -} - function buildHomeConversationContent( mode: HomeAgentMode, prompt: string, @@ -4146,6 +4121,8 @@ function RuntimeConfigDialog({ useEffect(() => { void readRuntimeConfig(); + // The dialog reads once on mount; subsequent reads are explicit user actions. + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); function updateRuntimeLlmConfig( @@ -5385,21 +5362,43 @@ export function WorkspaceLauncher({ const [runtimeConfigOpen, setRuntimeConfigOpen] = useState(false); const [pendingNonEmptyProject, setPendingNonEmptyProject] = useState(null); - const [profileDashboard, setProfileDashboard] = - useState(null); - const [walletPanelOpen, setWalletPanelOpen] = useState(false); - const [walletLedger, setWalletLedger] = useState( - [], + const { + mudPointBalance, + mudPointBalanceStatus, + mudPointBalanceError, + applyWalletBalanceSnapshot, + onWalletBalanceMayHaveChanged, + resetWalletBalance, + } = useWalletStore(); + const [walletLedgerOpen, setWalletLedgerOpen] = useState(false); + const [walletLedger, setWalletLedger] = + useState(null); + const [walletLedgerLoading, setWalletLedgerLoading] = useState(false); + const [walletLedgerError, setWalletLedgerError] = useState( + null, ); - const [walletLedgerStatus, setWalletLedgerStatus] = useState('尚未读取明细'); - const [accountMenuOpen, setAccountMenuOpen] = useState(false); - const [helpMenuOpen, setHelpMenuOpen] = useState(false); + const [rechargeOpen, setRechargeOpen] = useState(false); + const [rechargeContent, setRechargeContent] = + useState(null); + const [rechargeLoading, setRechargeLoading] = useState(false); + const [rechargeError, setRechargeError] = useState(null); + const [submittingRechargeProductId, setSubmittingRechargeProductId] = + useState(null); + const [nativeRechargePayment, setNativeRechargePayment] = + useState(null); + const rechargeLifecycleRef = useRef(0); + const rechargeModalCenter = + rechargeContent && mudPointBalance + ? { + ...rechargeContent, + walletBalance: mudPointBalance.totalPoints, + mudPointBalance, + } + : null; const [launcherNotice, setLauncherNotice] = useState<{ title: string; message: string; } | null>(null); - const accountMenuRef = useRef(null); - const helpMenuRef = useRef(null); const resetLauncherHomeDraft = useLauncherHomeDraftStore( (state) => state.reset, ); @@ -5483,6 +5482,23 @@ export function WorkspaceLauncher({ setAgentChatRunSubmitMode('steer'); }, [agentChatSelectedAgentId, agentChatSelectedSessionId]); + useEffect(() => { + resetWalletBalance(); + void onWalletBalanceMayHaveChanged(); + + const refreshWalletBalance = () => { + void onWalletBalanceMayHaveChanged(); + }; + window.addEventListener('focus', refreshWalletBalance); + return () => { + window.removeEventListener('focus', refreshWalletBalance); + }; + }, [ + currentUser.id, + onWalletBalanceMayHaveChanged, + resetWalletBalance, + ]); + function setAgentChatPendingRuntimeRun( pendingRun: AgentChatPendingRuntimeRun | null, ) { @@ -5839,81 +5855,15 @@ export function WorkspaceLauncher({ }; }, [agentChatPendingRuntimeRun]); - useEffect(() => { - let disposed = false; - void getClientProfileDashboard() - .then((dashboard) => { - if (disposed) { - return; - } - setProfileDashboard(dashboard); - }) - .catch((error) => { - if (disposed) { - return; - } - setProfileDashboard(null); - setWalletLedgerStatus( - error instanceof Error ? error.message : '泥点读取失败', - ); - }); - return () => { - disposed = true; - }; - }, []); - - useEffect(() => { - if (!walletPanelOpen || walletLedgerStatus !== '尚未读取明细') { - return; - } - void loadWalletLedger(); - }, [walletPanelOpen, walletLedgerStatus]); - useEffect(() => { if (launcherView !== 'agent-chat') { return; } void loadAgentChatLlmStatus(); + // Reload only when the active launcher selection changes. + // eslint-disable-next-line react-hooks/exhaustive-deps }, [launcherView, agentChatSelectedAgentId]); - useEffect(() => { - function closeSidebarMenusIfOutside(target: EventTarget | null) { - if (!(target instanceof Node)) { - return; - } - if ( - accountMenuRef.current?.contains(target) || - helpMenuRef.current?.contains(target) - ) { - return; - } - setAccountMenuOpen(false); - setHelpMenuOpen(false); - } - - function handlePointerDown(event: PointerEvent) { - closeSidebarMenusIfOutside(event.target); - } - - function handleFocusIn(event: FocusEvent) { - closeSidebarMenusIfOutside(event.target); - } - - function handleWindowBlur() { - setAccountMenuOpen(false); - setHelpMenuOpen(false); - } - - document.addEventListener('pointerdown', handlePointerDown); - document.addEventListener('focusin', handleFocusIn); - window.addEventListener('blur', handleWindowBlur); - return () => { - document.removeEventListener('pointerdown', handlePointerDown); - document.removeEventListener('focusin', handleFocusIn); - window.removeEventListener('blur', handleWindowBlur); - }; - }, []); - function validateProjectPath(nextProjectPath: string) { const trimmedProjectPath = nextProjectPath.trim(); if (!trimmedProjectPath || !isAbsoluteProjectPath(trimmedProjectPath)) { @@ -6244,25 +6194,160 @@ export function WorkspaceLauncher({ } async function loadWalletLedger() { - setWalletLedgerStatus('正在读取明细'); + setWalletLedgerLoading(true); + setWalletLedgerError(null); try { - const response = await getClientProfileWalletLedger(); - setWalletLedger(response.entries.slice(0, 5)); - setWalletLedgerStatus( - response.entries.length > 0 ? '已读取明细' : '暂无泥点明细', - ); + setWalletLedger(await getClientProfileWalletLedger()); } catch (error) { - setWalletLedger([]); - setWalletLedgerStatus( - error instanceof Error ? error.message : '明细读取失败', + setWalletLedger(null); + setWalletLedgerError( + error instanceof Error ? error.message : '读取泥点账单失败', ); + } finally { + setWalletLedgerLoading(false); + } + } + + function openWalletLedger() { + setWalletLedgerOpen(true); + void loadWalletLedger(); + } + + function applyRechargeContent(center: ProfileRechargeCenterResponse) { + const { walletBalance, mudPointBalance, ...content } = center; + void walletBalance; + if (mudPointBalance) { + applyWalletBalanceSnapshot(mudPointBalance); + } + setRechargeContent(content); + } + + async function loadRechargeCenter() { + const rechargeLifecycle = rechargeLifecycleRef.current; + setRechargeLoading(true); + setRechargeError(null); + try { + const center = await getClientProfileRechargeCenter(); + if (rechargeLifecycleRef.current !== rechargeLifecycle) { + return; + } + applyRechargeContent(center); + } catch (error) { + if (rechargeLifecycleRef.current === rechargeLifecycle) { + setRechargeError( + error instanceof Error ? error.message : '读取泥点购买信息失败', + ); + } + } finally { + if (rechargeLifecycleRef.current === rechargeLifecycle) { + setRechargeLoading(false); + } + } + } + + function openRecharge() { + rechargeLifecycleRef.current += 1; + setRechargeOpen(true); + setRechargeError(null); + void loadRechargeCenter(); + } + + function closeRecharge() { + rechargeLifecycleRef.current += 1; + setRechargeOpen(false); + setNativeRechargePayment(null); + setSubmittingRechargeProductId(null); + } + + async function buyRechargeProduct(product: ProfileRechargeProduct) { + if (submittingRechargeProductId) { + return; + } + const rechargeLifecycle = rechargeLifecycleRef.current; + setSubmittingRechargeProductId(product.productId); + setRechargeError(null); + try { + const response = await createClientProfileRechargeOrder( + product.productId, + ); + if (rechargeLifecycleRef.current !== rechargeLifecycle) { + return; + } + applyRechargeContent(response.center); + const nativePayment = response.wechatNativePayment; + const codeUrl = nativePayment?.codeUrl?.trim(); + const expiresAt = nativePayment?.expiresAt?.trim(); + if (!nativePayment || !codeUrl || !expiresAt) { + throw new Error('微信 Native 支付链接生成失败'); + } + setNativeRechargePayment({ + orderId: response.order.orderId, + productTitle: response.order.productTitle, + amountCents: response.order.amountCents, + codeUrl, + expiresAt, + isConfirming: false, + }); + } catch (error) { + if (rechargeLifecycleRef.current === rechargeLifecycle) { + setRechargeError(error instanceof Error ? error.message : '充值失败'); + } + } finally { + if (rechargeLifecycleRef.current === rechargeLifecycle) { + setSubmittingRechargeProductId(null); + } + } + } + + async function confirmNativeRechargePayment() { + if (!nativeRechargePayment || nativeRechargePayment.isConfirming) { + return; + } + const rechargeLifecycle = rechargeLifecycleRef.current; + const orderId = nativeRechargePayment.orderId; + setNativeRechargePayment((current) => + current?.orderId === orderId + ? { ...current, isConfirming: true, confirmMessage: undefined } + : current, + ); + try { + const response = await confirmClientWechatProfileRechargeOrder(orderId); + if (rechargeLifecycleRef.current !== rechargeLifecycle) { + return; + } + applyRechargeContent(response.center); + if (response.order.status === 'paid') { + setNativeRechargePayment(null); + void onWalletBalanceMayHaveChanged(); + return; + } + const confirmMessage = + response.order.status === 'pending' || + (response.order.status === 'expired' && + !response.order.expirationCheckedAt) + ? '暂未确认到账,请确认付款完成后再点一次。' + : '订单未支付成功,请关闭二维码后重新下单。'; + setNativeRechargePayment((current) => + current?.orderId === orderId + ? { ...current, isConfirming: false, confirmMessage } + : current, + ); + } catch { + if (rechargeLifecycleRef.current === rechargeLifecycle) { + setNativeRechargePayment((current) => + current?.orderId === orderId + ? { + ...current, + isConfirming: false, + confirmMessage: '暂时没能确认到账状态,请稍后再试。', + } + : current, + ); + } } } function showLauncherNotice(title: string) { - setAccountMenuOpen(false); - setHelpMenuOpen(false); - setWalletPanelOpen(false); setLauncherNotice({ title, message: `${title}正在接入中,当前版本会先保留入口。`, @@ -8549,145 +8634,23 @@ export function WorkspaceLauncher({ ]; return ( -
- +
+ { + resetLauncherHomeDraft(); + resetWalletBalance(); + onLogout(); + }} + onNoticeRequest={showLauncherNotice} + onRechargeRequest={openRecharge} + onRuntimeConfigOpen={() => setRuntimeConfigOpen(true)} + onViewChange={setLauncherView} + />
0 @@ -8708,49 +8671,15 @@ export function WorkspaceLauncher({ ) : null}
-
-
- - {walletPanelOpen ? ( -
- 泥点 -

{formatMudPoints(profileDashboard?.walletBalance)} 泥点

-
- {walletLedger.length > 0 ? ( - walletLedger.map((entry) => ( -
- {formatLedgerAmount(entry)} - {formatLocalDateTime(entry.createdAt)} -
- )) - ) : ( - {walletLedgerStatus} - )} -
- -
- ) : null} -
- -
+ void onWalletBalanceMayHaveChanged()} + onRecharge={openRecharge} + onOpenLedger={openWalletLedger} + />
{launcherView === 'home' ? ( @@ -9506,6 +9435,30 @@ export function WorkspaceLauncher({ onClose={() => setRuntimeConfigOpen(false)} /> ) : null} + {rechargeOpen ? ( + void loadRechargeCenter()} + onBuy={(product) => void buyRechargeProduct(product)} + onConfirmNativePayment={() => void confirmNativeRechargePayment()} + onCloseNativePayment={() => setNativeRechargePayment(null)} + /> + ) : null} + {walletLedgerOpen ? ( + setWalletLedgerOpen(false)} + onRetry={() => void loadWalletLedger()} + /> + ) : null} {agentChatGoalDialog ? (
{ @@ -17312,6 +17267,8 @@ export function App({ return; } void openWorkspace(initialProjectPath, false); + // Initial project opening is guarded by initialProjectOpenedRef. + // eslint-disable-next-line react-hooks/exhaustive-deps }, [initialProjectPath, projectSupervisorOnly]); useEffect(() => { @@ -17535,6 +17492,8 @@ export function App({ disposed = true; window.clearInterval(timer); }; + // Polling restarts only when the tracked runtime identity or phase changes. + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ localProject?.projectPath, projectSupervisorSessionId, @@ -17686,6 +17645,9 @@ export function App({ setConversationWriteVersion((current) => current + 1); } }); + // Writes are driven by message and confirmation state; the confirmation helper + // intentionally observes the latest policy refs. + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ localProject?.projectPath, messages, diff --git a/apps/ai-game-creator-shell/src/services/clientApi.ts b/apps/ai-game-creator-shell/src/services/clientApi.ts index 1684e04b9..c16b24437 100644 --- a/apps/ai-game-creator-shell/src/services/clientApi.ts +++ b/apps/ai-game-creator-shell/src/services/clientApi.ts @@ -1,7 +1,10 @@ import { API_RESPONSE_ENVELOPE_HEADER, API_RESPONSE_ENVELOPE_VERSION, + ConfirmWechatProfileRechargeOrderResponse, + CreateProfileRechargeOrderResponse, ProfileDashboardSummary, + ProfileRechargeCenterResponse, ProfileWalletLedgerResponse, unwrapApiResponse, } from '../../../../packages/shared/src'; @@ -134,11 +137,42 @@ export function getClientProfileDashboard() { ); } +export function getClientProfileRechargeCenter() { + return requestClientApi( + '/api/profile/recharge-center', + { method: 'GET' }, + '读取泥点明细失败', + ); +} + +export function createClientProfileRechargeOrder(productId: string) { + return requestClientApi( + '/api/profile/recharge/orders', + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + productId, + paymentChannel: 'wechat_native', + }), + }, + '充值失败', + ); +} + +export function confirmClientWechatProfileRechargeOrder(orderId: string) { + return requestClientApi( + `/api/profile/recharge/orders/${encodeURIComponent(orderId)}/wechat/confirm`, + { method: 'POST' }, + '确认微信支付订单失败', + ); +} + export function getClientProfileWalletLedger() { return requestClientApi( '/api/profile/wallet-ledger', { method: 'GET' }, - '读取泥点明细失败', + '读取泥点账单失败', ); } diff --git a/apps/ai-game-creator-shell/src/stores/useHomeDraftStore.ts b/apps/ai-game-creator-shell/src/stores/useHomeDraftStore.ts deleted file mode 100644 index 4773a84c3..000000000 --- a/apps/ai-game-creator-shell/src/stores/useHomeDraftStore.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { create } from 'zustand'; - -export type HomeAgentMode = 'game' | 'art' | 'doc'; - -export type HomeAttachmentDraft = { - id: string; - file: File; -}; - -export type HomeDraft = { - mode: HomeAgentMode; - prompt: string; - attachments: HomeAttachmentDraft[]; -}; - -type UseHomeDraftStore = HomeDraft & { - setMode: (mode: HomeAgentMode) => void; - setPrompt: (prompt: string) => void; - addAttachments: (attachments: HomeAttachmentDraft[]) => void; - removeAttachment: (attachmentId: string) => void; - reset: () => void; -}; - -const initialHomeDraft: HomeDraft = { - mode: 'game', - prompt: '', - attachments: [], -}; - -// Keep non-serializable File objects available while the Home view is unmounted. -export const useLauncherHomeDraftStore = create((set) => ({ - ...initialHomeDraft, - setMode: (mode) => set({ mode }), - setPrompt: (prompt) => set({ prompt }), - addAttachments: (attachments) => - set((state) => ({ attachments: [...state.attachments, ...attachments] })), - removeAttachment: (attachmentId) => - set((state) => ({ - attachments: state.attachments.filter( - (attachment) => attachment.id !== attachmentId, - ), - })), - reset: () => set(initialHomeDraft), -})); diff --git a/apps/ai-game-creator-shell/src/stores/useWalletStore.ts b/apps/ai-game-creator-shell/src/stores/useWalletStore.ts new file mode 100644 index 000000000..eba4d1b48 --- /dev/null +++ b/apps/ai-game-creator-shell/src/stores/useWalletStore.ts @@ -0,0 +1,100 @@ +import { create } from 'zustand'; + +import type { ProfileMudPointBalance } from '../../../../packages/shared/src'; +import { getClientProfileRechargeCenter } from '../services/clientApi'; + +export type MudPointBalanceStatus = 'idle' | 'loading' | 'ready' | 'error'; + +type WalletStore = { + mudPointBalance: ProfileMudPointBalance | null; + mudPointBalanceStatus: MudPointBalanceStatus; + mudPointBalanceError: string; + applyWalletBalanceSnapshot: (balance: ProfileMudPointBalance) => void; + onWalletBalanceMayHaveChanged: () => Promise; + resetWalletBalance: () => void; +}; + +const initialWalletState: Pick< + WalletStore, + 'mudPointBalance' | 'mudPointBalanceStatus' | 'mudPointBalanceError' +> = { + mudPointBalance: null, + mudPointBalanceStatus: 'idle', + mudPointBalanceError: '', +}; + +let refreshVersion = 0; +let requestGeneration = 0; +let activeRefresh: Promise | null = null; + +export const useWalletStore = create((set) => ({ + ...initialWalletState, + applyWalletBalanceSnapshot: (balance) => { + requestGeneration += 1; + refreshVersion = 0; + activeRefresh = null; + set({ + mudPointBalance: balance, + mudPointBalanceStatus: 'ready', + mudPointBalanceError: '', + }); + }, + onWalletBalanceMayHaveChanged: () => { + refreshVersion += 1; + if (activeRefresh) { + return activeRefresh; + } + + const refreshRequestGeneration = requestGeneration; + const refresh = (async () => { + while (refreshRequestGeneration === requestGeneration) { + const requestedVersion = refreshVersion; + set({ mudPointBalanceStatus: 'loading', mudPointBalanceError: '' }); + try { + const center = await getClientProfileRechargeCenter(); + if (!center.mudPointBalance) { + throw new Error('充值中心响应缺少泥点余额'); + } + if (refreshRequestGeneration !== requestGeneration) { + return; + } + if (requestedVersion !== refreshVersion) { + continue; + } + set({ + mudPointBalance: center.mudPointBalance, + mudPointBalanceStatus: 'ready', + mudPointBalanceError: '', + }); + return; + } catch (error) { + if (refreshRequestGeneration !== requestGeneration) { + return; + } + if (requestedVersion !== refreshVersion) { + continue; + } + set({ + mudPointBalanceStatus: 'error', + mudPointBalanceError: + error instanceof Error ? error.message : '泥点明细读取失败', + }); + return; + } + } + })(); + activeRefresh = refresh; + void refresh.finally(() => { + if (activeRefresh === refresh) { + activeRefresh = null; + } + }); + return refresh; + }, + resetWalletBalance: () => { + requestGeneration += 1; + refreshVersion = 0; + activeRefresh = null; + set(initialWalletState); + }, +})); diff --git a/apps/ai-game-creator-shell/src/styles.css b/apps/ai-game-creator-shell/src/styles.css index a51171a8c..6157dc1f6 100644 --- a/apps/ai-game-creator-shell/src/styles.css +++ b/apps/ai-game-creator-shell/src/styles.css @@ -1,4 +1,6 @@ @import 'tailwindcss'; +@source '../../../packages/shared/src/components'; +@import '../../../packages/shared/src/theme.css'; :root { color: #18202f; @@ -142,146 +144,43 @@ textarea { } .launcher-shell { + --launcher-sidebar-width: 64px; display: grid; - grid-template-columns: 48px minmax(0, 1fr); + grid-template-columns: var(--launcher-sidebar-width) minmax(0, 1fr); min-height: 100vh; background: #fff; color: #171717; } -.launcher-sidebar { - position: sticky; - top: 0; - z-index: 40; - display: flex; - flex-direction: column; - align-items: center; - gap: 18px; - height: 100vh; - padding: 16px 0 18px; - border-right: 1px solid #eceef2; - background: #fff; - overflow: visible; +.launcher-shell.platform-theme { + background: var(--platform-body-fill); + color: var(--platform-text-strong); } -.launcher-brand { - display: grid; - place-items: center; +.launcher-shell.platform-theme .launcher-main { + background: var(--platform-body-fill); } -.launcher-logo { - display: grid; - width: 24px; - height: 24px; - border-radius: 50%; - background: #101010; - color: #fff; - font-size: 10px; - font-weight: 700; - letter-spacing: 0; - place-items: center; +.launcher-shell.platform-theme .launcher-promo { + background: var(--platform-warm-bg); + color: var(--platform-warm-text); } -.launcher-sidebar nav { - display: grid; - gap: 13px; - justify-items: center; +.launcher-shell.platform-theme .launcher-promo button { + color: var(--platform-warm-text); } -.launcher-sidebar-bottom { - position: relative; - z-index: 60; - display: grid; - justify-items: center; - gap: 13px; - margin-top: auto; +.launcher-shell.platform-theme .launcher-countdown { + border-color: var(--platform-subpanel-border); + background: var(--platform-subpanel-fill); + color: var(--platform-text-strong); } -.launcher-sidebar button, -.launcher-promo button, -.launcher-account-bar button { +.launcher-promo button { border: 0; cursor: pointer; } -.launcher-sidebar nav button, -.launcher-config-button { - display: grid; - width: 28px; - height: 28px; - padding: 0; - border-radius: 8px; - background: transparent; - color: #161616; - font-size: 17px; - place-items: center; -} - -.launcher-sidebar .launcher-nav-active { - background: #f3f4f6; -} - -.launcher-account-nav, -.launcher-help-nav { - position: relative; -} - -.launcher-help-menu { - position: absolute; - top: auto; - bottom: -4px; - left: 36px; - display: grid; - min-width: 150px; - gap: 5px; - padding: 8px; - border: 1px solid #e5e7eb; - border-radius: 8px; - background: #fff; - box-shadow: 0 14px 34px rgb(15 23 42 / 14%); - z-index: 90; -} - -.launcher-sidebar .launcher-help-menu button, -.launcher-sidebar .launcher-account-popover button { - display: flex; - align-items: center; - justify-content: flex-start; - gap: 8px; - width: 100%; - height: 30px; - padding: 0 9px; - border-radius: 7px; - background: transparent; - color: #1f2937; - font-size: 12px; - white-space: nowrap; -} - -.launcher-sidebar .launcher-help-menu button:hover, -.launcher-sidebar .launcher-help-menu button:focus-visible, -.launcher-sidebar .launcher-account-popover button:hover, -.launcher-sidebar .launcher-account-popover button:focus-visible { - background: #f3f4f6; -} - -.launcher-config-button { - font-size: 15px; -} - -.launcher-config-button:hover { - background: #f3f4f6; -} - -.launcher-help-nav .launcher-config-button, -.launcher-help-nav .launcher-config-button:hover, -.launcher-help-nav .launcher-config-button:focus, -.launcher-help-nav .launcher-config-button:focus-visible, -.launcher-help-nav .launcher-config-button:active, -.launcher-help-nav .launcher-config-button[aria-expanded='true'] { - background: transparent !important; -} - .launcher-main { position: relative; display: grid; @@ -343,129 +242,6 @@ textarea { top: 44px; } -.launcher-account-bar button { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 4px; - height: 26px; - padding: 0 9px; - border: 0; - border-radius: 0; - background: #fff; - color: #1f2937; - font-size: 12px; -} - -.launcher-wallet-group { - display: inline-flex; - align-items: center; - overflow: visible; - border: 1px solid #e5e7eb; - border-radius: 8px; - background: #fff; - box-shadow: 0 1px 2px rgb(15 23 42 / 5%); -} - -.launcher-wallet-group > button, -.launcher-wallet-group .launcher-wallet-anchor > button { - border: 0; - border-radius: 0; -} - -.launcher-wallet-group .launcher-wallet-anchor > button { - border-radius: 8px 0 0 8px; - font-weight: 500; -} - -.launcher-wallet-group > button { - border-left: 1px solid #e5e7eb; - border-radius: 0 8px 8px 0; -} - -.launcher-wallet-anchor { - position: relative; - display: flex; -} - -.launcher-account-popover, -.launcher-wallet-popover { - position: absolute; - top: 34px; - right: 0; - display: grid; - width: 220px; - gap: 7px; - padding: 12px; - border: 1px solid #e5e7eb; - border-radius: 8px; - background: #fff; - box-shadow: 0 16px 38px rgb(15 23 42 / 14%); - color: #111827; - z-index: 90; -} - -.launcher-account-nav .launcher-account-popover { - top: auto; - right: auto; - bottom: -4px; - left: 36px; - width: 218px; - gap: 6px; - padding: 10px; -} - -.launcher-account-popover strong, -.launcher-wallet-popover strong { - font-size: 13px; -} - -.launcher-account-popover span, -.launcher-wallet-popover span, -.launcher-account-popover small, -.launcher-wallet-popover small { - color: #6b7280; - font-size: 11px; -} - -.launcher-account-popover button, -.launcher-wallet-popover button { - justify-content: flex-start; - width: 100%; - height: 30px; - padding: 0 8px; - border: 0; - background: transparent; - color: #111827; -} - -.launcher-wallet-popover p { - margin: 2px 0 4px; - font-size: 20px; - font-weight: 750; -} - -.launcher-wallet-ledger { - display: grid; - gap: 5px; - max-height: 132px; - overflow: auto; -} - -.launcher-wallet-ledger div { - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; - min-width: 0; -} - -.launcher-wallet-ledger div span { - color: #111827; - font-size: 12px; - font-weight: 700; -} - .launcher-project-list-actions { display: flex; align-items: center; @@ -1703,10 +1479,6 @@ textarea { } @media (max-width: 760px) { - .launcher-shell { - grid-template-columns: 48px minmax(0, 1fr); - } - .launcher-main { grid-template-rows: auto auto auto; overflow: visible; diff --git a/apps/ai-game-creator-shell/src/view/home/components/RichInputArea/attachment.tsx b/apps/ai-game-creator-shell/src/view/home/components/RichInputArea/attachment.tsx new file mode 100644 index 000000000..1e2c3330b --- /dev/null +++ b/apps/ai-game-creator-shell/src/view/home/components/RichInputArea/attachment.tsx @@ -0,0 +1,55 @@ +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; +import { $getNodeByKey, type NodeKey } from 'lexical'; +import { X } from 'lucide-react'; +import { useEffect, useState } from 'react'; + +import type { HomeAttachmentDraft } from '../../useHomeDraftStore'; + +export function AttachmentToken({ + attachment, + nodeKey, +}: { + attachment: HomeAttachmentDraft; + nodeKey: NodeKey; +}) { + const [editor] = useLexicalComposerContext(); + const [previewUrl, setPreviewUrl] = useState(null); + + useEffect(() => { + if (!attachment.file.type.startsWith('image/')) { + setPreviewUrl(null); + return; + } + + const objectUrl = URL.createObjectURL(attachment.file); + setPreviewUrl(objectUrl); + return () => URL.revokeObjectURL(objectUrl); + }, [attachment.file]); + + return ( + + {previewUrl ? ( + + {attachment.file.name} + + ) : null} + {attachment.file.name} + + + ); +} diff --git a/apps/ai-game-creator-shell/src/view/home/components/RichInputArea/attachmentNode.tsx b/apps/ai-game-creator-shell/src/view/home/components/RichInputArea/attachmentNode.tsx new file mode 100644 index 000000000..a0ccb24bd --- /dev/null +++ b/apps/ai-game-creator-shell/src/view/home/components/RichInputArea/attachmentNode.tsx @@ -0,0 +1,84 @@ +import { + $applyNodeReplacement, + DecoratorNode, + type EditorConfig, + type LexicalNode, + type NodeKey, + type SerializedLexicalNode, + type Spread, +} from 'lexical'; +import type { ReactNode } from 'react'; + +import type { HomeAttachmentDraft } from '../../useHomeDraftStore'; +import { AttachmentToken } from './attachment'; + +export type SerializedAttachmentNode = Spread< + { + attachment: HomeAttachmentDraft; + type: 'home-attachment'; + version: 1; + }, + SerializedLexicalNode +>; + +export class AttachmentNode extends DecoratorNode { + __attachment: HomeAttachmentDraft; + + static getType() { + return 'home-attachment'; + } + + static clone(node: AttachmentNode) { + return new AttachmentNode(node.__attachment, node.__key); + } + + static importJSON(serializedNode: SerializedAttachmentNode) { + return $createAttachmentNode(serializedNode.attachment); + } + + constructor(attachment: HomeAttachmentDraft, key?: NodeKey) { + super(key); + this.__attachment = attachment; + } + + exportJSON(): SerializedAttachmentNode { + return { + ...super.exportJSON(), + attachment: this.__attachment, + type: 'home-attachment', + version: 1, + }; + } + + createDOM(_config: EditorConfig) { + return document.createElement('span'); + } + + updateDOM() { + return false; + } + + getTextContent() { + return ''; + } + + isInline() { + return true; + } + + decorate() { + return ( + + ); + } +} + +export function $createAttachmentNode(attachment: HomeAttachmentDraft) { + return $applyNodeReplacement(new AttachmentNode(attachment)); +} + +export function $isAttachmentNode( + node: LexicalNode | null | undefined, +): node is AttachmentNode { + return node instanceof AttachmentNode; +} diff --git a/apps/ai-game-creator-shell/src/view/home/components/RichInputArea/index.tsx b/apps/ai-game-creator-shell/src/view/home/components/RichInputArea/index.tsx new file mode 100644 index 000000000..f8a36c53e --- /dev/null +++ b/apps/ai-game-creator-shell/src/view/home/components/RichInputArea/index.tsx @@ -0,0 +1,245 @@ +import { LexicalComposer } from '@lexical/react/LexicalComposer'; +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; +import { ContentEditable } from '@lexical/react/LexicalContentEditable'; +import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary'; +import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin'; +import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'; +import { readImage, readText } from '@tauri-apps/plugin-clipboard-manager'; +import { + $createParagraphNode, + $getRoot, + $getSelection, + $isRangeSelection, + COMMAND_PRIORITY_EDITOR, + COMMAND_PRIORITY_HIGH, + createCommand, + type LexicalCommand, + PASTE_COMMAND, +} from 'lexical'; +import { Upload } from 'lucide-react'; +import React, { useEffect, useRef } from 'react'; + +import type { Draft, HomeAttachmentDraft } from '../../useHomeDraftStore'; +import { + $createAttachmentNode, + AttachmentNode, +} from './attachmentNode'; + +type RichInputAreaProps = { + value: Draft; + placeholder: string; + onChange: (value: Draft) => void; + children?: React.ReactNode; +}; +const INSERT_ATTACHMENTS_COMMAND: LexicalCommand = + createCommand('INSERT_HOME_ATTACHMENTS_COMMAND'); +const INSERT_CLIPBOARD_TEXT_COMMAND: LexicalCommand = createCommand( + 'INSERT_HOME_CLIPBOARD_TEXT_COMMAND', +); + +function createAttachmentDrafts(files: readonly File[]) { + const now = Date.now(); + return files.map((file, index) => ({ + id: `${file.name}:${file.size}:${file.lastModified}:${index}:${now}`, + file, + })); +} + +async function readNativeClipboardImageFile() { + // this used Tauri native api, unavailable in browser env. + let image; + try { + image = await readImage(); + const [{ width, height }, rgba] = await Promise.all([ + image.size(), + image.rgba(), + ]); + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + const context = canvas.getContext('2d'); + if (!context) { + return null; + } + context.putImageData( + new ImageData(new Uint8ClampedArray(rgba), width, height), + 0, + 0, + ); + const blob = await new Promise((resolve) => + canvas.toBlob(resolve, 'image/png'), + ); + if (!blob) { + return null; + } + return new File([blob], `pasted-image-${Date.now()}.png`, { + type: 'image/png', + }); + } catch { + return null; + } finally { + await image?.close(); + } +} + +async function readNativeClipboardText() { + try { + return await readText(); + } catch { + return ''; + } +} + +function selectEditableEndWhenNeeded() { + const selection = $getSelection(); + if ($isRangeSelection(selection)) { + return; + } + const root = $getRoot(); + if (root.getChildrenSize() === 0) { + root.append($createParagraphNode()); + } + root.selectEnd(); +} + +function EditorPlugins({ + onChange, +}: Pick) { + const [editor] = useLexicalComposerContext(); + + useEffect( + () => + editor.registerCommand( + INSERT_ATTACHMENTS_COMMAND, + (nextAttachments) => { + selectEditableEndWhenNeeded(); + const currentSelection = $getSelection(); + if ($isRangeSelection(currentSelection)) { + currentSelection.insertNodes( + nextAttachments.map((attachment) => + $createAttachmentNode(attachment), + ), + ); + } + return true; + }, + COMMAND_PRIORITY_EDITOR, + ), + [editor], + ); + + useEffect( + () => + editor.registerCommand( + INSERT_CLIPBOARD_TEXT_COMMAND, + (text) => { + selectEditableEndWhenNeeded(); + const selection = $getSelection(); + if ($isRangeSelection(selection)) { + selection.insertText(text); + } + return true; + }, + COMMAND_PRIORITY_EDITOR, + ), + [editor], + ); + + useEffect( + () => + // this ONLY works in Tauri, no plan for browser by now. + editor.registerCommand( + PASTE_COMMAND, + (event) => { + event.preventDefault(); + void readNativeClipboardImageFile().then(async (file) => { + if (!file) { + const text = await readNativeClipboardText(); + if (text.length === 0) { + return; + } + editor.dispatchCommand(INSERT_CLIPBOARD_TEXT_COMMAND, text); + return; + } + const nextAttachments = createAttachmentDrafts([file]); + editor.dispatchCommand(INSERT_ATTACHMENTS_COMMAND, nextAttachments); + }); + return true; + }, + COMMAND_PRIORITY_HIGH, + ), + [editor], + ); + + return ( + { + onChange(editorState); + }} + /> + ); +} + +export function UploadButton() { + const [editor] = useLexicalComposerContext(); + const inputRef = useRef(null); + return ( + <> + { + const files = Array.from(event.currentTarget.files ?? []); + event.currentTarget.value = ''; + if (files.length === 0) return; + const nextAttachments = createAttachmentDrafts(files); + editor.dispatchCommand(INSERT_ATTACHMENTS_COMMAND, nextAttachments); + }} + /> + + + ); +} + +export default function RichInputArea(props: RichInputAreaProps) { + return ( + { + throw error; + }, + }} + > +
+ + } + placeholder={ + + {props.placeholder} + + } + ErrorBoundary={LexicalErrorBoundary} + /> + {props.children} +
+ +
+ ); +} diff --git a/apps/ai-game-creator-shell/src/view/home/components/RichInputArea/richTextToPrompt.tsx b/apps/ai-game-creator-shell/src/view/home/components/RichInputArea/richTextToPrompt.tsx new file mode 100644 index 000000000..a62bdb291 --- /dev/null +++ b/apps/ai-game-creator-shell/src/view/home/components/RichInputArea/richTextToPrompt.tsx @@ -0,0 +1,86 @@ +import { + $getRoot, + $isElementNode, + $isLineBreakNode, + $isTextNode, + type LexicalNode, +} from 'lexical'; + +import type { Draft, HomeAttachmentDraft } from '../../useHomeDraftStore'; +import { $isAttachmentNode } from './attachmentNode'; + +function ordinalSuffix(index: number) { + if (index % 100 >= 11 && index % 100 <= 13) return 'th'; + return ( + ({ 1: 'st', 2: 'nd', 3: 'rd' } as Record)[index % 10] ?? + 'th' + ); +} + +function collectPromptParts( + node: LexicalNode, + output: string[], + attachmentIndex: { value: number }, +) { + if ($isTextNode(node)) { + output.push(node.getTextContent()); + return; + } + if ($isLineBreakNode(node)) { + output.push('\n'); + return; + } + if ($isAttachmentNode(node)) { + attachmentIndex.value += 1; + output.push( + `{${attachmentIndex.value}${ordinalSuffix(attachmentIndex.value)} attachment: ${node.__attachment.file.name}}`, + ); + return; + } + if ($isElementNode(node)) { + const children = node.getChildren(); + children.forEach((child, index) => { + if (index > 0 && node.getType() === 'root') { + output.push('\n'); + } + collectPromptParts(child, output, attachmentIndex); + }); + } +} + +export function richTextToPrompt(richText: Draft) { + if (!richText) return ''; + return richText.read(() => { + const output: string[] = []; + collectPromptParts($getRoot(), output, { value: 0 }); + return output.join('').trim(); + }); +} + +function collectAttachments( + node: LexicalNode, + attachments: HomeAttachmentDraft[], + seenAttachmentIds: Set, +) { + if ($isAttachmentNode(node)) { + if (!seenAttachmentIds.has(node.__attachment.id)) { + seenAttachmentIds.add(node.__attachment.id); + attachments.push(node.__attachment); + } + return; + } + if ($isElementNode(node)) { + for (const child of node.getChildren()) { + collectAttachments(child, attachments, seenAttachmentIds); + } + } +} + +export function richTextToAttachments(richText: Draft) { + if (!richText) return []; + return richText.read(() => { + const attachments: HomeAttachmentDraft[] = []; + collectAttachments($getRoot(), attachments, new Set()); + return attachments; + }); +} diff --git a/apps/ai-game-creator-shell/src/view/home/index.tsx b/apps/ai-game-creator-shell/src/view/home/index.tsx index 9cfe6b4e4..024b41dce 100644 --- a/apps/ai-game-creator-shell/src/view/home/index.tsx +++ b/apps/ai-game-creator-shell/src/view/home/index.tsx @@ -3,27 +3,30 @@ import { type LucideIcon, Plus, Sparkles, - Upload, - X, } from 'lucide-react'; import type { FormEvent, } from 'react'; -import { useRef, useState } from 'react'; +import { useState } from 'react'; +import BRAND_ICON from '../../../../../packages/shared/src/icons/taonier-product-ip.png' +import RichInputArea, { UploadButton } from './components/RichInputArea'; +import { + richTextToAttachments, + richTextToPrompt, +} from './components/RichInputArea/richTextToPrompt'; import { - useLauncherHomeDraftStore, type HomeAgentMode, - type HomeAttachmentDraft, type HomeDraft, -} from '../../stores/useHomeDraftStore'; + useLauncherHomeDraftStore, +} from './useHomeDraftStore'; import { useHomeShowcase } from './useHomeShowcase'; export type { HomeAgentMode, HomeAttachmentDraft, HomeDraft, -} from '../../stores/useHomeDraftStore'; +} from './useHomeDraftStore'; export type HomeAgentModeItem = { mode: HomeAgentMode; @@ -39,17 +42,6 @@ export type HomeProjectRow = { status: string; canOpen: boolean; }; - -export type HomeShowcaseResource = { - resourceId: string; - showcaseId?: string | null; - label?: string | null; - imageSrc: string; - prompt?: string | null; - authorDisplayName?: string | null; - authorPublicUserCode?: string | null; -}; - type HomeViewProps = { hasPromo: boolean; status: string; @@ -72,22 +64,14 @@ export default function HomeView({ onProjectOpen, }: HomeViewProps) { const homeAgentMode = useLauncherHomeDraftStore((state) => state.mode); - const homePrompt = useLauncherHomeDraftStore((state) => state.prompt); - const homeAttachments = useLauncherHomeDraftStore( - (state) => state.attachments, - ); + const homeRichText = useLauncherHomeDraftStore((state) => state.draft); const setHomeAgentMode = useLauncherHomeDraftStore((state) => state.setMode); - const setHomePrompt = useLauncherHomeDraftStore((state) => state.setPrompt); - const addHomeAttachments = useLauncherHomeDraftStore( - (state) => state.addAttachments, - ); - const removeHomeAttachment = useLauncherHomeDraftStore( - (state) => state.removeAttachment, + const setHomeRichText = useLauncherHomeDraftStore( + (state) => state.setRichText, ); const [homeCreationBusy, setHomeCreationBusy] = useState(false); const { resources: showcaseResources, status: showcaseStatus } = useHomeShowcase(); - const homeAttachmentInputRef = useRef(null); const activeHomeMode = homeAgentModeItems.find((item) => item.mode === homeAgentMode) ?? homeAgentModeItems[0]; @@ -96,27 +80,11 @@ export default function HomeView({ return null; } - const ActiveHomeModeIcon = activeHomeMode.icon; - - function handleHomeAttachmentSelection( - event: React.ChangeEvent, - ) { - const files = Array.from(event.currentTarget.files ?? []); - event.currentTarget.value = ''; - if (files.length === 0) { - return; - } - addHomeAttachments( - files.map((file, index) => ({ - id: `${file.name}:${file.size}:${file.lastModified}:${index}:${Date.now()}`, - file, - })), - ); - } - async function handleHomeSubmit(event: FormEvent) { event.preventDefault(); - if (!homePrompt.trim() && homeAttachments.length === 0) { + const referencedAttachments = richTextToAttachments(homeRichText); + const prompt = richTextToPrompt(homeRichText); + if (!prompt) { onStatusChange( homeAgentModeItems.find((item) => item.mode === homeAgentMode) ?.emptyPrompt ?? '请输入需求或上传附件', @@ -129,8 +97,8 @@ export default function HomeView({ onStatusChange( await onCreateDraft({ mode: homeAgentMode, - prompt: homePrompt, - attachments: homeAttachments, + prompt, + attachments: referencedAttachments, }), ); } catch (error) { @@ -141,28 +109,28 @@ export default function HomeView({ } return ( - <> +
- -
{homeAgentModeItems.map((item) => { @@ -174,8 +142,8 @@ export default function HomeView({ key={item.mode} className={`inline-flex min-h-6.75 items-center gap-1.25 rounded-full border px-3 text-[12px] ${ isActive - ? 'border-violet-500 bg-[#f8f2ff] text-violet-600' - : 'border-[#d6d6d6] bg-white text-[#333]' + ? 'border-(--platform-nav-active-border) bg-(image:--platform-nav-active-fill) text-(--platform-warm-text) shadow-(--platform-nav-active-shadow)' + : 'border-(--platform-subpanel-border) bg-(image:--platform-subpanel-fill) text-(--platform-neutral-text)' }`} onClick={() => setHomeAgentMode(item.mode)} > @@ -185,80 +153,49 @@ export default function HomeView({ ); })}
+ + {status} +
-