mirror of
https://github.com/infinilabs/coco-app.git
synced 2026-07-10 04:25:35 +02:00
feat: add in-app document preview webview
This commit is contained in:
@@ -53,6 +53,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"docx-preview": "^0.3.7",
|
||||
"dotenv": "^16.5.0",
|
||||
"filesize": "^10.1.6",
|
||||
"i18next": "^23.16.8",
|
||||
@@ -62,6 +63,7 @@
|
||||
"mdast-util-gfm-autolink-literal": "2.0.0",
|
||||
"mermaid": "^11.6.0",
|
||||
"nanoid": "^5.1.5",
|
||||
"pptx-preview": "^1.0.7",
|
||||
"react": "^18.3.1",
|
||||
"react-day-picker": "^9.13.0",
|
||||
"react-dom": "^18.3.1",
|
||||
|
||||
128
pnpm-lock.yaml
generated
128
pnpm-lock.yaml
generated
@@ -110,6 +110,9 @@ importers:
|
||||
dayjs:
|
||||
specifier: ^1.11.13
|
||||
version: 1.11.18
|
||||
docx-preview:
|
||||
specifier: ^0.3.7
|
||||
version: 0.3.7
|
||||
dotenv:
|
||||
specifier: ^16.5.0
|
||||
version: 16.6.1
|
||||
@@ -137,6 +140,9 @@ importers:
|
||||
nanoid:
|
||||
specifier: ^5.1.5
|
||||
version: 5.1.6
|
||||
pptx-preview:
|
||||
specifier: ^1.0.7
|
||||
version: 1.0.7
|
||||
react:
|
||||
specifier: ^18.3.1
|
||||
version: 18.3.1
|
||||
@@ -2364,6 +2370,9 @@ packages:
|
||||
convert-source-map@2.0.0:
|
||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||
|
||||
core-util-is@1.0.3:
|
||||
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
|
||||
|
||||
cose-base@1.0.3:
|
||||
resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==}
|
||||
|
||||
@@ -2627,6 +2636,9 @@ packages:
|
||||
devlop@1.1.0:
|
||||
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
|
||||
|
||||
docx-preview@0.3.7:
|
||||
resolution: {integrity: sha512-Lav69CTA/IYZPJTsKH7oYeoZjyg96N0wEJMNslGJnZJ+dMUZK85Lt5ASC79yUlD48ecWjuv+rkcmFt6EVPV0Xg==}
|
||||
|
||||
dompurify@3.2.5:
|
||||
resolution: {integrity: sha512-mLPd29uoRe9HpvwP2TxClGQBzGXeEC/we/q+bFlmPPmj2p2Ugl3r6ATu/UU1v77DXNcehiBg9zsr1dREyA/dJQ==}
|
||||
|
||||
@@ -2645,6 +2657,9 @@ packages:
|
||||
eastasianwidth@0.2.0:
|
||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||
|
||||
echarts@5.6.0:
|
||||
resolution: {integrity: sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==}
|
||||
|
||||
electron-to-chromium@1.5.151:
|
||||
resolution: {integrity: sha512-Rl6uugut2l9sLojjS4H4SAr3A4IgACMLgpuEMPYCVcKydzfyPrn5absNRju38IhQOf/NwjJY8OGWjlteqYeBCA==}
|
||||
|
||||
@@ -2983,6 +2998,9 @@ packages:
|
||||
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
immediate@3.0.6:
|
||||
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
|
||||
|
||||
immer@10.2.0:
|
||||
resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==}
|
||||
|
||||
@@ -3128,6 +3146,9 @@ packages:
|
||||
resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
isarray@1.0.0:
|
||||
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
|
||||
|
||||
isexe@2.0.0:
|
||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||
|
||||
@@ -3173,6 +3194,9 @@ packages:
|
||||
engines: {node: '>=6'}
|
||||
hasBin: true
|
||||
|
||||
jszip@3.10.1:
|
||||
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
|
||||
|
||||
katex@0.16.22:
|
||||
resolution: {integrity: sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==}
|
||||
hasBin: true
|
||||
@@ -3201,6 +3225,9 @@ packages:
|
||||
layout-base@2.0.1:
|
||||
resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==}
|
||||
|
||||
lie@3.3.0:
|
||||
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
|
||||
|
||||
lightningcss-android-arm64@1.30.2:
|
||||
resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
@@ -3658,6 +3685,9 @@ packages:
|
||||
package-manager-detector@1.5.0:
|
||||
resolution: {integrity: sha512-uBj69dVlYe/+wxj8JOpr97XfsxH/eumMt6HqjNTmJDf/6NO9s+0uxeOneIz3AsPt2m6y9PqzDzd3ATcU17MNfw==}
|
||||
|
||||
pako@1.0.11:
|
||||
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
|
||||
|
||||
parent-module@1.0.1:
|
||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -3768,10 +3798,16 @@ packages:
|
||||
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
pptx-preview@1.0.7:
|
||||
resolution: {integrity: sha512-YByocJuyxAR4YB4Q3+VAxdLfEvA5LojG1gAJsx2Mw0QU5FJPps/2fkJOupJ6oBbA+KdWRpuAk6G6T34rKCHVxw==}
|
||||
|
||||
pretty-ms@9.2.0:
|
||||
resolution: {integrity: sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
process-nextick-args@2.0.1:
|
||||
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
||||
|
||||
property-information@7.1.0:
|
||||
resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
|
||||
|
||||
@@ -3916,6 +3952,9 @@ packages:
|
||||
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
readable-stream@2.3.8:
|
||||
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
|
||||
|
||||
readdirp@4.1.2:
|
||||
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
||||
engines: {node: '>= 14.18.0'}
|
||||
@@ -4020,6 +4059,9 @@ packages:
|
||||
rxjs@7.8.2:
|
||||
resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
|
||||
|
||||
safe-buffer@5.1.2:
|
||||
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
|
||||
|
||||
safer-buffer@2.1.2:
|
||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||
|
||||
@@ -4044,6 +4086,9 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
setimmediate@1.0.5:
|
||||
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
|
||||
|
||||
shebang-command@2.0.0:
|
||||
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -4115,6 +4160,9 @@ packages:
|
||||
resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
string_decoder@1.1.1:
|
||||
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
|
||||
|
||||
stringify-entities@4.0.4:
|
||||
resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
|
||||
|
||||
@@ -4239,6 +4287,9 @@ packages:
|
||||
ts-interface-checker@0.1.13:
|
||||
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
|
||||
|
||||
tslib@2.3.0:
|
||||
resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
|
||||
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
@@ -4379,6 +4430,14 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
|
||||
uuid@10.0.0:
|
||||
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
|
||||
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
|
||||
hasBin: true
|
||||
|
||||
uuid@11.1.0:
|
||||
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
|
||||
hasBin: true
|
||||
@@ -4524,6 +4583,9 @@ packages:
|
||||
resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
zrender@5.6.1:
|
||||
resolution: {integrity: sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==}
|
||||
|
||||
zustand@5.0.8:
|
||||
resolution: {integrity: sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
@@ -6440,6 +6502,8 @@ snapshots:
|
||||
|
||||
convert-source-map@2.0.0: {}
|
||||
|
||||
core-util-is@1.0.3: {}
|
||||
|
||||
cose-base@1.0.3:
|
||||
dependencies:
|
||||
layout-base: 1.0.2
|
||||
@@ -6709,6 +6773,10 @@ snapshots:
|
||||
dependencies:
|
||||
dequal: 2.0.3
|
||||
|
||||
docx-preview@0.3.7:
|
||||
dependencies:
|
||||
jszip: 3.10.1
|
||||
|
||||
dompurify@3.2.5:
|
||||
optionalDependencies:
|
||||
'@types/trusted-types': 2.0.7
|
||||
@@ -6727,6 +6795,11 @@ snapshots:
|
||||
|
||||
eastasianwidth@0.2.0: {}
|
||||
|
||||
echarts@5.6.0:
|
||||
dependencies:
|
||||
tslib: 2.3.0
|
||||
zrender: 5.6.1
|
||||
|
||||
electron-to-chromium@1.5.151: {}
|
||||
|
||||
electron-to-chromium@1.5.241: {}
|
||||
@@ -7169,6 +7242,8 @@ snapshots:
|
||||
|
||||
ignore@5.3.2: {}
|
||||
|
||||
immediate@3.0.6: {}
|
||||
|
||||
immer@10.2.0: {}
|
||||
|
||||
immutable@5.1.2: {}
|
||||
@@ -7279,6 +7354,8 @@ snapshots:
|
||||
dependencies:
|
||||
is-inside-container: 1.0.0
|
||||
|
||||
isarray@1.0.0: {}
|
||||
|
||||
isexe@2.0.0: {}
|
||||
|
||||
issue-parser@7.0.1:
|
||||
@@ -7315,6 +7392,13 @@ snapshots:
|
||||
|
||||
json5@2.2.3: {}
|
||||
|
||||
jszip@3.10.1:
|
||||
dependencies:
|
||||
lie: 3.3.0
|
||||
pako: 1.0.11
|
||||
readable-stream: 2.3.8
|
||||
setimmediate: 1.0.5
|
||||
|
||||
katex@0.16.22:
|
||||
dependencies:
|
||||
commander: 8.3.0
|
||||
@@ -7341,6 +7425,10 @@ snapshots:
|
||||
|
||||
layout-base@2.0.1: {}
|
||||
|
||||
lie@3.3.0:
|
||||
dependencies:
|
||||
immediate: 3.0.6
|
||||
|
||||
lightningcss-android-arm64@1.30.2:
|
||||
optional: true
|
||||
|
||||
@@ -8010,6 +8098,8 @@ snapshots:
|
||||
|
||||
package-manager-detector@1.5.0: {}
|
||||
|
||||
pako@1.0.11: {}
|
||||
|
||||
parent-module@1.0.1:
|
||||
dependencies:
|
||||
callsites: 3.1.0
|
||||
@@ -8113,10 +8203,20 @@ snapshots:
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
pptx-preview@1.0.7:
|
||||
dependencies:
|
||||
echarts: 5.6.0
|
||||
jszip: 3.10.1
|
||||
lodash: 4.17.21
|
||||
tslib: 2.8.1
|
||||
uuid: 10.0.0
|
||||
|
||||
pretty-ms@9.2.0:
|
||||
dependencies:
|
||||
parse-ms: 4.0.0
|
||||
|
||||
process-nextick-args@2.0.1: {}
|
||||
|
||||
property-information@7.1.0: {}
|
||||
|
||||
proto-list@1.2.4: {}
|
||||
@@ -8270,6 +8370,16 @@ snapshots:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
|
||||
readable-stream@2.3.8:
|
||||
dependencies:
|
||||
core-util-is: 1.0.3
|
||||
inherits: 2.0.4
|
||||
isarray: 1.0.0
|
||||
process-nextick-args: 2.0.1
|
||||
safe-buffer: 5.1.2
|
||||
string_decoder: 1.1.1
|
||||
util-deprecate: 1.0.2
|
||||
|
||||
readdirp@4.1.2: {}
|
||||
|
||||
rechoir@0.6.2:
|
||||
@@ -8455,6 +8565,8 @@ snapshots:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
safe-buffer@5.1.2: {}
|
||||
|
||||
safer-buffer@2.1.2: {}
|
||||
|
||||
sass@1.93.2:
|
||||
@@ -8475,6 +8587,8 @@ snapshots:
|
||||
|
||||
semver@7.6.3: {}
|
||||
|
||||
setimmediate@1.0.5: {}
|
||||
|
||||
shebang-command@2.0.0:
|
||||
dependencies:
|
||||
shebang-regex: 3.0.0
|
||||
@@ -8545,6 +8659,10 @@ snapshots:
|
||||
get-east-asian-width: 1.3.0
|
||||
strip-ansi: 7.1.0
|
||||
|
||||
string_decoder@1.1.1:
|
||||
dependencies:
|
||||
safe-buffer: 5.1.2
|
||||
|
||||
stringify-entities@4.0.4:
|
||||
dependencies:
|
||||
character-entities-html4: 2.1.0
|
||||
@@ -8663,6 +8781,8 @@ snapshots:
|
||||
|
||||
ts-interface-checker@0.1.13: {}
|
||||
|
||||
tslib@2.3.0: {}
|
||||
|
||||
tslib@2.8.1: {}
|
||||
|
||||
tsup@8.5.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.7.1):
|
||||
@@ -8814,6 +8934,10 @@ snapshots:
|
||||
react: 18.3.1
|
||||
optional: true
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
uuid@10.0.0: {}
|
||||
|
||||
uuid@11.1.0: {}
|
||||
|
||||
vfile-location@5.0.3:
|
||||
@@ -8933,6 +9057,10 @@ snapshots:
|
||||
|
||||
yoctocolors@2.1.1: {}
|
||||
|
||||
zrender@5.6.1:
|
||||
dependencies:
|
||||
tslib: 2.3.0
|
||||
|
||||
zustand@5.0.8(@types/react@18.3.26)(immer@10.2.0)(react@18.3.1)(use-sync-external-store@1.5.0(react@18.3.1)):
|
||||
optionalDependencies:
|
||||
'@types/react': 18.3.26
|
||||
|
||||
@@ -8,6 +8,10 @@ use serde_json::Value as Json;
|
||||
use std::collections::HashMap;
|
||||
use tauri::{AppHandle, Emitter};
|
||||
|
||||
fn is_web_url(url: &str) -> bool {
|
||||
url.starts_with("http://") || url.starts_with("https://")
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RichLabel {
|
||||
pub label: Option<String>,
|
||||
@@ -165,7 +169,13 @@ pub(crate) async fn open(
|
||||
OnOpened::Document { url } => {
|
||||
log::debug!("open document [{}]", url);
|
||||
|
||||
homemade_tauri_shell_open(tauri_app_handle.clone(), url).await?
|
||||
if is_web_url(&url) {
|
||||
tauri_app_handle
|
||||
.emit("open_url_window", url)
|
||||
.map_err(|e| e.to_string())?;
|
||||
} else {
|
||||
homemade_tauri_shell_open(tauri_app_handle.clone(), url).await?
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
OnOpened::WindowManagementAction { action } => {
|
||||
|
||||
@@ -211,6 +211,8 @@ pub fn run() {
|
||||
server::attachment::write_text_file,
|
||||
server::attachment::write_binary_file,
|
||||
server::attachment::delete_attachment,
|
||||
server::document::fetch_document_preview_resource,
|
||||
server::document::get_document_preview,
|
||||
server::transcription::transcription,
|
||||
server::system_settings::get_system_settings,
|
||||
extension::built_in::application::get_app_list,
|
||||
|
||||
97
src-tauri/src/server/document.rs
Normal file
97
src-tauri/src/server/document.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
use crate::common::http::get_response_body_text;
|
||||
use crate::server::http_client::HttpClient;
|
||||
use crate::server::servers::{get_server_by_id, get_server_token};
|
||||
use base64::encode;
|
||||
use reqwest::Method;
|
||||
use serde_json::{Value, json};
|
||||
use std::collections::HashMap;
|
||||
use tauri::AppHandle;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_document_preview(
|
||||
_app_handle: AppHandle,
|
||||
server_id: String,
|
||||
document_id: String,
|
||||
) -> Result<Value, String> {
|
||||
let response = HttpClient::get(&server_id, &format!("/document/{}", document_id), None)
|
||||
.await
|
||||
.map_err(|e| format!("Error fetching document: {}", e))?;
|
||||
|
||||
let response_body = get_response_body_text(response)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to read document response: {}", e))?;
|
||||
|
||||
let document: Value = serde_json::from_str(&response_body)
|
||||
.map_err(|e| format!("Failed to parse document response: {}", e))?;
|
||||
|
||||
let source = document.get("_source").cloned().unwrap_or(Value::Null);
|
||||
let owner_id = source
|
||||
.pointer("/_system/owner_id")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_string);
|
||||
|
||||
let owner = if let Some(owner_id) = owner_id {
|
||||
let owner_response = HttpClient::post(
|
||||
&server_id,
|
||||
&format!("/entity/card/user/{}", owner_id),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("Error fetching document owner: {}", e))?;
|
||||
|
||||
let owner_body = get_response_body_text(owner_response)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to read owner response: {}", e))?;
|
||||
|
||||
serde_json::from_str::<Value>(&owner_body).unwrap_or(Value::Null)
|
||||
} else {
|
||||
Value::Null
|
||||
};
|
||||
|
||||
Ok(json!({
|
||||
"document": document,
|
||||
"owner": owner,
|
||||
}))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn fetch_document_preview_resource(
|
||||
_app_handle: AppHandle,
|
||||
server_id: String,
|
||||
url: String,
|
||||
) -> Result<Value, String> {
|
||||
get_server_by_id(&server_id)
|
||||
.await
|
||||
.ok_or_else(|| format!("Server not found: {}", server_id))?;
|
||||
|
||||
let response = if url.starts_with("http://") || url.starts_with("https://") {
|
||||
let mut headers = HashMap::new();
|
||||
if let Some(token) = get_server_token(&server_id).await {
|
||||
headers.insert("X-API-TOKEN".to_string(), token.access_token);
|
||||
}
|
||||
|
||||
HttpClient::send_raw_request(Method::GET, &url, None, Some(headers), None).await
|
||||
} else {
|
||||
let path = HttpClient::join_url("/", &url);
|
||||
HttpClient::send_request(&server_id, Method::GET, &path, None, None, None).await
|
||||
}
|
||||
.map_err(|e| format!("Error fetching document resource: {}", e))?;
|
||||
|
||||
let content_type = response
|
||||
.headers()
|
||||
.get(reqwest::header::CONTENT_TYPE)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.unwrap_or("application/octet-stream")
|
||||
.to_string();
|
||||
|
||||
let bytes = response
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to read document resource: {}", e))?;
|
||||
|
||||
Ok(json!({
|
||||
"contentType": content_type,
|
||||
"base64": encode(bytes),
|
||||
}))
|
||||
}
|
||||
@@ -4,6 +4,7 @@ pub mod attachment;
|
||||
pub mod auth;
|
||||
pub mod connector;
|
||||
pub mod datasource;
|
||||
pub mod document;
|
||||
pub mod http_client;
|
||||
pub mod profile;
|
||||
pub mod search;
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
import { useState, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { OpenURLWithBrowser } from "@/utils/index";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import type { IChunkData } from "@/types/chat";
|
||||
import RetrieveIcon from "@/icons/Retrieve";
|
||||
|
||||
@@ -83,7 +83,7 @@ export const FetchSource = ({
|
||||
const sourceClick = (item: ISourceData) => () => {
|
||||
const url = (formatUrl && formatUrl(item)) || item.url;
|
||||
if (url) {
|
||||
OpenURLWithBrowser(url);
|
||||
platformAdapter.openUrl(url);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import RehypeKatex from "rehype-katex";
|
||||
import RemarkGfm from "remark-gfm";
|
||||
import RehypeHighlight from "rehype-highlight";
|
||||
import mermaid from "mermaid";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import {
|
||||
copyToClipboard,
|
||||
@@ -17,26 +17,117 @@ import {
|
||||
import "./markdown.scss";
|
||||
import "./highlight.css";
|
||||
|
||||
let mermaidDiagramId = 0;
|
||||
|
||||
function removeMermaidTempElements(id: string) {
|
||||
if (typeof document === "undefined") return;
|
||||
|
||||
[id, `d${id}`, `i${id}`].forEach((elementId) => {
|
||||
document.getElementById(elementId)?.remove();
|
||||
});
|
||||
}
|
||||
|
||||
function getNodeText(node: React.ReactNode): string {
|
||||
return React.Children.toArray(node)
|
||||
.map((child) => {
|
||||
if (typeof child === "string" || typeof child === "number") {
|
||||
return String(child);
|
||||
}
|
||||
if (React.isValidElement(child)) {
|
||||
return getNodeText(
|
||||
(child.props as { children?: React.ReactNode }).children
|
||||
);
|
||||
}
|
||||
return "";
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
function getCodeClassName(node: React.ReactNode): string {
|
||||
let className = "";
|
||||
|
||||
React.Children.forEach(node, (child) => {
|
||||
if (className || !React.isValidElement(child)) return;
|
||||
|
||||
const childProps = child.props as {
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
if (typeof childProps.className === "string") {
|
||||
className = childProps.className;
|
||||
return;
|
||||
}
|
||||
|
||||
className = getCodeClassName(childProps.children);
|
||||
});
|
||||
|
||||
return className;
|
||||
}
|
||||
|
||||
// 8
|
||||
function Mermaid(props: { code: string }) {
|
||||
function Mermaid(props: { code: string; onError?: () => void }) {
|
||||
const { t } = useTranslation();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const [svg, setSvg] = useState("");
|
||||
const [diagramId] = useState(() => `mermaid-${++mermaidDiagramId}`);
|
||||
const bindFunctionsRef = useRef<((element: Element) => void) | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
if (props.code && ref.current) {
|
||||
mermaid
|
||||
.run({
|
||||
nodes: [ref.current],
|
||||
if (props.code) {
|
||||
let cancelled = false;
|
||||
|
||||
setErrorMessage("");
|
||||
setSvg("");
|
||||
bindFunctionsRef.current = undefined;
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
securityLevel: "loose",
|
||||
suppressErrorRendering: true,
|
||||
});
|
||||
|
||||
const renderDiagram = async () => {
|
||||
const canParse = await mermaid.parse(props.code, {
|
||||
suppressErrors: true,
|
||||
});
|
||||
|
||||
if (!canParse) {
|
||||
throw new Error("Invalid Mermaid syntax");
|
||||
}
|
||||
|
||||
return mermaid.render(diagramId, props.code, ref.current || undefined);
|
||||
};
|
||||
|
||||
renderDiagram()
|
||||
.then(({ svg, bindFunctions }) => {
|
||||
if (cancelled) return;
|
||||
bindFunctionsRef.current = bindFunctions;
|
||||
setSvg(svg);
|
||||
})
|
||||
.catch((e) => {
|
||||
setHasError(true);
|
||||
console.error("[Mermaid] ", e.message);
|
||||
if (cancelled) return;
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
removeMermaidTempElements(diagramId);
|
||||
setErrorMessage(message);
|
||||
props.onError?.();
|
||||
console.error("[Mermaid] ", message);
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
removeMermaidTempElements(diagramId);
|
||||
};
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [props.code]);
|
||||
|
||||
useEffect(() => {
|
||||
if (svg && ref.current) {
|
||||
bindFunctionsRef.current?.(ref.current);
|
||||
}
|
||||
}, [svg]);
|
||||
|
||||
function viewSvgInNewWindow() {
|
||||
const svg = ref.current?.querySelector("svg");
|
||||
if (!svg) return;
|
||||
@@ -46,8 +137,20 @@ function Mermaid(props: { code: string }) {
|
||||
// URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
return null;
|
||||
if (errorMessage) {
|
||||
return (
|
||||
<div
|
||||
className={clsx("no-dark", "mermaid", "mermaid-error")}
|
||||
title={errorMessage}
|
||||
>
|
||||
<div className="mermaid-error-title">
|
||||
{t("markdown.mermaid.renderFailed")}
|
||||
</div>
|
||||
<div className="mermaid-error-message">
|
||||
{t("markdown.mermaid.renderFailedDescription")}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -59,37 +162,31 @@ function Mermaid(props: { code: string }) {
|
||||
}}
|
||||
ref={ref}
|
||||
onClick={() => viewSvgInNewWindow()}
|
||||
>
|
||||
{props.code}
|
||||
</div>
|
||||
dangerouslySetInnerHTML={svg ? { __html: svg } : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 7
|
||||
function PreCode(props: { children?: any; enableMermaid?: boolean }) {
|
||||
function PreCode(props: {
|
||||
children?: any;
|
||||
enableMermaid?: boolean;
|
||||
hideMermaidSource?: boolean;
|
||||
}) {
|
||||
const ref = useRef<HTMLPreElement>(null);
|
||||
// const previewRef = useRef<HTMLPreviewHander>(null);
|
||||
const [mermaidCode, setMermaidCode] = useState("");
|
||||
// const [htmlCode, setHtmlCode] = useState("");
|
||||
// const { height } = useWindowSize();
|
||||
// console.log(htmlCode, height);
|
||||
|
||||
const renderArtifacts = useDebouncedCallback(() => {
|
||||
if (!ref.current) return;
|
||||
if (!props.enableMermaid) return;
|
||||
const mermaidCode = useMemo(() => {
|
||||
if (!props.enableMermaid) return "";
|
||||
|
||||
const mermaidDom = ref.current.querySelector("code.language-mermaid");
|
||||
if (mermaidDom) {
|
||||
setMermaidCode((mermaidDom as HTMLElement).innerText);
|
||||
}
|
||||
// const htmlDom = ref.current.querySelector("code.language-html");
|
||||
// const refText = ref.current.querySelector("code")?.innerText;
|
||||
// if (htmlDom) {
|
||||
// setHtmlCode((htmlDom as HTMLElement).innerText);
|
||||
// } else if (refText?.startsWith("<!DOCTYPE")) {
|
||||
// setHtmlCode(refText);
|
||||
// }
|
||||
}, 600);
|
||||
const className = getCodeClassName(props.children);
|
||||
if (!/\blanguage-mermaid\b/.test(className)) return "";
|
||||
|
||||
return getNodeText(props.children).trim();
|
||||
}, [props.children, props.enableMermaid]);
|
||||
|
||||
// const enableArtifacts = true;
|
||||
// console.log(enableArtifacts);
|
||||
@@ -117,25 +214,33 @@ function PreCode(props: { children?: any; enableMermaid?: boolean }) {
|
||||
codeElement.style.whiteSpace = "pre-wrap";
|
||||
}
|
||||
});
|
||||
setTimeout(renderArtifacts, 1);
|
||||
}
|
||||
}, []);
|
||||
}, [props.children]);
|
||||
|
||||
const hideSource = props.hideMermaidSource && mermaidCode.length > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<pre ref={ref}>
|
||||
<span
|
||||
className="copy-code-button"
|
||||
onClick={() => {
|
||||
if (ref.current) {
|
||||
copyToClipboard(
|
||||
ref.current.querySelector("code")?.innerText ?? ""
|
||||
);
|
||||
}
|
||||
}}
|
||||
></span>
|
||||
{props.children}
|
||||
</pre>
|
||||
{!hideSource && (
|
||||
<pre
|
||||
ref={ref}
|
||||
className={clsx({
|
||||
"hidden-mermaid-source": hideSource,
|
||||
})}
|
||||
>
|
||||
<span
|
||||
className="copy-code-button"
|
||||
onClick={() => {
|
||||
if (ref.current) {
|
||||
copyToClipboard(
|
||||
ref.current.querySelector("code")?.innerText ?? ""
|
||||
);
|
||||
}
|
||||
}}
|
||||
></span>
|
||||
{props.children}
|
||||
</pre>
|
||||
)}
|
||||
{mermaidCode.length > 0 && (
|
||||
<Mermaid code={mermaidCode} key={mermaidCode} />
|
||||
)}
|
||||
@@ -238,6 +343,7 @@ function _MarkDownContent(props: {
|
||||
enableSyntaxHighlight?: boolean;
|
||||
enableMermaid?: boolean;
|
||||
enableMath?: boolean;
|
||||
hideMermaidSource?: boolean;
|
||||
}) {
|
||||
const enableMath = props.enableMath !== false;
|
||||
|
||||
@@ -278,7 +384,11 @@ function _MarkDownContent(props: {
|
||||
rehypePlugins={rehypePlugins as any}
|
||||
components={{
|
||||
pre: (preProps) => (
|
||||
<PreCode {...preProps} enableMermaid={props.enableMermaid !== false} />
|
||||
<PreCode
|
||||
{...preProps}
|
||||
enableMermaid={props.enableMermaid !== false}
|
||||
hideMermaidSource={props.hideMermaidSource}
|
||||
/>
|
||||
),
|
||||
code: CustomCode,
|
||||
p: (pProps) => <p {...pProps} dir="auto" />,
|
||||
@@ -324,6 +434,7 @@ export default function Markdown(
|
||||
enableSyntaxHighlight?: boolean;
|
||||
enableMermaid?: boolean;
|
||||
enableMath?: boolean;
|
||||
hideMermaidSource?: boolean;
|
||||
} & React.DOMAttributes<HTMLDivElement>
|
||||
) {
|
||||
const mdRef = useRef<HTMLDivElement>(null);
|
||||
@@ -346,6 +457,7 @@ export default function Markdown(
|
||||
enableSyntaxHighlight={props.enableSyntaxHighlight}
|
||||
enableMermaid={props.enableMermaid}
|
||||
enableMath={props.enableMath}
|
||||
hideMermaidSource={props.hideMermaidSource}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -42,6 +42,10 @@
|
||||
--color-accent-emphasis: #0969da;
|
||||
--color-attention-subtle: #fff8c5;
|
||||
--color-danger-fg: #cf222e;
|
||||
--color-mermaid-error-bg: #fffafa;
|
||||
--color-mermaid-error-border: #f2d0d0;
|
||||
--color-mermaid-error-title: #7f2f2f;
|
||||
--color-mermaid-error-message: #999;
|
||||
}
|
||||
|
||||
@mixin dark {
|
||||
@@ -88,6 +92,10 @@
|
||||
--color-accent-emphasis: #1f6feb;
|
||||
--color-attention-subtle: rgba(187, 128, 9, 0.15);
|
||||
--color-danger-fg: #f85149;
|
||||
--color-mermaid-error-bg: rgba(127, 47, 47, 0.12);
|
||||
--color-mermaid-error-border: rgba(248, 81, 73, 0.25);
|
||||
--color-mermaid-error-title: #ffb4ad;
|
||||
--color-mermaid-error-message: #a6a6a6;
|
||||
}
|
||||
|
||||
.light {
|
||||
@@ -1114,6 +1122,48 @@
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
background-color: var(--white);
|
||||
max-width: 100%;
|
||||
max-height: 70vh;
|
||||
}
|
||||
|
||||
.markdown-body .mermaid:empty {
|
||||
min-height: 180px;
|
||||
}
|
||||
|
||||
.markdown-body .mermaid.mermaid-error {
|
||||
display: flex;
|
||||
min-height: 140px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
border-color: var(--color-mermaid-error-border);
|
||||
background: var(--color-mermaid-error-bg);
|
||||
color: var(--color-mermaid-error-title);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.markdown-body .mermaid-error-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.markdown-body .mermaid-error-message {
|
||||
max-width: 100%;
|
||||
color: var(--color-mermaid-error-message);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
overflow-wrap: anywhere;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.markdown-body pre.hidden-mermaid-source {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.markdown-body .mermaid svg {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
#dmermaid {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useClickAway, useCreation, useReactive } from "ahooks";
|
||||
import clsx from "clsx";
|
||||
import { isNil, lowerCase, noop } from "lodash-es";
|
||||
import {
|
||||
Compass,
|
||||
Copy,
|
||||
Download,
|
||||
Info,
|
||||
@@ -22,6 +23,7 @@ import { useShortcutsStore } from "@/stores/shortcutsStore";
|
||||
import VisibleKey from "../Common/VisibleKey";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import SearchEmpty from "../Common/SearchEmpty";
|
||||
import { OpenURLWithBrowser } from "@/utils";
|
||||
|
||||
interface State {
|
||||
activeMenuIndex: number;
|
||||
@@ -112,6 +114,7 @@ const ContextMenu = ({ formatUrl }: ContextMenuProps) => {
|
||||
|
||||
const { id, url, category, type, payload, source } = selectedSearchContent;
|
||||
const { query, result } = payload ?? {};
|
||||
const formattedUrl = (formatUrl && formatUrl(selectedSearchContent)) || url;
|
||||
|
||||
if (category === "AI Overview") {
|
||||
return [];
|
||||
@@ -131,6 +134,21 @@ const ContextMenu = ({ formatUrl }: ContextMenuProps) => {
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: t("search.contextMenu.openInBrowser"),
|
||||
icon: <Compass />,
|
||||
keys: isMac ? ["⌘", "B"] : ["Ctrl", "B"],
|
||||
shortcut: isMac ? "meta.b" : "ctrl.b",
|
||||
hide:
|
||||
!platformAdapter.isTauri() ||
|
||||
category === "Calculator" ||
|
||||
type === "AI Assistant" ||
|
||||
id === "Extension Store" ||
|
||||
!formattedUrl,
|
||||
clickEvent() {
|
||||
OpenURLWithBrowser(formattedUrl);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: t("search.contextMenu.copyLink"),
|
||||
icon: <Link />,
|
||||
@@ -141,9 +159,7 @@ const ContextMenu = ({ formatUrl }: ContextMenuProps) => {
|
||||
type === "AI Assistant" ||
|
||||
id === "Extension Store",
|
||||
clickEvent() {
|
||||
copyToClipboard(
|
||||
(formatUrl && formatUrl(selectedSearchContent)) || url
|
||||
);
|
||||
copyToClipboard(formattedUrl);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
16
src/icons/AIInsightIcon.tsx
Normal file
16
src/icons/AIInsightIcon.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { createLucideIcon } from "lucide-react";
|
||||
|
||||
const AIInsightIcon = createLucideIcon("AIInsightIcon", [
|
||||
[
|
||||
"path",
|
||||
{
|
||||
d: "M789.759132 0a169.59947 169.59947 0 0 1 169.59947 169.023472l0.895997 342.846928a41.59987 41.59987 0 0 1-83.19974 0.256l-0.831997-342.846929A86.39973 86.39973 0 0 0 789.759132 83.19974l-556.094262 1.279996a86.39973 86.39973 0 0 0-86.39973 86.39973v684.79786c0 47.743851 38.655879 86.39973 86.39973 86.39973l214.20733-1.279996A41.59987 41.59987 0 1 1 448.0002 1023.9968H233.66487a168.511473 168.511473 0 0 1-169.59947-168.319474l0.064-684.79786A169.59947 169.59947 0 0 1 233.72887 1.279996L789.823132 0z m-28.79991 623.614051l36.799885 105.151672a47.423852 47.423852 0 0 0 31.551901 31.615901l105.215672 36.799885c10.495967 5.247984 10.495967 15.807951 0 21.055934l-105.215672 36.799885a47.423852 47.423852 0 0 0-31.551901 31.551901l-36.863885 105.215672c-5.247984 10.495967-15.743951 10.495967-20.991934 0l-36.863885-105.215672a47.423852 47.423852 0 0 0-31.551901-31.551901l-105.215672-36.799885c-10.495967-5.247984-10.495967-15.807951 0-21.055934l105.215672-36.799885a47.423852 47.423852 0 0 0 31.551901-31.615901l36.863885-105.151672c5.247984-10.559967 15.743951-10.559967 20.991934 0zM534.39993 598.39813a41.59987 41.59987 0 1 1 0 83.19974h-172.79946a41.59987 41.59987 0 1 1 0-83.19974h172.79946z m127.9996-191.9994a41.59987 41.59987 0 1 1 0 83.19974h-300.79906a41.59987 41.59987 0 1 1 0-83.19974h300.79906z m0-191.9994a41.59987 41.59987 0 1 1 0 83.19974h-300.79906a41.59987 41.59987 0 1 1 0-83.19974h300.79906z",
|
||||
transform: "scale(0.0234375)",
|
||||
fill: "currentColor",
|
||||
stroke: "none",
|
||||
key: "main",
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
export default AIInsightIcon;
|
||||
36
src/icons/PreviewIcon.tsx
Normal file
36
src/icons/PreviewIcon.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { createLucideIcon } from "lucide-react";
|
||||
|
||||
const PreviewIcon = createLucideIcon("PreviewIcon", [
|
||||
[
|
||||
"path",
|
||||
{
|
||||
d: "M938.666667 938.666667c-12.8 0-21.333333-4.266667-29.866667-12.8l-136.533333-136.533334c-17.066667-17.066667-17.066667-42.666667 0-59.733333s42.666667-17.066667 59.733333 0l136.533333 136.533333c17.066667 17.066667 17.066667 42.666667 0 59.733334-8.533333 8.533333-17.066667 12.8-29.866666 12.8z",
|
||||
transform: "scale(0.0234375)",
|
||||
fill: "currentColor",
|
||||
stroke: "none",
|
||||
key: "search-handle",
|
||||
},
|
||||
],
|
||||
[
|
||||
"path",
|
||||
{
|
||||
d: "M644.266667 866.133333c-68.266667 0-132.266667-25.6-183.466667-76.8-102.4-102.4-102.4-268.8 0-371.2 102.4-102.4 268.8-102.4 371.2 0 102.4 102.4 102.4 268.8 0 371.2-51.2 51.2-119.466667 76.8-187.733333 76.8z m0-439.466666c-46.933333 0-89.6 17.066667-123.733334 51.2-68.266667 68.266667-68.266667 179.2 0 251.733333 68.266667 68.266667 179.2 68.266667 251.733334 0 68.266667-68.266667 68.266667-179.2 0-251.733333-34.133333-34.133333-81.066667-51.2-128-51.2z",
|
||||
transform: "scale(0.0234375)",
|
||||
fill: "currentColor",
|
||||
stroke: "none",
|
||||
key: "search-circle",
|
||||
},
|
||||
],
|
||||
[
|
||||
"path",
|
||||
{
|
||||
d: "M499.2 981.333333H149.333333C89.6 981.333333 42.666667 934.4 42.666667 874.666667v-725.333334C42.666667 89.6 89.6 42.666667 149.333333 42.666667h640C849.066667 42.666667 896 89.6 896 149.333333v145.066667c0 25.6-17.066667 42.666667-42.666667 42.666667s-42.666667-17.066667-42.666666-42.666667V149.333333c0-12.8-8.533333-21.333333-21.333334-21.333333h-640c-12.8 0-21.333333 8.533333-21.333333 21.333333v725.333334c0 12.8 8.533333 21.333333 21.333333 21.333333h349.866667c25.6 0 42.666667 17.066667 42.666667 42.666667s-21.333333 42.666667-42.666667 42.666666z",
|
||||
transform: "scale(0.0234375)",
|
||||
fill: "currentColor",
|
||||
stroke: "none",
|
||||
key: "document",
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
export default PreviewIcon;
|
||||
@@ -56,6 +56,12 @@
|
||||
"cancel": "No"
|
||||
}
|
||||
},
|
||||
"markdown": {
|
||||
"mermaid": {
|
||||
"renderFailed": "Mind map can't be displayed",
|
||||
"renderFailedDescription": "The content format looks invalid, so the preview can't be generated yet"
|
||||
}
|
||||
},
|
||||
"labels": {
|
||||
"openSource": "Open Source",
|
||||
"cancelled": "Generation Cancelled",
|
||||
@@ -497,6 +503,7 @@
|
||||
},
|
||||
"contextMenu": {
|
||||
"open": "Open",
|
||||
"openInBrowser": "Open in Browser",
|
||||
"copyLink": "Copy Link",
|
||||
"copyAnswer": "Copy Answer",
|
||||
"copyUppercaseAnswer": "Copy Answer (in Word)",
|
||||
@@ -750,6 +757,29 @@
|
||||
"loading": "Loading...",
|
||||
"notFound": "Not found"
|
||||
},
|
||||
"documentPreview": {
|
||||
"labels": {
|
||||
"preview": "Preview",
|
||||
"aiInterpretation": "AI Interpretation"
|
||||
},
|
||||
"buttons": {
|
||||
"openSource": "Open Source",
|
||||
"reload": "Reload"
|
||||
},
|
||||
"hints": {
|
||||
"previewUnavailable": "Preview is not available for this document type.",
|
||||
"previewUnavailableTitle": "This file can't be previewed",
|
||||
"previewUnavailableDescription": "The file format may be unsupported or the content is temporarily unavailable"
|
||||
},
|
||||
"loading": {
|
||||
"document": "Loading document...",
|
||||
"preview": "Loading preview..."
|
||||
},
|
||||
"errors": {
|
||||
"missingParams": "Missing document preview parameters.",
|
||||
"loadFailed": "Failed to load document"
|
||||
}
|
||||
},
|
||||
"selection": {
|
||||
"title": "Selection Toolbar Settings",
|
||||
"actions": {
|
||||
|
||||
@@ -56,6 +56,12 @@
|
||||
"cancel": "否"
|
||||
}
|
||||
},
|
||||
"markdown": {
|
||||
"mermaid": {
|
||||
"renderFailed": "思维导图无法显示",
|
||||
"renderFailedDescription": "内容格式异常,暂时无法生成预览"
|
||||
}
|
||||
},
|
||||
"labels": {
|
||||
"openSource": "打开来源",
|
||||
"cancelled": "生成已取消",
|
||||
@@ -497,6 +503,7 @@
|
||||
},
|
||||
"contextMenu": {
|
||||
"open": "打开",
|
||||
"openInBrowser": "打开至浏览器",
|
||||
"copyLink": "复制链接",
|
||||
"copyAnswer": "复制答案",
|
||||
"copyUppercaseAnswer": "复制答案(大写)",
|
||||
@@ -749,6 +756,29 @@
|
||||
"loading": "加载中...",
|
||||
"notFound": "未找到"
|
||||
},
|
||||
"documentPreview": {
|
||||
"labels": {
|
||||
"preview": "预览",
|
||||
"aiInterpretation": "AI 解读"
|
||||
},
|
||||
"buttons": {
|
||||
"openSource": "打开来源",
|
||||
"reload": "重新加载"
|
||||
},
|
||||
"hints": {
|
||||
"previewUnavailable": "当前文档类型暂不支持预览。",
|
||||
"previewUnavailableTitle": "此文件无法预览",
|
||||
"previewUnavailableDescription": "文件格式可能不受支持,或内容暂时不可用"
|
||||
},
|
||||
"loading": {
|
||||
"document": "文档加载中...",
|
||||
"preview": "预览加载中..."
|
||||
},
|
||||
"errors": {
|
||||
"missingParams": "缺少文档预览参数。",
|
||||
"loadFailed": "文档加载失败"
|
||||
}
|
||||
},
|
||||
"selection": {
|
||||
"title": "划词工具栏设置",
|
||||
"actions": {
|
||||
|
||||
678
src/pages/document-preview/index.tsx
Normal file
678
src/pages/document-preview/index.tsx
Normal file
@@ -0,0 +1,678 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
AlertCircle,
|
||||
ChevronDown,
|
||||
Compass,
|
||||
FileText,
|
||||
RefreshCw,
|
||||
} from "lucide-react";
|
||||
import clsx from "clsx";
|
||||
import { renderAsync } from "docx-preview";
|
||||
import { init as initPptxPreview } from "pptx-preview";
|
||||
|
||||
import loadFailedSvg from "@/components/ChatMessage/DeepResearch/load-failed.svg";
|
||||
import Markdown from "@/components/ChatMessage/Markdown";
|
||||
import CommonIcon from "@/components/Common/Icons/CommonIcon";
|
||||
import Scrollbar from "@/components/Common/Scrollbar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import AIInsightIcon from "@/icons/AIInsightIcon";
|
||||
import PreviewIcon from "@/icons/PreviewIcon";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import { formatter, OpenURLWithBrowser } from "@/utils";
|
||||
import { formatDateToLocal } from "@/utils/date";
|
||||
|
||||
type PreviewResponse = {
|
||||
document?: {
|
||||
_id?: string;
|
||||
_source?: any;
|
||||
};
|
||||
owner?: any;
|
||||
};
|
||||
|
||||
type PreviewResourceResponse = {
|
||||
contentType?: string;
|
||||
base64?: string;
|
||||
};
|
||||
|
||||
type PreviewContentType =
|
||||
| "image"
|
||||
| "video"
|
||||
| "markdown"
|
||||
| "pdf"
|
||||
| "docx"
|
||||
| "pptx"
|
||||
| "html";
|
||||
|
||||
const previewContentTypes: PreviewContentType[] = [
|
||||
"image",
|
||||
"video",
|
||||
"markdown",
|
||||
"pdf",
|
||||
"docx",
|
||||
"pptx",
|
||||
"html",
|
||||
];
|
||||
|
||||
const normalizePreviewContentType = (
|
||||
contentType?: string,
|
||||
mimeType?: string
|
||||
): PreviewContentType | "" => {
|
||||
const value = String(contentType || mimeType || "")
|
||||
.toLowerCase()
|
||||
.split(";")[0]
|
||||
.trim();
|
||||
|
||||
if (!value) return "";
|
||||
if (value === "image" || value.startsWith("image/")) return "image";
|
||||
if (value === "video" || value.startsWith("video/")) return "video";
|
||||
if (["markdown", "md", "text/markdown"].includes(value)) return "markdown";
|
||||
if (["pdf", "application/pdf"].includes(value)) return "pdf";
|
||||
if (
|
||||
[
|
||||
"docx",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
].includes(value)
|
||||
) {
|
||||
return "docx";
|
||||
}
|
||||
if (
|
||||
[
|
||||
"pptx",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
].includes(value)
|
||||
) {
|
||||
return "pptx";
|
||||
}
|
||||
if (["html", "htm", "html_doc", "text/html"].includes(value)) return "html";
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
const getParams = () => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
return {
|
||||
serverId: params.get("serverId") || "",
|
||||
documentId: params.get("documentId") || "",
|
||||
sourceUrl: params.get("sourceUrl") || "",
|
||||
};
|
||||
};
|
||||
|
||||
const resolveRawContentUrl = (rawContent?: string) => {
|
||||
if (!rawContent) return "";
|
||||
if (/^https?:\/\//i.test(rawContent)) return rawContent;
|
||||
|
||||
const endpoint = JSON.parse(localStorage.getItem("app-store") || "{}")?.state
|
||||
?.endpoint_http;
|
||||
if (!endpoint) return rawContent;
|
||||
|
||||
return `${String(endpoint).replace(/\/$/, "")}/${rawContent.replace(/^\//, "")}`;
|
||||
};
|
||||
|
||||
const base64ToBlob = (base64: string, contentType: string) => {
|
||||
const binary = atob(base64);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
|
||||
for (let i = 0; i < binary.length; i += 1) {
|
||||
bytes[i] = binary.charCodeAt(i);
|
||||
}
|
||||
|
||||
return new Blob([bytes], { type: contentType });
|
||||
};
|
||||
|
||||
const injectHtmlPreviewStyles = (html: string, baseUrl?: string) => {
|
||||
const baseTag = baseUrl ? `<base href="${baseUrl}">` : "";
|
||||
const styles = `
|
||||
<style>
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(144, 147, 153, 0.3) transparent !important;
|
||||
}
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(144, 147, 153, 0.3) !important;
|
||||
border-radius: 7px !important;
|
||||
}
|
||||
*::-webkit-scrollbar {
|
||||
width: 7px !important;
|
||||
height: 7px !important;
|
||||
}
|
||||
*::-webkit-scrollbar-track-piece {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
body {
|
||||
background: #fff !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 24px 24px !important;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
|
||||
if (/<head[^>]*>/i.test(html)) {
|
||||
return html.replace(/<head([^>]*)>/i, `<head$1>${baseTag}${styles}`);
|
||||
}
|
||||
|
||||
return `${baseTag}${styles}${html}`;
|
||||
};
|
||||
|
||||
const HtmlPreview = ({ html, baseUrl }: { html: string; baseUrl?: string }) => {
|
||||
return (
|
||||
<iframe
|
||||
title="HTML preview"
|
||||
srcDoc={injectHtmlPreviewStyles(html, baseUrl)}
|
||||
className="h-[68vh] w-full border-0"
|
||||
sandbox="allow-same-origin allow-scripts"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const OfficePreview = ({
|
||||
type,
|
||||
blob,
|
||||
}: {
|
||||
type: "docx" | "pptx";
|
||||
blob?: Blob;
|
||||
}) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container || !blob) return;
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
const renderOffice = async () => {
|
||||
container.innerHTML = "";
|
||||
const buffer = await blob.arrayBuffer();
|
||||
if (cancelled || buffer.byteLength === 0) return;
|
||||
|
||||
if (type === "docx") {
|
||||
await renderAsync(buffer, container, undefined, {
|
||||
inWrapper: false,
|
||||
ignoreWidth: true,
|
||||
ignoreHeight: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const width = container.clientWidth || 840;
|
||||
const height = Math.round(width * (9 / 16));
|
||||
const pptx = initPptxPreview(container, { width, height });
|
||||
await pptx.preview(buffer);
|
||||
};
|
||||
|
||||
renderOffice().catch((error) => {
|
||||
container.innerHTML = "";
|
||||
const message = document.createElement("div");
|
||||
message.style.color = "#999";
|
||||
message.style.textAlign = "center";
|
||||
message.style.padding = "48px";
|
||||
message.textContent = String(error);
|
||||
container.appendChild(message);
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [blob, type]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="min-h-[360px] w-full overflow-hidden [&_.docx]:p-0!"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
function DocumentPreviewPage() {
|
||||
const { t } = useTranslation();
|
||||
const { serverId, documentId, sourceUrl } = useMemo(getParams, []);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
const [data, setData] = useState<any>();
|
||||
const [owner, setOwner] = useState<any>();
|
||||
const [resourceLoading, setResourceLoading] = useState(false);
|
||||
const [resourceError, setResourceError] = useState("");
|
||||
const [resourceUrl, setResourceUrl] = useState("");
|
||||
const [resourceText, setResourceText] = useState("");
|
||||
const [resourceBlob, setResourceBlob] = useState<Blob>();
|
||||
const [expandedMore, setExpandedMore] = useState(false);
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
if (!serverId || !documentId) {
|
||||
setError(t("documentPreview.errors.missingParams"));
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError("");
|
||||
|
||||
try {
|
||||
const result = await platformAdapter.invokeBackend<PreviewResponse>(
|
||||
"get_document_preview",
|
||||
{
|
||||
serverId,
|
||||
documentId,
|
||||
}
|
||||
);
|
||||
|
||||
setData(result?.document?._source);
|
||||
setOwner(result?.owner);
|
||||
} catch (error) {
|
||||
setError(error instanceof Error ? error.message : String(error));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [documentId, serverId, t]);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [loadData]);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = data?.title || "Document Preview";
|
||||
}, [data?.title]);
|
||||
|
||||
const rawContent = data?.metadata?.raw_content || sourceUrl;
|
||||
const rawContentUrl = resolveRawContentUrl(rawContent);
|
||||
const contentType = data?.metadata?.content_type;
|
||||
const previewType = normalizePreviewContentType(
|
||||
contentType,
|
||||
data?.metadata?.mime_type
|
||||
);
|
||||
const hasPreview =
|
||||
Boolean(rawContentUrl) &&
|
||||
Boolean(previewType) &&
|
||||
previewContentTypes.includes(previewType as PreviewContentType);
|
||||
const ownerName =
|
||||
owner?.title || owner?.name || owner?.username || data?.owner?.username;
|
||||
const createdAt = data?.created ? formatDateToLocal(data.created) : "";
|
||||
const updatedAt = data?.updated ? formatDateToLocal(data.updated) : "";
|
||||
const hasInlinePreview = previewType === "image" || previewType === "video";
|
||||
const hasCollapsiblePreview =
|
||||
hasPreview && ["markdown", "pdf", "docx", "pptx", "html"].includes(previewType);
|
||||
|
||||
useEffect(() => {
|
||||
let objectUrl = "";
|
||||
let cancelled = false;
|
||||
|
||||
const loadResource = async () => {
|
||||
setResourceUrl("");
|
||||
setResourceText("");
|
||||
setResourceBlob(undefined);
|
||||
setResourceError("");
|
||||
|
||||
if (!hasPreview || !rawContent) return;
|
||||
|
||||
setResourceLoading(true);
|
||||
try {
|
||||
const result =
|
||||
await platformAdapter.invokeBackend<PreviewResourceResponse>(
|
||||
"fetch_document_preview_resource",
|
||||
{
|
||||
serverId,
|
||||
url: rawContent,
|
||||
}
|
||||
);
|
||||
|
||||
if (cancelled) return;
|
||||
|
||||
const base64 = result?.base64;
|
||||
if (!base64) {
|
||||
throw new Error("Document resource is empty.");
|
||||
}
|
||||
|
||||
const blob = base64ToBlob(
|
||||
base64,
|
||||
result?.contentType || "application/octet-stream"
|
||||
);
|
||||
|
||||
if (previewType === "markdown" || previewType === "html") {
|
||||
setResourceText(await blob.text());
|
||||
} else if (previewType === "docx" || previewType === "pptx") {
|
||||
setResourceBlob(blob);
|
||||
} else {
|
||||
objectUrl = URL.createObjectURL(blob);
|
||||
setResourceUrl(objectUrl);
|
||||
}
|
||||
} catch (error) {
|
||||
if (!cancelled) {
|
||||
setResourceError(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setResourceLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadResource();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
if (objectUrl) {
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
}
|
||||
};
|
||||
}, [hasPreview, previewType, rawContent, serverId]);
|
||||
|
||||
const renderPreviewUnavailable = () => {
|
||||
return (
|
||||
<div className="flex min-h-[360px] flex-col items-center justify-center px-6 py-16 text-center">
|
||||
<img
|
||||
src={loadFailedSvg}
|
||||
alt=""
|
||||
className="h-[80px] w-[80px] select-none"
|
||||
/>
|
||||
<div className="mt-3 text-sm font-medium leading-5 text-[#999] dark:text-[#A3A3A3]">
|
||||
{t("documentPreview.hints.previewUnavailableTitle")}
|
||||
</div>
|
||||
<div className="mt-1 text-sm leading-5 text-[#999] dark:text-[#A3A3A3]">
|
||||
{t("documentPreview.hints.previewUnavailableDescription")}
|
||||
</div>
|
||||
{rawContentUrl && (
|
||||
<button
|
||||
className="mt-5 inline-flex h-9 items-center gap-1.5 rounded-full bg-[#0A84FF] px-4 text-sm font-medium text-white transition hover:bg-[#0072E5]"
|
||||
onClick={() => OpenURLWithBrowser(rawContentUrl)}
|
||||
>
|
||||
<Compass className="size-4" />
|
||||
{t("documentPreview.buttons.openSource")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderPreview = () => {
|
||||
if (!hasPreview) {
|
||||
return renderPreviewUnavailable();
|
||||
}
|
||||
|
||||
if (resourceLoading) {
|
||||
return (
|
||||
<div className="flex min-h-[260px] items-center justify-center rounded-md border border-[#E6E6E6] text-sm text-[#777] dark:border-[#2A2A2A] dark:text-[#AAA]">
|
||||
<RefreshCw className="mr-2 size-4 animate-spin" />
|
||||
{t("documentPreview.loading.preview")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (resourceError) {
|
||||
return renderPreviewUnavailable();
|
||||
}
|
||||
|
||||
const resourceReady =
|
||||
previewType === "markdown" || previewType === "html"
|
||||
? Boolean(resourceText)
|
||||
: previewType === "docx" || previewType === "pptx"
|
||||
? Boolean(resourceBlob)
|
||||
: Boolean(resourceUrl);
|
||||
|
||||
if (!resourceReady) {
|
||||
return (
|
||||
<div className="flex min-h-[260px] items-center justify-center rounded-md border border-[#E6E6E6] text-sm text-[#777] dark:border-[#2A2A2A] dark:text-[#AAA]">
|
||||
<RefreshCw className="mr-2 size-4 animate-spin" />
|
||||
{t("documentPreview.loading.preview")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (previewType === "image") {
|
||||
return (
|
||||
<div className="flex min-h-[320px] items-center justify-center overflow-hidden rounded-lg bg-black/3 p-4 dark:bg-white/5">
|
||||
<img
|
||||
src={resourceUrl}
|
||||
alt={data?.title || "Document image"}
|
||||
className="max-h-[65vh] max-w-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (previewType === "video") {
|
||||
return (
|
||||
<video
|
||||
controls
|
||||
src={resourceUrl}
|
||||
className="max-h-[65vh] w-full rounded-lg bg-black"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (previewType === "pdf") {
|
||||
return (
|
||||
<iframe
|
||||
title={data?.title || "PDF preview"}
|
||||
src={resourceUrl}
|
||||
className="h-[68vh] w-full border-0"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (previewType === "markdown") {
|
||||
return (
|
||||
<Markdown
|
||||
content={resourceText || String(data?.content || data?.summary || "")}
|
||||
enableMermaid
|
||||
hideMermaidSource
|
||||
enableMath={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (previewType === "html") {
|
||||
return <HtmlPreview html={resourceText} baseUrl={rawContentUrl} />;
|
||||
}
|
||||
|
||||
if (previewType === "docx" || previewType === "pptx") {
|
||||
return <OfficePreview type={previewType} blob={resourceBlob} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderPanel = (
|
||||
key: string,
|
||||
title: string,
|
||||
icon: React.ReactNode,
|
||||
children: React.ReactNode,
|
||||
defaultOpen = true
|
||||
) => {
|
||||
return (
|
||||
<details
|
||||
key={key}
|
||||
open={defaultOpen}
|
||||
className="group rounded-lg border border-[#E8E8E8] bg-white dark:border-[#303030] dark:bg-[#111]"
|
||||
>
|
||||
<summary className="flex cursor-pointer list-none items-center justify-between px-4 py-2.5">
|
||||
<div className="flex items-center gap-2 text-[14px] leading-5 text-[#333] dark:text-[#DDD]">
|
||||
<span className="text-[#0A84FF]">{icon}</span>
|
||||
{title}
|
||||
</div>
|
||||
<ChevronDown className="size-4 text-[#999] transition group-open:rotate-180" />
|
||||
</summary>
|
||||
<div className="border-t border-[#F0F0F0] px-6 py-8 dark:border-[#303030]">
|
||||
{children}
|
||||
</div>
|
||||
</details>
|
||||
);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex h-screen w-screen items-center justify-center bg-white text-[#777] dark:bg-[#111] dark:text-[#AAA]">
|
||||
<RefreshCw className="mr-2 size-4 animate-spin" />
|
||||
{t("documentPreview.loading.document")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex h-screen w-screen items-center justify-center bg-white p-8 dark:bg-[#111]">
|
||||
<div className="flex max-w-lg flex-col items-center gap-4 text-center">
|
||||
<AlertCircle className="size-10 text-[#EF4444]" />
|
||||
<div className="text-lg font-medium text-[#222] dark:text-[#EEE]">
|
||||
{t("documentPreview.errors.loadFailed")}
|
||||
</div>
|
||||
<div className="text-sm text-[#777] dark:text-[#AAA]">{error}</div>
|
||||
<button
|
||||
className="rounded-md bg-[#0A84FF] px-4 py-2 text-sm text-white hover:bg-[#0072E5]"
|
||||
onClick={loadData}
|
||||
>
|
||||
{t("documentPreview.buttons.reload")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-screen w-screen bg-white text-[#222] dark:bg-[#0F0F0F] dark:text-[#EEE]">
|
||||
<Scrollbar className="h-full overflow-y-auto">
|
||||
<main className="mx-auto flex min-h-full w-full max-w-[960px] flex-col px-4 py-8">
|
||||
<section>
|
||||
<div className="flex items-start justify-between gap-6">
|
||||
<div className="min-w-0 flex-1">
|
||||
<h1 className="flex items-center gap-2 break-words text-[18px] leading-[26px] text-[#1A0CAB] dark:text-[#8AB4F8]">
|
||||
<CommonIcon
|
||||
renderOrder={["item_icon", "connector_icon", "default_icon"]}
|
||||
item={data}
|
||||
itemIcon={data?.icon}
|
||||
defaultIcon={FileText}
|
||||
className="size-5 shrink-0"
|
||||
/>
|
||||
{data?.title || documentId}
|
||||
</h1>
|
||||
|
||||
<div className="mt-4 flex flex-wrap items-center gap-1 text-sm text-[#666] dark:text-[#AAA]">
|
||||
<span>{data?.source?.name || "-"}</span>
|
||||
{data?.category && (
|
||||
<>
|
||||
<span className="text-[#AAA]">›</span>
|
||||
<span>{data.category}</span>
|
||||
</>
|
||||
)}
|
||||
{ownerName && (
|
||||
<>
|
||||
<span className="mx-1 text-[#AAA]">|</span>
|
||||
<span>{ownerName}</span>
|
||||
</>
|
||||
)}
|
||||
{updatedAt && (
|
||||
<>
|
||||
<span className="mx-1 text-[#AAA]">•</span>
|
||||
<span>{updatedAt}</span>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
className={clsx(
|
||||
"ml-1 inline-flex size-5 items-center justify-center rounded text-[#888] transition hover:text-[#0A84FF]",
|
||||
{ "rotate-180": expandedMore }
|
||||
)}
|
||||
onClick={() => setExpandedMore((value) => !value)}
|
||||
aria-label="Toggle details"
|
||||
>
|
||||
<ChevronDown className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{rawContentUrl && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="mt-8 shrink-0"
|
||||
onClick={() => OpenURLWithBrowser(rawContentUrl)}
|
||||
>
|
||||
{t("documentPreview.buttons.openSource")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{expandedMore && (
|
||||
<div className="mt-4 grid grid-cols-2 gap-x-8 gap-y-3 rounded-lg bg-black/3 p-4 text-sm dark:bg-white/5 max-sm:grid-cols-1">
|
||||
<div>
|
||||
<span className="mr-3 text-[#999]">
|
||||
{t("search.document.type")}
|
||||
</span>
|
||||
{data?.type || contentType || "-"}
|
||||
</div>
|
||||
<div>
|
||||
<span className="mr-3 text-[#999]">
|
||||
{t("search.document.size")}
|
||||
</span>
|
||||
{formatter.bytes(data?.size || 0)}
|
||||
</div>
|
||||
<div>
|
||||
<span className="mr-3 text-[#999]">
|
||||
{t("search.document.createdBy")}
|
||||
</span>
|
||||
{ownerName || "-"}
|
||||
</div>
|
||||
<div>
|
||||
<span className="mr-3 text-[#999]">
|
||||
{t("search.document.createdAt")}
|
||||
</span>
|
||||
{createdAt || "-"}
|
||||
</div>
|
||||
<div>
|
||||
<span className="mr-3 text-[#999]">
|
||||
{t("search.document.updatedAt")}
|
||||
</span>
|
||||
{updatedAt || "-"}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="mt-5 flex flex-col gap-4">
|
||||
{hasInlinePreview &&
|
||||
renderPanel(
|
||||
"preview",
|
||||
t("documentPreview.labels.preview"),
|
||||
<PreviewIcon className="size-4" />,
|
||||
renderPreview()
|
||||
)}
|
||||
|
||||
{hasCollapsiblePreview &&
|
||||
renderPanel(
|
||||
"preview",
|
||||
t("documentPreview.labels.preview"),
|
||||
<PreviewIcon className="size-4" />,
|
||||
renderPreview(),
|
||||
!data?.ai_insights?.text
|
||||
)}
|
||||
|
||||
{data?.ai_insights?.text &&
|
||||
renderPanel(
|
||||
"ai",
|
||||
t("documentPreview.labels.aiInterpretation"),
|
||||
<AIInsightIcon className="size-4" />,
|
||||
<div className="px-2">
|
||||
<Markdown
|
||||
content={data.ai_insights.text}
|
||||
enableMermaid
|
||||
hideMermaidSource
|
||||
enableMath={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!hasInlinePreview &&
|
||||
!hasCollapsiblePreview &&
|
||||
!data?.ai_insights?.text &&
|
||||
renderPanel(
|
||||
"empty",
|
||||
t("documentPreview.labels.preview"),
|
||||
<PreviewIcon className="size-4" />,
|
||||
renderPreview()
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
</Scrollbar>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DocumentPreviewPage;
|
||||
@@ -25,6 +25,12 @@ function MainApp() {
|
||||
|
||||
setViewExtensionOpened(payload);
|
||||
});
|
||||
|
||||
platformAdapter.listenEvent("open_url_window", async ({ payload }) => {
|
||||
if (typeof payload === "string" && payload) {
|
||||
await platformAdapter.openUrl(payload);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const { synthesizeItem } = useChatStore();
|
||||
|
||||
65
src/pages/webview/index.tsx
Normal file
65
src/pages/webview/index.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
|
||||
const getInitialUrl = () => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
return params.get("url") || "";
|
||||
};
|
||||
|
||||
const getUrlTitle = (url: string) => {
|
||||
try {
|
||||
return new URL(url).hostname || "Coco";
|
||||
} catch {
|
||||
return "Coco";
|
||||
}
|
||||
};
|
||||
|
||||
function WebviewPage() {
|
||||
const url = useMemo(() => getInitialUrl(), []);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = getUrlTitle(url);
|
||||
}, [url]);
|
||||
|
||||
if (!url) {
|
||||
return (
|
||||
<div className="flex h-screen w-screen items-center justify-center bg-white text-sm text-[#666] dark:bg-[#111] dark:text-[#bbb]">
|
||||
URL unavailable
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative h-screen w-screen overflow-hidden bg-white dark:bg-[#111]">
|
||||
{!loaded && (
|
||||
<div className="absolute inset-0 flex items-center justify-center text-sm text-[#777] dark:text-[#aaa]">
|
||||
Loading...
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
aria-label="Open in browser"
|
||||
className="absolute right-3 top-3 z-10 flex size-9 items-center justify-center rounded-md bg-black/45 text-white shadow-sm transition hover:bg-black/65"
|
||||
onClick={() => {
|
||||
platformAdapter.openUrlWithBrowser(url);
|
||||
}}
|
||||
>
|
||||
<ExternalLink className="size-4" />
|
||||
</button>
|
||||
|
||||
<iframe
|
||||
title={getUrlTitle(url)}
|
||||
src={url}
|
||||
className="h-full w-full border-0"
|
||||
allow="fullscreen; clipboard-read; clipboard-write; geolocation; microphone; camera"
|
||||
allowFullScreen
|
||||
onLoad={() => setLoaded(true)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default WebviewPage;
|
||||
@@ -9,6 +9,8 @@ const SettingsPage = lazy(() => import("@/pages/settings/index"));
|
||||
const StandaloneChat = lazy(() => import("@/pages/chat/index"));
|
||||
const CheckPage = lazy(() => import("@/pages/check/index"));
|
||||
const SelectionWindow = lazy(() => import("@/pages/selection/index"));
|
||||
const WebviewPage = lazy(() => import("@/pages/webview/index"));
|
||||
const DocumentPreviewPage = lazy(() => import("@/pages/document-preview/index"));
|
||||
|
||||
const routerOptions = {
|
||||
basename: "/",
|
||||
@@ -30,6 +32,8 @@ export const router = createBrowserRouter(
|
||||
{ path: "/ui/chat", element: (<Suspense fallback={<></>}><StandaloneChat /></Suspense>) },
|
||||
{ path: "/ui/check", element: (<Suspense fallback={<></>}><CheckPage /></Suspense>) },
|
||||
{ path: "/ui/selection", element: (<Suspense fallback={<></>}><SelectionWindow /></Suspense>) },
|
||||
{ path: "/ui/webview", element: (<Suspense fallback={<></>}><WebviewPage /></Suspense>) },
|
||||
{ path: "/ui/document-preview", element: (<Suspense fallback={<></>}><DocumentPreviewPage /></Suspense>) },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -54,6 +54,7 @@ export interface EventPayloads {
|
||||
oauth_success: any;
|
||||
extension_install_success: any;
|
||||
open_view_extension: ViewExtensionOpened;
|
||||
open_url_window: string;
|
||||
"server-list-changed": Server[];
|
||||
"selection-text": string;
|
||||
"selection-ask-ai": any;
|
||||
@@ -124,6 +125,7 @@ export interface SystemOperations {
|
||||
relaunchApp: () => Promise<void>;
|
||||
isTauri: () => boolean;
|
||||
openUrl: (url: string) => Promise<unknown>;
|
||||
openUrlWithBrowser: (url: string) => Promise<unknown>;
|
||||
commands: <T>(commandName: string, ...args: any[]) => Promise<T>;
|
||||
isWindows10: () => Promise<boolean>;
|
||||
revealItemInDir: (path: string) => Promise<unknown>;
|
||||
|
||||
@@ -87,7 +87,7 @@ export const OpenURLWithBrowser = async (url?: string) => {
|
||||
if (!url) return;
|
||||
if (IsTauri()) {
|
||||
try {
|
||||
await platformAdapter.openUrl(url);
|
||||
await platformAdapter.openUrlWithBrowser(url);
|
||||
await platformAdapter.commands("hide_coco");
|
||||
console.log("URL opened in default browser");
|
||||
} catch (error) {
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import type { BasePlatformAdapter } from "@/types/platform";
|
||||
import type { AppTheme } from "@/types/index";
|
||||
import { useAppearanceStore } from "@/stores/appearanceStore";
|
||||
import { copyToClipboard, dispatchEvent, OpenURLWithBrowser } from ".";
|
||||
import { copyToClipboard, dispatchEvent } from ".";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import { unrequitable } from "@/utils";
|
||||
import {
|
||||
@@ -58,6 +58,102 @@ export interface TauriPlatformAdapter extends BasePlatformAdapter {
|
||||
|
||||
// Create Tauri adapter functions
|
||||
export const createTauriAdapter = (): TauriPlatformAdapter => {
|
||||
const getWebviewWindowTitle = (url: string) => {
|
||||
try {
|
||||
return new URL(url).hostname || "Coco";
|
||||
} catch {
|
||||
return "Coco";
|
||||
}
|
||||
};
|
||||
|
||||
const openUrlInWebviewWindow = async (url: string) => {
|
||||
const documentPreview = getDocumentPreviewRoute(url);
|
||||
const label = `webview-${Date.now()}-${Math.random()
|
||||
.toString(36)
|
||||
.slice(2, 8)}`;
|
||||
const route = documentPreview || `/ui/webview?url=${encodeURIComponent(url)}`;
|
||||
|
||||
return new WebviewWindow(label, {
|
||||
title: getWebviewWindowTitle(url),
|
||||
url: route,
|
||||
width: 1100,
|
||||
height: 760,
|
||||
minWidth: 640,
|
||||
minHeight: 420,
|
||||
center: true,
|
||||
resizable: true,
|
||||
maximizable: true,
|
||||
minimizable: true,
|
||||
closable: true,
|
||||
decorations: true,
|
||||
transparent: false,
|
||||
shadow: true,
|
||||
dragDropEnabled: false,
|
||||
});
|
||||
};
|
||||
|
||||
const getDocumentPreviewRoute = (
|
||||
url: string,
|
||||
options?: { serverId?: string; sourceUrl?: string }
|
||||
) => {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
const hashPath = parsed.hash.replace(/^#/, "");
|
||||
const routePath = `${parsed.pathname}${hashPath}`;
|
||||
const match = routePath.match(/\/preview\/document\/([^/?#]+)/);
|
||||
const documentId = match?.[1];
|
||||
const serverId = options?.serverId;
|
||||
|
||||
if (!documentId || !serverId) return "";
|
||||
|
||||
const params = new URLSearchParams({
|
||||
serverId,
|
||||
documentId,
|
||||
});
|
||||
|
||||
if (options?.sourceUrl) {
|
||||
params.set("sourceUrl", options.sourceUrl);
|
||||
}
|
||||
|
||||
return `/ui/document-preview?${params.toString()}`;
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
const openSearchUrl = async (url: string, data?: any) => {
|
||||
const documentPreviewRoute = getDocumentPreviewRoute(url, {
|
||||
serverId: data?.querySource?.id,
|
||||
sourceUrl: data?.metadata?.raw_content || data?.url,
|
||||
});
|
||||
|
||||
if (documentPreviewRoute) {
|
||||
const label = `document-preview-${Date.now()}-${Math.random()
|
||||
.toString(36)
|
||||
.slice(2, 8)}`;
|
||||
|
||||
return new WebviewWindow(label, {
|
||||
title: data?.title || getWebviewWindowTitle(url),
|
||||
url: documentPreviewRoute,
|
||||
width: 1100,
|
||||
height: 760,
|
||||
minWidth: 720,
|
||||
minHeight: 480,
|
||||
center: true,
|
||||
resizable: true,
|
||||
maximizable: true,
|
||||
minimizable: true,
|
||||
closable: true,
|
||||
decorations: true,
|
||||
transparent: false,
|
||||
shadow: true,
|
||||
dragDropEnabled: false,
|
||||
});
|
||||
}
|
||||
|
||||
return openUrlInWebviewWindow(url);
|
||||
};
|
||||
|
||||
return {
|
||||
async setWindowSize(width, height) {
|
||||
return windowWrapper.setLogicalSize(width, height);
|
||||
@@ -310,9 +406,13 @@ export const createTauriAdapter = (): TauriPlatformAdapter => {
|
||||
},
|
||||
|
||||
async openUrl(url) {
|
||||
return openUrlInWebviewWindow(url);
|
||||
},
|
||||
|
||||
async openUrlWithBrowser(url) {
|
||||
const { openUrl } = await import("@tauri-apps/plugin-opener");
|
||||
|
||||
openUrl(url);
|
||||
return openUrl(url);
|
||||
},
|
||||
|
||||
isWindows10,
|
||||
@@ -332,7 +432,7 @@ export const createTauriAdapter = (): TauriPlatformAdapter => {
|
||||
revealItemInDir(path);
|
||||
},
|
||||
|
||||
async openSearchItem(data) {
|
||||
async openSearchItem(data, formatUrl) {
|
||||
const { invoke } = await import("@tauri-apps/api/core");
|
||||
|
||||
console.log("openSearchItem", data);
|
||||
@@ -354,6 +454,14 @@ export const createTauriAdapter = (): TauriPlatformAdapter => {
|
||||
return invoke("hide_coco");
|
||||
};
|
||||
|
||||
const url = (formatUrl && formatUrl(data)) || data?.url;
|
||||
|
||||
if (url?.startsWith("http://") || url?.startsWith("https://")) {
|
||||
await openSearchUrl(url, data);
|
||||
|
||||
return hideCoco();
|
||||
}
|
||||
|
||||
if (data?.on_opened) {
|
||||
await invoke("open", {
|
||||
onOpened: data.on_opened,
|
||||
@@ -363,12 +471,6 @@ export const createTauriAdapter = (): TauriPlatformAdapter => {
|
||||
return hideCoco();
|
||||
}
|
||||
|
||||
if (data?.url) {
|
||||
OpenURLWithBrowser(data.url);
|
||||
|
||||
return hideCoco();
|
||||
}
|
||||
|
||||
if (data?.payload?.result?.value) {
|
||||
return copyToClipboard(data.payload.result.value);
|
||||
}
|
||||
|
||||
@@ -251,6 +251,13 @@ export const createWebAdapter = (): WebPlatformAdapter => {
|
||||
window.open(urlWithIntegrationId, "_blank");
|
||||
},
|
||||
|
||||
async openUrlWithBrowser(url) {
|
||||
const urlWithIntegrationId = appendAppIntegrationIdToUrl(url);
|
||||
|
||||
console.log(`Web mode opening URL in browser: ${urlWithIntegrationId}`);
|
||||
window.open(urlWithIntegrationId, "_blank");
|
||||
},
|
||||
|
||||
isWindows10: async () => false,
|
||||
|
||||
async setShadow(enable) {
|
||||
|
||||
Reference in New Issue
Block a user