import gradio as gr import requests import json import os import hashlib import uuid from datetime import datetime import io # ── Gemini client (REST API — no SDK dependency) ────────────────────────────── GEMINI_KEY = os.environ.get("GEMINI_API_KEY", "") GEMINI_MODEL = "gemini-2.0-flash" def ask_gemini(prompt: str, short: bool = False) -> str: if not GEMINI_KEY: return "⚠️ GEMINI_API_KEY not set. Add it in Space → Settings → Secrets." if short: prompt = "Answer concisely in 3-5 sentences or bullet points. No long paragraphs.\n\n" + prompt url = f"https://generativelanguage.googleapis.com/v1beta/models/{GEMINI_MODEL}:generateContent?key={GEMINI_KEY}" payload = { "contents": [{ "parts": [{"text": prompt}] }] } try: resp = requests.post(url, json=payload, timeout=60) data = resp.json() if resp.status_code != 200: error_msg = data.get("error", {}).get("message", str(data)) return f"⚠️ API Error: {error_msg}" candidates = data.get("candidates", []) if not candidates: return "⚠️ No response from Gemini." text = candidates[0].get("content", {}).get("parts", [{}])[0].get("text", "") return text if text else "⚠️ Empty response from Gemini." except Exception as e: return f"⚠️ API Error: {str(e)}" # ── Persistent storage paths ─────────────────────────────────────────────────── DATA_DIR = "/data" USERS_FILE = os.path.join(DATA_DIR, "users.json") SESSIONS_DIR = os.path.join(DATA_DIR, "sessions") def _ensure_dirs(): os.makedirs(DATA_DIR, exist_ok=True) os.makedirs(SESSIONS_DIR, exist_ok=True) def _load_users() -> dict: _ensure_dirs() if not os.path.exists(USERS_FILE): return {} try: with open(USERS_FILE) as f: return json.load(f) except Exception: return {} def _save_users(users: dict): _ensure_dirs() with open(USERS_FILE, "w") as f: json.dump(users, f, indent=2) def _hash_pw(pw: str) -> str: return hashlib.sha256(pw.encode()).hexdigest() def _user_session_file(username: str) -> str: return os.path.join(SESSIONS_DIR, f"{username}.json") def load_user_session(username: str) -> dict: path = _user_session_file(username) if os.path.exists(path): try: with open(path) as f: return json.load(f) except Exception: pass return {"username": username, "phases": {}, "last_saved": None, "search_count": 0} def save_user_session(username: str, session: dict): _ensure_dirs() session["last_saved"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") with open(_user_session_file(username), "w") as f: json.dump(session, f, indent=2) # ── Auth functions ───────────────────────────────────────────────────────────── FREE_SEARCH_LIMIT = 5 def register_user(username: str, password: str, confirm: str) -> tuple[str, str]: username = username.strip().lower() if not username or len(username) < 3: return "❌ Username must be at least 3 characters.", "" if not password or len(password) < 6: return "❌ Password must be at least 6 characters.", "" if password != confirm: return "❌ Passwords do not match.", "" users = _load_users() if username in users: return "❌ Username already taken. Choose another.", "" users[username] = { "pw_hash": _hash_pw(password), "created": datetime.now().strftime("%Y-%m-%d"), "plan": "free", } _save_users(users) return f"✅ Account created! Welcome, {username}. You can now log in.", "" def login_user(username: str, password: str) -> tuple[str, str, dict]: username = username.strip().lower() users = _load_users() if username not in users: return "❌ Username not found.", "", {} if users[username]["pw_hash"] != _hash_pw(password): return "❌ Incorrect password.", "", {} session = load_user_session(username) plan = users[username].get("plan", "free") count = session.get("search_count", 0) msg = f"✅ Welcome back, {username}! Plan: {plan.upper()} | Searches used: {count}" return msg, username, session def reset_password(username: str, new_pw: str, confirm: str) -> str: username = username.strip().lower() if new_pw != confirm: return "❌ Passwords do not match." if len(new_pw) < 6: return "❌ Password too short (min 6 characters)." users = _load_users() if username not in users: return "❌ Username not found." users[username]["pw_hash"] = _hash_pw(new_pw) _save_users(users) return f"✅ Password reset for {username}. You can now log in." def check_search_limit(username: str, session: dict) -> tuple[bool, str]: if not username: return False, "❌ Please log in first." users = _load_users() plan = users.get(username, {}).get("plan", "free") if plan != "free": return True, "" count = session.get("search_count", 0) if count >= FREE_SEARCH_LIMIT: return False, f"⚠️ Free plan limit reached ({FREE_SEARCH_LIMIT} searches). Contact admin to upgrade." return True, f"Free searches remaining: {FREE_SEARCH_LIMIT - count - 1}" def increment_search(username: str, session: dict) -> dict: session["search_count"] = session.get("search_count", 0) + 1 if username: save_user_session(username, session) return session # ── Domains & Study Types ────────────────────────────────────────────────────── DOMAINS = { "🫁 Respiratory Therapy": "respiratory therapy pulmonology ventilation", "🏥 Emergency Medicine": "emergency medicine acute care trauma", "💉 Anesthesia": "anesthesia anesthesiology perioperative", "🔬 Clinical Laboratory": "clinical laboratory diagnostics biomarkers hematology", "❤️ Cardiac Surgery (ICVT/ECVT)": "cardiac surgery cardiopulmonary bypass ECMO perfusion", "🫀 Cardiology Programs": "cardiology cardiac rehabilitation interventional cardiology", "📡 Radiology": "radiology diagnostic imaging MRI CT ultrasound", "🦻 Audiology": "audiology hearing loss vestibular rehabilitation", "🤲 Occupational Therapy": "occupational therapy rehabilitation functional outcomes", "🦯 Physical Therapy": "physical therapy physiotherapy musculoskeletal", "🧠 Neurology": "neurology neuroscience stroke rehabilitation", "🍼 Pediatrics": "pediatrics child health neonatal care", "🌡️ Critical Care / ICU": "critical care intensive care sepsis mechanical ventilation", "🧬 Oncology": "oncology cancer treatment chemotherapy immunotherapy", "💊 Pharmacy / Pharmacology": "clinical pharmacy pharmacology drug interactions", "🧪 Medical Biotechnology": "medical biotechnology molecular diagnostics genomics", "🏋️ Sports Medicine": "sports medicine athletic training exercise physiology", "👁️ Ophthalmology": "ophthalmology eye disease retina cornea", "🦷 Dental / Oral Health": "dentistry oral health periodontics", "🧘 Mental Health / Psychiatry": "psychiatry mental health cognitive behavioral therapy", "🤱 Midwifery / Obstetrics": "midwifery obstetrics maternal health", "🌍 Public Health / Epidemiology": "public health epidemiology health policy", "💻 Health Informatics": "health informatics digital health EHR AI in medicine", "🔩 Biomedical Engineering": "biomedical engineering medical devices prosthetics", "🧑‍⚕️ Nursing": "nursing patient care clinical outcomes", } STUDY_TYPES = [ "Systematic Review & Meta-Analysis", "Randomized Controlled Trial (RCT)", "Cohort Study", "Case-Control Study", "Cross-Sectional Study", "Case Series / Case Report", "Scoping Review", "Narrative Review", "Qualitative Study", "Mixed Methods", "Pilot / Feasibility Study", "Diagnostic Accuracy Study", "Economic Evaluation", ] JOURNALS = { "High Impact General": ["NEJM", "The Lancet", "JAMA", "BMJ", "Nature Medicine"], "Systematic Reviews": ["Systematic Reviews (BMC)", "Cochrane Database", "JBI Evidence Synthesis"], "Open Access Fast": ["PLOS Medicine", "PLOS ONE", "BMC Medicine", "Frontiers in Medicine"], "Digital Health": ["JMIR", "npj Digital Medicine", "Health Informatics Journal"], "Allied Health": ["Journal of Allied Health", "Physical Therapy", "AJOT", "Annals of Emergency Medicine"], "Gulf / Regional": ["Saudi Medical Journal", "Eastern Mediterranean Health Journal", "AAMJ"], "Student / First Pub": ["Cureus", "Medical Education Online", "BMC Medical Education"], } # ── Semantic Scholar ─────────────────────────────────────────────────────────── def search_papers(query: str, year_from: int = 2020, limit: int = 20): try: params = { "query": query, "limit": limit, "fields": "title,authors,year,abstract,citationCount,externalIds,publicationVenue", "year": f"{year_from}-2026", } r = requests.get("https://api.semanticscholar.org/graph/v1/paper/search", params=params, timeout=15) if r.status_code == 200: return r.json().get("data", []) except Exception: pass return [] def format_papers_table(papers): if not papers: return "*No papers found. Try broadening your search terms.*" rows = [] for i, p in enumerate(papers[:15], 1): authors = ", ".join(a.get("name", "") for a in p.get("authors", [])[:2]) if len(p.get("authors", [])) > 2: authors += " et al." year = p.get("year", "N/A") title = (p.get("title") or "N/A")[:80] cites = p.get("citationCount", 0) venue = ((p.get("publicationVenue") or {}).get("name") or "Unknown")[:35] doi = (p.get("externalIds") or {}).get("DOI", "") doi_str = f"[DOI]({doi})" if doi else "—" rows.append(f"| {i} | {title} | {authors} | {year} | {venue} | {cites} | {doi_str} |") header = "| # | Title | Authors | Year | Journal | Cites | DOI |\n|---|-------|---------|------|---------|-------|-----|\n" return header + "\n".join(rows) # ── Phase functions ──────────────────────────────────────────────────────────── def run_gap_analysis(domain, topic, year_from, study_type, username, session): ok, msg = check_search_limit(username, session) if not ok: return msg, "", "", session if not topic.strip(): return "❌ Please enter a research topic.", "", "", session domain_kw = DOMAINS.get(domain, domain) papers = search_papers(f"{topic} {domain_kw}", int(year_from)) table_md = format_papers_table(papers) abstracts = "\n\n".join( f"[{p.get('year')}] {p.get('title','')}: {(p.get('abstract') or '')[:280]}" for p in papers[:10] if p.get("abstract") ) prompt = f"""Expert research analyst in {domain}. Researcher topic: "{topic}". Study type: {study_type}. Recent papers 2020-2026: {abstracts or "Use expert knowledge of the field."} Provide: ## Executive Summary ## What Is Well-Researched (4-5 areas) ## 5 Research Gaps (each: Gap Title, Why it matters, Recommended study type, Target population, Expected contribution) ## Top Recommended Gap ## Methodological Gaps ## Gulf & Asian Context Gaps Flag unverified claims with [CITE NEEDED].""" gap = ask_gemini(prompt) session = increment_search(username, session) session.setdefault("phases", {}) session["phases"]["p1_topic"] = topic session["phases"]["p1_domain"] = domain session["phases"]["p1_gap"] = gap session["phases"]["p1_table"] = table_md if username: save_user_session(username, session) return table_md, gap, f"✅ Auto-saved | Searches used: {session['search_count']}", session def generate_design(domain, topic, gap_focus, study_type, username, session): if not topic.strip() or not gap_focus.strip(): return "❌ Enter topic and gap focus.", session prompt = f"""Senior research methodologist in {domain}. Topic: {topic} | Gap: {gap_focus} | Study: {study_type} Generate complete research design: ## PICO/SPIDER Framework ## Research Objectives (primary + 2 secondary) ## Research Questions ## Methodology (design, setting, sample, inclusion/exclusion, sample size, tools, stats) ## Structured Abstract Draft (Background/Objective/Methods/Expected Results/Conclusion/Keywords) ## Ethics & Registration (IRB, PROSPERO yes/no, checklist) ## 12-Month Timeline Flag claims with [CITE NEEDED].""" result = ask_gemini(prompt) session.setdefault("phases", {}) session["phases"]["p2_design"] = result if username: save_user_session(username, session) return result, session def writing_assistant(domain, topic, section, rough_notes, username, session): if not rough_notes.strip(): return "❌ Add your rough notes first.", session prompt = f"""Expert medical writer in {domain}. Paper: "{topic}". Section: {section}. Rough notes: {rough_notes} Transform into polished publication-ready academic prose for {section}. - Formal medical English - Tag unverified claims: [CITE NEEDED — search: "keywords"] - Tag missing data: [DATA NEEDED] - Vary sentence structure (avoid AI-detectable patterns) After text add: ## Writing Coach Notes (3 tips) ## Self-Check Checklist""" result = ask_gemini(prompt) session.setdefault("phases", {}) prev = session["phases"].get("p3_writing", "") session["phases"]["p3_writing"] = prev + f"\n\n=== {section} ===\n" + result if username: save_user_session(username, session) return result, session def match_journals(domain, topic, study_type, open_access, target_if, username, session): prompt = f"""Journal selection expert. Profile: {domain} | {topic} | {study_type} | {open_access} | IF target: {target_if} Recommend 8 journals. For each: ### [Rank]. [Journal] — IF: X.X - Publisher, Indexing, Scope fit, Acceptance rate, Review time, APC fee - Gulf/Asian author note, Strategic tip - ⚠️ Predatory check: SAFE/CAUTION Include 2 Gulf/regional journals, 1 fast-track. Flag non-Scopus/WoS journals.""" result = ask_gemini(prompt) session.setdefault("phases", {}) session["phases"]["p4_journals"] = result if username: save_user_session(username, session) return result, session def reviewer_response(domain, topic, reviewer_comments, username, session): if not reviewer_comments.strip(): return "❌ Paste reviewer comments.", session prompt = f"""Expert in peer review responses for {domain}. Paper: "{topic}" Comments: {reviewer_comments} Write professional response letter with: - Opening paragraph - Response to each comment (quote → author response → manuscript change) - Cover letter for revised submission - Re-submission checklist Professional tone, never defensive. [CITE NEEDED] where needed.""" result = ask_gemini(prompt) session.setdefault("phases", {}) session["phases"]["p5_reviewer"] = result if username: save_user_session(username, session) return result, session def export_document(username, session): if not username: return None phases = session.get("phases", {}) md_content = f"""# Research Portfolio: {phases.get("p1_topic", "Untitled")} **ScholarPath** | User: {username} | Saved: {session.get("last_saved", "—")} --- """ content_map = [ ("Phase 1 — Gap Analysis", "p1_gap"), ("Phase 2 — Research Design", "p2_design"), ("Phase 3 — Writing", "p3_writing"), ("Phase 4 — Journals", "p4_journals"), ("Phase 5 — Reviewer Response", "p5_reviewer"), ] for heading, key in content_map: if phases.get(key): md_content += f"\n\n## {heading}\n\n{phases[key]}\n\n---\n" topic_slug = (phases.get("p1_topic", "portfolio") or "portfolio")[:25].replace(" ", "_") path = f"/tmp/{username}_{topic_slug}.md" with open(path, "w", encoding="utf-8") as f: f.write(md_content) return path def load_saved_work(username, session): phases = session.get("phases", {}) saved = session.get("last_saved") if not phases: return "No saved work yet.", "", "", "", "" return ( phases.get("p1_gap", ""), phases.get("p2_design", ""), phases.get("p3_writing", ""), phases.get("p4_journals", ""), f"Last saved: {saved}" if saved else "Not saved yet.", ) # ── CSS ──────────────────────────────────────────────────────────────────────── CSS = """ @import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@400;600;700&family=Plus+Jakarta+Sans:wght@300;400;500;600&display=swap'); :root { --navy: #0a1628; --gold: #c9a84c; --gold-light: #e8c87a; --cream: #faf7f0; --sage: #1e3a5f; --text: #1a1a2e; } body, .gradio-container { font-family: 'Plus Jakarta Sans', sans-serif !important; background: var(--cream) !important; } h1,h2,h3 { font-family: 'Cormorant Garamond', serif !important; } .gradio-container { max-width: 1200px !important; margin: 0 auto !important; } /* Buttons */ .btn-primary button { background: linear-gradient(135deg, #0a1628, #1e3a5f) !important; color: #faf7f0 !important; font-weight: 600 !important; border: none !important; border-radius: 8px !important; box-shadow: 0 4px 12px rgba(10,22,40,.3) !important; transition: all .25s !important; } .btn-primary button:hover { background: linear-gradient(135deg,#1e3a5f,#c9a84c) !important; transform: translateY(-1px) !important; } .btn-gold button { background: linear-gradient(135deg, #c9a84c, #e8c87a) !important; color: #0a1628 !important; font-weight: 700 !important; border: none !important; border-radius: 8px !important; } /* Inputs */ textarea, input[type=text], input[type=password] { border: 1.5px solid #e2d9c8 !important; border-radius: 8px !important; background: white !important; color: var(--text) !important; font-family: 'Plus Jakarta Sans', sans-serif !important; } textarea:focus, input:focus { border-color: #c9a84c !important; box-shadow: 0 0 0 3px rgba(201,168,76,.15) !important; } /* Tabs */ .tab-nav button { font-family: 'Plus Jakarta Sans', sans-serif !important; font-weight: 500 !important; color: #1e3a5f !important; } .tab-nav button.selected { color: #c9a84c !important; border-bottom: 2px solid #c9a84c !important; } /* Notice */ .notice { background: linear-gradient(135deg,#fef3c7,#fde68a) !important; border: 1px solid #d97706 !important; border-radius: 8px !important; padding: 10px 14px !important; color: #92400e !important; font-size: 13px !important; } .notice-green { background: #d1fae5 !important; border: 1px solid #065f46 !important; border-radius: 8px !important; padding: 10px 14px !important; color: #065f46 !important; font-size: 13px !important; } /* Floating chat button */ #float-chat-btn { position: fixed !important; bottom: 24px !important; right: 24px !important; width: 60px !important; height: 60px !important; background: linear-gradient(135deg,#0a1628,#1e3a5f) !important; border-radius: 50% !important; border: 2px solid #c9a84c !important; display: flex !important; align-items: center !important; justify-content: center !important; cursor: pointer !important; z-index: 9999 !important; box-shadow: 0 4px 20px rgba(10,22,40,.5) !important; font-size: 26px !important; transition: transform .2s !important; } #float-chat-btn:hover { transform: scale(1.12) !important; } #chat-panel { position: fixed !important; bottom: 96px !important; right: 24px !important; width: 360px !important; height: 480px !important; background: white !important; border-radius: 16px !important; border: 1px solid #e2d9c8 !important; z-index: 9998 !important; box-shadow: 0 8px 40px rgba(10,22,40,.25) !important; display: none; flex-direction: column !important; overflow: hidden !important; } #chat-panel.open { display: flex !important; } #chat-panel-header { background: linear-gradient(135deg,#0a1628,#1e3a5f) !important; padding: 12px 16px !important; color: #e8c87a !important; font-family: 'Cormorant Garamond',serif !important; font-size: 16px !important; font-weight: 600 !important; display: flex !important; justify-content: space-between !important; align-items: center !important; } #chat-messages { flex: 1 !important; overflow-y: auto !important; padding: 12px !important; display: flex !important; flex-direction: column !important; gap: 8px !important; background: #faf7f0 !important; } .msg-user { background: #0a1628 !important; color: white !important; padding: 8px 12px !important; border-radius: 12px 12px 2px 12px !important; font-size: 13px !important; align-self: flex-end !important; max-width: 80% !important; } .msg-ai { background: white !important; color: #1a1a2e !important; padding: 8px 12px !important; border-radius: 12px 12px 12px 2px !important; font-size: 13px !important; border: 1px solid #e2d9c8 !important; align-self: flex-start !important; max-width: 85% !important; } #chat-input-row { display: flex !important; padding: 8px !important; gap: 6px !important; border-top: 1px solid #e2d9c8 !important; background: white !important; } #chat-input-row input { flex: 1 !important; border: 1px solid #e2d9c8 !important; border-radius: 20px !important; padding: 8px 14px !important; font-size: 13px !important; outline: none !important; } #chat-send-btn { background: linear-gradient(135deg,#c9a84c,#e8c87a) !important; border: none !important; border-radius: 50% !important; width: 36px !important; height: 36px !important; cursor: pointer !important; font-size: 16px !important; color: #0a1628 !important; font-weight: 700 !important; } """ HEADER = """
🔬

ScholarPath

Research Intelligence Portal · Allied Health & Medical Sciences

End-to-end AI research support — from gap to publication. Built for Gulf & Asian researchers.

✦ 200M+ Papers ✦ Gemini 2.5 Flash ✦ Auto-Save ✦ 25 Disciplines ✦ Account & History
""" FLOATING_CHAT_HTML = """
🤖
🔬 ScholarPath Assistant
👋 Hi! I'm your research assistant. Ask me anything about research methods, journals, citations, or how to use this portal.
""" # ── Gradio App ───────────────────────────────────────────────────────────────── with gr.Blocks(title="ScholarPath") as demo: gr.HTML(HEADER) gr.HTML('
🔒 Privacy: Only topics & public abstracts are processed. Never paste patient data or unpublished raw data. Nothing is shared externally.
') # Shared state state_user = gr.State("") state_session = gr.State({}) with gr.Tabs(): # ── Account Tab ────────────────────────────────────────────────────── with gr.Tab("👤 My Account"): with gr.Row(): # Login with gr.Column(): gr.Markdown("### 🔑 Login") li_user = gr.Textbox(label="Username") li_pw = gr.Textbox(label="Password", type="password") li_btn = gr.Button("Login", elem_classes=["btn-primary"], variant="primary") li_msg = gr.Markdown("") # Register with gr.Column(): gr.Markdown("### ✏️ Create Account") reg_user = gr.Textbox(label="Choose Username") reg_pw = gr.Textbox(label="Password (min 6 chars)", type="password") reg_pw2 = gr.Textbox(label="Confirm Password", type="password") reg_btn = gr.Button("Create Account", elem_classes=["btn-primary"], variant="primary") reg_msg = gr.Markdown("") # Reset with gr.Column(): gr.Markdown("### 🔄 Reset Password") rs_user = gr.Textbox(label="Username") rs_pw = gr.Textbox(label="New Password", type="password") rs_pw2 = gr.Textbox(label="Confirm New Password", type="password") rs_btn = gr.Button("Reset Password", elem_classes=["btn-gold"], variant="primary") rs_msg = gr.Markdown("") gr.Markdown("---") gr.Markdown("### 📂 Your Saved Work") load_btn = gr.Button("📥 Load My Saved Work", elem_classes=["btn-primary"], variant="primary") save_status = gr.Markdown("") with gr.Row(): sv_gap = gr.Textbox(label="Saved Gap Analysis", lines=4, interactive=False) sv_design = gr.Textbox(label="Saved Research Design", lines=4, interactive=False) with gr.Row(): sv_writing = gr.Textbox(label="Saved Writing", lines=4, interactive=False) sv_journals = gr.Textbox(label="Saved Journals", lines=4, interactive=False) gr.HTML('
✅ Your work auto-saves after every action. If internet drops, your last save is restored when you log in again. Use the Export tab to download a .md backup anytime.
') def do_login(u, p): msg, username, session = login_user(u, p) return msg, username, session def do_register(u, p, p2): msg, _ = register_user(u, p, p2) return msg def do_reset(u, p, p2): return reset_password(u, p, p2) def do_load(username, session): if not username: return "❌ Log in first.", "", "", "", "" g, d, w, j, s = load_saved_work(username, session) return s, g, d, w, j li_btn.click(do_login, [li_user, li_pw], [li_msg, state_user, state_session]) reg_btn.click(do_register, [reg_user, reg_pw, reg_pw2], reg_msg) rs_btn.click(do_reset, [rs_user, rs_pw, rs_pw2], rs_msg) load_btn.click(do_load, [state_user, state_session], [save_status, sv_gap, sv_design, sv_writing, sv_journals]) # ── Phase 1 ────────────────────────────────────────────────────────── with gr.Tab("🔍 Phase 1 · Gap Analysis"): gr.Markdown("### Discover Research Gaps — *Login required*") with gr.Row(): with gr.Column(scale=1): p1_domain = gr.Dropdown(choices=list(DOMAINS.keys()), label="Domain", value="🫁 Respiratory Therapy") p1_topic = gr.Textbox(label="Research Topic", placeholder="e.g., NIV weaning in ICU patients", lines=2) p1_year = gr.Slider(2015, 2025, value=2020, step=1, label="Search From Year") p1_study = gr.Dropdown(choices=STUDY_TYPES, label="Study Type", value=STUDY_TYPES[0]) p1_btn = gr.Button("🚀 Run Gap Analysis", elem_classes=["btn-primary"], variant="primary") p1_status = gr.Markdown("") with gr.Column(scale=2): p1_table = gr.Markdown("*Papers appear here after search.*") p1_gap_out = gr.Markdown("*Gap report appears here.*") def do_gap(domain, topic, year, study, username, session): tbl, gap, status, session = run_gap_analysis(domain, topic, year, study, username, session) return tbl, gap, status, session p1_btn.click(do_gap, [p1_domain, p1_topic, p1_year, p1_study, state_user, state_session], [p1_table, p1_gap_out, p1_status, state_session]) # ── Phase 2 ────────────────────────────────────────────────────────── with gr.Tab("📐 Phase 2 · Research Design"): gr.Markdown("### Build Your Research Protocol") with gr.Row(): with gr.Column(scale=1): p2_domain = gr.Textbox(label="Domain", placeholder="e.g., Respiratory Therapy") p2_topic = gr.Textbox(label="Topic", placeholder="From Phase 1") p2_gap = gr.Textbox(label="Specific Gap to Address", lines=4, placeholder="Paste the gap from Phase 1...") p2_study = gr.Dropdown(choices=STUDY_TYPES, label="Study Type", value=STUDY_TYPES[0]) p2_btn = gr.Button("🏗️ Generate Design", elem_classes=["btn-primary"], variant="primary") with gr.Column(scale=2): p2_out = gr.Markdown("*Research design appears here.*") def do_design(domain, topic, gap, study, username, session): result, session = generate_design(domain, topic, gap, study, username, session) return result, session p2_btn.click(do_design, [p2_domain, p2_topic, p2_gap, p2_study, state_user, state_session], [p2_out, state_session]) # ── Phase 3 ────────────────────────────────────────────────────────── with gr.Tab("✍️ Phase 3 · Writing"): gr.HTML('
⚠️ Do not paste patient data or unpublished raw datasets here.
') with gr.Row(): with gr.Column(scale=1): p3_domain = gr.Textbox(label="Domain") p3_topic = gr.Textbox(label="Paper Topic") p3_section = gr.Dropdown( choices=["Abstract","Introduction","Literature Review","Methodology", "Results","Discussion","Conclusion","References (APA 7)"], label="Section", value="Introduction") p3_notes = gr.Textbox(label="Your Rough Notes", lines=10, placeholder="Bullet points, rough draft, key ideas...") p3_btn = gr.Button("✨ Polish Section", elem_classes=["btn-primary"], variant="primary") with gr.Column(scale=2): p3_out = gr.Markdown("*Output appears here.*") def do_writing(domain, topic, section, notes, username, session): result, session = writing_assistant(domain, topic, section, notes, username, session) return result, session p3_btn.click(do_writing, [p3_domain, p3_topic, p3_section, p3_notes, state_user, state_session], [p3_out, state_session]) # ── Phase 4 ────────────────────────────────────────────────────────── with gr.Tab("📰 Phase 4 · Journals"): with gr.Row(): with gr.Column(scale=1): p4_domain = gr.Textbox(label="Domain") p4_topic = gr.Textbox(label="Topic") p4_study = gr.Dropdown(choices=STUDY_TYPES, label="Study Type", value=STUDY_TYPES[0]) p4_oa = gr.Radio(["Open Access Required","Open Access Preferred","Any"], label="Open Access", value="Open Access Preferred") p4_if = gr.Dropdown( choices=["Any (student/first pub)","IF 1–3","IF 3–6","IF 6+","Top 10 only"], label="Target Impact Factor", value="IF 1–3") p4_btn = gr.Button("🎯 Match Journals", elem_classes=["btn-primary"], variant="primary") with gr.Column(scale=2): p4_out = gr.Markdown("*Journal recommendations appear here.*") def do_journals(domain, topic, study, oa, if_t, username, session): result, session = match_journals(domain, topic, study, oa, if_t, username, session) return result, session p4_btn.click(do_journals, [p4_domain, p4_topic, p4_study, p4_oa, p4_if, state_user, state_session], [p4_out, state_session]) # ── Phase 5 ────────────────────────────────────────────────────────── with gr.Tab("📨 Phase 5 · Reviewer"): with gr.Row(): with gr.Column(scale=1): p5_domain = gr.Textbox(label="Domain") p5_topic = gr.Textbox(label="Paper Title") p5_comments = gr.Textbox(label="Reviewer Comments", lines=12, placeholder="Paste full reviewer comments...") p5_btn = gr.Button("📝 Generate Response", elem_classes=["btn-primary"], variant="primary") with gr.Column(scale=2): p5_out = gr.Markdown("*Response letter appears here.*") def do_reviewer(domain, topic, comments, username, session): result, session = reviewer_response(domain, topic, comments, username, session) return result, session p5_btn.click(do_reviewer, [p5_domain, p5_topic, p5_comments, state_user, state_session], [p5_out, state_session]) # ── Export ─────────────────────────────────────────────────────────── with gr.Tab("💾 Export"): gr.Markdown("""### Download Your Complete Research Portfolio Work auto-saves after every action. Use this to download a .md backup anytime.""") exp_btn = gr.Button("📥 Download My Portfolio (.md)", elem_classes=["btn-gold"], variant="primary") exp_file = gr.File(label="Download") exp_btn.click(export_document, [state_user, state_session], exp_file) # ── Inline AI Chat (full tab) ───────────────────────────────────── with gr.Tab("🤖 AI Assistant"): gr.Markdown("### Ask Research Questions — Concise answers") chat_hist = gr.State([]) chatbox = gr.Chatbot(height=380, show_label=False) with gr.Row(): chat_in = gr.Textbox(placeholder="Ask anything about research...", label="", scale=5, lines=1) chat_btn = gr.Button("Send", elem_classes=["btn-primary"], scale=1) ASSISTANT_SYS = """You are ScholarPath, a concise research assistant for health sciences researchers (Gulf/Asian). Rules: SHORT answers (3-6 sentences, bullet points for lists). Direct and practical. Portal phases: 1=Gap Analysis, 2=Research Design, 3=Writing, 4=Journals, 5=Reviewer Response. """ def chat_respond(message, history): if not message.strip(): return history, "" resp = ask_gemini(ASSISTANT_SYS + "\nQuestion: " + message, short=True) history = history + [[message, resp]] return history, "" chat_btn.click(chat_respond, [chat_in, chat_hist], [chatbox, chat_in]) chat_in.submit(chat_respond, [chat_in, chat_hist], [chatbox, chat_in]) # ── Journal Index ───────────────────────────────────────────────── with gr.Tab("📋 Journals"): jmd = "" for cat, jlist in JOURNALS.items(): jmd += f"\n#### {cat}\n" + "".join(f"- {j}\n" for j in jlist) gr.Markdown(jmd) # Floating chatbot (calls Gradio API endpoint) gr.HTML(FLOATING_CHAT_HTML) gr.HTML("""

ScholarPath Research Portal

Gulf & Asian Health Sciences Researchers

Gemini 2.5 Flash · Semantic Scholar · Free · No data stored externally
""") # ── Float chat API endpoint ──────────────────────────────────────────────────── from fastapi import Request as FastRequest from fastapi.responses import JSONResponse @demo.app.post("/float_chat") async def float_chat_endpoint(request: FastRequest): body = await request.json() q = body.get("q", "") if not q: return JSONResponse({"answer": "Please ask a question."}) sys = """You are ScholarPath AI assistant for health sciences researchers. Give SHORT answers (3-5 sentences max). Bullet points for lists. Direct and practical. Portal: Phase 1=Gap Analysis, 2=Research Design, 3=Writing, 4=Journal Match, 5=Reviewer Response. """ answer = ask_gemini(sys + "\nQuestion: " + q, short=True) return JSONResponse({"answer": answer}) demo.launch(css=CSS)