@adminforth/bulk-ai-flow 1.7.4 → 1.8.1
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/README.md +7 -1
- package/build.log +5 -5
- package/custom/{imageGenerationCarousel.vue → ImageGenerationCarousel.vue} +18 -14
- package/custom/{visionAction.vue → VisionAction.vue} +57 -75
- package/{dist/custom/visionTable.vue → custom/VisionTable.vue} +41 -27
- package/dist/custom/{imageGenerationCarousel.vue → ImageGenerationCarousel.vue} +18 -14
- package/dist/custom/{visionAction.vue → VisionAction.vue} +57 -75
- package/{custom/visionTable.vue → dist/custom/VisionTable.vue} +41 -27
- package/dist/index.js +99 -60
- package/index.ts +111 -86
- package/package.json +1 -1
- package/types.ts +37 -1
|
@@ -5,60 +5,38 @@
|
|
|
5
5
|
</div>
|
|
6
6
|
<p class="text-justify max-h-[18px] truncate max-w-[60vw] md:max-w-none">{{ props.meta.actionName }}</p>
|
|
7
7
|
</div>
|
|
8
|
-
<Dialog
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
@error="handleTableError"
|
|
41
|
-
/>
|
|
42
|
-
</div>
|
|
43
|
-
<div class="flex w-full flex-col md:flex-row items-stretch md:items-end justify-end gap-3 md:gap-4">
|
|
44
|
-
<div class="h-full text-red-600 font-semibold flex items-center justify-center md:mb-2">
|
|
45
|
-
<p v-if="isError === true">{{ errorMessage }}</p>
|
|
46
|
-
</div>
|
|
47
|
-
<button type="button" class="w-full md:w-auto py-2.5 px-5 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
|
|
48
|
-
@click="closeDialog"
|
|
49
|
-
>
|
|
50
|
-
{{'Cancel'}}
|
|
51
|
-
</button>
|
|
52
|
-
<Button
|
|
53
|
-
class="w-full md:w-64"
|
|
54
|
-
@click="saveData"
|
|
55
|
-
:disabled="isLoading || checkedCount < 1 || isCriticalError"
|
|
56
|
-
:loader="isLoading"
|
|
57
|
-
>
|
|
58
|
-
{{ checkedCount > 1 ? 'Save fields' : 'Save field' }}
|
|
59
|
-
</Button>
|
|
60
|
-
</div>
|
|
61
|
-
</div>
|
|
8
|
+
<Dialog
|
|
9
|
+
ref="confirmDialog"
|
|
10
|
+
header="Bulk AI Flow"
|
|
11
|
+
class="!max-w-full w-full lg:w-[1600px] !lg:max-w-[1600px]"
|
|
12
|
+
:buttons="[
|
|
13
|
+
{ label: checkedCount > 1 ? 'Save fields' : 'Save field', options: { disabled: isLoading || checkedCount < 1 || isCriticalError || isFetchingRecords, loader: isLoading, class: 'w-fit sm:w-40' }, onclick: (dialog) => { saveData(); dialog.hide(); } },
|
|
14
|
+
{ label: 'Cancel', onclick: (dialog) => dialog.hide() },
|
|
15
|
+
]"
|
|
16
|
+
>
|
|
17
|
+
<div class="bulk-vision-table flex flex-col items-center max-w-[1560px] md:max-h-[90vh] gap-3 md:gap-4 w-full h-full overflow-y-auto">
|
|
18
|
+
<div v-if="records && props.checkboxes.length" class="w-full overflow-x-auto">
|
|
19
|
+
<VisionTable
|
|
20
|
+
:checkbox="props.checkboxes"
|
|
21
|
+
:records="records"
|
|
22
|
+
:meta="props.meta"
|
|
23
|
+
:images="images"
|
|
24
|
+
:tableHeaders="tableHeaders"
|
|
25
|
+
:tableColumns="tableColumns"
|
|
26
|
+
:customFieldNames="customFieldNames"
|
|
27
|
+
:tableColumnsIndexes="tableColumnsIndexes"
|
|
28
|
+
:selected="selected"
|
|
29
|
+
:isAiResponseReceivedAnalize="isAiResponseReceivedAnalize"
|
|
30
|
+
:isAiResponseReceivedImage="isAiResponseReceivedImage"
|
|
31
|
+
:primaryKey="primaryKey"
|
|
32
|
+
:openGenerationCarousel="openGenerationCarousel"
|
|
33
|
+
@error="handleTableError"
|
|
34
|
+
:carouselSaveImages="carouselSaveImages"
|
|
35
|
+
:carouselImageIndex="carouselImageIndex"
|
|
36
|
+
/>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="text-red-600 flex items-center w-full">
|
|
39
|
+
<p v-if="isError === true">{{ errorMessage }}</p>
|
|
62
40
|
</div>
|
|
63
41
|
</div>
|
|
64
42
|
</Dialog>
|
|
@@ -68,13 +46,11 @@
|
|
|
68
46
|
import { callAdminForthApi } from '@/utils';
|
|
69
47
|
import { Ref, ref, watch } from 'vue'
|
|
70
48
|
import { Dialog, Button } from '@/afcl';
|
|
71
|
-
import VisionTable from './
|
|
49
|
+
import VisionTable from './VisionTable.vue'
|
|
72
50
|
import adminforth from '@/adminforth';
|
|
73
51
|
import { useI18n } from 'vue-i18n';
|
|
74
|
-
import { useRoute } from 'vue-router';
|
|
75
52
|
import { AdminUser, type AdminForthResourceCommon } from '@/types';
|
|
76
53
|
|
|
77
|
-
const route = useRoute();
|
|
78
54
|
const { t } = useI18n();
|
|
79
55
|
|
|
80
56
|
const props = defineProps<{
|
|
@@ -99,11 +75,14 @@ const tableColumns = ref([]);
|
|
|
99
75
|
const tableColumnsIndexes = ref([]);
|
|
100
76
|
const customFieldNames = ref([]);
|
|
101
77
|
const selected = ref<any[]>([]);
|
|
78
|
+
const carouselSaveImages = ref<any[]>([]);
|
|
79
|
+
const carouselImageIndex = ref<any[]>([]);
|
|
102
80
|
const isAiResponseReceivedAnalize = ref([]);
|
|
103
81
|
const isAiResponseReceivedImage = ref([]);
|
|
104
82
|
const primaryKey = props.meta.primaryKey;
|
|
105
83
|
const openGenerationCarousel = ref([]);
|
|
106
84
|
const isLoading = ref(false);
|
|
85
|
+
const isFetchingRecords = ref(false);
|
|
107
86
|
const isError = ref(false);
|
|
108
87
|
const isCriticalError = ref(false);
|
|
109
88
|
const isImageGenerationError = ref(false);
|
|
@@ -128,7 +107,7 @@ const openDialog = async () => {
|
|
|
128
107
|
return acc;
|
|
129
108
|
},{[primaryKey]: records.value[i][primaryKey]} as Record<string, boolean>);
|
|
130
109
|
}
|
|
131
|
-
|
|
110
|
+
isFetchingRecords.value = true;
|
|
132
111
|
const tasks = [];
|
|
133
112
|
if (props.meta.isFieldsForAnalizeFromImages) {
|
|
134
113
|
tasks.push(runAiAction({
|
|
@@ -152,7 +131,12 @@ const openDialog = async () => {
|
|
|
152
131
|
}));
|
|
153
132
|
}
|
|
154
133
|
await Promise.all(tasks);
|
|
155
|
-
|
|
134
|
+
|
|
135
|
+
if (props.meta.isImageGeneration) {
|
|
136
|
+
fillCarouselSaveImages();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
isFetchingRecords.value = false;
|
|
156
140
|
}
|
|
157
141
|
|
|
158
142
|
watch(selected, (val) => {
|
|
@@ -160,22 +144,22 @@ watch(selected, (val) => {
|
|
|
160
144
|
checkedCount.value = val.filter(item => item.isChecked === true).length;
|
|
161
145
|
}, { deep: true });
|
|
162
146
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
errorMessage.value = '';
|
|
147
|
+
function fillCarouselSaveImages() {
|
|
148
|
+
for (const item of selected.value) {
|
|
149
|
+
const tempItem: any = {};
|
|
150
|
+
const tempItemIndex: any = {};
|
|
151
|
+
for (const [key, value] of Object.entries(item)) {
|
|
152
|
+
if (props.meta.outputImageFields?.includes(key)) {
|
|
153
|
+
tempItem[key] = [value];
|
|
154
|
+
tempItemIndex[key] = 0;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
carouselSaveImages.value.push(tempItem);
|
|
158
|
+
carouselImageIndex.value.push(tempItemIndex);
|
|
159
|
+
}
|
|
177
160
|
}
|
|
178
161
|
|
|
162
|
+
|
|
179
163
|
function formatLabel(str) {
|
|
180
164
|
return str
|
|
181
165
|
.split('_')
|
|
@@ -270,7 +254,6 @@ async function getRecords() {
|
|
|
270
254
|
console.error('Failed to get records:', error);
|
|
271
255
|
isError.value = true;
|
|
272
256
|
errorMessage.value = `Failed to fetch records. Please, try to re-run the action.`;
|
|
273
|
-
// Handle error appropriately
|
|
274
257
|
}
|
|
275
258
|
}
|
|
276
259
|
|
|
@@ -288,7 +271,6 @@ async function getImages() {
|
|
|
288
271
|
console.error('Failed to get images:', error);
|
|
289
272
|
isError.value = true;
|
|
290
273
|
errorMessage.value = `Failed to fetch images. Please, try to re-run the action.`;
|
|
291
|
-
// Handle error appropriately
|
|
292
274
|
}
|
|
293
275
|
}
|
|
294
276
|
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div>
|
|
3
2
|
<Table
|
|
4
3
|
:columns="tableHeaders"
|
|
5
4
|
:data="tableColumns"
|
|
6
|
-
:pageSize="
|
|
5
|
+
:pageSize="6"
|
|
7
6
|
>
|
|
8
7
|
<!-- HEADER TEMPLATE -->
|
|
9
8
|
<template #header:checkboxes="{ item }">
|
|
@@ -28,18 +27,22 @@
|
|
|
28
27
|
@click="zoomImage(image)"
|
|
29
28
|
/>
|
|
30
29
|
</div>
|
|
30
|
+
</div>
|
|
31
|
+
<transition name="fade">
|
|
31
32
|
<div
|
|
32
33
|
v-if="zoomedImage"
|
|
33
34
|
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60"
|
|
34
35
|
@click.self="closeZoom"
|
|
35
36
|
>
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
<transition name="zoom">
|
|
38
|
+
<img
|
|
39
|
+
v-if="zoomedImage"
|
|
40
|
+
:src="zoomedImage"
|
|
41
|
+
class="max-w-full max-h-full rounded-lg object-contain cursor-grab z-75"
|
|
42
|
+
/>
|
|
43
|
+
</transition>
|
|
41
44
|
</div>
|
|
42
|
-
</
|
|
45
|
+
</transition>
|
|
43
46
|
</div>
|
|
44
47
|
</template>
|
|
45
48
|
<!-- CUSTOM FIELD TEMPLATES -->
|
|
@@ -73,7 +76,7 @@
|
|
|
73
76
|
<Input
|
|
74
77
|
type="number"
|
|
75
78
|
v-model="selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n]"
|
|
76
|
-
class="w-full "
|
|
79
|
+
class="w-full min-w-[80px]"
|
|
77
80
|
:fullWidth="true"
|
|
78
81
|
/>
|
|
79
82
|
</div>
|
|
@@ -91,13 +94,15 @@
|
|
|
91
94
|
<div>
|
|
92
95
|
<GenerationCarousel
|
|
93
96
|
v-if="openGenerationCarousel[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n]"
|
|
94
|
-
:images="
|
|
97
|
+
:images="carouselSaveImages[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n]"
|
|
95
98
|
:recordId="item[primaryKey]"
|
|
96
99
|
:meta="props.meta"
|
|
97
100
|
:fieldName="n"
|
|
101
|
+
:carouselImageIndex="carouselImageIndex[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n]"
|
|
98
102
|
@error="handleError"
|
|
99
103
|
@close="openGenerationCarousel[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] = false"
|
|
100
104
|
@selectImage="updateSelectedImage"
|
|
105
|
+
@updateCarouselIndex="updateActiveIndex"
|
|
101
106
|
/>
|
|
102
107
|
</div>
|
|
103
108
|
</div>
|
|
@@ -112,14 +117,12 @@
|
|
|
112
117
|
</div>
|
|
113
118
|
</template>
|
|
114
119
|
</Table>
|
|
115
|
-
</div>
|
|
116
120
|
</template>
|
|
117
121
|
|
|
118
122
|
<script lang="ts" setup>
|
|
119
|
-
import { ref
|
|
120
|
-
import mediumZoom from 'medium-zoom'
|
|
123
|
+
import { ref } from 'vue'
|
|
121
124
|
import { Select, Input, Textarea, Table, Checkbox, Skeleton, Toggle } from '@/afcl'
|
|
122
|
-
import GenerationCarousel from './
|
|
125
|
+
import GenerationCarousel from './ImageGenerationCarousel.vue'
|
|
123
126
|
|
|
124
127
|
const props = defineProps<{
|
|
125
128
|
meta: any,
|
|
@@ -134,12 +137,13 @@ const props = defineProps<{
|
|
|
134
137
|
openGenerationCarousel: any
|
|
135
138
|
isError: boolean,
|
|
136
139
|
errorMessage: string
|
|
140
|
+
carouselSaveImages: any[]
|
|
141
|
+
carouselImageIndex: any[]
|
|
137
142
|
}>();
|
|
138
143
|
const emit = defineEmits(['error']);
|
|
139
144
|
|
|
140
145
|
|
|
141
146
|
const zoomedImage = ref(null)
|
|
142
|
-
const zoomedImg = ref(null)
|
|
143
147
|
|
|
144
148
|
|
|
145
149
|
function zoomImage(img) {
|
|
@@ -150,17 +154,6 @@ function closeZoom() {
|
|
|
150
154
|
zoomedImage.value = null
|
|
151
155
|
}
|
|
152
156
|
|
|
153
|
-
watch(zoomedImage, async (val) => {
|
|
154
|
-
await nextTick()
|
|
155
|
-
if (val && zoomedImg.value) {
|
|
156
|
-
mediumZoom(zoomedImg.value, {
|
|
157
|
-
margin: 24,
|
|
158
|
-
background: 'rgba(0, 0, 0, 0.9)',
|
|
159
|
-
scrollOffset: 150
|
|
160
|
-
}).show()
|
|
161
|
-
}
|
|
162
|
-
})
|
|
163
|
-
|
|
164
157
|
function isInColumnEnum(key: string): boolean {
|
|
165
158
|
const colEnum = props.meta.columnEnums?.find(c => c.name === key);
|
|
166
159
|
if (!colEnum) {
|
|
@@ -193,4 +186,25 @@ function handleError({ isError, errorMessage }) {
|
|
|
193
186
|
});
|
|
194
187
|
}
|
|
195
188
|
|
|
196
|
-
|
|
189
|
+
function updateActiveIndex(newIndex: number, id: any, fieldName: string) {
|
|
190
|
+
props.carouselImageIndex[props.tableColumnsIndexes.findIndex(el => el[props.primaryKey] === id)][fieldName] = newIndex;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
</script>
|
|
194
|
+
|
|
195
|
+
<style scoped>
|
|
196
|
+
.fade-enter-active, .fade-leave-active {
|
|
197
|
+
transition: opacity 0.2s ease;
|
|
198
|
+
}
|
|
199
|
+
.fade-enter-from, .fade-leave-to {
|
|
200
|
+
opacity: 0;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.zoom-enter-active, .zoom-leave-active {
|
|
204
|
+
transition: transform 0.2s ease, opacity 0.2s ease;
|
|
205
|
+
}
|
|
206
|
+
.zoom-enter-from, .zoom-leave-to {
|
|
207
|
+
transform: scale(0.95);
|
|
208
|
+
opacity: 0;
|
|
209
|
+
}
|
|
210
|
+
</style>
|
package/dist/index.js
CHANGED
|
@@ -61,52 +61,26 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
61
61
|
const columns = this.resourceConfig.columns;
|
|
62
62
|
let columnEnums = [];
|
|
63
63
|
if (this.options.fillFieldsFromImages) {
|
|
64
|
-
if (!this.options.attachFiles) {
|
|
65
|
-
throw new Error('⚠️ attachFiles function must be provided in options when fillFieldsFromImages is used');
|
|
66
|
-
}
|
|
67
|
-
if (!this.options.visionAdapter) {
|
|
68
|
-
throw new Error('⚠️ visionAdapter must be provided in options when fillFieldsFromImages is used');
|
|
69
|
-
}
|
|
70
64
|
for (const [key, value] of Object.entries((this.options.fillFieldsFromImages))) {
|
|
71
65
|
const column = columns.find(c => c.name.toLowerCase() === key.toLowerCase());
|
|
72
|
-
if (column) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
throw new Error(`⚠️ No column found for key "${key}"`);
|
|
66
|
+
if (column && column.enum) {
|
|
67
|
+
this.options.fillFieldsFromImages[key] = `${value} Select ${key} from the list (USE ONLY VALUE FIELD. USE ONLY VALUES FROM THIS LIST): ${JSON.stringify(column.enum)}`;
|
|
68
|
+
columnEnums.push({
|
|
69
|
+
name: key,
|
|
70
|
+
enum: column.enum,
|
|
71
|
+
});
|
|
83
72
|
}
|
|
84
73
|
}
|
|
85
74
|
}
|
|
86
75
|
if (this.options.fillPlainFields) {
|
|
87
|
-
if (!this.options.textCompleteAdapter) {
|
|
88
|
-
throw new Error('⚠️ textCompleteAdapter must be provided in options when fillPlainFields is used');
|
|
89
|
-
}
|
|
90
76
|
for (const [key, value] of Object.entries((this.options.fillPlainFields))) {
|
|
91
77
|
const column = columns.find(c => c.name.toLowerCase() === key.toLowerCase());
|
|
92
|
-
if (column) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
else {
|
|
102
|
-
throw new Error(`⚠️ No column found for key "${key}"`);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
if (this.options.generateImages && !this.options.imageGenerationAdapter) {
|
|
107
|
-
for (const [key, value] of Object.entries(this.options.generateImages)) {
|
|
108
|
-
if (!this.options.generateImages[key].adapter) {
|
|
109
|
-
throw new Error(`⚠️ No image generation adapter found for key "${key}"`);
|
|
78
|
+
if (column && column.enum) {
|
|
79
|
+
this.options.fillPlainFields[key] = `${value} Select ${key} from the list (USE ONLY VALUE FIELD. USE ONLY VALUES FROM THIS LIST): ${JSON.stringify(column.enum)}`;
|
|
80
|
+
columnEnums.push({
|
|
81
|
+
name: key,
|
|
82
|
+
enum: column.enum,
|
|
83
|
+
});
|
|
110
84
|
}
|
|
111
85
|
}
|
|
112
86
|
}
|
|
@@ -120,28 +94,15 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
120
94
|
//check if Upload plugin is installed on all attachment fields
|
|
121
95
|
if (this.options.generateImages) {
|
|
122
96
|
for (const [key, value] of Object.entries(this.options.generateImages)) {
|
|
123
|
-
const column = columns.find(c => c.name.toLowerCase() === key.toLowerCase());
|
|
124
|
-
if (!column) {
|
|
125
|
-
throw new Error(`⚠️ No column found for key "${key}"`);
|
|
126
|
-
}
|
|
127
97
|
const plugin = adminforth.activatedPlugins.find(p => p.resourceConfig.resourceId === this.resourceConfig.resourceId &&
|
|
128
98
|
p.pluginOptions.pathColumnName === key);
|
|
129
|
-
if (!plugin) {
|
|
130
|
-
throw new Error(`Plugin for attachment field '${key}' not found in resource '${this.resourceConfig.resourceId}', please check if Upload Plugin is installed on the field ${key}`);
|
|
131
|
-
}
|
|
132
|
-
if (!plugin.pluginOptions.storageAdapter.objectCanBeAccesedPublicly()) {
|
|
133
|
-
throw new Error(`Upload Plugin for attachment field '${key}' in resource '${this.resourceConfig.resourceId}'
|
|
134
|
-
uses adapter which is not configured to store objects in public way, so it will produce only signed private URLs which can not be used in HTML text of blog posts.
|
|
135
|
-
Please configure adapter in such way that it will store objects publicly (e.g. for S3 use 'public-read' ACL).
|
|
136
|
-
`);
|
|
137
|
-
}
|
|
138
99
|
outputImagesPluginInstanceIds[key] = plugin.pluginInstanceId;
|
|
139
100
|
}
|
|
140
101
|
}
|
|
141
102
|
const outputFields = Object.assign(Object.assign(Object.assign({}, this.options.fillFieldsFromImages), this.options.fillPlainFields), (this.options.generateImages || {}));
|
|
142
103
|
const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
|
|
143
104
|
const pageInjection = {
|
|
144
|
-
file: this.componentPath('
|
|
105
|
+
file: this.componentPath('VisionAction.vue'),
|
|
145
106
|
meta: {
|
|
146
107
|
pluginInstanceId: this.pluginInstanceId,
|
|
147
108
|
outputFields: outputFields,
|
|
@@ -170,11 +131,63 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
170
131
|
});
|
|
171
132
|
}
|
|
172
133
|
validateConfigAfterDiscover(adminforth, resourceConfig) {
|
|
173
|
-
|
|
134
|
+
const columns = this.resourceConfig.columns;
|
|
135
|
+
if (this.options.fillFieldsFromImages) {
|
|
136
|
+
if (!this.options.attachFiles) {
|
|
137
|
+
throw new Error('⚠️ attachFiles function must be provided when fillFieldsFromImages is used');
|
|
138
|
+
}
|
|
139
|
+
if (!this.options.visionAdapter) {
|
|
140
|
+
throw new Error('⚠️ visionAdapter must be provided when fillFieldsFromImages is used');
|
|
141
|
+
}
|
|
142
|
+
for (const key of Object.keys(this.options.fillFieldsFromImages)) {
|
|
143
|
+
const column = columns.find(c => c.name.toLowerCase() === key.toLowerCase());
|
|
144
|
+
if (!column) {
|
|
145
|
+
throw new Error(`⚠️ No column found for key "${key}"`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (this.options.fillPlainFields) {
|
|
150
|
+
if (!this.options.textCompleteAdapter) {
|
|
151
|
+
throw new Error('⚠️ textCompleteAdapter must be provided when fillPlainFields is used');
|
|
152
|
+
}
|
|
153
|
+
for (const key of Object.keys(this.options.fillPlainFields)) {
|
|
154
|
+
const column = columns.find(c => c.name.toLowerCase() === key.toLowerCase());
|
|
155
|
+
if (!column) {
|
|
156
|
+
throw new Error(`⚠️ No column found for key "${key}"`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (this.options.generateImages) {
|
|
161
|
+
for (const key of Object.keys(this.options.generateImages)) {
|
|
162
|
+
const column = columns.find(c => c.name.toLowerCase() === key.toLowerCase());
|
|
163
|
+
if (!column) {
|
|
164
|
+
throw new Error(`⚠️ No column found for key "${key}"`);
|
|
165
|
+
}
|
|
166
|
+
const perKeyAdapter = this.options.generateImages[key].adapter;
|
|
167
|
+
if (!perKeyAdapter && !this.options.imageGenerationAdapter) {
|
|
168
|
+
throw new Error(`⚠️ No image generation adapter provided for key "${key}"`);
|
|
169
|
+
}
|
|
170
|
+
const plugin = adminforth.activatedPlugins.find(p => p.resourceConfig.resourceId === this.resourceConfig.resourceId &&
|
|
171
|
+
p.pluginOptions.pathColumnName === key);
|
|
172
|
+
if (!plugin) {
|
|
173
|
+
throw new Error(`Plugin for attachment field '${key}' not found in resource '${this.resourceConfig.resourceId}', please check if Upload Plugin is installed on the field ${key}`);
|
|
174
|
+
}
|
|
175
|
+
if (!plugin.pluginOptions || !plugin.pluginOptions.storageAdapter) {
|
|
176
|
+
throw new Error(`Upload Plugin for attachment field '${key}' in resource '${this.resourceConfig.resourceId}' is missing a storageAdapter configuration.`);
|
|
177
|
+
}
|
|
178
|
+
if (typeof plugin.pluginOptions.storageAdapter.objectCanBeAccesedPublicly !== 'function') {
|
|
179
|
+
throw new Error(`Upload Plugin for attachment field '${key}' in resource '${this.resourceConfig.resourceId}' uses a storage adapter without 'objectCanBeAccesedPublicly' method.`);
|
|
180
|
+
}
|
|
181
|
+
if (!plugin.pluginOptions.storageAdapter.objectCanBeAccesedPublicly()) {
|
|
182
|
+
throw new Error(`Upload Plugin for attachment field '${key}' in resource '${this.resourceConfig.resourceId}'
|
|
183
|
+
uses adapter which is not configured to store objects in public way, so it will produce only signed private URLs which can not be used in HTML text of blog posts.
|
|
184
|
+
Please configure adapter in such way that it will store objects publicly (e.g. for S3 use 'public-read' ACL).
|
|
185
|
+
`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
174
189
|
}
|
|
175
190
|
instanceUniqueRepresentation(pluginOptions) {
|
|
176
|
-
// optional method to return unique string representation of plugin instance.
|
|
177
|
-
// Needed if plugin can have multiple instances on one resource
|
|
178
191
|
return `${this.pluginOptions.actionName}`;
|
|
179
192
|
}
|
|
180
193
|
setupEndpoints(server) {
|
|
@@ -236,23 +249,21 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
236
249
|
}
|
|
237
250
|
}
|
|
238
251
|
const tasks = selectedIds.map((ID) => __awaiter(this, void 0, void 0, function* () {
|
|
239
|
-
// Fetch the record using the provided ID
|
|
240
252
|
const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
|
|
241
253
|
const record = yield this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(primaryKeyColumn.name, ID)]);
|
|
242
|
-
//create prompt for OpenAI
|
|
243
254
|
const compiledOutputFields = this.compileOutputFieldsTemplatesNoImage(record);
|
|
244
255
|
const prompt = `Analyze the following fields and return a single JSON in format like: {'param1': 'value1', 'param2': 'value2'}.
|
|
245
256
|
Do NOT return array of objects. Do NOT include any Markdown, code blocks, explanations, or extra text. Only return valid JSON.
|
|
246
257
|
Each object must contain the following fields: ${JSON.stringify(compiledOutputFields)} Use the exact field names.
|
|
247
258
|
If it's number field - return only number.`;
|
|
248
259
|
//send prompt to OpenAI and get response
|
|
249
|
-
const
|
|
260
|
+
const numberOfTokens = this.options.fillPlainFieldsMaxTokens ? this.options.fillPlainFieldsMaxTokens : 1000;
|
|
261
|
+
const { content: chatResponse } = yield this.options.textCompleteAdapter.complete(prompt, [], numberOfTokens);
|
|
250
262
|
const resp = chatResponse.response;
|
|
251
263
|
const topLevelError = chatResponse.error;
|
|
252
264
|
if (topLevelError || (resp === null || resp === void 0 ? void 0 : resp.error)) {
|
|
253
265
|
throw new Error(`ERROR: ${JSON.stringify(topLevelError || (resp === null || resp === void 0 ? void 0 : resp.error))}`);
|
|
254
266
|
}
|
|
255
|
-
//parse response and update record
|
|
256
267
|
const resData = JSON.parse(chatResponse);
|
|
257
268
|
return resData;
|
|
258
269
|
}));
|
|
@@ -270,8 +281,10 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
270
281
|
for (const [index, record] of records.entries()) {
|
|
271
282
|
records[index]._label = this.resourceConfig.recordLabel(records[index]);
|
|
272
283
|
}
|
|
284
|
+
const order = Object.fromEntries(body.body.record.map((id, i) => [id, i]));
|
|
285
|
+
const sortedRecords = records.sort((a, b) => order[a.id] - order[b.id]);
|
|
273
286
|
return {
|
|
274
|
-
records,
|
|
287
|
+
records: sortedRecords,
|
|
275
288
|
};
|
|
276
289
|
})
|
|
277
290
|
});
|
|
@@ -336,6 +349,32 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
336
349
|
}
|
|
337
350
|
}
|
|
338
351
|
}
|
|
352
|
+
try {
|
|
353
|
+
const AuditLogPlugin = this.adminforth.getPluginByClassName('AuditLogPlugin');
|
|
354
|
+
if (AuditLogPlugin) {
|
|
355
|
+
for (const [key, value] of Object.entries(oldRecord)) {
|
|
356
|
+
if (!(key in fieldsToUpdate[idx])) {
|
|
357
|
+
delete oldRecord[key];
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const reorderedOldRecord = Object.keys(fieldsToUpdate[idx]).reduce((acc, key) => {
|
|
361
|
+
if (key in oldRecord) {
|
|
362
|
+
acc[key] = oldRecord[key];
|
|
363
|
+
}
|
|
364
|
+
return acc;
|
|
365
|
+
}, {});
|
|
366
|
+
AuditLogPlugin.logCustomAction({
|
|
367
|
+
resourceId: this.resourceConfig.resourceId,
|
|
368
|
+
recordId: ID,
|
|
369
|
+
actionId: 'Bulk-ai-flow',
|
|
370
|
+
oldData: reorderedOldRecord,
|
|
371
|
+
data: fieldsToUpdate[idx],
|
|
372
|
+
user: adminUser,
|
|
373
|
+
headers: headers
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
catch (error) { }
|
|
339
378
|
return this.adminforth.resource(this.resourceConfig.resourceId).update(ID, fieldsToUpdate[idx]);
|
|
340
379
|
}));
|
|
341
380
|
yield Promise.all(updates);
|