broadfield-dev commited on
Commit
0519239
Β·
verified Β·
1 Parent(s): 07d78c6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1120 -372
app.py CHANGED
@@ -1,380 +1,1128 @@
1
- # keylock/app.py
2
  import gradio as gr
3
- from PIL import Image, ImageFont
4
- import tempfile
5
- import os
6
  import json
7
- import logging
8
- import traceback
9
- import base64
10
- import io
11
-
12
- import core # Use relative import for core module
13
- import __version__ # Import version for footer
14
-
15
- app_logger = logging.getLogger("keylock_app")
16
- if not app_logger.hasHandlers(): # Basic logging setup if not configured elsewhere
17
- handler = logging.StreamHandler()
18
- formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
19
- handler.setFormatter(formatter)
20
- app_logger.addHandler(handler)
21
- app_logger.setLevel(logging.INFO)
22
-
23
- # Theming
24
- try:
25
- font_family = [gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"]
26
- except AttributeError:
27
- app_logger.warning("gr.themes.GoogleFont not found. Using fallback fonts. This might be due to Gradio version.")
28
- font_family = ["ui-sans-serif", "system-ui", "sans-serif"]
29
-
30
- try:
31
- blue_color = gr.themes.colors.blue
32
- sky_color = gr.themes.colors.sky
33
- slate_color = gr.themes.colors.slate
34
- cyan_color = gr.themes.colors.cyan
35
- neutral_color = gr.themes.colors.neutral
36
- except AttributeError:
37
- app_logger.warning("gr.themes.colors not found. Using placeholder colors for themes. This might be due to Gradio version.")
38
- class FallbackColors: # Basic fallback colors
39
- blue = "blue"; sky = "skyblue"; slate = "slategray"; cyan = "cyan"; neutral = "gray"
40
- blue_color = FallbackColors.blue
41
- sky_color = FallbackColors.sky
42
- slate_color = FallbackColors.slate
43
- cyan_color = FallbackColors.cyan
44
- neutral_color = FallbackColors.neutral
45
-
46
- ICON_EMBED = "βž•"
47
- ICON_EXTRACT = "βž–"
48
-
49
- def pil_to_base64_html(pil_image, max_width_px=None):
50
- buffered = io.BytesIO(); pil_image.save(buffered, format="PNG")
51
- img_str = base64.b64encode(buffered.getvalue()).decode()
52
- style = f"max-width:{max_width_px}px; height:auto; border:1px solid #ccc; display:block; margin-left:auto; margin-right:auto;" if max_width_px else "border:1px solid #ccc; display:block; margin-left:auto; margin-right:auto;"
53
- return f"<div style='text-align:center;'><img src='data:image/png;base64,{img_str}' alt='Stego Image' style='{style}'/></div>"
54
-
55
- def gradio_embed_data(kv_string: str, password: str,
56
- input_image_pil: Image.Image, generate_carrier_flag: bool,
57
- show_keys_on_image_flag: bool, output_filename_base: str):
58
- output_html_img_str, status_msg, dl_file_path = None, "An error occurred.", None
59
- if not password: return None, "Error: Password cannot be empty.", None
60
- if not kv_string or not kv_string.strip(): return None, "Error: Key-Value data cannot be empty.", None
61
- try:
62
- data_dict = core.parse_kv_string_to_dict(kv_string)
63
- if not data_dict: return None, "Error: Parsed Key-Value data is empty.", None
64
-
65
- original_format_note = ""
66
- if generate_carrier_flag or input_image_pil is None:
67
- carrier_img = core.generate_keylock_carrier_image()
68
- else:
69
- carrier_img = input_image_pil.copy()
70
- if hasattr(input_image_pil, 'format') and input_image_pil.format and input_image_pil.format.upper() != 'PNG':
71
- original_format_note = (
72
- f"Input carrier image was format '{input_image_pil.format}'. "
73
- f"It will be processed and saved as PNG. "
74
- )
75
- app_logger.warning(
76
- f"{original_format_note}If original was lossy (e.g., JPEG), quality is preserved from upload; "
77
- f"if it had transparency (e.g., GIF), it will be lost during RGB conversion."
78
- )
79
-
80
- carrier_img = carrier_img.convert("RGB")
81
-
82
- keys_for_overlay = list(data_dict.keys()) if show_keys_on_image_flag else None
83
- overlay_title = "KeyLock: Data Embedded"
84
-
85
- final_carrier_with_overlay = core.draw_key_list_dropdown_overlay(
86
- carrier_img,
87
- keys=keys_for_overlay,
88
- title=overlay_title
89
- )
90
-
91
- serial_data = json.dumps(data_dict).encode('utf-8')
92
- encrypted_data = core.encrypt_data(serial_data, password)
93
-
94
- stego_final_img = core.embed_data_in_image(final_carrier_with_overlay, encrypted_data)
95
- stego_final_img = core.set_pil_image_format_to_png(stego_final_img)
96
-
97
- fname_base = "".join(c if c.isalnum() or c in ('_','-') else '_' for c in output_filename_base.strip()) or "keylock_img"
98
- temp_fp = None
99
- with tempfile.NamedTemporaryFile(prefix=fname_base+"_", suffix=".png", delete=False) as tmp:
100
- stego_final_img.save(tmp, format="PNG")
101
- temp_fp = tmp.name
102
-
103
- output_html_img_str = pil_to_base64_html(stego_final_img, max_width_px=480)
104
- status_msg = (f"Data embedded into '{os.path.basename(temp_fp)}'.\n"
105
- f"{original_format_note}"
106
- f"Image contains visual \"{overlay_title}\" overlay "
107
- f"{'(with key list)' if show_keys_on_image_flag and keys_for_overlay else ''} "
108
- f"and your LSB-encoded secret data.\n"
109
- f"Secrets: {len(serial_data)}B (raw), {len(encrypted_data)}B (encrypted).")
110
- return output_html_img_str, status_msg, temp_fp
111
- except ValueError as e: return None, f"Error: {str(e)}", None
112
- except Exception as e: app_logger.error(f"Embed Error: {e}", exc_info=True); return None, f"Unexpected Error: {str(e)}", None
113
-
114
- def gradio_extract_data(stego_image_pil: Image.Image, password: str):
115
- if stego_image_pil is None: return "Error: No image provided.", "Error: No image."
116
- if not password: return "Error: Password cannot be empty.", "Error: Password required."
117
- try:
118
- stego_image_rgb = stego_image_pil.convert("RGB")
119
- if hasattr(stego_image_pil, 'format') and stego_image_pil.format and stego_image_pil.format.upper() != "PNG":
120
- app_logger.warning(f"Uploaded image for extraction is format '{stego_image_pil.format}', not PNG. LSB data may be compromised if not the original KeyLock file.")
121
-
122
- extracted_data = core.extract_data_from_image(stego_image_rgb)
123
- decrypted_bytes = core.decrypt_data(extracted_data, password)
124
- try:
125
- data = json.loads(decrypted_bytes.decode('utf-8'))
126
- txt, stat = json.dumps(data, indent=2), "Data extracted successfully (JSON)."
127
- except (json.JSONDecodeError, UnicodeDecodeError):
128
- try:
129
- txt = "Decrypted (UTF-8, not JSON):\n"+decrypted_bytes.decode('utf-8')
130
- stat = "Warning: Decrypted as UTF-8 (not JSON)."
131
- except UnicodeDecodeError:
132
- txt = "Decrypted (raw hex, not JSON/UTF-8):\n"+decrypted_bytes.hex()
133
- stat = "Warning: Decrypted as raw hex."
134
- return txt, stat
135
- except ValueError as e: return f"Error: {str(e)}", f"Extraction Failed: {str(e)}"
136
- except Exception as e: app_logger.error(f"Extract Error: {e}", exc_info=True); return f"Unexpected Error: {str(e)}", f"Error: {str(e)}"
137
-
138
- def build_interface():
139
- custom_theme = gr.themes.Base(
140
- primary_hue="teal",
141
- secondary_hue="purple",
142
- neutral_hue="zinc",
143
- text_size="sm",
144
- spacing_size="md",
145
- radius_size="sm",
146
- font=["System UI", "sans-serif"]
147
- )
148
- custom_css = """
149
- body {
150
- background: linear-gradient(to bottom right, #2c3e50, #34495e);
151
- color: #ecf0f1;
152
- }
153
- .gradio-container {
154
- background: transparent !important;
155
- }
156
- .gr-box, .gr-panel, .gr-pill {
157
- background-color: rgba(44, 62, 80, 0.8) !important;
158
- border-color: rgba(189, 195, 199, 0.2) !important;
159
- }
160
- .gr-textbox, .gr-dropdown, .gr-button, .gr-code, .gr-chat-message, .gr-image {
161
- border-color: rgba(189, 195, 199, 0.3) !important;
162
- background-color: rgba(52, 73, 94, 0.9) !important;
163
- color: #ecf0f1 !important;
164
- }
165
- .gr-button.gr-button-primary {
166
- background-color: #1abc9c !important;
167
- color: white !important;
168
- border-color: #16a085 !important;
169
- }
170
- .gr-button.gr-button-secondary {
171
- background-color: #9b59b6 !important;
172
- color: white !important;
173
- border-color: #8e44ad !important;
174
- }
175
- .gr-button.gr-button-stop {
176
- background-color: #e74c3c !important;
177
- color: white !important;
178
- border-color: #c0392b !important;
179
- }
180
- .gr-markdown {
181
- background-color: rgba(44, 62, 80, 0.7) !important;
182
- padding: 10px;
183
- border-radius: 5px;
184
- }
185
- .gr-markdown h1, .gr-markdown h2, .gr-markdown h3, .gr-markdown h4, .gr-markdown h5, .gr-markdown h6 {
186
- color: #ecf0f1 !important;
187
- border-bottom-color: rgba(189, 195, 199, 0.3) !important;
188
- }
189
- .gr-markdown pre code {
190
- background-color: rgba(52, 73, 94, 0.95) !important;
191
- border-color: rgba(189, 195, 199, 0.3) !important;
192
- }
193
- .gr-image div img { /* Style for image preview */
194
- border: 1px solid #ccc;
195
- background-color: rgba(52, 73, 94, 0.9) !important;
196
- }
197
- .gr-file div button { /* Style for file download button */
198
- background-color: #1abc9c !important;
199
- color: white !important;
200
- border: 1px solid #16a085 !important;
201
- }
202
  """
203
- with gr.Blocks(theme=custom_theme, css=custom_css, title=f"KeyLock Steganography v{__version__}") as keylock_app_interface:
204
- gr.Markdown(f"<div align='center' style='margin-bottom:15px;'><span style='font-size:2.5em;font-weight:bold;'>πŸ”‘ KeyLock v{__version__}</span><h2 style='font-size:1.2em;color:#bdc3c7;margin-top:5px;'>Portable API Key Wallet in a PNG</h2></div>")
205
- gr.HTML("<div align='center' style='margin-bottom:10px;font-size:0.9em;color:#bdc3c7;'>Securely embed and extract API key-value pairs (or any text) within PNG images using LSB steganography and AES-256-GCM encryption.</div>")
206
- gr.HTML("<div align='center' style='margin-bottom:15px;font-size:0.9em;'><span style='font-weight:bold;'>GitHub: <a href='https://github.com/broadfield-dev/KeyLock-API-Wallet' target='_blank' style='color:#1abc9c;'>KeyLock-API-Wallet</a> | Decoder Module: <a href='https://github.com/broadfield-dev/keylock-decode' target='_blank' style='color:#1abc9c;'>keylock-decode</a></span></div>")
207
- gr.HTML("<hr style='border-color: rgba(189, 195, 199, 0.2); margin-bottom:25px;'>")
208
-
209
- with gr.Tabs():
210
- with gr.TabItem(f"{ICON_EMBED} Embed Data"):
211
- with gr.Row():
212
- with gr.Column(scale=2):
213
- embed_kv_input = gr.Textbox(
214
- label="Secret Data (Key:Value Pairs, one per line)",
215
- placeholder="API_KEY_1: your_secret_value_1\nSERVICE_USER = '[email protected]'\n# Lines starting with # are ignored",
216
- lines=7,
217
- info="Enter secrets as Key:Value or Key=Value. Each pair on a new line."
218
- )
219
- embed_password_input = gr.Textbox(
220
- label="Encryption Password",
221
- type="password",
222
- placeholder="Enter a strong password",
223
- info="Required to encrypt data. Keep this safe!"
224
- )
225
- embed_output_filename_base = gr.Textbox(
226
- label="Base Name for Downloaded Stego Image",
227
- value="keylock_wallet",
228
- info="'.png' will be appended. e.g., 'my_project_secrets'"
229
- )
230
- with gr.Accordion("Carrier Image Options", open=False):
231
- embed_generate_carrier_checkbox = gr.Checkbox(
232
- label="Generate new KeyLock Wallet image",
233
- value=True,
234
- info="Uncheck to upload your own PNG carrier image."
235
- )
236
- embed_input_image_upload = gr.Image(
237
- label="Upload Your Own PNG Carrier (Optional)",
238
- type="pil",
239
- image_mode="RGB",
240
- sources=["upload"],
241
- visible=False, # Initially hidden
242
- show_download_button=False,
243
- interactive=True
244
- )
245
- embed_show_keys_checkbox = gr.Checkbox(
246
- label="Show list of key names on image overlay",
247
- value=True,
248
- info="Displays embedded key names (not values) on the image."
249
- )
250
- embed_button = gr.Button("Embed Secrets "+ICON_EMBED, variant="primary")
251
-
252
- with gr.Column(scale=3):
253
- gr.Markdown("### Output Image & Status")
254
- embed_output_status = gr.Textbox(
255
- label="Embedding Status",
256
- lines=4,
257
- interactive=False,
258
- placeholder="Status messages will appear here..."
259
- )
260
- embed_output_image_html = gr.HTML(
261
- label="Preview of Stego Image (Max 480px width)",
262
- value="<div style='text-align:center; color:#bdc3c7; padding:20px;'>Image preview will appear here after embedding.</div>"
263
- )
264
- embed_download_file = gr.File(
265
- label="Download Your KeyLock Image (PNG)",
266
- interactive=False,
267
- file_count="single"
268
- )
269
-
270
- def toggle_carrier_upload(generate_flag):
271
- return gr.update(visible=not generate_flag)
272
-
273
- embed_generate_carrier_checkbox.change(
274
- fn=toggle_carrier_upload,
275
- inputs=[embed_generate_carrier_checkbox],
276
- outputs=[embed_input_image_upload]
277
- )
278
- embed_button.click(
279
- fn=gradio_embed_data,
280
- inputs=[
281
- embed_kv_input,
282
- embed_password_input,
283
- embed_input_image_upload,
284
- embed_generate_carrier_checkbox,
285
- embed_show_keys_checkbox,
286
- embed_output_filename_base
287
- ],
288
- outputs=[
289
- embed_output_image_html,
290
- embed_output_status,
291
- embed_download_file
292
- ]
293
- )
294
-
295
- with gr.TabItem(f"{ICON_EXTRACT} Extract Data"):
296
- with gr.Row():
297
- with gr.Column(scale=1):
298
- extract_stego_image_upload = gr.Image(
299
- label="Upload KeyLock PNG Image",
300
- type="pil",
301
- image_mode="RGB",
302
- sources=["upload"],
303
- show_download_button=False,
304
- interactive=True,
305
- info="Upload the PNG image containing KeyLock data."
306
- )
307
- extract_password_input = gr.Textbox(
308
- label="Decryption Password",
309
- type="password",
310
- placeholder="Enter the password used during embedding",
311
- info="Required to decrypt and extract data."
312
- )
313
- extract_button = gr.Button("Extract Secrets "+ICON_EXTRACT, variant="primary")
314
-
315
- with gr.Column(scale=2):
316
- gr.Markdown("### Extracted Data & Status")
317
- extract_output_status = gr.Textbox(
318
- label="Extraction Status",
319
- lines=2,
320
- interactive=False,
321
- placeholder="Status messages will appear here..."
322
- )
323
- extract_output_data = gr.Textbox(
324
- label="Extracted Secret Data",
325
- lines=10,
326
- interactive=False,
327
- placeholder="Extracted data (usually JSON) will appear here...",
328
- show_copy_button=True
329
- )
330
-
331
- extract_button.click(
332
- fn=gradio_extract_data,
333
- inputs=[
334
- extract_stego_image_upload,
335
- extract_password_input
336
- ],
337
- outputs=[
338
- extract_output_data,
339
- extract_output_status
340
- ]
341
- )
342
-
343
- gr.Markdown("<hr style='border-color: rgba(189, 195, 199, 0.1); margin-top: 30px; margin-bottom:10px;'>")
344
- gr.Markdown(f"<div style='text-align:center; font-size:0.8em; color:#7f8c8d;'>KeyLock-API-Wallet v{__version__}. Use responsibly.</div>")
345
-
346
- return keylock_app_interface
347
-
348
- def main():
349
- app_logger.info(f"Starting KeyLock Gradio Application v{__version__}...")
350
  try:
351
- ImageFont.truetype("DejaVuSans.ttf", 10)
352
- app_logger.info("DejaVuSans font found, PIL font rendering should be good.")
353
- except IOError:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
354
  try:
355
- ImageFont.truetype("arial.ttf", 10)
356
- app_logger.info("Arial font found, PIL font rendering should be good.")
357
- except IOError:
358
- app_logger.warning("Common system fonts (DejaVuSans/Arial) not found. PIL might use basic bitmap font if other preferred fonts in core.py are also unavailable. Overlay text quality might be affected.")
359
-
360
- keylock_app_interface = build_interface()
361
-
362
- launch_args = {"allowed_paths": [tempfile.gettempdir()]}
363
-
364
- server_name = os.environ.get('GRADIO_SERVER_NAME')
365
- server_port = os.environ.get('GRADIO_SERVER_PORT')
366
-
367
- if server_name:
368
- launch_args["server_name"] = server_name
369
- app_logger.info(f"Using server_name from environment: {server_name}")
370
- if server_port:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
371
  try:
372
- launch_args["server_port"] = int(server_port)
373
- app_logger.info(f"Using server_port from environment: {server_port}")
374
- except ValueError:
375
- app_logger.warning(f"Invalid GRADIO_SERVER_PORT: {server_port}. Using default.")
376
-
377
- keylock_app_interface.launch(**launch_args)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
378
 
379
  if __name__ == "__main__":
380
- main()
 
1
+ # app.py
2
  import gradio as gr
3
+ import re
 
 
4
  import json
5
+ # Remove direct requests import, will use model_logic
6
+ # import requests
7
+ import os
8
+ import tempfile
9
+
10
+ # --- build_logic.py is now a hard requirement ---
11
+ from build_logic import (
12
+ create_space as build_logic_create_space,
13
+ _get_api_token as build_logic_get_api_token, # Keep this for HF Hub token logic
14
+ whoami as build_logic_whoami, # Keep this for HF user info
15
+ list_space_files_for_browsing,
16
+ get_space_repository_info,
17
+ get_space_file_content,
18
+ update_space_file,
19
+ parse_markdown as build_logic_parse_markdown,
20
+ delete_space_file as build_logic_delete_space_file,
21
+ get_space_runtime_status
22
+ )
23
+ print("build_logic.py loaded successfully.")
24
+
25
+ # --- model_logic.py is now a hard requirement ---
26
+ from model_logic import (
27
+ get_available_providers,
28
+ get_models_for_provider,
29
+ get_default_model_for_provider,
30
+ get_model_id_from_display_name, # Might not be strictly needed in app.py, but good practice
31
+ generate_stream # This is the core function we'll use
32
+ )
33
+ print("model_logic.py loaded successfully.")
34
+ # --- End imports ---
35
+
36
+
37
+ bbb = chr(96) * 3
38
+ # Declare the global variable at the module level where it's initialized
39
+ parsed_code_blocks_state_cache = []
40
+ BOT_ROLE_NAME = "assistant"
41
+ # Removed GROQ_API_ENDPOINT as it's now in model_logic
42
+
43
+
44
+ DEFAULT_SYSTEM_PROMPT = f"""You are an expert AI programmer. Your role is to generate code and file structures based on user requests, or to modify existing code provided by the user.
45
+ When you provide NEW code for a file, or MODIFIED code for an existing file, use the following format exactly:
46
+ ### File: path/to/filename.ext
47
+ (You can add a short, optional, parenthesized description after the filename on the SAME line)
48
+ {bbb}language
49
+ # Your full code here
50
+ {bbb}
51
+ If the file is binary, or you cannot show its content, use this format:
52
+ ### File: path/to/binaryfile.ext
53
+ [Binary file - approximate_size bytes]
54
+ When you provide a project file structure, use this format:
55
+ ## File Structure
56
+ {bbb}
57
+ πŸ“ Root
58
+ πŸ“„ file1.py
59
+ πŸ“ subfolder
60
+ πŸ“„ file2.js
61
+ {bbb}
62
+ The role name for your responses in the chat history must be '{BOT_ROLE_NAME}'.
63
+ Adhere strictly to these formatting instructions.
64
+ If you update a file, provide the FULL file content again under the same filename.
65
+ Only the latest version of each file mentioned throughout the chat will be used for the final output.
66
+ Filenames in the '### File:' line should be clean paths (e.g., 'src/app.py', 'Dockerfile') and should NOT include Markdown backticks around the filename itself.
67
+ If the user provides existing code (e.g., by pasting a Markdown structure), and asks for modifications, ensure your response includes the complete, modified versions of ONLY the files that changed, using the ### File: format. Unchanged files do not need to be repeated by you. The system will merge your changes with the prior state.
68
+ If the user asks to delete a file, simply omit it from your next full ### File: list.
69
+ If no code is provided, assist the user with their tasks.
70
+ """
71
+
72
+ # --- Core Utility, Parsing, Export functions (mostly unchanged) ---
73
+ def escape_html_for_markdown(text):
74
+ if not isinstance(text, str): return ""
75
+ # Minimal escaping, expand if needed
76
+ return text.replace("&", "&").replace("<", "<").replace(">", ">")
77
+
78
+
79
+ def _infer_lang_from_filename(filename):
80
+ if not filename: return "plaintext"
81
+ if '.' in filename:
82
+ ext = filename.split('.')[-1].lower()
83
+ mapping = {
84
+ 'py': 'python', 'js': 'javascript', 'ts': 'typescript', 'jsx': 'javascript', 'tsx': 'typescript',
85
+ 'html': 'html', 'htm': 'html', 'css': 'css', 'scss': 'scss', 'sass': 'sass', 'less': 'less',
86
+ 'json': 'json', 'xml': 'xml', 'yaml': 'yaml', 'yml': 'yaml', 'toml': 'toml',
87
+ 'md': 'markdown', 'rst': 'rst',
88
+ 'sh': 'bash', 'bash': 'bash', 'zsh': 'bash', 'bat': 'batch', 'cmd': 'batch', 'ps1': 'powershell',
89
+ 'c': 'c', 'h': 'c', 'cpp': 'cpp', 'hpp': 'cpp', 'cs': 'csharp', 'java': 'java',
90
+ 'rb': 'ruby', 'php': 'php', 'go': 'go', 'rs': 'rust', 'swift': 'swift', 'kt': 'kotlin', 'kts': 'kotlin',
91
+ 'sql': 'sql', 'dockerfile': 'docker', 'tf': 'terraform', 'hcl': 'terraform',
92
+ 'txt': 'plaintext', 'log': 'plaintext', 'ini': 'ini', 'conf': 'plaintext', 'cfg': 'plaintext',
93
+ 'csv': 'plaintext', 'tsv': 'plaintext', 'err': 'plaintext',
94
+ '.env': 'plaintext', '.gitignore': 'plaintext', '.npmrc': 'plaintext', '.gitattributes': 'plaintext',
95
+ 'makefile': 'makefile',
96
+ }
97
+ return mapping.get(ext, "plaintext")
98
+ base_filename = os.path.basename(filename)
99
+ if base_filename == 'Dockerfile': return 'docker'
100
+ if base_filename == 'Makefile': return 'makefile'
101
+ if base_filename.startswith('.'): return 'plaintext'
102
+ return "plaintext"
103
+
104
+ def _clean_filename(filename_line_content):
105
+ text = filename_line_content.strip()
106
+ text = re.sub(r'[`\*_]+', '', text) # Remove markdown emphasis characters
107
+ # Try to match a valid-looking path first (allow spaces in folder/file names if quoted or part of a segment)
108
+ path_match = re.match(r'^([\w\-\.\s\/\\]+)', text) # Adjusted to be more general
109
+ if path_match:
110
+ # Further clean if it looks like "path/to/file (description)"
111
+ # Corrected split index was already correct in the original code, just ensure it's applied
112
+ parts = re.split(r'\s*\(', path_match.group(1).strip(), 1)
113
+ return parts[0].strip() if parts else ""
114
+
115
+ # Fallback for more complex lines, like "### File: `src/app.py` (main application)"
116
+ backtick_match = re.search(r'`([^`]+)`', text)
117
+ if backtick_match:
118
+ potential_fn = backtick_match.group(1).strip()
119
+ # Corrected split index was already correct
120
+ parts = re.split(r'\s*\(|\s{2,}', potential_fn, 1)
121
+ cleaned_fn = parts[0].strip() if parts else ""
122
+ cleaned_fn = cleaned_fn.strip('`\'":;,') # Clean common wrapping chars
123
+ if cleaned_fn: return cleaned_fn
124
+
125
+ # Final fallback
126
+ parts = re.split(r'\s*\(|\s{2,}', text, 1)
127
+ filename_candidate = parts[0].strip() if parts else text.strip()
128
+ filename_candidate = filename_candidate.strip('`\'":;,') # Clean common wrapping chars
129
+ return filename_candidate if filename_candidate else text.strip()
130
+
131
+
132
+ def _parse_chat_stream_logic(latest_bot_message_content, existing_files_state=None):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  """
134
+ Parses a single bot message content string to find file blocks and updates the state.
135
+ Assumes existing_files_state is the current state *before* this message.
136
+ """
137
+ # This function takes state as an argument and returns new state, it doesn't need 'global'
138
+ latest_blocks_dict = {}
139
+ if existing_files_state:
140
+ # Copy existing blocks, except for potential structure blocks that might be overwritten
141
+ for block in existing_files_state:
142
+ if not block.get("is_structure_block"):
143
+ latest_blocks_dict[block["filename"]] = block.copy()
144
+ # Keep existing structure block for now, it might be replaced below
145
+
146
+
147
+ results = {"parsed_code_blocks": [], "preview_md": "", "default_selected_filenames": [], "error_message": None}
148
+ content = latest_bot_message_content or ""
149
+
150
+ file_pattern = re.compile(r"### File:\s*(?P<filename_line>[^\n]+)\n(?:```(?P<lang>[\w\.\-\+]*)\n(?P<code>[\s\S]*?)\n```|(?P<binary_msg>\[Binary file(?: - [^\]]+)?\]))")
151
+ structure_pattern = re.compile(r"## File Structure\n```(?:(?P<struct_lang>[\w.-]*)\n)?(?P<structure_code>[\s\S]*?)\n```")
152
+
153
+ # Process the latest bot message for updates to file blocks
154
+ structure_match = structure_pattern.search(content)
155
+ if structure_match:
156
+ # Add/Overwrite the structure block from the latest response
157
+ latest_blocks_dict["File Structure (original)"] = {"filename": "File Structure (original)", "language": structure_match.group("struct_lang") or "plaintext", "code": structure_match.group("structure_code").strip(), "is_binary": False, "is_structure_block": True}
158
+ else:
159
+ # If the latest message *doesn't* have a structure block, keep the previous one if it existed
160
+ existing_structure_block = next((b for b in (existing_files_state or []) if b.get("is_structure_block")), None)
161
+ if existing_structure_block:
162
+ latest_blocks_dict["File Structure (original)"] = existing_structure_block.copy()
163
+
164
+
165
+ # Find all file blocks in the latest message
166
+ current_message_file_blocks = {}
167
+ for match in file_pattern.finditer(content):
168
+ filename = _clean_filename(match.group("filename_line"))
169
+ if not filename: continue
170
+ lang, code_block, binary_msg = match.group("lang"), match.group("code"), match.group("binary_msg")
171
+ item_data = {"filename": filename, "is_binary": False, "is_structure_block": False}
172
+ if code_block is not None:
173
+ item_data["code"], item_data["language"] = code_block.strip(), (lang.strip().lower() if lang else _infer_lang_from_filename(filename))
174
+ elif binary_msg is not None:
175
+ item_data["code"], item_data["language"], item_data["is_binary"] = binary_msg.strip(), "binary", True
176
+ else: continue # Should not happen with the regex
177
+ current_message_file_blocks[filename] = item_data
178
+
179
+ # Update latest_blocks_dict with blocks from the current message
180
+ # Any file mentioned in the latest message replaces its old version
181
+ latest_blocks_dict.update(current_message_file_blocks)
182
+
183
+
184
+ # Convert dictionary values back to a list
185
+ current_parsed_blocks = list(latest_blocks_dict.values())
186
+ # Sort: structure block first, then files alphabetically
187
+ current_parsed_blocks.sort(key=lambda b: (0, b["filename"]) if b.get("is_structure_block") else (1, b["filename"]))
188
+
189
+ # Update the global cache outside this function if needed, or pass it back
190
+ # For now, let's return the new state and let the caller update the cache.
191
+ results["parsed_code_blocks"] = current_parsed_blocks
192
+ results["default_selected_filenames"] = [b["filename"] for b in current_parsed_blocks if not b.get("is_structure_block")]
193
+ return results
194
+
195
+ def _export_selected_logic(selected_filenames, space_line_name_for_md, parsed_blocks_for_export):
196
+ # This function remains largely the same, using the provided parsed_blocks_for_export
197
+ # It takes state as an argument and doesn't need 'global'
198
+ results = {"output_str": "", "error_message": None, "download_filepath": None}
199
+ # Filter out structure blocks for file listing/export content
200
+ exportable_blocks_content = [b for b in parsed_blocks_for_export if not b.get("is_structure_block") and not b.get("is_binary") and not (b.get("code", "").startswith("[Error loading content:") or b.get("code", "").startswith("[Binary or Skipped file]"))]
201
+ binary_blocks_content = [b for b in parsed_blocks_for_export if b.get("is_binary") or b.get("code", "").startswith("[Binary or Skipped file]")]
202
+
203
+ # Collect all filenames (including binary/error ones) for the structure list
204
+ all_filenames_in_state = sorted(list(set(b["filename"] for b in parsed_blocks_for_export if not b.get("is_structure_block"))))
205
+
206
+ if not all_filenames_in_state and not any(b.get("is_structure_block") for b in parsed_blocks_for_export):
207
+ results["output_str"] = f"# Space: {space_line_name_for_md}\n## File Structure\n{bbb}\nπŸ“ Root\n{bbb}\n\n*No files to list in structure or export.*"
208
+ try:
209
+ with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".md", encoding='utf-8') as tmpfile:
210
+ tmpfile.write(results["output_str"]); results["download_filepath"] = tmpfile.name
211
+ except Exception as e: print(f"Error creating temp file for empty export: {e}")
212
+ return results
213
+
214
+ output_lines = [f"# Space: {space_line_name_for_md}"]
215
+
216
+ # Add File Structure block if it exists in parsed blocks
217
+ structure_block = next((b for b in parsed_blocks_for_export if b.get("is_structure_block")), None)
218
+ if structure_block:
219
+ output_lines.extend(["## File Structure", bbb, structure_block["code"].strip(), bbb, ""])
220
+ else:
221
+ # If no structure block from AI, generate a simple one from detected files
222
+ output_lines.extend(["## File Structure", bbb, "πŸ“ Root"])
223
+ if all_filenames_in_state:
224
+ for fname in all_filenames_in_state: output_lines.append(f" πŸ“„ {fname}")
225
+ output_lines.extend([bbb, ""])
226
+
227
+ output_lines.append("Below are the contents of all files in the space:\n")
228
+ exported_content = False
229
+
230
+ # Determine which files to export content for based on selection or default
231
+ # Exportable content blocks only
232
+ files_to_export_content = []
233
+ if selected_filenames:
234
+ files_to_export_content = [b for b in exportable_blocks_content if b["filename"] in selected_filenames]
235
+ else:
236
+ files_to_export_content = exportable_blocks_content # Export all content blocks by default
237
+
238
+ # Add binary/error blocks if they were selected or if exporting all (and they exist)
239
+ # Binary/error blocks are listed in the structure, but their *content* is just the marker string
240
+ binary_error_blocks_to_export = []
241
+ if selected_filenames:
242
+ binary_error_blocks_to_export = [b for b in binary_blocks_content if b["filename"] in selected_filenames]
243
+ elif binary_blocks_content:
244
+ binary_error_blocks_to_export = binary_blocks_content # Include all binary/error if exporting all
245
+
246
+ # Combine and sort all blocks whose content/marker should be included
247
+ all_blocks_to_export_content = sorted(files_to_export_content + binary_error_blocks_to_export, key=lambda b: b["filename"])
248
+
249
+
250
+ for block in all_blocks_to_export_content:
251
+ output_lines.append(f"### File: {block['filename']}")
252
+ if block.get('is_binary') or block.get("code", "").startswith("[Binary file") or block.get("code", "").startswith("[Error loading content:") or block.get("code", "").startswith("[Binary or Skipped file]"):
253
+ # For binary/error placeholders, just add the marker line
254
+ output_lines.append(block.get('code','[Binary or Skipped file]'))
255
+ else:
256
+ # For actual code/text content
257
+ output_lines.extend([f"{bbb}{block.get('language', 'plaintext') or 'plaintext'}", block.get('code',''), bbb])
258
+ output_lines.append(""); exported_content = True
259
+
260
+ if not exported_content and not all_filenames_in_state: output_lines.append("*No files in state.*")
261
+ elif not exported_content: output_lines.append("*No files with editable content are in the state or selected.*") # Message updated
262
+
263
+ final_output_str = "\n".join(output_lines)
264
+ results["output_str"] = final_output_str
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
  try:
266
+ with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".md", encoding='utf-8') as tmpfile:
267
+ tmpfile.write(final_output_str); results["download_filepath"] = tmpfile.name
268
+ except Exception as e: print(f"Error creating temp file: {e}"); results["error_message"] = "Could not prepare file for download."
269
+ return results
270
+
271
+
272
+ def _convert_gr_history_to_api_messages(system_prompt, gr_history, current_user_message=None):
273
+ # This function is fine as is, it produces standard OpenAI format
274
+ messages = [{"role": "system", "content": system_prompt}] if system_prompt else []
275
+ for user_msg, bot_msg in gr_history:
276
+ if user_msg: messages.append({"role": "user", "content": user_msg})
277
+ # Ensure bot_msg is not None or empty before adding
278
+ if bot_msg and isinstance(bot_msg, str): messages.append({"role": BOT_ROLE_NAME, "content": bot_msg})
279
+ if current_user_message: messages.append({"role": "user", "content": current_user_message})
280
+ return messages
281
+
282
+
283
+ def _generate_ui_outputs_from_cache(owner, space_name):
284
+ # Declare global at the top
285
+ global parsed_code_blocks_state_cache
286
+ # This function remains largely the same, generating UI previews and the export MD
287
+ preview_md_val = "*No files in cache to display.*"
288
+ formatted_md_val = f"# Space: {owner}/{space_name}\n## File Structure\n{bbb}\nπŸ“ Root\n{bbb}\n\n*No files in cache.*" if owner or space_name else "*Load or define a Space to see its Markdown structure.*"
289
+ download_file = None
290
+
291
+ if parsed_code_blocks_state_cache:
292
+ preview_md_lines = ["## Detected/Updated Files & Content (Latest Versions):"]
293
+ for block in parsed_code_blocks_state_cache:
294
+ preview_md_lines.append(f"\n----\n**File:** `{escape_html_for_markdown(block['filename'])}`")
295
+ if block.get('is_structure_block'): preview_md_lines.append(f" (Original File Structure from AI)\n")
296
+ elif block.get('is_binary'): preview_md_lines.append(f" (Binary File)\n")
297
+ elif block.get('language') and block.get('language') != 'binary': preview_md_lines.append(f" (Language: `{block['language']}`)\n")
298
+ else: preview_md_lines.append("\n")
299
+
300
+ # Handle content display
301
+ content = block.get('code', '')
302
+ if block.get('is_binary') or content.startswith("["): # Treat errors/skipped as binary for preview display
303
+ preview_md_lines.append(f"\n`{escape_html_for_markdown(content)}`\n")
304
+ elif block.get('is_structure_block'):
305
+ preview_md_lines.append(f"\n{bbb}{block.get('language', 'plaintext') or 'plaintext'}\n{content}\n{bbb}\n")
306
+ else:
307
+ preview_md_lines.append(f"\n{bbb}{block.get('language', 'plaintext') or 'plaintext'}\n{content}\n{bbb}\n")
308
+
309
+
310
+ preview_md_val = "\n".join(preview_md_lines)
311
+ space_line_name = f"{owner}/{space_name}" if owner and space_name else (owner or space_name or "your-space")
312
+
313
+ # _export_selected_logic handles selecting which files to include in the export MD
314
+ # Passing None means export all non-structure/non-binary/non-error content + list all files in structure
315
+ export_result = _export_selected_logic(None, space_line_name, parsed_code_blocks_state_cache)
316
+ formatted_md_val = export_result["output_str"]
317
+ download_file = export_result["download_filepath"]
318
+
319
+ return formatted_md_val, preview_md_val, gr.update(value=download_file, interactive=download_file is not None)
320
+
321
+
322
+ # --- Refactored Chat Submit Handler ---
323
+ def handle_chat_submit(user_message, chat_history, api_key_input, provider_select, model_select, system_prompt, hf_owner_name, hf_repo_name, _current_formatted_markdown):
324
+ # Declare global at the top
325
+ global parsed_code_blocks_state_cache
326
+ _chat_msg_in = ""
327
+ _chat_hist = list(chat_history)
328
+ _status = "Initializing..."
329
+ _detected_files_update, _formatted_output_update, _download_btn_update = gr.update(), gr.update(), gr.update(interactive=False, value=None)
330
+
331
+ # --- Before sending to AI: Parse existing files from the current formatted markdown ---
332
+ # This ensures the AI is aware of the *current* state including user edits
333
+ # or files loaded from HF, before it generates its response.
334
+ # Only do this on a new user message
335
+ if user_message and _current_formatted_markdown:
336
  try:
337
+ parsed_from_md = build_logic_parse_markdown(_current_formatted_markdown)
338
+ # Update cache with files parsed from the markdown.
339
+ # Structure block from AI is volatile, always prefer structure from latest AI message.
340
+ # Files from markdown overwrite any previous file blocks.
341
+ new_cache_state = []
342
+ # Add structure block from *current cache* if it exists, it will be replaced if the AI provides a new one
343
+ existing_structure_block = next((b for b in parsed_code_blocks_state_cache if b.get("is_structure_block")), None)
344
+ if existing_structure_block:
345
+ new_cache_state.append(existing_structure_block.copy()) # Add copy
346
+
347
+
348
+ for f_info in parsed_from_md.get("files", []):
349
+ # Only add if it has a path and isn't the structure block representation placeholder
350
+ if f_info.get("path") and f_info["path"] != "File Structure (original)"):
351
+ # Check if it's a binary representation string
352
+ is_binary_repr = isinstance(f_info.get("content"), str) and (f_info["content"].startswith("[Binary file") or f_info["content"].startswith("[Error loading content:") or f_info["content"].startswith("[Binary or Skipped file]"))
353
+ # Check if a block with this filename already exists in new_cache_state and replace it
354
+ found_existing = False
355
+ for i, block in enumerate(new_cache_state):
356
+ if block["filename"] == f_info["path"] and not block.get("is_structure_block"): # Only replace non-structure blocks
357
+ new_cache_state[i] = {
358
+ "filename": f_info["path"],
359
+ "code": f_info.get("content", ""),
360
+ "language": "binary" if is_binary_repr else _infer_lang_from_filename(f_info["path"]),
361
+ "is_binary": is_binary_repr,
362
+ "is_structure_block": False
363
+ }
364
+ found_existing = True
365
+ break
366
+ if not found_existing:
367
+ new_cache_state.append({
368
+ "filename": f_info["path"],
369
+ "code": f_info.get("content", ""),
370
+ "language": "binary" if is_binary_repr else _infer_lang_from_filename(f_info["path"]),
371
+ "is_binary": is_binary_repr,
372
+ "is_structure_block": False
373
+ })
374
+
375
+ # Sort the updated cache state
376
+ new_cache_state.sort(key=lambda b: (0, b["filename"]) if b.get("is_structure_block") else (1, b["filename"]))
377
+ parsed_code_blocks_state_cache = new_cache_state # Update global cache
378
+
379
+
380
+ except Exception as e:
381
+ # Log error but don't block chat submission
382
+ print(f"Error parsing formatted markdown before chat submit: {e}")
383
+ # Optionally update status: _status = f"Warning: Error parsing current files: {e}. Sending message anyway."
384
+
385
+ # --- End of pre-chat parsing ---
386
+
387
+
388
+ if not user_message.strip():
389
+ _status = "Cannot send an empty message."
390
+ yield (_chat_msg_in, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update); return
391
+ _chat_hist.append((user_message, None)); _status = f"Sending to {model_select} via {provider_select}..."
392
+ yield (_chat_msg_in, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update)
393
+
394
+ # Pass the API key from the UI directly to model_logic
395
+ api_key_override = api_key_input
396
+ # model_id = get_model_id_from_display_name(provider_select, model_select) # model_logic handles display name to ID
397
+
398
+
399
+ current_sys_prompt = system_prompt.strip() or DEFAULT_SYSTEM_PROMPT
400
+
401
+ # Include current file contents in the prompt as context for the AI
402
+ # This context is built from the *current cache state* (which was just updated from the formatted markdown)
403
+ current_files_context = ""
404
+ if parsed_code_blocks_state_cache:
405
+ current_files_context = "\n\n## Current Files in Space\n"
406
+ for block in parsed_code_blocks_state_cache:
407
+ if block.get("is_structure_block"):
408
+ current_files_context += f"### File: {block['filename']}\n{bbb}\n{block['code']}\n{bbb}\n"
409
+ else:
410
+ current_files_context += f"### File: {block['filename']}\n"
411
+ if block.get("is_binary") or block.get("code", "").startswith("["): # Include binary/error markers
412
+ current_files_context += f"{block['code']}\n" # e.g. [Binary file...]
413
+ else:
414
+ current_files_context += f"{bbb}{block.get('language', 'plaintext') or 'plaintext'}\n{block.get('code','')}\n{bbb}\n"
415
+ current_files_context += "\n"
416
+
417
+ # Append current file context to the user message
418
+ # This combined message structure helps the model understand the context and the expected output format
419
+ user_message_with_context = user_message.strip()
420
+ if current_files_context.strip():
421
+ user_message_with_context = user_message_with_context + current_files_context + "\nBased on the current files above and our chat history, please provide updated file contents using the `### File: ...\n```...\n```\n` format for any files you are creating, modifying, or want to include in the final output. If you are providing a file structure, use the `## File Structure\n```\n...\n```\n` format. Omit files you want to delete from your response."
422
+
423
+
424
+ # Convert history to API messages, including the user message with context
425
+ api_msgs = _convert_gr_history_to_api_messages(current_sys_prompt, _chat_hist[:-1], user_message_with_context)
426
+
427
+ # --- Call the new model_logic streaming function ---
428
+ try:
429
+ _status = f"Waiting for {model_select} via {provider_select}..."; yield (_chat_msg_in, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update)
430
+
431
+ # Accumulate the full response content for parsing *after* streaming
432
+ full_bot_response_content = ""
433
+ error_during_stream = None
434
+
435
+ # Generate stream from model_logic
436
+ for chunk in generate_stream(provider_select, model_select, api_key_override, api_msgs):
437
+ if chunk is None: continue # Skip None chunks if any
438
+ if isinstance(chunk, str) and (chunk.startswith("Error: ") or chunk.startswith("API HTTP Error")):
439
+ # If an error chunk is received, treat it as the final output and stop
440
+ full_bot_response_content = chunk
441
+ error_during_stream = chunk
442
+ break # Stop processing stream
443
+ else:
444
+ # Accumulate response and update the last message in chat_hist
445
+ full_bot_response_content += str(chunk) # Ensure chunk is string
446
+ _chat_hist[-1] = (user_message, full_bot_response_content)
447
+ _status = f"Streaming from {model_select}..."
448
+ # Yield update immediately after receiving chunk
449
+ yield (_chat_msg_in, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update)
450
+
451
+ # After the stream finishes or breaks
452
+ if error_during_stream:
453
+ _status = error_during_stream # Set status to the error message
454
+ elif full_bot_response_content and not full_bot_response_content.startswith("Error: "): # Only parse if it's not an error message
455
+ _status = f"Streaming complete. Processing files from {model_select} response..."
456
+
457
+ # Pass the *current state* (updated from markdown at the start)
458
+ # and the *latest bot message content* to the parsing logic.
459
+ # _parse_chat_stream_logic will merge and update based on the bot's response.
460
+ parsing_res = _parse_chat_stream_logic(full_bot_response_content, existing_files_state=parsed_code_blocks_state_cache)
461
+
462
+ if parsing_res["error_message"]:
463
+ _status = f"Parsing Error: {parsing_res['error_message']}"
464
+ # Append parsing error to the bot's response in chat for visibility? Or just status?
465
+ # For now, update status and detected files area with error message
466
+ _detected_files_update = gr.Markdown(f"## Parsing Error\n`{escape_html_for_markdown(parsing_res['error_message'])}`")
467
+ else:
468
+ # Update the global cache with the new state returned by the parser
469
+ global parsed_code_blocks_state_cache
470
+ parsed_code_blocks_state_cache = parsing_res["parsed_code_blocks"]
471
+
472
+ # Regenerate UI outputs from the *updated* cache
473
+ _formatted_output_update, _detected_files_update, _download_btn_update = _generate_ui_outputs_from_cache(hf_owner_name, hf_repo_name)
474
+ _status = "Processing complete. Previews updated."
475
+ else:
476
+ # Handle cases where the stream finished but yielded no content (e.g., filter) or only an error was yielded
477
+ if not error_during_stream:
478
+ _status = "AI response complete, but returned no content."
479
+ # Keep existing previews/markdown if no content was generated to parse or if it was an error message
480
+ _formatted_output_update, _detected_files_update, _download_btn_update = _generate_ui_outputs_from_cache(hf_owner_name, hf_repo_name)
481
+
482
+
483
+ except Exception as e:
484
+ # Catch any errors that occurred *before* or *during* the stream setup/iteration
485
+ error_msg = f"An unexpected error occurred during AI generation: {e}"
486
+ print(f"Unexpected error in chat submit stream: {e}")
487
+ # Update the last chat message with the error
488
+ if _chat_hist and len(_chat_hist) > 0 and _chat_hist[-1][1] is None:
489
+ _chat_hist[-1] = (_chat_hist[-1][0], error_msg) # Keep user message, add error as bot message
490
+ else:
491
+ _chat_hist.append((user_message, error_msg)) # Append as a new user/bot turn if structure unexpected
492
+ _status = error_msg
493
+ # Previews and markdown might not be affected by a generation error, keep existing state
494
+ _formatted_output_update, _detected_files_update, _download_btn_update = _generate_ui_outputs_from_cache(hf_owner_name, hf_repo_name)
495
+
496
+
497
+ # Final yield to update UI after all processing
498
+ yield (_chat_msg_in, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update)
499
+
500
+
501
+ # --- Handler to update model dropdown based on provider selection ---
502
+ def update_models_dropdown(provider_select):
503
+ """Updates the model dropdown choices and selects the default model."""
504
+ if not provider_select:
505
+ return gr.update(choices=[], value=None)
506
+ models = get_models_for_provider(provider_select)
507
+ default_model = get_default_model_for_provider(provider_select)
508
+ # Ensure default is in choices, or pick first, or None
509
+ if default_model and default_model in models:
510
+ selected_value = default_model
511
+ elif models:
512
+ selected_value = models[0]
513
+ else:
514
+ selected_value = None
515
+
516
+ return gr.update(choices=models, value=selected_value)
517
+
518
+
519
+ # --- Existing handlers for Load, Build, Edit, Delete, Status (Mostly unchanged, just global placement) ---
520
+
521
+ def handle_load_existing_space(hf_api_key_ui, ui_owner_name, ui_space_name):
522
+ # Declare global at the top
523
+ global parsed_code_blocks_state_cache
524
+ _formatted_md_val, _detected_preview_val, _status_val = "*Loading files...*", "*Loading files...*", f"Loading Space: {ui_owner_name}/{ui_space_name}..."
525
+ _file_browser_update, _iframe_html_update, _download_btn_update = gr.update(visible=False, choices=[], value=None), gr.update(value=None, visible=False), gr.update(interactive=False, value=None)
526
+ _build_status_clear, _edit_status_clear, _runtime_status_clear = "*Build status will appear here.*", "*Select a file to load or delete.*", "*Space runtime status will appear here after refresh.*"
527
+ _chat_history_clear = [] # Clear chat history on loading a new space
528
+
529
+ # Yield initial state to update UI
530
+ yield (_formatted_md_val, _detected_preview_val, _status_val, _file_browser_update, gr.update(value=ui_owner_name), gr.update(value=ui_space_name), _iframe_html_update, _download_btn_update, _build_status_clear, _edit_status_clear, _runtime_status_clear, _chat_history_clear)
531
+
532
+ owner_to_use, updated_owner_name_val = ui_owner_name, ui_owner_name
533
+ error_occurred = False
534
+
535
+ if not owner_to_use:
536
+ token, token_err = build_logic_get_api_token(hf_api_key_ui)
537
+ if token_err or not token:
538
+ _status_val = f"Error: {token_err or 'Cannot determine owner from token.'}"; error_occurred = True
539
+ else:
540
+ try:
541
+ user_info = build_logic_whoami(token=token)
542
+ if user_info and 'name' in user_info:
543
+ owner_to_use, updated_owner_name_val = user_info['name'], user_info['name']; _status_val += f" (Auto-detected owner: {owner_to_use})"
544
+ else:
545
+ _status_val = "Error: Could not auto-detect owner from token."; error_occurred = True
546
+ except Exception as e:
547
+ _status_val = f"Error auto-detecting owner: {e}"; error_occurred = True
548
+
549
+ if not owner_to_use or not ui_space_name:
550
+ if not error_occurred: _status_val = "Error: Owner and Space Name are required."; error_occurred = True
551
+
552
+ if error_occurred:
553
+ # Yield error state
554
+ yield (f"*Error: {_status_val}*", f"*Error: {_status_val}*", _status_val, _file_browser_update, updated_owner_name_val, ui_space_name, _iframe_html_update, _download_btn_update, _build_status_clear, _edit_status_clear, _runtime_status_clear, _chat_history_clear)
555
+ parsed_code_blocks_state_cache = [] # Clear cache on error
556
+ return # Stop execution
557
+
558
+ sdk_for_iframe, file_list, err_list_files = get_space_repository_info(hf_api_key_ui, ui_space_name, owner_to_use)
559
+
560
+ # Construct iframe URL early, even if file listing fails
561
+ sub_owner = re.sub(r'[^a-z0-9\-]+', '-', owner_to_use.lower()).strip('-') or 'owner' # Fallback owner
562
+ sub_repo = re.sub(r'[^a-z0-9\-]+', '-', ui_space_name.lower()).strip('-') or 'space' # Fallback repo
563
+ iframe_url = f"https://{sub_owner}-{sub_repo}{'.static.hf.space' if sdk_for_iframe == 'static' else '.hf.space'}"
564
+ _iframe_html_update = gr.update(value=f'<iframe src="{iframe_url}?__theme=light&embed=true" width="100%" height="500px" style="border:1px solid #eee; border-radius:8px;"></iframe>', visible=True)
565
+
566
+
567
+ if err_list_files and not file_list:
568
+ _status_val = f"File List Error: {err_list_files}"
569
+ parsed_code_blocks_state_cache = [] # Clear cache on error
570
+ _formatted_md_val, _detected_preview_val, _download_btn_update = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name)
571
+ _file_browser_update = gr.update(visible=True, choices=[], value="Error loading files") # Update file browser with error state
572
+ yield (f"*Error: {err_list_files}*", "*Error loading files*", _status_val, _file_browser_update, updated_owner_name_val, ui_space_name, _iframe_html_update, _download_btn_update, _build_status_clear, _edit_status_clear, _runtime_status_clear, _chat_history_clear)
573
+ return # Stop execution
574
+
575
+ if not file_list:
576
+ _status_val = f"Loaded Space: {owner_to_use}/{ui_space_name}. No files found ({err_list_files or 'Repository is empty'})."
577
+ parsed_code_blocks_state_cache = []
578
+ _formatted_md_val, _detected_preview_val, _download_btn_update = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name)
579
+ _file_browser_update = gr.update(visible=True, choices=[], value="No files found")
580
+ yield (_formatted_md_val, _detected_preview_val, _status_val, _file_browser_update, updated_owner_name_val, ui_space_name, _iframe_html_update, _download_btn_update, _build_status_clear, _edit_status_clear, _runtime_status_clear, _chat_history_clear)
581
+ return # Stop execution
582
+
583
+
584
+ loaded_files_for_cache = [] # Build a list to become the new cache state
585
+ _status_val = f"Loading {len(file_list)} files from {owner_to_use}/{ui_space_name} (SDK: {sdk_for_iframe or 'unknown'})...";
586
+ # Yield intermediate status while loading files
587
+ yield (_formatted_md_val, _detected_preview_val, _status_val, gr.update(visible=True, choices=sorted(file_list or []), value=None), updated_owner_name_val, ui_space_name, _iframe_html_update, _download_btn_update, _build_status_clear, _edit_status_clear, _runtime_status_clear, _chat_history_clear)
588
+
589
+
590
+ for file_path in file_list:
591
+ # Skip files that are likely binary or not user-editable code/text
592
+ # Added more extensions and common non-code files like lock files
593
+ _, ext = os.path.splitext(file_path)
594
+ if ext.lower() in [".png",".jpg",".jpeg",".gif",".ico",".svg",".pt",".bin",".safetensors",".onnx",".woff",".woff2",".ttf",".eot",".zip",".tar",".gz",". هفΨͺ",".pdf",".mp4",".avi",".mov",".mp3",".wav",".ogg"] or \
595
+ file_path.startswith(".git") or "/.git/" in file_path or \
596
+ file_path in ["requirements.txt", "environment.yml", "setup.py", "Pipfile", "pyproject.toml", "package.json", "yarn.lock", "pnpm-lock.yaml", "poetry.lock"] or \
597
+ file_path.endswith(".lock") or \
598
+ file_path.startswith("__pycache__/") or "/__pycache__/" in file_path or \
599
+ file_path.startswith("node_modules/") or "/node_modules/" in file_path or \
600
+ file_path.startswith("venv/") or "/venv/" in file_path or \
601
+ file_path.startswith(".venv/") or "/.venv/" in file_path or \
602
+ file_path == "README.md" or file_path == "LICENSE": # Optionally skip common non-code files like README/LICENSE
603
+ loaded_files_for_cache.append({"filename": file_path, "code": "[Binary or Skipped file]", "language": "binary", "is_binary": True, "is_structure_block": False}); continue
604
+
605
+ # Handle potential issues with reading large files or non-utf8 files
606
  try:
607
+ content, err_get = get_space_file_content(hf_api_key_ui, ui_space_name, owner_to_use, file_path)
608
+ if err_get:
609
+ # If there's an error getting content, record it but don't fail the whole load
610
+ loaded_files_for_cache.append({"filename": file_path, "code": f"[Error loading content: {err_get}]", "language": _infer_lang_from_filename(file_path), "is_binary": False, "is_structure_block": False})
611
+ print(f"Error loading {file_path}: {err_get}");
612
+ continue
613
+ # If content is successfully loaded
614
+ loaded_files_for_cache.append({"filename": file_path, "code": content, "language": _infer_lang_from_filename(file_path), "is_binary": False, "is_structure_block": False})
615
+ except Exception as content_ex:
616
+ # Catch any other unexpected exceptions during file content fetching
617
+ loaded_files_for_cache.append({"filename": file_path, "code": f"[Unexpected error loading content: {content_ex}]", "language": _infer_lang_from_filename(file_path), "is_binary": False, "is_structure_block": False})
618
+ print(f"Unexpected error loading {file_path}: {content_ex}")
619
+ continue
620
+
621
+ # Add a placeholder structure block if none was loaded (AI will generate one later if needed)
622
+ # This ensures the cache isn't empty except for files
623
+ # structure_block = next((b for b in loaded_files_for_cache if b.get("is_structure_block")), None)
624
+ # if not structure_block:
625
+ # loaded_files_for_cache.insert(0, {"filename": "File Structure (original)", "code": "πŸ“ Root\n ...\n", "language": "plaintext", "is_binary": False, "is_structure_block": True})
626
+
627
+ parsed_code_blocks_state_cache = loaded_files_for_cache
628
+ _formatted_md_val, _detected_preview_val, _download_btn_update = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name)
629
+ _status_val = f"Successfully loaded Space: {owner_to_use}/{ui_space_name}. Markdown ready. {len(file_list)} files listed."
630
+ _file_browser_update = gr.update(visible=True, choices=sorted(file_list or []), value=None) # Use the full file list for the dropdown
631
+
632
+ # Final yield with updated state
633
+ yield (_formatted_md_val, _detected_preview_val, _status_val, _file_browser_update, updated_owner_name_val, ui_space_name, _iframe_html_update, _download_btn_update, _build_status_clear, _edit_status_clear, _runtime_status_clear, _chat_history_clear)
634
+
635
+
636
+ def handle_build_space_button(hf_api_key_ui, ui_space_name_part, ui_owner_name_part, space_sdk_ui, formatted_markdown_content):
637
+ # Declare global at the top
638
+ global parsed_code_blocks_state_cache
639
+ # ... (this function calls build_logic_create_space and refreshes file list)
640
+ _build_status, _iframe_html, _file_browser_update = "Starting space build process...", gr.update(value=None, visible=False), gr.update(visible=False, choices=[], value=None)
641
+ # Include outputs for owner/space name textboxes in the initial yield
642
+ yield _build_status, _iframe_html, _file_browser_update, gr.update(value=ui_owner_name_part), gr.update(value=ui_space_name_part) # Yield initial status
643
+ if not ui_space_name_part or "/" in ui_space_name_part: _build_status = f"Build Error: HF Space Name '{ui_space_name_part}' must be repo name only (no '/')."; yield _build_status, _iframe_html, _file_browser_update, gr.update(), gr.update(); return
644
+ final_owner_for_build = ui_owner_name_part
645
+ if not final_owner_for_build:
646
+ token_for_whoami, token_err = build_logic_get_api_token(hf_api_key_ui)
647
+ if token_err: _build_status = f"Build Error: {token_err}"; yield _build_status, _iframe_html, _file_browser_update, gr.update(), gr.update(); return
648
+ if token_for_whoami:
649
+ try:
650
+ user_info = build_logic_whoami(token=token_for_whoami)
651
+ final_owner_for_build = user_info['name'] if user_info and 'name' in user_info else final_owner_for_build
652
+ if not final_owner_for_build: _build_status += "\n(Warning: Could not auto-detect owner from token for build. Please specify.)"
653
+ except Exception as e: _build_status += f"\n(Warning: Could not auto-detect owner for build: {e}. Please specify.)"
654
+ else: _build_status += "\n(Warning: Owner not specified and no token to auto-detect for build. Please specify owner or provide a token.)"
655
+
656
+ if not final_owner_for_build: _build_status = "Build Error: HF Owner Name could not be determined. Please specify it."; yield _build_status, _iframe_html, _file_browser_update, gr.update(), gr.update(); return
657
+
658
+ # Before building, parse the markdown to ensure the cache reflects exactly what's being built
659
+ # This prevents inconsistencies if the user manually edited the markdown output
660
+ try:
661
+ parsed_from_md_for_build = build_logic_parse_markdown(formatted_markdown_content)
662
+ # Replace the global cache state with the state derived from the markdown being built
663
+ parsed_code_blocks_state_cache = []
664
+ if parsed_from_md_for_build.get("owner_md"): # Update UI owner/space name if present in MD
665
+ ui_owner_name_part = parsed_from_md_for_build["owner_md"] # Use this updated value later
666
+ if parsed_from_md_for_build.get("repo_name_md"):
667
+ ui_space_name_part = parsed_from_md_for_build["repo_name_md"] # Use this updated value later
668
+
669
+ # Rebuild cache from parsed markdown files + structure block
670
+ structure_block_md = next((f for f in parsed_from_md_for_build.get("files", []) if f.get("path") == "File Structure (original)"), None)
671
+ if structure_block_md:
672
+ parsed_code_blocks_state_cache.append({
673
+ "filename": structure_block_md["path"],
674
+ "code": structure_block_md["content"],
675
+ "language": "plaintext", # Markdown parser doesn't detect lang for structure block ```
676
+ "is_binary": False,
677
+ "is_structure_block": True
678
+ })
679
+
680
+ for f_info in parsed_from_md_for_build.get("files", []):
681
+ if f_info.get("path") and f_info["path"] != "File Structure (original)"):
682
+ is_binary_repr = isinstance(f_info.get("content"), str) and (f_info["content"].startswith("[Binary file") or f_info["content"].startswith("[Error loading content:") or f_info["content"].startswith("[Binary or Skipped file]"))
683
+ parsed_code_blocks_state_cache.append({
684
+ "filename": f_info["path"],
685
+ "code": f_info.get("content", ""),
686
+ "language": "binary" if is_binary_repr else _infer_lang_from_filename(f_info["path"]),
687
+ "is_binary": is_binary_repr,
688
+ "is_structure_block": False
689
+ })
690
+ parsed_code_blocks_state_cache.sort(key=lambda b: (0, b["filename"]) if b.get("is_structure_block") else (1, b["filename"]))
691
+
692
+ except Exception as e:
693
+ _build_status = f"Build Error: Failed to parse Markdown structure before building: {e}";
694
+ # Yield error status, including keeping current owner/space name in textboxes
695
+ yield _build_status, _iframe_html, _file_browser_update, gr.update(value=ui_owner_name_part), gr.update(value=ui_space_name_part); return # Stop build on parse error
696
+
697
+
698
+ result_message = build_logic_create_space(hf_api_key_ui, ui_space_name_part, final_owner_for_build, space_sdk_ui, formatted_markdown_content)
699
+ _build_status = f"Build Process: {result_message}"
700
+
701
+ # Update UI with owner/space names extracted from markdown if present
702
+ owner_name_output = gr.update(value=ui_owner_name_part)
703
+ space_name_output = gr.update(value=ui_space_name_part)
704
+
705
+ if "Successfully" in result_message:
706
+ # Use potentially updated owner/space name from markdown parsing
707
+ sub_owner = re.sub(r'[^a-z0-9\-]+', '-', ui_owner_name_part.lower()).strip('-') or 'owner'
708
+ sub_repo = re.sub(r'[^a-z0-9\-]+', '-', ui_space_name_part.lower()).strip('-') or 'space'
709
+ iframe_url = f"https://{sub_owner}-{sub_repo}{'.static.hf.space' if space_sdk_ui == 'static' else '.hf.space'}"
710
+ _iframe_html = gr.update(value=f'<iframe src="{iframe_url}?__theme=light&embed=true" width="100%" height="700px" style="border:1px solid #eee; border-radius:8px;"></iframe>', visible=True)
711
+ _build_status += f"\nSpace live at: [Link]({iframe_url}) (Repo: https://huggingface.co/spaces/{ui_owner_name_part}/{ui_space_name_part})"
712
+
713
+ # Refresh file list after successful build
714
+ file_list, err_list = list_space_files_for_browsing(hf_api_key_ui, ui_space_name_part, ui_owner_name_part)
715
+ if err_list: _build_status += f"\nFile list refresh error after build: {err_list}"; _file_browser_update = gr.update(visible=True, choices=sorted(file_list or []), value="Error refreshing files")
716
+ else: _file_browser_update = gr.update(visible=True, choices=sorted(file_list or []), value=None if file_list else "No files found")
717
+
718
+ # Final yield including potential updates to owner/space name textboxes
719
+ yield _build_status, _iframe_html, _file_browser_update, owner_name_output, space_name_output
720
+
721
+
722
+ # File editing handlers are okay, just need to ensure they update the cache properly after commit/delete
723
+ def handle_load_file_for_editing(hf_api_key_ui, ui_space_name_part, ui_owner_name_part, selected_file_path):
724
+ # Declare global at the top (even though it's not modified here, it's good practice if the function *might* interact with it in the future, or just for consistency)
725
+ # In this specific function, `global` isn't strictly needed as it only *reads* indirectly via _generate_ui_outputs_from_cache which handles its own global
726
+ # Keeping it here for consistency as per the error symptoms observed in similar functions.
727
+ global parsed_code_blocks_state_cache # Added global here
728
+
729
+ _file_content_val, _edit_status_val, _commit_msg_val, _lang_update = "", "Error: No file selected.", gr.update(value=""), gr.update(language="python") # Reset values
730
+ if not selected_file_path or selected_file_path in ["No files found", "Error loading files", "Error refreshing files"]:
731
+ yield _file_content_val, "Select a file from the dropdown.", _commit_msg_val, _lang_update # Clear editor and status
732
+ return
733
+
734
+ owner_to_use = ui_owner_name_part
735
+ if not owner_to_use:
736
+ token, token_err = build_logic_get_api_token(hf_api_key_ui)
737
+ if token_err: _edit_status_val = f"Error: {token_err}"; yield (_file_content_val, _edit_status_val, _commit_msg_val, _lang_update); return
738
+ if token:
739
+ try:
740
+ user_info = build_logic_whoami(token=token); owner_to_use = user_info['name'] if user_info and 'name' in user_info else owner_to_use
741
+ if not owner_to_use: _edit_status_val = "Error: Could not auto-detect owner from token."; yield (_file_content_val, _edit_status_val, _commit_msg_val, _lang_update); return
742
+ except Exception as e: _edit_status_val = f"Error auto-detecting owner for editing file: {e}"; yield (_file_content_val, _edit_status_val, _commit_msg_val, _lang_update); return
743
+ else: _edit_status_val = "Error: HF Owner Name not set and no token to auto-detect."; yield (_file_content_val, _edit_status_val, _commit_msg_val, _lang_update); return
744
+
745
+ if not owner_to_use or not ui_space_name_part: _edit_status_val = "Error: HF Owner and/or Space Name is missing."; yield (_file_content_val, _edit_status_val, _commit_msg_val, _lang_update); return
746
+
747
+ _edit_status_val = f"Loading {selected_file_path}..."
748
+ yield gr.update(value=""), _edit_status_val, gr.update(value=""), gr.update(language="python") # Yield loading state
749
+
750
+ content, err = get_space_file_content(hf_api_key_ui, ui_space_name_part, owner_to_use, selected_file_path)
751
+
752
+ if err:
753
+ _edit_status_val = f"Error loading '{selected_file_path}': {err}"
754
+ _commit_msg_val = f"Error loading {selected_file_path}"
755
+ _file_content_val = f"Error loading {selected_file_path}:\n{err}"
756
+ _lang_update = gr.update(language="python") # Default language for error display
757
+ yield _file_content_val, _edit_status_val, _commit_msg_val, _lang_update
758
+ return
759
+
760
+ _file_content_val = content or ""
761
+ _edit_status_val = f"Loaded {selected_file_path} for editing."
762
+ _commit_msg_val = f"Update {selected_file_path} via AI Space Editor"
763
+ _lang_update = gr.update(language=_infer_lang_from_filename(selected_file_path))
764
+
765
+ yield _file_content_val, _edit_status_val, _commit_msg_val, _lang_update
766
+
767
+ def handle_commit_file_changes(hf_api_key_ui, ui_space_name_part, ui_owner_name_part, file_to_edit_path, edited_content, commit_message):
768
+ # Declare global at the top
769
+ global parsed_code_blocks_state_cache
770
+ _edit_status_val = "Processing commit..."
771
+ # Initialize updates for components that might change
772
+ _file_browser_update_val = gr.update() # Will update choices or value
773
+ _formatted_md_out = gr.update() # Will update markdown
774
+ _detected_preview_out = gr.update() # Will update markdown preview
775
+ _download_btn_out = gr.update() # Will update download button
776
+
777
+ yield _edit_status_val, _file_browser_update_val, _formatted_md_out, _detected_preview_out, _download_btn_out # Yield initial status
778
+
779
+
780
+ if not file_to_edit_path or file_to_edit_path in ["No files found", "Error loading files", "Error refreshing files"]:
781
+ _edit_status_val = "Error: No valid file selected for commit.";
782
+ yield _edit_status_val, gr.update(), gr.update(), gr.update(), gr.update(); return
783
+
784
+ owner_to_use = ui_owner_name_part
785
+ if not owner_to_use:
786
+ token, token_err = build_logic_get_api_token(hf_api_key_ui)
787
+ if token_err: _edit_status_val = f"Error: {token_err}"; yield (_edit_status_val, gr.update(), gr.update(), gr.update(), gr.update()); return
788
+ if token:
789
+ try:
790
+ user_info = build_logic_whoami(token=token); owner_to_use = user_info['name'] if user_info and 'name' in user_info else owner_to_use
791
+ if not owner_to_use: _edit_status_val = "Error: Could not auto-detect owner from token."; yield (_edit_status_val, gr.update(), gr.update(), gr.update(), gr.update()); return
792
+ except Exception as e: _edit_status_val = f"Error auto-detecting owner for committing file: {e}"; yield (_edit_status_val, gr.update(), gr.update(), gr.update(), gr.update()); return
793
+ else: _edit_status_val = "Error: HF Owner Name not set and no token to auto-detect."; yield (_edit_status_val, gr.update(), gr.update(), gr.update(), gr.update()); return
794
+
795
+ if not owner_to_use or not ui_space_name_part: _edit_status_val = "Error: HF Owner and/or Space Name is missing."; yield (_edit_status_val, gr.update(), gr.update(), gr.update(), gr.update()); return
796
+
797
+ status_msg = update_space_file(hf_api_key_ui, ui_space_name_part, owner_to_use, file_to_edit_path, edited_content, commit_message)
798
+ _edit_status_val = status_msg
799
+
800
+ if "Successfully updated" in status_msg:
801
+ # Update the cache with the new content
802
+ found_in_cache = False
803
+ for block in parsed_code_blocks_state_cache:
804
+ if block["filename"] == file_to_edit_path:
805
+ block["code"] = edited_content
806
+ block["language"] = _infer_lang_from_filename(file_to_edit_path)
807
+ block["is_binary"] = False # Assume user edited text content
808
+ block["is_structure_block"] = False # Ensure it's not marked as structure
809
+ found_in_cache = True
810
+ break
811
+ if not found_in_cache:
812
+ # If file was added/edited via editor and wasn't in initial load cache (e.g. binary/error placeholder), add/replace it
813
+ # First remove any existing placeholder for this file
814
+ parsed_code_blocks_state_cache = [b for b in parsed_code_blocks_state_cache if b["filename"] != file_to_edit_path]
815
+ # Then add the new text content block
816
+ parsed_code_blocks_state_cache.append({
817
+ "filename": file_to_edit_path,
818
+ "code": edited_content,
819
+ "language": _infer_lang_from_filename(file_to_edit_path),
820
+ "is_binary": False,
821
+ "is_structure_block": False
822
+ })
823
+ # Re-sort the cache to maintain consistent order
824
+ parsed_code_blocks_state_cache.sort(key=lambda b: (0, b["filename"]) if b.get("is_structure_block") else (1, b["filename"]))
825
+
826
+ # Regenerate markdown and preview from the updated cache
827
+ _formatted_md_out, _detected_preview_out, _download_btn_out = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name_part)
828
+
829
+ # Refresh file list choices and keep the current file selected
830
+ new_file_list, err_list = list_space_files_for_browsing(hf_api_key_ui, ui_space_name_part, owner_to_use)
831
+ if err_list:
832
+ _edit_status_val += f"\nFile list refresh error: {err_list}"
833
+ _file_browser_update_val = gr.update(choices=sorted(new_file_list or []), value="Error refreshing files")
834
+ else:
835
+ _file_browser_update_val = gr.update(choices=sorted(new_file_list or []), value=file_to_edit_path) # Keep current file selected
836
+
837
+ yield _edit_status_val, _file_browser_update_val, _formatted_md_out, _detected_preview_out, _download_btn_out
838
+
839
+
840
+ def handle_delete_file(hf_api_key_ui, ui_space_name_part, ui_owner_name_part, file_to_delete_path):
841
+ # Declare global at the top
842
+ global parsed_code_blocks_state_cache
843
+ _edit_status_val = "Processing deletion..."
844
+ # Initialize updates for components that might change/clear
845
+ _file_browser_choices_update = gr.update() # Update choices
846
+ _file_browser_value_update = None # Clear selected file value
847
+ _file_content_editor_update = gr.update(value="") # Clear editor content
848
+ _commit_msg_update = gr.update(value="") # Clear commit message
849
+ _lang_update = gr.update(language="plaintext") # Reset editor language
850
+ _formatted_md_out = gr.update() # Update markdown
851
+ _detected_preview_out = gr.update() # Update markdown preview
852
+ _download_btn_out = gr.update() # Update download button
853
+
854
+
855
+ yield (_edit_status_val, _file_browser_choices_update, _file_browser_value_update, _file_content_editor_update, _commit_msg_update, _lang_update, _formatted_md_out, _detected_preview_out, _download_btn_out) # Yield initial status
856
+
857
+
858
+ if not file_to_delete_path or file_to_delete_path in ["No files found", "Error loading files", "Error refreshing files"]:
859
+ _edit_status_val = "Error: No valid file selected for deletion.";
860
+ yield (_edit_status_val, gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()); return
861
+
862
+ owner_to_use = ui_owner_name_part
863
+ if not owner_to_use:
864
+ token, token_err = build_logic_get_api_token(hf_api_key_ui)
865
+ if token_err: _edit_status_val = f"API Token Error: {token_err}"; yield (_edit_status_val, gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()); return
866
+ if token:
867
+ try:
868
+ user_info = build_logic_whoami(token=token); owner_to_use = user_info['name'] if user_info and 'name' in user_info else owner_to_use
869
+ if not owner_to_use: _edit_status_val = "Error: Could not auto-detect owner from token."; yield (_edit_status_val, gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()); return
870
+ except Exception as e: _edit_status_val = f"Error auto-detecting owner for deleting file: {e}"; yield (_edit_status_val, gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()); return
871
+ else: _edit_status_val = "Error: HF Token needed to auto-detect owner."; yield (_edit_status_val, gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()); return
872
+
873
+
874
+ if not owner_to_use or not ui_space_name_part: _edit_status_val = "Error: Owner and Space Name are required."; yield (_edit_status_val, gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()); return
875
+
876
+ deletion_status_msg = build_logic_delete_space_file(hf_api_key_ui, ui_space_name_part, owner_to_use, file_to_delete_path)
877
+ _edit_status_val = deletion_status_msg
878
+
879
+ # Always refresh the file list dropdown choices after a delete attempt, successful or not
880
+ new_file_list, err_list = list_space_files_for_browsing(hf_api_key_ui, ui_space_name_part, owner_to_use)
881
+
882
+ if "Successfully deleted" in deletion_status_msg:
883
+ # Remove the file from the cache
884
+ parsed_code_blocks_state_cache = [b for b in parsed_code_blocks_state_cache if b["filename"] != file_to_delete_path]
885
+
886
+ # Regenerate markdown and preview from the updated cache
887
+ _formatted_md_out, _detected_preview_out, _download_btn_out = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name_part)
888
+
889
+
890
+ if err_list:
891
+ _edit_status_val += f"\nFile list refresh error: {err_list}"
892
+ _file_browser_choices_update = gr.update(choices=sorted(new_file_list or []), value="Error refreshing files") # Set value to error state
893
+ else:
894
+ _file_browser_choices_update = gr.update(choices=sorted(new_file_list or []), value=None) # Clear selection visually and internally
895
+
896
+ _file_browser_value_update = None # Explicitly set value to None to clear selection visual
897
+
898
+
899
+ else: # If deletion failed
900
+ if err_list:
901
+ _edit_status_val += f"\nFile list refresh error: {err_list}"
902
+ _file_browser_choices_update = gr.update(choices=sorted(new_file_list or []), value="Error refreshing files")
903
+ _file_browser_value_update = "Error refreshing files" # Keep error state in value if list failed
904
+ else:
905
+ # If list refresh succeeded but delete failed, refresh choices and keep the *failed-to-delete* file selected
906
+ _file_browser_choices_update = gr.update(choices=sorted(new_file_list or []), value=file_to_delete_path)
907
+ _file_browser_value_update = file_to_delete_path # Keep the file selected visually
908
+
909
+ # Markdown and preview are not changed if deletion failed, keep current updates as gr.update()
910
+ # Regenerate previews to show they are unchanged
911
+ _formatted_md_out, _detected_preview_out, _download_btn_out = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name_part)
912
+
913
+
914
+ yield (_edit_status_val, _file_browser_choices_update, _file_browser_value_update, _file_content_editor_update, _commit_msg_update, _lang_update, _formatted_md_out, _detected_preview_out, _download_btn_out)
915
+
916
+
917
+ # Space status handler is okay
918
+ def handle_refresh_space_status(hf_api_key_ui, ui_owner_name, ui_space_name):
919
+ # This function doesn't modify the global cache, so no global declaration needed.
920
+ # ... (rest of this function is the same)
921
+ yield "*Fetching space status...*" # Initial feedback
922
+ owner_to_use = ui_owner_name
923
+ if not owner_to_use:
924
+ token, token_err = build_logic_get_api_token(hf_api_key_ui)
925
+ if token_err or not token: yield f"**Error:** {token_err or 'Cannot determine owner.'}"; return
926
+ try: user_info = build_logic_whoami(token=token); owner_to_use = user_info['name'] if user_info and 'name' in user_info else owner_to_use
927
+ except Exception as e: yield f"**Error auto-detecting owner:** {e}"; return
928
+ if not owner_to_use or not ui_space_name: yield "**Error:** Owner and Space Name are required."; return
929
+ status_details, error_msg = get_space_runtime_status(hf_api_key_ui, ui_space_name, owner_to_use)
930
+ if error_msg: _status_display_md = f"**Error fetching status for {owner_to_use}/{ui_space_name}:**\n\n`{escape_html_for_markdown(error_msg)}`"
931
+ elif status_details:
932
+ stage, hardware, error, log_link = status_details.get('stage','N/A'), status_details.get('hardware','N/A'), status_details.get('error_message'), status_details.get('full_log_link','#')
933
+ md_lines = [f"### Space Status: {owner_to_use}/{ui_space_name}", f"- **Stage:** `{stage}`", f"- **Current Hardware:** `{hardware}`"]
934
+ if status_details.get('requested_hardware') and status_details.get('requested_hardware') != hardware: md_lines.append(f"- **Requested Hardware:** `{status_details.get('requested_hardware')}`")
935
+ if error: md_lines.append(f"- **Error:** <span style='color:red;'>`{escape_html_for_markdown(error)}`</span>")
936
+ md_lines.append(f"- [View Full Logs on Hugging Face]({log_link})")
937
+ if status_details.get('raw_data'):
938
+ # Add raw data in a collapsible section for debugging
939
+ md_lines.append(f"\n<details><summary>Raw Status Data (JSON)</summary>\n\n```json\n{json.dumps(status_details.get('raw_data', {}), indent=2)}\n```\n</details>")
940
+
941
+ _status_display_md = "\n".join(md_lines)
942
+ else: _status_display_md = "Could not retrieve status details."
943
+ yield _status_display_md
944
+
945
+
946
+ # Define a custom theme with a dark background and contrasting colors
947
+ # And add custom CSS for a background gradient and component styling
948
+ custom_theme = gr.themes.Base(
949
+ primary_hue="teal", # Teal for primary actions
950
+ secondary_hue="purple", # Purple for secondary elements
951
+ neutral_hue="zinc", # Zinc for neutral/backgrounds (dark gray)
952
+ text_size="sm", # Smaller text size for a denser, professional look
953
+ spacing_size="md", # Medium spacing
954
+ radius_size="sm", # Small border radius
955
+ font=["System UI", "sans-serif"] # Use system font
956
+ )
957
+
958
+ custom_css = """
959
+ body {
960
+ background: linear-gradient(to bottom right, #2c3e50, #34495e); /* Dark blue-gray gradient */
961
+ color: #ecf0f1; /* Light text color for dark background */
962
+ }
963
+ /* Adjust main Gradio container background to be transparent to see body gradient */
964
+ .gradio-container {
965
+ background: transparent !important;
966
+ }
967
+ /* Adjust component backgrounds for contrast against the body gradient */
968
+ .gr-box, .gr-panel, .gr-pill {
969
+ background-color: rgba(44, 62, 80, 0.8) !important; /* Slightly lighter transparent dark blue-gray */
970
+ border-color: rgba(189, 195, 199, 0.2) !important; /* Light border for contrast */
971
+ }
972
+ /* Adjust inputs, dropdowns, buttons etc. for visibility */
973
+ .gr-textbox, .gr-dropdown, .gr-button, .gr-code, .gr-chat-message {
974
+ border-color: rgba(189, 195, 199, 0.3) !important;
975
+ background-color: rgba(52, 73, 94, 0.9) !important; /* Slightly different dark blue-gray */
976
+ color: #ecf0f1 !important; /* Ensure text is light */
977
+ }
978
+ .gr-button.gr-button-primary {
979
+ background-color: #1abc9c !important; /* Teal from primary_hue */
980
+ color: white !important;
981
+ border-color: #16a085 !important;
982
+ }
983
+ .gr-button.gr-button-secondary {
984
+ background-color: #9b59b6 !important; /* Purple from secondary_hue */
985
+ color: white !important;
986
+ border-color: #8e44ad !important;
987
+ }
988
+ .gr-button.gr-button-stop {
989
+ background-color: #e74c3c !important; /* Red for stop/delete */
990
+ color: white !important;
991
+ border-color: #c0392b !important;
992
+ }
993
+ /* Adjust markdown backgrounds */
994
+ .gr-markdown {
995
+ background-color: rgba(44, 62, 80, 0.7) !important; /* Transparent dark background */
996
+ padding: 10px; /* Add some padding */
997
+ border-radius: 5 al;
998
+ }
999
+ /* Style markdown headers for better contrast */
1000
+ .gr-markdown h1, .gr-markdown h2, .gr-markdown h3, .gr-markdown h4, .gr-markdown h5, .gr-markdown h6 {
1001
+ color: #ecf0f1 !important; /* Ensure headers are light */
1002
+ border-bottom-color: rgba(189, 195, 199, 0.3) !important; /* Light separator */
1003
+ }
1004
+ /* Style code blocks within markdown */
1005
+ .gr-markdown pre code {
1006
+ background-color: rgba(52, 73, 94, 0.95) !important; /* Darker code background */
1007
+ border-color: rgba(189, 195, 199, 0.3) !important;
1008
+ }
1009
+ /* Chatbot specific styling */
1010
+ .gr-chatbot {
1011
+ background-color: rgba(44, 62, 80, 0.7) !important;
1012
+ border-color: rgba(189, 195, 199, 0.2) !important;
1013
+ }
1014
+ .gr-chatbot .message {
1015
+ background-color: rgba(52, 73, 94, 0.9) !important; /* Dark background for messages */
1016
+ color: #ecf0f1 !important;
1017
+ border-color: rgba(189, 195, 199, 0.3) !important;
1018
+ }
1019
+ .gr-chatbot .message.user {
1020
+ background-color: rgba(46, 204, 113, 0.9) !important; /* Greenish background for user messages */
1021
+ color: black !important; /* Dark text for green background */
1022
+ }
1023
+ """
1024
+
1025
+
1026
+ # Get initial providers and models for UI setup
1027
+ available_providers = get_available_providers()
1028
+ default_provider = available_providers[0] if available_providers else None
1029
+ initial_models = get_models_for_provider(default_provider) if default_provider else []
1030
+ initial_default_model = get_default_model_for_provider(default_provider) if default_provider else None
1031
+ # Ensure initial_default_model is in the initial_models list, fallback if not
1032
+ if initial_default_model not in initial_models and initial_models:
1033
+ initial_default_model = initial_models[0]
1034
+
1035
+
1036
+ with gr.Blocks(theme=custom_theme, css=custom_css) as demo:
1037
+ gr.Markdown("# πŸ€– AI Code & Space Generator")
1038
+ gr.Markdown("Configure settings, chat with AI to generate/modify Hugging Face Spaces, then build, preview, and edit.")
1039
+ with gr.Row():
1040
+ with gr.Sidebar():
1041
+ gr.Markdown("## βš™οΈ Configuration")
1042
+ with gr.Group(): gr.Markdown("### API Keys & Tokens");
1043
+ # Single API key input, model_logic decides which env var to check or uses this override
1044
+ api_key_input = gr.Textbox(label="AI Provider API Key (Optional Override)", type="password", placeholder="Paste key here or set env var (e.g., GROQ_API_KEY)");
1045
+ hf_api_key_input = gr.Textbox(label="Hugging Face Token (for building/loading)", type="password", placeholder="hf_...")
1046
+ with gr.Group(): gr.Markdown("### Hugging Face Space"); owner_name_input = gr.Textbox(label="HF Owner Name", placeholder="e.g., your-username"); space_name_input = gr.Textbox(label="HF Space Name", value="my-ai-space", placeholder="e.g., my-cool-app"); space_sdk_select = gr.Dropdown(label="Space SDK", choices=["gradio", "streamlit", "docker", "static"], value="gradio", info="Used for new/build."); load_space_button = gr.Button("πŸ”„ Load Existing Space", variant="secondary", size="sm")
1047
+ with gr.Group(): gr.Markdown("### AI Model Settings");
1048
+ provider_select = gr.Dropdown(label="AI Provider", choices=available_providers, value=default_provider, info="Select an AI model provider.");
1049
+ model_select = gr.Dropdown(label="AI Model", choices=initial_models, value=initial_default_model, info="Select a model.");
1050
+ system_prompt_input = gr.Textbox(label="System Prompt", lines=8, value=DEFAULT_SYSTEM_PROMPT, interactive=True)
1051
+ with gr.Column(scale=3):
1052
+ gr.Markdown("## πŸ’¬ AI Chat & Code Generation")
1053
+ # Updated chatbot avatar
1054
+ chatbot_display = gr.Chatbot(label="AI Chat", height=400, bubble_full_width=False, avatar_images=(None, "https://huggingface.co/datasets/huggingface/badges/resolve/main/huggingface-bot-avatar.svg"))
1055
+ with gr.Row(): chat_message_input = gr.Textbox(show_label=False, placeholder="Your Message...", scale=7); send_chat_button = gr.Button("Send", variant="primary", scale=1, size="lg")
1056
+ status_output = gr.Textbox(label="Chat/Process Status", interactive=False, lines=1, value="Ready.")
1057
+ gr.Markdown("---")
1058
+ with gr.Tabs():
1059
+ with gr.TabItem("πŸ“ Formatted Space Markdown"): gr.Markdown("Complete Markdown definition for your Space."); formatted_space_output_display = gr.Textbox(label="Current Space Definition", lines=15, interactive=True, show_copy_button=True, value="*Space definition...*"); download_button = gr.DownloadButton(label="Download .md", interactive=False, size="sm")
1060
+ with gr.TabItem("πŸ” Detected Files Preview"):
1061
+ detected_files_preview = gr.Markdown(value="*Files preview...*")
1062
+
1063
+ gr.Markdown("---")
1064
+ with gr.Tabs():
1065
+ with gr.TabItem("πŸš€ Build & Preview Space"):
1066
+ with gr.Row(): build_space_button = gr.Button("Build / Update Space on HF", variant="primary", scale=2); refresh_status_button = gr.Button("πŸ”„ Refresh Space Status", scale=1)
1067
+ # Build status outputs also include updating owner/space names in the textboxes
1068
+ build_status_display = gr.Textbox(label="Build Operation Status", interactive=False, lines=2, value="*Build status will appear here.*"); gr.Markdown("---"); space_runtime_status_display = gr.Markdown("*Space runtime status will appear here after refresh.*"); gr.Markdown("---"); space_iframe_display = gr.HTML(value="<!-- Space Iframe -->", visible=False)
1069
+ with gr.TabItem("✏️ Edit Space Files"):
1070
+ gr.Markdown("Select a file to view, edit, or delete. Changes are committed to HF Hub.")
1071
+ file_browser_dropdown = gr.Dropdown(label="Select File in Space", choices=[], interactive=True, visible=False, info="Load/build Space first.")
1072
+ file_content_editor = gr.Code(label="File Content Editor", language="python", lines=15, interactive=True)
1073
+ commit_message_input = gr.Textbox(label="Commit Message", placeholder="e.g., Updated app.py", value="Update via AI Space Editor")
1074
+ with gr.Row(): update_file_button = gr.Button("Commit Changes", variant="primary", scale=2); delete_file_button = gr.Button("πŸ—‘οΈ Delete Selected File", variant="stop", scale=1)
1075
+ edit_status_display = gr.Textbox(label="File Edit/Delete Status", interactive=False, lines=2, value="*Select file...*")
1076
+
1077
+ # --- Event Handlers ---
1078
+
1079
+ # Provider dropdown change event to update model dropdown
1080
+ provider_select.change(
1081
+ fn=update_models_dropdown,
1082
+ inputs=provider_select,
1083
+ outputs=model_select
1084
+ )
1085
+
1086
+ # Chat submit handler outputs
1087
+ chat_outputs = [chat_message_input, chatbot_display, status_output, detected_files_preview, formatted_space_output_display, download_button]
1088
+ # Chat submit handler inputs
1089
+ chat_inputs = [chat_message_input, chatbot_display, api_key_input, provider_select, model_select, system_prompt_input, owner_name_input, space_name_input, formatted_space_output_display] # Pass current formatted markdown as context
1090
+
1091
+ # Wire chat buttons
1092
+ send_chat_button.click(
1093
+ fn=handle_chat_submit,
1094
+ inputs=chat_inputs,
1095
+ outputs=chat_outputs
1096
+ )
1097
+ chat_message_input.submit( # Allow submitting with Enter key
1098
+ fn=handle_chat_submit,
1099
+ inputs=chat_inputs,
1100
+ outputs=chat_outputs
1101
+ )
1102
+
1103
+ # Load space outputs include clearing chat history
1104
+ load_space_outputs = [formatted_space_output_display, detected_files_preview, status_output, file_browser_dropdown, owner_name_input, space_name_input, space_iframe_display, download_button, build_status_display, edit_status_display, space_runtime_status_display, chatbot_display] # Added chatbot_display
1105
+ load_space_button.click(fn=handle_load_existing_space, inputs=[hf_api_key_input, owner_name_input, space_name_input], outputs=load_space_outputs)
1106
+
1107
+ # Build outputs now include updating owner/space name textboxes
1108
+ build_outputs = [build_status_display, space_iframe_display, file_browser_dropdown, owner_name_input, space_name_input] # Added owner_name_input, space_name_input
1109
+ build_space_button.click(fn=handle_build_space_button, inputs=[hf_api_key_input, space_name_input, owner_name_input, space_sdk_select, formatted_space_output_display], outputs=build_outputs)
1110
+
1111
+ # File edit load outputs include clearing/setting commit message and language
1112
+ file_edit_load_outputs = [file_content_editor, edit_status_display, commit_message_input, file_content_editor]
1113
+ file_browser_dropdown.change(fn=handle_load_file_for_editing, inputs=[hf_api_key_input, space_name_input, owner_name_input, file_browser_dropdown], outputs=file_edit_load_outputs)
1114
+
1115
+ # Commit file outputs include refreshing previews and file browser state
1116
+ commit_file_outputs = [edit_status_display, file_browser_dropdown, formatted_space_output_display, detected_files_preview, download_button]
1117
+ update_file_button.click(fn=handle_commit_file_changes, inputs=[hf_api_key_input, space_name_input, owner_name_input, file_browser_dropdown, file_content_editor, commit_message_input], outputs=commit_file_outputs)
1118
+
1119
+ # Delete file outputs include refreshing previews, file browser state, and clearing editor
1120
+ delete_file_outputs = [edit_status_display, file_browser_dropdown, file_browser_dropdown, file_content_editor, commit_message_input, file_content_editor, formatted_space_output_display, detected_files_preview, download_button] # Two file_browser_dropdown outputs: choices and value
1121
+ delete_file_button.click(fn=handle_delete_file, inputs=[hf_api_key_input, space_name_input, owner_name_input, file_browser_dropdown], outputs=delete_file_outputs)
1122
+
1123
+ # Refresh status handler is okay
1124
+ refresh_status_button.click(fn=handle_refresh_space_status, inputs=[hf_api_key_input, owner_name_input, space_name_input], outputs=[space_runtime_status_display])
1125
+
1126
 
1127
  if __name__ == "__main__":
1128
+ demo.launch(debug=True, share=False)