Ben Beinke commited on
Commit
a2df02b
·
1 Parent(s): 90df85c

Changed to iframe preview

Browse files
Files changed (4) hide show
  1. app.py +177 -67
  2. pyproject.toml +1 -0
  3. sandbox/app.py +2 -1
  4. uv.lock +0 -0
app.py CHANGED
@@ -1,6 +1,12 @@
1
  import importlib.util
2
  import os
3
  import sys
 
 
 
 
 
 
4
 
5
  import gradio as gr
6
 
@@ -12,6 +18,11 @@ gr.NO_RELOAD = False
12
  # Initialize the planning agent globally
13
  planning_agent = None
14
 
 
 
 
 
 
15
 
16
  def get_planning_agent():
17
  """Get or initialize the planning agent (lazy loading)."""
@@ -108,67 +119,140 @@ def save_file(path, new_text):
108
  gr.Error(f"❌ Error saving: {e}")
109
 
110
 
111
- def load_and_render_app():
112
- """Load and render the Gradio app from sandbox/app.py"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  app_path = "sandbox/app.py"
114
 
115
  if not os.path.exists(app_path):
116
- return gr.HTML(
117
- "<div style='padding: 20px; color: red;'>❌ No app.py found in \
118
- sandbox directory</div>"
119
- )
120
 
121
  try:
122
- # Read the app code
123
- with open(app_path, encoding="utf-8") as f:
124
- app_code = f.read()
125
-
126
- # Create a temporary module
127
- spec = importlib.util.spec_from_loader("dynamic_app", loader=None)
128
- module = importlib.util.module_from_spec(spec)
129
-
130
- # Add current directory to sys.path if not already there
131
- if os.getcwd() not in sys.path:
132
- sys.path.insert(0, os.getcwd())
133
-
134
- # Execute the code in the module's namespace
135
- exec(app_code, module.__dict__)
136
-
137
- # Look for common app creation patterns
138
- app_instance = None
139
-
140
- # Try to find the app instance
141
- if hasattr(module, "demo"):
142
- app_instance = module.demo
143
- elif hasattr(module, "app"):
144
- app_instance = module.app
145
- elif hasattr(module, "interface"):
146
- app_instance = module.interface
147
- else:
148
- # Look for any Gradio Blocks or Interface objects
149
- for _, obj in module.__dict__.items():
150
- if isinstance(obj, gr.Blocks | gr.Interface):
151
- app_instance = obj
152
- break
153
-
154
- if app_instance is None:
155
- return gr.HTML(
156
- "<div style='padding: 20px; color: orange;'>⚠️ No Gradio app found. \
157
- Make sure your app.py creates a Gradio Blocks or Interface object.</div>"
158
- )
159
-
160
- # Return the app instance to be rendered
161
- return app_instance
162
 
163
  except Exception as e:
164
- error_html = f"""
165
- <div style='padding: 20px; color: red; font-family: monospace;'>
166
- ❌ Error loading app:<br>
167
- <pre style='background: #f5f5f5; padding: 10px; margin-top: 10px; \
168
- border-radius: 4px;'>{str(e)}</pre>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  </div>
170
  """
171
- return gr.HTML(error_html)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
 
173
 
174
  # Create the main Lovable-style UI
@@ -189,7 +273,7 @@ def create_lovable_ui():
189
  with gr.Column(scale=1, elem_classes="chat-container"):
190
  chatbot = gr.Chatbot(
191
  show_copy_button=True,
192
- avatar_images=(None, "🤖"),
193
  bubble_full_width=False,
194
  height="75vh",
195
  )
@@ -205,13 +289,9 @@ def create_lovable_ui():
205
  # Right side - Preview/Code Toggle
206
  with gr.Column(scale=4, elem_classes="preview-container"):
207
  with gr.Tab("Preview"):
208
- # Create a trigger for refreshing the preview
209
- refresh_trigger = gr.State(value=0)
210
-
211
- # Use gr.render for dynamic app rendering
212
- @gr.render(inputs=refresh_trigger)
213
- def render_preview(trigger_value):
214
- return load_and_render_app()
215
 
216
  with gr.Tab("Code"):
217
  with gr.Row():
@@ -237,15 +317,17 @@ def create_lovable_ui():
237
  # Event handlers
238
  file_explorer.change(fn=load_file, inputs=file_explorer, outputs=code_editor)
239
 
240
- def save_and_refresh(path, new_text, current_trigger):
241
  save_file(path, new_text)
242
- # Increment trigger to refresh the preview
243
- return current_trigger + 1
 
 
244
 
245
  save_btn.click(
246
  fn=save_and_refresh,
247
- inputs=[file_explorer, code_editor, refresh_trigger],
248
- outputs=[refresh_trigger],
249
  )
250
 
251
  # Event handlers for chat
@@ -261,10 +343,38 @@ def create_lovable_ui():
261
  outputs=[chatbot, msg_input],
262
  )
263
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
  return demo
265
 
266
 
267
  if __name__ == "__main__":
 
 
 
268
  demo = create_lovable_ui()
269
- gradio_config = settings.get_gradio_config()
270
- demo.launch(**gradio_config)
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import importlib.util
2
  import os
3
  import sys
4
+ import subprocess
5
+ import time
6
+ import signal
7
+ import threading
8
+ import requests
9
+ import atexit
10
 
11
  import gradio as gr
12
 
 
18
  # Initialize the planning agent globally
19
  planning_agent = None
20
 
21
+ # Global variables for managing the preview app subprocess
22
+ preview_process = None
23
+ PREVIEW_PORT = 7861 # Different port from main app
24
+ PREVIEW_URL = f"http://localhost:{PREVIEW_PORT}"
25
+
26
 
27
  def get_planning_agent():
28
  """Get or initialize the planning agent (lazy loading)."""
 
119
  gr.Error(f"❌ Error saving: {e}")
120
 
121
 
122
+ def stop_preview_app():
123
+ """Stop the preview app subprocess if it's running."""
124
+ global preview_process
125
+ if preview_process and preview_process.poll() is None:
126
+ try:
127
+ # Send SIGTERM to gracefully shutdown
128
+ preview_process.terminate()
129
+ # Wait a bit for graceful shutdown
130
+ preview_process.wait(timeout=5)
131
+ except subprocess.TimeoutExpired:
132
+ # Force kill if graceful shutdown fails
133
+ preview_process.kill()
134
+ except Exception as e:
135
+ print(f"Error stopping preview app: {e}")
136
+ finally:
137
+ preview_process = None
138
+
139
+
140
+ def start_preview_app():
141
+ """Start the preview app in a subprocess."""
142
+ global preview_process
143
+
144
+ # Stop any existing preview app
145
+ stop_preview_app()
146
+
147
  app_path = "sandbox/app.py"
148
 
149
  if not os.path.exists(app_path):
150
+ return False, "No app.py found in sandbox directory"
 
 
 
151
 
152
  try:
153
+ # Start the subprocess to run the sandbox app
154
+ preview_process = subprocess.Popen(
155
+ [
156
+ sys.executable,
157
+ app_path,
158
+ "--server-port",
159
+ str(PREVIEW_PORT),
160
+ "--server-name",
161
+ "127.0.0.1",
162
+ ],
163
+ stdout=subprocess.PIPE,
164
+ stderr=subprocess.PIPE,
165
+ text=True,
166
+ )
167
+
168
+ # Wait a moment for the server to start
169
+ time.sleep(2)
170
+
171
+ # Check if the process is still running
172
+ if preview_process.poll() is not None:
173
+ # Process has terminated, read the error
174
+ stdout, stderr = preview_process.communicate()
175
+ return False, f"App failed to start:\n{stderr}\n{stdout}"
176
+
177
+ # Try to verify the server is responding
178
+ max_retries = 10
179
+ for i in range(max_retries):
180
+ try:
181
+ response = requests.get(PREVIEW_URL, timeout=1)
182
+ if response.status_code == 200:
183
+ return True, "App started successfully"
184
+ except requests.exceptions.RequestException:
185
+ pass
186
+ time.sleep(0.5)
187
+
188
+ return True, "App started successfully"
 
 
 
 
189
 
190
  except Exception as e:
191
+ return False, f"Error starting app: {str(e)}"
192
+
193
+
194
+ def create_iframe_preview():
195
+ """Create an iframe HTML element for the preview."""
196
+ app_path = "sandbox/app.py"
197
+
198
+ if not os.path.exists(app_path):
199
+ return """
200
+ <div style='padding: 20px; text-align: center; color: #666;'>
201
+ <h3>❌ No app.py found</h3>
202
+ <p>Create an app.py file in the sandbox directory to see the preview.</p>
203
+ </div>
204
+ """
205
+
206
+ # Start the preview app
207
+ success, message = start_preview_app()
208
+
209
+ if not success:
210
+ return f"""
211
+ <div style='padding: 20px; text-align: center; color: #d32f2f;'>
212
+ <h3>❌ Failed to start preview</h3>
213
+ <pre style='background: #f5f5f5; padding: 10px; border-radius: 4px; text-align: left;'>{message}</pre>
214
  </div>
215
  """
216
+
217
+ # Add timestamp to force iframe refresh
218
+ timestamp = int(time.time() * 1000)
219
+ preview_url_with_timestamp = f"{PREVIEW_URL}?t={timestamp}"
220
+
221
+ return f"""
222
+ <div style='width: 100%; height: 70vh; border: 1px solid #ddd; border-radius: 8px; overflow: hidden;'>
223
+ <iframe
224
+ src="{preview_url_with_timestamp}"
225
+ width="100%"
226
+ height="100%"
227
+ frameborder="0"
228
+ style="border: none;"
229
+ key="{timestamp}"
230
+ ></iframe>
231
+ </div>
232
+ <div style='padding: 10px; text-align: center; color: #666; font-size: 12px;'>
233
+ Preview running on <a href="{PREVIEW_URL}" target="_blank">{PREVIEW_URL}</a>
234
+ </div>
235
+ """
236
+
237
+
238
+ def is_preview_running():
239
+ """Check if the preview app is running and accessible."""
240
+ global preview_process
241
+ if preview_process is None or preview_process.poll() is not None:
242
+ return False
243
+
244
+ try:
245
+ response = requests.get(PREVIEW_URL, timeout=2)
246
+ return response.status_code == 200
247
+ except requests.exceptions.RequestException:
248
+ return False
249
+
250
+
251
+ def ensure_preview_running():
252
+ """Ensure the preview app is running, start it if needed."""
253
+ if not is_preview_running():
254
+ print("Preview app not running, starting...")
255
+ start_preview_app()
256
 
257
 
258
  # Create the main Lovable-style UI
 
273
  with gr.Column(scale=1, elem_classes="chat-container"):
274
  chatbot = gr.Chatbot(
275
  show_copy_button=True,
276
+ avatar_images=(None, "💗"),
277
  bubble_full_width=False,
278
  height="75vh",
279
  )
 
289
  # Right side - Preview/Code Toggle
290
  with gr.Column(scale=4, elem_classes="preview-container"):
291
  with gr.Tab("Preview"):
292
+ preview_html = gr.HTML(
293
+ value=create_iframe_preview(), elem_id="preview-container"
294
+ )
 
 
 
 
295
 
296
  with gr.Tab("Code"):
297
  with gr.Row():
 
317
  # Event handlers
318
  file_explorer.change(fn=load_file, inputs=file_explorer, outputs=code_editor)
319
 
320
+ def save_and_refresh(path, new_text):
321
  save_file(path, new_text)
322
+ # Wait a moment for file to be saved
323
+ time.sleep(0.5)
324
+ # Return updated iframe preview with forced refresh
325
+ return create_iframe_preview()
326
 
327
  save_btn.click(
328
  fn=save_and_refresh,
329
+ inputs=[file_explorer, code_editor],
330
+ outputs=[preview_html],
331
  )
332
 
333
  # Event handlers for chat
 
343
  outputs=[chatbot, msg_input],
344
  )
345
 
346
+ # Auto-start preview when the app loads
347
+ def on_app_load():
348
+ ensure_preview_running()
349
+ return create_iframe_preview()
350
+
351
+ demo.load(fn=on_app_load, outputs=[preview_html])
352
+
353
+ # Clean up on app close
354
+ def cleanup():
355
+ stop_preview_app()
356
+
357
+ demo.unload(cleanup)
358
+
359
  return demo
360
 
361
 
362
  if __name__ == "__main__":
363
+ # Register cleanup function to run on exit
364
+ atexit.register(stop_preview_app)
365
+
366
  demo = create_lovable_ui()
367
+ # gradio_config = settings.get_gradio_config()
368
+
369
+ # Ensure cleanup on exit
370
+ def signal_handler(signum, frame):
371
+ stop_preview_app()
372
+ sys.exit(0)
373
+
374
+ signal.signal(signal.SIGINT, signal_handler)
375
+ signal.signal(signal.SIGTERM, signal_handler)
376
+
377
+ try:
378
+ demo.launch(server_name="0.0.0.0", server_port=7862)
379
+ finally:
380
+ stop_preview_app()
pyproject.toml CHANGED
@@ -7,6 +7,7 @@ requires-python = ">=3.12"
7
  dependencies = [
8
  "gradio>=5.32.0",
9
  "smolagents[litellm]>=1.17.0",
 
10
  ]
11
 
12
  [dependency-groups]
 
7
  dependencies = [
8
  "gradio>=5.32.0",
9
  "smolagents[litellm]>=1.17.0",
10
+ "requests>=2.31.0",
11
  ]
12
 
13
  [dependency-groups]
sandbox/app.py CHANGED
@@ -66,4 +66,5 @@ with gr.Blocks(title="Simple To-Do List", theme=gr.themes.Monochrome()) as app:
66
  delete_btn.click(delete_task, inputs=[task_index], outputs=[task_display])
67
 
68
  if __name__ == "__main__":
69
- app.launch()
 
 
66
  delete_btn.click(delete_task, inputs=[task_index], outputs=[task_display])
67
 
68
  if __name__ == "__main__":
69
+ # Never change this port, otherwise the preview app will not work
70
+ app.launch(server_name="0.0.0.0", server_port=7861)
uv.lock CHANGED
The diff for this file is too large to render. See raw diff