From d44c2d3f5aeab25a9405896f48a36082cee5d8ac Mon Sep 17 00:00:00 2001 From: firecoperana Date: Sun, 20 Jul 2025 05:33:55 -0500 Subject: Webui: New Features for Conversations, Settings, and Chat Messages (#618) * Webui: add Rename/Upload conversation in header and sidebar webui: don't change modified date when renaming conversation * webui: add a preset feature to the settings #14649 * webui: Add editing assistant messages #13522 Webui: keep the following message while editing assistance response. webui: change icon to edit message * webui: DB import and export #14347 * webui: Wrap long numbers instead of infinite horizontal scroll (#14062) fix sidebar being covered by main content #14082 --------- Co-authored-by: firecoperana --- .../server/webui/src/components/SettingDialog.tsx | 268 ++++++++++++++++++++- 1 file changed, 263 insertions(+), 5 deletions(-) (limited to 'examples/server/webui/src/components/SettingDialog.tsx') diff --git a/examples/server/webui/src/components/SettingDialog.tsx b/examples/server/webui/src/components/SettingDialog.tsx index cd091a55..004b51ab 100644 --- a/examples/server/webui/src/components/SettingDialog.tsx +++ b/examples/server/webui/src/components/SettingDialog.tsx @@ -3,16 +3,21 @@ import { useAppContext } from '../utils/app.context'; import { CONFIG_DEFAULT, CONFIG_INFO } from '../Config'; import { isDev } from '../Config'; import StorageUtils from '../utils/storage'; +import { useModals } from './ModalProvider'; 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 { SettingsPreset } from '../utils/types'; +import toast from 'react-hot-toast' type SettKey = keyof typeof CONFIG_DEFAULT; @@ -74,7 +79,155 @@ 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()} +

+
+
+ + +
+
+
+ ))} +
+ )} +
+
+ ); +} + +const SETTING_SECTIONS = ( + localConfig: typeof CONFIG_DEFAULT, + setLocalConfig: (config: typeof CONFIG_DEFAULT) => void +): SettingSection[] => [ { title: ( <> @@ -187,6 +340,85 @@ const SETTING_SECTIONS: SettingSection[] = [ ); }, }, + { + type: SettingInputType.CUSTOM, + key: 'custom', // dummy key, won't be used + component: () => { + const exportDB = async () => { + const blob = await StorageUtils.exportDB(); + const a = document.createElement('a'); + document.body.appendChild(a); + a.href = URL.createObjectURL(blob); + document.body.appendChild(a); + a.download = `llamawebui_dump.json`; + a.click(); + document.body.removeChild(a); + }; + return ( + + ); + }, + }, + { + type: SettingInputType.CUSTOM, + key: 'custom', // dummy key, won't be used + component: () => { + const importDB = async (e: React.ChangeEvent) => { + console.log(e); + if (!e.target.files) { + toast.error('Target.files cant be null'); + throw new Error('e.target.files cant be null'); + } + if (e.target.files.length != 1) + { + toast.error( + 'Number of selected files for DB import must be 1 but was ' + + e.target.files.length + + '.'); + throw new Error( + 'Number of selected files for DB import must be 1 but was ' + + e.target.files.length + + '.' + ); + } + const file = e.target.files[0]; + try { + if (!file) throw new Error('No DB found to import.'); + console.log('Importing DB ' + file.name); + await StorageUtils.importDB(file); + toast.success('Import complete') + window.location.reload(); + } catch (error) { + console.error('' + error); + toast.error('' + error); + } + }; + return ( +
+ + +
+ ); + }, + }, + { type: SettingInputType.CHECKBOX, label: 'Show tokens per second', @@ -257,6 +489,26 @@ const SETTING_SECTIONS: SettingSection[] = [ }, ], }, + { + title: ( + <> + + Presets + + ), + fields: [ + { + type: SettingInputType.CUSTOM, + key: 'custom', // dummy key for presets + component: () => ( + + ), + }, + ], + }, ]; export default function SettingDialog({ @@ -274,6 +526,12 @@ export default function SettingDialog({ JSON.parse(JSON.stringify(config)) ); + // Generate sections with access to local state + const SETTING_SECTIONS_GENERATED = SETTING_SECTIONS( + localConfig, + setLocalConfig + ); + const resetConfig = () => { if (window.confirm('Are you sure you want to reset all settings?')) { setLocalConfig(CONFIG_DEFAULT); @@ -332,7 +590,7 @@ export default function SettingDialog({
{/* Left panel, showing sections - Desktop version */}
- {SETTING_SECTIONS.map((section, idx) => ( + {SETTING_SECTIONS_GENERATED.map((section, idx) => (
- {SETTING_SECTIONS[sectionIdx].title} + {SETTING_SECTIONS_GENERATED[sectionIdx].title}
    - {SETTING_SECTIONS.map((section, idx) => ( + {SETTING_SECTIONS_GENERATED.map((section, idx) => (
    - {SETTING_SECTIONS[sectionIdx].fields.map((field, idx) => { + {SETTING_SECTIONS_GENERATED[sectionIdx].fields.map((field, idx) => { const key = `${sectionIdx}-${idx}`; if (field.type === SettingInputType.SHORT_INPUT) { return ( -- cgit v1.2.3