Update API key usage and credit checking logic for comic generation

This commit is contained in:
Riccardo Giorato
2026-01-13 13:17:16 +01:00
parent 0a536dc5af
commit eb692bcca3
6 changed files with 212 additions and 26 deletions
+1 -1
View File
@@ -140,7 +140,7 @@ export async function POST(request: NextRequest) {
}); });
const client = new Together({ const client = new Together({
apiKey: process.env.TOGETHER_API_KEY_DEFAULT, apiKey: process.env.TOGETHER_API_KEY,
}); });
let response; let response;
+71
View File
@@ -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 }
);
}
}
+1 -1
View File
@@ -65,7 +65,7 @@ export async function POST(request: NextRequest) {
if (isUsingFreeTier) { if (isUsingFreeTier) {
// Use default API key for free tier // Use default API key for free tier
finalApiKey = process.env.TOGETHER_API_KEY_DEFAULT; finalApiKey = process.env.TOGETHER_API_KEY
if (!finalApiKey) { if (!finalApiKey) {
return NextResponse.json( return NextResponse.json(
{ {
+42 -3
View File
@@ -167,16 +167,55 @@ export function StoryEditorClient() {
}, [pages.length, apiKey]); }, [pages.length, apiKey]);
const handleAddPage = () => { const handleAddPage = async () => {
if (!isLoaded || !isSignedIn) { if (!isLoaded || !isSignedIn) {
return; return;
} }
if (!apiKey && pages.length >= 1) { // Check credits
setShowApiModal(true); 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; return;
} }
if (hasApiKey || data.creditsRemaining === "unlimited") {
// Has API key, unlimited
setShowGenerateModal(true); 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",
});
}
}; };
const handleRedrawPage = () => { const handleRedrawPage = () => {
+1 -1
View File
@@ -73,7 +73,7 @@ export function ApiKeyModal({ isOpen, onClose, onSubmit }: ApiKeyModalProps) {
<DialogDescription className="text-center text-muted-foreground"> <DialogDescription className="text-center text-muted-foreground">
{existingKey {existingKey
? "Update your Together API key or add a new one. You can also delete your existing key." ? "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."} : "You've used your weekly credit! Add your Together API key for unlimited generation."}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
+85 -9
View File
@@ -11,6 +11,7 @@ import { COMIC_STYLES } from "@/lib/constants";
import { useKeyboardShortcut } from "@/hooks/use-keyboard-shortcut"; import { useKeyboardShortcut } from "@/hooks/use-keyboard-shortcut";
import { useApiKey } from "@/hooks/use-api-key"; import { useApiKey } from "@/hooks/use-api-key";
import { isContentPolicyViolation } from "@/lib/utils"; import { isContentPolicyViolation } from "@/lib/utils";
import { ApiKeyModal } from "@/components/api-key-modal";
interface ComicCreationFormProps { interface ComicCreationFormProps {
prompt: string; prompt: string;
@@ -23,11 +24,14 @@ interface ComicCreationFormProps {
setIsLoading: (loading: boolean) => void; setIsLoading: (loading: boolean) => void;
} }
const DEFAULT_STYLE = 'noir';
const STYLE_STORAGE_KEY = 'comic-style-preference';
export function ComicCreationForm({ export function ComicCreationForm({
prompt, prompt,
setPrompt, setPrompt,
style, style: initialStyle,
setStyle, setStyle: setParentStyle,
characterFiles, characterFiles,
setCharacterFiles, setCharacterFiles,
isLoading, isLoading,
@@ -39,11 +43,22 @@ export function ComicCreationForm({
const { uploadToS3 } = useS3Upload(); const { uploadToS3 } = useS3Upload();
const { isSignedIn, isLoaded } = useAuth(); const { isSignedIn, isLoaded } = useAuth();
const { openSignIn } = useClerk(); const { openSignIn } = useClerk();
const [apiKey] = useApiKey(); const [apiKey, setApiKey] = useApiKey();
const hasApiKey = !!apiKey; const hasApiKey = !!apiKey;
const [previews, setPreviews] = useState<string[]>([]); const [previews, setPreviews] = useState<string[]>([]);
const [showPreview, setShowPreview] = useState<number | null>(null); const [showPreview, setShowPreview] = useState<number | null>(null);
const [showStyleDropdown, setShowStyleDropdown] = useState(false); const [showStyleDropdown, setShowStyleDropdown] = useState(false);
const [creditsRemaining, setCreditsRemaining] = useState<number | null>(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<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null); const textareaRef = useRef<HTMLTextAreaElement>(null);
@@ -78,6 +93,38 @@ export function ComicCreationForm({
} }
}, []); // Run only on mount }, []); // 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 // Keyboard shortcut for form submission
useKeyboardShortcut(() => { useKeyboardShortcut(() => {
if (!isLoading && prompt.trim()) { if (!isLoading && prompt.trim()) {
@@ -151,17 +198,35 @@ export function ComicCreationForm({
setLoadingStep(0); setLoadingStep(0);
try { try {
if (!apiKey) { // 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 }),
});
const creditsData = await creditsResponse.json();
if (!creditsResponse.ok) {
toast({ toast({
title: "API key required", title: "Error",
description: "Please add your API key to generate comics.", description: "Failed to check credits",
variant: "destructive", variant: "destructive",
duration: 3000,
}); });
setIsLoading(false); setIsLoading(false);
return; return;
} }
if (creditsData.creditsRemaining === 0) {
setShowApiModal(true);
setIsLoading(false);
return;
}
}
const characterUploads = await Promise.all( const characterUploads = await Promise.all(
characterFiles.map((file) => uploadToS3(file).then(({ url }) => url)) characterFiles.map((file) => uploadToS3(file).then(({ url }) => url))
); );
@@ -174,7 +239,7 @@ export function ComicCreationForm({
}, },
body: JSON.stringify({ body: JSON.stringify({
prompt, prompt,
apiKey, ...(apiKey && { apiKey }),
style, style,
characterImages: characterUploads, characterImages: characterUploads,
}), }),
@@ -214,6 +279,11 @@ export function ComicCreationForm({
} }
}; };
const handleApiKeySubmit = (key: string) => {
setApiKey(key);
setShowApiModal(false);
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => { const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
const isEnter = e.key === "Enter" || e.key === "\n" || e.keyCode === 13; const isEnter = e.key === "Enter" || e.key === "\n" || e.keyCode === 13;
const isModifierPressed = e.shiftKey || e.ctrlKey || e.metaKey; // metaKey for Cmd on Mac const isModifierPressed = e.shiftKey || e.ctrlKey || e.metaKey; // metaKey for Cmd on Mac
@@ -418,7 +488,7 @@ export function ComicCreationForm({
{hasApiKey ? ( {hasApiKey ? (
<>Using your API key (~$0.01 per comic)</> <>Using your API key (~$0.01 per comic)</>
) : ( ) : (
<>1 credit weekly</> <>{creditsRemaining !== null ? `${creditsRemaining} credit${creditsRemaining === 1 ? '' : 's'} remaining` : 'Checking credits...'}</>
)} )}
</div> </div>
</div> </div>
@@ -431,6 +501,12 @@ export function ComicCreationForm({
</SignInButton> </SignInButton>
)} )}
</div> </div>
<ApiKeyModal
isOpen={showApiModal}
onClose={() => setShowApiModal(false)}
onSubmit={handleApiKeySubmit}
/>
</> </>
); );
} }