// @ts-nocheck - interview flow state machine with dynamic question handling import { useState, useCallback, useRef, useEffect } from 'react'; import { createLogger } from '@automaker/utils/logger'; import { useAppStore, Feature } from '@/store/app-store'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Bot, Send, User, Sparkles, FileText, ArrowLeft, CheckCircle } from 'lucide-react'; import { Spinner } from '@/components/ui/spinner'; import { cn, generateUUID } from '@/lib/utils'; import { getElectronAPI } from '@/lib/electron'; import { Markdown } from '@/components/ui/markdown'; import { useFileBrowser } from '@/contexts/file-browser-context'; import { toast } from 'sonner'; import { useNavigate } from '@tanstack/react-router'; import { getDefaultWorkspaceDirectory, saveLastProjectDirectory } from '@/lib/workspace-config'; const logger = createLogger('InterviewView'); interface InterviewMessage { id: string; role: 'user' | 'assistant'; content: string; timestamp: Date; } interface InterviewState { projectName: string; projectDescription: string; techStack: string[]; features: string[]; additionalNotes: string; } // Interview questions flow const INTERVIEW_QUESTIONS = [ { id: 'project-description', question: 'What do you want to build?', hint: 'Describe your project idea in a few sentences', field: 'projectDescription' as const, }, { id: 'tech-stack', question: 'What tech stack would you like to use?', hint: 'e.g., React, Next.js, Node.js, Python, etc.', field: 'techStack' as const, }, { id: 'core-features', question: 'What are the core features you want to include?', hint: 'List the main functionalities your app should have', field: 'features' as const, }, { id: 'additional', question: 'Any additional requirements or preferences?', hint: 'Design preferences, integrations, deployment needs, etc.', field: 'additionalNotes' as const, }, ]; export function InterviewView() { const { addProject, setCurrentProject, setAppSpec } = useAppStore(); const { openFileBrowser } = useFileBrowser(); const navigate = useNavigate(); const [input, setInput] = useState(''); const [messages, setMessages] = useState([]); const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0); const [interviewData, setInterviewData] = useState({ projectName: '', projectDescription: '', techStack: [], features: [], additionalNotes: '', }); const [isGenerating, setIsGenerating] = useState(false); const [isComplete, setIsComplete] = useState(false); const [generatedSpec, setGeneratedSpec] = useState(null); const [projectPath, setProjectPath] = useState(''); const [projectName, setProjectName] = useState(''); const [showProjectSetup, setShowProjectSetup] = useState(false); const messagesContainerRef = useRef(null); const inputRef = useRef(null); // Default parent directory using workspace config utility useEffect(() => { if (projectPath) return; let isMounted = true; const loadWorkspaceDir = async () => { try { const defaultDir = await getDefaultWorkspaceDirectory(); if (!isMounted || projectPath) { return; } if (defaultDir) { setProjectPath(defaultDir); } } catch (error) { logger.error('Failed to load default workspace directory:', error); } }; loadWorkspaceDir(); return () => { isMounted = false; }; }, [projectPath]); // Initialize with first question useEffect(() => { if (messages.length === 0) { const welcomeMessage: InterviewMessage = { id: 'welcome', role: 'assistant', content: `Hello! I'm here to help you plan your new project. Let's go through a few questions to understand what you want to build.\n\n**${INTERVIEW_QUESTIONS[0].question}**\n\n_${INTERVIEW_QUESTIONS[0].hint}_`, timestamp: new Date(), }; setMessages([welcomeMessage]); } }, [messages.length]); // Auto-scroll to bottom when messages change useEffect(() => { let timeoutId: NodeJS.Timeout | undefined; if (messagesContainerRef.current) { // Use a small delay to ensure DOM is updated timeoutId = setTimeout(() => { if (messagesContainerRef.current) { messagesContainerRef.current.scrollTo({ top: messagesContainerRef.current.scrollHeight, behavior: 'smooth', }); } }, 100); } return () => { if (timeoutId) { clearTimeout(timeoutId); } }; }, [messages]); // Auto-focus input useEffect(() => { if (inputRef.current && !isComplete) { inputRef.current.focus(); } }, [currentQuestionIndex, isComplete]); const handleSend = useCallback(() => { if (!input.trim() || isGenerating || isComplete) return; const userMessage: InterviewMessage = { id: `user-${Date.now()}`, role: 'user', content: input, timestamp: new Date(), }; setMessages((prev) => [...prev, userMessage]); // Update interview data based on current question const currentQuestion = INTERVIEW_QUESTIONS[currentQuestionIndex]; if (currentQuestion) { setInterviewData((prev) => { const newData = { ...prev }; if (currentQuestion.field === 'techStack' || currentQuestion.field === 'features') { // Parse comma-separated values into array newData[currentQuestion.field] = input .split(',') .map((s) => s.trim()) .filter(Boolean); } else { (newData as Record)[currentQuestion.field] = input; } return newData; }); } setInput(''); // Move to next question or complete const nextIndex = currentQuestionIndex + 1; setTimeout(() => { if (nextIndex < INTERVIEW_QUESTIONS.length) { const nextQuestion = INTERVIEW_QUESTIONS[nextIndex]; const assistantMessage: InterviewMessage = { id: `assistant-${Date.now()}`, role: 'assistant', content: `Great! **${nextQuestion.question}**\n\n_${nextQuestion.hint}_`, timestamp: new Date(), }; setMessages((prev) => [...prev, assistantMessage]); setCurrentQuestionIndex(nextIndex); } else { // All questions answered - generate spec const summaryMessage: InterviewMessage = { id: `assistant-summary-${Date.now()}`, role: 'assistant', content: 'Perfect! I have all the information I need. Now let me generate your project specification...', timestamp: new Date(), }; setMessages((prev) => [...prev, summaryMessage]); generateSpec({ ...interviewData, projectDescription: currentQuestionIndex === 0 ? input : interviewData.projectDescription, techStack: currentQuestionIndex === 1 ? input .split(',') .map((s) => s.trim()) .filter(Boolean) : interviewData.techStack, features: currentQuestionIndex === 2 ? input .split(',') .map((s) => s.trim()) .filter(Boolean) : interviewData.features, additionalNotes: currentQuestionIndex === 3 ? input : interviewData.additionalNotes, }); } }, 500); }, [input, isGenerating, isComplete, currentQuestionIndex, interviewData]); const generateSpec = useCallback(async (data: InterviewState) => { setIsGenerating(true); // Generate a draft app_spec.txt based on the interview responses const spec = generateAppSpec(data); // Simulate some processing time for better UX await new Promise((resolve) => setTimeout(resolve, 1500)); setGeneratedSpec(spec); setIsGenerating(false); setIsComplete(true); setShowProjectSetup(true); const completionMessage: InterviewMessage = { id: `assistant-complete-${Date.now()}`, role: 'assistant', content: `I've generated a draft project specification based on our conversation!\n\nPlease provide a project name and choose where to save your project, then click "Create Project" to get started.`, timestamp: new Date(), }; setMessages((prev) => [...prev, completionMessage]); }, []); const generateAppSpec = (data: InterviewState): string => { const projectName = data.projectDescription .split(' ') .slice(0, 3) .join('-') .toLowerCase() .replace(/[^a-z0-9-]/g, ''); // Note: Must follow XML format as defined in apps/server/src/lib/app-spec-format.ts return ` ${projectName || 'my-project'} ${data.projectDescription} ${ data.techStack.length > 0 ? data.techStack.map((tech) => `${tech}`).join('\n ') : '' } ${ data.features.length > 0 ? data.features.map((feature) => `${feature}`).join('\n ') : '' } ${data.additionalNotes || 'None specified'} Write clean, production-quality code Include proper error handling Write comprehensive Playwright tests Ensure all tests pass before marking features complete `; }; const handleSelectDirectory = async () => { const selectedPath = await openFileBrowser({ title: 'Select Base Directory', description: 'Choose the parent directory where your new project will be created', initialPath: projectPath || undefined, }); if (selectedPath) { setProjectPath(selectedPath); saveLastProjectDirectory(selectedPath); } }; const handleCreateProject = async () => { if (!projectName || !projectPath || !generatedSpec) return; setIsGenerating(true); try { saveLastProjectDirectory(projectPath); const api = getElectronAPI(); // Use platform-specific path separator const pathSep = typeof window !== 'undefined' && (window as any).electronAPI ? navigator.platform.indexOf('Win') !== -1 ? '\\' : '/' : '/'; const fullProjectPath = `${projectPath}${pathSep}${projectName}`; // Create project directory const mkdirResult = await api.mkdir(fullProjectPath); if (!mkdirResult.success) { toast.error('Failed to create project directory', { description: mkdirResult.error || 'Unknown error occurred', }); setIsGenerating(false); return; } // Write app_spec.txt with generated content await api.writeFile(`${fullProjectPath}/.automaker/app_spec.txt`, generatedSpec); // Create initial feature in the features folder const initialFeature: Feature = { id: generateUUID(), category: 'Core', description: 'Initial project setup', status: 'backlog' as const, skipTests: true, }; if (!api.features) { throw new Error('Features API not available'); } await api.features.create(fullProjectPath, initialFeature); const project = { id: `project-${Date.now()}`, name: projectName, path: fullProjectPath, lastOpened: new Date().toISOString(), }; // Update app spec in store setAppSpec(generatedSpec); // Add and select the project addProject(project); setCurrentProject(project); } catch (error) { logger.error('Failed to create project:', error); setIsGenerating(false); } }; const handleKeyPress = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); } }; const handleGoBack = () => { navigate({ to: '/' }); }; return (
{/* Header */}

New Project Interview

{isComplete ? 'Specification generated!' : `Question ${currentQuestionIndex + 1} of ${INTERVIEW_QUESTIONS.length}`}

{/* Progress indicator */}
{INTERVIEW_QUESTIONS.map((_, index) => (
))} {isComplete && }
{/* Messages */}
{messages.map((message) => (
{message.role === 'assistant' ? ( ) : ( )}
{message.role === 'assistant' ? ( {message.content} ) : (

{message.content}

)}

{message.timestamp.toLocaleTimeString()}

))} {isGenerating && !showProjectSetup && (
Generating specification...
)} {/* Project Setup Form */} {showProjectSetup && (

Create Your Project

setProjectName(e.target.value)} className="bg-zinc-950/50 border-white/10 text-white placeholder:text-zinc-500" data-testid="interview-project-name-input" />
setProjectPath(e.target.value)} className="flex-1 bg-zinc-950/50 border-white/10 text-white placeholder:text-zinc-500" data-testid="interview-project-path-input" />
{/* Preview of generated spec */}
                        {generatedSpec}
                      
)}
{/* Input */} {!isComplete && (
setInput(e.target.value)} onKeyPress={handleKeyPress} disabled={isGenerating} data-testid="interview-input" className="flex-1" />
)}
); }