diff --git a/app/components/Icons.tsx b/app/components/Icons.tsx index d7fe1ea..dd90d10 100644 --- a/app/components/Icons.tsx +++ b/app/components/Icons.tsx @@ -255,3 +255,461 @@ export function AIIcon({ className = "", size = 16 }: IconProps) { ); } + +export function MicroscopeIcon({ className = "", size = 20 }: IconProps) { + return ( + + + + + + + + + + ); +} + +export function SparklesIcon({ className = "", size = 20 }: IconProps) { + return ( + + + + + + ); +} + +export function CacheIcon({ className = "", size = 20 }: IconProps) { + return ( + + + + + + ); +} + +export function HelpCircleIcon({ className = "", size = 20 }: IconProps) { + return ( + + + + + + ); +} + +export function FolderIcon({ className = "", size = 20 }: IconProps) { + return ( + + + + + ); +} + +export function SunIcon({ className = "", size = 20 }: IconProps) { + return ( + + + + + + + + + + + + ); +} + +export function MoonIcon({ className = "", size = 20 }: IconProps) { + return ( + + + + ); +} + +export function CrownIcon({ className = "", size = 20 }: IconProps) { + return ( + + {/* Ornate crown */} + + + {/* Crown points */} + + + {/* Decorative gems on crown points */} + + + + + {/* Inner gem details */} + + + + + {/* Ornate band decoration */} + + + {/* Side decorations */} + + + + + ); +} + +// Premium Feature Icons - Ornate and intricate +// Run All: Machine that processes genes into reports +export function RunAllIcon({ className = "", size = 24 }: IconProps) { + return ( + + {/* Machine body - ornate box */} + + + + {/* Input funnel (left) for genes */} + + + + {/* DNA helix going into machine */} + + + + + + {/* Output chute (right) for reports */} + + + + {/* Report paper coming out */} + + + + + {/* Machine gears and processing */} + + + + + + + + + {/* Gear teeth */} + + + + + + + + + + + {/* Control panel on top */} + + + + + + + {/* Steam/processing indicator */} + + + + + ); +} + +// LLM Chat: Witch using a typewriter +export function LLMChatIcon({ className = "", size = 24 }: IconProps) { + return ( + + {/* Witch hat - pointed with ornate brim */} + + + + + {/* Witch head/face */} + + + + {/* Witch hair flowing */} + + + + {/* Witch body/shoulders */} + + + + + {/* Arms reaching to typewriter */} + + + + {/* Typewriter body - ornate vintage design */} + + + + {/* Typewriter keys */} + + + + + + + + + + + + + + {/* Paper coming out of typewriter */} + + + + + {/* Magic sparkles around */} + + + + + ); +} + +// Overview Report: Lab tech looking into microscope +export function OverviewReportIcon({ className = "", size = 24 }: IconProps) { + return ( + + {/* Lab tech head */} + + + + {/* Lab coat collar */} + + + + + {/* Body/shoulders hunched over microscope */} + + + + + {/* Arms/hands on microscope */} + + + + {/* Microscope base/platform */} + + + + {/* Microscope body - ornate details */} + + + + {/* Microscope eyepiece where tech is looking */} + + + + {/* Microscope objective lens (bottom) */} + + + + {/* Focus knobs */} + + + + {/* Slide on stage */} + + + {/* Data/results visualization on side (screen/readout) */} + + + + {/* Graph/data lines on screen */} + + + + + + + + {/* DNA helix on screen */} + + + + + {/* Lab environment details */} + + + + {/* Analysis sparkle */} + + + ); +} + +// Robot AI icon for LLM Chat header +export function RobotIcon({ className = "", size = 24 }: IconProps) { + return ( + + {/* Antenna with ornate details */} + + + + + {/* Head - ornate box with multiple layers */} + + + + + {/* Eyes - glowing ornate circles */} + + + + + + + + {/* Mouth/display panel */} + + + + + {/* Body - ornate rectangle */} + + + + {/* Chest panel with circuit pattern */} + + + + + + + + {/* Arms - ornate mechanical limbs */} + + + + + + {/* Hands/claws */} + + + + {/* Legs - mechanical supports */} + + + + + + {/* Feet */} + + + + {/* Decorative screws/bolts */} + + + + + + {/* Energy/AI sparkles */} + + + + ); +} + +export function UserIcon({ className = "", size = 20 }: IconProps) { + return ( + + {/* Ornate decorative outer circle */} + + + + {/* User head with ornate details */} + + + + {/* Ornate body/shoulders */} + + + + {/* Decorative corner accents */} + + + + + + ); +} diff --git a/app/components/LLMChatInline.tsx b/app/components/LLMChatInline.tsx index d2f453a..1974615 100644 --- a/app/components/LLMChatInline.tsx +++ b/app/components/LLMChatInline.tsx @@ -9,6 +9,7 @@ import { useAuth } from "./AuthProvider"; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { callLLM, getLLMDescription } from "@/lib/llm-client"; +import { RobotIcon } from "./Icons"; type Message = { role: 'user' | 'assistant'; @@ -144,6 +145,25 @@ export default function AIChatInline() { const query = inputValue.trim(); if (!query) return; + // Check authentication first + if (!hasActiveSubscription && !hasPromoAccess) { + // Check if user is authenticated + const dynamicButton = document.querySelector('[data-dynamic-widget-button]') as HTMLElement; + if (dynamicButton) { + // Try to determine if user is logged in by checking for Dynamic's user indicator + const isLoggedIn = document.querySelector('[data-dynamic-user-profile]'); + if (!isLoggedIn) { + // Not logged in, trigger login + dynamicButton.click(); + return; + } + } + // User is logged in but not subscribed, show payment modal + const event = new CustomEvent('openPaymentModal'); + window.dispatchEvent(event); + return; + } + // Check consent before sending first message if (!hasConsent) { setShowConsentModal(true); @@ -595,9 +615,6 @@ Remember: You have plenty of space. Use ALL of it to provide a complete, thoroug ); } - // Show subscription required overlay if not subscribed and no promo access - const isBlocked = !hasActiveSubscription && !hasPromoAccess; - return ( <> {showConsentModal && ( @@ -608,38 +625,10 @@ Remember: You have plenty of space. Use ALL of it to provide a complete, thoroug /> )}
- {isBlocked && ( -
-
-

🔒 Premium Feature

-

- LLM Chat requires an active premium subscription. -

-

- Subscribe for $4.99/month to unlock LLM-powered analysis of your genetic results. -

-
-
- )}
-

🤖 LLM Chat: Your Genetic Results

+

+ LLM Chat: Your Genetic Results +

{getLLMDescription()} - Your data is processed securely

@@ -813,9 +802,10 @@ Remember: You have plenty of space. Use ALL of it to provide a complete, thoroug
diff --git a/app/components/MenuBar.tsx b/app/components/MenuBar.tsx index 152a45a..71c6076 100644 --- a/app/components/MenuBar.tsx +++ b/app/components/MenuBar.tsx @@ -6,37 +6,24 @@ import { useResults } from "./ResultsContext"; import { useCustomization } from "./CustomizationContext"; import CustomizationModal from "./CustomizationModal"; import LLMConfigModal from "./LLMConfigModal"; -import { FileIcon, SaveIcon, TrashIcon, MessageIcon, ClockIcon } from "./Icons"; -import { AuthButton, useAuth } from "./AuthProvider"; +import { MyDataDropdown, ResultsDropdown, CacheDropdown, HelpDropdown } from "./MenuDropdowns"; +import { DNAIcon, FolderIcon, MicroscopeIcon, SparklesIcon, CacheIcon, HelpCircleIcon, SunIcon, MoonIcon } from "./Icons"; import { getLLMConfig, getProviderDisplayName } from "@/lib/llm-config"; export default function MenuBar() { const { isUploaded, genotypeData, fileHash } = useGenotype(); const { savedResults, saveToFile, loadFromFile, clearResults } = useResults(); const { status: customizationStatus } = useCustomization(); - const { isAuthenticated, hasActiveSubscription, subscriptionData, user } = useAuth(); const [isLoadingFile, setIsLoadingFile] = useState(false); const [showCustomizationModal, setShowCustomizationModal] = useState(false); const [showLLMConfigModal, setShowLLMConfigModal] = useState(false); + const [showMyDataDropdown, setShowMyDataDropdown] = useState(false); + const [showResultsDropdown, setShowResultsDropdown] = useState(false); + const [showCacheDropdown, setShowCacheDropdown] = useState(false); + const [showHelpDropdown, setShowHelpDropdown] = useState(false); const [theme, setTheme] = useState<"light" | "dark">("dark"); const [cacheInfo, setCacheInfo] = useState<{ studies: number; sizeMB: number } | null>(null); const [llmProvider, setLlmProvider] = useState(''); - const [showSubscriptionMenu, setShowSubscriptionMenu] = useState(false); - - useEffect(() => { - // Close subscription menu when clicking outside - const handleClickOutside = (event: MouseEvent) => { - const target = event.target as HTMLElement; - if (!target.closest('.subscription-indicator')) { - setShowSubscriptionMenu(false); - } - }; - - if (showSubscriptionMenu) { - document.addEventListener('click', handleClickOutside); - return () => document.removeEventListener('click', handleClickOutside); - } - }, [showSubscriptionMenu]); useEffect(() => { // Detect system preference on mount @@ -90,17 +77,6 @@ export default function MenuBar() { } }; - const getCustomizationIcon = () => { - switch (customizationStatus) { - case 'not-set': - return '⚙️'; - case 'locked': - return '🔒'; - case 'unlocked': - return '🔓'; - } - }; - const getCustomizationTooltip = () => { switch (customizationStatus) { case 'not-set': @@ -112,10 +88,24 @@ export default function MenuBar() { } }; - const handleLLMConfigSave = () => { - // Reload LLM provider display name - const config = getLLMConfig(); - setLlmProvider(getProviderDisplayName(config.provider)); + const handleClearCache = async () => { + if (!cacheInfo) return; + + const confirmed = window.confirm( + `Clear cached GWAS Catalog data?\n\n` + + `${cacheInfo.studies.toLocaleString()} studies (${cacheInfo.sizeMB} MB)\n\n` + + `Data will be re-downloaded on next Run All.` + ); + if (confirmed) { + try { + const { gwasDB } = await import('@/lib/gwas-db'); + await gwasDB.clearDatabase(); + setCacheInfo(null); + alert('✓ Cache cleared successfully!'); + } catch { + alert('Failed to clear cache. Please try again.'); + } + } }; return ( @@ -126,8 +116,39 @@ export default function MenuBar() { /> setShowLLMConfigModal(false)} - onSave={handleLLMConfigSave} + onClose={() => { + setShowLLMConfigModal(false); + // Refresh LLM provider after closing modal + const config = getLLMConfig(); + setLlmProvider(getProviderDisplayName(config.provider)); + }} + onSave={() => {}} + /> + setShowMyDataDropdown(false)} + isUploaded={isUploaded} + genotypeData={genotypeData} + UserDataUploadComponent={UserDataUpload} + /> + setShowResultsDropdown(false)} + savedResults={savedResults} + onLoadFromFile={handleLoadFromFile} + onSaveToFile={() => saveToFile(genotypeData?.size, fileHash || undefined)} + onClearResults={clearResults} + isLoadingFile={isLoadingFile} + /> + setShowCacheDropdown(false)} + cacheInfo={cacheInfo} + onClearCache={handleClearCache} + /> + setShowHelpDropdown(false)} />
@@ -146,214 +167,98 @@ export default function MenuBar() {
-
- {isUploaded && genotypeData && ( - - {genotypeData.size.toLocaleString()} variants loaded + {/* Icon-based Navigation */} +
+
+ My Data + {isUploaded && genotypeData && ( + {genotypeData.size.toLocaleString()} + )} + -
-
- {savedResults.length > 0 && ( - - {savedResults.length} result{savedResults.length !== 1 ? 's' : ''} cached + + Results {savedResults.length > 0 && ( - <> - - - + {savedResults.length} )} -
-
- -
+ -
-
- -
- -
- {cacheInfo && ( - <> - - {cacheInfo.studies.toLocaleString()} studies cached ({cacheInfo.sizeMB} MB) - - - - )} + - Feedback + + + + Help
- -
- - {isAuthenticated && hasActiveSubscription && subscriptionData && ( - <> -
-
- - {showSubscriptionMenu && ( -
-
-

Premium Subscription

-

Expires: {subscriptionData.expiresAt ? new Date(subscriptionData.expiresAt).toLocaleDateString() : 'N/A'}

-

Days remaining: {subscriptionData.daysRemaining}

-
- -
- )} -
-
-
- - )} - -
- -
diff --git a/app/components/MenuDropdowns.tsx b/app/components/MenuDropdowns.tsx new file mode 100644 index 0000000..c3e7420 --- /dev/null +++ b/app/components/MenuDropdowns.tsx @@ -0,0 +1,251 @@ +"use client"; + +import { useEffect, useRef } from "react"; +import { FileIcon, SaveIcon, TrashIcon, MessageIcon, ClockIcon } from "./Icons"; + +// My Data Dropdown +export function MyDataDropdown({ + isOpen, + onClose, + isUploaded, + genotypeData, + UserDataUploadComponent, +}: { + isOpen: boolean; + onClose: () => void; + isUploaded: boolean; + genotypeData: { size: number } | null; + UserDataUploadComponent: React.ComponentType; +}) { + const dropdownRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + onClose(); + } + }; + + if (isOpen) { + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + } + }, [isOpen, onClose]); + + if (!isOpen) return null; + + return ( +
+
+

My Data

+ {isUploaded && genotypeData && ( +

{genotypeData.size.toLocaleString()} variants loaded

+ )} + +
+
+ ); +} + +// Results Dropdown +export function ResultsDropdown({ + isOpen, + onClose, + savedResults, + onLoadFromFile, + onSaveToFile, + onClearResults, + isLoadingFile, +}: { + isOpen: boolean; + onClose: () => void; + savedResults: unknown[]; + onLoadFromFile: () => void; + onSaveToFile: () => void; + onClearResults: () => void; + isLoadingFile: boolean; +}) { + const dropdownRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + onClose(); + } + }; + + if (isOpen) { + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + } + }, [isOpen, onClose]); + + if (!isOpen) return null; + + return ( +
+
+

Results

+ {savedResults.length > 0 && ( +

+ {savedResults.length} result{savedResults.length !== 1 ? "s" : ""} cached +

+ )} +
+ + {savedResults.length > 0 && ( + <> + + + + )} +
+
+
+ ); +} + +// Cache Dropdown +export function CacheDropdown({ + isOpen, + onClose, + cacheInfo, + onClearCache, +}: { + isOpen: boolean; + onClose: () => void; + cacheInfo: { studies: number; sizeMB: number } | null; + onClearCache: () => void; +}) { + const dropdownRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + onClose(); + } + }; + + if (isOpen) { + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + } + }, [isOpen, onClose]); + + if (!isOpen) return null; + + return ( +
+
+

Cache

+ {cacheInfo ? ( + <> +

+ {cacheInfo.studies.toLocaleString()} studies cached ({cacheInfo.sizeMB} MB) +

+
+ +
+ + ) : ( +

No cached data

+ )} +
+
+ ); +} + +// Help Dropdown +export function HelpDropdown({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) { + const dropdownRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + onClose(); + } + }; + + if (isOpen) { + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + } + }, [isOpen, onClose]); + + if (!isOpen) return null; + + return ( +
+
+

Help & Feedback

+ +
+
+ ); +} diff --git a/app/components/PremiumPaywall.tsx b/app/components/PremiumPaywall.tsx index 82c8ffd..f8989c9 100644 --- a/app/components/PremiumPaywall.tsx +++ b/app/components/PremiumPaywall.tsx @@ -42,6 +42,15 @@ export function PremiumPaywall({ children }: PremiumPaywallProps) { } }, []); + // Listen for payment modal trigger + useEffect(() => { + const handleOpenPaymentModal = () => { + setShowPaymentModal(true); + }; + window.addEventListener('openPaymentModal', handleOpenPaymentModal); + return () => window.removeEventListener('openPaymentModal', handleOpenPaymentModal); + }, []); + const handleRemovePromoCode = () => { localStorage.removeItem('promo_access'); setHasPromoAccess(false); @@ -64,7 +73,7 @@ export function PremiumPaywall({ children }: PremiumPaywallProps) { setTimeout(() => refreshSubscription(), 5000); }; - // Always show content, with subscription banner if needed + // Always show content return ( <> setShowPaymentModal(false)} onSuccess={handleModalSuccess} /> - - {!hasActiveSubscription && !hasPromoAccess && ( -
- - Premium subscription required - Subscribe for $4.99/month to access LLM Chat, Run All Analysis, and more. - - -
- )} - - {hasPromoAccess && ( -
- ✓ Premium access active (promo code: {promoCode}) - -
- )} - {children} ); diff --git a/app/globals.css b/app/globals.css index b146769..9d81dc4 100644 --- a/app/globals.css +++ b/app/globals.css @@ -132,6 +132,229 @@ body { box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.2); } +/* Icon-based Menu Navigation */ +.menu-icons { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.menu-icon-button { + background: var(--surface-bg); + border: 2px solid var(--border-color); + border-radius: 12px; + padding: 0.75rem 1rem; + cursor: pointer; + transition: all 0.3s ease; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + min-width: 85px; + position: relative; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + /* Tarot card aesthetic - decorative double border */ + outline: 1px solid transparent; + outline-offset: -4px; +} + +.menu-icon-button::before { + content: ''; + position: absolute; + inset: 3px; + border: 1px solid var(--border-color); + border-radius: 8px; + pointer-events: none; + opacity: 0.3; +} + +.menu-icon-button .icon { + display: flex; + align-items: center; + justify-content: center; + line-height: 1; + color: var(--text-secondary); + transition: all 0.3s ease; + padding: 0.25rem; +} + +.menu-icon-button .icon svg { + display: block; + filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1)); +} + +.menu-icon-button .label { + font-size: 0.7rem; + font-weight: 600; + color: var(--text-secondary); + line-height: 1; + transition: all 0.3s ease; + letter-spacing: 0.05em; + text-transform: uppercase; + font-family: 'Inter', sans-serif; +} + +.menu-icon-button .badge { + position: absolute; + top: -0.25rem; + right: -0.25rem; + background: var(--accent-blue); + color: white; + font-size: 0.65rem; + font-weight: 700; + padding: 0.15rem 0.4rem; + border-radius: 12px; + min-width: 20px; + text-align: center; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + border: 2px solid var(--primary-bg); +} + +.menu-icon-button:hover { + background: rgba(59, 130, 246, 0.08); + border-color: var(--accent-blue); + transform: translateY(-3px); + box-shadow: 0 4px 16px rgba(59, 130, 246, 0.2); +} + +.menu-icon-button:hover::before { + border-color: var(--accent-blue); + opacity: 0.5; +} + +.menu-icon-button:hover .label { + color: var(--text-primary); +} + +.menu-icon-button:hover .icon { + color: var(--accent-blue); + transform: scale(1.05); +} + +.menu-icon-button.not-set { + border-color: rgba(245, 158, 11, 0.4); +} + +.menu-icon-button.not-set::before { + border-color: rgba(245, 158, 11, 0.3); +} + +.menu-icon-button.locked { + border-color: rgba(239, 68, 68, 0.4); +} + +.menu-icon-button.locked::before { + border-color: rgba(239, 68, 68, 0.3); +} + +.menu-icon-button.unlocked { + border-color: rgba(16, 185, 129, 0.4); +} + +.menu-icon-button.unlocked::before { + border-color: rgba(16, 185, 129, 0.3); +} + +.menu-icon-button.subscribed { + border-color: rgba(16, 185, 129, 0.4); +} + +.menu-icon-button.subscribed::before { + border-color: rgba(16, 185, 129, 0.3); +} + +.menu-icon-button.not-subscribed { + border-color: rgba(239, 68, 68, 0.4); +} + +.menu-icon-button.not-subscribed::before { + border-color: rgba(239, 68, 68, 0.3); +} + +.menu-icon-button.authenticated { + border-color: rgba(59, 130, 246, 0.4); +} + +.menu-icon-button.authenticated::before { + border-color: rgba(59, 130, 246, 0.3); +} + +.menu-icon-button.not-authenticated { + border-color: rgba(156, 163, 175, 0.4); +} + +.menu-icon-button.not-authenticated::before { + border-color: rgba(156, 163, 175, 0.3); +} + +/* Auth icon wrapper - allow click-through to DynamicWidget */ +.auth-icon-wrapper { + position: relative; + pointer-events: none; +} + +.auth-icon-wrapper > * { + pointer-events: auto; +} + +/* Menu Dropdowns */ +.menu-dropdown { + position: fixed; + top: 5rem; + right: 2rem; + background: var(--secondary-bg); + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 1.5rem; + min-width: 300px; + max-width: 400px; + backdrop-filter: blur(20px); + box-shadow: 0 10px 30px -5px rgba(0, 0, 0, 0.3); + z-index: 1000; + animation: slideDown 0.2s ease-out; +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.dropdown-content h3 { + margin: 0 0 1rem 0; + font-size: 1.1rem; + font-weight: 600; + color: var(--text-primary); +} + +.dropdown-content .stat-display { + font-size: 0.9rem; + color: var(--text-secondary); + margin-bottom: 1rem; + padding: 0.5rem; + background: rgba(59, 130, 246, 0.05); + border: 1px solid rgba(59, 130, 246, 0.15); + border-radius: 6px; + text-align: center; +} + +.dropdown-actions { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.dropdown-actions .control-button, +.dropdown-actions a { + width: 100%; + justify-content: center; +} + .app-title { margin: 0; font-size: 1.5rem; @@ -3661,6 +3884,194 @@ details[open] .summary-arrow { } /* Premium Section */ +.premium-compact-header { + padding: 0.75rem 3rem 0.5rem 3rem; + max-width: 1400px; + margin: 0 auto; + width: 100%; +} + +.premium-header-content { + display: flex; + align-items: center; + gap: 1rem; + background: var(--surface-bg); + border: 2px solid var(--border-color); + border-radius: 8px; + padding: 0.75rem 1rem; +} + +.premium-wallet-section { + flex-shrink: 0; +} + +.auth-prompt-inline, +.subscription-prompt-inline, +.subscription-active-inline { + flex: 1; + display: flex; + align-items: center; + gap: 1rem; + font-size: 0.9rem; +} + +.auth-prompt-inline { + color: var(--text-secondary); +} + +.subscription-prompt-inline { + justify-content: space-between; +} + +.subscription-active-inline { + color: var(--text-primary); +} + +.subscription-message { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.subscription-message strong { + color: var(--text-primary); +} + +.subscription-message span { + color: var(--text-secondary); + font-size: 0.85rem; +} + +.subscribe-button { + padding: 0.5rem 1rem; + background: #f59e0b; + color: white; + border: none; + border-radius: 6px; + cursor: pointer; + font-weight: 600; + font-size: 0.875rem; + white-space: nowrap; + transition: background 0.2s; +} + +.subscribe-button:hover { + background: #d97706; +} + +.subscription-menu-container { + position: relative; +} + +.subscription-menu-button { + padding: 0.25rem; + background: transparent; + border: none; + cursor: pointer; + color: var(--text-secondary); + opacity: 0.6; + transition: opacity 0.2s; + display: flex; + align-items: center; + justify-content: center; +} + +.subscription-menu-button:hover { + opacity: 1; +} + +.subscription-menu-backdrop { + position: fixed; + inset: 0; + z-index: 999; +} + +.subscription-menu-dropdown { + position: absolute; + top: calc(100% + 0.5rem); + right: 0; + background: var(--surface-bg); + border: 2px solid var(--border-color); + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + min-width: 220px; + z-index: 1000; + overflow: hidden; +} + +.subscription-menu-header { + padding: 0.75rem 1rem; +} + +.subscription-menu-info { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.subscription-menu-info strong { + font-size: 0.875rem; + color: var(--text-primary); +} + +.subscription-menu-info span { + font-size: 0.8rem; + color: var(--text-secondary); +} + +.subscription-menu-info .expires-date { + font-size: 0.75rem; + opacity: 0.8; +} + +.subscription-menu-divider { + height: 1px; + background: var(--border-color); + margin: 0; +} + +.subscription-menu-item { + width: 100%; + padding: 0.75rem 1rem; + background: transparent; + border: none; + text-align: left; + cursor: pointer; + color: var(--text-primary); + font-size: 0.875rem; + transition: background 0.2s; + display: block; +} + +.subscription-menu-item:hover { + background: rgba(128, 128, 128, 0.1); +} + +.subscription-menu-item.cancel { + color: var(--text-secondary); +} + +.subscription-menu-item.cancel:hover { + background: rgba(239, 68, 68, 0.1); + color: #ef4444; +} + +@media (max-width: 900px) { + .premium-compact-header { + padding: 1rem 1.5rem; + } + + .premium-header-content { + flex-direction: column; + align-items: flex-start; + } + + .subscription-prompt-inline { + flex-direction: column; + align-items: flex-start; + } +} + .premium-section { flex: 1; display: flex; @@ -3681,20 +4092,22 @@ details[open] .summary-arrow { } .premium-features-header .collapse-button { - background: var(--surface-bg); - border: 1px solid var(--border-color); - border-radius: 6px; - padding: 0.5rem 1rem; - font-size: 0.875rem; + background: transparent; + border: none; + border-radius: 4px; + padding: 0.25rem 0.5rem; + font-size: 0.75rem; cursor: pointer; transition: all 0.2s ease; color: var(--text-secondary); - font-weight: 500; + font-weight: 400; + opacity: 0.6; } .premium-features-header .collapse-button:hover { - background: var(--border-color); + opacity: 1; color: var(--text-primary); + background: rgba(128, 128, 128, 0.1); } /* Premium Features Overview - Compact 3-column cards */ @@ -3747,15 +4160,20 @@ details[open] .summary-arrow { } .feature-icon { - width: 2rem; - height: 2rem; - color: var(--accent-blue); - stroke-width: 2; + display: flex; + align-items: center; + justify-content: center; + color: var(--text-primary); + opacity: 0.85; +} + +.feature-icon svg { + filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1)); } .feature-overview-card.disabled .feature-icon { color: var(--text-secondary); - opacity: 0.6; + opacity: 0.4; } .feature-overview-card h3 { diff --git a/app/page.tsx b/app/page.tsx index 8f3dc29..13f774f 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -4,6 +4,8 @@ import { useEffect, useMemo, useState, useCallback, useRef } from "react"; import { GenotypeProvider, useGenotype } from "./components/UserDataUpload"; import { ResultsProvider, useResults } from "./components/ResultsContext"; import { CustomizationProvider } from "./components/CustomizationContext"; +import { AuthButton, useAuth } from "./components/AuthProvider"; +import { RunAllIcon, LLMChatIcon, OverviewReportIcon } from "./components/Icons"; import StudyResultReveal from "./components/StudyResultReveal"; import MenuBar from "./components/MenuBar"; import VariantChips from "./components/VariantChips"; @@ -203,6 +205,7 @@ function MainContent() { const { genotypeData, isUploaded, setOnDataLoadedCallback } = useGenotype(); const { setOnResultsLoadedCallback, addResult, addResultsBatch, hasResult } = useResults(); const resultsContext = useResults(); + const { isAuthenticated, hasActiveSubscription, subscriptionData, checkingSubscription, user } = useAuth(); // Track client-side mounting to prevent hydration errors const [mounted, setMounted] = useState(false); @@ -253,6 +256,7 @@ function MainContent() { const [runAllProgress, setRunAllProgress] = useState({ current: 0, total: 0 }); const [showRunAllModal, setShowRunAllModal] = useState(false); const [showOverviewReportModal, setShowOverviewReportModal] = useState(false); + const [showSubscriptionMenu, setShowSubscriptionMenu] = useState(false); const [showRunAllDisclaimer, setShowRunAllDisclaimer] = useState(false); const [runAllStatus, setRunAllStatus] = useState<{ phase: 'fetching' | 'downloading' | 'decompressing' | 'parsing' | 'storing' | 'analyzing' | 'embeddings' | 'complete' | 'error'; @@ -1086,7 +1090,111 @@ function MainContent() { ) : ( /* Premium Tab - 3 Features with LLM Chat Primary */ - + <> + {/* Account & Subscription Compact Header */} +
+
+ {!isAuthenticated ? ( +
+ Sign in to access premium features → +
+ ) : !hasActiveSubscription ? ( +
+
+ Premium subscription required + Subscribe for $4.99/month to access Run All Analysis, LLM Chat, and Overview Report. +
+ +
+ ) : subscriptionData ? ( +
+ ✓ Premium Active +
+ ) : null} +
+ +
+ {subscriptionData && ( +
+ + {showSubscriptionMenu && ( + <> +
setShowSubscriptionMenu(false)} + /> +
+
+
+ Subscription Details + {subscriptionData.daysRemaining > 0 && ( + {subscriptionData.daysRemaining} days remaining in current cycle + )} + {subscriptionData.expiresAt && ( + Renews {new Date(subscriptionData.expiresAt).toLocaleDateString()} + )} +
+
+
+ +
+ + )} +
+ )} +
+
+ {null}
{/* Feature Overview Cards - Compact 3-column with collapse button */}
@@ -1103,23 +1211,30 @@ function MainContent() {
{/* Run All Card - First */}
- - - +
+ +

Run All

-

- {!mounted ? 'Loading...' : - !isUploaded ? 'Upload DNA data first' : - resultsContext.savedResults.length > 0 - ? `${resultsContext.savedResults.length.toLocaleString()} traits analyzed` - : 'Analyze all GWAS studies'} -

+

Run your data through all million+ traits

@@ -1127,38 +1242,39 @@ function MainContent() { {/* LLM Chat Card - Primary */}
- - - - - - +
+ +

LLM Chat

Ask a private LLM questions about your genetic data

{/* Overview Report Card */}
- - - - - - - +
+ +

Overview Report (Experimental)

-

- {!mounted ? 'Loading...' : - resultsContext.savedResults.length < 1000 - ? 'Analyze 1,000+ traits first' - : 'Generate comprehensive LLM report'} -

+

Have an LLM analyze all your traits

@@ -1170,7 +1286,7 @@ function MainContent() { {/* LLM Chat - Full Interface */}
-
+ )}