feat: redesign Settings UI with collapsible sections, no tabs
This commit is contained in:
1 parent
0388220324
commit
fc43aab80f
2 files changed
+147
-121
No files matched your search
+58
-22
@@ -2544,43 +2544,70 @@ html[data-theme="dark"] .collapsible-card:hover {
|
||||
.settings-section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
gap: 14px;
|
||||
margin: 32px 0 20px 0;
|
||||
padding: 16px 20px;
|
||||
border-radius: 14px;
|
||||
background: var(--md-sys-color-surface);
|
||||
border: 1px solid var(--md-sys-color-card-border);
|
||||
border-left: 4px solid;
|
||||
backdrop-filter: var(--glass-blur);
|
||||
-webkit-backdrop-filter: var(--glass-blur);
|
||||
box-shadow: var(--glass-shadow);
|
||||
}
|
||||
|
||||
.settings-section-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.settings-section-title {
|
||||
font-size: 1.1rem;
|
||||
font-size: 1.15rem;
|
||||
font-weight: 700;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.settings-section-count {
|
||||
font-size: 0.75rem;
|
||||
padding: 3px 10px;
|
||||
padding: 4px 12px;
|
||||
border-radius: 999px;
|
||||
background: var(--md-sys-color-primary-container);
|
||||
color: var(--md-sys-color-on-primary-container);
|
||||
background: var(--md-sys-color-surface-variant);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
font-weight: 600;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
/* --- Settings Grid Layout --- */
|
||||
/* --- Collapsible Header Badge --- */
|
||||
.collapsible-header-badge {
|
||||
font-size: 0.7rem;
|
||||
padding: 2px 10px;
|
||||
border-radius: 999px;
|
||||
margin-left: 10px;
|
||||
font-weight: 700;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.collapsible-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* --- Settings Grid (Single Column) --- */
|
||||
.settings-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
max-width: 800px;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@media (min-width: 900px) {
|
||||
.settings-grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Premium Toggle Switch --- */
|
||||
.premium-toggle {
|
||||
display: flex;
|
||||
@@ -2698,9 +2725,18 @@ html[data-theme="dark"] .collapsible-card:hover {
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
|
||||
.settings-tab {
|
||||
padding: 8px 16px;
|
||||
font-size: 0.85rem;
|
||||
.settings-section-header {
|
||||
padding: 12px 16px;
|
||||
margin: 24px 0 16px 0;
|
||||
}
|
||||
|
||||
.settings-section-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.settings-section-title {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@ 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
|
||||
Monitor, HardDrive, MemoryStick, Thermometer, Hash, Layers,
|
||||
ChevronRight, Box, Wand2, Lightbulb, RotateCcw, Check
|
||||
} from "lucide-react";
|
||||
import { stopServer, formatBytes, getLlmStatus } from "../services/api";
|
||||
|
||||
@@ -23,7 +24,7 @@ const isSD15OrCustomModel = (modelName) => {
|
||||
};
|
||||
|
||||
// ─── Collapsible Card Component ───
|
||||
function CollapsibleCard({ icon: Icon, title, subtitle, children, defaultExpanded = true, id, onToggle }) {
|
||||
function CollapsibleCard({ icon: Icon, title, subtitle, children, defaultExpanded = false, id, badge, badgeColor }) {
|
||||
const [isExpanded, setIsExpanded] = useState(() => {
|
||||
const saved = localStorage.getItem(`settings_card_${id}`);
|
||||
return saved !== null ? saved === "true" : defaultExpanded;
|
||||
@@ -33,8 +34,7 @@ function CollapsibleCard({ icon: Icon, title, subtitle, children, defaultExpande
|
||||
const newState = !isExpanded;
|
||||
setIsExpanded(newState);
|
||||
localStorage.setItem(`settings_card_${id}`, String(newState));
|
||||
onToggle?.(id, newState);
|
||||
}, [isExpanded, id, onToggle]);
|
||||
}, [isExpanded, id]);
|
||||
|
||||
return (
|
||||
<div className="collapsible-card">
|
||||
@@ -49,14 +49,29 @@ function CollapsibleCard({ icon: Icon, title, subtitle, children, defaultExpande
|
||||
<Icon size={18} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="collapsible-header-title">{title}</div>
|
||||
<div className="collapsible-header-title">
|
||||
{title}
|
||||
{badge && (
|
||||
<span
|
||||
className="collapsible-header-badge"
|
||||
style={{
|
||||
background: badgeColor || "var(--md-sys-color-primary-container)",
|
||||
color: badgeColor ? "#fff" : "var(--md-sys-color-on-primary-container)"
|
||||
}}
|
||||
>
|
||||
{badge}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{subtitle && <div className="collapsible-header-subtitle">{subtitle}</div>}
|
||||
</div>
|
||||
</div>
|
||||
<ChevronDown
|
||||
size={20}
|
||||
className={`collapsible-chevron ${isExpanded ? "expanded" : ""}`}
|
||||
/>
|
||||
<div className="collapsible-header-right">
|
||||
<ChevronDown
|
||||
size={20}
|
||||
className={`collapsible-chevron ${isExpanded ? "expanded" : ""}`}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
{isExpanded && (
|
||||
<div className="collapsible-content">
|
||||
@@ -72,9 +87,9 @@ 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" },
|
||||
high: { icon: "🚀", label: "High-End PC", color: "tier-high", accent: "#22c55e" },
|
||||
mid: { icon: "⚖️", label: "Balanced PC", color: "tier-mid", accent: "#3b82f6" },
|
||||
low: { icon: "🥔", label: "Potato PC", color: "tier-low", accent: "#f59e0b" },
|
||||
};
|
||||
|
||||
const tier = tierConfig[specs.tier] || tierConfig.low;
|
||||
@@ -109,40 +124,8 @@ function HardwareTierBadge({ specs }) {
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Section Tabs Component ───
|
||||
function SectionTabs({ activeTab, onTabChange }) {
|
||||
const tabs = [
|
||||
{ id: "image", label: "Image Generation", icon: Image },
|
||||
{ id: "text", label: "Text Generation", icon: Type },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="settings-tabs-container">
|
||||
<div className="settings-tabs" role="tablist">
|
||||
{tabs.map((tab) => {
|
||||
const Icon = tab.icon;
|
||||
const isActive = activeTab === tab.id;
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
className={`settings-tab ${isActive ? "active" : ""}`}
|
||||
onClick={() => onTabChange(tab.id)}
|
||||
role="tab"
|
||||
aria-selected={isActive}
|
||||
type="button"
|
||||
>
|
||||
<Icon size={18} className="tab-icon" />
|
||||
{tab.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Premium Toggle Component ───
|
||||
function PremiumToggle({ checked, onChange, label, description, icon: Icon }) {
|
||||
function PremiumToggle({ checked, onChange, label, description }) {
|
||||
return (
|
||||
<label className="premium-toggle" style={{ cursor: "pointer" }}>
|
||||
<div
|
||||
@@ -151,7 +134,7 @@ function PremiumToggle({ checked, onChange, label, description, icon: Icon }) {
|
||||
role="checkbox"
|
||||
aria-checked={checked}
|
||||
>
|
||||
{checked && <Sparkles size={14} />}
|
||||
{checked && <Check size={14} />}
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div className="premium-toggle-label">{label}</div>
|
||||
@@ -161,6 +144,23 @@ function PremiumToggle({ checked, onChange, label, description, icon: Icon }) {
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Section Header Component ───
|
||||
function SectionHeader({ icon: Icon, title, count, color }) {
|
||||
return (
|
||||
<div className="settings-section-header" style={{ borderLeftColor: color }}>
|
||||
<div className="settings-section-icon" style={{ background: color + "15", color }}>
|
||||
<Icon size={22} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="settings-section-title">{title}</div>
|
||||
</div>
|
||||
{count && (
|
||||
<span className="settings-section-count">{count} settings</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Main Settings Component ───
|
||||
function Settings({
|
||||
constraints,
|
||||
@@ -183,15 +183,8 @@ function Settings({
|
||||
cleanupSafeItems,
|
||||
diagnosticsCopied,
|
||||
}) {
|
||||
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 () => {
|
||||
@@ -312,21 +305,27 @@ function Settings({
|
||||
}));
|
||||
};
|
||||
|
||||
// ─── Image Settings Tab Content ───
|
||||
const ImageSettingsContent = () => (
|
||||
// ─── Image Settings ───
|
||||
const ImageSettings = () => (
|
||||
<>
|
||||
<SectionHeader
|
||||
icon={Image}
|
||||
title="Image Generation"
|
||||
count={4}
|
||||
color="#3b82f6"
|
||||
/>
|
||||
|
||||
<CollapsibleCard
|
||||
id="image_size"
|
||||
icon={Crop}
|
||||
title="Image Size & Shape"
|
||||
subtitle={`${constraints.width} × ${constraints.height} pixels`}
|
||||
defaultExpanded={true}
|
||||
title="Size & Shape"
|
||||
subtitle={`${constraints.width} × ${constraints.height} px`}
|
||||
defaultExpanded={false}
|
||||
>
|
||||
<div className="m3-field-group">
|
||||
{/* Size Mode Toggle */}
|
||||
<div className="m3-slider-group">
|
||||
<div className="m3-slider-header">
|
||||
<span className="m3-slider-label">Resolution Mode</span>
|
||||
<span className="m3-slider-label">Resolution</span>
|
||||
<span className="settings-value-badge">
|
||||
{constraints.width >= 1024 ? "SDXL" : "SD 1.5"}
|
||||
</span>
|
||||
@@ -346,18 +345,12 @@ function Settings({
|
||||
}}
|
||||
disabled={isSD15OrCustom && mode === "sdxl"}
|
||||
>
|
||||
{mode === "sd15" ? "512px Base" : "1024px HD"}
|
||||
{mode === "sd15" ? "512px" : "1024px"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{isSD15OrCustom && (
|
||||
<span style={{ fontSize: "0.75rem", color: "var(--md-sys-color-outline)" }}>
|
||||
Custom models use 512px base resolution for compatibility.
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Aspect Ratio Buttons */}
|
||||
<div className="m3-slider-group">
|
||||
<div className="m3-slider-header">
|
||||
<span className="m3-slider-label">Aspect Ratio</span>
|
||||
@@ -386,7 +379,6 @@ function Settings({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Custom Dimensions */}
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "12px" }}>
|
||||
<div className="m3-text-field">
|
||||
<label className="m3-text-field-label">Width</label>
|
||||
@@ -420,11 +412,10 @@ function Settings({
|
||||
id="image_quality"
|
||||
icon={Sliders}
|
||||
title="Quality & Speed"
|
||||
subtitle={`${constraints.steps} steps • ${constraints.sampler}`}
|
||||
defaultExpanded={true}
|
||||
subtitle={`${constraints.steps} steps`}
|
||||
defaultExpanded={false}
|
||||
>
|
||||
<div className="m3-field-group">
|
||||
{/* Steps Slider */}
|
||||
<div className="m3-slider-group">
|
||||
<div className="m3-slider-header">
|
||||
<span className="m3-slider-label">Detail Steps</span>
|
||||
@@ -440,12 +431,11 @@ function Settings({
|
||||
/>
|
||||
<span style={{ fontSize: "0.75rem", color: "var(--md-sys-color-outline)" }}>
|
||||
{isOpenVinoNpu
|
||||
? "LCM OpenVINO uses 1-8 fast inference steps."
|
||||
: "More steps = sharper details, but takes longer."}
|
||||
? "LCM OpenVINO: 1-8 fast steps"
|
||||
: "More steps = sharper details, longer time"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Seed */}
|
||||
<div className="m3-text-field">
|
||||
<label className="m3-text-field-label">Random Seed</label>
|
||||
<div style={{ display: "flex", gap: "8px" }}>
|
||||
@@ -454,7 +444,7 @@ function Settings({
|
||||
className="m3-input"
|
||||
value={constraints.seed}
|
||||
onChange={(e) => updateConstraint("seed", parseInt(e.target.value) || -1)}
|
||||
placeholder="-1 for random..."
|
||||
placeholder="-1 for random"
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<button
|
||||
@@ -472,7 +462,7 @@ function Settings({
|
||||
id="image_memory"
|
||||
icon={SlidersHorizontal}
|
||||
title="Memory Optimizations"
|
||||
subtitle="Reduce VRAM usage"
|
||||
subtitle="VRAM saving options"
|
||||
defaultExpanded={false}
|
||||
>
|
||||
<div className="m3-field-group">
|
||||
@@ -481,21 +471,18 @@ function Settings({
|
||||
onChange={(v) => updateConstraint("vaeTiling", v)}
|
||||
label="VAE Tiling"
|
||||
description="Process image in tiles to save VRAM"
|
||||
icon={Layers}
|
||||
/>
|
||||
<PremiumToggle
|
||||
checked={constraints.vaeOnCpu}
|
||||
onChange={(v) => updateConstraint("vaeOnCpu", v)}
|
||||
label="VAE on CPU"
|
||||
description="Run decoder on CPU if GPU runs out of memory"
|
||||
icon={Cpu}
|
||||
description="Run decoder on CPU if GPU OOM"
|
||||
/>
|
||||
<PremiumToggle
|
||||
checked={constraints.useFlashAttn}
|
||||
onChange={(v) => updateConstraint("useFlashAttn", v)}
|
||||
label="Flash Attention"
|
||||
description="Faster attention with less memory"
|
||||
icon={Zap}
|
||||
/>
|
||||
</div>
|
||||
</CollapsibleCard>
|
||||
@@ -530,15 +517,22 @@ function Settings({
|
||||
</>
|
||||
);
|
||||
|
||||
// ─── Text Settings Tab Content ───
|
||||
const TextSettingsContent = () => (
|
||||
// ─── Text Settings ───
|
||||
const TextSettings = () => (
|
||||
<>
|
||||
<SectionHeader
|
||||
icon={Type}
|
||||
title="Text Generation"
|
||||
count={5}
|
||||
color="#8b5cf6"
|
||||
/>
|
||||
|
||||
<CollapsibleCard
|
||||
id="text_model"
|
||||
icon={MessageSquare}
|
||||
title="Model & Context"
|
||||
subtitle={`${textSettings.contextSize || "Auto"} context`}
|
||||
defaultExpanded={true}
|
||||
defaultExpanded={false}
|
||||
>
|
||||
<div className="m3-field-group">
|
||||
<div className="m3-text-field">
|
||||
@@ -568,7 +562,7 @@ function Settings({
|
||||
step="512"
|
||||
/>
|
||||
<span style={{ fontSize: "0.75rem", color: "var(--md-sys-color-outline)" }}>
|
||||
0 = Auto (model default). Larger = more memory used.
|
||||
0 = Auto (model default)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -578,8 +572,8 @@ function Settings({
|
||||
id="text_params"
|
||||
icon={Settings2}
|
||||
title="Generation Parameters"
|
||||
subtitle={`Temp: ${textSettings.temperature} • TopP: ${textSettings.topP}`}
|
||||
defaultExpanded={true}
|
||||
subtitle={`Temp: ${textSettings.temperature}`}
|
||||
defaultExpanded={false}
|
||||
>
|
||||
<div className="m3-field-group">
|
||||
<div className="m3-slider-group">
|
||||
@@ -732,7 +726,7 @@ function Settings({
|
||||
max="50"
|
||||
/>
|
||||
<span style={{ fontSize: "0.75rem", color: "var(--md-sys-color-outline)" }}>
|
||||
50 = All layers on GPU. 0 = CPU only.
|
||||
50 = All layers on GPU
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -754,7 +748,7 @@ function Settings({
|
||||
|
||||
<div className="m3-slider-group">
|
||||
<div className="m3-slider-header">
|
||||
<span className="m3-slider-label">KV Cache Type</span>
|
||||
<span className="m3-slider-label">KV Cache</span>
|
||||
</div>
|
||||
<div className="m3-segmented-button">
|
||||
{["q4_0", "q8_0", "f16"].map((type) => (
|
||||
@@ -790,7 +784,6 @@ function Settings({
|
||||
? "Show model's reasoning process"
|
||||
: "Model does not support thinking"
|
||||
}
|
||||
icon={Brain}
|
||||
/>
|
||||
{!supportsThinking && (
|
||||
<div style={{
|
||||
@@ -802,7 +795,7 @@ function Settings({
|
||||
color: "var(--md-sys-color-error)"
|
||||
}}>
|
||||
<Info size={14} style={{ verticalAlign: "middle", marginRight: "6px" }} />
|
||||
Current model does not support thinking. Load a reasoning model (e.g., DeepSeek-R1, QwQ) to enable.
|
||||
Current model does not support thinking. Load a reasoning model to enable.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -812,7 +805,7 @@ function Settings({
|
||||
id="text_backend"
|
||||
icon={Cpu}
|
||||
title="Text Backend"
|
||||
subtitle={llmStatus.settings?.backendMode || "Not running"}
|
||||
subtitle={llmStatus.settings?.backendMode || "Stopped"}
|
||||
defaultExpanded={false}
|
||||
>
|
||||
<div className="m3-field-group">
|
||||
@@ -858,17 +851,14 @@ function Settings({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<SectionTabs activeTab={activeTab} onTabChange={setActiveTab} />
|
||||
|
||||
{/* Hardware Tier Badge */}
|
||||
<HardwareTierBadge specs={specs} />
|
||||
|
||||
{/* Tab Content */}
|
||||
<div className="settings-grid">
|
||||
{activeTab === "image" && <ImageSettingsContent />}
|
||||
{activeTab === "text" && <TextSettingsContent />}
|
||||
</div>
|
||||
{/* Image Settings Section */}
|
||||
<ImageSettings />
|
||||
|
||||
{/* Text Settings Section */}
|
||||
<TextSettings />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user