@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 +3 -2
- package/custom/ImageCompare.vue +196 -0
- package/custom/VisionAction.vue +36 -1
- package/custom/VisionTable.vue +78 -19
- package/dist/custom/ImageCompare.vue +196 -0
- package/dist/custom/VisionAction.vue +36 -1
- package/dist/custom/VisionTable.vue +78 -19
- package/dist/index.js +67 -38
- package/index.ts +69 -39
- package/package.json +1 -1
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
|
|
16
|
-
total size is
|
|
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>
|
package/custom/VisionAction.vue
CHANGED
|
@@ -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
|
-
|
|
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>
|
package/custom/VisionTable.vue
CHANGED
|
@@ -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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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]
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
<
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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]
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
<
|
|
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
|
|
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
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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
|
}
|