Skip to content

Commit b162945

Browse files
committed
feat: implement old data and image comparison feature
1 parent c83d89d commit b162945

File tree

4 files changed

+379
-59
lines changed

4 files changed

+379
-59
lines changed

custom/ImageCompare.vue

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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>

custom/VisionAction.vue

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,12 @@
2828
:customFieldNames="customFieldNames"
2929
:tableColumnsIndexes="tableColumnsIndexes"
3030
:selected="selected"
31+
:oldData="oldData"
3132
:isAiResponseReceivedAnalize="isAiResponseReceivedAnalize"
3233
:isAiResponseReceivedImage="isAiResponseReceivedImage"
3334
:primaryKey="primaryKey"
3435
:openGenerationCarousel="openGenerationCarousel"
36+
:openImageCompare="openImageCompare"
3537
@error="handleTableError"
3638
:carouselSaveImages="carouselSaveImages"
3739
:carouselImageIndex="carouselImageIndex"
@@ -41,6 +43,7 @@
4143
:isAiImageGenerationError="isAiImageGenerationError"
4244
:imageGenerationErrorMessage="imageGenerationErrorMessage"
4345
@regenerate-images="regenerateImages"
46+
:isImageHasPreviewUrl="isImageHasPreviewUrl"
4447
/>
4548
</div>
4649
<div class="text-red-600 flex items-center w-full">
@@ -83,12 +86,14 @@ const tableColumns = ref([]);
8386
const tableColumnsIndexes = ref([]);
8487
const customFieldNames = ref([]);
8588
const selected = ref<any[]>([]);
89+
const oldData = ref<any[]>([]);
8690
const carouselSaveImages = ref<any[]>([]);
8791
const carouselImageIndex = ref<any[]>([]);
8892
const isAiResponseReceivedAnalize = ref([]);
8993
const isAiResponseReceivedImage = ref([]);
9094
const primaryKey = props.meta.primaryKey;
9195
const openGenerationCarousel = ref([]);
96+
const openImageCompare = ref([]);
9297
const isLoading = ref(false);
9398
const isFetchingRecords = ref(false);
9499
const isError = ref(false);
@@ -104,6 +109,7 @@ const isAiGenerationError = ref<boolean[]>([false]);
104109
const aiGenerationErrorMessage = ref<string[]>([]);
105110
const isAiImageGenerationError = ref<boolean[]>([false]);
106111
const imageGenerationErrorMessage = ref<string[]>([]);
112+
const isImageHasPreviewUrl = ref<Record<string, boolean>>({});
107113
108114
const openDialog = async () => {
109115
isDialogOpen.value = true;
@@ -113,6 +119,7 @@ const openDialog = async () => {
113119
if (props.meta.isAttachFiles) {
114120
await getImages();
115121
}
122+
await findPreviewURLForImages();
116123
tableHeaders.value = generateTableHeaders(props.meta.outputFields);
117124
const result = generateTableColumns();
118125
tableColumns.value = result.tableData;
@@ -127,6 +134,10 @@ const openDialog = async () => {
127134
acc[key] = false;
128135
return acc;
129136
},{[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>);
130141
}
131142
isFetchingRecords.value = false;
132143
@@ -257,7 +268,7 @@ function setSelected() {
257268
}
258269
selected.value[index].isChecked = true;
259270
selected.value[index][primaryKey] = record[primaryKey];
260-
isAiResponseReceivedAnalize.value[index] = true;
271+
oldData.value[index] = { ...selected.value[index] };
261272
});
262273
}
263274
@@ -710,4 +721,28 @@ function regenerateImages(recordInfo: any) {
710721
});
711722
}
712723
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+
713748
</script>

0 commit comments

Comments
 (0)