diff --git a/app/components/LLMChatInline.tsx b/app/components/LLMChatInline.tsx index 453200b..a070f55 100644 --- a/app/components/LLMChatInline.tsx +++ b/app/components/LLMChatInline.tsx @@ -98,7 +98,6 @@ export default function AIChatInline({ initialInput }: { initialInput?: string } const requirePremium = (): boolean => { if (!hasPremiumAccess && !hasValidPromoAccess()) { - if (!isAuthenticated) { openAuthModal(); return false; } router.push('/subscribe'); return false; } @@ -692,9 +691,10 @@ Write questions from the user's perspective — as if the user is asking you. No }; const handleResearch = async () => { - const query = inputValue.trim(); - if (!query || isLoading) return; + if (isLoading) return; if (!requirePremium()) return; + const query = inputValue.trim(); + if (!query) return; setInputValue(''); setIsLoading(true); @@ -1236,7 +1236,7 @@ Write questions from the user's perspective — as if the user is asking you. No + + ); +} diff --git a/app/components/StudyPersonalSection.tsx b/app/components/StudyPersonalSection.tsx new file mode 100644 index 0000000..caf6aac --- /dev/null +++ b/app/components/StudyPersonalSection.tsx @@ -0,0 +1,65 @@ +"use client"; + +import { useResults } from "./ResultsContext"; +import StudyPersonalResultBanner from "./StudyPersonalResultBanner"; +import StudyInlineAnalysis from "./StudyInlineAnalysis"; + +type Props = { + studyId: number; + studyAccession: string | null; + snps: string | null; + traitName: string; + studyTitle: string; + riskAllele?: string | null; + orOrBeta?: string | null; + ciText?: string | null; + isAnalyzable: boolean; + nonAnalyzableReason?: string; + pubmedId?: string | null; + mappedGene?: string | null; + reportedTrait?: string | null; +}; + +export default function StudyPersonalSection({ + studyId, + studyAccession, + snps, + traitName, + studyTitle, + riskAllele, + orOrBeta, + ciText, + isAnalyzable, + nonAnalyzableReason, + pubmedId, + mappedGene, + reportedTrait, +}: Props) { + const { hasResult, getResult } = useResults(); + const result = getResult(studyId); + + return ( + <> + + {hasResult(studyId) && result && ( + + )} + + ); +} diff --git a/app/overview-report/page.tsx b/app/overview-report/page.tsx index a675d42..0cc0d49 100644 --- a/app/overview-report/page.tsx +++ b/app/overview-report/page.tsx @@ -55,7 +55,6 @@ export default function OverviewReportPage() { const requirePremium = () => { if (!hasPremiumAccess && !hasValidPromoAccess()) { - if (!isAuthenticated) { openAuthModal(); return false; } router.push('/subscribe'); return false; } diff --git a/app/study/[id]/page.tsx b/app/study/[id]/page.tsx index d9264df..637600f 100644 --- a/app/study/[id]/page.tsx +++ b/app/study/[id]/page.tsx @@ -1,154 +1,53 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { useParams, useRouter } from "next/navigation"; import Link from "next/link"; +import { notFound } from "next/navigation"; import MenuBar from "../../components/MenuBar"; import Footer from "../../components/Footer"; import VariantChips from "../../components/VariantChips"; -import StudyPersonalResultBanner from "../../components/StudyPersonalResultBanner"; -import { useResults } from "../../components/ResultsContext"; -import StudyInlineAnalysis from "../../components/StudyInlineAnalysis"; - -type Study = { - id: number; - study_accession: string | null; - study: string | null; - disease_trait: string | null; - mapped_trait: string | null; - mapped_trait_uri: string | null; - mapped_gene: string | null; - first_author: string | null; - date: string | null; - journal: string | null; - pubmedid: string | null; - link: string | null; - initial_sample_size: string | null; - replication_sample_size: string | null; - p_value: string | null; - pvalue_mlog: string | null; - or_or_beta: string | null; - ci_text: string | null; - risk_allele_frequency: string | null; - strongest_snp_risk_allele: string | null; - snps: string | null; - sampleSize: number | null; - sampleSizeLabel: string; - pValueNumeric: number | null; - pValueLabel: string; - logPValue: number | null; - qualityFlags: Array<{ message: string; severity: string }>; - isLowQuality: boolean; - confidenceBand: "high" | "medium" | "low"; - publicationDate: number | null; - isAnalyzable: boolean; - nonAnalyzableReason?: string; -}; - -export default function StudyDetailPage() { - const params = useParams(); - const router = useRouter(); - const studyId = params.id as string; - const [study, setStudy] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const { savedResults, hasResult, getResult } = useResults(); - const [totalStudies, setTotalStudies] = useState(null); - const [navigating, setNavigating] = useState(false); - - useEffect(() => { - if (savedResults.length > 0) return; - fetch("/api/studies?limit=1") - .then(r => r.json()) - .then(data => { if (data.total) setTotalStudies(data.total); }) - .catch(() => {}); - }, [savedResults.length]); - - const handleNextRandom = () => { - setNavigating(true); - if (savedResults.length > 0) { - const result = savedResults[Math.floor(Math.random() * savedResults.length)]; - router.push(`/study/${result.studyId}`); - } else if (totalStudies !== null) { - router.push(`/study/${Math.floor(Math.random() * totalStudies) + 1}`); - } - }; +import StudyNavButtons from "../../components/StudyNavButtons"; +import StudyPersonalSection from "../../components/StudyPersonalSection"; +import { fetchStudyById } from "@/lib/study-service"; + +function interpretSampleSize(n: number | null): string { + if (n === null) return ""; + if (n >= 100000) return "Very large study"; + if (n >= 10000) return "Large study"; + if (n >= 1000) return "Mid-size study"; + return "Smaller study"; +} - useEffect(() => { - const fetchStudy = async () => { - try { - setLoading(true); - setError(null); +function interpretPValue(logP: number | null): string { + if (logP === null) return ""; + if (logP >= 10) return "Exceptionally strong evidence"; + if (logP >= 7.3) return "Very strong evidence"; + if (logP >= 5) return "Strong evidence"; + if (logP >= 3) return "Moderate evidence"; + return "Suggestive evidence"; +} - // Fetch study by ID from the API - const response = await fetch(`/api/studies?id=${parseInt(studyId)}`); +function interpretEffectSize(orBeta: string | null): string { + if (!orBeta) return ""; + const val = parseFloat(orBeta); + if (isNaN(val)) return ""; + if (val >= 2 || val <= 0.5) return "Large effect"; + if (val >= 1.3 || val <= 0.77) return "Moderate effect"; + if (val >= 1.1 || val <= 0.91) return "Subtle effect"; + return "Very subtle effect"; +} - if (!response.ok) { - throw new Error('Failed to fetch study details'); - } +function formatDisplayDate(dateStr: string): string { + const ts = Date.parse(dateStr); + if (Number.isNaN(ts)) return dateStr; + return new Date(ts).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" }); +} - const data = await response.json(); +export default async function StudyDetailPage({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + const studyPk = parseInt(id); - if (data.data && data.data.length > 0) { - setStudy(data.data[0]); - } else { - setError('Study not found'); - } - } catch (err) { - setError(err instanceof Error ? err.message : 'An error occurred'); - } finally { - setLoading(false); - } - }; + if (isNaN(studyPk) || studyPk <= 0) notFound(); - if (studyId) { - fetchStudy(); - } - }, [studyId]); - - if (loading) { - return ( - <> -
- -
-
-

Loading study details...

-
-
-
-
- - ); - } - - if (error || !study) { - return ( - <> -
- -
-
-

Study Not Found

-

{error || 'The requested study could not be found.'}

- - ← Back to Browse - -
-
-
-
- - ); - } + const study = await fetchStudyById(studyPk); + if (!study) notFound(); const reportedTrait = study.disease_trait?.trim() || null; const mappedTrait = study.mapped_trait?.trim() || null; @@ -159,235 +58,181 @@ export default function StudyDetailPage() { const pubmedLink = study.pubmedid ? `https://pubmed.ncbi.nlm.nih.gov/${study.pubmedid}` : null; - const studyLink = gwasLink || study.link || pubmedLink; - - const interpretSampleSize = (n: number | null): string => { - if (n === null) return ""; - if (n >= 100000) return "Very large study"; - if (n >= 10000) return "Large study"; - if (n >= 1000) return "Mid-size study"; - return "Smaller study"; - }; - - const interpretPValue = (logP: number | null): string => { - if (logP === null) return ""; - if (logP >= 10) return "Exceptionally strong evidence"; - if (logP >= 7.3) return "Very strong evidence"; - if (logP >= 5) return "Strong evidence"; - if (logP >= 3) return "Moderate evidence"; - return "Suggestive evidence"; - }; - - const interpretEffectSize = (orBeta: string | null): string => { - if (!orBeta) return ""; - const val = parseFloat(orBeta); - if (isNaN(val)) return ""; - // OR interpretation - if (val >= 2 || val <= 0.5) return "Large effect"; - if (val >= 1.3 || val <= 0.77) return "Moderate effect"; - if (val >= 1.1 || val <= 0.91) return "Subtle effect"; - return "Very subtle effect"; - }; - - const navButtons = ( -
- - ← Back to Browse - - -
- ); return ( - <> -
- -
- {/* Breadcrumb + top nav */} -
- - Home - {" > "} - Browse - {" > "} - Study {study.id} +
+ +
+ {/* Breadcrumb + top nav */} +
+ + Home + {" > "} + Browse + {" > "} + Study {study.id} + + +
+ + {/* Study Header */} +
+
+ + {trait} - {navButtons} +
+

{study.study || "Untitled Study"}

+ +
+ {reportedTrait && mappedTrait && mappedTrait !== reportedTrait && ( + Reported trait: {reportedTrait} + )} + {study.first_author && ( + Author: {study.first_author} + )} + {study.date && ( + Date: {formatDisplayDate(study.date)} + )} + {study.journal && ( + Journal: {study.journal} + )} + {study.study_accession && ( + Accession: {study.study_accession} + )} + {study.mapped_gene && ( + Gene: {study.mapped_gene} + )}
- {/* Study Header */} -
-
+ + {/* Personal result banner + inline analysis (client-rendered, reads from browser) */} + + + {/* Study Details */} +
+
+ {study.date && ( +
+ Published + {new Date(study.date).getFullYear()} + {study.journal || "Peer-reviewed"} +
+ )} + {study.sampleSize !== null && ( +
+ Participants + {study.sampleSizeLabel} + {interpretSampleSize(study.sampleSize)} +
+ )} + {study.pValueNumeric !== null && ( +
+ P-value + {study.pValueLabel} + {interpretPValue(study.logPValue)} +
+ )} + {study.or_or_beta && ( +
+ Effect size + + {study.or_or_beta} + {study.ci_text ? {study.ci_text} : null} + + {interpretEffectSize(study.or_or_beta)} +
+ )} + {study.risk_allele_frequency && ( +
+ Variant frequency + {study.risk_allele_frequency} + In population +
+ )} +
+ Confidence + + + {study.confidenceBand === "high" ? "High" : study.confidenceBand === "medium" ? "Medium" : "Lower"} + + + + {study.confidenceBand === "high" ? "Well-replicated" : study.confidenceBand === "medium" ? "Some caveats" : "Interpret carefully"}
-

{study.study || "Untitled Study"}

+
-
- {reportedTrait && mappedTrait && mappedTrait !== reportedTrait && Reported trait: {reportedTrait}} - {study.first_author && Author: {study.first_author}} - {study.date && Date: {new Date(study.date).toLocaleDateString()}} - {study.journal && Journal: {study.journal}} - {study.study_accession && Accession: {study.study_accession}} - {study.mapped_gene && Gene: {study.mapped_gene}} + {study.snps && ( +
+ Variants tested +
+ )} -
- {gwasLink && ( - - Source data → - Full dataset on GWAS Catalog - + {(study.initial_sample_size || study.replication_sample_size) && ( +
+ {study.initial_sample_size && ( +

+ Initial sample + {study.initial_sample_size} +

)} - {pubmedLink && ( - - Research paper → - Published article on PubMed - + {study.replication_sample_size && ( +

+ Replication + {study.replication_sample_size} +

)}
-
- - {/* Personal Result Banner */} - - - {/* Inline LLM analysis when a saved result exists for this study */} - {hasResult(study.id) && getResult(study.id) && ( - )} - {/* Study Details */} -
- {/* Stat grid */} -
- {study.date && ( -
- Published - {new Date(study.date).getFullYear()} - {study.journal || "Peer-reviewed"} -
- )} - {study.sampleSize !== null && ( -
- Participants - {study.sampleSizeLabel} - {interpretSampleSize(study.sampleSize)} -
- )} - {study.pValueNumeric !== null && ( -
- P-value - {study.pValueLabel} - {interpretPValue(study.logPValue)} -
- )} - {study.or_or_beta && ( -
- Effect size - - {study.or_or_beta} - {study.ci_text ? {study.ci_text} : null} - - {interpretEffectSize(study.or_or_beta)} -
- )} - {study.risk_allele_frequency && ( -
- Variant frequency - {study.risk_allele_frequency} - In population + {study.qualityFlags.length > 0 && ( +
+ {study.qualityFlags.map((flag, index) => ( +
+ {flag.message}
- )} -
- Confidence - - - {study.confidenceBand === "high" ? "High" : study.confidenceBand === "medium" ? "Medium" : "Lower"} - - - - {study.confidenceBand === "high" ? "Well-replicated" : study.confidenceBand === "medium" ? "Some caveats" : "Interpret carefully"} - -
+ ))}
+ )} +
- {/* Genetic variants */} - {study.snps && ( -
- Variants tested - -
- )} - - {/* Sample breakdown */} - {(study.initial_sample_size || study.replication_sample_size) && ( -
- {study.initial_sample_size && ( -

- Initial sample - {study.initial_sample_size} -

- )} - {study.replication_sample_size && ( -

- Replication - {study.replication_sample_size} -

- )} -
- )} - - {/* Quality flags */} - {study.qualityFlags.length > 0 && ( -
- {study.qualityFlags.map((flag, index) => ( -
- {flag.message} -
- ))} -
- )} -
- - {/* Bottom nav */} -
- {navButtons} -
-
-
-
- + {/* Bottom nav */} +
+ +
+
+
+
); } diff --git a/app/subscribe/page.tsx b/app/subscribe/page.tsx index 9d93bad..22cc0f7 100644 --- a/app/subscribe/page.tsx +++ b/app/subscribe/page.tsx @@ -53,8 +53,8 @@ function SubscribeContent() {
Premium -

Subscribe to Monadic DNA Premium

-

$4.99/month

+

Subscribe to Monadic DNA Explorer Premium

+

$4.99/month. Cancel any time.

@@ -62,6 +62,32 @@ function SubscribeContent() {
+
+
+

Included with Premium

+
    +
  • + Research in DNA Chat + Searches your results from 10 angles before synthesizing an answer. More thorough than a standard chat response. +
  • +
  • + All premium reports in Analyze + Healthspan, Top Traits, and Comprehensive Overview reports. See what each report covers. +
  • +
+
+
+

Always free

+
    +
  • Health Insights Report
  • +
  • Send in DNA Chat
  • +
  • Browse and search all studies
  • +
  • Explore your results
  • +
  • Upload your own DNA file
  • +
+
+
+ {paymentCancelled && (
Payment was cancelled. No charge was made. @@ -72,22 +98,21 @@ function SubscribeContent() {

Premium access is active

- Your subscription is ready for DNA Chat and Overview Report. + Research in DNA Chat and all premium reports are unlocked. {subscriptionData?.expiresAt ? ` Your current billing period renews ${new Date(subscriptionData.expiresAt).toLocaleDateString()}.` : ""}

- +
) : !isAuthenticated ? (

Sign in to subscribe

- Sign in with your wallet first so your subscription can be tied - to your Monadic DNA account. + Sign in to continue. We do not store your genetic data, tie your DNA to your identity, or share your information with third parties.