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
52 changes: 29 additions & 23 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 chargeMetadata: Record<string, unknown> = {}

try {
const { analysisId, selectedRepos, userId } = (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?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
if (userId && userId !== user.id) {
return NextResponse.json({ error: 'Cannot charge credits for a different user.' }, { status: 403 })
}

const currentBalance = await getCreditBalance(userId)
if (currentBalance < CREDITS.ANALYSIS_COST) {
chargeMetadata = {
analysisId,
selectedRepos: selectedRepos.map((r) => r.name),
}
const creditResult = await deductCredits(user.id, CREDITS.ANALYSIS_COST, 'analysis', chargeMetadata)
if (!creditResult.success) {
return NextResponse.json(
{
error: 'Insufficient credits',
error: creditResult.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,21 +105,8 @@ 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
const newBalance = creditResult.transaction?.balance_after || 0
chargedUserId = null

return NextResponse.json({
analysisId,
Expand All @@ -120,6 +118,14 @@ 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', {
...chargeMetadata,
error: error instanceof Error ? error.message : String(error),
}).catch((refundError) => {
console.error('Failed to refund credits after legacy analysis failure:', refundError)
})
}
return NextResponse.json({ error: 'Failed to analyze repositories' }, { status: 500 })
}
}
Expand Down
63 changes: 39 additions & 24 deletions app/api/analyses/[id]/run/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
updateAnalysisStatus,
createRepoFile,
createBlueprint,
deleteBlueprintsByAnalysis,
deleteBlueprintByIdForAnalysis,
getBlueprintsByAnalysis,
getSubscriptionByGithubId,
upsertSubscription,
Expand Down Expand Up @@ -190,6 +190,7 @@ export async function POST(
controller.close()
return
}
const previousBlueprints = await getBlueprintsByAnalysis(id, user.id)

const repositories = await getRepositoriesForAnalysis(id, user.id)
if (repositories.length === 0) {
Expand All @@ -200,7 +201,6 @@ export async function POST(

// Update status to scanning
await updateAnalysisStatus(id, 'scanning')
await deleteBlueprintsByAnalysis(id)
send({ status: 'scanning', progress: 10 })

// Fetch file trees from GitHub for each repository
Expand Down Expand Up @@ -393,7 +393,12 @@ For each app blueprint:

// Check if response was truncated (hit max_tokens)
if (aiResponse.stop_reason === 'max_tokens') {
console.warn('[analysis] AI response truncated (max_tokens). Tool output may be incomplete.')
const msg = 'AI response was cut short (output too large). Try running with fewer repositories selected.'
console.warn('[analysis] AI response truncated (max_tokens). Refusing to replace existing blueprints.')
send({ status: 'failed', error: msg })
await updateAnalysisStatus(id, 'failed', { error_message: msg })
controller.close()
return
}

// Extract structured output from tool use response
Expand All @@ -411,12 +416,9 @@ For each app blueprint:
const blueprintsFromAI = parseBlueprints(rawInput)

if (blueprintsFromAI.length === 0) {
const wasMaxTokens = aiResponse.stop_reason === 'max_tokens'
const msg = wasMaxTokens
? 'AI response was cut short (output too large). Try running with fewer repositories selected.'
: rawInput
? 'AI returned empty results. Try running the analysis again — this can happen intermittently.'
: 'Model did not return usable blueprints (missing tool output). Check ANTHROPIC_API_KEY and model availability.'
const msg = rawInput
? 'AI returned empty results. Try running the analysis again — this can happen intermittently.'
: 'Model did not return usable blueprints (missing tool output). Check ANTHROPIC_API_KEY and model availability.'
console.error('[analysis] No valid blueprints.', { stop_reason: aiResponse.stop_reason, rawInput: JSON.stringify(rawInput).slice(0, 500) })
send({ status: 'failed', error: msg })
await updateAnalysisStatus(id, 'failed', { error_message: msg })
Expand All @@ -430,21 +432,34 @@ For each app blueprint:
.map((bp) => normalizeBlueprint(bp))
.sort((a, b) => getOpportunityScore(b) - getOpportunityScore(a))

for (const bp of rankedBlueprints) {
await createBlueprint({
analysis_id: id,
user_id: user.id,
name: bp.name.slice(0, 255),
description: bp.description,
app_type: bp.app_type?.slice(0, 100) ?? null,
complexity: bp.complexity,
reuse_percentage: bp.reuse_percentage,
existing_files: bp.existing_files,
missing_files: bp.missing_files,
estimated_effort: getEffortEstimate(bp.complexity, bp.missing_files.length),
technologies: bp.technologies,
ai_explanation: bp.explanation,
})
const createdBlueprintIds: string[] = []
try {
for (const bp of rankedBlueprints) {
const created = await createBlueprint({
analysis_id: id,
user_id: user.id,
name: bp.name.slice(0, 255),
description: bp.description,
app_type: bp.app_type?.slice(0, 100) ?? null,
complexity: bp.complexity,
reuse_percentage: bp.reuse_percentage,
existing_files: bp.existing_files,
missing_files: bp.missing_files,
estimated_effort: getEffortEstimate(bp.complexity, bp.missing_files.length),
technologies: bp.technologies,
ai_explanation: bp.explanation,
})
createdBlueprintIds.push(created.id)
}

for (const blueprint of previousBlueprints) {
await deleteBlueprintByIdForAnalysis(blueprint.id, id)
}
} catch (error) {
await Promise.allSettled(
createdBlueprintIds.map((blueprintId) => deleteBlueprintByIdForAnalysis(blueprintId, id)),
)
throw error
}
}

Expand Down
Loading
Loading