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( """
clinical psychoanalysis for software defects.