// 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) => `
`;
// Template for vanilla JavaScript
const JS_TEMPLATE = (code: string) => `
`;
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) => {
updateCodeSandboxState(sandboxId, (prev) => ({ ...prev, ...partial }));
};
const iframeRef = useRef(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 }) => (
{/* Run/Stop - always visible */}
{isRunning ? (
) : (
)}
{/* Refresh */}
{/* Copy */}
{copied ? (
) : (
)}
{/* Download */}
{/* Fullscreen/Minimize - ALWAYS visible */}
{
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 ? (
) : (
)}
);
return (
<>
{/* Fullscreen backdrop */}
{isFullscreen && (
updateState({ isFullscreen: false })}
/>
)}
{/* Header - minimal in fullscreen */}
{title || `${language.toUpperCase()}`}
{/* Show controls in header only when NOT fullscreen */}
{!isFullscreen &&
}
{/* In fullscreen, just show minimize button in header */}
{isFullscreen && (
updateState({ isFullscreen: false })}
className="p-2 rounded hover:bg-[var(--background)] transition-colors"
title="Exit fullscreen"
>
)}
{/* Error Display */}
{error && (
)}
{/* Preview */}
{/* Footer controls - only in fullscreen mode */}
{isFullscreen && (
)}
>
);
}