HuggingFaceDAO commited on
Commit
161411f
·
verified ·
1 Parent(s): 340ad13

Yes — you’re connecting two ideas:

Browse files

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. - Initial Deployment

Files changed (3) hide show
  1. README.md +7 -5
  2. index.html +395 -18
  3. prompts.txt +378 -0
README.md CHANGED
@@ -1,10 +1,12 @@
1
  ---
2
- title: Webcontainer Preview
3
- emoji: 🌍
4
- colorFrom: gray
5
- colorTo: purple
6
  sdk: static
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: webcontainer-preview
3
+ emoji: 🐳
4
+ colorFrom: blue
5
+ colorTo: pink
6
  sdk: static
7
  pinned: false
8
+ tags:
9
+ - deepsite
10
  ---
11
 
12
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
index.html CHANGED
@@ -1,19 +1,396 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8"/>
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
+ <title>WebContainer Live Preview</title>
7
+ <link rel="icon" type="image/x-icon" href="data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%233b82f6' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z'/%3E%3Ccircle cx='12' cy='12' r='3'/%3E%3C/svg%3E">
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet">
10
+ <script src="https://unpkg.com/aos@2.3.1/dist/aos.js"></script>
11
+ <script src="https://unpkg.com/feather-icons"></script>
12
+ <style>
13
+ /* Custom scrollbar */
14
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
15
+ ::-webkit-scrollbar-track { background: transparent; }
16
+ ::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; }
17
+ ::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
18
+ /* Animated pulse for live dot */
19
+ @keyframes pulse-dot {
20
+ 0%,100%{transform:scale(1);opacity:1;}
21
+ 50%{transform:scale(1.4);opacity:.8;}
22
+ }
23
+ .pulse-dot{animation:pulse-dot 1.5s infinite;}
24
+ </style>
25
+ </head>
26
+ <body class="bg-gray-50 text-gray-900 font-sans antialiased">
27
+
28
+ <!-- Header -->
29
+ <header class="sticky top-0 z-20 bg-white/80 backdrop-blur border-b border-gray-200">
30
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
31
+ <div class="flex items-center justify-between h-14">
32
+ <div class="flex items-center gap-2">
33
+ <i data-feather="box" class="w-5 h-5 text-blue-600"></i>
34
+ <h1 class="text-lg font-semibold">WebContainer Preview</h1>
35
+ </div>
36
+ <div class="flex items-center gap-3">
37
+ <button id="publishBtn" class="hidden text-sm bg-indigo-600 hover:bg-indigo-700 text-white px-3 py-1.5 rounded-md transition items-center gap-2">
38
+ <i data-feather="upload-cloud" class="w-4 h-4"></i><span>Publish to Space</span>
39
+ </button>
40
+ <div class="flex items-center gap-2 text-sm text-gray-500">
41
+ <span id="status">Offline</span>
42
+ <span class="w-2 h-2 bg-gray-300 rounded-full" id="status-dot"></span>
43
+ </div>
44
+ </div>
45
+ </div>
46
+ </div>
47
+ </header>
48
+
49
+ <!-- Main -->
50
+ <main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
51
+ <section class="grid grid-cols-1 md:grid-cols-6 lg:grid-cols-8 xl:grid-cols-9 gap-4">
52
+
53
+ <!-- Controls -->
54
+ <aside class="col-span-full md:col-span-6 lg:col-span-8 xl:col-span-3 flex flex-col gap-4" data-aos="fade-right">
55
+ <!-- Upload -->
56
+ <div class="bg-white rounded-xl border border-gray-200 p-4 shadow-sm">
57
+ <h2 class="text-sm font-semibold mb-3 flex items-center gap-2">
58
+ <i data-feather="upload" class="w-4 h-4 text-indigo-600"></i>Upload Project ZIP
59
+ </h2>
60
+ <label class="group relative flex flex-col items-center justify-center w-full h-28 border-2 border-dashed border-gray-300 rounded-lg cursor-pointer hover:border-indigo-400 hover:bg-indigo-50 transition">
61
+ <i data-feather="folder-plus" class="w-8 h-8 text-gray-400 group-hover:text-indigo-600"></i>
62
+ <span class="mt-2 text-sm text-gray-500 group-hover:text-indigo-600">Click or drop ZIP here</span>
63
+ <input id="zipInput" type="file" accept=".zip" class="absolute opacity-0 w-full h-full">
64
+ </label>
65
+ <p id="fileName" class="mt-2 text-xs text-gray-400 truncate"></p>
66
+ </div>
67
+
68
+ <!-- Actions -->
69
+ <div class="bg-white rounded-xl border border-gray-200 p-4 shadow-sm">
70
+ <h2 class="text-sm font-semibold mb-3 flex items-center gap-2">
71
+ <i data-feather="play" class="w-4 h-4 text-green-600"></i>Actions
72
+ </h2>
73
+ <div class="grid grid-cols-2 gap-2">
74
+ <button id="bootBtn" class="flex items-center justify-center gap-2 px-3 py-2 bg-green-600 hover:bg-green-700 text-white text-sm rounded-lg transition">
75
+ <i data-feather="zap"></i><span>Boot</span>
76
+ </button>
77
+ <button id="stopBtn" class="flex items-center justify-center gap-2 px-3 py-2 bg-gray-200 hover:bg-gray-300 text-gray-800 text-sm rounded-lg transition">
78
+ <i data-feather="square"></i><span>Stop</span>
79
+ </button>
80
+ </div>
81
+ </div>
82
+
83
+ <!-- Terminal -->
84
+ <div class="bg-gray-900 rounded-xl border border-gray-700 p-4 shadow-sm flex-1 flex flex-col">
85
+ <div class="flex items-center justify-between mb-2">
86
+ <h2 class="text-sm font-semibold text-gray-200 flex items-center gap-2">
87
+ <i data-feather="terminal" class="w-4 h-4 text-green-400"></i>Terminal
88
+ </h2>
89
+ <button id="clearTerm" class="text-xs text-gray-400 hover:text-white transition">
90
+ Clear
91
+ </button>
92
+ </div>
93
+ <pre id="terminal" class="flex-1 text-xs text-green-400 overflow-auto whitespace-pre-wrap"></pre>
94
+ </div>
95
+ </aside>
96
+
97
+ <!-- Preview -->
98
+ <div id="previewWrapper" class="col-span-full md:col-span-6 lg:col-span-8 xl:col-span-6 h-[calc(70vh-53px)] lg:h-[calc(100vh-54px)] bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden flex flex-col" data-aos="fade-left">
99
+ <!-- Preview Header -->
100
+ <div class="flex items-center justify-between px-4 py-2 bg-gray-50 border-b border-gray-200">
101
+ <div class="flex items-center gap-2">
102
+ <i data-feather="eye" class="w-4 h-4 text-gray-500"></i>
103
+ <span class="text-sm font-medium text-gray-700">Preview</span>
104
+ <span id="previewHost" class="text-xs text-gray-500 font-mono hidden"></span>
105
+ </div>
106
+ <div class="flex items-center gap-1">
107
+ <button id="refreshBtn" class="p-1.5 hover:bg-gray-200 rounded-md transition disabled:opacity-50" title="Refresh">
108
+ <i data-feather="refresh-cw" class="w-4 h-4 text-gray-600"></i>
109
+ </button>
110
+ <button id="openBtn" class="p-1.5 hover:bg-gray-200 rounded-md transition disabled:opacity-50" title="Open in new tab">
111
+ <i data-feather="external-link" class="w-4 h-4 text-gray-600"></i>
112
+ </button>
113
+ <button id="restartBtn" class="p-1.5 hover:bg-gray-200 rounded-md transition disabled:opacity-50" title="Restart container">
114
+ <i data-feather="rotate-ccw" class="w-4 h-4 text-gray-600"></i>
115
+ </button>
116
+ </div>
117
+ </div>
118
+
119
+ <!-- Preview Body -->
120
+ <div class="flex-1 relative">
121
+ <!-- Loading -->
122
+ <div id="loader" class="absolute inset-0 flex items-center justify-center bg-white z-10">
123
+ <div class="text-center">
124
+ <div class="w-8 h-8 border-2 border-gray-300 border-t-blue-600 rounded-full animate-spin mx-auto mb-3"></div>
125
+ <div class="text-sm text-gray-600 font-medium">Booting WebContainer…</div>
126
+ <div class="text-xs text-gray-400 mt-1">This may take a moment</div>
127
+ </div>
128
+ </div>
129
+
130
+ <!-- Error -->
131
+ <div id="error" class="absolute inset-0 hidden items-center justify-center bg-white z-10">
132
+ <div class="text-center max-w-md">
133
+ <div class="w-12 h-12 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
134
+ <i data-feather="alert-triangle" class="w-6 h-6 text-red-600"></i>
135
+ </div>
136
+ <h3 class="text-lg font-medium text-gray-900 mb-2">Container Error</h3>
137
+ <p id="errorText" class="text-sm text-red-600 mb-4"></p>
138
+ <button id="retryBtn" class="inline-flex items-center px-4 py-2 bg-red-600 hover:bg-red-700 text-white text-sm font-medium rounded-md transition">
139
+ <i data-feather="rotate-ccw" class="w-4 h-4 mr-2"></i>Restart Container
140
+ </button>
141
+ </div>
142
+ </div>
143
+
144
+ <!-- Iframe -->
145
+ <iframe id="preview" class="w-full h-full border-0" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"></iframe>
146
+
147
+ <!-- Live indicator -->
148
+ <div id="liveIndicator" class="absolute bottom-2 left-2 hidden items-center gap-2 bg-green-50 border border-green-200 rounded-full px-2 py-1">
149
+ <span class="w-2 h-2 bg-green-500 rounded-full pulse-dot"></span>
150
+ <span class="text-xs text-green-700 font-medium">Live</span>
151
+ </div>
152
+ </div>
153
+ </div>
154
+ </section>
155
+ </main>
156
+
157
+ <script type="module">
158
+ /* ---------- Feather Icons ---------- */
159
+ feather.replace();
160
+
161
+ /* ---------- Elements ---------- */
162
+ const zipInput = document.getElementById('zipInput');
163
+ const fileName = document.getElementById('fileName');
164
+ const bootBtn = document.getElementById('bootBtn');
165
+ const stopBtn = document.getElementById('stopBtn');
166
+ const clearTerm = document.getElementById('clearTerm');
167
+ const terminal = document.getElementById('terminal');
168
+ const status = document.getElementById('status');
169
+ const statusDot = document.getElementById('status-dot');
170
+ const loader = document.getElementById('loader');
171
+ const error = document.getElementById('error');
172
+ const errorText = document.getElementById('errorText');
173
+ const retryBtn = document.getElementById('retryBtn');
174
+ const refreshBtn = document.getElementById('refreshBtn');
175
+ const openBtn = document.getElementById('openBtn');
176
+ const restartBtn = document.getElementById('restartBtn');
177
+ const preview = document.getElementById('preview');
178
+ const previewHost = document.getElementById('previewHost');
179
+ const liveIndicator = document.getElementById('liveIndicator');
180
+ const publishBtn = document.getElementById('publishBtn');
181
+
182
+ let webcontainer = null;
183
+ let url = '';
184
+
185
+ /* ---------- Util ---------- */
186
+ const log = (msg, type = 'info') => {
187
+ const stamp = `[${new Date().toLocaleTimeString()}]`;
188
+ const color = type === 'error' ? 'text-red-400' : type === 'warn' ? 'text-yellow-400' : 'text-green-400';
189
+ terminal.insertAdjacentHTML('beforeend', `<div><span class="${color}">${stamp}</span> ${msg}</div>`);
190
+ terminal.scrollTop = terminal.scrollHeight;
191
+ };
192
+
193
+ const setStatus = (state) => {
194
+ const labels = { offline: 'Offline', booting: 'Booting', online: 'Online', error: 'Error' };
195
+ const colors = {
196
+ offline: 'bg-gray-300',
197
+ booting: 'bg-yellow-400',
198
+ online: 'bg-green-500',
199
+ error: 'bg-red-500'
200
+ };
201
+ status.textContent = labels[state];
202
+ statusDot.className = `w-2 h-2 rounded-full ${colors[state]}`;
203
+ };
204
+
205
+ const showLoader = () => {
206
+ loader.classList.remove('hidden');
207
+ loader.classList.add('flex');
208
+ error.classList.add('hidden');
209
+ error.classList.remove('flex');
210
+ };
211
+
212
+ const hideLoader = () => {
213
+ loader.classList.add('hidden');
214
+ loader.classList.remove('flex');
215
+ };
216
+
217
+ const showError = (msg) => {
218
+ hideLoader();
219
+ errorText.textContent = msg;
220
+ error.classList.remove('hidden');
221
+ error.classList.add('flex');
222
+ };
223
+
224
+ const hideError = () => {
225
+ error.classList.add('hidden');
226
+ error.classList.remove('flex');
227
+ };
228
+
229
+ const setLive = (u) => {
230
+ url = u;
231
+ preview.src = url;
232
+ previewHost.textContent = new URL(url).host;
233
+ previewHost.classList.remove('hidden');
234
+ liveIndicator.classList.remove('hidden');
235
+ setStatus('online');
236
+ publishBtn.classList.remove('hidden');
237
+ publishBtn.classList.add('inline-flex');
238
+ };
239
+
240
+ /* ---------- File Input ---------- */
241
+ zipInput.addEventListener('change', (e) => {
242
+ const f = e.target.files[0];
243
+ if (f) fileName.textContent = f.name;
244
+ });
245
+
246
+ /* ---------- Terminal Clear ---------- */
247
+ clearTerm.addEventListener('click', () => terminal.innerHTML = '');
248
+
249
+ /* ---------- Boot ---------- */
250
+ bootBtn.addEventListener('click', async () => {
251
+ if (!zipInput.files.length) return alert('Please select a ZIP file');
252
+ showLoader();
253
+ hideError();
254
+ setStatus('booting');
255
+ log('Booting WebContainer…');
256
+
257
+ try {
258
+ const { WebContainer } = await import('@webcontainer/api');
259
+ webcontainer = await WebContainer.boot();
260
+ log('WebContainer booted');
261
+
262
+ const zip = zipInput.files[0];
263
+ const zipBuffer = await zip.arrayBuffer();
264
+ const zipArray = new Uint8Array(zipBuffer);
265
+ const unzip = (await import('unzipit')).unzip;
266
+ const { entries } = await unzip(zipArray);
267
+
268
+ const files = {};
269
+ for (const [path, entry] of Object.entries(entries)) {
270
+ if (!entry.isFile) continue;
271
+ const blob = await entry.blob();
272
+ files[path] = { file: { blob } };
273
+ }
274
+
275
+ await webcontainer.mount(files);
276
+ log('Files mounted');
277
+
278
+ // Default package.json
279
+ const defaultPackageJson = {
280
+ name: 'preview-app',
281
+ type: 'module',
282
+ scripts: { dev: 'vite', build: 'vite build', preview: 'vite preview' },
283
+ devDependencies: { vite: '^5.0.0' }
284
+ };
285
+ if (!files['package.json']) {
286
+ await webcontainer.fs.writeFile('package.json', JSON.stringify(defaultPackageJson, null, 2));
287
+ }
288
+
289
+ log('Installing dependencies…');
290
+ const install = await webcontainer.spawn('npm', ['install']);
291
+ await install.exit;
292
+ log('Dependencies installed');
293
+
294
+ log('Starting dev server…');
295
+ await webcontainer.spawn('npm', ['run', 'dev']);
296
+
297
+ webcontainer.on('server-ready', (port, u) => {
298
+ hideLoader();
299
+ setLive(u);
300
+ log(`Server ready at ${u}`);
301
+ });
302
+
303
+ } catch (err) {
304
+ log(err.message, 'error');
305
+ setStatus('error');
306
+ showError(err.message);
307
+ }
308
+ });
309
+
310
+ /* ---------- Stop ---------- */
311
+ stopBtn.addEventListener('click', async () => {
312
+ if (!webcontainer) return;
313
+ try {
314
+ await webcontainer.spawn('pkill', ['-f', 'node']);
315
+ webcontainer = null;
316
+ url = '';
317
+ preview.src = '';
318
+ previewHost.textContent = '';
319
+ previewHost.classList.add('hidden');
320
+ liveIndicator.classList.add('hidden');
321
+ setStatus('offline');
322
+ log('Container stopped');
323
+ } catch (e) {
324
+ log(e.message, 'error');
325
+ }
326
+ });
327
+
328
+ /* ---------- Refresh ---------- */
329
+ refreshBtn.addEventListener('click', () => {
330
+ if (!url) return;
331
+ preview.src = '';
332
+ setTimeout(() => preview.src = url, 100);
333
+ log('Preview refreshed');
334
+ });
335
+
336
+ /* ---------- Open in new tab ---------- */
337
+ openBtn.addEventListener('click', () => {
338
+ if (url) window.open(url, '_blank');
339
+ });
340
+
341
+ /* ---------- Restart container ---------- */
342
+ restartBtn.addEventListener('click', async () => {
343
+ if (!webcontainer) return;
344
+ showLoader();
345
+ try {
346
+ await webcontainer.spawn('pkill', ['-f', 'node']);
347
+ await bootBtn.click();
348
+ } catch (e) {
349
+ log(e.message, 'error');
350
+ showError(e.message);
351
+ }
352
+ });
353
+
354
+ /* ---------- Retry ---------- */
355
+ retryBtn.addEventListener('click', () => bootBtn.click());
356
+
357
+ /* ---------- Publish to Space ---------- */
358
+ publishBtn.addEventListener('click', async () => {
359
+ if (!url) return;
360
+ publishBtn.disabled = true;
361
+ publishBtn.innerHTML = '<i data-feather="loader" class="w-4 h-4 animate-spin mr-2"></i>Publishing…';
362
+ feather.replace();
363
+
364
+ try {
365
+ // Create project snapshot
366
+ const snapshot = {
367
+ name: 'webcontainer-preview',
368
+ files: {}, // Populate from mounted files if needed
369
+ url: url,
370
+ timestamp: new Date().toISOString()
371
+ };
372
+
373
+ // Export snapshot as JSON file
374
+ const blob = new Blob([JSON.stringify(snapshot, null, 2)], { type: 'application/json' });
375
+ const a = document.createElement('a');
376
+ a.href = URL.createObjectURL(blob);
377
+ a.download = 'space-export.json';
378
+ a.click();
379
+
380
+ // Provide instructions for Spaces upload
381
+ alert('Snapshot downloaded! Upload the JSON file to Hugging Face Spaces to publish your preview.');
382
+ log('Published snapshot for Spaces');
383
+ } catch (e) {
384
+ log(e.message, 'error');
385
+ } finally {
386
+ publishBtn.disabled = false;
387
+ publishBtn.innerHTML = '<i data-feather="upload-cloud" class="w-4 h-4 mr-2"></i>Publish to Space';
388
+ feather.replace();
389
+ }
390
+ });
391
+
392
+ /* ---------- Init status ---------- */
393
+ setStatus('offline');
394
+ </script>
395
+ </body>
396
  </html>
prompts.txt ADDED
@@ -0,0 +1,378 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import classNames from "classnames";
2
+ import { useRef, useEffect, useState, forwardRef } from "react";
3
+ import { TbReload, TbLoader, TbExternalLink } from "react-icons/tb";
4
+ import { WebContainer } from '@webcontainer/api';
5
+
6
+ // PreviewEye icon component
7
+ const PreviewEye = ({ className = "w-4 h-4" }: { className?: string }) => (
8
+ <svg
9
+ className={className}
10
+ viewBox="0 0 24 24"
11
+ fill="none"
12
+ stroke="currentColor"
13
+ strokeWidth={2}
14
+ strokeLinecap="round"
15
+ strokeLinejoin="round"
16
+ >
17
+ <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
18
+ <circle cx="12" cy="12" r="3" />
19
+ </svg>
20
+ );
21
+
22
+ // Tooltip component
23
+ const Tooltip = ({
24
+ children,
25
+ content,
26
+ position = "top"
27
+ }: {
28
+ children: React.ReactNode;
29
+ content: string;
30
+ position?: "top" | "bottom" | "left" | "right"
31
+ }) => {
32
+ const [isVisible, setIsVisible] = useState(false);
33
+
34
+ const positionClasses = {
35
+ top: "bottom-full left-1/2 transform -translate-x-1/2 mb-2",
36
+ bottom: "top-full left-1/2 transform -translate-x-1/2 mt-2",
37
+ left: "right-full top-1/2 transform -translate-y-1/2 mr-2",
38
+ right: "left-full top-1/2 transform -translate-y-1/2 ml-2"
39
+ };
40
+
41
+ return (
42
+ <div
43
+ className="relative inline-block"
44
+ onMouseEnter={() => setIsVisible(true)}
45
+ onMouseLeave={() => setIsVisible(false)}
46
+ >
47
+ {children}
48
+ {isVisible && (
49
+ <div className={classNames(
50
+ "absolute z-50 px-2 py-1 text-xs text-white bg-gray-900 rounded shadow-lg whitespace-nowrap pointer-events-none",
51
+ positionClasses[position]
52
+ )}>
53
+ {content}
54
+ <div className={classNames(
55
+ "absolute w-1 h-1 bg-gray-900 transform rotate-45",
56
+ position === "top" && "top-full left-1/2 -translate-x-1/2 -mt-0.5",
57
+ position === "bottom" && "bottom-full left-1/2 -translate-x-1/2 -mb-0.5",
58
+ position === "left" && "left-full top-1/2 -translate-y-1/2 -ml-0.5",
59
+ position === "right" && "right-full top-1/2 -translate-y-1/2 -mr-0.5"
60
+ )} />
61
+ </div>
62
+ )}
63
+ </div>
64
+ );
65
+ };
66
+
67
+ type PreviewProps = {
68
+ files: Record<string, any>;
69
+ isResizing: boolean;
70
+ isAiWorking: boolean;
71
+ packageJson?: any;
72
+ className?: string;
73
+ };
74
+
75
+ const Preview = forwardRef<HTMLDivElement, PreviewProps>(
76
+ ({ files, isResizing, isAiWorking, packageJson, className }, ref) => {
77
+ const iframeRef = useRef<HTMLIFrameElement | null>(null);
78
+ const [webcontainer, setWebcontainer] = useState<WebContainer | null>(null);
79
+ const [url, setUrl] = useState<string>('');
80
+ const [isLoading, setIsLoading] = useState(false);
81
+ const [error, setError] = useState<string>('');
82
+ const [isFullscreen, setIsFullscreen] = useState(false);
83
+
84
+ // Initialize WebContainer
85
+ useEffect(() => {
86
+ const initWebContainer = async () => {
87
+ try {
88
+ const container = await WebContainer.boot();
89
+ setWebcontainer(container);
90
+ } catch (err) {
91
+ setError('Failed to initialize WebContainer');
92
+ console.error('WebContainer init error:', err);
93
+ }
94
+ };
95
+
96
+ initWebContainer();
97
+ }, []);
98
+
99
+ // Mount files and start dev server
100
+ useEffect(() => {
101
+ if (!webcontainer || !files) return;
102
+
103
+ const setupProject = async () => {
104
+ setIsLoading(true);
105
+ setError('');
106
+
107
+ try {
108
+ await webcontainer.mount(files);
109
+
110
+ const defaultPackageJson = {
111
+ name: 'preview-app',
112
+ type: 'module',
113
+ scripts: {
114
+ dev: 'vite',
115
+ build: 'vite build',
116
+ preview: 'vite preview'
117
+ },
118
+ devDependencies: {
119
+ vite: '^5.0.0'
120
+ },
121
+ ...packageJson
122
+ };
123
+
124
+ if (!files['package.json']) {
125
+ await webcontainer.fs.writeFile(
126
+ 'package.json',
127
+ JSON.stringify(defaultPackageJson, null, 2)
128
+ );
129
+ }
130
+
131
+ const installProcess = await webcontainer.spawn('npm', ['install']);
132
+ const installExitCode = await installProcess.exit;
133
+
134
+ if (installExitCode !== 0) {
135
+ throw new Error('Failed to install dependencies');
136
+ }
137
+
138
+ const serverProcess = await webcontainer.spawn('npm', ['run', 'dev']);
139
+
140
+ webcontainer.on('server-ready', (port, url) => {
141
+ setUrl(url);
142
+ setIsLoading(false);
143
+ });
144
+
145
+ serverProcess.output.pipeTo(
146
+ new WritableStream({
147
+ write(data) {
148
+ console.log('[Server]', data);
149
+ },
150
+ })
151
+ );
152
+
153
+ } catch (err) {
154
+ setError(err instanceof Error ? err.message : 'Setup failed');
155
+ setIsLoading(false);
156
+ console.error('Setup error:', err);
157
+ }
158
+ };
159
+
160
+ setupProject();
161
+ }, [webcontainer, files, packageJson]);
162
+
163
+ const handleRefresh = async () => {
164
+ if (!webcontainer || !url) return;
165
+
166
+ setIsLoading(true);
167
+ try {
168
+ await webcontainer.spawn('npm', ['run', 'dev']);
169
+
170
+ if (iframeRef.current) {
171
+ const iframe = iframeRef.current;
172
+ iframe.src = '';
173
+ setTimeout(() => {
174
+ iframe.src = url;
175
+ }, 100);
176
+ }
177
+ } catch (err) {
178
+ console.error('Refresh error:', err);
179
+ } finally {
180
+ setIsLoading(false);
181
+ }
182
+ };
183
+
184
+ const handleRestartContainer = async () => {
185
+ if (!webcontainer) return;
186
+
187
+ setIsLoading(true);
188
+ setUrl('');
189
+
190
+ try {
191
+ await webcontainer.spawn('pkill', ['-f', 'node']);
192
+ await webcontainer.mount(files);
193
+ const serverProcess = await webcontainer.spawn('npm', ['run', 'dev']);
194
+
195
+ webcontainer.on('server-ready', (port, url) => {
196
+ setUrl(url);
197
+ setIsLoading(false);
198
+ });
199
+ } catch (err) {
200
+ setError('Failed to restart container');
201
+ setIsLoading(false);
202
+ console.error('Restart error:', err);
203
+ }
204
+ };
205
+
206
+ const openInNewTab = () => {
207
+ if (url) {
208
+ window.open(url, '_blank');
209
+ }
210
+ };
211
+
212
+ return (
213
+ <div
214
+ ref={ref}
215
+ className={classNames(
216
+ // Vercel-style grid system
217
+ "w-full relative",
218
+ // Base responsive grid
219
+ "col-span-full", // Full width on mobile
220
+ "md:col-span-6", // Half width on medium screens
221
+ "lg:col-span-8", // Larger portion on desktop
222
+ "xl:col-span-9", // Even larger on xl screens
223
+ // Height system
224
+ "h-[calc(70dvh-53px)]",
225
+ "lg:h-[calc(100dvh-54px)]",
226
+ // Borders and styling
227
+ "border border-gray-200",
228
+ "rounded-lg overflow-hidden",
229
+ "bg-white shadow-sm",
230
+ className
231
+ )}
232
+ >
233
+ {/* Header bar - Vercel style */}
234
+ <div className="flex items-center justify-between px-4 py-2 bg-gray-50 border-b border-gray-200">
235
+ <div className="flex items-center gap-2">
236
+ <PreviewEye className="w-4 h-4 text-gray-500" />
237
+ <span className="text-sm font-medium text-gray-700">Preview</span>
238
+ {url && (
239
+ <span className="text-xs text-gray-500 font-mono">
240
+ {new URL(url).host}
241
+ </span>
242
+ )}
243
+ </div>
244
+
245
+ <div className="flex items-center gap-1">
246
+ <Tooltip content="Refresh preview">
247
+ <button
248
+ className="p-1.5 hover:bg-gray-200 rounded-md transition-colors disabled:opacity-50"
249
+ onClick={handleRefresh}
250
+ disabled={!url || isLoading}
251
+ >
252
+ <TbReload
253
+ className={classNames("w-4 h-4 text-gray-600", {
254
+ "animate-spin": isLoading
255
+ })}
256
+ />
257
+ </button>
258
+ </Tooltip>
259
+
260
+ <Tooltip content="Open in new tab">
261
+ <button
262
+ className="p-1.5 hover:bg-gray-200 rounded-md transition-colors disabled:opacity-50"
263
+ onClick={openInNewTab}
264
+ disabled={!url}
265
+ >
266
+ <TbExternalLink className="w-4 h-4 text-gray-600" />
267
+ </button>
268
+ </Tooltip>
269
+
270
+ <Tooltip content="Restart container">
271
+ <button
272
+ className="p-1.5 hover:bg-gray-200 rounded-md transition-colors disabled:opacity-50"
273
+ onClick={handleRestartContainer}
274
+ disabled={!webcontainer || isLoading}
275
+ >
276
+ <TbLoader
277
+ className={classNames("w-4 h-4 text-gray-600", {
278
+ "animate-spin": isLoading
279
+ })}
280
+ />
281
+ </button>
282
+ </Tooltip>
283
+ </div>
284
+ </div>
285
+
286
+ {/* Content area */}
287
+ <div className="flex-1 relative h-[calc(100%-49px)]">
288
+ {error ? (
289
+ <div className="flex items-center justify-center h-full p-8">
290
+ <div className="text-center max-w-md">
291
+ <div className="w-12 h-12 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
292
+ <svg className="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
293
+ <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" />
294
+ </svg>
295
+ </div>
296
+ <h3 className="text-lg font-medium text-gray-900 mb-2">
297
+ Container Error
298
+ </h3>
299
+ <p className="text-sm text-red-600 mb-4">{error}</p>
300
+ <button
301
+ onClick={handleRestartContainer}
302
+ 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"
303
+ >
304
+ <TbReload className="w-4 h-4 mr-2" />
305
+ Restart Container
306
+ </button>
307
+ </div>
308
+ </div>
309
+ ) : !url || isLoading ? (
310
+ <div className="flex items-center justify-center h-full">
311
+ <div className="text-center">
312
+ <div className="w-8 h-8 border-2 border-gray-300 border-t-blue-600 rounded-full animate-spin mx-auto mb-4"></div>
313
+ <div className="text-sm text-gray-600 font-medium">
314
+ {!webcontainer
315
+ ? 'Booting WebContainer...'
316
+ : 'Setting up project...'}
317
+ </div>
318
+ <div className="text-xs text-gray-400 mt-1">
319
+ This may take a moment
320
+ </div>
321
+ </div>
322
+ </div>
323
+ ) : (
324
+ <iframe
325
+ ref={iframeRef}
326
+ title="WebContainer Preview"
327
+ src={url}
328
+ className={classNames("w-full h-full border-0", {
329
+ "pointer-events-none": isResizing || isAiWorking,
330
+ })}
331
+ sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
332
+ />
333
+ )}
334
+ </div>
335
+
336
+ {/* Status indicator */}
337
+ {url && (
338
+ <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">
339
+ <div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
340
+ <span className="text-xs text-green-700 font-medium">Live</span>
341
+ </div>
342
+ )}
343
+ </div>
344
+ );
345
+ }
346
+ );
347
+
348
+ Preview.displayName = "Preview";
349
+
350
+ export default Preview;
351
+ Yes — you’re connecting two ideas:
352
+
353
+ 1. WebContainer API → run and preview code directly in the browser.
354
+
355
+
356
+ 2. Spaces (like Hugging Face Spaces) → publish/share the project so others can use it.
357
+
358
+
359
+
360
+ That flow is totally logical:
361
+
362
+
363
+ ---
364
+
365
+ 🔄 Workflow: From Local Run → Published Space
366
+
367
+ <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" ]} />
368
+
369
+
370
+ ---
371
+
372
+ ✅ Why this works
373
+
374
+ WebContainer gives you a runtime (sandboxed Node.js) to build/preview your app.
375
+
376
+ Spaces give you persistence & hosting so your preview can be shared, reused, or embedded.
377
+
378
+ By publishing as a Space, you create a versioned, public (or private) endpoint.