Compare commits
4 Commits
bd09e67988
...
6047af071c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6047af071c | ||
|
|
0acbc74192 | ||
|
|
2c292b0a8f | ||
|
|
6c411438e0 |
@ -17,7 +17,9 @@
|
|||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"drizzle-orm": "^0.45.1",
|
"drizzle-orm": "^0.45.1",
|
||||||
|
"html2canvas": "^1.4.1",
|
||||||
"jose": "^6.1.3",
|
"jose": "^6.1.3",
|
||||||
|
"jspdf": "^3.0.4",
|
||||||
"lucide-react": "^0.561.0",
|
"lucide-react": "^0.561.0",
|
||||||
"mammoth": "^1.11.0",
|
"mammoth": "^1.11.0",
|
||||||
"nanoid": "^5.1.6",
|
"nanoid": "^5.1.6",
|
||||||
|
|||||||
174
pnpm-lock.yaml
174
pnpm-lock.yaml
@ -20,9 +20,15 @@ importers:
|
|||||||
drizzle-orm:
|
drizzle-orm:
|
||||||
specifier: ^0.45.1
|
specifier: ^0.45.1
|
||||||
version: 0.45.1(@types/pg@8.16.0)(pg@8.16.3)
|
version: 0.45.1(@types/pg@8.16.0)(pg@8.16.3)
|
||||||
|
html2canvas:
|
||||||
|
specifier: ^1.4.1
|
||||||
|
version: 1.4.1
|
||||||
jose:
|
jose:
|
||||||
specifier: ^6.1.3
|
specifier: ^6.1.3
|
||||||
version: 6.1.3
|
version: 6.1.3
|
||||||
|
jspdf:
|
||||||
|
specifier: ^3.0.4
|
||||||
|
version: 3.0.4
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^0.561.0
|
specifier: ^0.561.0
|
||||||
version: 0.561.0(react@19.2.1)
|
version: 0.561.0(react@19.2.1)
|
||||||
@ -299,6 +305,10 @@ packages:
|
|||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
'@babel/runtime@7.28.4':
|
||||||
|
resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==}
|
||||||
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
'@babel/template@7.27.2':
|
'@babel/template@7.27.2':
|
||||||
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
|
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
@ -1358,12 +1368,18 @@ packages:
|
|||||||
'@types/nodemailer@7.0.4':
|
'@types/nodemailer@7.0.4':
|
||||||
resolution: {integrity: sha512-ee8fxWqOchH+Hv6MDDNNy028kwvVnLplrStm4Zf/3uHWw5zzo8FoYYeffpJtGs2wWysEumMH0ZIdMGMY1eMAow==}
|
resolution: {integrity: sha512-ee8fxWqOchH+Hv6MDDNNy028kwvVnLplrStm4Zf/3uHWw5zzo8FoYYeffpJtGs2wWysEumMH0ZIdMGMY1eMAow==}
|
||||||
|
|
||||||
|
'@types/pako@2.0.4':
|
||||||
|
resolution: {integrity: sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==}
|
||||||
|
|
||||||
'@types/pg@8.16.0':
|
'@types/pg@8.16.0':
|
||||||
resolution: {integrity: sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==}
|
resolution: {integrity: sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==}
|
||||||
|
|
||||||
'@types/prismjs@1.26.5':
|
'@types/prismjs@1.26.5':
|
||||||
resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==}
|
resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==}
|
||||||
|
|
||||||
|
'@types/raf@3.4.3':
|
||||||
|
resolution: {integrity: sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==}
|
||||||
|
|
||||||
'@types/react-dom@19.2.3':
|
'@types/react-dom@19.2.3':
|
||||||
resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
|
resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -1372,6 +1388,9 @@ packages:
|
|||||||
'@types/react@19.2.7':
|
'@types/react@19.2.7':
|
||||||
resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==}
|
resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==}
|
||||||
|
|
||||||
|
'@types/trusted-types@2.0.7':
|
||||||
|
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
|
||||||
|
|
||||||
'@types/unist@2.0.11':
|
'@types/unist@2.0.11':
|
||||||
resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
|
resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
|
||||||
|
|
||||||
@ -1627,6 +1646,10 @@ packages:
|
|||||||
balanced-match@1.0.2:
|
balanced-match@1.0.2:
|
||||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||||
|
|
||||||
|
base64-arraybuffer@1.0.2:
|
||||||
|
resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==}
|
||||||
|
engines: {node: '>= 0.6.0'}
|
||||||
|
|
||||||
base64-js@1.5.1:
|
base64-js@1.5.1:
|
||||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||||
|
|
||||||
@ -1684,6 +1707,10 @@ packages:
|
|||||||
caniuse-lite@1.0.30001760:
|
caniuse-lite@1.0.30001760:
|
||||||
resolution: {integrity: sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==}
|
resolution: {integrity: sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==}
|
||||||
|
|
||||||
|
canvg@3.0.11:
|
||||||
|
resolution: {integrity: sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==}
|
||||||
|
engines: {node: '>=10.0.0'}
|
||||||
|
|
||||||
ccount@2.0.1:
|
ccount@2.0.1:
|
||||||
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
|
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
|
||||||
|
|
||||||
@ -1734,6 +1761,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-js@3.47.0:
|
||||||
|
resolution: {integrity: sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==}
|
||||||
|
|
||||||
core-util-is@1.0.3:
|
core-util-is@1.0.3:
|
||||||
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
|
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
|
||||||
|
|
||||||
@ -1746,6 +1776,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
|
css-line-break@2.1.0:
|
||||||
|
resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==}
|
||||||
|
|
||||||
csstype@3.2.3:
|
csstype@3.2.3:
|
||||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||||
|
|
||||||
@ -1813,6 +1846,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
|
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
dompurify@3.3.1:
|
||||||
|
resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==}
|
||||||
|
|
||||||
drizzle-kit@0.31.8:
|
drizzle-kit@0.31.8:
|
||||||
resolution: {integrity: sha512-O9EC/miwdnRDY10qRxM8P3Pg8hXe3LyU4ZipReKOgTwn4OqANmftj8XJz1UPUAS6NMHf0E2htjsbQujUTkncCg==}
|
resolution: {integrity: sha512-O9EC/miwdnRDY10qRxM8P3Pg8hXe3LyU4ZipReKOgTwn4OqANmftj8XJz1UPUAS6NMHf0E2htjsbQujUTkncCg==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@ -2125,6 +2161,9 @@ packages:
|
|||||||
fast-levenshtein@2.0.6:
|
fast-levenshtein@2.0.6:
|
||||||
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
|
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
|
||||||
|
|
||||||
|
fast-png@6.4.0:
|
||||||
|
resolution: {integrity: sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==}
|
||||||
|
|
||||||
fast-xml-parser@5.2.5:
|
fast-xml-parser@5.2.5:
|
||||||
resolution: {integrity: sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==}
|
resolution: {integrity: sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@ -2144,6 +2183,9 @@ packages:
|
|||||||
picomatch:
|
picomatch:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
fflate@0.8.2:
|
||||||
|
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
|
||||||
|
|
||||||
file-entry-cache@8.0.0:
|
file-entry-cache@8.0.0:
|
||||||
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
|
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
|
||||||
engines: {node: '>=16.0.0'}
|
engines: {node: '>=16.0.0'}
|
||||||
@ -2278,6 +2320,10 @@ packages:
|
|||||||
html-url-attributes@3.0.1:
|
html-url-attributes@3.0.1:
|
||||||
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
|
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
|
||||||
|
|
||||||
|
html2canvas@1.4.1:
|
||||||
|
resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==}
|
||||||
|
engines: {node: '>=8.0.0'}
|
||||||
|
|
||||||
ignore@5.3.2:
|
ignore@5.3.2:
|
||||||
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
||||||
engines: {node: '>= 4'}
|
engines: {node: '>= 4'}
|
||||||
@ -2307,6 +2353,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
|
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
iobuffer@5.4.0:
|
||||||
|
resolution: {integrity: sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==}
|
||||||
|
|
||||||
is-alphabetical@2.0.1:
|
is-alphabetical@2.0.1:
|
||||||
resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==}
|
resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==}
|
||||||
|
|
||||||
@ -2476,6 +2525,9 @@ packages:
|
|||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
jspdf@3.0.4:
|
||||||
|
resolution: {integrity: sha512-dc6oQ8y37rRcHn316s4ngz/nOjayLF/FFxBF4V9zamQKRqXxyiH1zagkCdktdWhtoQId5K20xt1lB90XzkB+hQ==}
|
||||||
|
|
||||||
jsx-ast-utils@3.3.5:
|
jsx-ast-utils@3.3.5:
|
||||||
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
|
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
|
||||||
engines: {node: '>=4.0'}
|
engines: {node: '>=4.0'}
|
||||||
@ -2860,6 +2912,9 @@ packages:
|
|||||||
pako@1.0.11:
|
pako@1.0.11:
|
||||||
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
|
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
|
||||||
|
|
||||||
|
pako@2.1.0:
|
||||||
|
resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==}
|
||||||
|
|
||||||
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'}
|
||||||
@ -2885,6 +2940,9 @@ packages:
|
|||||||
pend@1.2.0:
|
pend@1.2.0:
|
||||||
resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
|
resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
|
||||||
|
|
||||||
|
performance-now@2.1.0:
|
||||||
|
resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==}
|
||||||
|
|
||||||
pg-cloudflare@1.2.7:
|
pg-cloudflare@1.2.7:
|
||||||
resolution: {integrity: sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==}
|
resolution: {integrity: sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==}
|
||||||
|
|
||||||
@ -2982,6 +3040,9 @@ packages:
|
|||||||
queue-microtask@1.2.3:
|
queue-microtask@1.2.3:
|
||||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||||
|
|
||||||
|
raf@3.4.1:
|
||||||
|
resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==}
|
||||||
|
|
||||||
react-dom@19.2.1:
|
react-dom@19.2.1:
|
||||||
resolution: {integrity: sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==}
|
resolution: {integrity: sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -3007,6 +3068,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
|
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
regenerator-runtime@0.13.11:
|
||||||
|
resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
|
||||||
|
|
||||||
regexp.prototype.flags@1.5.4:
|
regexp.prototype.flags@1.5.4:
|
||||||
resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
|
resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@ -3043,6 +3107,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
|
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
|
||||||
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
|
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
|
||||||
|
|
||||||
|
rgbcolor@1.0.1:
|
||||||
|
resolution: {integrity: sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==}
|
||||||
|
engines: {node: '>= 0.8.15'}
|
||||||
|
|
||||||
run-parallel@1.2.0:
|
run-parallel@1.2.0:
|
||||||
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
||||||
|
|
||||||
@ -3154,6 +3222,10 @@ packages:
|
|||||||
stable-hash@0.0.5:
|
stable-hash@0.0.5:
|
||||||
resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==}
|
resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==}
|
||||||
|
|
||||||
|
stackblur-canvas@2.7.0:
|
||||||
|
resolution: {integrity: sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==}
|
||||||
|
engines: {node: '>=0.1.14'}
|
||||||
|
|
||||||
stop-iteration-iterator@1.1.0:
|
stop-iteration-iterator@1.1.0:
|
||||||
resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
|
resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@ -3225,6 +3297,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
svg-pathdata@6.0.3:
|
||||||
|
resolution: {integrity: sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
tailwindcss@4.1.18:
|
tailwindcss@4.1.18:
|
||||||
resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==}
|
resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==}
|
||||||
|
|
||||||
@ -3232,6 +3308,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
|
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
text-segmentation@1.0.3:
|
||||||
|
resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==}
|
||||||
|
|
||||||
tinyglobby@0.2.15:
|
tinyglobby@0.2.15:
|
||||||
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
@ -3338,6 +3417,9 @@ packages:
|
|||||||
util-deprecate@1.0.2:
|
util-deprecate@1.0.2:
|
||||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
|
|
||||||
|
utrie@1.0.2:
|
||||||
|
resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==}
|
||||||
|
|
||||||
vfile-message@4.0.3:
|
vfile-message@4.0.3:
|
||||||
resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==}
|
resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==}
|
||||||
|
|
||||||
@ -3897,6 +3979,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@babel/types': 7.28.5
|
'@babel/types': 7.28.5
|
||||||
|
|
||||||
|
'@babel/runtime@7.28.4': {}
|
||||||
|
|
||||||
'@babel/template@7.27.2':
|
'@babel/template@7.27.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/code-frame': 7.27.1
|
'@babel/code-frame': 7.27.1
|
||||||
@ -4787,6 +4871,8 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- aws-crt
|
- aws-crt
|
||||||
|
|
||||||
|
'@types/pako@2.0.4': {}
|
||||||
|
|
||||||
'@types/pg@8.16.0':
|
'@types/pg@8.16.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 20.19.27
|
'@types/node': 20.19.27
|
||||||
@ -4795,6 +4881,9 @@ snapshots:
|
|||||||
|
|
||||||
'@types/prismjs@1.26.5': {}
|
'@types/prismjs@1.26.5': {}
|
||||||
|
|
||||||
|
'@types/raf@3.4.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@types/react-dom@19.2.3(@types/react@19.2.7)':
|
'@types/react-dom@19.2.3(@types/react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/react': 19.2.7
|
'@types/react': 19.2.7
|
||||||
@ -4803,6 +4892,9 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
csstype: 3.2.3
|
csstype: 3.2.3
|
||||||
|
|
||||||
|
'@types/trusted-types@2.0.7':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@types/unist@2.0.11': {}
|
'@types/unist@2.0.11': {}
|
||||||
|
|
||||||
'@types/unist@3.0.3': {}
|
'@types/unist@3.0.3': {}
|
||||||
@ -5071,6 +5163,8 @@ snapshots:
|
|||||||
|
|
||||||
balanced-match@1.0.2: {}
|
balanced-match@1.0.2: {}
|
||||||
|
|
||||||
|
base64-arraybuffer@1.0.2: {}
|
||||||
|
|
||||||
base64-js@1.5.1: {}
|
base64-js@1.5.1: {}
|
||||||
|
|
||||||
baseline-browser-mapping@2.9.9: {}
|
baseline-browser-mapping@2.9.9: {}
|
||||||
@ -5127,6 +5221,18 @@ snapshots:
|
|||||||
|
|
||||||
caniuse-lite@1.0.30001760: {}
|
caniuse-lite@1.0.30001760: {}
|
||||||
|
|
||||||
|
canvg@3.0.11:
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.28.4
|
||||||
|
'@types/raf': 3.4.3
|
||||||
|
core-js: 3.47.0
|
||||||
|
raf: 3.4.1
|
||||||
|
regenerator-runtime: 0.13.11
|
||||||
|
rgbcolor: 1.0.1
|
||||||
|
stackblur-canvas: 2.7.0
|
||||||
|
svg-pathdata: 6.0.3
|
||||||
|
optional: true
|
||||||
|
|
||||||
ccount@2.0.1: {}
|
ccount@2.0.1: {}
|
||||||
|
|
||||||
cfb@1.2.2:
|
cfb@1.2.2:
|
||||||
@ -5165,6 +5271,9 @@ snapshots:
|
|||||||
|
|
||||||
convert-source-map@2.0.0: {}
|
convert-source-map@2.0.0: {}
|
||||||
|
|
||||||
|
core-js@3.47.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
core-util-is@1.0.3: {}
|
core-util-is@1.0.3: {}
|
||||||
|
|
||||||
crc-32@1.2.2: {}
|
crc-32@1.2.2: {}
|
||||||
@ -5175,6 +5284,10 @@ snapshots:
|
|||||||
shebang-command: 2.0.0
|
shebang-command: 2.0.0
|
||||||
which: 2.0.2
|
which: 2.0.2
|
||||||
|
|
||||||
|
css-line-break@2.1.0:
|
||||||
|
dependencies:
|
||||||
|
utrie: 1.0.2
|
||||||
|
|
||||||
csstype@3.2.3: {}
|
csstype@3.2.3: {}
|
||||||
|
|
||||||
damerau-levenshtein@1.0.8: {}
|
damerau-levenshtein@1.0.8: {}
|
||||||
@ -5237,6 +5350,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
esutils: 2.0.3
|
esutils: 2.0.3
|
||||||
|
|
||||||
|
dompurify@3.3.1:
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/trusted-types': 2.0.7
|
||||||
|
optional: true
|
||||||
|
|
||||||
drizzle-kit@0.31.8:
|
drizzle-kit@0.31.8:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@drizzle-team/brocli': 0.10.2
|
'@drizzle-team/brocli': 0.10.2
|
||||||
@ -5688,6 +5806,12 @@ snapshots:
|
|||||||
|
|
||||||
fast-levenshtein@2.0.6: {}
|
fast-levenshtein@2.0.6: {}
|
||||||
|
|
||||||
|
fast-png@6.4.0:
|
||||||
|
dependencies:
|
||||||
|
'@types/pako': 2.0.4
|
||||||
|
iobuffer: 5.4.0
|
||||||
|
pako: 2.1.0
|
||||||
|
|
||||||
fast-xml-parser@5.2.5:
|
fast-xml-parser@5.2.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
strnum: 2.1.2
|
strnum: 2.1.2
|
||||||
@ -5704,6 +5828,8 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
picomatch: 4.0.3
|
picomatch: 4.0.3
|
||||||
|
|
||||||
|
fflate@0.8.2: {}
|
||||||
|
|
||||||
file-entry-cache@8.0.0:
|
file-entry-cache@8.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
flat-cache: 4.0.1
|
flat-cache: 4.0.1
|
||||||
@ -5853,6 +5979,11 @@ snapshots:
|
|||||||
|
|
||||||
html-url-attributes@3.0.1: {}
|
html-url-attributes@3.0.1: {}
|
||||||
|
|
||||||
|
html2canvas@1.4.1:
|
||||||
|
dependencies:
|
||||||
|
css-line-break: 2.1.0
|
||||||
|
text-segmentation: 1.0.3
|
||||||
|
|
||||||
ignore@5.3.2: {}
|
ignore@5.3.2: {}
|
||||||
|
|
||||||
ignore@7.0.5: {}
|
ignore@7.0.5: {}
|
||||||
@ -5876,6 +6007,8 @@ snapshots:
|
|||||||
hasown: 2.0.2
|
hasown: 2.0.2
|
||||||
side-channel: 1.1.0
|
side-channel: 1.1.0
|
||||||
|
|
||||||
|
iobuffer@5.4.0: {}
|
||||||
|
|
||||||
is-alphabetical@2.0.1: {}
|
is-alphabetical@2.0.1: {}
|
||||||
|
|
||||||
is-alphanumerical@2.0.1:
|
is-alphanumerical@2.0.1:
|
||||||
@ -6040,6 +6173,17 @@ snapshots:
|
|||||||
|
|
||||||
json5@2.2.3: {}
|
json5@2.2.3: {}
|
||||||
|
|
||||||
|
jspdf@3.0.4:
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.28.4
|
||||||
|
fast-png: 6.4.0
|
||||||
|
fflate: 0.8.2
|
||||||
|
optionalDependencies:
|
||||||
|
canvg: 3.0.11
|
||||||
|
core-js: 3.47.0
|
||||||
|
dompurify: 3.3.1
|
||||||
|
html2canvas: 1.4.1
|
||||||
|
|
||||||
jsx-ast-utils@3.3.5:
|
jsx-ast-utils@3.3.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
array-includes: 3.1.9
|
array-includes: 3.1.9
|
||||||
@ -6636,6 +6780,8 @@ snapshots:
|
|||||||
|
|
||||||
pako@1.0.11: {}
|
pako@1.0.11: {}
|
||||||
|
|
||||||
|
pako@2.1.0: {}
|
||||||
|
|
||||||
parent-module@1.0.1:
|
parent-module@1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
callsites: 3.1.0
|
callsites: 3.1.0
|
||||||
@ -6660,6 +6806,9 @@ snapshots:
|
|||||||
|
|
||||||
pend@1.2.0: {}
|
pend@1.2.0: {}
|
||||||
|
|
||||||
|
performance-now@2.1.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
pg-cloudflare@1.2.7:
|
pg-cloudflare@1.2.7:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@ -6743,6 +6892,11 @@ snapshots:
|
|||||||
|
|
||||||
queue-microtask@1.2.3: {}
|
queue-microtask@1.2.3: {}
|
||||||
|
|
||||||
|
raf@3.4.1:
|
||||||
|
dependencies:
|
||||||
|
performance-now: 2.1.0
|
||||||
|
optional: true
|
||||||
|
|
||||||
react-dom@19.2.1(react@19.2.1):
|
react-dom@19.2.1(react@19.2.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.1
|
react: 19.2.1
|
||||||
@ -6791,6 +6945,9 @@ snapshots:
|
|||||||
get-proto: 1.0.1
|
get-proto: 1.0.1
|
||||||
which-builtin-type: 1.2.1
|
which-builtin-type: 1.2.1
|
||||||
|
|
||||||
|
regenerator-runtime@0.13.11:
|
||||||
|
optional: true
|
||||||
|
|
||||||
regexp.prototype.flags@1.5.4:
|
regexp.prototype.flags@1.5.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind: 1.0.8
|
call-bind: 1.0.8
|
||||||
@ -6852,6 +7009,9 @@ snapshots:
|
|||||||
|
|
||||||
reusify@1.1.0: {}
|
reusify@1.1.0: {}
|
||||||
|
|
||||||
|
rgbcolor@1.0.1:
|
||||||
|
optional: true
|
||||||
|
|
||||||
run-parallel@1.2.0:
|
run-parallel@1.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
queue-microtask: 1.2.3
|
queue-microtask: 1.2.3
|
||||||
@ -7003,6 +7163,9 @@ snapshots:
|
|||||||
|
|
||||||
stable-hash@0.0.5: {}
|
stable-hash@0.0.5: {}
|
||||||
|
|
||||||
|
stackblur-canvas@2.7.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
stop-iteration-iterator@1.1.0:
|
stop-iteration-iterator@1.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
@ -7094,10 +7257,17 @@ snapshots:
|
|||||||
|
|
||||||
supports-preserve-symlinks-flag@1.0.0: {}
|
supports-preserve-symlinks-flag@1.0.0: {}
|
||||||
|
|
||||||
|
svg-pathdata@6.0.3:
|
||||||
|
optional: true
|
||||||
|
|
||||||
tailwindcss@4.1.18: {}
|
tailwindcss@4.1.18: {}
|
||||||
|
|
||||||
tapable@2.3.0: {}
|
tapable@2.3.0: {}
|
||||||
|
|
||||||
|
text-segmentation@1.0.3:
|
||||||
|
dependencies:
|
||||||
|
utrie: 1.0.2
|
||||||
|
|
||||||
tinyglobby@0.2.15:
|
tinyglobby@0.2.15:
|
||||||
dependencies:
|
dependencies:
|
||||||
fdir: 6.5.0(picomatch@4.0.3)
|
fdir: 6.5.0(picomatch@4.0.3)
|
||||||
@ -7261,6 +7431,10 @@ snapshots:
|
|||||||
|
|
||||||
util-deprecate@1.0.2: {}
|
util-deprecate@1.0.2: {}
|
||||||
|
|
||||||
|
utrie@1.0.2:
|
||||||
|
dependencies:
|
||||||
|
base64-arraybuffer: 1.0.2
|
||||||
|
|
||||||
vfile-message@4.0.3:
|
vfile-message@4.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
|
|||||||
167
src/app/api/conversations/[id]/export/route.ts
Normal file
167
src/app/api/conversations/[id]/export/route.ts
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
/**
|
||||||
|
* 对话导出 API
|
||||||
|
* GET /api/conversations/[id]/export?format=markdown|json|html|pdf
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { db } from '@/drizzle/db';
|
||||||
|
import { conversations, messages } from '@/drizzle/schema';
|
||||||
|
import { eq, asc } from 'drizzle-orm';
|
||||||
|
import { getCurrentUser } from '@/lib/auth';
|
||||||
|
import {
|
||||||
|
exportConversation,
|
||||||
|
getExportContentType,
|
||||||
|
generateExportFilename,
|
||||||
|
DEFAULT_EXPORT_OPTIONS,
|
||||||
|
type ExportFormat,
|
||||||
|
type ExportData,
|
||||||
|
type ExportMessageData,
|
||||||
|
} from '@/lib/export';
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
// 验证用户身份
|
||||||
|
const user = await getCurrentUser();
|
||||||
|
if (!user) {
|
||||||
|
return NextResponse.json({ error: '未登录' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id: conversationId } = await params;
|
||||||
|
|
||||||
|
// 获取导出参数
|
||||||
|
const searchParams = request.nextUrl.searchParams;
|
||||||
|
const format = (searchParams.get('format') || 'markdown') as ExportFormat;
|
||||||
|
const includeThinking = searchParams.get('includeThinking') !== 'false';
|
||||||
|
const includeToolCalls = searchParams.get('includeToolCalls') !== 'false';
|
||||||
|
const includeImages = searchParams.get('includeImages') !== 'false';
|
||||||
|
|
||||||
|
// 验证格式
|
||||||
|
if (!['markdown', 'json', 'html', 'pdf'].includes(format)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '不支持的导出格式' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// PDF 导出需要在客户端执行,服务端只返回数据
|
||||||
|
if (format === 'pdf') {
|
||||||
|
// 对于 PDF,返回 JSON 数据让客户端生成
|
||||||
|
const exportData = await getExportData(conversationId, user.userId);
|
||||||
|
if (!exportData) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '对话不存在或无权访问' },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return NextResponse.json({
|
||||||
|
...exportData,
|
||||||
|
exportFormat: 'pdf',
|
||||||
|
message: '请在客户端生成 PDF',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取对话和消息数据
|
||||||
|
const exportData = await getExportData(conversationId, user.userId);
|
||||||
|
if (!exportData) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '对话不存在或无权访问' },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行导出
|
||||||
|
const content = await exportConversation(exportData, {
|
||||||
|
format,
|
||||||
|
includeThinking,
|
||||||
|
includeToolCalls,
|
||||||
|
includeImages,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 生成文件名
|
||||||
|
const filename = generateExportFilename(exportData.conversation.title, format);
|
||||||
|
const contentType = getExportContentType(format);
|
||||||
|
|
||||||
|
// 返回文件
|
||||||
|
return new NextResponse(content as string, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': contentType,
|
||||||
|
'Content-Disposition': `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Export error:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '导出失败' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取导出数据
|
||||||
|
*/
|
||||||
|
async function getExportData(
|
||||||
|
conversationId: string,
|
||||||
|
userId: string
|
||||||
|
): Promise<ExportData | null> {
|
||||||
|
// 获取对话
|
||||||
|
const conversation = await db.query.conversations.findFirst({
|
||||||
|
where: eq(conversations.conversationId, conversationId),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!conversation) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证权限
|
||||||
|
if (conversation.userId !== userId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取消息
|
||||||
|
const messageList = await db.query.messages.findMany({
|
||||||
|
where: eq(messages.conversationId, conversationId),
|
||||||
|
orderBy: [asc(messages.createdAt)],
|
||||||
|
});
|
||||||
|
|
||||||
|
// 构建导出数据
|
||||||
|
const exportData: ExportData = {
|
||||||
|
exportInfo: {
|
||||||
|
exportedAt: new Date().toISOString(),
|
||||||
|
format: 'json', // 将在实际导出时更新
|
||||||
|
version: '1.0',
|
||||||
|
},
|
||||||
|
conversation: {
|
||||||
|
id: conversation.conversationId,
|
||||||
|
title: conversation.title,
|
||||||
|
model: conversation.model,
|
||||||
|
enableThinking: conversation.enableThinking || false,
|
||||||
|
tools: (conversation.tools as string[]) || [],
|
||||||
|
messageCount: conversation.messageCount || 0,
|
||||||
|
totalTokens: conversation.totalTokens || 0,
|
||||||
|
createdAt: conversation.createdAt?.toISOString() || new Date().toISOString(),
|
||||||
|
updatedAt: conversation.updatedAt?.toISOString() || new Date().toISOString(),
|
||||||
|
},
|
||||||
|
messages: messageList.map((msg): ExportMessageData => ({
|
||||||
|
id: msg.messageId,
|
||||||
|
role: msg.role as 'user' | 'assistant' | 'system',
|
||||||
|
content: msg.content,
|
||||||
|
thinkingContent: msg.thinkingContent,
|
||||||
|
toolCalls: msg.toolCalls as ExportMessageData['toolCalls'],
|
||||||
|
toolResults: msg.toolResults as ExportMessageData['toolResults'],
|
||||||
|
images: msg.images as string[] | null,
|
||||||
|
uploadedImages: msg.uploadedImages as string[] | null,
|
||||||
|
uploadedDocuments: msg.uploadedDocuments as ExportMessageData['uploadedDocuments'],
|
||||||
|
usedTools: msg.usedTools as string[] | null,
|
||||||
|
searchImages: msg.searchImages as ExportMessageData['searchImages'],
|
||||||
|
inputTokens: msg.inputTokens,
|
||||||
|
outputTokens: msg.outputTokens,
|
||||||
|
createdAt: msg.createdAt?.toISOString() || new Date().toISOString(),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
return exportData;
|
||||||
|
}
|
||||||
@ -3,7 +3,7 @@
|
|||||||
import { useState, useRef, useEffect, use } from 'react';
|
import { useState, useRef, useEffect, use } from 'react';
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Share2, MoreHorizontal, Loader2, Square, Clock, ChevronDown, Pencil, Trash2, Check, X, AlertTriangle, Settings } from 'lucide-react';
|
import { Share2, Loader2, Square, Clock, ChevronDown, Pencil, Trash2, Check, X, AlertTriangle, Settings } from 'lucide-react';
|
||||||
import { Sidebar, SidebarToggle } from '@/components/layout/Sidebar';
|
import { Sidebar, SidebarToggle } from '@/components/layout/Sidebar';
|
||||||
import { ChatInput } from '@/components/features/ChatInput';
|
import { ChatInput } from '@/components/features/ChatInput';
|
||||||
import { MessageBubble } from '@/components/features/MessageBubble';
|
import { MessageBubble } from '@/components/features/MessageBubble';
|
||||||
@ -11,6 +11,7 @@ import { ChatHeaderInfo } from '@/components/features/ChatHeader';
|
|||||||
import { SaveToNoteModal } from '@/components/features/SaveToNoteModal';
|
import { SaveToNoteModal } from '@/components/features/SaveToNoteModal';
|
||||||
import { PromptOptimizer } from '@/components/features/PromptOptimizer';
|
import { PromptOptimizer } from '@/components/features/PromptOptimizer';
|
||||||
import { LinkPreviewModal } from '@/components/features/LinkPreviewModal';
|
import { LinkPreviewModal } from '@/components/features/LinkPreviewModal';
|
||||||
|
import { ExportDropdown } from '@/components/features/ExportDropdown';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useConversation, useConversations } from '@/hooks/useConversations';
|
import { useConversation, useConversations } from '@/hooks/useConversations';
|
||||||
import { useStreamChat, type ChatMessage } from '@/hooks/useStreamChat';
|
import { useStreamChat, type ChatMessage } from '@/hooks/useStreamChat';
|
||||||
@ -503,12 +504,11 @@ export default function ChatPage({ params }: PageProps) {
|
|||||||
<Share2 size={16} />
|
<Share2 size={16} />
|
||||||
<span>分享</span>
|
<span>分享</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<ExportDropdown
|
||||||
className="w-8 h-8 flex items-center justify-center text-[var(--color-text-tertiary)] hover:bg-[var(--color-bg-hover)] rounded-lg transition-colors"
|
conversationId={chatId}
|
||||||
title="更多选项"
|
conversationTitle={conversation?.title || '新对话'}
|
||||||
>
|
disabled={isStreaming}
|
||||||
<MoreHorizontal size={18} />
|
/>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
257
src/components/features/ExportDropdown.tsx
Normal file
257
src/components/features/ExportDropdown.tsx
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useRef, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
MoreHorizontal,
|
||||||
|
FileText,
|
||||||
|
FileJson,
|
||||||
|
FileCode,
|
||||||
|
FileType,
|
||||||
|
Loader2,
|
||||||
|
Check,
|
||||||
|
AlertCircle,
|
||||||
|
Download,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import {
|
||||||
|
generateExportFilename,
|
||||||
|
generatePdfInBrowser,
|
||||||
|
type ExportFormat,
|
||||||
|
type ExportData,
|
||||||
|
} from '@/lib/export';
|
||||||
|
|
||||||
|
interface ExportDropdownProps {
|
||||||
|
conversationId: string;
|
||||||
|
conversationTitle: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出格式配置
|
||||||
|
const EXPORT_FORMATS: {
|
||||||
|
format: ExportFormat;
|
||||||
|
label: string;
|
||||||
|
icon: typeof FileText;
|
||||||
|
description: string;
|
||||||
|
}[] = [
|
||||||
|
{
|
||||||
|
format: 'markdown',
|
||||||
|
label: 'Markdown',
|
||||||
|
icon: FileText,
|
||||||
|
description: '纯文本格式,易于编辑',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: 'json',
|
||||||
|
label: 'JSON',
|
||||||
|
icon: FileJson,
|
||||||
|
description: '完整数据,便于备份',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: 'html',
|
||||||
|
label: 'HTML',
|
||||||
|
icon: FileCode,
|
||||||
|
description: '网页格式,保留样式',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: 'pdf',
|
||||||
|
label: 'PDF',
|
||||||
|
icon: FileType,
|
||||||
|
description: '文档格式,适合打印',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function ExportDropdown({
|
||||||
|
conversationId,
|
||||||
|
conversationTitle,
|
||||||
|
disabled,
|
||||||
|
}: ExportDropdownProps) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [exporting, setExporting] = useState<ExportFormat | null>(null);
|
||||||
|
const [exportSuccess, setExportSuccess] = useState<ExportFormat | null>(null);
|
||||||
|
const [exportError, setExportError] = useState<string | null>(null);
|
||||||
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// 点击外部关闭菜单
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (isOpen) {
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
};
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
// 清除成功/错误状态
|
||||||
|
useEffect(() => {
|
||||||
|
if (exportSuccess) {
|
||||||
|
const timer = setTimeout(() => setExportSuccess(null), 2000);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [exportSuccess]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (exportError) {
|
||||||
|
const timer = setTimeout(() => setExportError(null), 3000);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [exportError]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 触发文件下载
|
||||||
|
*/
|
||||||
|
const downloadFile = (content: string | Blob, filename: string, mimeType: string) => {
|
||||||
|
const blob = content instanceof Blob ? content : new Blob([content], { type: mimeType });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理导出
|
||||||
|
*/
|
||||||
|
const handleExport = async (format: ExportFormat) => {
|
||||||
|
if (exporting) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setExporting(format);
|
||||||
|
setExportError(null);
|
||||||
|
|
||||||
|
if (format === 'pdf') {
|
||||||
|
// PDF 需要先获取数据,然后在客户端生成
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/conversations/${conversationId}/export?format=pdf`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('获取导出数据失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json() as ExportData;
|
||||||
|
|
||||||
|
// 在客户端生成 PDF
|
||||||
|
const pdfBlob = await generatePdfInBrowser(data, {
|
||||||
|
format: 'pdf',
|
||||||
|
includeThinking: true,
|
||||||
|
includeToolCalls: true,
|
||||||
|
includeImages: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const filename = generateExportFilename(conversationTitle, 'pdf');
|
||||||
|
downloadFile(pdfBlob, filename, 'application/pdf');
|
||||||
|
} else {
|
||||||
|
// 其他格式直接从 API 获取
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/conversations/${conversationId}/export?format=${format}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('导出失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 Content-Disposition 获取文件名,或生成默认文件名
|
||||||
|
const contentDisposition = response.headers.get('Content-Disposition');
|
||||||
|
let filename = generateExportFilename(conversationTitle, format);
|
||||||
|
|
||||||
|
if (contentDisposition) {
|
||||||
|
const filenameMatch = contentDisposition.match(/filename\*=UTF-8''(.+)/);
|
||||||
|
if (filenameMatch) {
|
||||||
|
filename = decodeURIComponent(filenameMatch[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = await response.text();
|
||||||
|
const mimeType = response.headers.get('Content-Type') || 'text/plain';
|
||||||
|
downloadFile(content, filename, mimeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
setExportSuccess(format);
|
||||||
|
setIsOpen(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Export error:', error);
|
||||||
|
setExportError(error instanceof Error ? error.message : '导出失败');
|
||||||
|
} finally {
|
||||||
|
setExporting(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative" ref={menuRef}>
|
||||||
|
{/* 触发按钮 */}
|
||||||
|
<button
|
||||||
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
|
disabled={disabled || !!exporting}
|
||||||
|
className={cn(
|
||||||
|
'w-8 h-8 flex items-center justify-center rounded-lg transition-colors',
|
||||||
|
'text-[var(--color-text-tertiary)] hover:bg-[var(--color-bg-hover)] hover:text-[var(--color-text-secondary)]',
|
||||||
|
(disabled || exporting) && 'opacity-50 cursor-not-allowed'
|
||||||
|
)}
|
||||||
|
title="更多选项"
|
||||||
|
>
|
||||||
|
{exporting ? (
|
||||||
|
<Loader2 size={18} className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
<MoreHorizontal size={18} />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* 下拉菜单 */}
|
||||||
|
{isOpen && (
|
||||||
|
<div className="absolute right-0 top-full mt-1 bg-[var(--color-bg-primary)] border border-[var(--color-border)] rounded-md shadow-lg py-1 z-30 min-w-[200px]">
|
||||||
|
{/* 导出选项标题 */}
|
||||||
|
<div className="px-3 py-2 text-xs font-medium text-[var(--color-text-tertiary)] uppercase tracking-wider border-b border-[var(--color-border)]">
|
||||||
|
<Download size={12} className="inline mr-1.5" />
|
||||||
|
导出对话
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 导出格式列表 */}
|
||||||
|
{EXPORT_FORMATS.map(({ format, label, icon: Icon, description }) => (
|
||||||
|
<button
|
||||||
|
key={format}
|
||||||
|
onClick={() => handleExport(format)}
|
||||||
|
disabled={!!exporting}
|
||||||
|
className={cn(
|
||||||
|
'w-full px-3 py-2.5 text-left text-sm flex items-center gap-3 transition-colors',
|
||||||
|
'text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-hover)]',
|
||||||
|
exporting === format && 'bg-[var(--color-bg-hover)]',
|
||||||
|
exportSuccess === format && 'bg-green-50 text-green-600'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
{exporting === format ? (
|
||||||
|
<Loader2 size={16} className="animate-spin" />
|
||||||
|
) : exportSuccess === format ? (
|
||||||
|
<Check size={16} className="text-green-500" />
|
||||||
|
) : (
|
||||||
|
<Icon size={16} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="font-medium">{label}</div>
|
||||||
|
<div className="text-xs text-[var(--color-text-tertiary)] truncate">
|
||||||
|
{description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 错误提示 */}
|
||||||
|
{exportError && (
|
||||||
|
<div className="absolute right-0 top-full mt-1 bg-red-50 border border-red-200 rounded-lg shadow-lg p-3 z-40 min-w-[200px] flex items-center gap-2 text-sm text-red-600">
|
||||||
|
<AlertCircle size={16} />
|
||||||
|
<span>{exportError}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
1236
src/lib/export/html.ts
Normal file
1236
src/lib/export/html.ts
Normal file
File diff suppressed because it is too large
Load Diff
102
src/lib/export/index.ts
Normal file
102
src/lib/export/index.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
/**
|
||||||
|
* 对话导出功能 - 统一入口
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 导出类型定义
|
||||||
|
export type {
|
||||||
|
ExportFormat,
|
||||||
|
ExportOptions,
|
||||||
|
ExportData,
|
||||||
|
ExportConversationData,
|
||||||
|
ExportMessageData,
|
||||||
|
UploadedDocumentData,
|
||||||
|
SearchImageData,
|
||||||
|
ToolCallData,
|
||||||
|
ToolResultData,
|
||||||
|
Exporter,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
export { TOOL_DISPLAY_NAMES } from './types';
|
||||||
|
|
||||||
|
// 导出各格式导出器
|
||||||
|
export { markdownExporter } from './markdown';
|
||||||
|
export { jsonExporter } from './json';
|
||||||
|
export { htmlExporter } from './html';
|
||||||
|
export { pdfExporter, generatePdfInBrowser } from './pdf';
|
||||||
|
|
||||||
|
import type { ExportFormat, ExportOptions, ExportData, Exporter } from './types';
|
||||||
|
import { markdownExporter } from './markdown';
|
||||||
|
import { jsonExporter } from './json';
|
||||||
|
import { htmlExporter } from './html';
|
||||||
|
import { pdfExporter } from './pdf';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定格式的导出器
|
||||||
|
*/
|
||||||
|
export function getExporter(format: ExportFormat): Exporter {
|
||||||
|
switch (format) {
|
||||||
|
case 'markdown':
|
||||||
|
return markdownExporter;
|
||||||
|
case 'json':
|
||||||
|
return jsonExporter;
|
||||||
|
case 'html':
|
||||||
|
return htmlExporter;
|
||||||
|
case 'pdf':
|
||||||
|
return pdfExporter;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported export format: ${format}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出对话数据
|
||||||
|
*/
|
||||||
|
export async function exportConversation(
|
||||||
|
data: ExportData,
|
||||||
|
options: ExportOptions
|
||||||
|
): Promise<string | Blob> {
|
||||||
|
const exporter = getExporter(options.format);
|
||||||
|
return exporter.export(data, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取导出文件的 MIME 类型
|
||||||
|
*/
|
||||||
|
export function getExportContentType(format: ExportFormat): string {
|
||||||
|
return getExporter(format).getContentType();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取导出文件的扩展名
|
||||||
|
*/
|
||||||
|
export function getExportFileExtension(format: ExportFormat): string {
|
||||||
|
return getExporter(format).getFileExtension();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成导出文件名
|
||||||
|
*/
|
||||||
|
export function generateExportFilename(
|
||||||
|
title: string,
|
||||||
|
format: ExportFormat
|
||||||
|
): string {
|
||||||
|
// 清理文件名中的特殊字符
|
||||||
|
const cleanTitle = title
|
||||||
|
.replace(/[<>:"/\\|?*]/g, '')
|
||||||
|
.replace(/\s+/g, '_')
|
||||||
|
.slice(0, 50);
|
||||||
|
|
||||||
|
const timestamp = new Date().toISOString().slice(0, 10);
|
||||||
|
const extension = getExportFileExtension(format);
|
||||||
|
|
||||||
|
return `${cleanTitle}_${timestamp}.${extension}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认导出选项
|
||||||
|
*/
|
||||||
|
export const DEFAULT_EXPORT_OPTIONS: Omit<ExportOptions, 'format'> = {
|
||||||
|
includeThinking: true,
|
||||||
|
includeToolCalls: true,
|
||||||
|
includeImages: true,
|
||||||
|
};
|
||||||
54
src/lib/export/json.ts
Normal file
54
src/lib/export/json.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
/**
|
||||||
|
* JSON 导出器
|
||||||
|
* 将对话数据导出为 JSON 格式(完整数据备份)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ExportData, ExportOptions, Exporter } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON 导出器
|
||||||
|
*/
|
||||||
|
export const jsonExporter: Exporter = {
|
||||||
|
async export(data: ExportData, options: ExportOptions): Promise<string> {
|
||||||
|
// 根据选项过滤数据
|
||||||
|
const filteredData = { ...data };
|
||||||
|
|
||||||
|
// 如果不包含思考过程,移除 thinkingContent
|
||||||
|
if (!options.includeThinking) {
|
||||||
|
filteredData.messages = data.messages.map((msg) => ({
|
||||||
|
...msg,
|
||||||
|
thinkingContent: undefined,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果不包含工具调用,移除 toolCalls 和 toolResults
|
||||||
|
if (!options.includeToolCalls) {
|
||||||
|
filteredData.messages = (filteredData.messages || data.messages).map((msg) => ({
|
||||||
|
...msg,
|
||||||
|
toolCalls: undefined,
|
||||||
|
toolResults: undefined,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果不包含图片,移除图片数据
|
||||||
|
if (!options.includeImages) {
|
||||||
|
filteredData.messages = (filteredData.messages || data.messages).map((msg) => ({
|
||||||
|
...msg,
|
||||||
|
images: undefined,
|
||||||
|
uploadedImages: undefined,
|
||||||
|
searchImages: undefined,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化输出 JSON
|
||||||
|
return JSON.stringify(filteredData, null, 2);
|
||||||
|
},
|
||||||
|
|
||||||
|
getContentType(): string {
|
||||||
|
return 'application/json; charset=utf-8';
|
||||||
|
},
|
||||||
|
|
||||||
|
getFileExtension(): string {
|
||||||
|
return 'json';
|
||||||
|
},
|
||||||
|
};
|
||||||
226
src/lib/export/markdown.ts
Normal file
226
src/lib/export/markdown.ts
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
/**
|
||||||
|
* Markdown 导出器
|
||||||
|
* 将对话数据导出为 Markdown 格式
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ExportData,
|
||||||
|
ExportOptions,
|
||||||
|
ExportMessageData,
|
||||||
|
Exporter,
|
||||||
|
TOOL_DISPLAY_NAMES,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
// 工具名称中文映射
|
||||||
|
const TOOL_NAMES: Record<string, string> = {
|
||||||
|
web_search: '网络搜索',
|
||||||
|
web_fetch: '网页读取',
|
||||||
|
mita_search: '秘塔搜索',
|
||||||
|
mita_reader: '秘塔阅读',
|
||||||
|
code_execution: '代码执行',
|
||||||
|
youdao_translate: '有道翻译',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化日期时间
|
||||||
|
*/
|
||||||
|
function formatDateTime(dateString: string): string {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化文件大小
|
||||||
|
*/
|
||||||
|
function formatFileSize(bytes: number): string {
|
||||||
|
if (bytes === 0) return '0 B';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成消息的 Markdown 内容
|
||||||
|
*/
|
||||||
|
function generateMessageMarkdown(
|
||||||
|
message: ExportMessageData,
|
||||||
|
options: ExportOptions,
|
||||||
|
messageIndex: number
|
||||||
|
): string {
|
||||||
|
const lines: string[] = [];
|
||||||
|
const isUser = message.role === 'user';
|
||||||
|
const roleIcon = isUser ? '👤' : '🤖';
|
||||||
|
const roleName = isUser ? '用户' : 'AI 助手';
|
||||||
|
const time = formatDateTime(message.createdAt);
|
||||||
|
|
||||||
|
// 消息头部
|
||||||
|
lines.push(`## ${roleIcon} ${roleName} (${time})`);
|
||||||
|
lines.push('');
|
||||||
|
|
||||||
|
// 用户消息:显示上传的图片和文档
|
||||||
|
if (isUser) {
|
||||||
|
// 上传的图片
|
||||||
|
if (options.includeImages && message.uploadedImages && message.uploadedImages.length > 0) {
|
||||||
|
lines.push('**📷 上传的图片:**');
|
||||||
|
lines.push('');
|
||||||
|
message.uploadedImages.forEach((img, index) => {
|
||||||
|
// 如果是 Base64 图片,直接嵌入;否则作为链接
|
||||||
|
if (img.startsWith('data:')) {
|
||||||
|
lines.push(``);
|
||||||
|
} else {
|
||||||
|
lines.push(``);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传的文档
|
||||||
|
if (message.uploadedDocuments && message.uploadedDocuments.length > 0) {
|
||||||
|
lines.push('**📎 上传的文档:**');
|
||||||
|
lines.push('');
|
||||||
|
message.uploadedDocuments.forEach((doc) => {
|
||||||
|
lines.push(`- **${doc.name}** (${formatFileSize(doc.size)}, ${doc.type})`);
|
||||||
|
});
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AI 消息:显示思考过程
|
||||||
|
if (!isUser && options.includeThinking && message.thinkingContent) {
|
||||||
|
lines.push('<details>');
|
||||||
|
lines.push('<summary>💭 思考过程</summary>');
|
||||||
|
lines.push('');
|
||||||
|
lines.push('```');
|
||||||
|
lines.push(message.thinkingContent);
|
||||||
|
lines.push('```');
|
||||||
|
lines.push('');
|
||||||
|
lines.push('</details>');
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 消息内容
|
||||||
|
if (message.content) {
|
||||||
|
lines.push(message.content);
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// AI 消息:显示使用的工具
|
||||||
|
if (!isUser && message.usedTools && message.usedTools.length > 0) {
|
||||||
|
const toolNames = message.usedTools
|
||||||
|
.map((tool) => TOOL_NAMES[tool] || tool)
|
||||||
|
.join('、');
|
||||||
|
lines.push(`🔧 **使用工具:** ${toolNames}`);
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// AI 消息:显示工具调用详情
|
||||||
|
if (!isUser && options.includeToolCalls && message.toolCalls && message.toolCalls.length > 0) {
|
||||||
|
lines.push('<details>');
|
||||||
|
lines.push('<summary>🛠️ 工具调用详情</summary>');
|
||||||
|
lines.push('');
|
||||||
|
message.toolCalls.forEach((call, index) => {
|
||||||
|
lines.push(`**调用 ${index + 1}: ${TOOL_NAMES[call.name] || call.name}**`);
|
||||||
|
lines.push('```json');
|
||||||
|
lines.push(JSON.stringify(call.input, null, 2));
|
||||||
|
lines.push('```');
|
||||||
|
lines.push('');
|
||||||
|
});
|
||||||
|
lines.push('</details>');
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// AI 消息:显示代码执行图片
|
||||||
|
if (!isUser && options.includeImages && message.images && message.images.length > 0) {
|
||||||
|
lines.push('**📊 代码执行结果:**');
|
||||||
|
lines.push('');
|
||||||
|
message.images.forEach((img, index) => {
|
||||||
|
const imgSrc = img.startsWith('data:') ? img : `data:image/png;base64,${img}`;
|
||||||
|
lines.push(``);
|
||||||
|
});
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// AI 消息:显示搜索到的图片
|
||||||
|
if (!isUser && options.includeImages && message.searchImages && message.searchImages.length > 0) {
|
||||||
|
lines.push('**🔍 搜索结果图片:**');
|
||||||
|
lines.push('');
|
||||||
|
message.searchImages.forEach((img, index) => {
|
||||||
|
lines.push(`- [${img.title || `图片 ${index + 1}`}](${img.imageUrl})`);
|
||||||
|
});
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// AI 消息:显示 Token 统计
|
||||||
|
if (!isUser && (message.inputTokens || message.outputTokens)) {
|
||||||
|
const input = message.inputTokens || 0;
|
||||||
|
const output = message.outputTokens || 0;
|
||||||
|
lines.push(`*Token: 输入 ${input} / 输出 ${output}*`);
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分隔线
|
||||||
|
lines.push('---');
|
||||||
|
lines.push('');
|
||||||
|
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Markdown 导出器
|
||||||
|
*/
|
||||||
|
export const markdownExporter: Exporter = {
|
||||||
|
async export(data: ExportData, options: ExportOptions): Promise<string> {
|
||||||
|
const lines: string[] = [];
|
||||||
|
const { conversation, messages, exportInfo } = data;
|
||||||
|
|
||||||
|
// 标题
|
||||||
|
lines.push(`# ${conversation.title}`);
|
||||||
|
lines.push('');
|
||||||
|
|
||||||
|
// 元信息
|
||||||
|
lines.push('> **导出信息**');
|
||||||
|
lines.push(`> - 导出时间: ${formatDateTime(exportInfo.exportedAt)}`);
|
||||||
|
lines.push(`> - 模型: ${conversation.model}`);
|
||||||
|
lines.push(`> - 消息数: ${conversation.messageCount}`);
|
||||||
|
lines.push(`> - Token 消耗: ${conversation.totalTokens.toLocaleString()}`);
|
||||||
|
lines.push(`> - 思考模式: ${conversation.enableThinking ? '开启' : '关闭'}`);
|
||||||
|
if (conversation.tools && conversation.tools.length > 0) {
|
||||||
|
const toolNames = conversation.tools
|
||||||
|
.map((tool) => TOOL_NAMES[tool] || tool)
|
||||||
|
.join('、');
|
||||||
|
lines.push(`> - 启用工具: ${toolNames}`);
|
||||||
|
}
|
||||||
|
lines.push(`> - 创建时间: ${formatDateTime(conversation.createdAt)}`);
|
||||||
|
lines.push('');
|
||||||
|
lines.push('---');
|
||||||
|
lines.push('');
|
||||||
|
|
||||||
|
// 消息列表
|
||||||
|
messages.forEach((message, index) => {
|
||||||
|
lines.push(generateMessageMarkdown(message, options, index));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 页脚
|
||||||
|
lines.push('---');
|
||||||
|
lines.push('');
|
||||||
|
lines.push('*导出自 [LionCode AI](https://lioncode.com)*');
|
||||||
|
|
||||||
|
return lines.join('\n');
|
||||||
|
},
|
||||||
|
|
||||||
|
getContentType(): string {
|
||||||
|
return 'text/markdown; charset=utf-8';
|
||||||
|
},
|
||||||
|
|
||||||
|
getFileExtension(): string {
|
||||||
|
return 'md';
|
||||||
|
},
|
||||||
|
};
|
||||||
203
src/lib/export/pdf.ts
Normal file
203
src/lib/export/pdf.ts
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
/**
|
||||||
|
* PDF 导出器
|
||||||
|
* 使用 jspdf + html2canvas 在客户端生成 PDF
|
||||||
|
* 使用 iframe 方案确保HTML样式正确渲染
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ExportData, ExportOptions, Exporter } from './types';
|
||||||
|
import { htmlExporter } from './html';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在客户端生成 PDF(使用 iframe 方案)
|
||||||
|
* 这个函数需要在浏览器环境中调用
|
||||||
|
*/
|
||||||
|
export async function generatePdfInBrowser(
|
||||||
|
data: ExportData,
|
||||||
|
options: ExportOptions
|
||||||
|
): Promise<Blob> {
|
||||||
|
// 动态导入 jspdf 和 html2canvas(仅在客户端)
|
||||||
|
const [{ default: jsPDF }, { default: html2canvas }] = await Promise.all([
|
||||||
|
import('jspdf'),
|
||||||
|
import('html2canvas'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 首先生成 HTML 内容
|
||||||
|
const htmlContent = await htmlExporter.export(data, options) as string;
|
||||||
|
|
||||||
|
// 创建隐藏的 iframe 来渲染完整的 HTML 文档
|
||||||
|
const iframe = document.createElement('iframe');
|
||||||
|
iframe.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
left: -9999px;
|
||||||
|
top: 0;
|
||||||
|
width: 900px;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
visibility: hidden;
|
||||||
|
`;
|
||||||
|
document.body.appendChild(iframe);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 等待 iframe 准备就绪并写入 HTML 内容
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;
|
||||||
|
if (!iframeDoc) {
|
||||||
|
reject(new Error('无法访问 iframe 文档'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入完整的 HTML 文档
|
||||||
|
iframeDoc.open();
|
||||||
|
iframeDoc.write(htmlContent);
|
||||||
|
iframeDoc.close();
|
||||||
|
|
||||||
|
// 等待 iframe 加载完成
|
||||||
|
iframe.onload = () => resolve();
|
||||||
|
|
||||||
|
// 设置超时,防止 onload 不触发
|
||||||
|
setTimeout(() => resolve(), 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取 iframe 中的文档
|
||||||
|
const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;
|
||||||
|
if (!iframeDoc) {
|
||||||
|
throw new Error('无法访问 iframe 文档');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取要渲染的容器(.container 或 body)
|
||||||
|
const container = iframeDoc.querySelector('.container') || iframeDoc.body;
|
||||||
|
|
||||||
|
// 等待图片加载
|
||||||
|
const images = container.querySelectorAll('img');
|
||||||
|
await Promise.all(
|
||||||
|
Array.from(images).map(
|
||||||
|
(img) =>
|
||||||
|
new Promise<void>((resolve) => {
|
||||||
|
if ((img as HTMLImageElement).complete) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
(img as HTMLImageElement).onload = () => resolve();
|
||||||
|
(img as HTMLImageElement).onerror = () => resolve();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 额外等待一下确保样式完全渲染
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 300));
|
||||||
|
|
||||||
|
// 使用 html2canvas 渲染 iframe 中的内容
|
||||||
|
const canvas = await html2canvas(container as HTMLElement, {
|
||||||
|
scale: 2,
|
||||||
|
useCORS: true,
|
||||||
|
allowTaint: true,
|
||||||
|
backgroundColor: '#F8F9FA', // 与页面背景色一致
|
||||||
|
logging: false,
|
||||||
|
width: 900,
|
||||||
|
windowWidth: 900,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建 PDF(A4 尺寸)
|
||||||
|
const pdf = new jsPDF({
|
||||||
|
orientation: 'portrait',
|
||||||
|
unit: 'mm',
|
||||||
|
format: 'a4',
|
||||||
|
});
|
||||||
|
|
||||||
|
const pdfWidth = pdf.internal.pageSize.getWidth();
|
||||||
|
const pdfHeight = pdf.internal.pageSize.getHeight();
|
||||||
|
|
||||||
|
// 计算图片在 PDF 中的尺寸
|
||||||
|
// 保持宽度铺满,按比例计算高度
|
||||||
|
const imgWidth = canvas.width;
|
||||||
|
const imgHeight = canvas.height;
|
||||||
|
|
||||||
|
// 图片缩放到 PDF 宽度
|
||||||
|
const scaledWidth = pdfWidth;
|
||||||
|
const scaledHeight = (imgHeight * pdfWidth) / imgWidth;
|
||||||
|
|
||||||
|
// 计算需要多少页
|
||||||
|
const totalPages = Math.ceil(scaledHeight / pdfHeight);
|
||||||
|
|
||||||
|
// 分页添加内容
|
||||||
|
for (let page = 0; page < totalPages; page++) {
|
||||||
|
if (page > 0) {
|
||||||
|
pdf.addPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算当前页在原始图片中的位置
|
||||||
|
const sourceY = (page * pdfHeight * imgWidth) / pdfWidth;
|
||||||
|
const sourceHeight = Math.min(
|
||||||
|
(pdfHeight * imgWidth) / pdfWidth,
|
||||||
|
imgHeight - sourceY
|
||||||
|
);
|
||||||
|
|
||||||
|
// 如果没有更多内容,跳过
|
||||||
|
if (sourceHeight <= 0) break;
|
||||||
|
|
||||||
|
// 创建当前页的裁剪画布
|
||||||
|
const pageCanvas = document.createElement('canvas');
|
||||||
|
pageCanvas.width = imgWidth;
|
||||||
|
pageCanvas.height = Math.ceil(sourceHeight);
|
||||||
|
|
||||||
|
const ctx = pageCanvas.getContext('2d');
|
||||||
|
if (ctx) {
|
||||||
|
// 从原始画布中裁剪当前页的内容
|
||||||
|
ctx.drawImage(
|
||||||
|
canvas,
|
||||||
|
0, // 源 x
|
||||||
|
Math.floor(sourceY), // 源 y
|
||||||
|
imgWidth, // 源宽度
|
||||||
|
Math.ceil(sourceHeight), // 源高度
|
||||||
|
0, // 目标 x
|
||||||
|
0, // 目标 y
|
||||||
|
imgWidth, // 目标宽度
|
||||||
|
Math.ceil(sourceHeight) // 目标高度
|
||||||
|
);
|
||||||
|
|
||||||
|
// 将裁剪后的画布添加到 PDF
|
||||||
|
const pageImgData = pageCanvas.toDataURL('image/png');
|
||||||
|
const pageScaledHeight = (sourceHeight * pdfWidth) / imgWidth;
|
||||||
|
|
||||||
|
pdf.addImage(
|
||||||
|
pageImgData,
|
||||||
|
'PNG',
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
pdfWidth,
|
||||||
|
pageScaledHeight
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回 Blob
|
||||||
|
return pdf.output('blob');
|
||||||
|
} finally {
|
||||||
|
// 清理 iframe
|
||||||
|
document.body.removeChild(iframe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PDF 导出器(服务端占位符)
|
||||||
|
* 实际的 PDF 生成在客户端通过 generatePdfInBrowser 函数执行
|
||||||
|
*/
|
||||||
|
export const pdfExporter: Exporter = {
|
||||||
|
async export(data: ExportData, options: ExportOptions): Promise<Blob> {
|
||||||
|
// 检查是否在浏览器环境
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
return generatePdfInBrowser(data, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在服务端,返回一个错误提示
|
||||||
|
throw new Error('PDF export must be performed on the client side');
|
||||||
|
},
|
||||||
|
|
||||||
|
getContentType(): string {
|
||||||
|
return 'application/pdf';
|
||||||
|
},
|
||||||
|
|
||||||
|
getFileExtension(): string {
|
||||||
|
return 'pdf';
|
||||||
|
},
|
||||||
|
};
|
||||||
106
src/lib/export/types.ts
Normal file
106
src/lib/export/types.ts
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
/**
|
||||||
|
* 对话导出功能 - 类型定义
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 导出格式
|
||||||
|
export type ExportFormat = 'markdown' | 'json' | 'html' | 'pdf';
|
||||||
|
|
||||||
|
// 导出选项
|
||||||
|
export interface ExportOptions {
|
||||||
|
format: ExportFormat;
|
||||||
|
includeThinking?: boolean; // 是否包含思考过程,默认 true
|
||||||
|
includeToolCalls?: boolean; // 是否包含工具调用详情,默认 true
|
||||||
|
includeImages?: boolean; // 是否包含图片(Base64),默认 true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传的文档数据
|
||||||
|
export interface UploadedDocumentData {
|
||||||
|
name: string;
|
||||||
|
size: number;
|
||||||
|
type: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索图片数据
|
||||||
|
export interface SearchImageData {
|
||||||
|
title: string;
|
||||||
|
imageUrl: string;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
score: string;
|
||||||
|
position: number;
|
||||||
|
sourceUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具调用记录
|
||||||
|
export interface ToolCallData {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
input: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具结果记录
|
||||||
|
export interface ToolResultData {
|
||||||
|
toolUseId: string;
|
||||||
|
content: string;
|
||||||
|
isError?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 消息数据(用于导出)
|
||||||
|
export interface ExportMessageData {
|
||||||
|
id: string;
|
||||||
|
role: 'user' | 'assistant' | 'system';
|
||||||
|
content: string;
|
||||||
|
thinkingContent?: string | null;
|
||||||
|
toolCalls?: ToolCallData[] | null;
|
||||||
|
toolResults?: ToolResultData[] | null;
|
||||||
|
images?: string[] | null; // 代码执行产生的图片
|
||||||
|
uploadedImages?: string[] | null; // 用户上传的图片
|
||||||
|
uploadedDocuments?: UploadedDocumentData[] | null; // 用户上传的文档
|
||||||
|
usedTools?: string[] | null; // 使用的工具列表
|
||||||
|
searchImages?: SearchImageData[] | null; // 搜索到的图片
|
||||||
|
inputTokens?: number | null;
|
||||||
|
outputTokens?: number | null;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对话数据(用于导出)
|
||||||
|
export interface ExportConversationData {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
model: string;
|
||||||
|
enableThinking: boolean;
|
||||||
|
tools: string[];
|
||||||
|
messageCount: number;
|
||||||
|
totalTokens: number;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 完整的导出数据
|
||||||
|
export interface ExportData {
|
||||||
|
exportInfo: {
|
||||||
|
exportedAt: string;
|
||||||
|
format: ExportFormat;
|
||||||
|
version: string;
|
||||||
|
};
|
||||||
|
conversation: ExportConversationData;
|
||||||
|
messages: ExportMessageData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出器接口
|
||||||
|
export interface Exporter {
|
||||||
|
export(data: ExportData, options: ExportOptions): Promise<string | Blob>;
|
||||||
|
getContentType(): string;
|
||||||
|
getFileExtension(): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具名称中文映射
|
||||||
|
export const TOOL_DISPLAY_NAMES: Record<string, string> = {
|
||||||
|
web_search: '网络搜索',
|
||||||
|
web_fetch: '网页读取',
|
||||||
|
mita_search: '秘塔搜索',
|
||||||
|
mita_reader: '秘塔阅读',
|
||||||
|
code_execution: '代码执行',
|
||||||
|
youdao_translate: '有道翻译',
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user