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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,4 @@ yarn-error.log*
/AGENTS.md
/CLAUDE.md
/scripts/ga-performance.env
/.junie/
27 changes: 25 additions & 2 deletions app/api/stripe/create-checkout/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,24 @@ const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || 'sk_test_placeholder'
apiVersion: '2025-02-24.acacia',
});

async function getOrCreateCustomer(walletAddress: string) {
const normalizedWallet = walletAddress.toLowerCase();
const existingCustomers = await stripe.customers.search({
query: `metadata['walletAddress']:'${normalizedWallet}'`,
limit: 1,
});

if (existingCustomers.data[0]) {
return existingCustomers.data[0];
}

return stripe.customers.create({
metadata: {
walletAddress: normalizedWallet,
},
});
}

export async function POST(request: NextRequest) {
try {
const { walletAddress, couponCode } = await request.json();
Expand Down Expand Up @@ -40,8 +58,10 @@ export async function POST(request: NextRequest) {
);
}

// Get the base URL for redirect
const origin = request.headers.get('origin') || process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000';
const requestOrigin = request.headers.get('origin');
const forwardedProto = request.headers.get('x-forwarded-proto') || 'http';
const forwardedHost = request.headers.get('x-forwarded-host') || request.headers.get('host');
const origin = requestOrigin || process.env.NEXT_PUBLIC_APP_URL || (forwardedHost ? `${forwardedProto}://${forwardedHost}` : 'http://localhost:3001');

// Validate promotion code if provided
let discounts = undefined;
Expand Down Expand Up @@ -86,10 +106,13 @@ export async function POST(request: NextRequest) {
}
}

const customer = await getOrCreateCustomer(walletAddress);

// Create Stripe Checkout Session for subscription
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
mode: 'subscription', // Recurring subscription
customer: customer.id,
line_items: [
{
price: process.env.STRIPE_PRICE_ID, // Reference existing price from Stripe Dashboard
Expand Down
16 changes: 14 additions & 2 deletions app/components/LLMChatInline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { SavedResult } from "@/lib/results-manager";
import { useResults } from "./ResultsContext";
import { useCustomization } from "./CustomizationContext";
import { useAuth } from "./AuthProvider";
import { useGenotype } from "./UserDataUpload";
import { hasValidPromoAccess } from "@/lib/promo-access";
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
Expand Down Expand Up @@ -57,6 +58,7 @@ export default function AIChatInline({ initialInput }: { initialInput?: string }
const router = useRouter();
const resultsContext = useResults();
const { getTopResultsByRelevance } = resultsContext;
const { isUploaded } = useGenotype();
const { customization, status: customizationStatus } = useCustomization();
const { isAuthenticated, hasActiveSubscription, openAuthModal } = useAuth();
const [hasPromoAccess, setHasPromoAccess] = useState(false);
Expand Down Expand Up @@ -352,7 +354,12 @@ export default function AIChatInline({ initialInput }: { initialInput?: string }
setAttachmentError(null);

// Track LLM question
trackLLMQuestionAsked({ isFollowUp: messages.length > 0 });
trackLLMQuestionAsked({
isFollowUp: messages.length > 0,
hasUploadedDna: isUploaded,
resultCount: resultsContext.savedResults.length,
source: 'chat',
});

// Process attachments if any
let processedAttachments: Attachment[] = [];
Expand Down Expand Up @@ -701,7 +708,12 @@ Write questions from the user's perspective — as if the user is asking you. No
setError(null);
setAttachmentError(null);

trackLLMQuestionAsked({ isFollowUp: messages.length > 0 });
trackLLMQuestionAsked({
isFollowUp: messages.length > 0,
hasUploadedDna: isUploaded,
resultCount: resultsContext.savedResults.length,
source: 'research',
});

const userMessage: Message = { role: 'user', content: query, timestamp: new Date() };
const assistantMessage: Message = { role: 'assistant', content: '', timestamp: new Date() };
Expand Down
189 changes: 112 additions & 77 deletions app/components/MenuBar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useState, useEffect, type CSSProperties } from "react";
import { useCallback, useState, useEffect, type CSSProperties } from "react";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import UserDataUpload, { useGenotype } from "./UserDataUpload";
Expand Down Expand Up @@ -138,12 +138,15 @@ export default function MenuBar() {
}, [theme]);

useEffect(() => {
const handleOpenDNAUpload = () => {
const handleOpenDNAUpload = (event: Event) => {
setShowMyDataDropdown(true);
const source = event instanceof CustomEvent && typeof event.detail?.source === "string"
? event.detail.source
: "home_upload_raw_dna";

// Wait for the dropdown and uploader to mount, then open the file picker.
setTimeout(() => {
window.dispatchEvent(new CustomEvent('openDNAUploadPicker'));
window.dispatchEvent(new CustomEvent('openDNAUploadPicker', { detail: { source } }));
}, 60);
};

Expand All @@ -154,6 +157,14 @@ export default function MenuBar() {
};
}, []);

useEffect(() => {
const handleOpenPersonalization = () => setShowCustomizationModal(true);
window.addEventListener("openPersonalization", handleOpenPersonalization);
return () => {
window.removeEventListener("openPersonalization", handleOpenPersonalization);
};
}, []);

const toggleTheme = () => {
setTheme((prev) => (prev === "dark" ? "light" : "dark"));
};
Expand All @@ -171,7 +182,7 @@ export default function MenuBar() {
}
};

const handleRunAll = () => {
const handleRunAll = useCallback(() => {
window.dispatchEvent(new Event("showMobileCompatibilityNotice"));

if (isRunningAll) {
Expand All @@ -187,7 +198,7 @@ export default function MenuBar() {
}

setShowRunAllDisclaimer(true);
};
}, [genotypeData, isRunningAll]);

const handleRunAllDisclaimerAccept = async () => {
setShowRunAllDisclaimer(false);
Expand Down Expand Up @@ -317,6 +328,18 @@ export default function MenuBar() {

const isDNAChatActive = pathname === "/dna-chat" || pathname === "/llm-chat";
const isOverviewReportActive = pathname === "/overview-report";
const isPublicEntryPage = pathname === "/" || pathname === "/raw-dna-guide";
const hasUserWork = isUploaded || savedResults.length > 0;
const showAdvancedControls = !isPublicEntryPage || hasUserWork;
const showRunAllControl = !isPublicEntryPage || isUploaded || savedResults.length > 0;

useEffect(() => {
const handleStartRunAll = () => handleRunAll();
window.addEventListener("startRunAllAnalysis", handleStartRunAll);
return () => {
window.removeEventListener("startRunAllAnalysis", handleStartRunAll);
};
}, [handleRunAll]);

return (
<>
Expand Down Expand Up @@ -424,13 +447,15 @@ export default function MenuBar() {
>
Browse
</Link>
<Link
href="/overview-report"
className={isOverviewReportActive ? "nav-link active" : "nav-link"}
style={getNavLinkStyle(isOverviewReportActive)}
>
Analyze
</Link>
{showAdvancedControls && (
<Link
href="/overview-report"
className={isOverviewReportActive ? "nav-link active" : "nav-link"}
style={getNavLinkStyle(isOverviewReportActive)}
>
Analyze
</Link>
)}
</nav>
</div>

Expand All @@ -452,75 +477,85 @@ export default function MenuBar() {
)}
</button>

<button
className="menu-icon-button"
onClick={() => setShowResultsDropdown(!showResultsDropdown)}
title="Load, export, and manage results"
data-tour="results-button"
>
<span className="icon">
<FolderIcon size={32} />
</span>
<span className="label">Results</span>
{savedResults.length > 0 && (
<span className="badge">{savedResults.length}</span>
)}
</button>
{showAdvancedControls && (
<button
className="menu-icon-button"
onClick={() => setShowResultsDropdown(!showResultsDropdown)}
title="Load, export, and manage results"
data-tour="results-button"
>
<span className="icon">
<FolderIcon size={32} />
</span>
<span className="label">Results</span>
{savedResults.length > 0 && (
<span className="badge">{savedResults.length}</span>
)}
</button>
)}

<button
className={isRunningAll ? "menu-icon-button running" : "menu-icon-button"}
onClick={handleRunAll}
title="Analyze your DNA against all matching GWAS traits"
data-tour="run-all-button"
>
<span className="icon">
<RunAllIcon size={32} />
</span>
<span className="label">Run All</span>
{isRunningAll && (
<span className="badge">Running</span>
)}
</button>
{showRunAllControl && (
<button
className={isRunningAll ? "menu-icon-button running" : "menu-icon-button"}
onClick={handleRunAll}
title="Analyze your DNA against all matching GWAS traits"
data-tour="run-all-button"
>
<span className="icon">
<RunAllIcon size={32} />
</span>
<span className="label">Run All</span>
{isRunningAll && (
<span className="badge">Running</span>
)}
</button>
)}

<button
className={`menu-icon-button ${customizationStatus}`}
onClick={() => setShowCustomizationModal(true)}
title={getCustomizationTooltip()}
data-tour="personalize-button"
>
<span className="icon">
<MicroscopeIcon size={32} />
</span>
<span className="label">Personalize</span>
</button>
{showAdvancedControls && (
<button
className={`menu-icon-button ${customizationStatus}`}
onClick={() => setShowCustomizationModal(true)}
title={getCustomizationTooltip()}
data-tour="personalize-button"
>
<span className="icon">
<MicroscopeIcon size={32} />
</span>
<span className="label">Personalize</span>
</button>
)}

<button
className="menu-icon-button"
onClick={() => setShowLLMConfigModal(true)}
title="Configure LLM provider and model"
data-tour="llm-config-button"
>
<span className="icon">
<SparklesIcon size={32} />
</span>
<span className="label">LLM</span>
<span className="badge">{llmProvider || 'OpenAI'}</span>
</button>
{showAdvancedControls && (
<button
className="menu-icon-button"
onClick={() => setShowLLMConfigModal(true)}
title="Configure LLM provider and model"
data-tour="llm-config-button"
>
<span className="icon">
<SparklesIcon size={32} />
</span>
<span className="label">LLM</span>
<span className="badge">{llmProvider || 'OpenAI'}</span>
</button>
)}

<button
className="menu-icon-button"
onClick={() => setShowCacheDropdown(!showCacheDropdown)}
title="View and manage cached GWAS data"
data-tour="cache-button"
>
<span className="icon">
<CacheIcon size={32} />
</span>
<span className="label">Cache</span>
{cacheInfo && (
<span className="badge">{cacheInfo.studies.toLocaleString()}</span>
)}
</button>
{showAdvancedControls && (
<button
className="menu-icon-button"
onClick={() => setShowCacheDropdown(!showCacheDropdown)}
title="View and manage cached GWAS data"
data-tour="cache-button"
>
<span className="icon">
<CacheIcon size={32} />
</span>
<span className="label">Cache</span>
{cacheInfo && (
<span className="badge">{cacheInfo.studies.toLocaleString()}</span>
)}
</button>
)}

<button
className="menu-icon-button"
Expand Down
Loading
Loading