@adminforth/bulk-ai-flow 1.7.4 → 1.8.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/README.md +7 -1
- package/build.log +5 -5
- package/custom/{imageGenerationCarousel.vue → ImageGenerationCarousel.vue} +12 -8
- package/custom/{visionAction.vue → VisionAction.vue} +31 -9
- package/{dist/custom/visionTable.vue → custom/VisionTable.vue} +11 -3
- package/dist/custom/{imageGenerationCarousel.vue → ImageGenerationCarousel.vue} +12 -8
- package/dist/custom/{visionAction.vue → VisionAction.vue} +31 -9
- package/{custom/visionTable.vue → dist/custom/VisionTable.vue} +11 -3
- package/dist/index.js +95 -60
- package/index.ts +106 -85
- package/package.json +1 -1
- package/types.ts +37 -1
package/README.md
CHANGED
|
@@ -1 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
# AdminForth Bulk AI Flow Plugin
|
|
2
|
+
|
|
3
|
+
<img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT" /> <img src="https://woodpecker.devforth.io/api/badges/3848/status.svg" alt="Build Status" /> <a href="https://www.npmjs.com/package/@adminforth/bulk-ai-flow"> <img src="https://img.shields.io/npm/dt/@adminforth/bulk-ai-flow" alt="npm downloads" /></a> <a href="https://www.npmjs.com/package/@adminforth/bulk-ai-flow"><img src="https://img.shields.io/npm/v/@adminforth/bulk-ai-flow" alt="npm version" /></a> <a href="https://www.npmjs.com/package/@adminforth/bulk-ai-flow">
|
|
4
|
+
|
|
5
|
+
Allows you to log changes made to table records via adminforth.
|
|
6
|
+
|
|
7
|
+
## For usage, see [AdminForth Bulk AI Flow Documentation](https://adminforth.dev/docs/tutorial/Plugins/bulk-ai-flow/)
|
package/build.log
CHANGED
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
sending incremental file list
|
|
6
6
|
custom/
|
|
7
|
-
custom/
|
|
7
|
+
custom/ImageGenerationCarousel.vue
|
|
8
|
+
custom/VisionAction.vue
|
|
9
|
+
custom/VisionTable.vue
|
|
8
10
|
custom/package-lock.json
|
|
9
11
|
custom/package.json
|
|
10
12
|
custom/tsconfig.json
|
|
11
|
-
custom/visionAction.vue
|
|
12
|
-
custom/visionTable.vue
|
|
13
13
|
|
|
14
|
-
sent
|
|
15
|
-
total size is
|
|
14
|
+
sent 182,087 bytes received 134 bytes 364,442.00 bytes/sec
|
|
15
|
+
total size is 181,554 speedup is 1.00
|
|
@@ -175,10 +175,6 @@
|
|
|
175
175
|
</div>
|
|
176
176
|
</div>
|
|
177
177
|
</div>
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
178
|
</template>
|
|
183
179
|
|
|
184
180
|
<script setup lang="ts">
|
|
@@ -193,8 +189,8 @@ import { ProgressBar } from '@/afcl';
|
|
|
193
189
|
const { t: $t } = useI18n();
|
|
194
190
|
|
|
195
191
|
const prompt = ref('');
|
|
196
|
-
const emit = defineEmits(['close', 'selectImage', 'error']);
|
|
197
|
-
const props = defineProps(['meta', 'record', 'images', 'recordId', 'prompt', 'fieldName', 'isError', 'errorMessage']);
|
|
192
|
+
const emit = defineEmits(['close', 'selectImage', 'error', 'updateCarouselIndex']);
|
|
193
|
+
const props = defineProps(['meta', 'record', 'images', 'recordId', 'prompt', 'fieldName', 'isError', 'errorMessage', 'carouselImageIndex']);
|
|
198
194
|
const images = ref([]);
|
|
199
195
|
const loading = ref(false);
|
|
200
196
|
const attachmentFiles = ref<string[]>([])
|
|
@@ -208,12 +204,14 @@ function minifyField(field: string): string {
|
|
|
208
204
|
|
|
209
205
|
const caurosel = ref(null);
|
|
210
206
|
onMounted(async () => {
|
|
211
|
-
|
|
207
|
+
for (const img of props.images || []) {
|
|
208
|
+
images.value.push(img);
|
|
209
|
+
}
|
|
212
210
|
const temp = await getGenerationPrompt() || '';
|
|
213
211
|
prompt.value = temp[props.fieldName];
|
|
214
212
|
await nextTick();
|
|
215
213
|
|
|
216
|
-
const currentIndex =
|
|
214
|
+
const currentIndex = props.carouselImageIndex || 0;
|
|
217
215
|
caurosel.value = new Carousel(
|
|
218
216
|
document.getElementById('gallery'),
|
|
219
217
|
images.value.map((img, index) => {
|
|
@@ -298,6 +296,12 @@ async function confirmImage() {
|
|
|
298
296
|
const currentIndex = caurosel.value?.getActiveItem()?.position || 0;
|
|
299
297
|
const img = images.value[currentIndex];
|
|
300
298
|
|
|
299
|
+
props.images.splice(0, props.images.length);
|
|
300
|
+
for (const i of images.value) {
|
|
301
|
+
props.images.push(i);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
emit('updateCarouselIndex', currentIndex, props.recordId, props.fieldName);
|
|
301
305
|
emit('selectImage', img, props.recordId, props.fieldName);
|
|
302
306
|
emit('close');
|
|
303
307
|
|
|
@@ -25,7 +25,6 @@
|
|
|
25
25
|
<VisionTable
|
|
26
26
|
:checkbox="props.checkboxes"
|
|
27
27
|
:records="records"
|
|
28
|
-
:index="0"
|
|
29
28
|
:meta="props.meta"
|
|
30
29
|
:images="images"
|
|
31
30
|
:tableHeaders="tableHeaders"
|
|
@@ -38,6 +37,8 @@
|
|
|
38
37
|
:primaryKey="primaryKey"
|
|
39
38
|
:openGenerationCarousel="openGenerationCarousel"
|
|
40
39
|
@error="handleTableError"
|
|
40
|
+
:carouselSaveImages="carouselSaveImages"
|
|
41
|
+
:carouselImageIndex="carouselImageIndex"
|
|
41
42
|
/>
|
|
42
43
|
</div>
|
|
43
44
|
<div class="flex w-full flex-col md:flex-row items-stretch md:items-end justify-end gap-3 md:gap-4">
|
|
@@ -52,7 +53,7 @@
|
|
|
52
53
|
<Button
|
|
53
54
|
class="w-full md:w-64"
|
|
54
55
|
@click="saveData"
|
|
55
|
-
:disabled="isLoading || checkedCount < 1 || isCriticalError"
|
|
56
|
+
:disabled="isLoading || checkedCount < 1 || isCriticalError || isFetchingRecords"
|
|
56
57
|
:loader="isLoading"
|
|
57
58
|
>
|
|
58
59
|
{{ checkedCount > 1 ? 'Save fields' : 'Save field' }}
|
|
@@ -68,13 +69,11 @@
|
|
|
68
69
|
import { callAdminForthApi } from '@/utils';
|
|
69
70
|
import { Ref, ref, watch } from 'vue'
|
|
70
71
|
import { Dialog, Button } from '@/afcl';
|
|
71
|
-
import VisionTable from './
|
|
72
|
+
import VisionTable from './VisionTable.vue'
|
|
72
73
|
import adminforth from '@/adminforth';
|
|
73
74
|
import { useI18n } from 'vue-i18n';
|
|
74
|
-
import { useRoute } from 'vue-router';
|
|
75
75
|
import { AdminUser, type AdminForthResourceCommon } from '@/types';
|
|
76
76
|
|
|
77
|
-
const route = useRoute();
|
|
78
77
|
const { t } = useI18n();
|
|
79
78
|
|
|
80
79
|
const props = defineProps<{
|
|
@@ -99,11 +98,14 @@ const tableColumns = ref([]);
|
|
|
99
98
|
const tableColumnsIndexes = ref([]);
|
|
100
99
|
const customFieldNames = ref([]);
|
|
101
100
|
const selected = ref<any[]>([]);
|
|
101
|
+
const carouselSaveImages = ref<any[]>([]);
|
|
102
|
+
const carouselImageIndex = ref<any[]>([]);
|
|
102
103
|
const isAiResponseReceivedAnalize = ref([]);
|
|
103
104
|
const isAiResponseReceivedImage = ref([]);
|
|
104
105
|
const primaryKey = props.meta.primaryKey;
|
|
105
106
|
const openGenerationCarousel = ref([]);
|
|
106
107
|
const isLoading = ref(false);
|
|
108
|
+
const isFetchingRecords = ref(false);
|
|
107
109
|
const isError = ref(false);
|
|
108
110
|
const isCriticalError = ref(false);
|
|
109
111
|
const isImageGenerationError = ref(false);
|
|
@@ -128,7 +130,7 @@ const openDialog = async () => {
|
|
|
128
130
|
return acc;
|
|
129
131
|
},{[primaryKey]: records.value[i][primaryKey]} as Record<string, boolean>);
|
|
130
132
|
}
|
|
131
|
-
|
|
133
|
+
isFetchingRecords.value = true;
|
|
132
134
|
const tasks = [];
|
|
133
135
|
if (props.meta.isFieldsForAnalizeFromImages) {
|
|
134
136
|
tasks.push(runAiAction({
|
|
@@ -152,7 +154,12 @@ const openDialog = async () => {
|
|
|
152
154
|
}));
|
|
153
155
|
}
|
|
154
156
|
await Promise.all(tasks);
|
|
155
|
-
|
|
157
|
+
|
|
158
|
+
if (props.meta.isImageGeneration) {
|
|
159
|
+
fillCarouselSaveImages();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
isFetchingRecords.value = false;
|
|
156
163
|
}
|
|
157
164
|
|
|
158
165
|
watch(selected, (val) => {
|
|
@@ -174,6 +181,23 @@ const closeDialog = () => {
|
|
|
174
181
|
isCriticalError.value = false;
|
|
175
182
|
isImageGenerationError.value = false;
|
|
176
183
|
errorMessage.value = '';
|
|
184
|
+
carouselSaveImages.value = [];
|
|
185
|
+
carouselImageIndex.value = [];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function fillCarouselSaveImages() {
|
|
189
|
+
for (const item of selected.value) {
|
|
190
|
+
const tempItem: any = {};
|
|
191
|
+
const tempItemIndex: any = {};
|
|
192
|
+
for (const [key, value] of Object.entries(item)) {
|
|
193
|
+
if (props.meta.outputImageFields?.includes(key)) {
|
|
194
|
+
tempItem[key] = [value];
|
|
195
|
+
tempItemIndex[key] = 0;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
carouselSaveImages.value.push(tempItem);
|
|
199
|
+
carouselImageIndex.value.push(tempItemIndex);
|
|
200
|
+
}
|
|
177
201
|
}
|
|
178
202
|
|
|
179
203
|
function formatLabel(str) {
|
|
@@ -270,7 +294,6 @@ async function getRecords() {
|
|
|
270
294
|
console.error('Failed to get records:', error);
|
|
271
295
|
isError.value = true;
|
|
272
296
|
errorMessage.value = `Failed to fetch records. Please, try to re-run the action.`;
|
|
273
|
-
// Handle error appropriately
|
|
274
297
|
}
|
|
275
298
|
}
|
|
276
299
|
|
|
@@ -288,7 +311,6 @@ async function getImages() {
|
|
|
288
311
|
console.error('Failed to get images:', error);
|
|
289
312
|
isError.value = true;
|
|
290
313
|
errorMessage.value = `Failed to fetch images. Please, try to re-run the action.`;
|
|
291
|
-
// Handle error appropriately
|
|
292
314
|
}
|
|
293
315
|
}
|
|
294
316
|
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
<Input
|
|
74
74
|
type="number"
|
|
75
75
|
v-model="selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n]"
|
|
76
|
-
class="w-full "
|
|
76
|
+
class="w-full min-w-[80px]"
|
|
77
77
|
:fullWidth="true"
|
|
78
78
|
/>
|
|
79
79
|
</div>
|
|
@@ -91,13 +91,15 @@
|
|
|
91
91
|
<div>
|
|
92
92
|
<GenerationCarousel
|
|
93
93
|
v-if="openGenerationCarousel[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n]"
|
|
94
|
-
:images="
|
|
94
|
+
:images="carouselSaveImages[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n]"
|
|
95
95
|
:recordId="item[primaryKey]"
|
|
96
96
|
:meta="props.meta"
|
|
97
97
|
:fieldName="n"
|
|
98
|
+
:carouselImageIndex="carouselImageIndex[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n]"
|
|
98
99
|
@error="handleError"
|
|
99
100
|
@close="openGenerationCarousel[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] = false"
|
|
100
101
|
@selectImage="updateSelectedImage"
|
|
102
|
+
@updateCarouselIndex="updateActiveIndex"
|
|
101
103
|
/>
|
|
102
104
|
</div>
|
|
103
105
|
</div>
|
|
@@ -119,7 +121,7 @@
|
|
|
119
121
|
import { ref, nextTick, watch } from 'vue'
|
|
120
122
|
import mediumZoom from 'medium-zoom'
|
|
121
123
|
import { Select, Input, Textarea, Table, Checkbox, Skeleton, Toggle } from '@/afcl'
|
|
122
|
-
import GenerationCarousel from './
|
|
124
|
+
import GenerationCarousel from './ImageGenerationCarousel.vue'
|
|
123
125
|
|
|
124
126
|
const props = defineProps<{
|
|
125
127
|
meta: any,
|
|
@@ -134,6 +136,8 @@ const props = defineProps<{
|
|
|
134
136
|
openGenerationCarousel: any
|
|
135
137
|
isError: boolean,
|
|
136
138
|
errorMessage: string
|
|
139
|
+
carouselSaveImages: any[]
|
|
140
|
+
carouselImageIndex: any[]
|
|
137
141
|
}>();
|
|
138
142
|
const emit = defineEmits(['error']);
|
|
139
143
|
|
|
@@ -193,4 +197,8 @@ function handleError({ isError, errorMessage }) {
|
|
|
193
197
|
});
|
|
194
198
|
}
|
|
195
199
|
|
|
200
|
+
function updateActiveIndex(newIndex: number, id: any, fieldName: string) {
|
|
201
|
+
props.carouselImageIndex[props.tableColumnsIndexes.findIndex(el => el[props.primaryKey] === id)][fieldName] = newIndex;
|
|
202
|
+
}
|
|
203
|
+
|
|
196
204
|
</script>
|
|
@@ -175,10 +175,6 @@
|
|
|
175
175
|
</div>
|
|
176
176
|
</div>
|
|
177
177
|
</div>
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
178
|
</template>
|
|
183
179
|
|
|
184
180
|
<script setup lang="ts">
|
|
@@ -193,8 +189,8 @@ import { ProgressBar } from '@/afcl';
|
|
|
193
189
|
const { t: $t } = useI18n();
|
|
194
190
|
|
|
195
191
|
const prompt = ref('');
|
|
196
|
-
const emit = defineEmits(['close', 'selectImage', 'error']);
|
|
197
|
-
const props = defineProps(['meta', 'record', 'images', 'recordId', 'prompt', 'fieldName', 'isError', 'errorMessage']);
|
|
192
|
+
const emit = defineEmits(['close', 'selectImage', 'error', 'updateCarouselIndex']);
|
|
193
|
+
const props = defineProps(['meta', 'record', 'images', 'recordId', 'prompt', 'fieldName', 'isError', 'errorMessage', 'carouselImageIndex']);
|
|
198
194
|
const images = ref([]);
|
|
199
195
|
const loading = ref(false);
|
|
200
196
|
const attachmentFiles = ref<string[]>([])
|
|
@@ -208,12 +204,14 @@ function minifyField(field: string): string {
|
|
|
208
204
|
|
|
209
205
|
const caurosel = ref(null);
|
|
210
206
|
onMounted(async () => {
|
|
211
|
-
|
|
207
|
+
for (const img of props.images || []) {
|
|
208
|
+
images.value.push(img);
|
|
209
|
+
}
|
|
212
210
|
const temp = await getGenerationPrompt() || '';
|
|
213
211
|
prompt.value = temp[props.fieldName];
|
|
214
212
|
await nextTick();
|
|
215
213
|
|
|
216
|
-
const currentIndex =
|
|
214
|
+
const currentIndex = props.carouselImageIndex || 0;
|
|
217
215
|
caurosel.value = new Carousel(
|
|
218
216
|
document.getElementById('gallery'),
|
|
219
217
|
images.value.map((img, index) => {
|
|
@@ -298,6 +296,12 @@ async function confirmImage() {
|
|
|
298
296
|
const currentIndex = caurosel.value?.getActiveItem()?.position || 0;
|
|
299
297
|
const img = images.value[currentIndex];
|
|
300
298
|
|
|
299
|
+
props.images.splice(0, props.images.length);
|
|
300
|
+
for (const i of images.value) {
|
|
301
|
+
props.images.push(i);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
emit('updateCarouselIndex', currentIndex, props.recordId, props.fieldName);
|
|
301
305
|
emit('selectImage', img, props.recordId, props.fieldName);
|
|
302
306
|
emit('close');
|
|
303
307
|
|
|
@@ -25,7 +25,6 @@
|
|
|
25
25
|
<VisionTable
|
|
26
26
|
:checkbox="props.checkboxes"
|
|
27
27
|
:records="records"
|
|
28
|
-
:index="0"
|
|
29
28
|
:meta="props.meta"
|
|
30
29
|
:images="images"
|
|
31
30
|
:tableHeaders="tableHeaders"
|
|
@@ -38,6 +37,8 @@
|
|
|
38
37
|
:primaryKey="primaryKey"
|
|
39
38
|
:openGenerationCarousel="openGenerationCarousel"
|
|
40
39
|
@error="handleTableError"
|
|
40
|
+
:carouselSaveImages="carouselSaveImages"
|
|
41
|
+
:carouselImageIndex="carouselImageIndex"
|
|
41
42
|
/>
|
|
42
43
|
</div>
|
|
43
44
|
<div class="flex w-full flex-col md:flex-row items-stretch md:items-end justify-end gap-3 md:gap-4">
|
|
@@ -52,7 +53,7 @@
|
|
|
52
53
|
<Button
|
|
53
54
|
class="w-full md:w-64"
|
|
54
55
|
@click="saveData"
|
|
55
|
-
:disabled="isLoading || checkedCount < 1 || isCriticalError"
|
|
56
|
+
:disabled="isLoading || checkedCount < 1 || isCriticalError || isFetchingRecords"
|
|
56
57
|
:loader="isLoading"
|
|
57
58
|
>
|
|
58
59
|
{{ checkedCount > 1 ? 'Save fields' : 'Save field' }}
|
|
@@ -68,13 +69,11 @@
|
|
|
68
69
|
import { callAdminForthApi } from '@/utils';
|
|
69
70
|
import { Ref, ref, watch } from 'vue'
|
|
70
71
|
import { Dialog, Button } from '@/afcl';
|
|
71
|
-
import VisionTable from './
|
|
72
|
+
import VisionTable from './VisionTable.vue'
|
|
72
73
|
import adminforth from '@/adminforth';
|
|
73
74
|
import { useI18n } from 'vue-i18n';
|
|
74
|
-
import { useRoute } from 'vue-router';
|
|
75
75
|
import { AdminUser, type AdminForthResourceCommon } from '@/types';
|
|
76
76
|
|
|
77
|
-
const route = useRoute();
|
|
78
77
|
const { t } = useI18n();
|
|
79
78
|
|
|
80
79
|
const props = defineProps<{
|
|
@@ -99,11 +98,14 @@ const tableColumns = ref([]);
|
|
|
99
98
|
const tableColumnsIndexes = ref([]);
|
|
100
99
|
const customFieldNames = ref([]);
|
|
101
100
|
const selected = ref<any[]>([]);
|
|
101
|
+
const carouselSaveImages = ref<any[]>([]);
|
|
102
|
+
const carouselImageIndex = ref<any[]>([]);
|
|
102
103
|
const isAiResponseReceivedAnalize = ref([]);
|
|
103
104
|
const isAiResponseReceivedImage = ref([]);
|
|
104
105
|
const primaryKey = props.meta.primaryKey;
|
|
105
106
|
const openGenerationCarousel = ref([]);
|
|
106
107
|
const isLoading = ref(false);
|
|
108
|
+
const isFetchingRecords = ref(false);
|
|
107
109
|
const isError = ref(false);
|
|
108
110
|
const isCriticalError = ref(false);
|
|
109
111
|
const isImageGenerationError = ref(false);
|
|
@@ -128,7 +130,7 @@ const openDialog = async () => {
|
|
|
128
130
|
return acc;
|
|
129
131
|
},{[primaryKey]: records.value[i][primaryKey]} as Record<string, boolean>);
|
|
130
132
|
}
|
|
131
|
-
|
|
133
|
+
isFetchingRecords.value = true;
|
|
132
134
|
const tasks = [];
|
|
133
135
|
if (props.meta.isFieldsForAnalizeFromImages) {
|
|
134
136
|
tasks.push(runAiAction({
|
|
@@ -152,7 +154,12 @@ const openDialog = async () => {
|
|
|
152
154
|
}));
|
|
153
155
|
}
|
|
154
156
|
await Promise.all(tasks);
|
|
155
|
-
|
|
157
|
+
|
|
158
|
+
if (props.meta.isImageGeneration) {
|
|
159
|
+
fillCarouselSaveImages();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
isFetchingRecords.value = false;
|
|
156
163
|
}
|
|
157
164
|
|
|
158
165
|
watch(selected, (val) => {
|
|
@@ -174,6 +181,23 @@ const closeDialog = () => {
|
|
|
174
181
|
isCriticalError.value = false;
|
|
175
182
|
isImageGenerationError.value = false;
|
|
176
183
|
errorMessage.value = '';
|
|
184
|
+
carouselSaveImages.value = [];
|
|
185
|
+
carouselImageIndex.value = [];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function fillCarouselSaveImages() {
|
|
189
|
+
for (const item of selected.value) {
|
|
190
|
+
const tempItem: any = {};
|
|
191
|
+
const tempItemIndex: any = {};
|
|
192
|
+
for (const [key, value] of Object.entries(item)) {
|
|
193
|
+
if (props.meta.outputImageFields?.includes(key)) {
|
|
194
|
+
tempItem[key] = [value];
|
|
195
|
+
tempItemIndex[key] = 0;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
carouselSaveImages.value.push(tempItem);
|
|
199
|
+
carouselImageIndex.value.push(tempItemIndex);
|
|
200
|
+
}
|
|
177
201
|
}
|
|
178
202
|
|
|
179
203
|
function formatLabel(str) {
|
|
@@ -270,7 +294,6 @@ async function getRecords() {
|
|
|
270
294
|
console.error('Failed to get records:', error);
|
|
271
295
|
isError.value = true;
|
|
272
296
|
errorMessage.value = `Failed to fetch records. Please, try to re-run the action.`;
|
|
273
|
-
// Handle error appropriately
|
|
274
297
|
}
|
|
275
298
|
}
|
|
276
299
|
|
|
@@ -288,7 +311,6 @@ async function getImages() {
|
|
|
288
311
|
console.error('Failed to get images:', error);
|
|
289
312
|
isError.value = true;
|
|
290
313
|
errorMessage.value = `Failed to fetch images. Please, try to re-run the action.`;
|
|
291
|
-
// Handle error appropriately
|
|
292
314
|
}
|
|
293
315
|
}
|
|
294
316
|
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
<Input
|
|
74
74
|
type="number"
|
|
75
75
|
v-model="selected[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n]"
|
|
76
|
-
class="w-full "
|
|
76
|
+
class="w-full min-w-[80px]"
|
|
77
77
|
:fullWidth="true"
|
|
78
78
|
/>
|
|
79
79
|
</div>
|
|
@@ -91,13 +91,15 @@
|
|
|
91
91
|
<div>
|
|
92
92
|
<GenerationCarousel
|
|
93
93
|
v-if="openGenerationCarousel[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n]"
|
|
94
|
-
:images="
|
|
94
|
+
:images="carouselSaveImages[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n]"
|
|
95
95
|
:recordId="item[primaryKey]"
|
|
96
96
|
:meta="props.meta"
|
|
97
97
|
:fieldName="n"
|
|
98
|
+
:carouselImageIndex="carouselImageIndex[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n]"
|
|
98
99
|
@error="handleError"
|
|
99
100
|
@close="openGenerationCarousel[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] = false"
|
|
100
101
|
@selectImage="updateSelectedImage"
|
|
102
|
+
@updateCarouselIndex="updateActiveIndex"
|
|
101
103
|
/>
|
|
102
104
|
</div>
|
|
103
105
|
</div>
|
|
@@ -119,7 +121,7 @@
|
|
|
119
121
|
import { ref, nextTick, watch } from 'vue'
|
|
120
122
|
import mediumZoom from 'medium-zoom'
|
|
121
123
|
import { Select, Input, Textarea, Table, Checkbox, Skeleton, Toggle } from '@/afcl'
|
|
122
|
-
import GenerationCarousel from './
|
|
124
|
+
import GenerationCarousel from './ImageGenerationCarousel.vue'
|
|
123
125
|
|
|
124
126
|
const props = defineProps<{
|
|
125
127
|
meta: any,
|
|
@@ -134,6 +136,8 @@ const props = defineProps<{
|
|
|
134
136
|
openGenerationCarousel: any
|
|
135
137
|
isError: boolean,
|
|
136
138
|
errorMessage: string
|
|
139
|
+
carouselSaveImages: any[]
|
|
140
|
+
carouselImageIndex: any[]
|
|
137
141
|
}>();
|
|
138
142
|
const emit = defineEmits(['error']);
|
|
139
143
|
|
|
@@ -193,4 +197,8 @@ function handleError({ isError, errorMessage }) {
|
|
|
193
197
|
});
|
|
194
198
|
}
|
|
195
199
|
|
|
200
|
+
function updateActiveIndex(newIndex: number, id: any, fieldName: string) {
|
|
201
|
+
props.carouselImageIndex[props.tableColumnsIndexes.findIndex(el => el[props.primaryKey] === id)][fieldName] = newIndex;
|
|
202
|
+
}
|
|
203
|
+
|
|
196
204
|
</script>
|
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,17 @@ 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 (
|
|
130
|
-
|
|
99
|
+
if (plugin && plugin.pluginOptions.storageAdapter.objectCanBeAccesedPublicly()) {
|
|
100
|
+
outputImagesPluginInstanceIds[key] = plugin.pluginInstanceId;
|
|
131
101
|
}
|
|
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
|
-
outputImagesPluginInstanceIds[key] = plugin.pluginInstanceId;
|
|
139
102
|
}
|
|
140
103
|
}
|
|
141
104
|
const outputFields = Object.assign(Object.assign(Object.assign({}, this.options.fillFieldsFromImages), this.options.fillPlainFields), (this.options.generateImages || {}));
|
|
142
105
|
const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
|
|
143
106
|
const pageInjection = {
|
|
144
|
-
file: this.componentPath('
|
|
107
|
+
file: this.componentPath('VisionAction.vue'),
|
|
145
108
|
meta: {
|
|
146
109
|
pluginInstanceId: this.pluginInstanceId,
|
|
147
110
|
outputFields: outputFields,
|
|
@@ -170,11 +133,57 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
170
133
|
});
|
|
171
134
|
}
|
|
172
135
|
validateConfigAfterDiscover(adminforth, resourceConfig) {
|
|
173
|
-
|
|
136
|
+
const columns = this.resourceConfig.columns;
|
|
137
|
+
if (this.options.fillFieldsFromImages) {
|
|
138
|
+
if (!this.options.attachFiles) {
|
|
139
|
+
throw new Error('⚠️ attachFiles function must be provided when fillFieldsFromImages is used');
|
|
140
|
+
}
|
|
141
|
+
if (!this.options.visionAdapter) {
|
|
142
|
+
throw new Error('⚠️ visionAdapter must be provided when fillFieldsFromImages is used');
|
|
143
|
+
}
|
|
144
|
+
for (const key of Object.keys(this.options.fillFieldsFromImages)) {
|
|
145
|
+
const column = columns.find(c => c.name.toLowerCase() === key.toLowerCase());
|
|
146
|
+
if (!column) {
|
|
147
|
+
throw new Error(`⚠️ No column found for key "${key}"`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (this.options.fillPlainFields) {
|
|
152
|
+
if (!this.options.textCompleteAdapter) {
|
|
153
|
+
throw new Error('⚠️ textCompleteAdapter must be provided when fillPlainFields is used');
|
|
154
|
+
}
|
|
155
|
+
for (const key of Object.keys(this.options.fillPlainFields)) {
|
|
156
|
+
const column = columns.find(c => c.name.toLowerCase() === key.toLowerCase());
|
|
157
|
+
if (!column) {
|
|
158
|
+
throw new Error(`⚠️ No column found for key "${key}"`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (this.options.generateImages) {
|
|
163
|
+
for (const key of Object.keys(this.options.generateImages)) {
|
|
164
|
+
const column = columns.find(c => c.name.toLowerCase() === key.toLowerCase());
|
|
165
|
+
if (!column) {
|
|
166
|
+
throw new Error(`⚠️ No column found for key "${key}"`);
|
|
167
|
+
}
|
|
168
|
+
const perKeyAdapter = this.options.generateImages[key].adapter;
|
|
169
|
+
if (!perKeyAdapter && !this.options.imageGenerationAdapter) {
|
|
170
|
+
throw new Error(`⚠️ No image generation adapter provided for key "${key}"`);
|
|
171
|
+
}
|
|
172
|
+
const plugin = adminforth.activatedPlugins.find(p => p.resourceConfig.resourceId === this.resourceConfig.resourceId &&
|
|
173
|
+
p.pluginOptions.pathColumnName === key);
|
|
174
|
+
if (!plugin) {
|
|
175
|
+
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}`);
|
|
176
|
+
}
|
|
177
|
+
if (!plugin.pluginOptions.storageAdapter.objectCanBeAccesedPublicly()) {
|
|
178
|
+
throw new Error(`Upload Plugin for attachment field '${key}' in resource '${this.resourceConfig.resourceId}'
|
|
179
|
+
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.
|
|
180
|
+
Please configure adapter in such way that it will store objects publicly (e.g. for S3 use 'public-read' ACL).
|
|
181
|
+
`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
174
185
|
}
|
|
175
186
|
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
187
|
return `${this.pluginOptions.actionName}`;
|
|
179
188
|
}
|
|
180
189
|
setupEndpoints(server) {
|
|
@@ -236,23 +245,21 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
236
245
|
}
|
|
237
246
|
}
|
|
238
247
|
const tasks = selectedIds.map((ID) => __awaiter(this, void 0, void 0, function* () {
|
|
239
|
-
// Fetch the record using the provided ID
|
|
240
248
|
const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
|
|
241
249
|
const record = yield this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(primaryKeyColumn.name, ID)]);
|
|
242
|
-
//create prompt for OpenAI
|
|
243
250
|
const compiledOutputFields = this.compileOutputFieldsTemplatesNoImage(record);
|
|
244
251
|
const prompt = `Analyze the following fields and return a single JSON in format like: {'param1': 'value1', 'param2': 'value2'}.
|
|
245
252
|
Do NOT return array of objects. Do NOT include any Markdown, code blocks, explanations, or extra text. Only return valid JSON.
|
|
246
253
|
Each object must contain the following fields: ${JSON.stringify(compiledOutputFields)} Use the exact field names.
|
|
247
254
|
If it's number field - return only number.`;
|
|
248
255
|
//send prompt to OpenAI and get response
|
|
249
|
-
const
|
|
256
|
+
const numberOfTokens = this.options.fillPlainFieldsMaxTokens ? this.options.fillPlainFieldsMaxTokens : 1000;
|
|
257
|
+
const { content: chatResponse } = yield this.options.textCompleteAdapter.complete(prompt, [], numberOfTokens);
|
|
250
258
|
const resp = chatResponse.response;
|
|
251
259
|
const topLevelError = chatResponse.error;
|
|
252
260
|
if (topLevelError || (resp === null || resp === void 0 ? void 0 : resp.error)) {
|
|
253
261
|
throw new Error(`ERROR: ${JSON.stringify(topLevelError || (resp === null || resp === void 0 ? void 0 : resp.error))}`);
|
|
254
262
|
}
|
|
255
|
-
//parse response and update record
|
|
256
263
|
const resData = JSON.parse(chatResponse);
|
|
257
264
|
return resData;
|
|
258
265
|
}));
|
|
@@ -270,8 +277,10 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
270
277
|
for (const [index, record] of records.entries()) {
|
|
271
278
|
records[index]._label = this.resourceConfig.recordLabel(records[index]);
|
|
272
279
|
}
|
|
280
|
+
const order = Object.fromEntries(body.body.record.map((id, i) => [id, i]));
|
|
281
|
+
const sortedRecords = records.sort((a, b) => order[a.id] - order[b.id]);
|
|
273
282
|
return {
|
|
274
|
-
records,
|
|
283
|
+
records: sortedRecords,
|
|
275
284
|
};
|
|
276
285
|
})
|
|
277
286
|
});
|
|
@@ -336,6 +345,32 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
336
345
|
}
|
|
337
346
|
}
|
|
338
347
|
}
|
|
348
|
+
try {
|
|
349
|
+
const AuditLogPlugin = this.adminforth.getPluginByClassName('AuditLogPlugin');
|
|
350
|
+
if (AuditLogPlugin) {
|
|
351
|
+
for (const [key, value] of Object.entries(oldRecord)) {
|
|
352
|
+
if (!(key in fieldsToUpdate[idx])) {
|
|
353
|
+
delete oldRecord[key];
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
const reorderedOldRecord = Object.keys(fieldsToUpdate[idx]).reduce((acc, key) => {
|
|
357
|
+
if (key in oldRecord) {
|
|
358
|
+
acc[key] = oldRecord[key];
|
|
359
|
+
}
|
|
360
|
+
return acc;
|
|
361
|
+
}, {});
|
|
362
|
+
AuditLogPlugin.logCustomAction({
|
|
363
|
+
resourceId: this.resourceConfig.resourceId,
|
|
364
|
+
recordId: ID,
|
|
365
|
+
actionId: 'Bulk-ai-flow',
|
|
366
|
+
oldData: reorderedOldRecord,
|
|
367
|
+
data: fieldsToUpdate[idx],
|
|
368
|
+
user: adminUser,
|
|
369
|
+
headers: headers
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
catch (error) { }
|
|
339
374
|
return this.adminforth.resource(this.resourceConfig.resourceId).update(ID, fieldsToUpdate[idx]);
|
|
340
375
|
}));
|
|
341
376
|
yield Promise.all(updates);
|
package/index.ts
CHANGED
|
@@ -73,58 +73,30 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
73
73
|
const columns = this.resourceConfig.columns;
|
|
74
74
|
let columnEnums = [];
|
|
75
75
|
if (this.options.fillFieldsFromImages) {
|
|
76
|
-
if (!this.options.attachFiles) {
|
|
77
|
-
throw new Error('⚠️ attachFiles function must be provided in options when fillFieldsFromImages is used');
|
|
78
|
-
}
|
|
79
|
-
if (!this.options.visionAdapter) {
|
|
80
|
-
throw new Error('⚠️ visionAdapter must be provided in options when fillFieldsFromImages is used');
|
|
81
|
-
}
|
|
82
|
-
|
|
83
76
|
for (const [key, value] of Object.entries((this.options.fillFieldsFromImages ))) {
|
|
84
77
|
const column = columns.find(c => c.name.toLowerCase() === key.toLowerCase());
|
|
85
|
-
if (column) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
} else {
|
|
94
|
-
throw new Error(`⚠️ No column found for key "${key}"`);
|
|
78
|
+
if (column && column.enum) {
|
|
79
|
+
(this.options.fillFieldsFromImages as any)[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
|
+
});
|
|
95
84
|
}
|
|
96
85
|
}
|
|
97
86
|
}
|
|
98
87
|
|
|
99
88
|
if (this.options.fillPlainFields) {
|
|
100
|
-
if (!this.options.textCompleteAdapter) {
|
|
101
|
-
throw new Error('⚠️ textCompleteAdapter must be provided in options when fillPlainFields is used');
|
|
102
|
-
}
|
|
103
|
-
|
|
104
89
|
for (const [key, value] of Object.entries((this.options.fillPlainFields))) {
|
|
105
90
|
const column = columns.find(c => c.name.toLowerCase() === key.toLowerCase());
|
|
106
|
-
if (column) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
} else {
|
|
115
|
-
throw new Error(`⚠️ No column found for key "${key}"`);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (this.options.generateImages && !this.options.imageGenerationAdapter) {
|
|
121
|
-
for (const [key, value] of Object.entries(this.options.generateImages)) {
|
|
122
|
-
if (!this.options.generateImages[key].adapter) {
|
|
123
|
-
throw new Error(`⚠️ No image generation adapter found for key "${key}"`);
|
|
91
|
+
if (column && column.enum) {
|
|
92
|
+
(this.options.fillPlainFields as any)[key] = `${value} Select ${key} from the list (USE ONLY VALUE FIELD. USE ONLY VALUES FROM THIS LIST): ${JSON.stringify(column.enum)}`;
|
|
93
|
+
columnEnums.push({
|
|
94
|
+
name: key,
|
|
95
|
+
enum: column.enum,
|
|
96
|
+
});
|
|
124
97
|
}
|
|
125
98
|
}
|
|
126
|
-
}
|
|
127
|
-
|
|
99
|
+
}
|
|
128
100
|
|
|
129
101
|
const outputImageFields = [];
|
|
130
102
|
if (this.options.generateImages) {
|
|
@@ -136,25 +108,13 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
136
108
|
//check if Upload plugin is installed on all attachment fields
|
|
137
109
|
if (this.options.generateImages) {
|
|
138
110
|
for (const [key, value] of Object.entries(this.options.generateImages)) {
|
|
139
|
-
const column = columns.find(c => c.name.toLowerCase() === key.toLowerCase());
|
|
140
|
-
if (!column) {
|
|
141
|
-
throw new Error(`⚠️ No column found for key "${key}"`);
|
|
142
|
-
}
|
|
143
111
|
const plugin = adminforth.activatedPlugins.find(p =>
|
|
144
112
|
p.resourceConfig!.resourceId === this.resourceConfig.resourceId &&
|
|
145
113
|
p.pluginOptions.pathColumnName === key
|
|
146
114
|
);
|
|
147
|
-
if (
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
if (!plugin.pluginOptions.storageAdapter.objectCanBeAccesedPublicly()) {
|
|
151
|
-
throw new Error(`Upload Plugin for attachment field '${key}' in resource '${this.resourceConfig.resourceId}'
|
|
152
|
-
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.
|
|
153
|
-
Please configure adapter in such way that it will store objects publicly (e.g. for S3 use 'public-read' ACL).
|
|
154
|
-
`);
|
|
115
|
+
if (plugin && plugin.pluginOptions.storageAdapter.objectCanBeAccesedPublicly()) {
|
|
116
|
+
outputImagesPluginInstanceIds[key] = plugin.pluginInstanceId;
|
|
155
117
|
}
|
|
156
|
-
|
|
157
|
-
outputImagesPluginInstanceIds[key] = plugin.pluginInstanceId;
|
|
158
118
|
}
|
|
159
119
|
}
|
|
160
120
|
|
|
@@ -168,7 +128,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
168
128
|
const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
|
|
169
129
|
|
|
170
130
|
const pageInjection = {
|
|
171
|
-
file: this.componentPath('
|
|
131
|
+
file: this.componentPath('VisionAction.vue'),
|
|
172
132
|
meta: {
|
|
173
133
|
pluginInstanceId: this.pluginInstanceId,
|
|
174
134
|
outputFields: outputFields,
|
|
@@ -199,18 +159,65 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
199
159
|
}
|
|
200
160
|
|
|
201
161
|
validateConfigAfterDiscover(adminforth: IAdminForth, resourceConfig: AdminForthResource) {
|
|
202
|
-
|
|
162
|
+
const columns = this.resourceConfig.columns;
|
|
163
|
+
if (this.options.fillFieldsFromImages) {
|
|
164
|
+
if (!this.options.attachFiles) {
|
|
165
|
+
throw new Error('⚠️ attachFiles function must be provided when fillFieldsFromImages is used');
|
|
166
|
+
}
|
|
167
|
+
if (!this.options.visionAdapter) {
|
|
168
|
+
throw new Error('⚠️ visionAdapter must be provided when fillFieldsFromImages is used');
|
|
169
|
+
}
|
|
170
|
+
for (const key of Object.keys(this.options.fillFieldsFromImages)) {
|
|
171
|
+
const column = columns.find(c => c.name.toLowerCase() === key.toLowerCase());
|
|
172
|
+
if (!column) {
|
|
173
|
+
throw new Error(`⚠️ No column found for key "${key}"`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (this.options.fillPlainFields) {
|
|
178
|
+
if (!this.options.textCompleteAdapter) {
|
|
179
|
+
throw new Error('⚠️ textCompleteAdapter must be provided when fillPlainFields is used');
|
|
180
|
+
}
|
|
181
|
+
for (const key of Object.keys(this.options.fillPlainFields)) {
|
|
182
|
+
const column = columns.find(c => c.name.toLowerCase() === key.toLowerCase());
|
|
183
|
+
if (!column) {
|
|
184
|
+
throw new Error(`⚠️ No column found for key "${key}"`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (this.options.generateImages) {
|
|
189
|
+
for (const key of Object.keys(this.options.generateImages)) {
|
|
190
|
+
const column = columns.find(c => c.name.toLowerCase() === key.toLowerCase());
|
|
191
|
+
if (!column) {
|
|
192
|
+
throw new Error(`⚠️ No column found for key "${key}"`);
|
|
193
|
+
}
|
|
194
|
+
const perKeyAdapter = this.options.generateImages[key].adapter;
|
|
195
|
+
if (!perKeyAdapter && !this.options.imageGenerationAdapter) {
|
|
196
|
+
throw new Error(`⚠️ No image generation adapter provided for key "${key}"`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const plugin = adminforth.activatedPlugins.find(p =>
|
|
200
|
+
p.resourceConfig!.resourceId === this.resourceConfig.resourceId &&
|
|
201
|
+
p.pluginOptions.pathColumnName === key
|
|
202
|
+
);
|
|
203
|
+
if (!plugin) {
|
|
204
|
+
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}`);
|
|
205
|
+
}
|
|
206
|
+
if (!plugin.pluginOptions.storageAdapter.objectCanBeAccesedPublicly()) {
|
|
207
|
+
throw new Error(`Upload Plugin for attachment field '${key}' in resource '${this.resourceConfig.resourceId}'
|
|
208
|
+
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.
|
|
209
|
+
Please configure adapter in such way that it will store objects publicly (e.g. for S3 use 'public-read' ACL).
|
|
210
|
+
`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
203
214
|
}
|
|
204
215
|
|
|
205
216
|
instanceUniqueRepresentation(pluginOptions: any) : string {
|
|
206
|
-
// optional method to return unique string representation of plugin instance.
|
|
207
|
-
// Needed if plugin can have multiple instances on one resource
|
|
208
217
|
return `${this.pluginOptions.actionName}`;
|
|
209
218
|
}
|
|
210
219
|
|
|
211
220
|
setupEndpoints(server: IHttpServer) {
|
|
212
|
-
|
|
213
|
-
|
|
214
221
|
server.endpoint({
|
|
215
222
|
method: 'POST',
|
|
216
223
|
path: `/plugin/${this.pluginInstanceId}/analyze`,
|
|
@@ -224,7 +231,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
224
231
|
const tasks = selectedIds.map(async (ID) => {
|
|
225
232
|
// Fetch the record using the provided ID
|
|
226
233
|
const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
|
|
227
|
-
const record = await this.adminforth.resource(this.resourceConfig.resourceId).get(
|
|
234
|
+
const record = await this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(primaryKeyColumn.name, ID)] );
|
|
228
235
|
|
|
229
236
|
//recieve image URLs to analyze
|
|
230
237
|
const attachmentFiles = await this.options.attachFiles({ record: record });
|
|
@@ -274,26 +281,23 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
274
281
|
}
|
|
275
282
|
}
|
|
276
283
|
const tasks = selectedIds.map(async (ID) => {
|
|
277
|
-
// Fetch the record using the provided ID
|
|
278
284
|
const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
|
|
279
285
|
const record = await this.adminforth.resource(this.resourceConfig.resourceId).get( [Filters.EQ(primaryKeyColumn.name, ID)] );
|
|
280
286
|
|
|
281
|
-
//create prompt for OpenAI
|
|
282
287
|
const compiledOutputFields = this.compileOutputFieldsTemplatesNoImage(record);
|
|
283
288
|
const prompt = `Analyze the following fields and return a single JSON in format like: {'param1': 'value1', 'param2': 'value2'}.
|
|
284
289
|
Do NOT return array of objects. Do NOT include any Markdown, code blocks, explanations, or extra text. Only return valid JSON.
|
|
285
290
|
Each object must contain the following fields: ${JSON.stringify(compiledOutputFields)} Use the exact field names.
|
|
286
291
|
If it's number field - return only number.`;
|
|
287
292
|
//send prompt to OpenAI and get response
|
|
288
|
-
const
|
|
293
|
+
const numberOfTokens = this.options.fillPlainFieldsMaxTokens ? this.options.fillPlainFieldsMaxTokens : 1000;
|
|
294
|
+
const { content: chatResponse } = await this.options.textCompleteAdapter.complete(prompt, [], numberOfTokens);
|
|
289
295
|
|
|
290
296
|
const resp: any = (chatResponse as any).response;
|
|
291
297
|
const topLevelError = (chatResponse as any).error;
|
|
292
298
|
if (topLevelError || resp?.error) {
|
|
293
299
|
throw new Error(`ERROR: ${JSON.stringify(topLevelError || resp?.error)}`);
|
|
294
300
|
}
|
|
295
|
-
|
|
296
|
-
//parse response and update record
|
|
297
301
|
const resData = JSON.parse(chatResponse);
|
|
298
302
|
|
|
299
303
|
return resData;
|
|
@@ -304,8 +308,6 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
304
308
|
return { result };
|
|
305
309
|
}
|
|
306
310
|
});
|
|
307
|
-
|
|
308
|
-
|
|
309
311
|
server.endpoint({
|
|
310
312
|
method: 'POST',
|
|
311
313
|
path: `/plugin/${this.pluginInstanceId}/get_records`,
|
|
@@ -316,13 +318,16 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
316
318
|
for( const [index, record] of records.entries() ) {
|
|
317
319
|
records[index]._label = this.resourceConfig.recordLabel(records[index]);
|
|
318
320
|
}
|
|
321
|
+
const order = Object.fromEntries(body.body.record.map((id, i) => [id, i]));
|
|
322
|
+
|
|
323
|
+
const sortedRecords = records.sort(
|
|
324
|
+
(a, b) => order[a.id] - order[b.id]
|
|
325
|
+
);
|
|
319
326
|
return {
|
|
320
|
-
records,
|
|
327
|
+
records: sortedRecords,
|
|
321
328
|
};
|
|
322
329
|
}
|
|
323
330
|
});
|
|
324
|
-
|
|
325
|
-
|
|
326
331
|
server.endpoint({
|
|
327
332
|
method: 'POST',
|
|
328
333
|
path: `/plugin/${this.pluginInstanceId}/get_images`,
|
|
@@ -340,8 +345,6 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
340
345
|
};
|
|
341
346
|
}
|
|
342
347
|
});
|
|
343
|
-
|
|
344
|
-
|
|
345
348
|
server.endpoint({
|
|
346
349
|
method: 'POST',
|
|
347
350
|
path: `/plugin/${this.pluginInstanceId}/update_fields`,
|
|
@@ -387,6 +390,35 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
387
390
|
}
|
|
388
391
|
}
|
|
389
392
|
}
|
|
393
|
+
try {
|
|
394
|
+
const AuditLogPlugin:any = this.adminforth.getPluginByClassName('AuditLogPlugin');
|
|
395
|
+
if (AuditLogPlugin) {
|
|
396
|
+
|
|
397
|
+
for (const [key, value] of Object.entries(oldRecord)) {
|
|
398
|
+
if (!(key in fieldsToUpdate[idx])) {
|
|
399
|
+
delete oldRecord[key];
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const reorderedOldRecord = Object.keys(fieldsToUpdate[idx]).reduce((acc, key) => {
|
|
404
|
+
if (key in oldRecord) {
|
|
405
|
+
acc[key] = oldRecord[key];
|
|
406
|
+
}
|
|
407
|
+
return acc;
|
|
408
|
+
}, {} as Record<string, unknown>);
|
|
409
|
+
|
|
410
|
+
AuditLogPlugin.logCustomAction({
|
|
411
|
+
resourceId: this.resourceConfig.resourceId,
|
|
412
|
+
recordId: ID,
|
|
413
|
+
actionId: 'Bulk-ai-flow',
|
|
414
|
+
oldData: reorderedOldRecord,
|
|
415
|
+
data: fieldsToUpdate[idx],
|
|
416
|
+
user: adminUser,
|
|
417
|
+
headers: headers
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
} catch (error) { }
|
|
421
|
+
|
|
390
422
|
return this.adminforth.resource(this.resourceConfig.resourceId).update(ID, fieldsToUpdate[idx])
|
|
391
423
|
});
|
|
392
424
|
await Promise.all(updates);
|
|
@@ -396,8 +428,6 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
396
428
|
}
|
|
397
429
|
}
|
|
398
430
|
});
|
|
399
|
-
|
|
400
|
-
|
|
401
431
|
server.endpoint({
|
|
402
432
|
method: 'POST',
|
|
403
433
|
path: `/plugin/${this.pluginInstanceId}/regenerate_images`,
|
|
@@ -449,8 +479,6 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
449
479
|
return { images };
|
|
450
480
|
}
|
|
451
481
|
});
|
|
452
|
-
|
|
453
|
-
|
|
454
482
|
server.endpoint({
|
|
455
483
|
method: 'POST',
|
|
456
484
|
path: `/plugin/${this.pluginInstanceId}/initial_image_generate`,
|
|
@@ -518,8 +546,6 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
518
546
|
return { result };
|
|
519
547
|
}
|
|
520
548
|
});
|
|
521
|
-
|
|
522
|
-
|
|
523
549
|
server.endpoint({
|
|
524
550
|
method: 'POST',
|
|
525
551
|
path: `/plugin/${this.pluginInstanceId}/get_generation_prompts`,
|
|
@@ -530,8 +556,6 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
530
556
|
return { generationOptions: compiledGenerationOptions };
|
|
531
557
|
}
|
|
532
558
|
});
|
|
533
|
-
|
|
534
|
-
|
|
535
559
|
server.endpoint({
|
|
536
560
|
method: 'GET',
|
|
537
561
|
path: `/plugin/${this.pluginInstanceId}/averageDuration`,
|
|
@@ -543,8 +567,5 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
543
567
|
};
|
|
544
568
|
}
|
|
545
569
|
});
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
570
|
}
|
|
550
571
|
}
|
package/package.json
CHANGED
package/types.ts
CHANGED
|
@@ -1,20 +1,52 @@
|
|
|
1
|
-
import { ImageVisionAdapter,
|
|
1
|
+
import { ImageVisionAdapter, ImageGenerationAdapter, CompletionAdapter } from "adminforth";
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
export interface PluginOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Name of the action in three dots menu.
|
|
7
|
+
*/
|
|
5
8
|
actionName: string,
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The adapter to use for scaning images and filling fields basing on the image content.
|
|
12
|
+
*/
|
|
6
13
|
visionAdapter?: ImageVisionAdapter,
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The adapter to use for text->text generation.
|
|
17
|
+
*/
|
|
7
18
|
textCompleteAdapter?: CompletionAdapter,
|
|
19
|
+
|
|
8
20
|
/**
|
|
9
21
|
* The adapter to use for image generation.
|
|
10
22
|
*/
|
|
11
23
|
imageGenerationAdapter?: ImageGenerationAdapter,
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* List of fields that should be filled based on the image content analysis.
|
|
27
|
+
*/
|
|
12
28
|
fillFieldsFromImages?: Record<string, string>, // can analyze what is on image and fill fields, typical tasks "find dominant color", "describe what is on image", "clasify to one enum item, e.g. what is on image dog/cat/plant"
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* List of fields that should be filled based on the text.
|
|
32
|
+
*/
|
|
13
33
|
fillPlainFields?: Record<string, string>,
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Number of tokens to generate (Only for text completion adapter). Default is 1000. 1 token ~= ¾ words
|
|
37
|
+
*/
|
|
38
|
+
fillPlainFieldsMaxTokens?: number,
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* If you want to generate fields or images based on the image content attached images
|
|
42
|
+
*/
|
|
14
43
|
attachFiles?: ({ record }: {
|
|
15
44
|
record: any,
|
|
16
45
|
}) => string[] | Promise<string[]>,
|
|
17
46
|
|
|
47
|
+
/**
|
|
48
|
+
* List of image fields, that should be filled.
|
|
49
|
+
*/
|
|
18
50
|
generateImages?: Record<
|
|
19
51
|
string, {
|
|
20
52
|
// can generate from images or just from another fields, e.g. "remove text from images", "improve image quality", "turn image into ghibli style"
|
|
@@ -43,6 +75,10 @@ export interface PluginOptions {
|
|
|
43
75
|
*/
|
|
44
76
|
countToGenerate: number,
|
|
45
77
|
}>,
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Rate limits for each action.
|
|
81
|
+
*/
|
|
46
82
|
rateLimits?: {
|
|
47
83
|
fillFieldsFromImages?: string, // e.g. 5/1d - 5 requests per day
|
|
48
84
|
fillPlainFields?: string,
|