Skip to content
Merged
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
166 changes: 166 additions & 0 deletions app/api/app-idea-chat/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { NextRequest, NextResponse } from 'next/server'
import { Anthropic } from '@anthropic-ai/sdk'
import {
getAnalysisById,
getRepositoriesForAnalysis,
getBlueprintsByAnalysis,
getFilesByRepository,
} from '@/lib/queries'
import { getAnthropicModel } from '@/lib/anthropic-model'
import { getCurrentUser } from '@/lib/auth'
import { deductCredits, CREDITS } from '@/lib/credits'

const anthropic = new Anthropic()

export interface AppIdeaSuggestion {
name: string
tagline: string
description: string
type: string
difficulty: 'easy' | 'medium' | 'hard'
estimatedEffort: string
suggestedStack: string[]
monetizationAngle: string
whyNow: string
}

export interface AppIdeaChatResponse {
reply: string
suggestions: AppIdeaSuggestion[]
followUpQuestions: string[]
}

export interface ChatMessage {
role: 'user' | 'assistant'
content: string
}

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

const { message, analysisId, history = [] } = (await request.json()) as {
message: string
analysisId?: string
history?: ChatMessage[]
}

if (!message?.trim()) {
return NextResponse.json({ error: 'Message 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 })
}

// Optionally load codebase context
let codebaseContext = ''
if (analysisId) {
try {
const analysis = await getAnalysisById(analysisId)
if (analysis && analysis.status === 'complete') {
const [repositories, blueprints] = await Promise.all([
getRepositoriesForAnalysis(analysisId),
getBlueprintsByAnalysis(analysisId),
])

const allFiles = (
await Promise.all(repositories.map((r) => getFilesByRepository(r.id)))
).flat()

const techCount: Record<string, number> = {}
for (const file of allFiles) {
for (const tech of file.technologies) {
techCount[tech] = (techCount[tech] || 0) + 1
}
}
const topTech = Object.entries(techCount)
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
.map(([t]) => t)

codebaseContext = `
## Developer's codebase context
Repositories: ${repositories.map((r) => r.name).join(', ')}
Top technologies: ${topTech.join(', ')}
Total files: ${allFiles.length}
Existing blueprints: ${blueprints.slice(0, 5).map((b) => b.name).join(', ') || 'none yet'}
`
}
} catch {
// Codebase context optional — continue without it
}
}

// Build conversation history for context
const conversationHistory = history.slice(-6).map((m) => ({
role: m.role as 'user' | 'assistant',
content: m.content,
}))

const systemPrompt = `You are an expert product strategist and startup advisor helping developers discover what apps to build. You're having a friendly, concise conversation to help them find the perfect project idea.

${codebaseContext}

When responding:
- Keep your reply conversational and under 100 words
- Suggest 2-4 concrete project ideas tailored to their request${codebaseContext ? ' and their codebase' : ''}
- Ask a relevant follow-up question to refine suggestions
- Be enthusiastic and actionable

Always respond with valid JSON only (no markdown fences):
{
"reply": "conversational response under 100 words",
"suggestions": [
{
"name": "Project Name",
"tagline": "One punchy sentence",
"description": "2-3 sentences",
"type": "SaaS | CLI Tool | API | Dashboard | etc",
"difficulty": "easy | medium | hard",
"estimatedEffort": "e.g. 1–2 weeks",
"suggestedStack": ["tech1", "tech2"],
"monetizationAngle": "How to charge",
"whyNow": "Why this is timely"
}
],
"followUpQuestions": ["Question 1?", "Question 2?"]
}`

const messages: Array<{ role: 'user' | 'assistant'; content: string }> = [
...conversationHistory,
{ role: 'user', content: message },
]

const response = await anthropic.messages.create({
model: getAnthropicModel(),
max_tokens: 2048,
system: systemPrompt,
messages,
})

const raw = response.content[0].type === 'text' ? response.content[0].text.trim() : ''
const jsonText = raw.replace(/^```(?:json)?\s*/i, '').replace(/\s*```\s*$/, '').trim()

let parsed: AppIdeaChatResponse
try {
parsed = JSON.parse(jsonText)
} catch {
return NextResponse.json({ error: 'Failed to parse AI response' }, { status: 500 })
}

return NextResponse.json(parsed)
} catch (error) {
console.error('[app-idea-chat] error:', error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}
11 changes: 7 additions & 4 deletions app/dashboard/analyses/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,22 @@ export default async function AnalysisDetailPage({
getRepositoriesForAnalysis(id),
getBlueprintsByAnalysis(id),
])
} catch {
notFound()
}

if (user) {
if (user) {
try {
const [subscription, viewedIds] = await Promise.all([
getSubscriptionByGithubId(user.github_id),
getUserViewedBlueprintIds(user.id),
])
userPlan = subscription?.plan || 'free'
viewedBlueprintIds = viewedIds
// Check if in trial via Stripe (subscription status = 'trialing')
isTrialing = subscription?.status === 'trialing'
} catch {
// Subscription/views table not available yet — use free defaults
}
} catch {
notFound()
}

if (!analysis) {
Expand Down
2 changes: 1 addition & 1 deletion app/dashboard/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default async function DashboardLayout({
}

return (
<div className="min-h-screen bg-background">
<div className="min-h-screen bg-black text-white">
<DashboardHeader user={user} />
<main className="container mx-auto px-4 sm:px-6 lg:px-8 py-8 md:py-10">
{children}
Expand Down
62 changes: 35 additions & 27 deletions app/dashboard/templates/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { TemplateAssemblyCard } from '@/components/template-assembly-card'
import { CreateTemplateModal } from '@/components/create-template-modal'
import { getAllTemplates, getFeaturedTemplates, type Template } from '@/lib/queries'

export const dynamic = 'force-dynamic'
Expand Down Expand Up @@ -42,29 +43,33 @@ async function TemplateHubContent() {
if (setupRequired || !all.length) {
return (
<div className="space-y-6">
<div className="flex items-center gap-3">
<Link href="/dashboard/analyses">
<Button variant="ghost" size="icon">
<ArrowLeft className="w-4 h-4" />
</Button>
</Link>
<div>
<h1 className="text-3xl font-bold">Template Assembly Hub</h1>
<p className="text-muted-foreground">Pre-built project combinations ready to assemble</p>
<div className="flex items-center justify-between gap-3 flex-wrap">
<div className="flex items-center gap-3">
<Link href="/dashboard/analyses">
<Button variant="ghost" size="icon">
<ArrowLeft className="w-4 h-4" />
</Button>
</Link>
<div>
<h1 className="text-3xl font-bold">Template Assembly Hub</h1>
<p className="text-muted-foreground">Pre-built project combinations ready to assemble</p>
</div>
</div>
<CreateTemplateModal />
</div>

<Card className="p-12 text-center border-2 border-dashed">
<Lightbulb className="w-12 h-12 text-muted-foreground mx-auto mb-4" />
<h2 className="text-2xl font-bold mb-2">No templates available yet</h2>
<h2 className="text-2xl font-bold mb-2">No templates yet</h2>
<p className="text-muted-foreground mb-6 max-w-md mx-auto">
Templates will be generated once you run an analysis on your repositories.
Create a template by combining blueprints from your analyses, or run an analysis to discover new blueprints.
</p>
<Button asChild>
<Link href="/dashboard/analyses">
Run an Analysis
</Link>
</Button>
<div className="flex items-center justify-center gap-3 flex-wrap">
<CreateTemplateModal />
<Button variant="outline" asChild>
<Link href="/dashboard/analyses">Run an Analysis</Link>
</Button>
</div>
</Card>
</div>
)
Expand All @@ -85,18 +90,21 @@ async function TemplateHubContent() {
<div className="space-y-8">
{/* Header */}
<div className="space-y-4">
<div className="flex items-center gap-3">
<Link href="/dashboard/analyses">
<Button variant="ghost" size="icon">
<ArrowLeft className="w-4 h-4" />
</Button>
</Link>
<div>
<h1 className="text-3xl font-bold">Template Assembly Hub</h1>
<p className="text-muted-foreground">
Start building today from code you already have. Pre-configured templates combine your best pieces.
</p>
<div className="flex items-center justify-between gap-3 flex-wrap">
<div className="flex items-center gap-3">
<Link href="/dashboard/analyses">
<Button variant="ghost" size="icon">
<ArrowLeft className="w-4 h-4" />
</Button>
</Link>
<div>
<h1 className="text-3xl font-bold">Template Assembly Hub</h1>
<p className="text-muted-foreground">
Start building today from code you already have. Pre-configured templates combine your best pieces.
</p>
</div>
</div>
<CreateTemplateModal />
</div>

{/* Feature Cards */}
Expand Down
Loading
Loading