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
118 changes: 59 additions & 59 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 @@ -21,41 +22,48 @@ interface AppSuggestion {

export async function POST(request: NextRequest) {
try {
const { analysisId, selectedRepos, userId } = (await request.json()) as {
const user = await getCurrentUser()
if (!user) {
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 })
}

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 })
if (!Array.isArray(selectedRepos) || selectedRepos.length === 0) {
return NextResponse.json({ error: 'At least one repository is required' }, { status: 400 })
}

const currentBalance = await getCreditBalance(userId)
if (currentBalance < CREDITS.ANALYSIS_COST) {
const creditMetadata = {
analysisId,
selectedRepos: selectedRepos.map((r) => r.name),
}
const deductResult = await deductCredits(user.id, CREDITS.ANALYSIS_COST, 'analysis', creditMetadata)
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 }
)
}

// Get all repo files from database
const filesByRepo: Record<string, RepositoryTreeFile[]> = {}

for (const repo of selectedRepos) {
// Fetch repo structure from GitHub API
const files = await fetchRepoStructure(repo)
filesByRepo[repo.name] = files
}
try {
// Get all repo files from database
const filesByRepo: Record<string, RepositoryTreeFile[]> = {}

// Use AI to analyze cross-repo patterns
const prompt = `You are an expert software architect analyzing code across multiple repositories.
for (const repo of selectedRepos) {
// Fetch repo structure from GitHub API
const files = await fetchRepoStructure(repo)
filesByRepo[repo.name] = files
}

// Use AI to analyze cross-repo patterns
const prompt = `You are an expert software architect analyzing code across multiple repositories.

Given these repositories with their file structures:
${Object.entries(filesByRepo).map(([name, files]) =>
Expand All @@ -76,48 +84,40 @@ Your task is to discover what applications could be built by combining files fro

Return as JSON array of app suggestions. Focus on practical, buildable applications.`

const result = await generateText({
model,
prompt,
temperature: 0.7,
maxOutputTokens: 2000,
})

// Parse AI response and save suggestions
let suggestions: AppSuggestion[] = []
try {
const jsonMatch = result.text.match(/\[[\s\S]*\]/)
if (jsonMatch) {
suggestions = JSON.parse(jsonMatch[0])
const result = await generateText({
model,
prompt,
temperature: 0.7,
maxOutputTokens: 2000,
})

// Parse AI response and save suggestions
let suggestions: AppSuggestion[] = []
try {
const jsonMatch = result.text.match(/\[[\s\S]*\]/)
if (jsonMatch) {
suggestions = JSON.parse(jsonMatch[0])
}
} catch (e) {
console.error('Failed to parse AI response:', e)
}
} catch (e) {
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({
analysisId,
suggestions,
totalSuggestions: suggestions.length,
completeSuggestions: suggestions.filter((suggestion) => suggestion.is_complete).length,
creditsUsed: CREDITS.ANALYSIS_COST,
creditsRemaining: newBalance,
})
} catch (error) {
await refundCredits(user.id, CREDITS.ANALYSIS_COST, 'legacy analysis failed', creditMetadata).catch((refundError) => {
console.error('Failed to refund analysis credits:', refundError)
})
throw error
}

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

return NextResponse.json({
analysisId,
suggestions,
totalSuggestions: suggestions.length,
completeSuggestions: suggestions.filter((suggestion) => suggestion.is_complete).length,
creditsUsed: CREDITS.ANALYSIS_COST,
creditsRemaining: newBalance,
})
} catch (error) {
console.error('Analysis error:', error)
return NextResponse.json({ error: 'Failed to analyze repositories' }, { status: 500 })
Expand Down
62 changes: 31 additions & 31 deletions app/api/analyses/[id]/run/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ import {
getRepositoriesForAnalysis,
updateAnalysisStatus,
createRepoFile,
createBlueprint,
deleteBlueprintsByAnalysis,
replaceBlueprintsForAnalysis,
getBlueprintsByAnalysis,
getSubscriptionByGithubId,
upsertSubscription,
Expand Down Expand Up @@ -169,7 +168,12 @@ export async function POST(
if (!sub) {
sub = await upsertSubscription({ github_id: user.github_id }).catch(() => null)
}
if (isOnFreeTier(user, sub) && sub) {
if (isOnFreeTier(user, sub)) {
if (!sub) {
send({ error: 'Could not verify your free plan usage. Please try again later.', status: 'failed' })
controller.close()
return
}
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 @@ -198,9 +202,8 @@ export async function POST(
return
}

// Update status to scanning
// Update status to scanning. Keep existing blueprints visible until replacements are ready.
await updateAnalysisStatus(id, 'scanning')
await deleteBlueprintsByAnalysis(id)
send({ status: 'scanning', progress: 10 })

// Fetch file trees from GitHub for each repository
Expand Down Expand Up @@ -424,36 +427,33 @@ For each app blueprint:
return
}

// Save blueprints to database
{
const rankedBlueprints = blueprintsFromAI
.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 rankedBlueprints = blueprintsFromAI
.map((bp) => normalizeBlueprint(bp))
.sort((a, b) => getOpportunityScore(b) - getOpportunityScore(a))

await replaceBlueprintsForAnalysis(
id,
user.id,
rankedBlueprints.map((bp) => ({
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,
})),
)

// 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)
)
if (isOnFreeTier(user, sub)) {
await incrementAnalysisUsage(user.github_id)
}

// Get final blueprints and hydrate recurring-value surfaces from them.
const finalBlueprints = await getBlueprintsByAnalysis(id, user.id)
Expand Down
Loading
Loading