Images.svelte 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737
  1. <script lang="ts">
  2. import { toast } from 'svelte-sonner';
  3. import { createEventDispatcher, onMount, getContext } from 'svelte';
  4. import { config as backendConfig, user } from '$lib/stores';
  5. import { getBackendConfig } from '$lib/apis';
  6. import {
  7. getImageGenerationModels,
  8. getImageGenerationConfig,
  9. updateImageGenerationConfig,
  10. getConfig,
  11. updateConfig,
  12. verifyConfigUrl
  13. } from '$lib/apis/images';
  14. import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
  15. import Switch from '$lib/components/common/Switch.svelte';
  16. import Tooltip from '$lib/components/common/Tooltip.svelte';
  17. const dispatch = createEventDispatcher();
  18. const i18n = getContext('i18n');
  19. let loading = false;
  20. let config = null;
  21. let imageGenerationConfig = null;
  22. let models = null;
  23. let samplers = [
  24. 'DPM++ 2M',
  25. 'DPM++ SDE',
  26. 'DPM++ 2M SDE',
  27. 'DPM++ 2M SDE Heun',
  28. 'DPM++ 2S a',
  29. 'DPM++ 3M SDE',
  30. 'Euler a',
  31. 'Euler',
  32. 'LMS',
  33. 'Heun',
  34. 'DPM2',
  35. 'DPM2 a',
  36. 'DPM fast',
  37. 'DPM adaptive',
  38. 'Restart',
  39. 'DDIM',
  40. 'DDIM CFG++',
  41. 'PLMS',
  42. 'UniPC'
  43. ];
  44. let schedulers = [
  45. 'Automatic',
  46. 'Uniform',
  47. 'Karras',
  48. 'Exponential',
  49. 'Polyexponential',
  50. 'SGM Uniform',
  51. 'KL Optimal',
  52. 'Align Your Steps',
  53. 'Simple',
  54. 'Normal',
  55. 'DDIM',
  56. 'Beta'
  57. ];
  58. let requiredWorkflowNodes = [
  59. {
  60. type: 'prompt',
  61. key: 'text',
  62. node_ids: ''
  63. },
  64. {
  65. type: 'model',
  66. key: 'ckpt_name',
  67. node_ids: ''
  68. },
  69. {
  70. type: 'width',
  71. key: 'width',
  72. node_ids: ''
  73. },
  74. {
  75. type: 'height',
  76. key: 'height',
  77. node_ids: ''
  78. },
  79. {
  80. type: 'steps',
  81. key: 'steps',
  82. node_ids: ''
  83. },
  84. {
  85. type: 'seed',
  86. key: 'seed',
  87. node_ids: ''
  88. }
  89. ];
  90. const getModels = async () => {
  91. models = await getImageGenerationModels(localStorage.token).catch((error) => {
  92. toast.error(`${error}`);
  93. return null;
  94. });
  95. };
  96. const updateConfigHandler = async () => {
  97. const res = await updateConfig(localStorage.token, config)
  98. .catch((error) => {
  99. toast.error(`${error}`);
  100. return null;
  101. })
  102. .catch((error) => {
  103. toast.error(`${error}`);
  104. return null;
  105. });
  106. if (res) {
  107. config = res;
  108. }
  109. if (config.enabled) {
  110. backendConfig.set(await getBackendConfig());
  111. getModels();
  112. }
  113. };
  114. const validateJSON = (json) => {
  115. try {
  116. const obj = JSON.parse(json);
  117. if (obj && typeof obj === 'object') {
  118. return true;
  119. }
  120. } catch (e) {}
  121. return false;
  122. };
  123. const saveHandler = async () => {
  124. loading = true;
  125. if (config?.comfyui?.COMFYUI_WORKFLOW) {
  126. if (!validateJSON(config.comfyui.COMFYUI_WORKFLOW)) {
  127. toast.error('Invalid JSON format for ComfyUI Workflow.');
  128. loading = false;
  129. return;
  130. }
  131. }
  132. if (config?.comfyui?.COMFYUI_WORKFLOW) {
  133. config.comfyui.COMFYUI_WORKFLOW_NODES = requiredWorkflowNodes.map((node) => {
  134. return {
  135. type: node.type,
  136. key: node.key,
  137. node_ids:
  138. node.node_ids.trim() === '' ? [] : node.node_ids.split(',').map((id) => id.trim())
  139. };
  140. });
  141. }
  142. await updateConfig(localStorage.token, config).catch((error) => {
  143. toast.error(`${error}`);
  144. loading = false;
  145. return null;
  146. });
  147. await updateImageGenerationConfig(localStorage.token, imageGenerationConfig).catch((error) => {
  148. toast.error(`${error}`);
  149. loading = false;
  150. return null;
  151. });
  152. getModels();
  153. dispatch('save');
  154. loading = false;
  155. };
  156. onMount(async () => {
  157. if ($user.role === 'admin') {
  158. const res = await getConfig(localStorage.token).catch((error) => {
  159. toast.error(`${error}`);
  160. return null;
  161. });
  162. if (res) {
  163. config = res;
  164. }
  165. if (config.enabled) {
  166. getModels();
  167. }
  168. if (config.comfyui.COMFYUI_WORKFLOW) {
  169. config.comfyui.COMFYUI_WORKFLOW = JSON.stringify(
  170. JSON.parse(config.comfyui.COMFYUI_WORKFLOW),
  171. null,
  172. 2
  173. );
  174. }
  175. requiredWorkflowNodes = requiredWorkflowNodes.map((node) => {
  176. const n = config.comfyui.COMFYUI_WORKFLOW_NODES.find((n) => n.type === node.type) ?? node;
  177. console.log(n);
  178. return {
  179. type: n.type,
  180. key: n.key,
  181. node_ids: typeof n.node_ids === 'string' ? n.node_ids : n.node_ids.join(',')
  182. };
  183. });
  184. const imageConfigRes = await getImageGenerationConfig(localStorage.token).catch((error) => {
  185. toast.error(`${error}`);
  186. return null;
  187. });
  188. if (imageConfigRes) {
  189. imageGenerationConfig = imageConfigRes;
  190. }
  191. }
  192. });
  193. </script>
  194. <form
  195. class="flex flex-col h-full justify-between space-y-3 text-sm"
  196. on:submit|preventDefault={async () => {
  197. saveHandler();
  198. }}
  199. >
  200. <div class=" space-y-3 overflow-y-scroll scrollbar-hidden pr-2">
  201. {#if config && imageGenerationConfig}
  202. <div>
  203. <div class=" mb-1 text-sm font-medium">{$i18n.t('Image Settings')}</div>
  204. <div>
  205. <div class=" py-1 flex w-full justify-between">
  206. <div class=" self-center text-xs font-medium">
  207. {$i18n.t('Image Generation (Experimental)')}
  208. </div>
  209. <div class="px-1">
  210. <Switch
  211. bind:state={config.enabled}
  212. on:change={(e) => {
  213. const enabled = e.detail;
  214. if (enabled) {
  215. if (
  216. config.engine === 'automatic1111' &&
  217. config.automatic1111.AUTOMATIC1111_BASE_URL === ''
  218. ) {
  219. toast.error($i18n.t('AUTOMATIC1111 Base URL is required.'));
  220. config.enabled = false;
  221. } else if (
  222. config.engine === 'comfyui' &&
  223. config.comfyui.COMFYUI_BASE_URL === ''
  224. ) {
  225. toast.error($i18n.t('ComfyUI Base URL is required.'));
  226. config.enabled = false;
  227. } else if (config.engine === 'openai' && config.openai.OPENAI_API_KEY === '') {
  228. toast.error($i18n.t('OpenAI API Key is required.'));
  229. config.enabled = false;
  230. } else if (config.engine === 'gemini' && config.gemini.GEMINI_API_KEY === '') {
  231. toast.error($i18n.t('Gemini API Key is required.'));
  232. config.enabled = false;
  233. }
  234. }
  235. updateConfigHandler();
  236. }}
  237. />
  238. </div>
  239. </div>
  240. </div>
  241. {#if config.enabled}
  242. <div class=" py-1 flex w-full justify-between">
  243. <div class=" self-center text-xs font-medium">{$i18n.t('Image Prompt Generation')}</div>
  244. <div class="px-1">
  245. <Switch bind:state={config.prompt_generation} />
  246. </div>
  247. </div>
  248. {/if}
  249. <div class=" py-1 flex w-full justify-between">
  250. <div class=" self-center text-xs font-medium">{$i18n.t('Image Generation Engine')}</div>
  251. <div class="flex items-center relative">
  252. <select
  253. class=" dark:bg-gray-900 w-fit pr-8 cursor-pointer rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
  254. bind:value={config.engine}
  255. placeholder={$i18n.t('Select Engine')}
  256. on:change={async () => {
  257. updateConfigHandler();
  258. }}
  259. >
  260. <option value="openai">{$i18n.t('Default (Open AI)')}</option>
  261. <option value="comfyui">{$i18n.t('ComfyUI')}</option>
  262. <option value="automatic1111">{$i18n.t('Automatic1111')}</option>
  263. <option value="gemini">{$i18n.t('Gemini')}</option>
  264. </select>
  265. </div>
  266. </div>
  267. </div>
  268. <hr class=" border-gray-100 dark:border-gray-850" />
  269. <div class="flex flex-col gap-2">
  270. {#if (config?.engine ?? 'automatic1111') === 'automatic1111'}
  271. <div>
  272. <div class=" mb-2 text-sm font-medium">{$i18n.t('AUTOMATIC1111 Base URL')}</div>
  273. <div class="flex w-full">
  274. <div class="flex-1 mr-2">
  275. <input
  276. class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
  277. placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
  278. bind:value={config.automatic1111.AUTOMATIC1111_BASE_URL}
  279. />
  280. </div>
  281. <button
  282. class="px-2.5 bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
  283. type="button"
  284. on:click={async () => {
  285. await updateConfigHandler();
  286. const res = await verifyConfigUrl(localStorage.token).catch((error) => {
  287. toast.error(`${error}`);
  288. return null;
  289. });
  290. if (res) {
  291. toast.success($i18n.t('Server connection verified'));
  292. }
  293. }}
  294. >
  295. <svg
  296. xmlns="http://www.w3.org/2000/svg"
  297. viewBox="0 0 20 20"
  298. fill="currentColor"
  299. class="w-4 h-4"
  300. >
  301. <path
  302. fill-rule="evenodd"
  303. 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"
  304. clip-rule="evenodd"
  305. />
  306. </svg>
  307. </button>
  308. </div>
  309. <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
  310. {$i18n.t('Include `--api` flag when running stable-diffusion-webui')}
  311. <a
  312. class=" text-gray-300 font-medium"
  313. href="https://github.com/AUTOMATIC1111/stable-diffusion-webui/discussions/3734"
  314. target="_blank"
  315. >
  316. {$i18n.t('(e.g. `sh webui.sh --api`)')}
  317. </a>
  318. </div>
  319. </div>
  320. <div>
  321. <div class=" mb-2 text-sm font-medium">
  322. {$i18n.t('AUTOMATIC1111 Api Auth String')}
  323. </div>
  324. <SensitiveInput
  325. placeholder={$i18n.t('Enter api auth string (e.g. username:password)')}
  326. bind:value={config.automatic1111.AUTOMATIC1111_API_AUTH}
  327. required={false}
  328. />
  329. <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
  330. {$i18n.t('Include `--api-auth` flag when running stable-diffusion-webui')}
  331. <a
  332. class=" text-gray-300 font-medium"
  333. href="https://github.com/AUTOMATIC1111/stable-diffusion-webui/discussions/13993"
  334. target="_blank"
  335. >
  336. {$i18n
  337. .t('(e.g. `sh webui.sh --api --api-auth username_password`)')
  338. .replace('_', ':')}
  339. </a>
  340. </div>
  341. </div>
  342. <!---Sampler-->
  343. <div>
  344. <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Sampler')}</div>
  345. <div class="flex w-full">
  346. <div class="flex-1 mr-2">
  347. <Tooltip content={$i18n.t('Enter Sampler (e.g. Euler a)')} placement="top-start">
  348. <input
  349. list="sampler-list"
  350. class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
  351. placeholder={$i18n.t('Enter Sampler (e.g. Euler a)')}
  352. bind:value={config.automatic1111.AUTOMATIC1111_SAMPLER}
  353. />
  354. <datalist id="sampler-list">
  355. {#each samplers ?? [] as sampler}
  356. <option value={sampler}>{sampler}</option>
  357. {/each}
  358. </datalist>
  359. </Tooltip>
  360. </div>
  361. </div>
  362. </div>
  363. <!---Scheduler-->
  364. <div>
  365. <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Scheduler')}</div>
  366. <div class="flex w-full">
  367. <div class="flex-1 mr-2">
  368. <Tooltip content={$i18n.t('Enter Scheduler (e.g. Karras)')} placement="top-start">
  369. <input
  370. list="scheduler-list"
  371. class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
  372. placeholder={$i18n.t('Enter Scheduler (e.g. Karras)')}
  373. bind:value={config.automatic1111.AUTOMATIC1111_SCHEDULER}
  374. />
  375. <datalist id="scheduler-list">
  376. {#each schedulers ?? [] as scheduler}
  377. <option value={scheduler}>{scheduler}</option>
  378. {/each}
  379. </datalist>
  380. </Tooltip>
  381. </div>
  382. </div>
  383. </div>
  384. <!---CFG scale-->
  385. <div>
  386. <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set CFG Scale')}</div>
  387. <div class="flex w-full">
  388. <div class="flex-1 mr-2">
  389. <Tooltip content={$i18n.t('Enter CFG Scale (e.g. 7.0)')} placement="top-start">
  390. <input
  391. class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
  392. placeholder={$i18n.t('Enter CFG Scale (e.g. 7.0)')}
  393. bind:value={config.automatic1111.AUTOMATIC1111_CFG_SCALE}
  394. />
  395. </Tooltip>
  396. </div>
  397. </div>
  398. </div>
  399. {:else if config?.engine === 'comfyui'}
  400. <div class="">
  401. <div class=" mb-2 text-sm font-medium">{$i18n.t('ComfyUI Base URL')}</div>
  402. <div class="flex w-full">
  403. <div class="flex-1 mr-2">
  404. <input
  405. class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
  406. placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
  407. bind:value={config.comfyui.COMFYUI_BASE_URL}
  408. />
  409. </div>
  410. <button
  411. class="px-2.5 bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
  412. type="button"
  413. on:click={async () => {
  414. await updateConfigHandler();
  415. const res = await verifyConfigUrl(localStorage.token).catch((error) => {
  416. toast.error(`${error}`);
  417. return null;
  418. });
  419. if (res) {
  420. toast.success($i18n.t('Server connection verified'));
  421. }
  422. }}
  423. >
  424. <svg
  425. xmlns="http://www.w3.org/2000/svg"
  426. viewBox="0 0 20 20"
  427. fill="currentColor"
  428. class="w-4 h-4"
  429. >
  430. <path
  431. fill-rule="evenodd"
  432. 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"
  433. clip-rule="evenodd"
  434. />
  435. </svg>
  436. </button>
  437. </div>
  438. </div>
  439. <div class="">
  440. <div class=" mb-2 text-sm font-medium">{$i18n.t('ComfyUI API Key')}</div>
  441. <div class="flex w-full">
  442. <div class="flex-1 mr-2">
  443. <SensitiveInput
  444. placeholder={$i18n.t('sk-1234')}
  445. bind:value={config.comfyui.COMFYUI_API_KEY}
  446. required={false}
  447. />
  448. </div>
  449. </div>
  450. </div>
  451. <div class="">
  452. <div class=" mb-2 text-sm font-medium">{$i18n.t('ComfyUI Workflow')}</div>
  453. {#if config.comfyui.COMFYUI_WORKFLOW}
  454. <textarea
  455. class="w-full rounded-lg mb-1 py-2 px-4 text-xs bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden disabled:text-gray-600 resize-none"
  456. rows="10"
  457. bind:value={config.comfyui.COMFYUI_WORKFLOW}
  458. required
  459. />
  460. {/if}
  461. <div class="flex w-full">
  462. <div class="flex-1">
  463. <input
  464. id="upload-comfyui-workflow-input"
  465. hidden
  466. type="file"
  467. accept=".json"
  468. on:change={(e) => {
  469. const file = e.target.files[0];
  470. const reader = new FileReader();
  471. reader.onload = (e) => {
  472. config.comfyui.COMFYUI_WORKFLOW = e.target.result;
  473. e.target.value = null;
  474. };
  475. reader.readAsText(file);
  476. }}
  477. />
  478. <button
  479. class="w-full text-sm font-medium py-2 bg-transparent hover:bg-gray-100 border border-dashed dark:border-gray-850 dark:hover:bg-gray-850 text-center rounded-xl"
  480. type="button"
  481. on:click={() => {
  482. document.getElementById('upload-comfyui-workflow-input')?.click();
  483. }}
  484. >
  485. {$i18n.t('Click here to upload a workflow.json file.')}
  486. </button>
  487. </div>
  488. </div>
  489. <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
  490. {$i18n.t('Make sure to export a workflow.json file as API format from ComfyUI.')}
  491. </div>
  492. </div>
  493. {#if config.comfyui.COMFYUI_WORKFLOW}
  494. <div class="">
  495. <div class=" mb-2 text-sm font-medium">{$i18n.t('ComfyUI Workflow Nodes')}</div>
  496. <div class="text-xs flex flex-col gap-1.5">
  497. {#each requiredWorkflowNodes as node}
  498. <div class="flex w-full items-center border dark:border-gray-850 rounded-lg">
  499. <div class="shrink-0">
  500. <div
  501. class=" capitalize line-clamp-1 font-medium px-3 py-1 w-20 text-center rounded-l-lg bg-green-500/10 text-green-700 dark:text-green-200"
  502. >
  503. {node.type}{node.type === 'prompt' ? '*' : ''}
  504. </div>
  505. </div>
  506. <div class="">
  507. <Tooltip content="Input Key (e.g. text, unet_name, steps)">
  508. <input
  509. class="py-1 px-3 w-24 text-xs text-center bg-transparent outline-hidden border-r dark:border-gray-850"
  510. placeholder="Key"
  511. bind:value={node.key}
  512. required
  513. />
  514. </Tooltip>
  515. </div>
  516. <div class="w-full">
  517. <Tooltip
  518. content="Comma separated Node Ids (e.g. 1 or 1,2)"
  519. placement="top-start"
  520. >
  521. <input
  522. class="w-full py-1 px-4 rounded-r-lg text-xs bg-transparent outline-hidden"
  523. placeholder="Node Ids"
  524. bind:value={node.node_ids}
  525. />
  526. </Tooltip>
  527. </div>
  528. </div>
  529. {/each}
  530. </div>
  531. <div class="mt-2 text-xs text-right text-gray-400 dark:text-gray-500">
  532. {$i18n.t('*Prompt node ID(s) are required for image generation')}
  533. </div>
  534. </div>
  535. {/if}
  536. {:else if config?.engine === 'openai'}
  537. <div>
  538. <div class=" mb-1.5 text-sm font-medium">{$i18n.t('OpenAI API Config')}</div>
  539. <div class="flex gap-2 mb-1">
  540. <input
  541. class="flex-1 w-full text-sm bg-transparent outline-hidden"
  542. placeholder={$i18n.t('API Base URL')}
  543. bind:value={config.openai.OPENAI_API_BASE_URL}
  544. required
  545. />
  546. <SensitiveInput
  547. placeholder={$i18n.t('API Key')}
  548. bind:value={config.openai.OPENAI_API_KEY}
  549. />
  550. </div>
  551. </div>
  552. {:else if config?.engine === 'gemini'}
  553. <div>
  554. <div class=" mb-1.5 text-sm font-medium">{$i18n.t('Gemini API Config')}</div>
  555. <div class="flex gap-2 mb-1">
  556. <input
  557. class="flex-1 w-full text-sm bg-transparent outline-none"
  558. placeholder={$i18n.t('API Base URL')}
  559. bind:value={config.gemini.GEMINI_API_BASE_URL}
  560. required
  561. />
  562. <SensitiveInput
  563. placeholder={$i18n.t('API Key')}
  564. bind:value={config.gemini.GEMINI_API_KEY}
  565. />
  566. </div>
  567. </div>
  568. {/if}
  569. </div>
  570. {#if config?.enabled}
  571. <hr class=" border-gray-100 dark:border-gray-850" />
  572. <div>
  573. <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Default Model')}</div>
  574. <div class="flex w-full">
  575. <div class="flex-1 mr-2">
  576. <div class="flex w-full">
  577. <div class="flex-1">
  578. <Tooltip content={$i18n.t('Enter Model ID')} placement="top-start">
  579. <input
  580. list="model-list"
  581. class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
  582. bind:value={imageGenerationConfig.MODEL}
  583. placeholder="Select a model"
  584. required
  585. />
  586. <datalist id="model-list">
  587. {#each models ?? [] as model}
  588. <option value={model.id}>{model.name}</option>
  589. {/each}
  590. </datalist>
  591. </Tooltip>
  592. </div>
  593. </div>
  594. </div>
  595. </div>
  596. </div>
  597. <div>
  598. <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Image Size')}</div>
  599. <div class="flex w-full">
  600. <div class="flex-1 mr-2">
  601. <Tooltip content={$i18n.t('Enter Image Size (e.g. 512x512)')} placement="top-start">
  602. <input
  603. class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
  604. placeholder={$i18n.t('Enter Image Size (e.g. 512x512)')}
  605. bind:value={imageGenerationConfig.IMAGE_SIZE}
  606. required
  607. />
  608. </Tooltip>
  609. </div>
  610. </div>
  611. </div>
  612. <div>
  613. <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Steps')}</div>
  614. <div class="flex w-full">
  615. <div class="flex-1 mr-2">
  616. <Tooltip content={$i18n.t('Enter Number of Steps (e.g. 50)')} placement="top-start">
  617. <input
  618. class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
  619. placeholder={$i18n.t('Enter Number of Steps (e.g. 50)')}
  620. bind:value={imageGenerationConfig.IMAGE_STEPS}
  621. required
  622. />
  623. </Tooltip>
  624. </div>
  625. </div>
  626. </div>
  627. {/if}
  628. {/if}
  629. </div>
  630. <div class="flex justify-end pt-3 text-sm font-medium">
  631. <button
  632. class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full flex flex-row space-x-1 items-center {loading
  633. ? ' cursor-not-allowed'
  634. : ''}"
  635. type="submit"
  636. disabled={loading}
  637. >
  638. {$i18n.t('Save')}
  639. {#if loading}
  640. <div class="ml-2 self-center">
  641. <svg
  642. class=" w-4 h-4"
  643. viewBox="0 0 24 24"
  644. fill="currentColor"
  645. xmlns="http://www.w3.org/2000/svg"
  646. ><style>
  647. .spinner_ajPY {
  648. transform-origin: center;
  649. animation: spinner_AtaB 0.75s infinite linear;
  650. }
  651. @keyframes spinner_AtaB {
  652. 100% {
  653. transform: rotate(360deg);
  654. }
  655. }
  656. </style><path
  657. d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
  658. opacity=".25"
  659. /><path
  660. d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
  661. class="spinner_ajPY"
  662. /></svg
  663. >
  664. </div>
  665. {/if}
  666. </button>
  667. </div>
  668. </form>