Merge remote-tracking branch 'origin/main' into cloudflare

This commit is contained in:
javayhu 2025-07-24 00:10:11 +08:00
commit 70446d10b3
10 changed files with 290 additions and 199 deletions

View File

@ -181,6 +181,8 @@ FAL_API_KEY=""
FIREWORKS_API_KEY="" FIREWORKS_API_KEY=""
OPENAI_API_KEY="" OPENAI_API_KEY=""
REPLICATE_API_TOKEN="" REPLICATE_API_TOKEN=""
GOOGLE_API_KEY=""
DEEPSEEK_API_KEY=""
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Web Content Analyzer (Firecrawl) # Web Content Analyzer (Firecrawl)

View File

@ -25,8 +25,10 @@
"knip": "knip" "knip": "knip"
}, },
"dependencies": { "dependencies": {
"@ai-sdk/deepseek": "^0.2.16",
"@ai-sdk/fal": "^0.1.12", "@ai-sdk/fal": "^0.1.12",
"@ai-sdk/fireworks": "^0.2.14", "@ai-sdk/fireworks": "^0.2.14",
"@ai-sdk/google": "^1.2.22",
"@ai-sdk/google-vertex": "^2.2.24", "@ai-sdk/google-vertex": "^2.2.24",
"@ai-sdk/openai": "^1.1.13", "@ai-sdk/openai": "^1.1.13",
"@ai-sdk/replicate": "^0.2.8", "@ai-sdk/replicate": "^0.2.8",

81
pnpm-lock.yaml generated
View File

@ -8,12 +8,18 @@ importers:
.: .:
dependencies: dependencies:
'@ai-sdk/deepseek':
specifier: ^0.2.16
version: 0.2.16(zod@3.25.64)
'@ai-sdk/fal': '@ai-sdk/fal':
specifier: ^0.1.12 specifier: ^0.1.12
version: 0.1.12(zod@3.25.64) version: 0.1.12(zod@3.25.64)
'@ai-sdk/fireworks': '@ai-sdk/fireworks':
specifier: ^0.2.14 specifier: ^0.2.14
version: 0.2.14(zod@3.25.64) version: 0.2.14(zod@3.25.64)
'@ai-sdk/google':
specifier: ^1.2.22
version: 1.2.22(zod@3.25.64)
'@ai-sdk/google-vertex': '@ai-sdk/google-vertex':
specifier: ^2.2.24 specifier: ^2.2.24
version: 2.2.24(zod@3.25.64) version: 2.2.24(zod@3.25.64)
@ -390,6 +396,12 @@ packages:
peerDependencies: peerDependencies:
zod: ^3.0.0 zod: ^3.0.0
'@ai-sdk/deepseek@0.2.16':
resolution: {integrity: sha512-pIlwtjNehCpDr1wqxtSbXshynW4CiwS6S3yAKHzHi73QtmS2Hg9kE1DB0zgENKaZLmbsc4UgigGM6FzuUd4M8Q==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.0.0
'@ai-sdk/fal@0.1.12': '@ai-sdk/fal@0.1.12':
resolution: {integrity: sha512-Z0pUUR3qwLTj4HXgGJSes5fwjUbSowsMiKbpYKGl6V51sQeUk2EjZctdN4+a+GBuDNCP6Y32Wi8ejM54OMee+w==} resolution: {integrity: sha512-Z0pUUR3qwLTj4HXgGJSes5fwjUbSowsMiKbpYKGl6V51sQeUk2EjZctdN4+a+GBuDNCP6Y32Wi8ejM54OMee+w==}
engines: {node: '>=18'} engines: {node: '>=18'}
@ -414,12 +426,24 @@ packages:
peerDependencies: peerDependencies:
zod: ^3.0.0 zod: ^3.0.0
'@ai-sdk/google@1.2.22':
resolution: {integrity: sha512-Ppxu3DIieF1G9pyQ5O1Z646GYR0gkC57YdBqXJ82qvCdhEhZHu0TWhmnOoeIWe2olSbuDeoOY+MfJrW8dzS3Hw==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.0.0
'@ai-sdk/openai-compatible@0.2.14': '@ai-sdk/openai-compatible@0.2.14':
resolution: {integrity: sha512-icjObfMCHKSIbywijaoLdZ1nSnuRnWgMEMLgwoxPJgxsUHMx0aVORnsLUid4SPtdhHI3X2masrt6iaEQLvOSFw==} resolution: {integrity: sha512-icjObfMCHKSIbywijaoLdZ1nSnuRnWgMEMLgwoxPJgxsUHMx0aVORnsLUid4SPtdhHI3X2masrt6iaEQLvOSFw==}
engines: {node: '>=18'} engines: {node: '>=18'}
peerDependencies: peerDependencies:
zod: ^3.0.0 zod: ^3.0.0
'@ai-sdk/openai-compatible@0.2.16':
resolution: {integrity: sha512-LkvfcM8slJedRyJa/MiMiaOzcMjV1zNDwzTHEGz7aAsgsQV0maLfmJRi/nuSwf5jmp0EouC+JXXDUj2l94HgQw==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.0.0
'@ai-sdk/openai@1.1.13': '@ai-sdk/openai@1.1.13':
resolution: {integrity: sha512-IdChK1pJTW3NQis02PG/hHTG0gZSyQIMOLPt7f7ES56C0xH2yaKOU1Tp2aib7pZzWGwDlzTOW2h5TtAB8+V6CQ==} resolution: {integrity: sha512-IdChK1pJTW3NQis02PG/hHTG0gZSyQIMOLPt7f7ES56C0xH2yaKOU1Tp2aib7pZzWGwDlzTOW2h5TtAB8+V6CQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
@ -1029,28 +1053,24 @@ packages:
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@biomejs/cli-linux-arm64@1.9.4': '@biomejs/cli-linux-arm64@1.9.4':
resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==} resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@biomejs/cli-linux-x64-musl@1.9.4': '@biomejs/cli-linux-x64-musl@1.9.4':
resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==} resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@biomejs/cli-linux-x64@1.9.4': '@biomejs/cli-linux-x64@1.9.4':
resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==} resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@biomejs/cli-win32-arm64@1.9.4': '@biomejs/cli-win32-arm64@1.9.4':
resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==} resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==}
@ -2284,79 +2304,67 @@ packages:
resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-arm@1.0.5': '@img/sharp-libvips-linux-arm@1.0.5':
resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.0.4': '@img/sharp-libvips-linux-s390x@1.0.4':
resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-x64@1.0.4': '@img/sharp-libvips-linux-x64@1.0.4':
resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.0.4': '@img/sharp-libvips-linuxmusl-arm64@1.0.4':
resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.0.4': '@img/sharp-libvips-linuxmusl-x64@1.0.4':
resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@img/sharp-linux-arm64@0.33.5': '@img/sharp-linux-arm64@0.33.5':
resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-linux-arm@0.33.5': '@img/sharp-linux-arm@0.33.5':
resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-linux-s390x@0.33.5': '@img/sharp-linux-s390x@0.33.5':
resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-linux-x64@0.33.5': '@img/sharp-linux-x64@0.33.5':
resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.33.5': '@img/sharp-linuxmusl-arm64@0.33.5':
resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@img/sharp-linuxmusl-x64@0.33.5': '@img/sharp-linuxmusl-x64@0.33.5':
resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@img/sharp-wasm32@0.33.5': '@img/sharp-wasm32@0.33.5':
resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==}
@ -2469,56 +2477,48 @@ packages:
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@next/swc-linux-arm64-gnu@15.2.1': '@next/swc-linux-arm64-gnu@15.2.1':
resolution: {integrity: sha512-gXDX5lIboebbjhiMT6kFgu4svQyjoSed6dHyjx5uZsjlvTwOAnZpn13w9XDaIMFFHw7K8CpBK7HfDKw0VZvUXQ==} resolution: {integrity: sha512-gXDX5lIboebbjhiMT6kFgu4svQyjoSed6dHyjx5uZsjlvTwOAnZpn13w9XDaIMFFHw7K8CpBK7HfDKw0VZvUXQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@next/swc-linux-arm64-musl@15.1.2': '@next/swc-linux-arm64-musl@15.1.2':
resolution: {integrity: sha512-9CF1Pnivij7+M3G74lxr+e9h6o2YNIe7QtExWq1KUK4hsOLTBv6FJikEwCaC3NeYTflzrm69E5UfwEAbV2U9/g==} resolution: {integrity: sha512-9CF1Pnivij7+M3G74lxr+e9h6o2YNIe7QtExWq1KUK4hsOLTBv6FJikEwCaC3NeYTflzrm69E5UfwEAbV2U9/g==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@next/swc-linux-arm64-musl@15.2.1': '@next/swc-linux-arm64-musl@15.2.1':
resolution: {integrity: sha512-3v0pF/adKZkBWfUffmB/ROa+QcNTrnmYG4/SS+r52HPwAK479XcWoES2I+7F7lcbqc7mTeVXrIvb4h6rR/iDKg==} resolution: {integrity: sha512-3v0pF/adKZkBWfUffmB/ROa+QcNTrnmYG4/SS+r52HPwAK479XcWoES2I+7F7lcbqc7mTeVXrIvb4h6rR/iDKg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@next/swc-linux-x64-gnu@15.1.2': '@next/swc-linux-x64-gnu@15.1.2':
resolution: {integrity: sha512-tINV7WmcTUf4oM/eN3Yuu/f8jQ5C6AkueZPKeALs/qfdfX57eNv4Ij7rt0SA6iZ8+fMobVfcFVv664Op0caCCg==} resolution: {integrity: sha512-tINV7WmcTUf4oM/eN3Yuu/f8jQ5C6AkueZPKeALs/qfdfX57eNv4Ij7rt0SA6iZ8+fMobVfcFVv664Op0caCCg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@next/swc-linux-x64-gnu@15.2.1': '@next/swc-linux-x64-gnu@15.2.1':
resolution: {integrity: sha512-RbsVq2iB6KFJRZ2cHrU67jLVLKeuOIhnQB05ygu5fCNgg8oTewxweJE8XlLV+Ii6Y6u4EHwETdUiRNXIAfpBww==} resolution: {integrity: sha512-RbsVq2iB6KFJRZ2cHrU67jLVLKeuOIhnQB05ygu5fCNgg8oTewxweJE8XlLV+Ii6Y6u4EHwETdUiRNXIAfpBww==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@next/swc-linux-x64-musl@15.1.2': '@next/swc-linux-x64-musl@15.1.2':
resolution: {integrity: sha512-jf2IseC4WRsGkzeUw/cK3wci9pxR53GlLAt30+y+B+2qAQxMw6WAC3QrANIKxkcoPU3JFh/10uFfmoMDF9JXKg==} resolution: {integrity: sha512-jf2IseC4WRsGkzeUw/cK3wci9pxR53GlLAt30+y+B+2qAQxMw6WAC3QrANIKxkcoPU3JFh/10uFfmoMDF9JXKg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@next/swc-linux-x64-musl@15.2.1': '@next/swc-linux-x64-musl@15.2.1':
resolution: {integrity: sha512-QHsMLAyAIu6/fWjHmkN/F78EFPKmhQlyX5C8pRIS2RwVA7z+t9cTb0IaYWC3EHLOTjsU7MNQW+n2xGXr11QPpg==} resolution: {integrity: sha512-QHsMLAyAIu6/fWjHmkN/F78EFPKmhQlyX5C8pRIS2RwVA7z+t9cTb0IaYWC3EHLOTjsU7MNQW+n2xGXr11QPpg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@next/swc-win32-arm64-msvc@15.1.2': '@next/swc-win32-arm64-msvc@15.1.2':
resolution: {integrity: sha512-wvg7MlfnaociP7k8lxLX4s2iBJm4BrNiNFhVUY+Yur5yhAJHfkS8qPPeDEUH8rQiY0PX3u/P7Q/wcg6Mv6GSAA==} resolution: {integrity: sha512-wvg7MlfnaociP7k8lxLX4s2iBJm4BrNiNFhVUY+Yur5yhAJHfkS8qPPeDEUH8rQiY0PX3u/P7Q/wcg6Mv6GSAA==}
@ -3104,37 +3104,31 @@ packages:
resolution: {integrity: sha512-BhEzNLjn4HjP8+Q18D3/jeIDBxW7OgoJYIjw2CaaysnYneoTlij8hPTKxHfyqq4IGM3fFs9TLR/k338M3zkQ7g==} resolution: {integrity: sha512-BhEzNLjn4HjP8+Q18D3/jeIDBxW7OgoJYIjw2CaaysnYneoTlij8hPTKxHfyqq4IGM3fFs9TLR/k338M3zkQ7g==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@oxc-resolver/binding-linux-arm64-musl@11.2.0': '@oxc-resolver/binding-linux-arm64-musl@11.2.0':
resolution: {integrity: sha512-yxbMYUgRmN2V8x8XoxmD/Qq6aG7YIW3ToMDILfmcfeeRRVieEJ3DOWBT0JSE+YgrOy79OyFDH/1lO8VnqLmDQQ==} resolution: {integrity: sha512-yxbMYUgRmN2V8x8XoxmD/Qq6aG7YIW3ToMDILfmcfeeRRVieEJ3DOWBT0JSE+YgrOy79OyFDH/1lO8VnqLmDQQ==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@oxc-resolver/binding-linux-riscv64-gnu@11.2.0': '@oxc-resolver/binding-linux-riscv64-gnu@11.2.0':
resolution: {integrity: sha512-QG1UfgC2N2qhW1tOnDCgB/26vn1RCshR5sYPhMeaxO1gMQ3kEKbZ3QyBXxrG1IX5qsXYj5hPDJLDYNYUjRcOpg==} resolution: {integrity: sha512-QG1UfgC2N2qhW1tOnDCgB/26vn1RCshR5sYPhMeaxO1gMQ3kEKbZ3QyBXxrG1IX5qsXYj5hPDJLDYNYUjRcOpg==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [glibc]
'@oxc-resolver/binding-linux-s390x-gnu@11.2.0': '@oxc-resolver/binding-linux-s390x-gnu@11.2.0':
resolution: {integrity: sha512-uqTDsQdi6mrkSV1gvwbuT8jf/WFl6qVDVjNlx7IPSaAByrNiJfPrhTmH8b+Do58Dylz7QIRZgxQ8CHIZSyBUdg==} resolution: {integrity: sha512-uqTDsQdi6mrkSV1gvwbuT8jf/WFl6qVDVjNlx7IPSaAByrNiJfPrhTmH8b+Do58Dylz7QIRZgxQ8CHIZSyBUdg==}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
libc: [glibc]
'@oxc-resolver/binding-linux-x64-gnu@11.2.0': '@oxc-resolver/binding-linux-x64-gnu@11.2.0':
resolution: {integrity: sha512-GZdHXhJ7p6GaQg9MjRqLebwBf8BLvGIagccI6z5yMj4fV3LU4QuDfwSEERG+R6oQ/Su9672MBqWwncvKcKT68w==} resolution: {integrity: sha512-GZdHXhJ7p6GaQg9MjRqLebwBf8BLvGIagccI6z5yMj4fV3LU4QuDfwSEERG+R6oQ/Su9672MBqWwncvKcKT68w==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@oxc-resolver/binding-linux-x64-musl@11.2.0': '@oxc-resolver/binding-linux-x64-musl@11.2.0':
resolution: {integrity: sha512-YBAC3GOicYznReG2twE7oFPSeK9Z1f507z1EYWKg6HpGYRYRlJyszViu7PrhMT85r/MumDTs429zm+CNqpFWOA==} resolution: {integrity: sha512-YBAC3GOicYznReG2twE7oFPSeK9Z1f507z1EYWKg6HpGYRYRlJyszViu7PrhMT85r/MumDTs429zm+CNqpFWOA==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@oxc-resolver/binding-wasm32-wasi@11.2.0': '@oxc-resolver/binding-wasm32-wasi@11.2.0':
resolution: {integrity: sha512-+qlIg45CPVPy+Jn3vqU1zkxA/AAv6e/2Ax/ImX8usZa8Tr2JmQn/93bmSOOOnr9fXRV9d0n4JyqYzSWxWPYDEw==} resolution: {integrity: sha512-+qlIg45CPVPy+Jn3vqU1zkxA/AAv6e/2Ax/ImX8usZa8Tr2JmQn/93bmSOOOnr9fXRV9d0n4JyqYzSWxWPYDEw==}
@ -5254,28 +5248,24 @@ packages:
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-arm64-musl@4.0.14': '@tailwindcss/oxide-linux-arm64-musl@4.0.14':
resolution: {integrity: sha512-gVkJdnR/L6iIcGYXx64HGJRmlme2FGr/aZH0W6u4A3RgPMAb+6ELRLi+UBiH83RXBm9vwCfkIC/q8T51h8vUJQ==} resolution: {integrity: sha512-gVkJdnR/L6iIcGYXx64HGJRmlme2FGr/aZH0W6u4A3RgPMAb+6ELRLi+UBiH83RXBm9vwCfkIC/q8T51h8vUJQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@tailwindcss/oxide-linux-x64-gnu@4.0.14': '@tailwindcss/oxide-linux-x64-gnu@4.0.14':
resolution: {integrity: sha512-EE+EQ+c6tTpzsg+LGO1uuusjXxYx0Q00JE5ubcIGfsogSKth8n8i2BcS2wYTQe4jXGs+BQs35l78BIPzgwLddw==} resolution: {integrity: sha512-EE+EQ+c6tTpzsg+LGO1uuusjXxYx0Q00JE5ubcIGfsogSKth8n8i2BcS2wYTQe4jXGs+BQs35l78BIPzgwLddw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-x64-musl@4.0.14': '@tailwindcss/oxide-linux-x64-musl@4.0.14':
resolution: {integrity: sha512-KCCOzo+L6XPT0oUp2Jwh233ETRQ/F6cwUnMnR0FvMUCbkDAzHbcyOgpfuAtRa5HD0WbTbH4pVD+S0pn1EhNfbw==} resolution: {integrity: sha512-KCCOzo+L6XPT0oUp2Jwh233ETRQ/F6cwUnMnR0FvMUCbkDAzHbcyOgpfuAtRa5HD0WbTbH4pVD+S0pn1EhNfbw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@tailwindcss/oxide-win32-arm64-msvc@4.0.14': '@tailwindcss/oxide-win32-arm64-msvc@4.0.14':
resolution: {integrity: sha512-AHObFiFL9lNYcm3tZSPqa/cHGpM5wOrNmM2uOMoKppp+0Hom5uuyRh0QkOp7jftsHZdrZUpmoz0Mp6vhh2XtUg==} resolution: {integrity: sha512-AHObFiFL9lNYcm3tZSPqa/cHGpM5wOrNmM2uOMoKppp+0Hom5uuyRh0QkOp7jftsHZdrZUpmoz0Mp6vhh2XtUg==}
@ -6801,28 +6791,24 @@ packages:
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.29.2: lightningcss-linux-arm64-musl@1.29.2:
resolution: {integrity: sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==} resolution: {integrity: sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.29.2: lightningcss-linux-x64-gnu@1.29.2:
resolution: {integrity: sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==} resolution: {integrity: sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.29.2: lightningcss-linux-x64-musl@1.29.2:
resolution: {integrity: sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==} resolution: {integrity: sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.29.2: lightningcss-win32-arm64-msvc@1.29.2:
resolution: {integrity: sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==} resolution: {integrity: sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==}
@ -8405,6 +8391,13 @@ snapshots:
'@ai-sdk/provider-utils': 2.2.8(zod@3.25.64) '@ai-sdk/provider-utils': 2.2.8(zod@3.25.64)
zod: 3.25.64 zod: 3.25.64
'@ai-sdk/deepseek@0.2.16(zod@3.25.64)':
dependencies:
'@ai-sdk/openai-compatible': 0.2.16(zod@3.25.64)
'@ai-sdk/provider': 1.1.3
'@ai-sdk/provider-utils': 2.2.8(zod@3.25.64)
zod: 3.25.64
'@ai-sdk/fal@0.1.12(zod@3.25.64)': '@ai-sdk/fal@0.1.12(zod@3.25.64)':
dependencies: dependencies:
'@ai-sdk/provider': 1.1.3 '@ai-sdk/provider': 1.1.3
@ -8436,12 +8429,24 @@ snapshots:
'@ai-sdk/provider-utils': 2.2.8(zod@3.25.64) '@ai-sdk/provider-utils': 2.2.8(zod@3.25.64)
zod: 3.25.64 zod: 3.25.64
'@ai-sdk/google@1.2.22(zod@3.25.64)':
dependencies:
'@ai-sdk/provider': 1.1.3
'@ai-sdk/provider-utils': 2.2.8(zod@3.25.64)
zod: 3.25.64
'@ai-sdk/openai-compatible@0.2.14(zod@3.25.64)': '@ai-sdk/openai-compatible@0.2.14(zod@3.25.64)':
dependencies: dependencies:
'@ai-sdk/provider': 1.1.3 '@ai-sdk/provider': 1.1.3
'@ai-sdk/provider-utils': 2.2.8(zod@3.25.64) '@ai-sdk/provider-utils': 2.2.8(zod@3.25.64)
zod: 3.25.64 zod: 3.25.64
'@ai-sdk/openai-compatible@0.2.16(zod@3.25.64)':
dependencies:
'@ai-sdk/provider': 1.1.3
'@ai-sdk/provider-utils': 2.2.8(zod@3.25.64)
zod: 3.25.64
'@ai-sdk/openai@1.1.13(zod@3.25.64)': '@ai-sdk/openai@1.1.13(zod@3.25.64)':
dependencies: dependencies:
'@ai-sdk/provider': 1.0.8 '@ai-sdk/provider': 1.0.8

View File

@ -1,10 +1,7 @@
'use client'; 'use client';
import { checkWebContentAnalysisCreditsAction } from '@/actions/check-web-content-analysis-credits'; import { checkWebContentAnalysisCreditsAction } from '@/actions/check-web-content-analysis-credits';
import { import type { UrlInputFormProps } from '@/ai/text/utils/web-content-analyzer';
type UrlInputFormProps,
urlSchema,
} from '@/ai/text/utils/web-content-analyzer';
import { webContentAnalyzerConfig } from '@/ai/text/utils/web-content-analyzer-config'; import { webContentAnalyzerConfig } from '@/ai/text/utils/web-content-analyzer-config';
import { LoginWrapper } from '@/components/auth/login-wrapper'; import { LoginWrapper } from '@/components/auth/login-wrapper';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
@ -16,6 +13,13 @@ import {
FormMessage, FormMessage,
} from '@/components/ui/form'; } from '@/components/ui/form';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { useLocalePathname } from '@/i18n/navigation'; import { useLocalePathname } from '@/i18n/navigation';
import { authClient } from '@/lib/auth-client'; import { authClient } from '@/lib/auth-client';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
@ -36,7 +40,7 @@ import { useDebounce } from '../utils/performance';
// Form schema for URL input // Form schema for URL input
const urlFormSchema = z.object({ const urlFormSchema = z.object({
url: urlSchema, url: z.string().url().optional(), // Allow empty string for initial state
}); });
type UrlFormData = z.infer<typeof urlFormSchema>; type UrlFormData = z.infer<typeof urlFormSchema>;
@ -45,6 +49,8 @@ export function UrlInputForm({
onSubmit, onSubmit,
isLoading, isLoading,
disabled = false, disabled = false,
modelProvider,
setModelProvider,
}: UrlInputFormProps) { }: UrlInputFormProps) {
const [creditInfo, setCreditInfo] = useState<{ const [creditInfo, setCreditInfo] = useState<{
hasEnoughCredits: boolean; hasEnoughCredits: boolean;
@ -133,7 +139,7 @@ export function UrlInputForm({
}, 0); }, 0);
return; return;
} }
onSubmit(data.url); onSubmit(data.url ?? '', modelProvider);
}; };
const handleFormSubmit = form.handleSubmit(handleSubmit); const handleFormSubmit = form.handleSubmit(handleSubmit);
@ -144,6 +150,23 @@ export function UrlInputForm({
return ( return (
<> <>
<div className="w-full max-w-2xl mx-auto"> <div className="w-full max-w-2xl mx-auto">
{/* Model Provider Selection (for mobile/smaller screens, optional) */}
<div className="flex justify-end items-center mb-4">
<Select
value={modelProvider}
onValueChange={setModelProvider}
disabled={isLoading || disabled}
>
<SelectTrigger id="model-provider-select-form" className="w-40">
<SelectValue placeholder="Select model" />
</SelectTrigger>
<SelectContent>
<SelectItem value="openai">OpenAI GPT-4o</SelectItem>
<SelectItem value="gemini">Google Gemini</SelectItem>
<SelectItem value="deepseek">DeepSeek</SelectItem>
</SelectContent>
</Select>
</div>
<Form {...form}> <Form {...form}>
<form onSubmit={handleFormSubmit} className="space-y-4"> <form onSubmit={handleFormSubmit} className="space-y-4">
<FormField <FormField
@ -216,7 +239,7 @@ export function UrlInputForm({
) : isAuthenticated ? ( ) : isAuthenticated ? (
<Button <Button
type="submit" type="submit"
disabled={isFormDisabled || !urlValue.trim()} disabled={isFormDisabled || !urlValue?.trim()}
className="w-full" className="w-full"
size="lg" size="lg"
> >

View File

@ -3,6 +3,7 @@
import type { import type {
AnalysisState, AnalysisState,
AnalyzeContentResponse, AnalyzeContentResponse,
ModelProvider,
WebContentAnalyzerProps, WebContentAnalyzerProps,
} from '@/ai/text/utils/web-content-analyzer'; } from '@/ai/text/utils/web-content-analyzer';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
@ -192,150 +193,161 @@ export function WebContentAnalyzer({ className }: WebContentAnalyzerProps) {
// Use reducer for better state management and performance // Use reducer for better state management and performance
const [state, dispatch] = useReducer(analysisReducer, initialState); const [state, dispatch] = useReducer(analysisReducer, initialState);
// Model provider state
const [modelProvider, setModelProvider] = useState<ModelProvider>('openai');
// Enhanced error state // Enhanced error state
const [analyzedError, setAnalyzedError] = const [analyzedError, setAnalyzedError] =
useState<WebContentAnalyzerError | null>(null); useState<WebContentAnalyzerError | null>(null);
// Handle analysis submission with enhanced error handling // Handle analysis submission with enhanced error handling
const handleAnalyzeUrl = useCallback(async (url: string) => { const handleAnalyzeUrl = useCallback(
// Reset state and start analysis async (url: string, provider: ModelProvider) => {
dispatch({ type: 'START_ANALYSIS', payload: { url } }); // Reset state and start analysis
setAnalyzedError(null); dispatch({ type: 'START_ANALYSIS', payload: { url } });
setAnalyzedError(null);
try { try {
// Use retry mechanism for the API call // Use retry mechanism for the API call
const result = await withRetry(async () => { const result = await withRetry(async () => {
const response = await fetch('/api/analyze-content', { const response = await fetch('/api/analyze-content', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ url }), body: JSON.stringify({ url, modelProvider: provider }),
}); });
const data: AnalyzeContentResponse = await response.json(); const data: AnalyzeContentResponse = await response.json();
// Handle HTTP errors // Handle HTTP errors
if (!response.ok) { if (!response.ok) {
// Create specific error based on status code // Create specific error based on status code
let errorType = ErrorType.UNKNOWN; let errorType = ErrorType.UNKNOWN;
let severity = ErrorSeverity.MEDIUM; let severity = ErrorSeverity.MEDIUM;
let retryable = true; let retryable = true;
switch (response.status) { switch (response.status) {
case 400: case 400:
errorType = ErrorType.VALIDATION; errorType = ErrorType.VALIDATION;
retryable = false; retryable = false;
break; break;
case 401: case 401:
errorType = ErrorType.AUTHENTICATION; errorType = ErrorType.AUTHENTICATION;
severity = ErrorSeverity.HIGH; severity = ErrorSeverity.HIGH;
retryable = false; retryable = false;
break; break;
case 402: case 402:
errorType = ErrorType.CREDITS; errorType = ErrorType.CREDITS;
severity = ErrorSeverity.HIGH; severity = ErrorSeverity.HIGH;
retryable = false; retryable = false;
break; break;
case 408: case 408:
errorType = ErrorType.TIMEOUT; errorType = ErrorType.TIMEOUT;
break; break;
case 422: case 422:
errorType = ErrorType.SCRAPING; errorType = ErrorType.SCRAPING;
break; break;
case 429: case 429:
errorType = ErrorType.RATE_LIMIT; errorType = ErrorType.RATE_LIMIT;
break; break;
case 503: case 503:
errorType = ErrorType.SERVICE_UNAVAILABLE; errorType = ErrorType.SERVICE_UNAVAILABLE;
severity = ErrorSeverity.HIGH; severity = ErrorSeverity.HIGH;
break; break;
default: default:
errorType = ErrorType.NETWORK; errorType = ErrorType.NETWORK;
}
throw new WebContentAnalyzerError(
errorType,
data.error || `HTTP ${response.status}: ${response.statusText}`,
data.error || 'Failed to analyze website. Please try again.',
severity,
retryable
);
} }
throw new WebContentAnalyzerError( if (!data.success || !data.data) {
errorType, throw new WebContentAnalyzerError(
data.error || `HTTP ${response.status}: ${response.statusText}`, ErrorType.ANALYSIS,
data.error || 'Failed to analyze website. Please try again.', data.error || 'Analysis failed',
severity, data.error ||
retryable 'Failed to analyze website content. Please try again.',
); ErrorSeverity.MEDIUM,
} true
);
}
if (!data.success || !data.data) { return data;
throw new WebContentAnalyzerError(
ErrorType.ANALYSIS,
data.error || 'Analysis failed',
data.error ||
'Failed to analyze website content. Please try again.',
ErrorSeverity.MEDIUM,
true
);
}
return data;
});
// Update state to analyzing stage
dispatch({ type: 'SET_LOADING_STAGE', payload: { stage: 'analyzing' } });
// Simulate a brief delay for analyzing stage to show progress
await new Promise((resolve) => setTimeout(resolve, 1000));
// Set results and complete analysis
dispatch({
type: 'SET_RESULTS',
payload: {
results: result.data!.analysis,
screenshot: result.data!.screenshot,
},
});
// Show success toast - defer to avoid flushSync during render
setTimeout(() => {
toast.success('Website analysis completed successfully!', {
description: `Analyzed ${new URL(url).hostname}`,
}); });
}, 0);
} catch (error) {
// Classify the error
const analyzedError =
error instanceof WebContentAnalyzerError ? error : classifyError(error);
// Log the error // Update state to analyzing stage
logError(analyzedError, { url, component: 'WebContentAnalyzer' }); dispatch({
type: 'SET_LOADING_STAGE',
payload: { stage: 'analyzing' },
});
// Update state with error // Simulate a brief delay for analyzing stage to show progress
dispatch({ await new Promise((resolve) => setTimeout(resolve, 1000));
type: 'SET_ERROR',
payload: { error: analyzedError.userMessage },
});
// Set the analyzed error for the ErrorDisplay component // Set results and complete analysis
setAnalyzedError(analyzedError); dispatch({
type: 'SET_RESULTS',
payload: {
results: result.data!.analysis,
screenshot: result.data!.screenshot,
},
});
// Show error toast with appropriate severity - defer to avoid flushSync during render // Show success toast - defer to avoid flushSync during render
const toastOptions = { setTimeout(() => {
description: analyzedError.userMessage, toast.success('Website analysis completed successfully!', {
}; description: `Analyzed ${new URL(url).hostname}`,
});
}, 0);
} catch (error) {
// Classify the error
const analyzedError =
error instanceof WebContentAnalyzerError
? error
: classifyError(error);
setTimeout(() => { // Log the error
switch (analyzedError.severity) { logError(analyzedError, { url, component: 'WebContentAnalyzer' });
case ErrorSeverity.CRITICAL:
case ErrorSeverity.HIGH: // Update state with error
toast.error('Analysis Failed', toastOptions); dispatch({
break; type: 'SET_ERROR',
case ErrorSeverity.MEDIUM: payload: { error: analyzedError.userMessage },
toast.warning('Analysis Failed', toastOptions); });
break;
case ErrorSeverity.LOW: // Set the analyzed error for the ErrorDisplay component
toast.info('Analysis Issue', toastOptions); setAnalyzedError(analyzedError);
break;
} // Show error toast with appropriate severity - defer to avoid flushSync during render
}, 0); const toastOptions = {
} description: analyzedError.userMessage,
}, []); };
setTimeout(() => {
switch (analyzedError.severity) {
case ErrorSeverity.CRITICAL:
case ErrorSeverity.HIGH:
toast.error('Analysis Failed', toastOptions);
break;
case ErrorSeverity.MEDIUM:
toast.warning('Analysis Failed', toastOptions);
break;
case ErrorSeverity.LOW:
toast.info('Analysis Issue', toastOptions);
break;
}
}, 0);
}
},
[]
);
// Handle starting a new analysis // Handle starting a new analysis
const handleNewAnalysis = useCallback(() => { const handleNewAnalysis = useCallback(() => {
@ -374,6 +386,8 @@ export function WebContentAnalyzer({ className }: WebContentAnalyzerProps) {
onSubmit={handleAnalyzeUrl} onSubmit={handleAnalyzeUrl}
isLoading={state.isLoading} isLoading={state.isLoading}
disabled={state.isLoading} disabled={state.isLoading}
modelProvider={modelProvider}
setModelProvider={setModelProvider}
/> />
)} )}

View File

@ -100,16 +100,23 @@ export const webContentAnalyzerConfig = {
}, },
/** /**
* OpenAI analysis options * AI model providers
*/ */
openai: { openai: {
model: 'gpt-4o-mini', model: 'gpt-4o-mini',
temperature: 0.1, // Low temperature for consistent results temperature: 0.1, // Low temperature for consistent results
/**
* Token optimization settings
*/
maxTokens: 2000, // Limit response tokens for performance maxTokens: 2000, // Limit response tokens for performance
}, },
gemini: {
model: 'gemini-2.0-flash',
temperature: 0.1,
maxTokens: 2000,
},
deepseek: {
model: 'deepseek-chat',
temperature: 0.1,
maxTokens: 2000,
},
} as const; } as const;
/** /**

View File

@ -15,6 +15,7 @@ export interface AnalysisResults {
// API Request/Response Interfaces // API Request/Response Interfaces
export interface AnalyzeContentRequest { export interface AnalyzeContentRequest {
url: string; url: string;
modelProvider: ModelProvider;
} }
export interface AnalyzeContentResponse { export interface AnalyzeContentResponse {
@ -66,14 +67,19 @@ export interface AnalysisState {
} }
// Component Props Interfaces // Component Props Interfaces
export type ModelProvider = 'openai' | 'gemini' | 'deepseek';
export interface WebContentAnalyzerProps { export interface WebContentAnalyzerProps {
className?: string; className?: string;
modelProvider?: ModelProvider;
} }
export interface UrlInputFormProps { export interface UrlInputFormProps {
onSubmit: (url: string) => void; onSubmit: (url: string, modelProvider: ModelProvider) => void;
isLoading: boolean; isLoading: boolean;
disabled?: boolean; disabled?: boolean;
modelProvider: ModelProvider;
setModelProvider: (provider: ModelProvider) => void;
} }
export interface AnalysisResultsProps { export interface AnalysisResultsProps {
@ -114,6 +120,7 @@ export const analysisResultsSchema = z.object({
// API Request Schema // API Request Schema
export const analyzeContentRequestSchema = z.object({ export const analyzeContentRequestSchema = z.object({
url: urlSchema, url: urlSchema,
modelProvider: z.enum(['openai', 'gemini', 'deepseek']),
}); });
// API Response Schema // API Response Schema

View File

@ -19,7 +19,9 @@ import {
} from '@/ai/text/utils/web-content-analyzer-config'; } from '@/ai/text/utils/web-content-analyzer-config';
import { consumeCredits, hasEnoughCredits } from '@/credits/credits'; import { consumeCredits, hasEnoughCredits } from '@/credits/credits';
import { getSession } from '@/lib/server'; import { getSession } from '@/lib/server';
import { openai } from '@ai-sdk/openai'; import { createDeepSeek } from '@ai-sdk/deepseek';
import { createGoogleGenerativeAI } from '@ai-sdk/google';
import { createOpenAI } from '@ai-sdk/openai';
import FirecrawlApp from '@mendable/firecrawl-js'; import FirecrawlApp from '@mendable/firecrawl-js';
import { generateObject } from 'ai'; import { generateObject } from 'ai';
import { type NextRequest, NextResponse } from 'next/server'; import { type NextRequest, NextResponse } from 'next/server';
@ -195,15 +197,50 @@ async function scrapeWebpage(
}); });
} }
// Analyze content using OpenAI with retry logic // Analyze content using selected provider with retry logic
async function analyzeContent( async function analyzeContent(
content: string, content: string,
url: string url: string,
provider: string
): Promise<AnalysisResults> { ): Promise<AnalysisResults> {
return withRetry(async () => { return withRetry(async () => {
try { try {
let model: any;
let temperature: number | undefined;
let maxTokens: number | undefined;
switch (provider) {
case 'openai':
model = createOpenAI({
apiKey: process.env.OPENAI_API_KEY,
}).chat(webContentAnalyzerConfig.openai.model);
temperature = webContentAnalyzerConfig.openai.temperature;
maxTokens = webContentAnalyzerConfig.openai.maxTokens;
break;
case 'gemini':
model = createGoogleGenerativeAI({
apiKey: process.env.GOOGLE_API_KEY,
}).chat(webContentAnalyzerConfig.gemini.model);
temperature = webContentAnalyzerConfig.gemini.temperature;
maxTokens = webContentAnalyzerConfig.gemini.maxTokens;
break;
case 'deepseek':
model = createDeepSeek({
apiKey: process.env.DEEPSEEK_API_KEY,
}).chat(webContentAnalyzerConfig.deepseek.model);
temperature = webContentAnalyzerConfig.deepseek.temperature;
maxTokens = webContentAnalyzerConfig.deepseek.maxTokens;
break;
default:
throw new WebContentAnalyzerError(
ErrorType.VALIDATION,
'Invalid model provider',
'Please select a valid model provider.',
ErrorSeverity.MEDIUM,
false
);
}
const { object } = await generateObject({ const { object } = await generateObject({
model: openai(webContentAnalyzerConfig.openai.model), model,
schema: analysisSchema, schema: analysisSchema,
prompt: ` prompt: `
Analyze the following webpage content and extract structured information. Analyze the following webpage content and extract structured information.
@ -216,8 +253,8 @@ async function analyzeContent(
- For features and use cases: provide empty arrays if none are found - For features and use cases: provide empty arrays if none are found
- Ensure the title and description are meaningful and based on the actual content - Ensure the title and description are meaningful and based on the actual content
`, `,
temperature: webContentAnalyzerConfig.openai.temperature, temperature,
maxTokens: webContentAnalyzerConfig.openai.maxTokens, maxTokens,
}); });
return { return {
@ -229,11 +266,9 @@ async function analyzeContent(
if (error instanceof WebContentAnalyzerError) { if (error instanceof WebContentAnalyzerError) {
throw error; throw error;
} }
// Check for specific OpenAI/AI errors // Check for specific OpenAI/AI errors
if (error instanceof Error) { if (error instanceof Error) {
const message = error.message.toLowerCase(); const message = error.message.toLowerCase();
if (message.includes('rate limit') || message.includes('quota')) { if (message.includes('rate limit') || message.includes('quota')) {
throw new WebContentAnalyzerError( throw new WebContentAnalyzerError(
ErrorType.RATE_LIMIT, ErrorType.RATE_LIMIT,
@ -244,7 +279,6 @@ async function analyzeContent(
error error
); );
} }
if (message.includes('timeout') || message.includes('aborted')) { if (message.includes('timeout') || message.includes('aborted')) {
throw new WebContentAnalyzerError( throw new WebContentAnalyzerError(
ErrorType.TIMEOUT, ErrorType.TIMEOUT,
@ -256,7 +290,6 @@ async function analyzeContent(
); );
} }
} }
// Classify and throw the error // Classify and throw the error
throw classifyError(error); throw classifyError(error);
} }
@ -295,7 +328,8 @@ export async function POST(req: NextRequest) {
); );
} }
const { url } = validationResult.data; const { url, modelProvider } = validationResult.data;
console.log('modelProvider', modelProvider, 'url', url);
// Additional URL validation // Additional URL validation
const urlValidation = validateUrl(url); const urlValidation = validateUrl(url);
@ -402,8 +436,8 @@ export async function POST(req: NextRequest) {
// Step 1: Scrape webpage // Step 1: Scrape webpage
const { content, screenshot } = await scrapeWebpage(url); const { content, screenshot } = await scrapeWebpage(url);
// Step 2: Analyze content with AI // Step 2: Analyze content with AI (pass provider)
const analysis = await analyzeContent(content, url); const analysis = await analyzeContent(content, url, modelProvider);
// Step 3: Consume credits (only on successful analysis) // Step 3: Consume credits (only on successful analysis)
await consumeCredits({ await consumeCredits({

View File

@ -54,11 +54,6 @@ export default function CreditsBalanceCard() {
} | null>(null); } | null>(null);
const [isLoadingStats, setIsLoadingStats] = useState(true); const [isLoadingStats, setIsLoadingStats] = useState(true);
// Don't render if credits are disabled
if (!websiteConfig.credits.enableCredits) {
return null;
}
// Fetch credit statistics // Fetch credit statistics
const fetchCreditStats = useCallback(async () => { const fetchCreditStats = useCallback(async () => {
console.log('fetchCreditStats, fetch start'); console.log('fetchCreditStats, fetch start');
@ -114,6 +109,11 @@ export default function CreditsBalanceCard() {
fetchCreditStats(); fetchCreditStats();
}, [fetchCredits, fetchCreditStats]); }, [fetchCredits, fetchCreditStats]);
// Don't render if credits are disabled
if (!websiteConfig.credits.enableCredits) {
return null;
}
// Render loading skeleton // Render loading skeleton
const isPageLoading = isLoadingBalance || isLoadingStats; const isPageLoading = isLoadingBalance || isLoadingStats;
if (!mounted || isPageLoading) { if (!mounted || isPageLoading) {

View File

@ -23,11 +23,6 @@ import { CreditCheckoutButton } from './credit-checkout-button';
* @returns Credit packages component * @returns Credit packages component
*/ */
export function CreditPackages() { export function CreditPackages() {
// If credits are not enabled, return null
if (!websiteConfig.credits.enableCredits) {
return null;
}
const t = useTranslations('Dashboard.settings.credits.packages'); const t = useTranslations('Dashboard.settings.credits.packages');
// Get current user and payment info // Get current user and payment info
@ -36,6 +31,8 @@ export function CreditPackages() {
// Check if user is on free plan and enableForFreePlan is false // Check if user is on free plan and enableForFreePlan is false
const isFreePlan = currentPlan?.isFree === true; const isFreePlan = currentPlan?.isFree === true;
// Check if user is on free plan and enableForFreePlan is false
if (isFreePlan && !websiteConfig.credits.enableForFreePlan) { if (isFreePlan && !websiteConfig.credits.enableForFreePlan) {
return null; return null;
} }