refactor: remove credit-related functionality and components from the web content analysis module
This commit is contained in:
parent
31829ce17b
commit
80851fcf44
@ -1,37 +0,0 @@
|
|||||||
'use server';
|
|
||||||
|
|
||||||
import { getWebContentAnalysisCost } from '@/ai/text/utils/web-content-analyzer-config';
|
|
||||||
import { getUserCredits, hasEnoughCredits } from '@/credits/credits';
|
|
||||||
import type { User } from '@/lib/auth-types';
|
|
||||||
import { userActionClient } from '@/lib/safe-action';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if user has enough credits for web content analysis
|
|
||||||
*/
|
|
||||||
export const checkWebContentAnalysisCreditsAction = userActionClient.action(
|
|
||||||
async ({ ctx }) => {
|
|
||||||
const currentUser = (ctx as { user: User }).user;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const requiredCredits = getWebContentAnalysisCost();
|
|
||||||
const currentCredits = await getUserCredits(currentUser.id);
|
|
||||||
const hasCredits = await hasEnoughCredits({
|
|
||||||
userId: currentUser.id,
|
|
||||||
requiredCredits,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
hasEnoughCredits: hasCredits,
|
|
||||||
currentCredits,
|
|
||||||
requiredCredits,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error('check web content analysis credits error:', error);
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : 'Something went wrong',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
@ -34,7 +34,6 @@ interface ErrorDisplayProps {
|
|||||||
const errorIcons = {
|
const errorIcons = {
|
||||||
[ErrorType.VALIDATION]: AlertCircleIcon,
|
[ErrorType.VALIDATION]: AlertCircleIcon,
|
||||||
[ErrorType.NETWORK]: WifiOffIcon,
|
[ErrorType.NETWORK]: WifiOffIcon,
|
||||||
[ErrorType.CREDITS]: CreditCardIcon,
|
|
||||||
[ErrorType.SCRAPING]: ServerIcon,
|
[ErrorType.SCRAPING]: ServerIcon,
|
||||||
[ErrorType.ANALYSIS]: HelpCircleIcon,
|
[ErrorType.ANALYSIS]: HelpCircleIcon,
|
||||||
[ErrorType.TIMEOUT]: ClockIcon,
|
[ErrorType.TIMEOUT]: ClockIcon,
|
||||||
@ -84,7 +83,6 @@ const severityColors = {
|
|||||||
const errorTitles = {
|
const errorTitles = {
|
||||||
[ErrorType.VALIDATION]: 'Invalid Input',
|
[ErrorType.VALIDATION]: 'Invalid Input',
|
||||||
[ErrorType.NETWORK]: 'Connection Error',
|
[ErrorType.NETWORK]: 'Connection Error',
|
||||||
[ErrorType.CREDITS]: 'Insufficient Credits',
|
|
||||||
[ErrorType.SCRAPING]: 'Unable to Access Website',
|
[ErrorType.SCRAPING]: 'Unable to Access Website',
|
||||||
[ErrorType.ANALYSIS]: 'Analysis Failed',
|
[ErrorType.ANALYSIS]: 'Analysis Failed',
|
||||||
[ErrorType.TIMEOUT]: 'Request Timed Out',
|
[ErrorType.TIMEOUT]: 'Request Timed Out',
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { checkWebContentAnalysisCreditsAction } from '@/actions/check-web-content-analysis-credits';
|
|
||||||
import type { UrlInputFormProps } 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 { webContentAnalyzerConfig } from '@/ai/text/utils/web-content-analyzer-config';
|
||||||
import { LoginWrapper } from '@/components/auth/login-wrapper';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
@ -20,21 +18,10 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { useLocalePathname } from '@/i18n/navigation';
|
|
||||||
import { authClient } from '@/lib/auth-client';
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import {
|
import { LinkIcon, Loader2Icon, SparklesIcon } from 'lucide-react';
|
||||||
AlertCircleIcon,
|
|
||||||
CoinsIcon,
|
|
||||||
LinkIcon,
|
|
||||||
Loader2Icon,
|
|
||||||
LogInIcon,
|
|
||||||
SparklesIcon,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { useAction } from 'next-safe-action/hooks';
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { toast } from 'sonner';
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { useDebounce } from '../utils/performance';
|
import { useDebounce } from '../utils/performance';
|
||||||
|
|
||||||
@ -52,19 +39,9 @@ export function UrlInputForm({
|
|||||||
modelProvider,
|
modelProvider,
|
||||||
setModelProvider,
|
setModelProvider,
|
||||||
}: UrlInputFormProps) {
|
}: UrlInputFormProps) {
|
||||||
const [creditInfo, setCreditInfo] = useState<{
|
|
||||||
hasEnoughCredits: boolean;
|
|
||||||
currentCredits: number;
|
|
||||||
requiredCredits: number;
|
|
||||||
} | null>(null);
|
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
// Get authentication status and current path for callback
|
// Prevent hydration mismatch by only rendering content after mount
|
||||||
const { data: session, isPending: isAuthLoading } = authClient.useSession();
|
|
||||||
const isAuthenticated = !!session?.user;
|
|
||||||
const currentPath = useLocalePathname();
|
|
||||||
|
|
||||||
// Prevent hydration mismatch by only rendering auth-dependent content after mount
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
}, []);
|
}, []);
|
||||||
@ -84,42 +61,6 @@ export function UrlInputForm({
|
|||||||
webContentAnalyzerConfig.performance.urlInputDebounceMs
|
webContentAnalyzerConfig.performance.urlInputDebounceMs
|
||||||
);
|
);
|
||||||
|
|
||||||
const { execute: checkCredits, isExecuting: isCheckingCredits } = useAction(
|
|
||||||
checkWebContentAnalysisCreditsAction,
|
|
||||||
{
|
|
||||||
onSuccess: (result) => {
|
|
||||||
if (result.data?.success) {
|
|
||||||
setCreditInfo({
|
|
||||||
hasEnoughCredits: result.data.hasEnoughCredits ?? false,
|
|
||||||
currentCredits: result.data.currentCredits ?? 0,
|
|
||||||
requiredCredits: result.data.requiredCredits ?? 0,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Only show error toast if it's not an auth error
|
|
||||||
if (result.data?.error !== 'Unauthorized') {
|
|
||||||
setTimeout(() => {
|
|
||||||
toast.error(result.data?.error || 'Failed to check credits');
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
console.error('Credit check error:', error);
|
|
||||||
// Only show error toast for non-auth errors
|
|
||||||
setTimeout(() => {
|
|
||||||
toast.error('Failed to check credits');
|
|
||||||
}, 0);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check credits only when user is authenticated
|
|
||||||
useEffect(() => {
|
|
||||||
if (isAuthenticated && !isAuthLoading) {
|
|
||||||
checkCredits();
|
|
||||||
}
|
|
||||||
}, [isAuthenticated, isAuthLoading, checkCredits]);
|
|
||||||
|
|
||||||
// Debounced URL validation effect
|
// Debounced URL validation effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (debouncedUrl && debouncedUrl !== urlValue) {
|
if (debouncedUrl && debouncedUrl !== urlValue) {
|
||||||
@ -129,23 +70,12 @@ export function UrlInputForm({
|
|||||||
}, [debouncedUrl, urlValue, form]);
|
}, [debouncedUrl, urlValue, form]);
|
||||||
|
|
||||||
const handleSubmit = (data: UrlFormData) => {
|
const handleSubmit = (data: UrlFormData) => {
|
||||||
// For authenticated users, check credits before submitting
|
|
||||||
if (creditInfo && !creditInfo.hasEnoughCredits) {
|
|
||||||
// Defer toast to avoid flushSync during render
|
|
||||||
setTimeout(() => {
|
|
||||||
toast.error(
|
|
||||||
`Insufficient credits. You need ${creditInfo.requiredCredits} credits but only have ${creditInfo.currentCredits}.`
|
|
||||||
);
|
|
||||||
}, 0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onSubmit(data.url ?? '', modelProvider);
|
onSubmit(data.url ?? '', modelProvider);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFormSubmit = form.handleSubmit(handleSubmit);
|
const handleFormSubmit = form.handleSubmit(handleSubmit);
|
||||||
|
|
||||||
const isInsufficientCredits = creditInfo && !creditInfo.hasEnoughCredits;
|
const isFormDisabled = isLoading || disabled;
|
||||||
const isFormDisabled = isLoading || disabled || !!isInsufficientCredits;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -194,67 +124,20 @@ export function UrlInputForm({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Credit Information - Only show for authenticated users */}
|
|
||||||
{isAuthenticated && creditInfo && (
|
|
||||||
<div className="flex items-center justify-between p-3 bg-muted/50 rounded-lg text-sm">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<CoinsIcon className="size-4 text-muted-foreground" />
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
Cost: {creditInfo.requiredCredits} credits
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span
|
|
||||||
className={
|
|
||||||
creditInfo.hasEnoughCredits
|
|
||||||
? 'text-green-600 dark:text-green-400'
|
|
||||||
: 'text-red-600 dark:text-red-400'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Balance: {creditInfo.currentCredits}
|
|
||||||
</span>
|
|
||||||
{!creditInfo.hasEnoughCredits && (
|
|
||||||
<AlertCircleIcon className="size-4 text-red-600 dark:text-red-400" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Insufficient Credits Warning */}
|
|
||||||
{isAuthenticated && isInsufficientCredits && (
|
|
||||||
<div className="flex items-center gap-2 p-3 bg-red-50 dark:bg-red-950/20 border border-red-200 dark:border-red-800 rounded-lg text-sm text-red-700 dark:text-red-400">
|
|
||||||
<AlertCircleIcon className="size-4 flex-shrink-0" />
|
|
||||||
<span>
|
|
||||||
Insufficient credits. You need {creditInfo.requiredCredits}{' '}
|
|
||||||
credits but only have {creditInfo.currentCredits}.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!mounted ? (
|
{!mounted ? (
|
||||||
// Show loading state during hydration to prevent mismatch
|
// Show loading state during hydration to prevent mismatch
|
||||||
<Button type="button" disabled className="w-full" size="lg">
|
<Button type="button" disabled className="w-full" size="lg">
|
||||||
<Loader2Icon className="size-4 animate-spin" />
|
<Loader2Icon className="size-4 animate-spin" />
|
||||||
<span>Loading...</span>
|
<span>Loading...</span>
|
||||||
</Button>
|
</Button>
|
||||||
) : 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"
|
||||||
>
|
>
|
||||||
{isAuthLoading ? (
|
{isLoading ? (
|
||||||
<>
|
|
||||||
<Loader2Icon className="size-4 animate-spin" />
|
|
||||||
<span>Loading...</span>
|
|
||||||
</>
|
|
||||||
) : isCheckingCredits ? (
|
|
||||||
<>
|
|
||||||
<Loader2Icon className="size-4 animate-spin" />
|
|
||||||
<span>Checking Credits...</span>
|
|
||||||
</>
|
|
||||||
) : isLoading ? (
|
|
||||||
<>
|
<>
|
||||||
<Loader2Icon className="size-4 animate-spin" />
|
<Loader2Icon className="size-4 animate-spin" />
|
||||||
<span>Analyzing...</span>
|
<span>Analyzing...</span>
|
||||||
@ -262,24 +145,10 @@ export function UrlInputForm({
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<SparklesIcon className="size-4" />
|
<SparklesIcon className="size-4" />
|
||||||
<span>
|
<span>Analyze Website</span>
|
||||||
Analyze Website
|
|
||||||
{creditInfo && ` (${creditInfo.requiredCredits} credits)`}
|
|
||||||
</span>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
|
||||||
<LoginWrapper mode="modal" asChild callbackUrl={currentPath}>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
className="w-full cursor-pointer"
|
|
||||||
size="lg"
|
|
||||||
>
|
|
||||||
<LogInIcon className="size-4" />
|
|
||||||
<span>Sign In First</span>
|
|
||||||
</Button>
|
|
||||||
</LoginWrapper>
|
|
||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
@ -232,16 +232,6 @@ export function WebContentAnalyzer({ className }: WebContentAnalyzerProps) {
|
|||||||
errorType = ErrorType.VALIDATION;
|
errorType = ErrorType.VALIDATION;
|
||||||
retryable = false;
|
retryable = false;
|
||||||
break;
|
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:
|
case 408:
|
||||||
errorType = ErrorType.TIMEOUT;
|
errorType = ErrorType.TIMEOUT;
|
||||||
break;
|
break;
|
||||||
|
@ -9,7 +9,6 @@ import { webContentAnalyzerConfig } from '@/ai/text/utils/web-content-analyzer-c
|
|||||||
export enum ErrorType {
|
export enum ErrorType {
|
||||||
VALIDATION = 'validation',
|
VALIDATION = 'validation',
|
||||||
NETWORK = 'network',
|
NETWORK = 'network',
|
||||||
CREDITS = 'credits',
|
|
||||||
SCRAPING = 'scraping',
|
SCRAPING = 'scraping',
|
||||||
ANALYSIS = 'analysis',
|
ANALYSIS = 'analysis',
|
||||||
TIMEOUT = 'timeout',
|
TIMEOUT = 'timeout',
|
||||||
@ -96,22 +95,6 @@ export function classifyError(error: unknown): WebContentAnalyzerError {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Credit errors
|
|
||||||
if (
|
|
||||||
message.includes('credit') ||
|
|
||||||
message.includes('insufficient') ||
|
|
||||||
message.includes('balance')
|
|
||||||
) {
|
|
||||||
return new WebContentAnalyzerError(
|
|
||||||
ErrorType.CREDITS,
|
|
||||||
error.message,
|
|
||||||
'Insufficient credits to perform analysis. Please purchase more credits.',
|
|
||||||
ErrorSeverity.HIGH,
|
|
||||||
false,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scraping errors
|
// Scraping errors
|
||||||
if (
|
if (
|
||||||
message.includes('scrape') ||
|
message.includes('scrape') ||
|
||||||
@ -278,16 +261,6 @@ export function getRecoveryActions(error: WebContentAnalyzerError): Array<{
|
|||||||
{ label: 'Try Simpler URL', action: 'simplify_url' },
|
{ label: 'Try Simpler URL', action: 'simplify_url' },
|
||||||
];
|
];
|
||||||
|
|
||||||
case ErrorType.CREDITS:
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
label: 'Purchase Credits',
|
|
||||||
action: 'purchase_credits',
|
|
||||||
primary: true,
|
|
||||||
},
|
|
||||||
{ label: 'Check Balance', action: 'check_balance' },
|
|
||||||
];
|
|
||||||
|
|
||||||
case ErrorType.SCRAPING:
|
case ErrorType.SCRAPING:
|
||||||
return [
|
return [
|
||||||
{ label: 'Try Again', action: 'retry', primary: true },
|
{ label: 'Try Again', action: 'retry', primary: true },
|
||||||
|
@ -6,11 +6,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export const webContentAnalyzerConfig = {
|
export const webContentAnalyzerConfig = {
|
||||||
/**
|
|
||||||
* Credit cost for performing a web content analysis
|
|
||||||
*/
|
|
||||||
creditsCost: 100,
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maximum content length for AI analysis (in characters)
|
* Maximum content length for AI analysis (in characters)
|
||||||
* Optimized to prevent token limit issues while maintaining quality
|
* Optimized to prevent token limit issues while maintaining quality
|
||||||
@ -118,21 +113,14 @@ export const webContentAnalyzerConfig = {
|
|||||||
maxTokens: 2000,
|
maxTokens: 2000,
|
||||||
},
|
},
|
||||||
openrouter: {
|
openrouter: {
|
||||||
model: 'openrouter/horizon-beta',
|
// model: 'openrouter/horizon-beta',
|
||||||
// model: 'x-ai/grok-3-beta',
|
// model: 'x-ai/grok-3-beta',
|
||||||
// model: 'openai/gpt-4o-mini',
|
model: 'openai/gpt-4o-mini',
|
||||||
temperature: 0.1,
|
temperature: 0.1,
|
||||||
maxTokens: 2000,
|
maxTokens: 2000,
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the credit cost for web content analysis
|
|
||||||
*/
|
|
||||||
export function getWebContentAnalysisCost(): number {
|
|
||||||
return webContentAnalyzerConfig.creditsCost;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates if the Firecrawl API key is configured
|
* Validates if the Firecrawl API key is configured
|
||||||
*/
|
*/
|
||||||
@ -151,8 +139,6 @@ export function validateFirecrawlConfig(): boolean {
|
|||||||
*/
|
*/
|
||||||
export function validateWebContentAnalyzerConfig(): boolean {
|
export function validateWebContentAnalyzerConfig(): boolean {
|
||||||
return (
|
return (
|
||||||
typeof webContentAnalyzerConfig.creditsCost === 'number' &&
|
|
||||||
webContentAnalyzerConfig.creditsCost > 0 &&
|
|
||||||
typeof webContentAnalyzerConfig.maxContentLength === 'number' &&
|
typeof webContentAnalyzerConfig.maxContentLength === 'number' &&
|
||||||
webContentAnalyzerConfig.maxContentLength > 0 &&
|
webContentAnalyzerConfig.maxContentLength > 0 &&
|
||||||
typeof webContentAnalyzerConfig.timeoutMillis === 'number' &&
|
typeof webContentAnalyzerConfig.timeoutMillis === 'number' &&
|
||||||
|
@ -13,12 +13,9 @@ import {
|
|||||||
validateUrl,
|
validateUrl,
|
||||||
} from '@/ai/text/utils/web-content-analyzer';
|
} from '@/ai/text/utils/web-content-analyzer';
|
||||||
import {
|
import {
|
||||||
getWebContentAnalysisCost,
|
|
||||||
validateFirecrawlConfig,
|
validateFirecrawlConfig,
|
||||||
webContentAnalyzerConfig,
|
webContentAnalyzerConfig,
|
||||||
} from '@/ai/text/utils/web-content-analyzer-config';
|
} from '@/ai/text/utils/web-content-analyzer-config';
|
||||||
import { consumeCredits, hasEnoughCredits } from '@/credits/credits';
|
|
||||||
import { getSession } from '@/lib/server';
|
|
||||||
import { createDeepSeek } from '@ai-sdk/deepseek';
|
import { createDeepSeek } from '@ai-sdk/deepseek';
|
||||||
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
||||||
import { createOpenAI } from '@ai-sdk/openai';
|
import { createOpenAI } from '@ai-sdk/openai';
|
||||||
@ -30,7 +27,6 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
// Constants from configuration
|
// Constants from configuration
|
||||||
const TIMEOUT_MILLIS = webContentAnalyzerConfig.timeoutMillis;
|
const TIMEOUT_MILLIS = webContentAnalyzerConfig.timeoutMillis;
|
||||||
const CREDITS_COST = getWebContentAnalysisCost();
|
|
||||||
const MAX_CONTENT_LENGTH = webContentAnalyzerConfig.maxContentLength;
|
const MAX_CONTENT_LENGTH = webContentAnalyzerConfig.maxContentLength;
|
||||||
|
|
||||||
// Initialize Firecrawl client
|
// Initialize Firecrawl client
|
||||||
@ -361,28 +357,6 @@ export async function POST(req: NextRequest) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check authentication
|
|
||||||
const session = await getSession();
|
|
||||||
if (!session) {
|
|
||||||
const authError = new WebContentAnalyzerError(
|
|
||||||
ErrorType.AUTHENTICATION,
|
|
||||||
'Authentication required',
|
|
||||||
'Please sign in to analyze web content.',
|
|
||||||
ErrorSeverity.HIGH,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
logError(authError, { requestId });
|
|
||||||
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
success: false,
|
|
||||||
error: authError.userMessage,
|
|
||||||
} satisfies AnalyzeContentResponse,
|
|
||||||
{ status: 401 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if Firecrawl is configured
|
// Check if Firecrawl is configured
|
||||||
if (!validateFirecrawlConfig()) {
|
if (!validateFirecrawlConfig()) {
|
||||||
const configError = new WebContentAnalyzerError(
|
const configError = new WebContentAnalyzerError(
|
||||||
@ -404,39 +378,7 @@ export async function POST(req: NextRequest) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user has sufficient credits before starting analysis
|
console.log(`Starting analysis [requestId=${requestId}, url=${url}]`);
|
||||||
const hasCredits = await hasEnoughCredits({
|
|
||||||
userId: session.user.id,
|
|
||||||
requiredCredits: CREDITS_COST,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!hasCredits) {
|
|
||||||
const creditError = new WebContentAnalyzerError(
|
|
||||||
ErrorType.CREDITS,
|
|
||||||
'Insufficient credits to perform analysis',
|
|
||||||
"You don't have enough credits to analyze this webpage. Please purchase more credits.",
|
|
||||||
ErrorSeverity.HIGH,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
logError(creditError, {
|
|
||||||
requestId,
|
|
||||||
userId: session.user.id,
|
|
||||||
requiredCredits: CREDITS_COST,
|
|
||||||
});
|
|
||||||
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
success: false,
|
|
||||||
error: creditError.userMessage,
|
|
||||||
} satisfies AnalyzeContentResponse,
|
|
||||||
{ status: 402 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`Starting analysis [requestId=${requestId}, url=${url}, userId=${session.user.id}]`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Perform analysis with timeout and enhanced error handling
|
// Perform analysis with timeout and enhanced error handling
|
||||||
const analysisPromise = (async () => {
|
const analysisPromise = (async () => {
|
||||||
@ -447,13 +389,6 @@ export async function POST(req: NextRequest) {
|
|||||||
// Step 2: Analyze content with AI (pass provider)
|
// Step 2: Analyze content with AI (pass provider)
|
||||||
const analysis = await analyzeContent(content, url, modelProvider);
|
const analysis = await analyzeContent(content, url, modelProvider);
|
||||||
|
|
||||||
// Step 3: Consume credits (only on successful analysis)
|
|
||||||
await consumeCredits({
|
|
||||||
userId: session.user.id,
|
|
||||||
amount: CREDITS_COST,
|
|
||||||
description: `Web content analysis: ${url}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
return { analysis, screenshot };
|
return { analysis, screenshot };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If it's already a WebContentAnalyzerError, just re-throw
|
// If it's already a WebContentAnalyzerError, just re-throw
|
||||||
@ -477,7 +412,6 @@ export async function POST(req: NextRequest) {
|
|||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: result,
|
data: result,
|
||||||
creditsConsumed: CREDITS_COST,
|
|
||||||
} satisfies AnalyzeContentResponse);
|
} satisfies AnalyzeContentResponse);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const elapsed = ((performance.now() - startTime) / 1000).toFixed(1);
|
const elapsed = ((performance.now() - startTime) / 1000).toFixed(1);
|
||||||
@ -499,12 +433,6 @@ export async function POST(req: NextRequest) {
|
|||||||
case ErrorType.VALIDATION:
|
case ErrorType.VALIDATION:
|
||||||
statusCode = 400;
|
statusCode = 400;
|
||||||
break;
|
break;
|
||||||
case ErrorType.AUTHENTICATION:
|
|
||||||
statusCode = 401;
|
|
||||||
break;
|
|
||||||
case ErrorType.CREDITS:
|
|
||||||
statusCode = 402;
|
|
||||||
break;
|
|
||||||
case ErrorType.TIMEOUT:
|
case ErrorType.TIMEOUT:
|
||||||
statusCode = 408;
|
statusCode = 408;
|
||||||
break;
|
break;
|
||||||
|
Loading…
Reference in New Issue
Block a user