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 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="Home"
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={<Shell />}>
 
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
  };