topguy commited on
Commit
e1f9b50
·
1 Parent(s): 4eef090

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 CHANGED
@@ -1,7 +1,4 @@
1
- Migrate to Next.js & FastAPI Architecture with Premium UI
2
 
3
- - Decouples monolithic Gradio app into FastAPI backend (`api.py`) and Next.js frontend (`frontend/`).
4
- - Implements Stitch MCP-inspired 'Alchemist's Workshop' aesthetic with glassmorphism and Cinzel typography.
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">Randomize Flags</span>
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
- <select value={refinementBackend} onChange={e => setRefinementBackend(e.target.value)} className="input-alchemist mb-3 text-xs" title="Text Backbone">
340
- <option>Gemini (Cloud)</option>
341
- <option>Hugging Face (Cloud)</option>
342
- <option>Ollama (Local)</option>
343
- </select>
344
- <select value={imageBackend} onChange={e => setImageBackend(e.target.value)} className="input-alchemist text-xs" title="Image Engine">
345
- <option>ComfyUI (Local)</option>
346
- <option>Gemini (Cloud)</option>
347
- <option>Hugging Face (Cloud)</option>
348
- </select>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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-8 pb-24">
 
 
 
 
 
357
  {/* Section Headers */}
358
- <div className="mb-10 pl-2">
359
- <h2 className="font-cinzel text-3xl text-[#E2D1B3] mb-3">{tabs.find(t => t.id === activeTab)?.label}</h2>
360
  <div className="w-16 h-1 bg-[#DAB062] rounded-full opacity-50"></div>
361
  </div>
362
 
363
- <div className="glass-panel rounded-2xl p-10 mb-8 relative overflow-hidden group">
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-10 p-6 bg-[#0a0805] border border-[#2D241A] rounded-xl shadow-inner relative group">
370
- <label className="block text-[10px] uppercase tracking-[0.2em] text-[#DAB062] mb-3 font-bold">Subject Name</label>
 
 
 
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-8">
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-10">
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-12 border-t border-[#2D241A]/50"></div>
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-8">
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-16 gap-y-12">
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-10">
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-16 gap-y-12">
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-10">
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-16 gap-y-12">
430
  {renderFeatureUI('environment', 'background', 'Backdrop')}
431
  {renderFeatureUI('environment', 'lighting', 'Illumination')}
432
  {renderFeatureUI('environment', 'atmosphere', 'Atmosphere')}
433
  </div>
434
- <div className="mt-10">
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-16 gap-y-12">
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')}