← back to Letsbegin
components/PRDGeneratorStep.tsx
1284 lines
'use client'
import { useState } from 'react'
import CollapsibleSection from './CollapsibleSection'
import SimplifiedQuestionnaire from './SimplifiedQuestionnaire'
interface PRDData {
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
competitorUrls: string[]
appStoreUrls: string[]
uploadedFiles: string[]
researchOptions: {
searchGithub: boolean
searchPublicDBs: boolean
searchSkills: boolean
searchAgents: boolean
searchAppStore: boolean
searchPlayStore: boolean
}
}
interface Project {
name: string
path: string
hasClaudeMd: boolean
}
interface GitInfo {
isRepo: boolean
branch: string | null
remoteUrl: string | null
lastCommit: string | null
lastCommitDate: string | null
uncommittedChanges: number
hasGitignore: boolean
}
interface SecurityFeatures {
hasAuth: boolean
hasEnvExample: boolean
hasEnvLocal: boolean
hasSecrets: boolean
authType: string | null
hasHttps: boolean
hasCors: boolean
hasRateLimiting: boolean
hasInputValidation: boolean
hasCSRF: boolean
}
interface DetectedAPI {
name: string
type: 'ai' | 'payment' | 'storage' | 'auth' | 'email' | 'analytics' | 'other'
detected: boolean
envVar?: string
}
interface ProjectScanResult {
name: string
path: string
description: string
techStack: string[]
dependencies: string[]
components: string[]
existingPRDs: string[]
hasReadme: boolean
hasClaudeMd: boolean
hasPackageJson: boolean
git: GitInfo
security: SecurityFeatures
detectedAPIs: DetectedAPI[]
recentFiles: string[]
suggestedFeatures: string[]
}
interface Props {
prdData: PRDData
setPrdData: (data: PRDData) => void
selectedProject: Project | null
onGenerate: (prd: string) => void
generatedPRD: string
}
const targetUserOptions = [
{ value: 'A', label: 'New users', description: 'First-time visitors to the application' },
{ value: 'B', label: 'Existing users', description: 'Users already familiar with the app' },
{ value: 'C', label: 'Admin users', description: 'Administrators and power users' },
{ value: 'D', label: 'All users', description: 'Everyone who uses the application' },
]
const scopeOptions = [
{ value: 'A', label: 'Minimal viable', description: 'Core functionality only' },
{ value: 'B', label: 'Full-featured', description: 'Complete implementation' },
{ value: 'C', label: 'Backend only', description: 'API and database changes' },
{ value: 'D', label: 'Frontend only', description: 'UI and UX changes' },
]
const priorityOptions = [
{ value: 'A', label: 'Critical/ASAP', description: 'Needs immediate attention' },
{ value: 'B', label: 'High', description: 'Important for upcoming release' },
{ value: 'C', label: 'Medium', description: 'Should be done when possible' },
{ value: 'D', label: 'Low/Nice-to-have', description: 'Can wait for future releases' },
]
const techStackOptions = [
'Next.js 16 + React 19',
'React 19 (Standalone)',
'Node.js + Express',
'Python + FastAPI',
'Other',
]
// Clarifying question from the PRD skill
interface ClarifyingQuestion {
id: string
question: string
options: { key: string; label: string }[]
answer?: string
}
export default function PRDGeneratorStep({ prdData, setPrdData, selectedProject, onGenerate, generatedPRD }: Props) {
const [isGenerating, setIsGenerating] = useState(false)
const [isScanning, setIsScanning] = useState(false)
const [scanResult, setScanResult] = useState<ProjectScanResult | null>(null)
const [showPreview, setShowPreview] = useState(false)
const [useSimplifiedMode, setUseSimplifiedMode] = useState(false)
const [competitorSuggestion, setCompetitorSuggestion] = useState<string | null>(null)
const [appStoreSuggestion, setAppStoreSuggestion] = useState<string | null>(null)
const [isLoadingCompetitorSuggestion, setIsLoadingCompetitorSuggestion] = useState(false)
const [isLoadingAppStoreSuggestion, setIsLoadingAppStoreSuggestion] = useState(false)
// New state for skill-based clarifying questions flow
const [skillStep, setSkillStep] = useState<'questionnaire' | 'clarifying' | 'generating' | 'preview'>('questionnaire')
const [clarifyingQuestions, setClarifyingQuestions] = useState<ClarifyingQuestion[]>([])
const [isLoadingQuestions, setIsLoadingQuestions] = useState(false)
const [expandedSections, setExpandedSections] = useState({
foundation: false,
targetScope: false,
requirements: false,
technical: false,
inspiration: false,
additional: false,
})
// Handle simplified questionnaire completion
const handleSimplifiedComplete = async () => {
setUseSimplifiedMode(false)
// Submit to skill flow instead of directly generating
if (prdData.featureTitle && prdData.problemStatement) {
submitToSkill()
}
}
// Show simplified questionnaire if enabled
if (useSimplifiedMode) {
return (
<SimplifiedQuestionnaire
prdData={prdData}
setPrdData={setPrdData as React.Dispatch<React.SetStateAction<PRDData>>}
selectedProject={selectedProject}
onComplete={handleSimplifiedComplete}
onCancel={() => setUseSimplifiedMode(false)}
/>
)
}
// Fetch Claude suggestions for competitor URLs
async function fetchCompetitorSuggestions() {
setIsLoadingCompetitorSuggestion(true)
setCompetitorSuggestion(null)
try {
const res = await fetch('/api/claude/suggest', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
projectName: prdData.projectName || selectedProject?.name,
currentField: 'competitorUrls',
currentQuestion: 'Competitor or Inspiration Apps',
existingData: prdData
})
})
if (res.ok) {
const data = await res.json()
setCompetitorSuggestion(data.suggestion)
}
} catch (err) {
console.error('Suggestion error:', err)
setCompetitorSuggestion('Unable to get suggestions. Please try again.')
} finally {
setIsLoadingCompetitorSuggestion(false)
}
}
// Fetch Claude suggestions for App Store URLs
async function fetchAppStoreSuggestions() {
setIsLoadingAppStoreSuggestion(true)
setAppStoreSuggestion(null)
try {
const res = await fetch('/api/claude/suggest', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
projectName: prdData.projectName || selectedProject?.name,
currentField: 'appStoreUrls',
currentQuestion: 'Similar Apps on App Store / Play Store',
existingData: prdData
})
})
if (res.ok) {
const data = await res.json()
setAppStoreSuggestion(data.suggestion)
}
} catch (err) {
console.error('Suggestion error:', err)
setAppStoreSuggestion('Unable to get suggestions. Please try again.')
} finally {
setIsLoadingAppStoreSuggestion(false)
}
}
// Submit to PRD skill - gets clarifying questions
async function submitToSkill() {
if (!prdData.featureTitle || !prdData.problemStatement) {
alert('Please fill in Feature Title and Problem Statement')
return
}
setIsLoadingQuestions(true)
try {
const res = await fetch('/api/prd/clarify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...prdData,
projectPath: selectedProject?.path
})
})
if (!res.ok) {
throw new Error('Failed to get clarifying questions')
}
const data = await res.json()
if (data.questions && data.questions.length > 0) {
setClarifyingQuestions(data.questions)
setSkillStep('clarifying')
} else {
// No questions needed, go straight to generation
setSkillStep('generating')
await generatePRDWithAnswers([])
}
} catch (err) {
console.error('Failed to get clarifying questions:', err)
alert('Failed to submit to PRD skill. Please try again.')
} finally {
setIsLoadingQuestions(false)
}
}
// Handle answering a clarifying question
function handleClarifyingAnswer(questionId: string, answer: string) {
setClarifyingQuestions(prev =>
prev.map(q => q.id === questionId ? { ...q, answer } : q)
)
}
// Generate PRD with clarifying question answers
async function generatePRDWithAnswers(answers: ClarifyingQuestion[]) {
setSkillStep('generating')
setIsGenerating(true)
try {
// Format answers as "1A, 2C, 3B" style
const formattedAnswers = answers
.filter(q => q.answer)
.map((q, i) => `${i + 1}${q.answer}: ${q.question} → ${q.options.find(o => o.key === q.answer)?.label || q.answer}`)
.join('\n')
const res = await fetch('/api/prd/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...prdData,
additionalAnswers: prdData.additionalAnswers
? `${prdData.additionalAnswers}\n\nClarifying Questions:\n${formattedAnswers}`
: `Clarifying Questions:\n${formattedAnswers}`,
projectPath: selectedProject?.path
})
})
const data = await res.json()
if (data.prd) {
onGenerate(data.prd)
setSkillStep('preview')
setShowPreview(true)
} else {
throw new Error(data.error || 'Failed to generate PRD')
}
} catch (err) {
console.error('Failed to generate PRD:', err)
alert('Failed to generate PRD')
setSkillStep('clarifying')
} finally {
setIsGenerating(false)
}
}
// Submit clarifying answers and generate PRD
function submitClarifyingAnswers() {
const unanswered = clarifyingQuestions.filter(q => !q.answer)
if (unanswered.length > 0) {
alert(`Please answer all questions. ${unanswered.length} remaining.`)
return
}
generatePRDWithAnswers(clarifyingQuestions)
}
// Scan project and auto-fill form fields
async function handleScanProject() {
if (!selectedProject?.path) {
alert('Please select a project first')
return
}
setIsScanning(true)
try {
const res = await fetch(`/api/projects/scan?path=${encodeURIComponent(selectedProject.path)}`)
if (!res.ok) {
const err = await res.json()
throw new Error(err.error || 'Failed to scan project')
}
const data: ProjectScanResult = await res.json()
setScanResult(data)
// Auto-fill form fields from scan result
const techStackStr = data.techStack.length > 0
? (data.techStack.some(t => t.includes('Next.js')) ? 'Next.js 16 + React 19'
: data.techStack.some(t => t.includes('React')) ? 'React 19 (Standalone)'
: data.techStack.some(t => t.includes('Express')) ? 'Node.js + Express'
: 'Other')
: prdData.techStack
// Build suggested features as key features
const suggestedKeyFeatures = data.suggestedFeatures.length > 0
? data.suggestedFeatures.map(f => `• ${f}`).join('\n')
: prdData.keyFeatures
// Build integrations from dependencies
const integrations = data.dependencies
.filter(d => ['@anthropic-ai/sdk', 'axios', 'prisma', 'next-auth', 'stripe'].some(i => d.includes(i)))
.join(', ') || prdData.integrations
// Generate feature title if not set
const featureTitle = prdData.featureTitle || `New Feature for ${data.name}`
updatePrdData({
projectName: data.name,
featureTitle: featureTitle,
problemStatement: data.description || prdData.problemStatement,
techStack: techStackStr,
keyFeatures: suggestedKeyFeatures,
integrations: integrations,
components: data.components.map(c => c.replace('.tsx', '').replace('.jsx', ''))
})
// Open technical and inspiration sections to show filled data and Search Claude buttons
setExpandedSections(prev => ({ ...prev, technical: true, inspiration: true }))
} catch (err) {
console.error('Failed to scan project:', err)
alert(err instanceof Error ? err.message : 'Failed to scan project')
} finally {
setIsScanning(false)
}
}
function updatePrdData(updates: Partial<PRDData>) {
setPrdData({ ...prdData, ...updates })
}
function toggleSection(section: keyof typeof expandedSections) {
setExpandedSections(prev => ({ ...prev, [section]: !prev[section] }))
}
async function handleGenerate() {
if (!prdData.featureTitle || !prdData.problemStatement) {
alert('Please fill in Feature Title and Problem Statement')
return
}
setIsGenerating(true)
try {
const res = await fetch('/api/prd/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...prdData,
projectPath: selectedProject?.path
})
})
const data = await res.json()
if (data.prd) {
onGenerate(data.prd)
setShowPreview(true)
}
} catch (err) {
console.error('Failed to generate PRD:', err)
alert('Failed to generate PRD')
} finally {
setIsGenerating(false)
}
}
return (
<div className="space-y-6">
{/* Mode Toggle */}
<div className="flex items-center justify-between p-4 rounded-xl bg-gradient-to-r from-[var(--primary)]/10 to-[var(--secondary)]/10 border border-[var(--border)]">
<div className="flex items-center gap-4">
<div>
<div className="font-medium">Questionnaire Mode</div>
<div className="text-sm text-[var(--foreground-secondary)]">
{useSimplifiedMode ? 'One question at a time, large fonts' : 'All questions on one page'}
</div>
</div>
</div>
<div className="flex items-center gap-3">
<span className={`text-sm ${!useSimplifiedMode ? 'text-[var(--primary)] font-medium' : 'text-[var(--foreground-secondary)]'}`}>
Full Form
</span>
<button
onClick={() => setUseSimplifiedMode(!useSimplifiedMode)}
className={`relative w-14 h-7 rounded-full transition-colors ${
useSimplifiedMode ? 'bg-[var(--primary)]' : 'bg-[var(--surface-alt)]'
}`}
>
<span
className={`absolute top-1 w-5 h-5 rounded-full bg-white shadow transition-transform ${
useSimplifiedMode ? 'translate-x-8' : 'translate-x-1'
}`}
/>
</button>
<span className={`text-sm ${useSimplifiedMode ? 'text-[var(--primary)] font-medium' : 'text-[var(--foreground-secondary)]'}`}>
Step-by-Step
</span>
</div>
</div>
{/* Section A: Project Foundation */}
<CollapsibleSection
title="Section A: Project Foundation"
icon="🏗️"
isExpanded={expandedSections.foundation}
onToggle={() => toggleSection('foundation')}
>
<div className="space-y-4">
{/* Scan Project Button */}
{selectedProject && (
<div className="flex items-center justify-between p-3 rounded-lg bg-[var(--surface-alt)] border border-[var(--border)]">
<div className="flex items-center gap-3">
<span className="text-lg">📂</span>
<div>
<div className="font-medium">{selectedProject.name}</div>
<div className="text-xs text-[var(--foreground-tertiary)]">{selectedProject.path}</div>
</div>
</div>
<button
onClick={handleScanProject}
disabled={isScanning}
className="btn-primary text-sm px-4 py-2"
>
{isScanning ? (
<>
<svg className="w-4 h-4 animate-spin mr-2 inline" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Scanning...
</>
) : (
<>🔍 Scan & Fill Fields</>
)}
</button>
</div>
)}
{/* Scan Result Summary */}
{scanResult && (
<div className="space-y-3">
{/* Basic Info */}
<div className="p-3 rounded-lg bg-[var(--success)]/10 border border-[var(--success)]/30">
<div className="flex items-center gap-2 mb-2">
<span className="text-[var(--success)]">✓</span>
<span className="font-medium text-[var(--success)]">Project Scanned Successfully</span>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 text-xs">
<div className="flex items-center gap-1">
<span>{scanResult.hasPackageJson ? '✓' : '✗'}</span>
<span>package.json</span>
</div>
<div className="flex items-center gap-1">
<span>{scanResult.hasReadme ? '✓' : '✗'}</span>
<span>README.md</span>
</div>
<div className="flex items-center gap-1">
<span>{scanResult.hasClaudeMd ? '✓' : '✗'}</span>
<span>CLAUDE.md</span>
</div>
<div className="flex items-center gap-1">
<span>{scanResult.git.isRepo ? '✓' : '✗'}</span>
<span>Git</span>
</div>
</div>
{scanResult.techStack.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{scanResult.techStack.map((tech, i) => (
<span key={i} className="px-2 py-0.5 text-xs rounded bg-[var(--primary)]/20 text-[var(--primary)]">
{tech}
</span>
))}
</div>
)}
</div>
{/* Git Info */}
{scanResult.git.isRepo && (
<div className="p-3 rounded-lg bg-[var(--surface-alt)] border border-[var(--border)]">
<div className="flex items-center gap-2 mb-2">
<span>📦</span>
<span className="font-medium text-sm">Git Repository</span>
</div>
<div className="grid grid-cols-2 gap-2 text-xs">
<div>
<span className="text-[var(--foreground-tertiary)]">Branch:</span>{' '}
<span className="text-[var(--primary)]">{scanResult.git.branch}</span>
</div>
{scanResult.git.remoteUrl && (
<div className="truncate">
<span className="text-[var(--foreground-tertiary)]">Remote:</span>{' '}
<span>{scanResult.git.remoteUrl.replace('https://github.com/', '').replace('.git', '')}</span>
</div>
)}
{scanResult.git.lastCommit && (
<div className="col-span-2 truncate">
<span className="text-[var(--foreground-tertiary)]">Last Commit:</span>{' '}
<span>{scanResult.git.lastCommit}</span>
</div>
)}
{scanResult.git.uncommittedChanges > 0 && (
<div>
<span className="text-[var(--warning)]">⚠ {scanResult.git.uncommittedChanges} uncommitted changes</span>
</div>
)}
</div>
</div>
)}
{/* Security Features */}
<div className="p-3 rounded-lg bg-[var(--surface-alt)] border border-[var(--border)]">
<div className="flex items-center gap-2 mb-2">
<span>🔐</span>
<span className="font-medium text-sm">Security Features</span>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2 text-xs">
<label className="flex items-center gap-2 cursor-default">
<input type="checkbox" checked={scanResult.security.hasAuth} readOnly className="accent-[var(--success)]" />
<span>Authentication {scanResult.security.authType && `(${scanResult.security.authType})`}</span>
</label>
<label className="flex items-center gap-2 cursor-default">
<input type="checkbox" checked={scanResult.security.hasEnvLocal} readOnly className="accent-[var(--success)]" />
<span>Env Variables</span>
</label>
<label className="flex items-center gap-2 cursor-default">
<input type="checkbox" checked={scanResult.security.hasEnvExample} readOnly className="accent-[var(--success)]" />
<span>.env.example</span>
</label>
<label className="flex items-center gap-2 cursor-default">
<input type="checkbox" checked={scanResult.security.hasCors} readOnly className="accent-[var(--success)]" />
<span>CORS Protection</span>
</label>
<label className="flex items-center gap-2 cursor-default">
<input type="checkbox" checked={scanResult.security.hasRateLimiting} readOnly className="accent-[var(--success)]" />
<span>Rate Limiting</span>
</label>
<label className="flex items-center gap-2 cursor-default">
<input type="checkbox" checked={scanResult.security.hasInputValidation} readOnly className="accent-[var(--success)]" />
<span>Input Validation</span>
</label>
<label className="flex items-center gap-2 cursor-default">
<input type="checkbox" checked={scanResult.security.hasCSRF} readOnly className="accent-[var(--success)]" />
<span>CSRF Protection</span>
</label>
<label className="flex items-center gap-2 cursor-default">
<input type="checkbox" checked={scanResult.git.hasGitignore} readOnly className="accent-[var(--success)]" />
<span>.gitignore</span>
</label>
</div>
{scanResult.security.hasSecrets && (
<div className="mt-2 text-xs text-[var(--warning)]">
⚠ Potential hardcoded secrets detected in source files
</div>
)}
</div>
{/* Detected APIs */}
{scanResult.detectedAPIs.length > 0 && (
<div className="p-3 rounded-lg bg-[var(--surface-alt)] border border-[var(--border)]">
<div className="flex items-center gap-2 mb-2">
<span>🔌</span>
<span className="font-medium text-sm">Detected APIs ({scanResult.detectedAPIs.length})</span>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
{scanResult.detectedAPIs.map((api, i) => (
<div key={i} className="flex items-center gap-2 text-xs p-2 rounded bg-[var(--surface)] border border-[var(--border)]">
<span className={`w-2 h-2 rounded-full ${
api.type === 'ai' ? 'bg-purple-500' :
api.type === 'payment' ? 'bg-green-500' :
api.type === 'storage' ? 'bg-blue-500' :
api.type === 'auth' ? 'bg-yellow-500' :
api.type === 'email' ? 'bg-pink-500' :
api.type === 'analytics' ? 'bg-cyan-500' : 'bg-gray-500'
}`} />
<div>
<div className="font-medium">{api.name}</div>
<div className="text-[var(--foreground-tertiary)] text-[10px]">{api.envVar}</div>
</div>
</div>
))}
</div>
</div>
)}
{scanResult.existingPRDs.length > 0 && (
<div className="text-xs text-[var(--foreground-secondary)]">
Existing PRDs: {scanResult.existingPRDs.join(', ')}
</div>
)}
</div>
)}
<div>
<label className="block text-sm font-medium mb-2">Project Name</label>
<input
type="text"
className="input"
value={prdData.projectName}
onChange={(e) => updatePrdData({ projectName: e.target.value })}
placeholder={selectedProject?.name || 'Enter project name'}
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">Feature Title *</label>
<input
type="text"
className="input"
value={prdData.featureTitle}
onChange={(e) => updatePrdData({ featureTitle: e.target.value })}
placeholder="e.g., Voice Auto-Start Enhancement"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">Problem Statement *</label>
<textarea
className="textarea"
value={prdData.problemStatement}
onChange={(e) => updatePrdData({ problemStatement: e.target.value })}
placeholder="What problem does this feature solve? Why is it needed?"
rows={3}
/>
</div>
</div>
</CollapsibleSection>
{/* Section B: Target & Scope */}
<CollapsibleSection
title="Section B: Target & Scope"
icon="🎯"
isExpanded={expandedSections.targetScope}
onToggle={() => toggleSection('targetScope')}
>
<div className="space-y-6">
{/* Q1: Target User */}
<div>
<label className="block text-sm font-medium mb-3">
Q1: Who is the primary user?
</label>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{targetUserOptions.map((opt) => (
<div
key={opt.value}
className={`radio-option ${prdData.targetUser === opt.value ? 'selected' : ''}`}
onClick={() => updatePrdData({ targetUser: opt.value })}
>
<div className={`radio-dot ${prdData.targetUser === opt.value ? 'selected' : ''}`} />
<div>
<div className="font-medium">{opt.value}) {opt.label}</div>
<div className="text-xs text-[var(--foreground-tertiary)]">{opt.description}</div>
</div>
</div>
))}
</div>
</div>
{/* Q2: Scope */}
<div>
<label className="block text-sm font-medium mb-3">
Q2: What is the scope?
</label>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{scopeOptions.map((opt) => (
<div
key={opt.value}
className={`radio-option ${prdData.scope === opt.value ? 'selected' : ''}`}
onClick={() => updatePrdData({ scope: opt.value })}
>
<div className={`radio-dot ${prdData.scope === opt.value ? 'selected' : ''}`} />
<div>
<div className="font-medium">{opt.value}) {opt.label}</div>
<div className="text-xs text-[var(--foreground-tertiary)]">{opt.description}</div>
</div>
</div>
))}
</div>
</div>
{/* Q3: Priority */}
<div>
<label className="block text-sm font-medium mb-3">
Q3: Priority level?
</label>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{priorityOptions.map((opt) => (
<div
key={opt.value}
className={`radio-option ${prdData.priority === opt.value ? 'selected' : ''}`}
onClick={() => updatePrdData({ priority: opt.value })}
>
<div className={`radio-dot ${prdData.priority === opt.value ? 'selected' : ''}`} />
<div>
<div className="font-medium">{opt.value}) {opt.label}</div>
<div className="text-xs text-[var(--foreground-tertiary)]">{opt.description}</div>
</div>
</div>
))}
</div>
</div>
</div>
</CollapsibleSection>
{/* Section C: Core Requirements */}
<CollapsibleSection
title="Section C: Core Requirements"
icon="📋"
isExpanded={expandedSections.requirements}
onToggle={() => toggleSection('requirements')}
>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-2">Key Features (bullet points)</label>
<textarea
className="textarea"
value={prdData.keyFeatures}
onChange={(e) => updatePrdData({ keyFeatures: e.target.value })}
placeholder="• Feature 1 • Feature 2 • Feature 3"
rows={4}
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">Non-Goals (what this should NOT do)</label>
<textarea
className="textarea"
value={prdData.nonGoals}
onChange={(e) => updatePrdData({ nonGoals: e.target.value })}
placeholder="• Should not affect existing functionality • Not handling edge case X"
rows={3}
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">Success Metrics</label>
<textarea
className="textarea"
value={prdData.successMetrics}
onChange={(e) => updatePrdData({ successMetrics: e.target.value })}
placeholder="• Response time under 2 seconds • 90% user satisfaction"
rows={3}
/>
</div>
</div>
</CollapsibleSection>
{/* Section D: Technical Context */}
<CollapsibleSection
title="Section D: Technical Context"
icon="⚙️"
isExpanded={expandedSections.technical}
onToggle={() => toggleSection('technical')}
>
<div className="space-y-4">
{/* Scanned Components */}
{scanResult && scanResult.components.length > 0 && (
<div>
<label className="block text-sm font-medium mb-2">
Existing Components to Reuse ({scanResult.components.length} found)
</label>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2 p-3 rounded-lg bg-[var(--surface-alt)] border border-[var(--border)]">
{scanResult.components.map((comp, i) => {
const compName = comp.replace('.tsx', '').replace('.jsx', '')
const isSelected = prdData.components.includes(compName)
return (
<label
key={i}
className={`flex items-center gap-2 p-2 rounded cursor-pointer transition-colors ${
isSelected ? 'bg-[var(--primary)]/20' : 'hover:bg-[var(--surface)]'
}`}
>
<input
type="checkbox"
checked={isSelected}
onChange={(e) => {
if (e.target.checked) {
updatePrdData({ components: [...prdData.components, compName] })
} else {
updatePrdData({ components: prdData.components.filter(c => c !== compName) })
}
}}
className="accent-[var(--primary)]"
/>
<span className="text-sm">{comp}</span>
</label>
)
})}
</div>
</div>
)}
{/* Recent Files */}
{scanResult && scanResult.recentFiles.length > 0 && (
<div>
<label className="block text-sm font-medium mb-2">Recently Modified Files</label>
<div className="p-3 rounded-lg bg-[var(--surface-alt)] border border-[var(--border)] text-xs font-mono max-h-32 overflow-y-auto">
{scanResult.recentFiles.map((file, i) => (
<div key={i} className="text-[var(--foreground-secondary)] py-0.5">{file}</div>
))}
</div>
</div>
)}
{/* Dependencies */}
{scanResult && scanResult.dependencies.length > 0 && (
<div>
<label className="block text-sm font-medium mb-2">Dependencies ({scanResult.dependencies.length})</label>
<div className="flex flex-wrap gap-1">
{scanResult.dependencies.map((dep, i) => (
<span key={i} className="px-2 py-0.5 text-xs rounded bg-[var(--surface-alt)] border border-[var(--border)]">
{dep}
</span>
))}
</div>
</div>
)}
<div>
<label className="block text-sm font-medium mb-2">Integration Points</label>
<input
type="text"
className="input"
value={prdData.integrations}
onChange={(e) => updatePrdData({ integrations: e.target.value })}
placeholder="e.g., ElevenLabs API, Browser Web Audio API"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">Tech Stack</label>
<select
className="input"
value={prdData.techStack}
onChange={(e) => updatePrdData({ techStack: e.target.value })}
>
{techStackOptions.map((opt) => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
</div>
</div>
</CollapsibleSection>
{/* Section E: Inspiration & Research */}
<CollapsibleSection
title="Section E: Inspiration & Research"
icon="🔍"
isExpanded={expandedSections.inspiration || false}
onToggle={() => toggleSection('inspiration')}
>
<div className="space-y-6">
{/* Competitor/Inspiration URLs */}
<div>
<label className="block text-sm font-medium mb-2">
Competitor or Inspiration Apps (URLs)
</label>
<p className="text-xs text-[var(--foreground-tertiary)] mb-3">
Add links to similar apps or websites for reference
</p>
<div className="space-y-2">
{(prdData.competitorUrls || ['']).map((url, index) => (
<div key={index} className="flex gap-2">
<input
type="url"
className="input flex-1"
value={url}
onChange={(e) => {
const newUrls = [...(prdData.competitorUrls || [''])]
newUrls[index] = e.target.value
updatePrdData({ competitorUrls: newUrls })
}}
placeholder="https://example.com"
/>
{index === (prdData.competitorUrls || ['']).length - 1 ? (
<button
onClick={() => updatePrdData({ competitorUrls: [...(prdData.competitorUrls || ['']), ''] })}
className="px-3 py-2 rounded-lg bg-[var(--primary)]/20 text-[var(--primary)] hover:bg-[var(--primary)]/30 transition-colors"
>
+ Add
</button>
) : (
<button
onClick={() => {
const newUrls = (prdData.competitorUrls || ['']).filter((_, i) => i !== index)
updatePrdData({ competitorUrls: newUrls.length ? newUrls : [''] })
}}
className="px-3 py-2 rounded-lg bg-red-500/20 text-red-400 hover:bg-red-500/30 transition-colors"
>
Remove
</button>
)}
</div>
))}
</div>
{/* Search Claude Button */}
<button
onClick={fetchCompetitorSuggestions}
disabled={isLoadingCompetitorSuggestion}
className="mt-3 w-full flex items-center justify-center gap-2 px-4 py-3 rounded-lg bg-gradient-to-r from-purple-500/20 to-blue-500/20 border border-purple-500/30 text-purple-300 hover:from-purple-500/30 hover:to-blue-500/30 transition-all disabled:opacity-50"
>
{isLoadingCompetitorSuggestion ? (
<>
<svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
<span>Searching...</span>
</>
) : (
<>
<span>🔍</span>
<span>Search Claude for Suggestions</span>
</>
)}
</button>
{/* Suggestion Display */}
{competitorSuggestion && (
<div className="mt-3 p-4 rounded-lg bg-blue-500/10 border border-blue-500/30">
<div className="flex items-start gap-2">
<span className="text-lg">💡</span>
<div className="flex-1">
<p className="text-sm text-blue-300 whitespace-pre-wrap">{competitorSuggestion}</p>
<button
onClick={() => setCompetitorSuggestion(null)}
className="mt-2 text-xs text-blue-400 hover:underline"
>
Dismiss
</button>
</div>
</div>
</div>
)}
</div>
{/* App Store URLs */}
<div>
<label className="block text-sm font-medium mb-2">
Similar Apps on App Store / Play Store
</label>
<p className="text-xs text-[var(--foreground-tertiary)] mb-3">
Links to mobile apps for inspiration
</p>
<div className="space-y-2">
{(prdData.appStoreUrls || ['']).map((url, index) => (
<div key={index} className="flex gap-2">
<input
type="url"
className="input flex-1"
value={url}
onChange={(e) => {
const newUrls = [...(prdData.appStoreUrls || [''])]
newUrls[index] = e.target.value
updatePrdData({ appStoreUrls: newUrls })
}}
placeholder="https://apps.apple.com/... or https://play.google.com/..."
/>
{index === (prdData.appStoreUrls || ['']).length - 1 ? (
<button
onClick={() => updatePrdData({ appStoreUrls: [...(prdData.appStoreUrls || ['']), ''] })}
className="px-3 py-2 rounded-lg bg-[var(--primary)]/20 text-[var(--primary)] hover:bg-[var(--primary)]/30 transition-colors"
>
+ Add
</button>
) : (
<button
onClick={() => {
const newUrls = (prdData.appStoreUrls || ['']).filter((_, i) => i !== index)
updatePrdData({ appStoreUrls: newUrls.length ? newUrls : [''] })
}}
className="px-3 py-2 rounded-lg bg-red-500/20 text-red-400 hover:bg-red-500/30 transition-colors"
>
Remove
</button>
)}
</div>
))}
</div>
{/* Search Claude Button for App Store */}
<button
onClick={fetchAppStoreSuggestions}
disabled={isLoadingAppStoreSuggestion}
className="mt-3 w-full flex items-center justify-center gap-2 px-4 py-3 rounded-lg bg-gradient-to-r from-purple-500/20 to-blue-500/20 border border-purple-500/30 text-purple-300 hover:from-purple-500/30 hover:to-blue-500/30 transition-all disabled:opacity-50"
>
{isLoadingAppStoreSuggestion ? (
<>
<svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
<span>Searching...</span>
</>
) : (
<>
<span>🔍</span>
<span>Search Claude for App Suggestions</span>
</>
)}
</button>
{/* App Store Suggestion Display */}
{appStoreSuggestion && (
<div className="mt-3 p-4 rounded-lg bg-blue-500/10 border border-blue-500/30">
<div className="flex items-start gap-2">
<span className="text-lg">💡</span>
<div className="flex-1">
<p className="text-sm text-blue-300 whitespace-pre-wrap">{appStoreSuggestion}</p>
<button
onClick={() => setAppStoreSuggestion(null)}
className="mt-2 text-xs text-blue-400 hover:underline"
>
Dismiss
</button>
</div>
</div>
</div>
)}
</div>
{/* Research Options */}
<div>
<label className="block text-sm font-medium mb-2">
Research Options
</label>
<p className="text-xs text-[var(--foreground-tertiary)] mb-3">
Select what Claude should research while generating the PRD
</p>
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
{[
{ key: 'searchGithub', label: 'Search GitHub', icon: '📦', desc: 'Similar projects & code' },
{ key: 'searchPublicDBs', label: 'Public Databases', icon: '🗄️', desc: 'APIs & datasets' },
{ key: 'searchSkills', label: 'Skills Registry', icon: '🎯', desc: 'Existing skills to reuse' },
{ key: 'searchAgents', label: 'Agent Library', icon: '🤖', desc: 'Pre-built agents' },
{ key: 'searchAppStore', label: 'App Store', icon: '🍎', desc: 'iOS app inspiration' },
{ key: 'searchPlayStore', label: 'Play Store', icon: '🤖', desc: 'Android app inspiration' },
].map(({ key, label, icon, desc }) => (
<label
key={key}
className={`flex items-start gap-3 p-3 rounded-lg cursor-pointer transition-colors border ${
prdData.researchOptions?.[key as keyof typeof prdData.researchOptions]
? 'bg-[var(--primary)]/10 border-[var(--primary)]/50'
: 'bg-[var(--surface-alt)] border-[var(--border)] hover:border-[var(--primary)]/30'
}`}
>
<input
type="checkbox"
checked={prdData.researchOptions?.[key as keyof typeof prdData.researchOptions] || false}
onChange={(e) => {
updatePrdData({
researchOptions: {
...prdData.researchOptions,
[key]: e.target.checked
}
})
}}
className="mt-1 accent-[var(--primary)]"
/>
<div>
<div className="flex items-center gap-1.5 font-medium text-sm">
<span>{icon}</span>
<span>{label}</span>
</div>
<div className="text-xs text-[var(--foreground-tertiary)]">{desc}</div>
</div>
</label>
))}
</div>
</div>
</div>
</CollapsibleSection>
{/* Section F: Additional Questions */}
<CollapsibleSection
title="Section F: Additional Questions"
icon="❓"
isExpanded={expandedSections.additional}
onToggle={() => toggleSection('additional')}
>
<div>
<label className="block text-sm font-medium mb-2">
Additional Notes or Questions (Optional)
</label>
<textarea
className="textarea"
value={prdData.additionalAnswers}
onChange={(e) => updatePrdData({ additionalAnswers: e.target.value })}
placeholder="Any additional context, constraints, or questions..."
rows={4}
/>
</div>
</CollapsibleSection>
{/* Actions - Only show in questionnaire step */}
{skillStep === 'questionnaire' && (
<div className="flex items-center justify-between p-4 card">
<div className="text-sm text-[var(--foreground-secondary)]">
Answers: 1{prdData.targetUser}, 2{prdData.scope}, 3{prdData.priority}
</div>
<div className="flex items-center gap-3">
<button
onClick={submitToSkill}
className="btn-primary"
disabled={isLoadingQuestions}
>
{isLoadingQuestions ? (
<>
<svg className="w-4 h-4 animate-spin mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Submitting...
</>
) : (
<>📄 Submit to PRD Generator</>
)}
</button>
</div>
</div>
)}
{/* Clarifying Questions Step */}
{skillStep === 'clarifying' && (
<div className="card p-6">
<div className="mb-6">
<h3 className="text-xl font-semibold flex items-center gap-2 mb-2">
<span>🤔</span>
Clarifying Questions
</h3>
<p className="text-[var(--foreground-secondary)] text-sm">
Based on your input, please answer these questions to help generate a better PRD.
Select the best option for each (A, B, C, or D).
</p>
</div>
<div className="space-y-8">
{clarifyingQuestions.map((q, index) => (
<div key={q.id} className="border-b border-[var(--border)] pb-6 last:border-b-0 last:pb-0">
<div className="font-bold text-lg mb-4">
Q{index + 1}: {q.question}
</div>
<div className="space-y-2 pl-4">
{q.options.map((opt) => (
<button
key={opt.key}
onClick={() => handleClarifyingAnswer(q.id, opt.key)}
className={`w-full p-3 rounded-lg border text-left transition-all flex items-start gap-3 ${
q.answer === opt.key
? 'bg-[var(--primary)]/20 border-[var(--primary)]'
: 'bg-[var(--surface)] border-[var(--border)] hover:border-[var(--primary)]/50'
}`}
>
<span className={`font-bold min-w-[24px] ${q.answer === opt.key ? 'text-[var(--primary)]' : ''}`}>
{opt.key})
</span>
<span className="text-[var(--foreground-secondary)]">{opt.label}</span>
</button>
))}
</div>
</div>
))}
</div>
<div className="flex items-center justify-between mt-6 pt-4 border-t border-[var(--border)]">
<button
onClick={() => setSkillStep('questionnaire')}
className="btn-secondary"
>
← Back to Questionnaire
</button>
<div className="flex items-center gap-2 text-sm text-[var(--foreground-secondary)]">
<span>
{clarifyingQuestions.filter(q => q.answer).length}/{clarifyingQuestions.length} answered
</span>
</div>
<button
onClick={submitClarifyingAnswers}
className="btn-primary"
disabled={clarifyingQuestions.some(q => !q.answer)}
>
Generate PRD →
</button>
</div>
</div>
)}
{/* Generating Step */}
{skillStep === 'generating' && (
<div className="card p-8 text-center">
<div className="flex flex-col items-center gap-4">
<svg className="w-12 h-12 animate-spin text-[var(--primary)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
<div className="text-xl font-medium">Generating PRD...</div>
<p className="text-[var(--foreground-secondary)]">
Claude is creating your Product Requirements Document based on your inputs.
</p>
</div>
</div>
)}
{/* Preview - Only show after PRD is generated */}
{skillStep === 'preview' && generatedPRD && (
<div className="card p-4">
<div className="flex items-center justify-between mb-4">
<h3 className="font-medium flex items-center gap-2">
<span>📄</span>
Live Preview
</h3>
<button
onClick={() => navigator.clipboard.writeText(generatedPRD)}
className="text-sm text-[var(--primary)] hover:underline"
>
Copy to Clipboard
</button>
</div>
<div className="code-block max-h-96 overflow-y-auto text-sm">
<pre>{generatedPRD}</pre>
</div>
</div>
)}
</div>
)
}