Skip to content

Webui dynamic config #13429

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified tools/server/public/index.html.gz
Binary file not shown.
4 changes: 2 additions & 2 deletions tools/server/webui/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import StorageUtils from '../utils/storage';
import { useAppContext } from '../utils/app.context';
import { classNames } from '../utils/misc';
import daisyuiThemes from 'daisyui/theme/object';
import { THEMES } from '../Config';
import { THEMES } from '../utils/initConfig';
import {
Cog8ToothIcon,
MoonIcon,
Expand Down Expand Up @@ -66,7 +66,7 @@ export default function Header() {
auto
</button>
</li>
{THEMES.map((theme) => (
{THEMES.map((theme: string) => (
<li key={theme}>
<input
type="radio"
Expand Down
47 changes: 26 additions & 21 deletions tools/server/webui/src/components/SettingDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useState } from 'react';
import { useAppContext } from '../utils/app.context';
import { CONFIG_DEFAULT, CONFIG_INFO } from '../Config';
import { isDev } from '../Config';
import { CONFIG_INFO, isDev } from '../utils/initConfig';
import StorageUtils from '../utils/storage';
import { classNames, isBoolean, isNumeric, isString } from '../utils/misc';
import {
Expand All @@ -14,7 +13,8 @@ import {
} from '@heroicons/react/24/outline';
import { OpenInNewTab } from '../utils/common';

type SettKey = keyof typeof CONFIG_DEFAULT;
import type { AppConfig } from '../utils/initConfig';
type SettKey = keyof AppConfig;

const BASIC_KEYS: SettKey[] = [
'temperature',
Expand Down Expand Up @@ -279,27 +279,25 @@ export default function SettingDialog({
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 [localConfig, setLocalConfig] = useState<AppConfig>({ ...config });

const resetConfig = () => {
if (window.confirm('Are you sure you want to reset all settings?')) {
setLocalConfig(CONFIG_DEFAULT);
}
const resetConfig = async () => {
if (!window.confirm('Reset all settings from server defaults?')) return;
localStorage.removeItem('config');
saveConfig({} as AppConfig);
setLocalConfig({} as AppConfig);
console.info('[Config] Reset to empty (server fallback)');
};

const handleSave = () => {
// copy the local config to prevent direct mutation
const newConfig: typeof CONFIG_DEFAULT = JSON.parse(
JSON.stringify(localConfig)
);
const newConfig: AppConfig = 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]);
const mustBeBoolean = typeof config[key as SettKey] === 'boolean';
const mustBeString = typeof config[key as SettKey] === 'string';
const mustBeNumeric = typeof config[key as SettKey] === 'number';
if (mustBeString) {
if (!isString(value)) {
alert(`Value for ${key} must be string`);
Expand Down Expand Up @@ -392,6 +390,7 @@ export default function SettingDialog({
value={localConfig[field.key]}
onChange={onChange(field.key)}
label={field.label as string}
defaultValue={config[field.key]}
/>
);
} else if (field.type === SettingInputType.LONG_INPUT) {
Expand All @@ -402,6 +401,7 @@ export default function SettingDialog({
value={localConfig[field.key].toString()}
onChange={onChange(field.key)}
label={field.label as string}
defaultValue={config[field.key]}
/>
);
} else if (field.type === SettingInputType.CHECKBOX) {
Expand Down Expand Up @@ -455,18 +455,20 @@ function SettingsModalLongInput({
value,
onChange,
label,
defaultValue,
}: {
configKey: SettKey;
value: string;
onChange: (value: string) => void;
label?: string;
defaultValue: string | number | boolean;
}) {
return (
<label className="form-control">
<div className="label inline text-sm">{label || configKey}</div>
<textarea
className="textarea textarea-bordered h-24 mb-2"
placeholder={`Default: ${CONFIG_DEFAULT[configKey] || 'none'}`}
placeholder={`Default: ${defaultValue ?? 'none'}`}
value={value}
onChange={(e) => onChange(e.target.value)}
/>
Expand All @@ -479,12 +481,13 @@ function SettingsModalShortInput({
value,
onChange,
label,
defaultValue,
}: {
configKey: SettKey;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any;
value: string | number | boolean;
onChange: (value: string) => void;
label?: string;
defaultValue: string | number | boolean;
}) {
const helpMsg = CONFIG_INFO[configKey];

Expand All @@ -505,8 +508,10 @@ function SettingsModalShortInput({
<input
type="text"
className="grow"
placeholder={`Default: ${CONFIG_DEFAULT[configKey] || 'none'}`}
value={value}
placeholder={`Default: ${defaultValue ?? 'none'}`}
value={
typeof value === 'boolean' ? (value ? 'true' : 'false') : value
}
onChange={(e) => onChange(e.target.value)}
/>
</label>
Expand Down
21 changes: 15 additions & 6 deletions tools/server/webui/src/utils/app.context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
getSSEStreamAsync,
getServerProps,
} from './misc';
import { BASE_URL, CONFIG_DEFAULT, isDev } from '../Config';
import { BASE_URL, isDev, type AppConfig } from '../utils/initConfig';
import { matchPath, useLocation, useNavigate } from 'react-router';
import toast from 'react-hot-toast';

Expand Down Expand Up @@ -45,8 +45,8 @@ interface AppContextValue {
setCanvasData: (data: CanvasData | null) => void;

// config
config: typeof CONFIG_DEFAULT;
saveConfig: (config: typeof CONFIG_DEFAULT) => void;
config: AppConfig;
saveConfig: (config: AppConfig) => void;
showSettings: boolean;
setShowSettings: (show: boolean) => void;

Expand Down Expand Up @@ -90,16 +90,25 @@ export const AppContextProvider = ({
const [aborts, setAborts] = useState<
Record<Conversation['id'], AbortController>
>({});
const [config, setConfig] = useState(StorageUtils.getConfig());
const [config, setConfig] = useState<AppConfig>(() => {
const cfg = StorageUtils.getConfig();
if (Object.keys(cfg).length === 0) {
console.warn('Config is empty at init (using {})');
}
return cfg;
});
const [canvasData, setCanvasData] = useState<CanvasData | null>(null);
const [showSettings, setShowSettings] = useState(false);

// get server props
useEffect(() => {
getServerProps(BASE_URL, config.apiKey)
.then((props) => {
.then(async (props) => {
console.debug('Server props:', props);
setServerProps(props);
console.info(
'[Config] Loaded: user config only, server is authoritative by default.'
);
})
.catch((err) => {
console.error(err);
Expand Down Expand Up @@ -380,7 +389,7 @@ export const AppContextProvider = ({
await generateMessage(convId, parentNodeId, onChunk);
};

const saveConfig = (config: typeof CONFIG_DEFAULT) => {
const saveConfig = (config: AppConfig) => {
StorageUtils.setConfig(config);
setConfig(config);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,47 +1,44 @@
import daisyuiThemes from 'daisyui/theme/object';
import { isNumeric } from './utils/misc';

export const isDev = import.meta.env.MODE === 'development';

// constants
export const BASE_URL = new URL('.', document.baseURI).href
.toString()
.replace(/\/$/, '');

export const CONFIG_DEFAULT = {
export type AppConfig = {
// Note: in order not to introduce breaking changes, please keep the same data type (number, string, etc) if you want to change the default value. Do not use null or undefined for default value.
// Do not use nested objects, keep it single level. Prefix the key if you need to group them.
apiKey: '',
systemMessage: '',
showTokensPerSecond: false,
showThoughtInProgress: false,
excludeThoughtOnReq: true,
pasteLongTextToFileLen: 2500,
pdfAsImage: false,
apiKey: string;
systemMessage: string;
showTokensPerSecond: boolean;
showThoughtInProgress: boolean;
excludeThoughtOnReq: boolean;
pasteLongTextToFileLen: number;
pdfAsImage: boolean;

// make sure these default values are in sync with `common.h`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

samplers: 'edkypmxt',
temperature: 0.8,
dynatemp_range: 0.0,
dynatemp_exponent: 1.0,
top_k: 40,
top_p: 0.95,
min_p: 0.05,
xtc_probability: 0.0,
xtc_threshold: 0.1,
typical_p: 1.0,
repeat_last_n: 64,
repeat_penalty: 1.0,
presence_penalty: 0.0,
frequency_penalty: 0.0,
dry_multiplier: 0.0,
dry_base: 1.75,
dry_allowed_length: 2,
dry_penalty_last_n: -1,
max_tokens: -1,
custom: '', // custom json-stringified object
samplers: string;
temperature: number;
dynatemp_range: number;
dynatemp_exponent: number;
top_k: number;
top_p: number;
min_p: number;
xtc_probability: number;
xtc_threshold: number;
typical_p: number;
repeat_last_n: number;
repeat_penalty: number;
presence_penalty: number;
frequency_penalty: number;
dry_multiplier: number;
dry_base: number;
dry_allowed_length: number;
dry_penalty_last_n: number;
max_tokens: number;
custom: string; // custom json-stringified object

// experimental features
pyIntepreterEnabled: false,
pyIntepreterEnabled: boolean;
};

export const CONFIG_INFO: Record<string, string> = {
apiKey: 'Set the API Key if you are using --api-key option for the server.',
systemMessage: 'The starting message that defines how model should behave.',
Expand Down Expand Up @@ -84,13 +81,11 @@ export const CONFIG_INFO: Record<string, string> = {
max_tokens: 'The maximum number of token per output.',
custom: '', // custom json-stringified object
};
// config keys having numeric value (i.e. temperature, top_k, top_p, etc)
export const CONFIG_NUMERIC_KEYS = Object.entries(CONFIG_DEFAULT)
.filter((e) => isNumeric(e[1]))
.map((e) => e[0]);
// list of themes supported by daisyui
export const THEMES = ['light', 'dark']
// make sure light & dark are always at the beginning
.concat(
Comment on lines -87 to -94
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

Object.keys(daisyuiThemes).filter((t) => t !== 'light' && t !== 'dark')
);

import daisyuiThemes from 'daisyui/theme/object';

export const THEMES = ['light', 'dark'].concat(
Object.keys(daisyuiThemes).filter((t) => t !== 'light' && t !== 'dark')
);

export const isDev = import.meta.env.MODE === 'development';
20 changes: 11 additions & 9 deletions tools/server/webui/src/utils/storage.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// coversations is stored in localStorage
// format: { [convId]: { id: string, lastModified: number, messages: [...] } }

import { CONFIG_DEFAULT } from '../Config';
import type { AppConfig } from './initConfig';
import { Conversation, Message, TimingReport } from './types';
import Dexie, { Table } from 'dexie';

Expand Down Expand Up @@ -192,15 +192,17 @@ const StorageUtils = {
},

// manage config
getConfig(): typeof CONFIG_DEFAULT {
const savedVal = JSON.parse(localStorage.getItem('config') || '{}');
// to prevent breaking changes in the future, we always provide default value for missing keys
return {
...CONFIG_DEFAULT,
...savedVal,
};
getConfig(): AppConfig {
try {
return JSON.parse(localStorage.getItem('config') || '{}') as AppConfig;
} catch (err) {
console.warn(
'Malformed config in localStorage, falling back to empty config.'
);
return {} as AppConfig;
}
},
setConfig(config: typeof CONFIG_DEFAULT) {
setConfig(config: AppConfig) {
localStorage.setItem('config', JSON.stringify(config));
},
getTheme(): string {
Expand Down