|
from fasthtml.common import * |
|
from pathlib import Path |
|
from datetime import datetime |
|
from starlette.responses import RedirectResponse, FileResponse |
|
from starlette.requests import Request |
|
import json |
|
import numpy as np |
|
from sklearn.metrics.pairwise import cosine_similarity |
|
from fasthtml_hf import setup_hf_backup |
|
|
|
|
|
with open('data/projects.json', 'r') as f: |
|
projects = json.load(f) |
|
|
|
|
|
with open("data/acr/2024/summaries.json") as f: |
|
summaries = json.loads(f.read()) |
|
|
|
with open("data/acr/2024/abstracts.jsonl", "r") as f: |
|
abstracts = [json.loads(line) for line in f] |
|
|
|
for abstract in abstracts: |
|
abstract['abstract'] = abstract['abstract'].replace("## Background/Purpose\n", "Background/Purpose\n") |
|
abstract['abstract'] = abstract['abstract'].replace("## Methods\n", "Methods\n") |
|
|
|
|
|
embeddings = np.load('data/acr/2024/embeddings.npy') |
|
|
|
|
|
for abstract, embedding in zip(abstracts, embeddings): |
|
abstract['embedding'] = embedding |
|
|
|
|
|
hdrs = ( |
|
picolink, |
|
StyleX("static/css/style.css"), |
|
MarkdownJS(), |
|
HighlightJS(langs=['python', 'javascript', 'html', 'css']) |
|
) |
|
app = FastHTML(hdrs=hdrs) |
|
|
|
def get_posts(): |
|
posts = [] |
|
for path in Path("posts").glob("*.md"): |
|
with open(path, 'r') as f: |
|
title = f.readline().strip() |
|
date = datetime.fromtimestamp(path.stat().st_mtime) |
|
posts.append({"title": title, "date": date, "path": path}) |
|
return sorted(posts, key=lambda x: x['date'], reverse=True) |
|
|
|
def nav_menu(current_page="home"): |
|
return Nav( |
|
A("Home", href="/", cls="active" if current_page == "home" else ""), |
|
A("Blog", href="/blog", cls="active" if current_page == "blog" else ""), |
|
A("Projects", href="/projects", cls="active" if current_page == "projects" else ""), |
|
A("ACR24", href="/acr24", cls="active" if current_page == "acr24" else ""), |
|
) |
|
|
|
|
|
@app.get("/") |
|
def home(): |
|
content = Main( |
|
nav_menu("home"), |
|
Div( |
|
Img(src="media/me.jpeg", alt="Profile Photo"), |
|
H1("Dr. Chris McMaster"), |
|
P("Rheumatologist and Data Scientist"), |
|
P("Using AI to improve healthcare"), |
|
cls="profile" |
|
), |
|
cls="container" |
|
) |
|
return Title("RheumAI"), content |
|
|
|
@app.get("/blog") |
|
def blog(): |
|
posts = get_posts() |
|
content = Main( |
|
nav_menu("blog"), |
|
Div( |
|
H1("Blog Posts"), |
|
*[Div( |
|
H2(A(post['title'], href=f"/post/{post['path'].stem}")), |
|
P(post['date'].strftime('%Y-%m-%d'), cls="date"), |
|
cls="blog-post" |
|
) for post in posts], |
|
cls="container" |
|
) |
|
) |
|
return Title("Blog - RheumAI"), content |
|
|
|
@app.get("/projects") |
|
def projects_page(): |
|
content = Main( |
|
nav_menu("projects"), |
|
Div( |
|
H1("Projects"), |
|
*[Div( |
|
H2(project["name"]), |
|
P(project["description"]), |
|
A("Visit Project", href=project["external_link"], cls="button"), |
|
cls="blog-post" |
|
) for project in projects], |
|
cls="container" |
|
) |
|
) |
|
return Title("Projects - RheumAI"), content |
|
|
|
|
|
app.mount("/static", StaticFiles(directory="static"), name="static") |
|
app.mount("/media", StaticFiles(directory="media"), name="media") |
|
app.mount("/posts/images", StaticFiles(directory="posts/images"), name="post_images") |
|
app.mount("/data/acr/2024", StaticFiles(directory="data/acr/2024"), name="acr24_data") |
|
|
|
@app.get("/post/{slug}") |
|
def post(slug: str): |
|
path = Path(f"posts/{slug}.md") |
|
if not path.exists(): |
|
return "Post not found", 404 |
|
|
|
with open(path, 'r') as f: |
|
title = f.readline().strip() |
|
content = f.read() |
|
|
|
|
|
content = content.replace('](images/', '](/posts/images/') |
|
content = content.replace('](/images/', '](/posts/images/') |
|
|
|
|
|
img_style = Style(""" |
|
.blog-post img { |
|
max-width: 100%; |
|
height: auto; |
|
margin: 1em 0; |
|
} |
|
.blog-post .marked { |
|
overflow-x: auto; |
|
} |
|
""") |
|
|
|
|
|
current_abstract = next((a for a in abstracts if a['slug'] == slug), None) |
|
if not current_abstract: |
|
return "Abstract not found", 404 |
|
|
|
|
|
similar_button = Button("Find Similar Abstracts", |
|
hx_get=f"/acr24/similar/{slug}", |
|
hx_target="#similar-abstracts", |
|
cls="button") |
|
|
|
content = content.replace('</div>', f'\n<div id="similar-abstracts"></div>\n{similar_button}</div>') |
|
|
|
return Title(title), ( |
|
img_style, |
|
Main( |
|
nav_menu("blog"), |
|
Div( |
|
H1(title), |
|
Div(content, cls="marked"), |
|
cls="blog-post container" |
|
) |
|
) |
|
) |
|
|
|
@app.get("/favicon.ico") |
|
def favicon(): |
|
return FileResponse("static/favicon.ico") |
|
|
|
|
|
|
|
|
|
@app.get("/acr24") |
|
def acr24(): |
|
|
|
with open("data/acr/2024/summaries.json") as f: |
|
summaries = json.loads(f.read()) |
|
|
|
|
|
|
|
title = Title("ACR 2024") |
|
header = H1("ACR 2024") |
|
|
|
|
|
tabs = Nav( |
|
Ul( |
|
Li(A("AI Summaries", href="#", data_tab="summaries", cls="active")), |
|
Li(A("Search", href="#", data_tab="search")), |
|
Li(A("Embeddings", href="#", data_tab="embeddings")), |
|
cls="tabs", |
|
style="margin-bottom: 0" |
|
) |
|
) |
|
|
|
|
|
summaries_section = Section( |
|
H2("AI-Generated Topic Summaries"), |
|
A("Download as PDF", href="data/acr/2024/summaries.pdf", cls="button", style="margin-bottom: 1rem"), |
|
Div(*[ |
|
Article( |
|
H3(topic), |
|
Div(summary, cls="marked"), |
|
cls="summary-card" |
|
) for topic, summary in summaries.items() |
|
]), |
|
id="summaries", |
|
data_section="summaries", |
|
cls="active" |
|
) |
|
|
|
search_section = Section( |
|
H2("Search Abstracts"), |
|
Form( |
|
Input( |
|
type="search", |
|
name="q", |
|
placeholder="Search abstracts...", |
|
hx_post="/acr24/search", |
|
hx_trigger="keyup changed delay:500ms", |
|
hx_target="#search-results" |
|
), |
|
cls="search-form" |
|
), |
|
Div(id="search-results"), |
|
id="search", |
|
data_section="search" |
|
) |
|
|
|
|
|
style = Style(""" |
|
.tabs { |
|
margin-bottom: 2rem; |
|
list-style: none; |
|
padding: 0; |
|
} |
|
.tabs li { |
|
display: inline-block; |
|
margin-right: 1rem; |
|
} |
|
.tabs a { |
|
display: inline-block; |
|
padding: 0.5rem 1rem; |
|
text-decoration: none; |
|
border: 1px solid transparent; |
|
border-bottom: none; |
|
margin-bottom: -1px; |
|
} |
|
|
|
[data-section] { |
|
display: none; |
|
} |
|
[data-section].active { |
|
display: block; |
|
} |
|
""") |
|
|
|
|
|
script = Script(""" |
|
document.addEventListener('DOMContentLoaded', function() { |
|
const tabLinks = document.querySelectorAll('a[data-tab]'); |
|
const sections = document.querySelectorAll('[data-section]'); |
|
|
|
function switchTab(targetTab) { |
|
// Update active tab |
|
tabLinks.forEach(link => { |
|
link.classList.remove('active'); |
|
if (link.dataset.tab === targetTab) { |
|
link.classList.add('active'); |
|
} |
|
}); |
|
|
|
// Show/hide sections using classes |
|
sections.forEach(section => { |
|
section.classList.remove('active'); |
|
if (section.dataset.section === targetTab) { |
|
section.classList.add('active'); |
|
} |
|
}); |
|
} |
|
|
|
// Add click handlers to tab links |
|
tabLinks.forEach(link => { |
|
link.addEventListener('click', (e) => { |
|
e.preventDefault(); |
|
switchTab(link.dataset.tab); |
|
}); |
|
}); |
|
|
|
// Set initial active tab |
|
switchTab('summaries'); |
|
}); |
|
""") |
|
|
|
|
|
embeddings_section = Section( |
|
H2("Embeddings Plot"), |
|
A(Img(src="/data/acr/2024/embeddings.png", alt="Embeddings TSNE Plot"), |
|
href="/data/acr/2024/embeddings.png"), |
|
id="embeddings", |
|
data_section="embeddings" |
|
) |
|
|
|
content = Main( |
|
nav_menu("acr24"), |
|
Container( |
|
title, |
|
header, |
|
tabs, |
|
summaries_section, |
|
search_section, |
|
embeddings_section, |
|
style, |
|
script |
|
) |
|
) |
|
return Title("ACR 2024 - RheumAI"), content |
|
|
|
@app.post("/acr24/search") |
|
def search(request: Request, q: str = Form(...)): |
|
"""Handle abstract search requests""" |
|
if not q: |
|
return Div("Enter search terms above") |
|
|
|
|
|
q = q.lower() |
|
|
|
|
|
matches = [] |
|
for abstract in abstracts: |
|
|
|
if (q in abstract.get('title', '').lower() or |
|
q in abstract.get('abstract', '').lower()): |
|
matches.append(abstract) |
|
|
|
|
|
if not matches: |
|
return Div(P("No matching abstracts found")) |
|
|
|
|
|
toggle_buttons = Div( |
|
Button("Show First 10", |
|
hx_post=f"/acr24/search?q={q}&limit=10", |
|
hx_target="#search-results", |
|
cls="active"), |
|
Button("Show All", |
|
hx_post=f"/acr24/search?q={q}&limit=all", |
|
hx_target="#search-results"), |
|
cls="view-toggle" |
|
) |
|
|
|
|
|
limit = request.query_params.get('limit', '10') |
|
results_to_show = matches if limit == 'all' else matches[:10] |
|
|
|
|
|
modal_html = Div( |
|
Div( |
|
Button("×", cls="modal-close", onclick="closeModal()"), |
|
Div(id="modal-content"), |
|
cls="modal" |
|
), |
|
id="modal-overlay", |
|
cls="modal-overlay", |
|
) |
|
|
|
|
|
modal_script = Script(""" |
|
function showModal(abstractNumber) { |
|
fetch(`/acr24/similar/${abstractNumber}`) |
|
.then(response => response.text()) |
|
.then(html => { |
|
document.getElementById('modal-content').innerHTML = html; |
|
document.getElementById('modal-overlay').style.display = 'block'; |
|
}); |
|
} |
|
|
|
function closeModal() { |
|
document.getElementById('modal-overlay').style.display = 'none'; |
|
} |
|
|
|
// Close modal when clicking outside |
|
document.getElementById('modal-overlay').addEventListener('click', function(e) { |
|
if (e.target === this) { |
|
closeModal(); |
|
} |
|
}); |
|
""") |
|
|
|
|
|
return Div( |
|
modal_html, |
|
modal_script, |
|
toggle_buttons, |
|
P(f"Found {len(matches)} matching abstracts:"), |
|
*[Article( |
|
H5(A(abstract['title'], href=abstract['link'])), |
|
P(abstract.get('abstract', '')[:300] + "..."), |
|
Button("Find Similar", |
|
onclick=f"showModal('{abstract['abstract_number']}')", |
|
cls="button similar-button"), |
|
cls="abstract-result" |
|
) for abstract in results_to_show] |
|
) |
|
|
|
|
|
style = Style(""" |
|
.view-toggle { |
|
margin-bottom: 1rem; |
|
} |
|
.view-toggle button { |
|
margin-right: 0.5rem; |
|
} |
|
.view-toggle button.active { |
|
background: #4a5568; |
|
color: white; |
|
} |
|
""") |
|
|
|
|
|
@app.get("/{project_name}") |
|
def project(project_name: str): |
|
project = next((p for p in projects if p["internal_link"] == project_name), None) |
|
if project: |
|
return RedirectResponse(url=project["external_link"]) |
|
else: |
|
return "Project not found", 404 |
|
|
|
@app.get("/acr24/similar/{abstract_number}") |
|
def find_similar(abstract_number: str): |
|
|
|
current_abstract = next((a for a in abstracts if a['abstract_number'] == abstract_number), None) |
|
if not current_abstract: |
|
return Div("Abstract not found", cls="error") |
|
|
|
current_embedding = current_abstract['embedding'].reshape(1, -1) |
|
|
|
|
|
similarities = cosine_similarity(current_embedding, embeddings)[0] |
|
|
|
|
|
similar_indices = similarities.argsort()[::-1][1:6] |
|
similar_abstracts = [abstracts[i] for i in similar_indices] |
|
|
|
|
|
return Div( |
|
H2("Similar Abstracts"), |
|
Ul( |
|
*[ |
|
Li( |
|
H3(A(abstract['title'], href=abstract['link'])), |
|
P(f"Topic: {abstract['topic']}"), |
|
Div(abstract.get('abstract', '')[:200] + "...", cls="marked"), |
|
cls="similar-abstract" |
|
) for abstract in similar_abstracts |
|
], |
|
cls="similar-list" |
|
) |
|
) |
|
setup_hf_backup(app) |
|
if __name__ == "__main__": |
|
serve() |
|
|