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 { classNames, isBoolean, isNumeric, isString } from '../utils/misc'; import { BeakerIcon, ChatBubbleOvalLeftEllipsisIcon, Cog6ToothIcon, FunnelIcon, HandRaisedIcon, SquaresPlusIcon, } from '@heroicons/react/24/outline'; import { OpenInNewTab } from '../utils/common'; 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', ]; 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'; const SETTING_SECTIONS: 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.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', }, ], }, ]; 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)) ); 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.map((section, idx) => (
setSectionIdx(idx)} dir="auto" > {section.title}
))}
{/* Left panel, showing sections - Mobile version */}
{SETTING_SECTIONS[sectionIdx].title}
    {SETTING_SECTIONS.map((section, idx) => (
    setSectionIdx(idx)} dir="auto" > {section.title}
    ))}
{/* Right panel, showing setting fields */}
{SETTING_SECTIONS[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 (