Skip to content
Open
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
93 changes: 93 additions & 0 deletions app/api/blueprints/[id]/build-plan/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { NextRequest, NextResponse } from 'next/server'
import { getCurrentUser } from '@/lib/auth'
import { generateBuildPlanContent } from '@/lib/build-plan-generation'
import {
getBlueprintById,
getBuildPlanByBlueprintId,
getRepositoriesForAnalysis,
upsertBuildPlan,
} from '@/lib/queries'

export async function GET(
_request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
try {
const user = await getCurrentUser()
if (!user) {
return NextResponse.json({ error: 'Sign in with GitHub to view build plans.' }, { status: 401 })
}

const { id: blueprintId } = await params
const blueprint = await getBlueprintById(blueprintId)
if (!blueprint) {
return NextResponse.json({ error: 'Blueprint not found' }, { status: 404 })
}

const buildPlan = await getBuildPlanByBlueprintId(blueprintId)
if (!buildPlan) {
return NextResponse.json({ buildPlan: null })
}

return NextResponse.json({
buildPlan,
blueprint: { id: blueprint.id, name: blueprint.name },
})
} catch (error) {
console.error('[build-plan] GET error:', error)
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Failed to fetch build plan' },
{ status: 500 },
)
}
}

export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
try {
const user = await getCurrentUser()
if (!user) {
return NextResponse.json({ error: 'Sign in with GitHub to generate build plans.' }, { status: 401 })
}

const { id: blueprintId } = await params
const body = await request.json().catch(() => ({}))
const regenerate = Boolean((body as { regenerate?: boolean }).regenerate)

const blueprint = await getBlueprintById(blueprintId)
if (!blueprint) {
return NextResponse.json({ error: 'Blueprint not found' }, { status: 404 })
}

const existing = await getBuildPlanByBlueprintId(blueprintId)
if (existing && !regenerate) {
return NextResponse.json({
buildPlan: existing,
blueprint: { id: blueprint.id, name: blueprint.name },
cached: true,
})
}

const repositories = await getRepositoriesForAnalysis(blueprint.analysis_id)
const content = await generateBuildPlanContent(blueprint, repositories)
const buildPlan = await upsertBuildPlan({
blueprint_id: blueprintId,
content,
version: existing ? existing.version + 1 : 1,
})

return NextResponse.json({
buildPlan,
blueprint: { id: blueprint.id, name: blueprint.name },
cached: false,
})
} catch (error) {
console.error('[build-plan] POST error:', error)
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Failed to generate build plan' },
{ status: 500 },
)
}
}
13 changes: 13 additions & 0 deletions app/api/setup/init-db/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,19 @@ async function run() {
await sql`CREATE INDEX IF NOT EXISTS idx_analyses_status ON analyses(status)`
await sql`CREATE INDEX IF NOT EXISTS idx_app_blueprints_analysis_id ON app_blueprints(analysis_id)`

await sql`
CREATE TABLE IF NOT EXISTS build_plans (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
blueprint_id UUID NOT NULL REFERENCES app_blueprints(id) ON DELETE CASCADE,
content JSONB NOT NULL DEFAULT '{}'::jsonb,
version INTEGER NOT NULL DEFAULT 1,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
UNIQUE(blueprint_id)
)
`
await sql`CREATE INDEX IF NOT EXISTS idx_build_plans_blueprint_id ON build_plans(blueprint_id)`

return NextResponse.json({ success: true, message: 'Database schema initialized successfully.' })
} catch (err) {
console.error('[setup] DB init failed:', err)
Expand Down
24 changes: 24 additions & 0 deletions components/analysis-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ import {
Download,
Lock,
Crown,
Hammer,
} from 'lucide-react'
import type { Analysis, Repository, AppBlueprint } from '@/lib/queries'
import { BuildThisApp } from '@/components/build-this-app'
import {
getBlueprintTier,
tierCopy,
Expand Down Expand Up @@ -78,6 +80,7 @@ export function AnalysisDetail({
const isFreePlan = userPlan === 'free' && !isTrialing
const viewLimit = blueprintLimit > 0 ? blueprintLimit : Infinity
const [scaffoldLoadingId, setScaffoldLoadingId] = useState<string | null>(null)
const [buildAppBlueprintId, setBuildAppBlueprintId] = useState<string | null>(null)
const [isRunning, setIsRunning] = useState(false)
const [status, setStatus] = useState(analysis.status)
const [progress, setProgress] = useState(
Expand Down Expand Up @@ -606,6 +609,13 @@ export function AnalysisDetail({
)}
</Button>
) : null}
<Button
className={`${blueprint.missing_files.length > 0 ? 'mt-2' : 'mt-4'} w-full`}
onClick={() => setBuildAppBlueprintId(blueprint.id)}
>
<Hammer className="h-4 w-4 mr-2" />
Build This App
</Button>
<Button
variant="ghost"
className="mt-2 w-full"
Expand All @@ -632,6 +642,20 @@ export function AnalysisDetail({
</div>
)}
</section>

{buildAppBlueprintId && (() => {
const selectedBlueprint = localBlueprints.find((b) => b.id === buildAppBlueprintId)
if (!selectedBlueprint) return null
return (
<BuildThisApp
blueprint={selectedBlueprint}
open={!!buildAppBlueprintId}
onOpenChange={(open) => {
if (!open) setBuildAppBlueprintId(null)
}}
/>
)
})()}
</div>
)
}
Loading