From 5abf06c1b18e52cbd2156895970af34096095ab3 Mon Sep 17 00:00:00 2001 From: Oleg Shulyakov Date: Sat, 16 Aug 2025 02:21:27 +0300 Subject: [PATCH 01/10] feat: re-organize settings dialog --- src/components/SettingDialog.tsx | 295 ++++++++++++++++++++++--------- 1 file changed, 214 insertions(+), 81 deletions(-) diff --git a/src/components/SettingDialog.tsx b/src/components/SettingDialog.tsx index f02dbd6..191b77b 100644 --- a/src/components/SettingDialog.tsx +++ b/src/components/SettingDialog.tsx @@ -1,9 +1,15 @@ import { BeakerIcon, + ChatBubbleLeftEllipsisIcon, + ChatBubbleLeftRightIcon, ChatBubbleOvalLeftEllipsisIcon, + CircleStackIcon, Cog6ToothIcon, + CogIcon, + CpuChipIcon, FunnelIcon, HandRaisedIcon, + RocketLaunchIcon, SquaresPlusIcon, } from '@heroicons/react/24/outline'; import { useState } from 'react'; @@ -16,7 +22,7 @@ import { useModals } from './ModalProvider'; type SettKey = keyof typeof CONFIG_DEFAULT; -const BASIC_KEYS: SettKey[] = [ +const GENERATION_KEYS: SettKey[] = [ 'temperature', 'top_k', 'top_p', @@ -46,6 +52,8 @@ enum SettingInputType { LONG_INPUT, CHECKBOX, CUSTOM, + SECTION, + SPACE, } interface SettingFieldInput { @@ -67,13 +75,29 @@ interface SettingFieldCustom { } interface SettingSection { + type: SettingInputType.SECTION; + label: string | React.ReactElement; +} + +interface SettingSpace { + type: SettingInputType.SPACE; +} + +interface SettingTab { title: React.ReactElement; - fields: (SettingFieldInput | SettingFieldCustom)[]; + fields: ( + | SettingFieldInput + | SettingFieldCustom + | SettingSection + | SettingSpace + )[]; } const ICON_CLASSNAME = 'w-4 h-4 mr-1 inline'; +const TITLE_ICON_CLASSNAME = 'w-4 h-4 mr-1 inline'; -const SETTING_SECTIONS: SettingSection[] = [ +const SETTING_TABS: SettingTab[] = [ + /* General */ { title: ( <> @@ -97,14 +121,27 @@ const SETTING_SECTIONS: SettingSection[] = [ label: 'System Message (will be disabled if left empty)', key: 'systemMessage', }, - ...BASIC_KEYS.map( - (key) => - ({ - type: SettingInputType.SHORT_INPUT, - label: key, - key, - }) as SettingFieldInput - ), + ], + }, + + /* Conversations */ + { + title: ( + <> + + Conversations + + ), + fields: [ + { + type: SettingInputType.SECTION, + label: ( + <> + + Chat + + ), + }, { type: SettingInputType.SHORT_INPUT, label: 'Paste length to file', @@ -115,52 +152,39 @@ const SETTING_SECTIONS: SettingSection[] = [ label: 'Parse PDF as image instead of text', key: 'pdfAsImage', }, - ], - }, - { - title: ( - <> - - Samplers - - ), - fields: [ + + /* Performance */ { - type: SettingInputType.SHORT_INPUT, - label: 'Samplers queue', - key: 'samplers', + type: SettingInputType.SPACE, + }, + { + type: SettingInputType.SECTION, + label: ( + <> + + Performance + + ), + }, + { + type: SettingInputType.CHECKBOX, + label: 'Show performance metrics', + key: 'showTokensPerSecond', + }, + + /* Reasoning */ + { + type: SettingInputType.SPACE, + }, + { + type: SettingInputType.SECTION, + label: ( + <> + + Reasoning + + ), }, - ...SAMPLER_KEYS.map( - (key) => - ({ - type: SettingInputType.SHORT_INPUT, - label: key, - key, - }) as SettingFieldInput - ), - ], - }, - { - title: ( - <> - - Penalties - - ), - fields: PENALTY_KEYS.map((key) => ({ - type: SettingInputType.SHORT_INPUT, - label: key, - key, - })), - }, - { - title: ( - <> - - Reasoning - - ), - fields: [ { type: SettingInputType.CHECKBOX, label: 'Expand thought process by default when generating messages', @@ -174,11 +198,13 @@ const SETTING_SECTIONS: SettingSection[] = [ }, ], }, + + /* Import/Export */ { title: ( <> - - Advanced + + Import/Export ), fields: [ @@ -201,10 +227,98 @@ const SETTING_SECTIONS: SettingSection[] = [ ); }, }, + ], + }, + + /* Advanced */ + { + title: ( + <> + + Advanced + + ), + fields: [ + /* Generation */ { - type: SettingInputType.CHECKBOX, - label: 'Show tokens per second', - key: 'showTokensPerSecond', + type: SettingInputType.SECTION, + label: ( + <> + + Generation + + ), + }, + ...GENERATION_KEYS.map( + (key) => + ({ + type: SettingInputType.SHORT_INPUT, + label: key, + key, + }) as SettingFieldInput + ), + + /* Samplers */ + { + type: SettingInputType.SPACE, + }, + { + type: SettingInputType.SECTION, + label: ( + <> + + Samplers + + ), + }, + { + type: SettingInputType.SHORT_INPUT, + label: 'Samplers queue', + key: 'samplers', + }, + ...SAMPLER_KEYS.map( + (key) => + ({ + type: SettingInputType.SHORT_INPUT, + label: key, + key, + }) as SettingFieldInput + ), + + /* Penalties */ + { + type: SettingInputType.SPACE, + }, + { + type: SettingInputType.SECTION, + label: ( + <> + + Penalties + + ), + }, + ...PENALTY_KEYS.map( + (key) => + ({ + type: SettingInputType.SHORT_INPUT, + label: key, + key, + }) as SettingFieldInput + ), + + /* Custom */ + { + type: SettingInputType.SPACE, + }, + { + type: SettingInputType.SECTION, + label: ( + <> + + Custom + + ), }, { type: SettingInputType.LONG_INPUT, @@ -221,6 +335,8 @@ const SETTING_SECTIONS: SettingSection[] = [ }, ], }, + + /* Experimental */ { title: ( <> @@ -281,7 +397,7 @@ export default function SettingDialog({ onClose: () => void; }) { const { config, saveConfig } = useAppContext(); - const [sectionIdx, setSectionIdx] = useState(0); + const [tabIdx, setTabIdx] = useState(0); // clone the config object to prevent direct mutation const [localConfig, setLocalConfig] = useState( @@ -355,17 +471,17 @@ export default function SettingDialog({ aria-description="Settings sections" tabIndex={0} > - {SETTING_SECTIONS.map((section, idx) => ( + {SETTING_TABS.map((tab, idx) => ( ))} @@ -378,20 +494,20 @@ export default function SettingDialog({ >
- {SETTING_SECTIONS[sectionIdx].title} + {SETTING_TABS[tabIdx].title}
    - {SETTING_SECTIONS.map((section, idx) => ( + {SETTING_TABS.map((tab, idx) => (
    setSectionIdx(idx)} + onClick={() => setTabIdx(idx)} dir="auto" > - {section.title} + {tab.title}
    ))}
@@ -400,8 +516,8 @@ export default function SettingDialog({ {/* Right panel, showing setting fields */}
- {SETTING_SECTIONS[sectionIdx].fields.map((field, idx) => { - const key = `${sectionIdx}-${idx}`; + {SETTING_TABS[tabIdx].fields.map((field, idx) => { + const key = `${tabIdx}-${idx}`; if (field.type === SettingInputType.SHORT_INPUT) { return ( ); + } else if (field.type === SettingInputType.SECTION) { + return ( +
+

{field.label}

+
+ ); + } else if (field.type === SettingInputType.SPACE) { + return
; } })} @@ -543,15 +667,24 @@ function SettingsModalCheckbox({ onChange: (value: boolean) => void; label: string; }) { + const helpMsg = CONFIG_INFO[configKey]; return ( -
- onChange(e.target.checked)} - /> - {label || configKey} -
+ <> + {helpMsg && ( +
+

{helpMsg}

+
+ )} + +
+ onChange(e.target.checked)} + /> + {label || configKey} +
+ ); } From 72013b593733c203414b4c757d501d8a0a8a0ed2 Mon Sep 17 00:00:00 2001 From: Oleg Shulyakov Date: Sat, 16 Aug 2025 13:38:15 +0300 Subject: [PATCH 02/10] feat: add missed configuration descriptions --- src/components/SettingDialog.tsx | 7 +++---- src/config.ts | 9 ++++++++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/components/SettingDialog.tsx b/src/components/SettingDialog.tsx index 191b77b..6a08d03 100644 --- a/src/components/SettingDialog.tsx +++ b/src/components/SettingDialog.tsx @@ -149,7 +149,7 @@ const SETTING_TABS: SettingTab[] = [ }, { type: SettingInputType.CHECKBOX, - label: 'Parse PDF as image instead of text', + label: 'Use PDF as image instead of text', key: 'pdfAsImage', }, @@ -187,13 +187,12 @@ const SETTING_TABS: SettingTab[] = [ }, { type: SettingInputType.CHECKBOX, - label: 'Expand thought process by default when generating messages', + label: 'Expand thinking section', key: 'showThoughtInProgress', }, { type: SettingInputType.CHECKBOX, - label: - 'Exclude thought process when sending requests to API (Recommended for DeepSeek-R1)', + label: 'Exclude thinking messages on submit', key: 'excludeThoughtOnReq', }, ], diff --git a/src/config.ts b/src/config.ts index a47b513..9e863cf 100644 --- a/src/config.ts +++ b/src/config.ts @@ -40,6 +40,7 @@ export const CONFIG_DEFAULT = { dry_penalty_last_n: -1, max_tokens: -1, custom: '', // custom json-stringified object + // experimental features pyIntepreterEnabled: false, }; @@ -47,8 +48,14 @@ export const CONFIG_INFO: Record = { baseUrl: 'Set the Base URL if you are using standalone server.', 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.', + showTokensPerSecond: 'Enable to see processing speed, timings, etc.', + showThoughtInProgress: 'Expand thinking message when generating messages', + excludeThoughtOnReq: + 'Exclude thinking messages when sending requests to API (recommended)', pasteLongTextToFileLen: - 'On pasting long text, it will be converted to a file. You can control the file length by setting the value of this parameter. Value 0 means disable.', + 'On pasting long text, it will be converted to a file. You can control the file length by setting the value of this parameter. Set to 0 to disable.', + pdfAsImage: + 'Attach PDF as image instead of text. Supported only with multimodal models with vision support/', samplers: 'The order at which samplers are applied, in simplified way. Default is "dkypmxt": dry->top_k->typ_p->top_p->min_p->xtc->temperature', temperature: From a40c904318d0e9cc1df321a96c47be0517374a85 Mon Sep 17 00:00:00 2001 From: Oleg Shulyakov Date: Sat, 16 Aug 2025 13:41:48 +0300 Subject: [PATCH 03/10] style: increase pasteLongTextToFileLen default --- src/components/SettingDialog.tsx | 2 +- src/config.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/SettingDialog.tsx b/src/components/SettingDialog.tsx index 6a08d03..70f9614 100644 --- a/src/components/SettingDialog.tsx +++ b/src/components/SettingDialog.tsx @@ -144,7 +144,7 @@ const SETTING_TABS: SettingTab[] = [ }, { type: SettingInputType.SHORT_INPUT, - label: 'Paste length to file', + label: 'On Paste: convert to file if length >', key: 'pasteLongTextToFileLen', }, { diff --git a/src/config.ts b/src/config.ts index 9e863cf..ee5728e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -17,7 +17,7 @@ export const CONFIG_DEFAULT = { showTokensPerSecond: false, showThoughtInProgress: false, excludeThoughtOnReq: true, - pasteLongTextToFileLen: 2500, + pasteLongTextToFileLen: 10000, pdfAsImage: false, // make sure these default values are in sync with `common.h` samplers: 'edkypmxt', From eb004b60bde4257f3bafff5bc3c7079292c765d9 Mon Sep 17 00:00:00 2001 From: Oleg Shulyakov Date: Sat, 16 Aug 2025 14:00:40 +0300 Subject: [PATCH 04/10] style: introduce configuration type --- src/config.ts | 6 ++++-- src/utils/types.ts | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/config.ts b/src/config.ts index ee5728e..32f2cae 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,5 +1,6 @@ import daisyuiThemes from 'daisyui/theme/object'; import { isNumeric } from './utils/misc'; +import { Configuration, ConfigurationDetails } from './utils/types'; export const isDev = import.meta.env.MODE === 'development'; @@ -8,7 +9,7 @@ const baseUrl = new URL('.', document.baseURI).href .toString() .replace(/\/$/, ''); -export const CONFIG_DEFAULT = { +export const CONFIG_DEFAULT: Configuration = { // 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. baseUrl: baseUrl, @@ -44,7 +45,7 @@ export const CONFIG_DEFAULT = { // experimental features pyIntepreterEnabled: false, }; -export const CONFIG_INFO: Record = { +export const CONFIG_INFO: ConfigurationDetails = { baseUrl: 'Set the Base URL if you are using standalone server.', 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.', @@ -92,6 +93,7 @@ export const CONFIG_INFO: Record = { 'DRY sampling reduces repetition in generated text even across long contexts. This parameter sets DRY penalty for the last n tokens.', max_tokens: 'The maximum number of token per output.', custom: '', // custom json-stringified object + pyIntepreterEnabled: '', }; // config keys having numeric value (i.e. temperature, top_k, top_p, etc) export const CONFIG_NUMERIC_KEYS = Object.entries(CONFIG_DEFAULT) diff --git a/src/utils/types.ts b/src/utils/types.ts index ecc9b6a..2f66245 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -5,6 +5,40 @@ export interface TimingReport { predicted_ms: number; } +export interface Configuration { + baseUrl: string; + apiKey: string; + systemMessage: string; + showTokensPerSecond: boolean; + showThoughtInProgress: boolean; + excludeThoughtOnReq: boolean; + pasteLongTextToFileLen: number; + pdfAsImage: boolean; + 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; + pyIntepreterEnabled: boolean; +} + +export type ConfigurationDetails = Record; + /** * What is conversation "branching"? It is a feature that allows the user to edit an old message in the history, while still keeping the conversation flow. * Inspired by ChatGPT / Claude / Hugging Chat where you edit a message, a new branch of the conversation is created, and the old message is still visible. From bbc1809340fe2ddd454f1e113ceac74cbbbfa024 Mon Sep 17 00:00:00 2001 From: Oleg Shulyakov Date: Sat, 16 Aug 2025 16:16:40 +0300 Subject: [PATCH 05/10] refactor: extract configuration details --- src/components/SettingDialog.tsx | 173 ++++++++++++------------------- src/config.ts | 58 +---------- src/lang/en.json | 122 ++++++++++++++++++++++ src/utils/types.ts | 2 - 4 files changed, 191 insertions(+), 164 deletions(-) create mode 100644 src/lang/en.json diff --git a/src/components/SettingDialog.tsx b/src/components/SettingDialog.tsx index 70f9614..3f3c6ce 100644 --- a/src/components/SettingDialog.tsx +++ b/src/components/SettingDialog.tsx @@ -13,15 +13,18 @@ import { SquaresPlusIcon, } from '@heroicons/react/24/outline'; import { useState } from 'react'; -import { CONFIG_DEFAULT, CONFIG_INFO, isDev } from '../config'; +import { CONFIG_DEFAULT, isDev } from '../config'; +import * as messages from '../lang/en.json'; import { useAppContext } from '../utils/app.context'; import { OpenInNewTab } from '../utils/common'; import { classNames, isBoolean, isNumeric, isString } from '../utils/misc'; import StorageUtils from '../utils/storage'; +import { Configuration } from '../utils/types'; import { useModals } from './ModalProvider'; type SettKey = keyof typeof CONFIG_DEFAULT; +const GENERAL_KEYS: SettKey[] = ['baseUrl', 'apiKey']; const GENERATION_KEYS: SettKey[] = [ 'temperature', 'top_k', @@ -30,6 +33,7 @@ const GENERATION_KEYS: SettKey[] = [ 'max_tokens', ]; const SAMPLER_KEYS: SettKey[] = [ + 'samplers', 'dynatemp_range', 'dynatemp_exponent', 'typical_p', @@ -53,13 +57,18 @@ enum SettingInputType { CHECKBOX, CUSTOM, SECTION, - SPACE, + DELIMETER, } - +type SettingFieldInputType = Exclude< + SettingInputType, + | SettingInputType.CUSTOM + | SettingInputType.SECTION + | SettingInputType.DELIMETER +>; interface SettingFieldInput { - type: Exclude; + type: SettingFieldInputType; label: string | React.ReactElement; - help?: string | React.ReactElement; + note?: string | React.ReactElement; key: SettKey; } @@ -79,23 +88,35 @@ interface SettingSection { label: string | React.ReactElement; } -interface SettingSpace { - type: SettingInputType.SPACE; +interface SettingDelimeter { + type: SettingInputType.DELIMETER; } +type SettingField = + | SettingFieldInput + | SettingFieldCustom + | SettingSection + | SettingDelimeter; + interface SettingTab { title: React.ReactElement; - fields: ( - | SettingFieldInput - | SettingFieldCustom - | SettingSection - | SettingSpace - )[]; + fields: SettingField[]; } const ICON_CLASSNAME = 'w-4 h-4 mr-1 inline'; const TITLE_ICON_CLASSNAME = 'w-4 h-4 mr-1 inline'; +const toInput = ( + type: SettingFieldInputType, + key: keyof Configuration +): SettingFieldInput => { + return { + type, + ...messages.settings.parameters[key], + key, + }; +}; + const SETTING_TABS: SettingTab[] = [ /* General */ { @@ -106,21 +127,8 @@ const SETTING_TABS: SettingTab[] = [ ), fields: [ - { - type: SettingInputType.SHORT_INPUT, - label: 'Base URL', - key: 'baseUrl', - }, - { - type: SettingInputType.SHORT_INPUT, - label: 'API Key', - key: 'apiKey', - }, - { - type: SettingInputType.LONG_INPUT, - label: 'System Message (will be disabled if left empty)', - key: 'systemMessage', - }, + ...GENERAL_KEYS.map((key) => toInput(SettingInputType.SHORT_INPUT, key)), + toInput(SettingInputType.LONG_INPUT, 'systemMessage'), ], }, @@ -142,20 +150,12 @@ const SETTING_TABS: SettingTab[] = [ ), }, - { - type: SettingInputType.SHORT_INPUT, - label: 'On Paste: convert to file if length >', - key: 'pasteLongTextToFileLen', - }, - { - type: SettingInputType.CHECKBOX, - label: 'Use PDF as image instead of text', - key: 'pdfAsImage', - }, + toInput(SettingInputType.SHORT_INPUT, 'pasteLongTextToFileLen'), + toInput(SettingInputType.CHECKBOX, 'pdfAsImage'), /* Performance */ { - type: SettingInputType.SPACE, + type: SettingInputType.DELIMETER, }, { type: SettingInputType.SECTION, @@ -166,15 +166,11 @@ const SETTING_TABS: SettingTab[] = [ ), }, - { - type: SettingInputType.CHECKBOX, - label: 'Show performance metrics', - key: 'showTokensPerSecond', - }, + toInput(SettingInputType.CHECKBOX, 'showTokensPerSecond'), /* Reasoning */ { - type: SettingInputType.SPACE, + type: SettingInputType.DELIMETER, }, { type: SettingInputType.SECTION, @@ -185,16 +181,8 @@ const SETTING_TABS: SettingTab[] = [ ), }, - { - type: SettingInputType.CHECKBOX, - label: 'Expand thinking section', - key: 'showThoughtInProgress', - }, - { - type: SettingInputType.CHECKBOX, - label: 'Exclude thinking messages on submit', - key: 'excludeThoughtOnReq', - }, + toInput(SettingInputType.CHECKBOX, 'showThoughtInProgress'), + toInput(SettingInputType.CHECKBOX, 'excludeThoughtOnReq'), ], }, @@ -248,18 +236,13 @@ const SETTING_TABS: SettingTab[] = [ ), }, - ...GENERATION_KEYS.map( - (key) => - ({ - type: SettingInputType.SHORT_INPUT, - label: key, - key, - }) as SettingFieldInput + ...GENERATION_KEYS.map((key) => + toInput(SettingInputType.SHORT_INPUT, key) ), /* Samplers */ { - type: SettingInputType.SPACE, + type: SettingInputType.DELIMETER, }, { type: SettingInputType.SECTION, @@ -270,23 +253,11 @@ const SETTING_TABS: SettingTab[] = [ ), }, - { - type: SettingInputType.SHORT_INPUT, - label: 'Samplers queue', - key: 'samplers', - }, - ...SAMPLER_KEYS.map( - (key) => - ({ - type: SettingInputType.SHORT_INPUT, - label: key, - key, - }) as SettingFieldInput - ), + ...SAMPLER_KEYS.map((key) => toInput(SettingInputType.SHORT_INPUT, key)), /* Penalties */ { - type: SettingInputType.SPACE, + type: SettingInputType.DELIMETER, }, { type: SettingInputType.SECTION, @@ -297,18 +268,11 @@ const SETTING_TABS: SettingTab[] = [ ), }, - ...PENALTY_KEYS.map( - (key) => - ({ - type: SettingInputType.SHORT_INPUT, - label: key, - key, - }) as SettingFieldInput - ), + ...PENALTY_KEYS.map((key) => toInput(SettingInputType.SHORT_INPUT, key)), /* Custom */ { - type: SettingInputType.SPACE, + type: SettingInputType.DELIMETER, }, { type: SettingInputType.SECTION, @@ -522,9 +486,9 @@ export default function SettingDialog({ ); } else if (field.type === SettingInputType.LONG_INPUT) { @@ -532,9 +496,9 @@ export default function SettingDialog({ ); } else if (field.type === SettingInputType.CHECKBOX) { @@ -542,9 +506,9 @@ export default function SettingDialog({ ); } else if (field.type === SettingInputType.CUSTOM) { @@ -564,7 +528,7 @@ export default function SettingDialog({

{field.label}

); - } else if (field.type === SettingInputType.SPACE) { + } else if (field.type === SettingInputType.DELIMETER) { return
; } })} @@ -593,18 +557,18 @@ export default function SettingDialog({ function SettingsModalLongInput({ configKey, + field, value, onChange, - label, }: { configKey: SettKey; + field: SettingFieldInput; value: string; onChange: (value: string) => void; - label?: string; }) { return (