open-webui/src/lib/components/chat/SettingsModal.svelte

660 lines
19 KiB
Svelte
Raw Normal View History

2023-10-20 18:40:09 +00:00
<script lang="ts">
2024-07-06 04:27:59 +00:00
import { getContext, tick } from 'svelte';
2024-03-01 09:18:07 +00:00
import { toast } from 'svelte-sonner';
2024-02-05 09:58:54 +00:00
import { models, settings, user } from '$lib/stores';
2024-07-06 04:27:59 +00:00
import { updateUserSettings } from '$lib/apis/users';
2024-05-24 09:17:48 +00:00
import { getModels as _getModels } from '$lib/apis';
2024-07-06 04:27:59 +00:00
import { goto } from '$app/navigation';
2023-12-27 06:10:22 +00:00
import Modal from '../common/Modal.svelte';
import Account from './Settings/Account.svelte';
2024-02-05 09:58:54 +00:00
import About from './Settings/About.svelte';
import General from './Settings/General.svelte';
import Interface from './Settings/Interface.svelte';
2024-02-11 10:21:06 +00:00
import Audio from './Settings/Audio.svelte';
2024-02-05 09:58:54 +00:00
import Chats from './Settings/Chats.svelte';
2024-05-17 00:25:48 +00:00
import User from '../icons/User.svelte';
import Personalization from './Settings/Personalization.svelte';
2024-11-06 02:50:58 +00:00
import SearchInput from '../layout/Sidebar/SearchInput.svelte';
2024-11-06 05:05:14 +00:00
import Search from '../icons/Search.svelte';
2023-10-22 23:40:17 +00:00
const i18n = getContext('i18n');
2023-10-08 22:38:42 +00:00
export let show = false;
2023-11-20 01:47:07 +00:00
2024-11-06 02:50:58 +00:00
interface SettingsTab {
id: string;
title: string;
keywords: string[];
}
const searchData: SettingsTab[] = [
2024-11-06 03:16:22 +00:00
{
id: 'general',
title: 'General',
keywords: [
'general',
'theme',
'language',
'notifications',
'system',
'systemprompt',
'prompt',
'advanced',
'settings',
'defaultsettings',
'configuration',
'systemsettings',
'notificationsettings',
'systempromptconfig',
'languageoptions',
'defaultparameters',
'systemparameters'
]
},
{
id: 'interface',
title: 'Interface',
keywords: [
'defaultmodel',
'selectmodel',
'ui',
'userinterface',
'display',
'layout',
'design',
'landingpage',
'landingpagemode',
'default',
'chat',
'chatbubble',
'chatui',
'username',
'showusername',
'displayusername',
'widescreen',
'widescreenmode',
'fullscreen',
'expandmode',
'chatdirection',
'lefttoright',
'ltr',
'righttoleft',
'rtl',
'notifications',
'toast',
'toastnotifications',
'largechunks',
'streamlargechunks',
'scroll',
'scrollonbranchchange',
'scrollbehavior',
'richtext',
'richtextinput',
'background',
'chatbackground',
'chatbackgroundimage',
'backgroundimage',
'uploadbackground',
'resetbackground',
'titleautogen',
'titleautogeneration',
'autotitle',
'chattags',
'autochattags',
'responseautocopy',
'clipboard',
'location',
'userlocation',
'userlocationaccess',
'haptic',
'hapticfeedback',
'vibration',
'voice',
'voicecontrol',
'voiceinterruption',
'call',
'emojis',
'displayemoji',
'save',
'interfaceoptions',
'interfacecustomization'
]
},
{
id: 'personalization',
title: 'Personalization',
keywords: [
'personalization',
'memory',
'personalize',
'preferences',
'profile',
'personalsettings',
'customsettings',
'userpreferences',
'accountpreferences'
]
},
{
id: 'audio',
title: 'Audio',
keywords: [
'audio',
'sound',
'soundsettings',
'audiocontrol',
'volume',
'speech',
'speechrecognition',
'stt',
'speechtotext',
'tts',
'texttospeech',
'playback',
'playbackspeed',
'voiceplayback',
'speechplayback',
'audiooutput',
'speechengine',
'voicecontrol',
'audioplayback',
'transcription',
'autotranscribe',
'autosend',
'speechsettings',
'audiovoice',
'voiceoptions',
'setvoice',
'nonlocalvoices',
'savesettings',
'audioconfig',
'speechconfig',
'voicerecognition',
'speechsynthesis',
'speechmode',
'voicespeed',
'speechrate',
'speechspeed',
'audioinput',
'audiofeatures',
'voicemodes'
]
},
{
id: 'chats',
title: 'Chats',
keywords: [
'chat',
'messages',
'conversations',
'chatsettings',
'history',
'chathistory',
'messagehistory',
'messagearchive',
'convo',
'chats',
'conversationhistory',
'exportmessages',
'chatactivity'
]
},
{
id: 'account',
title: 'Account',
keywords: [
'account',
'profile',
'security',
'privacy',
'settings',
'login',
'useraccount',
'userdata',
'api',
'apikey',
'userprofile',
'profiledetails',
'accountsettings',
'accountpreferences',
'securitysettings',
'privacysettings'
]
},
2024-11-11 02:44:52 +00:00
{
id: 'admin',
title: 'Admin',
keywords: [
'admin',
'administrator',
'adminsettings',
'adminpanel',
'systemadmin',
'administratoraccess',
'systemcontrol',
'manage',
'management',
'admincontrols',
'adminfeatures',
'usercontrol'
]
},
2024-11-06 03:16:22 +00:00
{
id: 'about',
title: 'About',
keywords: [
'about',
'info',
'information',
'version',
'documentation',
'help',
'support',
'details',
'aboutus',
'softwareinfo',
'timothyjaeryangbaek',
'openwebui',
2024-11-06 03:16:22 +00:00
'release',
'updates',
'updateinfo',
'versioninfo',
'aboutapp',
'terms',
'termsandconditions',
'contact',
'aboutpage'
]
}
2024-11-06 02:50:58 +00:00
];
let search = '';
let visibleTabs = searchData.map((tab) => tab.id);
let searchDebounceTimeout;
const searchSettings = (query: string): string[] => {
const lowerCaseQuery = query.toLowerCase().trim();
return searchData
.filter(
(tab) =>
tab.title.toLowerCase().includes(lowerCaseQuery) ||
tab.keywords.some((keyword) => keyword.includes(lowerCaseQuery))
)
.map((tab) => tab.id);
};
const searchDebounceHandler = () => {
clearTimeout(searchDebounceTimeout);
searchDebounceTimeout = setTimeout(() => {
visibleTabs = searchSettings(search);
if (visibleTabs.length > 0 && !visibleTabs.includes(selectedTab)) {
selectedTab = visibleTabs[0];
}
}, 100);
2024-11-06 02:50:58 +00:00
};
2023-11-20 01:47:07 +00:00
const saveSettings = async (updated) => {
console.log(updated);
await settings.set({ ...$settings, ...updated });
2024-05-24 09:17:48 +00:00
await models.set(await getModels());
2024-05-27 05:47:42 +00:00
await updateUserSettings(localStorage.token, { ui: $settings });
2023-11-20 01:47:07 +00:00
};
2023-10-22 23:40:17 +00:00
2024-05-24 09:17:48 +00:00
const getModels = async () => {
return await _getModels(localStorage.token);
2024-04-21 01:49:16 +00:00
};
2023-11-04 00:16:02 +00:00
let selectedTab = 'general';
// Function to handle sideways scrolling
2024-07-06 04:27:59 +00:00
const scrollHandler = (event) => {
const settingsTabsContainer = document.getElementById('settings-tabs-container');
if (settingsTabsContainer) {
2024-07-06 04:27:59 +00:00
event.preventDefault(); // Prevent default vertical scrolling
settingsTabsContainer.scrollLeft += event.deltaY; // Scroll sideways
}
};
2024-07-06 04:27:59 +00:00
const addScrollListener = async () => {
await tick();
const settingsTabsContainer = document.getElementById('settings-tabs-container');
if (settingsTabsContainer) {
settingsTabsContainer.addEventListener('wheel', scrollHandler);
}
2024-07-06 04:27:59 +00:00
};
const removeScrollListener = async () => {
await tick();
const settingsTabsContainer = document.getElementById('settings-tabs-container');
if (settingsTabsContainer) {
settingsTabsContainer.removeEventListener('wheel', scrollHandler);
}
};
2024-07-06 04:27:59 +00:00
$: if (show) {
addScrollListener();
} else {
removeScrollListener();
}
2023-10-08 22:38:42 +00:00
</script>
2024-11-06 05:05:14 +00:00
<Modal size="xl" bind:show>
2024-05-02 06:11:16 +00:00
<div class="text-gray-700 dark:text-gray-100">
2024-05-02 00:55:18 +00:00
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-1">
<div class=" text-lg font-medium self-center">{$i18n.t('Settings')}</div>
2023-10-20 18:40:09 +00:00
<button
class="self-center"
on:click={() => {
show = false;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
>
<path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
/>
</svg>
</button>
</div>
2024-11-06 05:05:14 +00:00
<div class="flex flex-col md:flex-row w-full px-4 pt-1 pb-4 md:space-x-4">
2023-10-22 22:37:06 +00:00
<div
id="settings-tabs-container"
2024-11-06 05:05:14 +00:00
class="tabs flex flex-row overflow-x-auto space-x-1 md:space-x-0 md:space-y-1 md:flex-col flex-1 md:flex-none md:w-40 dark:text-gray-200 text-xs text-left mb-1 md:mb-0 -translate-y-1"
2023-10-22 22:37:06 +00:00
>
2024-11-06 05:05:14 +00:00
<div class="hidden md:flex w-full rounded-xl px-1 -py-0.5 gap-2" id="settings-search">
<div class="self-center rounded-l-xl bg-transparent">
<Search className="size-3.5" />
2023-10-22 22:37:06 +00:00
</div>
2024-11-06 02:50:58 +00:00
<input
2024-11-06 05:05:14 +00:00
class="w-full py-1.5 text-xs bg-transparent dark:text-gray-300 outline-none"
2024-11-06 02:50:58 +00:00
bind:value={search}
2024-11-06 05:05:14 +00:00
on:input={searchDebounceHandler}
2024-11-06 02:50:58 +00:00
placeholder={$i18n.t('Search')}
/>
2024-11-06 05:05:14 +00:00
</div>
{#if visibleTabs.length > 0}
{#each visibleTabs as tabId (tabId)}
{#if tabId === 'general'}
<button
class="px-2.5 py-2 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition {selectedTab ===
'general'
? 'bg-gray-100 dark:bg-gray-800'
: ' hover:bg-gray-100 dark:hover:bg-gray-850'}"
on:click={() => {
selectedTab = 'general';
}}
>
<div class=" self-center mr-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M8.34 1.804A1 1 0 019.32 1h1.36a1 1 0 01.98.804l.295 1.473c.497.144.971.342 1.416.587l1.25-.834a1 1 0 011.262.125l.962.962a1 1 0 01.125 1.262l-.834 1.25c.245.445.443.919.587 1.416l1.473.294a1 1 0 01.804.98v1.361a1 1 0 01-.804.98l-1.473.295a6.95 6.95 0 01-.587 1.416l.834 1.25a1 1 0 01-.125 1.262l-.962.962a1 1 0 01-1.262.125l-1.25-.834a6.953 6.953 0 01-1.416.587l-.294 1.473a1 1 0 01-.98.804H9.32a1 1 0 01-.98-.804l-.295-1.473a6.957 6.957 0 01-1.416-.587l-1.25.834a1 1 0 01-1.262-.125l-.962-.962a1 1 0 01-.125-1.262l.834-1.25a6.957 6.957 0 01-.587-1.416l-1.473-.294A1 1 0 011 10.68V9.32a1 1 0 01.804-.98l1.473-.295c.144-.497.342-.971.587-1.416l-.834-1.25a1 1 0 01.125-1.262l.962-.962A1 1 0 015.38 3.03l1.25.834a6.957 6.957 0 011.416-.587l.294-1.473zM13 10a3 3 0 11-6 0 3 3 0 016 0z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class=" self-center">{$i18n.t('General')}</div>
</button>
{:else if tabId === 'interface'}
<button
class="px-2.5 py-2 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition {selectedTab ===
'interface'
? 'bg-gray-100 dark:bg-gray-800'
: ' hover:bg-gray-100 dark:hover:bg-gray-850'}"
on:click={() => {
selectedTab = 'interface';
}}
>
<div class=" self-center mr-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M2 4.25A2.25 2.25 0 0 1 4.25 2h7.5A2.25 2.25 0 0 1 14 4.25v5.5A2.25 2.25 0 0 1 11.75 12h-1.312c.1.128.21.248.328.36a.75.75 0 0 1 .234.545v.345a.75.75 0 0 1-.75.75h-4.5a.75.75 0 0 1-.75-.75v-.345a.75.75 0 0 1 .234-.545c.118-.111.228-.232.328-.36H4.25A2.25 2.25 0 0 1 2 9.75v-5.5Zm2.25-.75a.75.75 0 0 0-.75.75v4.5c0 .414.336.75.75.75h7.5a.75.75 0 0 0 .75-.75v-4.5a.75.75 0 0 0-.75-.75h-7.5Z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class=" self-center">{$i18n.t('Interface')}</div>
</button>
{:else if tabId === 'personalization'}
<button
class="px-2.5 py-2 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition {selectedTab ===
'personalization'
? 'bg-gray-100 dark:bg-gray-800'
: ' hover:bg-gray-100 dark:hover:bg-gray-850'}"
on:click={() => {
selectedTab = 'personalization';
}}
>
<div class=" self-center mr-2">
<User />
</div>
<div class=" self-center">{$i18n.t('Personalization')}</div>
</button>
{:else if tabId === 'audio'}
2024-11-06 02:50:58 +00:00
<button
class="px-2.5 py-2 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition {selectedTab ===
'audio'
2024-11-06 02:50:58 +00:00
? 'bg-gray-100 dark:bg-gray-800'
: ' hover:bg-gray-100 dark:hover:bg-gray-850'}"
on:click={() => {
selectedTab = 'audio';
2024-11-06 02:50:58 +00:00
}}
>
<div class=" self-center mr-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
2024-11-06 02:50:58 +00:00
fill="currentColor"
class="w-4 h-4"
>
<path
d="M7.557 2.066A.75.75 0 0 1 8 2.75v10.5a.75.75 0 0 1-1.248.56L3.59 11H2a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h1.59l3.162-2.81a.75.75 0 0 1 .805-.124ZM12.95 3.05a.75.75 0 1 0-1.06 1.06 5.5 5.5 0 0 1 0 7.78.75.75 0 1 0 1.06 1.06 7 7 0 0 0 0-9.9Z"
/>
<path
d="M10.828 5.172a.75.75 0 1 0-1.06 1.06 2.5 2.5 0 0 1 0 3.536.75.75 0 1 0 1.06 1.06 4 4 0 0 0 0-5.656Z"
/>
</svg>
</div>
<div class=" self-center">{$i18n.t('Audio')}</div>
</button>
{:else if tabId === 'chats'}
<button
class="px-2.5 py-2 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition {selectedTab ===
'chats'
? 'bg-gray-100 dark:bg-gray-800'
: ' hover:bg-gray-100 dark:hover:bg-gray-850'}"
on:click={() => {
selectedTab = 'chats';
}}
>
<div class=" self-center mr-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
2024-11-06 02:50:58 +00:00
>
<path
fill-rule="evenodd"
d="M8 2C4.262 2 1 4.57 1 8c0 1.86.98 3.486 2.455 4.566a3.472 3.472 0 0 1-.469 1.26.75.75 0 0 0 .713 1.14 6.961 6.961 0 0 0 3.06-1.06c.403.062.818.094 1.241.094 3.738 0 7-2.57 7-6s-3.262-6-7-6ZM5 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2Zm7-1a1 1 0 1 1-2 0 1 1 0 0 1 2 0ZM8 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
2024-11-06 02:50:58 +00:00
clip-rule="evenodd"
/>
</svg>
</div>
<div class=" self-center">{$i18n.t('Chats')}</div>
2024-11-06 02:50:58 +00:00
</button>
{:else if tabId === 'account'}
<button
class="px-2.5 py-2 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition {selectedTab ===
'account'
? 'bg-gray-100 dark:bg-gray-800'
: ' hover:bg-gray-100 dark:hover:bg-gray-850'}"
on:click={() => {
selectedTab = 'account';
}}
>
<div class=" self-center mr-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M15 8A7 7 0 1 1 1 8a7 7 0 0 1 14 0Zm-5-2a2 2 0 1 1-4 0 2 2 0 0 1 4 0ZM8 9c-1.825 0-3.422.977-4.295 2.437A5.49 5.49 0 0 0 8 13.5a5.49 5.49 0 0 0 4.294-2.063A4.997 4.997 0 0 0 8 9Z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class=" self-center">{$i18n.t('Account')}</div>
</button>
{:else if tabId === 'about'}
<button
class="px-2.5 py-2 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition {selectedTab ===
'about'
? 'bg-gray-100 dark:bg-gray-800'
: ' hover:bg-gray-100 dark:hover:bg-gray-850'}"
on:click={() => {
selectedTab = 'about';
}}
>
<div class=" self-center mr-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class=" self-center">{$i18n.t('About')}</div>
</button>
{:else if tabId === 'admin'}
{#if $user.role === 'admin'}
<button
class="px-2.5 py-2 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition {selectedTab ===
'admin'
? 'bg-gray-100 dark:bg-gray-800'
: ' hover:bg-gray-100 dark:hover:bg-gray-850'}"
on:click={async () => {
await goto('/admin/settings');
show = false;
}}
>
<div class=" self-center mr-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="size-4"
>
<path
fill-rule="evenodd"
d="M4.5 3.75a3 3 0 0 0-3 3v10.5a3 3 0 0 0 3 3h15a3 3 0 0 0 3-3V6.75a3 3 0 0 0-3-3h-15Zm4.125 3a2.25 2.25 0 1 0 0 4.5 2.25 2.25 0 0 0 0-4.5Zm-3.873 8.703a4.126 4.126 0 0 1 7.746 0 .75.75 0 0 1-.351.92 7.47 7.47 0 0 1-3.522.877 7.47 7.47 0 0 1-3.522-.877.75.75 0 0 1-.351-.92ZM15 8.25a.75.75 0 0 0 0 1.5h3.75a.75.75 0 0 0 0-1.5H15ZM14.25 12a.75.75 0 0 1 .75-.75h3.75a.75.75 0 0 1 0 1.5H15a.75.75 0 0 1-.75-.75Zm.75 2.25a.75.75 0 0 0 0 1.5h3.75a.75.75 0 0 0 0-1.5H15Z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class=" self-center">{$i18n.t('Admin Settings')}</div>
</button>
{/if}
2024-11-06 02:50:58 +00:00
{/if}
{/each}
{:else}
<div class="text-center text-gray-500 mt-4">
{$i18n.t('No results found')}
</div>
{/if}
2023-10-20 18:40:09 +00:00
</div>
2024-11-11 02:48:54 +00:00
<div class="flex-1 md:min-h-[32rem] max-h-[32rem]">
2023-11-04 00:16:02 +00:00
{#if selectedTab === 'general'}
2024-02-05 09:58:54 +00:00
<General
2024-05-24 09:17:48 +00:00
{getModels}
2024-02-05 09:58:54 +00:00
{saveSettings}
on:save={() => {
2024-03-14 13:38:05 +00:00
toast.success($i18n.t('Settings saved successfully!'));
2024-02-05 09:58:54 +00:00
}}
/>
{:else if selectedTab === 'interface'}
2024-02-05 09:58:54 +00:00
<Interface
{saveSettings}
2024-02-05 09:58:54 +00:00
on:save={() => {
2024-03-14 13:38:05 +00:00
toast.success($i18n.t('Settings saved successfully!'));
}}
2024-02-05 09:58:54 +00:00
/>
2024-05-17 00:25:48 +00:00
{:else if selectedTab === 'personalization'}
<Personalization
{saveSettings}
on:save={() => {
toast.success($i18n.t('Settings saved successfully!'));
}}
/>
2024-02-11 10:21:06 +00:00
{:else if selectedTab === 'audio'}
<Audio
2024-02-05 09:58:54 +00:00
{saveSettings}
on:save={() => {
2024-03-14 13:38:05 +00:00
toast.success($i18n.t('Settings saved successfully!'));
2023-11-05 21:25:23 +00:00
}}
2024-02-05 09:58:54 +00:00
/>
2023-12-27 00:35:01 +00:00
{:else if selectedTab === 'chats'}
2024-02-05 09:58:54 +00:00
<Chats {saveSettings} />
2023-12-29 08:12:30 +00:00
{:else if selectedTab === 'account'}
<Account
saveHandler={() => {
2024-03-14 13:38:05 +00:00
toast.success($i18n.t('Settings saved successfully!'));
2023-12-29 08:12:30 +00:00
}}
/>
2023-11-05 23:41:25 +00:00
{:else if selectedTab === 'about'}
2024-02-05 09:58:54 +00:00
<About />
2023-10-22 22:37:06 +00:00
{/if}
</div>
2023-10-20 18:40:09 +00:00
</div>
2023-10-08 22:38:42 +00:00
</div>
</Modal>
2023-11-05 03:00:17 +00:00
<style>
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
/* display: none; <- Crashes Chrome on hover */
-webkit-appearance: none;
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
}
2023-11-05 20:55:44 +00:00
.tabs::-webkit-scrollbar {
display: none; /* for Chrome, Safari and Opera */
}
.tabs {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
2023-11-05 03:00:17 +00:00
input[type='number'] {
-moz-appearance: textfield; /* Firefox */
}
</style>