diff --git a/examples/server/public/index.html.gz b/examples/server/public/index.html.gz index 646988ad8..84bde767f 100644 Binary files a/examples/server/public/index.html.gz and b/examples/server/public/index.html.gz differ diff --git a/examples/server/webui/src/components/MarkdownDisplay.tsx b/examples/server/webui/src/components/MarkdownDisplay.tsx index 8ab8de655..814920a74 100644 --- a/examples/server/webui/src/components/MarkdownDisplay.tsx +++ b/examples/server/webui/src/components/MarkdownDisplay.tsx @@ -23,6 +23,7 @@ export default function MarkdownDisplay({ content }: { content: string }) { button: (props) => ( ), + // note: do not use "pre", "p" or other basic html elements here, it will cause the node to re-render when the message is being generated (this should be a bug with react-markdown, not sure how to fix it) }} > {preprocessedContent} diff --git a/examples/server/webui/src/components/SettingDialog.tsx b/examples/server/webui/src/components/SettingDialog.tsx index ae8117fd2..5565ab7bb 100644 --- a/examples/server/webui/src/components/SettingDialog.tsx +++ b/examples/server/webui/src/components/SettingDialog.tsx @@ -3,6 +3,7 @@ import { useAppContext } from '../utils/app.context'; import { CONFIG_DEFAULT, CONFIG_INFO } from '../Config'; import { isDev } from '../Config'; import StorageUtils from '../utils/storage'; +import { isBoolean, isNumeric, isString } from '../utils/misc'; type SettKey = keyof typeof CONFIG_DEFAULT; @@ -52,7 +53,42 @@ export default function SettingDialog({ }; const handleSave = () => { - saveConfig(localConfig); + // 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 trimedValue = value.toString().trim(); + const numVal = Number(trimedValue); + if (isNaN(numVal) || !isNumeric(numVal) || trimedValue.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(); }; @@ -66,6 +102,11 @@ export default function SettingDialog({ 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 (
@@ -79,9 +120,7 @@ export default function SettingDialog({ configKey="apiKey" configDefault={CONFIG_DEFAULT} value={localConfig.apiKey} - onChange={(value) => - setLocalConfig({ ...localConfig, apiKey: value }) - } + onChange={onChange('apiKey')} /> @@ -107,9 +141,7 @@ export default function SettingDialog({ configKey={key} configDefault={CONFIG_DEFAULT} value={localConfig[key]} - onChange={(value) => - setLocalConfig({ ...localConfig, [key]: value }) - } + onChange={onChange(key)} /> ))} @@ -123,9 +155,7 @@ export default function SettingDialog({ configKey="samplers" configDefault={CONFIG_DEFAULT} value={localConfig.samplers} - onChange={(value) => - setLocalConfig({ ...localConfig, samplers: value }) - } + onChange={onChange('samplers')} /> {OTHER_SAMPLER_KEYS.map((key) => ( - setLocalConfig({ ...localConfig, [key]: value }) - } + onChange={onChange(key)} /> ))}
@@ -152,9 +180,7 @@ export default function SettingDialog({ configKey={key} configDefault={CONFIG_DEFAULT} value={localConfig[key]} - onChange={(value) => - setLocalConfig({ ...localConfig, [key]: value }) - } + onChange={onChange(key)} /> ))} @@ -171,10 +197,7 @@ export default function SettingDialog({ className="checkbox" checked={localConfig.showThoughtInProgress} onChange={(e) => - setLocalConfig({ - ...localConfig, - showThoughtInProgress: e.target.checked, - }) + onChange('showThoughtInProgress')(e.target.checked) } /> @@ -187,10 +210,7 @@ export default function SettingDialog({ className="checkbox" checked={localConfig.excludeThoughtOnReq} onChange={(e) => - setLocalConfig({ - ...localConfig, - excludeThoughtOnReq: e.target.checked, - }) + onChange('excludeThoughtOnReq')(e.target.checked) } /> @@ -220,10 +240,7 @@ export default function SettingDialog({ className="checkbox" checked={localConfig.showTokensPerSecond} onChange={(e) => - setLocalConfig({ - ...localConfig, - showTokensPerSecond: e.target.checked, - }) + onChange('showTokensPerSecond')(e.target.checked) } /> Show tokens per second @@ -245,9 +262,7 @@ export default function SettingDialog({ className="textarea textarea-bordered h-24" placeholder='Example: { "mirostat": 1, "min_p": 0.1 }' value={localConfig.custom} - onChange={(e) => - setLocalConfig({ ...localConfig, custom: e.target.value }) - } + onChange={(e) => onChange('custom')(e.target.value)} />