import classNames from "classnames";
import { useRef, useEffect, useState, forwardRef } from "react";
import { TbReload, TbLoader, TbExternalLink } from "react-icons/tb";
import { WebContainer } from '@webcontainer/api';
// PreviewEye icon component
const PreviewEye = ({ className = "w-4 h-4" }: { className?: string }) => (
);
// Tooltip component
const Tooltip = ({
children,
content,
position = "top"
}: {
children: React.ReactNode;
content: string;
position?: "top" | "bottom" | "left" | "right"
}) => {
const [isVisible, setIsVisible] = useState(false);
const positionClasses = {
top: "bottom-full left-1/2 transform -translate-x-1/2 mb-2",
bottom: "top-full left-1/2 transform -translate-x-1/2 mt-2",
left: "right-full top-1/2 transform -translate-y-1/2 mr-2",
right: "left-full top-1/2 transform -translate-y-1/2 ml-2"
};
return (
setIsVisible(true)}
onMouseLeave={() => setIsVisible(false)}
>
{children}
{isVisible && (
)}
);
};
type PreviewProps = {
files: Record;
isResizing: boolean;
isAiWorking: boolean;
packageJson?: any;
className?: string;
};
const Preview = forwardRef(
({ files, isResizing, isAiWorking, packageJson, className }, ref) => {
const iframeRef = useRef(null);
const [webcontainer, setWebcontainer] = useState(null);
const [url, setUrl] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const [isFullscreen, setIsFullscreen] = useState(false);
// Initialize WebContainer
useEffect(() => {
const initWebContainer = async () => {
try {
const container = await WebContainer.boot();
setWebcontainer(container);
} catch (err) {
setError('Failed to initialize WebContainer');
console.error('WebContainer init error:', err);
}
};
initWebContainer();
}, []);
// Mount files and start dev server
useEffect(() => {
if (!webcontainer || !files) return;
const setupProject = async () => {
setIsLoading(true);
setError('');
try {
await webcontainer.mount(files);
const defaultPackageJson = {
name: 'preview-app',
type: 'module',
scripts: {
dev: 'vite',
build: 'vite build',
preview: 'vite preview'
},
devDependencies: {
vite: '^5.0.0'
},
...packageJson
};
if (!files['package.json']) {
await webcontainer.fs.writeFile(
'package.json',
JSON.stringify(defaultPackageJson, null, 2)
);
}
const installProcess = await webcontainer.spawn('npm', ['install']);
const installExitCode = await installProcess.exit;
if (installExitCode !== 0) {
throw new Error('Failed to install dependencies');
}
const serverProcess = await webcontainer.spawn('npm', ['run', 'dev']);
webcontainer.on('server-ready', (port, url) => {
setUrl(url);
setIsLoading(false);
});
serverProcess.output.pipeTo(
new WritableStream({
write(data) {
console.log('[Server]', data);
},
})
);
} catch (err) {
setError(err instanceof Error ? err.message : 'Setup failed');
setIsLoading(false);
console.error('Setup error:', err);
}
};
setupProject();
}, [webcontainer, files, packageJson]);
const handleRefresh = async () => {
if (!webcontainer || !url) return;
setIsLoading(true);
try {
await webcontainer.spawn('npm', ['run', 'dev']);
if (iframeRef.current) {
const iframe = iframeRef.current;
iframe.src = '';
setTimeout(() => {
iframe.src = url;
}, 100);
}
} catch (err) {
console.error('Refresh error:', err);
} finally {
setIsLoading(false);
}
};
const handleRestartContainer = async () => {
if (!webcontainer) return;
setIsLoading(true);
setUrl('');
try {
await webcontainer.spawn('pkill', ['-f', 'node']);
await webcontainer.mount(files);
const serverProcess = await webcontainer.spawn('npm', ['run', 'dev']);
webcontainer.on('server-ready', (port, url) => {
setUrl(url);
setIsLoading(false);
});
} catch (err) {
setError('Failed to restart container');
setIsLoading(false);
console.error('Restart error:', err);
}
};
const openInNewTab = () => {
if (url) {
window.open(url, '_blank');
}
};
return (
{/* Header bar - Vercel style */}
Preview
{url && (
{new URL(url).host}
)}
{/* Content area */}
{error ? (
Container Error
{error}
Restart Container
) : !url || isLoading ? (
{!webcontainer
? 'Booting WebContainer...'
: 'Setting up project...'}
This may take a moment
) : (
)}
{/* Status indicator */}
{url && (
)}
);
}
);
Preview.displayName = "Preview";
export default Preview;
Yes β youβre connecting two ideas:
1. WebContainer API β run and preview code directly in the browser.
2. Spaces (like Hugging Face Spaces) β publish/share the project so others can use it.
That flow is totally logical:
---
π Workflow: From Local Run β Published Space
---
β
Why this works
WebContainer gives you a runtime (sandboxed Node.js) to build/preview your app.
Spaces give you persistence & hosting so your preview can be shared, reused, or embedded.
By publishing as a Space, you create a versioned, public (or private) endpoint.
Yes π exactly β what youβve built is already 90% of the flow.
Right now your publishBtn just exports a space-export.json snapshot. To fully automate:
1. Boot WebContainer β Run project (you already do this β
).
2. Collect project files + metadata (zip them).
3. Call the Hugging Face Spaces API with your token to create/update a Space.
4. Push the ZIP β Space repo.
5. Instantly embed via