import gradio as gr import tempfile import os import datetime import re import time import concurrent.futures from therapist import ( run_therapy_individual, run_therapy_group, run_therapy_relationship ) def select_group_row(evt: gr.SelectData, df): row_idx = evt.index[0] if hasattr(df, "iloc"): circle_name = df.iloc[row_idx, 0] bugs = df.iloc[row_idx, 1] else: circle_name = df[row_idx][0] bugs = df[row_idx][1] return circle_name, bugs def add_group_to_history(circle_name, bugs, df): if not bugs or not bugs.strip(): return df name = circle_name.strip() if circle_name and circle_name.strip() else "Custom Circle" new_row = [name, bugs.strip()] if hasattr(df, "values") and hasattr(df, "tolist"): df_list = df.values.tolist() else: df_list = list(df) exists = False for r in df_list: if r[0] == name and r[1] == bugs.strip(): exists = True break if not exists: df_list.append(new_row) if hasattr(df, "iloc"): try: import pandas as pd return pd.DataFrame(df_list, columns=df.columns) except ImportError: pass return df_list def select_couples_row(evt: gr.SelectData, df): row_idx = evt.index[0] if hasattr(df, "iloc"): a = df.iloc[row_idx, 0] b = df.iloc[row_idx, 1] conflict = df.iloc[row_idx, 2] else: a = df[row_idx][0] b = df[row_idx][1] conflict = df[row_idx][2] return a, b, conflict def add_couples_to_history(a, b, conflict, df): if not a or not b or not conflict: return df new_row = [a.strip(), b.strip(), conflict.strip()] if hasattr(df, "values") and hasattr(df, "tolist"): df_list = df.values.tolist() else: df_list = list(df) exists = False for r in df_list: if r[0] == a.strip() and r[1] == b.strip() and r[2] == conflict.strip(): exists = True break if not exists: df_list.append(new_row) if hasattr(df, "iloc"): try: import pandas as pd return pd.DataFrame(df_list, columns=df.columns) except ImportError: pass return df_list # Monochromatic case-file design system (typewriter-inspired typography) custom_css = """ @import url('https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap'); body, .gradio-container { background-color: #0A0A0A !important; font-family: 'Geist', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important; color: #A3A3A3 !important; letter-spacing: -0.01em; } .gradio-container { max-width: 960px !important; margin: 0 auto !important; padding: 60px 20px !important; } /* Header */ .clinical-title { font-family: 'Geist', sans-serif !important; font-weight: 500 !important; color: #F5F5F5 !important; font-size: 2.2rem !important; letter-spacing: -0.03em; margin-bottom: 6px !important; text-align: center; } .clinical-subtitle { color: #737373 !important; font-size: 1rem !important; margin-bottom: 48px !important; font-weight: 400 !important; text-align: center; } /* Tabs */ .tabs { background: transparent !important; border-bottom: 1px solid #262626 !important; margin-bottom: 32px !important; } .tab-nav { border: none !important; display: flex !important; gap: 32px !important; } .tab-nav button { font-family: 'Geist', sans-serif !important; font-size: 0.95rem !important; font-weight: 500 !important; color: #737373 !important; background: transparent !important; border: none !important; border-bottom: 2px solid transparent !important; padding: 8px 0 !important; border-radius: 0 !important; transition: all 0.15s ease !important; } .tab-nav button:hover { color: #E5E5E5 !important; } .tab-nav button.selected { color: #F5F5F5 !important; border-bottom: 2px solid #F5F5F5 !important; } /* Patient Case File (Typewriter look) */ .patient-file-container { background-color: #111111 !important; border: 1px solid #262626 !important; border-radius: 8px !important; padding: 28px !important; color: #CCCCCC !important; font-size: 0.95rem !important; line-height: 1.8 !important; font-family: 'Courier New', Courier, monospace !important; } .patient-file-container h1, .patient-file-container h2 { color: #F5F5F5 !important; font-family: 'Courier New', Courier, monospace !important; font-weight: bold !important; border-bottom: 1px dashed #262626 !important; padding-bottom: 6px !important; margin-top: 28px !important; margin-bottom: 14px !important; font-size: 1.25rem !important; letter-spacing: -0.01em; } .patient-file-container h1:first-of-type { margin-top: 0 !important; } .patient-file-container p, .patient-file-container li { font-family: 'Courier New', Courier, monospace !important; color: #AAAAAA !important; font-size: 0.95rem !important; } .patient-file-container blockquote { border-left: 3px solid #525252 !important; padding: 4px 16px !important; margin: 16px 0 !important; color: #737373 !important; font-style: italic; } .patient-file-container code { font-family: 'JetBrains Mono', monospace !important; font-size: 0.85rem !important; background-color: #222222 !important; padding: 2px 6px !important; border-radius: 4px !important; color: #E5E5E5 !important; } /* Form inputs styling */ .gr-box { background-color: #171717 !important; border: 1px solid #262626 !important; border-radius: 8px !important; } textarea, input[type="text"] { background-color: #0A0A0A !important; border: 1px solid #262626 !important; color: #E5E5E5 !important; border-radius: 6px !important; font-size: 0.9rem !important; padding: 12px !important; } textarea:focus, input[type="text"]:focus { border-color: #525252 !important; box-shadow: none !important; outline: none !important; } /* Primary CTA Button */ .diagnose-btn { background-color: #F5F5F5 !important; border: 1px solid #F5F5F5 !important; color: #0A0A0A !important; font-weight: 500 !important; font-size: 0.95rem !important; font-family: 'Geist', sans-serif !important; border-radius: 6px !important; padding: 12px 24px !important; transition: all 0.15s ease !important; cursor: pointer !important; text-align: center !important; } .diagnose-btn:hover { background-color: #E5E5E5 !important; border-color: #E5E5E5 !important; } /* Secondary Action Buttons (Outlined) */ .export-btn { background-color: transparent !important; border: 1px solid #262626 !important; color: #737373 !important; font-weight: 500 !important; font-family: 'Geist', sans-serif !important; border-radius: 6px !important; padding: 10px 20px !important; transition: all 0.15s ease !important; margin-top: 16px !important; text-align: center !important; font-size: 0.85rem !important; cursor: pointer !important; width: 100% !important; } .export-btn:hover { border-color: #525252 !important; color: #F5F5F5 !important; } /* Small inline pill button */ .pill-btn { background-color: #171717 !important; border: 1px solid #262626 !important; color: #A3A3A3 !important; font-size: 0.8rem !important; font-weight: 400 !important; padding: 4px 10px !important; border-radius: 4px !important; cursor: pointer !important; transition: all 0.1s ease !important; } .pill-btn:hover { background-color: #262626 !important; color: #F5F5F5 !important; border-color: #525252 !important; } /* Examples box */ .gr-example { background-color: #171717 !important; border: 1px solid #262626 !important; border-radius: 6px !important; color: #A3A3A3 !important; font-size: 0.85rem !important; transition: all 0.15s !important; } .gr-example:hover { background-color: #262626 !important; border-color: #525252 !important; } /* Hidden download buttons selector (hides them from users but keeps in DOM for JS selectors) */ #ind_download_hidden, #group_download_hidden, #rel_download_hidden { display: none !important; } /* Remove confusing elapsed/ETA texts from Gradio's loading indicator, leaving only spinner/loader elements */ .progress-level-inner, .progress-bar-wrap, .eta-bar, .meta-text, .meta-text-center, .meta-text-container { display: none !important; } """ def clean_txt_for_pdf(text): """ Translites curly/unicode punctuation that FPDF standard 8-bit fonts can't encode. """ replacements = { '\u2018': "'", # Smart left single quote '\u2019': "'", # Smart right single quote '\u201c': '"', # Smart left double quote '\u201d': '"', # Smart right double quote '\u2013': '-', # En dash '\u2014': '-', # Em dash '\u2026': '...', # Ellipsis } for orig, rep in replacements.items(): text = text.replace(orig, rep) # Filter non-latin-1 characters safely return text.encode('latin-1', 'replace').decode('latin-1') def generate_pdf_report(report_text, mode, bug_name_or_comp_a="", comp_b=""): """ Saves the therapy report text into a professionally styled case-file PDF. """ if not report_text: return None default_texts = [ "Medical file will appear here after analysis", "Group therapy session transcript will appear here", "Relationship agreement notes will appear here", "Error" ] if any(dt in report_text for dt in default_texts): return None from fpdf import FPDF import tempfile import os # Dynamic safe filename matching target formatting exactly if mode == "individual": clean_name = re.sub(r'[^a-zA-Z0-9_\-]', '', bug_name_or_comp_a.strip().replace(' ', '_')) filename = f"{clean_name if clean_name else 'Bug'}_CaseFile.pdf" elif mode == "group": filename = "GroupTherapy_Report.pdf" elif mode == "relationship": filename = "RelationshipCounseling_Report.pdf" else: filename = "Therapy_Report.pdf" # Write to unique subdirectory to completely prevent permission locks temp_sub = tempfile.mkdtemp(prefix="bug_pdf_") path = os.path.join(temp_sub, filename) # PDF Configuration pdf = FPDF() pdf.add_page() pdf.set_margins(20, 20, 20) pdf.set_auto_page_break(auto=True, margin=20) # Clinic letterhead in clinical typewriter Courier pdf.set_font("Courier", "B", 16) pdf.cell(0, 10, "BUG THERAPY CLINIC", new_x="LMARGIN", new_y="NEXT", align="C") pdf.set_font("Courier", "", 9) pdf.cell(0, 5, "LICENSED SOFTWARE PSYCHOLOGISTS | CASE FILE CONFIDENTIAL", new_x="LMARGIN", new_y="NEXT", align="C") pdf.ln(4) pdf.line(20, pdf.get_y(), 190, pdf.get_y()) pdf.ln(8) # Metadata Block date_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") pdf.set_font("Courier", "", 9) pdf.cell(0, 5, f"DATE OF SESSION: {date_str}", new_x="LMARGIN", new_y="NEXT") pdf.cell(0, 5, f"THERAPY CLASSIFICATION: {mode.upper()} INTERVENTION", new_x="LMARGIN", new_y="NEXT") pdf.cell(0, 5, "STATUS: ARCHIVED NOTES", new_x="LMARGIN", new_y="NEXT") pdf.ln(6) pdf.line(20, pdf.get_y(), 190, pdf.get_y()) pdf.ln(8) # Parsing report lines = report_text.split("\n") for line in lines: line = clean_txt_for_pdf(line.strip()) if not line: continue # Draw clean solid partition lines for markdown horizontal dividers if line.startswith("---") or line.startswith("___") or (line.startswith("-") and len(line) > 5): pdf.ln(3) pdf.line(20, pdf.get_y(), 190, pdf.get_y()) pdf.ln(5) continue # Heading 1 if line.startswith("# "): pdf.ln(4) pdf.set_font("Courier", "B", 13) content = line.replace("# ", "").strip() pdf.cell(0, 8, content, new_x="LMARGIN", new_y="NEXT") pdf.set_font("Courier", "", 10) pdf.ln(2) # Bold labels elif line.startswith("**"): pdf.ln(1) parts = line.split("**") if len(parts) >= 3: pdf.set_font("Courier", "B", 10) pdf.write(6, parts[1] + ": ") pdf.set_font("Courier", "", 10) pdf.write(6, "".join(parts[2:]).strip() + "\n") else: pdf.set_font("Courier", "", 10) pdf.multi_cell(0, 6, line.replace("**", "")) pdf.ln(2) # Monospace bullet points elif line.startswith("* ") or line.startswith("- "): pdf.set_font("Courier", "", 10) content = line[2:].strip() pdf.multi_cell(0, 6, f" * {content}") pdf.ln(1) else: pdf.set_font("Courier", "", 10) pdf.multi_cell(0, 6, line) pdf.ln(1.5) pdf.output(path) return path # Simulated section-by-section progress stream generators def run_therapy_individual_stream(bug_text, personality="None"): with concurrent.futures.ThreadPoolExecutor() as executor: future = executor.submit(run_therapy_individual, bug_text, personality) stages = [ ("⏳ Case File", "⏳ Trauma History", "⏳ Therapy Session", "⏳ Treatment Plan"), ("✓ Case File", "⏳ Trauma History", "⏳ Therapy Session", "⏳ Treatment Plan"), ("✓ Case File", "✓ Trauma History", "⏳ Therapy Session", "⏳ Treatment Plan"), ("✓ Case File", "✓ Trauma History", "✓ Therapy Session", "⏳ Treatment Plan"), ("✓ Case File", "✓ Trauma History", "✓ Therapy Session", "✓ Treatment Plan") ] stage_idx = 0 while not future.done(): progress_md = f"""# Consulting Patient... -------------------------------- {stages[stage_idx][0]} {stages[stage_idx][1]} {stages[stage_idx][2]} {stages[stage_idx][3]} """ yield progress_md time.sleep(1.5) if stage_idx < len(stages) - 1: stage_idx += 1 try: yield future.result() except Exception as e: yield f"# Case File\n\nPatient:\n{bug_text}\n\n--------------------------------\n\n# Error\n\nTherapy interrupted: {str(e)}" def run_therapy_group_stream(bugs_text): with concurrent.futures.ThreadPoolExecutor() as executor: future = executor.submit(run_therapy_group, bugs_text) stages = [ ("⏳ Case File: Group Circle", "⏳ Circle Dynamics", "⏳ Therapy Session", "⏳ Group Treatment Plan"), ("✓ Case File: Group Circle", "⏳ Circle Dynamics", "⏳ Therapy Session", "⏳ Group Treatment Plan"), ("✓ Case File: Group Circle", "✓ Circle Dynamics", "⏳ Therapy Session", "⏳ Group Treatment Plan"), ("✓ Case File: Group Circle", "✓ Circle Dynamics", "✓ Therapy Session", "⏳ Group Treatment Plan"), ("✓ Case File: Group Circle", "✓ Circle Dynamics", "✓ Therapy Session", "✓ Group Treatment Plan") ] stage_idx = 0 while not future.done(): progress_md = f"""# Group Session In Progress... -------------------------------- {stages[stage_idx][0]} {stages[stage_idx][1]} {stages[stage_idx][2]} {stages[stage_idx][3]} """ yield progress_md time.sleep(1.5) if stage_idx < len(stages) - 1: stage_idx += 1 try: yield future.result() except Exception as e: yield f"# Case File: Group Circle\n\n--------------------------------\n\n# Error\n\nGroup therapy interrupted: {str(e)}" def run_therapy_relationship_stream(component_a, component_b, conflict): with concurrent.futures.ThreadPoolExecutor() as executor: future = executor.submit(run_therapy_relationship, component_a, component_b, conflict) stages = [ ("⏳ Case File: Couples Counseling", "⏳ Relationship Status", "⏳ Therapy Session", "⏳ Relationship Agreement"), ("✓ Case File: Couples Counseling", "⏳ Relationship Status", "⏳ Therapy Session", "⏳ Relationship Agreement"), ("✓ Case File: Couples Counseling", "✓ Relationship Status", "⏳ Therapy Session", "⏳ Relationship Agreement"), ("✓ Case File: Couples Counseling", "✓ Relationship Status", "✓ Therapy Session", "⏳ Relationship Agreement"), ("✓ Case File: Couples Counseling", "✓ Relationship Status", "✓ Therapy Session", "✓ Relationship Agreement") ] stage_idx = 0 while not future.done(): progress_md = f"""# Relationship Mediation In Progress... -------------------------------- {stages[stage_idx][0]} {stages[stage_idx][1]} {stages[stage_idx][2]} {stages[stage_idx][3]} """ yield progress_md time.sleep(1.5) if stage_idx < len(stages) - 1: stage_idx += 1 try: yield future.result() except Exception as e: yield f"# Case File: Couples Counseling\n\n--------------------------------\n\n# Error\n\nCouples therapy interrupted: {str(e)}" # Build Gradio UI with gr.Blocks(title="bug therapist") as demo: # Header Section gr.HTML( """

bug therapist

clinical psychoanalysis for software defects.

""" ) with gr.Tabs(elem_classes="tabs") as tabs: # TAB 1: Individual Session with gr.Tab("Individual Therapy", id=1): with gr.Row(): with gr.Column(scale=1): ind_bug_input = gr.Textbox( label="admit patient (paste stack trace, code snippet, or error log)", lines=8, placeholder="e.g. java.lang.NullPointerException: Attempt to invoke virtual method..." ) ind_personality = gr.Dropdown( choices=[ "None", "Abandonment issues (feeling ignored by developer)", "Control freak (compulsion to govern all processing states)", "Commitment issues (refusal to resolve callback/promise states)", "Identity crisis (type instability, constant casting errors)", "Attention seeker (verbose logs, printing internal operations)", "Existential dread (cycles of execution without clear intent)" ], value="None", label="Choose a Trauma (Optional)", info="Leave blank and the therapist will diagnose automatically." ) with gr.Column(): ind_btn = gr.Button("diagnose bug", elem_classes=["diagnose-btn"]) with gr.Column(scale=1): ind_output = gr.Markdown( value="*Medical file will appear here after analysis.*", elem_classes=["patient-file-container"] ) # Normal button that triggers hidden DownloadButton click ind_export_btn = gr.Button("download case report pdf", elem_classes=["export-btn"]) gr.Examples( examples=[ ["NullPointerException", "Abandonment issues (feeling ignored by developer)"], ["Memory Leak in loop", "Control freak (compulsion to govern all processing states)"], ["Segmentation Fault", "Existential dread (cycles of execution without clear intent)"], ["RecursionError: maximum recursion depth exceeded", "Identity crisis (type instability, constant casting errors)"] ], inputs=[ind_bug_input, ind_personality], label="common admissions", cache_examples=False ) # TAB 2: Group Therapy with gr.Tab("Group Therapy", id=2): with gr.Row(): with gr.Column(scale=1): # Preset triggers right above textbox gr.Markdown("Quick admission presets:") with gr.Row(): preset_classic = gr.Button("classic trio", elem_classes=["pill-btn"]) preset_concurrency = gr.Button("concurrency crisis", elem_classes=["pill-btn"]) preset_boundary = gr.Button("boundary conflict", elem_classes=["pill-btn"]) gr.HTML("
") group_circle_name = gr.Textbox( label="selected circle preset", interactive=False, placeholder="Select a preset circle or enter custom bugs below" ) group_bugs_input = gr.Textbox( label="admit patient group (separated by commas)", lines=6, placeholder="e.g. NullPointerException, Memory Leak, Segmentation Fault" ) with gr.Column(): group_btn = gr.Button("counsel group", elem_classes=["diagnose-btn"]) with gr.Column(scale=1): group_output = gr.Markdown( value="*Group therapy session transcript will appear here.*", elem_classes=["patient-file-container"] ) # Normal button that triggers hidden DownloadButton click group_export_btn = gr.Button("download group report pdf", elem_classes=["export-btn"]) # Map quick preset buttons preset_classic.click( fn=lambda: ("Classic Trio", "NullPointerException, Memory Leak, Segmentation Fault"), inputs=None, outputs=[group_circle_name, group_bugs_input] ) preset_concurrency.click( fn=lambda: ("Concurrency Crisis", "Deadlock, Race Condition, Thread Leak"), inputs=None, outputs=[group_circle_name, group_bugs_input] ) preset_boundary.click( fn=lambda: ("Boundary Conflict", "Segmentation Fault, IndexOutOfBoundsException, OffByOneError"), inputs=None, outputs=[group_circle_name, group_bugs_input] ) # Standard Circles dynamic dataframe group_history_df = gr.Dataframe( headers=["Circle Name", "Bugs"], datatype=["str", "str"], value=[ ["Classic Trio", "NullPointerException, Memory Leak, Segmentation Fault"], ["Concurrency Crisis", "Deadlock, Race Condition, Thread Leak"], ["Boundary Conflict", "Segmentation Fault, IndexOutOfBoundsException, OffByOneError"] ], interactive=False, label="standard circles" ) group_history_df.select( fn=select_group_row, inputs=[group_history_df], outputs=[group_circle_name, group_bugs_input] ) # TAB 3: Relationship Counseling with gr.Tab("Relationship Counseling", id=3): with gr.Row(): with gr.Column(scale=1): gr.Markdown("Quick relationship presets:") with gr.Row(): preset_api = gr.Button("api friction", elem_classes=["pill-btn"]) preset_nesting = gr.Button("nested hell", elem_classes=["pill-btn"]) preset_history = gr.Button("force pushing", elem_classes=["pill-btn"]) gr.HTML("
") rel_comp_a = gr.Textbox( label="component a", placeholder="e.g. React Frontend" ) rel_comp_b = gr.Textbox( label="component b", placeholder="e.g. Django Backend" ) rel_conflict = gr.Textbox( label="architectural conflict description", lines=4, placeholder="e.g. component a queries component b relentlessly; component b returns silent timeouts." ) with gr.Column(): rel_btn = gr.Button("counsel couple", elem_classes=["diagnose-btn"]) with gr.Column(scale=1): rel_output = gr.Markdown( value="*Relationship agreement notes will appear here.*", elem_classes=["patient-file-container"] ) # Normal button that triggers hidden DownloadButton click rel_export_btn = gr.Button("download couples report pdf", elem_classes=["export-btn"]) # Map quick relationship presets preset_api.click( fn=lambda: ("React Frontend", "Django Backend", "Frontend floods backend with requests"), inputs=None, outputs=[rel_comp_a, rel_comp_b, rel_conflict] ) preset_nesting.click( fn=lambda: ("Callback", "Promise", "Communication delays and trust issues"), inputs=None, outputs=[rel_comp_a, rel_comp_b, rel_conflict] ) preset_history.click( fn=lambda: ("Git CLI", "Developer", "Force pushes without communication"), inputs=None, outputs=[rel_comp_a, rel_comp_b, rel_conflict] ) # Troubled Couplings dynamic dataframe couples_history_df = gr.Dataframe( headers=["Component A", "Component B", "Conflict"], datatype=["str", "str", "str"], value=[ ["React Frontend", "Django Backend", "Frontend floods backend with requests"], ["Git CLI", "Developer", "Force pushes without communication"], ["Microservice A", "Microservice B", "Circular dependency blame game"], ["Callback", "Promise", "Communication delays and trust issues"] ], interactive=False, label="troubled couplings" ) couples_history_df.select( fn=select_couples_row, inputs=[couples_history_df], outputs=[rel_comp_a, rel_comp_b, rel_conflict] ) # Hidden download buttons to execute reliable one-click download stream via event trigger ind_download_hidden = gr.DownloadButton("Download Hidden 1", elem_id="ind_download_hidden") group_download_hidden = gr.DownloadButton("Download Hidden 2", elem_id="group_download_hidden") rel_download_hidden = gr.DownloadButton("Download Hidden 3", elem_id="rel_download_hidden") # Event handlers # 1. Individual ind_btn.click( run_therapy_individual_stream, inputs=[ind_bug_input, ind_personality], outputs=ind_output, show_progress="minimal" ) ind_export_btn.click( fn=lambda report, bug: generate_pdf_report(report, "individual", bug), inputs=[ind_output, ind_bug_input], outputs=ind_download_hidden, show_progress="minimal" ).then( fn=None, inputs=None, outputs=None, js="() => { const link = document.querySelector('#ind_download_hidden a') || document.querySelector('#ind_download_hidden'); if (link) link.click(); }" ) # 2. Group group_btn.click( run_therapy_group_stream, inputs=[group_bugs_input], outputs=group_output, show_progress="minimal" ).then( fn=add_group_to_history, inputs=[group_circle_name, group_bugs_input, group_history_df], outputs=group_history_df ) group_export_btn.click( fn=lambda report: generate_pdf_report(report, "group"), inputs=group_output, outputs=group_download_hidden, show_progress="minimal" ).then( fn=None, inputs=None, outputs=None, js="() => { setTimeout(() => { const link = document.querySelector('#group_download_hidden a') || document.querySelector('#group_download_hidden'); if (link) link.click(); }, 150); }" ) # 3. Relationship rel_btn.click( run_therapy_relationship_stream, inputs=[rel_comp_a, rel_comp_b, rel_conflict], outputs=rel_output, show_progress="minimal" ).then( fn=add_couples_to_history, inputs=[rel_comp_a, rel_comp_b, rel_conflict, couples_history_df], outputs=couples_history_df ) rel_export_btn.click( fn=lambda report, a, b: generate_pdf_report(report, "relationship", a, b), inputs=[rel_output, rel_comp_a, rel_comp_b], outputs=rel_download_hidden, show_progress="minimal" ).then( fn=None, inputs=None, outputs=None, js="() => { const link = document.querySelector('#rel_download_hidden a') || document.querySelector('#rel_download_hidden'); if (link) link.click(); }" ) # Launch app locally if __name__ == "__main__": demo.launch(css=custom_css)