Images.svelte 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693
  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-0.5 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. }
  231. }
  232. updateConfigHandler();
  233. }}
  234. />
  235. </div>
  236. </div>
  237. </div>
  238. <div class=" py-0.5 flex w-full justify-between">
  239. <div class=" self-center text-xs font-medium">{$i18n.t('Image Generation Engine')}</div>
  240. <div class="flex items-center relative">
  241. <select
  242. class="w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
  243. bind:value={config.engine}
  244. placeholder={$i18n.t('Select Engine')}
  245. on:change={async () => {
  246. updateConfigHandler();
  247. }}
  248. >
  249. <option value="openai">{$i18n.t('Default (Open AI)')}</option>
  250. <option value="comfyui">{$i18n.t('ComfyUI')}</option>
  251. <option value="automatic1111">{$i18n.t('Automatic1111')}</option>
  252. </select>
  253. </div>
  254. </div>
  255. </div>
  256. <hr class=" dark:border-gray-850" />
  257. <div class="flex flex-col gap-2">
  258. {#if (config?.engine ?? 'automatic1111') === 'automatic1111'}
  259. <div>
  260. <div class=" mb-2 text-sm font-medium">{$i18n.t('AUTOMATIC1111 Base URL')}</div>
  261. <div class="flex w-full">
  262. <div class="flex-1 mr-2">
  263. <input
  264. class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
  265. placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
  266. bind:value={config.automatic1111.AUTOMATIC1111_BASE_URL}
  267. />
  268. </div>
  269. <button
  270. 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"
  271. type="button"
  272. on:click={async () => {
  273. await updateConfigHandler();
  274. const res = await verifyConfigUrl(localStorage.token).catch((error) => {
  275. toast.error(error);
  276. return null;
  277. });
  278. if (res) {
  279. toast.success($i18n.t('Server connection verified'));
  280. }
  281. }}
  282. >
  283. <svg
  284. xmlns="http://www.w3.org/2000/svg"
  285. viewBox="0 0 20 20"
  286. fill="currentColor"
  287. class="w-4 h-4"
  288. >
  289. <path
  290. fill-rule="evenodd"
  291. 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"
  292. clip-rule="evenodd"
  293. />
  294. </svg>
  295. </button>
  296. </div>
  297. <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
  298. {$i18n.t('Include `--api` flag when running stable-diffusion-webui')}
  299. <a
  300. class=" text-gray-300 font-medium"
  301. href="https://github.com/AUTOMATIC1111/stable-diffusion-webui/discussions/3734"
  302. target="_blank"
  303. >
  304. {$i18n.t('(e.g. `sh webui.sh --api`)')}
  305. </a>
  306. </div>
  307. </div>
  308. <div>
  309. <div class=" mb-2 text-sm font-medium">
  310. {$i18n.t('AUTOMATIC1111 Api Auth String')}
  311. </div>
  312. <SensitiveInput
  313. placeholder={$i18n.t('Enter api auth string (e.g. username:password)')}
  314. bind:value={config.automatic1111.AUTOMATIC1111_API_AUTH}
  315. required={false}
  316. />
  317. <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
  318. {$i18n.t('Include `--api-auth` flag when running stable-diffusion-webui')}
  319. <a
  320. class=" text-gray-300 font-medium"
  321. href="https://github.com/AUTOMATIC1111/stable-diffusion-webui/discussions/13993"
  322. target="_blank"
  323. >
  324. {$i18n
  325. .t('(e.g. `sh webui.sh --api --api-auth username_password`)')
  326. .replace('_', ':')}
  327. </a>
  328. </div>
  329. </div>
  330. <!---Sampler-->
  331. <div>
  332. <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Sampler')}</div>
  333. <div class="flex w-full">
  334. <div class="flex-1 mr-2">
  335. <Tooltip content={$i18n.t('Enter Sampler (e.g. Euler a)')} placement="top-start">
  336. <input
  337. list="sampler-list"
  338. class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
  339. placeholder={$i18n.t('Enter Sampler (e.g. Euler a)')}
  340. bind:value={config.automatic1111.AUTOMATIC1111_SAMPLER}
  341. />
  342. <datalist id="sampler-list">
  343. {#each samplers ?? [] as sampler}
  344. <option value={sampler}>{sampler}</option>
  345. {/each}
  346. </datalist>
  347. </Tooltip>
  348. </div>
  349. </div>
  350. </div>
  351. <!---Scheduler-->
  352. <div>
  353. <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Scheduler')}</div>
  354. <div class="flex w-full">
  355. <div class="flex-1 mr-2">
  356. <Tooltip content={$i18n.t('Enter Scheduler (e.g. Karras)')} placement="top-start">
  357. <input
  358. list="scheduler-list"
  359. class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
  360. placeholder={$i18n.t('Enter Scheduler (e.g. Karras)')}
  361. bind:value={config.automatic1111.AUTOMATIC1111_SCHEDULER}
  362. />
  363. <datalist id="scheduler-list">
  364. {#each schedulers ?? [] as scheduler}
  365. <option value={scheduler}>{scheduler}</option>
  366. {/each}
  367. </datalist>
  368. </Tooltip>
  369. </div>
  370. </div>
  371. </div>
  372. <!---CFG scale-->
  373. <div>
  374. <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set CFG Scale')}</div>
  375. <div class="flex w-full">
  376. <div class="flex-1 mr-2">
  377. <Tooltip content={$i18n.t('Enter CFG Scale (e.g. 7.0)')} placement="top-start">
  378. <input
  379. class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
  380. placeholder={$i18n.t('Enter CFG Scale (e.g. 7.0)')}
  381. bind:value={config.automatic1111.AUTOMATIC1111_CFG_SCALE}
  382. />
  383. </Tooltip>
  384. </div>
  385. </div>
  386. </div>
  387. {:else if config?.engine === 'comfyui'}
  388. <div class="">
  389. <div class=" mb-2 text-sm font-medium">{$i18n.t('ComfyUI Base URL')}</div>
  390. <div class="flex w-full">
  391. <div class="flex-1 mr-2">
  392. <input
  393. class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
  394. placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
  395. bind:value={config.comfyui.COMFYUI_BASE_URL}
  396. />
  397. </div>
  398. <button
  399. 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"
  400. type="button"
  401. on:click={async () => {
  402. await updateConfigHandler();
  403. const res = await verifyConfigUrl(localStorage.token).catch((error) => {
  404. toast.error(error);
  405. return null;
  406. });
  407. if (res) {
  408. toast.success($i18n.t('Server connection verified'));
  409. }
  410. }}
  411. >
  412. <svg
  413. xmlns="http://www.w3.org/2000/svg"
  414. viewBox="0 0 20 20"
  415. fill="currentColor"
  416. class="w-4 h-4"
  417. >
  418. <path
  419. fill-rule="evenodd"
  420. 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"
  421. clip-rule="evenodd"
  422. />
  423. </svg>
  424. </button>
  425. </div>
  426. </div>
  427. <div class="">
  428. <div class=" mb-2 text-sm font-medium">{$i18n.t('ComfyUI Workflow')}</div>
  429. {#if config.comfyui.COMFYUI_WORKFLOW}
  430. <textarea
  431. 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"
  432. rows="10"
  433. bind:value={config.comfyui.COMFYUI_WORKFLOW}
  434. required
  435. />
  436. {/if}
  437. <div class="flex w-full">
  438. <div class="flex-1">
  439. <input
  440. id="upload-comfyui-workflow-input"
  441. hidden
  442. type="file"
  443. accept=".json"
  444. on:change={(e) => {
  445. const file = e.target.files[0];
  446. const reader = new FileReader();
  447. reader.onload = (e) => {
  448. config.comfyui.COMFYUI_WORKFLOW = e.target.result;
  449. e.target.value = null;
  450. };
  451. reader.readAsText(file);
  452. }}
  453. />
  454. <button
  455. 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"
  456. type="button"
  457. on:click={() => {
  458. document.getElementById('upload-comfyui-workflow-input')?.click();
  459. }}
  460. >
  461. {$i18n.t('Click here to upload a workflow.json file.')}
  462. </button>
  463. </div>
  464. </div>
  465. <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
  466. {$i18n.t('Make sure to export a workflow.json file as API format from ComfyUI.')}
  467. </div>
  468. </div>
  469. {#if config.comfyui.COMFYUI_WORKFLOW}
  470. <div class="">
  471. <div class=" mb-2 text-sm font-medium">{$i18n.t('ComfyUI Workflow Nodes')}</div>
  472. <div class="text-xs flex flex-col gap-1.5">
  473. {#each requiredWorkflowNodes as node}
  474. <div class="flex w-full items-center border dark:border-gray-850 rounded-lg">
  475. <div class="flex-shrink-0">
  476. <div
  477. 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"
  478. >
  479. {node.type}{node.type === 'prompt' ? '*' : ''}
  480. </div>
  481. </div>
  482. <div class="">
  483. <Tooltip content="Input Key (e.g. text, unet_name, steps)">
  484. <input
  485. class="py-1 px-3 w-24 text-xs text-center bg-transparent outline-none border-r dark:border-gray-850"
  486. placeholder="Key"
  487. bind:value={node.key}
  488. required
  489. />
  490. </Tooltip>
  491. </div>
  492. <div class="w-full">
  493. <Tooltip
  494. content="Comma separated Node Ids (e.g. 1 or 1,2)"
  495. placement="top-start"
  496. >
  497. <input
  498. class="w-full py-1 px-4 rounded-r-lg text-xs bg-transparent outline-none"
  499. placeholder="Node Ids"
  500. bind:value={node.node_ids}
  501. />
  502. </Tooltip>
  503. </div>
  504. </div>
  505. {/each}
  506. </div>
  507. <div class="mt-2 text-xs text-right text-gray-400 dark:text-gray-500">
  508. {$i18n.t('*Prompt node ID(s) are required for image generation')}
  509. </div>
  510. </div>
  511. {/if}
  512. {:else if config?.engine === 'openai'}
  513. <div>
  514. <div class=" mb-1.5 text-sm font-medium">{$i18n.t('OpenAI API Config')}</div>
  515. <div class="flex gap-2 mb-1">
  516. <input
  517. class="flex-1 w-full text-sm bg-transparent outline-none"
  518. placeholder={$i18n.t('API Base URL')}
  519. bind:value={config.openai.OPENAI_API_BASE_URL}
  520. required
  521. />
  522. <SensitiveInput
  523. placeholder={$i18n.t('API Key')}
  524. bind:value={config.openai.OPENAI_API_KEY}
  525. />
  526. </div>
  527. </div>
  528. {/if}
  529. </div>
  530. {#if config?.enabled}
  531. <hr class=" dark:border-gray-850" />
  532. <div>
  533. <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Default Model')}</div>
  534. <div class="flex w-full">
  535. <div class="flex-1 mr-2">
  536. <div class="flex w-full">
  537. <div class="flex-1">
  538. <Tooltip content={$i18n.t('Enter Model ID')} placement="top-start">
  539. <input
  540. list="model-list"
  541. class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
  542. bind:value={imageGenerationConfig.MODEL}
  543. placeholder="Select a model"
  544. required
  545. />
  546. <datalist id="model-list">
  547. {#each models ?? [] as model}
  548. <option value={model.id}>{model.name}</option>
  549. {/each}
  550. </datalist>
  551. </Tooltip>
  552. </div>
  553. </div>
  554. </div>
  555. </div>
  556. </div>
  557. <div>
  558. <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Image Size')}</div>
  559. <div class="flex w-full">
  560. <div class="flex-1 mr-2">
  561. <Tooltip content={$i18n.t('Enter Image Size (e.g. 512x512)')} placement="top-start">
  562. <input
  563. class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
  564. placeholder={$i18n.t('Enter Image Size (e.g. 512x512)')}
  565. bind:value={imageGenerationConfig.IMAGE_SIZE}
  566. required
  567. />
  568. </Tooltip>
  569. </div>
  570. </div>
  571. </div>
  572. <div>
  573. <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Steps')}</div>
  574. <div class="flex w-full">
  575. <div class="flex-1 mr-2">
  576. <Tooltip content={$i18n.t('Enter Number of Steps (e.g. 50)')} placement="top-start">
  577. <input
  578. class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
  579. placeholder={$i18n.t('Enter Number of Steps (e.g. 50)')}
  580. bind:value={imageGenerationConfig.IMAGE_STEPS}
  581. required
  582. />
  583. </Tooltip>
  584. </div>
  585. </div>
  586. </div>
  587. {/if}
  588. {/if}
  589. </div>
  590. <div class="flex justify-end pt-3 text-sm font-medium">
  591. <button
  592. 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
  593. ? ' cursor-not-allowed'
  594. : ''}"
  595. type="submit"
  596. disabled={loading}
  597. >
  598. {$i18n.t('Save')}
  599. {#if loading}
  600. <div class="ml-2 self-center">
  601. <svg
  602. class=" w-4 h-4"
  603. viewBox="0 0 24 24"
  604. fill="currentColor"
  605. xmlns="http://www.w3.org/2000/svg"
  606. ><style>
  607. .spinner_ajPY {
  608. transform-origin: center;
  609. animation: spinner_AtaB 0.75s infinite linear;
  610. }
  611. @keyframes spinner_AtaB {
  612. 100% {
  613. transform: rotate(360deg);
  614. }
  615. }
  616. </style><path
  617. 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"
  618. opacity=".25"
  619. /><path
  620. 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"
  621. class="spinner_ajPY"
  622. /></svg
  623. >
  624. </div>
  625. {/if}
  626. </button>
  627. </div>
  628. </form>