Spaces:
Build error
Build error
| import React, { useState, useRef, useCallback } from 'react'; | |
| import { Button } from './components/ui/button'; | |
| import { Card, CardContent, CardHeader, CardTitle } from './components/ui/card'; | |
| import { Textarea } from './components/ui/textarea'; | |
| import { Badge } from './components/ui/badge'; | |
| import { Separator } from './components/ui/separator'; | |
| import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from './components/ui/sheet'; | |
| import { ScrollArea } from './components/ui/scroll-area'; | |
| import { Alert, AlertDescription } from './components/ui/alert'; | |
| import { Tabs, TabsContent, TabsList, TabsTrigger } from './components/ui/tabs'; | |
| import { | |
| Play, | |
| Download, | |
| Code2, | |
| FileText, | |
| Loader2, | |
| AlertCircle, | |
| Eye, | |
| Sparkles, | |
| Terminal | |
| } from 'lucide-react'; | |
| import { toast, Toaster } from 'sonner'; | |
| import './App.css'; | |
| const BACKEND_URL = process.env.REACT_APP_BACKEND_URL || 'http://localhost:8001'; | |
| export default function App() { | |
| const [prompt, setPrompt] = useState(''); | |
| const [output, setOutput] = useState(''); | |
| const [isGenerating, setIsGenerating] = useState(false); | |
| const [parsedFiles, setParsedFiles] = useState({}); | |
| const [error, setError] = useState(''); | |
| const [previewUrl, setPreviewUrl] = useState(''); | |
| const eventSourceRef = useRef(null); | |
| const createPreviewUrl = useCallback((files) => { | |
| console.log('🔍 Creating preview for files:', Object.keys(files)); | |
| if (!files || Object.keys(files).length === 0) { | |
| console.log('❌ No files provided for preview'); | |
| return ''; | |
| } | |
| try { | |
| // Check if this is a React project | |
| const hasReactFiles = Object.keys(files).some(name => | |
| name.endsWith('.jsx') || name.endsWith('.tsx') | |
| ); | |
| console.log('📱 Has React files:', hasReactFiles); | |
| if (hasReactFiles) { | |
| const previewUrl = createReactPreview(files); | |
| console.log('⚛️ React preview URL created:', !!previewUrl); | |
| return previewUrl; | |
| } | |
| // Handle HTML files | |
| const htmlFile = files['index.html'] || Object.values(files).find(content => | |
| typeof content === 'string' && content.includes('<!DOCTYPE html>') | |
| ); | |
| if (htmlFile) { | |
| console.log('🌐 Processing HTML file'); | |
| let htmlContent = htmlFile; | |
| // Inject all CSS files | |
| const cssFiles = Object.entries(files).filter(([name]) => name.endsWith('.css')); | |
| if (cssFiles.length > 0) { | |
| console.log('🎨 Injecting CSS files:', cssFiles.map(([name]) => name)); | |
| const allCSS = cssFiles.map(([, content]) => content).join('\n\n'); | |
| htmlContent = htmlContent.replace( | |
| '</head>', | |
| `<style>\n${allCSS}\n</style>\n</head>` | |
| ); | |
| } | |
| const blob = new Blob([htmlContent], { type: 'text/html' }); | |
| const url = URL.createObjectURL(blob); | |
| console.log('✅ HTML preview URL created successfully'); | |
| return url; | |
| } | |
| console.log('❌ No valid HTML or React files found for preview'); | |
| return ''; | |
| } catch (error) { | |
| console.error('❌ Error creating preview:', error); | |
| toast.error(`Preview generation failed: ${error.message}`); | |
| return ''; | |
| } | |
| }, []); | |
| const createReactPreview = (files) => { | |
| console.log('⚛️ Creating React preview...'); | |
| try { | |
| // Collect all CSS files | |
| const cssFiles = Object.entries(files).filter(([name]) => name.endsWith('.css')); | |
| const allCSS = cssFiles.map(([, content]) => content).join('\n\n'); | |
| console.log('🎨 Combined CSS length:', allCSS.length); | |
| // Collect all JSX files | |
| const jsxFiles = Object.entries(files).filter(([name]) => | |
| name.endsWith('.jsx') || name.endsWith('.tsx') || name.endsWith('.js') | |
| ); | |
| console.log('📄 JSX files found:', jsxFiles.map(([name]) => name)); | |
| if (jsxFiles.length === 0) { | |
| console.log('❌ No JSX files found'); | |
| return ''; | |
| } | |
| // Get all component code | |
| const allComponents = jsxFiles.map(([filename, content]) => { | |
| const componentName = filename.split('/').pop().replace(/\.(jsx|tsx|js)$/, '') || 'Component'; | |
| // Simple cleanup - just remove imports and exports | |
| let cleanContent = content | |
| .replace(/^import\s+.*$/gm, '') // Remove imports | |
| .replace(/^export\s+default\s+/gm, '') // Remove export default | |
| .replace(/^export\s*\{[^}]*\}/gm, '') // Remove named exports | |
| .trim(); | |
| return cleanContent; | |
| }).join('\n\n'); | |
| // Create a simplified React preview with better error handling | |
| const htmlWrapper = `<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>React Preview</title> | |
| <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script> | |
| <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script> | |
| <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> | |
| <style> | |
| ${allCSS} | |
| body { | |
| margin: 0; | |
| padding: 0; | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; | |
| line-height: 1.6; | |
| } | |
| *, *::before, *::after { | |
| box-sizing: border-box; | |
| } | |
| .app, .App { | |
| min-height: 100vh; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="root"></div> | |
| <div id="error-info" style="display: none; padding: 20px; background: #ffe6e6; color: #d00; border: 1px solid #ff9999; margin: 20px; border-radius: 8px;"></div> | |
| <script type="text/babel"> | |
| window.onerror = function(msg, url, line, col, error) { | |
| document.getElementById('error-info').style.display = 'block'; | |
| document.getElementById('error-info').innerHTML = '<strong>Error:</strong> ' + msg + ' (Line: ' + line + ')'; | |
| return true; | |
| }; | |
| try { | |
| ${allComponents} | |
| // Try to render the main component | |
| const root = ReactDOM.createRoot(document.getElementById('root')); | |
| if (typeof App !== 'undefined') { | |
| root.render(<App />); | |
| } else { | |
| // Find any available component | |
| const availableComponents = Object.keys(window).filter(key => | |
| typeof window[key] === 'function' && key.charAt(0) === key.charAt(0).toUpperCase() | |
| ); | |
| if (availableComponents.length > 0) { | |
| const ComponentToRender = window[availableComponents[0]]; | |
| root.render(React.createElement(ComponentToRender)); | |
| } else { | |
| root.render( | |
| <div style={{padding: '40px', textAlign: 'center', color: '#666'}}> | |
| <h2>React Preview</h2> | |
| <p>Components loaded successfully!</p> | |
| <p style={{fontSize: '14px', marginTop: '20px'}}> | |
| Available: {availableComponents.join(', ') || 'No components detected'} | |
| </p> | |
| </div> | |
| ); | |
| } | |
| } | |
| } catch (error) { | |
| console.error('React render error:', error); | |
| document.getElementById('error-info').style.display = 'block'; | |
| document.getElementById('error-info').innerHTML = '<strong>Render Error:</strong> ' + error.message; | |
| } | |
| </script> | |
| </body> | |
| </html>`; | |
| console.log('📦 Created HTML wrapper, length:', htmlWrapper.length); | |
| const blob = new Blob([htmlWrapper], { type: 'text/html' }); | |
| const url = URL.createObjectURL(blob); | |
| console.log('✅ React preview URL created successfully'); | |
| return url; | |
| } catch (error) { | |
| console.error('❌ Error in createReactPreview:', error); | |
| toast.error(`React preview failed: ${error.message}`); | |
| return ''; | |
| } | |
| }; | |
| const handleGenerate = async () => { | |
| if (!prompt.trim()) { | |
| toast.error('Please enter a prompt'); | |
| return; | |
| } | |
| setIsGenerating(true); | |
| setOutput(''); | |
| setParsedFiles({}); | |
| setError(''); | |
| setPreviewUrl(''); | |
| try { | |
| const response = await fetch(`${BACKEND_URL}/api/generate`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ prompt }), | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`HTTP error! status: ${response.status}`); | |
| } | |
| const reader = response.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| while (true) { | |
| const { done, value } = await reader.read(); | |
| if (done) break; | |
| const chunk = decoder.decode(value); | |
| const lines = chunk.split('\n'); | |
| for (const line of lines) { | |
| if (line.startsWith('data: ')) { | |
| try { | |
| const data = JSON.parse(line.slice(6)); | |
| if (data.error) { | |
| setError(`${data.error}: ${data.details || 'Unknown error'}`); | |
| toast.error(data.error); | |
| continue; | |
| } | |
| if (data.type === 'content') { | |
| setOutput(data.full_content || ''); | |
| } else if (data.type === 'complete') { | |
| setParsedFiles(data.files || {}); | |
| if (data.files && Object.keys(data.files).length > 0) { | |
| const url = createPreviewUrl(data.files); | |
| setPreviewUrl(url); | |
| toast.success(`Generated ${Object.keys(data.files).length} file(s)!`); | |
| } | |
| } | |
| } catch (e) { | |
| console.error('Error parsing SSE data:', e); | |
| } | |
| } | |
| } | |
| } | |
| } catch (error) { | |
| console.error('Generation error:', error); | |
| setError(`Connection error: ${error.message}`); | |
| toast.error('Failed to generate code. Please try again.'); | |
| } finally { | |
| setIsGenerating(false); | |
| } | |
| }; | |
| const handleDownload = async () => { | |
| if (Object.keys(parsedFiles).length === 0) { | |
| toast.error('No files to download'); | |
| return; | |
| } | |
| try { | |
| const response = await fetch(`${BACKEND_URL}/api/download`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify(parsedFiles), | |
| }); | |
| if (!response.ok) { | |
| throw new Error('Download failed'); | |
| } | |
| const blob = await response.blob(); | |
| const url = window.URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = 'generated-code.zip'; | |
| document.body.appendChild(a); | |
| a.click(); | |
| window.URL.revokeObjectURL(url); | |
| document.body.removeChild(a); | |
| toast.success('Files downloaded successfully!'); | |
| } catch (error) { | |
| console.error('Download error:', error); | |
| toast.error('Failed to download files'); | |
| } | |
| }; | |
| const getFileIcon = (filename) => { | |
| if (filename.endsWith('.html')) return <FileText className="w-4 h-4 text-orange-600" />; | |
| if (filename.endsWith('.jsx') || filename.endsWith('.tsx')) return <Code2 className="w-4 h-4 text-blue-600" />; | |
| if (filename.endsWith('.js')) return <Code2 className="w-4 h-4 text-yellow-600" />; | |
| if (filename.endsWith('.css')) return <FileText className="w-4 h-4 text-purple-600" />; | |
| return <FileText className="w-4 h-4 text-gray-600" />; | |
| }; | |
| const getLanguageFromFilename = (filename) => { | |
| if (filename.endsWith('.html')) return 'html'; | |
| if (filename.endsWith('.jsx')) return 'jsx'; | |
| if (filename.endsWith('.tsx')) return 'tsx'; | |
| if (filename.endsWith('.css')) return 'css'; | |
| if (filename.endsWith('.js')) return 'javascript'; | |
| return 'text'; | |
| }; | |
| const copyToClipboard = async (content, filename) => { | |
| try { | |
| await navigator.clipboard.writeText(content); | |
| toast.success(`${filename} copied to clipboard!`); | |
| } catch (error) { | |
| console.error('Failed to copy to clipboard:', error); | |
| toast.error('Failed to copy to clipboard'); | |
| } | |
| }; | |
| return ( | |
| <div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100"> | |
| <div className="container mx-auto px-4 py-8 max-w-7xl"> | |
| {/* Header */} | |
| <div className="text-center mb-8"> | |
| <div className="flex items-center justify-center gap-3 mb-4"> | |
| <div className="p-3 bg-gradient-to-r from-blue-600 to-purple-600 text-white rounded-2xl"> | |
| <Sparkles className="w-8 h-8" /> | |
| </div> | |
| <h1 className="text-4xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent"> | |
| AI Coding Playground | |
| </h1> | |
| </div> | |
| <p className="text-lg text-slate-600 max-w-2xl mx-auto"> | |
| Generate clean, production-ready code with live preview and instant download. | |
| Powered by Qwen3 Coder via OpenRouter. | |
| </p> | |
| </div> | |
| <div className="grid lg:grid-cols-2 gap-8"> | |
| {/* Input Section */} | |
| <div className="space-y-6"> | |
| <Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm"> | |
| <CardHeader className="pb-4"> | |
| <CardTitle className="flex items-center gap-2 text-xl"> | |
| <Terminal className="w-5 h-5 text-blue-600" /> | |
| Code Generation | |
| </CardTitle> | |
| </CardHeader> | |
| <CardContent className="space-y-4"> | |
| <div> | |
| <label className="text-sm font-medium text-slate-700 mb-2 block"> | |
| Describe what you want to build | |
| </label> | |
| <Textarea | |
| placeholder="e.g., Create a beautiful landing page for a SaaS product with a hero section, features, and pricing..." | |
| value={prompt} | |
| onChange={(e) => setPrompt(e.target.value)} | |
| className="min-h-32 resize-none text-base" | |
| disabled={isGenerating} | |
| /> | |
| </div> | |
| <Button | |
| onClick={handleGenerate} | |
| disabled={isGenerating || !prompt.trim()} | |
| className="w-full bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 text-lg py-6" | |
| > | |
| {isGenerating ? ( | |
| <> | |
| <Loader2 className="w-5 h-5 mr-2 animate-spin" /> | |
| Generating Code... | |
| </> | |
| ) : ( | |
| <> | |
| <Play className="w-5 h-5 mr-2" /> | |
| Generate Code | |
| </> | |
| )} | |
| </Button> | |
| </CardContent> | |
| </Card> | |
| {/* Output Section */} | |
| <Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm"> | |
| <CardHeader className="pb-4"> | |
| <div className="flex items-center justify-between"> | |
| <CardTitle className="flex items-center gap-2 text-xl"> | |
| <Code2 className="w-5 h-5 text-green-600" /> | |
| Generated Output | |
| </CardTitle> | |
| {Object.keys(parsedFiles).length > 0 && ( | |
| <div className="flex gap-2"> | |
| <Sheet> | |
| <SheetTrigger asChild> | |
| <Button variant="outline" size="sm"> | |
| <Eye className="w-4 h-4 mr-2" /> | |
| View Code | |
| </Button> | |
| </SheetTrigger> | |
| <SheetContent className="w-[600px] sm:w-[800px]"> | |
| <SheetHeader> | |
| <SheetTitle>Generated Files</SheetTitle> | |
| </SheetHeader> | |
| <Tabs defaultValue={Object.keys(parsedFiles)[0]} className="mt-6"> | |
| <TabsList className="grid w-full grid-cols-2 lg:grid-cols-3"> | |
| {Object.keys(parsedFiles).map((filename) => ( | |
| <TabsTrigger key={filename} value={filename} className="text-xs"> | |
| <div className="flex items-center gap-1"> | |
| {getFileIcon(filename)} | |
| {filename} | |
| </div> | |
| </TabsTrigger> | |
| ))} | |
| </TabsList> | |
| {Object.entries(parsedFiles).map(([filename, content]) => ( | |
| <TabsContent key={filename} value={filename}> | |
| <ScrollArea className="h-[70vh] w-full"> | |
| <pre className="text-sm bg-slate-900 text-slate-100 p-4 rounded-lg overflow-x-auto"> | |
| <code>{content}</code> | |
| </pre> | |
| </ScrollArea> | |
| </TabsContent> | |
| ))} | |
| </Tabs> | |
| </SheetContent> | |
| </Sheet> | |
| <Button onClick={handleDownload} size="sm" className="bg-green-600 hover:bg-green-700"> | |
| <Download className="w-4 h-4 mr-2" /> | |
| Download ZIP | |
| </Button> | |
| </div> | |
| )} | |
| </div> | |
| </CardHeader> | |
| <CardContent> | |
| {error && ( | |
| <Alert className="mb-4 border-red-200 bg-red-50"> | |
| <AlertCircle className="h-4 w-4 text-red-600" /> | |
| <AlertDescription className="text-red-700"> | |
| {error} | |
| </AlertDescription> | |
| </Alert> | |
| )} | |
| {/* Show individual file blocks if files are parsed */} | |
| {Object.keys(parsedFiles).length > 0 ? ( | |
| <div className="space-y-4"> | |
| {Object.entries(parsedFiles).map(([filename, content]) => ( | |
| <div key={filename} className="border rounded-lg bg-slate-50"> | |
| <div className="flex items-center justify-between bg-slate-100 px-4 py-2 border-b"> | |
| <div className="flex items-center gap-2"> | |
| {getFileIcon(filename)} | |
| <span className="font-mono text-sm font-medium">{filename}</span> | |
| </div> | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| onClick={() => copyToClipboard(content, filename)} | |
| className="h-8 w-8 p-0" | |
| > | |
| <span className="sr-only">Copy {filename}</span> | |
| 📋 | |
| </Button> | |
| </div> | |
| <ScrollArea className="h-48 w-full"> | |
| <pre className="text-sm text-slate-800 p-4 font-mono whitespace-pre-wrap"> | |
| {content} | |
| </pre> | |
| </ScrollArea> | |
| </div> | |
| ))} | |
| </div> | |
| ) : ( | |
| <ScrollArea className="h-64 w-full rounded-lg border bg-slate-50 p-4"> | |
| {output ? ( | |
| <pre className="text-sm text-slate-800 whitespace-pre-wrap font-mono"> | |
| {output} | |
| </pre> | |
| ) : ( | |
| <div className="flex items-center justify-center h-full text-slate-500"> | |
| {isGenerating ? ( | |
| <div className="flex items-center gap-3"> | |
| <Loader2 className="w-5 h-5 animate-spin" /> | |
| <span>Waiting for response...</span> | |
| </div> | |
| ) : ( | |
| 'Generated code will appear here...' | |
| )} | |
| </div> | |
| )} | |
| </ScrollArea> | |
| )} | |
| {Object.keys(parsedFiles).length > 0 && ( | |
| <div className="mt-4 flex flex-wrap gap-2"> | |
| {Object.keys(parsedFiles).map((filename) => ( | |
| <Badge key={filename} variant="secondary" className="text-xs"> | |
| {getFileIcon(filename)} | |
| <span className="ml-1">{filename}</span> | |
| </Badge> | |
| ))} | |
| </div> | |
| )} | |
| </CardContent> | |
| </Card> | |
| </div> | |
| {/* Preview Section */} | |
| <div className="space-y-6"> | |
| <Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm"> | |
| <CardHeader className="pb-4"> | |
| <div className="flex items-center justify-between"> | |
| <CardTitle className="flex items-center gap-2 text-xl"> | |
| <Eye className="w-5 h-5 text-purple-600" /> | |
| Live Preview | |
| </CardTitle> | |
| {Object.keys(parsedFiles).length > 0 && ( | |
| <Button | |
| variant="outline" | |
| size="sm" | |
| onClick={() => { | |
| console.log('🔍 Debug info:'); | |
| console.log('Files:', parsedFiles); | |
| console.log('Preview URL:', previewUrl); | |
| if (previewUrl) { | |
| window.open(previewUrl, '_blank'); | |
| } | |
| }} | |
| > | |
| Debug Preview | |
| </Button> | |
| )} | |
| </div> | |
| </CardHeader> | |
| <CardContent> | |
| <div className="bg-white rounded-lg border-2 border-slate-200 overflow-hidden" style={{ height: '600px' }}> | |
| {previewUrl ? ( | |
| <iframe | |
| src={previewUrl} | |
| className="w-full h-full border-0" | |
| title="Generated Code Preview" | |
| sandbox="allow-scripts allow-same-origin allow-forms" | |
| onLoad={() => console.log('✅ Preview iframe loaded successfully')} | |
| onError={(e) => { | |
| console.error('❌ Preview iframe error:', e); | |
| toast.error('Preview failed to load'); | |
| }} | |
| /> | |
| ) : ( | |
| <div className="flex items-center justify-center h-full text-slate-500 bg-slate-50"> | |
| <div className="text-center"> | |
| <Eye className="w-12 h-12 mx-auto mb-4 text-slate-400" /> | |
| <p className="text-lg font-medium mb-2">Preview will appear here</p> | |
| <p className="text-sm text-slate-400">Generate code to see live preview</p> | |
| {Object.keys(parsedFiles).length > 0 && ( | |
| <p className="text-xs text-red-500 mt-2"> | |
| Preview generation failed. Check console for details. | |
| </p> | |
| )} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </CardContent> | |
| </Card> | |
| </div> | |
| </div> | |
| {/* Footer */} | |
| <div className="text-center mt-12 pt-8 border-t border-slate-200"> | |
| <p className="text-slate-500 text-sm"> | |
| Built with React + FastAPI • Powered by OpenRouter • | |
| <span className="text-blue-600 font-medium"> Ready for Hugging Face Spaces</span> | |
| </p> | |
| </div> | |
| </div> | |
| <Toaster | |
| position="bottom-right" | |
| toastOptions={{ | |
| style: { fontFamily: 'Inter, sans-serif' }, | |
| className: 'toast-container', | |
| duration: 4000, | |
| }} | |
| /> | |
| </div> | |
| ); | |
| } |