| """ |
| app.py — Maai's interface. |
| |
| A minimal Gradio skin over the proven pipeline: |
| paste a symptom description in any language -> see the advocacy |
| record -> download the clinician-ready PDF. |
| Function today; theming (palette, Fraunces/Inter) is weekend work. |
| """ |
|
|
| import gradio as gr |
| from chain import build_record |
| from make_pdf import make_pdf |
| from speak import speak_record |
| from socrates import annotate_socrates |
| from contribute import make_contribution, save_contribution |
| from aggregate import aggregate |
|
|
|
|
| def run_maai(description: str): |
| """Full pipeline: description -> record -> PDF. Returns display text + PDF path.""" |
| if not description or not description.strip(): |
| return "Please describe what you've been experiencing.", None, None |
|
|
| record = build_record(description) |
|
|
| lines = [ |
| f"Language detected: {record['language_detected']}", |
| "", |
| "HER WORDS → CLINICAL TERMS", |
| "", |
| ] |
| for item in record["items"]: |
| lines.append(f'"{item["verbatim"]}"') |
| lines.append(f" → {item['clinical']}") |
| lines.append("") |
|
|
| annotated = annotate_socrates(record) |
| lines.append("") |
| lines.append("CLINICAL FRAMEWORK (SOCRATES)") |
| lines.append("") |
| for item in annotated["items"]: |
| if item["dimensions"]: |
| lines.append(f'"{item["verbatim"]}" — {", ".join(item["dimensions"])}') |
| lines.append("") |
| lines.append("Not yet described — your doctor may ask about:") |
| for dim in annotated["not_yet_described"]: |
| lines.append(f" · {dim}") |
| pdf_path = make_pdf(record, annotated) |
| return "\n".join(lines), pdf_path, record |
|
|
| css = """ |
| @import url('https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400;9..144,600&display=swap'); |
| |
| .gradio-container { |
| background-color: #F7F4EF !important; |
| max-width: 780px !important; |
| margin: 0 auto !important; |
| } |
| |
| #header { text-align: center; } |
| #header h1 { |
| font-family: 'Fraunces', serif !important; |
| font-size: 3.2em !important; |
| font-weight: 600 !important; |
| color: #2B2A26 !important; |
| margin-bottom: 0.1em !important; |
| } |
| |
| .block, .form, textarea, input { |
| border-radius: 12px !important; |
| border-color: #DCCFBC !important; |
| } |
| |
| button.primary { |
| font-family: 'Fraunces', serif !important; |
| font-size: 1.1em !important; |
| } |
| """ |
|
|
| theme = gr.themes.Soft( |
| primary_hue=gr.themes.Color( |
| c50="#F7F4EF", c100="#EFE9DF", c200="#DCCFBC", c300="#C6B29A", |
| c400="#A98F73", c500="#8C7355", c600="#7A6349", c700="#65523C", |
| c800="#514230", c900="#3D3124", c950="#2B2A26", |
| ), |
| neutral_hue="stone", |
| font=gr.themes.GoogleFont("Inter"), |
| font_mono=gr.themes.GoogleFont("Inter"), |
| ) |
|
|
| with gr.Blocks(title="Maai", theme=theme, css=css) as demo: |
| gr.Markdown( |
| "# Maai\n" |
| "*See what time reveals.*\n\n" |
| "**The AI health advocate for endometriosis**", |
| elem_id="header", |
| ) |
|
|
| gr.Markdown( |
| "Maai means the meaningful interval between things — the space where " |
| "health patterns emerge.\n\n" |
| "Endometriosis takes an average of **9 years and 4 months** to diagnose " |
| "in the UK — and 11 years for women from ethnically diverse communities. " |
| "83% of women were told by a healthcare practitioner they were making " |
| "a fuss about nothing. Almost half visited their GP ten or more times " |
| "before anyone joined the dots.\n\n" |
| "Maai helps you be harder to dismiss. Describe what you've been " |
| "experiencing over recent weeks or months, in your own words, in any " |
| "language. Maai maps your words to clinical terms a doctor recognises — " |
| "never replacing them, never drawing conclusions. Bring a record to " |
| "every appointment; the pattern builds. The clinician interprets. " |
| "Maai helps you be heard.\n\n" |
| "**Maai is for patterns over time, not a medical emergency. If you have " |
| "severe or sudden symptoms right now, call 111 — or 999 if it's urgent.**" |
| ) |
| |
|
|
| description = gr.Textbox( |
| label="In your own words", |
| placeholder="e.g. For months I've had a deep dragging pain low in my belly, not just during my period, and I'm exhausted all the time...", |
| lines=5, |
| ) |
| submit = gr.Button("Prepare my record") |
|
|
| record_display = gr.Textbox(label="Your advocacy record", lines=14) |
| pdf_file = gr.File(label="Download for your appointment") |
|
|
| record_state = gr.State() |
| listen = gr.Button("Hear my record read aloud") |
| audio_out = gr.Audio(label="Your record, read back", type="filepath") |
|
|
| submit.click(fn=run_maai, inputs=description, |
| outputs=[record_display, pdf_file, record_state]) |
|
|
| def read_aloud(record): |
| if not record: |
| raise gr.Error("Prepare a record first, then listen.") |
| return speak_record(record) |
|
|
| listen.click(fn=read_aloud, inputs=record_state, outputs=audio_out) |
|
|
| gr.Markdown("---") |
| gr.Markdown("### Contribute to what women are revealing") |
| consent = gr.Checkbox( |
| label=( |
| "Contribute your pattern (anonymous) — adds your symptom pattern, " |
| "never your words, name, or any identifying detail, to a shared " |
| "dataset of women's heart-health experiences." |
| ), |
| value=False, |
| ) |
| age_band = gr.Dropdown( |
| choices=["not_given", "25-34", "35-44", "45-54", "55-64", "65-74", "75+"], |
| value="not_given", |
| label="Age band (optional)", |
| ) |
| contribute_btn = gr.Button("Contribute my pattern") |
| feedback = gr.Markdown() |
|
|
| def contribute(record, consented, band): |
| if not record: |
| raise gr.Error("Prepare a record first.") |
| if not consented: |
| raise gr.Error("Tick the consent box if you'd like to contribute — it's entirely optional.") |
| total = save_contribution(make_contribution(record, age_band=band)) |
| view = aggregate() |
| top = next(iter(view["symptom_prevalence"])) |
| return ( |
| f"**Your pattern joins {total - 1} others.** Together they're showing " |
| f"how often *{top}* appears in women's patterns — years before " |
| f"diagnosis. Thank you for helping make it visible." |
| ) |
|
|
| contribute_btn.click(fn=contribute, inputs=[record_state, consent, age_band], outputs=feedback) |
|
|
| gr.Markdown("---") |
| gr.Markdown("### What women are revealing") |
| gr.Markdown( |
| "*Seeded with representative synthetic data to show what the aggregate " |
| "view reveals at scale. Descriptive only — Maai counts and reveals, " |
| "never predicts.*" |
| ) |
|
|
| def render_aggregate(): |
| view = aggregate() |
| if view["total"] == 0: |
| return "No contributions yet." |
| lines = [f"**{view['total']} contributed patterns**\n"] |
| lines.append("| Symptom | Appears in |") |
| lines.append("|---|---|") |
| for symptom, pct in view["symptom_prevalence"].items(): |
| lines.append(f"| {symptom} | {pct}% |") |
| langs = ", ".join(view["languages"].keys()) |
| lines.append(f"\nContributed in **{len(view['languages'])} languages**: {langs}") |
| return "\n".join(lines) |
|
|
| aggregate_display = gr.Markdown(render_aggregate()) |
| refresh = gr.Button("Refresh the collective picture") |
| refresh.click(fn=render_aggregate, outputs=aggregate_display) |
|
|
| if __name__ == "__main__": |
| demo.launch(ssr_mode=False) |
|
|
|
|