Upload 79 files
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .env.example +31 -0
- .gitattributes +3 -0
- .gitignore +18 -0
- README.md +162 -0
- client/index.html +16 -0
- client/package.json +31 -0
- client/postcss.config.js +6 -0
- client/public/INTEGRATION_GUIDE.md +73 -0
- client/src/App.tsx +50 -0
- client/src/components/AITerminal.tsx +108 -0
- client/src/components/Navbar.tsx +108 -0
- client/src/hooks/useAgentOrchestrator.ts +40 -0
- client/src/index.css +149 -0
- client/src/main.tsx +17 -0
- client/src/pages/Dashboard.tsx +250 -0
- client/src/pages/Landing.tsx +524 -0
- client/src/pages/Login.tsx +90 -0
- client/src/pages/Preview.tsx +203 -0
- client/src/pages/Privacy.tsx +42 -0
- client/src/pages/ProjectPage.tsx +118 -0
- client/src/pages/Register.tsx +125 -0
- client/src/pages/Terms.tsx +48 -0
- client/src/pages/VideoCreate.tsx +353 -0
- client/src/services/api.ts +79 -0
- client/src/store/authSlice.ts +104 -0
- client/src/store/index.ts +15 -0
- client/src/store/projectsSlice.ts +86 -0
- client/src/store/videosSlice.ts +105 -0
- client/src/vite-env.d.ts +1 -0
- client/tailwind.config.js +102 -0
- client/tsconfig.json +33 -0
- client/vite.config.ts +15 -0
- client/vite.config.ts.timestamp-1772602542886-63bad0f79f7d48.mjs +19 -0
- data/db/WiredTiger +2 -0
- data/db/collection-3791c9a5-657d-4715-bd89-75d890810f01.wt +0 -0
- data/db/collection-4ee22b20-b377-4975-a039-62c6075c60a1.wt +0 -0
- data/db/collection-f901bcc1-09cb-4c9d-a4be-8ff5da48c862.wt +0 -0
- data/db/diagnostic.data/metrics.2026-03-04T05-33-55Z-00000 +3 -0
- data/db/diagnostic.data/metrics.interim +0 -0
- data/db/index-71d13bac-060b-40af-8cda-85b71358cc48.wt +0 -0
- data/db/index-870f2506-ea92-45c5-9b87-1a2cdf2b1b98.wt +0 -0
- data/db/index-9bc70cd3-e317-4136-bf76-2c9d2a6f9fa6.wt +0 -0
- data/db/index-e4f8019a-c6d2-435b-b9fc-4a33bd33f390.wt +0 -0
- data/db/journal/WiredTigerLog.0000000001 +3 -0
- data/db/journal/WiredTigerPreplog.0000000001 +3 -0
- data/db/mongod.lock +1 -0
- data/db/sizeStorer.wt +0 -0
- data/db/storage.bson +0 -0
- our_logging/1_implementation_plan.md.resolved +107 -0
- our_logging/1_prompt.md +97 -0
.env.example
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Server
|
| 2 |
+
PORT=5000
|
| 3 |
+
NODE_ENV=development
|
| 4 |
+
|
| 5 |
+
# MongoDB
|
| 6 |
+
MONGODB_URI=mongodb://localhost:27017/directorai
|
| 7 |
+
|
| 8 |
+
# Redis
|
| 9 |
+
REDIS_URL=redis://localhost:6379
|
| 10 |
+
|
| 11 |
+
# JWT
|
| 12 |
+
JWT_SECRET=your_jwt_secret_here
|
| 13 |
+
JWT_EXPIRES_IN=7d
|
| 14 |
+
|
| 15 |
+
# Antigravity CLI
|
| 16 |
+
ANTIGRAVITY_CLI_PATH=antigravity
|
| 17 |
+
|
| 18 |
+
# Email (Nodemailer)
|
| 19 |
+
SMTP_HOST=smtp.gmail.com
|
| 20 |
+
SMTP_PORT=587
|
| 21 |
+
SMTP_USER=your_email@gmail.com
|
| 22 |
+
SMTP_PASS=your_app_password
|
| 23 |
+
|
| 24 |
+
# Stripe (payments only)
|
| 25 |
+
STRIPE_SECRET_KEY=sk_test_xxx
|
| 26 |
+
STRIPE_WEBHOOK_SECRET=whsec_xxx
|
| 27 |
+
|
| 28 |
+
# File storage
|
| 29 |
+
UPLOAD_DIR=./uploads
|
| 30 |
+
GENERATED_DIR=./generated
|
| 31 |
+
MAX_UPLOAD_SIZE=104857600
|
.gitattributes
CHANGED
|
@@ -33,3 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
data/db/diagnostic.data/metrics.2026-03-04T05-33-55Z-00000 filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
data/db/journal/WiredTigerLog.0000000001 filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
data/db/journal/WiredTigerPreplog.0000000001 filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
node_modules/
|
| 2 |
+
dist/
|
| 3 |
+
build/
|
| 4 |
+
.env
|
| 5 |
+
*.log
|
| 6 |
+
uploads/
|
| 7 |
+
generated/
|
| 8 |
+
exports/
|
| 9 |
+
.DS_Store
|
| 10 |
+
Thumbs.db
|
| 11 |
+
coverage/
|
| 12 |
+
.vite/
|
| 13 |
+
*.mjs
|
| 14 |
+
vite.config.ts.timestamp-*
|
| 15 |
+
data/
|
| 16 |
+
*.lock
|
| 17 |
+
*.wt
|
| 18 |
+
WiredTiger*
|
README.md
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Director.AI
|
| 2 |
+
|
| 3 |
+
> **Faceless video creation platform powered by real AI CLI integration.**
|
| 4 |
+
> Built with React, TypeScript, Express, MongoDB, and Socket.IO.
|
| 5 |
+
|
| 6 |
+

|
| 7 |
+

|
| 8 |
+

|
| 9 |
+

|
| 10 |
+

|
| 11 |
+

|
| 12 |
+
|
| 13 |
+
---
|
| 14 |
+
|
| 15 |
+
## Quick Setup Tutorial
|
| 16 |
+
|
| 17 |
+
Follow these steps to get Director.AI running on your local machine.
|
| 18 |
+
|
| 19 |
+
### 1. Clone the Repository
|
| 20 |
+
```bash
|
| 21 |
+
git clone https://huggingface.co/algorembrant/1st-hackaton
|
| 22 |
+
cd 1st-hackaton
|
| 23 |
+
```
|
| 24 |
+
|
| 25 |
+
### 2. Install MongoDB and Compass
|
| 26 |
+
- **Download MongoDB Community Server**: Visit the [MongoDB Download Center](https://www.mongodb.com/try/download/community) and download the MSI installer for Windows.
|
| 27 |
+
- **Install**: Run the installer and ensure "Install MongoDB as a Service" is checked.
|
| 28 |
+
- **MongoDB Compass**: During installation, check the box to install MongoDB Compass (the GUI for managing your data).
|
| 29 |
+
- **Verify**: Open MongoDB Compass and connect to `mongodb://localhost:27017`.
|
| 30 |
+
|
| 31 |
+
### 3. Environment Configuration
|
| 32 |
+
- Create a `.env` file in the root directory by copying the example:
|
| 33 |
+
```bash
|
| 34 |
+
cp .env.example .env
|
| 35 |
+
```
|
| 36 |
+
- Open `.env` and fill in the following:
|
| 37 |
+
- `MONGODB_URI`: `mongodb://localhost:27017/director-ai`
|
| 38 |
+
- `PORT`: `5000`
|
| 39 |
+
- `JWT_SECRET`: Any random long string
|
| 40 |
+
- `REDIS_URL`: `redis://localhost:6379` (If using Redis features)
|
| 41 |
+
|
| 42 |
+
or simply just tell you AI agent to configure things for you.
|
| 43 |
+
|
| 44 |
+
### 4. Install Dependencies
|
| 45 |
+
```bash
|
| 46 |
+
npm install
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
### 5. Running the Application
|
| 50 |
+
- **Server**: `cd server && npm run dev`
|
| 51 |
+
- **Client**: `cd client && npm run dev`
|
| 52 |
+
- **Both**: From the root, run `npm run dev`.
|
| 53 |
+
|
| 54 |
+
---
|
| 55 |
+
|
| 56 |
+
## Overview
|
| 57 |
+
|
| 58 |
+
Director.AI allows users to input scripts, customize voices, music, visual styles, and assets, then generate optimized faceless videos for social platforms (TikTok, Reels, Shorts, YouTube).
|
| 59 |
+
|
| 60 |
+
The application features a novel **real-time AI Terminal** that directly bridges the webapp to the **Google Antigravity CLI**, streaming output via WebSocket—no simulations, no mocks.
|
| 61 |
+
|
| 62 |
+
### Key Features
|
| 63 |
+
|
| 64 |
+
- **AI Terminal**: Real-time CLI bridge via WebSocket—type prompts in the webapp, get live output from the Antigravity CLI.
|
| 65 |
+
- **5-Step Video Wizard**: Script, Voice, Music, Style, Assets → Generate.
|
| 66 |
+
- **Multi-Platform Export**: 9:16 (TikTok/Reels/Shorts), 16:9 (YouTube), 1:1 (Instagram).
|
| 67 |
+
- **JWT Authentication**: Secure register/login with bcrypt hashing.
|
| 68 |
+
- **Luxury Dark Theme**: Gold accents, Playfair Display + Montserrat, glassmorphism, Framer Motion animations.
|
| 69 |
+
- **Style Presets**: Save and reuse brand configurations.
|
| 70 |
+
- **File Upload**: Multer with 100MB limit and file type filtering.
|
| 71 |
+
- **Admin Dashboard**: Queue monitoring and stats overview.
|
| 72 |
+
|
| 73 |
+
---
|
| 74 |
+
|
| 75 |
+
## System Architecture
|
| 76 |
+
|
| 77 |
+
```mermaid
|
| 78 |
+
graph TD
|
| 79 |
+
A["User Browser"] -->|HTTP + WebSocket| B["Express Server :5000"]
|
| 80 |
+
B -->|Mongoose| C[MongoDB]
|
| 81 |
+
B -->|Socket.IO| D["WebSocket Layer"]
|
| 82 |
+
D -->|child_process.spawn| E["Antigravity CLI"]
|
| 83 |
+
E -->|stdout/stderr stream| D
|
| 84 |
+
D -->|Real-time output| A
|
| 85 |
+
B -->|Multer| F["File Storage /uploads"]
|
| 86 |
+
B -->|"Generated"| G["/generated"]
|
| 87 |
+
|
| 88 |
+
subgraph Frontend [":5173"]
|
| 89 |
+
H["React + TypeScript"]
|
| 90 |
+
I["Redux Toolkit"]
|
| 91 |
+
J["Framer Motion"]
|
| 92 |
+
K["Tailwind CSS"]
|
| 93 |
+
L["AI Terminal Component"]
|
| 94 |
+
end
|
| 95 |
+
|
| 96 |
+
subgraph Backend [":5000"]
|
| 97 |
+
M["JWT Auth"]
|
| 98 |
+
N["REST API Routes"]
|
| 99 |
+
O["CLI Bridge Service"]
|
| 100 |
+
P["Rate Limiting + Helmet"]
|
| 101 |
+
end
|
| 102 |
+
|
| 103 |
+
A --> H
|
| 104 |
+
H --> I
|
| 105 |
+
H --> L
|
| 106 |
+
L -->|Socket.IO Client| D
|
| 107 |
+
```
|
| 108 |
+
|
| 109 |
+
---
|
| 110 |
+
|
| 111 |
+
## Project Structure
|
| 112 |
+
|
| 113 |
+
```text
|
| 114 |
+
1st-hackaton/
|
| 115 |
+
├── .env.example
|
| 116 |
+
├── .gitignore
|
| 117 |
+
├── package.json
|
| 118 |
+
├── prompt.md
|
| 119 |
+
├── README.md
|
| 120 |
+
├── server/
|
| 121 |
+
│ ├── src/
|
| 122 |
+
│ │ ├── index.ts # Main Entry
|
| 123 |
+
│ │ ├── config/ # Database & App Config
|
| 124 |
+
│ │ ├── models/ # Mongoose Schemas
|
| 125 |
+
│ │ ├── routes/ # API Endpoints
|
| 126 |
+
│ │ ├── services/ # Business Logic
|
| 127 |
+
│ │ └── utils/ # Helpers
|
| 128 |
+
│ └── tsconfig.json
|
| 129 |
+
└── client/
|
| 130 |
+
├── src/
|
| 131 |
+
│ ├── components/ # UI Building Blocks
|
| 132 |
+
│ ├── pages/ # Main Views
|
| 133 |
+
│ ├── services/ # API Integration
|
| 134 |
+
│ └── store/ # Redux State Management
|
| 135 |
+
├── vite.config.ts
|
| 136 |
+
└── tailwind.config.js
|
| 137 |
+
```
|
| 138 |
+
|
| 139 |
+
---
|
| 140 |
+
|
| 141 |
+
## Hugging Face Deployment
|
| 142 |
+
|
| 143 |
+
This project is optimized for deployment on Hugging Face.
|
| 144 |
+
|
| 145 |
+
- **Repository**: [https://huggingface.co/algorembrant/1st-hackaton](https://huggingface.co/algorembrant/1st-hackaton)
|
| 146 |
+
- **Author Profile**: [https://huggingface.co/algorembrant](https://huggingface.co/algorembrant)
|
| 147 |
+
|
| 148 |
+
> [!IMPORTANT]
|
| 149 |
+
> Large assets or binary files should be handled via **Xet Storage** or **Git LFS** to ensure optimal performance on Hugging Face.
|
| 150 |
+
|
| 151 |
+
---
|
| 152 |
+
|
| 153 |
+
## Author
|
| 154 |
+
|
| 155 |
+
**Rembrant Oyangoren Albeos**
|
| 156 |
+
- Hugging Face: [@algorembrant](https://huggingface.co/algorembrant)
|
| 157 |
+
|
| 158 |
+
---
|
| 159 |
+
|
| 160 |
+
## License
|
| 161 |
+
|
| 162 |
+
MIT License | © 2026 **Rembrant Oyangoren Albeos**
|
client/index.html
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<meta name="description" content="Director.AI -- Turn your scripts into ready-to-post faceless videos. AI-powered video creation platform." />
|
| 7 |
+
<title>Director.AI -- Faceless Video Creation</title>
|
| 8 |
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
| 9 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
| 10 |
+
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700&family=Playfair+Display:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
|
| 11 |
+
</head>
|
| 12 |
+
<body class="bg-dark-900 text-light-300 font-body antialiased">
|
| 13 |
+
<div id="root"></div>
|
| 14 |
+
<script type="module" src="/src/main.tsx"></script>
|
| 15 |
+
</body>
|
| 16 |
+
</html>
|
client/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "director-ai-client",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "1.0.0",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite",
|
| 8 |
+
"build": "tsc -b && vite build",
|
| 9 |
+
"preview": "vite preview"
|
| 10 |
+
},
|
| 11 |
+
"dependencies": {
|
| 12 |
+
"@reduxjs/toolkit": "^2.1.0",
|
| 13 |
+
"axios": "^1.6.5",
|
| 14 |
+
"framer-motion": "^11.0.3",
|
| 15 |
+
"react": "^18.3.1",
|
| 16 |
+
"react-dom": "^18.3.1",
|
| 17 |
+
"react-redux": "^9.1.0",
|
| 18 |
+
"react-router-dom": "^6.22.0",
|
| 19 |
+
"socket.io-client": "^4.7.4"
|
| 20 |
+
},
|
| 21 |
+
"devDependencies": {
|
| 22 |
+
"@types/react": "^18.3.1",
|
| 23 |
+
"@types/react-dom": "^18.3.1",
|
| 24 |
+
"@vitejs/plugin-react": "^4.2.1",
|
| 25 |
+
"autoprefixer": "^10.4.17",
|
| 26 |
+
"postcss": "^8.4.34",
|
| 27 |
+
"tailwindcss": "^3.4.1",
|
| 28 |
+
"typescript": "^5.3.3",
|
| 29 |
+
"vite": "^5.1.0"
|
| 30 |
+
}
|
| 31 |
+
}
|
client/postcss.config.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export default {
|
| 2 |
+
plugins: {
|
| 3 |
+
tailwindcss: {},
|
| 4 |
+
autoprefixer: {},
|
| 5 |
+
},
|
| 6 |
+
};
|
client/public/INTEGRATION_GUIDE.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Director.AI - Integration Guide (Workspace MCP)
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
This document explains how the **Google Antigravity CLI** (or any autonomous agent script) should connect to the Director.AI webapp environment to control it autonomously.
|
| 5 |
+
|
| 6 |
+
Unlike a standard web app where a human clicks buttons, **Director.AI acts as a Host Workspace**. The AI Agent connects as a client and can read state, create projects, and force the human's browser viewport to navigate to specific pages so the human can watch the AI work in real time.
|
| 7 |
+
|
| 8 |
+
## Connection Details
|
| 9 |
+
|
| 10 |
+
- **Protocol**: WebSocket (Socket.IO v4)
|
| 11 |
+
- **Host**: `ws://localhost:5000/mcp`
|
| 12 |
+
- **Method**: The agent must connect using a standard Socket.IO client pointing to the `/mcp` namespace.
|
| 13 |
+
|
| 14 |
+
### Sample Node.js Agent Code
|
| 15 |
+
If you are writing an agent to control this workspace, here is how you connect:
|
| 16 |
+
|
| 17 |
+
```javascript
|
| 18 |
+
import { io } from 'socket.io-client';
|
| 19 |
+
|
| 20 |
+
const socket = io('http://localhost:5000/mcp');
|
| 21 |
+
|
| 22 |
+
socket.on('connect', () => {
|
| 23 |
+
console.log('Agent connected to Workspace Hub!');
|
| 24 |
+
|
| 25 |
+
// Example: Read current projects
|
| 26 |
+
socket.emit('mcp:read_state', (response) => {
|
| 27 |
+
console.log('Current projects:', response.data.projects);
|
| 28 |
+
});
|
| 29 |
+
});
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
## Available Tools (Socket.IO Events)
|
| 33 |
+
|
| 34 |
+
The following tools are exposed by the MCP server for the agent to use:
|
| 35 |
+
|
| 36 |
+
### 1. `mcp:read_state`
|
| 37 |
+
Reads the current high-level state of the application.
|
| 38 |
+
- **Payload**: None required.
|
| 39 |
+
- **Callback Returns**: `{ status: 'success', data: { projects: Project[] } }`
|
| 40 |
+
|
| 41 |
+
### 2. `mcp:navigate`
|
| 42 |
+
Forces all connected human browsers to instantly navigate to a specific React Router path. Use this to guide the user's attention.
|
| 43 |
+
- **Payload**: `{ path: string }`
|
| 44 |
+
- Example: `{ path: '/dashboard' }` or `{ path: '/project/12345/create' }`
|
| 45 |
+
- **Callback Returns**: `{ status: 'success' }`
|
| 46 |
+
|
| 47 |
+
### 3. `mcp:create_project`
|
| 48 |
+
Autonomously creates a new project in the database. The webapp will automatically refresh its project list, and force navigation to the newly created project's URL.
|
| 49 |
+
- **Payload**: `{ name: string, defaultPlatform: 'TikTok'|'YouTube', defaultFormat: '9:16'|'16:9' }`
|
| 50 |
+
- **Callback Returns**: `{ status: 'success', data: Project }`
|
| 51 |
+
|
| 52 |
+
### 4. `mcp:update_video_draft`
|
| 53 |
+
Used when the agent is iteratively drafting a video script. It streams updates directly to the human's screen.
|
| 54 |
+
- **Payload**: `{ projectId: string, script?: string, voiceType?: string }`
|
| 55 |
+
- **Callback Returns**: `{ status: 'success' }`
|
| 56 |
+
|
| 57 |
+
### 5. `mcp:activity_log`
|
| 58 |
+
Sends a log message to the "Agent Activity Log" panel visible on the human's screen. Use this constantly to tell the human what you are doing (e.g. "I am researching competitors to write a better script...").
|
| 59 |
+
- **Payload**: `{ message: string, type: 'info' | 'success' | 'warning' | 'error' }`
|
| 60 |
+
- **Callback Returns**: None
|
| 61 |
+
|
| 62 |
+
## Example Workflow for the Agent
|
| 63 |
+
|
| 64 |
+
1. **Connect**: Agent connects to `/mcp`.
|
| 65 |
+
2. **Log**: emit `mcp:activity_log` -> "Agent initialized. Analyzing workspace..."
|
| 66 |
+
3. **Navigate**: emit `mcp:navigate` (`{ path: '/dashboard' }`) -> Forces human browser to dashboard.
|
| 67 |
+
4. **Create**: emit `mcp:create_project` (`{ name: 'Viral Story', defaultPlatform: 'TikTok', defaultFormat: '9:16' }`).
|
| 68 |
+
5. **Log**: emit `mcp:activity_log` -> "Project created. Script drafting initiated."
|
| 69 |
+
6. **Navigate**: emit `mcp:navigate` -> Agent navigates browser to `/project/[id]/create` so human sees the creation wizard.
|
| 70 |
+
7. **Complete**: Agent continues to build the video via tools.
|
| 71 |
+
|
| 72 |
+
## Development Note for Antigravity Engine
|
| 73 |
+
If you are running the `antigravity` CLI, you will need to map these Socket.IO standard emissions into your internal tool calling schema. The webapp is completely passive and trusts the AI to orchestrate the flow.
|
client/src/App.tsx
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Routes, Route, Navigate } from 'react-router-dom';
|
| 2 |
+
import { useSelector } from 'react-redux';
|
| 3 |
+
import { RootState } from './store';
|
| 4 |
+
import { useAgentOrchestrator } from './hooks/useAgentOrchestrator';
|
| 5 |
+
import Navbar from './components/Navbar';
|
| 6 |
+
import Landing from './pages/Landing';
|
| 7 |
+
import Login from './pages/Login';
|
| 8 |
+
import Register from './pages/Register';
|
| 9 |
+
import Dashboard from './pages/Dashboard';
|
| 10 |
+
import ProjectPage from './pages/ProjectPage';
|
| 11 |
+
import VideoCreate from './pages/VideoCreate';
|
| 12 |
+
import Preview from './pages/Preview';
|
| 13 |
+
import Privacy from './pages/Privacy';
|
| 14 |
+
import Terms from './pages/Terms';
|
| 15 |
+
|
| 16 |
+
function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
| 17 |
+
const { token } = useSelector((state: RootState) => state.auth);
|
| 18 |
+
if (!token) return <Navigate to="/login" replace />;
|
| 19 |
+
return <>{children}</>;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
export default function App() {
|
| 23 |
+
// Listen to operations coming from the AI Agent connecting to our MCP Host
|
| 24 |
+
useAgentOrchestrator();
|
| 25 |
+
|
| 26 |
+
return (
|
| 27 |
+
<div className="min-h-screen bg-dark-900">
|
| 28 |
+
<Navbar />
|
| 29 |
+
<Routes>
|
| 30 |
+
<Route path="/" element={<Landing />} />
|
| 31 |
+
<Route path="/login" element={<Login />} />
|
| 32 |
+
<Route path="/register" element={<Register />} />
|
| 33 |
+
<Route path="/privacy" element={<Privacy />} />
|
| 34 |
+
<Route path="/terms" element={<Terms />} />
|
| 35 |
+
<Route path="/dashboard" element={
|
| 36 |
+
<ProtectedRoute><Dashboard /></ProtectedRoute>
|
| 37 |
+
} />
|
| 38 |
+
<Route path="/project/:id" element={
|
| 39 |
+
<ProtectedRoute><ProjectPage /></ProtectedRoute>
|
| 40 |
+
} />
|
| 41 |
+
<Route path="/project/:projectId/create" element={
|
| 42 |
+
<ProtectedRoute><VideoCreate /></ProtectedRoute>
|
| 43 |
+
} />
|
| 44 |
+
<Route path="/video/:id/preview" element={
|
| 45 |
+
<ProtectedRoute><Preview /></ProtectedRoute>
|
| 46 |
+
} />
|
| 47 |
+
</Routes>
|
| 48 |
+
</div>
|
| 49 |
+
);
|
| 50 |
+
}
|
client/src/components/AITerminal.tsx
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useRef, useEffect } from 'react';
|
| 2 |
+
import { useSelector } from 'react-redux';
|
| 3 |
+
import { io, Socket } from 'socket.io-client';
|
| 4 |
+
import { motion } from 'framer-motion';
|
| 5 |
+
import { RootState } from '../store';
|
| 6 |
+
|
| 7 |
+
interface LogEntry {
|
| 8 |
+
type: 'info' | 'success' | 'warning' | 'error' | 'system';
|
| 9 |
+
message: string;
|
| 10 |
+
timestamp: string;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
let socket: Socket | null = null;
|
| 14 |
+
|
| 15 |
+
export default function AITerminal() {
|
| 16 |
+
const { token } = useSelector((state: RootState) => state.auth);
|
| 17 |
+
const [logs, setLogs] = useState<LogEntry[]>([
|
| 18 |
+
{
|
| 19 |
+
type: 'system',
|
| 20 |
+
message: 'Workspace Activity Monitor',
|
| 21 |
+
timestamp: new Date().toLocaleTimeString(),
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
type: 'info',
|
| 25 |
+
message: 'Waiting for Google Antigravity CLI to connect and perform actions...',
|
| 26 |
+
timestamp: new Date().toLocaleTimeString(),
|
| 27 |
+
},
|
| 28 |
+
]);
|
| 29 |
+
const bottomRef = useRef<HTMLDivElement>(null);
|
| 30 |
+
|
| 31 |
+
useEffect(() => {
|
| 32 |
+
if (!token) return;
|
| 33 |
+
|
| 34 |
+
// Connect as a Browser viewport
|
| 35 |
+
socket = io('http://localhost:5000/browser', {
|
| 36 |
+
auth: { token },
|
| 37 |
+
});
|
| 38 |
+
|
| 39 |
+
socket.on('connect', () => {
|
| 40 |
+
addLog('system', 'Browser viewport connected to Workspace Hub');
|
| 41 |
+
});
|
| 42 |
+
|
| 43 |
+
socket.on('disconnect', () => {
|
| 44 |
+
addLog('error', 'Disconnected from Workspace Hub');
|
| 45 |
+
});
|
| 46 |
+
|
| 47 |
+
socket.on('agent:activity_log', (data: { message: string, type: 'info' | 'success' | 'warning' | 'error', timestamp: string }) => {
|
| 48 |
+
setLogs(prev => [...prev, {
|
| 49 |
+
type: data.type,
|
| 50 |
+
message: data.message,
|
| 51 |
+
timestamp: new Date(data.timestamp).toLocaleTimeString(),
|
| 52 |
+
}]);
|
| 53 |
+
});
|
| 54 |
+
|
| 55 |
+
return () => {
|
| 56 |
+
socket?.disconnect();
|
| 57 |
+
socket = null;
|
| 58 |
+
};
|
| 59 |
+
}, [token]);
|
| 60 |
+
|
| 61 |
+
useEffect(() => {
|
| 62 |
+
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
|
| 63 |
+
}, [logs]);
|
| 64 |
+
|
| 65 |
+
function addLog(type: LogEntry['type'], message: string) {
|
| 66 |
+
setLogs((prev) => [...prev, {
|
| 67 |
+
type,
|
| 68 |
+
message,
|
| 69 |
+
timestamp: new Date().toLocaleTimeString(),
|
| 70 |
+
}]);
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
function getTextColor(type: LogEntry['type']) {
|
| 74 |
+
switch (type) {
|
| 75 |
+
case 'info': return 'text-light-400';
|
| 76 |
+
case 'success': return 'text-green-400';
|
| 77 |
+
case 'warning': return 'text-gold-400';
|
| 78 |
+
case 'error': return 'text-red-400';
|
| 79 |
+
case 'system': return 'text-blue-400';
|
| 80 |
+
default: return 'text-light-400';
|
| 81 |
+
}
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
return (
|
| 85 |
+
<motion.div
|
| 86 |
+
initial={{ opacity: 0 }}
|
| 87 |
+
animate={{ opacity: 1 }}
|
| 88 |
+
className="terminal-bg overflow-hidden border border-dark-400/30 rounded-xl"
|
| 89 |
+
>
|
| 90 |
+
<div className="flex items-center justify-between px-4 py-2 bg-dark-800 border-b border-dark-400/30">
|
| 91 |
+
<div className="flex items-center gap-2">
|
| 92 |
+
<div className="w-2 h-2 rounded-full bg-blue-500 animate-pulse" />
|
| 93 |
+
<span className="text-xs text-light-500 font-mono tracking-wider">Agent Activity Log</span>
|
| 94 |
+
</div>
|
| 95 |
+
</div>
|
| 96 |
+
|
| 97 |
+
<div className="h-48 overflow-y-auto px-4 py-3 space-y-1">
|
| 98 |
+
{logs.map((log, i) => (
|
| 99 |
+
<div key={i} className={`font-mono text-xs leading-relaxed ${getTextColor(log.type)}`}>
|
| 100 |
+
<span className="text-dark-400 mr-2 select-none">[{log.timestamp}]</span>
|
| 101 |
+
<span className="break-words">{log.message}</span>
|
| 102 |
+
</div>
|
| 103 |
+
))}
|
| 104 |
+
<div ref={bottomRef} />
|
| 105 |
+
</div>
|
| 106 |
+
</motion.div>
|
| 107 |
+
);
|
| 108 |
+
}
|
client/src/components/Navbar.tsx
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState } from 'react';
|
| 2 |
+
import { Link, useNavigate } from 'react-router-dom';
|
| 3 |
+
import { useSelector, useDispatch } from 'react-redux';
|
| 4 |
+
import { motion, AnimatePresence } from 'framer-motion';
|
| 5 |
+
import { RootState, AppDispatch } from '../store';
|
| 6 |
+
import { logout } from '../store/authSlice';
|
| 7 |
+
|
| 8 |
+
export default function Navbar() {
|
| 9 |
+
const { user, token } = useSelector((state: RootState) => state.auth);
|
| 10 |
+
const dispatch = useDispatch<AppDispatch>();
|
| 11 |
+
const navigate = useNavigate();
|
| 12 |
+
const [menuOpen, setMenuOpen] = useState(false);
|
| 13 |
+
|
| 14 |
+
const handleLogout = () => {
|
| 15 |
+
dispatch(logout());
|
| 16 |
+
navigate('/');
|
| 17 |
+
};
|
| 18 |
+
|
| 19 |
+
return (
|
| 20 |
+
<nav className="fixed top-0 left-0 right-0 z-50 bg-dark-900/80 backdrop-blur-xl border-b border-dark-400/20">
|
| 21 |
+
<div className="max-w-7xl mx-auto px-6 h-16 flex items-center justify-between">
|
| 22 |
+
{/* Logo */}
|
| 23 |
+
<Link to="/" className="flex items-center gap-2 group">
|
| 24 |
+
<div className="w-8 h-8 bg-gold-gradient rounded-lg flex items-center justify-center
|
| 25 |
+
group-hover:shadow-gold transition-shadow duration-300">
|
| 26 |
+
<span className="text-dark-900 font-display font-bold text-sm">D</span>
|
| 27 |
+
</div>
|
| 28 |
+
<span className="font-display text-xl font-bold text-light-100">
|
| 29 |
+
Director<span className="text-gold-500">.AI</span>
|
| 30 |
+
</span>
|
| 31 |
+
</Link>
|
| 32 |
+
|
| 33 |
+
{/* Desktop Navigation */}
|
| 34 |
+
<div className="hidden md:flex items-center gap-1">
|
| 35 |
+
{token ? (
|
| 36 |
+
<>
|
| 37 |
+
<Link to="/dashboard" className="btn-ghost text-sm">Dashboard</Link>
|
| 38 |
+
<div className="w-px h-6 bg-dark-400/30 mx-2" />
|
| 39 |
+
<span className="text-sm text-light-500 mr-3">{user?.email}</span>
|
| 40 |
+
<button onClick={handleLogout} className="btn-ghost text-sm text-light-500 hover:text-red-400">
|
| 41 |
+
Sign Out
|
| 42 |
+
</button>
|
| 43 |
+
</>
|
| 44 |
+
) : (
|
| 45 |
+
<>
|
| 46 |
+
<a href="#features" className="btn-ghost text-sm">Features</a>
|
| 47 |
+
<a href="#pricing" className="btn-ghost text-sm">Pricing</a>
|
| 48 |
+
<a href="#faq" className="btn-ghost text-sm">FAQ</a>
|
| 49 |
+
<div className="w-px h-6 bg-dark-400/30 mx-2" />
|
| 50 |
+
<Link to="/login" className="btn-ghost text-sm">Sign In</Link>
|
| 51 |
+
<Link to="/register" className="btn-primary text-sm ml-2">Get Started</Link>
|
| 52 |
+
</>
|
| 53 |
+
)}
|
| 54 |
+
</div>
|
| 55 |
+
|
| 56 |
+
{/* Mobile Menu Button */}
|
| 57 |
+
<button
|
| 58 |
+
onClick={() => setMenuOpen(!menuOpen)}
|
| 59 |
+
className="md:hidden flex flex-col gap-1.5 p-2"
|
| 60 |
+
aria-label="Toggle Menu"
|
| 61 |
+
id="mobile-menu-toggle"
|
| 62 |
+
>
|
| 63 |
+
<motion.span
|
| 64 |
+
animate={menuOpen ? { rotate: 45, y: 6 } : { rotate: 0, y: 0 }}
|
| 65 |
+
className="w-5 h-0.5 bg-light-300 block"
|
| 66 |
+
/>
|
| 67 |
+
<motion.span
|
| 68 |
+
animate={menuOpen ? { opacity: 0 } : { opacity: 1 }}
|
| 69 |
+
className="w-5 h-0.5 bg-light-300 block"
|
| 70 |
+
/>
|
| 71 |
+
<motion.span
|
| 72 |
+
animate={menuOpen ? { rotate: -45, y: -6 } : { rotate: 0, y: 0 }}
|
| 73 |
+
className="w-5 h-0.5 bg-light-300 block"
|
| 74 |
+
/>
|
| 75 |
+
</button>
|
| 76 |
+
</div>
|
| 77 |
+
|
| 78 |
+
{/* Mobile Menu */}
|
| 79 |
+
<AnimatePresence>
|
| 80 |
+
{menuOpen && (
|
| 81 |
+
<motion.div
|
| 82 |
+
initial={{ height: 0, opacity: 0 }}
|
| 83 |
+
animate={{ height: 'auto', opacity: 1 }}
|
| 84 |
+
exit={{ height: 0, opacity: 0 }}
|
| 85 |
+
transition={{ duration: 0.3 }}
|
| 86 |
+
className="md:hidden overflow-hidden bg-dark-800/95 backdrop-blur-xl border-b border-dark-400/20"
|
| 87 |
+
>
|
| 88 |
+
<div className="px-6 py-4 flex flex-col gap-2">
|
| 89 |
+
{token ? (
|
| 90 |
+
<>
|
| 91 |
+
<Link to="/dashboard" onClick={() => setMenuOpen(false)} className="btn-ghost text-sm justify-start">Dashboard</Link>
|
| 92 |
+
<button onClick={() => { handleLogout(); setMenuOpen(false); }} className="btn-ghost text-sm justify-start text-red-400">Sign Out</button>
|
| 93 |
+
</>
|
| 94 |
+
) : (
|
| 95 |
+
<>
|
| 96 |
+
<a href="#features" onClick={() => setMenuOpen(false)} className="btn-ghost text-sm justify-start">Features</a>
|
| 97 |
+
<a href="#pricing" onClick={() => setMenuOpen(false)} className="btn-ghost text-sm justify-start">Pricing</a>
|
| 98 |
+
<Link to="/login" onClick={() => setMenuOpen(false)} className="btn-ghost text-sm justify-start">Sign In</Link>
|
| 99 |
+
<Link to="/register" onClick={() => setMenuOpen(false)} className="btn-primary text-sm mt-2">Get Started</Link>
|
| 100 |
+
</>
|
| 101 |
+
)}
|
| 102 |
+
</div>
|
| 103 |
+
</motion.div>
|
| 104 |
+
)}
|
| 105 |
+
</AnimatePresence>
|
| 106 |
+
</nav>
|
| 107 |
+
);
|
| 108 |
+
}
|
client/src/hooks/useAgentOrchestrator.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect } from 'react';
|
| 2 |
+
import { useNavigate } from 'react-router-dom';
|
| 3 |
+
import { useSelector, useDispatch } from 'react-redux';
|
| 4 |
+
import { io } from 'socket.io-client';
|
| 5 |
+
import { RootState, AppDispatch } from '../store';
|
| 6 |
+
import { fetchProjects } from '../store/projectsSlice';
|
| 7 |
+
|
| 8 |
+
export function useAgentOrchestrator() {
|
| 9 |
+
const navigate = useNavigate();
|
| 10 |
+
const dispatch = useDispatch<AppDispatch>();
|
| 11 |
+
const { token } = useSelector((state: RootState) => state.auth);
|
| 12 |
+
|
| 13 |
+
useEffect(() => {
|
| 14 |
+
if (!token) return;
|
| 15 |
+
|
| 16 |
+
// The browser connects to the /browser namespace to listen to the agent
|
| 17 |
+
const socket = io('http://localhost:5000/browser', {
|
| 18 |
+
auth: { token },
|
| 19 |
+
});
|
| 20 |
+
|
| 21 |
+
// When the CLI agent wants to guide the user's view:
|
| 22 |
+
socket.on('agent:navigate', (path: string) => {
|
| 23 |
+
console.log(`[Agent Orchestrator] Navigating to ${path}`);
|
| 24 |
+
navigate(path);
|
| 25 |
+
});
|
| 26 |
+
|
| 27 |
+
// When the CLI agent creates a project, we should refresh our state so the UI updates
|
| 28 |
+
socket.on('agent:project_created', () => {
|
| 29 |
+
console.log(`[Agent Orchestrator] Agent created a project, refreshing list`);
|
| 30 |
+
dispatch(fetchProjects());
|
| 31 |
+
});
|
| 32 |
+
|
| 33 |
+
// You could add logic here for 'agent:video_draft_updated'
|
| 34 |
+
// to fill out the form fields in VideoCreate automatically.
|
| 35 |
+
|
| 36 |
+
return () => {
|
| 37 |
+
socket.disconnect();
|
| 38 |
+
};
|
| 39 |
+
}, [token, navigate, dispatch]);
|
| 40 |
+
}
|
client/src/index.css
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@tailwind base;
|
| 2 |
+
@tailwind components;
|
| 3 |
+
@tailwind utilities;
|
| 4 |
+
|
| 5 |
+
@layer base {
|
| 6 |
+
* {
|
| 7 |
+
margin: 0;
|
| 8 |
+
padding: 0;
|
| 9 |
+
box-sizing: border-box;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
html {
|
| 13 |
+
scroll-behavior: smooth;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
body {
|
| 17 |
+
background-color: #0A0A0A;
|
| 18 |
+
color: #F5F5F5;
|
| 19 |
+
font-family: 'Montserrat', sans-serif;
|
| 20 |
+
-webkit-font-smoothing: antialiased;
|
| 21 |
+
-moz-osx-font-smoothing: grayscale;
|
| 22 |
+
overflow-x: hidden;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
::selection {
|
| 26 |
+
background-color: rgba(212, 175, 55, 0.3);
|
| 27 |
+
color: #F5F5F5;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
::-webkit-scrollbar {
|
| 31 |
+
width: 6px;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
::-webkit-scrollbar-track {
|
| 35 |
+
background: #111111;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
::-webkit-scrollbar-thumb {
|
| 39 |
+
background: #333333;
|
| 40 |
+
border-radius: 3px;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
::-webkit-scrollbar-thumb:hover {
|
| 44 |
+
background: #D4AF37;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
h1, h2, h3, h4, h5, h6 {
|
| 48 |
+
font-family: 'Playfair Display', serif;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
a {
|
| 52 |
+
color: inherit;
|
| 53 |
+
text-decoration: none;
|
| 54 |
+
transition: color 0.3s ease;
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
@layer components {
|
| 59 |
+
.btn-primary {
|
| 60 |
+
@apply inline-flex items-center justify-center px-8 py-3
|
| 61 |
+
bg-gold-500 text-dark-900 font-body font-semibold
|
| 62 |
+
rounded-lg transition-all duration-300 ease-out
|
| 63 |
+
hover:bg-gold-400 hover:shadow-gold-lg hover:-translate-y-0.5
|
| 64 |
+
active:translate-y-0 active:shadow-gold
|
| 65 |
+
disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:translate-y-0;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
.btn-secondary {
|
| 69 |
+
@apply inline-flex items-center justify-center px-8 py-3
|
| 70 |
+
border border-gold-500/40 text-gold-500 font-body font-medium
|
| 71 |
+
rounded-lg transition-all duration-300 ease-out
|
| 72 |
+
hover:border-gold-500 hover:bg-gold-500/10 hover:shadow-gold
|
| 73 |
+
active:bg-gold-500/20;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
.btn-ghost {
|
| 77 |
+
@apply inline-flex items-center justify-center px-6 py-2
|
| 78 |
+
text-light-400 font-body font-medium
|
| 79 |
+
rounded-lg transition-all duration-300 ease-out
|
| 80 |
+
hover:text-gold-500 hover:bg-dark-500/50;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
.card {
|
| 84 |
+
@apply bg-card-gradient backdrop-blur-sm border border-dark-400/30
|
| 85 |
+
rounded-2xl p-6 transition-all duration-500 ease-out
|
| 86 |
+
hover:border-gold-500/20 hover:shadow-card-hover hover:-translate-y-1;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
.card-static {
|
| 90 |
+
@apply bg-card-gradient backdrop-blur-sm border border-dark-400/30
|
| 91 |
+
rounded-2xl p-6;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
.input-field {
|
| 95 |
+
@apply w-full px-4 py-3 bg-dark-600 border border-dark-400/50
|
| 96 |
+
rounded-lg text-light-300 font-body placeholder-light-500/40
|
| 97 |
+
transition-all duration-300 ease-out
|
| 98 |
+
focus:outline-none focus:border-gold-500/60 focus:shadow-gold
|
| 99 |
+
hover:border-dark-300;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
.section-heading {
|
| 103 |
+
@apply font-display text-4xl md:text-5xl font-bold text-light-100 mb-4;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
.section-subheading {
|
| 107 |
+
@apply font-body text-lg text-light-500 max-w-2xl mx-auto;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
.gold-text {
|
| 111 |
+
@apply text-transparent bg-clip-text bg-gold-gradient;
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
.glass-panel {
|
| 115 |
+
@apply bg-dark-700/60 backdrop-blur-xl border border-dark-400/20
|
| 116 |
+
rounded-2xl shadow-card;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
.terminal-bg {
|
| 120 |
+
@apply bg-dark-900 border border-dark-400/30 rounded-xl
|
| 121 |
+
font-mono text-sm;
|
| 122 |
+
}
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
@layer utilities {
|
| 126 |
+
.text-balance {
|
| 127 |
+
text-wrap: balance;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
.animate-delay-100 {
|
| 131 |
+
animation-delay: 100ms;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
.animate-delay-200 {
|
| 135 |
+
animation-delay: 200ms;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
.animate-delay-300 {
|
| 139 |
+
animation-delay: 300ms;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
.animate-delay-400 {
|
| 143 |
+
animation-delay: 400ms;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.animate-delay-500 {
|
| 147 |
+
animation-delay: 500ms;
|
| 148 |
+
}
|
| 149 |
+
}
|
client/src/main.tsx
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import ReactDOM from 'react-dom/client';
|
| 3 |
+
import { Provider } from 'react-redux';
|
| 4 |
+
import { BrowserRouter } from 'react-router-dom';
|
| 5 |
+
import App from './App';
|
| 6 |
+
import { store } from './store';
|
| 7 |
+
import './index.css';
|
| 8 |
+
|
| 9 |
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
| 10 |
+
<React.StrictMode>
|
| 11 |
+
<Provider store={store}>
|
| 12 |
+
<BrowserRouter>
|
| 13 |
+
<App />
|
| 14 |
+
</BrowserRouter>
|
| 15 |
+
</Provider>
|
| 16 |
+
</React.StrictMode>
|
| 17 |
+
);
|
client/src/pages/Dashboard.tsx
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect, useState, FormEvent } from 'react';
|
| 2 |
+
import { useDispatch, useSelector } from 'react-redux';
|
| 3 |
+
import { Link } from 'react-router-dom';
|
| 4 |
+
import { motion, AnimatePresence } from 'framer-motion';
|
| 5 |
+
import { AppDispatch, RootState } from '../store';
|
| 6 |
+
import { fetchProjects, createProject } from '../store/projectsSlice';
|
| 7 |
+
import AITerminal from '../components/AITerminal';
|
| 8 |
+
|
| 9 |
+
export default function Dashboard() {
|
| 10 |
+
const dispatch = useDispatch<AppDispatch>();
|
| 11 |
+
const { user } = useSelector((state: RootState) => state.auth);
|
| 12 |
+
const { items: projects, loading } = useSelector((state: RootState) => state.projects);
|
| 13 |
+
const [showCreateModal, setShowCreateModal] = useState(false);
|
| 14 |
+
const [showTerminal, setShowTerminal] = useState(false);
|
| 15 |
+
|
| 16 |
+
useEffect(() => {
|
| 17 |
+
dispatch(fetchProjects());
|
| 18 |
+
}, [dispatch]);
|
| 19 |
+
|
| 20 |
+
return (
|
| 21 |
+
<div className="min-h-screen pt-20 pb-12 px-6">
|
| 22 |
+
<div className="max-w-6xl mx-auto">
|
| 23 |
+
{/* Welcome Header */}
|
| 24 |
+
<motion.div
|
| 25 |
+
initial={{ opacity: 0, y: 20 }}
|
| 26 |
+
animate={{ opacity: 1, y: 0 }}
|
| 27 |
+
className="mb-10"
|
| 28 |
+
>
|
| 29 |
+
<h1 className="font-display text-3xl md:text-4xl font-bold text-light-100 mb-2">
|
| 30 |
+
Welcome back{user?.email ? `, ${user.email.split('@')[0]}` : ''}
|
| 31 |
+
</h1>
|
| 32 |
+
<p className="font-body text-light-500">
|
| 33 |
+
Manage your projects and create new videos
|
| 34 |
+
</p>
|
| 35 |
+
</motion.div>
|
| 36 |
+
|
| 37 |
+
{/* Quick Actions */}
|
| 38 |
+
<motion.div
|
| 39 |
+
initial={{ opacity: 0, y: 20 }}
|
| 40 |
+
animate={{ opacity: 1, y: 0 }}
|
| 41 |
+
transition={{ delay: 0.1 }}
|
| 42 |
+
className="flex flex-wrap gap-4 mb-10"
|
| 43 |
+
>
|
| 44 |
+
<button
|
| 45 |
+
onClick={() => setShowCreateModal(true)}
|
| 46 |
+
className="btn-primary"
|
| 47 |
+
id="create-project-btn"
|
| 48 |
+
>
|
| 49 |
+
<span className="mr-2 text-xl leading-none">+</span>
|
| 50 |
+
Create New Project
|
| 51 |
+
</button>
|
| 52 |
+
<button
|
| 53 |
+
onClick={() => setShowTerminal(!showTerminal)}
|
| 54 |
+
className="btn-secondary"
|
| 55 |
+
id="toggle-terminal-btn"
|
| 56 |
+
>
|
| 57 |
+
<span className="mr-2 font-mono text-sm">>_</span>
|
| 58 |
+
{showTerminal ? 'Hide' : 'Open'} AI Terminal
|
| 59 |
+
</button>
|
| 60 |
+
</motion.div>
|
| 61 |
+
|
| 62 |
+
{/* AI Terminal */}
|
| 63 |
+
<AnimatePresence>
|
| 64 |
+
{showTerminal && (
|
| 65 |
+
<motion.div
|
| 66 |
+
initial={{ opacity: 0, height: 0 }}
|
| 67 |
+
animate={{ opacity: 1, height: 'auto' }}
|
| 68 |
+
exit={{ opacity: 0, height: 0 }}
|
| 69 |
+
transition={{ duration: 0.3 }}
|
| 70 |
+
className="mb-10 overflow-hidden"
|
| 71 |
+
>
|
| 72 |
+
<AITerminal />
|
| 73 |
+
</motion.div>
|
| 74 |
+
)}
|
| 75 |
+
</AnimatePresence>
|
| 76 |
+
|
| 77 |
+
{/* Stats Bar */}
|
| 78 |
+
<motion.div
|
| 79 |
+
initial={{ opacity: 0, y: 20 }}
|
| 80 |
+
animate={{ opacity: 1, y: 0 }}
|
| 81 |
+
transition={{ delay: 0.2 }}
|
| 82 |
+
className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-10"
|
| 83 |
+
>
|
| 84 |
+
{[
|
| 85 |
+
{ label: 'Projects', value: projects.length },
|
| 86 |
+
{ label: 'Videos Generated', value: user?.videosGenerated || 0 },
|
| 87 |
+
{ label: 'Subscription', value: user?.subscription || 'Free' },
|
| 88 |
+
{ label: 'This Month', value: '0 / 5' },
|
| 89 |
+
].map((stat) => (
|
| 90 |
+
<div key={stat.label} className="card-static text-center">
|
| 91 |
+
<p className="font-display text-2xl font-bold text-light-100">{stat.value}</p>
|
| 92 |
+
<p className="text-sm text-light-500 mt-1">{stat.label}</p>
|
| 93 |
+
</div>
|
| 94 |
+
))}
|
| 95 |
+
</motion.div>
|
| 96 |
+
|
| 97 |
+
{/* Projects Grid */}
|
| 98 |
+
<div className="mb-6">
|
| 99 |
+
<h2 className="font-display text-2xl font-semibold text-light-100">Your Projects</h2>
|
| 100 |
+
</div>
|
| 101 |
+
|
| 102 |
+
{loading ? (
|
| 103 |
+
<div className="flex items-center justify-center py-20">
|
| 104 |
+
<div className="w-8 h-8 border-2 border-gold-500 border-t-transparent rounded-full animate-spin" />
|
| 105 |
+
</div>
|
| 106 |
+
) : projects.length === 0 ? (
|
| 107 |
+
<motion.div
|
| 108 |
+
initial={{ opacity: 0 }}
|
| 109 |
+
animate={{ opacity: 1 }}
|
| 110 |
+
className="card-static text-center py-16"
|
| 111 |
+
>
|
| 112 |
+
<div className="w-16 h-16 bg-gold-500/10 border border-gold-500/20 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
| 113 |
+
<span className="text-gold-500 text-3xl font-light">+</span>
|
| 114 |
+
</div>
|
| 115 |
+
<h3 className="font-display text-xl text-light-100 mb-2">No projects yet</h3>
|
| 116 |
+
<p className="text-light-500 mb-6">Create your first project to start generating videos</p>
|
| 117 |
+
<button onClick={() => setShowCreateModal(true)} className="btn-primary">
|
| 118 |
+
Create Your First Project
|
| 119 |
+
</button>
|
| 120 |
+
</motion.div>
|
| 121 |
+
) : (
|
| 122 |
+
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
| 123 |
+
{projects.map((project, i) => (
|
| 124 |
+
<motion.div
|
| 125 |
+
key={project._id}
|
| 126 |
+
initial={{ opacity: 0, y: 20 }}
|
| 127 |
+
animate={{ opacity: 1, y: 0 }}
|
| 128 |
+
transition={{ delay: i * 0.05 }}
|
| 129 |
+
>
|
| 130 |
+
<Link to={`/project/${project._id}`} className="card block group">
|
| 131 |
+
<div className="flex items-start justify-between mb-4">
|
| 132 |
+
<div className="w-10 h-10 bg-gold-500/10 border border-gold-500/20 rounded-xl flex items-center justify-center group-hover:bg-gold-500/20 transition-colors">
|
| 133 |
+
<div className="w-3 h-3 bg-gold-500 rounded" />
|
| 134 |
+
</div>
|
| 135 |
+
<span className="text-xs text-light-500 bg-dark-600 px-2 py-1 rounded">
|
| 136 |
+
{project.defaultPlatform}
|
| 137 |
+
</span>
|
| 138 |
+
</div>
|
| 139 |
+
<h3 className="font-display text-lg font-semibold text-light-100 mb-1 group-hover:text-gold-400 transition-colors">
|
| 140 |
+
{project.name}
|
| 141 |
+
</h3>
|
| 142 |
+
<p className="text-sm text-light-500">
|
| 143 |
+
{project.videos?.length || 0} video{project.videos?.length !== 1 ? 's' : ''} -- {project.defaultFormat}
|
| 144 |
+
</p>
|
| 145 |
+
</Link>
|
| 146 |
+
</motion.div>
|
| 147 |
+
))}
|
| 148 |
+
</div>
|
| 149 |
+
)}
|
| 150 |
+
</div>
|
| 151 |
+
|
| 152 |
+
{/* Create Project Modal */}
|
| 153 |
+
<AnimatePresence>
|
| 154 |
+
{showCreateModal && (
|
| 155 |
+
<CreateProjectModal onClose={() => setShowCreateModal(false)} />
|
| 156 |
+
)}
|
| 157 |
+
</AnimatePresence>
|
| 158 |
+
</div>
|
| 159 |
+
);
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
function CreateProjectModal({ onClose }: { onClose: () => void }) {
|
| 163 |
+
const dispatch = useDispatch<AppDispatch>();
|
| 164 |
+
const [name, setName] = useState('');
|
| 165 |
+
const [platform, setPlatform] = useState('TikTok');
|
| 166 |
+
const [format, setFormat] = useState('9:16');
|
| 167 |
+
const [submitting, setSubmitting] = useState(false);
|
| 168 |
+
|
| 169 |
+
const handleSubmit = async (e: FormEvent) => {
|
| 170 |
+
e.preventDefault();
|
| 171 |
+
if (!name.trim()) return;
|
| 172 |
+
setSubmitting(true);
|
| 173 |
+
await dispatch(createProject({ name, defaultPlatform: platform, defaultFormat: format }));
|
| 174 |
+
setSubmitting(false);
|
| 175 |
+
onClose();
|
| 176 |
+
};
|
| 177 |
+
|
| 178 |
+
return (
|
| 179 |
+
<motion.div
|
| 180 |
+
initial={{ opacity: 0 }}
|
| 181 |
+
animate={{ opacity: 1 }}
|
| 182 |
+
exit={{ opacity: 0 }}
|
| 183 |
+
className="fixed inset-0 z-50 flex items-center justify-center bg-dark-900/80 backdrop-blur-sm px-6"
|
| 184 |
+
onClick={onClose}
|
| 185 |
+
>
|
| 186 |
+
<motion.div
|
| 187 |
+
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
| 188 |
+
animate={{ opacity: 1, scale: 1, y: 0 }}
|
| 189 |
+
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
| 190 |
+
transition={{ duration: 0.3 }}
|
| 191 |
+
className="glass-panel p-8 w-full max-w-lg"
|
| 192 |
+
onClick={(e) => e.stopPropagation()}
|
| 193 |
+
>
|
| 194 |
+
<h2 className="font-display text-2xl font-bold text-light-100 mb-6">Create New Project</h2>
|
| 195 |
+
|
| 196 |
+
<form onSubmit={handleSubmit} className="space-y-5">
|
| 197 |
+
<div>
|
| 198 |
+
<label htmlFor="project-name" className="block text-sm font-body text-light-400 mb-2">Project Name</label>
|
| 199 |
+
<input
|
| 200 |
+
id="project-name"
|
| 201 |
+
type="text"
|
| 202 |
+
value={name}
|
| 203 |
+
onChange={(e) => setName(e.target.value)}
|
| 204 |
+
className="input-field"
|
| 205 |
+
placeholder="My Awesome Video Series"
|
| 206 |
+
required
|
| 207 |
+
autoFocus
|
| 208 |
+
/>
|
| 209 |
+
</div>
|
| 210 |
+
|
| 211 |
+
<div>
|
| 212 |
+
<label htmlFor="project-platform" className="block text-sm font-body text-light-400 mb-2">Default Platform</label>
|
| 213 |
+
<select
|
| 214 |
+
id="project-platform"
|
| 215 |
+
value={platform}
|
| 216 |
+
onChange={(e) => setPlatform(e.target.value)}
|
| 217 |
+
className="input-field"
|
| 218 |
+
>
|
| 219 |
+
<option value="TikTok">TikTok</option>
|
| 220 |
+
<option value="Reels">Instagram Reels</option>
|
| 221 |
+
<option value="Shorts">YouTube Shorts</option>
|
| 222 |
+
<option value="YouTube">YouTube</option>
|
| 223 |
+
</select>
|
| 224 |
+
</div>
|
| 225 |
+
|
| 226 |
+
<div>
|
| 227 |
+
<label htmlFor="project-format" className="block text-sm font-body text-light-400 mb-2">Default Format</label>
|
| 228 |
+
<select
|
| 229 |
+
id="project-format"
|
| 230 |
+
value={format}
|
| 231 |
+
onChange={(e) => setFormat(e.target.value)}
|
| 232 |
+
className="input-field"
|
| 233 |
+
>
|
| 234 |
+
<option value="9:16">9:16 (Vertical)</option>
|
| 235 |
+
<option value="16:9">16:9 (Landscape)</option>
|
| 236 |
+
<option value="1:1">1:1 (Square)</option>
|
| 237 |
+
</select>
|
| 238 |
+
</div>
|
| 239 |
+
|
| 240 |
+
<div className="flex gap-3 pt-2">
|
| 241 |
+
<button type="button" onClick={onClose} className="btn-ghost flex-1">Cancel</button>
|
| 242 |
+
<button type="submit" disabled={submitting} className="btn-primary flex-1">
|
| 243 |
+
{submitting ? 'Creating...' : 'Create Project'}
|
| 244 |
+
</button>
|
| 245 |
+
</div>
|
| 246 |
+
</form>
|
| 247 |
+
</motion.div>
|
| 248 |
+
</motion.div>
|
| 249 |
+
);
|
| 250 |
+
}
|
client/src/pages/Landing.tsx
ADDED
|
@@ -0,0 +1,524 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { motion } from 'framer-motion';
|
| 2 |
+
import { Link } from 'react-router-dom';
|
| 3 |
+
import { useState } from 'react';
|
| 4 |
+
|
| 5 |
+
const fadeInUp = {
|
| 6 |
+
hidden: { opacity: 0, y: 30 },
|
| 7 |
+
visible: (i: number) => ({
|
| 8 |
+
opacity: 1, y: 0,
|
| 9 |
+
transition: { delay: i * 0.15, duration: 0.6, ease: 'easeOut' },
|
| 10 |
+
}),
|
| 11 |
+
};
|
| 12 |
+
|
| 13 |
+
const stagger = {
|
| 14 |
+
visible: { transition: { staggerChildren: 0.1 } },
|
| 15 |
+
};
|
| 16 |
+
|
| 17 |
+
export default function Landing() {
|
| 18 |
+
return (
|
| 19 |
+
<div className="overflow-hidden">
|
| 20 |
+
<HeroSection />
|
| 21 |
+
<HowItWorksSection />
|
| 22 |
+
<FeaturesSection />
|
| 23 |
+
<ForWhoSection />
|
| 24 |
+
<PricingSection />
|
| 25 |
+
<FAQSection />
|
| 26 |
+
<CTASection />
|
| 27 |
+
<Footer />
|
| 28 |
+
</div>
|
| 29 |
+
);
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
/* ─── Hero ─── */
|
| 33 |
+
function HeroSection() {
|
| 34 |
+
return (
|
| 35 |
+
<section className="relative min-h-screen flex items-center justify-center pt-16 overflow-hidden">
|
| 36 |
+
{/* Background Effects */}
|
| 37 |
+
<div className="absolute inset-0 bg-hero-gradient" />
|
| 38 |
+
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-gold-500/5 rounded-full blur-3xl animate-float" />
|
| 39 |
+
<div className="absolute bottom-1/4 right-1/4 w-80 h-80 bg-gold-500/3 rounded-full blur-3xl animate-float" style={{ animationDelay: '3s' }} />
|
| 40 |
+
|
| 41 |
+
{/* Grid pattern overlay */}
|
| 42 |
+
<div className="absolute inset-0 opacity-[0.03]"
|
| 43 |
+
style={{
|
| 44 |
+
backgroundImage: 'linear-gradient(rgba(212,175,55,0.3) 1px, transparent 1px), linear-gradient(90deg, rgba(212,175,55,0.3) 1px, transparent 1px)',
|
| 45 |
+
backgroundSize: '60px 60px',
|
| 46 |
+
}}
|
| 47 |
+
/>
|
| 48 |
+
|
| 49 |
+
<div className="relative z-10 max-w-5xl mx-auto px-6 text-center">
|
| 50 |
+
<motion.div
|
| 51 |
+
initial="hidden"
|
| 52 |
+
animate="visible"
|
| 53 |
+
variants={stagger}
|
| 54 |
+
>
|
| 55 |
+
<motion.div variants={fadeInUp} custom={0} className="mb-6">
|
| 56 |
+
<span className="inline-block px-4 py-1.5 bg-gold-500/10 border border-gold-500/20 rounded-full text-gold-500 text-xs font-body font-medium tracking-widest uppercase">
|
| 57 |
+
AI-Powered Video Creation
|
| 58 |
+
</span>
|
| 59 |
+
</motion.div>
|
| 60 |
+
|
| 61 |
+
<motion.h1 variants={fadeInUp} custom={1} className="font-display text-5xl md:text-7xl lg:text-8xl font-bold text-light-100 leading-tight mb-6 text-balance">
|
| 62 |
+
Turn your scripts into{' '}
|
| 63 |
+
<span className="text-transparent bg-clip-text bg-gold-gradient">
|
| 64 |
+
ready-to-post
|
| 65 |
+
</span>{' '}
|
| 66 |
+
faceless videos
|
| 67 |
+
</motion.h1>
|
| 68 |
+
|
| 69 |
+
<motion.p variants={fadeInUp} custom={2} className="font-body text-lg md:text-xl text-light-500 max-w-2xl mx-auto mb-10 leading-relaxed">
|
| 70 |
+
Stop wasting hours editing. Paste your script, choose a style, and let AI create
|
| 71 |
+
scroll-stopping videos optimized for TikTok, Reels, and Shorts -- in minutes, not days.
|
| 72 |
+
</motion.p>
|
| 73 |
+
|
| 74 |
+
<motion.div variants={fadeInUp} custom={3} className="flex flex-col sm:flex-row items-center justify-center gap-4">
|
| 75 |
+
<Link to="/register" className="btn-primary text-lg px-10 py-4 shadow-gold-lg">
|
| 76 |
+
Start now
|
| 77 |
+
</Link>
|
| 78 |
+
<button className="btn-secondary text-lg px-10 py-4">
|
| 79 |
+
Watch a 30-second demo
|
| 80 |
+
</button>
|
| 81 |
+
</motion.div>
|
| 82 |
+
|
| 83 |
+
<motion.p variants={fadeInUp} custom={4} className="mt-6 text-sm text-light-500/60">
|
| 84 |
+
Try 5 videos free -- No credit card required
|
| 85 |
+
</motion.p>
|
| 86 |
+
</motion.div>
|
| 87 |
+
</div>
|
| 88 |
+
|
| 89 |
+
{/* Bottom gradient fade */}
|
| 90 |
+
<div className="absolute bottom-0 left-0 right-0 h-32 bg-gradient-to-t from-dark-900 to-transparent" />
|
| 91 |
+
</section>
|
| 92 |
+
);
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
/* ─── How It Works ─── */
|
| 96 |
+
function HowItWorksSection() {
|
| 97 |
+
const steps = [
|
| 98 |
+
{
|
| 99 |
+
num: '01',
|
| 100 |
+
title: 'Paste Your Script',
|
| 101 |
+
desc: 'Write or paste your script. Upload a text file or type directly into the editor. AI helps refine your message for maximum impact.',
|
| 102 |
+
},
|
| 103 |
+
{
|
| 104 |
+
num: '02',
|
| 105 |
+
title: 'Customize Everything',
|
| 106 |
+
desc: 'Choose your voice, music, visual style, fonts, and transitions. Save presets to maintain brand consistency across all your videos.',
|
| 107 |
+
},
|
| 108 |
+
{
|
| 109 |
+
num: '03',
|
| 110 |
+
title: 'Generate and Export',
|
| 111 |
+
desc: 'Hit generate and watch AI assemble your video in real-time through the built-in terminal. Export in any format, any ratio, any quality.',
|
| 112 |
+
},
|
| 113 |
+
];
|
| 114 |
+
|
| 115 |
+
return (
|
| 116 |
+
<section className="py-32 relative">
|
| 117 |
+
<div className="max-w-6xl mx-auto px-6">
|
| 118 |
+
<motion.div
|
| 119 |
+
initial="hidden"
|
| 120 |
+
whileInView="visible"
|
| 121 |
+
viewport={{ once: true, margin: '-100px' }}
|
| 122 |
+
variants={stagger}
|
| 123 |
+
className="text-center mb-20"
|
| 124 |
+
>
|
| 125 |
+
<motion.h2 variants={fadeInUp} custom={0} className="section-heading">
|
| 126 |
+
How It <span className="gold-text">Works</span>
|
| 127 |
+
</motion.h2>
|
| 128 |
+
<motion.p variants={fadeInUp} custom={1} className="section-subheading">
|
| 129 |
+
Three simple steps from script to screen
|
| 130 |
+
</motion.p>
|
| 131 |
+
</motion.div>
|
| 132 |
+
|
| 133 |
+
<div className="grid md:grid-cols-3 gap-8">
|
| 134 |
+
{steps.map((step, i) => (
|
| 135 |
+
<motion.div
|
| 136 |
+
key={step.num}
|
| 137 |
+
initial="hidden"
|
| 138 |
+
whileInView="visible"
|
| 139 |
+
viewport={{ once: true, margin: '-50px' }}
|
| 140 |
+
variants={fadeInUp}
|
| 141 |
+
custom={i}
|
| 142 |
+
className="card group relative overflow-hidden"
|
| 143 |
+
>
|
| 144 |
+
{/* Step number */}
|
| 145 |
+
<span className="absolute top-4 right-4 font-display text-6xl font-bold text-gold-500/10 group-hover:text-gold-500/20 transition-colors duration-500">
|
| 146 |
+
{step.num}
|
| 147 |
+
</span>
|
| 148 |
+
|
| 149 |
+
{/* Gold accent line */}
|
| 150 |
+
<div className="w-12 h-1 bg-gold-gradient rounded-full mb-6" />
|
| 151 |
+
|
| 152 |
+
<h3 className="font-display text-2xl font-semibold text-light-100 mb-3">
|
| 153 |
+
{step.title}
|
| 154 |
+
</h3>
|
| 155 |
+
<p className="font-body text-light-500 leading-relaxed">
|
| 156 |
+
{step.desc}
|
| 157 |
+
</p>
|
| 158 |
+
</motion.div>
|
| 159 |
+
))}
|
| 160 |
+
</div>
|
| 161 |
+
</div>
|
| 162 |
+
</section>
|
| 163 |
+
);
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
/* ─── Features ─── */
|
| 167 |
+
function FeaturesSection() {
|
| 168 |
+
const features = [
|
| 169 |
+
{ title: 'AI Voice Generation', desc: 'Natural-sounding voiceovers in multiple languages, tones, and speeds.' },
|
| 170 |
+
{ title: 'Smart Subtitles', desc: 'Auto-synced captions with customizable fonts, colors, and animations.' },
|
| 171 |
+
{ title: 'Multi-Platform Export', desc: 'One-click export for TikTok (9:16), YouTube (16:9), and Instagram (1:1).' },
|
| 172 |
+
{ title: 'Style Presets', desc: 'Save and reuse your brand settings: fonts, colors, transitions, and more.' },
|
| 173 |
+
{ title: 'Real-Time AI Terminal', desc: 'Watch AI build your video live through the integrated command terminal.' },
|
| 174 |
+
{ title: 'Asset Library', desc: 'Upload your own clips, images, logos, and music -- or use the built-in library.' },
|
| 175 |
+
{ title: 'Batch Processing', desc: 'Queue multiple videos and let the system handle them autonomously.' },
|
| 176 |
+
{ title: 'No Subscription Lock-In', desc: 'Generous free tier. Pay only for what you use beyond the free limits.' },
|
| 177 |
+
];
|
| 178 |
+
|
| 179 |
+
return (
|
| 180 |
+
<section id="features" className="py-32 relative">
|
| 181 |
+
<div className="absolute inset-0 bg-dark-gradient opacity-50" />
|
| 182 |
+
<div className="relative max-w-6xl mx-auto px-6">
|
| 183 |
+
<motion.div
|
| 184 |
+
initial="hidden"
|
| 185 |
+
whileInView="visible"
|
| 186 |
+
viewport={{ once: true, margin: '-100px' }}
|
| 187 |
+
variants={stagger}
|
| 188 |
+
className="text-center mb-20"
|
| 189 |
+
>
|
| 190 |
+
<motion.h2 variants={fadeInUp} custom={0} className="section-heading">
|
| 191 |
+
Powerful <span className="gold-text">Features</span>
|
| 192 |
+
</motion.h2>
|
| 193 |
+
<motion.p variants={fadeInUp} custom={1} className="section-subheading">
|
| 194 |
+
Everything you need to create professional faceless content
|
| 195 |
+
</motion.p>
|
| 196 |
+
</motion.div>
|
| 197 |
+
|
| 198 |
+
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
| 199 |
+
{features.map((f, i) => (
|
| 200 |
+
<motion.div
|
| 201 |
+
key={f.title}
|
| 202 |
+
initial="hidden"
|
| 203 |
+
whileInView="visible"
|
| 204 |
+
viewport={{ once: true, margin: '-30px' }}
|
| 205 |
+
variants={fadeInUp}
|
| 206 |
+
custom={i % 4}
|
| 207 |
+
className="card text-center"
|
| 208 |
+
>
|
| 209 |
+
<div className="w-10 h-10 bg-gold-500/10 border border-gold-500/20 rounded-xl flex items-center justify-center mx-auto mb-4">
|
| 210 |
+
<div className="w-3 h-3 bg-gold-500 rounded-full" />
|
| 211 |
+
</div>
|
| 212 |
+
<h3 className="font-display text-lg font-semibold text-light-100 mb-2">{f.title}</h3>
|
| 213 |
+
<p className="font-body text-sm text-light-500">{f.desc}</p>
|
| 214 |
+
</motion.div>
|
| 215 |
+
))}
|
| 216 |
+
</div>
|
| 217 |
+
</div>
|
| 218 |
+
</section>
|
| 219 |
+
);
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
/* ─── For Who ─── */
|
| 223 |
+
function ForWhoSection() {
|
| 224 |
+
const audiences = [
|
| 225 |
+
{ title: 'Content Creators', desc: 'Scale your faceless channel output from 2 videos a week to 2 a day. Focus on strategy, let AI handle production.' },
|
| 226 |
+
{ title: 'Digital Marketers', desc: 'Create ad variations, explainer videos, and social content at a fraction of the cost and time of traditional production.' },
|
| 227 |
+
{ title: 'Educators and Coaches', desc: 'Transform your lessons, courses, and tips into engaging video content without ever showing your face on camera.' },
|
| 228 |
+
{ title: 'Agencies', desc: 'White-label video production for clients. Maintain brand consistency with presets. Scale without hiring more editors.' },
|
| 229 |
+
];
|
| 230 |
+
|
| 231 |
+
return (
|
| 232 |
+
<section className="py-32 relative">
|
| 233 |
+
<div className="max-w-6xl mx-auto px-6">
|
| 234 |
+
<motion.div
|
| 235 |
+
initial="hidden"
|
| 236 |
+
whileInView="visible"
|
| 237 |
+
viewport={{ once: true, margin: '-100px' }}
|
| 238 |
+
variants={stagger}
|
| 239 |
+
className="text-center mb-20"
|
| 240 |
+
>
|
| 241 |
+
<motion.h2 variants={fadeInUp} custom={0} className="section-heading">
|
| 242 |
+
Built <span className="gold-text">For You</span>
|
| 243 |
+
</motion.h2>
|
| 244 |
+
</motion.div>
|
| 245 |
+
|
| 246 |
+
<div className="grid md:grid-cols-2 gap-8">
|
| 247 |
+
{audiences.map((a, i) => (
|
| 248 |
+
<motion.div
|
| 249 |
+
key={a.title}
|
| 250 |
+
initial="hidden"
|
| 251 |
+
whileInView="visible"
|
| 252 |
+
viewport={{ once: true, margin: '-50px' }}
|
| 253 |
+
variants={fadeInUp}
|
| 254 |
+
custom={i}
|
| 255 |
+
className="card flex gap-5"
|
| 256 |
+
>
|
| 257 |
+
<div className="flex-shrink-0 w-12 h-12 bg-gold-500/10 border border-gold-500/20 rounded-xl flex items-center justify-center">
|
| 258 |
+
<div className="w-4 h-4 bg-gold-gradient rounded" />
|
| 259 |
+
</div>
|
| 260 |
+
<div>
|
| 261 |
+
<h3 className="font-display text-xl font-semibold text-light-100 mb-2">{a.title}</h3>
|
| 262 |
+
<p className="font-body text-light-500 leading-relaxed">{a.desc}</p>
|
| 263 |
+
</div>
|
| 264 |
+
</motion.div>
|
| 265 |
+
))}
|
| 266 |
+
</div>
|
| 267 |
+
</div>
|
| 268 |
+
</section>
|
| 269 |
+
);
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
/* ─── Pricing ─── */
|
| 273 |
+
function PricingSection() {
|
| 274 |
+
const tiers = [
|
| 275 |
+
{
|
| 276 |
+
name: 'Free',
|
| 277 |
+
price: '$0',
|
| 278 |
+
period: 'forever',
|
| 279 |
+
features: ['5 videos per month', 'Basic voices', '720p export', 'Watermarked'],
|
| 280 |
+
cta: 'Start Free',
|
| 281 |
+
highlighted: false,
|
| 282 |
+
},
|
| 283 |
+
{
|
| 284 |
+
name: 'Starter',
|
| 285 |
+
price: '$19',
|
| 286 |
+
period: '/month',
|
| 287 |
+
features: ['25 videos per month', 'All voices and languages', '1080p export', 'No watermark', '3 presets'],
|
| 288 |
+
cta: 'Get Starter',
|
| 289 |
+
highlighted: false,
|
| 290 |
+
},
|
| 291 |
+
{
|
| 292 |
+
name: 'Creator',
|
| 293 |
+
price: '$49',
|
| 294 |
+
period: '/month',
|
| 295 |
+
features: ['100 videos per month', 'Priority processing', '4K export', 'Unlimited presets', 'Batch processing', 'Custom branding'],
|
| 296 |
+
cta: 'Get Creator',
|
| 297 |
+
highlighted: true,
|
| 298 |
+
},
|
| 299 |
+
{
|
| 300 |
+
name: 'Studio',
|
| 301 |
+
price: '$149',
|
| 302 |
+
period: '/month',
|
| 303 |
+
features: ['Unlimited videos', 'Dedicated processing', '4K export', 'API access', 'White-label', 'Priority support', 'Team accounts'],
|
| 304 |
+
cta: 'Get Studio',
|
| 305 |
+
highlighted: false,
|
| 306 |
+
},
|
| 307 |
+
];
|
| 308 |
+
|
| 309 |
+
return (
|
| 310 |
+
<section id="pricing" className="py-32 relative">
|
| 311 |
+
<div className="absolute inset-0 bg-dark-gradient opacity-50" />
|
| 312 |
+
<div className="relative max-w-7xl mx-auto px-6">
|
| 313 |
+
<motion.div
|
| 314 |
+
initial="hidden"
|
| 315 |
+
whileInView="visible"
|
| 316 |
+
viewport={{ once: true, margin: '-100px' }}
|
| 317 |
+
variants={stagger}
|
| 318 |
+
className="text-center mb-20"
|
| 319 |
+
>
|
| 320 |
+
<motion.h2 variants={fadeInUp} custom={0} className="section-heading">
|
| 321 |
+
Simple <span className="gold-text">Pricing</span>
|
| 322 |
+
</motion.h2>
|
| 323 |
+
<motion.p variants={fadeInUp} custom={1} className="section-subheading">
|
| 324 |
+
Start free. Scale as you grow. No hidden fees.
|
| 325 |
+
</motion.p>
|
| 326 |
+
</motion.div>
|
| 327 |
+
|
| 328 |
+
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
| 329 |
+
{tiers.map((tier, i) => (
|
| 330 |
+
<motion.div
|
| 331 |
+
key={tier.name}
|
| 332 |
+
initial="hidden"
|
| 333 |
+
whileInView="visible"
|
| 334 |
+
viewport={{ once: true, margin: '-30px' }}
|
| 335 |
+
variants={fadeInUp}
|
| 336 |
+
custom={i}
|
| 337 |
+
className={`card-static relative flex flex-col ${tier.highlighted
|
| 338 |
+
? 'border-gold-500/40 shadow-gold-lg scale-[1.02]'
|
| 339 |
+
: ''
|
| 340 |
+
}`}
|
| 341 |
+
>
|
| 342 |
+
{tier.highlighted && (
|
| 343 |
+
<div className="absolute -top-3 left-1/2 -translate-x-1/2">
|
| 344 |
+
<span className="px-4 py-1 bg-gold-gradient text-dark-900 text-xs font-body font-bold rounded-full uppercase tracking-wider">
|
| 345 |
+
Most Popular
|
| 346 |
+
</span>
|
| 347 |
+
</div>
|
| 348 |
+
)}
|
| 349 |
+
|
| 350 |
+
<div className="mb-6">
|
| 351 |
+
<h3 className="font-display text-xl font-semibold text-light-100 mb-2">{tier.name}</h3>
|
| 352 |
+
<div className="flex items-baseline gap-1">
|
| 353 |
+
<span className="font-display text-4xl font-bold text-light-100">{tier.price}</span>
|
| 354 |
+
<span className="text-light-500 text-sm">{tier.period}</span>
|
| 355 |
+
</div>
|
| 356 |
+
</div>
|
| 357 |
+
|
| 358 |
+
<ul className="flex-1 space-y-3 mb-8">
|
| 359 |
+
{tier.features.map((f) => (
|
| 360 |
+
<li key={f} className="flex items-start gap-2 text-sm text-light-400">
|
| 361 |
+
<span className="mt-1 w-1.5 h-1.5 bg-gold-500 rounded-full flex-shrink-0" />
|
| 362 |
+
{f}
|
| 363 |
+
</li>
|
| 364 |
+
))}
|
| 365 |
+
</ul>
|
| 366 |
+
|
| 367 |
+
<Link
|
| 368 |
+
to="/register"
|
| 369 |
+
className={tier.highlighted ? 'btn-primary w-full' : 'btn-secondary w-full'}
|
| 370 |
+
>
|
| 371 |
+
{tier.cta}
|
| 372 |
+
</Link>
|
| 373 |
+
</motion.div>
|
| 374 |
+
))}
|
| 375 |
+
</div>
|
| 376 |
+
</div>
|
| 377 |
+
</section>
|
| 378 |
+
);
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
/* ─── FAQ ─── */
|
| 382 |
+
function FAQSection() {
|
| 383 |
+
const faqs = [
|
| 384 |
+
{
|
| 385 |
+
q: 'What is a faceless video?',
|
| 386 |
+
a: 'A faceless video is content that does not show the creator on camera. It uses voiceovers, stock footage, text overlays, and animations to deliver the message. This format is hugely popular on TikTok, YouTube Shorts, and Instagram Reels.',
|
| 387 |
+
},
|
| 388 |
+
{
|
| 389 |
+
q: 'Do I need video editing experience?',
|
| 390 |
+
a: 'Not at all. Director.AI handles the entire editing process. You provide the script, choose your preferences, and the AI assembles everything automatically. The built-in terminal lets you watch the process in real time.',
|
| 391 |
+
},
|
| 392 |
+
{
|
| 393 |
+
q: 'How does the AI Terminal work?',
|
| 394 |
+
a: 'The AI Terminal is a live bridge to our AI engine. When you generate a video, commands are sent to the AI which processes voiceovers, subtitles, and video assembly. You can see every step happening in real time and even send custom prompts.',
|
| 395 |
+
},
|
| 396 |
+
{
|
| 397 |
+
q: 'Can I use my own assets?',
|
| 398 |
+
a: 'Yes. Upload your own clips, images, logos, and music. You can also use the built-in asset library. All uploads are limited to 100MB per file for security.',
|
| 399 |
+
},
|
| 400 |
+
{
|
| 401 |
+
q: 'What platforms are supported for export?',
|
| 402 |
+
a: 'Export in 9:16 (TikTok, Reels, Shorts), 16:9 (YouTube, Facebook), and 1:1 (Instagram feed). Multiple quality options from 720p to 4K depending on your plan.',
|
| 403 |
+
},
|
| 404 |
+
{
|
| 405 |
+
q: 'Is there a free tier?',
|
| 406 |
+
a: 'Yes. The free tier gives you 5 videos per month at 720p with a small watermark. No credit card required to start.',
|
| 407 |
+
},
|
| 408 |
+
];
|
| 409 |
+
|
| 410 |
+
return (
|
| 411 |
+
<section id="faq" className="py-32 relative">
|
| 412 |
+
<div className="max-w-3xl mx-auto px-6">
|
| 413 |
+
<motion.div
|
| 414 |
+
initial="hidden"
|
| 415 |
+
whileInView="visible"
|
| 416 |
+
viewport={{ once: true, margin: '-100px' }}
|
| 417 |
+
variants={stagger}
|
| 418 |
+
className="text-center mb-16"
|
| 419 |
+
>
|
| 420 |
+
<motion.h2 variants={fadeInUp} custom={0} className="section-heading">
|
| 421 |
+
Common <span className="gold-text">Questions</span>
|
| 422 |
+
</motion.h2>
|
| 423 |
+
</motion.div>
|
| 424 |
+
|
| 425 |
+
<div className="space-y-4">
|
| 426 |
+
{faqs.map((faq, i) => (
|
| 427 |
+
<FAQItem key={i} question={faq.q} answer={faq.a} index={i} />
|
| 428 |
+
))}
|
| 429 |
+
</div>
|
| 430 |
+
</div>
|
| 431 |
+
</section>
|
| 432 |
+
);
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
function FAQItem({ question, answer, index }: { question: string; answer: string; index: number }) {
|
| 436 |
+
const [open, setOpen] = useState(false);
|
| 437 |
+
|
| 438 |
+
return (
|
| 439 |
+
<motion.div
|
| 440 |
+
initial="hidden"
|
| 441 |
+
whileInView="visible"
|
| 442 |
+
viewport={{ once: true }}
|
| 443 |
+
variants={fadeInUp}
|
| 444 |
+
custom={index}
|
| 445 |
+
className="card-static cursor-pointer"
|
| 446 |
+
onClick={() => setOpen(!open)}
|
| 447 |
+
>
|
| 448 |
+
<div className="flex items-center justify-between">
|
| 449 |
+
<h3 className="font-display text-lg font-medium text-light-100 pr-4">{question}</h3>
|
| 450 |
+
<motion.span
|
| 451 |
+
animate={{ rotate: open ? 45 : 0 }}
|
| 452 |
+
transition={{ duration: 0.3 }}
|
| 453 |
+
className="text-gold-500 text-2xl flex-shrink-0 font-light"
|
| 454 |
+
>
|
| 455 |
+
+
|
| 456 |
+
</motion.span>
|
| 457 |
+
</div>
|
| 458 |
+
<motion.div
|
| 459 |
+
initial={false}
|
| 460 |
+
animate={{ height: open ? 'auto' : 0, opacity: open ? 1 : 0 }}
|
| 461 |
+
transition={{ duration: 0.3 }}
|
| 462 |
+
className="overflow-hidden"
|
| 463 |
+
>
|
| 464 |
+
<p className="font-body text-light-500 mt-4 leading-relaxed">{answer}</p>
|
| 465 |
+
</motion.div>
|
| 466 |
+
</motion.div>
|
| 467 |
+
);
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
/* ─── CTA ─── */
|
| 471 |
+
function CTASection() {
|
| 472 |
+
return (
|
| 473 |
+
<section className="py-32 relative">
|
| 474 |
+
<div className="absolute inset-0 bg-hero-gradient" />
|
| 475 |
+
<div className="relative max-w-3xl mx-auto px-6 text-center">
|
| 476 |
+
<motion.div
|
| 477 |
+
initial="hidden"
|
| 478 |
+
whileInView="visible"
|
| 479 |
+
viewport={{ once: true, margin: '-100px' }}
|
| 480 |
+
variants={stagger}
|
| 481 |
+
>
|
| 482 |
+
<motion.h2 variants={fadeInUp} custom={0} className="font-display text-4xl md:text-6xl font-bold text-light-100 mb-6 text-balance">
|
| 483 |
+
Ready to stop wasting time editing?
|
| 484 |
+
</motion.h2>
|
| 485 |
+
<motion.p variants={fadeInUp} custom={1} className="font-body text-lg text-light-500 mb-10">
|
| 486 |
+
Join creators who are already producing professional faceless videos in minutes.
|
| 487 |
+
</motion.p>
|
| 488 |
+
<motion.div variants={fadeInUp} custom={2}>
|
| 489 |
+
<Link to="/register" className="btn-primary text-lg px-12 py-4 shadow-gold-lg">
|
| 490 |
+
Generate my first video
|
| 491 |
+
</Link>
|
| 492 |
+
</motion.div>
|
| 493 |
+
</motion.div>
|
| 494 |
+
</div>
|
| 495 |
+
</section>
|
| 496 |
+
);
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
/* ─── Footer ─── */
|
| 500 |
+
function Footer() {
|
| 501 |
+
return (
|
| 502 |
+
<footer className="border-t border-dark-400/20 py-12">
|
| 503 |
+
<div className="max-w-6xl mx-auto px-6">
|
| 504 |
+
<div className="flex flex-col md:flex-row items-center justify-between gap-6">
|
| 505 |
+
<div className="flex items-center gap-2">
|
| 506 |
+
<div className="w-6 h-6 bg-gold-gradient rounded flex items-center justify-center">
|
| 507 |
+
<span className="text-dark-900 font-display font-bold text-xs">D</span>
|
| 508 |
+
</div>
|
| 509 |
+
<span className="font-display text-lg font-bold text-light-300">
|
| 510 |
+
Director<span className="text-gold-500">.AI</span>
|
| 511 |
+
</span>
|
| 512 |
+
</div>
|
| 513 |
+
<div className="flex items-center gap-6 text-sm text-light-500">
|
| 514 |
+
<Link to="/privacy" className="hover:text-gold-500 transition-colors">Privacy Policy</Link>
|
| 515 |
+
<Link to="/terms" className="hover:text-gold-500 transition-colors">Terms of Use</Link>
|
| 516 |
+
</div>
|
| 517 |
+
<p className="text-sm text-light-500/60">
|
| 518 |
+
2026 Director.AI. All rights reserved.
|
| 519 |
+
</p>
|
| 520 |
+
</div>
|
| 521 |
+
</div>
|
| 522 |
+
</footer>
|
| 523 |
+
);
|
| 524 |
+
}
|
client/src/pages/Login.tsx
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, FormEvent } from 'react';
|
| 2 |
+
import { useDispatch, useSelector } from 'react-redux';
|
| 3 |
+
import { Link, useNavigate } from 'react-router-dom';
|
| 4 |
+
import { motion } from 'framer-motion';
|
| 5 |
+
import { AppDispatch, RootState } from '../store';
|
| 6 |
+
import { loginUser, clearError } from '../store/authSlice';
|
| 7 |
+
|
| 8 |
+
export default function Login() {
|
| 9 |
+
const [email, setEmail] = useState('');
|
| 10 |
+
const [password, setPassword] = useState('');
|
| 11 |
+
const dispatch = useDispatch<AppDispatch>();
|
| 12 |
+
const navigate = useNavigate();
|
| 13 |
+
const { loading, error } = useSelector((state: RootState) => state.auth);
|
| 14 |
+
|
| 15 |
+
const handleSubmit = async (e: FormEvent) => {
|
| 16 |
+
e.preventDefault();
|
| 17 |
+
const result = await dispatch(loginUser({ email, password }));
|
| 18 |
+
if (loginUser.fulfilled.match(result)) {
|
| 19 |
+
navigate('/dashboard');
|
| 20 |
+
}
|
| 21 |
+
};
|
| 22 |
+
|
| 23 |
+
return (
|
| 24 |
+
<div className="min-h-screen flex items-center justify-center pt-16 px-6">
|
| 25 |
+
<div className="absolute inset-0 bg-hero-gradient opacity-50" />
|
| 26 |
+
<motion.div
|
| 27 |
+
initial={{ opacity: 0, y: 20 }}
|
| 28 |
+
animate={{ opacity: 1, y: 0 }}
|
| 29 |
+
transition={{ duration: 0.5 }}
|
| 30 |
+
className="relative w-full max-w-md"
|
| 31 |
+
>
|
| 32 |
+
<div className="glass-panel p-8">
|
| 33 |
+
<div className="text-center mb-8">
|
| 34 |
+
<h1 className="font-display text-3xl font-bold text-light-100 mb-2">Welcome Back</h1>
|
| 35 |
+
<p className="font-body text-light-500">Sign in to your Director.AI account</p>
|
| 36 |
+
</div>
|
| 37 |
+
|
| 38 |
+
{error && (
|
| 39 |
+
<div className="mb-6 p-3 bg-red-500/10 border border-red-500/30 rounded-lg text-red-400 text-sm text-center">
|
| 40 |
+
{error}
|
| 41 |
+
</div>
|
| 42 |
+
)}
|
| 43 |
+
|
| 44 |
+
<form onSubmit={handleSubmit} className="space-y-5">
|
| 45 |
+
<div>
|
| 46 |
+
<label htmlFor="login-email" className="block text-sm font-body text-light-400 mb-2">Email</label>
|
| 47 |
+
<input
|
| 48 |
+
id="login-email"
|
| 49 |
+
type="email"
|
| 50 |
+
value={email}
|
| 51 |
+
onChange={(e) => { setEmail(e.target.value); dispatch(clearError()); }}
|
| 52 |
+
className="input-field"
|
| 53 |
+
placeholder="you@example.com"
|
| 54 |
+
required
|
| 55 |
+
/>
|
| 56 |
+
</div>
|
| 57 |
+
|
| 58 |
+
<div>
|
| 59 |
+
<label htmlFor="login-password" className="block text-sm font-body text-light-400 mb-2">Password</label>
|
| 60 |
+
<input
|
| 61 |
+
id="login-password"
|
| 62 |
+
type="password"
|
| 63 |
+
value={password}
|
| 64 |
+
onChange={(e) => { setPassword(e.target.value); dispatch(clearError()); }}
|
| 65 |
+
className="input-field"
|
| 66 |
+
placeholder="Enter your password"
|
| 67 |
+
required
|
| 68 |
+
/>
|
| 69 |
+
</div>
|
| 70 |
+
|
| 71 |
+
<button
|
| 72 |
+
type="submit"
|
| 73 |
+
disabled={loading}
|
| 74 |
+
className="btn-primary w-full py-3.5"
|
| 75 |
+
>
|
| 76 |
+
{loading ? 'Signing in...' : 'Sign In'}
|
| 77 |
+
</button>
|
| 78 |
+
</form>
|
| 79 |
+
|
| 80 |
+
<p className="mt-6 text-center text-sm text-light-500">
|
| 81 |
+
Don't have an account?{' '}
|
| 82 |
+
<Link to="/register" className="text-gold-500 hover:text-gold-400 font-medium transition-colors">
|
| 83 |
+
Create one
|
| 84 |
+
</Link>
|
| 85 |
+
</p>
|
| 86 |
+
</div>
|
| 87 |
+
</motion.div>
|
| 88 |
+
</div>
|
| 89 |
+
);
|
| 90 |
+
}
|
client/src/pages/Preview.tsx
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect, useState } from 'react';
|
| 2 |
+
import { useParams, Link } from 'react-router-dom';
|
| 3 |
+
import { useDispatch, useSelector } from 'react-redux';
|
| 4 |
+
import { motion } from 'framer-motion';
|
| 5 |
+
import { AppDispatch, RootState } from '../store';
|
| 6 |
+
import { fetchVideo, generateVideo, pollVideoStatus } from '../store/videosSlice';
|
| 7 |
+
import AITerminal from '../components/AITerminal';
|
| 8 |
+
|
| 9 |
+
export default function Preview() {
|
| 10 |
+
const { id } = useParams<{ id: string }>();
|
| 11 |
+
const dispatch = useDispatch<AppDispatch>();
|
| 12 |
+
const { current: video } = useSelector((state: RootState) => state.videos);
|
| 13 |
+
const [showTerminal, setShowTerminal] = useState(true);
|
| 14 |
+
const [exportFormats, setExportFormats] = useState({
|
| 15 |
+
'9:16': true,
|
| 16 |
+
'16:9': false,
|
| 17 |
+
'1:1': false,
|
| 18 |
+
});
|
| 19 |
+
|
| 20 |
+
useEffect(() => {
|
| 21 |
+
if (id) dispatch(fetchVideo(id));
|
| 22 |
+
}, [id, dispatch]);
|
| 23 |
+
|
| 24 |
+
// Poll status while generating
|
| 25 |
+
useEffect(() => {
|
| 26 |
+
if (!video || video.status !== 'generating' || !id) return;
|
| 27 |
+
const interval = setInterval(() => {
|
| 28 |
+
dispatch(pollVideoStatus(id));
|
| 29 |
+
}, 10000);
|
| 30 |
+
return () => clearInterval(interval);
|
| 31 |
+
}, [video?.status, id, dispatch]);
|
| 32 |
+
|
| 33 |
+
const handleGenerate = () => {
|
| 34 |
+
if (id) dispatch(generateVideo(id));
|
| 35 |
+
};
|
| 36 |
+
|
| 37 |
+
if (!video) {
|
| 38 |
+
return (
|
| 39 |
+
<div className="min-h-screen pt-20 flex items-center justify-center">
|
| 40 |
+
<div className="w-8 h-8 border-2 border-gold-500 border-t-transparent rounded-full animate-spin" />
|
| 41 |
+
</div>
|
| 42 |
+
);
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
return (
|
| 46 |
+
<div className="min-h-screen pt-20 pb-12 px-6">
|
| 47 |
+
<div className="max-w-6xl mx-auto">
|
| 48 |
+
{/* Breadcrumb */}
|
| 49 |
+
<div className="flex items-center gap-2 text-sm text-light-500 mb-6">
|
| 50 |
+
<Link to="/dashboard" className="hover:text-gold-500 transition-colors">Dashboard</Link>
|
| 51 |
+
<span>/</span>
|
| 52 |
+
<span className="text-light-300">Video Preview</span>
|
| 53 |
+
</div>
|
| 54 |
+
|
| 55 |
+
<div className="grid lg:grid-cols-3 gap-8">
|
| 56 |
+
{/* Video Player Area */}
|
| 57 |
+
<div className="lg:col-span-2">
|
| 58 |
+
<motion.div
|
| 59 |
+
initial={{ opacity: 0, y: 20 }}
|
| 60 |
+
animate={{ opacity: 1, y: 0 }}
|
| 61 |
+
className="glass-panel overflow-hidden"
|
| 62 |
+
>
|
| 63 |
+
{/* Player */}
|
| 64 |
+
<div className="aspect-video bg-dark-900 flex items-center justify-center relative">
|
| 65 |
+
{video.previewUrl ? (
|
| 66 |
+
<video
|
| 67 |
+
src={video.previewUrl}
|
| 68 |
+
controls
|
| 69 |
+
className="w-full h-full object-contain"
|
| 70 |
+
id="video-player"
|
| 71 |
+
/>
|
| 72 |
+
) : (
|
| 73 |
+
<div className="text-center p-8">
|
| 74 |
+
<div className="w-16 h-16 bg-gold-500/10 border border-gold-500/20 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
| 75 |
+
<svg className="w-8 h-8 text-gold-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 76 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
|
| 77 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
| 78 |
+
</svg>
|
| 79 |
+
</div>
|
| 80 |
+
<p className="text-light-400 mb-1">No preview available yet</p>
|
| 81 |
+
<p className="text-sm text-light-500">Generate a preview using the AI Terminal below</p>
|
| 82 |
+
</div>
|
| 83 |
+
)}
|
| 84 |
+
|
| 85 |
+
{/* Status Badge */}
|
| 86 |
+
<div className="absolute top-4 right-4">
|
| 87 |
+
<span className={`text-xs px-3 py-1 rounded-full font-medium ${video.status === 'exported' ? 'bg-green-500/20 text-green-400' :
|
| 88 |
+
video.status === 'generating' ? 'bg-gold-500/20 text-gold-500 animate-pulse' :
|
| 89 |
+
video.status === 'preview' ? 'bg-blue-500/20 text-blue-400' :
|
| 90 |
+
video.status === 'failed' ? 'bg-red-500/20 text-red-400' :
|
| 91 |
+
'bg-dark-500 text-light-500'
|
| 92 |
+
}`}>
|
| 93 |
+
{video.status}
|
| 94 |
+
</span>
|
| 95 |
+
</div>
|
| 96 |
+
</div>
|
| 97 |
+
|
| 98 |
+
{/* Action Bar */}
|
| 99 |
+
<div className="p-4 flex flex-wrap gap-3 border-t border-dark-400/30">
|
| 100 |
+
<button
|
| 101 |
+
onClick={handleGenerate}
|
| 102 |
+
disabled={video.status === 'generating'}
|
| 103 |
+
className="btn-primary text-sm"
|
| 104 |
+
id="regenerate-btn"
|
| 105 |
+
>
|
| 106 |
+
{video.status === 'generating' ? 'Generating...' : 'Generate / Regenerate'}
|
| 107 |
+
</button>
|
| 108 |
+
<button onClick={() => setShowTerminal(!showTerminal)} className="btn-secondary text-sm">
|
| 109 |
+
<span className="mr-1 font-mono">>_</span> Terminal
|
| 110 |
+
</button>
|
| 111 |
+
</div>
|
| 112 |
+
</motion.div>
|
| 113 |
+
|
| 114 |
+
{/* Terminal */}
|
| 115 |
+
{showTerminal && (
|
| 116 |
+
<motion.div
|
| 117 |
+
initial={{ opacity: 0 }}
|
| 118 |
+
animate={{ opacity: 1 }}
|
| 119 |
+
className="mt-6"
|
| 120 |
+
>
|
| 121 |
+
<AITerminal videoId={id} />
|
| 122 |
+
</motion.div>
|
| 123 |
+
)}
|
| 124 |
+
</div>
|
| 125 |
+
|
| 126 |
+
{/* Sidebar */}
|
| 127 |
+
<div className="space-y-6">
|
| 128 |
+
{/* Script */}
|
| 129 |
+
<motion.div
|
| 130 |
+
initial={{ opacity: 0, y: 20 }}
|
| 131 |
+
animate={{ opacity: 1, y: 0 }}
|
| 132 |
+
transition={{ delay: 0.1 }}
|
| 133 |
+
className="card-static"
|
| 134 |
+
>
|
| 135 |
+
<h3 className="font-display text-lg font-semibold text-light-100 mb-3">Script</h3>
|
| 136 |
+
<p className="text-sm text-light-400 whitespace-pre-wrap max-h-40 overflow-y-auto">
|
| 137 |
+
{video.script || 'No script provided'}
|
| 138 |
+
</p>
|
| 139 |
+
</motion.div>
|
| 140 |
+
|
| 141 |
+
{/* Voice Settings */}
|
| 142 |
+
<motion.div
|
| 143 |
+
initial={{ opacity: 0, y: 20 }}
|
| 144 |
+
animate={{ opacity: 1, y: 0 }}
|
| 145 |
+
transition={{ delay: 0.15 }}
|
| 146 |
+
className="card-static"
|
| 147 |
+
>
|
| 148 |
+
<h3 className="font-display text-lg font-semibold text-light-100 mb-3">Voice</h3>
|
| 149 |
+
<div className="space-y-2 text-sm">
|
| 150 |
+
<div className="flex justify-between">
|
| 151 |
+
<span className="text-light-500">Type</span>
|
| 152 |
+
<span className="text-light-300">{video.voice?.type || 'neutral'}</span>
|
| 153 |
+
</div>
|
| 154 |
+
<div className="flex justify-between">
|
| 155 |
+
<span className="text-light-500">Language</span>
|
| 156 |
+
<span className="text-light-300">{video.voice?.language || 'en'}</span>
|
| 157 |
+
</div>
|
| 158 |
+
<div className="flex justify-between">
|
| 159 |
+
<span className="text-light-500">Tone</span>
|
| 160 |
+
<span className="text-light-300">{video.voice?.tone || 'professional'}</span>
|
| 161 |
+
</div>
|
| 162 |
+
<div className="flex justify-between">
|
| 163 |
+
<span className="text-light-500">Speed</span>
|
| 164 |
+
<span className="text-light-300">{video.voice?.speed || 1.0}x</span>
|
| 165 |
+
</div>
|
| 166 |
+
</div>
|
| 167 |
+
</motion.div>
|
| 168 |
+
|
| 169 |
+
{/* Export */}
|
| 170 |
+
<motion.div
|
| 171 |
+
initial={{ opacity: 0, y: 20 }}
|
| 172 |
+
animate={{ opacity: 1, y: 0 }}
|
| 173 |
+
transition={{ delay: 0.2 }}
|
| 174 |
+
className="card-static"
|
| 175 |
+
>
|
| 176 |
+
<h3 className="font-display text-lg font-semibold text-light-100 mb-3">Export Options</h3>
|
| 177 |
+
<div className="space-y-3 mb-4">
|
| 178 |
+
{(['9:16', '16:9', '1:1'] as const).map((fmt) => (
|
| 179 |
+
<label key={fmt} className="flex items-center gap-3 text-sm cursor-pointer">
|
| 180 |
+
<input
|
| 181 |
+
type="checkbox"
|
| 182 |
+
checked={exportFormats[fmt]}
|
| 183 |
+
onChange={(e) => setExportFormats({ ...exportFormats, [fmt]: e.target.checked })}
|
| 184 |
+
className="accent-gold-500"
|
| 185 |
+
/>
|
| 186 |
+
<span className="text-light-300">{fmt} {fmt === '9:16' ? '(Vertical)' : fmt === '16:9' ? '(Landscape)' : '(Square)'}</span>
|
| 187 |
+
</label>
|
| 188 |
+
))}
|
| 189 |
+
</div>
|
| 190 |
+
<button
|
| 191 |
+
className="btn-primary w-full text-sm"
|
| 192 |
+
disabled={video.status !== 'preview'}
|
| 193 |
+
id="export-btn"
|
| 194 |
+
>
|
| 195 |
+
Export Selected Formats
|
| 196 |
+
</button>
|
| 197 |
+
</motion.div>
|
| 198 |
+
</div>
|
| 199 |
+
</div>
|
| 200 |
+
</div>
|
| 201 |
+
</div>
|
| 202 |
+
);
|
| 203 |
+
}
|
client/src/pages/Privacy.tsx
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { motion } from 'framer-motion';
|
| 2 |
+
import { Link } from 'react-router-dom';
|
| 3 |
+
|
| 4 |
+
export default function Privacy() {
|
| 5 |
+
return (
|
| 6 |
+
<div className="min-h-screen pt-24 pb-16 px-6">
|
| 7 |
+
<motion.div
|
| 8 |
+
initial={{ opacity: 0, y: 20 }}
|
| 9 |
+
animate={{ opacity: 1, y: 0 }}
|
| 10 |
+
className="max-w-3xl mx-auto"
|
| 11 |
+
>
|
| 12 |
+
<Link to="/" className="text-sm text-light-500 hover:text-gold-500 transition-colors mb-6 inline-block">
|
| 13 |
+
Back to Home
|
| 14 |
+
</Link>
|
| 15 |
+
|
| 16 |
+
<h1 className="font-display text-4xl font-bold text-light-100 mb-8">Privacy Policy</h1>
|
| 17 |
+
|
| 18 |
+
<div className="prose prose-invert max-w-none space-y-6 text-light-400 font-body leading-relaxed">
|
| 19 |
+
<p><strong className="text-light-200">Last updated:</strong> March 2026</p>
|
| 20 |
+
|
| 21 |
+
<h2 className="font-display text-2xl font-semibold text-light-100 mt-10">1. Information We Collect</h2>
|
| 22 |
+
<p>We collect information you provide directly: email address, password (encrypted), project data, uploaded assets, and video generation preferences. We also collect usage data including session duration, feature usage patterns, and error logs for improving our service.</p>
|
| 23 |
+
|
| 24 |
+
<h2 className="font-display text-2xl font-semibold text-light-100 mt-10">2. How We Use Your Information</h2>
|
| 25 |
+
<p>Your information is used to provide and improve the Director.AI service, process video generation requests, authenticate your account, send essential service notifications, and maintain security. We do not sell your personal data to third parties.</p>
|
| 26 |
+
|
| 27 |
+
<h2 className="font-display text-2xl font-semibold text-light-100 mt-10">3. Data Storage and Security</h2>
|
| 28 |
+
<p>All data is stored on secure servers with encryption at rest and in transit. Passwords are hashed using bcrypt with a cost factor of 12. Uploaded assets and generated videos are stored in isolated user-specific directories.</p>
|
| 29 |
+
|
| 30 |
+
<h2 className="font-display text-2xl font-semibold text-light-100 mt-10">4. Data Retention</h2>
|
| 31 |
+
<p>Account data is retained as long as your account is active. You may request deletion of your account and all associated data at any time by contacting support. Generated videos and uploaded assets are retained for 90 days after account deletion.</p>
|
| 32 |
+
|
| 33 |
+
<h2 className="font-display text-2xl font-semibold text-light-100 mt-10">5. Third-Party Services</h2>
|
| 34 |
+
<p>Director.AI may integrate with payment processors (Stripe) for subscription management. These services have their own privacy policies. No personal data is shared with AI model providers beyond what is necessary for video generation.</p>
|
| 35 |
+
|
| 36 |
+
<h2 className="font-display text-2xl font-semibold text-light-100 mt-10">6. Contact</h2>
|
| 37 |
+
<p>For privacy-related inquiries, contact the development team through the project repository.</p>
|
| 38 |
+
</div>
|
| 39 |
+
</motion.div>
|
| 40 |
+
</div>
|
| 41 |
+
);
|
| 42 |
+
}
|
client/src/pages/ProjectPage.tsx
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect, useState } from 'react';
|
| 2 |
+
import { useParams, Link, useNavigate } from 'react-router-dom';
|
| 3 |
+
import { useDispatch, useSelector } from 'react-redux';
|
| 4 |
+
import { motion } from 'framer-motion';
|
| 5 |
+
import { AppDispatch, RootState } from '../store';
|
| 6 |
+
import { fetchProject } from '../store/projectsSlice';
|
| 7 |
+
import AITerminal from '../components/AITerminal';
|
| 8 |
+
|
| 9 |
+
export default function ProjectPage() {
|
| 10 |
+
const { id } = useParams<{ id: string }>();
|
| 11 |
+
const dispatch = useDispatch<AppDispatch>();
|
| 12 |
+
const { current: project } = useSelector((state: RootState) => state.projects);
|
| 13 |
+
const [showTerminal, setShowTerminal] = useState(false);
|
| 14 |
+
const navigate = useNavigate();
|
| 15 |
+
|
| 16 |
+
useEffect(() => {
|
| 17 |
+
if (id) dispatch(fetchProject(id));
|
| 18 |
+
}, [id, dispatch]);
|
| 19 |
+
|
| 20 |
+
if (!project) {
|
| 21 |
+
return (
|
| 22 |
+
<div className="min-h-screen pt-20 flex items-center justify-center">
|
| 23 |
+
<div className="w-8 h-8 border-2 border-gold-500 border-t-transparent rounded-full animate-spin" />
|
| 24 |
+
</div>
|
| 25 |
+
);
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
return (
|
| 29 |
+
<div className="min-h-screen pt-20 pb-12 px-6">
|
| 30 |
+
<div className="max-w-6xl mx-auto">
|
| 31 |
+
{/* Breadcrumb */}
|
| 32 |
+
<div className="flex items-center gap-2 text-sm text-light-500 mb-6">
|
| 33 |
+
<Link to="/dashboard" className="hover:text-gold-500 transition-colors">Dashboard</Link>
|
| 34 |
+
<span>/</span>
|
| 35 |
+
<span className="text-light-300">{project.name}</span>
|
| 36 |
+
</div>
|
| 37 |
+
|
| 38 |
+
{/* Header */}
|
| 39 |
+
<motion.div
|
| 40 |
+
initial={{ opacity: 0, y: 20 }}
|
| 41 |
+
animate={{ opacity: 1, y: 0 }}
|
| 42 |
+
className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-8"
|
| 43 |
+
>
|
| 44 |
+
<div>
|
| 45 |
+
<h1 className="font-display text-3xl font-bold text-light-100 mb-1">{project.name}</h1>
|
| 46 |
+
<p className="text-light-500">
|
| 47 |
+
{project.defaultPlatform} -- {project.defaultFormat} -- {project.videos?.length || 0} videos
|
| 48 |
+
</p>
|
| 49 |
+
</div>
|
| 50 |
+
<div className="flex gap-3">
|
| 51 |
+
<button onClick={() => setShowTerminal(!showTerminal)} className="btn-ghost text-sm">
|
| 52 |
+
<span className="mr-1 font-mono">>_</span> Terminal
|
| 53 |
+
</button>
|
| 54 |
+
<button
|
| 55 |
+
onClick={() => navigate(`/project/${id}/create`)}
|
| 56 |
+
className="btn-primary"
|
| 57 |
+
id="create-video-btn"
|
| 58 |
+
>
|
| 59 |
+
Create Video
|
| 60 |
+
</button>
|
| 61 |
+
</div>
|
| 62 |
+
</motion.div>
|
| 63 |
+
|
| 64 |
+
{/* Terminal */}
|
| 65 |
+
{showTerminal && (
|
| 66 |
+
<motion.div
|
| 67 |
+
initial={{ opacity: 0, height: 0 }}
|
| 68 |
+
animate={{ opacity: 1, height: 'auto' }}
|
| 69 |
+
className="mb-8 overflow-hidden"
|
| 70 |
+
>
|
| 71 |
+
<AITerminal />
|
| 72 |
+
</motion.div>
|
| 73 |
+
)}
|
| 74 |
+
|
| 75 |
+
{/* Videos */}
|
| 76 |
+
{(!project.videos || project.videos.length === 0) ? (
|
| 77 |
+
<motion.div
|
| 78 |
+
initial={{ opacity: 0 }}
|
| 79 |
+
animate={{ opacity: 1 }}
|
| 80 |
+
className="card-static text-center py-16"
|
| 81 |
+
>
|
| 82 |
+
<h3 className="font-display text-xl text-light-100 mb-2">No videos yet</h3>
|
| 83 |
+
<p className="text-light-500 mb-6">Create your first video in this project</p>
|
| 84 |
+
<button onClick={() => navigate(`/project/${id}/create`)} className="btn-primary">
|
| 85 |
+
Create First Video
|
| 86 |
+
</button>
|
| 87 |
+
</motion.div>
|
| 88 |
+
) : (
|
| 89 |
+
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
| 90 |
+
{project.videos.map((video: any, i: number) => (
|
| 91 |
+
<motion.div
|
| 92 |
+
key={video._id || i}
|
| 93 |
+
initial={{ opacity: 0, y: 20 }}
|
| 94 |
+
animate={{ opacity: 1, y: 0 }}
|
| 95 |
+
transition={{ delay: i * 0.05 }}
|
| 96 |
+
>
|
| 97 |
+
<Link to={`/video/${video._id}/preview`} className="card block group">
|
| 98 |
+
<div className="flex items-center justify-between mb-3">
|
| 99 |
+
<span className={`text-xs px-2 py-1 rounded font-medium ${video.status === 'exported' ? 'bg-green-500/10 text-green-400' :
|
| 100 |
+
video.status === 'generating' ? 'bg-gold-500/10 text-gold-500' :
|
| 101 |
+
video.status === 'failed' ? 'bg-red-500/10 text-red-400' :
|
| 102 |
+
'bg-dark-500 text-light-500'
|
| 103 |
+
}`}>
|
| 104 |
+
{video.status || 'pending'}
|
| 105 |
+
</span>
|
| 106 |
+
</div>
|
| 107 |
+
<p className="text-sm text-light-400 line-clamp-3">
|
| 108 |
+
{video.script?.slice(0, 120) || 'No script yet'}...
|
| 109 |
+
</p>
|
| 110 |
+
</Link>
|
| 111 |
+
</motion.div>
|
| 112 |
+
))}
|
| 113 |
+
</div>
|
| 114 |
+
)}
|
| 115 |
+
</div>
|
| 116 |
+
</div>
|
| 117 |
+
);
|
| 118 |
+
}
|
client/src/pages/Register.tsx
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, FormEvent } from 'react';
|
| 2 |
+
import { useDispatch, useSelector } from 'react-redux';
|
| 3 |
+
import { Link, useNavigate } from 'react-router-dom';
|
| 4 |
+
import { motion } from 'framer-motion';
|
| 5 |
+
import { AppDispatch, RootState } from '../store';
|
| 6 |
+
import { registerUser, clearError } from '../store/authSlice';
|
| 7 |
+
|
| 8 |
+
export default function Register() {
|
| 9 |
+
const [email, setEmail] = useState('');
|
| 10 |
+
const [password, setPassword] = useState('');
|
| 11 |
+
const [confirmPassword, setConfirmPassword] = useState('');
|
| 12 |
+
const [localError, setLocalError] = useState('');
|
| 13 |
+
const dispatch = useDispatch<AppDispatch>();
|
| 14 |
+
const navigate = useNavigate();
|
| 15 |
+
const { loading, error } = useSelector((state: RootState) => state.auth);
|
| 16 |
+
|
| 17 |
+
const handleSubmit = async (e: FormEvent) => {
|
| 18 |
+
e.preventDefault();
|
| 19 |
+
setLocalError('');
|
| 20 |
+
|
| 21 |
+
if (password !== confirmPassword) {
|
| 22 |
+
setLocalError('Passwords do not match.');
|
| 23 |
+
return;
|
| 24 |
+
}
|
| 25 |
+
if (password.length < 8) {
|
| 26 |
+
setLocalError('Password must be at least 8 characters.');
|
| 27 |
+
return;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
const result = await dispatch(registerUser({ email, password }));
|
| 31 |
+
if (registerUser.fulfilled.match(result)) {
|
| 32 |
+
navigate('/dashboard');
|
| 33 |
+
}
|
| 34 |
+
};
|
| 35 |
+
|
| 36 |
+
const displayError = localError || error;
|
| 37 |
+
|
| 38 |
+
return (
|
| 39 |
+
<div className="min-h-screen flex items-center justify-center pt-16 px-6">
|
| 40 |
+
<div className="absolute inset-0 bg-hero-gradient opacity-50" />
|
| 41 |
+
<motion.div
|
| 42 |
+
initial={{ opacity: 0, y: 20 }}
|
| 43 |
+
animate={{ opacity: 1, y: 0 }}
|
| 44 |
+
transition={{ duration: 0.5 }}
|
| 45 |
+
className="relative w-full max-w-md"
|
| 46 |
+
>
|
| 47 |
+
<div className="glass-panel p-8">
|
| 48 |
+
<div className="text-center mb-8">
|
| 49 |
+
<h1 className="font-display text-3xl font-bold text-light-100 mb-2">Create Account</h1>
|
| 50 |
+
<p className="font-body text-light-500">Start creating faceless videos today</p>
|
| 51 |
+
</div>
|
| 52 |
+
|
| 53 |
+
{displayError && (
|
| 54 |
+
<div className="mb-6 p-3 bg-red-500/10 border border-red-500/30 rounded-lg text-red-400 text-sm text-center">
|
| 55 |
+
{displayError}
|
| 56 |
+
</div>
|
| 57 |
+
)}
|
| 58 |
+
|
| 59 |
+
<form onSubmit={handleSubmit} className="space-y-5">
|
| 60 |
+
<div>
|
| 61 |
+
<label htmlFor="register-email" className="block text-sm font-body text-light-400 mb-2">Email</label>
|
| 62 |
+
<input
|
| 63 |
+
id="register-email"
|
| 64 |
+
type="email"
|
| 65 |
+
value={email}
|
| 66 |
+
onChange={(e) => { setEmail(e.target.value); dispatch(clearError()); setLocalError(''); }}
|
| 67 |
+
className="input-field"
|
| 68 |
+
placeholder="you@example.com"
|
| 69 |
+
required
|
| 70 |
+
/>
|
| 71 |
+
</div>
|
| 72 |
+
|
| 73 |
+
<div>
|
| 74 |
+
<label htmlFor="register-password" className="block text-sm font-body text-light-400 mb-2">Password</label>
|
| 75 |
+
<input
|
| 76 |
+
id="register-password"
|
| 77 |
+
type="password"
|
| 78 |
+
value={password}
|
| 79 |
+
onChange={(e) => { setPassword(e.target.value); setLocalError(''); }}
|
| 80 |
+
className="input-field"
|
| 81 |
+
placeholder="Minimum 8 characters"
|
| 82 |
+
required
|
| 83 |
+
/>
|
| 84 |
+
</div>
|
| 85 |
+
|
| 86 |
+
<div>
|
| 87 |
+
<label htmlFor="register-confirm" className="block text-sm font-body text-light-400 mb-2">Confirm Password</label>
|
| 88 |
+
<input
|
| 89 |
+
id="register-confirm"
|
| 90 |
+
type="password"
|
| 91 |
+
value={confirmPassword}
|
| 92 |
+
onChange={(e) => { setConfirmPassword(e.target.value); setLocalError(''); }}
|
| 93 |
+
className="input-field"
|
| 94 |
+
placeholder="Re-enter your password"
|
| 95 |
+
required
|
| 96 |
+
/>
|
| 97 |
+
</div>
|
| 98 |
+
|
| 99 |
+
<button
|
| 100 |
+
type="submit"
|
| 101 |
+
disabled={loading}
|
| 102 |
+
className="btn-primary w-full py-3.5"
|
| 103 |
+
>
|
| 104 |
+
{loading ? 'Creating account...' : 'Create Account'}
|
| 105 |
+
</button>
|
| 106 |
+
</form>
|
| 107 |
+
|
| 108 |
+
<p className="mt-6 text-center text-sm text-light-500">
|
| 109 |
+
Already have an account?{' '}
|
| 110 |
+
<Link to="/login" className="text-gold-500 hover:text-gold-400 font-medium transition-colors">
|
| 111 |
+
Sign in
|
| 112 |
+
</Link>
|
| 113 |
+
</p>
|
| 114 |
+
|
| 115 |
+
<p className="mt-4 text-center text-xs text-light-500/50">
|
| 116 |
+
By creating an account, you agree to our{' '}
|
| 117 |
+
<Link to="/terms" className="text-light-500/70 hover:text-gold-500 transition-colors">Terms</Link>
|
| 118 |
+
{' '}and{' '}
|
| 119 |
+
<Link to="/privacy" className="text-light-500/70 hover:text-gold-500 transition-colors">Privacy Policy</Link>.
|
| 120 |
+
</p>
|
| 121 |
+
</div>
|
| 122 |
+
</motion.div>
|
| 123 |
+
</div>
|
| 124 |
+
);
|
| 125 |
+
}
|
client/src/pages/Terms.tsx
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { motion } from 'framer-motion';
|
| 2 |
+
import { Link } from 'react-router-dom';
|
| 3 |
+
|
| 4 |
+
export default function Terms() {
|
| 5 |
+
return (
|
| 6 |
+
<div className="min-h-screen pt-24 pb-16 px-6">
|
| 7 |
+
<motion.div
|
| 8 |
+
initial={{ opacity: 0, y: 20 }}
|
| 9 |
+
animate={{ opacity: 1, y: 0 }}
|
| 10 |
+
className="max-w-3xl mx-auto"
|
| 11 |
+
>
|
| 12 |
+
<Link to="/" className="text-sm text-light-500 hover:text-gold-500 transition-colors mb-6 inline-block">
|
| 13 |
+
Back to Home
|
| 14 |
+
</Link>
|
| 15 |
+
|
| 16 |
+
<h1 className="font-display text-4xl font-bold text-light-100 mb-8">Terms of Use</h1>
|
| 17 |
+
|
| 18 |
+
<div className="prose prose-invert max-w-none space-y-6 text-light-400 font-body leading-relaxed">
|
| 19 |
+
<p><strong className="text-light-200">Last updated:</strong> March 2026</p>
|
| 20 |
+
|
| 21 |
+
<h2 className="font-display text-2xl font-semibold text-light-100 mt-10">1. Acceptance of Terms</h2>
|
| 22 |
+
<p>By accessing or using Director.AI, you agree to be bound by these Terms of Use. If you do not agree to all terms, do not use the service.</p>
|
| 23 |
+
|
| 24 |
+
<h2 className="font-display text-2xl font-semibold text-light-100 mt-10">2. Description of Service</h2>
|
| 25 |
+
<p>Director.AI is a web-based platform for creating faceless videos using AI-powered tools. The service includes script-to-video conversion, AI voice generation, subtitle creation, and multi-format video export.</p>
|
| 26 |
+
|
| 27 |
+
<h2 className="font-display text-2xl font-semibold text-light-100 mt-10">3. User Accounts</h2>
|
| 28 |
+
<p>You must provide a valid email address and maintain the security of your account credentials. You are responsible for all activity under your account. Sharing account access is not permitted without prior authorization.</p>
|
| 29 |
+
|
| 30 |
+
<h2 className="font-display text-2xl font-semibold text-light-100 mt-10">4. Content and Ownership</h2>
|
| 31 |
+
<p>You retain ownership of scripts, assets, and other content you provide. Videos generated through the platform are owned by you. Director.AI does not claim any rights to user-generated content. You are responsible for ensuring your content does not infringe on third-party rights.</p>
|
| 32 |
+
|
| 33 |
+
<h2 className="font-display text-2xl font-semibold text-light-100 mt-10">5. Acceptable Use</h2>
|
| 34 |
+
<p>You agree not to use Director.AI for generating content that is illegal, harmful, defamatory, or infringes on the rights of others. Abuse of the AI generation system, including attempts to circumvent rate limits or usage quotas, may result in account suspension.</p>
|
| 35 |
+
|
| 36 |
+
<h2 className="font-display text-2xl font-semibold text-light-100 mt-10">6. Subscription and Payments</h2>
|
| 37 |
+
<p>Free tier users receive a limited number of video generations per month. Paid subscriptions unlock additional features and higher usage limits. All payments are processed through Stripe. Subscriptions can be cancelled at any time.</p>
|
| 38 |
+
|
| 39 |
+
<h2 className="font-display text-2xl font-semibold text-light-100 mt-10">7. Limitation of Liability</h2>
|
| 40 |
+
<p>Director.AI is provided "as is" without warranties of any kind. We are not liable for any damages arising from the use or inability to use the service, including lost profits or data loss.</p>
|
| 41 |
+
|
| 42 |
+
<h2 className="font-display text-2xl font-semibold text-light-100 mt-10">8. Changes to Terms</h2>
|
| 43 |
+
<p>We reserve the right to modify these terms at any time. Continued use of the service after changes constitutes acceptance of the updated terms.</p>
|
| 44 |
+
</div>
|
| 45 |
+
</motion.div>
|
| 46 |
+
</div>
|
| 47 |
+
);
|
| 48 |
+
}
|
client/src/pages/VideoCreate.tsx
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useCallback } from 'react';
|
| 2 |
+
import { useParams, useNavigate } from 'react-router-dom';
|
| 3 |
+
import { useDispatch } from 'react-redux';
|
| 4 |
+
import { motion, AnimatePresence } from 'framer-motion';
|
| 5 |
+
import { AppDispatch } from '../store';
|
| 6 |
+
import { createVideo } from '../store/videosSlice';
|
| 7 |
+
import AITerminal from '../components/AITerminal';
|
| 8 |
+
|
| 9 |
+
const STEPS = [
|
| 10 |
+
{ num: 1, title: 'Script', desc: 'Paste or write your video script' },
|
| 11 |
+
{ num: 2, title: 'Voice', desc: 'Choose voice settings' },
|
| 12 |
+
{ num: 3, title: 'Music', desc: 'Select background music' },
|
| 13 |
+
{ num: 4, title: 'Style', desc: 'Set visual preferences' },
|
| 14 |
+
{ num: 5, title: 'Assets', desc: 'Upload media files' },
|
| 15 |
+
];
|
| 16 |
+
|
| 17 |
+
export default function VideoCreate() {
|
| 18 |
+
const { projectId } = useParams<{ projectId: string }>();
|
| 19 |
+
const dispatch = useDispatch<AppDispatch>();
|
| 20 |
+
const navigate = useNavigate();
|
| 21 |
+
const [step, setStep] = useState(1);
|
| 22 |
+
const [showTerminal, setShowTerminal] = useState(false);
|
| 23 |
+
const [submitting, setSubmitting] = useState(false);
|
| 24 |
+
|
| 25 |
+
// Form state
|
| 26 |
+
const [script, setScript] = useState('');
|
| 27 |
+
const [voice, setVoice] = useState({
|
| 28 |
+
type: 'neutral',
|
| 29 |
+
language: 'en',
|
| 30 |
+
tone: 'professional',
|
| 31 |
+
speed: 1.0,
|
| 32 |
+
});
|
| 33 |
+
const [music, setMusic] = useState('');
|
| 34 |
+
const [style, setStyle] = useState({
|
| 35 |
+
fonts: { primary: 'Playfair Display', secondary: 'Montserrat' },
|
| 36 |
+
colors: { primary: '#1A1A1A', secondary: '#F5F5F5', accent: '#D4AF37' },
|
| 37 |
+
transitions: 'fade',
|
| 38 |
+
videoStyle: 'minimal',
|
| 39 |
+
});
|
| 40 |
+
const [files, setFiles] = useState<File[]>([]);
|
| 41 |
+
|
| 42 |
+
const next = () => setStep((s) => Math.min(s + 1, 5));
|
| 43 |
+
const prev = () => setStep((s) => Math.max(s - 1, 1));
|
| 44 |
+
|
| 45 |
+
const handleFileChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
| 46 |
+
if (e.target.files) {
|
| 47 |
+
setFiles(Array.from(e.target.files));
|
| 48 |
+
}
|
| 49 |
+
}, []);
|
| 50 |
+
|
| 51 |
+
const handleGenerate = async () => {
|
| 52 |
+
if (!projectId || !script.trim()) return;
|
| 53 |
+
setSubmitting(true);
|
| 54 |
+
|
| 55 |
+
const videoData = {
|
| 56 |
+
script,
|
| 57 |
+
voice,
|
| 58 |
+
music: { filePath: music },
|
| 59 |
+
assets: files.map((f) => ({ type: 'clip' as const, path: f.name })),
|
| 60 |
+
};
|
| 61 |
+
|
| 62 |
+
const result = await dispatch(createVideo({ projectId, data: videoData }));
|
| 63 |
+
setSubmitting(false);
|
| 64 |
+
|
| 65 |
+
if (createVideo.fulfilled.match(result)) {
|
| 66 |
+
navigate(`/video/${result.payload._id}/preview`);
|
| 67 |
+
}
|
| 68 |
+
};
|
| 69 |
+
|
| 70 |
+
return (
|
| 71 |
+
<div className="min-h-screen pt-20 pb-12 px-6">
|
| 72 |
+
<div className="max-w-4xl mx-auto">
|
| 73 |
+
{/* Stepper */}
|
| 74 |
+
<div className="flex items-center justify-between mb-12 relative">
|
| 75 |
+
<div className="absolute top-5 left-0 right-0 h-px bg-dark-400/30" />
|
| 76 |
+
{STEPS.map((s) => (
|
| 77 |
+
<div
|
| 78 |
+
key={s.num}
|
| 79 |
+
className="relative flex flex-col items-center cursor-pointer z-10"
|
| 80 |
+
onClick={() => setStep(s.num)}
|
| 81 |
+
>
|
| 82 |
+
<div className={`w-10 h-10 rounded-full flex items-center justify-center font-body font-semibold text-sm transition-all duration-300 ${step === s.num
|
| 83 |
+
? 'bg-gold-500 text-dark-900 shadow-gold'
|
| 84 |
+
: step > s.num
|
| 85 |
+
? 'bg-gold-500/20 text-gold-500 border border-gold-500/40'
|
| 86 |
+
: 'bg-dark-600 text-light-500 border border-dark-400/30'
|
| 87 |
+
}`}>
|
| 88 |
+
{step > s.num ? (
|
| 89 |
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 90 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
| 91 |
+
</svg>
|
| 92 |
+
) : s.num}
|
| 93 |
+
</div>
|
| 94 |
+
<span className={`mt-2 text-xs font-body transition-colors ${step === s.num ? 'text-gold-500' : 'text-light-500'
|
| 95 |
+
}`}>
|
| 96 |
+
{s.title}
|
| 97 |
+
</span>
|
| 98 |
+
</div>
|
| 99 |
+
))}
|
| 100 |
+
</div>
|
| 101 |
+
|
| 102 |
+
{/* Step Content */}
|
| 103 |
+
<AnimatePresence mode="wait">
|
| 104 |
+
<motion.div
|
| 105 |
+
key={step}
|
| 106 |
+
initial={{ opacity: 0, x: 20 }}
|
| 107 |
+
animate={{ opacity: 1, x: 0 }}
|
| 108 |
+
exit={{ opacity: 0, x: -20 }}
|
| 109 |
+
transition={{ duration: 0.3 }}
|
| 110 |
+
className="glass-panel p-8 mb-8"
|
| 111 |
+
>
|
| 112 |
+
{step === 1 && (
|
| 113 |
+
<div>
|
| 114 |
+
<h2 className="font-display text-2xl font-bold text-light-100 mb-2">Your Script</h2>
|
| 115 |
+
<p className="text-light-500 mb-6">Paste your video script or write it directly. This will be used for voiceover and subtitles.</p>
|
| 116 |
+
<textarea
|
| 117 |
+
value={script}
|
| 118 |
+
onChange={(e) => setScript(e.target.value)}
|
| 119 |
+
className="input-field h-48 resize-none"
|
| 120 |
+
placeholder="Paste your video script here... Example: Did you know that 90% of successful content creators use faceless videos? Here is why this strategy works and how you can start today..."
|
| 121 |
+
id="script-input"
|
| 122 |
+
/>
|
| 123 |
+
<p className="mt-2 text-xs text-light-500/60">{script.length} characters</p>
|
| 124 |
+
</div>
|
| 125 |
+
)}
|
| 126 |
+
|
| 127 |
+
{step === 2 && (
|
| 128 |
+
<div>
|
| 129 |
+
<h2 className="font-display text-2xl font-bold text-light-100 mb-2">Voice Settings</h2>
|
| 130 |
+
<p className="text-light-500 mb-6">Configure the AI voiceover for your video.</p>
|
| 131 |
+
<div className="grid sm:grid-cols-2 gap-5">
|
| 132 |
+
<div>
|
| 133 |
+
<label className="block text-sm text-light-400 mb-2">Voice Type</label>
|
| 134 |
+
<select value={voice.type} onChange={(e) => setVoice({ ...voice, type: e.target.value })} className="input-field" id="voice-type">
|
| 135 |
+
<option value="neutral">Neutral</option>
|
| 136 |
+
<option value="male">Male</option>
|
| 137 |
+
<option value="female">Female</option>
|
| 138 |
+
<option value="deep">Deep</option>
|
| 139 |
+
<option value="energetic">Energetic</option>
|
| 140 |
+
</select>
|
| 141 |
+
</div>
|
| 142 |
+
<div>
|
| 143 |
+
<label className="block text-sm text-light-400 mb-2">Language</label>
|
| 144 |
+
<select value={voice.language} onChange={(e) => setVoice({ ...voice, language: e.target.value })} className="input-field" id="voice-language">
|
| 145 |
+
<option value="en">English</option>
|
| 146 |
+
<option value="es">Spanish</option>
|
| 147 |
+
<option value="fr">French</option>
|
| 148 |
+
<option value="de">German</option>
|
| 149 |
+
<option value="pt">Portuguese</option>
|
| 150 |
+
<option value="ja">Japanese</option>
|
| 151 |
+
</select>
|
| 152 |
+
</div>
|
| 153 |
+
<div>
|
| 154 |
+
<label className="block text-sm text-light-400 mb-2">Tone</label>
|
| 155 |
+
<select value={voice.tone} onChange={(e) => setVoice({ ...voice, tone: e.target.value })} className="input-field" id="voice-tone">
|
| 156 |
+
<option value="professional">Professional</option>
|
| 157 |
+
<option value="casual">Casual</option>
|
| 158 |
+
<option value="dramatic">Dramatic</option>
|
| 159 |
+
<option value="upbeat">Upbeat</option>
|
| 160 |
+
<option value="calm">Calm</option>
|
| 161 |
+
</select>
|
| 162 |
+
</div>
|
| 163 |
+
<div>
|
| 164 |
+
<label className="block text-sm text-light-400 mb-2">Speed: {voice.speed}x</label>
|
| 165 |
+
<input
|
| 166 |
+
type="range"
|
| 167 |
+
min="0.5"
|
| 168 |
+
max="2.0"
|
| 169 |
+
step="0.1"
|
| 170 |
+
value={voice.speed}
|
| 171 |
+
onChange={(e) => setVoice({ ...voice, speed: parseFloat(e.target.value) })}
|
| 172 |
+
className="w-full accent-gold-500"
|
| 173 |
+
id="voice-speed"
|
| 174 |
+
/>
|
| 175 |
+
</div>
|
| 176 |
+
</div>
|
| 177 |
+
</div>
|
| 178 |
+
)}
|
| 179 |
+
|
| 180 |
+
{step === 3 && (
|
| 181 |
+
<div>
|
| 182 |
+
<h2 className="font-display text-2xl font-bold text-light-100 mb-2">Background Music</h2>
|
| 183 |
+
<p className="text-light-500 mb-6">Choose music for your video or upload your own track.</p>
|
| 184 |
+
<div className="space-y-3">
|
| 185 |
+
{['None', 'Ambient Calm', 'Upbeat Energy', 'Corporate', 'Cinematic', 'Lo-Fi Chill'].map((track) => (
|
| 186 |
+
<label
|
| 187 |
+
key={track}
|
| 188 |
+
className={`card-static flex items-center gap-4 cursor-pointer transition-all ${music === track ? 'border-gold-500/40 shadow-gold' : ''
|
| 189 |
+
}`}
|
| 190 |
+
>
|
| 191 |
+
<input
|
| 192 |
+
type="radio"
|
| 193 |
+
name="music"
|
| 194 |
+
value={track}
|
| 195 |
+
checked={music === track}
|
| 196 |
+
onChange={(e) => setMusic(e.target.value)}
|
| 197 |
+
className="accent-gold-500"
|
| 198 |
+
/>
|
| 199 |
+
<span className="text-light-300">{track}</span>
|
| 200 |
+
</label>
|
| 201 |
+
))}
|
| 202 |
+
</div>
|
| 203 |
+
</div>
|
| 204 |
+
)}
|
| 205 |
+
|
| 206 |
+
{step === 4 && (
|
| 207 |
+
<div>
|
| 208 |
+
<h2 className="font-display text-2xl font-bold text-light-100 mb-2">Visual Style</h2>
|
| 209 |
+
<p className="text-light-500 mb-6">Customize the look and feel of your video.</p>
|
| 210 |
+
<div className="grid sm:grid-cols-2 gap-5">
|
| 211 |
+
<div>
|
| 212 |
+
<label className="block text-sm text-light-400 mb-2">Video Style</label>
|
| 213 |
+
<select
|
| 214 |
+
value={style.videoStyle}
|
| 215 |
+
onChange={(e) => setStyle({ ...style, videoStyle: e.target.value })}
|
| 216 |
+
className="input-field"
|
| 217 |
+
id="video-style"
|
| 218 |
+
>
|
| 219 |
+
<option value="minimal">Minimal</option>
|
| 220 |
+
<option value="dynamic">Dynamic</option>
|
| 221 |
+
<option value="cinematic">Cinematic</option>
|
| 222 |
+
<option value="bold">Bold</option>
|
| 223 |
+
<option value="elegant">Elegant</option>
|
| 224 |
+
</select>
|
| 225 |
+
</div>
|
| 226 |
+
<div>
|
| 227 |
+
<label className="block text-sm text-light-400 mb-2">Transitions</label>
|
| 228 |
+
<select
|
| 229 |
+
value={style.transitions}
|
| 230 |
+
onChange={(e) => setStyle({ ...style, transitions: e.target.value })}
|
| 231 |
+
className="input-field"
|
| 232 |
+
id="transitions"
|
| 233 |
+
>
|
| 234 |
+
<option value="fade">Fade</option>
|
| 235 |
+
<option value="slide">Slide</option>
|
| 236 |
+
<option value="zoom">Zoom</option>
|
| 237 |
+
<option value="dissolve">Dissolve</option>
|
| 238 |
+
<option value="none">None</option>
|
| 239 |
+
</select>
|
| 240 |
+
</div>
|
| 241 |
+
<div>
|
| 242 |
+
<label className="block text-sm text-light-400 mb-2">Accent Color</label>
|
| 243 |
+
<div className="flex items-center gap-3">
|
| 244 |
+
<input
|
| 245 |
+
type="color"
|
| 246 |
+
value={style.colors.accent}
|
| 247 |
+
onChange={(e) => setStyle({ ...style, colors: { ...style.colors, accent: e.target.value } })}
|
| 248 |
+
className="w-10 h-10 rounded border border-dark-400/30 cursor-pointer"
|
| 249 |
+
/>
|
| 250 |
+
<span className="text-sm text-light-500 font-mono">{style.colors.accent}</span>
|
| 251 |
+
</div>
|
| 252 |
+
</div>
|
| 253 |
+
<div>
|
| 254 |
+
<label className="block text-sm text-light-400 mb-2">Headline Font</label>
|
| 255 |
+
<select
|
| 256 |
+
value={style.fonts.primary}
|
| 257 |
+
onChange={(e) => setStyle({ ...style, fonts: { ...style.fonts, primary: e.target.value } })}
|
| 258 |
+
className="input-field"
|
| 259 |
+
>
|
| 260 |
+
<option value="Playfair Display">Playfair Display</option>
|
| 261 |
+
<option value="Montserrat">Montserrat</option>
|
| 262 |
+
<option value="Inter">Inter</option>
|
| 263 |
+
<option value="Roboto">Roboto</option>
|
| 264 |
+
<option value="Lora">Lora</option>
|
| 265 |
+
</select>
|
| 266 |
+
</div>
|
| 267 |
+
</div>
|
| 268 |
+
</div>
|
| 269 |
+
)}
|
| 270 |
+
|
| 271 |
+
{step === 5 && (
|
| 272 |
+
<div>
|
| 273 |
+
<h2 className="font-display text-2xl font-bold text-light-100 mb-2">Upload Assets</h2>
|
| 274 |
+
<p className="text-light-500 mb-6">Add clips, images, or logos to include in your video.</p>
|
| 275 |
+
<div className="border-2 border-dashed border-dark-400/50 rounded-xl p-8 text-center hover:border-gold-500/40 transition-colors">
|
| 276 |
+
<input
|
| 277 |
+
type="file"
|
| 278 |
+
multiple
|
| 279 |
+
onChange={handleFileChange}
|
| 280 |
+
className="hidden"
|
| 281 |
+
id="asset-upload"
|
| 282 |
+
accept="image/*,video/*,audio/*"
|
| 283 |
+
/>
|
| 284 |
+
<label htmlFor="asset-upload" className="cursor-pointer">
|
| 285 |
+
<div className="w-12 h-12 bg-gold-500/10 border border-gold-500/20 rounded-xl flex items-center justify-center mx-auto mb-4">
|
| 286 |
+
<span className="text-gold-500 text-2xl">+</span>
|
| 287 |
+
</div>
|
| 288 |
+
<p className="text-light-300 font-medium mb-1">Drag and drop or click to upload</p>
|
| 289 |
+
<p className="text-sm text-light-500">Images, videos, logos, and audio (max 100MB each)</p>
|
| 290 |
+
</label>
|
| 291 |
+
</div>
|
| 292 |
+
{files.length > 0 && (
|
| 293 |
+
<div className="mt-4 space-y-2">
|
| 294 |
+
{files.map((file, i) => (
|
| 295 |
+
<div key={i} className="flex items-center justify-between card-static py-3">
|
| 296 |
+
<span className="text-sm text-light-300">{file.name}</span>
|
| 297 |
+
<span className="text-xs text-light-500">{(file.size / 1024 / 1024).toFixed(1)} MB</span>
|
| 298 |
+
</div>
|
| 299 |
+
))}
|
| 300 |
+
</div>
|
| 301 |
+
)}
|
| 302 |
+
</div>
|
| 303 |
+
)}
|
| 304 |
+
</motion.div>
|
| 305 |
+
</AnimatePresence>
|
| 306 |
+
|
| 307 |
+
{/* Navigation + Terminal Toggle */}
|
| 308 |
+
<div className="flex items-center justify-between">
|
| 309 |
+
<button
|
| 310 |
+
onClick={prev}
|
| 311 |
+
disabled={step === 1}
|
| 312 |
+
className="btn-ghost disabled:opacity-30"
|
| 313 |
+
>
|
| 314 |
+
Previous
|
| 315 |
+
</button>
|
| 316 |
+
|
| 317 |
+
<button
|
| 318 |
+
onClick={() => setShowTerminal(!showTerminal)}
|
| 319 |
+
className="btn-ghost text-sm"
|
| 320 |
+
>
|
| 321 |
+
<span className="mr-1 font-mono">>_</span> Terminal
|
| 322 |
+
</button>
|
| 323 |
+
|
| 324 |
+
{step < 5 ? (
|
| 325 |
+
<button onClick={next} className="btn-primary">
|
| 326 |
+
Next Step
|
| 327 |
+
</button>
|
| 328 |
+
) : (
|
| 329 |
+
<button
|
| 330 |
+
onClick={handleGenerate}
|
| 331 |
+
disabled={submitting || !script.trim()}
|
| 332 |
+
className="btn-primary px-10"
|
| 333 |
+
id="generate-btn"
|
| 334 |
+
>
|
| 335 |
+
{submitting ? 'Generating...' : 'Generate Preview'}
|
| 336 |
+
</button>
|
| 337 |
+
)}
|
| 338 |
+
</div>
|
| 339 |
+
|
| 340 |
+
{/* Terminal Panel */}
|
| 341 |
+
{showTerminal && (
|
| 342 |
+
<motion.div
|
| 343 |
+
initial={{ opacity: 0, height: 0 }}
|
| 344 |
+
animate={{ opacity: 1, height: 'auto' }}
|
| 345 |
+
className="mt-8 overflow-hidden"
|
| 346 |
+
>
|
| 347 |
+
<AITerminal />
|
| 348 |
+
</motion.div>
|
| 349 |
+
)}
|
| 350 |
+
</div>
|
| 351 |
+
</div>
|
| 352 |
+
);
|
| 353 |
+
}
|
client/src/services/api.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import axios from 'axios';
|
| 2 |
+
|
| 3 |
+
const API_BASE = '/api';
|
| 4 |
+
|
| 5 |
+
const api = axios.create({
|
| 6 |
+
baseURL: API_BASE,
|
| 7 |
+
headers: { 'Content-Type': 'application/json' },
|
| 8 |
+
});
|
| 9 |
+
|
| 10 |
+
// Attach JWT token to every request
|
| 11 |
+
api.interceptors.request.use((config) => {
|
| 12 |
+
const token = localStorage.getItem('director_token');
|
| 13 |
+
if (token && config.headers) {
|
| 14 |
+
config.headers.Authorization = `Bearer ${token}`;
|
| 15 |
+
}
|
| 16 |
+
return config;
|
| 17 |
+
});
|
| 18 |
+
|
| 19 |
+
// Handle 401 responses globally
|
| 20 |
+
api.interceptors.response.use(
|
| 21 |
+
(response) => response,
|
| 22 |
+
(error) => {
|
| 23 |
+
if (error.response?.status === 401) {
|
| 24 |
+
localStorage.removeItem('director_token');
|
| 25 |
+
localStorage.removeItem('director_user');
|
| 26 |
+
window.location.href = '/login';
|
| 27 |
+
}
|
| 28 |
+
return Promise.reject(error);
|
| 29 |
+
}
|
| 30 |
+
);
|
| 31 |
+
|
| 32 |
+
// Auth
|
| 33 |
+
export const authAPI = {
|
| 34 |
+
register: (email: string, password: string) =>
|
| 35 |
+
api.post('/auth/register', { email, password }),
|
| 36 |
+
login: (email: string, password: string) =>
|
| 37 |
+
api.post('/auth/login', { email, password }),
|
| 38 |
+
};
|
| 39 |
+
|
| 40 |
+
// Projects
|
| 41 |
+
export const projectsAPI = {
|
| 42 |
+
list: () => api.get('/projects'),
|
| 43 |
+
get: (id: string) => api.get(`/projects/${id}`),
|
| 44 |
+
create: (data: { name: string; defaultPlatform: string; defaultFormat: string }) =>
|
| 45 |
+
api.post('/projects', data),
|
| 46 |
+
delete: (id: string) => api.delete(`/projects/${id}`),
|
| 47 |
+
};
|
| 48 |
+
|
| 49 |
+
// Videos
|
| 50 |
+
export const videosAPI = {
|
| 51 |
+
create: (projectId: string, data: any) =>
|
| 52 |
+
api.post(`/videos/${projectId}`, data),
|
| 53 |
+
get: (id: string) => api.get(`/videos/${id}`),
|
| 54 |
+
generate: (id: string) => api.post(`/videos/${id}/generate`),
|
| 55 |
+
status: (id: string) => api.get(`/videos/${id}/status`),
|
| 56 |
+
export: (id: string, data: { formats: string[]; quality: string }) =>
|
| 57 |
+
api.post(`/videos/${id}/export`, data),
|
| 58 |
+
};
|
| 59 |
+
|
| 60 |
+
// Presets
|
| 61 |
+
export const presetsAPI = {
|
| 62 |
+
list: () => api.get('/presets'),
|
| 63 |
+
create: (data: any) => api.post('/presets', data),
|
| 64 |
+
update: (id: string, data: any) => api.put(`/presets/${id}`, data),
|
| 65 |
+
delete: (id: string) => api.delete(`/presets/${id}`),
|
| 66 |
+
};
|
| 67 |
+
|
| 68 |
+
// Assets
|
| 69 |
+
export const assetsAPI = {
|
| 70 |
+
upload: (files: FileList | File[]) => {
|
| 71 |
+
const formData = new FormData();
|
| 72 |
+
Array.from(files).forEach((file) => formData.append('files', file));
|
| 73 |
+
return api.post('/assets/upload', formData, {
|
| 74 |
+
headers: { 'Content-Type': 'multipart/form-data' },
|
| 75 |
+
});
|
| 76 |
+
},
|
| 77 |
+
};
|
| 78 |
+
|
| 79 |
+
export default api;
|
client/src/store/authSlice.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
|
| 2 |
+
import { authAPI } from '../services/api';
|
| 3 |
+
|
| 4 |
+
interface User {
|
| 5 |
+
id: string;
|
| 6 |
+
email: string;
|
| 7 |
+
subscription: string;
|
| 8 |
+
role: string;
|
| 9 |
+
videosGenerated?: number;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
interface AuthState {
|
| 13 |
+
user: User | null;
|
| 14 |
+
token: string | null;
|
| 15 |
+
loading: boolean;
|
| 16 |
+
error: string | null;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
const savedToken = localStorage.getItem('director_token');
|
| 20 |
+
const savedUser = localStorage.getItem('director_user');
|
| 21 |
+
|
| 22 |
+
const initialState: AuthState = {
|
| 23 |
+
user: savedUser ? JSON.parse(savedUser) : null,
|
| 24 |
+
token: savedToken || null,
|
| 25 |
+
loading: false,
|
| 26 |
+
error: null,
|
| 27 |
+
};
|
| 28 |
+
|
| 29 |
+
export const registerUser = createAsyncThunk(
|
| 30 |
+
'auth/register',
|
| 31 |
+
async ({ email, password }: { email: string; password: string }, { rejectWithValue }) => {
|
| 32 |
+
try {
|
| 33 |
+
const response = await authAPI.register(email, password);
|
| 34 |
+
return response.data;
|
| 35 |
+
} catch (error: any) {
|
| 36 |
+
return rejectWithValue(error.response?.data?.error || 'Registration failed.');
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
);
|
| 40 |
+
|
| 41 |
+
export const loginUser = createAsyncThunk(
|
| 42 |
+
'auth/login',
|
| 43 |
+
async ({ email, password }: { email: string; password: string }, { rejectWithValue }) => {
|
| 44 |
+
try {
|
| 45 |
+
const response = await authAPI.login(email, password);
|
| 46 |
+
return response.data;
|
| 47 |
+
} catch (error: any) {
|
| 48 |
+
return rejectWithValue(error.response?.data?.error || 'Login failed.');
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
);
|
| 52 |
+
|
| 53 |
+
const authSlice = createSlice({
|
| 54 |
+
name: 'auth',
|
| 55 |
+
initialState,
|
| 56 |
+
reducers: {
|
| 57 |
+
logout(state) {
|
| 58 |
+
state.user = null;
|
| 59 |
+
state.token = null;
|
| 60 |
+
state.error = null;
|
| 61 |
+
localStorage.removeItem('director_token');
|
| 62 |
+
localStorage.removeItem('director_user');
|
| 63 |
+
},
|
| 64 |
+
clearError(state) {
|
| 65 |
+
state.error = null;
|
| 66 |
+
},
|
| 67 |
+
},
|
| 68 |
+
extraReducers: (builder) => {
|
| 69 |
+
builder
|
| 70 |
+
.addCase(registerUser.pending, (state) => {
|
| 71 |
+
state.loading = true;
|
| 72 |
+
state.error = null;
|
| 73 |
+
})
|
| 74 |
+
.addCase(registerUser.fulfilled, (state, action: PayloadAction<any>) => {
|
| 75 |
+
state.loading = false;
|
| 76 |
+
state.user = action.payload.user;
|
| 77 |
+
state.token = action.payload.token;
|
| 78 |
+
localStorage.setItem('director_token', action.payload.token);
|
| 79 |
+
localStorage.setItem('director_user', JSON.stringify(action.payload.user));
|
| 80 |
+
})
|
| 81 |
+
.addCase(registerUser.rejected, (state, action) => {
|
| 82 |
+
state.loading = false;
|
| 83 |
+
state.error = action.payload as string;
|
| 84 |
+
})
|
| 85 |
+
.addCase(loginUser.pending, (state) => {
|
| 86 |
+
state.loading = true;
|
| 87 |
+
state.error = null;
|
| 88 |
+
})
|
| 89 |
+
.addCase(loginUser.fulfilled, (state, action: PayloadAction<any>) => {
|
| 90 |
+
state.loading = false;
|
| 91 |
+
state.user = action.payload.user;
|
| 92 |
+
state.token = action.payload.token;
|
| 93 |
+
localStorage.setItem('director_token', action.payload.token);
|
| 94 |
+
localStorage.setItem('director_user', JSON.stringify(action.payload.user));
|
| 95 |
+
})
|
| 96 |
+
.addCase(loginUser.rejected, (state, action) => {
|
| 97 |
+
state.loading = false;
|
| 98 |
+
state.error = action.payload as string;
|
| 99 |
+
});
|
| 100 |
+
},
|
| 101 |
+
});
|
| 102 |
+
|
| 103 |
+
export const { logout, clearError } = authSlice.actions;
|
| 104 |
+
export default authSlice.reducer;
|
client/src/store/index.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { configureStore } from '@reduxjs/toolkit';
|
| 2 |
+
import authReducer from './authSlice';
|
| 3 |
+
import projectsReducer from './projectsSlice';
|
| 4 |
+
import videosReducer from './videosSlice';
|
| 5 |
+
|
| 6 |
+
export const store = configureStore({
|
| 7 |
+
reducer: {
|
| 8 |
+
auth: authReducer,
|
| 9 |
+
projects: projectsReducer,
|
| 10 |
+
videos: videosReducer,
|
| 11 |
+
},
|
| 12 |
+
});
|
| 13 |
+
|
| 14 |
+
export type RootState = ReturnType<typeof store.getState>;
|
| 15 |
+
export type AppDispatch = typeof store.dispatch;
|
client/src/store/projectsSlice.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
|
| 2 |
+
import { projectsAPI } from '../services/api';
|
| 3 |
+
|
| 4 |
+
interface Project {
|
| 5 |
+
_id: string;
|
| 6 |
+
name: string;
|
| 7 |
+
defaultPlatform: string;
|
| 8 |
+
defaultFormat: string;
|
| 9 |
+
videos: any[];
|
| 10 |
+
createdAt: string;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
interface ProjectsState {
|
| 14 |
+
items: Project[];
|
| 15 |
+
current: Project | null;
|
| 16 |
+
loading: boolean;
|
| 17 |
+
error: string | null;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
const initialState: ProjectsState = {
|
| 21 |
+
items: [],
|
| 22 |
+
current: null,
|
| 23 |
+
loading: false,
|
| 24 |
+
error: null,
|
| 25 |
+
};
|
| 26 |
+
|
| 27 |
+
export const fetchProjects = createAsyncThunk('projects/fetchAll', async (_, { rejectWithValue }) => {
|
| 28 |
+
try {
|
| 29 |
+
const response = await projectsAPI.list();
|
| 30 |
+
return response.data;
|
| 31 |
+
} catch (error: any) {
|
| 32 |
+
return rejectWithValue(error.response?.data?.error || 'Failed to fetch projects.');
|
| 33 |
+
}
|
| 34 |
+
});
|
| 35 |
+
|
| 36 |
+
export const fetchProject = createAsyncThunk('projects/fetchOne', async (id: string, { rejectWithValue }) => {
|
| 37 |
+
try {
|
| 38 |
+
const response = await projectsAPI.get(id);
|
| 39 |
+
return response.data;
|
| 40 |
+
} catch (error: any) {
|
| 41 |
+
return rejectWithValue(error.response?.data?.error || 'Failed to fetch project.');
|
| 42 |
+
}
|
| 43 |
+
});
|
| 44 |
+
|
| 45 |
+
export const createProject = createAsyncThunk(
|
| 46 |
+
'projects/create',
|
| 47 |
+
async (data: { name: string; defaultPlatform: string; defaultFormat: string }, { rejectWithValue }) => {
|
| 48 |
+
try {
|
| 49 |
+
const response = await projectsAPI.create(data);
|
| 50 |
+
return response.data;
|
| 51 |
+
} catch (error: any) {
|
| 52 |
+
return rejectWithValue(error.response?.data?.error || 'Failed to create project.');
|
| 53 |
+
}
|
| 54 |
+
}
|
| 55 |
+
);
|
| 56 |
+
|
| 57 |
+
const projectsSlice = createSlice({
|
| 58 |
+
name: 'projects',
|
| 59 |
+
initialState,
|
| 60 |
+
reducers: {
|
| 61 |
+
clearCurrent(state) {
|
| 62 |
+
state.current = null;
|
| 63 |
+
},
|
| 64 |
+
},
|
| 65 |
+
extraReducers: (builder) => {
|
| 66 |
+
builder
|
| 67 |
+
.addCase(fetchProjects.pending, (state) => { state.loading = true; state.error = null; })
|
| 68 |
+
.addCase(fetchProjects.fulfilled, (state, action: PayloadAction<Project[]>) => {
|
| 69 |
+
state.loading = false;
|
| 70 |
+
state.items = action.payload;
|
| 71 |
+
})
|
| 72 |
+
.addCase(fetchProjects.rejected, (state, action) => {
|
| 73 |
+
state.loading = false;
|
| 74 |
+
state.error = action.payload as string;
|
| 75 |
+
})
|
| 76 |
+
.addCase(fetchProject.fulfilled, (state, action: PayloadAction<Project>) => {
|
| 77 |
+
state.current = action.payload;
|
| 78 |
+
})
|
| 79 |
+
.addCase(createProject.fulfilled, (state, action: PayloadAction<Project>) => {
|
| 80 |
+
state.items.unshift(action.payload);
|
| 81 |
+
});
|
| 82 |
+
},
|
| 83 |
+
});
|
| 84 |
+
|
| 85 |
+
export const { clearCurrent } = projectsSlice.actions;
|
| 86 |
+
export default projectsSlice.reducer;
|
client/src/store/videosSlice.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
|
| 2 |
+
import { videosAPI } from '../services/api';
|
| 3 |
+
|
| 4 |
+
interface Video {
|
| 5 |
+
_id: string;
|
| 6 |
+
projectId: string;
|
| 7 |
+
script: string;
|
| 8 |
+
voice: any;
|
| 9 |
+
music: any;
|
| 10 |
+
status: string;
|
| 11 |
+
previewUrl: string;
|
| 12 |
+
exportUrls: any[];
|
| 13 |
+
cliLog: string[];
|
| 14 |
+
createdAt: string;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
interface VideosState {
|
| 18 |
+
items: Video[];
|
| 19 |
+
current: Video | null;
|
| 20 |
+
loading: boolean;
|
| 21 |
+
error: string | null;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
const initialState: VideosState = {
|
| 25 |
+
items: [],
|
| 26 |
+
current: null,
|
| 27 |
+
loading: false,
|
| 28 |
+
error: null,
|
| 29 |
+
};
|
| 30 |
+
|
| 31 |
+
export const fetchVideo = createAsyncThunk('videos/fetchOne', async (id: string, { rejectWithValue }) => {
|
| 32 |
+
try {
|
| 33 |
+
const response = await videosAPI.get(id);
|
| 34 |
+
return response.data;
|
| 35 |
+
} catch (error: any) {
|
| 36 |
+
return rejectWithValue(error.response?.data?.error || 'Failed to fetch video.');
|
| 37 |
+
}
|
| 38 |
+
});
|
| 39 |
+
|
| 40 |
+
export const createVideo = createAsyncThunk(
|
| 41 |
+
'videos/create',
|
| 42 |
+
async ({ projectId, data }: { projectId: string; data: any }, { rejectWithValue }) => {
|
| 43 |
+
try {
|
| 44 |
+
const response = await videosAPI.create(projectId, data);
|
| 45 |
+
return response.data;
|
| 46 |
+
} catch (error: any) {
|
| 47 |
+
return rejectWithValue(error.response?.data?.error || 'Failed to create video.');
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
);
|
| 51 |
+
|
| 52 |
+
export const generateVideo = createAsyncThunk('videos/generate', async (id: string, { rejectWithValue }) => {
|
| 53 |
+
try {
|
| 54 |
+
const response = await videosAPI.generate(id);
|
| 55 |
+
return response.data;
|
| 56 |
+
} catch (error: any) {
|
| 57 |
+
return rejectWithValue(error.response?.data?.error || 'Failed to generate video.');
|
| 58 |
+
}
|
| 59 |
+
});
|
| 60 |
+
|
| 61 |
+
export const pollVideoStatus = createAsyncThunk('videos/pollStatus', async (id: string, { rejectWithValue }) => {
|
| 62 |
+
try {
|
| 63 |
+
const response = await videosAPI.status(id);
|
| 64 |
+
return response.data;
|
| 65 |
+
} catch (error: any) {
|
| 66 |
+
return rejectWithValue(error.response?.data?.error || 'Failed to poll status.');
|
| 67 |
+
}
|
| 68 |
+
});
|
| 69 |
+
|
| 70 |
+
const videosSlice = createSlice({
|
| 71 |
+
name: 'videos',
|
| 72 |
+
initialState,
|
| 73 |
+
reducers: {
|
| 74 |
+
clearCurrentVideo(state) {
|
| 75 |
+
state.current = null;
|
| 76 |
+
},
|
| 77 |
+
updateVideoFromSocket(state, action: PayloadAction<Partial<Video> & { _id: string }>) {
|
| 78 |
+
const idx = state.items.findIndex((v) => v._id === action.payload._id);
|
| 79 |
+
if (idx !== -1) {
|
| 80 |
+
state.items[idx] = { ...state.items[idx], ...action.payload };
|
| 81 |
+
}
|
| 82 |
+
if (state.current?._id === action.payload._id) {
|
| 83 |
+
state.current = { ...state.current, ...action.payload };
|
| 84 |
+
}
|
| 85 |
+
},
|
| 86 |
+
},
|
| 87 |
+
extraReducers: (builder) => {
|
| 88 |
+
builder
|
| 89 |
+
.addCase(fetchVideo.fulfilled, (state, action: PayloadAction<Video>) => {
|
| 90 |
+
state.current = action.payload;
|
| 91 |
+
})
|
| 92 |
+
.addCase(createVideo.fulfilled, (state, action: PayloadAction<Video>) => {
|
| 93 |
+
state.items.unshift(action.payload);
|
| 94 |
+
state.current = action.payload;
|
| 95 |
+
})
|
| 96 |
+
.addCase(pollVideoStatus.fulfilled, (state, action: PayloadAction<any>) => {
|
| 97 |
+
if (state.current) {
|
| 98 |
+
state.current = { ...state.current, ...action.payload };
|
| 99 |
+
}
|
| 100 |
+
});
|
| 101 |
+
},
|
| 102 |
+
});
|
| 103 |
+
|
| 104 |
+
export const { clearCurrentVideo, updateVideoFromSocket } = videosSlice.actions;
|
| 105 |
+
export default videosSlice.reducer;
|
client/src/vite-env.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
/// <reference types="vite/client" />
|
client/tailwind.config.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/** @type {import('tailwindcss').Config} */
|
| 2 |
+
export default {
|
| 3 |
+
content: [
|
| 4 |
+
"./index.html",
|
| 5 |
+
"./src/**/*.{js,ts,jsx,tsx}",
|
| 6 |
+
],
|
| 7 |
+
theme: {
|
| 8 |
+
extend: {
|
| 9 |
+
colors: {
|
| 10 |
+
dark: {
|
| 11 |
+
900: '#0A0A0A',
|
| 12 |
+
800: '#111111',
|
| 13 |
+
700: '#1A1A1A',
|
| 14 |
+
600: '#222222',
|
| 15 |
+
500: '#2A2A2A',
|
| 16 |
+
400: '#333333',
|
| 17 |
+
300: '#444444',
|
| 18 |
+
},
|
| 19 |
+
gold: {
|
| 20 |
+
100: '#FFF8E1',
|
| 21 |
+
200: '#FFECB3',
|
| 22 |
+
300: '#FFD54F',
|
| 23 |
+
400: '#E8C547',
|
| 24 |
+
500: '#D4AF37',
|
| 25 |
+
600: '#C49B30',
|
| 26 |
+
700: '#A67C29',
|
| 27 |
+
800: '#8B6914',
|
| 28 |
+
900: '#6B4F10',
|
| 29 |
+
},
|
| 30 |
+
light: {
|
| 31 |
+
100: '#FFFFFF',
|
| 32 |
+
200: '#FAFAFA',
|
| 33 |
+
300: '#F5F5F5',
|
| 34 |
+
400: '#E0E0E0',
|
| 35 |
+
500: '#BDBDBD',
|
| 36 |
+
},
|
| 37 |
+
},
|
| 38 |
+
fontFamily: {
|
| 39 |
+
display: ['Playfair Display', 'Georgia', 'serif'],
|
| 40 |
+
body: ['Montserrat', 'Helvetica', 'Arial', 'sans-serif'],
|
| 41 |
+
},
|
| 42 |
+
backgroundImage: {
|
| 43 |
+
'gold-gradient': 'linear-gradient(135deg, #D4AF37 0%, #F5E290 50%, #D4AF37 100%)',
|
| 44 |
+
'dark-gradient': 'linear-gradient(180deg, #0A0A0A 0%, #1A1A1A 50%, #0A0A0A 100%)',
|
| 45 |
+
'hero-gradient': 'linear-gradient(135deg, rgba(212,175,55,0.1) 0%, rgba(10,10,10,0.95) 50%, rgba(212,175,55,0.05) 100%)',
|
| 46 |
+
'card-gradient': 'linear-gradient(145deg, rgba(42,42,42,0.6) 0%, rgba(17,17,17,0.8) 100%)',
|
| 47 |
+
},
|
| 48 |
+
boxShadow: {
|
| 49 |
+
'gold': '0 0 20px rgba(212, 175, 55, 0.15)',
|
| 50 |
+
'gold-lg': '0 0 40px rgba(212, 175, 55, 0.2)',
|
| 51 |
+
'gold-glow': '0 0 60px rgba(212, 175, 55, 0.3)',
|
| 52 |
+
'card': '0 8px 32px rgba(0, 0, 0, 0.4)',
|
| 53 |
+
'card-hover': '0 16px 48px rgba(0, 0, 0, 0.6)',
|
| 54 |
+
},
|
| 55 |
+
animation: {
|
| 56 |
+
'fade-in': 'fadeIn 0.6s ease-out forwards',
|
| 57 |
+
'fade-in-up': 'fadeInUp 0.8s ease-out forwards',
|
| 58 |
+
'fade-in-down': 'fadeInDown 0.8s ease-out forwards',
|
| 59 |
+
'slide-in-left': 'slideInLeft 0.8s ease-out forwards',
|
| 60 |
+
'slide-in-right': 'slideInRight 0.8s ease-out forwards',
|
| 61 |
+
'gold-shimmer': 'goldShimmer 3s ease-in-out infinite',
|
| 62 |
+
'pulse-gold': 'pulseGold 2s ease-in-out infinite',
|
| 63 |
+
'float': 'float 6s ease-in-out infinite',
|
| 64 |
+
},
|
| 65 |
+
keyframes: {
|
| 66 |
+
fadeIn: {
|
| 67 |
+
'0%': { opacity: '0' },
|
| 68 |
+
'100%': { opacity: '1' },
|
| 69 |
+
},
|
| 70 |
+
fadeInUp: {
|
| 71 |
+
'0%': { opacity: '0', transform: 'translateY(30px)' },
|
| 72 |
+
'100%': { opacity: '1', transform: 'translateY(0)' },
|
| 73 |
+
},
|
| 74 |
+
fadeInDown: {
|
| 75 |
+
'0%': { opacity: '0', transform: 'translateY(-30px)' },
|
| 76 |
+
'100%': { opacity: '1', transform: 'translateY(0)' },
|
| 77 |
+
},
|
| 78 |
+
slideInLeft: {
|
| 79 |
+
'0%': { opacity: '0', transform: 'translateX(-50px)' },
|
| 80 |
+
'100%': { opacity: '1', transform: 'translateX(0)' },
|
| 81 |
+
},
|
| 82 |
+
slideInRight: {
|
| 83 |
+
'0%': { opacity: '0', transform: 'translateX(50px)' },
|
| 84 |
+
'100%': { opacity: '1', transform: 'translateX(0)' },
|
| 85 |
+
},
|
| 86 |
+
goldShimmer: {
|
| 87 |
+
'0%, 100%': { backgroundPosition: '200% center' },
|
| 88 |
+
'50%': { backgroundPosition: '-200% center' },
|
| 89 |
+
},
|
| 90 |
+
pulseGold: {
|
| 91 |
+
'0%, 100%': { boxShadow: '0 0 20px rgba(212, 175, 55, 0.15)' },
|
| 92 |
+
'50%': { boxShadow: '0 0 40px rgba(212, 175, 55, 0.35)' },
|
| 93 |
+
},
|
| 94 |
+
float: {
|
| 95 |
+
'0%, 100%': { transform: 'translateY(0)' },
|
| 96 |
+
'50%': { transform: 'translateY(-10px)' },
|
| 97 |
+
},
|
| 98 |
+
},
|
| 99 |
+
},
|
| 100 |
+
},
|
| 101 |
+
plugins: [],
|
| 102 |
+
};
|
client/tsconfig.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "ES2020",
|
| 4 |
+
"useDefineForClassFields": true,
|
| 5 |
+
"lib": [
|
| 6 |
+
"ES2020",
|
| 7 |
+
"DOM",
|
| 8 |
+
"DOM.Iterable"
|
| 9 |
+
],
|
| 10 |
+
"module": "ESNext",
|
| 11 |
+
"skipLibCheck": true,
|
| 12 |
+
"moduleResolution": "bundler",
|
| 13 |
+
"allowImportingTsExtensions": true,
|
| 14 |
+
"isolatedModules": true,
|
| 15 |
+
"moduleDetection": "force",
|
| 16 |
+
"noEmit": true,
|
| 17 |
+
"jsx": "react-jsx",
|
| 18 |
+
"strict": true,
|
| 19 |
+
"noUnusedLocals": false,
|
| 20 |
+
"noUnusedParameters": false,
|
| 21 |
+
"noFallthroughCasesInSwitch": true,
|
| 22 |
+
"forceConsistentCasingInFileNames": true,
|
| 23 |
+
"baseUrl": "./src",
|
| 24 |
+
"paths": {
|
| 25 |
+
"@/*": [
|
| 26 |
+
"./*"
|
| 27 |
+
]
|
| 28 |
+
}
|
| 29 |
+
},
|
| 30 |
+
"include": [
|
| 31 |
+
"src"
|
| 32 |
+
]
|
| 33 |
+
}
|
client/vite.config.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from 'vite';
|
| 2 |
+
import react from '@vitejs/plugin-react';
|
| 3 |
+
|
| 4 |
+
export default defineConfig({
|
| 5 |
+
plugins: [react()],
|
| 6 |
+
server: {
|
| 7 |
+
port: 5173,
|
| 8 |
+
proxy: {
|
| 9 |
+
'/api': {
|
| 10 |
+
target: 'http://localhost:5000',
|
| 11 |
+
changeOrigin: true,
|
| 12 |
+
},
|
| 13 |
+
},
|
| 14 |
+
},
|
| 15 |
+
});
|
client/vite.config.ts.timestamp-1772602542886-63bad0f79f7d48.mjs
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// vite.config.ts
|
| 2 |
+
import { defineConfig } from "file:///C:/Users/User/Desktop/debugrem/1st-hackaton/node_modules/vite/dist/node/index.js";
|
| 3 |
+
import react from "file:///C:/Users/User/Desktop/debugrem/1st-hackaton/node_modules/@vitejs/plugin-react/dist/index.js";
|
| 4 |
+
var vite_config_default = defineConfig({
|
| 5 |
+
plugins: [react()],
|
| 6 |
+
server: {
|
| 7 |
+
port: 5173,
|
| 8 |
+
proxy: {
|
| 9 |
+
"/api": {
|
| 10 |
+
target: "http://localhost:5000",
|
| 11 |
+
changeOrigin: true
|
| 12 |
+
}
|
| 13 |
+
}
|
| 14 |
+
}
|
| 15 |
+
});
|
| 16 |
+
export {
|
| 17 |
+
vite_config_default as default
|
| 18 |
+
};
|
| 19 |
+
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCJDOlxcXFxVc2Vyc1xcXFxVc2VyXFxcXERlc2t0b3BcXFxcZGVidWdyZW1cXFxcMXN0LWhhY2thdG9uXFxcXGNsaWVudFwiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9maWxlbmFtZSA9IFwiQzpcXFxcVXNlcnNcXFxcVXNlclxcXFxEZXNrdG9wXFxcXGRlYnVncmVtXFxcXDFzdC1oYWNrYXRvblxcXFxjbGllbnRcXFxcdml0ZS5jb25maWcudHNcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfaW1wb3J0X21ldGFfdXJsID0gXCJmaWxlOi8vL0M6L1VzZXJzL1VzZXIvRGVza3RvcC9kZWJ1Z3JlbS8xc3QtaGFja2F0b24vY2xpZW50L3ZpdGUuY29uZmlnLnRzXCI7aW1wb3J0IHsgZGVmaW5lQ29uZmlnIH0gZnJvbSAndml0ZSc7XHJcbmltcG9ydCByZWFjdCBmcm9tICdAdml0ZWpzL3BsdWdpbi1yZWFjdCc7XHJcblxyXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVDb25maWcoe1xyXG4gICAgcGx1Z2luczogW3JlYWN0KCldLFxyXG4gICAgc2VydmVyOiB7XHJcbiAgICAgICAgcG9ydDogNTE3MyxcclxuICAgICAgICBwcm94eToge1xyXG4gICAgICAgICAgICAnL2FwaSc6IHtcclxuICAgICAgICAgICAgICAgIHRhcmdldDogJ2h0dHA6Ly9sb2NhbGhvc3Q6NTAwMCcsXHJcbiAgICAgICAgICAgICAgICBjaGFuZ2VPcmlnaW46IHRydWUsXHJcbiAgICAgICAgICAgIH0sXHJcbiAgICAgICAgfSxcclxuICAgIH0sXHJcbn0pO1xyXG4iXSwKICAibWFwcGluZ3MiOiAiO0FBQXNWLFNBQVMsb0JBQW9CO0FBQ25YLE9BQU8sV0FBVztBQUVsQixJQUFPLHNCQUFRLGFBQWE7QUFBQSxFQUN4QixTQUFTLENBQUMsTUFBTSxDQUFDO0FBQUEsRUFDakIsUUFBUTtBQUFBLElBQ0osTUFBTTtBQUFBLElBQ04sT0FBTztBQUFBLE1BQ0gsUUFBUTtBQUFBLFFBQ0osUUFBUTtBQUFBLFFBQ1IsY0FBYztBQUFBLE1BQ2xCO0FBQUEsSUFDSjtBQUFBLEVBQ0o7QUFDSixDQUFDOyIsCiAgIm5hbWVzIjogW10KfQo=
|
data/db/WiredTiger
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
WiredTiger
|
| 2 |
+
WiredTiger 12.0.0: (November 15, 2024)
|
data/db/collection-3791c9a5-657d-4715-bd89-75d890810f01.wt
ADDED
|
Binary file (4.1 kB). View file
|
|
|
data/db/collection-4ee22b20-b377-4975-a039-62c6075c60a1.wt
ADDED
|
Binary file (20.5 kB). View file
|
|
|
data/db/collection-f901bcc1-09cb-4c9d-a4be-8ff5da48c862.wt
ADDED
|
Binary file (20.5 kB). View file
|
|
|
data/db/diagnostic.data/metrics.2026-03-04T05-33-55Z-00000
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ce2f313bfbb0af1eec51305c50aac2c050c884b128342c8efe85febf8a466d2c
|
| 3 |
+
size 606393
|
data/db/diagnostic.data/metrics.interim
ADDED
|
Binary file (36.2 kB). View file
|
|
|
data/db/index-71d13bac-060b-40af-8cda-85b71358cc48.wt
ADDED
|
Binary file (4.1 kB). View file
|
|
|
data/db/index-870f2506-ea92-45c5-9b87-1a2cdf2b1b98.wt
ADDED
|
Binary file (20.5 kB). View file
|
|
|
data/db/index-9bc70cd3-e317-4136-bf76-2c9d2a6f9fa6.wt
ADDED
|
Binary file (4.1 kB). View file
|
|
|
data/db/index-e4f8019a-c6d2-435b-b9fc-4a33bd33f390.wt
ADDED
|
Binary file (20.5 kB). View file
|
|
|
data/db/journal/WiredTigerLog.0000000001
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c59ea56b3830deb36e006e5f651f6283cd34d7727f139a48e09f8b23b1e44f70
|
| 3 |
+
size 104857600
|
data/db/journal/WiredTigerPreplog.0000000001
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:48747572eb4a79fdc2ff6fe7add9ce4a4382a3935bc26d5f24b5ae358e13755f
|
| 3 |
+
size 104857600
|
data/db/mongod.lock
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
10952
|
data/db/sizeStorer.wt
ADDED
|
Binary file (20.5 kB). View file
|
|
|
data/db/storage.bson
ADDED
|
Binary file (114 Bytes). View file
|
|
|
our_logging/1_implementation_plan.md.resolved
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Director.AI - Faceless Video Creation Platform
|
| 2 |
+
|
| 3 |
+
Full-stack web app: users input scripts, customize styles/voices/music/assets, and generate videos for social platforms. Luxury dark-mode aesthetic with gold accents.
|
| 4 |
+
|
| 5 |
+
## Novel CLI Bridge Approach (No Mocks, No Simulations)
|
| 6 |
+
|
| 7 |
+
> [!IMPORTANT]
|
| 8 |
+
> **Real CLI Bridge**: The webapp includes a built-in chatbox/CLI terminal that acts as a **live bridge** to the actual Google Antigravity CLI running on the host machine. When a user types a prompt in the webapp, it is sent to the backend, which spawns a real Antigravity CLI process, pipes the prompt in, captures the output, and streams it back to the frontend in real-time via WebSocket. This is NOT a simulation -- it is the real CLI doing real work.
|
| 9 |
+
|
| 10 |
+
**How it works:**
|
| 11 |
+
1. User types a prompt in the webapp's AI Terminal panel (e.g., "Generate a voiceover for this script in a warm male voice")
|
| 12 |
+
2. Frontend sends the prompt to the backend via WebSocket
|
| 13 |
+
3. Backend spawns `antigravity-cli` (or pipes to a persistent session) using `child_process`
|
| 14 |
+
4. CLI output is streamed back line-by-line to the frontend via WebSocket
|
| 15 |
+
5. Backend parses output for generated file paths, status updates, errors
|
| 16 |
+
6. Generated assets (audio, video, subtitles) are saved to the project's asset folder and linked to the Video record in MongoDB
|
| 17 |
+
|
| 18 |
+
**Benefits:**
|
| 19 |
+
- Zero mocking -- every AI task uses the real Antigravity CLI
|
| 20 |
+
- The webapp becomes an orchestration layer on top of the CLI
|
| 21 |
+
- Users can also issue freeform prompts for custom tasks the predefined workflows don't cover
|
| 22 |
+
|
| 23 |
+
## User Review Required
|
| 24 |
+
|
| 25 |
+
> [!WARNING]
|
| 26 |
+
> **No `npm install` auto-run**: Per your rules, I will NOT run `npm install`. I'll prepare all files and provide commands for you to run manually.
|
| 27 |
+
|
| 28 |
+
> [!IMPORTANT]
|
| 29 |
+
> **MongoDB + Redis required**: You'll need these running locally or via free cloud tiers (Atlas free, Redis Cloud free). Connection strings go in `.env`.
|
| 30 |
+
|
| 31 |
+
---
|
| 32 |
+
|
| 33 |
+
## Proposed Changes
|
| 34 |
+
|
| 35 |
+
### Phase 1: Project Scaffolding
|
| 36 |
+
|
| 37 |
+
#### [NEW] Root files
|
| 38 |
+
- `package.json` (workspace), `.gitignore`, `.env.example`
|
| 39 |
+
|
| 40 |
+
#### [NEW] `server/` - Express + TypeScript backend
|
| 41 |
+
- `server/package.json`, `server/tsconfig.json`
|
| 42 |
+
- `server/src/index.ts` - Express server (port 5000) + WebSocket server (Socket.IO)
|
| 43 |
+
- `server/src/config/` - Env config, DB connection, Redis connection
|
| 44 |
+
- `server/src/middleware/` - Auth (JWT), rate limiting, error handling
|
| 45 |
+
- `server/src/models/` - Mongoose: User, Project, Video, Preset
|
| 46 |
+
- `server/src/routes/` - REST: auth, projects, videos, presets, assets, admin
|
| 47 |
+
- `server/src/services/cliBridge.ts` - **Core**: spawns real Antigravity CLI, streams I/O via WebSocket
|
| 48 |
+
- `server/src/services/queueService.ts` - Bull.js job queue calling the CLI bridge
|
| 49 |
+
- `server/src/services/tokenTracker.ts` - Redis counter for CLI token usage
|
| 50 |
+
- `server/src/utils/logger.ts` - Winston logging
|
| 51 |
+
- `server/src/seed.ts` - Sample data seeder
|
| 52 |
+
|
| 53 |
+
#### [NEW] `client/` - Vite + React + TypeScript frontend
|
| 54 |
+
- Tailwind CSS: `#1A1A1A` bg, `#F5F5F5` text, `#D4AF37` gold
|
| 55 |
+
- Fonts: Playfair Display (headlines), Montserrat (body)
|
| 56 |
+
- Framer Motion animations
|
| 57 |
+
- Pages: Landing, Auth, Dashboard, Project, VideoCreation (5-step wizard), Preview, Export
|
| 58 |
+
- **AI Terminal component**: Real-time chatbox connected to backend CLI bridge via WebSocket
|
| 59 |
+
- Redux Toolkit store, Axios API client
|
| 60 |
+
|
| 61 |
+
---
|
| 62 |
+
|
| 63 |
+
### Phase 2: Backend Core
|
| 64 |
+
|
| 65 |
+
**Models**: User (email, bcrypt password, subscription, role), Project (userId, platform, format), Video (script, voice, music, style, assets, status), Preset (fonts, colors, transitions)
|
| 66 |
+
|
| 67 |
+
**Routes**: `/api/auth/*`, `/api/projects/*`, `/api/videos/*`, `/api/presets/*`, `/api/assets/upload`, `/api/admin/queue`
|
| 68 |
+
|
| 69 |
+
**CLI Bridge Service** (`cliBridge.ts`):
|
| 70 |
+
- `spawnCLI(command, args)` -- spawns `child_process` with the real CLI
|
| 71 |
+
- Streams stdout/stderr to the WebSocket room for the requesting user
|
| 72 |
+
- Parses output for file paths and status codes
|
| 73 |
+
- Handles errors gracefully (token limits, crashes) with user notification
|
| 74 |
+
|
| 75 |
+
**Queue Service**: Bull.js jobs call `cliBridge` for: `generateVoice`, `generateSubtitles`, `assembleVideo`, `exportVideo`
|
| 76 |
+
|
| 77 |
+
---
|
| 78 |
+
|
| 79 |
+
### Phase 3: Frontend
|
| 80 |
+
|
| 81 |
+
**Landing**: Hero, How It Works, Features, Pricing tiers, FAQ accordion, CTA
|
| 82 |
+
**Dashboard**: Project cards, create project modal
|
| 83 |
+
**Video Wizard**: Script input > Voice config > Music selection > Style preset > Asset upload > Generate
|
| 84 |
+
**AI Terminal**: Full-width chatbox panel, real-time streaming output, command history
|
| 85 |
+
**Preview**: HTML5 player, regenerate/export controls
|
| 86 |
+
**Export**: Format/quality selectors, download links
|
| 87 |
+
|
| 88 |
+
---
|
| 89 |
+
|
| 90 |
+
### Phase 4: Polish
|
| 91 |
+
|
| 92 |
+
- Legal pages (Privacy Policy, Terms)
|
| 93 |
+
- Seed script
|
| 94 |
+
- README.md
|
| 95 |
+
|
| 96 |
+
---
|
| 97 |
+
|
| 98 |
+
## Verification Plan
|
| 99 |
+
|
| 100 |
+
### Manual Verification
|
| 101 |
+
1. `cd server && npm run dev` -- backend starts on :5000
|
| 102 |
+
2. `cd client && npm run dev` -- frontend starts on :5173
|
| 103 |
+
3. Landing page: luxury dark design, gold accents, animations
|
| 104 |
+
4. Register/login flow returns JWT
|
| 105 |
+
5. AI Terminal: type a prompt, verify it reaches backend and WebSocket streams output
|
| 106 |
+
6. Video wizard: navigate all 5 steps
|
| 107 |
+
7. API endpoints respond correctly via dev tools
|
our_logging/1_prompt.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
You are an expert full-stack developer and AI system architect tasked with creating a complete, end-to-end web application for faceless video creation called Director.AI. This application allows users to input scripts, customize styles, voices, music, and assets, and generate optimized videos for social platforms without showing faces. The design must evoke luxury through elegant typography, subtle gradients, high-contrast dark modes with gold accents, spacious layouts, and premium animations like smooth fades and parallax effects. Use no emojis in any part of the implementation or documentation.
|
| 2 |
+
|
| 3 |
+
Follow these specifications precisely to build the system. The application must be self-contained, with no reliance on external paid APIs or subscriptions for core functionality. Instead, integrate the Google Antigravity system CLI (a command-line interface providing access to strong AI models with no subscription costs, a decent weekly token limit, and capabilities for text-to-speech, video editing simulation, subtitle generation, and asset integration). All AI-driven tasks (voiceover generation, subtitle timing, video assembly, and style application) will be executed via shell commands to this CLI from the backend. Assume the CLI is installed on the server and accessible via Node.js child_process or equivalent. Handle token limits by queuing jobs and notifying users of delays if limits are approached.
|
| 4 |
+
|
| 5 |
+
### 1. Overall Architecture
|
| 6 |
+
- **Frontend**: Use React.js with TypeScript for the user interface. Implement routing with React Router. Style with Tailwind CSS, customized for a luxurious theme: base colors #1A1A1A (dark background), #F5F5F5 (light text), #D4AF37 (gold accents for buttons and highlights). Use Framer Motion for animations. Ensure responsive design for desktop and mobile.
|
| 7 |
+
- **Backend**: Use Node.js with Express.js for the server. Handle authentication with JWT. Use MongoDB as the database for storing user data, projects, presets, and job queues. Implement a job queue system with Bull.js (Redis-based) to manage CLI calls asynchronously, respecting token limits.
|
| 8 |
+
- **Deployment**: Design for easy deployment on Vercel (frontend) and a Node.js host like Render or Heroku (backend). Use environment variables for secrets like database URI and CLI paths.
|
| 9 |
+
- **Security**: Implement HTTPS, input sanitization, rate limiting with express-rate-limit, and secure file uploads with Multer (limit to 100MB per upload).
|
| 10 |
+
- **Scalability**: Use the job queue to handle concurrent generations. Monitor CLI token usage with a simple counter in Redis, resetting weekly.
|
| 11 |
+
- **No External Dependencies**: All stock assets (music, voices, footage) must be locally stored or generated via CLI. For initial setup, include a seed script to populate sample assets.
|
| 12 |
+
|
| 13 |
+
### 2. Database Schema (MongoDB)
|
| 14 |
+
Define models with Mongoose:
|
| 15 |
+
- **User**: { _id, email: String (unique), password: String (hashed with bcrypt), subscription: String ('free', 'starter', 'creator', 'studio', 'custom'), videosGenerated: Number, presets: Array of Preset IDs, role: String ('user', 'admin'), createdAt: Date }
|
| 16 |
+
- **Project**: { _id, userId: User ID, name: String, defaultPlatform: String ('TikTok', 'Reels', 'Shorts', 'YouTube'), defaultFormat: String ('9:16', '16:9', '1:1'), videos: Array of Video IDs }
|
| 17 |
+
- **Video**: { _id, projectId: Project ID, script: String, voice: { type: String, language: String, tone: String, speed: Number }, music: { filePath: String or CLI-generated ID }, stylePreset: Preset ID, assets: Array of { type: String ('clip', 'image', 'logo'), path: String }, status: String ('pending', 'generating', 'preview', 'exported'), previewUrl: String, exportUrls: Array of { format: String, quality: String, url: String }, createdAt: Date }
|
| 18 |
+
- **Preset**: { _id, userId: User ID, name: String, voice: Object (as above), music: String, fonts: { primary: String, secondary: String }, colors: { primary: String, secondary: String, accent: String }, transitions: String ('fade', 'slide', etc.), videoStyle: String ('minimal', 'dynamic', etc.) }
|
| 19 |
+
- **JobQueue**: Managed by Bull.js, each job: { videoId: Video ID, type: String ('generateVoice', 'generateSubtitles', 'assembleVideo', 'export') }
|
| 20 |
+
|
| 21 |
+
### 3. Backend Implementation
|
| 22 |
+
- **Server Setup**: Express app listening on port 5000. Routes prefixed with /api.
|
| 23 |
+
- **Authentication Routes**:
|
| 24 |
+
- POST /auth/register: Create user, send verification email (use Nodemailer with Gmail SMTP, free tier).
|
| 25 |
+
- POST /auth/login: Validate credentials, return JWT.
|
| 26 |
+
- Middleware: authMiddleware to protect routes.
|
| 27 |
+
- **Project Routes**:
|
| 28 |
+
- POST /projects: Create new project (auth required).
|
| 29 |
+
- GET /projects: List user's projects.
|
| 30 |
+
- GET /projects/:id: Get project details.
|
| 31 |
+
- **Video Routes**:
|
| 32 |
+
- POST /videos/:projectId: Create video with script, voice, etc.
|
| 33 |
+
- POST /videos/:id/generate: Add to queue for generation.
|
| 34 |
+
- GET /videos/:id/status: Poll job status.
|
| 35 |
+
- POST /videos/:id/export: Trigger export job.
|
| 36 |
+
- **Preset Routes**:
|
| 37 |
+
- POST /presets: Create or update preset.
|
| 38 |
+
- GET /presets: List user's presets.
|
| 39 |
+
- **Asset Upload**: POST /assets/upload: Handle file uploads to /uploads folder, return path.
|
| 40 |
+
- **Job Processing**:
|
| 41 |
+
- Use Bull.js queue. Each job executes CLI commands via child_process.spawn.
|
| 42 |
+
- CLI Integration Examples:
|
| 43 |
+
- Voiceover: `antigravity-cli generate-voice --script "${script}" --voice "${voice.type}" --language "${voice.language}" --tone "${voice.tone}" --speed ${voice.speed} --output voice.mp3`
|
| 44 |
+
- Subtitles: `antigravity-cli generate-subtitles --script "${script}" --voice-file voice.mp3 --fonts "${preset.fonts}" --colors "${preset.colors}" --output subtitles.srt`
|
| 45 |
+
- Video Assembly: `antigravity-cli assemble-video --subtitles subtitles.srt --music "${music.path}" --assets "${assets.paths.join(',')}" --style "${preset.videoStyle}" --transitions "${preset.transitions}" --format "${defaultFormat}" --output preview.mp4`
|
| 46 |
+
- Export: `antigravity-cli export-video --input preview.mp4 --formats "9:16,16:9,1:1" --quality "1080p" --output exports/`
|
| 47 |
+
- Handle CLI output: Parse stdout for success/failure, store generated files in /generated folder, upload to S3-compatible storage (use local filesystem for start, later MinIO).
|
| 48 |
+
- Error Handling: If CLI fails (e.g., token limit), retry after delay or notify user via email.
|
| 49 |
+
- **Subscription Logic**: On video generation, check user's subscription limits. Use Stripe webhooks for payments (integrate Stripe.js, but since no external APIs for core, Stripe is for payments only).
|
| 50 |
+
- **Queue Monitoring**: Endpoint GET /admin/queue: For admins to view pending jobs.
|
| 51 |
+
|
| 52 |
+
### 4. Frontend Implementation
|
| 53 |
+
- **App Structure**: Pages for Landing, Dashboard, Project, Video Creation, Preview, Export. Use protected routes.
|
| 54 |
+
- **Luxurious Design Guidelines**:
|
| 55 |
+
- Typography: Use Google Fonts 'Playfair Display' for headlines (elegant serif), 'Montserrat' for body (sans-serif).
|
| 56 |
+
- Layout: Full-width hero with subtle gold gradient overlays. Cards with rounded corners, shadows, and hover scales.
|
| 57 |
+
- Animations: Fade-in on load, smooth transitions on button clicks.
|
| 58 |
+
- Dark Mode: Default, with toggle if needed.
|
| 59 |
+
- **Landing Page** (Public):
|
| 60 |
+
- Hero: Headline "Turn your scripts into ready-to-post faceless videos." Subheadline as provided. Buttons: "Start now" (to register), "Watch a 30-second demo" (embed local video).
|
| 61 |
+
- How It Works: 3-step section with numbered cards.
|
| 62 |
+
- Features: Bullet list in grid layout.
|
| 63 |
+
- For Who: Section with targeted paragraphs.
|
| 64 |
+
- Pricing: Tier cards with details, "Try 5 videos for free" button.
|
| 65 |
+
- FAQ: Accordion component.
|
| 66 |
+
- CTA: "Ready to stop wasting time editing?" with "Generate my first video" button.
|
| 67 |
+
- **Onboarding Flow** (Authenticated Dashboard):
|
| 68 |
+
- Welcome Screen: "Create new project" button, quick tip.
|
| 69 |
+
- Create Project: Form for name, default platform/format.
|
| 70 |
+
- Create Video: Multi-step wizard (use React Stepper):
|
| 71 |
+
- Step 1: Textarea for script paste or file upload (drag-drop).
|
| 72 |
+
- Step 2: Dropdowns for voice (fetch from CLI-supported list via backend API).
|
| 73 |
+
- Step 3: Music selector (library or upload).
|
| 74 |
+
- Step 4: Preset selector or form to create new (fonts, colors, etc.).
|
| 75 |
+
- Step 5: Asset uploader (multiple files).
|
| 76 |
+
- Final: "Generate preview" button (POST to /videos/:id/generate).
|
| 77 |
+
- Preview Page: Video player (HTML5), buttons for regenerate (specific parts), change ratio, approve/export.
|
| 78 |
+
- Export Page: Checkboxes for formats/quality, "Export all" button (POST to export), then download links or ZIP.
|
| 79 |
+
- **State Management**: Use Redux for user, projects, videos. Handle loading states with spinners.
|
| 80 |
+
- **API Calls**: Use Axios with interceptors for JWT. Poll status every 10s during generation.
|
| 81 |
+
|
| 82 |
+
### 5. Integration and Testing
|
| 83 |
+
- **CLI Setup**: Assume CLI is pre-installed. In code, use absolute path from env var ANTIGRAVITY_CLI_PATH.
|
| 84 |
+
- **Seeding**: Script to create sample users, presets, and assets.
|
| 85 |
+
- **Testing**: Unit tests with Jest for backend routes, frontend components. End-to-end with Cypress: Test full video creation flow.
|
| 86 |
+
- **Logging**: Use Winston for server logs, track CLI calls.
|
| 87 |
+
|
| 88 |
+
### 6. Launch Preparations
|
| 89 |
+
- **Content**: Implement as static pages or dynamic from DB.
|
| 90 |
+
- Short Pitch: For social bios.
|
| 91 |
+
- DM Script: Template in docs.
|
| 92 |
+
- Early Offer: Special pricing route for admins.
|
| 93 |
+
- **Legal**: Static pages for Privacy Policy, Terms of Use.
|
| 94 |
+
- **Checklist**: Ensure all elements from provided idea are implemented.
|
| 95 |
+
- **Domain and Payments**: Configure in env vars.
|
| 96 |
+
|
| 97 |
+
Build this system step-by-step: Start with backend setup, then database, routes, queue, CLI integration, frontend pages, and finally testing. Output progress logs in console. Upon completion, provide a README.md with setup instructions.
|