/** * Spec Creation Chat Component * * Full chat interface for interactive spec creation with Claude. * Handles the 7-phase conversation flow for creating app specifications. */ import { useCallback, useEffect, useRef, useState } from 'react' import { Send, X, CheckCircle2, AlertCircle, Wifi, WifiOff, RotateCcw, Loader2, ArrowRight, Zap, Paperclip, ExternalLink, FileText } from 'lucide-react' import { useSpecChat } from '../hooks/useSpecChat' import { ChatMessage } from './ChatMessage' import { QuestionOptions } from './QuestionOptions' import { TypingIndicator } from './TypingIndicator' import type { ImageAttachment } from '../lib/types' // Image upload validation constants const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5 MB const ALLOWED_TYPES = ['image/jpeg', 'image/png'] // Sample prompt for quick testing const SAMPLE_PROMPT = `Let's call it Simple Todo. This is a really simple web app that I can use to track my to-do items using a Kanban board. I should be able to add to-dos and then drag and drop them through the Kanban board. The different columns in the Kanban board are: - To Do - In Progress - Done The app should use a neobrutalism design. There is no need for user authentication either. All the to-dos will be stored in local storage, so each user has access to all of their to-dos when they open their browser. So do not worry about implementing a backend with user authentication or a database. Simply store everything in local storage. As for the design, please try to avoid AI slop, so use your front-end design skills to design something beautiful and practical. As for the content of the to-dos, we should store: - The name or the title at the very least - Optionally, we can also set tags, due dates, and priorities which should be represented as beautiful little badges on the to-do card Users should have the ability to easily clear out all the completed To-Dos. They should also be able to filter and search for To-Dos as well. You choose the rest. Keep it simple. Should be 25 features.` type InitializerStatus = 'idle' | 'starting' | 'error' interface SpecCreationChatProps { projectName: string onComplete: (specPath: string, yoloMode?: boolean) => void onCancel: () => void onExitToProject: () => void // Exit to project without starting agent initializerStatus?: InitializerStatus initializerError?: string | null onRetryInitializer?: () => void } export function SpecCreationChat({ projectName, onComplete, onCancel, onExitToProject, initializerStatus = 'idle', initializerError = null, onRetryInitializer, }: SpecCreationChatProps) { const [input, setInput] = useState('') const [error, setError] = useState(null) const [yoloEnabled, setYoloEnabled] = useState(false) const [pendingAttachments, setPendingAttachments] = useState([]) const messagesEndRef = useRef(null) const inputRef = useRef(null) const fileInputRef = useRef(null) const { messages, isLoading, isComplete, connectionStatus, currentQuestions, start, sendMessage, sendAnswer, disconnect, } = useSpecChat({ projectName, onComplete, onError: (err) => setError(err), }) // Start the chat session when component mounts useEffect(() => { start() return () => { disconnect() } }, []) // eslint-disable-line react-hooks/exhaustive-deps // Scroll to bottom when messages change useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [messages, currentQuestions, isLoading]) // Focus input when not loading and no questions useEffect(() => { if (!isLoading && !currentQuestions && inputRef.current) { inputRef.current.focus() } }, [isLoading, currentQuestions]) const handleSendMessage = () => { const trimmed = input.trim() // Allow sending if there's text OR attachments if ((!trimmed && pendingAttachments.length === 0) || isLoading) return // Detect /exit command - exit to project without sending to Claude if (/^\s*\/exit\s*$/i.test(trimmed)) { setInput('') onExitToProject() return } sendMessage(trimmed, pendingAttachments.length > 0 ? pendingAttachments : undefined) setInput('') setPendingAttachments([]) // Clear attachments after sending // Reset textarea height after sending if (inputRef.current) { inputRef.current.style.height = 'auto' } } const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault() handleSendMessage() } } const handleAnswerSubmit = (answers: Record) => { sendAnswer(answers) } // File handling for image attachments const handleFileSelect = useCallback((files: FileList | null) => { if (!files) return Array.from(files).forEach((file) => { // Validate file type if (!ALLOWED_TYPES.includes(file.type)) { setError(`Invalid file type: ${file.name}. Only JPEG and PNG are supported.`) return } // Validate file size if (file.size > MAX_FILE_SIZE) { setError(`File too large: ${file.name}. Maximum size is 5 MB.`) return } // Read and convert to base64 const reader = new FileReader() reader.onload = (e) => { const dataUrl = e.target?.result as string // dataUrl is "data:image/png;base64,XXXXXX" const base64Data = dataUrl.split(',')[1] const attachment: ImageAttachment = { id: `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`, filename: file.name, mimeType: file.type as 'image/jpeg' | 'image/png', base64Data, previewUrl: dataUrl, size: file.size, } setPendingAttachments((prev) => [...prev, attachment]) } reader.readAsDataURL(file) }) }, []) const handleRemoveAttachment = useCallback((id: string) => { setPendingAttachments((prev) => prev.filter((a) => a.id !== id)) }, []) const handleDrop = useCallback( (e: React.DragEvent) => { e.preventDefault() handleFileSelect(e.dataTransfer.files) }, [handleFileSelect] ) const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault() }, []) // Connection status indicator const ConnectionIndicator = () => { switch (connectionStatus) { case 'connected': return ( Connected ) case 'connecting': return ( Connecting... ) case 'error': return ( Error ) default: return ( Disconnected ) } } return (
{/* Header */}

Create Spec: {projectName}

{isComplete && ( Complete )} {/* Load Sample Prompt */} {/* Exit to Project - always visible escape hatch */}
{/* Error banner */} {error && (
{error}
)} {/* Messages area */}
{messages.length === 0 && !isLoading && (

Starting Spec Creation

Connecting to Claude to help you create your app specification...

{connectionStatus === 'error' && ( )}
)} {messages.map((message) => ( ))} {/* Structured questions */} {currentQuestions && currentQuestions.length > 0 && ( )} {/* Typing indicator - don't show when we have questions (waiting for user) */} {isLoading && !currentQuestions && } {/* Scroll anchor */}
{/* Input area */} {!isComplete && (
{/* Attachment previews */} {pendingAttachments.length > 0 && (
{pendingAttachments.map((attachment) => (
{attachment.filename} {attachment.filename.length > 10 ? `${attachment.filename.substring(0, 7)}...` : attachment.filename}
))}
)}
{/* Hidden file input */} handleFileSelect(e.target.files)} className="hidden" /> {/* Attach button */}