diff --git a/app/frontend/src/App.css b/app/frontend/src/App.css index 26b3263..43efee7 100644 --- a/app/frontend/src/App.css +++ b/app/frontend/src/App.css @@ -2271,27 +2271,461 @@ html[data-theme="dark"] .m3-segment-item:hover:not(.active):not(.disabled) { overflow-y: auto; } -/* DeepThink button animations and interactions */ -.chat-composer-deepthink-btn:hover { - background: rgba(99, 102, 241, 0.08) !important; - color: var(--md-sys-color-primary) !important; - border-color: color-mix(in srgb, var(--md-sys-color-primary) 50%, transparent) !important; +/* ============================================================ + PROFESSIONAL SETTINGS UI - Collapsible Cards, Tabs, Hardware Badge + ============================================================ */ + +/* --- Section Tabs (Image | Text) --- */ +.settings-tabs-container { + display: flex; + justify-content: center; + margin-bottom: 24px; } -.chat-composer-deepthink-btn.active:hover { - background: rgba(99, 102, 241, 0.22) !important; +.settings-tabs { + display: flex; + gap: 4px; + padding: 4px; + background: var(--md-sys-color-surface-variant); + border-radius: 14px; + border: 1px solid var(--border-color); + width: fit-content; } -@keyframes spin-slow { +.settings-tab { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 24px; + border-radius: 12px; + border: none; + font-weight: 600; + font-size: 0.9rem; + cursor: pointer; + transition: all 0.25s var(--md-transition-easing); + color: var(--md-sys-color-on-surface-variant); + background: transparent; + position: relative; + overflow: hidden; +} + +.settings-tab:hover { + background: var(--md-sys-color-surface); + color: var(--md-sys-color-on-surface); +} + +.settings-tab.active { + background: var(--md-sys-color-primary); + color: var(--md-sys-color-on-primary); + box-shadow: 0 4px 16px rgba(79, 70, 229, 0.25); +} + +.settings-tab .tab-icon { + transition: transform 0.3s ease; +} + +.settings-tab.active .tab-icon { + transform: scale(1.1); +} + +/* --- Hardware Tier Badge --- */ +.hardware-tier-badge { + border-radius: 20px; + padding: 24px; + margin-bottom: 28px; + position: relative; + overflow: hidden; + border: 1px solid var(--border-color); + backdrop-filter: var(--glass-blur); + -webkit-backdrop-filter: var(--glass-blur); + background: var(--md-sys-color-surface); + box-shadow: var(--glass-shadow); +} + +.hardware-tier-badge::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + border-radius: 20px 20px 0 0; +} + +.hardware-tier-badge.tier-high::before { + background: linear-gradient(90deg, #22c55e, #10b981); +} + +.hardware-tier-badge.tier-mid::before { + background: linear-gradient(90deg, #3b82f6, #0ea5e9); +} + +.hardware-tier-badge.tier-low::before { + background: linear-gradient(90deg, #f59e0b, #fbbf24); +} + +.hardware-tier-header { + display: flex; + align-items: center; + gap: 16px; + margin-bottom: 12px; +} + +.hardware-tier-icon { + width: 48px; + height: 48px; + border-radius: 14px; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.5rem; + flex-shrink: 0; +} + +.tier-high .hardware-tier-icon { + background: rgba(34, 197, 94, 0.12); +} + +.tier-mid .hardware-tier-icon { + background: rgba(59, 130, 246, 0.12); +} + +.tier-low .hardware-tier-icon { + background: rgba(245, 158, 11, 0.12); +} + +.hardware-tier-info { + flex: 1; +} + +.hardware-tier-name { + font-size: 1.15rem; + font-weight: 700; + color: var(--md-sys-color-on-surface); + margin-bottom: 4px; +} + +.hardware-tier-specs { + font-size: 0.8rem; + color: var(--md-sys-color-on-surface-variant); + line-height: 1.4; +} + +.hardware-tier-divider { + height: 1px; + background: var(--border-color); + margin: 14px 0; +} + +.hardware-tier-chips { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.hardware-tier-chip { + font-size: 0.75rem; + padding: 5px 14px; + border-radius: 20px; + background: var(--md-sys-color-surface-variant); + font-weight: 600; + color: var(--md-sys-color-on-surface-variant); + border: 1px solid var(--border-color); + transition: all 0.2s ease; +} + +.hardware-tier-chip:hover { + background: var(--md-sys-color-primary-container); + color: var(--md-sys-color-on-primary-container); + border-color: var(--md-sys-color-primary); +} + +/* --- Collapsible Card --- */ +.collapsible-card { + border: 1px solid var(--md-sys-color-card-border); + border-radius: 16px; + overflow: hidden; + margin-bottom: 16px; + background: var(--md-sys-color-surface); + backdrop-filter: var(--glass-blur); + -webkit-backdrop-filter: var(--glass-blur); + box-shadow: var(--glass-shadow); + transition: box-shadow 0.3s ease, border-color 0.3s ease; +} + +.collapsible-card:hover { + box-shadow: 0 12px 40px rgba(15, 23, 42, 0.08); + border-color: rgba(15, 23, 42, 0.12); +} + +html[data-theme="dark"] .collapsible-card:hover { + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25); + border-color: rgba(255, 255, 255, 0.08); +} + +.collapsible-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 18px 22px; + cursor: pointer; + user-select: none; + transition: background 0.25s ease; + border: none; + width: 100%; + background: transparent; + color: inherit; + font-family: inherit; + text-align: left; +} + +.collapsible-header:hover { + background: var(--md-sys-color-surface-variant); +} + +.collapsible-header-left { + display: flex; + align-items: center; + gap: 14px; +} + +.collapsible-header-icon { + width: 36px; + height: 36px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + background: var(--md-sys-color-primary-container); + color: var(--md-sys-color-primary); + flex-shrink: 0; +} + +.collapsible-header-title { + font-size: 0.95rem; + font-weight: 600; + color: var(--md-sys-color-on-surface); +} + +.collapsible-header-subtitle { + font-size: 0.75rem; + color: var(--md-sys-color-outline); + margin-top: 2px; +} + +.collapsible-chevron { + transition: transform 0.35s var(--md-transition-easing); + color: var(--md-sys-color-outline); + flex-shrink: 0; +} + +.collapsible-chevron.expanded { + transform: rotate(180deg); + color: var(--md-sys-color-primary); +} + +.collapsible-content { + padding: 0 22px 22px; + animation: fadeInSlide 0.3s var(--md-transition-easing); +} + +@keyframes fadeInSlide { from { - transform: rotate(0deg); + opacity: 0; + transform: translateY(-8px); } to { - transform: rotate(360deg); + opacity: 1; + transform: translateY(0); } } -.rotate-anim { - animation: spin-slow 8s linear infinite; +/* --- Settings Section Header --- */ +.settings-section-header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 20px; + padding-bottom: 12px; + border-bottom: 1px solid var(--border-color); +} + +.settings-section-title { + font-size: 1.1rem; + font-weight: 700; + color: var(--md-sys-color-on-surface); +} + +.settings-section-count { + font-size: 0.75rem; + padding: 3px 10px; + border-radius: 999px; + background: var(--md-sys-color-primary-container); + color: var(--md-sys-color-on-primary-container); + font-weight: 600; +} + +/* --- Settings Grid Layout --- */ +.settings-grid { + display: grid; + grid-template-columns: 1fr; + gap: 0; + max-width: 800px; + margin: 0 auto; +} + +@media (min-width: 900px) { + .settings-grid { + grid-template-columns: 1fr 1fr; + gap: 20px; + } +} + +/* --- Premium Toggle Switch --- */ +.premium-toggle { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + border-radius: 12px; + background: var(--md-sys-color-surface-variant); + border: 1px solid var(--border-color); + transition: all 0.2s ease; + cursor: pointer; +} + +.premium-toggle:hover { + border-color: var(--md-sys-color-primary); + background: var(--md-sys-color-primary-container); +} + +.premium-toggle-checkbox { + width: 22px; + height: 22px; + border-radius: 6px; + border: 2px solid var(--md-sys-color-outline); + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + flex-shrink: 0; + background: transparent; + cursor: pointer; +} + +.premium-toggle-checkbox.checked { + background: var(--md-sys-color-primary); + border-color: var(--md-sys-color-primary); + color: var(--md-sys-color-on-primary); +} + +.premium-toggle-label { + font-size: 0.85rem; + font-weight: 600; + color: var(--md-sys-color-on-surface); +} + +.premium-toggle-desc { + font-size: 0.75rem; + color: var(--md-sys-color-outline); + margin-top: 2px; +} + +/* --- Settings Value Badge --- */ +.settings-value-badge { + font-size: 0.75rem; + padding: 3px 10px; + border-radius: 8px; + background: var(--md-sys-color-primary-container); + color: var(--md-sys-color-on-primary-container); + font-weight: 700; + font-variant-numeric: tabular-nums; +} + +/* --- Empty State for Settings --- */ +.settings-empty-state { + text-align: center; + padding: 60px 20px; + color: var(--md-sys-color-outline); +} + +.settings-empty-state-icon { + font-size: 3rem; + margin-bottom: 16px; + opacity: 0.5; +} + +.settings-empty-state-title { + font-size: 1.1rem; + font-weight: 600; + color: var(--md-sys-color-on-surface); + margin-bottom: 8px; +} + +.settings-empty-state-desc { + font-size: 0.85rem; + max-width: 400px; + margin: 0 auto; + line-height: 1.5; +} + +/* --- Responsive Adjustments --- */ +@media (max-width: 600px) { + .hardware-tier-badge { + padding: 16px; + border-radius: 16px; + } + + .hardware-tier-header { + gap: 12px; + } + + .hardware-tier-icon { + width: 40px; + height: 40px; + font-size: 1.2rem; + } + + .hardware-tier-name { + font-size: 1rem; + } + + .collapsible-header { + padding: 14px 16px; + } + + .collapsible-content { + padding: 0 16px 16px; + } + + .settings-tab { + padding: 8px 16px; + font-size: 0.85rem; + } +} + +/* --- Settings Page Header --- */ +.settings-page-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 24px; + padding-bottom: 16px; + border-bottom: 1px solid var(--border-color); +} + +.settings-page-title { + font-size: 1.4rem; + font-weight: 700; + color: var(--md-sys-color-on-surface); + display: flex; + align-items: center; + gap: 12px; +} + +.settings-page-subtitle { + font-size: 0.85rem; + color: var(--md-sys-color-outline); + margin-top: 4px; } diff --git a/app/frontend/src/components/Settings.jsx b/app/frontend/src/components/Settings.jsx index b918fda..2976d1a 100644 --- a/app/frontend/src/components/Settings.jsx +++ b/app/frontend/src/components/Settings.jsx @@ -1,5 +1,9 @@ -import React, { memo, useEffect, useState } from "react"; -import { Crop, Sliders, Cpu, Info, MessageSquare, SlidersHorizontal, Zap } from "lucide-react"; +import React, { memo, useEffect, useState, useCallback } from "react"; +import { + Crop, Sliders, Cpu, Info, MessageSquare, SlidersHorizontal, Zap, + ChevronDown, Image, Type, Settings2, Gauge, Brain, Sparkles, + Monitor, HardDrive, MemoryStick, Thermometer, Hash, Layers +} from "lucide-react"; import { stopServer, formatBytes, getLlmStatus } from "../services/api"; const ASPECT_RATIOS = [ @@ -18,6 +22,146 @@ const isSD15OrCustomModel = (modelName) => { return true; }; +// ─── Collapsible Card Component ─── +function CollapsibleCard({ icon: Icon, title, subtitle, children, defaultExpanded = true, id, onToggle }) { + const [isExpanded, setIsExpanded] = useState(() => { + const saved = localStorage.getItem(`settings_card_${id}`); + return saved !== null ? saved === "true" : defaultExpanded; + }); + + const toggle = useCallback(() => { + const newState = !isExpanded; + setIsExpanded(newState); + localStorage.setItem(`settings_card_${id}`, String(newState)); + onToggle?.(id, newState); + }, [isExpanded, id, onToggle]); + + return ( +
+ + {isExpanded && ( +
+ {children} +
+ )} +
+ ); +} + +// ─── Hardware Tier Badge Component ─── +function HardwareTierBadge({ specs }) { + if (!specs?.tier) return null; + + const tierConfig = { + high: { icon: "🚀", label: "High-End PC", color: "tier-high" }, + mid: { icon: "⚖️", label: "Balanced PC", color: "tier-mid" }, + low: { icon: "🥔", label: "Potato PC", color: "tier-low" }, + }; + + const tier = tierConfig[specs.tier] || tierConfig.low; + const rec = specs.recommended_text_settings; + + return ( +
+
+
{tier.icon}
+
+
{tier.label}
+
+ {specs.cpu_name} • {specs.cpu_cores_physical} cores • {specs.ram_total_gb}GB RAM + {specs.gpu_name && specs.gpu_name !== "Loading..." && ` • ${specs.gpu_name}`} + {specs.gpu_vram_gb > 0 && ` • ${specs.gpu_vram_gb}GB VRAM`} +
+
+
+ {rec && ( + <> +
+
+ Ctx: {rec.contextSize} + Threads: {rec.threads} + KV: {rec.cacheTypeK} + Batch: {rec.batchSize} + Profile: {rec.performanceProfile} +
+ + )} +
+ ); +} + +// ─── Section Tabs Component ─── +function SectionTabs({ activeTab, onTabChange }) { + const tabs = [ + { id: "image", label: "Image Generation", icon: Image }, + { id: "text", label: "Text Generation", icon: Type }, + ]; + + return ( +
+
+ {tabs.map((tab) => { + const Icon = tab.icon; + const isActive = activeTab === tab.id; + return ( + + ); + })} +
+
+ ); +} + +// ─── Premium Toggle Component ─── +function PremiumToggle({ checked, onChange, label, description, icon: Icon }) { + return ( + + ); +} + +// ─── Main Settings Component ─── function Settings({ constraints, setConstraints, @@ -39,24 +183,15 @@ function Settings({ cleanupSafeItems, diagnosticsCopied, }) { - const isSD15OrCustom = activeModel ? isSD15OrCustomModel(activeModel) : false; - const isOpenVinoNpu = constraints.backendType === "openvino-npu"; - const forceStandardMode = isSD15OrCustom && !isOpenVinoNpu; - const availableBackends = backendOptions?.options?.length - ? backendOptions.options - : [{ id: "cpu", label: "CPU", available: true }]; - const isMac = availableBackends.some(b => b.id === "metal" || b.id === "apple-npu") || - (specs?.os_name && (specs.os_name.toLowerCase().includes("darwin") || specs.os_name.toLowerCase().includes("mac"))); - - const readinessIssues = [ - ...(health?.stale ? ["Restart Local AI Image Generator so the local server loads the latest API."] : []), - ...(health?.issues || []), - ]; - const cleanupBytes = (cleanupItems || []).reduce((sum, item) => sum + Number(item.sizeBytes || 0), 0); - const showReadinessPanel = Boolean(health && (readinessIssues.length > 0 || (cleanupItems && cleanupItems.length > 0))); - + const [activeTab, setActiveTab] = useState(() => { + return localStorage.getItem("settings_active_tab") || "image"; + }); const [llmStatus, setLlmStatus] = useState({ ready: false, settings: {} }); + useEffect(() => { + localStorage.setItem("settings_active_tab", activeTab); + }, [activeTab]); + useEffect(() => { let cancelled = false; const fetchLlmStatus = async () => { @@ -73,13 +208,12 @@ function Settings({ }; }, []); + const isSD15OrCustom = activeModel ? isSD15OrCustomModel(activeModel) : false; + const isOpenVinoNpu = constraints.backendType === "openvino-npu"; const supportsThinking = Boolean(llmStatus.ready && llmStatus.settings?.supportsThinking); - - useEffect(() => { - if (isOpenVinoNpu && constraints.steps > 8) { - setConstraints((prev) => ({ ...prev, steps: 8, npuSteps: 8 })); - } - }, [constraints.steps, isOpenVinoNpu, setConstraints]); + const availableBackends = backendOptions?.options?.length + ? backendOptions.options + : [{ id: "cpu", label: "CPU", available: true }]; const updateConstraint = (key, value) => { setConstraints((prev) => ({ @@ -178,910 +312,563 @@ function Settings({ })); }; - return ( -
-
-

Settings & Parameters

-

- Configure size, quality, and performance controls for local image and text models. -

-
- - {showReadinessPanel && ( -
0 ? "readiness-card-warning" : ""}`} style={{ marginBottom: "20px" }}> -
-
-

- {health?.stale ? "Restart Required" : readinessIssues.length > 0 ? "System Readiness" : "Safe Cleanup Available"} -

-

- {health?.stale - ? `Running server build: ${health.build || "unknown"}` - : readinessIssues.length > 0 - ? "Local AI Image Generator found setup items that may need attention." - : `${(cleanupItems || []).length} temporary item${(cleanupItems || []).length === 1 ? "" : "s"} can be cleaned (${formatBytes(cleanupBytes)}).`} -

+ // ─── Image Settings Tab Content ─── + const ImageSettingsContent = () => ( + <> + +
+ {/* Size Mode Toggle */} +
+
+ Resolution Mode + + {constraints.width >= 1024 ? "SDXL" : "SD 1.5"} +
-
- - - {(cleanupItems || []).length > 0 && ( - - )} + ))} +
+ {isSD15OrCustom && ( + + Custom models use 512px base resolution for compatibility. + + )} +
+ + {/* Aspect Ratio Buttons */} +
+
+ Aspect Ratio +
+
+ {ASPECT_RATIOS.map((ratio) => { + const isSDXL = constraints.width >= 1024 && !isSD15OrCustom; + const rw = isSDXL ? ratio.sdxl_width : ratio.width; + const rh = isSDXL ? ratio.sdxl_height : ratio.height; + const isActive = Math.abs(constraints.width - rw) < 10 && Math.abs(constraints.height - rh) < 10; + return ( + + ); + })}
- {readinessIssues.length > 0 && ( -
- {readinessIssues.slice(0, 4).map((issue) => ( - {issue} + + {/* Custom Dimensions */} +
+
+ + updateConstraint("width", Math.round(parseInt(e.target.value) / 64) * 64)} + min="64" + max="2048" + step="64" + /> +
+
+ + updateConstraint("height", Math.round(parseInt(e.target.value) / 64) * 64)} + min="64" + max="2048" + step="64" + /> +
+
+
+ + + +
+ {/* Steps Slider */} +
+
+ Detail Steps + {constraints.steps} +
+ updateConstraint("steps", parseInt(e.target.value))} + min="1" + max={isOpenVinoNpu ? "8" : "60"} + /> + + {isOpenVinoNpu + ? "LCM OpenVINO uses 1-8 fast inference steps." + : "More steps = sharper details, but takes longer."} + +
+ + {/* Seed */} +
+ +
+ updateConstraint("seed", parseInt(e.target.value) || -1)} + placeholder="-1 for random..." + style={{ flex: 1 }} + /> + +
+
+
+
+ + +
+ updateConstraint("vaeTiling", v)} + label="VAE Tiling" + description="Process image in tiles to save VRAM" + icon={Layers} + /> + updateConstraint("vaeOnCpu", v)} + label="VAE on CPU" + description="Run decoder on CPU if GPU runs out of memory" + icon={Cpu} + /> + updateConstraint("useFlashAttn", v)} + label="Flash Attention" + description="Faster attention with less memory" + icon={Zap} + /> +
+
+ + +
+
+
+ Accelerator +
+
+ {availableBackends.map((b) => ( + ))}
- )} - {(cleanupItems || []).length > 0 && ( -
- {cleanupItems.slice(0, 3).map((item) => ( - - {item.name} · {item.size} · {item.reason} - +
+
+ + + ); + + // ─── Text Settings Tab Content ─── + const TextSettingsContent = () => ( + <> + +
+
+ +