← back to Letsbegin
components/QuickModeStep.tsx
442 lines
'use client'
import { useState, useEffect } from 'react'
import { Zap, ArrowRight, Loader2, ChevronDown, ChevronRight, Clock, Trash2 } from 'lucide-react'
import { Project } from '@/app/page'
interface QuickModeEntry {
id: number
project_name: string
project_path: string
entry_text: string
created_at: string
}
interface ClarifyingQuestion {
id: string
question: string
options: { key: string; label: string }[]
answer?: string
}
interface QuickModeStepProps {
selectedProject: Project | null
quickModeText: string
onTextChange: (text: string) => void
onGenerate: (prd: string) => void
}
export default function QuickModeStep({
selectedProject,
quickModeText,
onTextChange,
onGenerate
}: QuickModeStepProps) {
const [isGenerating, setIsGenerating] = useState(false)
const [error, setError] = useState<string | null>(null)
const [recentEntries, setRecentEntries] = useState<QuickModeEntry[]>([])
const [isHistoryExpanded, setIsHistoryExpanded] = useState(false)
// Clarifying questions state
const [step, setStep] = useState<'input' | 'clarifying' | 'generating'>('input')
const [clarifyingQuestions, setClarifyingQuestions] = useState<ClarifyingQuestion[]>([])
const [isLoadingQuestions, setIsLoadingQuestions] = useState(false)
// Fetch recent entries on mount
useEffect(() => {
fetchRecentEntries()
}, [])
async function fetchRecentEntries() {
try {
const res = await fetch('/api/quickmode')
const data = await res.json()
if (data.entries) {
setRecentEntries(data.entries)
}
} catch (err) {
console.error('Failed to fetch recent entries:', err)
}
}
async function saveEntry() {
if (!selectedProject || !quickModeText.trim()) return
try {
await fetch('/api/quickmode', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
projectName: selectedProject.name,
projectPath: selectedProject.path,
entryText: quickModeText
})
})
// Refresh the list
fetchRecentEntries()
} catch (err) {
console.error('Failed to save entry:', err)
}
}
// Get clarifying questions first
async function handleSubmit() {
if (!selectedProject || !quickModeText.trim()) return
setIsLoadingQuestions(true)
setError(null)
// Save to database first
await saveEntry()
try {
// Extract a title from the first line or first 50 chars
const firstLine = quickModeText.split('\n')[0].trim()
const featureTitle = firstLine.length > 50
? firstLine.substring(0, 50) + '...'
: firstLine || 'Quick Mode Task'
// Call clarify endpoint to get questions
const res = await fetch('/api/prd/clarify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
projectName: selectedProject.name,
featureTitle,
problemStatement: quickModeText,
targetUser: 'D', // All users
scope: 'B', // Full-featured
priority: 'B', // High priority
keyFeatures: quickModeText,
nonGoals: '',
successMetrics: '',
components: [],
integrations: '',
techStack: 'Existing project stack',
additionalAnswers: '',
projectPath: selectedProject.path
})
})
const data = await res.json()
if (data.questions && data.questions.length > 0) {
setClarifyingQuestions(data.questions)
setStep('clarifying')
} else {
// No questions, go straight to generate
await generatePRD([])
}
} catch (err) {
setError('Failed to get clarifying questions. Please try again.')
console.error('Quick mode clarify error:', err)
} 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 answers
async function generatePRD(answers: ClarifyingQuestion[]) {
if (!selectedProject) return
setStep('generating')
setIsGenerating(true)
setError(null)
try {
const firstLine = quickModeText.split('\n')[0].trim()
const featureTitle = firstLine.length > 50
? firstLine.substring(0, 50) + '...'
: firstLine || 'Quick Mode Task'
// Format answers
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({
projectName: selectedProject.name,
featureTitle,
problemStatement: quickModeText,
targetUser: 'D', // All users
scope: 'B', // Full-featured
priority: 'B', // High priority
keyFeatures: quickModeText,
nonGoals: '',
successMetrics: '',
components: [],
integrations: '',
techStack: 'Existing project stack',
additionalAnswers: formattedAnswers ? `Clarifying Questions:\n${formattedAnswers}` : '',
projectPath: selectedProject.path
})
})
const data = await res.json()
if (data.error) {
setError(data.error)
setStep('input')
} else if (data.prd) {
onGenerate(data.prd)
}
} catch (err) {
setError('Failed to generate PRD. Please try again.')
console.error('Quick mode generation error:', err)
setStep('input')
} finally {
setIsGenerating(false)
}
}
// Submit clarifying answers
function submitClarifyingAnswers() {
const unanswered = clarifyingQuestions.filter(q => !q.answer)
if (unanswered.length > 0) {
setError(`Please answer all questions. ${unanswered.length} remaining.`)
return
}
generatePRD(clarifyingQuestions)
}
function loadEntry(entry: QuickModeEntry) {
onTextChange(entry.entry_text)
}
function formatDate(dateStr: string) {
const date = new Date(dateStr)
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
}
function truncateText(text: string, maxLength: number = 80) {
if (text.length <= maxLength) return text
return text.substring(0, maxLength) + '...'
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-[var(--primary)]/20">
<Zap className="w-5 h-5 text-[var(--primary)]" />
</div>
<div>
<h2 className="text-xl font-semibold text-[var(--foreground)]">Quick Mode</h2>
<p className="text-sm text-[var(--foreground-secondary)]">
{step === 'input' && 'Describe your task, answer clarifying questions, then generate PRD'}
{step === 'clarifying' && 'Answer these questions to help generate a better PRD'}
{step === 'generating' && 'Generating your PRD...'}
</p>
</div>
</div>
{/* Project Status */}
{selectedProject ? (
<div className="p-3 rounded-lg border border-[var(--border)] bg-[var(--surface-alt)]">
<p className="text-sm text-[var(--foreground-secondary)]">
Working on: <span className="font-medium text-[var(--primary)]">{selectedProject.name}</span>
</p>
</div>
) : (
<div className="p-3 rounded-lg border border-yellow-500/30 bg-yellow-500/10">
<p className="text-sm text-yellow-400">
Select a project from the dropdown above to get started
</p>
</div>
)}
{/* Error Message */}
{error && (
<div className="p-3 rounded-lg border border-red-500/30 bg-red-500/10">
<p className="text-sm text-red-400">{error}</p>
</div>
)}
{/* Step: Input */}
{step === 'input' && (
<>
{/* Text Area */}
<div className="space-y-2">
<label className="text-sm font-medium text-[var(--foreground)]">
What do you want to do?
</label>
<textarea
value={quickModeText}
onChange={(e) => onTextChange(e.target.value)}
disabled={isLoadingQuestions}
placeholder="Describe your task, feature, bug fix, or any work you need done...
Examples:
- Add a dark mode toggle to the settings page
- Fix the login form validation error
- Create an API endpoint for user profiles
- Refactor the database queries for better performance"
className="w-full h-64 p-4 rounded-lg border border-[var(--border)] bg-[var(--background)] text-[var(--foreground)] placeholder:text-[var(--foreground-tertiary)] focus:outline-none focus:ring-2 focus:ring-[var(--primary)] focus:border-transparent resize-none disabled:opacity-50"
/>
<p className="text-xs text-[var(--foreground-tertiary)]">
{quickModeText.length} characters
</p>
</div>
{/* Submit Button */}
<div className="flex justify-end">
<button
onClick={handleSubmit}
disabled={!selectedProject || !quickModeText.trim() || isLoadingQuestions}
className="flex items-center gap-2 px-6 py-3 rounded-lg bg-[var(--primary)] text-white font-medium hover:bg-[var(--primary)]/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{isLoadingQuestions ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
Getting Questions...
</>
) : (
<>
Next: Clarifying Questions
<ArrowRight className="w-4 h-4" />
</>
)}
</button>
</div>
</>
)}
{/* Step: Clarifying Questions */}
{step === 'clarifying' && (
<div className="card p-6 border border-[var(--border)] rounded-xl bg-[var(--surface)]">
<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. Select the best option (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(--background)] 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={() => setStep('input')}
className="px-4 py-2 rounded-lg border border-[var(--border)] text-[var(--foreground-secondary)] hover:bg-[var(--surface-alt)] transition-colors"
>
← Back
</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}
disabled={clarifyingQuestions.some(q => !q.answer)}
className="flex items-center gap-2 px-6 py-3 rounded-lg bg-[var(--primary)] text-white font-medium hover:bg-[var(--primary)]/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
Generate PRD
<ArrowRight className="w-4 h-4" />
</button>
</div>
</div>
)}
{/* Step: Generating */}
{step === 'generating' && (
<div className="card p-8 text-center border border-[var(--border)] rounded-xl bg-[var(--surface)]">
<div className="flex flex-col items-center gap-4">
<Loader2 className="w-12 h-12 animate-spin text-[var(--primary)]" />
<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>
)}
{/* Recent Entries - Collapsible (only show in input step) */}
{step === 'input' && recentEntries.length > 0 && (
<div className="border-t border-[var(--border)] pt-4">
<button
onClick={() => setIsHistoryExpanded(!isHistoryExpanded)}
className="flex items-center gap-2 text-sm text-[var(--foreground-secondary)] hover:text-[var(--foreground)] transition-colors w-full"
>
{isHistoryExpanded ? (
<ChevronDown className="w-4 h-4" />
) : (
<ChevronRight className="w-4 h-4" />
)}
<Clock className="w-4 h-4" />
<span>Recent Entries ({recentEntries.length})</span>
</button>
{isHistoryExpanded && (
<div className="mt-3 space-y-2">
{recentEntries.map((entry) => (
<div
key={entry.id}
onClick={() => loadEntry(entry)}
className="p-3 rounded-lg border border-[var(--border)] bg-[var(--background)] hover:border-[var(--primary)] cursor-pointer transition-colors group"
>
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<p className="text-sm text-[var(--foreground)] truncate">
{truncateText(entry.entry_text)}
</p>
<div className="flex items-center gap-2 mt-1">
<span className="text-xs text-[var(--primary)]">{entry.project_name}</span>
<span className="text-xs text-[var(--foreground-tertiary)]">
{formatDate(entry.created_at)}
</span>
</div>
</div>
<span className="text-xs text-[var(--foreground-tertiary)] group-hover:text-[var(--primary)]">
Click to load
</span>
</div>
</div>
))}
</div>
)}
</div>
)}
</div>
)
}