@adminforth/bulk-ai-flow 1.21.8 → 1.22.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/VisionAction.vue +771 -569
- package/custom/VisionTable.vue +97 -83
- package/custom/package-lock.json +28 -0
- package/custom/package.json +2 -1
- package/dist/custom/VisionAction.vue +771 -569
- package/dist/custom/VisionTable.vue +97 -83
- package/dist/custom/package-lock.json +28 -0
- package/dist/custom/package.json +2 -1
- package/dist/index.js +94 -9
- package/index.ts +99 -9
- package/package.json +1 -1
- package/types.ts +25 -0
package/custom/VisionAction.vue
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
{
|
|
21
21
|
label: checkedCount > 1 ? t('Save fields') : t('Save field'),
|
|
22
22
|
options: {
|
|
23
|
-
disabled: isLoading || checkedCount < 1 ||
|
|
23
|
+
disabled: isLoading || checkedCount < 1 || isFetchingRecords || isProcessingAny || isGenerationPaused,
|
|
24
24
|
loader: isLoading, class: 'w-fit'
|
|
25
25
|
},
|
|
26
26
|
onclick: async (dialog) => { await saveData(); dialog.hide(); }
|
|
@@ -67,46 +67,71 @@
|
|
|
67
67
|
:click-to-close-outside="false"
|
|
68
68
|
>
|
|
69
69
|
<div class="bulk-vision-table flex flex-col items-center gap-3 md:gap-4 overflow-y-auto">
|
|
70
|
-
<template v-if="
|
|
70
|
+
<template v-if="recordsList.length && popupMode === 'generation'" >
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
<div class="w-full">
|
|
74
|
+
<div v-if="isGenerationPaused" class="flex flex-col gap-2 mb-2">
|
|
75
|
+
<p class="text-sm font-semibold text-yellow-800">{{ t(`Generated ${startedRecordCount} records. `) + t('Generation on pause. Resume generation?') }}</p>
|
|
76
|
+
<div class="flex items-center gap-2">
|
|
77
|
+
<button
|
|
78
|
+
class="px-3 py-1.5 text-sm rounded-md bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 text-white"
|
|
79
|
+
@click="resumeGeneration"
|
|
80
|
+
>
|
|
81
|
+
{{ t('Resume generation') }}
|
|
82
|
+
</button>
|
|
83
|
+
<button
|
|
84
|
+
class="px-3 py-1.5 text-sm rounded-md bg-white hover:bg-gray-100 text-gray-900 border border-gray-200"
|
|
85
|
+
@click="cancelGeneration"
|
|
86
|
+
>
|
|
87
|
+
{{ t('Cancel generation') }}
|
|
88
|
+
</button>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
<div
|
|
92
|
+
class="w-full h-[30px] rounded-2xl bg-gray-200 dark:bg-gray-700 overflow-hidden relative"
|
|
93
|
+
:class="isGenerationPaused ? 'opacity-80' : ''"
|
|
94
|
+
role="progressbar"
|
|
95
|
+
:aria-valuenow="displayedProcessedCount"
|
|
96
|
+
:aria-valuemin="0"
|
|
97
|
+
:aria-valuemax="totalRecords"
|
|
98
|
+
>
|
|
99
|
+
<div
|
|
100
|
+
class="h-full bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 transition-all duration-200 "
|
|
101
|
+
:style="{ width: `${displayedProgressPercent}%` }"
|
|
102
|
+
></div>
|
|
103
|
+
<div class="absolute inset-0 flex items-center justify-center text-sm font-medium text-white drop-shadow">
|
|
104
|
+
<template v-if="isProcessingAny || isGenerationPaused">
|
|
105
|
+
{{ Math.floor((displayedProcessedCount / totalRecords) * 100) }}%
|
|
106
|
+
</template>
|
|
107
|
+
<template v-else-if="isGenerationCancelled">
|
|
108
|
+
{{ t('Generation cancelled') }}
|
|
109
|
+
</template>
|
|
110
|
+
<template v-else>
|
|
111
|
+
{{ t('Processed') }}
|
|
112
|
+
</template>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
|
|
71
118
|
<VisionTable
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
:records="
|
|
119
|
+
class="md:max-h-[75vh] max-w-[1560px] w-full h-full"
|
|
120
|
+
ref="tableRef"
|
|
121
|
+
:records="recordsList"
|
|
75
122
|
:meta="props.meta"
|
|
76
|
-
:images="images"
|
|
77
123
|
:tableHeaders="tableHeaders"
|
|
78
|
-
:tableColumns="tableColumns"
|
|
79
124
|
:customFieldNames="customFieldNames"
|
|
80
|
-
:tableColumnsIndexes="tableColumnsIndexes"
|
|
81
|
-
:selected="selected"
|
|
82
|
-
:oldData="oldData"
|
|
83
125
|
:isError="isError"
|
|
84
126
|
:errorMessage="errorMessage"
|
|
85
|
-
:isAiResponseReceivedAnalizeImage="isAiResponseReceivedAnalizeImage"
|
|
86
|
-
:isAiResponseReceivedAnalizeNoImage="isAiResponseReceivedAnalizeNoImage"
|
|
87
|
-
:isAiResponseReceivedImage="isAiResponseReceivedImage"
|
|
88
|
-
:primaryKey="primaryKey"
|
|
89
|
-
:openGenerationCarousel="openGenerationCarousel"
|
|
90
|
-
:openImageCompare="openImageCompare"
|
|
91
|
-
@error="handleTableError"
|
|
92
|
-
:carouselSaveImages="carouselSaveImages"
|
|
93
|
-
:carouselImageIndex="carouselImageIndex"
|
|
94
127
|
:regenerateImagesRefreshRate="props.meta.refreshRates?.regenerateImages"
|
|
95
|
-
:isAiGenerationError="isAiGenerationError"
|
|
96
|
-
:aiGenerationErrorMessage="aiGenerationErrorMessage"
|
|
97
|
-
:isAiImageGenerationError="isAiImageGenerationError"
|
|
98
|
-
:imageGenerationErrorMessage="imageGenerationErrorMessage"
|
|
99
|
-
@regenerate-images="regenerateImages"
|
|
100
128
|
:isImageHasPreviewUrl="isImageHasPreviewUrl"
|
|
101
129
|
:imageGenerationPrompts="generationPrompts.generateImages"
|
|
102
|
-
:isImageToTextGenerationError="isImageToTextGenerationError"
|
|
103
|
-
:imageToTextErrorMessages="imageToTextErrorMessages"
|
|
104
|
-
:isTextToTextGenerationError="isTextToTextGenerationError"
|
|
105
|
-
:textToTextErrorMessages="textToTextErrorMessages"
|
|
106
130
|
:outputImageFields="props.meta.outputImageFields"
|
|
107
131
|
:outputFieldsForAnalizeFromImages="props.meta.outputFieldsForAnalizeFromImages"
|
|
108
132
|
:outputPlainFields="props.meta.outputPlainFields"
|
|
109
|
-
|
|
133
|
+
@error="handleTableError"
|
|
134
|
+
@regenerate-images="regenerateImages"
|
|
110
135
|
@regenerate-cell="regenerateCell"
|
|
111
136
|
/>
|
|
112
137
|
<div class="text-red-600 flex items-center w-full">
|
|
@@ -165,8 +190,9 @@
|
|
|
165
190
|
|
|
166
191
|
<script lang="ts" setup>
|
|
167
192
|
import { callAdminForthApi } from '@/utils';
|
|
168
|
-
import {
|
|
169
|
-
import
|
|
193
|
+
import { ref, computed, reactive } from 'vue'
|
|
194
|
+
import pLimit from 'p-limit';
|
|
195
|
+
import { Dialog, Textarea, Toggle, Tooltip } from '@/afcl';
|
|
170
196
|
import VisionTable from './VisionTable.vue'
|
|
171
197
|
import adminforth from '@/adminforth';
|
|
172
198
|
import { useI18n } from 'vue-i18n';
|
|
@@ -174,9 +200,11 @@ import { AdminUser, type AdminForthResourceCommon } from '@/types/Common';
|
|
|
174
200
|
import { useCoreStore } from '@/stores/core';
|
|
175
201
|
import { IconShieldSolid, IconInfoCircleSolid } from '@iconify-prerendered/vue-flowbite';
|
|
176
202
|
import { IconExclamationTriangle } from '@iconify-prerendered/vue-humbleicons';
|
|
203
|
+
import { useFiltersStore } from '@/stores/filters';
|
|
177
204
|
|
|
178
205
|
|
|
179
206
|
const coreStore = useCoreStore();
|
|
207
|
+
const filtersStore = useFiltersStore();
|
|
180
208
|
|
|
181
209
|
const { t } = useI18n();
|
|
182
210
|
const props = defineProps<{
|
|
@@ -188,58 +216,110 @@ const props = defineProps<{
|
|
|
188
216
|
clearCheckboxes?: () => any,
|
|
189
217
|
}>();
|
|
190
218
|
|
|
219
|
+
type RecordStatus = 'pending' | 'processing' | 'completed' | 'failed';
|
|
220
|
+
|
|
221
|
+
type RecordState = {
|
|
222
|
+
id: string | number;
|
|
223
|
+
status: RecordStatus;
|
|
224
|
+
isChecked: boolean;
|
|
225
|
+
label: string;
|
|
226
|
+
data: Record<string, any>;
|
|
227
|
+
oldData: Record<string, any>;
|
|
228
|
+
images: any[];
|
|
229
|
+
aiStatus: {
|
|
230
|
+
generatedImages: boolean;
|
|
231
|
+
analyzedImages: boolean;
|
|
232
|
+
analyzedNoImages: boolean;
|
|
233
|
+
};
|
|
234
|
+
openGenerationCarousel: Record<string, boolean>;
|
|
235
|
+
openImageCompare: Record<string, boolean>;
|
|
236
|
+
carouselSaveImages: Record<string, any[]>;
|
|
237
|
+
carouselImageIndex: Record<string, number>;
|
|
238
|
+
imageGenerationErrorMessage: string;
|
|
239
|
+
imageGenerationFailed: boolean;
|
|
240
|
+
imageToTextErrorMessages: Record<string, string>;
|
|
241
|
+
textToTextErrorMessages: Record<string, string>;
|
|
242
|
+
regeneratingFieldsStatus: Record<string, boolean>;
|
|
243
|
+
listOfImageNotGenerated: Record<string, any>;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const recordIds = ref<Array<string | number>>([]);
|
|
247
|
+
const recordsById = new Map<string, RecordState>();
|
|
248
|
+
const uncheckedRecordIds = new Set<string>();
|
|
249
|
+
|
|
191
250
|
defineExpose({
|
|
192
251
|
click
|
|
193
252
|
});
|
|
194
253
|
|
|
195
|
-
const confirmDialog = ref(null);
|
|
196
|
-
const records = ref<any[]>([]);
|
|
197
|
-
const images = ref<any[]>([]);
|
|
198
|
-
const tableHeaders = ref([]);
|
|
199
|
-
const tableColumns = ref([]);
|
|
200
|
-
const tableColumnsIndexes = ref([]);
|
|
201
|
-
const customFieldNames = ref([]);
|
|
202
|
-
const selected = ref<any[]>([]);
|
|
203
|
-
const oldData = ref<any[]>([]);
|
|
204
|
-
const carouselSaveImages = ref<any[]>([]);
|
|
205
|
-
const carouselImageIndex = ref<any[]>([]);
|
|
206
|
-
const isAiResponseReceivedAnalizeImage = ref([]);
|
|
207
|
-
const isAiResponseReceivedAnalizeNoImage = ref([]);
|
|
208
|
-
const isAiResponseReceivedImage = ref([]);
|
|
254
|
+
const confirmDialog = ref<any>(null);
|
|
209
255
|
const primaryKey = props.meta.primaryKey;
|
|
210
|
-
const openGenerationCarousel = ref([]);
|
|
211
|
-
const openImageCompare = ref([]);
|
|
212
256
|
const isLoading = ref(false);
|
|
213
257
|
const isFetchingRecords = ref(false);
|
|
214
258
|
const isError = ref(false);
|
|
215
|
-
const isCriticalError = ref(false);
|
|
216
|
-
const isImageGenerationError = ref(false);
|
|
217
259
|
const errorMessage = ref('');
|
|
218
|
-
const checkedCount = ref(0);
|
|
219
|
-
const isGeneratingImages = ref(false);
|
|
220
|
-
const isAnalizingFields = ref(false);
|
|
221
|
-
const isAnalizingImages = ref(false);
|
|
222
260
|
const isDialogOpen = ref(false);
|
|
223
|
-
const isAiGenerationError = ref<boolean[]>([false]);
|
|
224
|
-
const aiGenerationErrorMessage = ref<string[]>([]);
|
|
225
|
-
const isAiImageGenerationError = ref<boolean[]>([false]);
|
|
226
|
-
|
|
227
|
-
const isImageToTextGenerationError = ref<boolean[]>([false]);
|
|
228
|
-
const imageToTextErrorMessages = ref<Record<string, string>[]>([]);
|
|
229
|
-
|
|
230
|
-
const isTextToTextGenerationError = ref<boolean[]>([false]);
|
|
231
|
-
const textToTextErrorMessages = ref<Record<string, string>[]>([]);
|
|
232
|
-
|
|
233
|
-
const imageGenerationErrorMessage = ref<string[]>([]);
|
|
234
261
|
const isImageHasPreviewUrl = ref<Record<string, boolean>>({});
|
|
235
262
|
const popupMode = ref<'generation' | 'confirmation' | 'settings'>('confirmation');
|
|
236
263
|
const generationPrompts = ref<any>({});
|
|
237
264
|
const isDataSaved = ref(false);
|
|
238
|
-
|
|
239
|
-
const regeneratingFieldsStatus = ref<Record<string, Record<string, boolean>>>({});
|
|
240
265
|
const overwriteExistingValues = ref<boolean>(false);
|
|
241
266
|
|
|
242
|
-
const
|
|
267
|
+
const checkedCount = computed(() => recordIds.value.length - uncheckedRecordIds.size);
|
|
268
|
+
const totalRecords = computed(() => recordIds.value.length);
|
|
269
|
+
const isGenerationPaused = ref(false);
|
|
270
|
+
const isGenerationCancelled = ref(false);
|
|
271
|
+
const pendingResumeResolver = ref<null | (() => void)>(null);
|
|
272
|
+
const completedRecordIds = ref<Set<string>>(new Set());
|
|
273
|
+
const isActiveGeneration = ref(false);
|
|
274
|
+
const pauseBreakpoints = computed(() => props.meta.askConfirmation || []);
|
|
275
|
+
const startedRecordCount = ref(0);
|
|
276
|
+
let startGate = Promise.resolve();
|
|
277
|
+
const tableRef = ref<any>(null);
|
|
278
|
+
const processedCount = computed(() => {
|
|
279
|
+
recordsVersion.value;
|
|
280
|
+
return Array.from(recordsById.values()).filter(record => record.status === 'completed' || record.status === 'failed').length;
|
|
281
|
+
});
|
|
282
|
+
const progressStep = computed(() => {
|
|
283
|
+
if (!totalRecords.value || totalRecords.value < 100) {
|
|
284
|
+
return 1;
|
|
285
|
+
}
|
|
286
|
+
return Math.max(1, Math.floor(totalRecords.value / 100));
|
|
287
|
+
});
|
|
288
|
+
const displayedProcessedCount = computed(() => {
|
|
289
|
+
const step = progressStep.value;
|
|
290
|
+
if (step <= 1) {
|
|
291
|
+
return processedCount.value;
|
|
292
|
+
}
|
|
293
|
+
if (processedCount.value >= totalRecords.value) {
|
|
294
|
+
return totalRecords.value;
|
|
295
|
+
}
|
|
296
|
+
return Math.floor(processedCount.value / step) * step;
|
|
297
|
+
});
|
|
298
|
+
const displayedProgressPercent = computed(() => {
|
|
299
|
+
if (!totalRecords.value) {
|
|
300
|
+
return 0;
|
|
301
|
+
}
|
|
302
|
+
return Math.min(100, Math.round((displayedProcessedCount.value / totalRecords.value) * 100));
|
|
303
|
+
});
|
|
304
|
+
const isProcessingAny = computed(() => {
|
|
305
|
+
recordsVersion.value;
|
|
306
|
+
return Array.from(recordsById.values()).some(record => record.status === 'processing');
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
const tableHeaders = computed(() => generateTableHeaders(props.meta.outputFields));
|
|
310
|
+
const customFieldNames = computed(() => tableHeaders.value.slice((props.meta.isAttachFiles) ? 3 : 2).map(h => h.fieldName));
|
|
311
|
+
const recordsVersion = ref(0);
|
|
312
|
+
const recordsList = computed(() => {
|
|
313
|
+
recordsVersion.value;
|
|
314
|
+
const ids = isGenerationCancelled.value
|
|
315
|
+
? recordIds.value.filter(id => completedRecordIds.value.has(String(id)))
|
|
316
|
+
: recordIds.value;
|
|
317
|
+
return ids.map(id => getOrCreateRecord(id));
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
function checkIfDialogOpen() {
|
|
321
|
+
return isDialogOpen.value === true;
|
|
322
|
+
}
|
|
243
323
|
|
|
244
324
|
const openDialog = async () => {
|
|
245
325
|
window.addEventListener('beforeunload', beforeUnloadHandler);
|
|
@@ -251,32 +331,9 @@ const openDialog = async () => {
|
|
|
251
331
|
isDialogOpen.value = true;
|
|
252
332
|
confirmDialog.value.open();
|
|
253
333
|
isFetchingRecords.value = true;
|
|
254
|
-
await
|
|
255
|
-
if (props.meta.isAttachFiles) {
|
|
256
|
-
await getImages();
|
|
257
|
-
}
|
|
334
|
+
await initializeGlobalState();
|
|
258
335
|
await findPreviewURLForImages();
|
|
259
|
-
tableHeaders.value = generateTableHeaders(props.meta.outputFields);
|
|
260
|
-
const result = generateTableColumns();
|
|
261
|
-
tableColumns.value = result.tableData;
|
|
262
|
-
tableColumnsIndexes.value = result.indexes;
|
|
263
|
-
customFieldNames.value = tableHeaders.value.slice((props.meta.isAttachFiles) ? 3 : 2).map(h => h.fieldName);
|
|
264
|
-
setSelected();
|
|
265
|
-
if (props.meta.isImageGeneration) {
|
|
266
|
-
fillCarouselSaveImages();
|
|
267
|
-
}
|
|
268
|
-
for (let i = 0; i < selected.value?.length; i++) {
|
|
269
|
-
openGenerationCarousel.value[i] = props.meta.outputImageFields?.reduce((acc,key) =>{
|
|
270
|
-
acc[key] = false;
|
|
271
|
-
return acc;
|
|
272
|
-
},{[primaryKey]: records.value[i][primaryKey]} as Record<string, boolean>);
|
|
273
|
-
openImageCompare.value[i] = props.meta.outputImageFields?.reduce((acc,key) =>{
|
|
274
|
-
acc[key] = false;
|
|
275
|
-
return acc;
|
|
276
|
-
},{[primaryKey]: records.value[i][primaryKey]} as Record<string, boolean>);
|
|
277
|
-
}
|
|
278
336
|
isFetchingRecords.value = false;
|
|
279
|
-
// Ensure prompts are loaded before any automatic AI action run
|
|
280
337
|
if (!generationPrompts.value || Object.keys(generationPrompts.value).length === 0) {
|
|
281
338
|
await getGenerationPrompts();
|
|
282
339
|
}
|
|
@@ -284,196 +341,544 @@ const openDialog = async () => {
|
|
|
284
341
|
runAiActions();
|
|
285
342
|
}
|
|
286
343
|
}
|
|
344
|
+
|
|
345
|
+
async function getListOfIds() {
|
|
346
|
+
if ( props.meta.recordSelector === 'filtered' ) {
|
|
347
|
+
const filters = filtersStore.getFilters(props.resource.resourceId);
|
|
348
|
+
let res;
|
|
349
|
+
try {
|
|
350
|
+
res = await callAdminForthApi({
|
|
351
|
+
path: `/plugin/${props.meta.pluginInstanceId}/get_filtered_ids`,
|
|
352
|
+
method: 'POST',
|
|
353
|
+
body: { filters },
|
|
354
|
+
silentError: true,
|
|
355
|
+
});
|
|
356
|
+
} catch (e) {
|
|
357
|
+
console.error('Failed to get records for filtered selector:', e);
|
|
358
|
+
isError.value = true;
|
|
359
|
+
errorMessage.value = t(`Failed to fetch records. Please, try to re-run the action.`);
|
|
360
|
+
return [];
|
|
361
|
+
}
|
|
362
|
+
if (!res?.ok || !res?.recordIds) {
|
|
363
|
+
console.error('Failed to get records for filtered selector, response error:', res);
|
|
364
|
+
isError.value = true;
|
|
365
|
+
errorMessage.value = t(`Failed to fetch records. Please, try to re-run the action.`);
|
|
366
|
+
return [];
|
|
367
|
+
}
|
|
368
|
+
return res.recordIds;
|
|
369
|
+
} else {
|
|
370
|
+
return props.checkboxes;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
287
373
|
|
|
288
374
|
|
|
289
|
-
function runAiActions() {
|
|
375
|
+
async function runAiActions() {
|
|
290
376
|
popupMode.value = 'generation';
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
isGeneratingImages.value = true;
|
|
294
|
-
runAiAction({
|
|
295
|
-
endpoint: 'initial_image_generate',
|
|
296
|
-
actionType: 'generate_images',
|
|
297
|
-
responseFlag: isAiResponseReceivedImage,
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
if (props.meta.isFieldsForAnalizeFromImages) {
|
|
301
|
-
isAnalizingImages.value = true;
|
|
302
|
-
runAiAction({
|
|
303
|
-
endpoint: 'analyze',
|
|
304
|
-
actionType: 'analyze',
|
|
305
|
-
responseFlag: isAiResponseReceivedAnalizeImage,
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
if (props.meta.isFieldsForAnalizePlain) {
|
|
309
|
-
isAnalizingFields.value = true;
|
|
310
|
-
runAiAction({
|
|
311
|
-
endpoint: 'analyze_no_images',
|
|
312
|
-
actionType: 'analyze_no_images',
|
|
313
|
-
responseFlag: isAiResponseReceivedAnalizeNoImage,
|
|
314
|
-
});
|
|
377
|
+
if (!await checkRateLimits()) {
|
|
378
|
+
return;
|
|
315
379
|
}
|
|
380
|
+
isGenerationCancelled.value = false;
|
|
381
|
+
isGenerationPaused.value = false;
|
|
382
|
+
isActiveGeneration.value = true;
|
|
383
|
+
completedRecordIds.value = new Set();
|
|
384
|
+
startedRecordCount.value = 0;
|
|
385
|
+
const limit = pLimit(props.meta.concurrencyLimit || 10);
|
|
386
|
+
const tasks = recordIds.value
|
|
387
|
+
.map(id => limit(() => processOneRecord(String(id))));
|
|
388
|
+
await Promise.all(tasks);
|
|
389
|
+
isActiveGeneration.value = false;
|
|
316
390
|
}
|
|
317
391
|
|
|
318
392
|
const closeDialog = () => {
|
|
319
393
|
window.removeEventListener('beforeunload', beforeUnloadHandler);
|
|
320
|
-
|
|
321
|
-
isAiResponseReceivedAnalizeNoImage.value = [];
|
|
322
|
-
isAiResponseReceivedImage.value = [];
|
|
323
|
-
|
|
324
|
-
imageToTextErrorMessages.value = [];
|
|
325
|
-
textToTextErrorMessages.value = [];
|
|
326
|
-
imageGenerationErrorMessage.value = [];
|
|
327
|
-
regeneratingFieldsStatus.value = {};
|
|
328
|
-
|
|
329
|
-
records.value = [];
|
|
330
|
-
images.value = [];
|
|
331
|
-
selected.value = [];
|
|
332
|
-
tableColumns.value = [];
|
|
333
|
-
tableColumnsIndexes.value = [];
|
|
394
|
+
resetGlobalState();
|
|
334
395
|
isError.value = false;
|
|
335
|
-
isCriticalError.value = false;
|
|
336
|
-
isImageGenerationError.value = false;
|
|
337
396
|
errorMessage.value = '';
|
|
338
397
|
isDialogOpen.value = false;
|
|
339
398
|
popupMode.value = 'confirmation';
|
|
340
399
|
isDataSaved.value = false;
|
|
341
400
|
}
|
|
342
401
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
402
|
+
async function initializeGlobalState() {
|
|
403
|
+
const ids = await getListOfIds();
|
|
404
|
+
recordIds.value = ids;
|
|
405
|
+
recordsById.clear();
|
|
406
|
+
uncheckedRecordIds.clear();
|
|
407
|
+
}
|
|
346
408
|
|
|
347
|
-
function
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
409
|
+
function resetGlobalState() {
|
|
410
|
+
recordIds.value = [];
|
|
411
|
+
recordsById.clear();
|
|
412
|
+
uncheckedRecordIds.clear();
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function getOrCreateRecord(recordId: string | number): RecordState {
|
|
416
|
+
const key = String(recordId);
|
|
417
|
+
let record = recordsById.get(key);
|
|
418
|
+
if (!record) {
|
|
419
|
+
record = createEmptyRecord(recordId);
|
|
420
|
+
record.isChecked = !uncheckedRecordIds.has(key);
|
|
421
|
+
recordsById.set(key, record);
|
|
422
|
+
}
|
|
423
|
+
return record;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function touchRecords() {
|
|
427
|
+
recordsVersion.value += 1;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function waitForResumeIfPaused() {
|
|
431
|
+
if (!isGenerationPaused.value) {
|
|
432
|
+
return Promise.resolve();
|
|
433
|
+
}
|
|
434
|
+
return new Promise<void>(resolve => {
|
|
435
|
+
pendingResumeResolver.value = resolve;
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function resolvePause() {
|
|
440
|
+
if (pendingResumeResolver.value) {
|
|
441
|
+
pendingResumeResolver.value();
|
|
442
|
+
pendingResumeResolver.value = null;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function shouldPauseAfterRecords(processed: number) {
|
|
447
|
+
if (!pauseBreakpoints.value?.length) {
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
return pauseBreakpoints.value.some((rule: any) => {
|
|
451
|
+
if (typeof rule?.afterRecords === 'number' && processed === rule.afterRecords) {
|
|
452
|
+
return true;
|
|
356
453
|
}
|
|
357
|
-
|
|
358
|
-
|
|
454
|
+
if (typeof rule?.everyRecords === 'number' && rule.everyRecords > 0 && processed % rule.everyRecords === 0) {
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
return false;
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function resumeGeneration() {
|
|
462
|
+
if (!isGenerationPaused.value) {
|
|
463
|
+
return;
|
|
359
464
|
}
|
|
465
|
+
isGenerationPaused.value = false;
|
|
466
|
+
resolvePause();
|
|
360
467
|
}
|
|
361
468
|
|
|
469
|
+
function cancelGeneration() {
|
|
470
|
+
if (isGenerationCancelled.value) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
isGenerationCancelled.value = true;
|
|
474
|
+
isGenerationPaused.value = false;
|
|
475
|
+
resolvePause();
|
|
476
|
+
const generatedIds = new Set(completedRecordIds.value);
|
|
477
|
+
recordIds.value = recordIds.value.filter(id => generatedIds.has(String(id)));
|
|
478
|
+
for (const key of Array.from(recordsById.keys())) {
|
|
479
|
+
if (!generatedIds.has(key)) {
|
|
480
|
+
recordsById.delete(key);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
for (const key of Array.from(uncheckedRecordIds)) {
|
|
484
|
+
if (!generatedIds.has(key)) {
|
|
485
|
+
uncheckedRecordIds.delete(key);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
touchRecords();
|
|
489
|
+
tableRef.value?.refresh();
|
|
490
|
+
}
|
|
362
491
|
|
|
363
|
-
function
|
|
364
|
-
const
|
|
365
|
-
let
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
492
|
+
async function withStartGate<T>(fn: () => Promise<T>) {
|
|
493
|
+
const previousGate = startGate;
|
|
494
|
+
let releaseGate: () => void;
|
|
495
|
+
startGate = new Promise<void>(resolve => {
|
|
496
|
+
releaseGate = resolve;
|
|
497
|
+
});
|
|
498
|
+
await previousGate;
|
|
499
|
+
try {
|
|
500
|
+
return await fn();
|
|
501
|
+
} finally {
|
|
502
|
+
releaseGate!();
|
|
369
503
|
}
|
|
370
|
-
if (labelFromMeta) return labelFromMeta;
|
|
371
|
-
return str
|
|
372
|
-
.split('_')
|
|
373
|
-
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
374
|
-
.join(' ');
|
|
375
504
|
}
|
|
376
505
|
|
|
377
|
-
function
|
|
378
|
-
const
|
|
506
|
+
function createImageFieldMap<T>(factory: () => T): Record<string, T> {
|
|
507
|
+
const result: Record<string, T> = {};
|
|
508
|
+
for (const field of props.meta.outputImageFields || []) {
|
|
509
|
+
result[field] = factory();
|
|
510
|
+
}
|
|
511
|
+
return result;
|
|
512
|
+
}
|
|
379
513
|
|
|
380
|
-
|
|
381
|
-
|
|
514
|
+
function createEmptyRecord(recordId: string | number): RecordState {
|
|
515
|
+
return {
|
|
516
|
+
id: recordId,
|
|
517
|
+
status: 'pending',
|
|
518
|
+
isChecked: true,
|
|
519
|
+
label: '',
|
|
520
|
+
data: {},
|
|
521
|
+
oldData: {},
|
|
522
|
+
images: [],
|
|
523
|
+
aiStatus: {
|
|
524
|
+
generatedImages: !props.meta.isImageGeneration,
|
|
525
|
+
analyzedImages: !props.meta.isFieldsForAnalizeFromImages,
|
|
526
|
+
analyzedNoImages: !props.meta.isFieldsForAnalizePlain,
|
|
527
|
+
},
|
|
528
|
+
openGenerationCarousel: createImageFieldMap(() => false),
|
|
529
|
+
openImageCompare: createImageFieldMap(() => false),
|
|
530
|
+
carouselSaveImages: createImageFieldMap(() => []),
|
|
531
|
+
carouselImageIndex: createImageFieldMap(() => 0),
|
|
532
|
+
imageGenerationErrorMessage: '',
|
|
533
|
+
imageGenerationFailed: false,
|
|
534
|
+
imageToTextErrorMessages: {},
|
|
535
|
+
textToTextErrorMessages: {},
|
|
536
|
+
regeneratingFieldsStatus: {},
|
|
537
|
+
listOfImageNotGenerated: {},
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
async function processOneRecord(recordId: string) {
|
|
542
|
+
if (!checkIfDialogOpen()) {
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
if (isGenerationCancelled.value) {
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
await withStartGate(async () => {
|
|
549
|
+
while (true) {
|
|
550
|
+
if (!checkIfDialogOpen() || isGenerationCancelled.value) {
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
if (isGenerationPaused.value) {
|
|
554
|
+
await waitForResumeIfPaused();
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
const nextStarted = startedRecordCount.value + 1;
|
|
558
|
+
startedRecordCount.value = nextStarted;
|
|
559
|
+
if (isActiveGeneration.value && shouldPauseAfterRecords(nextStarted)) {
|
|
560
|
+
isGenerationPaused.value = true;
|
|
561
|
+
}
|
|
562
|
+
break;
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
if (!checkIfDialogOpen() || isGenerationCancelled.value) {
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
const record = getOrCreateRecord(recordId);
|
|
569
|
+
if (!record || !record.isChecked) {
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
record.status = 'processing';
|
|
573
|
+
touchRecords();
|
|
574
|
+
record.imageGenerationFailed = false;
|
|
575
|
+
record.imageGenerationErrorMessage = '';
|
|
576
|
+
record.imageToTextErrorMessages = {};
|
|
577
|
+
record.textToTextErrorMessages = {};
|
|
578
|
+
record.aiStatus.generatedImages = !props.meta.isImageGeneration;
|
|
579
|
+
record.aiStatus.analyzedImages = !props.meta.isFieldsForAnalizeFromImages;
|
|
580
|
+
record.aiStatus.analyzedNoImages = !props.meta.isFieldsForAnalizePlain;
|
|
581
|
+
|
|
582
|
+
const oldDataResult = await fetchOldData(recordId);
|
|
583
|
+
if (!checkIfDialogOpen()) {
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
if (!oldDataResult) {
|
|
587
|
+
record.status = 'failed';
|
|
588
|
+
record.aiStatus.generatedImages = true;
|
|
589
|
+
record.aiStatus.analyzedImages = true;
|
|
590
|
+
record.aiStatus.analyzedNoImages = true;
|
|
591
|
+
touchRecords();
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
record.label = oldDataResult._label || record.label;
|
|
595
|
+
initializeRecordData(record, oldDataResult);
|
|
382
596
|
if (props.meta.isAttachFiles) {
|
|
383
|
-
|
|
597
|
+
await fetchImages(record, oldDataResult);
|
|
598
|
+
if (!checkIfDialogOpen()) {
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
384
601
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
});
|
|
602
|
+
|
|
603
|
+
const actions: Array<'generate_images' | 'analyze' | 'analyze_no_images'> = [];
|
|
604
|
+
if (props.meta.isImageGeneration) {
|
|
605
|
+
actions.push('generate_images');
|
|
390
606
|
}
|
|
391
|
-
|
|
607
|
+
if (props.meta.isFieldsForAnalizeFromImages) {
|
|
608
|
+
actions.push('analyze');
|
|
609
|
+
}
|
|
610
|
+
if (props.meta.isFieldsForAnalizePlain) {
|
|
611
|
+
actions.push('analyze_no_images');
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const results = await Promise.allSettled(actions.map(actionType => runActionForRecord(record, actionType)));
|
|
615
|
+
if (!checkIfDialogOpen()) {
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
const hasError = results.some(result => result.status === 'rejected');
|
|
619
|
+
record.status = hasError ? 'failed' : 'completed';
|
|
620
|
+
completedRecordIds.value.add(String(recordId));
|
|
621
|
+
touchRecords();
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
async function checkRateLimits() {
|
|
625
|
+
const actionsToCheck: Array<'generate_images' | 'analyze' | 'analyze_no_images'> = [];
|
|
626
|
+
if (props.meta.isImageGeneration) {
|
|
627
|
+
actionsToCheck.push('generate_images');
|
|
628
|
+
}
|
|
629
|
+
if (props.meta.isFieldsForAnalizeFromImages) {
|
|
630
|
+
actionsToCheck.push('analyze');
|
|
631
|
+
}
|
|
632
|
+
if (props.meta.isFieldsForAnalizePlain) {
|
|
633
|
+
actionsToCheck.push('analyze_no_images');
|
|
634
|
+
}
|
|
635
|
+
for (const actionType of actionsToCheck) {
|
|
636
|
+
try {
|
|
637
|
+
const rateLimitRes = await callAdminForthApi({
|
|
638
|
+
path: `/plugin/${props.meta.pluginInstanceId}/update-rate-limits`,
|
|
639
|
+
method: 'POST',
|
|
640
|
+
body: { actionType },
|
|
641
|
+
});
|
|
642
|
+
if (rateLimitRes?.error || rateLimitRes?.ok === false) {
|
|
643
|
+
adminforth.alert({
|
|
644
|
+
message: t(`Rate limit exceeded for "${actionType.replace('_', ' ')}" action. Please try again later.`),
|
|
645
|
+
variant: 'danger',
|
|
646
|
+
timeout: 'unlimited',
|
|
647
|
+
});
|
|
648
|
+
return false;
|
|
649
|
+
}
|
|
650
|
+
} catch (e) {
|
|
651
|
+
adminforth.alert({
|
|
652
|
+
message: t(`Error checking rate limit for "${actionType.replace('_', ' ')}" action.`),
|
|
653
|
+
variant: 'danger',
|
|
654
|
+
timeout: 'unlimited',
|
|
655
|
+
});
|
|
656
|
+
return false;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return true;
|
|
392
660
|
}
|
|
393
661
|
|
|
394
|
-
function
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
662
|
+
async function runActionForRecord(record: RecordState, actionType: 'analyze' | 'analyze_no_images' | 'generate_images') {
|
|
663
|
+
if (!checkIfDialogOpen()) {
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
const responseFlag = actionType === 'generate_images'
|
|
667
|
+
? 'generatedImages'
|
|
668
|
+
: actionType === 'analyze'
|
|
669
|
+
? 'analyzedImages'
|
|
670
|
+
: 'analyzedNoImages';
|
|
671
|
+
record.aiStatus[responseFlag] = false;
|
|
672
|
+
|
|
673
|
+
let customPrompt;
|
|
674
|
+
if (actionType === 'generate_images') {
|
|
675
|
+
customPrompt = generationPrompts.value.imageGenerationPrompts || generationPrompts.value.generateImages;
|
|
676
|
+
} else if (actionType === 'analyze') {
|
|
677
|
+
customPrompt = generationPrompts.value.imageFieldsPrompts;
|
|
678
|
+
} else if (actionType === 'analyze_no_images') {
|
|
679
|
+
customPrompt = generationPrompts.value.plainFieldsPrompts;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
let createJobResponse;
|
|
683
|
+
try {
|
|
684
|
+
if (!checkIfDialogOpen()) {
|
|
685
|
+
return;
|
|
406
686
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
687
|
+
createJobResponse = await callAdminForthApi({
|
|
688
|
+
path: `/plugin/${props.meta.pluginInstanceId}/create-job`,
|
|
689
|
+
method: 'POST',
|
|
690
|
+
body: {
|
|
691
|
+
actionType,
|
|
692
|
+
recordId: record.id,
|
|
693
|
+
...(customPrompt !== undefined ? { customPrompt: JSON.stringify(customPrompt) } : {}),
|
|
694
|
+
filterFilledFields: !overwriteExistingValues.value,
|
|
695
|
+
},
|
|
696
|
+
silentError: true,
|
|
697
|
+
});
|
|
698
|
+
} catch (e) {
|
|
699
|
+
record.aiStatus[responseFlag] = true;
|
|
700
|
+
throw e;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if (!checkIfDialogOpen()) {
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
if (createJobResponse?.error || !createJobResponse?.jobId) {
|
|
708
|
+
record.aiStatus[responseFlag] = true;
|
|
709
|
+
adminforth.alert({
|
|
710
|
+
message: t(`Failed to ${actionType.replace('_', ' ')}. Please, try to re-run the action.`),
|
|
711
|
+
variant: 'danger',
|
|
712
|
+
timeout: 'unlimited',
|
|
413
713
|
});
|
|
414
|
-
|
|
714
|
+
throw new Error(createJobResponse?.error || 'Failed to create job');
|
|
415
715
|
}
|
|
416
|
-
|
|
716
|
+
|
|
717
|
+
await pollJob(record, createJobResponse.jobId, actionType, responseFlag);
|
|
417
718
|
}
|
|
418
719
|
|
|
419
|
-
function
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
720
|
+
async function pollJob(
|
|
721
|
+
record: RecordState,
|
|
722
|
+
jobId: string,
|
|
723
|
+
actionType: 'analyze' | 'analyze_no_images' | 'generate_images',
|
|
724
|
+
responseFlag: keyof RecordState['aiStatus']
|
|
725
|
+
) {
|
|
726
|
+
let isInProgress = true;
|
|
727
|
+
while (isInProgress && isDialogOpen.value) {
|
|
728
|
+
const jobResponse = await callAdminForthApi({
|
|
729
|
+
path: `/plugin/${props.meta.pluginInstanceId}/get-job-status`,
|
|
730
|
+
method: 'POST',
|
|
731
|
+
body: { jobId },
|
|
732
|
+
silentError: true,
|
|
733
|
+
});
|
|
734
|
+
if (!jobResponse) {
|
|
735
|
+
await waitForRefresh(actionType);
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
if (jobResponse?.error) {
|
|
739
|
+
record.aiStatus[responseFlag] = true;
|
|
740
|
+
throw new Error(jobResponse.error);
|
|
741
|
+
}
|
|
742
|
+
const jobStatus = jobResponse?.job?.status;
|
|
743
|
+
if (jobStatus === 'in_progress') {
|
|
744
|
+
await waitForRefresh(actionType);
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
if (jobStatus === 'completed') {
|
|
748
|
+
applyJobResult(record, jobResponse.job, actionType);
|
|
749
|
+
record.aiStatus[responseFlag] = true;
|
|
750
|
+
isInProgress = false;
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
if (jobStatus === 'failed') {
|
|
754
|
+
applyJobFailure(record, jobResponse.job, actionType);
|
|
755
|
+
record.aiStatus[responseFlag] = true;
|
|
756
|
+
throw new Error(jobResponse.job?.error || 'Job failed');
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function applyJobResult(record: RecordState, job: any, actionType: 'analyze' | 'analyze_no_images' | 'generate_images') {
|
|
762
|
+
if (actionType === 'generate_images') {
|
|
763
|
+
for (const fieldName of props.meta.outputImageFields || []) {
|
|
764
|
+
const resultValue = job?.result?.[fieldName];
|
|
765
|
+
if (resultValue !== undefined) {
|
|
766
|
+
record.data[fieldName] = resultValue;
|
|
767
|
+
}
|
|
768
|
+
record.carouselSaveImages[fieldName] = resultValue ? [resultValue] : [];
|
|
769
|
+
if (job?.recordMeta?.[`${fieldName}_meta`]) {
|
|
770
|
+
record.carouselSaveImages[fieldName] = [job.recordMeta[`${fieldName}_meta`].originalImage];
|
|
771
|
+
record.listOfImageNotGenerated[fieldName] = job.recordMeta[`${fieldName}_meta`];
|
|
429
772
|
}
|
|
430
773
|
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
774
|
+
} else {
|
|
775
|
+
record.data = {
|
|
776
|
+
...record.data,
|
|
777
|
+
...(job?.result || {}),
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
touchRecords();
|
|
435
781
|
}
|
|
436
782
|
|
|
437
|
-
function
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
783
|
+
function applyJobFailure(record: RecordState, job: any, actionType: 'analyze' | 'analyze_no_images' | 'generate_images') {
|
|
784
|
+
adminforth.alert({
|
|
785
|
+
message: t(`Generation action "${actionType.replace('_', ' ')}" failed for record: ${record.id}. Error: ${job?.error || 'Unknown error'}`),
|
|
786
|
+
variant: 'danger',
|
|
787
|
+
timeout: 'unlimited',
|
|
788
|
+
});
|
|
789
|
+
if (actionType === 'generate_images') {
|
|
790
|
+
record.imageGenerationFailed = true;
|
|
791
|
+
record.imageGenerationErrorMessage = job?.error || 'Unknown error';
|
|
792
|
+
} else if (actionType === 'analyze') {
|
|
793
|
+
for (const field of Object.keys(props.meta.outputFieldsForAnalizeFromImages || {})) {
|
|
794
|
+
record.imageToTextErrorMessages[props.meta.outputFieldsForAnalizeFromImages[field]] = job?.error || 'Unknown error';
|
|
795
|
+
}
|
|
796
|
+
} else if (actionType === 'analyze_no_images') {
|
|
797
|
+
for (const field of Object.keys(props.meta.outputPlainFields || {})) {
|
|
798
|
+
record.textToTextErrorMessages[props.meta.outputPlainFields[field]] = job?.error || 'Unknown error';
|
|
799
|
+
}
|
|
441
800
|
}
|
|
442
|
-
|
|
801
|
+
touchRecords();
|
|
443
802
|
}
|
|
444
803
|
|
|
445
|
-
function
|
|
446
|
-
|
|
447
|
-
|
|
804
|
+
async function waitForRefresh(actionType: 'analyze' | 'analyze_no_images' | 'generate_images') {
|
|
805
|
+
if (actionType === 'generate_images') {
|
|
806
|
+
await new Promise(resolve => setTimeout(resolve, props.meta.refreshRates?.generateImages));
|
|
807
|
+
} else if (actionType === 'analyze') {
|
|
808
|
+
await new Promise(resolve => setTimeout(resolve, props.meta.refreshRates?.fillFieldsFromImages));
|
|
809
|
+
} else if (actionType === 'analyze_no_images') {
|
|
810
|
+
await new Promise(resolve => setTimeout(resolve, props.meta.refreshRates?.fillPlainFields));
|
|
811
|
+
}
|
|
448
812
|
}
|
|
449
813
|
|
|
450
|
-
async function
|
|
814
|
+
async function fetchOldData(recordId: string) {
|
|
451
815
|
try {
|
|
452
816
|
const res = await callAdminForthApi({
|
|
453
|
-
path: `/plugin/${props.meta.pluginInstanceId}/
|
|
817
|
+
path: `/plugin/${props.meta.pluginInstanceId}/get_old_data`,
|
|
454
818
|
method: 'POST',
|
|
455
|
-
body: {
|
|
456
|
-
|
|
457
|
-
},
|
|
819
|
+
body: { recordId },
|
|
820
|
+
silentError: true,
|
|
458
821
|
});
|
|
459
|
-
|
|
822
|
+
if (!res?.ok || !res?.record) {
|
|
823
|
+
adminforth.alert({
|
|
824
|
+
message: res?.error || t('Failed to fetch old data. Please, try to re-run the action.'),
|
|
825
|
+
variant: 'danger',
|
|
826
|
+
timeout: 'unlimited',
|
|
827
|
+
});
|
|
828
|
+
return null;
|
|
829
|
+
}
|
|
830
|
+
return res.record;
|
|
460
831
|
} catch (error) {
|
|
461
|
-
console.error('Failed to get
|
|
832
|
+
console.error('Failed to get old record:', error);
|
|
462
833
|
isError.value = true;
|
|
463
834
|
errorMessage.value = t(`Failed to fetch records. Please, try to re-run the action.`);
|
|
835
|
+
return null;
|
|
464
836
|
}
|
|
465
837
|
}
|
|
466
838
|
|
|
467
|
-
|
|
839
|
+
function initializeRecordData(record: RecordState, oldRecord: Record<string, any>) {
|
|
840
|
+
const newData: Record<string, any> = {};
|
|
841
|
+
const newOldData: Record<string, any> = {};
|
|
842
|
+
for (const key in props.meta.outputFields || {}) {
|
|
843
|
+
const normalizedValue = normalizeEnumValue(key, oldRecord[key] ?? null);
|
|
844
|
+
newData[key] = normalizedValue;
|
|
845
|
+
newOldData[key] = normalizedValue;
|
|
846
|
+
}
|
|
847
|
+
if (props.meta.outputImageFields) {
|
|
848
|
+
for (const key of props.meta.outputImageFields) {
|
|
849
|
+
const normalizedValue = normalizeEnumValue(key, oldRecord[key] ?? null);
|
|
850
|
+
newData[key] = normalizedValue;
|
|
851
|
+
newOldData[key] = normalizedValue;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
newData[primaryKey] = oldRecord[primaryKey] ?? record.id;
|
|
855
|
+
newOldData[primaryKey] = oldRecord[primaryKey] ?? record.id;
|
|
856
|
+
newOldData._label = oldRecord._label;
|
|
857
|
+
record.data = newData;
|
|
858
|
+
record.oldData = newOldData;
|
|
859
|
+
touchRecords();
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function normalizeEnumValue(key: string, value: any) {
|
|
863
|
+
const colEnum = props.meta.columnEnums?.find(c => c.name === key);
|
|
864
|
+
if (!colEnum) {
|
|
865
|
+
return value;
|
|
866
|
+
}
|
|
867
|
+
const match = colEnum.enum.find(item => item.value === value);
|
|
868
|
+
return match ? value : null;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
async function fetchImages(record: RecordState, oldRecord: Record<string, any>) {
|
|
468
872
|
try {
|
|
469
873
|
const res = await callAdminForthApi({
|
|
470
874
|
path: `/plugin/${props.meta.pluginInstanceId}/get_images`,
|
|
471
875
|
method: 'POST',
|
|
472
876
|
body: {
|
|
473
|
-
record:
|
|
877
|
+
record: [oldRecord],
|
|
474
878
|
},
|
|
475
879
|
});
|
|
476
|
-
images
|
|
880
|
+
record.images = res.images?.[0] || [];
|
|
881
|
+
touchRecords();
|
|
477
882
|
} catch (error) {
|
|
478
883
|
console.error('Failed to get images:', error);
|
|
479
884
|
isError.value = true;
|
|
@@ -481,33 +886,66 @@ async function getImages() {
|
|
|
481
886
|
}
|
|
482
887
|
}
|
|
483
888
|
|
|
484
|
-
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
889
|
+
function formatLabel(str) {
|
|
890
|
+
const labelsMap = props.meta?.columnLabels || {};
|
|
891
|
+
let labelFromMeta = labelsMap[str];
|
|
892
|
+
if (!labelFromMeta) {
|
|
893
|
+
const match = Object.keys(labelsMap).find(k => k.toLowerCase() === String(str).toLowerCase());
|
|
894
|
+
if (match) labelFromMeta = labelsMap[match];
|
|
895
|
+
}
|
|
896
|
+
if (labelFromMeta) return labelFromMeta;
|
|
897
|
+
return str
|
|
898
|
+
.split('_')
|
|
899
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
900
|
+
.join(' ');
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function generateTableHeaders(outputFields) {
|
|
904
|
+
const headers = [];
|
|
905
|
+
|
|
906
|
+
headers.push({ label: 'Checkboxes', fieldName: 'checkboxes' });
|
|
907
|
+
headers.push({ label: 'Field name', fieldName: 'label' });
|
|
908
|
+
if (props.meta.isAttachFiles) {
|
|
909
|
+
headers.push({ label: 'Source Images', fieldName: 'images' });
|
|
910
|
+
}
|
|
911
|
+
for (const key in outputFields) {
|
|
912
|
+
headers.push({
|
|
913
|
+
label: formatLabel(key),
|
|
914
|
+
fieldName: key,
|
|
490
915
|
});
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
916
|
+
}
|
|
917
|
+
return headers;
|
|
918
|
+
}
|
|
919
|
+
function handleTableError(errorData) {
|
|
920
|
+
isError.value = errorData.isError;
|
|
921
|
+
errorMessage.value = errorData.errorMessage;
|
|
922
|
+
}
|
|
923
|
+
async function prepareDataForSave() {
|
|
924
|
+
const checkedRecords = recordIds.value
|
|
925
|
+
.map(id => getOrCreateRecord(id))
|
|
926
|
+
.filter(record => record.isChecked === true);
|
|
927
|
+
const checkedItems = checkedRecords.map(record => ({
|
|
928
|
+
...record.data,
|
|
929
|
+
[primaryKey]: record.id,
|
|
930
|
+
}));
|
|
931
|
+
|
|
932
|
+
const promises: Promise<void>[] = [];
|
|
933
|
+
checkedRecords.forEach((record, index) => {
|
|
934
|
+
if (record.imageGenerationFailed) {
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
for (const [key, value] of Object.entries(checkedItems[index])) {
|
|
938
|
+
if (props.meta.outputImageFields?.includes(key)) {
|
|
939
|
+
const p = convertImages(key, value).then(result => {
|
|
940
|
+
checkedItems[index][key] = result;
|
|
941
|
+
});
|
|
942
|
+
promises.push(p);
|
|
506
943
|
}
|
|
507
944
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
await Promise.all(promises);
|
|
948
|
+
return [checkedItems, checkedRecords] as const;
|
|
511
949
|
}
|
|
512
950
|
|
|
513
951
|
async function convertImages(fieldName, img) {
|
|
@@ -532,36 +970,34 @@ async function convertImages(fieldName, img) {
|
|
|
532
970
|
|
|
533
971
|
|
|
534
972
|
async function saveData() {
|
|
535
|
-
|
|
536
|
-
adminforth.alert({ message: t('No items selected'), variant: 'warning' });
|
|
537
|
-
return;
|
|
538
|
-
}
|
|
973
|
+
const errorText = 'Failed to save some records. Not all data may be saved'
|
|
539
974
|
try {
|
|
540
975
|
isLoading.value = true;
|
|
541
|
-
|
|
542
|
-
if (
|
|
976
|
+
const [reqData, checkedRecords] = await prepareDataForSave();
|
|
977
|
+
if (checkedRecords.length < 1) {
|
|
978
|
+
adminforth.alert({ message: t('No items selected'), variant: 'warning' });
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
if (!checkedRecords.some(record => record.imageGenerationFailed)) {
|
|
543
982
|
const imagesToUpload = [];
|
|
544
|
-
for (
|
|
983
|
+
for (let index = 0; index < reqData.length; index++) {
|
|
984
|
+
const item = reqData[index];
|
|
985
|
+
const record = checkedRecords[index];
|
|
545
986
|
for (const [key, value] of Object.entries(item)) {
|
|
546
|
-
if(props.meta.outputImageFields?.includes(key)) {
|
|
987
|
+
if (props.meta.outputImageFields?.includes(key)) {
|
|
547
988
|
if (!value) {
|
|
548
989
|
continue;
|
|
549
990
|
}
|
|
550
991
|
if (!overwriteExistingValues.value) {
|
|
551
|
-
const imageURL =
|
|
552
|
-
|
|
553
|
-
try {
|
|
554
|
-
originalImageUrl = listOfImageThatWasNotGeneratedPerRecord.value[item[primaryKey]][key].originalImage;
|
|
555
|
-
} catch (error) {
|
|
556
|
-
originalImageUrl = '';
|
|
557
|
-
}
|
|
992
|
+
const imageURL = record.data[key];
|
|
993
|
+
const originalImageUrl = record.listOfImageNotGenerated?.[key]?.originalImage || '';
|
|
558
994
|
if (originalImageUrl === imageURL) {
|
|
559
|
-
reqData
|
|
995
|
+
reqData[index][key] = undefined;
|
|
560
996
|
continue;
|
|
561
997
|
}
|
|
562
998
|
}
|
|
563
|
-
const p = uploadImage(value,
|
|
564
|
-
|
|
999
|
+
const p = uploadImage(value, record.id, key).then(result => {
|
|
1000
|
+
reqData[index][key] = result;
|
|
565
1001
|
});
|
|
566
1002
|
imagesToUpload.push(p);
|
|
567
1003
|
}
|
|
@@ -569,287 +1005,53 @@ async function saveData() {
|
|
|
569
1005
|
}
|
|
570
1006
|
await Promise.all(imagesToUpload);
|
|
571
1007
|
}
|
|
572
|
-
const
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
1008
|
+
const limit = pLimit(props.meta.concurrencyLimit || 10);
|
|
1009
|
+
const saveTasks = checkedRecords.map((record, index) =>
|
|
1010
|
+
limit(async () => {
|
|
1011
|
+
return await callAdminForthApi({
|
|
1012
|
+
path: `/plugin/${props.meta.pluginInstanceId}/update_fields`,
|
|
1013
|
+
method: 'POST',
|
|
1014
|
+
body: {
|
|
1015
|
+
selectedIds: [record.id],
|
|
1016
|
+
fields: [reqData[index]],
|
|
1017
|
+
saveImages: !record.imageGenerationFailed,
|
|
1018
|
+
},
|
|
1019
|
+
});
|
|
1020
|
+
})
|
|
1021
|
+
);
|
|
581
1022
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
1023
|
+
const saveResults = await Promise.all(saveTasks);
|
|
1024
|
+
const failedResult = saveResults.find(res => res?.ok === false || res?.error);
|
|
1025
|
+
|
|
1026
|
+
if (failedResult && failedResult.ok === false) {
|
|
1027
|
+
saveResults.filter(res => res?.ok === false).forEach(res => {
|
|
1028
|
+
adminforth.alert({
|
|
1029
|
+
message: res.error || t(errorText),
|
|
1030
|
+
variant: 'danger',
|
|
1031
|
+
timeout: 'unlimited',
|
|
1032
|
+
});
|
|
1033
|
+
console.error('Error saving data:', res.error);
|
|
591
1034
|
});
|
|
592
1035
|
isError.value = true;
|
|
593
|
-
errorMessage.value = t(
|
|
594
|
-
} else {
|
|
595
|
-
console.error('Error saving data:',
|
|
1036
|
+
errorMessage.value = t(errorText);
|
|
1037
|
+
} else if ( failedResult ) {
|
|
1038
|
+
console.error('Error saving data:', failedResult);
|
|
596
1039
|
isError.value = true;
|
|
597
|
-
errorMessage.value = t(
|
|
1040
|
+
errorMessage.value = t(errorText);
|
|
598
1041
|
}
|
|
599
1042
|
} catch (error) {
|
|
600
1043
|
console.error('Error saving data:', error);
|
|
601
1044
|
isError.value = true;
|
|
602
|
-
errorMessage.value = t(
|
|
1045
|
+
errorMessage.value = t(errorText);
|
|
603
1046
|
} finally {
|
|
1047
|
+
confirmDialog.value.close();
|
|
1048
|
+
props.updateList();
|
|
1049
|
+
props.clearCheckboxes?.();
|
|
604
1050
|
isLoading.value = false;
|
|
605
1051
|
isDataSaved.value = true;
|
|
606
1052
|
window.removeEventListener('beforeunload', beforeUnloadHandler);
|
|
607
1053
|
}
|
|
608
1054
|
}
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
async function runAiAction({
|
|
612
|
-
endpoint,
|
|
613
|
-
actionType,
|
|
614
|
-
responseFlag,
|
|
615
|
-
updateOnSuccess = true,
|
|
616
|
-
recordsIds = props.checkboxes,
|
|
617
|
-
disableRateLimitCheck = false,
|
|
618
|
-
}: {
|
|
619
|
-
endpoint: string;
|
|
620
|
-
actionType: 'analyze' | 'analyze_no_images' | 'generate_images';
|
|
621
|
-
responseFlag: Ref<boolean[]>;
|
|
622
|
-
updateOnSuccess?: boolean;
|
|
623
|
-
recordsIds?: any[];
|
|
624
|
-
disableRateLimitCheck?: boolean;
|
|
625
|
-
}) {
|
|
626
|
-
let hasError = false;
|
|
627
|
-
let errorMessage = '';
|
|
628
|
-
const jobsIds: { jobId: any; recordId: any; }[] = [];
|
|
629
|
-
// responseFlag.value = props.checkboxes.map(() => false);
|
|
630
|
-
for (let i = 0; i < recordsIds.length; i++) {
|
|
631
|
-
const index = props.checkboxes.findIndex(item => String(item) === String(recordsIds[i]));
|
|
632
|
-
if (index !== -1) {
|
|
633
|
-
responseFlag.value[index] = false;
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
let isRateLimitExceeded = false;
|
|
637
|
-
if (!disableRateLimitCheck){
|
|
638
|
-
try {
|
|
639
|
-
const rateLimitRes = await callAdminForthApi({
|
|
640
|
-
path: `/plugin/${props.meta.pluginInstanceId}/update-rate-limits`,
|
|
641
|
-
method: 'POST',
|
|
642
|
-
body: {
|
|
643
|
-
actionType: actionType,
|
|
644
|
-
},
|
|
645
|
-
});
|
|
646
|
-
if (rateLimitRes?.error) {
|
|
647
|
-
isRateLimitExceeded = true;
|
|
648
|
-
adminforth.alert({
|
|
649
|
-
message: t(`Rate limit exceeded for "${actionType.replace('_', ' ')}" action. Please try again later.`),
|
|
650
|
-
variant: 'danger',
|
|
651
|
-
timeout: 'unlimited',
|
|
652
|
-
});
|
|
653
|
-
return;
|
|
654
|
-
}
|
|
655
|
-
} catch (e) {
|
|
656
|
-
adminforth.alert({
|
|
657
|
-
message: t(`Error checking rate limit for "${actionType.replace('_', ' ')}" action.`),
|
|
658
|
-
variant: 'danger',
|
|
659
|
-
timeout: 'unlimited',
|
|
660
|
-
});
|
|
661
|
-
isRateLimitExceeded = true;
|
|
662
|
-
}
|
|
663
|
-
if (isRateLimitExceeded) {
|
|
664
|
-
return;
|
|
665
|
-
};
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
let customPrompt;
|
|
669
|
-
if (actionType === 'generate_images') {
|
|
670
|
-
customPrompt = generationPrompts.value.imageGenerationPrompts || generationPrompts.value.generateImages;
|
|
671
|
-
} else if (actionType === 'analyze') {
|
|
672
|
-
customPrompt = generationPrompts.value.imageFieldsPrompts;
|
|
673
|
-
} else if (actionType === 'analyze_no_images') {
|
|
674
|
-
customPrompt = generationPrompts.value.plainFieldsPrompts;
|
|
675
|
-
}
|
|
676
|
-
//creating jobs
|
|
677
|
-
const tasks = recordsIds.map(async (checkbox, i) => {
|
|
678
|
-
try {
|
|
679
|
-
const res = await callAdminForthApi({
|
|
680
|
-
path: `/plugin/${props.meta.pluginInstanceId}/create-job`,
|
|
681
|
-
method: 'POST',
|
|
682
|
-
body: {
|
|
683
|
-
actionType: actionType,
|
|
684
|
-
recordId: checkbox,
|
|
685
|
-
...(customPrompt !== undefined ? { customPrompt: JSON.stringify(customPrompt) } : {}),
|
|
686
|
-
filterFilledFields: !overwriteExistingValues.value,
|
|
687
|
-
},
|
|
688
|
-
silentError: true,
|
|
689
|
-
});
|
|
690
|
-
|
|
691
|
-
if (res?.error) {
|
|
692
|
-
throw new Error(res.error);
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
if (!res) {
|
|
696
|
-
throw new Error(`${actionType} request returned empty response.`);
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
jobsIds.push({ jobId: res.jobId, recordId: checkbox });
|
|
700
|
-
} catch (e) {
|
|
701
|
-
console.error(`Error during ${actionType} for item ${i}:`, e);
|
|
702
|
-
hasError = true;
|
|
703
|
-
errorMessage = t(`Failed to ${actionType.replace('_', ' ')}. Please, try to re-run the action.`);
|
|
704
|
-
return { success: false, index: i, error: e };
|
|
705
|
-
}
|
|
706
|
-
});
|
|
707
|
-
await Promise.all(tasks);
|
|
708
|
-
|
|
709
|
-
//polling jobs
|
|
710
|
-
let isInProgress = true;
|
|
711
|
-
//if no jobs were created, skip polling
|
|
712
|
-
while (isInProgress && isDialogOpen.value) {
|
|
713
|
-
//check if at least one job is still in progress
|
|
714
|
-
let isAtLeastOneInProgress = false;
|
|
715
|
-
//checking status of each job
|
|
716
|
-
for (const { jobId, recordId } of jobsIds) {
|
|
717
|
-
//check job status
|
|
718
|
-
const jobResponse = await callAdminForthApi({
|
|
719
|
-
path: `/plugin/${props.meta.pluginInstanceId}/get-job-status`,
|
|
720
|
-
method: 'POST',
|
|
721
|
-
body: { jobId },
|
|
722
|
-
silentError: true,
|
|
723
|
-
});
|
|
724
|
-
//check for errors
|
|
725
|
-
if (!jobResponse) {
|
|
726
|
-
isAtLeastOneInProgress = true;
|
|
727
|
-
continue;
|
|
728
|
-
}
|
|
729
|
-
if (jobResponse?.error) {
|
|
730
|
-
console.error(`Error during ${actionType}:`, jobResponse.error);
|
|
731
|
-
break;
|
|
732
|
-
};
|
|
733
|
-
// extract job status
|
|
734
|
-
let jobStatus = jobResponse?.job?.status;
|
|
735
|
-
// check if job is still in progress. If in progress - skip to next job
|
|
736
|
-
if (jobStatus === 'in_progress') {
|
|
737
|
-
isAtLeastOneInProgress = true;
|
|
738
|
-
//if job is completed - update record data
|
|
739
|
-
} else if (jobStatus === 'completed') {
|
|
740
|
-
// finding index of the record in selected array
|
|
741
|
-
const index = selected.value.findIndex(item => String(item[primaryKey]) === String(recordId));
|
|
742
|
-
//if we are generating images - update carouselSaveImages with new image
|
|
743
|
-
if (actionType === 'generate_images') {
|
|
744
|
-
for (const [key, value] of Object.entries(carouselSaveImages.value[index])) {
|
|
745
|
-
if (props.meta.outputImageFields?.includes(key)) {
|
|
746
|
-
carouselSaveImages.value[index][key] = [jobResponse.job.result[key]];
|
|
747
|
-
if (jobResponse.job.recordMeta?.[`${key}_meta`]) {
|
|
748
|
-
carouselSaveImages.value[index][key] = [jobResponse.job.recordMeta[`${key}_meta`].originalImage];
|
|
749
|
-
if (!listOfImageThatWasNotGeneratedPerRecord.value[recordId]) {
|
|
750
|
-
listOfImageThatWasNotGeneratedPerRecord.value[recordId] = [];
|
|
751
|
-
}
|
|
752
|
-
listOfImageThatWasNotGeneratedPerRecord.value[recordId][key] = jobResponse.job.recordMeta[`${key}_meta`];
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
//marking that we received response for this record
|
|
758
|
-
//if (actionType !== 'analyze_no_images' || !props.meta.isFieldsForAnalizeFromImages) {
|
|
759
|
-
responseFlag.value[index] = true;
|
|
760
|
-
//}
|
|
761
|
-
//updating selected with new data from AI
|
|
762
|
-
const pk = selected.value[index]?.[primaryKey];
|
|
763
|
-
if (pk) {
|
|
764
|
-
selected.value[index] = {
|
|
765
|
-
...selected.value[index],
|
|
766
|
-
...jobResponse.job.result,
|
|
767
|
-
isChecked: true,
|
|
768
|
-
[primaryKey]: pk,
|
|
769
|
-
};
|
|
770
|
-
}
|
|
771
|
-
//removing job from jobsIds
|
|
772
|
-
if (index !== -1) {
|
|
773
|
-
jobsIds.splice(jobsIds.findIndex(j => j.jobId === jobId), 1);
|
|
774
|
-
}
|
|
775
|
-
// checking one more time if we have in progress jobs
|
|
776
|
-
isAtLeastOneInProgress = true;
|
|
777
|
-
// if job is failed - set error
|
|
778
|
-
} else if (jobStatus === 'failed') {
|
|
779
|
-
const index = selected.value.findIndex(item => String(item[primaryKey]) === String(recordId));
|
|
780
|
-
//if (actionType !== 'analyze_no_images' || !props.meta.isFieldsForAnalizeFromImages) {
|
|
781
|
-
responseFlag.value[index] = true;
|
|
782
|
-
//}
|
|
783
|
-
if (index !== -1) {
|
|
784
|
-
jobsIds.splice(jobsIds.findIndex(j => j.jobId === jobId), 1);
|
|
785
|
-
} else {
|
|
786
|
-
jobsIds.splice(0, jobsIds.length);
|
|
787
|
-
}
|
|
788
|
-
isAtLeastOneInProgress = true;
|
|
789
|
-
adminforth.alert({
|
|
790
|
-
message: t(`Generation action "${actionType.replace('_', ' ')}" failed for record: ${recordId}. Error: ${jobResponse.job?.error || 'Unknown error'}`),
|
|
791
|
-
variant: 'danger',
|
|
792
|
-
timeout: 'unlimited',
|
|
793
|
-
});
|
|
794
|
-
if (actionType === 'generate_images') {
|
|
795
|
-
isAiImageGenerationError.value[index] = true;
|
|
796
|
-
imageGenerationErrorMessage.value[index] = jobResponse.job?.error || 'Unknown error';
|
|
797
|
-
} else if (actionType === 'analyze') {
|
|
798
|
-
for (const field of Object.keys(props.meta.outputFieldsForAnalizeFromImages ) ) {
|
|
799
|
-
if (!imageToTextErrorMessages.value[index]) {
|
|
800
|
-
imageToTextErrorMessages.value[index] = {};
|
|
801
|
-
}
|
|
802
|
-
imageToTextErrorMessages.value[index][props.meta.outputFieldsForAnalizeFromImages[field]] = jobResponse.job?.error || 'Unknown error';
|
|
803
|
-
}
|
|
804
|
-
} else if (actionType === 'analyze_no_images') {
|
|
805
|
-
for( const field of Object.keys(props.meta.outputPlainFields ) ) {
|
|
806
|
-
if (!textToTextErrorMessages.value[index]) {
|
|
807
|
-
textToTextErrorMessages.value[index] = {};
|
|
808
|
-
}
|
|
809
|
-
textToTextErrorMessages.value[index][props.meta.outputPlainFields[field]] = jobResponse.job?.error || 'Unknown error';
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
if (!isAtLeastOneInProgress) {
|
|
815
|
-
isInProgress = false;
|
|
816
|
-
}
|
|
817
|
-
if (jobsIds.length > 0) {
|
|
818
|
-
if (actionType === 'generate_images') {
|
|
819
|
-
await new Promise(resolve => setTimeout(resolve, props.meta.refreshRates?.generateImages));
|
|
820
|
-
} else if (actionType === 'analyze') {
|
|
821
|
-
await new Promise(resolve => setTimeout(resolve, props.meta.refreshRates?.fillFieldsFromImages));
|
|
822
|
-
} else if (actionType === 'analyze_no_images') {
|
|
823
|
-
await new Promise(resolve => setTimeout(resolve, props.meta.refreshRates?.fillPlainFields));
|
|
824
|
-
} else {
|
|
825
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
if (hasError) {
|
|
831
|
-
adminforth.alert({
|
|
832
|
-
message: errorMessage,
|
|
833
|
-
variant: 'danger',
|
|
834
|
-
timeout: 'unlimited',
|
|
835
|
-
});
|
|
836
|
-
isError.value = true;
|
|
837
|
-
if (actionType === 'generate_images') {
|
|
838
|
-
isImageGenerationError.value = true;
|
|
839
|
-
}
|
|
840
|
-
this.errorMessage.value = errorMessage;
|
|
841
|
-
return;
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
if (actionType === 'generate_images') {
|
|
845
|
-
isGeneratingImages.value = false;
|
|
846
|
-
} else if (actionType === 'analyze') {
|
|
847
|
-
isAnalizingImages.value = false;
|
|
848
|
-
} else if (actionType === 'analyze_no_images') {
|
|
849
|
-
isAnalizingFields.value = false;
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
|
|
853
1055
|
async function uploadImage(imgBlob, id, fieldName) {
|
|
854
1056
|
const file = new File([imgBlob], `generated_${fieldName}_${id}.${imgBlob.type.split('/').pop()}`, { type: imgBlob.type });
|
|
855
1057
|
const { name, size, type } = file;
|
|
@@ -921,7 +1123,7 @@ async function uploadImage(imgBlob, id, fieldName) {
|
|
|
921
1123
|
}
|
|
922
1124
|
}
|
|
923
1125
|
|
|
924
|
-
function regenerateImages(
|
|
1126
|
+
function regenerateImages({ recordId }: { recordId: any }) {
|
|
925
1127
|
if (coreStore.isInternetError) {
|
|
926
1128
|
adminforth.alert({
|
|
927
1129
|
message: t('Cannot regenerate images while internet connection is lost. Please check your connection and try again.'),
|
|
@@ -930,13 +1132,15 @@ function regenerateImages(recordInfo: any) {
|
|
|
930
1132
|
});
|
|
931
1133
|
return;
|
|
932
1134
|
}
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
1135
|
+
const record = recordsById.get(String(recordId));
|
|
1136
|
+
if (!record) {
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
record.aiStatus.generatedImages = false;
|
|
1140
|
+
touchRecords();
|
|
1141
|
+
runActionForRecord(record, 'generate_images').catch(() => {
|
|
1142
|
+
record.aiStatus.generatedImages = true;
|
|
1143
|
+
touchRecords();
|
|
940
1144
|
});
|
|
941
1145
|
}
|
|
942
1146
|
|
|
@@ -1078,10 +1282,15 @@ async function regenerateCell(recordInfo: any) {
|
|
|
1078
1282
|
});
|
|
1079
1283
|
return;
|
|
1080
1284
|
}
|
|
1081
|
-
|
|
1082
|
-
|
|
1285
|
+
const record = recordsById.get(String(recordInfo.recordId));
|
|
1286
|
+
if (!record) {
|
|
1287
|
+
return;
|
|
1083
1288
|
}
|
|
1084
|
-
regeneratingFieldsStatus
|
|
1289
|
+
if (!record.regeneratingFieldsStatus[recordInfo.fieldName]) {
|
|
1290
|
+
record.regeneratingFieldsStatus[recordInfo.fieldName] = false;
|
|
1291
|
+
}
|
|
1292
|
+
record.regeneratingFieldsStatus[recordInfo.fieldName] = true;
|
|
1293
|
+
touchRecords();
|
|
1085
1294
|
const actionType = props.meta.outputFieldsForAnalizeFromImages?.includes(recordInfo.fieldName)
|
|
1086
1295
|
? 'analyze'
|
|
1087
1296
|
: props.meta.outputPlainFields?.includes(recordInfo.fieldName)
|
|
@@ -1089,16 +1298,18 @@ async function regenerateCell(recordInfo: any) {
|
|
|
1089
1298
|
: null;
|
|
1090
1299
|
if (!actionType) {
|
|
1091
1300
|
console.error(`Field ${recordInfo.fieldName} is not configured for analysis.`);
|
|
1301
|
+
record.regeneratingFieldsStatus[recordInfo.fieldName] = false;
|
|
1302
|
+
touchRecords();
|
|
1092
1303
|
return;
|
|
1093
1304
|
}
|
|
1094
1305
|
|
|
1095
1306
|
let generationPromptsForField = {};
|
|
1096
1307
|
if (actionType === 'analyze') {
|
|
1097
1308
|
generationPromptsForField = generationPrompts.value.imageFieldsPrompts || {};
|
|
1098
|
-
|
|
1309
|
+
record.aiStatus.analyzedImages = false;
|
|
1099
1310
|
} else if (actionType === 'analyze_no_images') {
|
|
1100
1311
|
generationPromptsForField = generationPrompts.value.plainFieldsPrompts || {};
|
|
1101
|
-
|
|
1312
|
+
record.aiStatus.analyzedNoImages = false;
|
|
1102
1313
|
}
|
|
1103
1314
|
|
|
1104
1315
|
let jobId;
|
|
@@ -1117,7 +1328,7 @@ async function regenerateCell(recordInfo: any) {
|
|
|
1117
1328
|
silentError: true,
|
|
1118
1329
|
});
|
|
1119
1330
|
} catch (e) {
|
|
1120
|
-
regeneratingFieldsStatus
|
|
1331
|
+
record.regeneratingFieldsStatus[recordInfo.fieldName] = false;
|
|
1121
1332
|
console.error(`Error during cell regeneration for record ${recordInfo.recordId}, field ${recordInfo.fieldName}:`, e);
|
|
1122
1333
|
}
|
|
1123
1334
|
if ( res.ok === false) {
|
|
@@ -1127,7 +1338,7 @@ async function regenerateCell(recordInfo: any) {
|
|
|
1127
1338
|
});
|
|
1128
1339
|
isError.value = true;
|
|
1129
1340
|
errorMessage.value = t(`Failed to regenerate field`);
|
|
1130
|
-
regeneratingFieldsStatus
|
|
1341
|
+
record.regeneratingFieldsStatus[recordInfo.fieldName] = false;
|
|
1131
1342
|
return;
|
|
1132
1343
|
}
|
|
1133
1344
|
jobId = res.jobId;
|
|
@@ -1152,38 +1363,29 @@ async function regenerateCell(recordInfo: any) {
|
|
|
1152
1363
|
timeout: 'unlimited',
|
|
1153
1364
|
});
|
|
1154
1365
|
if (actionType === 'analyze') {
|
|
1155
|
-
imageToTextErrorMessages
|
|
1156
|
-
|
|
1366
|
+
record.imageToTextErrorMessages[recordInfo.fieldName] = res.job?.error || 'Unknown error';
|
|
1367
|
+
record.aiStatus.analyzedImages = true;
|
|
1157
1368
|
} else if (actionType === 'analyze_no_images') {
|
|
1158
|
-
textToTextErrorMessages
|
|
1159
|
-
|
|
1369
|
+
record.textToTextErrorMessages[recordInfo.fieldName] = res.job?.error || 'Unknown error';
|
|
1370
|
+
record.aiStatus.analyzedNoImages = true;
|
|
1160
1371
|
}
|
|
1161
|
-
regeneratingFieldsStatus
|
|
1372
|
+
record.regeneratingFieldsStatus[recordInfo.fieldName] = false;
|
|
1373
|
+
touchRecords();
|
|
1162
1374
|
return;
|
|
1163
1375
|
} else if (res.job?.status === 'completed') {
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
selected.value[index] = {
|
|
1169
|
-
...selected.value[index],
|
|
1170
|
-
...res.job.result,
|
|
1171
|
-
isChecked: true,
|
|
1172
|
-
[primaryKey]: pk,
|
|
1173
|
-
};
|
|
1174
|
-
}
|
|
1376
|
+
record.data = {
|
|
1377
|
+
...record.data,
|
|
1378
|
+
...res.job.result,
|
|
1379
|
+
};
|
|
1175
1380
|
if (actionType === 'analyze') {
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
}
|
|
1179
|
-
isAnalizingFields.value = false;
|
|
1381
|
+
record.imageToTextErrorMessages[recordInfo.fieldName] = '';
|
|
1382
|
+
record.aiStatus.analyzedImages = true;
|
|
1180
1383
|
} else if (actionType === 'analyze_no_images') {
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
}
|
|
1184
|
-
isAnalizingImages.value = false;
|
|
1384
|
+
record.textToTextErrorMessages[recordInfo.fieldName] = '';
|
|
1385
|
+
record.aiStatus.analyzedNoImages = true;
|
|
1185
1386
|
}
|
|
1186
|
-
regeneratingFieldsStatus
|
|
1387
|
+
record.regeneratingFieldsStatus[recordInfo.fieldName] = false;
|
|
1388
|
+
touchRecords();
|
|
1187
1389
|
}
|
|
1188
1390
|
}
|
|
1189
1391
|
|