azlaan428 commited on
Commit
58d2397
·
1 Parent(s): bcceea4

feat: working pipeline, PDF export, progress bar, structured UI

Browse files
Files changed (5) hide show
  1. agent/agent.py +7 -9
  2. app.py +103 -14
  3. retrieval/pubmed.py +73 -6
  4. run.sh +8 -0
  5. templates/index.html +611 -26
agent/agent.py CHANGED
@@ -51,21 +51,19 @@ def run_literature_scout(queries):
51
  all_papers = {}
52
  def fetch_one(q):
53
  return fetch_pubmed(q, max_results=5)
54
- with ThreadPoolExecutor(max_workers=5) as executor:
55
- futures = {executor.submit(fetch_one, q): q for q in queries}
56
- for future in as_completed(futures):
57
- results = future.result()
58
- for r in results:
59
- pmid = r["pmid"]
60
- if pmid not in all_papers:
61
- all_papers[pmid] = r
62
  return all_papers
63
 
64
 
65
  def run_evidence_synthesiser(user_question, papers):
66
  llm = get_llm()
67
  parts = []
68
- for pmid, p in list(papers.items())[:20]:
69
  title = p.get("title", "N/A")
70
  abstract = p["abstract"]
71
  parts.append("[PMID " + pmid + "]\nTitle: " + title + "\n" + abstract)
 
51
  all_papers = {}
52
  def fetch_one(q):
53
  return fetch_pubmed(q, max_results=5)
54
+ import time
55
+ for q in queries:
56
+ time.sleep(0.4)
57
+ for r in fetch_one(q):
58
+ if r["pmid"] not in all_papers:
59
+ all_papers[r["pmid"]] = r
 
 
60
  return all_papers
61
 
62
 
63
  def run_evidence_synthesiser(user_question, papers):
64
  llm = get_llm()
65
  parts = []
66
+ for pmid, p in list(papers.items())[:6]:
67
  title = p.get("title", "N/A")
68
  abstract = p["abstract"]
69
  parts.append("[PMID " + pmid + "]\nTitle: " + title + "\n" + abstract)
app.py CHANGED
@@ -1,17 +1,16 @@
1
  import sys, os
2
  sys.path.append(os.path.dirname(os.path.abspath(__file__)))
3
-
4
- from flask import Flask, render_template, request, jsonify
5
- from agent.agent import build_agent
 
 
 
 
 
 
6
 
7
  app = Flask(__name__)
8
- agent_executor = None
9
-
10
- def get_agent():
11
- global agent_executor
12
- if agent_executor is None:
13
- agent_executor = build_agent()
14
- return agent_executor
15
 
16
  @app.route("/")
17
  def index():
@@ -24,12 +23,102 @@ def query():
24
  if not user_query:
25
  return jsonify({"error": "Empty query"}), 400
26
  try:
27
- agent = get_agent()
28
- result = agent.invoke({"messages": [{"role": "user", "content": user_query}]})
29
- response = result["messages"][-1].content
30
- return jsonify({"response": response})
 
 
 
31
  except Exception as e:
32
  return jsonify({"error": str(e)}), 500
33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  if __name__ == "__main__":
35
  app.run(debug=True, port=5000)
 
1
  import sys, os
2
  sys.path.append(os.path.dirname(os.path.abspath(__file__)))
3
+ from flask import Flask, render_template, request, jsonify, send_file
4
+ from agent.agent import run_pipeline
5
+ from reportlab.lib.pagesizes import A4
6
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
7
+ from reportlab.lib.units import mm
8
+ from reportlab.lib import colors
9
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, HRFlowable
10
+ from reportlab.lib.enums import TA_LEFT
11
+ import io
12
 
13
  app = Flask(__name__)
 
 
 
 
 
 
 
14
 
15
  @app.route("/")
16
  def index():
 
23
  if not user_query:
24
  return jsonify({"error": "Empty query"}), 400
25
  try:
26
+ result = run_pipeline(user_query)
27
+ return jsonify({
28
+ "synthesis": result["synthesis"],
29
+ "citations": result["citations"],
30
+ "paper_count": result["paper_count"],
31
+ "queries": result["queries"]
32
+ })
33
  except Exception as e:
34
  return jsonify({"error": str(e)}), 500
35
 
36
+ @app.route("/export-pdf", methods=["POST"])
37
+ def export_pdf():
38
+ data = request.get_json()
39
+ synthesis = data.get("synthesis", "")
40
+ citations = data.get("citations", "")
41
+ query = data.get("query", "Biomedical Research Query")
42
+ paper_count = data.get("paper_count", 0)
43
+
44
+ buf = io.BytesIO()
45
+ doc = SimpleDocTemplate(buf, pagesize=A4,
46
+ leftMargin=20*mm, rightMargin=20*mm,
47
+ topMargin=20*mm, bottomMargin=20*mm)
48
+
49
+ accent = colors.HexColor("#00e5a0")
50
+ dark = colors.HexColor("#111827")
51
+
52
+ title_style = ParagraphStyle("title",
53
+ fontName="Helvetica-Bold", fontSize=18,
54
+ textColor=dark, spaceAfter=4)
55
+ meta_style = ParagraphStyle("meta",
56
+ fontName="Helvetica", fontSize=9,
57
+ textColor=colors.HexColor("#5a6a7a"), spaceAfter=16)
58
+ section_label_style = ParagraphStyle("sec_label",
59
+ fontName="Helvetica-Bold", fontSize=10,
60
+ textColor=accent, spaceBefore=14, spaceAfter=4)
61
+ body_style = ParagraphStyle("body",
62
+ fontName="Helvetica", fontSize=10,
63
+ leading=16, textColor=dark, spaceAfter=6)
64
+ cite_style = ParagraphStyle("cite",
65
+ fontName="Helvetica", fontSize=8,
66
+ leading=13, textColor=colors.HexColor("#444444"),
67
+ spaceAfter=4)
68
+
69
+ story = []
70
+ story.append(Paragraph("ARIA — Autonomous Research Intelligence Agent", title_style))
71
+ story.append(Paragraph(
72
+ f"Query: {query} | {paper_count} papers retrieved | Groq LLaMA-3.1",
73
+ meta_style))
74
+ story.append(HRFlowable(width="100%", thickness=1,
75
+ color=colors.HexColor("#1e2936"), spaceAfter=16))
76
+
77
+ SECTIONS = [
78
+ ("## Background", "Background"),
79
+ ("## Key Findings", "Key Findings"),
80
+ ("## Level of Evidence", "Level of Evidence"),
81
+ ("## Conflicting Evidence", "Conflicting Evidence"),
82
+ ("## Research Gaps", "Research Gaps"),
83
+ ("## Clinical Implications", "Clinical Implications"),
84
+ ]
85
+ for marker, label in SECTIONS:
86
+ start = synthesis.find(marker)
87
+ if start == -1:
88
+ continue
89
+ content_start = start + len(marker)
90
+ next_markers = [synthesis.find(m) for m, _ in SECTIONS if synthesis.find(m) > start]
91
+ end = min(next_markers) if next_markers else len(synthesis)
92
+ text = synthesis[content_start:end].strip()
93
+ if not text:
94
+ continue
95
+ story.append(Paragraph(label.upper(), section_label_style))
96
+ for para in text.split("\n"):
97
+ para = para.strip()
98
+ if para:
99
+ story.append(Paragraph(para, body_style))
100
+
101
+ story.append(Spacer(1, 8*mm))
102
+ story.append(HRFlowable(width="100%", thickness=1,
103
+ color=colors.HexColor("#1e2936"), spaceAfter=8))
104
+ story.append(Paragraph("REFERENCES", section_label_style))
105
+ for line in citations.split("\n"):
106
+ line = line.strip()
107
+ if line:
108
+ story.append(Paragraph(line, cite_style))
109
+
110
+ story.append(Spacer(1, 6*mm))
111
+ story.append(Paragraph(
112
+ "AI-generated synthesis — verify against primary sources before clinical use.",
113
+ ParagraphStyle("disclaimer", fontName="Helvetica-Oblique",
114
+ fontSize=8, textColor=colors.HexColor("#999999"))))
115
+
116
+ doc.build(story)
117
+ buf.seek(0)
118
+ safe_query = "".join(c for c in query[:40] if c.isalnum() or c in " -_").strip()
119
+ filename = f"ARIA_{safe_query}.pdf".replace(" ", "_")
120
+ return send_file(buf, mimetype="application/pdf",
121
+ as_attachment=True, download_name=filename)
122
+
123
  if __name__ == "__main__":
124
  app.run(debug=True, port=5000)
retrieval/pubmed.py CHANGED
@@ -1,21 +1,88 @@
 
1
  from Bio import Entrez
2
 
3
  Entrez.email = "azlaanmohammad66@gmail.com"
4
 
5
- def fetch_pubmed(query: str, max_results: int = 5) -> list[dict]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  handle = Entrez.esearch(db="pubmed", term=query, retmax=max_results)
7
  record = Entrez.read(handle)
8
  ids = record["IdList"]
9
  if not ids:
10
  return []
11
-
12
  handle = Entrez.efetch(db="pubmed", id=ids, rettype="abstract", retmode="text")
13
  raw = handle.read()
14
-
15
  abstracts = [a.strip() for a in raw.strip().split("\n\n\n") if a.strip()]
16
- return [{"pmid": pmid, "abstract": ab} for pmid, ab in zip(ids, abstracts)]
 
17
 
18
  if __name__ == "__main__":
19
- results = fetch_pubmed("epilepsy seizure detection machine learning")
20
  for r in results:
21
- print(f"PMID: {r['pmid']}\n{r['abstract']}\n{'-'*60}")
 
 
 
 
 
 
1
+ import re
2
  from Bio import Entrez
3
 
4
  Entrez.email = "azlaanmohammad66@gmail.com"
5
 
6
+
7
+ def parse_abstract_block(pmid, block):
8
+ title = "Title unavailable"
9
+ authors = "Authors unavailable"
10
+ journal = "Journal unavailable"
11
+ year = "n.d."
12
+
13
+ lines = block.split("\n")
14
+
15
+ # Journal is line 0 (may wrap to line 1 before first blank line)
16
+ # Title is the first non-empty line AFTER the first blank line
17
+ # Authors is the first non-empty line AFTER the second blank line
18
+
19
+ # Collect journal lines (everything up to first blank)
20
+ journal_lines = []
21
+ i = 0
22
+ while i < len(lines) and lines[i].strip():
23
+ journal_lines.append(lines[i].strip())
24
+ i += 1
25
+
26
+ journal_raw = " ".join(journal_lines)
27
+ journal_raw = re.sub(r"^\d+\.\s*", "", journal_raw)
28
+ year_match = re.search(r"\b(19|20)\d{2}\b", journal_raw)
29
+ if year_match:
30
+ year = year_match.group(0)
31
+ journal = re.split(r"\.\s+\d{4}", journal_raw)[0].strip().rstrip(".")
32
+
33
+ # Skip blank lines, collect title (next non-empty block)
34
+ while i < len(lines) and not lines[i].strip():
35
+ i += 1
36
+ title_lines = []
37
+ while i < len(lines) and lines[i].strip():
38
+ title_lines.append(lines[i].strip())
39
+ i += 1
40
+ if title_lines:
41
+ title = " ".join(title_lines).rstrip(".")
42
+
43
+ # Skip blank lines, collect authors (next non-empty block)
44
+ while i < len(lines) and not lines[i].strip():
45
+ i += 1
46
+ author_lines = []
47
+ while i < len(lines) and lines[i].strip():
48
+ author_lines.append(lines[i].strip())
49
+ i += 1
50
+ if author_lines:
51
+ raw_authors = re.sub(r"\(\d+\)", "", " ".join(author_lines)).strip().rstrip(".,")
52
+ author_list = [a.strip() for a in raw_authors.split(",") if a.strip()]
53
+ if len(author_list) > 3:
54
+ authors = ", ".join(author_list[:3]) + " et al."
55
+ else:
56
+ authors = ", ".join(author_list)
57
+
58
+ return {
59
+ "pmid": pmid,
60
+ "title": title,
61
+ "authors": authors,
62
+ "journal": journal,
63
+ "year": year,
64
+ "abstract": block
65
+ }
66
+
67
+
68
+ def fetch_pubmed(query: str, max_results: int = 5) -> list:
69
  handle = Entrez.esearch(db="pubmed", term=query, retmax=max_results)
70
  record = Entrez.read(handle)
71
  ids = record["IdList"]
72
  if not ids:
73
  return []
 
74
  handle = Entrez.efetch(db="pubmed", id=ids, rettype="abstract", retmode="text")
75
  raw = handle.read()
 
76
  abstracts = [a.strip() for a in raw.strip().split("\n\n\n") if a.strip()]
77
+ return [parse_abstract_block(pmid, ab) for pmid, ab in zip(ids, abstracts)]
78
+
79
 
80
  if __name__ == "__main__":
81
+ results = fetch_pubmed("epilepsy seizure detection machine learning", max_results=2)
82
  for r in results:
83
+ print(f"PMID: {r['pmid']}")
84
+ print(f"Title: {r['title']}")
85
+ print(f"Authors: {r['authors']}")
86
+ print(f"Journal: {r['journal']}")
87
+ print(f"Year: {r['year']}")
88
+ print("-" * 60)
run.sh ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ cd ~/glitch-squad-biomedical-assistant
3
+ source venv/bin/activate
4
+ pkill -f "python app.py" 2>/dev/null
5
+ pkill -f "python3 app.py" 2>/dev/null
6
+ fuser -k 5000/tcp 2>/dev/null
7
+ sleep 1
8
+ python app.py
templates/index.html CHANGED
@@ -3,38 +3,623 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Glitch Squad Biomedical Assistant</title>
7
- <link rel="stylesheet" href="/static/style.css">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  </head>
9
  <body>
10
- <div class="container">
11
- <header>
12
- <h1>Glitch Squad</h1>
13
- <p class="subtitle">Biomedical AI Research Assistant</p>
14
- </header>
15
-
16
- <main>
17
- <div class="search-box">
18
- <textarea id="queryInput" placeholder="Ask a biomedical research question..." rows="3"></textarea>
19
- <button id="submitBtn" onclick="submitQuery()">Search Literature</button>
20
- </div>
21
 
22
- <div id="statusBar" class="status-bar hidden">
23
- <span class="spinner"></span>
24
- <span id="statusText">Retrieving PubMed literature...</span>
 
 
 
 
25
  </div>
 
 
26
 
27
- <div id="responseBox" class="response-box hidden">
28
- <h2>Research Summary</h2>
29
- <div id="responseText"></div>
 
 
 
30
  </div>
31
- </main>
 
 
 
 
 
32
 
33
- <footer>
34
- <p>Powered by LangGraph + Llama 3.2 + PubMed &mdash; AMD Developer Hackathon 2026</p>
35
- <p class="disclaimer">This is an AI model and may make mistakes. Always verify responses against primary sources.</p>
36
- </footer>
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  </div>
38
- <script src="/static/script.js"></script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  </body>
40
- </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>ARIA Biomedical Research Intelligence</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;600&family=IBM+Plex+Sans:wght@300;400;500&display=swap" rel="stylesheet">
8
+ <style>
9
+ :root {
10
+ --bg: #090c10;
11
+ --surface: #0d1117;
12
+ --border: #1e2936;
13
+ --accent: #00e5a0;
14
+ --accent-dim: #00e5a020;
15
+ --text: #c9d1d9;
16
+ --text-dim: #5a6a7a;
17
+ --text-bright: #e6edf3;
18
+ --red: #ff4d6d;
19
+ --yellow: #f0b429;
20
+ --mono: 'IBM Plex Mono', monospace;
21
+ --sans: 'IBM Plex Sans', sans-serif;
22
+ }
23
+
24
+ * { margin: 0; padding: 0; box-sizing: border-box; }
25
+
26
+ body {
27
+ background: var(--bg);
28
+ color: var(--text);
29
+ font-family: var(--sans);
30
+ font-size: 15px;
31
+ line-height: 1.6;
32
+ min-height: 100vh;
33
+ }
34
+
35
+ /* Grid background */
36
+ body::before {
37
+ content: '';
38
+ position: fixed;
39
+ inset: 0;
40
+ background-image:
41
+ linear-gradient(var(--border) 1px, transparent 1px),
42
+ linear-gradient(90deg, var(--border) 1px, transparent 1px);
43
+ background-size: 40px 40px;
44
+ opacity: 0.3;
45
+ pointer-events: none;
46
+ z-index: 0;
47
+ }
48
+
49
+ .container {
50
+ max-width: 900px;
51
+ margin: 0 auto;
52
+ padding: 40px 24px 80px;
53
+ position: relative;
54
+ z-index: 1;
55
+ }
56
+
57
+ /* Header */
58
+ header {
59
+ margin-bottom: 48px;
60
+ border-bottom: 1px solid var(--border);
61
+ padding-bottom: 24px;
62
+ }
63
+
64
+ .logo {
65
+ font-family: var(--mono);
66
+ font-size: 11px;
67
+ letter-spacing: 0.2em;
68
+ color: var(--accent);
69
+ text-transform: uppercase;
70
+ margin-bottom: 8px;
71
+ }
72
+
73
+ h1 {
74
+ font-family: var(--mono);
75
+ font-size: 32px;
76
+ font-weight: 600;
77
+ color: var(--text-bright);
78
+ letter-spacing: -0.02em;
79
+ }
80
+
81
+ h1 span { color: var(--accent); }
82
+
83
+ .subtitle {
84
+ font-size: 13px;
85
+ color: var(--text-dim);
86
+ font-family: var(--mono);
87
+ margin-top: 6px;
88
+ }
89
+
90
+ /* Search */
91
+ .search-section { margin-bottom: 32px; }
92
+
93
+ .input-wrapper {
94
+ position: relative;
95
+ border: 1px solid var(--border);
96
+ border-radius: 4px;
97
+ background: var(--surface);
98
+ transition: border-color 0.2s;
99
+ }
100
+
101
+ .input-wrapper:focus-within {
102
+ border-color: var(--accent);
103
+ box-shadow: 0 0 0 1px var(--accent)40;
104
+ }
105
+
106
+ .input-label {
107
+ font-family: var(--mono);
108
+ font-size: 10px;
109
+ letter-spacing: 0.15em;
110
+ color: var(--accent);
111
+ padding: 12px 16px 0;
112
+ text-transform: uppercase;
113
+ }
114
+
115
+ textarea {
116
+ width: 100%;
117
+ background: transparent;
118
+ border: none;
119
+ color: var(--text-bright);
120
+ font-family: var(--sans);
121
+ font-size: 15px;
122
+ line-height: 1.6;
123
+ padding: 8px 16px 12px;
124
+ resize: none;
125
+ outline: none;
126
+ }
127
+
128
+ textarea::placeholder { color: var(--text-dim); }
129
+
130
+ .search-actions {
131
+ display: flex;
132
+ align-items: center;
133
+ justify-content: space-between;
134
+ padding: 10px 12px;
135
+ border-top: 1px solid var(--border);
136
+ }
137
+
138
+ .hint {
139
+ font-family: var(--mono);
140
+ font-size: 11px;
141
+ color: var(--text-dim);
142
+ }
143
+
144
+ button#submitBtn {
145
+ background: var(--accent);
146
+ color: #000;
147
+ border: none;
148
+ border-radius: 3px;
149
+ padding: 8px 20px;
150
+ font-family: var(--mono);
151
+ font-size: 12px;
152
+ font-weight: 600;
153
+ letter-spacing: 0.08em;
154
+ cursor: pointer;
155
+ transition: opacity 0.2s;
156
+ }
157
+
158
+ button#submitBtn:hover { opacity: 0.85; }
159
+ button#submitBtn:disabled { opacity: 0.4; cursor: not-allowed; }
160
+
161
+ /* Status pipeline */
162
+ .pipeline {
163
+ display: none;
164
+ margin-bottom: 32px;
165
+ border: 1px solid var(--border);
166
+ border-radius: 4px;
167
+ overflow: hidden;
168
+ }
169
+
170
+ .pipeline.visible { display: block; }
171
+
172
+ .pipeline-header {
173
+ background: var(--surface);
174
+ padding: 10px 16px;
175
+ font-family: var(--mono);
176
+ font-size: 11px;
177
+ color: var(--text-dim);
178
+ letter-spacing: 0.1em;
179
+ text-transform: uppercase;
180
+ border-bottom: 1px solid var(--border);
181
+ display: flex;
182
+ align-items: center;
183
+ justify-content: space-between;
184
+ }
185
+ .progress-wrap {
186
+ width: 160px;
187
+ height: 4px;
188
+ background: var(--border);
189
+ border-radius: 2px;
190
+ overflow: hidden;
191
+ }
192
+ .progress-fill {
193
+ height: 100%;
194
+ width: 0%;
195
+ background: var(--accent);
196
+ border-radius: 2px;
197
+ transition: width 0.6s ease;
198
+ box-shadow: 0 0 8px var(--accent);
199
+ }
200
+ .progress-pct {
201
+ font-family: var(--mono);
202
+ font-size: 11px;
203
+ color: var(--accent);
204
+ min-width: 36px;
205
+ text-align: right;
206
+ }
207
+
208
+ .stage {
209
+ display: flex;
210
+ align-items: center;
211
+ gap: 12px;
212
+ padding: 10px 16px;
213
+ border-bottom: 1px solid var(--border);
214
+ font-family: var(--mono);
215
+ font-size: 12px;
216
+ color: var(--text-dim);
217
+ transition: color 0.3s;
218
+ }
219
+
220
+ .stage:last-child { border-bottom: none; }
221
+
222
+ .stage.active { color: var(--accent); }
223
+ .stage.done { color: var(--text); }
224
+
225
+ .stage-dot {
226
+ width: 6px;
227
+ height: 6px;
228
+ border-radius: 50%;
229
+ background: var(--border);
230
+ flex-shrink: 0;
231
+ transition: background 0.3s;
232
+ }
233
+
234
+ .stage.active .stage-dot {
235
+ background: var(--accent);
236
+ box-shadow: 0 0 8px var(--accent);
237
+ animation: pulse 1s infinite;
238
+ }
239
+
240
+ .stage.done .stage-dot { background: var(--text-dim); }
241
+
242
+ @keyframes pulse {
243
+ 0%, 100% { opacity: 1; }
244
+ 50% { opacity: 0.3; }
245
+ }
246
+
247
+ /* Results */
248
+ .results { display: none; }
249
+ .results.visible { display: block; }
250
+
251
+ .meta-bar {
252
+ display: flex;
253
+ gap: 16px;
254
+ margin-bottom: 24px;
255
+ flex-wrap: wrap;
256
+ }
257
+
258
+ .badge {
259
+ font-family: var(--mono);
260
+ font-size: 11px;
261
+ padding: 4px 10px;
262
+ border-radius: 2px;
263
+ border: 1px solid var(--border);
264
+ color: var(--text-dim);
265
+ }
266
+
267
+ .badge.green {
268
+ border-color: var(--accent);
269
+ color: var(--accent);
270
+ background: var(--accent-dim);
271
+ }
272
+
273
+ /* Synthesis sections */
274
+ .synthesis { margin-bottom: 32px; }
275
+
276
+ .section {
277
+ margin-bottom: 2px;
278
+ border: 1px solid var(--border);
279
+ border-radius: 4px;
280
+ overflow: hidden;
281
+ }
282
+
283
+ .section-header {
284
+ display: flex;
285
+ align-items: center;
286
+ gap: 10px;
287
+ padding: 12px 16px;
288
+ background: var(--surface);
289
+ cursor: pointer;
290
+ user-select: none;
291
+ }
292
+
293
+ .section-header:hover { background: #111820; }
294
+
295
+ .section-tag {
296
+ font-family: var(--mono);
297
+ font-size: 10px;
298
+ letter-spacing: 0.15em;
299
+ text-transform: uppercase;
300
+ color: var(--accent);
301
+ min-width: 180px;
302
+ }
303
+
304
+ .section-chevron {
305
+ margin-left: auto;
306
+ font-size: 10px;
307
+ color: var(--text-dim);
308
+ transition: transform 0.2s;
309
+ }
310
+
311
+ .section.open .section-chevron { transform: rotate(180deg); }
312
+
313
+ .section-body {
314
+ display: none;
315
+ padding: 16px;
316
+ border-top: 1px solid var(--border);
317
+ font-size: 14px;
318
+ line-height: 1.75;
319
+ color: var(--text);
320
+ }
321
+
322
+ .section.open .section-body { display: block; }
323
+
324
+ /* Citations */
325
+ .citations-block {
326
+ border: 1px solid var(--border);
327
+ border-radius: 4px;
328
+ overflow: hidden;
329
+ }
330
+
331
+ .citations-header {
332
+ padding: 12px 16px;
333
+ background: var(--surface);
334
+ font-family: var(--mono);
335
+ font-size: 11px;
336
+ letter-spacing: 0.15em;
337
+ text-transform: uppercase;
338
+ color: var(--text-dim);
339
+ border-bottom: 1px solid var(--border);
340
+ cursor: pointer;
341
+ display: flex;
342
+ align-items: center;
343
+ justify-content: space-between;
344
+ }
345
+
346
+ .citations-list {
347
+ display: none;
348
+ padding: 16px;
349
+ }
350
+
351
+ .citations-list.open { display: block; }
352
+
353
+ .citation-item {
354
+ font-family: var(--mono);
355
+ font-size: 12px;
356
+ color: var(--text-dim);
357
+ padding: 8px 0;
358
+ border-bottom: 1px solid var(--border);
359
+ line-height: 1.6;
360
+ }
361
+
362
+ .citation-item:last-child { border-bottom: none; }
363
+
364
+ .citation-pmid {
365
+ color: var(--accent);
366
+ font-weight: 600;
367
+ }
368
+
369
+ /* Error */
370
+ .error-box {
371
+ display: none;
372
+ padding: 16px;
373
+ border: 1px solid var(--red);
374
+ border-radius: 4px;
375
+ background: #ff4d6d10;
376
+ font-family: var(--mono);
377
+ font-size: 13px;
378
+ color: var(--red);
379
+ margin-bottom: 24px;
380
+ }
381
+
382
+ .error-box.visible { display: block; }
383
+
384
+ /* Footer */
385
+ footer {
386
+ margin-top: 64px;
387
+ padding-top: 24px;
388
+ border-top: 1px solid var(--border);
389
+ font-family: var(--mono);
390
+ font-size: 11px;
391
+ color: var(--text-dim);
392
+ display: flex;
393
+ justify-content: space-between;
394
+ flex-wrap: wrap;
395
+ gap: 8px;
396
+ }
397
+ </style>
398
  </head>
399
  <body>
400
+ <div class="container">
401
+ <header>
402
+ <div class="logo">AMD Developer Hackathon 2026 &mdash; Glitch Squad</div>
403
+ <h1>ARIA <span>//</span> Autonomous Research Intelligence Agent</h1>
404
+ <div class="subtitle">Multi-agent biomedical literature synthesis &middot; PubMed &middot; Groq LLaMA-3.1</div>
405
+ </header>
 
 
 
 
 
406
 
407
+ <div class="search-section">
408
+ <div class="input-wrapper">
409
+ <div class="input-label">Clinical Question</div>
410
+ <textarea id="queryInput" rows="3" placeholder="e.g. What machine learning methods are most effective for epilepsy detection from EEG signals?"></textarea>
411
+ <div class="search-actions">
412
+ <span class="hint">Enter to submit &middot; Shift+Enter for new line</span>
413
+ <button id="submitBtn" onclick="submitQuery()">Run Pipeline</button>
414
  </div>
415
+ </div>
416
+ </div>
417
 
418
+ <div class="pipeline" id="pipeline">
419
+ <div class="pipeline-header">
420
+ <span>Pipeline Status</span>
421
+ <div style="display:flex;align-items:center;gap:10px;">
422
+ <div class="progress-wrap"><div class="progress-fill" id="progressFill"></div></div>
423
+ <span class="progress-pct" id="progressPct">0%</span>
424
  </div>
425
+ </div>
426
+ <div class="stage" id="stage1"><div class="stage-dot"></div>Query Architect &mdash; generating MeSH-optimised PubMed queries</div>
427
+ <div class="stage" id="stage2"><div class="stage-dot"></div>Literature Scout &mdash; fetching and deduplicating papers</div>
428
+ <div class="stage" id="stage3"><div class="stage-dot"></div>Evidence Synthesiser &mdash; building structured synthesis</div>
429
+ <div class="stage" id="stage4"><div class="stage-dot"></div>Citation Builder &mdash; formatting references</div>
430
+ </div>
431
 
432
+ <div class="error-box" id="errorBox"></div>
433
+
434
+ <div class="results" id="results">
435
+ <div class="meta-bar" id="metaBar"></div>
436
+ <div class="synthesis" id="synthesis"></div>
437
+ <div style="display:flex;justify-content:flex-end;margin-bottom:12px;">
438
+ <button id="exportBtn" onclick="exportPDF()" style="background:transparent;border:1px solid var(--accent);color:var(--accent);padding:7px 18px;font-family:var(--mono);font-size:11px;border-radius:3px;cursor:pointer;letter-spacing:0.08em;">Export PDF</button>
439
+ </div>
440
+ <div style="display:flex;justify-content:flex-end;margin-bottom:12px;">
441
+ <button id="exportBtn" onclick="exportPDF()" style="background:transparent;border:1px solid var(--accent);color:var(--accent);padding:7px 18px;font-family:var(--mono);font-size:11px;border-radius:3px;cursor:pointer;letter-spacing:0.08em;">Export PDF</button>
442
+ </div>
443
+ <div class="citations-block">
444
+ <div class="citations-header" onclick="toggleCitations()">
445
+ <span>References</span><span id="citChevron">&#9660;</span>
446
+ </div>
447
+ <div class="citations-list" id="citationsList"></div>
448
+ </div>
449
  </div>
450
+
451
+ <footer>
452
+ <span>Powered by LangGraph &middot; Groq LLaMA-3.1 &middot; PubMed NCBI</span>
453
+ <span>AI-generated synthesis &mdash; verify against primary sources</span>
454
+ </footer>
455
+ </div>
456
+
457
+ <script>
458
+ const SECTIONS = [
459
+ { key: "## Background", label: "Background" },
460
+ { key: "## Key Findings", label: "Key Findings" },
461
+ { key: "## Level of Evidence", label: "Level of Evidence" },
462
+ { key: "## Conflicting Evidence", label: "Conflicting Evidence" },
463
+ { key: "## Research Gaps", label: "Research Gaps" },
464
+ { key: "## Clinical Implications", label: "Clinical Implications" },
465
+ ];
466
+
467
+ function parseSynthesis(text) {
468
+ const result = {};
469
+ for (let i = 0; i < SECTIONS.length; i++) {
470
+ const start = text.indexOf(SECTIONS[i].key);
471
+ if (start === -1) continue;
472
+ const contentStart = start + SECTIONS[i].key.length;
473
+ const nextSection = i + 1 < SECTIONS.length ? text.indexOf(SECTIONS[i + 1].key) : -1;
474
+ result[SECTIONS[i].label] = nextSection === -1
475
+ ? text.slice(contentStart).trim()
476
+ : text.slice(contentStart, nextSection).trim();
477
+ }
478
+ return result;
479
+ }
480
+
481
+ const STAGE_PCT = { 1: 10, 2: 35, 3: 70, 4: 90, 5: 100 };
482
+
483
+ function setStage(n) {
484
+ for (let i = 1; i <= 4; i++) {
485
+ const el = document.getElementById("stage" + i);
486
+ el.classList.remove("active", "done");
487
+ if (i < n) el.classList.add("done");
488
+ else if (i === n) el.classList.add("active");
489
+ }
490
+ const pct = STAGE_PCT[n] || 0;
491
+ document.getElementById("progressFill").style.width = pct + "%";
492
+ document.getElementById("progressPct").textContent = pct + "%";
493
+ }
494
+
495
+ function toggleSection(el) {
496
+ el.classList.toggle("open");
497
+ }
498
+
499
+ function toggleCitations() {
500
+ document.getElementById("citationsList").classList.toggle("open");
501
+ document.getElementById("citChevron").textContent =
502
+ document.getElementById("citationsList").classList.contains("open") ? "▲" : "▼";
503
+ }
504
+
505
+ async function submitQuery() {
506
+ const q = document.getElementById("queryInput").value.trim();
507
+ if (!q) return;
508
+
509
+ const btn = document.getElementById("submitBtn");
510
+ btn.disabled = true;
511
+
512
+ document.getElementById("results").classList.remove("visible");
513
+ document.getElementById("errorBox").classList.remove("visible");
514
+ document.getElementById("pipeline").classList.add("visible");
515
+ setStage(1);
516
+
517
+ // Simulate stage progression while waiting
518
+ const delays = [0, 3000, 8000, 14000];
519
+ delays.forEach((d, i) => setTimeout(() => setStage(i + 1), d));
520
+
521
+ try {
522
+ const res = await fetch("/query", {
523
+ method: "POST",
524
+ headers: { "Content-Type": "application/json" },
525
+ body: JSON.stringify({ query: q })
526
+ });
527
+ const data = await res.json();
528
+ lastResult = data;
529
+ lastQuery = q;
530
+
531
+ setStage(5); // all done
532
+
533
+ if (data.error) {
534
+ document.getElementById("errorBox").textContent = "Error: " + data.error;
535
+ document.getElementById("errorBox").classList.add("visible");
536
+ return;
537
+ }
538
+
539
+ // Meta bar
540
+ const meta = document.getElementById("metaBar");
541
+ meta.innerHTML = `
542
+ <div class="badge green">${data.paper_count} papers retrieved</div>
543
+ <div class="badge">${data.queries ? data.queries.length : 0} PubMed queries</div>
544
+ <div class="badge">LLaMA-3.1-8B via Groq</div>
545
+ `;
546
+
547
+ // Synthesis sections
548
+ const sections = parseSynthesis(data.synthesis);
549
+ const synthEl = document.getElementById("synthesis");
550
+ synthEl.innerHTML = "";
551
+ SECTIONS.forEach(s => {
552
+ if (!sections[s.label]) return;
553
+ const div = document.createElement("div");
554
+ div.className = "section open";
555
+ div.innerHTML = `
556
+ <div class="section-header" onclick="toggleSection(this.parentElement)">
557
+ <span class="section-tag">${s.label}</span>
558
+ <span class="section-chevron">▲</span>
559
+ </div>
560
+ <div class="section-body">${sections[s.label].replace(/\n/g, '<br>')}</div>
561
+ `;
562
+ synthEl.appendChild(div);
563
+ });
564
+
565
+ // Citations
566
+ const citList = document.getElementById("citationsList");
567
+ citList.innerHTML = "";
568
+ citList.classList.add("open");
569
+ data.citations.split("\n").forEach(line => {
570
+ if (!line.trim()) return;
571
+ const pmidMatch = line.match(/PMID:\s*(\d+)/);
572
+ const pmid = pmidMatch ? pmidMatch[1] : null;
573
+ const div = document.createElement("div");
574
+ div.className = "citation-item";
575
+ div.innerHTML = pmid
576
+ ? line.replace(/PMID:\s*\d+/, `<span class="citation-pmid">PMID: ${pmid}</span>`)
577
+ : line;
578
+ citList.appendChild(div);
579
+ });
580
+
581
+ document.getElementById("results").classList.add("visible");
582
+
583
+ } catch (err) {
584
+ document.getElementById("errorBox").textContent = "Network error: " + err.message;
585
+ document.getElementById("errorBox").classList.add("visible");
586
+ } finally {
587
+ btn.disabled = false;
588
+ }
589
+ }
590
+
591
+ document.getElementById("queryInput").addEventListener("keydown", e => {
592
+ if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); submitQuery(); }
593
+ });
594
+
595
+ let lastResult = null;
596
+ let lastQuery = '';
597
+
598
+ async function exportPDF() {
599
+ const btn = document.getElementById('exportBtn');
600
+ btn.textContent = 'Generating...';
601
+ btn.disabled = true;
602
+ try {
603
+ const res = await fetch('/export-pdf', {
604
+ method: 'POST',
605
+ headers: {'Content-Type': 'application/json'},
606
+ body: JSON.stringify({
607
+ synthesis: lastResult.synthesis,
608
+ citations: lastResult.citations,
609
+ query: lastQuery,
610
+ paper_count: lastResult.paper_count
611
+ })
612
+ });
613
+ const blob = await res.blob();
614
+ const url = URL.createObjectURL(blob);
615
+ const a = document.createElement('a');
616
+ a.href = url; a.download = 'ARIA_report.pdf'; a.click();
617
+ URL.revokeObjectURL(url);
618
+ } finally {
619
+ btn.textContent = 'Export PDF';
620
+ btn.disabled = false;
621
+ }
622
+ }
623
+ </script>
624
  </body>
625
+ </html>