bug_therapist / app.py
Sukan007's picture
Upload 4 files
7a4e1ef verified
Raw
History Blame Contribute Delete
30.5 kB
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(
"""
<div style="text-align: center; padding: 24px 0 12px 0;">
<h1 class="clinical-title">bug therapist</h1>
<p class="clinical-subtitle">clinical psychoanalysis for software defects.</p>
</div>
"""
)
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("<div style='height: 10px;'></div>")
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("<div style='height: 10px;'></div>")
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)