diff --git a/backend/app/models/app_settings.py b/backend/app/models/app_settings.py index 9488fb81..eb5d167f 100644 --- a/backend/app/models/app_settings.py +++ b/backend/app/models/app_settings.py @@ -20,8 +20,8 @@ DEFAULTS = { "run_cost": 40, "trial_grant": 50, "credit_packs": [ - {"id": "starter", "name": "Starter", "price_usd": 49, "credits": 50}, - {"id": "pro", "name": "Pro", "price_usd": 199, "credits": 220}, + {"id": "starter", "name": "Starter", "price_usd": 49, "credits": 50}, + {"id": "pro", "name": "Pro", "price_usd": 199, "credits": 220, "popular": True}, {"id": "scale", "name": "Scale", "price_usd": 499, "credits": 600}, ], } diff --git a/backend/app/routes/billing.py b/backend/app/routes/billing.py index 86772fab..dd31f81a 100644 --- a/backend/app/routes/billing.py +++ b/backend/app/routes/billing.py @@ -20,9 +20,13 @@ billing_bp = Blueprint('billing', __name__) @billing_bp.route('/packs', methods=['GET']) async def get_credit_packs(): - """Public — returns available credit pack definitions for the landing page.""" + """Public — returns credit packs plus action costs for the landing page.""" settings = await get_settings() - return jsonify({'packs': settings.get('credit_packs', [])}), 200 + return jsonify({ + 'packs': settings.get('credit_packs', []), + 'persona_cost': settings.get('persona_cost', 2), + 'run_cost': settings.get('run_cost', 40), + }), 200 @billing_bp.route('/balance', methods=['GET']) diff --git a/index.html b/index.html index e13d5f07..0a2d8c8c 100755 --- a/index.html +++ b/index.html @@ -4,12 +4,27 @@ - Cohorta - + Cohorta — AI-Powered Synthetic Focus Groups + - - - + + + + + + + + + + + + + + + + + + diff --git a/public/og-image.png b/public/og-image.png index 89e22ee9..4526a83d 100755 Binary files a/public/og-image.png and b/public/og-image.png differ diff --git a/src/components/landing/Pricing.tsx b/src/components/landing/Pricing.tsx index 0469eafb..89ec65dd 100644 --- a/src/components/landing/Pricing.tsx +++ b/src/components/landing/Pricing.tsx @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react'; import { motion } from 'framer-motion'; import { useNavigate } from 'react-router-dom'; -import { CheckCircle2, Info } from 'lucide-react'; +import { CheckCircle2, Info, RefreshCw } from 'lucide-react'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { fadeUp, staggerChildren, viewportOnce } from '@/lib/motion'; import { billingApi } from '@/lib/api'; @@ -14,25 +14,30 @@ interface CreditPack { popular?: boolean; } -const DEFAULT_PACKS: CreditPack[] = [ - { id: 'starter', name: 'Starter', price_usd: 49, credits: 50 }, - { id: 'pro', name: 'Pro', price_usd: 199, credits: 220, popular: true }, - { id: 'scale', name: 'Scale', price_usd: 499, credits: 600 }, -]; +interface PacksResponse { + packs: CreditPack[]; + persona_cost: number; + run_cost: number; +} -const PACK_FEATURES: Record = { - starter: ['50 credits', '~25 AI personas', '1 focus group run', 'Export transcripts', 'Email support'], - pro: ['220 credits', '~110 AI personas', '5 focus group runs', 'Bulk export', 'Priority support', 'Advanced analytics'], - scale: ['600 credits', '~300 AI personas', '15 focus group runs', 'Unlimited exports', 'Dedicated support', 'Custom prompts', 'API access'], +const STATIC_FEATURES: Record = { + starter: ['Export transcripts', 'Email support'], + pro: ['Bulk export', 'Priority support', 'Advanced analytics'], + scale: ['Unlimited exports', 'Dedicated support', 'Custom prompts', 'API access'], }; -const COST_MATH = { - starter: { sessions: 1, personas: 25 }, - pro: { sessions: 5, personas: 110 }, - scale: { sessions: 15, personas: 300 }, -}; +function buildFeatures(pack: CreditPack, personaCost: number, runCost: number): string[] { + const personas = Math.floor(pack.credits / personaCost); + const runs = Math.floor(pack.credits / runCost); + return [ + `${pack.credits} credits`, + `~${personas} AI personas`, + `${runs} focus group run${runs !== 1 ? 's' : ''}`, + ...(STATIC_FEATURES[pack.id] ?? STATIC_FEATURES.starter), + ]; +} -function CreditTooltip() { +function CreditTooltip({ personaCost, runCost }: { personaCost: number; runCost: number }) { return ( @@ -44,8 +49,8 @@ function CreditTooltip() {

- 1 persona = 2 credits
- 1 focus group run = 40 credits
+ 1 persona = {personaCost} credits
+ 1 focus group run = {runCost} credits
Credits never expire. Trial: 50 free on signup.

@@ -54,15 +59,58 @@ function CreditTooltip() { ); } +function SkeletonCard({ popular }: { popular?: boolean }) { + return ( +
+
+
+
+
+
+ {[1, 2, 3, 4].map(i => ( +
+
+
+
+ ))} +
+
+
+ ); +} + export default function Pricing() { const navigate = useNavigate(); - const [packs, setPacks] = useState(DEFAULT_PACKS); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); - useEffect(() => { + const load = () => { + setLoading(true); + setError(false); billingApi.getPacks() - .then(r => { if (r.data?.packs?.length) setPacks(r.data.packs); }) - .catch(() => {}); - }, []); + .then(r => { + const { packs, persona_cost, run_cost } = r.data as PacksResponse; + if (packs?.length) { + setData({ packs, persona_cost: persona_cost ?? 2, run_cost: run_cost ?? 40 }); + } else { + setError(true); + } + }) + .catch(() => setError(true)) + .finally(() => setLoading(false)); + }; + + useEffect(() => { load(); }, []); + + const personaCost = data?.persona_cost ?? 2; + const runCost = data?.run_cost ?? 40; return (
@@ -83,16 +131,40 @@ export default function Pricing() {

Credits never expire. Start with 50 free — no card required.{' '} - + {!loading && !error && ( + + )}

{/* Cards */} - {packs.map((pack, i) => { - const features = PACK_FEATURES[pack.id] || PACK_FEATURES.pro; - const math = COST_MATH[pack.id as keyof typeof COST_MATH]; - const costPerSession = (pack.price_usd / math.sessions).toFixed(0); + {loading && ( + <> + + + + + )} + + {error && !loading && ( +
+

Could not load pricing.

+ +
+ )} + + {!loading && !error && data?.packs.map((pack, i) => { + const features = buildFeatures(pack, personaCost, runCost); + const runs = Math.floor(pack.credits / runCost); + const costPerSession = runs > 0 ? (pack.price_usd / runs).toFixed(0) : '—'; + const personas = Math.floor(pack.credits / personaCost); return ( {pack.credits} credits included

- {/* Cost-per-session visualisation */}
- ~{math.personas} personas + ~{personas} personas ~${costPerSession}/session