← 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 })
  }
}