From cc29cacca98370536456dbe998fec90eadf6fc86 Mon Sep 17 00:00:00 2001 From: Jaya Prasad Kavuru Date: Wed, 20 May 2026 09:47:32 +0530 Subject: [PATCH 1/2] added credits display, polling endpoint and minor fixes --- src/app/api/creations/[id]/route.js | 53 ++++++++++++++++++++++++++++- src/app/api/generate/route.js | 29 +++++++++++++--- src/app/page.js | 50 ++++++++++++++++++++++++--- 3 files changed, 123 insertions(+), 9 deletions(-) diff --git a/src/app/api/creations/[id]/route.js b/src/app/api/creations/[id]/route.js index 1a5406a..7c7f88a 100644 --- a/src/app/api/creations/[id]/route.js +++ b/src/app/api/creations/[id]/route.js @@ -13,7 +13,7 @@ export async function GET(req, { params }) { const { id } = await params; - const creation = await prisma.creation.findUnique({ + let creation = await prisma.creation.findUnique({ where: { id, userId: session.user.id // Security check @@ -24,6 +24,57 @@ export async function GET(req, { params }) { return new NextResponse("Not Found", { status: 404 }); } + // Active polling fallback in case webhook failed to deliver or wasn't set up + const activeStatuses = ['processing', 'pending', 'starting', 'queued']; + if (activeStatuses.includes(creation.status) && creation.requestId) { + const apiKey = process.env.UGC_API_KEY; + if (apiKey) { + try { + const checkRes = await fetch(`https://api.muapi.ai/api/v1/predictions/${creation.requestId}/result`, { + method: "GET", + headers: { + "x-api-key": apiKey, + }, + }); + + if (checkRes.ok) { + const checkData = await checkRes.json(); + const upstreamStatus = checkData.status || "processing"; + + if (upstreamStatus === "failed") { + creation = await prisma.creation.update({ + where: { id }, + data: { + status: "failed", + error: checkData.error || "Generation failed" + } + }); + } else if (upstreamStatus === "completed" || (checkData.outputs && checkData.outputs.length > 0)) { + const outputs = checkData.outputs || []; + const videoUrl = outputs.length > 0 ? outputs[0] : null; + + creation = await prisma.creation.update({ + where: { id }, + data: { + status: "completed", + url: videoUrl + } + }); + } else if (upstreamStatus !== "processing") { + creation = await prisma.creation.update({ + where: { id }, + data: { + status: upstreamStatus + } + }); + } + } + } catch (pollErr) { + console.error("Error polling prediction status from upstream:", pollErr); + } + } + } + return NextResponse.json(creation); } catch (error) { diff --git a/src/app/api/generate/route.js b/src/app/api/generate/route.js index 59bd85e..a901ce4 100644 --- a/src/app/api/generate/route.js +++ b/src/app/api/generate/route.js @@ -30,14 +30,35 @@ export async function POST(req) { return new NextResponse("Invalid model selected", { status: 400 }); } + // Calculate required credits + let requiredCredits = 10; + const duration = typeof settings.duration === "number" ? settings.duration : 5; + const resolution = settings.resolution || ""; + + if (modelId === "grok-video") { + const grokDuration = typeof settings.duration === "number" ? settings.duration : 6; + const rate = resolution === "720p" ? 10 : 5; + requiredCredits = grokDuration * rate; + } else if (modelId === "veo-3-1") { + const veoDuration = typeof settings.duration === "number" ? settings.duration : 8; + let rate = 500; + if (resolution === "1080p") rate = 650; + else if (resolution === "4k") rate = 740; + requiredCredits = veoDuration * rate; + } else if (modelId === "happy-horse") { + requiredCredits = duration * 36; + } else if (modelId === "seedance-2") { + requiredCredits = duration * 50; + } + // Check credits const user = await prisma.user.findUnique({ where: { id: session.user.id }, select: { credits: true } }); - if (!user || user.credits <= 0) { - return NextResponse.json({ error: "Insufficient credits" }, { status: 403 }); + if (!user || user.credits < requiredCredits) { + return NextResponse.json({ error: `Insufficient credits. This requires ${requiredCredits} credits but you only have ${user?.credits || 0}.` }, { status: 403 }); } // Prepare payload based on MUAPI specs @@ -84,10 +105,10 @@ export async function POST(req) { } }); - // Deduct 1 credit + // Deduct required credits await prisma.user.update({ where: { id: session.user.id }, - data: { credits: { decrement: 1 } } + data: { credits: { decrement: requiredCredits } } }); return NextResponse.json({ diff --git a/src/app/page.js b/src/app/page.js index 15284d6..eac607d 100644 --- a/src/app/page.js +++ b/src/app/page.js @@ -207,15 +207,18 @@ export default function Home() { // Polling for last generation status useEffect(() => { let interval; - if (lastGeneration && lastGeneration.status === 'processing') { + const activeStatuses = ['processing', 'pending', 'starting', 'queued']; + if (lastGeneration && activeStatuses.includes(lastGeneration.status)) { interval = setInterval(async () => { try { const res = await fetch(`/api/creations/${lastGeneration.id}`); if (res.ok) { const data = await res.json(); - if (data.status !== 'processing') { + if (!activeStatuses.includes(data.status)) { setLastGeneration(data); clearInterval(interval); + } else if (data.status !== lastGeneration.status) { + setLastGeneration(data); } } } catch (error) { @@ -236,6 +239,35 @@ export default function Home() { } }, [selectedModel]); + const getRequiredCredits = () => { + const duration = typeof modelSettings.duration === "number" ? modelSettings.duration : 5; + const resolution = modelSettings.resolution || ""; + + if (selectedModel.id === "grok-video") { + const grokDuration = typeof modelSettings.duration === "number" ? modelSettings.duration : 6; + const rate = resolution === "720p" ? 10 : 5; + return grokDuration * rate; + } + + if (selectedModel.id === "veo-3-1") { + const veoDuration = typeof modelSettings.duration === "number" ? modelSettings.duration : 8; + let rate = 500; + if (resolution === "1080p") rate = 650; + else if (resolution === "4k") rate = 740; + return veoDuration * rate; + } + + if (selectedModel.id === "happy-horse") { + return duration * 36; + } + + if (selectedModel.id === "seedance-2") { + return duration * 50; + } + + return 10; + }; + const handleImageUpload = async (e) => { const files = Array.from(e.target.files); @@ -364,10 +396,12 @@ export default function Home() { animate={{ opacity: 1, scale: 1, y: 0 }} className="relative w-full max-w-lg aspect-[9/16] max-h-[60vh] bg-glass-bg rounded border border-glass-border shadow-2xl overflow-hidden flex flex-col items-center justify-center" > - {lastGeneration.status === 'processing' ? ( + {['processing', 'pending', 'starting', 'queued'].includes(lastGeneration.status) ? (
- Manifesting... + + Manifesting ({lastGeneration.status})... +
) : lastGeneration.status === 'failed' ? (
@@ -552,6 +586,14 @@ export default function Home() {
+ {/* Show credit cost */} +
+ + + Cost: {getRequiredCredits()} + +
+