← back to Letsbegin
components/SimplifiedQuestionnaire.tsx
629 lines
'use client'
import { useState } from 'react'
import { Project, PRDData } from '@/app/page'
interface Question {
id: string
title: string
subtitle: string
field: keyof PRDData | 'competitorUrls' | 'appStoreUrls' | 'researchOptions'
type: 'text' | 'textarea' | 'select' | 'multiselect' | 'url-list' | 'checkbox-grid'
options?: { value: string; label: string; icon?: string; desc?: string }[]
placeholder?: string
required?: boolean
}
const questions: Question[] = [
{
id: 'projectName',
title: 'What project are you working on?',
subtitle: 'Select the project from your Projects folder',
field: 'projectName',
type: 'text',
placeholder: 'Project name...',
required: true
},
{
id: 'featureTitle',
title: 'What feature are you building?',
subtitle: 'Give your feature a short, descriptive title',
field: 'featureTitle',
type: 'text',
placeholder: 'e.g., User Authentication System',
required: true
},
{
id: 'problemStatement',
title: 'What problem does this solve?',
subtitle: 'Describe the issue or need this feature addresses',
field: 'problemStatement',
type: 'textarea',
placeholder: 'Users currently have to... but they need...',
required: true
},
{
id: 'targetUser',
title: 'Who is this for?',
subtitle: 'Select the primary user type',
field: 'targetUser',
type: 'select',
options: [
{ value: 'A', label: 'New users - First time visitors' },
{ value: 'B', label: 'Existing users - Regular customers' },
{ value: 'C', label: 'Admin users - Internal team' },
{ value: 'D', label: 'All users - Everyone' }
]
},
{
id: 'scope',
title: 'How big is this feature?',
subtitle: 'Choose the scope of implementation',
field: 'scope',
type: 'select',
options: [
{ value: 'A', label: 'Minimal - Quick MVP, bare essentials' },
{ value: 'B', label: 'Full-featured - Complete implementation' },
{ value: 'C', label: 'Backend only - APIs and data' },
{ value: 'D', label: 'Frontend only - UI components' }
]
},
{
id: 'priority',
title: 'How urgent is this?',
subtitle: 'Set the priority level',
field: 'priority',
type: 'select',
options: [
{ value: 'A', label: 'Critical - ASAP, blocking other work' },
{ value: 'B', label: 'High - Important, do soon' },
{ value: 'C', label: 'Medium - Standard priority' },
{ value: 'D', label: 'Low - Nice to have' }
]
},
{
id: 'keyFeatures',
title: 'What are the key features?',
subtitle: 'List the main capabilities (one per line)',
field: 'keyFeatures',
type: 'textarea',
placeholder: '• User can sign up with email\n• Password reset via email\n• Remember me option'
},
{
id: 'nonGoals',
title: 'What should it NOT do?',
subtitle: 'Define boundaries and exclusions',
field: 'nonGoals',
type: 'textarea',
placeholder: '• No social login in v1\n• No 2FA initially'
},
{
id: 'successMetrics',
title: 'How will you measure success?',
subtitle: 'Define specific, measurable outcomes',
field: 'successMetrics',
type: 'textarea',
placeholder: '• Login takes < 2 seconds\n• 99% uptime\n• Zero security breaches'
},
{
id: 'integrations',
title: 'What integrations are needed?',
subtitle: 'APIs, services, or third-party tools',
field: 'integrations',
type: 'textarea',
placeholder: 'e.g., Stripe for payments, SendGrid for email'
},
{
id: 'techStack',
title: 'What tech stack?',
subtitle: 'Framework and technology preferences',
field: 'techStack',
type: 'text',
placeholder: 'Next.js 16 + React 19'
},
{
id: 'competitorUrls',
title: 'Any apps or websites for inspiration?',
subtitle: 'Add URLs to similar apps or competitors',
field: 'competitorUrls',
type: 'url-list',
placeholder: 'https://example.com'
},
{
id: 'appStoreUrls',
title: 'Similar mobile apps?',
subtitle: 'Links to App Store or Play Store apps for reference',
field: 'appStoreUrls',
type: 'url-list',
placeholder: 'https://apps.apple.com/...'
},
{
id: 'researchOptions',
title: 'What should we research?',
subtitle: 'Select sources to explore for inspiration',
field: 'researchOptions',
type: 'checkbox-grid',
options: [
{ value: 'searchGithub', label: 'GitHub', icon: '📦', desc: 'Similar projects' },
{ value: 'searchPublicDBs', label: 'Public APIs', icon: '🗄️', desc: 'APIs & datasets' },
{ value: 'searchSkills', label: 'Skills', icon: '🎯', desc: 'Reusable skills' },
{ value: 'searchAgents', label: 'Agents', icon: '🤖', desc: 'Pre-built agents' },
{ value: 'searchAppStore', label: 'App Store', icon: '🍎', desc: 'iOS apps' },
{ value: 'searchPlayStore', label: 'Play Store', icon: '📱', desc: 'Android apps' }
]
}
]
interface Props {
prdData: PRDData
setPrdData: React.Dispatch<React.SetStateAction<PRDData>>
selectedProject: Project | null
onComplete: () => void
onCancel: () => void
}
export default function SimplifiedQuestionnaire({
prdData,
setPrdData,
selectedProject,
onComplete,
onCancel
}: Props) {
const [currentIndex, setCurrentIndex] = useState(0)
const [suggestion, setSuggestion] = useState<string | null>(null)
const [isLoadingSuggestion, setIsLoadingSuggestion] = useState(false)
const [urlInfoLoading, setUrlInfoLoading] = useState<Record<number, boolean>>({})
const [urlInfo, setUrlInfo] = useState<Record<number, { title: string; description: string; features: string[] } | null>>({})
const handleLoadUrlInfo = async (url: string, index: number) => {
if (!url || !url.startsWith('http')) return
setUrlInfoLoading(prev => ({ ...prev, [index]: true }))
try {
const res = await fetch('/api/url-info', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url })
})
if (res.ok) {
const data = await res.json()
setUrlInfo(prev => ({ ...prev, [index]: data }))
} else {
setUrlInfo(prev => ({ ...prev, [index]: null }))
}
} catch (err) {
console.error('URL info error:', err)
setUrlInfo(prev => ({ ...prev, [index]: null }))
} finally {
setUrlInfoLoading(prev => ({ ...prev, [index]: false }))
}
}
const handleAddToField = (info: { title: string; description: string; features: string[] }, field: 'keyFeatures' | 'problemStatement') => {
if (field === 'keyFeatures') {
const existing = prdData.keyFeatures || ''
const newFeatures = info.features.map(f => `• ${f}`).join('\n')
const combined = existing ? `${existing}\n\n📌 From ${info.title}:\n${newFeatures}` : `📌 From ${info.title}:\n${newFeatures}`
setPrdData(prev => ({ ...prev, keyFeatures: combined }))
} else if (field === 'problemStatement') {
const existing = prdData.problemStatement || ''
const combined = existing ? `${existing}\n\n📌 Reference (${info.title}): ${info.description}` : `📌 Reference (${info.title}): ${info.description}`
setPrdData(prev => ({ ...prev, problemStatement: combined }))
}
}
const currentQuestion = questions[currentIndex]
const progress = ((currentIndex + 1) / questions.length) * 100
const handleChange = (value: string | string[]) => {
setPrdData(prev => ({
...prev,
[currentQuestion.field]: value
}))
}
const getValue = (): string => {
const val = prdData[currentQuestion.field as keyof PRDData]
if (Array.isArray(val)) return val.join(', ')
if (typeof val === 'object' && val !== null) return '' // For researchOptions
return (val as string) || ''
}
const canProceed = () => {
if (!currentQuestion.required) return true
// URL lists and checkbox grids are optional
if (currentQuestion.type === 'url-list' || currentQuestion.type === 'checkbox-grid') return true
const value = getValue()
return value.trim().length > 0
}
const handleNext = () => {
setSuggestion(null)
if (currentIndex < questions.length - 1) {
setCurrentIndex(currentIndex + 1)
} else {
onComplete()
}
}
const handleBack = () => {
setSuggestion(null)
if (currentIndex > 0) {
setCurrentIndex(currentIndex - 1)
}
}
const handleGetSuggestion = async () => {
setIsLoadingSuggestion(true)
try {
const context = {
projectName: prdData.projectName || selectedProject?.name || 'Unknown Project',
currentField: currentQuestion.field,
currentQuestion: currentQuestion.title,
existingData: prdData
}
const res = await fetch('/api/claude/suggest', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(context)
})
if (res.ok) {
const data = await res.json()
setSuggestion(data.suggestion)
} else {
setSuggestion('Unable to get suggestion. Please try again.')
}
} catch (err) {
console.error('Suggestion error:', err)
setSuggestion('Error getting suggestion. Check your connection.')
} finally {
setIsLoadingSuggestion(false)
}
}
const applySuggestion = () => {
if (suggestion) {
// For url-list fields, parse the suggestion to extract URLs
if (currentQuestion.type === 'url-list') {
// Extract URLs from the formatted suggestion
// Matches URLs like https://example.com or http://example.com
const urlRegex = /https?:\/\/[^\s\)]+/g
const matches = suggestion.match(urlRegex)
if (matches && matches.length > 0) {
// Clean up URLs (remove trailing punctuation)
const urls = matches.map(url => url.replace(/[,\.\)\]]+$/, ''))
handleChange(urls)
} else {
// Fallback: split by newlines and filter for URL-like strings
const lines = suggestion.split('\n').filter(line => line.includes('http'))
const urls = lines.map(line => {
const match = line.match(/https?:\/\/[^\s\)]+/)
return match ? match[0].replace(/[,\.\)\]]+$/, '') : ''
}).filter(Boolean)
if (urls.length > 0) {
handleChange(urls)
}
}
} else {
handleChange(suggestion)
}
setSuggestion(null)
}
}
return (
<div className="fixed inset-0 bg-[var(--background)] z-[9999] flex flex-col overflow-y-auto">
{/* Progress bar */}
<div className="h-2 bg-[var(--surface-alt)] flex-shrink-0">
<div
className="h-full bg-gradient-to-r from-[var(--primary)] to-[var(--secondary)] transition-all duration-300"
style={{ width: `${progress}%` }}
/>
</div>
{/* Header - centered */}
<div className="relative flex items-center justify-center px-8 py-4 border-b border-[var(--border)] flex-shrink-0">
<button
onClick={onCancel}
className="absolute left-8 text-[var(--foreground-secondary)] hover:text-[var(--foreground)] transition-colors"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<div className="text-lg font-medium text-[var(--foreground)]">
Question {currentIndex + 1} of {questions.length}
</div>
</div>
{/* Main content - fully centered with max-width container */}
<div className="flex-1 flex flex-col items-center justify-center px-4 md:px-8 py-8 w-full">
<div className="w-full max-w-2xl mx-auto flex flex-col items-center">
{/* Question title */}
<h1 className="text-4xl md:text-5xl font-bold text-center mb-4 leading-tight">
{currentQuestion.title}
</h1>
{/* Subtitle */}
<p className="text-xl text-[var(--foreground-secondary)] text-center mb-8">
{currentQuestion.subtitle}
</p>
{/* Input field */}
<div className="w-full">
{currentQuestion.type === 'text' && (
<input
type="text"
value={getValue()}
onChange={(e) => handleChange(e.target.value)}
placeholder={currentQuestion.placeholder}
className="w-full text-2xl p-6 rounded-xl bg-[var(--surface)] border-2 border-[var(--border)] focus:border-[var(--primary)] focus:outline-none transition-colors text-center"
autoFocus
/>
)}
{currentQuestion.type === 'textarea' && (
<textarea
value={getValue()}
onChange={(e) => handleChange(e.target.value)}
placeholder={currentQuestion.placeholder}
rows={5}
className="w-full text-xl p-6 rounded-xl bg-[var(--surface)] border-2 border-[var(--border)] focus:border-[var(--primary)] focus:outline-none transition-colors resize-none"
autoFocus
/>
)}
{currentQuestion.type === 'select' && currentQuestion.options && (
<div className="space-y-3">
{currentQuestion.options.map((option) => (
<button
key={option.value}
onClick={() => handleChange(option.value)}
className={`w-full text-left p-5 rounded-xl border-2 transition-all ${
getValue() === option.value
? 'border-[var(--primary)] bg-[var(--primary)]/10'
: 'border-[var(--border)] bg-[var(--surface)] hover:border-[var(--primary)]/50'
}`}
>
<span className="text-xl font-medium">{option.label}</span>
</button>
))}
</div>
)}
{currentQuestion.type === 'url-list' && (
<div className="space-y-4">
{((prdData[currentQuestion.field as keyof PRDData] as string[]) || ['']).map((url, index) => (
<div key={index} className="space-y-2">
<div className="flex gap-2">
<input
type="url"
value={url}
onChange={(e) => {
const urls = [...((prdData[currentQuestion.field as keyof PRDData] as string[]) || [''])]
urls[index] = e.target.value
setPrdData(prev => ({ ...prev, [currentQuestion.field]: urls }))
// Clear loaded info when URL changes
setUrlInfo(prev => ({ ...prev, [index]: null }))
}}
placeholder={currentQuestion.placeholder}
className="flex-1 text-xl p-4 rounded-xl bg-[var(--surface)] border-2 border-[var(--border)] focus:border-[var(--primary)] focus:outline-none transition-colors"
autoFocus={index === 0}
/>
{/* Load Info Button */}
{url && url.startsWith('http') && (
<button
onClick={() => handleLoadUrlInfo(url, index)}
disabled={urlInfoLoading[index]}
className="px-4 py-2 rounded-xl bg-gradient-to-r from-green-500/20 to-emerald-500/20 border border-green-500/30 text-green-400 font-medium hover:from-green-500/30 hover:to-emerald-500/30 disabled:opacity-50 transition-all whitespace-nowrap"
>
{urlInfoLoading[index] ? (
<span className="flex items-center gap-2">
<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>
Loading...
</span>
) : urlInfo[index] ? '✓ Loaded' : '🔍 Load'}
</button>
)}
{index === ((prdData[currentQuestion.field as keyof PRDData] as string[]) || ['']).length - 1 ? (
<button
onClick={() => {
const urls = [...((prdData[currentQuestion.field as keyof PRDData] as string[]) || [''])]
setPrdData(prev => ({ ...prev, [currentQuestion.field]: [...urls, ''] }))
}}
className="px-4 py-2 rounded-xl bg-[var(--primary)] text-white font-medium hover:opacity-90"
>
+ Add
</button>
) : (
<button
onClick={() => {
const urls = ((prdData[currentQuestion.field as keyof PRDData] as string[]) || ['']).filter((_, i) => i !== index)
setPrdData(prev => ({ ...prev, [currentQuestion.field]: urls.length ? urls : [''] }))
// Clear loaded info for removed URL
setUrlInfo(prev => {
const newInfo = { ...prev }
delete newInfo[index]
return newInfo
})
}}
className="px-4 py-2 rounded-xl bg-red-500/20 text-red-400 font-medium hover:bg-red-500/30"
>
Remove
</button>
)}
</div>
{/* Loaded URL Info Display */}
{urlInfo[index] && (
<div className="ml-2 p-4 rounded-xl bg-[var(--surface-alt)] border border-[var(--border)]">
<div className="flex items-start justify-between gap-4">
<div className="flex-1 min-w-0">
<h4 className="font-semibold text-lg text-[var(--foreground)] truncate">{urlInfo[index]?.title || 'Untitled'}</h4>
<p className="text-sm text-[var(--foreground-secondary)] mt-1 line-clamp-2">{urlInfo[index]?.description || 'No description'}</p>
{urlInfo[index]?.features && urlInfo[index]!.features.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{urlInfo[index]!.features.slice(0, 5).map((f, i) => (
<span key={i} className="px-2 py-1 text-xs rounded-full bg-[var(--primary)]/20 text-[var(--primary)]">{f}</span>
))}
{urlInfo[index]!.features.length > 5 && (
<span className="px-2 py-1 text-xs rounded-full bg-[var(--border)] text-[var(--foreground-tertiary)]">+{urlInfo[index]!.features.length - 5} more</span>
)}
</div>
)}
</div>
<div className="flex flex-col gap-2">
<button
onClick={() => handleAddToField(urlInfo[index]!, 'keyFeatures')}
className="px-3 py-2 text-sm rounded-lg bg-blue-500/20 text-blue-400 hover:bg-blue-500/30 transition-colors whitespace-nowrap"
>
+ Add to Features
</button>
<button
onClick={() => handleAddToField(urlInfo[index]!, 'problemStatement')}
className="px-3 py-2 text-sm rounded-lg bg-purple-500/20 text-purple-400 hover:bg-purple-500/30 transition-colors whitespace-nowrap"
>
+ Add to Problem
</button>
</div>
</div>
</div>
)}
</div>
))}
{/* Search Claude for competitor suggestions button */}
<button
onClick={handleGetSuggestion}
disabled={isLoadingSuggestion}
className="w-full mt-4 flex items-center justify-center gap-3 px-6 py-4 rounded-xl bg-gradient-to-r from-purple-500/20 to-blue-500/20 border-2 border-purple-500/30 text-purple-300 hover:from-purple-500/30 hover:to-blue-500/30 hover:border-purple-500/50 transition-all disabled:opacity-50"
>
{isLoadingSuggestion ? (
<>
<svg className="w-5 h-5 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 className="text-lg font-medium">Searching for inspiration...</span>
</>
) : (
<>
<span className="text-2xl">🔍</span>
<span className="text-lg font-medium">Search Claude for Suggestions</span>
</>
)}
</button>
<p className="text-sm text-[var(--foreground-tertiary)] text-center mt-2">
Skip this step if you don't have any URLs
</p>
</div>
)}
{currentQuestion.type === 'checkbox-grid' && currentQuestion.options && (
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
{currentQuestion.options.map((option) => {
const researchOpts = prdData.researchOptions || {}
const isChecked = researchOpts[option.value as keyof typeof researchOpts] || false
return (
<button
key={option.value}
onClick={() => {
setPrdData(prev => ({
...prev,
researchOptions: {
...prev.researchOptions,
[option.value]: !isChecked
}
}))
}}
className={`p-4 rounded-xl border-2 transition-all text-left ${
isChecked
? 'border-[var(--primary)] bg-[var(--primary)]/10'
: 'border-[var(--border)] bg-[var(--surface)] hover:border-[var(--primary)]/50'
}`}
>
<div className="flex items-center gap-3">
<span className="text-2xl">{option.icon}</span>
<div>
<div className="font-medium">{option.label}</div>
<div className="text-sm text-[var(--foreground-tertiary)]">{option.desc}</div>
</div>
</div>
</button>
)
})}
</div>
)}
</div>
{/* Claude suggestion button */}
<button
onClick={handleGetSuggestion}
disabled={isLoadingSuggestion}
className="mt-8 flex items-center gap-2 px-6 py-3 rounded-full bg-gradient-to-r from-blue-500/20 to-purple-500/20 border border-blue-500/30 text-blue-400 hover:from-blue-500/30 hover:to-purple-500/30 transition-all disabled:opacity-50"
>
{isLoadingSuggestion ? (
<>
<svg className="w-5 h-5 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>Thinking...</span>
</>
) : (
<>
<span className="text-lg">💡</span>
<span className="font-medium">Get Claude's Suggestion</span>
</>
)}
</button>
{/* Suggestion box */}
{suggestion && (
<div className="mt-6 w-full max-w-xl p-5 rounded-xl bg-blue-500/10 border border-blue-500/30">
<div className="flex items-start gap-3">
<span className="text-2xl">💡</span>
<div className="flex-1">
<p className="text-blue-300 whitespace-pre-wrap">{suggestion}</p>
<button
onClick={applySuggestion}
className="mt-3 px-4 py-2 rounded-lg bg-blue-500 text-white font-medium hover:bg-blue-600 transition-colors"
>
Use This Suggestion
</button>
</div>
</div>
</div>
)}
{/* Navigation buttons - underneath input field */}
<div className="flex items-center justify-center gap-4 mt-10 w-full">
<button
onClick={handleBack}
disabled={currentIndex === 0}
className="flex-1 max-w-[200px] px-6 py-4 rounded-xl text-lg font-medium border-2 border-[var(--border)] text-[var(--foreground-secondary)] hover:text-[var(--foreground)] hover:border-[var(--foreground-secondary)] disabled:opacity-30 disabled:cursor-not-allowed transition-all"
>
← Back
</button>
<button
onClick={handleNext}
disabled={!canProceed()}
className="flex-1 max-w-[200px] px-8 py-4 rounded-xl text-lg font-medium bg-gradient-to-r from-[var(--primary)] to-[var(--secondary)] text-white hover:opacity-90 disabled:opacity-30 disabled:cursor-not-allowed transition-all"
>
{currentIndex === questions.length - 1 ? '📄 Submit' : 'Next →'}
</button>
</div>
</div>
</div>
</div>
)
}