Connections.svelte 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. <script lang="ts">
  2. import { models, user } from '$lib/stores';
  3. import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
  4. const dispatch = createEventDispatcher();
  5. import {
  6. getOllamaConfig,
  7. getOllamaUrls,
  8. getOllamaVersion,
  9. updateOllamaConfig,
  10. updateOllamaUrls
  11. } from '$lib/apis/ollama';
  12. import {
  13. getOpenAIConfig,
  14. getOpenAIKeys,
  15. getOpenAIModels,
  16. getOpenAIUrls,
  17. updateOpenAIConfig,
  18. updateOpenAIKeys,
  19. updateOpenAIUrls
  20. } from '$lib/apis/openai';
  21. import { toast } from 'svelte-sonner';
  22. import Switch from '$lib/components/common/Switch.svelte';
  23. import Spinner from '$lib/components/common/Spinner.svelte';
  24. import Tooltip from '$lib/components/common/Tooltip.svelte';
  25. const i18n = getContext('i18n');
  26. export let getModels: Function;
  27. // External
  28. let OLLAMA_BASE_URLS = [''];
  29. let OPENAI_API_KEYS = [''];
  30. let OPENAI_API_BASE_URLS = [''];
  31. let pipelineUrls = {};
  32. let ENABLE_OPENAI_API = null;
  33. let ENABLE_OLLAMA_API = null;
  34. const verifyOpenAIHandler = async (idx) => {
  35. OPENAI_API_BASE_URLS = await updateOpenAIUrls(localStorage.token, OPENAI_API_BASE_URLS);
  36. OPENAI_API_KEYS = await updateOpenAIKeys(localStorage.token, OPENAI_API_KEYS);
  37. const res = await getOpenAIModels(localStorage.token, idx).catch((error) => {
  38. toast.error(error);
  39. return null;
  40. });
  41. if (res) {
  42. toast.success($i18n.t('Server connection verified'));
  43. if (res.pipelines) {
  44. pipelineUrls[OPENAI_API_BASE_URLS[idx]] = true;
  45. }
  46. }
  47. await models.set(await getModels());
  48. };
  49. const verifyOllamaHandler = async (idx) => {
  50. OLLAMA_BASE_URLS = await updateOllamaUrls(localStorage.token, OLLAMA_BASE_URLS);
  51. const res = await getOllamaVersion(localStorage.token, idx).catch((error) => {
  52. toast.error(error);
  53. return null;
  54. });
  55. if (res) {
  56. toast.success($i18n.t('Server connection verified'));
  57. }
  58. await models.set(await getModels());
  59. };
  60. const updateOpenAIHandler = async () => {
  61. // Check if API KEYS length is same than API URLS length
  62. if (OPENAI_API_KEYS.length !== OPENAI_API_BASE_URLS.length) {
  63. // if there are more keys than urls, remove the extra keys
  64. if (OPENAI_API_KEYS.length > OPENAI_API_BASE_URLS.length) {
  65. OPENAI_API_KEYS = OPENAI_API_KEYS.slice(0, OPENAI_API_BASE_URLS.length);
  66. }
  67. // if there are more urls than keys, add empty keys
  68. if (OPENAI_API_KEYS.length < OPENAI_API_BASE_URLS.length) {
  69. const diff = OPENAI_API_BASE_URLS.length - OPENAI_API_KEYS.length;
  70. for (let i = 0; i < diff; i++) {
  71. OPENAI_API_KEYS.push('');
  72. }
  73. }
  74. }
  75. OPENAI_API_BASE_URLS = await updateOpenAIUrls(localStorage.token, OPENAI_API_BASE_URLS);
  76. OPENAI_API_KEYS = await updateOpenAIKeys(localStorage.token, OPENAI_API_KEYS);
  77. await models.set(await getModels());
  78. };
  79. const updateOllamaUrlsHandler = async () => {
  80. OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter((url) => url !== '');
  81. console.log(OLLAMA_BASE_URLS);
  82. if (OLLAMA_BASE_URLS.length === 0) {
  83. ENABLE_OLLAMA_API = false;
  84. await updateOllamaConfig(localStorage.token, ENABLE_OLLAMA_API);
  85. toast.info($i18n.t('Ollama API disabled'));
  86. } else {
  87. OLLAMA_BASE_URLS = await updateOllamaUrls(localStorage.token, OLLAMA_BASE_URLS);
  88. const ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => {
  89. toast.error(error);
  90. return null;
  91. });
  92. if (ollamaVersion) {
  93. toast.success($i18n.t('Server connection verified'));
  94. await models.set(await getModels());
  95. }
  96. }
  97. };
  98. onMount(async () => {
  99. if ($user.role === 'admin') {
  100. await Promise.all([
  101. (async () => {
  102. OLLAMA_BASE_URLS = await getOllamaUrls(localStorage.token);
  103. })(),
  104. (async () => {
  105. OPENAI_API_BASE_URLS = await getOpenAIUrls(localStorage.token);
  106. })(),
  107. (async () => {
  108. OPENAI_API_KEYS = await getOpenAIKeys(localStorage.token);
  109. })()
  110. ]);
  111. OPENAI_API_BASE_URLS.forEach(async (url, idx) => {
  112. const res = await getOpenAIModels(localStorage.token, idx);
  113. if (res.pipelines) {
  114. pipelineUrls[url] = true;
  115. }
  116. });
  117. const ollamaConfig = await getOllamaConfig(localStorage.token);
  118. const openaiConfig = await getOpenAIConfig(localStorage.token);
  119. ENABLE_OPENAI_API = openaiConfig.ENABLE_OPENAI_API;
  120. ENABLE_OLLAMA_API = ollamaConfig.ENABLE_OLLAMA_API;
  121. }
  122. });
  123. </script>
  124. <form
  125. class="flex flex-col h-full justify-between text-sm"
  126. on:submit|preventDefault={() => {
  127. updateOpenAIHandler();
  128. updateOllamaUrlsHandler();
  129. dispatch('save');
  130. }}
  131. >
  132. <div class="space-y-3 pr-1.5 overflow-y-scroll h-[24rem] max-h-[25rem]">
  133. {#if ENABLE_OPENAI_API !== null && ENABLE_OLLAMA_API !== null}
  134. <div class=" space-y-3">
  135. <div class="mt-2 space-y-2 pr-1.5">
  136. <div class="flex justify-between items-center text-sm">
  137. <div class=" font-medium">{$i18n.t('OpenAI API')}</div>
  138. <div class="mt-1">
  139. <Switch
  140. bind:state={ENABLE_OPENAI_API}
  141. on:change={async () => {
  142. updateOpenAIConfig(localStorage.token, ENABLE_OPENAI_API);
  143. }}
  144. />
  145. </div>
  146. </div>
  147. {#if ENABLE_OPENAI_API}
  148. <div class="flex flex-col gap-1">
  149. {#each OPENAI_API_BASE_URLS as url, idx}
  150. <div class="flex w-full gap-2">
  151. <div class="flex-1 relative">
  152. <input
  153. class="w-full rounded-lg py-2 px-4 {pipelineUrls[url]
  154. ? 'pr-8'
  155. : ''} text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  156. placeholder={$i18n.t('API Base URL')}
  157. bind:value={url}
  158. autocomplete="off"
  159. />
  160. {#if pipelineUrls[url]}
  161. <div class=" absolute top-2.5 right-2.5">
  162. <Tooltip content="Pipelines">
  163. <svg
  164. xmlns="http://www.w3.org/2000/svg"
  165. viewBox="0 0 24 24"
  166. fill="currentColor"
  167. class="size-4"
  168. >
  169. <path
  170. d="M11.644 1.59a.75.75 0 0 1 .712 0l9.75 5.25a.75.75 0 0 1 0 1.32l-9.75 5.25a.75.75 0 0 1-.712 0l-9.75-5.25a.75.75 0 0 1 0-1.32l9.75-5.25Z"
  171. />
  172. <path
  173. d="m3.265 10.602 7.668 4.129a2.25 2.25 0 0 0 2.134 0l7.668-4.13 1.37.739a.75.75 0 0 1 0 1.32l-9.75 5.25a.75.75 0 0 1-.71 0l-9.75-5.25a.75.75 0 0 1 0-1.32l1.37-.738Z"
  174. />
  175. <path
  176. d="m10.933 19.231-7.668-4.13-1.37.739a.75.75 0 0 0 0 1.32l9.75 5.25c.221.12.489.12.71 0l9.75-5.25a.75.75 0 0 0 0-1.32l-1.37-.738-7.668 4.13a2.25 2.25 0 0 1-2.134-.001Z"
  177. />
  178. </svg>
  179. </Tooltip>
  180. </div>
  181. {/if}
  182. </div>
  183. <div class="flex-1">
  184. <input
  185. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  186. placeholder={$i18n.t('API Key')}
  187. bind:value={OPENAI_API_KEYS[idx]}
  188. autocomplete="off"
  189. />
  190. </div>
  191. <div class="self-center flex items-center">
  192. {#if idx === 0}
  193. <button
  194. class="px-1"
  195. on:click={() => {
  196. OPENAI_API_BASE_URLS = [...OPENAI_API_BASE_URLS, ''];
  197. OPENAI_API_KEYS = [...OPENAI_API_KEYS, ''];
  198. }}
  199. type="button"
  200. >
  201. <svg
  202. xmlns="http://www.w3.org/2000/svg"
  203. viewBox="0 0 16 16"
  204. fill="currentColor"
  205. class="w-4 h-4"
  206. >
  207. <path
  208. d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
  209. />
  210. </svg>
  211. </button>
  212. {:else}
  213. <button
  214. class="px-1"
  215. on:click={() => {
  216. OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS.filter(
  217. (url, urlIdx) => idx !== urlIdx
  218. );
  219. OPENAI_API_KEYS = OPENAI_API_KEYS.filter((key, keyIdx) => idx !== keyIdx);
  220. }}
  221. type="button"
  222. >
  223. <svg
  224. xmlns="http://www.w3.org/2000/svg"
  225. viewBox="0 0 16 16"
  226. fill="currentColor"
  227. class="w-4 h-4"
  228. >
  229. <path d="M3.75 7.25a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z" />
  230. </svg>
  231. </button>
  232. {/if}
  233. </div>
  234. <div class="flex">
  235. <Tooltip content="Verify connection" className="self-start mt-0.5">
  236. <button
  237. class="self-center p-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-900 dark:hover:bg-gray-850 rounded-lg transition"
  238. on:click={() => {
  239. verifyOpenAIHandler(idx);
  240. }}
  241. type="button"
  242. >
  243. <svg
  244. xmlns="http://www.w3.org/2000/svg"
  245. viewBox="0 0 20 20"
  246. fill="currentColor"
  247. class="w-4 h-4"
  248. >
  249. <path
  250. fill-rule="evenodd"
  251. d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
  252. clip-rule="evenodd"
  253. />
  254. </svg>
  255. </button>
  256. </Tooltip>
  257. </div>
  258. </div>
  259. <div class=" mb-1 text-xs text-gray-400 dark:text-gray-500">
  260. {$i18n.t('WebUI will make requests to')}
  261. <span class=" text-gray-200">'{url}/models'</span>
  262. </div>
  263. {/each}
  264. </div>
  265. {/if}
  266. </div>
  267. </div>
  268. <hr class=" dark:border-gray-700" />
  269. <div class="pr-1.5 space-y-2">
  270. <div class="flex justify-between items-center text-sm">
  271. <div class=" font-medium">{$i18n.t('Ollama API')}</div>
  272. <div class="mt-1">
  273. <Switch
  274. bind:state={ENABLE_OLLAMA_API}
  275. on:change={async () => {
  276. updateOllamaConfig(localStorage.token, ENABLE_OLLAMA_API);
  277. if (OLLAMA_BASE_URLS.length === 0) {
  278. OLLAMA_BASE_URLS = [''];
  279. }
  280. }}
  281. />
  282. </div>
  283. </div>
  284. {#if ENABLE_OLLAMA_API}
  285. <div class="flex w-full gap-1.5">
  286. <div class="flex-1 flex flex-col gap-2">
  287. {#each OLLAMA_BASE_URLS as url, idx}
  288. <div class="flex gap-1.5">
  289. <input
  290. class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
  291. placeholder={$i18n.t('Enter URL (e.g. http://localhost:11434)')}
  292. bind:value={url}
  293. />
  294. <div class="self-center flex items-center">
  295. {#if idx === 0}
  296. <button
  297. class="px-1"
  298. on:click={() => {
  299. OLLAMA_BASE_URLS = [...OLLAMA_BASE_URLS, ''];
  300. }}
  301. type="button"
  302. >
  303. <svg
  304. xmlns="http://www.w3.org/2000/svg"
  305. viewBox="0 0 16 16"
  306. fill="currentColor"
  307. class="w-4 h-4"
  308. >
  309. <path
  310. d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
  311. />
  312. </svg>
  313. </button>
  314. {:else}
  315. <button
  316. class="px-1"
  317. on:click={() => {
  318. OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter(
  319. (url, urlIdx) => idx !== urlIdx
  320. );
  321. }}
  322. type="button"
  323. >
  324. <svg
  325. xmlns="http://www.w3.org/2000/svg"
  326. viewBox="0 0 16 16"
  327. fill="currentColor"
  328. class="w-4 h-4"
  329. >
  330. <path d="M3.75 7.25a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z" />
  331. </svg>
  332. </button>
  333. {/if}
  334. </div>
  335. <div class="flex">
  336. <Tooltip content="Verify connection" className="self-start mt-0.5">
  337. <button
  338. class="self-center p-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-900 dark:hover:bg-gray-850 rounded-lg transition"
  339. on:click={() => {
  340. verifyOllamaHandler(idx);
  341. }}
  342. type="button"
  343. >
  344. <svg
  345. xmlns="http://www.w3.org/2000/svg"
  346. viewBox="0 0 20 20"
  347. fill="currentColor"
  348. class="w-4 h-4"
  349. >
  350. <path
  351. fill-rule="evenodd"
  352. d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
  353. clip-rule="evenodd"
  354. />
  355. </svg>
  356. </button>
  357. </Tooltip>
  358. </div>
  359. </div>
  360. {/each}
  361. </div>
  362. </div>
  363. <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
  364. {$i18n.t('Trouble accessing Ollama?')}
  365. <a
  366. class=" text-gray-300 font-medium underline"
  367. href="https://github.com/open-webui/open-webui#troubleshooting"
  368. target="_blank"
  369. >
  370. {$i18n.t('Click here for help.')}
  371. </a>
  372. </div>
  373. {/if}
  374. </div>
  375. {:else}
  376. <div class="flex h-full justify-center">
  377. <div class="my-auto">
  378. <Spinner className="size-6" />
  379. </div>
  380. </div>
  381. {/if}
  382. </div>
  383. <div class="flex justify-end pt-3 text-sm font-medium">
  384. <button
  385. class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
  386. type="submit"
  387. >
  388. {$i18n.t('Save')}
  389. </button>
  390. </div>
  391. </form>