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
6 changes: 4 additions & 2 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,
deleteBlueprintsByIds,
getBlueprintsByAnalysis,
getSubscriptionByGithubId,
upsertSubscription,
Expand Down Expand Up @@ -191,10 +191,10 @@ export async function POST(
controller.close()
return
}
const existingBlueprintIds = (await getBlueprintsByAnalysis(id)).map((blueprint) => blueprint.id)

// 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,6 +430,8 @@ Constraints:
ai_explanation: bp.explanation,
})
}

await deleteBlueprintsByIds(existingBlueprintIds)
}

// Update to complete
Expand Down
30 changes: 29 additions & 1 deletion app/api/app-idea-chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from '@/lib/queries'
import { getAnthropicModel } from '@/lib/anthropic-model'
import { getCurrentUser } from '@/lib/auth'
import { deductCredits, CREDITS } from '@/lib/credits'
import { deductCredits, refundCredits, CREDITS } from '@/lib/credits'

let __anthropicClient: Anthropic | null = null
function getAnthropic(): Anthropic {
Expand Down Expand Up @@ -44,7 +44,24 @@ export interface ChatMessage {
content: string
}

async function refundFailedChatCredits(
userId: string,
analysisId: string | undefined,
reason: string,
) {
try {
await refundCredits(userId, CREDITS.PATTERN_ANALYZER_COST, reason, {
analysisId,
feature: 'app_idea_chat',
})
} catch (error) {
console.error('[app-idea-chat] failed to refund credits:', error)
}
}

export async function POST(request: NextRequest) {
let refundContext: { userId: string; analysisId?: string } | null = null

try {
const user = await getCurrentUser()
if (!user) {
Expand All @@ -70,6 +87,7 @@ export async function POST(request: NextRequest) {
if (!creditResult.success) {
return NextResponse.json({ error: creditResult.error || 'Insufficient credits' }, { status: 402 })
}
refundContext = { userId: user.id, analysisId }

// Optionally load codebase context
let codebaseContext = ''
Expand Down Expand Up @@ -164,12 +182,22 @@ Always respond with valid JSON only (no markdown fences):
try {
parsed = JSON.parse(jsonText)
} catch {
await refundFailedChatCredits(user.id, analysisId, 'App Idea Chat returned an invalid AI response')
refundContext = null
return NextResponse.json({ error: 'Failed to parse AI response' }, { status: 500 })
}

refundContext = null
return NextResponse.json(parsed)
} catch (error) {
console.error('[app-idea-chat] error:', error)
if (refundContext) {
await refundFailedChatCredits(
refundContext.userId,
refundContext.analysisId,
'App Idea Chat failed before completing',
)
}
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}
3 changes: 2 additions & 1 deletion app/api/auth/github/callback/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { cookies } from 'next/headers'
import { getDb } from '@/lib/db'
import { GITHUB_ACCESS_TOKEN_COOKIE } from '@/lib/auth'
import { upsertSubscription } from '@/lib/queries'
import { DEFAULT_GITHUB_RETURN_TO, getSafeRedirectPath } from '@/lib/redirects'

function getBaseUrl(request: NextRequest) {
return process.env.NEXT_PUBLIC_APP_URL || request.nextUrl.origin
Expand All @@ -21,7 +22,7 @@ export async function GET(request: NextRequest) {
const errorDescription = searchParams.get('error_description')
const cookieStore = await cookies()
const savedState = cookieStore.get('github_oauth_state')?.value
const returnTo = cookieStore.get('github_oauth_return_to')?.value || '/dashboard/repositories?connected=github'
const returnTo = getSafeRedirectPath(cookieStore.get('github_oauth_return_to')?.value, DEFAULT_GITHUB_RETURN_TO)

if (error) {
console.error('[v0] GitHub returned OAuth error:', error, errorDescription)
Expand Down
3 changes: 2 additions & 1 deletion app/api/auth/github/login/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import crypto from 'node:crypto'
import { NextRequest, NextResponse } from 'next/server'
import { DEFAULT_GITHUB_RETURN_TO, getSafeRedirectPath } from '@/lib/redirects'

function getBaseUrl(request: NextRequest) {
return process.env.NEXT_PUBLIC_APP_URL || request.nextUrl.origin
Expand All @@ -18,7 +19,7 @@ export async function GET(request: NextRequest) {

const state = crypto.randomUUID()
const redirectUri = `${getBaseUrl(request)}/api/auth/github/callback`
const returnTo = request.nextUrl.searchParams.get('returnTo') || '/dashboard/repositories?connected=github'
const returnTo = getSafeRedirectPath(request.nextUrl.searchParams.get('returnTo'), DEFAULT_GITHUB_RETURN_TO)

const params = new URLSearchParams({
client_id: clientId,
Expand Down
36 changes: 30 additions & 6 deletions app/api/pattern-analyzer/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from '@/lib/queries'
import { getAnthropicModel } from '@/lib/anthropic-model'
import { getCurrentUser } from '@/lib/auth'
import { deductCredits, CREDITS } from '@/lib/credits'
import { deductCredits, refundCredits, CREDITS } from '@/lib/credits'

let __anthropicClient: Anthropic | null = null
function getAnthropic(): Anthropic {
Expand Down Expand Up @@ -41,7 +41,20 @@ export interface PatternAnalyzerResult {
analysisId: string
}

async function refundFailedPatternCredits(userId: string, analysisId: string, reason: string) {
try {
await refundCredits(userId, CREDITS.PATTERN_ANALYZER_COST, reason, {
analysisId,
feature: 'pattern_analyzer',
})
} catch (error) {
console.error('[pattern-analyzer] failed to refund credits:', error)
}
}

export async function POST(request: NextRequest) {
let refundContext: { userId: string; analysisId: string } | null = null

try {
const user = await getCurrentUser()
if (!user) {
Expand All @@ -54,11 +67,6 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: 'analysisId is required' }, { status: 400 })
}

const creditResult = await deductCredits(user.id, CREDITS.PATTERN_ANALYZER_COST, 'pattern_analyzer', { analysisId })
if (!creditResult.success) {
return NextResponse.json({ error: creditResult.error || 'Insufficient credits' }, { status: 402 })
}

const analysis = await getAnalysisById(analysisId)
if (!analysis) {
return NextResponse.json({ error: 'Analysis not found' }, { status: 404 })
Expand Down Expand Up @@ -163,6 +171,12 @@ Respond ONLY with a valid JSON object (no markdown fences) matching this exact s
]
}`

const creditResult = await deductCredits(user.id, CREDITS.PATTERN_ANALYZER_COST, 'pattern_analyzer', { analysisId })
if (!creditResult.success) {
return NextResponse.json({ error: creditResult.error || 'Insufficient credits' }, { status: 402 })
}
refundContext = { userId: user.id, analysisId }

const response = await getAnthropic().messages.create({
model: getAnthropicModel(),
max_tokens: 4096,
Expand All @@ -178,6 +192,8 @@ Respond ONLY with a valid JSON object (no markdown fences) matching this exact s
try {
parsed = JSON.parse(jsonText)
} catch {
await refundFailedPatternCredits(user.id, analysisId, 'Pattern Analyzer returned an invalid AI response')
refundContext = null
return NextResponse.json({ error: 'Failed to parse AI response' }, { status: 500 })
}

Expand All @@ -188,9 +204,17 @@ Respond ONLY with a valid JSON object (no markdown fences) matching this exact s
analysisId,
}

refundContext = null
return NextResponse.json(result)
} catch (error) {
console.error('[pattern-analyzer] error:', error)
if (refundContext) {
await refundFailedPatternCredits(
refundContext.userId,
refundContext.analysisId,
'Pattern Analyzer failed before completing',
)
}
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}
Loading
Loading