wuhp commited on
Commit
8666ed0
·
verified ·
1 Parent(s): e4ef554

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +243 -218
app.py CHANGED
@@ -517,7 +517,8 @@ def orchestrate_development(client, project_state: dict, config, oauth_token_tok
517
  project_state['files'] = {}
518
  project_state['iframe_ok'] = False
519
  project_state['log_error_state'] = 'None'
520
- project_state['chat_history'].append({"role": "assistant", "content": "Project initialized. Starting development team."})
 
521
  return # Exit after initialization, next call will run the first task
522
 
523
  if project_state['status'] != 'In Progress':
@@ -535,7 +536,7 @@ def orchestrate_development(client, project_state: dict, config, oauth_token_tok
535
  if not project_state['chat_history'] or project_state['chat_history'][-1].get('content', '').strip() != task_message.strip():
536
  project_state['chat_history'].append({"role": "assistant", "content": task_message})
537
 
538
- step_successful = True # Assume success until a step explicitly fails
539
 
540
  if current_task == 'START':
541
  # This state should only be hit once for initialization, handled above.
@@ -599,7 +600,7 @@ This is an auto-generated HF Space.
599
  project_state['current_task'] = 'PUSHING'
600
  # Clear previous logs and feedback before pushing a new version
601
  project_state['logs'] = {'build': '', 'run': ''}
602
- project_state['feedback'] = ''
603
  project_state['iframe_ok'] = False
604
  project_state['log_error_state'] = 'None' # Reset log error state
605
  # Do NOT reset attempt count here, it's for total cycles
@@ -615,7 +616,6 @@ This is an auto-generated HF Space.
615
  project_state['status_message'] = "ERROR: Cannot push without Hugging Face token."
616
  project_state['chat_history'].append({"role": "assistant", "content": project_state['status_message']})
617
  project_state['current_task'] = 'FINISHED'
618
- step_successful = False
619
  print(project_state['status_message'])
620
  return # Exit early on critical error
621
 
@@ -642,6 +642,7 @@ This is an auto-generated HF Space.
642
 
643
  # Use a temporary directory to avoid cluttering the working directory
644
  import tempfile
 
645
  with tempfile.TemporaryDirectory() as tmpdir:
646
  temp_file_paths = []
647
  for fn, content in files_to_push.items():
@@ -654,39 +655,45 @@ This is an auto-generated HF Space.
654
  temp_file_paths.append(full_path)
655
  except Exception as e:
656
  print(f"Error writing temporary file {fn}: {e}")
657
- # Decide how to handle: skip file? Fail push? For now, skip.
 
658
  project_state['feedback'] = project_state.get('feedback', '') + f"\n\nError writing file {fn} for push: {e}"
 
659
 
660
 
661
- if not temp_file_paths:
662
  print("No valid files prepared for push.")
663
- project_state['status_message'] = "ERROR: No files were prepared for push after Code-Gen. Project Failed?"
664
  project_state['chat_history'].append({"role": "assistant", "content": project_state['status_message']})
665
  project_state['status'] = 'Failed'
666
  project_state['current_task'] = 'FINISHED'
667
  step_successful = False
668
- return
669
-
670
-
671
- # Upload files one by one
672
- for fn in files_to_push.keys(): # Iterate over keys to get path_in_repo
673
- filepath = os.path.join(tmpdir, fn)
674
- if filepath in temp_file_paths: # Only try to upload if temp file was successfully written
675
- try:
676
- upload_file(
677
- path_or_fileobj=filepath, path_in_repo=fn,
678
- repo_id=repo_id, token=oauth_token_token,
679
- repo_type="space"
680
- )
681
- print(f"Uploaded: {fn}")
682
- except Exception as e:
683
- print(f"Error uploading file {fn}: {e}")
684
- step_successful = False # Mark push as failed if any upload fails
685
- project_state['feedback'] = project_state.get('feedback', '') + f"\n\nError uploading {fn}: {e}"
686
- project_state['status_message'] = f"ERROR: Failed to upload {fn}. Details in feedback."
687
- # Continue trying other files, but mark overall step as failed
688
- else:
689
- print(f"Skipping upload for {fn} due to previous write error.")
 
 
 
 
690
 
691
  if step_successful:
692
  project_state['status_message'] = f"Pushed code to HF Space **{repo_id}**. Build triggered. Waiting for build and logs..."
@@ -696,11 +703,13 @@ This is an auto-generated HF Space.
696
  # Logs, iframe_ok, log_error_state were reset before push, will be updated by poller
697
 
698
  else:
699
- # If any upload failed, the push step failed
700
  project_state['status'] = 'Failed'
701
  project_state['current_task'] = 'FINISHED'
702
- project_state['status_message'] = project_state.get('status_message', f"ERROR: One or more files failed to upload to HF Space {repo_id}. See feedback.")
703
- project_state['chat_history'].append({"role": "assistant", "content": project_state['status_message']})
 
 
704
  print(project_state['status_message'])
705
 
706
 
@@ -747,16 +756,16 @@ This is an auto-generated HF Space.
747
  project_state['status_message'] = "Iframe check passed. Analyzing build/run logs."
748
 
749
  # Check for explicit log fetching errors
750
- elif log_error_state != 'None':
751
  print(f"Orchestrator: Detected log fetching error state: {log_error_state}. Moving to Debugging.")
752
  ready_to_debug = True
753
  project_state['status_message'] = f"Log fetching encountered an error ({log_error_state}). Moving to Debugging."
754
 
755
  # Check for significant logs that indicate build/run started/failed
756
  # Avoid triggering debug if it's just the 'No logs found yet' message
757
- elif not (build_logs.startswith("No build logs") or run_logs.startswith("No run logs")):
758
  # Check for actual log content or error indicators within logs
759
- if len(build_logs) > 50 or len(run_logs) > 10 or "ERROR" in build_logs.upper() or "FATAL" in build_logs.upper() or "ERROR" in run_logs.upper() or "FATAL" in run_logs.upper():
760
  print("Orchestrator: Detected significant logs. Moving to Debugging.")
761
  ready_to_debug = True
762
  project_state['status_message'] = "Significant logs detected. Analyzing build/run logs."
@@ -765,20 +774,17 @@ This is an auto-generated HF Space.
765
  # Timeout check - Use attempt count as a global timeout for the entire process,
766
  # but also use it to limit time spent *just* in LOGGING if no progress is detected.
767
  if project_state['attempt_count'] >= 6: # Use attempt count as a global timeout trigger
768
- print("Orchestrator: Max attempts reached in LOGGING. Forcing move to Debugging.")
769
  ready_to_debug = True # Force debug to get final feedback before failing
770
 
771
 
772
  if ready_to_debug:
773
  project_state['current_task'] = 'DEBUGGING'
774
  # Add a small delay before debugging starts, giving UI time to update logs
775
- time.sleep(2) # Add a slight pause
776
-
777
  else:
778
  # If not ready to debug, stay in LOGGING. The orchestrator loop will naturally pause between calls.
779
- # Increment attempt count only when staying in LOGGING without progress *after* the initial state.
780
- # The attempt count is more for the overall process, but we'll use it to track cycles spent here.
781
- # The attempt count is incremented globally at the end of the loop if not finished/failed.
782
  pass # Stay in LOGGING, loop will repeat
783
 
784
 
@@ -817,9 +823,9 @@ This is an auto-generated HF Space.
817
  is_failed = True
818
  project_state['status_message'] = f"Max attempts ({project_state['attempt_count']+1}/7) reached. Project failed."
819
  print(f"Debug Analysis: Failed due to max attempts ({project_state['attempt_count']+1}).")
820
- elif log_error_state in ['Auth Failed', 'Auth Failed JWT', 'Auth Failed STREAM', 'Request Error', 'Unexpected Error']: # Critical log fetching errors
821
  is_failed = True
822
- project_state['status_message'] = f"Project failed due to critical log fetching error: {log_error_state}"
823
  print(f"Debug Analysis: Failed due to critical log error: {log_error_state}.")
824
  elif ("ERROR" in feedback.upper() or error_types != "none") and project_state['attempt_count'] >= 4:
825
  # If debugger finds errors or classified errors exist after several attempts (e.g., attempts 5, 6, 7)
@@ -857,7 +863,7 @@ This is an auto-generated HF Space.
857
 
858
  else:
859
  # Should not happen - unknown state
860
- step_successful = False # Force failure path
861
  project_state['status'] = 'Failed'
862
  project_state['status_message'] = f"ERROR: Orchestrator entered an unknown task state: {current_task}"
863
  project_state['chat_history'].append({"role": "assistant", "content": project_state['status_message']})
@@ -873,6 +879,7 @@ This is an auto-generated HF Space.
873
  # time.sleep(1) # Adding a 1-second pause between steps
874
 
875
  # Final outcome message when status changes to Complete or Failed
 
876
  if project_state['status'] != 'In Progress' and not any(msg.get('content', '').strip().startswith("**Project Outcome:") for msg in project_state['chat_history']):
877
  final_outcome_message = f"**Project Outcome:** {project_state['status']} - {project_state.get('status_message', 'No specific outcome message.')}"
878
  project_state['chat_history'].append({"role": "assistant", "content": final_outcome_message})
@@ -887,7 +894,7 @@ This is an auto-generated HF Space.
887
  project_state['chat_history'].append({"role": "assistant", "content": failure_message})
888
 
889
 
890
- # orchestration_development function doesn't return UI outputs directly anymore.
891
  # It updates the project_state dictionary, which is a gr.State,
892
  # and the UI outputs are updated by the poller (_update_logs_and_preview)
893
  # or by the main handle_user_message function after calling orchestrate_development.
@@ -916,159 +923,147 @@ def _update_logs_and_preview(profile_token_state: tuple[gr.OAuthProfile | None,
916
  rid = f"{profile.username}/{clean}"
917
  url = f"https://huggingface.co/spaces/{profile.username}/{clean}"
918
 
919
- # Ensure project_state is initialized if poller runs before the first handle_user_message
920
- # This shouldn't happen in the intended flow, but defensively check.
921
  if current_project_state is None:
 
922
  current_project_state = {
923
  'repo_id': rid, # Set repo_id based on current UI inputs
924
  'iframe_url': url, # Set iframe_url based on current UI inputs
925
- 'logs': {'build': 'Initializing...', 'run': 'Initializing...'},
926
  'iframe_ok': False,
927
- 'log_error_state': 'Initializing...',
928
- 'chat_history': [],
929
  'status': 'Not Started',
930
- 'status_message': 'Enter requirements to start.',
931
  'attempt_count': 0,
932
  'requirements': '', 'plan': '', 'files': {}, 'feedback': '', 'current_task': None, 'sdk_choice': '', 'sdk_version': '', 'main_app_file': ''
933
  }
 
 
 
 
934
  elif current_project_state.get('repo_id') != rid:
935
  # If the UI Space name changes while a project is in progress for a *different* space,
936
  # reset the project state to focus on the new space name.
937
- # Alternatively, could just fetch logs for the new space without resetting the whole state.
938
- # Resetting seems safer to avoid confusion.
939
  print(f"Poller detected space name change from {current_project_state.get('repo_id')} to {rid}. Resetting state.")
940
  current_project_state = {
941
  'repo_id': rid,
942
  'iframe_url': url,
943
  'logs': {'build': 'Space name changed, fetching logs...', 'run': 'Space name changed, fetching logs...'},
944
  'iframe_ok': False,
945
- 'log_error_state': 'None',
946
  'chat_history': [{"role": "assistant", "content": f"Space name changed to **{rid}**. Project state reset. Enter requirements to start development on this Space."}],
947
  'status': 'Not Started',
948
  'status_message': 'Space name changed. Enter requirements to start.',
949
- 'attempt_count': 0,
950
  'requirements': '', 'plan': '', 'files': {}, 'feedback': '', 'current_task': None, 'sdk_choice': '', 'sdk_version': '', 'main_app_file': ''
951
  }
952
  else:
953
- # If space name matches the one in state, update status message to indicate polling is active
954
- if current_project_state.get('status') == 'In Progress' and current_project_state.get('current_task') == 'LOGGING':
955
- # Orchestrator updates status message in LOGGING, poller shouldn't overwrite it here.
956
- pass # Let orchestrator control message during LOGGING task
957
- elif current_project_state.get('status') in ['Complete', 'Failed']:
958
- # Don't change status message if project is finished
959
- pass
960
- else:
961
- # For other states (Not Started, In Progress but not LOGGING task), indicate polling
962
- current_status_message = f"Polling logs for {rid}... Current status: {current_project_state.get('status_message', '...')}"
963
- if not current_project_state['chat_history'] or current_project_state['chat_history'][-1].get('content', '').strip() != current_status_message.strip():
964
- # Avoid flooding chat history, maybe just update project_status_md?
965
- # Let's update state message, UI will show it.
966
- current_project_state['status_message'] = current_status_message
967
 
968
 
969
  # Fetch logs and check iframe status using the revised functions
970
- build_logs_content = current_project_state['logs'].get('build', 'Fetching...')
971
- run_logs_content = current_project_state['logs'].get('run', 'Fetching...')
972
- iframe_status_ok = current_project_state.get('iframe_ok', False)
973
- current_log_error_state = current_project_state.get('log_error_state', 'None')
974
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
975
 
976
- # Only fetch logs/check iframe if the project is 'In Progress' and the task is 'LOGGING',
977
- # OR if the status is 'Not Started' but a space name is entered (to show logs immediately).
978
- # OR if status is Complete/Failed, to allow checking final state.
979
- is_orchestration_polling = current_project_state.get('status') == 'In Progress' and current_project_state.get('current_task') == 'LOGGING'
980
- is_initial_or_finished_poll = current_project_state.get('status') in ['Not Started', 'Complete', 'Failed']
981
 
982
- if is_orchestration_polling or is_initial_or_finished_poll:
983
- print(f"Poller fetching logs for {rid} (Status: {current_project_state.get('status')}, Task: {current_project_state.get('current_task')})...")
984
- try:
985
- build_logs_content = fetch_logs(rid, "build")
986
- # Check for specific error markers returned by fetch_logs
987
- if build_logs_content.startswith("ERROR_"):
988
- current_log_error_state = build_logs_content.split(':')[0].replace('ERROR_', '') # Extract error type
989
- elif current_log_error_state == 'None' and build_logs_content.strip() == "No build logs found yet.":
990
- current_log_error_state = 'No Logs Yet' # Indicate waiting
991
- elif current_log_error_state == 'No Logs Yet' and build_logs_content.strip() != "No build logs found yet.":
992
- current_log_error_state = 'None' # Logs started appearing
993
-
994
- except Exception as e: # Should be caught by fetch_logs internal try/except
995
- build_logs_content = f"Poller Unexpected error fetching build logs: {e}"
996
- current_log_error_state = 'Poller Unexpected Error'
997
- print(build_logs_content)
998
-
999
-
1000
- # Only fetch run logs if a critical error wasn't found for build logs immediately
1001
- if current_log_error_state not in ['Auth Failed JWT', 'Not Found', 'Request Error', 'Poller Unexpected Error']:
1002
- try:
1003
- run_logs_content = fetch_logs(rid, "run")
1004
- if run_logs_content.startswith("ERROR_"):
1005
- current_log_error_state = run_logs_content.split(':')[0].replace('ERROR_', '') # Overwrite if run log error is more specific
1006
- elif current_log_error_state == 'No Logs Yet' and run_logs_content.strip() == "No run logs found yet.":
1007
- pass # Keep 'No Logs Yet'
1008
- elif current_log_error_state == 'No Logs Yet' and run_logs_content.strip() != "No run logs found yet.":
1009
- current_log_error_state = 'None' # Logs started appearing
1010
-
1011
- except Exception as e:
1012
- run_logs_content = f"Poller Unexpected error fetching run logs: {e}"
1013
- if current_log_error_state == 'None' or current_log_error_state == 'No Logs Yet':
1014
- current_log_error_state = 'Poller Unexpected Error'
1015
- print(run_logs_content)
1016
- else:
1017
- run_logs_content = f"Skipped fetching run logs due to build log error state: {current_log_error_state}"
1018
-
1019
-
1020
- try:
1021
- iframe_status_ok = check_iframe(url)
1022
- if not iframe_status_ok and current_log_error_state == 'None':
1023
- # If iframe isn't OK but no log error, assume it's still building or subtle issue
1024
- current_log_error_state = 'Iframe Not Ready'
1025
- elif iframe_status_ok and current_log_error_state == 'Iframe Not Ready':
1026
- current_log_error_state = 'None' # Iframe is now ready
1027
-
1028
- except Exception as e:
1029
- iframe_status_ok = False
1030
- if current_log_error_state == 'None':
1031
- current_log_error_state = 'Iframe Check Error'
1032
- print(f"Poller error checking iframe {url}: {e}")
1033
  else:
1034
- # If not polling based on orchestration or status, return existing state/placeholders
1035
- print(f"Poller skipping log fetch (Status: {current_project_state.get('status')}, Task: {current_project_state.get('current_task')})")
1036
- pass # Keep existing logs/status
 
 
 
1037
 
1038
 
 
1039
  preview_html = (
1040
- f'<iframe src="{url}" width="100%" height="500px" allow="accelerometer; ambient-light-sensor; autoplay; camera; gyroscope; hid; fullscreen; illustration; xr-spatial-tracking; sync-xhr;" sandbox="allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" frameborder="0"></iframe>'
1041
- if iframe_status_ok else
1042
- f"<p style='color:red;'>⚠️ App preview not loading or check failed ({url})."
1043
- f"{' **Authentication Error:** Ensure HF_TOKEN secret is set on the Space running this app.' if current_log_error_state in ['Auth Failed', 'Auth Failed JWT', 'Auth Failed STREAM'] else ''}"
1044
- f"{' **Space or logs not found yet:** Make sure the Space name is correct and wait for the build to complete.' if current_log_error_state == 'Not Found' else ''}"
1045
- f"{' **Timeout:** Log fetch timed out.' if current_log_error_state == 'TIMEOUT' else ''}"
1046
- f"{' **Iframe not ready:** Still building or error. Check logs.' if current_log_error_state == 'Iframe Not Ready' else ''}"
1047
- f"{' **Polling Error:** Encountered error fetching logs.' if current_log_error_state in ['Request Error', 'Poller Unexpected Error', 'Iframe Check Error'] else ''}"
1048
- f"{' Check build logs for errors.' if current_log_error_state not in ['Auth Failed', 'Auth Failed JWT', 'Auth Failed STREAM', 'Not Found'] else ''}"
 
1049
  f"</p>"
1050
  )
1051
 
1052
- # Update the project state with the latest info from the poller
1053
- # Only update if the state exists and matches the current repo_id being polled
1054
- if current_project_state is not None and current_project_state.get('repo_id') == rid:
1055
- current_project_state['logs']['build'] = build_logs_content
1056
- current_project_state['logs']['run'] = run_logs_content
1057
- current_project_state['iframe_ok'] = iframe_status_ok
1058
- current_project_state['log_error_state'] = current_log_error_state
1059
- # Do NOT update chat_history or status_message from the poller here,
1060
- # as the orchestrator handles those for better flow control.
1061
- # The UI will display the *state's* status_message and chat_history.
1062
 
1063
- return build_logs_content, run_logs_content, preview_html, current_project_state # Return updated state
 
 
 
 
 
 
1064
 
1065
 
1066
  # --- MAIN HANDLER (Called by Gradio) ---
1067
  # Updated signature to include login state explicitly and project state first
 
 
1068
  def handle_user_message(
1069
- profile: gr.OAuthProfile | None, # (1) - from login_btn (first in inputs)
1070
- oauth_token: gr.OAuthToken | None, # (2) - from login_btn (first in inputs)
1071
- project_state: dict | None, # (3) - from gr.State
1072
  user_input: str, # (4)
1073
  sdk_choice: str, # (5)
1074
  gemini_api_key: str, # (6)
@@ -1077,15 +1072,17 @@ def handle_user_message(
1077
  temperature: float, # (9)
1078
  max_output_tokens: int, # (10)
1079
  ):
1080
- # Now the function expects 10 parameters.
1081
- # The inputs list will be structured to match this.
1082
 
1083
  # Initialize or update project_state based on inputs and previous state
1084
  # Decide whether to start a new project or continue
 
1085
  is_new_project_request = (
1086
  project_state is None or # First run
1087
- project_state.get('status') in ['Complete', 'Failed'] or # Previous project finished
1088
  (user_input is not None and user_input.strip() != "" and user_input.strip() != project_state.get('requirements', '').strip()) # New requirements provided
 
 
1089
  )
1090
 
1091
  if is_new_project_request:
@@ -1098,7 +1095,7 @@ def handle_user_message(
1098
  sdk_version = get_sdk_version(sdk_choice)
1099
 
1100
  project_state = {
1101
- 'requirements': user_input,
1102
  'plan': '',
1103
  'files': {},
1104
  'logs': {'build': '', 'run': ''},
@@ -1116,60 +1113,79 @@ def handle_user_message(
1116
  'iframe_ok': False,
1117
  'log_error_state': 'None',
1118
  }
1119
- project_state['chat_history'].append({"role": "user", "content": user_input}) # Add initial user requirement
 
1120
 
1121
  else:
1122
- # Continue existing project state - just update potentially changed inputs
1123
  print("Continuing existing project...")
1124
- # history is part of project_state
1125
- # project_state['requirements'] remains the requirement that started the project
1126
- project_state['sdk_choice'] = sdk_choice # Allow changing SDK mid-project? Maybe not intended.
1127
- project_state['main_app_file'] = "app.py" if sdk_choice == "gradio" else "streamlit_app.py" # Update main file name
1128
- project_state['sdk_version'] = get_sdk_version(sdk_choice) # Update version
 
1129
  clean_space_name = space_name.strip() if space_name else ''
1130
- project_state['repo_id'] = f"{profile.username}/{clean_space_name}" if profile and clean_space_name else ''
1131
- project_state['iframe_url'] = f"https://huggingface.co/spaces/{profile.username}/{clean_space_name}" if profile and clean_space_name else ''
1132
- # Do NOT change task, status, attempts, logs, feedback, plan, files here.
1133
- # These are managed by the orchestration loop.
1134
- # The user_input might trigger a new plan if the Orchestrator is made to handle that.
1135
- # For now, a new user_input starts a new project (handled by is_new_project_request).
 
 
 
 
 
 
 
 
 
 
 
1136
 
1137
 
1138
  # Validation Checks (using received profile/token)
1139
- if not profile or not oauth_token or not hasattr(oauth_token, 'token') or not oauth_token.token:
1140
- error_msg = "⚠️ Please log in first via the Hugging Face button."
1141
- if not project_state['chat_history'] or project_state['chat_history'][-1].get("content") != error_msg:
1142
- project_state['chat_history'].append({"role":"assistant","content":error_msg})
1143
- project_state['status'] = 'Failed' # Mark as failed due to login
1144
- project_state['status_message'] = "Login required."
1145
- project_state['current_task'] = 'FINISHED' # End orchestration
1146
 
 
 
 
1147
  elif not space_name or not space_name.strip():
1148
- msg = "⚠️ Please enter a Space name."
1149
- if not project_state['chat_history'] or project_state['chat_history'][-1].get("content") != msg:
1150
- project_state['chat_history'].append({"role":"assistant","content":msg})
1151
- project_state['status'] = 'Failed' # Mark as failed due to missing space name
1152
- project_state['status_message'] = "Space name required."
1153
- project_state['current_task'] = 'FINISHED' # End orchestration
1154
-
1155
  elif not gemini_api_key:
1156
- error_msg = "⚠️ Please provide your Gemini API Key."
1157
- if not project_state['chat_history'] or project_state['chat_history'][-1].get("content") != error_msg:
1158
- project_state['chat_history'].append({"role":"assistant","content":error_msg})
1159
- project_state['status'] = 'Failed' # Mark as failed due to missing API key
1160
- project_state['status_message'] = "API Key required."
1161
- project_state['current_task'] = 'FINISHED' # End orchestration
1162
-
1163
  elif not user_input or user_input.strip() == "":
1164
- error_msg = "Please enter requirements for the application."
1165
- if not project_state['chat_history'] or project_state['chat_history'][-1].get("content") != error_msg:
1166
- project_state['chat_history'].append({"role":"assistant","content":error_msg})
1167
- # Don't fail immediately, wait for requirements
1168
- project_state['status_message'] = "Waiting for prompt."
1169
- project_state['status'] = 'Not Started' # Explicitly set state while waiting
1170
-
1171
-
1172
- # If validation passed and status is still 'In Progress', run orchestration
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1173
  if project_state.get('status') == 'In Progress':
1174
  client = genai.Client(api_key=gemini_api_key)
1175
 
@@ -1181,10 +1197,11 @@ def handle_user_message(
1181
  )
1182
 
1183
  # Run *one step* of orchestration. orchestrate_development updates project_state directly.
1184
- # The function is designed to be called repeatedly.
 
1185
  try:
1186
  orchestrate_development(
1187
- client, project_state, cfg, oauth_token.token if oauth_token else None
1188
  )
1189
  except Exception as e:
1190
  # Catch unexpected errors during orchestration step
@@ -1213,8 +1230,9 @@ def handle_user_message(
1213
  f"{' **Space or logs not found yet:** Make sure the Space name is correct and wait for the build to complete.' if project_state.get('log_error_state') == 'Not Found' else ''}"
1214
  f"{' **Timeout:** Log fetch timed out.' if project_state.get('log_error_state') == 'TIMEOUT' else ''}"
1215
  f"{' **Iframe not ready:** Still building or error. Check logs.' if project_state.get('log_error_state') == 'Iframe Not Ready' else ''}"
1216
- f"{' **Polling Error:** Encountered error fetching logs.' if project_state.get('log_error_state') in ['Request Error', 'Poller Unexpected Error', 'Iframe Check Error'] else ''}"
1217
- f"{' Check build logs for errors.' if project_state.get('log_error_state') in ['None', 'No Logs Yet', 'Iframe Not Ready'] else ''}" # Suggest checking logs if no specific error or just waiting
 
1218
  f"</p>"),
1219
  project_state.get('status_message', '...'), # Status message output (get from state)
1220
  )
@@ -1238,17 +1256,19 @@ with gr.Blocks(title="HF Space Auto‑Builder (Team AI)") as demo:
1238
  models_md = gr.Markdown()
1239
 
1240
  # Use api_name to make these callable via API if needed, but not strictly necessary for UI load
 
1241
  demo.load(show_profile, inputs=None, outputs=status_md) # api_name="load_profile" removed
1242
  demo.load(list_private_models, inputs=None, outputs=models_md) # api_name="load_models" removed
1243
 
 
1244
  login_btn.click(
1245
  fn=show_profile,
1246
- inputs=None,
1247
  outputs=status_md
1248
  )
1249
  login_btn.click(
1250
  fn=list_private_models,
1251
- inputs=None,
1252
  outputs=models_md
1253
  )
1254
  # --- END LOGIN FIX ---
@@ -1283,17 +1303,15 @@ with gr.Blocks(title="HF Space Auto‑Builder (Team AI)") as demo:
1283
  preview = gr.HTML("<p>App preview will load here when available.</p>")
1284
 
1285
  # --- Hidden poller (using Timer) ---
1286
- # Polls less frequently initially, increases if project is in progress? Or keep constant?
1287
- # Constant 2s is fine for demonstration.
1288
  log_poller = gr.Timer(value=2, active=True, render=False)
1289
  # --- End hidden poller ---
1290
 
1291
  # handle_user_message is defined ABOVE this block
1292
  # The main button and submit handler now receive and return the project_state
1293
- # INPUTS: gr.LoginButton outputs (profile, token) first, then state, then other UI inputs
1294
  inputs_list = [
1295
- login_btn, # <-- Login button first (provides profile, token)
1296
- project_state, # <-- Pass the state in
1297
  user_in, # Other UI inputs in order
1298
  sdk_choice,
1299
  api_key,
@@ -1301,8 +1319,9 @@ with gr.Blocks(title="HF Space Auto‑Builder (Team AI)") as demo:
1301
  grounding,
1302
  temp,
1303
  max_tokens,
 
1304
  # Note: chatbot is NOT in inputs, its history is managed via project_state['chat_history']
1305
- ] # Total 9 components -> should provide 2 + 1 + 7 = 10 arguments
1306
 
1307
  # OUTPUTS: Must match the return values of handle_user_message
1308
  outputs_list = [
@@ -1318,23 +1337,27 @@ with gr.Blocks(title="HF Space Auto‑Builder (Team AI)") as demo:
1318
  fn=handle_user_message,
1319
  inputs=inputs_list,
1320
  outputs=outputs_list
 
1321
  )
1322
 
1323
  user_in.submit(
1324
  fn=handle_user_message,
1325
  inputs=inputs_list,
1326
  outputs=outputs_list
 
1327
  )
1328
 
1329
  # --- Wire the poller to the update function ---
1330
  # _update_logs_and_preview is defined ABOVE this block
1331
- # The poller needs login state, space name, and the current project state
1332
  # INPUTS: login_btn state (as tuple), space name, project state
 
 
1333
  poller_inputs = [
1334
- login_btn, # Provides (profile, token) as a tuple when put first for a single param
1335
  space_name,
1336
  project_state # Passes the current state value
1337
- ] # Total 3 components -> should provide 1 (tuple) + 1 + 1 = 3 arguments
1338
  # Note: _update_logs_and_preview signature is (profile_token_state, space_name, current_project_state)
1339
 
1340
  # OUTPUTS: Must match the return values of _update_logs_and_preview
@@ -1349,6 +1372,7 @@ with gr.Blocks(title="HF Space Auto‑Builder (Team AI)") as demo:
1349
  fn=_update_logs_and_preview,
1350
  inputs=poller_inputs,
1351
  outputs=poller_outputs
 
1352
  )
1353
  # --- End wire poller ---
1354
 
@@ -1360,4 +1384,5 @@ with gr.Blocks(title="HF Space Auto‑Builder (Team AI)") as demo:
1360
  # If running on a Space, server_name='0.0.0.0' and server_port=7860 are standard.
1361
  # If running locally, just demo.launch() or specify a port.
1362
  if __name__ == "__main__":
1363
- demo.launch(server_name="0.0.0.0", server_port=7860)
 
 
517
  project_state['files'] = {}
518
  project_state['iframe_ok'] = False
519
  project_state['log_error_state'] = 'None'
520
+ # Initial message handled in handle_user_message now
521
+ # project_state['chat_history'].append({"role": "assistant", "content": "Project initialized. Starting development team."})
522
  return # Exit after initialization, next call will run the first task
523
 
524
  if project_state['status'] != 'In Progress':
 
536
  if not project_state['chat_history'] or project_state['chat_history'][-1].get('content', '').strip() != task_message.strip():
537
  project_state['chat_history'].append({"role": "assistant", "content": task_message})
538
 
539
+ # step_successful is managed within each task block
540
 
541
  if current_task == 'START':
542
  # This state should only be hit once for initialization, handled above.
 
600
  project_state['current_task'] = 'PUSHING'
601
  # Clear previous logs and feedback before pushing a new version
602
  project_state['logs'] = {'build': '', 'run': ''}
603
+ project_state['feedback'] = '' # Clear feedback from previous debug cycle
604
  project_state['iframe_ok'] = False
605
  project_state['log_error_state'] = 'None' # Reset log error state
606
  # Do NOT reset attempt count here, it's for total cycles
 
616
  project_state['status_message'] = "ERROR: Cannot push without Hugging Face token."
617
  project_state['chat_history'].append({"role": "assistant", "content": project_state['status_message']})
618
  project_state['current_task'] = 'FINISHED'
 
619
  print(project_state['status_message'])
620
  return # Exit early on critical error
621
 
 
642
 
643
  # Use a temporary directory to avoid cluttering the working directory
644
  import tempfile
645
+ step_successful = True # Assume success initially for pushing
646
  with tempfile.TemporaryDirectory() as tmpdir:
647
  temp_file_paths = []
648
  for fn, content in files_to_push.items():
 
655
  temp_file_paths.append(full_path)
656
  except Exception as e:
657
  print(f"Error writing temporary file {fn}: {e}")
658
+ # Decide how to handle: skip file? Fail push? For now, mark push as failed.
659
+ step_successful = False # Mark push as failed if any file fails to write
660
  project_state['feedback'] = project_state.get('feedback', '') + f"\n\nError writing file {fn} for push: {e}"
661
+ project_state['status_message'] = f"ERROR: Failed to write {fn} locally for push. Details in feedback."
662
 
663
 
664
+ if not temp_file_paths and len(files_to_push) > 0: # If files were supposed to be pushed but none were written
665
  print("No valid files prepared for push.")
666
+ project_state['status_message'] = "ERROR: No files were prepared for push after Code-Gen. Project Failed."
667
  project_state['chat_history'].append({"role": "assistant", "content": project_state['status_message']})
668
  project_state['status'] = 'Failed'
669
  project_state['current_task'] = 'FINISHED'
670
  step_successful = False
671
+ # No need to return here, let the rest of the block handle the failure state
672
+
673
+
674
+ # Upload files one by one IF step_successful is still True after writing
675
+ if step_successful:
676
+ for fn in files_to_push.keys(): # Iterate over keys to get path_in_repo
677
+ filepath = os.path.join(tmpdir, fn)
678
+ # Only try to upload if temp file was successfully written and overall push isn't already failed
679
+ if filepath in temp_file_paths and step_successful:
680
+ try:
681
+ upload_file(
682
+ path_or_fileobj=filepath, path_in_repo=fn,
683
+ repo_id=repo_id, token=oauth_token_token,
684
+ repo_type="space"
685
+ )
686
+ print(f"Uploaded: {fn}")
687
+ except Exception as e:
688
+ print(f"Error uploading file {fn}: {e}")
689
+ step_successful = False # Mark push as failed if any upload fails
690
+ project_state['feedback'] = project_state.get('feedback', '') + f"\n\nError uploading {fn}: {e}"
691
+ project_state['status_message'] = f"ERROR: Failed to upload {fn}. Details in feedback."
692
+ # Continue trying other files, but mark overall step as failed
693
+ elif step_successful: # If step_successful is True but file wasn't written
694
+ print(f"Skipping upload for {fn} as it failed to write locally.")
695
+ # This case is already covered by the write loop failing step_successful
696
+
697
 
698
  if step_successful:
699
  project_state['status_message'] = f"Pushed code to HF Space **{repo_id}**. Build triggered. Waiting for build and logs..."
 
703
  # Logs, iframe_ok, log_error_state were reset before push, will be updated by poller
704
 
705
  else:
706
+ # If any write or upload failed, the push step failed
707
  project_state['status'] = 'Failed'
708
  project_state['current_task'] = 'FINISHED'
709
+ project_state['status_message'] = project_state.get('status_message', f"ERROR: One or more files failed to process or upload to HF Space {repo_id}. See feedback.")
710
+ # Ensure the error message is added to chat history if not already there from writing/uploading
711
+ if not project_state['chat_history'] or project_state['chat_history'][-1].get('content', '').strip() != project_state['status_message'].strip():
712
+ project_state['chat_history'].append({"role": "assistant", "content": project_state['status_message']})
713
  print(project_state['status_message'])
714
 
715
 
 
756
  project_state['status_message'] = "Iframe check passed. Analyzing build/run logs."
757
 
758
  # Check for explicit log fetching errors
759
+ elif log_error_state not in ['None', 'No Logs Yet', 'Iframe Not Ready']: # Any error state that is NOT just 'waiting'
760
  print(f"Orchestrator: Detected log fetching error state: {log_error_state}. Moving to Debugging.")
761
  ready_to_debug = True
762
  project_state['status_message'] = f"Log fetching encountered an error ({log_error_state}). Moving to Debugging."
763
 
764
  # Check for significant logs that indicate build/run started/failed
765
  # Avoid triggering debug if it's just the 'No logs found yet' message
766
+ elif not (build_logs.strip() == "No build logs found yet." and run_logs.strip() == "No run logs found yet."):
767
  # Check for actual log content or error indicators within logs
768
+ if len(build_logs.strip()) > 50 or len(run_logs.strip()) > 10 or "ERROR" in build_logs.upper() or "FATAL" in build_logs.upper() or "ERROR" in run_logs.upper() or "FATAL" in run_logs.upper():
769
  print("Orchestrator: Detected significant logs. Moving to Debugging.")
770
  ready_to_debug = True
771
  project_state['status_message'] = "Significant logs detected. Analyzing build/run logs."
 
774
  # Timeout check - Use attempt count as a global timeout for the entire process,
775
  # but also use it to limit time spent *just* in LOGGING if no progress is detected.
776
  if project_state['attempt_count'] >= 6: # Use attempt count as a global timeout trigger
777
+ print("Orchestrator: Max attempts reached in LOGGING/overall. Forcing move to Debugging.")
778
  ready_to_debug = True # Force debug to get final feedback before failing
779
 
780
 
781
  if ready_to_debug:
782
  project_state['current_task'] = 'DEBUGGING'
783
  # Add a small delay before debugging starts, giving UI time to update logs
784
+ # time.sleep(2) # Moved implicit wait to poller tick
 
785
  else:
786
  # If not ready to debug, stay in LOGGING. The orchestrator loop will naturally pause between calls.
787
+ # Attempt count is incremented globally at the end of the loop if not finished/failed.
 
 
788
  pass # Stay in LOGGING, loop will repeat
789
 
790
 
 
823
  is_failed = True
824
  project_state['status_message'] = f"Max attempts ({project_state['attempt_count']+1}/7) reached. Project failed."
825
  print(f"Debug Analysis: Failed due to max attempts ({project_state['attempt_count']+1}).")
826
+ elif log_error_state in ['Auth Failed', 'Auth Failed JWT', 'Auth Failed STREAM', 'Request Error', 'Unexpected Error', 'Poller Unexpected Error', 'HTTP Error']: # Critical log fetching errors
827
  is_failed = True
828
+ project_state['status_message'] = f"Project failed due to critical log or iframe fetching error: {log_error_state}"
829
  print(f"Debug Analysis: Failed due to critical log error: {log_error_state}.")
830
  elif ("ERROR" in feedback.upper() or error_types != "none") and project_state['attempt_count'] >= 4:
831
  # If debugger finds errors or classified errors exist after several attempts (e.g., attempts 5, 6, 7)
 
863
 
864
  else:
865
  # Should not happen - unknown state
866
+ # step_successful = False # Force failure path - not needed as status is set
867
  project_state['status'] = 'Failed'
868
  project_state['status_message'] = f"ERROR: Orchestrator entered an unknown task state: {current_task}"
869
  project_state['chat_history'].append({"role": "assistant", "content": project_state['status_message']})
 
879
  # time.sleep(1) # Adding a 1-second pause between steps
880
 
881
  # Final outcome message when status changes to Complete or Failed
882
+ # Only add if it's the final state and the message hasn't been added
883
  if project_state['status'] != 'In Progress' and not any(msg.get('content', '').strip().startswith("**Project Outcome:") for msg in project_state['chat_history']):
884
  final_outcome_message = f"**Project Outcome:** {project_state['status']} - {project_state.get('status_message', 'No specific outcome message.')}"
885
  project_state['chat_history'].append({"role": "assistant", "content": final_outcome_message})
 
894
  project_state['chat_history'].append({"role": "assistant", "content": failure_message})
895
 
896
 
897
+ # orchestrate_development function doesn't return UI outputs directly anymore.
898
  # It updates the project_state dictionary, which is a gr.State,
899
  # and the UI outputs are updated by the poller (_update_logs_and_preview)
900
  # or by the main handle_user_message function after calling orchestrate_development.
 
923
  rid = f"{profile.username}/{clean}"
924
  url = f"https://huggingface.co/spaces/{profile.username}/{clean}"
925
 
926
+ # Initialize project_state if it's the first call or if space name changed while not mid-project
 
927
  if current_project_state is None:
928
+ print(f"Poller: Initializing state for space {rid}")
929
  current_project_state = {
930
  'repo_id': rid, # Set repo_id based on current UI inputs
931
  'iframe_url': url, # Set iframe_url based on current UI inputs
932
+ 'logs': {'build': 'Polling...', 'run': 'Polling...'},
933
  'iframe_ok': False,
934
+ 'log_error_state': 'Polling...', # Initial polling state
935
+ 'chat_history': [], # Start with empty history for a new space
936
  'status': 'Not Started',
937
+ 'status_message': f'Polling logs for {rid}... Enter requirements to start project.',
938
  'attempt_count': 0,
939
  'requirements': '', 'plan': '', 'files': {}, 'feedback': '', 'current_task': None, 'sdk_choice': '', 'sdk_version': '', 'main_app_file': ''
940
  }
941
+ # Add an initial message if history is empty
942
+ if not current_project_state['chat_history']:
943
+ current_project_state['chat_history'].append({"role": "assistant", "content": current_project_state['status_message']})
944
+
945
  elif current_project_state.get('repo_id') != rid:
946
  # If the UI Space name changes while a project is in progress for a *different* space,
947
  # reset the project state to focus on the new space name.
 
 
948
  print(f"Poller detected space name change from {current_project_state.get('repo_id')} to {rid}. Resetting state.")
949
  current_project_state = {
950
  'repo_id': rid,
951
  'iframe_url': url,
952
  'logs': {'build': 'Space name changed, fetching logs...', 'run': 'Space name changed, fetching logs...'},
953
  'iframe_ok': False,
954
+ 'log_error_state': 'None', # Reset error state
955
  'chat_history': [{"role": "assistant", "content": f"Space name changed to **{rid}**. Project state reset. Enter requirements to start development on this Space."}],
956
  'status': 'Not Started',
957
  'status_message': 'Space name changed. Enter requirements to start.',
958
+ 'attempt_count': 0, # Reset attempts for a new project
959
  'requirements': '', 'plan': '', 'files': {}, 'feedback': '', 'current_task': None, 'sdk_choice': '', 'sdk_version': '', 'main_app_file': ''
960
  }
961
  else:
962
+ # If space name matches the one in state, potentially update polling status message
963
+ if current_project_state.get('status') in ['Not Started']:
964
+ current_status_message = f"Polling logs for {rid}... Current status: {current_project_state.get('status_message', '...')}"
965
+ # Avoid flooding chat history with this, just update the status message field
966
+ current_project_state['status_message'] = current_status_message
967
+ # Ensure there's at least an initial message in history if needed
968
+ if not current_project_state['chat_history']:
969
+ current_project_state['chat_history'].append({"role": "assistant", "content": current_project_state['status_message']})
970
+ # If status is 'In Progress' and task is 'LOGGING', the orchestrator manages status_message.
971
+ # If status is 'Complete' or 'Failed', the final status_message is set.
972
+ # Otherwise, keep the current status_message.
 
 
 
973
 
974
 
975
  # Fetch logs and check iframe status using the revised functions
976
+ # Always attempt to fetch logs and check iframe if a repo_id is set in the state.
977
+ # The fetch_logs function handles errors like 401/404/timeout.
978
+ if current_project_state.get('repo_id'):
979
+ print(f"Poller fetching logs for {rid} (Status: {current_project_state.get('status')}, Task: {current_project_state.get('current_task')})...")
980
 
981
+ build_logs_content = fetch_logs(rid, "build")
982
+ run_logs_content = fetch_logs(rid, "run") # Fetch run logs regardless of build log status
983
+ iframe_status_ok = check_iframe(url)
984
+
985
+ # Update log_error_state based on fetch results
986
+ # Priority to critical errors > iframe issues > waiting
987
+ new_log_error_state = 'None'
988
+
989
+ if build_logs_content.startswith("ERROR_AUTH_FAILED_JWT:") or run_logs_content.startswith("ERROR_AUTH_FAILED_JWT:"):
990
+ new_log_error_state = 'Auth Failed JWT'
991
+ elif build_logs_content.startswith("ERROR_AUTH_FAILED_STREAM:") or run_logs_content.startswith("ERROR_AUTH_FAILED_STREAM:"):
992
+ new_log_error_state = 'Auth Failed STREAM'
993
+ elif build_logs_content.startswith("ERROR_NOT_FOUND:") or run_logs_content.startswith("ERROR_NOT_FOUND:"):
994
+ new_log_error_state = 'Not Found'
995
+ elif build_logs_content.startswith("ERROR_TIMEOUT:") or run_logs_content.startswith("ERROR_TIMEOUT:"):
996
+ new_log_error_state = 'TIMEOUT'
997
+ elif build_logs_content.startswith("ERROR_REQUEST:") or run_logs_content.startswith("ERROR_REQUEST:"):
998
+ new_log_error_state = 'Request Error'
999
+ elif build_logs_content.startswith("ERROR_HTTP_") or run_logs_content.startswith("ERROR_HTTP_"):
1000
+ new_log_error_state = 'HTTP Error'
1001
+ elif build_logs_content.startswith("ERROR_UNEXPECTED:") or run_logs_content.startswith("ERROR_UNEXPECTED:"):
1002
+ new_log_error_state = 'Poller Unexpected Error'
1003
+ elif not iframe_status_ok:
1004
+ # If iframe is not ok, and no explicit log error, assume it's not ready
1005
+ new_log_error_state = 'Iframe Not Ready'
1006
+ elif build_logs_content.strip() == "No build logs found yet." and run_logs_content.strip() == "No run logs found yet.":
1007
+ # If no logs and iframe ok, maybe it's a very simple app? Or a very fast build?
1008
+ # Let's not mark it as 'No Logs Yet' if iframe is OK.
1009
+ pass # Keep None
1010
+ elif (build_logs_content.strip() == "No build logs found yet." or run_logs_content.strip() == "No run logs found yet.") and iframe_status_ok:
1011
+ # Partial logs, but iframe is OK
1012
+ pass # Keep None
1013
+ elif (build_logs_content.strip() != "No build logs found yet." or run_logs_content.strip() != "No run logs found yet.") and not iframe_status_ok:
1014
+ # Logs are appearing but iframe is not OK
1015
+ new_log_error_state = 'Iframe Not Ready' # Re-assert iframe not ready if logs appear but it's still down
1016
 
 
 
 
 
 
1017
 
1018
+ current_project_state['logs']['build'] = build_logs_content
1019
+ current_project_state['logs']['run'] = run_logs_content
1020
+ current_project_state['iframe_ok'] = iframe_status_ok
1021
+ current_project_state['log_error_state'] = new_log_error_state
1022
+ # Do NOT update chat_history or status_message from the poller here.
1023
+ # The orchestrator needs to react to the *state* changes, not manage the UI messages directly from poller.
1024
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1025
  else:
1026
+ # If repo_id is not set in state, cannot poll logs
1027
+ build_logs_content = current_project_state['logs'].get('build', 'Space name not set, cannot poll logs.')
1028
+ run_logs_content = current_project_state['logs'].get('run', 'Space name not set, cannot poll logs.')
1029
+ iframe_status_ok = False
1030
+ current_log_error_state = current_project_state.get('log_error_state', 'Space Not Set')
1031
+ print("Poller skipping log fetch because repo_id is not set in state.")
1032
 
1033
 
1034
+ # Reconstruct preview HTML based on the updated state
1035
  preview_html = (
1036
+ f'<iframe src="{current_project_state.get("iframe_url", "")}" width="100%" height="500px" allow="accelerometer; ambient-light-sensor; autoplay; camera; gyroscope; hid; fullscreen; illustration; xr-spatial-tracking; sync-xhr;" sandbox="allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" frameborder="0"></iframe>'
1037
+ if current_project_state.get('iframe_ok') else
1038
+ f"<p style='color:red;'>⚠️ App preview not loading or check failed ({current_project_state.get('iframe_url', 'URL not set.')})."
1039
+ f"{' **Authentication Error:** Ensure HF_TOKEN secret is set on the Space running this app.' if current_project_state.get('log_error_state') in ['Auth Failed', 'Auth Failed JWT', 'Auth Failed STREAM'] else ''}"
1040
+ f"{' **Space or logs not found yet:** Make sure the Space name is correct and wait for the build to complete.' if current_project_state.get('log_error_state') == 'Not Found' else ''}"
1041
+ f"{' **Timeout:** Log fetch timed out.' if current_project_state.get('log_error_state') == 'TIMEOUT' else ''}"
1042
+ f"{' **Iframe not ready:** Still building or error. Check logs.' if current_project_state.get('log_error_state') == 'Iframe Not Ready' else ''}"
1043
+ f"{' **Polling Error:** Encountered error fetching logs.' if current_project_state.get('log_error_state') in ['Request Error', 'Poller Unexpected Error', 'Iframe Check Error', 'HTTP Error'] else ''}"
1044
+ f"{' **Space Name Not Set:** Cannot fetch logs or check iframe.' if current_project_state.get('log_error_state') == 'Space Not Set' else ''}"
1045
+ f"{' Check build logs for details.' if current_project_state.get('log_error_state') not in ['Auth Failed', 'Auth Failed JWT', 'Auth Failed STREAM', 'Not Found', 'Space Not Set'] else ''}" # Suggest checking logs if no specific critical error
1046
  f"</p>"
1047
  )
1048
 
 
 
 
 
 
 
 
 
 
 
1049
 
1050
+ # Return the updated state along with the UI outputs derived *from* the state
1051
+ return (
1052
+ build_logs_content, # Build logs output
1053
+ run_logs_content, # Run logs output
1054
+ preview_html, # Preview HTML output
1055
+ current_project_state # Updated project state
1056
+ )
1057
 
1058
 
1059
  # --- MAIN HANDLER (Called by Gradio) ---
1060
  # Updated signature to include login state explicitly and project state first
1061
+ # Note: Gradio 4.x might prefer profile and token as the first two args when auto-injecting.
1062
+ # Let's stick to the recommended Gradio pattern where auto-injected params come first.
1063
  def handle_user_message(
1064
+ profile: gr.OAuthProfile | None, # (1) - Auto-injected by Gradio
1065
+ oauth_token: gr.OAuthToken | None, # (2) - Auto-injected by Gradio
1066
+ project_state: dict | None, # (3) - from gr.State component
1067
  user_input: str, # (4)
1068
  sdk_choice: str, # (5)
1069
  gemini_api_key: str, # (6)
 
1072
  temperature: float, # (9)
1073
  max_output_tokens: int, # (10)
1074
  ):
1075
+ # Now the function signature has 10 parameters.
1076
+ # The inputs list will exclude login_btn to allow auto-injection.
1077
 
1078
  # Initialize or update project_state based on inputs and previous state
1079
  # Decide whether to start a new project or continue
1080
+ # A new project starts if state is None, or if requirements change
1081
  is_new_project_request = (
1082
  project_state is None or # First run
 
1083
  (user_input is not None and user_input.strip() != "" and user_input.strip() != project_state.get('requirements', '').strip()) # New requirements provided
1084
+ # We could also check if status is 'Complete' or 'Failed' to auto-reset, but letting
1085
+ # the user input change trigger the reset is more explicit.
1086
  )
1087
 
1088
  if is_new_project_request:
 
1095
  sdk_version = get_sdk_version(sdk_choice)
1096
 
1097
  project_state = {
1098
+ 'requirements': user_input.strip() if user_input else '', # Store cleaned requirements
1099
  'plan': '',
1100
  'files': {},
1101
  'logs': {'build': '', 'run': ''},
 
1113
  'iframe_ok': False,
1114
  'log_error_state': 'None',
1115
  }
1116
+ if user_input and user_input.strip():
1117
+ project_state['chat_history'].append({"role": "user", "content": user_input.strip()}) # Add initial user requirement
1118
 
1119
  else:
1120
+ # Continue existing project state
1121
  print("Continuing existing project...")
1122
+ # history is part of project_state - no need to copy
1123
+ # project_state['requirements'] stays the same requirement that started the project
1124
+ # Update other settings that could change mid-project (though maybe shouldn't)
1125
+ project_state['sdk_choice'] = sdk_choice
1126
+ project_state['main_app_file'] = "app.py" if sdk_choice == "gradio" else "streamlit_app.py"
1127
+ project_state['sdk_version'] = get_sdk_version(sdk_choice)
1128
  clean_space_name = space_name.strip() if space_name else ''
1129
+ # Only update repo_id/iframe_url if the name changes (and state is not 'Not Started')
1130
+ if project_state.get('repo_id') != f"{profile.username}/{clean_space_name}" and project_state.get('status') != 'Not Started' :
1131
+ # This case should ideally be handled by the poller's state reset, but double check.
1132
+ print(f"Handle_user_message detected space name change mid-project from {project_state.get('repo_id')} to {profile.username}/{clean_space_name}. Forcing project reset.")
1133
+ # Re-initialize as a new project
1134
+ return handle_user_message(profile, oauth_token, None, user_input, sdk_choice, gemini_api_key, space_name, grounding_enabled, temperature, max_output_tokens)
1135
+ elif profile and clean_space_name:
1136
+ project_state['repo_id'] = f"{profile.username}/{clean_space_name}"
1137
+ project_state['iframe_url'] = f"https://huggingface.co/spaces/{profile.username}/{clean_space_name}"
1138
+
1139
+ # If status is 'Not Started' and user sends input again (maybe after fixing something),
1140
+ # set task to START to begin orchestration.
1141
+ if project_state.get('status') == 'Not Started' and user_input and user_input.strip() and project_state.get('current_task') is None:
1142
+ print("Status was 'Not Started', user sent input. Starting orchestration.")
1143
+ project_state['current_task'] = 'START'
1144
+ project_state['status'] = 'In Progress'
1145
+ project_state['status_message'] = 'Initializing...'
1146
 
1147
 
1148
  # Validation Checks (using received profile/token)
1149
+ validation_error = None
1150
+ validation_message = None
 
 
 
 
 
1151
 
1152
+ if not profile or not oauth_token or not hasattr(oauth_token, 'token') or not oauth_token.token:
1153
+ validation_error = "Login required."
1154
+ validation_message = "⚠️ Please log in first via the Hugging Face button."
1155
  elif not space_name or not space_name.strip():
1156
+ validation_error = "Space name required."
1157
+ validation_message = "⚠️ Please enter a Space name."
 
 
 
 
 
1158
  elif not gemini_api_key:
1159
+ validation_error = "API Key required."
1160
+ validation_message = "⚠️ Please provide your Gemini API Key."
 
 
 
 
 
1161
  elif not user_input or user_input.strip() == "":
1162
+ validation_error = "Requirements required."
1163
+ validation_message = "Please enter requirements for the application."
1164
+
1165
+
1166
+ # If there's a validation error, update state and return early
1167
+ if validation_error:
1168
+ if project_state.get('status') != 'Failed' or project_state.get('status_message') != validation_message:
1169
+ # Avoid adding duplicate error messages
1170
+ project_state['status'] = 'Failed' # Mark as failed due to validation
1171
+ project_state['status_message'] = validation_message
1172
+ project_state['current_task'] = 'FINISHED' # End orchestration
1173
+ if not project_state['chat_history'] or project_state['chat_history'][-1].get("content") != validation_message:
1174
+ project_state['chat_history'].append({"role":"assistant","content":validation_message})
1175
+
1176
+ # Return current state and UI outputs
1177
+ return (
1178
+ project_state,
1179
+ project_state.get('chat_history', []),
1180
+ project_state['logs'].get('build', ''),
1181
+ project_state['logs'].get('run', ''),
1182
+ f"<p style='color:red;'>{validation_message}</p>", # Simple error preview
1183
+ project_state.get('status_message', 'Validation Failed')
1184
+ )
1185
+
1186
+
1187
+ # If validation passed and status is 'In Progress' or being set to 'In Progress' ('START' task)
1188
+ # Note: orchestrate_development handles the 'START' -> 'In Progress' transition internally
1189
  if project_state.get('status') == 'In Progress':
1190
  client = genai.Client(api_key=gemini_api_key)
1191
 
 
1197
  )
1198
 
1199
  # Run *one step* of orchestration. orchestrate_development updates project_state directly.
1200
+ # The function is designed to be called repeatedly by subsequent handle_user_message calls
1201
+ # triggered by the poller implicitly updating state and thus triggering retries.
1202
  try:
1203
  orchestrate_development(
1204
+ client, project_state, cfg, oauth_token.token # Pass the token for pushing
1205
  )
1206
  except Exception as e:
1207
  # Catch unexpected errors during orchestration step
 
1230
  f"{' **Space or logs not found yet:** Make sure the Space name is correct and wait for the build to complete.' if project_state.get('log_error_state') == 'Not Found' else ''}"
1231
  f"{' **Timeout:** Log fetch timed out.' if project_state.get('log_error_state') == 'TIMEOUT' else ''}"
1232
  f"{' **Iframe not ready:** Still building or error. Check logs.' if project_state.get('log_error_state') == 'Iframe Not Ready' else ''}"
1233
+ f"{' **Polling Error:** Encountered error fetching logs.' if project_state.get('log_error_state') in ['Request Error', 'Poller Unexpected Error', 'Iframe Check Error', 'HTTP Error'] else ''}"
1234
+ f"{' **Space Name Not Set:** Cannot fetch logs or check iframe.' if project_state.get('log_error_state') == 'Space Not Set' else ''}"
1235
+ f"{' Check build logs for details.' if project_state.get('log_error_state') not in ['Auth Failed', 'Auth Failed JWT', 'Auth Failed STREAM', 'Not Found', 'Space Not Set'] else ''}" # Suggest checking logs if no specific critical error
1236
  f"</p>"),
1237
  project_state.get('status_message', '...'), # Status message output (get from state)
1238
  )
 
1256
  models_md = gr.Markdown()
1257
 
1258
  # Use api_name to make these callable via API if needed, but not strictly necessary for UI load
1259
+ # Ensure these load on initial page load
1260
  demo.load(show_profile, inputs=None, outputs=status_md) # api_name="load_profile" removed
1261
  demo.load(list_private_models, inputs=None, outputs=models_md) # api_name="load_models" removed
1262
 
1263
+ # Ensure these update on login button click
1264
  login_btn.click(
1265
  fn=show_profile,
1266
+ inputs=None, # OAuth auto-injected
1267
  outputs=status_md
1268
  )
1269
  login_btn.click(
1270
  fn=list_private_models,
1271
+ inputs=None, # OAuth auto-injected
1272
  outputs=models_md
1273
  )
1274
  # --- END LOGIN FIX ---
 
1303
  preview = gr.HTML("<p>App preview will load here when available.</p>")
1304
 
1305
  # --- Hidden poller (using Timer) ---
1306
+ # Polls every 2 seconds to update logs and preview
 
1307
  log_poller = gr.Timer(value=2, active=True, render=False)
1308
  # --- End hidden poller ---
1309
 
1310
  # handle_user_message is defined ABOVE this block
1311
  # The main button and submit handler now receive and return the project_state
1312
+ # INPUTS: relies on auto-injection of profile/token, then state, then other UI inputs
1313
  inputs_list = [
1314
+ project_state, # <-- Pass the state in (after auto-injected profile/token)
 
1315
  user_in, # Other UI inputs in order
1316
  sdk_choice,
1317
  api_key,
 
1319
  grounding,
1320
  temp,
1321
  max_tokens,
1322
+ # Note: login_btn is NOT in inputs, profile/token are auto-injected
1323
  # Note: chatbot is NOT in inputs, its history is managed via project_state['chat_history']
1324
+ ] # Total 8 components listed here, plus 2 auto-injected = 10 arguments
1325
 
1326
  # OUTPUTS: Must match the return values of handle_user_message
1327
  outputs_list = [
 
1337
  fn=handle_user_message,
1338
  inputs=inputs_list,
1339
  outputs=outputs_list
1340
+ # No api_name needed unless you plan to call this via API directly
1341
  )
1342
 
1343
  user_in.submit(
1344
  fn=handle_user_message,
1345
  inputs=inputs_list,
1346
  outputs=outputs_list
1347
+ # No api_name needed
1348
  )
1349
 
1350
  # --- Wire the poller to the update function ---
1351
  # _update_logs_and_preview is defined ABOVE this block
1352
+ # The poller needs login state (as tuple), space name, and the current project state
1353
  # INPUTS: login_btn state (as tuple), space name, project state
1354
+ # When login_btn is the *first* component in the inputs list and the function's
1355
+ # first parameter is a tuple of the correct types, Gradio passes the outputs as a tuple.
1356
  poller_inputs = [
1357
+ login_btn, # Provides (profile, token) as a tuple because it's first
1358
  space_name,
1359
  project_state # Passes the current state value
1360
+ ] # Total 3 components -> function receives 3 arguments (1 tuple + 1 + 1)
1361
  # Note: _update_logs_and_preview signature is (profile_token_state, space_name, current_project_state)
1362
 
1363
  # OUTPUTS: Must match the return values of _update_logs_and_preview
 
1372
  fn=_update_logs_and_preview,
1373
  inputs=poller_inputs,
1374
  outputs=poller_outputs
1375
+ # No api_name needed
1376
  )
1377
  # --- End wire poller ---
1378
 
 
1384
  # If running on a Space, server_name='0.0.0.0' and server_port=7860 are standard.
1385
  # If running locally, just demo.launch() or specify a port.
1386
  if __name__ == "__main__":
1387
+ # Use `show_error=True` to see the actual Python errors in the UI during development
1388
+ demo.launch(server_name="0.0.0.0", server_port=7860, show_error=True)