Skip to content

Commit 34ddd7b

Browse files
Refactor config loading to use dynamic AppConfig from server
1 parent d891942 commit 34ddd7b

File tree

6 files changed

+131
-80
lines changed

6 files changed

+131
-80
lines changed

tools/server/public/index.html.gz

6.49 KB
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: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
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 } from '../utils/initConfig';
4+
import { isDev } from '../utils/initConfig';
55
import StorageUtils from '../utils/storage';
66
import { classNames, isBoolean, isNumeric, isString } from '../utils/misc';
77
import {
@@ -14,7 +14,8 @@ import {
1414
} from '@heroicons/react/24/outline';
1515
import { OpenInNewTab } from '../utils/common';
1616

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

1920
const BASIC_KEYS: SettKey[] = [
2021
'temperature',
@@ -269,27 +270,23 @@ export default function SettingDialog({
269270
const [sectionIdx, setSectionIdx] = useState(0);
270271

271272
// clone the config object to prevent direct mutation
272-
const [localConfig, setLocalConfig] = useState<typeof CONFIG_DEFAULT>(
273-
JSON.parse(JSON.stringify(config))
274-
);
273+
const [localConfig, setLocalConfig] = useState<AppConfig>({ ...config });
275274

276275
const resetConfig = () => {
277276
if (window.confirm('Are you sure you want to reset all settings?')) {
278-
setLocalConfig(CONFIG_DEFAULT);
277+
setLocalConfig({ ...config });
279278
}
280279
};
281280

282281
const handleSave = () => {
283282
// copy the local config to prevent direct mutation
284-
const newConfig: typeof CONFIG_DEFAULT = JSON.parse(
285-
JSON.stringify(localConfig)
286-
);
283+
const newConfig: AppConfig = JSON.parse(JSON.stringify(localConfig));
287284
// validate the config
288285
for (const key in newConfig) {
289286
const value = newConfig[key as SettKey];
290-
const mustBeBoolean = isBoolean(CONFIG_DEFAULT[key as SettKey]);
291-
const mustBeString = isString(CONFIG_DEFAULT[key as SettKey]);
292-
const mustBeNumeric = isNumeric(CONFIG_DEFAULT[key as SettKey]);
287+
const mustBeBoolean = typeof config[key as SettKey] === 'boolean';
288+
const mustBeString = typeof config[key as SettKey] === 'string';
289+
const mustBeNumeric = typeof config[key as SettKey] === 'number';
293290
if (mustBeString) {
294291
if (!isString(value)) {
295292
alert(`Value for ${key} must be string`);
@@ -382,6 +379,7 @@ export default function SettingDialog({
382379
value={localConfig[field.key]}
383380
onChange={onChange(field.key)}
384381
label={field.label as string}
382+
defaultValue={config[field.key]}
385383
/>
386384
);
387385
} else if (field.type === SettingInputType.LONG_INPUT) {
@@ -392,6 +390,7 @@ export default function SettingDialog({
392390
value={localConfig[field.key].toString()}
393391
onChange={onChange(field.key)}
394392
label={field.label as string}
393+
defaultValue={config[field.key]}
395394
/>
396395
);
397396
} else if (field.type === SettingInputType.CHECKBOX) {
@@ -445,18 +444,20 @@ function SettingsModalLongInput({
445444
value,
446445
onChange,
447446
label,
447+
defaultValue,
448448
}: {
449449
configKey: SettKey;
450450
value: string;
451451
onChange: (value: string) => void;
452452
label?: string;
453+
defaultValue: string | number | boolean;
453454
}) {
454455
return (
455456
<label className="form-control mb-2">
456457
<div className="label inline">{label || configKey}</div>
457458
<textarea
458459
className="textarea textarea-bordered h-24"
459-
placeholder={`Default: ${CONFIG_DEFAULT[configKey] || 'none'}`}
460+
placeholder={`Default: ${defaultValue ?? 'none'}`}
460461
value={value}
461462
onChange={(e) => onChange(e.target.value)}
462463
/>
@@ -469,12 +470,13 @@ function SettingsModalShortInput({
469470
value,
470471
onChange,
471472
label,
473+
defaultValue,
472474
}: {
473475
configKey: SettKey;
474-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
475476
value: any;
476477
onChange: (value: string) => void;
477478
label?: string;
479+
defaultValue: string | number | boolean;
478480
}) {
479481
const helpMsg = CONFIG_INFO[configKey];
480482

@@ -502,7 +504,7 @@ function SettingsModalShortInput({
502504
<input
503505
type="text"
504506
className="grow"
505-
placeholder={`Default: ${CONFIG_DEFAULT[configKey] || 'none'}`}
507+
placeholder={`Default: ${defaultValue ?? 'none'}`}
506508
value={value}
507509
onChange={(e) => onChange(e.target.value)}
508510
/>

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, getServerDefaultConfig, 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+
const cfg = await getServerDefaultConfig(config.apiKey);
110+
StorageUtils.setConfig(cfg);
111+
setConfig(cfg);
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: 92 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,92 @@
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 = {
12-
// 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.
13-
// 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-
// make sure these default values are in sync with `common.h`
20-
samplers: 'edkypmxt',
21-
temperature: 0.8,
22-
dynatemp_range: 0.0,
23-
dynatemp_exponent: 1.0,
24-
top_k: 40,
25-
top_p: 0.95,
26-
min_p: 0.05,
27-
xtc_probability: 0.0,
28-
xtc_threshold: 0.1,
29-
typical_p: 1.0,
30-
repeat_last_n: 64,
31-
repeat_penalty: 1.0,
32-
presence_penalty: 0.0,
33-
frequency_penalty: 0.0,
34-
dry_multiplier: 0.0,
35-
dry_base: 1.75,
36-
dry_allowed_length: 2,
37-
dry_penalty_last_n: -1,
38-
max_tokens: -1,
39-
custom: '', // custom json-stringified object
40-
// experimental features
41-
pyIntepreterEnabled: false,
5+
export type AppConfig = {
6+
apiKey: string;
7+
systemMessage: string;
8+
showTokensPerSecond: boolean;
9+
showThoughtInProgress: boolean;
10+
excludeThoughtOnReq: boolean;
11+
samplers: string;
12+
temperature: number;
13+
dynatemp_range: number;
14+
dynatemp_exponent: number;
15+
top_k: number;
16+
top_p: number;
17+
min_p: number;
18+
xtc_probability: number;
19+
xtc_threshold: number;
20+
typical_p: number;
21+
repeat_last_n: number;
22+
repeat_penalty: number;
23+
presence_penalty: number;
24+
frequency_penalty: number;
25+
dry_multiplier: number;
26+
dry_base: number;
27+
dry_allowed_length: number;
28+
dry_penalty_last_n: number;
29+
max_tokens: number;
30+
custom: string;
31+
pyIntepreterEnabled: boolean;
4232
};
33+
34+
const mapSamplerNameToChar = (name: string): string => {
35+
const mapping: Record<string, string> = {
36+
penalties: 'e',
37+
dry: 'd',
38+
top_n_sigma: 'k',
39+
top_k: 'k',
40+
typ_p: 'y',
41+
top_p: 'p',
42+
min_p: 'm',
43+
xtc: 'x',
44+
temperature: 't',
45+
};
46+
return mapping[name] || '';
47+
};
48+
49+
export async function getServerDefaultConfig(apiKey?: string): Promise<AppConfig> {
50+
const response = await fetch(`${BASE_URL}/props`, {
51+
headers: {
52+
'Content-Type': 'application/json',
53+
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
54+
},
55+
});
56+
57+
if (!response.ok) throw new Error('Failed to fetch server props');
58+
const data = await response.json();
59+
const params = data.default_generation_settings?.params;
60+
if (!params) throw new Error('Missing generation params in /props');
61+
62+
const baseConfig: Partial<AppConfig> = {};
63+
64+
for (const [key, value] of Object.entries(params)) {
65+
if (typeof value === 'number') {
66+
(baseConfig as any)[key] = parseFloat(value.toFixed(3));
67+
} else if (typeof value === 'boolean' || typeof value === 'string') {
68+
baseConfig[key as keyof AppConfig] = value as any;
69+
}
70+
}
71+
72+
return {
73+
// client-only settings
74+
apiKey: '',
75+
systemMessage: '',
76+
showTokensPerSecond: false,
77+
showThoughtInProgress: false,
78+
excludeThoughtOnReq: true,
79+
custom: '',
80+
pyIntepreterEnabled: false,
81+
82+
// server-provided
83+
samplers: Array.isArray(params.samplers)
84+
? params.samplers.map(mapSamplerNameToChar).join('')
85+
: 'edkypmxt',
86+
...baseConfig,
87+
} as AppConfig;
88+
}
89+
4390
export const CONFIG_INFO: Record<string, string> = {
4491
apiKey: 'Set the API Key if you are using --api-key option for the server.',
4592
systemMessage: 'The starting message that defines how model should behave.',
@@ -80,13 +127,11 @@ export const CONFIG_INFO: Record<string, string> = {
80127
max_tokens: 'The maximum number of token per output.',
81128
custom: '', // custom json-stringified object
82129
};
83-
// config keys having numeric value (i.e. temperature, top_k, top_p, etc)
84-
export const CONFIG_NUMERIC_KEYS = Object.entries(CONFIG_DEFAULT)
85-
.filter((e) => isNumeric(e[1]))
86-
.map((e) => e[0]);
87-
// list of themes supported by daisyui
88-
export const THEMES = ['light', 'dark']
89-
// make sure light & dark are always at the beginning
90-
.concat(
91-
Object.keys(daisyuiThemes).filter((t) => t !== 'light' && t !== 'dark')
92-
);
130+
131+
import daisyuiThemes from 'daisyui/theme/object';
132+
133+
export const THEMES = ['light', 'dark'].concat(
134+
Object.keys(daisyuiThemes).filter((t) => t !== 'light' && t !== 'dark')
135+
);
136+
137+
export const isDev = import.meta.env.MODE === 'development';

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

Lines changed: 4 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,10 @@ 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+
return JSON.parse(localStorage.getItem('config') || '{}') as AppConfig;
202197
},
203-
setConfig(config: typeof CONFIG_DEFAULT) {
198+
setConfig(config: AppConfig) {
204199
localStorage.setItem('config', JSON.stringify(config));
205200
},
206201
getTheme(): string {

0 commit comments

Comments
 (0)