mgbam commited on
Commit
074e628
·
verified ·
1 Parent(s): e38c633

Create sandbox.py

Browse files
Files changed (1) hide show
  1. sandbox.py +403 -0
sandbox.py ADDED
@@ -0,0 +1,403 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Sandbox rendering and preview functionality for different code formats.
3
+ """
4
+
5
+ import base64
6
+ import time
7
+ import re
8
+ import mimetypes
9
+ import urllib.parse as _uparse
10
+ from typing import Dict, Optional
11
+
12
+ from code_processing import (
13
+ is_streamlit_code, is_gradio_code, parse_multipage_html_output,
14
+ validate_and_autofix_files, inline_multipage_into_single_preview,
15
+ build_transformers_inline_html
16
+ )
17
+
18
+ class SandboxRenderer:
19
+ """Handles rendering of code in sandboxed environments"""
20
+
21
+ @staticmethod
22
+ def send_to_sandbox(code: str) -> str:
23
+ """Render HTML in a sandboxed iframe"""
24
+ html_doc = (code or "").strip()
25
+
26
+ # Convert file:// URLs to data URIs for iframe compatibility
27
+ html_doc = SandboxRenderer._inline_file_urls_as_data_uris(html_doc)
28
+
29
+ # Encode and create iframe
30
+ encoded_html = base64.b64encode(html_doc.encode('utf-8')).decode('utf-8')
31
+ data_uri = f"data:text/html;charset=utf-8;base64,{encoded_html}"
32
+
33
+ iframe = (
34
+ f'<iframe src="{data_uri}" '
35
+ f'width="100%" height="920px" '
36
+ f'sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-modals allow-presentation" '
37
+ f'allow="display-capture">'
38
+ f'</iframe>'
39
+ )
40
+
41
+ return iframe
42
+
43
+ @staticmethod
44
+ def send_to_sandbox_with_refresh(code: str) -> str:
45
+ """Render HTML with cache-busting for media generation updates"""
46
+ html_doc = (code or "").strip()
47
+
48
+ # Convert file:// URLs to data URIs for iframe compatibility
49
+ html_doc = SandboxRenderer._inline_file_urls_as_data_uris(html_doc)
50
+
51
+ # Add cache-busting timestamp
52
+ timestamp = str(int(time.time() * 1000))
53
+ cache_bust_comment = f"<!-- refresh-{timestamp} -->"
54
+ html_doc = cache_bust_comment + html_doc
55
+
56
+ # Encode and create iframe with unique key
57
+ encoded_html = base64.b64encode(html_doc.encode('utf-8')).decode('utf-8')
58
+ data_uri = f"data:text/html;charset=utf-8;base64,{encoded_html}"
59
+
60
+ iframe = (
61
+ f'<iframe src="{data_uri}" '
62
+ f'width="100%" height="920px" '
63
+ f'sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-modals allow-presentation" '
64
+ f'allow="display-capture" '
65
+ f'key="preview-{timestamp}">'
66
+ f'</iframe>'
67
+ )
68
+
69
+ return iframe
70
+
71
+ @staticmethod
72
+ def _inline_file_urls_as_data_uris(html_content: str) -> str:
73
+ """Convert file:// URLs to data URIs for iframe compatibility"""
74
+ try:
75
+ def _file_url_to_data_uri(file_url: str) -> Optional[str]:
76
+ try:
77
+ parsed = _uparse.urlparse(file_url)
78
+ path = _uparse.unquote(parsed.path)
79
+ if not path:
80
+ return None
81
+
82
+ with open(path, 'rb') as _f:
83
+ raw = _f.read()
84
+
85
+ mime = mimetypes.guess_type(path)[0] or 'application/octet-stream'
86
+ b64 = base64.b64encode(raw).decode()
87
+ return f"data:{mime};base64,{b64}"
88
+ except Exception:
89
+ return None
90
+
91
+ def _repl_double(m):
92
+ url = m.group(1)
93
+ data_uri = _file_url_to_data_uri(url)
94
+ return f'src="{data_uri}"' if data_uri else m.group(0)
95
+
96
+ def _repl_single(m):
97
+ url = m.group(1)
98
+ data_uri = _file_url_to_data_uri(url)
99
+ return f"src='{data_uri}'" if data_uri else m.group(0)
100
+
101
+ # Replace file:// URLs in both single and double quotes
102
+ html_content = re.sub(r'src="(file:[^"]+)"', _repl_double, html_content)
103
+ html_content = re.sub(r"src='(file:[^']+)'", _repl_single, html_content)
104
+
105
+ except Exception:
106
+ # Best-effort; continue without inlining
107
+ pass
108
+
109
+ return html_content
110
+
111
+ class StreamlitRenderer:
112
+ """Handles Streamlit app rendering using stlite"""
113
+
114
+ @staticmethod
115
+ def send_streamlit_to_stlite(code: str) -> str:
116
+ """Render Streamlit code using stlite in sandboxed iframe"""
117
+ html_doc = f"""<!doctype html>
118
+ <html>
119
+ <head>
120
+ <meta charset="UTF-8" />
121
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
122
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
123
+ <title>Streamlit Preview</title>
124
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@stlite/[email protected]/build/stlite.css" />
125
+ <style>
126
+ html, body {{ margin: 0; padding: 0; height: 100%; }}
127
+ streamlit-app {{ display: block; height: 100%; }}
128
+ .stlite-loading {{
129
+ display: flex;
130
+ justify-content: center;
131
+ align-items: center;
132
+ height: 100vh;
133
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
134
+ color: #666;
135
+ }}
136
+ </style>
137
+ <script type="module" src="https://cdn.jsdelivr.net/npm/@stlite/[email protected]/build/stlite.js"></script>
138
+ </head>
139
+ <body>
140
+ <div class="stlite-loading">Loading Streamlit app...</div>
141
+ <streamlit-app style="display: none;">
142
+ {code or ""}
143
+ </streamlit-app>
144
+
145
+ <script>
146
+ // Show the app once stlite loads
147
+ document.addEventListener('DOMContentLoaded', function() {{
148
+ setTimeout(() => {{
149
+ const loading = document.querySelector('.stlite-loading');
150
+ const app = document.querySelector('streamlit-app');
151
+ if (loading) loading.style.display = 'none';
152
+ if (app) app.style.display = 'block';
153
+ }}, 2000);
154
+ }});
155
+ </script>
156
+ </body>
157
+ </html>"""
158
+
159
+ encoded_html = base64.b64encode(html_doc.encode('utf-8')).decode('utf-8')
160
+ data_uri = f"data:text/html;charset=utf-8;base64,{encoded_html}"
161
+
162
+ iframe = (
163
+ f'<iframe src="{data_uri}" '
164
+ f'width="100%" height="920px" '
165
+ f'sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-modals allow-presentation" '
166
+ f'allow="display-capture">'
167
+ f'</iframe>'
168
+ )
169
+
170
+ return iframe
171
+
172
+ class GradioRenderer:
173
+ """Handles Gradio app rendering using gradio-lite"""
174
+
175
+ @staticmethod
176
+ def send_gradio_to_lite(code: str) -> str:
177
+ """Render Gradio code using gradio-lite in sandboxed iframe"""
178
+ html_doc = f"""<!doctype html>
179
+ <html>
180
+ <head>
181
+ <meta charset="UTF-8" />
182
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
183
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
184
+ <title>Gradio Preview</title>
185
+ <script type="module" crossorigin src="https://cdn.jsdelivr.net/npm/@gradio/lite/dist/lite.js"></script>
186
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@gradio/lite/dist/lite.css" />
187
+ <style>
188
+ html, body {{ margin: 0; padding: 0; height: 100%; }}
189
+ gradio-lite {{ display: block; height: 100%; }}
190
+ .gradio-loading {{
191
+ display: flex;
192
+ justify-content: center;
193
+ align-items: center;
194
+ height: 100vh;
195
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
196
+ color: #666;
197
+ flex-direction: column;
198
+ gap: 16px;
199
+ }}
200
+ .loading-spinner {{
201
+ border: 3px solid #f3f3f3;
202
+ border-top: 3px solid #ff6b6b;
203
+ border-radius: 50%;
204
+ width: 32px;
205
+ height: 32px;
206
+ animation: spin 1s linear infinite;
207
+ }}
208
+ @keyframes spin {{
209
+ 0% {{ transform: rotate(0deg); }}
210
+ 100% {{ transform: rotate(360deg); }}
211
+ }}
212
+ </style>
213
+ </head>
214
+ <body>
215
+ <div class="gradio-loading">
216
+ <div class="loading-spinner"></div>
217
+ <div>Loading Gradio app...</div>
218
+ </div>
219
+ <gradio-lite style="display: none;">
220
+ {code or ""}
221
+ </gradio-lite>
222
+
223
+ <script>
224
+ // Show the app once gradio-lite loads
225
+ document.addEventListener('DOMContentLoaded', function() {{
226
+ setTimeout(() => {{
227
+ const loading = document.querySelector('.gradio-loading');
228
+ const app = document.querySelector('gradio-lite');
229
+ if (loading) loading.style.display = 'none';
230
+ if (app) app.style.display = 'block';
231
+ }}, 3000);
232
+ }});
233
+ </script>
234
+ </body>
235
+ </html>"""
236
+
237
+ encoded_html = base64.b64encode(html_doc.encode('utf-8')).decode('utf-8')
238
+ data_uri = f"data:text/html;charset=utf-8;base64,{encoded_html}"
239
+
240
+ iframe = (
241
+ f'<iframe src="{data_uri}" '
242
+ f'width="100%" height="920px" '
243
+ f'sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-modals allow-presentation" '
244
+ f'allow="display-capture">'
245
+ f'</iframe>'
246
+ )
247
+
248
+ return iframe
249
+
250
+ class TransformersJSRenderer:
251
+ """Handles Transformers.js app rendering"""
252
+
253
+ @staticmethod
254
+ def send_transformers_to_sandbox(files: Dict[str, str]) -> str:
255
+ """Build and render transformers.js app in sandbox"""
256
+ merged_html = build_transformers_inline_html(files)
257
+ return SandboxRenderer.send_to_sandbox(merged_html)
258
+
259
+ class PreviewEngine:
260
+ """Main preview engine that routes to appropriate renderers"""
261
+
262
+ @staticmethod
263
+ def generate_preview(code: str, language: str, **kwargs) -> str:
264
+ """Generate appropriate preview based on code and language"""
265
+ if not code or not code.strip():
266
+ return PreviewEngine._create_empty_preview()
267
+
268
+ try:
269
+ if language == "html":
270
+ return PreviewEngine._handle_html_preview(code)
271
+
272
+ elif language == "streamlit" or (language == "python" and is_streamlit_code(code)):
273
+ if is_streamlit_code(code):
274
+ return StreamlitRenderer.send_streamlit_to_stlite(code)
275
+ else:
276
+ return PreviewEngine._create_info_preview(
277
+ "Streamlit Preview",
278
+ "Add `import streamlit as st` to enable Streamlit preview."
279
+ )
280
+
281
+ elif language == "gradio" or (language == "python" and is_gradio_code(code)):
282
+ if is_gradio_code(code):
283
+ return GradioRenderer.send_gradio_to_lite(code)
284
+ else:
285
+ return PreviewEngine._create_info_preview(
286
+ "Gradio Preview",
287
+ "Add `import gradio as gr` to enable Gradio preview."
288
+ )
289
+
290
+ elif language == "python":
291
+ if is_streamlit_code(code):
292
+ return StreamlitRenderer.send_streamlit_to_stlite(code)
293
+ elif is_gradio_code(code):
294
+ return GradioRenderer.send_gradio_to_lite(code)
295
+ else:
296
+ return PreviewEngine._create_info_preview(
297
+ "Python Preview",
298
+ "Preview available for Streamlit and Gradio apps. Add the appropriate import statements."
299
+ )
300
+
301
+ elif language == "transformers.js":
302
+ # Handle transformers.js with optional file parts
303
+ html_part = kwargs.get('html_part', '')
304
+ js_part = kwargs.get('js_part', '')
305
+ css_part = kwargs.get('css_part', '')
306
+
307
+ if html_part or js_part or css_part:
308
+ files = {'index.html': html_part or '', 'index.js': js_part or '', 'style.css': css_part or ''}
309
+ else:
310
+ from code_processing import parse_transformers_js_output
311
+ files = parse_transformers_js_output(code)
312
+
313
+ if files.get('index.html'):
314
+ return TransformersJSRenderer.send_transformers_to_sandbox(files)
315
+ else:
316
+ return PreviewEngine._create_info_preview(
317
+ "Transformers.js Preview",
318
+ "Generating transformers.js app... Please wait for all three files to be created."
319
+ )
320
+
321
+ elif language == "svelte":
322
+ return PreviewEngine._create_info_preview(
323
+ "Svelte Preview",
324
+ "Preview is not available for Svelte apps. Download your code and deploy it to see the result."
325
+ )
326
+
327
+ else:
328
+ return PreviewEngine._create_info_preview(
329
+ "Code Preview",
330
+ f"Preview is not available for {language}. Supported: HTML, Streamlit, Gradio, Transformers.js"
331
+ )
332
+
333
+ except Exception as e:
334
+ print(f"[Preview] Error generating preview: {str(e)}")
335
+ return PreviewEngine._create_error_preview(f"Preview error: {str(e)}")
336
+
337
+ @staticmethod
338
+ def _handle_html_preview(code: str) -> str:
339
+ """Handle HTML preview with multi-page support"""
340
+ # Check for multi-page structure
341
+ files = parse_multipage_html_output(code)
342
+ files = validate_and_autofix_files(files)
343
+
344
+ if files and files.get('index.html'):
345
+ # Multi-page HTML
346
+ merged = inline_multipage_into_single_preview(files)
347
+ return SandboxRenderer.send_to_sandbox_with_refresh(merged)
348
+ else:
349
+ # Single HTML document
350
+ from code_processing import extract_html_document
351
+ safe_preview = extract_html_document(code)
352
+ return SandboxRenderer.send_to_sandbox_with_refresh(safe_preview)
353
+
354
+ @staticmethod
355
+ def _create_empty_preview() -> str:
356
+ """Create preview for empty content"""
357
+ return PreviewEngine._create_info_preview("No Content", "Generate some code to see the preview.")
358
+
359
+ @staticmethod
360
+ def _create_info_preview(title: str, message: str) -> str:
361
+ """Create informational preview"""
362
+ return f"""<div style='padding: 2rem; text-align: center; color: #666;
363
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
364
+ background: #f8fafc; border-radius: 8px; margin: 1rem;'>
365
+ <h3 style='color: #374151; margin-top: 0;'>{title}</h3>
366
+ <p>{message}</p>
367
+ </div>"""
368
+
369
+ @staticmethod
370
+ def _create_error_preview(error_message: str) -> str:
371
+ """Create error preview"""
372
+ return f"""<div style='padding: 2rem; text-align: center; color: #dc2626;
373
+ font-family: -apple-system, BlinkMacSystemFont, "Segeo UI", sans-serif;
374
+ background: #fef2f2; border: 1px solid #fecaca; border-radius: 8px; margin: 1rem;'>
375
+ <h3 style='color: #dc2626; margin-top: 0;'>Preview Error</h3>
376
+ <p>{error_message}</p>
377
+ </div>"""
378
+
379
+ # Export main functions and classes
380
+ sandbox_renderer = SandboxRenderer()
381
+ streamlit_renderer = StreamlitRenderer()
382
+ gradio_renderer = GradioRenderer()
383
+ transformers_renderer = TransformersJSRenderer()
384
+ preview_engine = PreviewEngine()
385
+
386
+ # Main exports
387
+ def send_to_sandbox(code: str) -> str:
388
+ return sandbox_renderer.send_to_sandbox(code)
389
+
390
+ def send_to_sandbox_with_refresh(code: str) -> str:
391
+ return sandbox_renderer.send_to_sandbox_with_refresh(code)
392
+
393
+ def send_streamlit_to_stlite(code: str) -> str:
394
+ return streamlit_renderer.send_streamlit_to_stlite(code)
395
+
396
+ def send_gradio_to_lite(code: str) -> str:
397
+ return gradio_renderer.send_gradio_to_lite(code)
398
+
399
+ def send_transformers_to_sandbox(files: Dict[str, str]) -> str:
400
+ return transformers_renderer.send_transformers_to_sandbox(files)
401
+
402
+ def generate_preview(code: str, language: str, **kwargs) -> str:
403
+ return preview_engine.generate_preview(code, language, **kwargs)