@adminforth/bulk-ai-flow 1.1.5 → 1.2.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.
@@ -1,17 +1,28 @@
1
1
  <template>
2
- <div @click="openDialog">
3
- <p class="">{{ props.meta.actionName }}</p>
2
+ <div class="flex items-end justify-start gap-2" @click="openDialog">
3
+ <div class="flex items-center justify-center text-white bg-gradient-to-r h-[18px] 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 font-medium rounded-md text-sm px-1 text-center">
4
+ AI
5
+ </div>
6
+ <p class="text-justify max-h-[18px]">{{ props.meta.actionName }}</p>
4
7
  </div>
5
8
  <Dialog ref="confirmDialog">
6
9
  <div
7
- class="fixed inset-0 z-50 flex items-center justify-center bg-black/40"
10
+ class="fixed inset-0 z-20 flex items-center justify-center bg-black/40"
8
11
  @click="closeDialog"
9
12
  >
10
13
  <div
11
- class="bulk-vision-dialog relative max-w-[95vw] max-h-[90vh] bg-white dark:bg-gray-900 rounded-md shadow-2xl overflow-hidden"
14
+ class="bulk-vision-dialog flex items-center justify-center relative max-w-[95vw] min-w-[640px] max-h-[90vh] bg-white dark:bg-gray-900 rounded-md shadow-2xl overflow-hidden"
12
15
  @click.stop
13
16
  >
14
- <div class="bulk-vision-table flex flex-col items-end justify-evenly gap-4 w-full h-full p-6 overflow-y-auto">
17
+ <div class="bulk-vision-table flex flex-col items-center justify-evenly gap-4 w-full h-full p-6 overflow-y-auto">
18
+ <button type="button"
19
+ @click="closeDialog"
20
+ class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white" >
21
+ <svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
22
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
23
+ </svg>
24
+ </button>
25
+
15
26
  <VisionTable
16
27
  v-if="records && props.checkboxes.length"
17
28
  :checkbox="props.checkboxes"
@@ -24,15 +35,30 @@
24
35
  :customFieldNames="customFieldNames"
25
36
  :tableColumnsIndexes="tableColumnsIndexes"
26
37
  :selected="selected"
27
- :isAiResponseReceived="isAiResponseReceived"
38
+ :isAiResponseReceivedAnalize="isAiResponseReceivedAnalize"
39
+ :isAiResponseReceivedImage="isAiResponseReceivedImage"
28
40
  :primaryKey="primaryKey"
41
+ :openGenerationCarousel="openGenerationCarousel"
42
+ @error="handleTableError"
29
43
  />
30
- <Button
31
- class="bulk-vision-button w-64"
32
- @click="saveData"
33
- >
34
- {{ props.checkboxes.length > 1 ? 'Save fields' : 'Save field' }}
35
- </Button>
44
+ <div class="flex w-full items-end justify-end gap-4">
45
+ <div class="h-full text-red-600 font-semibold flex items-center justify-center mb-2">
46
+ <p v-if="isError === true">{{ errorMessage }}</p>
47
+ </div>
48
+ <button type="button" class="py-2.5 px-5 ms-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
49
+ @click="closeDialog"
50
+ >
51
+ {{'Cancel'}}
52
+ </button>
53
+ <Button
54
+ class="w-64"
55
+ @click="saveData"
56
+ :disabled="isLoading || checkedCount < 1 || isCriticalError"
57
+ :loader="isLoading"
58
+ >
59
+ {{ checkedCount > 1 ? 'Save fields' : 'Save field' }}
60
+ </Button>
61
+ </div>
36
62
  </div>
37
63
  </div>
38
64
  </div>
@@ -41,15 +67,22 @@
41
67
 
42
68
  <script lang="ts" setup>
43
69
  import { callAdminForthApi } from '@/utils';
44
- import { ref, watch } from 'vue'
70
+ import { handleError, ref, watch } from 'vue'
45
71
  import { Dialog, Button } from '@/afcl';
46
72
  import VisionTable from './visionTable.vue'
73
+ import adminforth from '@/adminforth';
74
+ import { useI18n } from 'vue-i18n';
75
+ import { useRoute } from 'vue-router';
76
+ import { type AdminUser, type AdminForthResourceCommon } from '@/types';
77
+
78
+ const route = useRoute();
79
+ const { t } = useI18n();
47
80
 
48
81
  const props = defineProps<{
49
82
  checkboxes: any,
50
83
  meta: any,
51
- resource: any,
52
- adminUser: any,
84
+ resource: AdminForthResourceCommon,
85
+ adminUser: AdminUser,
53
86
  updateList: {
54
87
  type: Function,
55
88
  required: true
@@ -67,30 +100,66 @@ const tableColumns = ref([]);
67
100
  const tableColumnsIndexes = ref([]);
68
101
  const customFieldNames = ref([]);
69
102
  const selected = ref<any[]>([]);
70
- const isAiResponseReceived = ref([]);
103
+ const isAiResponseReceivedAnalize = ref([]);
104
+ const isAiResponseReceivedImage = ref([]);
71
105
  const primaryKey = props.meta.primaryKey;
106
+ const openGenerationCarousel = ref([]);
107
+ const isLoading = ref(false);
108
+ const isError = ref(false);
109
+ const isCriticalError = ref(false);
110
+ const errorMessage = ref('');
111
+ const checkedCount = ref(0);
72
112
 
73
113
  const openDialog = async () => {
74
114
  confirmDialog.value.open();
75
115
  await getRecords();
76
- await getImages();
116
+ if (props.meta.isFieldsForAnalizeFromImages || props.meta.isImageGeneration) {
117
+ await getImages();
118
+ }
77
119
  tableHeaders.value = generateTableHeaders(props.meta.outputFields);
78
120
  const result = generateTableColumns();
79
121
  tableColumns.value = result.tableData;
80
122
  tableColumnsIndexes.value = result.indexes;
81
-
82
- customFieldNames.value = tableHeaders.value.slice(3).map(h => h.fieldName);
123
+ customFieldNames.value = tableHeaders.value.slice(props.meta.isFieldsForAnalizeFromImages ? 3 : 2).map(h => h.fieldName);
83
124
  setSelected();
84
- analyzeFields();
125
+ for (let i = 0; i < selected.value?.length; i++) {
126
+ openGenerationCarousel.value[i] = props.meta.outputImageFields?.reduce((acc,key) =>{
127
+ acc[key] = false;
128
+ return acc;
129
+ },{[primaryKey]: records.value[i][primaryKey]} as Record<string, boolean>);
130
+ }
131
+ isLoading.value = true;
132
+ const tasks = [];
133
+ if (props.meta.isFieldsForAnalizeFromImages) {
134
+ tasks.push(analyzeFields());
135
+ }
136
+ if (props.meta.isFieldsForAnalizePlain) {
137
+ tasks.push(analyzeFieldsNoImages());
138
+ }
139
+ if (props.meta.isImageGeneration) {
140
+ tasks.push(generateImages());
141
+ }
142
+ await Promise.all(tasks);
143
+ isLoading.value = false;
85
144
  }
86
145
 
87
- // watch(selected, (val) => {
88
- // console.log('Selected changed:', val);
89
- // }, { deep: true });
146
+ watch(selected, (val) => {
147
+ console.log('Selected changed:', val);
148
+ checkedCount.value = val.filter(item => item.isChecked === true).length;
149
+ }, { deep: true });
90
150
 
91
151
  const closeDialog = () => {
92
152
  confirmDialog.value.close();
93
- isAiResponseReceived.value = [];
153
+ isAiResponseReceivedAnalize.value = [];
154
+ isAiResponseReceivedImage.value = [];
155
+
156
+ records.value = [];
157
+ images.value = [];
158
+ selected.value = [];
159
+ tableColumns.value = [];
160
+ tableColumnsIndexes.value = [];
161
+ isError.value = false;
162
+ errorMessage.value = '';
94
163
  }
95
164
 
96
165
  function formatLabel(str) {
@@ -105,8 +174,9 @@ function generateTableHeaders(outputFields) {
105
174
 
106
175
  headers.push({ label: 'Checkboxes', fieldName: 'checkboxes' });
107
176
  headers.push({ label: 'Field name', fieldName: 'label' });
108
- headers.push({ label: 'Source Images', fieldName: 'images' });
109
-
177
+ if (props.meta.isFieldsForAnalizeFromImages) {
178
+ headers.push({ label: 'Source Images', fieldName: 'images' });
179
+ }
110
180
  for (const key in outputFields) {
111
181
  headers.push({
112
182
  label: formatLabel(key),
@@ -145,7 +215,7 @@ function setSelected() {
145
215
  selected.value = records.value.map(() => ({}));
146
216
  records.value.forEach((record, index) => {
147
217
  for (const key in props.meta.outputFields) {
148
- if(isInColumnEnum(key)){
218
+ if (isInColumnEnum(key)) {
149
219
  const colEnum = props.meta.columnEnums.find(c => c.name === key);
150
220
  const object = colEnum.enum.find(item => item.value === record[key]);
151
221
  selected.value[index][key] = object ? record[key] : null;
@@ -155,7 +225,7 @@ function setSelected() {
155
225
  }
156
226
  selected.value[index].isChecked = true;
157
227
  selected.value[index][primaryKey] = record[primaryKey];
158
- isAiResponseReceived.value[index] = true;
228
+ isAiResponseReceivedAnalize.value[index] = true;
159
229
  });
160
230
  }
161
231
 
@@ -167,30 +237,48 @@ function isInColumnEnum(key: string): boolean {
167
237
  return true;
168
238
  }
169
239
 
240
+ function handleTableError(errorData) {
241
+ isError.value = errorData.isError;
242
+ errorMessage.value = errorData.errorMessage;
243
+ }
244
+
170
245
  async function getRecords() {
171
- const res = await callAdminForthApi({
172
- path: `/plugin/${props.meta.pluginInstanceId}/get_records`,
173
- method: 'POST',
174
- body: {
175
- record: props.checkboxes,
176
- },
177
- });
178
- records.value = res.records;
246
+ try {
247
+ const res = await callAdminForthApi({
248
+ path: `/plugin/${props.meta.pluginInstanceId}/get_records`,
249
+ method: 'POST',
250
+ body: {
251
+ record: props.checkboxes,
252
+ },
253
+ });
254
+ records.value = res.records;
255
+ } catch (error) {
256
+ console.error('Failed to get records:', error);
257
+ isError.value = true;
258
+ errorMessage.value = `Failed to fetch records. Please, try to re-run the action.`;
259
+ // Handle error appropriately
260
+ }
179
261
  }
180
262
 
181
263
  async function getImages() {
182
- const res = await callAdminForthApi({
183
- path: `/plugin/${props.meta.pluginInstanceId}/get_images`,
184
- method: 'POST',
185
- body: {
186
- record: records.value,
187
- },
188
- });
189
-
190
- images.value = res.images;
264
+ try {
265
+ const res = await callAdminForthApi({
266
+ path: `/plugin/${props.meta.pluginInstanceId}/get_images`,
267
+ method: 'POST',
268
+ body: {
269
+ record: records.value,
270
+ },
271
+ });
272
+ images.value = res.images;
273
+ } catch (error) {
274
+ console.error('Failed to get images:', error);
275
+ isError.value = true;
276
+ errorMessage.value = `Failed to fetch images. Please, try to re-run the action.`;
277
+ // Handle error appropriately
278
+ }
191
279
  }
192
280
 
193
- function prepareDataForSave() {
281
+ async function prepareDataForSave() {
194
282
  const checkedItems = selected.value
195
283
  .filter(item => item.isChecked === true)
196
284
  .map(item => {
@@ -200,51 +288,286 @@ function prepareDataForSave() {
200
288
  const checkedItemsIDs = selected.value
201
289
  .filter(item => item.isChecked === true)
202
290
  .map(item => item[primaryKey]);
291
+
292
+ const promises = [];
293
+ for (const item of checkedItems) {
294
+ for (const [key, value] of Object.entries(item)) {
295
+ if(props.meta.outputImageFields?.includes(key)) {
296
+ const p = convertImages(key, value).then(result => {
297
+ item[key] = result;
298
+ });
299
+
300
+ promises.push(p);
301
+ }
302
+ }
303
+ }
304
+ await Promise.all(promises);
305
+
203
306
  return [checkedItemsIDs, checkedItems];
204
307
  }
205
308
 
309
+ async function convertImages(fieldName, img) {
310
+ let imgBlob;
311
+ if (img.startsWith('data:')) {
312
+ const base64 = img.split(',')[1];
313
+ const mimeType = img.split(';')[0].split(':')[1];
314
+ const byteCharacters = atob(base64);
315
+ const byteNumbers = new Array(byteCharacters.length);
316
+ for (let i = 0; i < byteCharacters.length; i++) {
317
+ byteNumbers[i] = byteCharacters.charCodeAt(i);
318
+ }
319
+ const byteArray = new Uint8Array(byteNumbers);
320
+ imgBlob = new Blob([byteArray], { type: mimeType });
321
+ } else {
322
+ imgBlob = await fetch(
323
+ `/adminapi/v1/plugin/${props.meta.outputImagesPluginInstanceIds[fieldName]}/cors-proxy?url=${encodeURIComponent(img)}`
324
+ ).then(res => { return res.blob() });
325
+ }
326
+ return imgBlob;
327
+ }
328
+
329
+
206
330
  async function analyzeFields() {
207
- isAiResponseReceived.value = props.checkboxes.map(() => false);
208
-
209
- const res = await callAdminForthApi({
210
- path: `/plugin/${props.meta.pluginInstanceId}/analyze`,
211
- method: 'POST',
212
- body: {
213
- selectedIds: props.checkboxes,
214
- },
215
- });
331
+ try {
332
+ isAiResponseReceivedAnalize.value = props.checkboxes.map(() => false);
333
+
334
+ const res = await callAdminForthApi({
335
+ path: `/plugin/${props.meta.pluginInstanceId}/analyze`,
336
+ method: 'POST',
337
+ body: {
338
+ selectedIds: props.checkboxes,
339
+ },
340
+ });
341
+
342
+ isAiResponseReceivedAnalize.value = props.checkboxes.map(() => true);
343
+
344
+ res.result.forEach((item, idx) => {
345
+ const pk = selected.value[idx]?.[primaryKey]
346
+
347
+ if (pk) {
348
+ selected.value[idx] = {
349
+ ...selected.value[idx],
350
+ ...item,
351
+ isChecked: true,
352
+ [primaryKey]: pk
353
+ }
354
+ }
355
+ })
356
+ } catch (error) {
357
+ console.error('Failed to analyze image(s):', error);
358
+ isError.value = true;
359
+ errorMessage.value = `Failed to fetch analyze image(s). Please, try to re-run the action.`;
360
+ }
361
+ }
362
+
363
+
364
+ async function analyzeFieldsNoImages() {
365
+ try {
366
+ isAiResponseReceivedAnalize.value = props.checkboxes.map(() => false);
216
367
 
217
- isAiResponseReceived.value = props.checkboxes.map(() => true);
368
+ const res = await callAdminForthApi({
369
+ path: `/plugin/${props.meta.pluginInstanceId}/analyze_no_images`,
370
+ method: 'POST',
371
+ body: {
372
+ selectedIds: props.checkboxes,
373
+ },
374
+ });
375
+ if(!props.meta.isFieldsForAnalizeFromImages) {
376
+ isAiResponseReceivedAnalize.value = props.checkboxes.map(() => true);
377
+ }
378
+
379
+ res.result.forEach((item, idx) => {
380
+ const pk = selected.value[idx]?.[primaryKey]
218
381
 
219
- selected.value.splice(
220
- 0,
221
- selected.value.length,
222
- ...res.result.map((item, idx) => ({
223
- ...item,
224
- isChecked: true,
225
- [primaryKey]: selected.value[idx]?.[primaryKey],
226
- }))
227
- )
382
+ if (pk) {
383
+ selected.value[idx] = {
384
+ ...selected.value[idx],
385
+ ...item,
386
+ isChecked: true,
387
+ [primaryKey]: pk
388
+ }
389
+ }
390
+ })
391
+ } catch (error) {
392
+ console.error('Failed to analyze fields:', error);
393
+ isError.value = true;
394
+ errorMessage.value = `Failed to analyze fields. Please, try to re-run the action.`;
395
+ }
228
396
  }
229
397
 
398
+
399
+
400
+
230
401
  async function saveData() {
231
- const [checkedItemsIDs, reqData] = prepareDataForSave();
232
-
233
- const res = await callAdminForthApi({
234
- path: `/plugin/${props.meta.pluginInstanceId}/update_fields`,
235
- method: 'POST',
236
- body: {
237
- selectedIds: checkedItemsIDs,
238
- fields: reqData,
239
- },
240
- });
402
+ if (!selected.value?.length) {
403
+ adminforth.alert({ message: 'No items selected', variant: 'warning' });
404
+ return;
405
+ }
406
+ try {
407
+ isLoading.value = true;
408
+ const [checkedItemsIDs, reqData] = await prepareDataForSave();
409
+
410
+ const imagesToUpload = [];
411
+ for (const item of reqData) {
412
+ for (const [key, value] of Object.entries(item)) {
413
+ if(props.meta.outputImageFields?.includes(key)) {
414
+ const p = uploadImage(value, item[primaryKey], key).then(result => {
415
+ item[key] = result;
416
+ });
417
+ imagesToUpload.push(p);
418
+ }
419
+ }
420
+ }
421
+ await Promise.all(imagesToUpload);
422
+
423
+ const res = await callAdminForthApi({
424
+ path: `/plugin/${props.meta.pluginInstanceId}/update_fields`,
425
+ method: 'POST',
426
+ body: {
427
+ selectedIds: checkedItemsIDs,
428
+ fields: reqData,
429
+ },
430
+ });
241
431
 
242
- if(res.ok) {
243
- confirmDialog.value.close();
244
- props.updateList();
245
- props.clearCheckboxes();
432
+ if(res.ok) {
433
+ confirmDialog.value.close();
434
+ props.updateList();
435
+ props.clearCheckboxes();
436
+ } else {
437
+ console.error('Error saving data:', res);
438
+ isError.value = true;
439
+ errorMessage.value = `Failed to save data. Please, try to re-run the action.`;
440
+ }
441
+ } catch (error) {
442
+ console.error('Error saving data:', error);
443
+ isError.value = true;
444
+ errorMessage.value = `Failed to save data. Please, try to re-run the action.`;
445
+ } finally {
446
+ isLoading.value = false;
447
+ }
448
+ }
449
+
450
+ async function generateImages() {
451
+ isAiResponseReceivedImage.value = props.checkboxes.map(() => false);
452
+ let res;
453
+ let error = null;
454
+
455
+ try {
456
+ res = await callAdminForthApi({
457
+ path: `/plugin/${props.meta.pluginInstanceId}/initial_image_generate`,
458
+ method: 'POST',
459
+ body: {
460
+ selectedIds: props.checkboxes,
461
+ },
462
+ });
463
+ } catch (e) {
464
+ console.error('Error generating images:', e);
465
+ isError.value = true;
466
+ isCriticalError.value = true;
467
+ errorMessage.value = `Failed to generate images. Please, try to re-run the action.`;
468
+ }
469
+ isAiResponseReceivedImage.value = props.checkboxes.map(() => true);
470
+
471
+ if (res?.error) {
472
+ error = res.error;
473
+ }
474
+ if (!res) {
475
+ error = 'Error generating images, something went wrong';
476
+ isError.value = true;
477
+ isCriticalError.value = true;
478
+ errorMessage.value = `Failed to generate images. Please, try to re-run the action.`;
479
+ }
480
+
481
+ if (error) {
482
+ adminforth.alert({
483
+ message: error,
484
+ variant: 'danger',
485
+ timeout: 'unlimited',
486
+ });
246
487
  } else {
247
- console.error('Error saving data:', res);
488
+ res.result.forEach((item, idx) => {
489
+ const pk = selected.value[idx]?.[primaryKey]
490
+ if (pk) {
491
+ selected.value[idx] = {
492
+ ...selected.value[idx],
493
+ ...item,
494
+ [primaryKey]: pk
495
+ }
496
+ }
497
+ })
248
498
  }
249
499
  }
500
+
501
+
502
+ async function uploadImage(imgBlob, id, fieldName) {
503
+ const file = new File([imgBlob], `generated_${fieldName}_${id}.${imgBlob.type.split('/').pop()}`, { type: imgBlob.type });
504
+ const { name, size, type } = file;
505
+
506
+ const extension = name.split('.').pop();
507
+ const nameNoExtension = name.replace(`.${extension}`, '');
508
+
509
+ try {
510
+ const { uploadUrl, uploadExtraParams, filePath, error } = await callAdminForthApi({
511
+ path: `/plugin/${props.meta.outputImagesPluginInstanceIds[fieldName]}/get_file_upload_url`,
512
+ method: 'POST',
513
+ body: {
514
+ originalFilename: nameNoExtension,
515
+ contentType: type,
516
+ size,
517
+ originalExtension: extension,
518
+ recordPk: route?.params?.primaryKey,
519
+ },
520
+ });
521
+
522
+ if (error) {
523
+ adminforth.alert({
524
+ message: t('File was not uploaded because of error: {error}', { error }),
525
+ variant: 'danger'
526
+ });
527
+ return;
528
+ }
529
+
530
+ const xhr = new XMLHttpRequest();
531
+ const success = await new Promise((resolve) => {
532
+ xhr.upload.onprogress = (e) => {
533
+ if (e.lengthComputable) {
534
+ }
535
+ };
536
+ xhr.addEventListener('loadend', () => {
537
+ const success = xhr.readyState === 4 && xhr.status === 200;
538
+ // try to read response
539
+ resolve(success);
540
+ });
541
+ xhr.open('PUT', uploadUrl, true);
542
+ xhr.setRequestHeader('Content-Type', type);
543
+ uploadExtraParams && Object.entries(uploadExtraParams).forEach(([key, value]: [string, string]) => {
544
+ xhr.setRequestHeader(key, value);
545
+ })
546
+ xhr.send(file);
547
+ });
548
+ if (!success) {
549
+ adminforth.alert({
550
+ messageHtml: `<div>${t('Sorry but the file was not uploaded because of internal storage Request Error:')}</div>
551
+ <pre style="white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; max-width: 100%;">${
552
+ xhr.responseText.replace(/</g, '&lt;').replace(/>/g, '&gt;')
553
+ }</pre>`,
554
+ variant: 'danger',
555
+ timeout: 30,
556
+ });
557
+ return;
558
+ }
559
+ return filePath;
560
+ } catch (error) {
561
+ console.error('Error uploading file:', error);
562
+ adminforth.alert({
563
+ message: 'Sorry but the file was not be uploaded. Please try again.',
564
+ variant: 'danger'
565
+ });
566
+
567
+ isError.value = true;
568
+ errorMessage.value = `Failed to upload images. Please, try to re-run the action.`;
569
+ return null;
570
+ }
571
+ }
572
+
250
573
  </script>