@adminforth/bulk-ai-flow 1.10.3 → 1.12.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.
@@ -11,6 +11,7 @@
11
11
  "license": "ISC",
12
12
  "dependencies": {
13
13
  "@iconify-prerendered/vue-mdi": "^0.25.1718880438",
14
- "medium-zoom": "^1.1.0"
14
+ "medium-zoom": "^1.1.0",
15
+ "swiper": "^11.2.10"
15
16
  }
16
17
  }
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  });
9
9
  };
10
10
  import { AdminForthPlugin, Filters } from "adminforth";
11
+ import { suggestIfTypo } from "adminforth";
11
12
  import Handlebars from 'handlebars';
12
13
  import { RateLimiter } from "adminforth";
13
14
  import { randomUUID } from "crypto";
@@ -59,12 +60,6 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
59
60
  var _a, _b, _c, _d, _e, _f, _g, _h, _j;
60
61
  const selectedId = recordId;
61
62
  let isError = false;
62
- // if (typeof(this.options.rateLimits?.fillFieldsFromImages) === 'string'){
63
- // if (this.checkRateLimit("fillFieldsFromImages" ,this.options.rateLimits.fillFieldsFromImages, headers)) {
64
- // jobs.set(jobId, { status: 'failed', error: "Rate limit exceeded" });
65
- // return { error: "Rate limit exceeded" };
66
- // }
67
- // }
68
63
  // Fetch the record using the provided ID
69
64
  const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
70
65
  const record = yield this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(primaryKeyColumn.name, selectedId)]);
@@ -72,7 +67,16 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
72
67
  const attachmentFiles = yield this.options.attachFiles({ record: record });
73
68
  if (STUB_MODE) {
74
69
  yield new Promise((resolve) => setTimeout(resolve, Math.floor(Math.random() * 8000) + 1000));
75
- jobs.set(jobId, { status: 'completed', result: {} });
70
+ const fakeError = Math.random() < 0.5; // 50% chance of error
71
+ if (attachmentFiles.length === 0) {
72
+ jobs.set(jobId, { status: 'failed', error: 'No source images found' });
73
+ }
74
+ else if (!fakeError) {
75
+ jobs.set(jobId, { status: 'completed', result: {} });
76
+ }
77
+ else {
78
+ jobs.set(jobId, { status: 'failed', error: 'AI provider refused to analyze images' });
79
+ }
76
80
  return {};
77
81
  }
78
82
  else if (attachmentFiles.length !== 0) {
@@ -110,8 +114,8 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
110
114
  }
111
115
  }
112
116
  else {
113
- jobs.set(jobId, { status: 'failed', error: "No attachment files found" });
114
- return { ok: false, error: "No attachment files found" };
117
+ jobs.set(jobId, { status: 'failed', error: "No source images found" });
118
+ return { ok: false, error: "No source images found" };
115
119
  }
116
120
  });
117
121
  }
@@ -119,12 +123,6 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
119
123
  return __awaiter(this, void 0, void 0, function* () {
120
124
  const selectedId = recordId;
121
125
  let isError = false;
122
- // if (typeof(this.options.rateLimits?.fillPlainFields) === 'string'){
123
- // if (this.checkRateLimit("fillPlainFields", this.options.rateLimits.fillPlainFields, headers)) {
124
- // jobs.set(jobId, { status: 'failed', error: "Rate limit exceeded" });
125
- // return { error: "Rate limit exceeded" };
126
- // }
127
- // }
128
126
  if (STUB_MODE) {
129
127
  yield new Promise((resolve) => setTimeout(resolve, Math.floor(Math.random() * 20000) + 1000));
130
128
  jobs.set(jobId, { status: 'completed', result: {} });
@@ -168,12 +166,6 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
168
166
  var _a, _b;
169
167
  const selectedId = recordId;
170
168
  let isError = false;
171
- // if (typeof(this.options.rateLimits?.generateImages) === 'string'){
172
- // if (this.checkRateLimit("generateImages", this.options.rateLimits.generateImages, headers)) {
173
- // jobs.set(jobId, { status: 'failed', error: "Rate limit exceeded" });
174
- // return { error: "Rate limit exceeded" };
175
- // }
176
- // }
177
169
  const start = +new Date();
178
170
  const record = yield this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ((_a = this.resourceConfig.columns.find(c => c.primaryKey)) === null || _a === void 0 ? void 0 : _a.name, selectedId)]);
179
171
  let attachmentFiles;
@@ -187,13 +179,21 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
187
179
  const prompt = this.compileGenerationFieldTemplates(record)[key];
188
180
  let images;
189
181
  if (this.options.attachFiles && attachmentFiles.length === 0) {
190
- jobs.set(jobId, { status: 'failed', error: "No attachment files found" });
182
+ isError = true;
183
+ jobs.set(jobId, { status: 'failed', error: "No source images found" });
191
184
  return { key, images: [] };
192
185
  }
193
186
  else {
194
187
  if (STUB_MODE) {
195
188
  yield new Promise((resolve) => setTimeout(resolve, Math.floor(Math.random() * 20000) + 1000));
196
- images = `https://pic.re/image`;
189
+ const fakeError = Math.random() < 0.5; // 50% chance of error
190
+ if (!fakeError) {
191
+ images = `https://pic.re/image`;
192
+ }
193
+ else {
194
+ isError = true;
195
+ jobs.set(jobId, { status: 'failed', error: 'AI provider refused to generate image' });
196
+ }
197
197
  }
198
198
  else {
199
199
  let generationAdapter;
@@ -259,7 +259,8 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
259
259
  }
260
260
  const images = yield Promise.all((new Array(this.options.generateImages[fieldName].countToGenerate)).fill(0).map(() => __awaiter(this, void 0, void 0, function* () {
261
261
  if (this.options.attachFiles && attachmentFiles.length === 0) {
262
- jobs.set(jobId, { status: 'failed', error: "No attachment files found" });
262
+ isError = true;
263
+ jobs.set(jobId, { status: 'failed', error: "No source images found" });
263
264
  return null;
264
265
  }
265
266
  if (STUB_MODE) {
@@ -443,6 +444,52 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
443
444
  `);
444
445
  }
445
446
  }
447
+ if (this.options.fillFieldsFromImages || this.options.fillPlainFields || this.options.generateImages) {
448
+ let matches = [];
449
+ const regex = /{{(.*?)}}/g;
450
+ if (this.options.fillFieldsFromImages) {
451
+ for (const [key, value] of Object.entries((this.options.fillFieldsFromImages))) {
452
+ const template = value;
453
+ const templateMatches = template.match(regex);
454
+ if (templateMatches) {
455
+ matches.push(...templateMatches);
456
+ }
457
+ }
458
+ }
459
+ if (this.options.fillPlainFields) {
460
+ for (const [key, value] of Object.entries((this.options.fillPlainFields))) {
461
+ const template = value;
462
+ const templateMatches = template.match(regex);
463
+ if (templateMatches) {
464
+ matches.push(...templateMatches);
465
+ }
466
+ }
467
+ }
468
+ if (this.options.generateImages) {
469
+ for (const [key, value] of Object.entries((this.options.generateImages))) {
470
+ const template = value.prompt;
471
+ const templateMatches = template.match(regex);
472
+ if (templateMatches) {
473
+ matches.push(...templateMatches);
474
+ }
475
+ }
476
+ }
477
+ if (matches) {
478
+ matches.forEach((match) => {
479
+ const field = match.replace(/{{|}}/g, '').trim();
480
+ if (!resourceConfig.columns.find((column) => column.name === field)) {
481
+ const similar = suggestIfTypo(resourceConfig.columns.map((column) => column.name), field);
482
+ throw new Error(`Field "${field}" specified in generationPrompt not found in resource "${resourceConfig.label}". ${similar ? `Did you mean "${similar}"?` : ''}`);
483
+ }
484
+ else {
485
+ let column = resourceConfig.columns.find((column) => column.name === field);
486
+ if (column.backendOnly === true) {
487
+ throw new Error(`Field "${field}" specified in generationPrompt is marked as backendOnly in resource "${resourceConfig.label}". Please remove backendOnly or choose another field.`);
488
+ }
489
+ }
490
+ });
491
+ }
492
+ }
446
493
  }
447
494
  }
448
495
  instanceUniqueRepresentation(pluginOptions) {
@@ -594,33 +641,36 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
594
641
  jobs.set(jobId, { status: "in_progress" });
595
642
  if (!actionType) {
596
643
  jobs.set(jobId, { status: "failed", error: "Missing action type" });
597
- return { error: "Missing action type" };
644
+ //return { error: "Missing action type" };
598
645
  }
599
- if (!recordId) {
646
+ else if (!recordId) {
600
647
  jobs.set(jobId, { status: "failed", error: "Missing record id" });
601
- return { error: "Missing record id" };
602
- }
603
- switch (actionType) {
604
- case 'generate_images':
605
- this.initialImageGenerate(jobId, recordId, adminUser, headers);
606
- break;
607
- case 'analyze_no_images':
608
- this.analyzeNoImages(jobId, recordId, adminUser, headers);
609
- break;
610
- case 'analyze':
611
- this.analyze_image(jobId, recordId, adminUser, headers);
612
- break;
613
- case 'regenerate_images':
614
- if (!body.prompt || !body.fieldName) {
615
- jobs.set(jobId, { status: "failed", error: "Missing prompt or field name" });
648
+ //return { error: "Missing record id" };
649
+ }
650
+ else {
651
+ switch (actionType) {
652
+ case 'generate_images':
653
+ this.initialImageGenerate(jobId, recordId, adminUser, headers);
616
654
  break;
617
- }
618
- this.regenerateImage(jobId, recordId, body.fieldName, body.prompt, adminUser, headers);
619
- break;
620
- default:
621
- jobs.set(jobId, { status: "failed", error: "Unknown action type" });
655
+ case 'analyze_no_images':
656
+ this.analyzeNoImages(jobId, recordId, adminUser, headers);
657
+ break;
658
+ case 'analyze':
659
+ this.analyze_image(jobId, recordId, adminUser, headers);
660
+ break;
661
+ case 'regenerate_images':
662
+ if (!body.prompt || !body.fieldName) {
663
+ jobs.set(jobId, { status: "failed", error: "Missing prompt or field name" });
664
+ break;
665
+ }
666
+ this.regenerateImage(jobId, recordId, body.fieldName, body.prompt, adminUser, headers);
667
+ break;
668
+ default:
669
+ jobs.set(jobId, { status: "failed", error: "Unknown action type" });
670
+ }
622
671
  }
623
672
  setTimeout(() => jobs.delete(jobId), 1800000);
673
+ setTimeout(() => jobs.set(jobId, { status: "failed", error: "Job timed out" }), 180000);
624
674
  return { ok: true, jobId };
625
675
  })
626
676
  });
package/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { AdminForthPlugin, Filters } from "adminforth";
2
2
  import type { IAdminForth, IHttpServer, AdminForthComponentDeclaration, AdminForthResource } from "adminforth";
3
+ import { suggestIfTypo } from "adminforth";
3
4
  import type { PluginOptions } from './types.js';
4
5
  import Handlebars from 'handlebars';
5
6
  import { RateLimiter } from "adminforth";
@@ -70,12 +71,6 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
70
71
  private async analyze_image(jobId: string, recordId: string, adminUser: any, headers: Record<string, string | string[] | undefined>) {
71
72
  const selectedId = recordId;
72
73
  let isError = false;
73
- // if (typeof(this.options.rateLimits?.fillFieldsFromImages) === 'string'){
74
- // if (this.checkRateLimit("fillFieldsFromImages" ,this.options.rateLimits.fillFieldsFromImages, headers)) {
75
- // jobs.set(jobId, { status: 'failed', error: "Rate limit exceeded" });
76
- // return { error: "Rate limit exceeded" };
77
- // }
78
- // }
79
74
  // Fetch the record using the provided ID
80
75
  const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
81
76
  const record = await this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(primaryKeyColumn.name, selectedId)] );
@@ -84,7 +79,14 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
84
79
  const attachmentFiles = await this.options.attachFiles({ record: record });
85
80
  if (STUB_MODE) {
86
81
  await new Promise((resolve) => setTimeout(resolve, Math.floor(Math.random() * 8000) + 1000));
87
- jobs.set(jobId, { status: 'completed', result: {} });
82
+ const fakeError = Math.random() < 0.5; // 50% chance of error
83
+ if (attachmentFiles.length === 0) {
84
+ jobs.set(jobId, { status: 'failed', error: 'No source images found' });
85
+ } else if (!fakeError) {
86
+ jobs.set(jobId, { status: 'completed', result: {} });
87
+ } else {
88
+ jobs.set(jobId, { status: 'failed', error: 'AI provider refused to analyze images' });
89
+ }
88
90
  return {};
89
91
  } else if (attachmentFiles.length !== 0) {
90
92
  //create prompt for OpenAI
@@ -122,8 +124,8 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
122
124
  return { ok: true };
123
125
  }
124
126
  } else {
125
- jobs.set(jobId, { status: 'failed', error: "No attachment files found" });
126
- return { ok: false, error: "No attachment files found" };
127
+ jobs.set(jobId, { status: 'failed', error: "No source images found" });
128
+ return { ok: false, error: "No source images found" };
127
129
  }
128
130
 
129
131
  }
@@ -131,12 +133,6 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
131
133
  private async analyzeNoImages(jobId: string, recordId: string, adminUser: any, headers: Record<string, string | string[] | undefined>) {
132
134
  const selectedId = recordId;
133
135
  let isError = false;
134
- // if (typeof(this.options.rateLimits?.fillPlainFields) === 'string'){
135
- // if (this.checkRateLimit("fillPlainFields", this.options.rateLimits.fillPlainFields, headers)) {
136
- // jobs.set(jobId, { status: 'failed', error: "Rate limit exceeded" });
137
- // return { error: "Rate limit exceeded" };
138
- // }
139
- // }
140
136
  if (STUB_MODE) {
141
137
  await new Promise((resolve) => setTimeout(resolve, Math.floor(Math.random() * 20000) + 1000));
142
138
  jobs.set(jobId, { status: 'completed', result: {} });
@@ -177,12 +173,6 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
177
173
  private async initialImageGenerate(jobId: string, recordId: string, adminUser: any, headers: Record<string, string | string[] | undefined>) {
178
174
  const selectedId = recordId;
179
175
  let isError = false;
180
- // if (typeof(this.options.rateLimits?.generateImages) === 'string'){
181
- // if (this.checkRateLimit("generateImages", this.options.rateLimits.generateImages, headers)) {
182
- // jobs.set(jobId, { status: 'failed', error: "Rate limit exceeded" });
183
- // return { error: "Rate limit exceeded" };
184
- // }
185
- // }
186
176
  const start = +new Date();
187
177
  const record = await this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(this.resourceConfig.columns.find(c => c.primaryKey)?.name, selectedId)]);
188
178
  let attachmentFiles
@@ -195,12 +185,19 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
195
185
  const prompt = this.compileGenerationFieldTemplates(record)[key];
196
186
  let images;
197
187
  if (this.options.attachFiles && attachmentFiles.length === 0) {
198
- jobs.set(jobId, { status: 'failed', error: "No attachment files found" });
188
+ isError = true;
189
+ jobs.set(jobId, { status: 'failed', error: "No source images found" });
199
190
  return { key, images: [] };
200
191
  } else {
201
192
  if (STUB_MODE) {
202
193
  await new Promise((resolve) => setTimeout(resolve, Math.floor(Math.random() * 20000) + 1000));
203
- images = `https://pic.re/image`;
194
+ const fakeError = Math.random() < 0.5; // 50% chance of error
195
+ if (!fakeError) {
196
+ images = `https://pic.re/image`;
197
+ } else {
198
+ isError = true;
199
+ jobs.set(jobId, { status: 'failed', error: 'AI provider refused to generate image' });
200
+ }
204
201
  } else {
205
202
  let generationAdapter;
206
203
  if (this.options.generateImages[key].adapter) {
@@ -266,7 +263,8 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
266
263
  const images = await Promise.all(
267
264
  (new Array(this.options.generateImages[fieldName].countToGenerate)).fill(0).map(async () => {
268
265
  if (this.options.attachFiles && attachmentFiles.length === 0) {
269
- jobs.set(jobId, { status: 'failed', error: "No attachment files found" });
266
+ isError = true;
267
+ jobs.set(jobId, { status: 'failed', error: "No source images found" });
270
268
  return null;
271
269
  }
272
270
  if (STUB_MODE) {
@@ -464,6 +462,53 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
464
462
  `);
465
463
  }
466
464
  }
465
+ if (this.options.fillFieldsFromImages || this.options.fillPlainFields || this.options.generateImages) {
466
+ let matches: string[] = [];
467
+ const regex = /{{(.*?)}}/g;
468
+
469
+ if (this.options.fillFieldsFromImages) {
470
+ for (const [key, value] of Object.entries((this.options.fillFieldsFromImages ))) {
471
+ const template = value;
472
+ const templateMatches = template.match(regex);
473
+ if (templateMatches) {
474
+ matches.push(...templateMatches);
475
+ }
476
+ }
477
+ }
478
+ if (this.options.fillPlainFields) {
479
+ for (const [key, value] of Object.entries((this.options.fillPlainFields))) {
480
+ const template = value;
481
+ const templateMatches = template.match(regex);
482
+ if (templateMatches) {
483
+ matches.push(...templateMatches);
484
+ }
485
+ }
486
+ }
487
+ if (this.options.generateImages) {
488
+ for (const [key, value] of Object.entries((this.options.generateImages ))) {
489
+ const template = value.prompt;
490
+ const templateMatches = template.match(regex);
491
+ if (templateMatches) {
492
+ matches.push(...templateMatches);
493
+ }
494
+ }
495
+ }
496
+
497
+ if (matches) {
498
+ matches.forEach((match) => {
499
+ const field = match.replace(/{{|}}/g, '').trim();
500
+ if (!resourceConfig.columns.find((column: any) => column.name === field)) {
501
+ const similar = suggestIfTypo(resourceConfig.columns.map((column: any) => column.name), field);
502
+ throw new Error(`Field "${field}" specified in generationPrompt not found in resource "${resourceConfig.label}". ${similar ? `Did you mean "${similar}"?` : ''}`);
503
+ } else {
504
+ let column = resourceConfig.columns.find((column: any) => column.name === field);
505
+ if (column.backendOnly === true) {
506
+ throw new Error(`Field "${field}" specified in generationPrompt is marked as backendOnly in resource "${resourceConfig.label}". Please remove backendOnly or choose another field.`);
507
+ }
508
+ }
509
+ });
510
+ }
511
+ }
467
512
  }
468
513
  }
469
514
 
@@ -633,34 +678,35 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
633
678
 
634
679
  if (!actionType) {
635
680
  jobs.set(jobId, { status: "failed", error: "Missing action type" });
636
- return { error: "Missing action type" };
681
+ //return { error: "Missing action type" };
637
682
  }
638
- if (!recordId) {
683
+ else if (!recordId) {
639
684
  jobs.set(jobId, { status: "failed", error: "Missing record id" });
640
- return { error: "Missing record id" };
641
- }
642
-
643
- switch(actionType) {
644
- case 'generate_images':
645
- this.initialImageGenerate(jobId, recordId, adminUser, headers);
646
- break;
647
- case 'analyze_no_images':
648
- this.analyzeNoImages(jobId, recordId, adminUser, headers);
649
- break;
650
- case 'analyze':
651
- this.analyze_image(jobId, recordId, adminUser, headers);
652
- break;
653
- case 'regenerate_images':
654
- if (!body.prompt || !body.fieldName) {
655
- jobs.set(jobId, { status: "failed", error: "Missing prompt or field name" });
656
- break;
657
- }
658
- this.regenerateImage(jobId, recordId, body.fieldName, body.prompt, adminUser, headers);
659
- break;
660
- default:
661
- jobs.set(jobId, { status: "failed", error: "Unknown action type" });
685
+ //return { error: "Missing record id" };
686
+ } else {
687
+ switch(actionType) {
688
+ case 'generate_images':
689
+ this.initialImageGenerate(jobId, recordId, adminUser, headers);
690
+ break;
691
+ case 'analyze_no_images':
692
+ this.analyzeNoImages(jobId, recordId, adminUser, headers);
693
+ break;
694
+ case 'analyze':
695
+ this.analyze_image(jobId, recordId, adminUser, headers);
696
+ break;
697
+ case 'regenerate_images':
698
+ if (!body.prompt || !body.fieldName) {
699
+ jobs.set(jobId, { status: "failed", error: "Missing prompt or field name" });
700
+ break;
701
+ }
702
+ this.regenerateImage(jobId, recordId, body.fieldName, body.prompt, adminUser, headers);
703
+ break;
704
+ default:
705
+ jobs.set(jobId, { status: "failed", error: "Unknown action type" });
706
+ }
662
707
  }
663
708
  setTimeout(() => jobs.delete(jobId), 1_800_000);
709
+ setTimeout(() => jobs.set(jobId, { status: "failed", error: "Job timed out" }), 180_000);
664
710
  return { ok: true, jobId };
665
711
  }
666
712
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/bulk-ai-flow",
3
- "version": "1.10.3",
3
+ "version": "1.12.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },