"use client"; import { useState, useCallback, useRef, useEffect } from "react"; 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, Loader2, Sparkles, FileText, ArrowLeft, CheckCircle, } from "lucide-react"; import { cn } from "@/lib/utils"; import { getElectronAPI } from "@/lib/electron"; import { Markdown } from "@/components/ui/markdown"; 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 { setCurrentView, addProject, setCurrentProject, setAppSpec } = useAppStore(); 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); // 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, ""); 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 api = getElectronAPI(); const result = await api.openDirectory(); if (!result.canceled && result.filePaths[0]) { setProjectPath(result.filePaths[0]); } }; const handleCreateProject = async () => { if (!projectName || !projectPath || !generatedSpec) return; setIsGenerating(true); try { const api = getElectronAPI(); const fullProjectPath = `${projectPath}/${projectName}`; // Create project directory await api.mkdir(fullProjectPath); // 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: crypto.randomUUID(), category: "Core", description: "Initial project setup", status: "backlog" as const, steps: [ "Step 1: Review app_spec.txt", "Step 2: Set up development environment", "Step 3: Start implementing features", ], 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) { console.error("Failed to create project:", error); setIsGenerating(false); } }; const handleKeyPress = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSend(); } }; const handleGoBack = () => { setCurrentView("welcome"); }; 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" />
)}
); }