diff --git a/app/browse/page.tsx b/app/browse/page.tsx index 8744cf7..480689e 100644 --- a/app/browse/page.tsx +++ b/app/browse/page.tsx @@ -28,6 +28,11 @@ import { trackQueryRun, trackExploreTabViewed, trackSearchModeChanged, + trackBrowsePageViewed, + trackBrowseFiltersExpanded, + trackBrowseSearchChanged, + trackStudyOpened, + trackRunAllCtaClicked, } from "@/lib/analytics"; // Note: Metadata must be exported from layout.tsx or a server component @@ -310,13 +315,7 @@ function ExplorePage() { useEffect(() => { if (mounted) { trackExploreTabViewed(); - } - }, [mounted]); - - // Auto-show guided tour on first visit - useEffect(() => { - if (mounted && !hasCompletedTour(exploreTour.id)) { - setTourOpen(true); + trackBrowsePageViewed(); } }, [mounted]); @@ -340,7 +339,7 @@ function ExplorePage() { }); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const [sectionCollapsed, setSectionCollapsed] = useState(true); + const [sectionCollapsed, setSectionCollapsed] = useState(false); const [showTermsModal, setShowTermsModal] = useState(false); const [isRunningAll, setIsRunningAll] = useState(false); const [runAllProgress, setRunAllProgress] = useState({ current: 0, total: 0 }); @@ -415,6 +414,7 @@ function ExplorePage() { startTransition(() => { updateFilter("search", value); }); + trackBrowseSearchChanged(value); } }, [filters.search, updateFilter]); @@ -577,6 +577,15 @@ function ExplorePage() { userInitiatedSearchRef.current = false; }; + const SEARCH_CHIPS = ['sleep', 'cholesterol', 'caffeine', "Alzheimer's", 'diabetes'] as const; + + const handleChipClick = (term: string) => { + userInitiatedSearchRef.current = true; + startTransition(() => updateFilter("search", term)); + setSectionCollapsed(false); + trackBrowseSearchChanged(term); + }; + const handleColumnSort = (sortKey: SortOption) => { const newDirection = filters.sort === sortKey @@ -608,6 +617,7 @@ function ExplorePage() { }; const handleRunAll = () => { + trackRunAllCtaClicked(); if (!genotypeData || genotypeData.size === 0) { trackRunAllFailed("explore", "no_genotype_data"); alert("No SNPs found in your genetic data"); @@ -782,13 +792,22 @@ function ExplorePage() {

Filter genetic association studies by various criteria.

)} - {sectionCollapsed &&

Study Filters

} + {sectionCollapsed && ( + <> +

Study Filters

+
+ {SEARCH_CHIPS.map(term => ( + + ))} +
+ + )}
{!sectionCollapsed && ( - + )} {!sectionCollapsed && (
- +
diff --git a/app/components/PaymentModal.tsx b/app/components/PaymentModal.tsx index 1901d24..77eb501 100644 --- a/app/components/PaymentModal.tsx +++ b/app/components/PaymentModal.tsx @@ -409,7 +409,7 @@ export default function PaymentModal({ isOpen = true, onClose, onSuccess, displa

Subscribe to Premium

-

Get DNA Chat and Overview Report

+

Research in DNA Chat + all premium reports

{step === 'choice' && ( diff --git a/app/components/StripeSubscriptionForm.tsx b/app/components/StripeSubscriptionForm.tsx index 21d93cc..fac2bc2 100644 --- a/app/components/StripeSubscriptionForm.tsx +++ b/app/components/StripeSubscriptionForm.tsx @@ -170,11 +170,11 @@ function SubscriptionForm({ clientSecret, walletAddress, couponCode, discount, i
- DNA Chat Assistant + Research in DNA Chat
- Overview Report + All premium reports (Healthspan, Top Traits, Overview)
diff --git a/app/components/UserDataUpload.tsx b/app/components/UserDataUpload.tsx index 2bbdb8c..5703bff 100644 --- a/app/components/UserDataUpload.tsx +++ b/app/components/UserDataUpload.tsx @@ -50,10 +50,31 @@ export function GenotypeProvider({ children }: { children: React.ReactNode }) { } if (!validateFileFormat(file)) { - throw new Error('Unsupported file type. Please upload a .txt, .tsv, or .csv file exported from 23andMe, AncestryDNA, MyHeritage, FTDNA, LivingDNA, or a compatible provider.'); + throw new Error('Unsupported file type. Please upload a .txt, .tsv, .csv, or .gz file exported from 23andMe, AncestryDNA, MyHeritage, FTDNA, LivingDNA, or a compatible provider.'); } - const fileContent = await file.text(); + let fileContent: string; + if (fileExtension === 'gz') { + const buffer = await file.arrayBuffer(); + const ds = new DecompressionStream('gzip'); + const writer = ds.writable.getWriter(); + writer.write(buffer); + writer.close(); + const chunks: Uint8Array[] = []; + const reader = ds.readable.getReader(); + while (true) { + const { value, done } = await reader.read(); + if (done) break; + if (value) chunks.push(value); + } + const total = chunks.reduce((n, c) => n + c.length, 0); + const combined = new Uint8Array(total); + let offset = 0; + for (const chunk of chunks) { combined.set(chunk, offset); offset += chunk.length; } + fileContent = new TextDecoder().decode(combined); + } else { + fileContent = await file.text(); + } const hash = calculateFileHash(fileContent); const parseResult = detectAndParseGenotypeFile(fileContent); @@ -199,7 +220,7 @@ export default function UserDataUpload() { {isLoading ? 'Analyzing your genetic map...' : 'Choose File to Upload'} -

23andMe, AncestryDNA, MyHeritage, FTDNA, LivingDNA, and more

+

23andMe, AncestryDNA, MyHeritage, FTDNA, LivingDNA, and more. Compressed .gz files supported.

{error && (
{error} diff --git a/app/components/tours/tourContent.ts b/app/components/tours/tourContent.ts index 2766cc3..793205d 100644 --- a/app/components/tours/tourContent.ts +++ b/app/components/tours/tourContent.ts @@ -53,13 +53,6 @@ export const exploreTour: TourContent = { selector: '[data-tour="run-all-button"]', placement: "bottom", }, - { - name: "your_result", - title: "Your personal result", - body: "After analyzing, the Your Result column shows how your genotype compares to each study. Click any row to see the details.", - selector: '[data-tour="your-result-header"]', - placement: "bottom", - }, ], }; @@ -73,27 +66,6 @@ export const dnaChatTour: TourContent = { body: "DNA Chat is an anonymous and confidential LLM chat that uses your genetic results as context. Ask anything about your traits, sleep, diet, or risks.", }, myDataStep, - { - name: "prompts", - title: "Try a suggested prompt", - body: "Not sure what to ask? Tap any of these suggestions to fill the chat box. They're a great starting point.", - selector: ".example-questions", - placement: "top", - }, - { - name: "input", - title: "Or type your own", - body: "Type any question about your DNA here. The chat keeps your context across follow-up questions.", - selector: "textarea.chat-input", - placement: "top", - }, - { - name: "send", - title: "Send your question", - body: "Click here to send. The first answer pulls in your most relevant results as context and follow-ups continue the conversation.", - selector: ".chat-send-button", - placement: "top", - }, { name: "attach", title: "Upload lab reports and documents", @@ -161,13 +133,6 @@ export const menuBarTour: TourContent = { selector: '[data-tour="my-data-button"]', placement: "bottom", }, - { - name: "results", - title: "Results", - body: "Save your analysis results to a file or load a previous session. Use this to pick up where you left off or share results between devices.", - selector: '[data-tour="results-button"]', - placement: "bottom", - }, { name: "run_all", title: "Run All", @@ -196,19 +161,5 @@ export const menuBarTour: TourContent = { selector: '[data-tour="cache-button"]', placement: "bottom", }, - { - name: "help", - title: "Help", - body: "Reopen the onboarding tour or find answers to common questions here.", - selector: '[data-tour="help-button"]', - placement: "bottom", - }, - { - name: "theme", - title: "Light / Dark mode", - body: "Toggle between light and dark themes. Your preference is saved and applied on every visit.", - selector: '[data-tour="theme-button"]', - placement: "bottom", - }, ], }; diff --git a/app/dna-chat/page.tsx b/app/dna-chat/page.tsx index 5d33d5e..4eaa99f 100644 --- a/app/dna-chat/page.tsx +++ b/app/dna-chat/page.tsx @@ -230,13 +230,15 @@ export default function DNAChatPage() { )}
- +
+ +
diff --git a/app/globals.css b/app/globals.css index eb608af..4f28451 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1237,6 +1237,30 @@ button { margin-bottom: 0; } +.search-chips { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; + margin-top: 0.5rem; +} + +.search-chip { + padding: 0.2rem 0.65rem; + border-radius: 999px; + border: 1px solid var(--border-color); + background: var(--surface-bg); + color: var(--text-secondary); + font-size: 0.8rem; + cursor: pointer; + transition: border-color 0.15s, color 0.15s, background 0.15s; +} + +.search-chip:hover { + border-color: var(--accent-blue); + color: var(--accent-blue); + background: rgba(59, 130, 246, 0.06); +} + .panel-title { margin: 0; font-size: 1.1rem; @@ -11832,37 +11856,27 @@ details[open] .summary-arrow { padding: 0.1rem 1rem 0.35rem; } -.menu-explain-link { - background: none; - border: none; - color: var(--text-muted, #888); - font-size: 0.72rem; +.tour-help-button { + width: 1.6rem; + height: 1.6rem; + border-radius: 50%; + border: 1.5px solid var(--text-secondary); + background: transparent; + color: var(--text-secondary); + font-size: 0.9rem; + font-weight: 600; cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + line-height: 1; padding: 0; - text-decoration: underline; - text-underline-offset: 2px; - opacity: 0.65; -} - -.menu-explain-link:hover { - opacity: 1; - color: var(--accent-blue); + transition: border-color 0.15s, color 0.15s; } -.tour-trigger-link { - background: none; - border: none; - color: var(--accent-blue); - font-size: 0.85rem; - cursor: pointer; - padding: 0.25rem 0.5rem; - border-radius: 4px; - text-decoration: underline; - text-underline-offset: 3px; -} - -.tour-trigger-link:hover { - background: rgba(59, 130, 246, 0.08); +.tour-help-button:hover { + border-color: var(--text-primary); + color: var(--text-primary); } .mobile-compatibility-notice { @@ -12048,26 +12062,6 @@ details[open] .summary-arrow { position: relative; } -.dna-chat-tour-button { - position: absolute; - top: 0.5rem; - right: 0.6rem; - z-index: 10; - padding: 0.25rem 0.6rem; - border-radius: 4px; - border: 1px solid rgba(15, 23, 42, 0.18); - background: rgba(255, 255, 255, 0.9); - color: rgba(15, 23, 42, 0.55); - font-size: 0.72rem; - font-weight: 500; - cursor: pointer; -} - -.dna-chat-tour-button:hover { - background: #ffffff; - color: rgba(15, 23, 42, 0.85); - border-color: rgba(15, 23, 42, 0.35); -} /* Browse page tab toggle */ .browse-tab-toggle { diff --git a/app/landing-client.tsx b/app/landing-client.tsx index 44db694..3249193 100644 --- a/app/landing-client.tsx +++ b/app/landing-client.tsx @@ -77,13 +77,13 @@ export default function LandingClient() { const [sampleTotalBytes, setSampleTotalBytes] = useState(0); const loadSampleData = async () => { - trackSampleDataStarted('home'); - if (savedResults.length > 0) { router.push("/explore"); return; } + trackSampleDataStarted('home'); + try { setSampleStatus("downloading"); setSampleError(null); diff --git a/app/overview-report/page.tsx b/app/overview-report/page.tsx index 0cc0d49..340e947 100644 --- a/app/overview-report/page.tsx +++ b/app/overview-report/page.tsx @@ -14,7 +14,7 @@ import { OverviewReportIcon } from "../components/Icons"; import { useAuth } from "../components/AuthProvider"; import { useResults } from "../components/ResultsContext"; import { hasValidPromoAccess } from "@/lib/promo-access"; -import GuidedTour, { hasCompletedTour } from "../components/GuidedTour"; +import GuidedTour from "../components/GuidedTour"; import { overviewReportTour } from "../components/tours/tourContent"; import { trackOverviewReportViewed } from "@/lib/analytics"; @@ -44,12 +44,6 @@ export default function OverviewReportPage() { }, []); - useEffect(() => { - if (!hasCompletedTour(overviewReportTour.id)) { - setTourOpen(true); - } - }, []); - const hasPremiumAccess = hasActiveSubscription || hasPromoAccess; const hasResults = savedResults.length > 0; @@ -92,9 +86,7 @@ export default function OverviewReportPage() { gateDescription="Subscribe for $4.99/month to access Healthspan, Top Traits, and Overview reports." />
- +
{null} diff --git a/app/subscribe/page.tsx b/app/subscribe/page.tsx index 22cc0f7..a6f323f 100644 --- a/app/subscribe/page.tsx +++ b/app/subscribe/page.tsx @@ -53,7 +53,7 @@ function SubscribeContent() {
Premium -

Subscribe to Monadic DNA Explorer Premium

+

Unlock research and premium reports

$4.99/month. Cancel any time.

@@ -80,7 +80,7 @@ function SubscribeContent() {

Always free

  • Health Insights Report
  • -
  • Send in DNA Chat
  • +
  • Ask questions in DNA Chat
  • Browse and search all studies
  • Explore your results
  • Upload your own DNA file
  • diff --git a/lib/analytics.ts b/lib/analytics.ts index fe1af0f..d6a7ed8 100644 --- a/lib/analytics.ts +++ b/lib/analytics.ts @@ -678,6 +678,26 @@ export function trackSearchModeChanged(mode: 'similarity' | 'exact') { trackEvent('search_mode_changed', { mode }); } +export function trackBrowsePageViewed() { + trackEvent('browse_page_viewed'); +} + +export function trackBrowseFiltersExpanded() { + trackEvent('browse_filters_expanded'); +} + +export function trackBrowseSearchChanged(query: string) { + trackEvent('browse_search_changed', { query_length: query.length }); +} + +export function trackStudyOpened(studyId: number) { + trackEvent('study_opened', { study_id: String(studyId) }); +} + +export function trackRunAllCtaClicked() { + trackEvent('run_all_cta_clicked'); +} + export function trackStudyAnalysisStarted() { trackEvent('study_analysis_started'); } diff --git a/lib/genotype-parser.ts b/lib/genotype-parser.ts index f4007b0..e83a0d0 100644 --- a/lib/genotype-parser.ts +++ b/lib/genotype-parser.ts @@ -361,7 +361,7 @@ export function validateFileSize(file: File, maxSizeMB: number = 50): boolean { } export function validateFileFormat(file: File): boolean { - const validExtensions = ['.txt', '.tsv', '.csv']; + const validExtensions = ['.txt', '.tsv', '.csv', '.gz']; const fileName = file.name.toLowerCase(); return validExtensions.some(ext => fileName.endsWith(ext)); } 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.