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: 60 additions & 58 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,47 @@ interface AppSuggestion {

export async function POST(request: NextRequest) {
try {
const { analysisId, selectedRepos, userId } = (await request.json()) as {
const user = await getCurrentUser()
if (!user?.id) {
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 (!analysisId || !Array.isArray(selectedRepos)) {
return NextResponse.json({ error: 'analysisId and selectedRepos are required' }, { status: 400 })
}

const currentBalance = await getCreditBalance(userId)
if (currentBalance < CREDITS.ANALYSIS_COST) {
const deductResult = await deductCredits(user.id, CREDITS.ANALYSIS_COST, 'analysis', {
analysisId,
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[]> = {}
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
}
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 +83,43 @@ 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, 'Analysis failed', {
analysisId,
originalTransactionId: deductResult.transaction?.id,
}).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
24 changes: 20 additions & 4 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 @@ -59,13 +59,17 @@ function parseAppIdeaChatResponse(raw: string): AppIdeaChatResponse {
}

export async function POST(request: NextRequest) {
let chargedUserId: string | undefined
let chargedTransactionId: string | undefined
let chargedAnalysisId: string | undefined

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

const user = await getCurrentUser()
if (!user) {
if (!user?.id) {
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 })
}

Expand Down Expand Up @@ -96,6 +100,9 @@ export async function POST(request: NextRequest) {
if (!creditResult.success) {
return NextResponse.json({ error: creditResult.error || 'Insufficient credits' }, { status: 402 })
}
chargedUserId = user.id
chargedTransactionId = creditResult.transaction?.id
chargedAnalysisId = analysisId

let codebaseContext = ''
if (analysisId) {
Expand Down Expand Up @@ -198,13 +205,14 @@ Always respond with valid JSON only (no markdown fences):
try {
parsed = parseAppIdeaChatResponse(raw)
} catch {
return NextResponse.json({ error: 'Failed to parse AI response' }, { status: 500 })
throw new Error('Failed to parse AI response')
}

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

chargedUserId = undefined
return NextResponse.json({
reply: parsed.reply,
suggestions: Array.isArray(parsed.suggestions) ? parsed.suggestions : [],
Expand All @@ -216,6 +224,14 @@ Always respond with valid JSON only (no markdown fences):
if (error instanceof Error) {
console.error('[v0] Stack:', error.stack)
}
if (chargedUserId) {
await refundCredits(chargedUserId, CREDITS.PATTERN_ANALYZER_COST, 'App Idea Chat failed', {
analysisId: chargedAnalysisId,
originalTransactionId: chargedTransactionId,
}).catch((refundError) => {
console.error('[v0] Failed to refund app idea chat credits:', refundError)
})
}
return NextResponse.json({ error: errorMsg }, { status: 500 })
}
}
Loading
Loading