@adminforth/bulk-ai-flow 1.25.2 → 1.26.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,24 +1,32 @@
1
1
  <template>
2
2
  <div class="flex items-end justify-start gap-2 cursor-pointer">
3
- <div class="flex items-center justify-center text-white bg-gradient-to-r h-[18px] 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 font-medium rounded-md text-sm px-1 text-center">
3
+ <div class="flex items-center justify-center text-white bg-gradient-to-r h-[18px] from-lightPrimary via-lightPrimary/90 to-lightPrimary/80 hover:opacity-90 focus:ring-4 focus:outline-none focus:ring-lightPrimary/30 dark:focus:ring-darkPrimary/30 font-medium rounded-md text-sm px-1 text-center dark:bg-none dark:bg-darkPrimary/10 dark:text-darkPrimary dark:brightness-200">
4
4
  {{t('AI')}}
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
8
+ <Dialog
9
9
  ref="confirmDialog"
10
- header="Bulk AI Generation"
11
- class="[scrollbar-gutter:stable] !max-w-full w-fit h-fit"
10
+ :header="t('Bulk AI Generation')"
11
+ class="[scrollbar-gutter:stable] max-w-full h-fit"
12
+ :beforeCancelFunction="handleBeforeCancel"
12
13
  :class="popupMode === 'generation' ? 'lg:w-auto !lg:max-w-[1600px]'
13
14
  : popupMode === 'settings' ? 'lg:w-[1000px] !lg:max-w-[1000px]'
14
15
  : 'lg:w-[500px] !lg:max-w-[500px]'"
15
- :beforeCloseFunction="handleBeforeClose"
16
- :closable="false"
16
+ :closable="true"
17
17
  :buttons="popupMode === 'generation' ? generationModeButtons : popupMode === 'settings' ? [
18
+ {
19
+ label: t('Cancel'),
20
+ options: {
21
+ class: 'afcl-button w-1/6',
22
+ mode: 'secondary'
23
+ },
24
+ onclick: () => { popupMode = 'confirmation'; }
25
+ },
18
26
  {
19
27
  label: t('Save settings'),
20
28
  options: {
21
- class: 'w-fit'
29
+ class: 'afcl-button w-1/6',
22
30
  },
23
31
  onclick: (dialog) => { saveSettings(); }
24
32
  },
@@ -27,29 +35,36 @@
27
35
  {
28
36
  label: t('Cancel'),
29
37
  options: {
30
- class: 'w-2/5 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 dark:!border-gray-600'
38
+ class: 'afcl-button w-1/2',
39
+ mode: 'secondary'
31
40
  },
32
41
  onclick: (dialog) => confirmDialog.tryToHideModal()
33
42
  },
34
43
  {
35
44
  label: t('Start generation'),
36
45
  options: {
37
- class: 'w-3/5 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 border-none',
46
+ class: 'afcl-button w-1/2',
38
47
  loader: isCheckingRateLimits
39
48
  },
40
- onclick: (dialog) => { runAiActions(); }
49
+ onclick: async () => { await runAiActions(); }
41
50
  }
42
51
  ]"
43
52
  :click-to-close-outside="false"
44
53
  >
45
- <div class="bulk-vision-table flex flex-col items-center gap-3 md:gap-4 overflow-y-auto">
54
+ <div
55
+ class="bulk-vision-table flex flex-col gap-3 w-full"
56
+ :class="{
57
+ 'h-[830px]': popupMode === 'generation',
58
+ 'h-auto max-h-[720px]': popupMode !== 'generation'
59
+ }"
60
+ >
46
61
  <template v-if="recordsList.length && popupMode === 'generation'" >
47
- <div class="w-full">
48
- <p :class="isGenerationPaused ? '' : 'opacity-0'" class="text-sm font-semibold text-yellow-800">{{ t(`Generated ${startedRecordCount} records. `) + t('Generation on pause. Resume generation?') }}</p>
62
+ <div class="w-full min-w-[980px]">
49
63
  <div v-if="isGenerationPaused" class="flex flex-col gap-2 mb-2">
64
+ <p class="text-sm font-semibold text-yellow-800">{{ t(`Generated ${startedRecordCount} records. `) + t('Generation on pause. Resume generation?') }}</p>
50
65
  <div class="flex items-center gap-2">
51
66
  <button
52
- class="h-8 px-3 py-1.5 text-sm rounded-md bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 text-white hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-purple-300 dark:focus:ring-purple-800 border-none ml-2"
67
+ class="h-8 px-3 py-1.5 text-sm rounded-md bg-gradient-to-r from-lightPrimary to-lightPrimary/80 text-white border-none ml-2"
53
68
  @click="resumeGeneration"
54
69
  >
55
70
  {{ t('Resume generation') }}
@@ -62,36 +77,59 @@
62
77
  </button>
63
78
  </div>
64
79
  </div>
65
- <div
66
- v-if="!isGenerationPaused"
67
- class="w-full h-8 rounded-md bg-gray-200 dark:bg-gray-700 overflow-hidden relative"
68
- :class="isGenerationPaused ? 'opacity-80' : ''"
69
- role="progressbar"
70
- :aria-valuenow="displayedProcessedCount"
71
- :aria-valuemin="0"
72
- :aria-valuemax="totalRecords"
73
- >
74
- <div
75
- class="h-full bg-gradient-to-r from-lightPrimary/70 via-lightPrimary/80 to-lightPrimary/90 dark:from-darkPrimary/70 dark:via-darkPrimary/80 dark:to-darkPrimary/90 transition-all duration-200 "
76
- :style="{ width: `${displayedProgressPercent}%` }"
77
- ></div>
78
- <div class="absolute inset-0 flex items-center justify-center text-sm font-medium text-white drop-shadow">
79
- <template v-if="isProcessingAny || isGenerationPaused">
80
- {{ Math.floor((displayedProcessedCount / totalRecords) * 100) }}%
81
- </template>
82
- <template v-else-if="isGenerationCancelled">
83
- {{ t('Generation cancelled') }}
84
- </template>
85
- <template v-else>
86
- {{ t('Processed') }}
87
- </template>
80
+
81
+ <div class="generation-progress-panel w-full bg-white dark:bg-gray-900 border border-gray-200/80 dark:border-gray-800 p-5 m-1 rounded-2xl shadow-sm flex flex-col gap-4">
82
+ <div class="generation-status-header flex items-center justify-between">
83
+ <div class="flex items-center gap-3">
84
+ <div
85
+ class="p-2 rounded-xl shrink-0"
86
+ :class="generationStatusClass"
87
+ >
88
+ <IconShieldCheckOutline v-if="displayedProcessedCount === totalRecords" class="w-5 h-5" />
89
+ <IconRefreshOutline v-else :class="isProcessingAny ? 'animate-spin' : ''" class="w-5 h-5" />
90
+ </div>
91
+
92
+ <div>
93
+ <h4 class="text-sm font-bold text-gray-800 dark:text-gray-200 tracking-tight">
94
+ {{ displayedProcessedCount }} {{ t('of') }} {{ totalRecords }} {{ t('fields ready to save') }}
95
+ </h4>
96
+ </div>
97
+ </div>
98
+
99
+ <span
100
+ class="px-2.5 py-1 text-xs rounded-default border flex items-center gap-1.5"
101
+ :class="generationStatusClass"
102
+ >
103
+ <span
104
+ class="w-1.5 h-1.5 rounded-full"
105
+ :class="isGenerationPaused ? 'bg-yellow-500' : displayedProcessedCount === totalRecords ? 'bg-green-500' : 'bg-blue-500'"
106
+ ></span>
107
+ {{
108
+ isGenerationPaused ? t('On pause')
109
+ : displayedProcessedCount === totalRecords ? t('Ready')
110
+ : t('Processing')
111
+ }}
112
+ </span>
88
113
  </div>
114
+
115
+ <ProgressBar
116
+ class="progress-bar w-full"
117
+ :class="isGenerationPaused ? 'opacity-80' : ''"
118
+ :current-value="Math.floor((displayedProcessedCount / totalRecords) * 100)"
119
+ :max-value="100"
120
+ :min-value="0"
121
+ :showAnimation="isProcessingAny && !isGenerationPaused"
122
+ :showLabels="false"
123
+ :showValues="false"
124
+ :show-progress="false"
125
+ />
126
+
89
127
  </div>
90
128
  </div>
91
129
 
92
130
 
93
131
  <VisionTable
94
- class="md:max-h-[75vh] max-w-[1560px] w-full h-full"
132
+ class="w-full flex-1 min-h-0 overflow-y-auto [scrollbar-gutter:stable] [mask-image:linear-gradient(to_bottom,transparent_0px,black_20px)]"
95
133
  ref="tableRef"
96
134
  :records="recordsList"
97
135
  :meta="props.meta"
@@ -105,62 +143,167 @@
105
143
  :outputImageFields="props.meta.outputImageFields"
106
144
  :outputFieldsForAnalizeFromImages="props.meta.outputFieldsForAnalizeFromImages"
107
145
  :outputPlainFields="props.meta.outputPlainFields"
146
+ :overwriteExistingValues="overwriteExistingValues"
108
147
  @error="handleTableError"
109
148
  @regenerate-images="regenerateImages"
110
- @regenerate-cell="regenerateCell"
149
+ @regenerate-record="regenerateRecord"
111
150
  />
112
- <div class="text-red-600 flex items-center w-full">
151
+ <div class="mt-2 flex items-center text-lightPrimary dark:text-darkPrimary bg-lightPrimary/10 dark:bg-darkPrimary/10 px-4 py-3 rounded-default border border-lightPrimary/20 dark:border-darkPrimary/20 text-sm">
152
+ <IconInfoCircleSolid class="w-5 h-5 me-2 shrink-0 text-lightPrimary dark:text-darkPrimary dark:brightness-200" />
153
+ <p class="text-lightPrimary dark:text-darkPrimary dark:brightness-200">
154
+ <span class="font-semibold text-lightPrimary dark:text-darkPrimary dark:brightness-200">{{ t('Only checked fields will be saved.') }}</span>
155
+ {{ t('You can regenerate fields before saving if needed.') }}
156
+ </p>
157
+ </div>
158
+ <div class="af-error-message flex items-center w-full text-red-600">
113
159
  <p v-if="isError === true">{{ errorMessage }}</p>
114
160
  </div>
115
161
  </template>
116
162
  <template v-else-if="!recordsList.length && popupMode === 'generation'">
117
- <p>No data to save. Feel free to click "Cancel"</p>
163
+ <p>{{ t('No data to save. Feel free to click "Cancel"') }}</p>
118
164
  </template>
119
- <div
120
- v-else-if="popupMode === 'settings'"
121
- v-for="(promptsCategory, key) in generationPrompts"
122
- :key="key"
123
- class="w-full"
124
- >
125
- <div v-if="Object.keys(promptsCategory).length > 0" class="gap-4 mb-6 ml-1">
126
- <p class="text-start w-full text-xl font-bold mb-2">{{
127
- key === "plainFieldsPrompts" ? "Prompts for non-image fields"
128
- : key === "generateImages" ? "Prompts for image fields"
129
- : "Prompts for image analysis"
130
- }}</p>
131
- <div class="grid grid-cols-2 gap-4">
132
- <div v-for="(prompt, promptKey) in promptsCategory" :key="promptKey">
133
- {{ formatLabel(promptKey) }} {{ t('prompt') }}:
134
- <Textarea
135
- v-model="generationPrompts[key][promptKey]"
136
- class="w-full h-64 p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"
137
- ></Textarea>
138
- <p class="text-red-500 hover:underline hover:cursor-pointer mt-2" @click="resetPromptToDefault(key, promptKey)">reset to default</p>
165
+ <div v-else-if="popupMode === 'settings'" class="af-settings-panel w-full flex flex-col gap-6 overflow-y-auto overflow-x-hidden pr-2 max-h-[70vh]">
166
+ <template v-for="(promptsCategory, key) in generationPrompts" :key="key">
167
+ <div v-if="Object.keys(promptsCategory).length > 0" class="w-full flex flex-col">
168
+ <div class="flex items-start gap-3.5 mb-6">
169
+
170
+ <div class="flex-1 min-w-0">
171
+ <h3 class="text-base font-bold text-gray-900 dark:text-white tracking-tight">
172
+ {{
173
+ key === "plainFieldsPrompts" ? t("Prompts for non-image fields")
174
+ : key === "generateImages" ? t("Prompts for image fields")
175
+ : t("Prompts for image analysis")
176
+ }}
177
+ </h3>
178
+ <p class="text-xs text-gray-400 mt-0.5">
179
+ {{
180
+ key === "plainFieldsPrompts" ? t("Define how AI should generate content for text-based fields.")
181
+ : key === "generateImages" ? t("Configure prompt rules for generating media and images.")
182
+ : t("Instructions for extracting text specifications from images.")
183
+ }}
184
+ </p>
185
+ </div>
186
+ </div>
187
+
188
+ <div class="grid gap-6" :class="Object.keys(promptsCategory).length === 1 ? 'grid-cols-1' : 'grid-cols-1 md:grid-cols-2'">
189
+ <div
190
+ v-for="(prompt, promptKey) in promptsCategory"
191
+ :key="promptKey"
192
+ class="
193
+ flex flex-col bg-white dark:bg-gray-900 border border-gray-200/80 p-5 rounded-default shadow-sm transition-all duration-200
194
+ hover:shadow-md hover:border-gray-300
195
+ dark:border-gray-800
196
+ dark:hover:border-gray-700"
197
+ >
198
+ <div class="flex items-center justify-between mb-4">
199
+ <div class="flex items-center gap-3">
200
+ <div class="p-2 bg-lightPrimary/10 text-lightPrimary rounded-lg shrink-0
201
+ dark:bg-darkPrimary/20
202
+ dark:text-darkPrimary ">
203
+ <IconImageSolid
204
+ v-if="key === 'generateImages'"
205
+ class="w-5 h-5 dark:brightness-200"
206
+ />
207
+ <IconFileLinesOutline
208
+ v-else
209
+ class="w-5 h-5 dark:brightness-200"
210
+ />
211
+ </div>
212
+ <div>
213
+ <h4 class="text-sm font-bold text-gray-800 dark:text-gray-200 capitalize tracking-tight">
214
+ {{ formatLabel(promptKey) }}
215
+ </h4>
216
+ <p class="text-[11px] text-gray-400 dark:text-gray-500">
217
+ {{ key === 'generateImages' ? t('Generate source media') : t('Generate a detailed content') }}
218
+ </p>
219
+ </div>
220
+ </div>
221
+
222
+ <span class="px-2 py-0.5 text-[10px] font-semibold tracking-wide rounded-md bg-lightPrimary/10 text-lightPrimary dark:bg-darkPrimary/10 dark:text-darkPrimary dark:brightness-200">
223
+ {{ key === 'generateImages' ? t('Image field') : t('Text field') }}
224
+ </span>
225
+ </div>
226
+
227
+ <div class="relative flex-1">
228
+ <Textarea
229
+ v-model="generationPrompts[key][promptKey]"
230
+ class="w-full h-56 p-3.5 text-sm leading-relaxed border border-gray-200 bg-gray-50/30 text-gray-800 shadow-sm resize-none font-sans
231
+ dark:border-gray-700
232
+ dark:bg-gray-800/40
233
+ dark:text-gray-100 rounded-xl
234
+ dark:focus:border-gray-700
235
+ focus:outline-none
236
+ focus:border-gray-200"
237
+ :placeholder="t('Enter prompt instructions...')"
238
+ ></Textarea>
239
+ </div>
240
+
241
+ <button
242
+ type="button"
243
+ mode="secondary"
244
+ @click="resetPromptToDefault(key, promptKey)"
245
+ class="
246
+ mt-4 flex items-center gap-1.5 text-xs font-semibold bg-transparent px-3 py-1 w-fit rounded-default
247
+ border border-red-200 hover:border-red-600
248
+ text-red-600 hover:text-red-700
249
+ dark:border-red-900/50 dark:hover:border-red-400
250
+ dark:text-red-500 dark:hover:text-red-400
251
+ transition-colors duration-150"
252
+ >
253
+ <IconUndoOutline class="w-3.5 h-3.5" />
254
+ {{ t('Reset') }}
255
+ </button>
256
+
139
257
  </div>
140
258
  </div>
141
259
  </div>
260
+ </template>
142
261
  </div>
143
- <div v-else class="flex flex-col gap-2 mt-2 w-full h-full">
144
- <div class="flex items-center justify-between mb-2 w-full">
145
- <div class="flex items-center justify-center gap-2">
146
- <IconShieldSolid class="w-6 h-6 text-lightPrimary dark:text-darkPrimary" />
147
- <p class="sm:text-base text-sm">{{ t('Overwrite existing values') }}</p>
148
- <Tooltip>
149
- <IconInfoCircleSolid class="w-5 h-5 me-2 text-lightPrimary dark:text-darkPrimary"/>
150
- <template #tooltip>
151
- <p class="max-w-64">{{ t('When enabled, the AI will overwrite content for fields that already have data. When off - this helps to preserve existing information and avoid overwriting valuable content.') }}</p>
152
- </template>
153
- </Tooltip>
262
+ <div v-else class="af-confirmation-panel flex flex-col gap-4 w-full h-full">
263
+ <div class="af-overwrite-settings-card flex flex-col p-5 rounded-default border border-gray-100 bg-white shadow-sm dark:border-gray-800 dark:bg-gray-900/40">
264
+ <div class="flex items-start justify-between gap-4">
265
+ <div class="flex items-start gap-3 min-w-0">
266
+ <div class="p-2 bg-lightPrimary/10 dark:bg-darkPrimary/10 rounded-xl text-lightPrimary dark:text-darkPrimary shrink-0 mt-0.5">
267
+ <IconShieldCheckOutline class="w-5 h-5 dark:brightness-200" />
268
+ </div>
269
+ <div class="min-w-0">
270
+ <p class="text-base font-semibold text-gray-900 dark:text-white leading-tight">{{ t('Overwrite existing values') }}</p>
271
+ <p class="text-sm text-gray-500 dark:text-gray-400 mt-1 break-words pr-2">{{ t('AI-generated data will replace existing values.') }}</p>
272
+ </div>
273
+ </div>
274
+ <div class="shrink-0 pt-1">
275
+ <Toggle v-model="overwriteExistingValues" />
276
+ </div>
277
+ </div>
278
+
279
+ <div v-if="overwriteExistingValues" class="mt-4 flex items-start text-amber-700 dark:text-amber-400 bg-amber-50 dark:bg-amber-950/30 px-3 py-2.5 rounded-xl border border-amber-200 dark:border-amber-900/50 text-sm">
280
+ <IconInfoCircleSolid class="w-4 h-4 me-2 shrink-0 mt-0.5 " />
281
+ <p>{{ t('Warning: Existing values will be overwritten.') }}</p>
154
282
  </div>
155
- <Toggle
156
- v-model="overwriteExistingValues"
157
- />
158
283
  </div>
159
- <div :class="overwriteExistingValues === true ? 'opacity-100' : 'opacity-0'" class="flex items-center text-yellow-800 bg-yellow-100 p-2 rounded-md border border-yellow-300">
160
- <IconExclamationTriangle class="w-6 h-6 me-2"/>
161
- <p class="sm:text-base text-sm">{{ t('Warning: Existing values will be overwritten.') }}</p>
284
+
285
+ <div
286
+ @click="clickSettingsButton()"
287
+ class="af-configure-prompts-card
288
+ flex items-center justify-between p-5 rounded-default border border-gray-100 bg-white shadow-sm cursor-pointer transition-all duration-150
289
+ dark:border-gray-800 dark:bg-gray-900/40 "
290
+ >
291
+ <div class="flex items-start gap-3 min-w-0">
292
+ <div class="p-2 bg-lightPrimary/10 dark:bg-darkPrimary/10 rounded-xl text-lightPrimary dark:text-darkPrimary shrink-0 mt-0.5">
293
+ <IconMessageCaptionOutline class="w-5 h-5 dark:brightness-200" />
294
+ </div>
295
+ <div class="min-w-0">
296
+ <p class="text-base font-semibold text-gray-900 dark:text-white leading-tight">{{ t('Configure prompts')}}</p>
297
+ <p class="text-sm text-gray-500 dark:text-gray-400 mt-1 break-words">{{ t('Change the instructions for AI to generate actual data.')}}</p>
298
+ </div>
299
+ </div>
300
+ </div>
301
+
302
+ <div class="flex items-center gap-2 px-1 text-xs font-medium text-gray-400 dark:text-gray-500 dark:brightness-200">
303
+ <IconInfoCircleSolid class="w-4 h-4 " />
304
+ <p>{{ t('This action will only apply to the fields included in your prompts.') }}</p>
162
305
  </div>
163
- <p class="w-fit flex justify-start text-lightPrimary dark:text-white hover:underline cursor-pointer" @click="clickSettingsButton()">{{ t('Configure prompts') }}</p>
306
+
164
307
  </div>
165
308
  </div>
166
309
  </Dialog>
@@ -176,9 +319,10 @@ import adminforth from '@/adminforth';
176
319
  import { useI18n } from 'vue-i18n';
177
320
  import { AdminUser, type AdminForthResourceCommon } from '@/types/Common';
178
321
  import { useCoreStore } from '@/stores/core';
179
- import { IconShieldSolid, IconInfoCircleSolid } from '@iconify-prerendered/vue-flowbite';
180
- import { IconExclamationTriangle } from '@iconify-prerendered/vue-humbleicons';
322
+ import { IconInfoCircleSolid } from '@iconify-prerendered/vue-flowbite';
181
323
  import { useFiltersStore } from '@/stores/filters';
324
+ import { IconMessageCaptionOutline, IconShieldCheckOutline, IconFileLinesOutline, IconUndoOutline, IconImageSolid, IconRefreshOutline } from '@iconify-prerendered/vue-flowbite';
325
+ import { ProgressBar } from '@/afcl';
182
326
 
183
327
 
184
328
  const coreStore = useCoreStore();
@@ -309,16 +453,18 @@ const generationModeButtons = computed(() => {
309
453
  label: checkedCount.value > 1 ? t('Save fields') : t('Save field'),
310
454
  options: {
311
455
  disabled: isLoading.value || checkedCount.value < 1 || isFetchingRecords.value || isProcessingAny.value || isGenerationPaused.value,
312
- loader: isLoading.value, class: 'w-fit'
456
+ loader: isLoading.value,
457
+ class: 'afcl-button w-1/6'
313
458
  },
314
- onclick: async (dialog) => { await saveData(); dialog.hide(); }
459
+ onclick: async (dialog) => { await saveData(); closeDialog(); dialog.hide(false); }
315
460
  },
316
461
  {
317
462
  label: t('Cancel'),
318
463
  options: {
319
- 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 dark:!border-gray-600'
464
+ class: 'afcl-button w-1/6',
465
+ mode: 'secondary'
320
466
  },
321
- onclick: async (dialog) => { await handleBeforeClose(dialog); }
467
+ onclick: async (dialog) => { await dialog.hide(true); }
322
468
  },
323
469
  ]
324
470
 
@@ -327,7 +473,8 @@ const generationModeButtons = computed(() => {
327
473
  label: t('Save processed'),
328
474
  options: {
329
475
  disabled: isLoading.value || isSavingCurrent.value || completedRecordIds.value.size < 1,
330
- loader: isSavingCurrent.value, class: 'w-fit'
476
+ loader: isSavingCurrent.value,
477
+ class: 'afcl-button w-1/6',
331
478
  },
332
479
  onclick: async () => { await saveCurrentGenerated(); }
333
480
  })
@@ -335,6 +482,35 @@ const generationModeButtons = computed(() => {
335
482
  return arrayToReturn;
336
483
  });
337
484
 
485
+ async function regenerateRecord({ recordId }) {
486
+
487
+ const promises = [];
488
+
489
+ if (props.meta.isImageGeneration) {
490
+ promises.push(regenerateImages({ recordId }));
491
+ }
492
+
493
+ for (const fieldName of props.meta.outputFieldsForAnalizeFromImages || []) {
494
+ promises.push(
495
+ regenerateCell({
496
+ recordId,
497
+ fieldName,
498
+ })
499
+ );
500
+ }
501
+
502
+ for (const fieldName of props.meta.outputPlainFields || []) {
503
+ promises.push(
504
+ regenerateCell({
505
+ recordId,
506
+ fieldName,
507
+ })
508
+ );
509
+ }
510
+
511
+ await Promise.allSettled(promises);
512
+ }
513
+
338
514
  const handleBeforeClose = async (dialog?: any) => {
339
515
  if (popupMode.value === 'generation') {
340
516
  const confirmed = await adminforth.confirm({
@@ -344,16 +520,16 @@ const handleBeforeClose = async (dialog?: any) => {
344
520
  no: t('Cancel'),
345
521
  });
346
522
 
347
- if (confirmed) {
348
- closeDialog();
349
-
350
- if (confirmDialog.value && typeof confirmDialog.value.hide === 'function') {
351
- confirmDialog.value.hide();
352
- } else if (dialog && typeof dialog.hide === 'function') {
353
- dialog.hide();
523
+ if (confirmed) {
524
+ closeDialog();
525
+
526
+ if (confirmDialog.value && typeof confirmDialog.value.hide === 'function') {
527
+ confirmDialog.value.hide();
528
+ } else if (dialog && typeof dialog.hide === 'function') {
529
+ dialog.hide();
530
+ }
531
+ return true;
354
532
  }
355
- return true;
356
- }
357
533
  return false;
358
534
  }
359
535
  }
@@ -384,6 +560,25 @@ const openDialog = async () => {
384
560
  }
385
561
  }
386
562
 
563
+ const handleBeforeCancel = async () => {
564
+ if (popupMode.value === 'generation') {
565
+ const confirmed = await adminforth.confirm({
566
+ title: t('Close without saving?'),
567
+ message: t('Are you sure you want to close without saving?'),
568
+ yes: t('Yes'),
569
+ no: t('Cancel'),
570
+ });
571
+
572
+ if (confirmed) {
573
+ closeDialog();
574
+ return true;
575
+ }
576
+ return false;
577
+ }
578
+ closeDialog();
579
+ return true;
580
+ };
581
+
387
582
  async function getListOfIds() {
388
583
  if ( props.meta.recordSelector === 'filtered' ) {
389
584
  const filters = filtersStore.getFilters(props.resource.resourceId);
@@ -490,7 +685,7 @@ function registerGenerationFailure(record: RecordState, actionType: GenerationAc
490
685
  }
491
686
 
492
687
  function showGenerationFailureSummary() {
493
- for (const group of generationFailureGroups.values()) {
688
+ for (const group of Array.from(generationFailureGroups.values())) {
494
689
  const failedCount = group.recordIds.size;
495
690
  const firstRecordId = Array.from(group.recordIds)[0];
496
691
  adminforth.alert({
@@ -754,7 +949,7 @@ async function checkRateLimits() {
754
949
  return true;
755
950
  }
756
951
 
757
- async function runActionForRecord(record: RecordState, actionType: GenerationAction) {
952
+ async function runActionForRecord(record: RecordState, actionType: GenerationAction, forceFilterFilledFields?: boolean) {
758
953
  if (!checkIfDialogOpen()) {
759
954
  return;
760
955
  }
@@ -786,7 +981,7 @@ async function runActionForRecord(record: RecordState, actionType: GenerationAct
786
981
  actionType,
787
982
  recordId: record.id,
788
983
  ...(customPrompt !== undefined ? { customPrompt: JSON.stringify(customPrompt) } : {}),
789
- filterFilledFields: !overwriteExistingValues.value,
984
+ filterFilledFields: forceFilterFilledFields !== undefined ? forceFilterFilledFields: !overwriteExistingValues.value,
790
985
  },
791
986
  silentError: true,
792
987
  });
@@ -874,17 +1069,17 @@ function applyJobResult(record: RecordState, job: any, actionType: GenerationAct
874
1069
  }
875
1070
 
876
1071
  function applyJobFailure(record: RecordState, job: any, actionType: GenerationAction) {
877
- registerGenerationFailure(record, actionType, job?.error || 'Unknown error');
1072
+ registerGenerationFailure(record, actionType, job?.error || t('Unknown error'));
878
1073
  if (actionType === 'generate_images') {
879
1074
  record.imageGenerationFailed = true;
880
- record.imageGenerationErrorMessage = job?.error || 'Unknown error';
1075
+ record.imageGenerationErrorMessage = job?.error || t('Unknown error');
881
1076
  } else if (actionType === 'analyze') {
882
1077
  for (const field of Object.keys(props.meta.outputFieldsForAnalizeFromImages || {})) {
883
- record.imageToTextErrorMessages[props.meta.outputFieldsForAnalizeFromImages[field]] = job?.error || 'Unknown error';
1078
+ record.imageToTextErrorMessages[props.meta.outputFieldsForAnalizeFromImages[field]] = job?.error || t('Unknown error');
884
1079
  }
885
1080
  } else if (actionType === 'analyze_no_images') {
886
1081
  for (const field of Object.keys(props.meta.outputPlainFields || {})) {
887
- record.textToTextErrorMessages[props.meta.outputPlainFields[field]] = job?.error || 'Unknown error';
1082
+ record.textToTextErrorMessages[props.meta.outputPlainFields[field]] = job?.error || t('Unknown error');
888
1083
  }
889
1084
  }
890
1085
  touchRecords();
@@ -1232,17 +1427,17 @@ async function uploadImage(imgBlob, id, fieldName) {
1232
1427
  } catch (error) {
1233
1428
  console.error('Error uploading file:', error);
1234
1429
  adminforth.alert({
1235
- message: 'Sorry but the file was not be uploaded. Please try again.',
1430
+ message: t('Sorry but the file was not be uploaded. Please try again.'),
1236
1431
  variant: 'danger'
1237
1432
  });
1238
1433
 
1239
1434
  isError.value = true;
1240
- errorMessage.value = `Failed to upload images. Please, try to re-run the action.`;
1435
+ errorMessage.value = t('Failed to upload images. Please, try to re-run the action.');
1241
1436
  return null;
1242
1437
  }
1243
1438
  }
1244
1439
 
1245
- function regenerateImages({ recordId }: { recordId: any }) {
1440
+ async function regenerateImages({ recordId }: { recordId: any }) {
1246
1441
  if (coreStore.isInternetError) {
1247
1442
  adminforth.alert({
1248
1443
  message: t('Cannot regenerate images while internet connection is lost. Please check your connection and try again.'),
@@ -1256,11 +1451,27 @@ function regenerateImages({ recordId }: { recordId: any }) {
1256
1451
  return;
1257
1452
  }
1258
1453
  record.aiStatus.generatedImages = false;
1454
+ record.status = 'processing';
1259
1455
  touchRecords();
1260
- runActionForRecord(record, 'generate_images').catch(() => {
1456
+
1457
+ try {
1458
+ await runActionForRecord(
1459
+ record,
1460
+ 'generate_images',
1461
+ false
1462
+ );
1463
+
1464
+ record.status = 'completed';
1465
+ completedRecordIds.value.add(String(recordId));
1466
+
1261
1467
  record.aiStatus.generatedImages = true;
1262
1468
  touchRecords();
1263
- });
1469
+
1470
+ } catch (e) {
1471
+ record.aiStatus.generatedImages = true;
1472
+ record.status = 'failed';
1473
+ touchRecords();
1474
+ }
1264
1475
  }
1265
1476
 
1266
1477
  async function findPreviewURLForImages() {
@@ -1326,10 +1537,14 @@ async function getGenerationPrompts() {
1326
1537
  const calculatedGenerationPrompts: any = {};
1327
1538
  const savedPrompts = localStorage.getItem(`bulkAiFlowGenerationPrompts_${props.meta.pluginInstanceId}`);
1328
1539
  if (props.meta.generationPrompts.plainFieldsPrompts) {
1329
- calculatedGenerationPrompts.plainFieldsPrompts = props.meta.generationPrompts.plainFieldsPrompts;
1540
+ calculatedGenerationPrompts.plainFieldsPrompts = {
1541
+ ...props.meta.generationPrompts.plainFieldsPrompts
1542
+ };
1330
1543
  }
1331
1544
  if (props.meta.generationPrompts.imageFieldsPrompts) {
1332
- calculatedGenerationPrompts.imageFieldsPrompts = props.meta.generationPrompts.imageFieldsPrompts;
1545
+ calculatedGenerationPrompts.imageFieldsPrompts = {
1546
+ ...props.meta.generationPrompts.imageFieldsPrompts
1547
+ };
1333
1548
  }
1334
1549
  if (props.meta.generationPrompts.imageGenerationPrompts) {
1335
1550
  let imageFields = {};
@@ -1596,4 +1811,16 @@ async function saveCurrentGenerated() {
1596
1811
  props.updateList();
1597
1812
  }
1598
1813
 
1814
+ const generationStatusClass = computed(() => {
1815
+ if (isGenerationPaused.value) {
1816
+ return 'bg-yellow-100 text-yellow-800 border-yellow-200 dark:bg-yellow-950/30 dark:text-yellow-400 dark:border-yellow-400'
1817
+ }
1818
+
1819
+ if (displayedProcessedCount.value === totalRecords.value) {
1820
+ return 'bg-green-100 text-green-800 border-green-200 dark:bg-green-950/30 dark:text-green-400 dark:border-green-400'
1821
+ }
1822
+
1823
+ return 'bg-blue-100 text-blue-800 border-blue-200 dark:bg-blue-950/30 dark:text-blue-400 dark:border-blue-400'
1824
+ })
1825
+
1599
1826
  </script>