vish85521 commited on
Commit
6a44dcb
Β·
verified Β·
1 Parent(s): 20011dc

Upload 18 files

Browse files
.env.example ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ # ─────────────────────────────────────────────────────────────
2
+ # Copy this file to .env.local for local development.
3
+ # On Hugging Face Spaces, set these as Space Secrets/Variables.
4
+ # ─────────────────────────────────────────────────────────────
5
+
6
+ # URL of your FastAPI backend (Railway / Render / other HF Space)
7
+ # This is baked into the Next.js build at build time.
8
+ NEXT_PUBLIC_API_URL=https://vish85521-services.hf.space/
Dockerfile ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ── Stage 1: Build ──────────────────────────────────────────────
2
+ FROM node:20-alpine AS builder
3
+
4
+ WORKDIR /app
5
+
6
+ # Copy dependency files first for better layer caching
7
+ COPY package.json package-lock.json* ./
8
+
9
+ RUN npm ci
10
+
11
+ # Copy the rest of the source
12
+ COPY . .
13
+
14
+ # NEXT_PUBLIC_ vars must be available at build time
15
+ ARG NEXT_PUBLIC_API_URL=https://your-backend.hf.space
16
+ ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
17
+
18
+ RUN npm run build
19
+
20
+ # ── Stage 2: Runner ──────────────────────────────────────────────
21
+ FROM node:20-alpine AS runner
22
+
23
+ WORKDIR /app
24
+
25
+ ENV NODE_ENV=production
26
+ # Hugging Face Spaces requires the app to listen on port 7860
27
+ ENV PORT=7860
28
+ ENV HOSTNAME=0.0.0.0
29
+
30
+ # Only copy what is needed at runtime
31
+ COPY --from=builder /app/public ./public
32
+ COPY --from=builder /app/.next/standalone ./
33
+ COPY --from=builder /app/.next/static ./.next/static
34
+
35
+ EXPOSE 7860
36
+
37
+ CMD ["node", "server.js"]
app/dashboard/new/page.tsx ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { useState, useCallback } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ import Link from 'next/link';
6
+ import { useDropzone } from 'react-dropzone';
7
+ import { Zap, ArrowLeft, Upload, FileVideo, Loader2, X } from 'lucide-react';
8
+ import { projectsApi } from '@/lib/api';
9
+
10
+ export default function NewProjectPage() {
11
+ const router = useRouter();
12
+ const [title, setTitle] = useState('');
13
+ const [file, setFile] = useState<File | null>(null);
14
+ const [uploading, setUploading] = useState(false);
15
+ const [error, setError] = useState('');
16
+ const [uploadProgress, setUploadProgress] = useState(0);
17
+
18
+ const onDrop = useCallback((acceptedFiles: File[]) => {
19
+ if (acceptedFiles.length > 0) {
20
+ setFile(acceptedFiles[0]);
21
+ setError('');
22
+ }
23
+ }, []);
24
+
25
+ const { getRootProps, getInputProps, isDragActive } = useDropzone({
26
+ onDrop,
27
+ accept: {
28
+ 'video/*': ['.mp4', '.mov', '.avi', '.webm', '.mkv'],
29
+ },
30
+ maxFiles: 1,
31
+ maxSize: 500 * 1024 * 1024, // 500MB
32
+ });
33
+
34
+ const handleSubmit = async (e: React.FormEvent) => {
35
+ e.preventDefault();
36
+ setError('');
37
+
38
+ if (!title.trim()) {
39
+ setError('Please enter a project title');
40
+ return;
41
+ }
42
+
43
+ if (!file) {
44
+ setError('Please upload a video file');
45
+ return;
46
+ }
47
+
48
+ setUploading(true);
49
+ setUploadProgress(0);
50
+
51
+ try {
52
+ const formData = new FormData();
53
+ formData.append('title', title);
54
+ formData.append('video', file);
55
+
56
+ // Simulate progress (actual progress would require XHR)
57
+ const progressInterval = setInterval(() => {
58
+ setUploadProgress((prev) => Math.min(prev + 10, 90));
59
+ }, 500);
60
+
61
+ const project = await projectsApi.create(formData);
62
+
63
+ clearInterval(progressInterval);
64
+ setUploadProgress(100);
65
+
66
+ router.push(`/dashboard/project/${project.id}`);
67
+ } catch (err: any) {
68
+ setError(err.response?.data?.detail || 'Failed to create project');
69
+ } finally {
70
+ setUploading(false);
71
+ }
72
+ };
73
+
74
+ const removeFile = () => {
75
+ setFile(null);
76
+ };
77
+
78
+ const formatFileSize = (bytes: number) => {
79
+ if (bytes < 1024 * 1024) {
80
+ return `${(bytes / 1024).toFixed(1)} KB`;
81
+ }
82
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
83
+ };
84
+
85
+ return (
86
+ <div className="min-h-screen p-8">
87
+ <div className="max-w-2xl mx-auto">
88
+ {/* Header */}
89
+ <div className="flex items-center space-x-4 mb-8">
90
+ <Link
91
+ href="/dashboard"
92
+ className="p-2 rounded-xl glass hover:bg-white/10 transition-colors"
93
+ >
94
+ <ArrowLeft className="w-5 h-5" />
95
+ </Link>
96
+ <div>
97
+ <h1 className="text-2xl font-bold">New Project</h1>
98
+ <p className="text-white/60">Upload a video to analyze with AI</p>
99
+ </div>
100
+ </div>
101
+
102
+ {/* Form */}
103
+ <div className="glass-card rounded-2xl p-8">
104
+ {error && (
105
+ <div className="mb-6 p-4 rounded-xl bg-red-500/10 border border-red-500/30 text-red-400 text-sm">
106
+ {error}
107
+ </div>
108
+ )}
109
+
110
+ <form onSubmit={handleSubmit} className="space-y-6">
111
+ {/* Title */}
112
+ <div>
113
+ <label className="block text-sm font-medium mb-2">
114
+ Project Title
115
+ </label>
116
+ <input
117
+ type="text"
118
+ value={title}
119
+ onChange={(e) => setTitle(e.target.value)}
120
+ className="input-field"
121
+ placeholder="e.g., Q1 Campaign Ad"
122
+ disabled={uploading}
123
+ />
124
+ </div>
125
+
126
+ {/* Video Upload */}
127
+ <div>
128
+ <label className="block text-sm font-medium mb-2">
129
+ Video File
130
+ </label>
131
+
132
+ {file ? (
133
+ <div className="glass rounded-xl p-4">
134
+ <div className="flex items-center justify-between">
135
+ <div className="flex items-center space-x-3">
136
+ <div className="w-10 h-10 rounded-lg bg-primary-500/20 flex items-center justify-center">
137
+ <FileVideo className="w-5 h-5 text-primary-400" />
138
+ </div>
139
+ <div>
140
+ <p className="font-medium truncate max-w-xs">
141
+ {file.name}
142
+ </p>
143
+ <p className="text-sm text-white/40">
144
+ {formatFileSize(file.size)}
145
+ </p>
146
+ </div>
147
+ </div>
148
+ {!uploading && (
149
+ <button
150
+ type="button"
151
+ onClick={removeFile}
152
+ className="p-2 rounded-lg hover:bg-white/10 transition-colors"
153
+ >
154
+ <X className="w-5 h-5 text-white/60" />
155
+ </button>
156
+ )}
157
+ </div>
158
+
159
+ {uploading && (
160
+ <div className="mt-4">
161
+ <div className="progress-bar">
162
+ <div
163
+ className="progress-bar-fill"
164
+ style={{ width: `${uploadProgress}%` }}
165
+ />
166
+ </div>
167
+ <p className="text-sm text-white/60 mt-2">
168
+ Uploading... {uploadProgress}%
169
+ </p>
170
+ </div>
171
+ )}
172
+ </div>
173
+ ) : (
174
+ <div
175
+ {...getRootProps()}
176
+ className={`
177
+ border-2 border-dashed rounded-xl p-8 text-center cursor-pointer
178
+ transition-colors duration-200
179
+ ${isDragActive
180
+ ? 'border-primary-500 bg-primary-500/10'
181
+ : 'border-white/20 hover:border-white/40'
182
+ }
183
+ `}
184
+ >
185
+ <input {...getInputProps()} />
186
+ <Upload className="w-12 h-12 text-white/40 mx-auto mb-4" />
187
+ <p className="text-lg font-medium mb-1">
188
+ {isDragActive
189
+ ? 'Drop your video here'
190
+ : 'Drag & drop your video'}
191
+ </p>
192
+ <p className="text-white/60 text-sm">
193
+ or click to browse (MP4, MOV, WebM, max 500MB)
194
+ </p>
195
+ </div>
196
+ )}
197
+ </div>
198
+
199
+ {/* Submit */}
200
+ <button
201
+ type="submit"
202
+ disabled={uploading || !file || !title.trim()}
203
+ className="w-full btn-primary py-4 flex items-center justify-center disabled:opacity-50 disabled:cursor-not-allowed"
204
+ >
205
+ {uploading ? (
206
+ <>
207
+ <Loader2 className="w-5 h-5 animate-spin mr-2" />
208
+ Processing...
209
+ </>
210
+ ) : (
211
+ <>
212
+ <Zap className="w-5 h-5 mr-2" />
213
+ Create Project
214
+ </>
215
+ )}
216
+ </button>
217
+ </form>
218
+ </div>
219
+ </div>
220
+ </div>
221
+ );
222
+ }
app/dashboard/page.tsx ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ import Link from 'next/link';
6
+ import { useQuery } from '@tanstack/react-query';
7
+ import {
8
+ Zap,
9
+ Plus,
10
+ FileVideo,
11
+ Clock,
12
+ CheckCircle,
13
+ AlertCircle,
14
+ LogOut,
15
+ Loader2,
16
+ TrendingUp
17
+ } from 'lucide-react';
18
+ import { projectsApi, authApi } from '@/lib/api';
19
+ import { useAuthStore } from '@/lib/store';
20
+
21
+ export default function DashboardPage() {
22
+ const router = useRouter();
23
+ const { user, setUser, logout } = useAuthStore();
24
+ const [mounted, setMounted] = useState(false);
25
+
26
+ useEffect(() => {
27
+ setMounted(true);
28
+ // Check auth on mount
29
+ const checkAuth = async () => {
30
+ const token = localStorage.getItem('token');
31
+ if (!token) {
32
+ router.push('/login');
33
+ return;
34
+ }
35
+ try {
36
+ const userData = await authApi.getMe();
37
+ setUser(userData);
38
+ } catch {
39
+ router.push('/login');
40
+ }
41
+ };
42
+ checkAuth();
43
+ }, []);
44
+
45
+ const { data: projects, isLoading } = useQuery({
46
+ queryKey: ['projects'],
47
+ queryFn: projectsApi.list,
48
+ enabled: mounted,
49
+ });
50
+
51
+ const handleLogout = () => {
52
+ authApi.logout();
53
+ logout();
54
+ router.push('/');
55
+ };
56
+
57
+ const getStatusIcon = (status: string) => {
58
+ switch (status) {
59
+ case 'READY':
60
+ return <CheckCircle className="w-5 h-5 text-emerald-400" />;
61
+ case 'PROCESSING':
62
+ return <Loader2 className="w-5 h-5 text-primary-400 animate-spin" />;
63
+ case 'FAILED':
64
+ return <AlertCircle className="w-5 h-5 text-red-400" />;
65
+ default:
66
+ return <Clock className="w-5 h-5 text-yellow-400" />;
67
+ }
68
+ };
69
+
70
+ const getStatusText = (status: string) => {
71
+ switch (status) {
72
+ case 'READY':
73
+ return 'Ready';
74
+ case 'PROCESSING':
75
+ return 'Processing';
76
+ case 'FAILED':
77
+ return 'Failed';
78
+ default:
79
+ return 'Pending';
80
+ }
81
+ };
82
+
83
+ if (!mounted) return null;
84
+
85
+ return (
86
+ <div className="min-h-screen">
87
+ {/* Sidebar */}
88
+ <aside className="fixed left-0 top-0 w-64 h-full glass border-r border-white/10 p-6">
89
+ <div className="flex items-center space-x-2 mb-8">
90
+ <div className="w-10 h-10 rounded-xl bg-gradient-to-r from-primary-500 to-accent-500 flex items-center justify-center">
91
+ <Zap className="w-6 h-6 text-white" />
92
+ </div>
93
+ <span className="text-xl font-bold gradient-text">AgentSociety</span>
94
+ </div>
95
+
96
+ <nav className="space-y-2">
97
+ <Link
98
+ href="/dashboard"
99
+ className="flex items-center space-x-3 px-4 py-3 rounded-xl bg-white/10 text-white"
100
+ >
101
+ <TrendingUp className="w-5 h-5" />
102
+ <span>Projects</span>
103
+ </Link>
104
+ </nav>
105
+
106
+ <div className="absolute bottom-6 left-6 right-6">
107
+ <div className="glass rounded-xl p-4 mb-4">
108
+ <p className="text-sm text-white/60">Logged in as</p>
109
+ <p className="text-sm font-medium truncate">{user?.email}</p>
110
+ </div>
111
+ <button
112
+ onClick={handleLogout}
113
+ className="flex items-center space-x-2 text-white/60 hover:text-white transition-colors"
114
+ >
115
+ <LogOut className="w-5 h-5" />
116
+ <span>Logout</span>
117
+ </button>
118
+ </div>
119
+ </aside>
120
+
121
+ {/* Main Content */}
122
+ <main className="ml-64 p-8">
123
+ <div className="max-w-6xl mx-auto">
124
+ {/* Header */}
125
+ <div className="flex items-center justify-between mb-8">
126
+ <div>
127
+ <h1 className="text-3xl font-bold">Your Projects</h1>
128
+ <p className="text-white/60 mt-1">Upload videos and run AI simulations</p>
129
+ </div>
130
+
131
+ <Link href="/dashboard/new" className="btn-primary flex items-center space-x-2">
132
+ <Plus className="w-5 h-5" />
133
+ <span>New Project</span>
134
+ </Link>
135
+ </div>
136
+
137
+ {/* Projects Grid */}
138
+ {isLoading ? (
139
+ <div className="flex items-center justify-center py-24">
140
+ <div className="spinner" />
141
+ </div>
142
+ ) : projects?.length === 0 ? (
143
+ <div className="glass-card rounded-2xl p-12 text-center">
144
+ <FileVideo className="w-16 h-16 text-white/20 mx-auto mb-4" />
145
+ <h3 className="text-xl font-semibold mb-2">No projects yet</h3>
146
+ <p className="text-white/60 mb-6">
147
+ Upload your first video to start simulating AI reactions
148
+ </p>
149
+ <Link href="/dashboard/new" className="btn-primary inline-flex items-center space-x-2">
150
+ <Plus className="w-5 h-5" />
151
+ <span>Create Project</span>
152
+ </Link>
153
+ </div>
154
+ ) : (
155
+ <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
156
+ {projects?.map((project: any) => (
157
+ <Link
158
+ key={project.id}
159
+ href={`/dashboard/project/${project.id}`}
160
+ className="glass-card rounded-2xl p-6 hover-lift cursor-pointer"
161
+ >
162
+ <div className="flex items-start justify-between mb-4">
163
+ <div className="w-12 h-12 rounded-xl bg-gradient-to-r from-primary-500/20 to-accent-500/20 flex items-center justify-center">
164
+ <FileVideo className="w-6 h-6 text-primary-400" />
165
+ </div>
166
+ <div className="flex items-center space-x-2">
167
+ {getStatusIcon(project.status)}
168
+ <span className="text-sm">{getStatusText(project.status)}</span>
169
+ </div>
170
+ </div>
171
+
172
+ <h3 className="text-lg font-semibold mb-2 truncate">{project.title}</h3>
173
+
174
+ <p className="text-sm text-white/40">
175
+ Created {new Date(project.created_at).toLocaleDateString()}
176
+ </p>
177
+ </Link>
178
+ ))}
179
+ </div>
180
+ )}
181
+ </div>
182
+ </main>
183
+ </div>
184
+ );
185
+ }
app/dashboard/project/[id]/page.tsx ADDED
@@ -0,0 +1,353 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import { useParams, useRouter } from 'next/navigation';
5
+ import Link from 'next/link';
6
+ import { useQuery, useMutation } from '@tanstack/react-query';
7
+ import {
8
+ ArrowLeft,
9
+ Play,
10
+ Loader2,
11
+ AlertTriangle,
12
+ CheckCircle,
13
+ Users,
14
+ TrendingUp,
15
+ MessageCircle,
16
+ AlertCircle,
17
+ } from 'lucide-react';
18
+ import {
19
+ PieChart,
20
+ Pie,
21
+ Cell,
22
+ ResponsiveContainer,
23
+ BarChart,
24
+ Bar,
25
+ XAxis,
26
+ YAxis,
27
+ Tooltip,
28
+ } from 'recharts';
29
+ import { projectsApi, simulationsApi } from '@/lib/api';
30
+
31
+ const COLORS = {
32
+ positive: '#10b981',
33
+ neutral: '#eab308',
34
+ negative: '#ef4444',
35
+ };
36
+
37
+ export default function ProjectDetailPage() {
38
+ const params = useParams();
39
+ const router = useRouter();
40
+ const projectId = params.id as string;
41
+
42
+ const [numAgents, setNumAgents] = useState(100);
43
+ const [simulationDays, setSimulationDays] = useState(5);
44
+ const [activeSimulation, setActiveSimulation] = useState<any>(null);
45
+ const [pollingEnabled, setPollingEnabled] = useState(false);
46
+
47
+ // Fetch project - poll while processing
48
+ const { data: project, isLoading: projectLoading } = useQuery({
49
+ queryKey: ['project', projectId],
50
+ queryFn: () => projectsApi.get(projectId),
51
+ refetchInterval: (query) => {
52
+ // Poll every 5 seconds while video is processing
53
+ const status = query.state.data?.status;
54
+ return status && status !== 'READY' && status !== 'FAILED' ? 5000 : false;
55
+ },
56
+ });
57
+
58
+ // Poll simulation status
59
+ const { data: simulationStatus } = useQuery({
60
+ queryKey: ['simulationStatus', activeSimulation?.id],
61
+ queryFn: () => simulationsApi.getStatus(activeSimulation.id),
62
+ enabled: pollingEnabled && !!activeSimulation,
63
+ refetchInterval: 2000,
64
+ });
65
+
66
+ // Check if simulation completed
67
+ useEffect(() => {
68
+ if (simulationStatus?.status === 'COMPLETED' || simulationStatus?.status === 'FAILED') {
69
+ setPollingEnabled(false);
70
+ }
71
+ }, [simulationStatus]);
72
+
73
+ // Start simulation mutation
74
+ const startSimulation = useMutation({
75
+ mutationFn: () => simulationsApi.start(projectId, { num_agents: numAgents, simulation_days: simulationDays }),
76
+ onSuccess: (data) => {
77
+ setActiveSimulation(data);
78
+ setPollingEnabled(true);
79
+ },
80
+ });
81
+
82
+ // Fetch results when completed
83
+ const { data: results, isLoading: resultsLoading } = useQuery({
84
+ queryKey: ['simulationResults', activeSimulation?.id],
85
+ queryFn: () => simulationsApi.getResults(activeSimulation.id),
86
+ enabled: simulationStatus?.status === 'COMPLETED',
87
+ });
88
+
89
+ const handleStartSimulation = () => {
90
+ startSimulation.mutate();
91
+ };
92
+
93
+ const getSeverityClass = (severity: string) => {
94
+ switch (severity) {
95
+ case 'CRITICAL': return 'risk-critical';
96
+ case 'HIGH': return 'risk-high';
97
+ case 'MEDIUM': return 'risk-medium';
98
+ default: return 'risk-low';
99
+ }
100
+ };
101
+
102
+ if (projectLoading) {
103
+ return (
104
+ <div className="min-h-screen flex items-center justify-center">
105
+ <div className="spinner" />
106
+ </div>
107
+ );
108
+ }
109
+
110
+ if (!project) {
111
+ return (
112
+ <div className="min-h-screen flex items-center justify-center">
113
+ <p>Project not found</p>
114
+ </div>
115
+ );
116
+ }
117
+
118
+ const sentimentData = results?.sentiment_breakdown
119
+ ? [
120
+ { name: 'Positive', value: results.sentiment_breakdown.positive, color: COLORS.positive },
121
+ { name: 'Neutral', value: results.sentiment_breakdown.neutral, color: COLORS.neutral },
122
+ { name: 'Negative', value: results.sentiment_breakdown.negative, color: COLORS.negative },
123
+ ]
124
+ : [];
125
+
126
+ return (
127
+ <div className="min-h-screen p-8">
128
+ <div className="max-w-6xl mx-auto">
129
+ {/* Header */}
130
+ <div className="flex items-center space-x-4 mb-8">
131
+ <Link
132
+ href="/dashboard"
133
+ className="p-2 rounded-xl glass hover:bg-white/10 transition-colors"
134
+ >
135
+ <ArrowLeft className="w-5 h-5" />
136
+ </Link>
137
+ <div>
138
+ <h1 className="text-2xl font-bold">{project.title}</h1>
139
+ <p className="text-white/60">
140
+ Status: {project.status} β€’ Created {new Date(project.created_at).toLocaleDateString()}
141
+ </p>
142
+ </div>
143
+ </div>
144
+
145
+ <div className="grid lg:grid-cols-3 gap-8">
146
+ {/* Left Column - Controls */}
147
+ <div className="lg:col-span-1">
148
+ <div className="glass-card rounded-2xl p-6">
149
+ <h2 className="text-lg font-semibold mb-4">Simulation Settings</h2>
150
+
151
+ {project.status !== 'READY' ? (
152
+ <div className="text-center py-8">
153
+ <Loader2 className="w-8 h-8 animate-spin mx-auto mb-4 text-primary-400" />
154
+ <p className="text-white/60">Video is being processed...</p>
155
+ <p className="text-sm text-white/40 mt-2">This may take a few minutes</p>
156
+ </div>
157
+ ) : (
158
+ <>
159
+ <div className="space-y-4 mb-6">
160
+ <div>
161
+ <label className="block text-sm font-medium mb-2">
162
+ Number of Agents
163
+ </label>
164
+ <input
165
+ type="number"
166
+ value={numAgents}
167
+ onChange={(e) => setNumAgents(Math.max(10, Math.min(1000, parseInt(e.target.value) || 10)))}
168
+ className="input-field"
169
+ min={10}
170
+ max={1000}
171
+ disabled={pollingEnabled}
172
+ />
173
+ <p className="text-xs text-white/40 mt-1">10 - 1,000 agents</p>
174
+ </div>
175
+
176
+ <div>
177
+ <label className="block text-sm font-medium mb-2">
178
+ Simulation Days
179
+ </label>
180
+ <input
181
+ type="number"
182
+ value={simulationDays}
183
+ onChange={(e) => setSimulationDays(Math.max(1, Math.min(30, parseInt(e.target.value) || 1)))}
184
+ className="input-field"
185
+ min={1}
186
+ max={30}
187
+ disabled={pollingEnabled}
188
+ />
189
+ </div>
190
+ </div>
191
+
192
+ <button
193
+ onClick={handleStartSimulation}
194
+ disabled={pollingEnabled || startSimulation.isPending}
195
+ className="w-full btn-primary py-3 flex items-center justify-center disabled:opacity-50"
196
+ >
197
+ {pollingEnabled ? (
198
+ <>
199
+ <Loader2 className="w-5 h-5 animate-spin mr-2" />
200
+ Running...
201
+ </>
202
+ ) : (
203
+ <>
204
+ <Play className="w-5 h-5 mr-2" />
205
+ Start Simulation
206
+ </>
207
+ )}
208
+ </button>
209
+ </>
210
+ )}
211
+
212
+ {/* Progress */}
213
+ {pollingEnabled && simulationStatus && (
214
+ <div className="mt-6">
215
+ <div className="flex justify-between text-sm mb-2">
216
+ <span>Progress</span>
217
+ <span>{simulationStatus.progress}%</span>
218
+ </div>
219
+ <div className="progress-bar">
220
+ <div
221
+ className="progress-bar-fill"
222
+ style={{ width: `${simulationStatus.progress}%` }}
223
+ />
224
+ </div>
225
+ <p className="text-sm text-white/40 mt-2">
226
+ Day {simulationStatus.current_day} β€’ {simulationStatus.active_agents} agents active
227
+ </p>
228
+ </div>
229
+ )}
230
+ </div>
231
+ </div>
232
+
233
+ {/* Right Column - Results */}
234
+ <div className="lg:col-span-2 space-y-6">
235
+ {results ? (
236
+ <>
237
+ {/* Stats Cards */}
238
+ <div className="grid grid-cols-3 gap-4">
239
+ <div className="glass-card rounded-xl p-4 text-center">
240
+ <TrendingUp className="w-6 h-6 mx-auto mb-2 text-primary-400" />
241
+ <p className="text-2xl font-bold gradient-text">{results.virality_score}%</p>
242
+ <p className="text-sm text-white/60">Virality Score</p>
243
+ </div>
244
+ <div className="glass-card rounded-xl p-4 text-center">
245
+ <Users className="w-6 h-6 mx-auto mb-2 text-emerald-400" />
246
+ <p className="text-2xl font-bold">{results.total_agents}</p>
247
+ <p className="text-sm text-white/60">Total Agents</p>
248
+ </div>
249
+ <div className="glass-card rounded-xl p-4 text-center">
250
+ <AlertTriangle className="w-6 h-6 mx-auto mb-2 text-red-400" />
251
+ <p className="text-2xl font-bold">{results.risk_flags?.length || 0}</p>
252
+ <p className="text-sm text-white/60">Risk Flags</p>
253
+ </div>
254
+ </div>
255
+
256
+ {/* Sentiment Chart */}
257
+ <div className="glass-card rounded-2xl p-6">
258
+ <h3 className="text-lg font-semibold mb-4">Sentiment Breakdown</h3>
259
+ <div className="flex items-center">
260
+ <div className="w-48 h-48">
261
+ <ResponsiveContainer width="100%" height="100%">
262
+ <PieChart>
263
+ <Pie
264
+ data={sentimentData}
265
+ cx="50%"
266
+ cy="50%"
267
+ innerRadius={50}
268
+ outerRadius={80}
269
+ dataKey="value"
270
+ stroke="none"
271
+ >
272
+ {sentimentData.map((entry, index) => (
273
+ <Cell key={`cell-${index}`} fill={entry.color} />
274
+ ))}
275
+ </Pie>
276
+ </PieChart>
277
+ </ResponsiveContainer>
278
+ </div>
279
+ <div className="flex-1 space-y-3 ml-8">
280
+ {sentimentData.map((item) => (
281
+ <div key={item.name} className="flex items-center justify-between">
282
+ <div className="flex items-center space-x-2">
283
+ <div
284
+ className="w-3 h-3 rounded-full"
285
+ style={{ backgroundColor: item.color }}
286
+ />
287
+ <span>{item.name}</span>
288
+ </div>
289
+ <span className="font-semibold">{item.value}</span>
290
+ </div>
291
+ ))}
292
+ </div>
293
+ </div>
294
+ </div>
295
+
296
+ {/* Risk Flags */}
297
+ {results.risk_flags?.length > 0 && (
298
+ <div className="glass-card rounded-2xl p-6">
299
+ <h3 className="text-lg font-semibold mb-4 flex items-center">
300
+ <AlertCircle className="w-5 h-5 mr-2 text-red-400" />
301
+ Risk Flags
302
+ </h3>
303
+ <div className="space-y-4">
304
+ {results.risk_flags.map((flag: any, index: number) => (
305
+ <div
306
+ key={index}
307
+ className={`p-4 rounded-xl border ${getSeverityClass(flag.severity)}`}
308
+ >
309
+ <div className="flex items-center justify-between mb-2">
310
+ <span className="font-medium">{flag.flag_type.replace(/_/g, ' ')}</span>
311
+ <span className="text-sm px-2 py-1 rounded-lg bg-white/10">
312
+ {flag.severity}
313
+ </span>
314
+ </div>
315
+ <p className="text-sm opacity-80">{flag.description}</p>
316
+
317
+ {flag.sample_agent_reactions?.length > 0 && (
318
+ <div className="mt-3 space-y-2">
319
+ <p className="text-xs font-medium opacity-60">Sample reactions:</p>
320
+ {flag.sample_agent_reactions.map((reaction: any, i: number) => (
321
+ <div key={i} className="text-xs opacity-60 flex items-start space-x-2">
322
+ <MessageCircle className="w-3 h-3 mt-0.5 flex-shrink-0" />
323
+ <span>"{reaction.reasoning}"</span>
324
+ </div>
325
+ ))}
326
+ </div>
327
+ )}
328
+ </div>
329
+ ))}
330
+ </div>
331
+ </div>
332
+ )}
333
+ </>
334
+ ) : simulationStatus?.status === 'COMPLETED' && resultsLoading ? (
335
+ <div className="glass-card rounded-2xl p-12 text-center">
336
+ <Loader2 className="w-8 h-8 animate-spin mx-auto mb-4" />
337
+ <p>Loading results...</p>
338
+ </div>
339
+ ) : (
340
+ <div className="glass-card rounded-2xl p-12 text-center">
341
+ <TrendingUp className="w-16 h-16 text-white/20 mx-auto mb-4" />
342
+ <h3 className="text-xl font-semibold mb-2">No Results Yet</h3>
343
+ <p className="text-white/60">
344
+ Run a simulation to see AI agent reactions
345
+ </p>
346
+ </div>
347
+ )}
348
+ </div>
349
+ </div>
350
+ </div>
351
+ </div>
352
+ );
353
+ }
app/globals.css ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ :root {
6
+ --foreground-rgb: 255, 255, 255;
7
+ --background-start-rgb: 15, 23, 42;
8
+ --background-end-rgb: 30, 41, 59;
9
+ }
10
+
11
+ body {
12
+ color: rgb(var(--foreground-rgb));
13
+ background: linear-gradient(
14
+ 135deg,
15
+ rgb(var(--background-start-rgb)) 0%,
16
+ rgb(var(--background-end-rgb)) 100%
17
+ );
18
+ min-height: 100vh;
19
+ }
20
+
21
+ /* Glass morphism effect */
22
+ .glass {
23
+ background: rgba(255, 255, 255, 0.05);
24
+ backdrop-filter: blur(10px);
25
+ border: 1px solid rgba(255, 255, 255, 0.1);
26
+ }
27
+
28
+ .glass-card {
29
+ background: linear-gradient(
30
+ 135deg,
31
+ rgba(255, 255, 255, 0.1) 0%,
32
+ rgba(255, 255, 255, 0.05) 100%
33
+ );
34
+ backdrop-filter: blur(20px);
35
+ border: 1px solid rgba(255, 255, 255, 0.15);
36
+ box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
37
+ }
38
+
39
+ /* Gradient text */
40
+ .gradient-text {
41
+ background: linear-gradient(135deg, #0ea5e9 0%, #d946ef 100%);
42
+ -webkit-background-clip: text;
43
+ -webkit-text-fill-color: transparent;
44
+ background-clip: text;
45
+ }
46
+
47
+ /* Button styles */
48
+ .btn-primary {
49
+ @apply px-6 py-3 bg-gradient-to-r from-primary-500 to-accent-500 rounded-xl font-semibold;
50
+ @apply hover:from-primary-600 hover:to-accent-600 transition-all duration-300;
51
+ @apply shadow-lg hover:shadow-primary-500/30;
52
+ }
53
+
54
+ .btn-secondary {
55
+ @apply px-6 py-3 glass rounded-xl font-semibold;
56
+ @apply hover:bg-white/10 transition-all duration-300;
57
+ }
58
+
59
+ /* Input styles */
60
+ .input-field {
61
+ @apply w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl;
62
+ @apply focus:outline-none focus:border-primary-500 focus:ring-2 focus:ring-primary-500/20;
63
+ @apply transition-all duration-300 text-white placeholder-white/40;
64
+ }
65
+
66
+ /* Card hover effect */
67
+ .hover-lift {
68
+ @apply transition-all duration-300;
69
+ }
70
+
71
+ .hover-lift:hover {
72
+ @apply -translate-y-1 shadow-xl;
73
+ }
74
+
75
+ /* Animate gradient background */
76
+ .animate-gradient-bg {
77
+ background: linear-gradient(
78
+ -45deg,
79
+ #0ea5e9,
80
+ #6366f1,
81
+ #d946ef,
82
+ #0ea5e9
83
+ );
84
+ background-size: 400% 400%;
85
+ animation: gradient 15s ease infinite;
86
+ }
87
+
88
+ @keyframes gradient {
89
+ 0% {
90
+ background-position: 0% 50%;
91
+ }
92
+ 50% {
93
+ background-position: 100% 50%;
94
+ }
95
+ 100% {
96
+ background-position: 0% 50%;
97
+ }
98
+ }
99
+
100
+ /* Loading spinner */
101
+ .spinner {
102
+ width: 40px;
103
+ height: 40px;
104
+ border: 3px solid rgba(255, 255, 255, 0.1);
105
+ border-top-color: #0ea5e9;
106
+ border-radius: 50%;
107
+ animation: spin 1s linear infinite;
108
+ }
109
+
110
+ @keyframes spin {
111
+ to {
112
+ transform: rotate(360deg);
113
+ }
114
+ }
115
+
116
+ /* Progress bar */
117
+ .progress-bar {
118
+ @apply h-2 rounded-full bg-white/10 overflow-hidden;
119
+ }
120
+
121
+ .progress-bar-fill {
122
+ @apply h-full rounded-full transition-all duration-500 ease-out;
123
+ background: linear-gradient(90deg, #0ea5e9 0%, #d946ef 100%);
124
+ }
125
+
126
+ /* Sentiment colors */
127
+ .sentiment-positive {
128
+ @apply text-emerald-400;
129
+ }
130
+
131
+ .sentiment-neutral {
132
+ @apply text-yellow-400;
133
+ }
134
+
135
+ .sentiment-negative {
136
+ @apply text-red-400;
137
+ }
138
+
139
+ /* Risk severity colors */
140
+ .risk-critical {
141
+ @apply bg-red-500/20 border-red-500/50 text-red-400;
142
+ }
143
+
144
+ .risk-high {
145
+ @apply bg-orange-500/20 border-orange-500/50 text-orange-400;
146
+ }
147
+
148
+ .risk-medium {
149
+ @apply bg-yellow-500/20 border-yellow-500/50 text-yellow-400;
150
+ }
151
+
152
+ .risk-low {
153
+ @apply bg-blue-500/20 border-blue-500/50 text-blue-400;
154
+ }
app/layout.tsx ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Metadata } from 'next';
2
+ import { Inter } from 'next/font/google';
3
+ import './globals.css';
4
+ import { Providers } from './providers';
5
+
6
+ const inter = Inter({ subsets: ['latin'] });
7
+
8
+ export const metadata: Metadata = {
9
+ title: 'AgentSociety - AI Marketing Simulation',
10
+ description: 'Simulate 1000+ AI agents reacting to your advertisements before launch',
11
+ };
12
+
13
+ export default function RootLayout({
14
+ children,
15
+ }: {
16
+ children: React.ReactNode;
17
+ }) {
18
+ return (
19
+ <html lang="en">
20
+ <body className={inter.className}>
21
+ <Providers>
22
+ {children}
23
+ </Providers>
24
+ </body>
25
+ </html>
26
+ );
27
+ }
app/login/page.tsx ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ import Link from 'next/link';
6
+ import { Zap, Mail, Lock, Loader2 } from 'lucide-react';
7
+ import { authApi } from '@/lib/api';
8
+ import { useAuthStore } from '@/lib/store';
9
+
10
+ export default function LoginPage() {
11
+ const router = useRouter();
12
+ const { setUser } = useAuthStore();
13
+ const [email, setEmail] = useState('');
14
+ const [password, setPassword] = useState('');
15
+ const [error, setError] = useState('');
16
+ const [loading, setLoading] = useState(false);
17
+
18
+ const handleSubmit = async (e: React.FormEvent) => {
19
+ e.preventDefault();
20
+ setError('');
21
+ setLoading(true);
22
+
23
+ try {
24
+ await authApi.login(email, password);
25
+ const user = await authApi.getMe();
26
+ setUser(user);
27
+ router.push('/dashboard');
28
+ } catch (err: any) {
29
+ setError(err.response?.data?.detail || 'Login failed. Please try again.');
30
+ } finally {
31
+ setLoading(false);
32
+ }
33
+ };
34
+
35
+ return (
36
+ <div className="min-h-screen flex items-center justify-center px-4">
37
+ {/* Background decoration */}
38
+ <div className="absolute inset-0 overflow-hidden">
39
+ <div className="absolute -top-40 -right-40 w-80 h-80 bg-primary-500/20 rounded-full blur-3xl" />
40
+ <div className="absolute -bottom-40 -left-40 w-80 h-80 bg-accent-500/20 rounded-full blur-3xl" />
41
+ </div>
42
+
43
+ <div className="relative w-full max-w-md">
44
+ {/* Logo */}
45
+ <div className="flex items-center justify-center mb-8">
46
+ <Link href="/" className="flex items-center space-x-2">
47
+ <div className="w-12 h-12 rounded-xl bg-gradient-to-r from-primary-500 to-accent-500 flex items-center justify-center">
48
+ <Zap className="w-7 h-7 text-white" />
49
+ </div>
50
+ <span className="text-2xl font-bold gradient-text">AgentSociety</span>
51
+ </Link>
52
+ </div>
53
+
54
+ {/* Login Card */}
55
+ <div className="glass-card rounded-2xl p-8">
56
+ <h1 className="text-2xl font-bold text-center mb-2">Welcome Back</h1>
57
+ <p className="text-white/60 text-center mb-8">
58
+ Sign in to access your dashboard
59
+ </p>
60
+
61
+ {error && (
62
+ <div className="mb-6 p-4 rounded-xl bg-red-500/10 border border-red-500/30 text-red-400 text-sm">
63
+ {error}
64
+ </div>
65
+ )}
66
+
67
+ <form onSubmit={handleSubmit} className="space-y-6">
68
+ <div>
69
+ <label className="block text-sm font-medium mb-2">Email</label>
70
+ <div className="relative">
71
+ <Mail className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-white/40" />
72
+ <input
73
+ type="email"
74
+ value={email}
75
+ onChange={(e) => setEmail(e.target.value)}
76
+ className="input-field pl-12"
77
+ placeholder="you@example.com"
78
+ required
79
+ />
80
+ </div>
81
+ </div>
82
+
83
+ <div>
84
+ <label className="block text-sm font-medium mb-2">Password</label>
85
+ <div className="relative">
86
+ <Lock className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-white/40" />
87
+ <input
88
+ type="password"
89
+ value={password}
90
+ onChange={(e) => setPassword(e.target.value)}
91
+ className="input-field pl-12"
92
+ placeholder="β€’β€’β€’β€’β€’β€’β€’β€’"
93
+ required
94
+ />
95
+ </div>
96
+ </div>
97
+
98
+ <button
99
+ type="submit"
100
+ disabled={loading}
101
+ className="w-full btn-primary py-4 flex items-center justify-center"
102
+ >
103
+ {loading ? (
104
+ <Loader2 className="w-5 h-5 animate-spin" />
105
+ ) : (
106
+ 'Sign In'
107
+ )}
108
+ </button>
109
+ </form>
110
+
111
+ <p className="mt-6 text-center text-white/60">
112
+ Don't have an account?{' '}
113
+ <Link href="/register" className="text-primary-400 hover:text-primary-300">
114
+ Sign up
115
+ </Link>
116
+ </p>
117
+ </div>
118
+ </div>
119
+ </div>
120
+ );
121
+ }
app/page.tsx ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Link from 'next/link';
2
+ import { Zap, Shield, BarChart3, Users } from 'lucide-react';
3
+
4
+ export default function HomePage() {
5
+ return (
6
+ <div className="min-h-screen">
7
+ {/* Hero Section */}
8
+ <div className="relative overflow-hidden">
9
+ {/* Animated background */}
10
+ <div className="absolute inset-0 animate-gradient-bg opacity-20" />
11
+
12
+ {/* Navigation */}
13
+ <nav className="relative z-10 flex items-center justify-between p-6 max-w-7xl mx-auto">
14
+ <div className="flex items-center space-x-2">
15
+ <div className="w-10 h-10 rounded-xl bg-gradient-to-r from-primary-500 to-accent-500 flex items-center justify-center">
16
+ <Zap className="w-6 h-6 text-white" />
17
+ </div>
18
+ <span className="text-xl font-bold gradient-text">AgentSociety</span>
19
+ </div>
20
+
21
+ <div className="flex items-center space-x-4">
22
+ <Link href="/login" className="btn-secondary">
23
+ Login
24
+ </Link>
25
+ <Link href="/register" className="btn-primary">
26
+ Get Started
27
+ </Link>
28
+ </div>
29
+ </nav>
30
+
31
+ {/* Hero Content */}
32
+ <div className="relative z-10 max-w-7xl mx-auto px-6 py-24">
33
+ <div className="text-center">
34
+ <h1 className="text-5xl md:text-7xl font-bold mb-6">
35
+ <span className="gradient-text">AI-Powered</span>
36
+ <br />
37
+ Marketing Risk Detection
38
+ </h1>
39
+
40
+ <p className="text-xl text-white/60 max-w-2xl mx-auto mb-12">
41
+ Simulate 1,000+ AI agents reacting to your advertisements.
42
+ Detect potential PR crises before they happen.
43
+ </p>
44
+
45
+ <div className="flex flex-col sm:flex-row items-center justify-center gap-4">
46
+ <Link href="/register" className="btn-primary text-lg px-8 py-4">
47
+ Try Free Simulation
48
+ </Link>
49
+ <Link href="#how-it-works" className="btn-secondary text-lg px-8 py-4">
50
+ See How It Works
51
+ </Link>
52
+ </div>
53
+ </div>
54
+
55
+ {/* Stats */}
56
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-6 mt-24">
57
+ {[
58
+ { value: '1000+', label: 'AI Agents' },
59
+ { value: '< 5min', label: 'Simulation Time' },
60
+ { value: '95%', label: 'Accuracy' },
61
+ { value: '24/7', label: 'Available' },
62
+ ].map((stat, i) => (
63
+ <div key={i} className="glass-card rounded-2xl p-6 text-center hover-lift">
64
+ <div className="text-3xl font-bold gradient-text">{stat.value}</div>
65
+ <div className="text-white/60 mt-1">{stat.label}</div>
66
+ </div>
67
+ ))}
68
+ </div>
69
+ </div>
70
+ </div>
71
+
72
+ {/* Features Section */}
73
+ <section id="how-it-works" className="py-24 max-w-7xl mx-auto px-6">
74
+ <h2 className="text-4xl font-bold text-center mb-16">
75
+ How It <span className="gradient-text">Works</span>
76
+ </h2>
77
+
78
+ <div className="grid md:grid-cols-3 gap-8">
79
+ {[
80
+ {
81
+ icon: <BarChart3 className="w-8 h-8" />,
82
+ title: 'Upload Your Ad',
83
+ description: 'Upload your video advertisement. Our AI analyzes every scene for potential triggers.',
84
+ },
85
+ {
86
+ icon: <Users className="w-8 h-8" />,
87
+ title: 'AI Simulation',
88
+ description: '1,000+ diverse AI agents react to your ad based on demographics, values, and social influence.',
89
+ },
90
+ {
91
+ icon: <Shield className="w-8 h-8" />,
92
+ title: 'Risk Report',
93
+ description: 'Get detailed risk analysis with sentiment breakdown, virality score, and controversy detection.',
94
+ },
95
+ ].map((feature, i) => (
96
+ <div key={i} className="glass-card rounded-2xl p-8 hover-lift">
97
+ <div className="w-16 h-16 rounded-xl bg-gradient-to-r from-primary-500/20 to-accent-500/20 flex items-center justify-center mb-6 text-primary-400">
98
+ {feature.icon}
99
+ </div>
100
+ <h3 className="text-xl font-semibold mb-3">{feature.title}</h3>
101
+ <p className="text-white/60">{feature.description}</p>
102
+ </div>
103
+ ))}
104
+ </div>
105
+ </section>
106
+
107
+ {/* CTA Section */}
108
+ <section className="py-24">
109
+ <div className="max-w-4xl mx-auto px-6">
110
+ <div className="glass-card rounded-3xl p-12 text-center">
111
+ <h2 className="text-3xl font-bold mb-4">
112
+ Ready to Protect Your Brand?
113
+ </h2>
114
+ <p className="text-white/60 mb-8">
115
+ Start your first simulation free. No credit card required.
116
+ </p>
117
+ <Link href="/register" className="btn-primary text-lg px-8 py-4">
118
+ Get Started Now
119
+ </Link>
120
+ </div>
121
+ </div>
122
+ </section>
123
+
124
+ {/* Footer */}
125
+ <footer className="border-t border-white/10 py-8">
126
+ <div className="max-w-7xl mx-auto px-6 text-center text-white/40">
127
+ <p>Β© 2024 AgentSociety. Powered by AI.</p>
128
+ </div>
129
+ </footer>
130
+ </div>
131
+ );
132
+ }
app/providers.tsx ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
4
+ import { useState } from 'react';
5
+
6
+ export function Providers({ children }: { children: React.ReactNode }) {
7
+ const [queryClient] = useState(
8
+ () =>
9
+ new QueryClient({
10
+ defaultOptions: {
11
+ queries: {
12
+ staleTime: 60 * 1000,
13
+ refetchOnWindowFocus: false,
14
+ },
15
+ },
16
+ })
17
+ );
18
+
19
+ return (
20
+ <QueryClientProvider client={queryClient}>
21
+ {children}
22
+ </QueryClientProvider>
23
+ );
24
+ }
app/register/page.tsx ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ import Link from 'next/link';
6
+ import { Zap, Mail, Lock, Loader2 } from 'lucide-react';
7
+ import { authApi } from '@/lib/api';
8
+
9
+ export default function RegisterPage() {
10
+ const router = useRouter();
11
+ const [email, setEmail] = useState('');
12
+ const [password, setPassword] = useState('');
13
+ const [confirmPassword, setConfirmPassword] = useState('');
14
+ const [error, setError] = useState('');
15
+ const [loading, setLoading] = useState(false);
16
+
17
+ const handleSubmit = async (e: React.FormEvent) => {
18
+ e.preventDefault();
19
+ setError('');
20
+
21
+ if (password !== confirmPassword) {
22
+ setError('Passwords do not match');
23
+ return;
24
+ }
25
+
26
+ if (password.length < 8) {
27
+ setError('Password must be at least 8 characters');
28
+ return;
29
+ }
30
+
31
+ setLoading(true);
32
+
33
+ try {
34
+ await authApi.register(email, password);
35
+ router.push('/login?registered=true');
36
+ } catch (err: any) {
37
+ setError(err.response?.data?.detail || 'Registration failed. Please try again.');
38
+ } finally {
39
+ setLoading(false);
40
+ }
41
+ };
42
+
43
+ return (
44
+ <div className="min-h-screen flex items-center justify-center px-4">
45
+ {/* Background decoration */}
46
+ <div className="absolute inset-0 overflow-hidden">
47
+ <div className="absolute -top-40 -left-40 w-80 h-80 bg-accent-500/20 rounded-full blur-3xl" />
48
+ <div className="absolute -bottom-40 -right-40 w-80 h-80 bg-primary-500/20 rounded-full blur-3xl" />
49
+ </div>
50
+
51
+ <div className="relative w-full max-w-md">
52
+ {/* Logo */}
53
+ <div className="flex items-center justify-center mb-8">
54
+ <Link href="/" className="flex items-center space-x-2">
55
+ <div className="w-12 h-12 rounded-xl bg-gradient-to-r from-primary-500 to-accent-500 flex items-center justify-center">
56
+ <Zap className="w-7 h-7 text-white" />
57
+ </div>
58
+ <span className="text-2xl font-bold gradient-text">AgentSociety</span>
59
+ </Link>
60
+ </div>
61
+
62
+ {/* Register Card */}
63
+ <div className="glass-card rounded-2xl p-8">
64
+ <h1 className="text-2xl font-bold text-center mb-2">Create Account</h1>
65
+ <p className="text-white/60 text-center mb-8">
66
+ Start simulating AI reactions today
67
+ </p>
68
+
69
+ {error && (
70
+ <div className="mb-6 p-4 rounded-xl bg-red-500/10 border border-red-500/30 text-red-400 text-sm">
71
+ {error}
72
+ </div>
73
+ )}
74
+
75
+ <form onSubmit={handleSubmit} className="space-y-6">
76
+ <div>
77
+ <label className="block text-sm font-medium mb-2">Email</label>
78
+ <div className="relative">
79
+ <Mail className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-white/40" />
80
+ <input
81
+ type="email"
82
+ value={email}
83
+ onChange={(e) => setEmail(e.target.value)}
84
+ className="input-field pl-12"
85
+ placeholder="you@example.com"
86
+ required
87
+ />
88
+ </div>
89
+ </div>
90
+
91
+ <div>
92
+ <label className="block text-sm font-medium mb-2">Password</label>
93
+ <div className="relative">
94
+ <Lock className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-white/40" />
95
+ <input
96
+ type="password"
97
+ value={password}
98
+ onChange={(e) => setPassword(e.target.value)}
99
+ className="input-field pl-12"
100
+ placeholder="β€’β€’β€’β€’β€’β€’β€’β€’"
101
+ required
102
+ />
103
+ </div>
104
+ </div>
105
+
106
+ <div>
107
+ <label className="block text-sm font-medium mb-2">Confirm Password</label>
108
+ <div className="relative">
109
+ <Lock className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-white/40" />
110
+ <input
111
+ type="password"
112
+ value={confirmPassword}
113
+ onChange={(e) => setConfirmPassword(e.target.value)}
114
+ className="input-field pl-12"
115
+ placeholder="β€’β€’β€’β€’β€’β€’β€’β€’"
116
+ required
117
+ />
118
+ </div>
119
+ </div>
120
+
121
+ <button
122
+ type="submit"
123
+ disabled={loading}
124
+ className="w-full btn-primary py-4 flex items-center justify-center"
125
+ >
126
+ {loading ? (
127
+ <Loader2 className="w-5 h-5 animate-spin" />
128
+ ) : (
129
+ 'Create Account'
130
+ )}
131
+ </button>
132
+ </form>
133
+
134
+ <p className="mt-6 text-center text-white/60">
135
+ Already have an account?{' '}
136
+ <Link href="/login" className="text-primary-400 hover:text-primary-300">
137
+ Sign in
138
+ </Link>
139
+ </p>
140
+ </div>
141
+ </div>
142
+ </div>
143
+ );
144
+ }
next-env.d.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+
4
+ // NOTE: This file should not be edited
5
+ // see https://nextjs.org/docs/basic-features/typescript for more information.
next.config.js ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ reactStrictMode: true,
4
+ // Required for the standalone Docker output used in Hugging Face Spaces
5
+ output: 'standalone',
6
+ async rewrites() {
7
+ // Use NEXT_PUBLIC_API_URL for HF Spaces / production,
8
+ // fall back to localhost for local development
9
+ const backendUrl = process.env.NEXT_PUBLIC_API_URL || 'http://127.0.0.1:8001';
10
+ return [
11
+ {
12
+ source: '/api/:path*',
13
+ destination: `${backendUrl}/:path*`,
14
+ },
15
+ ];
16
+ },
17
+ };
18
+
19
+ module.exports = nextConfig;
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "agentsociety-frontend",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "next lint"
10
+ },
11
+ "dependencies": {
12
+ "next": "14.1.0",
13
+ "react": "^18.2.0",
14
+ "react-dom": "^18.2.0",
15
+ "@tanstack/react-query": "^5.17.0",
16
+ "recharts": "^2.10.4",
17
+ "react-dropzone": "^14.2.3",
18
+ "zustand": "^4.5.0",
19
+ "axios": "^1.6.5",
20
+ "lucide-react": "^0.309.0",
21
+ "clsx": "^2.1.0"
22
+ },
23
+ "devDependencies": {
24
+ "typescript": "^5.3.3",
25
+ "@types/node": "^20.10.0",
26
+ "@types/react": "^18.2.0",
27
+ "@types/react-dom": "^18.2.0",
28
+ "autoprefixer": "^10.4.16",
29
+ "postcss": "^8.4.32",
30
+ "tailwindcss": "^3.4.0",
31
+ "eslint": "^8.56.0",
32
+ "eslint-config-next": "14.1.0"
33
+ }
34
+ }
postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ module.exports = {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ };
tailwind.config.js ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('tailwindcss').Config} */
2
+ module.exports = {
3
+ content: [
4
+ './pages/**/*.{js,ts,jsx,tsx,mdx}',
5
+ './components/**/*.{js,ts,jsx,tsx,mdx}',
6
+ './app/**/*.{js,ts,jsx,tsx,mdx}',
7
+ ],
8
+ theme: {
9
+ extend: {
10
+ colors: {
11
+ primary: {
12
+ 50: '#f0f9ff',
13
+ 100: '#e0f2fe',
14
+ 200: '#bae6fd',
15
+ 300: '#7dd3fc',
16
+ 400: '#38bdf8',
17
+ 500: '#0ea5e9',
18
+ 600: '#0284c7',
19
+ 700: '#0369a1',
20
+ 800: '#075985',
21
+ 900: '#0c4a6e',
22
+ },
23
+ accent: {
24
+ 50: '#fdf4ff',
25
+ 100: '#fae8ff',
26
+ 200: '#f5d0fe',
27
+ 300: '#f0abfc',
28
+ 400: '#e879f9',
29
+ 500: '#d946ef',
30
+ 600: '#c026d3',
31
+ 700: '#a21caf',
32
+ 800: '#86198f',
33
+ 900: '#701a75',
34
+ },
35
+ },
36
+ animation: {
37
+ 'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
38
+ 'gradient': 'gradient 8s linear infinite',
39
+ },
40
+ keyframes: {
41
+ gradient: {
42
+ '0%, 100%': {
43
+ 'background-size': '200% 200%',
44
+ 'background-position': 'left center'
45
+ },
46
+ '50%': {
47
+ 'background-size': '200% 200%',
48
+ 'background-position': 'right center'
49
+ },
50
+ },
51
+ },
52
+ },
53
+ },
54
+ plugins: [],
55
+ };
tsconfig.json ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "lib": [
4
+ "dom",
5
+ "dom.iterable",
6
+ "esnext"
7
+ ],
8
+ "allowJs": true,
9
+ "skipLibCheck": true,
10
+ "strict": true,
11
+ "noEmit": true,
12
+ "esModuleInterop": true,
13
+ "module": "esnext",
14
+ "moduleResolution": "bundler",
15
+ "resolveJsonModule": true,
16
+ "isolatedModules": true,
17
+ "jsx": "preserve",
18
+ "incremental": true,
19
+ "plugins": [
20
+ {
21
+ "name": "next"
22
+ }
23
+ ],
24
+ "paths": {
25
+ "@/*": [
26
+ "./*"
27
+ ]
28
+ }
29
+ },
30
+ "include": [
31
+ "next-env.d.ts",
32
+ "**/*.ts",
33
+ "**/*.tsx",
34
+ ".next/types/**/*.ts"
35
+ ],
36
+ "exclude": [
37
+ "node_modules"
38
+ ]
39
+ }