← back to Letsbegin
app/api/prd/generate/route.ts
166 lines
import { NextRequest, NextResponse } from 'next/server'
import { logGemini } from '@/lib/cost-track'
import fs from 'fs'
import path from 'path'
import { trackEvent } from '@/lib/sheets'
import { autoCommit } from '@/lib/git'
// Gemini API configuration (FREE - no credits needed!)
const GEMINI_API_KEY = process.env.GEMINI_API_KEY
if (!GEMINI_API_KEY) {
throw new Error('GEMINI_API_KEY environment variable is required')
}
const GEMINI_ENDPOINT = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent'
async function callGemini(prompt: string): Promise<string> {
const response = await fetch(`${GEMINI_ENDPOINT}?key=${GEMINI_API_KEY}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{ parts: [{ text: prompt }] }],
generationConfig: { temperature: 0.7, maxOutputTokens: 4000 }
})
})
if (!response.ok) {
const error = await response.text()
throw new Error(`Gemini API error: ${error}`)
}
const data = await response.json()
logGemini(data, { note: 'prd-generate' })
return data.candidates?.[0]?.content?.parts?.[0]?.text || ''
}
interface PRDInput {
projectName: string
featureTitle: string
problemStatement: string
targetUser: string
scope: string
priority: string
keyFeatures: string
nonGoals: string
successMetrics: string
components: string[]
integrations: string
techStack: string
additionalAnswers: string
projectPath?: string
}
const targetUserMap: Record<string, string> = {
'A': 'New users',
'B': 'Existing users',
'C': 'Admin users',
'D': 'All users'
}
const scopeMap: Record<string, string> = {
'A': 'Minimal viable implementation',
'B': 'Full-featured implementation',
'C': 'Backend/API only',
'D': 'Frontend/UI only'
}
const priorityMap: Record<string, string> = {
'A': 'Critical (ASAP)',
'B': 'High priority',
'C': 'Medium priority',
'D': 'Low priority (nice-to-have)'
}
export async function POST(request: NextRequest) {
try {
let input: PRDInput
try {
input = await request.json()
} catch {
return NextResponse.json({ error: 'Invalid JSON in request body' }, { status: 400 })
}
const prompt = `Generate a detailed Product Requirements Document (PRD) for the following feature:
**Project**: ${input.projectName}
**Feature Title**: ${input.featureTitle}
**Problem Statement**: ${input.problemStatement}
**Clarifying Questions Answered**:
- Q1 (Target User): ${targetUserMap[input.targetUser] || input.targetUser}
- Q2 (Scope): ${scopeMap[input.scope] || input.scope}
- Q3 (Priority): ${priorityMap[input.priority] || input.priority}
**Key Features/Requirements**:
${input.keyFeatures || 'Not specified'}
**Non-Goals (Out of Scope)**:
${input.nonGoals || 'Not specified'}
**Success Metrics**:
${input.successMetrics || 'Not specified'}
**Technical Context**:
- Tech Stack: ${input.techStack}
- Integrations: ${input.integrations || 'None specified'}
**Additional Notes**:
${input.additionalAnswers || 'None'}
Generate a PRD with the following sections:
1. Introduction/Overview
2. Goals (bullet list)
3. User Stories (format: US-001, US-002, etc. with Description and Acceptance Criteria)
4. Functional Requirements (FR-1, FR-2, etc.)
5. Non-Goals
6. Design Considerations
7. Technical Considerations
8. Success Metrics
9. Open Questions
IMPORTANT:
- Each user story should be small enough to implement in ONE focused session
- Acceptance criteria must be VERIFIABLE (not vague like "works correctly")
- For UI stories, include "Verify in browser using dev-browser skill" as acceptance criterion
- Always include "Typecheck passes" as acceptance criterion
- Write for junior developers or AI agents - be explicit, avoid jargon
Format as markdown.`
const prd = await callGemini(prompt)
// Save to file if project path provided
let prdFilePath = ''
if (input.projectPath && prd) {
const tasksDir = path.join(input.projectPath, 'tasks')
if (!fs.existsSync(tasksDir)) {
fs.mkdirSync(tasksDir, { recursive: true })
}
const kebabTitle = input.featureTitle.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '')
prdFilePath = path.join(tasksDir, `prd-${kebabTitle}.md`)
fs.writeFileSync(prdFilePath, prd)
// Track the event
await trackEvent({
timestamp: new Date().toISOString(),
project: input.projectName,
step: 'prd_generated',
details: `Generated PRD: ${input.featureTitle}`,
status: 'success'
})
// Auto-commit the PRD
const commitResult = await autoCommit(
input.projectPath,
`docs: Add PRD for ${input.featureTitle}`,
[prdFilePath]
)
console.log('Auto-commit result:', commitResult)
}
return NextResponse.json({ prd, prdFilePath, success: true })
} catch (error) {
console.error('Failed to generate PRD:', error)
return NextResponse.json({ error: 'Failed to generate PRD' }, { status: 500 })
}
}