akhaliq's picture
akhaliq HF Staff
updates
7f9535b
Raw History Blame Contribute Delete
105 kB
""" Gradio user interface for AnyCoder. Defines the main UI layout, components, and event handlers. """ import os import gradio as gr from typing import Dict, Optional from huggingface_hub import HfApi import httpx # Monkey-patch httpx to increase timeout for OAuth # This prevents ReadTimeout errors during HuggingFace OAuth flow _original_client_init = httpx.AsyncClient.__init__ def _patched_client_init(self, *args, **kwargs): # If no timeout is specified, use longer timeouts if 'timeout' not in kwargs: kwargs['timeout'] = httpx.Timeout( connect=30.0, # 30 seconds for connection read=60.0, # 60 seconds for reading response (increased from default 5s) write=30.0, # 30 seconds for writing pool=30.0 # 30 seconds for pool operations ) return _original_client_init(self, *args, **kwargs) httpx.AsyncClient.__init__ = _patched_client_init from .config import ( AVAILABLE_MODELS, DEFAULT_MODEL, DEFAULT_MODEL_NAME, LANGUAGE_CHOICES, get_gradio_language ) from .themes import THEME_CONFIGS, get_saved_theme, current_theme from .prompts import HTML_SYSTEM_PROMPT from .models import history_to_chatbot_messages from .parsers import ( history_render, clear_history, create_multimodal_message, parse_multipage_html_output, parse_transformers_js_output, parse_react_output, format_transformers_js_output, validate_and_autofix_files, parse_multi_file_python_output, is_streamlit_code, is_gradio_code ) from .deploy import ( check_authentication, update_ui_for_auth_status, generation_code, deploy_to_spaces, add_anycoder_tag_to_readme, _parse_repo_or_model_url, load_project_from_url, check_hf_space_url, import_repo_to_app, extract_import_statements, generate_requirements_txt_with_llm, prettify_comfyui_json_for_html, get_trending_models, import_model_from_hf, get_trending_spaces, import_space_from_hf, switch_model_code_type ) from .agent import ( agent_generate_with_questions, agent_process_answers_and_generate ) # Main application with proper Gradio theming with gr.Blocks( title="AnyCoder - AI Code Generator", theme=current_theme, css=""" .theme-info { font-size: 0.9em; opacity: 0.8; } .theme-description { padding: 8px 0; } .theme-status { padding: 10px; border-radius: 8px; background: rgba(34, 197, 94, 0.1); border: 1px solid rgba(34, 197, 94, 0.2); margin: 8px 0; } .restart-needed { padding: 12px; border-radius: 8px; background: rgba(255, 193, 7, 0.1); border: 1px solid rgba(255, 193, 7, 0.3); margin: 8px 0; text-align: center; } /* Authentication status styling */ .auth-status { padding: 8px 12px; border-radius: 6px; margin: 8px 0; font-weight: 500; text-align: center; } .auth-status:has-text("πŸ”’") { background: rgba(231, 76, 60, 0.1); border: 1px solid rgba(231, 76, 60, 0.3); color: #e74c3c; } .auth-status:has-text("βœ…") { background: rgba(46, 204, 113, 0.1); border: 1px solid rgba(46, 204, 113, 0.3); color: #2ecc71; } /* App link styling (visible on all devices) */ .app-link { display: block; padding: 12px; border-radius: 8px; background: rgba(59, 130, 246, 0.1); border: 1px solid rgba(59, 130, 246, 0.3); margin: 12px 0; text-align: center; } """ ) as demo: history = gr.State([]) setting = gr.State({ "system": HTML_SYSTEM_PROMPT, }) current_model = gr.State(DEFAULT_MODEL) open_panel = gr.State(None) last_login_state = gr.State(None) models_first_change = gr.State(True) spaces_first_change = gr.State(True) agent_mode_enabled = gr.State(False) current_trending_model_id = gr.State("") # Track current trending model for code switching agent_conversation_state = gr.State({ "stage": "initial", # initial, waiting_for_answers, generating "original_query": "", "questions": "" }) with gr.Sidebar() as sidebar: login_button = gr.LoginButton() # App link (visible on all devices) mobile_link = gr.HTML( """ <div class="app-link"> πŸ“± <strong>Using Mobile?</strong><br/> <a href="https://akhaliq-anycoder.hf.space" target="_blank" style="color: #007bff; text-decoration: underline;"> Use the app here </a> </div> """, visible=True ) # Unified Import section import_header_md = gr.Markdown("πŸ“₯ Import Project (Space, GitHub, or Model)", visible=False) load_project_url = gr.Textbox( label="Project URL", placeholder="https://huggingface.co/spaces/user/space OR https://huggingface.co/user/model OR https://github.com/owner/repo", lines=1 , visible=False) load_project_btn = gr.Button("πŸ“₯ Import Project", variant="secondary", size="sm", visible=True) load_project_status = gr.Markdown(visible=False) # Trending HuggingFace Models section trending_models_dropdown = gr.Dropdown( label="πŸ”₯ Trending HuggingFace Models", choices=[], # Will be populated on load value=None, interactive=True, visible=True ) trending_models_status = gr.Markdown(visible=False) switch_model_code_btn = gr.Button("πŸ”„ Switch Code Type", visible=False, size="sm", variant="secondary") # Trending HuggingFace Spaces section trending_spaces_dropdown = gr.Dropdown( label="πŸš€ Trending HuggingFace Spaces", choices=[], # Will be populated on load value=None, interactive=True, visible=True ) trending_spaces_status = gr.Markdown(visible=False) # Chat history display in sidebar chat_history = gr.Chatbot( label="Conversation History", type="messages", height=300, show_copy_button=True, visible=True ) # Input textbox for new messages input = gr.Textbox( label="What would you like to build?", placeholder="πŸ”’ Please log in with Hugging Face to use AnyCoder...", lines=2, visible=True, interactive=False ) # Language dropdown for code generation (add Streamlit and Gradio as first-class options) language_choices = [ "html", "gradio", "transformers.js", "streamlit", "comfyui", "react" ] language_dropdown = gr.Dropdown( choices=language_choices, value="html", label="Code Language", visible=True ) # Agent mode checkbox agent_mode_checkbox = gr.Checkbox( label="πŸ€– Enable Agent Mode", value=False, info="Agent will ask follow-up questions and create a task list before coding", visible=True ) # Removed image generation components with gr.Row(): btn = gr.Button("Generate", variant="secondary", size="lg", scale=2, visible=True, interactive=False) clear_btn = gr.Button("Clear", variant="secondary", size="sm", scale=1, visible=True) # --- Deploy components (visible by default) --- deploy_header_md = gr.Markdown("", visible=False) deploy_btn = gr.Button("Publish", variant="primary", visible=True) deploy_status = gr.Markdown(visible=False, label="Deploy status") # --- End move --- # Removed media generation and web search UI components # Removed media generation toggle event handlers model_dropdown = gr.Dropdown( choices=[model['name'] for model in AVAILABLE_MODELS], value=DEFAULT_MODEL_NAME, label="Model", visible=True ) provider_state = gr.State("auto") # Removed web search availability indicator def on_model_change(model_name): for m in AVAILABLE_MODELS: if m['name'] == model_name: return m return AVAILABLE_MODELS[0] def save_prompt(input): return {setting: {"system": input}} model_dropdown.change( lambda model_name: on_model_change(model_name), inputs=model_dropdown, outputs=[current_model] ) # --- Remove deploy/app name/sdk from bottom column --- # (delete the gr.Column() block containing space_name_input, sdk_dropdown, deploy_btn, deploy_status) with gr.Column() as main_column: with gr.Tabs(): with gr.Tab("Code"): code_output = gr.Code( language="html", lines=25, interactive=True, label="Generated code" ) # Transformers.js multi-file editors (hidden by default) with gr.Group(visible=False) as tjs_group: with gr.Tabs(): with gr.Tab("index.html"): tjs_html_code = gr.Code(language="html", lines=20, interactive=True, label="index.html") with gr.Tab("index.js"): tjs_js_code = gr.Code(language="javascript", lines=20, interactive=True, label="index.js") with gr.Tab("style.css"): tjs_css_code = gr.Code(language="css", lines=20, interactive=True, label="style.css") # Python multi-file editors (hidden by default) for Gradio/Streamlit with gr.Group(visible=False) as python_group_2: with gr.Tabs(): with gr.Tab("app.py") as python_tab_2_1: python_code_2_1 = gr.Code(language="python", lines=20, interactive=True, label="app.py") with gr.Tab("file 2") as python_tab_2_2: python_code_2_2 = gr.Code(language="python", lines=18, interactive=True, label="file 2") with gr.Group(visible=False) as python_group_3: with gr.Tabs(): with gr.Tab("app.py") as python_tab_3_1: python_code_3_1 = gr.Code(language="python", lines=20, interactive=True, label="app.py") with gr.Tab("file 2") as python_tab_3_2: python_code_3_2 = gr.Code(language="python", lines=18, interactive=True, label="file 2") with gr.Tab("file 3") as python_tab_3_3: python_code_3_3 = gr.Code(language="python", lines=18, interactive=True, label="file 3") with gr.Group(visible=False) as python_group_4: with gr.Tabs(): with gr.Tab("app.py") as python_tab_4_1: python_code_4_1 = gr.Code(language="python", lines=20, interactive=True, label="app.py") with gr.Tab("file 2") as python_tab_4_2: python_code_4_2 = gr.Code(language="python", lines=18, interactive=True, label="file 2") with gr.Tab("file 3") as python_tab_4_3: python_code_4_3 = gr.Code(language="python", lines=18, interactive=True, label="file 3") with gr.Tab("file 4") as python_tab_4_4: python_code_4_4 = gr.Code(language="python", lines=18, interactive=True, label="file 4") with gr.Group(visible=False) as python_group_5plus: with gr.Tabs(): with gr.Tab("app.py") as python_tab_5_1: python_code_5_1 = gr.Code(language="python", lines=20, interactive=True, label="app.py") with gr.Tab("file 2") as python_tab_5_2: python_code_5_2 = gr.Code(language="python", lines=18, interactive=True, label="file 2") with gr.Tab("file 3") as python_tab_5_3: python_code_5_3 = gr.Code(language="python", lines=18, interactive=True, label="file 3") with gr.Tab("file 4") as python_tab_5_4: python_code_5_4 = gr.Code(language="python", lines=18, interactive=True, label="file 4") with gr.Tab("file 5") as python_tab_5_5: python_code_5_5 = gr.Code(language="python", lines=18, interactive=True, label="file 5") # Static HTML multi-file editors (hidden by default). Use separate tab groups for different file counts. with gr.Group(visible=False) as static_group_2: with gr.Tabs(): with gr.Tab("index.html") as static_tab_2_1: static_code_2_1 = gr.Code(language="html", lines=20, interactive=True, label="index.html") with gr.Tab("file 2") as static_tab_2_2: static_code_2_2 = gr.Code(language="html", lines=18, interactive=True, label="file 2") with gr.Group(visible=False) as static_group_3: with gr.Tabs(): with gr.Tab("index.html") as static_tab_3_1: static_code_3_1 = gr.Code(language="html", lines=20, interactive=True, label="index.html") with gr.Tab("file 2") as static_tab_3_2: static_code_3_2 = gr.Code(language="html", lines=18, interactive=True, label="file 2") with gr.Tab("file 3") as static_tab_3_3: static_code_3_3 = gr.Code(language="html", lines=18, interactive=True, label="file 3") with gr.Group(visible=False) as static_group_4: with gr.Tabs(): with gr.Tab("index.html") as static_tab_4_1: static_code_4_1 = gr.Code(language="html", lines=20, interactive=True, label="index.html") with gr.Tab("file 2") as static_tab_4_2: static_code_4_2 = gr.Code(language="html", lines=18, interactive=True, label="file 2") with gr.Tab("file 3") as static_tab_4_3: static_code_4_3 = gr.Code(language="html", lines=18, interactive=True, label="file 3") with gr.Tab("file 4") as static_tab_4_4: static_code_4_4 = gr.Code(language="html", lines=18, interactive=True, label="file 4") with gr.Group(visible=False) as static_group_5plus: with gr.Tabs(): with gr.Tab("index.html") as static_tab_5_1: static_code_5_1 = gr.Code(language="html", lines=20, interactive=True, label="index.html") with gr.Tab("file 2") as static_tab_5_2: static_code_5_2 = gr.Code(language="html", lines=18, interactive=True, label="file 2") with gr.Tab("file 3") as static_tab_5_3: static_code_5_3 = gr.Code(language="html", lines=18, interactive=True, label="file 3") with gr.Tab("file 4") as static_tab_5_4: static_code_5_4 = gr.Code(language="html", lines=18, interactive=True, label="file 4") with gr.Tab("file 5") as static_tab_5_5: static_code_5_5 = gr.Code(language="html", lines=18, interactive=True, label="file 5") # React Next.js multi-file editors (hidden by default) with gr.Group(visible=False) as react_group: with gr.Tabs(): with gr.Tab("Dockerfile"): react_code_dockerfile = gr.Code(language="dockerfile", lines=15, interactive=True, label="Dockerfile") with gr.Tab("package.json"): react_code_package_json = gr.Code(language="json", lines=20, interactive=True, label="package.json") with gr.Tab("next.config.js"): react_code_next_config = gr.Code(language="javascript", lines=15, interactive=True, label="next.config.js") with gr.Tab("postcss.config.js"): react_code_postcss_config = gr.Code(language="javascript", lines=10, interactive=True, label="postcss.config.js") with gr.Tab("tailwind.config.js"): react_code_tailwind_config = gr.Code(language="javascript", lines=15, interactive=True, label="tailwind.config.js") with gr.Tab("pages/_app.js"): react_code_pages_app = gr.Code(language="javascript", lines=15, interactive=True, label="pages/_app.js") with gr.Tab("pages/index.js"): react_code_pages_index = gr.Code(language="javascript", lines=20, interactive=True, label="pages/index.js") with gr.Tab("components/ChatApp.jsx"): react_code_components = gr.Code(language="javascript", lines=25, interactive=True, label="components/ChatApp.jsx") with gr.Tab("styles/globals.css"): react_code_styles = gr.Code(language="css", lines=20, interactive=True, label="styles/globals.css") # Removed Import Logs tab for cleaner UI # History tab hidden per user request # with gr.Tab("History"): # history_output = gr.Chatbot(show_label=False, height=400, type="messages") # Keep history_output as hidden component to maintain functionality history_output = gr.Chatbot(show_label=False, height=400, type="messages", visible=False) # Global generation status view (disabled placeholder) generating_status = gr.Markdown("", visible=False) # Unified import handler def handle_import_project(url): if not url.strip(): return [ gr.update(value="Please enter a URL.", visible=True), gr.update(), gr.update(), [], [], gr.update(value="Publish", visible=False), gr.update(), # keep import header as-is gr.update(), # keep import button as-is gr.update(), # language dropdown - no change [] # chat_history ] kind, meta = _parse_repo_or_model_url(url) if kind == "hf_space": status, code = load_project_from_url(url) # Extract space info for deployment is_valid, username, project_name = check_hf_space_url(url) space_name = f"{username}/{project_name}" if is_valid else "" loaded_history = [[f"Imported Space from {url}", code]] # Determine the correct language/framework based on the imported content code_lang = "html" # default framework_type = "html" # for language dropdown # Check imports to determine framework for Python code if is_streamlit_code(code): code_lang = "python" framework_type = "streamlit" elif is_gradio_code(code): code_lang = "python" framework_type = "gradio" elif "=== index.html ===" in code and "=== index.js ===" in code and "=== style.css ===" in code: # This is a transformers.js app with the combined format code_lang = "html" # Use html for code display framework_type = "transformers.js" # But set dropdown to transformers.js elif ("import " in code or "def " in code) and not ("<!DOCTYPE html>" in code or "<html" in code): # This looks like Python code but doesn't match Streamlit/Gradio patterns # Default to Gradio for Python spaces code_lang = "python" framework_type = "gradio" # Return the updates with proper language settings return [ gr.update(value=status, visible=True), gr.update(value=code, language=code_lang), # Use html for transformers.js display gr.update(value="", visible=False), # hide import textbox after submit loaded_history, history_to_chatbot_messages(loaded_history), gr.update(value="Publish", visible=True), gr.update(visible=False), # hide import header gr.update(visible=False), # hide import button gr.update(value=framework_type), # set language dropdown to framework type history_to_chatbot_messages(loaded_history) # chat_history ] else: # GitHub or HF model β†’ return raw snippet for LLM starting point status, code, _ = import_repo_to_app(url) loaded_history = [[f"Imported Repo/Model from {url}", code]] code_lang = "python" framework_type = "gradio" # Default to gradio for Python code lower = (code or "").lower() if code.strip().startswith("<!doctype html>") or code.strip().startswith("<html"): code_lang = "html" framework_type = "html" elif "```json" in lower: code_lang = "json" framework_type = "json" return [ gr.update(value=status, visible=True), gr.update(value=code, language=code_lang), gr.update(value="", visible=False), # hide import textbox after submit loaded_history, history_to_chatbot_messages(loaded_history), gr.update(value="Publish", visible=False), gr.update(visible=False), # hide import header gr.update(visible=False), # hide import button gr.update(value=framework_type), # set language dropdown to detected language history_to_chatbot_messages(loaded_history) # chat_history ] # Import repo/model handler def handle_import_repo(url, framework): status, code, preview = import_repo_to_app(url, framework) # Heuristically set editor language based on snippet fencing or content code_lang = "python" lowered = (code or "").lower() if code.strip().startswith("<!doctype html>") or code.strip().startswith("<html"): code_lang = "html" elif "import gradio" in lowered or "from gradio" in lowered: code_lang = "python" elif "streamlit as st" in lowered or "import streamlit" in lowered: code_lang = "python" elif "from transformers" in lowered or "import transformers" in lowered: code_lang = "python" elif "from diffusers" in lowered or "import diffusers" in lowered: code_lang = "python" return [ gr.update(value=status, visible=True), gr.update(value=code, language=code_lang), gr.update(value=""), gr.update(value=f"URL: {url}\n\n{status}"), ] # Event handlers def update_code_language(language): return gr.update(language=get_gradio_language(language)) language_dropdown.change(update_code_language, inputs=language_dropdown, outputs=code_output) # Toggle single vs multi-file editors for transformers.js and populate when switching def toggle_editors(language, code_text): if language == "transformers.js": files = parse_transformers_js_output(code_text or "") # Hide multi-file editors until all files exist; show single code until then editors_visible = True if (files.get('index.html') and files.get('index.js') and files.get('style.css')) else False return [ gr.update(visible=not editors_visible), # code_output shown if editors hidden gr.update(visible=editors_visible), # tjs_group shown only when complete gr.update(value=files.get('index.html', '')), gr.update(value=files.get('index.js', '')), gr.update(value=files.get('style.css', '')), # React group hidden gr.update(visible=False), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), ] elif language == "react": files = parse_react_output(code_text or "") # Show react group if we have files, else show single code editor editors_visible = True if files else False if editors_visible: return [ gr.update(visible=False), # code_output hidden gr.update(visible=False), # tjs_group hidden gr.update(), gr.update(), gr.update(), # React group shown gr.update(visible=editors_visible), # react_group shown gr.update(value=files.get('Dockerfile', '')), gr.update(value=files.get('package.json', '')), gr.update(value=files.get('next.config.js', '')), gr.update(value=files.get('postcss.config.js', '')), gr.update(value=files.get('tailwind.config.js', '')), gr.update(value=files.get('pages/_app.js', '')), gr.update(value=files.get('pages/index.js', '')), gr.update(value=files.get('components/ChatApp.jsx', '')), gr.update(value=files.get('styles/globals.css', '')), ] else: return [ gr.update(visible=True), # code_output shown gr.update(visible=False), # tjs_group hidden gr.update(), gr.update(), gr.update(), # React group hidden gr.update(visible=False), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), ] else: return [ gr.update(visible=True), # code_output shown gr.update(visible=False), # tjs_group hidden gr.update(), gr.update(), gr.update(), # React group hidden gr.update(visible=False), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), ] language_dropdown.change( toggle_editors, inputs=[language_dropdown, code_output], outputs=[code_output, tjs_group, tjs_html_code, tjs_js_code, tjs_css_code, react_group, react_code_dockerfile, react_code_package_json, react_code_next_config, react_code_postcss_config, react_code_tailwind_config, react_code_pages_app, react_code_pages_index, react_code_components, react_code_styles], ) # Toggle Python multi-file editors for Gradio/Streamlit def toggle_python_editors(language, code_text): if language not in ["gradio", "streamlit"]: return [ gr.update(visible=True), # code_output gr.update(visible=False), # python_group_2 gr.update(visible=False), # python_group_3 gr.update(visible=False), # python_group_4 gr.update(visible=False), # python_group_5plus # All tab and code components get empty updates gr.update(), gr.update(), gr.update(), gr.update(), # 2-file group gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), # 3-file group gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), # 4-file group gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update() # 5-file group ] files = parse_multi_file_python_output(code_text or "") if not isinstance(files, dict) or len(files) <= 1: # No multi-file content; keep single editor return [ gr.update(visible=True), # code_output gr.update(visible=False), # python_group_2 gr.update(visible=False), # python_group_3 gr.update(visible=False), # python_group_4 gr.update(visible=False), # python_group_5plus # All tab and code components get empty updates gr.update(), gr.update(), gr.update(), gr.update(), # 2-file group gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), # 3-file group gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), # 4-file group gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update() # 5-file group ] # We have multi-file Python output: hide single editor, show appropriate group # Order: main app first, then others sorted by name ordered_paths = [] main_files = ['app.py', 'streamlit_app.py', 'main.py'] for main_file in main_files: if main_file in files: ordered_paths.append(main_file) break for p in sorted(files.keys()): if p not in ordered_paths: ordered_paths.append(p) num_files = len(ordered_paths) # Hide single editor, show appropriate group based on file count updates = [gr.update(visible=False)] # code_output if num_files == 2: updates.extend([ gr.update(visible=True), # python_group_2 gr.update(visible=False), # python_group_3 gr.update(visible=False), # python_group_4 gr.update(visible=False), # python_group_5plus ]) # Populate 2-file group path1, path2 = ordered_paths[0], ordered_paths[1] updates.extend([ gr.update(label=path1), gr.update(value=files.get(path1, ''), label=path1, language="python"), gr.update(label=path2), gr.update(value=files.get(path2, ''), label=path2, language="python"), # Empty updates for unused groups gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update() ]) elif num_files == 3: updates.extend([ gr.update(visible=False), # python_group_2 gr.update(visible=True), # python_group_3 gr.update(visible=False), # python_group_4 gr.update(visible=False), # python_group_5plus ]) # Populate 3-file group path1, path2, path3 = ordered_paths[0], ordered_paths[1], ordered_paths[2] updates.extend([ # Empty updates for 2-file group gr.update(), gr.update(), gr.update(), gr.update(), # Populate 3-file group gr.update(label=path1), gr.update(value=files.get(path1, ''), label=path1, language="python"), gr.update(label=path2), gr.update(value=files.get(path2, ''), label=path2, language="python"), gr.update(label=path3), gr.update(value=files.get(path3, ''), label=path3, language="python"), # Empty updates for unused groups gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update() ]) elif num_files == 4: updates.extend([ gr.update(visible=False), # python_group_2 gr.update(visible=False), # python_group_3 gr.update(visible=True), # python_group_4 gr.update(visible=False), # python_group_5plus ]) # Populate 4-file group paths = ordered_paths[:4] updates.extend([ # Empty updates for 2-file and 3-file groups gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), # Populate 4-file group gr.update(label=paths[0]), gr.update(value=files.get(paths[0], ''), label=paths[0], language="python"), gr.update(label=paths[1]), gr.update(value=files.get(paths[1], ''), label=paths[1], language="python"), gr.update(label=paths[2]), gr.update(value=files.get(paths[2], ''), label=paths[2], language="python"), gr.update(label=paths[3]), gr.update(value=files.get(paths[3], ''), label=paths[3], language="python"), # Empty updates for 5-file group gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update() ]) else: # 5+ files updates.extend([ gr.update(visible=False), # python_group_2 gr.update(visible=False), # python_group_3 gr.update(visible=False), # python_group_4 gr.update(visible=True), # python_group_5plus ]) # Populate 5-file group (show first 5 files) paths = ordered_paths[:5] updates.extend([ # Empty updates for 2-file, 3-file, and 4-file groups gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), # Populate 5-file group gr.update(label=paths[0]), gr.update(value=files.get(paths[0], ''), label=paths[0], language="python"), gr.update(label=paths[1]), gr.update(value=files.get(paths[1], ''), label=paths[1], language="python"), gr.update(label=paths[2]), gr.update(value=files.get(paths[2], ''), label=paths[2], language="python"), gr.update(label=paths[3]), gr.update(value=files.get(paths[3], ''), label=paths[3], language="python"), gr.update(label=paths[4]), gr.update(value=files.get(paths[4], ''), label=paths[4], language="python"), ]) return updates language_dropdown.change( toggle_python_editors, inputs=[language_dropdown, code_output], outputs=[ code_output, python_group_2, python_group_3, python_group_4, python_group_5plus, python_tab_2_1, python_code_2_1, python_tab_2_2, python_code_2_2, python_tab_3_1, python_code_3_1, python_tab_3_2, python_code_3_2, python_tab_3_3, python_code_3_3, python_tab_4_1, python_code_4_1, python_tab_4_2, python_code_4_2, python_tab_4_3, python_code_4_3, python_tab_4_4, python_code_4_4, python_tab_5_1, python_code_5_1, python_tab_5_2, python_code_5_2, python_tab_5_3, python_code_5_3, python_tab_5_4, python_code_5_4, python_tab_5_5, python_code_5_5 ], ) # Static HTML multi-file toggling and population def toggle_static_editors(language, code_text): # If not static HTML language, ensure single editor visible and all static groups hidden if language != "html": return [ gr.update(visible=True), # code_output gr.update(visible=False), # static_group_2 gr.update(visible=False), # static_group_3 gr.update(visible=False), # static_group_4 gr.update(visible=False), # static_group_5plus # All tab and code components get empty updates (tab, code, tab, code, ...) gr.update(), gr.update(), gr.update(), gr.update(), # 2-file group gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), # 3-file group gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), # 4-file group gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update() # 5-file group ] # Parse multi-file output first original_files = parse_multipage_html_output(code_text or "") # Check if we actually have multi-file content BEFORE validation # (validate_and_autofix_files can create additional files from single-file HTML) if not isinstance(original_files, dict) or len(original_files) <= 1: # No genuine multi-file content; keep single editor return [ gr.update(visible=True), # code_output gr.update(visible=False), # static_group_2 gr.update(visible=False), # static_group_3 gr.update(visible=False), # static_group_4 gr.update(visible=False), # static_group_5plus # All tab and code components get empty updates (tab, code, tab, code, ...) gr.update(), gr.update(), gr.update(), gr.update(), # 2-file group gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), # 3-file group gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), # 4-file group gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update() # 5-file group ] # We have genuine multi-file content - now validate and proceed with multi-file display files = validate_and_autofix_files(original_files) # We have multi-file static output: hide single editor, show appropriate static group # Order: index.html first, then others sorted by path ordered_paths = [] if 'index.html' in files: ordered_paths.append('index.html') for p in sorted(files.keys()): if p == 'index.html': continue ordered_paths.append(p) # Map extension to language def _lang_for(path: str): p = (path or '').lower() if p.endswith('.html'): return 'html' if p.endswith('.css'): return 'css' if p.endswith('.js'): return 'javascript' if p.endswith('.json'): return 'json' if p.endswith('.md') or p.endswith('.markdown'): return 'markdown' return 'html' num_files = len(ordered_paths) # TEMPORARY FIX: For now, always keep single editor visible for HTML multi-file # This ensures code is always visible while we debug the multi-file editors # TODO: Remove this once multi-file editors are working properly updates = [ gr.update(visible=True), # code_output - keep visible gr.update(visible=False), # static_group_2 - hide multi-file editors for now gr.update(visible=False), # static_group_3 gr.update(visible=False), # static_group_4 gr.update(visible=False), # static_group_5plus ] # Add empty updates for all the tab and code components updates.extend([ # All tab and code components get empty updates (tab, code, tab, code, ...) gr.update(), gr.update(), gr.update(), gr.update(), # 2-file group gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), # 3-file group gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), # 4-file group gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update() # 5-file group ]) return updates # Respond to language change to show/hide static multi-file editors appropriately language_dropdown.change( toggle_static_editors, inputs=[language_dropdown, code_output], outputs=[ code_output, static_group_2, static_group_3, static_group_4, static_group_5plus, static_tab_2_1, static_code_2_1, static_tab_2_2, static_code_2_2, static_tab_3_1, static_code_3_1, static_tab_3_2, static_code_3_2, static_tab_3_3, static_code_3_3, static_tab_4_1, static_code_4_1, static_tab_4_2, static_code_4_2, static_tab_4_3, static_code_4_3, static_tab_4_4, static_code_4_4, static_tab_5_1, static_code_5_1, static_tab_5_2, static_code_5_2, static_tab_5_3, static_code_5_3, static_tab_5_4, static_code_5_4, static_tab_5_5, static_code_5_5, ], ) def sync_tjs_from_code(code_text, language): if language != "transformers.js": return [gr.update(), gr.update(), gr.update(), gr.update()] files = parse_transformers_js_output(code_text or "") # Only reveal the multi-file editors when all three files are present editors_visible = True if (files.get('index.html') and files.get('index.js') and files.get('style.css')) else None return [ gr.update(value=files.get('index.html', '')), gr.update(value=files.get('index.js', '')), gr.update(value=files.get('style.css', '')), gr.update(visible=editors_visible) if editors_visible is not None else gr.update(), ] # Keep multi-file editors in sync when code_output changes and language is transformers.js code_output.change( sync_tjs_from_code, inputs=[code_output, language_dropdown], outputs=[tjs_html_code, tjs_js_code, tjs_css_code, tjs_group], ) # Preview functions removed - replaced with deployment messaging # The following functions are no longer used as preview has been removed: # - preview_logic: replaced with deployment messages # - preview_from_tjs_editors: replaced with deployment messages # - send_to_sandbox: still used in some places but could be removed in future cleanup # Show deployment message for transformers.js editors def show_tjs_deployment_message(*args): return """ <div style='padding: 1.5em; text-align: center; background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%); color: white; border-radius: 10px;'> <h3 style='margin-top: 0; color: white;'>πŸš€ Transformers.js App Ready!</h3> <p style='margin: 0.5em 0; opacity: 0.9;'>Your multi-file Transformers.js application is ready for deployment.</p> <p style='margin: 0.5em 0; font-weight: bold;'>πŸ‘‰ Use the Deploy button in the sidebar to publish your app!</p> </div> """ def show_deploy_components(*args): return gr.Button(visible=True) def hide_deploy_components(*args): return gr.Button(visible=True) # Show textbox when import button is clicked def toggle_import_textbox(url_visible): # If textbox is already visible and has content, proceed with import # Otherwise, just show the textbox return gr.update(visible=True) load_project_btn.click( fn=toggle_import_textbox, inputs=[load_project_url], outputs=[load_project_url] ).then( handle_import_project, inputs=[load_project_url], outputs=[ load_project_status, code_output, load_project_url, history, history_output, deploy_btn, import_header_md, load_project_btn, language_dropdown, chat_history, # Add chat_history to outputs ], ) def begin_generation_ui(agent_enabled): # In agent mode, keep sidebar open during question/task planning phase # Only close it when actual code generation starts if agent_enabled: return [gr.update(), gr.update(visible=False)] # Keep sidebar as-is else: # Normal mode: collapse sidebar immediately return [gr.update(open=False), gr.update(visible=False)] def end_generation_ui(): # Open sidebar after generation; hide the status return [gr.update(open=True), gr.update(visible=False)] def close_sidebar_for_coding(): # Close sidebar when transitioning to actual code generation return gr.update(open=False) def generation_code_wrapper(inp, sett, hist, model, lang, prov, agent_enabled, agent_state, profile: Optional[gr.OAuthProfile] = None, token: Optional[gr.OAuthToken] = None): """Wrapper to call generation_code or agent mode based on settings""" # Check if agent mode is enabled if agent_enabled and agent_state["stage"] == "initial": # Agent mode - first interaction, ask questions # Sidebar stays open during this phase for updated_hist, chatbot_msgs in agent_generate_with_questions( inp, sett, hist, model, lang, prov, profile, token ): # Update agent state to track that we're waiting for answers new_agent_state = { "stage": "waiting_for_answers", "original_query": inp, "questions": updated_hist[-1][1] if updated_hist else "" } # Yield: code_output, history, history_output, chat_history, agent_conversation_state, sidebar yield "", updated_hist, chatbot_msgs, chatbot_msgs, new_agent_state, gr.update() return elif agent_enabled and agent_state["stage"] == "waiting_for_answers": # Agent mode - user has answered questions, now create task list and generate code original_query = agent_state.get("original_query", "") questions = agent_state.get("questions", "") # Track whether we've started code generation to close sidebar started_code_generation = False # Process answers and generate code for result in agent_process_answers_and_generate( inp, original_query, questions, sett, hist, model, lang, prov, profile, token, code_output, history_output, history ): # Extract values from result dict code_val = result.get(code_output, "") hist_val = result.get(history, hist) history_output_val = result.get(history_output, []) # Reset agent state after generation reset_agent_state = { "stage": "initial", "original_query": "", "questions": "" } # Close sidebar when we start generating code (when code_output has content) if code_val and not started_code_generation: sidebar_update = gr.update(open=False) started_code_generation = True else: sidebar_update = gr.update() # Yield: code_output, history, history_output, chat_history, agent_conversation_state, sidebar yield code_val, hist_val, history_output_val, history_output_val, reset_agent_state, sidebar_update return else: # Normal mode - direct code generation # Sidebar was already closed by begin_generation_ui for result in generation_code(inp, sett, hist, model, lang, prov, profile, token, code_output, history_output, history): # generation_code yields dictionaries with component keys # Extract the values and yield them for our outputs code_val = result.get(code_output, "") hist_val = result.get(history, hist) history_output_val = result.get(history_output, []) # Yield for: code_output, history, history_output, chat_history, agent_conversation_state, sidebar yield code_val, hist_val, history_output_val, history_output_val, agent_state, gr.update() # Update agent_mode_enabled state when checkbox changes agent_mode_checkbox.change( lambda enabled: enabled, inputs=[agent_mode_checkbox], outputs=[agent_mode_enabled] ) btn.click( begin_generation_ui, inputs=[agent_mode_enabled], outputs=[sidebar, generating_status], show_progress="hidden", ).then( generation_code_wrapper, inputs=[input, setting, history, current_model, language_dropdown, provider_state, agent_mode_enabled, agent_conversation_state], outputs=[code_output, history, history_output, chat_history, agent_conversation_state, sidebar] ).then( end_generation_ui, inputs=None, outputs=[sidebar, generating_status] ).then( # After generation, toggle editors for transformers.js and populate toggle_editors, inputs=[language_dropdown, code_output], outputs=[code_output, tjs_group, tjs_html_code, tjs_js_code, tjs_css_code, react_group, react_code_dockerfile, react_code_package_json, react_code_next_config, react_code_postcss_config, react_code_tailwind_config, react_code_pages_app, react_code_pages_index, react_code_components, react_code_styles] ).then( # After generation, toggle static multi-file editors for HTML toggle_static_editors, inputs=[language_dropdown, code_output], outputs=[ code_output, static_group_2, static_group_3, static_group_4, static_group_5plus, static_tab_2_1, static_code_2_1, static_tab_2_2, static_code_2_2, static_tab_3_1, static_code_3_1, static_tab_3_2, static_code_3_2, static_tab_3_3, static_code_3_3, static_tab_4_1, static_code_4_1, static_tab_4_2, static_code_4_2, static_tab_4_3, static_code_4_3, static_tab_4_4, static_code_4_4, static_tab_5_1, static_code_5_1, static_tab_5_2, static_code_5_2, static_tab_5_3, static_code_5_3, static_tab_5_4, static_code_5_4, static_tab_5_5, static_code_5_5, ] ).then( # After generation, toggle Python multi-file editors for Gradio/Streamlit toggle_python_editors, inputs=[language_dropdown, code_output], outputs=[ code_output, python_group_2, python_group_3, python_group_4, python_group_5plus, python_tab_2_1, python_code_2_1, python_tab_2_2, python_code_2_2, python_tab_3_1, python_code_3_1, python_tab_3_2, python_code_3_2, python_tab_3_3, python_code_3_3, python_tab_4_1, python_code_4_1, python_tab_4_2, python_code_4_2, python_tab_4_3, python_code_4_3, python_tab_4_4, python_code_4_4, python_tab_5_1, python_code_5_1, python_tab_5_2, python_code_5_2, python_tab_5_3, python_code_5_3, python_tab_5_4, python_code_5_4, python_tab_5_5, python_code_5_5 ] ).then( show_deploy_components, None, [deploy_btn] ) # Pressing Enter in the main input should trigger generation and collapse the sidebar input.submit( begin_generation_ui, inputs=[agent_mode_enabled], outputs=[sidebar, generating_status], show_progress="hidden", ).then( generation_code_wrapper, inputs=[input, setting, history, current_model, language_dropdown, provider_state, agent_mode_enabled, agent_conversation_state], outputs=[code_output, history, history_output, chat_history, agent_conversation_state, sidebar] ).then( end_generation_ui, inputs=None, outputs=[sidebar, generating_status] ).then( # After generation, toggle editors for transformers.js and populate toggle_editors, inputs=[language_dropdown, code_output], outputs=[code_output, tjs_group, tjs_html_code, tjs_js_code, tjs_css_code, react_group, react_code_dockerfile, react_code_package_json, react_code_next_config, react_code_postcss_config, react_code_tailwind_config, react_code_pages_app, react_code_pages_index, react_code_components, react_code_styles] ).then( # After generation, toggle static multi-file editors for HTML toggle_static_editors, inputs=[language_dropdown, code_output], outputs=[ code_output, static_group_2, static_group_3, static_group_4, static_group_5plus, static_tab_2_1, static_code_2_1, static_tab_2_2, static_code_2_2, static_tab_3_1, static_code_3_1, static_tab_3_2, static_code_3_2, static_tab_3_3, static_code_3_3, static_tab_4_1, static_code_4_1, static_tab_4_2, static_code_4_2, static_tab_4_3, static_code_4_3, static_tab_4_4, static_code_4_4, static_tab_5_1, static_code_5_1, static_tab_5_2, static_code_5_2, static_tab_5_3, static_code_5_3, static_tab_5_4, static_code_5_4, static_tab_5_5, static_code_5_5, ] ).then( # After generation, toggle Python multi-file editors for Gradio/Streamlit toggle_python_editors, inputs=[language_dropdown, code_output], outputs=[ code_output, python_group_2, python_group_3, python_group_4, python_group_5plus, python_tab_2_1, python_code_2_1, python_tab_2_2, python_code_2_2, python_tab_3_1, python_code_3_1, python_tab_3_2, python_code_3_2, python_tab_3_3, python_code_3_3, python_tab_4_1, python_code_4_1, python_tab_4_2, python_code_4_2, python_tab_4_3, python_code_4_3, python_tab_4_4, python_code_4_4, python_tab_5_1, python_code_5_1, python_tab_5_2, python_code_5_2, python_tab_5_3, python_code_5_3, python_tab_5_4, python_code_5_4, python_tab_5_5, python_code_5_5 ] ).then( show_deploy_components, None, [deploy_btn] ) # --- Chat-based sidebar controller logic --- def _find_model_by_name(name: str): for m in AVAILABLE_MODELS: if m["name"].lower() == name.lower(): return m return None def _extract_url(text: str) -> Optional[str]: import re match = re.search(r"https?://[^\s]+", text or "") return match.group(0) if match else None # Show deployment message when code or language changes def show_deployment_message(code, language, *args): if not code or not code.strip(): return "<div style='padding:1em;color:#888;text-align:center;'>Generate some code to see deployment options.</div>" return f""" <div style='padding: 1.5em; text-align: center; background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%); color: white; border-radius: 10px;'> <h3 style='margin-top: 0; color: white;'>Ready to Deploy!</h3> <p style='margin: 0.5em 0; opacity: 0.9;'>Your {language.upper()} code is ready for deployment.</p> <p style='margin: 0.5em 0; font-weight: bold;'>πŸ‘‰ Use the Deploy button in the sidebar to publish your app!</p> </div> """ def reset_agent_state(): """Reset agent conversation state when clearing history""" return { "stage": "initial", "original_query": "", "questions": "" } clear_btn.click(clear_history, outputs=[history, history_output, chat_history]) clear_btn.click(hide_deploy_components, None, [deploy_btn]) clear_btn.click(reset_agent_state, outputs=[agent_conversation_state]) # Reset button text when clearing clear_btn.click( lambda: gr.update(value="Publish"), outputs=[deploy_btn] ) # Deploy to Spaces logic def generate_random_app_name(): """Generate a random app name that's unlikely to clash with existing apps""" import random import string # Common app prefixes prefixes = ["my", "cool", "awesome", "smart", "quick", "super", "mini", "auto", "fast", "easy"] # Common app suffixes suffixes = ["app", "tool", "hub", "space", "demo", "ai", "gen", "bot", "lab", "studio"] # Random adjectives adjectives = ["blue", "red", "green", "bright", "dark", "light", "swift", "bold", "clean", "fresh"] # Generate different patterns patterns = [ lambda: f"{random.choice(prefixes)}-{random.choice(suffixes)}-{random.randint(100, 999)}", lambda: f"{random.choice(adjectives)}-{random.choice(suffixes)}-{random.randint(10, 99)}", lambda: f"{random.choice(prefixes)}-{random.choice(adjectives)}-{random.choice(suffixes)}", lambda: f"app-{''.join(random.choices(string.ascii_lowercase, k=6))}-{random.randint(10, 99)}", lambda: f"{random.choice(suffixes)}-{''.join(random.choices(string.ascii_lowercase + string.digits, k=8))}" ] return random.choice(patterns)() def deploy_with_history_tracking( code, language, history, profile: Optional[gr.OAuthProfile] = None, token: Optional[gr.OAuthToken] = None ): """Wrapper function that handles history tracking for deployments""" # Check if we have a previously deployed space in the history username = profile.username if profile else None existing_space = None # Look for previous deployment or imported space in history if history and username: for user_msg, assistant_msg in history: if assistant_msg and "βœ… Deployed!" in assistant_msg: import re # Look for space URL pattern match = re.search(r'huggingface\.co/spaces/([^/\s\)]+/[^/\s\)]+)', assistant_msg) if match: existing_space = match.group(1) break elif assistant_msg and "βœ… Updated!" in assistant_msg: import re # Look for space URL pattern match = re.search(r'huggingface\.co/spaces/([^/\s\)]+/[^/\s\)]+)', assistant_msg) if match: existing_space = match.group(1) break elif user_msg and user_msg.startswith("Imported Space from"): import re # Extract space name from import message match = re.search(r'huggingface\.co/spaces/([^/\s\)]+/[^/\s\)]+)', user_msg) if match: imported_space = match.group(1) # Only use imported space if user owns it (can update it) if imported_space.startswith(f"{username}/"): existing_space = imported_space break # If user doesn't own the imported space, we'll create a new one # (existing_space remains None, triggering new deployment) # Call the original deploy function status = deploy_to_user_space_original(code, language, existing_space, profile, token) # Update history if deployment was successful updated_history = history if isinstance(status, dict) and "value" in status and "βœ…" in status["value"]: action_type = "Deploy" if "Deployed!" in status["value"] else "Update" if existing_space: updated_history = history + [[f"{action_type} {language} app to {existing_space}", status["value"]]] else: updated_history = history + [[f"{action_type} {language} app", status["value"]]] return [status, updated_history] def deploy_to_user_space_original( code, language, existing_space_name=None, # Pass existing space name if updating profile: Optional[gr.OAuthProfile] = None, token: Optional[gr.OAuthToken] = None ): import shutil if not code or not code.strip(): return gr.update(value="No code to deploy.", visible=True) if profile is None or token is None: return gr.update(value="Please log in with your Hugging Face account to deploy to your own Space. Otherwise, use the default deploy (opens in new tab).", visible=True) # Check if token has write permissions if not token.token or token.token == "hf_": return gr.update(value="Error: Invalid token. Please log in again with your Hugging Face account to get a valid write token.", visible=True) # Determine if this is an update or new deployment username = profile.username if existing_space_name and existing_space_name.startswith(f"{username}/"): # This is an update to existing space repo_id = existing_space_name space_name = existing_space_name.split('/')[-1] is_update = True else: # Generate a random space name for new deployment space_name = generate_random_app_name() repo_id = f"{username}/{space_name}" is_update = False # Map language to HF SDK slug language_to_sdk_map = { "gradio": "gradio", "streamlit": "docker", # Use 'docker' for Streamlit Spaces "react": "docker", # Use 'docker' for React/Next.js Spaces "html": "static", "transformers.js": "static", # Transformers.js uses static SDK "comfyui": "static" # ComfyUI uses static SDK } sdk = language_to_sdk_map.get(language, "gradio") # Create API client with user's token for proper authentication api = HfApi(token=token.token) # Only create the repo for new spaces (not updates) and non-Transformers.js, non-Streamlit SDKs if not is_update and sdk != "docker" and language not in ["transformers.js"]: try: api.create_repo( repo_id=repo_id, # e.g. username/space_name repo_type="space", space_sdk=sdk, # Use selected SDK exist_ok=True # Don't error if it already exists ) except Exception as e: return gr.update(value=f"Error creating Space: {e}", visible=True) # Streamlit/React/docker logic if sdk == "docker" and language in ["streamlit", "react"]: try: # For new spaces, create a fresh Docker-based space if not is_update: # Use create_repo to create a new Docker space from huggingface_hub import create_repo if language == "react": # Create a new React Docker space with docker SDK created_repo = create_repo( repo_id=repo_id, repo_type="space", space_sdk="docker", token=token.token, exist_ok=True ) else: # Create a new Streamlit Docker space created_repo = create_repo( repo_id=repo_id, repo_type="space", space_sdk="docker", token=token.token, exist_ok=True ) # Handle React or Streamlit deployment if language == "react": # Parse React/Next.js files files = parse_react_output(code) if not files: return gr.update(value="Error: Could not parse React output. Please regenerate the code.", visible=True) # If Dockerfile is missing, use template if 'Dockerfile' not in files: files['Dockerfile'] = """FROM node:18-slim # Use the existing node user (UID 1000) USER node # Set environment variables ENV HOME=/home/node \\ PATH=/home/node/.local/bin:$PATH # Set working directory WORKDIR /home/node/app # Copy package files with proper ownership COPY --chown=node:node package*.json ./ # Install dependencies RUN npm install # Copy rest of the application with proper ownership COPY --chown=node:node . . # Build the Next.js app RUN npm run build # Expose port 7860 EXPOSE 7860 # Start the application on port 7860 CMD ["npm", "start", "--", "-p", "7860"] """ # Upload React files import tempfile import time for file_name, file_content in files.items(): if not file_content: continue success = False last_error = None max_attempts = 3 for attempt in range(max_attempts): try: # Determine file extension if file_name == 'Dockerfile': suffix = '' else: suffix = f".{file_name.split('.')[-1]}" with tempfile.NamedTemporaryFile("w", suffix=suffix, delete=False) as f: f.write(file_content) temp_path = f.name api.upload_file( path_or_fileobj=temp_path, path_in_repo=file_name, repo_id=repo_id, repo_type="space" ) success = True break except Exception as e: last_error = e error_msg = str(e) if "403 Forbidden" in error_msg and "write token" in error_msg: return gr.update(value=f"Error: Permission denied. Please ensure you have write access to {repo_id} and your token has the correct permissions.", visible=True) if attempt < max_attempts - 1: time.sleep(2) finally: import os if 'temp_path' in locals(): os.unlink(temp_path) if not success: return gr.update(value=f"Error uploading {file_name}: {last_error}", visible=True) # Add anycoder tag and app_port to existing README add_anycoder_tag_to_readme(api, repo_id, app_port=7860) space_url = f"https://huggingface.co/spaces/{repo_id}" action_text = "Updated" if is_update else "Deployed" return gr.update(value=f"βœ… {action_text}! [Open your React Space here]({space_url})", visible=True) # Streamlit logic - Parse multi-file structure files = parse_multi_file_python_output(code) if not files: return gr.update(value="Error: Could not parse Streamlit output. Please regenerate the code.", visible=True) # Verify required files exist has_streamlit_app = 'streamlit_app.py' in files or 'app.py' in files has_requirements = 'requirements.txt' in files has_dockerfile = 'Dockerfile' in files if not has_streamlit_app: return gr.update(value="Error: Missing streamlit_app.py. Please regenerate the code.", visible=True) # If Dockerfile or requirements.txt is missing, generate them if not has_dockerfile: # Generate default Dockerfile files['Dockerfile'] = """FROM python:3.11-slim # Set up user with ID 1000 RUN useradd -m -u 1000 user USER user ENV HOME=/home/user \\ PATH=/home/user/.local/bin:$PATH # Set working directory WORKDIR $HOME/app # Copy requirements file with proper ownership COPY --chown=user requirements.txt . # Install dependencies RUN pip install --no-cache-dir -r requirements.txt # Copy application files with proper ownership COPY --chown=user . . # Expose port 7860 EXPOSE 7860 # Start Streamlit app CMD ["streamlit", "run", "streamlit_app.py", "--server.port=7860", "--server.address=0.0.0.0"] """ if not has_requirements: # Generate requirements.txt from imports in the main app file main_app = files.get('streamlit_app.py') or files.get('app.py', '') import_statements = extract_import_statements(main_app) files['requirements.txt'] = generate_requirements_txt_with_llm(import_statements) # Upload Streamlit files import tempfile import time for file_name, file_content in files.items(): if not file_content: continue success = False last_error = None max_attempts = 3 for attempt in range(max_attempts): try: # Determine file extension if file_name == 'Dockerfile': suffix = '' else: suffix = f".{file_name.split('.')[-1]}" with tempfile.NamedTemporaryFile("w", suffix=suffix, delete=False) as f: f.write(file_content) temp_path = f.name api.upload_file( path_or_fileobj=temp_path, path_in_repo=file_name, repo_id=repo_id, repo_type="space" ) success = True break except Exception as e: last_error = e error_msg = str(e) if "403 Forbidden" in error_msg and "write token" in error_msg: return gr.update(value=f"Error: Permission denied. Please ensure you have write access to {repo_id} and your token has the correct permissions.", visible=True) if attempt < max_attempts - 1: time.sleep(2) finally: import os if 'temp_path' in locals(): os.unlink(temp_path) if not success: return gr.update(value=f"Error uploading {file_name}: {last_error}", visible=True) # Add anycoder tag and app_port to existing README add_anycoder_tag_to_readme(api, repo_id, app_port=7860) space_url = f"https://huggingface.co/spaces/{repo_id}" action_text = "Updated" if is_update else "Deployed" return gr.update(value=f"βœ… {action_text}! [Open your Streamlit Space here]({space_url})", visible=True) except Exception as e: error_prefix = "Error duplicating Streamlit space" if not is_update else "Error updating Streamlit space" return gr.update(value=f"{error_prefix}: {e}", visible=True) # Transformers.js logic elif language == "transformers.js": try: # For new spaces, duplicate the template. For updates, just verify access. if not is_update: # Use duplicate_space to create a transformers.js template space from huggingface_hub import duplicate_space # Duplicate the transformers.js template space duplicated_repo = duplicate_space( from_id="static-templates/transformers.js", to_id=space_name.strip(), token=token.token, exist_ok=True ) print("Duplicated repo result:", duplicated_repo, type(duplicated_repo)) else: # For updates, verify we can access the existing space try: space_info = api.space_info(repo_id) if not space_info: return gr.update(value=f"Error: Could not access space {repo_id} for update.", visible=True) except Exception as e: return gr.update(value=f"Error: Cannot update space {repo_id}. {str(e)}", visible=True) # Parse the code parameter which should contain the formatted transformers.js output files = parse_transformers_js_output(code) if not files['index.html'] or not files['index.js'] or not files['style.css']: return gr.update(value="Error: Could not parse transformers.js output. Please regenerate the code.", visible=True) # Upload the three files to the space (with retry logic for reliability) import tempfile import time # Define files to upload files_to_upload = [ ("index.html", files['index.html']), ("index.js", files['index.js']), ("style.css", files['style.css']) ] # Upload each file with retry logic (similar to static HTML pattern) max_attempts = 3 for file_name, file_content in files_to_upload: success = False last_error = None for attempt in range(max_attempts): try: with tempfile.NamedTemporaryFile("w", suffix=f".{file_name.split('.')[-1]}", delete=False) as f: f.write(file_content) temp_path = f.name api.upload_file( path_or_fileobj=temp_path, path_in_repo=file_name, repo_id=repo_id, repo_type="space" ) success = True break except Exception as e: last_error = e error_msg = str(e) if "403 Forbidden" in error_msg and "write token" in error_msg: # Permission errors won't be fixed by retrying return gr.update(value=f"Error: Permission denied. Please ensure you have write access to {repo_id} and your token has the correct permissions.", visible=True) if attempt < max_attempts - 1: # Not the last attempt time.sleep(2) # Wait before retrying finally: import os if 'temp_path' in locals(): os.unlink(temp_path) if not success: return gr.update(value=f"Error uploading {file_name}: {last_error}", visible=True) # Add anycoder tag to existing README (for both new and update) add_anycoder_tag_to_readme(api, repo_id) # For updates, trigger a space restart to ensure changes take effect if is_update: try: api.restart_space(repo_id=repo_id) except Exception as restart_error: # Don't fail the deployment if restart fails, just log it print(f"Note: Could not restart space after update: {restart_error}") space_url = f"https://huggingface.co/spaces/{repo_id}" action_text = "Updated" if is_update else "Deployed" return gr.update(value=f"βœ… {action_text}! [Open your Transformers.js Space here]({space_url})", visible=True) except Exception as e: # Handle potential RepoUrl object errors error_msg = str(e) if "'url'" in error_msg or "RepoUrl" in error_msg: # For RepoUrl object issues, check if the space was actually created successfully try: # Check if space exists by trying to access it space_url = f"https://huggingface.co/spaces/{repo_id}" test_api = HfApi(token=token.token) space_exists = test_api.space_info(repo_id) if space_exists and not is_update: # Space was created successfully despite the RepoUrl error return gr.update(value=f"βœ… Deployed! Space was created successfully despite a technical error. [Open your Transformers.js Space here]({space_url})", visible=True) elif space_exists and is_update: # Space was updated successfully despite the RepoUrl error return gr.update(value=f"βœ… Updated! Space was updated successfully despite a technical error. [Open your Transformers.js Space here]({space_url})", visible=True) else: # Space doesn't exist, real error return gr.update(value=f"Error: Could not create/update space. Please try again manually at https://huggingface.co/new-space", visible=True) except: # Fallback to informative error with link repo_url = f"https://huggingface.co/spaces/{repo_id}" return gr.update(value=f"Error: Could not properly handle space creation response. Space may have been created successfully. Check: {repo_url}", visible=True) # General error handling for both creation and updates action_verb = "updating" if is_update else "duplicating" return gr.update(value=f"Error {action_verb} Transformers.js space: {error_msg}", visible=True) # Other SDKs (existing logic) if sdk == "static": import time # Add anycoder tag to existing README (after repo creation) add_anycoder_tag_to_readme(api, repo_id) # Detect whether the HTML output is multi-file (=== filename === blocks) files = {} parse_error = None try: files = parse_multipage_html_output(code) print(f"[Deploy] Parsed files: {list(files.keys())}") files = validate_and_autofix_files(files) print(f"[Deploy] After validation: {list(files.keys())}") except Exception as e: parse_error = str(e) print(f"[Deploy] Parse error: {parse_error}") files = {} # If we have multiple files (or at least a parsed index.html), upload the whole folder if isinstance(files, dict) and len(files) > 0 and files.get('index.html'): import tempfile import os try: with tempfile.TemporaryDirectory() as tmpdir: # Write each file preserving subdirectories if any for rel_path, content in files.items(): safe_rel_path = rel_path.strip().lstrip('/') abs_path = os.path.join(tmpdir, safe_rel_path) os.makedirs(os.path.dirname(abs_path), exist_ok=True) with open(abs_path, 'w') as fh: fh.write(content) # Upload the folder in a single commit api.upload_folder( folder_path=tmpdir, repo_id=repo_id, repo_type="space" ) space_url = f"https://huggingface.co/spaces/{repo_id}" action_text = "Updated" if is_update else "Deployed" return gr.update(value=f"{action_text}! [Open your app here]({space_url})", visible=True) except Exception as e: error_msg = str(e) if "403 Forbidden" in error_msg and "write token" in error_msg: return gr.update(value=f"Error: Permission denied. Please ensure you have write access to {repo_id} and your token has the correct permissions.", visible=True) else: return gr.update(value=f"Error uploading static app folder: {e}", visible=True) # Fallback: single-file static HTML (upload index.html only) file_name = "index.html" # Special handling for ComfyUI: prettify JSON and wrap in HTML if language == "comfyui": print("[Deploy] Converting ComfyUI JSON to prettified HTML display") code = prettify_comfyui_json_for_html(code) max_attempts = 3 for attempt in range(max_attempts): import tempfile with tempfile.NamedTemporaryFile("w", suffix=".html", delete=False) as f: f.write(code) temp_path = f.name try: api.upload_file( path_or_fileobj=temp_path, path_in_repo=file_name, repo_id=repo_id, repo_type="space" ) space_url = f"https://huggingface.co/spaces/{repo_id}" action_text = "Updated" if is_update else "Deployed" return gr.update(value=f"{action_text}! [Open your app here]({space_url})", visible=True) except Exception as e: error_msg = str(e) if "403 Forbidden" in error_msg and "write token" in error_msg: return gr.update(value=f"Error: Permission denied. Please ensure you have write access to {repo_id} and your token has the correct permissions.", visible=True) elif attempt < max_attempts - 1: time.sleep(2) else: return gr.update(value=f"Error uploading file after {max_attempts} attempts: {e}. Please check your permissions and try again.", visible=True) finally: import os os.unlink(temp_path) else: # Generate requirements.txt for Gradio apps and upload only if needed import_statements = extract_import_statements(code) requirements_content = generate_requirements_txt_with_llm(import_statements) import tempfile # Check if we need to upload requirements.txt should_upload_requirements = True if is_update: try: # Try to get existing requirements.txt content existing_requirements = api.hf_hub_download( repo_id=repo_id, filename="requirements.txt", repo_type="space" ) with open(existing_requirements, 'r') as f: existing_content = f.read().strip() # Compare with new content if existing_content == requirements_content.strip(): should_upload_requirements = False except Exception: # File doesn't exist or can't be accessed, so we should upload should_upload_requirements = True # Note: requirements.txt upload is now handled by the multi-file commit logic below # This ensures all files are committed atomically in a single operation # Add anycoder tag to existing README add_anycoder_tag_to_readme(api, repo_id) # Check if code contains multi-file format if ('=== app.py ===' in code or '=== requirements.txt ===' in code): # Parse multi-file format and upload each file separately files = parse_multi_file_python_output(code) if files: # Ensure requirements.txt is present - auto-generate if missing if 'app.py' in files and 'requirements.txt' not in files: import_statements = extract_import_statements(files['app.py']) requirements_content = generate_requirements_txt_with_llm(import_statements) files['requirements.txt'] = requirements_content try: from huggingface_hub import CommitOperationAdd operations = [] temp_files = [] # Create CommitOperation for each file for filename, content in files.items(): # Clean content to ensure no stray backticks are deployed cleaned_content = content if filename.endswith('.txt') or filename.endswith('.py'): # Additional safety: remove any standalone backtick lines lines = cleaned_content.split('\n') clean_lines = [] for line in lines: stripped = line.strip() # Skip lines that are just backticks if stripped == '```' or (stripped.startswith('```') and len(stripped) <= 10): continue clean_lines.append(line) cleaned_content = '\n'.join(clean_lines) # Create temporary file with tempfile.NamedTemporaryFile("w", suffix=f".{filename.split('.')[-1]}", delete=False) as f: f.write(cleaned_content) temp_path = f.name temp_files.append(temp_path) # Add to operations operations.append(CommitOperationAdd( path_in_repo=filename, path_or_fileobj=temp_path )) # Commit all files at once api.create_commit( repo_id=repo_id, operations=operations, commit_message=f"{'Update' if is_update else 'Deploy'} Gradio app with multiple files", repo_type="space" ) # Clean up temp files for temp_path in temp_files: try: os.unlink(temp_path) except Exception: pass space_url = f"https://huggingface.co/spaces/{repo_id}" action_text = "Updated" if is_update else "Deployed" return gr.update(value=f"βœ… {action_text}! [Open your Space here]({space_url})", visible=True) except Exception as e: # Clean up temp files on error for temp_path in temp_files: try: os.unlink(temp_path) except Exception: pass error_msg = str(e) if "403 Forbidden" in error_msg and "write token" in error_msg: return gr.update(value=f"Error: Permission denied. Please ensure you have write access to {repo_id} and your token has the correct permissions.", visible=True) else: return gr.update(value=f"Error uploading multi-file app: {e}", visible=True) else: # Fallback to single file if parsing failed pass # Single file upload (fallback or non-multi-file format) file_name = "app.py" with tempfile.NamedTemporaryFile("w", suffix=f".{file_name.split('.')[-1]}", delete=False) as f: f.write(code) temp_path = f.name try: api.upload_file( path_or_fileobj=temp_path, path_in_repo=file_name, repo_id=repo_id, repo_type="space" ) space_url = f"https://huggingface.co/spaces/{repo_id}" action_text = "Updated" if is_update else "Deployed" return gr.update(value=f"βœ… {action_text}! [Open your Space here]({space_url})", visible=True) except Exception as e: error_msg = str(e) if "403 Forbidden" in error_msg and "write token" in error_msg: return gr.update(value=f"Error: Permission denied. Please ensure you have write access to {repo_id} and your token has the correct permissions.", visible=True) else: return gr.update(value=f"Error uploading file: {e}", visible=True) finally: import os os.unlink(temp_path) # Connect the deploy button to the new function def gather_code_for_deploy(code_text, language, html_part, js_part, css_part): # When transformers.js is selected, ensure multi-file editors are used; otherwise, return single code if language == "transformers.js": # Join into a combined display string for auditing; actual deploy reads editor values directly files = { 'index.html': html_part or '', 'index.js': js_part or '', 'style.css': css_part or '', } if files['index.html'] and files['index.js'] and files['style.css']: return format_transformers_js_output(files) return code_text deploy_btn.click( gather_code_for_deploy, inputs=[code_output, language_dropdown, tjs_html_code, tjs_js_code, tjs_css_code], outputs=[code_output], queue=False, ).then( deploy_with_history_tracking, inputs=[code_output, language_dropdown, history], outputs=[deploy_status, history] ).then( lambda hist: history_to_chatbot_messages(hist), inputs=[history], outputs=[chat_history] ) # Keep the old deploy method as fallback (if not logged in, user can still use the old method) # Optionally, you can keep the old deploy_btn.click for the default method as a secondary button. # Handle authentication state updates # The LoginButton automatically handles OAuth flow and passes profile/token to the function def handle_auth_update(profile: Optional[gr.OAuthProfile] = None, token: Optional[gr.OAuthToken] = None): return update_ui_for_auth_status(profile, token) # Update UI when login button is clicked (handles both login and logout) login_button.click( handle_auth_update, inputs=[], outputs=[input, btn], queue=False ) # Also update UI when the page loads in case user is already authenticated demo.load( handle_auth_update, inputs=[], outputs=[input, btn], queue=False ) # Load trending models when app starts def load_trending_models(): """Load trending models from HuggingFace Hub""" models = get_trending_models(limit=10) # Create choices list with display names and values as model IDs choices = [(display, model_id) for display, model_id in models] # Set first model as default value if available default_value = models[0][1] if models and len(models) > 0 and models[0][1] != "" else None return gr.update(choices=choices, value=default_value) demo.load( load_trending_models, inputs=[], outputs=[trending_models_dropdown], queue=False ) # Load trending spaces when app starts def load_trending_spaces(): """Load trending spaces from HuggingFace Hub""" spaces = get_trending_spaces(limit=10) # Create choices list with display names and values as space IDs choices = [(display, space_id) for display, space_id in spaces] # Set first space as default value if available default_value = spaces[0][1] if spaces and len(spaces) > 0 and spaces[0][1] != "" else None return gr.update(choices=choices, value=default_value) demo.load( load_trending_spaces, inputs=[], outputs=[trending_spaces_dropdown], queue=False ) # Handle trending model selection def handle_trending_model_selection(model_id, hist, is_first): """Handle when user selects a trending model""" # Skip import on first change (when default value is set on load) if is_first: return [ gr.update(), # status gr.update(), # code_output gr.update(), # language_dropdown hist, # history history_to_chatbot_messages(hist), # history_output history_to_chatbot_messages(hist), # chat_history False, # Set first_change to False after first trigger gr.update(visible=False), # switch_model_code_btn "" # current_trending_model_id ] if not model_id or model_id == "": return [ gr.update(value="Please select a model.", visible=True), # status gr.update(), # code_output gr.update(), # language_dropdown hist, # history history_to_chatbot_messages(hist), # history_output history_to_chatbot_messages(hist), # chat_history False, # Keep first_change as False gr.update(visible=False), # switch_model_code_btn "" # current_trending_model_id ] # Import the model status, code, language, model_url = import_model_from_hf(model_id) # Add to history loaded_history = hist + [[f"Imported model: {model_id}", code]] # Determine code language for display code_lang = "python" # Check if button should be visible (both code types available) show_switch_btn = "Found multiple code options" in status return [ gr.update(value=status, visible=True), # status gr.update(value=code, language=code_lang), # code_output gr.update(value=language), # language_dropdown loaded_history, # history history_to_chatbot_messages(loaded_history), # history_output history_to_chatbot_messages(loaded_history), # chat_history False, # Keep first_change as False gr.update(visible=show_switch_btn), # switch_model_code_btn model_id # current_trending_model_id ] trending_models_dropdown.change( handle_trending_model_selection, inputs=[trending_models_dropdown, history, models_first_change], outputs=[ trending_models_status, code_output, language_dropdown, history, history_output, chat_history, models_first_change, switch_model_code_btn, current_trending_model_id ] ) # Handle switching between inference provider and local code def handle_switch_model_code(model_id, current_code, hist): """Switch between inference provider and local transformers/diffusers code""" if not model_id: return [ gr.update(), # status gr.update(), # code_output hist, # history history_to_chatbot_messages(hist), # history_output history_to_chatbot_messages(hist) # chat_history ] status_msg, new_code = switch_model_code_type(model_id, current_code) # Update history with switch message switch_history = hist + [[f"Switched code type for {model_id}", new_code]] return [ gr.update(value=status_msg, visible=True), # status gr.update(value=new_code, language="python"), # code_output switch_history, # history history_to_chatbot_messages(switch_history), # history_output history_to_chatbot_messages(switch_history) # chat_history ] switch_model_code_btn.click( handle_switch_model_code, inputs=[current_trending_model_id, code_output, history], outputs=[ trending_models_status, code_output, history, history_output, chat_history ] ) # Handle trending space selection def handle_trending_space_selection(space_id, hist, is_first): """Handle when user selects a trending space""" # Skip import on first change (when default value is set on load) if is_first: return [ gr.update(), # status gr.update(), # code_output gr.update(), # language_dropdown hist, # history history_to_chatbot_messages(hist), # history_output history_to_chatbot_messages(hist), # chat_history gr.update(), # deploy_btn False # Set first_change to False after first trigger ] if not space_id or space_id == "": return [ gr.update(value="Please select a space.", visible=True), # status gr.update(), # code_output gr.update(), # language_dropdown hist, # history history_to_chatbot_messages(hist), # history_output history_to_chatbot_messages(hist), # chat_history gr.update(visible=True), # deploy_btn False # Keep first_change as False ] # Import the space status, code, language, space_url = import_space_from_hf(space_id) # Add to history loaded_history = hist + [[f"Imported space: {space_id}", code]] # Determine code language for display based on framework if language == "gradio" or language == "streamlit": code_lang = "python" elif language == "transformers.js": code_lang = "html" else: code_lang = "html" return [ gr.update(value=status, visible=True), # status gr.update(value=code, language=code_lang), # code_output gr.update(value=language), # language_dropdown loaded_history, # history history_to_chatbot_messages(loaded_history), # history_output history_to_chatbot_messages(loaded_history), # chat_history gr.update(value="Publish", visible=True), # deploy_btn False # Keep first_change as False ] trending_spaces_dropdown.change( handle_trending_space_selection, inputs=[trending_spaces_dropdown, history, spaces_first_change], outputs=[ trending_spaces_status, code_output, language_dropdown, history, history_output, chat_history, deploy_btn, spaces_first_change ] )
"""
Gradio user interface for AnyCoder.
Defines the main UI layout, components, and event handlers.
"""
import os
import gradio as gr
from typing import Dict, Optional
from huggingface_hub import HfApi
import httpx
# Monkey-patch httpx to increase timeout for OAuth
# This prevents ReadTimeout errors during HuggingFace OAuth flow
_original_client_init = httpx.AsyncClient.__init__
def _patched_client_init(self, *args, **kwargs):
# If no timeout is specified, use longer timeouts
if 'timeout' not in kwargs:
kwargs['timeout'] = httpx.Timeout(
connect=30.0, # 30 seconds for connection
read=60.0, # 60 seconds for reading response (increased from default 5s)
write=30.0, # 30 seconds for writing
pool=30.0 # 30 seconds for pool operations
)
return _original_client_init(self, *args, **kwargs)
httpx.AsyncClient.__init__ = _patched_client_init
from .config import (
AVAILABLE_MODELS, DEFAULT_MODEL, DEFAULT_MODEL_NAME,
LANGUAGE_CHOICES, get_gradio_language
)
from .themes import THEME_CONFIGS, get_saved_theme, current_theme
from .prompts import HTML_SYSTEM_PROMPT
from .models import history_to_chatbot_messages
from .parsers import (
history_render, clear_history, create_multimodal_message,
parse_multipage_html_output, parse_transformers_js_output,
parse_react_output, format_transformers_js_output,
validate_and_autofix_files, parse_multi_file_python_output,
is_streamlit_code, is_gradio_code
)
from .deploy import (
check_authentication, update_ui_for_auth_status,
generation_code, deploy_to_spaces, add_anycoder_tag_to_readme,
_parse_repo_or_model_url, load_project_from_url, check_hf_space_url,
import_repo_to_app, extract_import_statements,
generate_requirements_txt_with_llm, prettify_comfyui_json_for_html,
get_trending_models, import_model_from_hf, get_trending_spaces, import_space_from_hf,
switch_model_code_type
)
from .agent import (
agent_generate_with_questions, agent_process_answers_and_generate
)
# Main application with proper Gradio theming
with gr.Blocks(
title="AnyCoder - AI Code Generator",
theme=current_theme,
css="""
.theme-info { font-size: 0.9em; opacity: 0.8; }
.theme-description { padding: 8px 0; }
.theme-status {
padding: 10px;
border-radius: 8px;
background: rgba(34, 197, 94, 0.1);
border: 1px solid rgba(34, 197, 94, 0.2);
margin: 8px 0;
}
.restart-needed {
padding: 12px;
border-radius: 8px;
background: rgba(255, 193, 7, 0.1);
border: 1px solid rgba(255, 193, 7, 0.3);
margin: 8px 0;
text-align: center;
}
/* Authentication status styling */
.auth-status {
padding: 8px 12px;
border-radius: 6px;
margin: 8px 0;
font-weight: 500;
text-align: center;
}
.auth-status:has-text("πŸ”’") {
background: rgba(231, 76, 60, 0.1);
border: 1px solid rgba(231, 76, 60, 0.3);
color: #e74c3c;
}
.auth-status:has-text("βœ…") {
background: rgba(46, 204, 113, 0.1);
border: 1px solid rgba(46, 204, 113, 0.3);
color: #2ecc71;
}
/* App link styling (visible on all devices) */
.app-link {
display: block;
padding: 12px;
border-radius: 8px;
background: rgba(59, 130, 246, 0.1);