Images.svelte 36 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237
  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 Spinner from '$lib/components/common/Spinner.svelte';
  15. import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
  16. import Switch from '$lib/components/common/Switch.svelte';
  17. import Tooltip from '$lib/components/common/Tooltip.svelte';
  18. import Textarea from '$lib/components/common/Textarea.svelte';
  19. import CodeEditorModal from '$lib/components/common/CodeEditorModal.svelte';
  20. const dispatch = createEventDispatcher();
  21. const i18n = getContext('i18n');
  22. let loading = false;
  23. let models = null;
  24. let config = null;
  25. let showComfyUIWorkflowEditor = false;
  26. let REQUIRED_WORKFLOW_NODES = [
  27. {
  28. type: 'prompt',
  29. key: 'text',
  30. node_ids: ''
  31. },
  32. {
  33. type: 'model',
  34. key: 'ckpt_name',
  35. node_ids: ''
  36. },
  37. {
  38. type: 'width',
  39. key: 'width',
  40. node_ids: ''
  41. },
  42. {
  43. type: 'height',
  44. key: 'height',
  45. node_ids: ''
  46. },
  47. {
  48. type: 'steps',
  49. key: 'steps',
  50. node_ids: ''
  51. },
  52. {
  53. type: 'seed',
  54. key: 'seed',
  55. node_ids: ''
  56. }
  57. ];
  58. let showComfyUIEditWorkflowEditor = false;
  59. let REQUIRED_EDIT_WORKFLOW_NODES = [
  60. {
  61. type: 'image',
  62. key: 'image',
  63. node_ids: ''
  64. },
  65. {
  66. type: 'prompt',
  67. key: 'prompt',
  68. node_ids: ''
  69. },
  70. {
  71. type: 'model',
  72. key: 'unet_name',
  73. node_ids: ''
  74. },
  75. {
  76. type: 'width',
  77. key: 'width',
  78. node_ids: ''
  79. },
  80. {
  81. type: 'height',
  82. key: 'height',
  83. node_ids: ''
  84. }
  85. ];
  86. const getModels = async () => {
  87. models = await getImageGenerationModels(localStorage.token).catch((error) => {
  88. toast.error(`${error}`);
  89. return null;
  90. });
  91. };
  92. const updateConfigHandler = async () => {
  93. if (
  94. config.IMAGE_GENERATION_ENGINE === 'automatic1111' &&
  95. config.AUTOMATIC1111_BASE_URL === ''
  96. ) {
  97. toast.error($i18n.t('AUTOMATIC1111 Base URL is required.'));
  98. config.ENABLE_IMAGE_GENERATION = false;
  99. return null;
  100. } else if (config.IMAGE_GENERATION_ENGINE === 'comfyui' && config.COMFYUI_BASE_URL === '') {
  101. toast.error($i18n.t('ComfyUI Base URL is required.'));
  102. config.ENABLE_IMAGE_GENERATION = false;
  103. return null;
  104. } else if (config.IMAGE_GENERATION_ENGINE === 'openai' && config.OPENAI_API_KEY === '') {
  105. toast.error($i18n.t('OpenAI API Key is required.'));
  106. config.ENABLE_IMAGE_GENERATION = false;
  107. return null;
  108. } else if (config.IMAGE_GENERATION_ENGINE === 'gemini' && config.GEMINI_API_KEY === '') {
  109. toast.error($i18n.t('Gemini API Key is required.'));
  110. config.ENABLE_IMAGE_GENERATION = false;
  111. return null;
  112. }
  113. const res = await updateConfig(localStorage.token, config).catch((error) => {
  114. toast.error(`${error}`);
  115. return null;
  116. });
  117. if (res) {
  118. config = res;
  119. if (config.ENABLE_IMAGE_GENERATION) {
  120. backendConfig.set(await getBackendConfig());
  121. getModels();
  122. }
  123. return config;
  124. }
  125. return null;
  126. };
  127. const validateJSON = (json) => {
  128. try {
  129. const obj = JSON.parse(json);
  130. if (obj && typeof obj === 'object') {
  131. return true;
  132. }
  133. } catch (e) {}
  134. return false;
  135. };
  136. const saveHandler = async () => {
  137. loading = true;
  138. if (config?.COMFYUI_WORKFLOW) {
  139. if (!validateJSON(config?.COMFYUI_WORKFLOW)) {
  140. toast.error($i18n.t('Invalid JSON format for ComfyUI Workflow.'));
  141. loading = false;
  142. return;
  143. }
  144. config.COMFYUI_WORKFLOW_NODES = REQUIRED_WORKFLOW_NODES.map((node) => {
  145. return {
  146. type: node.type,
  147. key: node.key,
  148. node_ids:
  149. node.node_ids.trim() === '' ? [] : node.node_ids.split(',').map((id) => id.trim())
  150. };
  151. });
  152. }
  153. if (config?.IMAGES_EDIT_COMFYUI_WORKFLOW) {
  154. if (!validateJSON(config?.IMAGES_EDIT_COMFYUI_WORKFLOW)) {
  155. toast.error($i18n.t('Invalid JSON format for ComfyUI Edit Workflow.'));
  156. loading = false;
  157. return;
  158. }
  159. config.IMAGES_EDIT_COMFYUI_WORKFLOW_NODES = REQUIRED_EDIT_WORKFLOW_NODES.map((node) => {
  160. return {
  161. type: node.type,
  162. key: node.key,
  163. node_ids:
  164. node.node_ids.trim() === '' ? [] : node.node_ids.split(',').map((id) => id.trim())
  165. };
  166. });
  167. }
  168. const res = await updateConfigHandler();
  169. if (res) {
  170. dispatch('save');
  171. }
  172. loading = false;
  173. };
  174. onMount(async () => {
  175. if ($user?.role === 'admin') {
  176. const res = await getConfig(localStorage.token).catch((error) => {
  177. toast.error(`${error}`);
  178. return null;
  179. });
  180. if (res) {
  181. config = res;
  182. }
  183. if (config.ENABLE_IMAGE_GENERATION) {
  184. getModels();
  185. }
  186. if (config.COMFYUI_WORKFLOW) {
  187. try {
  188. config.COMFYUI_WORKFLOW = JSON.stringify(JSON.parse(config.COMFYUI_WORKFLOW), null, 2);
  189. } catch (e) {
  190. console.error(e);
  191. }
  192. }
  193. REQUIRED_WORKFLOW_NODES = REQUIRED_WORKFLOW_NODES.map((node) => {
  194. const n = config.COMFYUI_WORKFLOW_NODES.find((n) => n.type === node.type) ?? node;
  195. console.debug(n);
  196. return {
  197. type: n.type,
  198. key: n.key,
  199. node_ids: typeof n.node_ids === 'string' ? n.node_ids : n.node_ids.join(',')
  200. };
  201. });
  202. if (config.IMAGES_EDIT_COMFYUI_WORKFLOW) {
  203. try {
  204. config.IMAGES_EDIT_COMFYUI_WORKFLOW = JSON.stringify(
  205. JSON.parse(config.IMAGES_EDIT_COMFYUI_WORKFLOW),
  206. null,
  207. 2
  208. );
  209. } catch (e) {
  210. console.error(e);
  211. }
  212. }
  213. REQUIRED_EDIT_WORKFLOW_NODES = REQUIRED_EDIT_WORKFLOW_NODES.map((node) => {
  214. const n =
  215. config.IMAGES_EDIT_COMFYUI_WORKFLOW_NODES.find((n) => n.type === node.type) ?? node;
  216. console.debug(n);
  217. return {
  218. type: n.type,
  219. key: n.key,
  220. node_ids: typeof n.node_ids === 'string' ? n.node_ids : n.node_ids.join(',')
  221. };
  222. });
  223. }
  224. });
  225. </script>
  226. <form
  227. class="flex flex-col h-full justify-between space-y-3 text-sm"
  228. on:submit|preventDefault={async () => {
  229. saveHandler();
  230. }}
  231. >
  232. <div class=" space-y-3 overflow-y-scroll scrollbar-hidden pr-2">
  233. {#if config}
  234. <div>
  235. <div class="mb-3">
  236. <div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
  237. <hr class=" border-gray-100 dark:border-gray-850 my-2" />
  238. <div class="mb-2.5">
  239. <div class="flex w-full justify-between items-center">
  240. <div class="text-xs pr-2">
  241. <div class="">
  242. {$i18n.t('Image Generation')}
  243. </div>
  244. </div>
  245. <Switch bind:state={config.ENABLE_IMAGE_GENERATION} />
  246. </div>
  247. </div>
  248. </div>
  249. <div class="mb-3">
  250. <div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Create Image')}</div>
  251. <hr class=" border-gray-100 dark:border-gray-850 my-2" />
  252. {#if config.ENABLE_IMAGE_GENERATION}
  253. <div class="mb-2.5">
  254. <div class="flex w-full justify-between items-center">
  255. <div class="text-xs pr-2">
  256. <div class="shrink-0">
  257. {$i18n.t('Model')}
  258. </div>
  259. </div>
  260. <Tooltip content={$i18n.t('Enter Model ID')} placement="top-start">
  261. <input
  262. list="model-list"
  263. class=" text-right text-sm bg-transparent outline-hidden max-w-full w-52"
  264. bind:value={config.IMAGE_GENERATION_MODEL}
  265. placeholder={$i18n.t('Select a model')}
  266. required
  267. />
  268. <datalist id="model-list">
  269. {#each models ?? [] as model}
  270. <option value={model.id}>{model.name}</option>
  271. {/each}
  272. </datalist>
  273. </Tooltip>
  274. </div>
  275. </div>
  276. <div class="mb-2.5">
  277. <div class="flex w-full justify-between items-center">
  278. <div class="text-xs pr-2">
  279. <div class="shrink-0">
  280. {$i18n.t('Image Size')}
  281. </div>
  282. </div>
  283. <Tooltip content={$i18n.t('Enter Image Size (e.g. 512x512)')} placement="top-start">
  284. <input
  285. class=" text-right text-sm bg-transparent outline-hidden max-w-full w-52"
  286. placeholder={$i18n.t('Enter Image Size (e.g. 512x512)')}
  287. bind:value={config.IMAGE_SIZE}
  288. />
  289. </Tooltip>
  290. </div>
  291. </div>
  292. {#if ['comfyui', 'automatic1111', ''].includes(config?.IMAGE_GENERATION_ENGINE)}
  293. <div class="mb-2.5">
  294. <div class="flex w-full justify-between items-center">
  295. <div class="text-xs pr-2">
  296. <div class="">
  297. {$i18n.t('Steps')}
  298. </div>
  299. </div>
  300. <Tooltip
  301. content={$i18n.t('Enter Number of Steps (e.g. 50)')}
  302. placement="top-start"
  303. >
  304. <input
  305. class=" text-right text-sm bg-transparent outline-hidden"
  306. placeholder={$i18n.t('Enter Number of Steps (e.g. 50)')}
  307. bind:value={config.IMAGE_STEPS}
  308. required
  309. />
  310. </Tooltip>
  311. </div>
  312. </div>
  313. {/if}
  314. <div class="mb-2.5">
  315. <div class="flex w-full justify-between items-center">
  316. <div class="text-xs pr-2">
  317. <div class="">
  318. {$i18n.t('Image Prompt Generation')}
  319. </div>
  320. </div>
  321. <Switch bind:state={config.ENABLE_IMAGE_PROMPT_GENERATION} />
  322. </div>
  323. </div>
  324. {/if}
  325. <div class="mb-2.5">
  326. <div class="flex w-full justify-between items-center">
  327. <div class="text-xs pr-2">
  328. <div class="">
  329. {$i18n.t('Image Generation Engine')}
  330. </div>
  331. </div>
  332. <select
  333. class=" dark:bg-gray-900 w-fit pr-8 cursor-pointer rounded-sm px-2 text-xs bg-transparent outline-hidden text-right"
  334. bind:value={config.IMAGE_GENERATION_ENGINE}
  335. placeholder={$i18n.t('Select Engine')}
  336. >
  337. <option value="openai">{$i18n.t('Default (Open AI)')}</option>
  338. <option value="comfyui">{$i18n.t('ComfyUI')}</option>
  339. <option value="automatic1111">{$i18n.t('Automatic1111')}</option>
  340. <option value="gemini">{$i18n.t('Gemini')}</option>
  341. </select>
  342. </div>
  343. </div>
  344. {#if config?.IMAGE_GENERATION_ENGINE === 'openai'}
  345. <div class="mb-2.5">
  346. <div class="flex w-full justify-between items-center">
  347. <div class="text-xs pr-2 shrink-0">
  348. <div class="">
  349. {$i18n.t('OpenAI API Base URL')}
  350. </div>
  351. </div>
  352. <div class="flex w-full">
  353. <div class="flex-1">
  354. <input
  355. class="w-full text-sm bg-transparent outline-hidden text-right"
  356. placeholder={$i18n.t('API Base URL')}
  357. bind:value={config.IMAGES_OPENAI_API_BASE_URL}
  358. />
  359. </div>
  360. </div>
  361. </div>
  362. </div>
  363. <div class="mb-2.5">
  364. <div class="flex w-full justify-between items-center">
  365. <div class="text-xs pr-2 shrink-0">
  366. <div class="">
  367. {$i18n.t('OpenAI API Key')}
  368. </div>
  369. </div>
  370. <div class="flex w-full">
  371. <div class="flex-1">
  372. <SensitiveInput
  373. inputClassName="text-right w-full"
  374. placeholder={$i18n.t('API Key')}
  375. bind:value={config.IMAGES_OPENAI_API_KEY}
  376. required={false}
  377. />
  378. </div>
  379. </div>
  380. </div>
  381. </div>
  382. <div class="mb-2.5">
  383. <div class="flex w-full justify-between items-center">
  384. <div class="text-xs pr-2 shrink-0">
  385. <div class="">
  386. {$i18n.t('OpenAI API Version')}
  387. </div>
  388. </div>
  389. <div class="flex w-full">
  390. <div class="flex-1">
  391. <input
  392. class="w-full text-sm bg-transparent outline-hidden text-right"
  393. placeholder={$i18n.t('API Version')}
  394. bind:value={config.IMAGES_OPENAI_API_VERSION}
  395. />
  396. </div>
  397. </div>
  398. </div>
  399. </div>
  400. {:else if (config?.IMAGE_GENERATION_ENGINE ?? 'automatic1111') === 'automatic1111'}
  401. <div class="mb-2.5">
  402. <div class="flex w-full justify-between items-center">
  403. <div class="text-xs pr-2 shrink-0">
  404. <div class="">
  405. {$i18n.t('AUTOMATIC1111 Base URL')}
  406. </div>
  407. </div>
  408. <div class="flex w-full">
  409. <div class="flex-1 mr-2">
  410. <input
  411. class="w-full text-sm bg-transparent outline-hidden text-right"
  412. placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
  413. bind:value={config.AUTOMATIC1111_BASE_URL}
  414. />
  415. </div>
  416. <button
  417. class=" transition"
  418. type="button"
  419. aria-label="verify connection"
  420. on:click={async () => {
  421. await updateConfigHandler();
  422. const res = await verifyConfigUrl(localStorage.token).catch((error) => {
  423. toast.error(`${error}`);
  424. return null;
  425. });
  426. if (res) {
  427. toast.success($i18n.t('Server connection verified'));
  428. }
  429. }}
  430. >
  431. <svg
  432. xmlns="http://www.w3.org/2000/svg"
  433. viewBox="0 0 20 20"
  434. fill="currentColor"
  435. class="w-4 h-4"
  436. >
  437. <path
  438. fill-rule="evenodd"
  439. 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"
  440. clip-rule="evenodd"
  441. />
  442. </svg>
  443. </button>
  444. </div>
  445. </div>
  446. <div class="mt-1 text-xs text-gray-400 dark:text-gray-500">
  447. {$i18n.t('Include `--api` flag when running stable-diffusion-webui')}
  448. <a
  449. class=" text-gray-300 font-medium"
  450. href="https://github.com/AUTOMATIC1111/stable-diffusion-webui/discussions/3734"
  451. target="_blank"
  452. >
  453. {$i18n.t('(e.g. `sh webui.sh --api`)')}
  454. </a>
  455. </div>
  456. </div>
  457. <div class="mb-2.5">
  458. <div class="flex w-full justify-between items-center">
  459. <div class="text-xs pr-2 shrink-0">
  460. <div class="">
  461. {$i18n.t('AUTOMATIC1111 Api Auth String')}
  462. </div>
  463. </div>
  464. <div class="flex w-full">
  465. <div class="flex-1">
  466. <SensitiveInput
  467. inputClassName="text-right w-full"
  468. placeholder={$i18n.t('Enter api auth string (e.g. username:password)')}
  469. bind:value={config.AUTOMATIC1111_API_AUTH}
  470. required={false}
  471. />
  472. </div>
  473. </div>
  474. </div>
  475. <div class="mt-1 text-xs text-gray-400 dark:text-gray-500">
  476. {$i18n.t('Include `--api-auth` flag when running stable-diffusion-webui')}
  477. <a
  478. class=" text-gray-300 font-medium"
  479. href="https://github.com/AUTOMATIC1111/stable-diffusion-webui/discussions/13993"
  480. target="_blank"
  481. >
  482. {$i18n
  483. .t('(e.g. `sh webui.sh --api --api-auth username_password`)')
  484. .replace('_', ':')}
  485. </a>
  486. </div>
  487. </div>
  488. <div class="mb-2.5">
  489. <div class="flex w-full justify-between items-center">
  490. <div class="text-xs pr-2 shrink-0">
  491. <div class="">
  492. {$i18n.t('Additional Parameters')}
  493. </div>
  494. </div>
  495. </div>
  496. <div class="mt-1.5 flex w-full">
  497. <div class="flex-1 mr-2">
  498. <Textarea
  499. className="rounded-lg w-full py-2 px-3 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
  500. bind:value={config.AUTOMATIC1111_PARAMS}
  501. placeholder={$i18n.t('Enter additional parameters in JSON format')}
  502. minSize={100}
  503. />
  504. </div>
  505. </div>
  506. </div>
  507. {:else if config?.IMAGE_GENERATION_ENGINE === 'comfyui'}
  508. <div class="mb-2.5">
  509. <div class="flex w-full justify-between items-center">
  510. <div class="text-xs pr-2 shrink-0">
  511. <div class="">
  512. {$i18n.t('ComfyUI Base URL')}
  513. </div>
  514. </div>
  515. <div class="flex w-full">
  516. <div class="flex-1 mr-2">
  517. <input
  518. class="w-full text-sm bg-transparent outline-hidden text-right"
  519. placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
  520. bind:value={config.COMFYUI_BASE_URL}
  521. />
  522. </div>
  523. <button
  524. class=" rounded-lg transition"
  525. type="button"
  526. aria-label="verify connection"
  527. on:click={async () => {
  528. await updateConfigHandler();
  529. const res = await verifyConfigUrl(localStorage.token).catch((error) => {
  530. toast.error(`${error}`);
  531. return null;
  532. });
  533. if (res) {
  534. toast.success($i18n.t('Server connection verified'));
  535. }
  536. }}
  537. >
  538. <svg
  539. xmlns="http://www.w3.org/2000/svg"
  540. viewBox="0 0 20 20"
  541. fill="currentColor"
  542. class="w-4 h-4"
  543. >
  544. <path
  545. fill-rule="evenodd"
  546. 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"
  547. clip-rule="evenodd"
  548. />
  549. </svg>
  550. </button>
  551. </div>
  552. </div>
  553. </div>
  554. <div class="mb-2.5">
  555. <div class="flex w-full justify-between items-center">
  556. <div class="text-xs pr-2 shrink-0">
  557. <div class="">
  558. {$i18n.t('ComfyUI API Key')}
  559. </div>
  560. </div>
  561. <div class="flex w-full">
  562. <div class="flex-1">
  563. <SensitiveInput
  564. inputClassName="text-right w-full"
  565. placeholder={$i18n.t('sk-1234')}
  566. bind:value={config.COMFYUI_API_KEY}
  567. required={false}
  568. />
  569. </div>
  570. </div>
  571. </div>
  572. </div>
  573. <div class="mb-2.5">
  574. <input
  575. id="upload-comfyui-workflow-input"
  576. hidden
  577. type="file"
  578. accept=".json"
  579. on:change={(e) => {
  580. const file = e.target.files[0];
  581. const reader = new FileReader();
  582. reader.onload = (e) => {
  583. config.COMFYUI_WORKFLOW = e.target.result;
  584. e.target.value = null;
  585. };
  586. reader.readAsText(file);
  587. }}
  588. />
  589. <div class="flex w-full justify-between items-center">
  590. <div class="text-xs pr-2 shrink-0">
  591. <div class="">
  592. {$i18n.t('ComfyUI Workflow')}
  593. </div>
  594. </div>
  595. <div class="flex w-full">
  596. <div class="flex-1 mr-2 justify-end flex gap-1">
  597. {#if config.COMFYUI_WORKFLOW}
  598. <button
  599. class="text-xs text-gray-700 dark:text-gray-400 underline"
  600. type="button"
  601. aria-label={$i18n.t('Edit workflow.json content')}
  602. on:click={() => {
  603. // open code editor modal
  604. showComfyUIWorkflowEditor = true;
  605. }}
  606. >
  607. {$i18n.t('Edit')}
  608. </button>
  609. {/if}
  610. <Tooltip content={$i18n.t('Click here to upload a workflow.json file.')}>
  611. <button
  612. class="text-xs text-gray-700 dark:text-gray-400 underline"
  613. type="button"
  614. aria-label={$i18n.t('Click here to upload a workflow.json file.')}
  615. on:click={() => {
  616. document.getElementById('upload-comfyui-workflow-input')?.click();
  617. }}
  618. >
  619. {$i18n.t('Upload')}
  620. </button>
  621. </Tooltip>
  622. </div>
  623. </div>
  624. </div>
  625. <div class="mt-1 text-xs text-gray-400 dark:text-gray-500">
  626. <CodeEditorModal
  627. bind:show={showComfyUIWorkflowEditor}
  628. value={config.COMFYUI_WORKFLOW}
  629. lang="json"
  630. onChange={(e) => {
  631. config.COMFYUI_WORKFLOW = e;
  632. }}
  633. onSave={() => {
  634. console.log('Saved');
  635. }}
  636. />
  637. <!-- {#if config.COMFYUI_WORKFLOW}
  638. <Textarea
  639. class="w-full rounded-lg my-1 py-2 px-3 text-xs bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden disabled:text-gray-600 resize-none"
  640. rows="10"
  641. bind:value={config.COMFYUI_WORKFLOW}
  642. required
  643. />
  644. {/if} -->
  645. {$i18n.t('Make sure to export a workflow.json file as API format from ComfyUI.')}
  646. </div>
  647. </div>
  648. {#if config.COMFYUI_WORKFLOW}
  649. <div class="mb-2.5">
  650. <div class="flex w-full justify-between items-center">
  651. <div class="text-xs pr-2 shrink-0">
  652. <div class="">
  653. {$i18n.t('ComfyUI Workflow Nodes')}
  654. </div>
  655. </div>
  656. </div>
  657. <div class="mt-1 text-xs flex flex-col gap-1.5">
  658. {#each REQUIRED_WORKFLOW_NODES as node}
  659. <div class="flex w-full flex-col">
  660. <div class="shrink-0">
  661. <div class=" capitalize line-clamp-1 w-20 text-gray-400 dark:text-gray-500">
  662. {node.type}{node.type === 'prompt' ? '*' : ''}
  663. </div>
  664. </div>
  665. <div class="flex mt-0.5 items-center">
  666. <div class="">
  667. <Tooltip content={$i18n.t('Input Key (e.g. text, unet_name, steps)')}>
  668. <input
  669. class="py-1 w-24 text-xs bg-transparent outline-hidden"
  670. placeholder={$i18n.t('Key')}
  671. bind:value={node.key}
  672. required
  673. />
  674. </Tooltip>
  675. </div>
  676. <div class="px-2 text-gray-400 dark:text-gray-500">:</div>
  677. <div class="w-full">
  678. <Tooltip
  679. content={$i18n.t('Comma separated Node Ids (e.g. 1 or 1,2)')}
  680. placement="top-start"
  681. >
  682. <input
  683. class="w-full py-1 text-xs bg-transparent outline-hidden"
  684. placeholder={$i18n.t('Node Ids')}
  685. bind:value={node.node_ids}
  686. />
  687. </Tooltip>
  688. </div>
  689. </div>
  690. </div>
  691. {/each}
  692. </div>
  693. <div class="mt-1 text-xs text-gray-400 dark:text-gray-500">
  694. {$i18n.t('*Prompt node ID(s) are required for image generation')}
  695. </div>
  696. </div>
  697. {/if}
  698. {:else if config?.IMAGE_GENERATION_ENGINE === 'gemini'}
  699. <div class="mb-2.5">
  700. <div class="flex w-full justify-between items-center">
  701. <div class="text-xs pr-2 shrink-0">
  702. <div class="">
  703. {$i18n.t('Gemini Base URL')}
  704. </div>
  705. </div>
  706. <div class="flex w-full">
  707. <div class="flex-1">
  708. <input
  709. class="w-full text-sm bg-transparent outline-hidden text-right"
  710. placeholder={$i18n.t('API Base URL')}
  711. bind:value={config.IMAGES_GEMINI_API_BASE_URL}
  712. />
  713. </div>
  714. </div>
  715. </div>
  716. </div>
  717. <div class="mb-2.5">
  718. <div class="flex w-full justify-between items-center">
  719. <div class="text-xs pr-2 shrink-0">
  720. <div class="">
  721. {$i18n.t('Gemini API Key')}
  722. </div>
  723. </div>
  724. <div class="flex w-full">
  725. <div class="flex-1">
  726. <SensitiveInput
  727. inputClassName="text-right w-full"
  728. placeholder={$i18n.t('API Key')}
  729. bind:value={config.IMAGES_GEMINI_API_KEY}
  730. required={true}
  731. />
  732. </div>
  733. </div>
  734. </div>
  735. </div>
  736. <div class="mb-2.5">
  737. <div class="flex w-full justify-between items-center">
  738. <div class="text-xs pr-2">
  739. <div class="">
  740. {$i18n.t('Gemini Endpoint Method')}
  741. </div>
  742. </div>
  743. <select
  744. class=" dark:bg-gray-900 w-fit pr-8 cursor-pointer rounded-sm px-2 text-xs bg-transparent outline-hidden text-right"
  745. bind:value={config.IMAGES_GEMINI_ENDPOINT_METHOD}
  746. placeholder={$i18n.t('Select Method')}
  747. >
  748. <option value="predict">predict</option>
  749. <option value="generateContent">generateContent</option>
  750. </select>
  751. </div>
  752. </div>
  753. {/if}
  754. </div>
  755. <div class="mb-3">
  756. <div class=" mt-0.5 mb-2.5 text-base font-medium">{$i18n.t('Edit Image')}</div>
  757. <hr class=" border-gray-100 dark:border-gray-850 my-2" />
  758. <div class="mb-2.5">
  759. <div class="flex w-full justify-between items-center">
  760. <div class="text-xs pr-2">
  761. <div class="">
  762. {$i18n.t('Image Edit Engine')}
  763. </div>
  764. </div>
  765. <select
  766. class=" dark:bg-gray-900 w-fit pr-8 cursor-pointer rounded-sm px-2 text-xs bg-transparent outline-hidden text-right"
  767. bind:value={config.IMAGE_EDIT_ENGINE}
  768. placeholder={$i18n.t('Select Engine')}
  769. >
  770. <option value="openai">{$i18n.t('Default (Open AI)')}</option>
  771. <option value="comfyui">{$i18n.t('ComfyUI')}</option>
  772. <option value="gemini">{$i18n.t('Gemini')}</option>
  773. </select>
  774. </div>
  775. </div>
  776. {#if config.ENABLE_IMAGE_GENERATION}
  777. <div class="mb-2.5">
  778. <div class="flex w-full justify-between items-center">
  779. <div class="text-xs pr-2">
  780. <div class="shrink-0">
  781. {$i18n.t('Model')}
  782. </div>
  783. </div>
  784. <Tooltip content={$i18n.t('Enter Model ID')} placement="top-start">
  785. <input
  786. list="model-list"
  787. class="text-right text-sm bg-transparent outline-hidden max-w-full w-52"
  788. bind:value={config.IMAGE_EDIT_MODEL}
  789. placeholder={$i18n.t('Select a model')}
  790. />
  791. <datalist id="model-list">
  792. {#each models ?? [] as model}
  793. <option value={model.id}>{model.name}</option>
  794. {/each}
  795. </datalist>
  796. </Tooltip>
  797. </div>
  798. </div>
  799. <div class="mb-2.5">
  800. <div class="flex w-full justify-between items-center">
  801. <div class="text-xs pr-2">
  802. <div class="shrink-0">
  803. {$i18n.t('Image Size')}
  804. </div>
  805. </div>
  806. <Tooltip content={$i18n.t('Enter Image Size (e.g. 512x512)')} placement="top-start">
  807. <input
  808. class="text-right text-sm bg-transparent outline-hidden max-w-full w-52"
  809. placeholder={$i18n.t('Enter Image Size (e.g. 512x512)')}
  810. bind:value={config.IMAGE_EDIT_SIZE}
  811. />
  812. </Tooltip>
  813. </div>
  814. </div>
  815. {/if}
  816. {#if config?.IMAGE_EDIT_ENGINE === 'openai'}
  817. <div class="mb-2.5">
  818. <div class="flex w-full justify-between items-center">
  819. <div class="text-xs pr-2 shrink-0">
  820. <div class="">
  821. {$i18n.t('OpenAI API Base URL')}
  822. </div>
  823. </div>
  824. <div class="flex w-full">
  825. <div class="flex-1">
  826. <input
  827. class="w-full text-sm bg-transparent outline-hidden text-right"
  828. placeholder={$i18n.t('API Base URL')}
  829. bind:value={config.IMAGES_EDIT_OPENAI_API_BASE_URL}
  830. />
  831. </div>
  832. </div>
  833. </div>
  834. </div>
  835. <div class="mb-2.5">
  836. <div class="flex w-full justify-between items-center">
  837. <div class="text-xs pr-2 shrink-0">
  838. <div class="">
  839. {$i18n.t('OpenAI API Key')}
  840. </div>
  841. </div>
  842. <div class="flex w-full">
  843. <div class="flex-1">
  844. <SensitiveInput
  845. inputClassName="text-right w-full"
  846. placeholder={$i18n.t('API Key')}
  847. bind:value={config.IMAGES_EDIT_OPENAI_API_KEY}
  848. required={false}
  849. />
  850. </div>
  851. </div>
  852. </div>
  853. </div>
  854. <div class="mb-2.5">
  855. <div class="flex w-full justify-between items-center">
  856. <div class="text-xs pr-2 shrink-0">
  857. <div class="">
  858. {$i18n.t('OpenAI API Version')}
  859. </div>
  860. </div>
  861. <div class="flex w-full">
  862. <div class="flex-1">
  863. <input
  864. class="w-full text-sm bg-transparent outline-hidden text-right"
  865. placeholder={$i18n.t('API Version')}
  866. bind:value={config.IMAGES_EDIT_OPENAI_API_VERSION}
  867. />
  868. </div>
  869. </div>
  870. </div>
  871. </div>
  872. {:else if config?.IMAGE_EDIT_ENGINE === 'comfyui'}
  873. <div class="mb-2.5">
  874. <div class="flex w-full justify-between items-center">
  875. <div class="text-xs pr-2 shrink-0">
  876. <div class="">
  877. {$i18n.t('ComfyUI Base URL')}
  878. </div>
  879. </div>
  880. <div class="flex w-full">
  881. <div class="flex-1 mr-2">
  882. <input
  883. class="w-full text-sm bg-transparent outline-hidden text-right"
  884. placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
  885. bind:value={config.IMAGES_EDIT_COMFYUI_BASE_URL}
  886. />
  887. </div>
  888. <button
  889. class=" transition"
  890. type="button"
  891. aria-label="verify connection"
  892. on:click={async () => {
  893. await updateConfigHandler();
  894. const res = await verifyConfigUrl(localStorage.token).catch((error) => {
  895. toast.error(`${error}`);
  896. return null;
  897. });
  898. if (res) {
  899. toast.success($i18n.t('Server connection verified'));
  900. }
  901. }}
  902. >
  903. <svg
  904. xmlns="http://www.w3.org/2000/svg"
  905. viewBox="0 0 20 20"
  906. fill="currentColor"
  907. class="w-4 h-4"
  908. >
  909. <path
  910. fill-rule="evenodd"
  911. 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"
  912. clip-rule="evenodd"
  913. />
  914. </svg>
  915. </button>
  916. </div>
  917. </div>
  918. </div>
  919. <div class="mb-2.5">
  920. <div class="flex w-full justify-between items-center">
  921. <div class="text-xs pr-2 shrink-0">
  922. <div class="">
  923. {$i18n.t('ComfyUI API Key')}
  924. </div>
  925. </div>
  926. <div class="flex w-full">
  927. <div class="flex-1">
  928. <SensitiveInput
  929. inputClassName="text-right w-full"
  930. placeholder={$i18n.t('sk-1234')}
  931. bind:value={config.IMAGES_EDIT_COMFYUI_API_KEY}
  932. required={false}
  933. />
  934. </div>
  935. </div>
  936. </div>
  937. </div>
  938. <div class="mb-2.5">
  939. <input
  940. id="upload-comfyui-edit-workflow-input"
  941. hidden
  942. type="file"
  943. accept=".json"
  944. on:change={(e) => {
  945. const file = e.target.files[0];
  946. const reader = new FileReader();
  947. reader.onload = (e) => {
  948. config.IMAGES_EDIT_COMFYUI_WORKFLOW = e.target.result;
  949. e.target.value = null;
  950. };
  951. reader.readAsText(file);
  952. }}
  953. />
  954. <div class="flex w-full justify-between items-center">
  955. <div class="text-xs pr-2 shrink-0">
  956. <div class="">
  957. {$i18n.t('ComfyUI Workflow')}
  958. </div>
  959. </div>
  960. <div class="flex w-full">
  961. <div class="flex-1 mr-2 justify-end flex gap-1">
  962. {#if config.IMAGES_EDIT_COMFYUI_WORKFLOW}
  963. <button
  964. class="text-xs text-gray-700 dark:text-gray-400 underline"
  965. type="button"
  966. aria-label={$i18n.t('Edit workflow.json content')}
  967. on:click={() => {
  968. // open code editor modal
  969. showComfyUIEditWorkflowEditor = true;
  970. }}
  971. >
  972. {$i18n.t('Edit')}
  973. </button>
  974. {/if}
  975. <Tooltip content={$i18n.t('Click here to upload a workflow.json file.')}>
  976. <button
  977. class="text-xs text-gray-700 dark:text-gray-400 underline"
  978. type="button"
  979. aria-label={$i18n.t('Click here to upload a workflow.json file.')}
  980. on:click={() => {
  981. document.getElementById('upload-comfyui-edit-workflow-input')?.click();
  982. }}
  983. >
  984. {$i18n.t('Upload')}
  985. </button>
  986. </Tooltip>
  987. </div>
  988. </div>
  989. </div>
  990. <div class="mt-1 text-xs text-gray-400 dark:text-gray-500">
  991. <CodeEditorModal
  992. bind:show={showComfyUIEditWorkflowEditor}
  993. value={config.IMAGES_EDIT_COMFYUI_WORKFLOW}
  994. lang="json"
  995. onChange={(e) => {
  996. config.IMAGES_EDIT_COMFYUI_WORKFLOW = e;
  997. }}
  998. onSave={() => {
  999. console.log('Saved');
  1000. }}
  1001. />
  1002. {$i18n.t('Make sure to export a workflow.json file as API format from ComfyUI.')}
  1003. </div>
  1004. </div>
  1005. {#if config.IMAGES_EDIT_COMFYUI_WORKFLOW}
  1006. <div class="mb-2.5">
  1007. <div class="flex w-full justify-between items-center">
  1008. <div class="text-xs pr-2 shrink-0">
  1009. <div class="">
  1010. {$i18n.t('ComfyUI Workflow Nodes')}
  1011. </div>
  1012. </div>
  1013. </div>
  1014. <div class="mt-1 text-xs flex flex-col gap-1.5">
  1015. {#each REQUIRED_EDIT_WORKFLOW_NODES as node}
  1016. <div class="flex w-full flex-col">
  1017. <div class="shrink-0">
  1018. <div class=" capitalize line-clamp-1 w-20 text-gray-400 dark:text-gray-500">
  1019. {node.type}{['prompt', 'image'].includes(node.type) ? '*' : ''}
  1020. </div>
  1021. </div>
  1022. <div class="flex mt-0.5 items-center">
  1023. <div class="">
  1024. <Tooltip content={$i18n.t('Input Key (e.g. text, unet_name, steps)')}>
  1025. <input
  1026. class="py-1 w-24 text-xs bg-transparent outline-hidden"
  1027. placeholder={$i18n.t('Key')}
  1028. bind:value={node.key}
  1029. required
  1030. />
  1031. </Tooltip>
  1032. </div>
  1033. <div class="px-2 text-gray-400 dark:text-gray-500">:</div>
  1034. <div class="w-full">
  1035. <Tooltip
  1036. content={$i18n.t('Comma separated Node Ids (e.g. 1 or 1,2)')}
  1037. placement="top-start"
  1038. >
  1039. <input
  1040. class="w-full py-1 text-xs bg-transparent outline-hidden"
  1041. placeholder={$i18n.t('Node Ids')}
  1042. bind:value={node.node_ids}
  1043. />
  1044. </Tooltip>
  1045. </div>
  1046. </div>
  1047. </div>
  1048. {/each}
  1049. </div>
  1050. <div class="mt-1 text-xs text-gray-400 dark:text-gray-500">
  1051. {$i18n.t('*Prompt node ID(s) are required for image generation')}
  1052. </div>
  1053. </div>
  1054. {/if}
  1055. {:else if config?.IMAGE_GENERATION_ENGINE === 'gemini'}
  1056. <div class="mb-2.5">
  1057. <div class="flex w-full justify-between items-center">
  1058. <div class="text-xs pr-2 shrink-0">
  1059. <div class="">
  1060. {$i18n.t('Gemini Base URL')}
  1061. </div>
  1062. </div>
  1063. <div class="flex w-full">
  1064. <div class="flex-1">
  1065. <input
  1066. class="w-full text-sm bg-transparent outline-hidden text-right"
  1067. placeholder={$i18n.t('API Base URL')}
  1068. bind:value={config.IMAGES_EDIT_GEMINI_API_BASE_URL}
  1069. />
  1070. </div>
  1071. </div>
  1072. </div>
  1073. </div>
  1074. <div class="mb-2.5">
  1075. <div class="flex w-full justify-between items-center">
  1076. <div class="text-xs pr-2 shrink-0">
  1077. <div class="">
  1078. {$i18n.t('Gemini API Key')}
  1079. </div>
  1080. </div>
  1081. <div class="flex w-full">
  1082. <div class="flex-1">
  1083. <SensitiveInput
  1084. inputClassName="text-right w-full"
  1085. placeholder={$i18n.t('API Key')}
  1086. bind:value={config.IMAGES_EDIT_GEMINI_API_KEY}
  1087. required={true}
  1088. />
  1089. </div>
  1090. </div>
  1091. </div>
  1092. </div>
  1093. {/if}
  1094. </div>
  1095. </div>
  1096. {/if}
  1097. </div>
  1098. <div class="flex justify-end pt-3 text-sm font-medium">
  1099. <button
  1100. 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
  1101. ? ' cursor-not-allowed'
  1102. : ''}"
  1103. type="submit"
  1104. disabled={loading}
  1105. >
  1106. {$i18n.t('Save')}
  1107. {#if loading}
  1108. <div class="ml-2 self-center">
  1109. <Spinner />
  1110. </div>
  1111. {/if}
  1112. </button>
  1113. </div>
  1114. </form>