"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(null); const [activeTab, setActiveTab] = useState('identity'); const [showLightbox, setShowLightbox] = useState(false); // Feedback System State const [toasts, setToasts] = useState([]); const [logs, setLogs] = useState([]); 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([]); const [randomization, setRandomization] = useState([]); const [extraInfo, setExtraInfo] = useState(Array(5).fill("")); const [promptOutput, setPromptOutput] = useState(""); const [refinedOutput, setRefinedOutput] = useState(""); const [imageSrc, setImageSrc] = useState(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(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(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) => { 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 (
); }; if (!config) return
Igniting forge...
; return (
{/* Hidden file input for loading JSON */} {/* 1. Left Sidebar Navigation */}

Chronicle

Portrait Studio

v1.0.0

{/* 2. Main Middle Area (Form Aspects) */}
{/* App Logo */}
Chronicle Portrait Studio Logo
{/* Section Headers */}

{tabs.find(t => t.id === activeTab)?.label}

{activeTab === 'engines' && (

Text Intelligence

{refinementBackend === "Ollama (Local)" && (
)} {refinementBackend === "Hugging Face (Cloud)" && (
{manualTextEntry ? ( setHfTextModel(e.target.value)} className="input-alchemist w-full" placeholder="Enter model ID..." /> ) : ( )}
)}

Visual Synthesis

{imageBackend === "Hugging Face (Cloud)" && (
{manualImageEntry ? ( setHfImageModel(e.target.value)} className="input-alchemist w-full" placeholder="Enter model ID..." /> ) : ( )}
)}

Hugging Face Identity

{hfToken ? 'Authenticated' : (config?.hf_active ? 'Developer Mode' : 'Disconnected')}

{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.") }

{hfToken ? ( ) : ( )}
)} {activeTab === 'identity' && (
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..."/>
{renderFeatureUI('identity', 'race', 'Race')} {renderFeatureUI('identity', 'class', 'Class')} {renderFeatureUI('identity', 'gender', 'Gender')} {renderFeatureUI('identity', 'age', 'Age')}

Expression & Stance

{renderFeatureUI('expression_pose', 'expression', 'Expression')} {renderFeatureUI('expression_pose', 'pose', 'Pose')}
)} {activeTab === 'appearance' && (
{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')}
)} {activeTab === 'equipment' && (
{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')}
)} {activeTab === 'environment' && (
{renderFeatureUI('environment', 'background', 'Backdrop')} {renderFeatureUI('environment', 'lighting', 'Illumination')} {renderFeatureUI('environment', 'atmosphere', 'Atmosphere')}
)} {activeTab === 'style' && (
{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')}
)}
{/* 3. Right Crucible Area (Output & Synthesis) */}
{/* Output Image Canvas */}
{imageSrc ? (
setShowLightbox(true)}>
Portrait
) : statusMsg && (statusMsg.includes("Error") || statusMsg.includes("Failed")) ? (

Synthesis Chamber Fault

"{statusMsg}"

) : (

Visual Synthesis Pending

)}
{/* Action Panel */}
{statusMsg && ( {statusMsg.toLowerCase().includes('error') || statusMsg.toLowerCase().includes('failed') ? : } {statusMsg} )}
{/* FULL SCREEN LIGHTBOX MODAL */} {showLightbox && imageSrc && (
setShowLightbox(false)}>
e.stopPropagation()}>
Portrait High Resolution
)} {/* SYSTEM LOG PANEL */} {showLogs && (
Alchemical Synthesis Logs
{logs.length === 0 ? (
The records are empty. Initiate an operation.
) : ( logs.map((log, i) => (
[{log.timestamp}] {log.message}
)) )}
)} {/* TOAST SYSTEM */}
{toasts.map(toast => (
{toast.type === 'error' ? : }
{toast.type}
{toast.message}
))}
{/* MAXIMIZED PROMPT MODAL */} {maximizedField && (
{maximizedField === 'refined' ? : }

{maximizedField === 'refined' ? 'Expanded Narrative Scroll' : 'Internal Technical Formula'}

{maximizedField === 'refined' ? 'Detailed character chronicle' : 'Direct alchemical markers'}