Skip to content

Commit 1372069

Browse files
webui: dynamic config override support
- Only send user-overridden config to backend - Load from localStorage if available, otherwise fallback to server default - Add reset button to clear config and re-sync with server - Removed hardcoded CONFIG_DEFAULT - Simplified AppConfig and ensured backend is authoritative
1 parent c6a2c9e commit 1372069

File tree

6 files changed

+94
-83
lines changed

6 files changed

+94
-83
lines changed

tools/server/public/index.html.gz

-120 Bytes
Binary file not shown.

tools/server/webui/src/components/Header.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import StorageUtils from '../utils/storage';
33
import { useAppContext } from '../utils/app.context';
44
import { classNames } from '../utils/misc';
55
import daisyuiThemes from 'daisyui/theme/object';
6-
import { THEMES } from '../Config';
6+
import { THEMES } from '../utils/initConfig';
77
import {
88
Cog8ToothIcon,
99
MoonIcon,
@@ -66,7 +66,7 @@ export default function Header() {
6666
auto
6767
</button>
6868
</li>
69-
{THEMES.map((theme) => (
69+
{THEMES.map((theme: string) => (
7070
<li key={theme}>
7171
<input
7272
type="radio"

tools/server/webui/src/components/SettingDialog.tsx

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { useState } from 'react';
22
import { useAppContext } from '../utils/app.context';
3-
import { CONFIG_DEFAULT, CONFIG_INFO } from '../Config';
4-
import { isDev } from '../Config';
3+
import { CONFIG_INFO, isDev } from '../utils/initConfig';
54
import StorageUtils from '../utils/storage';
65
import { classNames, isBoolean, isNumeric, isString } from '../utils/misc';
76
import {
@@ -14,7 +13,8 @@ import {
1413
} from '@heroicons/react/24/outline';
1514
import { OpenInNewTab } from '../utils/common';
1615

17-
type SettKey = keyof typeof CONFIG_DEFAULT;
16+
import type { AppConfig } from '../utils/initConfig';
17+
type SettKey = keyof AppConfig;
1818

1919
const BASIC_KEYS: SettKey[] = [
2020
'temperature',
@@ -279,27 +279,25 @@ export default function SettingDialog({
279279
const [sectionIdx, setSectionIdx] = useState(0);
280280

281281
// clone the config object to prevent direct mutation
282-
const [localConfig, setLocalConfig] = useState<typeof CONFIG_DEFAULT>(
283-
JSON.parse(JSON.stringify(config))
284-
);
282+
const [localConfig, setLocalConfig] = useState<AppConfig>({ ...config });
285283

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

292292
const handleSave = () => {
293293
// copy the local config to prevent direct mutation
294-
const newConfig: typeof CONFIG_DEFAULT = JSON.parse(
295-
JSON.stringify(localConfig)
296-
);
294+
const newConfig: AppConfig = JSON.parse(JSON.stringify(localConfig));
297295
// validate the config
298296
for (const key in newConfig) {
299297
const value = newConfig[key as SettKey];
300-
const mustBeBoolean = isBoolean(CONFIG_DEFAULT[key as SettKey]);
301-
const mustBeString = isString(CONFIG_DEFAULT[key as SettKey]);
302-
const mustBeNumeric = isNumeric(CONFIG_DEFAULT[key as SettKey]);
298+
const mustBeBoolean = typeof config[key as SettKey] === 'boolean';
299+
const mustBeString = typeof config[key as SettKey] === 'string';
300+
const mustBeNumeric = typeof config[key as SettKey] === 'number';
303301
if (mustBeString) {
304302
if (!isString(value)) {
305303
alert(`Value for ${key} must be string`);
@@ -392,6 +390,7 @@ export default function SettingDialog({
392390
value={localConfig[field.key]}
393391
onChange={onChange(field.key)}
394392
label={field.label as string}
393+
defaultValue={config[field.key]}
395394
/>
396395
);
397396
} else if (field.type === SettingInputType.LONG_INPUT) {
@@ -402,6 +401,7 @@ export default function SettingDialog({
402401
value={localConfig[field.key].toString()}
403402
onChange={onChange(field.key)}
404403
label={field.label as string}
404+
defaultValue={config[field.key]}
405405
/>
406406
);
407407
} else if (field.type === SettingInputType.CHECKBOX) {
@@ -455,18 +455,20 @@ function SettingsModalLongInput({
455455
value,
456456
onChange,
457457
label,
458+
defaultValue,
458459
}: {
459460
configKey: SettKey;
460461
value: string;
461462
onChange: (value: string) => void;
462463
label?: string;
464+
defaultValue: string | number | boolean;
463465
}) {
464466
return (
465467
<label className="form-control">
466468
<div className="label inline text-sm">{label || configKey}</div>
467469
<textarea
468470
className="textarea textarea-bordered h-24 mb-2"
469-
placeholder={`Default: ${CONFIG_DEFAULT[configKey] || 'none'}`}
471+
placeholder={`Default: ${defaultValue ?? 'none'}`}
470472
value={value}
471473
onChange={(e) => onChange(e.target.value)}
472474
/>
@@ -479,12 +481,13 @@ function SettingsModalShortInput({
479481
value,
480482
onChange,
481483
label,
484+
defaultValue,
482485
}: {
483486
configKey: SettKey;
484-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
485-
value: any;
487+
value: string | number | boolean;
486488
onChange: (value: string) => void;
487489
label?: string;
490+
defaultValue: string | number | boolean;
488491
}) {
489492
const helpMsg = CONFIG_INFO[configKey];
490493

@@ -505,8 +508,10 @@ function SettingsModalShortInput({
505508
<input
506509
type="text"
507510
className="grow"
508-
placeholder={`Default: ${CONFIG_DEFAULT[configKey] || 'none'}`}
509-
value={value}
511+
placeholder={`Default: ${defaultValue ?? 'none'}`}
512+
value={
513+
typeof value === 'boolean' ? (value ? 'true' : 'false') : value
514+
}
510515
onChange={(e) => onChange(e.target.value)}
511516
/>
512517
</label>

tools/server/webui/src/utils/app.context.tsx

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
getSSEStreamAsync,
1616
getServerProps,
1717
} from './misc';
18-
import { BASE_URL, CONFIG_DEFAULT, isDev } from '../Config';
18+
import { BASE_URL, isDev, type AppConfig } from '../utils/initConfig';
1919
import { matchPath, useLocation, useNavigate } from 'react-router';
2020
import toast from 'react-hot-toast';
2121

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

4747
// config
48-
config: typeof CONFIG_DEFAULT;
49-
saveConfig: (config: typeof CONFIG_DEFAULT) => void;
48+
config: AppConfig;
49+
saveConfig: (config: AppConfig) => void;
5050
showSettings: boolean;
5151
setShowSettings: (show: boolean) => void;
5252

@@ -90,16 +90,25 @@ export const AppContextProvider = ({
9090
const [aborts, setAborts] = useState<
9191
Record<Conversation['id'], AbortController>
9292
>({});
93-
const [config, setConfig] = useState(StorageUtils.getConfig());
93+
const [config, setConfig] = useState<AppConfig>(() => {
94+
const cfg = StorageUtils.getConfig();
95+
if (Object.keys(cfg).length === 0) {
96+
console.warn('Config is empty at init (using {})');
97+
}
98+
return cfg;
99+
});
94100
const [canvasData, setCanvasData] = useState<CanvasData | null>(null);
95101
const [showSettings, setShowSettings] = useState(false);
96102

97103
// get server props
98104
useEffect(() => {
99105
getServerProps(BASE_URL, config.apiKey)
100-
.then((props) => {
106+
.then(async (props) => {
101107
console.debug('Server props:', props);
102108
setServerProps(props);
109+
console.info(
110+
'[Config] Loaded: user config only, server is authoritative by default.'
111+
);
103112
})
104113
.catch((err) => {
105114
console.error(err);
@@ -380,7 +389,7 @@ export const AppContextProvider = ({
380389
await generateMessage(convId, parentNodeId, onChunk);
381390
};
382391

383-
const saveConfig = (config: typeof CONFIG_DEFAULT) => {
392+
const saveConfig = (config: AppConfig) => {
384393
StorageUtils.setConfig(config);
385394
setConfig(config);
386395
};
Lines changed: 40 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,44 @@
1-
import daisyuiThemes from 'daisyui/theme/object';
2-
import { isNumeric } from './utils/misc';
3-
4-
export const isDev = import.meta.env.MODE === 'development';
5-
6-
// constants
71
export const BASE_URL = new URL('.', document.baseURI).href
82
.toString()
93
.replace(/\/$/, '');
104

11-
export const CONFIG_DEFAULT = {
5+
export type AppConfig = {
126
// 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.
137
// Do not use nested objects, keep it single level. Prefix the key if you need to group them.
14-
apiKey: '',
15-
systemMessage: '',
16-
showTokensPerSecond: false,
17-
showThoughtInProgress: false,
18-
excludeThoughtOnReq: true,
19-
pasteLongTextToFileLen: 2500,
20-
pdfAsImage: false,
8+
apiKey: string;
9+
systemMessage: string;
10+
showTokensPerSecond: boolean;
11+
showThoughtInProgress: boolean;
12+
excludeThoughtOnReq: boolean;
13+
pasteLongTextToFileLen: number;
14+
pdfAsImage: boolean;
15+
2116
// make sure these default values are in sync with `common.h`
22-
samplers: 'edkypmxt',
23-
temperature: 0.8,
24-
dynatemp_range: 0.0,
25-
dynatemp_exponent: 1.0,
26-
top_k: 40,
27-
top_p: 0.95,
28-
min_p: 0.05,
29-
xtc_probability: 0.0,
30-
xtc_threshold: 0.1,
31-
typical_p: 1.0,
32-
repeat_last_n: 64,
33-
repeat_penalty: 1.0,
34-
presence_penalty: 0.0,
35-
frequency_penalty: 0.0,
36-
dry_multiplier: 0.0,
37-
dry_base: 1.75,
38-
dry_allowed_length: 2,
39-
dry_penalty_last_n: -1,
40-
max_tokens: -1,
41-
custom: '', // custom json-stringified object
17+
samplers: string;
18+
temperature: number;
19+
dynatemp_range: number;
20+
dynatemp_exponent: number;
21+
top_k: number;
22+
top_p: number;
23+
min_p: number;
24+
xtc_probability: number;
25+
xtc_threshold: number;
26+
typical_p: number;
27+
repeat_last_n: number;
28+
repeat_penalty: number;
29+
presence_penalty: number;
30+
frequency_penalty: number;
31+
dry_multiplier: number;
32+
dry_base: number;
33+
dry_allowed_length: number;
34+
dry_penalty_last_n: number;
35+
max_tokens: number;
36+
custom: string; // custom json-stringified object
37+
4238
// experimental features
43-
pyIntepreterEnabled: false,
39+
pyIntepreterEnabled: boolean;
4440
};
41+
4542
export const CONFIG_INFO: Record<string, string> = {
4643
apiKey: 'Set the API Key if you are using --api-key option for the server.',
4744
systemMessage: 'The starting message that defines how model should behave.',
@@ -84,13 +81,11 @@ export const CONFIG_INFO: Record<string, string> = {
8481
max_tokens: 'The maximum number of token per output.',
8582
custom: '', // custom json-stringified object
8683
};
87-
// config keys having numeric value (i.e. temperature, top_k, top_p, etc)
88-
export const CONFIG_NUMERIC_KEYS = Object.entries(CONFIG_DEFAULT)
89-
.filter((e) => isNumeric(e[1]))
90-
.map((e) => e[0]);
91-
// list of themes supported by daisyui
92-
export const THEMES = ['light', 'dark']
93-
// make sure light & dark are always at the beginning
94-
.concat(
95-
Object.keys(daisyuiThemes).filter((t) => t !== 'light' && t !== 'dark')
96-
);
84+
85+
import daisyuiThemes from 'daisyui/theme/object';
86+
87+
export const THEMES = ['light', 'dark'].concat(
88+
Object.keys(daisyuiThemes).filter((t) => t !== 'light' && t !== 'dark')
89+
);
90+
91+
export const isDev = import.meta.env.MODE === 'development';

tools/server/webui/src/utils/storage.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// coversations is stored in localStorage
22
// format: { [convId]: { id: string, lastModified: number, messages: [...] } }
33

4-
import { CONFIG_DEFAULT } from '../Config';
4+
import type { AppConfig } from './initConfig';
55
import { Conversation, Message, TimingReport } from './types';
66
import Dexie, { Table } from 'dexie';
77

@@ -192,15 +192,17 @@ const StorageUtils = {
192192
},
193193

194194
// manage config
195-
getConfig(): typeof CONFIG_DEFAULT {
196-
const savedVal = JSON.parse(localStorage.getItem('config') || '{}');
197-
// to prevent breaking changes in the future, we always provide default value for missing keys
198-
return {
199-
...CONFIG_DEFAULT,
200-
...savedVal,
201-
};
195+
getConfig(): AppConfig {
196+
try {
197+
return JSON.parse(localStorage.getItem('config') || '{}') as AppConfig;
198+
} catch (err) {
199+
console.warn(
200+
'Malformed config in localStorage, falling back to empty config.'
201+
);
202+
return {} as AppConfig;
203+
}
202204
},
203-
setConfig(config: typeof CONFIG_DEFAULT) {
205+
setConfig(config: AppConfig) {
204206
localStorage.setItem('config', JSON.stringify(config));
205207
},
206208
getTheme(): string {

0 commit comments

Comments
 (0)