@adminforth/bulk-ai-flow 1.8.0 → 1.9.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 +2 -2
- package/custom/ImageGenerationCarousel.vue +48 -19
- package/custom/VisionAction.vue +185 -136
- package/custom/VisionTable.vue +33 -25
- package/dist/custom/ImageGenerationCarousel.vue +48 -19
- package/dist/custom/VisionAction.vue +185 -136
- package/dist/custom/VisionTable.vue +33 -25
- package/dist/index.js +317 -198
- package/index.ts +321 -209
- package/package.json +1 -1
- package/types.ts +9 -0
package/build.log
CHANGED
|
@@ -11,5 +11,5 @@ custom/package-lock.json
|
|
|
11
11
|
custom/package.json
|
|
12
12
|
custom/tsconfig.json
|
|
13
13
|
|
|
14
|
-
sent
|
|
15
|
-
total size is
|
|
14
|
+
sent 184,984 bytes received 134 bytes 370,236.00 bytes/sec
|
|
15
|
+
total size is 184,451 speedup is 1.00
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
|
|
2
2
|
<template>
|
|
3
3
|
<!-- Main modal -->
|
|
4
|
-
<div tabindex="-1" class="
|
|
5
|
-
<div class="relative p-4 w-
|
|
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] max-h-[90vh] ">
|
|
6
6
|
<!-- Modal content -->
|
|
7
7
|
<div class="relative bg-white rounded-lg shadow-xl dark:bg-gray-700">
|
|
8
8
|
<!-- Modal header -->
|
|
@@ -95,20 +95,20 @@
|
|
|
95
95
|
</div>
|
|
96
96
|
|
|
97
97
|
|
|
98
|
-
<div id="gallery" class="relative w-full" data-carousel="static">
|
|
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
101
|
<!-- Item 1 -->
|
|
102
102
|
<div
|
|
103
103
|
v-for="(img, index) in images"
|
|
104
104
|
:key="index"
|
|
105
|
+
class="flex items-center justify-center w-full h-full"
|
|
105
106
|
:class="[
|
|
106
|
-
index === 0 ? 'block' : 'hidden'
|
|
107
|
-
'duration-700 ease-in-out'
|
|
107
|
+
index === 0 ? 'block' : 'hidden'
|
|
108
108
|
]"
|
|
109
109
|
data-carousel-item
|
|
110
110
|
>
|
|
111
|
-
<img :src="img" class="
|
|
111
|
+
<img :src="img" class="max-w-full max-h-full object-contain"
|
|
112
112
|
:alt="`Generated image ${index + 1}`"
|
|
113
113
|
/>
|
|
114
114
|
</div>
|
|
@@ -190,7 +190,7 @@ const { t: $t } = useI18n();
|
|
|
190
190
|
|
|
191
191
|
const prompt = ref('');
|
|
192
192
|
const emit = defineEmits(['close', 'selectImage', 'error', 'updateCarouselIndex']);
|
|
193
|
-
const props = defineProps(['meta', 'record', 'images', 'recordId', 'prompt', 'fieldName', 'isError', 'errorMessage', 'carouselImageIndex']);
|
|
193
|
+
const props = defineProps(['meta', 'record', 'images', 'recordId', 'prompt', 'fieldName', 'isError', 'errorMessage', 'carouselImageIndex', 'regenerateImagesRefreshRate']);
|
|
194
194
|
const images = ref([]);
|
|
195
195
|
const loading = ref(false);
|
|
196
196
|
const attachmentFiles = ref<string[]>([])
|
|
@@ -370,26 +370,24 @@ async function generateImages() {
|
|
|
370
370
|
let error = null;
|
|
371
371
|
try {
|
|
372
372
|
resp = await callAdminForthApi({
|
|
373
|
-
path: `/plugin/${props.meta.pluginInstanceId}/
|
|
373
|
+
path: `/plugin/${props.meta.pluginInstanceId}/create-job`,
|
|
374
374
|
method: 'POST',
|
|
375
375
|
body: {
|
|
376
|
-
|
|
376
|
+
actionType: 'regenerate_images',
|
|
377
377
|
recordId: props.recordId,
|
|
378
|
-
|
|
378
|
+
prompt: prompt.value,
|
|
379
|
+
fieldName: props.fieldName
|
|
379
380
|
},
|
|
380
381
|
});
|
|
381
382
|
} catch (e) {
|
|
382
383
|
console.error(e);
|
|
383
|
-
} finally {
|
|
384
|
-
clearInterval(ticker);
|
|
385
|
-
loadingTimer.value = null;
|
|
386
|
-
loading.value = false;
|
|
387
384
|
}
|
|
385
|
+
|
|
388
386
|
if (resp?.error) {
|
|
389
387
|
error = resp.error;
|
|
390
388
|
}
|
|
391
389
|
if (!resp) {
|
|
392
|
-
error = $t('Error
|
|
390
|
+
error = $t('Error creating image generation job');
|
|
393
391
|
}
|
|
394
392
|
|
|
395
393
|
if (error) {
|
|
@@ -401,19 +399,50 @@ async function generateImages() {
|
|
|
401
399
|
variant: 'danger',
|
|
402
400
|
timeout: 'unlimited',
|
|
403
401
|
});
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
402
|
+
}
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const jobId = resp.jobId;
|
|
407
|
+
let jobStatus = null;
|
|
408
|
+
let jobResponse = null;
|
|
409
|
+
while (jobStatus !== 'completed' && jobStatus !== 'failed') {
|
|
410
|
+
jobResponse = await callAdminForthApi({
|
|
411
|
+
path: `/plugin/${props.meta.pluginInstanceId}/get-job-status`,
|
|
412
|
+
method: 'POST',
|
|
413
|
+
body: { jobId },
|
|
407
414
|
});
|
|
415
|
+
if (jobResponse?.error) {
|
|
416
|
+
error = jobResponse.error;
|
|
417
|
+
break;
|
|
418
|
+
};
|
|
419
|
+
jobStatus = jobResponse?.job?.status;
|
|
420
|
+
if (jobStatus === 'failed') {
|
|
421
|
+
error = jobResponse?.job?.error || $t('Image generation job failed');
|
|
408
422
|
}
|
|
423
|
+
await new Promise((resolve) => setTimeout(resolve, props.regenerateImagesRefreshRate));
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (error) {
|
|
427
|
+
adminforth.alert({
|
|
428
|
+
message: error,
|
|
429
|
+
variant: 'danger',
|
|
430
|
+
timeout: 'unlimited',
|
|
431
|
+
});
|
|
409
432
|
return;
|
|
410
433
|
}
|
|
411
434
|
|
|
435
|
+
const respImages = jobResponse?.job?.result[props.fieldName] || [];
|
|
436
|
+
|
|
412
437
|
images.value = [
|
|
413
438
|
...images.value,
|
|
414
|
-
...
|
|
439
|
+
...respImages,
|
|
415
440
|
];
|
|
416
441
|
|
|
442
|
+
clearInterval(ticker);
|
|
443
|
+
loadingTimer.value = null;
|
|
444
|
+
loading.value = false;
|
|
445
|
+
|
|
417
446
|
await nextTick();
|
|
418
447
|
|
|
419
448
|
|
package/custom/VisionAction.vue
CHANGED
|
@@ -5,61 +5,39 @@
|
|
|
5
5
|
</div>
|
|
6
6
|
<p class="text-justify max-h-[18px] truncate max-w-[60vw] md:max-w-none">{{ props.meta.actionName }}</p>
|
|
7
7
|
</div>
|
|
8
|
-
<Dialog
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
:carouselImageIndex="carouselImageIndex"
|
|
42
|
-
/>
|
|
43
|
-
</div>
|
|
44
|
-
<div class="flex w-full flex-col md:flex-row items-stretch md:items-end justify-end gap-3 md:gap-4">
|
|
45
|
-
<div class="h-full text-red-600 font-semibold flex items-center justify-center md:mb-2">
|
|
46
|
-
<p v-if="isError === true">{{ errorMessage }}</p>
|
|
47
|
-
</div>
|
|
48
|
-
<button type="button" class="w-full md:w-auto 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"
|
|
49
|
-
@click="closeDialog"
|
|
50
|
-
>
|
|
51
|
-
{{'Cancel'}}
|
|
52
|
-
</button>
|
|
53
|
-
<Button
|
|
54
|
-
class="w-full md:w-64"
|
|
55
|
-
@click="saveData"
|
|
56
|
-
:disabled="isLoading || checkedCount < 1 || isCriticalError || isFetchingRecords"
|
|
57
|
-
:loader="isLoading"
|
|
58
|
-
>
|
|
59
|
-
{{ checkedCount > 1 ? 'Save fields' : 'Save field' }}
|
|
60
|
-
</Button>
|
|
61
|
-
</div>
|
|
62
|
-
</div>
|
|
8
|
+
<Dialog
|
|
9
|
+
ref="confirmDialog"
|
|
10
|
+
header="Bulk AI Flow"
|
|
11
|
+
class="!max-w-full w-full lg:w-[1600px] !lg:max-w-[1600px]"
|
|
12
|
+
:buttons="[
|
|
13
|
+
{ label: checkedCount > 1 ? 'Save fields' : 'Save field', options: { disabled: isLoading || checkedCount < 1 || isCriticalError || isFetchingRecords || isGeneratingImages || isAnalizingFields || isAnalizingImages, loader: isLoading, class: 'w-fit sm:w-40' }, onclick: (dialog) => { saveData(); dialog.hide(); } },
|
|
14
|
+
{ label: 'Cancel', onclick: (dialog) => dialog.hide() },
|
|
15
|
+
]"
|
|
16
|
+
>
|
|
17
|
+
<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">
|
|
18
|
+
<div v-if="records && props.checkboxes.length" class="w-full overflow-x-auto">
|
|
19
|
+
<VisionTable
|
|
20
|
+
:checkbox="props.checkboxes"
|
|
21
|
+
:records="records"
|
|
22
|
+
:meta="props.meta"
|
|
23
|
+
:images="images"
|
|
24
|
+
:tableHeaders="tableHeaders"
|
|
25
|
+
:tableColumns="tableColumns"
|
|
26
|
+
:customFieldNames="customFieldNames"
|
|
27
|
+
:tableColumnsIndexes="tableColumnsIndexes"
|
|
28
|
+
:selected="selected"
|
|
29
|
+
:isAiResponseReceivedAnalize="isAiResponseReceivedAnalize"
|
|
30
|
+
:isAiResponseReceivedImage="isAiResponseReceivedImage"
|
|
31
|
+
:primaryKey="primaryKey"
|
|
32
|
+
:openGenerationCarousel="openGenerationCarousel"
|
|
33
|
+
@error="handleTableError"
|
|
34
|
+
:carouselSaveImages="carouselSaveImages"
|
|
35
|
+
:carouselImageIndex="carouselImageIndex"
|
|
36
|
+
:regenerateImagesRefreshRate="props.meta.refreshRates?.regenerateImages"
|
|
37
|
+
/>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="text-red-600 flex items-center w-full">
|
|
40
|
+
<p v-if="isError === true">{{ errorMessage }}</p>
|
|
63
41
|
</div>
|
|
64
42
|
</div>
|
|
65
43
|
</Dialog>
|
|
@@ -111,9 +89,14 @@ const isCriticalError = ref(false);
|
|
|
111
89
|
const isImageGenerationError = ref(false);
|
|
112
90
|
const errorMessage = ref('');
|
|
113
91
|
const checkedCount = ref(0);
|
|
92
|
+
const isGeneratingImages = ref(false);
|
|
93
|
+
const isAnalizingFields = ref(false);
|
|
94
|
+
const isAnalizingImages = ref(false);
|
|
95
|
+
|
|
114
96
|
|
|
115
97
|
const openDialog = async () => {
|
|
116
98
|
confirmDialog.value.open();
|
|
99
|
+
isFetchingRecords.value = true;
|
|
117
100
|
await getRecords();
|
|
118
101
|
if (props.meta.isAttachFiles) {
|
|
119
102
|
await getImages();
|
|
@@ -124,42 +107,41 @@ const openDialog = async () => {
|
|
|
124
107
|
tableColumnsIndexes.value = result.indexes;
|
|
125
108
|
customFieldNames.value = tableHeaders.value.slice((props.meta.isAttachFiles) ? 3 : 2).map(h => h.fieldName);
|
|
126
109
|
setSelected();
|
|
110
|
+
if (props.meta.isImageGeneration) {
|
|
111
|
+
fillCarouselSaveImages();
|
|
112
|
+
}
|
|
127
113
|
for (let i = 0; i < selected.value?.length; i++) {
|
|
128
114
|
openGenerationCarousel.value[i] = props.meta.outputImageFields?.reduce((acc,key) =>{
|
|
129
115
|
acc[key] = false;
|
|
130
116
|
return acc;
|
|
131
117
|
},{[primaryKey]: records.value[i][primaryKey]} as Record<string, boolean>);
|
|
132
118
|
}
|
|
133
|
-
isFetchingRecords.value =
|
|
134
|
-
|
|
119
|
+
isFetchingRecords.value = false;
|
|
120
|
+
|
|
121
|
+
if (props.meta.isImageGeneration) {
|
|
122
|
+
isGeneratingImages.value = true;
|
|
123
|
+
runAiAction({
|
|
124
|
+
endpoint: 'initial_image_generate',
|
|
125
|
+
actionType: 'generate_images',
|
|
126
|
+
responseFlag: isAiResponseReceivedImage,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
135
129
|
if (props.meta.isFieldsForAnalizeFromImages) {
|
|
136
|
-
|
|
130
|
+
isAnalizingImages.value = true;
|
|
131
|
+
runAiAction({
|
|
137
132
|
endpoint: 'analyze',
|
|
138
133
|
actionType: 'analyze',
|
|
139
134
|
responseFlag: isAiResponseReceivedAnalize,
|
|
140
|
-
})
|
|
135
|
+
});
|
|
141
136
|
}
|
|
142
137
|
if (props.meta.isFieldsForAnalizePlain) {
|
|
143
|
-
|
|
138
|
+
isAnalizingFields.value = true;
|
|
139
|
+
runAiAction({
|
|
144
140
|
endpoint: 'analyze_no_images',
|
|
145
141
|
actionType: 'analyze_no_images',
|
|
146
142
|
responseFlag: isAiResponseReceivedAnalize,
|
|
147
|
-
})
|
|
148
|
-
}
|
|
149
|
-
if (props.meta.isImageGeneration) {
|
|
150
|
-
tasks.push(runAiAction({
|
|
151
|
-
endpoint: 'initial_image_generate',
|
|
152
|
-
actionType: 'generate_images',
|
|
153
|
-
responseFlag: isAiResponseReceivedImage,
|
|
154
|
-
}));
|
|
155
|
-
}
|
|
156
|
-
await Promise.all(tasks);
|
|
157
|
-
|
|
158
|
-
if (props.meta.isImageGeneration) {
|
|
159
|
-
fillCarouselSaveImages();
|
|
143
|
+
});
|
|
160
144
|
}
|
|
161
|
-
|
|
162
|
-
isFetchingRecords.value = false;
|
|
163
145
|
}
|
|
164
146
|
|
|
165
147
|
watch(selected, (val) => {
|
|
@@ -167,39 +149,22 @@ watch(selected, (val) => {
|
|
|
167
149
|
checkedCount.value = val.filter(item => item.isChecked === true).length;
|
|
168
150
|
}, { deep: true });
|
|
169
151
|
|
|
170
|
-
const closeDialog = () => {
|
|
171
|
-
confirmDialog.value.close();
|
|
172
|
-
isAiResponseReceivedAnalize.value = [];
|
|
173
|
-
isAiResponseReceivedImage.value = [];
|
|
174
|
-
|
|
175
|
-
records.value = [];
|
|
176
|
-
images.value = [];
|
|
177
|
-
selected.value = [];
|
|
178
|
-
tableColumns.value = [];
|
|
179
|
-
tableColumnsIndexes.value = [];
|
|
180
|
-
isError.value = false;
|
|
181
|
-
isCriticalError.value = false;
|
|
182
|
-
isImageGenerationError.value = false;
|
|
183
|
-
errorMessage.value = '';
|
|
184
|
-
carouselSaveImages.value = [];
|
|
185
|
-
carouselImageIndex.value = [];
|
|
186
|
-
}
|
|
187
|
-
|
|
188
152
|
function fillCarouselSaveImages() {
|
|
189
153
|
for (const item of selected.value) {
|
|
190
154
|
const tempItem: any = {};
|
|
191
155
|
const tempItemIndex: any = {};
|
|
192
156
|
for (const [key, value] of Object.entries(item)) {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
157
|
+
if (props.meta.outputImageFields?.includes(key)) {
|
|
158
|
+
tempItem[key] = [];
|
|
159
|
+
tempItemIndex[key] = 0;
|
|
160
|
+
}
|
|
197
161
|
}
|
|
198
162
|
carouselSaveImages.value.push(tempItem);
|
|
199
163
|
carouselImageIndex.value.push(tempItemIndex);
|
|
200
164
|
}
|
|
201
165
|
}
|
|
202
166
|
|
|
167
|
+
|
|
203
168
|
function formatLabel(str) {
|
|
204
169
|
return str
|
|
205
170
|
.split('_')
|
|
@@ -438,38 +403,129 @@ async function runAiAction({
|
|
|
438
403
|
responseFlag: Ref<boolean[]>;
|
|
439
404
|
updateOnSuccess?: boolean;
|
|
440
405
|
}) {
|
|
441
|
-
let
|
|
442
|
-
let
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
406
|
+
let hasError = false;
|
|
407
|
+
let errorMessage = '';
|
|
408
|
+
const jobsIds: { jobId: any; recordId: any; }[] = [];
|
|
409
|
+
responseFlag.value = props.checkboxes.map(() => false);
|
|
410
|
+
|
|
411
|
+
//creating jobs
|
|
412
|
+
const tasks = props.checkboxes.map(async (checkbox, i) => {
|
|
413
|
+
try {
|
|
414
|
+
const res = await callAdminForthApi({
|
|
415
|
+
path: `/plugin/${props.meta.pluginInstanceId}/create-job`,
|
|
416
|
+
method: 'POST',
|
|
417
|
+
body: {
|
|
418
|
+
actionType: actionType,
|
|
419
|
+
recordId: checkbox,
|
|
420
|
+
},
|
|
421
|
+
});
|
|
446
422
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
423
|
+
if (res?.error) {
|
|
424
|
+
throw new Error(res.error);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (!res) {
|
|
428
|
+
throw new Error(`${actionType} request returned empty response.`);
|
|
429
|
+
}
|
|
454
430
|
|
|
455
|
-
|
|
456
|
-
|
|
431
|
+
jobsIds.push({ jobId: res.jobId, recordId: checkbox });
|
|
432
|
+
} catch (e) {
|
|
433
|
+
console.error(`Error during ${actionType} for item ${i}:`, e);
|
|
434
|
+
hasError = true;
|
|
435
|
+
errorMessage = `Failed to ${actionType.replace('_', ' ')}. Please, try to re-run the action.`;
|
|
436
|
+
return { success: false, index: i, error: e };
|
|
457
437
|
}
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
error = `Failed to ${actionType.replace('_', ' ')}. Please, try to re-run the action.`;
|
|
461
|
-
}
|
|
438
|
+
});
|
|
439
|
+
await Promise.all(tasks);
|
|
462
440
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
441
|
+
//polling jobs
|
|
442
|
+
let isInProgress = true;
|
|
443
|
+
//if no jobs were created, skip polling
|
|
444
|
+
while (isInProgress) {
|
|
445
|
+
//check if at least one job is still in progress
|
|
446
|
+
let isAtLeastOneInProgress = false;
|
|
447
|
+
//checking status of each job
|
|
448
|
+
for (const { jobId, recordId } of jobsIds) {
|
|
449
|
+
//check job status
|
|
450
|
+
const jobResponse = await callAdminForthApi({
|
|
451
|
+
path: `/plugin/${props.meta.pluginInstanceId}/get-job-status`,
|
|
452
|
+
method: 'POST',
|
|
453
|
+
body: { jobId },
|
|
454
|
+
});
|
|
455
|
+
//check for errors
|
|
456
|
+
if (jobResponse?.error) {
|
|
457
|
+
console.error(`Error during ${actionType}:`, jobResponse.error);
|
|
458
|
+
break;
|
|
459
|
+
};
|
|
460
|
+
// extract job status
|
|
461
|
+
let jobStatus = jobResponse?.job?.status;
|
|
462
|
+
// check if job is still in progress. If in progress - skip to next job
|
|
463
|
+
if (jobStatus === 'in_progress') {
|
|
464
|
+
isAtLeastOneInProgress = true;
|
|
465
|
+
//if job is completed - update record data
|
|
466
|
+
} else if (jobStatus === 'completed') {
|
|
467
|
+
// finding index of the record in selected array
|
|
468
|
+
const index = selected.value.findIndex(item => String(item[primaryKey]) === String(recordId));
|
|
469
|
+
//if we are generating images - update carouselSaveImages with new image
|
|
470
|
+
if (actionType === 'generate_images') {
|
|
471
|
+
for (const [key, value] of Object.entries(carouselSaveImages.value[index])) {
|
|
472
|
+
if (props.meta.outputImageFields?.includes(key)) {
|
|
473
|
+
carouselSaveImages.value[index][key] = [jobResponse.job.result[key]];
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
//marking that we received response for this record
|
|
478
|
+
if (actionType !== 'analyze_no_images' || !props.meta.isFieldsForAnalizeFromImages) {
|
|
479
|
+
responseFlag.value[index] = true;
|
|
480
|
+
}
|
|
481
|
+
//updating selected with new data from AI
|
|
482
|
+
const pk = selected.value[index]?.[primaryKey];
|
|
483
|
+
if (pk) {
|
|
484
|
+
selected.value[index] = {
|
|
485
|
+
...selected.value[index],
|
|
486
|
+
...jobResponse.job.result,
|
|
487
|
+
isChecked: true,
|
|
488
|
+
[primaryKey]: pk,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
//removing job from jobsIds
|
|
492
|
+
if (index !== -1) {
|
|
493
|
+
jobsIds.splice(jobsIds.findIndex(j => j.jobId === jobId), 1);
|
|
494
|
+
}
|
|
495
|
+
// checking one more time if we have in progress jobs
|
|
496
|
+
isAtLeastOneInProgress = true;
|
|
497
|
+
// if job is failed - set error
|
|
498
|
+
} else if (jobStatus === 'failed') {
|
|
499
|
+
const index = selected.value.findIndex(item => String(item[primaryKey]) === String(recordId));
|
|
500
|
+
if (actionType !== 'analyze_no_images' || !props.meta.isFieldsForAnalizeFromImages) {
|
|
501
|
+
responseFlag.value[index] = true;
|
|
502
|
+
}
|
|
503
|
+
adminforth.alert({
|
|
504
|
+
message: `Generation action "${actionType.replace('_', ' ')}" failed for record: ${recordId}. Error: ${jobResponse.job?.error || 'Unknown error'}`,
|
|
505
|
+
variant: 'danger',
|
|
506
|
+
timeout: 'unlimited',
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
if (!isAtLeastOneInProgress) {
|
|
511
|
+
isInProgress = false;
|
|
512
|
+
}
|
|
513
|
+
if (jobsIds.length > 0) {
|
|
514
|
+
if (actionType === 'generate_images') {
|
|
515
|
+
await new Promise(resolve => setTimeout(resolve, props.meta.refreshRates?.generateImages));
|
|
516
|
+
} else if (actionType === 'analyze') {
|
|
517
|
+
await new Promise(resolve => setTimeout(resolve, props.meta.refreshRates?.fillFieldsFromImages));
|
|
518
|
+
} else if (actionType === 'analyze_no_images') {
|
|
519
|
+
await new Promise(resolve => setTimeout(resolve, props.meta.refreshRates?.fillPlainFields));
|
|
520
|
+
} else {
|
|
521
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
522
|
+
}
|
|
523
|
+
}
|
|
468
524
|
}
|
|
469
525
|
|
|
470
|
-
if (
|
|
526
|
+
if (hasError) {
|
|
471
527
|
adminforth.alert({
|
|
472
|
-
message:
|
|
528
|
+
message: errorMessage,
|
|
473
529
|
variant: 'danger',
|
|
474
530
|
timeout: 'unlimited',
|
|
475
531
|
});
|
|
@@ -477,26 +533,19 @@ async function runAiAction({
|
|
|
477
533
|
if (actionType === 'generate_images') {
|
|
478
534
|
isImageGenerationError.value = true;
|
|
479
535
|
}
|
|
480
|
-
errorMessage.value =
|
|
536
|
+
this.errorMessage.value = errorMessage;
|
|
481
537
|
return;
|
|
482
538
|
}
|
|
483
539
|
|
|
484
|
-
if (
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
...item,
|
|
491
|
-
isChecked: true,
|
|
492
|
-
[primaryKey]: pk,
|
|
493
|
-
};
|
|
494
|
-
}
|
|
495
|
-
});
|
|
540
|
+
if (actionType === 'generate_images') {
|
|
541
|
+
isGeneratingImages.value = false;
|
|
542
|
+
} else if (actionType === 'analyze') {
|
|
543
|
+
isAnalizingImages.value = false;
|
|
544
|
+
} else if (actionType === 'analyze_no_images') {
|
|
545
|
+
isAnalizingFields.value = false;
|
|
496
546
|
}
|
|
497
547
|
}
|
|
498
548
|
|
|
499
|
-
|
|
500
549
|
async function uploadImage(imgBlob, id, fieldName) {
|
|
501
550
|
const file = new File([imgBlob], `generated_${fieldName}_${id}.${imgBlob.type.split('/').pop()}`, { type: imgBlob.type });
|
|
502
551
|
const { name, size, type } = file;
|
package/custom/VisionTable.vue
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div>
|
|
3
2
|
<Table
|
|
4
3
|
:columns="tableHeaders"
|
|
5
4
|
:data="tableColumns"
|
|
6
|
-
:pageSize="
|
|
5
|
+
:pageSize="6"
|
|
7
6
|
>
|
|
8
7
|
<!-- HEADER TEMPLATE -->
|
|
9
8
|
<template #header:checkboxes="{ item }">
|
|
@@ -11,7 +10,7 @@
|
|
|
11
10
|
</template>
|
|
12
11
|
<!-- CHECKBOX CELL TEMPLATE -->
|
|
13
12
|
<template #cell:checkboxes="{ item }">
|
|
14
|
-
<div class="flex items-center justify-center">
|
|
13
|
+
<div class="max-w-[100px] flex items-center justify-center">
|
|
15
14
|
<Checkbox
|
|
16
15
|
v-model="selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])].isChecked"
|
|
17
16
|
/>
|
|
@@ -28,18 +27,22 @@
|
|
|
28
27
|
@click="zoomImage(image)"
|
|
29
28
|
/>
|
|
30
29
|
</div>
|
|
30
|
+
</div>
|
|
31
|
+
<transition name="fade">
|
|
31
32
|
<div
|
|
32
33
|
v-if="zoomedImage"
|
|
33
34
|
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60"
|
|
34
35
|
@click.self="closeZoom"
|
|
35
36
|
>
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
<transition name="zoom">
|
|
38
|
+
<img
|
|
39
|
+
v-if="zoomedImage"
|
|
40
|
+
:src="zoomedImage"
|
|
41
|
+
class="max-w-full max-h-full rounded-lg object-contain cursor-grab z-75"
|
|
42
|
+
/>
|
|
43
|
+
</transition>
|
|
41
44
|
</div>
|
|
42
|
-
</
|
|
45
|
+
</transition>
|
|
43
46
|
</div>
|
|
44
47
|
</template>
|
|
45
48
|
<!-- CUSTOM FIELD TEMPLATES -->
|
|
@@ -96,6 +99,7 @@
|
|
|
96
99
|
:meta="props.meta"
|
|
97
100
|
:fieldName="n"
|
|
98
101
|
:carouselImageIndex="carouselImageIndex[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n]"
|
|
102
|
+
:regenerateImagesRefreshRate="regenerateImagesRefreshRate"
|
|
99
103
|
@error="handleError"
|
|
100
104
|
@close="openGenerationCarousel[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] = false"
|
|
101
105
|
@selectImage="updateSelectedImage"
|
|
@@ -114,12 +118,10 @@
|
|
|
114
118
|
</div>
|
|
115
119
|
</template>
|
|
116
120
|
</Table>
|
|
117
|
-
</div>
|
|
118
121
|
</template>
|
|
119
122
|
|
|
120
123
|
<script lang="ts" setup>
|
|
121
|
-
import { ref
|
|
122
|
-
import mediumZoom from 'medium-zoom'
|
|
124
|
+
import { ref } from 'vue'
|
|
123
125
|
import { Select, Input, Textarea, Table, Checkbox, Skeleton, Toggle } from '@/afcl'
|
|
124
126
|
import GenerationCarousel from './ImageGenerationCarousel.vue'
|
|
125
127
|
|
|
@@ -138,12 +140,12 @@ const props = defineProps<{
|
|
|
138
140
|
errorMessage: string
|
|
139
141
|
carouselSaveImages: any[]
|
|
140
142
|
carouselImageIndex: any[]
|
|
143
|
+
regenerateImagesRefreshRate: number
|
|
141
144
|
}>();
|
|
142
145
|
const emit = defineEmits(['error']);
|
|
143
146
|
|
|
144
147
|
|
|
145
148
|
const zoomedImage = ref(null)
|
|
146
|
-
const zoomedImg = ref(null)
|
|
147
149
|
|
|
148
150
|
|
|
149
151
|
function zoomImage(img) {
|
|
@@ -154,17 +156,6 @@ function closeZoom() {
|
|
|
154
156
|
zoomedImage.value = null
|
|
155
157
|
}
|
|
156
158
|
|
|
157
|
-
watch(zoomedImage, async (val) => {
|
|
158
|
-
await nextTick()
|
|
159
|
-
if (val && zoomedImg.value) {
|
|
160
|
-
mediumZoom(zoomedImg.value, {
|
|
161
|
-
margin: 24,
|
|
162
|
-
background: 'rgba(0, 0, 0, 0.9)',
|
|
163
|
-
scrollOffset: 150
|
|
164
|
-
}).show()
|
|
165
|
-
}
|
|
166
|
-
})
|
|
167
|
-
|
|
168
159
|
function isInColumnEnum(key: string): boolean {
|
|
169
160
|
const colEnum = props.meta.columnEnums?.find(c => c.name === key);
|
|
170
161
|
if (!colEnum) {
|
|
@@ -201,4 +192,21 @@ function updateActiveIndex(newIndex: number, id: any, fieldName: string) {
|
|
|
201
192
|
props.carouselImageIndex[props.tableColumnsIndexes.findIndex(el => el[props.primaryKey] === id)][fieldName] = newIndex;
|
|
202
193
|
}
|
|
203
194
|
|
|
204
|
-
</script>
|
|
195
|
+
</script>
|
|
196
|
+
|
|
197
|
+
<style scoped>
|
|
198
|
+
.fade-enter-active, .fade-leave-active {
|
|
199
|
+
transition: opacity 0.2s ease;
|
|
200
|
+
}
|
|
201
|
+
.fade-enter-from, .fade-leave-to {
|
|
202
|
+
opacity: 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.zoom-enter-active, .zoom-leave-active {
|
|
206
|
+
transition: transform 0.2s ease, opacity 0.2s ease;
|
|
207
|
+
}
|
|
208
|
+
.zoom-enter-from, .zoom-leave-to {
|
|
209
|
+
transform: scale(0.95);
|
|
210
|
+
opacity: 0;
|
|
211
|
+
}
|
|
212
|
+
</style>
|