← back to Letsbegin
app/page.tsx
410 lines
'use client'
import { useState, useEffect } from 'react'
import ProjectSelector from '@/components/ProjectSelector'
import DirectoryTree from '@/components/DirectoryTree'
import PRDGeneratorStep from '@/components/PRDGeneratorStep'
import RalphConverterStep from '@/components/RalphConverterStep'
import RalphyBoyStep from '@/components/RalphyBoyStep'
import QuickModeStep from '@/components/QuickModeStep'
import ProcessStatusBar from '@/components/ProcessStatusBar'
import RecentItemsFooter, { addRecentItem, RecentItem } from '@/components/RecentItemsFooter'
import { useAuth } from '@/components/AuthProvider'
import { SidebarDemo } from '@/components/ui/SidebarDemo'
import { FolderOpen, ChevronDown, ChevronRight, Activity } from 'lucide-react'
import Link from 'next/link'
export interface Project {
name: string
path: string
hasClaudeMd: boolean
}
export interface DirectoryNode {
name: string
path: string
type: 'file' | 'directory'
children?: DirectoryNode[]
}
export 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
}
}
export interface UserStory {
id: string
title: string
description: string
acceptanceCriteria: string[]
priority: number
passes: boolean
notes: string
}
export interface RalphData {
project: string
branchName: string
description: string
userStories: UserStory[]
}
export default function Home() {
const { username, logout } = useAuth()
const [projects, setProjects] = useState<Project[]>([])
const [selectedProject, setSelectedProject] = useState<Project | null>(null)
const [directoryTree, setDirectoryTree] = useState<DirectoryNode | null>(null)
const [isTreeCollapsed, setIsTreeCollapsed] = useState(true)
const [currentStep, setCurrentStep] = useState(0)
const [stepStatuses, setStepStatuses] = useState<Record<number, string>>({
0: 'active',
1: 'pending',
2: 'pending',
3: 'pending'
})
// Quick Mode State
const [quickModeText, setQuickModeText] = useState('')
const [isQuickMode, setIsQuickMode] = useState(true)
// PRD State
const [prdData, setPrdData] = useState<PRDData>({
projectName: '',
featureTitle: '',
problemStatement: '',
targetUser: 'D',
scope: 'B',
priority: 'B',
keyFeatures: '',
nonGoals: '',
successMetrics: '',
components: [],
integrations: '',
techStack: 'Next.js 16 + React 19',
additionalAnswers: '',
competitorUrls: [''],
appStoreUrls: [''],
uploadedFiles: [],
researchOptions: {
searchGithub: false,
searchPublicDBs: false,
searchSkills: false,
searchAgents: false,
searchAppStore: false,
searchPlayStore: false
}
})
const [generatedPRD, setGeneratedPRD] = useState('')
// Ralph State
const [ralphData, setRalphData] = useState<RalphData | null>(null)
const [validationResults, setValidationResults] = useState<Record<string, boolean>>({})
// Ralphy Boy State
const [isRunning, setIsRunning] = useState(false)
const [currentStoryIndex, setCurrentStoryIndex] = useState(0)
const [logs, setLogs] = useState<string[]>([])
// Load projects on mount
useEffect(() => {
loadProjects()
}, [])
// Load directory tree when project selected
useEffect(() => {
if (selectedProject) {
loadDirectoryTree(selectedProject.path)
const defaultFeatureTitle = `New Feature for ${selectedProject.name}`
setPrdData(prev => ({
...prev,
projectName: selectedProject.name,
featureTitle: defaultFeatureTitle
}))
// Track as recent item
addRecentItem({
name: selectedProject.name,
path: selectedProject.path,
type: 'project'
})
}
}, [selectedProject])
async function loadProjects() {
try {
const res = await fetch('/api/projects')
const data = await res.json()
setProjects(data.projects || [])
} catch (err) {
console.error('Failed to load projects:', err)
}
}
async function loadDirectoryTree(path: string) {
try {
const res = await fetch(`/api/directory?path=${encodeURIComponent(path)}`)
const data = await res.json()
setDirectoryTree(data)
} catch (err) {
console.error('Failed to load directory:', err)
}
}
function handleStepChange(step: number) {
// Allow navigation to completed steps or current step
if (step <= currentStep || stepStatuses[step] === 'completed' || stepStatuses[step] === 'active') {
setCurrentStep(step)
}
}
function handleStepComplete(step: number) {
setStepStatuses(prev => ({
...prev,
[step]: 'completed',
[step + 1]: step < 3 ? 'active' : prev[step + 1]
}))
if (step < 3) {
setCurrentStep(step + 1)
}
}
function handlePRDGenerated(prd: string) {
setGeneratedPRD(prd)
// Mark both step 0 and step 1 as complete when PRD is generated
setStepStatuses(prev => ({
...prev,
0: 'completed',
1: 'completed',
2: 'active'
}))
setCurrentStep(2) // Go directly to Ralph Converter
}
function handleModeToggle(useQuickMode: boolean) {
if (!useQuickMode && quickModeText.trim()) {
// Switching to form mode - copy quick mode text to additional notes
setPrdData(prev => ({
...prev,
additionalAnswers: quickModeText + (prev.additionalAnswers ? '\n\n' + prev.additionalAnswers : '')
}))
}
setIsQuickMode(useQuickMode)
setCurrentStep(useQuickMode ? 0 : 1)
}
function handleRalphConverted(data: RalphData) {
setRalphData(data)
handleStepComplete(2)
}
const stepTitles: Record<number, string> = {
0: 'Quick Mode',
1: 'PRD Generator',
2: 'Ralph Converter',
3: 'Ralphy Boy Runner'
}
function handleRecentItemClick(item: RecentItem) {
// Find and select the project
const project = projects.find(p => p.path === item.path)
if (project) {
setSelectedProject(project)
}
}
return (
<SidebarDemo
currentStep={currentStep}
onStepChange={handleStepChange}
onLogout={logout}
username={username ?? undefined}
>
<main className="h-full flex flex-col bg-[var(--background)] overflow-hidden">
{/* Top Bar with Project Selector */}
<header className="flex-shrink-0 border-b border-[var(--border)] bg-[var(--surface)] px-6 py-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<h2 className="text-lg font-semibold text-[var(--foreground)]">
{stepTitles[currentStep]}
</h2>
{stepStatuses[currentStep] === 'completed' && (
<span className="text-xs px-2 py-1 rounded-full bg-[var(--success)]/20 text-[var(--success)]">
✓ Completed
</span>
)}
</div>
<div className="flex items-center gap-3">
<Link
href="/live"
className="flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm bg-green-500/20 text-green-400 border border-green-500/30 hover:bg-green-500/30 transition-colors"
>
<Activity className="w-4 h-4" />
<span className="hidden sm:inline">Live Processes</span>
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
</span>
</Link>
<ProjectSelector
projects={projects}
selectedProject={selectedProject}
onSelect={setSelectedProject}
onCreateNew={() => {/* TODO */}}
/>
</div>
</div>
{/* Project Banner */}
{selectedProject && (
<div className="mt-3 flex items-center gap-2 text-sm">
<FolderOpen className="w-4 h-4 text-[var(--primary)]" />
<span className="font-medium text-[var(--primary)]">{selectedProject.name}</span>
<span className="text-[var(--foreground-tertiary)]">|</span>
<span className="text-[var(--foreground-tertiary)] text-xs">{selectedProject.path}</span>
</div>
)}
</header>
{/* Main Content Area */}
<div className="flex-1 overflow-auto">
<div className="p-6">
{/* Directory Navigator (Collapsible) */}
<div className="mb-6">
<button
onClick={() => setIsTreeCollapsed(!isTreeCollapsed)}
className="flex items-center gap-2 text-sm text-[var(--foreground-secondary)] hover:text-[var(--foreground)] transition-colors"
>
{isTreeCollapsed ? (
<ChevronRight className="w-4 h-4" />
) : (
<ChevronDown className="w-4 h-4" />
)}
<FolderOpen className="w-4 h-4" />
<span>Directory Navigator</span>
{selectedProject && (
<span className="text-[var(--foreground-tertiary)]">— {selectedProject.name}</span>
)}
</button>
{!isTreeCollapsed && (
<div className="mt-3 p-4 rounded-lg border border-[var(--border)] bg-[var(--surface)] max-h-64 overflow-y-auto">
{directoryTree ? (
<DirectoryTree
node={directoryTree}
depth={0}
selectedPath={selectedProject?.path || ''}
/>
) : (
<p className="text-[var(--foreground-tertiary)] text-sm">
Select a project to view its directory structure
</p>
)}
</div>
)}
</div>
{/* Step Content */}
<div className="rounded-xl border border-[var(--border)] bg-[var(--surface)] p-6">
{/* Mode Toggle for Step 0/1 */}
{(currentStep === 0 || currentStep === 1) && (
<div className="flex items-center gap-2 mb-6 pb-4 border-b border-[var(--border)]">
<span className="text-sm text-[var(--foreground-secondary)]">Mode:</span>
<button
onClick={() => handleModeToggle(true)}
className={`px-3 py-1.5 text-sm rounded-lg transition-colors ${
isQuickMode
? 'bg-[var(--primary)] text-white'
: 'bg-[var(--surface-alt)] text-[var(--foreground-secondary)] hover:bg-[var(--border)]'
}`}
>
Quick Mode
</button>
<button
onClick={() => handleModeToggle(false)}
className={`px-3 py-1.5 text-sm rounded-lg transition-colors ${
!isQuickMode
? 'bg-[var(--primary)] text-white'
: 'bg-[var(--surface-alt)] text-[var(--foreground-secondary)] hover:bg-[var(--border)]'
}`}
>
Full Form
</button>
</div>
)}
{currentStep === 0 && (
<QuickModeStep
selectedProject={selectedProject}
quickModeText={quickModeText}
onTextChange={setQuickModeText}
onGenerate={handlePRDGenerated}
/>
)}
{currentStep === 1 && (
<PRDGeneratorStep
prdData={prdData}
setPrdData={setPrdData}
selectedProject={selectedProject}
onGenerate={handlePRDGenerated}
generatedPRD={generatedPRD}
/>
)}
{currentStep === 2 && (
<RalphConverterStep
prdMarkdown={generatedPRD}
projectName={selectedProject?.name || ''}
onConvert={handleRalphConverted}
ralphData={ralphData}
validationResults={validationResults}
setValidationResults={setValidationResults}
onBack={() => setCurrentStep(1)}
/>
)}
{currentStep === 3 && (
<RalphyBoyStep
ralphData={ralphData}
projectPath={selectedProject?.path || ''}
isRunning={isRunning}
setIsRunning={setIsRunning}
currentStoryIndex={currentStoryIndex}
setCurrentStoryIndex={setCurrentStoryIndex}
logs={logs}
setLogs={setLogs}
onBack={() => setCurrentStep(2)}
/>
)}
</div>
</div>
</div>
{/* Process Status Bar */}
<ProcessStatusBar />
{/* Recent Items Footer */}
<RecentItemsFooter onItemClick={handleRecentItemClick} />
</main>
</SidebarDemo>
)
}