From d80a37c894fae6df769b9c04e037ffbe5fb40a8b Mon Sep 17 00:00:00 2001 From: Your Name <55459720+gabriellarson@users.noreply.github.com> Date: Thu, 3 Jul 2025 13:22:53 -0700 Subject: [PATCH 1/3] webui: add settings presets feature --- .../webui/src/components/SettingDialog.tsx | 185 +++++++++++++++++- tools/server/webui/src/utils/storage.ts | 40 +++- tools/server/webui/src/utils/types.ts | 7 + 3 files changed, 226 insertions(+), 6 deletions(-) diff --git a/tools/server/webui/src/components/SettingDialog.tsx b/tools/server/webui/src/components/SettingDialog.tsx index 45a8d73b00592..26ebe5fb9953f 100644 --- a/tools/server/webui/src/components/SettingDialog.tsx +++ b/tools/server/webui/src/components/SettingDialog.tsx @@ -6,14 +6,17 @@ import StorageUtils from '../utils/storage'; import { classNames, isBoolean, isNumeric, isString } from '../utils/misc'; import { BeakerIcon, + BookmarkIcon, ChatBubbleOvalLeftEllipsisIcon, Cog6ToothIcon, FunnelIcon, HandRaisedIcon, SquaresPlusIcon, + TrashIcon, } from '@heroicons/react/24/outline'; import { OpenInNewTab } from '../utils/common'; import { useModals } from './ModalProvider'; +import { SettingsPreset } from '../utils/types'; type SettKey = keyof typeof CONFIG_DEFAULT; @@ -74,7 +77,156 @@ interface SettingSection { const ICON_CLASSNAME = 'w-4 h-4 mr-1 inline'; -const SETTING_SECTIONS: SettingSection[] = [ +// Presets Component +function PresetsManager({ + currentConfig, + onLoadPreset, +}: { + currentConfig: typeof CONFIG_DEFAULT; + onLoadPreset: (config: typeof CONFIG_DEFAULT) => void; +}) { + const [presets, setPresets] = useState(() => + StorageUtils.getPresets() + ); + const [presetName, setPresetName] = useState(''); + const [selectedPresetId, setSelectedPresetId] = useState(null); + const { showConfirm, showAlert } = useModals(); + + const handleSavePreset = async () => { + if (!presetName.trim()) { + await showAlert('Please enter a preset name'); + return; + } + + // Check if preset name already exists + const existingPreset = presets.find((p) => p.name === presetName.trim()); + if (existingPreset) { + if ( + await showConfirm( + `Preset "${presetName}" already exists. Do you want to overwrite it?` + ) + ) { + StorageUtils.updatePreset(existingPreset.id, currentConfig); + setPresets(StorageUtils.getPresets()); + setPresetName(''); + await showAlert('Preset updated successfully'); + } + } else { + const newPreset = StorageUtils.savePreset( + presetName.trim(), + currentConfig + ); + setPresets([...presets, newPreset]); + setPresetName(''); + await showAlert('Preset saved successfully'); + } + }; + + const handleLoadPreset = async (preset: SettingsPreset) => { + if ( + await showConfirm( + `Load preset "${preset.name}"? Current settings will be replaced.` + ) + ) { + onLoadPreset(preset.config as typeof CONFIG_DEFAULT); + setSelectedPresetId(preset.id); + } + }; + + const handleDeletePreset = async (preset: SettingsPreset) => { + if (await showConfirm(`Delete preset "${preset.name}"?`)) { + StorageUtils.deletePreset(preset.id); + setPresets(presets.filter((p) => p.id !== preset.id)); + if (selectedPresetId === preset.id) { + setSelectedPresetId(null); + } + } + }; + + return ( +
+ {/* Save current settings as preset */} +
+ +
+ setPresetName(e.target.value)} + onKeyPress={(e) => { + if (e.key === 'Enter') { + handleSavePreset(); + } + }} + /> + +
+
+ + {/* List of saved presets */} +
+ + {presets.length === 0 ? ( +
+ No presets saved yet +
+ ) : ( +
+ {presets.map((preset) => ( +
+
+
+

{preset.name}

+

+ Created: {new Date(preset.createdAt).toLocaleString()} +

+
+
+ + +
+
+
+ ))} +
+ )} +
+
+ ); +} + +// Moved SETTING_SECTIONS inside component so it can access localConfig +const createSettingSections = ( + localConfig: typeof CONFIG_DEFAULT, + setLocalConfig: (config: typeof CONFIG_DEFAULT) => void +): SettingSection[] => [ { title: ( <> @@ -267,6 +419,26 @@ const SETTING_SECTIONS: SettingSection[] = [ }, ], }, + { + title: ( + <> + + Presets + + ), + fields: [ + { + type: SettingInputType.CUSTOM, + key: 'custom', // dummy key for presets + component: () => ( + + ), + }, + ], + }, ]; export default function SettingDialog({ @@ -285,6 +457,9 @@ export default function SettingDialog({ ); const { showConfirm, showAlert } = useModals(); + // Generate sections with access to local state + const settingSections = createSettingSections(localConfig, setLocalConfig); + const resetConfig = async () => { if (await showConfirm('Are you sure you want to reset all settings?')) { setLocalConfig(CONFIG_DEFAULT); @@ -351,7 +526,7 @@ export default function SettingDialog({ aria-description="Settings sections" tabIndex={0} > - {SETTING_SECTIONS.map((section, idx) => ( + {settingSections.map((section, idx) => (