Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,4 @@ yarn-error.log*
/embeddings_backup.npz
/AGENTS.md
/CLAUDE.md
/scripts/ga-performance.env
43 changes: 43 additions & 0 deletions app/api/pubmed-abstract/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { NextRequest, NextResponse } from "next/server";

const cache = new Map<string, { abstract: string; title: string; fetchedAt: number }>();
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours

export async function GET(request: NextRequest) {
const pmid = request.nextUrl.searchParams.get("pmid");
if (!pmid || !/^\d+$/.test(pmid)) {
return NextResponse.json({ error: "Invalid PMID" }, { status: 400 });
}

const cached = cache.get(pmid);
if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
return NextResponse.json({ abstract: cached.abstract, title: cached.title });
}

try {
const url = `https://www.ebi.ac.uk/europepmc/webservices/rest/article/MED/${pmid}?resultType=core&format=json`;
const res = await fetch(url, {
headers: { Accept: "application/json" },
signal: AbortSignal.timeout(8000),
});

if (!res.ok) {
return NextResponse.json({ error: `Europe PMC returned ${res.status}` }, { status: 502 });
}

const data = await res.json();
const article = data?.result;
if (!article) {
return NextResponse.json({ error: "Article not found" }, { status: 404 });
}

const abstract: string = article.abstractText ?? "";
const title: string = article.title ?? "";

cache.set(pmid, { abstract, title, fetchedAt: Date.now() });
return NextResponse.json({ abstract, title });
} catch (err) {
const message = err instanceof Error ? err.message : "Fetch failed";
return NextResponse.json({ error: message }, { status: 502 });
}
}
43 changes: 43 additions & 0 deletions app/api/studies/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,49 @@ export async function GET(request: NextRequest) {
if (originError) return originError;

const searchParams = request.nextUrl.searchParams;

// Fast single-study lookup by primary key — used by the study detail page
const idParam = searchParams.get("id");
if (idParam !== null) {
const studyPk = parseInt(idParam);
if (isNaN(studyPk) || studyPk <= 0) {
return NextResponse.json({ error: "Invalid id" }, { status: 400 });
}
try {
const rows = await executeQuery<RawStudy>(
`SELECT id, study_accession, study, disease_trait, mapped_trait,
mapped_trait_uri, mapped_gene, first_author, date, journal,
pubmedid, link, initial_sample_size, replication_sample_size,
p_value, pvalue_mlog, or_or_beta, ci_text, risk_allele_frequency,
strongest_snp_risk_allele, snps
FROM gwas_catalog WHERE id = $1 LIMIT 1`,
[studyPk]
);
if (rows.length === 0) {
return NextResponse.json({ data: [], total: 0, limit: 1 });
}
const row = rows[0];
const sampleSize = parseSampleSize(row.initial_sample_size) ?? parseSampleSize(row.replication_sample_size);
const pValueNumeric = parsePValue(row.p_value);
const logPValue = parseLogPValue(row.pvalue_mlog) ?? (pValueNumeric ? -Math.log10(pValueNumeric) : null);
const qualityFlags = computeQualityFlags(sampleSize, pValueNumeric, logPValue);
const isLowQuality = qualityFlags.some(f => f.severity === 'major');
const confidenceBand = determineConfidenceBand(sampleSize, pValueNumeric, logPValue, qualityFlags);
const publicationDate = parseStudyDate(row.date);
const isAnalyzable = !!(row.snps && row.or_or_beta && row.strongest_snp_risk_allele);
const nonAnalyzableReason = !isAnalyzable
? (!row.snps ? 'Missing SNP data' : !row.or_or_beta ? 'Missing effect size (OR/beta)' : 'Missing risk allele')
: undefined;
const study = { ...row, sampleSize, sampleSizeLabel: formatNumber(sampleSize), pValueNumeric,
pValueLabel: formatPValue(pValueNumeric), logPValue, qualityFlags, isLowQuality, confidenceBand,
publicationDate, isAnalyzable, nonAnalyzableReason };
return NextResponse.json({ data: [study], total: 1, limit: 1 });
} catch (error) {
const message = error instanceof Error ? error.message : "Failed to fetch study";
return NextResponse.json({ error: message }, { status: 500 });
}
}

const search = searchParams.get("search")?.trim();
const searchTerms = search ? getSearchTerms(search) : [];
const trait = searchParams.get("trait")?.trim();
Expand Down
20 changes: 20 additions & 0 deletions app/browse/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { Metadata } from "next";

export const metadata: Metadata = {
title: "Browse GWAS Studies - Monadic DNA Explorer",
description: "Search and filter millions of genetic associations from the GWAS Catalog. Upload your DNA data to see personalized results.",
keywords: ["GWAS", "genetic studies", "DNA research", "genome-wide association", "genetic variants", "SNP analysis"],
openGraph: {
title: "Browse GWAS Studies - Monadic DNA Explorer",
description: "Search millions of genetic associations and analyze your DNA data",
type: "website",
},
};

export default function ExploreLayout({
children,
}: {
children: React.ReactNode;
}) {
return <>{children}</>;
}
Loading
Loading