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 = {
|
||||
[ErrorType.VALIDATION]: AlertCircleIcon,
|
||||
[ErrorType.NETWORK]: WifiOffIcon,
|
||||
[ErrorType.CREDITS]: CreditCardIcon,
|
||||
[ErrorType.SCRAPING]: ServerIcon,
|
||||
[ErrorType.ANALYSIS]: HelpCircleIcon,
|
||||
[ErrorType.TIMEOUT]: ClockIcon,
|
||||
@ -84,7 +83,6 @@ const severityColors = {
|
||||
const errorTitles = {
|
||||
[ErrorType.VALIDATION]: 'Invalid Input',
|
||||
[ErrorType.NETWORK]: 'Connection Error',
|
||||
[ErrorType.CREDITS]: 'Insufficient Credits',
|
||||
[ErrorType.SCRAPING]: 'Unable to Access Website',
|
||||
[ErrorType.ANALYSIS]: 'Analysis Failed',
|
||||
[ErrorType.TIMEOUT]: 'Request Timed Out',
|
||||
|
@ -1,9 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { checkWebContentAnalysisCreditsAction } from '@/actions/check-web-content-analysis-credits';
|
||||
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';
|
||||
import {
|
||||
Form,
|
||||
@ -20,21 +18,10 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { useLocalePathname } from '@/i18n/navigation';
|
||||
import { authClient } from '@/lib/auth-client';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import {
|
||||
AlertCircleIcon,
|
||||
CoinsIcon,
|
||||
LinkIcon,
|
||||
Loader2Icon,
|
||||
LogInIcon,
|
||||
SparklesIcon,
|
||||
} from 'lucide-react';
|
||||
import { useAction } from 'next-safe-action/hooks';
|
||||
import { LinkIcon, Loader2Icon, SparklesIcon } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod';
|
||||
import { useDebounce } from '../utils/performance';
|
||||
|
||||
@ -52,19 +39,9 @@ export function UrlInputForm({
|
||||
modelProvider,
|
||||
setModelProvider,
|
||||
}: UrlInputFormProps) {
|
||||
const [creditInfo, setCreditInfo] = useState<{
|
||||
hasEnoughCredits: boolean;
|
||||
currentCredits: number;
|
||||
requiredCredits: number;
|
||||
} | null>(null);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
// Get authentication status and current path for callback
|
||||
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
|
||||
// Prevent hydration mismatch by only rendering content after mount
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
@ -84,42 +61,6 @@ export function UrlInputForm({
|
||||
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
|
||||
useEffect(() => {
|
||||
if (debouncedUrl && debouncedUrl !== urlValue) {
|
||||
@ -129,23 +70,12 @@ export function UrlInputForm({
|
||||
}, [debouncedUrl, urlValue, form]);
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
const handleFormSubmit = form.handleSubmit(handleSubmit);
|
||||
|
||||
const isInsufficientCredits = creditInfo && !creditInfo.hasEnoughCredits;
|
||||
const isFormDisabled = isLoading || disabled || !!isInsufficientCredits;
|
||||
const isFormDisabled = isLoading || disabled;
|
||||
|
||||
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 ? (
|
||||
// Show loading state during hydration to prevent mismatch
|
||||
<Button type="button" disabled className="w-full" size="lg">
|
||||
<Loader2Icon className="size-4 animate-spin" />
|
||||
<span>Loading...</span>
|
||||
</Button>
|
||||
) : isAuthenticated ? (
|
||||
) : (
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isFormDisabled || !urlValue?.trim()}
|
||||
className="w-full"
|
||||
size="lg"
|
||||
>
|
||||
{isAuthLoading ? (
|
||||
<>
|
||||
<Loader2Icon className="size-4 animate-spin" />
|
||||
<span>Loading...</span>
|
||||
</>
|
||||
) : isCheckingCredits ? (
|
||||
<>
|
||||
<Loader2Icon className="size-4 animate-spin" />
|
||||
<span>Checking Credits...</span>
|
||||
</>
|
||||
) : isLoading ? (
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Loader2Icon className="size-4 animate-spin" />
|
||||
<span>Analyzing...</span>
|
||||
@ -262,24 +145,10 @@ export function UrlInputForm({
|
||||
) : (
|
||||
<>
|
||||
<SparklesIcon className="size-4" />
|
||||
<span>
|
||||
Analyze Website
|
||||
{creditInfo && ` (${creditInfo.requiredCredits} credits)`}
|
||||
</span>
|
||||
<span>Analyze Website</span>
|
||||
</>
|
||||
)}
|
||||
</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>
|
||||
|
@ -232,16 +232,6 @@ export function WebContentAnalyzer({ className }: WebContentAnalyzerProps) {
|
||||
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;
|
||||
|
@ -9,7 +9,6 @@ import { webContentAnalyzerConfig } from '@/ai/text/utils/web-content-analyzer-c
|
||||
export enum ErrorType {
|
||||
VALIDATION = 'validation',
|
||||
NETWORK = 'network',
|
||||
CREDITS = 'credits',
|
||||
SCRAPING = 'scraping',
|
||||
ANALYSIS = 'analysis',
|
||||
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
|
||||
if (
|
||||
message.includes('scrape') ||
|
||||
@ -278,16 +261,6 @@ export function getRecoveryActions(error: WebContentAnalyzerError): Array<{
|
||||
{ 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:
|
||||
return [
|
||||
{ label: 'Try Again', action: 'retry', primary: true },
|
||||
|
@ -6,11 +6,6 @@
|
||||
*/
|
||||
|
||||
export const webContentAnalyzerConfig = {
|
||||
/**
|
||||
* Credit cost for performing a web content analysis
|
||||
*/
|
||||
creditsCost: 100,
|
||||
|
||||
/**
|
||||
* Maximum content length for AI analysis (in characters)
|
||||
* Optimized to prevent token limit issues while maintaining quality
|
||||
@ -118,21 +113,14 @@ export const webContentAnalyzerConfig = {
|
||||
maxTokens: 2000,
|
||||
},
|
||||
openrouter: {
|
||||
model: 'openrouter/horizon-beta',
|
||||
// model: 'openrouter/horizon-beta',
|
||||
// model: 'x-ai/grok-3-beta',
|
||||
// model: 'openai/gpt-4o-mini',
|
||||
model: 'openai/gpt-4o-mini',
|
||||
temperature: 0.1,
|
||||
maxTokens: 2000,
|
||||
},
|
||||
} 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
|
||||
*/
|
||||
@ -151,8 +139,6 @@ export function validateFirecrawlConfig(): boolean {
|
||||
*/
|
||||
export function validateWebContentAnalyzerConfig(): boolean {
|
||||
return (
|
||||
typeof webContentAnalyzerConfig.creditsCost === 'number' &&
|
||||
webContentAnalyzerConfig.creditsCost > 0 &&
|
||||
typeof webContentAnalyzerConfig.maxContentLength === 'number' &&
|
||||
webContentAnalyzerConfig.maxContentLength > 0 &&
|
||||
typeof webContentAnalyzerConfig.timeoutMillis === 'number' &&
|
||||
|
@ -13,12 +13,9 @@ import {
|
||||
validateUrl,
|
||||
} from '@/ai/text/utils/web-content-analyzer';
|
||||
import {
|
||||
getWebContentAnalysisCost,
|
||||
validateFirecrawlConfig,
|
||||
webContentAnalyzerConfig,
|
||||
} 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 { createGoogleGenerativeAI } from '@ai-sdk/google';
|
||||
import { createOpenAI } from '@ai-sdk/openai';
|
||||
@ -30,7 +27,6 @@ import { z } from 'zod';
|
||||
|
||||
// Constants from configuration
|
||||
const TIMEOUT_MILLIS = webContentAnalyzerConfig.timeoutMillis;
|
||||
const CREDITS_COST = getWebContentAnalysisCost();
|
||||
const MAX_CONTENT_LENGTH = webContentAnalyzerConfig.maxContentLength;
|
||||
|
||||
// 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
|
||||
if (!validateFirecrawlConfig()) {
|
||||
const configError = new WebContentAnalyzerError(
|
||||
@ -404,39 +378,7 @@ export async function POST(req: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
// Check if user has sufficient credits before starting analysis
|
||||
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}]`
|
||||
);
|
||||
console.log(`Starting analysis [requestId=${requestId}, url=${url}]`);
|
||||
|
||||
// Perform analysis with timeout and enhanced error handling
|
||||
const analysisPromise = (async () => {
|
||||
@ -447,13 +389,6 @@ export async function POST(req: NextRequest) {
|
||||
// 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({
|
||||
userId: session.user.id,
|
||||
amount: CREDITS_COST,
|
||||
description: `Web content analysis: ${url}`,
|
||||
});
|
||||
|
||||
return { analysis, screenshot };
|
||||
} catch (error) {
|
||||
// If it's already a WebContentAnalyzerError, just re-throw
|
||||
@ -477,7 +412,6 @@ export async function POST(req: NextRequest) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: result,
|
||||
creditsConsumed: CREDITS_COST,
|
||||
} satisfies AnalyzeContentResponse);
|
||||
} catch (error) {
|
||||
const elapsed = ((performance.now() - startTime) / 1000).toFixed(1);
|
||||
@ -499,12 +433,6 @@ export async function POST(req: NextRequest) {
|
||||
case ErrorType.VALIDATION:
|
||||
statusCode = 400;
|
||||
break;
|
||||
case ErrorType.AUTHENTICATION:
|
||||
statusCode = 401;
|
||||
break;
|
||||
case ErrorType.CREDITS:
|
||||
statusCode = 402;
|
||||
break;
|
||||
case ErrorType.TIMEOUT:
|
||||
statusCode = 408;
|
||||
break;
|
||||
|
Loading…
Reference in New Issue
Block a user