@adminforth/bulk-ai-flow 1.12.0 → 1.13.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 CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  sending incremental file list
6
6
  custom/
7
+ custom/ImageCompare.vue
7
8
  custom/ImageGenerationCarousel.vue
8
9
  custom/Swiper.vue
9
10
  custom/VisionAction.vue
@@ -12,5 +13,5 @@ custom/package-lock.json
12
13
  custom/package.json
13
14
  custom/tsconfig.json
14
15
 
15
- sent 62,830 bytes received 153 bytes 125,966.00 bytes/sec
16
- total size is 62,264 speedup is 0.99
16
+ sent 73,072 bytes received 172 bytes 146,488.00 bytes/sec
17
+ total size is 72,431 speedup is 0.99
@@ -0,0 +1,196 @@
1
+ <template>
2
+ <!-- Popup Overlay -->
3
+ <div class="fixed inset-0 z-40 flex items-center justify-center bg-black/50" @click.self="closePopup">
4
+ <div class="image-compare-container max-w-4xl max-h-[90vh] overflow-y-auto">
5
+ <!-- Close Button -->
6
+ <div class="flex justify-end mb-4">
7
+ <button type="button"
8
+ @click="closePopup"
9
+ class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white" >
10
+ <svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
11
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
12
+ </svg>
13
+ <span class="sr-only">Close modal</span>
14
+ </button>
15
+ </div>
16
+ <div class="flex gap-4 items-start justify-between">
17
+ <h3 class="text-sm font-medium text-gray-700 mb-2">Old Image</h3>
18
+ <h3 class="text-sm font-medium text-gray-700 mb-2">New Image</h3>
19
+ </div>
20
+ <div class="flex gap-4 items-center">
21
+ <!-- Old Image -->
22
+ <div class="flex-1">
23
+ <div class="relative">
24
+ <img
25
+ v-if="isValidUrl(compiledOldImage)"
26
+ ref="oldImg"
27
+ :src="compiledOldImage"
28
+ alt="Old image"
29
+ class="w-full max-w-sm h-auto object-cover rounded-lg cursor-pointer border hover:border-blue-500 transition-colors duration-200"
30
+ />
31
+ <div v-else class="w-full max-w-sm h-48 bg-gray-100 rounded-lg flex items-center justify-center">
32
+ <p class="text-gray-500">No old image</p>
33
+ </div>
34
+ </div>
35
+ </div>
36
+
37
+ <!-- Comparison Arrow -->
38
+ <div class="flex items-center justify-center">
39
+ <div class="w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center">
40
+ <svg class="w-4 h-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
41
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
42
+ </svg>
43
+ </div>
44
+ </div>
45
+
46
+ <!-- New Image -->
47
+ <div class="flex-1">
48
+ <div class="relative">
49
+ <img
50
+ v-if="isValidUrl(newImage)"
51
+ ref="newImg"
52
+ :src="newImage"
53
+ alt="New image"
54
+ class="w-full max-w-sm h-auto object-cover rounded-lg cursor-pointer border hover:border-blue-500 transition-colors duration-200"
55
+ />
56
+ <div v-else class="w-full max-w-sm h-48 bg-gray-100 rounded-lg flex items-center justify-center">
57
+ <p class="text-gray-500">No new image</p>
58
+ </div>
59
+ </div>
60
+ </div>
61
+ </div>
62
+ </div>
63
+ </div>
64
+
65
+
66
+ </template>
67
+
68
+ <script setup lang="ts">
69
+ import { ref, onMounted, watch, nextTick } from 'vue'
70
+ import mediumZoom from 'medium-zoom'
71
+ import { callAdminForthApi } from '@/utils';
72
+
73
+ const props = defineProps<{
74
+ oldImage: string
75
+ newImage: string
76
+ meta: any
77
+ columnName: string
78
+ }>()
79
+
80
+ const emit = defineEmits<{
81
+ close: []
82
+ }>()
83
+
84
+ const oldImg = ref<HTMLImageElement | null>(null)
85
+ const newImg = ref<HTMLImageElement | null>(null)
86
+ const oldZoom = ref<any>(null)
87
+ const newZoom = ref<any>(null)
88
+ const compiledOldImage = ref<string>('')
89
+
90
+ async function compileOldImage() {
91
+ try {
92
+ const res = await callAdminForthApi({
93
+ path: `/plugin/${props.meta.pluginInstanceId}/compile_old_image_link`,
94
+ method: 'POST',
95
+ body: {
96
+ image: props.oldImage,
97
+ columnName: props.columnName,
98
+ },
99
+ });
100
+ compiledOldImage.value = res.previewUrl;
101
+ } catch (e) {
102
+ console.error("Error compiling old image:", e)
103
+ return;
104
+ }
105
+ }
106
+
107
+ function closePopup() {
108
+ emit('close')
109
+ }
110
+
111
+ function isValidUrl(str: string): boolean {
112
+ if (!str) return false
113
+ try {
114
+ new URL(str)
115
+ return true
116
+ } catch {
117
+ return false
118
+ }
119
+ }
120
+
121
+ function initializeZoom() {
122
+ // Clean up existing zoom instances
123
+ if (oldZoom.value) {
124
+ oldZoom.value.detach()
125
+ }
126
+ if (newZoom.value) {
127
+ newZoom.value.detach()
128
+ }
129
+
130
+ // Initialize zoom for old image
131
+ if (oldImg.value && isValidUrl(compiledOldImage.value)) {
132
+ oldZoom.value = mediumZoom(oldImg.value, {
133
+ margin: 24,
134
+ background: 'rgba(0, 0, 0, 0.8)',
135
+ scrollOffset: 150
136
+ })
137
+ }
138
+
139
+ // Initialize zoom for new image
140
+ if (newImg.value && isValidUrl(props.newImage)) {
141
+ newZoom.value = mediumZoom(newImg.value, {
142
+ margin: 24,
143
+ background: 'rgba(0, 0, 0, 0.8)',
144
+ scrollOffset: 150
145
+ })
146
+ }
147
+ }
148
+
149
+ onMounted(async () => {
150
+ await compileOldImage()
151
+ await nextTick()
152
+ initializeZoom()
153
+ })
154
+
155
+ // Re-initialize zoom when images change
156
+ watch([() => props.oldImage, () => props.newImage, () => compiledOldImage.value], async () => {
157
+ await nextTick()
158
+ initializeZoom()
159
+ })
160
+ </script>
161
+
162
+ <style>
163
+ .medium-zoom-image {
164
+ z-index: 999999 !important;
165
+ background: rgba(0, 0, 0, 0.8);
166
+ border: none !important;
167
+ border-radius: 0 !important;
168
+ }
169
+ .medium-zoom-overlay {
170
+ z-index: 99999 !important;
171
+ background: rgba(0, 0, 0, 0.8) !important;
172
+ }
173
+ html.dark .medium-zoom-overlay {
174
+ background: rgba(17, 24, 39, 0.8) !important;
175
+ }
176
+ body.medium-zoom--opened aside {
177
+ filter: grayscale(1);
178
+ }
179
+ </style>
180
+
181
+ <style scoped>
182
+ .image-compare-container {
183
+ padding: 1rem;
184
+ background-color: white;
185
+ box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
186
+ border: 1px solid #e5e7eb;
187
+ }
188
+
189
+ .fade-enter-active, .fade-leave-active {
190
+ transition: opacity 0.3s ease;
191
+ }
192
+
193
+ .fade-enter-from, .fade-leave-to {
194
+ opacity: 0;
195
+ }
196
+ </style>
@@ -28,10 +28,12 @@
28
28
  :customFieldNames="customFieldNames"
29
29
  :tableColumnsIndexes="tableColumnsIndexes"
30
30
  :selected="selected"
31
+ :oldData="oldData"
31
32
  :isAiResponseReceivedAnalize="isAiResponseReceivedAnalize"
32
33
  :isAiResponseReceivedImage="isAiResponseReceivedImage"
33
34
  :primaryKey="primaryKey"
34
35
  :openGenerationCarousel="openGenerationCarousel"
36
+ :openImageCompare="openImageCompare"
35
37
  @error="handleTableError"
36
38
  :carouselSaveImages="carouselSaveImages"
37
39
  :carouselImageIndex="carouselImageIndex"
@@ -41,6 +43,7 @@
41
43
  :isAiImageGenerationError="isAiImageGenerationError"
42
44
  :imageGenerationErrorMessage="imageGenerationErrorMessage"
43
45
  @regenerate-images="regenerateImages"
46
+ :isImageHasPreviewUrl="isImageHasPreviewUrl"
44
47
  />
45
48
  </div>
46
49
  <div class="text-red-600 flex items-center w-full">
@@ -83,12 +86,14 @@ const tableColumns = ref([]);
83
86
  const tableColumnsIndexes = ref([]);
84
87
  const customFieldNames = ref([]);
85
88
  const selected = ref<any[]>([]);
89
+ const oldData = ref<any[]>([]);
86
90
  const carouselSaveImages = ref<any[]>([]);
87
91
  const carouselImageIndex = ref<any[]>([]);
88
92
  const isAiResponseReceivedAnalize = ref([]);
89
93
  const isAiResponseReceivedImage = ref([]);
90
94
  const primaryKey = props.meta.primaryKey;
91
95
  const openGenerationCarousel = ref([]);
96
+ const openImageCompare = ref([]);
92
97
  const isLoading = ref(false);
93
98
  const isFetchingRecords = ref(false);
94
99
  const isError = ref(false);
@@ -104,6 +109,7 @@ const isAiGenerationError = ref<boolean[]>([false]);
104
109
  const aiGenerationErrorMessage = ref<string[]>([]);
105
110
  const isAiImageGenerationError = ref<boolean[]>([false]);
106
111
  const imageGenerationErrorMessage = ref<string[]>([]);
112
+ const isImageHasPreviewUrl = ref<Record<string, boolean>>({});
107
113
 
108
114
  const openDialog = async () => {
109
115
  isDialogOpen.value = true;
@@ -113,6 +119,7 @@ const openDialog = async () => {
113
119
  if (props.meta.isAttachFiles) {
114
120
  await getImages();
115
121
  }
122
+ await findPreviewURLForImages();
116
123
  tableHeaders.value = generateTableHeaders(props.meta.outputFields);
117
124
  const result = generateTableColumns();
118
125
  tableColumns.value = result.tableData;
@@ -127,6 +134,10 @@ const openDialog = async () => {
127
134
  acc[key] = false;
128
135
  return acc;
129
136
  },{[primaryKey]: records.value[i][primaryKey]} as Record<string, boolean>);
137
+ openImageCompare.value[i] = props.meta.outputImageFields?.reduce((acc,key) =>{
138
+ acc[key] = false;
139
+ return acc;
140
+ },{[primaryKey]: records.value[i][primaryKey]} as Record<string, boolean>);
130
141
  }
131
142
  isFetchingRecords.value = false;
132
143
 
@@ -257,7 +268,7 @@ function setSelected() {
257
268
  }
258
269
  selected.value[index].isChecked = true;
259
270
  selected.value[index][primaryKey] = record[primaryKey];
260
- isAiResponseReceivedAnalize.value[index] = true;
271
+ oldData.value[index] = { ...selected.value[index] };
261
272
  });
262
273
  }
263
274
 
@@ -710,4 +721,28 @@ function regenerateImages(recordInfo: any) {
710
721
  });
711
722
  }
712
723
 
724
+ async function findPreviewURLForImages() {
725
+ if (props.meta.outputImageFields){
726
+ for (const fieldName of props.meta.outputImageFields) {
727
+ try {
728
+ const res = await callAdminForthApi({
729
+ path: `/plugin/${props.meta.pluginInstanceId}/compile_old_image_link`,
730
+ method: 'POST',
731
+ body: {
732
+ image: "test",
733
+ columnName: fieldName,
734
+ },
735
+ });
736
+ if (res?.ok) {
737
+ isImageHasPreviewUrl.value[fieldName] = true;
738
+ } else {
739
+ isImageHasPreviewUrl.value[fieldName] = false;
740
+ }
741
+ } catch (e) {
742
+ console.error("Error finding preview URL for field", fieldName, e);
743
+ }
744
+ }
745
+ }
746
+ }
747
+
713
748
  </script>
@@ -55,48 +55,95 @@
55
55
  <!-- CUSTOM FIELD TEMPLATES -->
56
56
  <template v-for="n in customFieldNames" :key="n" #[`cell:${n}`]="{ item, column }">
57
57
  <div v-if="isAiResponseReceivedAnalize[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])] && !isInColumnImage(n)">
58
- <div v-if="isInColumnEnum(n)">
59
- <Select
60
- class="min-w-[150px] "
61
- :options="convertColumnEnumToSelectOptions(props.meta.columnEnums, n)"
62
- v-model="selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n]"
63
- :teleportToTop="true"
64
- :teleportToBody="false"
65
- >
66
- </Select>
58
+ <div v-if="isInColumnEnum(n)" class="flex flex-col items-start justify-end min-h-[90px]">
59
+ <Select
60
+ class="min-w-[150px]"
61
+ :options="convertColumnEnumToSelectOptions(props.meta.columnEnums, n)"
62
+ v-model="selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n]"
63
+ :teleportToTop="true"
64
+ :teleportToBody="false"
65
+ >
66
+ </Select>
67
+ <Tooltip>
68
+ <div class="mt-2 flex items-center justify-start gap-1 hover:text-blue-500">
69
+ <p class="text-sm ">original</p>
70
+ <IconScaleBalancedOutline />
71
+ </div>
72
+ <template #tooltip>
73
+ {{ oldData[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] }}
74
+ </template>
75
+ </Tooltip>
67
76
  </div>
68
- <div v-else-if="typeof selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] === 'string' || typeof selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] === 'object'">
77
+ <div v-else-if="typeof selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] === 'string' || typeof selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] === 'object'" class="flex flex-col items-start justify-end min-h-[90px]">
69
78
  <Textarea
70
- class="min-w-[150px] w-full h-full"
79
+ class="min-w-[150px] h-full"
71
80
  type="text"
72
81
  v-model="selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n]"
73
82
  >
74
83
  </Textarea>
75
- </div>
76
- <div v-else-if="typeof selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] === 'boolean'">
84
+ <Tooltip>
85
+ <div class="mt-2 flex items-center justify-start gap-1 hover:text-blue-500">
86
+ <p class="text-sm ">original</p>
87
+ <IconScaleBalancedOutline />
88
+ </div>
89
+ <template #tooltip>
90
+ <p class="max-w-[200px]">{{ oldData[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] }}</p>
91
+ </template>
92
+ </Tooltip>
93
+ </div>
94
+ <div v-else-if="typeof selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] === 'boolean'" class="flex flex-col items-start justify-end min-h-[90px]">
77
95
  <Toggle
96
+ class="p-2"
78
97
  v-model="selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n]"
79
98
  >
80
99
  </Toggle>
100
+ <Tooltip>
101
+ <div class="mt-2 flex items-center justify-start gap-1 hover:text-blue-500">
102
+ <p class="text-sm ">original</p>
103
+ <IconScaleBalancedOutline />
104
+ </div>
105
+ <template #tooltip>
106
+ {{ oldData[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] }}
107
+ </template>
108
+ </Tooltip>
81
109
  </div>
82
- <div v-else>
110
+ <div v-else class="flex flex-col items-start justify-end min-h-[90px]">
83
111
  <Input
84
112
  type="number"
85
113
  v-model="selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n]"
86
114
  class="w-full min-w-[80px]"
87
115
  :fullWidth="true"
88
116
  />
117
+ <Tooltip>
118
+ <div class="mt-2 flex items-center justify-start gap-1 hover:text-blue-500">
119
+ <p class="text-sm ">original</p>
120
+ <IconScaleBalancedOutline />
121
+ </div>
122
+ <template #tooltip>
123
+ {{ oldData[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] }}
124
+ </template>
125
+ </Tooltip>
89
126
  </div>
90
127
  </div>
91
128
 
92
129
  <div v-if="isAiResponseReceivedImage[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])]">
93
130
  <div v-if="isInColumnImage(n)">
94
131
  <div class="mt-2 flex items-center justify-start gap-2">
95
- <img v-if="isValidUrl(selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n])"
132
+ <div v-if="isValidUrl(selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n])" class="flex flex-col items-center">
133
+ <img
96
134
  :src="selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n]"
97
135
  class="w-20 h-20 object-cover rounded cursor-pointer border hover:border-blue-500 transition"
98
136
  @click="() => {openGenerationCarousel[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] = true}"
99
137
  />
138
+ <p
139
+ v-if="isImageHasPreviewUrl[n]"
140
+ class="mt-2 text-sm hover:text-blue-500 hover:underline hover:cursor-pointer flex items-center gap-1"
141
+ @click="() => {openImageCompare[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] = true}"
142
+ >
143
+ old image
144
+ <IconScaleBalancedOutline />
145
+ </p>
146
+ </div>
100
147
  <div v-else class="flex items-center justify-center text-center w-20 h-20">
101
148
  <Tooltip v-if="imageGenerationErrorMessage[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])] === 'No source images found'">
102
149
  <p
@@ -135,6 +182,14 @@
135
182
  @selectImage="updateSelectedImage"
136
183
  @updateCarouselIndex="updateActiveIndex"
137
184
  />
185
+ <ImageCompare
186
+ v-if="openImageCompare[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n]"
187
+ :meta="props.meta"
188
+ :columnName="n"
189
+ :oldImage="oldData[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n]"
190
+ :newImage="selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n]"
191
+ @close="openImageCompare[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] = false"
192
+ />
138
193
  </div>
139
194
  </div>
140
195
  </div>
@@ -154,7 +209,8 @@
154
209
  import { ref } from 'vue'
155
210
  import { Select, Input, Textarea, Table, Checkbox, Skeleton, Toggle, Tooltip } from '@/afcl'
156
211
  import GenerationCarousel from './ImageGenerationCarousel.vue'
157
- import { IconRefreshOutline } from '@iconify-prerendered/vue-flowbite';
212
+ import ImageCompare from './ImageCompare.vue';
213
+ import { IconRefreshOutline, IconScaleBalancedOutline } from '@iconify-prerendered/vue-flowbite';
158
214
 
159
215
  const props = defineProps<{
160
216
  meta: any,
@@ -166,7 +222,8 @@ const props = defineProps<{
166
222
  isAiResponseReceivedAnalize: boolean[],
167
223
  isAiResponseReceivedImage: boolean[],
168
224
  primaryKey: any,
169
- openGenerationCarousel: any
225
+ openGenerationCarousel: any,
226
+ openImageCompare: any,
170
227
  isError: boolean,
171
228
  errorMessage: string
172
229
  carouselSaveImages: any[]
@@ -175,12 +232,14 @@ const props = defineProps<{
175
232
  isAiGenerationError: boolean[],
176
233
  aiGenerationErrorMessage: string[],
177
234
  isAiImageGenerationError: boolean[],
178
- imageGenerationErrorMessage: string[]
235
+ imageGenerationErrorMessage: string[],
236
+ oldData: any[],
237
+ isImageHasPreviewUrl: Record<string, boolean>
179
238
  }>();
180
239
  const emit = defineEmits(['error', 'regenerateImages']);
181
240
 
182
241
 
183
- const zoomedImage = ref(null)
242
+ const zoomedImage = ref(null);
184
243
 
185
244
 
186
245
  function zoomImage(img) {
@@ -0,0 +1,196 @@
1
+ <template>
2
+ <!-- Popup Overlay -->
3
+ <div class="fixed inset-0 z-40 flex items-center justify-center bg-black/50" @click.self="closePopup">
4
+ <div class="image-compare-container max-w-4xl max-h-[90vh] overflow-y-auto">
5
+ <!-- Close Button -->
6
+ <div class="flex justify-end mb-4">
7
+ <button type="button"
8
+ @click="closePopup"
9
+ class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white" >
10
+ <svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
11
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
12
+ </svg>
13
+ <span class="sr-only">Close modal</span>
14
+ </button>
15
+ </div>
16
+ <div class="flex gap-4 items-start justify-between">
17
+ <h3 class="text-sm font-medium text-gray-700 mb-2">Old Image</h3>
18
+ <h3 class="text-sm font-medium text-gray-700 mb-2">New Image</h3>
19
+ </div>
20
+ <div class="flex gap-4 items-center">
21
+ <!-- Old Image -->
22
+ <div class="flex-1">
23
+ <div class="relative">
24
+ <img
25
+ v-if="isValidUrl(compiledOldImage)"
26
+ ref="oldImg"
27
+ :src="compiledOldImage"
28
+ alt="Old image"
29
+ class="w-full max-w-sm h-auto object-cover rounded-lg cursor-pointer border hover:border-blue-500 transition-colors duration-200"
30
+ />
31
+ <div v-else class="w-full max-w-sm h-48 bg-gray-100 rounded-lg flex items-center justify-center">
32
+ <p class="text-gray-500">No old image</p>
33
+ </div>
34
+ </div>
35
+ </div>
36
+
37
+ <!-- Comparison Arrow -->
38
+ <div class="flex items-center justify-center">
39
+ <div class="w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center">
40
+ <svg class="w-4 h-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
41
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
42
+ </svg>
43
+ </div>
44
+ </div>
45
+
46
+ <!-- New Image -->
47
+ <div class="flex-1">
48
+ <div class="relative">
49
+ <img
50
+ v-if="isValidUrl(newImage)"
51
+ ref="newImg"
52
+ :src="newImage"
53
+ alt="New image"
54
+ class="w-full max-w-sm h-auto object-cover rounded-lg cursor-pointer border hover:border-blue-500 transition-colors duration-200"
55
+ />
56
+ <div v-else class="w-full max-w-sm h-48 bg-gray-100 rounded-lg flex items-center justify-center">
57
+ <p class="text-gray-500">No new image</p>
58
+ </div>
59
+ </div>
60
+ </div>
61
+ </div>
62
+ </div>
63
+ </div>
64
+
65
+
66
+ </template>
67
+
68
+ <script setup lang="ts">
69
+ import { ref, onMounted, watch, nextTick } from 'vue'
70
+ import mediumZoom from 'medium-zoom'
71
+ import { callAdminForthApi } from '@/utils';
72
+
73
+ const props = defineProps<{
74
+ oldImage: string
75
+ newImage: string
76
+ meta: any
77
+ columnName: string
78
+ }>()
79
+
80
+ const emit = defineEmits<{
81
+ close: []
82
+ }>()
83
+
84
+ const oldImg = ref<HTMLImageElement | null>(null)
85
+ const newImg = ref<HTMLImageElement | null>(null)
86
+ const oldZoom = ref<any>(null)
87
+ const newZoom = ref<any>(null)
88
+ const compiledOldImage = ref<string>('')
89
+
90
+ async function compileOldImage() {
91
+ try {
92
+ const res = await callAdminForthApi({
93
+ path: `/plugin/${props.meta.pluginInstanceId}/compile_old_image_link`,
94
+ method: 'POST',
95
+ body: {
96
+ image: props.oldImage,
97
+ columnName: props.columnName,
98
+ },
99
+ });
100
+ compiledOldImage.value = res.previewUrl;
101
+ } catch (e) {
102
+ console.error("Error compiling old image:", e)
103
+ return;
104
+ }
105
+ }
106
+
107
+ function closePopup() {
108
+ emit('close')
109
+ }
110
+
111
+ function isValidUrl(str: string): boolean {
112
+ if (!str) return false
113
+ try {
114
+ new URL(str)
115
+ return true
116
+ } catch {
117
+ return false
118
+ }
119
+ }
120
+
121
+ function initializeZoom() {
122
+ // Clean up existing zoom instances
123
+ if (oldZoom.value) {
124
+ oldZoom.value.detach()
125
+ }
126
+ if (newZoom.value) {
127
+ newZoom.value.detach()
128
+ }
129
+
130
+ // Initialize zoom for old image
131
+ if (oldImg.value && isValidUrl(compiledOldImage.value)) {
132
+ oldZoom.value = mediumZoom(oldImg.value, {
133
+ margin: 24,
134
+ background: 'rgba(0, 0, 0, 0.8)',
135
+ scrollOffset: 150
136
+ })
137
+ }
138
+
139
+ // Initialize zoom for new image
140
+ if (newImg.value && isValidUrl(props.newImage)) {
141
+ newZoom.value = mediumZoom(newImg.value, {
142
+ margin: 24,
143
+ background: 'rgba(0, 0, 0, 0.8)',
144
+ scrollOffset: 150
145
+ })
146
+ }
147
+ }
148
+
149
+ onMounted(async () => {
150
+ await compileOldImage()
151
+ await nextTick()
152
+ initializeZoom()
153
+ })
154
+
155
+ // Re-initialize zoom when images change
156
+ watch([() => props.oldImage, () => props.newImage, () => compiledOldImage.value], async () => {
157
+ await nextTick()
158
+ initializeZoom()
159
+ })
160
+ </script>
161
+
162
+ <style>
163
+ .medium-zoom-image {
164
+ z-index: 999999 !important;
165
+ background: rgba(0, 0, 0, 0.8);
166
+ border: none !important;
167
+ border-radius: 0 !important;
168
+ }
169
+ .medium-zoom-overlay {
170
+ z-index: 99999 !important;
171
+ background: rgba(0, 0, 0, 0.8) !important;
172
+ }
173
+ html.dark .medium-zoom-overlay {
174
+ background: rgba(17, 24, 39, 0.8) !important;
175
+ }
176
+ body.medium-zoom--opened aside {
177
+ filter: grayscale(1);
178
+ }
179
+ </style>
180
+
181
+ <style scoped>
182
+ .image-compare-container {
183
+ padding: 1rem;
184
+ background-color: white;
185
+ box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
186
+ border: 1px solid #e5e7eb;
187
+ }
188
+
189
+ .fade-enter-active, .fade-leave-active {
190
+ transition: opacity 0.3s ease;
191
+ }
192
+
193
+ .fade-enter-from, .fade-leave-to {
194
+ opacity: 0;
195
+ }
196
+ </style>
@@ -28,10 +28,12 @@
28
28
  :customFieldNames="customFieldNames"
29
29
  :tableColumnsIndexes="tableColumnsIndexes"
30
30
  :selected="selected"
31
+ :oldData="oldData"
31
32
  :isAiResponseReceivedAnalize="isAiResponseReceivedAnalize"
32
33
  :isAiResponseReceivedImage="isAiResponseReceivedImage"
33
34
  :primaryKey="primaryKey"
34
35
  :openGenerationCarousel="openGenerationCarousel"
36
+ :openImageCompare="openImageCompare"
35
37
  @error="handleTableError"
36
38
  :carouselSaveImages="carouselSaveImages"
37
39
  :carouselImageIndex="carouselImageIndex"
@@ -41,6 +43,7 @@
41
43
  :isAiImageGenerationError="isAiImageGenerationError"
42
44
  :imageGenerationErrorMessage="imageGenerationErrorMessage"
43
45
  @regenerate-images="regenerateImages"
46
+ :isImageHasPreviewUrl="isImageHasPreviewUrl"
44
47
  />
45
48
  </div>
46
49
  <div class="text-red-600 flex items-center w-full">
@@ -83,12 +86,14 @@ const tableColumns = ref([]);
83
86
  const tableColumnsIndexes = ref([]);
84
87
  const customFieldNames = ref([]);
85
88
  const selected = ref<any[]>([]);
89
+ const oldData = ref<any[]>([]);
86
90
  const carouselSaveImages = ref<any[]>([]);
87
91
  const carouselImageIndex = ref<any[]>([]);
88
92
  const isAiResponseReceivedAnalize = ref([]);
89
93
  const isAiResponseReceivedImage = ref([]);
90
94
  const primaryKey = props.meta.primaryKey;
91
95
  const openGenerationCarousel = ref([]);
96
+ const openImageCompare = ref([]);
92
97
  const isLoading = ref(false);
93
98
  const isFetchingRecords = ref(false);
94
99
  const isError = ref(false);
@@ -104,6 +109,7 @@ const isAiGenerationError = ref<boolean[]>([false]);
104
109
  const aiGenerationErrorMessage = ref<string[]>([]);
105
110
  const isAiImageGenerationError = ref<boolean[]>([false]);
106
111
  const imageGenerationErrorMessage = ref<string[]>([]);
112
+ const isImageHasPreviewUrl = ref<Record<string, boolean>>({});
107
113
 
108
114
  const openDialog = async () => {
109
115
  isDialogOpen.value = true;
@@ -113,6 +119,7 @@ const openDialog = async () => {
113
119
  if (props.meta.isAttachFiles) {
114
120
  await getImages();
115
121
  }
122
+ await findPreviewURLForImages();
116
123
  tableHeaders.value = generateTableHeaders(props.meta.outputFields);
117
124
  const result = generateTableColumns();
118
125
  tableColumns.value = result.tableData;
@@ -127,6 +134,10 @@ const openDialog = async () => {
127
134
  acc[key] = false;
128
135
  return acc;
129
136
  },{[primaryKey]: records.value[i][primaryKey]} as Record<string, boolean>);
137
+ openImageCompare.value[i] = props.meta.outputImageFields?.reduce((acc,key) =>{
138
+ acc[key] = false;
139
+ return acc;
140
+ },{[primaryKey]: records.value[i][primaryKey]} as Record<string, boolean>);
130
141
  }
131
142
  isFetchingRecords.value = false;
132
143
 
@@ -257,7 +268,7 @@ function setSelected() {
257
268
  }
258
269
  selected.value[index].isChecked = true;
259
270
  selected.value[index][primaryKey] = record[primaryKey];
260
- isAiResponseReceivedAnalize.value[index] = true;
271
+ oldData.value[index] = { ...selected.value[index] };
261
272
  });
262
273
  }
263
274
 
@@ -710,4 +721,28 @@ function regenerateImages(recordInfo: any) {
710
721
  });
711
722
  }
712
723
 
724
+ async function findPreviewURLForImages() {
725
+ if (props.meta.outputImageFields){
726
+ for (const fieldName of props.meta.outputImageFields) {
727
+ try {
728
+ const res = await callAdminForthApi({
729
+ path: `/plugin/${props.meta.pluginInstanceId}/compile_old_image_link`,
730
+ method: 'POST',
731
+ body: {
732
+ image: "test",
733
+ columnName: fieldName,
734
+ },
735
+ });
736
+ if (res?.ok) {
737
+ isImageHasPreviewUrl.value[fieldName] = true;
738
+ } else {
739
+ isImageHasPreviewUrl.value[fieldName] = false;
740
+ }
741
+ } catch (e) {
742
+ console.error("Error finding preview URL for field", fieldName, e);
743
+ }
744
+ }
745
+ }
746
+ }
747
+
713
748
  </script>
@@ -55,48 +55,95 @@
55
55
  <!-- CUSTOM FIELD TEMPLATES -->
56
56
  <template v-for="n in customFieldNames" :key="n" #[`cell:${n}`]="{ item, column }">
57
57
  <div v-if="isAiResponseReceivedAnalize[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])] && !isInColumnImage(n)">
58
- <div v-if="isInColumnEnum(n)">
59
- <Select
60
- class="min-w-[150px] "
61
- :options="convertColumnEnumToSelectOptions(props.meta.columnEnums, n)"
62
- v-model="selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n]"
63
- :teleportToTop="true"
64
- :teleportToBody="false"
65
- >
66
- </Select>
58
+ <div v-if="isInColumnEnum(n)" class="flex flex-col items-start justify-end min-h-[90px]">
59
+ <Select
60
+ class="min-w-[150px]"
61
+ :options="convertColumnEnumToSelectOptions(props.meta.columnEnums, n)"
62
+ v-model="selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n]"
63
+ :teleportToTop="true"
64
+ :teleportToBody="false"
65
+ >
66
+ </Select>
67
+ <Tooltip>
68
+ <div class="mt-2 flex items-center justify-start gap-1 hover:text-blue-500">
69
+ <p class="text-sm ">original</p>
70
+ <IconScaleBalancedOutline />
71
+ </div>
72
+ <template #tooltip>
73
+ {{ oldData[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] }}
74
+ </template>
75
+ </Tooltip>
67
76
  </div>
68
- <div v-else-if="typeof selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] === 'string' || typeof selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] === 'object'">
77
+ <div v-else-if="typeof selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] === 'string' || typeof selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] === 'object'" class="flex flex-col items-start justify-end min-h-[90px]">
69
78
  <Textarea
70
- class="min-w-[150px] w-full h-full"
79
+ class="min-w-[150px] h-full"
71
80
  type="text"
72
81
  v-model="selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n]"
73
82
  >
74
83
  </Textarea>
75
- </div>
76
- <div v-else-if="typeof selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] === 'boolean'">
84
+ <Tooltip>
85
+ <div class="mt-2 flex items-center justify-start gap-1 hover:text-blue-500">
86
+ <p class="text-sm ">original</p>
87
+ <IconScaleBalancedOutline />
88
+ </div>
89
+ <template #tooltip>
90
+ <p class="max-w-[200px]">{{ oldData[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] }}</p>
91
+ </template>
92
+ </Tooltip>
93
+ </div>
94
+ <div v-else-if="typeof selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] === 'boolean'" class="flex flex-col items-start justify-end min-h-[90px]">
77
95
  <Toggle
96
+ class="p-2"
78
97
  v-model="selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n]"
79
98
  >
80
99
  </Toggle>
100
+ <Tooltip>
101
+ <div class="mt-2 flex items-center justify-start gap-1 hover:text-blue-500">
102
+ <p class="text-sm ">original</p>
103
+ <IconScaleBalancedOutline />
104
+ </div>
105
+ <template #tooltip>
106
+ {{ oldData[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] }}
107
+ </template>
108
+ </Tooltip>
81
109
  </div>
82
- <div v-else>
110
+ <div v-else class="flex flex-col items-start justify-end min-h-[90px]">
83
111
  <Input
84
112
  type="number"
85
113
  v-model="selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n]"
86
114
  class="w-full min-w-[80px]"
87
115
  :fullWidth="true"
88
116
  />
117
+ <Tooltip>
118
+ <div class="mt-2 flex items-center justify-start gap-1 hover:text-blue-500">
119
+ <p class="text-sm ">original</p>
120
+ <IconScaleBalancedOutline />
121
+ </div>
122
+ <template #tooltip>
123
+ {{ oldData[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] }}
124
+ </template>
125
+ </Tooltip>
89
126
  </div>
90
127
  </div>
91
128
 
92
129
  <div v-if="isAiResponseReceivedImage[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])]">
93
130
  <div v-if="isInColumnImage(n)">
94
131
  <div class="mt-2 flex items-center justify-start gap-2">
95
- <img v-if="isValidUrl(selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n])"
132
+ <div v-if="isValidUrl(selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n])" class="flex flex-col items-center">
133
+ <img
96
134
  :src="selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n]"
97
135
  class="w-20 h-20 object-cover rounded cursor-pointer border hover:border-blue-500 transition"
98
136
  @click="() => {openGenerationCarousel[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] = true}"
99
137
  />
138
+ <p
139
+ v-if="isImageHasPreviewUrl[n]"
140
+ class="mt-2 text-sm hover:text-blue-500 hover:underline hover:cursor-pointer flex items-center gap-1"
141
+ @click="() => {openImageCompare[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] = true}"
142
+ >
143
+ old image
144
+ <IconScaleBalancedOutline />
145
+ </p>
146
+ </div>
100
147
  <div v-else class="flex items-center justify-center text-center w-20 h-20">
101
148
  <Tooltip v-if="imageGenerationErrorMessage[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])] === 'No source images found'">
102
149
  <p
@@ -135,6 +182,14 @@
135
182
  @selectImage="updateSelectedImage"
136
183
  @updateCarouselIndex="updateActiveIndex"
137
184
  />
185
+ <ImageCompare
186
+ v-if="openImageCompare[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n]"
187
+ :meta="props.meta"
188
+ :columnName="n"
189
+ :oldImage="oldData[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n]"
190
+ :newImage="selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n]"
191
+ @close="openImageCompare[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] = false"
192
+ />
138
193
  </div>
139
194
  </div>
140
195
  </div>
@@ -154,7 +209,8 @@
154
209
  import { ref } from 'vue'
155
210
  import { Select, Input, Textarea, Table, Checkbox, Skeleton, Toggle, Tooltip } from '@/afcl'
156
211
  import GenerationCarousel from './ImageGenerationCarousel.vue'
157
- import { IconRefreshOutline } from '@iconify-prerendered/vue-flowbite';
212
+ import ImageCompare from './ImageCompare.vue';
213
+ import { IconRefreshOutline, IconScaleBalancedOutline } from '@iconify-prerendered/vue-flowbite';
158
214
 
159
215
  const props = defineProps<{
160
216
  meta: any,
@@ -166,7 +222,8 @@ const props = defineProps<{
166
222
  isAiResponseReceivedAnalize: boolean[],
167
223
  isAiResponseReceivedImage: boolean[],
168
224
  primaryKey: any,
169
- openGenerationCarousel: any
225
+ openGenerationCarousel: any,
226
+ openImageCompare: any,
170
227
  isError: boolean,
171
228
  errorMessage: string
172
229
  carouselSaveImages: any[]
@@ -175,12 +232,14 @@ const props = defineProps<{
175
232
  isAiGenerationError: boolean[],
176
233
  aiGenerationErrorMessage: string[],
177
234
  isAiImageGenerationError: boolean[],
178
- imageGenerationErrorMessage: string[]
235
+ imageGenerationErrorMessage: string[],
236
+ oldData: any[],
237
+ isImageHasPreviewUrl: Record<string, boolean>
179
238
  }>();
180
239
  const emit = defineEmits(['error', 'regenerateImages']);
181
240
 
182
241
 
183
- const zoomedImage = ref(null)
242
+ const zoomedImage = ref(null);
184
243
 
185
244
 
186
245
  function zoomImage(img) {
package/dist/index.js CHANGED
@@ -444,51 +444,51 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
444
444
  `);
445
445
  }
446
446
  }
447
- if (this.options.fillFieldsFromImages || this.options.fillPlainFields || this.options.generateImages) {
448
- let matches = [];
449
- const regex = /{{(.*?)}}/g;
450
- if (this.options.fillFieldsFromImages) {
451
- for (const [key, value] of Object.entries((this.options.fillFieldsFromImages))) {
452
- const template = value;
453
- const templateMatches = template.match(regex);
454
- if (templateMatches) {
455
- matches.push(...templateMatches);
456
- }
447
+ }
448
+ if (this.options.fillFieldsFromImages || this.options.fillPlainFields || this.options.generateImages) {
449
+ let matches = [];
450
+ const regex = /{{(.*?)}}/g;
451
+ if (this.options.fillFieldsFromImages) {
452
+ for (const [key, value] of Object.entries((this.options.fillFieldsFromImages))) {
453
+ const template = value;
454
+ const templateMatches = template.match(regex);
455
+ if (templateMatches) {
456
+ matches.push(...templateMatches);
457
457
  }
458
458
  }
459
- if (this.options.fillPlainFields) {
460
- for (const [key, value] of Object.entries((this.options.fillPlainFields))) {
461
- const template = value;
462
- const templateMatches = template.match(regex);
463
- if (templateMatches) {
464
- matches.push(...templateMatches);
465
- }
459
+ }
460
+ if (this.options.fillPlainFields) {
461
+ for (const [key, value] of Object.entries((this.options.fillPlainFields))) {
462
+ const template = value;
463
+ const templateMatches = template.match(regex);
464
+ if (templateMatches) {
465
+ matches.push(...templateMatches);
466
466
  }
467
467
  }
468
- if (this.options.generateImages) {
469
- for (const [key, value] of Object.entries((this.options.generateImages))) {
470
- const template = value.prompt;
471
- const templateMatches = template.match(regex);
472
- if (templateMatches) {
473
- matches.push(...templateMatches);
474
- }
468
+ }
469
+ if (this.options.generateImages) {
470
+ for (const [key, value] of Object.entries((this.options.generateImages))) {
471
+ const template = value.prompt;
472
+ const templateMatches = template.match(regex);
473
+ if (templateMatches) {
474
+ matches.push(...templateMatches);
475
475
  }
476
476
  }
477
- if (matches) {
478
- matches.forEach((match) => {
479
- const field = match.replace(/{{|}}/g, '').trim();
480
- if (!resourceConfig.columns.find((column) => column.name === field)) {
481
- const similar = suggestIfTypo(resourceConfig.columns.map((column) => column.name), field);
482
- throw new Error(`Field "${field}" specified in generationPrompt not found in resource "${resourceConfig.label}". ${similar ? `Did you mean "${similar}"?` : ''}`);
483
- }
484
- else {
485
- let column = resourceConfig.columns.find((column) => column.name === field);
486
- if (column.backendOnly === true) {
487
- throw new Error(`Field "${field}" specified in generationPrompt is marked as backendOnly in resource "${resourceConfig.label}". Please remove backendOnly or choose another field.`);
488
- }
477
+ }
478
+ if (matches) {
479
+ matches.forEach((match) => {
480
+ const field = match.replace(/{{|}}/g, '').trim();
481
+ if (!resourceConfig.columns.find((column) => column.name === field)) {
482
+ const similar = suggestIfTypo(resourceConfig.columns.map((column) => column.name), field);
483
+ throw new Error(`Field "${field}" specified in generationPrompt not found in resource "${resourceConfig.label}". ${similar ? `Did you mean "${similar}"?` : ''}`);
484
+ }
485
+ else {
486
+ let column = resourceConfig.columns.find((column) => column.name === field);
487
+ if (column.backendOnly === true) {
488
+ throw new Error(`Field "${field}" specified in generationPrompt is marked as backendOnly in resource "${resourceConfig.label}". Please remove backendOnly or choose another field.`);
489
489
  }
490
- });
491
- }
490
+ }
491
+ });
492
492
  }
493
493
  }
494
494
  }
@@ -713,5 +713,34 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
713
713
  return { ok: true };
714
714
  })
715
715
  });
716
+ server.endpoint({
717
+ method: 'POST',
718
+ path: `/plugin/${this.pluginInstanceId}/compile_old_image_link`,
719
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, headers }) {
720
+ var _b, _c;
721
+ const image = body.image;
722
+ const columnName = body.columnName;
723
+ if (!image) {
724
+ return { ok: false, error: "Can't find image url" };
725
+ }
726
+ if (!columnName) {
727
+ return { ok: false, error: "Can't find column name" };
728
+ }
729
+ try {
730
+ if ((_b = this.options) === null || _b === void 0 ? void 0 : _b.generateImages) {
731
+ const plugin = this.adminforth.activatedPlugins.find(p => p.resourceConfig.resourceId === this.resourceConfig.resourceId &&
732
+ p.pluginOptions.pathColumnName === columnName);
733
+ if ((_c = plugin === null || plugin === void 0 ? void 0 : plugin.pluginOptions) === null || _c === void 0 ? void 0 : _c.preview) {
734
+ const compiledPreviewUrl = plugin.pluginOptions.preview.previewUrl({ filePath: image });
735
+ return { ok: true, previewUrl: compiledPreviewUrl };
736
+ }
737
+ return { ok: false, error: "Can't find plugin for column" };
738
+ }
739
+ }
740
+ catch (e) {
741
+ return { ok: false, error: "Error compiling preview url" };
742
+ }
743
+ })
744
+ });
716
745
  }
717
746
  }
package/index.ts CHANGED
@@ -363,7 +363,6 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
363
363
  ...(this.options.generateImages || {})
364
364
  };
365
365
 
366
-
367
366
  const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
368
367
 
369
368
  const pageInjection = {
@@ -462,52 +461,52 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
462
461
  `);
463
462
  }
464
463
  }
465
- if (this.options.fillFieldsFromImages || this.options.fillPlainFields || this.options.generateImages) {
466
- let matches: string[] = [];
467
- const regex = /{{(.*?)}}/g;
468
-
469
- if (this.options.fillFieldsFromImages) {
470
- for (const [key, value] of Object.entries((this.options.fillFieldsFromImages ))) {
471
- const template = value;
472
- const templateMatches = template.match(regex);
473
- if (templateMatches) {
474
- matches.push(...templateMatches);
475
- }
464
+ }
465
+ if (this.options.fillFieldsFromImages || this.options.fillPlainFields || this.options.generateImages) {
466
+ let matches: string[] = [];
467
+ const regex = /{{(.*?)}}/g;
468
+
469
+ if (this.options.fillFieldsFromImages) {
470
+ for (const [key, value] of Object.entries((this.options.fillFieldsFromImages ))) {
471
+ const template = value;
472
+ const templateMatches = template.match(regex);
473
+ if (templateMatches) {
474
+ matches.push(...templateMatches);
476
475
  }
477
476
  }
478
- if (this.options.fillPlainFields) {
479
- for (const [key, value] of Object.entries((this.options.fillPlainFields))) {
480
- const template = value;
481
- const templateMatches = template.match(regex);
482
- if (templateMatches) {
483
- matches.push(...templateMatches);
484
- }
477
+ }
478
+ if (this.options.fillPlainFields) {
479
+ for (const [key, value] of Object.entries((this.options.fillPlainFields))) {
480
+ const template = value;
481
+ const templateMatches = template.match(regex);
482
+ if (templateMatches) {
483
+ matches.push(...templateMatches);
485
484
  }
486
485
  }
487
- if (this.options.generateImages) {
488
- for (const [key, value] of Object.entries((this.options.generateImages ))) {
489
- const template = value.prompt;
490
- const templateMatches = template.match(regex);
491
- if (templateMatches) {
492
- matches.push(...templateMatches);
493
- }
486
+ }
487
+ if (this.options.generateImages) {
488
+ for (const [key, value] of Object.entries((this.options.generateImages ))) {
489
+ const template = value.prompt;
490
+ const templateMatches = template.match(regex);
491
+ if (templateMatches) {
492
+ matches.push(...templateMatches);
494
493
  }
495
494
  }
495
+ }
496
496
 
497
- if (matches) {
498
- matches.forEach((match) => {
499
- const field = match.replace(/{{|}}/g, '').trim();
500
- if (!resourceConfig.columns.find((column: any) => column.name === field)) {
501
- const similar = suggestIfTypo(resourceConfig.columns.map((column: any) => column.name), field);
502
- throw new Error(`Field "${field}" specified in generationPrompt not found in resource "${resourceConfig.label}". ${similar ? `Did you mean "${similar}"?` : ''}`);
503
- } else {
504
- let column = resourceConfig.columns.find((column: any) => column.name === field);
505
- if (column.backendOnly === true) {
506
- throw new Error(`Field "${field}" specified in generationPrompt is marked as backendOnly in resource "${resourceConfig.label}". Please remove backendOnly or choose another field.`);
507
- }
497
+ if (matches) {
498
+ matches.forEach((match) => {
499
+ const field = match.replace(/{{|}}/g, '').trim();
500
+ if (!resourceConfig.columns.find((column: any) => column.name === field)) {
501
+ const similar = suggestIfTypo(resourceConfig.columns.map((column: any) => column.name), field);
502
+ throw new Error(`Field "${field}" specified in generationPrompt not found in resource "${resourceConfig.label}". ${similar ? `Did you mean "${similar}"?` : ''}`);
503
+ } else {
504
+ let column = resourceConfig.columns.find((column: any) => column.name === field);
505
+ if (column.backendOnly === true) {
506
+ throw new Error(`Field "${field}" specified in generationPrompt is marked as backendOnly in resource "${resourceConfig.label}". Please remove backendOnly or choose another field.`);
508
507
  }
509
- });
510
- }
508
+ }
509
+ });
511
510
  }
512
511
  }
513
512
  }
@@ -755,5 +754,36 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
755
754
  });
756
755
 
757
756
 
757
+ server.endpoint({
758
+ method: 'POST',
759
+ path: `/plugin/${this.pluginInstanceId}/compile_old_image_link`,
760
+ handler: async ({ body, adminUser, headers }) => {
761
+ const image = body.image;
762
+ const columnName = body.columnName;
763
+ if (!image) {
764
+ return { ok: false, error: "Can't find image url" };
765
+ }
766
+ if (!columnName) {
767
+ return { ok: false, error: "Can't find column name" };
768
+ }
769
+ try {
770
+ if (this.options?.generateImages) {
771
+ const plugin = this.adminforth.activatedPlugins.find(p =>
772
+ p.resourceConfig!.resourceId === this.resourceConfig.resourceId &&
773
+ p.pluginOptions.pathColumnName === columnName
774
+ );
775
+ if (plugin?.pluginOptions?.preview) {
776
+ const compiledPreviewUrl = plugin.pluginOptions.preview.previewUrl({ filePath: image });
777
+ return { ok: true, previewUrl: compiledPreviewUrl };
778
+ }
779
+ return { ok: false, error: "Can't find plugin for column" };
780
+ }
781
+ } catch (e) {
782
+ return { ok: false, error: "Error compiling preview url" };
783
+ }
784
+ }
785
+ });
786
+
787
+
758
788
  }
759
789
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/bulk-ai-flow",
3
- "version": "1.12.0",
3
+ "version": "1.13.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },