From 88133f643d12f54b840002d177fdf2adf28b5bf1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=8C=97=E6=9E=B3?=
<7854742+wang_rumeng@user.noreply.gitee.com>
Date: Thu, 19 Jun 2025 17:15:03 +0800
Subject: [PATCH] init
---
.bolt/config.json | 3 +
.bolt/ignore | 2 +
.bolt/prompt | 9 +
.eslintrc.json | 3 +
.gitignore | 36 +
app/actors/page.tsx | 10 +
app/create/page.tsx | 10 +
app/globals.css | 86 +
app/history/page.tsx | 10 +
app/layout.tsx | 34 +
app/media/page.tsx | 10 +
app/page.tsx | 10 +
components.json | 20 +
components/layout/dashboard-layout.tsx | 25 +
components/layout/sidebar.tsx | 123 +
components/layout/top-bar.tsx | 71 +
components/pages/actors-library-page.tsx | 360 +
components/pages/create-video-workflow.tsx | 147 +
components/pages/history-page.tsx | 331 +
components/pages/home-page.tsx | 163 +
components/pages/media-library-page.tsx | 391 +
components/theme-provider.tsx | 9 +
components/ui/accordion.tsx | 58 +
components/ui/alert-dialog.tsx | 141 +
components/ui/alert.tsx | 59 +
components/ui/aspect-ratio.tsx | 7 +
components/ui/avatar.tsx | 50 +
components/ui/badge.tsx | 36 +
components/ui/breadcrumb.tsx | 115 +
components/ui/button.tsx | 56 +
components/ui/calendar.tsx | 66 +
components/ui/card.tsx | 86 +
components/ui/carousel.tsx | 262 +
components/ui/chart.tsx | 365 +
components/ui/checkbox.tsx | 30 +
components/ui/collapsible.tsx | 11 +
components/ui/command.tsx | 155 +
components/ui/context-menu.tsx | 200 +
components/ui/dialog.tsx | 122 +
components/ui/drawer.tsx | 118 +
components/ui/dropdown-menu.tsx | 200 +
components/ui/form.tsx | 179 +
components/ui/hover-card.tsx | 29 +
components/ui/input-otp.tsx | 71 +
components/ui/input.tsx | 25 +
components/ui/label.tsx | 26 +
components/ui/menubar.tsx | 236 +
components/ui/navigation-menu.tsx | 128 +
components/ui/pagination.tsx | 117 +
components/ui/popover.tsx | 31 +
components/ui/progress.tsx | 28 +
components/ui/radio-group.tsx | 44 +
components/ui/resizable.tsx | 45 +
components/ui/scroll-area.tsx | 48 +
components/ui/select.tsx | 160 +
components/ui/separator.tsx | 31 +
components/ui/sheet.tsx | 140 +
components/ui/skeleton.tsx | 15 +
components/ui/slider.tsx | 28 +
components/ui/sonner.tsx | 31 +
components/ui/switch.tsx | 29 +
components/ui/table.tsx | 117 +
components/ui/tabs.tsx | 55 +
components/ui/textarea.tsx | 24 +
components/ui/toast.tsx | 129 +
components/ui/toaster.tsx | 35 +
components/ui/toggle-group.tsx | 61 +
components/ui/toggle.tsx | 45 +
components/ui/tooltip.tsx | 30 +
components/workflow/add-music-step.tsx | 317 +
.../workflow/final-composition-step.tsx | 288 +
.../workflow/generate-chapters-step.tsx | 230 +
components/workflow/generate-shots-step.tsx | 897 +++
components/workflow/input-script-step.tsx | 119 +
hooks/use-toast.ts | 191 +
lib/utils.ts | 6 +
next.config.js | 10 +
package-lock.json | 7119 +++++++++++++++++
package.json | 71 +
postcss.config.js | 6 +
tailwind.config.ts | 90 +
tsconfig.json | 27 +
82 files changed, 15308 insertions(+)
create mode 100644 .bolt/config.json
create mode 100644 .bolt/ignore
create mode 100644 .bolt/prompt
create mode 100644 .eslintrc.json
create mode 100644 .gitignore
create mode 100644 app/actors/page.tsx
create mode 100644 app/create/page.tsx
create mode 100644 app/globals.css
create mode 100644 app/history/page.tsx
create mode 100644 app/layout.tsx
create mode 100644 app/media/page.tsx
create mode 100644 app/page.tsx
create mode 100644 components.json
create mode 100644 components/layout/dashboard-layout.tsx
create mode 100644 components/layout/sidebar.tsx
create mode 100644 components/layout/top-bar.tsx
create mode 100644 components/pages/actors-library-page.tsx
create mode 100644 components/pages/create-video-workflow.tsx
create mode 100644 components/pages/history-page.tsx
create mode 100644 components/pages/home-page.tsx
create mode 100644 components/pages/media-library-page.tsx
create mode 100644 components/theme-provider.tsx
create mode 100644 components/ui/accordion.tsx
create mode 100644 components/ui/alert-dialog.tsx
create mode 100644 components/ui/alert.tsx
create mode 100644 components/ui/aspect-ratio.tsx
create mode 100644 components/ui/avatar.tsx
create mode 100644 components/ui/badge.tsx
create mode 100644 components/ui/breadcrumb.tsx
create mode 100644 components/ui/button.tsx
create mode 100644 components/ui/calendar.tsx
create mode 100644 components/ui/card.tsx
create mode 100644 components/ui/carousel.tsx
create mode 100644 components/ui/chart.tsx
create mode 100644 components/ui/checkbox.tsx
create mode 100644 components/ui/collapsible.tsx
create mode 100644 components/ui/command.tsx
create mode 100644 components/ui/context-menu.tsx
create mode 100644 components/ui/dialog.tsx
create mode 100644 components/ui/drawer.tsx
create mode 100644 components/ui/dropdown-menu.tsx
create mode 100644 components/ui/form.tsx
create mode 100644 components/ui/hover-card.tsx
create mode 100644 components/ui/input-otp.tsx
create mode 100644 components/ui/input.tsx
create mode 100644 components/ui/label.tsx
create mode 100644 components/ui/menubar.tsx
create mode 100644 components/ui/navigation-menu.tsx
create mode 100644 components/ui/pagination.tsx
create mode 100644 components/ui/popover.tsx
create mode 100644 components/ui/progress.tsx
create mode 100644 components/ui/radio-group.tsx
create mode 100644 components/ui/resizable.tsx
create mode 100644 components/ui/scroll-area.tsx
create mode 100644 components/ui/select.tsx
create mode 100644 components/ui/separator.tsx
create mode 100644 components/ui/sheet.tsx
create mode 100644 components/ui/skeleton.tsx
create mode 100644 components/ui/slider.tsx
create mode 100644 components/ui/sonner.tsx
create mode 100644 components/ui/switch.tsx
create mode 100644 components/ui/table.tsx
create mode 100644 components/ui/tabs.tsx
create mode 100644 components/ui/textarea.tsx
create mode 100644 components/ui/toast.tsx
create mode 100644 components/ui/toaster.tsx
create mode 100644 components/ui/toggle-group.tsx
create mode 100644 components/ui/toggle.tsx
create mode 100644 components/ui/tooltip.tsx
create mode 100644 components/workflow/add-music-step.tsx
create mode 100644 components/workflow/final-composition-step.tsx
create mode 100644 components/workflow/generate-chapters-step.tsx
create mode 100644 components/workflow/generate-shots-step.tsx
create mode 100644 components/workflow/input-script-step.tsx
create mode 100644 hooks/use-toast.ts
create mode 100644 lib/utils.ts
create mode 100644 next.config.js
create mode 100644 package-lock.json
create mode 100644 package.json
create mode 100644 postcss.config.js
create mode 100644 tailwind.config.ts
create mode 100644 tsconfig.json
diff --git a/.bolt/config.json b/.bolt/config.json
new file mode 100644
index 0000000..f236591
--- /dev/null
+++ b/.bolt/config.json
@@ -0,0 +1,3 @@
+{
+ "template": "nextjs-shadcn"
+}
diff --git a/.bolt/ignore b/.bolt/ignore
new file mode 100644
index 0000000..bbe3a15
--- /dev/null
+++ b/.bolt/ignore
@@ -0,0 +1,2 @@
+components/ui/*
+hooks/use-toast.ts
diff --git a/.bolt/prompt b/.bolt/prompt
new file mode 100644
index 0000000..88d020b
--- /dev/null
+++ b/.bolt/prompt
@@ -0,0 +1,9 @@
+For all designs I ask you to make, have them be beautiful, not cookie cutter. Make webpages that are fully featured and worthy for production.
+
+When using client-side hooks (useState and useEffect) in a component that's being treated as a Server Component by Next.js, always add the "use client" directive at the top of the file.
+
+Do not write code that will trigger this error: "Warning: Extra attributes from the server: %s%s""class,style"
+
+By default, this template supports JSX syntax with Tailwind CSS classes, the shadcn/ui library, React hooks, and Lucide React for icons. Do not install other packages for UI themes, icons, etc unless absolutely necessary or I request them.
+
+Use icons from lucide-react for logos.
diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 0000000..bffb357
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,3 @@
+{
+ "extends": "next/core-web-vitals"
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..9b1913e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,36 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# local env files
+.env
+.env*.local
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
diff --git a/app/actors/page.tsx b/app/actors/page.tsx
new file mode 100644
index 0000000..cb00c48
--- /dev/null
+++ b/app/actors/page.tsx
@@ -0,0 +1,10 @@
+import { DashboardLayout } from '@/components/layout/dashboard-layout';
+import { ActorsLibraryPage } from '@/components/pages/actors-library-page';
+
+export default function ActorsPage() {
+ return (
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/create/page.tsx b/app/create/page.tsx
new file mode 100644
index 0000000..016734b
--- /dev/null
+++ b/app/create/page.tsx
@@ -0,0 +1,10 @@
+import { DashboardLayout } from '@/components/layout/dashboard-layout';
+import { CreateVideoWorkflow } from '@/components/pages/create-video-workflow';
+
+export default function CreatePage() {
+ return (
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/globals.css b/app/globals.css
new file mode 100644
index 0000000..52f5692
--- /dev/null
+++ b/app/globals.css
@@ -0,0 +1,86 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+:root {
+ --foreground-rgb: 0, 0, 0;
+ --background-start-rgb: 214, 219, 220;
+ --background-end-rgb: 255, 255, 255;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --foreground-rgb: 255, 255, 255;
+ --background-start-rgb: 0, 0, 0;
+ --background-end-rgb: 0, 0, 0;
+ }
+}
+
+@layer base {
+ :root {
+ --background: 0 0% 100%;
+ --foreground: 0 0% 3.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 0 0% 3.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 0 0% 3.9%;
+ --primary: 0 0% 9%;
+ --primary-foreground: 0 0% 98%;
+ --secondary: 0 0% 96.1%;
+ --secondary-foreground: 0 0% 9%;
+ --muted: 0 0% 96.1%;
+ --muted-foreground: 0 0% 45.1%;
+ --accent: 0 0% 96.1%;
+ --accent-foreground: 0 0% 9%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 0 0% 89.8%;
+ --input: 0 0% 89.8%;
+ --ring: 0 0% 3.9%;
+ --chart-1: 12 76% 61%;
+ --chart-2: 173 58% 39%;
+ --chart-3: 197 37% 24%;
+ --chart-4: 43 74% 66%;
+ --chart-5: 27 87% 67%;
+ --radius: 0.5rem;
+ }
+ .dark {
+ --background: 0 0% 3.9%;
+ --foreground: 0 0% 98%;
+ --card: 0 0% 3.9%;
+ --card-foreground: 0 0% 98%;
+ --popover: 0 0% 3.9%;
+ --popover-foreground: 0 0% 98%;
+ --primary: 0 0% 98%;
+ --primary-foreground: 0 0% 9%;
+ --secondary: 0 0% 14.9%;
+ --secondary-foreground: 0 0% 98%;
+ --muted: 0 0% 14.9%;
+ --muted-foreground: 0 0% 63.9%;
+ --accent: 0 0% 14.9%;
+ --accent-foreground: 0 0% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 0 0% 14.9%;
+ --input: 0 0% 14.9%;
+ --ring: 0 0% 83.1%;
+ --chart-1: 220 70% 50%;
+ --chart-2: 160 60% 45%;
+ --chart-3: 30 80% 55%;
+ --chart-4: 280 65% 60%;
+ --chart-5: 340 75% 55%;
+ }
+}
+
+.hide-scrollbar::-webkit-scrollbar {
+ display: none;
+}
+
+@layer base {
+ * {
+ @apply border-border;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+}
diff --git a/app/history/page.tsx b/app/history/page.tsx
new file mode 100644
index 0000000..300dd4d
--- /dev/null
+++ b/app/history/page.tsx
@@ -0,0 +1,10 @@
+import { DashboardLayout } from '@/components/layout/dashboard-layout';
+import { HistoryPage } from '@/components/pages/history-page';
+
+export default function History() {
+ return (
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/layout.tsx b/app/layout.tsx
new file mode 100644
index 0000000..61e4a5e
--- /dev/null
+++ b/app/layout.tsx
@@ -0,0 +1,34 @@
+import './globals.css';
+import type { Metadata } from 'next';
+import { Inter } from 'next/font/google';
+import { ThemeProvider } from '@/components/theme-provider';
+import { Toaster } from '@/components/ui/sonner';
+
+const inter = Inter({ subsets: ['latin'] });
+
+export const metadata: Metadata = {
+ title: 'AI Video Studio - Create Amazing Videos with AI',
+ description: 'Professional AI-powered video creation platform with advanced editing tools',
+};
+
+export default function RootLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+
+
+ {children}
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/media/page.tsx b/app/media/page.tsx
new file mode 100644
index 0000000..f951411
--- /dev/null
+++ b/app/media/page.tsx
@@ -0,0 +1,10 @@
+import { DashboardLayout } from '@/components/layout/dashboard-layout';
+import { MediaLibraryPage } from '@/components/pages/media-library-page';
+
+export default function MediaPage() {
+ return (
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/page.tsx b/app/page.tsx
new file mode 100644
index 0000000..872a03e
--- /dev/null
+++ b/app/page.tsx
@@ -0,0 +1,10 @@
+import { DashboardLayout } from '@/components/layout/dashboard-layout';
+import { HomePage } from '@/components/pages/home-page';
+
+export default function Home() {
+ return (
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components.json b/components.json
new file mode 100644
index 0000000..c597462
--- /dev/null
+++ b/components.json
@@ -0,0 +1,20 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "default",
+ "rsc": true,
+ "tsx": true,
+ "tailwind": {
+ "config": "tailwind.config.ts",
+ "css": "app/globals.css",
+ "baseColor": "neutral",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ }
+}
diff --git a/components/layout/dashboard-layout.tsx b/components/layout/dashboard-layout.tsx
new file mode 100644
index 0000000..81f93d3
--- /dev/null
+++ b/components/layout/dashboard-layout.tsx
@@ -0,0 +1,25 @@
+"use client";
+
+import { useState } from 'react';
+import { Sidebar } from './sidebar';
+import { TopBar } from './top-bar';
+
+interface DashboardLayoutProps {
+ children: React.ReactNode;
+}
+
+export function DashboardLayout({ children }: DashboardLayoutProps) {
+ const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
+
+ return (
+
+
+
+
+
+ {children}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/layout/sidebar.tsx b/components/layout/sidebar.tsx
new file mode 100644
index 0000000..e37b9fd
--- /dev/null
+++ b/components/layout/sidebar.tsx
@@ -0,0 +1,123 @@
+"use client";
+
+import { useState } from 'react';
+import Link from 'next/link';
+import { usePathname } from 'next/navigation';
+import { cn } from '@/lib/utils';
+import { Button } from '@/components/ui/button';
+import { Separator } from '@/components/ui/separator';
+import {
+ Home,
+ FolderOpen,
+ Users,
+ Type,
+ Image,
+ History,
+ ChevronLeft,
+ ChevronRight,
+ Video,
+} from 'lucide-react';
+
+interface SidebarProps {
+ collapsed: boolean;
+ onToggle: (collapsed: boolean) => void;
+}
+
+const navigationItems = [
+ {
+ title: 'Main',
+ items: [
+ { name: 'Home', href: '/', icon: Home },
+ { name: 'Media Library', href: '/media', icon: FolderOpen },
+ { name: 'Actors Library', href: '/actors', icon: Users },
+ ],
+ },
+ {
+ title: 'Plugins',
+ items: [
+ { name: 'Text to Clip', href: '/plugins/text-to-clip', icon: Type },
+ { name: 'Text to Image', href: '/plugins/text-to-image', icon: Image },
+ ],
+ },
+ {
+ title: 'History',
+ items: [
+ { name: 'Task History', href: '/history', icon: History },
+ ],
+ },
+];
+
+export function Sidebar({ collapsed, onToggle }: SidebarProps) {
+ const pathname = usePathname();
+
+ return (
+
+
+ {/* Logo */}
+
+ {!collapsed && (
+
+
+
+ AI Studio
+
+
+ )}
+
onToggle(!collapsed)}
+ className="h-8 w-8 p-0"
+ >
+ {collapsed ? (
+
+ ) : (
+
+ )}
+
+
+
+ {/* Navigation */}
+
+ {navigationItems.map((section, index) => (
+
+ {!collapsed && (
+
+ {section.title}
+
+ )}
+
+ {section.items.map((item) => {
+ const isActive = pathname === item.href;
+ return (
+
+
+
+ {!collapsed && {item.name} }
+
+
+ );
+ })}
+
+ {index < navigationItems.length - 1 && (
+
+ )}
+
+ ))}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/layout/top-bar.tsx b/components/layout/top-bar.tsx
new file mode 100644
index 0000000..c18d8fc
--- /dev/null
+++ b/components/layout/top-bar.tsx
@@ -0,0 +1,71 @@
+"use client";
+
+import { Button } from '@/components/ui/button';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu';
+import { useTheme } from 'next-themes';
+import {
+ Sun,
+ Moon,
+ User,
+ Settings,
+ LogOut,
+ Bell,
+} from 'lucide-react';
+
+export function TopBar() {
+ const { theme, setTheme } = useTheme();
+
+ return (
+
+
+
+
AI Video Studio
+
+
+
+ {/* Notifications */}
+
+
+
+
+ {/* Theme Toggle */}
+ setTheme(theme === 'dark' ? 'light' : 'dark')}
+ >
+
+
+
+
+ {/* User Menu */}
+
+
+
+
+ User
+
+
+
+
+
+ Settings
+
+
+
+
+ Log out
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/pages/actors-library-page.tsx b/components/pages/actors-library-page.tsx
new file mode 100644
index 0000000..5963fd1
--- /dev/null
+++ b/components/pages/actors-library-page.tsx
@@ -0,0 +1,360 @@
+"use client";
+
+import { useState } from 'react';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Textarea } from '@/components/ui/textarea';
+import { Badge } from '@/components/ui/badge';
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '@/components/ui/dialog';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu';
+import {
+ Plus,
+ Upload,
+ Search,
+ MoreHorizontal,
+ Play,
+ Pause,
+ Wand2,
+ Edit,
+ Trash2,
+ Volume2,
+ User,
+} from 'lucide-react';
+
+const mockActors = [
+ {
+ id: 1,
+ name: 'Sarah Chen',
+ description: 'Professional corporate presenter with clear articulation',
+ avatar: 'https://images.pexels.com/photos/774909/pexels-photo-774909.jpeg?auto=compress&cs=tinysrgb&w=200',
+ voice: {
+ type: 'generated',
+ name: 'Professional Female',
+ sample: 'Hello, I\'m Sarah and I\'ll be your guide through this presentation.',
+ },
+ tags: ['corporate', 'professional', 'female'],
+ createdAt: '2024-01-15',
+ usageCount: 12,
+ },
+ {
+ id: 2,
+ name: 'Dr. Marcus Webb',
+ description: 'Expert educator with authoritative voice for technical content',
+ avatar: 'https://images.pexels.com/photos/1222271/pexels-photo-1222271.jpeg?auto=compress&cs=tinysrgb&w=200',
+ voice: {
+ type: 'generated',
+ name: 'Expert Male',
+ sample: 'Welcome to today\'s lesson on advanced artificial intelligence.',
+ },
+ tags: ['education', 'expert', 'male'],
+ createdAt: '2024-01-12',
+ usageCount: 8,
+ },
+ {
+ id: 3,
+ name: 'Alex Rivera',
+ description: 'Energetic host perfect for engaging, casual content',
+ avatar: 'https://images.pexels.com/photos/1239291/pexels-photo-1239291.jpeg?auto=compress&cs=tinysrgb&w=200',
+ voice: {
+ type: 'uploaded',
+ name: 'Custom Voice',
+ sample: 'Hey everyone! Ready to dive into something amazing?',
+ },
+ tags: ['casual', 'energetic', 'neutral'],
+ createdAt: '2024-01-10',
+ usageCount: 5,
+ },
+];
+
+const voiceTypes = [
+ 'Professional Female',
+ 'Professional Male',
+ 'Casual Female',
+ 'Casual Male',
+ 'Expert Female',
+ 'Expert Male',
+ 'Enthusiastic Neutral',
+];
+
+export function ActorsLibraryPage() {
+ const [searchQuery, setSearchQuery] = useState('');
+ const [playingVoice, setPlayingVoice] = useState(null);
+ const [newActorName, setNewActorName] = useState('');
+ const [newActorDescription, setNewActorDescription] = useState('');
+ const [newActorPrompt, setNewActorPrompt] = useState('');
+
+ const filteredActors = mockActors.filter(actor =>
+ actor.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ actor.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ actor.tags.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase()))
+ );
+
+ const toggleVoicePlayback = (actorId: number) => {
+ setPlayingVoice(playingVoice === actorId ? null : actorId);
+ };
+
+ return (
+
+ {/* Header */}
+
+
+
Actors Library
+
+ Create and manage AI actors with custom voices for your videos
+
+
+
+
+
+
+ Create Actor
+
+
+
+
+ Create New Actor
+
+
+
+ Upload Image
+ AI Generated
+
+
+
+
+
+
+
+
+
+ Click to upload or drag and drop
+
+
PNG, JPG up to 10MB
+
+
+
+
+
+
+
+
+
+ AI Generation Prompt
+
+
+
+ Generate Actor Image
+
+
+
+
+
+
+ Actor Name
+ setNewActorName(e.target.value)}
+ className="mt-1"
+ />
+
+
+ Description
+
+
+
+
+ Cancel
+ Create Actor
+
+
+
+
+
+
+ {/* Search */}
+
+
+
+
+
+ setSearchQuery(e.target.value)}
+ className="pl-10"
+ />
+
+
+ {filteredActors.length} actors
+
+
+
+
+
+ {/* Actors Grid */}
+
+ {filteredActors.map((actor) => (
+
+
+
+
+
+
+
+
+
+
+
{actor.name}
+
+ {actor.description}
+
+
+
+
+
+
+
+
+
+
+
+ Edit
+
+
+ Duplicate
+
+
+
+ Delete
+
+
+
+
+
+
+ {/* Tags */}
+
+ {actor.tags.map((tag) => (
+
+ {tag}
+
+ ))}
+
+
+ {/* Voice Section */}
+
+
+
+
+ Voice
+
+
+ {actor.voice.type === 'generated' ? 'AI Generated' : 'Custom'}
+
+
+
+
+
+
{actor.voice.name}
+
toggleVoicePlayback(actor.id)}
+ >
+ {playingVoice === actor.id ? (
+
+ ) : (
+
+ )}
+
+
+
+ "{actor.voice.sample}"
+
+
+
+
+
+
+ Upload Voice
+
+
+
+ Generate New
+
+
+
+
+ {/* Stats */}
+
+ Used in {actor.usageCount} videos
+ Created {new Date(actor.createdAt).toLocaleDateString()}
+
+
+
+ ))}
+
+
+ {filteredActors.length === 0 && (
+
+
+
+
+
+
+
+
No actors found
+
+ {searchQuery
+ ? `No actors match "${searchQuery}". Try a different search term.`
+ : 'Get started by creating your first AI actor'
+ }
+
+
+ {!searchQuery && (
+
+
+
+
+ Create Your First Actor
+
+
+
+ )}
+
+
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/components/pages/create-video-workflow.tsx b/components/pages/create-video-workflow.tsx
new file mode 100644
index 0000000..82632d5
--- /dev/null
+++ b/components/pages/create-video-workflow.tsx
@@ -0,0 +1,147 @@
+"use client";
+
+import { useState } from 'react';
+import { Card, CardContent } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import { Progress } from '@/components/ui/progress';
+import { Separator } from '@/components/ui/separator';
+import {
+ ArrowLeft,
+ ArrowRight,
+ FileText,
+ Users,
+ Film,
+ Music,
+ Video,
+ CheckCircle,
+} from 'lucide-react';
+import { InputScriptStep } from '@/components/workflow/input-script-step';
+import { GenerateChaptersStep } from '@/components/workflow/generate-chapters-step';
+import { GenerateShotsStep } from '@/components/workflow/generate-shots-step';
+import { AddMusicStep } from '@/components/workflow/add-music-step';
+import { FinalCompositionStep } from '@/components/workflow/final-composition-step';
+
+const steps = [
+ { id: 1, name: 'Input Script', icon: FileText, description: 'Enter your script and settings' },
+ { id: 2, name: 'Generate Chapters', icon: Users, description: 'AI splits script and assigns actors' },
+ { id: 3, name: 'Generate Shots', icon: Film, description: 'Create storyboard and scenes' },
+ { id: 4, name: 'Add Music', icon: Music, description: 'Background music and audio' },
+ { id: 5, name: 'Final Video', icon: Video, description: 'Compose and export video' },
+];
+
+export function CreateVideoWorkflow() {
+ const [currentStep, setCurrentStep] = useState(1);
+ const [completedSteps, setCompletedSteps] = useState([]);
+
+ const handleNext = () => {
+ if (currentStep < steps.length) {
+ setCompletedSteps([...completedSteps, currentStep]);
+ setCurrentStep(currentStep + 1);
+ }
+ };
+
+ const handlePrevious = () => {
+ if (currentStep > 1) {
+ setCurrentStep(currentStep - 1);
+ }
+ };
+
+ const handleStepClick = (stepId: number) => {
+ if (stepId <= currentStep || completedSteps.includes(stepId)) {
+ setCurrentStep(stepId);
+ }
+ };
+
+ const renderStepContent = () => {
+ switch (currentStep) {
+ case 1:
+ return ;
+ case 2:
+ return ;
+ case 3:
+ return ;
+ case 4:
+ return ;
+ case 5:
+ return ;
+ default:
+ return null;
+ }
+ };
+
+ const progress = ((currentStep - 1) / (steps.length - 1)) * 100;
+
+ return (
+
+ {/* Header */}
+
+
+
window.history.back()}>
+
+ Back to Home
+
+
+
+
+
Create AI Video
+
+ Follow these steps to create your AI-powered video. You can navigate back and forth to make changes.
+
+
+
+
+ {/* Steps Navigation */}
+
+
+
+ {steps.map((step, index) => (
+
+
handleStepClick(step.id)}
+ >
+
+ {completedSteps.includes(step.id) ? (
+
+ ) : (
+
+ )}
+
+
+
{step.name}
+
{step.description}
+
+
+ {index < steps.length - 1 && (
+
+ )}
+
+ ))}
+
+
+
+
+ {/* Step Content */}
+
+ {renderStepContent()}
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/pages/history-page.tsx b/components/pages/history-page.tsx
new file mode 100644
index 0000000..21f0e00
--- /dev/null
+++ b/components/pages/history-page.tsx
@@ -0,0 +1,331 @@
+"use client";
+
+import { useState } from 'react';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Badge } from '@/components/ui/badge';
+import { Progress } from '@/components/ui/progress';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu';
+import {
+ Search,
+ Filter,
+ MoreHorizontal,
+ Play,
+ Download,
+ Trash2,
+ RefreshCw,
+ Clock,
+ CheckCircle,
+ XCircle,
+ Eye,
+} from 'lucide-react';
+
+const mockTasks = [
+ {
+ id: 1,
+ title: 'Tech Product Demo',
+ status: 'completed',
+ progress: 100,
+ duration: '2:45',
+ createdAt: '2024-01-15T10:30:00Z',
+ completedAt: '2024-01-15T11:15:00Z',
+ chapters: 4,
+ actors: ['Sarah Chen', 'Dr. Marcus Webb'],
+ thumbnail: 'https://images.pexels.com/photos/3861969/pexels-photo-3861969.jpeg?auto=compress&cs=tinysrgb&w=300',
+ },
+ {
+ id: 2,
+ title: 'Marketing Campaign Video',
+ status: 'processing',
+ progress: 65,
+ duration: '1:30',
+ createdAt: '2024-01-14T15:20:00Z',
+ completedAt: null,
+ chapters: 3,
+ actors: ['Alex Rivera'],
+ thumbnail: 'https://images.pexels.com/photos/3184287/pexels-photo-3184287.jpeg?auto=compress&cs=tinysrgb&w=300',
+ },
+ {
+ id: 3,
+ title: 'Educational Content Series',
+ status: 'completed',
+ progress: 100,
+ duration: '5:20',
+ createdAt: '2024-01-12T09:00:00Z',
+ completedAt: '2024-01-12T10:30:00Z',
+ chapters: 6,
+ actors: ['Dr. Marcus Webb', 'Sarah Chen'],
+ thumbnail: 'https://images.pexels.com/photos/3184465/pexels-photo-3184465.jpeg?auto=compress&cs=tinysrgb&w=300',
+ },
+ {
+ id: 4,
+ title: 'Company Introduction',
+ status: 'failed',
+ progress: 0,
+ duration: '0:00',
+ createdAt: '2024-01-10T14:45:00Z',
+ completedAt: null,
+ chapters: 2,
+ actors: ['Sarah Chen'],
+ thumbnail: null,
+ },
+ {
+ id: 5,
+ title: 'Quarterly Report Presentation',
+ status: 'processing',
+ progress: 25,
+ duration: '3:15',
+ createdAt: '2024-01-09T11:00:00Z',
+ completedAt: null,
+ chapters: 5,
+ actors: ['Dr. Marcus Webb', 'Alex Rivera'],
+ thumbnail: 'https://images.pexels.com/photos/3184291/pexels-photo-3184291.jpeg?auto=compress&cs=tinysrgb&w=300',
+ },
+];
+
+const statusConfig = {
+ completed: {
+ icon: CheckCircle,
+ color: 'text-green-600',
+ bgColor: 'bg-green-100 dark:bg-green-900/20',
+ label: 'Completed',
+ },
+ processing: {
+ icon: RefreshCw,
+ color: 'text-blue-600',
+ bgColor: 'bg-blue-100 dark:bg-blue-900/20',
+ label: 'Processing',
+ },
+ failed: {
+ icon: XCircle,
+ color: 'text-red-600',
+ bgColor: 'bg-red-100 dark:bg-red-900/20',
+ label: 'Failed',
+ },
+};
+
+export function HistoryPage() {
+ const [searchQuery, setSearchQuery] = useState('');
+ const [statusFilter, setStatusFilter] = useState('all');
+
+ const filteredTasks = mockTasks.filter(task => {
+ const matchesSearch = task.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ task.actors.some(actor => actor.toLowerCase().includes(searchQuery.toLowerCase()));
+ const matchesStatus = statusFilter === 'all' || task.status === statusFilter;
+ return matchesSearch && matchesStatus;
+ });
+
+ const formatDate = (dateString: string) => {
+ return new Date(dateString).toLocaleString();
+ };
+
+ const getTimeSince = (dateString: string) => {
+ const now = new Date();
+ const date = new Date(dateString);
+ const diffInHours = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60));
+
+ if (diffInHours < 1) return 'Less than an hour ago';
+ if (diffInHours < 24) return `${diffInHours} hours ago`;
+ const diffInDays = Math.floor(diffInHours / 24);
+ return `${diffInDays} days ago`;
+ };
+
+ return (
+
+ {/* Header */}
+
+
+
Task History
+
+ Track the progress and manage your video generation tasks
+
+
+
+
+ {/* Filters */}
+
+
+
+
+
+
+ setSearchQuery(e.target.value)}
+ className="pl-10 w-64"
+ />
+
+
+
+
+
+ Status: {statusFilter === 'all' ? 'All' : statusConfig[statusFilter as keyof typeof statusConfig]?.label}
+
+
+
+ setStatusFilter('all')}>
+ All Status
+
+ setStatusFilter('completed')}>
+ Completed
+
+ setStatusFilter('processing')}>
+ Processing
+
+ setStatusFilter('failed')}>
+ Failed
+
+
+
+
+
+ {filteredTasks.length} tasks
+
+
+
+
+
+ {/* Tasks List */}
+
+ {filteredTasks.map((task) => {
+ const StatusIcon = statusConfig[task.status as keyof typeof statusConfig].icon;
+ const statusProps = statusConfig[task.status as keyof typeof statusConfig];
+
+ return (
+
+
+
+ {/* Thumbnail */}
+
+ {task.thumbnail ? (
+
+ ) : (
+
+ 🎬
+
+ )}
+ {task.status === 'processing' && (
+
+
+
+ )}
+
+
+ {/* Task Info */}
+
+
+
{task.title}
+
+
+
+
+
+
+
+ {task.status === 'completed' && (
+ <>
+
+
+ Preview
+
+
+
+ Download
+
+ >
+ )}
+ {task.status === 'failed' && (
+
+
+ Retry
+
+ )}
+
+
+ View Details
+
+
+
+ Delete
+
+
+
+
+
+
+
+
+ {statusProps.label}
+
+
+
+ {task.duration}
+
+
+ {task.chapters} chapters
+
+
+ {task.actors.join(', ')}
+
+
+
+ {task.status === 'processing' && (
+
+
+ Processing...
+ {task.progress}%
+
+
+
+ )}
+
+
+ Started {getTimeSince(task.createdAt)}
+ {task.completedAt && (
+ Completed {formatDate(task.completedAt)}
+ )}
+
+
+
+
+
+ );
+ })}
+
+
+ {filteredTasks.length === 0 && (
+
+
+
+
+
+
+
+
No tasks found
+
+ {searchQuery || statusFilter !== 'all'
+ ? 'No tasks match your current filters. Try adjusting your search.'
+ : 'You haven\'t created any videos yet. Start by creating your first AI video!'
+ }
+
+
+
+
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/components/pages/home-page.tsx b/components/pages/home-page.tsx
new file mode 100644
index 0000000..0e01b7b
--- /dev/null
+++ b/components/pages/home-page.tsx
@@ -0,0 +1,163 @@
+"use client";
+
+import { useState } from 'react';
+import Link from 'next/link';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { Separator } from '@/components/ui/separator';
+import {
+ Play,
+ Clock,
+ Eye,
+ MoreHorizontal,
+ Plus,
+ Sparkles,
+} from 'lucide-react';
+
+const mockTasks = [
+ {
+ id: 1,
+ title: 'Tech Product Demo',
+ status: 'completed',
+ duration: '2:45',
+ views: 1234,
+ createdAt: '2024-01-15',
+ thumbnail: 'https://images.pexels.com/photos/3861969/pexels-photo-3861969.jpeg?auto=compress&cs=tinysrgb&w=400',
+ },
+ {
+ id: 2,
+ title: 'Marketing Campaign Video',
+ status: 'processing',
+ duration: '1:30',
+ views: 0,
+ createdAt: '2024-01-14',
+ thumbnail: 'https://images.pexels.com/photos/3184287/pexels-photo-3184287.jpeg?auto=compress&cs=tinysrgb&w=400',
+ },
+ {
+ id: 3,
+ title: 'Educational Content',
+ status: 'completed',
+ duration: '5:20',
+ views: 856,
+ createdAt: '2024-01-12',
+ thumbnail: 'https://images.pexels.com/photos/3184465/pexels-photo-3184465.jpeg?auto=compress&cs=tinysrgb&w=400',
+ },
+];
+
+export function HomePage() {
+ return (
+
+ {/* Hero Section */}
+
+
+
+ Create Amazing Videos with{' '}
+
+ AI Power
+
+
+
+ Transform your ideas into professional videos using our advanced AI technology.
+ From script to screen in minutes, not hours.
+
+
+
+
+
+
+
+ Create AI Video
+
+
+
+
+
+
+
+ {/* Recent Projects */}
+
+
+
Recent Projects
+
+
+ New Project
+
+
+
+
+ {mockTasks.map((task) => (
+
+
+
+
+
+
+ {task.status}
+
+
+
+
+ {task.title}
+
+
+
+ {task.duration}
+
+
+
+ {task.views}
+
+
+
+ Created on {new Date(task.createdAt).toLocaleDateString()}
+
+
+
+
+
+
+
+
+ ))}
+
+
+ {mockTasks.length === 0 && (
+
+
+
+
+
+
+
+
No projects yet
+
+ Get started by creating your first AI-powered video
+
+
+
+
+
+ Create Your First Video
+
+
+
+
+
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/pages/media-library-page.tsx b/components/pages/media-library-page.tsx
new file mode 100644
index 0000000..2ffe52c
--- /dev/null
+++ b/components/pages/media-library-page.tsx
@@ -0,0 +1,391 @@
+"use client";
+
+import { useState } from 'react';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Badge } from '@/components/ui/badge';
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '@/components/ui/dialog';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu';
+import {
+ Plus,
+ Upload,
+ Search,
+ FolderPlus,
+ MoreHorizontal,
+ Play,
+ Download,
+ Trash2,
+ Eye,
+ Grid3X3,
+ List,
+} from 'lucide-react';
+
+const mockAlbums = [
+ {
+ id: 1,
+ name: 'Tech Videos',
+ itemCount: 12,
+ thumbnail: 'https://images.pexels.com/photos/3861969/pexels-photo-3861969.jpeg?auto=compress&cs=tinysrgb&w=300',
+ createdAt: '2024-01-15',
+ },
+ {
+ id: 2,
+ name: 'Marketing Assets',
+ itemCount: 8,
+ thumbnail: 'https://images.pexels.com/photos/3184287/pexels-photo-3184287.jpeg?auto=compress&cs=tinysrgb&w=300',
+ createdAt: '2024-01-10',
+ },
+ {
+ id: 3,
+ name: 'Stock Footage',
+ itemCount: 24,
+ thumbnail: 'https://images.pexels.com/photos/3184465/pexels-photo-3184465.jpeg?auto=compress&cs=tinysrgb&w=300',
+ createdAt: '2024-01-05',
+ },
+];
+
+const mockMediaFiles = [
+ {
+ id: 1,
+ name: 'intro-video.mp4',
+ type: 'video',
+ size: '45.2 MB',
+ duration: '2:30',
+ thumbnail: 'https://images.pexels.com/photos/3861969/pexels-photo-3861969.jpeg?auto=compress&cs=tinysrgb&w=300',
+ albumId: 1,
+ uploadedAt: '2024-01-15',
+ },
+ {
+ id: 2,
+ name: 'product-demo.mp4',
+ type: 'video',
+ size: '78.9 MB',
+ duration: '4:15',
+ thumbnail: 'https://images.pexels.com/photos/3184287/pexels-photo-3184287.jpeg?auto=compress&cs=tinysrgb&w=300',
+ albumId: 1,
+ uploadedAt: '2024-01-14',
+ },
+ {
+ id: 3,
+ name: 'background-music.mp3',
+ type: 'audio',
+ size: '8.3 MB',
+ duration: '3:45',
+ thumbnail: null,
+ albumId: 2,
+ uploadedAt: '2024-01-12',
+ },
+ {
+ id: 4,
+ name: 'hero-image.jpg',
+ type: 'image',
+ size: '2.1 MB',
+ duration: null,
+ thumbnail: 'https://images.pexels.com/photos/3184465/pexels-photo-3184465.jpeg?auto=compress&cs=tinysrgb&w=300',
+ albumId: 2,
+ uploadedAt: '2024-01-10',
+ },
+];
+
+export function MediaLibraryPage() {
+ const [searchQuery, setSearchQuery] = useState('');
+ const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
+ const [selectedAlbum, setSelectedAlbum] = useState(null);
+
+ const filteredFiles = mockMediaFiles.filter(file =>
+ file.name.toLowerCase().includes(searchQuery.toLowerCase()) &&
+ (selectedAlbum ? file.albumId === selectedAlbum : true)
+ );
+
+ const getFileIcon = (type: string) => {
+ switch (type) {
+ case 'video':
+ return '🎥';
+ case 'audio':
+ return '🎵';
+ case 'image':
+ return '🖼️';
+ default:
+ return '📄';
+ }
+ };
+
+ return (
+
+ {/* Header */}
+
+
+
Media Library
+
+ Organize and manage your video, audio, and image assets
+
+
+
+
+
+
+
+ New Album
+
+
+
+
+ Create New Album
+
+
+
+
+ Cancel
+ Create Album
+
+
+
+
+
+
+ Upload Files
+
+
+
+
+ {/* Search and Filters */}
+
+
+
+
+
+
+ setSearchQuery(e.target.value)}
+ className="pl-10 w-64"
+ />
+
+
setSelectedAlbum(null)}
+ >
+ All Files
+
+ {mockAlbums.map((album) => (
+
setSelectedAlbum(album.id)}
+ >
+ {album.name}
+
+ ))}
+
+
+ setViewMode('grid')}
+ >
+
+
+ setViewMode('list')}
+ >
+
+
+
+
+
+
+
+
+
+ Files
+ Albums
+
+
+
+ {viewMode === 'grid' ? (
+
+ {filteredFiles.map((file) => (
+
+
+
+ {file.thumbnail ? (
+
+ ) : (
+
+ {getFileIcon(file.type)}
+
+ )}
+
+
+
+
+
+ {file.type === 'video' && (
+
+
+
+ )}
+
+
+
+ {file.type}
+
+ {file.duration && (
+
+ {file.duration}
+
+ )}
+
+
+
+
+
{file.name}
+
+ {file.size}
+
+
+
+
+
+
+
+
+
+ Download
+
+
+
+ Delete
+
+
+
+
+
+
+
+ ))}
+
+ ) : (
+
+
+
+ {filteredFiles.map((file) => (
+
+
+ {file.thumbnail ? (
+
+ ) : (
+
+ {getFileIcon(file.type)}
+
+ )}
+
+
{file.name}
+
+ {file.size} • {file.type}
+ {file.duration && ` • ${file.duration}`}
+
+
+
+
+
+ {file.uploadedAt}
+
+
+
+
+
+
+
+
+
+
+ Download
+
+
+
+ Delete
+
+
+
+
+
+ ))}
+
+
+
+ )}
+
+
+
+
+ {mockAlbums.map((album) => (
+
+
+
+
+
+
+
+
+
+
{album.name}
+
+
+
+
+
+
+
+ Edit
+
+ Delete
+
+
+
+
+
+ {album.itemCount} items
+ Created {new Date(album.createdAt).toLocaleDateString()}
+
+
+
+
+ ))}
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/theme-provider.tsx b/components/theme-provider.tsx
new file mode 100644
index 0000000..4203e6a
--- /dev/null
+++ b/components/theme-provider.tsx
@@ -0,0 +1,9 @@
+"use client"
+
+import * as React from "react"
+import { ThemeProvider as NextThemesProvider } from "next-themes"
+import { type ThemeProviderProps } from "next-themes/dist/types"
+
+export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
+ return {children}
+}
\ No newline at end of file
diff --git a/components/ui/accordion.tsx b/components/ui/accordion.tsx
new file mode 100644
index 0000000..84bf2eb
--- /dev/null
+++ b/components/ui/accordion.tsx
@@ -0,0 +1,58 @@
+'use client';
+
+import * as React from 'react';
+import * as AccordionPrimitive from '@radix-ui/react-accordion';
+import { ChevronDown } from 'lucide-react';
+
+import { cn } from '@/lib/utils';
+
+const Accordion = AccordionPrimitive.Root;
+
+const AccordionItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AccordionItem.displayName = 'AccordionItem';
+
+const AccordionTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ svg]:rotate-180',
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+));
+AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
+
+const AccordionContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ {children}
+
+));
+
+AccordionContent.displayName = AccordionPrimitive.Content.displayName;
+
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
diff --git a/components/ui/alert-dialog.tsx b/components/ui/alert-dialog.tsx
new file mode 100644
index 0000000..5cba559
--- /dev/null
+++ b/components/ui/alert-dialog.tsx
@@ -0,0 +1,141 @@
+'use client';
+
+import * as React from 'react';
+import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
+
+import { cn } from '@/lib/utils';
+import { buttonVariants } from '@/components/ui/button';
+
+const AlertDialog = AlertDialogPrimitive.Root;
+
+const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
+
+const AlertDialogPortal = AlertDialogPrimitive.Portal;
+
+const AlertDialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
+
+const AlertDialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+));
+AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
+
+const AlertDialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+);
+AlertDialogHeader.displayName = 'AlertDialogHeader';
+
+const AlertDialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+);
+AlertDialogFooter.displayName = 'AlertDialogFooter';
+
+const AlertDialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
+
+const AlertDialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AlertDialogDescription.displayName =
+ AlertDialogPrimitive.Description.displayName;
+
+const AlertDialogAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
+
+const AlertDialogCancel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
+
+export {
+ AlertDialog,
+ AlertDialogPortal,
+ AlertDialogOverlay,
+ AlertDialogTrigger,
+ AlertDialogContent,
+ AlertDialogHeader,
+ AlertDialogFooter,
+ AlertDialogTitle,
+ AlertDialogDescription,
+ AlertDialogAction,
+ AlertDialogCancel,
+};
diff --git a/components/ui/alert.tsx b/components/ui/alert.tsx
new file mode 100644
index 0000000..d2b59cc
--- /dev/null
+++ b/components/ui/alert.tsx
@@ -0,0 +1,59 @@
+import * as React from 'react';
+import { cva, type VariantProps } from 'class-variance-authority';
+
+import { cn } from '@/lib/utils';
+
+const alertVariants = cva(
+ 'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',
+ {
+ variants: {
+ variant: {
+ default: 'bg-background text-foreground',
+ destructive:
+ 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ },
+ }
+);
+
+const Alert = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & VariantProps
+>(({ className, variant, ...props }, ref) => (
+
+));
+Alert.displayName = 'Alert';
+
+const AlertTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+AlertTitle.displayName = 'AlertTitle';
+
+const AlertDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+AlertDescription.displayName = 'AlertDescription';
+
+export { Alert, AlertTitle, AlertDescription };
diff --git a/components/ui/aspect-ratio.tsx b/components/ui/aspect-ratio.tsx
new file mode 100644
index 0000000..aaabffb
--- /dev/null
+++ b/components/ui/aspect-ratio.tsx
@@ -0,0 +1,7 @@
+'use client';
+
+import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio';
+
+const AspectRatio = AspectRatioPrimitive.Root;
+
+export { AspectRatio };
diff --git a/components/ui/avatar.tsx b/components/ui/avatar.tsx
new file mode 100644
index 0000000..1346957
--- /dev/null
+++ b/components/ui/avatar.tsx
@@ -0,0 +1,50 @@
+'use client';
+
+import * as React from 'react';
+import * as AvatarPrimitive from '@radix-ui/react-avatar';
+
+import { cn } from '@/lib/utils';
+
+const Avatar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+Avatar.displayName = AvatarPrimitive.Root.displayName;
+
+const AvatarImage = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AvatarImage.displayName = AvatarPrimitive.Image.displayName;
+
+const AvatarFallback = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
+
+export { Avatar, AvatarImage, AvatarFallback };
diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx
new file mode 100644
index 0000000..2eb790a
--- /dev/null
+++ b/components/ui/badge.tsx
@@ -0,0 +1,36 @@
+import * as React from 'react';
+import { cva, type VariantProps } from 'class-variance-authority';
+
+import { cn } from '@/lib/utils';
+
+const badgeVariants = cva(
+ 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
+ {
+ variants: {
+ variant: {
+ default:
+ 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
+ secondary:
+ 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
+ destructive:
+ 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
+ outline: 'text-foreground',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ },
+ }
+);
+
+export interface BadgeProps
+ extends React.HTMLAttributes,
+ VariantProps {}
+
+function Badge({ className, variant, ...props }: BadgeProps) {
+ return (
+
+ );
+}
+
+export { Badge, badgeVariants };
diff --git a/components/ui/breadcrumb.tsx b/components/ui/breadcrumb.tsx
new file mode 100644
index 0000000..8b62197
--- /dev/null
+++ b/components/ui/breadcrumb.tsx
@@ -0,0 +1,115 @@
+import * as React from 'react';
+import { Slot } from '@radix-ui/react-slot';
+import { ChevronRight, MoreHorizontal } from 'lucide-react';
+
+import { cn } from '@/lib/utils';
+
+const Breadcrumb = React.forwardRef<
+ HTMLElement,
+ React.ComponentPropsWithoutRef<'nav'> & {
+ separator?: React.ReactNode;
+ }
+>(({ ...props }, ref) => );
+Breadcrumb.displayName = 'Breadcrumb';
+
+const BreadcrumbList = React.forwardRef<
+ HTMLOListElement,
+ React.ComponentPropsWithoutRef<'ol'>
+>(({ className, ...props }, ref) => (
+
+));
+BreadcrumbList.displayName = 'BreadcrumbList';
+
+const BreadcrumbItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentPropsWithoutRef<'li'>
+>(({ className, ...props }, ref) => (
+
+));
+BreadcrumbItem.displayName = 'BreadcrumbItem';
+
+const BreadcrumbLink = React.forwardRef<
+ HTMLAnchorElement,
+ React.ComponentPropsWithoutRef<'a'> & {
+ asChild?: boolean;
+ }
+>(({ asChild, className, ...props }, ref) => {
+ const Comp = asChild ? Slot : 'a';
+
+ return (
+
+ );
+});
+BreadcrumbLink.displayName = 'BreadcrumbLink';
+
+const BreadcrumbPage = React.forwardRef<
+ HTMLSpanElement,
+ React.ComponentPropsWithoutRef<'span'>
+>(({ className, ...props }, ref) => (
+
+));
+BreadcrumbPage.displayName = 'BreadcrumbPage';
+
+const BreadcrumbSeparator = ({
+ children,
+ className,
+ ...props
+}: React.ComponentProps<'li'>) => (
+ svg]:size-3.5', className)}
+ {...props}
+ >
+ {children ?? }
+
+);
+BreadcrumbSeparator.displayName = 'BreadcrumbSeparator';
+
+const BreadcrumbEllipsis = ({
+ className,
+ ...props
+}: React.ComponentProps<'span'>) => (
+
+
+ More
+
+);
+BreadcrumbEllipsis.displayName = 'BreadcrumbElipssis';
+
+export {
+ Breadcrumb,
+ BreadcrumbList,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+ BreadcrumbEllipsis,
+};
diff --git a/components/ui/button.tsx b/components/ui/button.tsx
new file mode 100644
index 0000000..81e2e6e
--- /dev/null
+++ b/components/ui/button.tsx
@@ -0,0 +1,56 @@
+import * as React from 'react';
+import { Slot } from '@radix-ui/react-slot';
+import { cva, type VariantProps } from 'class-variance-authority';
+
+import { cn } from '@/lib/utils';
+
+const buttonVariants = cva(
+ 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
+ {
+ variants: {
+ variant: {
+ default: 'bg-primary text-primary-foreground hover:bg-primary/90',
+ destructive:
+ 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
+ outline:
+ 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
+ secondary:
+ 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
+ ghost: 'hover:bg-accent hover:text-accent-foreground',
+ link: 'text-primary underline-offset-4 hover:underline',
+ },
+ size: {
+ default: 'h-10 px-4 py-2',
+ sm: 'h-9 rounded-md px-3',
+ lg: 'h-11 rounded-md px-8',
+ icon: 'h-10 w-10',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ size: 'default',
+ },
+ }
+);
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean;
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : 'button';
+ return (
+
+ );
+ }
+);
+Button.displayName = 'Button';
+
+export { Button, buttonVariants };
diff --git a/components/ui/calendar.tsx b/components/ui/calendar.tsx
new file mode 100644
index 0000000..7ee4f82
--- /dev/null
+++ b/components/ui/calendar.tsx
@@ -0,0 +1,66 @@
+'use client';
+
+import * as React from 'react';
+import { ChevronLeft, ChevronRight } from 'lucide-react';
+import { DayPicker } from 'react-day-picker';
+
+import { cn } from '@/lib/utils';
+import { buttonVariants } from '@/components/ui/button';
+
+export type CalendarProps = React.ComponentProps;
+
+function Calendar({
+ className,
+ classNames,
+ showOutsideDays = true,
+ ...props
+}: CalendarProps) {
+ return (
+ ,
+ IconRight: ({ ...props }) => ,
+ }}
+ {...props}
+ />
+ );
+}
+Calendar.displayName = 'Calendar';
+
+export { Calendar };
diff --git a/components/ui/card.tsx b/components/ui/card.tsx
new file mode 100644
index 0000000..520ecb4
--- /dev/null
+++ b/components/ui/card.tsx
@@ -0,0 +1,86 @@
+import * as React from 'react';
+
+import { cn } from '@/lib/utils';
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+Card.displayName = 'Card';
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardHeader.displayName = 'CardHeader';
+
+const CardTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardTitle.displayName = 'CardTitle';
+
+const CardDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardDescription.displayName = 'CardDescription';
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardContent.displayName = 'CardContent';
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardFooter.displayName = 'CardFooter';
+
+export {
+ Card,
+ CardHeader,
+ CardFooter,
+ CardTitle,
+ CardDescription,
+ CardContent,
+};
diff --git a/components/ui/carousel.tsx b/components/ui/carousel.tsx
new file mode 100644
index 0000000..f689200
--- /dev/null
+++ b/components/ui/carousel.tsx
@@ -0,0 +1,262 @@
+'use client';
+
+import * as React from 'react';
+import useEmblaCarousel, {
+ type UseEmblaCarouselType,
+} from 'embla-carousel-react';
+import { ArrowLeft, ArrowRight } from 'lucide-react';
+
+import { cn } from '@/lib/utils';
+import { Button } from '@/components/ui/button';
+
+type CarouselApi = UseEmblaCarouselType[1];
+type UseCarouselParameters = Parameters;
+type CarouselOptions = UseCarouselParameters[0];
+type CarouselPlugin = UseCarouselParameters[1];
+
+type CarouselProps = {
+ opts?: CarouselOptions;
+ plugins?: CarouselPlugin;
+ orientation?: 'horizontal' | 'vertical';
+ setApi?: (api: CarouselApi) => void;
+};
+
+type CarouselContextProps = {
+ carouselRef: ReturnType[0];
+ api: ReturnType[1];
+ scrollPrev: () => void;
+ scrollNext: () => void;
+ canScrollPrev: boolean;
+ canScrollNext: boolean;
+} & CarouselProps;
+
+const CarouselContext = React.createContext(null);
+
+function useCarousel() {
+ const context = React.useContext(CarouselContext);
+
+ if (!context) {
+ throw new Error('useCarousel must be used within a ');
+ }
+
+ return context;
+}
+
+const Carousel = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & CarouselProps
+>(
+ (
+ {
+ orientation = 'horizontal',
+ opts,
+ setApi,
+ plugins,
+ className,
+ children,
+ ...props
+ },
+ ref
+ ) => {
+ const [carouselRef, api] = useEmblaCarousel(
+ {
+ ...opts,
+ axis: orientation === 'horizontal' ? 'x' : 'y',
+ },
+ plugins
+ );
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false);
+ const [canScrollNext, setCanScrollNext] = React.useState(false);
+
+ const onSelect = React.useCallback((api: CarouselApi) => {
+ if (!api) {
+ return;
+ }
+
+ setCanScrollPrev(api.canScrollPrev());
+ setCanScrollNext(api.canScrollNext());
+ }, []);
+
+ const scrollPrev = React.useCallback(() => {
+ api?.scrollPrev();
+ }, [api]);
+
+ const scrollNext = React.useCallback(() => {
+ api?.scrollNext();
+ }, [api]);
+
+ const handleKeyDown = React.useCallback(
+ (event: React.KeyboardEvent) => {
+ if (event.key === 'ArrowLeft') {
+ event.preventDefault();
+ scrollPrev();
+ } else if (event.key === 'ArrowRight') {
+ event.preventDefault();
+ scrollNext();
+ }
+ },
+ [scrollPrev, scrollNext]
+ );
+
+ React.useEffect(() => {
+ if (!api || !setApi) {
+ return;
+ }
+
+ setApi(api);
+ }, [api, setApi]);
+
+ React.useEffect(() => {
+ if (!api) {
+ return;
+ }
+
+ onSelect(api);
+ api.on('reInit', onSelect);
+ api.on('select', onSelect);
+
+ return () => {
+ api?.off('select', onSelect);
+ };
+ }, [api, onSelect]);
+
+ return (
+
+
+ {children}
+
+
+ );
+ }
+);
+Carousel.displayName = 'Carousel';
+
+const CarouselContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { carouselRef, orientation } = useCarousel();
+
+ return (
+
+ );
+});
+CarouselContent.displayName = 'CarouselContent';
+
+const CarouselItem = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { orientation } = useCarousel();
+
+ return (
+
+ );
+});
+CarouselItem.displayName = 'CarouselItem';
+
+const CarouselPrevious = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps
+>(({ className, variant = 'outline', size = 'icon', ...props }, ref) => {
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel();
+
+ return (
+
+
+ Previous slide
+
+ );
+});
+CarouselPrevious.displayName = 'CarouselPrevious';
+
+const CarouselNext = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps
+>(({ className, variant = 'outline', size = 'icon', ...props }, ref) => {
+ const { orientation, scrollNext, canScrollNext } = useCarousel();
+
+ return (
+
+
+ Next slide
+
+ );
+});
+CarouselNext.displayName = 'CarouselNext';
+
+export {
+ type CarouselApi,
+ Carousel,
+ CarouselContent,
+ CarouselItem,
+ CarouselPrevious,
+ CarouselNext,
+};
diff --git a/components/ui/chart.tsx b/components/ui/chart.tsx
new file mode 100644
index 0000000..9b6a04b
--- /dev/null
+++ b/components/ui/chart.tsx
@@ -0,0 +1,365 @@
+'use client';
+
+import * as React from 'react';
+import * as RechartsPrimitive from 'recharts';
+
+import { cn } from '@/lib/utils';
+
+// Format: { THEME_NAME: CSS_SELECTOR }
+const THEMES = { light: '', dark: '.dark' } as const;
+
+export type ChartConfig = {
+ [k in string]: {
+ label?: React.ReactNode;
+ icon?: React.ComponentType;
+ } & (
+ | { color?: string; theme?: never }
+ | { color?: never; theme: Record }
+ );
+};
+
+type ChartContextProps = {
+ config: ChartConfig;
+};
+
+const ChartContext = React.createContext(null);
+
+function useChart() {
+ const context = React.useContext(ChartContext);
+
+ if (!context) {
+ throw new Error('useChart must be used within a ');
+ }
+
+ return context;
+}
+
+const ChartContainer = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<'div'> & {
+ config: ChartConfig;
+ children: React.ComponentProps<
+ typeof RechartsPrimitive.ResponsiveContainer
+ >['children'];
+ }
+>(({ id, className, children, config, ...props }, ref) => {
+ const uniqueId = React.useId();
+ const chartId = `chart-${id || uniqueId.replace(/:/g, '')}`;
+
+ return (
+
+
+
+
+ {children}
+
+
+
+ );
+});
+ChartContainer.displayName = 'Chart';
+
+const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
+ const colorConfig = Object.entries(config).filter(
+ ([_, config]) => config.theme || config.color
+ );
+
+ if (!colorConfig.length) {
+ return null;
+ }
+
+ return (
+