@adminforth/bulk-ai-flow 1.14.6 → 1.15.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/build.log CHANGED
@@ -13,5 +13,5 @@ custom/package-lock.json
13
13
  custom/package.json
14
14
  custom/tsconfig.json
15
15
 
16
- sent 74,310 bytes received 172 bytes 148,964.00 bytes/sec
17
- total size is 73,669 speedup is 0.99
16
+ sent 80,944 bytes received 172 bytes 162,232.00 bytes/sec
17
+ total size is 80,312 speedup is 0.99
@@ -143,7 +143,7 @@ const sliderRef = ref(null)
143
143
 
144
144
  const prompt = ref('');
145
145
  const emit = defineEmits(['close', 'selectImage', 'error', 'updateCarouselIndex']);
146
- const props = defineProps(['meta', 'record', 'images', 'recordId', 'prompt', 'fieldName', 'isError', 'errorMessage', 'carouselImageIndex', 'regenerateImagesRefreshRate','sourceImage']);
146
+ const props = defineProps(['meta', 'record', 'images', 'recordId', 'prompt', 'fieldName', 'isError', 'errorMessage', 'carouselImageIndex', 'regenerateImagesRefreshRate','sourceImage', 'imageGenerationPrompts']);
147
147
  const images = ref([]);
148
148
  const loading = ref(false);
149
149
  const attachmentFiles = ref<string[]>([])
@@ -154,7 +154,7 @@ onMounted(async () => {
154
154
  }
155
155
  const temp = await getGenerationPrompt() || '';
156
156
  attachmentFiles.value = props.sourceImage || [];
157
- prompt.value = temp[props.fieldName];
157
+ prompt.value = Object.keys(JSON.parse(temp))[0];
158
158
  await nextTick();
159
159
 
160
160
  const currentIndex = props.carouselImageIndex || 0;
@@ -212,12 +212,20 @@ async function getHistoricalAverage() {
212
212
  }
213
213
 
214
214
  async function getGenerationPrompt() {
215
- try{
215
+ const [key, ...rest] = props.imageGenerationPrompts.split(":");
216
+ const value = rest.join(":").trim();
217
+
218
+ const json = {
219
+ [key.trim()]: value
220
+ };
221
+
222
+ try {
216
223
  const resp = await callAdminForthApi({
217
- path: `/plugin/${props.meta.pluginInstanceId}/get_generation_prompts`,
224
+ path: `/plugin/${props.meta.pluginInstanceId}/get_image_generation_prompts`,
218
225
  method: 'POST',
219
226
  body: {
220
227
  recordId: props.recordId,
228
+ customPrompt: JSON.stringify(json) || {},
221
229
  },
222
230
  });
223
231
  if(!resp) {
@@ -226,7 +234,7 @@ async function getGenerationPrompt() {
226
234
  errorMessage: "Error getting generation prompts."
227
235
  });
228
236
  }
229
- return resp?.generationOptions || null;
237
+ return resp?.prompt || null;
230
238
  } catch (e) {
231
239
  emit('error', {
232
240
  isError: true,
@@ -8,16 +8,49 @@
8
8
  <Dialog
9
9
  ref="confirmDialog"
10
10
  header="Bulk AI Flow"
11
- class="[scrollbar-gutter:stable] !max-w-full w-full lg:w-[1600px] !lg:max-w-[1600px]"
11
+ class="[scrollbar-gutter:stable] !max-w-full w-fit h-fit"
12
+ :class="popupMode === 'generation' ? 'lg:w-[1600px] !lg:max-w-[1600px]'
13
+ : popupMode === 'settings' ? 'lg:w-[1000px] !lg:max-w-[1000px]'
14
+ : 'lg:w-[500px] !lg:max-w-[500px]'"
12
15
  :beforeCloseFunction="closeDialog"
13
- :buttons="[
14
- { label: checkedCount > 1 ? 'Save fields' : 'Save field', options: { disabled: isLoading || checkedCount < 1 || isCriticalError || isFetchingRecords || isGeneratingImages || isAnalizingFields || isAnalizingImages, loader: isLoading, class: 'w-fit' }, onclick: async (dialog) => { await saveData(); dialog.hide(); } },
15
- { label: 'Cancel', options: {class: 'bg-white hover:!bg-gray-100 !text-gray-900 hover:!text-gray-800 dark:!bg-gray-800 dark:!text-gray-100 dark:hover:!bg-gray-700 !border-gray-200'}, onclick: (dialog) => dialog.hide() },
16
- ]"
16
+ :buttons="popupMode === 'generation' ? [
17
+ {
18
+ label: checkedCount > 1 ? 'Save fields' : 'Save field',
19
+ options: {
20
+ disabled: isLoading || checkedCount < 1 || isCriticalError || isFetchingRecords || isGeneratingImages || isAnalizingFields || isAnalizingImages,
21
+ loader: isLoading, class: 'w-fit'
22
+ },
23
+ onclick: async (dialog) => { await saveData(); dialog.hide(); }
24
+ },
25
+ {
26
+ label: 'Cancel',
27
+ options: {
28
+ class: 'bg-white hover:!bg-gray-100 !text-gray-900 hover:!text-gray-800 dark:!bg-gray-800 dark:!text-gray-100 dark:hover:!bg-gray-700 !border-gray-200'
29
+ },
30
+ onclick: (dialog) => dialog.hide()
31
+ },
32
+ ] : popupMode === 'settings' ? [
33
+ {
34
+ label: 'Save settings',
35
+ options: {
36
+ class: 'w-fit'
37
+ },
38
+ onclick: (dialog) => { saveSettings(); }
39
+ },
40
+ ] :
41
+ [
42
+ {
43
+ label: 'Edit prompts',
44
+ options: {
45
+ class: 'w-fit ml-auto'
46
+ },
47
+ onclick: (dialog) => { clickSettingsButton(); }
48
+ },
49
+ ]"
17
50
  :click-to-close-outside="false"
18
51
  >
19
- <div class="[scrollbar-gutter:stable] 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">
20
- <div v-if="records && props.checkboxes.length" class="w-full overflow-x-auto">
52
+ <div class="[scrollbar-gutter:stable] bulk-vision-table flex flex-col items-center max-w-[1560px] md:max-h-[75vh] gap-3 md:gap-4 w-full h-full overflow-y-auto">
53
+ <div v-if="records && props.checkboxes.length && popupMode === 'generation'" class="w-full overflow-x-auto">
21
54
  <VisionTable
22
55
  :checkbox="props.checkboxes"
23
56
  :records="records"
@@ -44,10 +77,40 @@
44
77
  :imageGenerationErrorMessage="imageGenerationErrorMessage"
45
78
  @regenerate-images="regenerateImages"
46
79
  :isImageHasPreviewUrl="isImageHasPreviewUrl"
80
+ :imageGenerationPrompts="generationPrompts.generateImages"
47
81
  />
82
+ <div class="text-red-600 flex items-center w-full">
83
+ <p v-if="isError === true">{{ errorMessage }}</p>
84
+ </div>
48
85
  </div>
49
- <div class="text-red-600 flex items-center w-full">
50
- <p v-if="isError === true">{{ errorMessage }}</p>
86
+ <div
87
+ v-else-if="popupMode === 'settings'"
88
+ v-for="(promptsCategory, key) in generationPrompts"
89
+ :key="key"
90
+ class="w-full"
91
+ >
92
+ <div v-if="Object.keys(promptsCategory).length > 0" class="gap-4 mb-6 ml-1">
93
+ <p class="text-start w-full text-xl font-bold mb-2">{{
94
+ key === "plainFieldsPrompts" ? "Prompts for non-image fields"
95
+ : key === "generateImages" ? "Prompts for image fields"
96
+ : "Prompts for image analysis"
97
+ }}</p>
98
+ <div class="grid grid-cols-2 gap-4">
99
+ <div v-for="(prompt, promptKey) in promptsCategory" :key="promptKey">
100
+ {{ formatLabel(promptKey) }} prompt:
101
+ <Textarea
102
+ v-model="generationPrompts[key][promptKey]"
103
+ class="w-full h-32 p-2 border border-gray-300 rounded-md resize-none focus:outline-none focus:ring-2 focus:ring-purple-500"
104
+ ></Textarea>
105
+ <p class="text-red-500 hover:underline hover:cursor-pointer mt-2" @click="resetPromptToDefault(key, promptKey)">reset to default</p>
106
+ </div>
107
+ </div>
108
+ </div>
109
+ </div>
110
+ <div v-else class="flex flex-col gap-2">
111
+ <Button @click="runAiActions" class="px-5 py-2.5 my-20 bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-purple-300 dark:focus:ring-purple-800 rounded-md text-white border-none">
112
+ Start generation
113
+ </Button>
51
114
  </div>
52
115
  </div>
53
116
  </Dialog>
@@ -56,11 +119,11 @@
56
119
  <script lang="ts" setup>
57
120
  import { callAdminForthApi } from '@/utils';
58
121
  import { Ref, ref, watch } from 'vue'
59
- import { Dialog, Button } from '@/afcl';
122
+ import { Dialog, Button, Textarea } from '@/afcl';
60
123
  import VisionTable from './VisionTable.vue'
61
124
  import adminforth from '@/adminforth';
62
125
  import { useI18n } from 'vue-i18n';
63
- import { AdminUser, type AdminForthResourceCommon } from '@/types';
126
+ import { AdminUser, type AdminForthResourceCommon } from '@/types/Common';
64
127
  import { run } from 'node:test';
65
128
 
66
129
  const { t } = useI18n();
@@ -114,6 +177,8 @@ const aiGenerationErrorMessage = ref<string[]>([]);
114
177
  const isAiImageGenerationError = ref<boolean[]>([false]);
115
178
  const imageGenerationErrorMessage = ref<string[]>([]);
116
179
  const isImageHasPreviewUrl = ref<Record<string, boolean>>({});
180
+ const popupMode = ref<'generation' | 'confirmation' | 'settings'>('confirmation');
181
+ const generationPrompts = ref<any>({});
117
182
 
118
183
  const openDialog = async () => {
119
184
  isDialogOpen.value = true;
@@ -144,8 +209,19 @@ const openDialog = async () => {
144
209
  },{[primaryKey]: records.value[i][primaryKey]} as Record<string, boolean>);
145
210
  }
146
211
  isFetchingRecords.value = false;
147
-
148
- if (props.meta.isImageGeneration) {
212
+ // Ensure prompts are loaded before any automatic AI action run
213
+ if (!generationPrompts.value || Object.keys(generationPrompts.value).length === 0) {
214
+ await getGenerationPrompts();
215
+ }
216
+ if (!props.meta.askConfirmationBeforeGenerating) {
217
+ runAiActions();
218
+ }
219
+ }
220
+
221
+
222
+ function runAiActions() {
223
+ popupMode.value = 'generation';
224
+ if (props.meta.isImageGeneration) {
149
225
  isGeneratingImages.value = true;
150
226
  runAiAction({
151
227
  endpoint: 'initial_image_generate',
@@ -170,9 +246,8 @@ const openDialog = async () => {
170
246
  });
171
247
  }
172
248
  }
173
-
249
+
174
250
  const closeDialog = () => {
175
- confirmDialog.value.close();
176
251
  isAiResponseReceivedAnalize.value = [];
177
252
  isAiResponseReceivedImage.value = [];
178
253
 
@@ -186,10 +261,10 @@ const closeDialog = () => {
186
261
  isImageGenerationError.value = false;
187
262
  errorMessage.value = '';
188
263
  isDialogOpen.value = false;
264
+ popupMode.value = 'confirmation';
189
265
  }
190
266
 
191
267
  watch(selected, (val) => {
192
- //console.log('Selected changed:', val);
193
268
  checkedCount.value = val.filter(item => item.isChecked === true).length;
194
269
  }, { deep: true });
195
270
 
@@ -492,6 +567,15 @@ async function runAiAction({
492
567
  return;
493
568
  };
494
569
  }
570
+
571
+ let customPrompt;
572
+ if (actionType === 'generate_images') {
573
+ customPrompt = generationPrompts.value.imageGenerationPrompts || generationPrompts.value.generateImages;
574
+ } else if (actionType === 'analyze') {
575
+ customPrompt = generationPrompts.value.imageFieldsPrompts;
576
+ } else if (actionType === 'analyze_no_images') {
577
+ customPrompt = generationPrompts.value.plainFieldsPrompts;
578
+ }
495
579
  //creating jobs
496
580
  const tasks = recordsIds.map(async (checkbox, i) => {
497
581
  try {
@@ -501,6 +585,7 @@ async function runAiAction({
501
585
  body: {
502
586
  actionType: actionType,
503
587
  recordId: checkbox,
588
+ ...(customPrompt !== undefined ? { customPrompt: JSON.stringify(customPrompt) } : {}),
504
589
  },
505
590
  });
506
591
 
@@ -753,4 +838,76 @@ function click() {
753
838
  openDialog();
754
839
  }
755
840
 
841
+ function saveSettings() {
842
+ popupMode.value = 'confirmation';
843
+ localStorage.setItem(`bulkAiFlowGenerationPrompts_${props.meta.pluginInstanceId}`, JSON.stringify(generationPrompts.value));
844
+ }
845
+
846
+ async function getGenerationPrompts() {
847
+ const calculatedGenerationPrompts: any = {};
848
+ const savedPrompts = localStorage.getItem(`bulkAiFlowGenerationPrompts_${props.meta.pluginInstanceId}`);
849
+ if (props.meta.generationPrompts.plainFieldsPrompts) {
850
+ calculatedGenerationPrompts.plainFieldsPrompts = props.meta.generationPrompts.plainFieldsPrompts;
851
+ }
852
+ if (props.meta.generationPrompts.imageFieldsPrompts) {
853
+ calculatedGenerationPrompts.imageFieldsPrompts = props.meta.generationPrompts.imageFieldsPrompts;
854
+ }
855
+ if (props.meta.generationPrompts.imageGenerationPrompts) {
856
+ let imageFields = {};
857
+ for (const [key, value] of Object.entries(props.meta.generationPrompts.imageGenerationPrompts)) {
858
+ // value might be typed as unknown; cast to any to access prompt safely
859
+ imageFields[key] = (value as any).prompt;
860
+ }
861
+ calculatedGenerationPrompts.generateImages = imageFields;
862
+ }
863
+ if (savedPrompts && props.meta.askConfirmationBeforeGenerating) {
864
+
865
+ generationPrompts.value = checkAndAddNewFieldsToPrompts(JSON.parse(savedPrompts), calculatedGenerationPrompts);
866
+
867
+ return;
868
+ }
869
+ generationPrompts.value = calculatedGenerationPrompts;
870
+ }
871
+
872
+ function resetPromptToDefault(categoryKey, promptKey) {
873
+ if (categoryKey === 'generateImages') {
874
+ generationPrompts.value[categoryKey][promptKey] = props.meta.generationPrompts.imageGenerationPrompts[promptKey].prompt;
875
+ return;
876
+ }
877
+ generationPrompts.value[categoryKey][promptKey] = props.meta.generationPrompts[categoryKey][promptKey];
878
+ }
879
+
880
+ function clickSettingsButton() {
881
+ getGenerationPrompts();
882
+ popupMode.value = 'settings';
883
+ }
884
+
885
+
886
+ function checkAndAddNewFieldsToPrompts(savedPrompts, defaultPrompts) {
887
+ for (const categoryKey in defaultPrompts) {
888
+ if (!savedPrompts.hasOwnProperty(categoryKey)) {
889
+ savedPrompts[categoryKey] = defaultPrompts[categoryKey];
890
+ } else {
891
+ for (const promptKey in defaultPrompts[categoryKey]) {
892
+ if (!savedPrompts[categoryKey].hasOwnProperty(promptKey)) {
893
+ savedPrompts[categoryKey][promptKey] = defaultPrompts[categoryKey][promptKey];
894
+ }
895
+ }
896
+ }
897
+ }
898
+ //remove deprecated fields
899
+ for (const categoryKey in savedPrompts) {
900
+ if (!defaultPrompts.hasOwnProperty(categoryKey)) {
901
+ delete savedPrompts[categoryKey];
902
+ } else {
903
+ for (const promptKey in savedPrompts[categoryKey]) {
904
+ if (!defaultPrompts[categoryKey].hasOwnProperty(promptKey)) {
905
+ delete savedPrompts[categoryKey][promptKey];
906
+ }
907
+ }
908
+ }
909
+ }
910
+ return savedPrompts;
911
+ }
912
+
756
913
  </script>
@@ -173,6 +173,7 @@
173
173
  :carouselImageIndex="carouselImageIndex[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n]"
174
174
  :regenerateImagesRefreshRate="regenerateImagesRefreshRate"
175
175
  :sourceImage="item.images && item.images.length ? item.images : null"
176
+ :imageGenerationPrompts="imageGenerationPrompts[n]"
176
177
  @error="handleError"
177
178
  @close="openGenerationCarousel[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] = false"
178
179
  @selectImage="updateSelectedImage"
@@ -231,6 +232,7 @@ const props = defineProps<{
231
232
  imageGenerationErrorMessage: string[],
232
233
  oldData: any[],
233
234
  isImageHasPreviewUrl: Record<string, boolean>
235
+ imageGenerationPrompts: Record<string, any>
234
236
  }>();
235
237
  const emit = defineEmits(['error', 'regenerateImages']);
236
238
 
@@ -4,16 +4,27 @@
4
4
  "paths": {
5
5
  "@/*": [
6
6
  // "node_modules/adminforth/dist/spa/src/*"
7
- "../../../spa/src/*"
7
+ "../../../adminforth/spa/src/*"
8
8
  ],
9
9
  "*": [
10
10
  // "node_modules/adminforth/dist/spa/node_modules/*"
11
- "../../../spa/node_modules/*"
11
+ "../../../adminforth/spa/node_modules/*"
12
12
  ],
13
13
  "@@/*": [
14
14
  // "node_modules/adminforth/dist/spa/src/*"
15
15
  "."
16
16
  ]
17
17
  }
18
- }
18
+ },
19
+ "include": [
20
+ "./**/*.ts",
21
+ "./**/*.tsx",
22
+ "./**/*.vue",
23
+ "../**/*.ts",
24
+ "../**/*.tsx",
25
+ "../**/*.vue",
26
+ "../*.vue",
27
+ "../*.ts",
28
+ "../*.tsx"
29
+ ]
19
30
  }
@@ -143,7 +143,7 @@ const sliderRef = ref(null)
143
143
 
144
144
  const prompt = ref('');
145
145
  const emit = defineEmits(['close', 'selectImage', 'error', 'updateCarouselIndex']);
146
- const props = defineProps(['meta', 'record', 'images', 'recordId', 'prompt', 'fieldName', 'isError', 'errorMessage', 'carouselImageIndex', 'regenerateImagesRefreshRate','sourceImage']);
146
+ const props = defineProps(['meta', 'record', 'images', 'recordId', 'prompt', 'fieldName', 'isError', 'errorMessage', 'carouselImageIndex', 'regenerateImagesRefreshRate','sourceImage', 'imageGenerationPrompts']);
147
147
  const images = ref([]);
148
148
  const loading = ref(false);
149
149
  const attachmentFiles = ref<string[]>([])
@@ -154,7 +154,7 @@ onMounted(async () => {
154
154
  }
155
155
  const temp = await getGenerationPrompt() || '';
156
156
  attachmentFiles.value = props.sourceImage || [];
157
- prompt.value = temp[props.fieldName];
157
+ prompt.value = Object.keys(JSON.parse(temp))[0];
158
158
  await nextTick();
159
159
 
160
160
  const currentIndex = props.carouselImageIndex || 0;
@@ -212,12 +212,20 @@ async function getHistoricalAverage() {
212
212
  }
213
213
 
214
214
  async function getGenerationPrompt() {
215
- try{
215
+ const [key, ...rest] = props.imageGenerationPrompts.split(":");
216
+ const value = rest.join(":").trim();
217
+
218
+ const json = {
219
+ [key.trim()]: value
220
+ };
221
+
222
+ try {
216
223
  const resp = await callAdminForthApi({
217
- path: `/plugin/${props.meta.pluginInstanceId}/get_generation_prompts`,
224
+ path: `/plugin/${props.meta.pluginInstanceId}/get_image_generation_prompts`,
218
225
  method: 'POST',
219
226
  body: {
220
227
  recordId: props.recordId,
228
+ customPrompt: JSON.stringify(json) || {},
221
229
  },
222
230
  });
223
231
  if(!resp) {
@@ -226,7 +234,7 @@ async function getGenerationPrompt() {
226
234
  errorMessage: "Error getting generation prompts."
227
235
  });
228
236
  }
229
- return resp?.generationOptions || null;
237
+ return resp?.prompt || null;
230
238
  } catch (e) {
231
239
  emit('error', {
232
240
  isError: true,
@@ -8,16 +8,49 @@
8
8
  <Dialog
9
9
  ref="confirmDialog"
10
10
  header="Bulk AI Flow"
11
- class="[scrollbar-gutter:stable] !max-w-full w-full lg:w-[1600px] !lg:max-w-[1600px]"
11
+ class="[scrollbar-gutter:stable] !max-w-full w-fit h-fit"
12
+ :class="popupMode === 'generation' ? 'lg:w-[1600px] !lg:max-w-[1600px]'
13
+ : popupMode === 'settings' ? 'lg:w-[1000px] !lg:max-w-[1000px]'
14
+ : 'lg:w-[500px] !lg:max-w-[500px]'"
12
15
  :beforeCloseFunction="closeDialog"
13
- :buttons="[
14
- { label: checkedCount > 1 ? 'Save fields' : 'Save field', options: { disabled: isLoading || checkedCount < 1 || isCriticalError || isFetchingRecords || isGeneratingImages || isAnalizingFields || isAnalizingImages, loader: isLoading, class: 'w-fit' }, onclick: async (dialog) => { await saveData(); dialog.hide(); } },
15
- { label: 'Cancel', options: {class: 'bg-white hover:!bg-gray-100 !text-gray-900 hover:!text-gray-800 dark:!bg-gray-800 dark:!text-gray-100 dark:hover:!bg-gray-700 !border-gray-200'}, onclick: (dialog) => dialog.hide() },
16
- ]"
16
+ :buttons="popupMode === 'generation' ? [
17
+ {
18
+ label: checkedCount > 1 ? 'Save fields' : 'Save field',
19
+ options: {
20
+ disabled: isLoading || checkedCount < 1 || isCriticalError || isFetchingRecords || isGeneratingImages || isAnalizingFields || isAnalizingImages,
21
+ loader: isLoading, class: 'w-fit'
22
+ },
23
+ onclick: async (dialog) => { await saveData(); dialog.hide(); }
24
+ },
25
+ {
26
+ label: 'Cancel',
27
+ options: {
28
+ class: 'bg-white hover:!bg-gray-100 !text-gray-900 hover:!text-gray-800 dark:!bg-gray-800 dark:!text-gray-100 dark:hover:!bg-gray-700 !border-gray-200'
29
+ },
30
+ onclick: (dialog) => dialog.hide()
31
+ },
32
+ ] : popupMode === 'settings' ? [
33
+ {
34
+ label: 'Save settings',
35
+ options: {
36
+ class: 'w-fit'
37
+ },
38
+ onclick: (dialog) => { saveSettings(); }
39
+ },
40
+ ] :
41
+ [
42
+ {
43
+ label: 'Edit prompts',
44
+ options: {
45
+ class: 'w-fit ml-auto'
46
+ },
47
+ onclick: (dialog) => { clickSettingsButton(); }
48
+ },
49
+ ]"
17
50
  :click-to-close-outside="false"
18
51
  >
19
- <div class="[scrollbar-gutter:stable] 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">
20
- <div v-if="records && props.checkboxes.length" class="w-full overflow-x-auto">
52
+ <div class="[scrollbar-gutter:stable] bulk-vision-table flex flex-col items-center max-w-[1560px] md:max-h-[75vh] gap-3 md:gap-4 w-full h-full overflow-y-auto">
53
+ <div v-if="records && props.checkboxes.length && popupMode === 'generation'" class="w-full overflow-x-auto">
21
54
  <VisionTable
22
55
  :checkbox="props.checkboxes"
23
56
  :records="records"
@@ -44,10 +77,40 @@
44
77
  :imageGenerationErrorMessage="imageGenerationErrorMessage"
45
78
  @regenerate-images="regenerateImages"
46
79
  :isImageHasPreviewUrl="isImageHasPreviewUrl"
80
+ :imageGenerationPrompts="generationPrompts.generateImages"
47
81
  />
82
+ <div class="text-red-600 flex items-center w-full">
83
+ <p v-if="isError === true">{{ errorMessage }}</p>
84
+ </div>
48
85
  </div>
49
- <div class="text-red-600 flex items-center w-full">
50
- <p v-if="isError === true">{{ errorMessage }}</p>
86
+ <div
87
+ v-else-if="popupMode === 'settings'"
88
+ v-for="(promptsCategory, key) in generationPrompts"
89
+ :key="key"
90
+ class="w-full"
91
+ >
92
+ <div v-if="Object.keys(promptsCategory).length > 0" class="gap-4 mb-6 ml-1">
93
+ <p class="text-start w-full text-xl font-bold mb-2">{{
94
+ key === "plainFieldsPrompts" ? "Prompts for non-image fields"
95
+ : key === "generateImages" ? "Prompts for image fields"
96
+ : "Prompts for image analysis"
97
+ }}</p>
98
+ <div class="grid grid-cols-2 gap-4">
99
+ <div v-for="(prompt, promptKey) in promptsCategory" :key="promptKey">
100
+ {{ formatLabel(promptKey) }} prompt:
101
+ <Textarea
102
+ v-model="generationPrompts[key][promptKey]"
103
+ class="w-full h-32 p-2 border border-gray-300 rounded-md resize-none focus:outline-none focus:ring-2 focus:ring-purple-500"
104
+ ></Textarea>
105
+ <p class="text-red-500 hover:underline hover:cursor-pointer mt-2" @click="resetPromptToDefault(key, promptKey)">reset to default</p>
106
+ </div>
107
+ </div>
108
+ </div>
109
+ </div>
110
+ <div v-else class="flex flex-col gap-2">
111
+ <Button @click="runAiActions" class="px-5 py-2.5 my-20 bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-purple-300 dark:focus:ring-purple-800 rounded-md text-white border-none">
112
+ Start generation
113
+ </Button>
51
114
  </div>
52
115
  </div>
53
116
  </Dialog>
@@ -56,11 +119,11 @@
56
119
  <script lang="ts" setup>
57
120
  import { callAdminForthApi } from '@/utils';
58
121
  import { Ref, ref, watch } from 'vue'
59
- import { Dialog, Button } from '@/afcl';
122
+ import { Dialog, Button, Textarea } from '@/afcl';
60
123
  import VisionTable from './VisionTable.vue'
61
124
  import adminforth from '@/adminforth';
62
125
  import { useI18n } from 'vue-i18n';
63
- import { AdminUser, type AdminForthResourceCommon } from '@/types';
126
+ import { AdminUser, type AdminForthResourceCommon } from '@/types/Common';
64
127
  import { run } from 'node:test';
65
128
 
66
129
  const { t } = useI18n();
@@ -114,6 +177,8 @@ const aiGenerationErrorMessage = ref<string[]>([]);
114
177
  const isAiImageGenerationError = ref<boolean[]>([false]);
115
178
  const imageGenerationErrorMessage = ref<string[]>([]);
116
179
  const isImageHasPreviewUrl = ref<Record<string, boolean>>({});
180
+ const popupMode = ref<'generation' | 'confirmation' | 'settings'>('confirmation');
181
+ const generationPrompts = ref<any>({});
117
182
 
118
183
  const openDialog = async () => {
119
184
  isDialogOpen.value = true;
@@ -144,8 +209,19 @@ const openDialog = async () => {
144
209
  },{[primaryKey]: records.value[i][primaryKey]} as Record<string, boolean>);
145
210
  }
146
211
  isFetchingRecords.value = false;
147
-
148
- if (props.meta.isImageGeneration) {
212
+ // Ensure prompts are loaded before any automatic AI action run
213
+ if (!generationPrompts.value || Object.keys(generationPrompts.value).length === 0) {
214
+ await getGenerationPrompts();
215
+ }
216
+ if (!props.meta.askConfirmationBeforeGenerating) {
217
+ runAiActions();
218
+ }
219
+ }
220
+
221
+
222
+ function runAiActions() {
223
+ popupMode.value = 'generation';
224
+ if (props.meta.isImageGeneration) {
149
225
  isGeneratingImages.value = true;
150
226
  runAiAction({
151
227
  endpoint: 'initial_image_generate',
@@ -170,9 +246,8 @@ const openDialog = async () => {
170
246
  });
171
247
  }
172
248
  }
173
-
249
+
174
250
  const closeDialog = () => {
175
- confirmDialog.value.close();
176
251
  isAiResponseReceivedAnalize.value = [];
177
252
  isAiResponseReceivedImage.value = [];
178
253
 
@@ -186,10 +261,10 @@ const closeDialog = () => {
186
261
  isImageGenerationError.value = false;
187
262
  errorMessage.value = '';
188
263
  isDialogOpen.value = false;
264
+ popupMode.value = 'confirmation';
189
265
  }
190
266
 
191
267
  watch(selected, (val) => {
192
- //console.log('Selected changed:', val);
193
268
  checkedCount.value = val.filter(item => item.isChecked === true).length;
194
269
  }, { deep: true });
195
270
 
@@ -492,6 +567,15 @@ async function runAiAction({
492
567
  return;
493
568
  };
494
569
  }
570
+
571
+ let customPrompt;
572
+ if (actionType === 'generate_images') {
573
+ customPrompt = generationPrompts.value.imageGenerationPrompts || generationPrompts.value.generateImages;
574
+ } else if (actionType === 'analyze') {
575
+ customPrompt = generationPrompts.value.imageFieldsPrompts;
576
+ } else if (actionType === 'analyze_no_images') {
577
+ customPrompt = generationPrompts.value.plainFieldsPrompts;
578
+ }
495
579
  //creating jobs
496
580
  const tasks = recordsIds.map(async (checkbox, i) => {
497
581
  try {
@@ -501,6 +585,7 @@ async function runAiAction({
501
585
  body: {
502
586
  actionType: actionType,
503
587
  recordId: checkbox,
588
+ ...(customPrompt !== undefined ? { customPrompt: JSON.stringify(customPrompt) } : {}),
504
589
  },
505
590
  });
506
591
 
@@ -753,4 +838,76 @@ function click() {
753
838
  openDialog();
754
839
  }
755
840
 
841
+ function saveSettings() {
842
+ popupMode.value = 'confirmation';
843
+ localStorage.setItem(`bulkAiFlowGenerationPrompts_${props.meta.pluginInstanceId}`, JSON.stringify(generationPrompts.value));
844
+ }
845
+
846
+ async function getGenerationPrompts() {
847
+ const calculatedGenerationPrompts: any = {};
848
+ const savedPrompts = localStorage.getItem(`bulkAiFlowGenerationPrompts_${props.meta.pluginInstanceId}`);
849
+ if (props.meta.generationPrompts.plainFieldsPrompts) {
850
+ calculatedGenerationPrompts.plainFieldsPrompts = props.meta.generationPrompts.plainFieldsPrompts;
851
+ }
852
+ if (props.meta.generationPrompts.imageFieldsPrompts) {
853
+ calculatedGenerationPrompts.imageFieldsPrompts = props.meta.generationPrompts.imageFieldsPrompts;
854
+ }
855
+ if (props.meta.generationPrompts.imageGenerationPrompts) {
856
+ let imageFields = {};
857
+ for (const [key, value] of Object.entries(props.meta.generationPrompts.imageGenerationPrompts)) {
858
+ // value might be typed as unknown; cast to any to access prompt safely
859
+ imageFields[key] = (value as any).prompt;
860
+ }
861
+ calculatedGenerationPrompts.generateImages = imageFields;
862
+ }
863
+ if (savedPrompts && props.meta.askConfirmationBeforeGenerating) {
864
+
865
+ generationPrompts.value = checkAndAddNewFieldsToPrompts(JSON.parse(savedPrompts), calculatedGenerationPrompts);
866
+
867
+ return;
868
+ }
869
+ generationPrompts.value = calculatedGenerationPrompts;
870
+ }
871
+
872
+ function resetPromptToDefault(categoryKey, promptKey) {
873
+ if (categoryKey === 'generateImages') {
874
+ generationPrompts.value[categoryKey][promptKey] = props.meta.generationPrompts.imageGenerationPrompts[promptKey].prompt;
875
+ return;
876
+ }
877
+ generationPrompts.value[categoryKey][promptKey] = props.meta.generationPrompts[categoryKey][promptKey];
878
+ }
879
+
880
+ function clickSettingsButton() {
881
+ getGenerationPrompts();
882
+ popupMode.value = 'settings';
883
+ }
884
+
885
+
886
+ function checkAndAddNewFieldsToPrompts(savedPrompts, defaultPrompts) {
887
+ for (const categoryKey in defaultPrompts) {
888
+ if (!savedPrompts.hasOwnProperty(categoryKey)) {
889
+ savedPrompts[categoryKey] = defaultPrompts[categoryKey];
890
+ } else {
891
+ for (const promptKey in defaultPrompts[categoryKey]) {
892
+ if (!savedPrompts[categoryKey].hasOwnProperty(promptKey)) {
893
+ savedPrompts[categoryKey][promptKey] = defaultPrompts[categoryKey][promptKey];
894
+ }
895
+ }
896
+ }
897
+ }
898
+ //remove deprecated fields
899
+ for (const categoryKey in savedPrompts) {
900
+ if (!defaultPrompts.hasOwnProperty(categoryKey)) {
901
+ delete savedPrompts[categoryKey];
902
+ } else {
903
+ for (const promptKey in savedPrompts[categoryKey]) {
904
+ if (!defaultPrompts[categoryKey].hasOwnProperty(promptKey)) {
905
+ delete savedPrompts[categoryKey][promptKey];
906
+ }
907
+ }
908
+ }
909
+ }
910
+ return savedPrompts;
911
+ }
912
+
756
913
  </script>
@@ -173,6 +173,7 @@
173
173
  :carouselImageIndex="carouselImageIndex[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n]"
174
174
  :regenerateImagesRefreshRate="regenerateImagesRefreshRate"
175
175
  :sourceImage="item.images && item.images.length ? item.images : null"
176
+ :imageGenerationPrompts="imageGenerationPrompts[n]"
176
177
  @error="handleError"
177
178
  @close="openGenerationCarousel[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] = false"
178
179
  @selectImage="updateSelectedImage"
@@ -231,6 +232,7 @@ const props = defineProps<{
231
232
  imageGenerationErrorMessage: string[],
232
233
  oldData: any[],
233
234
  isImageHasPreviewUrl: Record<string, boolean>
235
+ imageGenerationPrompts: Record<string, any>
234
236
  }>();
235
237
  const emit = defineEmits(['error', 'regenerateImages']);
236
238
 
@@ -4,16 +4,27 @@
4
4
  "paths": {
5
5
  "@/*": [
6
6
  // "node_modules/adminforth/dist/spa/src/*"
7
- "../../../spa/src/*"
7
+ "../../../adminforth/spa/src/*"
8
8
  ],
9
9
  "*": [
10
10
  // "node_modules/adminforth/dist/spa/node_modules/*"
11
- "../../../spa/node_modules/*"
11
+ "../../../adminforth/spa/node_modules/*"
12
12
  ],
13
13
  "@@/*": [
14
14
  // "node_modules/adminforth/dist/spa/src/*"
15
15
  "."
16
16
  ]
17
17
  }
18
- }
18
+ },
19
+ "include": [
20
+ "./**/*.ts",
21
+ "./**/*.tsx",
22
+ "./**/*.vue",
23
+ "../**/*.ts",
24
+ "../**/*.tsx",
25
+ "../**/*.vue",
26
+ "../*.vue",
27
+ "../*.ts",
28
+ "../*.tsx"
29
+ ]
19
30
  }
package/dist/index.js CHANGED
@@ -25,27 +25,39 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
25
25
  }
26
26
  // Compile Handlebars templates in outputFields using record fields as context
27
27
  compileTemplates(source, record, valueSelector) {
28
- const compiled = {};
29
- for (const [key, value] of Object.entries(source)) {
30
- const templateStr = valueSelector(value);
31
- try {
32
- const tpl = Handlebars.compile(templateStr);
33
- compiled[key] = tpl(record);
28
+ return __awaiter(this, void 0, void 0, function* () {
29
+ if (this.options.provideAdditionalContextForRecord) {
30
+ const additionalFields = yield this.options.provideAdditionalContextForRecord({ record, adminUser: null, resource: this.resourceConfig });
31
+ record = Object.assign(Object.assign({}, record), additionalFields);
34
32
  }
35
- catch (_a) {
36
- compiled[key] = templateStr;
33
+ const compiled = {};
34
+ for (const [key, value] of Object.entries(source)) {
35
+ const templateStr = valueSelector(value);
36
+ try {
37
+ const tpl = Handlebars.compile(templateStr);
38
+ compiled[key] = tpl(record);
39
+ }
40
+ catch (_a) {
41
+ compiled[key] = templateStr;
42
+ }
37
43
  }
38
- }
39
- return compiled;
44
+ return compiled;
45
+ });
40
46
  }
41
- compileOutputFieldsTemplates(record) {
42
- return this.compileTemplates(this.options.fillFieldsFromImages, record, v => String(v));
47
+ compileOutputFieldsTemplates(record, customPrompt) {
48
+ return __awaiter(this, void 0, void 0, function* () {
49
+ return yield this.compileTemplates(customPrompt ? JSON.parse(customPrompt) : this.options.fillFieldsFromImages, record, v => String(v));
50
+ });
43
51
  }
44
- compileOutputFieldsTemplatesNoImage(record) {
45
- return this.compileTemplates(this.options.fillPlainFields, record, v => String(v));
52
+ compileOutputFieldsTemplatesNoImage(record, customPrompt) {
53
+ return __awaiter(this, void 0, void 0, function* () {
54
+ return yield this.compileTemplates(customPrompt ? JSON.parse(customPrompt) : this.options.fillPlainFields, record, v => String(v));
55
+ });
46
56
  }
47
- compileGenerationFieldTemplates(record) {
48
- return this.compileTemplates(this.options.generateImages, record, v => String(v.prompt));
57
+ compileGenerationFieldTemplates(record, customPrompt) {
58
+ return __awaiter(this, void 0, void 0, function* () {
59
+ return yield this.compileTemplates(customPrompt ? JSON.parse(customPrompt) : this.options.generateImages, record, v => String(customPrompt ? v : v.prompt));
60
+ });
49
61
  }
50
62
  checkRateLimit(field, fieldNameRateLimit, headers) {
51
63
  return __awaiter(this, void 0, void 0, function* () {
@@ -65,7 +77,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
65
77
  }
66
78
  });
67
79
  }
68
- analyze_image(jobId, recordId, adminUser, headers) {
80
+ analyze_image(jobId, recordId, adminUser, headers, customPrompt) {
69
81
  return __awaiter(this, void 0, void 0, function* () {
70
82
  var _a, _b, _c, _d, _e, _f, _g, _h, _j;
71
83
  const selectedId = recordId;
@@ -100,7 +112,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
100
112
  return { ok: false, error: 'One of the image URLs is not valid' };
101
113
  }
102
114
  //create prompt for OpenAI
103
- const compiledOutputFields = this.compileOutputFieldsTemplates(record);
115
+ const compiledOutputFields = yield this.compileOutputFieldsTemplates(record, customPrompt);
104
116
  const prompt = `Analyze the following image(s) and return a single JSON in format like: {'param1': 'value1', 'param2': 'value2'}.
105
117
  Do NOT return array of objects. Do NOT include any Markdown, code blocks, explanations, or extra text. Only return valid JSON.
106
118
  Each object must contain the following fields: ${JSON.stringify(compiledOutputFields)} Use the exact field names. If it's number field - return only number.
@@ -145,7 +157,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
145
157
  }
146
158
  });
147
159
  }
148
- analyzeNoImages(jobId, recordId, adminUser, headers) {
160
+ analyzeNoImages(jobId, recordId, adminUser, headers, customPrompt) {
149
161
  return __awaiter(this, void 0, void 0, function* () {
150
162
  const selectedId = recordId;
151
163
  let isError = false;
@@ -157,7 +169,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
157
169
  else {
158
170
  const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
159
171
  const record = yield this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(primaryKeyColumn.name, selectedId)]);
160
- const compiledOutputFields = this.compileOutputFieldsTemplatesNoImage(record);
172
+ const compiledOutputFields = yield this.compileOutputFieldsTemplatesNoImage(record, customPrompt);
161
173
  const prompt = `Analyze the following fields and return a single JSON in format like: {'param1': 'value1', 'param2': 'value2'}.
162
174
  Do NOT return array of objects. Do NOT include any Markdown, code blocks, explanations, or extra text. Only return valid JSON.
163
175
  Each object must contain the following fields: ${JSON.stringify(compiledOutputFields)} Use the exact field names.
@@ -187,7 +199,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
187
199
  }
188
200
  });
189
201
  }
190
- initialImageGenerate(jobId, recordId, adminUser, headers) {
202
+ initialImageGenerate(jobId, recordId, adminUser, headers, customPrompt) {
191
203
  return __awaiter(this, void 0, void 0, function* () {
192
204
  var _a, _b;
193
205
  const selectedId = recordId;
@@ -211,7 +223,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
211
223
  }
212
224
  }
213
225
  const fieldTasks = Object.keys(((_b = this.options) === null || _b === void 0 ? void 0 : _b.generateImages) || {}).map((key) => __awaiter(this, void 0, void 0, function* () {
214
- const prompt = this.compileGenerationFieldTemplates(record)[key];
226
+ const prompt = (yield this.compileGenerationFieldTemplates(record, customPrompt))[key];
215
227
  let images;
216
228
  if (this.options.attachFiles && attachmentFiles.length === 0) {
217
229
  isError = true;
@@ -409,6 +421,12 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
409
421
  fillPlainFields: ((_b = this.options.refreshRates) === null || _b === void 0 ? void 0 : _b.fillPlainFields) || 1000,
410
422
  generateImages: ((_c = this.options.refreshRates) === null || _c === void 0 ? void 0 : _c.generateImages) || 5000,
411
423
  regenerateImages: ((_d = this.options.refreshRates) === null || _d === void 0 ? void 0 : _d.regenerateImages) || 5000,
424
+ },
425
+ askConfirmationBeforeGenerating: this.options.askConfirmationBeforeGenerating || false,
426
+ generationPrompts: {
427
+ plainFieldsPrompts: this.options.fillPlainFields || {},
428
+ imageFieldsPrompts: this.options.fillFieldsFromImages || {},
429
+ imageGenerationPrompts: this.options.generateImages || {},
412
430
  }
413
431
  }
414
432
  };
@@ -480,7 +498,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
480
498
  }
481
499
  }
482
500
  }
483
- if (this.options.fillFieldsFromImages || this.options.fillPlainFields || this.options.generateImages) {
501
+ if ((this.options.fillFieldsFromImages || this.options.fillPlainFields || this.options.generateImages) && !this.options.provideAdditionalContextForRecord) {
484
502
  let matches = [];
485
503
  const regex = /{{(.*?)}}/g;
486
504
  if (this.options.fillFieldsFromImages) {
@@ -594,7 +612,12 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
594
612
  if (oldRecord[value]) {
595
613
  // put tag to delete old file
596
614
  try {
597
- yield columnPlugin.pluginOptions.storageAdapter.markKeyForDeletation(oldRecord[value]);
615
+ if (columnPlugin.pluginOptions.storageAdapter.markKeyForDeletion !== undefined) {
616
+ yield columnPlugin.pluginOptions.storageAdapter.markKeyForDeletion(oldRecord[value]);
617
+ }
618
+ else {
619
+ yield columnPlugin.pluginOptions.storageAdapter.markKeyForDeletation(oldRecord[value]);
620
+ }
598
621
  }
599
622
  catch (e) {
600
623
  // file might be e.g. already deleted, so we catch error
@@ -604,7 +627,12 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
604
627
  if (fieldsToUpdate[idx][value] && fieldsToUpdate[idx][value] !== null) {
605
628
  // remove tag from new file
606
629
  // in this case we let it crash if it fails: this is a new file which just was uploaded.
607
- yield columnPlugin.pluginOptions.storageAdapter.markKeyForNotDeletation(fieldsToUpdate[idx][value]);
630
+ if (columnPlugin.pluginOptions.storageAdapter.markKeyForNotDeletion !== undefined) {
631
+ yield columnPlugin.pluginOptions.storageAdapter.markKeyForNotDeletion(fieldsToUpdate[idx][value]);
632
+ }
633
+ else {
634
+ yield columnPlugin.pluginOptions.storageAdapter.markKeyForNotDeletation(fieldsToUpdate[idx][value]);
635
+ }
608
636
  }
609
637
  }
610
638
  }
@@ -628,13 +656,14 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
628
656
  });
629
657
  server.endpoint({
630
658
  method: 'POST',
631
- path: `/plugin/${this.pluginInstanceId}/get_generation_prompts`,
659
+ path: `/plugin/${this.pluginInstanceId}/get_image_generation_prompts`,
632
660
  handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, headers }) {
633
661
  var _b;
634
662
  const Id = body.recordId || [];
663
+ const customPrompt = body.customPrompt || null;
635
664
  const record = yield this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ((_b = this.resourceConfig.columns.find(c => c.primaryKey)) === null || _b === void 0 ? void 0 : _b.name, Id)]);
636
- const compiledGenerationOptions = this.compileGenerationFieldTemplates(record);
637
- return { generationOptions: compiledGenerationOptions };
665
+ const compiledGenerationOptions = yield this.compileGenerationFieldTemplates(record, JSON.stringify({ "prompt": customPrompt }));
666
+ return compiledGenerationOptions;
638
667
  })
639
668
  });
640
669
  server.endpoint({
@@ -652,7 +681,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
652
681
  method: 'POST',
653
682
  path: `/plugin/${this.pluginInstanceId}/create-job`,
654
683
  handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, headers }) {
655
- const { actionType, recordId } = body;
684
+ const { actionType, recordId, customPrompt } = body;
656
685
  const jobId = randomUUID();
657
686
  jobs.set(jobId, { status: "in_progress" });
658
687
  if (!actionType) {
@@ -666,13 +695,13 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
666
695
  else {
667
696
  switch (actionType) {
668
697
  case 'generate_images':
669
- this.initialImageGenerate(jobId, recordId, adminUser, headers);
698
+ this.initialImageGenerate(jobId, recordId, adminUser, headers, customPrompt);
670
699
  break;
671
700
  case 'analyze_no_images':
672
- this.analyzeNoImages(jobId, recordId, adminUser, headers);
701
+ this.analyzeNoImages(jobId, recordId, adminUser, headers, customPrompt);
673
702
  break;
674
703
  case 'analyze':
675
- this.analyze_image(jobId, recordId, adminUser, headers);
704
+ this.analyze_image(jobId, recordId, adminUser, headers, customPrompt);
676
705
  break;
677
706
  case 'regenerate_images':
678
707
  if (!body.prompt || !body.fieldName) {
package/index.ts CHANGED
@@ -25,11 +25,15 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
25
25
  }
26
26
 
27
27
  // Compile Handlebars templates in outputFields using record fields as context
28
- private compileTemplates<T extends Record<string, any>>(
28
+ private async compileTemplates<T extends Record<string, any>>(
29
29
  source: T,
30
30
  record: any,
31
31
  valueSelector: (value: T[keyof T]) => string
32
- ): Record<string, string> {
32
+ ): Promise<Record<string, string>> {
33
+ if (this.options.provideAdditionalContextForRecord) {
34
+ const additionalFields = await this.options.provideAdditionalContextForRecord({ record, adminUser: null, resource: this.resourceConfig });
35
+ record = { ...record, ...additionalFields };
36
+ }
33
37
  const compiled: Record<string, string> = {};
34
38
  for (const [key, value] of Object.entries(source)) {
35
39
  const templateStr = valueSelector(value);
@@ -43,16 +47,16 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
43
47
  return compiled;
44
48
  }
45
49
 
46
- private compileOutputFieldsTemplates(record: any) {
47
- return this.compileTemplates(this.options.fillFieldsFromImages, record, v => String(v));
50
+ private async compileOutputFieldsTemplates(record: any, customPrompt? : string) {
51
+ return await this.compileTemplates(customPrompt ? JSON.parse(customPrompt) :this.options.fillFieldsFromImages, record, v => String(v));
48
52
  }
49
53
 
50
- private compileOutputFieldsTemplatesNoImage(record: any) {
51
- return this.compileTemplates(this.options.fillPlainFields, record, v => String(v));
54
+ private async compileOutputFieldsTemplatesNoImage(record: any, customPrompt? : string) {
55
+ return await this.compileTemplates(customPrompt ? JSON.parse(customPrompt) : this.options.fillPlainFields, record, v => String(v));
52
56
  }
53
57
 
54
- private compileGenerationFieldTemplates(record: any) {
55
- return this.compileTemplates(this.options.generateImages, record, v => String(v.prompt));
58
+ private async compileGenerationFieldTemplates(record: any, customPrompt? : string) {
59
+ return await this.compileTemplates(customPrompt ? JSON.parse(customPrompt) : this.options.generateImages, record, v => String(customPrompt ? v : v.prompt));
56
60
  }
57
61
 
58
62
  private async checkRateLimit(field: string, fieldNameRateLimit: string | undefined, headers: Record<string, string | string[] | undefined>): Promise<void | { error?: string; }> {
@@ -72,7 +76,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
72
76
  }
73
77
  }
74
78
 
75
- private async analyze_image(jobId: string, recordId: string, adminUser: any, headers: Record<string, string | string[] | undefined>) {
79
+ private async analyze_image(jobId: string, recordId: string, adminUser: any, headers: Record<string, string | string[] | undefined>, customPrompt? : string) {
76
80
  const selectedId = recordId;
77
81
  let isError = false;
78
82
  // Fetch the record using the provided ID
@@ -102,7 +106,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
102
106
  return { ok: false, error: 'One of the image URLs is not valid' };
103
107
  }
104
108
  //create prompt for OpenAI
105
- const compiledOutputFields = this.compileOutputFieldsTemplates(record);
109
+ const compiledOutputFields = await this.compileOutputFieldsTemplates(record, customPrompt);
106
110
  const prompt = `Analyze the following image(s) and return a single JSON in format like: {'param1': 'value1', 'param2': 'value2'}.
107
111
  Do NOT return array of objects. Do NOT include any Markdown, code blocks, explanations, or extra text. Only return valid JSON.
108
112
  Each object must contain the following fields: ${JSON.stringify(compiledOutputFields)} Use the exact field names. If it's number field - return only number.
@@ -148,7 +152,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
148
152
 
149
153
  }
150
154
 
151
- private async analyzeNoImages(jobId: string, recordId: string, adminUser: any, headers: Record<string, string | string[] | undefined>) {
155
+ private async analyzeNoImages(jobId: string, recordId: string, adminUser: any, headers: Record<string, string | string[] | undefined>, customPrompt? : string) {
152
156
  const selectedId = recordId;
153
157
  let isError = false;
154
158
  if (STUB_MODE) {
@@ -159,7 +163,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
159
163
  const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
160
164
  const record = await this.adminforth.resource(this.resourceConfig.resourceId).get( [Filters.EQ(primaryKeyColumn.name, selectedId)] );
161
165
 
162
- const compiledOutputFields = this.compileOutputFieldsTemplatesNoImage(record);
166
+ const compiledOutputFields = await this.compileOutputFieldsTemplatesNoImage(record, customPrompt);
163
167
  const prompt = `Analyze the following fields and return a single JSON in format like: {'param1': 'value1', 'param2': 'value2'}.
164
168
  Do NOT return array of objects. Do NOT include any Markdown, code blocks, explanations, or extra text. Only return valid JSON.
165
169
  Each object must contain the following fields: ${JSON.stringify(compiledOutputFields)} Use the exact field names.
@@ -188,7 +192,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
188
192
  }
189
193
  }
190
194
 
191
- private async initialImageGenerate(jobId: string, recordId: string, adminUser: any, headers: Record<string, string | string[] | undefined>) {
195
+ private async initialImageGenerate(jobId: string, recordId: string, adminUser: any, headers: Record<string, string | string[] | undefined>, customPrompt? : string) {
192
196
  const selectedId = recordId;
193
197
  let isError = false;
194
198
  const start = +new Date();
@@ -208,7 +212,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
208
212
  }
209
213
  }
210
214
  const fieldTasks = Object.keys(this.options?.generateImages || {}).map(async (key) => {
211
- const prompt = this.compileGenerationFieldTemplates(record)[key];
215
+ const prompt = (await this.compileGenerationFieldTemplates(record, customPrompt))[key];
212
216
  let images;
213
217
  if (this.options.attachFiles && attachmentFiles.length === 0) {
214
218
  isError = true;
@@ -391,7 +395,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
391
395
  };
392
396
 
393
397
  const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
394
-
398
+
395
399
  const pageInjection = {
396
400
  file: this.componentPath('VisionAction.vue'),
397
401
  meta: {
@@ -413,6 +417,12 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
413
417
  fillPlainFields: this.options.refreshRates?.fillPlainFields || 1_000,
414
418
  generateImages: this.options.refreshRates?.generateImages || 5_000,
415
419
  regenerateImages: this.options.refreshRates?.regenerateImages || 5_000,
420
+ },
421
+ askConfirmationBeforeGenerating: this.options.askConfirmationBeforeGenerating || false,
422
+ generationPrompts: {
423
+ plainFieldsPrompts: this.options.fillPlainFields || {},
424
+ imageFieldsPrompts: this.options.fillFieldsFromImages || {},
425
+ imageGenerationPrompts: this.options.generateImages || {},
416
426
  }
417
427
  }
418
428
  }
@@ -489,7 +499,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
489
499
  }
490
500
  }
491
501
  }
492
- if (this.options.fillFieldsFromImages || this.options.fillPlainFields || this.options.generateImages) {
502
+ if ((this.options.fillFieldsFromImages || this.options.fillPlainFields || this.options.generateImages) && !this.options.provideAdditionalContextForRecord) {
493
503
  let matches: string[] = [];
494
504
  const regex = /{{(.*?)}}/g;
495
505
 
@@ -615,7 +625,11 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
615
625
  if (oldRecord[value]) {
616
626
  // put tag to delete old file
617
627
  try {
618
- await columnPlugin.pluginOptions.storageAdapter.markKeyForDeletation(oldRecord[value]);
628
+ if (columnPlugin.pluginOptions.storageAdapter.markKeyForDeletion !== undefined) {
629
+ await columnPlugin.pluginOptions.storageAdapter.markKeyForDeletion(oldRecord[value]);
630
+ } else {
631
+ await columnPlugin.pluginOptions.storageAdapter.markKeyForDeletation(oldRecord[value]);
632
+ }
619
633
  } catch (e) {
620
634
  // file might be e.g. already deleted, so we catch error
621
635
  console.error(`Error setting tag to true for object ${oldRecord[value]}. File will not be auto-cleaned up`);
@@ -624,7 +638,11 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
624
638
  if (fieldsToUpdate[idx][value] && fieldsToUpdate[idx][value] !== null) {
625
639
  // remove tag from new file
626
640
  // in this case we let it crash if it fails: this is a new file which just was uploaded.
627
- await columnPlugin.pluginOptions.storageAdapter.markKeyForNotDeletation(fieldsToUpdate[idx][value]);
641
+ if (columnPlugin.pluginOptions.storageAdapter.markKeyForNotDeletion !== undefined) {
642
+ await columnPlugin.pluginOptions.storageAdapter.markKeyForNotDeletion(fieldsToUpdate[idx][value]);
643
+ } else {
644
+ await columnPlugin.pluginOptions.storageAdapter.markKeyForNotDeletation(fieldsToUpdate[idx][value]);
645
+ }
628
646
  }
629
647
  }
630
648
  }
@@ -652,12 +670,13 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
652
670
 
653
671
  server.endpoint({
654
672
  method: 'POST',
655
- path: `/plugin/${this.pluginInstanceId}/get_generation_prompts`,
673
+ path: `/plugin/${this.pluginInstanceId}/get_image_generation_prompts`,
656
674
  handler: async ({ body, headers }) => {
657
675
  const Id = body.recordId || [];
676
+ const customPrompt = body.customPrompt || null;
658
677
  const record = await this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(this.resourceConfig.columns.find(c => c.primaryKey)?.name, Id)]);
659
- const compiledGenerationOptions = this.compileGenerationFieldTemplates(record);
660
- return { generationOptions: compiledGenerationOptions };
678
+ const compiledGenerationOptions = await this.compileGenerationFieldTemplates(record, JSON.stringify({"prompt": customPrompt}));
679
+ return compiledGenerationOptions;
661
680
  }
662
681
  });
663
682
 
@@ -679,10 +698,9 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
679
698
  method: 'POST',
680
699
  path: `/plugin/${this.pluginInstanceId}/create-job`,
681
700
  handler: async ({ body, adminUser, headers }) => {
682
- const { actionType, recordId } = body;
701
+ const { actionType, recordId, customPrompt } = body;
683
702
  const jobId = randomUUID();
684
703
  jobs.set(jobId, { status: "in_progress" });
685
-
686
704
  if (!actionType) {
687
705
  jobs.set(jobId, { status: "failed", error: "Missing action type" });
688
706
  //return { error: "Missing action type" };
@@ -693,13 +711,13 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
693
711
  } else {
694
712
  switch(actionType) {
695
713
  case 'generate_images':
696
- this.initialImageGenerate(jobId, recordId, adminUser, headers);
714
+ this.initialImageGenerate(jobId, recordId, adminUser, headers, customPrompt);
697
715
  break;
698
716
  case 'analyze_no_images':
699
- this.analyzeNoImages(jobId, recordId, adminUser, headers);
717
+ this.analyzeNoImages(jobId, recordId, adminUser, headers, customPrompt);
700
718
  break;
701
719
  case 'analyze':
702
- this.analyze_image(jobId, recordId, adminUser, headers);
720
+ this.analyze_image(jobId, recordId, adminUser, headers, customPrompt);
703
721
  break;
704
722
  case 'regenerate_images':
705
723
  if (!body.prompt || !body.fieldName) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/bulk-ai-flow",
3
- "version": "1.14.6",
3
+ "version": "1.15.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "@types/handlebars": "^4.0.40",
30
- "adminforth": "^2.4.0-next.222",
30
+ "adminforth": "^2.4.0-next.315",
31
31
  "handlebars": "^4.7.8"
32
32
  },
33
33
  "release": {
package/types.ts CHANGED
@@ -1,5 +1,4 @@
1
- import { ImageVisionAdapter, ImageGenerationAdapter, CompletionAdapter } from "adminforth";
2
-
1
+ import AdminForth, { ImageVisionAdapter, ImageGenerationAdapter, CompletionAdapter } from "adminforth";
3
2
 
4
3
  export interface PluginOptions {
5
4
  /**
@@ -109,4 +108,15 @@ export interface PluginOptions {
109
108
  ok: boolean;
110
109
  error?: undefined;
111
110
  }>
111
+
112
+ /**
113
+ * Custom message for the context shown to the user when performing the action
114
+ */
115
+ provideAdditionalContextForRecord?: ({record, adminUser, resource}: {
116
+ record: any;
117
+ adminUser: any;
118
+ resource: any;
119
+ }) => Record<string, any> | Promise<Record<string, any>>;
120
+
121
+ askConfirmationBeforeGenerating?: boolean;
112
122
  }