1
+ import { ref , computed , onMounted , inject , watch , nextTick } from "vue"
2
+ import { useClient } from "@servicestack/vue"
3
+ import { createErrorStatus } from "@servicestack/client"
4
+ import { ImageToImage , ActiveMediaModels } from "dtos"
5
+ import { UiLayout , ThreadStorage , HistoryTitle , HistoryGroups , useUiLayout , icons } from "../utils.mjs"
6
+ import { ArtifactGallery } from "./Artifacts.mjs"
7
+ import PromptGenerator from "./PromptGenerator.mjs"
8
+ import FileUpload from "./FileUpload.mjs"
9
+
1
10
export default {
2
- template : `
3
- <h3 class="p-2 sm:block text-xl md:text-2xl font-semibold">Image to Image</h3>
11
+ components : {
12
+ UiLayout,
13
+ HistoryTitle,
14
+ HistoryGroups,
15
+ ArtifactGallery,
16
+ PromptGenerator,
17
+ FileUpload,
18
+ } ,
19
+ template :`
20
+ <UiLayout>
21
+ <template #main>
22
+ <div class="flex flex-1 gap-4 text-base md:gap-5 lg:gap-6">
23
+ <form ref="refForm" :disabled="client.loading.value" class="w-full mb-0" @submit.prevent="send">
24
+ <div class="relative flex h-full max-w-full flex-1 flex-col">
25
+ <div class="flex flex-col w-full items-center">
26
+ <fieldset class="w-full">
27
+ <ErrorSummary :except="visibleFields" class="mb-4" />
28
+ <div class="grid grid-cols-6 gap-4">
29
+ <div class="col-span-6">
30
+ <FileUpload ref="refImage" id="image" v-model="request.image" required
31
+ accept=".webp,.jpg,.jpeg,.png,.gif" @change="renderKey++">
32
+ <template #title>
33
+ <span class="font-semibold text-green-600">Click to upload</span> or drag and drop
34
+ </template>
35
+ <template #icon>
36
+ <svg class="mb-2 h-12 w-12 text-green-500 inline" stroke="currentColor" fill="none" viewBox="0 0 48 48" aria-hidden="true" data-phx-id="m9-phx-F_34be7KYfTF66Xh">
37
+ <path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
38
+ </svg>
39
+ </template>
40
+ </FileUpload>
41
+ </div>
42
+
43
+ <div class="col-span-6 sm:col-span-2">
44
+ <TextInput type="number" id="denoise" v-model="request.denoise" min="0" step="0.01" required />
45
+ </div>
46
+ <div class="col-span-6 sm:col-span-4">
47
+ <TextInput id="negativePrompt" v-model="request.negativePrompt" required placeholder="Negative Prompt" />
48
+ </div>
49
+ <div class="col-span-6 sm:col-span-1">
50
+ <TextInput type="number" id="width" v-model="request.width" min="0" required />
51
+ </div>
52
+ <div class="col-span-6 sm:col-span-1">
53
+ <TextInput type="number" id="height" v-model="request.height" min="0" required />
54
+ </div>
55
+ <div class="col-span-6 sm:col-span-1">
56
+ <TextInput type="number" id="batchSize" label="Image Count" v-model="request.batchSize" min="0" required />
57
+ </div>
58
+ <div class="col-span-6 sm:col-span-1">
59
+ <TextInput type="number" id="seed" v-model="request.seed" min="0" />
60
+ </div>
61
+ <div class="col-span-6 sm:col-span-2">
62
+ <TextInput id="tag" v-model="request.tag" placeholder="Tag" />
63
+ </div>
64
+ </div>
65
+
66
+ <div class="mt-4 flex w-full flex-col gap-1.5 rounded-md p-1.5 transition-colors bg-[#f4f4f4] shadow border border-gray-300">
67
+ <div class="flex items-center gap-1.5 md:gap-2">
68
+ <div class="pl-4 flex min-w-0 flex-1 flex-col">
69
+ <textarea ref="refMessage" id="txtMessage" v-model="request.positivePrompt" :disabled="client.loading.value" rows="2"
70
+ placeholder="Generate Image Prompt..." spellcheck="false" autocomplete="off"
71
+ @keydown.enter.prevent="send"
72
+ :class="[{'opacity-50' : client.loading.value},'m-0 resize-none border-0 bg-transparent px-0 text-token-text-primary focus:ring-0 focus-visible:ring-0 max-h-[25dvh] max-h-52']"
73
+ style="height:60px; overflow-y: hidden;"></textarea>
74
+ </div>
75
+ <button :disabled="!validPrompt || client.loading.value" title="Send (Enter)"
76
+ class="mb-1 me-2 flex h-8 w-8 items-center justify-center rounded-full bg-black text-white transition-colors hover:opacity-70 focus-visible:outline-none focus-visible:outline-black disabled:bg-[#D7D7D7] disabled:text-[#f4f4f4] disabled:hover:opacity-100 dark:bg-white dark:text-black dark:focus-visible:outline-white disabled:dark:bg-token-text-quaternary dark:disabled:text-token-main-surface-secondary">
77
+ <svg v-if="!client.loading.value" class="ml-1 w-5 h-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="currentColor" d="M3 3.732a1.5 1.5 0 0 1 2.305-1.265l6.706 4.267a1.5 1.5 0 0 1 0 2.531l-6.706 4.268A1.5 1.5 0 0 1 3 12.267z"/></svg>
78
+ <svg v-else class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M8 16h8V8H8zm4 6q-2.075 0-3.9-.788t-3.175-2.137T2.788 15.9T2 12t.788-3.9t2.137-3.175T8.1 2.788T12 2t3.9.788t3.175 2.137T21.213 8.1T22 12t-.788 3.9t-2.137 3.175t-3.175 2.138T12 22"/></svg>
79
+ </button>
80
+ </div>
81
+ </div>
82
+ </fieldset>
83
+ </div>
84
+ </div>
85
+ </form>
86
+ </div>
87
+
88
+ <PromptGenerator :thread="thread" promptId="comfyui-image-prompter"
89
+ @save="saveThread()" @selected="selectPrompt($event)" />
90
+
91
+ <div class="pb-20">
92
+
93
+ <div v-if="client.loading.value" class="mt-8 mb-20 flex justify-center">
94
+ <Loading class="text-gray-300 font-normal" imageClass="w-7 h-7 mt-1.5">generating images...</Loading>
95
+ </div>
96
+
97
+ <div v-for="result in getThreadResults()" class="w-full ">
98
+ <div class="flex items-center justify-between">
99
+ <span @click="selectRequest(result.request)" class="cursor-pointer my-4 flex justify-center items-center text-xl hover:underline underline-offset-4" title="New from this">
100
+ {{ result.request.positivePrompt }}
101
+ </span>
102
+ <div class="group flex cursor-pointer" @click="discardResult(result)">
103
+ <div class="ml-1 invisible group-hover:visible">discard</div>
104
+ <svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path fill="currentColor" d="M12 12h2v12h-2zm6 0h2v12h-2z"></path><path fill="currentColor" d="M4 6v2h2v20a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8h2V6zm4 22V8h16v20zm4-26h8v2h-8z"></path></svg>
105
+ </div>
106
+ </div>
107
+
108
+ <ArtifactGallery :results="toArtifacts(result)">
109
+ <template #bottom="{ selected }">
110
+ <div class="z-40 fixed bottom-0 gap-x-6 w-full flex justify-center p-4 bg-black/20">
111
+ <a :href="selected.url + '?download=1'" class="flex text-sm text-gray-300 hover:text-gray-100 hover:drop-shadow">
112
+ <svg class="w-5 h-5 mr-0.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M6 20h12M12 4v12m0 0l3.5-3.5M12 16l-3.5-3.5"></path></svg>
113
+ download
114
+ </a>
115
+ <div @click.stop.prevent="toggleIcon(selected)" class="flex cursor-pointer text-sm text-gray-300 hover:text-gray-100 hover:drop-shadow">
116
+ <svg :class="['w-5 h-5 mr-0.5',selected.url == threadRef.icon ? '-rotate-45' : '']" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m14 18l-8 8M20.667 4L28 11.333l-6.38 6.076a2 2 0 0 0-.62 1.448v3.729c0 .89-1.077 1.337-1.707.707L8.707 12.707c-.63-.63-.184-1.707.707-1.707h3.729a2 2 0 0 0 1.448-.62z"/></svg>
117
+ {{selected.url == threadRef.icon ? 'unpin icon' : 'pin icon' }}
118
+ </div>
119
+ </div>
120
+ </template>
121
+ </ArtifactGallery>
122
+ </div>
123
+ </div>
124
+ </template>
125
+
126
+ <template #sidebar>
127
+ <HistoryTitle :prefix="storage.prefix" />
128
+ <HistoryGroups :history="history" v-slot="{ item }" @save="saveHistoryItem($event)" @remove="removeHistoryItem($event)">
129
+ <Icon class="h-5 w-5 rounded-full flex-shrink-0 mr-1" :src="item.icon ?? icons.image" loading="lazy" :alt="item.model" />
130
+ <span :title="item.title">{{item.title}}</span>
131
+ </HistoryGroups>
132
+ </template>
133
+ </UiLayout>
4
134
` ,
5
- setup ( ) {
6
- return { }
135
+ setup ( props ) {
136
+ const client = useClient ( )
137
+ const routes = inject ( 'routes' )
138
+ const refUi = ref ( )
139
+ const refForm = ref ( )
140
+ const refImage = ref ( )
141
+ const ui = useUiLayout ( refUi )
142
+
143
+ const storage = new ThreadStorage ( `img2img` , {
144
+ denoise : '' ,
145
+ positivePrompt : "" ,
146
+ negativePrompt : '(nsfw),(explicit),(gore),(violence),(blood)' ,
147
+ width : 1024 ,
148
+ height : 1024 ,
149
+ batchSize : 1 ,
150
+ seed : '' ,
151
+ tag : '' ,
152
+ } )
153
+ const error = ref ( )
154
+
155
+ const prefs = ref ( storage . getPrefs ( ) )
156
+ const history = ref ( storage . getHistory ( ) )
157
+ const thread = ref ( )
158
+ const threadRef = ref ( )
159
+
160
+ const validPrompt = computed ( ( ) => ( request . value . model && request . value . positivePrompt
161
+ && request . value . negativePrompt && request . value . width && request . value . height
162
+ && request . value . batchSize ) )
163
+ const refMessage = ref ( )
164
+ const visibleFields = 'denoise,positivePrompt,negativePrompt,width,height,batchSize,seed,tag' . split ( ',' )
165
+ const request = ref ( new ImageToImage ( prefs . value ) )
166
+ const activeModels = ref ( [ ] )
167
+
168
+ function savePrefs ( ) {
169
+ storage . savePrefs ( Object . assign ( { } , request . value , { positivePrompt :'' } ) )
170
+ }
171
+ function loadHistory ( ) {
172
+ history . value = storage . getHistory ( )
173
+ }
174
+ function saveHistory ( ) {
175
+ storage . saveHistory ( history . value )
176
+ }
177
+ function saveThread ( ) {
178
+ if ( thread . value ) {
179
+ storage . saveThread ( thread . value )
180
+ }
181
+ }
182
+
183
+ async function send ( ) {
184
+ if ( ! validPrompt . value || client . loading . value ) return
185
+
186
+ savePrefs ( )
187
+ console . debug ( `${ storage . prefix } .request` , Object . assign ( { } , request . value ) )
188
+
189
+ error . value = null
190
+ let formData = new FormData ( refForm . value )
191
+ const image = formData . get ( 'image' ) . name
192
+
193
+ const api = await client . apiForm ( request . value , formData , { jsconfig : 'eccn' } )
194
+
195
+ /** @type {GenerationResponse } */
196
+ const r = api . response
197
+ if ( r ) {
198
+ console . debug ( `${ storage . prefix } .response` , r )
199
+
200
+ if ( ! r . outputs ?. length ) {
201
+ error . value = createErrorStatus ( "no results were returned" )
202
+ } else {
203
+ const id = parseInt ( routes . id ) || storage . createId ( )
204
+ thread . value = thread . value ?? storage . createThread ( Object . assign ( {
205
+ id : storage . getThreadId ( id ) ,
206
+ title : request . value . positivePrompt
207
+ } , request . value ) )
208
+
209
+ const result = {
210
+ id : storage . createId ( ) ,
211
+ request : Object . assign ( { } , request . value ) ,
212
+ response : r
213
+ }
214
+ thread . value . results . push ( result )
215
+ saveThread ( )
216
+
217
+ if ( ! history . value . find ( x => x . id === id ) ) {
218
+ history . value . push ( {
219
+ id,
220
+ title : thread . value . title ,
221
+ icon : r . outputs [ 0 ] . url
222
+ } )
223
+ }
224
+ saveHistory ( )
225
+
226
+ if ( routes . id !== id ) {
227
+ routes . to ( { id } )
228
+ }
229
+ }
230
+
231
+ } else {
232
+ console . error ( 'send.error' , api . error )
233
+ error . value = api . error
234
+ }
235
+ }
236
+
237
+ function getThreadResults ( ) {
238
+ const ret = Array . from ( thread . value ?. results ?? [ ] )
239
+ ret . reverse ( )
240
+ return ret
241
+ }
242
+
243
+ function toArtifacts ( result ) {
244
+ return result . response ?. outputs ?. map ( x => ( {
245
+ width : result . request . width ,
246
+ height : result . request . height ,
247
+ url : x . url ,
248
+ filePath : x . url . substring ( x . url . indexOf ( '/artifacts' ) ) ,
249
+ } ) ) ?? [ ]
250
+ }
251
+
252
+ function selectRequest ( req ) {
253
+ Object . assign ( request . value , req )
254
+ ui . scrollTop ( )
255
+ }
256
+
257
+ function selectPrompt ( prompt ) {
258
+ request . value . positivePrompt = prompt
259
+ refMessage . value ?. focus ( )
260
+ }
261
+
262
+ function discardResult ( result ) {
263
+ thread . value . results = thread . value . results . filter ( x => x . id != result . id )
264
+ saveThread ( )
265
+ }
266
+
267
+ function toggleIcon ( item ) {
268
+ threadRef . value . icon = item . url
269
+ saveHistory ( )
270
+ }
271
+
272
+ function onRouteChange ( ) {
273
+ console . log ( 'onRouteChange' , routes . id )
274
+ loadHistory ( )
275
+ request . value . positivePrompt = ''
276
+ if ( routes . id ) {
277
+ const id = parseInt ( routes . id )
278
+ thread . value = storage . getThread ( storage . getThreadId ( id ) )
279
+ threadRef . value = history . value . find ( x => x . id === parseInt ( routes . id ) )
280
+ Object . keys ( storage . defaults ) . forEach ( k =>
281
+ request . value [ k ] = thread . value [ k ] ?? storage . defaults [ k ] )
282
+ } else {
283
+ thread . value = null
284
+ Object . keys ( storage . defaults ) . forEach ( k => request . value [ k ] = storage . defaults [ k ] )
285
+ }
286
+ if ( ! request . value . model && activeModels . value ) {
287
+ request . value . model = activeModels . value [ 0 ]
288
+ }
289
+ }
290
+
291
+ function updated ( ) {
292
+ onRouteChange ( )
293
+ }
294
+
295
+ function saveHistoryItem ( item ) {
296
+ storage . saveHistory ( history . value )
297
+ if ( thread . value && item . title ) {
298
+ thread . value . title = item . title
299
+ saveThread ( )
300
+ }
301
+ }
302
+
303
+ function removeHistoryItem ( item ) {
304
+ const idx = history . value . findIndex ( x => x . id === item . id )
305
+ if ( idx >= 0 ) {
306
+ history . value . splice ( idx , 1 )
307
+ storage . saveHistory ( history . value )
308
+ storage . deleteThread ( storage . getThreadId ( item . id ) )
309
+ if ( routes . id == item . id ) {
310
+ routes . to ( { id :undefined } )
311
+ }
312
+ }
313
+ }
314
+
315
+ watch ( ( ) => routes . id , updated )
316
+ watch ( ( ) => [
317
+ request . value . model ,
318
+ request . value . positivePrompt ,
319
+ request . value . negativePrompt ,
320
+ request . value . width ,
321
+ request . value . height ,
322
+ request . value . batchSize ,
323
+ request . value . seed ,
324
+ request . value . tag ,
325
+ ] , ( ) => {
326
+ if ( ! thread . value ) return
327
+ Object . keys ( storage . defaults ) . forEach ( k =>
328
+ thread . value [ k ] = request . value [ k ] ?? storage . defaults [ k ] )
329
+ saveThread ( )
330
+ } )
331
+
332
+ onMounted ( async ( ) => {
333
+ const api = await client . api ( new ActiveMediaModels ( ) )
334
+ if ( api . response ) {
335
+ activeModels . value = api . response . results
336
+ }
337
+ updated ( )
338
+ } )
339
+
340
+ return {
341
+ storage,
342
+ routes,
343
+ client,
344
+ refForm,
345
+ refImage,
346
+ history,
347
+ request,
348
+ visibleFields,
349
+ validPrompt,
350
+ refMessage,
351
+ activeModels,
352
+ thread,
353
+ threadRef,
354
+ icons,
355
+ send,
356
+ saveHistory,
357
+ saveThread,
358
+ toArtifacts,
359
+ toggleIcon,
360
+ selectRequest,
361
+ selectPrompt,
362
+ discardResult,
363
+ getThreadResults,
364
+ saveHistoryItem,
365
+ removeHistoryItem,
366
+ }
7
367
}
8
- }
368
+ }
0 commit comments