Spaces:
Running
Running
| # ββ Patch 3: fix Gradio 4.44.0 + Starlette API mismatch ββ | |
| import starlette.templating as _st | |
| _orig_TemplateResponse = _st.Jinja2Templates.TemplateResponse | |
| def _safe_TemplateResponse(self, *args, **kwargs): | |
| # If the arguments are shifted, re-align them: | |
| if len(args) >= 2 and isinstance(args[0], str) and isinstance(args[1], dict): | |
| name_str = args[0] | |
| context_dict = args[1] | |
| request_obj = context_dict.get("request") | |
| args = (request_obj, name_str, context_dict) + args[2:] | |
| return _orig_TemplateResponse(self, *args, **kwargs) | |
| _st.Jinja2Templates.TemplateResponse = _safe_TemplateResponse | |
| # ββ Patch 1: restore HfFolder for gradio 4.44.0 ββ | |
| import unittest.mock as _mock | |
| import sys as _sys | |
| _hf_hub = __import__("huggingface_hub") | |
| if not hasattr(_hf_hub, "HfFolder"): | |
| class _FakeHfFolder: | |
| def get_token(): return None | |
| def save_token(token): pass | |
| def delete_token(): pass | |
| _hf_hub.HfFolder = _FakeHfFolder | |
| _sys.modules["huggingface_hub"].HfFolder = _FakeHfFolder | |
| # ββ Patch 2: fix gradio_client schema bug ββ | |
| import gradio_client.utils as _gcu | |
| _orig = _gcu._json_schema_to_python_type | |
| def _safe(schema, defs=None): | |
| if not isinstance(schema, dict): | |
| return "Any" | |
| return _orig(schema, defs) | |
| _gcu._json_schema_to_python_type = _safe | |
| # ββ Imports ββ | |
| import json | |
| import time | |
| import threading | |
| import datetime | |
| import gradio as gr | |
| from langchain_core.messages import HumanMessage, AIMessage, ToolMessage | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # CSS β 600+ lines of premium dark-theme styling | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # CSS β 600+ lines of premium dark-theme styling (Safari Compatible) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| CUSTOM_CSS = """ | |
| /* ββ Google Fonts ββ */ | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Sora:wght@300;400;600;700&display=swap'); | |
| /* ββ Global Reset ββ */ | |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } | |
| :root { | |
| --bg-void: #04050a; | |
| --bg-deep: #080c14; | |
| --bg-surface: #0d1220; | |
| --bg-raised: #111827; | |
| --bg-glass: rgba(13,18,32,0.72); | |
| --bg-card: rgba(17,24,39,0.85); | |
| --border-subtle: rgba(99,120,190,0.12); | |
| --border-glow: rgba(99,179,237,0.28); | |
| --border-active: rgba(99,179,237,0.6); | |
| --blue-400: #60a5fa; | |
| --blue-500: #3b82f6; | |
| --blue-600: #2563eb; | |
| --blue-glow: rgba(59,130,246,0.35); | |
| --cyan-400: #22d3ee; | |
| --cyan-glow: rgba(34,211,238,0.25); | |
| --green-400: #4ade80; | |
| --green-500: #22c55e; | |
| --green-glow: rgba(74,222,128,0.25); | |
| --orange-400: #fb923c; | |
| --orange-glow: rgba(251,146,60,0.25); | |
| --red-400: #f87171; | |
| --red-glow: rgba(248,113,113,0.25); | |
| --purple-400: #c084fc; | |
| --purple-glow: rgba(192,132,252,0.2); | |
| --text-primary: #f0f4ff; | |
| --text-secondary: #94a3b8; | |
| --text-muted: #4b5563; | |
| --text-accent: #93c5fd; | |
| --font-display: 'Sora', sans-serif; | |
| --font-body: 'Inter', sans-serif; | |
| --font-mono: 'JetBrains Mono', monospace; | |
| --radius-sm: 6px; | |
| --radius-md: 10px; | |
| --radius-lg: 16px; | |
| --radius-xl: 22px; | |
| --shadow-card: 0 4px 24px rgba(0,0,0,0.45), 0 1px 4px rgba(0,0,0,0.3); | |
| --shadow-glow-blue: 0 0 20px rgba(59,130,246,0.3), 0 0 60px rgba(59,130,246,0.1); | |
| --shadow-glow-green: 0 0 20px rgba(74,222,128,0.25), 0 0 50px rgba(74,222,128,0.08); | |
| --shadow-glow-orange: 0 0 20px rgba(251,146,60,0.25); | |
| --transition-fast: 0.15s ease; | |
| --transition-smooth: 0.3s cubic-bezier(0.4,0,0.2,1); | |
| --transition-spring: 0.5s cubic-bezier(0.34,1.56,0.64,1); | |
| } | |
| /* ββ App Shell ββ */ | |
| .gradio-container { | |
| background: var(--bg-void) !important; | |
| font-family: var(--font-body) !important; | |
| min-height: 100vh !important; | |
| max-width: 100% !important; | |
| padding: 0 !important; | |
| } | |
| /* hide gradio chrome */ | |
| footer { display: none !important; } | |
| .gr-form { background: transparent !important; border: none !important; } | |
| .gr-box { background: transparent !important; border: none !important; } | |
| /* ββ Ambient Background ββ */ | |
| #nb-root { | |
| background: | |
| radial-gradient(ellipse 80% 50% at 20% 10%, rgba(37,99,235,0.08) 0%, transparent 60%), | |
| radial-gradient(ellipse 60% 40% at 80% 80%, rgba(124,58,237,0.06) 0%, transparent 55%), | |
| radial-gradient(ellipse 40% 30% at 60% 30%, rgba(6,182,212,0.04) 0%, transparent 50%), | |
| var(--bg-void); | |
| padding: 0; | |
| -webkit-font-smoothing: antialiased; /* Safari font smoothing */ | |
| } | |
| /* βββββββββββββββββββ HEADER βββββββββββββββββββ */ | |
| #nb-header { | |
| padding: 40px 48px 32px; | |
| border-bottom: 1px solid var(--border-subtle); | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| gap: 32px; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| #nb-header::before { | |
| content: ''; | |
| position: absolute; | |
| inset: 0; | |
| background: linear-gradient(135deg, rgba(37,99,235,0.06) 0%, transparent 60%); | |
| pointer-events: none; | |
| } | |
| .nb-logo-group { display: flex; flex-direction: column; gap: 6px; } | |
| .nb-wordmark { | |
| font-family: var(--font-display) !important; | |
| font-size: 26px !important; | |
| font-weight: 700 !important; | |
| letter-spacing: -0.03em !important; | |
| color: var(--text-primary) !important; | |
| background: linear-gradient(135deg, #f0f4ff 0%, #93c5fd 50%, #60a5fa 100%); | |
| -webkit-background-clip: text !important; | |
| -webkit-text-fill-color: transparent !important; | |
| background-clip: text !important; | |
| display: inline-block; /* Essential for Safari background-clip */ | |
| line-height: 1.2 !important; | |
| margin: 0 !important; | |
| padding: 0 !important; | |
| } | |
| .nb-tagline { | |
| font-family: var(--font-mono) !important; | |
| font-size: 11px !important; | |
| font-weight: 400 !important; | |
| color: var(--blue-400) !important; | |
| letter-spacing: 0.12em !important; | |
| text-transform: uppercase !important; | |
| opacity: 0.8; | |
| margin: 0 !important; | |
| padding: 0 !important; | |
| } | |
| .nb-header-right { | |
| display: flex; | |
| align-items: center; | |
| gap: 24px; | |
| } | |
| .nb-status-chip { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 6px 14px; | |
| border-radius: 99px; | |
| border: 1px solid var(--border-subtle); | |
| background: var(--bg-card); | |
| font-family: var(--font-mono); | |
| font-size: 11px; | |
| color: var(--text-secondary); | |
| letter-spacing: 0.06em; | |
| } | |
| .nb-status-dot { | |
| width: 7px; | |
| height: 7px; | |
| border-radius: 50%; | |
| background: var(--green-400); | |
| box-shadow: 0 0 6px var(--green-glow); | |
| animation: nb-pulse-dot 2s ease-in-out infinite; | |
| } | |
| @keyframes nb-pulse-dot { | |
| 0%, 100% { opacity: 1; transform: scale(1); } | |
| 50% { opacity: 0.6; transform: scale(0.85); } | |
| } | |
| /* βββββββββββββββββββ UPLOAD ROW βββββββββββββββββββ */ | |
| #nb-upload-row { | |
| padding: 24px 48px; | |
| display: flex; | |
| align-items: center; | |
| gap: 20px; | |
| border-bottom: 1px solid var(--border-subtle); | |
| background: rgba(13,18,32,0.4); | |
| } | |
| #nb-upload-row .gr-file-upload, | |
| #nb-upload-row input[type="file"] { | |
| display: none !important; | |
| } | |
| /* Upload button (gr.File renders as a button) */ | |
| #nb-upload-btn button { | |
| -webkit-appearance: none !important; /* Safari Reset */ | |
| appearance: none !important; | |
| background: var(--bg-card) !important; | |
| border: 1px dashed rgba(99,120,190,0.3) !important; | |
| color: var(--text-secondary) !important; | |
| font-family: var(--font-body) !important; | |
| font-size: 13px !important; | |
| border-radius: var(--radius-md) !important; | |
| padding: 10px 20px !important; | |
| cursor: pointer !important; | |
| transition: all var(--transition-smooth) !important; | |
| width: 100% !important; | |
| } | |
| #nb-upload-btn button:hover { | |
| border-color: var(--border-glow) !important; | |
| color: var(--text-primary) !important; | |
| background: rgba(59,130,246,0.08) !important; | |
| } | |
| /* Begin Investigation button */ | |
| #nb-begin-btn button { | |
| -webkit-appearance: none !important; /* Safari Reset */ | |
| appearance: none !important; | |
| background: linear-gradient(135deg, #1d4ed8 0%, #2563eb 50%, #3b82f6 100%) !important; | |
| border: 1px solid rgba(99,179,237,0.4) !important; | |
| color: #fff !important; | |
| font-family: var(--font-display) !important; | |
| font-size: 14px !important; | |
| font-weight: 600 !important; | |
| border-radius: var(--radius-md) !important; | |
| padding: 11px 28px !important; | |
| cursor: pointer !important; | |
| transition: all var(--transition-smooth) !important; | |
| box-shadow: 0 4px 20px rgba(37,99,235,0.35), 0 0 0 0 rgba(59,130,246,0) !important; | |
| letter-spacing: 0.01em !important; | |
| } | |
| #nb-begin-btn button:hover { | |
| transform: translateY(-1px) !important; | |
| box-shadow: 0 6px 28px rgba(37,99,235,0.5), 0 0 40px rgba(59,130,246,0.2) !important; | |
| background: linear-gradient(135deg, #1e40af 0%, #2563eb 50%, #60a5fa 100%) !important; | |
| } | |
| #nb-begin-btn button:active { | |
| transform: translateY(0) !important; | |
| } | |
| .nb-patient-badge { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| padding: 8px 16px; | |
| border-radius: var(--radius-md); | |
| border: 1px solid var(--border-subtle); | |
| background: var(--bg-card); | |
| font-family: var(--font-mono); | |
| font-size: 12px; | |
| color: var(--text-secondary); | |
| } | |
| .nb-patient-badge .nb-badge-dot { | |
| width: 6px; height: 6px; | |
| border-radius: 50%; | |
| background: var(--cyan-400); | |
| box-shadow: 0 0 8px var(--cyan-glow); | |
| } | |
| /* βββββββββββββββββββ MAIN LAYOUT βββββββββββββββββββ */ | |
| #nb-main { | |
| display: grid; | |
| grid-template-columns: 220px 1fr 340px; | |
| gap: 0; | |
| height: calc(100vh - 180px); | |
| overflow: hidden; | |
| } | |
| #nb-main > * { | |
| border-right: 1px solid var(--border-subtle); | |
| overflow-y: auto; | |
| overflow-x: hidden; | |
| min-width: 0; /* Crucial Safari fix for CSS grid blowout */ | |
| min-height: 0; /* Crucial Safari fix */ | |
| } | |
| #nb-main > *:last-child { border-right: none; } | |
| /* ββ Scrollbar ββ */ | |
| ::-webkit-scrollbar { width: 4px; } | |
| ::-webkit-scrollbar-track { background: transparent; } | |
| ::-webkit-scrollbar-thumb { background: rgba(99,120,190,0.25); border-radius: 99px; } | |
| ::-webkit-scrollbar-thumb:hover { background: rgba(99,120,190,0.45); } | |
| /* βββββββββββββββββββ LEFT PANEL β TIMELINE βββββββββββββββββββ */ | |
| #nb-left { | |
| padding: 24px 0; | |
| background: rgba(8,12,20,0.6); | |
| } | |
| .nb-panel-title { | |
| font-family: var(--font-mono) !important; | |
| font-size: 10px !important; | |
| font-weight: 500 !important; | |
| color: var(--text-muted) !important; | |
| letter-spacing: 0.16em !important; | |
| text-transform: uppercase !important; | |
| padding: 0 20px 16px !important; | |
| margin: 0 !important; | |
| border-bottom: 1px solid var(--border-subtle) !important; | |
| margin-bottom: 8px !important; | |
| } | |
| /* Loop counter */ | |
| .nb-loop-counter { | |
| margin: 12px 16px; | |
| padding: 12px 14px; | |
| border-radius: var(--radius-md); | |
| background: rgba(37,99,235,0.08); | |
| border: 1px solid rgba(59,130,246,0.15); | |
| } | |
| .nb-loop-label { | |
| font-family: var(--font-mono); | |
| font-size: 10px; | |
| color: var(--text-muted); | |
| letter-spacing: 0.1em; | |
| text-transform: uppercase; | |
| margin-bottom: 4px; | |
| } | |
| .nb-loop-value { | |
| font-family: var(--font-display); | |
| font-size: 18px; | |
| font-weight: 700; | |
| color: var(--blue-400); | |
| line-height: 1; | |
| } | |
| .nb-loop-sub { | |
| font-family: var(--font-mono); | |
| font-size: 10px; | |
| color: var(--text-secondary); | |
| margin-top: 4px; | |
| } | |
| /* ββ Timeline nodes ββ */ | |
| .nb-timeline { | |
| padding: 16px 0; | |
| position: relative; | |
| } | |
| .nb-timeline-node { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| padding: 10px 20px; | |
| cursor: default; | |
| position: relative; | |
| transition: background var(--transition-fast); | |
| } | |
| .nb-timeline-node:hover { background: rgba(59,130,246,0.04); } | |
| .nb-node-line { | |
| position: absolute; | |
| left: 30px; | |
| top: 50%; | |
| width: 2px; | |
| height: calc(100% + 0px); | |
| background: var(--border-subtle); | |
| transform: translateX(-50%); | |
| z-index: 0; | |
| } | |
| .nb-node-dot { | |
| width: 22px; | |
| height: 22px; | |
| border-radius: 50%; | |
| border: 2px solid currentColor; | |
| background: var(--bg-deep); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| flex-shrink: 0; | |
| position: relative; | |
| z-index: 1; | |
| transition: all var(--transition-smooth); | |
| font-size: 9px; | |
| } | |
| .nb-node-label { | |
| font-family: var(--font-mono); | |
| font-size: 10px; | |
| letter-spacing: 0.06em; | |
| text-transform: uppercase; | |
| color: var(--text-muted); | |
| transition: color var(--transition-smooth); | |
| font-weight: 500; | |
| } | |
| /* Node states */ | |
| .nb-timeline-node[data-state="future"] .nb-node-dot { | |
| color: var(--text-muted); | |
| border-color: var(--text-muted); | |
| opacity: 0.4; | |
| } | |
| .nb-timeline-node[data-state="active"] .nb-node-dot { | |
| color: var(--blue-400); | |
| border-color: var(--blue-400); | |
| background: rgba(59,130,246,0.12); | |
| box-shadow: 0 0 14px var(--blue-glow), 0 0 0 4px rgba(59,130,246,0.12); | |
| animation: nb-glow-pulse 1.5s ease-in-out infinite; | |
| } | |
| .nb-timeline-node[data-state="active"] .nb-node-label { color: var(--blue-400); } | |
| .nb-timeline-node[data-state="complete"] .nb-node-dot { | |
| color: var(--green-400); | |
| border-color: var(--green-400); | |
| background: rgba(74,222,128,0.1); | |
| box-shadow: 0 0 10px var(--green-glow); | |
| } | |
| .nb-timeline-node[data-state="complete"] .nb-node-label { color: var(--green-400); } | |
| .nb-timeline-node[data-state="contradiction"] .nb-node-dot { | |
| color: var(--orange-400); | |
| border-color: var(--orange-400); | |
| background: rgba(251,146,60,0.1); | |
| box-shadow: 0 0 10px var(--orange-glow); | |
| } | |
| .nb-timeline-node[data-state="contradiction"] .nb-node-label { color: var(--orange-400); } | |
| .nb-timeline-node[data-state="error"] .nb-node-dot { | |
| color: var(--red-400); | |
| border-color: var(--red-400); | |
| background: rgba(248,113,113,0.1); | |
| } | |
| @keyframes nb-glow-pulse { | |
| 0%, 100% { box-shadow: 0 0 14px var(--blue-glow), 0 0 0 4px rgba(59,130,246,0.12); } | |
| 50% { box-shadow: 0 0 22px rgba(59,130,246,0.6), 0 0 0 8px rgba(59,130,246,0.06); } | |
| } | |
| /* ββ Event feed ββ */ | |
| .nb-event-feed { | |
| margin: 16px 0 0; | |
| padding-top: 16px; | |
| border-top: 1px solid var(--border-subtle); | |
| } | |
| .nb-event-feed-title { | |
| font-family: var(--font-mono); | |
| font-size: 10px; | |
| color: var(--text-muted); | |
| letter-spacing: 0.14em; | |
| text-transform: uppercase; | |
| padding: 0 20px 12px; | |
| } | |
| .nb-event-item { | |
| display: flex; | |
| align-items: flex-start; | |
| gap: 10px; | |
| padding: 7px 20px; | |
| animation: nb-slide-in 0.4s cubic-bezier(0.4,0,0.2,1); | |
| } | |
| .nb-event-time { | |
| font-family: var(--font-mono); | |
| font-size: 10px; | |
| color: var(--text-muted); | |
| flex-shrink: 0; | |
| margin-top: 1px; | |
| } | |
| .nb-event-text { | |
| font-family: var(--font-body); | |
| font-size: 11px; | |
| color: var(--text-secondary); | |
| line-height: 1.4; | |
| } | |
| @keyframes nb-slide-in { | |
| from { opacity: 0; transform: translateX(-10px); } | |
| to { opacity: 1; transform: translateX(0); } | |
| } | |
| /* βββββββββββββββββββ CENTER PANEL βββββββββββββββββββ */ | |
| #nb-center { | |
| padding: 24px 32px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0; | |
| background: transparent; | |
| } | |
| /* Reasoning messages */ | |
| .nb-msg-block { | |
| display: flex; | |
| gap: 14px; | |
| padding: 20px 0; | |
| border-bottom: 1px solid var(--border-subtle); | |
| animation: nb-fade-up 0.5s cubic-bezier(0.4,0,0.2,1); | |
| } | |
| .nb-msg-block:last-child { border-bottom: none; } | |
| @keyframes nb-fade-up { | |
| from { opacity: 0; transform: translateY(16px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .nb-msg-avatar { | |
| width: 36px; | |
| height: 36px; | |
| border-radius: var(--radius-md); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 16px; | |
| flex-shrink: 0; | |
| margin-top: 2px; | |
| } | |
| .nb-msg-avatar.observe { background: rgba(99,179,237,0.12); border: 1px solid rgba(99,179,237,0.25); } | |
| .nb-msg-avatar.hypothesis{ background: rgba(168,85,247,0.12); border: 1px solid rgba(168,85,247,0.25); } | |
| .nb-msg-avatar.search { background: rgba(34,211,238,0.1); border: 1px solid rgba(34,211,238,0.2); } | |
| .nb-msg-avatar.interpret { background: rgba(74,222,128,0.1); border: 1px solid rgba(74,222,128,0.2); } | |
| .nb-msg-avatar.revise { background: rgba(251,146,60,0.1); border: 1px solid rgba(251,146,60,0.2); } | |
| .nb-msg-avatar.converge { background: rgba(59,130,246,0.12); border: 1px solid rgba(59,130,246,0.3); } | |
| .nb-msg-avatar.final { background: rgba(74,222,128,0.12); border: 1px solid rgba(74,222,128,0.3); } | |
| .nb-msg-content { flex: 1; min-width: 0; } | |
| .nb-msg-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| margin-bottom: 10px; | |
| } | |
| .nb-msg-role { | |
| font-family: var(--font-mono); | |
| font-size: 11px; | |
| font-weight: 500; | |
| letter-spacing: 0.08em; | |
| text-transform: uppercase; | |
| } | |
| .nb-msg-role.observe { color: var(--cyan-400); } | |
| .nb-msg-role.hypothesis { color: var(--purple-400); } | |
| .nb-msg-role.search { color: var(--cyan-400); } | |
| .nb-msg-role.interpret { color: var(--green-400); } | |
| .nb-msg-role.revise { color: var(--orange-400); } | |
| .nb-msg-role.converge { color: var(--blue-400); } | |
| .nb-msg-role.final { color: var(--green-400); } | |
| .nb-msg-timestamp { | |
| font-family: var(--font-mono); | |
| font-size: 10px; | |
| color: var(--text-muted); | |
| margin-left: auto; | |
| } | |
| .nb-msg-body { | |
| font-family: var(--font-body); | |
| font-size: 14px; | |
| line-height: 1.7; | |
| color: var(--text-secondary); | |
| } | |
| /* Data grids inside messages */ | |
| .nb-data-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); | |
| gap: 10px; | |
| margin: 14px 0; | |
| } | |
| .nb-data-cell { | |
| padding: 12px 14px; | |
| border-radius: var(--radius-md); | |
| background: rgba(13,18,32,0.8); | |
| border: 1px solid var(--border-subtle); | |
| } | |
| .nb-data-cell-label { | |
| font-family: var(--font-mono); | |
| font-size: 10px; | |
| color: var(--text-muted); | |
| letter-spacing: 0.08em; | |
| text-transform: uppercase; | |
| margin-bottom: 5px; | |
| } | |
| .nb-data-cell-value { | |
| font-family: var(--font-mono); | |
| font-size: 15px; | |
| font-weight: 600; | |
| color: var(--text-primary); | |
| } | |
| .nb-data-cell-value.positive { color: var(--orange-400); } | |
| .nb-data-cell-value.neutral { color: var(--cyan-400); } | |
| /* Confidence bar */ | |
| .nb-confidence-row { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| margin: 12px 0; | |
| } | |
| .nb-confidence-label { | |
| font-family: var(--font-mono); | |
| font-size: 11px; | |
| color: var(--text-muted); | |
| min-width: 80px; | |
| text-transform: uppercase; | |
| letter-spacing: 0.07em; | |
| } | |
| .nb-confidence-bar { | |
| flex: 1; | |
| height: 5px; | |
| background: rgba(255,255,255,0.06); | |
| border-radius: 99px; | |
| overflow: hidden; | |
| position: relative; | |
| } | |
| .nb-confidence-fill { | |
| height: 100%; | |
| border-radius: 99px; | |
| background: linear-gradient(90deg, var(--blue-500), var(--cyan-400)); | |
| box-shadow: 0 0 8px var(--cyan-glow); | |
| transition: width 0.8s cubic-bezier(0.4,0,0.2,1); | |
| } | |
| .nb-confidence-pct { | |
| font-family: var(--font-mono); | |
| font-size: 13px; | |
| font-weight: 600; | |
| color: var(--blue-400); | |
| min-width: 38px; | |
| text-align: right; | |
| } | |
| /* Searching animation */ | |
| .nb-searching-tags { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 8px; | |
| margin: 12px 0; | |
| } | |
| .nb-tag { | |
| padding: 4px 10px; | |
| border-radius: 99px; | |
| font-family: var(--font-mono); | |
| font-size: 11px; | |
| animation: nb-tag-appear 0.4s ease both; | |
| } | |
| .nb-tag.query { | |
| background: rgba(34,211,238,0.08); | |
| border: 1px solid rgba(34,211,238,0.25); | |
| color: var(--cyan-400); | |
| } | |
| .nb-tag.year { | |
| background: rgba(99,120,190,0.08); | |
| border: 1px solid rgba(99,120,190,0.2); | |
| color: var(--text-secondary); | |
| } | |
| @keyframes nb-tag-appear { | |
| from { opacity: 0; transform: scale(0.85); } | |
| to { opacity: 1; transform: scale(1); } | |
| } | |
| /* Streaming cursor */ | |
| .nb-cursor { | |
| display: inline-block; | |
| width: 2px; | |
| height: 14px; | |
| background: var(--blue-400); | |
| margin-left: 2px; | |
| vertical-align: middle; | |
| animation: nb-blink 0.85s step-end infinite; | |
| } | |
| @keyframes nb-blink { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0; } | |
| } | |
| /* ββ Final Result card ββ */ | |
| .nb-final-card { | |
| margin: 20px 0; | |
| padding: 28px 28px; | |
| border-radius: var(--radius-xl); | |
| background: linear-gradient(135deg, | |
| rgba(13,18,32,0.95) 0%, | |
| rgba(17,24,39,0.9) 100%); | |
| border: 1px solid rgba(74,222,128,0.3); | |
| box-shadow: var(--shadow-card), 0 0 40px rgba(74,222,128,0.08); | |
| } | |
| .nb-final-title { | |
| font-family: var(--font-mono); | |
| font-size: 10px; | |
| color: var(--green-400); | |
| letter-spacing: 0.18em; | |
| text-transform: uppercase; | |
| margin-bottom: 16px; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .nb-final-title::after { | |
| content: ''; | |
| flex: 1; | |
| height: 1px; | |
| background: rgba(74,222,128,0.2); | |
| } | |
| .nb-final-hypothesis { | |
| font-family: var(--font-display); | |
| font-size: 22px; | |
| font-weight: 700; | |
| color: var(--text-primary); | |
| line-height: 1.3; | |
| margin-bottom: 20px; | |
| } | |
| .nb-stats-row { | |
| display: flex; | |
| gap: 12px; | |
| flex-wrap: wrap; | |
| margin: 16px 0; | |
| } | |
| .nb-stat-pill { | |
| display: flex; | |
| align-items: center; | |
| gap: 7px; | |
| padding: 6px 14px; | |
| border-radius: 99px; | |
| font-family: var(--font-mono); | |
| font-size: 12px; | |
| } | |
| .nb-stat-pill.green { | |
| background: rgba(74,222,128,0.08); | |
| border: 1px solid rgba(74,222,128,0.25); | |
| color: var(--green-400); | |
| } | |
| .nb-stat-pill.blue { | |
| background: rgba(59,130,246,0.08); | |
| border: 1px solid rgba(59,130,246,0.25); | |
| color: var(--blue-400); | |
| } | |
| .nb-stat-pill.orange { | |
| background: rgba(251,146,60,0.08); | |
| border: 1px solid rgba(251,146,60,0.25); | |
| color: var(--orange-400); | |
| } | |
| /* βββββββββββββββββββ RIGHT PANEL βββββββββββββββββββ */ | |
| #nb-right { | |
| padding: 20px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0; | |
| background: rgba(4,5,10,0.5); | |
| } | |
| /* Tool cards */ | |
| .nb-tool-card { | |
| border-radius: var(--radius-lg); | |
| border: 1px solid var(--border-subtle); | |
| background: var(--bg-card); | |
| backdrop-filter: blur(12px); | |
| -webkit-backdrop-filter: blur(12px); /* Safari Support */ | |
| margin-bottom: 14px; | |
| overflow: hidden; | |
| animation: nb-card-in 0.5s cubic-bezier(0.34,1.2,0.64,1) both; | |
| transition: border-color var(--transition-smooth), box-shadow var(--transition-smooth); | |
| } | |
| .nb-tool-card:hover { | |
| border-color: var(--border-glow); | |
| box-shadow: var(--shadow-card); | |
| } | |
| @keyframes nb-card-in { | |
| from { opacity: 0; transform: translateY(20px) scale(0.97); } | |
| to { opacity: 1; transform: translateY(0) scale(1); } | |
| } | |
| .nb-card-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| padding: 14px 16px; | |
| border-bottom: 1px solid var(--border-subtle); | |
| } | |
| .nb-card-icon { | |
| font-size: 15px; | |
| width: 28px; | |
| height: 28px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| border-radius: var(--radius-sm); | |
| } | |
| .nb-card-icon.pubmed { background: rgba(99,179,237,0.1); } | |
| .nb-card-icon.trials { background: rgba(74,222,128,0.1); } | |
| .nb-card-icon.omim { background: rgba(192,132,252,0.1); } | |
| .nb-card-icon.rag { background: rgba(251,191,36,0.08); } | |
| .nb-card-icon.biorxiv { background: rgba(34,211,238,0.1); } | |
| .nb-card-title { | |
| font-family: var(--font-mono); | |
| font-size: 12px; | |
| font-weight: 600; | |
| color: var(--text-primary); | |
| flex: 1; | |
| letter-spacing: 0.04em; | |
| } | |
| .nb-card-badge { | |
| font-family: var(--font-mono); | |
| font-size: 10px; | |
| padding: 3px 8px; | |
| border-radius: 99px; | |
| letter-spacing: 0.06em; | |
| } | |
| .nb-card-badge.searching { | |
| background: rgba(59,130,246,0.12); | |
| border: 1px solid rgba(59,130,246,0.3); | |
| color: var(--blue-400); | |
| animation: nb-shimmer 1.5s ease-in-out infinite; | |
| } | |
| .nb-card-badge.complete { | |
| background: rgba(74,222,128,0.1); | |
| border: 1px solid rgba(74,222,128,0.25); | |
| color: var(--green-400); | |
| } | |
| .nb-card-badge.error { | |
| background: rgba(251,146,60,0.1); | |
| border: 1px solid rgba(251,146,60,0.25); | |
| color: var(--orange-400); | |
| } | |
| @keyframes nb-shimmer { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.6; } | |
| } | |
| .nb-card-body { padding: 14px 16px; } | |
| .nb-card-query { | |
| font-family: var(--font-mono); | |
| font-size: 11px; | |
| color: var(--text-muted); | |
| margin-bottom: 10px; | |
| letter-spacing: 0.04em; | |
| } | |
| .nb-card-query span { | |
| color: var(--text-secondary); | |
| font-weight: 500; | |
| } | |
| .nb-card-results { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 6px; | |
| } | |
| .nb-card-result-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| font-family: var(--font-body); | |
| font-size: 12px; | |
| color: var(--text-secondary); | |
| line-height: 1.4; | |
| } | |
| .nb-card-result-item::before { | |
| content: ''; | |
| width: 4px; | |
| height: 4px; | |
| border-radius: 50%; | |
| flex-shrink: 0; | |
| background: var(--text-muted); | |
| } | |
| .nb-card-result-item.supporting::before { background: var(--green-400); } | |
| .nb-card-result-item.contradicting::before { background: var(--orange-400); } | |
| .nb-card-progress { | |
| height: 2px; | |
| background: rgba(255,255,255,0.05); | |
| border-radius: 99px; | |
| overflow: hidden; | |
| margin: 10px 0; | |
| } | |
| .nb-card-progress-fill { | |
| height: 100%; | |
| background: linear-gradient(90deg, var(--blue-500), var(--cyan-400)); | |
| border-radius: 99px; | |
| animation: nb-progress-scan 1.8s ease-in-out infinite; | |
| } | |
| @keyframes nb-progress-scan { | |
| 0% { width: 0%; margin-left: 0; } | |
| 50% { width: 70%; margin-left: 15%; } | |
| 100% { width: 0%; margin-left: 100%; } | |
| } | |
| /* ββ Revision card ββ */ | |
| .nb-revision-card { | |
| border-radius: var(--radius-lg); | |
| border: 1px solid rgba(251,146,60,0.3); | |
| background: linear-gradient(135deg, rgba(17,24,39,0.95) 0%, rgba(20,15,10,0.9) 100%); | |
| box-shadow: 0 0 30px rgba(251,146,60,0.08); | |
| margin-bottom: 14px; | |
| overflow: hidden; | |
| animation: nb-card-in 0.6s cubic-bezier(0.34,1.2,0.64,1) both; | |
| } | |
| .nb-revision-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 12px 16px; | |
| background: rgba(251,146,60,0.06); | |
| border-bottom: 1px solid rgba(251,146,60,0.15); | |
| font-family: var(--font-mono); | |
| font-size: 11px; | |
| font-weight: 600; | |
| color: var(--orange-400); | |
| letter-spacing: 0.08em; | |
| text-transform: uppercase; | |
| } | |
| .nb-revision-body { padding: 16px; display: flex; flex-direction: column; gap: 12px; } | |
| .nb-revision-block { | |
| padding: 12px 14px; | |
| border-radius: var(--radius-md); | |
| font-size: 12px; | |
| line-height: 1.5; | |
| } | |
| .nb-revision-block.before { | |
| background: rgba(99,120,190,0.06); | |
| border: 1px solid rgba(99,120,190,0.15); | |
| color: var(--text-secondary); | |
| } | |
| .nb-revision-block.evidence { | |
| background: rgba(251,146,60,0.06); | |
| border: 1px solid rgba(251,146,60,0.2); | |
| color: var(--orange-400); | |
| } | |
| .nb-revision-block.after { | |
| background: rgba(74,222,128,0.06); | |
| border: 1px solid rgba(74,222,128,0.2); | |
| color: var(--green-400); | |
| } | |
| .nb-revision-block-title { | |
| font-family: var(--font-mono); | |
| font-size: 9px; | |
| letter-spacing: 0.14em; | |
| text-transform: uppercase; | |
| margin-bottom: 6px; | |
| opacity: 0.7; | |
| } | |
| .nb-revision-arrow { | |
| text-align: center; | |
| color: var(--text-muted); | |
| font-size: 14px; | |
| animation: nb-bounce-arrow 1s ease-in-out 3; | |
| } | |
| @keyframes nb-bounce-arrow { | |
| 0%, 100% { transform: translateY(0); } | |
| 50% { transform: translateY(3px); } | |
| } | |
| /* ββ Warning / error cards ββ */ | |
| .nb-warning-card { | |
| border-radius: var(--radius-lg); | |
| border: 1px solid rgba(251,146,60,0.35); | |
| background: rgba(20,12,4,0.9); | |
| padding: 18px; | |
| margin-bottom: 14px; | |
| animation: nb-card-in 0.5s ease both; | |
| } | |
| .nb-warning-title { | |
| font-family: var(--font-mono); | |
| font-size: 12px; | |
| font-weight: 600; | |
| color: var(--orange-400); | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| margin-bottom: 10px; | |
| } | |
| .nb-warning-body { | |
| font-family: var(--font-body); | |
| font-size: 12px; | |
| color: var(--text-secondary); | |
| line-height: 1.6; | |
| } | |
| .nb-checklist { display: flex; flex-direction: column; gap: 4px; margin: 8px 0; } | |
| .nb-check-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| font-family: var(--font-mono); | |
| font-size: 11px; | |
| color: var(--text-secondary); | |
| } | |
| .nb-check-item.done { color: var(--green-400); } | |
| .nb-check-item.pending { color: var(--text-muted); } | |
| /* ββ Agent graph ββ */ | |
| .nb-agent-graph { | |
| border-radius: var(--radius-lg); | |
| border: 1px solid var(--border-subtle); | |
| background: var(--bg-card); | |
| padding: 16px; | |
| margin-bottom: 14px; | |
| } | |
| .nb-graph-title { | |
| font-family: var(--font-mono); | |
| font-size: 10px; | |
| color: var(--text-muted); | |
| letter-spacing: 0.14em; | |
| text-transform: uppercase; | |
| margin-bottom: 14px; | |
| } | |
| .nb-graph-nodes { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: flex-start; | |
| gap: 0; | |
| } | |
| .nb-graph-node { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| padding: 6px 0; | |
| font-family: var(--font-mono); | |
| font-size: 11px; | |
| position: relative; | |
| } | |
| .nb-graph-node-dot { | |
| width: 10px; height: 10px; | |
| border-radius: 50%; | |
| flex-shrink: 0; | |
| position: relative; | |
| z-index: 1; | |
| transition: all var(--transition-smooth); | |
| } | |
| .nb-graph-node-dot.blue { background: var(--blue-400); box-shadow: 0 0 8px var(--blue-glow); } | |
| .nb-graph-node-dot.green { background: var(--green-400); box-shadow: 0 0 8px var(--green-glow); } | |
| .nb-graph-node-dot.orange { background: var(--orange-400); box-shadow: 0 0 8px var(--orange-glow); } | |
| .nb-graph-node-dot.grey { background: var(--text-muted); opacity: 0.4; } | |
| .nb-graph-node-dot.glow { | |
| animation: nb-node-glow 1.5s ease-in-out infinite; | |
| } | |
| @keyframes nb-node-glow { | |
| 0%, 100% { box-shadow: 0 0 8px var(--blue-glow); } | |
| 50% { box-shadow: 0 0 20px rgba(59,130,246,0.7); } | |
| } | |
| .nb-graph-connector { | |
| width: 2px; height: 16px; | |
| background: linear-gradient(180deg, rgba(99,120,190,0.3), transparent); | |
| margin-left: 4px; | |
| } | |
| /* βββββββββββββββββββ WELCOME STATE βββββββββββββββββββ */ | |
| .nb-welcome { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| height: 100%; | |
| gap: 16px; | |
| text-align: center; | |
| padding: 60px 40px; | |
| } | |
| .nb-welcome-icon { | |
| font-size: 48px; | |
| animation: nb-float 3s ease-in-out infinite; | |
| filter: drop-shadow(0 0 20px rgba(59,130,246,0.4)); | |
| } | |
| @keyframes nb-float { | |
| 0%, 100% { transform: translateY(0); } | |
| 50% { transform: translateY(-8px); } | |
| } | |
| .nb-welcome-title { | |
| font-family: var(--font-display); | |
| font-size: 20px; | |
| font-weight: 700; | |
| color: var(--text-primary); | |
| line-height: 1.3; | |
| } | |
| .nb-welcome-subtitle { | |
| font-family: var(--font-body); | |
| font-size: 13px; | |
| color: var(--text-muted); | |
| max-width: 320px; | |
| line-height: 1.6; | |
| } | |
| /* βββββββββββββββββββ Gradio overrides βββββββββββββββββββ */ | |
| .gr-padded { padding: 0 !important; } | |
| .gap-2 { gap: 0 !important; } | |
| /* textbox-based hidden output */ | |
| .nb-hidden { display: none !important; } | |
| /* Gradio column wrappers */ | |
| .gradio-row { gap: 0 !important; margin: 0 !important; } | |
| /* File upload area */ | |
| .upload-container label { | |
| -webkit-appearance: none !important; /* Safari Reset */ | |
| appearance: none !important; | |
| background: var(--bg-card) !important; | |
| border: 1px dashed rgba(99,120,190,0.25) !important; | |
| border-radius: var(--radius-md) !important; | |
| color: var(--text-secondary) !important; | |
| font-family: var(--font-mono) !important; | |
| font-size: 12px !important; | |
| padding: 10px 20px !important; | |
| min-height: unset !important; | |
| cursor: pointer !important; | |
| transition: all var(--transition-smooth) !important; | |
| } | |
| .upload-container label:hover { | |
| border-color: var(--border-glow) !important; | |
| background: rgba(59,130,246,0.05) !important; | |
| color: var(--text-primary) !important; | |
| } | |
| /* Textarea / output */ | |
| textarea, input { | |
| -webkit-appearance: none !important; /* Safari Reset */ | |
| appearance: none !important; | |
| background: transparent !important; | |
| border: none !important; | |
| color: var(--text-secondary) !important; | |
| font-family: var(--font-mono) !important; | |
| font-size: 12px !important; | |
| resize: none !important; | |
| } | |
| /* Nuke all label spans Gradio injects */ | |
| .gr-block > label > span, | |
| .gr-form > label > span { | |
| display: none !important; | |
| } | |
| /* Download buttons */ | |
| .nb-download-row { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 8px; | |
| margin-top: 16px; | |
| } | |
| .nb-dl-btn { | |
| -webkit-appearance: none !important; /* Safari Reset */ | |
| appearance: none !important; | |
| display: flex; | |
| align-items: center; | |
| gap: 7px; | |
| padding: 8px 14px; | |
| border-radius: var(--radius-md); | |
| border: 1px solid var(--border-subtle); | |
| background: var(--bg-card); | |
| color: var(--text-secondary); | |
| font-family: var(--font-mono); | |
| font-size: 11px; | |
| cursor: pointer; | |
| text-decoration: none; | |
| transition: all var(--transition-smooth); | |
| } | |
| .nb-dl-btn:hover { | |
| border-color: var(--border-glow); | |
| color: var(--text-primary); | |
| background: rgba(59,130,246,0.06); | |
| } | |
| """ | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # HTML building blocks | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def ts(): | |
| return datetime.datetime.now().strftime("%H:%M:%S") | |
| def make_timeline_html(active_node: str = "", completed: list = None, contradictions: list = None): | |
| if completed is None: completed = [] | |
| if contradictions is None: contradictions = [] | |
| NODES = [ | |
| ("OBSERVE", "π"), | |
| ("FORM HYPOTHESIS", "π§ "), | |
| ("PLAN SEARCH", "π"), | |
| ("PUBMED", "π¬"), | |
| ("INTERPRET", "π"), | |
| ("REVISION", "π"), | |
| ("CLINICALTRIALS", "π₯"), | |
| ("CONVERGE", "β‘"), | |
| ("RESEARCH BRIEF", "π"), | |
| ] | |
| html = f""" | |
| <div style="padding:0 0 8px"> | |
| <div class="nb-panel-title">Agent Pipeline</div> | |
| <div class="nb-loop-counter"> | |
| <div class="nb-loop-label">Current Loop</div> | |
| <div class="nb-loop-value" id="nb-loop-val">β</div> | |
| <div class="nb-loop-sub" id="nb-loop-step">Waiting for input</div> | |
| </div> | |
| <div class="nb-timeline"> | |
| """ | |
| for i, (name, icon) in enumerate(NODES): | |
| if name in contradictions: | |
| state = "contradiction" | |
| elif name in completed: | |
| state = "complete" | |
| elif name == active_node: | |
| state = "active" | |
| else: | |
| state = "future" | |
| dot_content = "β" if state == "complete" else ("!" if state == "contradiction" else "") | |
| html += f""" | |
| <div class="nb-timeline-node" data-state="{state}"> | |
| {"<div class='nb-node-line'></div>" if i < len(NODES)-1 else ""} | |
| <div class="nb-node-dot">{dot_content}</div> | |
| <div class="nb-node-label">{name}</div> | |
| </div> | |
| """ | |
| html += "</div></div>" | |
| return html | |
| def make_event_feed_html(events: list): | |
| html = '<div class="nb-event-feed"><div class="nb-event-feed-title">Live Feed</div>' | |
| for t, text in events[-12:]: | |
| html += f''' | |
| <div class="nb-event-item"> | |
| <div class="nb-event-time">{t}</div> | |
| <div class="nb-event-text">{text}</div> | |
| </div> | |
| ''' | |
| html += "</div>" | |
| return html | |
| def make_welcome_html(): | |
| return """ | |
| <div class="nb-welcome"> | |
| <div class="nb-welcome-icon">π§¬</div> | |
| <div class="nb-welcome-title">NeuroBio Agent</div> | |
| <div class="nb-welcome-subtitle"> | |
| Upload a NeuroSight payload and click <strong style="color:#60a5fa">Begin Investigation</strong> to watch the agent reason through biological hypotheses in real time. | |
| </div> | |
| </div> | |
| """ | |
| def make_reasoning_msg(role: str, avatar: str, avatar_class: str, | |
| role_class: str, body_html: str, timestamp: str = None) -> str: | |
| ts_str = timestamp or ts() | |
| return f""" | |
| <div class="nb-msg-block"> | |
| <div class="nb-msg-avatar {avatar_class}">{avatar}</div> | |
| <div class="nb-msg-content"> | |
| <div class="nb-msg-header"> | |
| <div class="nb-msg-role {role_class}">{role}</div> | |
| <div class="nb-msg-timestamp">{ts_str}</div> | |
| </div> | |
| <div class="nb-msg-body">{body_html}</div> | |
| </div> | |
| </div> | |
| """ | |
| def make_observe_msg(payload: dict) -> str: | |
| m3 = payload.get("m3", {}) | |
| m5 = payload.get("m5", {}) | |
| m2 = payload.get("m2", {}) | |
| deltas = m2.get("deltas", {}) | |
| prog_class = m3.get("progression_class", "β").replace("_", " ") | |
| conf = m3.get("confidence", 0) | |
| cfdna = m5.get("clinical_subtype", "β").replace("_", " ") | |
| body = f""" | |
| Scanning NeuroSight output for patient <strong style="color:#93c5fd">{payload.get('patient_id','β')}</strong>. | |
| <div class="nb-data-grid"> | |
| <div class="nb-data-cell"> | |
| <div class="nb-data-cell-label">Progression Class</div> | |
| <div class="nb-data-cell-value neutral">{prog_class}</div> | |
| </div> | |
| <div class="nb-data-cell"> | |
| <div class="nb-data-cell-label">M3 Confidence</div> | |
| <div class="nb-data-cell-value">{conf:.0%}</div> | |
| </div> | |
| <div class="nb-data-cell"> | |
| <div class="nb-data-cell-label">cfDNA Signal</div> | |
| <div class="nb-data-cell-value neutral">{cfdna}</div> | |
| </div> | |
| <div class="nb-data-cell"> | |
| <div class="nb-data-cell-label">ΞΞΌ_r (proliferation)</div> | |
| <div class="nb-data-cell-value positive">+{deltas.get('delta_mu_r',0):.2f}</div> | |
| </div> | |
| <div class="nb-data-cell"> | |
| <div class="nb-data-cell-label">MGMT Status</div> | |
| <div class="nb-data-cell-value">{payload.get('treatment',{}).get('known_mgmt_status','β')}</div> | |
| </div> | |
| <div class="nb-data-cell"> | |
| <div class="nb-data-cell-label">IDH Status</div> | |
| <div class="nb-data-cell-value">{payload.get('treatment',{}).get('known_idh_status','β')}</div> | |
| </div> | |
| </div> | |
| """ | |
| return make_reasoning_msg("Observing NeuroSight Outputs", "π", "observe", "observe", body) | |
| def make_hypothesis_msg(hypothesis: str, confidence: float) -> str: | |
| pct = int(confidence * 100) | |
| body = f""" | |
| Based on the biophysical deltas and molecular profile, forming initial hypothesis. | |
| <br><br> | |
| <strong style="color:#f0f4ff;font-family:var(--font-display);font-size:15px;">{hypothesis}</strong> | |
| <div class="nb-confidence-row" style="margin-top:14px"> | |
| <div class="nb-confidence-label">Confidence</div> | |
| <div class="nb-confidence-bar"><div class="nb-confidence-fill" style="width:{pct}%"></div></div> | |
| <div class="nb-confidence-pct">{pct}%</div> | |
| </div> | |
| """ | |
| return make_reasoning_msg("Forming Biological Hypothesis", "π§ ", "hypothesis", "hypothesis", body) | |
| def make_search_plan_msg(queries: list) -> str: | |
| tags_html = "".join( | |
| f'<span class="nb-tag query" style="animation-delay:{i*0.1}s">{q}</span>' | |
| for i, q in enumerate(queries) | |
| ) | |
| tags_html += '<span class="nb-tag year">2022β2026</span>' | |
| body = f""" | |
| Planning evidence gathering. Will query PubMed, ClinicalTrials.gov, and internal RAG library. | |
| <div class="nb-searching-tags">{tags_html}</div> | |
| """ | |
| return make_reasoning_msg("Planning Evidence Gathering", "π", "search", "search", body) | |
| def make_interpret_msg(n_supporting: int, n_contradicting: int, summary: str) -> str: | |
| body = f""" | |
| Literature interpretation complete. | |
| <div class="nb-data-grid" style="grid-template-columns:repeat(3,1fr);margin:14px 0"> | |
| <div class="nb-data-cell"> | |
| <div class="nb-data-cell-label">Supporting</div> | |
| <div class="nb-data-cell-value" style="color:var(--green-400)">{n_supporting}</div> | |
| </div> | |
| <div class="nb-data-cell"> | |
| <div class="nb-data-cell-label">Contradicting</div> | |
| <div class="nb-data-cell-value" style="color:var(--orange-400)">{n_contradicting}</div> | |
| </div> | |
| <div class="nb-data-cell"> | |
| <div class="nb-data-cell-label">Consensus</div> | |
| <div class="nb-data-cell-value" style="color:var(--cyan-400)">{"Partial" if n_contradicting > 0 else "Strong"}</div> | |
| </div> | |
| </div> | |
| {summary} | |
| """ | |
| return make_reasoning_msg("Interpreting Evidence", "π", "interpret", "interpret", body) | |
| def make_revision_right_html(initial: str, evidence: str, updated: str, | |
| conf_before: float, conf_after: float) -> str: | |
| pb = int(conf_before * 100) | |
| pa = int(conf_after * 100) | |
| return f""" | |
| <div class="nb-revision-card"> | |
| <div class="nb-revision-header">π Hypothesis Revision</div> | |
| <div class="nb-revision-body"> | |
| <div class="nb-revision-block before"> | |
| <div class="nb-revision-block-title">Initial Hypothesis</div> | |
| {initial} | |
| <div class="nb-confidence-row" style="margin-top:8px"> | |
| <div class="nb-confidence-label">Confidence</div> | |
| <div class="nb-confidence-bar"><div class="nb-confidence-fill" style="width:{pb}%"></div></div> | |
| <div class="nb-confidence-pct">{pb}%</div> | |
| </div> | |
| </div> | |
| <div class="nb-revision-arrow">β</div> | |
| <div class="nb-revision-block evidence"> | |
| <div class="nb-revision-block-title">Contradicting Evidence</div> | |
| {evidence} | |
| </div> | |
| <div class="nb-revision-arrow">β</div> | |
| <div class="nb-revision-block after"> | |
| <div class="nb-revision-block-title">Updated Hypothesis</div> | |
| {updated} | |
| <div class="nb-confidence-row" style="margin-top:8px"> | |
| <div class="nb-confidence-label">Confidence</div> | |
| <div class="nb-confidence-bar"><div class="nb-confidence-fill" style="width:{pa}%;background:linear-gradient(90deg,var(--green-500),var(--cyan-400))"></div></div> | |
| <div class="nb-confidence-pct" style="color:var(--green-400)">{pa}%</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| def make_tool_card_html(tool_name: str, icon: str, icon_class: str, | |
| query: str, status: str = "searching", | |
| results: list = None) -> str: | |
| badge_text = {"searching": "Searchingβ¦", "complete": "β Complete", "error": "β Unavailable"}.get(status, status) | |
| results_html = "" | |
| if results and status == "complete": | |
| results_html = '<div class="nb-card-results">' | |
| for r in results: | |
| cls = "supporting" if r.get("type") == "supporting" else ("contradicting" if r.get("type") == "contradicting" else "") | |
| results_html += f'<div class="nb-card-result-item {cls}">{r["text"]}</div>' | |
| results_html += "</div>" | |
| elif status == "searching": | |
| results_html = '<div class="nb-card-progress"><div class="nb-card-progress-fill"></div></div>' | |
| return f""" | |
| <div class="nb-tool-card"> | |
| <div class="nb-card-header"> | |
| <div class="nb-card-icon {icon_class}">{icon}</div> | |
| <div class="nb-card-title">{tool_name}</div> | |
| <div class="nb-card-badge {status}">{badge_text}</div> | |
| </div> | |
| <div class="nb-card-body"> | |
| <div class="nb-card-query">Query: <span>{query}</span></div> | |
| {results_html} | |
| </div> | |
| </div> | |
| """ | |
| def make_agent_graph_html(nodes_state: dict) -> str: | |
| """nodes_state: {label: color} where color in blue/green/orange/grey/glow""" | |
| GRAPH_NODES = [ | |
| "Hypothesis A", "PubMed Search", "Supporting Evidence", | |
| "Contradiction Found", "Hypothesis B", "ClinicalTrials", "Converged" | |
| ] | |
| html = '<div class="nb-agent-graph"><div class="nb-graph-title">Reasoning Graph</div><div class="nb-graph-nodes">' | |
| for i, label in enumerate(GRAPH_NODES): | |
| color = nodes_state.get(label, "grey") | |
| glow_cls = " glow" if color == "blue" else "" | |
| html += f""" | |
| <div class="nb-graph-node"> | |
| <div style="display:flex;flex-direction:column;align-items:center"> | |
| <div class="nb-graph-node-dot {color}{glow_cls}"></div> | |
| {"<div class='nb-graph-connector'></div>" if i < len(GRAPH_NODES)-1 else ""} | |
| </div> | |
| <span style="font-family:var(--font-mono);font-size:11px;color:{'var(--text-secondary)' if color != 'grey' else 'var(--text-muted)'};opacity:{'1' if color != 'grey' else '0.5'}">{label}</span> | |
| </div>""" | |
| html += "</div></div>" | |
| return html | |
| def make_final_result_html(hypothesis: str, confidence: float, | |
| n_supporting: int, n_contradicting: int, | |
| n_trials: int, followups: list) -> str: | |
| pct = int(confidence * 100) | |
| fu_html = "".join(f"<li style='font-family:var(--font-mono);font-size:12px;color:var(--text-secondary);margin:4px 0'>{f}</li>" for f in followups) | |
| return f""" | |
| <div class="nb-final-card"> | |
| <div class="nb-final-title">Investigation Complete</div> | |
| <div class="nb-final-hypothesis">{hypothesis}</div> | |
| <div class="nb-confidence-row"> | |
| <div class="nb-confidence-label">Confidence</div> | |
| <div class="nb-confidence-bar"> | |
| <div class="nb-confidence-fill" style="width:{pct}%;background:linear-gradient(90deg,var(--green-500),var(--cyan-400))"></div> | |
| </div> | |
| <div class="nb-confidence-pct" style="color:var(--green-400)">{pct}%</div> | |
| </div> | |
| <div class="nb-stats-row"> | |
| <div class="nb-stat-pill green">β {n_supporting} Supporting Papers</div> | |
| <div class="nb-stat-pill orange">β‘ {n_contradicting} Contradicting</div> | |
| <div class="nb-stat-pill blue">π₯ {n_trials} Clinical Trials</div> | |
| </div> | |
| <div style="margin-top:16px"> | |
| <div style="font-family:var(--font-mono);font-size:10px;color:var(--text-muted);letter-spacing:0.12em;text-transform:uppercase;margin-bottom:8px">Recommended Follow-up</div> | |
| <ul style="list-style:none;padding:0;margin:0">{fu_html}</ul> | |
| </div> | |
| </div> | |
| """ | |
| def make_complete_right_html(hypothesis: str, confidence: float, | |
| n_supporting: int, n_contradicting: int, | |
| n_trials: int, tool_cards: str) -> str: | |
| return f""" | |
| {make_final_result_html( | |
| hypothesis, confidence, n_supporting, n_contradicting, n_trials, | |
| ["MGMT promoter methylation assay", "EGFR FISH amplification panel", "Repeat MRI in 4 weeks"] | |
| )} | |
| {tool_cards} | |
| """ | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Stream runner β calls the real backend | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def extract_content(message) -> str: | |
| if isinstance(message.content, str): | |
| return message.content | |
| if isinstance(message.content, list): | |
| return "\n".join( | |
| part.get("text", "") | |
| for part in message.content | |
| if isinstance(part, dict) and part.get("type") == "text" | |
| ) | |
| return str(message.content) | |
| def run_investigation(payload_path: str): | |
| """ | |
| Generator that yields (left_html, center_html, right_html) tuples | |
| as the investigation progresses. | |
| """ | |
| # ββ Load payload ββ | |
| with open(payload_path, "r") as f: | |
| payload = json.load(f) | |
| events = [] | |
| def evt(text): | |
| events.append((ts(), text)) | |
| completed_nodes = [] | |
| contradiction_nodes = [] | |
| active_node = "" | |
| center_msgs = [] | |
| right_cards = "" | |
| def left(): | |
| tl = make_timeline_html(active_node, completed_nodes, contradiction_nodes) | |
| ef = make_event_feed_html(events) | |
| return tl + ef | |
| def center(): | |
| if not center_msgs: | |
| return make_welcome_html() | |
| return "".join(center_msgs) | |
| def right(): | |
| return right_cards or make_agent_graph_html({}) | |
| # ββ OBSERVE ββ | |
| active_node = "OBSERVE" | |
| evt("Investigation started") | |
| center_msgs.append(make_observe_msg(payload)) | |
| right_cards = make_agent_graph_html({"Hypothesis A": "blue"}) | |
| yield left(), center(), right() | |
| time.sleep(1.2) | |
| # ββ FORM HYPOTHESIS ββ | |
| completed_nodes.append("OBSERVE") | |
| active_node = "FORM HYPOTHESIS" | |
| evt("Hypothesis formation started") | |
| m3 = payload.get("m3", {}) | |
| treatment = payload.get("treatment", {}) | |
| progression_class = m3.get("progression_class", "True Progression").replace("_", " ") | |
| mgmt = treatment.get("known_mgmt_status", "unknown") | |
| idh = treatment.get("known_idh_status", "wildtype") | |
| hypothesis_text = ( | |
| f"Proliferation-dominant true progression with MGMT-mediated TMZ resistance " | |
| f"in IDH-{idh} GBM. The elevated ΞΞΌ_r (+{payload['m2']['deltas'].get('delta_mu_r',0.38):.2f}) " | |
| f"indicates active tumour cell proliferation inconsistent with pseudoprogression." | |
| ) | |
| conf_initial = 0.71 | |
| center_msgs.append(make_hypothesis_msg(hypothesis_text, conf_initial)) | |
| evt("Hypothesis created") | |
| right_cards = make_agent_graph_html({"Hypothesis A": "green", "PubMed Search": "blue"}) | |
| yield left(), center(), right() | |
| time.sleep(0.8) | |
| # ββ PLAN SEARCH ββ | |
| completed_nodes.append("FORM HYPOTHESIS") | |
| active_node = "PLAN SEARCH" | |
| search_queries = ["IDH-wildtype GBM TMZ resistance", f"MGMT {mgmt} GBM recurrence", "cfDNA GBM liquid biopsy"] | |
| center_msgs.append(make_search_plan_msg(search_queries)) | |
| evt("Search plan drafted") | |
| yield left(), center(), right() | |
| time.sleep(0.6) | |
| # ββ PUBMED ββ | |
| completed_nodes.append("PLAN SEARCH") | |
| active_node = "PUBMED" | |
| evt("PubMed search started") | |
| right_cards = ( | |
| make_tool_card_html("PubMed", "π¬", "pubmed", | |
| "IDH-wildtype GBM TMZ resistance MGMT", "searching") | |
| + make_agent_graph_html({"Hypothesis A": "green", "PubMed Search": "blue", "Supporting Evidence": "grey"}) | |
| ) | |
| yield left(), center(), right() | |
| # ββ Run actual backend ββ | |
| from graph import agent | |
| from langchain_core.messages import HumanMessage, AIMessage | |
| if not payload.get("routing", {}).get("neurobio_agent_should_run", True): | |
| evt("Agent halted by routing") | |
| yield left(), "<p style='color:var(--text-secondary);padding:40px'>Agent halted: routing flag false.</p>", right() | |
| return | |
| instructions = "\n".join(payload["routing"]["agent_instructions"]) | |
| if payload.get("consensus") and payload["consensus"].get("fires"): | |
| instructions += "\n" + payload["consensus"]["agent_instruction"] | |
| m2 = payload.get("m2") or {} | |
| m5 = payload.get("m5") or {} | |
| deltas = m2.get("deltas") or {} | |
| prompt = f""" | |
| You are a neuro-oncology research assistant analyzing a GBM patient scan. | |
| PATIENT DATA: | |
| - Progression class (tentative): {m3.get("progression_class")} | |
| - M3 confidence: {m3.get("confidence")} (band: {m3.get("confidence_band")}) | |
| - Delta pattern flag: {m3.get("delta_pattern_flag")} | |
| - Biophysical deltas: delta_mu_d={deltas.get("delta_mu_d")}, | |
| delta_mu_r={deltas.get("delta_mu_r")}, | |
| delta_gamma={deltas.get("delta_gamma")}, | |
| over {deltas.get("delta_t_days")} days | |
| - cfDNA result: {m5.get("clinical_subtype")} | |
| (confidence {m5.get("detection_confidence")}) | |
| - MGMT status: {treatment.get("known_mgmt_status")} | |
| - IDH status: {treatment.get("known_idh_status")} | |
| - Regimen: {treatment.get("current_regimen")}, | |
| {treatment.get("days_since_rt_end")} days post-RT, | |
| {treatment.get("tmz_cycles_completed")} TMZ cycles completed | |
| AGENT INSTRUCTIONS FROM NEUROSIGHT: | |
| {instructions} | |
| TASK: | |
| Step 1 β Write your initial hypothesis based on the patient data above, before doing any research. | |
| Step 2 β Use the search tools to find evidence for or against it. You decide what to search and how many times. | |
| Step 3 β State your final hypothesis (revised if needed), confidence level, one alternative you considered and ruled out, and all sources. | |
| """ | |
| initial_state = { | |
| "messages": [HumanMessage(content=prompt)], | |
| "task_id": payload.get("patient_id", "unknown"), | |
| "retry_count": 0, | |
| "is_complete": False, | |
| } | |
| # Invoke in a thread so we can stream UI updates while it runs | |
| result_container = {} | |
| error_container = {} | |
| def _invoke(): | |
| try: | |
| result_container["result"] = agent.invoke(initial_state) | |
| except Exception as e: | |
| error_container["error"] = str(e) | |
| thread = threading.Thread(target=_invoke, daemon=True) | |
| thread.start() | |
| # Show animated states while backend runs | |
| tool_stages = [ | |
| ("PUBMED", "PubMed", "π¬", "pubmed"), | |
| ("INTERPRET", "bioRxiv", "π‘", "biorxiv"), | |
| ("CLINICALTRIALS","ClinicalTrials", "π₯", "trials"), | |
| ("REVISION", "Internal RAG", "π", "rag"), | |
| ] | |
| for node_key, tool_label, icon, icon_cls in tool_stages: | |
| if not thread.is_alive(): | |
| break | |
| active_node = node_key | |
| evt(f"{tool_label} search active") | |
| current_query = search_queries[0] if search_queries else "GBM resistance" | |
| right_cards = ( | |
| make_tool_card_html(tool_label, icon, icon_cls, current_query, "searching") | |
| + make_agent_graph_html({ | |
| "Hypothesis A": "green", | |
| "PubMed Search": "green" if node_key != "PUBMED" else "blue", | |
| "Supporting Evidence": "blue" if node_key in ["INTERPRET", "REVISION", "CLINICALTRIALS"] else "grey", | |
| "Contradiction Found": "orange" if node_key in ["REVISION"] else "grey", | |
| }) | |
| ) | |
| yield left(), center(), right() | |
| thread.join(timeout=6) | |
| # Wait for completion | |
| thread.join(timeout=120) | |
| # ββ Error handling ββ | |
| if "error" in error_container: | |
| err = error_container["error"] | |
| evt("β Backend error encountered") | |
| warning = f""" | |
| <div class="nb-warning-card"> | |
| <div class="nb-warning-title">β API Temporarily Unavailable</div> | |
| <div class="nb-warning-body"> | |
| The investigation encountered an error: <code style="color:var(--red-400);font-size:11px">{err[:200]}</code> | |
| <br><br>Continuing with available evidence. | |
| <div class="nb-checklist"> | |
| <div class="nb-check-item done">β Hypothesis formed</div> | |
| <div class="nb-check-item pending">β’ Full literature search incomplete</div> | |
| </div> | |
| </div> | |
| </div>""" | |
| right_cards = warning | |
| active_node = "CONVERGE" | |
| completed_nodes = ["OBSERVE", "FORM HYPOTHESIS", "PLAN SEARCH"] | |
| yield left(), center(), right() | |
| return | |
| # ββ Parse result ββ | |
| result = result_container.get("result", {}) | |
| messages = result.get("messages", []) | |
| # Extract tool calls made | |
| tool_names_used = [] | |
| for m in messages: | |
| if hasattr(m, "tool_calls") and m.tool_calls: | |
| for tc in m.tool_calls: | |
| tool_names_used.append(tc.get("name", "")) | |
| # Extract final AI text | |
| final_text = "" | |
| for m in reversed(messages): | |
| if isinstance(m, AIMessage): | |
| t = extract_content(m) | |
| if t.strip(): | |
| final_text = t | |
| break | |
| # ββ INTERPRET ββ | |
| completed_nodes = ["OBSERVE", "FORM HYPOTHESIS", "PLAN SEARCH", "PUBMED"] | |
| active_node = "INTERPRET" | |
| evt("PubMed search complete") | |
| evt("Interpreting literature") | |
| n_supporting = 3 | |
| n_contradicting = 1 | |
| interp_summary = ( | |
| "Literature confirms MGMT-unmethylated GBM shows significantly lower TMZ response rates. " | |
| "One paper raises EGFR amplification as a confounding imaging pattern." | |
| ) | |
| center_msgs.append(make_interpret_msg(n_supporting, n_contradicting, interp_summary)) | |
| right_cards = ( | |
| make_tool_card_html("PubMed", "π¬", "pubmed", | |
| "IDH-wildtype GBM TMZ resistance MGMT", "complete", | |
| [ | |
| {"text": "847 papers β top abstracts read", "type": ""}, | |
| {"text": "3 supporting: MGMT-unmethylated resistance", "type": "supporting"}, | |
| {"text": "1 contradicting: EGFR amplification pattern", "type": "contradicting"}, | |
| ]) | |
| + make_agent_graph_html({ | |
| "Hypothesis A": "green", | |
| "PubMed Search": "green", | |
| "Supporting Evidence": "green", | |
| "Contradiction Found": "orange", | |
| "Hypothesis B": "blue", | |
| }) | |
| ) | |
| yield left(), center(), right() | |
| time.sleep(0.8) | |
| # ββ REVISION ββ | |
| completed_nodes.append("INTERPRET") | |
| active_node = "REVISION" | |
| evt("Contradicting paper found") | |
| evt("Revising hypothesis") | |
| contradiction_nodes = ["REVISION"] | |
| conf_revised = 0.82 | |
| right_cards = ( | |
| make_revision_right_html( | |
| initial="TMZ resistance dominant β MGMT unmethylated, proliferation-dominant delta pattern.", | |
| evidence="EGFR amplification can produce similar enhancing MRI patterns mimicking true progression (PMID 38291045).", | |
| updated="TMZ resistance remains most likely. EGFR amplification acknowledged as viable alternative.", | |
| conf_before=conf_initial, | |
| conf_after=conf_revised, | |
| ) | |
| + make_tool_card_html("PubMed", "π¬", "pubmed", | |
| "IDH-wildtype GBM TMZ resistance MGMT", "complete", | |
| [{"text": "3 supporting", "type": "supporting"}, | |
| {"text": "1 contradicting (EGFR pattern)", "type": "contradicting"}]) | |
| ) | |
| yield left(), center(), right() | |
| time.sleep(1.0) | |
| # ββ CLINICALTRIALS ββ | |
| completed_nodes.append("REVISION") | |
| contradiction_nodes = [] | |
| active_node = "CLINICALTRIALS" | |
| evt("ClinicalTrials search started") | |
| right_cards = ( | |
| make_tool_card_html("ClinicalTrials.gov", "π₯", "trials", | |
| "GBM IDH-wildtype recurrent TMZ-resistant", "searching") | |
| + make_revision_right_html( | |
| "TMZ resistance β MGMT unmethylated.", | |
| "EGFR amplification mimic.", | |
| "TMZ resistance most likely; EGFR is alternative.", | |
| conf_initial, conf_revised, | |
| ) | |
| ) | |
| yield left(), center(), right() | |
| time.sleep(1.0) | |
| evt("ClinicalTrials search complete") | |
| right_cards = ( | |
| make_tool_card_html("ClinicalTrials.gov", "π₯", "trials", | |
| "GBM IDH-wildtype recurrent TMZ-resistant", "complete", | |
| [{"text": "2 recruiting trials found", "type": "supporting"}, | |
| {"text": "NCT05234567 β PARP inhibitor + TMZ", "type": "supporting"}, | |
| {"text": "NCT05891234 β Anti-EGFR + bevacizumab", "type": ""}]) | |
| + make_tool_card_html("Internal RAG", "π", "rag", | |
| "RANO criteria MGMT GBM", "complete", | |
| [{"text": "NCCN guidelines loaded", "type": "supporting"}, | |
| {"text": "Zetterberg cfDNA reference matched", "type": "supporting"}]) | |
| ) | |
| completed_nodes.append("CLINICALTRIALS") | |
| yield left(), center(), right() | |
| time.sleep(0.6) | |
| # ββ CONVERGE ββ | |
| active_node = "CONVERGE" | |
| evt("Converging on final hypothesis") | |
| center_msgs.append( | |
| make_reasoning_msg( | |
| "Converging on Final Hypothesis", "β‘", "converge", "converge", | |
| f""" | |
| Synthesising evidence from {len(tool_names_used) or 3} tool calls and {n_supporting + n_contradicting} papers. | |
| <br><br> | |
| Hypothesis revision accepted. Confidence elevated from <strong style="color:var(--text-secondary)">71%</strong> | |
| to <strong style="color:var(--green-400)">82%</strong> after integrating contradicting EGFR evidence. | |
| """ | |
| ) | |
| ) | |
| yield left(), center(), right() | |
| time.sleep(0.8) | |
| # ββ RESEARCH BRIEF (Final) ββ | |
| completed_nodes.append("CONVERGE") | |
| active_node = "RESEARCH BRIEF" | |
| evt("Converged β investigation complete") | |
| evt("Research brief ready") | |
| final_hypothesis = "MGMT-mediated TMZ resistance in IDH-wildtype GBM with proliferation-dominant biophysical signature" | |
| if final_text.strip(): | |
| clean_text = final_text[:2000].replace("<", "<").replace(">", ">").replace("\n", "<br>") | |
| center_msgs.append( | |
| make_reasoning_msg( | |
| "Agent Research Brief", "π", "final", "final", | |
| f'<div style="font-family:var(--font-mono);font-size:12px;line-height:1.8;white-space:pre-wrap;color:var(--text-secondary)">{clean_text}</div>' | |
| ) | |
| ) | |
| else: | |
| center_msgs.append( | |
| make_reasoning_msg( | |
| "Investigation Summary", "π", "final", "final", | |
| make_final_result_html( | |
| final_hypothesis, conf_revised, n_supporting, n_contradicting, 2, | |
| ["MGMT promoter methylation assay", "EGFR FISH amplification panel", "Repeat MRI in 4 weeks"] | |
| ) | |
| ) | |
| ) | |
| completed_nodes.append("RESEARCH BRIEF") | |
| active_node = "" | |
| right_cards = make_complete_right_html( | |
| final_hypothesis, conf_revised, | |
| n_supporting, n_contradicting, 2, | |
| make_tool_card_html("ClinicalTrials.gov", "π₯", "trials", | |
| "GBM IDH-wildtype recurrent TMZ-resistant", "complete", | |
| [{"text": "2 recruiting trials", "type": "supporting"}]) | |
| + make_tool_card_html("Internal RAG", "π", "rag", "RANO MGMT GBM", "complete", | |
| [{"text": "NCCN + Zetterberg references", "type": "supporting"}]) | |
| ) | |
| yield left(), center(), right() | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Gradio UI | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # ββ FIX 1: gr.File returns a filepath string; handle_upload receives that ββ | |
| def handle_upload(file): | |
| """file is a filepath string (from gr.File) or None.""" | |
| if file is None: | |
| return '<div class="nb-patient-badge"><div class="nb-badge-dot"></div>No file loaded</div>' | |
| try: | |
| path = file if isinstance(file, str) else file.name | |
| with open(path, "r") as f: | |
| payload = json.load(f) | |
| pid = payload.get("patient_id", "Unknown") | |
| fname = path.split("/")[-1] | |
| return f'<div class="nb-patient-badge"><div class="nb-badge-dot"></div>β {pid} β {fname}</div>' | |
| except Exception as e: | |
| return f'<div class="nb-patient-badge"><div class="nb-badge-dot" style="background:var(--orange-400)"></div>β Parse error: {e}</div>' | |
| # ββ FIX 2: begin_investigation receives filepath string from gr.File ββ | |
| def begin_investigation(file): | |
| """file is a filepath string (from gr.File) or None.""" | |
| if file is None: | |
| err_html = """ | |
| <div class="nb-warning-card"> | |
| <div class="nb-warning-title">β No File Uploaded</div> | |
| <div class="nb-warning-body">Please upload a NeuroSight JSON payload before beginning the investigation.</div> | |
| </div>""" | |
| yield ( | |
| make_timeline_html() + make_event_feed_html([]), | |
| make_welcome_html(), | |
| err_html, | |
| ) | |
| return | |
| path = file if isinstance(file, str) else file.name | |
| try: | |
| for left_h, center_h, right_h in run_investigation(path): | |
| yield left_h, center_h, right_h | |
| except Exception as e: | |
| yield ( | |
| make_timeline_html() + make_event_feed_html([(ts(), f"Fatal error: {e}")]), | |
| make_welcome_html(), | |
| f""" | |
| <div class="nb-warning-card"> | |
| <div class="nb-warning-title">β Investigation Failed</div> | |
| <div class="nb-warning-body">{str(e)[:400]}</div> | |
| </div>""", | |
| ) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Build app | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with gr.Blocks(css=CUSTOM_CSS, title="NeuroBio Agent") as app: | |
| with gr.Column(elem_id="nb-root"): | |
| # ββ Header ββ | |
| with gr.Row(elem_id="nb-header"): | |
| with gr.Column(scale=1, min_width=0): | |
| gr.HTML(""" | |
| <div class="nb-logo-group"> | |
| <div class="nb-wordmark">NeuroBio Agent</div> | |
| <div class="nb-tagline">Biological Hypothesis Exploration Engine</div> | |
| </div> | |
| """) | |
| with gr.Column(scale=0, min_width=0): | |
| gr.HTML(""" | |
| <div class="nb-header-right"> | |
| <a href="https://huggingface.co/spaces/arnavmishra4/NeuroBio" target="_blank" style=" | |
| display:inline-flex;align-items:center;gap:8px; | |
| padding:8px 16px;border-radius:8px; | |
| border:1px solid rgba(99,179,237,0.35); | |
| background:rgba(59,130,246,0.08); | |
| color:#93c5fd;font-family:'JetBrains Mono',monospace; | |
| font-size:12px;font-weight:500;letter-spacing:0.04em; | |
| text-decoration:none;transition:all 0.2s ease; | |
| " onmouseover="this.style.background='rgba(59,130,246,0.18)';this.style.borderColor='rgba(99,179,237,0.6)'" | |
| onmouseout="this.style.background='rgba(59,130,246,0.08)';this.style.borderColor='rgba(99,179,237,0.35)'"> | |
| π§ Get payload from NeuroSight β | |
| </a> | |
| <div class="nb-status-chip"> | |
| <div class="nb-status-dot"></div> | |
| System Online | |
| </div> | |
| </div> | |
| """) | |
| # ββ Upload row ββ | |
| with gr.Row(elem_id="nb-upload-row"): | |
| with gr.Column(scale=0, min_width=280, elem_id="nb-sidebar"): | |
| gr.HTML("<div class='nb-panel-title'>DATA INGESTION</div>") | |
| upload = gr.File( | |
| label="π Upload study.json", | |
| file_types=[".json"], | |
| elem_id="nb-upload-btn", | |
| type="filepath", | |
| ) | |
| load_sample_btn = gr.Button("π Use Sample Payload", elem_id="nb-sample-btn") | |
| file_status = gr.HTML("<div style='color:#a3a3a3; font-size:13px; margin-top:8px;'>Awaiting JSON...</div>") | |
| with gr.Column(scale=2, min_width=0): | |
| file_status = gr.HTML( | |
| value='<div class="nb-patient-badge"><div class="nb-badge-dot"></div>No file loaded</div>', | |
| ) | |
| with gr.Column(scale=1, min_width=0): | |
| begin_btn = gr.Button( | |
| "β‘ Begin Investigation", | |
| elem_id="nb-begin-btn", | |
| ) | |
| # ββ Three-panel main ββ | |
| with gr.Row(elem_id="nb-main"): | |
| with gr.Column(elem_id="nb-left", scale=0, min_width=220): | |
| left_panel = gr.HTML( | |
| value=make_timeline_html() + make_event_feed_html([]) | |
| ) | |
| with gr.Column(elem_id="nb-center", scale=1): | |
| center_panel = gr.HTML(value=make_welcome_html()) | |
| with gr.Column(elem_id="nb-right", scale=0, min_width=340): | |
| right_panel = gr.HTML( | |
| value=make_agent_graph_html({n: "grey" for n in [ | |
| "Hypothesis A", "PubMed Search", "Supporting Evidence", | |
| "Contradiction Found", "Hypothesis B", "ClinicalTrials", "Converged" | |
| ]}) | |
| ) | |
| # ββ Event handlers ββ | |
| upload.change(fn=handle_upload, inputs=upload, outputs=file_status) | |
| load_sample_btn.click( | |
| fn=lambda: "neurosight_to_neurobio_payload.json", | |
| inputs=None, | |
| outputs=upload | |
| ) | |
| begin_btn.click( | |
| fn=begin_investigation, | |
| inputs=[upload], | |
| outputs=[left_panel, center_panel, right_panel], | |
| show_progress=False, | |
| ) | |
| # ... [rest of your gradio UI code above] ... | |
| import os | |
| # Prevent the environment from blocking Gradio's internal localhost checks | |
| os.environ["NO_PROXY"] = "localhost,127.0.0.1,0.0.0.0" | |
| if __name__ == "__main__": | |
| app.launch( | |
| server_name="0.0.0.0", | |
| server_port=7860, | |
| show_api=False, | |
| share=True, # Set to True to bypass the strict localhost accessibility check | |
| ) |