@adminforth/bulk-ai-flow 2.0.3 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build.log +7 -6
- package/custom/GenerationSetup.vue +148 -0
- package/custom/ImageLightbox.vue +45 -0
- package/custom/RecordDetail.vue +276 -0
- package/custom/ReviewQueue.vue +253 -0
- package/custom/VisionAction.vue +266 -878
- package/custom/package-lock.json +1 -331
- package/custom/package.json +0 -3
- package/custom/pnpm-lock.yaml +0 -214
- package/custom/reviewModel.ts +124 -0
- package/dist/custom/GenerationSetup.vue +148 -0
- package/dist/custom/ImageLightbox.vue +45 -0
- package/dist/custom/RecordDetail.vue +276 -0
- package/dist/custom/ReviewQueue.vue +253 -0
- package/dist/custom/VisionAction.vue +266 -878
- package/dist/custom/package-lock.json +1 -331
- package/dist/custom/package.json +0 -3
- package/dist/custom/pnpm-lock.yaml +0 -214
- package/dist/custom/reviewModel.ts +124 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +34 -39
- package/dist/types.d.ts +2 -2
- package/index.ts +39 -46
- package/package.json +2 -2
- package/types.ts +2 -2
- package/custom/ImageCompare.vue +0 -185
- package/custom/ImageGenerationCarousel.vue +0 -327
- package/custom/Swiper.vue +0 -76
- package/custom/VisionTable.vue +0 -575
- package/dist/custom/ImageCompare.vue +0 -185
- package/dist/custom/ImageGenerationCarousel.vue +0 -327
- package/dist/custom/Swiper.vue +0 -76
- package/dist/custom/VisionTable.vue +0 -575
package/custom/VisionAction.vue
CHANGED
|
@@ -1,333 +1,85 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
<p class="text-justify max-h-[18px] truncate max-w-[60vw] md:max-w-none">{{ props.meta.actionName }}</p>
|
|
7
|
-
</div>
|
|
2
|
+
<span class="flex items-center gap-2">
|
|
3
|
+
<IconSparkles class="w-4 h-4 shrink-0 text-lightInputIconColor dark:text-darkInputIconColor" aria-hidden="true" />
|
|
4
|
+
<span class="truncate">{{ props.meta.actionName }}</span>
|
|
5
|
+
</span>
|
|
8
6
|
<Dialog
|
|
9
7
|
ref="confirmDialog"
|
|
10
|
-
:header="
|
|
11
|
-
class="
|
|
8
|
+
:header="props.meta.actionName"
|
|
9
|
+
class="w-11/12"
|
|
10
|
+
:class="popupMode === 'generation' ? 'max-w-6xl' : 'max-w-2xl'"
|
|
11
|
+
:buttons="popupMode === 'generation' ? generationModeButtons : confirmationModeButtons"
|
|
12
12
|
:beforeCancelFunction="handleBeforeCancel"
|
|
13
|
-
:
|
|
14
|
-
|
|
15
|
-
: 'lg:w-[500px] !lg:max-w-[500px]'"
|
|
16
|
-
:buttons="popupMode === 'generation' ? generationModeButtons : popupMode === 'settings' ? [
|
|
17
|
-
{
|
|
18
|
-
label: t('Cancel'),
|
|
19
|
-
options: {
|
|
20
|
-
class: 'afcl-button w-1/6',
|
|
21
|
-
variant: 'secondary'
|
|
22
|
-
},
|
|
23
|
-
onclick: () => { popupMode = 'confirmation'; }
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
label: t('Save settings'),
|
|
27
|
-
options: {
|
|
28
|
-
class: 'afcl-button w-1/6',
|
|
29
|
-
},
|
|
30
|
-
onclick: (dialog) => { saveSettings(); }
|
|
31
|
-
},
|
|
32
|
-
] :
|
|
33
|
-
[
|
|
34
|
-
{
|
|
35
|
-
label: t('Cancel'),
|
|
36
|
-
options: {
|
|
37
|
-
class: 'afcl-button w-1/2',
|
|
38
|
-
variant: 'secondary'
|
|
39
|
-
},
|
|
40
|
-
onclick: (dialog) => confirmDialog.tryToHideModal()
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
label: t('Start generation'),
|
|
44
|
-
options: {
|
|
45
|
-
class: 'afcl-button w-1/2',
|
|
46
|
-
loader: isCheckingRateLimits
|
|
47
|
-
},
|
|
48
|
-
onclick: async () => { await runAiActions(); }
|
|
49
|
-
}
|
|
50
|
-
]"
|
|
51
|
-
:close-by-click-outside="false"
|
|
13
|
+
:closeByClickOutside="false"
|
|
14
|
+
:closeByEsc="!zoomedImage"
|
|
52
15
|
>
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
:
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
:
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
<div class="flex items-center gap-2">
|
|
66
|
-
<button
|
|
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"
|
|
68
|
-
@click="resumeGeneration"
|
|
69
|
-
>
|
|
70
|
-
{{ t('Resume generation') }}
|
|
71
|
-
</button>
|
|
72
|
-
<button
|
|
73
|
-
class="h-8 px-3 py-1.5 text-sm rounded-md bg-white hover:bg-gray-100 text-gray-900 border border-gray-200 dark:bg-gray-800 dark:text-gray-100 dark:hover:bg-gray-700 dark:border-gray-600"
|
|
74
|
-
@click="cancelGeneration"
|
|
75
|
-
>
|
|
76
|
-
{{ t('Cancel generation') }}
|
|
77
|
-
</button>
|
|
78
|
-
</div>
|
|
79
|
-
</div>
|
|
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>
|
|
113
|
-
</div>
|
|
114
|
-
|
|
115
|
-
<ProgressBar
|
|
116
|
-
class="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
|
-
|
|
127
|
-
</div>
|
|
128
|
-
</div>
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
<VisionTable
|
|
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)]"
|
|
133
|
-
ref="tableRef"
|
|
16
|
+
<GenerationSetup
|
|
17
|
+
v-if="popupMode === 'confirmation'"
|
|
18
|
+
v-model:prompts="generationPrompts"
|
|
19
|
+
v-model:overwrite="overwriteExistingValues"
|
|
20
|
+
:meta="props.meta"
|
|
21
|
+
:defaultPrompts="defaultPrompts"
|
|
22
|
+
:savedPrompts="savedPrompts"
|
|
23
|
+
@save="savePrompt"
|
|
24
|
+
/>
|
|
25
|
+
<div v-else class="bulk-vision-table flex flex-col gap-3">
|
|
26
|
+
<template v-if="recordsList.length">
|
|
27
|
+
<ReviewQueue
|
|
134
28
|
:records="recordsList"
|
|
135
29
|
:meta="props.meta"
|
|
136
|
-
:
|
|
137
|
-
:
|
|
138
|
-
:
|
|
139
|
-
:
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
:overwriteExistingValues="overwriteExistingValues"
|
|
147
|
-
@error="handleTableError"
|
|
148
|
-
@regenerate-images="regenerateImages"
|
|
149
|
-
@regenerate-record="regenerateRecord"
|
|
30
|
+
:processed="displayedProcessedCount"
|
|
31
|
+
:total="totalRecords"
|
|
32
|
+
:isProcessing="isProcessingAny"
|
|
33
|
+
:isPaused="isGenerationPaused"
|
|
34
|
+
@regenerateRecord="regenerateRecord"
|
|
35
|
+
@regenerateImages="regenerateImages"
|
|
36
|
+
@regenerateCell="regenerateCell"
|
|
37
|
+
@zoom="(url) => zoomedImage = url"
|
|
38
|
+
@stop="cancelGeneration"
|
|
39
|
+
@resume="resumeGeneration"
|
|
150
40
|
/>
|
|
151
|
-
<
|
|
152
|
-
<
|
|
153
|
-
|
|
154
|
-
|
|
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">
|
|
159
|
-
<p v-if="isError === true">{{ errorMessage }}</p>
|
|
160
|
-
</div>
|
|
161
|
-
</template>
|
|
162
|
-
<template v-else-if="!recordsList.length && popupMode === 'generation'">
|
|
163
|
-
<p>{{ t('No data to save. Feel free to click "Cancel"') }}</p>
|
|
41
|
+
<p class="flex items-start gap-2 text-xs text-lightInputIconColor dark:text-darkInputIconColor">
|
|
42
|
+
<IconInfo class="mt-px w-3.5 h-3.5 shrink-0" aria-hidden="true" />
|
|
43
|
+
{{ t('Only checked records are saved. You can edit or regenerate values before saving.') }}
|
|
44
|
+
</p>
|
|
164
45
|
</template>
|
|
165
|
-
<
|
|
166
|
-
|
|
167
|
-
<
|
|
168
|
-
|
|
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
|
-
|
|
257
|
-
</div>
|
|
258
|
-
</div>
|
|
259
|
-
</div>
|
|
260
|
-
</template>
|
|
261
|
-
</div>
|
|
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>
|
|
282
|
-
</div>
|
|
283
|
-
</div>
|
|
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>
|
|
305
|
-
</div>
|
|
306
|
-
|
|
46
|
+
<Skeleton v-else-if="isFetchingRecords" type="input" class="h-[20rem] w-full rounded-lg" />
|
|
47
|
+
<div v-else class="flex flex-col items-center gap-2 rounded-lg border border-dashed border-lightFormBorder px-6 py-10 text-center dark:border-darkFormBorder">
|
|
48
|
+
<IconInbox class="w-6 h-6 text-lightInputPlaceholderText dark:text-darkInputPlaceholderText" aria-hidden="true" />
|
|
49
|
+
<p class="text-sm text-lightInputIconColor dark:text-darkInputIconColor">{{ t('No data to save. Feel free to click "Cancel"') }}</p>
|
|
307
50
|
</div>
|
|
51
|
+
|
|
52
|
+
<p v-if="isError" class="af-error-message flex items-start gap-1.5 text-sm text-lightDanger dark:text-darkDanger">
|
|
53
|
+
<IconCircleAlert class="mt-0.5 w-4 h-4 shrink-0" aria-hidden="true" />
|
|
54
|
+
{{ errorMessage }}
|
|
55
|
+
</p>
|
|
308
56
|
</div>
|
|
309
57
|
</Dialog>
|
|
58
|
+
<ImageLightbox v-model="zoomedImage" />
|
|
310
59
|
</template>
|
|
311
60
|
|
|
312
61
|
<script lang="ts" setup>
|
|
313
62
|
import { callAdminForthApi } from '@/utils';
|
|
314
|
-
import { ref,
|
|
63
|
+
import { ref, reactive, computed } from 'vue'
|
|
315
64
|
import pLimit from 'p-limit';
|
|
316
|
-
import { Dialog,
|
|
317
|
-
import
|
|
65
|
+
import { Dialog, Skeleton } from '@/afcl';
|
|
66
|
+
import { IconCircleAlert, IconInbox, IconInfo, IconSparkles } from '@iconify-prerendered/vue-lucide';
|
|
67
|
+
import GenerationSetup from './GenerationSetup.vue';
|
|
68
|
+
import ReviewQueue from './ReviewQueue.vue';
|
|
69
|
+
import ImageLightbox from './ImageLightbox.vue';
|
|
318
70
|
import adminforth from '@/adminforth';
|
|
319
71
|
import { useI18n } from 'vue-i18n';
|
|
320
72
|
import { AdminUser, type AdminForthResourceCommon } from '@/types/Common';
|
|
321
73
|
import { useCoreStore } from '@/stores/core';
|
|
322
|
-
import { IconInfoCircleSolid } from '@iconify-prerendered/vue-flowbite';
|
|
323
74
|
import { useFiltersStore } from '@/stores/filters';
|
|
324
|
-
import {
|
|
325
|
-
import { ProgressBar } from '@/afcl';
|
|
75
|
+
import { PROMPT_CATEGORY, isRegenerating, recordError, type GenerationAction, type PromptCategory, type RecordState } from './reviewModel';
|
|
326
76
|
|
|
77
|
+
const DURATION_RE = /\b\d+\.\d+s\b/g;
|
|
78
|
+
const RETRY_IN_RE = /Please retry in [\d.]+s\.?/g;
|
|
79
|
+
const UUID_RE = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi;
|
|
327
80
|
|
|
328
81
|
const coreStore = useCoreStore();
|
|
329
82
|
const filtersStore = useFiltersStore();
|
|
330
|
-
const showCloseConfirmModal = ref(false);
|
|
331
83
|
|
|
332
84
|
const { t } = useI18n();
|
|
333
85
|
const props = defineProps<{
|
|
@@ -339,37 +91,10 @@ const props = defineProps<{
|
|
|
339
91
|
clearCheckboxes?: () => any,
|
|
340
92
|
}>();
|
|
341
93
|
|
|
342
|
-
type
|
|
343
|
-
type GenerationAction = 'analyze' | 'analyze_no_images' | 'generate_images';
|
|
344
|
-
|
|
345
|
-
type RecordState = {
|
|
346
|
-
id: string | number;
|
|
347
|
-
status: RecordStatus;
|
|
348
|
-
isChecked: boolean;
|
|
349
|
-
label: string;
|
|
350
|
-
data: Record<string, any>;
|
|
351
|
-
oldData: Record<string, any>;
|
|
352
|
-
images: any[];
|
|
353
|
-
aiStatus: {
|
|
354
|
-
generatedImages: boolean;
|
|
355
|
-
analyzedImages: boolean;
|
|
356
|
-
analyzedNoImages: boolean;
|
|
357
|
-
};
|
|
358
|
-
openGenerationCarousel: Record<string, boolean>;
|
|
359
|
-
openImageCompare: Record<string, boolean>;
|
|
360
|
-
carouselSaveImages: Record<string, any[]>;
|
|
361
|
-
carouselImageIndex: Record<string, number>;
|
|
362
|
-
imageGenerationErrorMessage: string;
|
|
363
|
-
imageGenerationFailed: boolean;
|
|
364
|
-
imageToTextErrorMessages: Record<string, string>;
|
|
365
|
-
textToTextErrorMessages: Record<string, string>;
|
|
366
|
-
regeneratingFieldsStatus: Record<string, boolean>;
|
|
367
|
-
listOfImageNotGenerated: Record<string, any>;
|
|
368
|
-
};
|
|
94
|
+
type Prompts = Record<PromptCategory, Record<string, string>>;
|
|
369
95
|
|
|
370
96
|
const recordIds = ref<Array<string | number>>([]);
|
|
371
97
|
const recordsById = new Map<string, RecordState>();
|
|
372
|
-
const uncheckedRecordIds = new Set<string>();
|
|
373
98
|
|
|
374
99
|
defineExpose({
|
|
375
100
|
click
|
|
@@ -377,18 +102,24 @@ defineExpose({
|
|
|
377
102
|
|
|
378
103
|
const confirmDialog = ref<any>(null);
|
|
379
104
|
const primaryKey = props.meta.primaryKey;
|
|
105
|
+
const promptsStorageKey = `bulkAiFlowGenerationPrompts_${props.meta.pluginInstanceId}`;
|
|
106
|
+
const defaultPrompts: Prompts = {
|
|
107
|
+
imageFieldsPrompts: props.meta.generationPrompts.imageFieldsPrompts,
|
|
108
|
+
plainFieldsPrompts: props.meta.generationPrompts.plainFieldsPrompts,
|
|
109
|
+
generateImages: props.meta.generationPrompts.imageGenerationPrompts,
|
|
110
|
+
};
|
|
380
111
|
const isLoading = ref(false);
|
|
381
112
|
const isFetchingRecords = ref(false);
|
|
382
113
|
const isError = ref(false);
|
|
383
114
|
const errorMessage = ref('');
|
|
384
115
|
const isDialogOpen = ref(false);
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
const
|
|
388
|
-
const
|
|
116
|
+
const popupMode = ref<'generation' | 'confirmation'>('confirmation');
|
|
117
|
+
// prompts kept in this browser (the plugin config for the ones never saved) and the ones on the screen
|
|
118
|
+
const savedPrompts = ref<Prompts>(promptsWithSaved({}));
|
|
119
|
+
const generationPrompts = ref<Prompts>(promptsWithSaved({}));
|
|
389
120
|
const overwriteExistingValues = ref<boolean>(false);
|
|
121
|
+
const zoomedImage = ref<string | null>(null);
|
|
390
122
|
|
|
391
|
-
const checkedCount = computed(() => recordIds.value.length - uncheckedRecordIds.size);
|
|
392
123
|
const totalRecords = computed(() => recordIds.value.length);
|
|
393
124
|
const isGenerationPaused = ref(false);
|
|
394
125
|
const isGenerationCancelled = ref(false);
|
|
@@ -399,16 +130,18 @@ const pauseBreakpoints = computed(() => props.meta.askConfirmation || []);
|
|
|
399
130
|
const startedRecordCount = ref(0);
|
|
400
131
|
const isCheckingRateLimits = ref(false);
|
|
401
132
|
let startGate = Promise.resolve();
|
|
402
|
-
const tableRef = ref<any>(null);
|
|
403
133
|
const generationFailureGroups = new Map<string, {
|
|
404
134
|
actionType: GenerationAction;
|
|
405
135
|
error: string;
|
|
406
136
|
recordIds: Set<string>;
|
|
407
137
|
}>();
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
138
|
+
const recordsList = computed(() => {
|
|
139
|
+
const ids = isGenerationCancelled.value
|
|
140
|
+
? recordIds.value.filter(id => completedRecordIds.value.has(String(id)))
|
|
141
|
+
: recordIds.value;
|
|
142
|
+
return ids.map(id => getOrCreateRecord(id));
|
|
411
143
|
});
|
|
144
|
+
const processedCount = computed(() => recordsList.value.filter(record => record.status === 'completed' || record.status === 'failed').length);
|
|
412
145
|
const progressStep = computed(() => {
|
|
413
146
|
if (!totalRecords.value || totalRecords.value < 100) {
|
|
414
147
|
return 1;
|
|
@@ -425,64 +158,52 @@ const displayedProcessedCount = computed(() => {
|
|
|
425
158
|
}
|
|
426
159
|
return Math.floor(processedCount.value / step) * step;
|
|
427
160
|
});
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
recordsVersion.value;
|
|
444
|
-
const ids = isGenerationCancelled.value
|
|
445
|
-
? recordIds.value.filter(id => completedRecordIds.value.has(String(id)))
|
|
446
|
-
: recordIds.value;
|
|
447
|
-
return ids.map(id => getOrCreateRecord(id));
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
const generationModeButtons = computed(() => {
|
|
451
|
-
const arrayToReturn = [
|
|
452
|
-
{
|
|
453
|
-
label: checkedCount.value > 1 ? t('Save fields') : t('Save field'),
|
|
454
|
-
options: {
|
|
455
|
-
disabled: isLoading.value || checkedCount.value < 1 || isFetchingRecords.value || isProcessingAny.value || isGenerationPaused.value,
|
|
456
|
-
loader: isLoading.value,
|
|
457
|
-
class: 'afcl-button w-1/6'
|
|
458
|
-
},
|
|
459
|
-
onclick: async (dialog) => { await saveData(); closeDialog(); dialog.hide(false); }
|
|
161
|
+
const isProcessingAny = computed(() => recordsList.value.some(record => record.status === 'processing'));
|
|
162
|
+
const checkedCount = computed(() => recordsList.value.filter(record => record.isChecked).length);
|
|
163
|
+
const checkedProcessedCount = computed(() => recordsList.value.filter(record => record.isChecked && completedRecordIds.value.has(String(record.id))).length);
|
|
164
|
+
|
|
165
|
+
const confirmationModeButtons = computed(() => [
|
|
166
|
+
{
|
|
167
|
+
label: t('Cancel'),
|
|
168
|
+
options: { variant: 'secondary' },
|
|
169
|
+
onclick: (dialog) => dialog.hide(true),
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
label: t('Generate for {count} record | Generate for {count} records', totalRecords.value),
|
|
173
|
+
options: {
|
|
174
|
+
disabled: isFetchingRecords.value || isCheckingRateLimits.value || totalRecords.value < 1,
|
|
175
|
+
loader: isCheckingRateLimits.value,
|
|
460
176
|
},
|
|
461
|
-
{
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
177
|
+
onclick: async () => { await runAiActions(); },
|
|
178
|
+
},
|
|
179
|
+
]);
|
|
180
|
+
|
|
181
|
+
const generationModeButtons = computed(() => [
|
|
182
|
+
{
|
|
183
|
+
label: t('Cancel'),
|
|
184
|
+
options: { variant: 'secondary' },
|
|
185
|
+
onclick: (dialog) => dialog.hide(true),
|
|
186
|
+
},
|
|
187
|
+
...(isProcessingAny.value ? [{
|
|
188
|
+
label: t('Save processed ({count})', { count: checkedProcessedCount.value }),
|
|
189
|
+
options: {
|
|
190
|
+
variant: 'secondary',
|
|
191
|
+
disabled: isLoading.value || isSavingCurrent.value || checkedProcessedCount.value < 1,
|
|
192
|
+
loader: isSavingCurrent.value,
|
|
468
193
|
},
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
})
|
|
481
|
-
}
|
|
482
|
-
return arrayToReturn;
|
|
483
|
-
});
|
|
194
|
+
onclick: async () => { await saveCurrentGenerated(); },
|
|
195
|
+
}] : []),
|
|
196
|
+
{
|
|
197
|
+
label: t('Save {count} record | Save {count} records', checkedCount.value),
|
|
198
|
+
options: {
|
|
199
|
+
disabled: isLoading.value || checkedCount.value < 1 || isFetchingRecords.value || isProcessingAny.value || isGenerationPaused.value,
|
|
200
|
+
loader: isLoading.value,
|
|
201
|
+
},
|
|
202
|
+
onclick: async (dialog) => { await saveData(); closeDialog(); dialog.hide(false); },
|
|
203
|
+
},
|
|
204
|
+
]);
|
|
484
205
|
|
|
485
|
-
async function regenerateRecord({ recordId }: { recordId: string }) {
|
|
206
|
+
async function regenerateRecord({ recordId }: { recordId: string | number }) {
|
|
486
207
|
|
|
487
208
|
const promises = [];
|
|
488
209
|
|
|
@@ -490,16 +211,7 @@ async function regenerateRecord({ recordId }: { recordId: string }) {
|
|
|
490
211
|
promises.push(regenerateImages({ recordId }));
|
|
491
212
|
}
|
|
492
213
|
|
|
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 || []) {
|
|
214
|
+
for (const fieldName of [...props.meta.outputFieldsForAnalizeFromImages, ...props.meta.outputPlainFields]) {
|
|
503
215
|
promises.push(
|
|
504
216
|
regenerateCell({
|
|
505
217
|
recordId,
|
|
@@ -511,50 +223,19 @@ async function regenerateRecord({ recordId }: { recordId: string }) {
|
|
|
511
223
|
await Promise.allSettled(promises);
|
|
512
224
|
}
|
|
513
225
|
|
|
514
|
-
const handleBeforeClose = async (dialog?: any) => {
|
|
515
|
-
if (popupMode.value === 'generation') {
|
|
516
|
-
const confirmed = await adminforth.confirm({
|
|
517
|
-
title: t('Close without saving?'),
|
|
518
|
-
message: t('Are you sure you want to close without saving?'),
|
|
519
|
-
yes: t('Yes'),
|
|
520
|
-
no: t('Cancel'),
|
|
521
|
-
});
|
|
522
|
-
|
|
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;
|
|
532
|
-
}
|
|
533
|
-
return false;
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
|
|
537
226
|
const isSavingCurrent = ref(false);
|
|
538
227
|
function checkIfDialogOpen() {
|
|
539
228
|
return isDialogOpen.value === true;
|
|
540
229
|
}
|
|
541
230
|
|
|
542
231
|
const openDialog = async () => {
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
popupMode.value = 'confirmation';
|
|
546
|
-
} else {
|
|
547
|
-
popupMode.value = 'generation';
|
|
548
|
-
}
|
|
232
|
+
popupMode.value = props.meta.askConfirmationBeforeGenerating ? 'confirmation' : 'generation';
|
|
233
|
+
loadGenerationPrompts();
|
|
549
234
|
isDialogOpen.value = true;
|
|
550
235
|
confirmDialog.value.open();
|
|
551
236
|
isFetchingRecords.value = true;
|
|
552
237
|
await initializeGlobalState();
|
|
553
|
-
await findPreviewURLForImages();
|
|
554
238
|
isFetchingRecords.value = false;
|
|
555
|
-
if (!generationPrompts.value || Object.keys(generationPrompts.value).length === 0) {
|
|
556
|
-
await getGenerationPrompts();
|
|
557
|
-
}
|
|
558
239
|
if (!props.meta.askConfirmationBeforeGenerating) {
|
|
559
240
|
runAiActions();
|
|
560
241
|
}
|
|
@@ -608,14 +289,17 @@ async function getListOfIds() {
|
|
|
608
289
|
return props.checkboxes;
|
|
609
290
|
}
|
|
610
291
|
}
|
|
611
|
-
|
|
292
|
+
|
|
612
293
|
|
|
613
294
|
async function runAiActions() {
|
|
614
295
|
const { ok, sessionIds } = await checkRateLimits();
|
|
615
296
|
if (!ok || !sessionIds) {
|
|
297
|
+
closeDialog();
|
|
616
298
|
confirmDialog.value.close();
|
|
617
299
|
return;
|
|
618
300
|
}
|
|
301
|
+
// from here a page reload loses the generated values and the generation in progress
|
|
302
|
+
window.addEventListener('beforeunload', beforeUnloadHandler);
|
|
619
303
|
popupMode.value = 'generation';
|
|
620
304
|
isGenerationCancelled.value = false;
|
|
621
305
|
isGenerationPaused.value = false;
|
|
@@ -623,8 +307,6 @@ async function runAiActions() {
|
|
|
623
307
|
completedRecordIds.value = new Set();
|
|
624
308
|
startedRecordCount.value = 0;
|
|
625
309
|
generationFailureGroups.clear();
|
|
626
|
-
await nextTick();
|
|
627
|
-
tableRef.value?.refresh();
|
|
628
310
|
const limit = pLimit(props.meta.concurrencyLimit || 10);
|
|
629
311
|
const tasks = recordIds.value
|
|
630
312
|
.map(id => limit(() => processOneRecord(String(id), sessionIds)));
|
|
@@ -640,7 +322,6 @@ const closeDialog = () => {
|
|
|
640
322
|
errorMessage.value = '';
|
|
641
323
|
isDialogOpen.value = false;
|
|
642
324
|
popupMode.value = 'confirmation';
|
|
643
|
-
isDataSaved.value = false;
|
|
644
325
|
if (isGenerationPaused.value || isGenerationCancelled.value) {
|
|
645
326
|
resolvePause();
|
|
646
327
|
}
|
|
@@ -648,15 +329,13 @@ const closeDialog = () => {
|
|
|
648
329
|
|
|
649
330
|
async function initializeGlobalState() {
|
|
650
331
|
const ids = await getListOfIds();
|
|
651
|
-
recordIds.value = ids;
|
|
652
332
|
recordsById.clear();
|
|
653
|
-
|
|
333
|
+
recordIds.value = ids;
|
|
654
334
|
}
|
|
655
335
|
|
|
656
336
|
function resetGlobalState() {
|
|
657
337
|
recordIds.value = [];
|
|
658
338
|
recordsById.clear();
|
|
659
|
-
uncheckedRecordIds.clear();
|
|
660
339
|
generationFailureGroups.clear();
|
|
661
340
|
}
|
|
662
341
|
|
|
@@ -666,9 +345,9 @@ function getActionLabel(actionType: GenerationAction) {
|
|
|
666
345
|
|
|
667
346
|
function getGenerationFailureGroupKey(actionType: GenerationAction, error: string) {
|
|
668
347
|
const normalizedError = error
|
|
669
|
-
.replace(
|
|
670
|
-
.replace(
|
|
671
|
-
.replace(
|
|
348
|
+
.replace(RETRY_IN_RE, 'Please retry later.')
|
|
349
|
+
.replace(DURATION_RE, '<duration>')
|
|
350
|
+
.replace(UUID_RE, '<uuid>');
|
|
672
351
|
return `${actionType}:${normalizedError}`;
|
|
673
352
|
}
|
|
674
353
|
|
|
@@ -700,21 +379,17 @@ function showGenerationFailureSummary() {
|
|
|
700
379
|
}
|
|
701
380
|
}
|
|
702
381
|
|
|
382
|
+
// records are reactive, so the review rerenders on every result, edit and checkbox
|
|
703
383
|
function getOrCreateRecord(recordId: string | number): RecordState {
|
|
704
384
|
const key = String(recordId);
|
|
705
385
|
let record = recordsById.get(key);
|
|
706
386
|
if (!record) {
|
|
707
|
-
record = createEmptyRecord(recordId);
|
|
708
|
-
record.isChecked = !uncheckedRecordIds.has(key);
|
|
387
|
+
record = reactive(createEmptyRecord(recordId)) as RecordState;
|
|
709
388
|
recordsById.set(key, record);
|
|
710
389
|
}
|
|
711
390
|
return record;
|
|
712
391
|
}
|
|
713
392
|
|
|
714
|
-
function touchRecords() {
|
|
715
|
-
recordsVersion.value += 1;
|
|
716
|
-
}
|
|
717
|
-
|
|
718
393
|
function waitForResumeIfPaused() {
|
|
719
394
|
if (!isGenerationPaused.value) {
|
|
720
395
|
return Promise.resolve();
|
|
@@ -754,7 +429,7 @@ function resumeGeneration() {
|
|
|
754
429
|
resolvePause();
|
|
755
430
|
}
|
|
756
431
|
|
|
757
|
-
|
|
432
|
+
function cancelGeneration() {
|
|
758
433
|
if (isGenerationCancelled.value) {
|
|
759
434
|
return;
|
|
760
435
|
}
|
|
@@ -768,14 +443,6 @@ async function cancelGeneration() {
|
|
|
768
443
|
recordsById.delete(key);
|
|
769
444
|
}
|
|
770
445
|
}
|
|
771
|
-
for (const key of Array.from(uncheckedRecordIds)) {
|
|
772
|
-
if (!generatedIds.has(key)) {
|
|
773
|
-
uncheckedRecordIds.delete(key);
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
touchRecords();
|
|
777
|
-
await nextTick();
|
|
778
|
-
tableRef.value?.refresh();
|
|
779
446
|
}
|
|
780
447
|
|
|
781
448
|
async function withStartGate<T>(fn: () => Promise<T>) {
|
|
@@ -792,14 +459,6 @@ async function withStartGate<T>(fn: () => Promise<T>) {
|
|
|
792
459
|
}
|
|
793
460
|
}
|
|
794
461
|
|
|
795
|
-
function createImageFieldMap<T>(factory: () => T): Record<string, T> {
|
|
796
|
-
const result: Record<string, T> = {};
|
|
797
|
-
for (const field of props.meta.outputImageFields || []) {
|
|
798
|
-
result[field] = factory();
|
|
799
|
-
}
|
|
800
|
-
return result;
|
|
801
|
-
}
|
|
802
|
-
|
|
803
462
|
function createEmptyRecord(recordId: string | number): RecordState {
|
|
804
463
|
return {
|
|
805
464
|
id: recordId,
|
|
@@ -808,16 +467,13 @@ function createEmptyRecord(recordId: string | number): RecordState {
|
|
|
808
467
|
label: '',
|
|
809
468
|
data: {},
|
|
810
469
|
oldData: {},
|
|
470
|
+
oldImagePreviews: {},
|
|
811
471
|
images: [],
|
|
812
472
|
aiStatus: {
|
|
813
473
|
generatedImages: !props.meta.isImageGeneration,
|
|
814
474
|
analyzedImages: !props.meta.isFieldsForAnalizeFromImages,
|
|
815
475
|
analyzedNoImages: !props.meta.isFieldsForAnalizePlain,
|
|
816
476
|
},
|
|
817
|
-
openGenerationCarousel: createImageFieldMap(() => false),
|
|
818
|
-
openImageCompare: createImageFieldMap(() => false),
|
|
819
|
-
carouselSaveImages: createImageFieldMap(() => []),
|
|
820
|
-
carouselImageIndex: createImageFieldMap(() => 0),
|
|
821
477
|
imageGenerationErrorMessage: '',
|
|
822
478
|
imageGenerationFailed: false,
|
|
823
479
|
imageToTextErrorMessages: {},
|
|
@@ -827,6 +483,13 @@ function createEmptyRecord(recordId: string | number): RecordState {
|
|
|
827
483
|
};
|
|
828
484
|
}
|
|
829
485
|
|
|
486
|
+
// a regeneration can fix the last error of a record or add a new one
|
|
487
|
+
function settleRecordStatus(record: RecordState) {
|
|
488
|
+
if (!isRegenerating(record)) {
|
|
489
|
+
record.status = recordError(record) ? 'failed' : 'completed';
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
830
493
|
async function processOneRecord(recordId: string, sessionIds: string[]) {
|
|
831
494
|
if (!checkIfDialogOpen()) {
|
|
832
495
|
return;
|
|
@@ -855,11 +518,10 @@ async function processOneRecord(recordId: string, sessionIds: string[]) {
|
|
|
855
518
|
return;
|
|
856
519
|
}
|
|
857
520
|
const record = getOrCreateRecord(recordId);
|
|
858
|
-
if (!record
|
|
521
|
+
if (!record.isChecked) {
|
|
859
522
|
return;
|
|
860
523
|
}
|
|
861
524
|
record.status = 'processing';
|
|
862
|
-
touchRecords();
|
|
863
525
|
record.imageGenerationFailed = false;
|
|
864
526
|
record.imageGenerationErrorMessage = '';
|
|
865
527
|
record.imageToTextErrorMessages = {};
|
|
@@ -877,11 +539,11 @@ async function processOneRecord(recordId: string, sessionIds: string[]) {
|
|
|
877
539
|
record.aiStatus.generatedImages = true;
|
|
878
540
|
record.aiStatus.analyzedImages = true;
|
|
879
541
|
record.aiStatus.analyzedNoImages = true;
|
|
880
|
-
touchRecords();
|
|
881
542
|
return;
|
|
882
543
|
}
|
|
883
|
-
record.label = oldDataResult._label || record.label;
|
|
884
|
-
|
|
544
|
+
record.label = oldDataResult.record._label || record.label;
|
|
545
|
+
record.oldImagePreviews = oldDataResult.imagePreviews;
|
|
546
|
+
initializeRecordData(record, oldDataResult.record);
|
|
885
547
|
if (props.meta.isAttachFiles) {
|
|
886
548
|
await fetchImages(record, recordId);
|
|
887
549
|
if (!checkIfDialogOpen()) {
|
|
@@ -907,7 +569,6 @@ async function processOneRecord(recordId: string, sessionIds: string[]) {
|
|
|
907
569
|
const hasError = results.some(result => result.status === 'rejected');
|
|
908
570
|
record.status = hasError ? 'failed' : 'completed';
|
|
909
571
|
completedRecordIds.value.add(String(recordId));
|
|
910
|
-
touchRecords();
|
|
911
572
|
}
|
|
912
573
|
|
|
913
574
|
async function checkRateLimits(actions?: GenerationAction[]): Promise<{ ok: boolean; sessionIds?: string[] }> {
|
|
@@ -940,6 +601,7 @@ async function checkRateLimits(actions?: GenerationAction[]): Promise<{ ok: bool
|
|
|
940
601
|
variant: 'danger',
|
|
941
602
|
timeout: 'unlimited',
|
|
942
603
|
});
|
|
604
|
+
isCheckingRateLimits.value = false;
|
|
943
605
|
return { ok: false };
|
|
944
606
|
}
|
|
945
607
|
sessionIds.push(rateLimitRes.sessionId);
|
|
@@ -968,27 +630,15 @@ async function runActionForRecord(record: RecordState, actionType: GenerationAct
|
|
|
968
630
|
: 'analyzedNoImages';
|
|
969
631
|
record.aiStatus[responseFlag] = false;
|
|
970
632
|
|
|
971
|
-
let customPrompt;
|
|
972
|
-
if (actionType === 'generate_images') {
|
|
973
|
-
customPrompt = generationPrompts.value.imageGenerationPrompts || generationPrompts.value.generateImages;
|
|
974
|
-
} else if (actionType === 'analyze') {
|
|
975
|
-
customPrompt = generationPrompts.value.imageFieldsPrompts;
|
|
976
|
-
} else if (actionType === 'analyze_no_images') {
|
|
977
|
-
customPrompt = generationPrompts.value.plainFieldsPrompts;
|
|
978
|
-
}
|
|
979
|
-
|
|
980
633
|
let createJobResponse;
|
|
981
634
|
try {
|
|
982
|
-
if (!checkIfDialogOpen()) {
|
|
983
|
-
return;
|
|
984
|
-
}
|
|
985
635
|
createJobResponse = await callAdminForthApi({
|
|
986
636
|
path: `/plugin/${props.meta.pluginInstanceId}/create-job`,
|
|
987
637
|
method: 'POST',
|
|
988
638
|
body: {
|
|
989
639
|
actionType,
|
|
990
640
|
recordId: record.id,
|
|
991
|
-
|
|
641
|
+
customPrompt: JSON.stringify(generationPrompts.value[PROMPT_CATEGORY[actionType]]),
|
|
992
642
|
filterFilledFields: forceFilterFilledFields !== undefined ? forceFilterFilledFields: !overwriteExistingValues.value,
|
|
993
643
|
sessionIds
|
|
994
644
|
},
|
|
@@ -996,7 +646,7 @@ async function runActionForRecord(record: RecordState, actionType: GenerationAct
|
|
|
996
646
|
});
|
|
997
647
|
} catch (e) {
|
|
998
648
|
record.aiStatus[responseFlag] = true;
|
|
999
|
-
|
|
649
|
+
applyJobFailure(record, actionType, e instanceof Error ? e.message : String(e));
|
|
1000
650
|
throw e;
|
|
1001
651
|
}
|
|
1002
652
|
|
|
@@ -1006,7 +656,7 @@ async function runActionForRecord(record: RecordState, actionType: GenerationAct
|
|
|
1006
656
|
|
|
1007
657
|
if (createJobResponse?.error || !createJobResponse?.jobId) {
|
|
1008
658
|
record.aiStatus[responseFlag] = true;
|
|
1009
|
-
|
|
659
|
+
applyJobFailure(record, actionType, createJobResponse?.error || `Failed to ${getActionLabel(actionType)}. Please, try to re-run the action.`);
|
|
1010
660
|
throw new Error(createJobResponse?.error || 'Failed to create job');
|
|
1011
661
|
}
|
|
1012
662
|
|
|
@@ -1031,67 +681,57 @@ async function pollJob(
|
|
|
1031
681
|
await waitForRefresh(actionType);
|
|
1032
682
|
continue;
|
|
1033
683
|
}
|
|
1034
|
-
|
|
684
|
+
// a failed job comes back as { ok: false, error }
|
|
685
|
+
if (jobResponse.error) {
|
|
1035
686
|
record.aiStatus[responseFlag] = true;
|
|
1036
|
-
|
|
687
|
+
applyJobFailure(record, actionType, jobResponse.error);
|
|
1037
688
|
throw new Error(jobResponse.error);
|
|
1038
689
|
}
|
|
1039
|
-
|
|
1040
|
-
if (jobStatus === 'in_progress') {
|
|
690
|
+
if (jobResponse.job.status === 'in_progress') {
|
|
1041
691
|
await waitForRefresh(actionType);
|
|
1042
692
|
continue;
|
|
1043
693
|
}
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
isInProgress = false;
|
|
1048
|
-
continue;
|
|
1049
|
-
}
|
|
1050
|
-
if (jobStatus === 'failed') {
|
|
1051
|
-
applyJobFailure(record, jobResponse.job, actionType);
|
|
1052
|
-
record.aiStatus[responseFlag] = true;
|
|
1053
|
-
throw new Error(jobResponse.job?.error || 'Job failed');
|
|
1054
|
-
}
|
|
694
|
+
applyJobResult(record, jobResponse.job, actionType);
|
|
695
|
+
record.aiStatus[responseFlag] = true;
|
|
696
|
+
isInProgress = false;
|
|
1055
697
|
}
|
|
1056
698
|
}
|
|
1057
699
|
|
|
1058
700
|
function applyJobResult(record: RecordState, job: any, actionType: GenerationAction) {
|
|
1059
701
|
if (actionType === 'generate_images') {
|
|
1060
|
-
for (const fieldName of props.meta.outputImageFields
|
|
1061
|
-
const resultValue = job
|
|
702
|
+
for (const fieldName of props.meta.outputImageFields) {
|
|
703
|
+
const resultValue = job.result[fieldName];
|
|
1062
704
|
if (resultValue !== undefined) {
|
|
1063
705
|
record.data[fieldName] = resultValue;
|
|
1064
706
|
}
|
|
1065
|
-
|
|
1066
|
-
if (
|
|
1067
|
-
record.
|
|
1068
|
-
|
|
707
|
+
const imageMeta = job.recordMeta?.[`${fieldName}_meta`];
|
|
708
|
+
if (imageMeta) {
|
|
709
|
+
record.listOfImageNotGenerated[fieldName] = imageMeta;
|
|
710
|
+
} else {
|
|
711
|
+
delete record.listOfImageNotGenerated[fieldName];
|
|
1069
712
|
}
|
|
1070
713
|
}
|
|
1071
714
|
} else {
|
|
1072
715
|
record.data = {
|
|
1073
716
|
...record.data,
|
|
1074
|
-
...
|
|
717
|
+
...job.result,
|
|
1075
718
|
};
|
|
1076
719
|
}
|
|
1077
|
-
touchRecords();
|
|
1078
720
|
}
|
|
1079
721
|
|
|
1080
|
-
function applyJobFailure(record: RecordState,
|
|
1081
|
-
registerGenerationFailure(record, actionType,
|
|
722
|
+
function applyJobFailure(record: RecordState, actionType: GenerationAction, error: string) {
|
|
723
|
+
registerGenerationFailure(record, actionType, error);
|
|
1082
724
|
if (actionType === 'generate_images') {
|
|
1083
725
|
record.imageGenerationFailed = true;
|
|
1084
|
-
record.imageGenerationErrorMessage =
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
}
|
|
726
|
+
record.imageGenerationErrorMessage = error;
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
const [fields, messages] = actionType === 'analyze'
|
|
730
|
+
? [props.meta.outputFieldsForAnalizeFromImages, record.imageToTextErrorMessages]
|
|
731
|
+
: [props.meta.outputPlainFields, record.textToTextErrorMessages];
|
|
732
|
+
for (const field of fields) {
|
|
733
|
+
messages[field] = error;
|
|
1093
734
|
}
|
|
1094
|
-
touchRecords();
|
|
1095
735
|
}
|
|
1096
736
|
|
|
1097
737
|
async function waitForRefresh(actionType: GenerationAction) {
|
|
@@ -1104,7 +744,7 @@ async function waitForRefresh(actionType: GenerationAction) {
|
|
|
1104
744
|
}
|
|
1105
745
|
}
|
|
1106
746
|
|
|
1107
|
-
async function fetchOldData(recordId: string) {
|
|
747
|
+
async function fetchOldData(recordId: string): Promise<{ record: Record<string, any>, imagePreviews: Record<string, string | null> } | null> {
|
|
1108
748
|
try {
|
|
1109
749
|
const res = await callAdminForthApi({
|
|
1110
750
|
path: `/plugin/${props.meta.pluginInstanceId}/get_old_data`,
|
|
@@ -1120,7 +760,7 @@ async function fetchOldData(recordId: string) {
|
|
|
1120
760
|
});
|
|
1121
761
|
return null;
|
|
1122
762
|
}
|
|
1123
|
-
return res
|
|
763
|
+
return res;
|
|
1124
764
|
} catch (error) {
|
|
1125
765
|
console.error('Failed to get old record:', error);
|
|
1126
766
|
isError.value = true;
|
|
@@ -1132,24 +772,16 @@ async function fetchOldData(recordId: string) {
|
|
|
1132
772
|
function initializeRecordData(record: RecordState, oldRecord: Record<string, any>) {
|
|
1133
773
|
const newData: Record<string, any> = {};
|
|
1134
774
|
const newOldData: Record<string, any> = {};
|
|
1135
|
-
for (const key
|
|
775
|
+
for (const key of props.meta.outputFields) {
|
|
1136
776
|
const normalizedValue = normalizeEnumValue(key, oldRecord[key] ?? null);
|
|
1137
777
|
newData[key] = normalizedValue;
|
|
1138
778
|
newOldData[key] = normalizedValue;
|
|
1139
779
|
}
|
|
1140
|
-
if (props.meta.outputImageFields) {
|
|
1141
|
-
for (const key of props.meta.outputImageFields) {
|
|
1142
|
-
const normalizedValue = normalizeEnumValue(key, oldRecord[key] ?? null);
|
|
1143
|
-
newData[key] = normalizedValue;
|
|
1144
|
-
newOldData[key] = normalizedValue;
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
780
|
newData[primaryKey] = oldRecord[primaryKey] ?? record.id;
|
|
1148
781
|
newOldData[primaryKey] = oldRecord[primaryKey] ?? record.id;
|
|
1149
782
|
newOldData._label = oldRecord._label;
|
|
1150
783
|
record.data = newData;
|
|
1151
784
|
record.oldData = newOldData;
|
|
1152
|
-
touchRecords();
|
|
1153
785
|
}
|
|
1154
786
|
|
|
1155
787
|
function normalizeEnumValue(key: string, value: any) {
|
|
@@ -1169,7 +801,6 @@ async function fetchImages(record: RecordState, recordId: string) {
|
|
|
1169
801
|
body: { recordId },
|
|
1170
802
|
});
|
|
1171
803
|
record.images = res.images || [];
|
|
1172
|
-
touchRecords();
|
|
1173
804
|
} catch (error) {
|
|
1174
805
|
console.error('Failed to get images:', error);
|
|
1175
806
|
isError.value = true;
|
|
@@ -1177,68 +808,6 @@ async function fetchImages(record: RecordState, recordId: string) {
|
|
|
1177
808
|
}
|
|
1178
809
|
}
|
|
1179
810
|
|
|
1180
|
-
function formatLabel(str) {
|
|
1181
|
-
const labelsMap = props.meta?.columnLabels || {};
|
|
1182
|
-
let labelFromMeta = labelsMap[str];
|
|
1183
|
-
if (!labelFromMeta) {
|
|
1184
|
-
const match = Object.keys(labelsMap).find(k => k.toLowerCase() === String(str).toLowerCase());
|
|
1185
|
-
if (match) labelFromMeta = labelsMap[match];
|
|
1186
|
-
}
|
|
1187
|
-
if (labelFromMeta) return labelFromMeta;
|
|
1188
|
-
return str
|
|
1189
|
-
.split('_')
|
|
1190
|
-
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
1191
|
-
.join(' ');
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
function generateTableHeaders(outputFields) {
|
|
1195
|
-
const headers = [];
|
|
1196
|
-
|
|
1197
|
-
headers.push({ label: 'Checkboxes', fieldName: 'checkboxes' });
|
|
1198
|
-
headers.push({ label: 'Field name', fieldName: 'label' });
|
|
1199
|
-
if (props.meta.isAttachFiles) {
|
|
1200
|
-
headers.push({ label: 'Source Images', fieldName: 'images' });
|
|
1201
|
-
}
|
|
1202
|
-
for (const key in outputFields) {
|
|
1203
|
-
headers.push({
|
|
1204
|
-
label: formatLabel(key),
|
|
1205
|
-
fieldName: key,
|
|
1206
|
-
});
|
|
1207
|
-
}
|
|
1208
|
-
return headers;
|
|
1209
|
-
}
|
|
1210
|
-
function handleTableError(errorData) {
|
|
1211
|
-
isError.value = errorData.isError;
|
|
1212
|
-
errorMessage.value = errorData.errorMessage;
|
|
1213
|
-
}
|
|
1214
|
-
async function prepareDataForSave() {
|
|
1215
|
-
const checkedRecords = recordIds.value
|
|
1216
|
-
.map(id => getOrCreateRecord(id))
|
|
1217
|
-
.filter(record => record.isChecked === true);
|
|
1218
|
-
const checkedItems = checkedRecords.map(record => ({
|
|
1219
|
-
...record.data,
|
|
1220
|
-
[primaryKey]: record.id,
|
|
1221
|
-
}));
|
|
1222
|
-
|
|
1223
|
-
const promises: Promise<void>[] = [];
|
|
1224
|
-
checkedRecords.forEach((record, index) => {
|
|
1225
|
-
if (record.imageGenerationFailed) {
|
|
1226
|
-
return;
|
|
1227
|
-
}
|
|
1228
|
-
for (const [key, value] of Object.entries(checkedItems[index])) {
|
|
1229
|
-
if (props.meta.outputImageFields?.includes(key)) {
|
|
1230
|
-
const p = convertImages(key, value).then(result => {
|
|
1231
|
-
checkedItems[index][key] = result;
|
|
1232
|
-
});
|
|
1233
|
-
promises.push(p);
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1236
|
-
});
|
|
1237
|
-
|
|
1238
|
-
await Promise.all(promises);
|
|
1239
|
-
return [checkedItems, checkedRecords] as const;
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
811
|
async function prepareDataForRecords(records: RecordState[]) {
|
|
1243
812
|
const checkedItems = records.map(record => ({
|
|
1244
813
|
...record.data,
|
|
@@ -1251,7 +820,7 @@ async function prepareDataForRecords(records: RecordState[]) {
|
|
|
1251
820
|
return;
|
|
1252
821
|
}
|
|
1253
822
|
for (const [key, value] of Object.entries(checkedItems[index])) {
|
|
1254
|
-
if (props.meta.outputImageFields
|
|
823
|
+
if (props.meta.outputImageFields.includes(key)) {
|
|
1255
824
|
const p = convertImages(key, value).then(result => {
|
|
1256
825
|
checkedItems[index][key] = result;
|
|
1257
826
|
});
|
|
@@ -1293,7 +862,7 @@ async function uploadImagesForRecords(reqData: Record<string, any>[], checkedRec
|
|
|
1293
862
|
const item = reqData[index];
|
|
1294
863
|
const record = checkedRecords[index];
|
|
1295
864
|
for (const [key, value] of Object.entries(item)) {
|
|
1296
|
-
if (props.meta.outputImageFields
|
|
865
|
+
if (props.meta.outputImageFields.includes(key)) {
|
|
1297
866
|
if (!value) {
|
|
1298
867
|
continue;
|
|
1299
868
|
}
|
|
@@ -1315,12 +884,11 @@ async function uploadImagesForRecords(reqData: Record<string, any>[], checkedRec
|
|
|
1315
884
|
await Promise.all(imagesToUpload);
|
|
1316
885
|
}
|
|
1317
886
|
|
|
1318
|
-
|
|
1319
887
|
async function saveData() {
|
|
1320
888
|
const errorText = 'Failed to save some records. Not all data may be saved'
|
|
1321
889
|
try {
|
|
1322
890
|
isLoading.value = true;
|
|
1323
|
-
|
|
891
|
+
const [reqData, checkedRecords] = await prepareDataForRecords(recordsList.value.filter(record => record.isChecked));
|
|
1324
892
|
if (checkedRecords.length < 1) {
|
|
1325
893
|
adminforth.alert({ message: t('No items selected'), variant: 'warning' });
|
|
1326
894
|
return;
|
|
@@ -1369,14 +937,13 @@ async function saveData() {
|
|
|
1369
937
|
props.updateList();
|
|
1370
938
|
props.clearCheckboxes?.();
|
|
1371
939
|
isLoading.value = false;
|
|
1372
|
-
isDataSaved.value = true;
|
|
1373
940
|
window.removeEventListener('beforeunload', beforeUnloadHandler);
|
|
1374
941
|
}
|
|
1375
942
|
}
|
|
1376
943
|
async function uploadImage(imgBlob, id, fieldName) {
|
|
1377
944
|
const file = new File([imgBlob], `generated_${fieldName}_${id}.${imgBlob.type.split('/').pop()}`, { type: imgBlob.type });
|
|
1378
945
|
const { name, size, type } = file;
|
|
1379
|
-
|
|
946
|
+
|
|
1380
947
|
const extension = name.split('.').pop();
|
|
1381
948
|
const nameNoExtension = name.replace(`.${extension}`, '');
|
|
1382
949
|
|
|
@@ -1403,14 +970,8 @@ async function uploadImage(imgBlob, id, fieldName) {
|
|
|
1403
970
|
|
|
1404
971
|
const xhr = new XMLHttpRequest();
|
|
1405
972
|
const success = await new Promise((resolve) => {
|
|
1406
|
-
xhr.upload.onprogress = (e) => {
|
|
1407
|
-
if (e.lengthComputable) {
|
|
1408
|
-
}
|
|
1409
|
-
};
|
|
1410
973
|
xhr.addEventListener('loadend', () => {
|
|
1411
|
-
|
|
1412
|
-
// try to read response
|
|
1413
|
-
resolve(success);
|
|
974
|
+
resolve(xhr.readyState === 4 && xhr.status === 200);
|
|
1414
975
|
});
|
|
1415
976
|
xhr.open('PUT', uploadUrl, true);
|
|
1416
977
|
xhr.setRequestHeader('Content-Type', type);
|
|
@@ -1444,7 +1005,7 @@ async function uploadImage(imgBlob, id, fieldName) {
|
|
|
1444
1005
|
}
|
|
1445
1006
|
}
|
|
1446
1007
|
|
|
1447
|
-
async function regenerateImages({ recordId }: { recordId:
|
|
1008
|
+
async function regenerateImages({ recordId }: { recordId: string | number }) {
|
|
1448
1009
|
const {ok, sessionIds} = await checkRateLimits(['generate_images']);
|
|
1449
1010
|
if (!ok || !sessionIds) {
|
|
1450
1011
|
return;
|
|
@@ -1457,13 +1018,10 @@ async function regenerateImages({ recordId }: { recordId: any }) {
|
|
|
1457
1018
|
});
|
|
1458
1019
|
return;
|
|
1459
1020
|
}
|
|
1460
|
-
const record = recordsById.get(String(recordId))
|
|
1461
|
-
if (!record) {
|
|
1462
|
-
return;
|
|
1463
|
-
}
|
|
1464
|
-
record.aiStatus.generatedImages = false;
|
|
1021
|
+
const record = recordsById.get(String(recordId))!;
|
|
1465
1022
|
record.status = 'processing';
|
|
1466
|
-
|
|
1023
|
+
record.imageGenerationFailed = false;
|
|
1024
|
+
record.imageGenerationErrorMessage = '';
|
|
1467
1025
|
|
|
1468
1026
|
try {
|
|
1469
1027
|
await runActionForRecord(
|
|
@@ -1472,41 +1030,10 @@ async function regenerateImages({ recordId }: { recordId: any }) {
|
|
|
1472
1030
|
sessionIds,
|
|
1473
1031
|
false
|
|
1474
1032
|
);
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
record.aiStatus.generatedImages = true;
|
|
1480
|
-
touchRecords();
|
|
1481
|
-
|
|
1482
|
-
} catch (e) {
|
|
1483
|
-
record.aiStatus.generatedImages = true;
|
|
1484
|
-
record.status = 'failed';
|
|
1485
|
-
touchRecords();
|
|
1486
|
-
}
|
|
1487
|
-
}
|
|
1488
|
-
|
|
1489
|
-
async function findPreviewURLForImages() {
|
|
1490
|
-
if (props.meta.outputImageFields){
|
|
1491
|
-
for (const fieldName of props.meta.outputImageFields) {
|
|
1492
|
-
try {
|
|
1493
|
-
const res = await callAdminForthApi({
|
|
1494
|
-
path: `/plugin/${props.meta.pluginInstanceId}/compile_old_image_link`,
|
|
1495
|
-
method: 'POST',
|
|
1496
|
-
body: {
|
|
1497
|
-
image: "test",
|
|
1498
|
-
columnName: fieldName,
|
|
1499
|
-
},
|
|
1500
|
-
});
|
|
1501
|
-
if (res?.ok) {
|
|
1502
|
-
isImageHasPreviewUrl.value[fieldName] = true;
|
|
1503
|
-
} else {
|
|
1504
|
-
isImageHasPreviewUrl.value[fieldName] = false;
|
|
1505
|
-
}
|
|
1506
|
-
} catch (e) {
|
|
1507
|
-
console.error("Error finding preview URL for field", fieldName, e);
|
|
1508
|
-
}
|
|
1509
|
-
}
|
|
1033
|
+
} catch {
|
|
1034
|
+
// the error is already shown on the image fields of the record
|
|
1035
|
+
} finally {
|
|
1036
|
+
settleRecordStatus(record);
|
|
1510
1037
|
}
|
|
1511
1038
|
}
|
|
1512
1039
|
|
|
@@ -1514,112 +1041,40 @@ function click() {
|
|
|
1514
1041
|
openDialog();
|
|
1515
1042
|
}
|
|
1516
1043
|
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
)
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
([
|
|
1527
|
-
)
|
|
1528
|
-
);
|
|
1529
|
-
|
|
1530
|
-
const promptsToSaveAnalyzeNoImages = Object.fromEntries(
|
|
1531
|
-
Object.entries(generationPrompts.value.plainFieldsPrompts).filter(
|
|
1532
|
-
([key, value]) => value !== props.meta.generationPrompts.plainFieldsPrompts[key]
|
|
1533
|
-
)
|
|
1044
|
+
// a saved prompt is kept in this browser, only the ones which differ from the plugin config are stored
|
|
1045
|
+
function savePrompt(category: PromptCategory, field: string) {
|
|
1046
|
+
savedPrompts.value[category][field] = generationPrompts.value[category][field];
|
|
1047
|
+
const changedPrompts = Object.fromEntries(
|
|
1048
|
+
Object.entries(savedPrompts.value)
|
|
1049
|
+
.map(([category, prompts]) => [
|
|
1050
|
+
category,
|
|
1051
|
+
Object.fromEntries(Object.entries(prompts).filter(([field, prompt]) => prompt !== defaultPrompts[category][field])),
|
|
1052
|
+
])
|
|
1053
|
+
.filter(([, prompts]) => Object.keys(prompts).length > 0),
|
|
1534
1054
|
);
|
|
1535
|
-
|
|
1536
|
-
const promptsToSave = {
|
|
1537
|
-
...(Object.keys(promptsToSaveAnalyzeNoImages).length > 0 ? { plainFieldsPrompts: promptsToSaveAnalyzeNoImages } : {}),
|
|
1538
|
-
...(Object.keys(promptsToSaveAnalyzeImages).length > 0 ? { imageFieldsPrompts: promptsToSaveAnalyzeImages } : {}),
|
|
1539
|
-
...(Object.keys(promptsToSaveGenerateImages).length > 0 ? { generateImages: promptsToSaveGenerateImages } : {}),
|
|
1540
|
-
};
|
|
1541
|
-
|
|
1542
|
-
console.log('Saving prompts:', promptsToSave);
|
|
1543
|
-
|
|
1544
|
-
localStorage.setItem(`bulkAiFlowGenerationPrompts_${props.meta.pluginInstanceId}`, JSON.stringify(promptsToSave));
|
|
1545
|
-
popupMode.value = 'confirmation';
|
|
1546
|
-
}
|
|
1547
|
-
|
|
1548
|
-
async function getGenerationPrompts() {
|
|
1549
|
-
const calculatedGenerationPrompts: any = {};
|
|
1550
|
-
const savedPrompts = localStorage.getItem(`bulkAiFlowGenerationPrompts_${props.meta.pluginInstanceId}`);
|
|
1551
|
-
if (props.meta.generationPrompts.plainFieldsPrompts) {
|
|
1552
|
-
calculatedGenerationPrompts.plainFieldsPrompts = {
|
|
1553
|
-
...props.meta.generationPrompts.plainFieldsPrompts
|
|
1554
|
-
};
|
|
1555
|
-
}
|
|
1556
|
-
if (props.meta.generationPrompts.imageFieldsPrompts) {
|
|
1557
|
-
calculatedGenerationPrompts.imageFieldsPrompts = {
|
|
1558
|
-
...props.meta.generationPrompts.imageFieldsPrompts
|
|
1559
|
-
};
|
|
1560
|
-
}
|
|
1561
|
-
if (props.meta.generationPrompts.imageGenerationPrompts) {
|
|
1562
|
-
let imageFields = {};
|
|
1563
|
-
for (const [key, value] of Object.entries(props.meta.generationPrompts.imageGenerationPrompts)) {
|
|
1564
|
-
// value might be typed as unknown; cast to any to access prompt safely
|
|
1565
|
-
imageFields[key] = (value as any).prompt;
|
|
1566
|
-
}
|
|
1567
|
-
calculatedGenerationPrompts.generateImages = imageFields;
|
|
1568
|
-
}
|
|
1569
|
-
if (savedPrompts && props.meta.askConfirmationBeforeGenerating) {
|
|
1570
|
-
|
|
1571
|
-
generationPrompts.value = checkAndAddNewFieldsToPrompts(JSON.parse(savedPrompts), calculatedGenerationPrompts);
|
|
1572
|
-
|
|
1573
|
-
return;
|
|
1574
|
-
}
|
|
1575
|
-
generationPrompts.value = calculatedGenerationPrompts;
|
|
1055
|
+
localStorage.setItem(promptsStorageKey, JSON.stringify(changedPrompts));
|
|
1576
1056
|
}
|
|
1577
1057
|
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
generationPrompts.value[categoryKey][promptKey] = props.meta.generationPrompts[categoryKey][promptKey];
|
|
1587
|
-
}
|
|
1588
|
-
|
|
1589
|
-
function clickSettingsButton() {
|
|
1590
|
-
getGenerationPrompts();
|
|
1591
|
-
popupMode.value = 'settings';
|
|
1058
|
+
// fields which were removed from the plugin config drop out of the saved prompts, new fields get their defaults
|
|
1059
|
+
function promptsWithSaved(saved: Partial<Prompts>): Prompts {
|
|
1060
|
+
return Object.fromEntries(
|
|
1061
|
+
Object.entries(defaultPrompts).map(([category, prompts]) => [
|
|
1062
|
+
category,
|
|
1063
|
+
Object.fromEntries(Object.entries(prompts).map(([field, prompt]) => [field, saved[category]?.[field] ?? prompt])),
|
|
1064
|
+
]),
|
|
1065
|
+
) as Prompts;
|
|
1592
1066
|
}
|
|
1593
1067
|
|
|
1594
|
-
|
|
1595
|
-
function
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
if (!savedPrompts[categoryKey].hasOwnProperty(promptKey)) {
|
|
1602
|
-
savedPrompts[categoryKey][promptKey] = defaultPrompts[categoryKey][promptKey];
|
|
1603
|
-
}
|
|
1604
|
-
}
|
|
1605
|
-
}
|
|
1606
|
-
}
|
|
1607
|
-
//remove deprecated fields
|
|
1608
|
-
for (const categoryKey in savedPrompts) {
|
|
1609
|
-
if (!defaultPrompts.hasOwnProperty(categoryKey)) {
|
|
1610
|
-
delete savedPrompts[categoryKey];
|
|
1611
|
-
} else {
|
|
1612
|
-
for (const promptKey in savedPrompts[categoryKey]) {
|
|
1613
|
-
if (!defaultPrompts[categoryKey].hasOwnProperty(promptKey)) {
|
|
1614
|
-
delete savedPrompts[categoryKey][promptKey];
|
|
1615
|
-
}
|
|
1616
|
-
}
|
|
1617
|
-
}
|
|
1618
|
-
}
|
|
1619
|
-
return savedPrompts;
|
|
1068
|
+
// only a user who sees the setup screen can edit prompts, so only then the saved ones apply
|
|
1069
|
+
function loadGenerationPrompts() {
|
|
1070
|
+
const saved = props.meta.askConfirmationBeforeGenerating
|
|
1071
|
+
? JSON.parse(localStorage.getItem(promptsStorageKey) || '{}')
|
|
1072
|
+
: {};
|
|
1073
|
+
savedPrompts.value = promptsWithSaved(saved);
|
|
1074
|
+
generationPrompts.value = promptsWithSaved(saved);
|
|
1620
1075
|
}
|
|
1621
1076
|
|
|
1622
|
-
async function regenerateCell(
|
|
1077
|
+
async function regenerateCell({ recordId, fieldName }: { recordId: string | number, fieldName: string }) {
|
|
1623
1078
|
if (coreStore.isInternetError) {
|
|
1624
1079
|
adminforth.alert({
|
|
1625
1080
|
message: t('Cannot regenerate column while internet connection is lost. Please check your connection and try again.'),
|
|
@@ -1629,135 +1084,84 @@ async function regenerateCell(recordInfo: any) {
|
|
|
1629
1084
|
return;
|
|
1630
1085
|
}
|
|
1631
1086
|
|
|
1632
|
-
const
|
|
1087
|
+
const actionType: GenerationAction = props.meta.outputFieldsForAnalizeFromImages.includes(fieldName) ? 'analyze' : 'analyze_no_images';
|
|
1088
|
+
const { ok, sessionIds } = await checkRateLimits([actionType]);
|
|
1633
1089
|
if (!ok || !sessionIds) {
|
|
1634
1090
|
return;
|
|
1635
1091
|
}
|
|
1636
1092
|
|
|
1637
|
-
const record = recordsById.get(String(
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
record.regeneratingFieldsStatus[recordInfo.fieldName] = false;
|
|
1643
|
-
}
|
|
1644
|
-
record.regeneratingFieldsStatus[recordInfo.fieldName] = true;
|
|
1645
|
-
touchRecords();
|
|
1646
|
-
const actionType = props.meta.outputFieldsForAnalizeFromImages?.includes(recordInfo.fieldName)
|
|
1647
|
-
? 'analyze'
|
|
1648
|
-
: props.meta.outputPlainFields?.includes(recordInfo.fieldName)
|
|
1649
|
-
? 'analyze_no_images'
|
|
1650
|
-
: null;
|
|
1651
|
-
if (!actionType) {
|
|
1652
|
-
console.error(`Field ${recordInfo.fieldName} is not configured for analysis.`);
|
|
1653
|
-
record.regeneratingFieldsStatus[recordInfo.fieldName] = false;
|
|
1654
|
-
touchRecords();
|
|
1655
|
-
return;
|
|
1656
|
-
}
|
|
1657
|
-
|
|
1658
|
-
let generationPromptsForField = {};
|
|
1659
|
-
if (actionType === 'analyze') {
|
|
1660
|
-
generationPromptsForField = generationPrompts.value.imageFieldsPrompts || {};
|
|
1661
|
-
record.aiStatus.analyzedImages = false;
|
|
1662
|
-
} else if (actionType === 'analyze_no_images') {
|
|
1663
|
-
generationPromptsForField = generationPrompts.value.plainFieldsPrompts || {};
|
|
1664
|
-
record.aiStatus.analyzedNoImages = false;
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
|
-
let jobId;
|
|
1668
|
-
let res;
|
|
1093
|
+
const record = recordsById.get(String(recordId))!;
|
|
1094
|
+
const errorMessages = actionType === 'analyze' ? record.imageToTextErrorMessages : record.textToTextErrorMessages;
|
|
1095
|
+
const refreshRate = actionType === 'analyze' ? props.meta.refreshRates.fillFieldsFromImages : props.meta.refreshRates.fillPlainFields;
|
|
1096
|
+
record.regeneratingFieldsStatus[fieldName] = true;
|
|
1097
|
+
|
|
1669
1098
|
try {
|
|
1670
|
-
res = await callAdminForthApi({
|
|
1099
|
+
const res = await callAdminForthApi({
|
|
1671
1100
|
path: `/plugin/${props.meta.pluginInstanceId}/create-job`,
|
|
1672
1101
|
method: 'POST',
|
|
1673
1102
|
body: {
|
|
1674
|
-
fieldToRegenerate:
|
|
1675
|
-
recordId
|
|
1103
|
+
fieldToRegenerate: fieldName,
|
|
1104
|
+
recordId,
|
|
1676
1105
|
action: actionType,
|
|
1677
1106
|
actionType: "regenerate_cell",
|
|
1678
|
-
prompt:
|
|
1107
|
+
prompt: generationPrompts.value[PROMPT_CATEGORY[actionType]][fieldName] || null,
|
|
1679
1108
|
sessionIds
|
|
1680
1109
|
},
|
|
1681
1110
|
silentError: true,
|
|
1682
1111
|
});
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
});
|
|
1692
|
-
isError.value = true;
|
|
1693
|
-
errorMessage.value = t(`Failed to regenerate field`);
|
|
1694
|
-
record.regeneratingFieldsStatus[recordInfo.fieldName] = false;
|
|
1695
|
-
return;
|
|
1696
|
-
}
|
|
1697
|
-
jobId = res.jobId;
|
|
1698
|
-
res = {}
|
|
1699
|
-
do {
|
|
1700
|
-
res = await callAdminForthApi({
|
|
1701
|
-
path: `/plugin/${props.meta.pluginInstanceId}/get-job-status`,
|
|
1702
|
-
method: 'POST',
|
|
1703
|
-
body: { jobId },
|
|
1704
|
-
silentError: true,
|
|
1705
|
-
});
|
|
1706
|
-
if (actionType === 'analyze') {
|
|
1707
|
-
await new Promise(resolve => setTimeout(resolve, props.meta.refreshRates?.fillFieldsFromImages));
|
|
1708
|
-
} else if (actionType === 'analyze_no_images') {
|
|
1709
|
-
await new Promise(resolve => setTimeout(resolve, props.meta.refreshRates?.fillPlainFields));
|
|
1112
|
+
if (res.ok === false) {
|
|
1113
|
+
adminforth.alert({
|
|
1114
|
+
message: res.error,
|
|
1115
|
+
variant: 'danger',
|
|
1116
|
+
});
|
|
1117
|
+
isError.value = true;
|
|
1118
|
+
errorMessage.value = t(`Failed to regenerate field`);
|
|
1119
|
+
return;
|
|
1710
1120
|
}
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
}
|
|
1722
|
-
|
|
1723
|
-
|
|
1121
|
+
|
|
1122
|
+
let jobResponse;
|
|
1123
|
+
do {
|
|
1124
|
+
await new Promise(resolve => setTimeout(resolve, refreshRate));
|
|
1125
|
+
jobResponse = await callAdminForthApi({
|
|
1126
|
+
path: `/plugin/${props.meta.pluginInstanceId}/get-job-status`,
|
|
1127
|
+
method: 'POST',
|
|
1128
|
+
body: { jobId: res.jobId },
|
|
1129
|
+
silentError: true,
|
|
1130
|
+
});
|
|
1131
|
+
} while (jobResponse?.job?.status === 'in_progress');
|
|
1132
|
+
|
|
1133
|
+
// a failed job comes back as { ok: false, error }
|
|
1134
|
+
if (!jobResponse?.ok) {
|
|
1135
|
+
const error = jobResponse?.error || t('Unknown error');
|
|
1136
|
+
adminforth.alert({
|
|
1137
|
+
message: t(`Regeneration action failed for record: ${recordId}. Error: ${error}`),
|
|
1138
|
+
variant: 'danger',
|
|
1139
|
+
timeout: 'unlimited',
|
|
1140
|
+
});
|
|
1141
|
+
errorMessages[fieldName] = error;
|
|
1142
|
+
return;
|
|
1724
1143
|
}
|
|
1725
|
-
record.regeneratingFieldsStatus[recordInfo.fieldName] = false;
|
|
1726
|
-
touchRecords();
|
|
1727
|
-
return;
|
|
1728
|
-
} else if (res.job?.status === 'completed') {
|
|
1729
1144
|
record.data = {
|
|
1730
1145
|
...record.data,
|
|
1731
|
-
...
|
|
1146
|
+
...jobResponse.job.result,
|
|
1732
1147
|
};
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
}
|
|
1740
|
-
record.regeneratingFieldsStatus[recordInfo.fieldName] = false;
|
|
1741
|
-
touchRecords();
|
|
1148
|
+
delete errorMessages[fieldName];
|
|
1149
|
+
} catch (e) {
|
|
1150
|
+
console.error(`Error during cell regeneration for record ${recordId}, field ${fieldName}:`, e);
|
|
1151
|
+
} finally {
|
|
1152
|
+
record.regeneratingFieldsStatus[fieldName] = false;
|
|
1153
|
+
settleRecordStatus(record);
|
|
1742
1154
|
}
|
|
1743
1155
|
}
|
|
1744
1156
|
|
|
1745
1157
|
const beforeUnloadHandler = (event) => {
|
|
1746
|
-
if (isDataSaved.value) return;
|
|
1747
1158
|
event.preventDefault();
|
|
1748
1159
|
event.returnValue = '';
|
|
1749
1160
|
};
|
|
1750
1161
|
|
|
1751
1162
|
async function saveCurrentGenerated() {
|
|
1752
1163
|
const errorText = 'Failed to save some records. Not all data may be saved';
|
|
1753
|
-
const
|
|
1754
|
-
if (generatedIds.size < 1) {
|
|
1755
|
-
return;
|
|
1756
|
-
}
|
|
1757
|
-
const recordsToSave = Array.from(generatedIds)
|
|
1758
|
-
.map(id => recordsById.get(String(id)))
|
|
1759
|
-
.filter((record): record is RecordState => Boolean(record))
|
|
1760
|
-
.filter(record => record.isChecked === true);
|
|
1164
|
+
const recordsToSave = recordsList.value.filter(record => record.isChecked && completedRecordIds.value.has(String(record.id)));
|
|
1761
1165
|
if (recordsToSave.length < 1) {
|
|
1762
1166
|
return;
|
|
1763
1167
|
}
|
|
@@ -1802,19 +1206,17 @@ async function saveCurrentGenerated() {
|
|
|
1802
1206
|
|
|
1803
1207
|
if (savedIds.length > 0) {
|
|
1804
1208
|
recordIds.value = recordIds.value.filter(id => !savedIds.includes(String(id)));
|
|
1805
|
-
for (
|
|
1806
|
-
const id = savedIds[index];
|
|
1209
|
+
for (const id of savedIds) {
|
|
1807
1210
|
recordsById.delete(id);
|
|
1808
|
-
uncheckedRecordIds.delete(id);
|
|
1809
1211
|
completedRecordIds.value.delete(id);
|
|
1810
1212
|
}
|
|
1811
1213
|
adminforth.alert({
|
|
1812
1214
|
message: t('Successfully saved generated data for {count} records', { count: savedIds.length }),
|
|
1813
1215
|
variant: 'success',
|
|
1814
1216
|
});
|
|
1815
|
-
touchRecords();
|
|
1816
1217
|
}
|
|
1817
1218
|
if (recordIds.value.length === 0) {
|
|
1219
|
+
closeDialog();
|
|
1818
1220
|
confirmDialog.value.close();
|
|
1819
1221
|
return;
|
|
1820
1222
|
}
|
|
@@ -1825,21 +1227,7 @@ async function saveCurrentGenerated() {
|
|
|
1825
1227
|
} finally {
|
|
1826
1228
|
isSavingCurrent.value = false;
|
|
1827
1229
|
}
|
|
1828
|
-
await nextTick();
|
|
1829
|
-
tableRef.value.refresh();
|
|
1830
1230
|
props.updateList();
|
|
1831
1231
|
}
|
|
1832
1232
|
|
|
1833
|
-
const generationStatusClass = computed(() => {
|
|
1834
|
-
if (isGenerationPaused.value) {
|
|
1835
|
-
return 'bg-yellow-100 text-yellow-800 border-yellow-200 dark:bg-yellow-950/30 dark:text-yellow-400 dark:border-yellow-400'
|
|
1836
|
-
}
|
|
1837
|
-
|
|
1838
|
-
if (displayedProcessedCount.value === totalRecords.value) {
|
|
1839
|
-
return 'bg-green-100 text-green-800 border-green-200 dark:bg-green-950/30 dark:text-green-400 dark:border-green-400'
|
|
1840
|
-
}
|
|
1841
|
-
|
|
1842
|
-
return 'bg-blue-100 text-blue-800 border-blue-200 dark:bg-blue-950/30 dark:text-blue-400 dark:border-blue-400'
|
|
1843
|
-
})
|
|
1844
|
-
|
|
1845
1233
|
</script>
|