diff --git a/README.md b/README.md index 881c395c..3018208f 100755 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Semblance Synthetic Society +# Cohorta An AI-powered platform for creating synthetic personas and running autonomous focus group sessions. Build realistic consumer profiles, moderate live discussions or let AI drive the conversation, and extract actionable themes and insights — all in real time. @@ -157,7 +157,7 @@ npm run dev │ │ └── utils/ # Prompt loader, discussion guide schema │ └── prompts/ # LLM prompt templates (20 markdown files) ├── deploy.sh # Production deployment script -├── semblance.service # systemd service file +├── cohorta.service # systemd service file ├── start.sh # Local development start script ├── .env.development # Development environment config └── .env.production # Production environment config @@ -185,10 +185,10 @@ The `llm_service.py` module provides a unified interface across multiple AI prov | Setting | Development | Production | |---|---|---| -| **Base Path** | `/` | `/semblance/` | -| **API Base URL** | `/api` (proxied to `:5137`) | `https://optical-dev.oliver.solutions/semblance_back/api` | -| **WebSocket Path** | `/socket.io/` | `/semblance_back/socket.io/` | -| **MSAL Redirect** | `http://localhost:5173/` | `https://optical-dev.oliver.solutions/semblance` | +| **Base Path** | `/` | `/` | +| **API Base URL** | `/api` (proxied to `:5137`) | `https://cohorta.ai-impress.com_back/api` | +| **WebSocket Path** | `/socket.io/` | `/api/socket.io/` | +| **MSAL Redirect** | `http://localhost:5173/` | `https://cohorta.ai-impress.com` | | **Local Login** | Enabled | Disabled | Environment files: @@ -218,7 +218,7 @@ This script handles: 3. Installing backend dependencies 4. Building the frontend 5. Deploying built assets to the web server directory -6. Restarting the `semblance.service` systemd unit +6. Restarting the `cohorta.service` systemd unit ### Manual Backend Start (Production) @@ -228,7 +228,7 @@ source venv/bin/activate hypercorn "app:create_app()" --bind 0.0.0.0:5137 ``` -The included `semblance.service` file can be installed as a systemd unit for process management. +The included `cohorta.service` file can be installed as a systemd unit for process management. ## Contributing diff --git a/backend/README.md b/backend/README.md index 12686c92..f8e20371 100755 --- a/backend/README.md +++ b/backend/README.md @@ -1,6 +1,6 @@ -# Semblance Synthetic Society Backend +# Cohorta Backend -This is the Python backend for the Semblance Synthetic Society project. It provides API endpoints for authentication, personas, and focus groups. +This is the Python backend for the Cohorta project. It provides API endpoints for authentication, personas, and focus groups. ## Setup diff --git a/backend/migrate_legacy_folders.py b/backend/migrate_legacy_folders.py index f8eecfe2..0c07c7da 100755 --- a/backend/migrate_legacy_folders.py +++ b/backend/migrate_legacy_folders.py @@ -40,9 +40,9 @@ async def get_db_connection(): # Try each set of standard credentials for creds in standard_credentials: try: - uri = f"mongodb://{creds['user']}:{creds['pass']}@{mongo_host}:{mongo_port}/semblance_db?authSource={creds['db']}" + uri = f"mongodb://{creds['user']}:{creds['pass']}@{mongo_host}:{mongo_port}/cohorta_db?authSource={creds['db']}" motor_client = AsyncIOMotorClient(uri, serverSelectionTimeoutMS=2000) - database = motor_client.semblance_db + database = motor_client.cohorta_db # Test the connection await database.command('ping') logger.info(f"Connected to MongoDB with credentials: {creds['user']}") @@ -53,7 +53,7 @@ async def get_db_connection(): # Try without authentication try: motor_client = AsyncIOMotorClient(f'mongodb://{mongo_host}:{mongo_port}', serverSelectionTimeoutMS=5000) - database = motor_client.semblance_db + database = motor_client.cohorta_db await database.command('ping') # Test write access test_result = await database.test_collection.insert_one({"test": "migration_test"}) @@ -66,9 +66,9 @@ async def get_db_connection(): # Try with environment variables if mongo_user and mongo_pass: try: - uri = f"mongodb://{mongo_user}:{mongo_pass}@{mongo_host}:{mongo_port}/semblance_db?authSource=admin" + uri = f"mongodb://{mongo_user}:{mongo_pass}@{mongo_host}:{mongo_port}/cohorta_db?authSource=admin" motor_client = AsyncIOMotorClient(uri, serverSelectionTimeoutMS=5000) - database = motor_client.semblance_db + database = motor_client.cohorta_db await database.command('ping') logger.info(f"Connected to MongoDB with env credentials: {mongo_user}") return motor_client, database diff --git a/backend/scripts/backfill_usage.py b/backend/scripts/backfill_usage.py index d174a70b..d4a42ff3 100644 --- a/backend/scripts/backfill_usage.py +++ b/backend/scripts/backfill_usage.py @@ -16,7 +16,7 @@ Usage: Environment: MONGO_URI — connection string (falls back to localhost:27017 without auth) - DB_NAME — database name (default: semblance_db) + DB_NAME — database name (default: cohorta_db) """ import argparse @@ -107,7 +107,7 @@ def _estimate_cost(prompt_tokens: int, completion_tokens: int, model: str) -> di def connect(): mongo_uri = os.environ.get("MONGO_URI", "mongodb://localhost:27017") - db_name = os.environ.get("DB_NAME", "semblance_db") + db_name = os.environ.get("DB_NAME", "cohorta_db") try: client = MongoClient(mongo_uri, serverSelectionTimeoutMS=5000) client.admin.command("ping") diff --git a/backend/scripts/generate_architecture_doc.py b/backend/scripts/generate_architecture_doc.py index eccc32c7..47a26eb1 100644 --- a/backend/scripts/generate_architecture_doc.py +++ b/backend/scripts/generate_architecture_doc.py @@ -427,8 +427,8 @@ class ArchDocTemplate(BaseDocTemplate): filename, pagesize=A4, leftMargin=MARGIN_LEFT, rightMargin=MARGIN_RIGHT, topMargin=MARGIN_TOP, bottomMargin=MARGIN_BOTTOM, - title="Semblance Technical Architecture", - author="Semblance", + title="Cohorta Technical Architecture", + author="Cohorta", ) frame = Frame( MARGIN_LEFT, MARGIN_BOTTOM, CONTENT_WIDTH, FRAME_HEIGHT, id="main", @@ -484,7 +484,7 @@ class ArchDocTemplate(BaseDocTemplate): # Title c.setFillColor(white) c.setFont(FONTS["heading"], 44) - c.drawCentredString(w / 2, 440, "Semblance") + c.drawCentredString(w / 2, 440, "Cohorta") # Subtitle c.setFont(FONTS["body"], 20) @@ -624,11 +624,11 @@ DIAGRAMS = { "deployment_architecture": """graph TB User["User Browser"] - subgraph Production["Production Server (optical-dev.oliver.solutions)"] + subgraph Production["Production Server (cohorta.ai-impress.com)"] Nginx["Nginx
Reverse Proxy"] subgraph Static["Static Assets"] - Vite["Vite Build
/semblance/"] + Vite["Vite Build"] end subgraph App["Application Server"] @@ -647,8 +647,8 @@ DIAGRAMS = { end User -->|"HTTPS"| Nginx - Nginx -->|"/semblance/*"| Vite - Nginx -->|"/semblance_back/*"| Hypercorn + Nginx -->|"/*"| Vite + Nginx -->|"/*"| Hypercorn Hypercorn --> Quart Hypercorn --> SocketIO Quart --> Mongo @@ -1232,7 +1232,7 @@ def build_chapter_2(rendered): e.append(h2("Deployment Topology", "ch2_deploy")) e.append(p( - "In production, the application is deployed at optical-dev.oliver.solutions behind an " + "In production, the application is deployed at cohorta.ai-impress.com behind an " "Nginx reverse proxy that routes requests to either the static frontend assets or the backend " "application server." )) @@ -1247,12 +1247,12 @@ def build_chapter_2(rendered): e.append(styled_table( ["Setting", "Development", "Production"], [ - ["Base Path", "/", "/semblance/"], - ["API Base URL", "/api", "https://optical-dev.oliver.solutions/semblance_back/api"], - ["WebSocket Path", "/socket.io/", "/semblance_back/socket.io/"], + ["Base Path", "/", "/"], + ["API Base URL", "/api", "https://cohorta.ai-impress.com/api"], + ["WebSocket Path", "/socket.io/", "/socket.io/"], ["Frontend Port", "5173 (Vite dev server)", "Static assets via Nginx"], ["Backend Port", "5137 (Hypercorn)", "5137 (proxied via Nginx)"], - ["MSAL Redirect", "http://localhost:5173/", "https://optical-dev.oliver.solutions/semblance"], + [" ], col_widths=[110, (CONTENT_WIDTH - 118) / 2, (CONTENT_WIDTH - 118) / 2], )) @@ -2201,11 +2201,11 @@ def build_chapter_11(rendered): def build_architecture_doc(output_path): print("=" * 60) - print("Building Semblance Technical Architecture Document") + print("Building Cohorta Technical Architecture Document") print("=" * 60) # Create temp directory for Mermaid diagrams - diagram_dir = tempfile.mkdtemp(prefix="semblance_diagrams_") + diagram_dir = tempfile.mkdtemp(prefix="cohorta_diagrams_") print(f"\n Diagram directory: {diagram_dir}") # Step 1: Render Mermaid diagrams @@ -2267,6 +2267,6 @@ def build_architecture_doc(output_path): if __name__ == "__main__": output = sys.argv[1] if len(sys.argv) > 1 else os.path.join( os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), - "semblance_architecture.pdf" + "cohorta_architecture.pdf" ) build_architecture_doc(output) diff --git a/backend/scripts/populate_db.py b/backend/scripts/populate_db.py index 47b04520..eff9665c 100755 --- a/backend/scripts/populate_db.py +++ b/backend/scripts/populate_db.py @@ -22,7 +22,7 @@ def get_script_db(): if mongo_uri: try: client = MongoClient(mongo_uri, serverSelectionTimeoutMS=5000) - db = client.semblance_db + db = client.cohorta_db db.command('ping') print("Successfully connected to MongoDB using MONGO_URI") return client, db @@ -33,7 +33,7 @@ def get_script_db(): # Try connecting without auth first try: client = MongoClient('mongodb://localhost:27017', serverSelectionTimeoutMS=2000) - db = client.semblance_db + db = client.cohorta_db db.command('ping') print("Successfully connected to MongoDB without authentication") return client, db @@ -47,9 +47,9 @@ def get_script_db(): try: from urllib.parse import quote_plus - uri = f"mongodb://{quote_plus(username)}:{quote_plus(password)}@localhost:27017/semblance_db?authSource=admin" + uri = f"mongodb://{quote_plus(username)}:{quote_plus(password)}@localhost:27017/cohorta_db?authSource=admin" client = MongoClient(uri, serverSelectionTimeoutMS=5000) - db = client.semblance_db + db = client.cohorta_db db.command('ping') print("Successfully connected to MongoDB with credentials") return client, db diff --git a/backend/scripts/populate_db_direct.py b/backend/scripts/populate_db_direct.py index 52e42211..f931132f 100755 --- a/backend/scripts/populate_db_direct.py +++ b/backend/scripts/populate_db_direct.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ -Standalone script to populate MongoDB with sample data for the Semblance Synthetic Society app. +Standalone script to populate MongoDB with sample data for the Cohorta Synthetic Focus Groups app. This script doesn't rely on the backend modules and connects directly to MongoDB. """ @@ -377,7 +377,7 @@ def connect_to_mongodb(): if mongo_uri: try: client = MongoClient(mongo_uri, serverSelectionTimeoutMS=5000) - db = client.semblance_db + db = client.cohorta_db db.command('ping') print("Successfully connected to MongoDB using MONGO_URI") return client, db @@ -388,7 +388,7 @@ def connect_to_mongodb(): # Try connecting without auth (development environments) try: client = MongoClient('mongodb://localhost:27017', serverSelectionTimeoutMS=2000) - db = client.semblance_db + db = client.cohorta_db db.command('ping') print("Successfully connected to MongoDB without authentication") return client, db @@ -402,9 +402,9 @@ def connect_to_mongodb(): try: from urllib.parse import quote_plus - uri = f"mongodb://{quote_plus(username)}:{quote_plus(password)}@localhost:27017/semblance_db?authSource=admin" + uri = f"mongodb://{quote_plus(username)}:{quote_plus(password)}@localhost:27017/cohorta_db?authSource=admin" client = MongoClient(uri, serverSelectionTimeoutMS=5000) - db = client.semblance_db + db = client.cohorta_db db.command('ping') print("Successfully connected to MongoDB with provided credentials") return client, db diff --git a/backend/scripts/setup_mongodb.sh b/backend/scripts/setup_mongodb.sh index f73bb835..b78c5db0 100755 --- a/backend/scripts/setup_mongodb.sh +++ b/backend/scripts/setup_mongodb.sh @@ -61,11 +61,11 @@ else echo -e "${GREEN}MongoDB is already running.${NC}" fi -echo -e "${YELLOW}Creating semblance_db database and collections...${NC}" +echo -e "${YELLOW}Creating cohorta_db database and collections...${NC}" # S-M3: Use mongosh instead of deprecated mongo CLI mongosh --eval ' - db = db.getSiblingDB("semblance_db"); + db = db.getSiblingDB("cohorta_db"); db.createCollection("users"); db.createCollection("personas"); db.createCollection("focus_groups"); diff --git a/semblance_app_documentation.md b/cohorta_app_documentation.md similarity index 98% rename from semblance_app_documentation.md rename to cohorta_app_documentation.md index f2f43a8f..11765d5f 100755 --- a/semblance_app_documentation.md +++ b/cohorta_app_documentation.md @@ -1,4 +1,4 @@ -# Semblance Synthetic Society - Application Documentation +# Cohorta - Application Documentation ## Table of Contents 1. [Application Overview](#application-overview) @@ -14,7 +14,7 @@ ## Application Overview -Semblance Synthetic Society is an AI-powered platform for creating and managing synthetic personas for focus groups and market research. It enables researchers to: +Cohorta is an AI-powered platform for creating and managing synthetic personas for focus groups and market research. It enables researchers to: - Create detailed synthetic personas with demographic profiles and personality traits - Organize personas into focus groups @@ -432,7 +432,7 @@ gunicorn -w 4 "app:create_app()" Frontend (`.env`): ``` -VITE_API_BASE_URL=/semblance_back/api +VITE_API_BASE_URL=/api ``` Backend: diff --git a/public/vercel.json b/public/vercel.json index 4a9f6106..29c0269f 100755 --- a/public/vercel.json +++ b/public/vercel.json @@ -1,8 +1,8 @@ { "rewrites": [ { - "source": "/semblance/(.*)", - "destination": "/semblance/index.html" + "source": "/(.*)", + "destination": "/index.html" } ] } \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 157464ec..9e494f4b 100755 --- a/src/App.tsx +++ b/src/App.tsx @@ -21,7 +21,8 @@ import AdminRoute from "./components/admin/AdminRoute"; import { AuthProvider } from "./contexts/AuthContext"; import { NavigationProvider } from "./contexts/NavigationContext"; import { WebSocketProvider } from "./contexts/WebSocketContextNew"; -// CSS for consistent back button positioning +import PublicLayout from "./components/layout/PublicLayout"; +import AppLayout from "./components/layout/AppLayout"; import "./styles/backButton.css"; const queryClient = new QueryClient(); @@ -35,21 +36,41 @@ const App = () => ( - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + {/* Public pages — with landing header + footer */} + }> + } /> + } /> + } /> + } /> + } /> + + + {/* App pages — with app header, no footer */} + + + + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + {/* Admin */} + + + + + + } /> + } /> - } /> - } /> - } /> diff --git a/src/components/FeatureCard.tsx b/src/components/FeatureCard.tsx deleted file mode 100755 index b056cd29..00000000 --- a/src/components/FeatureCard.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { LucideIcon } from 'lucide-react'; -import { cn } from '@/lib/utils'; - -interface FeatureCardProps { - title: string; - description: string; - icon: LucideIcon; - className?: string; - gradient?: string; -} - -export default function FeatureCard({ - title, - description, - icon: Icon, - className, - gradient = 'from-[#06B6D4] to-[#8B5CF6]', -}: FeatureCardProps) { - return ( -
-
- -
- -

- {title} -

-

{description}

-
- ); -} diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx deleted file mode 100755 index 4b054814..00000000 --- a/src/components/Hero.tsx +++ /dev/null @@ -1,233 +0,0 @@ -import { ArrowRight, Sparkles, Users, BarChart3, Shield } from 'lucide-react'; -import { Link } from 'react-router-dom'; - -const StatBadge = ({ value, label }: { value: string; label: string }) => ( -
- - {value} - - {label} -
-); - -export default function Hero() { - return ( -
- {/* Background orbs */} -
-
-
- - {/* Grid overlay */} -
- -
-
- {/* Left — copy */} -
- {/* Badge */} -
- - AI-Powered Synthetic Research -
- -

- Research with{' '} - - Synthetic -
- Personas -
-

- -

- Conduct market research and user interviews using AI-powered synthetic personas. - Gain deep insights without the cost and time of traditional methods. -

- -
- { - (e.currentTarget as HTMLElement).style.boxShadow = '0 0 40px hsl(188 91% 44% / 0.5)'; - (e.currentTarget as HTMLElement).style.transform = 'translateY(-2px)'; - }} - onMouseLeave={e => { - (e.currentTarget as HTMLElement).style.boxShadow = '0 0 24px hsl(188 91% 44% / 0.3)'; - (e.currentTarget as HTMLElement).style.transform = 'translateY(0)'; - }} - > - Start for free - - - - See how it works - -
- - {/* Stats row */} -
- -
- -
- -
-
- - {/* Right — mock UI */} -
-
- {/* Floating feature badges */} -
- - 42 personas active -
- -
- - Insights ready -
- - {/* Main mock card */} -
- {/* Window bar */} -
-
-
-
- Focus Group Session -
- - {/* Chat content */} -
- {[ - { role: 'mod', text: 'What qualities matter most when choosing a premium brand?' }, - { role: 'user', name: 'Sarah, 32', text: 'Sustainability and transparency in sourcing. I want to trust the brand.' }, - { role: 'mod', text: 'How does packaging influence your perception?' }, - { role: 'user', name: 'Marcus, 28', text: 'Minimalist design signals confidence. Loud packaging feels insecure.' }, - { role: 'user', name: 'Priya, 35', text: 'Agreed — and eco-friendly materials are now a baseline expectation for me.' }, - ].map((msg, i) => ( -
-
- {msg.name && ( -
{msg.name}
- )} - {msg.text} -
-
- ))} -
- - {/* Input bar */} -
-
-
- -
-
-
- - {/* Trust badge */} -
- - SOC 2 Ready -
-
-
-
-
-
- ); -} diff --git a/src/components/Navigation.tsx b/src/components/Navigation.tsx deleted file mode 100755 index 1244eeba..00000000 --- a/src/components/Navigation.tsx +++ /dev/null @@ -1,275 +0,0 @@ -import { useState, useEffect } from 'react'; -import { Link, useLocation, useNavigate } from 'react-router-dom'; -import { Menu, X, LayoutDashboard, Users, MessageSquare, Home, LogIn, LogOut, ShieldCheck, CreditCard, Zap } from 'lucide-react'; -import { cn } from '@/lib/utils'; -import { useAuth } from '@/contexts/AuthContext'; -import { billingApi } from '@/lib/api'; - -const LogoMark = () => ( - - - - - - - - - - - -); - -export default function Navigation() { - const [mobileMenuOpen, setMobileMenuOpen] = useState(false); - const [creditsBalance, setCreditsBalance] = useState(null); - const [scrolled, setScrolled] = useState(false); - const location = useLocation(); - const navigate = useNavigate(); - const { isAuthenticated, logout, user } = useAuth(); - - useEffect(() => { - const onScroll = () => setScrolled(window.scrollY > 12); - window.addEventListener('scroll', onScroll, { passive: true }); - return () => window.removeEventListener('scroll', onScroll); - }, []); - - useEffect(() => { - if (!isAuthenticated) { setCreditsBalance(null); return; } - billingApi.getBalance().then(r => setCreditsBalance(r.data.credits_balance)).catch(() => {}); - }, [isAuthenticated, location.pathname]); - - const navigationItems = [ - { name: 'Home', href: '/', icon: Home }, - { name: 'Personas', href: '/synthetic-users', icon: Users }, - { name: 'Focus Groups', href: '/focus-groups', icon: MessageSquare }, - { name: 'Dashboard', href: '/dashboard', icon: LayoutDashboard }, - { name: 'Billing', href: '/billing', icon: CreditCard }, - ]; - - const isActive = (path: string) => location.pathname === path; - - const handleAuthNavigation = (path: string) => { - if (path === '/synthetic-users') { - window.dispatchEvent(new CustomEvent('syntheticUsersNavigation')); - } - navigate(path); - }; - - return ( -
-
-
- {/* Logo */} - - - - Cohorta - - - - {/* Desktop Nav */} - - - {/* Right side */} -
- {isAuthenticated && creditsBalance !== null && ( - - )} - - {isAuthenticated ? ( - - ) : ( -
- - Sign in - - - Get Started - -
- )} -
- - {/* Mobile toggle */} - -
-
- - {/* Mobile menu */} - {mobileMenuOpen && ( -
-
- {navigationItems.map((item) => ( -
- {item.href === '/' ? ( - setMobileMenuOpen(false)} - > - - {item.name} - - ) : ( - - )} -
- ))} - - {user?.role === 'admin' && ( - - )} - -
- {isAuthenticated ? ( - - ) : ( -
- setMobileMenuOpen(false)} - > - - Sign in - - setMobileMenuOpen(false)} - > - Get Started - -
- )} -
-
-
- )} -
- ); -} diff --git a/src/components/brand/Logo.tsx b/src/components/brand/Logo.tsx new file mode 100644 index 00000000..68197bc7 --- /dev/null +++ b/src/components/brand/Logo.tsx @@ -0,0 +1,47 @@ +import { cn } from '@/lib/utils'; + +interface LogoProps { + className?: string; + withWordmark?: boolean; + size?: 'sm' | 'md' | 'lg'; +} + +const sizes = { sm: 'h-6 w-6', md: 'h-8 w-8', lg: 'h-10 w-10' }; + +export default function Logo({ className, withWordmark = false, size = 'md' }: LogoProps) { + return ( +
+ + + + + + + + {/* Hexagonal mark */} + + + + {withWordmark && ( + + Cohorta + + )} +
+ ); +} diff --git a/src/components/brand/UserDropdown.tsx b/src/components/brand/UserDropdown.tsx new file mode 100644 index 00000000..5d3af952 --- /dev/null +++ b/src/components/brand/UserDropdown.tsx @@ -0,0 +1,61 @@ +import { useNavigate } from 'react-router-dom'; +import { useAuth } from '@/contexts/AuthContext'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { User, CreditCard, BarChart2, LogOut } from 'lucide-react'; + +export default function UserDropdown() { + const { user, logout } = useAuth(); + const navigate = useNavigate(); + + if (!user) return null; + + const initials = user.username?.slice(0, 2).toUpperCase() || 'U'; + + return ( + + + + + +
+

{user.username}

+

{user.email}

+
+ + navigate('/dashboard')}> + + My account + + navigate('/billing')}> + + Billing + + navigate('/usage')}> + + Usage + + + { logout(); navigate('/'); }} + className="text-destructive focus:text-destructive" + > + + Sign out + +
+
+ ); +} diff --git a/src/components/dashboard/Dashboard.tsx b/src/components/dashboard/Dashboard.tsx index a9ef4bba..4464fa6e 100755 --- a/src/components/dashboard/Dashboard.tsx +++ b/src/components/dashboard/Dashboard.tsx @@ -1,74 +1,461 @@ +import { useState, useEffect, useCallback } from 'react'; +import { useNavigate, Link } from 'react-router-dom'; +import { useAuth } from '@/contexts/AuthContext'; +import { billingApi, personasApi, focusGroupsApi, authApi } from '@/lib/api'; +import { useMyUsage } from '@/hooks/useMyUsage'; +import { + Users, MessageSquare, Zap, CreditCard, BarChart2, ArrowRight, + Plus, CheckCircle2, AlertCircle, Clock, TrendingUp, Activity, Loader2 +} from 'lucide-react'; +import { toastService } from '@/lib/toast'; +import { Badge } from '@/components/ui/badge'; +import api from '@/lib/api'; -import { useState } from 'react'; -import { Users, MessageSquare, Lightbulb } from 'lucide-react'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import Navigation from '@/components/Navigation'; -import DashboardHeader from '@/components/dashboard/DashboardHeader'; -import StatCard from '@/components/dashboard/StatCard'; -import OverviewTab from '@/components/dashboard/OverviewTab'; -import UsersTab from '@/components/dashboard/UsersTab'; -import FocusGroupsTab from '@/components/dashboard/FocusGroupsTab'; +// ────────────────────────────────────────────── +// Types +// ────────────────────────────────────────────── + +interface BalanceData { + credits_balance: number; + persona_cost: number; + run_cost: number; +} + +interface Transaction { + _id: string; + type: string; + amount: number; + balance_after: number; + created_at: string; + description?: string; +} + +interface Persona { + _id: string; + name: string; + created_at: string; +} + +interface FocusGroup { + _id: string; + title: string; + status: string; +} + +interface UserTask { + task_id: string; + task_type: string; + status: string; + progress?: number; + created_at: string; +} + +// ────────────────────────────────────────────── +// Helpers +// ────────────────────────────────────────────── + +const TX_TYPE_LABELS: Record = { + purchase: 'Purchase', + grant: 'Free grant', + admin_grant: 'Admin grant', + debit: 'Used', + refund: 'Refund', +}; + +const TX_TYPE_COLORS: Record = { + purchase: 'text-primary', + grant: 'text-green-400', + admin_grant: 'text-green-400', + debit: 'text-red-400', + refund: 'text-blue-400', +}; + +function formatDate(iso: string) { + const d = new Date(iso); + return d.toLocaleDateString('en-GB', { day: 'numeric', month: 'short' }); +} + +// ────────────────────────────────────────────── +// Stat Card +// ────────────────────────────────────────────── + +function StatCard({ + icon: Icon, title, value, sub, action, actionLabel, loading, +}: { + icon: React.ElementType; + title: string; + value: React.ReactNode; + sub?: string; + action?: () => void; + actionLabel?: string; + loading?: boolean; +}) { + return ( +
+
+
+ +
+ {action && actionLabel && ( + + )} +
+
+

{title}

+ {loading + ?
+ :
{value}
+ } + {sub &&

{sub}

} +
+
+ ); +} + +// ────────────────────────────────────────────── +// Quick action card +// ────────────────────────────────────────────── + +function QuickAction({ icon: Icon, title, desc, to }: { + icon: React.ElementType; title: string; desc: string; to: string; +}) { + return ( + +
+ +
+
+

{title}

+

{desc}

+
+ + + ); +} + +// ────────────────────────────────────────────── +// Main component +// ────────────────────────────────────────────── const Dashboard = () => { - const [activeTab, setActiveTab] = useState('overview'); + const { user } = useAuth(); + const navigate = useNavigate(); + const { data: usageData, isLoading: usageLoading } = useMyUsage(); + + const [balance, setBalance] = useState(null); + const [balanceLoading, setBalanceLoading] = useState(true); + const [transactions, setTransactions] = useState([]); + const [txLoading, setTxLoading] = useState(true); + const [personasCount, setPersonasCount] = useState(null); + const [fgCount, setFgCount] = useState(null); + const [activeFgCount, setActiveFgCount] = useState(0); + const [recentPersonas, setRecentPersonas] = useState([]); + const [activeTasks, setActiveTasks] = useState([]); + const [quotaExceeded, setQuotaExceeded] = useState(false); + const [resendingEmail, setResendingEmail] = useState(false); + + const loadData = useCallback(async () => { + // Balance + billingApi.getBalance() + .then(r => { setBalance(r.data); setBalanceLoading(false); }) + .catch(() => setBalanceLoading(false)); + + // Transactions (last 5) + billingApi.getTransactions(5) + .then(r => { setTransactions(r.data.transactions || []); setTxLoading(false); }) + .catch(() => setTxLoading(false)); + + // Personas + personasApi.getAll() + .then(r => { + const all: Persona[] = r.data.personas || r.data || []; + setPersonasCount(all.length); + setRecentPersonas(all.slice(0, 5)); + }) + .catch(() => {}); + + // Focus groups + focusGroupsApi.getAll() + .then(r => { + const all: FocusGroup[] = r.data.focus_groups || r.data || []; + setFgCount(all.length); + setActiveFgCount(all.filter(fg => fg.status === 'active' || fg.status === 'in_progress').length); + }) + .catch(() => {}); + + // Active tasks + api.get('/tasks/user/me') + .then(r => { + const tasks: UserTask[] = r.data.tasks || r.data || []; + setActiveTasks(tasks.filter(t => t.status === 'running' || t.status === 'pending')); + }) + .catch(() => {}); + }, []); + + useEffect(() => { + loadData(); + + // Listen for quota exceeded events + const onQuota = () => setQuotaExceeded(true); + window.addEventListener('quota_exceeded', onQuota); + return () => window.removeEventListener('quota_exceeded', onQuota); + }, [loadData]); + + const handleResendEmail = async () => { + setResendingEmail(true); + try { + await authApi.resendVerification(); + toastService.success('Verification email sent', { description: 'Check your inbox' }); + } catch { + toastService.error('Failed to send email'); + } finally { + setResendingEmail(false); + } + }; return ( -
- - -
- - -
- +
+ + {/* ── Greeting ── */} +
+
+

+ Welcome back, {user?.username} +

+
+ {user?.role === 'admin' && ( + Admin + )} + + {balance?.credits_balance ?? '…'} credits remaining + +
+
+ +
+ + {/* ── Banners ── */} + {user && !(user as any).email_verified && ( +
+
+ +

+ Please verify your email address to unlock all features. +

+
+ +
+ )} + + {quotaExceeded && ( +
+
+ +

Credit quota exceeded. Top up to continue.

+
+ +
+ )} + + {/* ── Stat row ── */} +
+ navigate('/billing')} + actionLabel="Top up" + loading={balanceLoading} + /> + navigate('/usage')} + actionLabel="Details" + loading={usageLoading} + /> + navigate('/synthetic-users')} + actionLabel="View all" + loading={personasCount === null} /> - - - 0 ? `${activeFgCount} active now` : 'None active'} + action={() => navigate('/focus-groups')} + actionLabel="View all" + loading={fgCount === null} />
- - - - Overview - Synthetic Users - Focus Groups - - - - - - - - - - - - - - -
+ + {/* ── Quick actions ── */} +
+ + + +
+ + {/* ── Active tasks + Recent activity ── */} +
+ + {/* Active tasks */} +
+
+ +

Running tasks

+
+ {activeTasks.length === 0 ? ( +
+ +

No active tasks

+
+ ) : ( +
+ {activeTasks.map(task => ( +
+ +
+

+ {task.task_type.replace(/_/g, ' ')} +

+

{formatDate(task.created_at)}

+
+ {task.progress !== undefined && ( + {task.progress}% + )} +
+ ))} +
+ )} +
+ + {/* Recent transactions */} +
+
+
+ +

Recent transactions

+
+ + View all → + +
+ {txLoading ? ( +
+ {[...Array(4)].map((_, i) => ( +
+ ))} +
+ ) : transactions.length === 0 ? ( +

No transactions yet

+ ) : ( +
+ {transactions.map(tx => ( +
+
+

+ {TX_TYPE_LABELS[tx.type] || tx.type} +

+

{formatDate(tx.created_at)}

+
+
+

+ {tx.amount > 0 ? '+' : ''}{tx.amount} cr +

+

{tx.balance_after} cr left

+
+
+ ))} +
+ )} +
+
+ + {/* ── Recent personas ── */} + {recentPersonas.length > 0 && ( +
+
+
+ +

Recent personas

+
+ + View all → + +
+
+ {recentPersonas.map(p => ( + +
+ {p.name?.[0] || '?'} +
+ {p.name} + + ))} +
+
+ )} + + {/* ── Admin link ── */} + {user?.role === 'admin' && ( +
+
+ +
+

Admin panel

+

Manage users, usage, pricing, and analytics

+
+
+ + Open admin → + +
+ )} + +
); }; diff --git a/src/components/focus-group-session/ReasoningPanel.tsx b/src/components/focus-group-session/ReasoningPanel.tsx index ef73b0b4..2945ca78 100755 --- a/src/components/focus-group-session/ReasoningPanel.tsx +++ b/src/components/focus-group-session/ReasoningPanel.tsx @@ -28,7 +28,7 @@ const ActionIcon = ({ action }: { action: string }) => { case 'participant_respond': return ; case 'participant_interaction': - return ; + return ; case 'probe_trigger': return ; case 'end_session': @@ -172,7 +172,7 @@ const ReasoningPanel = ({ reasoningHistory, isVisible, onToggle, isAiMode = fals
- + {isAiMode ? "AI Decision Reasoning" : "AI Moderator Logic"} diff --git a/src/components/layout/AppLayout.tsx b/src/components/layout/AppLayout.tsx new file mode 100644 index 00000000..91027ae6 --- /dev/null +++ b/src/components/layout/AppLayout.tsx @@ -0,0 +1,125 @@ +import { useState, useEffect } from 'react'; +import { Link, NavLink, useNavigate, Outlet } from 'react-router-dom'; +import { useAuth } from '@/contexts/AuthContext'; +import { billingApi } from '@/lib/api'; +import Logo from '@/components/brand/Logo'; +import UserDropdown from '@/components/brand/UserDropdown'; +import { Users, MessageSquare, LayoutDashboard, CreditCard, ShieldCheck, Zap, Menu, X } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +const appNavItems = [ + { label: 'Dashboard', to: '/dashboard', icon: LayoutDashboard }, + { label: 'Personas', to: '/synthetic-users', icon: Users }, + { label: 'Focus Groups', to: '/focus-groups', icon: MessageSquare }, + { label: 'Billing', to: '/billing', icon: CreditCard }, +]; + +export default function AppLayout({ children }: { children?: React.ReactNode }) { + const { user, isAuthenticated } = useAuth(); + const navigate = useNavigate(); + const [credits, setCredits] = useState(null); + const [mobileOpen, setMobileOpen] = useState(false); + + useEffect(() => { + if (!isAuthenticated) return; + billingApi.getBalance().then(r => setCredits(r.data.credits_balance)).catch(() => {}); + }, [isAuthenticated]); + + return ( +
+ {/* App header */} +
+
+ {/* Logo */} + + + + + {/* Desktop nav */} + + + {/* Right side */} +
+ {credits !== null && ( + + )} + + +
+
+ + {/* Mobile menu */} + {mobileOpen && ( +
+ {appNavItems.map(({ label, to, icon: Icon }) => ( + + cn( + 'flex items-center gap-2.5 px-3 py-2.5 rounded-xl text-sm font-medium', + isActive ? 'text-primary bg-primary/10' : 'text-muted-foreground hover:text-foreground hover:bg-secondary/60' + ) + } + onClick={() => setMobileOpen(false)} + > + + {label} + + ))} +
+ )} +
+ + {/* Page content — supports both nested and explicit children */} +
+ {children ?? } +
+
+ ); +} diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx new file mode 100644 index 00000000..6344cf24 --- /dev/null +++ b/src/components/layout/Footer.tsx @@ -0,0 +1,73 @@ +import { Link } from 'react-router-dom'; +import Logo from '@/components/brand/Logo'; + +const footerLinks = { + Platform: [ + { label: 'Synthetic Personas', to: '/synthetic-users' }, + { label: 'Focus Groups', to: '/focus-groups' }, + { label: 'Dashboard', to: '/dashboard' }, + { label: 'Billing & Credits', to: '/billing' }, + ], + Company: [ + { label: 'About', to: '#' }, + { label: 'Blog', to: '#' }, + { label: 'Careers', to: '#' }, + { label: 'Contact', to: '#' }, + ], + Legal: [ + { label: 'Privacy Policy', to: '#' }, + { label: 'Terms of Service', to: '#' }, + { label: 'Cookie Policy', to: '#' }, + { label: 'GDPR', to: '#' }, + ], +}; + +export default function Footer() { + return ( +
+
+
+ {/* Brand */} +
+ +

+ AI-powered synthetic research platform. Generate personas, run focus groups, extract insights — in minutes. +

+

+ Powered by{' '} + AImpress LTD +

+
+ + {/* Link columns */} + {Object.entries(footerLinks).map(([group, links]) => ( +
+

{group}

+
    + {links.map(({ label, to }) => ( +
  • + + {label} + +
  • + ))} +
+
+ ))} +
+ +
+

+ © {new Date().getFullYear()} AImpress LTD. All rights reserved. +

+

+ Cohorta is a product of AImpress LTD +

+
+
+
+ ); +} diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx new file mode 100644 index 00000000..438375b7 --- /dev/null +++ b/src/components/layout/Header.tsx @@ -0,0 +1,157 @@ +import { useState, useEffect } from 'react'; +import { Link, NavLink, useNavigate } from 'react-router-dom'; +import { useAuth } from '@/contexts/AuthContext'; +import Logo from '@/components/brand/Logo'; +import UserDropdown from '@/components/brand/UserDropdown'; +import { Menu, X } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +const navLinks = [ + { label: 'Home', to: '/' }, + { label: 'Product', to: '/#product' }, + { label: 'Pricing', to: '/#pricing' }, + { label: 'Blog', to: '/blog' }, + { label: 'Contact', to: '/contact' }, +]; + +export default function Header() { + const { isAuthenticated } = useAuth(); + const navigate = useNavigate(); + const [scrolled, setScrolled] = useState(false); + const [mobileOpen, setMobileOpen] = useState(false); + + useEffect(() => { + const onScroll = () => setScrolled(window.scrollY > 24); + window.addEventListener('scroll', onScroll, { passive: true }); + return () => window.removeEventListener('scroll', onScroll); + }, []); + + return ( +
+
+
+ {/* Logo */} + + + + + {/* Pill nav — desktop */} + + + {/* Right side */} +
+ {/* Language switcher */} +
+ 🌐 + Eng +
+ + {isAuthenticated ? ( + + ) : ( + <> + + Log in + + + + )} +
+ + {/* Mobile toggle */} + +
+ + {/* Mobile menu */} + {mobileOpen && ( +
+ +
+ {isAuthenticated ? ( + setMobileOpen(false)}> + + + ) : ( + <> + setMobileOpen(false)} + > + Log in + + setMobileOpen(false)} + > + Get started free + + + )} +
+
+ )} +
+
+ ); +} diff --git a/src/components/layout/PublicLayout.tsx b/src/components/layout/PublicLayout.tsx new file mode 100644 index 00000000..1226bda0 --- /dev/null +++ b/src/components/layout/PublicLayout.tsx @@ -0,0 +1,15 @@ +import { Outlet } from 'react-router-dom'; +import Header from './Header'; +import Footer from './Footer'; + +export default function PublicLayout() { + return ( +
+
+
+ +
+
+
+ ); +} diff --git a/src/components/persona/PersonaPersonality.tsx b/src/components/persona/PersonaPersonality.tsx index f2544303..5afc6e24 100755 --- a/src/components/persona/PersonaPersonality.tsx +++ b/src/components/persona/PersonaPersonality.tsx @@ -49,7 +49,7 @@ export function PersonaPersonality({ persona }: PersonaPersonalityProps) { {oceanData[0].value}%
-
+

{oceanData[0].value > 75 ? 'Highly creative and curious' : diff --git a/src/components/persona/PersonaProfile.tsx b/src/components/persona/PersonaProfile.tsx index 012a1cba..f088c14a 100755 --- a/src/components/persona/PersonaProfile.tsx +++ b/src/components/persona/PersonaProfile.tsx @@ -1,6 +1,5 @@ import { useState, useEffect, useCallback } from 'react'; -import Navigation from '@/components/Navigation'; import { Button } from '@/components/ui/button'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { @@ -211,7 +210,7 @@ export default function PersonaProfile() { return (

- +
{isEditing ? ( diff --git a/src/components/persona/PersonaProfileSkeleton.tsx b/src/components/persona/PersonaProfileSkeleton.tsx index 32496973..cda5a35d 100755 --- a/src/components/persona/PersonaProfileSkeleton.tsx +++ b/src/components/persona/PersonaProfileSkeleton.tsx @@ -1,11 +1,10 @@ -import Navigation from '@/components/Navigation'; import { Card } from '@/components/ui/card'; import { Skeleton } from '@/components/ui/skeleton'; export function PersonaProfileSkeleton() { return (
- +
{/* Header with back button and title */} diff --git a/src/components/persona/PersonaSidebar.tsx b/src/components/persona/PersonaSidebar.tsx index 55b4aaad..adf04fee 100755 --- a/src/components/persona/PersonaSidebar.tsx +++ b/src/components/persona/PersonaSidebar.tsx @@ -100,7 +100,7 @@ export function PersonaSidebar({ persona }: PersonaSidebarProps) { {persona.brandLoyalty}%
-
+
)} diff --git a/src/index.css b/src/index.css index d519727c..03d52f5e 100755 --- a/src/index.css +++ b/src/index.css @@ -1,4 +1,4 @@ -@import url('https://fonts.googleapis.com/css2?family=Syne:wght@400;500;600;700;800&family=Inter:wght@300;400;500;600;700&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700;800&family=Inter:wght@300;400;500;600;700&display=swap'); @tailwind base; @tailwind components; @@ -6,47 +6,47 @@ @layer base { :root { - /* Cohorta — dark navy + cyan palette (AImpress style) */ - --background: 222 47% 4%; - --foreground: 210 40% 96%; + /* Cohorta — AIMPRESS rebrand: warm dark + orange */ + --background: 220 22% 12%; + --foreground: 30 18% 92%; - --card: 222 45% 7%; - --card-foreground: 210 40% 96%; + --card: 220 20% 16%; + --card-foreground: 30 18% 92%; - --popover: 222 45% 7%; - --popover-foreground: 210 40% 96%; + --popover: 220 22% 14%; + --popover-foreground: 30 18% 92%; - /* Cyan primary */ - --primary: 188 91% 44%; - --primary-foreground: 222 47% 4%; + /* Orange primary */ + --primary: 28 78% 56%; + --primary-foreground: 220 25% 10%; - --secondary: 222 38% 13%; - --secondary-foreground: 210 40% 80%; + --secondary: 220 18% 20%; + --secondary-foreground: 30 12% 78%; - --muted: 222 38% 11%; - --muted-foreground: 215 20% 50%; + --muted: 220 16% 22%; + --muted-foreground: 30 10% 58%; - /* Violet accent */ - --accent: 258 89% 66%; - --accent-foreground: 0 0% 100%; + /* Orange accent — used for full-bleed orange band sections */ + --accent: 28 85% 60%; + --accent-foreground: 220 25% 10%; --destructive: 0 84% 60%; --destructive-foreground: 0 0% 100%; - --border: 222 38% 16%; - --input: 222 38% 13%; - --ring: 188 91% 44%; + --border: 220 16% 24%; + --input: 220 18% 18%; + --ring: 28 78% 56%; - --radius: 0.75rem; + --radius: 1rem; - --sidebar-background: 222 47% 4%; - --sidebar-foreground: 210 40% 96%; - --sidebar-primary: 188 91% 44%; - --sidebar-primary-foreground: 222 47% 4%; - --sidebar-accent: 222 38% 11%; - --sidebar-accent-foreground: 210 40% 80%; - --sidebar-border: 222 38% 16%; - --sidebar-ring: 188 91% 44%; + --sidebar-background: 220 22% 10%; + --sidebar-foreground: 30 18% 92%; + --sidebar-primary: 28 78% 56%; + --sidebar-primary-foreground: 220 25% 10%; + --sidebar-accent: 220 16% 20%; + --sidebar-accent-foreground: 30 12% 78%; + --sidebar-border: 220 16% 22%; + --sidebar-ring: 28 78% 56%; } } @@ -62,7 +62,7 @@ } h1, h2, h3, h4 { - font-family: 'Syne', system-ui, sans-serif; + font-family: 'Space Grotesk', system-ui, sans-serif; } ::-webkit-scrollbar { @@ -75,42 +75,42 @@ } ::-webkit-scrollbar-thumb { - background: hsl(222 38% 20%); + background: hsl(220 16% 28%); border-radius: 9999px; } ::-webkit-scrollbar-thumb:hover { - background: hsl(188 91% 44% / 0.5); + background: hsl(28 78% 56% / 0.5); } } @layer components { - /* Glass cards — dark */ + /* Glass cards — warm dark */ .glass-card { - background: linear-gradient(135deg, hsl(222 45% 9% / 0.8), hsl(222 45% 7% / 0.9)); + background: linear-gradient(135deg, hsl(220 20% 18% / 0.85), hsl(220 22% 14% / 0.9)); backdrop-filter: blur(12px); - border: 1px solid hsl(222 38% 20% / 0.6); - box-shadow: 0 4px 24px hsl(222 47% 2% / 0.4); + border: 1px solid hsl(220 16% 26% / 0.6); + box-shadow: 0 4px 24px hsl(220 25% 6% / 0.45); } .glass-panel { - background: hsl(222 45% 8% / 0.7); + background: hsl(220 20% 16% / 0.75); backdrop-filter: blur(16px); - border: 1px solid hsl(222 38% 18% / 0.5); + border: 1px solid hsl(220 16% 24% / 0.5); } - /* Gradient text — cyan to violet */ + /* Gradient text — orange to warm amber */ .text-gradient { - background: linear-gradient(135deg, #06B6D4 0%, #8B5CF6 100%); + background: linear-gradient(135deg, hsl(28 78% 56%) 0%, hsl(38 90% 65%) 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } - /* Gradient border card */ + /* Gradient border card — orange glow on hover */ .gradient-border-card { position: relative; - background: hsl(222 45% 7%); + background: hsl(220 20% 16%); border-radius: var(--radius); } .gradient-border-card::before { @@ -119,7 +119,7 @@ inset: 0; border-radius: var(--radius); padding: 1px; - background: linear-gradient(135deg, #06B6D4, #8B5CF6); + background: linear-gradient(135deg, hsl(28 78% 56%), hsl(38 90% 65%)); -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); -webkit-mask-composite: xor; mask-composite: exclude; @@ -130,33 +130,34 @@ opacity: 1; } - /* CTA gradient button */ + /* CTA primary button */ .btn-gradient { - background: linear-gradient(135deg, #06B6D4 0%, #7C3AED 100%); - color: white; + background: hsl(var(--primary)); + color: hsl(var(--primary-foreground)); border: none; transition: all 0.2s ease; - box-shadow: 0 0 20px hsl(188 91% 44% / 0.25); + box-shadow: 0 0 20px hsl(28 78% 56% / 0.3); } .btn-gradient:hover { - box-shadow: 0 0 32px hsl(188 91% 44% / 0.45); + background: hsl(28 78% 62%); + box-shadow: 0 0 32px hsl(28 78% 56% / 0.5); transform: translateY(-1px); } .btn-gradient:active { transform: translateY(0); } - /* Ghost button — white outline */ + /* Ghost button — subtle outline */ .btn-ghost-outline { background: transparent; - color: hsl(210 40% 96%); - border: 1px solid hsl(222 38% 25%); + color: hsl(var(--foreground)); + border: 1px solid hsl(220 16% 30%); transition: all 0.2s ease; } .btn-ghost-outline:hover { - border-color: hsl(188 91% 44% / 0.6); - color: #06B6D4; - background: hsl(188 91% 44% / 0.05); + border-color: hsl(28 78% 56% / 0.6); + color: hsl(28 78% 65%); + background: hsl(28 78% 56% / 0.06); } /* Animated glow orb */ @@ -167,6 +168,49 @@ pointer-events: none; } + /* Outline display text — huge hero-scale, stroke only */ + .outline-display { + color: transparent; + -webkit-text-stroke: 2px hsl(var(--foreground) / 0.15); + text-stroke: 2px hsl(var(--foreground) / 0.15); + user-select: none; + } + + /* Full-bleed orange band sections */ + .orange-band { + background: hsl(var(--accent)); + color: hsl(var(--accent-foreground)); + } + + /* Corner card — dark card with diagonal graphic overlay (like AIMPRESS stat cards) */ + .corner-card { + position: relative; + overflow: hidden; + background: hsl(var(--card)); + border: 1px solid hsl(var(--border)); + border-radius: var(--radius); + } + .corner-card::after { + content: ''; + position: absolute; + top: -30%; + right: -20%; + width: 60%; + height: 160%; + background: hsl(28 78% 56% / 0.06); + border-radius: 50%; + transform: rotate(-15deg); + pointer-events: none; + } + + /* Persona orb — circular image mask */ + .persona-orb { + aspect-ratio: 1; + border-radius: 9999px; + overflow: hidden; + flex-shrink: 0; + } + /* Persona card */ .persona-card { @apply relative overflow-hidden; @@ -178,19 +222,19 @@ } .persona-card:hover .persona-card-overlay { - background: hsl(188 91% 44% / 0.08); + background: hsl(28 78% 56% / 0.07); } .persona-card.selected .persona-card-overlay { - background: hsl(188 91% 44% / 0.10); + background: hsl(28 78% 56% / 0.10); } .persona-card-checkmark { @apply absolute top-3 left-3 z-20 opacity-0 transition-all duration-200 ease-out; - background: hsl(222 45% 12% / 0.9); + background: hsl(220 22% 14% / 0.9); border-radius: 9999px; padding: 4px; - border: 1px solid hsl(222 38% 22%); + border: 1px solid hsl(220 16% 26%); } .persona-card.selected .persona-card-checkmark { @@ -200,7 +244,7 @@ /* Sidebar utilities */ .sidebar-icon { @apply h-4 w-4 mr-3 mt-0.5 flex-shrink-0; - color: hsl(215 20% 50%); + color: hsl(30 10% 55%); } .sidebar-section { diff --git a/src/lib/api.ts b/src/lib/api.ts index 3ce14fc9..4f1760d9 100755 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -123,8 +123,11 @@ export const authApi = { register: (username: string, email: string, password: string) => api.post('/auth/register', { username, email, password }), - getProfile: () => - api.get('/auth/me') + getProfile: () => + api.get('/auth/me'), + + resendVerification: () => + api.post('/auth/resend-verification'), }; // Billing endpoints diff --git a/src/pages/Billing.tsx b/src/pages/Billing.tsx index b3c6c8a8..07067f7f 100644 --- a/src/pages/Billing.tsx +++ b/src/pages/Billing.tsx @@ -2,12 +2,7 @@ import { useState, useEffect } from 'react'; import { useSearchParams } from 'react-router-dom'; import { billingApi } from '@/lib/api'; import { toastService } from '@/lib/toast'; -import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; -import { Badge } from '@/components/ui/badge'; -import { Separator } from '@/components/ui/separator'; -import { Loader2, CreditCard, Zap, TrendingUp, Package } from 'lucide-react'; -import Navigation from '@/components/Navigation'; +import { Loader2, CreditCard, Zap, TrendingUp, Package, CheckCircle2 } from 'lucide-react'; interface CreditPack { id: string; @@ -23,6 +18,7 @@ interface Transaction { balance_after: number; description: string; ts: string; + created_at?: string; } interface BalanceData { @@ -32,12 +28,21 @@ interface BalanceData { credit_packs: CreditPack[]; } -const TX_BADGE: Record = { - purchase: { label: 'Purchase', variant: 'default' }, - grant: { label: 'Grant', variant: 'secondary' }, - admin_grant: { label: 'Admin Grant', variant: 'secondary' }, - debit: { label: 'Used', variant: 'destructive' }, - refund: { label: 'Refund', variant: 'outline' }, +const TX_LABELS: Record = { + purchase: 'Purchase', grant: 'Free grant', admin_grant: 'Admin grant', debit: 'Used', refund: 'Refund', +}; +const TX_COLORS: Record = { + purchase: 'text-primary', grant: 'text-green-400', admin_grant: 'text-green-400', debit: 'text-red-400', refund: 'text-blue-400', +}; +const TX_BG: Record = { + purchase: 'bg-primary/10 text-primary', grant: 'bg-green-400/10 text-green-400', admin_grant: 'bg-green-400/10 text-green-400', + debit: 'bg-red-400/10 text-red-400', refund: 'bg-blue-400/10 text-blue-400', +}; + +const PACK_FEATURES: Record = { + starter: ['50 credits', '~25 personas', '1 focus group run'], + pro: ['220 credits', '~110 personas', '5 focus group runs', 'Bulk export'], + scale: ['600 credits', '~300 personas', '15 focus group runs', 'Unlimited exports', 'API access'], }; export default function Billing() { @@ -49,7 +54,7 @@ export default function Billing() { useEffect(() => { if (searchParams.get('success')) { - toastService.success('Payment successful!', { description: 'Your credits have been added to your account.' }); + toastService.success('Payment successful!', { description: 'Credits have been added to your account.' }); } if (searchParams.get('cancelled')) { toastService.info('Payment cancelled.'); @@ -64,8 +69,8 @@ export default function Billing() { billingApi.getTransactions(20), ]); setBalanceData(balRes.data); - setTransactions(txRes.data.transactions); - } catch (e: any) { + setTransactions(txRes.data.transactions || []); + } catch { toastService.error('Failed to load billing data'); } finally { setLoading(false); @@ -78,9 +83,7 @@ export default function Billing() { setCheckoutPack(packId); try { const res = await billingApi.createCheckout(packId); - if (res.data.checkout_url) { - window.location.href = res.data.checkout_url; - } + if (res.data.checkout_url) window.location.href = res.data.checkout_url; } catch (e: any) { toastService.error('Checkout failed', { description: e.response?.data?.message || 'Please try again' }); setCheckoutPack(null); @@ -89,126 +92,128 @@ export default function Billing() { return (
- -
+
+
-

Billing

-

Manage your credits and purchases

+

Billing

+

Manage your credits and purchase history

{loading ? (
- +
) : balanceData && ( <> - {/* Balance card */} - - - - - Your Balance - - - -
- {balanceData.credits_balance} - credits + {/* Balance */} +
+
+
+
-
- Persona creation: {balanceData.persona_cost} cr - Focus group run: {balanceData.run_cost} cr +
+

Credit balance

+
+ {balanceData.credits_balance.toLocaleString()} + credits +
+
+
+
+
+

{balanceData.persona_cost} cr

+

per persona

+
+
+

{balanceData.run_cost} cr

+

per FG run

- - - - {/* Credit packs */} -
-

Buy Credits

-
- {balanceData.credit_packs.map((pack) => ( - - {pack.id === 'pro' && ( -
- Popular -
- )} - - - - {pack.name} - - {pack.credits} credits - - -
${pack.price_usd}
-
- ${(pack.price_usd / pack.credits).toFixed(2)} per credit -
-
- - - -
- ))}
- {/* Transaction history */} + {/* Credit packs */}
-

- - Transaction History +

+ Buy credits +

+
+ {balanceData.credit_packs.map(pack => { + const isPopular = pack.id === 'pro'; + const features = PACK_FEATURES[pack.id] || []; + return ( +
+ {isPopular && ( +
+ Most popular +
+ )} +

{pack.name}

+
+ ${pack.price_usd} + one-time +
+
    + {features.map(f => ( +
  • + {f} +
  • + ))} +
+ +
+ ); + })} +
+

+ + {/* Transactions */} +
+

+ Transaction history

{transactions.length === 0 ? ( -

No transactions yet.

+

No transactions yet.

) : ( - - - {transactions.map((tx, i) => { - const badge = TX_BADGE[tx.type] || { label: tx.type, variant: 'outline' as const }; - const isPositive = tx.amount > 0; - return ( -
- {i > 0 && } -
-
- {badge.label} -
-

{tx.description}

-

- {new Date(tx.ts).toLocaleString()} -

-
-
-
-

- {isPositive ? '+' : ''}{tx.amount} cr -

-

bal: {tx.balance_after}

-
-
+
+ {transactions.map((tx, i) => ( +
+
+ + {TX_LABELS[tx.type] || tx.type} + +
+ {tx.description &&

{tx.description}

} +

+ {new Date(tx.ts || tx.created_at || '').toLocaleString('en-GB', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' })} +

- ); - })} - - +
+
+

+ {tx.amount > 0 ? '+' : ''}{tx.amount} cr +

+

{tx.balance_after} left

+
+
+ ))} +
)}
diff --git a/src/pages/FocusGroupSession.tsx b/src/pages/FocusGroupSession.tsx index 14afd4fb..2b203ed7 100755 --- a/src/pages/FocusGroupSession.tsx +++ b/src/pages/FocusGroupSession.tsx @@ -21,7 +21,6 @@ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, D import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Badge } from '@/components/ui/badge'; import { Progress } from '@/components/ui/progress'; -import Navigation from '@/components/Navigation'; import ParticipantPanel from '@/components/focus-group-session/ParticipantPanel'; import DiscussionPanel from '@/components/focus-group-session/DiscussionPanel'; import ThemesPanel from '@/components/focus-group-session/ThemesPanel'; @@ -1762,7 +1761,7 @@ const FocusGroupSession = () => { if (isLoading) { return (
- +
@@ -1777,7 +1776,7 @@ const FocusGroupSession = () => { if (!focusGroup) { return (
- +

Focus group not found

We couldn't find the focus group you're looking for.

@@ -1794,7 +1793,7 @@ const FocusGroupSession = () => { return (
- + {/* WebSocket Connection Status Bar */} {useWebSocketEnabled && isStatusBarVisible && ( diff --git a/src/pages/FocusGroups.tsx b/src/pages/FocusGroups.tsx index 9dc33474..198fef31 100755 --- a/src/pages/FocusGroups.tsx +++ b/src/pages/FocusGroups.tsx @@ -1,7 +1,6 @@ import React, { useState, useEffect, useRef } from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; -import Navigation from '@/components/Navigation'; import FocusGroupModerator from '@/components/FocusGroupModerator'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -66,7 +65,7 @@ const statusColors = { 'scheduled': 'bg-blue-100 text-blue-800 border-blue-200', 'in-progress': 'bg-amber-100 text-amber-800 border-amber-200', 'active': 'bg-amber-100 text-amber-800 border-amber-200', - 'paused': 'bg-purple-100 text-purple-800 border-purple-200', + 'paused': 'bg-primary/10 text-primary border-primary/20', 'new': 'bg-slate-100 text-slate-800 border-slate-200', 'ai_mode': 'bg-amber-100 text-amber-800 border-amber-200', 'draft': 'bg-gray-100 text-gray-800 border-gray-200', @@ -286,7 +285,7 @@ const FocusGroups = () => { return (
- +
diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index c76e8555..7def41c8 100755 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -1,381 +1,519 @@ -import { Link } from 'react-router-dom'; -import { Users, MessageSquare, LayoutDashboard, Sparkles, ArrowRight, Zap, Brain, Target, BarChart3, Globe } from 'lucide-react'; -import Navigation from '@/components/Navigation'; -import Hero from '@/components/Hero'; -import FeatureCard from '@/components/FeatureCard'; +import { useState, useEffect } from 'react'; +import { Link, useNavigate } from 'react-router-dom'; +import { + Users, MessageSquare, Sparkles, Download, ArrowRight, ArrowDown, + Zap, Clock, DollarSign, Globe, ChevronDown, ChevronUp, + CheckCircle2, Star +} from 'lucide-react'; +import { billingApi } from '@/lib/api'; -const SectionLabel = ({ children }: { children: React.ReactNode }) => ( -
- - {children} +// ────────────────────────────────────────────── +// Helpers +// ────────────────────────────────────────────── + +const AVATAR_COLORS = ['#E89B3C', '#D97706', '#B45309', '#92400E']; + +const PersonaOrb = ({ index, size, label }: { index: number; size: string; label?: string }) => ( +
+ {label && ( +
+ {label} +
+ )} +
); -const StepCard = ({ step, title, description }: { step: string; title: string; description: string }) => ( -
-
- {step} -
-

{title}

-

{description}

-
-); +interface CreditPack { + id: string; + name: string; + price_usd: number; + credits: number; + popular?: boolean; +} -const LogoMark = () => ( - - - - - - - - - - - -); +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 }, +]; -const Index = () => { +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 FAQ_ITEMS = [ + { + q: 'What is a synthetic persona?', + a: 'A synthetic persona is an AI-generated profile that mimics a real human respondent — complete with demographics, psychographics, attitudes, and communication style. Unlike survey panels, synthetic personas are available instantly, cost nothing per respondent, and scale to thousands.', + }, + { + q: 'How is this different from a real focus group?', + a: 'Traditional focus groups take 2–4 weeks to recruit, cost $5,000–$20,000, and max out at 12 participants. Cohorta generates your panel in minutes, runs sessions 24/7, and lets you test dozens of segments in parallel — all for the cost of a SaaS subscription.', + }, + { + q: 'How accurate are the AI personas?', + a: 'Our two-stage generation pipeline builds each persona from a detailed audience brief, creating internally consistent profiles with demographic context, psychographic depth, and realistic response patterns. Results are directionally accurate for concept testing, message testing, and exploratory research.', + }, + { + q: 'Is my research data secure?', + a: "All data is encrypted in transit (TLS 1.3) and at rest. Each user's personas and sessions are fully isolated. We do not use your research data to train AI models. Infrastructure is hosted on EU servers by AImpress LTD.", + }, + { + q: 'How does the credit system work?', + a: 'Creating a persona costs 2 credits. Running a full focus group session costs 40 credits. You get 10 free trial credits on signup. Purchase credit packs as needed — credits never expire.', + }, + { + q: 'Can I export the results?', + a: 'Yes. Download full discussion transcripts as Markdown, export personas as CSV, and generate structured discussion guides. Pro and Scale plans include bulk export.', + }, +]; + +// ────────────────────────────────────────────── +// FAQ accordion item +// ────────────────────────────────────────────── + +function FAQItem({ q, a }: { q: string; a: string }) { + const [open, setOpen] = useState(false); return ( -
- +
setOpen(v => !v)} + > +
+

{q}

+ {open + ? + : + } +
+ {open && ( +
+

{a}

+
+ )} +
+ ); +} -
- +// ────────────────────────────────────────────── +// Main page +// ────────────────────────────────────────────── - {/* Features */} -
+export default function Index() { + const navigate = useNavigate(); + const [packs, setPacks] = useState(DEFAULT_PACKS); + + useEffect(() => { + billingApi.getBalance() + .then(r => { + const apiPacks: CreditPack[] = (r.data.credit_packs || []).map((p: any) => ({ + ...p, + popular: p.id === 'pro', + })); + if (apiPacks.length >= 2) setPacks(apiPacks); + }) + .catch(() => {}); + }, []); + + return ( +
+ + {/* ── 1. HERO ── */} +
+
+
+ +
+ {/* Outline hero text */}
-
-
- Why Cohorta? -

- Everything you need for{' '} - - better research - -

-

- Our platform combines advanced AI with intuitive design to help researchers - gain deeper insights faster than traditional methods. -

-
+ className="absolute inset-x-0 flex items-center justify-center select-none pointer-events-none" + style={{ top: '50%', transform: 'translateY(-50%)' }} + > + + COHORTA + +
-
- - - - - - + {/* Persona orbs */} +
+
+ +
+
+ + Cohorta +
+

Generate.
Moderate.
Decide.

+
+
+
+ +
+
+
-
- {/* How It Works */} -
-
-
-
- Simple Process -

- From idea to insight in{' '} - - minutes - -

-

- Three simple steps to gather valuable insights from synthetic personas. -

-
- -
- {/* Connector line */} -
- - - -
- -
- { - (e.currentTarget as HTMLElement).style.boxShadow = '0 0 48px hsl(188 91% 44% / 0.5)'; - (e.currentTarget as HTMLElement).style.transform = 'translateY(-2px)'; - }} - onMouseLeave={e => { - (e.currentTarget as HTMLElement).style.boxShadow = '0 0 28px hsl(188 91% 44% / 0.3)'; - (e.currentTarget as HTMLElement).style.transform = 'translateY(0)'; - }} + {/* Copy + CTA */} +
+

+ Skip recruiting. Run{' '} + synthetic focus groups{' '} + in minutes — at any scale. +

+
+ + + Log in
+

No credit card required • 10 free credits on signup

-
- {/* CTA Banner */} -
-
-
+ +
+
+
+ + {/* ── 2. STATS TRIPLET ── */} +
+
+ {[ + { icon: Clock, stat: '10×', label: 'FASTER', sub: 'Insights in hours, not weeks' }, + { icon: DollarSign, stat: '99%', label: 'CHEAPER', sub: 'No recruiter, incentives, or no-shows' }, + { icon: Globe, stat: '24/7', label: 'SCALE', sub: 'Run 50 sessions in parallel' }, + ].map(({ icon: Icon, stat, label, sub }) => ( +
+
+ +
+
{stat}
+
{label}
+

{sub}

+
+ ))} +
+
+ + {/* ── 3. ORANGE BAND — FEATURES ── */} +
+
+
+

+ Built for product, marketing & UX researchers +

+

+ Everything you need to generate insight — without recruiting a single real participant. +

+
+
+ {[ + { icon: Users, title: 'AI Personas', desc: 'Two-stage generation from one brief. Demographic + psychographic depth in minutes.' }, + { icon: MessageSquare, title: 'Focus Groups', desc: 'AI-moderated sessions — autonomous or manual. Real-time chat with your synthetic panel.' }, + { icon: Sparkles, title: 'Theme Extraction', desc: 'Live key themes extracted per session. See patterns emerge as your panel speaks.' }, + { icon: Download, title: 'Bulk Export', desc: 'Markdown discussion guides, CSV transcripts, full persona profiles — ready to share.' }, + ].map(({ icon: Icon, title, desc }) => ( +
+
+ +
+

{title}

+

{desc}

+
+ + Learn more +
+
+ ))} +
+
+
+ + {/* ── 4. HOW IT WORKS ── */} +
+
+
+
+ + How it works +
+

+ From brief to insight in three steps +

+
+
+ {[ + { + num: '01', title: 'Write a brief', + desc: 'Describe your target audience — age range, lifestyle, attitudes, geography. One paragraph is enough.', + }, + { + num: '02', title: 'Generate your panel', + desc: 'Cohorta builds 5–50 rich synthetic personas from your brief in under 2 minutes. Review and adjust before proceeding.', + }, + { + num: '03', title: 'Run your session', + desc: 'Launch an AI-moderated focus group — autonomous or manual mode. Export themes and transcripts when done.', + }, + ].map(({ num, title, desc }) => ( +
+
+ {num} +
+

{title}

+

{desc}

+
+ ))} +
+
+ +
+
+
+ + {/* ── 5. LIVE PREVIEW ── */} +
+
+
+
+ + Live session +
+

+ Watch your synthetic panel debate your product. +

+

+ Each persona speaks from their own perspective, challenges other panelists, and responds to the moderator's questions in real time. The AI extracts themes and flags consensus as the session unfolds. +

+ +
+ {/* Mock chat UI */} +
+
+
+
+ + Session: Product Concept Test A
-

- Ready to transform your research? -

-

- Join hundreds of researchers and product teams using Cohorta to make faster, - smarter decisions with AI-powered synthetic insights. -

-
- - Create Free Account - - - - Sign in - + 4 participants +
+
+ {[ + { name: 'Maya, 32', color: '#E89B3C', msg: 'I love the idea, but $49 feels steep for someone who only does one research project per quarter.' }, + { name: 'James, 45', color: '#D97706', msg: "If it replaces even one recruiter day it's already cheaper. Time-to-insight matters more to me." }, + { name: 'Sofia, 28', color: '#B45309', msg: "The autonomous mode is what sold me. I don't want to moderate — I want to read results." }, + { name: 'Moderator AI', color: 'hsl(28, 78%, 56%)', msg: 'Great point Sofia. Team — how important is moderation control vs. output quality?' }, + ].map(({ name, color, msg }) => ( +
+
+ {name[0]} +
+
+

{name}

+

{msg}

+
+
+ ))} +
+
+
+ Ask a follow-up…
+
-
-
+
+ - {/* Footer */} -
-
-
- {/* Brand */} -
- - - - Cohorta - - -

- AI-powered synthetic persona research platform. Faster insights, better decisions. -

-

Powered by AImpress LTD

-
- - {/* Links */} -
-

- Platform -

-
    - {[ - { label: 'Synthetic Personas', to: '/synthetic-users' }, - { label: 'Focus Groups', to: '/focus-groups' }, - { label: 'Dashboard', to: '/dashboard' }, - { label: 'Billing', to: '/billing' }, - ].map((item) => ( -
  • - - {item.label} - -
  • - ))} -
-
- -
-

- Company -

-
    - {[ - { label: 'About AImpress', href: 'https://ai-impress.com' }, - { label: 'Contact', href: '#' }, - { label: 'Blog', href: '#' }, - { label: 'Careers', href: '#' }, - ].map((item) => ( -
  • - - {item.label} - -
  • - ))} -
-
- -
-

- Legal -

-
    - {['Privacy Policy', 'Terms of Service', 'Cookie Policy', 'Security'].map((item) => ( -
  • - - {item} - -
  • - ))} -
-
+ {/* ── 6. TESTIMONIALS ── */} +
+
+
+

+ Researchers who switched to synthetic +

- - {/* Bottom bar */} -
-

- © {new Date().getFullYear()} AImpress LTD. All rights reserved. -

-
- Cohorta is a product of - - AImpress LTD - -
+
+ {[ + { + quote: "We cut our concept-testing cycle from 3 weeks to 48 hours. The insights are surprisingly nuanced — the personas push back in ways real respondents would.", + name: 'Alex K.', role: 'Product Manager, B2B SaaS', initials: 'AK', + }, + { + quote: "I ran six audience segments in one afternoon. That would have taken $40k and two months with traditional research. Directionally accurate for early-stage work.", + name: 'Sarah M.', role: 'Marketing Director, Consumer Goods', initials: 'SM', + }, + { + quote: "The autonomous moderation is the killer feature. I briefed the system at 9am and had a full transcript + theme report by 9:20. Game-changer for agile research.", + name: 'Tom R.', role: 'UX Research Lead, Fintech', initials: 'TR', + }, + ].map(({ quote, name, role, initials }) => ( +
+
+ {[...Array(5)].map((_, i) => )} +
+

"{quote}"

+
+
+ {initials} +
+
+

{name}

+

{role}

+
+
+
+ ))}
-
+ + + {/* ── 7. PRICING ── */} +
+
+
+
+ + Pricing +
+

+ Pay per project, not per seat +

+

Credits never expire. Start with 10 free.

+
+
+ {packs.map(pack => { + const features = PACK_FEATURES[pack.id] || PACK_FEATURES['pro']; + return ( +
+ {pack.popular && ( +
+ + Most popular + +
+ )} +

{pack.name}

+
+ ${pack.price_usd} + one-time +
+

{pack.credits} credits included

+
    + {features.map(f => ( +
  • + + {f} +
  • + ))} +
+ + Get started + +
+ ); + })} +
+

+ Not sure?{' '} + Start with 10 free credits + {' '}— no card required. +

+
+
+ + {/* ── 8. FAQ ── */} +
+
+

+ Frequently asked questions +

+
+ {FAQ_ITEMS.map(item => )} +
+
+
+ + {/* ── 9. FINAL CTA BANNER ── */} +
+
+

+ Try Cohorta free. +

+

+ 10 credits on signup. No credit card required. Results in under 5 minutes. +

+ +
+
+
); -}; - -export default Index; +} diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index e647e06f..98639923 100755 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -8,6 +8,7 @@ import { Input } from '@/components/ui/input'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; import { useAuth } from '@/contexts/AuthContext'; import { Loader2, Eye, EyeOff } from 'lucide-react'; +import Logo from '@/components/brand/Logo'; const loginSchema = z.object({ username: z.string().min(3, 'Username must be at least 3 characters'), @@ -16,23 +17,6 @@ const loginSchema = z.object({ type LoginFormValues = z.infer; -const LogoMark = () => ( - - - - - - - - - - - -); - export default function Login() { const navigate = useNavigate(); const location = useLocation(); @@ -40,10 +24,10 @@ export default function Login() { const [isLoading, setIsLoading] = useState(false); const [showPassword, setShowPassword] = useState(false); - const from = location.state?.from || '/'; + const from = location.state?.from || '/dashboard'; useEffect(() => { - if (isAuthenticated) navigate('/', { replace: true }); + if (isAuthenticated) navigate('/dashboard', { replace: true }); }, [isAuthenticated, navigate]); const form = useForm({ @@ -64,59 +48,17 @@ export default function Login() { } return ( -
- {/* Background orbs */} -
-
+
- {/* Grid overlay */} -
+ {/* Left: form */} +
+
+ + + -
- {/* Card */} -
- {/* Logo */} -
- - - - Cohorta - - -

Welcome back

-

Sign in to your account

-
+

Welcome back

+

Sign in to your Cohorta account

@@ -125,17 +67,17 @@ export default function Login() { name="username" render={({ field }) => ( - Username + Username - + )} /> @@ -145,7 +87,7 @@ export default function Login() { name="password" render={({ field }) => ( - Password + Password
- +
)} /> @@ -173,55 +115,50 @@ export default function Login() {
-

- Don't have an account?{' '} - +

+ No account?{' '} + Create one free

- - {/* Footer note */} -

- A product of{' '} - - AImpress LTD - -

+ + {/* Right: orange panel */} +
+ {/* Outline display */} +
+ COHORTA +
+
+

+ "We cut concept testing from 3 weeks to 48 hours. The AI personas push back in ways real respondents would." +

+

— Alex K., Product Manager

+
+ + {/* Decorative orbs */} +
+
+
+
); } diff --git a/src/pages/MyUsage.tsx b/src/pages/MyUsage.tsx index e8c1435e..41564105 100644 --- a/src/pages/MyUsage.tsx +++ b/src/pages/MyUsage.tsx @@ -1,70 +1,75 @@ import { useMyUsage } from '@/hooks/useMyUsage'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { Loader2, DollarSign, Zap, Activity } from 'lucide-react'; -import Navigation from '@/components/Navigation'; export default function MyUsage() { const { data, isLoading } = useMyUsage(); const totals = data?.totals ?? {}; const byFeature: any[] = data?.by_feature ?? []; - const periodStart = data?.period_start ? new Date(data.period_start).toLocaleDateString() : '—'; + const periodStart = data?.period_start ? new Date(data.period_start).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' }) : '—'; return (
- -
-
-

My Usage

-

Month-to-date since {periodStart}

+
+ +
+

Usage

+

Month-to-date since {periodStart}

{isLoading ? ( -
+
+ +
) : (
-
+
{[ { label: 'Total Cost (MTD)', value: `$${(totals.total_cost ?? 0).toFixed(4)}`, icon: DollarSign }, { label: 'LLM Calls', value: (totals.calls ?? 0).toLocaleString(), icon: Activity }, { label: 'Total Tokens', value: (((totals.prompt_tokens ?? 0) + (totals.completion_tokens ?? 0)) / 1000).toFixed(1) + 'k', icon: Zap }, ].map(({ label, value, icon: Icon }) => ( - - - {label} - - - -
{value}
-
-
+
+
+ +
+
+

{label}

+

{value}

+
+
))}
-
- - - - Feature - Cost - Calls - - - - {byFeature.length === 0 && ( - - No usage data yet +
+

By Feature

+
+
+ + + Feature + Cost + Calls - )} - {byFeature.map((row: any, i: number) => ( - - {row._id ?? '—'} - ${(row.total_cost ?? 0).toFixed(6)} - {row.calls} - - ))} - -
+ + + {byFeature.length === 0 && ( + + + No usage data yet this month. + + + )} + {byFeature.map((row: any, i: number) => ( + + {row._id ?? '—'} + ${(row.total_cost ?? 0).toFixed(6)} + {row.calls} + + ))} + + +
)} diff --git a/src/pages/NotFound.tsx b/src/pages/NotFound.tsx index d273a3f5..60c3c33a 100755 --- a/src/pages/NotFound.tsx +++ b/src/pages/NotFound.tsx @@ -1,63 +1,48 @@ - import { useLocation, useNavigate } from "react-router-dom"; import { useEffect } from "react"; -import { Button } from "@/components/ui/button"; +import { ArrowRight } from "lucide-react"; const NotFound = () => { const location = useLocation(); const navigate = useNavigate(); useEffect(() => { - console.error( - "404 Error: User attempted to access non-existent route:", - location.pathname - ); + console.error("404:", location.pathname); }, [location.pathname]); - // Check if this is a persona not found path - const isPersonaPath = location.pathname.startsWith("/synthetic-users/"); - - // Check if user came from review page - const searchParams = new URLSearchParams(location.search); - const fromReview = searchParams.get('fromReview') === 'true'; - return ( -
-
-

404

- - {isPersonaPath ? ( - <> -

Persona Not Found

-

- The persona you're looking for may have been removed or doesn't exist. -

- {fromReview ? ( - - ) : ( - - )} - - ) : ( - <> -

Oops! Page not found

-

- The page you're looking for doesn't exist or has been moved. -

- - )} - - + 404 +
+

Page not found

+

+ The page you're looking for doesn't exist or has been moved. +

+
+ + +
); diff --git a/src/pages/Register.tsx b/src/pages/Register.tsx index a771bcef..f9027248 100644 --- a/src/pages/Register.tsx +++ b/src/pages/Register.tsx @@ -7,8 +7,9 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; import { useAuth } from '@/contexts/AuthContext'; -import { Loader2, Eye, EyeOff, CheckCircle2, Mail } from 'lucide-react'; +import { Loader2, Eye, EyeOff, Mail, CheckCircle2 } from 'lucide-react'; import { toastService } from '@/lib/toast'; +import Logo from '@/components/brand/Logo'; import axios from 'axios'; const registerSchema = z.object({ @@ -23,26 +24,9 @@ const registerSchema = z.object({ type RegisterFormValues = z.infer; -const LogoMark = () => ( - - - - - - - - - - - -); - export default function Register() { const navigate = useNavigate(); - const { isAuthenticated, login } = useAuth(); + const { isAuthenticated } = useAuth(); const [isLoading, setIsLoading] = useState(false); const [showPassword, setShowPassword] = useState(false); const [showConfirm, setShowConfirm] = useState(false); @@ -50,7 +34,7 @@ export default function Register() { const [registeredEmail, setRegisteredEmail] = useState(''); useEffect(() => { - if (isAuthenticated) navigate('/', { replace: true }); + if (isAuthenticated) navigate('/dashboard', { replace: true }); }, [isAuthenticated, navigate]); const form = useForm({ @@ -66,147 +50,74 @@ export default function Register() { email: values.email, password: values.password, }); - setRegisteredEmail(values.email); setRegistered(true); - - // Auto-login after registration if (res.data.access_token) { localStorage.setItem('auth_token', res.data.access_token); - toastService.success('Account created!', { description: 'Check your email to verify your account.' }); + toastService.success('Account created!', { description: 'Check your email to verify.' }); } } catch (err: any) { - const msg = err.response?.data?.message || 'Registration failed. Please try again.'; - toastService.error(msg); + toastService.error(err.response?.data?.message || 'Registration failed. Please try again.'); } finally { setIsLoading(false); } } + // Success state if (registered) { return ( -
-
-
-
-
- -
-

Check your inbox

-

- We sent a verification link to -

-

{registeredEmail}

-

- Click the link in the email to verify your account. The link expires in 24 hours. -

-
- -

- Didn't receive it?{' '} - -

-
+
+
+
+
+

Check your inbox

+

We sent a verification link to

+

{registeredEmail}

+

+ Click the link to verify your account. The link expires in 24 hours. +

+ +

+ Didn't receive it?{' '} + +

); } return ( -
-
-
-
+
-
-
- {/* Logo */} -
- - - - Cohorta - - -

Create your account

-

Get started with 50 free credits

-
+ {/* Left: form */} +
+
+ + + - {/* Benefits row */} -
- {['50 free credits', 'No credit card', 'Cancel anytime'].map(b => ( -
- - {b} -
- ))} -
+

Create your account

+

+ Free to start. 10 credits on signup. +

@@ -215,17 +126,17 @@ export default function Register() { name="username" render={({ field }) => ( - Username + Username - + )} /> @@ -235,18 +146,18 @@ export default function Register() { name="email" render={({ field }) => ( - Email address + Email - + )} /> @@ -256,7 +167,7 @@ export default function Register() { name="password" render={({ field }) => ( - Password + Password
-
- +
)} /> @@ -286,84 +194,81 @@ export default function Register() { name="confirmPassword" render={({ field }) => ( - Confirm password + Confirm password
-
- +
)} /> -

- By creating an account, you agree to our{' '} - Terms of Service - {' '}and{' '} - Privacy Policy. -

- + +
+ 10 free credits + No card required +
-

+

Already have an account?{' '} - + Sign in

- -

- A product of{' '} - - AImpress LTD - -

+ + {/* Right: orange panel */} +
+
+ COHORTA +
+
+ {[ + '10 free credits on signup', + 'AI personas in under 2 minutes', + 'Run your first focus group today', + 'No credit card required', + ].map(item => ( +
+ +

{item}

+
+ ))} +
+
+
+
+
); } diff --git a/src/pages/SyntheticUsers.tsx b/src/pages/SyntheticUsers.tsx index a716f35b..7071c27f 100755 --- a/src/pages/SyntheticUsers.tsx +++ b/src/pages/SyntheticUsers.tsx @@ -2,7 +2,6 @@ import { useState, useEffect, useCallback } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { useNavigation } from '@/contexts/NavigationContext'; -import Navigation from '@/components/Navigation'; import AIRecruiter from '@/components/AIRecruiter'; import UserCreator from '@/components/UserCreator'; import UserCard from '@/components/UserCard'; @@ -1152,7 +1151,7 @@ const SyntheticUsers = () => { return (
- +
diff --git a/src/pages/VerifyEmail.tsx b/src/pages/VerifyEmail.tsx index 13a6f603..b4ea984a 100644 --- a/src/pages/VerifyEmail.tsx +++ b/src/pages/VerifyEmail.tsx @@ -1,30 +1,16 @@ import { useState, useEffect } from 'react'; import { useSearchParams, Link, useNavigate } from 'react-router-dom'; import { CheckCircle2, XCircle, Loader2 } from 'lucide-react'; +import { useAuth } from '@/contexts/AuthContext'; +import Logo from '@/components/brand/Logo'; import axios from 'axios'; -const LogoMark = () => ( - - - - - - - - - - - -); - type Status = 'verifying' | 'success' | 'error'; export default function VerifyEmail() { const [searchParams] = useSearchParams(); const navigate = useNavigate(); + const { isAuthenticated } = useAuth(); const [status, setStatus] = useState('verifying'); const [message, setMessage] = useState(''); @@ -32,126 +18,81 @@ export default function VerifyEmail() { const token = searchParams.get('token'); if (!token) { setStatus('error'); - setMessage('No verification token found in the link. Please use the link from your email.'); + setMessage('No verification token found. Please use the link from your email.'); return; } - axios.post('/api/auth/verify-email', { token }) .then(res => { setStatus('success'); setMessage(res.data.message || 'Email verified successfully!'); - // Auto-redirect after 3s setTimeout(() => navigate('/dashboard'), 3000); }) .catch(err => { setStatus('error'); - setMessage(err.response?.data?.message || 'Verification failed. Please try again.'); + setMessage(err.response?.data?.message || 'Verification failed. The link may have expired.'); }); }, []); return ( -
+
+ {/* Background glow */}
-
-
-
- {/* Logo */} - - - + + + + + {status === 'verifying' && ( + <> +
+ +
+

Verifying your email

+

Please wait a moment…

+ + )} + + {status === 'success' && ( + <> +
+ +
+

Email verified!

+

{message}

+

Redirecting to dashboard in a moment…

+ - Cohorta -
- + Go to Dashboard + + + )} - {status === 'verifying' && ( - <> -
- -
-

Verifying your email

-

Please wait a moment…

- - )} - - {status === 'success' && ( - <> -
- -
-

Email verified!

-

{message}

-

Redirecting to dashboard in a moment…

+ {status === 'error' && ( + <> +
+ +
+

Verification failed

+

{message}

+
- Go to Dashboard + Back to Sign In - - )} - - {status === 'error' && ( - <> -
- -
-

Verification failed

-

{message}

-
- - Back to Sign In - - - Go to homepage - -
- - )} -
- -

- A product of{' '} - - AImpress LTD - -

+ + Go to homepage + +
+ + )}
); diff --git a/tailwind.config.ts b/tailwind.config.ts index 8c21be0a..aa303861 100755 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -121,8 +121,8 @@ export default { }, fontFamily: { sans: ['Inter', 'system-ui', 'sans-serif'], - sf: ['Syne', 'Inter', 'system-ui', 'sans-serif'], - display: ['Syne', 'system-ui', 'sans-serif'], + sf: ['Space Grotesk', 'Inter', 'system-ui', 'sans-serif'], + display: ['Space Grotesk', 'system-ui', 'sans-serif'], }, transitionProperty: { 'height': 'height',