@adminforth/bulk-ai-flow 1.1.4 → 1.2.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.
@@ -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="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="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="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">{{ "Network error, please try again" }}</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"
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,65 @@ 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 errorMessage = ref('');
110
+ const checkedCount = ref(0);
72
111
 
73
112
  const openDialog = async () => {
74
113
  confirmDialog.value.open();
75
114
  await getRecords();
76
- await getImages();
115
+ if (props.meta.isFieldsForAnalizeFromImages || props.meta.isImageGeneration) {
116
+ await getImages();
117
+ }
77
118
  tableHeaders.value = generateTableHeaders(props.meta.outputFields);
78
119
  const result = generateTableColumns();
79
120
  tableColumns.value = result.tableData;
80
121
  tableColumnsIndexes.value = result.indexes;
81
-
82
- customFieldNames.value = tableHeaders.value.slice(3).map(h => h.fieldName);
122
+ customFieldNames.value = tableHeaders.value.slice(props.meta.isFieldsForAnalizeFromImages ? 3 : 2).map(h => h.fieldName);
83
123
  setSelected();
84
- analyzeFields();
124
+ for (let i = 0; i < selected.value?.length; i++) {
125
+ openGenerationCarousel.value[i] = props.meta.outputImageFields?.reduce((acc,key) =>{
126
+ acc[key] = false;
127
+ return acc;
128
+ },{[primaryKey]: records.value[i][primaryKey]} as Record<string, boolean>);
129
+ }
130
+ isLoading.value = true;
131
+ const tasks = [];
132
+ if (props.meta.isFieldsForAnalizeFromImages) {
133
+ tasks.push(analyzeFields());
134
+ }
135
+ if (props.meta.isFieldsForAnalizePlain) {
136
+ tasks.push(analyzeFieldsNoImages());
137
+ }
138
+ if (props.meta.isImageGeneration) {
139
+ tasks.push(generateImages());
140
+ }
141
+ await Promise.all(tasks);
142
+ isLoading.value = false;
85
143
  }
86
144
 
87
- // watch(selected, (val) => {
88
- // console.log('Selected changed:', val);
89
- // }, { deep: true });
145
+ watch(selected, (val) => {
146
+ //console.log('Selected changed:', val);
147
+ checkedCount.value = val.filter(item => item.isChecked === true).length;
148
+ }, { deep: true });
90
149
 
91
150
  const closeDialog = () => {
92
151
  confirmDialog.value.close();
93
- isAiResponseReceived.value = [];
152
+ isAiResponseReceivedAnalize.value = [];
153
+ isAiResponseReceivedImage.value = [];
154
+
155
+ records.value = [];
156
+ images.value = [];
157
+ selected.value = [];
158
+ tableColumns.value = [];
159
+ tableColumnsIndexes.value = [];
160
+ isError.value = false;
161
+ errorMessage.value = '';
94
162
  }
95
163
 
96
164
  function formatLabel(str) {
@@ -105,8 +173,9 @@ function generateTableHeaders(outputFields) {
105
173
 
106
174
  headers.push({ label: 'Checkboxes', fieldName: 'checkboxes' });
107
175
  headers.push({ label: 'Field name', fieldName: 'label' });
108
- headers.push({ label: 'Source Images', fieldName: 'images' });
109
-
176
+ if (props.meta.isFieldsForAnalizeFromImages) {
177
+ headers.push({ label: 'Source Images', fieldName: 'images' });
178
+ }
110
179
  for (const key in outputFields) {
111
180
  headers.push({
112
181
  label: formatLabel(key),
@@ -145,7 +214,7 @@ function setSelected() {
145
214
  selected.value = records.value.map(() => ({}));
146
215
  records.value.forEach((record, index) => {
147
216
  for (const key in props.meta.outputFields) {
148
- if(isInColumnEnum(key)){
217
+ if (isInColumnEnum(key)) {
149
218
  const colEnum = props.meta.columnEnums.find(c => c.name === key);
150
219
  const object = colEnum.enum.find(item => item.value === record[key]);
151
220
  selected.value[index][key] = object ? record[key] : null;
@@ -155,7 +224,7 @@ function setSelected() {
155
224
  }
156
225
  selected.value[index].isChecked = true;
157
226
  selected.value[index][primaryKey] = record[primaryKey];
158
- isAiResponseReceived.value[index] = true;
227
+ isAiResponseReceivedAnalize.value[index] = true;
159
228
  });
160
229
  }
161
230
 
@@ -167,30 +236,48 @@ function isInColumnEnum(key: string): boolean {
167
236
  return true;
168
237
  }
169
238
 
239
+ function handleTableError(errorData) {
240
+ isError.value = errorData.isError;
241
+ errorMessage.value = errorData.errorMessage;
242
+ }
243
+
170
244
  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;
245
+ try {
246
+ const res = await callAdminForthApi({
247
+ path: `/plugin/${props.meta.pluginInstanceId}/get_records`,
248
+ method: 'POST',
249
+ body: {
250
+ record: props.checkboxes,
251
+ },
252
+ });
253
+ records.value = res.records;
254
+ } catch (error) {
255
+ console.error('Failed to get records:', error);
256
+ isError.value = true;
257
+ errorMessage.value = `Failed to get records ${error}. Please, try to re-run the action.`;
258
+ // Handle error appropriately
259
+ }
179
260
  }
180
261
 
181
262
  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;
263
+ try {
264
+ const res = await callAdminForthApi({
265
+ path: `/plugin/${props.meta.pluginInstanceId}/get_images`,
266
+ method: 'POST',
267
+ body: {
268
+ record: records.value,
269
+ },
270
+ });
271
+ images.value = res.images;
272
+ } catch (error) {
273
+ console.error('Failed to get images:', error);
274
+ isError.value = true;
275
+ errorMessage.value = `Failed to get images ${error}. Please, try to re-run the action.`;
276
+ // Handle error appropriately
277
+ }
191
278
  }
192
279
 
193
- function prepareDataForSave() {
280
+ async function prepareDataForSave() {
194
281
  const checkedItems = selected.value
195
282
  .filter(item => item.isChecked === true)
196
283
  .map(item => {
@@ -200,51 +287,284 @@ function prepareDataForSave() {
200
287
  const checkedItemsIDs = selected.value
201
288
  .filter(item => item.isChecked === true)
202
289
  .map(item => item[primaryKey]);
290
+
291
+ const promises = [];
292
+ for (const item of checkedItems) {
293
+ for (const [key, value] of Object.entries(item)) {
294
+ if(props.meta.outputImageFields?.includes(key)) {
295
+ const p = convertImages(key, value).then(result => {
296
+ item[key] = result;
297
+ });
298
+
299
+ promises.push(p);
300
+ }
301
+ }
302
+ }
303
+ await Promise.all(promises);
304
+
203
305
  return [checkedItemsIDs, checkedItems];
204
306
  }
205
307
 
308
+ async function convertImages(fieldName, img) {
309
+ let imgBlob;
310
+ if (img.startsWith('data:')) {
311
+ const base64 = img.split(',')[1];
312
+ const mimeType = img.split(';')[0].split(':')[1];
313
+ const byteCharacters = atob(base64);
314
+ const byteNumbers = new Array(byteCharacters.length);
315
+ for (let i = 0; i < byteCharacters.length; i++) {
316
+ byteNumbers[i] = byteCharacters.charCodeAt(i);
317
+ }
318
+ const byteArray = new Uint8Array(byteNumbers);
319
+ imgBlob = new Blob([byteArray], { type: mimeType });
320
+ } else {
321
+ imgBlob = await fetch(
322
+ `/adminapi/v1/plugin/${props.meta.outputImagesPluginInstanceIds[fieldName]}/cors-proxy?url=${encodeURIComponent(img)}`
323
+ ).then(res => { return res.blob() });
324
+ }
325
+ return imgBlob;
326
+ }
327
+
328
+
206
329
  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
- });
330
+ try {
331
+ isAiResponseReceivedAnalize.value = props.checkboxes.map(() => false);
332
+
333
+ const res = await callAdminForthApi({
334
+ path: `/plugin/${props.meta.pluginInstanceId}/analyze`,
335
+ method: 'POST',
336
+ body: {
337
+ selectedIds: props.checkboxes,
338
+ },
339
+ });
340
+
341
+ isAiResponseReceivedAnalize.value = props.checkboxes.map(() => true);
342
+
343
+ res.result.forEach((item, idx) => {
344
+ const pk = selected.value[idx]?.[primaryKey]
345
+
346
+ if (pk) {
347
+ selected.value[idx] = {
348
+ ...selected.value[idx],
349
+ ...item,
350
+ isChecked: true,
351
+ [primaryKey]: pk
352
+ }
353
+ }
354
+ })
355
+ } catch (error) {
356
+ console.error('Failed to get records:', error);
357
+ isError.value = true;
358
+ errorMessage.value = `Failed to get records ${error}. Please, try to re-run the action.`;
359
+ }
360
+ }
361
+
362
+
363
+ async function analyzeFieldsNoImages() {
364
+ try {
365
+ isAiResponseReceivedAnalize.value = props.checkboxes.map(() => false);
216
366
 
217
- isAiResponseReceived.value = props.checkboxes.map(() => true);
367
+ const res = await callAdminForthApi({
368
+ path: `/plugin/${props.meta.pluginInstanceId}/analyze_no_images`,
369
+ method: 'POST',
370
+ body: {
371
+ selectedIds: props.checkboxes,
372
+ },
373
+ });
374
+ if(!props.meta.isFieldsForAnalizeFromImages) {
375
+ isAiResponseReceivedAnalize.value = props.checkboxes.map(() => true);
376
+ }
377
+
378
+ res.result.forEach((item, idx) => {
379
+ const pk = selected.value[idx]?.[primaryKey]
218
380
 
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
- )
381
+ if (pk) {
382
+ selected.value[idx] = {
383
+ ...selected.value[idx],
384
+ ...item,
385
+ isChecked: true,
386
+ [primaryKey]: pk
387
+ }
388
+ }
389
+ })
390
+ } catch (error) {
391
+ console.error('Failed to get records:', error);
392
+ isError.value = true;
393
+ errorMessage.value = `Failed to get records ${error}. Please, try to re-run the action.`;
394
+ }
228
395
  }
229
396
 
397
+
398
+
399
+
230
400
  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
- });
401
+ if (!selected.value?.length) {
402
+ adminforth.alert({ message: 'No items selected', variant: 'warning' });
403
+ return;
404
+ }
405
+ try {
406
+ isLoading.value = true;
407
+ const [checkedItemsIDs, reqData] = await prepareDataForSave();
408
+
409
+ const imagesToUpload = [];
410
+ for (const item of reqData) {
411
+ for (const [key, value] of Object.entries(item)) {
412
+ if(props.meta.outputImageFields?.includes(key)) {
413
+ const p = uploadImage(value, item[primaryKey], key).then(result => {
414
+ item[key] = result;
415
+ });
416
+ imagesToUpload.push(p);
417
+ }
418
+ }
419
+ }
420
+ await Promise.all(imagesToUpload);
421
+
422
+ const res = await callAdminForthApi({
423
+ path: `/plugin/${props.meta.pluginInstanceId}/update_fields`,
424
+ method: 'POST',
425
+ body: {
426
+ selectedIds: checkedItemsIDs,
427
+ fields: reqData,
428
+ },
429
+ });
241
430
 
242
- if(res.ok) {
243
- confirmDialog.value.close();
244
- props.updateList();
245
- props.clearCheckboxes();
431
+ if(res.ok) {
432
+ confirmDialog.value.close();
433
+ props.updateList();
434
+ props.clearCheckboxes();
435
+ } else {
436
+ console.error('Error saving data:', res);
437
+ isError.value = true;
438
+ errorMessage.value = `Failed to save data ${res}. Please, try to re-run the action.`;
439
+ }
440
+ } catch (error) {
441
+ console.error('Error saving data:', error);
442
+ isError.value = true;
443
+ errorMessage.value = `Failed to save data ${error}. Please, try to re-run the action.`;
444
+ } finally {
445
+ isLoading.value = false;
446
+ }
447
+ }
448
+
449
+ async function generateImages() {
450
+ isAiResponseReceivedImage.value = props.checkboxes.map(() => false);
451
+ let res;
452
+ let error = null;
453
+
454
+ try {
455
+ res = await callAdminForthApi({
456
+ path: `/plugin/${props.meta.pluginInstanceId}/initial_image_generate`,
457
+ method: 'POST',
458
+ body: {
459
+ selectedIds: props.checkboxes,
460
+ },
461
+ });
462
+ } catch (e) {
463
+ console.error('Error generating images:', e);
464
+ isError.value = true;
465
+ errorMessage.value = `Failed to generate images ${e}. Please, try to re-run the action.`;
466
+ }
467
+ isAiResponseReceivedImage.value = props.checkboxes.map(() => true);
468
+
469
+ if (res?.error) {
470
+ error = res.error;
471
+ }
472
+ if (!res) {
473
+ error = 'Error generating images, something went wrong';
474
+ isError.value = true;
475
+ errorMessage.value = `Failed to generate images ${e}. Please, try to re-run the action.`;
476
+ }
477
+
478
+ if (error) {
479
+ adminforth.alert({
480
+ message: error,
481
+ variant: 'danger',
482
+ timeout: 'unlimited',
483
+ });
246
484
  } else {
247
- console.error('Error saving data:', res);
485
+ res.result.forEach((item, idx) => {
486
+ const pk = selected.value[idx]?.[primaryKey]
487
+ if (pk) {
488
+ selected.value[idx] = {
489
+ ...selected.value[idx],
490
+ ...item,
491
+ [primaryKey]: pk
492
+ }
493
+ }
494
+ })
248
495
  }
249
496
  }
497
+
498
+
499
+ async function uploadImage(imgBlob, id, fieldName) {
500
+ const file = new File([imgBlob], `generated_${fieldName}_${id}.${imgBlob.type.split('/').pop()}`, { type: imgBlob.type });
501
+ const { name, size, type } = file;
502
+
503
+ const extension = name.split('.').pop();
504
+ const nameNoExtension = name.replace(`.${extension}`, '');
505
+
506
+ try {
507
+ const { uploadUrl, uploadExtraParams, filePath, error } = await callAdminForthApi({
508
+ path: `/plugin/${props.meta.outputImagesPluginInstanceIds[fieldName]}/get_file_upload_url`,
509
+ method: 'POST',
510
+ body: {
511
+ originalFilename: nameNoExtension,
512
+ contentType: type,
513
+ size,
514
+ originalExtension: extension,
515
+ recordPk: route?.params?.primaryKey,
516
+ },
517
+ });
518
+
519
+ if (error) {
520
+ adminforth.alert({
521
+ message: t('File was not uploaded because of error: {error}', { error }),
522
+ variant: 'danger'
523
+ });
524
+ return;
525
+ }
526
+
527
+ const xhr = new XMLHttpRequest();
528
+ const success = await new Promise((resolve) => {
529
+ xhr.upload.onprogress = (e) => {
530
+ if (e.lengthComputable) {
531
+ }
532
+ };
533
+ xhr.addEventListener('loadend', () => {
534
+ const success = xhr.readyState === 4 && xhr.status === 200;
535
+ // try to read response
536
+ resolve(success);
537
+ });
538
+ xhr.open('PUT', uploadUrl, true);
539
+ xhr.setRequestHeader('Content-Type', type);
540
+ uploadExtraParams && Object.entries(uploadExtraParams).forEach(([key, value]: [string, string]) => {
541
+ xhr.setRequestHeader(key, value);
542
+ })
543
+ xhr.send(file);
544
+ });
545
+ if (!success) {
546
+ adminforth.alert({
547
+ messageHtml: `<div>${t('Sorry but the file was not uploaded because of internal storage Request Error:')}</div>
548
+ <pre style="white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; max-width: 100%;">${
549
+ xhr.responseText.replace(/</g, '&lt;').replace(/>/g, '&gt;')
550
+ }</pre>`,
551
+ variant: 'danger',
552
+ timeout: 30,
553
+ });
554
+ return;
555
+ }
556
+ return filePath;
557
+ } catch (error) {
558
+ console.error('Error uploading file:', error);
559
+ adminforth.alert({
560
+ message: 'Sorry but the file was not be uploaded. Please try again.',
561
+ variant: 'danger'
562
+ });
563
+
564
+ isError.value = true;
565
+ errorMessage.value = `Failed to upload images. Please, try to re-run the action.`;
566
+ return null;
567
+ }
568
+ }
569
+
250
570
  </script>