broadfield-dev commited on
Commit
11affab
Β·
verified Β·
1 Parent(s): eac82c6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +885 -372
app.py CHANGED
@@ -1,380 +1,893 @@
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
- from . import core # Use relative import for core module
13
- from . 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,
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
  import gradio as gr
2
+ import re
 
 
3
  import json
4
+ import os
5
+ import tempfile
6
+
7
+ from build_logic import (
8
+ create_space as build_logic_create_space,
9
+ _get_api_token as build_logic_get_api_token,
10
+ whoami as build_logic_whoami,
11
+ list_space_files_for_browsing,
12
+ get_space_repository_info,
13
+ get_space_file_content,
14
+ update_space_file,
15
+ parse_markdown as build_logic_parse_markdown,
16
+ delete_space_file as build_logic_delete_space_file,
17
+ get_space_runtime_status
18
+ )
19
+ print("build_logic.py loaded successfully.")
20
+
21
+ from model_logic import (
22
+ get_available_providers,
23
+ get_models_for_provider,
24
+ get_default_model_for_provider,
25
+ get_model_id_from_display_name,
26
+ generate_stream
27
+ )
28
+ print("model_logic.py loaded successfully.")
29
+
30
+ bbb = chr(96) * 3
31
+ parsed_code_blocks_state_cache = []
32
+ BOT_ROLE_NAME = "assistant"
33
+
34
+ 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.
35
+ When you provide NEW code for a file, or MODIFIED code for an existing file, use the following format exactly:
36
+ ### File: path/to/filename.ext
37
+ (You can add a short, optional, parenthesized description after the filename on the SAME line)
38
+ {bbb}language
39
+ # Your full code here
40
+ {bbb}
41
+ If the file is binary, or you cannot show its content, use this format:
42
+ ### File: path/to/binaryfile.ext
43
+ [Binary file - approximate_size bytes]
44
+ When you provide a project file structure, use this format:
45
+ ## File Structure
46
+ {bbb}
47
+ πŸ“ Root
48
+ πŸ“„ file1.py
49
+ πŸ“ subfolder
50
+ πŸ“„ file2.js
51
+ {bbb}
52
+ The role name for your responses in the chat history must be '{BOT_ROLE_NAME}'.
53
+ Adhere strictly to these formatting instructions.
54
+ If you update a file, provide the FULL file content again under the same filename.
55
+ Only the latest version of each file mentioned throughout the chat will be used for the final output.
56
+ 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.
57
+ 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.
58
+ If the user asks to delete a file, simply omit it from your next full ### File: list.
59
+ If no code is provided, assist the user with their tasks.
60
+ """
61
+
62
+ def escape_html_for_markdown(text):
63
+ if not isinstance(text, str): return ""
64
+ return text.replace("&", "&").replace("<", "<").replace(">", ">")
65
+
66
+ def _infer_lang_from_filename(filename):
67
+ if not filename: return "plaintext"
68
+ if '.' in filename:
69
+ ext = filename.split('.')[-1].lower()
70
+ mapping = {
71
+ 'py': 'python', 'js': 'javascript', 'ts': 'typescript', 'jsx': 'javascript', 'tsx': 'typescript',
72
+ 'html': 'html', 'htm': 'html', 'css': 'css', 'scss': 'scss', 'sass': 'sass', 'less': 'less',
73
+ 'json': 'json', 'xml': 'xml', 'yaml': 'yaml', 'yml': 'yaml', 'toml': 'toml',
74
+ 'md': 'markdown', 'rst': 'rst',
75
+ 'sh': 'bash', 'bash': 'bash', 'zsh': 'bash', 'bat': 'batch', 'cmd': 'batch', 'ps1': 'powershell',
76
+ 'c': 'c', 'h': 'c', 'cpp': 'cpp', 'hpp': 'cpp', 'cs': 'csharp', 'java': 'java',
77
+ 'rb': 'ruby', 'php': 'php', 'go': 'go', 'rs': 'rust', 'swift': 'swift', 'kt': 'kotlin', 'kts': 'kotlin',
78
+ 'sql': 'sql', 'dockerfile': 'docker', 'tf': 'terraform', 'hcl': 'terraform',
79
+ 'txt': 'plaintext', 'log': 'plaintext', 'ini': 'ini', 'conf': 'plaintext', 'cfg': 'plaintext',
80
+ 'csv': 'plaintext', 'tsv': 'plaintext', 'err': 'plaintext',
81
+ '.env': 'plaintext', '.gitignore': 'plaintext', '.npmrc': 'plaintext', '.gitattributes': 'plaintext',
82
+ 'makefile': 'makefile',
83
+ }
84
+ return mapping.get(ext, "plaintext")
85
+ base_filename = os.path.basename(filename)
86
+ if base_filename == 'Dockerfile': return 'docker'
87
+ if base_filename == 'Makefile': return 'makefile'
88
+ if base_filename.startswith('.'): return 'plaintext'
89
+ return "plaintext"
90
+
91
+ def _clean_filename(filename_line_content):
92
+ text = filename_line_content.strip()
93
+ text = re.sub(r'[`\*_]+', '', text)
94
+ path_match = re.match(r'^([\w\-\.\s\/\\]+)', text)
95
+ if path_match:
96
+ parts = re.split(r'\s*\(', path_match.group(1).strip(), 1)
97
+ return parts[0].strip() if parts else ""
98
+ backtick_match = re.search(r'`([^`]+)`', text)
99
+ if backtick_match:
100
+ potential_fn = backtick_match.group(1).strip()
101
+ parts = re.split(r'\s*\(|\s{2,}', potential_fn, 1)
102
+ cleaned_fn = parts[0].strip() if parts else ""
103
+ cleaned_fn = cleaned_fn.strip('`\'":;,')
104
+ if cleaned_fn: return cleaned_fn
105
+ parts = re.split(r'\s*\(|\s{2,}', text, 1)
106
+ filename_candidate = parts[0].strip() if parts else text.strip()
107
+ filename_candidate = filename_candidate.strip('`\'":;,')
108
+ return filename_candidate if filename_candidate else text.strip()
109
+
110
+ def _parse_chat_stream_logic(latest_bot_message_content, existing_files_state=None):
111
+ latest_blocks_dict = {}
112
+ if existing_files_state:
113
+ for block in existing_files_state:
114
+ if not block.get("is_structure_block"):
115
+ latest_blocks_dict[block["filename"]] = block.copy()
116
+
117
+ results = {"parsed_code_blocks": [], "preview_md": "", "default_selected_filenames": [], "error_message": None}
118
+ content = latest_bot_message_content or ""
119
+
120
+ 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(?: - [^\]]+)?\]))")
121
+ structure_pattern = re.compile(r"## File Structure\n```(?:(?P<struct_lang>[\w.-]*)\n)?(?P<structure_code>[\s\S]*?)\n```")
122
+
123
+ structure_match = structure_pattern.search(content)
124
+ if structure_match:
125
+ 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}
126
+ else:
127
+ existing_structure_block = next((b for b in (existing_files_state or []) if b.get("is_structure_block")), None)
128
+ if existing_structure_block:
129
+ latest_blocks_dict["File Structure (original)"] = existing_structure_block.copy()
130
+
131
+ current_message_file_blocks = {}
132
+ for match in file_pattern.finditer(content):
133
+ filename = _clean_filename(match.group("filename_line"))
134
+ if not filename: continue
135
+ lang, code_block, binary_msg = match.group("lang"), match.group("code"), match.group("binary_msg")
136
+ item_data = {"filename": filename, "is_binary": False, "is_structure_block": False}
137
+ if code_block is not None:
138
+ item_data["code"], item_data["language"] = code_block.strip(), (lang.strip().lower() if lang else _infer_lang_from_filename(filename))
139
+ elif binary_msg is not None:
140
+ item_data["code"], item_data["language"], item_data["is_binary"] = binary_msg.strip(), "binary", True
141
+ else: continue
142
+ current_message_file_blocks[filename] = item_data
143
+
144
+ latest_blocks_dict.update(current_message_file_blocks)
145
+
146
+ current_parsed_blocks = list(latest_blocks_dict.values())
147
+ current_parsed_blocks.sort(key=lambda b: (0, b["filename"]) if b.get("is_structure_block") else (1, b["filename"]))
148
+
149
+ results["parsed_code_blocks"] = current_parsed_blocks
150
+ results["default_selected_filenames"] = [b["filename"] for b in current_parsed_blocks if not b.get("is_structure_block")]
151
+ return results
152
+
153
+ def _export_selected_logic(selected_filenames, space_line_name_for_md, parsed_blocks_for_export):
154
+ results = {"output_str": "", "error_message": None, "download_filepath": None}
155
+ 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]"))]
156
+ 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]")]
157
+
158
+ all_filenames_in_state = sorted(list(set(b["filename"] for b in parsed_blocks_for_export if not b.get("is_structure_block"))))
159
+
160
+ if not all_filenames_in_state and not any(b.get("is_structure_block") for b in parsed_blocks_for_export):
161
+ 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.*"
162
+ try:
163
+ with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".md", encoding='utf-8') as tmpfile:
164
+ tmpfile.write(results["output_str"]); results["download_filepath"] = tmpfile.name
165
+ except Exception as e: print(f"Error creating temp file for empty export: {e}")
166
+ return results
167
+
168
+ output_lines = [f"# Space: {space_line_name_for_md}"]
169
+
170
+ structure_block = next((b for b in parsed_blocks_for_export if b.get("is_structure_block")), None)
171
+ if structure_block:
172
+ output_lines.extend(["## File Structure", bbb, structure_block["code"].strip(), bbb, ""])
173
+ else:
174
+ output_lines.extend(["## File Structure", bbb, "πŸ“ Root"])
175
+ if all_filenames_in_state:
176
+ for fname in all_filenames_in_state: output_lines.append(f" πŸ“„ {fname}")
177
+ output_lines.extend([bbb, ""])
178
+
179
+ output_lines.append("Below are the contents of all files in the space:\n")
180
+ exported_content = False
181
+
182
+ files_to_export_content = []
183
+ if selected_filenames:
184
+ files_to_export_content = [b for b in exportable_blocks_content if b["filename"] in selected_filenames]
185
+ else:
186
+ files_to_export_content = exportable_blocks_content
187
+
188
+ binary_error_blocks_to_export = []
189
+ if selected_filenames:
190
+ binary_error_blocks_to_export = [b for b in binary_blocks_content if b["filename"] in selected_filenames]
191
+ elif binary_blocks_content:
192
+ binary_error_blocks_to_export = binary_blocks_content
193
+
194
+ all_blocks_to_export_content = sorted(files_to_export_content + binary_error_blocks_to_export, key=lambda b: b["filename"])
195
+
196
+ for block in all_blocks_to_export_content:
197
+ output_lines.append(f"### File: {block['filename']}")
198
+ 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]"):
199
+ output_lines.append(block.get('code','[Binary or Skipped file]'))
200
  else:
201
+ output_lines.extend([f"{bbb}{block.get('language', 'plaintext') or 'plaintext'}", block.get('code',''), bbb])
202
+ output_lines.append(""); exported_content = True
203
+
204
+ if not exported_content and not all_filenames_in_state: output_lines.append("*No files in state.*")
205
+ elif not exported_content: output_lines.append("*No files with editable content are in the state or selected.*")
206
+
207
+ final_output_str = "\n".join(output_lines)
208
+ results["output_str"] = final_output_str
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
  try:
210
+ with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".md", encoding='utf-8') as tmpfile:
211
+ tmpfile.write(final_output_str); results["download_filepath"] = tmpfile.name
212
+ except Exception as e: print(f"Error creating temp file: {e}"); results["error_message"] = "Could not prepare file for download."
213
+ return results
214
+
215
+ def _convert_gr_history_to_api_messages(system_prompt, gr_history, current_user_message=None):
216
+ messages = [{"role": "system", "content": system_prompt}] if system_prompt else []
217
+ for user_msg, bot_msg in gr_history:
218
+ if user_msg: messages.append({"role": "user", "content": user_msg})
219
+ if bot_msg and isinstance(bot_msg, str): messages.append({"role": BOT_ROLE_NAME, "content": bot_msg})
220
+ if current_user_message: messages.append({"role": "user", "content": current_user_message})
221
+ return messages
222
+
223
+ def _generate_ui_outputs_from_cache(owner, space_name):
224
+ global parsed_code_blocks_state_cache
225
+ preview_md_val = "*No files in cache to display.*"
226
+ 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.*"
227
+ download_file = None
228
+
229
+ if parsed_code_blocks_state_cache:
230
+ preview_md_lines = ["## Detected/Updated Files & Content (Latest Versions):"]
231
+ for block in parsed_code_blocks_state_cache:
232
+ preview_md_lines.append(f"\n----\n**File:** `{escape_html_for_markdown(block['filename'])}`")
233
+ if block.get('is_structure_block'): preview_md_lines.append(f" (Original File Structure from AI)\n")
234
+ elif block.get('is_binary'): preview_md_lines.append(f" (Binary File)\n")
235
+ elif block.get('language') and block.get('language') != 'binary': preview_md_lines.append(f" (Language: `{block['language']}`)\n")
236
+ else: preview_md_lines.append("\n")
237
+
238
+ content = block.get('code', '')
239
+ if block.get('is_binary') or content.startswith("["):
240
+ preview_md_lines.append(f"\n`{escape_html_for_markdown(content)}`\n")
241
+ elif block.get('is_structure_block'):
242
+ preview_md_lines.append(f"\n{bbb}{block.get('language', 'plaintext') or 'plaintext'}\n{content}\n{bbb}\n")
243
+ else:
244
+ preview_md_lines.append(f"\n{bbb}{block.get('language', 'plaintext') or 'plaintext'}\n{content}\n{bbb}\n")
245
+
246
+ preview_md_val = "\n".join(preview_md_lines)
247
+ space_line_name = f"{owner}/{space_name}" if owner and space_name else (owner or space_name or "your-space")
248
+
249
+ export_result = _export_selected_logic(None, space_line_name, parsed_code_blocks_state_cache)
250
+ formatted_md_val = export_result["output_str"]
251
+ download_file = export_result["download_filepath"]
252
+
253
+ return formatted_md_val, preview_md_val, gr.update(value=download_file, interactive=download_file is not None)
254
+
255
+ 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):
256
+ global parsed_code_blocks_state_cache
257
+ _chat_msg_in = ""
258
+ _chat_hist = list(chat_history)
259
+ _status = "Initializing..."
260
+ _detected_files_update, _formatted_output_update, _download_btn_update = gr.update(), gr.update(), gr.update(interactive=False, value=None)
261
+
262
+ if user_message and _current_formatted_markdown:
263
  try:
264
+ parsed_from_md = build_logic_parse_markdown(_current_formatted_markdown)
265
+ new_cache_state = []
266
+ existing_structure_block = next((b for b in parsed_code_blocks_state_cache if b.get("is_structure_block")), None)
267
+ if existing_structure_block:
268
+ new_cache_state.append(existing_structure_block.copy())
269
+
270
+ for f_info in parsed_from_md.get("files", []):
271
+ if f_info.get("path") and f_info["path"] != "File Structure (original)":
272
+ 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]"))
273
+ found_existing = False
274
+ for i, block in enumerate(new_cache_state):
275
+ if block["filename"] == f_info["path"] and not block.get("is_structure_block"):
276
+ new_cache_state[i] = {
277
+ "filename": f_info["path"],
278
+ "code": f_info.get("content", ""),
279
+ "language": "binary" if is_binary_repr else _infer_lang_from_filename(f_info["path"]),
280
+ "is_binary": is_binary_repr,
281
+ "is_structure_block": False
282
+ }
283
+ found_existing = True
284
+ break
285
+ if not found_existing:
286
+ new_cache_state.append({
287
+ "filename": f_info["path"],
288
+ "code": f_info.get("content", ""),
289
+ "language": "binary" if is_binary_repr else _infer_lang_from_filename(f_info["path"]),
290
+ "is_binary": is_binary_repr,
291
+ "is_structure_block": False
292
+ })
293
+ new_cache_state.sort(key=lambda b: (0, b["filename"]) if b.get("is_structure_block") else (1, b["filename"]))
294
+ parsed_code_blocks_state_cache = new_cache_state
295
+
296
+ except Exception as e:
297
+ print(f"Error parsing formatted markdown before chat submit: {e}")
298
+
299
+ if not user_message.strip():
300
+ _status = "Cannot send an empty message."
301
+ yield (_chat_msg_in, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update); return
302
+ _chat_hist.append((user_message, None)); _status = f"Sending to {model_select} via {provider_select}..."
303
+ yield (_chat_msg_in, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update)
304
+
305
+ api_key_override = api_key_input
306
+ current_sys_prompt = system_prompt.strip() or DEFAULT_SYSTEM_PROMPT
307
+
308
+ current_files_context = ""
309
+ if parsed_code_blocks_state_cache:
310
+ current_files_context = "\n\n## Current Files in Space\n"
311
+ for block in parsed_code_blocks_state_cache:
312
+ if block.get("is_structure_block"):
313
+ current_files_context += f"### File: {block['filename']}\n{bbb}\n{block['code']}\n{bbb}\n"
314
+ else:
315
+ current_files_context += f"### File: {block['filename']}\n"
316
+ if block.get("is_binary") or block.get("code", "").startswith("["):
317
+ current_files_context += f"{block['code']}\n"
318
+ else:
319
+ current_files_context += f"{bbb}{block.get('language', 'plaintext') or 'plaintext'}\n{block.get('code','')}\n{bbb}\n"
320
+ current_files_context += "\n"
321
+
322
+ user_message_with_context = user_message.strip()
323
+ if current_files_context.strip():
324
+ 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."
325
+
326
+ api_msgs = _convert_gr_history_to_api_messages(current_sys_prompt, _chat_hist[:-1], user_message_with_context)
327
+
328
+ try:
329
+ _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)
330
+
331
+ full_bot_response_content = ""
332
+ error_during_stream = None
333
+
334
+ for chunk in generate_stream(provider_select, model_select, api_key_override, api_msgs):
335
+ if chunk is None: continue
336
+ if isinstance(chunk, str) and (chunk.startswith("Error: ") or chunk.startswith("API HTTP Error")):
337
+ full_bot_response_content = chunk
338
+ error_during_stream = chunk
339
+ break
340
+ else:
341
+ full_bot_response_content += str(chunk)
342
+ _chat_hist[-1] = (user_message, full_bot_response_content)
343
+ _status = f"Streaming from {model_select}..."
344
+ yield (_chat_msg_in, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update)
345
+
346
+ if error_during_stream:
347
+ _status = error_during_stream
348
+ elif full_bot_response_content and not full_bot_response_content.startswith("Error: "):
349
+ _status = f"Streaming complete. Processing files from {model_select} response..."
350
+
351
+ parsing_res = _parse_chat_stream_logic(full_bot_response_content, existing_files_state=parsed_code_blocks_state_cache)
352
+
353
+ if parsing_res["error_message"]:
354
+ _status = f"Parsing Error: {parsing_res['error_message']}"
355
+ _detected_files_update = gr.Markdown(f"## Parsing Error\n`{escape_html_for_markdown(parsing_res['error_message'])}`")
356
+ else:
357
+ global parsed_code_blocks_state_cache
358
+ parsed_code_blocks_state_cache = parsing_res["parsed_code_blocks"]
359
+
360
+ _formatted_output_update, _detected_files_update, _download_btn_update = _generate_ui_outputs_from_cache(hf_owner_name, hf_repo_name)
361
+ _status = "Processing complete. Previews updated."
362
+ else:
363
+ if not error_during_stream:
364
+ _status = "AI response complete, but returned no content."
365
+ _formatted_output_update, _detected_files_update, _download_btn_update = _generate_ui_outputs_from_cache(hf_owner_name, hf_repo_name)
366
+
367
+ except Exception as e:
368
+ error_msg = f"An unexpected error occurred during AI generation: {e}"
369
+ print(f"Unexpected error in chat submit stream: {e}")
370
+ if _chat_hist and len(_chat_hist) > 0 and _chat_hist[-1][1] is None:
371
+ _chat_hist[-1] = (_chat_hist[-1][0], error_msg)
372
+ else:
373
+ _chat_hist.append((user_message, error_msg))
374
+ _status = error_msg
375
+ _formatted_output_update, _detected_files_update, _download_btn_update = _generate_ui_outputs_from_cache(hf_owner_name, hf_repo_name)
376
+
377
+ yield (_chat_msg_in, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update)
378
+
379
+ def update_models_dropdown(provider_select):
380
+ if not provider_select:
381
+ return gr.update(choices=[], value=None)
382
+ models = get_models_for_provider(provider_select)
383
+ default_model = get_default_model_for_provider(provider_select)
384
+ if default_model and default_model in models:
385
+ selected_value = default_model
386
+ elif models:
387
+ selected_value = models[0]
388
+ else:
389
+ selected_value = None
390
+ return gr.update(choices=models, value=selected_value)
391
+
392
+ def handle_load_existing_space(hf_api_key_ui, ui_owner_name, ui_space_name):
393
+ global parsed_code_blocks_state_cache
394
+ _formatted_md_val, _detected_preview_val, _status_val = "*Loading files...*", "*Loading files...*", f"Loading Space: {ui_owner_name}/{ui_space_name}..."
395
+ _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)
396
+ _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.*"
397
+ _chat_history_clear = []
398
+
399
+ 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)
400
+
401
+ owner_to_use, updated_owner_name_val = ui_owner_name, ui_owner_name
402
+ error_occurred = False
403
+
404
+ if not owner_to_use:
405
+ token, token_err = build_logic_get_api_token(hf_api_key_ui)
406
+ if token_err or not token:
407
+ _status_val = f"Error: {token_err or 'Cannot determine owner from token.'}"; error_occurred = True
408
+ else:
409
+ try:
410
+ user_info = build_logic_whoami(token=token)
411
+ if user_info and 'name' in user_info:
412
+ owner_to_use, updated_owner_name_val = user_info['name'], user_info['name']; _status_val += f" (Auto-detected owner: {owner_to_use})"
413
+ else:
414
+ _status_val = "Error: Could not auto-detect owner from token."; error_occurred = True
415
+ except Exception as e:
416
+ _status_val = f"Error auto-detecting owner: {e}"; error_occurred = True
417
+
418
+ if not owner_to_use or not ui_space_name:
419
+ if not error_occurred: _status_val = "Error: Owner and Space Name are required."; error_occurred = True
420
+
421
+ if error_occurred:
422
+ 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)
423
+ parsed_code_blocks_state_cache = []
424
+ return
425
+
426
+ sdk_for_iframe, file_list, err_list_files = get_space_repository_info(hf_api_key_ui, ui_space_name, owner_to_use)
427
+
428
+ sub_owner = re.sub(r'[^a-z0-9\-]+', '-', owner_to_use.lower()).strip('-') or 'owner'
429
+ sub_repo = re.sub(r'[^a-z0-9\-]+', '-', ui_space_name.lower()).strip('-') or 'space'
430
+ iframe_url = f"https://{sub_owner}-{sub_repo}{'.static.hf.space' if sdk_for_iframe == 'static' else '.hf.space'}"
431
+ _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)
432
+
433
+ if err_list_files and not file_list:
434
+ _status_val = f"File List Error: {err_list_files}"
435
+ parsed_code_blocks_state_cache = []
436
+ _formatted_md_val, _detected_preview_val, _download_btn_update = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name)
437
+ _file_browser_update = gr.update(visible=True, choices=[], value="Error loading files")
438
+ 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)
439
+ return
440
+
441
+ if not file_list:
442
+ _status_val = f"Loaded Space: {owner_to_use}/{ui_space_name}. No files found ({err_list_files or 'Repository is empty'})."
443
+ parsed_code_blocks_state_cache = []
444
+ _formatted_md_val, _detected_preview_val, _download_btn_update = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name)
445
+ _file_browser_update = gr.update(visible=True, choices=[], value="No files found")
446
+ 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)
447
+ return
448
+
449
+ loaded_files_for_cache = []
450
+ _status_val = f"Loading {len(file_list)} files from {owner_to_use}/{ui_space_name} (SDK: {sdk_for_iframe or 'unknown'})...";
451
+ 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)
452
+
453
+ for file_path in file_list:
454
+ _, ext = os.path.splitext(file_path)
455
+ 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 \
456
+ file_path.startswith(".git") or "/.git/" in file_path or \
457
+ file_path in ["requirements.txt", "environment.yml", "setup.py", "Pipfile", "pyproject.toml", "package.json", "yarn.lock", "pnpm-lock.yaml", "poetry.lock"] or \
458
+ file_path.endswith(".lock") or \
459
+ file_path.startswith("__pycache__/") or "/__pycache__/" in file_path or \
460
+ file_path.startswith("node_modules/") or "/node_modules/" in file_path or \
461
+ file_path.startswith("venv/") or "/venv/" in file_path or \
462
+ file_path.startswith(".venv/") or "/.venv/" in file_path or \
463
+ file_path == "README.md" or file_path == "LICENSE":
464
+ loaded_files_for_cache.append({"filename": file_path, "code": "[Binary or Skipped file]", "language": "binary", "is_binary": True, "is_structure_block": False}); continue
465
  try:
466
+ content, err_get = get_space_file_content(hf_api_key_ui, ui_space_name, owner_to_use, file_path)
467
+ if err_get:
468
+ 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})
469
+ print(f"Error loading {file_path}: {err_get}");
470
+ continue
471
+ loaded_files_for_cache.append({"filename": file_path, "code": content, "language": _infer_lang_from_filename(file_path), "is_binary": False, "is_structure_block": False})
472
+ except Exception as content_ex:
473
+ 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})
474
+ print(f"Unexpected error loading {file_path}: {content_ex}")
475
+ continue
476
+
477
+ parsed_code_blocks_state_cache = loaded_files_for_cache
478
+ _formatted_md_val, _detected_preview_val, _download_btn_update = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name)
479
+ _status_val = f"Successfully loaded Space: {owner_to_use}/{ui_space_name}. Markdown ready. {len(file_list)} files listed."
480
+ _file_browser_update = gr.update(visible=True, choices=sorted(file_list or []), value=None)
481
+
482
+ 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)
483
+
484
+ def handle_build_space_button(hf_api_key_ui, ui_space_name_part, ui_owner_name_part, space_sdk_ui, formatted_markdown_content):
485
+ global parsed_code_blocks_state_cache
486
+ _build_status, _iframe_html, _file_browser_update = "Starting space build process...", gr.update(value=None, visible=False), gr.update(visible=False, choices=[], value=None)
487
+ yield _build_status, _iframe_html, _file_browser_update, gr.update(value=ui_owner_name_part), gr.update(value=ui_space_name_part)
488
+ 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
489
+ final_owner_for_build = ui_owner_name_part
490
+ if not final_owner_for_build:
491
+ token_for_whoami, token_err = build_logic_get_api_token(hf_api_key_ui)
492
+ if token_err: _build_status = f"Build Error: {token_err}"; yield _build_status, _iframe_html, _file_browser_update, gr.update(), gr.update(); return
493
+ if token_for_whoami:
494
+ try:
495
+ user_info = build_logic_whoami(token=token_for_whoami)
496
+ final_owner_for_build = user_info['name'] if user_info and 'name' in user_info else final_owner_for_build
497
+ if not final_owner_for_build: _build_status += "\n(Warning: Could not auto-detect owner from token for build. Please specify.)"
498
+ except Exception as e: _build_status += f"\n(Warning: Could not auto-detect owner for build: {e}. Please specify.)"
499
+ else: _build_status += "\n(Warning: Owner not specified and no token to auto-detect for build. Please specify owner or provide a token.)"
500
+
501
+ 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
502
+
503
+ try:
504
+ parsed_from_md_for_build = build_logic_parse_markdown(formatted_markdown_content)
505
+ parsed_code_blocks_state_cache = []
506
+ if parsed_from_md_for_build.get("owner_md"):
507
+ ui_owner_name_part = parsed_from_md_for_build["owner_md"]
508
+ if parsed_from_md_for_build.get("repo_name_md"):
509
+ ui_space_name_part = parsed_from_md_for_build["repo_name_md"]
510
+
511
+ structure_block_md = next((f for f in parsed_from_md_for_build.get("files", []) if f.get("path") == "File Structure (original)"), None)
512
+ if structure_block_md:
513
+ parsed_code_blocks_state_cache.append({
514
+ "filename": structure_block_md["path"],
515
+ "code": structure_block_md["content"],
516
+ "language": "plaintext",
517
+ "is_binary": False,
518
+ "is_structure_block": True
519
+ })
520
+
521
+ for f_info in parsed_from_md_for_build.get("files", []):
522
+ if f_info.get("path") and f_info["path"] != "File Structure (original)":
523
+ 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]"))
524
+ parsed_code_blocks_state_cache.append({
525
+ "filename": f_info["path"],
526
+ "code": f_info.get("content", ""),
527
+ "language": "binary" if is_binary_repr else _infer_lang_from_filename(f_info["path"]),
528
+ "is_binary": is_binary_repr,
529
+ "is_structure_block": False
530
+ })
531
+ parsed_code_blocks_state_cache.sort(key=lambda b: (0, b["filename"]) if b.get("is_structure_block") else (1, b["filename"]))
532
+
533
+ except Exception as e:
534
+ _build_status = f"Build Error: Failed to parse Markdown structure before building: {e}";
535
+ yield _build_status, _iframe_html, _file_browser_update, gr.update(value=ui_owner_name_part), gr.update(value=ui_space_name_part); return
536
+
537
+ result_message = build_logic_create_space(hf_api_key_ui, ui_space_name_part, final_owner_for_build, space_sdk_ui, formatted_markdown_content)
538
+ _build_status = f"Build Process: {result_message}"
539
+
540
+ owner_name_output = gr.update(value=ui_owner_name_part)
541
+ space_name_output = gr.update(value=ui_space_name_part)
542
+
543
+ if "Successfully" in result_message:
544
+ sub_owner = re.sub(r'[^a-z0-9\-]+', '-', ui_owner_name_part.lower()).strip('-') or 'owner'
545
+ sub_repo = re.sub(r'[^a-z0-9\-]+', '-', ui_space_name_part.lower()).strip('-') or 'space'
546
+ iframe_url = f"https://{sub_owner}-{sub_repo}{'.static.hf.space' if space_sdk_ui == 'static' else '.hf.space'}"
547
+ _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)
548
+ _build_status += f"\nSpace live at: [Link]({iframe_url}) (Repo: https://huggingface.co/spaces/{ui_owner_name_part}/{ui_space_name_part})"
549
+
550
+ file_list, err_list = list_space_files_for_browsing(hf_api_key_ui, ui_space_name_part, ui_owner_name_part)
551
+ 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")
552
+ else: _file_browser_update = gr.update(visible=True, choices=sorted(file_list or []), value=None if file_list else "No files found")
553
+
554
+ yield _build_status, _iframe_html, _file_browser_update, owner_name_output, space_name_output
555
+
556
+ def handle_load_file_for_editing(hf_api_key_ui, ui_space_name_part, ui_owner_name_part, selected_file_path):
557
+ global parsed_code_blocks_state_cache
558
+ _file_content_val, _edit_status_val, _commit_msg_val, _lang_update = "", "Error: No file selected.", gr.update(value=""), gr.update(language="python")
559
+ if not selected_file_path or selected_file_path in ["No files found", "Error loading files", "Error refreshing files"]:
560
+ yield _file_content_val, "Select a file from the dropdown.", _commit_msg_val, _lang_update
561
+ return
562
+
563
+ owner_to_use = ui_owner_name_part
564
+ if not owner_to_use:
565
+ token, token_err = build_logic_get_api_token(hf_api_key_ui)
566
+ if token_err: _edit_status_val = f"Error: {token_err}"; yield (_file_content_val, _edit_status_val, _commit_msg_val, _lang_update); return
567
+ if token:
568
+ try:
569
+ 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
570
+ 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
571
+ 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
572
+ 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
573
+
574
+ 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
575
+
576
+ _edit_status_val = f"Loading {selected_file_path}..."
577
+ yield gr.update(value=""), _edit_status_val, gr.update(value=""), gr.update(language="python")
578
+
579
+ content, err = get_space_file_content(hf_api_key_ui, ui_space_name_part, owner_to_use, selected_file_path)
580
+
581
+ if err:
582
+ _edit_status_val = f"Error loading '{selected_file_path}': {err}"
583
+ _commit_msg_val = f"Error loading {selected_file_path}"
584
+ _file_content_val = f"Error loading {selected_file_path}:\n{err}"
585
+ _lang_update = gr.update(language="python")
586
+ yield _file_content_val, _edit_status_val, _commit_msg_val, _lang_update
587
+ return
588
+
589
+ _file_content_val = content or ""
590
+ _edit_status_val = f"Loaded {selected_file_path} for editing."
591
+ _commit_msg_val = f"Update {selected_file_path} via AI Space Editor"
592
+ _lang_update = gr.update(language=_infer_lang_from_filename(selected_file_path))
593
+
594
+ yield _file_content_val, _edit_status_val, _commit_msg_val, _lang_update
595
+
596
+ 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):
597
+ global parsed_code_blocks_state_cache
598
+ _edit_status_val = "Processing commit..."
599
+ _file_browser_update_val = gr.update()
600
+ _formatted_md_out = gr.update()
601
+ _detected_preview_out = gr.update()
602
+ _download_btn_out = gr.update()
603
+
604
+ yield _edit_status_val, _file_browser_update_val, _formatted_md_out, _detected_preview_out, _download_btn_out
605
+
606
+ if not file_to_edit_path or file_to_edit_path in ["No files found", "Error loading files", "Error refreshing files"]:
607
+ _edit_status_val = "Error: No valid file selected for commit.";
608
+ yield _edit_status_val, gr.update(), gr.update(), gr.update(), gr.update(); return
609
+
610
+ owner_to_use = ui_owner_name_part
611
+ if not owner_to_use:
612
+ token, token_err = build_logic_get_api_token(hf_api_key_ui)
613
+ if token_err: _edit_status_val = f"Error: {token_err}"; yield (_edit_status_val, gr.update(), gr.update(), gr.update(), gr.update()); return
614
+ if token:
615
+ try:
616
+ 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
617
+ 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
618
+ 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
619
+ 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
620
+
621
+ 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
622
+
623
+ status_msg = update_space_file(hf_api_key_ui, ui_space_name_part, owner_to_use, file_to_edit_path, edited_content, commit_message)
624
+ _edit_status_val = status_msg
625
+
626
+ if "Successfully updated" in status_msg:
627
+ found_in_cache = False
628
+ for block in parsed_code_blocks_state_cache:
629
+ if block["filename"] == file_to_edit_path:
630
+ block["code"] = edited_content
631
+ block["language"] = _infer_lang_from_filename(file_to_edit_path)
632
+ block["is_binary"] = False
633
+ block["is_structure_block"] = False
634
+ found_in_cache = True
635
+ break
636
+ if not found_in_cache:
637
+ parsed_code_blocks_state_cache = [b for b in parsed_code_blocks_state_cache if b["filename"] != file_to_edit_path]
638
+ parsed_code_blocks_state_cache.append({
639
+ "filename": file_to_edit_path,
640
+ "code": edited_content,
641
+ "language": _infer_lang_from_filename(file_to_edit_path),
642
+ "is_binary": False,
643
+ "is_structure_block": False
644
+ })
645
+ parsed_code_blocks_state_cache.sort(key=lambda b: (0, b["filename"]) if b.get("is_structure_block") else (1, b["filename"]))
646
+
647
+ _formatted_md_out, _detected_preview_out, _download_btn_out = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name_part)
648
+
649
+ new_file_list, err_list = list_space_files_for_browsing(hf_api_key_ui, ui_space_name_part, owner_to_use)
650
+ if err_list:
651
+ _edit_status_val += f"\nFile list refresh error: {err_list}"
652
+ _file_browser_update_val = gr.update(choices=sorted(new_file_list or []), value="Error refreshing files")
653
+ else:
654
+ _file_browser_update_val = gr.update(choices=sorted(new_file_list or []), value=file_to_edit_path)
655
+
656
+ yield _edit_status_val, _file_browser_update_val, _formatted_md_out, _detected_preview_out, _download_btn_out
657
+
658
+ def handle_delete_file(hf_api_key_ui, ui_space_name_part, ui_owner_name_part, file_to_delete_path):
659
+ global parsed_code_blocks_state_cache
660
+ _edit_status_val = "Processing deletion..."
661
+ _file_browser_choices_update = gr.update()
662
+ _file_browser_value_update = None
663
+ _file_content_editor_update = gr.update(value="")
664
+ _commit_msg_update = gr.update(value="")
665
+ _lang_update = gr.update(language="plaintext")
666
+ _formatted_md_out = gr.update()
667
+ _detected_preview_out = gr.update()
668
+ _download_btn_out = gr.update()
669
+
670
+ 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)
671
+
672
+ if not file_to_delete_path or file_to_delete_path in ["No files found", "Error loading files", "Error refreshing files"]:
673
+ _edit_status_val = "Error: No valid file selected for deletion.";
674
+ yield (_edit_status_val, gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()); return
675
+
676
+ owner_to_use = ui_owner_name_part
677
+ if not owner_to_use:
678
+ token, token_err = build_logic_get_api_token(hf_api_key_ui)
679
+ 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
680
+ if token:
681
+ try:
682
+ 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
683
+ 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
684
+ 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
685
+ 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
686
+
687
+ 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
688
+
689
+ deletion_status_msg = build_logic_delete_space_file(hf_api_key_ui, ui_space_name_part, owner_to_use, file_to_delete_path)
690
+ _edit_status_val = deletion_status_msg
691
+
692
+ new_file_list, err_list = list_space_files_for_browsing(hf_api_key_ui, ui_space_name_part, owner_to_use)
693
+
694
+ if "Successfully deleted" in deletion_status_msg:
695
+ parsed_code_blocks_state_cache = [b for b in parsed_code_blocks_state_cache if b["filename"] != file_to_delete_path]
696
+ _formatted_md_out, _detected_preview_out, _download_btn_out = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name_part)
697
+
698
+ if err_list:
699
+ _edit_status_val += f"\nFile list refresh error: {err_list}"
700
+ _file_browser_choices_update = gr.update(choices=sorted(new_file_list or []), value="Error refreshing files")
701
+ else:
702
+ _file_browser_choices_update = gr.update(choices=sorted(new_file_list or []), value=None)
703
+
704
+ _file_browser_value_update = None
705
+ else:
706
+ if err_list:
707
+ _edit_status_val += f"\nFile list refresh error: {err_list}"
708
+ _file_browser_choices_update = gr.update(choices=sorted(new_file_list or []), value="Error refreshing files")
709
+ _file_browser_value_update = "Error refreshing files"
710
+ else:
711
+ _file_browser_choices_update = gr.update(choices=sorted(new_file_list or []), value=file_to_delete_path)
712
+ _file_browser_value_update = file_to_delete_path
713
+ _formatted_md_out, _detected_preview_out, _download_btn_out = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name_part)
714
+
715
+ 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)
716
+
717
+ def handle_refresh_space_status(hf_api_key_ui, ui_owner_name, ui_space_name):
718
+ yield "*Fetching space status...*"
719
+ owner_to_use = ui_owner_name
720
+ if not owner_to_use:
721
+ token, token_err = build_logic_get_api_token(hf_api_key_ui)
722
+ if token_err or not token: yield f"**Error:** {token_err or 'Cannot determine owner.'}"; return
723
+ 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
724
+ except Exception as e: yield f"**Error auto-detecting owner:** {e}"; return
725
+ if not owner_to_use or not ui_space_name: yield "**Error:** Owner and Space Name are required."; return
726
+ status_details, error_msg = get_space_runtime_status(hf_api_key_ui, ui_space_name, owner_to_use)
727
+ 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)}`"
728
+ elif status_details:
729
+ 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','#')
730
+ md_lines = [f"### Space Status: {owner_to_use}/{ui_space_name}", f"- **Stage:** `{stage}`", f"- **Current Hardware:** `{hardware}`"]
731
+ if status_details.get('requested_hardware') and status_details.get('requested_hardware') != hardware: md_lines.append(f"- **Requested Hardware:** `{status_details.get('requested_hardware')}`")
732
+ if error: md_lines.append(f"- **Error:** <span style='color:red;'>`{escape_html_for_markdown(error)}`</span>")
733
+ md_lines.append(f"- [View Full Logs on Hugging Face]({log_link})")
734
+ if status_details.get('raw_data'):
735
+ 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>")
736
+ _status_display_md = "\n".join(md_lines)
737
+ else: _status_display_md = "Could not retrieve status details."
738
+ yield _status_display_md
739
+
740
+ custom_theme = gr.themes.Base(
741
+ primary_hue="teal",
742
+ secondary_hue="purple",
743
+ neutral_hue="zinc",
744
+ text_size="sm",
745
+ spacing_size="md",
746
+ radius_size="sm",
747
+ font=["System UI", "sans-serif"]
748
+ )
749
+
750
+ custom_css = """
751
+ body {
752
+ background: linear-gradient(to bottom right, #2c3e50, #34495e);
753
+ color: #ecf0f1;
754
+ }
755
+ .gradio-container {
756
+ background: transparent !important;
757
+ }
758
+ .gr-box, .gr-panel, .gr-pill {
759
+ background-color: rgba(44, 62, 80, 0.8) !important;
760
+ border-color: rgba(189, 195, 199, 0.2) !important;
761
+ }
762
+ .gr-textbox, .gr-dropdown, .gr-button, .gr-code, .gr-chat-message {
763
+ border-color: rgba(189, 195, 199, 0.3) !important;
764
+ background-color: rgba(52, 73, 94, 0.9) !important;
765
+ color: #ecf0f1 !important;
766
+ }
767
+ .gr-button.gr-button-primary {
768
+ background-color: #1abc9c !important;
769
+ color: white !important;
770
+ border-color: #16a085 !important;
771
+ }
772
+ .gr-button.gr-button-secondary {
773
+ background-color: #9b59b6 !important;
774
+ color: white !important;
775
+ border-color: #8e44ad !important;
776
+ }
777
+ .gr-button.gr-button-stop {
778
+ background-color: #e74c3c !important;
779
+ color: white !important;
780
+ border-color: #c0392b !important;
781
+ }
782
+ .gr-markdown {
783
+ background-color: rgba(44, 62, 80, 0.7) !important;
784
+ padding: 10px;
785
+ border-radius: 5px;
786
+ }
787
+ .gr-markdown h1, .gr-markdown h2, .gr-markdown h3, .gr-markdown h4, .gr-markdown h5, .gr-markdown h6 {
788
+ color: #ecf0f1 !important;
789
+ border-bottom-color: rgba(189, 195, 199, 0.3) !important;
790
+ }
791
+ .gr-markdown pre code {
792
+ background-color: rgba(52, 73, 94, 0.95) !important;
793
+ border-color: rgba(189, 195, 199, 0.3) !important;
794
+ }
795
+ .gr-chatbot {
796
+ background-color: rgba(44, 62, 80, 0.7) !important;
797
+ border-color: rgba(189, 195, 199, 0.2) !important;
798
+ }
799
+ .gr-chatbot .message {
800
+ background-color: rgba(52, 73, 94, 0.9) !important;
801
+ color: #ecf0f1 !important;
802
+ border-color: rgba(189, 195, 199, 0.3) !important;
803
+ }
804
+ .gr-chatbot .message.user {
805
+ background-color: rgba(46, 204, 113, 0.9) !important;
806
+ color: black !important;
807
+ }
808
+ """
809
+
810
+ available_providers = get_available_providers()
811
+ default_provider = available_providers[0] if available_providers else None
812
+ initial_models = get_models_for_provider(default_provider) if default_provider else []
813
+ initial_default_model = get_default_model_for_provider(default_provider) if default_provider else None
814
+ if initial_default_model not in initial_models and initial_models:
815
+ initial_default_model = initial_models[0]
816
+
817
+ with gr.Blocks(theme=custom_theme, css=custom_css) as demo:
818
+ gr.Markdown("# πŸ€– AI Code & Space Generator")
819
+ gr.Markdown("Configure settings, chat with AI to generate/modify Hugging Face Spaces, then build, preview, and edit.")
820
+ with gr.Row():
821
+ with gr.Sidebar():
822
+ gr.Markdown("## βš™οΈ Configuration")
823
+ with gr.Group(): gr.Markdown("### API Keys & Tokens");
824
+ 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)");
825
+ hf_api_key_input = gr.Textbox(label="Hugging Face Token (for building/loading)", type="password", placeholder="hf_...")
826
+ 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")
827
+ with gr.Group(): gr.Markdown("### AI Model Settings");
828
+ provider_select = gr.Dropdown(label="AI Provider", choices=available_providers, value=default_provider, info="Select an AI model provider.");
829
+ model_select = gr.Dropdown(label="AI Model", choices=initial_models, value=initial_default_model, info="Select a model.");
830
+ system_prompt_input = gr.Textbox(label="System Prompt", lines=8, value=DEFAULT_SYSTEM_PROMPT, interactive=True)
831
+ with gr.Column(scale=3):
832
+ gr.Markdown("## πŸ’¬ AI Chat & Code Generation")
833
+ 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"))
834
+ 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")
835
+ status_output = gr.Textbox(label="Chat/Process Status", interactive=False, lines=1, value="Ready.")
836
+ gr.Markdown("---")
837
+ with gr.Tabs():
838
+ 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")
839
+ with gr.TabItem("πŸ” Detected Files Preview"):
840
+ detected_files_preview = gr.Markdown(value="*Files preview...*")
841
+
842
+ gr.Markdown("---")
843
+ with gr.Tabs():
844
+ with gr.TabItem("πŸš€ Build & Preview Space"):
845
+ 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)
846
+ 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)
847
+ with gr.TabItem("✏️ Edit Space Files"):
848
+ gr.Markdown("Select a file to view, edit, or delete. Changes are committed to HF Hub.")
849
+ file_browser_dropdown = gr.Dropdown(label="Select File in Space", choices=[], interactive=True, visible=False, info="Load/build Space first.")
850
+ file_content_editor = gr.Code(label="File Content Editor", language="python", lines=15, interactive=True)
851
+ commit_message_input = gr.Textbox(label="Commit Message", placeholder="e.g., Updated app.py", value="Update via AI Space Editor")
852
+ 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)
853
+ edit_status_display = gr.Textbox(label="File Edit/Delete Status", interactive=False, lines=2, value="*Select file...*")
854
+
855
+ provider_select.change(
856
+ fn=update_models_dropdown,
857
+ inputs=provider_select,
858
+ outputs=model_select
859
+ )
860
+
861
+ chat_outputs = [chat_message_input, chatbot_display, status_output, detected_files_preview, formatted_space_output_display, download_button]
862
+ 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]
863
+
864
+ send_chat_button.click(
865
+ fn=handle_chat_submit,
866
+ inputs=chat_inputs,
867
+ outputs=chat_outputs
868
+ )
869
+ chat_message_input.submit(
870
+ fn=handle_chat_submit,
871
+ inputs=chat_inputs,
872
+ outputs=chat_outputs
873
+ )
874
+
875
+ 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]
876
+ load_space_button.click(fn=handle_load_existing_space, inputs=[hf_api_key_input, owner_name_input, space_name_input], outputs=load_space_outputs)
877
+
878
+ build_outputs = [build_status_display, space_iframe_display, file_browser_dropdown, owner_name_input, space_name_input]
879
+ 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)
880
+
881
+ file_edit_load_outputs = [file_content_editor, edit_status_display, commit_message_input, file_content_editor]
882
+ 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)
883
+
884
+ commit_file_outputs = [edit_status_display, file_browser_dropdown, formatted_space_output_display, detected_files_preview, download_button]
885
+ 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)
886
+
887
+ 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]
888
+ 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)
889
+
890
+ 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])
891
 
892
  if __name__ == "__main__":
893
+ demo.launch(debug=True, share=False)