Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -93,18 +93,25 @@ def _clean_filename(filename_line_content):
|
|
93 |
path_match = re.match(r'^([\w\-\.\s\/\\]+)', text) # Adjusted to be more general
|
94 |
if path_match:
|
95 |
# Further clean if it looks like "path/to/file (description)"
|
96 |
-
|
|
|
|
|
97 |
|
98 |
# Fallback for more complex lines, like "### File: `src/app.py` (main application)"
|
99 |
backtick_match = re.search(r'`([^`]+)`', text)
|
100 |
if backtick_match:
|
101 |
potential_fn = backtick_match.group(1).strip()
|
102 |
-
|
|
|
|
|
|
|
103 |
if cleaned_fn: return cleaned_fn
|
104 |
|
105 |
-
|
|
|
|
|
106 |
filename_candidate = filename_candidate.strip('`\'":;,') # Clean common wrapping chars
|
107 |
-
return filename_candidate if filename_candidate else text
|
108 |
|
109 |
|
110 |
def _parse_chat_stream_logic(chat_json_string, existing_files_state=None):
|
@@ -120,27 +127,57 @@ def _parse_chat_stream_logic(chat_json_string, existing_files_state=None):
|
|
120 |
except json.JSONDecodeError as e: results["error_message"] = f"JSON Parsing Error: {e}."; return results
|
121 |
except ValueError as e: results["error_message"] = str(e); return results
|
122 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
123 |
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(?: - [^\]]+)?\]))")
|
124 |
structure_pattern = re.compile(r"## File Structure\n```(?:(?P<struct_lang>[\w.-]*)\n)?(?P<structure_code>[\s\S]*?)\n```")
|
125 |
|
126 |
-
for
|
127 |
-
|
128 |
-
|
129 |
-
if
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
144 |
|
145 |
current_parsed_blocks = list(latest_blocks_dict.values())
|
146 |
current_parsed_blocks.sort(key=lambda b: (0, b["filename"]) if b.get("is_structure_block") else (1, b["filename"]))
|
@@ -152,9 +189,15 @@ def _parse_chat_stream_logic(chat_json_string, existing_files_state=None):
|
|
152 |
def _export_selected_logic(selected_filenames, space_line_name_for_md, parsed_blocks_for_export):
|
153 |
results = {"output_str": "", "error_message": None, "download_filepath": None}
|
154 |
global parsed_code_blocks_state_cache
|
155 |
-
|
|
|
|
|
156 |
|
157 |
-
|
|
|
|
|
|
|
|
|
158 |
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.*"
|
159 |
try:
|
160 |
with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".md", encoding='utf-8') as tmpfile:
|
@@ -162,27 +205,43 @@ def _export_selected_logic(selected_filenames, space_line_name_for_md, parsed_bl
|
|
162 |
except Exception as e: print(f"Error creating temp file for empty export: {e}")
|
163 |
return results
|
164 |
|
165 |
-
output_lines = [f"# Space: {space_line_name_for_md}"
|
166 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
167 |
if selected_filenames:
|
168 |
-
|
169 |
else:
|
170 |
-
|
171 |
|
172 |
-
|
173 |
-
|
174 |
-
|
175 |
-
for block in
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
elif not exported_content and filenames_for_structure: output_lines.append("*Selected files have no content blocks defined by AI or are binary.*")
|
186 |
final_output_str = "\n".join(output_lines)
|
187 |
results["output_str"] = final_output_str
|
188 |
try:
|
@@ -195,20 +254,30 @@ def _convert_gr_history_to_api_messages(system_prompt, gr_history, current_user_
|
|
195 |
messages = [{"role": "system", "content": system_prompt}] if system_prompt else []
|
196 |
for user_msg, bot_msg in gr_history:
|
197 |
if user_msg: messages.append({"role": "user", "content": user_msg})
|
198 |
-
|
|
|
199 |
if current_user_message: messages.append({"role": "user", "content": current_user_message})
|
200 |
return messages
|
201 |
|
202 |
def get_latest_bot_message_as_json(gr_history):
|
203 |
-
|
204 |
-
|
205 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
206 |
|
207 |
|
208 |
def _generate_ui_outputs_from_cache(owner, space_name):
|
209 |
global parsed_code_blocks_state_cache
|
210 |
preview_md_val = "*No files in cache to display.*"
|
211 |
-
formatted_md_val = f"# Space: {owner}/{space_name}\n## File Structure\n{bbb}\nπ Root\n{bbb}\n\n*No files in cache.*"
|
212 |
download_file = None
|
213 |
|
214 |
if parsed_code_blocks_state_cache:
|
@@ -217,211 +286,473 @@ def _generate_ui_outputs_from_cache(owner, space_name):
|
|
217 |
preview_md_lines.append(f"\n----\n**File:** `{escape_html_for_markdown(block['filename'])}`")
|
218 |
if block.get('is_structure_block'): preview_md_lines.append(f" (Original File Structure from AI)\n")
|
219 |
elif block.get('is_binary'): preview_md_lines.append(f" (Binary File)\n")
|
220 |
-
elif block.get('language'): preview_md_lines.append(f" (Language: `{block['language']}`)\n")
|
221 |
else: preview_md_lines.append("\n")
|
222 |
-
|
223 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
224 |
preview_md_val = "\n".join(preview_md_lines)
|
225 |
space_line_name = f"{owner}/{space_name}" if owner and space_name else (owner or space_name or "your-space")
|
226 |
-
|
227 |
-
|
|
|
228 |
formatted_md_val = export_result["output_str"]
|
229 |
download_file = export_result["download_filepath"]
|
230 |
-
|
231 |
-
preview_md_val = "No files loaded or detected yet."
|
232 |
-
formatted_md_val = f"# Space: {owner or 'owner'}/{space_name or 'space'}\n## File Structure\n{bbb}\nπ Root\n{bbb}\n\n*No files found or defined for this Space.*" if owner and space_name else "*Load or define a Space to see its Markdown structure.*"
|
233 |
return formatted_md_val, preview_md_val, gr.update(value=download_file, interactive=download_file is not None)
|
234 |
|
235 |
def handle_groq_chat_submit(user_message, chat_history, api_key_input, model_select, system_prompt, hf_owner_name, hf_repo_name, _current_formatted_markdown):
|
236 |
global parsed_code_blocks_state_cache
|
237 |
_chat_msg_in, _chat_hist, _status = "", list(chat_history), "Initializing..."
|
238 |
_detected_files_update, _formatted_output_update, _download_btn_update = gr.update(), gr.update(), gr.update(interactive=False, value=None)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
239 |
if not user_message.strip():
|
240 |
_status = "Cannot send an empty message."
|
241 |
yield (user_message, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update); return
|
242 |
_chat_hist.append((user_message, None)); _status = "Sending to AI..."
|
243 |
yield (_chat_msg_in, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update)
|
|
|
244 |
effective_api_key = api_key_input or os.environ.get("GROQ_API_KEY")
|
245 |
if not effective_api_key:
|
246 |
_chat_hist[-1] = (user_message, "Error: Groq API Key not set."); _status = "API Key missing."
|
247 |
yield (_chat_msg_in, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update); return
|
|
|
248 |
current_sys_prompt = system_prompt.strip() or DEFAULT_SYSTEM_PROMPT
|
249 |
-
|
250 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
251 |
try:
|
252 |
_status = f"Waiting for {model_select}..."; yield (_chat_msg_in, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update)
|
253 |
-
response = requests.post(GROQ_API_ENDPOINT, headers=headers, json=payload, timeout=180)
|
|
|
254 |
api_resp_json = response.json()
|
255 |
-
|
256 |
bot_response_actual = None
|
257 |
-
|
258 |
-
|
259 |
-
|
260 |
-
|
|
|
|
|
|
|
|
|
261 |
|
262 |
if bot_response_actual:
|
|
|
263 |
_chat_hist[-1] = (user_message, bot_response_actual); _status = "AI response received. Processing files..."
|
264 |
yield (_chat_msg_in, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update)
|
265 |
-
|
|
|
|
|
|
|
266 |
parsing_res = _parse_chat_stream_logic(latest_bot_message_json, existing_files_state=parsed_code_blocks_state_cache)
|
|
|
267 |
if parsing_res["error_message"]:
|
268 |
_status = f"Parsing Error: {parsing_res['error_message']}"
|
|
|
|
|
269 |
_detected_files_update = gr.Markdown(f"## Parsing Error\n`{escape_html_for_markdown(parsing_res['error_message'])}`")
|
270 |
else:
|
|
|
271 |
_formatted_output_update, _detected_files_update, _download_btn_update = _generate_ui_outputs_from_cache(hf_owner_name, hf_repo_name)
|
272 |
_status = "Processing complete. Previews updated."
|
|
|
273 |
yield (_chat_msg_in, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update); return
|
274 |
else:
|
|
|
275 |
error_msg = f"API Error: Unexpected response structure or empty message content. Response: {json.dumps(api_resp_json)}"
|
276 |
-
|
277 |
-
|
278 |
except requests.exceptions.HTTPError as e: error_msg = f"API HTTP Error: {e} - {e.response.text if e.response else 'No details'}"
|
279 |
except requests.exceptions.RequestException as e: error_msg = f"API Request Error: {e}"
|
280 |
except Exception as e: error_msg = f"Unexpected error in chat submit: {e}"
|
281 |
-
|
282 |
-
|
283 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
284 |
_status = error_msg
|
285 |
yield (_chat_msg_in, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update)
|
286 |
|
|
|
287 |
def handle_load_existing_space(hf_api_key_ui, ui_owner_name, ui_space_name):
|
288 |
global parsed_code_blocks_state_cache
|
289 |
_formatted_md_val, _detected_preview_val, _status_val = "*Loading files...*", "*Loading files...*", f"Loading Space: {ui_owner_name}/{ui_space_name}..."
|
290 |
_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)
|
291 |
_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.*"
|
|
|
|
|
292 |
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)
|
|
|
293 |
owner_to_use, updated_owner_name_val = ui_owner_name, ui_owner_name
|
|
|
|
|
294 |
if not owner_to_use:
|
295 |
token, token_err = build_logic_get_api_token(hf_api_key_ui)
|
296 |
-
if token_err or not token:
|
|
|
297 |
else:
|
298 |
try:
|
299 |
user_info = build_logic_whoami(token=token)
|
300 |
-
if user_info and 'name' in user_info:
|
301 |
-
|
302 |
-
|
303 |
-
|
|
|
|
|
|
|
304 |
if not owner_to_use or not ui_space_name:
|
305 |
-
_status_val = "Error: Owner and Space Name are required."
|
306 |
-
|
|
|
|
|
|
|
|
|
307 |
|
308 |
sdk_for_iframe, file_list, err_list_files = get_space_repository_info(hf_api_key_ui, ui_space_name, owner_to_use)
|
309 |
-
|
|
|
|
|
|
|
310 |
iframe_url = f"https://{sub_owner}-{sub_repo}{'.static.hf.space' if sdk_for_iframe == 'static' else '.hf.space'}"
|
311 |
_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)
|
|
|
|
|
312 |
if err_list_files and not file_list:
|
313 |
_status_val = f"File List Error: {err_list_files}"
|
314 |
-
|
|
|
|
|
|
|
|
|
|
|
315 |
if not file_list:
|
316 |
-
_status_val = f"Loaded Space: {owner_to_use}/{ui_space_name}. No files found ({err_list_files or '
|
317 |
parsed_code_blocks_state_cache = []
|
318 |
_formatted_md_val, _detected_preview_val, _download_btn_update = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name)
|
319 |
_file_browser_update = gr.update(visible=True, choices=[], value="No files found")
|
320 |
-
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)
|
|
|
|
|
321 |
|
322 |
loaded_files_for_parse = []
|
323 |
-
_status_val = f"Loading {len(file_list)} files from {owner_to_use}/{ui_space_name} (SDK: {sdk_for_iframe or 'unknown'})...";
|
|
|
|
|
|
|
|
|
324 |
for file_path in file_list:
|
|
|
|
|
325 |
_, ext = os.path.splitext(file_path)
|
326 |
-
if ext.lower() in [".png",".jpg",".jpeg",".gif",".ico",".svg",".pt",".bin",".safetensors",".onnx",".woff",".woff2",".ttf",".eot"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
327 |
loaded_files_for_parse.append({"filename": file_path, "code": "[Binary or Skipped file]", "language": "binary", "is_binary": True, "is_structure_block": False}); continue
|
328 |
-
|
329 |
-
|
330 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
331 |
parsed_code_blocks_state_cache = loaded_files_for_parse
|
332 |
_formatted_md_val, _detected_preview_val, _download_btn_update = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name)
|
333 |
_status_val = f"Successfully loaded Space: {owner_to_use}/{ui_space_name}. Markdown ready."
|
334 |
-
_file_browser_update = gr.update(visible=True, choices=sorted(file_list or []), value=None)
|
|
|
335 |
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)
|
336 |
|
|
|
337 |
def handle_build_space_button(hf_api_key_ui, ui_space_name_part, ui_owner_name_part, space_sdk_ui, formatted_markdown_content):
|
338 |
_build_status, _iframe_html, _file_browser_update = "Starting space build process...", gr.update(value=None, visible=False), gr.update(visible=False, choices=[], value=None)
|
339 |
-
yield _build_status, _iframe_html, _file_browser_update
|
340 |
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; return
|
341 |
final_owner_for_build = ui_owner_name_part
|
342 |
if not final_owner_for_build:
|
343 |
-
token_for_whoami,
|
|
|
344 |
if token_for_whoami:
|
345 |
-
try:
|
346 |
-
|
347 |
-
|
348 |
-
|
|
|
|
|
|
|
|
|
|
|
349 |
result_message = build_logic_create_space(hf_api_key_ui, ui_space_name_part, final_owner_for_build, space_sdk_ui, formatted_markdown_content)
|
350 |
_build_status = f"Build Process: {result_message}"
|
|
|
351 |
if "Successfully" in result_message:
|
352 |
-
sub_owner
|
|
|
353 |
iframe_url = f"https://{sub_owner}-{sub_repo}{'.static.hf.space' if space_sdk_ui == 'static' else '.hf.space'}"
|
354 |
_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)
|
355 |
-
_build_status += f"\nSpace live at: {iframe_url} (Repo: https://huggingface.co/spaces/{final_owner_for_build}/{ui_space_name_part})"
|
|
|
|
|
356 |
file_list, err_list = list_space_files_for_browsing(hf_api_key_ui, ui_space_name_part, final_owner_for_build)
|
357 |
if err_list: _build_status += f"\nFile list refresh error after build: {err_list}"; _file_browser_update = gr.update(visible=True, choices=[], value="Error refreshing files")
|
358 |
else: _file_browser_update = gr.update(visible=True, choices=sorted(file_list or []), value=None if file_list else "No files found")
|
|
|
359 |
yield _build_status, _iframe_html, _file_browser_update
|
360 |
|
|
|
361 |
def handle_load_file_for_editing(hf_api_key_ui, ui_space_name_part, ui_owner_name_part, selected_file_path):
|
362 |
-
_file_content_val, _edit_status_val, _commit_msg_val, _lang_update = "", "Error: No file selected.", gr.update(), gr.update()
|
363 |
-
if not selected_file_path or selected_file_path in ["No files found", "Error
|
|
|
|
|
|
|
364 |
owner_to_use = ui_owner_name_part
|
365 |
if not owner_to_use:
|
366 |
-
token,
|
|
|
367 |
if token:
|
368 |
-
try:
|
369 |
-
|
|
|
|
|
370 |
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
|
|
|
371 |
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
|
|
|
|
|
|
|
|
|
372 |
content, err = get_space_file_content(hf_api_key_ui, ui_space_name_part, owner_to_use, selected_file_path)
|
373 |
-
|
374 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
375 |
yield _file_content_val, _edit_status_val, _commit_msg_val, _lang_update
|
376 |
|
377 |
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):
|
378 |
global parsed_code_blocks_state_cache
|
379 |
-
_edit_status_val
|
380 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
381 |
owner_to_use = ui_owner_name_part
|
382 |
if not owner_to_use:
|
383 |
-
token,
|
|
|
384 |
if token:
|
385 |
-
try:
|
386 |
-
|
387 |
-
|
388 |
-
|
|
|
|
|
|
|
|
|
389 |
status_msg = update_space_file(hf_api_key_ui, ui_space_name_part, owner_to_use, file_to_edit_path, edited_content, commit_message)
|
390 |
_edit_status_val = status_msg
|
|
|
391 |
if "Successfully updated" in status_msg:
|
|
|
392 |
found_in_cache = False
|
393 |
for block in parsed_code_blocks_state_cache:
|
394 |
-
if block["filename"] == file_to_edit_path:
|
395 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
396 |
_formatted_md_out, _detected_preview_out, _download_btn_out = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name_part)
|
397 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
398 |
yield _edit_status_val, _file_browser_update_val, _formatted_md_out, _detected_preview_out, _download_btn_out
|
399 |
|
|
|
400 |
def handle_delete_file(hf_api_key_ui, ui_space_name_part, ui_owner_name_part, file_to_delete_path):
|
401 |
global parsed_code_blocks_state_cache
|
402 |
-
_edit_status_val
|
|
|
|
|
|
|
|
|
|
|
403 |
_formatted_md_out, _detected_preview_out, _download_btn_out = gr.update(), gr.update(), gr.update()
|
404 |
-
|
405 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
406 |
owner_to_use = ui_owner_name_part
|
407 |
if not owner_to_use:
|
408 |
-
token,
|
|
|
409 |
if token:
|
410 |
-
try:
|
411 |
-
|
412 |
-
|
413 |
-
|
|
|
|
|
|
|
|
|
|
|
414 |
deletion_status_msg = build_logic_delete_space_file(hf_api_key_ui, ui_space_name_part, owner_to_use, file_to_delete_path)
|
415 |
_edit_status_val = deletion_status_msg
|
|
|
416 |
if "Successfully deleted" in deletion_status_msg:
|
|
|
417 |
parsed_code_blocks_state_cache = [b for b in parsed_code_blocks_state_cache if b["filename"] != file_to_delete_path]
|
|
|
|
|
418 |
_formatted_md_out, _detected_preview_out, _download_btn_out = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name_part)
|
|
|
|
|
419 |
new_file_list, err_list = list_space_files_for_browsing(hf_api_key_ui, ui_space_name_part, owner_to_use)
|
420 |
-
if err_list:
|
421 |
-
|
422 |
-
|
423 |
-
|
424 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
425 |
|
426 |
def handle_refresh_space_status(hf_api_key_ui, ui_owner_name, ui_space_name):
|
427 |
yield "*Fetching space status...*" # Initial feedback
|
@@ -433,25 +764,29 @@ def handle_refresh_space_status(hf_api_key_ui, ui_owner_name, ui_space_name):
|
|
433 |
except Exception as e: yield f"**Error auto-detecting owner:** {e}"; return
|
434 |
if not owner_to_use or not ui_space_name: yield "**Error:** Owner and Space Name are required."; return
|
435 |
status_details, error_msg = get_space_runtime_status(hf_api_key_ui, ui_space_name, owner_to_use)
|
436 |
-
if error_msg: _status_display_md = f"**Error fetching status for {owner_to_use}/{ui_space_name}:**\n\n`{error_msg}`"
|
437 |
elif status_details:
|
438 |
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','#')
|
439 |
md_lines = [f"### Space Status: {owner_to_use}/{ui_space_name}", f"- **Stage:** `{stage}`", f"- **Current Hardware:** `{hardware}`"]
|
440 |
if status_details.get('requested_hardware') and status_details.get('requested_hardware') != hardware: md_lines.append(f"- **Requested Hardware:** `{status_details.get('requested_hardware')}`")
|
441 |
if error: md_lines.append(f"- **Error:** <span style='color:red;'>`{escape_html_for_markdown(error)}`</span>")
|
442 |
md_lines.append(f"- [View Full Logs on Hugging Face]({log_link})")
|
443 |
-
|
|
|
|
|
|
|
444 |
_status_display_md = "\n".join(md_lines)
|
445 |
else: _status_display_md = "Could not retrieve status details."
|
446 |
yield _status_display_md
|
447 |
|
|
|
448 |
with gr.Blocks(theme=gr.themes.Soft(primary_hue="teal", secondary_hue="orange")) as demo:
|
449 |
gr.Markdown("# π€ AI Code & Space Generator")
|
450 |
gr.Markdown("Configure settings, chat with AI to generate/modify Hugging Face Spaces, then build, preview, and edit.")
|
451 |
with gr.Row():
|
452 |
with gr.Sidebar():
|
453 |
gr.Markdown("## βοΈ Configuration")
|
454 |
-
with gr.Group(): gr.Markdown("### API Keys & Tokens"); groq_api_key_input = gr.Textbox(label="Groq API Key", type="password", placeholder="gsk_..."); hf_api_key_input = gr.Textbox(label="Hugging Face Token", type="password", placeholder="hf_...")
|
455 |
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")
|
456 |
with gr.Group(): gr.Markdown("### AI Model Settings"); groq_model_select = gr.Dropdown(label="Groq Model", choices=["mixtral-8x7b-32768", "llama3-8b-8192", "llama3-70b-8192", "gemma-7b-it"], value="llama3-8b-8192"); groq_system_prompt_input = gr.Textbox(label="System Prompt", lines=8, value=DEFAULT_SYSTEM_PROMPT, interactive=True)
|
457 |
with gr.Column(scale=3):
|
@@ -462,12 +797,12 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="teal", secondary_hue="orange"))
|
|
462 |
gr.Markdown("---")
|
463 |
with gr.Tabs():
|
464 |
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")
|
465 |
-
with gr.TabItem("π Detected Files Preview"): gr.Markdown(
|
466 |
gr.Markdown("---")
|
467 |
with gr.Tabs():
|
468 |
with gr.TabItem("π Build & Preview Space"):
|
469 |
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)
|
470 |
-
build_status_display = gr.Textbox(label="Build Operation Status", interactive=False, lines=2, value="*Build status
|
471 |
with gr.TabItem("βοΈ Edit Space Files"):
|
472 |
gr.Markdown("Select a file to view, edit, or delete. Changes are committed to HF Hub.")
|
473 |
file_browser_dropdown = gr.Dropdown(label="Select File in Space", choices=[], interactive=True, visible=False, info="Load/build Space first.")
|
@@ -477,19 +812,29 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="teal", secondary_hue="orange"))
|
|
477 |
edit_status_display = gr.Textbox(label="File Edit/Delete Status", interactive=False, lines=2, value="*Select file...*")
|
478 |
|
479 |
chat_outputs = [groq_chat_message_input, groq_chatbot_display, groq_status_output, detected_files_preview, formatted_space_output_display, download_button]
|
480 |
-
chat_inputs = [groq_chat_message_input, groq_chatbot_display, groq_api_key_input, groq_model_select, groq_system_prompt_input, owner_name_input, space_name_input, formatted_space_output_display]
|
481 |
groq_send_chat_button.click(fn=handle_groq_chat_submit, inputs=chat_inputs, outputs=chat_outputs)
|
482 |
groq_chat_message_input.submit(fn=handle_groq_chat_submit, inputs=chat_inputs, outputs=chat_outputs)
|
|
|
|
|
483 |
load_space_outputs = [formatted_space_output_display, detected_files_preview, groq_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]
|
484 |
load_space_button.click(fn=handle_load_existing_space, inputs=[hf_api_key_input, owner_name_input, space_name_input], outputs=load_space_outputs)
|
|
|
485 |
build_outputs = [build_status_display, space_iframe_display, file_browser_dropdown]
|
486 |
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)
|
|
|
|
|
487 |
file_edit_load_outputs = [file_content_editor, edit_status_display, commit_message_input, file_content_editor]
|
488 |
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)
|
|
|
|
|
489 |
commit_file_outputs = [edit_status_display, file_browser_dropdown, formatted_space_output_display, detected_files_preview, download_button]
|
490 |
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)
|
491 |
-
|
|
|
|
|
492 |
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)
|
|
|
493 |
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])
|
494 |
|
495 |
if __name__ == "__main__":
|
|
|
93 |
path_match = re.match(r'^([\w\-\.\s\/\\]+)', text) # Adjusted to be more general
|
94 |
if path_match:
|
95 |
# Further clean if it looks like "path/to/file (description)"
|
96 |
+
# Corrected split index was already correct in the original code, just ensure it's applied
|
97 |
+
parts = re.split(r'\s*\(', path_match.group(1).strip(), 1)
|
98 |
+
return parts[0].strip() if parts else ""
|
99 |
|
100 |
# Fallback for more complex lines, like "### File: `src/app.py` (main application)"
|
101 |
backtick_match = re.search(r'`([^`]+)`', text)
|
102 |
if backtick_match:
|
103 |
potential_fn = backtick_match.group(1).strip()
|
104 |
+
# Corrected split index was already correct
|
105 |
+
parts = re.split(r'\s*\(|\s{2,}', potential_fn, 1)
|
106 |
+
cleaned_fn = parts[0].strip() if parts else ""
|
107 |
+
cleaned_fn = cleaned_fn.strip('`\'":;,') # Clean common wrapping chars
|
108 |
if cleaned_fn: return cleaned_fn
|
109 |
|
110 |
+
# Final fallback
|
111 |
+
parts = re.split(r'\s*\(|\s{2,}', text, 1)
|
112 |
+
filename_candidate = parts[0].strip() if parts else text.strip()
|
113 |
filename_candidate = filename_candidate.strip('`\'":;,') # Clean common wrapping chars
|
114 |
+
return filename_candidate if filename_candidate else text.strip()
|
115 |
|
116 |
|
117 |
def _parse_chat_stream_logic(chat_json_string, existing_files_state=None):
|
|
|
127 |
except json.JSONDecodeError as e: results["error_message"] = f"JSON Parsing Error: {e}."; return results
|
128 |
except ValueError as e: results["error_message"] = str(e); return results
|
129 |
|
130 |
+
# Ensure the bot's message is the last one and is the only one being parsed for new/updated files
|
131 |
+
# This prevents reprocessing the entire history on every new AI turn
|
132 |
+
message_obj = None
|
133 |
+
if ai_chat_history and isinstance(ai_chat_history[-1], dict) and ai_chat_history[-1].get("role", "").lower() == BOT_ROLE_NAME:
|
134 |
+
message_obj = ai_chat_history[-1]
|
135 |
+
|
136 |
+
if not message_obj:
|
137 |
+
# If the last message isn't the bot's or is malformed, don't update state based on it
|
138 |
+
results["parsed_code_blocks"] = list(latest_blocks_dict.values()) # Return existing state
|
139 |
+
results["default_selected_filenames"] = [b["filename"] for b in results["parsed_code_blocks"] if not b.get("is_structure_block")]
|
140 |
+
# results["error_message"] = "No bot message found in the last entry for parsing." # Optional: add a debug message
|
141 |
+
return results
|
142 |
+
|
143 |
+
|
144 |
+
role, content = message_obj.get("role", "").lower(), message_obj.get("content", "")
|
145 |
+
|
146 |
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(?: - [^\]]+)?\]))")
|
147 |
structure_pattern = re.compile(r"## File Structure\n```(?:(?P<struct_lang>[\w.-]*)\n)?(?P<structure_code>[\s\S]*?)\n```")
|
148 |
|
149 |
+
# Process only the *last* bot message for updates to file blocks
|
150 |
+
if role == BOT_ROLE_NAME:
|
151 |
+
structure_match = structure_pattern.search(content)
|
152 |
+
if structure_match:
|
153 |
+
# Overwrite or add the structure block from the latest response
|
154 |
+
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}
|
155 |
+
|
156 |
+
# Find all file blocks in the latest message
|
157 |
+
current_message_file_blocks = {}
|
158 |
+
for match in file_pattern.finditer(content):
|
159 |
+
filename = _clean_filename(match.group("filename_line"))
|
160 |
+
if not filename: continue
|
161 |
+
lang, code_block, binary_msg = match.group("lang"), match.group("code"), match.group("binary_msg")
|
162 |
+
item_data = {"filename": filename, "is_binary": False, "is_structure_block": False}
|
163 |
+
if code_block is not None:
|
164 |
+
item_data["code"], item_data["language"] = code_block.strip(), (lang.strip().lower() if lang else _infer_lang_from_filename(filename))
|
165 |
+
elif binary_msg is not None:
|
166 |
+
item_data["code"], item_data["language"], item_data["is_binary"] = binary_msg.strip(), "binary", True
|
167 |
+
else: continue
|
168 |
+
current_message_file_blocks[filename] = item_data
|
169 |
+
|
170 |
+
# Update latest_blocks_dict with blocks from the current message
|
171 |
+
# Any file mentioned in the latest message replaces its old version
|
172 |
+
# Any file *not* mentioned in the latest message but present previously is kept
|
173 |
+
# If the AI response implies deletion (e.g., by omitting a file it previously listed),
|
174 |
+
# the user needs to signal this intention in a way the prompt covers (e.g., "omit it from your next full ### File: list").
|
175 |
+
# Our current logic keeps files unless explicitly removed from the *entire* chat history's final list of blocks.
|
176 |
+
# A more sophisticated approach might look at AI language indicating deletion, but simple omission
|
177 |
+
# in a single turn's output isn't a reliable delete signal with complex interaction.
|
178 |
+
# For this implementation, we'll just update based on the latest message's defined blocks.
|
179 |
+
latest_blocks_dict.update(current_message_file_blocks)
|
180 |
+
|
181 |
|
182 |
current_parsed_blocks = list(latest_blocks_dict.values())
|
183 |
current_parsed_blocks.sort(key=lambda b: (0, b["filename"]) if b.get("is_structure_block") else (1, b["filename"]))
|
|
|
189 |
def _export_selected_logic(selected_filenames, space_line_name_for_md, parsed_blocks_for_export):
|
190 |
results = {"output_str": "", "error_message": None, "download_filepath": None}
|
191 |
global parsed_code_blocks_state_cache
|
192 |
+
# Use parsed_blocks_for_export if provided, otherwise use cache
|
193 |
+
if parsed_blocks_for_export is None:
|
194 |
+
parsed_blocks_for_export = parsed_code_blocks_state_cache
|
195 |
|
196 |
+
# Filter out structure blocks and binary/error files for file listing/export
|
197 |
+
exportable_blocks = [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]"))]
|
198 |
+
binary_blocks = [b for b in parsed_blocks_for_export if b.get("is_binary") or b.get("code", "").startswith("[Binary or Skipped file]")]
|
199 |
+
|
200 |
+
if not exportable_blocks and not binary_blocks and not any(b.get("is_structure_block") for b in parsed_blocks_for_export):
|
201 |
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.*"
|
202 |
try:
|
203 |
with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".md", encoding='utf-8') as tmpfile:
|
|
|
205 |
except Exception as e: print(f"Error creating temp file for empty export: {e}")
|
206 |
return results
|
207 |
|
208 |
+
output_lines = [f"# Space: {space_line_name_for_md}"]
|
209 |
+
|
210 |
+
# Add File Structure block if it exists in parsed blocks
|
211 |
+
structure_block = next((b for b in parsed_blocks_for_export if b.get("is_structure_block")), None)
|
212 |
+
if structure_block:
|
213 |
+
output_lines.extend(["## File Structure", bbb, structure_block["code"].strip(), bbb, ""])
|
214 |
+
else:
|
215 |
+
# If no structure block from AI, generate a simple one from detected files
|
216 |
+
output_lines.extend(["## File Structure", bbb, "π Root"])
|
217 |
+
filenames_for_structure_list = sorted(list(set(b["filename"] for b in exportable_blocks + binary_blocks))) # Include binary files in structure list
|
218 |
+
if filenames_for_structure_list:
|
219 |
+
for fname in filenames_for_structure_list: output_lines.append(f" π {fname}")
|
220 |
+
output_lines.extend([bbb, ""])
|
221 |
+
|
222 |
+
output_lines.append("Below are the contents of all files in the space:\n")
|
223 |
+
exported_content = False
|
224 |
+
|
225 |
+
# Determine which files to export content for based on selection or default
|
226 |
+
files_to_export_content = []
|
227 |
if selected_filenames:
|
228 |
+
files_to_export_content = [b for b in parsed_blocks_for_export if b["filename"] in selected_filenames and not b.get("is_structure_block")]
|
229 |
else:
|
230 |
+
files_to_export_content = [b for b in parsed_blocks_for_export if not b.get("is_structure_block")]
|
231 |
|
232 |
+
# Sort blocks for consistent output order
|
233 |
+
files_to_export_content.sort(key=lambda b: (0, b["filename"]) if b.get("is_binary") else (1, b["filename"]))
|
234 |
+
|
235 |
+
for block in files_to_export_content:
|
236 |
+
output_lines.append(f"### File: {block['filename']}")
|
237 |
+
if block.get('is_binary') or block.get("code", "").startswith("["): # Treat errors/skipped as binary for output format
|
238 |
+
output_lines.append(block.get('code',''))
|
239 |
+
else:
|
240 |
+
output_lines.extend([f"{bbb}{block.get('language', 'plaintext') or 'plaintext'}", block.get('code',''), bbb])
|
241 |
+
output_lines.append(""); exported_content = True
|
242 |
+
|
243 |
+
if not exported_content and not filenames_for_structure_list: output_lines.append("*No file content selected or available for export.*")
|
244 |
+
elif not exported_content and filenames_for_structure_list: output_lines.append("*Selected files have no content blocks defined by AI, are binary, or encountered loading errors.*")
|
|
|
245 |
final_output_str = "\n".join(output_lines)
|
246 |
results["output_str"] = final_output_str
|
247 |
try:
|
|
|
254 |
messages = [{"role": "system", "content": system_prompt}] if system_prompt else []
|
255 |
for user_msg, bot_msg in gr_history:
|
256 |
if user_msg: messages.append({"role": "user", "content": user_msg})
|
257 |
+
# Ensure bot_msg is not None or empty before adding
|
258 |
+
if bot_msg and isinstance(bot_msg, str): messages.append({"role": BOT_ROLE_NAME, "content": bot_msg})
|
259 |
if current_user_message: messages.append({"role": "user", "content": current_user_message})
|
260 |
return messages
|
261 |
|
262 |
def get_latest_bot_message_as_json(gr_history):
|
263 |
+
# Find the last non-empty bot message
|
264 |
+
latest_bot_msg_content = None
|
265 |
+
for user_msg, bot_msg in reversed(gr_history):
|
266 |
+
if bot_msg and isinstance(bot_msg, str):
|
267 |
+
latest_bot_msg_content = bot_msg
|
268 |
+
break # Found the latest, stop searching
|
269 |
+
|
270 |
+
if latest_bot_msg_content is None:
|
271 |
+
return json.dumps([]) # Return empty list if no bot message found
|
272 |
+
|
273 |
+
# Return JSON containing only the latest bot message for parsing
|
274 |
+
return json.dumps([{"role": BOT_ROLE_NAME, "content": latest_bot_msg_content}], indent=2)
|
275 |
|
276 |
|
277 |
def _generate_ui_outputs_from_cache(owner, space_name):
|
278 |
global parsed_code_blocks_state_cache
|
279 |
preview_md_val = "*No files in cache to display.*"
|
280 |
+
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.*"
|
281 |
download_file = None
|
282 |
|
283 |
if parsed_code_blocks_state_cache:
|
|
|
286 |
preview_md_lines.append(f"\n----\n**File:** `{escape_html_for_markdown(block['filename'])}`")
|
287 |
if block.get('is_structure_block'): preview_md_lines.append(f" (Original File Structure from AI)\n")
|
288 |
elif block.get('is_binary'): preview_md_lines.append(f" (Binary File)\n")
|
289 |
+
elif block.get('language') and block.get('language') != 'binary': preview_md_lines.append(f" (Language: `{block['language']}`)\n")
|
290 |
else: preview_md_lines.append("\n")
|
291 |
+
|
292 |
+
# Handle content display
|
293 |
+
content = block.get('code', '')
|
294 |
+
if block.get('is_binary') or content.startswith("[") : # Treat errors/skipped as binary for preview display
|
295 |
+
preview_md_lines.append(f"\n`{escape_html_for_markdown(content)}`\n")
|
296 |
+
elif block.get('is_structure_block'):
|
297 |
+
preview_md_lines.append(f"\n{bbb}{block.get('language', 'plaintext') or 'plaintext'}\n{content}\n{bbb}\n")
|
298 |
+
else:
|
299 |
+
preview_md_lines.append(f"\n{bbb}{block.get('language', 'plaintext') or 'plaintext'}\n{content}\n{bbb}\n")
|
300 |
+
|
301 |
+
|
302 |
preview_md_val = "\n".join(preview_md_lines)
|
303 |
space_line_name = f"{owner}/{space_name}" if owner and space_name else (owner or space_name or "your-space")
|
304 |
+
|
305 |
+
# _export_selected_logic handles selecting which files to include in the export MD
|
306 |
+
export_result = _export_selected_logic(None, space_line_name, parsed_code_blocks_state_cache) # Passing None means export all non-structure/non-binary/non-error content
|
307 |
formatted_md_val = export_result["output_str"]
|
308 |
download_file = export_result["download_filepath"]
|
309 |
+
|
|
|
|
|
310 |
return formatted_md_val, preview_md_val, gr.update(value=download_file, interactive=download_file is not None)
|
311 |
|
312 |
def handle_groq_chat_submit(user_message, chat_history, api_key_input, model_select, system_prompt, hf_owner_name, hf_repo_name, _current_formatted_markdown):
|
313 |
global parsed_code_blocks_state_cache
|
314 |
_chat_msg_in, _chat_hist, _status = "", list(chat_history), "Initializing..."
|
315 |
_detected_files_update, _formatted_output_update, _download_btn_update = gr.update(), gr.update(), gr.update(interactive=False, value=None)
|
316 |
+
|
317 |
+
# --- Before sending to AI: Parse existing files from the current formatted markdown ---
|
318 |
+
# This ensures the AI is aware of the *current* state including user edits
|
319 |
+
# or files loaded from HF, before it generates its response.
|
320 |
+
# Only do this on a new user message
|
321 |
+
if user_message and _current_formatted_markdown:
|
322 |
+
try:
|
323 |
+
parsed_from_md = build_logic_parse_markdown(_current_formatted_markdown)
|
324 |
+
# Update cache only with files parsed from the markdown, preserving structure block if any
|
325 |
+
structure_block = next((b for b in parsed_code_blocks_state_cache if b.get("is_structure_block")), None)
|
326 |
+
parsed_code_blocks_state_cache = []
|
327 |
+
if structure_block:
|
328 |
+
parsed_code_blocks_state_cache.append(structure_block)
|
329 |
+
|
330 |
+
for f_info in parsed_from_md.get("files", []):
|
331 |
+
# Only add if it has content and isn't the structure block representation
|
332 |
+
if f_info.get("path") and f_info.get("path") != "File Structure (original)":
|
333 |
+
# Check if it's a binary representation string
|
334 |
+
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]"))
|
335 |
+
parsed_code_blocks_state_cache.append({
|
336 |
+
"filename": f_info["path"],
|
337 |
+
"code": f_info.get("content", ""),
|
338 |
+
"language": "binary" if is_binary_repr else _infer_lang_from_filename(f_info["path"]),
|
339 |
+
"is_binary": is_binary_repr,
|
340 |
+
"is_structure_block": False
|
341 |
+
})
|
342 |
+
parsed_code_blocks_state_cache.sort(key=lambda b: (0, b["filename"]) if b.get("is_structure_block") else (1, b["filename"]))
|
343 |
+
# print(f"Parsed {len(parsed_code_blocks_state_cache)} blocks from current Markdown.")
|
344 |
+
# print(parsed_code_blocks_state_cache)
|
345 |
+
|
346 |
+
except Exception as e:
|
347 |
+
# Log error but don't block chat submission
|
348 |
+
print(f"Error parsing formatted markdown before chat submit: {e}")
|
349 |
+
# Optionally update status: _status = f"Warning: Error parsing current files: {e}. Sending message anyway."
|
350 |
+
|
351 |
+
# --- End of pre-chat parsing ---
|
352 |
+
|
353 |
+
|
354 |
if not user_message.strip():
|
355 |
_status = "Cannot send an empty message."
|
356 |
yield (user_message, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update); return
|
357 |
_chat_hist.append((user_message, None)); _status = "Sending to AI..."
|
358 |
yield (_chat_msg_in, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update)
|
359 |
+
|
360 |
effective_api_key = api_key_input or os.environ.get("GROQ_API_KEY")
|
361 |
if not effective_api_key:
|
362 |
_chat_hist[-1] = (user_message, "Error: Groq API Key not set."); _status = "API Key missing."
|
363 |
yield (_chat_msg_in, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update); return
|
364 |
+
|
365 |
current_sys_prompt = system_prompt.strip() or DEFAULT_SYSTEM_PROMPT
|
366 |
+
|
367 |
+
# Include current file contents in the prompt as context
|
368 |
+
current_files_context = ""
|
369 |
+
if parsed_code_blocks_state_cache:
|
370 |
+
current_files_context = "\n\n## Current Files in Space\n"
|
371 |
+
for block in parsed_code_blocks_state_cache:
|
372 |
+
if block.get("is_structure_block"):
|
373 |
+
current_files_context += f"### File: {block['filename']}\n{bbb}\n{block['code']}\n{bbb}\n"
|
374 |
+
else:
|
375 |
+
current_files_context += f"### File: {block['filename']}\n"
|
376 |
+
if block.get("is_binary"):
|
377 |
+
current_files_context += f"{block['code']}\n" # e.g. [Binary file...]
|
378 |
+
else:
|
379 |
+
current_files_context += f"{bbb}{block.get('language', 'plaintext') or 'plaintext'}\n{block.get('code','')}\n{bbb}\n"
|
380 |
+
current_files_context += "\n"
|
381 |
+
|
382 |
+
# Append current file context to the user message
|
383 |
+
user_message_with_context = user_message.strip()
|
384 |
+
if current_files_context.strip():
|
385 |
+
user_message_with_context = user_message_with_context + current_files_context + "Based 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. Omit files you want to delete from your response."
|
386 |
+
|
387 |
+
|
388 |
+
api_msgs = _convert_gr_history_to_api_messages(current_sys_prompt, _chat_hist[:-1], user_message_with_context)
|
389 |
+
|
390 |
+
headers = {"Authorization": f"Bearer {effective_api_key}", "Content-Type": "application/json"}
|
391 |
+
payload = {"model": model_select, "messages": api_msgs}
|
392 |
+
|
393 |
try:
|
394 |
_status = f"Waiting for {model_select}..."; yield (_chat_msg_in, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update)
|
395 |
+
response = requests.post(GROQ_API_ENDPOINT, headers=headers, json=payload, timeout=180)
|
396 |
+
response.raise_for_status()
|
397 |
api_resp_json = response.json()
|
398 |
+
|
399 |
bot_response_actual = None
|
400 |
+
# --- Corrected logic to safely access the message content ---
|
401 |
+
if api_resp_json and isinstance(api_resp_json, dict) and api_resp_json.get("choices") and isinstance(api_resp_json["choices"], list) and len(api_resp_json["choices"]) > 0:
|
402 |
+
first_choice = api_resp_json["choices"][0] # Access the first item in the 'choices' list
|
403 |
+
if isinstance(first_choice, dict) and first_choice.get("message"): # Ensure it's a dict and has 'message' key
|
404 |
+
message_obj = first_choice["message"] # Get the 'message' dictionary from the choice
|
405 |
+
if isinstance(message_obj, dict): # Ensure message_obj is a dictionary
|
406 |
+
bot_response_actual = message_obj.get("content") # Get content from the message dictionary
|
407 |
+
# --- End corrected logic ---
|
408 |
|
409 |
if bot_response_actual:
|
410 |
+
# If bot_response_actual was successfully extracted
|
411 |
_chat_hist[-1] = (user_message, bot_response_actual); _status = "AI response received. Processing files..."
|
412 |
yield (_chat_msg_in, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update)
|
413 |
+
|
414 |
+
# Pass the *existing* state (which was updated from markdown at the start of the function)
|
415 |
+
# to the parsing logic, so it knows about files already present.
|
416 |
+
latest_bot_message_json = json.dumps([{"role": BOT_ROLE_NAME, "content": bot_response_actual}], indent=2)
|
417 |
parsing_res = _parse_chat_stream_logic(latest_bot_message_json, existing_files_state=parsed_code_blocks_state_cache)
|
418 |
+
|
419 |
if parsing_res["error_message"]:
|
420 |
_status = f"Parsing Error: {parsing_res['error_message']}"
|
421 |
+
# Append parsing error to the bot's response in chat? Or show separately?
|
422 |
+
# For now, update status and detected files area with error
|
423 |
_detected_files_update = gr.Markdown(f"## Parsing Error\n`{escape_html_for_markdown(parsing_res['error_message'])}`")
|
424 |
else:
|
425 |
+
# parsed_code_blocks_state_cache is updated inside _parse_chat_stream_logic
|
426 |
_formatted_output_update, _detected_files_update, _download_btn_update = _generate_ui_outputs_from_cache(hf_owner_name, hf_repo_name)
|
427 |
_status = "Processing complete. Previews updated."
|
428 |
+
|
429 |
yield (_chat_msg_in, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update); return
|
430 |
else:
|
431 |
+
# Handle cases where bot_response_actual is None (e.g., malformed API response)
|
432 |
error_msg = f"API Error: Unexpected response structure or empty message content. Response: {json.dumps(api_resp_json)}"
|
433 |
+
# This error_msg will be handled by the final error block below
|
434 |
+
|
435 |
except requests.exceptions.HTTPError as e: error_msg = f"API HTTP Error: {e} - {e.response.text if e.response else 'No details'}"
|
436 |
except requests.exceptions.RequestException as e: error_msg = f"API Request Error: {e}"
|
437 |
except Exception as e: error_msg = f"Unexpected error in chat submit: {e}"
|
438 |
+
|
439 |
+
# If an error occurred (and bot_response_actual was not set or an exception happened),
|
440 |
+
# update chat history with the error message.
|
441 |
+
# Assumes the last entry is the one for the current turn, added as (user_message, None).
|
442 |
+
# It's safer to append a new message if the last one isn't the one we expected to update.
|
443 |
+
if _chat_hist and len(_chat_hist) > 0 and _chat_hist[-1][1] is None:
|
444 |
+
_chat_hist[-1] = (_chat_hist[-1][0], error_msg) # Keep user message, add error as bot message
|
445 |
+
else:
|
446 |
+
_chat_hist.append((user_message, error_msg)) # Append as a new user/bot turn if structure unexpected
|
447 |
+
|
448 |
_status = error_msg
|
449 |
yield (_chat_msg_in, _chat_hist, _status, _detected_files_update, _formatted_output_update, _download_btn_update)
|
450 |
|
451 |
+
|
452 |
def handle_load_existing_space(hf_api_key_ui, ui_owner_name, ui_space_name):
|
453 |
global parsed_code_blocks_state_cache
|
454 |
_formatted_md_val, _detected_preview_val, _status_val = "*Loading files...*", "*Loading files...*", f"Loading Space: {ui_owner_name}/{ui_space_name}..."
|
455 |
_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)
|
456 |
_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.*"
|
457 |
+
|
458 |
+
# Yield initial state to update UI
|
459 |
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)
|
460 |
+
|
461 |
owner_to_use, updated_owner_name_val = ui_owner_name, ui_owner_name
|
462 |
+
error_occurred = False
|
463 |
+
|
464 |
if not owner_to_use:
|
465 |
token, token_err = build_logic_get_api_token(hf_api_key_ui)
|
466 |
+
if token_err or not token:
|
467 |
+
_status_val = f"Error: {token_err or 'Cannot determine owner from token.'}"; error_occurred = True
|
468 |
else:
|
469 |
try:
|
470 |
user_info = build_logic_whoami(token=token)
|
471 |
+
if user_info and 'name' in user_info:
|
472 |
+
owner_to_use, updated_owner_name_val = user_info['name'], user_info['name']; _status_val += f" (Auto-detected owner: {owner_to_use})"
|
473 |
+
else:
|
474 |
+
_status_val = "Error: Could not auto-detect owner from token."; error_occurred = True
|
475 |
+
except Exception as e:
|
476 |
+
_status_val = f"Error auto-detecting owner: {e}"; error_occurred = True
|
477 |
+
|
478 |
if not owner_to_use or not ui_space_name:
|
479 |
+
if not error_occurred: _status_val = "Error: Owner and Space Name are required."; error_occurred = True
|
480 |
+
|
481 |
+
if error_occurred:
|
482 |
+
# Yield error state
|
483 |
+
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)
|
484 |
+
return # Stop execution
|
485 |
|
486 |
sdk_for_iframe, file_list, err_list_files = get_space_repository_info(hf_api_key_ui, ui_space_name, owner_to_use)
|
487 |
+
|
488 |
+
# Construct iframe URL early, even if file listing fails
|
489 |
+
sub_owner = re.sub(r'[^a-z0-9\-]+', '-', owner_to_use.lower()).strip('-') or 'owner' # Fallback owner
|
490 |
+
sub_repo = re.sub(r'[^a-z0-9\-]+', '-', ui_space_name.lower()).strip('-') or 'space' # Fallback repo
|
491 |
iframe_url = f"https://{sub_owner}-{sub_repo}{'.static.hf.space' if sdk_for_iframe == 'static' else '.hf.space'}"
|
492 |
_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)
|
493 |
+
|
494 |
+
|
495 |
if err_list_files and not file_list:
|
496 |
_status_val = f"File List Error: {err_list_files}"
|
497 |
+
parsed_code_blocks_state_cache = [] # Clear cache on error
|
498 |
+
_formatted_md_val, _detected_preview_val, _download_btn_update = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name)
|
499 |
+
_file_browser_update = gr.update(visible=True, choices=[], value="Error loading files") # Update file browser with error state
|
500 |
+
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)
|
501 |
+
return # Stop execution
|
502 |
+
|
503 |
if not file_list:
|
504 |
+
_status_val = f"Loaded Space: {owner_to_use}/{ui_space_name}. No files found ({err_list_files or 'Repository is empty'})."
|
505 |
parsed_code_blocks_state_cache = []
|
506 |
_formatted_md_val, _detected_preview_val, _download_btn_update = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name)
|
507 |
_file_browser_update = gr.update(visible=True, choices=[], value="No files found")
|
508 |
+
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)
|
509 |
+
return # Stop execution
|
510 |
+
|
511 |
|
512 |
loaded_files_for_parse = []
|
513 |
+
_status_val = f"Loading {len(file_list)} files from {owner_to_use}/{ui_space_name} (SDK: {sdk_for_iframe or 'unknown'})...";
|
514 |
+
# Yield intermediate status while loading files
|
515 |
+
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)
|
516 |
+
|
517 |
+
|
518 |
for file_path in file_list:
|
519 |
+
# Skip files that are likely binary or not user-editable code/text
|
520 |
+
# Added more extensions and common non-code files like lock files
|
521 |
_, ext = os.path.splitext(file_path)
|
522 |
+
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 \
|
523 |
+
file_path.startswith(".git") or "/.git/" in file_path or \
|
524 |
+
file_path in ["requirements.txt", "environment.yml", "setup.py", "Pipfile", "pyproject.toml", "package.json", "yarn.lock", "pnpm-lock.yaml", "poetry.lock"] or \
|
525 |
+
file_path.endswith(".lock") or \
|
526 |
+
file_path.startswith("__pycache__/") or "/__pycache__/" in file_path or \
|
527 |
+
file_path.startswith("node_modules/") or "/node_modules/" in file_path or \
|
528 |
+
file_path.startswith("venv/") or "/venv/" in file_path or \
|
529 |
+
file_path.startswith(".venv/") or "/.venv/" in file_path:
|
530 |
loaded_files_for_parse.append({"filename": file_path, "code": "[Binary or Skipped file]", "language": "binary", "is_binary": True, "is_structure_block": False}); continue
|
531 |
+
|
532 |
+
# Handle potential issues with reading large files or non-utf8 files
|
533 |
+
try:
|
534 |
+
content, err_get = get_space_file_content(hf_api_key_ui, ui_space_name, owner_to_use, file_path)
|
535 |
+
if err_get:
|
536 |
+
# If there's an error getting content, record it but don't fail the whole load
|
537 |
+
loaded_files_for_parse.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})
|
538 |
+
print(f"Error loading {file_path}: {err_get}");
|
539 |
+
continue
|
540 |
+
# If content is successfully loaded
|
541 |
+
loaded_files_for_parse.append({"filename": file_path, "code": content, "language": _infer_lang_from_filename(file_path), "is_binary": False, "is_structure_block": False})
|
542 |
+
except Exception as content_ex:
|
543 |
+
# Catch any other unexpected exceptions during file content fetching
|
544 |
+
loaded_files_for_parse.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})
|
545 |
+
print(f"Unexpected error loading {file_path}: {content_ex}")
|
546 |
+
continue
|
547 |
+
|
548 |
+
|
549 |
parsed_code_blocks_state_cache = loaded_files_for_parse
|
550 |
_formatted_md_val, _detected_preview_val, _download_btn_update = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name)
|
551 |
_status_val = f"Successfully loaded Space: {owner_to_use}/{ui_space_name}. Markdown ready."
|
552 |
+
_file_browser_update = gr.update(visible=True, choices=sorted(file_list or []), value=None) # Use the full file list for the dropdown
|
553 |
+
|
554 |
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)
|
555 |
|
556 |
+
|
557 |
def handle_build_space_button(hf_api_key_ui, ui_space_name_part, ui_owner_name_part, space_sdk_ui, formatted_markdown_content):
|
558 |
_build_status, _iframe_html, _file_browser_update = "Starting space build process...", gr.update(value=None, visible=False), gr.update(visible=False, choices=[], value=None)
|
559 |
+
yield _build_status, _iframe_html, _file_browser_update # Yield initial status
|
560 |
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; return
|
561 |
final_owner_for_build = ui_owner_name_part
|
562 |
if not final_owner_for_build:
|
563 |
+
token_for_whoami, token_err = build_logic_get_api_token(hf_api_key_ui)
|
564 |
+
if token_err: _build_status = f"Build Error: {token_err}"; yield _build_status, _iframe_html, _file_browser_update; return
|
565 |
if token_for_whoami:
|
566 |
+
try:
|
567 |
+
user_info = build_logic_whoami(token=token_for_whoami)
|
568 |
+
final_owner_for_build = user_info['name'] if user_info and 'name' in user_info else final_owner_for_build
|
569 |
+
if not final_owner_for_build: _build_status += "\n(Warning: Could not auto-detect owner from token for build. Please specify.)"
|
570 |
+
except Exception as e: _build_status += f"\n(Warning: Could not auto-detect owner for build: {e}. Please specify.)"
|
571 |
+
else: _build_status += "\n(Warning: Owner not specified and no token to auto-detect for build. Please specify owner or provide a token.)"
|
572 |
+
|
573 |
+
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; return
|
574 |
+
|
575 |
result_message = build_logic_create_space(hf_api_key_ui, ui_space_name_part, final_owner_for_build, space_sdk_ui, formatted_markdown_content)
|
576 |
_build_status = f"Build Process: {result_message}"
|
577 |
+
|
578 |
if "Successfully" in result_message:
|
579 |
+
sub_owner = re.sub(r'[^a-z0-9\-]+', '-', final_owner_for_build.lower()).strip('-') or 'owner'
|
580 |
+
sub_repo = re.sub(r'[^a-z0-9\-]+', '-', ui_space_name_part.lower()).strip('-') or 'space'
|
581 |
iframe_url = f"https://{sub_owner}-{sub_repo}{'.static.hf.space' if space_sdk_ui == 'static' else '.hf.space'}"
|
582 |
_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)
|
583 |
+
_build_status += f"\nSpace live at: [Link]({iframe_url}) (Repo: https://huggingface.co/spaces/{final_owner_for_build}/{ui_space_name_part})"
|
584 |
+
|
585 |
+
# Refresh file list after successful build
|
586 |
file_list, err_list = list_space_files_for_browsing(hf_api_key_ui, ui_space_name_part, final_owner_for_build)
|
587 |
if err_list: _build_status += f"\nFile list refresh error after build: {err_list}"; _file_browser_update = gr.update(visible=True, choices=[], value="Error refreshing files")
|
588 |
else: _file_browser_update = gr.update(visible=True, choices=sorted(file_list or []), value=None if file_list else "No files found")
|
589 |
+
|
590 |
yield _build_status, _iframe_html, _file_browser_update
|
591 |
|
592 |
+
|
593 |
def handle_load_file_for_editing(hf_api_key_ui, ui_space_name_part, ui_owner_name_part, selected_file_path):
|
594 |
+
_file_content_val, _edit_status_val, _commit_msg_val, _lang_update = "", "Error: No file selected.", gr.update(value=""), gr.update(language="plaintext") # Reset values
|
595 |
+
if not selected_file_path or selected_file_path in ["No files found", "Error loading files", "Error refreshing files"]:
|
596 |
+
yield _file_content_val, "Select a file from the dropdown.", _commit_msg_val, _lang_update # Clear editor and status
|
597 |
+
return
|
598 |
+
|
599 |
owner_to_use = ui_owner_name_part
|
600 |
if not owner_to_use:
|
601 |
+
token, token_err = build_logic_get_api_token(hf_api_key_ui)
|
602 |
+
if token_err: _edit_status_val = f"Error: {token_err}"; yield (_file_content_val, _edit_status_val, _commit_msg_val, _lang_update); return
|
603 |
if token:
|
604 |
+
try:
|
605 |
+
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
|
606 |
+
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
|
607 |
+
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
|
608 |
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
|
609 |
+
|
610 |
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
|
611 |
+
|
612 |
+
_edit_status_val = f"Loading {selected_file_path}..."
|
613 |
+
yield gr.update(value=""), _edit_status_val, gr.update(value=""), gr.update(language="plaintext") # Yield loading state
|
614 |
+
|
615 |
content, err = get_space_file_content(hf_api_key_ui, ui_space_name_part, owner_to_use, selected_file_path)
|
616 |
+
|
617 |
+
if err:
|
618 |
+
_edit_status_val = f"Error loading '{selected_file_path}': {err}"
|
619 |
+
_commit_msg_val = f"Error loading {selected_file_path}"
|
620 |
+
_file_content_val = f"Error loading {selected_file_path}:\n{err}"
|
621 |
+
_lang_update = gr.update(language="plaintext")
|
622 |
+
yield _file_content_val, _edit_status_val, _commit_msg_val, _lang_update
|
623 |
+
return
|
624 |
+
|
625 |
+
_file_content_val = content or ""
|
626 |
+
_edit_status_val = f"Loaded {selected_file_path} for editing."
|
627 |
+
_commit_msg_val = f"Update {selected_file_path} via AI Space Editor"
|
628 |
+
_lang_update = gr.update(language=_infer_lang_from_filename(selected_file_path))
|
629 |
+
|
630 |
yield _file_content_val, _edit_status_val, _commit_msg_val, _lang_update
|
631 |
|
632 |
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):
|
633 |
global parsed_code_blocks_state_cache
|
634 |
+
_edit_status_val = "Processing commit..."
|
635 |
+
_file_browser_update_val = gr.update()
|
636 |
+
_formatted_md_out, _detected_preview_out, _download_btn_out = gr.update(), gr.update(), gr.update()
|
637 |
+
|
638 |
+
yield _edit_status_val, _file_browser_update_val, _formatted_md_out, _detected_preview_out, _download_btn_out # Yield initial status
|
639 |
+
|
640 |
+
if not file_to_edit_path or file_to_edit_path in ["No files found", "Error loading files", "Error refreshing files"]:
|
641 |
+
_edit_status_val = "Error: No valid file selected for commit.";
|
642 |
+
yield _edit_status_val, gr.update(), gr.update(), gr.update(), gr.update(); return
|
643 |
+
|
644 |
owner_to_use = ui_owner_name_part
|
645 |
if not owner_to_use:
|
646 |
+
token, token_err = build_logic_get_api_token(hf_api_key_ui)
|
647 |
+
if token_err: _edit_status_val = f"Error: {token_err}"; yield (_edit_status_val, gr.update(), gr.update(), gr.update(), gr.update()); return
|
648 |
if token:
|
649 |
+
try:
|
650 |
+
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
|
651 |
+
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
|
652 |
+
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
|
653 |
+
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
|
654 |
+
|
655 |
+
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
|
656 |
+
|
657 |
status_msg = update_space_file(hf_api_key_ui, ui_space_name_part, owner_to_use, file_to_edit_path, edited_content, commit_message)
|
658 |
_edit_status_val = status_msg
|
659 |
+
|
660 |
if "Successfully updated" in status_msg:
|
661 |
+
# Update the cache with the new content
|
662 |
found_in_cache = False
|
663 |
for block in parsed_code_blocks_state_cache:
|
664 |
+
if block["filename"] == file_to_edit_path:
|
665 |
+
block["code"] = edited_content
|
666 |
+
block["language"] = _infer_lang_from_filename(file_to_edit_path)
|
667 |
+
block["is_binary"] = False # Assume user edited text content
|
668 |
+
block["is_structure_block"] = False # Ensure it's not marked as structure
|
669 |
+
found_in_cache = True
|
670 |
+
break
|
671 |
+
if not found_in_cache:
|
672 |
+
# If file was added via editor or wasn't in initial load cache (e.g. binary/error), add it
|
673 |
+
parsed_code_blocks_state_cache.append({
|
674 |
+
"filename": file_to_edit_path,
|
675 |
+
"code": edited_content,
|
676 |
+
"language": _infer_lang_from_filename(file_to_edit_path),
|
677 |
+
"is_binary": False,
|
678 |
+
"is_structure_block": False
|
679 |
+
})
|
680 |
+
# Re-sort the cache to maintain consistent order
|
681 |
+
parsed_code_blocks_state_cache.sort(key=lambda b: (0, b["filename"]) if b.get("is_structure_block") else (1, b["filename"]))
|
682 |
+
|
683 |
+
# Regenerate markdown and preview from the updated cache
|
684 |
_formatted_md_out, _detected_preview_out, _download_btn_out = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name_part)
|
685 |
+
|
686 |
+
# Refresh file list choices and keep the current file selected
|
687 |
+
new_file_list, err_list = list_space_files_for_browsing(hf_api_key_ui, ui_space_name_part, owner_to_use)
|
688 |
+
if err_list:
|
689 |
+
_edit_status_val += f"\nFile list refresh error: {err_list}"
|
690 |
+
_file_browser_update_val = gr.update(choices=sorted(new_file_list or []), value="Error refreshing files")
|
691 |
+
else:
|
692 |
+
_file_browser_update_val = gr.update(choices=sorted(new_file_list or []), value=file_to_edit_path) # Keep current file selected
|
693 |
+
|
694 |
yield _edit_status_val, _file_browser_update_val, _formatted_md_out, _detected_preview_out, _download_btn_out
|
695 |
|
696 |
+
|
697 |
def handle_delete_file(hf_api_key_ui, ui_space_name_part, ui_owner_name_part, file_to_delete_path):
|
698 |
global parsed_code_blocks_state_cache
|
699 |
+
_edit_status_val = "Processing deletion..."
|
700 |
+
_file_browser_choices_update = gr.update()
|
701 |
+
_file_browser_value_update = None # Clear selected file
|
702 |
+
_file_content_editor_update = gr.update(value="") # Clear editor content
|
703 |
+
_commit_msg_update = gr.update(value="") # Clear commit message
|
704 |
+
_lang_update = gr.update(language="plaintext") # Reset editor language
|
705 |
_formatted_md_out, _detected_preview_out, _download_btn_out = gr.update(), gr.update(), gr.update()
|
706 |
+
|
707 |
+
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
|
708 |
+
|
709 |
+
|
710 |
+
if not file_to_delete_path or file_to_delete_path in ["No files found", "Error loading files", "Error refreshing files"]:
|
711 |
+
_edit_status_val = "Error: No valid file selected for deletion.";
|
712 |
+
yield (_edit_status_val, gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()); return
|
713 |
+
|
714 |
owner_to_use = ui_owner_name_part
|
715 |
if not owner_to_use:
|
716 |
+
token, token_err = build_logic_get_api_token(hf_api_key_ui)
|
717 |
+
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
|
718 |
if token:
|
719 |
+
try:
|
720 |
+
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
|
721 |
+
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
|
722 |
+
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
|
723 |
+
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
|
724 |
+
|
725 |
+
|
726 |
+
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
|
727 |
+
|
728 |
deletion_status_msg = build_logic_delete_space_file(hf_api_key_ui, ui_space_name_part, owner_to_use, file_to_delete_path)
|
729 |
_edit_status_val = deletion_status_msg
|
730 |
+
|
731 |
if "Successfully deleted" in deletion_status_msg:
|
732 |
+
# Remove the file from the cache
|
733 |
parsed_code_blocks_state_cache = [b for b in parsed_code_blocks_state_cache if b["filename"] != file_to_delete_path]
|
734 |
+
|
735 |
+
# Regenerate markdown and preview from the updated cache
|
736 |
_formatted_md_out, _detected_preview_out, _download_btn_out = _generate_ui_outputs_from_cache(owner_to_use, ui_space_name_part)
|
737 |
+
|
738 |
+
# Refresh file list choices and clear selected file
|
739 |
new_file_list, err_list = list_space_files_for_browsing(hf_api_key_ui, ui_space_name_part, owner_to_use)
|
740 |
+
if err_list:
|
741 |
+
_edit_status_val += f"\nFile list refresh error: {err_list}"
|
742 |
+
_file_browser_choices_update = gr.update(choices=sorted(new_file_list or []), value="Error refreshing files")
|
743 |
+
else:
|
744 |
+
_file_browser_choices_update = gr.update(choices=sorted(new_file_list or []), value=None) # Clear selection
|
745 |
+
|
746 |
+
_file_browser_value_update = None # Explicitly set value to None to clear selection visual
|
747 |
+
|
748 |
+
else:
|
749 |
+
# If deletion failed, refresh the file list but keep the file selected
|
750 |
+
new_file_list, _ = list_space_files_for_browsing(hf_api_key_ui, ui_space_name_part, owner_to_use)
|
751 |
+
_file_browser_choices_update = gr.update(choices=sorted(new_file_list or []), value=file_to_delete_path)
|
752 |
+
_file_browser_value_update = file_to_delete_path # Keep the file selected
|
753 |
+
|
754 |
+
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)
|
755 |
+
|
756 |
|
757 |
def handle_refresh_space_status(hf_api_key_ui, ui_owner_name, ui_space_name):
|
758 |
yield "*Fetching space status...*" # Initial feedback
|
|
|
764 |
except Exception as e: yield f"**Error auto-detecting owner:** {e}"; return
|
765 |
if not owner_to_use or not ui_space_name: yield "**Error:** Owner and Space Name are required."; return
|
766 |
status_details, error_msg = get_space_runtime_status(hf_api_key_ui, ui_space_name, owner_to_use)
|
767 |
+
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)}`"
|
768 |
elif status_details:
|
769 |
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','#')
|
770 |
md_lines = [f"### Space Status: {owner_to_use}/{ui_space_name}", f"- **Stage:** `{stage}`", f"- **Current Hardware:** `{hardware}`"]
|
771 |
if status_details.get('requested_hardware') and status_details.get('requested_hardware') != hardware: md_lines.append(f"- **Requested Hardware:** `{status_details.get('requested_hardware')}`")
|
772 |
if error: md_lines.append(f"- **Error:** <span style='color:red;'>`{escape_html_for_markdown(error)}`</span>")
|
773 |
md_lines.append(f"- [View Full Logs on Hugging Face]({log_link})")
|
774 |
+
if status_details.get('raw_data'):
|
775 |
+
# Add raw data in a collapsible section for debugging
|
776 |
+
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>")
|
777 |
+
|
778 |
_status_display_md = "\n".join(md_lines)
|
779 |
else: _status_display_md = "Could not retrieve status details."
|
780 |
yield _status_display_md
|
781 |
|
782 |
+
|
783 |
with gr.Blocks(theme=gr.themes.Soft(primary_hue="teal", secondary_hue="orange")) as demo:
|
784 |
gr.Markdown("# π€ AI Code & Space Generator")
|
785 |
gr.Markdown("Configure settings, chat with AI to generate/modify Hugging Face Spaces, then build, preview, and edit.")
|
786 |
with gr.Row():
|
787 |
with gr.Sidebar():
|
788 |
gr.Markdown("## βοΈ Configuration")
|
789 |
+
with gr.Group(): gr.Markdown("### API Keys & Tokens"); groq_api_key_input = gr.Textbox(label="Groq API Key", type="password", placeholder="gsk_...", allow_paste=True); hf_api_key_input = gr.Textbox(label="Hugging Face Token", type="password", placeholder="hf_...", allow_paste=True)
|
790 |
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")
|
791 |
with gr.Group(): gr.Markdown("### AI Model Settings"); groq_model_select = gr.Dropdown(label="Groq Model", choices=["mixtral-8x7b-32768", "llama3-8b-8192", "llama3-70b-8192", "gemma-7b-it"], value="llama3-8b-8192"); groq_system_prompt_input = gr.Textbox(label="System Prompt", lines=8, value=DEFAULT_SYSTEM_PROMPT, interactive=True)
|
792 |
with gr.Column(scale=3):
|
|
|
797 |
gr.Markdown("---")
|
798 |
with gr.Tabs():
|
799 |
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")
|
800 |
+
with gr.TabItem("π Detected Files Preview"): gr.Markdown(value="*Files preview...*")
|
801 |
gr.Markdown("---")
|
802 |
with gr.Tabs():
|
803 |
with gr.TabItem("π Build & Preview Space"):
|
804 |
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)
|
805 |
+
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)
|
806 |
with gr.TabItem("βοΈ Edit Space Files"):
|
807 |
gr.Markdown("Select a file to view, edit, or delete. Changes are committed to HF Hub.")
|
808 |
file_browser_dropdown = gr.Dropdown(label="Select File in Space", choices=[], interactive=True, visible=False, info="Load/build Space first.")
|
|
|
812 |
edit_status_display = gr.Textbox(label="File Edit/Delete Status", interactive=False, lines=2, value="*Select file...*")
|
813 |
|
814 |
chat_outputs = [groq_chat_message_input, groq_chatbot_display, groq_status_output, detected_files_preview, formatted_space_output_display, download_button]
|
815 |
+
chat_inputs = [groq_chat_message_input, groq_chatbot_display, groq_api_key_input, groq_model_select, groq_system_prompt_input, owner_name_input, space_name_input, formatted_space_output_display] # Added formatted_space_output_display to inputs
|
816 |
groq_send_chat_button.click(fn=handle_groq_chat_submit, inputs=chat_inputs, outputs=chat_outputs)
|
817 |
groq_chat_message_input.submit(fn=handle_groq_chat_submit, inputs=chat_inputs, outputs=chat_outputs)
|
818 |
+
|
819 |
+
# Added outputs for clearing/updating edit status when loading a space
|
820 |
load_space_outputs = [formatted_space_output_display, detected_files_preview, groq_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]
|
821 |
load_space_button.click(fn=handle_load_existing_space, inputs=[hf_api_key_input, owner_name_input, space_name_input], outputs=load_space_outputs)
|
822 |
+
|
823 |
build_outputs = [build_status_display, space_iframe_display, file_browser_dropdown]
|
824 |
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)
|
825 |
+
|
826 |
+
# Added commit message and language update to file edit load outputs
|
827 |
file_edit_load_outputs = [file_content_editor, edit_status_display, commit_message_input, file_content_editor]
|
828 |
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)
|
829 |
+
|
830 |
+
# Added formatted_space_output_display, detected_files_preview, download_button to commit outputs to refresh previews
|
831 |
commit_file_outputs = [edit_status_display, file_browser_dropdown, formatted_space_output_display, detected_files_preview, download_button]
|
832 |
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)
|
833 |
+
|
834 |
+
# Added file_content_editor, commit_message_input, language update, formatted_space_output_display, detected_files_preview, download_button to delete outputs
|
835 |
+
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]
|
836 |
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)
|
837 |
+
|
838 |
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])
|
839 |
|
840 |
if __name__ == "__main__":
|