From 7c9b0a26972baf9ffbd1926c8323ad15e46496a9 Mon Sep 17 00:00:00 2001 From: javayhu Date: Mon, 25 Aug 2025 09:22:56 +0800 Subject: [PATCH 01/11] refactor: remove ConsumeCreditCard component and integrate its functionality into ConsumeCreditsCard --- .../text/components/consume-credit-card.tsx | 57 ------------------- src/ai/text/components/index.ts | 1 - .../(marketing)/(pages)/test/page.tsx | 4 +- src/components/test/credits-test.tsx | 24 +++++--- 4 files changed, 19 insertions(+), 67 deletions(-) delete mode 100644 src/ai/text/components/consume-credit-card.tsx diff --git a/src/ai/text/components/consume-credit-card.tsx b/src/ai/text/components/consume-credit-card.tsx deleted file mode 100644 index ea4f828..0000000 --- a/src/ai/text/components/consume-credit-card.tsx +++ /dev/null @@ -1,57 +0,0 @@ -'use client'; - -import { CreditsBalanceButton } from '@/components/layout/credits-balance-button'; -import { Button } from '@/components/ui/button'; -import { useConsumeCredits, useCreditBalance } from '@/hooks/use-credits'; -import { CoinsIcon } from 'lucide-react'; -import { useState } from 'react'; -import { toast } from 'sonner'; - -const CONSUME_CREDITS = 50; - -export function ConsumeCreditCard() { - const { data: balance = 0, isLoading: isLoadingBalance } = useCreditBalance(); - const consumeCreditsMutation = useConsumeCredits(); - const [loading, setLoading] = useState(false); - - const hasEnoughCredits = (amount: number) => balance >= amount; - - const handleConsume = async () => { - if (!hasEnoughCredits(CONSUME_CREDITS)) { - toast.error('Insufficient credits, please buy more credits.'); - return; - } - setLoading(true); - try { - await consumeCreditsMutation.mutateAsync({ - amount: CONSUME_CREDITS, - description: `AI Text Credit Consumption (${CONSUME_CREDITS} credits)`, - }); - toast.success(`${CONSUME_CREDITS} credits have been consumed.`); - } catch (error) { - toast.error('Failed to consume credits, please try again later.'); - } finally { - setLoading(false); - } - }; - - return ( -
-
- -
- -
- ); -} diff --git a/src/ai/text/components/index.ts b/src/ai/text/components/index.ts index d0cea3d..de8876e 100644 --- a/src/ai/text/components/index.ts +++ b/src/ai/text/components/index.ts @@ -1,5 +1,4 @@ export { AnalysisResults } from './analysis-results'; -export { ConsumeCreditCard } from './consume-credit-card'; export { LoadingStates } from './loading-states'; export { UrlInputForm } from './url-input-form'; export { WebContentAnalyzer } from './web-content-analyzer'; diff --git a/src/app/[locale]/(marketing)/(pages)/test/page.tsx b/src/app/[locale]/(marketing)/(pages)/test/page.tsx index 8d5a2a6..a5bc1d4 100644 --- a/src/app/[locale]/(marketing)/(pages)/test/page.tsx +++ b/src/app/[locale]/(marketing)/(pages)/test/page.tsx @@ -1,12 +1,12 @@ import Container from '@/components/layout/container'; -import { CreditsTest } from '@/components/test/credits-test'; +import { ConsumeCreditsCard } from '@/components/test/credits-test'; export default async function TestPage() { return (
{/* credits test */} - +
); diff --git a/src/components/test/credits-test.tsx b/src/components/test/credits-test.tsx index f3bf510..129588f 100644 --- a/src/components/test/credits-test.tsx +++ b/src/components/test/credits-test.tsx @@ -6,19 +6,27 @@ import { CoinsIcon } from 'lucide-react'; import { useState } from 'react'; import { toast } from 'sonner'; -export function CreditsTest() { - const { data: balance = 0, isLoading } = useCreditBalance(); +const CONSUME_CREDITS = 10; + +export function ConsumeCreditsCard() { + const { data: balance = 0, isLoading: isLoadingBalance } = useCreditBalance(); const consumeCreditsMutation = useConsumeCredits(); const [loading, setLoading] = useState(false); + const hasEnoughCredits = (amount: number) => balance >= amount; + const handleConsume = async () => { + if (!hasEnoughCredits(CONSUME_CREDITS)) { + toast.error('Insufficient credits, please buy more credits.'); + return; + } setLoading(true); try { await consumeCreditsMutation.mutateAsync({ - amount: 10, - description: 'Test credit consumption', + amount: CONSUME_CREDITS, + description: `Test credit consumption (${CONSUME_CREDITS} credits)`, }); - toast.success('10 credits consumed successfully!'); + toast.success(`${CONSUME_CREDITS} credits consumed successfully!`); } catch (error) { toast.error('Failed to consume credits'); } finally { @@ -39,11 +47,13 @@ export function CreditsTest() {
From 31829ce17b8f867e659815f58277f6821ecf9233 Mon Sep 17 00:00:00 2001 From: javayhu Date: Mon, 25 Aug 2025 09:23:27 +0800 Subject: [PATCH 02/11] feat: add ConsumeCreditsCard component for credit consumption functionality --- src/app/[locale]/(marketing)/(pages)/test/page.tsx | 2 +- .../test/{credits-test.tsx => consume-credits-card.tsx} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/components/test/{credits-test.tsx => consume-credits-card.tsx} (100%) diff --git a/src/app/[locale]/(marketing)/(pages)/test/page.tsx b/src/app/[locale]/(marketing)/(pages)/test/page.tsx index a5bc1d4..abea424 100644 --- a/src/app/[locale]/(marketing)/(pages)/test/page.tsx +++ b/src/app/[locale]/(marketing)/(pages)/test/page.tsx @@ -1,5 +1,5 @@ import Container from '@/components/layout/container'; -import { ConsumeCreditsCard } from '@/components/test/credits-test'; +import { ConsumeCreditsCard } from '@/components/test/consume-credits-card'; export default async function TestPage() { return ( diff --git a/src/components/test/credits-test.tsx b/src/components/test/consume-credits-card.tsx similarity index 100% rename from src/components/test/credits-test.tsx rename to src/components/test/consume-credits-card.tsx From 80851fcf44dc4ea6a07394f48c929e16f37f05e9 Mon Sep 17 00:00:00 2001 From: javayhu Date: Mon, 25 Aug 2025 09:48:20 +0800 Subject: [PATCH 03/11] refactor: remove credit-related functionality and components from the web content analysis module --- .../check-web-content-analysis-credits.ts | 37 ----- src/ai/text/components/error-display.tsx | 2 - src/ai/text/components/url-input-form.tsx | 143 +----------------- .../text/components/web-content-analyzer.tsx | 10 -- src/ai/text/utils/error-handling.ts | 27 ---- .../text/utils/web-content-analyzer-config.ts | 18 +-- src/app/api/analyze-content/route.ts | 74 +-------- 7 files changed, 9 insertions(+), 302 deletions(-) delete mode 100644 src/actions/check-web-content-analysis-credits.ts diff --git a/src/actions/check-web-content-analysis-credits.ts b/src/actions/check-web-content-analysis-credits.ts deleted file mode 100644 index 3ca2c7f..0000000 --- a/src/actions/check-web-content-analysis-credits.ts +++ /dev/null @@ -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', - }; - } - } -); diff --git a/src/ai/text/components/error-display.tsx b/src/ai/text/components/error-display.tsx index 489ddd6..43a9435 100644 --- a/src/ai/text/components/error-display.tsx +++ b/src/ai/text/components/error-display.tsx @@ -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', diff --git a/src/ai/text/components/url-input-form.tsx b/src/ai/text/components/url-input-form.tsx index 504d7de..8528b2e 100644 --- a/src/ai/text/components/url-input-form.tsx +++ b/src/ai/text/components/url-input-form.tsx @@ -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 && ( -
-
- - - Cost: {creditInfo.requiredCredits} credits - -
-
- - Balance: {creditInfo.currentCredits} - - {!creditInfo.hasEnoughCredits && ( - - )} -
-
- )} - - {/* Insufficient Credits Warning */} - {isAuthenticated && isInsufficientCredits && ( -
- - - Insufficient credits. You need {creditInfo.requiredCredits}{' '} - credits but only have {creditInfo.currentCredits}. - -
- )} - {!mounted ? ( // Show loading state during hydration to prevent mismatch - ) : isAuthenticated ? ( + ) : ( - ) : ( - - - )} diff --git a/src/ai/text/components/web-content-analyzer.tsx b/src/ai/text/components/web-content-analyzer.tsx index e2b23d2..64efc6f 100644 --- a/src/ai/text/components/web-content-analyzer.tsx +++ b/src/ai/text/components/web-content-analyzer.tsx @@ -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; diff --git a/src/ai/text/utils/error-handling.ts b/src/ai/text/utils/error-handling.ts index 8896d5e..ea648a6 100644 --- a/src/ai/text/utils/error-handling.ts +++ b/src/ai/text/utils/error-handling.ts @@ -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 }, diff --git a/src/ai/text/utils/web-content-analyzer-config.ts b/src/ai/text/utils/web-content-analyzer-config.ts index 83b183c..078cb14 100644 --- a/src/ai/text/utils/web-content-analyzer-config.ts +++ b/src/ai/text/utils/web-content-analyzer-config.ts @@ -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' && diff --git a/src/app/api/analyze-content/route.ts b/src/app/api/analyze-content/route.ts index 91db24e..ef01183 100644 --- a/src/app/api/analyze-content/route.ts +++ b/src/app/api/analyze-content/route.ts @@ -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; From fc024ea0da769ded69229a32696f0ee7c40579c4 Mon Sep 17 00:00:00 2001 From: javayhu Date: Mon, 25 Aug 2025 10:00:28 +0800 Subject: [PATCH 04/11] refactor: simplify layout of ChatBot and ImagePlayground components, update model configuration in web content analyzer --- src/ai/chat/components/ChatBot.tsx | 2 +- src/ai/image/components/ImagePlayground.tsx | 4 ++-- src/ai/text/components/url-input-form.tsx | 4 ++-- .../text/components/web-content-analyzer.tsx | 3 ++- .../text/utils/web-content-analyzer-config.ts | 3 ++- src/ai/text/utils/web-content-analyzer.ts | 2 +- src/app/[locale]/(marketing)/ai/chat/page.tsx | 4 +++- src/app/[locale]/(marketing)/ai/image/page.tsx | 18 ++++++++++++++++-- 8 files changed, 29 insertions(+), 11 deletions(-) diff --git a/src/ai/chat/components/ChatBot.tsx b/src/ai/chat/components/ChatBot.tsx index 10408ee..9ac8aac 100644 --- a/src/ai/chat/components/ChatBot.tsx +++ b/src/ai/chat/components/ChatBot.tsx @@ -70,7 +70,7 @@ export default function ChatBot() { }; return ( -
+
diff --git a/src/ai/image/components/ImagePlayground.tsx b/src/ai/image/components/ImagePlayground.tsx index a88887d..105e3db 100644 --- a/src/ai/image/components/ImagePlayground.tsx +++ b/src/ai/image/components/ImagePlayground.tsx @@ -76,9 +76,9 @@ export function ImagePlayground({ return (
-
+
{/* header */} - + {/* */} {/* input prompt */} + OpenRouter OpenAI GPT-4o Google Gemini - DeepSeek - OpenRouter + DeepSeek R1
diff --git a/src/ai/text/components/web-content-analyzer.tsx b/src/ai/text/components/web-content-analyzer.tsx index 64efc6f..4e7f769 100644 --- a/src/ai/text/components/web-content-analyzer.tsx +++ b/src/ai/text/components/web-content-analyzer.tsx @@ -194,7 +194,8 @@ export function WebContentAnalyzer({ className }: WebContentAnalyzerProps) { const [state, dispatch] = useReducer(analysisReducer, initialState); // Model provider state - const [modelProvider, setModelProvider] = useState('openai'); + const [modelProvider, setModelProvider] = + useState('openrouter'); // Enhanced error state const [analyzedError, setAnalyzedError] = diff --git a/src/ai/text/utils/web-content-analyzer-config.ts b/src/ai/text/utils/web-content-analyzer-config.ts index 078cb14..1fb1c58 100644 --- a/src/ai/text/utils/web-content-analyzer-config.ts +++ b/src/ai/text/utils/web-content-analyzer-config.ts @@ -115,7 +115,8 @@ export const webContentAnalyzerConfig = { openrouter: { // model: 'openrouter/horizon-beta', // model: 'x-ai/grok-3-beta', - model: 'openai/gpt-4o-mini', + // model: 'openai/gpt-4o-mini', + model: 'deepseek/deepseek-r1:free', temperature: 0.1, maxTokens: 2000, }, diff --git a/src/ai/text/utils/web-content-analyzer.ts b/src/ai/text/utils/web-content-analyzer.ts index 9e9e07b..a49d8c5 100644 --- a/src/ai/text/utils/web-content-analyzer.ts +++ b/src/ai/text/utils/web-content-analyzer.ts @@ -67,7 +67,7 @@ export interface AnalysisState { } // Component Props Interfaces -export type ModelProvider = 'openai' | 'gemini' | 'deepseek'; +export type ModelProvider = 'openai' | 'gemini' | 'deepseek' | 'openrouter'; export interface WebContentAnalyzerProps { className?: string; diff --git a/src/app/[locale]/(marketing)/ai/chat/page.tsx b/src/app/[locale]/(marketing)/ai/chat/page.tsx index ba4041d..bbafd80 100644 --- a/src/app/[locale]/(marketing)/ai/chat/page.tsx +++ b/src/app/[locale]/(marketing)/ai/chat/page.tsx @@ -37,7 +37,9 @@ export default async function AIChatPage() {
{/* Chat Bot */} - +
+ +
); diff --git a/src/app/[locale]/(marketing)/ai/image/page.tsx b/src/app/[locale]/(marketing)/ai/image/page.tsx index 09a440f..fa34aa0 100644 --- a/src/app/[locale]/(marketing)/ai/image/page.tsx +++ b/src/app/[locale]/(marketing)/ai/image/page.tsx @@ -2,6 +2,7 @@ import { ImagePlayground } from '@/ai/image/components/ImagePlayground'; import { getRandomSuggestions } from '@/ai/image/lib/suggestions'; import { constructMetadata } from '@/lib/metadata'; import { getUrlWithLocale } from '@/lib/urls/urls'; +import { ImageIcon } from 'lucide-react'; import type { Metadata } from 'next'; import type { Locale } from 'next-intl'; import { getTranslations } from 'next-intl/server'; @@ -26,8 +27,21 @@ export default async function AIImagePage() { const t = await getTranslations('AIImagePage'); return ( -
- +
+
+ {/* Header Section */} +
+
+ + {t('title')} +
+
+ + {/* Image Playground Component */} +
+ +
+
); } From 0ae3f27c78c232a4f7f1d2d2da079698c1184633 Mon Sep 17 00:00:00 2001 From: javayhu Date: Mon, 25 Aug 2025 10:00:45 +0800 Subject: [PATCH 05/11] refactor: change default website mode from 'system' to 'dark' in website configuration --- src/config/website.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/website.tsx b/src/config/website.tsx index 5bd39f0..cbfa9e7 100644 --- a/src/config/website.tsx +++ b/src/config/website.tsx @@ -14,7 +14,7 @@ export const websiteConfig: WebsiteConfig = { enableSwitch: true, }, mode: { - defaultMode: 'system', + defaultMode: 'dark', enableSwitch: true, }, images: { From 1c0c46fa34d823737a837371f60be8c00e087b81 Mon Sep 17 00:00:00 2001 From: javayhu Date: Mon, 25 Aug 2025 10:01:54 +0800 Subject: [PATCH 06/11] Revert "feat: enhance credit hooks to trigger updates on store changes" This reverts commit 7851a715a3d56843d603698e4dcf00bf5360ecbb. --- src/hooks/use-credits.ts | 34 ++-------------------------------- src/stores/credits-store.ts | 23 ----------------------- 2 files changed, 2 insertions(+), 55 deletions(-) delete mode 100644 src/stores/credits-store.ts diff --git a/src/hooks/use-credits.ts b/src/hooks/use-credits.ts index 2ccae9e..94a0d90 100644 --- a/src/hooks/use-credits.ts +++ b/src/hooks/use-credits.ts @@ -2,10 +2,8 @@ import { consumeCreditsAction } from '@/actions/consume-credits'; import { getCreditBalanceAction } from '@/actions/get-credit-balance'; import { getCreditStatsAction } from '@/actions/get-credit-stats'; import { getCreditTransactionsAction } from '@/actions/get-credit-transactions'; -import { useCreditsStore } from '@/stores/credits-store'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import type { SortingState } from '@tanstack/react-table'; -import { useEffect } from 'react'; // Query keys export const creditsKeys = { @@ -23,9 +21,7 @@ export const creditsKeys = { // Hook to fetch credit balance export function useCreditBalance() { - const updateTrigger = useCreditsStore((state) => state.updateTrigger); - - const query = useQuery({ + return useQuery({ queryKey: creditsKeys.balance(), queryFn: async () => { console.log('Fetching credit balance...'); @@ -39,23 +35,11 @@ export function useCreditBalance() { return result.data.credits || 0; }, }); - - // Refetch when updateTrigger changes - useEffect(() => { - if (updateTrigger > 0) { - console.log('Credits update triggered, refetching balance...'); - query.refetch(); - } - }, [updateTrigger, query]); - - return query; } // Hook to fetch credit statistics export function useCreditStats() { - const updateTrigger = useCreditsStore((state) => state.updateTrigger); - - const query = useQuery({ + return useQuery({ queryKey: creditsKeys.stats(), queryFn: async () => { console.log('Fetching credit stats...'); @@ -67,22 +51,11 @@ export function useCreditStats() { return result.data.data; }, }); - - // Refetch when updateTrigger changes - useEffect(() => { - if (updateTrigger > 0) { - console.log('Credits update triggered, refetching stats...'); - query.refetch(); - } - }, [updateTrigger, query]); - - return query; } // Hook to consume credits export function useConsumeCredits() { const queryClient = useQueryClient(); - const triggerUpdate = useCreditsStore((state) => state.triggerUpdate); return useMutation({ mutationFn: async ({ @@ -102,9 +75,6 @@ export function useConsumeCredits() { return result.data; }, onSuccess: () => { - // Trigger credits update in store to notify all components - triggerUpdate(); - // Invalidate credit balance and stats after consuming credits queryClient.invalidateQueries({ queryKey: creditsKeys.balance(), diff --git a/src/stores/credits-store.ts b/src/stores/credits-store.ts deleted file mode 100644 index 3c2f909..0000000 --- a/src/stores/credits-store.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { create } from 'zustand'; - -interface CreditsState { - // trigger for credit updates, incremented each time credits change - updateTrigger: number; - // method to trigger credit updates - triggerUpdate: () => void; -} - -/** - * Credits store for managing credit balance updates. - * - * This store provides a simple trigger mechanism to notify components - * when credits have been consumed or updated, ensuring UI components can - * refetch the latest credit balance. - */ -export const useCreditsStore = create((set) => ({ - updateTrigger: 0, - triggerUpdate: () => - set((state) => ({ - updateTrigger: state.updateTrigger + 1, - })), -})); From fa4b9a19a1dafb34a46cf8adec35c249c2277f72 Mon Sep 17 00:00:00 2001 From: javayhu Date: Mon, 25 Aug 2025 23:39:10 +0800 Subject: [PATCH 07/11] refactor: remove service worker and related registration utilities --- public/sw.js | 129 --------------------------------------- src/lib/serviceWorker.ts | 68 --------------------- 2 files changed, 197 deletions(-) delete mode 100644 public/sw.js delete mode 100644 src/lib/serviceWorker.ts diff --git a/public/sw.js b/public/sw.js deleted file mode 100644 index 3ac3175..0000000 --- a/public/sw.js +++ /dev/null @@ -1,129 +0,0 @@ -// Service Worker for caching iframe content -const CACHE_NAME = 'cnblocks-iframe-cache-v1' - -// Add iframe URLs to this list to prioritize caching -const URLS_TO_CACHE = [ - // Default assets that should be cached - '/favicon.ico', - // Images used in iframes - '/payments.png', - '/payments-light.png', - '/origin-cal.png', - '/origin-cal-dark.png', - '/exercice.png', - '/exercice-dark.png', - '/charts-light.png', - '/charts.png', - '/music-light.png', - '/music.png', - '/mail-back-light.png', - '/mail-upper.png', - '/mail-back.png', - '/card.png', - '/dark-card.webp', -] - -// Install event - cache resources -self.addEventListener('install', (event) => { - event.waitUntil( - caches - .open(CACHE_NAME) - .then((cache) => { - console.log('Opened cache') - return cache.addAll(URLS_TO_CACHE) - }) - .then(() => self.skipWaiting()) // Activate SW immediately - ) -}) - -// Activate event - clean up old caches -self.addEventListener('activate', (event) => { - const currentCaches = [CACHE_NAME] - event.waitUntil( - caches - .keys() - .then((cacheNames) => { - return cacheNames.filter((cacheName) => !currentCaches.includes(cacheName)) - }) - .then((cachesToDelete) => { - return Promise.all( - cachesToDelete.map((cacheToDelete) => { - return caches.delete(cacheToDelete) - }) - ) - }) - .then(() => self.clients.claim()) // Take control of clients immediately - ) -}) - -// Fetch event - serve from cache or fetch from network and cache -self.addEventListener('fetch', (event) => { - // Check if this is an iframe request - typically they'll be HTML or have 'preview' in the URL - const isIframeRequest = event.request.url.includes('/preview/') || event.request.url.includes('/examples/') - - if (isIframeRequest) { - event.respondWith( - caches.match(event.request, { ignoreSearch: true }).then((response) => { - // Return cached response if found - if (response) { - return response - } - - // Clone the request (requests are one-time use) - const fetchRequest = event.request.clone() - - return fetch(fetchRequest).then((response) => { - // Check if we received a valid response - if (!response || response.status !== 200 || response.type !== 'basic') { - return response - } - - // Clone the response (responses are one-time use) - const responseToCache = response.clone() - - caches.open(CACHE_NAME).then((cache) => { - cache.put(event.request, responseToCache) - }) - - return response - }) - }) - ) - } else { - // For non-iframe requests, use a standard cache-first strategy - event.respondWith( - caches.match(event.request).then((response) => { - if (response) { - return response - } - return fetch(event.request) - }) - ) - } -}) - -// Listen for messages from clients (to force cache update, etc) -self.addEventListener('message', (event) => { - if (event.data && event.data.type === 'SKIP_WAITING') { - self.skipWaiting() - } - - // Handle cache clearing - if (event.data && event.data.type === 'CLEAR_IFRAME_CACHE') { - const url = event.data.url - - if (url) { - // Clear specific URL from cache - caches.open(CACHE_NAME).then((cache) => { - cache.delete(url).then(() => { - console.log(`Cleared cache for: ${url}`) - }) - }) - } else { - // Clear the entire cache - caches.delete(CACHE_NAME).then(() => { - console.log('Cleared entire iframe cache') - }) - } - } -}) diff --git a/src/lib/serviceWorker.ts b/src/lib/serviceWorker.ts deleted file mode 100644 index 861b647..0000000 --- a/src/lib/serviceWorker.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * Service worker registration and management utilities - */ - -// Register the service worker -export function registerServiceWorker() { - if (typeof window !== 'undefined' && 'serviceWorker' in navigator) { - window.addEventListener('load', () => { - navigator.serviceWorker - .register('/sw.js') - .then((registration) => { - console.log('SW registered: ', registration); - }) - .catch((registrationError) => { - console.log('SW registration failed: ', registrationError); - }); - }); - } -} - -// Send a message to the service worker -type SWMessage = { - type: string; - url?: string; -}; - -export function sendMessageToSW(message: SWMessage) { - if ( - typeof window !== 'undefined' && - 'serviceWorker' in navigator && - navigator.serviceWorker.controller - ) { - navigator.serviceWorker.controller.postMessage(message); - } -} - -// Clear iframe cache for a specific URL or all iframe caches if no URL provided -export function clearIframeCache(url?: string) { - sendMessageToSW({ - type: 'CLEAR_IFRAME_CACHE', - url, - }); -} - -// Update the service worker -export function updateServiceWorker() { - if (typeof window !== 'undefined' && 'serviceWorker' in navigator) { - navigator.serviceWorker.ready.then((registration) => { - registration.update(); - }); - } -} - -// Check if a URL is already cached by the service worker -export async function isUrlCached(url: string): Promise { - if (typeof window === 'undefined' || !('caches' in window)) { - return false; - } - - try { - const cache = await caches.open('cnblocks-iframe-cache-v1'); - const cachedResponse = await cache.match(url); - return cachedResponse !== undefined; - } catch (error) { - console.error('Error checking cache:', error); - return false; - } -} From 4bad9714faa6f7944555f82fd37e480117d34b81 Mon Sep 17 00:00:00 2001 From: javayhu Date: Mon, 25 Aug 2025 23:43:45 +0800 Subject: [PATCH 08/11] refactor: remove BlockCategory pages, and BlockPreview components --- .../(marketing)/blocks/[category]/layout.tsx | 16 - .../(marketing)/blocks/[category]/page.tsx | 54 --- src/components/tailark/block-preview.tsx | 368 ------------------ 3 files changed, 438 deletions(-) delete mode 100644 src/app/[locale]/(marketing)/blocks/[category]/layout.tsx delete mode 100644 src/app/[locale]/(marketing)/blocks/[category]/page.tsx delete mode 100644 src/components/tailark/block-preview.tsx diff --git a/src/app/[locale]/(marketing)/blocks/[category]/layout.tsx b/src/app/[locale]/(marketing)/blocks/[category]/layout.tsx deleted file mode 100644 index c988f6f..0000000 --- a/src/app/[locale]/(marketing)/blocks/[category]/layout.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { categories } from '@/components/tailark/blocks'; -import BlocksNav from '@/components/tailark/blocks-nav'; -import type { PropsWithChildren } from 'react'; - -/** - * The locale inconsistency issue has been fixed in the BlocksNav component - */ -export default function BlockCategoryLayout({ children }: PropsWithChildren) { - return ( - <> - - -
{children}
- - ); -} diff --git a/src/app/[locale]/(marketing)/blocks/[category]/page.tsx b/src/app/[locale]/(marketing)/blocks/[category]/page.tsx deleted file mode 100644 index cd893d1..0000000 --- a/src/app/[locale]/(marketing)/blocks/[category]/page.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import BlockPreview from '@/components/tailark/block-preview'; -import { blocks, categories } from '@/components/tailark/blocks'; -import { constructMetadata } from '@/lib/metadata'; -import { getUrlWithLocale } from '@/lib/urls/urls'; -import type { Metadata } from 'next'; -import type { Locale } from 'next-intl'; -import { getTranslations } from 'next-intl/server'; -import { notFound } from 'next/navigation'; - -export const dynamic = 'force-static'; -export const revalidate = 3600; - -export async function generateStaticParams() { - return categories.map((category) => ({ - category: category, - })); -} - -export async function generateMetadata({ - params, -}: { - params: Promise<{ locale: Locale; category: string }>; -}): Promise { - const { locale, category } = await params; - const t = await getTranslations({ locale, namespace: 'Metadata' }); - return constructMetadata({ - title: category + ' | ' + t('title'), - description: t('description'), - canonicalUrl: getUrlWithLocale(`/blocks/${category}`, locale), - }); -} - -interface BlockCategoryPageProps { - params: Promise<{ category: string }>; -} - -export default async function BlockCategoryPage({ - params, -}: BlockCategoryPageProps) { - const { category } = await params; - const categoryBlocks = blocks.filter((b) => b.category === category); - - if (categoryBlocks.length === 0) { - notFound(); - } - - return ( - <> - {categoryBlocks.map((block, index) => ( - - ))} - - ); -} diff --git a/src/components/tailark/block-preview.tsx b/src/components/tailark/block-preview.tsx deleted file mode 100644 index 797e49e..0000000 --- a/src/components/tailark/block-preview.tsx +++ /dev/null @@ -1,368 +0,0 @@ -'use client'; - -import { Button } from '@/components/ui/button'; -import { Separator } from '@/components/ui/separator'; -import { useCopyToClipboard } from '@/hooks/use-clipboard'; -import { isUrlCached } from '@/lib/serviceWorker'; -import { cn } from '@/lib/utils'; -import * as RadioGroup from '@radix-ui/react-radio-group'; -import { Check, Code2, Copy, Eye, Maximize, Terminal } from 'lucide-react'; -import Link from 'next/link'; -import type React from 'react'; -import { useEffect, useRef, useState } from 'react'; -import { - Panel, - PanelGroup, - PanelResizeHandle, - type ImperativePanelGroupHandle, -} from 'react-resizable-panels'; -import { useMedia } from 'use-media'; - -export interface BlockPreviewProps { - code?: string; - preview: string; - title: string; - category: string; - previewOnly?: boolean; -} - -const radioItem = - 'rounded-(--radius) duration-200 flex items-center justify-center h-8 px-2.5 gap-2 transition-[color] data-[state=checked]:bg-muted'; - -const DEFAULTSIZE = 100; -const SMSIZE = 30; -const MDSIZE = 62; -const LGSIZE = 82; - -const getCacheKey = (src: string) => `iframe-cache-${src}`; - -const titleToNumber = (title: string): number => { - const titles = ["one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen", "twenty"]; - return titles.indexOf(title.toLowerCase()) + 1; -}; - -export const BlockPreview: React.FC = ({ - code, - preview, - title, - category, - previewOnly, -}) => { - const [width, setWidth] = useState(DEFAULTSIZE); - const [mode, setMode] = useState<'preview' | 'code'>('preview'); - const [iframeHeight, setIframeHeight] = useState(0); - const [shouldLoadIframe, setShouldLoadIframe] = useState(false); - const [cachedHeight, setCachedHeight] = useState(null); - const [isIframeCached, setIsIframeCached] = useState(false); - - const terminalCode = `pnpm dlx shadcn@canary add https://nsui.irung.me/r/${category}-${titleToNumber(title)}.json`; - const { copied, copy } = useCopyToClipboard({ code: code as string, title, category, eventName: 'block_copy' }) - const { copied: cliCopied, copy: cliCopy } = useCopyToClipboard({ code: terminalCode, title, category, eventName: 'block_cli_copy' }) - - const ref = useRef(null); - const isLarge = useMedia('(min-width: 1024px)'); - - const iframeRef = useRef(null); - const observer = useRef(null); - const blockRef = useRef(null); - - useEffect(() => { - observer.current = new IntersectionObserver( - (entries) => { - if (entries[0].isIntersecting) { - setShouldLoadIframe(true); - observer.current?.disconnect(); - } - }, - { threshold: 0.1 } - ); - - if (blockRef.current) { - observer.current.observe(blockRef.current); - } - - return () => { - observer.current?.disconnect(); - }; - }, []); - - useEffect(() => { - const checkCache = async () => { - try { - const isCached = await isUrlCached(preview); - setIsIframeCached(isCached); - if (isCached) { - setShouldLoadIframe(true); - } - } catch (error) { - console.error('Error checking cache status:', error); - } - }; - - checkCache(); - - try { - const cacheKey = getCacheKey(preview); - const cached = localStorage.getItem(cacheKey); - if (cached) { - const { height, timestamp } = JSON.parse(cached); - const now = Date.now(); - if (now - timestamp < 24 * 60 * 60 * 1000) { - setCachedHeight(height); - setIframeHeight(height); - } - } - } catch (error) { - console.error('Error retrieving cache:', error); - } - }, [preview]); - - useEffect(() => { - const iframe = iframeRef.current; - if (!iframe || !shouldLoadIframe) return; - - const handleLoad = () => { - try { - const contentHeight = iframe.contentWindow!.document.body.scrollHeight; - setIframeHeight(contentHeight); - - const cacheKey = getCacheKey(preview); - const cacheValue = JSON.stringify({ - height: contentHeight, - timestamp: Date.now(), - }); - localStorage.setItem(cacheKey, cacheValue); - } catch (e) { - console.error('Error accessing iframe content:', e); - } - }; - - iframe.addEventListener('load', handleLoad); - return () => { - iframe.removeEventListener('load', handleLoad); - }; - }, [shouldLoadIframe, preview]); - - useEffect(() => { - if (!blockRef.current || shouldLoadIframe) return; - - const linkElement = document.createElement('link'); - linkElement.rel = 'preload'; - linkElement.href = preview; - linkElement.as = 'document'; - - if ( - !document.head.querySelector(`link[rel="preload"][href="${preview}"]`) - ) { - document.head.appendChild(linkElement); - } - - return () => { - const existingLink = document.head.querySelector( - `link[rel="preload"][href="${preview}"]` - ); - if (existingLink) { - document.head.removeChild(existingLink); - } - }; - }, [preview, shouldLoadIframe]); - - return ( -
-
-
-
-
-
- -
-
- {code && ( - <> - - setMode('preview')} - aria-label="Block preview" - value="100" - checked={mode == 'preview'} - className={radioItem} - > - - Preview - - - setMode('code')} - aria-label="Code" - value="0" - checked={mode == 'code'} - className={radioItem} - > - - Code - - - - - - )} - {previewOnly && ( - <> - {' '} - {title} - {' '} - - )} - {/* */} - - - {width < MDSIZE - ? 'Mobile' - : width < LGSIZE - ? 'Tablet' - : 'Desktop'} - {' '} -
- -
- {code && ( - <> - - - {/* */} - - - - - )} - {!code && ( - - {/* pnpm dlx shadcn@canary add */}{category}-{titleToNumber(title)} - - )} -
-
-
- -
-
-
-
-
- -
-
- - { - setWidth(Number(size)); - }} - defaultSize={DEFAULTSIZE} - minSize={SMSIZE} - className="h-fit border-x" - > -
- {shouldLoadIframe ? ( -