Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 26 additions & 24 deletions app/api/analyses/[id]/analyze/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from 'next/server'
import { generateText } from 'ai'
import { getCreditBalance, deductCredits, CREDITS } from '@/lib/credits'
import { deductCredits, refundCredits, CREDITS } from '@/lib/credits'
import { getCurrentUser } from '@/lib/auth'

const model = 'openai/gpt-4-turbo'

Expand All @@ -20,30 +21,40 @@ interface AppSuggestion {
}

export async function POST(request: NextRequest) {
let chargedUserId: string | null = null
let refundMetadata: Record<string, unknown> = {}

try {
const { analysisId, selectedRepos, userId } = (await request.json()) as {
const { analysisId, selectedRepos } = (await request.json()) as {
analysisId: string
selectedRepos: SelectedRepository[]
userId: string
}

// Check credit balance before proceeding
if (!userId) {
return NextResponse.json({ error: 'User ID required' }, { status: 401 })
const user = await getCurrentUser()
if (!user) {
return NextResponse.json({ error: 'Sign in before analyzing repositories.' }, { status: 401 })
}

if (!analysisId || !Array.isArray(selectedRepos)) {
return NextResponse.json({ error: 'Analysis ID and selected repositories are required' }, { status: 400 })
}

const currentBalance = await getCreditBalance(userId)
if (currentBalance < CREDITS.ANALYSIS_COST) {
refundMetadata = {
analysisId,
selectedRepos: selectedRepos.map((repo) => repo.name),
}
const deductResult = await deductCredits(user.id, CREDITS.ANALYSIS_COST, 'analysis', refundMetadata)
if (!deductResult.success) {
return NextResponse.json(
{
error: 'Insufficient credits',
error: deductResult.error ?? 'Insufficient credits',
required: CREDITS.ANALYSIS_COST,
available: currentBalance,
message: 'Upgrade to Pro to get unlimited analyses with 3,000 monthly credits.',
},
{ status: 402 }
)
}
chargedUserId = user.id

// Get all repo files from database
const filesByRepo: Record<string, RepositoryTreeFile[]> = {}
Expand Down Expand Up @@ -94,20 +105,6 @@ Return as JSON array of app suggestions. Focus on practical, buildable applicati
console.error('Failed to parse AI response:', e)
}

// Deduct credits for successful analysis
const deductResult = await deductCredits(userId, CREDITS.ANALYSIS_COST, 'analysis', {
analysisId,
selectedRepos: selectedRepos.map((r) => r.name),
})

if (!deductResult.success) {
console.error('Failed to deduct credits:', deductResult.error)
return NextResponse.json(
{ error: 'Failed to process analysis' },
{ status: 500 }
)
}

const newBalance = deductResult.transaction?.balance_after || 0

return NextResponse.json({
Expand All @@ -120,6 +117,11 @@ Return as JSON array of app suggestions. Focus on practical, buildable applicati
})
} catch (error) {
console.error('Analysis error:', error)
if (chargedUserId) {
await refundCredits(chargedUserId, CREDITS.ANALYSIS_COST, 'legacy analysis failed', refundMetadata).catch((refundError) =>
console.error('Failed to refund analysis credits:', refundError),
)
}
return NextResponse.json({ error: 'Failed to analyze repositories' }, { status: 500 })
}
}
Expand Down
73 changes: 48 additions & 25 deletions app/api/build-app/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { generateWithGateway } from '@/lib/ai-gateway'
import { getCurrentUser } from '@/lib/auth'
import { getSubscriptionByGithubId, upsertSubscription, type AppBlueprint } from '@/lib/queries'
import { hasProAccess } from '@/lib/pro-access'
import { CREDITS, deductCredits, refundCredits } from '@/lib/credits'

type Platform = 'github' | 'gitlab'

Expand Down Expand Up @@ -69,7 +70,14 @@ function getFilesToGenerate(blueprint: BuildAppRequest['blueprint']): Array<{ pa
files.push({ path: '.env.example', purpose: 'All required environment variables with placeholder values' })
files.push({ path: '.gitignore', purpose: 'Gitignore file appropriate for this stack' })

return files
const seen = new Set<string>()
return files.filter((file) => {
const normalizedPath = file.path.trim()
if (!normalizedPath || seen.has(normalizedPath)) return false
seen.add(normalizedPath)
file.path = normalizedPath
return true
})
}

async function createGitHubRepo(
Expand All @@ -88,7 +96,7 @@ async function createGitHubRepo(
body: JSON.stringify({
name: repoName,
description,
private: false,
private: true,
auto_init: false,
}),
})
Expand Down Expand Up @@ -128,7 +136,7 @@ async function pushFileToGitHub(

if (!res.ok) {
const err = (await res.json()) as { message?: string }
console.warn(`[build-app] Failed to push ${path}: ${err.message}`)
throw new Error(`Failed to push ${path}: ${err.message ?? res.statusText}`)
}
}

Expand Down Expand Up @@ -190,7 +198,7 @@ async function pushFileToGitLab(

if (!res.ok) {
const err = (await res.json()) as { message?: string }
console.warn(`[build-app] Failed to push ${path} to GitLab: ${err.message}`)
throw new Error(`Failed to push ${path} to GitLab: ${err.message ?? res.statusText}`)
}
}

Expand All @@ -202,11 +210,13 @@ export async function POST(request: NextRequest) {
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
}

let chargedUserId: string | null = null
let refundMetadata: Record<string, unknown> = {}

try {
const user = await getCurrentUser()
if (!user) {
send({ step: 'error', message: 'Sign in before building an app.' })
controller.close()
return
}

Expand All @@ -216,20 +226,37 @@ export async function POST(request: NextRequest) {
}
if (!hasProAccess(user, sub)) {
send({ step: 'error', message: 'Build This App is available on paid plans. Upgrade to create and push a generated repo.' })
controller.close()
return
}

const body = (await request.json()) as BuildAppRequest
const { platform, repoName, blueprint } = body

if (platform !== 'github' && platform !== 'gitlab') {
send({ step: 'error', message: 'Choose GitHub or GitLab before building an app.' })
return
}
if (!repoName?.trim()) {
send({ step: 'error', message: 'Repository name is required.' })
controller.close()
return
}
if (!blueprint?.name || !Array.isArray(blueprint.existing_files) || !Array.isArray(blueprint.missing_files)) {
send({ step: 'error', message: 'Blueprint details are required.' })
return
}

const cleanRepoName = repoName.trim().replace(/\s+/g, '-').toLowerCase()
refundMetadata = { platform, repoName: cleanRepoName, blueprintName: blueprint.name }

const chargeResult = await deductCredits(user.id, CREDITS.BUILD_APP_COST, 'build_app', refundMetadata)
if (!chargeResult.success) {
send({
step: 'error',
message: chargeResult.error ?? `Build This App requires ${CREDITS.BUILD_APP_COST} credits.`,
})
return
}
chargedUserId = user.id

// Step 1 — determine files to generate
const filesToGenerate = getFilesToGenerate(blueprint)
Expand Down Expand Up @@ -264,12 +291,7 @@ export async function POST(request: NextRequest) {
gitlabBranch = project.default_branch || 'main'
}
} catch (e) {
send({
step: 'error',
message: `Could not create repository: ${e instanceof Error ? e.message : String(e)}. Make sure you are connected to ${platform === 'github' ? 'GitHub' : 'GitLab'}.`,
})
controller.close()
return
throw new Error(`Could not create repository: ${e instanceof Error ? e.message : String(e)}. Make sure you are connected to ${platform === 'github' ? 'GitHub' : 'GitLab'}.`)
}

send({ step: 'repo_created', message: 'Repository created. Generating and pushing files…', repoUrl })
Expand All @@ -279,13 +301,9 @@ export async function POST(request: NextRequest) {
const total = filesToGenerate.length

for (const { path, purpose } of filesToGenerate) {
// Generate this file
let content: string
try {
content = await generateSingleFile(blueprint, path, purpose, user.id)
} catch (e) {
console.warn(`[build-app] Failed to generate ${path}:`, e)
content = `# Error generating ${path}\n# ${e instanceof Error ? e.message : String(e)}\n`
const content = await generateSingleFile(blueprint, path, purpose, user.id)
if (!content.trim()) {
throw new Error(`Generated empty content for ${path}`)
}

// Push to platform
Expand All @@ -312,13 +330,18 @@ export async function POST(request: NextRequest) {
repoUrl,
filesCreated: pushed,
})
chargedUserId = null
} catch (e) {
console.error('[build-app] unhandled error:', e)
controller.enqueue(
encoder.encode(
`data: ${JSON.stringify({ step: 'error', message: 'An unexpected error occurred.' })}\n\n`,
),
)
if (chargedUserId) {
await refundCredits(chargedUserId, CREDITS.BUILD_APP_COST, 'build app failed', refundMetadata).catch((refundError) =>
console.error('[build-app] Failed to refund credits:', refundError),
)
}
send({
step: 'error',
message: e instanceof Error ? e.message : 'An unexpected error occurred.',
})
} finally {
controller.close()
}
Expand Down
51 changes: 23 additions & 28 deletions app/api/generate-scaffold/route.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import { NextRequest, NextResponse } from 'next/server'
import { aiConfigErrorMessage, generateWithGateway, isAiConfigured } from '@/lib/ai-gateway'
import { getCreditBalance, deductCredits, CREDITS } from '@/lib/credits'
import { deductCredits, refundCredits, CREDITS } from '@/lib/credits'
import { getCurrentUser } from '@/lib/auth'
import { getSubscriptionByGithubId, upsertSubscription } from '@/lib/queries'
import { hasProAccess } from '@/lib/pro-access'

export async function POST(request: NextRequest) {
let chargedUserId: string | null = null
let refundMetadata: Record<string, unknown> = {}

try {
if (!isAiConfigured()) {
return NextResponse.json({ error: aiConfigErrorMessage() }, { status: 503 })
}

const { appName, description, technologies, existingFiles, missingFiles, userId } = await request.json()
const { appName, description, technologies, existingFiles, missingFiles } = await request.json()
const user = await getCurrentUser()
if (!user) {
return NextResponse.json({ error: 'Sign in with GitHub to generate scaffolds.' }, { status: 401 })
Expand All @@ -32,20 +35,19 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 })
}

if (userId) {
const currentBalance = await getCreditBalance(userId)
if (currentBalance < CREDITS.SCAFFOLD_COST) {
return NextResponse.json(
{
error: 'Insufficient credits',
required: CREDITS.SCAFFOLD_COST,
available: currentBalance,
message: 'Upgrade to Pro to get unlimited scaffold generation with 3,000 monthly credits.',
},
{ status: 402 },
)
}
refundMetadata = { appName, technologies }
const deductResult = await deductCredits(user.id, CREDITS.SCAFFOLD_COST, 'scaffold', refundMetadata)
if (!deductResult.success) {
return NextResponse.json(
{
error: deductResult.error ?? 'Insufficient credits',
required: CREDITS.SCAFFOLD_COST,
message: 'Upgrade to Pro to get unlimited scaffold generation with 3,000 monthly credits.',
},
{ status: 402 },
)
}
chargedUserId = user.id

const raw = await generateWithGateway({
feature: 'scaffold',
Expand Down Expand Up @@ -120,26 +122,19 @@ Example structure:
throw new Error(`Failed to parse scaffold: ${e instanceof Error ? e.message : 'Invalid JSON'}`)
}

let creditsUsed = 0
if (userId) {
const deductResult = await deductCredits(userId, CREDITS.SCAFFOLD_COST, 'scaffold', {
appName,
technologies,
})

if (deductResult.success) {
creditsUsed = CREDITS.SCAFFOLD_COST
}
}

return NextResponse.json({
success: true,
scaffold,
appName,
creditsUsed,
creditsUsed: CREDITS.SCAFFOLD_COST,
})
} catch (error) {
console.error('[scaffold] Generation error:', error)
if (chargedUserId) {
await refundCredits(chargedUserId, CREDITS.SCAFFOLD_COST, 'scaffold generation failed', refundMetadata).catch((refundError) =>
console.error('[scaffold] Failed to refund credits:', refundError),
)
}
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Failed to generate scaffold' },
{ status: 500 },
Expand Down
Loading
Loading