webcontainer-preview / prompts.txt
HuggingFaceDAO's picture
Yes 👌 exactly — what you’ve built is already 90% of the flow.
df6ab71 verified
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 }) => (
<svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
<circle cx="12" cy="12" r="3" />
</svg>
);
// 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 (
<div
className="relative inline-block"
onMouseEnter={() => setIsVisible(true)}
onMouseLeave={() => setIsVisible(false)}
>
{children}
{isVisible && (
<div className={classNames(
"absolute z-50 px-2 py-1 text-xs text-white bg-gray-900 rounded shadow-lg whitespace-nowrap pointer-events-none",
positionClasses[position]
)}>
{content}
<div className={classNames(
"absolute w-1 h-1 bg-gray-900 transform rotate-45",
position === "top" && "top-full left-1/2 -translate-x-1/2 -mt-0.5",
position === "bottom" && "bottom-full left-1/2 -translate-x-1/2 -mb-0.5",
position === "left" && "left-full top-1/2 -translate-y-1/2 -ml-0.5",
position === "right" && "right-full top-1/2 -translate-y-1/2 -mr-0.5"
)} />
</div>
)}
</div>
);
};
type PreviewProps = {
files: Record<string, any>;
isResizing: boolean;
isAiWorking: boolean;
packageJson?: any;
className?: string;
};
const Preview = forwardRef<HTMLDivElement, PreviewProps>(
({ files, isResizing, isAiWorking, packageJson, className }, ref) => {
const iframeRef = useRef<HTMLIFrameElement | null>(null);
const [webcontainer, setWebcontainer] = useState<WebContainer | null>(null);
const [url, setUrl] = useState<string>('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string>('');
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 (
<div
ref={ref}
className={classNames(
// Vercel-style grid system
"w-full relative",
// Base responsive grid
"col-span-full", // Full width on mobile
"md:col-span-6", // Half width on medium screens
"lg:col-span-8", // Larger portion on desktop
"xl:col-span-9", // Even larger on xl screens
// Height system
"h-[calc(70dvh-53px)]",
"lg:h-[calc(100dvh-54px)]",
// Borders and styling
"border border-gray-200",
"rounded-lg overflow-hidden",
"bg-white shadow-sm",
className
)}
>
{/* Header bar - Vercel style */}
<div className="flex items-center justify-between px-4 py-2 bg-gray-50 border-b border-gray-200">
<div className="flex items-center gap-2">
<PreviewEye className="w-4 h-4 text-gray-500" />
<span className="text-sm font-medium text-gray-700">Preview</span>
{url && (
<span className="text-xs text-gray-500 font-mono">
{new URL(url).host}
</span>
)}
</div>
<div className="flex items-center gap-1">
<Tooltip content="Refresh preview">
<button
className="p-1.5 hover:bg-gray-200 rounded-md transition-colors disabled:opacity-50"
onClick={handleRefresh}
disabled={!url || isLoading}
>
<TbReload
className={classNames("w-4 h-4 text-gray-600", {
"animate-spin": isLoading
})}
/>
</button>
</Tooltip>
<Tooltip content="Open in new tab">
<button
className="p-1.5 hover:bg-gray-200 rounded-md transition-colors disabled:opacity-50"
onClick={openInNewTab}
disabled={!url}
>
<TbExternalLink className="w-4 h-4 text-gray-600" />
</button>
</Tooltip>
<Tooltip content="Restart container">
<button
className="p-1.5 hover:bg-gray-200 rounded-md transition-colors disabled:opacity-50"
onClick={handleRestartContainer}
disabled={!webcontainer || isLoading}
>
<TbLoader
className={classNames("w-4 h-4 text-gray-600", {
"animate-spin": isLoading
})}
/>
</button>
</Tooltip>
</div>
</div>
{/* Content area */}
<div className="flex-1 relative h-[calc(100%-49px)]">
{error ? (
<div className="flex items-center justify-center h-full p-8">
<div className="text-center max-w-md">
<div className="w-12 h-12 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
<svg className="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">
Container Error
</h3>
<p className="text-sm text-red-600 mb-4">{error}</p>
<button
onClick={handleRestartContainer}
className="inline-flex items-center px-4 py-2 bg-red-600 hover:bg-red-700 text-white text-sm font-medium rounded-md transition-colors"
>
<TbReload className="w-4 h-4 mr-2" />
Restart Container
</button>
</div>
</div>
) : !url || isLoading ? (
<div className="flex items-center justify-center h-full">
<div className="text-center">
<div className="w-8 h-8 border-2 border-gray-300 border-t-blue-600 rounded-full animate-spin mx-auto mb-4"></div>
<div className="text-sm text-gray-600 font-medium">
{!webcontainer
? 'Booting WebContainer...'
: 'Setting up project...'}
</div>
<div className="text-xs text-gray-400 mt-1">
This may take a moment
</div>
</div>
</div>
) : (
<iframe
ref={iframeRef}
title="WebContainer Preview"
src={url}
className={classNames("w-full h-full border-0", {
"pointer-events-none": isResizing || isAiWorking,
})}
sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
/>
)}
</div>
{/* Status indicator */}
{url && (
<div className="absolute bottom-2 left-2 flex items-center gap-2 bg-green-50 border border-green-200 rounded-full px-2 py-1">
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
<span className="text-xs text-green-700 font-medium">Live</span>
</div>
)}
</div>
);
}
);
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
<LinearProcessFlow steps={[ "⚡ Boot WebContainer runtime in the browser", "📂 Load project files into the container (FS API)", "📦 Install deps (npm / yarn / pnpm)", "▶️ Start dev/build server in-browser", "🌍 Save and publish to a Space on the Hub", "🖼️ Import and use Space directly in your UI" ]} />
---
✅ 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 <iframe> or query via API.
---
🔄 Combined Flow
<LinearProcessFlow steps={[ "Boot WebContainer (mount project)", "Zip project files (with JSZip or similar)", "POST to Hugging Face API → create/update Space repo", "Upload files to repo", "Trigger Space build & wait for it to go live", "Embed Space iframe in your UI" ]} />
---
Example: Auto-Publish Script (client-side with token)
import JSZip from "jszip";
async function publishToSpaces(webcontainer, spaceName, hfToken) {
// 1. Collect files from WebContainer FS
const zip = new JSZip();
async function walk(path = ".") {
const entries = await webcontainer.fs.readdir(path, { withFileTypes: true });
for (const entry of entries) {
const fullPath = `${path}/${entry.name}`;
if (entry.kind === "directory") {
await walk(fullPath);
} else {
const content = await webcontainer.fs.readFile(fullPath, "utf-8");
zip.file(fullPath.replace(/^\.\//, ""), content);
}
}
}
await walk(".");
// 2. Generate ZIP blob
const blob = await zip.generateAsync({ type: "blob" });
// 3. Upload to Hugging Face Spaces repo
const formData = new FormData();
formData.append("file", blob, "project.zip");
const res = await fetch(
`https://huggingface.co/api/spaces/likhonsheikh/${spaceName}/commit/main`,
{
method: "POST",
headers: {
Authorization: `Bearer ${hfToken}`,
},
body: formData,
}
);
if (!res.ok) throw new Error(`Failed to publish: ${res.statusText}`);
return await res.json();
}
---
Usage inside your page
publishBtn.addEventListener("click", async () => {
try {
const result = await publishToSpaces(
webcontainer,
"my-preview-app",
"<YOUR_HF_TOKEN>"
);
alert("✅ Space published! Visit: https://huggingface.co/spaces/likhonsheikh/my-preview-app");
} catch (err) {
console.error(err);
alert("❌ Failed to publish: " + err.message);
}
});
---
⚡ Now the flow is fully automated: run → package → publish → embed.
No manual upload needed — your project “becomes a Space” directly from the browser.
---
👉 Do you want me to also add the UI feedback loop (spinner on Publish, success/error toast, and a direct “Open Space” button after publishing)?
Perfect! Let's build the complete auto-publish flow with UI feedback. I'll create a comprehensive publish system that handles the entire WebContainer → Hugging Face Spaces pipeline:// publishUtils.ts - Production-ready Hugging Face Spaces auto-publish
import JSZip from 'jszip';
export interface PublishConfig {
spaceName: string;
hfToken: string;
spaceType?: 'gradio' | 'streamlit' | 'static' | 'docker';
isPrivate?: boolean;
description?: string;
}
export interface PublishProgress {
step: 'collecting' | 'zipping' | 'creating' | 'uploading' | 'building' | 'complete' | 'error';
progress: number;
message: string;
error?: string;
}
export class HuggingFaceSpacesPublisher {
private onProgress: (progress: PublishProgress) => void;
constructor(onProgress: (progress: PublishProgress) => void) {
this.onProgress = onProgress;
}
/**
* Recursively collect all files from WebContainer filesystem
*/
private async collectFilesFromWebContainer(webcontainer: any): Promise<JSZip> {
const zip = new JSZip();
const walkDirectory = async (path: string = '.') => {
try {
const entries = await webcontainer.fs.readdir(path, { withFileTypes: true });
for (const entry of entries) {
// Skip node_modules and other unnecessary directories
if (entry.name === 'node_modules' || entry.name === '.git' || entry.name.startsWith('.')) {
continue;
}
const fullPath = path === '.' ? entry.name : `${path}/${entry.name}`;
if (entry.isDirectory()) {
await walkDirectory(fullPath);
} else {
try {
const content = await webcontainer.fs.readFile(fullPath, 'utf-8');
zip.file(fullPath, content);
} catch (err) {
// Try reading as binary if UTF-8 fails
try {
const content = await webcontainer.fs.readFile(fullPath);
zip.file(fullPath, content);
} catch (binaryErr) {
console.warn(`Failed to read file: ${fullPath}`, binaryErr);
}
}
}
}
} catch (err) {
console.error(`Failed to read directory: ${path}`, err);
}
};
await walkDirectory();
return zip;
}
/**
* Create or update a Space on Hugging Face
*/
private async createOrUpdateSpace(config: PublishConfig): Promise<string> {
const spaceId = `${this.extractUsername(config.hfToken)}/${config.spaceName}`;
// First, try to create the space
try {
const createResponse = await fetch('https://huggingface.co/api/repos/create', {
method: 'POST',
headers: {
'Authorization': `Bearer ${config.hfToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'space',
name: config.spaceName,
private: config.isPrivate || false,
sdk: config.spaceType || 'static',
...(config.description && { description: config.description }),
}),
});
if (!createResponse.ok && createResponse.status !== 409) {
throw new Error(`Failed to create space: ${createResponse.statusText}`);
}
return spaceId;
} catch (err) {
// If space already exists, that's fine - we'll update it
console.log('Space might already exist, proceeding with upload...');
return spaceId;
}
}
/**
* Extract username from HF token (simplified - in production you'd call the API)
*/
private extractUsername(token: string): string {
// In production, you'd call https://huggingface.co/api/whoami
// For now, we'll use a placeholder or ask user to provide username
return 'likhonsheikh'; // Replace with actual username extraction
}
/**
* Upload files to the Space repository
*/
private async uploadToSpace(spaceId: string, zipBlob: Blob, hfToken: string): Promise<void> {
// For simplicity, we'll upload the entire project as a single commit
// In production, you might want to upload files individually for better progress tracking
const formData = new FormData();
formData.append('file', zipBlob, 'project.zip');
const response = await fetch(
`https://huggingface.co/api/repos/${spaceId}/upload/main`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${hfToken}`,
},
body: formData,
}
);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Upload failed: ${response.statusText} - ${errorText}`);
}
}
/**
* Wait for Space to build and become ready
*/
private async waitForSpaceBuild(spaceId: string, hfToken: string): Promise<void> {
const maxAttempts = 30; // 5 minutes max
const delayMs = 10000; // 10 seconds between checks
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
const response = await fetch(`https://huggingface.co/api/spaces/${spaceId}`, {
headers: {
'Authorization': `Bearer ${hfToken}`,
},
});
if (response.ok) {
const spaceInfo = await response.json();
if (spaceInfo.runtime?.stage === 'RUNNING') {
return; // Space is ready!
}
// Update progress based on build stage
const stage = spaceInfo.runtime?.stage || 'PENDING';
const progressMap: { [key: string]: number } = {
'PENDING': 75,
'BUILDING': 85,
'RUNNING': 100,
};
this.onProgress({
step: 'building',
progress: progressMap[stage] || 80,
message: `Building Space... Status: ${stage}`,
});
}
await new Promise(resolve => setTimeout(resolve, delayMs));
} catch (err) {
console.warn('Failed to check space status:', err);
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
// If we get here, the space didn't become ready in time
console.warn('Space build timeout - it may still be building');
}
/**
* Main publish method - orchestrates the entire flow
*/
async publishToSpaces(webcontainer: any, config: PublishConfig): Promise<string> {
try {
// Step 1: Collect files
this.onProgress({
step: 'collecting',
progress: 10,
message: 'Collecting project files from WebContainer...',
});
const zip = await this.collectFilesFromWebContainer(webcontainer);
// Step 2: Create ZIP
this.onProgress({
step: 'zipping',
progress: 25,
message: 'Creating deployment package...',
});
const zipBlob = await zip.generateAsync({
type: 'blob',
compression: 'DEFLATE',
compressionOptions: { level: 6 }
});
// Step 3: Create/Update Space
this.onProgress({
step: 'creating',
progress: 40,
message: 'Creating Hugging Face Space...',
});
const spaceId = await this.createOrUpdateSpace(config);
// Step 4: Upload files
this.onProgress({
step: 'uploading',
progress: 60,
message: 'Uploading files to Space repository...',
});
await this.uploadToSpace(spaceId, zipBlob, config.hfToken);
// Step 5: Wait for build
this.onProgress({
step: 'building',
progress: 80,
message: 'Building Space environment...',
});
await this.waitForSpaceBuild(spaceId, config.hfToken);
// Step 6: Complete
const spaceUrl = `https://huggingface.co/spaces/${spaceId}`;
this.onProgress({
step: 'complete',
progress: 100,
message: `Space published successfully! 🎉`,
});
return spaceUrl;
} catch (error) {
this.onProgress({
step: 'error',
progress: 0,
message: 'Publishing failed',
error: error instanceof Error ? error.message : 'Unknown error occurred',
});
throw error;
}
}
}
// Helper function to generate Space-appropriate files
export const generateSpaceFiles = (webcontainerFiles: Record<string, any>) => {
const files = { ...webcontainerFiles };
// Add README.md if it doesn't exist
if (!files['README.md']) {
files['README.md'] = {
file: {
contents: `---
title: My App
emoji: 🚀
colorFrom: blue
colorTo: purple
sdk: static
pinned: false
---
# My App
This Space was automatically deployed from WebContainer.
`
}
};
}
// Add .gitignore
if (!files['.gitignore']) {
files['.gitignore'] = {
file: {
contents: `node_modules/
.env
.DS_Store
*.log
dist/
build/
.next/
.nuxt/
`
}
};
}
return files;
};
// Usage example:
/*
import { HuggingFaceSpacesPublisher, generateSpaceFiles } from './publishUtils';
const publisher = new HuggingFaceSpacesPublisher((progress) => {
console.log(progress);
// Update your UI here
});
const publishConfig = {
spaceName: 'my-awesome-app',
hfToken: 'hf_...',
spaceType: 'static' as const,
isPrivate: false,
description: 'My awesome WebContainer app'
};
const spaceUrl = await publisher.publishToSpaces(webcontainer, publishConfig);
console.log('Published to:', spaceUrl);
*/
import classNames from "classnames";
import { useRef, useEffect, useState, forwardRef } from "react";
import { TbReload, TbLoader, TbExternalLink, TbUpload, TbCheck, TbX } from "react-icons/tb";
import { WebContainer } from '@webcontainer/api';
import { HuggingFaceSpacesPublisher, PublishProgress, PublishConfig } from './publishUtils';
// PreviewEye icon component
const PreviewEye = ({ className = "w-4 h-4" }: { className?: string }) => (
<svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
<circle cx="12" cy="12" r="3" />
</svg>
);
// Publish Modal Component
const PublishModal = ({
isOpen,
onClose,
onPublish,
publishProgress
}: {
isOpen: boolean;
onClose: () => void;
onPublish: (config: PublishConfig) => void;
publishProgress: PublishProgress | null;
}) => {
const [config, setConfig] = useState<PublishConfig>({
spaceName: '',
hfToken: '',
spaceType: 'static',
isPrivate: false,
description: ''
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onPublish(config);
};
const isPublishing = publishProgress && ['collecting',