@adminforth/bulk-ai-flow 2.0.3 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,327 +0,0 @@
1
-
2
- <template>
3
- <Dialog
4
- ref="dialogRef"
5
- :header="$t('Generate image with AI')"
6
- class="w-full lg:w-[1100px]"
7
- :beforeCancelFunction="async () => { emit('close'); return true; }"
8
- :buttons="dialogButtons"
9
- :close-by-click-outside="false"
10
- >
11
- <div class="flex flex-col gap-4">
12
- <Textarea
13
- v-model="prompt"
14
- :placeholder="$t('Prompt which will be passed to AI network')"
15
- class="w-full text-sm leading-relaxed border border-gray-200 bg-gray-50/30 dark:border-gray-700 dark:bg-gray-800/40 dark:text-gray-100 rounded-xl focus:outline-none focus:border-gray-300 resize-none"
16
- />
17
-
18
- <div class="grid grid-cols-2 gap-4">
19
- <div class="flex flex-col gap-2">
20
- <p class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">
21
- {{ $t('Source Image') }}
22
- </p>
23
- <div class="h-96 rounded-default border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 overflow-hidden flex items-center justify-center">
24
- <img
25
- v-if="attachmentFiles[0]"
26
- :src="attachmentFiles[0]"
27
- class="w-full h-full object-cover"
28
- />
29
- <div v-else class="flex flex-col items-center justify-center gap-2 text-gray-400">
30
- <svg class="w-12 h-12 text-gray-300 stroke-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
31
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
32
- </svg>
33
- <p class="text-xs font-medium">{{ $t('No source image') }}</p>
34
- </div>
35
- </div>
36
- <div v-if="attachmentFiles.length > 1" class="flex flex-wrap gap-1.5">
37
- <img
38
- v-for="(img, idx) in attachmentFiles"
39
- :key="idx"
40
- :src="img"
41
- class="w-10 h-10 object-cover rounded border border-gray-200"
42
- />
43
- </div>
44
- </div>
45
-
46
- <div class="flex flex-col gap-2">
47
- <p class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">
48
- {{ $t('Generated Image') }}
49
- </p>
50
- <div class="relative h-96 rounded-default border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 overflow-hidden">
51
- <div v-if="loading || loadingTimer" class="absolute inset-0 z-10 flex flex-col items-center justify-center bg-white/80 dark:bg-gray-900/80">
52
- <Spinner v-if="loading" class="w-8 h-8" />
53
- <div v-if="loadingTimer" class="mt-3">
54
- <div v-if="!historicalAverage" class="text-gray-800 dark:text-gray-100 text-sm font-semibold">
55
- {{ formatTime(loadingTimer) }} {{ $t('passed...') }}
56
- </div>
57
- <div v-else class="w-40">
58
- <ProgressBar
59
- :currentValue="loadingTimer < historicalAverage ? loadingTimer : historicalAverage"
60
- :minValue="0"
61
- :maxValue="historicalAverage"
62
- :showValues="false"
63
- :progressFormatter="(_value: number, _percentage: number) => `${ formatTime(loadingTimer) } ( ~ ${ Math.floor( (
64
- loadingTimer < historicalAverage ? loadingTimer : historicalAverage
65
- ) / historicalAverage * 100) }% )`"
66
- />
67
- </div>
68
- </div>
69
- </div>
70
-
71
- <div v-if="errorMessage" class="absolute inset-0 z-10 flex items-center justify-center bg-white/80 dark:bg-gray-900/80">
72
- <p class="text-red-500 dark:text-red-400 text-sm font-semibold px-4 text-center">{{ errorMessage }}</p>
73
- </div>
74
-
75
- <Skeleton v-if="!images.length && !loading && !loadingTimer && !errorMessage" type="image" class="w-full h-full" />
76
- <Swiper v-else ref="sliderRef" :images="images" class="h-full" />
77
- </div>
78
- </div>
79
- </div>
80
- </div>
81
- </Dialog>
82
-
83
- </template>
84
-
85
- <script setup lang="ts">
86
-
87
- import { ref, onMounted, nextTick, Ref, computed } from 'vue'
88
- import { callAdminForthApi } from '@/utils';
89
- import { useI18n } from 'vue-i18n';
90
- import adminforth from '@/adminforth';
91
- import { ProgressBar, Dialog, Textarea, Spinner, Skeleton } from '@/afcl';
92
- import Swiper from './Swiper.vue';
93
-
94
- const { t } = useI18n();
95
- const dialogRef = ref(null)
96
- const sliderRef = ref(null)
97
-
98
- const prompt = ref('');
99
- const emit = defineEmits(['close', 'selectImage', 'error', 'updateCarouselIndex']);
100
- const props = defineProps(['meta', 'record', 'images', 'recordId', 'prompt', 'fieldName', 'isError', 'errorMessage', 'carouselImageIndex', 'regenerateImagesRefreshRate','sourceImage', 'imageGenerationPrompts']);
101
- const images = ref([]);
102
- const loading = ref(false);
103
- const attachmentFiles = ref<string[]>([])
104
-
105
- onMounted(async () => {
106
- for (const img of props.images || []) {
107
- images.value.push(img);
108
- }
109
- const temp = await getGenerationPrompt() || '';
110
- attachmentFiles.value = props.sourceImage || [];
111
- prompt.value = Object.keys(JSON.parse(temp))[0];
112
- await nextTick();
113
-
114
- const currentIndex = props.carouselImageIndex || 0;
115
- sliderRef.value?.slideTo(currentIndex);
116
-
117
-
118
- let template = '';
119
- if (prompt.value) {
120
- template = prompt.value;
121
- } else {
122
- template = 'Generate image for field {{field}} in {{resource}}. No text should be on image.';
123
- }
124
- prompt.value = template;
125
- dialogRef.value?.open();
126
- });
127
-
128
- const dialogButtons = computed(() => [
129
- {
130
- label: t('Cancel'),
131
- options: {
132
- class: 'afcl-button',
133
- variant: 'secondary',
134
- },
135
- onclick: () => emit('close'),
136
- },
137
- {
138
- label: t('Use image'),
139
- options: {
140
- class: 'afcl-button',
141
- disabled: loading.value || images.value.length === 0,
142
- loader: loading.value,
143
- },
144
- onclick: () => confirmImage(),
145
- },
146
- {
147
- label: t('Regenerate'),
148
- options: {
149
- class: 'afcl-button',
150
- },
151
- onclick: () => generateImages(),
152
- },
153
- ]);
154
-
155
- async function confirmImage() {
156
- loading.value = true;
157
-
158
- const currentIndex = sliderRef.value?.getActiveIndex() || 0;
159
- const img = images.value[currentIndex];
160
-
161
- props.images.splice(0, props.images.length);
162
- for (const i of images.value) {
163
- props.images.push(i);
164
- }
165
-
166
- emit('updateCarouselIndex', currentIndex, props.recordId, props.fieldName);
167
- emit('selectImage', img, props.recordId, props.fieldName);
168
- emit('close');
169
-
170
- loading.value = false;
171
- }
172
-
173
- const loadingTimer: Ref<number | null> = ref(null);
174
-
175
-
176
- const errorMessage: Ref<string | null> = ref(null);
177
-
178
- const historicalAverage: Ref<number | null> = ref(null);
179
-
180
-
181
- function formatTime(seconds: number): string {
182
- const minutes = Math.floor(seconds / 60);
183
- return `${minutes % 60}m ${Math.floor(seconds % 60)}s`;
184
- }
185
-
186
-
187
- async function getHistoricalAverage() {
188
- const resp = await callAdminForthApi({
189
- path: `/plugin/${props.meta.pluginInstanceId}/averageDuration`,
190
- method: 'GET',
191
- });
192
- historicalAverage.value = resp?.averageDuration || null;
193
- }
194
-
195
- async function getGenerationPrompt() {
196
- const [key, ...rest] = props.imageGenerationPrompts.split(":");
197
- const value = rest.join(":").trim();
198
-
199
- const json = {
200
- [key.trim()]: value
201
- };
202
-
203
- try {
204
- const resp = await callAdminForthApi({
205
- path: `/plugin/${props.meta.pluginInstanceId}/get_image_generation_prompts`,
206
- method: 'POST',
207
- body: {
208
- recordId: props.recordId,
209
- customPrompt: JSON.stringify(json) || {},
210
- },
211
- });
212
- if(!resp) {
213
- emit('error', {
214
- isError: true,
215
- errorMessage: "Error getting generation prompts."
216
- });
217
- }
218
- return resp?.prompt || null;
219
- } catch (e) {
220
- emit('error', {
221
- isError: true,
222
- errorMessage: e.message
223
- });
224
- }
225
- }
226
-
227
- async function generateImages() {
228
- errorMessage.value = null;
229
- loading.value = true;
230
- loadingTimer.value = 0;
231
- const start = Date.now();
232
- const ticker = setInterval(() => {
233
- const elapsed = (Date.now() - start) / 1000;
234
- loadingTimer.value = elapsed;
235
- }, 100);
236
-
237
- await getHistoricalAverage();
238
- let resp = null;
239
- let error = null;
240
- try {
241
- resp = await callAdminForthApi({
242
- path: `/plugin/${props.meta.pluginInstanceId}/create-job`,
243
- method: 'POST',
244
- body: {
245
- actionType: 'regenerate_images',
246
- recordId: props.recordId,
247
- prompt: prompt.value,
248
- fieldName: props.fieldName
249
- },
250
- });
251
- } catch (e) {
252
- console.error(e);
253
- }
254
-
255
- if (resp?.error) {
256
- error = resp.error;
257
- }
258
- if (!resp) {
259
- error = t('Error creating image generation job');
260
- }
261
-
262
- if (error) {
263
- if (images.value.length === 0) {
264
- errorMessage.value = error;
265
- } else {
266
- adminforth.alert({
267
- message: error,
268
- variant: 'danger',
269
- timeout: 'unlimited',
270
- });
271
- }
272
- return;
273
- }
274
-
275
- const jobId = resp.jobId;
276
- let jobStatus = null;
277
- let jobResponse = null;
278
- while (jobStatus !== 'completed' && jobStatus !== 'failed') {
279
- jobResponse = await callAdminForthApi({
280
- path: `/plugin/${props.meta.pluginInstanceId}/get-job-status`,
281
- method: 'POST',
282
- body: { jobId },
283
- });
284
- if (jobResponse?.error) {
285
- error = jobResponse.error;
286
- break;
287
- };
288
- jobStatus = jobResponse?.job?.status;
289
- if (jobStatus === 'failed') {
290
- error = jobResponse?.job?.error || t('Image generation job failed');
291
- }
292
- await new Promise((resolve) => setTimeout(resolve, props.regenerateImagesRefreshRate));
293
- }
294
-
295
- if (error) {
296
- adminforth.alert({
297
- message: error,
298
- variant: 'danger',
299
- timeout: 'unlimited',
300
- });
301
- clearInterval(ticker);
302
- loadingTimer.value = null;
303
- loading.value = false;
304
- return;
305
- }
306
-
307
- const respImages = jobResponse?.job?.result[props.fieldName] || [];
308
-
309
- images.value = [
310
- ...images.value,
311
- ...respImages,
312
- ];
313
-
314
- clearInterval(ticker);
315
- loadingTimer.value = null;
316
- loading.value = false;
317
-
318
- await nextTick();
319
-
320
- sliderRef.value?.slideTo(images.value.length-1);
321
-
322
- await nextTick();
323
-
324
- loading.value = false;
325
- }
326
-
327
- </script>
package/custom/Swiper.vue DELETED
@@ -1,76 +0,0 @@
1
- <template>
2
- <swiper-container class="flex items-center justify-center w-full h-full">
3
- <swiper-slide v-for="(image, index) in images" :key="index">
4
- <img :src="image" class="object-cover w-full h-full" />
5
- </swiper-slide>
6
- </swiper-container>
7
- </template>
8
-
9
- <script setup lang="ts">
10
- import { onMounted } from 'vue'
11
- import { register } from 'swiper/element/bundle'
12
- import { SwiperOptions } from 'swiper/types';
13
-
14
- const props = defineProps<{images: string[]}>()
15
- let swiperEl: any;
16
-
17
- function getActiveIndex() {
18
- if (swiperEl && swiperEl.swiper) {
19
- return swiperEl.swiper.activeIndex;
20
- }
21
- return 0;
22
- }
23
-
24
- function slideTo(index) {
25
-
26
- if (!swiperEl || !swiperEl.swiper) {
27
- setTimeout(() => slideTo(index), 50);
28
- return;
29
- }
30
-
31
- if (index >= 0 && index < props.images.length) {
32
- swiperEl.swiper.update();
33
- setTimeout(() => {
34
- swiperEl.swiper.slideTo(index, 300);
35
- }, 10);
36
- }
37
- }
38
-
39
- defineExpose({
40
- getActiveIndex,
41
- slideTo
42
- })
43
-
44
- register()
45
- onMounted(() => {
46
- swiperEl = document.querySelector('swiper-container')
47
-
48
- const swiperParams: SwiperOptions = {
49
- slidesPerView: 1,
50
- navigation: true,
51
- pagination: {
52
- type: 'fraction',
53
- },
54
- allowTouchMove: true,
55
- }
56
-
57
- Object.assign(swiperEl, swiperParams)
58
- swiperEl.initialize()
59
- })
60
- </script>
61
-
62
- <style>
63
- .swiper {
64
- width: 100%;
65
- height: 100%;
66
- }
67
-
68
- .swiper-slide {
69
- text-align: center;
70
- font-size: 18px;
71
- background: #444;
72
- display: flex;
73
- justify-content: center;
74
- align-items: center;
75
- }
76
- </style>