chore: support google gemini and deepseek in ai text demo

This commit is contained in:
javayhu 2025-07-23 00:26:58 +08:00
parent 4384a1d43f
commit 971b0d65a0
8 changed files with 283 additions and 189 deletions

View File

@ -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)

View File

@ -24,8 +24,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",

81
pnpm-lock.yaml generated
View File

@ -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)
@ -381,6 +387,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'}
@ -405,12 +417,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'}
@ -593,28 +617,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==}
@ -1487,79 +1507,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==}
@ -1666,56 +1674,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==}
@ -2267,37 +2267,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==}
@ -3917,28 +3911,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==}
@ -5264,28 +5254,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==}
@ -6622,6 +6608,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
@ -6653,12 +6646,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

View File

@ -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<typeof urlFormSchema>;
@ -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 (
<>
<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 onSubmit={handleFormSubmit} className="space-y-4">
<FormField
@ -216,7 +239,7 @@ export function UrlInputForm({
) : isAuthenticated ? (
<Button
type="submit"
disabled={isFormDisabled || !urlValue.trim()}
disabled={isFormDisabled || !urlValue?.trim()}
className="w-full"
size="lg"
>

View File

@ -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<ModelProvider>('openai');
// Enhanced error state
const [analyzedError, setAnalyzedError] =
useState<WebContentAnalyzerError | null>(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}
/>
)}

View File

@ -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;
/**

View File

@ -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

View File

@ -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<AnalysisResults> {
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({