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
130 changes: 70 additions & 60 deletions app/api/analyses/[id]/analyze/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
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'
import { getAnalysisById } from '@/lib/queries'

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

Expand All @@ -19,43 +21,57 @@ interface AppSuggestion {
is_complete?: boolean
}

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

const { selectedRepos } = (await request.json()) as {
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 analysis = await getAnalysisById(id, user.id)
if (!analysis) {
return NextResponse.json({ error: 'Analysis not found' }, { status: 404 })
}

const currentBalance = await getCreditBalance(userId)
if (currentBalance < CREDITS.ANALYSIS_COST) {
const deductResult = await deductCredits(user.id, CREDITS.ANALYSIS_COST, 'analysis', {
analysisId: id,
selectedRepos: selectedRepos.map((r) => r.name),
})
if (!deductResult.success) {
return NextResponse.json(
{
error: 'Insufficient credits',
required: CREDITS.ANALYSIS_COST,
available: currentBalance,
message: 'Upgrade to Pro to get unlimited analyses with 3,000 monthly credits.',
message: deductResult.error || '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[]> = {}

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.
// 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 +92,42 @@ 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),
})
const newBalance = deductResult.transaction?.balance_after || 0

if (!deductResult.success) {
console.error('Failed to deduct credits:', deductResult.error)
return NextResponse.json(
{ error: 'Failed to process analysis' },
{ status: 500 }
)
return NextResponse.json({
analysisId: id,
suggestions,
totalSuggestions: suggestions.length,
completeSuggestions: suggestions.filter((suggestion) => suggestion.is_complete).length,
creditsUsed: CREDITS.ANALYSIS_COST,
creditsRemaining: newBalance,
})
} catch (e) {
await refundCredits(user.id, CREDITS.ANALYSIS_COST, 'Legacy analysis failed', {
analysisId: id,
}).catch((refundError) => {
console.error('Failed to refund analysis credits:', refundError)
})
throw e
}

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
41 changes: 26 additions & 15 deletions app/api/analyses/[id]/run/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
createRepoFile,
createBlueprint,
deleteBlueprintsByAnalysis,
deleteBlueprintsByIds,
getBlueprintsByAnalysis,
getSubscriptionByGithubId,
upsertSubscription,
Expand Down Expand Up @@ -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 @@ -430,21 +430,32 @@ 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 replacementBlueprintIds: string[] = []
try {
for (const bp of rankedBlueprints) {
const blueprint = 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,
})
replacementBlueprintIds.push(blueprint.id)
}

await deleteBlueprintsByAnalysis(id, user.id, replacementBlueprintIds)
} catch (e) {
await deleteBlueprintsByIds(replacementBlueprintIds, user.id).catch((cleanupError) => {
console.error('[analysis] Failed to clean up partial replacement blueprints:', cleanupError)
})
throw e
}
}

Expand Down
53 changes: 31 additions & 22 deletions app/api/app-idea-chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
getFilesByRepository,
} from '@/lib/queries'
import { getCurrentUser } from '@/lib/auth'
import { deductCredits, CREDITS } from '@/lib/credits'
import { deductCredits, refundCredits, CREDITS } from '@/lib/credits'
import type { AppIdeaChatResponse, ChatMessage } from '@/lib/app-idea-chat-types'
import { aiConfigErrorMessage, generateWithGateway, isAiConfigured } from '@/lib/ai-gateway'

Expand Down Expand Up @@ -186,30 +186,39 @@ Always respond with valid JSON only (no markdown fences):
{ role: 'user', content: message.trim() },
])

const raw = await generateWithGateway({
system: systemPrompt,
messages,
maxOutputTokens: 2048,
userId: user.id,
feature: 'app-idea-chat',
})

let parsed: AppIdeaChatResponse
try {
parsed = parseAppIdeaChatResponse(raw)
} catch {
return NextResponse.json({ error: 'Failed to parse AI response' }, { status: 500 })
}
const raw = await generateWithGateway({
system: systemPrompt,
messages,
maxOutputTokens: 2048,
userId: user.id,
feature: 'app-idea-chat',
})

let parsed: AppIdeaChatResponse
try {
parsed = parseAppIdeaChatResponse(raw)
} catch {
throw new Error('Failed to parse AI response')
}

if (!parsed.reply?.trim()) {
return NextResponse.json({ error: 'Empty AI response' }, { status: 500 })
}
if (!parsed.reply?.trim()) {
throw new Error('Empty AI response')
}

return NextResponse.json({
reply: parsed.reply,
suggestions: Array.isArray(parsed.suggestions) ? parsed.suggestions : [],
followUpQuestions: Array.isArray(parsed.followUpQuestions) ? parsed.followUpQuestions : [],
})
return NextResponse.json({
reply: parsed.reply,
suggestions: Array.isArray(parsed.suggestions) ? parsed.suggestions : [],
followUpQuestions: Array.isArray(parsed.followUpQuestions) ? parsed.followUpQuestions : [],
})
} catch (error) {
await refundCredits(user.id, CREDITS.PATTERN_ANALYZER_COST, 'App Idea Chat failed', {
analysisId,
}).catch((refundError) => {
console.error('[v0] Failed to refund app idea chat credits:', refundError)
})
throw error
}
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error)
console.error('[v0] app-idea-chat error:', errorMsg)
Expand Down
Loading
Loading