| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536 |
- 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<SettingInputType, SettingInputType.CUSTOM>;
- 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: (
- <>
- <Cog6ToothIcon className={ICON_CLASSNAME} />
- 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: (
- <>
- <FunnelIcon className={ICON_CLASSNAME} />
- 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: (
- <>
- <HandRaisedIcon className={ICON_CLASSNAME} />
- Penalties
- </>
- ),
- fields: PENALTY_KEYS.map((key) => ({
- type: SettingInputType.SHORT_INPUT,
- label: key,
- key,
- })),
- },
- {
- title: (
- <>
- <ChatBubbleOvalLeftEllipsisIcon className={ICON_CLASSNAME} />
- 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: (
- <>
- <SquaresPlusIcon className={ICON_CLASSNAME} />
- 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 (
- <button className="btn" onClick={debugImportDemoConv}>
- (debug) Import demo conversation
- </button>
- );
- },
- },
- {
- type: SettingInputType.CHECKBOX,
- label: 'Show tokens per second',
- key: 'showTokensPerSecond',
- },
- {
- type: SettingInputType.LONG_INPUT,
- label: (
- <>
- Custom JSON config (For more info, refer to{' '}
- <OpenInNewTab href="https://github.com/ggerganov/llama.cpp/blob/master/examples/server/README.md">
- server documentation
- </OpenInNewTab>
- )
- </>
- ),
- key: 'custom',
- },
- ],
- },
- {
- title: (
- <>
- <BeakerIcon className={ICON_CLASSNAME} />
- Experimental
- </>
- ),
- fields: [
- {
- type: SettingInputType.CUSTOM,
- key: 'custom', // dummy key, won't be used
- component: () => (
- <>
- <p className="mb-8">
- Experimental features are not guaranteed to work correctly.
- <br />
- <br />
- If you encounter any problems, create a{' '}
- <OpenInNewTab href="https://github.com/ggerganov/llama.cpp/issues/new?template=019-bug-misc.yml">
- Bug (misc.)
- </OpenInNewTab>{' '}
- report on Github. Please also specify <b>webui/experimental</b> on
- the report title and include screenshots.
- <br />
- <br />
- Some features may require packages downloaded from CDN, so they
- need internet connection.
- </p>
- </>
- ),
- },
- {
- type: SettingInputType.CHECKBOX,
- label: (
- <>
- <b>Enable Python interpreter</b>
- <br />
- <small className="text-xs">
- This feature uses{' '}
- <OpenInNewTab href="https://pyodide.org">pyodide</OpenInNewTab>,
- 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.
- </small>
- </>
- ),
- 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<typeof CONFIG_DEFAULT>(
- 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 (
- <dialog className={classNames({ modal: true, 'modal-open': show })}>
- <div className="modal-box w-11/12 max-w-3xl">
- <h3 className="text-lg font-bold mb-6">Settings</h3>
- <div className="flex flex-col md:flex-row h-[calc(90vh-12rem)]">
- {/* Left panel, showing sections - Desktop version */}
- <div className="hidden md:flex flex-col items-stretch pr-4 mr-4 border-r-2 border-base-200">
- {SETTING_SECTIONS.map((section, idx) => (
- <div
- key={idx}
- className={classNames({
- 'btn btn-ghost justify-start font-normal w-44 mb-1': true,
- 'btn-active': sectionIdx === idx,
- })}
- onClick={() => setSectionIdx(idx)}
- dir="auto"
- >
- {section.title}
- </div>
- ))}
- </div>
- {/* Left panel, showing sections - Mobile version */}
- <div className="md:hidden flex flex-row gap-2 mb-4">
- <details className="dropdown">
- <summary className="btn bt-sm w-full m-1">
- {SETTING_SECTIONS[sectionIdx].title}
- </summary>
- <ul className="menu dropdown-content bg-base-100 rounded-box z-[1] w-52 p-2 shadow">
- {SETTING_SECTIONS.map((section, idx) => (
- <div
- key={idx}
- className={classNames({
- 'btn btn-ghost justify-start font-normal': true,
- 'btn-active': sectionIdx === idx,
- })}
- onClick={() => setSectionIdx(idx)}
- dir="auto"
- >
- {section.title}
- </div>
- ))}
- </ul>
- </details>
- </div>
- {/* Right panel, showing setting fields */}
- <div className="grow overflow-y-auto px-4">
- {SETTING_SECTIONS[sectionIdx].fields.map((field, idx) => {
- const key = `${sectionIdx}-${idx}`;
- if (field.type === SettingInputType.SHORT_INPUT) {
- return (
- <SettingsModalShortInput
- key={key}
- configKey={field.key}
- value={localConfig[field.key]}
- onChange={onChange(field.key)}
- label={field.label as string}
- />
- );
- } else if (field.type === SettingInputType.LONG_INPUT) {
- return (
- <SettingsModalLongInput
- key={key}
- configKey={field.key}
- value={localConfig[field.key].toString()}
- onChange={onChange(field.key)}
- label={field.label as string}
- />
- );
- } else if (field.type === SettingInputType.CHECKBOX) {
- return (
- <SettingsModalCheckbox
- key={key}
- configKey={field.key}
- value={!!localConfig[field.key]}
- onChange={onChange(field.key)}
- label={field.label as string}
- />
- );
- } else if (field.type === SettingInputType.CUSTOM) {
- return (
- <div key={key} className="mb-2">
- {typeof field.component === 'string'
- ? field.component
- : field.component({
- value: localConfig[field.key],
- onChange: onChange(field.key),
- })}
- </div>
- );
- }
- })}
- <p className="opacity-40 mb-6 text-sm mt-8">
- Settings are saved in browser's localStorage
- </p>
- </div>
- </div>
- <div className="modal-action">
- <button className="btn" onClick={resetConfig}>
- Reset to default
- </button>
- <button className="btn" onClick={onClose}>
- Close
- </button>
- <button className="btn btn-primary" onClick={handleSave}>
- Save
- </button>
- </div>
- </div>
- </dialog>
- );
- }
- function SettingsModalLongInput({
- configKey,
- value,
- onChange,
- label,
- }: {
- configKey: SettKey;
- value: string;
- onChange: (value: string) => void;
- label?: string;
- }) {
- return (
- <label className="form-control mb-2">
- <div className="label inline">{label || configKey}</div>
- <textarea
- className="textarea textarea-bordered h-24"
- placeholder={`Default: ${CONFIG_DEFAULT[configKey] || 'none'}`}
- value={value}
- onChange={(e) => onChange(e.target.value)}
- />
- </label>
- );
- }
- function SettingsModalShortInput({
- configKey,
- value,
- onChange,
- label,
- }: {
- configKey: SettKey;
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- value: any;
- onChange: (value: string) => void;
- label?: string;
- }) {
- const helpMsg = CONFIG_INFO[configKey];
- return (
- <>
- {/* on mobile, we simply show the help message here */}
- {helpMsg && (
- <div className="block md:hidden mb-1">
- <b>{label || configKey}</b>
- <br />
- <p className="text-xs">{helpMsg}</p>
- </div>
- )}
- <label className="input input-bordered join-item grow flex items-center gap-2 mb-2">
- <div className="dropdown dropdown-hover">
- <div tabIndex={0} role="button" className="font-bold hidden md:block">
- {label || configKey}
- </div>
- {helpMsg && (
- <div className="dropdown-content menu bg-base-100 rounded-box z-10 w-64 p-2 shadow mt-4">
- {helpMsg}
- </div>
- )}
- </div>
- <input
- type="text"
- className="grow"
- placeholder={`Default: ${CONFIG_DEFAULT[configKey] || 'none'}`}
- value={value}
- onChange={(e) => onChange(e.target.value)}
- />
- </label>
- </>
- );
- }
- function SettingsModalCheckbox({
- configKey,
- value,
- onChange,
- label,
- }: {
- configKey: SettKey;
- value: boolean;
- onChange: (value: boolean) => void;
- label: string;
- }) {
- return (
- <div className="flex flex-row items-center mb-2">
- <input
- type="checkbox"
- className="toggle"
- checked={value}
- onChange={(e) => onChange(e.target.checked)}
- />
- <span className="ml-4">{label || configKey}</span>
- </div>
- );
- }
|