mirror of
https://github.com/infinilabs/coco-app.git
synced 2026-08-29 01:59:22 +02:00
feat: add in-app document preview webview (#1077)
* feat: add in-app document preview webview * feat: improve mermaid viewer
This commit is contained in:
@@ -53,6 +53,7 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
|
"docx-preview": "^0.3.7",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
"filesize": "^10.1.6",
|
"filesize": "^10.1.6",
|
||||||
"i18next": "^23.16.8",
|
"i18next": "^23.16.8",
|
||||||
@@ -62,6 +63,7 @@
|
|||||||
"mdast-util-gfm-autolink-literal": "2.0.0",
|
"mdast-util-gfm-autolink-literal": "2.0.0",
|
||||||
"mermaid": "^11.6.0",
|
"mermaid": "^11.6.0",
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
|
"pptx-preview": "^1.0.7",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-day-picker": "^9.13.0",
|
"react-day-picker": "^9.13.0",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
|||||||
128
pnpm-lock.yaml
generated
128
pnpm-lock.yaml
generated
@@ -110,6 +110,9 @@ importers:
|
|||||||
dayjs:
|
dayjs:
|
||||||
specifier: ^1.11.13
|
specifier: ^1.11.13
|
||||||
version: 1.11.18
|
version: 1.11.18
|
||||||
|
docx-preview:
|
||||||
|
specifier: ^0.3.7
|
||||||
|
version: 0.3.7
|
||||||
dotenv:
|
dotenv:
|
||||||
specifier: ^16.5.0
|
specifier: ^16.5.0
|
||||||
version: 16.6.1
|
version: 16.6.1
|
||||||
@@ -137,6 +140,9 @@ importers:
|
|||||||
nanoid:
|
nanoid:
|
||||||
specifier: ^5.1.5
|
specifier: ^5.1.5
|
||||||
version: 5.1.6
|
version: 5.1.6
|
||||||
|
pptx-preview:
|
||||||
|
specifier: ^1.0.7
|
||||||
|
version: 1.0.7
|
||||||
react:
|
react:
|
||||||
specifier: ^18.3.1
|
specifier: ^18.3.1
|
||||||
version: 18.3.1
|
version: 18.3.1
|
||||||
@@ -2364,6 +2370,9 @@ packages:
|
|||||||
convert-source-map@2.0.0:
|
convert-source-map@2.0.0:
|
||||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||||
|
|
||||||
|
core-util-is@1.0.3:
|
||||||
|
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
|
||||||
|
|
||||||
cose-base@1.0.3:
|
cose-base@1.0.3:
|
||||||
resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==}
|
resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==}
|
||||||
|
|
||||||
@@ -2627,6 +2636,9 @@ packages:
|
|||||||
devlop@1.1.0:
|
devlop@1.1.0:
|
||||||
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
|
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:
|
dompurify@3.2.5:
|
||||||
resolution: {integrity: sha512-mLPd29uoRe9HpvwP2TxClGQBzGXeEC/we/q+bFlmPPmj2p2Ugl3r6ATu/UU1v77DXNcehiBg9zsr1dREyA/dJQ==}
|
resolution: {integrity: sha512-mLPd29uoRe9HpvwP2TxClGQBzGXeEC/we/q+bFlmPPmj2p2Ugl3r6ATu/UU1v77DXNcehiBg9zsr1dREyA/dJQ==}
|
||||||
|
|
||||||
@@ -2645,6 +2657,9 @@ packages:
|
|||||||
eastasianwidth@0.2.0:
|
eastasianwidth@0.2.0:
|
||||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||||
|
|
||||||
|
echarts@5.6.0:
|
||||||
|
resolution: {integrity: sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==}
|
||||||
|
|
||||||
electron-to-chromium@1.5.151:
|
electron-to-chromium@1.5.151:
|
||||||
resolution: {integrity: sha512-Rl6uugut2l9sLojjS4H4SAr3A4IgACMLgpuEMPYCVcKydzfyPrn5absNRju38IhQOf/NwjJY8OGWjlteqYeBCA==}
|
resolution: {integrity: sha512-Rl6uugut2l9sLojjS4H4SAr3A4IgACMLgpuEMPYCVcKydzfyPrn5absNRju38IhQOf/NwjJY8OGWjlteqYeBCA==}
|
||||||
|
|
||||||
@@ -2983,6 +2998,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
||||||
engines: {node: '>= 4'}
|
engines: {node: '>= 4'}
|
||||||
|
|
||||||
|
immediate@3.0.6:
|
||||||
|
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
|
||||||
|
|
||||||
immer@10.2.0:
|
immer@10.2.0:
|
||||||
resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==}
|
resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==}
|
||||||
|
|
||||||
@@ -3128,6 +3146,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==}
|
resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
|
|
||||||
|
isarray@1.0.0:
|
||||||
|
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
|
||||||
|
|
||||||
isexe@2.0.0:
|
isexe@2.0.0:
|
||||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||||
|
|
||||||
@@ -3173,6 +3194,9 @@ packages:
|
|||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
jszip@3.10.1:
|
||||||
|
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
|
||||||
|
|
||||||
katex@0.16.22:
|
katex@0.16.22:
|
||||||
resolution: {integrity: sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==}
|
resolution: {integrity: sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -3201,6 +3225,9 @@ packages:
|
|||||||
layout-base@2.0.1:
|
layout-base@2.0.1:
|
||||||
resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==}
|
resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==}
|
||||||
|
|
||||||
|
lie@3.3.0:
|
||||||
|
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
|
||||||
|
|
||||||
lightningcss-android-arm64@1.30.2:
|
lightningcss-android-arm64@1.30.2:
|
||||||
resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==}
|
resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
@@ -3658,6 +3685,9 @@ packages:
|
|||||||
package-manager-detector@1.5.0:
|
package-manager-detector@1.5.0:
|
||||||
resolution: {integrity: sha512-uBj69dVlYe/+wxj8JOpr97XfsxH/eumMt6HqjNTmJDf/6NO9s+0uxeOneIz3AsPt2m6y9PqzDzd3ATcU17MNfw==}
|
resolution: {integrity: sha512-uBj69dVlYe/+wxj8JOpr97XfsxH/eumMt6HqjNTmJDf/6NO9s+0uxeOneIz3AsPt2m6y9PqzDzd3ATcU17MNfw==}
|
||||||
|
|
||||||
|
pako@1.0.11:
|
||||||
|
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
|
||||||
|
|
||||||
parent-module@1.0.1:
|
parent-module@1.0.1:
|
||||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -3768,10 +3798,16 @@ packages:
|
|||||||
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
|
|
||||||
|
pptx-preview@1.0.7:
|
||||||
|
resolution: {integrity: sha512-YByocJuyxAR4YB4Q3+VAxdLfEvA5LojG1gAJsx2Mw0QU5FJPps/2fkJOupJ6oBbA+KdWRpuAk6G6T34rKCHVxw==}
|
||||||
|
|
||||||
pretty-ms@9.2.0:
|
pretty-ms@9.2.0:
|
||||||
resolution: {integrity: sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==}
|
resolution: {integrity: sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
process-nextick-args@2.0.1:
|
||||||
|
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
||||||
|
|
||||||
property-information@7.1.0:
|
property-information@7.1.0:
|
||||||
resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
|
resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
|
||||||
|
|
||||||
@@ -3916,6 +3952,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
|
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
readable-stream@2.3.8:
|
||||||
|
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
|
||||||
|
|
||||||
readdirp@4.1.2:
|
readdirp@4.1.2:
|
||||||
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
||||||
engines: {node: '>= 14.18.0'}
|
engines: {node: '>= 14.18.0'}
|
||||||
@@ -4020,6 +4059,9 @@ packages:
|
|||||||
rxjs@7.8.2:
|
rxjs@7.8.2:
|
||||||
resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
|
resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
|
||||||
|
|
||||||
|
safe-buffer@5.1.2:
|
||||||
|
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
|
||||||
|
|
||||||
safer-buffer@2.1.2:
|
safer-buffer@2.1.2:
|
||||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||||
|
|
||||||
@@ -4044,6 +4086,9 @@ packages:
|
|||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
setimmediate@1.0.5:
|
||||||
|
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
|
||||||
|
|
||||||
shebang-command@2.0.0:
|
shebang-command@2.0.0:
|
||||||
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -4115,6 +4160,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
|
resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
string_decoder@1.1.1:
|
||||||
|
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
|
||||||
|
|
||||||
stringify-entities@4.0.4:
|
stringify-entities@4.0.4:
|
||||||
resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
|
resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
|
||||||
|
|
||||||
@@ -4239,6 +4287,9 @@ packages:
|
|||||||
ts-interface-checker@0.1.13:
|
ts-interface-checker@0.1.13:
|
||||||
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
|
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
|
||||||
|
|
||||||
|
tslib@2.3.0:
|
||||||
|
resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
|
||||||
|
|
||||||
tslib@2.8.1:
|
tslib@2.8.1:
|
||||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||||
|
|
||||||
@@ -4379,6 +4430,14 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
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:
|
uuid@11.1.0:
|
||||||
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
|
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -4524,6 +4583,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==}
|
resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
zrender@5.6.1:
|
||||||
|
resolution: {integrity: sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==}
|
||||||
|
|
||||||
zustand@5.0.8:
|
zustand@5.0.8:
|
||||||
resolution: {integrity: sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==}
|
resolution: {integrity: sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==}
|
||||||
engines: {node: '>=12.20.0'}
|
engines: {node: '>=12.20.0'}
|
||||||
@@ -6440,6 +6502,8 @@ snapshots:
|
|||||||
|
|
||||||
convert-source-map@2.0.0: {}
|
convert-source-map@2.0.0: {}
|
||||||
|
|
||||||
|
core-util-is@1.0.3: {}
|
||||||
|
|
||||||
cose-base@1.0.3:
|
cose-base@1.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
layout-base: 1.0.2
|
layout-base: 1.0.2
|
||||||
@@ -6709,6 +6773,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
dequal: 2.0.3
|
dequal: 2.0.3
|
||||||
|
|
||||||
|
docx-preview@0.3.7:
|
||||||
|
dependencies:
|
||||||
|
jszip: 3.10.1
|
||||||
|
|
||||||
dompurify@3.2.5:
|
dompurify@3.2.5:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/trusted-types': 2.0.7
|
'@types/trusted-types': 2.0.7
|
||||||
@@ -6727,6 +6795,11 @@ snapshots:
|
|||||||
|
|
||||||
eastasianwidth@0.2.0: {}
|
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.151: {}
|
||||||
|
|
||||||
electron-to-chromium@1.5.241: {}
|
electron-to-chromium@1.5.241: {}
|
||||||
@@ -7169,6 +7242,8 @@ snapshots:
|
|||||||
|
|
||||||
ignore@5.3.2: {}
|
ignore@5.3.2: {}
|
||||||
|
|
||||||
|
immediate@3.0.6: {}
|
||||||
|
|
||||||
immer@10.2.0: {}
|
immer@10.2.0: {}
|
||||||
|
|
||||||
immutable@5.1.2: {}
|
immutable@5.1.2: {}
|
||||||
@@ -7279,6 +7354,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-inside-container: 1.0.0
|
is-inside-container: 1.0.0
|
||||||
|
|
||||||
|
isarray@1.0.0: {}
|
||||||
|
|
||||||
isexe@2.0.0: {}
|
isexe@2.0.0: {}
|
||||||
|
|
||||||
issue-parser@7.0.1:
|
issue-parser@7.0.1:
|
||||||
@@ -7315,6 +7392,13 @@ snapshots:
|
|||||||
|
|
||||||
json5@2.2.3: {}
|
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:
|
katex@0.16.22:
|
||||||
dependencies:
|
dependencies:
|
||||||
commander: 8.3.0
|
commander: 8.3.0
|
||||||
@@ -7341,6 +7425,10 @@ snapshots:
|
|||||||
|
|
||||||
layout-base@2.0.1: {}
|
layout-base@2.0.1: {}
|
||||||
|
|
||||||
|
lie@3.3.0:
|
||||||
|
dependencies:
|
||||||
|
immediate: 3.0.6
|
||||||
|
|
||||||
lightningcss-android-arm64@1.30.2:
|
lightningcss-android-arm64@1.30.2:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -8010,6 +8098,8 @@ snapshots:
|
|||||||
|
|
||||||
package-manager-detector@1.5.0: {}
|
package-manager-detector@1.5.0: {}
|
||||||
|
|
||||||
|
pako@1.0.11: {}
|
||||||
|
|
||||||
parent-module@1.0.1:
|
parent-module@1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
callsites: 3.1.0
|
callsites: 3.1.0
|
||||||
@@ -8113,10 +8203,20 @@ snapshots:
|
|||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
source-map-js: 1.2.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:
|
pretty-ms@9.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
parse-ms: 4.0.0
|
parse-ms: 4.0.0
|
||||||
|
|
||||||
|
process-nextick-args@2.0.1: {}
|
||||||
|
|
||||||
property-information@7.1.0: {}
|
property-information@7.1.0: {}
|
||||||
|
|
||||||
proto-list@1.2.4: {}
|
proto-list@1.2.4: {}
|
||||||
@@ -8270,6 +8370,16 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
loose-envify: 1.4.0
|
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: {}
|
readdirp@4.1.2: {}
|
||||||
|
|
||||||
rechoir@0.6.2:
|
rechoir@0.6.2:
|
||||||
@@ -8455,6 +8565,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
safe-buffer@5.1.2: {}
|
||||||
|
|
||||||
safer-buffer@2.1.2: {}
|
safer-buffer@2.1.2: {}
|
||||||
|
|
||||||
sass@1.93.2:
|
sass@1.93.2:
|
||||||
@@ -8475,6 +8587,8 @@ snapshots:
|
|||||||
|
|
||||||
semver@7.6.3: {}
|
semver@7.6.3: {}
|
||||||
|
|
||||||
|
setimmediate@1.0.5: {}
|
||||||
|
|
||||||
shebang-command@2.0.0:
|
shebang-command@2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
shebang-regex: 3.0.0
|
shebang-regex: 3.0.0
|
||||||
@@ -8545,6 +8659,10 @@ snapshots:
|
|||||||
get-east-asian-width: 1.3.0
|
get-east-asian-width: 1.3.0
|
||||||
strip-ansi: 7.1.0
|
strip-ansi: 7.1.0
|
||||||
|
|
||||||
|
string_decoder@1.1.1:
|
||||||
|
dependencies:
|
||||||
|
safe-buffer: 5.1.2
|
||||||
|
|
||||||
stringify-entities@4.0.4:
|
stringify-entities@4.0.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
character-entities-html4: 2.1.0
|
character-entities-html4: 2.1.0
|
||||||
@@ -8663,6 +8781,8 @@ snapshots:
|
|||||||
|
|
||||||
ts-interface-checker@0.1.13: {}
|
ts-interface-checker@0.1.13: {}
|
||||||
|
|
||||||
|
tslib@2.3.0: {}
|
||||||
|
|
||||||
tslib@2.8.1: {}
|
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):
|
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
|
react: 18.3.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
util-deprecate@1.0.2: {}
|
||||||
|
|
||||||
|
uuid@10.0.0: {}
|
||||||
|
|
||||||
uuid@11.1.0: {}
|
uuid@11.1.0: {}
|
||||||
|
|
||||||
vfile-location@5.0.3:
|
vfile-location@5.0.3:
|
||||||
@@ -8933,6 +9057,10 @@ snapshots:
|
|||||||
|
|
||||||
yoctocolors@2.1.1: {}
|
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)):
|
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:
|
optionalDependencies:
|
||||||
'@types/react': 18.3.26
|
'@types/react': 18.3.26
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ use serde_json::Value as Json;
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use tauri::{AppHandle, Emitter};
|
use tauri::{AppHandle, Emitter};
|
||||||
|
|
||||||
|
fn is_web_url(url: &str) -> bool {
|
||||||
|
url.starts_with("http://") || url.starts_with("https://")
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct RichLabel {
|
pub struct RichLabel {
|
||||||
pub label: Option<String>,
|
pub label: Option<String>,
|
||||||
@@ -165,7 +169,13 @@ pub(crate) async fn open(
|
|||||||
OnOpened::Document { url } => {
|
OnOpened::Document { url } => {
|
||||||
log::debug!("open 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")]
|
#[cfg(target_os = "macos")]
|
||||||
OnOpened::WindowManagementAction { action } => {
|
OnOpened::WindowManagementAction { action } => {
|
||||||
|
|||||||
@@ -211,6 +211,8 @@ pub fn run() {
|
|||||||
server::attachment::write_text_file,
|
server::attachment::write_text_file,
|
||||||
server::attachment::write_binary_file,
|
server::attachment::write_binary_file,
|
||||||
server::attachment::delete_attachment,
|
server::attachment::delete_attachment,
|
||||||
|
server::document::fetch_document_preview_resource,
|
||||||
|
server::document::get_document_preview,
|
||||||
server::transcription::transcription,
|
server::transcription::transcription,
|
||||||
server::system_settings::get_system_settings,
|
server::system_settings::get_system_settings,
|
||||||
extension::built_in::application::get_app_list,
|
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 auth;
|
||||||
pub mod connector;
|
pub mod connector;
|
||||||
pub mod datasource;
|
pub mod datasource;
|
||||||
|
pub mod document;
|
||||||
pub mod http_client;
|
pub mod http_client;
|
||||||
pub mod profile;
|
pub mod profile;
|
||||||
pub mod search;
|
pub mod search;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { OpenURLWithBrowser } from "@/utils/index";
|
import platformAdapter from "@/utils/platformAdapter";
|
||||||
import type { IChunkData } from "@/types/chat";
|
import type { IChunkData } from "@/types/chat";
|
||||||
import RetrieveIcon from "@/icons/Retrieve";
|
import RetrieveIcon from "@/icons/Retrieve";
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ export const FetchSource = ({
|
|||||||
const sourceClick = (item: ISourceData) => () => {
|
const sourceClick = (item: ISourceData) => () => {
|
||||||
const url = (formatUrl && formatUrl(item)) || item.url;
|
const url = (formatUrl && formatUrl(item)) || item.url;
|
||||||
if (url) {
|
if (url) {
|
||||||
OpenURLWithBrowser(url);
|
platformAdapter.openUrl(url);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import RehypeKatex from "rehype-katex";
|
|||||||
import RemarkGfm from "remark-gfm";
|
import RemarkGfm from "remark-gfm";
|
||||||
import RehypeHighlight from "rehype-highlight";
|
import RehypeHighlight from "rehype-highlight";
|
||||||
import mermaid from "mermaid";
|
import mermaid from "mermaid";
|
||||||
import { useDebouncedCallback } from "use-debounce";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Copy, Download, RotateCcw, ZoomIn, ZoomOut } from "lucide-react";
|
||||||
|
|
||||||
|
import Tooltip from "@/components/Common/Tooltip";
|
||||||
import {
|
import {
|
||||||
copyToClipboard,
|
copyToClipboard,
|
||||||
// useWindowSize
|
// useWindowSize
|
||||||
@@ -17,79 +19,642 @@ import {
|
|||||||
import "./markdown.scss";
|
import "./markdown.scss";
|
||||||
import "./highlight.css";
|
import "./highlight.css";
|
||||||
|
|
||||||
|
let mermaidDiagramId = 0;
|
||||||
|
const MERMAID_MIN_ZOOM = 0.5;
|
||||||
|
const MERMAID_MAX_ZOOM = 10;
|
||||||
|
const MERMAID_MAX_FIT_ZOOM = 8;
|
||||||
|
const MERMAID_ZOOM_STEP = 0.2;
|
||||||
|
const MERMAID_FIT_PADDING_RATIO = 0.92;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNextMermaidZoom(current: number, delta: number) {
|
||||||
|
const next = Math.min(
|
||||||
|
MERMAID_MAX_ZOOM,
|
||||||
|
Math.max(MERMAID_MIN_ZOOM, current + delta)
|
||||||
|
);
|
||||||
|
|
||||||
|
return Number(next.toFixed(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getElementBackgroundColor(element?: HTMLElement | null) {
|
||||||
|
if (!element) return "#fff";
|
||||||
|
|
||||||
|
const color = window.getComputedStyle(element).backgroundColor;
|
||||||
|
|
||||||
|
if (!color || color === "transparent" || color === "rgba(0, 0, 0, 0)") {
|
||||||
|
return getElementBackgroundColor(element.parentElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMermaidContentBox(svgElement: SVGSVGElement) {
|
||||||
|
try {
|
||||||
|
const box = svgElement.getBBox();
|
||||||
|
|
||||||
|
if (box.width > 0 && box.height > 0) {
|
||||||
|
return box;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[Mermaid] Failed to measure SVG content", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewBox = svgElement.viewBox?.baseVal;
|
||||||
|
if (viewBox?.width && viewBox?.height) return viewBox;
|
||||||
|
|
||||||
|
const width = Number.parseFloat(svgElement.getAttribute("width") || "");
|
||||||
|
const height = Number.parseFloat(svgElement.getAttribute("height") || "");
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: Number.isFinite(width) && width > 0 ? width : 1,
|
||||||
|
height: Number.isFinite(height) && height > 0 ? height : 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeMermaidSvgViewport(svgElement: SVGSVGElement) {
|
||||||
|
const box = getMermaidContentBox(svgElement);
|
||||||
|
const padding = Math.max(Math.min(box.width, box.height) * 0.04, 12);
|
||||||
|
const width = box.width + padding * 2;
|
||||||
|
const height = box.height + padding * 2;
|
||||||
|
|
||||||
|
if (!width || !height) return { width: 1, height: 1 };
|
||||||
|
|
||||||
|
svgElement.setAttribute(
|
||||||
|
"viewBox",
|
||||||
|
`${box.x - padding} ${box.y - padding} ${width} ${height}`
|
||||||
|
);
|
||||||
|
svgElement.setAttribute("width", String(width));
|
||||||
|
svgElement.setAttribute("height", String(height));
|
||||||
|
svgElement.style.width = `${width}px`;
|
||||||
|
svgElement.style.height = `${height}px`;
|
||||||
|
svgElement.style.maxWidth = "none";
|
||||||
|
|
||||||
|
return { width, height };
|
||||||
|
}
|
||||||
|
|
||||||
|
function centerMermaidStage(stage: HTMLElement) {
|
||||||
|
const maxLeft = stage.scrollWidth - stage.clientWidth;
|
||||||
|
const maxTop = stage.scrollHeight - stage.clientHeight;
|
||||||
|
|
||||||
|
if (maxLeft > 0 || maxTop > 0) {
|
||||||
|
stage.scrollTo({
|
||||||
|
left: Math.max(maxLeft / 2, 0),
|
||||||
|
top: Math.max(maxTop / 2, 0),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSvgExportSize(svgElement: SVGSVGElement) {
|
||||||
|
const width = Number.parseFloat(svgElement.getAttribute("width") || "");
|
||||||
|
const height = Number.parseFloat(svgElement.getAttribute("height") || "");
|
||||||
|
|
||||||
|
if (Number.isFinite(width) && width > 0 && Number.isFinite(height) && height > 0) {
|
||||||
|
return { width, height };
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewBox = svgElement.viewBox?.baseVal;
|
||||||
|
if (viewBox?.width && viewBox?.height) {
|
||||||
|
return {
|
||||||
|
width: viewBox.width,
|
||||||
|
height: viewBox.height,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const box = getMermaidContentBox(svgElement);
|
||||||
|
return {
|
||||||
|
width: box.width,
|
||||||
|
height: box.height,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceForeignObjectForExport(
|
||||||
|
originalForeignObject: SVGForeignObjectElement,
|
||||||
|
clonedForeignObject: SVGForeignObjectElement
|
||||||
|
) {
|
||||||
|
const text = originalForeignObject.textContent?.trim() || "";
|
||||||
|
|
||||||
|
if (!text) {
|
||||||
|
clonedForeignObject.remove();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelElement =
|
||||||
|
originalForeignObject.querySelector<HTMLElement>(
|
||||||
|
".nodeLabel, .edgeLabel, p, span, div"
|
||||||
|
) || originalForeignObject;
|
||||||
|
const labelStyle = window.getComputedStyle(labelElement);
|
||||||
|
const textElement = document.createElementNS(
|
||||||
|
"http://www.w3.org/2000/svg",
|
||||||
|
"text"
|
||||||
|
);
|
||||||
|
const x = Number.parseFloat(originalForeignObject.getAttribute("x") || "0");
|
||||||
|
const y = Number.parseFloat(originalForeignObject.getAttribute("y") || "0");
|
||||||
|
const width = Number.parseFloat(
|
||||||
|
originalForeignObject.getAttribute("width") || "0"
|
||||||
|
);
|
||||||
|
const height = Number.parseFloat(
|
||||||
|
originalForeignObject.getAttribute("height") || "0"
|
||||||
|
);
|
||||||
|
|
||||||
|
textElement.setAttribute("x", String(x + width / 2));
|
||||||
|
textElement.setAttribute("y", String(y + height / 2));
|
||||||
|
textElement.setAttribute("dominant-baseline", "middle");
|
||||||
|
textElement.setAttribute("text-anchor", "middle");
|
||||||
|
textElement.setAttribute("fill", labelStyle.color || "#333");
|
||||||
|
textElement.style.fontFamily =
|
||||||
|
labelStyle.fontFamily || "trebuchet ms, verdana, arial, sans-serif";
|
||||||
|
textElement.style.fontSize = labelStyle.fontSize || "16px";
|
||||||
|
textElement.style.fontWeight = labelStyle.fontWeight || "400";
|
||||||
|
textElement.textContent = text;
|
||||||
|
|
||||||
|
clonedForeignObject.parentNode?.replaceChild(textElement, clonedForeignObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
function serializeMermaidSvg(svgElement: SVGSVGElement) {
|
||||||
|
const clone = svgElement.cloneNode(true) as SVGSVGElement;
|
||||||
|
const originalForeignObjects = Array.from(
|
||||||
|
svgElement.querySelectorAll<SVGForeignObjectElement>("foreignObject")
|
||||||
|
);
|
||||||
|
const clonedForeignObjects = Array.from(
|
||||||
|
clone.querySelectorAll<SVGForeignObjectElement>("foreignObject")
|
||||||
|
);
|
||||||
|
|
||||||
|
clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
||||||
|
clone.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
|
||||||
|
|
||||||
|
clonedForeignObjects.forEach((clonedForeignObject, index) => {
|
||||||
|
const originalForeignObject = originalForeignObjects[index];
|
||||||
|
|
||||||
|
if (!originalForeignObject) {
|
||||||
|
clonedForeignObject.remove();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceForeignObjectForExport(originalForeignObject, clonedForeignObject);
|
||||||
|
});
|
||||||
|
|
||||||
|
return new XMLSerializer().serializeToString(clone);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadImage(src: string) {
|
||||||
|
return new Promise<HTMLImageElement>((resolve, reject) => {
|
||||||
|
const image = new Image();
|
||||||
|
|
||||||
|
image.onload = () => resolve(image);
|
||||||
|
image.onerror = () => reject(new Error("Failed to load Mermaid image"));
|
||||||
|
image.src = src;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function svgToPngBytes(
|
||||||
|
svgText: string,
|
||||||
|
size: { width: number; height: number },
|
||||||
|
backgroundColor: string
|
||||||
|
) {
|
||||||
|
const svgBlob = new Blob([svgText], {
|
||||||
|
type: "image/svg+xml;charset=utf-8",
|
||||||
|
});
|
||||||
|
const url = URL.createObjectURL(svgBlob);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const image = await loadImage(url);
|
||||||
|
const scale = Math.max(window.devicePixelRatio || 1, 2);
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
const width = Math.ceil(size.width * scale);
|
||||||
|
const height = Math.ceil(size.height * scale);
|
||||||
|
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
|
||||||
|
const context = canvas.getContext("2d");
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("Canvas is not available");
|
||||||
|
}
|
||||||
|
|
||||||
|
context.fillStyle = backgroundColor;
|
||||||
|
context.fillRect(0, 0, width, height);
|
||||||
|
context.drawImage(image, 0, 0, width, height);
|
||||||
|
|
||||||
|
const pngBlob = await new Promise<Blob>((resolve, reject) => {
|
||||||
|
canvas.toBlob((blob) => {
|
||||||
|
if (blob) {
|
||||||
|
resolve(blob);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
reject(new Error("Failed to export Mermaid PNG"));
|
||||||
|
}, "image/png");
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Uint8Array(await pngBlob.arrayBuffer());
|
||||||
|
} finally {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function savePngBytes(
|
||||||
|
bytes: Uint8Array,
|
||||||
|
fileName: string,
|
||||||
|
filterName: string
|
||||||
|
) {
|
||||||
|
if (window.__TAURI__?.dialog?.save && window.__TAURI__?.fs?.writeBinaryFile) {
|
||||||
|
let filePath = await window.__TAURI__.dialog.save({
|
||||||
|
defaultPath: fileName,
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
name: filterName,
|
||||||
|
extensions: ["png"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!filePath) return;
|
||||||
|
|
||||||
|
if (!filePath.toLowerCase().endsWith(".png")) {
|
||||||
|
filePath = `${filePath}.png`;
|
||||||
|
}
|
||||||
|
|
||||||
|
await window.__TAURI__.fs.writeBinaryFile(filePath, bytes);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pngBuffer = bytes.buffer.slice(
|
||||||
|
bytes.byteOffset,
|
||||||
|
bytes.byteOffset + bytes.byteLength
|
||||||
|
) as ArrayBuffer;
|
||||||
|
const blob = new Blob([pngBuffer], { type: "image/png" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement("a");
|
||||||
|
|
||||||
|
link.href = url;
|
||||||
|
link.download = fileName;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMermaidFitZoom(stage: HTMLElement, svgElement: SVGSVGElement) {
|
||||||
|
const styles = window.getComputedStyle(stage);
|
||||||
|
const verticalPadding =
|
||||||
|
Number.parseFloat(styles.paddingTop || "0") +
|
||||||
|
Number.parseFloat(styles.paddingBottom || "0");
|
||||||
|
const availableHeight = Math.max(stage.clientHeight - verticalPadding, 1);
|
||||||
|
const { height } = normalizeMermaidSvgViewport(svgElement);
|
||||||
|
|
||||||
|
if (!height) return 1;
|
||||||
|
|
||||||
|
const fitZoom = (availableHeight / height) * MERMAID_FIT_PADDING_RATIO;
|
||||||
|
|
||||||
|
return Number(
|
||||||
|
Math.min(
|
||||||
|
MERMAID_MAX_FIT_ZOOM,
|
||||||
|
Math.max(MERMAID_MIN_ZOOM, fitZoom)
|
||||||
|
).toFixed(2)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// 8
|
// 8
|
||||||
function Mermaid(props: { code: string }) {
|
function Mermaid(props: { code: string; onError?: () => void }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const [hasError, setHasError] = useState(false);
|
const stageRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
|
const [svg, setSvg] = useState("");
|
||||||
|
const [activeView, setActiveView] = useState<"image" | "code">("image");
|
||||||
|
const [zoom, setZoom] = useState(1);
|
||||||
|
const [fitZoom, setFitZoom] = useState(1);
|
||||||
|
const [diagramId] = useState(() => `mermaid-${++mermaidDiagramId}`);
|
||||||
|
const bindFunctionsRef = useRef<((element: Element) => void) | undefined>();
|
||||||
|
const userAdjustedZoomRef = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (props.code && ref.current) {
|
if (props.code) {
|
||||||
mermaid
|
let cancelled = false;
|
||||||
.run({
|
|
||||||
nodes: [ref.current],
|
setActiveView("image");
|
||||||
|
setErrorMessage("");
|
||||||
|
setSvg("");
|
||||||
|
setZoom(1);
|
||||||
|
setFitZoom(1);
|
||||||
|
userAdjustedZoomRef.current = false;
|
||||||
|
bindFunctionsRef.current = undefined;
|
||||||
|
mermaid.initialize({
|
||||||
|
startOnLoad: false,
|
||||||
|
theme: "default",
|
||||||
|
securityLevel: "loose",
|
||||||
|
suppressErrorRendering: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderDiagram = async () => {
|
||||||
|
const canParse = await mermaid.parse(props.code, {
|
||||||
suppressErrors: true,
|
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) => {
|
.catch((e) => {
|
||||||
setHasError(true);
|
if (cancelled) return;
|
||||||
console.error("[Mermaid] ", e.message);
|
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
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [props.code]);
|
}, [props.code]);
|
||||||
|
|
||||||
function viewSvgInNewWindow() {
|
useEffect(() => {
|
||||||
const svg = ref.current?.querySelector("svg");
|
if (svg && ref.current) {
|
||||||
if (!svg) return;
|
bindFunctionsRef.current?.(ref.current);
|
||||||
// const text = new XMLSerializer().serializeToString(svg);
|
}
|
||||||
// const blob = new Blob([text], { type: "image/svg+xml" });
|
}, [svg]);
|
||||||
// view img
|
|
||||||
// URL.createObjectURL(blob);
|
useEffect(() => {
|
||||||
|
const stage = stageRef.current;
|
||||||
|
const svgElement = ref.current?.querySelector("svg");
|
||||||
|
|
||||||
|
if (!svg || errorMessage || !stage || !svgElement) return;
|
||||||
|
|
||||||
|
let frame = 0;
|
||||||
|
|
||||||
|
const updateFitZoom = () => {
|
||||||
|
const nextFitZoom = getMermaidFitZoom(stage, svgElement);
|
||||||
|
|
||||||
|
setFitZoom(nextFitZoom);
|
||||||
|
if (!userAdjustedZoomRef.current) {
|
||||||
|
setZoom(nextFitZoom);
|
||||||
|
window.requestAnimationFrame(() => centerMermaidStage(stage));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
frame = window.requestAnimationFrame(updateFitZoom);
|
||||||
|
|
||||||
|
if (typeof ResizeObserver === "undefined") {
|
||||||
|
return () => {
|
||||||
|
window.cancelAnimationFrame(frame);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver(updateFitZoom);
|
||||||
|
resizeObserver.observe(stage);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.cancelAnimationFrame(frame);
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
};
|
||||||
|
}, [errorMessage, svg]);
|
||||||
|
|
||||||
|
function changeZoom(delta: number) {
|
||||||
|
userAdjustedZoomRef.current = true;
|
||||||
|
setZoom((current) => getNextMermaidZoom(current, delta));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasError) {
|
function resetZoom() {
|
||||||
return null;
|
userAdjustedZoomRef.current = false;
|
||||||
|
setZoom(fitZoom);
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
if (stageRef.current) {
|
||||||
|
centerMermaidStage(stageRef.current);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadPng() {
|
||||||
|
const svgElement = ref.current?.querySelector("svg");
|
||||||
|
if (!svgElement) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
normalizeMermaidSvgViewport(svgElement);
|
||||||
|
const svgText = serializeMermaidSvg(svgElement);
|
||||||
|
const pngBytes = await svgToPngBytes(
|
||||||
|
svgText,
|
||||||
|
getSvgExportSize(svgElement),
|
||||||
|
getElementBackgroundColor(stageRef.current)
|
||||||
|
);
|
||||||
|
|
||||||
|
await savePngBytes(
|
||||||
|
pngBytes,
|
||||||
|
`${diagramId}.png`,
|
||||||
|
t("markdown.mermaid.pngImage")
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[Mermaid] Failed to download PNG", error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="mermaid-viewer">
|
||||||
className={clsx("no-dark", "mermaid")}
|
<div className="mermaid-viewer-toolbar">
|
||||||
style={{
|
<div className="mermaid-viewer-tabs">
|
||||||
cursor: "pointer",
|
<Tooltip content={t("markdown.mermaid.showImage")}>
|
||||||
overflow: "auto",
|
<button
|
||||||
}}
|
type="button"
|
||||||
ref={ref}
|
className={clsx("mermaid-viewer-tab", {
|
||||||
onClick={() => viewSvgInNewWindow()}
|
active: activeView === "image",
|
||||||
>
|
})}
|
||||||
{props.code}
|
aria-label={t("markdown.mermaid.showImage")}
|
||||||
|
onClick={() => setActiveView("image")}
|
||||||
|
>
|
||||||
|
{t("markdown.mermaid.image")}
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip content={t("markdown.mermaid.showCode")}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={clsx("mermaid-viewer-tab", {
|
||||||
|
active: activeView === "code",
|
||||||
|
})}
|
||||||
|
aria-label={t("markdown.mermaid.showCode")}
|
||||||
|
onClick={() => setActiveView("code")}
|
||||||
|
>
|
||||||
|
{t("markdown.mermaid.code")}
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mermaid-viewer-actions">
|
||||||
|
{activeView === "image" ? (
|
||||||
|
<>
|
||||||
|
<Tooltip content={t("markdown.mermaid.zoomIn")}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="mermaid-viewer-action"
|
||||||
|
aria-label={t("markdown.mermaid.zoomIn")}
|
||||||
|
onClick={() => changeZoom(MERMAID_ZOOM_STEP)}
|
||||||
|
>
|
||||||
|
<ZoomIn size={18} strokeWidth={2} />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip content={t("markdown.mermaid.zoomOut")}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="mermaid-viewer-action"
|
||||||
|
aria-label={t("markdown.mermaid.zoomOut")}
|
||||||
|
disabled={zoom <= MERMAID_MIN_ZOOM}
|
||||||
|
onClick={() => changeZoom(-MERMAID_ZOOM_STEP)}
|
||||||
|
>
|
||||||
|
<ZoomOut size={18} strokeWidth={2} />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip content={t("markdown.mermaid.resetView")}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="mermaid-viewer-action"
|
||||||
|
aria-label={t("markdown.mermaid.resetView")}
|
||||||
|
onClick={resetZoom}
|
||||||
|
>
|
||||||
|
<RotateCcw size={18} strokeWidth={2} />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip content={t("markdown.mermaid.downloadPng")}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="mermaid-viewer-action"
|
||||||
|
aria-label={t("markdown.mermaid.downloadPng")}
|
||||||
|
disabled={!svg || Boolean(errorMessage)}
|
||||||
|
onClick={downloadPng}
|
||||||
|
>
|
||||||
|
<Download size={18} strokeWidth={2} />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Tooltip content={t("markdown.mermaid.copyCode")}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="mermaid-viewer-action"
|
||||||
|
aria-label={t("markdown.mermaid.copyCode")}
|
||||||
|
onClick={() => copyToClipboard(props.code)}
|
||||||
|
>
|
||||||
|
<Copy size={18} strokeWidth={2} />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mermaid-viewer-content">
|
||||||
|
<div
|
||||||
|
className="mermaid-diagram-stage"
|
||||||
|
ref={stageRef}
|
||||||
|
hidden={activeView !== "image"}
|
||||||
|
>
|
||||||
|
{errorMessage ? (
|
||||||
|
<div className="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>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="mermaid-diagram-canvas"
|
||||||
|
style={{ zoom } as React.CSSProperties & { zoom: number }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="mermaid-render-target"
|
||||||
|
ref={ref}
|
||||||
|
dangerouslySetInnerHTML={svg ? { __html: svg } : undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<pre className="mermaid-source-code" hidden={activeView !== "code"}>
|
||||||
|
<code>{props.code}</code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7
|
// 7
|
||||||
function PreCode(props: { children?: any; enableMermaid?: boolean }) {
|
function PreCode(props: {
|
||||||
|
children?: any;
|
||||||
|
enableMermaid?: boolean;
|
||||||
|
hideMermaidSource?: boolean;
|
||||||
|
}) {
|
||||||
const ref = useRef<HTMLPreElement>(null);
|
const ref = useRef<HTMLPreElement>(null);
|
||||||
// const previewRef = useRef<HTMLPreviewHander>(null);
|
// const previewRef = useRef<HTMLPreviewHander>(null);
|
||||||
const [mermaidCode, setMermaidCode] = useState("");
|
|
||||||
// const [htmlCode, setHtmlCode] = useState("");
|
// const [htmlCode, setHtmlCode] = useState("");
|
||||||
// const { height } = useWindowSize();
|
// const { height } = useWindowSize();
|
||||||
// console.log(htmlCode, height);
|
// console.log(htmlCode, height);
|
||||||
|
|
||||||
const renderArtifacts = useDebouncedCallback(() => {
|
const mermaidCode = useMemo(() => {
|
||||||
if (!ref.current) return;
|
if (!props.enableMermaid) return "";
|
||||||
if (!props.enableMermaid) return;
|
|
||||||
|
|
||||||
const mermaidDom = ref.current.querySelector("code.language-mermaid");
|
const className = getCodeClassName(props.children);
|
||||||
if (mermaidDom) {
|
if (!/\blanguage-mermaid\b/.test(className)) return "";
|
||||||
setMermaidCode((mermaidDom as HTMLElement).innerText);
|
|
||||||
}
|
return getNodeText(props.children).trim();
|
||||||
// const htmlDom = ref.current.querySelector("code.language-html");
|
}, [props.children, props.enableMermaid]);
|
||||||
// const refText = ref.current.querySelector("code")?.innerText;
|
|
||||||
// if (htmlDom) {
|
|
||||||
// setHtmlCode((htmlDom as HTMLElement).innerText);
|
|
||||||
// } else if (refText?.startsWith("<!DOCTYPE")) {
|
|
||||||
// setHtmlCode(refText);
|
|
||||||
// }
|
|
||||||
}, 600);
|
|
||||||
|
|
||||||
// const enableArtifacts = true;
|
// const enableArtifacts = true;
|
||||||
// console.log(enableArtifacts);
|
// console.log(enableArtifacts);
|
||||||
@@ -117,25 +682,33 @@ function PreCode(props: { children?: any; enableMermaid?: boolean }) {
|
|||||||
codeElement.style.whiteSpace = "pre-wrap";
|
codeElement.style.whiteSpace = "pre-wrap";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
setTimeout(renderArtifacts, 1);
|
|
||||||
}
|
}
|
||||||
}, []);
|
}, [props.children]);
|
||||||
|
|
||||||
|
const hideSource = props.hideMermaidSource && mermaidCode.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<pre ref={ref}>
|
{!hideSource && mermaidCode.length === 0 && (
|
||||||
<span
|
<pre
|
||||||
className="copy-code-button"
|
ref={ref}
|
||||||
onClick={() => {
|
className={clsx({
|
||||||
if (ref.current) {
|
"hidden-mermaid-source": hideSource,
|
||||||
copyToClipboard(
|
})}
|
||||||
ref.current.querySelector("code")?.innerText ?? ""
|
>
|
||||||
);
|
<span
|
||||||
}
|
className="copy-code-button"
|
||||||
}}
|
onClick={() => {
|
||||||
></span>
|
if (ref.current) {
|
||||||
{props.children}
|
copyToClipboard(
|
||||||
</pre>
|
ref.current.querySelector("code")?.innerText ?? ""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
></span>
|
||||||
|
{props.children}
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
{mermaidCode.length > 0 && (
|
{mermaidCode.length > 0 && (
|
||||||
<Mermaid code={mermaidCode} key={mermaidCode} />
|
<Mermaid code={mermaidCode} key={mermaidCode} />
|
||||||
)}
|
)}
|
||||||
@@ -238,6 +811,7 @@ function _MarkDownContent(props: {
|
|||||||
enableSyntaxHighlight?: boolean;
|
enableSyntaxHighlight?: boolean;
|
||||||
enableMermaid?: boolean;
|
enableMermaid?: boolean;
|
||||||
enableMath?: boolean;
|
enableMath?: boolean;
|
||||||
|
hideMermaidSource?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const enableMath = props.enableMath !== false;
|
const enableMath = props.enableMath !== false;
|
||||||
|
|
||||||
@@ -278,7 +852,11 @@ function _MarkDownContent(props: {
|
|||||||
rehypePlugins={rehypePlugins as any}
|
rehypePlugins={rehypePlugins as any}
|
||||||
components={{
|
components={{
|
||||||
pre: (preProps) => (
|
pre: (preProps) => (
|
||||||
<PreCode {...preProps} enableMermaid={props.enableMermaid !== false} />
|
<PreCode
|
||||||
|
{...preProps}
|
||||||
|
enableMermaid={props.enableMermaid !== false}
|
||||||
|
hideMermaidSource={props.hideMermaidSource}
|
||||||
|
/>
|
||||||
),
|
),
|
||||||
code: CustomCode,
|
code: CustomCode,
|
||||||
p: (pProps) => <p {...pProps} dir="auto" />,
|
p: (pProps) => <p {...pProps} dir="auto" />,
|
||||||
@@ -324,6 +902,7 @@ export default function Markdown(
|
|||||||
enableSyntaxHighlight?: boolean;
|
enableSyntaxHighlight?: boolean;
|
||||||
enableMermaid?: boolean;
|
enableMermaid?: boolean;
|
||||||
enableMath?: boolean;
|
enableMath?: boolean;
|
||||||
|
hideMermaidSource?: boolean;
|
||||||
} & React.DOMAttributes<HTMLDivElement>
|
} & React.DOMAttributes<HTMLDivElement>
|
||||||
) {
|
) {
|
||||||
const mdRef = useRef<HTMLDivElement>(null);
|
const mdRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -346,6 +925,7 @@ export default function Markdown(
|
|||||||
enableSyntaxHighlight={props.enableSyntaxHighlight}
|
enableSyntaxHighlight={props.enableSyntaxHighlight}
|
||||||
enableMermaid={props.enableMermaid}
|
enableMermaid={props.enableMermaid}
|
||||||
enableMath={props.enableMath}
|
enableMath={props.enableMath}
|
||||||
|
hideMermaidSource={props.hideMermaidSource}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -42,6 +42,10 @@
|
|||||||
--color-accent-emphasis: #0969da;
|
--color-accent-emphasis: #0969da;
|
||||||
--color-attention-subtle: #fff8c5;
|
--color-attention-subtle: #fff8c5;
|
||||||
--color-danger-fg: #cf222e;
|
--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 {
|
@mixin dark {
|
||||||
@@ -88,6 +92,10 @@
|
|||||||
--color-accent-emphasis: #1f6feb;
|
--color-accent-emphasis: #1f6feb;
|
||||||
--color-attention-subtle: rgba(187, 128, 9, 0.15);
|
--color-attention-subtle: rgba(187, 128, 9, 0.15);
|
||||||
--color-danger-fg: #f85149;
|
--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 {
|
.light {
|
||||||
@@ -1108,15 +1116,237 @@
|
|||||||
filter: invert(50%);
|
filter: invert(50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body .mermaid {
|
.markdown-body .mermaid-viewer {
|
||||||
border: var(--border-in-light);
|
position: relative;
|
||||||
margin-bottom: 10px;
|
width: 100%;
|
||||||
border-radius: 4px;
|
max-width: 100%;
|
||||||
padding: 10px;
|
margin: 12px 0 16px;
|
||||||
background-color: var(--white);
|
overflow: visible;
|
||||||
|
border: 1px solid var(--color-border-muted);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .mermaid-viewer-toolbar {
|
||||||
|
position: relative;
|
||||||
|
z-index: 3;
|
||||||
|
display: flex;
|
||||||
|
min-height: 54px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px 14px 10px 16px;
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
background: #f1f1f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .mermaid-viewer-tabs {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 2px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .mermaid-viewer-tab,
|
||||||
|
.markdown-body .mermaid-viewer-action {
|
||||||
|
appearance: none;
|
||||||
|
border: 0;
|
||||||
|
font: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .mermaid-viewer-tab {
|
||||||
|
min-width: 74px;
|
||||||
|
padding: 8px 14px;
|
||||||
|
border-radius: 7px;
|
||||||
|
background: transparent;
|
||||||
|
color: #4d4d4d;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .mermaid-viewer-tab.active {
|
||||||
|
background: #fff;
|
||||||
|
color: #222;
|
||||||
|
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .mermaid-viewer-actions {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .mermaid-viewer-action {
|
||||||
|
display: inline-flex;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: transparent;
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .mermaid-viewer-action:hover:not(:disabled) {
|
||||||
|
background: rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .mermaid-viewer-action:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.38;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .mermaid-viewer-content {
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 0 0 8px 8px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .mermaid-diagram-stage {
|
||||||
|
display: flex;
|
||||||
|
height: clamp(320px, 52vh, 560px);
|
||||||
|
min-height: 320px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 28px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .mermaid-diagram-stage[hidden],
|
||||||
|
.markdown-body .mermaid-source-code[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .mermaid-diagram-canvas {
|
||||||
|
display: inline-flex;
|
||||||
|
min-width: max-content;
|
||||||
|
min-height: max-content;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .mermaid-render-target:empty {
|
||||||
|
min-width: 240px;
|
||||||
|
min-height: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .mermaid-render-target svg {
|
||||||
|
display: block;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .mermaid-source-code {
|
||||||
|
min-height: 320px;
|
||||||
|
max-height: 560px;
|
||||||
|
margin: 0;
|
||||||
|
overflow: auto;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
background: #fff;
|
||||||
|
color: #444;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.75;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .mermaid-source-code code {
|
||||||
|
display: block;
|
||||||
|
padding: 24px;
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
white-space: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .mermaid-error {
|
||||||
|
display: flex;
|
||||||
|
min-height: 140px;
|
||||||
|
width: min(420px, 100%);
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 24px;
|
||||||
|
border: 1px solid var(--color-mermaid-error-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
#dmermaid {
|
#dmermaid {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:where(.dark, [data-theme="dark"]) & {
|
||||||
|
.markdown-body .mermaid-viewer {
|
||||||
|
border-color: var(--color-border-muted);
|
||||||
|
background: #0f1115;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .mermaid-viewer-toolbar {
|
||||||
|
background: #202328;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .mermaid-viewer-tabs {
|
||||||
|
background: #262a30;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .mermaid-viewer-tab {
|
||||||
|
color: #c2c7cf;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .mermaid-viewer-tab.active {
|
||||||
|
background: #111418;
|
||||||
|
color: #f2f4f8;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .mermaid-viewer-action {
|
||||||
|
color: #eef1f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .mermaid-viewer-action:hover:not(:disabled) {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .mermaid-viewer-content,
|
||||||
|
.markdown-body .mermaid-diagram-stage {
|
||||||
|
background: #0f1115;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .mermaid-render-target svg {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body .mermaid-source-code {
|
||||||
|
background: #0f1115;
|
||||||
|
color: #d7dbe2;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-box {
|
.tooltip-box {
|
||||||
|
--tooltip-offset: translateX(-50%);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background-color: rgba(0, 0, 0, 0.75);
|
background-color: rgba(0, 0, 0, 0.75);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
@@ -13,42 +14,43 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: scale(0.9);
|
transform: var(--tooltip-offset) scale(0.9);
|
||||||
transition: opacity 0.2s, transform 0.2s;
|
transition: opacity 0.2s, transform 0.2s;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
transform-origin: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-box.tooltip-top {
|
.tooltip-box.tooltip-top {
|
||||||
|
--tooltip-offset: translateX(-50%);
|
||||||
bottom: 100%;
|
bottom: 100%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-box.tooltip-bottom {
|
.tooltip-box.tooltip-bottom {
|
||||||
|
--tooltip-offset: translateX(-50%);
|
||||||
top: 100%;
|
top: 100%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-box.tooltip-left {
|
.tooltip-box.tooltip-left {
|
||||||
|
--tooltip-offset: translateY(-50%);
|
||||||
right: 100%;
|
right: 100%;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-box.tooltip-right {
|
.tooltip-box.tooltip-right {
|
||||||
|
--tooltip-offset: translateY(-50%);
|
||||||
left: 100%;
|
left: 100%;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-container:hover .tooltip-box,
|
.tooltip-container:hover .tooltip-box,
|
||||||
.tooltip-container:focus-within .tooltip-box {
|
.tooltip-container:focus-within .tooltip-box {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: scale(1);
|
transform: var(--tooltip-offset) scale(1);
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useClickAway, useCreation, useReactive } from "ahooks";
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { isNil, lowerCase, noop } from "lodash-es";
|
import { isNil, lowerCase, noop } from "lodash-es";
|
||||||
import {
|
import {
|
||||||
|
Compass,
|
||||||
Copy,
|
Copy,
|
||||||
Download,
|
Download,
|
||||||
Info,
|
Info,
|
||||||
@@ -22,6 +23,7 @@ import { useShortcutsStore } from "@/stores/shortcutsStore";
|
|||||||
import VisibleKey from "../Common/VisibleKey";
|
import VisibleKey from "../Common/VisibleKey";
|
||||||
import platformAdapter from "@/utils/platformAdapter";
|
import platformAdapter from "@/utils/platformAdapter";
|
||||||
import SearchEmpty from "../Common/SearchEmpty";
|
import SearchEmpty from "../Common/SearchEmpty";
|
||||||
|
import { OpenURLWithBrowser } from "@/utils";
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
activeMenuIndex: number;
|
activeMenuIndex: number;
|
||||||
@@ -112,6 +114,7 @@ const ContextMenu = ({ formatUrl }: ContextMenuProps) => {
|
|||||||
|
|
||||||
const { id, url, category, type, payload, source } = selectedSearchContent;
|
const { id, url, category, type, payload, source } = selectedSearchContent;
|
||||||
const { query, result } = payload ?? {};
|
const { query, result } = payload ?? {};
|
||||||
|
const formattedUrl = (formatUrl && formatUrl(selectedSearchContent)) || url;
|
||||||
|
|
||||||
if (category === "AI Overview") {
|
if (category === "AI Overview") {
|
||||||
return [];
|
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"),
|
name: t("search.contextMenu.copyLink"),
|
||||||
icon: <Link />,
|
icon: <Link />,
|
||||||
@@ -141,9 +159,7 @@ const ContextMenu = ({ formatUrl }: ContextMenuProps) => {
|
|||||||
type === "AI Assistant" ||
|
type === "AI Assistant" ||
|
||||||
id === "Extension Store",
|
id === "Extension Store",
|
||||||
clickEvent() {
|
clickEvent() {
|
||||||
copyToClipboard(
|
copyToClipboard(formattedUrl);
|
||||||
(formatUrl && formatUrl(selectedSearchContent)) || url
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
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,22 @@
|
|||||||
"cancel": "No"
|
"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",
|
||||||
|
"image": "Image",
|
||||||
|
"code": "Code",
|
||||||
|
"showImage": "Show image",
|
||||||
|
"showCode": "Show code",
|
||||||
|
"zoomIn": "Zoom in",
|
||||||
|
"zoomOut": "Zoom out",
|
||||||
|
"resetView": "Reset view",
|
||||||
|
"downloadPng": "Download PNG",
|
||||||
|
"copyCode": "Copy code",
|
||||||
|
"pngImage": "PNG Image"
|
||||||
|
}
|
||||||
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"openSource": "Open Source",
|
"openSource": "Open Source",
|
||||||
"cancelled": "Generation Cancelled",
|
"cancelled": "Generation Cancelled",
|
||||||
@@ -497,6 +513,7 @@
|
|||||||
},
|
},
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
"open": "Open",
|
"open": "Open",
|
||||||
|
"openInBrowser": "Open in Browser",
|
||||||
"copyLink": "Copy Link",
|
"copyLink": "Copy Link",
|
||||||
"copyAnswer": "Copy Answer",
|
"copyAnswer": "Copy Answer",
|
||||||
"copyUppercaseAnswer": "Copy Answer (in Word)",
|
"copyUppercaseAnswer": "Copy Answer (in Word)",
|
||||||
@@ -750,6 +767,29 @@
|
|||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
"notFound": "Not found"
|
"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": {
|
"selection": {
|
||||||
"title": "Selection Toolbar Settings",
|
"title": "Selection Toolbar Settings",
|
||||||
"actions": {
|
"actions": {
|
||||||
|
|||||||
@@ -56,6 +56,22 @@
|
|||||||
"cancel": "否"
|
"cancel": "否"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"markdown": {
|
||||||
|
"mermaid": {
|
||||||
|
"renderFailed": "思维导图无法显示",
|
||||||
|
"renderFailedDescription": "内容格式异常,暂时无法生成预览",
|
||||||
|
"image": "图片",
|
||||||
|
"code": "代码",
|
||||||
|
"showImage": "显示图片",
|
||||||
|
"showCode": "显示代码",
|
||||||
|
"zoomIn": "放大",
|
||||||
|
"zoomOut": "缩小",
|
||||||
|
"resetView": "重置视图",
|
||||||
|
"downloadPng": "下载 PNG",
|
||||||
|
"copyCode": "复制代码",
|
||||||
|
"pngImage": "PNG 图片"
|
||||||
|
}
|
||||||
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"openSource": "打开来源",
|
"openSource": "打开来源",
|
||||||
"cancelled": "生成已取消",
|
"cancelled": "生成已取消",
|
||||||
@@ -497,6 +513,7 @@
|
|||||||
},
|
},
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
"open": "打开",
|
"open": "打开",
|
||||||
|
"openInBrowser": "打开至浏览器",
|
||||||
"copyLink": "复制链接",
|
"copyLink": "复制链接",
|
||||||
"copyAnswer": "复制答案",
|
"copyAnswer": "复制答案",
|
||||||
"copyUppercaseAnswer": "复制答案(大写)",
|
"copyUppercaseAnswer": "复制答案(大写)",
|
||||||
@@ -749,6 +766,29 @@
|
|||||||
"loading": "加载中...",
|
"loading": "加载中...",
|
||||||
"notFound": "未找到"
|
"notFound": "未找到"
|
||||||
},
|
},
|
||||||
|
"documentPreview": {
|
||||||
|
"labels": {
|
||||||
|
"preview": "预览",
|
||||||
|
"aiInterpretation": "AI 解读"
|
||||||
|
},
|
||||||
|
"buttons": {
|
||||||
|
"openSource": "打开来源",
|
||||||
|
"reload": "重新加载"
|
||||||
|
},
|
||||||
|
"hints": {
|
||||||
|
"previewUnavailable": "当前文档类型暂不支持预览。",
|
||||||
|
"previewUnavailableTitle": "此文件无法预览",
|
||||||
|
"previewUnavailableDescription": "文件格式可能不受支持,或内容暂时不可用"
|
||||||
|
},
|
||||||
|
"loading": {
|
||||||
|
"document": "文档加载中...",
|
||||||
|
"preview": "预览加载中..."
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"missingParams": "缺少文档预览参数。",
|
||||||
|
"loadFailed": "文档加载失败"
|
||||||
|
}
|
||||||
|
},
|
||||||
"selection": {
|
"selection": {
|
||||||
"title": "划词工具栏设置",
|
"title": "划词工具栏设置",
|
||||||
"actions": {
|
"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);
|
setViewExtensionOpened(payload);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
platformAdapter.listenEvent("open_url_window", async ({ payload }) => {
|
||||||
|
if (typeof payload === "string" && payload) {
|
||||||
|
await platformAdapter.openUrl(payload);
|
||||||
|
}
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const { synthesizeItem } = useChatStore();
|
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 StandaloneChat = lazy(() => import("@/pages/chat/index"));
|
||||||
const CheckPage = lazy(() => import("@/pages/check/index"));
|
const CheckPage = lazy(() => import("@/pages/check/index"));
|
||||||
const SelectionWindow = lazy(() => import("@/pages/selection/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 = {
|
const routerOptions = {
|
||||||
basename: "/",
|
basename: "/",
|
||||||
@@ -30,6 +32,8 @@ export const router = createBrowserRouter(
|
|||||||
{ path: "/ui/chat", element: (<Suspense fallback={<></>}><StandaloneChat /></Suspense>) },
|
{ path: "/ui/chat", element: (<Suspense fallback={<></>}><StandaloneChat /></Suspense>) },
|
||||||
{ path: "/ui/check", element: (<Suspense fallback={<></>}><CheckPage /></Suspense>) },
|
{ path: "/ui/check", element: (<Suspense fallback={<></>}><CheckPage /></Suspense>) },
|
||||||
{ path: "/ui/selection", element: (<Suspense fallback={<></>}><SelectionWindow /></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;
|
oauth_success: any;
|
||||||
extension_install_success: any;
|
extension_install_success: any;
|
||||||
open_view_extension: ViewExtensionOpened;
|
open_view_extension: ViewExtensionOpened;
|
||||||
|
open_url_window: string;
|
||||||
"server-list-changed": Server[];
|
"server-list-changed": Server[];
|
||||||
"selection-text": string;
|
"selection-text": string;
|
||||||
"selection-ask-ai": any;
|
"selection-ask-ai": any;
|
||||||
@@ -124,6 +125,7 @@ export interface SystemOperations {
|
|||||||
relaunchApp: () => Promise<void>;
|
relaunchApp: () => Promise<void>;
|
||||||
isTauri: () => boolean;
|
isTauri: () => boolean;
|
||||||
openUrl: (url: string) => Promise<unknown>;
|
openUrl: (url: string) => Promise<unknown>;
|
||||||
|
openUrlWithBrowser: (url: string) => Promise<unknown>;
|
||||||
commands: <T>(commandName: string, ...args: any[]) => Promise<T>;
|
commands: <T>(commandName: string, ...args: any[]) => Promise<T>;
|
||||||
isWindows10: () => Promise<boolean>;
|
isWindows10: () => Promise<boolean>;
|
||||||
revealItemInDir: (path: string) => Promise<unknown>;
|
revealItemInDir: (path: string) => Promise<unknown>;
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export const OpenURLWithBrowser = async (url?: string) => {
|
|||||||
if (!url) return;
|
if (!url) return;
|
||||||
if (IsTauri()) {
|
if (IsTauri()) {
|
||||||
try {
|
try {
|
||||||
await platformAdapter.openUrl(url);
|
await platformAdapter.openUrlWithBrowser(url);
|
||||||
await platformAdapter.commands("hide_coco");
|
await platformAdapter.commands("hide_coco");
|
||||||
console.log("URL opened in default browser");
|
console.log("URL opened in default browser");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
import type { BasePlatformAdapter } from "@/types/platform";
|
import type { BasePlatformAdapter } from "@/types/platform";
|
||||||
import type { AppTheme } from "@/types/index";
|
import type { AppTheme } from "@/types/index";
|
||||||
import { useAppearanceStore } from "@/stores/appearanceStore";
|
import { useAppearanceStore } from "@/stores/appearanceStore";
|
||||||
import { copyToClipboard, dispatchEvent, OpenURLWithBrowser } from ".";
|
import { copyToClipboard, dispatchEvent } from ".";
|
||||||
import { useAppStore } from "@/stores/appStore";
|
import { useAppStore } from "@/stores/appStore";
|
||||||
import { unrequitable } from "@/utils";
|
import { unrequitable } from "@/utils";
|
||||||
import {
|
import {
|
||||||
@@ -58,6 +58,102 @@ export interface TauriPlatformAdapter extends BasePlatformAdapter {
|
|||||||
|
|
||||||
// Create Tauri adapter functions
|
// Create Tauri adapter functions
|
||||||
export const createTauriAdapter = (): TauriPlatformAdapter => {
|
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 {
|
return {
|
||||||
async setWindowSize(width, height) {
|
async setWindowSize(width, height) {
|
||||||
return windowWrapper.setLogicalSize(width, height);
|
return windowWrapper.setLogicalSize(width, height);
|
||||||
@@ -310,9 +406,13 @@ export const createTauriAdapter = (): TauriPlatformAdapter => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async openUrl(url) {
|
async openUrl(url) {
|
||||||
|
return openUrlInWebviewWindow(url);
|
||||||
|
},
|
||||||
|
|
||||||
|
async openUrlWithBrowser(url) {
|
||||||
const { openUrl } = await import("@tauri-apps/plugin-opener");
|
const { openUrl } = await import("@tauri-apps/plugin-opener");
|
||||||
|
|
||||||
openUrl(url);
|
return openUrl(url);
|
||||||
},
|
},
|
||||||
|
|
||||||
isWindows10,
|
isWindows10,
|
||||||
@@ -332,7 +432,7 @@ export const createTauriAdapter = (): TauriPlatformAdapter => {
|
|||||||
revealItemInDir(path);
|
revealItemInDir(path);
|
||||||
},
|
},
|
||||||
|
|
||||||
async openSearchItem(data) {
|
async openSearchItem(data, formatUrl) {
|
||||||
const { invoke } = await import("@tauri-apps/api/core");
|
const { invoke } = await import("@tauri-apps/api/core");
|
||||||
|
|
||||||
console.log("openSearchItem", data);
|
console.log("openSearchItem", data);
|
||||||
@@ -354,6 +454,14 @@ export const createTauriAdapter = (): TauriPlatformAdapter => {
|
|||||||
return invoke("hide_coco");
|
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) {
|
if (data?.on_opened) {
|
||||||
await invoke("open", {
|
await invoke("open", {
|
||||||
onOpened: data.on_opened,
|
onOpened: data.on_opened,
|
||||||
@@ -363,12 +471,6 @@ export const createTauriAdapter = (): TauriPlatformAdapter => {
|
|||||||
return hideCoco();
|
return hideCoco();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data?.url) {
|
|
||||||
OpenURLWithBrowser(data.url);
|
|
||||||
|
|
||||||
return hideCoco();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data?.payload?.result?.value) {
|
if (data?.payload?.result?.value) {
|
||||||
return copyToClipboard(data.payload.result.value);
|
return copyToClipboard(data.payload.result.value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -251,6 +251,13 @@ export const createWebAdapter = (): WebPlatformAdapter => {
|
|||||||
window.open(urlWithIntegrationId, "_blank");
|
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,
|
isWindows10: async () => false,
|
||||||
|
|
||||||
async setShadow(enable) {
|
async setShadow(enable) {
|
||||||
|
|||||||
Reference in New Issue
Block a user