import React, { useEffect, useMemo, useRef, useState } from "react"; import { Editor } from "@monaco-editor/react"; import { motion } from "framer-motion"; import { Download, Wand2, GitBranch, Eye, Layers, Loader2, Bug } from "lucide-react"; import JSZip from "jszip"; // NOTE: Use default import for file-saver to avoid ESM named-export errors in some bundlers/CDNs import saveAs from "file-saver"; // --- shadcn/ui --- import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; import { Textarea } from "@/components/ui/textarea"; import { Switch } from "@/components/ui/switch"; import { Label } from "@/components/ui/label"; // --- Minimal in-browser sandbox that behaves like a tiny Lovable preview --- // We keep a virtual FS with three files and render them into an iframe using a Blob URL. const DEFAULT_HTML = ` My App
`; const DEFAULT_CSS = `:root{--fg:#111827;--bg:#ffffff;--brand:#6366f1} *{box-sizing:border-box} body{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,\n Cantarell,Noto Sans,sans-serif;margin:0;background:var(--bg);color:var(--fg)} .container{max-width:960px;margin:40px auto;padding:24px} .button{background:var(--brand);color:#fff;border:none;border-radius:12px;padding:12px 16px;cursor:pointer} .card{border:1px solid #e5e7eb;border-radius:20px;padding:20px;box-shadow:0 10px 30px rgba(0,0,0,.05)} `; const DEFAULT_JS = `const el = document.getElementById('app'); el.innerHTML = \`

✨ Hello from your Lovable‑style sandbox

Edit index.html, style.css, or script.js and see changes live.

\`; document.getElementById('btn').addEventListener('click', ()=>{ alert('It\\'s working!'); }); `; // Compose an HTML document with inline `); const withJs = withCss.replace("", ``); return withJs; } function useBlobPreview({ html, css, js }) { const urlRef = useRef(null); const srcDoc = useMemo(() => composeHtml(html, css, js), [html, css, js]); useEffect(() => { if (urlRef.current) URL.revokeObjectURL(urlRef.current); const blob = new Blob([srcDoc], { type: 'text/html' }); const url = URL.createObjectURL(blob); urlRef.current = url; return () => { if (urlRef.current) URL.revokeObjectURL(urlRef.current); }; }, [srcDoc]); return urlRef.current; } // --- Heuristic AI scaffolder (offline) --- // In production, replace with your AI API call; for now we synthesize boilerplates deterministically from the prompt. async function mockGenerateFromPrompt(prompt) { await new Promise(r => setTimeout(r, 700)); const p = prompt.toLowerCase(); const wantsAuth = /auth|login|signup|sign up|account/.test(p); const wantsTodo = /todo|task|kanban|list/.test(p); const wantsChat = /chat|message|support|assistant/.test(p); const html = `${escapeHtml(prompt)}

${escapeHtml(prompt)}

`; let css = DEFAULT_CSS + (wantsKanban(p) ? `\n.board{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px}` : ''); let js = `const root = document.getElementById('root');\n`; if (wantsTodo) { js += `let items = [];\nconst ui = () => {\n root.innerHTML = \`
\`;\n document.getElementById('add').onclick = ()=>{ const v = (document.getElementById('in')).value.trim(); if(v){ items.push(v); ui(); } };\n document.querySelectorAll('[data-i]').forEach(b=> b.onclick = ()=>{ items.splice(+b.dataset.i,1); ui(); });\n}; ui();\n`; } else if (wantsChat) { js += `let messages = [{role:'system',content:'Welcome!'}];\nconst ui = () => {\n root.innerHTML = \`
\${messages.map(m=>\`

\${m.role}: \${m.content}

\`).join('')}
\`;\n document.getElementById('send').onclick = ()=>{ const v=(document.getElementById('msg')).value.trim(); if(!v) return; messages.push({role:'user',content:v}); messages.push({role:'assistant',content:'(demo reply) '+v}); ui(); };\n}; ui();\n`; } else { js += DEFAULT_JS; } if (wantsAuth) { js += `\n// Fake auth UI\nconst banner = document.createElement('div');\nbanner.className='card'; banner.style.marginTop='16px';\nbanner.innerHTML = '

Auth (demo)

Replace with real auth provider.

';\nroot.after(banner);\n`; } return { html, css, js }; } function wantsKanban(p){ return /kanban|board|columns/.test(p); } function escapeHtml(s){ return s.replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"','\'':'''}[c])); } // Fallback downloader if file-saver default import is unavailable in runtime function downloadBlob(blob, filename){ try { if (typeof saveAs === 'function') return saveAs(blob, filename); } catch (_) {} const a = document.createElement('a'); const url = URL.createObjectURL(blob); a.href = url; a.download = filename; a.style.display = 'none'; document.body.appendChild(a); a.click(); setTimeout(()=>{ document.body.removeChild(a); URL.revokeObjectURL(url); }, 0); } export default function App() { const [prompt, setPrompt] = useState("Build a todo app with auth and live preview"); const [files, setFiles] = useState({ html: DEFAULT_HTML, css: DEFAULT_CSS, js: DEFAULT_JS, }); const [active, setActive] = useState("html"); const [autoRun, setAutoRun] = useState(true); const [isGenerating, setIsGenerating] = useState(false); const [testResults, setTestResults] = useState([]); const iframeUrl = useBlobPreview({ html: files.html, css: files.css, js: files.js }); // Autorun toggle is intentionally passive: preview refreshes whenever files change useEffect(() => { if (!autoRun) {/* reserved for future pause logic */} }, [files, autoRun]); const updateFile = (key, value) => setFiles(prev => ({ ...prev, [key]: value })); const onGenerate = async () => { setIsGenerating(true); try { const out = await mockGenerateFromPrompt(prompt); setFiles({ html: out.html, css: out.css, js: out.js }); setActive("html"); } finally { setIsGenerating(false); } }; const onDownloadZip = async () => { const zip = new JSZip(); zip.file("index.html", files.html); zip.file("style.css", files.css); zip.file("script.js", files.js); const blob = await zip.generateAsync({ type: "blob" }); downloadBlob(blob, "lovable-clone-mvp.zip"); }; // --- Self tests --- const runTests = async () => { const results = []; const assert = (name, pass, details = "") => results.push({ name, pass, details }); // Test 1: saveAs existence (without invoking downloads) assert("file-saver default import is a function", typeof saveAs === 'function'); // Test 2: composeHtml injects style and module script const testDoc = composeHtml("", "body{margin:0}", "console.log('ok')"); assert("composeHtml injects