@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/VisionTable.vue
DELETED
|
@@ -1,575 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="vision-table-container overflow-auto pr-2" style="max-height: calc(100vh - 260px);">
|
|
3
|
-
<div class="records-grid p-1 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 gap-6">
|
|
4
|
-
<div
|
|
5
|
-
v-for="item in paginatedRecords"
|
|
6
|
-
:key="item.id"
|
|
7
|
-
class="record-card bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-default p-4 shadow-sm hover:shadow-md transition-shadow relative flex flex-col justify-between gap-4"
|
|
8
|
-
>
|
|
9
|
-
<div class="flex flex-col h-full">
|
|
10
|
-
<div class="record-card-header flex items-start justify-between gap-4 mb-4">
|
|
11
|
-
<div class="flex items-center gap-3 w-full min-w-0">
|
|
12
|
-
<Checkbox
|
|
13
|
-
v-model="item.isChecked"
|
|
14
|
-
class="shrink-0"
|
|
15
|
-
/>
|
|
16
|
-
<!-- CRITICAL WRAPPER -->
|
|
17
|
-
<div class="flex-1 min-w-0 w-0">
|
|
18
|
-
<h3 class="font-bold text-gray-900 dark:text-white w-full truncate overflow-hidden whitespace-nowrap">
|
|
19
|
-
{{ item.label || item.data?.name || $t('Item Record') }}
|
|
20
|
-
</h3>
|
|
21
|
-
</div>
|
|
22
|
-
</div>
|
|
23
|
-
<div class="flex items-center gap-3 shrink-0" v-if="hasNonImageOutputFields">
|
|
24
|
-
<div class="flex items-center gap-2">
|
|
25
|
-
<span class="text-xs font-medium text-gray-600 dark:text-gray-400">
|
|
26
|
-
{{ cardValueMode?.[String(item.id)] === 'old' ? $t('OLD VALUE') : $t('NEW VALUE') }}
|
|
27
|
-
</span>
|
|
28
|
-
<button
|
|
29
|
-
@click="toggleCardValueMode(item.id)"
|
|
30
|
-
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
|
|
31
|
-
:class="cardValueMode?.[String(item.id)] === 'old' ? 'bg-lightPrimary dark:bg-darkPrimary' : 'bg-gray-200 dark:bg-gray-700'"
|
|
32
|
-
>
|
|
33
|
-
<span
|
|
34
|
-
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform dark:bg-gray-200"
|
|
35
|
-
:class="cardValueMode?.[String(item.id)] === 'old' ? 'translate-x-6' : 'translate-x-1'"
|
|
36
|
-
/>
|
|
37
|
-
</button>
|
|
38
|
-
</div>
|
|
39
|
-
</div>
|
|
40
|
-
</div>
|
|
41
|
-
|
|
42
|
-
<div class="flex flex-col gap-3 flex-grow">
|
|
43
|
-
<div
|
|
44
|
-
v-if="props.meta.isAttachFiles"
|
|
45
|
-
class="source-images-section w-full flex flex-col items-center justify-center bg-gray-50 dark:bg-gray-700 border border-dashed border-gray-200 dark:border-gray-600 h-full max-h-[260px] max-w-[446.5px] rounded-default text-center "
|
|
46
|
-
>
|
|
47
|
-
<!-- HAS IMAGES -->
|
|
48
|
-
<template v-if="item.images?.length">
|
|
49
|
-
<template
|
|
50
|
-
v-for="(image, index) in item.images"
|
|
51
|
-
:key="image || index"
|
|
52
|
-
>
|
|
53
|
-
<img
|
|
54
|
-
v-if="image && isValidUrl(image)"
|
|
55
|
-
:src="image"
|
|
56
|
-
class="w-full h-full object-cover rounded-default cursor-pointer border hover:border-blue-500 transition"
|
|
57
|
-
@click="zoomImage(image)"
|
|
58
|
-
/>
|
|
59
|
-
|
|
60
|
-
<div
|
|
61
|
-
v-else
|
|
62
|
-
class="text-xs text-red-500 flex flex-col items-center justify-center h-full p-2"
|
|
63
|
-
>
|
|
64
|
-
<IconExclamationCircleSolid class="w-6 h-6 mb-1" />
|
|
65
|
-
<p>{{ $t('Invalid source image') }}</p>
|
|
66
|
-
</div>
|
|
67
|
-
</template>
|
|
68
|
-
</template>
|
|
69
|
-
|
|
70
|
-
<!-- EMPTY ARRAY -->
|
|
71
|
-
<template v-else-if="item.images?.length === 0">
|
|
72
|
-
<div class="text-gray-400 dark:text-gray-500 flex flex-col items-center justify-center w-full h-[258px]">
|
|
73
|
-
<svg
|
|
74
|
-
class="w-20 h-20 mb-2 stroke-1 text-gray-300 dark:text-gray-600"
|
|
75
|
-
fill="none"
|
|
76
|
-
stroke="currentColor"
|
|
77
|
-
viewBox="0 0 24 24"
|
|
78
|
-
>
|
|
79
|
-
<path
|
|
80
|
-
stroke-linecap="round"
|
|
81
|
-
stroke-linejoin="round"
|
|
82
|
-
stroke-width="2"
|
|
83
|
-
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
|
|
84
|
-
/>
|
|
85
|
-
</svg>
|
|
86
|
-
<p class="text-xs font-medium">{{ $t('No images found') }}</p>
|
|
87
|
-
</div>
|
|
88
|
-
</template>
|
|
89
|
-
|
|
90
|
-
<!-- NO DATA -->
|
|
91
|
-
<template v-else>
|
|
92
|
-
<div class="text-gray-400 dark:text-gray-500 flex flex-col items-center justify-center h-full">
|
|
93
|
-
<svg
|
|
94
|
-
class="w-20 h-20 mb-2 stroke-1 text-gray-300 dark:text-gray-600"
|
|
95
|
-
fill="none"
|
|
96
|
-
stroke="currentColor"
|
|
97
|
-
viewBox="0 0 24 24"
|
|
98
|
-
>
|
|
99
|
-
<path
|
|
100
|
-
stroke-linecap="round"
|
|
101
|
-
stroke-linejoin="round"
|
|
102
|
-
stroke-width="2"
|
|
103
|
-
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
|
|
104
|
-
/>
|
|
105
|
-
</svg>
|
|
106
|
-
<p class="text-xs font-medium">{{ $t('No image found') }}</p>
|
|
107
|
-
</div>
|
|
108
|
-
</template>
|
|
109
|
-
</div>
|
|
110
|
-
|
|
111
|
-
<div class="fields-section flex flex-col gap-3">
|
|
112
|
-
<div
|
|
113
|
-
v-for="n in customFieldNames"
|
|
114
|
-
:key="n"
|
|
115
|
-
class="flex flex-col relative"
|
|
116
|
-
@mouseenter="(() => { setHover(item.id, n, true) })"
|
|
117
|
-
@mouseleave="(() => { setHover(item.id, n, false) })"
|
|
118
|
-
>
|
|
119
|
-
<div class="flex justify-between items-center mb-1">
|
|
120
|
-
<label class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
121
|
-
{{ n.replace(/([A-Z])/g, ' $1').trim() }}
|
|
122
|
-
</label>
|
|
123
|
-
|
|
124
|
-
<div class="flex gap-1 items-center" v-if="!isInColumnImage(n)">
|
|
125
|
-
<Tooltip v-if="checkForError(item, n)">
|
|
126
|
-
<IconExclamationCircleSolid class="w-4 h-4 text-red-500" />
|
|
127
|
-
<template #tooltip>{{ checkForError(item, n) }}</template>
|
|
128
|
-
</Tooltip>
|
|
129
|
-
</div>
|
|
130
|
-
</div>
|
|
131
|
-
|
|
132
|
-
<div v-if="isInColumnImage(n)">
|
|
133
|
-
<div v-if="item.aiStatus?.generatedImages" class="flex flex-col gap-2">
|
|
134
|
-
<div v-if="isValidUrl(item.data?.[n])" class="flex items-center gap-3 relative w-full">
|
|
135
|
-
<Skeleton v-if="imageLoadErrors[item.id]?.[n]" type="image" class="w-full h-[280px]" />
|
|
136
|
-
<img
|
|
137
|
-
v-else
|
|
138
|
-
:key="item.data?.[n]"
|
|
139
|
-
:src="item.data?.[n]"
|
|
140
|
-
class="w-full max-h-[280px] object-cover border cursor-pointer hover:border-blue-500 rounded-default"
|
|
141
|
-
@error="() => { imageLoadErrors[item.id] ??= {}; imageLoadErrors[item.id][n] = true }"
|
|
142
|
-
@click="() => zoomImage(item.data?.[n])"
|
|
143
|
-
style="image-rendering: auto;"
|
|
144
|
-
/>
|
|
145
|
-
</div>
|
|
146
|
-
<div v-else>
|
|
147
|
-
<div class="text-gray-400 dark:text-gray-500 flex flex-col items-center justify-center w-full h-[258px] bg-gray-50 dark:bg-gray-700 border border-dashed border-gray-200 dark:border-gray-600">
|
|
148
|
-
<svg
|
|
149
|
-
class="w-20 h-20 mb-2 stroke-1 text-gray-300 dark:text-gray-600"
|
|
150
|
-
fill="none"
|
|
151
|
-
stroke="currentColor"
|
|
152
|
-
viewBox="0 0 24 24"
|
|
153
|
-
>
|
|
154
|
-
<path
|
|
155
|
-
stroke-linecap="round"
|
|
156
|
-
stroke-linejoin="round"
|
|
157
|
-
stroke-width="2"
|
|
158
|
-
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
|
|
159
|
-
/>
|
|
160
|
-
</svg>
|
|
161
|
-
<p class="text-xs font-medium">{{ $t('No images found') }}</p>
|
|
162
|
-
</div>
|
|
163
|
-
</div>
|
|
164
|
-
|
|
165
|
-
<ImageCompare
|
|
166
|
-
v-if="item.openImageCompare?.[n]"
|
|
167
|
-
:meta="props.meta"
|
|
168
|
-
:columnName="n"
|
|
169
|
-
:oldImage="item.oldData?.[n]"
|
|
170
|
-
:newImage="item.data?.[n]"
|
|
171
|
-
@close="item.openImageCompare[n] = false"
|
|
172
|
-
/>
|
|
173
|
-
</div>
|
|
174
|
-
<div v-else>
|
|
175
|
-
<Skeleton type="image" class="w-full h-[258px] rounded-default" />
|
|
176
|
-
</div>
|
|
177
|
-
</div>
|
|
178
|
-
|
|
179
|
-
<div v-else-if="(isAnalyzing(item, n) && !item.regeneratingFieldsStatus?.[n])">
|
|
180
|
-
<div v-if="isInColumnEnum(n)">
|
|
181
|
-
<Select
|
|
182
|
-
v-if="cardValueMode?.[String(item.id)] !== 'old'"
|
|
183
|
-
class="w-full"
|
|
184
|
-
:options="convertColumnEnumToSelectOptions(props.meta.columnEnums, n)"
|
|
185
|
-
v-model="item.data[n]"
|
|
186
|
-
:teleportToTop="true"
|
|
187
|
-
/>
|
|
188
|
-
|
|
189
|
-
<input
|
|
190
|
-
v-else
|
|
191
|
-
type="text"
|
|
192
|
-
:value="item.oldData?.[n] || item.oldData?.[n] === 0 ? item.oldData[n] : $t('no old value')"
|
|
193
|
-
disabled
|
|
194
|
-
class="w-full h-10 px-3 border border-gray-200 dark:border-gray-600 rounded-default bg-gray-50 dark:bg-gray-700 text-gray-400 dark:text-gray-500 text-sm focus:outline-none"
|
|
195
|
-
/>
|
|
196
|
-
</div>
|
|
197
|
-
|
|
198
|
-
<div v-else-if="typeof item.data?.[n] === 'string' || typeof item.data?.[n] === 'object'">
|
|
199
|
-
<textarea
|
|
200
|
-
:value="cardValueMode?.[String(item.id)] === 'old' ? (item.oldData?.[n] || item.oldData?.[n] === 0 ? item.oldData[n] : $t('no old value')) : item.data[n]"
|
|
201
|
-
@input="(e) => { if (cardValueMode?.[String(item.id)] !== 'old') item.data[n] = (e.target as HTMLInputElement).value }"
|
|
202
|
-
:disabled="cardValueMode?.[String(item.id)] === 'old'"
|
|
203
|
-
class="w-full text-sm p-2 border border-gray-200 dark:border-gray-600 rounded-default bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-200 focus:outline-none focus:border-blue-500 dark:focus:border-blue-400 disabled:bg-gray-50 dark:disabled:bg-gray-700 disabled:text-gray-400 dark:disabled:text-gray-500"
|
|
204
|
-
:class="props.meta.isAttachFiles ? 'min-h-[42px]' : 'min-h-[120px]'"
|
|
205
|
-
/>
|
|
206
|
-
</div>
|
|
207
|
-
|
|
208
|
-
<div v-else-if="typeof item.data?.[n] === 'boolean'" class="flex items-center h-10">
|
|
209
|
-
<Toggle v-model="item.data[n]" />
|
|
210
|
-
</div>
|
|
211
|
-
|
|
212
|
-
<div v-else>
|
|
213
|
-
<input
|
|
214
|
-
:type="cardValueMode?.[String(item.id)] === 'old' && !item.oldData?.[n] && item.oldData?.[n] !== 0 ? 'text' : 'number'"
|
|
215
|
-
:value="cardValueMode?.[String(item.id)] === 'old' ? (item.oldData?.[n] || item.oldData?.[n] === 0 ? item.oldData[n] : $t('no old value')) : item.data[n]"
|
|
216
|
-
@input="(e) => { if (cardValueMode?.[String(item.id)] !== 'old') item.data[n] = Number((e.target as HTMLInputElement).value) }"
|
|
217
|
-
:disabled="cardValueMode?.[String(item.id)] === 'old'"
|
|
218
|
-
class="w-full h-10 px-3 border border-gray-200 dark:border-gray-600 rounded-default bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-200 focus:outline-none focus:border-blue-500 dark:focus:border-blue-400 disabled:bg-gray-50 dark:disabled:bg-gray-700 disabled:text-gray-400 dark:disabled:text-gray-500 text-sm"
|
|
219
|
-
/>
|
|
220
|
-
</div>
|
|
221
|
-
</div>
|
|
222
|
-
|
|
223
|
-
<div v-else>
|
|
224
|
-
<Skeleton type="input" class="w-full h-10 rounded-default" />
|
|
225
|
-
</div>
|
|
226
|
-
</div>
|
|
227
|
-
</div>
|
|
228
|
-
|
|
229
|
-
</div>
|
|
230
|
-
</div>
|
|
231
|
-
|
|
232
|
-
<div class="record-actions flex justify-end shrink-0">
|
|
233
|
-
<button
|
|
234
|
-
@click="() => canRegenerate(item) && regenerateRecord(item.id)"
|
|
235
|
-
:disabled="!canRegenerate(item)"
|
|
236
|
-
class="px-3 py-1.5 text-xs font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-default hover:bg-gray-50 dark:hover:bg-gray-600 flex items-center gap-1.5 transition disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-white dark:disabled:hover:bg-gray-700"
|
|
237
|
-
>
|
|
238
|
-
<IconRefreshOutline class="w-3.5 h-3.5" /> {{ $t('Regenerate') }}
|
|
239
|
-
</button>
|
|
240
|
-
</div>
|
|
241
|
-
</div>
|
|
242
|
-
</div>
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
<Teleport to="body">
|
|
247
|
-
<transition name="fade">
|
|
248
|
-
<div v-if="zoomedImage" class="zoom-overlay fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm" @click="closeZoom">
|
|
249
|
-
<transition name="zoom">
|
|
250
|
-
<img v-if="zoomedImage" :src="zoomedImage" class="max-w-[90vw] max-h-[90vh] rounded-default object-contain shadow-2xl cursor-zoom-out" @click.stop="closeZoom" />
|
|
251
|
-
</transition>
|
|
252
|
-
</div>
|
|
253
|
-
</transition>
|
|
254
|
-
</Teleport>
|
|
255
|
-
|
|
256
|
-
<nav v-show="totalItems > 0" class="pagination-nav bg-lightTableBackground dark:bg-darkTableBackground py-3 flex flex-col gap-2 items-center sm:flex-row justify-center sm:justify-between px-4 mt-4 rounded-default">
|
|
257
|
-
<span class="text-sm font-normal text-center text-lightTablePaginationText dark:text-darkTablePaginationText sm:mb-4 md:mb-0 block w-full md:inline md:w-auto">
|
|
258
|
-
{{ $t('Showing') }}
|
|
259
|
-
<span class="font-semibold text-lightTablePaginationNumeration dark:text-darkTablePaginationNumeration">{{ fromIndex }}</span>
|
|
260
|
-
{{ $t('to') }}
|
|
261
|
-
<span class="font-semibold text-lightTablePaginationNumeration dark:text-darkTablePaginationNumeration">{{ toIndex }}</span>
|
|
262
|
-
{{ $t('of') }}
|
|
263
|
-
<span class="font-semibold text-lightTablePaginationNumeration dark:text-darkTablePaginationNumeration">{{ totalItems }}</span>
|
|
264
|
-
</span>
|
|
265
|
-
<div class="af-pagination-container flex flex-row items-center gap-3">
|
|
266
|
-
<div class="inline-flex">
|
|
267
|
-
<button
|
|
268
|
-
class="flex items-center py-1 px-3 gap-1 text-sm font-medium text-lightActivePaginationButtonText bg-lightActivePaginationButtonBackground border-r-0 rounded-s hover:opacity-90 dark:bg-darkActivePaginationButtonBackground dark:text-darkActivePaginationButtonText disabled:opacity-50"
|
|
269
|
-
:disabled="currentPage <= 1"
|
|
270
|
-
@click="previousPage"
|
|
271
|
-
>
|
|
272
|
-
<svg class="w-3.5 h-3.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 10">
|
|
273
|
-
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 5H1m0 0 4 4M1 5l4-4"/>
|
|
274
|
-
</svg>
|
|
275
|
-
</button>
|
|
276
|
-
<button
|
|
277
|
-
class="flex items-center py-1 px-3 text-sm font-medium text-lightUnactivePaginationButtonText focus:outline-none bg-lightUnactivePaginationButtonBackground border-r-0 border border-lightUnactivePaginationButtonBorder hover:bg-lightUnactivePaginationButtonHoverBackground hover:text-lightUnactivePaginationButtonHoverText dark:bg-darkUnactivePaginationButtonBackground dark:text-darkUnactivePaginationButtonText dark:border-darkUnactivePaginationButtonBorder dark:hover:text-darkUnactivePaginationButtonHoverText dark:hover:bg-darkUnactivePaginationButtonHoverBackground disabled:opacity-50"
|
|
278
|
-
:disabled="currentPage <= 1"
|
|
279
|
-
@click="goToPage(1)"
|
|
280
|
-
>1</button>
|
|
281
|
-
<input
|
|
282
|
-
type="text"
|
|
283
|
-
v-model="pageInput"
|
|
284
|
-
:style="{ width: `${Math.max(1, pageInput.length + 4)}ch` }"
|
|
285
|
-
class="min-w-10 outline-none inline-block w-auto py-1.5 px-3 text-sm text-center text-lightTablePaginationInputText border border-lightTablePaginationInputBorder bg-lightTablePaginationInputBackground dark:border-darkTablePaginationInputBorder dark:text-darkTablePaginationInputText dark:bg-darkTablePaginationInputBackground z-10"
|
|
286
|
-
@keydown="onPageKeydown"
|
|
287
|
-
@blur="validatePageInput"
|
|
288
|
-
/>
|
|
289
|
-
<button
|
|
290
|
-
class="flex items-center py-1 px-3 text-sm font-medium text-lightUnactivePaginationButtonText focus:outline-none bg-lightUnactivePaginationButtonBackground border-l-0 border border-lightUnactivePaginationButtonBorder hover:bg-lightUnactivePaginationButtonHoverBackground hover:text-lightUnactivePaginationButtonHoverText dark:bg-darkUnactivePaginationButtonBackground dark:text-darkUnactivePaginationButtonText dark:border-darkUnactivePaginationButtonBorder dark:hover:text-darkUnactivePaginationButtonHoverText dark:hover:bg-darkUnactivePaginationButtonHoverBackground disabled:opacity-50"
|
|
291
|
-
:disabled="currentPage >= totalPages"
|
|
292
|
-
@click="goToPage(totalPages)"
|
|
293
|
-
>{{ totalPages }}</button>
|
|
294
|
-
<button
|
|
295
|
-
class="flex items-center py-1 px-3 gap-1 text-sm font-medium text-lightActivePaginationButtonText focus:outline-none bg-lightActivePaginationButtonBackground border-l-0 rounded-e hover:opacity-90 dark:bg-darkActivePaginationButtonBackground dark:text-darkActivePaginationButtonText disabled:opacity-50"
|
|
296
|
-
:disabled="currentPage >= totalPages"
|
|
297
|
-
@click="nextPage"
|
|
298
|
-
>
|
|
299
|
-
<svg class="w-3.5 h-3.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 10">
|
|
300
|
-
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 5h12m0 0L9 1m4 4L9 9"/>
|
|
301
|
-
</svg>
|
|
302
|
-
</button>
|
|
303
|
-
</div>
|
|
304
|
-
</div>
|
|
305
|
-
</nav>
|
|
306
|
-
</div>
|
|
307
|
-
</template>
|
|
308
|
-
|
|
309
|
-
<script lang="ts" setup>
|
|
310
|
-
import { ref, watch, computed, reactive } from 'vue'
|
|
311
|
-
import { Select, Input, Textarea, Table, Checkbox, Skeleton, Toggle, Tooltip } from '@/afcl'
|
|
312
|
-
import ImageCompare from './ImageCompare.vue';
|
|
313
|
-
import { IconRefreshOutline, IconExclamationCircleSolid } from '@iconify-prerendered/vue-flowbite';
|
|
314
|
-
|
|
315
|
-
const props = defineProps<{
|
|
316
|
-
meta: any,
|
|
317
|
-
tableHeaders: any,
|
|
318
|
-
records: any[],
|
|
319
|
-
customFieldNames: any,
|
|
320
|
-
isError: boolean,
|
|
321
|
-
errorMessage: string
|
|
322
|
-
regenerateImagesRefreshRate: number
|
|
323
|
-
isImageHasPreviewUrl: Record<string, boolean>
|
|
324
|
-
imageGenerationPrompts: Record<string, any>
|
|
325
|
-
outputImageFields: string[],
|
|
326
|
-
outputFieldsForAnalizeFromImages: string[],
|
|
327
|
-
outputPlainFields: string[],
|
|
328
|
-
overwriteExistingValues: boolean,
|
|
329
|
-
}>();
|
|
330
|
-
const emit = defineEmits(['error', 'regenerateImages', 'regenerateCell', 'regenerateRecord']);
|
|
331
|
-
|
|
332
|
-
function regenerateRecord(recordId: any) {
|
|
333
|
-
emit('regenerateRecord', { recordId });
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
function canRegenerate(item: any): boolean {
|
|
337
|
-
if (item.status === 'processing') return false;
|
|
338
|
-
if (item.status === 'completed' || item.status === 'failed') return true;
|
|
339
|
-
if (props.overwriteExistingValues) return true;
|
|
340
|
-
for (const n of props.customFieldNames) {
|
|
341
|
-
const val = item.data?.[n];
|
|
342
|
-
if (val === null || val === undefined || val === '') return true;
|
|
343
|
-
}
|
|
344
|
-
return false;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
const DEFAULT_PAGE_SIZE = 6;
|
|
348
|
-
const initialPageSize = props.meta?.pageSize ?? DEFAULT_PAGE_SIZE;
|
|
349
|
-
console.log(props.meta?.pageSize);
|
|
350
|
-
|
|
351
|
-
const pagination = reactive({ offset: 0, limit: initialPageSize });
|
|
352
|
-
const zoomedImage = ref(null);
|
|
353
|
-
const hovers = ref<Record<string, Record<string, boolean>>>({});
|
|
354
|
-
const tableRef = ref(null);
|
|
355
|
-
const cardValueMode = ref<Record<string, 'old' | 'new'>>({});
|
|
356
|
-
|
|
357
|
-
defineExpose({
|
|
358
|
-
refresh() {
|
|
359
|
-
if (tableRef.value) {
|
|
360
|
-
tableRef.value.refreshTable();
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
const paginatedRecords = computed(() => props.records.slice(pagination.offset, pagination.offset + pagination.limit));
|
|
366
|
-
|
|
367
|
-
const imageLoadErrors = ref<Record<string, Record<string, boolean>>>({})
|
|
368
|
-
|
|
369
|
-
const totalItems = computed(() => props.records.length);
|
|
370
|
-
const currentPage = computed(() => Math.floor(pagination.offset / pagination.limit) + 1);
|
|
371
|
-
const totalPages = computed(() => Math.max(1, Math.ceil(totalItems.value / pagination.limit)));
|
|
372
|
-
const fromIndex = computed(() => totalItems.value === 0 ? 0 : Math.min(pagination.offset + 1, totalItems.value));
|
|
373
|
-
const toIndex = computed(() => Math.min(pagination.offset + pagination.limit, totalItems.value));
|
|
374
|
-
const tableDataProvider = async ({ offset, limit }) => {
|
|
375
|
-
pagination.offset = offset;
|
|
376
|
-
pagination.limit = limit;
|
|
377
|
-
return {
|
|
378
|
-
data: paginatedRecords.value,
|
|
379
|
-
total: props.records.length,
|
|
380
|
-
};
|
|
381
|
-
};
|
|
382
|
-
|
|
383
|
-
watch(() => props.records.length, (newLength) => {
|
|
384
|
-
const maxOffset = Math.max(0, (Math.ceil(newLength / pagination.limit) - 1) * pagination.limit);
|
|
385
|
-
if (pagination.offset > maxOffset) {
|
|
386
|
-
pagination.offset = maxOffset;
|
|
387
|
-
}
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
watch(() => props.meta?.pageSize, (newSize) => {
|
|
391
|
-
const size = Number(newSize) || DEFAULT_PAGE_SIZE;
|
|
392
|
-
pagination.limit = size;
|
|
393
|
-
const maxOffset = Math.max(0, (Math.ceil(props.records.length / pagination.limit) - 1) * pagination.limit);
|
|
394
|
-
if (pagination.offset > maxOffset) {
|
|
395
|
-
pagination.offset = maxOffset;
|
|
396
|
-
}
|
|
397
|
-
}, { immediate: true });
|
|
398
|
-
|
|
399
|
-
watch(() => props.records, (newVal) => {
|
|
400
|
-
hovers.value = Object.fromEntries((newVal || []).map(record => [String(record.id), {}]));
|
|
401
|
-
const newCardValueMode = {};
|
|
402
|
-
(newVal || []).forEach(record => {
|
|
403
|
-
newCardValueMode[String(record.id)] = 'new';
|
|
404
|
-
});
|
|
405
|
-
cardValueMode.value = newCardValueMode;
|
|
406
|
-
}, { immediate: true, deep: true });
|
|
407
|
-
|
|
408
|
-
function zoomImage(img) {
|
|
409
|
-
zoomedImage.value = img
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
function closeZoom() {
|
|
413
|
-
zoomedImage.value = null
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
function isInColumnEnum(key: string): boolean {
|
|
417
|
-
const colEnum = props.meta.columnEnums?.find(c => c.name === key);
|
|
418
|
-
if (!colEnum) {
|
|
419
|
-
return false;
|
|
420
|
-
}
|
|
421
|
-
return true;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
function isInColumnImage(key: string): boolean {
|
|
425
|
-
return props.meta.outputImageFields?.includes(key) || false;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
const hasNonImageOutputFields = computed(() => {
|
|
429
|
-
if (!props.customFieldNames) return false;
|
|
430
|
-
|
|
431
|
-
for (const fieldName of props.customFieldNames) {
|
|
432
|
-
const isImage = isInColumnImage(fieldName);
|
|
433
|
-
if (!isImage) return true;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
return false;
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
function convertColumnEnumToSelectOptions(columnEnumArray: any[], key: string) {
|
|
440
|
-
const col = columnEnumArray.find(c => c.name === key);
|
|
441
|
-
if (!col) return [];
|
|
442
|
-
return col.enum.map(item => ({
|
|
443
|
-
label: item.label,
|
|
444
|
-
value: item.value
|
|
445
|
-
}));
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
function isValidUrl(str: string): boolean {
|
|
449
|
-
try {
|
|
450
|
-
new URL(str);
|
|
451
|
-
return true;
|
|
452
|
-
} catch {
|
|
453
|
-
return false;
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
function regerenerateFieldIconClick(item, name) {
|
|
458
|
-
if (shouldDisableRegenerateFieldIcon(item, name)) {
|
|
459
|
-
return;
|
|
460
|
-
}
|
|
461
|
-
emit('regenerateCell', {
|
|
462
|
-
recordId: item.id,
|
|
463
|
-
fieldName: name
|
|
464
|
-
});
|
|
465
|
-
};
|
|
466
|
-
|
|
467
|
-
function shouldDisableRegenerateFieldIcon(item, name) {
|
|
468
|
-
if (props.outputFieldsForAnalizeFromImages.findIndex( el => el === name) !== -1 &&
|
|
469
|
-
item.imageToTextErrorMessages?.[name] === 'No source images found') {
|
|
470
|
-
return true;
|
|
471
|
-
}
|
|
472
|
-
return false;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
function checkForError(item, name) {
|
|
476
|
-
if (props.outputFieldsForAnalizeFromImages.findIndex( el => el === name) !== -1) {
|
|
477
|
-
const errorMessage = item.imageToTextErrorMessages?.[name];
|
|
478
|
-
if (errorMessage) {
|
|
479
|
-
return errorMessage;
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
if (props.outputPlainFields.findIndex( el => el === name) !== -1) {
|
|
483
|
-
const errorMessage = item.textToTextErrorMessages?.[name];
|
|
484
|
-
if (errorMessage) {
|
|
485
|
-
return errorMessage;
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
return false;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
function isAnalyzing(item, name) {
|
|
492
|
-
if (props.outputFieldsForAnalizeFromImages.findIndex( el => el === name) !== -1) {
|
|
493
|
-
return isImagesAnalyzing(item);
|
|
494
|
-
}
|
|
495
|
-
if (props.outputPlainFields.findIndex( el => el === name) !== -1) {
|
|
496
|
-
return isNoImageAnalyzing(item);
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
return false;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
function isImagesAnalyzing(item) {
|
|
503
|
-
return item.aiStatus.analyzedImages;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
function isNoImageAnalyzing(item) {
|
|
507
|
-
return item.aiStatus.analyzedNoImages;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
function setHover(recordId: string | number, field: string, value: boolean) {
|
|
511
|
-
const key = String(recordId);
|
|
512
|
-
if (!hovers.value[key]) {
|
|
513
|
-
hovers.value[key] = {};
|
|
514
|
-
}
|
|
515
|
-
hovers.value[key][field] = value;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
function isHovered(recordId: string | number, field: string) {
|
|
519
|
-
return Boolean(hovers.value?.[String(recordId)]?.[field]);
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
function toggleCardValueMode(recordId: string | number) {
|
|
523
|
-
const key = String(recordId);
|
|
524
|
-
const currentMode = cardValueMode.value[key] || 'new';
|
|
525
|
-
cardValueMode.value[key] = currentMode === 'new' ? 'old' : 'new';
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
const pageInput = ref(String(currentPage.value));
|
|
529
|
-
watch(currentPage, (val) => { pageInput.value = String(val); });
|
|
530
|
-
|
|
531
|
-
function goToPage(page: number) {
|
|
532
|
-
const clampedPage = Math.min(Math.max(page, 1), totalPages.value);
|
|
533
|
-
pagination.offset = (clampedPage - 1) * pagination.limit;
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
function previousPage() {
|
|
537
|
-
goToPage(currentPage.value - 1);
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
function nextPage() {
|
|
541
|
-
goToPage(currentPage.value + 1);
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
function onPageKeydown(e: KeyboardEvent) {
|
|
545
|
-
if (e.key === 'Enter') validatePageInput();
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
function validatePageInput() {
|
|
549
|
-
const parsed = parseInt(pageInput.value);
|
|
550
|
-
if (!isNaN(parsed)) goToPage(parsed);
|
|
551
|
-
pageInput.value = String(currentPage.value);
|
|
552
|
-
}
|
|
553
|
-
</script>
|
|
554
|
-
|
|
555
|
-
<style scoped>
|
|
556
|
-
.fade-enter-active, .fade-leave-active {
|
|
557
|
-
transition: opacity 0.2s ease;
|
|
558
|
-
}
|
|
559
|
-
.fade-enter-from, .fade-leave-to {
|
|
560
|
-
opacity: 0;
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
.zoom-enter-active, .zoom-leave-active {
|
|
564
|
-
transition: transform 0.2s ease, opacity 0.2s ease;
|
|
565
|
-
}
|
|
566
|
-
.zoom-enter-from, .zoom-leave-to {
|
|
567
|
-
transform: scale(0.95);
|
|
568
|
-
opacity: 0;
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
.overflow-auto {
|
|
572
|
-
scrollbar-width: thin;
|
|
573
|
-
scrollbar-color: #d1d5db transparent;
|
|
574
|
-
}
|
|
575
|
-
</style>
|