Images.svelte 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688
  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).catch((error) => {
  98. toast.error(error);
  99. return null;
  100. });
  101. if (res) {
  102. config = res;
  103. }
  104. if (config.enabled) {
  105. backendConfig.set(await getBackendConfig());
  106. getModels();
  107. }
  108. };
  109. const validateJSON = (json) => {
  110. try {
  111. const obj = JSON.parse(json);
  112. if (obj && typeof obj === 'object') {
  113. return true;
  114. }
  115. } catch (e) {}
  116. return false;
  117. };
  118. const saveHandler = async () => {
  119. loading = true;
  120. if (config?.comfyui?.COMFYUI_WORKFLOW) {
  121. if (!validateJSON(config.comfyui.COMFYUI_WORKFLOW)) {
  122. toast.error('Invalid JSON format for ComfyUI Workflow.');
  123. loading = false;
  124. return;
  125. }
  126. }
  127. if (config?.comfyui?.COMFYUI_WORKFLOW) {
  128. config.comfyui.COMFYUI_WORKFLOW_NODES = requiredWorkflowNodes.map((node) => {
  129. return {
  130. type: node.type,
  131. key: node.key,
  132. node_ids:
  133. node.node_ids.trim() === '' ? [] : node.node_ids.split(',').map((id) => id.trim())
  134. };
  135. });
  136. }
  137. await updateConfig(localStorage.token, config).catch((error) => {
  138. toast.error(error);
  139. loading = false;
  140. return null;
  141. });
  142. await updateImageGenerationConfig(localStorage.token, imageGenerationConfig).catch((error) => {
  143. toast.error(error);
  144. loading = false;
  145. return null;
  146. });
  147. getModels();
  148. dispatch('save');
  149. loading = false;
  150. };
  151. onMount(async () => {
  152. if ($user.role === 'admin') {
  153. const res = await getConfig(localStorage.token).catch((error) => {
  154. toast.error(error);
  155. return null;
  156. });
  157. if (res) {
  158. config = res;
  159. }
  160. if (config.enabled) {
  161. getModels();
  162. }
  163. if (config.comfyui.COMFYUI_WORKFLOW) {
  164. config.comfyui.COMFYUI_WORKFLOW = JSON.stringify(
  165. JSON.parse(config.comfyui.COMFYUI_WORKFLOW),
  166. null,
  167. 2
  168. );
  169. }
  170. requiredWorkflowNodes = requiredWorkflowNodes.map((node) => {
  171. const n = config.comfyui.COMFYUI_WORKFLOW_NODES.find((n) => n.type === node.type) ?? node;
  172. console.log(n);
  173. return {
  174. type: n.type,
  175. key: n.key,
  176. node_ids: typeof n.node_ids === 'string' ? n.node_ids : n.node_ids.join(',')
  177. };
  178. });
  179. const imageConfigRes = await getImageGenerationConfig(localStorage.token).catch((error) => {
  180. toast.error(error);
  181. return null;
  182. });
  183. if (imageConfigRes) {
  184. imageGenerationConfig = imageConfigRes;
  185. }
  186. }
  187. });
  188. </script>
  189. <form
  190. class="flex flex-col h-full justify-between space-y-3 text-sm"
  191. on:submit|preventDefault={async () => {
  192. saveHandler();
  193. }}
  194. >
  195. <div class=" space-y-3 overflow-y-scroll scrollbar-hidden pr-2">
  196. {#if config && imageGenerationConfig}
  197. <div>
  198. <div class=" mb-1 text-sm font-medium">{$i18n.t('Image Settings')}</div>
  199. <div>
  200. <div class=" py-0.5 flex w-full justify-between">
  201. <div class=" self-center text-xs font-medium">
  202. {$i18n.t('Image Generation (Experimental)')}
  203. </div>
  204. <div class="px-1">
  205. <Switch
  206. bind:state={config.enabled}
  207. on:change={(e) => {
  208. const enabled = e.detail;
  209. if (enabled) {
  210. if (
  211. config.engine === 'automatic1111' &&
  212. config.automatic1111.AUTOMATIC1111_BASE_URL === ''
  213. ) {
  214. toast.error($i18n.t('AUTOMATIC1111 Base URL is required.'));
  215. config.enabled = false;
  216. } else if (
  217. config.engine === 'comfyui' &&
  218. config.comfyui.COMFYUI_BASE_URL === ''
  219. ) {
  220. toast.error($i18n.t('ComfyUI Base URL is required.'));
  221. config.enabled = false;
  222. } else if (config.engine === 'openai' && config.openai.OPENAI_API_KEY === '') {
  223. toast.error($i18n.t('OpenAI API Key is required.'));
  224. config.enabled = false;
  225. }
  226. }
  227. updateConfigHandler();
  228. }}
  229. />
  230. </div>
  231. </div>
  232. </div>
  233. <div class=" py-0.5 flex w-full justify-between">
  234. <div class=" self-center text-xs font-medium">{$i18n.t('Image Generation Engine')}</div>
  235. <div class="flex items-center relative">
  236. <select
  237. class="w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
  238. bind:value={config.engine}
  239. placeholder={$i18n.t('Select Engine')}
  240. on:change={async () => {
  241. updateConfigHandler();
  242. }}
  243. >
  244. <option value="openai">{$i18n.t('Default (Open AI)')}</option>
  245. <option value="comfyui">{$i18n.t('ComfyUI')}</option>
  246. <option value="automatic1111">{$i18n.t('Automatic1111')}</option>
  247. </select>
  248. </div>
  249. </div>
  250. </div>
  251. <hr class=" dark:border-gray-850" />
  252. <div class="flex flex-col gap-2">
  253. {#if (config?.engine ?? 'automatic1111') === 'automatic1111'}
  254. <div>
  255. <div class=" mb-2 text-sm font-medium">{$i18n.t('AUTOMATIC1111 Base URL')}</div>
  256. <div class="flex w-full">
  257. <div class="flex-1 mr-2">
  258. <input
  259. class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
  260. placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
  261. bind:value={config.automatic1111.AUTOMATIC1111_BASE_URL}
  262. />
  263. </div>
  264. <button
  265. 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"
  266. type="button"
  267. on:click={async () => {
  268. await updateConfigHandler();
  269. const res = await verifyConfigUrl(localStorage.token).catch((error) => {
  270. toast.error(error);
  271. return null;
  272. });
  273. if (res) {
  274. toast.success($i18n.t('Server connection verified'));
  275. }
  276. }}
  277. >
  278. <svg
  279. xmlns="http://www.w3.org/2000/svg"
  280. viewBox="0 0 20 20"
  281. fill="currentColor"
  282. class="w-4 h-4"
  283. >
  284. <path
  285. fill-rule="evenodd"
  286. 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"
  287. clip-rule="evenodd"
  288. />
  289. </svg>
  290. </button>
  291. </div>
  292. <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
  293. {$i18n.t('Include `--api` flag when running stable-diffusion-webui')}
  294. <a
  295. class=" text-gray-300 font-medium"
  296. href="https://github.com/AUTOMATIC1111/stable-diffusion-webui/discussions/3734"
  297. target="_blank"
  298. >
  299. {$i18n.t('(e.g. `sh webui.sh --api`)')}
  300. </a>
  301. </div>
  302. </div>
  303. <div>
  304. <div class=" mb-2 text-sm font-medium">
  305. {$i18n.t('AUTOMATIC1111 Api Auth String')}
  306. </div>
  307. <SensitiveInput
  308. placeholder={$i18n.t('Enter api auth string (e.g. username:password)')}
  309. bind:value={config.automatic1111.AUTOMATIC1111_API_AUTH}
  310. required={false}
  311. />
  312. <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
  313. {$i18n.t('Include `--api-auth` flag when running stable-diffusion-webui')}
  314. <a
  315. class=" text-gray-300 font-medium"
  316. href="https://github.com/AUTOMATIC1111/stable-diffusion-webui/discussions/13993"
  317. target="_blank"
  318. >
  319. {$i18n
  320. .t('(e.g. `sh webui.sh --api --api-auth username_password`)')
  321. .replace('_', ':')}
  322. </a>
  323. </div>
  324. </div>
  325. <!---Sampler-->
  326. <div>
  327. <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Sampler')}</div>
  328. <div class="flex w-full">
  329. <div class="flex-1 mr-2">
  330. <Tooltip content={$i18n.t('Enter Sampler (e.g. Euler a)')} placement="top-start">
  331. <input
  332. list="sampler-list"
  333. class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
  334. placeholder={$i18n.t('Enter Sampler (e.g. Euler a)')}
  335. bind:value={config.automatic1111.AUTOMATIC1111_SAMPLER}
  336. />
  337. <datalist id="sampler-list">
  338. {#each samplers ?? [] as sampler}
  339. <option value={sampler}>{sampler}</option>
  340. {/each}
  341. </datalist>
  342. </Tooltip>
  343. </div>
  344. </div>
  345. </div>
  346. <!---Scheduler-->
  347. <div>
  348. <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Scheduler')}</div>
  349. <div class="flex w-full">
  350. <div class="flex-1 mr-2">
  351. <Tooltip content={$i18n.t('Enter Scheduler (e.g. Karras)')} placement="top-start">
  352. <input
  353. list="scheduler-list"
  354. class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
  355. placeholder={$i18n.t('Enter Scheduler (e.g. Karras)')}
  356. bind:value={config.automatic1111.AUTOMATIC1111_SCHEDULER}
  357. />
  358. <datalist id="scheduler-list">
  359. {#each schedulers ?? [] as scheduler}
  360. <option value={scheduler}>{scheduler}</option>
  361. {/each}
  362. </datalist>
  363. </Tooltip>
  364. </div>
  365. </div>
  366. </div>
  367. <!---CFG scale-->
  368. <div>
  369. <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set CFG Scale')}</div>
  370. <div class="flex w-full">
  371. <div class="flex-1 mr-2">
  372. <Tooltip content={$i18n.t('Enter CFG Scale (e.g. 7.0)')} placement="top-start">
  373. <input
  374. class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
  375. placeholder={$i18n.t('Enter CFG Scale (e.g. 7.0)')}
  376. bind:value={config.automatic1111.AUTOMATIC1111_CFG_SCALE}
  377. />
  378. </Tooltip>
  379. </div>
  380. </div>
  381. </div>
  382. {:else if config?.engine === 'comfyui'}
  383. <div class="">
  384. <div class=" mb-2 text-sm font-medium">{$i18n.t('ComfyUI Base URL')}</div>
  385. <div class="flex w-full">
  386. <div class="flex-1 mr-2">
  387. <input
  388. class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
  389. placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
  390. bind:value={config.comfyui.COMFYUI_BASE_URL}
  391. />
  392. </div>
  393. <button
  394. 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"
  395. type="button"
  396. on:click={async () => {
  397. await updateConfigHandler();
  398. const res = await verifyConfigUrl(localStorage.token).catch((error) => {
  399. toast.error(error);
  400. return null;
  401. });
  402. if (res) {
  403. toast.success($i18n.t('Server connection verified'));
  404. }
  405. }}
  406. >
  407. <svg
  408. xmlns="http://www.w3.org/2000/svg"
  409. viewBox="0 0 20 20"
  410. fill="currentColor"
  411. class="w-4 h-4"
  412. >
  413. <path
  414. fill-rule="evenodd"
  415. 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"
  416. clip-rule="evenodd"
  417. />
  418. </svg>
  419. </button>
  420. </div>
  421. </div>
  422. <div class="">
  423. <div class=" mb-2 text-sm font-medium">{$i18n.t('ComfyUI Workflow')}</div>
  424. {#if config.comfyui.COMFYUI_WORKFLOW}
  425. <textarea
  426. 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-none disabled:text-gray-600 resize-none"
  427. rows="10"
  428. bind:value={config.comfyui.COMFYUI_WORKFLOW}
  429. required
  430. />
  431. {/if}
  432. <div class="flex w-full">
  433. <div class="flex-1">
  434. <input
  435. id="upload-comfyui-workflow-input"
  436. hidden
  437. type="file"
  438. accept=".json"
  439. on:change={(e) => {
  440. const file = e.target.files[0];
  441. const reader = new FileReader();
  442. reader.onload = (e) => {
  443. config.comfyui.COMFYUI_WORKFLOW = e.target.result;
  444. e.target.value = null;
  445. };
  446. reader.readAsText(file);
  447. }}
  448. />
  449. <button
  450. class="w-full text-sm font-medium py-2 bg-transparent hover:bg-gray-100 border border-dashed dark:border-gray-800 dark:hover:bg-gray-850 text-center rounded-xl"
  451. type="button"
  452. on:click={() => {
  453. document.getElementById('upload-comfyui-workflow-input')?.click();
  454. }}
  455. >
  456. {$i18n.t('Click here to upload a workflow.json file.')}
  457. </button>
  458. </div>
  459. </div>
  460. <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
  461. {$i18n.t('Make sure to export a workflow.json file as API format from ComfyUI.')}
  462. </div>
  463. </div>
  464. {#if config.comfyui.COMFYUI_WORKFLOW}
  465. <div class="">
  466. <div class=" mb-2 text-sm font-medium">{$i18n.t('ComfyUI Workflow Nodes')}</div>
  467. <div class="text-xs flex flex-col gap-1.5">
  468. {#each requiredWorkflowNodes as node}
  469. <div class="flex w-full items-center border dark:border-gray-850 rounded-lg">
  470. <div class="flex-shrink-0">
  471. <div
  472. 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"
  473. >
  474. {node.type}{node.type === 'prompt' ? '*' : ''}
  475. </div>
  476. </div>
  477. <div class="">
  478. <Tooltip content="Input Key (e.g. text, unet_name, steps)">
  479. <input
  480. class="py-1 px-3 w-24 text-xs text-center bg-transparent outline-none border-r dark:border-gray-850"
  481. placeholder="Key"
  482. bind:value={node.key}
  483. required
  484. />
  485. </Tooltip>
  486. </div>
  487. <div class="w-full">
  488. <Tooltip
  489. content="Comma separated Node Ids (e.g. 1 or 1,2)"
  490. placement="top-start"
  491. >
  492. <input
  493. class="w-full py-1 px-4 rounded-r-lg text-xs bg-transparent outline-none"
  494. placeholder="Node Ids"
  495. bind:value={node.node_ids}
  496. />
  497. </Tooltip>
  498. </div>
  499. </div>
  500. {/each}
  501. </div>
  502. <div class="mt-2 text-xs text-right text-gray-400 dark:text-gray-500">
  503. {$i18n.t('*Prompt node ID(s) are required for image generation')}
  504. </div>
  505. </div>
  506. {/if}
  507. {:else if config?.engine === 'openai'}
  508. <div>
  509. <div class=" mb-1.5 text-sm font-medium">{$i18n.t('OpenAI API Config')}</div>
  510. <div class="flex gap-2 mb-1">
  511. <input
  512. class="flex-1 w-full text-sm bg-transparent outline-none"
  513. placeholder={$i18n.t('API Base URL')}
  514. bind:value={config.openai.OPENAI_API_BASE_URL}
  515. required
  516. />
  517. <SensitiveInput
  518. placeholder={$i18n.t('API Key')}
  519. bind:value={config.openai.OPENAI_API_KEY}
  520. />
  521. </div>
  522. </div>
  523. {/if}
  524. </div>
  525. {#if config?.enabled}
  526. <hr class=" dark:border-gray-850" />
  527. <div>
  528. <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Default Model')}</div>
  529. <div class="flex w-full">
  530. <div class="flex-1 mr-2">
  531. <div class="flex w-full">
  532. <div class="flex-1">
  533. <Tooltip content={$i18n.t('Enter Model ID')} placement="top-start">
  534. <input
  535. list="model-list"
  536. class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
  537. bind:value={imageGenerationConfig.MODEL}
  538. placeholder="Select a model"
  539. required
  540. />
  541. <datalist id="model-list">
  542. {#each models ?? [] as model}
  543. <option value={model.id}>{model.name}</option>
  544. {/each}
  545. </datalist>
  546. </Tooltip>
  547. </div>
  548. </div>
  549. </div>
  550. </div>
  551. </div>
  552. <div>
  553. <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Image Size')}</div>
  554. <div class="flex w-full">
  555. <div class="flex-1 mr-2">
  556. <Tooltip content={$i18n.t('Enter Image Size (e.g. 512x512)')} placement="top-start">
  557. <input
  558. class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
  559. placeholder={$i18n.t('Enter Image Size (e.g. 512x512)')}
  560. bind:value={imageGenerationConfig.IMAGE_SIZE}
  561. required
  562. />
  563. </Tooltip>
  564. </div>
  565. </div>
  566. </div>
  567. <div>
  568. <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Steps')}</div>
  569. <div class="flex w-full">
  570. <div class="flex-1 mr-2">
  571. <Tooltip content={$i18n.t('Enter Number of Steps (e.g. 50)')} placement="top-start">
  572. <input
  573. class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
  574. placeholder={$i18n.t('Enter Number of Steps (e.g. 50)')}
  575. bind:value={imageGenerationConfig.IMAGE_STEPS}
  576. required
  577. />
  578. </Tooltip>
  579. </div>
  580. </div>
  581. </div>
  582. {/if}
  583. {/if}
  584. </div>
  585. <div class="flex justify-end pt-3 text-sm font-medium">
  586. <button
  587. 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
  588. ? ' cursor-not-allowed'
  589. : ''}"
  590. type="submit"
  591. disabled={loading}
  592. >
  593. {$i18n.t('Save')}
  594. {#if loading}
  595. <div class="ml-2 self-center">
  596. <svg
  597. class=" w-4 h-4"
  598. viewBox="0 0 24 24"
  599. fill="currentColor"
  600. xmlns="http://www.w3.org/2000/svg"
  601. ><style>
  602. .spinner_ajPY {
  603. transform-origin: center;
  604. animation: spinner_AtaB 0.75s infinite linear;
  605. }
  606. @keyframes spinner_AtaB {
  607. 100% {
  608. transform: rotate(360deg);
  609. }
  610. }
  611. </style><path
  612. 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"
  613. opacity=".25"
  614. /><path
  615. 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"
  616. class="spinner_ajPY"
  617. /></svg
  618. >
  619. </div>
  620. {/if}
  621. </button>
  622. </div>
  623. </form>