/** * 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 { FileAttachment } from '../lib/types' import { ALL_ALLOWED_MIME_TYPES, IMAGE_MIME_TYPES, isImageAttachment, resolveMimeType } from '../lib/types' import { isSubmitEnter } from '../lib/keyboard' import { Button } from '@/components/ui/button' import { Textarea } from '@/components/ui/textarea' import { Card, CardContent } from '@/components/ui/card' import { Alert, AlertDescription } from '@/components/ui/alert' // File upload validation constants const MAX_IMAGE_SIZE = 5 * 1024 * 1024 // 5 MB for images const MAX_DOCUMENT_SIZE = 20 * 1024 * 1024 // 20 MB for documents const ALLOWED_EXTENSIONS = ['md', 'txt', 'csv', 'docx', 'xlsx', 'pdf', 'pptx', 'jpg', 'jpeg', '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 (isSubmitEnter(e)) { e.preventDefault() handleSendMessage() } } const handleAnswerSubmit = (answers: Record) => { sendAnswer(answers) } // File handling for attachments (images and documents) const handleFileSelect = useCallback((files: FileList | null) => { if (!files) return Array.from(files).forEach((file) => { // Resolve MIME type (browsers may not set it for .md files) let mimeType = file.type if (!mimeType || !ALL_ALLOWED_MIME_TYPES.includes(mimeType)) { mimeType = resolveMimeType(file.name) } // Validate file type if (!ALL_ALLOWED_MIME_TYPES.includes(mimeType)) { const ext = file.name.split('.').pop()?.toLowerCase() if (!ext || !ALLOWED_EXTENSIONS.includes(ext)) { setError(`Unsupported file type: ${file.name}. Supported: images (JPEG, PNG) and documents (MD, TXT, CSV, DOCX, XLSX, PDF, PPTX).`) return } mimeType = resolveMimeType(file.name) } // Validate size based on type const isImage = (IMAGE_MIME_TYPES as readonly string[]).includes(mimeType) const maxSize = isImage ? MAX_IMAGE_SIZE : MAX_DOCUMENT_SIZE const maxLabel = isImage ? '5 MB' : '20 MB' if (file.size > maxSize) { setError(`File too large: ${file.name}. Maximum size is ${maxLabel}.`) return } // Read and convert to base64 const reader = new FileReader() reader.onload = (e) => { const dataUrl = e.target?.result as string const base64Data = dataUrl.split(',')[1] const attachment: FileAttachment = { id: `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`, filename: file.name, mimeType: mimeType as FileAttachment['mimeType'], base64Data, previewUrl: isImage ? 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) => (
{isImageAttachment(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 */}