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.
+
+
+ Retry
+
+
+ )}
+
+ {!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