@@ -311,6 +444,27 @@ export default function ExplorePage() {
>
) : (
<>
+ {privateSessionWasCleared && (
+
How it works
diff --git a/app/globals.css b/app/globals.css
index 4f28451..bcc696c 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -1888,6 +1888,23 @@ tbody tr:hover {
opacity: 0.8;
}
+.provider-guide-row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.4rem;
+}
+
+.provider-guide-row a {
+ border: 1px solid rgba(59, 130, 246, 0.18);
+ border-radius: 999px;
+ padding: 0.25rem 0.55rem;
+ background: rgba(59, 130, 246, 0.06);
+ color: var(--accent-blue);
+ font-size: 0.72rem;
+ font-weight: 700;
+ text-decoration: none;
+}
+
.sample-data-section {
display: flex;
flex-direction: column;
@@ -1984,18 +2001,45 @@ tbody tr:hover {
.genotype-status {
display: flex;
- align-items: center;
- gap: 0.5rem;
- padding: 0.5rem 0.75rem;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 0.75rem;
+ padding: 0.75rem;
background: rgba(16, 185, 129, 0.05);
border: 1px solid rgba(16, 185, 129, 0.1);
border-radius: 6px;
}
+.genotype-status-main {
+ display: flex;
+ min-width: 0;
+ flex-direction: column;
+ gap: 0.2rem;
+}
+
.genotype-indicator {
font-size: 0.85rem;
color: var(--accent-green);
- font-weight: 500;
+ font-weight: 800;
+}
+
+.genotype-status-detail,
+.genotype-status-file,
+.genotype-status-privacy {
+ color: var(--text-secondary);
+ font-size: 0.75rem;
+ line-height: 1.35;
+}
+
+.genotype-status-file {
+ overflow: hidden;
+ max-width: 18rem;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.genotype-status-privacy {
+ color: var(--accent-green);
}
.genotype-clear {
@@ -2458,6 +2502,236 @@ tbody tr:hover {
flex-shrink: 0;
}
+.explore-next-steps {
+ display: grid;
+ grid-template-columns: minmax(0, 1.25fr) minmax(280px, 0.75fr);
+ gap: 1.25rem;
+ margin-bottom: 1.5rem;
+ padding: 1.5rem;
+ border: 1px solid rgba(37, 99, 235, 0.22);
+ border-left: 4px solid var(--accent-blue);
+ border-radius: 12px;
+ background:
+ linear-gradient(135deg, rgba(37, 99, 235, 0.08), rgba(16, 185, 129, 0.05)),
+ var(--surface-bg);
+ box-shadow: 0 10px 26px rgba(15, 23, 42, 0.06);
+}
+
+.explore-next-steps-copy {
+ display: flex;
+ flex-direction: column;
+ gap: 0.7rem;
+ min-width: 0;
+}
+
+.explore-next-steps-eyebrow {
+ width: fit-content;
+ border: 1px solid rgba(37, 99, 235, 0.24);
+ border-radius: 999px;
+ padding: 0.25rem 0.6rem;
+ background: rgba(37, 99, 235, 0.08);
+ color: var(--accent-blue);
+ font-size: 0.72rem;
+ font-weight: 800;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+}
+
+.explore-next-steps h2 {
+ margin: 0;
+ color: var(--text-primary);
+ font-size: 1.35rem;
+ line-height: 1.2;
+}
+
+.explore-next-steps p {
+ max-width: 68ch;
+ margin: 0;
+ color: var(--text-secondary);
+ font-size: 0.95rem;
+ line-height: 1.65;
+}
+
+.explore-next-actions {
+ display: flex;
+ flex-direction: column;
+ gap: 0.55rem;
+ align-self: start;
+}
+
+.explore-next-primary,
+.explore-next-secondary {
+ display: inline-flex;
+ min-height: 2.55rem;
+ align-items: center;
+ justify-content: center;
+ border-radius: 8px;
+ padding: 0.65rem 0.9rem;
+ font-size: 0.88rem;
+ font-weight: 800;
+ text-decoration: none;
+ cursor: pointer;
+ transition: transform 0.15s, opacity 0.15s, border-color 0.15s, background 0.15s;
+}
+
+.explore-next-primary {
+ border: 1px solid transparent;
+ background: linear-gradient(135deg, #2563eb, #0f766e);
+ color: #ffffff;
+ box-shadow: 0 8px 20px rgba(37, 99, 235, 0.2);
+}
+
+.explore-next-secondary {
+ border: 1px solid rgba(148, 163, 184, 0.3);
+ background: rgba(255, 255, 255, 0.66);
+ color: var(--text-primary);
+}
+
+.explore-next-primary:hover,
+.explore-next-secondary:hover {
+ transform: translateY(-1px);
+ opacity: 0.9;
+}
+
+.explore-preview-guide {
+ grid-column: 1 / -1;
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 0.75rem;
+ padding-top: 0.25rem;
+}
+
+.explore-preview-guide > * {
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+ padding: 0.8rem;
+ border: 1px solid rgba(148, 163, 184, 0.2);
+ border-radius: 8px;
+ background: rgba(255, 255, 255, 0.48);
+ text-decoration: none;
+}
+
+.explore-preview-guide strong,
+.explore-preview-results-panel h3 {
+ color: var(--text-primary);
+ font-size: 0.82rem;
+}
+
+.explore-preview-guide span {
+ color: var(--text-secondary);
+ font-size: 0.8rem;
+ line-height: 1.45;
+}
+
+.explore-preview-insight-card--link {
+ transition: transform 0.15s, border-color 0.15s, background 0.15s;
+}
+
+.explore-preview-insight-card--link:hover {
+ transform: translateY(-1px);
+ border-color: rgba(37, 99, 235, 0.35);
+ background: rgba(255, 255, 255, 0.72);
+}
+
+.explore-preview-insight-title {
+ color: var(--text-primary) !important;
+ font-weight: 800;
+}
+
+.explore-preview-insight-card small {
+ color: var(--text-muted);
+ font-size: 0.76rem;
+ line-height: 1.35;
+}
+
+.explore-theme-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.35rem;
+ margin-top: 0.1rem;
+}
+
+.explore-theme-list span {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.25rem;
+ width: fit-content;
+ border: 1px solid rgba(37, 99, 235, 0.18);
+ border-radius: 999px;
+ padding: 0.2rem 0.5rem;
+ background: rgba(37, 99, 235, 0.06);
+ color: var(--text-primary);
+ font-weight: 650;
+}
+
+.explore-theme-list b {
+ color: var(--accent-blue);
+}
+
+.explore-preview-results-panel {
+ grid-column: 1 / -1;
+ display: grid;
+ gap: 0.7rem;
+ padding-top: 0.25rem;
+}
+
+.explore-preview-results-panel h3 {
+ margin: 0;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+.explore-preview-result-list {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 0.5rem;
+}
+
+:root[data-theme="dark"] .explore-next-secondary,
+:root[data-theme="dark"] .explore-preview-guide > * {
+ background: rgba(15, 23, 42, 0.58);
+}
+
+.explore-session-recovery {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) minmax(220px, 0.35fr);
+ gap: 1rem;
+ margin-bottom: 1.5rem;
+ padding: 1.2rem;
+ border: 1px solid rgba(245, 158, 11, 0.3);
+ border-left: 4px solid #f59e0b;
+ border-radius: 10px;
+ background: rgba(245, 158, 11, 0.07);
+}
+
+.explore-session-recovery h2 {
+ margin: 0.6rem 0 0.4rem;
+ color: var(--text-primary);
+ font-size: 1.2rem;
+ line-height: 1.25;
+}
+
+.explore-session-recovery p {
+ max-width: 68ch;
+ margin: 0;
+ color: var(--text-secondary);
+ font-size: 0.92rem;
+ line-height: 1.55;
+}
+
+.explore-session-recovery-actions {
+ display: flex;
+ flex-direction: column;
+ gap: 0.55rem;
+ align-self: center;
+}
+
+.explore-session-recovery-error {
+ margin-top: 0.6rem !important;
+ color: var(--error) !important;
+}
+
/* Discovery card */
.explore-discovery-card {
background: linear-gradient(135deg, rgba(102,126,234,0.08) 0%, rgba(118,75,162,0.08) 100%);
@@ -3656,8 +3930,12 @@ details[open] .summary-arrow {
.page-nav .nav-link {
flex: 1;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
text-align: center;
flex-shrink: 1;
+ white-space: nowrap;
}
.page-nav::-webkit-scrollbar {
@@ -3742,6 +4020,24 @@ details[open] .summary-arrow {
white-space: normal;
}
+ .explore-next-steps {
+ grid-template-columns: 1fr;
+ padding: 1.1rem;
+ }
+
+ .explore-session-recovery {
+ grid-template-columns: 1fr;
+ }
+
+ .explore-next-actions {
+ width: 100%;
+ }
+
+ .explore-preview-guide,
+ .explore-preview-result-list {
+ grid-template-columns: 1fr;
+ }
+
.status-section {
flex-direction: column;
gap: 0.5rem;
@@ -9617,6 +9913,7 @@ details[open] .summary-arrow {
flex-direction: column;
justify-content: center;
gap: 1.6rem;
+ min-width: 0;
}
.landing-home-copy h1 {
@@ -9628,6 +9925,35 @@ details[open] .summary-arrow {
color: var(--text-primary);
}
+.landing-home-subtitle {
+ max-width: 62ch;
+ margin: 0;
+ color: var(--text-secondary);
+ font-size: 1.08rem;
+ line-height: 1.7;
+ overflow-wrap: anywhere;
+}
+
+.landing-home-proof {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.55rem;
+}
+
+.landing-home-proof span {
+ display: inline-flex;
+ align-items: center;
+ max-width: 100%;
+ min-height: 2rem;
+ padding: 0.35rem 0.7rem;
+ border: 1px solid rgba(37, 99, 235, 0.2);
+ border-radius: 999px;
+ background: rgba(37, 99, 235, 0.06);
+ color: var(--text-primary);
+ font-size: 0.82rem;
+ font-weight: 700;
+}
+
.landing-home-explainer {
display: grid;
gap: 0;
@@ -9669,6 +9995,7 @@ details[open] .summary-arrow {
box-shadow: 0 10px 26px rgba(15, 23, 42, 0.08);
position: relative;
overflow: hidden;
+ min-width: 0;
}
.landing-home-start-panel::before {
@@ -9688,6 +10015,12 @@ details[open] .summary-arrow {
letter-spacing: 0;
}
+.landing-home-start-panel .landing-primary-button,
+.landing-home-start-panel .landing-secondary-button {
+ width: 100%;
+ min-height: 2.75rem;
+}
+
.landing-start-actions {
display: flex;
flex-direction: column;
@@ -9720,6 +10053,82 @@ details[open] .summary-arrow {
transform: translateY(-1px);
}
+.landing-start-note,
+.landing-start-error,
+.landing-start-help {
+ margin: 0;
+ color: var(--text-secondary);
+ font-size: 0.84rem;
+ line-height: 1.55;
+}
+
+.landing-start-error {
+ color: var(--error);
+}
+
+.landing-start-help a {
+ color: var(--accent-blue);
+ font-weight: 700;
+ text-decoration: none;
+}
+
+.landing-upload-success,
+.landing-preview-results {
+ display: grid;
+ gap: 0.25rem;
+ margin: 0;
+ padding: 0.75rem;
+ border: 1px solid rgba(16, 185, 129, 0.22);
+ border-radius: 8px;
+ background: rgba(16, 185, 129, 0.06);
+ color: var(--text-secondary);
+ font-size: 0.84rem;
+ line-height: 1.45;
+}
+
+.landing-upload-success strong,
+.landing-preview-results strong {
+ color: var(--text-primary);
+ font-size: 0.9rem;
+}
+
+.landing-preview-results {
+ border-color: rgba(59, 130, 246, 0.22);
+ background: rgba(59, 130, 246, 0.06);
+}
+
+.landing-preview-results span {
+ color: var(--text-primary);
+}
+
+.landing-home-steps {
+ display: grid;
+ gap: 0.55rem;
+ margin: 0;
+ padding: 0.75rem 0 0 1.35rem;
+ border-top: 1px solid var(--border-color);
+ color: var(--text-primary);
+ font-size: 0.9rem;
+ line-height: 1.45;
+}
+
+.landing-provider-guides {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.4rem;
+}
+
+.landing-provider-guides a {
+ border: 1px solid rgba(59, 130, 246, 0.18);
+ border-radius: 999px;
+ padding: 0.25rem 0.55rem;
+ background: rgba(59, 130, 246, 0.06);
+ color: var(--accent-blue);
+ font-size: 0.72rem;
+ font-weight: 800;
+ text-decoration: none;
+}
+
:root[data-theme="dark"] .landing-home-intro {
background: rgba(17, 24, 39, 0.54);
}
@@ -9754,17 +10163,36 @@ details[open] .summary-arrow {
}
.landing-home-intro {
+ gap: 1.1rem;
padding: 1.25rem;
}
+ .landing-home-copy {
+ display: contents;
+ }
+
.landing-home-start-panel {
border-radius: 8px;
padding: 1.1rem;
+ order: 3;
}
.landing-home-copy h1 {
max-width: none;
- font-size: clamp(2.2rem, 11vw, 3.2rem);
+ font-size: clamp(2rem, 10vw, 2.85rem);
+ order: 1;
+ }
+
+ .landing-home-subtitle {
+ order: 2;
+ }
+
+ .landing-home-proof {
+ order: 4;
+ }
+
+ .landing-home-explainer {
+ order: 5;
}
.landing-home-explainer p {
@@ -11479,6 +11907,44 @@ details[open] .summary-arrow {
margin: 0 0 1.25rem;
}
+.srb-evidence {
+ display: grid;
+ gap: 0.6rem;
+ margin-top: 0.5rem;
+ padding: 0.85rem;
+ border: 1px solid rgba(59, 130, 246, 0.18);
+ border-radius: 8px;
+ background: rgba(59, 130, 246, 0.05);
+}
+
+.srb-evidence strong {
+ color: var(--text-primary);
+ font-size: 0.88rem;
+}
+
+.srb-evidence-items {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.4rem;
+}
+
+.srb-evidence-items span {
+ display: inline-flex;
+ align-items: center;
+ min-height: 1.65rem;
+ border: 1px solid rgba(148, 163, 184, 0.28);
+ border-radius: 999px;
+ padding: 0.25rem 0.55rem;
+ background: rgba(255, 255, 255, 0.5);
+ color: var(--text-secondary);
+ font-size: 0.74rem;
+ font-weight: 700;
+}
+
+:root[data-theme="dark"] .srb-evidence-items span {
+ background: rgba(15, 23, 42, 0.58);
+}
+
.srb-no-match {
font-size: 0.9rem;
color: var(--text-secondary);
@@ -12247,3 +12713,92 @@ a.heatmap-chip:hover {
text-align: center;
}
+/* Raw DNA guide */
+.guide-page {
+ max-width: 980px;
+ overflow-x: hidden;
+}
+
+.guide-hero {
+ display: grid;
+ gap: 1rem;
+ min-width: 0;
+ padding: 2rem;
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ background: var(--surface-bg);
+}
+
+.guide-eyebrow {
+ color: var(--accent-blue);
+ font-size: 0.78rem;
+ font-weight: 800;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+}
+
+.guide-hero h1 {
+ margin: 0;
+ color: var(--text-primary);
+ font-size: clamp(2.1rem, 5vw, 3.7rem);
+ line-height: 1;
+ letter-spacing: 0;
+ overflow-wrap: anywhere;
+}
+
+.guide-hero p {
+ max-width: 68ch;
+ margin: 0;
+ color: var(--text-secondary);
+ font-size: 1.05rem;
+ line-height: 1.7;
+ overflow-wrap: anywhere;
+}
+
+.guide-hero .landing-primary-button {
+ width: fit-content;
+ max-width: 100%;
+ white-space: normal;
+ text-align: center;
+}
+
+.guide-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 1rem;
+}
+
+.guide-grid article,
+.guide-provider-section {
+ padding: 1.25rem;
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ background: var(--surface-bg);
+}
+
+.guide-grid h2,
+.guide-provider-section h2 {
+ margin: 0 0 0.6rem;
+ color: var(--text-primary);
+ font-size: 1.1rem;
+}
+
+.guide-grid p {
+ margin: 0;
+ color: var(--text-secondary);
+ line-height: 1.65;
+}
+
+@media (max-width: 768px) {
+ .guide-hero {
+ padding: 1.25rem;
+ }
+
+ .guide-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .guide-hero .landing-primary-button {
+ width: 100%;
+ }
+}
diff --git a/app/landing-client.tsx b/app/landing-client.tsx
index 3249193..cbfe39b 100644
--- a/app/landing-client.tsx
+++ b/app/landing-client.tsx
@@ -7,7 +7,19 @@ import { useGenotype } from "./components/UserDataUpload";
import { useResults } from "./components/ResultsContext";
import { useCustomization, type UserCustomization } from "./components/CustomizationContext";
import { ResultsManager } from "@/lib/results-manager";
-import { trackGetStartedClicked, trackSampleDataStarted, trackSampleDataLoaded, trackSampleDataFailed } from "@/lib/analytics";
+import { runAllAnalysisOnboarding, type OnboardingRunAllProgress } from "@/lib/run-all-onboarding";
+import { selectAhaPreviewResults } from "@/lib/preview-insights";
+import {
+ trackFirstResultViewed,
+ trackGetStartedClicked,
+ trackProviderGuideClicked,
+ trackQuickPreviewCompleted,
+ trackQuickPreviewFailed,
+ trackQuickPreviewStarted,
+ trackSampleDataStarted,
+ trackSampleDataLoaded,
+ trackSampleDataFailed,
+} from "@/lib/analytics";
const SAMPLE_RESULTS_FILE_NAME = "monadic_dna_explorer_results_2026-05-19.tsv";
const SAMPLE_CUSTOMIZATION_PASSWORD = "sample-data";
@@ -26,6 +38,7 @@ const SAMPLE_CUSTOMIZATION: UserCustomization = {
};
type SampleLoadStatus = "idle" | "downloading" | "loading" | "loaded" | "error";
+type PreviewStatus = "idle" | "running" | "complete" | "error";
function formatBytes(bytes: number): string {
if (!bytes) return "0 KB";
@@ -68,13 +81,62 @@ const featureCopy = [
export default function LandingClient() {
const router = useRouter();
- const { error } = useGenotype();
- const { addResultsBatch, clearResults, savedResults } = useResults();
+ const { error, isUploaded, genotypeData, originalFileName, originalFileSize, detectedFormat } = useGenotype();
+ const { addResultsBatch, clearResults, savedResults, hasResult } = useResults();
const { saveCustomization, status: customizationStatus } = useCustomization();
const [sampleStatus, setSampleStatus] = useState
("idle");
const [sampleError, setSampleError] = useState(null);
const [sampleBytes, setSampleBytes] = useState(0);
const [sampleTotalBytes, setSampleTotalBytes] = useState(0);
+ const [previewStatus, setPreviewStatus] = useState("idle");
+ const [previewError, setPreviewError] = useState(null);
+ const [previewProgress, setPreviewProgress] = useState(null);
+ const [previewTraitNames, setPreviewTraitNames] = useState([]);
+
+ const openDNAUpload = () => {
+ trackGetStartedClicked("home_upload_raw_dna");
+ window.dispatchEvent(new CustomEvent("openDNAUpload", { detail: { source: "home_upload_raw_dna" } }));
+ };
+
+ const startFullAnalysis = () => {
+ trackGetStartedClicked("home_analyze_uploaded_dna");
+ window.dispatchEvent(new CustomEvent("startRunAllAnalysis"));
+ };
+
+ const runQuickPreview = async () => {
+ if (!genotypeData || previewStatus === "running") return;
+
+ trackQuickPreviewStarted("home");
+ setPreviewStatus("running");
+ setPreviewError(null);
+ setPreviewTraitNames([]);
+
+ try {
+ const results = await runAllAnalysisOnboarding(
+ genotypeData,
+ (progress) => setPreviewProgress(progress),
+ hasResult,
+ { maxResults: 1000 }
+ );
+
+ if (!results.length) {
+ throw new Error("No preview matches were found in the quick scan. You can still run the full catalog analysis.");
+ }
+
+ const curatedResults = selectAhaPreviewResults(results, 12);
+ await addResultsBatch(curatedResults);
+ const traitNames = curatedResults.slice(0, 3).map((result) => result.traitName);
+ setPreviewTraitNames(traitNames);
+ setPreviewStatus("complete");
+ trackQuickPreviewCompleted(curatedResults.length, "home");
+ trackFirstResultViewed("home_preview");
+ } catch (err) {
+ const message = err instanceof Error ? err.message : "The quick preview could not complete.";
+ setPreviewStatus("error");
+ setPreviewError(message);
+ trackQuickPreviewFailed(message, "home");
+ }
+ };
const loadSampleData = async () => {
if (savedResults.length > 0) {
@@ -156,50 +218,41 @@ export default function LandingClient() {
? "Parsing results…"
: null;
+ const formatFileSize = (bytes: number | null) => {
+ if (!bytes) return null;
+ if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
+ };
+
+ const previewProgressText =
+ previewStatus === "running" && previewProgress
+ ? previewProgress.phase === "downloading"
+ ? "Preparing preview catalog..."
+ : previewProgress.phase === "analyzing"
+ ? `Scanning studies, ${previewProgress.matchCount} candidate matches found`
+ : previewProgress.message
+ : null;
+
return (
-
+
-
Understand your DNA without giving it away.
+
Analyze your raw DNA file privately.
-
- Upload your DNA file or try it now with sample data, free.
+
+ Upload a 23andMe, AncestryDNA, MyHeritage, FTDNA, LivingDNA, CSV, or TSV file. Your raw DNA file stays in your browser while the app matches variants against GWAS Catalog research.
{error &&
{error}
}
-
-
- {sampleProgressText && (
-
- {sampleProgressText}
-
- )}
- {sampleError && (
-
{sampleError}
- )}
-
- No DNA file needed. Your data never leaves your device.{' '}
- trackGetStartedClicked("schedule_video_call")}
- >
- Need help? Book a free call.
-
-
+
+ Local file processing
+ No DNA account required
+ GWAS evidence, effect sizes, and p-values
+ Educational use, not diagnosis
-
+
{featureCopy.map((item) => (
@@ -222,6 +275,128 @@ export default function LandingClient() {
))}
+
+
+
Start with your raw DNA file
+
+
+ {isUploaded ? (
+
+ ) : (
+
+ )}
+
+
+
+ {isUploaded && savedResults.length === 0 && (
+
+ )}
+
+ {sampleProgressText && (
+
{sampleProgressText}
+ )}
+ {previewProgressText && (
+
{previewProgressText}
+ )}
+ {sampleError && (
+
{sampleError}
+ )}
+ {previewError && (
+
{previewError}
+ )}
+
+
+ {isUploaded && genotypeData ? (
+
+ File parsed successfully
+ {genotypeData.size.toLocaleString()} variants loaded{detectedFormat ? ` from ${detectedFormat}` : ""}.
+ {originalFileName && {originalFileName}{formatFileSize(originalFileSize) ? `, ${formatFileSize(originalFileSize)}` : ""}}
+ Your raw DNA file stayed in this browser.
+
+ ) : (
+
+ Nothing is uploaded to us. The file picker opens locally, and analysis runs in this browser.
+
+ )}
+
+ {previewTraitNames.length > 0 && (
+
+ Preview results ready
+ {previewTraitNames.map((traitName) => (
+ {traitName}
+ ))}
+
+ )}
+
+
+ - Choose your raw DNA file.
+ - Start with a quick local preview.
+ - Explore results and ask DNA Chat questions.
+
+
+
+ {[
+ ["23andMe", "23andme"],
+ ["AncestryDNA", "ancestry"],
+ ["MyHeritage", "myheritage"],
+ ["FTDNA", "ftdna"],
+ ["LivingDNA", "livingdna"],
+ ].map(([label, provider]) => (
+
trackProviderGuideClicked(provider, "home")}
+ >
+ {label}
+
+ ))}
+
+
+
+ Need help getting your raw DNA file?{" "}
+ trackGetStartedClicked("schedule_video_call")}
+ >
+ Book a free call
+
+ {" "}or use a provider guide above.{" "}
+
+ Share the raw DNA guide
+ .
+
+
);
diff --git a/app/overview-report/page.tsx b/app/overview-report/page.tsx
index d11548e..337650a 100644
--- a/app/overview-report/page.tsx
+++ b/app/overview-report/page.tsx
@@ -40,8 +40,11 @@ export default function OverviewReportPage() {
}, []);
useEffect(() => {
- trackOverviewReportViewed();
- }, []);
+ trackOverviewReportViewed({
+ resultCount: savedResults.length,
+ hasResults: savedResults.length > 0,
+ });
+ }, [savedResults.length]);
const hasPremiumAccess = hasActiveSubscription || hasPromoAccess;
diff --git a/app/page.tsx b/app/page.tsx
index bd088dd..c364bc5 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -5,12 +5,12 @@ import Footer from "./components/Footer";
import LandingClient from "./landing-client";
export const metadata: Metadata = {
- title: "Monadic DNA | Personal DNA insights with privacy, autonomy, and boundless curiosity",
- description: "Private DNA insights from trusted genetic research. Learn from your DNA while your data remains private, protected, and entirely in your hands.",
- keywords: ["GWAS", "genetics", "DNA analysis", "genome explorer", "genetic traits", "personal genomics", "23andMe", "AncestryDNA"],
+ title: "Private Raw DNA Analysis | Monadic DNA Explorer",
+ description: "Upload a raw DNA file from 23andMe, AncestryDNA, MyHeritage, FTDNA, LivingDNA, CSV, or TSV. Analyze genetic traits privately in your browser using GWAS Catalog research.",
+ keywords: ["raw DNA upload", "private DNA analysis", "secure DNA analysis", "GWAS", "genetics", "DNA analysis", "genome explorer", "genetic traits", "personal genomics", "23andMe", "AncestryDNA"],
openGraph: {
- title: "Monadic DNA | Personal DNA insights with privacy, autonomy, and boundless curiosity",
- description: "Private DNA insights from trusted genetic research. Learn from your DNA while your data remains private, protected, and entirely in your hands.",
+ title: "Private Raw DNA Analysis | Monadic DNA Explorer",
+ description: "Upload a raw DNA file and analyze genetic traits privately in your browser using GWAS Catalog research.",
type: "website",
url: "https://explorer.monadicdna.com",
siteName: "Monadic DNA Explorer",
@@ -26,8 +26,8 @@ export const metadata: Metadata = {
},
twitter: {
card: "summary_large_image",
- title: "Monadic DNA | Personal DNA insights with privacy, autonomy, and boundless curiosity",
- description: "Private DNA insights from trusted genetic research. Learn from your DNA while your data remains private, protected, and entirely in your hands.",
+ title: "Private Raw DNA Analysis | Monadic DNA Explorer",
+ description: "Upload a raw DNA file and analyze genetic traits privately in your browser using GWAS Catalog research.",
images: ["/og-image.png"],
},
};
@@ -46,7 +46,7 @@ const websiteJsonLd = {
"@type": "WebSite",
"name": "Monadic DNA Explorer",
"url": "https://explorer.monadicdna.com",
- "description": "Private DNA insights from trusted genetic research.",
+ "description": "Private raw DNA analysis from trusted genetic research.",
"potentialAction": {
"@type": "SearchAction",
"target": {
diff --git a/app/raw-dna-guide/page.tsx b/app/raw-dna-guide/page.tsx
new file mode 100644
index 0000000..c8c982d
--- /dev/null
+++ b/app/raw-dna-guide/page.tsx
@@ -0,0 +1,77 @@
+import type { Metadata } from "next";
+import Link from "next/link";
+import MenuBar from "../components/MenuBar";
+import Footer from "../components/Footer";
+
+export const metadata: Metadata = {
+ title: "What Raw DNA Analysis Can Tell You | Monadic DNA Explorer",
+ description: "A privacy-first guide to raw DNA files, GWAS associations, scientific caveats, and browser-local genetic analysis.",
+ keywords: ["raw DNA analysis", "private DNA analysis", "GWAS guide", "23andMe raw data", "AncestryDNA raw data"],
+};
+
+export default function RawDNAGuidePage() {
+ return (
+
+
+
+
+ Privacy-safe guide
+ What raw DNA analysis can tell you
+
+ Raw DNA files from consumer tests contain genotype calls at many known variants.
+ Monadic DNA Explorer compares those variants with GWAS Catalog studies in your browser.
+
+
+ Analyze a raw DNA file
+
+
+
+
+
+ What you can learn
+
+ GWAS findings can show associations between variants and traits such as metabolism,
+ sleep, physical traits, disease susceptibility, and medication response research.
+
+
+
+
+ How to read the science
+
+ Each finding should be read with its study, p-value, effect size, sample context,
+ population context, and replication status. Most effects are probabilistic and small.
+
+
+
+
+ Privacy model
+
+ The raw DNA file is parsed locally. Results are held in browser memory unless you
+ choose to export them. The app should never need a DNA account to analyze your file.
+
+
+
+
+ Limits
+
+ GWAS associations are educational research signals. They are not medical diagnosis,
+ ancestry assignment, or a complete picture of health risk.
+
+
+
+
+
+ Download your raw file
+
+
+
+
+
+ );
+}
diff --git a/app/study/[id]/page.tsx b/app/study/[id]/page.tsx
index 637600f..0317ba5 100644
--- a/app/study/[id]/page.tsx
+++ b/app/study/[id]/page.tsx
@@ -136,6 +136,10 @@ export default async function StudyDetailPage({ params }: { params: Promise<{ id
pubmedId={study.pubmedid}
mappedGene={study.mapped_gene}
reportedTrait={study.disease_trait}
+ pValue={study.p_value}
+ pValueMlog={study.pvalue_mlog}
+ initialSampleSize={study.initial_sample_size}
+ replicationSampleSize={study.replication_sample_size}
/>
{/* Study Details */}
diff --git a/lib/analytics.ts b/lib/analytics.ts
index d6a7ed8..50cd914 100644
--- a/lib/analytics.ts
+++ b/lib/analytics.ts
@@ -25,7 +25,7 @@ declare global {
function trackEvent(eventName: string, params?: Record
) {
if (typeof window !== 'undefined' && window.gtag) {
try {
- window.gtag('event', eventName, params);
+ window.gtag('event', eventName, sanitizeAnalyticsParams(params));
} catch (error) {
console.warn('Analytics tracking failed:', error);
}
@@ -128,6 +128,20 @@ function sanitizeErrorReason(reason?: string): string | undefined {
return reason.slice(0, 120);
}
+const SENSITIVE_ANALYTICS_KEYS = new Set();
+
+function sanitizeAnalyticsParams(params?: Record): Record | undefined {
+ if (!params) return undefined;
+
+ const safeParams = Object.fromEntries(
+ Object.entries(params).filter(([key, value]) =>
+ value !== undefined && !SENSITIVE_ANALYTICS_KEYS.has(key)
+ )
+ );
+
+ return Object.keys(safeParams).length > 0 ? safeParams : undefined;
+}
+
/**
* User accepted terms and moved past initial modal
*/
@@ -190,7 +204,7 @@ export function trackOnboardingDismissed(step: string) {
/**
* User clicked one of the homepage Get Started actions
*/
-export function trackGetStartedClicked(action: 'onboarding_tour' | 'welcome_options' | 'try_dna_chat_directly' | 'instructional_video' | 'schedule_video_call' | 'restart_onboarding') {
+export function trackGetStartedClicked(action: 'onboarding_tour' | 'welcome_options' | 'try_dna_chat_directly' | 'instructional_video' | 'schedule_video_call' | 'restart_onboarding' | 'home_upload_raw_dna' | 'home_analyze_uploaded_dna') {
trackEvent('get_started_clicked', {
action,
});
@@ -327,11 +341,32 @@ export function trackGenotypeFileUploadStarted(source: string = 'unknown') {
});
}
+export function trackUploadPickerOpened(source: string = 'unknown') {
+ trackEvent('upload_picker_opened', {
+ source,
+ });
+}
+
+export function trackGenotypeParseStarted(source: string = 'unknown', fileExtension?: string) {
+ trackEvent('genotype_parse_started', {
+ source,
+ file_extension: fileExtension,
+ });
+}
+
+export function trackGenotypeParseSucceeded(source: string = 'unknown', detectedFormat?: string, variantCount?: number) {
+ trackEvent('genotype_parse_succeeded', {
+ source,
+ detected_format: detectedFormat,
+ variant_count: variantCount,
+ });
+}
+
export function trackGenotypeFileUploadFailed(source: string = 'unknown', reason?: string, fileExtension?: string) {
trackEvent('genotype_file_upload_failed', {
source,
reason: sanitizeErrorReason(reason),
- ...(fileExtension && { file_extension: fileExtension }),
+ file_extension: fileExtension,
});
}
@@ -340,13 +375,12 @@ export function trackGenotypeFileLoaded(fileSize: number, variantCount: number,
file_size_kb: Math.round(fileSize / 1024),
variant_count: variantCount,
source,
- ...(detectedFormat && { detected_format: detectedFormat }),
- ...(fileExtension && { file_extension: fileExtension }),
+ detected_format: detectedFormat,
+ file_extension: fileExtension,
};
trackEvent('genotype_file_loaded', metadata);
- // Track as Lead event on Reddit (DNA upload is a lead generation action)
const conversionId = `dna_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
trackRedditEvent('Lead', {
conversionId,
@@ -355,8 +389,7 @@ export function trackGenotypeFileLoaded(fileSize: number, variantCount: number,
conversion_id: conversionId,
});
- // Track as Lead event on X
- trackXEvent('tw-r9lkr-rbtjs', { // Custom event - DNA File Upload
+ trackXEvent('tw-r9lkr-rbtjs', {
conversion_id: conversionId,
});
trackXConversion('Lead', {
@@ -459,9 +492,45 @@ export function trackRunAllFailed(source: 'menu' | 'explore' | 'onboarding', rea
});
}
-export function trackLLMQuestionAsked(params?: { isFollowUp?: boolean }) {
+export function trackQuickPreviewStarted(source: 'home' | 'onboarding' = 'home') {
+ trackEvent('quick_preview_started', {
+ source,
+ });
+}
+
+export function trackQuickPreviewCompleted(resultCount: number, source: 'home' | 'onboarding' = 'home') {
+ trackEvent('quick_preview_completed', {
+ source,
+ result_count: resultCount,
+ });
+}
+
+export function trackQuickPreviewFailed(reason?: string, source: 'home' | 'onboarding' = 'home') {
+ trackEvent('quick_preview_failed', {
+ source,
+ reason: sanitizeErrorReason(reason),
+ });
+}
+
+export function trackFirstResultViewed(source: 'home_preview' | 'study' | 'results' = 'results') {
+ trackEvent('first_result_viewed', {
+ source,
+ });
+}
+
+export function trackProviderGuideClicked(provider: string, source: 'home' | 'upload' | 'study' = 'home') {
+ trackEvent('provider_guide_clicked', {
+ provider,
+ source,
+ });
+}
+
+export function trackLLMQuestionAsked(params?: { isFollowUp?: boolean; hasUploadedDna?: boolean; resultCount?: number; source?: 'chat' | 'research' | 'onboarding' }) {
trackEvent('llm_question_asked', {
is_follow_up: params?.isFollowUp ?? false,
+ has_uploaded_dna: params?.hasUploadedDna,
+ result_count: params?.resultCount,
+ source: params?.source,
});
}
@@ -489,8 +558,11 @@ export function trackAIConsentModalShown() {
trackEvent('ai_consent_modal_shown');
}
-export function trackOverviewReportViewed() {
- trackEvent('overview_report_viewed');
+export function trackOverviewReportViewed(params?: { resultCount?: number; hasResults?: boolean }) {
+ trackEvent('overview_report_viewed', {
+ result_count: params?.resultCount,
+ has_results: params?.hasResults,
+ });
}
/**
@@ -533,6 +605,12 @@ export function trackExplorePageViewed() {
trackEvent('explore_page_viewed');
}
+export function trackExploreFollowupClicked(action: 'run_full_analysis' | 'ask_dna_chat' | 'preview_best_question' | 'personalize' | 'browse') {
+ trackEvent('explore_followup_clicked', {
+ action,
+ });
+}
+
export function trackContinueInDNAChat(source: 'study_analysis') {
trackEvent('continue_in_dna_chat', { source });
}
diff --git a/lib/evidence-labels.ts b/lib/evidence-labels.ts
new file mode 100644
index 0000000..0f51691
--- /dev/null
+++ b/lib/evidence-labels.ts
@@ -0,0 +1,76 @@
+import type { SavedResult } from "./results-manager";
+
+function parsePValue(result: Pick): number | null {
+ if (result.pValue) {
+ const normalized = result.pValue.replace(/\s/g, "").replace(/x10\^/i, "e");
+ const value = Number.parseFloat(normalized);
+ if (Number.isFinite(value) && value > 0) return value;
+ }
+
+ if (result.pValueMlog) {
+ const mlog = Number.parseFloat(result.pValueMlog);
+ if (Number.isFinite(mlog) && mlog > 0) return Math.pow(10, -mlog);
+ }
+
+ return null;
+}
+
+function parseLargestNumber(value?: string): number | null {
+ if (!value) return null;
+ const matches = value.match(/\d[\d,]*/g);
+ if (!matches?.length) return null;
+ const values = matches
+ .map((match) => Number.parseInt(match.replace(/,/g, ""), 10))
+ .filter((number) => Number.isFinite(number));
+ if (!values.length) return null;
+ return Math.max(...values);
+}
+
+export function getEvidenceBand(result: SavedResult): "strong" | "moderate" | "limited" {
+ const pValue = parsePValue(result);
+ const sampleSize = parseLargestNumber(result.sampleSize);
+ const hasReplication = !!result.replicationSampleSize?.trim();
+
+ if ((pValue !== null && pValue <= 5e-8) && (hasReplication || (sampleSize !== null && sampleSize >= 10000))) {
+ return "strong";
+ }
+
+ if ((pValue !== null && pValue <= 5e-8) || (sampleSize !== null && sampleSize >= 10000)) {
+ return "moderate";
+ }
+
+ return "limited";
+}
+
+export function getEvidenceLabel(result: SavedResult): string {
+ const band = getEvidenceBand(result);
+ if (band === "strong") return "Stronger GWAS evidence";
+ if (band === "moderate") return "Moderate GWAS evidence";
+ return "Limited context available";
+}
+
+export function getEvidenceDetails(result: SavedResult): string[] {
+ const details: string[] = [];
+ const pValue = parsePValue(result);
+
+ if (pValue !== null) {
+ details.push(pValue <= 5e-8 ? "Genome-wide significant p-value" : "Association p-value reported");
+ }
+
+ if (result.sampleSize) {
+ details.push(`Initial sample: ${result.sampleSize}`);
+ }
+
+ if (result.replicationSampleSize) {
+ details.push(`Replication sample: ${result.replicationSampleSize}`);
+ }
+
+ if (result.effectType === "beta") {
+ details.push("Beta effect, not an odds ratio");
+ } else {
+ details.push("Odds ratio is relative, not absolute risk");
+ }
+
+ details.push("Educational interpretation, not diagnosis");
+ return details;
+}
diff --git a/lib/preview-insights.ts b/lib/preview-insights.ts
new file mode 100644
index 0000000..f06602d
--- /dev/null
+++ b/lib/preview-insights.ts
@@ -0,0 +1,247 @@
+import type { SavedResult } from "./results-manager";
+
+type PreviewCategory =
+ | "body_composition"
+ | "cardiometabolic"
+ | "immune"
+ | "brain_behavior"
+ | "sleep_energy"
+ | "reproductive"
+ | "appearance"
+ | "medication"
+ | "other";
+
+type CategoryDefinition = {
+ id: PreviewCategory;
+ label: string;
+ keywords: string[];
+};
+
+export type PreviewInsight = {
+ themes: { label: string; count: number }[];
+ standout: SavedResult | null;
+ protectiveCount: number;
+ elevatedCount: number;
+ headline: string;
+};
+
+const CATEGORY_DEFINITIONS: CategoryDefinition[] = [
+ {
+ id: "body_composition",
+ label: "body composition",
+ keywords: ["body mass", "bmi", "weight", "obesity", "height", "adiposity", "waist", "lean mass", "fat mass"],
+ },
+ {
+ id: "cardiometabolic",
+ label: "cardiometabolic traits",
+ keywords: ["cholesterol", "lipid", "triglyceride", "glucose", "insulin", "diabetes", "blood pressure", "coronary", "heart"],
+ },
+ {
+ id: "immune",
+ label: "immune and inflammation",
+ keywords: ["immune", "inflammatory", "asthma", "allergy", "eczema", "psoriasis", "arthritis", "celiac", "crohn", "colitis"],
+ },
+ {
+ id: "brain_behavior",
+ label: "brain and behavior",
+ keywords: ["cognitive", "intelligence", "education", "neuroticism", "depression", "anxiety", "adhd", "risk taking", "alcohol", "smoking"],
+ },
+ {
+ id: "sleep_energy",
+ label: "sleep and energy",
+ keywords: ["sleep", "chronotype", "morning", "insomnia", "fatigue", "caffeine", "restless"],
+ },
+ {
+ id: "reproductive",
+ label: "hormonal and reproductive traits",
+ keywords: ["endometriosis", "menopause", "menarche", "testosterone", "estrogen", "fertility", "reproductive", "puberty"],
+ },
+ {
+ id: "appearance",
+ label: "visible traits",
+ keywords: ["hair", "skin", "eye color", "freckle", "baldness", "pigmentation", "facial", "tooth"],
+ },
+ {
+ id: "medication",
+ label: "medication response",
+ keywords: ["drug", "medication", "statin", "warfarin", "metformin", "response", "adverse", "pharmacogen"],
+ },
+];
+
+const GENERIC_TRAIT_WORDS = new Set([
+ "stage",
+ "type",
+ "disease",
+ "disorder",
+ "condition",
+ "measurement",
+ "self",
+ "reported",
+ "adjusted",
+ "male",
+ "female",
+ "men",
+ "women",
+]);
+
+function normalizeTraitName(traitName: string): string {
+ return traitName
+ .toLowerCase()
+ .replace(/\([^)]*\)/g, " ")
+ .replace(/[^a-z0-9]+/g, " ")
+ .split(/\s+/)
+ .filter((word) => word.length > 2 && !GENERIC_TRAIT_WORDS.has(word))
+ .join(" ")
+ .trim();
+}
+
+export function getPreviewCategory(result: SavedResult): { id: PreviewCategory; label: string } {
+ const text = `${result.traitName} ${result.studyTitle} ${result.mappedGene || ""}`.toLowerCase();
+ const match = CATEGORY_DEFINITIONS.find((category) =>
+ category.keywords.some((keyword) => text.includes(keyword))
+ );
+
+ if (!match) return { id: "other", label: "other traits" };
+ return { id: match.id, label: match.label };
+}
+
+export function formatPreviewEffect(result: SavedResult, compact = false): string {
+ if (result.effectType === "beta") {
+ return `β=${result.riskScore >= 0 ? "+" : ""}${result.riskScore.toFixed(3)}`;
+ }
+
+ if (result.riskLevel === "neutral") return "baseline";
+ if (compact) return `${result.riskScore.toFixed(2)}x ${result.riskLevel === "increased" ? "↑" : "↓"}`;
+ return `${result.riskScore.toFixed(2)}x ${result.riskLevel === "increased" ? "higher relative odds" : "lower relative odds"}`;
+}
+
+function parseNumeric(value?: string): number | null {
+ if (!value) return null;
+ const parsed = Number.parseFloat(value.replace(/,/g, ""));
+ return Number.isFinite(parsed) ? parsed : null;
+}
+
+function effectMagnitude(result: SavedResult): number {
+ if (result.effectType === "beta") return Math.min(Math.abs(result.riskScore) * 4, 2.5);
+ if (result.riskScore <= 0 || result.riskScore > 50) return 0;
+ return Math.min(Math.abs(Math.log(result.riskScore)), 2.5);
+}
+
+function evidenceScore(result: SavedResult): number {
+ const mlog = parseNumeric(result.pValueMlog);
+ const sampleText = `${result.sampleSize || ""} ${result.replicationSampleSize || ""}`;
+ const sampleNumbers = sampleText.match(/\d[\d,]*/g)?.map((value) => Number.parseInt(value.replace(/,/g, ""), 10)) || [];
+ const largestSample = sampleNumbers.length ? Math.max(...sampleNumbers) : 0;
+
+ let score = 0;
+ if (mlog !== null) score += Math.min(mlog / 10, 1.5);
+ if (largestSample >= 100000) score += 1;
+ else if (largestSample >= 10000) score += 0.6;
+ if (result.replicationSampleSize) score += 0.4;
+ return score;
+}
+
+function interestScore(result: SavedResult): number {
+ const category = getPreviewCategory(result).id;
+ const categoryBoost =
+ category === "other" ? 0 :
+ category === "appearance" ? 0.25 :
+ category === "cardiometabolic" || category === "immune" || category === "medication" ? 1 :
+ 0.75;
+ return effectMagnitude(result) + evidenceScore(result) + categoryBoost;
+}
+
+function previewCategoryLimit(category: PreviewCategory): number {
+ if (category === "appearance") return 3;
+ if (category === "other") return 2;
+ return 3;
+}
+
+const CATEGORY_DISPLAY_PRIORITY: Record = {
+ cardiometabolic: 0,
+ medication: 1,
+ immune: 2,
+ sleep_energy: 3,
+ reproductive: 4,
+ brain_behavior: 5,
+ body_composition: 6,
+ appearance: 7,
+ other: 8,
+};
+
+function orderPreviewResultsForDisplay(results: SavedResult[]): SavedResult[] {
+ return [...results].sort((a, b) => {
+ const categoryDiff = CATEGORY_DISPLAY_PRIORITY[getPreviewCategory(a).id] - CATEGORY_DISPLAY_PRIORITY[getPreviewCategory(b).id];
+ if (categoryDiff !== 0) return categoryDiff;
+ return interestScore(b) - interestScore(a);
+ });
+}
+
+export function selectAhaPreviewResults(results: SavedResult[], limit = 12): SavedResult[] {
+ const sorted = [...results].sort((a, b) => interestScore(b) - interestScore(a));
+ const selected: SavedResult[] = [];
+ const seenTraits = new Set();
+ const categoryCounts = new Map();
+
+ for (const result of sorted) {
+ const normalized = normalizeTraitName(result.traitName);
+ if (normalized && seenTraits.has(normalized)) continue;
+
+ const category = getPreviewCategory(result).id;
+ const categoryLimit = previewCategoryLimit(category);
+ if ((categoryCounts.get(category) || 0) >= categoryLimit) continue;
+
+ selected.push(result);
+ if (normalized) seenTraits.add(normalized);
+ categoryCounts.set(category, (categoryCounts.get(category) || 0) + 1);
+ if (selected.length >= limit) break;
+ }
+
+ if (selected.length < limit) {
+ for (const result of sorted) {
+ if (selected.some((item) => item.studyId === result.studyId)) continue;
+ const normalized = normalizeTraitName(result.traitName);
+ if (normalized && seenTraits.has(normalized)) continue;
+
+ const category = getPreviewCategory(result).id;
+ if ((categoryCounts.get(category) || 0) >= previewCategoryLimit(category)) continue;
+
+ selected.push(result);
+ if (normalized) seenTraits.add(normalized);
+ categoryCounts.set(category, (categoryCounts.get(category) || 0) + 1);
+ if (selected.length >= limit) break;
+ }
+ }
+
+ return orderPreviewResultsForDisplay(selected);
+}
+
+export function buildPreviewInsight(results: SavedResult[]): PreviewInsight {
+ const categoryCounts = new Map();
+ for (const result of results) {
+ const category = getPreviewCategory(result).label;
+ categoryCounts.set(category, (categoryCounts.get(category) || 0) + 1);
+ }
+
+ const themes = [...categoryCounts.entries()]
+ .sort((a, b) => b[1] - a[1])
+ .slice(0, 3)
+ .map(([label, count]) => ({ label, count }));
+
+ const standout = [...results]
+ .filter((result) => result.riskLevel !== "neutral")
+ .sort((a, b) => interestScore(b) - interestScore(a))[0] || results[0] || null;
+
+ const protectiveCount = results.filter((result) => result.riskLevel === "decreased").length;
+ const elevatedCount = results.filter((result) => result.riskLevel === "increased").length;
+
+ return {
+ themes,
+ standout,
+ protectiveCount,
+ elevatedCount,
+ headline: themes.length > 0
+ ? `Your first matches point to ${themes.map((theme) => theme.label).join(", ")}.`
+ : "Your first matches are ready to explore.",
+ };
+}
diff --git a/lib/run-all-onboarding.ts b/lib/run-all-onboarding.ts
index 9383553..59868ce 100644
--- a/lib/run-all-onboarding.ts
+++ b/lib/run-all-onboarding.ts
@@ -17,7 +17,8 @@ const ONBOARDING_CATALOG_URL =
export async function runAllAnalysisOnboarding(
genotypeData: Map,
onProgress: (progress: OnboardingRunAllProgress) => void,
- hasResult: (studyId: number) => boolean
+ hasResult: (studyId: number) => boolean,
+ options: { maxResults?: number } = {}
): Promise {
const startTime = Date.now();
@@ -120,6 +121,10 @@ export async function runAllAnalysisOnboarding(
const orBetaIdx = colMap.or_or_beta;
const ciTextIdx = colMap.ci_text;
const mappedGeneIdx = colMap.mapped_gene;
+ const pValueIdx = colMap.p_value;
+ const pValueMlogIdx = colMap.pvalue_mlog;
+ const sampleSizeIdx = colMap.initial_sample_size;
+ const replicationSampleSizeIdx = colMap.replication_sample_size;
if (
idIdx === undefined ||
@@ -244,9 +249,24 @@ export async function runAllAnalysisOnboarding(
riskLevel,
matchedSnp: riskSnpId,
analysisDate: new Date().toISOString(),
+ pValue: pValueIdx !== undefined ? cols[pValueIdx] || undefined : undefined,
+ pValueMlog: pValueMlogIdx !== undefined ? cols[pValueMlogIdx] || undefined : undefined,
mappedGene: cols[mappedGeneIdx] || undefined,
+ sampleSize: sampleSizeIdx !== undefined ? cols[sampleSizeIdx] || undefined : undefined,
+ replicationSampleSize: replicationSampleSizeIdx !== undefined ? cols[replicationSampleSizeIdx] || undefined : undefined,
});
matchCount++;
+ if (options.maxResults && results.length >= options.maxResults) {
+ emitProgress({
+ phase: "complete",
+ loaded: processedStudies,
+ total: processedStudies,
+ processedStudies,
+ matchCount,
+ message: "Preview analysis complete.",
+ });
+ return results;
+ }
}
}
@@ -311,7 +331,11 @@ export async function runAllAnalysisOnboarding(
riskLevel,
matchedSnp: riskSnpId,
analysisDate: new Date().toISOString(),
+ pValue: pValueIdx !== undefined ? cols[pValueIdx] || undefined : undefined,
+ pValueMlog: pValueMlogIdx !== undefined ? cols[pValueMlogIdx] || undefined : undefined,
mappedGene: cols[mappedGeneIdx] || undefined,
+ sampleSize: sampleSizeIdx !== undefined ? cols[sampleSizeIdx] || undefined : undefined,
+ replicationSampleSize: replicationSampleSizeIdx !== undefined ? cols[replicationSampleSizeIdx] || undefined : undefined,
});
matchCount++;
}
diff --git a/next-env.d.ts b/next-env.d.ts
index 9edff1c..c4b7818 100644
--- a/next-env.d.ts
+++ b/next-env.d.ts
@@ -1,6 +1,6 @@
///
///
-import "./.next/types/routes.d.ts";
+import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.