redo Settings modal UI
This commit is contained in:
parent
d80be897ac
commit
422e53e607
3 changed files with 364 additions and 185 deletions
10
examples/server/webui/package-lock.json
generated
10
examples/server/webui/package-lock.json
generated
|
@ -8,6 +8,7 @@
|
||||||
"name": "webui",
|
"name": "webui",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@heroicons/react": "^2.2.0",
|
||||||
"@sec-ant/readable-stream": "^0.6.0",
|
"@sec-ant/readable-stream": "^0.6.0",
|
||||||
"@vscode/markdown-it-katex": "^1.1.1",
|
"@vscode/markdown-it-katex": "^1.1.1",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
|
@ -902,6 +903,15 @@
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@heroicons/react": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">= 16 || ^19.0.0-rc"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@humanfs/core": {
|
"node_modules/@humanfs/core": {
|
||||||
"version": "0.19.1",
|
"version": "0.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@heroicons/react": "^2.2.0",
|
||||||
"@sec-ant/readable-stream": "^0.6.0",
|
"@sec-ant/readable-stream": "^0.6.0",
|
||||||
"@vscode/markdown-it-katex": "^1.1.1",
|
"@vscode/markdown-it-katex": "^1.1.1",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
|
|
|
@ -3,18 +3,25 @@ import { useAppContext } from '../utils/app.context';
|
||||||
import { CONFIG_DEFAULT, CONFIG_INFO } from '../Config';
|
import { CONFIG_DEFAULT, CONFIG_INFO } from '../Config';
|
||||||
import { isDev } from '../Config';
|
import { isDev } from '../Config';
|
||||||
import StorageUtils from '../utils/storage';
|
import StorageUtils from '../utils/storage';
|
||||||
import { isBoolean, isNumeric, isString } from '../utils/misc';
|
import { classNames, isBoolean, isNumeric, isString } from '../utils/misc';
|
||||||
|
import {
|
||||||
|
ChatBubbleOvalLeftEllipsisIcon,
|
||||||
|
Cog6ToothIcon,
|
||||||
|
FunnelIcon,
|
||||||
|
HandRaisedIcon,
|
||||||
|
SquaresPlusIcon,
|
||||||
|
} from '@heroicons/react/24/outline';
|
||||||
|
|
||||||
type SettKey = keyof typeof CONFIG_DEFAULT;
|
type SettKey = keyof typeof CONFIG_DEFAULT;
|
||||||
|
|
||||||
const COMMON_SAMPLER_KEYS: SettKey[] = [
|
const BASIC_KEYS: SettKey[] = [
|
||||||
'temperature',
|
'temperature',
|
||||||
'top_k',
|
'top_k',
|
||||||
'top_p',
|
'top_p',
|
||||||
'min_p',
|
'min_p',
|
||||||
'max_tokens',
|
'max_tokens',
|
||||||
];
|
];
|
||||||
const OTHER_SAMPLER_KEYS: SettKey[] = [
|
const SAMPLER_KEYS: SettKey[] = [
|
||||||
'dynatemp_range',
|
'dynatemp_range',
|
||||||
'dynatemp_exponent',
|
'dynatemp_exponent',
|
||||||
'typical_p',
|
'typical_p',
|
||||||
|
@ -32,6 +39,178 @@ const PENALTY_KEYS: SettKey[] = [
|
||||||
'dry_penalty_last_n',
|
'dry_penalty_last_n',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
enum SettingInputType {
|
||||||
|
SHORT_INPUT,
|
||||||
|
LONG_INPUT,
|
||||||
|
CHECKBOX,
|
||||||
|
CUSTOM,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SettingFieldInput {
|
||||||
|
type: Exclude<SettingInputType, SettingInputType.CUSTOM>;
|
||||||
|
label: string | React.ReactElement;
|
||||||
|
help?: string | React.ReactElement;
|
||||||
|
key: SettKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SettingFieldCustom {
|
||||||
|
type: SettingInputType.CUSTOM;
|
||||||
|
key: SettKey;
|
||||||
|
component:
|
||||||
|
| string
|
||||||
|
| React.FC<{
|
||||||
|
value: string | boolean | number;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SettingSection {
|
||||||
|
title: React.ReactElement;
|
||||||
|
fields: (SettingFieldInput | SettingFieldCustom)[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const ICON_CLASSNAME = 'w-4 h-4 mr-1 inline';
|
||||||
|
|
||||||
|
const SETTING_SECTIONS: SettingSection[] = [
|
||||||
|
{
|
||||||
|
title: (
|
||||||
|
<>
|
||||||
|
<Cog6ToothIcon className={ICON_CLASSNAME} />
|
||||||
|
General
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: SettingInputType.SHORT_INPUT,
|
||||||
|
label: 'API Key',
|
||||||
|
key: 'apiKey',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: SettingInputType.LONG_INPUT,
|
||||||
|
label: 'System Message (will be disabled if left empty)',
|
||||||
|
key: 'systemMessage',
|
||||||
|
},
|
||||||
|
...BASIC_KEYS.map(
|
||||||
|
(key) =>
|
||||||
|
({
|
||||||
|
type: SettingInputType.SHORT_INPUT,
|
||||||
|
label: key,
|
||||||
|
key,
|
||||||
|
}) as SettingFieldInput
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: (
|
||||||
|
<>
|
||||||
|
<FunnelIcon className={ICON_CLASSNAME} />
|
||||||
|
Samplers
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: SettingInputType.SHORT_INPUT,
|
||||||
|
label: 'Samplers queue',
|
||||||
|
key: 'samplers',
|
||||||
|
},
|
||||||
|
...SAMPLER_KEYS.map(
|
||||||
|
(key) =>
|
||||||
|
({
|
||||||
|
type: SettingInputType.SHORT_INPUT,
|
||||||
|
label: key,
|
||||||
|
key,
|
||||||
|
}) as SettingFieldInput
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: (
|
||||||
|
<>
|
||||||
|
<HandRaisedIcon className={ICON_CLASSNAME} />
|
||||||
|
Penalties
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
fields: PENALTY_KEYS.map((key) => ({
|
||||||
|
type: SettingInputType.SHORT_INPUT,
|
||||||
|
label: key,
|
||||||
|
key,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: (
|
||||||
|
<>
|
||||||
|
<ChatBubbleOvalLeftEllipsisIcon className={ICON_CLASSNAME} />
|
||||||
|
Reasoning
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: SettingInputType.CHECKBOX,
|
||||||
|
label: 'Expand though process by default for generating message',
|
||||||
|
key: 'showThoughtInProgress',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: SettingInputType.CHECKBOX,
|
||||||
|
label:
|
||||||
|
'Exclude thought process when sending request to API (Recommended for DeepSeek-R1)',
|
||||||
|
key: 'excludeThoughtOnReq',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: (
|
||||||
|
<>
|
||||||
|
<SquaresPlusIcon className={ICON_CLASSNAME} />
|
||||||
|
Advanced
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: SettingInputType.CUSTOM,
|
||||||
|
key: 'custom', // dummy key, won't be used
|
||||||
|
component: () => {
|
||||||
|
const debugImportDemoConv = async () => {
|
||||||
|
const res = await fetch('/demo-conversation.json');
|
||||||
|
const demoConv = await res.json();
|
||||||
|
StorageUtils.remove(demoConv.id);
|
||||||
|
for (const msg of demoConv.messages) {
|
||||||
|
StorageUtils.appendMsg(demoConv.id, msg);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<button className="btn" onClick={debugImportDemoConv}>
|
||||||
|
(debug) Import demo conversation
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: SettingInputType.CHECKBOX,
|
||||||
|
label: 'Show tokens per second',
|
||||||
|
key: 'showTokensPerSecond',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: SettingInputType.LONG_INPUT,
|
||||||
|
label: (
|
||||||
|
<>
|
||||||
|
Custom JSON config (For more info, refer to{' '}
|
||||||
|
<a
|
||||||
|
className="underline"
|
||||||
|
href="https://github.com/ggerganov/llama.cpp/blob/master/examples/server/README.md"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
server documentation
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
key: 'custom',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export default function SettingDialog({
|
export default function SettingDialog({
|
||||||
show,
|
show,
|
||||||
onClose,
|
onClose,
|
||||||
|
@ -40,6 +219,7 @@ export default function SettingDialog({
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}) {
|
}) {
|
||||||
const { config, saveConfig } = useAppContext();
|
const { config, saveConfig } = useAppContext();
|
||||||
|
const [sectionIdx, setSectionIdx] = useState(0);
|
||||||
|
|
||||||
// clone the config object to prevent direct mutation
|
// clone the config object to prevent direct mutation
|
||||||
const [localConfig, setLocalConfig] = useState<typeof CONFIG_DEFAULT>(
|
const [localConfig, setLocalConfig] = useState<typeof CONFIG_DEFAULT>(
|
||||||
|
@ -92,16 +272,6 @@ export default function SettingDialog({
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const debugImportDemoConv = async () => {
|
|
||||||
const res = await fetch('/demo-conversation.json');
|
|
||||||
const demoConv = await res.json();
|
|
||||||
StorageUtils.remove(demoConv.id);
|
|
||||||
for (const msg of demoConv.messages) {
|
|
||||||
StorageUtils.appendMsg(demoConv.id, msg);
|
|
||||||
}
|
|
||||||
onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onChange = (key: SettKey) => (value: string | boolean) => {
|
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
|
// note: we do not perform validation here, because we may get incomplete value as user is still typing it
|
||||||
setLocalConfig({ ...localConfig, [key]: value });
|
setLocalConfig({ ...localConfig, [key]: value });
|
||||||
|
@ -109,164 +279,102 @@ export default function SettingDialog({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<dialog className={`modal ${show ? 'modal-open' : ''}`}>
|
<dialog className={`modal ${show ? 'modal-open' : ''}`}>
|
||||||
<div className="modal-box">
|
<div className="modal-box w-11/12 max-w-3xl">
|
||||||
<h3 className="text-lg font-bold mb-6">Settings</h3>
|
<h3 className="text-lg font-bold mb-6">Settings</h3>
|
||||||
<div className="h-[calc(90vh-12rem)] overflow-y-auto">
|
<div className="flex flex-col md:flex-row h-[calc(90vh-12rem)]">
|
||||||
<p className="opacity-40 mb-6">
|
{/* Left panel, showing sections - Desktop version */}
|
||||||
Settings below are saved in browser's localStorage
|
<div className="hidden md:flex flex-col items-stretch pr-4 mr-4 border-r-2 border-base-200">
|
||||||
</p>
|
{SETTING_SECTIONS.map((section, idx) => (
|
||||||
|
<div
|
||||||
<SettingsModalShortInput
|
key={idx}
|
||||||
configKey="apiKey"
|
className={classNames({
|
||||||
configDefault={CONFIG_DEFAULT}
|
'btn btn-ghost justify-start font-normal w-44 mb-1': true,
|
||||||
value={localConfig.apiKey}
|
'btn-active': sectionIdx === idx,
|
||||||
onChange={onChange('apiKey')}
|
})}
|
||||||
/>
|
onClick={() => setSectionIdx(idx)}
|
||||||
|
dir="auto"
|
||||||
<label className="form-control mb-2">
|
>
|
||||||
<div className="label">
|
{section.title}
|
||||||
System Message (will be disabled if left empty)
|
|
||||||
</div>
|
|
||||||
<textarea
|
|
||||||
className="textarea textarea-bordered h-24"
|
|
||||||
placeholder={`Default: ${CONFIG_DEFAULT.systemMessage}`}
|
|
||||||
value={localConfig.systemMessage}
|
|
||||||
onChange={(e) => onChange('systemMessage')(e.target.value)}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
{COMMON_SAMPLER_KEYS.map((key) => (
|
|
||||||
<SettingsModalShortInput
|
|
||||||
key={key}
|
|
||||||
configKey={key}
|
|
||||||
configDefault={CONFIG_DEFAULT}
|
|
||||||
value={localConfig[key]}
|
|
||||||
onChange={onChange(key)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<details className="collapse collapse-arrow bg-base-200 mb-2 overflow-visible">
|
|
||||||
<summary className="collapse-title font-bold">
|
|
||||||
Other sampler settings
|
|
||||||
</summary>
|
|
||||||
<div className="collapse-content">
|
|
||||||
<SettingsModalShortInput
|
|
||||||
label="Samplers queue"
|
|
||||||
configKey="samplers"
|
|
||||||
configDefault={CONFIG_DEFAULT}
|
|
||||||
value={localConfig.samplers}
|
|
||||||
onChange={onChange('samplers')}
|
|
||||||
/>
|
|
||||||
{OTHER_SAMPLER_KEYS.map((key) => (
|
|
||||||
<SettingsModalShortInput
|
|
||||||
key={key}
|
|
||||||
configKey={key}
|
|
||||||
configDefault={CONFIG_DEFAULT}
|
|
||||||
value={localConfig[key]}
|
|
||||||
onChange={onChange(key)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details className="collapse collapse-arrow bg-base-200 mb-2 overflow-visible">
|
|
||||||
<summary className="collapse-title font-bold">
|
|
||||||
Penalties settings
|
|
||||||
</summary>
|
|
||||||
<div className="collapse-content">
|
|
||||||
{PENALTY_KEYS.map((key) => (
|
|
||||||
<SettingsModalShortInput
|
|
||||||
key={key}
|
|
||||||
configKey={key}
|
|
||||||
configDefault={CONFIG_DEFAULT}
|
|
||||||
value={localConfig[key]}
|
|
||||||
onChange={onChange(key)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details className="collapse collapse-arrow bg-base-200 mb-2 overflow-visible">
|
|
||||||
<summary className="collapse-title font-bold">
|
|
||||||
Reasoning models
|
|
||||||
</summary>
|
|
||||||
<div className="collapse-content">
|
|
||||||
<div className="flex flex-row items-center mb-2">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="checkbox"
|
|
||||||
checked={localConfig.showThoughtInProgress}
|
|
||||||
onChange={(e) =>
|
|
||||||
onChange('showThoughtInProgress')(e.target.checked)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<span className="ml-4">
|
|
||||||
Expand though process by default for generating message
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row items-center mb-2">
|
))}
|
||||||
<input
|
</div>
|
||||||
type="checkbox"
|
|
||||||
className="checkbox"
|
|
||||||
checked={localConfig.excludeThoughtOnReq}
|
|
||||||
onChange={(e) =>
|
|
||||||
onChange('excludeThoughtOnReq')(e.target.checked)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<span className="ml-4">
|
|
||||||
Exclude thought process when sending request to API
|
|
||||||
(Recommended for DeepSeek-R1)
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details className="collapse collapse-arrow bg-base-200 mb-2 overflow-visible">
|
{/* Left panel, showing sections - Mobile version */}
|
||||||
<summary className="collapse-title font-bold">
|
<div className="md:hidden flex flex-row gap-2 mb-4">
|
||||||
Advanced config
|
<details className="dropdown">
|
||||||
</summary>
|
<summary className="btn bt-sm w-full m-1">
|
||||||
<div className="collapse-content">
|
{SETTING_SECTIONS[sectionIdx].title}
|
||||||
{/* this button only shows in dev mode, used to import a demo conversation to test message rendering */}
|
</summary>
|
||||||
{isDev && (
|
<ul className="menu dropdown-content bg-base-100 rounded-box z-[1] w-52 p-2 shadow">
|
||||||
<div className="flex flex-row items-center mb-2">
|
{SETTING_SECTIONS.map((section, idx) => (
|
||||||
<button className="btn" onClick={debugImportDemoConv}>
|
<div
|
||||||
(debug) Import demo conversation
|
key={idx}
|
||||||
</button>
|
className={classNames({
|
||||||
</div>
|
'btn btn-ghost justify-start font-normal': true,
|
||||||
)}
|
'btn-active': sectionIdx === idx,
|
||||||
<div className="flex flex-row items-center mb-2">
|
})}
|
||||||
<input
|
onClick={() => setSectionIdx(idx)}
|
||||||
type="checkbox"
|
dir="auto"
|
||||||
className="checkbox"
|
|
||||||
checked={localConfig.showTokensPerSecond}
|
|
||||||
onChange={(e) =>
|
|
||||||
onChange('showTokensPerSecond')(e.target.checked)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<span className="ml-4">Show tokens per second</span>
|
|
||||||
</div>
|
|
||||||
<label className="form-control mb-2">
|
|
||||||
<div className="label inline">
|
|
||||||
Custom JSON config (For more info, refer to{' '}
|
|
||||||
<a
|
|
||||||
className="underline"
|
|
||||||
href="https://github.com/ggerganov/llama.cpp/blob/master/examples/server/README.md"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
>
|
||||||
server documentation
|
{section.title}
|
||||||
</a>
|
</div>
|
||||||
)
|
))}
|
||||||
</div>
|
</ul>
|
||||||
<textarea
|
</details>
|
||||||
className="textarea textarea-bordered h-24"
|
</div>
|
||||||
placeholder='Example: { "mirostat": 1, "min_p": 0.1 }'
|
|
||||||
value={localConfig.custom}
|
{/* Right panel, showing setting fields */}
|
||||||
onChange={(e) => onChange('custom')(e.target.value)}
|
<div className="grow overflow-y-auto px-4">
|
||||||
/>
|
{SETTING_SECTIONS[sectionIdx].fields.map((field, idx) => {
|
||||||
</label>
|
const key = `${sectionIdx}-${idx}`;
|
||||||
</div>
|
if (field.type === SettingInputType.SHORT_INPUT) {
|
||||||
</details>
|
return (
|
||||||
|
<SettingsModalShortInput
|
||||||
|
key={key}
|
||||||
|
configKey={field.key}
|
||||||
|
value={localConfig[field.key]}
|
||||||
|
onChange={onChange(field.key)}
|
||||||
|
label={field.label as string}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (field.type === SettingInputType.LONG_INPUT) {
|
||||||
|
return (
|
||||||
|
<SettingsModalLongInput
|
||||||
|
key={key}
|
||||||
|
configKey={field.key}
|
||||||
|
value={localConfig[field.key].toString()}
|
||||||
|
onChange={onChange(field.key)}
|
||||||
|
label={field.label as string}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (field.type === SettingInputType.CHECKBOX) {
|
||||||
|
return (
|
||||||
|
<SettingsModalCheckbox
|
||||||
|
key={key}
|
||||||
|
configKey={field.key}
|
||||||
|
value={!!localConfig[field.key]}
|
||||||
|
onChange={onChange(field.key)}
|
||||||
|
label={field.label as string}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (field.type === SettingInputType.CUSTOM) {
|
||||||
|
return (
|
||||||
|
<div key={key} className="mb-2">
|
||||||
|
{typeof field.component === 'string'
|
||||||
|
? field.component
|
||||||
|
: field.component({
|
||||||
|
value: localConfig[field.key],
|
||||||
|
onChange: onChange(field.key),
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
|
||||||
|
<p className="opacity-40 mb-6 text-sm mt-8">
|
||||||
|
Settings are saved in browser's localStorage
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="modal-action">
|
<div className="modal-action">
|
||||||
|
@ -285,37 +393,97 @@ export default function SettingDialog({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SettingsModalShortInput({
|
function SettingsModalLongInput({
|
||||||
configKey,
|
configKey,
|
||||||
configDefault,
|
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
label,
|
label,
|
||||||
}: {
|
}: {
|
||||||
configKey: SettKey;
|
configKey: SettKey;
|
||||||
configDefault: typeof CONFIG_DEFAULT;
|
value: string;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
value: any;
|
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
label?: string;
|
label?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<label className="input input-bordered join-item grow flex items-center gap-2 mb-2">
|
<label className="form-control mb-2">
|
||||||
<div className="dropdown dropdown-hover">
|
<div className="label inline">{label || configKey}</div>
|
||||||
<div tabIndex={0} role="button" className="font-bold">
|
<textarea
|
||||||
{label || configKey}
|
className="textarea textarea-bordered h-24"
|
||||||
</div>
|
placeholder={`Default: ${CONFIG_DEFAULT[configKey] || 'none'}`}
|
||||||
<div className="dropdown-content menu bg-base-100 rounded-box z-10 w-64 p-2 shadow mt-4">
|
|
||||||
{CONFIG_INFO[configKey] ?? '(no help message available)'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="grow"
|
|
||||||
placeholder={`Default: ${configDefault[configKey] || 'none'}`}
|
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function SettingsModalShortInput({
|
||||||
|
configKey,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
label,
|
||||||
|
}: {
|
||||||
|
configKey: SettKey;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
value: any;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
label?: string;
|
||||||
|
}) {
|
||||||
|
const helpMsg = CONFIG_INFO[configKey];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* on mobile, we simply show the help message here */}
|
||||||
|
{helpMsg && (
|
||||||
|
<div className="block md:hidden mb-1">
|
||||||
|
<b>{label || configKey}</b>
|
||||||
|
<br />
|
||||||
|
<p className="text-xs">{helpMsg}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<label className="input input-bordered join-item grow flex items-center gap-2 mb-2">
|
||||||
|
<div className="dropdown dropdown-hover">
|
||||||
|
<div tabIndex={0} role="button" className="font-bold hidden md:block">
|
||||||
|
{label || configKey}
|
||||||
|
</div>
|
||||||
|
{helpMsg && (
|
||||||
|
<div className="dropdown-content menu bg-base-100 rounded-box z-10 w-64 p-2 shadow mt-4">
|
||||||
|
{helpMsg}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="grow"
|
||||||
|
placeholder={`Default: ${CONFIG_DEFAULT[configKey] || 'none'}`}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SettingsModalCheckbox({
|
||||||
|
configKey,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
label,
|
||||||
|
}: {
|
||||||
|
configKey: SettKey;
|
||||||
|
value: boolean;
|
||||||
|
onChange: (value: boolean) => void;
|
||||||
|
label: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-row items-center mb-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="checkbox"
|
||||||
|
checked={value}
|
||||||
|
onChange={(e) => onChange(e.target.checked)}
|
||||||
|
/>
|
||||||
|
<span className="ml-4">{label || configKey}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue