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
48 changes: 23 additions & 25 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,36 @@ 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 {
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?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

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 +101,7 @@ 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

return NextResponse.json({
analysisId,
Expand All @@ -119,6 +112,11 @@ Return as JSON array of app suggestions. Focus on practical, buildable applicati
creditsRemaining: newBalance,
})
} catch (error) {
if (chargedUserId) {
await refundCredits(chargedUserId, CREDITS.ANALYSIS_COST, 'Legacy analysis failed', chargeMetadata).catch(
(refundError) => console.error('Failed to refund analysis credits:', refundError),
)
}
console.error('Analysis error:', error)
return NextResponse.json({ error: 'Failed to analyze repositories' }, { status: 500 })
}
Expand Down
41 changes: 27 additions & 14 deletions app/api/analyses/[id]/run/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import {
updateAnalysisStatus,
createRepoFile,
createBlueprint,
deleteBlueprintsByAnalysis,
deleteBlueprintsByAnalysisExcept,
deleteBlueprintsByIds,
getBlueprintsByAnalysis,
getSubscriptionByGithubId,
upsertSubscription,
Expand Down Expand Up @@ -144,6 +145,7 @@ export async function POST(
const send = (data: object) => {
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
}
const replacementBlueprintIds: string[] = []

try {
const accessToken = await getCurrentAccessToken()
Expand All @@ -169,7 +171,12 @@ export async function POST(
if (!sub) {
sub = await upsertSubscription({ github_id: user.github_id }).catch(() => null)
}
if (isOnFreeTier(user, sub) && sub) {
if (!sub) {
send({ error: 'Unable to initialize billing usage. Please try again.', status: 'failed' })
controller.close()
return
}
if (isOnFreeTier(user, sub)) {
const limit = PLANS.free.analyses_per_month
if (sub.analyses_used_this_month >= limit) {
send({ error: `You've reached your free plan limit of ${limit} analyses per month. Upgrade to Pro for unlimited analyses.`, status: 'failed' })
Expand Down Expand Up @@ -200,7 +207,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 +399,12 @@ For each app blueprint:

// Check if response was truncated (hit max_tokens)
if (aiResponse.stop_reason === 'max_tokens') {
const msg = 'AI response was cut short (output too large). Try running with fewer repositories selected.'
console.warn('[analysis] AI response truncated (max_tokens). Tool output may be incomplete.')
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 +422,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 @@ -431,7 +439,7 @@ For each app blueprint:
.sort((a, b) => getOpportunityScore(b) - getOpportunityScore(a))

for (const bp of rankedBlueprints) {
await createBlueprint({
const created = await createBlueprint({
analysis_id: id,
user_id: user.id,
name: bp.name.slice(0, 255),
Expand All @@ -445,16 +453,16 @@ For each app blueprint:
technologies: bp.technologies,
ai_explanation: bp.explanation,
})
replacementBlueprintIds.push(created.id)
}
}

await incrementAnalysisUsage(user.github_id)
await deleteBlueprintsByAnalysisExcept(id, replacementBlueprintIds)

// Update to complete
await updateAnalysisStatus(id, 'complete', { analyzed_files: allFiles.length })

await incrementAnalysisUsage(user.github_id).catch((e) =>
console.error('[analysis] Failed to increment usage:', e)
)

// Get final blueprints and hydrate recurring-value surfaces from them.
const finalBlueprints = await getBlueprintsByAnalysis(id, user.id)
for (const blueprint of finalBlueprints) {
Expand All @@ -468,6 +476,11 @@ For each app blueprint:
} catch (error) {
console.error('Analysis error:', error)
const errorDetail = error instanceof Error ? error.message : 'Unknown error'
try {
await deleteBlueprintsByIds(replacementBlueprintIds)
} catch (dbErr) {
console.error('Failed to clean up partial replacement blueprints:', dbErr)
}
try {
await updateAnalysisStatus(id, 'failed', { error_message: errorDetail })
} catch (dbErr) {
Expand Down
6 changes: 6 additions & 0 deletions app/api/analyze/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,15 @@ import { NextResponse } from 'next/server'
import { generateText } from 'ai'
import { scanCrossPlatformCode } from '@/lib/cross-platform-scanner'
import { analyzeScannedFiles, createAnthropicPromptRunner } from '@/lib/repofuse-core.js'
import { getCurrentUser } from '@/lib/auth'

export async function POST() {
try {
const user = await getCurrentUser()
if (!user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

const scannedFiles = await scanCrossPlatformCode()

if (scannedFiles.length === 0) {
Expand Down
Loading
Loading