// CRITICAL'use client'; import { useRef, useEffect, useCallback, useId } from 'react';import { Play, Square, Copy, Check, Download, Maximize2, Minimize2, RefreshCw, AlertTriangle,} from 'lucide-react';import { useAppStore } from '@/store'; interface CodeSandboxProps { code: string; language: 'html' | 'react' | 'javascript'; title?: string; autoRun?: boolean; onOutput?: (output: string) => void; onError?: (error: string) => void;} const transformReactCode = (code: string) => { let transformed = code || ''; // Strip common import/export patterns that won't work in a plain iframe without bundling. transformed = transformed.replace(/^\s*import\s+.*?;?\s*$/gm, ''); transformed = transformed.replace(/^\s*export\s+\{[^}]+\}\s*;?\s*$/gm, ''); transformed = transformed.replace(/^\s*export\s+(const|let|var|function|class)\s+/gm, '$1 '); // Capture default export into a known global. transformed = transformed.replace(/\bexport\s+default\s+/g, 'window.__DEFAULT_EXPORT__ = '); return transformed.trim();}; // Template for React executionconst REACT_TEMPLATE = (code: string) => `<!DOCTYPE html><html><head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <script src="https://unpkg.com/react@18/umd/react.development.js"></script> <script 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> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: system-ui, -apple-system, sans-serif; padding: 16px; } .error { color: #b91c1c; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; white-space: pre-wrap; } </style></head><body> <div id="root"></div> <div id="error" class="error" style="display:none;"></div> <script type="text/babel" data-presets="react,typescript"> (() => { const send = (type, message) => { try { window.parent && window.parent.postMessage({ type, message }, '*'); } catch {} }; const showError = (message) => { const el = document.getElementById('error'); if (el) { el.style.display = 'block'; el.textContent = message; } }; window.addEventListener('error', (event) => { const msg = event?.error?.stack || event?.message || 'Unknown error'; send('error', msg); showError(msg); }); try { window.__DEFAULT_EXPORT__ = undefined; const { useState, useEffect, useMemo, useRef, useCallback } = React; ${transformReactCode(code)} // Try to render common component patterns const container = document.getElementById('root'); const root = ReactDOM.createRoot(container); const candidate = (typeof App !== 'undefined' && App) || window.__DEFAULT_EXPORT__ || null; if (!candidate) { const msg = 'React preview: define an App component or use export default.'; send('error', msg); showError(msg); return; } const element = React.isValidElement(candidate) ? candidate : React.createElement(candidate); root.render(element); } catch (e) { const msg = e?.stack || e?.message || 'Failed to render React preview'; send('error', msg); showError(msg); } })(); </script></body></html>`; // Template for vanilla JavaScriptconst JS_TEMPLATE = (code: string) => `<!DOCTYPE html><html><head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: system-ui, -apple-system, sans-serif; padding: 16px; } #output { white-space: pre-wrap; font-family: monospace; } </style></head><body> <div id="output"></div> <script> const output = document.getElementById('output'); const send = (type, message) => { try { window.parent && window.parent.postMessage({ type, message }, '*'); } catch {} }; const log = (...args) => { const line = args.map(a => typeof a === 'object' ? JSON.stringify(a, null, 2) : String(a) ).join(' ') + '\\n'; output.textContent += line; send('output', line); }; console.log = log; console.info = log; console.warn = log; console.error = (...args) => { log(...args); send('error', args.map(String).join(' ')); }; window.addEventListener('error', (event) => { const msg = event?.error?.stack || event?.message || 'Unknown error'; send('error', msg); }); try { ${code} } catch (e) { output.textContent = 'Error: ' + e.message; send('error', e?.stack || e?.message || String(e)); } </script></body></html>`; export function CodeSandbox({ code, language, title, autoRun = false, onOutput, onError,}: CodeSandboxProps) { const sandboxId = useId(); const sandboxState = useAppStore( (state) => state.codeSandboxState[sandboxId] ?? { isRunning: false, isFullscreen: false, copied: false, error: null, }, ); const updateCodeSandboxState = useAppStore((state) => state.updateCodeSandboxState); const { isRunning, isFullscreen, copied, error } = sandboxState; const updateState = (partial: Partial<typeof sandboxState>) => { updateCodeSandboxState(sandboxId, (prev) => ({ ...prev, ...partial })); }; const iframeRef = useRef<HTMLIFrameElement>(null); const getSrcDoc = useCallback(() => { switch (language) { case 'html': return code; case 'react': return REACT_TEMPLATE(code); case 'javascript': return JS_TEMPLATE(code); default: return code; } }, [code, language]); const runCode = useCallback(() => { updateState({ isRunning: true, error: null }); if (iframeRef.current) { try { iframeRef.current.srcdoc = getSrcDoc(); } catch (e) { const errorMsg = e instanceof Error ? e.message : 'Failed to run code'; updateState({ error: errorMsg }); onError?.(errorMsg); } } }, [getSrcDoc, onError, updateState]); const stopCode = useCallback(() => { updateState({ isRunning: false }); if (iframeRef.current) { iframeRef.current.srcdoc = ''; } }, [updateState]); const copyCode = useCallback(() => { navigator.clipboard.writeText(code); updateState({ copied: true }); setTimeout(() => updateState({ copied: false }), 2000); }, [code, updateState]); const downloadCode = useCallback(() => { const blob = new Blob([getSrcDoc()], { type: 'text/html' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${title || 'artifact'}.html`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }, [getSrcDoc, title]); // Auto-run on mount if enabled useEffect(() => { if (!autoRun) return; const id = window.setTimeout(() => { runCode(); }, 0); return () => window.clearTimeout(id); }, [autoRun, runCode]); // Listen for iframe errors useEffect(() => { const handleMessage = (event: MessageEvent) => { if (event.data?.type === 'error') { updateState({ error: event.data.message }); onError?.(event.data.message); } else if (event.data?.type === 'output') { onOutput?.(event.data.message); } }; window.addEventListener('message', handleMessage); return () => window.removeEventListener('message', handleMessage); }, [onError, onOutput, updateState]); // Toolbar buttons component to avoid duplication const ToolbarButtons = ({ inFooter = false }: { inFooter?: boolean }) => ( <div className={`flex items-center gap-0.5 md:gap-1 ${inFooter ? 'justify-center' : ''}`}> {/* Run/Stop - always visible */} {isRunning ? ( <button onClick={stopCode} className="p-2 md:p-1.5 rounded hover:bg-[var(--error)]/20 text-[var(--error)] transition-colors" title="Stop" > <Square className="h-5 w-5 md:h-4 md:w-4" /> </button> ) : ( <button onClick={runCode} className="p-2 md:p-1.5 rounded hover:bg-[var(--success)]/20 text-[var(--success)] transition-colors" title="Run" > <Play className="h-5 w-5 md:h-4 md:w-4" /> </button> )} {/* Refresh */} <button onClick={runCode} className={`${inFooter ? 'block' : 'hidden md:block'} p-2 md:p-1.5 rounded hover:bg-[var(--accent)] transition-colors`} title="Refresh" > <RefreshCw className="h-5 w-5 md:h-4 md:w-4 text-[#9a9590]" /> </button> {/* Copy */} <button onClick={copyCode} className={`${inFooter ? 'block' : 'hidden md:block'} p-2 md:p-1.5 rounded hover:bg-[var(--accent)] transition-colors`} title="Copy code" > {copied ? ( <Check className="h-5 w-5 md:h-4 md:w-4 text-[var(--success)]" /> ) : ( <Copy className="h-5 w-5 md:h-4 md:w-4 text-[#9a9590]" /> )} </button> {/* Download */} <button onClick={downloadCode} className={`${inFooter ? 'block' : 'hidden md:block'} p-2 md:p-1.5 rounded hover:bg-[var(--accent)] transition-colors`} title="Download" > <Download className="h-5 w-5 md:h-4 md:w-4 text-[#9a9590]" /> </button> {/* Fullscreen/Minimize - ALWAYS visible */} <button onClick={(e) => { e.stopPropagation(); e.preventDefault(); updateState({ isFullscreen: !isFullscreen }); }} className="p-2 md:p-1.5 rounded bg-[var(--background)] hover:bg-[var(--card-hover)] transition-colors ml-1" title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'} > {isFullscreen ? ( <Minimize2 className="h-5 w-5 md:h-4 md:w-4" /> ) : ( <Maximize2 className="h-5 w-5 md:h-4 md:w-4" /> )} </button> </div> ); return ( <> {/* Fullscreen backdrop */} {isFullscreen && ( <div className="fixed inset-0 z-[100] bg-black/80" onClick={() => updateState({ isFullscreen: false })} /> )} <div className={`rounded-lg border border-[var(--border)] overflow-hidden code-sandbox ${ isFullscreen ? 'fixed inset-2 md:inset-4 z-[101] bg-[var(--background)] rounded-lg flex flex-col' : 'max-w-full' }`} style={isFullscreen ? { paddingTop: 'env(safe-area-inset-top, 0)', paddingBottom: 'env(safe-area-inset-bottom, 0)' } : undefined} > {/* Header - minimal in fullscreen */} <div className="flex items-center justify-between px-2 md:px-3 py-2 bg-[var(--accent)] border-b border-[var(--border)] flex-shrink-0"> <div className="flex items-center gap-2 min-w-0"> <span className="text-xs font-medium truncate"> {title || `${language.toUpperCase()}`} </span> </div> {/* Show controls in header only when NOT fullscreen */} {!isFullscreen && <ToolbarButtons />} {/* In fullscreen, just show minimize button in header */} {isFullscreen && ( <button onClick={() => updateState({ isFullscreen: false })} className="p-2 rounded hover:bg-[var(--background)] transition-colors" title="Exit fullscreen" > <Minimize2 className="h-5 w-5" /> </button> )} </div> {/* Error Display */} {error && ( <div className="flex items-center gap-2 px-3 py-2 bg-[var(--error)]/10 text-[var(--error)] text-xs flex-shrink-0"> <AlertTriangle className="h-3.5 w-3.5" /> {error} </div> )} {/* Preview */} <div className={`overflow-auto ${isFullscreen ? 'flex-1 min-h-0 bg-[var(--card)]' : 'h-48 md:h-64 bg-[var(--card)]'}`}> <iframe ref={iframeRef} className="w-full h-full border-0 bg-white" sandbox="allow-scripts allow-modals allow-same-origin" title={title || 'Code Preview'} /> </div> {/* Footer controls - only in fullscreen mode */} {isFullscreen && ( <div className="flex-shrink-0 px-3 py-3 bg-[var(--accent)] border-t border-[var(--border)]"> <ToolbarButtons inFooter /> </div> )} </div> </> );}
// CRITICAL
'use client';
import { useRef, useEffect, useCallback, useId } from 'react';
import {
Play,
Square,
Copy,
Check,
Download,
Maximize2,
Minimize2,
RefreshCw,
AlertTriangle,
} from 'lucide-react';
import { useAppStore } from '@/store';
interface CodeSandboxProps {
code: string;
language: 'html' | 'react' | 'javascript';
title?: string;
autoRun?: boolean;
onOutput?: (output: string) => void;
onError?: (error: string) => void;
}
const transformReactCode = (code: string) => {
let transformed = code || '';
// Strip common import/export patterns that won't work in a plain iframe without bundling.
transformed = transformed.replace(/^\s*import\s+.*?;?\s*$/gm, '');
transformed = transformed.replace(/^\s*export\s+\{[^}]+\}\s*;?\s*$/gm, '');
transformed = transformed.replace(/^\s*export\s+(const|let|var|function|class)\s+/gm, '$1 ');
// Capture default export into a known global.
transformed = transformed.replace(/\bexport\s+default\s+/g, 'window.__DEFAULT_EXPORT__ = ');
return transformed.trim();
};
// Template for React execution
const REACT_TEMPLATE = (code: string) => `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script 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>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: system-ui, -apple-system, sans-serif; padding: 16px; }
.error { color: #b91c1c; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; white-space: pre-wrap; }
</style>
</head>
<body>
<div id="root"></div>
<div id="error" class="error" style="display:none;"></div>
<script type="text/babel" data-presets="react,typescript">
(() => {
const send = (type, message) => {
try { window.parent && window.parent.postMessage({ type, message }, '*'); } catch {}
};
const showError = (message) => {
const el = document.getElementById('error');
if (el) {
el.style.display = 'block';
el.textContent = message;
}
};
window.addEventListener('error', (event) => {
const msg = event?.error?.stack || event?.message || 'Unknown error';
send('error', msg);
showError(msg);
});
try {
window.__DEFAULT_EXPORT__ = undefined;
const { useState, useEffect, useMemo, useRef, useCallback } = React;
${transformReactCode(code)}
// Try to render common component patterns
const container = document.getElementById('root');
const root = ReactDOM.createRoot(container);
const candidate =
(typeof App !== 'undefined' && App) ||
window.__DEFAULT_EXPORT__ ||
null;
if (!candidate) {
const msg = 'React preview: define an App component or use export default.';
send('error', msg);
showError(msg);
return;
}
const element = React.isValidElement(candidate)
? candidate
: React.createElement(candidate);
root.render(element);
} catch (e) {
const msg = e?.stack || e?.message || 'Failed to render React preview';
send('error', msg);
showError(msg);
}
})();
</script>
</body>
</html>
`;
// Template for vanilla JavaScript
const JS_TEMPLATE = (code: string) => `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: system-ui, -apple-system, sans-serif; padding: 16px; }
#output { white-space: pre-wrap; font-family: monospace; }
</style>
</head>
<body>
<div id="output"></div>
<script>
const output = document.getElementById('output');
const send = (type, message) => {
try { window.parent && window.parent.postMessage({ type, message }, '*'); } catch {}
};
const log = (...args) => {
const line = args.map(a =>
typeof a === 'object' ? JSON.stringify(a, null, 2) : String(a)
).join(' ') + '\\n';
output.textContent += line;
send('output', line);
};
console.log = log;
console.info = log;
console.warn = log;
console.error = (...args) => {
log(...args);
send('error', args.map(String).join(' '));
};
window.addEventListener('error', (event) => {
const msg = event?.error?.stack || event?.message || 'Unknown error';
send('error', msg);
});
try {
${code}
} catch (e) {
output.textContent = 'Error: ' + e.message;
send('error', e?.stack || e?.message || String(e));
}
</script>
</body>
</html>
`;
export function CodeSandbox({
code,
language,
title,
autoRun = false,
onOutput,
onError,
}: CodeSandboxProps) {
const sandboxId = useId();
const sandboxState = useAppStore(
(state) =>
state.codeSandboxState[sandboxId] ?? {
isRunning: false,
isFullscreen: false,
copied: false,
error: null,
},
);
const updateCodeSandboxState = useAppStore((state) => state.updateCodeSandboxState);
const { isRunning, isFullscreen, copied, error } = sandboxState;
const updateState = (partial: Partial<typeof sandboxState>) => {
updateCodeSandboxState(sandboxId, (prev) => ({ ...prev, ...partial }));
};
const iframeRef = useRef<HTMLIFrameElement>(null);
const getSrcDoc = useCallback(() => {
switch (language) {
case 'html':
return code;
case 'react':
return REACT_TEMPLATE(code);
case 'javascript':
return JS_TEMPLATE(code);
default:
return code;
}
}, [code, language]);
const runCode = useCallback(() => {
updateState({ isRunning: true, error: null });
if (iframeRef.current) {
try {
iframeRef.current.srcdoc = getSrcDoc();
} catch (e) {
const errorMsg = e instanceof Error ? e.message : 'Failed to run code';
updateState({ error: errorMsg });
onError?.(errorMsg);
}
}
}, [getSrcDoc, onError, updateState]);
const stopCode = useCallback(() => {
updateState({ isRunning: false });
if (iframeRef.current) {
iframeRef.current.srcdoc = '';
}
}, [updateState]);
const copyCode = useCallback(() => {
navigator.clipboard.writeText(code);
updateState({ copied: true });
setTimeout(() => updateState({ copied: false }), 2000);
}, [code, updateState]);
const downloadCode = useCallback(() => {
const blob = new Blob([getSrcDoc()], { type: 'text/html' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${title || 'artifact'}.html`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, [getSrcDoc, title]);
// Auto-run on mount if enabled
useEffect(() => {
if (!autoRun) return;
const id = window.setTimeout(() => {
runCode();
}, 0);
return () => window.clearTimeout(id);
}, [autoRun, runCode]);
// Listen for iframe errors
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
if (event.data?.type === 'error') {
updateState({ error: event.data.message });
onError?.(event.data.message);
} else if (event.data?.type === 'output') {
onOutput?.(event.data.message);
}
};
window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, [onError, onOutput, updateState]);
// Toolbar buttons component to avoid duplication
const ToolbarButtons = ({ inFooter = false }: { inFooter?: boolean }) => (
<div className={`flex items-center gap-0.5 md:gap-1 ${inFooter ? 'justify-center' : ''}`}>
{/* Run/Stop - always visible */}
{isRunning ? (
<button
onClick={stopCode}
className="p-2 md:p-1.5 rounded hover:bg-[var(--error)]/20 text-[var(--error)] transition-colors"
title="Stop"
>
<Square className="h-5 w-5 md:h-4 md:w-4" />
</button>
) : (
<button
onClick={runCode}
className="p-2 md:p-1.5 rounded hover:bg-[var(--success)]/20 text-[var(--success)] transition-colors"
title="Run"
>
<Play className="h-5 w-5 md:h-4 md:w-4" />
</button>
)}
{/* Refresh */}
<button
onClick={runCode}
className={`${inFooter ? 'block' : 'hidden md:block'} p-2 md:p-1.5 rounded hover:bg-[var(--accent)] transition-colors`}
title="Refresh"
>
<RefreshCw className="h-5 w-5 md:h-4 md:w-4 text-[#9a9590]" />
</button>
{/* Copy */}
<button
onClick={copyCode}
className={`${inFooter ? 'block' : 'hidden md:block'} p-2 md:p-1.5 rounded hover:bg-[var(--accent)] transition-colors`}
title="Copy code"
>
{copied ? (
<Check className="h-5 w-5 md:h-4 md:w-4 text-[var(--success)]" />
) : (
<Copy className="h-5 w-5 md:h-4 md:w-4 text-[#9a9590]" />
)}
</button>
{/* Download */}
<button
onClick={downloadCode}
className={`${inFooter ? 'block' : 'hidden md:block'} p-2 md:p-1.5 rounded hover:bg-[var(--accent)] transition-colors`}
title="Download"
>
<Download className="h-5 w-5 md:h-4 md:w-4 text-[#9a9590]" />
</button>
{/* Fullscreen/Minimize - ALWAYS visible */}
<button
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
updateState({ isFullscreen: !isFullscreen });
}}
className="p-2 md:p-1.5 rounded bg-[var(--background)] hover:bg-[var(--card-hover)] transition-colors ml-1"
title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}
>
{isFullscreen ? (
<Minimize2 className="h-5 w-5 md:h-4 md:w-4" />
) : (
<Maximize2 className="h-5 w-5 md:h-4 md:w-4" />
)}
</button>
</div>
);
return (
<>
{/* Fullscreen backdrop */}
{isFullscreen && (
<div
className="fixed inset-0 z-[100] bg-black/80"
onClick={() => updateState({ isFullscreen: false })}
/>
)}
<div
className={`rounded-lg border border-[var(--border)] overflow-hidden code-sandbox ${
isFullscreen
? 'fixed inset-2 md:inset-4 z-[101] bg-[var(--background)] rounded-lg flex flex-col'
: 'max-w-full'
}`}
style={isFullscreen ? { paddingTop: 'env(safe-area-inset-top, 0)', paddingBottom: 'env(safe-area-inset-bottom, 0)' } : undefined}
>
{/* Header - minimal in fullscreen */}
<div className="flex items-center justify-between px-2 md:px-3 py-2 bg-[var(--accent)] border-b border-[var(--border)] flex-shrink-0">
<div className="flex items-center gap-2 min-w-0">
<span className="text-xs font-medium truncate">
{title || `${language.toUpperCase()}`}
</span>
</div>
{/* Show controls in header only when NOT fullscreen */}
{!isFullscreen && <ToolbarButtons />}
{/* In fullscreen, just show minimize button in header */}
{isFullscreen && (
<button
onClick={() => updateState({ isFullscreen: false })}
className="p-2 rounded hover:bg-[var(--background)] transition-colors"
title="Exit fullscreen"
>
<Minimize2 className="h-5 w-5" />
</button>
)}
</div>
{/* Error Display */}
{error && (
<div className="flex items-center gap-2 px-3 py-2 bg-[var(--error)]/10 text-[var(--error)] text-xs flex-shrink-0">
<AlertTriangle className="h-3.5 w-3.5" />
{error}
</div>
)}
{/* Preview */}
<div className={`overflow-auto ${isFullscreen ? 'flex-1 min-h-0 bg-[var(--card)]' : 'h-48 md:h-64 bg-[var(--card)]'}`}>
<iframe
ref={iframeRef}
className="w-full h-full border-0 bg-white"
sandbox="allow-scripts allow-modals allow-same-origin"
title={title || 'Code Preview'}
/>
</div>
{/* Footer controls - only in fullscreen mode */}
{isFullscreen && (
<div className="flex-shrink-0 px-3 py-3 bg-[var(--accent)] border-t border-[var(--border)]">
<ToolbarButtons inFooter />
</div>
)}
</div>
</>
);
}