Spaces:
Sleeping
Sleeping
| 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 = """ | |
| <div style="background:linear-gradient(135deg,#0a1628 0%,#1e3a5f 50%,#0a1628 100%); | |
| padding:36px 48px;border-radius:16px;margin-bottom:8px;position:relative;overflow:hidden;"> | |
| <div style="position:absolute;top:-20px;right:-20px;width:200px;height:200px; | |
| border:1px solid rgba(201,168,76,.15);border-radius:50%;"></div> | |
| <div style="position:absolute;top:10px;right:40px;width:120px;height:120px; | |
| border:1px solid rgba(201,168,76,.1);border-radius:50%;"></div> | |
| <div style="position:relative;z-index:1;"> | |
| <div style="display:flex;align-items:center;gap:14px;margin-bottom:10px;"> | |
| <div style="width:48px;height:48px;background:linear-gradient(135deg,#c9a84c,#e8c87a); | |
| border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:22px;">π¬</div> | |
| <div> | |
| <h1 style="font-family:'Cormorant Garamond',serif;color:#e8c87a;font-size:30px; | |
| font-weight:700;margin:0;letter-spacing:1px;">ScholarPath</h1> | |
| <p style="color:rgba(255,255,255,.5);font-size:12px;margin:0;"> | |
| Research Intelligence Portal Β· Allied Health & Medical Sciences</p> | |
| </div> | |
| </div> | |
| <p style="color:rgba(255,255,255,.85);font-size:14px;margin:0;max-width:660px;line-height:1.6;"> | |
| End-to-end AI research support β from gap to publication. Built for Gulf & Asian researchers. | |
| </p> | |
| <div style="display:flex;gap:10px;margin-top:14px;flex-wrap:wrap;"> | |
| <span style="background:rgba(201,168,76,.2);color:#e8c87a;padding:3px 10px; | |
| border-radius:20px;font-size:11px;border:1px solid rgba(201,168,76,.3);">β¦ 200M+ Papers</span> | |
| <span style="background:rgba(201,168,76,.2);color:#e8c87a;padding:3px 10px; | |
| border-radius:20px;font-size:11px;border:1px solid rgba(201,168,76,.3);">β¦ Gemini 2.5 Flash</span> | |
| <span style="background:rgba(201,168,76,.2);color:#e8c87a;padding:3px 10px; | |
| border-radius:20px;font-size:11px;border:1px solid rgba(201,168,76,.3);">β¦ Auto-Save</span> | |
| <span style="background:rgba(201,168,76,.2);color:#e8c87a;padding:3px 10px; | |
| border-radius:20px;font-size:11px;border:1px solid rgba(201,168,76,.3);">β¦ 25 Disciplines</span> | |
| <span style="background:rgba(201,168,76,.2);color:#e8c87a;padding:3px 10px; | |
| border-radius:20px;font-size:11px;border:1px solid rgba(201,168,76,.3);">β¦ Account & History</span> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| FLOATING_CHAT_HTML = """ | |
| <div id="float-chat-btn" onclick="toggleChat()" title="Ask AI Assistant">π€</div> | |
| <div id="chat-panel"> | |
| <div id="chat-panel-header"> | |
| <span>π¬ ScholarPath Assistant</span> | |
| <span onclick="toggleChat()" style="cursor:pointer;opacity:.7;font-size:18px;">β</span> | |
| </div> | |
| <div id="chat-messages"> | |
| <div class="msg-ai">π Hi! I'm your research assistant. Ask me anything about research methods, journals, citations, or how to use this portal.</div> | |
| </div> | |
| <div id="chat-input-row"> | |
| <input id="chat-float-input" type="text" placeholder="Ask a question..." onkeydown="if(event.key==='Enter')sendFloatChat()"/> | |
| <button id="chat-send-btn" onclick="sendFloatChat()">β€</button> | |
| </div> | |
| </div> | |
| <script> | |
| function toggleChat(){ | |
| const p=document.getElementById('chat-panel'); | |
| p.classList.toggle('open'); | |
| } | |
| async function sendFloatChat(){ | |
| const inp=document.getElementById('chat-float-input'); | |
| const msgs=document.getElementById('chat-messages'); | |
| const text=inp.value.trim(); | |
| if(!text)return; | |
| inp.value=''; | |
| msgs.innerHTML+=`<div class="msg-user">${text}</div>`; | |
| msgs.innerHTML+=`<div class="msg-ai" id="typing">β³ Thinking...</div>`; | |
| msgs.scrollTop=msgs.scrollHeight; | |
| try{ | |
| const r=await fetch('/float_chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({q:text})}); | |
| const d=await r.json(); | |
| document.getElementById('typing').outerHTML=`<div class="msg-ai">${d.answer.replace(/\\n/g,'<br>')}</div>`; | |
| }catch(e){ | |
| document.getElementById('typing').outerHTML=`<div class="msg-ai">β οΈ Error. Please try again.</div>`; | |
| } | |
| msgs.scrollTop=msgs.scrollHeight; | |
| } | |
| </script> | |
| """ | |
| # ββ Gradio App βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with gr.Blocks(title="ScholarPath") as demo: | |
| gr.HTML(HEADER) | |
| gr.HTML('<div class="notice">π Privacy: Only topics & public abstracts are processed. Never paste patient data or unpublished raw data. Nothing is shared externally.</div>') | |
| # 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('<div class="notice-green">β 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.</div>') | |
| 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('<div class="notice">β οΈ Do not paste patient data or unpublished raw datasets here.</div>') | |
| 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(""" | |
| <div style="margin-top:20px;padding:18px 24px;background:linear-gradient(135deg,#0a1628,#1e3a5f); | |
| border-radius:12px;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:10px;"> | |
| <div> | |
| <p style="color:#e8c87a;font-family:'Cormorant Garamond',serif;font-size:17px;margin:0;font-weight:600;">ScholarPath Research Portal</p> | |
| <p style="color:rgba(255,255,255,.45);font-size:11px;margin:3px 0 0;">Gulf & Asian Health Sciences Researchers</p> | |
| </div> | |
| <div style="font-size:11px;color:rgba(255,255,255,.4);">Gemini 2.5 Flash Β· Semantic Scholar Β· Free Β· No data stored externally</div> | |
| </div> | |
| """) | |
| # ββ Float chat API endpoint ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| from fastapi import Request as FastRequest | |
| from fastapi.responses import JSONResponse | |
| 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) |