diff --git a/env.example b/env.example index 93eb301..8a680de 100644 --- a/env.example +++ b/env.example @@ -181,6 +181,8 @@ FAL_API_KEY="" FIREWORKS_API_KEY="" OPENAI_API_KEY="" REPLICATE_API_TOKEN="" +GOOGLE_API_KEY="" +DEEPSEEK_API_KEY="" # ----------------------------------------------------------------------------- # Web Content Analyzer (Firecrawl) diff --git a/package.json b/package.json index b9d2ab5..ac9a75e 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,10 @@ "knip": "knip" }, "dependencies": { + "@ai-sdk/deepseek": "^0.2.16", "@ai-sdk/fal": "^0.1.12", "@ai-sdk/fireworks": "^0.2.14", + "@ai-sdk/google": "^1.2.22", "@ai-sdk/google-vertex": "^2.2.24", "@ai-sdk/openai": "^1.1.13", "@ai-sdk/replicate": "^0.2.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b3c4f74..67aae87 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,12 +8,18 @@ importers: .: dependencies: + '@ai-sdk/deepseek': + specifier: ^0.2.16 + version: 0.2.16(zod@3.25.64) '@ai-sdk/fal': specifier: ^0.1.12 version: 0.1.12(zod@3.25.64) '@ai-sdk/fireworks': specifier: ^0.2.14 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': specifier: ^2.2.24 version: 2.2.24(zod@3.25.64) @@ -390,6 +396,12 @@ packages: peerDependencies: 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': resolution: {integrity: sha512-Z0pUUR3qwLTj4HXgGJSes5fwjUbSowsMiKbpYKGl6V51sQeUk2EjZctdN4+a+GBuDNCP6Y32Wi8ejM54OMee+w==} engines: {node: '>=18'} @@ -414,12 +426,24 @@ packages: peerDependencies: 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': resolution: {integrity: sha512-icjObfMCHKSIbywijaoLdZ1nSnuRnWgMEMLgwoxPJgxsUHMx0aVORnsLUid4SPtdhHI3X2masrt6iaEQLvOSFw==} engines: {node: '>=18'} peerDependencies: 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': resolution: {integrity: sha512-IdChK1pJTW3NQis02PG/hHTG0gZSyQIMOLPt7f7ES56C0xH2yaKOU1Tp2aib7pZzWGwDlzTOW2h5TtAB8+V6CQ==} engines: {node: '>=18'} @@ -1029,28 +1053,24 @@ packages: engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - libc: [musl] '@biomejs/cli-linux-arm64@1.9.4': resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - libc: [glibc] '@biomejs/cli-linux-x64-musl@1.9.4': resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - libc: [musl] '@biomejs/cli-linux-x64@1.9.4': resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - libc: [glibc] '@biomejs/cli-win32-arm64@1.9.4': resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==} @@ -2284,79 +2304,67 @@ packages: resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-arm@1.0.5': resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-s390x@1.0.4': resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-x64@1.0.4': resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.0.4': resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.0.4': resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-linux-arm64@0.33.5': resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-linux-arm@0.33.5': resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-linux-s390x@0.33.5': resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-linux-x64@0.33.5': resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-linuxmusl-arm64@0.33.5': resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-linuxmusl-x64@0.33.5': resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-wasm32@0.33.5': resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} @@ -2469,56 +2477,48 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@next/swc-linux-arm64-gnu@15.2.1': resolution: {integrity: sha512-gXDX5lIboebbjhiMT6kFgu4svQyjoSed6dHyjx5uZsjlvTwOAnZpn13w9XDaIMFFHw7K8CpBK7HfDKw0VZvUXQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@next/swc-linux-arm64-musl@15.1.2': resolution: {integrity: sha512-9CF1Pnivij7+M3G74lxr+e9h6o2YNIe7QtExWq1KUK4hsOLTBv6FJikEwCaC3NeYTflzrm69E5UfwEAbV2U9/g==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@next/swc-linux-arm64-musl@15.2.1': resolution: {integrity: sha512-3v0pF/adKZkBWfUffmB/ROa+QcNTrnmYG4/SS+r52HPwAK479XcWoES2I+7F7lcbqc7mTeVXrIvb4h6rR/iDKg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@next/swc-linux-x64-gnu@15.1.2': resolution: {integrity: sha512-tINV7WmcTUf4oM/eN3Yuu/f8jQ5C6AkueZPKeALs/qfdfX57eNv4Ij7rt0SA6iZ8+fMobVfcFVv664Op0caCCg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@next/swc-linux-x64-gnu@15.2.1': resolution: {integrity: sha512-RbsVq2iB6KFJRZ2cHrU67jLVLKeuOIhnQB05ygu5fCNgg8oTewxweJE8XlLV+Ii6Y6u4EHwETdUiRNXIAfpBww==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@next/swc-linux-x64-musl@15.1.2': resolution: {integrity: sha512-jf2IseC4WRsGkzeUw/cK3wci9pxR53GlLAt30+y+B+2qAQxMw6WAC3QrANIKxkcoPU3JFh/10uFfmoMDF9JXKg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@next/swc-linux-x64-musl@15.2.1': resolution: {integrity: sha512-QHsMLAyAIu6/fWjHmkN/F78EFPKmhQlyX5C8pRIS2RwVA7z+t9cTb0IaYWC3EHLOTjsU7MNQW+n2xGXr11QPpg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@next/swc-win32-arm64-msvc@15.1.2': resolution: {integrity: sha512-wvg7MlfnaociP7k8lxLX4s2iBJm4BrNiNFhVUY+Yur5yhAJHfkS8qPPeDEUH8rQiY0PX3u/P7Q/wcg6Mv6GSAA==} @@ -3104,37 +3104,31 @@ packages: resolution: {integrity: sha512-BhEzNLjn4HjP8+Q18D3/jeIDBxW7OgoJYIjw2CaaysnYneoTlij8hPTKxHfyqq4IGM3fFs9TLR/k338M3zkQ7g==} cpu: [arm64] os: [linux] - libc: [glibc] '@oxc-resolver/binding-linux-arm64-musl@11.2.0': resolution: {integrity: sha512-yxbMYUgRmN2V8x8XoxmD/Qq6aG7YIW3ToMDILfmcfeeRRVieEJ3DOWBT0JSE+YgrOy79OyFDH/1lO8VnqLmDQQ==} cpu: [arm64] os: [linux] - libc: [musl] '@oxc-resolver/binding-linux-riscv64-gnu@11.2.0': resolution: {integrity: sha512-QG1UfgC2N2qhW1tOnDCgB/26vn1RCshR5sYPhMeaxO1gMQ3kEKbZ3QyBXxrG1IX5qsXYj5hPDJLDYNYUjRcOpg==} cpu: [riscv64] os: [linux] - libc: [glibc] '@oxc-resolver/binding-linux-s390x-gnu@11.2.0': resolution: {integrity: sha512-uqTDsQdi6mrkSV1gvwbuT8jf/WFl6qVDVjNlx7IPSaAByrNiJfPrhTmH8b+Do58Dylz7QIRZgxQ8CHIZSyBUdg==} cpu: [s390x] os: [linux] - libc: [glibc] '@oxc-resolver/binding-linux-x64-gnu@11.2.0': resolution: {integrity: sha512-GZdHXhJ7p6GaQg9MjRqLebwBf8BLvGIagccI6z5yMj4fV3LU4QuDfwSEERG+R6oQ/Su9672MBqWwncvKcKT68w==} cpu: [x64] os: [linux] - libc: [glibc] '@oxc-resolver/binding-linux-x64-musl@11.2.0': resolution: {integrity: sha512-YBAC3GOicYznReG2twE7oFPSeK9Z1f507z1EYWKg6HpGYRYRlJyszViu7PrhMT85r/MumDTs429zm+CNqpFWOA==} cpu: [x64] os: [linux] - libc: [musl] '@oxc-resolver/binding-wasm32-wasi@11.2.0': resolution: {integrity: sha512-+qlIg45CPVPy+Jn3vqU1zkxA/AAv6e/2Ax/ImX8usZa8Tr2JmQn/93bmSOOOnr9fXRV9d0n4JyqYzSWxWPYDEw==} @@ -5254,28 +5248,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.0.14': resolution: {integrity: sha512-gVkJdnR/L6iIcGYXx64HGJRmlme2FGr/aZH0W6u4A3RgPMAb+6ELRLi+UBiH83RXBm9vwCfkIC/q8T51h8vUJQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.0.14': resolution: {integrity: sha512-EE+EQ+c6tTpzsg+LGO1uuusjXxYx0Q00JE5ubcIGfsogSKth8n8i2BcS2wYTQe4jXGs+BQs35l78BIPzgwLddw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.0.14': resolution: {integrity: sha512-KCCOzo+L6XPT0oUp2Jwh233ETRQ/F6cwUnMnR0FvMUCbkDAzHbcyOgpfuAtRa5HD0WbTbH4pVD+S0pn1EhNfbw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@tailwindcss/oxide-win32-arm64-msvc@4.0.14': resolution: {integrity: sha512-AHObFiFL9lNYcm3tZSPqa/cHGpM5wOrNmM2uOMoKppp+0Hom5uuyRh0QkOp7jftsHZdrZUpmoz0Mp6vhh2XtUg==} @@ -6801,28 +6791,24 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.29.2: resolution: {integrity: sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.29.2: resolution: {integrity: sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.29.2: resolution: {integrity: sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.29.2: resolution: {integrity: sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==} @@ -8405,6 +8391,13 @@ snapshots: '@ai-sdk/provider-utils': 2.2.8(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)': dependencies: '@ai-sdk/provider': 1.1.3 @@ -8436,12 +8429,24 @@ snapshots: '@ai-sdk/provider-utils': 2.2.8(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)': 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.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)': dependencies: '@ai-sdk/provider': 1.0.8 diff --git a/src/ai/text/components/url-input-form.tsx b/src/ai/text/components/url-input-form.tsx index e5c9090..4592d5d 100644 --- a/src/ai/text/components/url-input-form.tsx +++ b/src/ai/text/components/url-input-form.tsx @@ -1,10 +1,7 @@ 'use client'; import { checkWebContentAnalysisCreditsAction } from '@/actions/check-web-content-analysis-credits'; -import { - type UrlInputFormProps, - urlSchema, -} from '@/ai/text/utils/web-content-analyzer'; +import type { UrlInputFormProps } from '@/ai/text/utils/web-content-analyzer'; import { webContentAnalyzerConfig } from '@/ai/text/utils/web-content-analyzer-config'; import { LoginWrapper } from '@/components/auth/login-wrapper'; import { Button } from '@/components/ui/button'; @@ -16,6 +13,13 @@ import { FormMessage, } from '@/components/ui/form'; import { Input } from '@/components/ui/input'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; import { useLocalePathname } from '@/i18n/navigation'; import { authClient } from '@/lib/auth-client'; import { zodResolver } from '@hookform/resolvers/zod'; @@ -36,7 +40,7 @@ import { useDebounce } from '../utils/performance'; // Form schema for URL input const urlFormSchema = z.object({ - url: urlSchema, + url: z.string().url().optional(), // Allow empty string for initial state }); type UrlFormData = z.infer; @@ -45,6 +49,8 @@ export function UrlInputForm({ onSubmit, isLoading, disabled = false, + modelProvider, + setModelProvider, }: UrlInputFormProps) { const [creditInfo, setCreditInfo] = useState<{ hasEnoughCredits: boolean; @@ -133,7 +139,7 @@ export function UrlInputForm({ }, 0); return; } - onSubmit(data.url); + onSubmit(data.url ?? '', modelProvider); }; const handleFormSubmit = form.handleSubmit(handleSubmit); @@ -144,6 +150,23 @@ export function UrlInputForm({ return ( <>
+ {/* Model Provider Selection (for mobile/smaller screens, optional) */} +
+ +
diff --git a/src/ai/text/components/web-content-analyzer.tsx b/src/ai/text/components/web-content-analyzer.tsx index 1c32944..e2b23d2 100644 --- a/src/ai/text/components/web-content-analyzer.tsx +++ b/src/ai/text/components/web-content-analyzer.tsx @@ -3,6 +3,7 @@ import type { AnalysisState, AnalyzeContentResponse, + ModelProvider, WebContentAnalyzerProps, } from '@/ai/text/utils/web-content-analyzer'; import { Button } from '@/components/ui/button'; @@ -192,150 +193,161 @@ export function WebContentAnalyzer({ className }: WebContentAnalyzerProps) { // Use reducer for better state management and performance const [state, dispatch] = useReducer(analysisReducer, initialState); + // Model provider state + const [modelProvider, setModelProvider] = useState('openai'); + // Enhanced error state const [analyzedError, setAnalyzedError] = useState(null); // Handle analysis submission with enhanced error handling - const handleAnalyzeUrl = useCallback(async (url: string) => { - // Reset state and start analysis - dispatch({ type: 'START_ANALYSIS', payload: { url } }); - setAnalyzedError(null); + const handleAnalyzeUrl = useCallback( + async (url: string, provider: ModelProvider) => { + // Reset state and start analysis + dispatch({ type: 'START_ANALYSIS', payload: { url } }); + setAnalyzedError(null); - try { - // Use retry mechanism for the API call - const result = await withRetry(async () => { - const response = await fetch('/api/analyze-content', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ url }), - }); + try { + // Use retry mechanism for the API call + const result = await withRetry(async () => { + const response = await fetch('/api/analyze-content', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ url, modelProvider: provider }), + }); - const data: AnalyzeContentResponse = await response.json(); + const data: AnalyzeContentResponse = await response.json(); - // Handle HTTP errors - if (!response.ok) { - // Create specific error based on status code - let errorType = ErrorType.UNKNOWN; - let severity = ErrorSeverity.MEDIUM; - let retryable = true; + // Handle HTTP errors + if (!response.ok) { + // Create specific error based on status code + let errorType = ErrorType.UNKNOWN; + let severity = ErrorSeverity.MEDIUM; + let retryable = true; - switch (response.status) { - case 400: - errorType = ErrorType.VALIDATION; - retryable = false; - break; - case 401: - errorType = ErrorType.AUTHENTICATION; - severity = ErrorSeverity.HIGH; - retryable = false; - break; - case 402: - errorType = ErrorType.CREDITS; - severity = ErrorSeverity.HIGH; - retryable = false; - break; - case 408: - errorType = ErrorType.TIMEOUT; - break; - case 422: - errorType = ErrorType.SCRAPING; - break; - case 429: - errorType = ErrorType.RATE_LIMIT; - break; - case 503: - errorType = ErrorType.SERVICE_UNAVAILABLE; - severity = ErrorSeverity.HIGH; - break; - default: - errorType = ErrorType.NETWORK; + switch (response.status) { + case 400: + errorType = ErrorType.VALIDATION; + retryable = false; + break; + case 401: + errorType = ErrorType.AUTHENTICATION; + severity = ErrorSeverity.HIGH; + retryable = false; + break; + case 402: + errorType = ErrorType.CREDITS; + severity = ErrorSeverity.HIGH; + retryable = false; + break; + case 408: + errorType = ErrorType.TIMEOUT; + break; + case 422: + errorType = ErrorType.SCRAPING; + break; + case 429: + errorType = ErrorType.RATE_LIMIT; + break; + case 503: + errorType = ErrorType.SERVICE_UNAVAILABLE; + severity = ErrorSeverity.HIGH; + break; + default: + 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( - errorType, - data.error || `HTTP ${response.status}: ${response.statusText}`, - data.error || 'Failed to analyze website. Please try again.', - severity, - retryable - ); - } + if (!data.success || !data.data) { + throw new WebContentAnalyzerError( + ErrorType.ANALYSIS, + data.error || 'Analysis failed', + data.error || + 'Failed to analyze website content. Please try again.', + ErrorSeverity.MEDIUM, + true + ); + } - if (!data.success || !data.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}`, + return data; }); - }, 0); - } catch (error) { - // Classify the error - const analyzedError = - error instanceof WebContentAnalyzerError ? error : classifyError(error); - // Log the error - logError(analyzedError, { url, component: 'WebContentAnalyzer' }); + // Update state to analyzing stage + dispatch({ + type: 'SET_LOADING_STAGE', + payload: { stage: 'analyzing' }, + }); - // Update state with error - dispatch({ - type: 'SET_ERROR', - payload: { error: analyzedError.userMessage }, - }); + // Simulate a brief delay for analyzing stage to show progress + await new Promise((resolve) => setTimeout(resolve, 1000)); - // Set the analyzed error for the ErrorDisplay component - setAnalyzedError(analyzedError); + // Set results and complete analysis + 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 - const toastOptions = { - description: analyzedError.userMessage, - }; + // 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); - 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); - } - }, []); + // Log the error + logError(analyzedError, { url, component: 'WebContentAnalyzer' }); + + // Update state with error + dispatch({ + type: 'SET_ERROR', + payload: { error: analyzedError.userMessage }, + }); + + // Set the analyzed error for the ErrorDisplay component + setAnalyzedError(analyzedError); + + // Show error toast with appropriate severity - defer to avoid flushSync during render + 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 const handleNewAnalysis = useCallback(() => { @@ -374,6 +386,8 @@ export function WebContentAnalyzer({ className }: WebContentAnalyzerProps) { onSubmit={handleAnalyzeUrl} isLoading={state.isLoading} disabled={state.isLoading} + modelProvider={modelProvider} + setModelProvider={setModelProvider} /> )} diff --git a/src/ai/text/utils/web-content-analyzer-config.ts b/src/ai/text/utils/web-content-analyzer-config.ts index 784acef..221e1b2 100644 --- a/src/ai/text/utils/web-content-analyzer-config.ts +++ b/src/ai/text/utils/web-content-analyzer-config.ts @@ -100,16 +100,23 @@ export const webContentAnalyzerConfig = { }, /** - * OpenAI analysis options + * AI model providers */ openai: { model: 'gpt-4o-mini', temperature: 0.1, // Low temperature for consistent results - /** - * Token optimization settings - */ 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; /** diff --git a/src/ai/text/utils/web-content-analyzer.ts b/src/ai/text/utils/web-content-analyzer.ts index a0929d8..72d2b7d 100644 --- a/src/ai/text/utils/web-content-analyzer.ts +++ b/src/ai/text/utils/web-content-analyzer.ts @@ -15,6 +15,7 @@ export interface AnalysisResults { // API Request/Response Interfaces export interface AnalyzeContentRequest { url: string; + modelProvider: ModelProvider; } export interface AnalyzeContentResponse { @@ -66,14 +67,19 @@ export interface AnalysisState { } // Component Props Interfaces +export type ModelProvider = 'openai' | 'gemini' | 'deepseek'; + export interface WebContentAnalyzerProps { className?: string; + modelProvider?: ModelProvider; } export interface UrlInputFormProps { - onSubmit: (url: string) => void; + onSubmit: (url: string, modelProvider: ModelProvider) => void; isLoading: boolean; disabled?: boolean; + modelProvider: ModelProvider; + setModelProvider: (provider: ModelProvider) => void; } export interface AnalysisResultsProps { @@ -114,6 +120,7 @@ export const analysisResultsSchema = z.object({ // API Request Schema export const analyzeContentRequestSchema = z.object({ url: urlSchema, + modelProvider: z.enum(['openai', 'gemini', 'deepseek']), }); // API Response Schema diff --git a/src/app/api/analyze-content/route.ts b/src/app/api/analyze-content/route.ts index 455c29d..26bfddf 100644 --- a/src/app/api/analyze-content/route.ts +++ b/src/app/api/analyze-content/route.ts @@ -19,7 +19,9 @@ import { } from '@/ai/text/utils/web-content-analyzer-config'; import { consumeCredits, hasEnoughCredits } from '@/credits/credits'; 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 { generateObject } from 'ai'; 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( content: string, - url: string + url: string, + provider: string ): Promise { return withRetry(async () => { 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({ - model: openai(webContentAnalyzerConfig.openai.model), + model, schema: analysisSchema, prompt: ` 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 - Ensure the title and description are meaningful and based on the actual content `, - temperature: webContentAnalyzerConfig.openai.temperature, - maxTokens: webContentAnalyzerConfig.openai.maxTokens, + temperature, + maxTokens, }); return { @@ -229,11 +266,9 @@ async function analyzeContent( if (error instanceof WebContentAnalyzerError) { throw error; } - // Check for specific OpenAI/AI errors if (error instanceof Error) { const message = error.message.toLowerCase(); - if (message.includes('rate limit') || message.includes('quota')) { throw new WebContentAnalyzerError( ErrorType.RATE_LIMIT, @@ -244,7 +279,6 @@ async function analyzeContent( error ); } - if (message.includes('timeout') || message.includes('aborted')) { throw new WebContentAnalyzerError( ErrorType.TIMEOUT, @@ -256,7 +290,6 @@ async function analyzeContent( ); } } - // Classify and throw the 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 const urlValidation = validateUrl(url); @@ -402,8 +436,8 @@ export async function POST(req: NextRequest) { // Step 1: Scrape webpage const { content, screenshot } = await scrapeWebpage(url); - // Step 2: Analyze content with AI - const analysis = await analyzeContent(content, url); + // Step 2: Analyze content with AI (pass provider) + const analysis = await analyzeContent(content, url, modelProvider); // Step 3: Consume credits (only on successful analysis) await consumeCredits({ diff --git a/src/components/settings/billing/credits-balance-card.tsx b/src/components/settings/billing/credits-balance-card.tsx index 9f41773..bddc3ee 100644 --- a/src/components/settings/billing/credits-balance-card.tsx +++ b/src/components/settings/billing/credits-balance-card.tsx @@ -54,11 +54,6 @@ export default function CreditsBalanceCard() { } | null>(null); const [isLoadingStats, setIsLoadingStats] = useState(true); - // Don't render if credits are disabled - if (!websiteConfig.credits.enableCredits) { - return null; - } - // Fetch credit statistics const fetchCreditStats = useCallback(async () => { console.log('fetchCreditStats, fetch start'); @@ -114,6 +109,11 @@ export default function CreditsBalanceCard() { fetchCreditStats(); }, [fetchCredits, fetchCreditStats]); + // Don't render if credits are disabled + if (!websiteConfig.credits.enableCredits) { + return null; + } + // Render loading skeleton const isPageLoading = isLoadingBalance || isLoadingStats; if (!mounted || isPageLoading) { diff --git a/src/components/settings/credits/credit-packages.tsx b/src/components/settings/credits/credit-packages.tsx index 6d983b8..9a2945a 100644 --- a/src/components/settings/credits/credit-packages.tsx +++ b/src/components/settings/credits/credit-packages.tsx @@ -23,11 +23,6 @@ import { CreditCheckoutButton } from './credit-checkout-button'; * @returns Credit packages component */ export function CreditPackages() { - // If credits are not enabled, return null - if (!websiteConfig.credits.enableCredits) { - return null; - } - const t = useTranslations('Dashboard.settings.credits.packages'); // Get current user and payment info @@ -36,6 +31,8 @@ export function CreditPackages() { // Check if user is on free plan and enableForFreePlan is false const isFreePlan = currentPlan?.isFree === true; + + // Check if user is on free plan and enableForFreePlan is false if (isFreePlan && !websiteConfig.credits.enableForFreePlan) { return null; }