This commit is contained in:
2025-11-13 10:18:36 +08:00
parent 787906c566
commit 7b955de2f0
6 changed files with 1301 additions and 427 deletions
+380 -26
View File
@@ -11,9 +11,12 @@
"dependencies": {
"@electron-toolkit/preload": "^3.0.2",
"@electron-toolkit/utils": "^4.0.0",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"antd": "^5.28.0",
"axios": "^1.13.2",
"electron-updater": "^6.3.9",
"framer-motion": "^12.23.24",
"playwright": "^1.56.1"
},
"devDependencies": {
@@ -139,7 +142,6 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.27.1",
@@ -195,7 +197,6 @@
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz",
"integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.28.5",
@@ -229,7 +230,6 @@
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
"integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -239,7 +239,6 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
"integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/traverse": "^7.27.1",
@@ -281,7 +280,6 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -291,7 +289,6 @@
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -325,7 +322,6 @@
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
"integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.28.5"
@@ -398,7 +394,6 @@
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
"integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
@@ -413,7 +408,6 @@
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz",
"integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
@@ -432,7 +426,6 @@
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
"integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
@@ -866,18 +859,197 @@
"node": ">= 10.0.0"
}
},
"node_modules/@emotion/babel-plugin": {
"version": "11.13.5",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
"integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==",
"license": "MIT",
"dependencies": {
"@babel/helper-module-imports": "^7.16.7",
"@babel/runtime": "^7.18.3",
"@emotion/hash": "^0.9.2",
"@emotion/memoize": "^0.9.0",
"@emotion/serialize": "^1.3.3",
"babel-plugin-macros": "^3.1.0",
"convert-source-map": "^1.5.0",
"escape-string-regexp": "^4.0.0",
"find-root": "^1.1.0",
"source-map": "^0.5.7",
"stylis": "4.2.0"
}
},
"node_modules/@emotion/babel-plugin/node_modules/@emotion/hash": {
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
"integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==",
"license": "MIT"
},
"node_modules/@emotion/babel-plugin/node_modules/convert-source-map": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
"license": "MIT"
},
"node_modules/@emotion/babel-plugin/node_modules/source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
"integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/@emotion/babel-plugin/node_modules/stylis": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
"integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==",
"license": "MIT"
},
"node_modules/@emotion/cache": {
"version": "11.14.0",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz",
"integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==",
"license": "MIT",
"dependencies": {
"@emotion/memoize": "^0.9.0",
"@emotion/sheet": "^1.4.0",
"@emotion/utils": "^1.4.2",
"@emotion/weak-memoize": "^0.4.0",
"stylis": "4.2.0"
}
},
"node_modules/@emotion/cache/node_modules/stylis": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
"integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==",
"license": "MIT"
},
"node_modules/@emotion/hash": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
"integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==",
"license": "MIT"
},
"node_modules/@emotion/is-prop-valid": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz",
"integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==",
"license": "MIT",
"dependencies": {
"@emotion/memoize": "^0.9.0"
}
},
"node_modules/@emotion/memoize": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
"integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==",
"license": "MIT"
},
"node_modules/@emotion/react": {
"version": "11.14.0",
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
"@emotion/cache": "^11.14.0",
"@emotion/serialize": "^1.3.3",
"@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
"@emotion/utils": "^1.4.2",
"@emotion/weak-memoize": "^0.4.0",
"hoist-non-react-statics": "^3.3.1"
},
"peerDependencies": {
"react": ">=16.8.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@emotion/serialize": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz",
"integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==",
"license": "MIT",
"dependencies": {
"@emotion/hash": "^0.9.2",
"@emotion/memoize": "^0.9.0",
"@emotion/unitless": "^0.10.0",
"@emotion/utils": "^1.4.2",
"csstype": "^3.0.2"
}
},
"node_modules/@emotion/serialize/node_modules/@emotion/hash": {
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
"integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==",
"license": "MIT"
},
"node_modules/@emotion/serialize/node_modules/@emotion/unitless": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz",
"integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==",
"license": "MIT"
},
"node_modules/@emotion/sheet": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz",
"integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==",
"license": "MIT"
},
"node_modules/@emotion/styled": {
"version": "11.14.1",
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz",
"integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
"@emotion/is-prop-valid": "^1.3.0",
"@emotion/serialize": "^1.3.3",
"@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
"@emotion/utils": "^1.4.2"
},
"peerDependencies": {
"@emotion/react": "^11.0.0-rc.0",
"react": ">=16.8.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@emotion/unitless": {
"version": "0.7.5",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz",
"integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==",
"license": "MIT"
},
"node_modules/@emotion/use-insertion-effect-with-fallbacks": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz",
"integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==",
"license": "MIT",
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@emotion/utils": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz",
"integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==",
"license": "MIT"
},
"node_modules/@emotion/weak-memoize": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz",
"integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==",
"license": "MIT"
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
@@ -1692,7 +1864,6 @@
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
@@ -1714,7 +1885,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
@@ -1724,14 +1894,12 @@
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
@@ -2539,6 +2707,12 @@
"undici-types": "~6.21.0"
}
},
"node_modules/@types/parse-json": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
"integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==",
"license": "MIT"
},
"node_modules/@types/plist": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz",
@@ -3543,6 +3717,41 @@
"proxy-from-env": "^1.1.0"
}
},
"node_modules/babel-plugin-macros": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
"integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.5",
"cosmiconfig": "^7.0.0",
"resolve": "^1.19.0"
},
"engines": {
"node": ">=10",
"npm": ">=6"
}
},
"node_modules/babel-plugin-macros/node_modules/resolve": {
"version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
"integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
"license": "MIT",
"dependencies": {
"is-core-module": "^2.16.1",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -3968,7 +4177,6 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -4332,6 +4540,31 @@
"dev": true,
"license": "MIT"
},
"node_modules/cosmiconfig": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
"integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
"license": "MIT",
"dependencies": {
"@types/parse-json": "^4.0.0",
"import-fresh": "^3.2.1",
"parse-json": "^5.0.0",
"path-type": "^4.0.0",
"yaml": "^1.10.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/cosmiconfig/node_modules/yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"license": "ISC",
"engines": {
"node": ">= 6"
}
},
"node_modules/crc": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz",
@@ -5128,6 +5361,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/error-ex": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
"integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
"license": "MIT",
"dependencies": {
"is-arrayish": "^0.2.1"
}
},
"node_modules/es-abstract": {
"version": "1.24.0",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz",
@@ -5364,7 +5606,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -5830,6 +6071,12 @@
"node": ">=8"
}
},
"node_modules/find-root": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
"integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==",
"license": "MIT"
},
"node_modules/find-up": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@@ -5950,6 +6197,33 @@
"node": ">= 6"
}
},
"node_modules/framer-motion": {
"version": "12.23.24",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.24.tgz",
"integrity": "sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==",
"license": "MIT",
"dependencies": {
"motion-dom": "^12.23.23",
"motion-utils": "^12.23.6",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
@@ -6425,6 +6699,15 @@
"node": ">= 0.4"
}
},
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
"license": "BSD-3-Clause",
"dependencies": {
"react-is": "^16.7.0"
}
},
"node_modules/hosted-git-info": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
@@ -6581,7 +6864,6 @@
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"parent-module": "^1.0.0",
@@ -6683,6 +6965,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-arrayish": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
"integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
"license": "MIT"
},
"node_modules/is-async-function": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz",
@@ -6766,7 +7054,6 @@
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"dev": true,
"license": "MIT",
"dependencies": {
"hasown": "^2.0.2"
@@ -7193,7 +7480,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true,
"license": "MIT"
},
"node_modules/js-yaml": {
@@ -7212,7 +7498,6 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
"dev": true,
"license": "MIT",
"bin": {
"jsesc": "bin/jsesc"
@@ -7227,6 +7512,12 @@
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
"license": "MIT"
},
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
"license": "MIT"
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -7382,6 +7673,12 @@
"node": ">= 0.8.0"
}
},
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"license": "MIT"
},
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -7853,6 +8150,21 @@
"node": ">=10"
}
},
"node_modules/motion-dom": {
"version": "12.23.23",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz",
"integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==",
"license": "MIT",
"dependencies": {
"motion-utils": "^12.23.6"
}
},
"node_modules/motion-utils": {
"version": "12.23.6",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz",
"integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==",
"license": "MIT"
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -8315,7 +8627,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
"dev": true,
"license": "MIT",
"dependencies": {
"callsites": "^3.0.0"
@@ -8324,6 +8635,24 @@
"node": ">=6"
}
},
"node_modules/parse-json": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
"integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.0.0",
"error-ex": "^1.3.1",
"json-parse-even-better-errors": "^2.3.0",
"lines-and-columns": "^1.1.6"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -8358,7 +8687,6 @@
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true,
"license": "MIT"
},
"node_modules/path-scurry": {
@@ -8395,6 +8723,15 @@
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/pe-library": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz",
@@ -8420,7 +8757,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
@@ -9319,7 +9655,6 @@
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true,
"license": "MIT"
},
"node_modules/react-refresh": {
@@ -9502,7 +9837,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
@@ -10355,7 +10689,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -10607,6 +10940,12 @@
"typescript": ">=4.8.4"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -11191,6 +11530,21 @@
"dev": true,
"license": "ISC"
},
"node_modules/yaml": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
}
},
"node_modules/yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+3
View File
@@ -23,9 +23,12 @@
"dependencies": {
"@electron-toolkit/preload": "^3.0.2",
"@electron-toolkit/utils": "^4.0.0",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"antd": "^5.28.0",
"axios": "^1.13.2",
"electron-updater": "^6.3.9",
"framer-motion": "^12.23.24",
"playwright": "^1.56.1"
},
"devDependencies": {
+29 -12
View File
@@ -29,8 +29,8 @@ function createFloatingWindow(): void {
const { width } = screen.getPrimaryDisplay().workAreaSize
floatingWindow = new BrowserWindow({
width: 240,
height: 210,
width: 260,
height: 160,
x: width - 100,
y: 20,
frame: false,
@@ -92,15 +92,23 @@ function createSettingsWindow(): void {
}
function createChatWindow(initialText?: string): void {
console.log('createChatWindow called with initialText:', initialText)
// If chat window already exists, focus it and send new text if provided
if (chatWindow && !chatWindow.isDestroyed()) {
console.log('Chat window already exists, focusing and sending text')
chatWindow.focus()
if (initialText) {
chatWindow.webContents.send('set-initial-text', initialText)
// Add a small delay to ensure the renderer is ready
setTimeout(() => {
chatWindow?.webContents.send('set-initial-text', initialText)
console.log('Sent initial text to existing window:', initialText)
}, 100)
}
return
}
console.log('Creating new chat window')
chatWindow = new BrowserWindow({
width: 800,
height: 600,
@@ -122,8 +130,16 @@ function createChatWindow(initialText?: string): void {
// Send initial text after page loads
if (initialText) {
console.log('Setting up did-finish-load listener for initial text')
chatWindow.webContents.once('did-finish-load', () => {
chatWindow?.webContents.send('set-initial-text', initialText)
console.log('Chat window did-finish-load event fired')
// Add a small delay to ensure React components are mounted
setTimeout(() => {
if (chatWindow && !chatWindow.isDestroyed()) {
console.log('Sending initial text to new window:', initialText)
chatWindow.webContents.send('set-initial-text', initialText)
}
}, 200)
})
}
@@ -338,15 +354,16 @@ function registerGlobalShortcuts(): void {
const registered = globalShortcut.register(shortcut, () => {
if (floatingWindow && !floatingWindow.isDestroyed()) {
// Get clipboard text for selected text
const selectedText = clipboard.readText('selection')
const text = selectedText || clipboard.readText()
// Read clipboard content (user should copy text with Command+C first)
// We only read the main clipboard, not the selection clipboard
const text = clipboard.readText()
if (text && text.trim()) {
// Send event to renderer to show prompt
floatingWindow.webContents.send('show-text-prompt', text.trim())
floatingWindow.focus()
}
console.log('Command+K pressed, clipboard content:', text?.substring(0, 50))
// Always send the event to toggle action menu
// Pass clipboard text (empty string if clipboard is empty)
floatingWindow.webContents.send('show-text-prompt', text || '')
floatingWindow.focus()
}
})
+404 -176
View File
@@ -1,6 +1,10 @@
import React, { useState, useEffect, useRef } from 'react'
import { Input, Button, Typography, Space, Modal, message, Drawer, Skeleton } from 'antd'
import { Input, Button, Typography, Modal, message, Drawer, Skeleton } from 'antd'
import type { TextAreaRef } from 'antd/es/input/TextArea'
import { SendOutlined, CommentOutlined, ReloadOutlined, SettingOutlined } from '@ant-design/icons'
import { motion, AnimatePresence } from 'framer-motion'
import styled from '@emotion/styled'
import { lightTheme, darkTheme, Theme } from '../theme'
const { TextArea } = Input
const { Text } = Typography
@@ -35,10 +39,178 @@ interface CommentData {
replies?: CommentData[]
}
// Styled Components
const ChatContainer = styled.div<{ theme: Theme }>`
display: flex;
flex-direction: column;
height: 100vh;
background: ${(props) => props.theme.colors.background};
font-family: ${(props) => props.theme.typography.fontFamily};
transition: background ${(props) => props.theme.animation.normal} ease;
`
const Header = styled.div<{ theme: Theme }>`
display: flex;
align-items: center;
justify-content: space-between;
padding: ${(props) => props.theme.spacing.md} ${(props) => props.theme.spacing.lg};
background: ${(props) => props.theme.colors.glassBackground};
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-bottom: 1px solid ${(props) => props.theme.colors.glassBorder};
position: sticky;
top: 0;
z-index: 10;
`
const Title = styled.h1<{ theme: Theme }>`
margin: 0;
font-size: ${(props) => props.theme.typography.fontSize.xl};
font-weight: ${(props) => props.theme.typography.fontWeight.semibold};
color: ${(props) => props.theme.colors.textPrimary};
`
const HeaderActions = styled.div`
display: flex;
gap: 8px;
`
const MessagesContainer = styled.div<{ theme: Theme }>`
flex: 1;
overflow-y: auto;
padding: ${(props) => props.theme.spacing.lg};
display: flex;
flex-direction: column;
gap: ${(props) => props.theme.spacing.md};
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: ${(props) => props.theme.colors.border};
border-radius: ${(props) => props.theme.borderRadius.full};
}
&::-webkit-scrollbar-thumb:hover {
background: ${(props) => props.theme.colors.textTertiary};
}
`
const MessageBubble = styled(motion.div)<{ role: 'user' | 'assistant'; theme: Theme }>`
max-width: 70%;
padding: ${(props) => props.theme.spacing.md} ${(props) => props.theme.spacing.lg};
border-radius: ${(props) => props.theme.borderRadius.md};
align-self: ${(props) => (props.role === 'user' ? 'flex-end' : 'flex-start')};
background: ${(props) =>
props.role === 'user' ? props.theme.colors.userBubble : props.theme.colors.aiBubble};
color: ${(props) =>
props.role === 'user' ? props.theme.colors.userBubbleText : props.theme.colors.aiBubbleText};
box-shadow: ${(props) => props.theme.shadows.md};
word-wrap: break-word;
white-space: pre-wrap;
font-size: ${(props) => props.theme.typography.fontSize.base};
line-height: 1.5;
backdrop-filter: ${(props) => (props.role === 'assistant' ? 'blur(20px)' : 'none')};
-webkit-backdrop-filter: ${(props) => (props.role === 'assistant' ? 'blur(20px)' : 'none')};
border: ${(props) =>
props.role === 'assistant' ? `1px solid ${props.theme.colors.glassBorder}` : 'none'};
transition: transform ${(props) => props.theme.animation.fast} ease;
&:hover {
transform: scale(1.01);
}
`
const MessageActions = styled(motion.div)<{ theme: Theme }>`
display: flex;
gap: ${(props) => props.theme.spacing.sm};
margin-top: ${(props) => props.theme.spacing.sm};
`
const InputContainer = styled.div<{ theme: Theme }>`
padding: ${(props) => props.theme.spacing.lg};
background: ${(props) => props.theme.colors.glassBackground};
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-top: 1px solid ${(props) => props.theme.colors.glassBorder};
`
const InputWrapper = styled(motion.div)<{ theme: Theme; isMultiLine: boolean }>`
background: ${(props) => props.theme.colors.surface};
border-radius: ${(props) => props.theme.borderRadius.lg};
box-shadow: ${(props) => props.theme.shadows.lg};
overflow: hidden;
display: flex;
flex-direction: ${(props) => (props.isMultiLine ? 'column' : 'row')};
align-items: ${(props) => (props.isMultiLine ? 'flex-end' : 'flex-end')};
gap: ${(props) => props.theme.spacing.sm};
padding: ${(props) => props.theme.spacing.sm};
transition: box-shadow ${(props) => props.theme.animation.fast} ease;
&:focus-within {
box-shadow: ${(props) => props.theme.shadows.xl};
}
`
const SendButton = styled(motion.button)<{ theme: Theme; disabled: boolean }>`
width: 36px;
height: 36px;
border-radius: 50%;
border: none;
background: ${(props) =>
props.disabled
? props.theme.colors.border
: `linear-gradient(135deg, ${props.theme.colors.primary} 0%, #0051d5 100%)`};
color: white;
display: flex;
align-items: center;
justify-content: center;
cursor: ${(props) => (props.disabled ? 'not-allowed' : 'pointer')};
box-shadow: ${(props) =>
props.disabled ? 'none' : '0 2px 8px rgba(0, 122, 255, 0.3)'};
transition: all ${(props) => props.theme.animation.fast} ease;
flex-shrink: 0;
padding: 0;
outline: none;
&:hover:not(:disabled) {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0, 122, 255, 0.4);
}
&:active:not(:disabled) {
transform: scale(0.95);
}
svg {
width: 18px;
height: 18px;
transform: translateX(1px);
}
`
const EmptyState = styled.div<{ theme: Theme }>`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: ${(props) => props.theme.colors.textSecondary};
font-size: ${(props) => props.theme.typography.fontSize.lg};
`
const Chat: React.FC = () => {
const [isDarkMode, setIsDarkMode] = useState(false)
const theme = isDarkMode ? darkTheme : lightTheme
const [messages, setMessages] = useState<Message[]>([])
const [inputValue, setInputValue] = useState('')
const [isLoading, setIsLoading] = useState(false)
const [isMultiLine, setIsMultiLine] = useState(false)
const [currentArticleUrl, setCurrentArticleUrl] = useState<string>('')
const [lastAiResponse, setLastAiResponse] = useState<string>('')
const [isQrModalVisible, setIsQrModalVisible] = useState(false)
@@ -46,6 +218,7 @@ const Chat: React.FC = () => {
const [qrCodeError, setQrCodeError] = useState<string>('')
const [isConfirmModalVisible, setIsConfirmModalVisible] = useState(false)
const [confirmUsername, setConfirmUsername] = useState<string>('')
const [editableComment, setEditableComment] = useState<string>('')
const [regeneratingMessageId, setRegeneratingMessageId] = useState<string | null>(null)
const [isAccountDrawerVisible, setIsAccountDrawerVisible] = useState(false)
const [isCheckingLoginStatus, setIsCheckingLoginStatus] = useState(false)
@@ -58,13 +231,28 @@ const Chat: React.FC = () => {
const messagesContainerRef = useRef<HTMLDivElement>(null)
const shouldAutoScrollRef = useRef(true)
const isArticleRequestRef = useRef(false) // 使用 ref 而不是 state 以避免异步问题
const inputRef = useRef<TextAreaRef>(null) // Ant Design TextArea ref
// Listen for initial text from main process
useEffect(() => {
const unsubscribe = window.electron.ipcRenderer.on(
'set-initial-text',
(_: unknown, text: string) => {
setInputValue(text)
console.log('Chat: Received initial text:', text?.substring(0, 50))
if (text && text.trim()) {
setInputValue(text)
// Focus the input area after setting the value
setTimeout(() => {
if (inputRef.current) {
inputRef.current.focus()
// Move cursor to the end
const textarea = inputRef.current.resizableTextArea?.textArea
if (textarea) {
textarea.setSelectionRange(textarea.value.length, textarea.value.length)
}
}
}, 100)
}
}
)
@@ -417,9 +605,7 @@ const Chat: React.FC = () => {
setRegeneratingMessageId(messageId)
// 清空当前消息内容,显示加载状态
setMessages((prev) =>
prev.map((msg) => (msg.id === messageId ? { ...msg, content: '' } : msg))
)
setMessages((prev) => prev.map((msg) => (msg.id === messageId ? { ...msg, content: '' } : msg)))
try {
// 获取模型配置
@@ -590,6 +776,7 @@ const Chat: React.FC = () => {
const showConfirmDialog = (username?: string): void => {
console.log('showConfirmDialog called with username:', username)
setConfirmUsername(username || '当前用户')
setEditableComment(lastAiResponse) // 初始化可编辑内容
setIsConfirmModalVisible(true)
}
@@ -597,7 +784,7 @@ const Chat: React.FC = () => {
const handleConfirmOk = async (): Promise<void> => {
console.log('handleConfirmOk called')
console.log('currentArticleUrl:', currentArticleUrl)
console.log('lastAiResponse length:', lastAiResponse?.length)
console.log('editableComment length:', editableComment?.length)
setIsConfirmModalVisible(false)
message.loading({ content: '正在发送评论...', key: 'posting', duration: 0 })
@@ -606,7 +793,7 @@ const Chat: React.FC = () => {
console.log('Invoking post-comment...')
const result = await window.electron.ipcRenderer.invoke('post-comment', {
url: currentArticleUrl,
comment: lastAiResponse
comment: editableComment // 使用编辑后的内容
})
console.log('post-comment result:', result)
@@ -629,7 +816,9 @@ const Chat: React.FC = () => {
}
const handleConfirmCancel = (): void => {
console.log('handleConfirmCancel called')
setIsConfirmModalVisible(false)
setEditableComment('')
}
const handleQrModalCancel = (): void => {
@@ -728,183 +917,205 @@ const Chat: React.FC = () => {
opacity: 0;
}
}
`}</style>
<div
style={{
display: 'flex',
flexDirection: 'column',
height: '100vh',
background: '#f5f5f5'
}}
>
{/* Header */}
<div
style={{
padding: '16px 24px',
background: '#fff',
borderBottom: '1px solid #e8e8e8',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.06)',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}
>
<Text strong style={{ fontSize: '16px' }}>
AI
</Text>
<Button
icon={<SettingOutlined />}
onClick={() => {
setIsAccountDrawerVisible(true)
// 打开 Drawer 时检查登录状态
checkXiaoheiheLoginStatus()
}}
>
</Button>
</div>
{/* Messages List */}
<div
ref={messagesContainerRef}
style={{
flex: 1,
overflowY: 'auto',
padding: '24px'
}}
>
{messages.length === 0 ? (
<div
/* 自定义 TextArea 滚动条样式 - WebKit 浏览器 */
.ant-input::-webkit-scrollbar {
width: 6px;
}
.ant-input::-webkit-scrollbar-track {
background: transparent;
}
.ant-input::-webkit-scrollbar-thumb {
background: ${theme.colors.border};
border-radius: 3px;
}
.ant-input::-webkit-scrollbar-thumb:hover {
background: ${theme.colors.textTertiary};
}
/* Firefox 滚动条样式 */
.ant-input {
scrollbar-width: thin;
scrollbar-color: ${theme.colors.border} transparent;
}
`}</style>
<ChatContainer theme={theme}>
{/* Header */}
<Header theme={theme}>
<Title theme={theme}>AI </Title>
<HeaderActions>
<Button
type="text"
icon={<SettingOutlined />}
onClick={() => setIsDarkMode(!isDarkMode)}
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100%',
color: '#999'
color: theme.colors.textPrimary,
transition: `all ${theme.animation.fast} ease`
}}
/>
<Button
type="primary"
icon={<SettingOutlined />}
onClick={() => {
setIsAccountDrawerVisible(true)
checkXiaoheiheLoginStatus()
}}
>
</Button>
</HeaderActions>
</Header>
{/* Messages List */}
<MessagesContainer ref={messagesContainerRef} theme={theme}>
{messages.length === 0 ? (
<EmptyState theme={theme}>
<Text type="secondary">...</Text>
</div>
</EmptyState>
) : (
<Space direction="vertical" style={{ width: '100%' }} size="large">
{messages.map((message) => (
<div
<AnimatePresence mode="popLayout">
{messages.map((message, index) => (
<MessageBubble
key={message.id}
style={{
display: 'flex',
justifyContent: message.role === 'user' ? 'flex-end' : 'flex-start'
role={message.role}
theme={theme}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{
duration: 0.15,
delay: index * 0.03,
ease: [0.25, 0.1, 0.25, 1]
}}
whileHover={{ scale: 1.01 }}
>
<div
<Text
style={{
maxWidth: '70%',
padding: '12px 16px',
borderRadius: '12px',
background: message.role === 'user' ? '#1890ff' : '#fff',
color: message.role === 'user' ? '#fff' : '#000',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)',
wordBreak: 'break-word',
whiteSpace: 'pre-wrap'
color:
message.role === 'user'
? theme.colors.userBubbleText
: theme.colors.aiBubbleText
}}
>
<Text style={{ color: message.role === 'user' ? '#fff' : '#000' }}>
{message.content}
{message.role === 'assistant' && !message.content && isLoading && (
<span
style={{
display: 'inline-block',
width: '8px',
height: '16px',
background: '#1890ff',
marginLeft: '2px',
animation: 'blink 1s infinite'
}}
/>
)}
</Text>
<div
style={{
marginTop: '4px',
fontSize: '11px',
opacity: 0.7
}}
>
{message.timestamp.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
})}
</div>
{message.content}
{message.role === 'assistant' && !message.content && isLoading && (
<span
style={{
display: 'inline-block',
width: '8px',
height: '16px',
background: theme.colors.primary,
marginLeft: '2px',
animation: 'blink 1s infinite'
}}
/>
)}
</Text>
<div
style={{
marginTop: '4px',
fontSize: theme.typography.fontSize.xs,
opacity: 0.7
}}
>
{message.timestamp.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
})}
</div>
{/* AI 消息的操作按钮 */}
{message.role === 'assistant' && message.content && (
<div style={{ marginTop: '12px', display: 'flex', gap: '8px' }}>
{/* AI 消息的操作按钮 */}
{message.role === 'assistant' && message.content && (
<MessageActions
theme={theme}
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
>
<Button
size="small"
icon={<ReloadOutlined />}
onClick={() => handleRegenerateMessage(message.id)}
loading={regeneratingMessageId === message.id}
disabled={regeneratingMessageId !== null}
>
</Button>
{message.metadata?.type === 'article' && message.metadata.articleUrl && (
<Button
size="small"
icon={<ReloadOutlined />}
onClick={() => handleRegenerateMessage(message.id)}
loading={regeneratingMessageId === message.id}
disabled={regeneratingMessageId !== null}
type="primary"
icon={<CommentOutlined />}
onClick={() => {
setCurrentArticleUrl(message.metadata!.articleUrl!)
setLastAiResponse(message.content)
handlePostComment()
}}
>
</Button>
{message.metadata?.type === 'article' && message.metadata.articleUrl && (
<Button
size="small"
type="primary"
icon={<CommentOutlined />}
onClick={() => {
// 更新当前文章 URL 和 AI 回复
setCurrentArticleUrl(message.metadata!.articleUrl!)
setLastAiResponse(message.content)
handlePostComment()
}}
>
</Button>
)}
</div>
)}
</div>
</div>
)}
</MessageActions>
)}
</MessageBubble>
))}
</Space>
</AnimatePresence>
)}
<div ref={messagesEndRef} />
</div>
</MessagesContainer>
{/* Input Area */}
<div
style={{
padding: '16px 24px',
background: '#fff',
borderTop: '1px solid #e8e8e8',
boxShadow: '0 -2px 8px rgba(0, 0, 0, 0.06)'
}}
>
<Space direction="vertical" style={{ width: '100%' }} size="middle">
<Space.Compact style={{ width: '100%' }}>
<TextArea
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="输入消息... (Enter 发送, Shift+Enter 换行)"
autoSize={{ minRows: 1, maxRows: 4 }}
disabled={isLoading}
style={{ flex: 1 }}
/>
<Button
type="primary"
icon={<SendOutlined />}
onClick={handleSend}
disabled={!inputValue.trim() || isLoading}
style={{ height: 'auto' }}
>
</Button>
</Space.Compact>
</Space>
</div>
</div>
<InputContainer theme={theme}>
<InputWrapper
theme={theme}
isMultiLine={isMultiLine}
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.2 }}
>
<TextArea
rows={4}
ref={inputRef}
value={inputValue}
onChange={(e) => {
const value = e.target.value
setInputValue(value)
// Check if text contains newline or has multiple lines
setIsMultiLine(value.includes('\n') || value.split('\n').length > 1)
}}
onKeyPress={handleKeyPress}
placeholder="输入消息... (Enter 发送, Shift+Enter 换行)"
disabled={isLoading}
style={{
flex: 1,
border: 'none',
boxShadow: 'none',
resize: 'none',
outline: 'none'
}}
styles={{
textarea: {
scrollbarWidth: 'thin',
scrollbarColor: `${theme.colors.border} transparent`
}
}}
/>
<SendButton
theme={theme}
disabled={!inputValue.trim() || isLoading}
onClick={handleSend}
whileTap={{ scale: 0.9 }}
whileHover={{ scale: 1.05 }}
transition={{ duration: 0.15 }}
>
<SendOutlined />
</SendButton>
</InputWrapper>
</InputContainer>
</ChatContainer>
{/* QR Code Login Modal */}
<Modal
@@ -915,6 +1126,11 @@ const Chat: React.FC = () => {
okText="已完成登录"
cancelText="取消"
width={400}
styles={{
mask: { backdropFilter: 'blur(10px)', background: 'rgba(0, 0, 0, 0.45)' }
}}
transitionName="zoom"
maskTransitionName="fade"
>
<div style={{ textAlign: 'center', padding: '20px 0' }}>
<p style={{ marginBottom: 16, color: '#666' }}>使 APP </p>
@@ -952,27 +1168,35 @@ const Chat: React.FC = () => {
onCancel={handleConfirmCancel}
okText="确认发送"
cancelText="取消"
width={500}
width={600}
maskClosable={true}
keyboard={true}
styles={{
mask: { backdropFilter: 'blur(10px)', background: 'rgba(0, 0, 0, 0.45)' }
}}
>
<div>
<p>
<strong>{confirmUsername}</strong>
</p>
<p style={{ wordBreak: 'break-all', color: '#666' }}>{currentArticleUrl}</p>
<p style={{ marginTop: 16 }}></p>
<div
<p style={{ wordBreak: 'break-all', color: '#666', marginBottom: 16 }}>
{currentArticleUrl}
</p>
<p style={{ marginTop: 16, marginBottom: 8, fontWeight: 500 }}></p>
<TextArea
value={editableComment}
onChange={(e) => setEditableComment(e.target.value)}
placeholder="请输入评论内容"
autoSize={{ minRows: 4, maxRows: 12 }}
style={{
maxHeight: 200,
overflow: 'auto',
padding: 8,
background: '#f5f5f5',
borderRadius: 4,
whiteSpace: 'pre-wrap',
fontSize: 12
fontSize: 14,
borderRadius: 8,
border: '1px solid #d9d9d9'
}}
>
{lastAiResponse}
</div>
/>
<p style={{ marginTop: 8, fontSize: 12, color: '#999' }}>
{editableComment?.length || 0}
</p>
</div>
</Modal>
@@ -983,6 +1207,10 @@ const Chat: React.FC = () => {
onClose={() => setIsAccountDrawerVisible(false)}
open={isAccountDrawerVisible}
width={400}
styles={{
mask: { backdropFilter: 'blur(10px)', background: 'rgba(0, 0, 0, 0.45)' },
body: { padding: '24px' }
}}
>
<div>
<div style={{ marginBottom: 24 }}>
+223 -213
View File
@@ -1,14 +1,12 @@
import React, { useState, useRef, useEffect } from 'react'
import ContextMenu from './ContextMenu'
import { AnimatePresence, motion } from 'framer-motion'
const FloatingBall: React.FC = () => {
const [isBlinking, setIsBlinking] = useState(false)
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false)
const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 })
const [isMouseOverBall, setIsMouseOverBall] = useState(false)
const [isActionMenuOpen, setIsActionMenuOpen] = useState(false)
const [showTextPrompt, setShowTextPrompt] = useState(false)
const [selectedText, setSelectedText] = useState('')
const [selectedButtonIndex, setSelectedButtonIndex] = useState(0) // 0: 对话, 1: 设置, 2: 退出
const isDraggingRef = useRef(false)
const startPosRef = useRef({ x: 0, y: 0 })
const windowStartRef = useRef({ x: 0, y: 0 })
@@ -42,8 +40,15 @@ const FloatingBall: React.FC = () => {
'show-text-prompt',
(_: unknown, text: string) => {
setSelectedText(text)
setShowTextPrompt(true)
setIsActionMenuOpen(true)
// 切换按钮显示状态
setIsActionMenuOpen((prev) => {
const newState = !prev
if (newState) {
// When opening menu, reset to first button
setSelectedButtonIndex(0)
}
return newState
})
}
)
@@ -54,6 +59,61 @@ const FloatingBall: React.FC = () => {
}
}, [])
// Handle keyboard navigation when menu is open
useEffect(() => {
if (!isActionMenuOpen) return
const handleKeyDown = (e: KeyboardEvent): void => {
switch (e.key) {
case 'ArrowUp':
e.preventDefault()
setSelectedButtonIndex((prev) => (prev === 0 ? 2 : prev - 1))
break
case 'ArrowDown':
e.preventDefault()
setSelectedButtonIndex((prev) => (prev === 2 ? 0 : prev + 1))
break
case 'Tab':
e.preventDefault()
// Tab cycles through options
setSelectedButtonIndex((prev) => (prev === 2 ? 0 : prev + 1))
break
case 'Enter':
e.preventDefault()
executeSelectedAction()
break
case 'Escape':
e.preventDefault()
setIsActionMenuOpen(false)
setSelectedText('')
break
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [isActionMenuOpen, selectedButtonIndex, selectedText])
// Execute the action for the currently selected button
const executeSelectedAction = (): void => {
switch (selectedButtonIndex) {
case 0: // 对话
console.log('对话按钮选中 - 打开聊天窗口')
window.electron.ipcRenderer.send('open-chat', selectedText || undefined)
break
case 1: // 设置
console.log('设置按钮选中 - 打开设置窗口')
window.electron.ipcRenderer.send('open-settings')
break
case 2: // 退出
console.log('退出按钮选中 - 退出应用')
window.electron.ipcRenderer.send('quit-app')
break
}
setIsActionMenuOpen(false)
setSelectedText('')
}
const handleMouseEnterBall = (): void => {
setIsMouseOverBall(true)
// When mouse enters the ball area, stop ignoring mouse events
@@ -63,45 +123,14 @@ const FloatingBall: React.FC = () => {
const handleMouseLeaveBall = (): void => {
setIsMouseOverBall(false)
// When mouse leaves the ball area, always restore click-through
if (!isContextMenuOpen) {
window.electron.ipcRenderer.send('set-ignore-mouse-events', true, { forward: true })
}
}
const handleContextMenu = (e: React.MouseEvent): void => {
e.preventDefault()
// Show custom context menu at cursor position
setContextMenuPosition({ x: e.clientX, y: e.clientY })
setIsContextMenuOpen(true)
// Disable mouse events pass-through when menu is open
window.electron.ipcRenderer.send('set-ignore-mouse-events', false)
}
const handleCloseContextMenu = (): void => {
setIsContextMenuOpen(false)
// Re-enable mouse events pass-through when menu closes, but only if mouse is not over the ball
setTimeout(() => {
if (!isMouseOverBall) {
window.electron.ipcRenderer.send('set-ignore-mouse-events', true, { forward: true })
}
}, 50)
}
const handleSettingsClick = (): void => {
// Send message to main process to open settings
window.electron.ipcRenderer.send('open-settings')
}
const handleQuitClick = (): void => {
// Send message to main process to quit app
window.electron.ipcRenderer.send('quit-app')
window.electron.ipcRenderer.send('set-ignore-mouse-events', true, { forward: true })
}
const handleMouseDown = async (e: React.MouseEvent): Promise<void> => {
e.preventDefault()
e.stopPropagation()
// Ignore right click for context menu
// Ignore right click
if (e.button === 2) {
return
}
@@ -196,23 +225,6 @@ const FloatingBall: React.FC = () => {
}
}
`}</style>
<ContextMenu
isOpen={isContextMenuOpen}
position={contextMenuPosition}
onClose={handleCloseContextMenu}
onSettings={handleSettingsClick}
onQuit={handleQuitClick}
onMouseEnter={() => {
// Keep mouse events enabled when hovering over menu
window.electron.ipcRenderer.send('set-ignore-mouse-events', false)
}}
onMouseLeave={() => {
// When mouse leaves menu, restore click-through
if (!isMouseOverBall) {
window.electron.ipcRenderer.send('set-ignore-mouse-events', true, { forward: true })
}
}}
/>
<div
style={{
width: '100%',
@@ -224,202 +236,201 @@ const FloatingBall: React.FC = () => {
pointerEvents: 'none'
}}
>
{/* Text Prompt */}
{showTextPrompt && selectedText && (
<div
onMouseEnter={() => {
window.electron.ipcRenderer.send('set-ignore-mouse-events', false)
}}
onMouseDown={(e) => {
e.stopPropagation()
}}
style={{
position: 'absolute',
top: 'calc(50% - 100px)',
left: '50%',
transform: 'translateX(-50%)',
background: 'rgba(33, 150, 243, 0.95)',
color: 'white',
padding: '8px 16px',
borderRadius: '20px',
fontSize: '12px',
fontWeight: 500,
whiteSpace: 'nowrap',
boxShadow: '0 2px 12px rgba(33, 150, 243, 0.4)',
pointerEvents: 'auto',
animation: 'fadeInDown 0.3s ease-out',
zIndex: 10,
display: 'flex',
alignItems: 'center',
gap: '8px'
}}
>
<span></span>
<button
onMouseDown={(e) => {
e.preventDefault()
e.stopPropagation()
}}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
console.log('Close button clicked')
setShowTextPrompt(false)
setSelectedText('')
setIsActionMenuOpen(false)
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.4)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.2)'
}}
style={{
background: 'rgba(255, 255, 255, 0.2)',
border: 'none',
borderRadius: '50%',
width: '16px',
height: '16px',
cursor: 'pointer',
fontSize: '10px',
color: 'white',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
padding: 0,
flexShrink: 0,
transition: 'background 0.2s ease'
}}
>
×
</button>
</div>
)}
{/* Action Menu Items */}
{isActionMenuOpen && (
<>
{/* Action Item 1 - Top Left */}
<div
style={{
<AnimatePresence>
{isActionMenuOpen && (
<>
{/* Action Item 1 - 对话 (Top Left) */}
<motion.div
initial={{ opacity: 0, scale: 0.3, x: 30, y: 30 }}
animate={{
opacity: 1,
scale: selectedButtonIndex === 0 ? 1.1 : 1,
x: 0,
y: 0
}}
exit={{ opacity: 0, scale: 0.3, x: 30, y: 30 }}
transition={{ duration: 0.2, ease: 'easeOut' }}
style={{
position: 'absolute',
left: 'calc(50% - 90px)',
top: 'calc(50% - 60px)',
width: '40px',
height: '40px',
left: 'calc(50% - 105px)',
top: 'calc(50% - 75px)',
width: '44px',
height: '44px',
borderRadius: '50%',
background: 'linear-gradient(135deg, #4CAF50 0%, #45a049 100%)',
background: selectedButtonIndex === 0
? 'rgba(0, 122, 255, 0.15)'
: 'rgba(255, 255, 255, 0.95)',
backdropFilter: 'blur(20px)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
boxShadow: '0 2px 8px rgba(76, 175, 80, 0.4)',
boxShadow: selectedButtonIndex === 0
? '0 4px 16px rgba(0, 122, 255, 0.3)'
: '0 2px 8px rgba(0, 0, 0, 0.1)',
pointerEvents: 'auto',
animation: 'slideIn1 0.3s ease-out',
transition: 'transform 0.2s ease, box-shadow 0.2s ease'
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
border: selectedButtonIndex === 0
? '2px solid #007AFF'
: '0.5px solid rgba(0, 0, 0, 0.04)'
}}
onClick={() => {
console.log('Action 1 clicked - Opening chat window')
// Open chat window with selected text
console.log('对话按钮点击 - 打开聊天窗口')
window.electron.ipcRenderer.send('open-chat', selectedText || undefined)
setIsActionMenuOpen(false)
setShowTextPrompt(false)
setSelectedText('')
}}
onMouseEnter={(e) => {
setSelectedButtonIndex(0)
window.electron.ipcRenderer.send('set-ignore-mouse-events', false)
e.currentTarget.style.transform = 'scale(1.1)'
e.currentTarget.style.boxShadow = '0 4px 12px rgba(76, 175, 80, 0.6)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'scale(1)'
e.currentTarget.style.boxShadow = '0 2px 8px rgba(76, 175, 80, 0.4)'
onMouseLeave={() => {
window.electron.ipcRenderer.send('set-ignore-mouse-events', true, { forward: true })
}}
>
<span style={{ fontSize: '20px', color: 'white' }}></span>
</div>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="#007AFF"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" />
</svg>
</motion.div>
{/* Action Item 2 - Middle Left */}
<div
{/* Action Item 2 - 设置 (Middle Left) */}
<motion.div
initial={{ opacity: 0, scale: 0.3, x: 40 }}
animate={{
opacity: 1,
scale: selectedButtonIndex === 1 ? 1.1 : 1,
x: 0
}}
exit={{ opacity: 0, scale: 0.3, x: 40 }}
transition={{ duration: 0.2, ease: 'easeOut', delay: 0.05 }}
style={{
position: 'absolute',
left: 'calc(50% - 100px)',
top: 'calc(50% - 20px)',
width: '40px',
height: '40px',
left: 'calc(50% - 120px)',
top: 'calc(50% - 22px)',
width: '44px',
height: '44px',
borderRadius: '50%',
background: 'linear-gradient(135deg, #FF9800 0%, #F57C00 100%)',
background: selectedButtonIndex === 1
? 'rgba(142, 142, 147, 0.15)'
: 'rgba(255, 255, 255, 0.95)',
backdropFilter: 'blur(20px)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
boxShadow: '0 2px 8px rgba(255, 152, 0, 0.4)',
boxShadow: selectedButtonIndex === 1
? '0 4px 16px rgba(142, 142, 147, 0.3)'
: '0 2px 8px rgba(0, 0, 0, 0.1)',
pointerEvents: 'auto',
animation: 'slideIn2 0.3s ease-out',
transition: 'transform 0.2s ease, box-shadow 0.2s ease'
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
border: selectedButtonIndex === 1
? '2px solid #8E8E93'
: '0.5px solid rgba(0, 0, 0, 0.04)'
}}
onClick={() => {
console.log('Action 2 clicked')
if (selectedText) {
console.log('Selected text:', selectedText)
}
setIsActionMenuOpen(false)
setShowTextPrompt(false)
setSelectedText('')
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = 'scale(1.1)'
e.currentTarget.style.boxShadow = '0 4px 12px rgba(255, 152, 0, 0.6)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'scale(1)'
e.currentTarget.style.boxShadow = '0 2px 8px rgba(255, 152, 0, 0.4)'
}}
>
<span style={{ fontSize: '20px', color: 'white' }}></span>
</div>
{/* Action Item 3 - Bottom Left */}
<div
style={{
position: 'absolute',
left: 'calc(50% - 90px)',
top: 'calc(50% + 20px)',
width: '40px',
height: '40px',
borderRadius: '50%',
background: 'linear-gradient(135deg, #f44336 0%, #d32f2f 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
boxShadow: '0 2px 8px rgba(244, 67, 54, 0.4)',
pointerEvents: 'auto',
animation: 'slideIn3 0.3s ease-out',
transition: 'transform 0.2s ease, box-shadow 0.2s ease'
}}
onClick={() => {
console.log('Action 3 clicked - Opening settings window')
// Open settings window
console.log('设置按钮点击 - 打开设置窗口')
window.electron.ipcRenderer.send('open-settings')
setIsActionMenuOpen(false)
setShowTextPrompt(false)
setSelectedText('')
}}
onMouseEnter={(e) => {
onMouseEnter={() => {
setSelectedButtonIndex(1)
window.electron.ipcRenderer.send('set-ignore-mouse-events', false)
e.currentTarget.style.transform = 'scale(1.1)'
e.currentTarget.style.boxShadow = '0 4px 12px rgba(244, 67, 54, 0.6)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'scale(1)'
e.currentTarget.style.boxShadow = '0 2px 8px rgba(244, 67, 54, 0.4)'
onMouseLeave={() => {
window.electron.ipcRenderer.send('set-ignore-mouse-events', true, { forward: true })
}}
>
<span style={{ fontSize: '20px', color: 'white' }}></span>
</div>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="#8E8E93"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="12" cy="12" r="3" />
<path d="M12 1v6M12 17v6M4.22 4.22l4.24 4.24M15.54 15.54l4.24 4.24M1 12h6M17 12h6M4.22 19.78l4.24-4.24M15.54 8.46l4.24-4.24" />
</svg>
</motion.div>
{/* Action Item 3 - 退出 (Bottom Left) */}
<motion.div
initial={{ opacity: 0, scale: 0.3, x: 30, y: -30 }}
animate={{
opacity: 1,
scale: selectedButtonIndex === 2 ? 1.1 : 1,
x: 0,
y: 0
}}
exit={{ opacity: 0, scale: 0.3, x: 30, y: -30 }}
transition={{ duration: 0.2, ease: 'easeOut', delay: 0.1 }}
style={{
position: 'absolute',
left: 'calc(50% - 105px)',
top: 'calc(50% + 31px)',
width: '44px',
height: '44px',
borderRadius: '50%',
background: selectedButtonIndex === 2
? 'rgba(255, 59, 48, 0.15)'
: 'rgba(255, 255, 255, 0.95)',
backdropFilter: 'blur(20px)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
boxShadow: selectedButtonIndex === 2
? '0 4px 16px rgba(255, 59, 48, 0.3)'
: '0 2px 8px rgba(0, 0, 0, 0.1)',
pointerEvents: 'auto',
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
border: selectedButtonIndex === 2
? '2px solid #FF3B30'
: '0.5px solid rgba(0, 0, 0, 0.04)'
}}
onClick={() => {
console.log('退出按钮点击 - 退出应用')
window.electron.ipcRenderer.send('quit-app')
setIsActionMenuOpen(false)
setSelectedText('')
}}
onMouseEnter={() => {
setSelectedButtonIndex(2)
window.electron.ipcRenderer.send('set-ignore-mouse-events', false)
}}
onMouseLeave={() => {
window.electron.ipcRenderer.send('set-ignore-mouse-events', true, { forward: true })
}}
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="#FF3B30"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="15 18 9 12 15 6" />
</svg>
</motion.div>
</>
)}
</AnimatePresence>
{/* Robot Ball Container */}
<div
@@ -433,7 +444,6 @@ const FloatingBall: React.FC = () => {
{/* Robot Ball */}
<div
onMouseDown={handleMouseDown}
onContextMenu={handleContextMenu}
onMouseEnter={(e) => {
handleMouseEnterBall()
e.currentTarget.style.boxShadow = '0 4px 14px rgba(33, 150, 243, 0.6)'
+262
View File
@@ -0,0 +1,262 @@
// Apple-inspired design system
export interface Theme {
colors: {
// Surfaces
background: string
surface: string
surfaceElevated: string
surfaceHover: string
// Text
textPrimary: string
textSecondary: string
textTertiary: string
// Brand
primary: string
primaryHover: string
primaryActive: string
// Message bubbles
userBubble: string
userBubbleText: string
aiBubble: string
aiBubbleText: string
// Borders
border: string
borderLight: string
// States
success: string
error: string
warning: string
// Glass effect
glassBackground: string
glassBorder: string
}
shadows: {
sm: string
md: string
lg: string
xl: string
}
spacing: {
xs: string
sm: string
md: string
lg: string
xl: string
xxl: string
}
borderRadius: {
sm: string
md: string
lg: string
xl: string
full: string
}
typography: {
fontFamily: string
fontSize: {
xs: string
sm: string
base: string
lg: string
xl: string
xxl: string
}
fontWeight: {
normal: number
medium: number
semibold: number
bold: number
}
}
animation: {
fast: string
normal: string
slow: string
}
}
export const lightTheme: Theme = {
colors: {
// Surfaces - Apple's clean light grays
background: '#f5f5f7',
surface: '#ffffff',
surfaceElevated: '#ffffff',
surfaceHover: '#f9f9f9',
// Text - Apple's neutral grays
textPrimary: '#1d1d1f',
textSecondary: '#6e6e73',
textTertiary: '#86868b',
// Brand - Apple's blue accent
primary: '#007aff',
primaryHover: '#0051d5',
primaryActive: '#004ecb',
// Message bubbles
userBubble: 'linear-gradient(135deg, #007aff 0%, #0051d5 100%)',
userBubbleText: '#ffffff',
aiBubble: 'rgba(255, 255, 255, 0.7)',
aiBubbleText: '#1d1d1f',
// Borders
border: '#d2d2d7',
borderLight: '#e5e5ea',
// States
success: '#34c759',
error: '#ff3b30',
warning: '#ff9500',
// Glass effect
glassBackground: 'rgba(255, 255, 255, 0.7)',
glassBorder: 'rgba(255, 255, 255, 0.18)',
},
shadows: {
sm: '0 1px 3px rgba(0, 0, 0, 0.04), 0 1px 2px rgba(0, 0, 0, 0.02)',
md: '0 4px 6px rgba(0, 0, 0, 0.04), 0 2px 4px rgba(0, 0, 0, 0.02)',
lg: '0 10px 15px rgba(0, 0, 0, 0.06), 0 4px 6px rgba(0, 0, 0, 0.03)',
xl: '0 20px 25px rgba(0, 0, 0, 0.08), 0 10px 10px rgba(0, 0, 0, 0.04)',
},
spacing: {
xs: '4px',
sm: '8px',
md: '16px',
lg: '24px',
xl: '32px',
xxl: '48px',
},
borderRadius: {
sm: '6px',
md: '12px',
lg: '16px',
xl: '24px',
full: '9999px',
},
typography: {
fontFamily: '-apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", "Helvetica Neue", Arial, sans-serif',
fontSize: {
xs: '12px',
sm: '14px',
base: '16px',
lg: '18px',
xl: '20px',
xxl: '24px',
},
fontWeight: {
normal: 400,
medium: 500,
semibold: 600,
bold: 700,
},
},
animation: {
fast: '150ms',
normal: '250ms',
slow: '350ms',
},
}
export const darkTheme: Theme = {
colors: {
// Surfaces - Apple's dark mode grays
background: '#000000',
surface: '#1c1c1e',
surfaceElevated: '#2c2c2e',
surfaceHover: '#3a3a3c',
// Text - Apple's light grays for dark mode
textPrimary: '#f5f5f7',
textSecondary: '#98989d',
textTertiary: '#636366',
// Brand - Apple's blue accent (slightly brighter for dark mode)
primary: '#0a84ff',
primaryHover: '#409cff',
primaryActive: '#0077ed',
// Message bubbles
userBubble: 'linear-gradient(135deg, #0a84ff 0%, #0077ed 100%)',
userBubbleText: '#ffffff',
aiBubble: 'rgba(44, 44, 46, 0.9)',
aiBubbleText: '#f5f5f7',
// Borders
border: '#38383a',
borderLight: '#48484a',
// States
success: '#32d74b',
error: '#ff453a',
warning: '#ff9f0a',
// Glass effect
glassBackground: 'rgba(44, 44, 46, 0.7)',
glassBorder: 'rgba(255, 255, 255, 0.1)',
},
shadows: {
sm: '0 1px 3px rgba(0, 0, 0, 0.3), 0 1px 2px rgba(0, 0, 0, 0.2)',
md: '0 4px 6px rgba(0, 0, 0, 0.3), 0 2px 4px rgba(0, 0, 0, 0.2)',
lg: '0 10px 15px rgba(0, 0, 0, 0.4), 0 4px 6px rgba(0, 0, 0, 0.3)',
xl: '0 20px 25px rgba(0, 0, 0, 0.5), 0 10px 10px rgba(0, 0, 0, 0.4)',
},
spacing: {
xs: '4px',
sm: '8px',
md: '16px',
lg: '24px',
xl: '32px',
xxl: '48px',
},
borderRadius: {
sm: '6px',
md: '12px',
lg: '16px',
xl: '24px',
full: '9999px',
},
typography: {
fontFamily: '-apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", "Helvetica Neue", Arial, sans-serif',
fontSize: {
xs: '12px',
sm: '14px',
base: '16px',
lg: '18px',
xl: '20px',
xxl: '24px',
},
fontWeight: {
normal: 400,
medium: 500,
semibold: 600,
bold: 700,
},
},
animation: {
fast: '150ms',
normal: '250ms',
slow: '350ms',
},
}