@adminforth/bulk-ai-flow 1.10.2 → 1.11.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.
- package/build.log +3 -2
- package/custom/ImageGenerationCarousel.vue +35 -168
- package/custom/Swiper.vue +76 -0
- package/custom/VisionAction.vue +74 -31
- package/custom/VisionTable.vue +61 -9
- package/custom/package-lock.json +99 -3527
- package/custom/package.json +2 -1
- package/dist/custom/ImageGenerationCarousel.vue +35 -168
- package/dist/custom/Swiper.vue +76 -0
- package/dist/custom/VisionAction.vue +74 -31
- package/dist/custom/VisionTable.vue +61 -9
- package/dist/custom/package-lock.json +99 -3527
- package/dist/custom/package.json +2 -1
- package/dist/index.js +49 -46
- package/index.ts +46 -48
- package/package.json +1 -1
package/build.log
CHANGED
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
sending incremental file list
|
|
6
6
|
custom/
|
|
7
7
|
custom/ImageGenerationCarousel.vue
|
|
8
|
+
custom/Swiper.vue
|
|
8
9
|
custom/VisionAction.vue
|
|
9
10
|
custom/VisionTable.vue
|
|
10
11
|
custom/package-lock.json
|
|
11
12
|
custom/package.json
|
|
12
13
|
custom/tsconfig.json
|
|
13
14
|
|
|
14
|
-
sent
|
|
15
|
-
total size is
|
|
15
|
+
sent 62,830 bytes received 153 bytes 125,966.00 bytes/sec
|
|
16
|
+
total size is 62,264 speedup is 0.99
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
|
|
2
2
|
<template>
|
|
3
3
|
<!-- Main modal -->
|
|
4
|
-
<div tabindex="-1" class="fixed inset-0 z-10 flex justify-center items-center dark:bg-gray-900/50 overflow-y-auto">
|
|
5
|
-
<div class="relative p-4 w-full max-w-[1600px]
|
|
4
|
+
<div tabindex="-1" class="[scrollbar-gutter:stable] fixed inset-0 z-10 flex justify-center items-center bg-gray-800/50 dark:bg-gray-900/50 overflow-y-auto">
|
|
5
|
+
<div class="relative p-4 w-full max-w-[1600px]">
|
|
6
6
|
<!-- Modal content -->
|
|
7
7
|
<div class="relative bg-white rounded-lg shadow-xl dark:bg-gray-700">
|
|
8
8
|
<!-- Modal header -->
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
</button>
|
|
21
21
|
</div>
|
|
22
22
|
<!-- Modal body -->
|
|
23
|
-
<div class="p-4 md:p-5
|
|
23
|
+
<div class="p-4 md:p-5">
|
|
24
24
|
<!-- PROMPT TEXTAREA -->
|
|
25
25
|
<!-- Textarea -->
|
|
26
26
|
<textarea
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
<!-- Fullscreen Modal -->
|
|
48
48
|
<div
|
|
49
49
|
v-if="zoomedImage"
|
|
50
|
-
class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-80"
|
|
50
|
+
class="w-full h-full fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-80"
|
|
51
51
|
@click.self="closeZoom"
|
|
52
52
|
>
|
|
53
53
|
<img
|
|
@@ -98,79 +98,30 @@
|
|
|
98
98
|
<div id="gallery" class="relative w-full min-w-0" data-carousel="static">
|
|
99
99
|
<!-- Carousel wrapper -->
|
|
100
100
|
<div class="relative h-56 overflow-hidden rounded-lg md:h-[calc(100vh-400px)]">
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
class="flex items-center justify-center w-full h-full"
|
|
106
|
-
:class="[
|
|
107
|
-
index === 0 ? 'block' : 'hidden'
|
|
108
|
-
]"
|
|
109
|
-
data-carousel-item
|
|
110
|
-
>
|
|
111
|
-
<img :src="img" class="max-w-full max-h-full object-contain"
|
|
112
|
-
:alt="`Generated image ${index + 1}`"
|
|
113
|
-
/>
|
|
114
|
-
</div>
|
|
115
|
-
|
|
116
|
-
<div v-if="images.length === 0" class="flex items-center justify-center w-full h-full">
|
|
117
|
-
|
|
118
|
-
<button @click="generateImages" type="button" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4
|
|
119
|
-
focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center
|
|
120
|
-
dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 ms-2">{{ $t('Generate images') }}</button>
|
|
121
|
-
|
|
122
|
-
</div>
|
|
123
|
-
|
|
101
|
+
<Swiper
|
|
102
|
+
ref="sliderRef"
|
|
103
|
+
:images="images"
|
|
104
|
+
/>
|
|
124
105
|
</div>
|
|
125
|
-
<!-- Slider controls -->
|
|
126
|
-
<button type="button" class="absolute top-0 start-0 z-30 flex items-center justify-center h-full px-4 cursor-pointer group focus:outline-none"
|
|
127
|
-
@click="slide(-1)"
|
|
128
|
-
:disabled="images.length === 0"
|
|
129
|
-
>
|
|
130
|
-
<span class="inline-flex items-center justify-center w-10 h-10 rounded-full bg-white/30 dark:bg-gray-800/30 group-hover:bg-white/50 dark:group-hover:bg-gray-800/60 group-focus:ring-4 group-focus:ring-white dark:group-focus:ring-gray-800/70 group-focus:outline-none ">
|
|
131
|
-
<svg class="w-4 h-4 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10"
|
|
132
|
-
:class="{
|
|
133
|
-
'text-gray-800 dark:text-gray-200': images.length > 0,
|
|
134
|
-
'text-gray-200 dark:text-gray-800': images.length === 0
|
|
135
|
-
}"
|
|
136
|
-
>
|
|
137
|
-
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 1 1 5l4 4"/>
|
|
138
|
-
</svg>
|
|
139
|
-
<span class="sr-only">{{ $t('Previous') }}</span>
|
|
140
|
-
</span>
|
|
141
|
-
</button>
|
|
142
|
-
<button type="button" class="absolute top-0 end-0 z-30 flex items-center justify-center h-full px-4 cursor-pointer group focus:outline-none "
|
|
143
|
-
:disabled="images.length === 0"
|
|
144
|
-
@click="slide(1)"
|
|
145
|
-
>
|
|
146
|
-
<span class="inline-flex items-center justify-center w-10 h-10 rounded-full bg-white/30 dark:bg-gray-800/30 group-hover:bg-white/50 dark:group-hover:bg-gray-800/60 group-focus:ring-4 group-focus:ring-white dark:group-focus:ring-gray-800/70 group-focus:outline-none ">
|
|
147
|
-
<svg class="w-4 h-4 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10"
|
|
148
|
-
:class="{
|
|
149
|
-
'text-gray-800 dark:text-gray-200': images.length > 0,
|
|
150
|
-
'text-gray-200 dark:text-gray-800': images.length === 0
|
|
151
|
-
}"
|
|
152
|
-
>
|
|
153
|
-
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 9 4-4-4-4"/>
|
|
154
|
-
</svg>
|
|
155
|
-
<span class="sr-only">{{ $t('Next') }}</span>
|
|
156
|
-
</span>
|
|
157
|
-
</button>
|
|
158
|
-
|
|
159
|
-
|
|
160
106
|
</div>
|
|
161
107
|
</div>
|
|
162
108
|
</div>
|
|
163
109
|
<!-- Modal footer -->
|
|
164
|
-
<div class="flex
|
|
165
|
-
<button type="button"
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
@click="
|
|
173
|
-
|
|
110
|
+
<div class="flex justify-between p-4 md:p-5 border-t border-gray-200 rounded-b dark:border-gray-600 gap-3">
|
|
111
|
+
<button type="button" class="px-5 py-2.5 bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-purple-300 dark:focus:ring-purple-800 rounded-md text-white"
|
|
112
|
+
@click="generateImages"
|
|
113
|
+
>{{ $t('Regenerate') }}</button>
|
|
114
|
+
<div class="flex gap-3">
|
|
115
|
+
<button type="button" class="py-2.5 px-5 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
|
|
116
|
+
@click="emit('close')"
|
|
117
|
+
>{{ $t('Cancel') }}</button>
|
|
118
|
+
<button type="button" @click="confirmImage"
|
|
119
|
+
:disabled="loading || images.length === 0"
|
|
120
|
+
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center
|
|
121
|
+
dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800
|
|
122
|
+
disabled:opacity-50 disabled:cursor-not-allowed"
|
|
123
|
+
>{{ $t('Use image') }}</button>
|
|
124
|
+
</div>
|
|
174
125
|
</div>
|
|
175
126
|
</div>
|
|
176
127
|
</div>
|
|
@@ -185,115 +136,45 @@ import { callAdminForthApi } from '@/utils';
|
|
|
185
136
|
import { useI18n } from 'vue-i18n';
|
|
186
137
|
import adminforth from '@/adminforth';
|
|
187
138
|
import { ProgressBar } from '@/afcl';
|
|
139
|
+
import Swiper from './Swiper.vue';
|
|
188
140
|
|
|
189
141
|
const { t: $t } = useI18n();
|
|
142
|
+
const sliderRef = ref(null)
|
|
190
143
|
|
|
191
144
|
const prompt = ref('');
|
|
192
145
|
const emit = defineEmits(['close', 'selectImage', 'error', 'updateCarouselIndex']);
|
|
193
|
-
const props = defineProps(['meta', 'record', 'images', 'recordId', 'prompt', 'fieldName', 'isError', 'errorMessage', 'carouselImageIndex', 'regenerateImagesRefreshRate']);
|
|
146
|
+
const props = defineProps(['meta', 'record', 'images', 'recordId', 'prompt', 'fieldName', 'isError', 'errorMessage', 'carouselImageIndex', 'regenerateImagesRefreshRate','sourceImage']);
|
|
194
147
|
const images = ref([]);
|
|
195
148
|
const loading = ref(false);
|
|
196
149
|
const attachmentFiles = ref<string[]>([])
|
|
197
150
|
|
|
198
|
-
function minifyField(field: string): string {
|
|
199
|
-
if (field.length > 100) {
|
|
200
|
-
return field.slice(0, 100) + '...';
|
|
201
|
-
}
|
|
202
|
-
return field;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const caurosel = ref(null);
|
|
206
151
|
onMounted(async () => {
|
|
207
152
|
for (const img of props.images || []) {
|
|
208
153
|
images.value.push(img);
|
|
209
154
|
}
|
|
210
155
|
const temp = await getGenerationPrompt() || '';
|
|
156
|
+
attachmentFiles.value = props.sourceImage || [];
|
|
211
157
|
prompt.value = temp[props.fieldName];
|
|
212
158
|
await nextTick();
|
|
213
159
|
|
|
214
160
|
const currentIndex = props.carouselImageIndex || 0;
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
images.value.map((img, index) => {
|
|
218
|
-
return {
|
|
219
|
-
image: img,
|
|
220
|
-
el: document.getElementById('gallery').querySelector(`[data-carousel-item]:nth-child(${index + 1})`),
|
|
221
|
-
position: index,
|
|
222
|
-
};
|
|
223
|
-
}),
|
|
224
|
-
{
|
|
225
|
-
internal: 0,
|
|
226
|
-
defaultPosition: currentIndex,
|
|
227
|
-
},
|
|
228
|
-
{
|
|
229
|
-
override: true,
|
|
230
|
-
}
|
|
231
|
-
);
|
|
161
|
+
sliderRef.value?.slideTo(currentIndex);
|
|
162
|
+
|
|
232
163
|
|
|
233
|
-
const context = {
|
|
234
|
-
field: props.meta.pathColumnLabel,
|
|
235
|
-
resource: props.meta.resourceLabel,
|
|
236
|
-
};
|
|
237
164
|
let template = '';
|
|
238
165
|
if (prompt.value) {
|
|
239
166
|
template = prompt.value;
|
|
240
167
|
} else {
|
|
241
168
|
template = 'Generate image for field {{field}} in {{resource}}. No text should be on image.';
|
|
242
169
|
}
|
|
243
|
-
|
|
244
|
-
// if field is not present in props.record[field] then replace it with empty string and drop warning
|
|
245
|
-
const regex = /{{(.*?)}}/g;
|
|
246
|
-
const matches = template.match(regex);
|
|
247
|
-
if (matches) {
|
|
248
|
-
matches.forEach((match) => {
|
|
249
|
-
const field = match.replace(/{{|}}/g, '').trim();
|
|
250
|
-
if (field in context) {
|
|
251
|
-
return;
|
|
252
|
-
} else if (field in props.record) {
|
|
253
|
-
context[field] = minifyField(props.record[field]);
|
|
254
|
-
} else {
|
|
255
|
-
adminforth.alert({
|
|
256
|
-
message: $t('Field {{field}} defined in template but not found in record', { field }),
|
|
257
|
-
variant: 'warning',
|
|
258
|
-
timeout: 15,
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
prompt.value = template.replace(regex, (_, field) => {
|
|
265
|
-
return context[field.trim()] || '';
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
const recordId = props.record[props.meta.recorPkFieldName];
|
|
269
|
-
if (!recordId) {
|
|
270
|
-
emit('error', {
|
|
271
|
-
isError: true,
|
|
272
|
-
errorMessage: 'Record ID not found, cannot generate images'
|
|
273
|
-
});
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
|
|
170
|
+
prompt.value = template;
|
|
277
171
|
});
|
|
278
172
|
|
|
279
|
-
async function slide(direction: number) {
|
|
280
|
-
if (!caurosel.value) return;
|
|
281
|
-
const curPos = caurosel.value.getActiveItem().position;
|
|
282
|
-
if (curPos === 0 && direction === -1) return;
|
|
283
|
-
if (curPos === images.value.length - 1 && direction === 1) {
|
|
284
|
-
await generateImages();
|
|
285
|
-
}
|
|
286
|
-
if (direction === 1) {
|
|
287
|
-
caurosel.value.next();
|
|
288
|
-
} else {
|
|
289
|
-
caurosel.value.prev();
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
173
|
|
|
293
174
|
async function confirmImage() {
|
|
294
175
|
loading.value = true;
|
|
295
176
|
|
|
296
|
-
const currentIndex =
|
|
177
|
+
const currentIndex = sliderRef.value?.getActiveIndex() || 0;
|
|
297
178
|
const img = images.value[currentIndex];
|
|
298
179
|
|
|
299
180
|
props.images.splice(0, props.images.length);
|
|
@@ -363,7 +244,6 @@ async function generateImages() {
|
|
|
363
244
|
const elapsed = (Date.now() - start) / 1000;
|
|
364
245
|
loadingTimer.value = elapsed;
|
|
365
246
|
}, 100);
|
|
366
|
-
const currentIndex = caurosel.value?.getActiveItem()?.position || 0;
|
|
367
247
|
|
|
368
248
|
await getHistoricalAverage();
|
|
369
249
|
let resp = null;
|
|
@@ -429,6 +309,9 @@ async function generateImages() {
|
|
|
429
309
|
variant: 'danger',
|
|
430
310
|
timeout: 'unlimited',
|
|
431
311
|
});
|
|
312
|
+
clearInterval(ticker);
|
|
313
|
+
loadingTimer.value = null;
|
|
314
|
+
loading.value = false;
|
|
432
315
|
return;
|
|
433
316
|
}
|
|
434
317
|
|
|
@@ -445,24 +328,8 @@ async function generateImages() {
|
|
|
445
328
|
|
|
446
329
|
await nextTick();
|
|
447
330
|
|
|
331
|
+
sliderRef.value?.slideTo(images.value.length-1);
|
|
448
332
|
|
|
449
|
-
caurosel.value = new Carousel(
|
|
450
|
-
document.getElementById('gallery'),
|
|
451
|
-
images.value.map((img, index) => {
|
|
452
|
-
return {
|
|
453
|
-
image: img,
|
|
454
|
-
el: document.getElementById('gallery').querySelector(`[data-carousel-item]:nth-child(${index + 1})`),
|
|
455
|
-
position: index,
|
|
456
|
-
};
|
|
457
|
-
}),
|
|
458
|
-
{
|
|
459
|
-
internal: 0,
|
|
460
|
-
defaultPosition: currentIndex,
|
|
461
|
-
},
|
|
462
|
-
{
|
|
463
|
-
override: true,
|
|
464
|
-
}
|
|
465
|
-
);
|
|
466
333
|
await nextTick();
|
|
467
334
|
|
|
468
335
|
loading.value = false;
|
|
@@ -0,0 +1,76 @@
|
|
|
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-contain 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>
|
package/custom/VisionAction.vue
CHANGED
|
@@ -8,14 +8,15 @@
|
|
|
8
8
|
<Dialog
|
|
9
9
|
ref="confirmDialog"
|
|
10
10
|
header="Bulk AI Flow"
|
|
11
|
-
class="!max-w-full w-full lg:w-[1600px] !lg:max-w-[1600px]"
|
|
11
|
+
class="[scrollbar-gutter:stable] !max-w-full w-full lg:w-[1600px] !lg:max-w-[1600px]"
|
|
12
12
|
:beforeCloseFunction="closeDialog"
|
|
13
13
|
:buttons="[
|
|
14
|
-
{ label: checkedCount > 1 ? 'Save fields' : 'Save field', options: { disabled: isLoading || checkedCount < 1 || isCriticalError || isFetchingRecords || isGeneratingImages || isAnalizingFields || isAnalizingImages, loader: isLoading, class: 'w-fit
|
|
15
|
-
{ label: 'Cancel', onclick: (dialog) => dialog.hide() },
|
|
14
|
+
{ label: checkedCount > 1 ? 'Save fields' : 'Save field', options: { disabled: isLoading || checkedCount < 1 || isCriticalError || isFetchingRecords || isGeneratingImages || isAnalizingFields || isAnalizingImages, loader: isLoading, class: 'w-fit' }, onclick: async (dialog) => { await saveData(); dialog.hide(); } },
|
|
15
|
+
{ label: 'Cancel', options: {class: 'bg-white hover:!bg-gray-100 !text-gray-900 hover:!text-gray-800 dark:!bg-gray-800 dark:!text-gray-100 dark:hover:!bg-gray-700 !border-gray-200'}, onclick: (dialog) => dialog.hide() },
|
|
16
16
|
]"
|
|
17
|
+
:click-to-close-outside="false"
|
|
17
18
|
>
|
|
18
|
-
<div class="bulk-vision-table flex flex-col items-center max-w-[1560px] md:max-h-[90vh] gap-3 md:gap-4 w-full h-full overflow-y-auto">
|
|
19
|
+
<div class="[scrollbar-gutter:stable] bulk-vision-table flex flex-col items-center max-w-[1560px] md:max-h-[90vh] gap-3 md:gap-4 w-full h-full overflow-y-auto">
|
|
19
20
|
<div v-if="records && props.checkboxes.length" class="w-full overflow-x-auto">
|
|
20
21
|
<VisionTable
|
|
21
22
|
:checkbox="props.checkboxes"
|
|
@@ -35,6 +36,11 @@
|
|
|
35
36
|
:carouselSaveImages="carouselSaveImages"
|
|
36
37
|
:carouselImageIndex="carouselImageIndex"
|
|
37
38
|
:regenerateImagesRefreshRate="props.meta.refreshRates?.regenerateImages"
|
|
39
|
+
:isAiGenerationError="isAiGenerationError"
|
|
40
|
+
:aiGenerationErrorMessage="aiGenerationErrorMessage"
|
|
41
|
+
:isAiImageGenerationError="isAiImageGenerationError"
|
|
42
|
+
:imageGenerationErrorMessage="imageGenerationErrorMessage"
|
|
43
|
+
@regenerate-images="regenerateImages"
|
|
38
44
|
/>
|
|
39
45
|
</div>
|
|
40
46
|
<div class="text-red-600 flex items-center w-full">
|
|
@@ -52,6 +58,7 @@ import VisionTable from './VisionTable.vue'
|
|
|
52
58
|
import adminforth from '@/adminforth';
|
|
53
59
|
import { useI18n } from 'vue-i18n';
|
|
54
60
|
import { AdminUser, type AdminForthResourceCommon } from '@/types';
|
|
61
|
+
import { run } from 'node:test';
|
|
55
62
|
|
|
56
63
|
const { t } = useI18n();
|
|
57
64
|
const props = defineProps<{
|
|
@@ -93,6 +100,10 @@ const isGeneratingImages = ref(false);
|
|
|
93
100
|
const isAnalizingFields = ref(false);
|
|
94
101
|
const isAnalizingImages = ref(false);
|
|
95
102
|
const isDialogOpen = ref(false);
|
|
103
|
+
const isAiGenerationError = ref<boolean[]>([false]);
|
|
104
|
+
const aiGenerationErrorMessage = ref<string[]>([]);
|
|
105
|
+
const isAiImageGenerationError = ref<boolean[]>([false]);
|
|
106
|
+
const imageGenerationErrorMessage = ref<string[]>([]);
|
|
96
107
|
|
|
97
108
|
const openDialog = async () => {
|
|
98
109
|
isDialogOpen.value = true;
|
|
@@ -415,47 +426,59 @@ async function runAiAction({
|
|
|
415
426
|
actionType,
|
|
416
427
|
responseFlag,
|
|
417
428
|
updateOnSuccess = true,
|
|
429
|
+
recordsIds = props.checkboxes,
|
|
430
|
+
disableRateLimitCheck = false,
|
|
418
431
|
}: {
|
|
419
432
|
endpoint: string;
|
|
420
433
|
actionType: 'analyze' | 'analyze_no_images' | 'generate_images';
|
|
421
434
|
responseFlag: Ref<boolean[]>;
|
|
422
435
|
updateOnSuccess?: boolean;
|
|
436
|
+
recordsIds?: any[];
|
|
437
|
+
disableRateLimitCheck?: boolean;
|
|
423
438
|
}) {
|
|
424
439
|
let hasError = false;
|
|
425
440
|
let errorMessage = '';
|
|
426
441
|
const jobsIds: { jobId: any; recordId: any; }[] = [];
|
|
427
|
-
responseFlag.value = props.checkboxes.map(() => false);
|
|
442
|
+
// responseFlag.value = props.checkboxes.map(() => false);
|
|
443
|
+
for (let i = 0; i < recordsIds.length; i++) {
|
|
444
|
+
const index = props.checkboxes.findIndex(item => String(item) === String(recordsIds[i]));
|
|
445
|
+
if (index !== -1) {
|
|
446
|
+
responseFlag.value[index] = false;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
428
449
|
let isRateLimitExceeded = false;
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
450
|
+
if (!disableRateLimitCheck){
|
|
451
|
+
try {
|
|
452
|
+
const rateLimitRes = await callAdminForthApi({
|
|
453
|
+
path: `/plugin/${props.meta.pluginInstanceId}/update-rate-limits`,
|
|
454
|
+
method: 'POST',
|
|
455
|
+
body: {
|
|
456
|
+
actionType: actionType,
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
if (rateLimitRes?.error) {
|
|
460
|
+
isRateLimitExceeded = true;
|
|
461
|
+
adminforth.alert({
|
|
462
|
+
message: `Rate limit exceeded for "${actionType.replace('_', ' ')}" action. Please try again later.`,
|
|
463
|
+
variant: 'danger',
|
|
464
|
+
timeout: 'unlimited',
|
|
465
|
+
});
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
} catch (e) {
|
|
439
469
|
adminforth.alert({
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
470
|
+
message: `Error checking rate limit for "${actionType.replace('_', ' ')}" action.`,
|
|
471
|
+
variant: 'danger',
|
|
472
|
+
timeout: 'unlimited',
|
|
473
|
+
});
|
|
474
|
+
isRateLimitExceeded = true;
|
|
445
475
|
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
variant: 'danger',
|
|
450
|
-
timeout: 'unlimited',
|
|
451
|
-
});
|
|
452
|
-
isRateLimitExceeded = true;
|
|
476
|
+
if (isRateLimitExceeded) {
|
|
477
|
+
return;
|
|
478
|
+
};
|
|
453
479
|
}
|
|
454
|
-
if (isRateLimitExceeded) {
|
|
455
|
-
return;
|
|
456
|
-
};
|
|
457
480
|
//creating jobs
|
|
458
|
-
const tasks =
|
|
481
|
+
const tasks = recordsIds.map(async (checkbox, i) => {
|
|
459
482
|
try {
|
|
460
483
|
const res = await callAdminForthApi({
|
|
461
484
|
path: `/plugin/${props.meta.pluginInstanceId}/create-job`,
|
|
@@ -548,6 +571,8 @@ async function runAiAction({
|
|
|
548
571
|
}
|
|
549
572
|
if (index !== -1) {
|
|
550
573
|
jobsIds.splice(jobsIds.findIndex(j => j.jobId === jobId), 1);
|
|
574
|
+
} else {
|
|
575
|
+
jobsIds.splice(0, jobsIds.length);
|
|
551
576
|
}
|
|
552
577
|
isAtLeastOneInProgress = true;
|
|
553
578
|
adminforth.alert({
|
|
@@ -555,6 +580,13 @@ async function runAiAction({
|
|
|
555
580
|
variant: 'danger',
|
|
556
581
|
timeout: 'unlimited',
|
|
557
582
|
});
|
|
583
|
+
if (actionType === 'generate_images') {
|
|
584
|
+
isAiImageGenerationError.value[index] = true;
|
|
585
|
+
imageGenerationErrorMessage.value[index] = jobResponse.job?.error || 'Unknown error';
|
|
586
|
+
} else {
|
|
587
|
+
isAiGenerationError.value[index] = true;
|
|
588
|
+
aiGenerationErrorMessage.value[index] = jobResponse.job?.error || 'Unknown error';
|
|
589
|
+
}
|
|
558
590
|
}
|
|
559
591
|
}
|
|
560
592
|
if (!isAtLeastOneInProgress) {
|
|
@@ -667,4 +699,15 @@ async function uploadImage(imgBlob, id, fieldName) {
|
|
|
667
699
|
}
|
|
668
700
|
}
|
|
669
701
|
|
|
702
|
+
function regenerateImages(recordInfo: any) {
|
|
703
|
+
isGeneratingImages.value = true;
|
|
704
|
+
runAiAction({
|
|
705
|
+
endpoint: 'initial_image_generate',
|
|
706
|
+
actionType: 'generate_images',
|
|
707
|
+
responseFlag: isAiResponseReceivedImage,
|
|
708
|
+
recordsIds: [recordInfo.recordInfo],
|
|
709
|
+
disableRateLimitCheck: true,
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
|
|
670
713
|
</script>
|