@adminforth/bulk-ai-flow 2.0.4 → 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,185 +0,0 @@
1
- <template>
2
- <!-- Popup Overlay -->
3
- <div class="fixed inset-0 z-40 flex items-center justify-center bg-lightScrim dark:bg-darkScrim" @click.self="closePopup">
4
- <div class="image-compare-container shadow-sm p-4 border-white dark:border-black max-w-4xl max-h-[90vh] overflow-y-auto rounded-lg bg-white dark:bg-gray-800">
5
- <!-- Close Button -->
6
- <div class="flex justify-end mb-4">
7
- <button type="button"
8
- @click="closePopup"
9
- class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white" >
10
- <svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
11
- <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
12
- </svg>
13
- <span class="sr-only">{{ $t('Close modal') }}</span>
14
- </button>
15
- </div>
16
- <div class="flex gap-4 items-start justify-between">
17
- <h3 class="text-sm font-medium text-gray-700 dark:text-white mb-2">{{ $t('Old Image') }}</h3>
18
- <h3 class="text-sm font-medium text-gray-700 dark:text-white mb-2">{{ $t('New Image') }}</h3>
19
- </div>
20
- <div class="flex gap-4 items-center">
21
- <!-- Old Image -->
22
- <div class="flex-1">
23
- <div class="relative">
24
- <img
25
- v-if="isValidUrl(compiledOldImage)"
26
- ref="oldImg"
27
- :src="compiledOldImage"
28
- alt="Old image"
29
- class="w-full max-w-sm h-auto object-cover rounded-lg cursor-pointer border hover:border-blue-500 transition-colors duration-200"
30
- />
31
- <div v-else class="w-full max-w-sm h-48 bg-gray-100 rounded-lg flex items-center justify-center">
32
- <p class="text-gray-500">{{ $t('No old image') }}</p>
33
- </div>
34
- </div>
35
- </div>
36
-
37
- <!-- Comparison Arrow -->
38
- <div class="flex items-center justify-center">
39
- <div class="w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center dark:bg-gray-500">
40
- <svg class="w-4 h-4 text-blue-600 dark:text-black" fill="none" stroke="currentColor" viewBox="0 0 24 24">
41
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
42
- </svg>
43
- </div>
44
- </div>
45
-
46
- <!-- New Image -->
47
- <div class="flex-1">
48
- <div class="relative">
49
- <img
50
- v-if="isValidUrl(newImage)"
51
- ref="newImg"
52
- :src="newImage"
53
- alt="New image"
54
- class="w-full max-w-sm h-auto object-cover rounded-lg cursor-pointer border hover:border-blue-500 transition-colors duration-200"
55
- />
56
- <div v-else class="w-full max-w-sm h-48 bg-gray-100 rounded-lg flex items-center justify-center">
57
- <p class="text-gray-500">{{ $t('No new image') }}</p>
58
- </div>
59
- </div>
60
- </div>
61
- </div>
62
- </div>
63
- </div>
64
-
65
-
66
- </template>
67
-
68
- <script setup lang="ts">
69
- import { ref, onMounted, watch, nextTick } from 'vue'
70
- import mediumZoom from 'medium-zoom'
71
- import { callAdminForthApi } from '@/utils';
72
-
73
- const props = defineProps<{
74
- oldImage: string
75
- newImage: string
76
- meta: any
77
- columnName: string
78
- }>()
79
-
80
- const emit = defineEmits<{
81
- close: []
82
- }>()
83
-
84
- const oldImg = ref<HTMLImageElement | null>(null)
85
- const newImg = ref<HTMLImageElement | null>(null)
86
- const oldZoom = ref<any>(null)
87
- const newZoom = ref<any>(null)
88
- const compiledOldImage = ref<string>('')
89
-
90
- async function compileOldImage() {
91
- try {
92
- const res = await callAdminForthApi({
93
- path: `/plugin/${props.meta.pluginInstanceId}/compile_old_image_link`,
94
- method: 'POST',
95
- body: {
96
- image: props.oldImage,
97
- columnName: props.columnName,
98
- },
99
- });
100
- compiledOldImage.value = res.previewUrl;
101
- } catch (e) {
102
- console.error("Error compiling old image:", e)
103
- return;
104
- }
105
- }
106
-
107
- function closePopup() {
108
- emit('close')
109
- }
110
-
111
- function isValidUrl(str: string): boolean {
112
- if (!str) return false
113
- try {
114
- new URL(str)
115
- return true
116
- } catch {
117
- return false
118
- }
119
- }
120
-
121
- function initializeZoom() {
122
- // Clean up existing zoom instances
123
- if (oldZoom.value) {
124
- oldZoom.value.detach()
125
- }
126
- if (newZoom.value) {
127
- newZoom.value.detach()
128
- }
129
-
130
- // Initialize zoom for old image
131
- if (oldImg.value && isValidUrl(compiledOldImage.value)) {
132
- oldZoom.value = mediumZoom(oldImg.value, {
133
- margin: 24,
134
- background: 'rgba(0, 0, 0, 0.8)',
135
- scrollOffset: 150
136
- })
137
- }
138
-
139
- // Initialize zoom for new image
140
- if (newImg.value && isValidUrl(props.newImage)) {
141
- newZoom.value = mediumZoom(newImg.value, {
142
- margin: 24,
143
- background: 'rgba(0, 0, 0, 0.8)',
144
- scrollOffset: 150
145
- })
146
- }
147
- }
148
-
149
- onMounted(async () => {
150
- await compileOldImage()
151
- await nextTick()
152
- initializeZoom()
153
- })
154
-
155
- // Re-initialize zoom when images change
156
- watch([() => props.oldImage, () => props.newImage, () => compiledOldImage.value], async () => {
157
- await nextTick()
158
- initializeZoom()
159
- })
160
- </script>
161
-
162
- <style scoped>
163
- .medium-zoom-image {
164
- z-index: 999999;
165
- background: rgba(0, 0, 0, 0.8);
166
- border: none;
167
- border-radius: 0;
168
- }
169
- .medium-zoom-overlay {
170
- z-index: 99999;
171
- background: rgba(0, 0, 0, 0.8);
172
- }
173
- html.dark .medium-zoom-overlay {
174
- background: rgba(17, 24, 39, 0.8);
175
- }
176
- body.medium-zoom--opened aside {
177
- filter: grayscale(1);
178
- }
179
- .fade-enter-active, .fade-leave-active {
180
- transition: opacity 0.3s ease;
181
- }
182
- .fade-enter-from, .fade-leave-to {
183
- opacity: 0;
184
- }
185
- </style>
@@ -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>
@@ -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>