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 = { 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',

View File

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

View File

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

View File

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

View File

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

View File

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