diff --git a/.env.example b/.env.example new file mode 100644 index 0000000000000000000000000000000000000000..b0f389181d8e9abe88b1194d72781f969930af40 --- /dev/null +++ b/.env.example @@ -0,0 +1,31 @@ +# Server +PORT=5000 +NODE_ENV=development + +# MongoDB +MONGODB_URI=mongodb://localhost:27017/directorai + +# Redis +REDIS_URL=redis://localhost:6379 + +# JWT +JWT_SECRET=your_jwt_secret_here +JWT_EXPIRES_IN=7d + +# Antigravity CLI +ANTIGRAVITY_CLI_PATH=antigravity + +# Email (Nodemailer) +SMTP_HOST=smtp.gmail.com +SMTP_PORT=587 +SMTP_USER=your_email@gmail.com +SMTP_PASS=your_app_password + +# Stripe (payments only) +STRIPE_SECRET_KEY=sk_test_xxx +STRIPE_WEBHOOK_SECRET=whsec_xxx + +# File storage +UPLOAD_DIR=./uploads +GENERATED_DIR=./generated +MAX_UPLOAD_SIZE=104857600 diff --git a/.gitattributes b/.gitattributes index a6344aac8c09253b3b630fb776ae94478aa0275b..a3e39872a8e6c47dc270e47dd8c3a90fcf6d9eff 100644 --- a/.gitattributes +++ b/.gitattributes @@ -33,3 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text *.zip filter=lfs diff=lfs merge=lfs -text *.zst filter=lfs diff=lfs merge=lfs -text *tfevents* filter=lfs diff=lfs merge=lfs -text +data/db/diagnostic.data/metrics.2026-03-04T05-33-55Z-00000 filter=lfs diff=lfs merge=lfs -text +data/db/journal/WiredTigerLog.0000000001 filter=lfs diff=lfs merge=lfs -text +data/db/journal/WiredTigerPreplog.0000000001 filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..ec23899ef83c8229b2f0e157c7ff018672f8f166 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +node_modules/ +dist/ +build/ +.env +*.log +uploads/ +generated/ +exports/ +.DS_Store +Thumbs.db +coverage/ +.vite/ +*.mjs +vite.config.ts.timestamp-* +data/ +*.lock +*.wt +WiredTiger* diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..0890510c47229396ba7d6d607cea297c0454eb0a --- /dev/null +++ b/README.md @@ -0,0 +1,162 @@ +# Director.AI + +> **Faceless video creation platform powered by real AI CLI integration.** +> Built with React, TypeScript, Express, MongoDB, and Socket.IO. + +![License](https://img.shields.io/badge/License-MIT-blue) +![Node](https://img.shields.io/badge/Node-20+-green) +![TypeScript](https://img.shields.io/badge/TypeScript-5.3-blue) +![React](https://img.shields.io/badge/React-18-blue) +![Express](https://img.shields.io/badge/Express-4.18-lightgrey) +![MongoDB](https://img.shields.io/badge/MongoDB-8-green) + +--- + +## Quick Setup Tutorial + +Follow these steps to get Director.AI running on your local machine. + +### 1. Clone the Repository +```bash +git clone https://huggingface.co/algorembrant/1st-hackaton +cd 1st-hackaton +``` + +### 2. Install MongoDB and Compass +- **Download MongoDB Community Server**: Visit the [MongoDB Download Center](https://www.mongodb.com/try/download/community) and download the MSI installer for Windows. +- **Install**: Run the installer and ensure "Install MongoDB as a Service" is checked. +- **MongoDB Compass**: During installation, check the box to install MongoDB Compass (the GUI for managing your data). +- **Verify**: Open MongoDB Compass and connect to `mongodb://localhost:27017`. + +### 3. Environment Configuration +- Create a `.env` file in the root directory by copying the example: + ```bash + cp .env.example .env + ``` +- Open `.env` and fill in the following: + - `MONGODB_URI`: `mongodb://localhost:27017/director-ai` + - `PORT`: `5000` + - `JWT_SECRET`: Any random long string + - `REDIS_URL`: `redis://localhost:6379` (If using Redis features) + + or simply just tell you AI agent to configure things for you. + +### 4. Install Dependencies +```bash +npm install +``` + +### 5. Running the Application +- **Server**: `cd server && npm run dev` +- **Client**: `cd client && npm run dev` +- **Both**: From the root, run `npm run dev`. + +--- + +## Overview + +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). + +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. + +### Key Features + +- **AI Terminal**: Real-time CLI bridge via WebSocket—type prompts in the webapp, get live output from the Antigravity CLI. +- **5-Step Video Wizard**: Script, Voice, Music, Style, Assets → Generate. +- **Multi-Platform Export**: 9:16 (TikTok/Reels/Shorts), 16:9 (YouTube), 1:1 (Instagram). +- **JWT Authentication**: Secure register/login with bcrypt hashing. +- **Luxury Dark Theme**: Gold accents, Playfair Display + Montserrat, glassmorphism, Framer Motion animations. +- **Style Presets**: Save and reuse brand configurations. +- **File Upload**: Multer with 100MB limit and file type filtering. +- **Admin Dashboard**: Queue monitoring and stats overview. + +--- + +## System Architecture + +```mermaid +graph TD + A["User Browser"] -->|HTTP + WebSocket| B["Express Server :5000"] + B -->|Mongoose| C[MongoDB] + B -->|Socket.IO| D["WebSocket Layer"] + D -->|child_process.spawn| E["Antigravity CLI"] + E -->|stdout/stderr stream| D + D -->|Real-time output| A + B -->|Multer| F["File Storage /uploads"] + B -->|"Generated"| G["/generated"] + + subgraph Frontend [":5173"] + H["React + TypeScript"] + I["Redux Toolkit"] + J["Framer Motion"] + K["Tailwind CSS"] + L["AI Terminal Component"] + end + + subgraph Backend [":5000"] + M["JWT Auth"] + N["REST API Routes"] + O["CLI Bridge Service"] + P["Rate Limiting + Helmet"] + end + + A --> H + H --> I + H --> L + L -->|Socket.IO Client| D +``` + +--- + +## Project Structure + +```text +1st-hackaton/ +├── .env.example +├── .gitignore +├── package.json +├── prompt.md +├── README.md +├── server/ +│ ├── src/ +│ │ ├── index.ts # Main Entry +│ │ ├── config/ # Database & App Config +│ │ ├── models/ # Mongoose Schemas +│ │ ├── routes/ # API Endpoints +│ │ ├── services/ # Business Logic +│ │ └── utils/ # Helpers +│ └── tsconfig.json +└── client/ + ├── src/ + │ ├── components/ # UI Building Blocks + │ ├── pages/ # Main Views + │ ├── services/ # API Integration + │ └── store/ # Redux State Management + ├── vite.config.ts + └── tailwind.config.js +``` + +--- + +## Hugging Face Deployment + +This project is optimized for deployment on Hugging Face. + +- **Repository**: [https://huggingface.co/algorembrant/1st-hackaton](https://huggingface.co/algorembrant/1st-hackaton) +- **Author Profile**: [https://huggingface.co/algorembrant](https://huggingface.co/algorembrant) + +> [!IMPORTANT] +> Large assets or binary files should be handled via **Xet Storage** or **Git LFS** to ensure optimal performance on Hugging Face. + +--- + +## Author + +**Rembrant Oyangoren Albeos** +- Hugging Face: [@algorembrant](https://huggingface.co/algorembrant) + +--- + +## License + +MIT License | © 2026 **Rembrant Oyangoren Albeos** diff --git a/client/index.html b/client/index.html new file mode 100644 index 0000000000000000000000000000000000000000..c7c265393d761b588fc406e99d3d5fef4f998e57 --- /dev/null +++ b/client/index.html @@ -0,0 +1,16 @@ + + + + + + + Director.AI -- Faceless Video Creation + + + + + +
+ + + diff --git a/client/package.json b/client/package.json new file mode 100644 index 0000000000000000000000000000000000000000..fe738d4f3be0a3da859092927681e6b42db75c50 --- /dev/null +++ b/client/package.json @@ -0,0 +1,31 @@ +{ + "name": "director-ai-client", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@reduxjs/toolkit": "^2.1.0", + "axios": "^1.6.5", + "framer-motion": "^11.0.3", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-redux": "^9.1.0", + "react-router-dom": "^6.22.0", + "socket.io-client": "^4.7.4" + }, + "devDependencies": { + "@types/react": "^18.3.1", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.17", + "postcss": "^8.4.34", + "tailwindcss": "^3.4.1", + "typescript": "^5.3.3", + "vite": "^5.1.0" + } +} \ No newline at end of file diff --git a/client/postcss.config.js b/client/postcss.config.js new file mode 100644 index 0000000000000000000000000000000000000000..6e392ade555971b33840bf9ab070653d88eef93d --- /dev/null +++ b/client/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/client/public/INTEGRATION_GUIDE.md b/client/public/INTEGRATION_GUIDE.md new file mode 100644 index 0000000000000000000000000000000000000000..da89fd8bcbb61af1418815cfaf3c2accea0b315f --- /dev/null +++ b/client/public/INTEGRATION_GUIDE.md @@ -0,0 +1,73 @@ +# Director.AI - Integration Guide (Workspace MCP) + +## Overview +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. + +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. + +## Connection Details + +- **Protocol**: WebSocket (Socket.IO v4) +- **Host**: `ws://localhost:5000/mcp` +- **Method**: The agent must connect using a standard Socket.IO client pointing to the `/mcp` namespace. + +### Sample Node.js Agent Code +If you are writing an agent to control this workspace, here is how you connect: + +```javascript +import { io } from 'socket.io-client'; + +const socket = io('http://localhost:5000/mcp'); + +socket.on('connect', () => { + console.log('Agent connected to Workspace Hub!'); + + // Example: Read current projects + socket.emit('mcp:read_state', (response) => { + console.log('Current projects:', response.data.projects); + }); +}); +``` + +## Available Tools (Socket.IO Events) + +The following tools are exposed by the MCP server for the agent to use: + +### 1. `mcp:read_state` +Reads the current high-level state of the application. +- **Payload**: None required. +- **Callback Returns**: `{ status: 'success', data: { projects: Project[] } }` + +### 2. `mcp:navigate` +Forces all connected human browsers to instantly navigate to a specific React Router path. Use this to guide the user's attention. +- **Payload**: `{ path: string }` + - Example: `{ path: '/dashboard' }` or `{ path: '/project/12345/create' }` +- **Callback Returns**: `{ status: 'success' }` + +### 3. `mcp:create_project` +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. +- **Payload**: `{ name: string, defaultPlatform: 'TikTok'|'YouTube', defaultFormat: '9:16'|'16:9' }` +- **Callback Returns**: `{ status: 'success', data: Project }` + +### 4. `mcp:update_video_draft` +Used when the agent is iteratively drafting a video script. It streams updates directly to the human's screen. +- **Payload**: `{ projectId: string, script?: string, voiceType?: string }` +- **Callback Returns**: `{ status: 'success' }` + +### 5. `mcp:activity_log` +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..."). +- **Payload**: `{ message: string, type: 'info' | 'success' | 'warning' | 'error' }` +- **Callback Returns**: None + +## Example Workflow for the Agent + +1. **Connect**: Agent connects to `/mcp`. +2. **Log**: emit `mcp:activity_log` -> "Agent initialized. Analyzing workspace..." +3. **Navigate**: emit `mcp:navigate` (`{ path: '/dashboard' }`) -> Forces human browser to dashboard. +4. **Create**: emit `mcp:create_project` (`{ name: 'Viral Story', defaultPlatform: 'TikTok', defaultFormat: '9:16' }`). +5. **Log**: emit `mcp:activity_log` -> "Project created. Script drafting initiated." +6. **Navigate**: emit `mcp:navigate` -> Agent navigates browser to `/project/[id]/create` so human sees the creation wizard. +7. **Complete**: Agent continues to build the video via tools. + +## Development Note for Antigravity Engine +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. diff --git a/client/src/App.tsx b/client/src/App.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6a5bb48609e7c258b305806d8ae2b5c9ec0e9f07 --- /dev/null +++ b/client/src/App.tsx @@ -0,0 +1,50 @@ +import { Routes, Route, Navigate } from 'react-router-dom'; +import { useSelector } from 'react-redux'; +import { RootState } from './store'; +import { useAgentOrchestrator } from './hooks/useAgentOrchestrator'; +import Navbar from './components/Navbar'; +import Landing from './pages/Landing'; +import Login from './pages/Login'; +import Register from './pages/Register'; +import Dashboard from './pages/Dashboard'; +import ProjectPage from './pages/ProjectPage'; +import VideoCreate from './pages/VideoCreate'; +import Preview from './pages/Preview'; +import Privacy from './pages/Privacy'; +import Terms from './pages/Terms'; + +function ProtectedRoute({ children }: { children: React.ReactNode }) { + const { token } = useSelector((state: RootState) => state.auth); + if (!token) return ; + return <>{children}; +} + +export default function App() { + // Listen to operations coming from the AI Agent connecting to our MCP Host + useAgentOrchestrator(); + + return ( +
+ + + } /> + } /> + } /> + } /> + } /> + + } /> + + } /> + + } /> + + } /> + +
+ ); +} diff --git a/client/src/components/AITerminal.tsx b/client/src/components/AITerminal.tsx new file mode 100644 index 0000000000000000000000000000000000000000..59d403388ec5ffaa2f773ee063e8558c299ad0a8 --- /dev/null +++ b/client/src/components/AITerminal.tsx @@ -0,0 +1,108 @@ +import { useState, useRef, useEffect } from 'react'; +import { useSelector } from 'react-redux'; +import { io, Socket } from 'socket.io-client'; +import { motion } from 'framer-motion'; +import { RootState } from '../store'; + +interface LogEntry { + type: 'info' | 'success' | 'warning' | 'error' | 'system'; + message: string; + timestamp: string; +} + +let socket: Socket | null = null; + +export default function AITerminal() { + const { token } = useSelector((state: RootState) => state.auth); + const [logs, setLogs] = useState([ + { + type: 'system', + message: 'Workspace Activity Monitor', + timestamp: new Date().toLocaleTimeString(), + }, + { + type: 'info', + message: 'Waiting for Google Antigravity CLI to connect and perform actions...', + timestamp: new Date().toLocaleTimeString(), + }, + ]); + const bottomRef = useRef(null); + + useEffect(() => { + if (!token) return; + + // Connect as a Browser viewport + socket = io('http://localhost:5000/browser', { + auth: { token }, + }); + + socket.on('connect', () => { + addLog('system', 'Browser viewport connected to Workspace Hub'); + }); + + socket.on('disconnect', () => { + addLog('error', 'Disconnected from Workspace Hub'); + }); + + socket.on('agent:activity_log', (data: { message: string, type: 'info' | 'success' | 'warning' | 'error', timestamp: string }) => { + setLogs(prev => [...prev, { + type: data.type, + message: data.message, + timestamp: new Date(data.timestamp).toLocaleTimeString(), + }]); + }); + + return () => { + socket?.disconnect(); + socket = null; + }; + }, [token]); + + useEffect(() => { + bottomRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [logs]); + + function addLog(type: LogEntry['type'], message: string) { + setLogs((prev) => [...prev, { + type, + message, + timestamp: new Date().toLocaleTimeString(), + }]); + } + + function getTextColor(type: LogEntry['type']) { + switch (type) { + case 'info': return 'text-light-400'; + case 'success': return 'text-green-400'; + case 'warning': return 'text-gold-400'; + case 'error': return 'text-red-400'; + case 'system': return 'text-blue-400'; + default: return 'text-light-400'; + } + } + + return ( + +
+
+
+ Agent Activity Log +
+
+ +
+ {logs.map((log, i) => ( +
+ [{log.timestamp}] + {log.message} +
+ ))} +
+
+ + ); +} diff --git a/client/src/components/Navbar.tsx b/client/src/components/Navbar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2dd1def2f602e58e2e10a3654d08887e5b4685c6 --- /dev/null +++ b/client/src/components/Navbar.tsx @@ -0,0 +1,108 @@ +import { useState } from 'react'; +import { Link, useNavigate } from 'react-router-dom'; +import { useSelector, useDispatch } from 'react-redux'; +import { motion, AnimatePresence } from 'framer-motion'; +import { RootState, AppDispatch } from '../store'; +import { logout } from '../store/authSlice'; + +export default function Navbar() { + const { user, token } = useSelector((state: RootState) => state.auth); + const dispatch = useDispatch(); + const navigate = useNavigate(); + const [menuOpen, setMenuOpen] = useState(false); + + const handleLogout = () => { + dispatch(logout()); + navigate('/'); + }; + + return ( + + ); +} diff --git a/client/src/hooks/useAgentOrchestrator.ts b/client/src/hooks/useAgentOrchestrator.ts new file mode 100644 index 0000000000000000000000000000000000000000..1826d7b028d432052692ebbe33742bfc4102d412 --- /dev/null +++ b/client/src/hooks/useAgentOrchestrator.ts @@ -0,0 +1,40 @@ +import { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useSelector, useDispatch } from 'react-redux'; +import { io } from 'socket.io-client'; +import { RootState, AppDispatch } from '../store'; +import { fetchProjects } from '../store/projectsSlice'; + +export function useAgentOrchestrator() { + const navigate = useNavigate(); + const dispatch = useDispatch(); + const { token } = useSelector((state: RootState) => state.auth); + + useEffect(() => { + if (!token) return; + + // The browser connects to the /browser namespace to listen to the agent + const socket = io('http://localhost:5000/browser', { + auth: { token }, + }); + + // When the CLI agent wants to guide the user's view: + socket.on('agent:navigate', (path: string) => { + console.log(`[Agent Orchestrator] Navigating to ${path}`); + navigate(path); + }); + + // When the CLI agent creates a project, we should refresh our state so the UI updates + socket.on('agent:project_created', () => { + console.log(`[Agent Orchestrator] Agent created a project, refreshing list`); + dispatch(fetchProjects()); + }); + + // You could add logic here for 'agent:video_draft_updated' + // to fill out the form fields in VideoCreate automatically. + + return () => { + socket.disconnect(); + }; + }, [token, navigate, dispatch]); +} diff --git a/client/src/index.css b/client/src/index.css new file mode 100644 index 0000000000000000000000000000000000000000..9d5d1b8e6491724968f23b1c974705c0b989be96 --- /dev/null +++ b/client/src/index.css @@ -0,0 +1,149 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + * { + margin: 0; + padding: 0; + box-sizing: border-box; + } + + html { + scroll-behavior: smooth; + } + + body { + background-color: #0A0A0A; + color: #F5F5F5; + font-family: 'Montserrat', sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + overflow-x: hidden; + } + + ::selection { + background-color: rgba(212, 175, 55, 0.3); + color: #F5F5F5; + } + + ::-webkit-scrollbar { + width: 6px; + } + + ::-webkit-scrollbar-track { + background: #111111; + } + + ::-webkit-scrollbar-thumb { + background: #333333; + border-radius: 3px; + } + + ::-webkit-scrollbar-thumb:hover { + background: #D4AF37; + } + + h1, h2, h3, h4, h5, h6 { + font-family: 'Playfair Display', serif; + } + + a { + color: inherit; + text-decoration: none; + transition: color 0.3s ease; + } +} + +@layer components { + .btn-primary { + @apply inline-flex items-center justify-center px-8 py-3 + bg-gold-500 text-dark-900 font-body font-semibold + rounded-lg transition-all duration-300 ease-out + hover:bg-gold-400 hover:shadow-gold-lg hover:-translate-y-0.5 + active:translate-y-0 active:shadow-gold + disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:translate-y-0; + } + + .btn-secondary { + @apply inline-flex items-center justify-center px-8 py-3 + border border-gold-500/40 text-gold-500 font-body font-medium + rounded-lg transition-all duration-300 ease-out + hover:border-gold-500 hover:bg-gold-500/10 hover:shadow-gold + active:bg-gold-500/20; + } + + .btn-ghost { + @apply inline-flex items-center justify-center px-6 py-2 + text-light-400 font-body font-medium + rounded-lg transition-all duration-300 ease-out + hover:text-gold-500 hover:bg-dark-500/50; + } + + .card { + @apply bg-card-gradient backdrop-blur-sm border border-dark-400/30 + rounded-2xl p-6 transition-all duration-500 ease-out + hover:border-gold-500/20 hover:shadow-card-hover hover:-translate-y-1; + } + + .card-static { + @apply bg-card-gradient backdrop-blur-sm border border-dark-400/30 + rounded-2xl p-6; + } + + .input-field { + @apply w-full px-4 py-3 bg-dark-600 border border-dark-400/50 + rounded-lg text-light-300 font-body placeholder-light-500/40 + transition-all duration-300 ease-out + focus:outline-none focus:border-gold-500/60 focus:shadow-gold + hover:border-dark-300; + } + + .section-heading { + @apply font-display text-4xl md:text-5xl font-bold text-light-100 mb-4; + } + + .section-subheading { + @apply font-body text-lg text-light-500 max-w-2xl mx-auto; + } + + .gold-text { + @apply text-transparent bg-clip-text bg-gold-gradient; + } + + .glass-panel { + @apply bg-dark-700/60 backdrop-blur-xl border border-dark-400/20 + rounded-2xl shadow-card; + } + + .terminal-bg { + @apply bg-dark-900 border border-dark-400/30 rounded-xl + font-mono text-sm; + } +} + +@layer utilities { + .text-balance { + text-wrap: balance; + } + + .animate-delay-100 { + animation-delay: 100ms; + } + + .animate-delay-200 { + animation-delay: 200ms; + } + + .animate-delay-300 { + animation-delay: 300ms; + } + + .animate-delay-400 { + animation-delay: 400ms; + } + + .animate-delay-500 { + animation-delay: 500ms; + } +} diff --git a/client/src/main.tsx b/client/src/main.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2685e40bc9cae2581d0faffe8f1da0da7949a0cc --- /dev/null +++ b/client/src/main.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { Provider } from 'react-redux'; +import { BrowserRouter } from 'react-router-dom'; +import App from './App'; +import { store } from './store'; +import './index.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + + + +); diff --git a/client/src/pages/Dashboard.tsx b/client/src/pages/Dashboard.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b995a8a8e321f99ac126e721cb541c772b0e1b0c --- /dev/null +++ b/client/src/pages/Dashboard.tsx @@ -0,0 +1,250 @@ +import { useEffect, useState, FormEvent } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { Link } from 'react-router-dom'; +import { motion, AnimatePresence } from 'framer-motion'; +import { AppDispatch, RootState } from '../store'; +import { fetchProjects, createProject } from '../store/projectsSlice'; +import AITerminal from '../components/AITerminal'; + +export default function Dashboard() { + const dispatch = useDispatch(); + const { user } = useSelector((state: RootState) => state.auth); + const { items: projects, loading } = useSelector((state: RootState) => state.projects); + const [showCreateModal, setShowCreateModal] = useState(false); + const [showTerminal, setShowTerminal] = useState(false); + + useEffect(() => { + dispatch(fetchProjects()); + }, [dispatch]); + + return ( +
+
+ {/* Welcome Header */} + +

+ Welcome back{user?.email ? `, ${user.email.split('@')[0]}` : ''} +

+

+ Manage your projects and create new videos +

+
+ + {/* Quick Actions */} + + + + + + {/* AI Terminal */} + + {showTerminal && ( + + + + )} + + + {/* Stats Bar */} + + {[ + { label: 'Projects', value: projects.length }, + { label: 'Videos Generated', value: user?.videosGenerated || 0 }, + { label: 'Subscription', value: user?.subscription || 'Free' }, + { label: 'This Month', value: '0 / 5' }, + ].map((stat) => ( +
+

{stat.value}

+

{stat.label}

+
+ ))} +
+ + {/* Projects Grid */} +
+

Your Projects

+
+ + {loading ? ( +
+
+
+ ) : projects.length === 0 ? ( + +
+ + +
+

No projects yet

+

Create your first project to start generating videos

+ +
+ ) : ( +
+ {projects.map((project, i) => ( + + +
+
+
+
+ + {project.defaultPlatform} + +
+

+ {project.name} +

+

+ {project.videos?.length || 0} video{project.videos?.length !== 1 ? 's' : ''} -- {project.defaultFormat} +

+ + + ))} +
+ )} +
+ + {/* Create Project Modal */} + + {showCreateModal && ( + setShowCreateModal(false)} /> + )} + +
+ ); +} + +function CreateProjectModal({ onClose }: { onClose: () => void }) { + const dispatch = useDispatch(); + const [name, setName] = useState(''); + const [platform, setPlatform] = useState('TikTok'); + const [format, setFormat] = useState('9:16'); + const [submitting, setSubmitting] = useState(false); + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + if (!name.trim()) return; + setSubmitting(true); + await dispatch(createProject({ name, defaultPlatform: platform, defaultFormat: format })); + setSubmitting(false); + onClose(); + }; + + return ( + + e.stopPropagation()} + > +

Create New Project

+ +
+
+ + setName(e.target.value)} + className="input-field" + placeholder="My Awesome Video Series" + required + autoFocus + /> +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+
+ ); +} diff --git a/client/src/pages/Landing.tsx b/client/src/pages/Landing.tsx new file mode 100644 index 0000000000000000000000000000000000000000..18ebb13c56c0cfa71bc5817048a747f24227a62a --- /dev/null +++ b/client/src/pages/Landing.tsx @@ -0,0 +1,524 @@ +import { motion } from 'framer-motion'; +import { Link } from 'react-router-dom'; +import { useState } from 'react'; + +const fadeInUp = { + hidden: { opacity: 0, y: 30 }, + visible: (i: number) => ({ + opacity: 1, y: 0, + transition: { delay: i * 0.15, duration: 0.6, ease: 'easeOut' }, + }), +}; + +const stagger = { + visible: { transition: { staggerChildren: 0.1 } }, +}; + +export default function Landing() { + return ( +
+ + + + + + + +
+
+ ); +} + +/* ─── Hero ─── */ +function HeroSection() { + return ( +
+ {/* Background Effects */} +
+
+
+ + {/* Grid pattern overlay */} +
+ +
+ + + + AI-Powered Video Creation + + + + + Turn your scripts into{' '} + + ready-to-post + {' '} + faceless videos + + + + Stop wasting hours editing. Paste your script, choose a style, and let AI create + scroll-stopping videos optimized for TikTok, Reels, and Shorts -- in minutes, not days. + + + + + Start now + + + + + + Try 5 videos free -- No credit card required + + +
+ + {/* Bottom gradient fade */} +
+
+ ); +} + +/* ─── How It Works ─── */ +function HowItWorksSection() { + const steps = [ + { + num: '01', + title: 'Paste Your Script', + desc: 'Write or paste your script. Upload a text file or type directly into the editor. AI helps refine your message for maximum impact.', + }, + { + num: '02', + title: 'Customize Everything', + desc: 'Choose your voice, music, visual style, fonts, and transitions. Save presets to maintain brand consistency across all your videos.', + }, + { + num: '03', + title: 'Generate and Export', + 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.', + }, + ]; + + return ( +
+
+ + + How It Works + + + Three simple steps from script to screen + + + +
+ {steps.map((step, i) => ( + + {/* Step number */} + + {step.num} + + + {/* Gold accent line */} +
+ +

+ {step.title} +

+

+ {step.desc} +

+ + ))} +
+
+
+ ); +} + +/* ─── Features ─── */ +function FeaturesSection() { + const features = [ + { title: 'AI Voice Generation', desc: 'Natural-sounding voiceovers in multiple languages, tones, and speeds.' }, + { title: 'Smart Subtitles', desc: 'Auto-synced captions with customizable fonts, colors, and animations.' }, + { title: 'Multi-Platform Export', desc: 'One-click export for TikTok (9:16), YouTube (16:9), and Instagram (1:1).' }, + { title: 'Style Presets', desc: 'Save and reuse your brand settings: fonts, colors, transitions, and more.' }, + { title: 'Real-Time AI Terminal', desc: 'Watch AI build your video live through the integrated command terminal.' }, + { title: 'Asset Library', desc: 'Upload your own clips, images, logos, and music -- or use the built-in library.' }, + { title: 'Batch Processing', desc: 'Queue multiple videos and let the system handle them autonomously.' }, + { title: 'No Subscription Lock-In', desc: 'Generous free tier. Pay only for what you use beyond the free limits.' }, + ]; + + return ( +
+
+
+ + + Powerful Features + + + Everything you need to create professional faceless content + + + +
+ {features.map((f, i) => ( + +
+
+
+

{f.title}

+

{f.desc}

+ + ))} +
+
+
+ ); +} + +/* ─── For Who ─── */ +function ForWhoSection() { + const audiences = [ + { 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.' }, + { title: 'Digital Marketers', desc: 'Create ad variations, explainer videos, and social content at a fraction of the cost and time of traditional production.' }, + { title: 'Educators and Coaches', desc: 'Transform your lessons, courses, and tips into engaging video content without ever showing your face on camera.' }, + { title: 'Agencies', desc: 'White-label video production for clients. Maintain brand consistency with presets. Scale without hiring more editors.' }, + ]; + + return ( +
+
+ + + Built For You + + + +
+ {audiences.map((a, i) => ( + +
+
+
+
+

{a.title}

+

{a.desc}

+
+ + ))} +
+
+
+ ); +} + +/* ─── Pricing ─── */ +function PricingSection() { + const tiers = [ + { + name: 'Free', + price: '$0', + period: 'forever', + features: ['5 videos per month', 'Basic voices', '720p export', 'Watermarked'], + cta: 'Start Free', + highlighted: false, + }, + { + name: 'Starter', + price: '$19', + period: '/month', + features: ['25 videos per month', 'All voices and languages', '1080p export', 'No watermark', '3 presets'], + cta: 'Get Starter', + highlighted: false, + }, + { + name: 'Creator', + price: '$49', + period: '/month', + features: ['100 videos per month', 'Priority processing', '4K export', 'Unlimited presets', 'Batch processing', 'Custom branding'], + cta: 'Get Creator', + highlighted: true, + }, + { + name: 'Studio', + price: '$149', + period: '/month', + features: ['Unlimited videos', 'Dedicated processing', '4K export', 'API access', 'White-label', 'Priority support', 'Team accounts'], + cta: 'Get Studio', + highlighted: false, + }, + ]; + + return ( +
+
+
+ + + Simple Pricing + + + Start free. Scale as you grow. No hidden fees. + + + +
+ {tiers.map((tier, i) => ( + + {tier.highlighted && ( +
+ + Most Popular + +
+ )} + +
+

{tier.name}

+
+ {tier.price} + {tier.period} +
+
+ +
    + {tier.features.map((f) => ( +
  • + + {f} +
  • + ))} +
+ + + {tier.cta} + +
+ ))} +
+
+
+ ); +} + +/* ─── FAQ ─── */ +function FAQSection() { + const faqs = [ + { + q: 'What is a faceless video?', + 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.', + }, + { + q: 'Do I need video editing experience?', + 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.', + }, + { + q: 'How does the AI Terminal work?', + 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.', + }, + { + q: 'Can I use my own assets?', + 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.', + }, + { + q: 'What platforms are supported for export?', + 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.', + }, + { + q: 'Is there a free tier?', + a: 'Yes. The free tier gives you 5 videos per month at 720p with a small watermark. No credit card required to start.', + }, + ]; + + return ( +
+
+ + + Common Questions + + + +
+ {faqs.map((faq, i) => ( + + ))} +
+
+
+ ); +} + +function FAQItem({ question, answer, index }: { question: string; answer: string; index: number }) { + const [open, setOpen] = useState(false); + + return ( + setOpen(!open)} + > +
+

{question}

+ + + + +
+ +

{answer}

+
+
+ ); +} + +/* ─── CTA ─── */ +function CTASection() { + return ( +
+
+
+ + + Ready to stop wasting time editing? + + + Join creators who are already producing professional faceless videos in minutes. + + + + Generate my first video + + + +
+
+ ); +} + +/* ─── Footer ─── */ +function Footer() { + return ( +
+
+
+
+
+ D +
+ + Director.AI + +
+
+ Privacy Policy + Terms of Use +
+

+ 2026 Director.AI. All rights reserved. +

+
+
+
+ ); +} diff --git a/client/src/pages/Login.tsx b/client/src/pages/Login.tsx new file mode 100644 index 0000000000000000000000000000000000000000..3aa6d9beddb21907432da1469e7b1c46c075bb67 --- /dev/null +++ b/client/src/pages/Login.tsx @@ -0,0 +1,90 @@ +import { useState, FormEvent } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { Link, useNavigate } from 'react-router-dom'; +import { motion } from 'framer-motion'; +import { AppDispatch, RootState } from '../store'; +import { loginUser, clearError } from '../store/authSlice'; + +export default function Login() { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const dispatch = useDispatch(); + const navigate = useNavigate(); + const { loading, error } = useSelector((state: RootState) => state.auth); + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + const result = await dispatch(loginUser({ email, password })); + if (loginUser.fulfilled.match(result)) { + navigate('/dashboard'); + } + }; + + return ( +
+
+ +
+
+

Welcome Back

+

Sign in to your Director.AI account

+
+ + {error && ( +
+ {error} +
+ )} + +
+
+ + { setEmail(e.target.value); dispatch(clearError()); }} + className="input-field" + placeholder="you@example.com" + required + /> +
+ +
+ + { setPassword(e.target.value); dispatch(clearError()); }} + className="input-field" + placeholder="Enter your password" + required + /> +
+ + +
+ +

+ Don't have an account?{' '} + + Create one + +

+
+
+
+ ); +} diff --git a/client/src/pages/Preview.tsx b/client/src/pages/Preview.tsx new file mode 100644 index 0000000000000000000000000000000000000000..52729c274155b94c881c12fd20ec8739f54eb080 --- /dev/null +++ b/client/src/pages/Preview.tsx @@ -0,0 +1,203 @@ +import { useEffect, useState } from 'react'; +import { useParams, Link } from 'react-router-dom'; +import { useDispatch, useSelector } from 'react-redux'; +import { motion } from 'framer-motion'; +import { AppDispatch, RootState } from '../store'; +import { fetchVideo, generateVideo, pollVideoStatus } from '../store/videosSlice'; +import AITerminal from '../components/AITerminal'; + +export default function Preview() { + const { id } = useParams<{ id: string }>(); + const dispatch = useDispatch(); + const { current: video } = useSelector((state: RootState) => state.videos); + const [showTerminal, setShowTerminal] = useState(true); + const [exportFormats, setExportFormats] = useState({ + '9:16': true, + '16:9': false, + '1:1': false, + }); + + useEffect(() => { + if (id) dispatch(fetchVideo(id)); + }, [id, dispatch]); + + // Poll status while generating + useEffect(() => { + if (!video || video.status !== 'generating' || !id) return; + const interval = setInterval(() => { + dispatch(pollVideoStatus(id)); + }, 10000); + return () => clearInterval(interval); + }, [video?.status, id, dispatch]); + + const handleGenerate = () => { + if (id) dispatch(generateVideo(id)); + }; + + if (!video) { + return ( +
+
+
+ ); + } + + return ( +
+
+ {/* Breadcrumb */} +
+ Dashboard + / + Video Preview +
+ +
+ {/* Video Player Area */} +
+ + {/* Player */} +
+ {video.previewUrl ? ( +
+ + {/* Action Bar */} +
+ + +
+
+ + {/* Terminal */} + {showTerminal && ( + + + + )} +
+ + {/* Sidebar */} +
+ {/* Script */} + +

Script

+

+ {video.script || 'No script provided'} +

+
+ + {/* Voice Settings */} + +

Voice

+
+
+ Type + {video.voice?.type || 'neutral'} +
+
+ Language + {video.voice?.language || 'en'} +
+
+ Tone + {video.voice?.tone || 'professional'} +
+
+ Speed + {video.voice?.speed || 1.0}x +
+
+
+ + {/* Export */} + +

Export Options

+
+ {(['9:16', '16:9', '1:1'] as const).map((fmt) => ( + + ))} +
+ +
+
+
+
+
+ ); +} diff --git a/client/src/pages/Privacy.tsx b/client/src/pages/Privacy.tsx new file mode 100644 index 0000000000000000000000000000000000000000..dbae9cdaf6f0aab3ae8571a0dd878cec68445d0e --- /dev/null +++ b/client/src/pages/Privacy.tsx @@ -0,0 +1,42 @@ +import { motion } from 'framer-motion'; +import { Link } from 'react-router-dom'; + +export default function Privacy() { + return ( +
+ + + Back to Home + + +

Privacy Policy

+ +
+

Last updated: March 2026

+ +

1. Information We Collect

+

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.

+ +

2. How We Use Your Information

+

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.

+ +

3. Data Storage and Security

+

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.

+ +

4. Data Retention

+

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.

+ +

5. Third-Party Services

+

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.

+ +

6. Contact

+

For privacy-related inquiries, contact the development team through the project repository.

+
+
+
+ ); +} diff --git a/client/src/pages/ProjectPage.tsx b/client/src/pages/ProjectPage.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ad59c092bd8e318fa8818f078c93bca0a7c2af61 --- /dev/null +++ b/client/src/pages/ProjectPage.tsx @@ -0,0 +1,118 @@ +import { useEffect, useState } from 'react'; +import { useParams, Link, useNavigate } from 'react-router-dom'; +import { useDispatch, useSelector } from 'react-redux'; +import { motion } from 'framer-motion'; +import { AppDispatch, RootState } from '../store'; +import { fetchProject } from '../store/projectsSlice'; +import AITerminal from '../components/AITerminal'; + +export default function ProjectPage() { + const { id } = useParams<{ id: string }>(); + const dispatch = useDispatch(); + const { current: project } = useSelector((state: RootState) => state.projects); + const [showTerminal, setShowTerminal] = useState(false); + const navigate = useNavigate(); + + useEffect(() => { + if (id) dispatch(fetchProject(id)); + }, [id, dispatch]); + + if (!project) { + return ( +
+
+
+ ); + } + + return ( +
+
+ {/* Breadcrumb */} +
+ Dashboard + / + {project.name} +
+ + {/* Header */} + +
+

{project.name}

+

+ {project.defaultPlatform} -- {project.defaultFormat} -- {project.videos?.length || 0} videos +

+
+
+ + +
+
+ + {/* Terminal */} + {showTerminal && ( + + + + )} + + {/* Videos */} + {(!project.videos || project.videos.length === 0) ? ( + +

No videos yet

+

Create your first video in this project

+ +
+ ) : ( +
+ {project.videos.map((video: any, i: number) => ( + + +
+ + {video.status || 'pending'} + +
+

+ {video.script?.slice(0, 120) || 'No script yet'}... +

+ +
+ ))} +
+ )} +
+
+ ); +} diff --git a/client/src/pages/Register.tsx b/client/src/pages/Register.tsx new file mode 100644 index 0000000000000000000000000000000000000000..49417ce3887d3007829da9b8616dbe8edf5e1f5e --- /dev/null +++ b/client/src/pages/Register.tsx @@ -0,0 +1,125 @@ +import { useState, FormEvent } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { Link, useNavigate } from 'react-router-dom'; +import { motion } from 'framer-motion'; +import { AppDispatch, RootState } from '../store'; +import { registerUser, clearError } from '../store/authSlice'; + +export default function Register() { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [localError, setLocalError] = useState(''); + const dispatch = useDispatch(); + const navigate = useNavigate(); + const { loading, error } = useSelector((state: RootState) => state.auth); + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + setLocalError(''); + + if (password !== confirmPassword) { + setLocalError('Passwords do not match.'); + return; + } + if (password.length < 8) { + setLocalError('Password must be at least 8 characters.'); + return; + } + + const result = await dispatch(registerUser({ email, password })); + if (registerUser.fulfilled.match(result)) { + navigate('/dashboard'); + } + }; + + const displayError = localError || error; + + return ( +
+
+ +
+
+

Create Account

+

Start creating faceless videos today

+
+ + {displayError && ( +
+ {displayError} +
+ )} + +
+
+ + { setEmail(e.target.value); dispatch(clearError()); setLocalError(''); }} + className="input-field" + placeholder="you@example.com" + required + /> +
+ +
+ + { setPassword(e.target.value); setLocalError(''); }} + className="input-field" + placeholder="Minimum 8 characters" + required + /> +
+ +
+ + { setConfirmPassword(e.target.value); setLocalError(''); }} + className="input-field" + placeholder="Re-enter your password" + required + /> +
+ + +
+ +

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

+ +

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

+
+
+
+ ); +} diff --git a/client/src/pages/Terms.tsx b/client/src/pages/Terms.tsx new file mode 100644 index 0000000000000000000000000000000000000000..efd4568fc7fd41f1fa9fae696e54b36991706af3 --- /dev/null +++ b/client/src/pages/Terms.tsx @@ -0,0 +1,48 @@ +import { motion } from 'framer-motion'; +import { Link } from 'react-router-dom'; + +export default function Terms() { + return ( +
+ + + Back to Home + + +

Terms of Use

+ +
+

Last updated: March 2026

+ +

1. Acceptance of Terms

+

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.

+ +

2. Description of Service

+

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.

+ +

3. User Accounts

+

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.

+ +

4. Content and Ownership

+

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.

+ +

5. Acceptable Use

+

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.

+ +

6. Subscription and Payments

+

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.

+ +

7. Limitation of Liability

+

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.

+ +

8. Changes to Terms

+

We reserve the right to modify these terms at any time. Continued use of the service after changes constitutes acceptance of the updated terms.

+
+
+
+ ); +} diff --git a/client/src/pages/VideoCreate.tsx b/client/src/pages/VideoCreate.tsx new file mode 100644 index 0000000000000000000000000000000000000000..93c39092088eecf2b4adf04e4b57e433f6b12eef --- /dev/null +++ b/client/src/pages/VideoCreate.tsx @@ -0,0 +1,353 @@ +import { useState, useCallback } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { useDispatch } from 'react-redux'; +import { motion, AnimatePresence } from 'framer-motion'; +import { AppDispatch } from '../store'; +import { createVideo } from '../store/videosSlice'; +import AITerminal from '../components/AITerminal'; + +const STEPS = [ + { num: 1, title: 'Script', desc: 'Paste or write your video script' }, + { num: 2, title: 'Voice', desc: 'Choose voice settings' }, + { num: 3, title: 'Music', desc: 'Select background music' }, + { num: 4, title: 'Style', desc: 'Set visual preferences' }, + { num: 5, title: 'Assets', desc: 'Upload media files' }, +]; + +export default function VideoCreate() { + const { projectId } = useParams<{ projectId: string }>(); + const dispatch = useDispatch(); + const navigate = useNavigate(); + const [step, setStep] = useState(1); + const [showTerminal, setShowTerminal] = useState(false); + const [submitting, setSubmitting] = useState(false); + + // Form state + const [script, setScript] = useState(''); + const [voice, setVoice] = useState({ + type: 'neutral', + language: 'en', + tone: 'professional', + speed: 1.0, + }); + const [music, setMusic] = useState(''); + const [style, setStyle] = useState({ + fonts: { primary: 'Playfair Display', secondary: 'Montserrat' }, + colors: { primary: '#1A1A1A', secondary: '#F5F5F5', accent: '#D4AF37' }, + transitions: 'fade', + videoStyle: 'minimal', + }); + const [files, setFiles] = useState([]); + + const next = () => setStep((s) => Math.min(s + 1, 5)); + const prev = () => setStep((s) => Math.max(s - 1, 1)); + + const handleFileChange = useCallback((e: React.ChangeEvent) => { + if (e.target.files) { + setFiles(Array.from(e.target.files)); + } + }, []); + + const handleGenerate = async () => { + if (!projectId || !script.trim()) return; + setSubmitting(true); + + const videoData = { + script, + voice, + music: { filePath: music }, + assets: files.map((f) => ({ type: 'clip' as const, path: f.name })), + }; + + const result = await dispatch(createVideo({ projectId, data: videoData })); + setSubmitting(false); + + if (createVideo.fulfilled.match(result)) { + navigate(`/video/${result.payload._id}/preview`); + } + }; + + return ( +
+
+ {/* Stepper */} +
+
+ {STEPS.map((s) => ( +
setStep(s.num)} + > +
s.num + ? 'bg-gold-500/20 text-gold-500 border border-gold-500/40' + : 'bg-dark-600 text-light-500 border border-dark-400/30' + }`}> + {step > s.num ? ( + + + + ) : s.num} +
+ + {s.title} + +
+ ))} +
+ + {/* Step Content */} + + + {step === 1 && ( +
+

Your Script

+

Paste your video script or write it directly. This will be used for voiceover and subtitles.

+