import { useState } from 'react'; 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; const BASIC_KEYS: SettKey[] = [ 'temperature', 'top_k', 'top_p', 'min_p', 'max_tokens', ]; const SAMPLER_KEYS: SettKey[] = [ 'dynatemp_range', 'dynatemp_exponent', 'typical_p', 'xtc_probability', 'xtc_threshold', 'top_n_sigma' ]; const PENALTY_KEYS: SettKey[] = [ 'repeat_last_n', 'repeat_penalty', 'presence_penalty', 'frequency_penalty', 'dry_multiplier', 'dry_base', 'dry_allowed_length', 'dry_penalty_last_n', ]; enum SettingInputType { SHORT_INPUT, LONG_INPUT, CHECKBOX, CUSTOM, } interface SettingFieldInput { type: Exclude; label: string | React.ReactElement; help?: string | React.ReactElement; key: SettKey; } interface SettingFieldCustom { type: SettingInputType.CUSTOM; key: SettKey; component: | string | React.FC<{ value: string | boolean | number; onChange: (value: string) => void; }>; } interface SettingSection { title: React.ReactElement; fields: (SettingFieldInput | SettingFieldCustom)[]; } const ICON_CLASSNAME = 'w-4 h-4 mr-1 inline'; // 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: ( <> General ), fields: [ { type: SettingInputType.SHORT_INPUT, label: 'API Key', key: 'apiKey', }, { type: SettingInputType.LONG_INPUT, label: 'System Message (will be disabled if left empty)', key: 'systemMessage', }, ...BASIC_KEYS.map( (key) => ({ type: SettingInputType.SHORT_INPUT, label: key, key, }) as SettingFieldInput ), ], }, { title: ( <> Samplers ), fields: [ { type: SettingInputType.SHORT_INPUT, label: 'Samplers queue', key: 'samplers', }, ...SAMPLER_KEYS.map( (key) => ({ type: SettingInputType.SHORT_INPUT, label: key, key, }) as SettingFieldInput ), ], }, { title: ( <> Penalties ), fields: PENALTY_KEYS.map((key) => ({ type: SettingInputType.SHORT_INPUT, label: key, key, })), }, { title: ( <> Reasoning ), fields: [ { type: SettingInputType.CHECKBOX, label: 'Expand thought process by default when generating messages', key: 'showThoughtInProgress', }, { type: SettingInputType.CHECKBOX, label: 'Exclude thought process when sending requests to API (Recommended for DeepSeek-R1)', key: 'excludeThoughtOnReq', }, ], }, { title: ( <> Advanced ), fields: [ { type: SettingInputType.CUSTOM, key: 'custom', // dummy key, won't be used component: () => { const debugImportDemoConv = async () => { const res = await fetch('/demo-conversation.json'); const demoConv = await res.json(); StorageUtils.remove(demoConv.id); for (const msg of demoConv.messages) { StorageUtils.appendMsg(demoConv.id, msg); } }; return ( ); }, }, { 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', key: 'showTokensPerSecond', }, { type: SettingInputType.LONG_INPUT, label: ( <> Custom JSON config (For more info, refer to{' '} server documentation ) ), key: 'custom', }, ], }, { title: ( <> Experimental ), fields: [ { type: SettingInputType.CUSTOM, key: 'custom', // dummy key, won't be used component: () => ( <>

Experimental features are not guaranteed to work correctly.

If you encounter any problems, create a{' '} Bug (misc.) {' '} report on Github. Please also specify webui/experimental on the report title and include screenshots.

Some features may require packages downloaded from CDN, so they need internet connection.

), }, { type: SettingInputType.CHECKBOX, label: ( <> Enable Python interpreter
This feature uses{' '} pyodide, downloaded from CDN. To use this feature, ask the LLM to generate Python code inside a Markdown code block. You will see a "Run" button on the code block, near the "Copy" button. ), key: 'pyIntepreterEnabled', }, ], }, { title: ( <> Presets ), fields: [ { type: SettingInputType.CUSTOM, key: 'custom', // dummy key for presets component: () => ( ), }, ], }, ]; export default function SettingDialog({ show, onClose, }: { show: boolean; onClose: () => void; }) { const { config, saveConfig } = useAppContext(); const [sectionIdx, setSectionIdx] = useState(0); // clone the config object to prevent direct mutation const [localConfig, setLocalConfig] = useState( 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); } }; const handleSave = () => { // copy the local config to prevent direct mutation const newConfig: typeof CONFIG_DEFAULT = JSON.parse( JSON.stringify(localConfig) ); // validate the config for (const key in newConfig) { const value = newConfig[key as SettKey]; const mustBeBoolean = isBoolean(CONFIG_DEFAULT[key as SettKey]); const mustBeString = isString(CONFIG_DEFAULT[key as SettKey]); const mustBeNumeric = isNumeric(CONFIG_DEFAULT[key as SettKey]); if (mustBeString) { if (!isString(value)) { alert(`Value for ${key} must be string`); return; } } else if (mustBeNumeric) { const trimmedValue = value.toString().trim(); const numVal = Number(trimmedValue); if (isNaN(numVal) || !isNumeric(numVal) || trimmedValue.length === 0) { alert(`Value for ${key} must be numeric`); return; } // force conversion to number // @ts-expect-error this is safe newConfig[key] = numVal; } else if (mustBeBoolean) { if (!isBoolean(value)) { alert(`Value for ${key} must be boolean`); return; } } else { console.error(`Unknown default type for key ${key}`); } } if (isDev) console.log('Saving config', newConfig); saveConfig(newConfig); onClose(); }; const onChange = (key: SettKey) => (value: string | boolean) => { // note: we do not perform validation here, because we may get incomplete value as user is still typing it setLocalConfig({ ...localConfig, [key]: value }); }; return (

Settings

{/* Left panel, showing sections - Desktop version */}
{SETTING_SECTIONS_GENERATED.map((section, idx) => (
setSectionIdx(idx)} dir="auto" > {section.title}
))}
{/* Left panel, showing sections - Mobile version */}
{SETTING_SECTIONS_GENERATED[sectionIdx].title}
    {SETTING_SECTIONS_GENERATED.map((section, idx) => (
    setSectionIdx(idx)} dir="auto" > {section.title}
    ))}
{/* Right panel, showing setting fields */}
{SETTING_SECTIONS_GENERATED[sectionIdx].fields.map((field, idx) => { const key = `${sectionIdx}-${idx}`; if (field.type === SettingInputType.SHORT_INPUT) { return ( ); } else if (field.type === SettingInputType.LONG_INPUT) { return ( ); } else if (field.type === SettingInputType.CHECKBOX) { return ( ); } else if (field.type === SettingInputType.CUSTOM) { return (
{typeof field.component === 'string' ? field.component : field.component({ value: localConfig[field.key], onChange: onChange(field.key), })}
); } })}

Settings are saved in browser's localStorage

); } function SettingsModalLongInput({ configKey, value, onChange, label, }: { configKey: SettKey; value: string; onChange: (value: string) => void; label?: string; }) { return (