@adminforth/bulk-ai-flow 1.14.5 → 1.15.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.
@@ -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
 
@@ -302,7 +377,7 @@ async function getRecords() {
302
377
  } catch (error) {
303
378
  console.error('Failed to get records:', error);
304
379
  isError.value = true;
305
- errorMessage.value = `Failed to fetch records. Please, try to re-run the action.`;
380
+ errorMessage.value = t(`Failed to fetch records. Please, try to re-run the action.`);
306
381
  }
307
382
  }
308
383
 
@@ -319,7 +394,7 @@ async function getImages() {
319
394
  } catch (error) {
320
395
  console.error('Failed to get images:', error);
321
396
  isError.value = true;
322
- errorMessage.value = `Failed to fetch images. Please, try to re-run the action.`;
397
+ errorMessage.value = t(`Failed to fetch images. Please, try to re-run the action.`);
323
398
  }
324
399
  }
325
400
 
@@ -375,7 +450,7 @@ async function convertImages(fieldName, img) {
375
450
 
376
451
  async function saveData() {
377
452
  if (!selected.value?.length) {
378
- adminforth.alert({ message: 'No items selected', variant: 'warning' });
453
+ adminforth.alert({ message: t('No items selected'), variant: 'warning' });
379
454
  return;
380
455
  }
381
456
  try {
@@ -420,16 +495,16 @@ async function saveData() {
420
495
  timeout: 'unlimited',
421
496
  });
422
497
  isError.value = true;
423
- errorMessage.value = `Failed to save data. You are not allowed to save.`;
498
+ errorMessage.value = t(`Failed to save data. You are not allowed to save.`);
424
499
  } else {
425
500
  console.error('Error saving data:', res);
426
501
  isError.value = true;
427
- errorMessage.value = `Failed to save data. Please, try to re-run the action.`;
502
+ errorMessage.value = t(`Failed to save data. Please, try to re-run the action.`);
428
503
  }
429
504
  } catch (error) {
430
505
  console.error('Error saving data:', error);
431
506
  isError.value = true;
432
- errorMessage.value = `Failed to save data. Please, try to re-run the action.`;
507
+ errorMessage.value = t(`Failed to save data. Please, try to re-run the action.`);
433
508
  } finally {
434
509
  isLoading.value = false;
435
510
  }
@@ -474,7 +549,7 @@ async function runAiAction({
474
549
  if (rateLimitRes?.error) {
475
550
  isRateLimitExceeded = true;
476
551
  adminforth.alert({
477
- message: `Rate limit exceeded for "${actionType.replace('_', ' ')}" action. Please try again later.`,
552
+ message: t(`Rate limit exceeded for "${actionType.replace('_', ' ')}" action. Please try again later.`),
478
553
  variant: 'danger',
479
554
  timeout: 'unlimited',
480
555
  });
@@ -482,7 +557,7 @@ async function runAiAction({
482
557
  }
483
558
  } catch (e) {
484
559
  adminforth.alert({
485
- message: `Error checking rate limit for "${actionType.replace('_', ' ')}" action.`,
560
+ message: t(`Error checking rate limit for "${actionType.replace('_', ' ')}" action.`),
486
561
  variant: 'danger',
487
562
  timeout: 'unlimited',
488
563
  });
@@ -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
 
@@ -516,7 +601,7 @@ async function runAiAction({
516
601
  } catch (e) {
517
602
  console.error(`Error during ${actionType} for item ${i}:`, e);
518
603
  hasError = true;
519
- errorMessage = `Failed to ${actionType.replace('_', ' ')}. Please, try to re-run the action.`;
604
+ errorMessage = t(`Failed to ${actionType.replace('_', ' ')}. Please, try to re-run the action.`);
520
605
  return { success: false, index: i, error: e };
521
606
  }
522
607
  });
@@ -591,7 +676,7 @@ async function runAiAction({
591
676
  }
592
677
  isAtLeastOneInProgress = true;
593
678
  adminforth.alert({
594
- message: `Generation action "${actionType.replace('_', ' ')}" failed for record: ${recordId}. Error: ${jobResponse.job?.error || 'Unknown error'}`,
679
+ message: t(`Generation action "${actionType.replace('_', ' ')}" failed for record: ${recordId}. Error: ${jobResponse.job?.error || 'Unknown error'}`),
595
680
  variant: 'danger',
596
681
  timeout: 'unlimited',
597
682
  });
@@ -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>
@@ -6,7 +6,7 @@
6
6
  >
7
7
  <!-- HEADER TEMPLATE -->
8
8
  <template #header:checkboxes="{ item }">
9
- MARK FOR SAVE
9
+ {{ $t('MARK FOR SAVE') }}
10
10
  </template>
11
11
  <!-- CHECKBOX CELL TEMPLATE -->
12
12
  <template #cell:checkboxes="{ item }">
@@ -28,12 +28,12 @@
28
28
  @click="zoomImage(image)"
29
29
  />
30
30
  <div v-else class="w-20 h-20">
31
- <p>Invalid source image</p>
31
+ <p>{{ $t('Invalid source image') }}</p>
32
32
  </div>
33
33
  </div>
34
34
  </div>
35
35
  <div class="flex items-center justify-center text-center w-20 h-20" v-else>
36
- <p>No images found</p>
36
+ <p>{{ $t('No images found') }}</p>
37
37
  </div>
38
38
  <transition name="fade">
39
39
  <div
@@ -66,7 +66,7 @@
66
66
  </Select>
67
67
  <Tooltip>
68
68
  <div class="mt-2 flex items-center justify-start gap-1 hover:text-blue-500" :class="{ 'opacity-0': !hovers[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] }">
69
- <p class="text-sm ">old value</p>
69
+ <p class="text-sm ">{{ $t('old value') }}</p>
70
70
  </div>
71
71
  <template #tooltip>
72
72
  {{ oldData[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] }}
@@ -82,7 +82,7 @@
82
82
  </Textarea>
83
83
  <Tooltip>
84
84
  <div class="mt-2 flex items-center justify-start gap-1 hover:text-blue-500" :class="{ 'opacity-0': !hovers[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] }">
85
- <p class="text-sm ">old value</p>
85
+ <p class="text-sm ">{{ $t('old value') }}</p>
86
86
  </div>
87
87
  <template #tooltip>
88
88
  <p class="max-w-[200px]">{{ oldData[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] }}</p>
@@ -97,7 +97,7 @@
97
97
  </Toggle>
98
98
  <Tooltip>
99
99
  <div class="mt-2 flex items-center justify-start gap-1 hover:text-blue-500" :class="{ 'opacity-0': !hovers[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] }">
100
- <p class="text-sm ">old value</p>
100
+ <p class="text-sm ">{{ $t('old value') }}</p>
101
101
  </div>
102
102
  <template #tooltip>
103
103
  {{ oldData[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] }}
@@ -113,7 +113,7 @@
113
113
  />
114
114
  <Tooltip>
115
115
  <div class="mt-2 flex items-center justify-start gap-1 hover:text-blue-500" :class="{ 'opacity-0': !hovers[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] }">
116
- <p class="text-sm ">old value</p>
116
+ <p class="text-sm ">{{ $t('old value') }}</p>
117
117
  </div>
118
118
  <template #tooltip>
119
119
  {{ oldData[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] }}
@@ -137,14 +137,14 @@
137
137
  :class="{ 'opacity-0': !hovers[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] }"
138
138
  @click="() => {openImageCompare[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])][n] = true}"
139
139
  >
140
- old image
140
+ {{ $t('old image') }}
141
141
  </p>
142
142
  </div>
143
143
  <div v-else class="flex items-center justify-center text-center w-20 h-20">
144
144
  <Tooltip v-if="imageGenerationErrorMessage[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])] === 'No source images found'">
145
145
  <p
146
146
  >
147
- Can't generate image.
147
+ {{ $t("Can't generate image.") }}
148
148
  </p>
149
149
  <template #tooltip>
150
150
  {{ imageGenerationErrorMessage[tableColumnsIndexes.findIndex(el => el[primaryKey] === item[primaryKey])] }}
@@ -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) {
@@ -628,13 +646,14 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
628
646
  });
629
647
  server.endpoint({
630
648
  method: 'POST',
631
- path: `/plugin/${this.pluginInstanceId}/get_generation_prompts`,
649
+ path: `/plugin/${this.pluginInstanceId}/get_image_generation_prompts`,
632
650
  handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, headers }) {
633
651
  var _b;
634
652
  const Id = body.recordId || [];
653
+ const customPrompt = body.customPrompt || null;
635
654
  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 };
655
+ const compiledGenerationOptions = yield this.compileGenerationFieldTemplates(record, JSON.stringify({ "prompt": customPrompt }));
656
+ return compiledGenerationOptions;
638
657
  })
639
658
  });
640
659
  server.endpoint({
@@ -652,7 +671,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
652
671
  method: 'POST',
653
672
  path: `/plugin/${this.pluginInstanceId}/create-job`,
654
673
  handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, headers }) {
655
- const { actionType, recordId } = body;
674
+ const { actionType, recordId, customPrompt } = body;
656
675
  const jobId = randomUUID();
657
676
  jobs.set(jobId, { status: "in_progress" });
658
677
  if (!actionType) {
@@ -666,13 +685,13 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
666
685
  else {
667
686
  switch (actionType) {
668
687
  case 'generate_images':
669
- this.initialImageGenerate(jobId, recordId, adminUser, headers);
688
+ this.initialImageGenerate(jobId, recordId, adminUser, headers, customPrompt);
670
689
  break;
671
690
  case 'analyze_no_images':
672
- this.analyzeNoImages(jobId, recordId, adminUser, headers);
691
+ this.analyzeNoImages(jobId, recordId, adminUser, headers, customPrompt);
673
692
  break;
674
693
  case 'analyze':
675
- this.analyze_image(jobId, recordId, adminUser, headers);
694
+ this.analyze_image(jobId, recordId, adminUser, headers, customPrompt);
676
695
  break;
677
696
  case 'regenerate_images':
678
697
  if (!body.prompt || !body.fieldName) {