Rodrigo Ortega Claude Sonnet 4.6 commited on
Commit Β·
11c9fce
1
Parent(s): f6c6c73
feat: landing page at /, studio at /studio, ShadCN token fix
Browse files- Add LandingPage.tsx β dark-themed hero, pillars, how-it-works,
features grid, and CTA sections using ShadCN Card/Badge
- Move studio shell to /studio/* route; / renders landing page
- Update all internal navigation to use /studio prefix
- Fix tailwind.config.js to map ShadCN CSS variables so bg-card,
text-muted-foreground, etc. actually generate CSS (variables are
full oklch() values, not channel refs, so use var(--X) directly)
- Replace base-ui Button render-prop links with plain Link/<a> tags
to avoid runtime cloning conflict with React Router
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- studio/src/App.tsx +20 -15
- studio/src/LandingPage.tsx +430 -0
- studio/tailwind.config.js +22 -1
studio/src/App.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
import { createContext, useContext, useState, useEffect, useCallback, useRef } from "react";
|
| 2 |
-
import { BrowserRouter, Routes, Route, Outlet, useMatch, useNavigate, useParams } from "react-router-dom";
|
| 3 |
import { Menu, Sparkles, UserCircle } from "lucide-react";
|
| 4 |
import { StudioView } from "./components/GalleryView";
|
| 5 |
import { CharacterProfileView } from "./components/CharacterProfileView";
|
|
@@ -7,6 +7,7 @@ import { ProjectsView } from "./components/ProjectsView";
|
|
| 7 |
import { LoraTrainingPage } from "./components/LoraTrainingPage";
|
| 8 |
import { ProjectDetailView } from "./components/ProjectDetailView";
|
| 9 |
import { AppSidebar } from "./components/sidebar/AppSidebar";
|
|
|
|
| 10 |
import type { SidebarTab } from "./components/sidebar/AppSidebar";
|
| 11 |
import type { JobItem, LoraCheckpoint, LoraTrainingStatus, MediaItem, Project, Scene, Shot, ProjectPhase, ProjectModeData } from "./types";
|
| 12 |
|
|
@@ -60,8 +61,8 @@ export function useApp() {
|
|
| 60 |
function Shell() {
|
| 61 |
const ctx = useApp();
|
| 62 |
const navigate = useNavigate();
|
| 63 |
-
const projectMatch = useMatch("/projects/:projectId");
|
| 64 |
-
const loraMatch = useMatch("/lora-training");
|
| 65 |
|
| 66 |
// Load project when entering a project-detail route
|
| 67 |
const lastProjectId = useRef<string | undefined>(undefined);
|
|
@@ -100,7 +101,7 @@ function Shell() {
|
|
| 100 |
onSelectShot: ctx.setSelectedShotId,
|
| 101 |
onBack: () => {
|
| 102 |
ctx.setSelectedShotId(null);
|
| 103 |
-
navigate("/projects");
|
| 104 |
},
|
| 105 |
onRefresh: () => {
|
| 106 |
const pid = projectMatch?.params.projectId;
|
|
@@ -127,11 +128,11 @@ function Shell() {
|
|
| 127 |
)}
|
| 128 |
<button
|
| 129 |
onClick={() => {
|
| 130 |
-
navigate("/");
|
| 131 |
ctx.setActiveSidebarTab("generate");
|
| 132 |
}}
|
| 133 |
className="flex items-center gap-3 min-w-0 hover:opacity-80 transition"
|
| 134 |
-
title="
|
| 135 |
>
|
| 136 |
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-rose-500 via-fuchsia-500 to-amber-400 flex items-center justify-center shadow-lg shadow-rose-500/20 ring-1 ring-white/10">
|
| 137 |
<Sparkles className="w-4 h-4 text-white" />
|
|
@@ -141,6 +142,9 @@ function Shell() {
|
|
| 141 |
<p className="text-[10px] text-rose-400/60 tracking-wide">AMD MI300X</p>
|
| 142 |
</div>
|
| 143 |
</button>
|
|
|
|
|
|
|
|
|
|
| 144 |
</div>
|
| 145 |
|
| 146 |
<div className="flex items-center gap-2 text-[11px]">
|
|
@@ -187,16 +191,16 @@ function Shell() {
|
|
| 187 |
activeTab={ctx.activeSidebarTab}
|
| 188 |
onTabChange={(tab) => {
|
| 189 |
ctx.setActiveSidebarTab(tab);
|
| 190 |
-
if (tab === "generate") navigate("/");
|
| 191 |
-
if (tab === "projects") navigate("/projects");
|
| 192 |
-
if (tab === "characters") navigate("/lora-training");
|
| 193 |
}}
|
| 194 |
onClose={() => ctx.setSidebarOpen(false)}
|
| 195 |
checkpoints={ctx.checkpoints}
|
| 196 |
onQueued={ctx.load}
|
| 197 |
onSelectCharacter={(id) => {
|
| 198 |
ctx.setActiveSidebarTab("characters");
|
| 199 |
-
navigate(`/characters/${id}`);
|
| 200 |
}}
|
| 201 |
projectMode={projectMode}
|
| 202 |
/>
|
|
@@ -237,14 +241,14 @@ function StudioRoute() {
|
|
| 237 |
error={ctx.error}
|
| 238 |
onOpen={ctx.setSelected}
|
| 239 |
onDelete={ctx.deleteItem}
|
| 240 |
-
onOpenProjects={() => window.location.href = "/projects"}
|
| 241 |
/>
|
| 242 |
);
|
| 243 |
}
|
| 244 |
|
| 245 |
function ProjectsRoute() {
|
| 246 |
const navigate = useNavigate();
|
| 247 |
-
return <ProjectsView onOpenProject={(id) => navigate(`/projects/${id}`)} />;
|
| 248 |
}
|
| 249 |
|
| 250 |
function ProjectRoute() {
|
|
@@ -277,7 +281,7 @@ function ProjectRoute() {
|
|
| 277 |
onRefresh={() => (projectId ? ctx.loadProject(projectId) : Promise.resolve())}
|
| 278 |
onBack={() => {
|
| 279 |
ctx.setSelectedShotId(null);
|
| 280 |
-
navigate("/projects");
|
| 281 |
}}
|
| 282 |
onDeleteScene={ctx.deleteScene}
|
| 283 |
onDeleteShot={ctx.deleteShot}
|
|
@@ -299,7 +303,7 @@ function CharacterRoute() {
|
|
| 299 |
onGenerate={() => {
|
| 300 |
setSidebarOpen(true);
|
| 301 |
setActiveSidebarTab("generate");
|
| 302 |
-
navigate(`/?character=${characterId}`);
|
| 303 |
}}
|
| 304 |
/>
|
| 305 |
);
|
|
@@ -483,7 +487,8 @@ export default function App() {
|
|
| 483 |
<BrowserRouter>
|
| 484 |
<AppContext.Provider value={ctxValue}>
|
| 485 |
<Routes>
|
| 486 |
-
<Route element={<
|
|
|
|
| 487 |
<Route index element={<StudioRoute />} />
|
| 488 |
<Route path="projects" element={<ProjectsRoute />} />
|
| 489 |
<Route path="projects/:projectId" element={<ProjectRoute />} />
|
|
|
|
| 1 |
import { createContext, useContext, useState, useEffect, useCallback, useRef } from "react";
|
| 2 |
+
import { BrowserRouter, Routes, Route, Outlet, useMatch, useNavigate, useParams, Link } from "react-router-dom";
|
| 3 |
import { Menu, Sparkles, UserCircle } from "lucide-react";
|
| 4 |
import { StudioView } from "./components/GalleryView";
|
| 5 |
import { CharacterProfileView } from "./components/CharacterProfileView";
|
|
|
|
| 7 |
import { LoraTrainingPage } from "./components/LoraTrainingPage";
|
| 8 |
import { ProjectDetailView } from "./components/ProjectDetailView";
|
| 9 |
import { AppSidebar } from "./components/sidebar/AppSidebar";
|
| 10 |
+
import LandingPage from "./LandingPage";
|
| 11 |
import type { SidebarTab } from "./components/sidebar/AppSidebar";
|
| 12 |
import type { JobItem, LoraCheckpoint, LoraTrainingStatus, MediaItem, Project, Scene, Shot, ProjectPhase, ProjectModeData } from "./types";
|
| 13 |
|
|
|
|
| 61 |
function Shell() {
|
| 62 |
const ctx = useApp();
|
| 63 |
const navigate = useNavigate();
|
| 64 |
+
const projectMatch = useMatch("/studio/projects/:projectId");
|
| 65 |
+
const loraMatch = useMatch("/studio/lora-training");
|
| 66 |
|
| 67 |
// Load project when entering a project-detail route
|
| 68 |
const lastProjectId = useRef<string | undefined>(undefined);
|
|
|
|
| 101 |
onSelectShot: ctx.setSelectedShotId,
|
| 102 |
onBack: () => {
|
| 103 |
ctx.setSelectedShotId(null);
|
| 104 |
+
navigate("/studio/projects");
|
| 105 |
},
|
| 106 |
onRefresh: () => {
|
| 107 |
const pid = projectMatch?.params.projectId;
|
|
|
|
| 128 |
)}
|
| 129 |
<button
|
| 130 |
onClick={() => {
|
| 131 |
+
navigate("/studio");
|
| 132 |
ctx.setActiveSidebarTab("generate");
|
| 133 |
}}
|
| 134 |
className="flex items-center gap-3 min-w-0 hover:opacity-80 transition"
|
| 135 |
+
title="Studio home"
|
| 136 |
>
|
| 137 |
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-rose-500 via-fuchsia-500 to-amber-400 flex items-center justify-center shadow-lg shadow-rose-500/20 ring-1 ring-white/10">
|
| 138 |
<Sparkles className="w-4 h-4 text-white" />
|
|
|
|
| 142 |
<p className="text-[10px] text-rose-400/60 tracking-wide">AMD MI300X</p>
|
| 143 |
</div>
|
| 144 |
</button>
|
| 145 |
+
<Link to="/" className="hidden sm:inline-flex items-center text-[11px] text-gray-600 hover:text-gray-400 transition ml-1" title="Back to home">
|
| 146 |
+
β Home
|
| 147 |
+
</Link>
|
| 148 |
</div>
|
| 149 |
|
| 150 |
<div className="flex items-center gap-2 text-[11px]">
|
|
|
|
| 191 |
activeTab={ctx.activeSidebarTab}
|
| 192 |
onTabChange={(tab) => {
|
| 193 |
ctx.setActiveSidebarTab(tab);
|
| 194 |
+
if (tab === "generate") navigate("/studio");
|
| 195 |
+
if (tab === "projects") navigate("/studio/projects");
|
| 196 |
+
if (tab === "characters") navigate("/studio/lora-training");
|
| 197 |
}}
|
| 198 |
onClose={() => ctx.setSidebarOpen(false)}
|
| 199 |
checkpoints={ctx.checkpoints}
|
| 200 |
onQueued={ctx.load}
|
| 201 |
onSelectCharacter={(id) => {
|
| 202 |
ctx.setActiveSidebarTab("characters");
|
| 203 |
+
navigate(`/studio/characters/${id}`);
|
| 204 |
}}
|
| 205 |
projectMode={projectMode}
|
| 206 |
/>
|
|
|
|
| 241 |
error={ctx.error}
|
| 242 |
onOpen={ctx.setSelected}
|
| 243 |
onDelete={ctx.deleteItem}
|
| 244 |
+
onOpenProjects={() => window.location.href = "/studio/projects"}
|
| 245 |
/>
|
| 246 |
);
|
| 247 |
}
|
| 248 |
|
| 249 |
function ProjectsRoute() {
|
| 250 |
const navigate = useNavigate();
|
| 251 |
+
return <ProjectsView onOpenProject={(id) => navigate(`/studio/projects/${id}`)} />;
|
| 252 |
}
|
| 253 |
|
| 254 |
function ProjectRoute() {
|
|
|
|
| 281 |
onRefresh={() => (projectId ? ctx.loadProject(projectId) : Promise.resolve())}
|
| 282 |
onBack={() => {
|
| 283 |
ctx.setSelectedShotId(null);
|
| 284 |
+
navigate("/studio/projects");
|
| 285 |
}}
|
| 286 |
onDeleteScene={ctx.deleteScene}
|
| 287 |
onDeleteShot={ctx.deleteShot}
|
|
|
|
| 303 |
onGenerate={() => {
|
| 304 |
setSidebarOpen(true);
|
| 305 |
setActiveSidebarTab("generate");
|
| 306 |
+
navigate(`/studio?character=${characterId}`);
|
| 307 |
}}
|
| 308 |
/>
|
| 309 |
);
|
|
|
|
| 487 |
<BrowserRouter>
|
| 488 |
<AppContext.Provider value={ctxValue}>
|
| 489 |
<Routes>
|
| 490 |
+
<Route path="/" element={<LandingPage />} />
|
| 491 |
+
<Route path="/studio" element={<Shell />}>
|
| 492 |
<Route index element={<StudioRoute />} />
|
| 493 |
<Route path="projects" element={<ProjectsRoute />} />
|
| 494 |
<Route path="projects/:projectId" element={<ProjectRoute />} />
|
studio/src/LandingPage.tsx
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Link } from "react-router-dom";
|
| 2 |
+
import {
|
| 3 |
+
ArrowRight,
|
| 4 |
+
Bot,
|
| 5 |
+
Cpu,
|
| 6 |
+
Film,
|
| 7 |
+
Lock,
|
| 8 |
+
Server,
|
| 9 |
+
Sparkles,
|
| 10 |
+
Users,
|
| 11 |
+
Zap,
|
| 12 |
+
} from "lucide-react";
|
| 13 |
+
import { Badge } from "@/components/ui/badge";
|
| 14 |
+
import {
|
| 15 |
+
Card,
|
| 16 |
+
CardContent,
|
| 17 |
+
CardDescription,
|
| 18 |
+
CardHeader,
|
| 19 |
+
CardTitle,
|
| 20 |
+
} from "@/components/ui/card";
|
| 21 |
+
|
| 22 |
+
const btnPrimary = "inline-flex items-center gap-2 rounded-xl bg-rose-600 hover:bg-rose-500 px-6 py-2.5 text-sm font-semibold text-white transition shadow-lg shadow-rose-500/20";
|
| 23 |
+
const btnOutline = "inline-flex items-center gap-2 rounded-xl border border-gray-700 hover:border-gray-500 px-6 py-2.5 text-sm font-semibold text-gray-300 hover:text-white transition";
|
| 24 |
+
|
| 25 |
+
function GitHubIcon({ className }: { className?: string }) {
|
| 26 |
+
return (
|
| 27 |
+
<svg viewBox="0 0 24 24" fill="currentColor" className={className}>
|
| 28 |
+
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
|
| 29 |
+
</svg>
|
| 30 |
+
);
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
/* ββ Navbar ββ */
|
| 34 |
+
function Navbar() {
|
| 35 |
+
return (
|
| 36 |
+
<nav className="fixed top-0 inset-x-0 z-50 h-14 border-b border-gray-800/60 bg-black/90 backdrop-blur-xl flex items-center px-6">
|
| 37 |
+
<div className="max-w-6xl mx-auto w-full flex items-center justify-between">
|
| 38 |
+
<div className="flex items-center gap-2.5">
|
| 39 |
+
<div className="w-7 h-7 rounded-lg bg-gradient-to-br from-rose-500 via-fuchsia-500 to-amber-400 flex items-center justify-center">
|
| 40 |
+
<Sparkles className="w-3.5 h-3.5 text-white" />
|
| 41 |
+
</div>
|
| 42 |
+
<span className="font-bold text-sm tracking-tight text-white">
|
| 43 |
+
<span className="text-rose-400">Nemo</span>flix
|
| 44 |
+
</span>
|
| 45 |
+
</div>
|
| 46 |
+
|
| 47 |
+
<div className="flex items-center gap-2">
|
| 48 |
+
<a
|
| 49 |
+
href="https://github.com/ortegarod/nemoflix"
|
| 50 |
+
target="_blank"
|
| 51 |
+
rel="noopener noreferrer"
|
| 52 |
+
className="inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs text-gray-500 hover:text-gray-200 transition"
|
| 53 |
+
>
|
| 54 |
+
<GitHubIcon className="w-3.5 h-3.5" />
|
| 55 |
+
<span className="hidden sm:inline">GitHub</span>
|
| 56 |
+
</a>
|
| 57 |
+
<Link
|
| 58 |
+
to="/studio"
|
| 59 |
+
className="inline-flex items-center gap-1.5 rounded-lg bg-rose-600 hover:bg-rose-500 px-3 py-1.5 text-xs font-semibold text-white transition"
|
| 60 |
+
>
|
| 61 |
+
Launch Studio <ArrowRight className="w-3.5 h-3.5" />
|
| 62 |
+
</Link>
|
| 63 |
+
</div>
|
| 64 |
+
</div>
|
| 65 |
+
</nav>
|
| 66 |
+
);
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
/* ββ Hero ββ */
|
| 70 |
+
function Hero() {
|
| 71 |
+
return (
|
| 72 |
+
<section className="min-h-screen flex items-center justify-center pt-14 px-6">
|
| 73 |
+
<div className="max-w-4xl text-center">
|
| 74 |
+
<Badge
|
| 75 |
+
className="mb-8 gap-1.5 border-amber-500/30 bg-amber-500/5 text-amber-300 h-7 px-3"
|
| 76 |
+
variant="outline"
|
| 77 |
+
>
|
| 78 |
+
<Cpu className="w-3.5 h-3.5" />
|
| 79 |
+
AMD MI300X Β· 192 GB VRAM Β· ROCm Β· Open Source
|
| 80 |
+
</Badge>
|
| 81 |
+
|
| 82 |
+
<h1 className="text-5xl md:text-7xl font-bold leading-tight tracking-tight mb-6 text-white">
|
| 83 |
+
AI Visual Studio
|
| 84 |
+
<br />
|
| 85 |
+
<span className="bg-gradient-to-r from-rose-400 via-fuchsia-400 to-amber-400 bg-clip-text text-transparent">
|
| 86 |
+
Built for Agents.
|
| 87 |
+
</span>
|
| 88 |
+
</h1>
|
| 89 |
+
|
| 90 |
+
<p className="text-xl text-gray-400 max-w-2xl mx-auto mb-10 leading-relaxed">
|
| 91 |
+
Train character LoRAs on AMD MI300X in ~90 minutes. Generate
|
| 92 |
+
photorealistic images. Animate to video. Your AI agent controls the
|
| 93 |
+
whole pipeline through a simple HTTP API.
|
| 94 |
+
</p>
|
| 95 |
+
|
| 96 |
+
<div className="flex items-center justify-center gap-3 flex-wrap">
|
| 97 |
+
<Link to="/studio" className={btnPrimary}>
|
| 98 |
+
<Sparkles className="w-4 h-4" />
|
| 99 |
+
Launch Studio
|
| 100 |
+
</Link>
|
| 101 |
+
<a
|
| 102 |
+
href="https://github.com/ortegarod/nemoflix"
|
| 103 |
+
target="_blank"
|
| 104 |
+
rel="noopener noreferrer"
|
| 105 |
+
className={btnOutline}
|
| 106 |
+
>
|
| 107 |
+
<GitHubIcon className="w-4 h-4" />
|
| 108 |
+
View on GitHub
|
| 109 |
+
</a>
|
| 110 |
+
</div>
|
| 111 |
+
|
| 112 |
+
<p className="mt-8 text-xs text-gray-700">
|
| 113 |
+
ComfyUI-native Β· PostgreSQL Β· Python 3.11+
|
| 114 |
+
</p>
|
| 115 |
+
</div>
|
| 116 |
+
</section>
|
| 117 |
+
);
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
/* ββ Three pillars ββ */
|
| 121 |
+
function Pillars() {
|
| 122 |
+
const items = [
|
| 123 |
+
{
|
| 124 |
+
icon: Cpu,
|
| 125 |
+
color: "text-amber-400",
|
| 126 |
+
border: "border-amber-500/20",
|
| 127 |
+
bg: "from-amber-950/20",
|
| 128 |
+
label: "AMD MI300X",
|
| 129 |
+
body: "Fine-tune Flux.2 LoRAs on 192 GB VRAM via ROCm. ~90 minutes to a consistent character. No CUDA.",
|
| 130 |
+
},
|
| 131 |
+
{
|
| 132 |
+
icon: Bot,
|
| 133 |
+
color: "text-rose-400",
|
| 134 |
+
border: "border-rose-500/20",
|
| 135 |
+
bg: "from-rose-950/20",
|
| 136 |
+
label: "Open Agent API",
|
| 137 |
+
body: "Any AI agent connects with one HTTP call. REST API, no SDK, no vendor lock-in. Agents drive everything.",
|
| 138 |
+
},
|
| 139 |
+
{
|
| 140 |
+
icon: Film,
|
| 141 |
+
color: "text-fuchsia-400",
|
| 142 |
+
border: "border-fuchsia-500/20",
|
| 143 |
+
bg: "from-fuchsia-950/20",
|
| 144 |
+
label: "Image + Video",
|
| 145 |
+
body: "Photorealistic Flux.2 images and Wan 2.2 I2V animation on the same pipeline. One character, every format.",
|
| 146 |
+
},
|
| 147 |
+
];
|
| 148 |
+
|
| 149 |
+
return (
|
| 150 |
+
<section className="py-20 px-6 border-y border-gray-800/40">
|
| 151 |
+
<div className="max-w-6xl mx-auto grid md:grid-cols-3 gap-5">
|
| 152 |
+
{items.map((item) => (
|
| 153 |
+
<Card
|
| 154 |
+
key={item.label}
|
| 155 |
+
className={`border ${item.border} bg-gradient-to-b ${item.bg} to-gray-950/60 ring-0`}
|
| 156 |
+
>
|
| 157 |
+
<CardHeader>
|
| 158 |
+
<item.icon className={`w-5 h-5 ${item.color} mb-1`} />
|
| 159 |
+
<CardTitle className="text-white text-sm">{item.label}</CardTitle>
|
| 160 |
+
</CardHeader>
|
| 161 |
+
<CardContent>
|
| 162 |
+
<CardDescription className="text-gray-400 leading-relaxed">
|
| 163 |
+
{item.body}
|
| 164 |
+
</CardDescription>
|
| 165 |
+
</CardContent>
|
| 166 |
+
</Card>
|
| 167 |
+
))}
|
| 168 |
+
</div>
|
| 169 |
+
</section>
|
| 170 |
+
);
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
/* ββ How It Works ββ */
|
| 174 |
+
function HowItWorks() {
|
| 175 |
+
const steps = [
|
| 176 |
+
{
|
| 177 |
+
n: "01",
|
| 178 |
+
color: "text-amber-400",
|
| 179 |
+
ring: "ring-amber-500/30",
|
| 180 |
+
title: "Train Your Character LoRA",
|
| 181 |
+
body: "Upload 15β25 reference photos. Nemoflix fine-tunes a Flux.2 LoRA on AMD MI300X β 192 GB VRAM, ROCm, no CUDA. ~90 minutes to a character that looks consistent in every single frame.",
|
| 182 |
+
aside: (
|
| 183 |
+
<Card className="border-amber-500/20 bg-gradient-to-b from-amber-950/20 to-gray-950/60 ring-0">
|
| 184 |
+
<CardHeader>
|
| 185 |
+
<Badge className="w-fit gap-1.5 border-amber-500/20 bg-amber-500/5 text-amber-400" variant="outline">
|
| 186 |
+
<Cpu className="w-3 h-3" /> AMD MI300X
|
| 187 |
+
</Badge>
|
| 188 |
+
<CardTitle className="text-white text-sm mt-2">Training Config</CardTitle>
|
| 189 |
+
</CardHeader>
|
| 190 |
+
<CardContent>
|
| 191 |
+
<div className="space-y-1.5 font-mono text-xs text-gray-400">
|
| 192 |
+
{[
|
| 193 |
+
["model", "flux.2-dev"],
|
| 194 |
+
["vram", "192 GB"],
|
| 195 |
+
["runtime", "~90 min"],
|
| 196 |
+
["steps", "1000"],
|
| 197 |
+
["framework", "ROCm 7.2"],
|
| 198 |
+
].map(([k, v]) => (
|
| 199 |
+
<p key={k}>
|
| 200 |
+
<span className="text-gray-600 inline-block w-20">{k}</span>
|
| 201 |
+
{v}
|
| 202 |
+
</p>
|
| 203 |
+
))}
|
| 204 |
+
</div>
|
| 205 |
+
</CardContent>
|
| 206 |
+
</Card>
|
| 207 |
+
),
|
| 208 |
+
},
|
| 209 |
+
{
|
| 210 |
+
n: "02",
|
| 211 |
+
color: "text-rose-400",
|
| 212 |
+
ring: "ring-rose-500/30",
|
| 213 |
+
title: "Generate Images via API",
|
| 214 |
+
body: "Your AI agent calls the API with a prompt and character ID. Nemoflix builds the ComfyUI workflow, routes to the right GPU node, queues the job, and returns a prompt ID. Photorealistic results in seconds.",
|
| 215 |
+
aside: (
|
| 216 |
+
<Card className="border-gray-800 bg-gray-950 ring-0">
|
| 217 |
+
<CardHeader>
|
| 218 |
+
<CardTitle className="text-xs text-gray-500 font-mono font-normal">API call</CardTitle>
|
| 219 |
+
</CardHeader>
|
| 220 |
+
<CardContent className="space-y-2 font-mono text-xs">
|
| 221 |
+
<p className="text-gray-500">
|
| 222 |
+
POST <span className="text-rose-400">/api/image/generate</span>
|
| 223 |
+
</p>
|
| 224 |
+
<div className="rounded-lg bg-black/60 p-3 space-y-1 text-gray-400">
|
| 225 |
+
<p className="text-gray-600">{"{"}</p>
|
| 226 |
+
<p className="pl-3">
|
| 227 |
+
<span className="text-amber-300">"character"</span>:{" "}
|
| 228 |
+
<span className="text-emerald-300">"rigo"</span>,
|
| 229 |
+
</p>
|
| 230 |
+
<p className="pl-3">
|
| 231 |
+
<span className="text-amber-300">"prompt"</span>:{" "}
|
| 232 |
+
<span className="text-emerald-300">"walking through a rainy street"</span>
|
| 233 |
+
</p>
|
| 234 |
+
<p className="text-gray-600">{"}"}</p>
|
| 235 |
+
</div>
|
| 236 |
+
<p className="text-emerald-400/70 pt-1">β prompt_id: a3f9c1d2</p>
|
| 237 |
+
</CardContent>
|
| 238 |
+
</Card>
|
| 239 |
+
),
|
| 240 |
+
},
|
| 241 |
+
{
|
| 242 |
+
n: "03",
|
| 243 |
+
color: "text-fuchsia-400",
|
| 244 |
+
ring: "ring-fuchsia-500/30",
|
| 245 |
+
title: "Animate to Video",
|
| 246 |
+
body: "One more API call and Wan 2.2 I2V animates the image into a short video clip. The same character, moving. String clips together into a full scene. Your agent builds the whole sequence autonomously.",
|
| 247 |
+
aside: (
|
| 248 |
+
<Card className="border-fuchsia-500/20 bg-gradient-to-b from-fuchsia-950/20 to-gray-950/60 ring-0">
|
| 249 |
+
<CardHeader>
|
| 250 |
+
<Badge className="w-fit gap-1.5 border-fuchsia-500/20 bg-fuchsia-500/5 text-fuchsia-400" variant="outline">
|
| 251 |
+
Wan 2.2 I2V
|
| 252 |
+
</Badge>
|
| 253 |
+
<CardTitle className="text-white text-sm mt-2">Video Config</CardTitle>
|
| 254 |
+
</CardHeader>
|
| 255 |
+
<CardContent>
|
| 256 |
+
<div className="space-y-1.5 font-mono text-xs text-gray-400">
|
| 257 |
+
{[
|
| 258 |
+
["model", "wan2.2-i2v-14b"],
|
| 259 |
+
["input", "still image"],
|
| 260 |
+
["output", "5s video clip"],
|
| 261 |
+
["cfg", "3.5 Β· steps 20"],
|
| 262 |
+
].map(([k, v]) => (
|
| 263 |
+
<p key={k}>
|
| 264 |
+
<span className="text-gray-600 inline-block w-20">{k}</span>
|
| 265 |
+
{v}
|
| 266 |
+
</p>
|
| 267 |
+
))}
|
| 268 |
+
</div>
|
| 269 |
+
<p className="text-[11px] text-fuchsia-300/50 mt-4">
|
| 270 |
+
Same character. Real motion. Not a filter.
|
| 271 |
+
</p>
|
| 272 |
+
</CardContent>
|
| 273 |
+
</Card>
|
| 274 |
+
),
|
| 275 |
+
},
|
| 276 |
+
];
|
| 277 |
+
|
| 278 |
+
return (
|
| 279 |
+
<section className="py-28 px-6">
|
| 280 |
+
<div className="max-w-6xl mx-auto">
|
| 281 |
+
<div className="text-center mb-20">
|
| 282 |
+
<h2 className="text-4xl font-bold text-white mb-4">How It Works</h2>
|
| 283 |
+
<p className="text-gray-500 text-lg">
|
| 284 |
+
From reference photos to animated video in three steps.
|
| 285 |
+
</p>
|
| 286 |
+
</div>
|
| 287 |
+
|
| 288 |
+
<div className="space-y-24">
|
| 289 |
+
{steps.map((step, i) => (
|
| 290 |
+
<div key={step.n} className="grid md:grid-cols-2 gap-12 items-center">
|
| 291 |
+
<div className={i % 2 === 1 ? "md:order-2" : ""}>
|
| 292 |
+
<div
|
| 293 |
+
className={`w-10 h-10 rounded-full ring-1 ${step.ring} bg-gray-950 flex items-center justify-center mb-4`}
|
| 294 |
+
>
|
| 295 |
+
<span className={`text-xs font-bold font-mono ${step.color}`}>
|
| 296 |
+
{step.n}
|
| 297 |
+
</span>
|
| 298 |
+
</div>
|
| 299 |
+
<h3 className="text-3xl font-bold text-white mb-4">{step.title}</h3>
|
| 300 |
+
<p className="text-gray-400 text-lg leading-relaxed">{step.body}</p>
|
| 301 |
+
</div>
|
| 302 |
+
<div className={i % 2 === 1 ? "md:order-1" : ""}>{step.aside}</div>
|
| 303 |
+
</div>
|
| 304 |
+
))}
|
| 305 |
+
</div>
|
| 306 |
+
</div>
|
| 307 |
+
</section>
|
| 308 |
+
);
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
/* ββ Features grid ββ */
|
| 312 |
+
function Features() {
|
| 313 |
+
const features = [
|
| 314 |
+
{ icon: Cpu, color: "text-amber-400", title: "LoRA Training on AMD", body: "Fine-tune Flux.2 on AMD MI300X via ROCm. No CUDA required. Monitor job progress live in the Studio." },
|
| 315 |
+
{ icon: Server, color: "text-rose-400", title: "Self-Hosted", body: "Your hardware, your models, your data. No cloud dependency, no rate limits, no data leaving your machine." },
|
| 316 |
+
{ icon: Bot, color: "text-violet-400", title: "Agent API", body: "REST API any agent can call. Characters, images, video, training jobs β all simple HTTP endpoints." },
|
| 317 |
+
{ icon: Users, color: "text-fuchsia-400", title: "Character Registry", body: "Register characters with trigger words and LoRA weights. Every generation references them consistently." },
|
| 318 |
+
{ icon: Zap, color: "text-emerald-400", title: "Multi-GPU Routing", body: "Images and video automatically routed to the right node. Add more GPUs without changing any code." },
|
| 319 |
+
{ icon: Lock, color: "text-blue-400", title: "Open Source", body: "MIT license. Audit the code, fork it, extend it. No black boxes." },
|
| 320 |
+
];
|
| 321 |
+
|
| 322 |
+
return (
|
| 323 |
+
<section className="py-28 px-6 border-t border-gray-800/40">
|
| 324 |
+
<div className="max-w-6xl mx-auto">
|
| 325 |
+
<div className="text-center mb-16">
|
| 326 |
+
<h2 className="text-4xl font-bold text-white mb-4">What's Inside</h2>
|
| 327 |
+
<p className="text-gray-500 text-lg">
|
| 328 |
+
Everything you need to run a visual AI studio.
|
| 329 |
+
</p>
|
| 330 |
+
</div>
|
| 331 |
+
|
| 332 |
+
<div className="grid md:grid-cols-3 gap-5">
|
| 333 |
+
{features.map((f) => (
|
| 334 |
+
<Card
|
| 335 |
+
key={f.title}
|
| 336 |
+
className="border-gray-800/60 bg-gray-950/50 ring-0 hover:border-gray-700 transition-colors"
|
| 337 |
+
>
|
| 338 |
+
<CardHeader>
|
| 339 |
+
<f.icon className={`w-5 h-5 ${f.color}`} />
|
| 340 |
+
<CardTitle className="text-white text-sm mt-3">{f.title}</CardTitle>
|
| 341 |
+
</CardHeader>
|
| 342 |
+
<CardContent>
|
| 343 |
+
<CardDescription className="text-gray-500 leading-relaxed">
|
| 344 |
+
{f.body}
|
| 345 |
+
</CardDescription>
|
| 346 |
+
</CardContent>
|
| 347 |
+
</Card>
|
| 348 |
+
))}
|
| 349 |
+
</div>
|
| 350 |
+
</div>
|
| 351 |
+
</section>
|
| 352 |
+
);
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
/* ββ CTA ββ */
|
| 356 |
+
function CTA() {
|
| 357 |
+
return (
|
| 358 |
+
<section className="py-32 px-6">
|
| 359 |
+
<div className="max-w-2xl mx-auto text-center">
|
| 360 |
+
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-rose-500 via-fuchsia-500 to-amber-400 flex items-center justify-center mx-auto mb-8 shadow-2xl shadow-rose-500/20">
|
| 361 |
+
<Sparkles className="w-7 h-7 text-white" />
|
| 362 |
+
</div>
|
| 363 |
+
<h2 className="text-4xl md:text-5xl font-bold text-white mb-6">
|
| 364 |
+
Ready to Run It?
|
| 365 |
+
</h2>
|
| 366 |
+
<p className="text-xl text-gray-400 mb-10 leading-relaxed">
|
| 367 |
+
Clone the repo, point it at your GPU nodes, and your agent is
|
| 368 |
+
generating in minutes.
|
| 369 |
+
</p>
|
| 370 |
+
<div className="flex items-center justify-center gap-3 flex-wrap">
|
| 371 |
+
<Link to="/studio" className={btnPrimary}>
|
| 372 |
+
<Sparkles className="w-4 h-4" />
|
| 373 |
+
Launch Studio
|
| 374 |
+
</Link>
|
| 375 |
+
<a
|
| 376 |
+
href="https://github.com/ortegarod/nemoflix"
|
| 377 |
+
target="_blank"
|
| 378 |
+
rel="noopener noreferrer"
|
| 379 |
+
className={btnOutline}
|
| 380 |
+
>
|
| 381 |
+
<GitHubIcon className="w-4 h-4" />
|
| 382 |
+
View on GitHub
|
| 383 |
+
</a>
|
| 384 |
+
</div>
|
| 385 |
+
</div>
|
| 386 |
+
</section>
|
| 387 |
+
);
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
/* ββ Footer ββ */
|
| 391 |
+
function Footer() {
|
| 392 |
+
return (
|
| 393 |
+
<footer className="border-t border-gray-800/40 py-10 px-6">
|
| 394 |
+
<div className="max-w-6xl mx-auto flex flex-col sm:flex-row items-center justify-between gap-4">
|
| 395 |
+
<span className="text-sm font-bold">
|
| 396 |
+
<span className="text-rose-400">Nemo</span>
|
| 397 |
+
<span className="text-gray-500">flix</span>
|
| 398 |
+
</span>
|
| 399 |
+
<div className="flex items-center gap-6 text-xs text-gray-600">
|
| 400 |
+
<a
|
| 401 |
+
href="https://github.com/ortegarod/nemoflix"
|
| 402 |
+
target="_blank"
|
| 403 |
+
rel="noreferrer"
|
| 404 |
+
className="hover:text-gray-400 transition"
|
| 405 |
+
>
|
| 406 |
+
GitHub
|
| 407 |
+
</a>
|
| 408 |
+
<Link to="/studio" className="hover:text-gray-400 transition">
|
| 409 |
+
Studio
|
| 410 |
+
</Link>
|
| 411 |
+
<span>Β© {new Date().getFullYear()} Nemoflix Β· MIT License</span>
|
| 412 |
+
</div>
|
| 413 |
+
</div>
|
| 414 |
+
</footer>
|
| 415 |
+
);
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
export default function LandingPage() {
|
| 419 |
+
return (
|
| 420 |
+
<div className="min-h-screen bg-black text-white">
|
| 421 |
+
<Navbar />
|
| 422 |
+
<Hero />
|
| 423 |
+
<Pillars />
|
| 424 |
+
<HowItWorks />
|
| 425 |
+
<Features />
|
| 426 |
+
<CTA />
|
| 427 |
+
<Footer />
|
| 428 |
+
</div>
|
| 429 |
+
);
|
| 430 |
+
}
|
studio/tailwind.config.js
CHANGED
|
@@ -1,8 +1,29 @@
|
|
| 1 |
/** @type {import('tailwindcss').Config} */
|
| 2 |
export default {
|
|
|
|
| 3 |
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
| 4 |
theme: {
|
| 5 |
-
extend: {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
},
|
| 7 |
plugins: [],
|
| 8 |
};
|
|
|
|
| 1 |
/** @type {import('tailwindcss').Config} */
|
| 2 |
export default {
|
| 3 |
+
darkMode: ["class"],
|
| 4 |
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
| 5 |
theme: {
|
| 6 |
+
extend: {
|
| 7 |
+
colors: {
|
| 8 |
+
background: "var(--background)",
|
| 9 |
+
foreground: "var(--foreground)",
|
| 10 |
+
card: { DEFAULT: "var(--card)", foreground: "var(--card-foreground)" },
|
| 11 |
+
popover: { DEFAULT: "var(--popover)", foreground: "var(--popover-foreground)" },
|
| 12 |
+
primary: { DEFAULT: "var(--primary)", foreground: "var(--primary-foreground)" },
|
| 13 |
+
secondary: { DEFAULT: "var(--secondary)", foreground: "var(--secondary-foreground)" },
|
| 14 |
+
muted: { DEFAULT: "var(--muted)", foreground: "var(--muted-foreground)" },
|
| 15 |
+
accent: { DEFAULT: "var(--accent)", foreground: "var(--accent-foreground)" },
|
| 16 |
+
destructive: { DEFAULT: "var(--destructive)" },
|
| 17 |
+
border: "var(--border)",
|
| 18 |
+
input: "var(--input)",
|
| 19 |
+
ring: "var(--ring)",
|
| 20 |
+
},
|
| 21 |
+
borderRadius: {
|
| 22 |
+
lg: "var(--radius)",
|
| 23 |
+
md: "calc(var(--radius) - 2px)",
|
| 24 |
+
sm: "calc(var(--radius) - 4px)",
|
| 25 |
+
},
|
| 26 |
+
},
|
| 27 |
},
|
| 28 |
plugins: [],
|
| 29 |
};
|