@adminforth/bulk-ai-flow 1.11.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 +76 -0
- package/index.ts +79 -1
- 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
|
@@ -8,6 +8,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
10
|
import { AdminForthPlugin, Filters } from "adminforth";
|
|
11
|
+
import { suggestIfTypo } from "adminforth";
|
|
11
12
|
import Handlebars from 'handlebars';
|
|
12
13
|
import { RateLimiter } from "adminforth";
|
|
13
14
|
import { randomUUID } from "crypto";
|
|
@@ -444,6 +445,52 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
444
445
|
}
|
|
445
446
|
}
|
|
446
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
|
+
}
|
|
458
|
+
}
|
|
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
|
+
}
|
|
467
|
+
}
|
|
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
|
+
}
|
|
476
|
+
}
|
|
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
|
+
}
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
}
|
|
447
494
|
}
|
|
448
495
|
instanceUniqueRepresentation(pluginOptions) {
|
|
449
496
|
return `${this.pluginOptions.actionName}`;
|
|
@@ -666,5 +713,34 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
666
713
|
return { ok: true };
|
|
667
714
|
})
|
|
668
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
|
+
});
|
|
669
745
|
}
|
|
670
746
|
}
|
package/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AdminForthPlugin, Filters } from "adminforth";
|
|
2
2
|
import type { IAdminForth, IHttpServer, AdminForthComponentDeclaration, AdminForthResource } from "adminforth";
|
|
3
|
+
import { suggestIfTypo } from "adminforth";
|
|
3
4
|
import type { PluginOptions } from './types.js';
|
|
4
5
|
import Handlebars from 'handlebars';
|
|
5
6
|
import { RateLimiter } from "adminforth";
|
|
@@ -362,7 +363,6 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
362
363
|
...(this.options.generateImages || {})
|
|
363
364
|
};
|
|
364
365
|
|
|
365
|
-
|
|
366
366
|
const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
|
|
367
367
|
|
|
368
368
|
const pageInjection = {
|
|
@@ -462,6 +462,53 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
462
462
|
}
|
|
463
463
|
}
|
|
464
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);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
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);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
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);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
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
|
+
}
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
}
|
|
465
512
|
}
|
|
466
513
|
|
|
467
514
|
instanceUniqueRepresentation(pluginOptions: any) : string {
|
|
@@ -707,5 +754,36 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
707
754
|
});
|
|
708
755
|
|
|
709
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
|
+
|
|
710
788
|
}
|
|
711
789
|
}
|