KumarArpit8649's picture
Rename app.js to App.js
4f03fcd verified
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>
);
}