ChronicleNext / frontend /src /components /ClientApp.tsx
topguy's picture
Release v1.0.0: Official milestone with Alchemical Feedback, Thinking Model support, and Zoom-In Prompt editing.
c60a62f
Raw
History Blame Contribute Delete
61 kB
"use client";
import React, { useState, useEffect, useRef } from 'react';
import { User, Sparkles, Sword, Globe, Palette, Settings, Image as ImageIcon, Wand2, Dices, Save, Download, Upload, Copy, X, LogIn, LogOut, Loader2, Terminal, AlertTriangle, Maximize2 } from 'lucide-react';
import { oauthLoginUrl, oauthHandleRedirectIfPresent } from "@huggingface/hub";
const API_URL = process.env.NODE_ENV === "development" ? "http://127.0.0.1:8000" : "";
const tabs = [
{ id: 'identity', label: 'Identity & Race', icon: User },
{ id: 'appearance', label: 'Physical Traits', icon: Sparkles },
{ id: 'equipment', label: 'Gear & Weapons', icon: Sword },
{ id: 'environment', label: 'Atmosphere', icon: Globe },
{ id: 'style', label: 'Render Style', icon: Palette },
{ id: 'engines', label: 'Configuration', icon: Settings }
];
export default function ClientApp() {
const [config, setConfig] = useState<any>(null);
const [activeTab, setActiveTab] = useState('identity');
const [showLightbox, setShowLightbox] = useState(false);
// Feedback System State
const [toasts, setToasts] = useState<any[]>([]);
const [logs, setLogs] = useState<any[]>([]);
const [showLogs, setShowLogs] = useState(false);
const [maximizedField, setMaximizedField] = useState<'technical' | 'refined' | null>(null);
const addToast = (message: string, type: 'success' | 'error' | 'info' = 'info') => {
const id = Date.now();
setToasts(prev => [...prev, { id, message, type }]);
addLog(message, type);
setTimeout(() => {
setToasts(prev => prev.filter(t => t.id !== id));
}, 5000);
};
const addLog = (message: string, type: 'success' | 'error' | 'info' = 'info') => {
setLogs(prev => [{
timestamp: new Date().toLocaleTimeString(),
message,
type
}, ...prev].slice(0, 50)); // Keep last 50
};
const [characterName, setCharacterName] = useState("Unnamed Hero");
const [features, setFeatures] = useState<string[]>([]);
const [randomization, setRandomization] = useState<boolean[]>([]);
const [extraInfo, setExtraInfo] = useState<string[]>(Array(5).fill(""));
const [promptOutput, setPromptOutput] = useState("");
const [refinedOutput, setRefinedOutput] = useState("");
const [imageSrc, setImageSrc] = useState<string | null>(null);
const [statusMsg, setStatusMsg] = useState("");
const [refinementBackend, setRefinementBackend] = useState("Gemini (Cloud)");
const [imageBackend, setImageBackend] = useState("ComfyUI (Local)");
const [ollamaModel, setOllamaModel] = useState("");
const [hfTextModel, setHfTextModel] = useState("Qwen/Qwen2.5-72B-Instruct");
const [hfTextProvider, setHfTextProvider] = useState("auto");
const [hfImageModel, setHfImageModel] = useState("black-forest-labs/FLUX.1-dev");
const [hfImageProvider, setHfImageProvider] = useState("auto");
const [hfToken, setHfToken] = useState<string | null>(null);
const [manualTextEntry, setManualTextEntry] = useState(false);
const [manualImageEntry, setManualImageEntry] = useState(false);
const [isRefining, setIsRefining] = useState(false);
const [isGenerating, setIsGenerating] = useState(false);
// Restore Settings from LocalStorage & Handle OAuth Redirect
useEffect(() => {
// 1. Handle OAuth Redirect
const handleOAuth = async () => {
try {
// First fetch the client ID provided by Hugging Face to our backend
const configRes = await fetch(`${API_URL}/api/config/oauth`);
const config = await configRes.json();
const clientId = config.oauth_client_id;
const oauthResult = await oauthHandleRedirectIfPresent();
if (oauthResult) {
setHfToken(oauthResult.accessToken);
localStorage.setItem('hf_auth_token', oauthResult.accessToken);
// Clear the URL hash
window.history.replaceState({}, document.title, window.location.pathname);
setStatusMsg("Successfully authenticated with Hugging Face!");
}
} catch (e: any) {
console.error("OAuth error", e);
}
};
handleOAuth();
// 2. Restore Token from LocalStorage
const storedToken = localStorage.getItem('hf_auth_token');
if (storedToken) setHfToken(storedToken);
// 3. Restore Other Settings
const saved = localStorage.getItem('rpg_portrait_settings');
if (saved) {
try {
const data = JSON.parse(saved);
if (data.refinementBackend) setRefinementBackend(data.refinementBackend);
if (data.imageBackend) setImageBackend(data.imageBackend);
if (data.ollamaModel) setOllamaModel(data.ollamaModel);
if (data.hfTextModel) setHfTextModel(data.hfTextModel);
if (data.hfTextProvider) setHfTextProvider(data.hfTextProvider);
if (data.hfImageModel) setHfImageModel(data.hfImageModel);
if (data.hfImageProvider) setHfImageProvider(data.hfImageProvider);
} catch (e) {
console.error("Failed to restore settings", e);
}
}
}, []);
// Persist Settings to LocalStorage
useEffect(() => {
const settings = {
refinementBackend,
imageBackend,
ollamaModel,
hfTextModel,
hfTextProvider,
hfImageModel,
hfImageProvider
};
localStorage.setItem('rpg_portrait_settings', JSON.stringify(settings));
}, [refinementBackend, imageBackend, ollamaModel, hfTextModel, hfTextProvider, hfImageModel, hfImageProvider]);
const fileInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
fetch(`${API_URL}/api/config`)
.then(res => res.json())
.then(data => {
setConfig(data);
const initFeatures = data.feature_sequence.map(([cat, sub]: any) => {
const ks = Object.keys(data.features_data[cat]?.[sub] || {});
return ks.length > 0 ? ks[0] : "None";
});
setFeatures(initFeatures);
setRandomization(Array(data.feature_sequence.length).fill(false));
// Smart Active Engine Defaults
if (data.is_hf_space) {
setImageBackend("Hugging Face (Cloud)");
setRefinementBackend("Hugging Face (Cloud)");
} else {
if (!data.comfy_active) {
if (data.hf_active) setImageBackend("Hugging Face (Cloud)");
else if (data.gemini_active) setImageBackend("Gemini (Cloud)");
}
if (!data.ollama_models?.length) {
if (data.gemini_active) setRefinementBackend("Gemini (Cloud)");
else if (data.hf_active) setRefinementBackend("Hugging Face (Cloud)");
}
}
})
.catch(err => console.error("Failed to load config", err));
}, []);
const updatePrompt = async (newFeatures = features, newExtraInfo = extraInfo, newName = characterName) => {
if (!config || newFeatures.length === 0) return;
try {
const res = await fetch(`${API_URL}/api/generate_prompt`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
character_name: newName,
features: newFeatures,
randomization: randomization,
extra_info: newExtraInfo
})
});
const data = await res.json();
setPromptOutput(data.prompt);
} catch (e) {
console.error(e);
}
};
useEffect(() => {
if (config && features.length > 0) updatePrompt();
}, [features, extraInfo, characterName]);
const handleFeatureChange = (index: number, val: string) => {
const f = [...features];
f[index] = val;
setFeatures(f);
};
const handleExtraInfoChange = (index: number, val: string) => {
const e = [...extraInfo];
e[index] = val;
setExtraInfo(e);
};
const handleRandomize = async () => {
try {
setStatusMsg("Randomizing flagged parameters...");
const res = await fetch(`${API_URL}/api/regenerate_features`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ current_values: features, checkboxes: randomization })
});
const data = await res.json();
setFeatures(data.new_values);
setRefinedOutput(""); // Clear refined narrative when traits are re-rolled
setStatusMsg("Randomization complete.");
} catch (e) { console.error(e); }
};
const handleGenerateName = async () => {
if (!config) return;
try {
setStatusMsg("Weaving a name from the threads of fate...");
// find race index
const raceIndex = config?.feature_sequence?.findIndex((seq: any) => seq[1] === "race");
const race = raceIndex !== -1 && raceIndex !== undefined ? features[raceIndex] : "Human";
const res = await fetch(`${API_URL}/api/generate_name`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ race })
});
if (res.ok) {
const data = await res.json();
setCharacterName(data.name);
setStatusMsg("Name generation complete.");
}
} catch (e) { console.error(e); }
};
const applyLoadedData = (data: any) => {
if (!config) return;
setCharacterName(data.name || "Unnamed Hero");
const loadedFeatures = config.feature_sequence.map((seq: any) => data.features?.[seq[2]] || "None");
setFeatures(loadedFeatures);
const loadedRandom = config.feature_sequence.map((seq: any) => data.randomization?.[seq[2]] || false);
setRandomization(loadedRandom);
const loadedExtra = config.sections.map((sec: string) => data.extra_info?.[sec.toLowerCase()] || "");
setExtraInfo(loadedExtra);
setRefinedOutput(""); // Clear refined narrative when new traits are loaded
setStatusMsg(`Loaded ${data.name || "character"} successfully!`);
};
const handleLoadFile = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (evt) => {
try {
const data = JSON.parse(evt.target?.result as string);
applyLoadedData(data);
} catch (err) { setStatusMsg("Error: Invalid JSON file."); }
};
reader.readAsText(file);
if (fileInputRef.current) fileInputRef.current.value = "";
};
const handleSaveFile = () => {
if (!config) return;
const data: any = { name: characterName, features: {}, randomization: {}, extra_info: {} };
config.feature_sequence.forEach((seq: any, i: number) => {
data.features[seq[2]] = features[i];
data.randomization[seq[2]] = randomization[i];
});
config.sections.forEach((sec: string, i: number) => {
data.extra_info[sec.toLowerCase()] = extraInfo[i];
});
const blob = new Blob([JSON.stringify(data, null, 4)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${characterName.replace(/[^a-z0-9]/gi, '_').toLowerCase() || 'character'}_data.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
setStatusMsg("Character configuration saved!");
};
const handleLoadExample = async (filename: string) => {
if (!filename) return;
try {
const res = await fetch(`${API_URL}/api/example/${filename}`);
if(res.ok) {
const data = await res.json();
applyLoadedData(data);
} else {
setStatusMsg("Failed to load example.");
}
} catch (e) {
setStatusMsg("Failed to load example.");
}
};
const handleHFLogin = async () => {
try {
const configRes = await fetch(`${API_URL}/api/config/oauth`);
const config = await configRes.json();
const clientId = config.oauth_client_id;
if (!clientId) {
throw new Error("OAUTH_CLIENT_ID not found. Is your Space configured correctly with hf_oauth: true?");
}
const url = await oauthLoginUrl({
clientId: clientId,
scopes: "openid profile inference-api"
});
window.location.href = url;
} catch (e: any) {
console.error("HF Login Error:", e);
setStatusMsg(`Failed to initiate HF login: ${e.message || String(e)}`);
}
};
const handleHFLogout = () => {
setHfToken(null);
localStorage.removeItem('hf_auth_token');
setStatusMsg("Logged out of Hugging Face.");
};
const handleRefine = async () => {
const activeModel = refinementBackend === "Ollama (Local)" ? (ollamaModel || "llama3") :
(refinementBackend === "Hugging Face (Cloud)" ? hfTextModel : "Gemini-Pro");
addLog(`Initiating prompt refinement with ${activeModel}...`, "info");
setIsRefining(true);
try {
const res = await fetch(`${API_URL}/api/refine_prompt`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt: promptOutput,
backend: refinementBackend,
ollama_model: ollamaModel,
hf_text_model: hfTextModel,
hf_text_provider: hfTextProvider,
oauth_token: hfToken,
character_name: characterName
})
});
if (res.ok) {
const data = await res.json();
setRefinedOutput(data.refined_prompt);
addToast("Refinement successful!", "success");
} else {
const err = await res.json();
const msg = err.detail || "Refinement failed.";
addToast(msg, "error");
addLog(`Refinement Error Details: ${JSON.stringify(err)}`, "error");
}
} catch (e: any) {
addToast("Refinement failed to connect.", "error");
addLog(`Refinement Exception: ${e.message || String(e)}`, "error");
}
finally { setIsRefining(false); }
};
const handleGenerateImage = async () => {
addLog("Synthesizing portrait...", "info");
setIsGenerating(true);
setImageSrc(null);
try {
const aspectIndex = config.feature_sequence.findIndex((seq: any) => seq[2] === "aspect_ratio");
const ar = aspectIndex !== -1 ? features[aspectIndex] : "1:1";
const res = await fetch(`${API_URL}/api/generate_image`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
refined_prompt: refinedOutput,
technical_prompt: promptOutput,
aspect_ratio: ar,
backend: imageBackend,
hf_image_model: hfImageModel,
hf_image_provider: hfImageProvider,
oauth_token: hfToken,
character_name: characterName
})
});
if (res.ok) {
const blob = await res.blob();
setImageSrc(URL.createObjectURL(blob));
const statusMsg = res.headers.get("X-Status-Msg") || "Synthesis complete!";
addToast(statusMsg, "success");
} else {
const err = await res.json();
const msg = err.detail || "Synthesis failed.";
addToast(msg, "error");
addLog(`Synthesis Error Details: ${JSON.stringify(err)}`, "error");
}
} catch (e: any) {
addToast("Synthesis failed to initialize.", "error");
addLog(`Synthesis Exception: ${e.message || String(e)}`, "error");
}
finally { setIsGenerating(false); }
};
const handleDownloadImage = () => {
if (!imageSrc) return;
const a = document.createElement('a');
a.href = imageSrc;
a.download = `${characterName.replace(/[^a-z0-9]/gi, '_').toLowerCase() || 'portrait'}.png`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
};
const handleCopy = (text: string) => {
if(!text) return;
navigator.clipboard.writeText(text);
setStatusMsg("Copied to clipboard!");
};
const renderFeatureUI = (cat: string, subcat: string, label: string) => {
const idx = config.feature_sequence.findIndex((seq: any) => seq[0] === cat && seq[1] === subcat);
if (idx === -1) return null;
let mappedIdx = idx;
if (subcat === 'accessory' && label.includes('2')) mappedIdx = idx + 1;
const choices = Object.keys(config.features_data[cat]?.[subcat] || {});
return (
<div className="mb-8 relative group" key={`${cat}-${subcat}-${label}`}>
<label className="block text-[10px] uppercase tracking-[0.2em] text-[#DAB062] mb-2 font-bold">{label}</label>
<div className="flex items-center gap-3">
<select
className="input-alchemist flex-1 appearance-none cursor-pointer"
value={features[mappedIdx] || ""}
onChange={(e) => handleFeatureChange(mappedIdx, e.target.value)}
>
{choices.map(c => <option key={c} value={c}>{c}</option>)}
</select>
<div className="flex flex-col items-center justify-center">
<button
onClick={() => {
const r = [...randomization];
r[mappedIdx] = !r[mappedIdx];
setRandomization(r);
}}
className={`p-2 rounded-md transition-all duration-300 ${randomization[mappedIdx] ? 'bg-[#DAB062] text-[#080604] shadow-[0_0_12px_rgba(218,176,98,0.6)]' : 'bg-[#0a0805] text-[#A89880] hover:text-[#DAB062] border border-[#2D241A] hover:border-[#DAB062]/50'}`}
title="Toggle Randomization"
>
<Dices size={16} />
</button>
</div>
</div>
</div>
);
};
if (!config) return <div className="h-screen w-full flex items-center justify-center font-cinzel text-2xl text-[#DAB062]">Igniting forge...</div>;
return (
<div className="flex h-screen w-full overflow-hidden text-[#E2D1B3] antialiased">
{/* Hidden file input for loading JSON */}
<input type="file" accept=".json" className="hidden" ref={fileInputRef} onChange={handleLoadFile} />
{/* 1. Left Sidebar Navigation */}
<div className="w-[300px] border-r border-[#2D241A] bg-[#0a0805]/95 backdrop-blur-xl flex flex-col relative z-20">
<div className="p-8 border-b border-[#2D241A] text-center">
<h1 className="font-cinzel font-bold text-3xl gold-gradient-text drop-shadow-md mb-2">Chronicle</h1>
<p className="text-[#A89880] text-sm uppercase tracking-[0.2em]">Portrait Studio</p>
<p className="text-[#665D4F] text-[10px] font-mono mt-2 opacity-80">v1.0.0</p>
</div>
<nav className="flex-1 px-4 py-8 space-y-2 overflow-y-auto custom-scrollbar">
<div className="text-[10px] uppercase tracking-widest text-[#665D4F] mb-4 px-4">Creation Aspects</div>
{tabs.map(t => {
const Icon = t.icon;
const isActive = activeTab === t.id;
return (
<button
key={t.id}
onClick={() => setActiveTab(t.id)}
className={`w-full flex items-center gap-4 px-4 py-3 rounded-lg transition-all duration-300 group
${isActive ? 'bg-[#18130E] border border-[#3D3224] shadow-[inset_0_0_15px_rgba(218,176,98,0.05)]' : 'border border-transparent hover:bg-[#120E0A]'}`}
>
<Icon size={20} className={isActive ? 'text-[#DAB062]' : 'text-[#665D4F] group-hover:text-[#A89880]'} />
<span className={`text-sm tracking-wide flex-1 text-left ${isActive ? 'text-[#E2D1B3] font-medium' : 'text-[#8C7F6B] group-hover:text-[#A89880]'}`}>
{t.label}
</span>
</button>
);
})}
<div className="h-4"></div>
<div className="text-[10px] uppercase tracking-widest text-[#A89880] mb-4 px-4">State Management</div>
<div className="px-2 grid grid-cols-2 gap-2 mb-4">
<button onClick={() => fileInputRef.current?.click()} className="flex items-center justify-center gap-2 px-3 py-2 border border-[#2D241A] bg-[#0E151A]/50 rounded font-medium text-[#A89880] hover:text-[#DAB062] hover:border-[#DAB062]/50 transition-colors text-xs tracking-wider">
<Upload size={14} /> Load
</button>
<button onClick={handleSaveFile} className="flex items-center justify-center gap-2 px-3 py-2 border border-[#2D241A] bg-[#0E151A]/50 rounded font-medium text-[#A89880] hover:text-[#DAB062] hover:border-[#DAB062]/50 transition-colors text-xs tracking-wider">
<Save size={14} /> Save
</button>
</div>
<div className="px-2 mb-6">
<label className="text-[10px] block uppercase tracking-widest text-[#A89880] mb-2 px-2">Load Example</label>
<select
onChange={(e) => handleLoadExample(e.target.value)}
className="input-alchemist py-2 text-xs"
defaultValue=""
>
<option value="" disabled>Select a Preset...</option>
{config.examples?.map((ex: string) => <option key={ex} value={ex}>{ex.replace('.json', '')}</option>)}
</select>
</div>
<div className="text-[10px] uppercase tracking-widest text-[#A89880] mb-4 px-4 border-t border-[#2D241A] pt-6">Global Actions</div>
<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 mb-4" style={{ width: 'calc(100% - 1rem)'}}>
<Dices size={18} />
<span className="text-sm tracking-wide font-medium">Reroll Marked Traits</span>
</button>
<button
onClick={() => setShowLogs(!showLogs)}
className={`w-full mx-2 mb-8 flex items-center justify-center gap-3 px-4 py-2 border rounded-lg transition-all text-[10px] uppercase tracking-[0.2em] font-bold ${showLogs ? 'bg-[#DAB062] text-[#080604] border-[#DAB062]' : 'bg-[#0a0805] text-[#A89880] border-[#2D241A] hover:border-[#DAB062]/50'}`}
style={{ width: 'calc(100% - 1rem)'}}
>
<Terminal size={14} />
<span>System Status {logs.length > 0 && `(${logs.length})`}</span>
</button>
</nav>
</div>
{/* 2. Main Middle Area (Form Aspects) */}
<div className="flex-1 flex flex-col relative z-10 overflow-y-auto shadow-inner custom-scrollbar bg-[#0f0c08]">
<div className="sticky top-0 h-12 bg-gradient-to-b from-[#080604] to-transparent z-10 pointer-events-none w-full"></div>
<div className="max-w-4xl mx-auto w-full px-6 pb-24 pt-2">
{/* App Logo */}
<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">
<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)]" />
</div>
{/* Section Headers */}
<div className="mb-6 pl-2">
<h2 className="font-cinzel text-3xl text-[#E2D1B3] mb-2">{tabs.find(t => t.id === activeTab)?.label}</h2>
<div className="w-16 h-1 bg-[#DAB062] rounded-full opacity-50"></div>
</div>
<div className="glass-panel rounded-xl p-6 mb-6 relative overflow-hidden group">
<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>
<div className="relative z-10 w-full">
{activeTab === 'engines' && (
<div className="w-full space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-[#0a0805]/80 border border-[#2D241A] rounded-xl p-6 shadow-2xl backdrop-blur-sm">
<div className="flex items-center gap-3 mb-6">
<div className="p-2 bg-[#DAB062]/10 rounded-lg">
<Sparkles className="text-[#DAB062]" size={20} />
</div>
<h3 className="font-cinzel text-xl text-[#E2D1B3]">Text Intelligence</h3>
</div>
<div className="space-y-5">
<div>
<label className="text-[10px] block uppercase tracking-widest text-[#A89880] mb-2">Backbone Engine</label>
<select value={refinementBackend} onChange={e => setRefinementBackend(e.target.value)} className="input-alchemist w-full font-cinzel">
<option value="Gemini (Cloud)" disabled={!config?.gemini_active}>Gemini (Cloud){!config?.gemini_active ? ' (Missing Key)' : ''}</option>
<option value="Hugging Face (Cloud)" disabled={!config?.hf_active && !hfToken}>Hugging Face (Cloud){(!config?.hf_active && !hfToken) ? ' (Missing Token)' : ''}</option>
<option value="Ollama (Local)" disabled={config?.is_hf_space || !config?.ollama_models?.length}>Ollama (Local){config?.is_hf_space ? ' (Local Only)' : (!config?.ollama_models?.length ? ' (Offline)' : '')}</option>
</select>
</div>
{refinementBackend === "Ollama (Local)" && (
<div className="animate-in fade-in slide-in-from-top-2">
<label className="text-[10px] block uppercase tracking-widest text-[#A89880] mb-2">Local Model Identifier</label>
<select value={ollamaModel} onChange={e => setOllamaModel(e.target.value)} className="input-alchemist w-full">
<option value="">Default (llama3)</option>
{config?.ollama_models?.map((m: string) => <option key={m} value={m}>{m}</option>)}
</select>
</div>
)}
{refinementBackend === "Hugging Face (Cloud)" && (
<div className="animate-in fade-in slide-in-from-top-2">
<div className="flex items-center justify-between mb-2">
<label className="text-[10px] block uppercase tracking-widest text-[#A89880]">Refinement Model</label>
<button onClick={() => setManualTextEntry(!manualTextEntry)} className="text-[10px] uppercase text-[#DAB062] hover:underline transition-all">
{manualTextEntry ? "Standard List" : "Manual Entry"}
</button>
</div>
{manualTextEntry ? (
<input
type="text"
value={hfTextModel}
onChange={e => setHfTextModel(e.target.value)}
className="input-alchemist w-full"
placeholder="Enter model ID..."
/>
) : (
<select
value={hfTextModel}
onChange={e => setHfTextModel(e.target.value)}
className="input-alchemist w-full"
>
{config?.hf_text_models?.map((m: string) => (
<option key={m} value={m}>{m}</option>
))}
{config?.hf_text_models && !config.hf_text_models.includes(hfTextModel) && (
<option value={hfTextModel}>{hfTextModel}</option>
)}
</select>
)}
</div>
)}
</div>
</div>
<div className="bg-[#0a0805]/80 border border-[#2D241A] rounded-xl p-6 shadow-2xl backdrop-blur-sm">
<div className="flex items-center gap-3 mb-6">
<div className="p-2 bg-[#DAB062]/10 rounded-lg">
<ImageIcon className="text-[#DAB062]" size={20} />
</div>
<h3 className="font-cinzel text-xl text-[#E2D1B3]">Visual Synthesis</h3>
</div>
<div className="space-y-5">
<div>
<label className="text-[10px] block uppercase tracking-widest text-[#A89880] mb-2">Generation Core</label>
<select value={imageBackend} onChange={e => setImageBackend(e.target.value)} className="input-alchemist w-full font-cinzel">
<option value="Gemini (Cloud)" disabled={!config?.gemini_active}>Gemini (Cloud){!config?.gemini_active ? ' (Missing Key)' : ''}</option>
<option value="Hugging Face (Cloud)" disabled={!config?.hf_active && !hfToken}>Hugging Face (Cloud){(!config?.hf_active && !hfToken) ? ' (Missing Token)' : ''}</option>
<option value="ComfyUI (Local)" disabled={config?.is_hf_space || !config?.comfy_active}>ComfyUI (Local){config?.is_hf_space ? ' (Local Only)' : (!config?.comfy_active ? ' (Offline)' : '')}</option>
</select>
</div>
{imageBackend === "Hugging Face (Cloud)" && (
<div className="space-y-4 animate-in fade-in slide-in-from-top-2">
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-[10px] block uppercase tracking-widest text-[#A89880]">Diffusion Model</label>
<button onClick={() => setManualImageEntry(!manualImageEntry)} className="text-[10px] uppercase text-[#DAB062] hover:underline transition-all">
{manualImageEntry ? "Standard List" : "Manual Entry"}
</button>
</div>
{manualImageEntry ? (
<input
type="text"
value={hfImageModel}
onChange={e => setHfImageModel(e.target.value)}
className="input-alchemist w-full"
placeholder="Enter model ID..."
/>
) : (
<select
value={hfImageModel}
onChange={e => setHfImageModel(e.target.value)}
className="input-alchemist w-full"
>
{config?.hf_image_models?.map((m: string) => (
<option key={m} value={m}>{m}</option>
))}
{config?.hf_image_models && !config.hf_image_models.includes(hfImageModel) && (
<option value={hfImageModel}>{hfImageModel}</option>
)}
</select>
)}
</div>
<div>
<label className="text-[10px] block uppercase tracking-widest text-[#A89880] mb-2">Inference Gateway</label>
<select value={hfImageProvider} onChange={e => setHfImageProvider(e.target.value)} className="input-alchemist w-full">
<option value="auto">Auto-Managed</option>
<option value="fal-ai">Fal-AI (Fast)</option>
<option value="replicate">Replicate</option>
<option value="hf-inference">Direct HF API</option>
</select>
</div>
</div>
)}
</div>
</div>
</div>
<div className="mt-8 border-t border-[#2D241A] pt-8 animate-in fade-in slide-in-from-bottom-2 duration-700 delay-200">
<div className="bg-[#0a0805]/80 border border-[#2D241A] rounded-xl p-6 shadow-2xl backdrop-blur-sm">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<div className="p-2 bg-[#DAB062]/10 rounded-lg">
<User className="text-[#DAB062]" size={20} />
</div>
<h3 className="font-cinzel text-xl text-[#E2D1B3]">Hugging Face Identity</h3>
</div>
<div className="flex items-center gap-2">
<div className={`w-2 h-2 rounded-full ${hfToken ? 'bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.6)]' : (config?.hf_active ? 'bg-yellow-500 shadow-[0_0_8px_rgba(234,179,8,0.6)]' : 'bg-red-500 shadow-[0_0_8px_rgba(239,68,68,0.6)]')}`}></div>
<span className="text-[10px] uppercase tracking-widest text-[#A89880]">
{hfToken ? 'Authenticated' : (config?.hf_active ? 'Developer Mode' : 'Disconnected')}
</span>
</div>
</div>
<p className="text-xs text-[#8C7F6B] mb-6 leading-relaxed">
{hfToken
? "You are currently signed in with your Hugging Face account. Your scoped access token is being used for inference requests."
: (config?.hf_active
? "The studio is using the developer's pre-configured API key (.env). Sign in to use your own account and bypass server limits."
: "No Hugging Face credentials found. Please sign in to enable cloud-based generation.")
}
</p>
{hfToken ? (
<button onClick={handleHFLogout} className="flex items-center gap-2 px-4 py-2 border border-red-900/30 bg-red-950/10 rounded-lg text-red-400 hover:bg-red-900/20 transition-all text-xs font-medium uppercase tracking-widest">
<LogOut size={14} /> Sign Out of HF
</button>
) : (
<button onClick={handleHFLogin} className="flex items-center gap-2 px-6 py-3 bg-[#DAB062] text-[#0a0805] rounded-xl font-cinzel font-bold text-sm tracking-widest hover:bg-[#F2DAA4] transition-all shadow-lg shadow-yellow-900/10">
<LogIn size={18} /> Sign in with Hugging Face
</button>
)}
</div>
</div>
</div>
)}
{activeTab === 'identity' && (
<div className="w-full">
<div className="mb-6 p-5 bg-[#0a0805] border border-[#2D241A] rounded-xl shadow-inner relative group">
<div className="flex items-center justify-between mb-3">
<label className="text-[10px] uppercase tracking-[0.2em] text-[#DAB062] font-bold">Subject Name</label>
<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>
</div>
<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..."/>
</div>
<div className="grid grid-cols-2 gap-x-12 gap-y-6">
{renderFeatureUI('identity', 'race', 'Race')}
{renderFeatureUI('identity', 'class', 'Class')}
{renderFeatureUI('identity', 'gender', 'Gender')}
{renderFeatureUI('identity', 'age', 'Age')}
</div>
<div className="mt-6">
<label className="block text-xs uppercase tracking-widest text-[#A89880] mb-4">Lore / Background Text</label>
<textarea value={extraInfo[0]} onChange={e => handleExtraInfoChange(0, e.target.value)} className="input-alchemist h-32 resize-none leading-relaxed"></textarea>
</div>
<div className="my-8 border-t border-[#2D241A]/50"></div>
<h3 className="font-cinzel text-2xl text-[#DAB062] mb-8 drop-shadow-sm">Expression & Stance</h3>
<div className="grid grid-cols-2 gap-x-12 gap-y-6">
{renderFeatureUI('expression_pose', 'expression', 'Expression')}
{renderFeatureUI('expression_pose', 'pose', 'Pose')}
</div>
</div>
)}
{activeTab === 'appearance' && (
<div className="w-full">
<div className="grid grid-cols-2 gap-x-12 gap-y-6">
{renderFeatureUI('appearance', 'hair_color', 'Hair Color')}
{renderFeatureUI('appearance', 'hair_style', 'Hair Style')}
{renderFeatureUI('appearance', 'eye_color', 'Eye Color')}
{renderFeatureUI('appearance', 'build', 'Build')}
{renderFeatureUI('appearance', 'skin_tone', 'Skin Tone')}
{renderFeatureUI('appearance', 'distinguishing_feature', 'Main Feature')}
</div>
<div className="mt-6">
<label className="block text-xs uppercase tracking-widest text-[#A89880] mb-4">Additional Visuals</label>
<textarea value={extraInfo[1]} onChange={e => handleExtraInfoChange(1, e.target.value)} className="input-alchemist h-32 resize-none leading-relaxed"></textarea>
</div>
</div>
)}
{activeTab === 'equipment' && (
<div className="w-full">
<div className="grid grid-cols-2 gap-x-12 gap-y-6">
{renderFeatureUI('equipment', 'armor', 'Primary Gear')}
{renderFeatureUI('equipment', 'weapon', 'Weapon Variant')}
{renderFeatureUI('equipment', 'accessory', 'Acc. 1')}
{renderFeatureUI('equipment', 'accessory', 'Acc. 2')}
{renderFeatureUI('equipment', 'material', 'Material Finish')}
</div>
<div className="mt-6">
<label className="block text-xs uppercase tracking-widest text-[#A89880] mb-4">Custom Equipment Description</label>
<textarea value={extraInfo[2]} onChange={e => handleExtraInfoChange(2, e.target.value)} className="input-alchemist h-32 resize-none leading-relaxed"></textarea>
</div>
</div>
)}
{activeTab === 'environment' && (
<div className="w-full">
<div className="grid grid-cols-2 gap-x-12 gap-y-6">
{renderFeatureUI('environment', 'background', 'Backdrop')}
{renderFeatureUI('environment', 'lighting', 'Illumination')}
{renderFeatureUI('environment', 'atmosphere', 'Atmosphere')}
</div>
<div className="mt-6">
<label className="block text-xs uppercase tracking-widest text-[#A89880] mb-4">Custom Environment Info</label>
<textarea value={extraInfo[3]} onChange={e => handleExtraInfoChange(3, e.target.value)} className="input-alchemist h-32 resize-none leading-relaxed"></textarea>
</div>
</div>
)}
{activeTab === 'style' && (
<div className="w-full">
<div className="grid grid-cols-2 gap-x-12 gap-y-6">
{renderFeatureUI('vfx_style', 'vfx', 'Particle Effects')}
{renderFeatureUI('vfx_style', 'style', 'Illustration Style')}
{renderFeatureUI('vfx_style', 'mood', 'Color Grading/Mood')}
{renderFeatureUI('vfx_style', 'camera', 'Camera Setup')}
{renderFeatureUI('technical', 'aspect_ratio', 'Aspect Ratio')}
</div>
<div className="mt-10">
<label className="block text-xs uppercase tracking-widest text-[#A89880] mb-4">Additional Technical Prompts</label>
<textarea value={extraInfo[4]} onChange={e => handleExtraInfoChange(4, e.target.value)} className="input-alchemist h-32 resize-none leading-relaxed"></textarea>
</div>
</div>
)}
</div>
</div>
</div>
</div>
{/* 3. Right Crucible Area (Output & Synthesis) */}
<div className="w-[450px] border-l border-[#2D241A] bg-[#0C0A08] flex flex-col flex-shrink-0 shadow-[-10px_0_30px_rgba(0,0,0,0.5)] z-30">
{/* Output Image Canvas */}
<div className="h-[45%] p-6 border-b border-[#2D241A] relative flex items-center justify-center overflow-hidden">
<div className="absolute inset-0 bg-[url('https://www.transparenttextures.com/patterns/dark-matter.png')] opacity-10 pointer-events-none"></div>
{imageSrc ? (
<div className="relative w-full h-full flex items-center justify-center rounded-lg overflow-hidden group cursor-zoom-in" onClick={() => setShowLightbox(true)}>
<div className="absolute inset-0 border border-[#DAB062] opacity-30 pointer-events-none z-10 rounded-lg group-hover:opacity-60 transition-opacity"></div>
<img src={imageSrc || ""} alt="Portrait" className="max-w-full max-h-full object-contain drop-shadow-[0_0_20px_rgba(218,176,98,0.2)] transition-transform duration-700 ease-in-out group-hover:scale-[1.02]" />
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/30 transition-colors pointer-events-none flex items-center justify-center">
<div className="opacity-0 group-hover:opacity-100 transition-opacity bg-[#0a0805]/80 backdrop-blur-md border border-[#DAB062]/40 p-3 rounded-full text-[#DAB062] shadow-xl">
<ImageIcon size={24} />
</div>
</div>
<div className="absolute bottom-3 right-3 flex items-center gap-2">
<button onClick={(e) => { e.stopPropagation(); handleDownloadImage(); }} title="Download Portrait" className="opacity-0 group-hover:opacity-100 bg-black/60 backdrop-blur-md text-[#E2D1B3] p-2 rounded-md hover:text-[#DAB062] hover:bg-black/80 border border-[#2D241A] transition-all"><Download size={18}/></button>
</div>
</div>
) : statusMsg && (statusMsg.includes("Error") || statusMsg.includes("Failed")) ? (
<div className="flex flex-col items-center justify-center text-center px-8 animate-in fade-in zoom-in-95 duration-500">
<div className="p-4 bg-red-950/20 border border-red-500/30 rounded-full mb-6 relative">
<X size={48} className="text-red-500/60" />
<div className="absolute inset-0 bg-red-500/10 blur-xl rounded-full"></div>
</div>
<h4 className="font-cinzel text-red-400 tracking-[0.3em] text-sm mb-3 font-bold uppercase drop-shadow-sm">Synthesis Chamber Fault</h4>
<p className="text-red-300/60 text-xs italic font-medium leading-relaxed max-w-[280px]">"{statusMsg}"</p>
<button onClick={() => setStatusMsg("")} className="mt-6 px-4 py-1.5 border border-red-900/50 rounded-full text-[10px] uppercase tracking-widest text-red-400/50 hover:bg-red-900/10 hover:text-red-400 transition-all">Clear Error</button>
</div>
) : (
<div className="flex flex-col items-center justify-center text-center opacity-40">
<ImageIcon size={56} className="text-[#A89880] mb-4 stroke-1 line-through opacity-20" />
<p className="font-cinzel text-[#A89880] tracking-widest text-sm italic">Visual Synthesis Pending</p>
</div>
)}
</div>
{/* Action Panel */}
<div className="flex-1 p-6 flex flex-col overflow-y-auto custom-scrollbar">
<div className="flex-1 flex flex-col group relative shrink-0 min-h-[160px]">
<div className="flex items-center justify-between mb-4">
<label className="text-xs uppercase tracking-widest text-[#A89880] font-medium flex items-center gap-2"><Sparkles size={14} className="text-[#61C2DF]" /> Narrative Prompt</label>
<div className="flex items-center gap-2">
<button onClick={() => setMaximizedField('refined')} title="Zoom In" className="p-1.5 rounded-lg bg-transparent border border-[#1A2E38] hover:bg-[#1A2E38]/50 text-[#A89880] hover:text-[#DAB062] transition-all"><Maximize2 size={14} /></button>
<button onClick={() => handleCopy(refinedOutput || promptOutput)} title="Copy Narrative" className="p-1.5 rounded-lg bg-transparent border border-[#1A2E38] hover:bg-[#1A2E38]/50 text-[#A89880] hover:text-[#61C2DF] transition-all"><Copy size={14} /></button>
</div>
</div>
<textarea
value={refinedOutput}
onChange={e => setRefinedOutput(e.target.value)}
placeholder="Click refine to generate an artistic prompt... or type manually."
className="flex-1 bg-[#12181C] border border-[#1A2E38] rounded-xl p-4 text-sm text-[#E0F2FE] tracking-wide resize-none focus:outline-none focus:border-[#61C2DF] focus:ring-1 focus:ring-[#61C2DF]/30 custom-scrollbar shadow-inner transition-colors group-hover:border-[#223F4D]"
></textarea>
<button
onClick={handleRefine}
disabled={isRefining}
className={`mt-4 w-full shrink-0 relative overflow-hidden group bg-gradient-to-r from-[#1A3F4D] via-[#2F657D] to-[#1A3F4D] text-[#E0F2FE] font-cinzel font-bold text-sm tracking-widest py-3.5 rounded-xl transition-all ${isRefining ? 'opacity-70 cursor-not-allowed border border-[#61C2DF]/30 shadow-[0_0_15px_rgba(97,194,223,0.3)]' : 'shadow-[0_0_15px_rgba(97,194,223,0.15)] hover:shadow-[0_0_25px_rgba(97,194,223,0.3)]'}`}
>
{!isRefining && <div className="absolute inset-0 bg-white/10 translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-1000 ease-out"></div>}
<span className="relative z-10 flex items-center justify-center gap-3">
{isRefining ? <Loader2 size={18} className="animate-spin" /> : <Sparkles size={18} />}
{isRefining ? 'Refining...' : 'Enhance Narrative'}
</span>
</button>
</div>
<div className="my-4 border-t border-[#2D241A]/30"></div>
<div className="flex flex-col group relative shrink-0">
<div className="flex items-center justify-between mb-4">
<label className="text-xs uppercase tracking-widest text-[#A89880] font-medium">Autogenerated Base Prompt</label>
<div className="flex items-center gap-2">
<button onClick={() => setMaximizedField('technical')} title="Zoom In" className="p-1.5 rounded-lg bg-transparent border border-[#2D241A] hover:bg-[#2D241A]/50 text-[#A89880] hover:text-[#DAB062] transition-all"><Maximize2 size={14} /></button>
<button onClick={() => handleCopy(promptOutput)} title="Copy Base Prompt" className="p-1.5 rounded-lg bg-transparent border border-[#2D241A] hover:bg-[#2D241A]/50 text-[#A89880] hover:text-[#DAB062] transition-all"><Copy size={14} /></button>
</div>
</div>
<textarea readOnly value={promptOutput} className="h-40 bg-[#080604] border border-[#1A2E38] rounded-xl p-4 text-xs text-[#E2D1B3] resize-none custom-scrollbar shadow-inner outline-none leading-relaxed"></textarea>
</div>
<div className={`text-center font-cinzel text-[10px] tracking-[0.2em] my-4 min-h-[20px] transition-all duration-300 ${statusMsg.toLowerCase().includes('error') || statusMsg.toLowerCase().includes('failed') ? 'text-red-400 font-bold animate-pulse' : 'text-[#DAB062] font-medium'}`}>
{statusMsg && (
<span className="flex items-center justify-center gap-2">
{statusMsg.toLowerCase().includes('error') || statusMsg.toLowerCase().includes('failed') ? <X size={12}/> : <Sparkles size={12}/>}
{statusMsg}
</span>
)}
</div>
<button
onClick={handleGenerateImage}
disabled={isGenerating}
className={`w-full shrink-0 relative overflow-hidden group bg-gradient-to-r from-[#8C6D3B] via-[#DAB062] to-[#8C6D3B] text-[#0a0805] font-cinzel font-bold text-base tracking-widest py-4 rounded-xl transition-all ${isGenerating ? 'opacity-70 cursor-not-allowed border border-[#DAB062]/50 shadow-[0_0_20px_rgba(218,176,98,0.4)]' : 'shadow-[0_0_20px_rgba(218,176,98,0.2)] hover:shadow-[0_0_30px_rgba(218,176,98,0.4)]'}`}
>
{!isGenerating && <div className="absolute inset-0 bg-white/20 translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-1000 ease-out"></div>}
<span className="relative z-10 flex items-center justify-center gap-3">
{isGenerating ? <Loader2 size={22} className="animate-spin" /> : <Wand2 size={22} />}
{isGenerating ? 'Synthesizing...' : 'Synthesize'}
</span>
</button>
</div>
</div>
{/* FULL SCREEN LIGHTBOX MODAL */}
{showLightbox && imageSrc && (
<div className="fixed inset-0 z-[100] bg-[#080604]/95 backdrop-blur-2xl flex items-center justify-center p-4 md:p-8 animate-in fade-in duration-300" onClick={() => setShowLightbox(false)}>
<div className="absolute top-8 right-8 z-[110] flex gap-4">
<button onClick={(e) => { e.stopPropagation(); handleDownloadImage(); }} className="text-[#A89880] hover:text-[#DAB062] transition-colors bg-black/40 p-3 rounded-full border border-[#2D241A] shadow-xl group/btn" title="Download Masterpiece">
<Download size={24} />
<span className="absolute right-full mr-4 top-1/2 -translate-y-1/2 px-3 py-1 bg-black/80 text-[#DAB062] text-[10px] uppercase tracking-widest rounded-md border border-[#2D241A] opacity-0 group-hover/btn:opacity-100 transition-opacity whitespace-nowrap pointer-events-none">Preserve Masterpiece</span>
</button>
<button onClick={() => setShowLightbox(false)} className="text-[#A89880] hover:text-[#61C2DF] transition-colors bg-black/40 p-3 rounded-full border border-[#2D241A] shadow-xl" title="Close">
<X size={24} />
</button>
</div>
<div className="relative max-w-full max-h-full flex items-center justify-center animate-in zoom-in-95 duration-500" onClick={e => e.stopPropagation()}>
<div className="relative">
<div className="absolute inset-0 border border-[#DAB062]/20 rounded-xl pointer-events-none blur-sm"></div>
<img
src={imageSrc || ""}
alt="Portrait High Resolution"
className="max-w-full max-h-[92vh] rounded-xl shadow-[0_0_100px_rgba(218,176,98,0.15)] object-contain border border-[#2D241A]"
/>
</div>
</div>
</div>
)}
{/* SYSTEM LOG PANEL */}
{showLogs && (
<div className="fixed left-[300px] bottom-0 right-0 h-[300px] bg-[#0a0805]/95 backdrop-blur-2xl border-t border-[#2D241A] z-40 animate-in slide-in-from-bottom duration-500 shadow-[0_-20px_50px_rgba(0,0,0,0.5)]">
<div className="p-4 border-b border-[#2D241A] flex items-center justify-between">
<div className="flex items-center gap-3">
<Terminal size={16} className="text-[#DAB062]" />
<span className="font-cinzel text-xs uppercase tracking-widest text-[#DAB062]">Alchemical Synthesis Logs</span>
</div>
<button onClick={() => setShowLogs(false)} className="text-[#665D4F] hover:text-[#E2D1B3]">
<X size={18} />
</button>
</div>
<div className="p-4 overflow-y-auto h-[calc(300px-54px)] custom-scrollbar font-mono text-[11px] space-y-2">
{logs.length === 0 ? (
<div className="text-[#665D4F] italic text-center py-10">The records are empty. Initiate an operation.</div>
) : (
logs.map((log, i) => (
<div key={i} className="flex gap-4 group/log">
<span className="text-[#443C32] select-none shrink-0">[{log.timestamp}]</span>
<span className={`
${log.type === 'error' ? 'text-red-400' : ''}
${log.type === 'success' ? 'text-green-400' : ''}
${log.type === 'info' ? 'text-[#8C7F6B]' : ''}
`}>{log.message}</span>
</div>
))
)}
</div>
</div>
)}
{/* TOAST SYSTEM */}
<div className="fixed top-8 right-8 z-[150] flex flex-col gap-4 pointer-events-none">
{toasts.map(toast => (
<div
key={toast.id}
className={`
pointer-events-auto px-6 py-4 rounded-xl border shadow-2xl backdrop-blur-xl animate-in fade-in slide-in-from-right-8 duration-500 flex items-center gap-4 min-w-[320px] max-w-md
${toast.type === 'error' ? 'bg-red-500/10 border-red-500/20 text-red-200' : ''}
${toast.type === 'success' ? 'bg-[#DAB062]/10 border-[#DAB062]/20 text-[#DAB062]' : ''}
${toast.type === 'info' ? 'bg-blue-500/10 border-blue-500/20 text-blue-200' : ''}
`}
>
{toast.type === 'error' ? <AlertTriangle size={20} className="text-red-400" /> : <Sparkles size={20} className="text-[#DAB062]" />}
<div className="flex-1">
<div className="text-[10px] uppercase tracking-widest opacity-60 font-bold mb-1">{toast.type}</div>
<div className="text-xs font-medium leading-relaxed">{toast.message}</div>
</div>
<button onClick={() => setToasts(prev => prev.filter(t => t.id !== toast.id))} className="text-white/20 hover:text-white transition-colors">
<X size={14} />
</button>
</div>
))}
</div>
{/* MAXIMIZED PROMPT MODAL */}
{maximizedField && (
<div className="fixed inset-0 z-[200] bg-[#080604]/98 backdrop-blur-3xl flex items-center justify-center p-6 md:p-12 animate-in fade-in duration-300">
<div className="absolute inset-0 bg-[url('https://www.transparenttextures.com/patterns/dark-matter.png')] opacity-10 pointer-events-none"></div>
<div className="relative w-full max-w-5xl h-full flex flex-col bg-[#0a0805] border border-[#2D241A] rounded-2xl shadow-[0_0_100px_rgba(0,0,0,0.8)] overflow-hidden animate-in zoom-in-95 duration-500">
<div className="p-6 border-b border-[#2D241A] flex items-center justify-between bg-gradient-to-r from-[#0a0805] to-[#120E0A]">
<div className="flex items-center gap-4">
<div className="p-2 bg-[#DAB062]/10 rounded-lg">
{maximizedField === 'refined' ? <Sparkles className="text-[#DAB062]" size={20} /> : <Settings className="text-[#A89880]" size={20} />}
</div>
<div>
<h3 className="font-cinzel text-xl text-[#E2D1B3] tracking-wider">
{maximizedField === 'refined' ? 'Expanded Narrative Scroll' : 'Internal Technical Formula'}
</h3>
<p className="text-[10px] uppercase tracking-[0.2em] text-[#665D4F] mt-1">
{maximizedField === 'refined' ? 'Detailed character chronicle' : 'Direct alchemical markers'}
</p>
</div>
</div>
<button onClick={() => setMaximizedField(null)} className="p-2 text-[#665D4F] hover:text-[#E2D1B3] transition-colors"><X size={24} /></button>
</div>
<div className="flex-1 p-8 relative">
<textarea
readOnly={maximizedField === 'technical'}
value={maximizedField === 'refined' ? refinedOutput : promptOutput}
onChange={e => maximizedField === 'refined' && setRefinedOutput(e.target.value)}
className="w-full h-full bg-transparent text-lg md:text-xl text-[#E2D1B3] leading-relaxed resize-none font-serif tracking-wide focus:outline-none custom-scrollbar"
placeholder={maximizedField === 'refined' ? "Write your epic narrative here..." : ""}
/>
<div className="absolute bottom-8 right-8 flex gap-4">
<button
onClick={() => handleCopy(maximizedField === 'refined' ? (refinedOutput || promptOutput) : promptOutput)}
className="flex items-center gap-3 px-6 py-3 bg-[#0a0805] border border-[#2D241A] rounded-xl text-[#A89880] hover:text-[#DAB062] hover:border-[#DAB062]/50 transition-all font-cinzel text-xs uppercase tracking-widest"
>
<Copy size={16} /> Copy Record
</button>
<button
onClick={() => setMaximizedField(null)}
className="flex items-center gap-3 px-8 py-3 bg-[#DAB062] text-[#0a0805] rounded-xl font-cinzel font-bold text-xs uppercase tracking-widest hover:shadow-[0_0_20px_rgba(218,176,98,0.4)] transition-all"
>
Close Scroll
</button>
</div>
</div>
</div>
</div>
)}
</div>
);
}