diff --git a/tools/server/public/index.html.gz b/tools/server/public/index.html.gz
index 01eec46e842ac..9b88bb96dc0b8 100644
Binary files a/tools/server/public/index.html.gz and b/tools/server/public/index.html.gz differ
diff --git a/tools/server/webui/src/components/Header.tsx b/tools/server/webui/src/components/Header.tsx
index 45775ff7a6258..e75f0c66f0f8a 100644
--- a/tools/server/webui/src/components/Header.tsx
+++ b/tools/server/webui/src/components/Header.tsx
@@ -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,
@@ -66,7 +66,7 @@ export default function Header() {
auto
- {THEMES.map((theme) => (
+ {THEMES.map((theme: string) => (
(
- JSON.parse(JSON.stringify(config))
- );
+ const [localConfig, setLocalConfig] = useState({ ...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`);
@@ -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) {
@@ -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) {
@@ -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 (
diff --git a/tools/server/webui/src/utils/app.context.tsx b/tools/server/webui/src/utils/app.context.tsx
index 96cffd95aba7c..15687322aa005 100644
--- a/tools/server/webui/src/utils/app.context.tsx
+++ b/tools/server/webui/src/utils/app.context.tsx
@@ -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';
@@ -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;
@@ -90,16 +90,25 @@ export const AppContextProvider = ({
const [aborts, setAborts] = useState<
Record
>({});
- const [config, setConfig] = useState(StorageUtils.getConfig());
+ const [config, setConfig] = useState(() => {
+ const cfg = StorageUtils.getConfig();
+ if (Object.keys(cfg).length === 0) {
+ console.warn('Config is empty at init (using {})');
+ }
+ return cfg;
+ });
const [canvasData, setCanvasData] = useState(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);
@@ -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);
};
diff --git a/tools/server/webui/src/Config.ts b/tools/server/webui/src/utils/initConfig.ts
similarity index 74%
rename from tools/server/webui/src/Config.ts
rename to tools/server/webui/src/utils/initConfig.ts
index c03ac287f3484..eecd8b18bcc78 100644
--- a/tools/server/webui/src/Config.ts
+++ b/tools/server/webui/src/utils/initConfig.ts
@@ -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`
- 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 = {
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.',
@@ -84,13 +81,11 @@ export const CONFIG_INFO: Record = {
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(
- 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';
diff --git a/tools/server/webui/src/utils/storage.ts b/tools/server/webui/src/utils/storage.ts
index 505693e9272ac..8cc577355e29d 100644
--- a/tools/server/webui/src/utils/storage.ts
+++ b/tools/server/webui/src/utils/storage.ts
@@ -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';
@@ -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 {