From eb692bcca3194738839d91512fc8e543ff6981f1 Mon Sep 17 00:00:00 2001 From: Riccardo Giorato Date: Tue, 13 Jan 2026 13:17:16 +0100 Subject: [PATCH] Update API key usage and credit checking logic for comic generation --- app/api/add-page/route.ts | 2 +- app/api/check-credits/route.ts | 71 ++++++++++++ app/api/generate-comic/route.ts | 2 +- app/story/[storySlug]/story-editor-client.tsx | 51 +++++++-- components/api-key-modal.tsx | 10 +- components/landing/comic-creation-form.tsx | 102 +++++++++++++++--- 6 files changed, 212 insertions(+), 26 deletions(-) create mode 100644 app/api/check-credits/route.ts diff --git a/app/api/add-page/route.ts b/app/api/add-page/route.ts index 1a5f2fc..eaf72e5 100644 --- a/app/api/add-page/route.ts +++ b/app/api/add-page/route.ts @@ -140,7 +140,7 @@ export async function POST(request: NextRequest) { }); const client = new Together({ - apiKey: process.env.TOGETHER_API_KEY_DEFAULT, + apiKey: process.env.TOGETHER_API_KEY, }); let response; diff --git a/app/api/check-credits/route.ts b/app/api/check-credits/route.ts new file mode 100644 index 0000000..7c65b93 --- /dev/null +++ b/app/api/check-credits/route.ts @@ -0,0 +1,71 @@ +import { type NextRequest, NextResponse } from "next/server"; +import { auth } from "@clerk/nextjs/server"; +import { freeTierRateLimit } from "@/lib/rate-limit"; +import { db } from "@/lib/db"; +import { stories } from "@/lib/schema"; +import { gte } from "drizzle-orm"; + +export async function POST(request: NextRequest) { + try { + const { userId } = await auth(); + + if (!userId) { + return NextResponse.json( + { error: "Authentication required" }, + { status: 401 } + ); + } + + const { hasApiKey } = await request.json(); + + // Check if user has API key (unlimited) + if (hasApiKey) { + return NextResponse.json({ + hasApiKey: true, + creditsRemaining: "unlimited", + resetTime: null, + }); + } + + // For free tier, check rate limit status + try { + // Try to check remaining without consuming + const limitResult = await freeTierRateLimit.getRemaining(userId); + + return NextResponse.json({ + hasApiKey: false, + creditsRemaining: limitResult.remaining, + resetTime: limitResult.reset, + }); + } catch (error) { + console.error("Rate limit check error:", error); + // Fallback to database check + const sevenDaysAgo = new Date(); + sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); + + const recentStories = await db + .select() + .from(stories) + .where(gte(stories.createdAt, sevenDaysAgo)) + .limit(1); + + const hasUsedCredit = recentStories.length > 0; + + return NextResponse.json({ + hasApiKey: false, + creditsRemaining: hasUsedCredit ? 0 : 1, + resetTime: hasUsedCredit ? sevenDaysAgo.getTime() + 7 * 24 * 60 * 60 * 1000 : null, + }); + } + } catch (error) { + console.error("Error in check-credits API:", error); + return NextResponse.json( + { + error: `Internal server error: ${ + error instanceof Error ? error.message : "Unknown error" + }`, + }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/generate-comic/route.ts b/app/api/generate-comic/route.ts index 3270c62..512f14c 100644 --- a/app/api/generate-comic/route.ts +++ b/app/api/generate-comic/route.ts @@ -65,7 +65,7 @@ export async function POST(request: NextRequest) { if (isUsingFreeTier) { // Use default API key for free tier - finalApiKey = process.env.TOGETHER_API_KEY_DEFAULT; + finalApiKey = process.env.TOGETHER_API_KEY if (!finalApiKey) { return NextResponse.json( { diff --git a/app/story/[storySlug]/story-editor-client.tsx b/app/story/[storySlug]/story-editor-client.tsx index acaa741..ca8fac5 100644 --- a/app/story/[storySlug]/story-editor-client.tsx +++ b/app/story/[storySlug]/story-editor-client.tsx @@ -167,16 +167,55 @@ export function StoryEditorClient() { }, [pages.length, apiKey]); - const handleAddPage = () => { + const handleAddPage = async () => { if (!isLoaded || !isSignedIn) { return; } - - if (!apiKey && pages.length >= 1) { - setShowApiModal(true); - return; + + // Check credits + try { + const hasApiKey = !!apiKey; + const response = await fetch('/api/check-credits', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ hasApiKey }), + }); + const data = await response.json(); + + if (!response.ok) { + toast({ + title: "Error", + description: "Failed to check credits", + variant: "destructive", + }); + return; + } + + if (hasApiKey || data.creditsRemaining === "unlimited") { + // Has API key, unlimited + setShowGenerateModal(true); + } else if (data.creditsRemaining > 0) { + // Has credits + setShowGenerateModal(true); + } else { + // No credits left, show API modal + setShowApiModal(true); + toast({ + title: "No credits remaining", + description: "You get 1 credit weekly. Add an API key for unlimited generation.", + variant: "destructive", + }); + } + } catch (error) { + console.error("Error checking credits:", error); + toast({ + title: "Error", + description: "Failed to check credits", + variant: "destructive", + }); } - setShowGenerateModal(true); }; const handleRedrawPage = () => { diff --git a/components/api-key-modal.tsx b/components/api-key-modal.tsx index 955fcbe..51c8117 100644 --- a/components/api-key-modal.tsx +++ b/components/api-key-modal.tsx @@ -70,11 +70,11 @@ export function ApiKeyModal({ isOpen, onClose, onSubmit }: ApiKeyModalProps) { : "Add your API key to continue"} - - {existingKey - ? "Update your Together API key or add a new one. You can also delete your existing key." - : "Your first page was free! Add your Together API key to generate more pages."} - + + {existingKey + ? "Update your Together API key or add a new one. You can also delete your existing key." + : "You've used your weekly credit! Add your Together API key for unlimited generation."} +
diff --git a/components/landing/comic-creation-form.tsx b/components/landing/comic-creation-form.tsx index 01980d8..2204d2b 100644 --- a/components/landing/comic-creation-form.tsx +++ b/components/landing/comic-creation-form.tsx @@ -11,6 +11,7 @@ import { COMIC_STYLES } from "@/lib/constants"; import { useKeyboardShortcut } from "@/hooks/use-keyboard-shortcut"; import { useApiKey } from "@/hooks/use-api-key"; import { isContentPolicyViolation } from "@/lib/utils"; +import { ApiKeyModal } from "@/components/api-key-modal"; interface ComicCreationFormProps { prompt: string; @@ -23,11 +24,14 @@ interface ComicCreationFormProps { setIsLoading: (loading: boolean) => void; } +const DEFAULT_STYLE = 'noir'; +const STYLE_STORAGE_KEY = 'comic-style-preference'; + export function ComicCreationForm({ prompt, setPrompt, - style, - setStyle, + style: initialStyle, + setStyle: setParentStyle, characterFiles, setCharacterFiles, isLoading, @@ -39,11 +43,22 @@ export function ComicCreationForm({ const { uploadToS3 } = useS3Upload(); const { isSignedIn, isLoaded } = useAuth(); const { openSignIn } = useClerk(); - const [apiKey] = useApiKey(); + const [apiKey, setApiKey] = useApiKey(); const hasApiKey = !!apiKey; const [previews, setPreviews] = useState([]); const [showPreview, setShowPreview] = useState(null); const [showStyleDropdown, setShowStyleDropdown] = useState(false); + const [creditsRemaining, setCreditsRemaining] = useState(null); + const [showApiModal, setShowApiModal] = useState(false); + + // Initialize style with saved preference or default + const [style, setStyle] = useState(() => { + if (typeof window !== 'undefined') { + const saved = localStorage.getItem(STYLE_STORAGE_KEY); + return saved || initialStyle || DEFAULT_STYLE; + } + return initialStyle || DEFAULT_STYLE; + }); const fileInputRef = useRef(null); const textareaRef = useRef(null); @@ -78,6 +93,38 @@ export function ComicCreationForm({ } }, []); // Run only on mount + // Save style to localStorage and sync with parent + useEffect(() => { + localStorage.setItem(STYLE_STORAGE_KEY, style); + setParentStyle(style); + }, [style, setParentStyle]); + + // Fetch credits on mount + useEffect(() => { + if (isSignedIn && !hasApiKey) { + const fetchCredits = async () => { + try { + const response = await fetch('/api/check-credits', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ hasApiKey: false }), + }); + const data = await response.json(); + if (response.ok) { + setCreditsRemaining(data.creditsRemaining); + } + } catch (error) { + console.error('Error fetching credits:', error); + } + }; + fetchCredits(); + } else if (hasApiKey) { + setCreditsRemaining(null); // Unlimited + } + }, [isSignedIn, hasApiKey]); + // Keyboard shortcut for form submission useKeyboardShortcut(() => { if (!isLoading && prompt.trim()) { @@ -151,15 +198,33 @@ export function ComicCreationForm({ setLoadingStep(0); try { - if (!apiKey) { - toast({ - title: "API key required", - description: "Please add your API key to generate comics.", - variant: "destructive", - duration: 3000, + // Check credits + const hasApiKey = !!apiKey; + if (!hasApiKey) { + const creditsResponse = await fetch('/api/check-credits', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ hasApiKey }), }); - setIsLoading(false); - return; + const creditsData = await creditsResponse.json(); + + if (!creditsResponse.ok) { + toast({ + title: "Error", + description: "Failed to check credits", + variant: "destructive", + }); + setIsLoading(false); + return; + } + + if (creditsData.creditsRemaining === 0) { + setShowApiModal(true); + setIsLoading(false); + return; + } } const characterUploads = await Promise.all( @@ -174,7 +239,7 @@ export function ComicCreationForm({ }, body: JSON.stringify({ prompt, - apiKey, + ...(apiKey && { apiKey }), style, characterImages: characterUploads, }), @@ -214,6 +279,11 @@ export function ComicCreationForm({ } }; + const handleApiKeySubmit = (key: string) => { + setApiKey(key); + setShowApiModal(false); + }; + const handleKeyDown = (e: React.KeyboardEvent) => { const isEnter = e.key === "Enter" || e.key === "\n" || e.keyCode === 13; const isModifierPressed = e.shiftKey || e.ctrlKey || e.metaKey; // metaKey for Cmd on Mac @@ -418,7 +488,7 @@ export function ComicCreationForm({ {hasApiKey ? ( <>Using your API key (~$0.01 per comic) ) : ( - <>1 credit weekly + <>{creditsRemaining !== null ? `${creditsRemaining} credit${creditsRemaining === 1 ? '' : 's'} remaining` : 'Checking credits...'} )} @@ -431,6 +501,12 @@ export function ComicCreationForm({ )} + + setShowApiModal(false)} + onSubmit={handleApiKeySubmit} + /> ); } \ No newline at end of file