合并远端 AI 游戏创作分支改动
同步首页侧栏与富文本输入改造 保留当前 GameAgent 工作台和自主 Runtime 改动
This commit is contained in:
@@ -26,7 +26,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",
|
||||
|
||||
+326
-2
@@ -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"
|
||||
@@ -599,6 +626,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"
|
||||
@@ -720,6 +756,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"
|
||||
@@ -967,7 +1009,7 @@ checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89"
|
||||
dependencies = [
|
||||
"bit-set",
|
||||
"cssparser",
|
||||
"foldhash",
|
||||
"foldhash 0.2.0",
|
||||
"html5ever",
|
||||
"precomputed-hash",
|
||||
"selectors",
|
||||
@@ -1120,6 +1162,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"
|
||||
@@ -1156,6 +1204,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"
|
||||
@@ -1192,6 +1246,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"
|
||||
@@ -1208,6 +1268,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"
|
||||
@@ -1493,6 +1559,7 @@ dependencies = [
|
||||
"similar",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-clipboard-manager",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-opener",
|
||||
"tempfile",
|
||||
@@ -1513,6 +1580,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"
|
||||
@@ -1719,12 +1796,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"
|
||||
@@ -2087,6 +2184,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"
|
||||
@@ -2386,7 +2497,7 @@ dependencies = [
|
||||
"itoa",
|
||||
"log",
|
||||
"md-5",
|
||||
"nom",
|
||||
"nom 7.1.3",
|
||||
"rangemap",
|
||||
"time",
|
||||
"weezl",
|
||||
@@ -2477,6 +2588,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"
|
||||
@@ -2579,6 +2700,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"
|
||||
@@ -2636,6 +2766,7 @@ dependencies = [
|
||||
"block2",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-graphics",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
@@ -2889,6 +3020,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"
|
||||
@@ -2970,6 +3111,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"
|
||||
@@ -3261,6 +3413,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"
|
||||
@@ -4579,6 +4743,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"
|
||||
@@ -4806,6 +4985,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"
|
||||
@@ -5151,6 +5344,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"
|
||||
@@ -5488,6 +5692,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"
|
||||
@@ -6236,6 +6510,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"
|
||||
@@ -6307,6 +6599,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"
|
||||
@@ -6506,6 +6815,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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -1655,6 +1655,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())?;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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<ProfileRechargeCenterResponse>(
|
||||
'/api/profile/recharge-center',
|
||||
{ method: 'GET' },
|
||||
'读取泥点明细失败',
|
||||
);
|
||||
}
|
||||
|
||||
export function createClientProfileRechargeOrder(productId: string) {
|
||||
return requestClientApi<CreateProfileRechargeOrderResponse>(
|
||||
'/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<ConfirmWechatProfileRechargeOrderResponse>(
|
||||
`/api/profile/recharge/orders/${encodeURIComponent(orderId)}/wechat/confirm`,
|
||||
{ method: 'POST' },
|
||||
'确认微信支付订单失败',
|
||||
);
|
||||
}
|
||||
|
||||
export function getClientProfileWalletLedger() {
|
||||
return requestClientApi<ProfileWalletLedgerResponse>(
|
||||
'/api/profile/wallet-ledger',
|
||||
{ method: 'GET' },
|
||||
'读取泥点明细失败',
|
||||
'读取泥点账单失败',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<UseHomeDraftStore>((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),
|
||||
}));
|
||||
@@ -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<void>;
|
||||
resetWalletBalance: () => void;
|
||||
};
|
||||
|
||||
const initialWalletState: Pick<
|
||||
WalletStore,
|
||||
'mudPointBalance' | 'mudPointBalanceStatus' | 'mudPointBalanceError'
|
||||
> = {
|
||||
mudPointBalance: null,
|
||||
mudPointBalanceStatus: 'idle',
|
||||
mudPointBalanceError: '',
|
||||
};
|
||||
|
||||
let refreshVersion = 0;
|
||||
let requestGeneration = 0;
|
||||
let activeRefresh: Promise<void> | null = null;
|
||||
|
||||
export const useWalletStore = create<WalletStore>((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);
|
||||
},
|
||||
}));
|
||||
@@ -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;
|
||||
@@ -2147,10 +1923,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;
|
||||
|
||||
@@ -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<string | null>(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 (
|
||||
<span className="group/attachment relative mx-0.75 inline-flex min-h-6 max-w-full items-center gap-1 rounded-[7px] border border-(--platform-subpanel-border) bg-(--platform-neutral-bg) px-1.75 align-middle text-[11px] text-(--platform-neutral-text)">
|
||||
{previewUrl ? (
|
||||
<span className="pointer-events-none absolute bottom-[calc(100%+6px)] left-1/2 z-20 hidden max-h-60 w-max max-w-60 -translate-x-1/2 overflow-hidden rounded-[7px] border border-(--platform-subpanel-border) bg-(--platform-neutral-bg) p-1 shadow-lg group-hover/attachment:block group-focus-within/attachment:block">
|
||||
<img
|
||||
className="block max-h-56 max-w-56 object-contain"
|
||||
src={previewUrl}
|
||||
alt={attachment.file.name}
|
||||
/>
|
||||
</span>
|
||||
) : null}
|
||||
{attachment.file.name}
|
||||
<button
|
||||
className="grid size-4.5 cursor-pointer place-items-center border-0 bg-transparent p-0 text-(--platform-text-soft)"
|
||||
type="button"
|
||||
aria-label={`移除附件 ${attachment.file.name}`}
|
||||
onClick={() => {
|
||||
editor.update(() => {
|
||||
$getNodeByKey(nodeKey)?.remove();
|
||||
});
|
||||
}}
|
||||
>
|
||||
<X size={12} aria-hidden="true" />
|
||||
</button>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -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<ReactNode> {
|
||||
__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 (
|
||||
<AttachmentToken attachment={this.__attachment} nodeKey={this.__key} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function $createAttachmentNode(attachment: HomeAttachmentDraft) {
|
||||
return $applyNodeReplacement(new AttachmentNode(attachment));
|
||||
}
|
||||
|
||||
export function $isAttachmentNode(
|
||||
node: LexicalNode | null | undefined,
|
||||
): node is AttachmentNode {
|
||||
return node instanceof AttachmentNode;
|
||||
}
|
||||
@@ -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<HomeAttachmentDraft[]> =
|
||||
createCommand('INSERT_HOME_ATTACHMENTS_COMMAND');
|
||||
const INSERT_CLIPBOARD_TEXT_COMMAND: LexicalCommand<string> = 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<Blob | null>((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<RichInputAreaProps, 'onChange'>) {
|
||||
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 (
|
||||
<OnChangePlugin
|
||||
onChange={(editorState) => {
|
||||
onChange(editorState);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function UploadButton() {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
ref={inputRef}
|
||||
className="launcher-file-input pointer-events-none absolute size-px opacity-0"
|
||||
type="file"
|
||||
multiple
|
||||
onChange={(event) => {
|
||||
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);
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
className="grid size-6 cursor-pointer place-items-center rounded-full border-0 bg-transparent p-0 text-(--platform-icon-text)"
|
||||
type="button"
|
||||
aria-label="上传素材"
|
||||
onClick={() => inputRef.current?.click()}
|
||||
>
|
||||
<Upload size={15} aria-hidden="true" />
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function RichInputArea(props: RichInputAreaProps) {
|
||||
return (
|
||||
<LexicalComposer
|
||||
initialConfig={{
|
||||
namespace: 'home-rich-input',
|
||||
nodes: [AttachmentNode],
|
||||
editorState: props.value ?? undefined,
|
||||
onError: (error) => {
|
||||
throw error;
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div className="grid min-h-9 gap-2">
|
||||
<RichTextPlugin
|
||||
contentEditable={
|
||||
<ContentEditable
|
||||
aria-label="创作想法"
|
||||
className="min-h-9 w-full whitespace-pre-wrap wrap-break-word border-0 bg-transparent p-0 text-[13px] text-(--platform-text-strong) outline-0"
|
||||
/>
|
||||
}
|
||||
placeholder={
|
||||
<span className="pointer-events-none absolute text-[13px] text-(--platform-text-muted)">
|
||||
{props.placeholder}
|
||||
</span>
|
||||
}
|
||||
ErrorBoundary={LexicalErrorBoundary}
|
||||
/>
|
||||
{props.children}
|
||||
</div>
|
||||
<EditorPlugins onChange={props.onChange} />
|
||||
</LexicalComposer>
|
||||
);
|
||||
}
|
||||
+86
@@ -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<number, string>)[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<string>,
|
||||
) {
|
||||
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;
|
||||
});
|
||||
}
|
||||
@@ -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<HTMLInputElement | null>(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<HTMLInputElement>,
|
||||
) {
|
||||
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<HTMLFormElement>) {
|
||||
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 (
|
||||
<>
|
||||
<div className="platform-theme platform-theme--light min-h-screen bg-(image:--platform-body-fill) text-(--platform-text-strong)">
|
||||
<section
|
||||
className={`grid min-h-93.5 content-start justify-items-center gap-3.25 px-6 pb-4.5 pt-25.75 max-[760px]:min-h-0 max-[760px]:gap-4 max-[760px]:px-3.5 max-[760px]:pb-[34px] max-[760px]:pt-[84px] ${
|
||||
className={`grid min-h-93.5 content-start justify-items-center gap-3.25 px-6 pb-4.5 pt-25.75 max-[760px]:min-h-0 max-[760px]:gap-4 max-[760px]:px-3.5 max-[760px]:pb-8.5 max-[760px]:pt-21 ${
|
||||
hasPromo ? 'pt-33.75 max-[760px]:pt-21' : ''
|
||||
}`}
|
||||
aria-label="首页输入"
|
||||
>
|
||||
<div className="flex items-center gap-3 text-center max-[760px]:flex-col max-[760px]:gap-2.5">
|
||||
<span className="grid size-7.5 shrink-0 place-items-center rounded-full bg-[#060606] text-[11px] font-bold text-white">
|
||||
<ActiveHomeModeIcon size={18} aria-hidden="true" />
|
||||
<span className="grid size-32 shrink-0 place-items-center rounded-full shadow-(--platform-profile-action-shadow)">
|
||||
<img src={BRAND_ICON} alt="logo" className="size-32" />
|
||||
</span>
|
||||
<div>
|
||||
<h1 className="m-0 text-[27px] font-[750] tracking-normal max-[760px]:text-[22px]">
|
||||
<h1 className="m-0 text-[27px] font-[750] tracking-normal text-(--platform-brand-logo-title) max-[760px]:text-[22px]">
|
||||
陶泥儿 GameAgent
|
||||
</h1>
|
||||
<p className="m-0 mt-3 text-[13px] text-[#a0a0a0]">
|
||||
<p className="m-0 mt-3 text-[13px] text-(--platform-text-soft)">
|
||||
{activeHomeMode.placeholder}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="flex w-[min(488px,calc(100vw-110px))] flex-wrap justify-center gap-[9px] max-[760px]:w-[min(100%,calc(100vw-76px))]"
|
||||
className="flex w-[min(488px,calc(100vw-110px))] flex-wrap justify-center gap-2.25 max-[760px]:w-[min(100%,calc(100vw-76px))]"
|
||||
aria-label="Agent 分类"
|
||||
>
|
||||
{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({
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<span className="text-[12px] text-(--platform-text-soft)">
|
||||
{status}
|
||||
</span>
|
||||
<form
|
||||
className="grid min-h-20 w-[min(488px,calc(100vw-110px))] grid-rows-[minmax(42px,auto)_auto_auto] rounded-[18px] border border-[#e1e4e8] bg-white px-3 pb-2 pt-[13px] shadow-[0_8px_24px_rgb(15_23_42_/_8%)] max-[760px]:w-[min(100%,calc(100vw-76px))]"
|
||||
className="grid min-h-20 w-[min(1054px,calc(100vw-122px))] grid-rows-[minmax(42px,auto)_auto] rounded-[18px] border border-(--platform-surface-border) bg-(--platform-input-fill) px-3 pb-2 pt-3.25 shadow-(--platform-panel-shadow) focus-within:bg-(--platform-input-fill-focus) focus-within:ring-4 focus-within:ring-(--platform-input-focus-ring) max-[760px]:w-[min(100%,calc(100vw-76px))]"
|
||||
onSubmit={handleHomeSubmit}
|
||||
>
|
||||
<textarea
|
||||
className="min-h-9 w-full resize-none border-0 bg-transparent p-0 text-[13px] text-[#171717] outline-0 placeholder:text-[#bababa]"
|
||||
aria-label="创作想法"
|
||||
<RichInputArea
|
||||
value={homeRichText}
|
||||
placeholder={activeHomeMode.placeholder}
|
||||
value={homePrompt}
|
||||
onChange={(event) => setHomePrompt(event.currentTarget.value)}
|
||||
/>
|
||||
{homeAttachments.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-1.5 py-2 pb-1" aria-label="附件队列">
|
||||
{homeAttachments.map((attachment) => (
|
||||
<span
|
||||
className="inline-flex min-h-6 max-w-full items-center gap-1 rounded-[7px] border border-gray-200 bg-slate-50 px-[7px] text-[11px] text-gray-700"
|
||||
key={attachment.id}
|
||||
>
|
||||
{attachment.file.name}
|
||||
<button
|
||||
className="size-[18px] cursor-pointer border-0 bg-transparent p-0 text-gray-500"
|
||||
type="button"
|
||||
aria-label={`移除附件 ${attachment.file.name}`}
|
||||
onClick={() => removeHomeAttachment(attachment.id)}
|
||||
>
|
||||
<X size={12} aria-hidden="true" />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
onChange={setHomeRichText}
|
||||
>
|
||||
<div className="grid grid-cols-[1fr_auto] items-center gap-2.5 text-[12px] text-(--platform-text-soft)">
|
||||
<UploadButton />
|
||||
<button
|
||||
className="grid size-6 cursor-pointer place-items-center rounded-full border-0 bg-(image:--platform-button-primary-fill) p-0 text-(--platform-button-primary-text) shadow-(--platform-profile-action-shadow) disabled:cursor-not-allowed disabled:opacity-55"
|
||||
type="submit"
|
||||
aria-label="开启创作"
|
||||
disabled={homeCreationBusy}
|
||||
>
|
||||
<Plus size={16} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="grid grid-cols-[auto_1fr_auto] items-center gap-2.5 text-[12px] text-[#777]">
|
||||
<input
|
||||
ref={homeAttachmentInputRef}
|
||||
className="launcher-file-input pointer-events-none absolute size-px opacity-0"
|
||||
type="file"
|
||||
multiple
|
||||
onChange={handleHomeAttachmentSelection}
|
||||
/>
|
||||
<button
|
||||
className="grid size-6 cursor-pointer place-items-center rounded-full border-0 bg-transparent p-0 text-[#666]"
|
||||
type="button"
|
||||
aria-label="上传素材"
|
||||
onClick={() => homeAttachmentInputRef.current?.click()}
|
||||
>
|
||||
<Upload size={15} aria-hidden="true" />
|
||||
</button>
|
||||
<span>{status}</span>
|
||||
<button
|
||||
className="grid size-6 cursor-pointer place-items-center rounded-full border-0 bg-[#f1f1f1] p-0 text-[#9a9a9a] disabled:cursor-not-allowed disabled:opacity-55"
|
||||
type="submit"
|
||||
aria-label="开启创作"
|
||||
disabled={homeCreationBusy}
|
||||
>
|
||||
<Plus size={16} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</RichInputArea>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section
|
||||
className="mx-auto grid w-[min(1054px,calc(100vw-122px))] gap-[14px] max-[760px]:w-[min(100%,calc(100vw-76px))]"
|
||||
className="mx-auto grid w-[min(1054px,calc(100vw-122px))] gap-3.5 max-[760px]:w-[min(100%,calc(100vw-76px))]"
|
||||
aria-label="最近项目"
|
||||
>
|
||||
<header className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 className="m-0 text-[15px] text-gray-900">最近项目</h2>
|
||||
<p className="mb-0 mt-1 text-[12px] text-[#8b8b8b]">
|
||||
<h2 className="m-0 text-[15px] text-(--platform-text-strong)">
|
||||
最近项目
|
||||
</h2>
|
||||
<p className="mb-0 mt-1 text-[12px] text-(--platform-text-soft)">
|
||||
{status}
|
||||
</p>
|
||||
</div>
|
||||
<div className="launcher-project-list-actions">
|
||||
<button
|
||||
className="cursor-pointer border-0 bg-transparent p-0 text-[12px] text-[#888]"
|
||||
className="cursor-pointer border-0 bg-transparent p-0 text-[12px] text-(--platform-warm-text)"
|
||||
type="button"
|
||||
onClick={onProjectsOpen}
|
||||
>
|
||||
@@ -271,21 +208,21 @@ export default function HomeView({
|
||||
{recentProjectRows.map((project) => (
|
||||
<article className="grid min-w-0" key={project.path}>
|
||||
<button
|
||||
className="grid h-auto w-full cursor-pointer content-start justify-items-start gap-1.5 overflow-hidden rounded-lg border border-[#e1e4e8] bg-[#f7f7f7] p-3 text-left disabled:cursor-not-allowed disabled:opacity-[0.58] max-[760px]:h-[132px]"
|
||||
className="grid h-auto w-full cursor-pointer content-start justify-items-start gap-1.5 overflow-hidden rounded-lg border border-(--platform-surface-border) bg-(image:--platform-subpanel-fill) p-3 text-left shadow-(--platform-panel-shadow) transition hover:border-(--platform-surface-hover-border) disabled:cursor-not-allowed disabled:opacity-[0.58] max-[760px]:h-33"
|
||||
type="button"
|
||||
disabled={!project.canOpen}
|
||||
onClick={() => onProjectOpen(project.path)}
|
||||
>
|
||||
<span className="grid size-[42px] place-items-center rounded-xl bg-white text-[16px] font-bold text-[#f26d2d]">
|
||||
<span className="grid size-10.5 place-items-center rounded-xl bg-(--platform-warm-bg) text-[16px] font-bold text-(--platform-warm-text)">
|
||||
{project.name.slice(0, 2)}
|
||||
</span>
|
||||
<strong className="min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-[12px] font-medium text-[#111]">
|
||||
<strong className="min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-[12px] font-medium text-(--platform-text-strong)">
|
||||
{project.name}
|
||||
</strong>
|
||||
<small className="-mt-[5px] min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-[11px] text-[#8b8b8b]">
|
||||
<small className="-mt-1.25 min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-[11px] text-(--platform-text-soft)">
|
||||
{project.path}
|
||||
</small>
|
||||
<small className="-mt-[5px] min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-[11px] text-[#8b8b8b]">
|
||||
<small className="-mt-1.25 min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-[11px] text-(--platform-text-soft)">
|
||||
{project.status}
|
||||
</small>
|
||||
</button>
|
||||
@@ -293,11 +230,17 @@ export default function HomeView({
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid justify-items-center gap-2.5 px-0 pb-8 pt-[18px]">
|
||||
<FolderKanban size={22} aria-hidden="true" />
|
||||
<strong>暂无最近项目</strong>
|
||||
<div className="grid justify-items-center gap-2.5 px-0 pb-8 pt-4.5">
|
||||
<FolderKanban
|
||||
className="text-(--platform-icon-text)"
|
||||
size={22}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<strong className="text-[13px] text-(--platform-text-strong)">
|
||||
暂无最近项目
|
||||
</strong>
|
||||
<button
|
||||
className="cursor-pointer border-0 bg-transparent p-0 text-[12px] text-[#888]"
|
||||
className="cursor-pointer border-0 bg-transparent p-0 text-[12px] text-(--platform-warm-text)"
|
||||
type="button"
|
||||
onClick={onProjectsOpen}
|
||||
>
|
||||
@@ -308,34 +251,42 @@ export default function HomeView({
|
||||
</section>
|
||||
|
||||
<section
|
||||
className="mx-auto mt-[22px] grid w-[min(1054px,calc(100vw-122px))] gap-[14px] max-[760px]:w-[min(100%,calc(100vw-76px))]"
|
||||
className="mx-auto mt-5.5 grid w-[min(1054px,calc(100vw-122px))] gap-3.5 max-[760px]:w-[min(100%,calc(100vw-76px))]"
|
||||
aria-label="灵感推荐"
|
||||
>
|
||||
<header className="flex items-center justify-start gap-3">
|
||||
<div className="grid gap-1">
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<h2 className="m-0 text-[15px] text-gray-900">灵感推荐</h2>
|
||||
<Sparkles size={15} aria-hidden="true" />
|
||||
<h2 className="m-0 text-[15px] text-(--platform-text-strong)">
|
||||
灵感推荐
|
||||
</h2>
|
||||
<Sparkles
|
||||
className="text-(--platform-warm-text)"
|
||||
size={15}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
<p className="mb-0 mt-1 text-[12px] text-[#8b8b8b]">{showcaseStatus}</p>
|
||||
<p className="mb-0 mt-1 text-[12px] text-(--platform-text-soft)">
|
||||
{showcaseStatus}
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
{showcaseResources.length > 0 ? (
|
||||
<div className="grid grid-cols-6 gap-3 max-[760px]:grid-cols-1">
|
||||
{showcaseResources.map((resource) => (
|
||||
<article
|
||||
className="grid gap-[7px] overflow-hidden rounded-lg border border-gray-200 bg-white pb-2.5"
|
||||
className="grid gap-1.75 overflow-hidden rounded-lg border border-(--platform-surface-border) bg-(image:--platform-subpanel-fill) pb-2.5 shadow-(--platform-panel-shadow)"
|
||||
key={resource.showcaseId ?? resource.resourceId}
|
||||
>
|
||||
<img
|
||||
className="aspect-square w-full object-cover bg-gray-100"
|
||||
className="aspect-square w-full object-cover bg-(--platform-warm-bg)"
|
||||
src={resource.imageSrc}
|
||||
alt={resource.label || resource.prompt || '灵感素材'}
|
||||
/>
|
||||
<strong className="min-w-0 overflow-hidden text-ellipsis whitespace-nowrap px-[9px] text-[12px] text-gray-900">
|
||||
<strong className="min-w-0 overflow-hidden text-ellipsis whitespace-nowrap px-2.25 text-[12px] text-(--platform-text-strong)">
|
||||
{resource.label || '未命名素材'}
|
||||
</strong>
|
||||
<small className="min-w-0 overflow-hidden text-ellipsis whitespace-nowrap px-[9px] text-[11px] text-gray-500">
|
||||
<small className="min-w-0 overflow-hidden text-ellipsis whitespace-nowrap px-2.25 text-[11px] text-(--platform-text-soft)">
|
||||
{resource.authorDisplayName ||
|
||||
resource.authorPublicUserCode ||
|
||||
'陶泥儿精选'}
|
||||
@@ -344,11 +295,11 @@ export default function HomeView({
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid min-h-24 place-items-center overflow-hidden rounded-lg border border-gray-200 bg-white text-[11px] text-gray-500">
|
||||
<div className="grid min-h-24 place-items-center overflow-hidden rounded-lg border border-(--platform-surface-border) bg-(image:--platform-subpanel-fill) text-[11px] text-(--platform-text-soft)">
|
||||
{showcaseStatus}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { EditorState } from 'lexical';
|
||||
import { create } from 'zustand';
|
||||
|
||||
export type HomeAttachmentDraft = {
|
||||
id: string;
|
||||
file: File;
|
||||
};
|
||||
|
||||
export type Draft = EditorState | null;
|
||||
|
||||
export type HomeAgentMode = 'game' | 'art' | 'doc';
|
||||
|
||||
export type HomeDraft = {
|
||||
mode: HomeAgentMode;
|
||||
prompt: string;
|
||||
attachments: HomeAttachmentDraft[];
|
||||
};
|
||||
|
||||
type UseHomeDraftStore = {
|
||||
mode: HomeAgentMode;
|
||||
draft: Draft;
|
||||
setMode: (mode: HomeAgentMode) => void;
|
||||
setRichText: (richText: Draft) => void;
|
||||
reset: () => void;
|
||||
};
|
||||
|
||||
const initialHomeDraft: Pick<
|
||||
UseHomeDraftStore,
|
||||
'mode' | 'draft'
|
||||
> = {
|
||||
mode: 'game',
|
||||
draft: null,
|
||||
};
|
||||
|
||||
// Keep the immutable Lexical snapshot available while the Home view is unmounted.
|
||||
export const useLauncherHomeDraftStore = create<UseHomeDraftStore>((set) => ({
|
||||
...initialHomeDraft,
|
||||
setMode: (mode) => set({ mode }),
|
||||
setRichText: (draft) => set({ draft: draft }),
|
||||
reset: () => set(initialHomeDraft),
|
||||
}));
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
listClientShowcaseResources,
|
||||
type EditorShowcaseResource,
|
||||
listClientShowcaseResources,
|
||||
} from '../../services/clientApi';
|
||||
|
||||
export function useHomeShowcase() {
|
||||
|
||||
@@ -0,0 +1,341 @@
|
||||
import { openUrl } from '@tauri-apps/plugin-opener';
|
||||
import {
|
||||
CircleHelp,
|
||||
FolderKanban,
|
||||
Home,
|
||||
Plus,
|
||||
Settings,
|
||||
User,
|
||||
} from 'lucide-react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import BRAND_ICON from '../../../../packages/shared/src/icons/taonier-product-ip.png';
|
||||
import { useWalletStore } from '../stores/useWalletStore';
|
||||
|
||||
function cx(...classNames: Array<string | false | null | undefined>) {
|
||||
return classNames.filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
const sidebarIconButtonClass =
|
||||
'grid h-12 w-12 cursor-pointer place-items-center rounded-full border-0 bg-transparent p-0 text-[15px] text-(--platform-nav-item-text) hover:bg-(--platform-nav-item-hover-fill)';
|
||||
const sidebarHelpButtonClass =
|
||||
'grid h-12 w-12 cursor-pointer place-items-center rounded-full border-0 bg-transparent p-0 text-[15px] text-(--platform-nav-item-text)';
|
||||
const sidebarMenuItemClass =
|
||||
'flex h-[30px] w-full cursor-pointer items-center justify-start gap-2 whitespace-nowrap rounded-[7px] border-0 bg-transparent px-[9px] py-0 text-left text-xs text-(--platform-text-strong) hover:bg-(--platform-nav-item-hover-fill) focus-visible:bg-(--platform-nav-item-hover-fill)';
|
||||
|
||||
export type LauncherView =
|
||||
| 'home'
|
||||
| 'projects'
|
||||
| 'agent-chat'
|
||||
| 'guide'
|
||||
| 'contact'
|
||||
| 'news'
|
||||
| 'project-development';
|
||||
|
||||
type SidebarUserInfo = {
|
||||
displayName?: string | null;
|
||||
phoneNumberMasked?: string | null;
|
||||
publicUserCode?: string | null;
|
||||
};
|
||||
|
||||
type LauncherSidebarProps = {
|
||||
activeView: LauncherView;
|
||||
currentUser: SidebarUserInfo;
|
||||
onViewChange: (view: LauncherView) => void;
|
||||
onNoticeRequest: (title: string) => void;
|
||||
onRechargeRequest: () => void;
|
||||
onRuntimeConfigOpen: () => void;
|
||||
onLogout: () => void;
|
||||
};
|
||||
|
||||
type SidebarAccountMenuProps = {
|
||||
currentUser: SidebarUserInfo;
|
||||
onClose: () => void;
|
||||
onLogout: () => void;
|
||||
onNoticeRequest: (title: string) => void;
|
||||
onRechargeRequest: () => void;
|
||||
};
|
||||
|
||||
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 SidebarAccountMenu({
|
||||
currentUser,
|
||||
onClose,
|
||||
onLogout,
|
||||
onNoticeRequest,
|
||||
onRechargeRequest,
|
||||
}: SidebarAccountMenuProps) {
|
||||
const walletBalance = useWalletStore(
|
||||
(state) => state.mudPointBalance?.totalPoints ?? null,
|
||||
);
|
||||
const walletBalanceError = useWalletStore(
|
||||
(state) => state.mudPointBalanceError || null,
|
||||
);
|
||||
const walletBalanceLabel = formatMudPoints(walletBalance);
|
||||
const walletBalanceStatus = walletBalanceError
|
||||
? walletBalanceError
|
||||
: walletBalance !== null
|
||||
? '泥点余额已读取'
|
||||
: '正在读取泥点余额';
|
||||
|
||||
function showNotice(title: string) {
|
||||
onClose();
|
||||
onNoticeRequest(title);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute -bottom-1 left-14 z-90 grid w-54.5 gap-1.5 rounded-lg border border-(--platform-subpanel-border) [background:var(--platform-subpanel-fill)] p-2.5 text-(--platform-text-strong)"
|
||||
role="menu"
|
||||
>
|
||||
<strong className="text-[13px]">
|
||||
{currentUser.displayName || '陶泥儿用户'}
|
||||
</strong>
|
||||
<span className="text-[11px] text-(--platform-text-soft)">
|
||||
{currentUser.phoneNumberMasked ||
|
||||
currentUser.publicUserCode ||
|
||||
'账号信息读取中'}
|
||||
</span>
|
||||
<div
|
||||
className="grid gap-2 rounded-xl bg-[linear-gradient(135deg,#eaa06a,#cf7a4a_58%,#b55c3b)] p-3 text-white shadow-[0_18px_38px_rgb(189_103_60_/_22%)]"
|
||||
role="group"
|
||||
aria-label="会员方案"
|
||||
>
|
||||
{/*SHOULD NOT SHOW IN THIS VERSION*/}
|
||||
{/*<span className="grid size-8 shrink-0 place-items-center rounded-[0.85rem] bg-white/25">*/}
|
||||
{/* <Crown size={16} aria-hidden="true" />*/}
|
||||
{/*</span>*/}
|
||||
{/*<span className="min-w-0">*/}
|
||||
{/* <span className="block truncate text-sm font-black">*/}
|
||||
{/* /!*{membershipLabel}*!/*/}
|
||||
{/* </span>*/}
|
||||
{/* <span className="block truncate text-[11px] font-semibold text-white/85">*/}
|
||||
{/* 会员方案*/}
|
||||
{/* </span>*/}
|
||||
{/*</span>*/}
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span
|
||||
className="min-w-0 text-[12px] font-bold"
|
||||
title={walletBalanceStatus}
|
||||
>
|
||||
剩余泥点 {walletBalanceLabel}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="grid size-7 shrink-0 place-items-center rounded-full border border-white/70 bg-white/15 text-white"
|
||||
aria-label="充值泥点"
|
||||
onClick={() => {
|
||||
onClose();
|
||||
onRechargeRequest();
|
||||
}}
|
||||
>
|
||||
<Plus size={14} aria-hidden="true" />
|
||||
</button>
|
||||
{/*<button*/}
|
||||
{/* type="button"*/}
|
||||
{/* className="h-8 rounded-full border border-white/85 bg-white/10 px-3 text-[12px] font-black text-white"*/}
|
||||
{/* onClick={() => {*/}
|
||||
{/* closeMenus();*/}
|
||||
{/* // TODO upgrade component and logic is outdated on this branch*/}
|
||||
{/* }}*/}
|
||||
{/*>*/}
|
||||
{/* 升级*/}
|
||||
{/*</button>*/}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
className={sidebarMenuItemClass}
|
||||
onClick={
|
||||
() =>{
|
||||
onClose();
|
||||
showNotice('使用指南');
|
||||
}
|
||||
// TODO doc is not ready
|
||||
}
|
||||
>
|
||||
使用指南
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
className={sidebarMenuItemClass}
|
||||
onClick={
|
||||
() => openUrl("https://kcnz41bksl1c.feishu.cn/share/base/form/shrcn1p9WpuZMDxrI5ysLLJhw7k")
|
||||
}
|
||||
>
|
||||
反馈联系
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
className={sidebarMenuItemClass}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
onLogout();
|
||||
}}
|
||||
>
|
||||
退出登录
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Sidebar({
|
||||
activeView,
|
||||
currentUser,
|
||||
onViewChange,
|
||||
onNoticeRequest,
|
||||
onRechargeRequest,
|
||||
onRuntimeConfigOpen,
|
||||
onLogout,
|
||||
}: LauncherSidebarProps) {
|
||||
const [accountMenuOpen, setAccountMenuOpen] = useState(false);
|
||||
const accountMenuRef = useRef<HTMLDivElement | null>(null);
|
||||
const helpMenuRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
function closeSidebarMenusIfOutside(target: EventTarget | null) {
|
||||
if (!(target instanceof Node)) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
accountMenuRef.current?.contains(target) ||
|
||||
helpMenuRef.current?.contains(target)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setAccountMenuOpen(false);
|
||||
}
|
||||
|
||||
function handlePointerDown(event: PointerEvent) {
|
||||
closeSidebarMenusIfOutside(event.target);
|
||||
}
|
||||
|
||||
function handleFocusIn(event: FocusEvent) {
|
||||
closeSidebarMenusIfOutside(event.target);
|
||||
}
|
||||
|
||||
function handleWindowBlur() {
|
||||
setAccountMenuOpen(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 closeMenus() {
|
||||
setAccountMenuOpen(false);
|
||||
}
|
||||
|
||||
function changeView(view: LauncherView) {
|
||||
closeMenus();
|
||||
onViewChange(view);
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className="sticky top-0 z-40 flex h-screen w-full flex-col items-center gap-4 overflow-visible border-r border-(--platform-surface-border) bg-(--platform-nav-fill) px-0 pb-4.5 pt-4">
|
||||
<div className="grid place-items-center" aria-label="陶泥儿">
|
||||
<span className="grid size-12 place-items-center rounded-full bg-(--platform-button-primary-fill) text-[10px] font-bold tracking-normal text-(--platform-button-primary-text) shadow-(--platform-profile-action-shadow)">
|
||||
<img src={BRAND_ICON} alt="brand logo" />
|
||||
</span>
|
||||
</div>
|
||||
<nav
|
||||
className="grid justify-items-center gap-3.25"
|
||||
aria-label="客户端导航"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="首页"
|
||||
title="首页"
|
||||
className={cx(
|
||||
sidebarIconButtonClass,
|
||||
activeView === 'home' &&
|
||||
'border border-(--platform-nav-active-border) bg-(image:--platform-nav-active-fill) text-(--platform-nav-item-text-active) shadow-(--platform-nav-active-shadow)',
|
||||
)}
|
||||
onClick={() => changeView('home')}
|
||||
>
|
||||
<Home size={17} aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="项目组"
|
||||
title="项目组"
|
||||
className={cx(
|
||||
sidebarIconButtonClass,
|
||||
activeView === 'projects' &&
|
||||
'border border-(--platform-nav-active-border) bg-(image:--platform-nav-active-fill) text-(--platform-nav-item-text-active) shadow-(--platform-nav-active-shadow)',
|
||||
)}
|
||||
onClick={() => changeView('projects')}
|
||||
>
|
||||
<FolderKanban size={17} aria-hidden="true" />
|
||||
</button>
|
||||
</nav>
|
||||
<div className="relative z-[60] mt-auto grid justify-items-center gap-[13px]">
|
||||
<div className="relative" ref={accountMenuRef}>
|
||||
<button
|
||||
type="button"
|
||||
className={sidebarIconButtonClass}
|
||||
onClick={() => {
|
||||
setAccountMenuOpen((open) => !open);
|
||||
}}
|
||||
aria-expanded={accountMenuOpen}
|
||||
aria-label="我的"
|
||||
title="我的"
|
||||
>
|
||||
<User size={16} aria-hidden="true" />
|
||||
</button>
|
||||
{accountMenuOpen ? (
|
||||
<SidebarAccountMenu
|
||||
currentUser={currentUser}
|
||||
onClose={closeMenus}
|
||||
onLogout={onLogout}
|
||||
onNoticeRequest={onNoticeRequest}
|
||||
onRechargeRequest={onRechargeRequest}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="relative" ref={helpMenuRef}>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="帮助"
|
||||
title="帮助"
|
||||
className={sidebarHelpButtonClass}
|
||||
onClick={() => {
|
||||
closeMenus();
|
||||
onNoticeRequest('使用指南');
|
||||
// TODO open the guide
|
||||
// TODO doc is not ready
|
||||
}}
|
||||
>
|
||||
<CircleHelp size={17} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className={sidebarIconButtonClass}
|
||||
onClick={() => {
|
||||
closeMenus();
|
||||
onRuntimeConfigOpen();
|
||||
}}
|
||||
aria-label="配置"
|
||||
title="配置"
|
||||
>
|
||||
<Settings size={16} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,182 @@
|
||||
/** @vitest-environment jsdom */
|
||||
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
const clientApi = vi.hoisted(() => ({
|
||||
getClientProfileRechargeCenter: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../src/services/clientApi', () => clientApi);
|
||||
|
||||
import { useWalletStore } from '../src/stores/useWalletStore';
|
||||
|
||||
function detailedBalance(totalPoints: number) {
|
||||
return {
|
||||
totalPoints,
|
||||
permanentPoints: Math.max(0, totalPoints - 20),
|
||||
limitedPoints: 20,
|
||||
limitedExpiresAt: '2026-08-01T00:00:00Z',
|
||||
dailyFreePoints: 0,
|
||||
dailyFreeResetPoints: 20,
|
||||
dailyFreeResetsAt: '2026-07-18T00:00:00Z',
|
||||
};
|
||||
}
|
||||
|
||||
describe('useWalletStore', () => {
|
||||
beforeEach(() => {
|
||||
useWalletStore.getState().resetWalletBalance();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test('keeps mudPointBalance as the only balance state', async () => {
|
||||
const mudPointBalance = detailedBalance(120);
|
||||
clientApi.getClientProfileRechargeCenter.mockResolvedValue({
|
||||
walletBalance: 120,
|
||||
mudPointBalance,
|
||||
});
|
||||
const { result } = renderHook(() => useWalletStore());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.onWalletBalanceMayHaveChanged();
|
||||
});
|
||||
|
||||
expect(result.current.mudPointBalance).toEqual(mudPointBalance);
|
||||
expect(result.current.mudPointBalanceStatus).toBe('ready');
|
||||
expect(result.current.mudPointBalanceError).toBe('');
|
||||
expect(Object.keys(result.current).sort()).toEqual([
|
||||
'applyWalletBalanceSnapshot',
|
||||
'mudPointBalance',
|
||||
'mudPointBalanceError',
|
||||
'mudPointBalanceStatus',
|
||||
'onWalletBalanceMayHaveChanged',
|
||||
'resetWalletBalance',
|
||||
]);
|
||||
});
|
||||
|
||||
test('recovers from a failed refresh with a recharge-center snapshot', async () => {
|
||||
clientApi.getClientProfileRechargeCenter.mockRejectedValueOnce(
|
||||
new Error('首次读取失败'),
|
||||
);
|
||||
await useWalletStore.getState().onWalletBalanceMayHaveChanged();
|
||||
|
||||
const mudPointBalance = detailedBalance(120);
|
||||
useWalletStore.getState().applyWalletBalanceSnapshot(mudPointBalance);
|
||||
|
||||
expect(useWalletStore.getState().mudPointBalance).toEqual(mudPointBalance);
|
||||
expect(useWalletStore.getState().mudPointBalanceStatus).toBe('ready');
|
||||
expect(useWalletStore.getState().mudPointBalanceError).toBe('');
|
||||
});
|
||||
|
||||
test('does not let an older refresh overwrite an applied snapshot', async () => {
|
||||
let rejectRequest: ((reason: Error) => void) | undefined;
|
||||
clientApi.getClientProfileRechargeCenter.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise((_, reject) => {
|
||||
rejectRequest = reject;
|
||||
}),
|
||||
);
|
||||
|
||||
const refresh = useWalletStore.getState().onWalletBalanceMayHaveChanged();
|
||||
const mudPointBalance = detailedBalance(160);
|
||||
useWalletStore.getState().applyWalletBalanceSnapshot(mudPointBalance);
|
||||
rejectRequest?.(new Error('过期请求失败'));
|
||||
await refresh;
|
||||
|
||||
expect(useWalletStore.getState().mudPointBalance).toEqual(mudPointBalance);
|
||||
expect(useWalletStore.getState().mudPointBalanceStatus).toBe('ready');
|
||||
expect(useWalletStore.getState().mudPointBalanceError).toBe('');
|
||||
});
|
||||
|
||||
test('rejects a response that omits mudPointBalance instead of inventing a breakdown', async () => {
|
||||
clientApi.getClientProfileRechargeCenter.mockResolvedValue({
|
||||
walletBalance: 120,
|
||||
});
|
||||
|
||||
await useWalletStore.getState().onWalletBalanceMayHaveChanged();
|
||||
|
||||
expect(useWalletStore.getState().mudPointBalance).toBeNull();
|
||||
expect(useWalletStore.getState().mudPointBalanceStatus).toBe('error');
|
||||
expect(useWalletStore.getState().mudPointBalanceError).toBe(
|
||||
'充值中心响应缺少泥点余额',
|
||||
);
|
||||
});
|
||||
|
||||
test('uses mudPointBalance from recharge-center as the authoritative snapshot', async () => {
|
||||
const mudPointBalance = detailedBalance(180);
|
||||
clientApi.getClientProfileRechargeCenter.mockResolvedValue({
|
||||
walletBalance: 999,
|
||||
mudPointBalance,
|
||||
});
|
||||
|
||||
await useWalletStore.getState().onWalletBalanceMayHaveChanged();
|
||||
|
||||
expect(useWalletStore.getState().mudPointBalance).toEqual(mudPointBalance);
|
||||
});
|
||||
|
||||
test('discards an outdated response and performs one trailing refresh', async () => {
|
||||
let resolveFirst: ((value: unknown) => void) | undefined;
|
||||
clientApi.getClientProfileRechargeCenter
|
||||
.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
resolveFirst = resolve;
|
||||
}),
|
||||
)
|
||||
.mockResolvedValueOnce({
|
||||
walletBalance: 80,
|
||||
mudPointBalance: detailedBalance(80),
|
||||
});
|
||||
|
||||
const firstRefresh =
|
||||
useWalletStore.getState().onWalletBalanceMayHaveChanged();
|
||||
const trailingRefresh =
|
||||
useWalletStore.getState().onWalletBalanceMayHaveChanged();
|
||||
resolveFirst?.({
|
||||
walletBalance: 120,
|
||||
mudPointBalance: detailedBalance(120),
|
||||
});
|
||||
await Promise.all([firstRefresh, trailingRefresh]);
|
||||
|
||||
expect(clientApi.getClientProfileRechargeCenter).toHaveBeenCalledTimes(2);
|
||||
expect(useWalletStore.getState().mudPointBalance?.totalPoints).toBe(80);
|
||||
});
|
||||
|
||||
test('retains the last balance when a refresh fails', async () => {
|
||||
clientApi.getClientProfileRechargeCenter.mockResolvedValueOnce({
|
||||
walletBalance: 90,
|
||||
mudPointBalance: detailedBalance(90),
|
||||
});
|
||||
await useWalletStore.getState().onWalletBalanceMayHaveChanged();
|
||||
clientApi.getClientProfileRechargeCenter.mockRejectedValueOnce(
|
||||
new Error('刷新失败'),
|
||||
);
|
||||
|
||||
await useWalletStore.getState().onWalletBalanceMayHaveChanged();
|
||||
|
||||
expect(useWalletStore.getState().mudPointBalance?.totalPoints).toBe(90);
|
||||
expect(useWalletStore.getState().mudPointBalanceStatus).toBe('error');
|
||||
expect(useWalletStore.getState().mudPointBalanceError).toBe('刷新失败');
|
||||
});
|
||||
|
||||
test('does not restore a late response after reset', async () => {
|
||||
let resolveRequest: ((value: unknown) => void) | undefined;
|
||||
clientApi.getClientProfileRechargeCenter.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
resolveRequest = resolve;
|
||||
}),
|
||||
);
|
||||
|
||||
const refresh = useWalletStore.getState().onWalletBalanceMayHaveChanged();
|
||||
useWalletStore.getState().resetWalletBalance();
|
||||
resolveRequest?.({
|
||||
walletBalance: 75,
|
||||
mudPointBalance: detailedBalance(75),
|
||||
});
|
||||
await refresh;
|
||||
|
||||
expect(useWalletStore.getState().mudPointBalance).toBeNull();
|
||||
expect(useWalletStore.getState().mudPointBalanceStatus).toBe('idle');
|
||||
});
|
||||
});
|
||||
@@ -273,6 +273,7 @@
|
||||
|
||||
- 背景:主站与图片画板的泥点余额入口、余额明细和充值弹窗存在不同实现,旧充值口径仍展示六档泥点、首充双倍和会员购买 / 升级入口,容易让展示、商品资格与后端余额真相发生漂移。
|
||||
- 决策:主站与图片画板统一复用公共泥点资产入口,收起态展示总额与充值,展开态只展示不限时泥点、每日免费泥点和使用详情;充值中心 BFF 继续统一下发总额、三桶余额、限时到期时间、每日免费基础重置额及下次重置时间,前端不得自行相减推算,但会员周期限时泥点仅用于存量兼容和后端结算,当前版本不在前台展示。钱包明细每次展开都重新读取充值中心 BFF,打开期间实时总额变化时继续补读;图片画板的生成扣费或退款完成后同时刷新总额与充值中心拆分。充值中心读请求必须使用 revision 门禁,支付创建、到账确认等权威响应写入时使旧读失效,避免旧响应覆盖新的每日免费 / 不限时明细。默认泥点商品收敛为 `60 / ¥6`、`180 + 90 / ¥18`、`300 + 150 / ¥30`、`680 + 340 / ¥68` 四档,`60` 档无赠送,后三档按现有 `user_id + product_id` 独立资格规则首次购买加赠 `50%`。当前版本关闭会员购买页签、会员商品和购买 / 升级入口。
|
||||
- 2026-07-17 追加:主站、图片画板与 AI 游戏创作独立 App 的泥点账单统一复用 `packages/shared/src/components/PlatformProfileWalletLedgerModal`。共享组件只依赖 `ProfileWalletLedgerResponse`,承接来源 label、金额正负号、UTC 日期、余额兜底和 loading / empty / error 展示;`/api/profile/wallet-ledger` 请求、鉴权、打开状态与重试生命周期继续由各宿主持有,不把账户事实或后端副作用下沉到共享 UI。
|
||||
- 影响范围:`profile_recharge_product_config` 默认商品、充值中心 read model、共享前后端契约、主站与图片画板泥点资产入口、充值弹窗、后台充值商品默认值。
|
||||
- 验证方式:充值与统一入口定向前端测试、`npm run typecheck`、充值商品定向 Rust 测试、`cargo check -p spacetime-module -p spacetime-client -p api-server --manifest-path server-rs/Cargo.toml`、`npm run check:encoding`、`git diff --check`。
|
||||
- 关联文档:`docs/【项目基线】当前产品与工程约束-2026-05-15.md`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`。
|
||||
@@ -4994,6 +4995,13 @@
|
||||
- 运维口径:`GENARRATIVE_SPACETIME_POOL_SIZE=8` 表示 8 条调用连接,HTTP 基础拓扑另加 1 条缓存读连接;外部生成和充值过期监听的独立窄订阅不计入该值。读模型行缓存从 8 份降为 1 份,但 SDK 空 table metadata、8 条调用 socket 和 runner 仍存在,不承诺总 RSS 等比例降为八分之一。
|
||||
- 验证方式:`cargo test -p spacetime-client --manifest-path server-rs/Cargo.toml --lib`、`cargo check -p api-server --manifest-path server-rs/Cargo.toml`、`npm run check:encoding`、`git diff --check`;发布后在 8 个调用槽暖机后对比 api-server cgroup memory / PSS,并确认 `/readyz` 与代表性 gallery、公开详情、创作入口和用户标签读取正常。
|
||||
|
||||
## 2026-07-17 主站与 AI 游戏创作客户端复用微信 Native 充值生命周期
|
||||
|
||||
- 背景:主站个人中心已具备微信 Native 二维码确认重试和 SSE 自动到账监听,AI 游戏创作客户端又在大型 `App.tsx` 中维护一套简化充值状态,关闭、迟到响应和终态语义容易继续分叉。
|
||||
- 决策:`packages/shared` 的充值组件目录新增宿主无关的 `useWechatNativeRechargeController`,通过注入确认、监听和余额快照回调统一管理二维码校验、手动确认重试、SSE 监听、终态映射和 lifecycle 隔离。主站只把 Native 分支委托给共享 controller,H5、JSAPI、小程序、登录恢复、任务和邀请码仍留在原 controller;AI 游戏创作客户端通过本地 `useRechargeController` 托管弹窗加载与固定 `wechat_native` 下单,`App.tsx` 只负责视图接线。
|
||||
- 余额边界:AI 游戏创作客户端的 `useWalletStore.mudPointBalance` 仍是唯一余额真相;充值响应只把后端完整快照写入 store,支付成功后触发完整刷新,不在客户端本地推算或增减泥点。
|
||||
- 验证方式:共享 hook Vitest、AI 游戏创作客户端充值与 Wallet Store 定向测试、主站充值渠道定向测试、两个 TypeScript 边界、`npm run check:encoding` 和 `git diff --check`。
|
||||
|
||||
## 2026-07-17 Project Supervisor 协作合同由 Runtime 强制执行
|
||||
|
||||
- 背景:V1.31 已真实证明同一父 run 可以组合 static delegate 与 isolated all-join,但模型仍可能漏掉某一类协作、只提交一个 static delegate,或在委派后由 Supervisor 自己执行项目修改。重复采样和继续堆 prompt 不能作为可靠性门禁。
|
||||
|
||||
@@ -45,6 +45,8 @@ hermes
|
||||
npm install
|
||||
```
|
||||
|
||||
仓库当前不使用 npm workspaces,根目录 `npm install` 是统一安装入口。子包新增运行时依赖时,必须同步写入根 `package.json` 和根 `package-lock.json`;不能只修改子包 `package.json`。
|
||||
|
||||
完整联调开发环境:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -101,6 +101,8 @@ V1.18 开发窗口把模式扩展为 `执行 / 聊天 / 目标`,Goal 创建和
|
||||
|
||||
以下能力清单保留 Runtime V1 的演进记录;其中“App 进程内 tokio task”“跨进程同项目写入不作为支持目标”和“恢复到当前 App 进程”的旧描述均已由 V1.1 替代。当前边界是 App / CLI 只落账并唤醒同一发布二进制的独立 Runner,append-only JSONL 使用进程内锁加 OS 文件锁,恢复继续由 Runner 接管同一 run / session。
|
||||
|
||||
客户端泥点余额统一由 `apps/ai-game-creator-shell/src/stores/useWalletStore.ts` Zustand store 持有,唯一余额真相是 `mudPointBalance`。`ProfileDashboardSummary.walletBalance` 不作为余额来源;所有可能改变余额的动作统一调用 `onWalletBalanceMayHaveChanged()` 从 recharge-center 完整刷新,不在客户端本地增减余额。账单请求、充值产品内容、下单 / 支付确认和弹窗状态留在使用它们的组件,侧栏账户菜单直接订阅该 store,不通过布局 props 传递余额。
|
||||
|
||||
Agent Runtime 负责:
|
||||
|
||||
- 当前执行边界:App / CLI 只负责 durable 入队、查询和唤醒;同一发布二进制的独立 Runner 取得项目 owner 与 per-Agent OS 锁后执行 loop。Agent DB、conversation、events、tasks、activity 和 output 的 append-only JSONL 同时使用进程内互斥与 OS 文件锁,恢复由 Runner 接管原 run / session,不再重接到当前 App 进程。
|
||||
@@ -359,7 +361,7 @@ game-project/
|
||||
- 短期记忆、长期记忆、项目黑板和角色私有记忆按授权本地项目路径读写;普通用户仍只通过聊天命令访问短期 / 长期 / 黑板记忆,角色私有记忆只在单 agent 对话和生成 loop 中按目标 agent 读取。
|
||||
- 结构化对话记录按授权本地项目路径追加 JSONL;正式聊天读取 Supervisor active Session 与只读 legacy 项目历史,开发单 Agent 对话读取对应 Agent Session。开发入口已支持本地 Session 新建、切换、归档和分叉,但不提供云端同步。
|
||||
- Agent 状态列表从 `.agent/manifest.json` 的任务 / 角色清单、`.agent/run.latest.json` / `.agent/runs/<runId>.json` 的 step、taskGraph、passPlans、lifecycleStatus,以及 `read_game_creator_agent_runtimes` 批量读取的 `.agent/runtime/agents/<taskId>.json` 和最近任务派生;v1 不新增独立状态数据库,也不承诺完整后台 runner。
|
||||
- App 启动先检查平台登录态;登录后进入同一个客户端首页,不再有面向用户的启动器 / 主窗口切换概念。首页按 `做游戏` / `做素材` / `做方案` 保存 `game` / `art` / `doc` 初始意图,发送时弹出原生目录选择,目标目录存在且非空时必须二次确认;确认后调用 `init_local_game_project` 初始化本地项目、`upload_local_asset` 导入附件,再把首条需求直接投递给 active `project-supervisor` Session 的后台 Runtime,写入最近项目并切到项目开发页。缺少 active Session 时先创建并激活;成功后清空首页草稿,取消或创建失败时在首页回显状态,首页响应式断点与应用外壳统一为 `760px`。本流程不调用 `generate_local_game_draft`、`generate_platform_art_asset`、一次性 `chat_with_game_creator_agent` 或 legacy 项目对话 append。
|
||||
- App 启动先检查平台登录态;登录后进入同一个客户端首页,不再有面向用户的启动器 / 主窗口切换概念。首页按 `做游戏` / `做素材` / `做方案` 保存 `game` / `art` / `doc` 初始意图,输入状态按文字与附件 token 的顺序保存,附件以文件名 token 内嵌在输入框中而非堆叠在下方;提交时才将 token 转为 LLM 可读的 `{1st attachment}` 引用。发送时弹出原生目录选择,目标目录存在且非空时必须二次确认;确认后调用 `init_local_game_project` 初始化本地项目、`upload_local_asset` 导入附件,再把首条需求直接投递给 active `project-supervisor` Session 的后台 Runtime,写入最近项目并切到项目开发页。缺少 active Session 时先创建并激活;成功后清空首页草稿,取消或创建失败时在首页回显状态,首页响应式断点与应用外壳统一为 `760px`。本流程不调用 `generate_local_game_draft`、`generate_platform_art_asset`、一次性 `chat_with_game_creator_agent` 或 legacy 项目对话 append。
|
||||
- 首页发送、项目组目录选择和本地文件选择必须使用 Tauri 非阻塞原生 picker,并把选择器绑定到当前 `client` 窗口;禁止在同步 command 中调用 `blocking_pick_folder` / `blocking_pick_file` 阻塞 WebView 事件循环。选择器打开期间保留首页草稿可编辑,取消后恢复“开启创作”按钮并回显“已取消”。
|
||||
- debug 构建启动后在用户 `client` 窗口之外额外打开 `developer` 窗口;该窗口用于开发者单独选择 Agent、管理对应 active/archived Session 并读取历史,用户消息和真实 Agent 回复只持久化到 `.agent/conversations/agents/<agentId>/` 下的规范 Session。普通用户窗口不得出现 `Agent 聊天` 导航、picker 或工具台入口。
|
||||
- 首页最近项目只展示最近 3 个有效项目;项目组页在同一窗口管理最近项目、打开项目、新建项目和显示目录。打开项目只读取已初始化项目并切到项目开发页,不打开第二窗口;新建项目仍沿用非空目录确认,不自动重建无效历史路径。
|
||||
@@ -567,6 +569,7 @@ game-project/
|
||||
- 聊天输入 `/publish` 只使用主窗口当前已加载的 manifest、最近 run trace、预览状态、资产来源和最近命令摘要,在聊天里生成发布准备清单,列出原型通过状态、预览、任务、资产、音频、包装说明和试玩包导出状态,并提供 `/run`、`/trace`、`/agent-resume ` 或 `/export` 草稿;该命令不调用 Tauri 读写、不启动或打开预览、不读取文件、不新增普通用户面板,真正导出仍由用户发送 `/export` 并走确认流。
|
||||
- 开发窗口可从 Agent 状态列表进入单个专业 Agent 对话并管理其 Session;正式用户项目开发页只读展示底部专业 Agent 状态和悬浮详情,不提供单 Agent 对话入口、Session 控件或调度工具台。
|
||||
- v1 普通用户登录后直接进入单窗口客户端首页;同一窗口中切换首页、项目组、指南 / 反馈和项目开发页。项目组页管理最近项目、打开项目、新建项目和显示目录;打开项目只切换到项目开发页,不调用 `open_game_creator_workspace_window` 打开第二窗口。旧 Tauri 窗口 command 只保留兼容,不进入用户主流程。
|
||||
- 2026-07-16 补充,2026-07-17 扩展:普通用户窗口顶部账户资产统一复用 `PlatformMudPointWalletEntry`;首屏余额来自 `/api/profile/dashboard`,展开时按需读取 `/api/profile/recharge-center` 中的泥点拆分。顶部资产“充值”与侧栏账户菜单“充值泥点”共用 `PlatformProfileRechargeModal`,桌面壳固定使用 `wechat_native` 下单并在弹窗内完成扫码与到账确认,成功后同步刷新顶部余额和泥点拆分。“使用详情”统一打开 `packages/shared` 中的 `PlatformProfileWalletLedgerModal`,由各宿主分别维护打开、加载、失败重试状态并读取 `/api/profile/wallet-ledger`,共享组件只承接账单来源文案、金额与日期展示及 loading / empty / error 视图,不发请求、不持有账户事实。AI 游戏创作壳的 Tailwind 入口必须显式扫描 `packages/shared/src/components`,避免共享组件的 utility 样式在构建时被遗漏。
|
||||
- 主窗口可通过系统文件管理器显示当前项目目录,也可在聊天输入 `/open-project` 走同一只读打开动作;该操作只打开本地目录,不初始化项目、不写项目文件、不切换工作区。主窗口头部显示最近 `.agent/run.latest.json` 的 run 状态摘要和当前项目预览状态,并通过“刷新状态”重新读取同一 trace,不新增状态数据库。
|
||||
- 首页、项目组页和项目开发页共用单窗口壳的全局运行时配置弹窗,读写 Tauri 应用配置目录中的 `game-creator.config.json`;正式 Supervisor 项目页缺配置时只显示错误,不自动打开该弹窗。API Key 仍不进入本地项目、trace、manifest 或聊天记录。
|
||||
- 首页发送和项目组新建都通过 Tauri 原生目录选择器选择项目路径;用户取消目录选择时不覆盖已有输入或草稿。
|
||||
|
||||
Generated
+1039
File diff suppressed because it is too large
Load Diff
@@ -158,7 +158,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/metro-runtime": "^56.0.15",
|
||||
"@lexical/react": "^0.47.0",
|
||||
"@lexical/utils": "^0.47.0",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@tauri-apps/plugin-clipboard-manager": "2.3.2",
|
||||
"@tauri-apps/plugin-opener": "~2",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"cannon-es": "^0.20.0",
|
||||
"dotenv": "^17.2.3",
|
||||
@@ -175,6 +179,7 @@
|
||||
"expo-sharing": "^56.0.18",
|
||||
"expo-status-bar": "^56.0.4",
|
||||
"jszip": "^3.10.1",
|
||||
"lexical": "^0.47.0",
|
||||
"lucide-react": "^0.546.0",
|
||||
"motion": "^12.23.24",
|
||||
"qrcode": "^1.5.4",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user