SettingsModal.svelte 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017
  1. <script lang="ts">
  2. import { getContext, tick } from 'svelte';
  3. import { toast } from 'svelte-sonner';
  4. import { config, models, settings, user } from '$lib/stores';
  5. import { updateUserSettings } from '$lib/apis/users';
  6. import { getModels as _getModels } from '$lib/apis';
  7. import { goto } from '$app/navigation';
  8. import Modal from '../common/Modal.svelte';
  9. import Account from './Settings/Account.svelte';
  10. import About from './Settings/About.svelte';
  11. import General from './Settings/General.svelte';
  12. import Interface from './Settings/Interface.svelte';
  13. import Audio from './Settings/Audio.svelte';
  14. import Chats from './Settings/Chats.svelte';
  15. import User from '../icons/User.svelte';
  16. import Personalization from './Settings/Personalization.svelte';
  17. import Search from '../icons/Search.svelte';
  18. import XMark from '../icons/XMark.svelte';
  19. import Connections from './Settings/Connections.svelte';
  20. import Tools from './Settings/Tools.svelte';
  21. const i18n = getContext('i18n');
  22. export let show = false;
  23. interface SettingsTab {
  24. id: string;
  25. title: string;
  26. keywords: string[];
  27. }
  28. const searchData: SettingsTab[] = [
  29. {
  30. id: 'general',
  31. title: 'General',
  32. keywords: [
  33. 'advancedparams',
  34. 'advancedparameters',
  35. 'advanced params',
  36. 'advanced parameters',
  37. 'configuration',
  38. 'defaultparameters',
  39. 'default parameters',
  40. 'defaultsettings',
  41. 'default settings',
  42. 'general',
  43. 'keepalive',
  44. 'keep alive',
  45. 'languages',
  46. 'notifications',
  47. 'requestmode',
  48. 'request mode',
  49. 'systemparameters',
  50. 'system parameters',
  51. 'systemprompt',
  52. 'system prompt',
  53. 'systemsettings',
  54. 'system settings',
  55. 'theme',
  56. 'translate',
  57. 'webuisettings',
  58. 'webui settings'
  59. ]
  60. },
  61. {
  62. id: 'interface',
  63. title: 'Interface',
  64. keywords: [
  65. 'allow user location',
  66. 'allow voice interruption in call',
  67. 'allowuserlocation',
  68. 'allowvoiceinterruptionincall',
  69. 'always collapse codeblocks',
  70. 'always collapse code blocks',
  71. 'always expand details',
  72. 'always on web search',
  73. 'always play notification sound',
  74. 'alwayscollapsecodeblocks',
  75. 'alwaysexpanddetails',
  76. 'alwaysonwebsearch',
  77. 'alwaysplaynotificationsound',
  78. 'android',
  79. 'auto chat tags',
  80. 'auto copy response to clipboard',
  81. 'auto title',
  82. 'autochattags',
  83. 'autocopyresponsetoclipboard',
  84. 'autotitle',
  85. 'beta',
  86. 'call',
  87. 'chat background image',
  88. 'chat bubble ui',
  89. 'chat direction',
  90. 'chat tags autogen',
  91. 'chat tags autogeneration',
  92. 'chat ui',
  93. 'chatbackgroundimage',
  94. 'chatbubbleui',
  95. 'chatdirection',
  96. 'chat tags autogeneration',
  97. 'chattagsautogeneration',
  98. 'chatui',
  99. 'copy formatted text',
  100. 'copyformattedtext',
  101. 'default model',
  102. 'defaultmodel',
  103. 'design',
  104. 'detect artifacts automatically',
  105. 'detectartifactsautomatically',
  106. 'display emoji in call',
  107. 'display username',
  108. 'displayemojiincall',
  109. 'displayusername',
  110. 'enter key behavior',
  111. 'enterkeybehavior',
  112. 'expand mode',
  113. 'expandmode',
  114. 'file',
  115. 'followup autogeneration',
  116. 'followupautogeneration',
  117. 'fullscreen',
  118. 'fullwidthmode',
  119. 'full width mode',
  120. 'haptic feedback',
  121. 'hapticfeedback',
  122. 'high contrast mode',
  123. 'highcontrastmode',
  124. 'iframe sandbox allow forms',
  125. 'iframe sandbox allow same origin',
  126. 'iframesandboxallowforms',
  127. 'iframesandboxallowsameorigin',
  128. 'imagecompression',
  129. 'image compression',
  130. 'imagemaxcompressionsize',
  131. 'image max compression size',
  132. 'interface customization',
  133. 'interface options',
  134. 'interfacecustomization',
  135. 'interfaceoptions',
  136. 'landing page mode',
  137. 'landingpagemode',
  138. 'layout',
  139. 'left to right',
  140. 'left-to-right',
  141. 'lefttoright',
  142. 'ltr',
  143. 'paste large text as file',
  144. 'pastelargetextasfile',
  145. 'reset background',
  146. 'resetbackground',
  147. 'response auto copy',
  148. 'responseautocopy',
  149. 'rich text input for chat',
  150. 'richtextinputforchat',
  151. 'right to left',
  152. 'right-to-left',
  153. 'righttoleft',
  154. 'rtl',
  155. 'scroll behavior',
  156. 'scroll on branch change',
  157. 'scrollbehavior',
  158. 'scrollonbranchchange',
  159. 'select model',
  160. 'selectmodel',
  161. 'settings',
  162. 'show username',
  163. 'showusername',
  164. 'stream large chunks',
  165. 'streamlargechunks',
  166. 'stylized pdf export',
  167. 'stylizedpdfexport',
  168. 'title autogeneration',
  169. 'titleautogeneration',
  170. 'toast notifications for new updates',
  171. 'toastnotificationsfornewupdates',
  172. 'upload background',
  173. 'uploadbackground',
  174. 'user interface',
  175. 'user location access',
  176. 'userinterface',
  177. 'userlocationaccess',
  178. 'vibration',
  179. 'voice control',
  180. 'voicecontrol',
  181. 'widescreen mode',
  182. 'widescreenmode',
  183. 'whatsnew',
  184. 'whats new',
  185. 'websearchinchat',
  186. 'web search in chat'
  187. ]
  188. },
  189. ...($user?.role === 'admin' ||
  190. ($user?.role === 'user' && $config?.features?.enable_direct_connections)
  191. ? [
  192. {
  193. id: 'connections',
  194. title: 'Connections',
  195. keywords: [
  196. 'addconnection',
  197. 'add connection',
  198. 'manageconnections',
  199. 'manage connections',
  200. 'manage direct connections',
  201. 'managedirectconnections',
  202. 'settings'
  203. ]
  204. }
  205. ]
  206. : []),
  207. ...($user?.role === 'admin' ||
  208. ($user?.role === 'user' &&
  209. $user?.permissions?.features?.direct_tool_servers &&
  210. $config?.features?.direct_tool_servers)
  211. ? [
  212. {
  213. id: 'tools',
  214. title: 'Tools',
  215. keywords: [
  216. 'addconnection',
  217. 'add connection',
  218. 'managetools',
  219. 'manage tools',
  220. 'manage tool servers',
  221. 'managetoolservers',
  222. 'settings'
  223. ]
  224. }
  225. ]
  226. : []),
  227. {
  228. id: 'personalization',
  229. title: 'Personalization',
  230. keywords: [
  231. 'account preferences',
  232. 'account settings',
  233. 'accountpreferences',
  234. 'accountsettings',
  235. 'custom settings',
  236. 'customsettings',
  237. 'experimental',
  238. 'memories',
  239. 'memory',
  240. 'personalization',
  241. 'personalize',
  242. 'personal settings',
  243. 'personalsettings',
  244. 'profile',
  245. 'user preferences',
  246. 'userpreferences'
  247. ]
  248. },
  249. {
  250. id: 'audio',
  251. title: 'Audio',
  252. keywords: [
  253. 'audio config',
  254. 'audio control',
  255. 'audio features',
  256. 'audio input',
  257. 'audio output',
  258. 'audio playback',
  259. 'audio voice',
  260. 'audioconfig',
  261. 'audiocontrol',
  262. 'audiofeatures',
  263. 'audioinput',
  264. 'audiooutput',
  265. 'audioplayback',
  266. 'audiovoice',
  267. 'auto playback response',
  268. 'autoplaybackresponse',
  269. 'auto transcribe',
  270. 'autotranscribe',
  271. 'instant auto send after voice transcription',
  272. 'instantautosendaftervoicetranscription',
  273. 'language',
  274. 'non local voices',
  275. 'nonlocalvoices',
  276. 'save settings',
  277. 'savesettings',
  278. 'set voice',
  279. 'setvoice',
  280. 'sound settings',
  281. 'soundsettings',
  282. 'speech config',
  283. 'speech mode',
  284. 'speech playback speed',
  285. 'speech rate',
  286. 'speech recognition',
  287. 'speech settings',
  288. 'speech speed',
  289. 'speech synthesis',
  290. 'speech to text engine',
  291. 'speechconfig',
  292. 'speechmode',
  293. 'speechplaybackspeed',
  294. 'speechrate',
  295. 'speechrecognition',
  296. 'speechsettings',
  297. 'speechspeed',
  298. 'speechsynthesis',
  299. 'speechtotextengine',
  300. 'speedch playback rate',
  301. 'speedchplaybackrate',
  302. 'stt settings',
  303. 'sttsettings',
  304. 'text to speech engine',
  305. 'text to speech',
  306. 'textospeechengine',
  307. 'texttospeech',
  308. 'texttospeechvoice',
  309. 'text to speech voice',
  310. 'voice control',
  311. 'voice modes',
  312. 'voice options',
  313. 'voice playback',
  314. 'voice recognition',
  315. 'voice speed',
  316. 'voicecontrol',
  317. 'voicemodes',
  318. 'voiceoptions',
  319. 'voiceplayback',
  320. 'voicerecognition',
  321. 'voicespeed',
  322. 'volume'
  323. ]
  324. },
  325. {
  326. id: 'chats',
  327. title: 'Chats',
  328. keywords: [
  329. 'archive all chats',
  330. 'archive chats',
  331. 'archiveallchats',
  332. 'archivechats',
  333. 'archived chats',
  334. 'archivedchats',
  335. 'chat activity',
  336. 'chat history',
  337. 'chat settings',
  338. 'chatactivity',
  339. 'chathistory',
  340. 'chatsettings',
  341. 'conversation activity',
  342. 'conversation history',
  343. 'conversationactivity',
  344. 'conversationhistory',
  345. 'conversations',
  346. 'convos',
  347. 'delete all chats',
  348. 'delete chats',
  349. 'deleteallchats',
  350. 'deletechats',
  351. 'export chats',
  352. 'exportchats',
  353. 'import chats',
  354. 'importchats',
  355. 'message activity',
  356. 'message archive',
  357. 'message history',
  358. 'messagearchive',
  359. 'messagehistory'
  360. ]
  361. },
  362. {
  363. id: 'account',
  364. title: 'Account',
  365. keywords: [
  366. 'account preferences',
  367. 'account settings',
  368. 'accountpreferences',
  369. 'accountsettings',
  370. 'api keys',
  371. 'apikeys',
  372. 'change password',
  373. 'changepassword',
  374. 'jwt token',
  375. 'jwttoken',
  376. 'login',
  377. 'new password',
  378. 'newpassword',
  379. 'notification webhook url',
  380. 'notificationwebhookurl',
  381. 'personal settings',
  382. 'personalsettings',
  383. 'privacy settings',
  384. 'privacysettings',
  385. 'profileavatar',
  386. 'profile avatar',
  387. 'profile details',
  388. 'profile image',
  389. 'profile picture',
  390. 'profiledetails',
  391. 'profileimage',
  392. 'profilepicture',
  393. 'security settings',
  394. 'securitysettings',
  395. 'update account',
  396. 'update password',
  397. 'updateaccount',
  398. 'updatepassword',
  399. 'user account',
  400. 'user data',
  401. 'user preferences',
  402. 'user profile',
  403. 'useraccount',
  404. 'userdata',
  405. 'username',
  406. 'userpreferences',
  407. 'userprofile',
  408. 'webhook url',
  409. 'webhookurl'
  410. ]
  411. },
  412. {
  413. id: 'about',
  414. title: 'About',
  415. keywords: [
  416. 'about app',
  417. 'about me',
  418. 'about open webui',
  419. 'about page',
  420. 'about us',
  421. 'aboutapp',
  422. 'aboutme',
  423. 'aboutopenwebui',
  424. 'aboutpage',
  425. 'aboutus',
  426. 'check for updates',
  427. 'checkforupdates',
  428. 'contact',
  429. 'copyright',
  430. 'details',
  431. 'discord',
  432. 'documentation',
  433. 'github',
  434. 'help',
  435. 'information',
  436. 'license',
  437. 'redistributions',
  438. 'release',
  439. 'see whats new',
  440. 'seewhatsnew',
  441. 'settings',
  442. 'software info',
  443. 'softwareinfo',
  444. 'support',
  445. 'terms and conditions',
  446. 'terms of use',
  447. 'termsandconditions',
  448. 'termsofuse',
  449. 'timothy jae ryang baek',
  450. 'timothy j baek',
  451. 'timothyjaeryangbaek',
  452. 'timothyjbaek',
  453. 'twitter',
  454. 'update info',
  455. 'updateinfo',
  456. 'version info',
  457. 'versioninfo'
  458. ]
  459. }
  460. ];
  461. let search = '';
  462. let visibleTabs = searchData.map((tab) => tab.id);
  463. let searchDebounceTimeout;
  464. const searchSettings = (query: string): string[] => {
  465. const lowerCaseQuery = query.toLowerCase().trim();
  466. return searchData
  467. .filter(
  468. (tab) =>
  469. tab.title.toLowerCase().includes(lowerCaseQuery) ||
  470. tab.keywords.some((keyword) => keyword.includes(lowerCaseQuery))
  471. )
  472. .map((tab) => tab.id);
  473. };
  474. const searchDebounceHandler = () => {
  475. clearTimeout(searchDebounceTimeout);
  476. searchDebounceTimeout = setTimeout(() => {
  477. visibleTabs = searchSettings(search);
  478. if (visibleTabs.length > 0 && !visibleTabs.includes(selectedTab)) {
  479. selectedTab = visibleTabs[0];
  480. }
  481. }, 100);
  482. };
  483. const saveSettings = async (updated) => {
  484. console.log(updated);
  485. await settings.set({ ...$settings, ...updated });
  486. await models.set(await getModels());
  487. await updateUserSettings(localStorage.token, { ui: $settings });
  488. };
  489. const getModels = async () => {
  490. return await _getModels(
  491. localStorage.token,
  492. $config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
  493. );
  494. };
  495. let selectedTab = 'general';
  496. // Function to handle sideways scrolling
  497. const scrollHandler = (event) => {
  498. const settingsTabsContainer = document.getElementById('settings-tabs-container');
  499. if (settingsTabsContainer) {
  500. event.preventDefault(); // Prevent default vertical scrolling
  501. settingsTabsContainer.scrollLeft += event.deltaY; // Scroll sideways
  502. }
  503. };
  504. const addScrollListener = async () => {
  505. await tick();
  506. const settingsTabsContainer = document.getElementById('settings-tabs-container');
  507. if (settingsTabsContainer) {
  508. settingsTabsContainer.addEventListener('wheel', scrollHandler);
  509. }
  510. };
  511. const removeScrollListener = async () => {
  512. await tick();
  513. const settingsTabsContainer = document.getElementById('settings-tabs-container');
  514. if (settingsTabsContainer) {
  515. settingsTabsContainer.removeEventListener('wheel', scrollHandler);
  516. }
  517. };
  518. $: if (show) {
  519. addScrollListener();
  520. } else {
  521. removeScrollListener();
  522. }
  523. </script>
  524. <Modal size="xl" bind:show>
  525. <div class="text-gray-700 dark:text-gray-100">
  526. <div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-1">
  527. <div class=" text-lg font-medium self-center">{$i18n.t('Settings')}</div>
  528. <button
  529. aria-label={$i18n.t('Close settings modal')}
  530. class="self-center"
  531. on:click={() => {
  532. show = false;
  533. }}
  534. >
  535. <XMark className="w-5 h-5"></XMark>
  536. </button>
  537. </div>
  538. <div class="flex flex-col md:flex-row w-full px-4 pt-1 pb-4 md:space-x-4">
  539. <div
  540. role="tablist"
  541. id="settings-tabs-container"
  542. class="tabs flex flex-row overflow-x-auto gap-2.5 md:gap-1 md:flex-col flex-1 md:flex-none md:w-40 md:min-h-[32rem] md:max-h-[32rem] dark:text-gray-200 text-sm font-medium text-left mb-1 md:mb-0 -translate-y-1"
  543. >
  544. <div class="hidden md:flex w-full rounded-xl -mb-1 px-0.5 gap-2" id="settings-search">
  545. <div class="self-center rounded-l-xl bg-transparent">
  546. <Search
  547. className="size-3.5"
  548. strokeWidth={($settings?.highContrastMode ?? false) ? '3' : '1.5'}
  549. />
  550. </div>
  551. <label class="sr-only" for="search-input-settings-modal">{$i18n.t('Search')}</label>
  552. <input
  553. class={`w-full py-1.5 text-sm bg-transparent dark:text-gray-300 outline-hidden
  554. ${($settings?.highContrastMode ?? false) ? 'placeholder-gray-800' : ''}`}
  555. bind:value={search}
  556. id="search-input-settings-modal"
  557. on:input={searchDebounceHandler}
  558. placeholder={$i18n.t('Search')}
  559. />
  560. </div>
  561. {#if visibleTabs.length > 0}
  562. {#each visibleTabs as tabId (tabId)}
  563. {#if tabId === 'general'}
  564. <button
  565. role="tab"
  566. aria-controls="tab-general"
  567. aria-selected={selectedTab === 'general'}
  568. class={`px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition
  569. ${
  570. selectedTab === 'general'
  571. ? ($settings?.highContrastMode ?? false)
  572. ? 'dark:bg-gray-800 bg-gray-200'
  573. : ''
  574. : ($settings?.highContrastMode ?? false)
  575. ? 'hover:bg-gray-200 dark:hover:bg-gray-800'
  576. : 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'
  577. }`}
  578. on:click={() => {
  579. selectedTab = 'general';
  580. }}
  581. >
  582. <div class=" self-center mr-2">
  583. <svg
  584. xmlns="http://www.w3.org/2000/svg"
  585. aria-hidden="true"
  586. viewBox="0 0 20 20"
  587. fill="currentColor"
  588. class="w-4 h-4"
  589. >
  590. <path
  591. fill-rule="evenodd"
  592. 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"
  593. clip-rule="evenodd"
  594. />
  595. </svg>
  596. </div>
  597. <div class=" self-center">{$i18n.t('General')}</div>
  598. </button>
  599. {:else if tabId === 'interface'}
  600. <button
  601. role="tab"
  602. aria-controls="tab-interface"
  603. aria-selected={selectedTab === 'interface'}
  604. class={`px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition
  605. ${
  606. selectedTab === 'interface'
  607. ? ($settings?.highContrastMode ?? false)
  608. ? 'dark:bg-gray-800 bg-gray-200'
  609. : ''
  610. : ($settings?.highContrastMode ?? false)
  611. ? 'hover:bg-gray-200 dark:hover:bg-gray-800'
  612. : 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'
  613. }`}
  614. on:click={() => {
  615. selectedTab = 'interface';
  616. }}
  617. >
  618. <div class=" self-center mr-2">
  619. <svg
  620. xmlns="http://www.w3.org/2000/svg"
  621. aria-hidden="true"
  622. viewBox="0 0 16 16"
  623. fill="currentColor"
  624. class="w-4 h-4"
  625. >
  626. <path
  627. fill-rule="evenodd"
  628. 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"
  629. clip-rule="evenodd"
  630. />
  631. </svg>
  632. </div>
  633. <div class=" self-center">{$i18n.t('Interface')}</div>
  634. </button>
  635. {:else if tabId === 'connections'}
  636. {#if $user?.role === 'admin' || ($user?.role === 'user' && $config?.features?.enable_direct_connections)}
  637. <button
  638. role="tab"
  639. aria-controls="tab-connections"
  640. aria-selected={selectedTab === 'connections'}
  641. class={`px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition
  642. ${
  643. selectedTab === 'connections'
  644. ? ($settings?.highContrastMode ?? false)
  645. ? 'dark:bg-gray-800 bg-gray-200'
  646. : ''
  647. : ($settings?.highContrastMode ?? false)
  648. ? 'hover:bg-gray-200 dark:hover:bg-gray-800'
  649. : 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'
  650. }`}
  651. on:click={() => {
  652. selectedTab = 'connections';
  653. }}
  654. >
  655. <div class=" self-center mr-2">
  656. <svg
  657. xmlns="http://www.w3.org/2000/svg"
  658. aria-hidden="true"
  659. viewBox="0 0 16 16"
  660. fill="currentColor"
  661. class="w-4 h-4"
  662. >
  663. <path
  664. d="M1 9.5A3.5 3.5 0 0 0 4.5 13H12a3 3 0 0 0 .917-5.857 2.503 2.503 0 0 0-3.198-3.019 3.5 3.5 0 0 0-6.628 2.171A3.5 3.5 0 0 0 1 9.5Z"
  665. />
  666. </svg>
  667. </div>
  668. <div class=" self-center">{$i18n.t('Connections')}</div>
  669. </button>
  670. {/if}
  671. {:else if tabId === 'tools'}
  672. {#if $user?.role === 'admin' || ($user?.role === 'user' && $user?.permissions?.features?.direct_tool_servers && $config?.features?.direct_tool_servers)}
  673. <button
  674. role="tab"
  675. aria-controls="tab-tools"
  676. aria-selected={selectedTab === 'tools'}
  677. class={`px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition
  678. ${
  679. selectedTab === 'tools'
  680. ? ($settings?.highContrastMode ?? false)
  681. ? 'dark:bg-gray-800 bg-gray-200'
  682. : ''
  683. : ($settings?.highContrastMode ?? false)
  684. ? 'hover:bg-gray-200 dark:hover:bg-gray-800'
  685. : 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'
  686. }`}
  687. on:click={() => {
  688. selectedTab = 'tools';
  689. }}
  690. >
  691. <div class=" self-center mr-2">
  692. <svg
  693. xmlns="http://www.w3.org/2000/svg"
  694. aria-hidden="true"
  695. viewBox="0 0 24 24"
  696. fill="currentColor"
  697. class="size-4"
  698. >
  699. <path
  700. fill-rule="evenodd"
  701. d="M12 6.75a5.25 5.25 0 0 1 6.775-5.025.75.75 0 0 1 .313 1.248l-3.32 3.319c.063.475.276.934.641 1.299.365.365.824.578 1.3.64l3.318-3.319a.75.75 0 0 1 1.248.313 5.25 5.25 0 0 1-5.472 6.756c-1.018-.086-1.87.1-2.309.634L7.344 21.3A3.298 3.298 0 1 1 2.7 16.657l8.684-7.151c.533-.44.72-1.291.634-2.309A5.342 5.342 0 0 1 12 6.75ZM4.117 19.125a.75.75 0 0 1 .75-.75h.008a.75.75 0 0 1 .75.75v.008a.75.75 0 0 1-.75.75h-.008a.75.75 0 0 1-.75-.75v-.008Z"
  702. clip-rule="evenodd"
  703. />
  704. </svg>
  705. </div>
  706. <div class=" self-center">{$i18n.t('Tools')}</div>
  707. </button>
  708. {/if}
  709. {:else if tabId === 'personalization'}
  710. <button
  711. role="tab"
  712. aria-controls="tab-personalization"
  713. aria-selected={selectedTab === 'personalization'}
  714. class={`px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition
  715. ${
  716. selectedTab === 'personalization'
  717. ? ($settings?.highContrastMode ?? false)
  718. ? 'dark:bg-gray-800 bg-gray-200'
  719. : ''
  720. : ($settings?.highContrastMode ?? false)
  721. ? 'hover:bg-gray-200 dark:hover:bg-gray-800'
  722. : 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'
  723. }`}
  724. on:click={() => {
  725. selectedTab = 'personalization';
  726. }}
  727. >
  728. <div class=" self-center mr-2">
  729. <User />
  730. </div>
  731. <div class=" self-center">{$i18n.t('Personalization')}</div>
  732. </button>
  733. {:else if tabId === 'audio'}
  734. <button
  735. role="tab"
  736. aria-controls="tab-audio"
  737. aria-selected={selectedTab === 'audio'}
  738. class={`px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition
  739. ${
  740. selectedTab === 'audio'
  741. ? ($settings?.highContrastMode ?? false)
  742. ? 'dark:bg-gray-800 bg-gray-200'
  743. : ''
  744. : ($settings?.highContrastMode ?? false)
  745. ? 'hover:bg-gray-200 dark:hover:bg-gray-800'
  746. : 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'
  747. }`}
  748. on:click={() => {
  749. selectedTab = 'audio';
  750. }}
  751. >
  752. <div class=" self-center mr-2">
  753. <svg
  754. xmlns="http://www.w3.org/2000/svg"
  755. aria-hidden="true"
  756. viewBox="0 0 16 16"
  757. fill="currentColor"
  758. class="w-4 h-4"
  759. >
  760. <path
  761. 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"
  762. />
  763. <path
  764. 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"
  765. />
  766. </svg>
  767. </div>
  768. <div class=" self-center">{$i18n.t('Audio')}</div>
  769. </button>
  770. {:else if tabId === 'chats'}
  771. <button
  772. role="tab"
  773. aria-controls="tab-chats"
  774. aria-selected={selectedTab === 'chats'}
  775. class={`px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition
  776. ${
  777. selectedTab === 'chats'
  778. ? ($settings?.highContrastMode ?? false)
  779. ? 'dark:bg-gray-800 bg-gray-200'
  780. : ''
  781. : ($settings?.highContrastMode ?? false)
  782. ? 'hover:bg-gray-200 dark:hover:bg-gray-800'
  783. : 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'
  784. }`}
  785. on:click={() => {
  786. selectedTab = 'chats';
  787. }}
  788. >
  789. <div class=" self-center mr-2">
  790. <svg
  791. xmlns="http://www.w3.org/2000/svg"
  792. aria-hidden="true"
  793. viewBox="0 0 16 16"
  794. fill="currentColor"
  795. class="w-4 h-4"
  796. >
  797. <path
  798. fill-rule="evenodd"
  799. 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"
  800. clip-rule="evenodd"
  801. />
  802. </svg>
  803. </div>
  804. <div class=" self-center">{$i18n.t('Chats')}</div>
  805. </button>
  806. {:else if tabId === 'account'}
  807. <button
  808. role="tab"
  809. aria-controls="tab-account"
  810. aria-selected={selectedTab === 'account'}
  811. class={`px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition
  812. ${
  813. selectedTab === 'account'
  814. ? ($settings?.highContrastMode ?? false)
  815. ? 'dark:bg-gray-800 bg-gray-200'
  816. : ''
  817. : ($settings?.highContrastMode ?? false)
  818. ? 'hover:bg-gray-200 dark:hover:bg-gray-800'
  819. : 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'
  820. }`}
  821. on:click={() => {
  822. selectedTab = 'account';
  823. }}
  824. >
  825. <div class=" self-center mr-2">
  826. <svg
  827. xmlns="http://www.w3.org/2000/svg"
  828. aria-hidden="true"
  829. viewBox="0 0 16 16"
  830. fill="currentColor"
  831. class="w-4 h-4"
  832. >
  833. <path
  834. fill-rule="evenodd"
  835. 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"
  836. clip-rule="evenodd"
  837. />
  838. </svg>
  839. </div>
  840. <div class=" self-center">{$i18n.t('Account')}</div>
  841. </button>
  842. {:else if tabId === 'about'}
  843. <button
  844. role="tab"
  845. aria-controls="tab-about"
  846. aria-selected={selectedTab === 'about'}
  847. class={`px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition
  848. ${
  849. selectedTab === 'about'
  850. ? ($settings?.highContrastMode ?? false)
  851. ? 'dark:bg-gray-800 bg-gray-200'
  852. : ''
  853. : ($settings?.highContrastMode ?? false)
  854. ? 'hover:bg-gray-200 dark:hover:bg-gray-800'
  855. : 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'
  856. }`}
  857. on:click={() => {
  858. selectedTab = 'about';
  859. }}
  860. >
  861. <div class=" self-center mr-2">
  862. <svg
  863. xmlns="http://www.w3.org/2000/svg"
  864. aria-hidden="true"
  865. viewBox="0 0 20 20"
  866. fill="currentColor"
  867. class="w-4 h-4"
  868. >
  869. <path
  870. fill-rule="evenodd"
  871. 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"
  872. clip-rule="evenodd"
  873. />
  874. </svg>
  875. </div>
  876. <div class=" self-center">{$i18n.t('About')}</div>
  877. </button>
  878. {/if}
  879. {/each}
  880. {:else}
  881. <div class="text-center text-gray-500 mt-4">
  882. {$i18n.t('No results found')}
  883. </div>
  884. {/if}
  885. {#if $user?.role === 'admin'}
  886. <a
  887. href="/admin/settings"
  888. class="px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none md:mt-auto flex text-left transition {$settings?.highContrastMode
  889. ? 'hover:bg-gray-200 dark:hover:bg-gray-800'
  890. : 'text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
  891. on:click={async (e) => {
  892. e.preventDefault();
  893. await goto('/admin/settings');
  894. show = false;
  895. }}
  896. >
  897. <div class=" self-center mr-2">
  898. <svg
  899. xmlns="http://www.w3.org/2000/svg"
  900. aria-hidden="true"
  901. viewBox="0 0 24 24"
  902. fill="currentColor"
  903. class="size-4"
  904. >
  905. <path
  906. fill-rule="evenodd"
  907. 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"
  908. clip-rule="evenodd"
  909. />
  910. </svg>
  911. </div>
  912. <div class=" self-center">{$i18n.t('Admin Settings')}</div>
  913. </a>
  914. {/if}
  915. </div>
  916. <div class="flex-1 md:min-h-[32rem] max-h-[32rem]">
  917. {#if selectedTab === 'general'}
  918. <General
  919. {getModels}
  920. {saveSettings}
  921. on:save={() => {
  922. toast.success($i18n.t('Settings saved successfully!'));
  923. }}
  924. />
  925. {:else if selectedTab === 'interface'}
  926. <Interface
  927. {saveSettings}
  928. on:save={() => {
  929. toast.success($i18n.t('Settings saved successfully!'));
  930. }}
  931. />
  932. {:else if selectedTab === 'connections'}
  933. <Connections
  934. saveSettings={async (updated) => {
  935. await saveSettings(updated);
  936. toast.success($i18n.t('Settings saved successfully!'));
  937. }}
  938. />
  939. {:else if selectedTab === 'tools'}
  940. <Tools
  941. saveSettings={async (updated) => {
  942. await saveSettings(updated);
  943. toast.success($i18n.t('Settings saved successfully!'));
  944. }}
  945. />
  946. {:else if selectedTab === 'personalization'}
  947. <Personalization
  948. {saveSettings}
  949. on:save={() => {
  950. toast.success($i18n.t('Settings saved successfully!'));
  951. }}
  952. />
  953. {:else if selectedTab === 'audio'}
  954. <Audio
  955. {saveSettings}
  956. on:save={() => {
  957. toast.success($i18n.t('Settings saved successfully!'));
  958. }}
  959. />
  960. {:else if selectedTab === 'chats'}
  961. <Chats {saveSettings} />
  962. {:else if selectedTab === 'account'}
  963. <Account
  964. {saveSettings}
  965. saveHandler={() => {
  966. toast.success($i18n.t('Settings saved successfully!'));
  967. }}
  968. />
  969. {:else if selectedTab === 'about'}
  970. <About />
  971. {/if}
  972. </div>
  973. </div>
  974. </div>
  975. </Modal>
  976. <style>
  977. input::-webkit-outer-spin-button,
  978. input::-webkit-inner-spin-button {
  979. /* display: none; <- Crashes Chrome on hover */
  980. -webkit-appearance: none;
  981. margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
  982. }
  983. .tabs::-webkit-scrollbar {
  984. display: none; /* for Chrome, Safari and Opera */
  985. }
  986. .tabs {
  987. -ms-overflow-style: none; /* IE and Edge */
  988. scrollbar-width: none; /* Firefox */
  989. }
  990. input[type='number'] {
  991. appearance: textfield;
  992. -moz-appearance: textfield; /* Firefox */
  993. }
  994. </style>