Upload 18 files
Browse files- .env.example +8 -0
- Dockerfile +37 -0
- app/dashboard/new/page.tsx +222 -0
- app/dashboard/page.tsx +185 -0
- app/dashboard/project/[id]/page.tsx +353 -0
- app/globals.css +154 -0
- app/layout.tsx +27 -0
- app/login/page.tsx +121 -0
- app/page.tsx +132 -0
- app/providers.tsx +24 -0
- app/register/page.tsx +144 -0
- next-env.d.ts +5 -0
- next.config.js +19 -0
- package-lock.json +0 -0
- package.json +34 -0
- postcss.config.js +6 -0
- tailwind.config.js +55 -0
- tsconfig.json +39 -0
.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 |
+
}
|