Spaces:
Sleeping
Sleeping
feat: Add Advanced Engine Settings and native UI logo
Browse files- Implemented collapsible 'Advanced Engine Settings' allowing users to override Ollama and Hugging Face models directly from the UI.
- Swapped the generic banner placeholder for the Chronicle Portrait Studio logo.svg.
- Color-matched the SVG logo to the Next.js Obsidian & Gold palette (#0a0805, #DAB062).
- .gemini-commit-msg.txt +3 -6
- frontend/public/logo.svg +28 -0
- frontend/src/components/ClientApp.tsx +123 -30
.gemini-commit-msg.txt
CHANGED
|
@@ -1,7 +1,4 @@
|
|
| 1 |
-
|
| 2 |
|
| 3 |
-
-
|
| 4 |
-
-
|
| 5 |
-
- Replaces generic Gradio grids with precisely constrained, center-aligned HTML forms using Tailwind v4.
|
| 6 |
-
- Preserves 100% functional parity including JSON Save/Load, Example Characters, and Image Generation.
|
| 7 |
-
- Updates README documentation to reflect the new decoupled setup instructions.
|
|
|
|
| 1 |
+
Refine Active Engines UI component
|
| 2 |
|
| 3 |
+
- Adds explicit labels (Text Refinement, Image Synthesis) to the active engines dropdowns for clarity.
|
| 4 |
+
- Restores dynamic availability checking: Local options (Ollama, ComfyUI) are automatically disabled with an '(Offline)' tag if the FastAPI backend detects they are unreachable.
|
|
|
|
|
|
|
|
|
frontend/public/logo.svg
ADDED
|
|
frontend/src/components/ClientApp.tsx
CHANGED
|
@@ -3,7 +3,7 @@
|
|
| 3 |
import React, { useState, useEffect, useRef } from 'react';
|
| 4 |
import { User, Sparkles, Sword, Globe, Palette, Settings, Image as ImageIcon, Wand2, Dices, Save, Download, Upload, Copy } from 'lucide-react';
|
| 5 |
|
| 6 |
-
const API_URL = "http://127.0.0.1:8000";
|
| 7 |
|
| 8 |
const tabs = [
|
| 9 |
{ id: 'identity', label: 'Identity & Race', icon: User },
|
|
@@ -48,6 +48,17 @@ export default function ClientApp() {
|
|
| 48 |
});
|
| 49 |
setFeatures(initFeatures);
|
| 50 |
setRandomization(Array(data.feature_sequence.length).fill(false));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
})
|
| 52 |
.catch(err => console.error("Failed to load config", err));
|
| 53 |
}, []);
|
|
@@ -98,11 +109,32 @@ export default function ClientApp() {
|
|
| 98 |
});
|
| 99 |
const data = await res.json();
|
| 100 |
setFeatures(data.new_values);
|
| 101 |
-
setRefinedOutput("");
|
| 102 |
setStatusMsg("Randomization complete.");
|
| 103 |
} catch (e) { console.error(e); }
|
| 104 |
};
|
| 105 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
const applyLoadedData = (data: any) => {
|
| 107 |
if (!config) return;
|
| 108 |
setCharacterName(data.name || "Unnamed Hero");
|
|
@@ -326,7 +358,7 @@ export default function ClientApp() {
|
|
| 326 |
<div className="text-[10px] uppercase tracking-widest text-[#A89880] mb-4 px-4 border-t border-[#2D241A] pt-6">Global Actions</div>
|
| 327 |
<button onClick={handleRandomize} className="w-full mx-2 flex items-center justify-center gap-3 px-4 py-3 border border-[#3A4E59] bg-[#0E151A] rounded-lg text-[#61C2DF] hover:bg-[#121B22] hover:border-[#61C2DF] transition-all group" style={{ width: 'calc(100% - 1rem)'}}>
|
| 328 |
<Dices size={18} />
|
| 329 |
-
<span className="text-sm tracking-wide font-medium">
|
| 330 |
</button>
|
| 331 |
</nav>
|
| 332 |
|
|
@@ -336,16 +368,69 @@ export default function ClientApp() {
|
|
| 336 |
<Settings size={16} />
|
| 337 |
<span className="text-xs uppercase tracking-wider font-semibold">Active Engines</span>
|
| 338 |
</div>
|
| 339 |
-
<
|
| 340 |
-
<
|
| 341 |
-
<
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
<
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 349 |
</div>
|
| 350 |
</div>
|
| 351 |
|
|
@@ -353,38 +438,46 @@ export default function ClientApp() {
|
|
| 353 |
<div className="flex-1 flex flex-col relative z-10 overflow-y-auto shadow-inner custom-scrollbar bg-[#0f0c08]">
|
| 354 |
<div className="sticky top-0 h-12 bg-gradient-to-b from-[#080604] to-transparent z-10 pointer-events-none w-full"></div>
|
| 355 |
|
| 356 |
-
<div className="max-w-2xl mx-auto w-full px-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 357 |
{/* Section Headers */}
|
| 358 |
-
<div className="mb-
|
| 359 |
-
<h2 className="font-cinzel text-3xl text-[#E2D1B3] mb-
|
| 360 |
<div className="w-16 h-1 bg-[#DAB062] rounded-full opacity-50"></div>
|
| 361 |
</div>
|
| 362 |
|
| 363 |
-
<div className="glass-panel rounded-
|
| 364 |
<div className="absolute top-0 right-0 w-96 h-96 bg-[#DAB062]/5 rounded-full blur-3xl -mr-32 -mt-32 pointer-events-none transition-all duration-700 group-hover:bg-[#DAB062]/10"></div>
|
| 365 |
|
| 366 |
<div className="relative z-10 w-full">
|
| 367 |
{activeTab === 'identity' && (
|
| 368 |
<div className="w-full">
|
| 369 |
-
<div className="mb-
|
| 370 |
-
<
|
|
|
|
|
|
|
|
|
|
| 371 |
<input type="text" value={characterName} onChange={e => setCharacterName(e.target.value)} className="w-full bg-[#120E0A] border border-[#3D3224] text-[#E2D1B3] font-cinzel text-xl font-semibold rounded-lg px-4 py-3 outline-none focus:border-[#DAB062] focus:ring-1 focus:ring-[#DAB062]/50 transition-all placeholder-[#E2D1B3]/20" placeholder="Enter name..."/>
|
| 372 |
</div>
|
| 373 |
-
<div className="grid grid-cols-2 gap-x-12 gap-y-
|
| 374 |
{renderFeatureUI('identity', 'race', 'Race')}
|
| 375 |
{renderFeatureUI('identity', 'class', 'Class')}
|
| 376 |
{renderFeatureUI('identity', 'gender', 'Gender')}
|
| 377 |
{renderFeatureUI('identity', 'age', 'Age')}
|
| 378 |
</div>
|
| 379 |
-
<div className="mt-
|
| 380 |
<label className="block text-xs uppercase tracking-widest text-[#A89880] mb-4">Lore / Background Text</label>
|
| 381 |
<textarea value={extraInfo[0]} onChange={e => handleExtraInfoChange(0, e.target.value)} className="input-alchemist h-32 resize-none leading-relaxed"></textarea>
|
| 382 |
</div>
|
| 383 |
|
| 384 |
-
<div className="my-
|
| 385 |
|
| 386 |
<h3 className="font-cinzel text-2xl text-[#DAB062] mb-8 drop-shadow-sm">Expression & Stance</h3>
|
| 387 |
-
<div className="grid grid-cols-2 gap-x-12 gap-y-
|
| 388 |
{renderFeatureUI('expression_pose', 'expression', 'Expression')}
|
| 389 |
{renderFeatureUI('expression_pose', 'pose', 'Pose')}
|
| 390 |
</div>
|
|
@@ -393,7 +486,7 @@ export default function ClientApp() {
|
|
| 393 |
|
| 394 |
{activeTab === 'appearance' && (
|
| 395 |
<div className="w-full">
|
| 396 |
-
<div className="grid grid-cols-2 gap-x-
|
| 397 |
{renderFeatureUI('appearance', 'hair_color', 'Hair Color')}
|
| 398 |
{renderFeatureUI('appearance', 'hair_style', 'Hair Style')}
|
| 399 |
{renderFeatureUI('appearance', 'eye_color', 'Eye Color')}
|
|
@@ -401,7 +494,7 @@ export default function ClientApp() {
|
|
| 401 |
{renderFeatureUI('appearance', 'skin_tone', 'Skin Tone')}
|
| 402 |
{renderFeatureUI('appearance', 'distinguishing_feature', 'Main Feature')}
|
| 403 |
</div>
|
| 404 |
-
<div className="mt-
|
| 405 |
<label className="block text-xs uppercase tracking-widest text-[#A89880] mb-4">Additional Visuals</label>
|
| 406 |
<textarea value={extraInfo[1]} onChange={e => handleExtraInfoChange(1, e.target.value)} className="input-alchemist h-32 resize-none leading-relaxed"></textarea>
|
| 407 |
</div>
|
|
@@ -410,14 +503,14 @@ export default function ClientApp() {
|
|
| 410 |
|
| 411 |
{activeTab === 'equipment' && (
|
| 412 |
<div className="w-full">
|
| 413 |
-
<div className="grid grid-cols-2 gap-x-
|
| 414 |
{renderFeatureUI('equipment', 'armor', 'Primary Gear')}
|
| 415 |
{renderFeatureUI('equipment', 'weapon', 'Weapon Variant')}
|
| 416 |
{renderFeatureUI('equipment', 'accessory', 'Acc. 1')}
|
| 417 |
{renderFeatureUI('equipment', 'accessory', 'Acc. 2')}
|
| 418 |
{renderFeatureUI('equipment', 'material', 'Material Finish')}
|
| 419 |
</div>
|
| 420 |
-
<div className="mt-
|
| 421 |
<label className="block text-xs uppercase tracking-widest text-[#A89880] mb-4">Custom Equipment Description</label>
|
| 422 |
<textarea value={extraInfo[2]} onChange={e => handleExtraInfoChange(2, e.target.value)} className="input-alchemist h-32 resize-none leading-relaxed"></textarea>
|
| 423 |
</div>
|
|
@@ -426,12 +519,12 @@ export default function ClientApp() {
|
|
| 426 |
|
| 427 |
{activeTab === 'environment' && (
|
| 428 |
<div className="w-full">
|
| 429 |
-
<div className="grid grid-cols-2 gap-x-
|
| 430 |
{renderFeatureUI('environment', 'background', 'Backdrop')}
|
| 431 |
{renderFeatureUI('environment', 'lighting', 'Illumination')}
|
| 432 |
{renderFeatureUI('environment', 'atmosphere', 'Atmosphere')}
|
| 433 |
</div>
|
| 434 |
-
<div className="mt-
|
| 435 |
<label className="block text-xs uppercase tracking-widest text-[#A89880] mb-4">Custom Environment Info</label>
|
| 436 |
<textarea value={extraInfo[3]} onChange={e => handleExtraInfoChange(3, e.target.value)} className="input-alchemist h-32 resize-none leading-relaxed"></textarea>
|
| 437 |
</div>
|
|
@@ -440,7 +533,7 @@ export default function ClientApp() {
|
|
| 440 |
|
| 441 |
{activeTab === 'style' && (
|
| 442 |
<div className="w-full">
|
| 443 |
-
<div className="grid grid-cols-2 gap-x-
|
| 444 |
{renderFeatureUI('vfx_style', 'vfx', 'Particle Effects')}
|
| 445 |
{renderFeatureUI('vfx_style', 'style', 'Illustration Style')}
|
| 446 |
{renderFeatureUI('vfx_style', 'mood', 'Color Grading/Mood')}
|
|
|
|
| 3 |
import React, { useState, useEffect, useRef } from 'react';
|
| 4 |
import { User, Sparkles, Sword, Globe, Palette, Settings, Image as ImageIcon, Wand2, Dices, Save, Download, Upload, Copy } from 'lucide-react';
|
| 5 |
|
| 6 |
+
const API_URL = process.env.NODE_ENV === "development" ? "http://127.0.0.1:8000" : "";
|
| 7 |
|
| 8 |
const tabs = [
|
| 9 |
{ id: 'identity', label: 'Identity & Race', icon: User },
|
|
|
|
| 48 |
});
|
| 49 |
setFeatures(initFeatures);
|
| 50 |
setRandomization(Array(data.feature_sequence.length).fill(false));
|
| 51 |
+
|
| 52 |
+
// Smart Active Engine Defaults
|
| 53 |
+
if (!data.comfy_active) {
|
| 54 |
+
if (data.hf_active) setImageBackend("Hugging Face (Cloud)");
|
| 55 |
+
else if (data.gemini_active) setImageBackend("Gemini (Cloud)");
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
if (!data.ollama_models?.length) {
|
| 59 |
+
if (data.gemini_active) setRefinementBackend("Gemini (Cloud)");
|
| 60 |
+
else if (data.hf_active) setRefinementBackend("Hugging Face (Cloud)");
|
| 61 |
+
}
|
| 62 |
})
|
| 63 |
.catch(err => console.error("Failed to load config", err));
|
| 64 |
}, []);
|
|
|
|
| 109 |
});
|
| 110 |
const data = await res.json();
|
| 111 |
setFeatures(data.new_values);
|
| 112 |
+
setRefinedOutput(""); // Clear refined narrative when traits are re-rolled
|
| 113 |
setStatusMsg("Randomization complete.");
|
| 114 |
} catch (e) { console.error(e); }
|
| 115 |
};
|
| 116 |
|
| 117 |
+
const handleGenerateName = async () => {
|
| 118 |
+
if (!config) return;
|
| 119 |
+
try {
|
| 120 |
+
setStatusMsg("Weaving a name from the threads of fate...");
|
| 121 |
+
// find race index
|
| 122 |
+
const raceIndex = config?.feature_sequence?.findIndex((seq: any) => seq[1] === "race");
|
| 123 |
+
const race = raceIndex !== -1 && raceIndex !== undefined ? features[raceIndex] : "Human";
|
| 124 |
+
|
| 125 |
+
const res = await fetch(`${API_URL}/api/generate_name`, {
|
| 126 |
+
method: 'POST',
|
| 127 |
+
headers: { 'Content-Type': 'application/json' },
|
| 128 |
+
body: JSON.stringify({ race })
|
| 129 |
+
});
|
| 130 |
+
if (res.ok) {
|
| 131 |
+
const data = await res.json();
|
| 132 |
+
setCharacterName(data.name);
|
| 133 |
+
setStatusMsg("Name generation complete.");
|
| 134 |
+
}
|
| 135 |
+
} catch (e) { console.error(e); }
|
| 136 |
+
};
|
| 137 |
+
|
| 138 |
const applyLoadedData = (data: any) => {
|
| 139 |
if (!config) return;
|
| 140 |
setCharacterName(data.name || "Unnamed Hero");
|
|
|
|
| 358 |
<div className="text-[10px] uppercase tracking-widest text-[#A89880] mb-4 px-4 border-t border-[#2D241A] pt-6">Global Actions</div>
|
| 359 |
<button onClick={handleRandomize} className="w-full mx-2 flex items-center justify-center gap-3 px-4 py-3 border border-[#3A4E59] bg-[#0E151A] rounded-lg text-[#61C2DF] hover:bg-[#121B22] hover:border-[#61C2DF] transition-all group" style={{ width: 'calc(100% - 1rem)'}}>
|
| 360 |
<Dices size={18} />
|
| 361 |
+
<span className="text-sm tracking-wide font-medium">Reroll Marked Traits</span>
|
| 362 |
</button>
|
| 363 |
</nav>
|
| 364 |
|
|
|
|
| 368 |
<Settings size={16} />
|
| 369 |
<span className="text-xs uppercase tracking-wider font-semibold">Active Engines</span>
|
| 370 |
</div>
|
| 371 |
+
<div className="mb-4">
|
| 372 |
+
<label className="text-[10px] block uppercase tracking-widest text-[#A89880] mb-1.5 flex items-center gap-1"><Sparkles size={10}/> Text Refinement</label>
|
| 373 |
+
<select value={refinementBackend} onChange={e => setRefinementBackend(e.target.value)} className="input-alchemist text-xs" title="Text Backbone">
|
| 374 |
+
<option value="Gemini (Cloud)" disabled={!config?.gemini_active}>Gemini (Cloud){!config?.gemini_active ? ' (Missing Key)' : ''}</option>
|
| 375 |
+
<option value="Hugging Face (Cloud)" disabled={!config?.hf_active}>Hugging Face (Cloud){!config?.hf_active ? ' (Missing Token)' : ''}</option>
|
| 376 |
+
<option value="Ollama (Local)" disabled={!config?.ollama_models?.length}>Ollama (Local){!config?.ollama_models?.length ? ' (Offline)' : ''}</option>
|
| 377 |
+
</select>
|
| 378 |
+
</div>
|
| 379 |
+
<div className="mb-4">
|
| 380 |
+
<label className="text-[10px] block uppercase tracking-widest text-[#A89880] mb-1.5 flex items-center gap-1"><ImageIcon size={10}/> Image Synthesis</label>
|
| 381 |
+
<select value={imageBackend} onChange={e => setImageBackend(e.target.value)} className="input-alchemist text-xs" title="Image Engine">
|
| 382 |
+
<option value="Gemini (Cloud)" disabled={!config?.gemini_active}>Gemini (Cloud){!config?.gemini_active ? ' (Missing Key)' : ''}</option>
|
| 383 |
+
<option value="Hugging Face (Cloud)" disabled={!config?.hf_active}>Hugging Face (Cloud){!config?.hf_active ? ' (Missing Token)' : ''}</option>
|
| 384 |
+
<option value="ComfyUI (Local)" disabled={!config?.comfy_active}>ComfyUI (Local){!config?.comfy_active ? ' (Offline)' : ''}</option>
|
| 385 |
+
</select>
|
| 386 |
+
</div>
|
| 387 |
+
|
| 388 |
+
<details className="group [&_summary::-webkit-details-marker]:hidden">
|
| 389 |
+
<summary className="flex items-center justify-between cursor-pointer py-2 text-[#A89880] hover:text-[#DAB062] transition-colors border-t border-[#2D241A] mt-2">
|
| 390 |
+
<span className="text-[10px] uppercase tracking-widest font-semibold">Advanced Constraints</span>
|
| 391 |
+
<span className="transition group-open:rotate-180">▼</span>
|
| 392 |
+
</summary>
|
| 393 |
+
<div className="pt-2 text-xs space-y-3">
|
| 394 |
+
{refinementBackend === "Ollama (Local)" && (
|
| 395 |
+
<div>
|
| 396 |
+
<label className="block text-[9px] uppercase tracking-widest text-[#665D4F] mb-1">Ollama Model</label>
|
| 397 |
+
<select value={ollamaModel} onChange={e => setOllamaModel(e.target.value)} className="input-alchemist w-full text-[10px] py-1.5 px-2 bg-[#120E0A]">
|
| 398 |
+
<option value="">Default (llama3)</option>
|
| 399 |
+
{config?.ollama_models?.map((m: string) => <option key={m} value={m}>{m}</option>)}
|
| 400 |
+
</select>
|
| 401 |
+
</div>
|
| 402 |
+
)}
|
| 403 |
+
{refinementBackend === "Hugging Face (Cloud)" && (
|
| 404 |
+
<>
|
| 405 |
+
<div>
|
| 406 |
+
<label className="block text-[9px] uppercase tracking-widest text-[#665D4F] mb-1">HF Text Model</label>
|
| 407 |
+
<input type="text" value={hfTextModel} onChange={e => setHfTextModel(e.target.value)} className="input-alchemist w-full text-[10px] py-1.5 px-2 bg-[#120E0A]" placeholder="e.g. meta-llama/Meta-Llama-3-8B-Instruct" />
|
| 408 |
+
</div>
|
| 409 |
+
</>
|
| 410 |
+
)}
|
| 411 |
+
{imageBackend === "Hugging Face (Cloud)" && (
|
| 412 |
+
<>
|
| 413 |
+
<div>
|
| 414 |
+
<label className="block text-[9px] uppercase tracking-widest text-[#665D4F] mb-1">HF Image Model</label>
|
| 415 |
+
<input type="text" value={hfImageModel} onChange={e => setHfImageModel(e.target.value)} className="input-alchemist w-full text-[10px] py-1.5 px-2 bg-[#120E0A]" placeholder="e.g. black-forest-labs/FLUX.1-schnell" />
|
| 416 |
+
</div>
|
| 417 |
+
<div>
|
| 418 |
+
<label className="block text-[9px] uppercase tracking-widest text-[#665D4F] mb-1">Inference Provider</label>
|
| 419 |
+
<select value={hfImageProvider} onChange={e => setHfImageProvider(e.target.value)} className="input-alchemist w-full text-[10px] py-1.5 px-2 bg-[#120E0A]">
|
| 420 |
+
<option value="auto">Auto-Managed (Serverless)</option>
|
| 421 |
+
<option value="fal-ai">Fal-AI (Partner)</option>
|
| 422 |
+
<option value="replicate">Replicate (Partner)</option>
|
| 423 |
+
<option value="hf-inference">HF Inference Endpoints</option>
|
| 424 |
+
</select>
|
| 425 |
+
</div>
|
| 426 |
+
</>
|
| 427 |
+
)}
|
| 428 |
+
<div>
|
| 429 |
+
<label className="block text-[9px] uppercase tracking-widest text-[#665D4F] mb-1">Manual API Token (Optional Override)</label>
|
| 430 |
+
<input type="password" value={manualToken} onChange={e => setManualToken(e.target.value)} className="input-alchemist w-full text-[10px] py-1.5 px-2 bg-[#120E0A] font-mono placeholder:font-sans" placeholder="hf_..., fal_..." />
|
| 431 |
+
</div>
|
| 432 |
+
</div>
|
| 433 |
+
</details>
|
| 434 |
</div>
|
| 435 |
</div>
|
| 436 |
|
|
|
|
| 438 |
<div className="flex-1 flex flex-col relative z-10 overflow-y-auto shadow-inner custom-scrollbar bg-[#0f0c08]">
|
| 439 |
<div className="sticky top-0 h-12 bg-gradient-to-b from-[#080604] to-transparent z-10 pointer-events-none w-full"></div>
|
| 440 |
|
| 441 |
+
<div className="max-w-2xl mx-auto w-full px-4 pb-24 pt-2">
|
| 442 |
+
{/* App Logo */}
|
| 443 |
+
<div className="w-full h-32 mb-6 relative border border-[#2D241A] rounded-xl bg-gradient-to-b from-[#0a0805] to-transparent shadow-inner overflow-hidden flex items-center justify-center">
|
| 444 |
+
<img src="/logo.svg" alt="Chronicle Portrait Studio Logo" className="w-full h-full object-cover drop-shadow-[0_0_15px_rgba(218,176,98,0.3)]" />
|
| 445 |
+
</div>
|
| 446 |
+
|
| 447 |
{/* Section Headers */}
|
| 448 |
+
<div className="mb-6 pl-2">
|
| 449 |
+
<h2 className="font-cinzel text-3xl text-[#E2D1B3] mb-2">{tabs.find(t => t.id === activeTab)?.label}</h2>
|
| 450 |
<div className="w-16 h-1 bg-[#DAB062] rounded-full opacity-50"></div>
|
| 451 |
</div>
|
| 452 |
|
| 453 |
+
<div className="glass-panel rounded-xl p-6 mb-6 relative overflow-hidden group">
|
| 454 |
<div className="absolute top-0 right-0 w-96 h-96 bg-[#DAB062]/5 rounded-full blur-3xl -mr-32 -mt-32 pointer-events-none transition-all duration-700 group-hover:bg-[#DAB062]/10"></div>
|
| 455 |
|
| 456 |
<div className="relative z-10 w-full">
|
| 457 |
{activeTab === 'identity' && (
|
| 458 |
<div className="w-full">
|
| 459 |
+
<div className="mb-6 p-5 bg-[#0a0805] border border-[#2D241A] rounded-xl shadow-inner relative group">
|
| 460 |
+
<div className="flex items-center justify-between mb-3">
|
| 461 |
+
<label className="text-[10px] uppercase tracking-[0.2em] text-[#DAB062] font-bold">Subject Name</label>
|
| 462 |
+
<button onClick={handleGenerateName} title="Generate random name based on Race" className="text-[#A89880] hover:text-[#DAB062] transition-colors flex items-center gap-1.5"><Dices size={14} /><span className="text-[10px] uppercase tracking-widest font-medium">Auto</span></button>
|
| 463 |
+
</div>
|
| 464 |
<input type="text" value={characterName} onChange={e => setCharacterName(e.target.value)} className="w-full bg-[#120E0A] border border-[#3D3224] text-[#E2D1B3] font-cinzel text-xl font-semibold rounded-lg px-4 py-3 outline-none focus:border-[#DAB062] focus:ring-1 focus:ring-[#DAB062]/50 transition-all placeholder-[#E2D1B3]/20" placeholder="Enter name..."/>
|
| 465 |
</div>
|
| 466 |
+
<div className="grid grid-cols-2 gap-x-12 gap-y-6">
|
| 467 |
{renderFeatureUI('identity', 'race', 'Race')}
|
| 468 |
{renderFeatureUI('identity', 'class', 'Class')}
|
| 469 |
{renderFeatureUI('identity', 'gender', 'Gender')}
|
| 470 |
{renderFeatureUI('identity', 'age', 'Age')}
|
| 471 |
</div>
|
| 472 |
+
<div className="mt-6">
|
| 473 |
<label className="block text-xs uppercase tracking-widest text-[#A89880] mb-4">Lore / Background Text</label>
|
| 474 |
<textarea value={extraInfo[0]} onChange={e => handleExtraInfoChange(0, e.target.value)} className="input-alchemist h-32 resize-none leading-relaxed"></textarea>
|
| 475 |
</div>
|
| 476 |
|
| 477 |
+
<div className="my-8 border-t border-[#2D241A]/50"></div>
|
| 478 |
|
| 479 |
<h3 className="font-cinzel text-2xl text-[#DAB062] mb-8 drop-shadow-sm">Expression & Stance</h3>
|
| 480 |
+
<div className="grid grid-cols-2 gap-x-12 gap-y-6">
|
| 481 |
{renderFeatureUI('expression_pose', 'expression', 'Expression')}
|
| 482 |
{renderFeatureUI('expression_pose', 'pose', 'Pose')}
|
| 483 |
</div>
|
|
|
|
| 486 |
|
| 487 |
{activeTab === 'appearance' && (
|
| 488 |
<div className="w-full">
|
| 489 |
+
<div className="grid grid-cols-2 gap-x-12 gap-y-6">
|
| 490 |
{renderFeatureUI('appearance', 'hair_color', 'Hair Color')}
|
| 491 |
{renderFeatureUI('appearance', 'hair_style', 'Hair Style')}
|
| 492 |
{renderFeatureUI('appearance', 'eye_color', 'Eye Color')}
|
|
|
|
| 494 |
{renderFeatureUI('appearance', 'skin_tone', 'Skin Tone')}
|
| 495 |
{renderFeatureUI('appearance', 'distinguishing_feature', 'Main Feature')}
|
| 496 |
</div>
|
| 497 |
+
<div className="mt-6">
|
| 498 |
<label className="block text-xs uppercase tracking-widest text-[#A89880] mb-4">Additional Visuals</label>
|
| 499 |
<textarea value={extraInfo[1]} onChange={e => handleExtraInfoChange(1, e.target.value)} className="input-alchemist h-32 resize-none leading-relaxed"></textarea>
|
| 500 |
</div>
|
|
|
|
| 503 |
|
| 504 |
{activeTab === 'equipment' && (
|
| 505 |
<div className="w-full">
|
| 506 |
+
<div className="grid grid-cols-2 gap-x-12 gap-y-6">
|
| 507 |
{renderFeatureUI('equipment', 'armor', 'Primary Gear')}
|
| 508 |
{renderFeatureUI('equipment', 'weapon', 'Weapon Variant')}
|
| 509 |
{renderFeatureUI('equipment', 'accessory', 'Acc. 1')}
|
| 510 |
{renderFeatureUI('equipment', 'accessory', 'Acc. 2')}
|
| 511 |
{renderFeatureUI('equipment', 'material', 'Material Finish')}
|
| 512 |
</div>
|
| 513 |
+
<div className="mt-6">
|
| 514 |
<label className="block text-xs uppercase tracking-widest text-[#A89880] mb-4">Custom Equipment Description</label>
|
| 515 |
<textarea value={extraInfo[2]} onChange={e => handleExtraInfoChange(2, e.target.value)} className="input-alchemist h-32 resize-none leading-relaxed"></textarea>
|
| 516 |
</div>
|
|
|
|
| 519 |
|
| 520 |
{activeTab === 'environment' && (
|
| 521 |
<div className="w-full">
|
| 522 |
+
<div className="grid grid-cols-2 gap-x-12 gap-y-6">
|
| 523 |
{renderFeatureUI('environment', 'background', 'Backdrop')}
|
| 524 |
{renderFeatureUI('environment', 'lighting', 'Illumination')}
|
| 525 |
{renderFeatureUI('environment', 'atmosphere', 'Atmosphere')}
|
| 526 |
</div>
|
| 527 |
+
<div className="mt-6">
|
| 528 |
<label className="block text-xs uppercase tracking-widest text-[#A89880] mb-4">Custom Environment Info</label>
|
| 529 |
<textarea value={extraInfo[3]} onChange={e => handleExtraInfoChange(3, e.target.value)} className="input-alchemist h-32 resize-none leading-relaxed"></textarea>
|
| 530 |
</div>
|
|
|
|
| 533 |
|
| 534 |
{activeTab === 'style' && (
|
| 535 |
<div className="w-full">
|
| 536 |
+
<div className="grid grid-cols-2 gap-x-12 gap-y-6">
|
| 537 |
{renderFeatureUI('vfx_style', 'vfx', 'Particle Effects')}
|
| 538 |
{renderFeatureUI('vfx_style', 'style', 'Illustration Style')}
|
| 539 |
{renderFeatureUI('vfx_style', 'mood', 'Color Grading/Mood')}
|