Spaces:
Running
Running
Upload folder using huggingface_hub
Browse files- App.tsx +79 -0
- Dockerfile +15 -0
- README.md +25 -6
- components/Accuracy.tsx +86 -0
- components/ChatPage.tsx +255 -0
- components/ConversationPage.tsx +115 -0
- components/Documentation.tsx +81 -0
- components/FAQ.tsx +54 -0
- components/Footer.tsx +27 -0
- components/Hero.tsx +78 -0
- components/HowItWorks.tsx +80 -0
- components/InteractiveDemo.tsx +374 -0
- components/Navbar.tsx +86 -0
- components/Pricing.tsx +104 -0
- components/ProductOverview.tsx +55 -0
- components/RealNetworkGraph.tsx +128 -0
- components/SimulationGraph.tsx +203 -0
- components/SimulationPage.tsx +246 -0
- components/Testimonials.tsx +47 -0
- components/TrustedBy.tsx +89 -0
- components/UseCases.tsx +32 -0
- components/ui/Button.tsx +40 -0
- constants.ts +73 -0
- dist/index.html +78 -0
- index.html +1 -10
- index.tsx +15 -0
- metadata.json +5 -0
- package-lock.json +0 -0
- package.json +28 -0
- server.js +18 -0
- services/gradioService.ts +49 -0
- tsconfig.json +29 -0
- types.ts +32 -0
- vite.config.ts +33 -0
App.tsx
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import React, { useState } from 'react';
|
| 3 |
+
import Navbar from './components/Navbar';
|
| 4 |
+
import Hero from './components/Hero';
|
| 5 |
+
import TrustedBy from './components/TrustedBy';
|
| 6 |
+
import ProductOverview from './components/ProductOverview';
|
| 7 |
+
import InteractiveDemo from './components/InteractiveDemo';
|
| 8 |
+
import UseCases from './components/UseCases';
|
| 9 |
+
import HowItWorks from './components/HowItWorks';
|
| 10 |
+
import Accuracy from './components/Accuracy';
|
| 11 |
+
import Documentation from './components/Documentation';
|
| 12 |
+
import FAQ from './components/FAQ';
|
| 13 |
+
import Footer from './components/Footer';
|
| 14 |
+
import SimulationPage from './components/SimulationPage';
|
| 15 |
+
import ConversationPage from './components/ConversationPage';
|
| 16 |
+
import ChatPage from './components/ChatPage';
|
| 17 |
+
|
| 18 |
+
function App() {
|
| 19 |
+
const [currentView, setCurrentView] = useState<'landing' | 'simulation' | 'conversation' | 'chat'>('landing');
|
| 20 |
+
|
| 21 |
+
const startSimulation = () => {
|
| 22 |
+
setCurrentView('simulation');
|
| 23 |
+
window.scrollTo(0,0);
|
| 24 |
+
};
|
| 25 |
+
|
| 26 |
+
const goBackToLanding = () => {
|
| 27 |
+
setCurrentView('landing');
|
| 28 |
+
};
|
| 29 |
+
|
| 30 |
+
const openConversation = () => {
|
| 31 |
+
setCurrentView('conversation');
|
| 32 |
+
};
|
| 33 |
+
|
| 34 |
+
const openChat = () => {
|
| 35 |
+
setCurrentView('chat');
|
| 36 |
+
};
|
| 37 |
+
|
| 38 |
+
const goBackToSimulation = () => {
|
| 39 |
+
setCurrentView('simulation');
|
| 40 |
+
};
|
| 41 |
+
|
| 42 |
+
if (currentView === 'simulation') {
|
| 43 |
+
return (
|
| 44 |
+
<SimulationPage
|
| 45 |
+
onBack={goBackToLanding}
|
| 46 |
+
onOpenConversation={openConversation}
|
| 47 |
+
onOpenChat={openChat}
|
| 48 |
+
/>
|
| 49 |
+
);
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
if (currentView === 'conversation') {
|
| 53 |
+
return <ConversationPage onBack={goBackToSimulation} />;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
if (currentView === 'chat') {
|
| 57 |
+
return <ChatPage onBack={goBackToSimulation} />;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
return (
|
| 61 |
+
<div className="bg-black min-h-screen text-white selection:bg-teal-500/30">
|
| 62 |
+
<Navbar onStart={startSimulation} />
|
| 63 |
+
<main>
|
| 64 |
+
<Hero onStart={startSimulation} />
|
| 65 |
+
<TrustedBy />
|
| 66 |
+
<ProductOverview />
|
| 67 |
+
<InteractiveDemo />
|
| 68 |
+
<UseCases />
|
| 69 |
+
<HowItWorks />
|
| 70 |
+
<Accuracy />
|
| 71 |
+
<Documentation />
|
| 72 |
+
<FAQ />
|
| 73 |
+
</main>
|
| 74 |
+
<Footer />
|
| 75 |
+
</div>
|
| 76 |
+
);
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
export default App;
|
Dockerfile
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:20-slim AS build
|
| 2 |
+
WORKDIR /app
|
| 3 |
+
COPY package*.json ./
|
| 4 |
+
RUN npm install
|
| 5 |
+
COPY . .
|
| 6 |
+
RUN npm run build
|
| 7 |
+
|
| 8 |
+
FROM node:20-slim
|
| 9 |
+
WORKDIR /app
|
| 10 |
+
COPY --from=build /app/dist ./dist
|
| 11 |
+
COPY --from=build /app/package*.json ./
|
| 12 |
+
RUN npm install --only=production
|
| 13 |
+
COPY server.js ./
|
| 14 |
+
EXPOSE 7860
|
| 15 |
+
CMD ["node", "server.js"]
|
README.md
CHANGED
|
@@ -1,10 +1,29 @@
|
|
| 1 |
---
|
| 2 |
title: UserSyncInterface
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
-
sdk:
|
| 7 |
-
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
title: UserSyncInterface
|
| 3 |
+
emoji: 🔄
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: blue
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
---
|
| 9 |
|
| 10 |
+
<div align="center">
|
| 11 |
+
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
|
| 12 |
+
</div>
|
| 13 |
+
|
| 14 |
+
# Run and deploy your AI Studio app
|
| 15 |
+
|
| 16 |
+
This contains everything you need to run your app locally.
|
| 17 |
+
|
| 18 |
+
View your app in AI Studio: https://ai.studio/apps/drive/1uBpK_suSmvNgTEgSyU7qMenb62ZRrQwX
|
| 19 |
+
|
| 20 |
+
## Run Locally
|
| 21 |
+
|
| 22 |
+
**Prerequisites:** Node.js
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
1. Install dependencies:
|
| 26 |
+
`npm install`
|
| 27 |
+
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
|
| 28 |
+
3. Run the app:
|
| 29 |
+
`npm run dev`
|
components/Accuracy.tsx
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Cell } from 'recharts';
|
| 3 |
+
|
| 4 |
+
const data = [
|
| 5 |
+
{ name: 'SyncUsers', score: 82, color: '#ffffff' }, // White
|
| 6 |
+
{ name: 'Claude 3.7 Sonnet', score: 64, color: '#4b5563' }, // Gray-600
|
| 7 |
+
{ name: 'Gemini 1.5 pro', score: 63, color: '#4b5563' },
|
| 8 |
+
{ name: 'GPT-4o', score: 60, color: '#4b5563' },
|
| 9 |
+
{ name: 'GPT-3.5 Turbo', score: 54, color: '#4b5563' },
|
| 10 |
+
];
|
| 11 |
+
|
| 12 |
+
const Accuracy: React.FC = () => {
|
| 13 |
+
return (
|
| 14 |
+
<section id="accuracy" className="py-24 bg-black">
|
| 15 |
+
<div className="max-w-7xl mx-auto px-6 grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
|
| 16 |
+
<div>
|
| 17 |
+
<div className="inline-block px-4 py-1.5 rounded-full border border-gray-700 text-sm text-gray-300 mb-8">Accuracy</div>
|
| 18 |
+
<h2 className="text-3xl md:text-5xl font-semibold leading-tight mb-8">
|
| 19 |
+
SyncUsers is <span className="text-white">30% more accurate</span> at predicting engagement than standard LLMs.
|
| 20 |
+
</h2>
|
| 21 |
+
|
| 22 |
+
<div className="bg-gray-900/30 border border-gray-800 rounded-2xl p-6 md:p-8">
|
| 23 |
+
<h4 className="text-gray-400 mb-6">Success rate at picking winners from pairs of LinkedIn posts</h4>
|
| 24 |
+
<div className="h-[300px] w-full">
|
| 25 |
+
<ResponsiveContainer width="100%" height="100%">
|
| 26 |
+
<BarChart data={data} layout="vertical" margin={{ left: 0, right: 40 }}>
|
| 27 |
+
<XAxis type="number" hide />
|
| 28 |
+
<YAxis
|
| 29 |
+
dataKey="name"
|
| 30 |
+
type="category"
|
| 31 |
+
width={150}
|
| 32 |
+
tick={{ fill: '#9ca3af', fontSize: 12 }}
|
| 33 |
+
axisLine={false}
|
| 34 |
+
tickLine={false}
|
| 35 |
+
/>
|
| 36 |
+
<Tooltip
|
| 37 |
+
cursor={{ fill: 'transparent' }}
|
| 38 |
+
contentStyle={{ backgroundColor: '#111', border: '1px solid #333' }}
|
| 39 |
+
/>
|
| 40 |
+
<Bar dataKey="score" radius={[0, 4, 4, 0]} barSize={32}>
|
| 41 |
+
{data.map((entry, index) => (
|
| 42 |
+
<Cell key={`cell-${index}`} fill={entry.name === 'SyncUsers' ? 'url(#gradient)' : '#374151'} />
|
| 43 |
+
))}
|
| 44 |
+
</Bar>
|
| 45 |
+
</BarChart>
|
| 46 |
+
</ResponsiveContainer>
|
| 47 |
+
{/* Def for gradient */}
|
| 48 |
+
<svg style={{ height: 0 }}>
|
| 49 |
+
<defs>
|
| 50 |
+
<linearGradient id="gradient" x1="0" y1="0" x2="1" y2="0">
|
| 51 |
+
<stop offset="0%" stopColor="#e5e7eb" />
|
| 52 |
+
<stop offset="100%" stopColor="#ffffff" />
|
| 53 |
+
</linearGradient>
|
| 54 |
+
</defs>
|
| 55 |
+
</svg>
|
| 56 |
+
</div>
|
| 57 |
+
{/* Custom Labels overlay for scores since Recharts labels are tricky with layout vertical sometimes */}
|
| 58 |
+
<div className="hidden">
|
| 59 |
+
{/* Placeholder for accessibility */}
|
| 60 |
+
</div>
|
| 61 |
+
</div>
|
| 62 |
+
</div>
|
| 63 |
+
|
| 64 |
+
{/* Right 3D Visual */}
|
| 65 |
+
<div className="relative h-[600px] flex items-end justify-center perspective-1000">
|
| 66 |
+
{/* Abstract 3D Bar representation */}
|
| 67 |
+
<div className="relative w-40 h-80 bg-teal-800 transform rotate-y-12 shadow-2xl translate-x-10 translate-y-10 z-10 opacity-90">
|
| 68 |
+
<div className="absolute top-0 left-0 w-full h-10 bg-teal-400 transform -skew-x-[40deg] origin-top -translate-y-10"></div>
|
| 69 |
+
<div className="absolute top-0 left-0 h-full w-10 bg-teal-900 transform skew-y-[50deg] origin-left -translate-x-10"></div>
|
| 70 |
+
</div>
|
| 71 |
+
|
| 72 |
+
<div className="relative w-24 h-40 bg-blue-900 transform rotate-y-12 shadow-xl -translate-x-10 translate-y-20 opacity-70">
|
| 73 |
+
<div className="absolute top-0 left-0 w-full h-8 bg-blue-500 transform -skew-x-[40deg] origin-top -translate-y-8"></div>
|
| 74 |
+
<div className="absolute top-0 left-0 h-full w-8 bg-blue-950 transform skew-y-[50deg] origin-left -translate-x-8"></div>
|
| 75 |
+
</div>
|
| 76 |
+
|
| 77 |
+
<div className="absolute top-20 right-20 text-9xl font-bold text-gray-800 opacity-20 select-none">
|
| 78 |
+
Λ
|
| 79 |
+
</div>
|
| 80 |
+
</div>
|
| 81 |
+
</div>
|
| 82 |
+
</section>
|
| 83 |
+
);
|
| 84 |
+
};
|
| 85 |
+
|
| 86 |
+
export default Accuracy;
|
components/ChatPage.tsx
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import React, { useState, useRef } from 'react';
|
| 3 |
+
import { X, ClipboardList, Linkedin, Instagram, Mail, Layout, Edit3, MonitorPlay, Lightbulb, Image, Plus, Sparkles, Zap, AlertCircle, Video, Megaphone, Link as LinkIcon, Loader2, RefreshCw } from 'lucide-react';
|
| 4 |
+
import { GradioService } from '../services/gradioService';
|
| 5 |
+
|
| 6 |
+
// --- Types ---
|
| 7 |
+
interface ChatPageProps {
|
| 8 |
+
onBack: () => void;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
// --- Sub-components (Modular Structure) ---
|
| 12 |
+
|
| 13 |
+
const ChatButton: React.FC<{ label: string; primary?: boolean; icon?: React.ReactNode; onClick?: () => void; className?: string }> = ({ label, primary, icon, onClick, className = "" }) => (
|
| 14 |
+
<button
|
| 15 |
+
onClick={onClick}
|
| 16 |
+
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-xs md:text-sm font-medium transition-all duration-200 whitespace-nowrap
|
| 17 |
+
${primary
|
| 18 |
+
? 'bg-white text-black hover:bg-gray-200 shadow-[0_0_15px_rgba(255,255,255,0.2)]'
|
| 19 |
+
: 'bg-gray-900 border border-gray-800 text-gray-300 hover:bg-gray-800 hover:text-white hover:border-gray-600'
|
| 20 |
+
} ${className}`}
|
| 21 |
+
>
|
| 22 |
+
{icon}
|
| 23 |
+
{primary && <Zap size={14} className="fill-black" />}
|
| 24 |
+
{label}
|
| 25 |
+
</button>
|
| 26 |
+
);
|
| 27 |
+
|
| 28 |
+
const CategoryCard: React.FC<{ title: string; options: { label: string; icon: React.ReactNode }[] }> = ({ title, options }) => (
|
| 29 |
+
<div className="flex flex-col gap-3">
|
| 30 |
+
<h3 className="text-[10px] font-bold text-gray-500 uppercase tracking-widest mb-2 ml-1">{title}</h3>
|
| 31 |
+
<div className="space-y-1">
|
| 32 |
+
{options.map((option) => (
|
| 33 |
+
<div key={option.label} className="group flex items-center gap-3 p-3 rounded-xl hover:bg-gray-900/80 cursor-pointer border border-transparent hover:border-gray-800 transition-all duration-200">
|
| 34 |
+
<div className="text-gray-500 group-hover:text-white transition-colors w-5 flex justify-center">
|
| 35 |
+
{option.icon}
|
| 36 |
+
</div>
|
| 37 |
+
<span className="text-sm font-medium text-gray-400 group-hover:text-gray-200">{option.label}</span>
|
| 38 |
+
</div>
|
| 39 |
+
))}
|
| 40 |
+
</div>
|
| 41 |
+
</div>
|
| 42 |
+
);
|
| 43 |
+
|
| 44 |
+
const ChatInput: React.FC<{ onSimulate: (msg: string) => void; onHelpMeCraft: (msg: string) => void; isSimulating: boolean }> = ({ onSimulate, onHelpMeCraft, isSimulating }) => {
|
| 45 |
+
const [message, setMessage] = useState('');
|
| 46 |
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
| 47 |
+
|
| 48 |
+
const handleUploadClick = () => {
|
| 49 |
+
fileInputRef.current?.click();
|
| 50 |
+
};
|
| 51 |
+
|
| 52 |
+
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
| 53 |
+
const file = e.target.files?.[0];
|
| 54 |
+
if (file) {
|
| 55 |
+
console.log("Selected file:", file.name);
|
| 56 |
+
// In a real app, you'd handle the upload here
|
| 57 |
+
}
|
| 58 |
+
};
|
| 59 |
+
|
| 60 |
+
return (
|
| 61 |
+
<div className="border-t border-gray-800 pt-6 mt-4 bg-[#0a0a0a] px-6 pb-8 md:pb-10 absolute bottom-0 left-0 right-0 z-20 shadow-[0_-20px_50px_rgba(0,0,0,0.8)]">
|
| 62 |
+
<div className="max-w-5xl mx-auto space-y-4">
|
| 63 |
+
<textarea
|
| 64 |
+
className="w-full h-24 bg-black border border-gray-800 text-gray-200 placeholder-gray-600 p-4 rounded-2xl resize-none focus:outline-none focus:border-gray-600 focus:ring-1 focus:ring-gray-600 transition-all text-sm leading-relaxed"
|
| 65 |
+
placeholder="Paste your website link here"
|
| 66 |
+
value={message}
|
| 67 |
+
onChange={(e) => setMessage(e.target.value)}
|
| 68 |
+
/>
|
| 69 |
+
<div className="flex flex-wrap justify-between items-start gap-4">
|
| 70 |
+
<div className="flex gap-2 md:gap-3 flex-wrap">
|
| 71 |
+
<div className="flex flex-col gap-2">
|
| 72 |
+
<ChatButton label="Website Link for UX Testing" icon={<LinkIcon size={14} />} />
|
| 73 |
+
</div>
|
| 74 |
+
<input
|
| 75 |
+
type="file"
|
| 76 |
+
ref={fileInputRef}
|
| 77 |
+
onChange={handleFileChange}
|
| 78 |
+
className="hidden"
|
| 79 |
+
accept="image/*"
|
| 80 |
+
/>
|
| 81 |
+
<ChatButton
|
| 82 |
+
label="Upload Images"
|
| 83 |
+
icon={<Image size={14} />}
|
| 84 |
+
className="h-fit"
|
| 85 |
+
onClick={handleUploadClick}
|
| 86 |
+
/>
|
| 87 |
+
</div>
|
| 88 |
+
<div className="flex gap-3 items-center mt-auto">
|
| 89 |
+
<button
|
| 90 |
+
onClick={() => onHelpMeCraft(message)}
|
| 91 |
+
className="hidden md:flex text-xs text-gray-500 hover:text-white transition-colors items-center gap-1 mr-2"
|
| 92 |
+
>
|
| 93 |
+
Help Me Craft <Sparkles size={12} />
|
| 94 |
+
</button>
|
| 95 |
+
<ChatButton
|
| 96 |
+
label={isSimulating ? "Simulating..." : "Simulate"}
|
| 97 |
+
primary
|
| 98 |
+
onClick={() => onSimulate(message)}
|
| 99 |
+
icon={isSimulating ? <Loader2 size={14} className="animate-spin" /> : undefined}
|
| 100 |
+
/>
|
| 101 |
+
</div>
|
| 102 |
+
</div>
|
| 103 |
+
</div>
|
| 104 |
+
</div>
|
| 105 |
+
);
|
| 106 |
+
};
|
| 107 |
+
|
| 108 |
+
// --- Main Page Component ---
|
| 109 |
+
|
| 110 |
+
const ChatPage: React.FC<ChatPageProps> = ({ onBack }) => {
|
| 111 |
+
const [showNotification, setShowNotification] = useState(false);
|
| 112 |
+
const [isSimulating, setIsSimulating] = useState(false);
|
| 113 |
+
const [simulationResult, setSimulationResult] = useState<string | null>(null);
|
| 114 |
+
|
| 115 |
+
const handleSimulate = (msg: string) => {
|
| 116 |
+
setIsSimulating(true);
|
| 117 |
+
setShowNotification(true);
|
| 118 |
+
|
| 119 |
+
// Simulate API call
|
| 120 |
+
setTimeout(() => {
|
| 121 |
+
setIsSimulating(false);
|
| 122 |
+
setSimulationResult("Please wait, the results will show here. Click Refresh to gather results from the API.");
|
| 123 |
+
}, 2000);
|
| 124 |
+
};
|
| 125 |
+
|
| 126 |
+
const handleRefresh = async () => {
|
| 127 |
+
setIsSimulating(true);
|
| 128 |
+
try {
|
| 129 |
+
// In a real scenario, we would poll the Gradio API for results
|
| 130 |
+
// result = await GradioService.simulate(...)
|
| 131 |
+
setTimeout(() => {
|
| 132 |
+
setIsSimulating(false);
|
| 133 |
+
setSimulationResult("Final Simulation Results: The content resonated well with 85% of the target audience. Key feedback: 'Clearer value proposition needed in the header'.");
|
| 134 |
+
}, 1500);
|
| 135 |
+
} catch (error) {
|
| 136 |
+
setIsSimulating(false);
|
| 137 |
+
}
|
| 138 |
+
};
|
| 139 |
+
|
| 140 |
+
const handleHelpMeCraft = async (msg: string) => {
|
| 141 |
+
const crafted = await GradioService.helpMeCraft(msg);
|
| 142 |
+
alert("Help Me Craft suggestion: " + crafted);
|
| 143 |
+
};
|
| 144 |
+
|
| 145 |
+
const categories = {
|
| 146 |
+
'Survey': [
|
| 147 |
+
{ label: 'Survey', icon: <ClipboardList size={18} /> }
|
| 148 |
+
],
|
| 149 |
+
'Marketing Content': [
|
| 150 |
+
{ label: 'Article', icon: <Edit3 size={18} /> },
|
| 151 |
+
{ label: 'Website Link', icon: <Layout size={18} /> },
|
| 152 |
+
{ label: 'Advertisement', icon: <Megaphone size={18} /> }
|
| 153 |
+
],
|
| 154 |
+
'Social Media Posts': [
|
| 155 |
+
{ label: 'LinkedIn Post', icon: <Linkedin size={18} /> },
|
| 156 |
+
{ label: 'Instagram Post', icon: <Instagram size={18} /> },
|
| 157 |
+
{ label: 'X Post', icon: <X size={18} /> },
|
| 158 |
+
{ label: 'TikTok Script', icon: <Video size={18} /> }
|
| 159 |
+
],
|
| 160 |
+
'Communication': [
|
| 161 |
+
{ label: 'Email Subject Line', icon: <Mail size={18} /> },
|
| 162 |
+
{ label: 'Email', icon: <Mail size={18} /> }
|
| 163 |
+
],
|
| 164 |
+
'Product': [
|
| 165 |
+
{ label: 'Product Proposition', icon: <Lightbulb size={18} /> }
|
| 166 |
+
]
|
| 167 |
+
};
|
| 168 |
+
|
| 169 |
+
return (
|
| 170 |
+
<div className="fixed inset-0 z-50 bg-[#050505] text-white flex flex-col animate-in fade-in duration-300">
|
| 171 |
+
|
| 172 |
+
{/* Header / Nav */}
|
| 173 |
+
<div className="flex items-center justify-between px-6 py-4 md:px-8 md:py-6 border-b border-gray-800/50 bg-[#050505] z-10 relative">
|
| 174 |
+
<div className="flex items-center gap-3">
|
| 175 |
+
<div className="w-8 h-8 flex items-center justify-center bg-gray-800 rounded-lg text-white font-bold">Λ</div>
|
| 176 |
+
<h2 className="text-xl font-medium tracking-tight">New Simulation</h2>
|
| 177 |
+
</div>
|
| 178 |
+
<button onClick={onBack} className="p-2 text-gray-500 hover:text-white hover:bg-gray-900 rounded-full transition-colors">
|
| 179 |
+
<X size={24} />
|
| 180 |
+
</button>
|
| 181 |
+
</div>
|
| 182 |
+
|
| 183 |
+
{/* Notification Banner */}
|
| 184 |
+
{showNotification && (
|
| 185 |
+
<div className="absolute top-24 left-1/2 -translate-x-1/2 z-50 w-[90%] max-w-lg animate-in slide-in-from-top-4 fade-in duration-500">
|
| 186 |
+
<div className="bg-[#0f1f15] backdrop-blur-xl border border-green-500/20 text-green-100 px-6 py-5 rounded-2xl shadow-2xl flex items-start gap-4 ring-1 ring-green-500/10">
|
| 187 |
+
<div className="p-1.5 bg-green-500/20 rounded-full mt-0.5 shrink-0">
|
| 188 |
+
<AlertCircle size={18} className="text-green-400" />
|
| 189 |
+
</div>
|
| 190 |
+
<div className="flex-1">
|
| 191 |
+
<h4 className="font-semibold text-green-300 mb-1">Simulation Initiated</h4>
|
| 192 |
+
<p className="text-sm text-green-200/70 leading-relaxed">
|
| 193 |
+
The simulation has started. This process can take up to <strong className="text-white">30 minutes</strong>. The results will show here when ready.
|
| 194 |
+
</p>
|
| 195 |
+
{simulationResult && (
|
| 196 |
+
<div className="mt-4 p-3 bg-black/40 rounded-xl border border-green-500/20 text-xs text-green-200 flex flex-col gap-3">
|
| 197 |
+
<p>{simulationResult}</p>
|
| 198 |
+
<button
|
| 199 |
+
onClick={handleRefresh}
|
| 200 |
+
disabled={isSimulating}
|
| 201 |
+
className="flex items-center gap-2 px-3 py-1.5 bg-green-600/20 hover:bg-green-600/40 rounded-lg self-end transition-colors disabled:opacity-50"
|
| 202 |
+
>
|
| 203 |
+
<RefreshCw size={14} className={isSimulating ? "animate-spin" : ""} />
|
| 204 |
+
Gather Results
|
| 205 |
+
</button>
|
| 206 |
+
</div>
|
| 207 |
+
)}
|
| 208 |
+
</div>
|
| 209 |
+
<button onClick={() => setShowNotification(false)} className="text-green-400 hover:text-white ml-auto p-1">
|
| 210 |
+
<X size={16} />
|
| 211 |
+
</button>
|
| 212 |
+
</div>
|
| 213 |
+
</div>
|
| 214 |
+
)}
|
| 215 |
+
|
| 216 |
+
{/* Scrollable Content Area */}
|
| 217 |
+
<div className="flex-1 overflow-y-auto custom-scrollbar p-6 md:p-8 pb-80"> {/* Large bottom padding for fixed input */}
|
| 218 |
+
<div className="max-w-5xl mx-auto">
|
| 219 |
+
<h1 className="text-2xl md:text-3xl font-semibold text-center mb-12 mt-4 md:mt-8">What would you like to simulate?</h1>
|
| 220 |
+
|
| 221 |
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 md:gap-12">
|
| 222 |
+
{/* Column 1 */}
|
| 223 |
+
<div className="space-y-12">
|
| 224 |
+
<CategoryCard title="Survey" options={categories['Survey']} />
|
| 225 |
+
<CategoryCard title="Marketing Content" options={categories['Marketing Content']} />
|
| 226 |
+
</div>
|
| 227 |
+
|
| 228 |
+
{/* Column 2 */}
|
| 229 |
+
<div className="space-y-12">
|
| 230 |
+
<CategoryCard title="Social Media Posts" options={categories['Social Media Posts']} />
|
| 231 |
+
</div>
|
| 232 |
+
|
| 233 |
+
{/* Column 3 */}
|
| 234 |
+
<div className="space-y-12">
|
| 235 |
+
<CategoryCard title="Communication" options={categories['Communication']} />
|
| 236 |
+
<CategoryCard title="Product" options={categories['Product']} />
|
| 237 |
+
</div>
|
| 238 |
+
</div>
|
| 239 |
+
|
| 240 |
+
<div className="flex justify-center mt-16 mb-8">
|
| 241 |
+
<button className="flex items-center gap-2 text-gray-500 hover:text-gray-300 transition-colors text-sm px-4 py-2 hover:bg-gray-900 rounded-lg">
|
| 242 |
+
<Plus size={16} />
|
| 243 |
+
Request a new context
|
| 244 |
+
</button>
|
| 245 |
+
</div>
|
| 246 |
+
</div>
|
| 247 |
+
</div>
|
| 248 |
+
|
| 249 |
+
{/* Input Footer */}
|
| 250 |
+
<ChatInput onSimulate={handleSimulate} onHelpMeCraft={handleHelpMeCraft} isSimulating={isSimulating} />
|
| 251 |
+
</div>
|
| 252 |
+
);
|
| 253 |
+
};
|
| 254 |
+
|
| 255 |
+
export default ChatPage;
|
components/ConversationPage.tsx
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import React, { useState } from 'react';
|
| 3 |
+
import { X, ClipboardList, Linkedin, Instagram, Mail, Layout, Edit3, Type, MonitorPlay, Lightbulb, Image, Plus } from 'lucide-react';
|
| 4 |
+
import Button from './ui/Button';
|
| 5 |
+
|
| 6 |
+
interface ConversationPageProps {
|
| 7 |
+
onBack: () => void;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
const ConversationPage: React.FC<ConversationPageProps> = ({ onBack }) => {
|
| 11 |
+
const [activeTab, setActiveTab] = useState('Website Content');
|
| 12 |
+
|
| 13 |
+
return (
|
| 14 |
+
<div className="fixed inset-0 z-50 bg-black flex items-center justify-center p-6 animate-in fade-in duration-300">
|
| 15 |
+
{/* Background Mesh (Subtle) */}
|
| 16 |
+
<div className="absolute inset-0 pointer-events-none opacity-20">
|
| 17 |
+
<div className="absolute top-10 left-10 w-64 h-64 bg-purple-900/30 rounded-full blur-[100px]" />
|
| 18 |
+
<div className="absolute bottom-10 right-10 w-96 h-96 bg-teal-900/20 rounded-full blur-[120px]" />
|
| 19 |
+
</div>
|
| 20 |
+
|
| 21 |
+
<div className="bg-[#050505] border border-gray-800 w-full max-w-5xl rounded-3xl shadow-2xl flex flex-col overflow-hidden max-h-[90vh] relative z-10">
|
| 22 |
+
|
| 23 |
+
{/* Close Button */}
|
| 24 |
+
<button onClick={onBack} className="absolute top-6 right-6 text-gray-500 hover:text-white transition-colors">
|
| 25 |
+
<X size={24} />
|
| 26 |
+
</button>
|
| 27 |
+
|
| 28 |
+
<div className="p-10 pb-0">
|
| 29 |
+
<h2 className="text-2xl font-semibold text-center mb-8">What would you like to simulate?</h2>
|
| 30 |
+
|
| 31 |
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-12 gap-y-8 max-w-4xl mx-auto">
|
| 32 |
+
|
| 33 |
+
{/* Column 1 */}
|
| 34 |
+
<div className="space-y-6">
|
| 35 |
+
<h3 className="text-xs font-bold text-gray-500 uppercase tracking-widest mb-4">Survey</h3>
|
| 36 |
+
<OptionItem icon={<ClipboardList />} label="Survey" />
|
| 37 |
+
|
| 38 |
+
<h3 className="text-xs font-bold text-gray-500 uppercase tracking-widest mt-8 mb-4">Marketing Content</h3>
|
| 39 |
+
<OptionItem icon={<Edit3 />} label="Article" />
|
| 40 |
+
<OptionItem icon={<Layout />} label="Website Content" active />
|
| 41 |
+
<OptionItem icon={<MonitorPlay />} label="Advertisement" />
|
| 42 |
+
</div>
|
| 43 |
+
|
| 44 |
+
{/* Column 2 */}
|
| 45 |
+
<div className="space-y-6">
|
| 46 |
+
<h3 className="text-xs font-bold text-gray-500 uppercase tracking-widest mb-4">Social Media Posts</h3>
|
| 47 |
+
<OptionItem icon={<Linkedin />} label="LinkedIn Post" />
|
| 48 |
+
<OptionItem icon={<Instagram />} label="Instagram Post" />
|
| 49 |
+
<OptionItem icon={<X className="text-white" />} label="X Post" />
|
| 50 |
+
<OptionItem icon={<MonitorPlay />} label="TikTok Script" />
|
| 51 |
+
</div>
|
| 52 |
+
|
| 53 |
+
{/* Column 3 */}
|
| 54 |
+
<div className="space-y-6">
|
| 55 |
+
<h3 className="text-xs font-bold text-gray-500 uppercase tracking-widest mb-4">Communication</h3>
|
| 56 |
+
<OptionItem icon={<Mail />} label="Email Subject Line" />
|
| 57 |
+
<OptionItem icon={<Mail />} label="Email" />
|
| 58 |
+
|
| 59 |
+
<h3 className="text-xs font-bold text-gray-500 uppercase tracking-widest mt-8 mb-4">Product</h3>
|
| 60 |
+
<OptionItem icon={<Lightbulb />} label="Product Proposition" />
|
| 61 |
+
</div>
|
| 62 |
+
|
| 63 |
+
</div>
|
| 64 |
+
</div>
|
| 65 |
+
|
| 66 |
+
{/* Bottom Input Area */}
|
| 67 |
+
<div className="mt-8 p-6 bg-gray-900/30 border-t border-gray-800 flex-1 flex flex-col justify-end">
|
| 68 |
+
<div className="max-w-4xl mx-auto w-full">
|
| 69 |
+
<div className="bg-black border border-gray-700 rounded-2xl p-4 shadow-lg">
|
| 70 |
+
<textarea
|
| 71 |
+
placeholder="Upload an image of your website, describe it, or write your website content here..."
|
| 72 |
+
className="w-full bg-transparent text-gray-300 placeholder-gray-600 resize-none focus:outline-none min-h-[80px]"
|
| 73 |
+
/>
|
| 74 |
+
<div className="flex justify-between items-center mt-4">
|
| 75 |
+
<div className="flex gap-3">
|
| 76 |
+
<Button variant="outline" size="sm" className="gap-2 text-xs border-gray-800 bg-gray-900 text-gray-300">
|
| 77 |
+
<Layout size={14} /> Website Content
|
| 78 |
+
</Button>
|
| 79 |
+
<Button variant="outline" size="sm" className="gap-2 text-xs border-gray-800 bg-gray-900 text-gray-300">
|
| 80 |
+
<Image size={14} /> Upload Images
|
| 81 |
+
</Button>
|
| 82 |
+
</div>
|
| 83 |
+
<div className="flex gap-3">
|
| 84 |
+
<Button variant="ghost" size="sm" className="text-xs text-teal-400 hover:text-teal-300">
|
| 85 |
+
Help Me Craft <span className="ml-1">✨</span>
|
| 86 |
+
</Button>
|
| 87 |
+
<Button variant="primary" size="sm" className="gap-2 bg-white text-black hover:bg-gray-200">
|
| 88 |
+
Simulate <span className="ml-1">⚡</span>
|
| 89 |
+
</Button>
|
| 90 |
+
</div>
|
| 91 |
+
</div>
|
| 92 |
+
</div>
|
| 93 |
+
|
| 94 |
+
<div className="flex justify-center mt-4">
|
| 95 |
+
<button className="text-gray-500 text-sm hover:text-white flex items-center gap-2">
|
| 96 |
+
<Plus size={14} /> Request a new context
|
| 97 |
+
</button>
|
| 98 |
+
</div>
|
| 99 |
+
</div>
|
| 100 |
+
</div>
|
| 101 |
+
</div>
|
| 102 |
+
</div>
|
| 103 |
+
);
|
| 104 |
+
};
|
| 105 |
+
|
| 106 |
+
const OptionItem: React.FC<{ icon: React.ReactNode, label: string, active?: boolean }> = ({ icon, label, active }) => (
|
| 107 |
+
<div className={`flex items-center gap-4 group cursor-pointer p-2 rounded-lg transition-colors ${active ? 'bg-gray-800' : 'hover:bg-gray-900'}`}>
|
| 108 |
+
<div className="w-8 h-8 flex items-center justify-center text-gray-400 group-hover:text-white transition-colors">
|
| 109 |
+
{icon}
|
| 110 |
+
</div>
|
| 111 |
+
<span className={`text-sm font-medium ${active ? 'text-white' : 'text-gray-400 group-hover:text-white'}`}>{label}</span>
|
| 112 |
+
</div>
|
| 113 |
+
);
|
| 114 |
+
|
| 115 |
+
export default ConversationPage;
|
components/Documentation.tsx
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { Book, Code, Terminal, Zap } from 'lucide-react';
|
| 3 |
+
import Button from './ui/Button';
|
| 4 |
+
|
| 5 |
+
const Documentation: React.FC = () => {
|
| 6 |
+
return (
|
| 7 |
+
<section id="docs" className="py-24 bg-black border-t border-gray-900">
|
| 8 |
+
<div className="max-w-7xl mx-auto px-6">
|
| 9 |
+
<div className="flex flex-col md:flex-row justify-between items-end mb-12 gap-6">
|
| 10 |
+
<div>
|
| 11 |
+
<div className="inline-block px-4 py-1.5 rounded-full border border-gray-700 text-sm text-gray-300 mb-6">
|
| 12 |
+
Developers First
|
| 13 |
+
</div>
|
| 14 |
+
<h2 className="text-3xl md:text-5xl font-semibold mb-4">
|
| 15 |
+
Build with our API
|
| 16 |
+
</h2>
|
| 17 |
+
<p className="text-gray-400 text-lg max-w-xl">
|
| 18 |
+
Integrate user simulation directly into your CI/CD pipeline or product workflow.
|
| 19 |
+
Get started for free.
|
| 20 |
+
</p>
|
| 21 |
+
</div>
|
| 22 |
+
<Button variant="outline">View Full Documentation</Button>
|
| 23 |
+
</div>
|
| 24 |
+
|
| 25 |
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
| 26 |
+
{/* Code Snippet Card */}
|
| 27 |
+
<div className="bg-gray-900/30 border border-gray-800 rounded-2xl p-6 md:p-8 font-mono text-sm overflow-hidden relative group">
|
| 28 |
+
<div className="absolute top-4 right-4 flex gap-2">
|
| 29 |
+
<div className="w-3 h-3 rounded-full bg-red-500/20 border border-red-500/50"></div>
|
| 30 |
+
<div className="w-3 h-3 rounded-full bg-yellow-500/20 border border-yellow-500/50"></div>
|
| 31 |
+
<div className="w-3 h-3 rounded-full bg-green-500/20 border border-green-500/50"></div>
|
| 32 |
+
</div>
|
| 33 |
+
<div className="text-gray-500 mb-2"># Install the SDK</div>
|
| 34 |
+
<div className="text-teal-400 mb-6">$ npm install @aux/sdk</div>
|
| 35 |
+
|
| 36 |
+
<div className="text-gray-500 mb-2"># Run a simulation</div>
|
| 37 |
+
<div className="text-purple-300">import</div> <div className="text-white inline">{`{ AuxClient }`}</div> <div className="text-purple-300 inline">from</div> <div className="text-green-300 inline">'@aux/sdk'</div>;
|
| 38 |
+
<br/>
|
| 39 |
+
<br/>
|
| 40 |
+
<div className="text-purple-300">const</div> <div className="text-white inline">client</div> = <div className="text-purple-300 inline">new</div> <div className="text-yellow-300 inline">AuxClient</div>({`{ apiKey: '...' }`});
|
| 41 |
+
<br/>
|
| 42 |
+
<br/>
|
| 43 |
+
<div className="text-purple-300">const</div> <div className="text-white inline">result</div> = <div className="text-purple-300 inline">await</div> client.simulation.<div className="text-blue-300 inline">create</div>({`{`}
|
| 44 |
+
<br/>
|
| 45 |
+
audience: <div className="text-green-300 inline">'tech-founders'</div>,
|
| 46 |
+
<br/>
|
| 47 |
+
content: <div className="text-green-300 inline">'https://myapp.com/launch'</div>
|
| 48 |
+
<br/>
|
| 49 |
+
{`}`});
|
| 50 |
+
</div>
|
| 51 |
+
|
| 52 |
+
{/* Docs Links Grid */}
|
| 53 |
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
| 54 |
+
<div className="bg-gray-900/20 border border-gray-800 p-6 rounded-xl hover:bg-gray-900/40 transition-colors cursor-pointer group">
|
| 55 |
+
<Book className="w-8 h-8 text-teal-500 mb-4 group-hover:scale-110 transition-transform" />
|
| 56 |
+
<h3 className="text-white font-medium mb-2">Quick Start Guide</h3>
|
| 57 |
+
<p className="text-gray-500 text-sm">Deploy your first simulation in under 5 minutes.</p>
|
| 58 |
+
</div>
|
| 59 |
+
<div className="bg-gray-900/20 border border-gray-800 p-6 rounded-xl hover:bg-gray-900/40 transition-colors cursor-pointer group">
|
| 60 |
+
<Terminal className="w-8 h-8 text-purple-500 mb-4 group-hover:scale-110 transition-transform" />
|
| 61 |
+
<h3 className="text-white font-medium mb-2">API Reference</h3>
|
| 62 |
+
<p className="text-gray-500 text-sm">Detailed endpoints, parameters, and response types.</p>
|
| 63 |
+
</div>
|
| 64 |
+
<div className="bg-gray-900/20 border border-gray-800 p-6 rounded-xl hover:bg-gray-900/40 transition-colors cursor-pointer group">
|
| 65 |
+
<Code className="w-8 h-8 text-blue-500 mb-4 group-hover:scale-110 transition-transform" />
|
| 66 |
+
<h3 className="text-white font-medium mb-2">SDKs & Libraries</h3>
|
| 67 |
+
<p className="text-gray-500 text-sm">Official libraries for Node.js, Python, and Go.</p>
|
| 68 |
+
</div>
|
| 69 |
+
<div className="bg-gray-900/20 border border-gray-800 p-6 rounded-xl hover:bg-gray-900/40 transition-colors cursor-pointer group">
|
| 70 |
+
<Zap className="w-8 h-8 text-yellow-500 mb-4 group-hover:scale-110 transition-transform" />
|
| 71 |
+
<h3 className="text-white font-medium mb-2">Webhooks</h3>
|
| 72 |
+
<p className="text-gray-500 text-sm">Real-time event notifications for your integrations.</p>
|
| 73 |
+
</div>
|
| 74 |
+
</div>
|
| 75 |
+
</div>
|
| 76 |
+
</div>
|
| 77 |
+
</section>
|
| 78 |
+
);
|
| 79 |
+
};
|
| 80 |
+
|
| 81 |
+
export default Documentation;
|
components/FAQ.tsx
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState } from 'react';
|
| 2 |
+
import { FAQS } from '../constants';
|
| 3 |
+
import { ChevronDown, ChevronUp } from 'lucide-react';
|
| 4 |
+
|
| 5 |
+
const FAQ: React.FC = () => {
|
| 6 |
+
const [openIndex, setOpenIndex] = useState<number | null>(0);
|
| 7 |
+
|
| 8 |
+
const toggleFAQ = (index: number) => {
|
| 9 |
+
setOpenIndex(openIndex === index ? null : index);
|
| 10 |
+
};
|
| 11 |
+
|
| 12 |
+
return (
|
| 13 |
+
<section className="py-24 bg-black">
|
| 14 |
+
<div className="max-w-7xl mx-auto px-6 grid grid-cols-1 lg:grid-cols-2 gap-16">
|
| 15 |
+
<div>
|
| 16 |
+
<div className="inline-block px-4 py-1.5 rounded-full border border-gray-700 text-sm text-gray-300 mb-8">FAQ</div>
|
| 17 |
+
<h2 className="text-3xl md:text-5xl font-semibold mb-6">
|
| 18 |
+
We simulated what questions you need answering
|
| 19 |
+
</h2>
|
| 20 |
+
<p className="text-gray-400 text-lg mb-8">
|
| 21 |
+
Explore quick solutions to common questions. Need more? Feel free to contact our <u className="text-white cursor-pointer">support team</u>.
|
| 22 |
+
</p>
|
| 23 |
+
</div>
|
| 24 |
+
|
| 25 |
+
<div className="space-y-4">
|
| 26 |
+
{FAQS.map((faq, idx) => (
|
| 27 |
+
<div
|
| 28 |
+
key={idx}
|
| 29 |
+
className="border border-gray-800 rounded-xl overflow-hidden bg-gray-900/20"
|
| 30 |
+
>
|
| 31 |
+
<button
|
| 32 |
+
className="w-full flex justify-between items-center p-6 text-left hover:bg-gray-900/50 transition-colors"
|
| 33 |
+
onClick={() => toggleFAQ(idx)}
|
| 34 |
+
>
|
| 35 |
+
<span className="font-medium text-lg">{faq.question}</span>
|
| 36 |
+
{openIndex === idx ? <ChevronUp className="text-gray-400" /> : <ChevronDown className="text-gray-400" />}
|
| 37 |
+
</button>
|
| 38 |
+
|
| 39 |
+
<div
|
| 40 |
+
className={`transition-all duration-300 ease-in-out overflow-hidden ${openIndex === idx ? 'max-h-48 opacity-100' : 'max-h-0 opacity-0'}`}
|
| 41 |
+
>
|
| 42 |
+
<div className="p-6 pt-0 text-gray-400 leading-relaxed">
|
| 43 |
+
{faq.answer}
|
| 44 |
+
</div>
|
| 45 |
+
</div>
|
| 46 |
+
</div>
|
| 47 |
+
))}
|
| 48 |
+
</div>
|
| 49 |
+
</div>
|
| 50 |
+
</section>
|
| 51 |
+
);
|
| 52 |
+
};
|
| 53 |
+
|
| 54 |
+
export default FAQ;
|
components/Footer.tsx
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
|
| 3 |
+
const Footer: React.FC = () => {
|
| 4 |
+
return (
|
| 5 |
+
<footer className="py-12 bg-black border-t border-gray-900 text-sm text-gray-500">
|
| 6 |
+
<div className="max-w-7xl mx-auto px-6 flex flex-col md:flex-row justify-between items-center gap-6">
|
| 7 |
+
<div className="flex items-center gap-2">
|
| 8 |
+
<div className="w-6 h-6 flex items-center justify-center font-bold text-white bg-gray-800 rounded">Λ</div>
|
| 9 |
+
<span className="text-white font-semibold">SyncUsers</span>
|
| 10 |
+
</div>
|
| 11 |
+
|
| 12 |
+
<div className="flex gap-8">
|
| 13 |
+
<a href="#" className="hover:text-white transition-colors">Twitter</a>
|
| 14 |
+
<a href="#" className="hover:text-white transition-colors">LinkedIn</a>
|
| 15 |
+
<a href="#" className="hover:text-white transition-colors">Privacy</a>
|
| 16 |
+
<a href="#" className="hover:text-white transition-colors">Terms</a>
|
| 17 |
+
</div>
|
| 18 |
+
|
| 19 |
+
<div>
|
| 20 |
+
© 2024 SyncUsers Inc.
|
| 21 |
+
</div>
|
| 22 |
+
</div>
|
| 23 |
+
</footer>
|
| 24 |
+
);
|
| 25 |
+
};
|
| 26 |
+
|
| 27 |
+
export default Footer;
|
components/Hero.tsx
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import React, { useState, useEffect } from 'react';
|
| 3 |
+
import Button from './ui/Button';
|
| 4 |
+
import RealNetworkGraph from './RealNetworkGraph';
|
| 5 |
+
|
| 6 |
+
interface HeroProps {
|
| 7 |
+
onStart?: () => void;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
const Hero: React.FC<HeroProps> = ({ onStart }) => {
|
| 11 |
+
const [currentSlide, setCurrentSlide] = useState(0);
|
| 12 |
+
|
| 13 |
+
useEffect(() => {
|
| 14 |
+
const interval = setInterval(() => {
|
| 15 |
+
setCurrentSlide((prev) => (prev === 0 ? 1 : 0));
|
| 16 |
+
}, 6000);
|
| 17 |
+
return () => clearInterval(interval);
|
| 18 |
+
}, []);
|
| 19 |
+
|
| 20 |
+
return (
|
| 21 |
+
<section className="relative min-h-[90vh] flex flex-col items-center justify-center pt-32 pb-20 overflow-hidden">
|
| 22 |
+
{/* Background Ambience */}
|
| 23 |
+
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-[800px] h-[500px] bg-purple-900/10 rounded-full blur-[120px] pointer-events-none" />
|
| 24 |
+
|
| 25 |
+
<div className="relative z-10 max-w-7xl mx-auto px-6 w-full grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
|
| 26 |
+
|
| 27 |
+
{/* Left Content - Animated Transitions */}
|
| 28 |
+
<div className="space-y-8 relative h-[400px]">
|
| 29 |
+
{/* Slide 1 */}
|
| 30 |
+
<div className={`absolute top-0 left-0 w-full transition-all duration-1000 ease-in-out transform ${currentSlide === 0 ? 'opacity-100 translate-y-0' : 'opacity-0 -translate-y-10 pointer-events-none'}`}>
|
| 31 |
+
<h1 className="text-5xl md:text-7xl font-bold leading-tight">
|
| 32 |
+
User Simulation <span className="text-gray-400">API</span> for Developers
|
| 33 |
+
</h1>
|
| 34 |
+
<p className="text-xl text-gray-400 mt-6 max-w-lg">
|
| 35 |
+
Programmatically test your UX decisions. Integrate accurate user simulation into your CI/CD pipeline. Free for developers.
|
| 36 |
+
</p>
|
| 37 |
+
<div className="flex flex-wrap gap-4 mt-8">
|
| 38 |
+
<Button variant="primary" size="lg" onClick={onStart}>HF Space Demo</Button>
|
| 39 |
+
<Button variant="outline" size="lg" onClick={() => window.location.href='#docs'}>Read the Docs</Button>
|
| 40 |
+
</div>
|
| 41 |
+
</div>
|
| 42 |
+
|
| 43 |
+
{/* Slide 2 */}
|
| 44 |
+
<div className={`absolute top-0 left-0 w-full transition-all duration-1000 ease-in-out transform ${currentSlide === 1 ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-10 pointer-events-none'}`}>
|
| 45 |
+
<h1 className="text-5xl md:text-7xl font-bold leading-tight">
|
| 46 |
+
Stop guessing. <span className="bg-gradient-to-r from-purple-400 to-pink-600 bg-clip-text text-transparent">Start simulating.</span>
|
| 47 |
+
</h1>
|
| 48 |
+
<p className="text-xl text-gray-400 mt-6 max-w-lg">
|
| 49 |
+
Validate features with AI-generated user societies before writing a single line of frontend code. The open standard for agentic UX testing.
|
| 50 |
+
</p>
|
| 51 |
+
<div className="flex flex-wrap gap-4 mt-8">
|
| 52 |
+
<Button variant="primary" size="lg" onClick={onStart}>Start Building</Button>
|
| 53 |
+
<Button variant="outline" size="lg" onClick={() => window.location.href='#docs'}>Documentation</Button>
|
| 54 |
+
</div>
|
| 55 |
+
</div>
|
| 56 |
+
</div>
|
| 57 |
+
|
| 58 |
+
{/* Right Visuals - Real Network Graph */}
|
| 59 |
+
<div className="relative h-[500px] w-full flex items-center justify-center">
|
| 60 |
+
<div
|
| 61 |
+
className="w-full h-full opacity-80"
|
| 62 |
+
style={{
|
| 63 |
+
maskImage: 'linear-gradient(to bottom, black 0%, black 70%, transparent 100%)',
|
| 64 |
+
WebkitMaskImage: 'linear-gradient(to bottom, black 0%, black 70%, transparent 100%)'
|
| 65 |
+
}}
|
| 66 |
+
>
|
| 67 |
+
<RealNetworkGraph />
|
| 68 |
+
</div>
|
| 69 |
+
|
| 70 |
+
{/* Overlay Elements for depth */}
|
| 71 |
+
<div className="absolute inset-0 pointer-events-none bg-gradient-to-t from-black via-transparent to-transparent"></div>
|
| 72 |
+
</div>
|
| 73 |
+
</div>
|
| 74 |
+
</section>
|
| 75 |
+
);
|
| 76 |
+
};
|
| 77 |
+
|
| 78 |
+
export default Hero;
|
components/HowItWorks.tsx
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { User, Share2, Cpu, CheckCircle, Scale } from 'lucide-react';
|
| 3 |
+
|
| 4 |
+
const HowItWorks: React.FC = () => {
|
| 5 |
+
const steps = [
|
| 6 |
+
{ icon: <User />, label: "Persona Creation" },
|
| 7 |
+
{ icon: <Share2 />, label: "Focus Group Construction" },
|
| 8 |
+
{ icon: <Cpu />, label: "AI-driven Simulations" },
|
| 9 |
+
{ icon: <CheckCircle />, label: "Result Generation" },
|
| 10 |
+
{ icon: <Scale />, label: "Automatic A/B Testing" },
|
| 11 |
+
];
|
| 12 |
+
|
| 13 |
+
return (
|
| 14 |
+
<section id="how-it-works" className="py-24 bg-black border-t border-gray-900">
|
| 15 |
+
<div className="max-w-7xl mx-auto px-6 text-center">
|
| 16 |
+
<div className="inline-block px-4 py-1.5 rounded-full border border-gray-700 text-sm text-gray-300 mb-8">How It Works</div>
|
| 17 |
+
<h2 className="text-3xl md:text-5xl font-semibold mb-20">From raw data to real understanding</h2>
|
| 18 |
+
|
| 19 |
+
{/* Steps Navigation/Visual */}
|
| 20 |
+
<div className="flex flex-wrap justify-center gap-4 md:gap-8 mb-20">
|
| 21 |
+
{steps.map((step, idx) => (
|
| 22 |
+
<div key={idx} className="flex flex-col items-center gap-3 group cursor-default">
|
| 23 |
+
<div className="w-16 h-12 md:w-auto md:h-auto px-6 py-3 rounded-full border border-gray-700 bg-gray-900/50 flex items-center gap-2 text-gray-400 group-hover:text-white group-hover:border-gray-500 transition-all">
|
| 24 |
+
{step.icon}
|
| 25 |
+
<span className="text-sm font-medium hidden md:inline">{step.label}</span>
|
| 26 |
+
</div>
|
| 27 |
+
</div>
|
| 28 |
+
))}
|
| 29 |
+
</div>
|
| 30 |
+
|
| 31 |
+
{/* Abstract Visualization */}
|
| 32 |
+
<div className="relative h-[400px] w-full max-w-4xl mx-auto border border-gray-800 rounded-3xl bg-black overflow-hidden flex items-center justify-center">
|
| 33 |
+
<div className="absolute inset-0 bg-[url('https://www.transparenttextures.com/patterns/stardust.png')] opacity-20"></div>
|
| 34 |
+
{/* Radar/Network Graphic */}
|
| 35 |
+
<div className="relative w-96 h-96">
|
| 36 |
+
<div className="absolute inset-0 border border-blue-900/30 rounded-full animate-ping duration-[3000ms]"></div>
|
| 37 |
+
<div className="absolute inset-10 border border-blue-800/30 rounded-full"></div>
|
| 38 |
+
<div className="absolute inset-20 border border-blue-700/30 rounded-full"></div>
|
| 39 |
+
|
| 40 |
+
{/* Connecting Lines */}
|
| 41 |
+
<svg className="absolute inset-0 w-full h-full pointer-events-none">
|
| 42 |
+
{/* Center to Top (in) */}
|
| 43 |
+
<line x1="50%" y1="50%" x2="50%" y2="5%" stroke="#3b82f6" strokeWidth="2" strokeOpacity="0.5" />
|
| 44 |
+
{/* Center to Bottom Right (X) */}
|
| 45 |
+
<line x1="50%" y1="50%" x2="85%" y2="85%" stroke="#3b82f6" strokeWidth="2" strokeOpacity="0.5" />
|
| 46 |
+
{/* Center to Bottom Left (Globe) */}
|
| 47 |
+
<line x1="50%" y1="50%" x2="15%" y2="85%" stroke="#3b82f6" strokeWidth="2" strokeOpacity="0.5" />
|
| 48 |
+
</svg>
|
| 49 |
+
|
| 50 |
+
{/* Center Node */}
|
| 51 |
+
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-16 h-16 bg-gray-900 rounded-full border-2 border-blue-500 flex items-center justify-center shadow-[0_0_30px_rgba(59,130,246,0.5)] z-10">
|
| 52 |
+
<User className="text-white w-6 h-6" />
|
| 53 |
+
</div>
|
| 54 |
+
|
| 55 |
+
{/* Satellite Nodes */}
|
| 56 |
+
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-10 h-10 bg-gray-900 border border-gray-600 rounded-full flex items-center justify-center z-10">
|
| 57 |
+
<span className="text-xs font-bold">in</span>
|
| 58 |
+
</div>
|
| 59 |
+
<div className="absolute bottom-10 right-10 w-10 h-10 bg-gray-900 border border-gray-600 rounded-full flex items-center justify-center z-10">
|
| 60 |
+
<span className="text-xs font-bold">X</span>
|
| 61 |
+
</div>
|
| 62 |
+
<div className="absolute bottom-10 left-10 w-10 h-10 bg-gray-900 border border-gray-600 rounded-full flex items-center justify-center z-10">
|
| 63 |
+
<GlobeIcon />
|
| 64 |
+
</div>
|
| 65 |
+
</div>
|
| 66 |
+
</div>
|
| 67 |
+
</div>
|
| 68 |
+
</section>
|
| 69 |
+
);
|
| 70 |
+
};
|
| 71 |
+
|
| 72 |
+
const GlobeIcon = () => (
|
| 73 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="w-4 h-4">
|
| 74 |
+
<circle cx="12" cy="12" r="10"></circle>
|
| 75 |
+
<line x1="2" y1="12" x2="22" y2="12"></line>
|
| 76 |
+
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path>
|
| 77 |
+
</svg>
|
| 78 |
+
);
|
| 79 |
+
|
| 80 |
+
export default HowItWorks;
|
components/InteractiveDemo.tsx
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect, useMemo } from 'react';
|
| 2 |
+
import Button from './ui/Button';
|
| 3 |
+
import { Check, Sparkles, Send, MessageSquare, X } from 'lucide-react';
|
| 4 |
+
|
| 5 |
+
// --- Shared Components ---
|
| 6 |
+
|
| 7 |
+
const NetworkBackground: React.FC<{ className?: string }> = ({ className = "" }) => {
|
| 8 |
+
// Static node positions for consistency in other steps
|
| 9 |
+
const nodes = [
|
| 10 |
+
{ cx: "20%", cy: "20%", r: 4, fill: "#14b8a6" }, // teal
|
| 11 |
+
{ cx: "50%", cy: "10%", r: 6, fill: "#a855f7" }, // purple
|
| 12 |
+
{ cx: "80%", cy: "30%", r: 5, fill: "#f97316" }, // orange
|
| 13 |
+
{ cx: "30%", cy: "50%", r: 7, fill: "#3b82f6" }, // blue
|
| 14 |
+
{ cx: "70%", cy: "60%", r: 6, fill: "#ec4899" }, // pink
|
| 15 |
+
{ cx: "10%", cy: "70%", r: 5, fill: "#ef4444" }, // red
|
| 16 |
+
{ cx: "40%", cy: "80%", r: 4, fill: "#14b8a6" },
|
| 17 |
+
{ cx: "90%", cy: "80%", r: 5, fill: "#8b5cf6" },
|
| 18 |
+
{ cx: "60%", cy: "40%", r: 3, fill: "#eab308" },
|
| 19 |
+
{ cx: "25%", cy: "90%", r: 6, fill: "#3b82f6" },
|
| 20 |
+
];
|
| 21 |
+
|
| 22 |
+
return (
|
| 23 |
+
<div className={`absolute inset-0 overflow-hidden pointer-events-none opacity-30 ${className}`}>
|
| 24 |
+
<svg className="w-full h-full">
|
| 25 |
+
{/* Lines */}
|
| 26 |
+
<line x1="20%" y1="20%" x2="50%" y2="10%" stroke="#374151" strokeWidth="1" />
|
| 27 |
+
<line x1="50%" y1="10%" x2="80%" y2="30%" stroke="#374151" strokeWidth="1" />
|
| 28 |
+
<line x1="20%" y1="20%" x2="30%" y2="50%" stroke="#374151" strokeWidth="1" />
|
| 29 |
+
<line x1="30%" y1="50%" x2="70%" y2="60%" stroke="#374151" strokeWidth="1" />
|
| 30 |
+
<line x1="70%" y1="60%" x2="80%" y2="30%" stroke="#374151" strokeWidth="1" />
|
| 31 |
+
<line x1="30%" y1="50%" x2="10%" y2="70%" stroke="#374151" strokeWidth="1" />
|
| 32 |
+
<line x1="40%" y1="80%" x2="10%" y2="70%" stroke="#374151" strokeWidth="1" />
|
| 33 |
+
<line x1="70%" y1="60%" x2="90%" y2="80%" stroke="#374151" strokeWidth="1" />
|
| 34 |
+
<line x1="50%" y1="10%" x2="60%" y2="40%" stroke="#374151" strokeWidth="1" />
|
| 35 |
+
<line x1="25%" y1="90%" x2="40%" y2="80%" stroke="#374151" strokeWidth="1" />
|
| 36 |
+
|
| 37 |
+
{/* Nodes */}
|
| 38 |
+
{nodes.map((node, i) => (
|
| 39 |
+
<circle key={i} {...node} className="animate-pulse" style={{ animationDelay: `${i * 0.5}s` }} />
|
| 40 |
+
))}
|
| 41 |
+
</svg>
|
| 42 |
+
</div>
|
| 43 |
+
);
|
| 44 |
+
};
|
| 45 |
+
|
| 46 |
+
const SectionLayout: React.FC<{
|
| 47 |
+
number: string;
|
| 48 |
+
title: string;
|
| 49 |
+
description: React.ReactNode;
|
| 50 |
+
visual: React.ReactNode;
|
| 51 |
+
}> = ({ number, title, description, visual }) => {
|
| 52 |
+
return (
|
| 53 |
+
<section className="py-24 border-t border-gray-900 bg-black">
|
| 54 |
+
<div className="max-w-7xl mx-auto px-6 grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
|
| 55 |
+
{/* Text Content */}
|
| 56 |
+
<div className="order-2 lg:order-1">
|
| 57 |
+
<div className="w-12 h-12 rounded-full border border-gray-700 flex items-center justify-center mb-8 text-xl font-mono text-white">
|
| 58 |
+
{number}
|
| 59 |
+
</div>
|
| 60 |
+
<h2 className="text-4xl md:text-5xl font-semibold mb-6 text-white">{title}</h2>
|
| 61 |
+
<div className="text-xl text-gray-400 leading-relaxed max-w-lg">
|
| 62 |
+
{description}
|
| 63 |
+
</div>
|
| 64 |
+
</div>
|
| 65 |
+
|
| 66 |
+
{/* Visual Content */}
|
| 67 |
+
<div className="order-1 lg:order-2 relative h-[500px] bg-gray-900/20 border border-gray-800 rounded-3xl overflow-hidden flex items-center justify-center">
|
| 68 |
+
<div className="absolute inset-0 w-full h-full flex items-center justify-center p-0">
|
| 69 |
+
{visual}
|
| 70 |
+
</div>
|
| 71 |
+
</div>
|
| 72 |
+
</div>
|
| 73 |
+
</section>
|
| 74 |
+
);
|
| 75 |
+
};
|
| 76 |
+
|
| 77 |
+
// --- Step 1: Generate Focus Group ---
|
| 78 |
+
|
| 79 |
+
const Step1 = () => {
|
| 80 |
+
const [status, setStatus] = useState<'input' | 'creating' | 'ready'>('input');
|
| 81 |
+
const [inputValue, setInputValue] = useState("AI-focused startup founders in Europe");
|
| 82 |
+
|
| 83 |
+
const handleCreate = () => {
|
| 84 |
+
setStatus('creating');
|
| 85 |
+
setTimeout(() => {
|
| 86 |
+
setStatus('ready');
|
| 87 |
+
}, 2000);
|
| 88 |
+
};
|
| 89 |
+
|
| 90 |
+
return (
|
| 91 |
+
<SectionLayout
|
| 92 |
+
number="1"
|
| 93 |
+
title="Generate Any Focus Group"
|
| 94 |
+
description={
|
| 95 |
+
<div className="space-y-6">
|
| 96 |
+
<p>
|
| 97 |
+
Use plain english to describe your target audience, or generate a personal focus group based on your real social media interactions.
|
| 98 |
+
</p>
|
| 99 |
+
<div className="space-y-2 pt-2">
|
| 100 |
+
<p className="text-base font-medium text-gray-300">
|
| 101 |
+
Personalize your experience with your own LinkedIn network database.
|
| 102 |
+
</p>
|
| 103 |
+
<p className="text-sm text-gray-500">
|
| 104 |
+
LinkedIn and X data are going into the persona simulator.
|
| 105 |
+
</p>
|
| 106 |
+
</div>
|
| 107 |
+
</div>
|
| 108 |
+
}
|
| 109 |
+
visual={
|
| 110 |
+
<>
|
| 111 |
+
{/* Background for all phases, fully visible in 'ready' */}
|
| 112 |
+
<div className={`transition-opacity duration-1000 ${status === 'ready' ? 'opacity-100' : 'opacity-30'}`}>
|
| 113 |
+
<NetworkBackground className="opacity-100" />
|
| 114 |
+
</div>
|
| 115 |
+
|
| 116 |
+
{/* Input State */}
|
| 117 |
+
<div className={`transition-all duration-500 absolute w-full max-w-sm z-20 ${status === 'input' ? 'opacity-100 scale-100' : 'opacity-0 scale-95 pointer-events-none'}`}>
|
| 118 |
+
<div className="bg-black border border-gray-800 rounded-xl p-6 shadow-2xl">
|
| 119 |
+
<div className="flex justify-between items-center mb-6">
|
| 120 |
+
<span className="text-gray-400 text-sm">Target Audience</span>
|
| 121 |
+
<button className="text-gray-500 hover:text-white"><X size={16}/></button>
|
| 122 |
+
</div>
|
| 123 |
+
<h4 className="text-xl mb-4 text-center">Who would you like to simulate?</h4>
|
| 124 |
+
<div className="relative mb-6">
|
| 125 |
+
<input
|
| 126 |
+
type="text"
|
| 127 |
+
value={inputValue}
|
| 128 |
+
onChange={(e) => setInputValue(e.target.value)}
|
| 129 |
+
className="w-full bg-gray-900 border border-gray-700 rounded-lg py-3 px-4 text-white focus:outline-none focus:border-teal-500"
|
| 130 |
+
/>
|
| 131 |
+
<span className="absolute right-3 top-3.5 w-0.5 h-5 bg-teal-500 animate-blink"></span>
|
| 132 |
+
</div>
|
| 133 |
+
<Button className="w-full py-3" onClick={handleCreate}>
|
| 134 |
+
Generate Your Focus Group <Sparkles size={16} className="ml-2" />
|
| 135 |
+
</Button>
|
| 136 |
+
</div>
|
| 137 |
+
</div>
|
| 138 |
+
|
| 139 |
+
{/* Creating State */}
|
| 140 |
+
<div className={`transition-all duration-500 absolute z-20 ${status === 'creating' ? 'opacity-100 scale-100' : 'opacity-0 scale-95 pointer-events-none'}`}>
|
| 141 |
+
<div className="bg-black/80 backdrop-blur-md border border-gray-700 rounded-full px-8 py-4 flex items-center gap-3 shadow-2xl">
|
| 142 |
+
<Sparkles className="text-teal-400 animate-pulse" />
|
| 143 |
+
<span className="text-lg font-medium">Generating Focus Group...</span>
|
| 144 |
+
</div>
|
| 145 |
+
</div>
|
| 146 |
+
|
| 147 |
+
{/* Ready State */}
|
| 148 |
+
<div className={`transition-all duration-1000 absolute inset-0 flex items-end justify-center pb-8 ${status === 'ready' ? 'opacity-100' : 'opacity-0 pointer-events-none'}`}>
|
| 149 |
+
{/* Bottom Toast Overlay */}
|
| 150 |
+
<div className="bg-black/80 backdrop-blur border border-gray-800 rounded-full pl-3 pr-6 py-2 flex items-center gap-3 shadow-2xl pointer-events-auto">
|
| 151 |
+
<div className="bg-green-500/20 rounded-full p-1">
|
| 152 |
+
<Check className="w-4 h-4 text-green-500" />
|
| 153 |
+
</div>
|
| 154 |
+
<span className="text-sm font-medium text-white whitespace-nowrap">Your Personal Focus Group is Ready</span>
|
| 155 |
+
</div>
|
| 156 |
+
<div className="absolute bottom-2 text-center pointer-events-auto">
|
| 157 |
+
<button
|
| 158 |
+
onClick={() => setStatus('input')}
|
| 159 |
+
className="text-xs text-gray-500 hover:text-white transition-colors"
|
| 160 |
+
>
|
| 161 |
+
Start Over
|
| 162 |
+
</button>
|
| 163 |
+
</div>
|
| 164 |
+
</div>
|
| 165 |
+
</>
|
| 166 |
+
}
|
| 167 |
+
/>
|
| 168 |
+
);
|
| 169 |
+
};
|
| 170 |
+
|
| 171 |
+
// --- Step 2: Run Experiments ---
|
| 172 |
+
|
| 173 |
+
const Step2 = () => {
|
| 174 |
+
const [isSimulating, setIsSimulating] = useState(false);
|
| 175 |
+
|
| 176 |
+
const handleSimulate = () => {
|
| 177 |
+
setIsSimulating(true);
|
| 178 |
+
setTimeout(() => {
|
| 179 |
+
setIsSimulating(false);
|
| 180 |
+
}, 2500);
|
| 181 |
+
};
|
| 182 |
+
|
| 183 |
+
return (
|
| 184 |
+
<SectionLayout
|
| 185 |
+
number="2"
|
| 186 |
+
title="Run Rapid Experiments"
|
| 187 |
+
description="Execute simulations in minutes to find the optimal form of your content or idea."
|
| 188 |
+
visual={
|
| 189 |
+
<>
|
| 190 |
+
<NetworkBackground />
|
| 191 |
+
<div className="w-full max-w-sm relative z-10">
|
| 192 |
+
{/* Post Card */}
|
| 193 |
+
<div className={`bg-black border border-gray-800 rounded-xl p-6 shadow-2xl transition-all duration-500 ${isSimulating ? 'opacity-40 blur-sm scale-95' : 'opacity-100 scale-100'}`}>
|
| 194 |
+
<div className="flex items-center gap-3 mb-4">
|
| 195 |
+
<div className="w-10 h-10 rounded-full bg-gray-700"></div>
|
| 196 |
+
<div>
|
| 197 |
+
<div className="w-24 h-3 bg-gray-700 rounded mb-1"></div>
|
| 198 |
+
<div className="w-16 h-2 bg-gray-800 rounded"></div>
|
| 199 |
+
</div>
|
| 200 |
+
</div>
|
| 201 |
+
<div className="space-y-2 mb-6">
|
| 202 |
+
<div className="w-full h-2 bg-gray-800 rounded"></div>
|
| 203 |
+
<div className="w-full h-2 bg-gray-800 rounded"></div>
|
| 204 |
+
<div className="w-3/4 h-2 bg-gray-800 rounded"></div>
|
| 205 |
+
</div>
|
| 206 |
+
<div className="bg-gray-900 rounded-lg p-4 mb-4 border border-gray-800">
|
| 207 |
+
<p className="text-gray-300 text-sm">
|
| 208 |
+
We just secured $5.3M to build AI-native tools...
|
| 209 |
+
</p>
|
| 210 |
+
</div>
|
| 211 |
+
<Button
|
| 212 |
+
className="w-full flex items-center justify-center gap-2"
|
| 213 |
+
onClick={handleSimulate}
|
| 214 |
+
>
|
| 215 |
+
Simulate Post <Send size={16} />
|
| 216 |
+
</Button>
|
| 217 |
+
</div>
|
| 218 |
+
|
| 219 |
+
{/* Simulating Overlay */}
|
| 220 |
+
<div className={`absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 transition-all duration-300 ${isSimulating ? 'opacity-100 scale-100' : 'opacity-0 scale-90 pointer-events-none'}`}>
|
| 221 |
+
<div className="bg-black/90 backdrop-blur border border-gray-700 rounded-2xl p-6 shadow-2xl flex flex-col items-center gap-4 min-w-[200px]">
|
| 222 |
+
<div className="relative">
|
| 223 |
+
<div className="w-12 h-12 rounded-full border-2 border-gray-800 border-t-teal-500 animate-spin"></div>
|
| 224 |
+
<Sparkles className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-teal-500 w-5 h-5" />
|
| 225 |
+
</div>
|
| 226 |
+
<span className="text-white font-medium">Simulating reactions...</span>
|
| 227 |
+
</div>
|
| 228 |
+
</div>
|
| 229 |
+
</div>
|
| 230 |
+
</>
|
| 231 |
+
}
|
| 232 |
+
/>
|
| 233 |
+
);
|
| 234 |
+
};
|
| 235 |
+
|
| 236 |
+
// --- Step 3: Get Insights ---
|
| 237 |
+
|
| 238 |
+
const Step3 = () => {
|
| 239 |
+
return (
|
| 240 |
+
<SectionLayout
|
| 241 |
+
number="3"
|
| 242 |
+
title="Get Instant Insights"
|
| 243 |
+
description="Evaluate the performance of your experiment with scores, comments, and summaries."
|
| 244 |
+
visual={
|
| 245 |
+
<>
|
| 246 |
+
<NetworkBackground />
|
| 247 |
+
<div className="w-full max-w-sm space-y-4 relative z-10">
|
| 248 |
+
{/* Score Card */}
|
| 249 |
+
<div className="bg-black border border-gray-800 rounded-xl p-6 shadow-2xl hover:border-gray-600 transition-colors cursor-default group">
|
| 250 |
+
<div className="flex justify-between items-start mb-6">
|
| 251 |
+
<div>
|
| 252 |
+
<span className="text-xs text-gray-400 uppercase tracking-wider">Impact Score</span>
|
| 253 |
+
<div className="text-3xl font-bold text-white mt-1">88<span className="text-base font-normal text-gray-500">/100</span></div>
|
| 254 |
+
</div>
|
| 255 |
+
<div className="bg-green-500/10 text-green-400 text-xs px-2 py-1 rounded border border-green-500/20">Exceptional</div>
|
| 256 |
+
</div>
|
| 257 |
+
|
| 258 |
+
<div className="space-y-4">
|
| 259 |
+
<div>
|
| 260 |
+
<div className="flex justify-between text-xs text-gray-400 mb-1">
|
| 261 |
+
<span>Attention</span>
|
| 262 |
+
<span>80%</span>
|
| 263 |
+
</div>
|
| 264 |
+
<div className="h-1.5 bg-gray-800 rounded-full overflow-hidden">
|
| 265 |
+
<div className="h-full bg-green-500 w-[80%] rounded-full group-hover:bg-green-400 transition-colors"></div>
|
| 266 |
+
</div>
|
| 267 |
+
</div>
|
| 268 |
+
<div>
|
| 269 |
+
<div className="flex justify-between text-xs text-gray-400 mb-1">
|
| 270 |
+
<span>Relevance</span>
|
| 271 |
+
<span>92%</span>
|
| 272 |
+
</div>
|
| 273 |
+
<div className="h-1.5 bg-gray-800 rounded-full overflow-hidden">
|
| 274 |
+
<div className="h-full bg-teal-500 w-[92%] rounded-full group-hover:bg-teal-400 transition-colors"></div>
|
| 275 |
+
</div>
|
| 276 |
+
</div>
|
| 277 |
+
</div>
|
| 278 |
+
</div>
|
| 279 |
+
|
| 280 |
+
{/* Feedback Card */}
|
| 281 |
+
<div className="bg-gray-900/50 border border-gray-800 rounded-xl p-5 backdrop-blur-sm animate-fade-in-up" style={{ animationDelay: '0.2s' }}>
|
| 282 |
+
<div className="flex items-center gap-2 mb-3">
|
| 283 |
+
<MessageSquare size={14} className="text-purple-400" />
|
| 284 |
+
<span className="text-xs font-medium text-purple-200">Key Insight</span>
|
| 285 |
+
</div>
|
| 286 |
+
<p className="text-sm text-gray-300 leading-relaxed italic">
|
| 287 |
+
"Founders in the EU region responded strongly to the 'no-code' angle, seeing it as a major time-saver."
|
| 288 |
+
</p>
|
| 289 |
+
</div>
|
| 290 |
+
</div>
|
| 291 |
+
</>
|
| 292 |
+
}
|
| 293 |
+
/>
|
| 294 |
+
);
|
| 295 |
+
};
|
| 296 |
+
|
| 297 |
+
// --- Step 4: Forecast Outcome ---
|
| 298 |
+
|
| 299 |
+
const Step4 = () => {
|
| 300 |
+
const [activeVariant, setActiveVariant] = useState(0);
|
| 301 |
+
|
| 302 |
+
const variants = [
|
| 303 |
+
{ label: "Original", score: 48, text: "We just secured $5.3M to build AI-native tools..." },
|
| 304 |
+
{ label: "Variant 1", score: 88, text: "Stop writing code before you have product-market fit. We just raised $5.3M to help you simulate it first." },
|
| 305 |
+
{ label: "Variant 2", score: 83, text: "Big news: $5.3M raised! We're building the future of founder tools in Europe." },
|
| 306 |
+
];
|
| 307 |
+
|
| 308 |
+
return (
|
| 309 |
+
<SectionLayout
|
| 310 |
+
number="4"
|
| 311 |
+
title="Forecast Every Outcome"
|
| 312 |
+
description="SyncUsers uses your style to generate and test variations of your original post every time you run a simulation."
|
| 313 |
+
visual={
|
| 314 |
+
<div className="relative w-full max-w-lg flex items-center justify-center">
|
| 315 |
+
<NetworkBackground />
|
| 316 |
+
|
| 317 |
+
{/* Main Content Area */}
|
| 318 |
+
<div className="w-full max-w-xs bg-black border border-gray-800 rounded-xl p-6 shadow-2xl relative z-10 transition-all duration-300">
|
| 319 |
+
<div className="flex justify-between items-center mb-4">
|
| 320 |
+
<span className="text-xs text-gray-500 uppercase tracking-wide">{variants[activeVariant].label}</span>
|
| 321 |
+
<div className={`px-2 py-0.5 rounded text-xs font-bold ${activeVariant === 0 ? 'bg-gray-800 text-gray-400' : 'bg-green-900 text-green-400'}`}>
|
| 322 |
+
Score: {variants[activeVariant].score}
|
| 323 |
+
</div>
|
| 324 |
+
</div>
|
| 325 |
+
<div className="min-h-[100px]">
|
| 326 |
+
<p className="text-sm text-gray-200 leading-relaxed animate-fade-in">
|
| 327 |
+
{variants[activeVariant].text}
|
| 328 |
+
</p>
|
| 329 |
+
</div>
|
| 330 |
+
<div className="mt-4 pt-4 border-t border-gray-800 flex gap-4">
|
| 331 |
+
<div className="h-2 w-8 bg-gray-800 rounded-full"></div>
|
| 332 |
+
<div className="h-2 w-16 bg-gray-800 rounded-full"></div>
|
| 333 |
+
</div>
|
| 334 |
+
</div>
|
| 335 |
+
|
| 336 |
+
{/* Floating Menu */}
|
| 337 |
+
<div className="absolute -right-4 top-1/2 -translate-y-1/2 translate-x-1/2 w-48 bg-gray-900/90 backdrop-blur border border-gray-700 rounded-xl p-2 shadow-2xl z-20">
|
| 338 |
+
<div className="text-[10px] text-gray-500 uppercase px-2 mb-2 font-bold tracking-wider">Select Variant</div>
|
| 339 |
+
<div className="space-y-1">
|
| 340 |
+
{variants.map((v, i) => (
|
| 341 |
+
<div
|
| 342 |
+
key={i}
|
| 343 |
+
onMouseEnter={() => setActiveVariant(i)}
|
| 344 |
+
className={`p-2 rounded cursor-pointer transition-all flex justify-between items-center group ${activeVariant === i ? 'bg-white text-black' : 'hover:bg-white/10 text-gray-400'}`}
|
| 345 |
+
>
|
| 346 |
+
<span className="text-xs font-medium">{v.label}</span>
|
| 347 |
+
<span className={`text-xs font-bold ${activeVariant === i ? 'text-black' : 'text-gray-500 group-hover:text-white'}`}>{v.score}</span>
|
| 348 |
+
</div>
|
| 349 |
+
))}
|
| 350 |
+
</div>
|
| 351 |
+
</div>
|
| 352 |
+
|
| 353 |
+
{/* Connecting Line Visual */}
|
| 354 |
+
<div className="absolute right-[50%] top-1/2 w-[30%] h-[1px] bg-gradient-to-r from-transparent to-gray-700 -z-10"></div>
|
| 355 |
+
</div>
|
| 356 |
+
}
|
| 357 |
+
/>
|
| 358 |
+
);
|
| 359 |
+
};
|
| 360 |
+
|
| 361 |
+
// --- Main Container ---
|
| 362 |
+
|
| 363 |
+
const InteractiveDemo: React.FC = () => {
|
| 364 |
+
return (
|
| 365 |
+
<div className="flex flex-col">
|
| 366 |
+
<Step1 />
|
| 367 |
+
<Step2 />
|
| 368 |
+
<Step3 />
|
| 369 |
+
<Step4 />
|
| 370 |
+
</div>
|
| 371 |
+
);
|
| 372 |
+
};
|
| 373 |
+
|
| 374 |
+
export default InteractiveDemo;
|
components/Navbar.tsx
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect } from 'react';
|
| 2 |
+
import { Menu, X } from 'lucide-react';
|
| 3 |
+
import { NAV_LINKS } from '../constants';
|
| 4 |
+
import Button from './ui/Button';
|
| 5 |
+
|
| 6 |
+
interface NavbarProps {
|
| 7 |
+
onStart?: () => void;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
const Navbar: React.FC<NavbarProps> = ({ onStart }) => {
|
| 11 |
+
const [isScrolled, setIsScrolled] = useState(false);
|
| 12 |
+
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
| 13 |
+
|
| 14 |
+
useEffect(() => {
|
| 15 |
+
const handleScroll = () => {
|
| 16 |
+
setIsScrolled(window.scrollY > 20);
|
| 17 |
+
};
|
| 18 |
+
window.addEventListener('scroll', handleScroll);
|
| 19 |
+
return () => window.removeEventListener('scroll', handleScroll);
|
| 20 |
+
}, []);
|
| 21 |
+
|
| 22 |
+
return (
|
| 23 |
+
<nav className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${isScrolled ? 'bg-black/80 backdrop-blur-md py-3 border-b border-gray-800' : 'bg-transparent py-5'}`}>
|
| 24 |
+
<div className="max-w-7xl mx-auto px-6 flex items-center justify-between">
|
| 25 |
+
<div className="flex items-center gap-2 cursor-pointer" onClick={() => window.scrollTo(0,0)}>
|
| 26 |
+
<div className="w-8 h-8 flex items-center justify-center font-bold text-xl text-white">
|
| 27 |
+
{/* Stylized Logo Mockup */}
|
| 28 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="w-full h-full">
|
| 29 |
+
<path d="M12 2L2 22h20L12 2z" className="text-gray-400" />
|
| 30 |
+
<path d="M12 6L6 20h12L12 6z" className="text-white fill-white" />
|
| 31 |
+
</svg>
|
| 32 |
+
</div>
|
| 33 |
+
<span className="font-semibold text-lg tracking-tight">SyncUsers</span>
|
| 34 |
+
</div>
|
| 35 |
+
|
| 36 |
+
{/* Desktop Nav */}
|
| 37 |
+
<div className="hidden md:flex items-center gap-8">
|
| 38 |
+
{NAV_LINKS.map((link) => (
|
| 39 |
+
<a
|
| 40 |
+
key={link.label}
|
| 41 |
+
href={link.href}
|
| 42 |
+
className="text-sm font-medium text-gray-300 hover:text-white transition-colors"
|
| 43 |
+
>
|
| 44 |
+
{link.label}
|
| 45 |
+
</a>
|
| 46 |
+
))}
|
| 47 |
+
</div>
|
| 48 |
+
|
| 49 |
+
<div className="hidden md:block">
|
| 50 |
+
<Button variant="primary" size="sm" onClick={onStart}>Start Building</Button>
|
| 51 |
+
</div>
|
| 52 |
+
|
| 53 |
+
{/* Mobile Toggle */}
|
| 54 |
+
<button
|
| 55 |
+
className="md:hidden text-gray-300 hover:text-white"
|
| 56 |
+
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
| 57 |
+
>
|
| 58 |
+
{isMobileMenuOpen ? <X /> : <Menu />}
|
| 59 |
+
</button>
|
| 60 |
+
</div>
|
| 61 |
+
|
| 62 |
+
{/* Mobile Menu */}
|
| 63 |
+
{isMobileMenuOpen && (
|
| 64 |
+
<div className="md:hidden bg-black border-t border-gray-800 p-6 absolute top-full left-0 right-0">
|
| 65 |
+
<div className="flex flex-col gap-4">
|
| 66 |
+
{NAV_LINKS.map((link) => (
|
| 67 |
+
<a
|
| 68 |
+
key={link.label}
|
| 69 |
+
href={link.href}
|
| 70 |
+
className="text-lg font-medium text-gray-300 hover:text-white"
|
| 71 |
+
onClick={() => setIsMobileMenuOpen(false)}
|
| 72 |
+
>
|
| 73 |
+
{link.label}
|
| 74 |
+
</a>
|
| 75 |
+
))}
|
| 76 |
+
<div className="pt-4">
|
| 77 |
+
<Button variant="primary" className="w-full" onClick={() => { setIsMobileMenuOpen(false); if(onStart) onStart(); }}>Start Building</Button>
|
| 78 |
+
</div>
|
| 79 |
+
</div>
|
| 80 |
+
</div>
|
| 81 |
+
)}
|
| 82 |
+
</nav>
|
| 83 |
+
);
|
| 84 |
+
};
|
| 85 |
+
|
| 86 |
+
export default Navbar;
|
components/Pricing.tsx
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState } from 'react';
|
| 2 |
+
import Button from './ui/Button';
|
| 3 |
+
import { Check } from 'lucide-react';
|
| 4 |
+
|
| 5 |
+
const Pricing: React.FC = () => {
|
| 6 |
+
const [isYearly, setIsYearly] = useState(false);
|
| 7 |
+
|
| 8 |
+
const plans = [
|
| 9 |
+
{
|
| 10 |
+
name: "Free",
|
| 11 |
+
price: "$0",
|
| 12 |
+
desc: "No cost to run your first experiments",
|
| 13 |
+
features: ["Access all features", "Configure your own focus groups", "3 Starting simulation credits"],
|
| 14 |
+
cta: "Select Free",
|
| 15 |
+
featured: false
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
name: "Pro",
|
| 19 |
+
price: "$55",
|
| 20 |
+
period: "/ mo",
|
| 21 |
+
desc: "Billed monthly",
|
| 22 |
+
features: ["Everything in Free", "Unlimited focus groups", "Unlimited simulation credits"],
|
| 23 |
+
cta: "Select Pro",
|
| 24 |
+
featured: true
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
name: "Enterprise",
|
| 28 |
+
price: "Get in Touch",
|
| 29 |
+
desc: "Custom builds for businesses",
|
| 30 |
+
features: ["Custom audience builds", "Custom contexts & segments", "Data & CRM integration", "API Access", "Dedicated Account Manager"],
|
| 31 |
+
cta: "Enquire",
|
| 32 |
+
featured: false
|
| 33 |
+
}
|
| 34 |
+
];
|
| 35 |
+
|
| 36 |
+
return (
|
| 37 |
+
<section id="pricing" className="py-24 bg-black">
|
| 38 |
+
<div className="max-w-7xl mx-auto px-6">
|
| 39 |
+
<div className="text-center mb-16">
|
| 40 |
+
<h2 className="text-4xl md:text-5xl font-semibold mb-6">Get started today</h2>
|
| 41 |
+
|
| 42 |
+
{/* Toggle */}
|
| 43 |
+
<div className="inline-flex items-center bg-gray-900 rounded-full p-1 border border-gray-800 relative">
|
| 44 |
+
<button
|
| 45 |
+
className={`px-6 py-2 rounded-full text-sm font-medium transition-all ${!isYearly ? 'bg-white text-black' : 'text-gray-400 hover:text-white'}`}
|
| 46 |
+
onClick={() => setIsYearly(false)}
|
| 47 |
+
>
|
| 48 |
+
Monthly
|
| 49 |
+
</button>
|
| 50 |
+
<button
|
| 51 |
+
className={`px-6 py-2 rounded-full text-sm font-medium transition-all ${isYearly ? 'bg-white text-black' : 'text-gray-400 hover:text-white'}`}
|
| 52 |
+
onClick={() => setIsYearly(true)}
|
| 53 |
+
>
|
| 54 |
+
Yearly
|
| 55 |
+
</button>
|
| 56 |
+
<span className="absolute -top-3 -right-6 bg-teal-900 text-teal-200 text-[10px] font-bold px-2 py-0.5 rounded-full border border-teal-700">
|
| 57 |
+
-30%
|
| 58 |
+
</span>
|
| 59 |
+
</div>
|
| 60 |
+
</div>
|
| 61 |
+
|
| 62 |
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
| 63 |
+
{plans.map((plan, idx) => (
|
| 64 |
+
<div
|
| 65 |
+
key={idx}
|
| 66 |
+
className={`flex flex-col p-8 rounded-2xl border ${plan.featured ? 'border-gray-600 bg-gray-900/40 relative' : 'border-gray-800 bg-black'}`}
|
| 67 |
+
>
|
| 68 |
+
<div className="mb-8">
|
| 69 |
+
<h3 className="text-lg font-medium text-white mb-2">{plan.name}</h3>
|
| 70 |
+
<p className="text-gray-500 text-sm">{plan.name === 'Free' ? 'Get started with simulations' : plan.name === 'Pro' ? 'For founders, creators and builders' : 'Custom builds for businesses'}</p>
|
| 71 |
+
</div>
|
| 72 |
+
|
| 73 |
+
<div className="mb-8">
|
| 74 |
+
<div className="flex items-baseline">
|
| 75 |
+
<span className="text-4xl font-bold text-white">{plan.price}</span>
|
| 76 |
+
{plan.period && <span className="text-gray-400 ml-1">{plan.period}</span>}
|
| 77 |
+
</div>
|
| 78 |
+
<p className="text-gray-500 text-sm mt-2">{plan.desc}</p>
|
| 79 |
+
</div>
|
| 80 |
+
|
| 81 |
+
<div className="flex-grow space-y-4 mb-8">
|
| 82 |
+
{plan.features.map((feature, fIdx) => (
|
| 83 |
+
<div key={fIdx} className="flex items-start gap-3">
|
| 84 |
+
<Check className="w-5 h-5 text-white shrink-0" />
|
| 85 |
+
<span className="text-gray-300 text-sm">{feature}</span>
|
| 86 |
+
</div>
|
| 87 |
+
))}
|
| 88 |
+
</div>
|
| 89 |
+
|
| 90 |
+
<Button
|
| 91 |
+
variant={plan.featured ? 'primary' : 'outline'}
|
| 92 |
+
className="w-full"
|
| 93 |
+
>
|
| 94 |
+
{plan.cta}
|
| 95 |
+
</Button>
|
| 96 |
+
</div>
|
| 97 |
+
))}
|
| 98 |
+
</div>
|
| 99 |
+
</div>
|
| 100 |
+
</section>
|
| 101 |
+
);
|
| 102 |
+
};
|
| 103 |
+
|
| 104 |
+
export default Pricing;
|
components/ProductOverview.tsx
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { Target, Maximize, Zap, Code2 } from 'lucide-react';
|
| 3 |
+
|
| 4 |
+
const ProductOverview: React.FC = () => {
|
| 5 |
+
const features = [
|
| 6 |
+
{
|
| 7 |
+
icon: <Target className="w-6 h-6" />,
|
| 8 |
+
title: "Targeted",
|
| 9 |
+
desc: "Accurately model even hard-to-reach audiences via API."
|
| 10 |
+
},
|
| 11 |
+
{
|
| 12 |
+
icon: <Maximize className="w-6 h-6" />,
|
| 13 |
+
title: "Scalable",
|
| 14 |
+
desc: "Run thousands of concurrent simulations."
|
| 15 |
+
},
|
| 16 |
+
{
|
| 17 |
+
icon: <Zap className="w-6 h-6" />,
|
| 18 |
+
title: "Low Latency",
|
| 19 |
+
desc: "Get actionable insights in your pipeline in seconds."
|
| 20 |
+
},
|
| 21 |
+
{
|
| 22 |
+
icon: <Code2 className="w-6 h-6" />,
|
| 23 |
+
title: "Free for Devs",
|
| 24 |
+
desc: "Generous free tier to build and test your integration."
|
| 25 |
+
}
|
| 26 |
+
];
|
| 27 |
+
|
| 28 |
+
return (
|
| 29 |
+
<section id="features" className="py-24 bg-black">
|
| 30 |
+
<div className="max-w-7xl mx-auto px-6">
|
| 31 |
+
<div className="flex justify-center mb-8">
|
| 32 |
+
<span className="px-4 py-1.5 rounded-full border border-gray-700 text-sm text-gray-300">Product Overview</span>
|
| 33 |
+
</div>
|
| 34 |
+
|
| 35 |
+
<h2 className="text-3xl md:text-5xl font-semibold text-center max-w-4xl mx-auto leading-tight mb-20">
|
| 36 |
+
Create realistic simulations of your target audience to instantly test messages, content, or ideas
|
| 37 |
+
</h2>
|
| 38 |
+
|
| 39 |
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
| 40 |
+
{features.map((feature, idx) => (
|
| 41 |
+
<div key={idx} className="p-8 border border-gray-800 rounded-2xl hover:border-gray-600 transition-colors bg-gray-900/20">
|
| 42 |
+
<div className="w-12 h-12 rounded-full border border-gray-700 flex items-center justify-center mb-6 text-white">
|
| 43 |
+
{feature.icon}
|
| 44 |
+
</div>
|
| 45 |
+
<h3 className="text-xl font-medium mb-3">{feature.title}</h3>
|
| 46 |
+
<p className="text-gray-400 leading-relaxed">{feature.desc}</p>
|
| 47 |
+
</div>
|
| 48 |
+
))}
|
| 49 |
+
</div>
|
| 50 |
+
</div>
|
| 51 |
+
</section>
|
| 52 |
+
);
|
| 53 |
+
};
|
| 54 |
+
|
| 55 |
+
export default ProductOverview;
|
components/RealNetworkGraph.tsx
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import React, { useEffect, useRef } from 'react';
|
| 3 |
+
import Plotly from 'plotly.js';
|
| 4 |
+
|
| 5 |
+
const RealNetworkGraph: React.FC = () => {
|
| 6 |
+
const graphDiv = useRef<HTMLDivElement>(null);
|
| 7 |
+
|
| 8 |
+
useEffect(() => {
|
| 9 |
+
if (!graphDiv.current) return;
|
| 10 |
+
|
| 11 |
+
// --- Data Generation Logic ---
|
| 12 |
+
const N = 80; // Node count
|
| 13 |
+
const radius = 0.22; // Connection threshold
|
| 14 |
+
const nodes = [];
|
| 15 |
+
|
| 16 |
+
// 1. Create nodes
|
| 17 |
+
for (let i = 0; i < N; i++) {
|
| 18 |
+
nodes.push({
|
| 19 |
+
x: Math.random(),
|
| 20 |
+
y: Math.random(),
|
| 21 |
+
connections: 0
|
| 22 |
+
});
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
// 2. Create edges
|
| 26 |
+
const edgeX: (number | null)[] = [];
|
| 27 |
+
const edgeY: (number | null)[] = [];
|
| 28 |
+
|
| 29 |
+
for (let i = 0; i < N; i++) {
|
| 30 |
+
for (let j = i + 1; j < N; j++) {
|
| 31 |
+
const dx = nodes[i].x - nodes[j].x;
|
| 32 |
+
const dy = nodes[i].y - nodes[j].y;
|
| 33 |
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
| 34 |
+
|
| 35 |
+
if (dist < radius) {
|
| 36 |
+
nodes[i].connections++;
|
| 37 |
+
nodes[j].connections++;
|
| 38 |
+
edgeX.push(nodes[i].x, nodes[j].x, null);
|
| 39 |
+
edgeY.push(nodes[i].y, nodes[j].y, null);
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
const nodeX = nodes.map(n => n.x);
|
| 45 |
+
const nodeY = nodes.map(n => n.y);
|
| 46 |
+
const nodeColor = nodes.map(n => n.connections);
|
| 47 |
+
|
| 48 |
+
// --- Plotly Configuration ---
|
| 49 |
+
|
| 50 |
+
const edgeTrace = {
|
| 51 |
+
x: edgeX,
|
| 52 |
+
y: edgeY,
|
| 53 |
+
mode: 'lines',
|
| 54 |
+
line: { width: 0.5, color: '#4b5563' }, // gray-600
|
| 55 |
+
hoverinfo: 'none',
|
| 56 |
+
type: 'scatter'
|
| 57 |
+
};
|
| 58 |
+
|
| 59 |
+
const nodeTrace = {
|
| 60 |
+
x: nodeX,
|
| 61 |
+
y: nodeY,
|
| 62 |
+
mode: 'markers',
|
| 63 |
+
hoverinfo: 'none',
|
| 64 |
+
marker: {
|
| 65 |
+
showscale: false,
|
| 66 |
+
colorscale: 'Viridis',
|
| 67 |
+
color: nodeColor,
|
| 68 |
+
size: 8,
|
| 69 |
+
line: { width: 0 }
|
| 70 |
+
},
|
| 71 |
+
type: 'scatter'
|
| 72 |
+
};
|
| 73 |
+
|
| 74 |
+
const layout = {
|
| 75 |
+
showlegend: false,
|
| 76 |
+
hovermode: false,
|
| 77 |
+
margin: { b: 0, l: 0, r: 0, t: 0 },
|
| 78 |
+
xaxis: {
|
| 79 |
+
showgrid: false,
|
| 80 |
+
zeroline: false,
|
| 81 |
+
showticklabels: false,
|
| 82 |
+
range: [-0.05, 1.05],
|
| 83 |
+
fixedrange: true
|
| 84 |
+
},
|
| 85 |
+
yaxis: {
|
| 86 |
+
showgrid: false,
|
| 87 |
+
zeroline: false,
|
| 88 |
+
showticklabels: false,
|
| 89 |
+
range: [-0.05, 1.05],
|
| 90 |
+
fixedrange: true
|
| 91 |
+
},
|
| 92 |
+
paper_bgcolor: 'rgba(0,0,0,0)',
|
| 93 |
+
plot_bgcolor: 'rgba(0,0,0,0)',
|
| 94 |
+
autosize: true,
|
| 95 |
+
dragmode: false
|
| 96 |
+
};
|
| 97 |
+
|
| 98 |
+
const config = {
|
| 99 |
+
staticPlot: true, // Disables all interactions for a cleaner background look
|
| 100 |
+
displayModeBar: false,
|
| 101 |
+
responsive: true
|
| 102 |
+
};
|
| 103 |
+
|
| 104 |
+
// @ts-ignore - Plotly types can be tricky with ESM imports
|
| 105 |
+
Plotly.newPlot(graphDiv.current, [edgeTrace, nodeTrace], layout, config);
|
| 106 |
+
|
| 107 |
+
// Handle Resize
|
| 108 |
+
const resizeObserver = new ResizeObserver(() => {
|
| 109 |
+
if (graphDiv.current) {
|
| 110 |
+
// @ts-ignore
|
| 111 |
+
Plotly.Plots.resize(graphDiv.current);
|
| 112 |
+
}
|
| 113 |
+
});
|
| 114 |
+
resizeObserver.observe(graphDiv.current);
|
| 115 |
+
|
| 116 |
+
return () => {
|
| 117 |
+
resizeObserver.disconnect();
|
| 118 |
+
// @ts-ignore
|
| 119 |
+
if (graphDiv.current) Plotly.purge(graphDiv.current);
|
| 120 |
+
};
|
| 121 |
+
}, []);
|
| 122 |
+
|
| 123 |
+
return (
|
| 124 |
+
<div ref={graphDiv} className="w-full h-full" />
|
| 125 |
+
);
|
| 126 |
+
};
|
| 127 |
+
|
| 128 |
+
export default RealNetworkGraph;
|
components/SimulationGraph.tsx
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useEffect, useRef, useState } from 'react';
|
| 2 |
+
import Plotly from 'plotly.js';
|
| 3 |
+
import { X, Linkedin, Globe, MapPin, User, Briefcase, Users } from 'lucide-react';
|
| 4 |
+
import Button from './ui/Button';
|
| 5 |
+
|
| 6 |
+
interface SimulationGraphProps {
|
| 7 |
+
isBuilding: boolean;
|
| 8 |
+
societyType: string;
|
| 9 |
+
onStartChat?: () => void;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
const SimulationGraph: React.FC<SimulationGraphProps> = ({ isBuilding, societyType, onStartChat }) => {
|
| 13 |
+
const graphDiv = useRef<HTMLDivElement>(null);
|
| 14 |
+
const [selectedProfile, setSelectedProfile] = useState<{ x: number, y: number, data: any } | null>(null);
|
| 15 |
+
|
| 16 |
+
// Close popup if building starts
|
| 17 |
+
useEffect(() => {
|
| 18 |
+
if (isBuilding) setSelectedProfile(null);
|
| 19 |
+
}, [isBuilding]);
|
| 20 |
+
|
| 21 |
+
useEffect(() => {
|
| 22 |
+
if (!graphDiv.current || isBuilding) return;
|
| 23 |
+
|
| 24 |
+
// --- Dynamic Data Generation based on Society Type ---
|
| 25 |
+
const isTech = societyType.includes('Tech') || societyType.includes('Founders');
|
| 26 |
+
|
| 27 |
+
const N = isTech ? 120 : 80;
|
| 28 |
+
const radius = isTech ? 0.18 : 0.22;
|
| 29 |
+
const nodes = [];
|
| 30 |
+
|
| 31 |
+
// Create nodes
|
| 32 |
+
for (let i = 0; i < N; i++) {
|
| 33 |
+
nodes.push({
|
| 34 |
+
x: Math.random(),
|
| 35 |
+
y: Math.random(),
|
| 36 |
+
connections: 0,
|
| 37 |
+
// Mock data for the popup
|
| 38 |
+
role: isTech ? ['Founder', 'CTO', 'Product Lead', 'VC'][Math.floor(Math.random() * 4)] : ['Journalist', 'Reader', 'Editor', 'Subscriber'][Math.floor(Math.random() * 4)],
|
| 39 |
+
location: ['New York, USA', 'London, UK', 'Berlin, DE', 'Paris, FR'][Math.floor(Math.random() * 4)]
|
| 40 |
+
});
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
// Create edges
|
| 44 |
+
const edgeX: (number | null)[] = [];
|
| 45 |
+
const edgeY: (number | null)[] = [];
|
| 46 |
+
|
| 47 |
+
for (let i = 0; i < N; i++) {
|
| 48 |
+
for (let j = i + 1; j < N; j++) {
|
| 49 |
+
const dx = nodes[i].x - nodes[j].x;
|
| 50 |
+
const dy = nodes[i].y - nodes[j].y;
|
| 51 |
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
| 52 |
+
|
| 53 |
+
if (dist < radius) {
|
| 54 |
+
nodes[i].connections++;
|
| 55 |
+
nodes[j].connections++;
|
| 56 |
+
edgeX.push(nodes[i].x, nodes[j].x, null);
|
| 57 |
+
edgeY.push(nodes[i].y, nodes[j].y, null);
|
| 58 |
+
}
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
const nodeX = nodes.map(n => n.x);
|
| 63 |
+
const nodeY = nodes.map(n => n.y);
|
| 64 |
+
const nodeColor = nodes.map(n => n.connections);
|
| 65 |
+
|
| 66 |
+
// --- Plotly Config ---
|
| 67 |
+
const edgeTrace = {
|
| 68 |
+
x: edgeX,
|
| 69 |
+
y: edgeY,
|
| 70 |
+
mode: 'lines',
|
| 71 |
+
line: { width: 0.5, color: '#4b5563' },
|
| 72 |
+
hoverinfo: 'none',
|
| 73 |
+
type: 'scatter'
|
| 74 |
+
};
|
| 75 |
+
|
| 76 |
+
const nodeTrace = {
|
| 77 |
+
x: nodeX,
|
| 78 |
+
y: nodeY,
|
| 79 |
+
mode: 'markers',
|
| 80 |
+
hoverinfo: 'none',
|
| 81 |
+
marker: {
|
| 82 |
+
showscale: false,
|
| 83 |
+
colorscale: isTech ? 'Electric' : 'Viridis',
|
| 84 |
+
color: nodeColor,
|
| 85 |
+
size: 10,
|
| 86 |
+
line: { width: 0 }
|
| 87 |
+
},
|
| 88 |
+
type: 'scatter'
|
| 89 |
+
};
|
| 90 |
+
|
| 91 |
+
const layout = {
|
| 92 |
+
showlegend: false,
|
| 93 |
+
hovermode: 'closest',
|
| 94 |
+
margin: { b: 0, l: 0, r: 0, t: 0 },
|
| 95 |
+
xaxis: { showgrid: false, zeroline: false, showticklabels: false, range: [-0.05, 1.05], fixedrange: true },
|
| 96 |
+
yaxis: { showgrid: false, zeroline: false, showticklabels: false, range: [-0.05, 1.05], fixedrange: true },
|
| 97 |
+
paper_bgcolor: 'rgba(0,0,0,0)',
|
| 98 |
+
plot_bgcolor: 'rgba(0,0,0,0)',
|
| 99 |
+
autosize: true,
|
| 100 |
+
dragmode: false
|
| 101 |
+
};
|
| 102 |
+
|
| 103 |
+
const config = {
|
| 104 |
+
staticPlot: false,
|
| 105 |
+
displayModeBar: false,
|
| 106 |
+
responsive: true
|
| 107 |
+
};
|
| 108 |
+
|
| 109 |
+
// @ts-ignore
|
| 110 |
+
Plotly.newPlot(graphDiv.current, [edgeTrace, nodeTrace], layout, config).then((gd) => {
|
| 111 |
+
// @ts-ignore
|
| 112 |
+
gd.on('plotly_click', (data) => {
|
| 113 |
+
const point = data.points[0];
|
| 114 |
+
if (point) {
|
| 115 |
+
const nodeIndex = point.pointNumber;
|
| 116 |
+
const nodeData = nodes[nodeIndex];
|
| 117 |
+
|
| 118 |
+
setSelectedProfile({
|
| 119 |
+
x: point.x,
|
| 120 |
+
y: point.y,
|
| 121 |
+
data: nodeData
|
| 122 |
+
});
|
| 123 |
+
}
|
| 124 |
+
});
|
| 125 |
+
});
|
| 126 |
+
|
| 127 |
+
}, [isBuilding, societyType]);
|
| 128 |
+
|
| 129 |
+
return (
|
| 130 |
+
<div className="relative w-full h-full bg-black">
|
| 131 |
+
{/* Loading Overlay */}
|
| 132 |
+
{isBuilding && (
|
| 133 |
+
<div className="absolute inset-0 z-50 flex flex-col items-center justify-center bg-black/80 backdrop-blur-sm transition-opacity duration-300">
|
| 134 |
+
<div className="w-16 h-16 border-4 border-teal-900 border-t-teal-500 rounded-full animate-spin mb-4"></div>
|
| 135 |
+
<p className="text-teal-400 font-mono animate-pulse">Constructing Focus Group Graph...</p>
|
| 136 |
+
</div>
|
| 137 |
+
)}
|
| 138 |
+
|
| 139 |
+
{/* The Graph */}
|
| 140 |
+
<div ref={graphDiv} className="w-full h-full" />
|
| 141 |
+
|
| 142 |
+
{/* Profile Popup */}
|
| 143 |
+
{selectedProfile && !isBuilding && (
|
| 144 |
+
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-80 bg-gray-900/90 backdrop-blur-md border border-gray-700 rounded-2xl shadow-2xl overflow-hidden z-40 animate-in fade-in zoom-in-95 duration-200">
|
| 145 |
+
{/* Header */}
|
| 146 |
+
<div className="p-4 border-b border-gray-800 flex justify-between items-start">
|
| 147 |
+
<div className="flex items-center gap-3">
|
| 148 |
+
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-teal-400 to-blue-600 flex items-center justify-center text-white font-bold text-lg">
|
| 149 |
+
{selectedProfile.data.role[0]}
|
| 150 |
+
</div>
|
| 151 |
+
<div>
|
| 152 |
+
<h3 className="text-white font-semibold text-sm">{selectedProfile.data.role}</h3>
|
| 153 |
+
<p className="text-gray-400 text-xs">Head of Product at BrightCore</p>
|
| 154 |
+
</div>
|
| 155 |
+
</div>
|
| 156 |
+
<button
|
| 157 |
+
onClick={() => setSelectedProfile(null)}
|
| 158 |
+
className="text-gray-500 hover:text-white"
|
| 159 |
+
>
|
| 160 |
+
<X size={16} />
|
| 161 |
+
</button>
|
| 162 |
+
</div>
|
| 163 |
+
|
| 164 |
+
{/* Body */}
|
| 165 |
+
<div className="p-4 space-y-4">
|
| 166 |
+
<div className="flex items-center gap-2 text-xs text-gray-400">
|
| 167 |
+
<span>Built from</span>
|
| 168 |
+
<Linkedin size={14} className="text-[#0077b5]" />
|
| 169 |
+
<Globe size={14} className="text-gray-300" />
|
| 170 |
+
</div>
|
| 171 |
+
|
| 172 |
+
<div className="flex flex-wrap gap-2">
|
| 173 |
+
<div className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg bg-gray-800 border border-gray-700 text-xs text-gray-300">
|
| 174 |
+
<MapPin size={12} /> {selectedProfile.data.location}
|
| 175 |
+
</div>
|
| 176 |
+
<div className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg bg-gray-800 border border-gray-700 text-xs text-gray-300">
|
| 177 |
+
<User size={12} /> Millennial
|
| 178 |
+
</div>
|
| 179 |
+
<div className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg bg-gray-800 border border-gray-700 text-xs text-gray-300">
|
| 180 |
+
<Briefcase size={12} /> Mid Level
|
| 181 |
+
</div>
|
| 182 |
+
<div className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg bg-gray-800 border border-gray-700 text-xs text-gray-300">
|
| 183 |
+
<Users size={12} /> Creative & Design
|
| 184 |
+
</div>
|
| 185 |
+
</div>
|
| 186 |
+
</div>
|
| 187 |
+
|
| 188 |
+
{/* Footer */}
|
| 189 |
+
<div className="p-4 pt-0">
|
| 190 |
+
<Button
|
| 191 |
+
className="w-full text-sm py-2"
|
| 192 |
+
onClick={onStartChat}
|
| 193 |
+
>
|
| 194 |
+
Start Conversation
|
| 195 |
+
</Button>
|
| 196 |
+
</div>
|
| 197 |
+
</div>
|
| 198 |
+
)}
|
| 199 |
+
</div>
|
| 200 |
+
);
|
| 201 |
+
};
|
| 202 |
+
|
| 203 |
+
export default SimulationGraph;
|
components/SimulationPage.tsx
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState } from 'react';
|
| 2 |
+
import { ChevronDown, Plus, Info, MessageSquare, BookOpen, LogOut, PanelLeftClose, MessageCircle, AlertTriangle } from 'lucide-react';
|
| 3 |
+
import SimulationGraph from './SimulationGraph';
|
| 4 |
+
|
| 5 |
+
interface SimulationPageProps {
|
| 6 |
+
onBack: () => void;
|
| 7 |
+
onOpenConversation: () => void;
|
| 8 |
+
onOpenChat: () => void;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
// Define the data structure for filters
|
| 12 |
+
const VIEW_FILTERS: Record<string, Array<{ label: string; color: string }>> = {
|
| 13 |
+
'Country': [
|
| 14 |
+
{ label: "United States", color: "bg-blue-600" },
|
| 15 |
+
{ label: "United Kingdom", color: "bg-purple-600" },
|
| 16 |
+
{ label: "Netherlands", color: "bg-teal-600" },
|
| 17 |
+
{ label: "France", color: "bg-orange-600" },
|
| 18 |
+
{ label: "India", color: "bg-pink-600" }
|
| 19 |
+
],
|
| 20 |
+
'Job Title': [
|
| 21 |
+
{ label: "Founder", color: "bg-indigo-500" },
|
| 22 |
+
{ label: "Product Manager", color: "bg-emerald-500" },
|
| 23 |
+
{ label: "Engineer", color: "bg-rose-500" },
|
| 24 |
+
{ label: "Investor", color: "bg-amber-500" },
|
| 25 |
+
{ label: "Designer", color: "bg-fuchsia-500" }
|
| 26 |
+
],
|
| 27 |
+
'Sentiment': [
|
| 28 |
+
{ label: "Positive", color: "bg-green-500" },
|
| 29 |
+
{ label: "Neutral", color: "bg-gray-500" },
|
| 30 |
+
{ label: "Negative", color: "bg-red-500" },
|
| 31 |
+
{ label: "Mixed", color: "bg-yellow-500" }
|
| 32 |
+
],
|
| 33 |
+
'Activity Level': [
|
| 34 |
+
{ label: "Power User", color: "bg-red-600" },
|
| 35 |
+
{ label: "Daily Active", color: "bg-orange-500" },
|
| 36 |
+
{ label: "Weekly Active", color: "bg-blue-500" },
|
| 37 |
+
{ label: "Lurker", color: "bg-slate-600" }
|
| 38 |
+
]
|
| 39 |
+
};
|
| 40 |
+
|
| 41 |
+
const SimulationPage: React.FC<SimulationPageProps> = ({ onBack, onOpenConversation, onOpenChat }) => {
|
| 42 |
+
const [society, setSociety] = useState('User Group 1');
|
| 43 |
+
const [viewMode, setViewMode] = useState('Job Title');
|
| 44 |
+
const [isBuilding, setIsBuilding] = useState(false);
|
| 45 |
+
const [isRightPanelOpen, setIsRightPanelOpen] = useState(true);
|
| 46 |
+
|
| 47 |
+
// Function to simulate rebuilding the graph when settings change
|
| 48 |
+
const handleSettingChange = (setter: (val: string) => void, value: string) => {
|
| 49 |
+
if (value === society || (value === viewMode && setter === setViewMode)) return; // No change
|
| 50 |
+
|
| 51 |
+
setter(value);
|
| 52 |
+
setIsBuilding(true);
|
| 53 |
+
// Simulate network delay
|
| 54 |
+
setTimeout(() => {
|
| 55 |
+
setIsBuilding(false);
|
| 56 |
+
}, 1500);
|
| 57 |
+
};
|
| 58 |
+
|
| 59 |
+
const currentFilters = VIEW_FILTERS[viewMode] || VIEW_FILTERS['Country'];
|
| 60 |
+
|
| 61 |
+
return (
|
| 62 |
+
<div className="flex h-screen w-screen overflow-hidden bg-black text-white font-sans">
|
| 63 |
+
{/* Sidebar */}
|
| 64 |
+
<aside className="w-[300px] flex-shrink-0 border-r border-gray-800 flex flex-col bg-[#0a0a0a] z-20">
|
| 65 |
+
{/* Header */}
|
| 66 |
+
<div className="p-4 h-16 border-b border-gray-800 flex items-center justify-between">
|
| 67 |
+
<div className="flex items-center gap-2 cursor-pointer" onClick={onBack}>
|
| 68 |
+
<div className="w-6 h-6 flex items-center justify-center font-bold text-white">Λ</div>
|
| 69 |
+
<span className="font-semibold tracking-tight">SyncUsers</span>
|
| 70 |
+
</div>
|
| 71 |
+
<button className="text-gray-500 hover:text-white"><PanelLeftClose size={18}/></button>
|
| 72 |
+
</div>
|
| 73 |
+
|
| 74 |
+
{/* Scrollable Content */}
|
| 75 |
+
<div className="flex-1 overflow-y-auto p-4 space-y-6">
|
| 76 |
+
|
| 77 |
+
{/* Current Focus Group Control */}
|
| 78 |
+
<div className="space-y-2">
|
| 79 |
+
<label className="text-xs text-gray-500 font-medium uppercase tracking-wider">Current Focus Group</label>
|
| 80 |
+
<div className="relative group">
|
| 81 |
+
<select
|
| 82 |
+
value={society}
|
| 83 |
+
onChange={(e) => handleSettingChange(setSociety, e.target.value)}
|
| 84 |
+
className="w-full appearance-none bg-[#111] border border-gray-700 text-white rounded-lg px-3 py-2.5 text-sm focus:outline-none focus:border-teal-500 cursor-pointer"
|
| 85 |
+
>
|
| 86 |
+
<option>User Group 1</option>
|
| 87 |
+
</select>
|
| 88 |
+
<ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 pointer-events-none w-4 h-4" />
|
| 89 |
+
</div>
|
| 90 |
+
</div>
|
| 91 |
+
|
| 92 |
+
{/* Current View Control */}
|
| 93 |
+
<div className="space-y-2">
|
| 94 |
+
<label className="text-xs text-gray-500 font-medium uppercase tracking-wider">Current View</label>
|
| 95 |
+
<div className="relative">
|
| 96 |
+
<select
|
| 97 |
+
value={viewMode}
|
| 98 |
+
onChange={(e) => handleSettingChange(setViewMode, e.target.value)}
|
| 99 |
+
className="w-full appearance-none bg-[#111] border border-gray-700 text-white rounded-lg px-3 py-2.5 text-sm focus:outline-none focus:border-teal-500 cursor-pointer"
|
| 100 |
+
>
|
| 101 |
+
<option>Country</option>
|
| 102 |
+
<option>Job Title</option>
|
| 103 |
+
<option>Sentiment</option>
|
| 104 |
+
<option>Activity Level</option>
|
| 105 |
+
</select>
|
| 106 |
+
<ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 pointer-events-none w-4 h-4" />
|
| 107 |
+
</div>
|
| 108 |
+
</div>
|
| 109 |
+
|
| 110 |
+
<div className="h-px bg-gray-800 my-4" />
|
| 111 |
+
|
| 112 |
+
{/* Actions */}
|
| 113 |
+
<button
|
| 114 |
+
onClick={onOpenConversation}
|
| 115 |
+
className="w-full flex items-center justify-between text-left text-sm text-gray-300 hover:text-white group py-2"
|
| 116 |
+
>
|
| 117 |
+
<span>Create a new test</span>
|
| 118 |
+
<Plus size={18} className="text-gray-500 group-hover:text-white" />
|
| 119 |
+
</button>
|
| 120 |
+
|
| 121 |
+
{/* Global Chat Button (Sidebar) */}
|
| 122 |
+
<button
|
| 123 |
+
onClick={onOpenChat}
|
| 124 |
+
className="w-full flex items-center gap-3 px-4 py-3 bg-gray-800 hover:bg-gray-700 text-white rounded-lg transition-colors border border-gray-700"
|
| 125 |
+
>
|
| 126 |
+
<MessageCircle size={18} />
|
| 127 |
+
<span className="font-medium text-sm">Open Global Chat</span>
|
| 128 |
+
</button>
|
| 129 |
+
|
| 130 |
+
{/* Setup Warning */}
|
| 131 |
+
<div className="bg-amber-900/30 border border-amber-700/50 rounded-xl p-4 mt-4">
|
| 132 |
+
<div className="flex items-center gap-2 text-amber-200 font-bold text-xs mb-1">
|
| 133 |
+
<AlertTriangle size={14}/>
|
| 134 |
+
<span>Action Required</span>
|
| 135 |
+
</div>
|
| 136 |
+
<p className="text-amber-200/70 text-[10px] leading-relaxed">
|
| 137 |
+
Assemble a new group and create a new test before using the chat features.
|
| 138 |
+
</p>
|
| 139 |
+
</div>
|
| 140 |
+
|
| 141 |
+
{/* History List */}
|
| 142 |
+
<div className="space-y-1 pt-4">
|
| 143 |
+
<label className="text-xs text-gray-500 font-medium uppercase tracking-wider mb-2 block">Recent Tests</label>
|
| 144 |
+
<div className="text-sm text-gray-400 py-2 px-2 hover:bg-gray-800/50 rounded cursor-pointer truncate">
|
| 145 |
+
Duality in portraits and sha...
|
| 146 |
+
</div>
|
| 147 |
+
<div className="text-sm text-gray-400 py-2 px-2 hover:bg-gray-800/50 rounded cursor-pointer truncate">
|
| 148 |
+
Volcanoes: Threat or Misun...
|
| 149 |
+
</div>
|
| 150 |
+
<div className="text-sm text-gray-400 py-2 px-2 hover:bg-gray-800/50 rounded cursor-pointer truncate">
|
| 151 |
+
Sustainable Fashion 2024
|
| 152 |
+
</div>
|
| 153 |
+
</div>
|
| 154 |
+
</div>
|
| 155 |
+
|
| 156 |
+
{/* Footer */}
|
| 157 |
+
<div className="border-t border-gray-800 p-4 space-y-1 bg-[#0a0a0a]">
|
| 158 |
+
<div className="flex justify-between items-center py-2 text-sm text-gray-400 border-b border-gray-800 mb-2 pb-4">
|
| 159 |
+
<span>Credits: 0</span>
|
| 160 |
+
<Info size={14} className="cursor-help" />
|
| 161 |
+
</div>
|
| 162 |
+
|
| 163 |
+
<MenuItem icon={<Plus size={16}/>} label="Start Free Trial" highlight />
|
| 164 |
+
<MenuItem icon={<MessageSquare size={16}/>} label="Leave Feedback" />
|
| 165 |
+
<MenuItem icon={<BookOpen size={16}/>} label="Product Guide" />
|
| 166 |
+
<MenuItem icon={<LogOut size={16}/>} label="Log Out" />
|
| 167 |
+
|
| 168 |
+
<div className="pt-4 text-[10px] text-gray-600">Version 2.1</div>
|
| 169 |
+
</div>
|
| 170 |
+
</aside>
|
| 171 |
+
|
| 172 |
+
{/* Main Content Area */}
|
| 173 |
+
<main className="flex-1 flex flex-col relative bg-black overflow-hidden">
|
| 174 |
+
{/* Top Navigation Overlay */}
|
| 175 |
+
<div className="absolute top-6 left-6 right-6 z-10 flex justify-center pointer-events-none">
|
| 176 |
+
{/* Legend / Filter Chips */}
|
| 177 |
+
<div className="flex flex-wrap justify-center gap-2 pointer-events-auto">
|
| 178 |
+
{currentFilters.map((filter, idx) => (
|
| 179 |
+
<FilterChip key={idx} color={filter.color} label={filter.label} />
|
| 180 |
+
))}
|
| 181 |
+
</div>
|
| 182 |
+
</div>
|
| 183 |
+
|
| 184 |
+
{/* Graph Container */}
|
| 185 |
+
<div className="flex-1 w-full h-full">
|
| 186 |
+
<SimulationGraph isBuilding={isBuilding} societyType={society} onStartChat={onOpenChat} />
|
| 187 |
+
</div>
|
| 188 |
+
|
| 189 |
+
{/* Floating Chat Button (Bottom) */}
|
| 190 |
+
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 z-30">
|
| 191 |
+
<button
|
| 192 |
+
onClick={onOpenChat}
|
| 193 |
+
className="flex items-center gap-2 bg-black/80 backdrop-blur-md border border-gray-700 text-white px-6 py-3 rounded-full shadow-2xl hover:bg-gray-900 transition-all hover:scale-105"
|
| 194 |
+
>
|
| 195 |
+
<MessageCircle size={20} />
|
| 196 |
+
<span className="font-medium">Open Simulation Chat</span>
|
| 197 |
+
</button>
|
| 198 |
+
</div>
|
| 199 |
+
</main>
|
| 200 |
+
|
| 201 |
+
{/* Right Sidebar (Output) */}
|
| 202 |
+
<aside className={`w-[300px] flex-shrink-0 border-l border-gray-800 flex flex-col bg-[#0a0a0a] z-20 transition-all duration-300 ${isRightPanelOpen ? 'mr-0' : '-mr-[300px]'}`}>
|
| 203 |
+
<div className="p-4 h-16 border-b border-gray-800 flex items-center justify-between">
|
| 204 |
+
<span className="font-semibold tracking-tight uppercase text-xs text-gray-500">Output</span>
|
| 205 |
+
<button onClick={() => setIsRightPanelOpen(false)} className="text-gray-500 hover:text-white"><PanelLeftClose size={18} className="rotate-180"/></button>
|
| 206 |
+
</div>
|
| 207 |
+
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
| 208 |
+
<div className="bg-gray-900/50 border border-gray-800 rounded-xl p-4">
|
| 209 |
+
<p className="text-xs text-gray-500 mb-2">Simulation Results</p>
|
| 210 |
+
<div className="text-sm text-gray-400 italic text-center py-8">
|
| 211 |
+
Results will appear here after running a simulation.
|
| 212 |
+
</div>
|
| 213 |
+
</div>
|
| 214 |
+
</div>
|
| 215 |
+
</aside>
|
| 216 |
+
</div>
|
| 217 |
+
);
|
| 218 |
+
};
|
| 219 |
+
|
| 220 |
+
// Helper Components
|
| 221 |
+
interface MenuItemProps {
|
| 222 |
+
icon: React.ReactNode;
|
| 223 |
+
label: string;
|
| 224 |
+
highlight?: boolean;
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
const MenuItem: React.FC<MenuItemProps> = ({ icon, label, highlight = false }) => (
|
| 228 |
+
<button className={`w-full flex items-center gap-3 px-2 py-2.5 rounded-md text-sm transition-colors ${highlight ? 'text-teal-400 hover:bg-teal-950/30' : 'text-gray-400 hover:bg-gray-800 hover:text-white'}`}>
|
| 229 |
+
{icon}
|
| 230 |
+
<span>{label}</span>
|
| 231 |
+
</button>
|
| 232 |
+
);
|
| 233 |
+
|
| 234 |
+
interface FilterChipProps {
|
| 235 |
+
color: string;
|
| 236 |
+
label: string;
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
const FilterChip: React.FC<FilterChipProps> = ({ color, label }) => (
|
| 240 |
+
<button className="flex items-center gap-2 bg-gray-900/80 backdrop-blur border border-gray-700 rounded-full pl-2 pr-4 py-1.5 hover:border-gray-500 transition-colors">
|
| 241 |
+
<span className={`w-2.5 h-2.5 rounded-full ${color}`}></span>
|
| 242 |
+
<span className="text-xs font-medium text-gray-300">{label}</span>
|
| 243 |
+
</button>
|
| 244 |
+
);
|
| 245 |
+
|
| 246 |
+
export default SimulationPage;
|
components/Testimonials.tsx
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import React from 'react';
|
| 3 |
+
import { TESTIMONIALS } from '../constants';
|
| 4 |
+
import { ArrowLeft, ArrowRight } from 'lucide-react';
|
| 5 |
+
|
| 6 |
+
const Testimonials: React.FC = () => {
|
| 7 |
+
return (
|
| 8 |
+
<section className="py-24 bg-black border-y border-gray-900">
|
| 9 |
+
<div className="max-w-7xl mx-auto px-6">
|
| 10 |
+
<div className="flex justify-center mb-8">
|
| 11 |
+
<span className="px-4 py-1.5 rounded-full border border-gray-700 text-sm text-gray-300">Testimonials</span>
|
| 12 |
+
</div>
|
| 13 |
+
<h2 className="text-3xl md:text-5xl font-semibold text-center mb-16 max-w-3xl mx-auto">
|
| 14 |
+
Join thousands using Agentic User Experience (AUX) to run simulations and test ideas
|
| 15 |
+
</h2>
|
| 16 |
+
|
| 17 |
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
| 18 |
+
{TESTIMONIALS.map((t, idx) => (
|
| 19 |
+
<div key={idx} className="bg-gray-900/30 border border-gray-800 p-8 rounded-2xl flex flex-col justify-between h-full">
|
| 20 |
+
<p className="text-xl leading-relaxed text-gray-200 mb-8 font-light">"{t.quote}"</p>
|
| 21 |
+
<div className="flex items-center gap-4">
|
| 22 |
+
<div className="w-12 h-12 rounded-full bg-white text-black flex items-center justify-center font-bold text-lg">
|
| 23 |
+
{t.company[0]}
|
| 24 |
+
</div>
|
| 25 |
+
<div>
|
| 26 |
+
<div className="font-semibold">{t.company}</div>
|
| 27 |
+
<div className="text-sm text-gray-400">{t.author} • {t.role}</div>
|
| 28 |
+
</div>
|
| 29 |
+
</div>
|
| 30 |
+
</div>
|
| 31 |
+
))}
|
| 32 |
+
</div>
|
| 33 |
+
|
| 34 |
+
<div className="flex justify-center gap-4 mt-12">
|
| 35 |
+
<button className="w-12 h-12 rounded-full border border-gray-700 flex items-center justify-center hover:bg-white hover:text-black transition-colors">
|
| 36 |
+
<ArrowLeft size={20} />
|
| 37 |
+
</button>
|
| 38 |
+
<button className="w-12 h-12 rounded-full border border-gray-700 flex items-center justify-center hover:bg-white hover:text-black transition-colors">
|
| 39 |
+
<ArrowRight size={20} />
|
| 40 |
+
</button>
|
| 41 |
+
</div>
|
| 42 |
+
</div>
|
| 43 |
+
</section>
|
| 44 |
+
);
|
| 45 |
+
};
|
| 46 |
+
|
| 47 |
+
export default Testimonials;
|
components/TrustedBy.tsx
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { CheckCircle, Timer, Clock, ClipboardCheck, Smile, Gauge } from 'lucide-react';
|
| 3 |
+
|
| 4 |
+
const TrustedBy: React.FC = () => {
|
| 5 |
+
const metrics = [
|
| 6 |
+
{
|
| 7 |
+
icon: <CheckCircle size={20} />,
|
| 8 |
+
category: "Task Success Rate",
|
| 9 |
+
question: "Do users complete their goals?",
|
| 10 |
+
outcome: "Identify drop-off points in critical flows."
|
| 11 |
+
},
|
| 12 |
+
{
|
| 13 |
+
icon: <Timer size={20} />,
|
| 14 |
+
category: "Time-on-Task",
|
| 15 |
+
question: "How quickly do they finish?",
|
| 16 |
+
outcome: "Benchmark efficiency against previous versions."
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
icon: <Clock size={20} />,
|
| 20 |
+
category: "Session Duration",
|
| 21 |
+
question: "How long do they stay engaged?",
|
| 22 |
+
outcome: "Balance retention with efficient task completion."
|
| 23 |
+
},
|
| 24 |
+
{
|
| 25 |
+
icon: <ClipboardCheck size={20} />,
|
| 26 |
+
category: "System Usability Scale",
|
| 27 |
+
question: "Is the product usable?",
|
| 28 |
+
outcome: "Standardized scoring for overall usability."
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
icon: <Smile size={20} />,
|
| 32 |
+
category: "Customer Satisfaction",
|
| 33 |
+
question: "Are users happy with the experience?",
|
| 34 |
+
outcome: "Predict long-term retention and loyalty."
|
| 35 |
+
},
|
| 36 |
+
{
|
| 37 |
+
icon: <Gauge size={20} />,
|
| 38 |
+
category: "Customer Effort Score",
|
| 39 |
+
question: "Is it easy to get things done?",
|
| 40 |
+
outcome: "Reduce friction to increase conversion."
|
| 41 |
+
}
|
| 42 |
+
];
|
| 43 |
+
|
| 44 |
+
return (
|
| 45 |
+
<section className="py-24 border-t border-gray-800 bg-black">
|
| 46 |
+
<div className="max-w-7xl mx-auto px-6">
|
| 47 |
+
<h3 className="text-center text-3xl md:text-4xl font-semibold text-white mb-6">
|
| 48 |
+
Quantify your UX quality programmatically.
|
| 49 |
+
</h3>
|
| 50 |
+
<p className="text-center text-gray-400 mb-16 max-w-2xl mx-auto text-lg">
|
| 51 |
+
Developers use SyncUsers to predict performance on core metrics without waiting for live traffic data.
|
| 52 |
+
</p>
|
| 53 |
+
|
| 54 |
+
{/* UX Metrics Grid */}
|
| 55 |
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-24">
|
| 56 |
+
{metrics.map((item, idx) => (
|
| 57 |
+
<div key={idx} className="group p-8 rounded-2xl border border-gray-800 bg-gray-900/20 hover:border-teal-900/50 hover:bg-gray-900/40 transition-all duration-300">
|
| 58 |
+
<div className="flex items-center gap-3 mb-5 text-gray-500 group-hover:text-teal-400 transition-colors">
|
| 59 |
+
{item.icon}
|
| 60 |
+
<span className="text-xs font-mono uppercase tracking-widest">{item.category}</span>
|
| 61 |
+
</div>
|
| 62 |
+
<div className="text-xl font-medium text-white mb-3 group-hover:translate-x-1 transition-transform duration-300">
|
| 63 |
+
{item.question}
|
| 64 |
+
</div>
|
| 65 |
+
<p className="text-sm text-gray-500 leading-relaxed group-hover:text-gray-400 transition-colors">
|
| 66 |
+
{item.outcome}
|
| 67 |
+
</p>
|
| 68 |
+
</div>
|
| 69 |
+
))}
|
| 70 |
+
</div>
|
| 71 |
+
|
| 72 |
+
{/* Stats Grid */}
|
| 73 |
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-4xl mx-auto">
|
| 74 |
+
{[
|
| 75 |
+
{ value: "2,000+", label: "Persona Database ready", color: "border-blue-900" },
|
| 76 |
+
{ value: "REST & GraphQL", label: "API Available", color: "border-teal-900" },
|
| 77 |
+
].map((stat, idx) => (
|
| 78 |
+
<div key={idx} className={`relative flex flex-col items-center justify-center py-16 rounded-full border border-t-2 ${stat.color} bg-gradient-to-b from-gray-900 to-black`}>
|
| 79 |
+
<h4 className="text-4xl md:text-5xl font-bold text-white mb-2">{stat.value}</h4>
|
| 80 |
+
<p className="text-gray-400">{stat.label}</p>
|
| 81 |
+
</div>
|
| 82 |
+
))}
|
| 83 |
+
</div>
|
| 84 |
+
</div>
|
| 85 |
+
</section>
|
| 86 |
+
);
|
| 87 |
+
};
|
| 88 |
+
|
| 89 |
+
export default TrustedBy;
|
components/UseCases.tsx
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { USE_CASES } from '../constants';
|
| 3 |
+
|
| 4 |
+
const UseCases: React.FC = () => {
|
| 5 |
+
return (
|
| 6 |
+
<section id="use-cases" className="py-24 bg-black">
|
| 7 |
+
<div className="max-w-7xl mx-auto px-6">
|
| 8 |
+
<div className="flex justify-center mb-8">
|
| 9 |
+
<span className="px-4 py-1.5 rounded-full border border-gray-700 text-sm text-gray-300">Use Cases</span>
|
| 10 |
+
</div>
|
| 11 |
+
<h2 className="text-3xl md:text-5xl font-semibold text-center mb-20">Optimize any kind of message</h2>
|
| 12 |
+
|
| 13 |
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
| 14 |
+
{USE_CASES.map((useCase, idx) => (
|
| 15 |
+
<div key={idx} className="group p-8 border border-gray-800 rounded-2xl bg-gradient-to-br from-gray-900/50 to-black hover:border-gray-600 transition-all duration-300 hover:shadow-lg hover:shadow-gray-900/20">
|
| 16 |
+
<div className="mb-6">
|
| 17 |
+
<span className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-gray-800 text-xs font-medium text-gray-300 border border-gray-700">
|
| 18 |
+
<span className={`w-2 h-2 rounded-full ${useCase.color}`}></span>
|
| 19 |
+
{useCase.category}
|
| 20 |
+
</span>
|
| 21 |
+
</div>
|
| 22 |
+
<h3 className="text-2xl font-semibold mb-3 group-hover:text-white transition-colors">{useCase.title}</h3>
|
| 23 |
+
<p className="text-gray-400 leading-relaxed text-sm">{useCase.description}</p>
|
| 24 |
+
</div>
|
| 25 |
+
))}
|
| 26 |
+
</div>
|
| 27 |
+
</div>
|
| 28 |
+
</section>
|
| 29 |
+
);
|
| 30 |
+
};
|
| 31 |
+
|
| 32 |
+
export default UseCases;
|
components/ui/Button.tsx
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
|
| 3 |
+
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
| 4 |
+
variant?: 'primary' | 'secondary' | 'outline' | 'ghost';
|
| 5 |
+
size?: 'sm' | 'md' | 'lg';
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
const Button: React.FC<ButtonProps> = ({
|
| 9 |
+
children,
|
| 10 |
+
variant = 'primary',
|
| 11 |
+
size = 'md',
|
| 12 |
+
className = '',
|
| 13 |
+
...props
|
| 14 |
+
}) => {
|
| 15 |
+
const baseStyles = "inline-flex items-center justify-center rounded-full font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-black disabled:opacity-50 disabled:pointer-events-none";
|
| 16 |
+
|
| 17 |
+
const variants = {
|
| 18 |
+
primary: "bg-white text-black hover:bg-gray-200 border border-transparent",
|
| 19 |
+
secondary: "bg-gray-800 text-white hover:bg-gray-700 border border-transparent",
|
| 20 |
+
outline: "bg-transparent text-white border border-gray-600 hover:border-gray-400 hover:text-gray-200",
|
| 21 |
+
ghost: "bg-transparent text-white hover:bg-white/10"
|
| 22 |
+
};
|
| 23 |
+
|
| 24 |
+
const sizes = {
|
| 25 |
+
sm: "px-4 py-1.5 text-sm",
|
| 26 |
+
md: "px-6 py-2.5 text-base",
|
| 27 |
+
lg: "px-8 py-3.5 text-lg"
|
| 28 |
+
};
|
| 29 |
+
|
| 30 |
+
return (
|
| 31 |
+
<button
|
| 32 |
+
className={`${baseStyles} ${variants[variant]} ${sizes[size]} ${className}`}
|
| 33 |
+
{...props}
|
| 34 |
+
>
|
| 35 |
+
{children}
|
| 36 |
+
</button>
|
| 37 |
+
);
|
| 38 |
+
};
|
| 39 |
+
|
| 40 |
+
export default Button;
|
constants.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NavLink, UseCase, UseCaseCategory, Testimonial, FaqItem } from './types';
|
| 2 |
+
|
| 3 |
+
export const NAV_LINKS: NavLink[] = [
|
| 4 |
+
{ label: 'Features', href: '#features' },
|
| 5 |
+
{ label: 'Use Cases', href: '#use-cases' },
|
| 6 |
+
{ label: 'How it Works', href: '#how-it-works' },
|
| 7 |
+
{ label: 'Accuracy', href: '#accuracy' },
|
| 8 |
+
{ label: 'Docs', href: '#docs' },
|
| 9 |
+
];
|
| 10 |
+
|
| 11 |
+
export const USE_CASES: UseCase[] = [
|
| 12 |
+
{
|
| 13 |
+
category: UseCaseCategory.PR_COMMS,
|
| 14 |
+
title: "Craft Narratives",
|
| 15 |
+
description: "Test different communication strategies via API to deliver the right reaction",
|
| 16 |
+
color: "bg-purple-500"
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
category: UseCaseCategory.PRODUCT,
|
| 20 |
+
title: "Decide Features",
|
| 21 |
+
description: "Test how your target customers react to product ideas and new features",
|
| 22 |
+
color: "bg-teal-500"
|
| 23 |
+
},
|
| 24 |
+
{
|
| 25 |
+
category: UseCaseCategory.BRANDING,
|
| 26 |
+
title: "Stand Out",
|
| 27 |
+
description: "Test how different brand and voice ideas resonate with your ideal buyer.",
|
| 28 |
+
color: "bg-pink-500"
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
category: UseCaseCategory.MARKETING,
|
| 32 |
+
title: "Generate Leads",
|
| 33 |
+
description: "Test marketing content in a simulation of your target customers",
|
| 34 |
+
color: "bg-orange-500"
|
| 35 |
+
},
|
| 36 |
+
{
|
| 37 |
+
category: UseCaseCategory.SOCIAL_MEDIA,
|
| 38 |
+
title: "Make Content",
|
| 39 |
+
description: "Test social content in simulations of your network and audience",
|
| 40 |
+
color: "bg-blue-500"
|
| 41 |
+
},
|
| 42 |
+
{
|
| 43 |
+
category: UseCaseCategory.JOURNALISM,
|
| 44 |
+
title: "Capture Attention",
|
| 45 |
+
description: "Test headlines, thumbnails, and article content to maximise reader attention",
|
| 46 |
+
color: "bg-yellow-500"
|
| 47 |
+
}
|
| 48 |
+
];
|
| 49 |
+
|
| 50 |
+
export const TESTIMONIALS: Testimonial[] = [];
|
| 51 |
+
|
| 52 |
+
export const FAQS: FaqItem[] = [
|
| 53 |
+
{
|
| 54 |
+
question: "Is the API free for developers?",
|
| 55 |
+
answer: "Yes. We offer a generous free tier specifically designed for developers and hobbyists to build, test, and integrate user simulations without upfront costs."
|
| 56 |
+
},
|
| 57 |
+
{
|
| 58 |
+
question: "What is a Focus Group?",
|
| 59 |
+
answer: "A Focus Group is a simulated collective of AI personas that mirror the behaviors, preferences, and interactions of a specific real-world audience."
|
| 60 |
+
},
|
| 61 |
+
{
|
| 62 |
+
question: "How do credits work in the free tier?",
|
| 63 |
+
answer: "Credits are used to run simulations. One credit equals one simulated interaction. The developer plan includes 1,000 free credits per month, refreshing automatically."
|
| 64 |
+
},
|
| 65 |
+
{
|
| 66 |
+
question: "Can I integrate this into my CI/CD?",
|
| 67 |
+
answer: "Absolutely. Our CLI tool and API are designed to run as part of your testing pipeline, allowing you to validate UX decisions before merging code."
|
| 68 |
+
},
|
| 69 |
+
{
|
| 70 |
+
question: "How do I get an API key?",
|
| 71 |
+
answer: "You need a Huggingface token with a valid account."
|
| 72 |
+
}
|
| 73 |
+
];
|
dist/index.html
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
<!DOCTYPE html>
|
| 3 |
+
<html lang="en">
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8" />
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
+
<title>SyncUsers</title>
|
| 8 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 9 |
+
<style>
|
| 10 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
| 11 |
+
|
| 12 |
+
body {
|
| 13 |
+
font-family: 'Inter', sans-serif;
|
| 14 |
+
background-color: #050505;
|
| 15 |
+
color: #ffffff;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
/* Custom Scrollbar */
|
| 19 |
+
|
| 20 |
+
::-webkit-scrollbar {
|
| 21 |
+
width: 8px;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
::-webkit-scrollbar-track {
|
| 25 |
+
background: #050505;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
::-webkit-scrollbar-thumb {
|
| 29 |
+
background: #333;
|
| 30 |
+
border-radius: 4px;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
::-webkit-scrollbar-thumb:hover {
|
| 34 |
+
background: #555;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
/* Utilities for gradients */
|
| 38 |
+
|
| 39 |
+
.bg-glass {
|
| 40 |
+
background: rgba(255, 255, 255, 0.05);
|
| 41 |
+
backdrop-filter: blur(10px);
|
| 42 |
+
-webkit-backdrop-filter: blur(10px);
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
.gradient-text {
|
| 46 |
+
background: linear-gradient(to right, #fff, #aaa);
|
| 47 |
+
-webkit-background-clip: text;
|
| 48 |
+
-webkit-text-fill-color: transparent;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
/* Blink animation for cursor */
|
| 52 |
+
|
| 53 |
+
@keyframes blink {
|
| 54 |
+
0%, 100% { opacity: 1; }
|
| 55 |
+
50% { opacity: 0; }
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
.animate-blink {
|
| 59 |
+
animation: blink 1s step-end infinite;
|
| 60 |
+
}
|
| 61 |
+
</style>
|
| 62 |
+
<script type="importmap">
|
| 63 |
+
{
|
| 64 |
+
"imports": {
|
| 65 |
+
"react-dom/": "https://aistudiocdn.com/react-dom@^19.2.0/",
|
| 66 |
+
"react/": "https://aistudiocdn.com/react@^19.2.0/",
|
| 67 |
+
"react": "https://aistudiocdn.com/react@^19.2.0",
|
| 68 |
+
"lucide-react": "https://aistudiocdn.com/lucide-react@^0.555.0",
|
| 69 |
+
"recharts": "https://aistudiocdn.com/recharts@^3.5.0",
|
| 70 |
+
"plotly.js": "https://cdn.jsdelivr.net/npm/plotly.js-dist-min@2.30.0/+esm"
|
| 71 |
+
}
|
| 72 |
+
}
|
| 73 |
+
</script>
|
| 74 |
+
</head>
|
| 75 |
+
<body>
|
| 76 |
+
<div id="root"></div>
|
| 77 |
+
</body>
|
| 78 |
+
</html>
|
index.html
CHANGED
|
@@ -14,28 +14,23 @@
|
|
| 14 |
background-color: #050505;
|
| 15 |
color: #ffffff;
|
| 16 |
}
|
| 17 |
-
|
| 18 |
/* Custom Scrollbar */
|
| 19 |
-
|
| 20 |
::-webkit-scrollbar {
|
| 21 |
width: 8px;
|
| 22 |
}
|
| 23 |
-
|
| 24 |
::-webkit-scrollbar-track {
|
| 25 |
background: #050505;
|
| 26 |
}
|
| 27 |
-
|
| 28 |
::-webkit-scrollbar-thumb {
|
| 29 |
background: #333;
|
| 30 |
border-radius: 4px;
|
| 31 |
}
|
| 32 |
-
|
| 33 |
::-webkit-scrollbar-thumb:hover {
|
| 34 |
background: #555;
|
| 35 |
}
|
| 36 |
|
| 37 |
/* Utilities for gradients */
|
| 38 |
-
|
| 39 |
.bg-glass {
|
| 40 |
background: rgba(255, 255, 255, 0.05);
|
| 41 |
backdrop-filter: blur(10px);
|
|
@@ -49,17 +44,14 @@
|
|
| 49 |
}
|
| 50 |
|
| 51 |
/* Blink animation for cursor */
|
| 52 |
-
|
| 53 |
@keyframes blink {
|
| 54 |
0%, 100% { opacity: 1; }
|
| 55 |
50% { opacity: 0; }
|
| 56 |
}
|
| 57 |
-
|
| 58 |
.animate-blink {
|
| 59 |
animation: blink 1s step-end infinite;
|
| 60 |
}
|
| 61 |
</style>
|
| 62 |
-
|
| 63 |
<script type="importmap">
|
| 64 |
{
|
| 65 |
"imports": {
|
|
@@ -72,7 +64,6 @@
|
|
| 72 |
}
|
| 73 |
}
|
| 74 |
</script>
|
| 75 |
-
<script type="module" crossorigin src="/assets/index-Ca5kstnp.js"></script>
|
| 76 |
</head>
|
| 77 |
<body>
|
| 78 |
<div id="root"></div>
|
|
|
|
| 14 |
background-color: #050505;
|
| 15 |
color: #ffffff;
|
| 16 |
}
|
| 17 |
+
|
| 18 |
/* Custom Scrollbar */
|
|
|
|
| 19 |
::-webkit-scrollbar {
|
| 20 |
width: 8px;
|
| 21 |
}
|
|
|
|
| 22 |
::-webkit-scrollbar-track {
|
| 23 |
background: #050505;
|
| 24 |
}
|
|
|
|
| 25 |
::-webkit-scrollbar-thumb {
|
| 26 |
background: #333;
|
| 27 |
border-radius: 4px;
|
| 28 |
}
|
|
|
|
| 29 |
::-webkit-scrollbar-thumb:hover {
|
| 30 |
background: #555;
|
| 31 |
}
|
| 32 |
|
| 33 |
/* Utilities for gradients */
|
|
|
|
| 34 |
.bg-glass {
|
| 35 |
background: rgba(255, 255, 255, 0.05);
|
| 36 |
backdrop-filter: blur(10px);
|
|
|
|
| 44 |
}
|
| 45 |
|
| 46 |
/* Blink animation for cursor */
|
|
|
|
| 47 |
@keyframes blink {
|
| 48 |
0%, 100% { opacity: 1; }
|
| 49 |
50% { opacity: 0; }
|
| 50 |
}
|
|
|
|
| 51 |
.animate-blink {
|
| 52 |
animation: blink 1s step-end infinite;
|
| 53 |
}
|
| 54 |
</style>
|
|
|
|
| 55 |
<script type="importmap">
|
| 56 |
{
|
| 57 |
"imports": {
|
|
|
|
| 64 |
}
|
| 65 |
}
|
| 66 |
</script>
|
|
|
|
| 67 |
</head>
|
| 68 |
<body>
|
| 69 |
<div id="root"></div>
|
index.tsx
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import ReactDOM from 'react-dom/client';
|
| 3 |
+
import App from './App';
|
| 4 |
+
|
| 5 |
+
const rootElement = document.getElementById('root');
|
| 6 |
+
if (!rootElement) {
|
| 7 |
+
throw new Error("Could not find root element to mount to");
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
const root = ReactDOM.createRoot(rootElement);
|
| 11 |
+
root.render(
|
| 12 |
+
<React.StrictMode>
|
| 13 |
+
<App />
|
| 14 |
+
</React.StrictMode>
|
| 15 |
+
);
|
metadata.json
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "SyncUsers Testing MVP",
|
| 3 |
+
"description": "A high-fidelity clone of the SyncUsers marketing frontend, featuring interactive simulations and data visualizations.",
|
| 4 |
+
"requestFramePermissions": []
|
| 5 |
+
}
|
package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "syncusers-testing-mvp",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "0.0.0",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite",
|
| 8 |
+
"build": "vite build",
|
| 9 |
+
"preview": "vite preview"
|
| 10 |
+
},
|
| 11 |
+
"dependencies": {
|
| 12 |
+
"@gradio/client": "^2.0.4",
|
| 13 |
+
"express": "^5.2.1",
|
| 14 |
+
"lucide-react": "^0.555.0",
|
| 15 |
+
"plotly.js": "2.30.0",
|
| 16 |
+
"react": "^19.2.0",
|
| 17 |
+
"react-dom": "^19.2.0",
|
| 18 |
+
"react-is": "^19.2.4",
|
| 19 |
+
"recharts": "^3.5.0",
|
| 20 |
+
"vite-plugin-node-polyfills": "^0.25.0"
|
| 21 |
+
},
|
| 22 |
+
"devDependencies": {
|
| 23 |
+
"@types/node": "^22.14.0",
|
| 24 |
+
"@vitejs/plugin-react": "^5.0.0",
|
| 25 |
+
"typescript": "~5.8.2",
|
| 26 |
+
"vite": "^6.2.0"
|
| 27 |
+
}
|
| 28 |
+
}
|
server.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const express = require('express');
|
| 2 |
+
const path = require('path');
|
| 3 |
+
const app = express();
|
| 4 |
+
const port = 7860;
|
| 5 |
+
|
| 6 |
+
app.use(express.static(path.join(__dirname, 'dist')));
|
| 7 |
+
|
| 8 |
+
app.get('/health', (req, res) => {
|
| 9 |
+
res.status(200).send('OK');
|
| 10 |
+
});
|
| 11 |
+
|
| 12 |
+
app.get('*', (req, res) => {
|
| 13 |
+
res.sendFile(path.join(__dirname, 'dist', 'index.html'));
|
| 14 |
+
});
|
| 15 |
+
|
| 16 |
+
app.listen(port, () => {
|
| 17 |
+
console.log(`Server running on port ${port}`);
|
| 18 |
+
});
|
services/gradioService.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Client } from "@gradio/client";
|
| 2 |
+
|
| 3 |
+
const HF_SPACE = "AUXteam/tiny_factory";
|
| 4 |
+
|
| 5 |
+
export class GradioService {
|
| 6 |
+
private static client: any = null;
|
| 7 |
+
|
| 8 |
+
static async getClient() {
|
| 9 |
+
if (!this.client) {
|
| 10 |
+
this.client = await Client.connect(HF_SPACE);
|
| 11 |
+
}
|
| 12 |
+
return this.client;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
static async identifyPersonas(context: string) {
|
| 16 |
+
try {
|
| 17 |
+
const client = await this.getClient();
|
| 18 |
+
const result = await client.predict("/identify_personas", [context]);
|
| 19 |
+
return result.data;
|
| 20 |
+
} catch (error) {
|
| 21 |
+
console.error("Error identifying personas:", error);
|
| 22 |
+
throw error;
|
| 23 |
+
}
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
static async simulate(persona: string, message: string) {
|
| 27 |
+
try {
|
| 28 |
+
const client = await this.getClient();
|
| 29 |
+
const result = await client.predict("/simulate", [persona, message]);
|
| 30 |
+
return result.data;
|
| 31 |
+
} catch (error) {
|
| 32 |
+
console.error("Error in simulation:", error);
|
| 33 |
+
throw error;
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
static async helpMeCraft(content: string) {
|
| 38 |
+
try {
|
| 39 |
+
// Assuming an endpoint for help me craft or similar
|
| 40 |
+
const client = await this.getClient();
|
| 41 |
+
const result = await client.predict("/help_me_craft", [content]);
|
| 42 |
+
return result.data;
|
| 43 |
+
} catch (error) {
|
| 44 |
+
console.error("Error helping to craft content:", error);
|
| 45 |
+
// Fallback or generic error handling
|
| 46 |
+
return "Unable to craft content at this time.";
|
| 47 |
+
}
|
| 48 |
+
}
|
| 49 |
+
}
|
tsconfig.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "ES2022",
|
| 4 |
+
"experimentalDecorators": true,
|
| 5 |
+
"useDefineForClassFields": false,
|
| 6 |
+
"module": "ESNext",
|
| 7 |
+
"lib": [
|
| 8 |
+
"ES2022",
|
| 9 |
+
"DOM",
|
| 10 |
+
"DOM.Iterable"
|
| 11 |
+
],
|
| 12 |
+
"skipLibCheck": true,
|
| 13 |
+
"types": [
|
| 14 |
+
"node"
|
| 15 |
+
],
|
| 16 |
+
"moduleResolution": "bundler",
|
| 17 |
+
"isolatedModules": true,
|
| 18 |
+
"moduleDetection": "force",
|
| 19 |
+
"allowJs": true,
|
| 20 |
+
"jsx": "react-jsx",
|
| 21 |
+
"paths": {
|
| 22 |
+
"@/*": [
|
| 23 |
+
"./*"
|
| 24 |
+
]
|
| 25 |
+
},
|
| 26 |
+
"allowImportingTsExtensions": true,
|
| 27 |
+
"noEmit": true
|
| 28 |
+
}
|
| 29 |
+
}
|
types.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export enum UseCaseCategory {
|
| 2 |
+
PR_COMMS = "PR & Comms",
|
| 3 |
+
PRODUCT = "Product",
|
| 4 |
+
BRANDING = "Branding",
|
| 5 |
+
MARKETING = "Marketing",
|
| 6 |
+
SOCIAL_MEDIA = "Social Media",
|
| 7 |
+
JOURNALISM = "Journalism"
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
export interface UseCase {
|
| 11 |
+
category: UseCaseCategory;
|
| 12 |
+
title: string;
|
| 13 |
+
description: string;
|
| 14 |
+
color: string;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
export interface NavLink {
|
| 18 |
+
label: string;
|
| 19 |
+
href: string;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
export interface Testimonial {
|
| 23 |
+
quote: string;
|
| 24 |
+
author: string;
|
| 25 |
+
role: string;
|
| 26 |
+
company: string;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
export interface FaqItem {
|
| 30 |
+
question: string;
|
| 31 |
+
answer: string;
|
| 32 |
+
}
|
vite.config.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import path from 'path';
|
| 2 |
+
import { defineConfig, loadEnv } from 'vite';
|
| 3 |
+
import react from '@vitejs/plugin-react';
|
| 4 |
+
import { nodePolyfills } from 'vite-plugin-node-polyfills';
|
| 5 |
+
|
| 6 |
+
export default defineConfig(({ mode }) => {
|
| 7 |
+
const env = loadEnv(mode, '.', '');
|
| 8 |
+
return {
|
| 9 |
+
server: {
|
| 10 |
+
port: 3000,
|
| 11 |
+
host: '0.0.0.0',
|
| 12 |
+
},
|
| 13 |
+
plugins: [
|
| 14 |
+
react(),
|
| 15 |
+
nodePolyfills({
|
| 16 |
+
include: ['buffer', 'process', 'util', 'stream'],
|
| 17 |
+
globals: {
|
| 18 |
+
Buffer: true,
|
| 19 |
+
process: true,
|
| 20 |
+
},
|
| 21 |
+
}),
|
| 22 |
+
],
|
| 23 |
+
define: {
|
| 24 |
+
'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
|
| 25 |
+
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
|
| 26 |
+
},
|
| 27 |
+
resolve: {
|
| 28 |
+
alias: {
|
| 29 |
+
'@': path.resolve(__dirname, '.'),
|
| 30 |
+
}
|
| 31 |
+
}
|
| 32 |
+
};
|
| 33 |
+
});
|