← back to Letsbegin

app/api/claude/suggest/route.ts

188 lines

import { NextRequest, NextResponse } from 'next/server'
import { logGemini } from '@/lib/cost-track'
import * as fs from 'fs'
import * as path from 'path'

// 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, maxTokens: number = 500): 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: maxTokens }
    })
  })

  if (!response.ok) {
    const error = await response.text()
    throw new Error(`Gemini API error: ${error}`)
  }

  const data = await response.json()
  logGemini(data, { note: 'claude-suggest' })
  return data.candidates?.[0]?.content?.parts?.[0]?.text || ''
}

// Helper to read project context files
async function getProjectContext(projectName: string): Promise<string> {
  const projectsDir = '/root/Projects'
  const contextParts: string[] = []

  // Find the project directory
  const projectPath = path.join(projectsDir, projectName)
  if (!fs.existsSync(projectPath)) {
    // Try to find it in subdirectories
    const dirs = fs.readdirSync(projectsDir)
    for (const dir of dirs) {
      const subPath = path.join(projectsDir, dir, projectName)
      if (fs.existsSync(subPath)) {
        return getProjectContextFromPath(subPath)
      }
    }
    return ''
  }

  return getProjectContextFromPath(projectPath)
}

function getProjectContextFromPath(projectPath: string): string {
  const contextParts: string[] = []

  // Read CLAUDE.md if exists
  const claudeMdPaths = [
    path.join(projectPath, 'CLAUDE.md'),
    path.join(projectPath, '.claude', 'CLAUDE.md')
  ]

  for (const claudePath of claudeMdPaths) {
    if (fs.existsSync(claudePath)) {
      try {
        const content = fs.readFileSync(claudePath, 'utf-8')
        contextParts.push(`\n### Project Documentation (CLAUDE.md):\n${content.slice(0, 2000)}`)
        break
      } catch {}
    }
  }

  // Read package.json for tech context
  const pkgPath = path.join(projectPath, 'package.json')
  if (fs.existsSync(pkgPath)) {
    try {
      const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
      const deps = Object.keys(pkg.dependencies || {}).slice(0, 10).join(', ')
      contextParts.push(`\n### Tech Stack: ${deps}`)
    } catch {}
  }

  // Look for existing PRD files
  const tasksDirs = [
    path.join(projectPath, 'tasks'),
    path.join(projectPath, 'prd'),
    path.join(projectPath, 'scripts', 'ralph')
  ]

  for (const tasksDir of tasksDirs) {
    if (fs.existsSync(tasksDir)) {
      try {
        const files = fs.readdirSync(tasksDir)
        const prdFiles = files.filter(f => f.endsWith('.md') && f.includes('prd'))
        if (prdFiles.length > 0) {
          const latestPrd = prdFiles[0]
          const prdContent = fs.readFileSync(path.join(tasksDir, latestPrd), 'utf-8')
          contextParts.push(`\n### Previous PRD Example (${latestPrd}):\n${prdContent.slice(0, 1500)}`)
          break
        }
      } catch {}
    }
  }

  return contextParts.join('\n')
}

const fieldPrompts: Record<string, string> = {
  featureTitle: 'Suggest a clear, concise feature title that describes what this feature does.',
  problemStatement: 'Write a clear problem statement explaining what issue this feature solves and why it matters to users.',
  keyFeatures: 'List 3-5 key features as bullet points that would make this feature complete and useful.',
  nonGoals: 'List 2-3 things that should be explicitly OUT of scope for this feature to keep it focused.',
  successMetrics: 'Suggest 2-3 measurable success criteria that would indicate this feature is working well.',
  integrations: 'Based on this feature, suggest relevant APIs or services that might be needed.',
  techStack: 'Suggest the most appropriate tech stack for this type of feature.',
  additionalAnswers: 'Provide any additional context or considerations for this feature.',
  competitorUrls: `Based on this project and feature, suggest 3-5 similar apps, websites, or competitors that could serve as inspiration.
Format each as:
• [Name] - [URL] - [Brief description of what makes it relevant]

Focus on well-known, successful products that solve similar problems. Include a mix of direct competitors and products with similar UX patterns.`,
  appStoreUrls: `Based on this project and feature, suggest 3-5 mobile apps on the App Store or Google Play that could serve as inspiration.
Format each as:
• [App Name] - [Platform: iOS/Android/Both] - [Brief description of relevant features]

Focus on popular, well-designed apps that have similar functionality or UX patterns worth studying.`
}

export async function POST(request: NextRequest) {
  try {
    let body: { projectName?: string; currentField?: string; currentQuestion?: string; existingData?: any }
    try {
      body = await request.json()
    } catch {
      return NextResponse.json({ error: 'Invalid JSON in request body' }, { status: 400 })
    }
    const {
      projectName,
      currentField = '',
      currentQuestion = '',
      existingData = {}
    } = body

    const fieldPrompt = fieldPrompts[currentField] || `Provide a helpful suggestion for: ${currentQuestion}`

    // Build context from existing data
    const contextParts = []
    if (existingData.projectName) contextParts.push(`Project: ${existingData.projectName}`)
    if (existingData.featureTitle) contextParts.push(`Feature: ${existingData.featureTitle}`)
    if (existingData.problemStatement) contextParts.push(`Problem: ${existingData.problemStatement}`)
    if (existingData.targetUser) {
      const userMap: Record<string, string> = { A: 'New users', B: 'Existing users', C: 'Admin users', D: 'All users' }
      contextParts.push(`Target: ${userMap[existingData.targetUser] || existingData.targetUser}`)
    }
    if (existingData.scope) {
      const scopeMap: Record<string, string> = { A: 'Minimal MVP', B: 'Full-featured', C: 'Backend only', D: 'Frontend only' }
      contextParts.push(`Scope: ${scopeMap[existingData.scope] || existingData.scope}`)
    }
    if (existingData.keyFeatures) contextParts.push(`Key features: ${existingData.keyFeatures}`)

    // Get project-specific context from CLAUDE.md and PRD files
    const projectContext = await getProjectContext(projectName || existingData.projectName || '')

    const context = contextParts.length > 0
      ? `\n\nContext about this project:\n${contextParts.join('\n')}${projectContext}`
      : projectContext

    const prompt = `You are helping a developer plan a software feature. ${fieldPrompt}${context}

Be concise and practical. If this is a text field, provide a single clear answer. If it's a list field, use bullet points starting with •.

Use the project documentation and existing PRDs as reference for style and naming conventions.

Only provide the suggested content, no explanations or meta-commentary.`

    const suggestion = (await callGemini(prompt, 500)).trim() || 'Unable to generate suggestion'

    return NextResponse.json({ suggestion })
  } catch (error) {
    console.error('Claude suggestion error:', error)
    return NextResponse.json(
      { error: 'Failed to get suggestion', suggestion: 'Unable to generate suggestion at this time.' },
      { status: 500 }
    )
  }
}