refactor: remove credit-related functionality and components from the web content analysis module

This commit is contained in:
javayhu 2025-08-25 09:48:20 +08:00
parent 31829ce17b
commit 80851fcf44
7 changed files with 9 additions and 302 deletions

View File

@ -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',
};
}
}
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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