Skip to content

Commit b3a80cf

Browse files
committed
feat: send generation requests in parallel from frontend
1 parent 076ad08 commit b3a80cf

File tree

2 files changed

+107
-96
lines changed

2 files changed

+107
-96
lines changed

custom/VisionAction.vue

Lines changed: 56 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,13 @@ const openDialog = async () => {
109109
}
110110
isFetchingRecords.value = true;
111111
const tasks = [];
112+
if (props.meta.isImageGeneration) {
113+
tasks.push(runAiAction({
114+
endpoint: 'initial_image_generate',
115+
actionType: 'generate_images',
116+
responseFlag: isAiResponseReceivedImage,
117+
}));
118+
}
112119
if (props.meta.isFieldsForAnalizeFromImages) {
113120
tasks.push(runAiAction({
114121
endpoint: 'analyze',
@@ -123,13 +130,6 @@ const openDialog = async () => {
123130
responseFlag: isAiResponseReceivedAnalize,
124131
}));
125132
}
126-
if (props.meta.isImageGeneration) {
127-
tasks.push(runAiAction({
128-
endpoint: 'initial_image_generate',
129-
actionType: 'generate_images',
130-
responseFlag: isAiResponseReceivedImage,
131-
}));
132-
}
133133
await Promise.all(tasks);
134134
135135
if (props.meta.isImageGeneration) {
@@ -140,7 +140,7 @@ const openDialog = async () => {
140140
}
141141
142142
watch(selected, (val) => {
143-
//console.log('Selected changed:', val);
143+
console.log('Selected changed:', val);
144144
checkedCount.value = val.filter(item => item.isChecked === true).length;
145145
}, { deep: true });
146146
@@ -398,65 +398,72 @@ async function runAiAction({
398398
responseFlag: Ref<boolean[]>;
399399
updateOnSuccess?: boolean;
400400
}) {
401-
let res: any;
402-
let error: any = null;
401+
const results: any[] = new Array(props.checkboxes.length);
402+
let hasError = false;
403+
let errorMessage = '';
404+
405+
responseFlag.value = props.checkboxes.map(() => false);
403406
404-
try {
405-
responseFlag.value = props.checkboxes.map(() => false);
407+
const tasks = props.checkboxes.map(async (checkbox, i) => {
408+
try {
409+
const res = await callAdminForthApi({
410+
path: `/plugin/${props.meta.pluginInstanceId}/${endpoint}`,
411+
method: 'POST',
412+
body: {
413+
selectedId: checkbox,
414+
},
415+
});
406416
407-
res = await callAdminForthApi({
408-
path: `/plugin/${props.meta.pluginInstanceId}/${endpoint}`,
409-
method: 'POST',
410-
body: {
411-
selectedIds: props.checkboxes,
412-
},
413-
});
417+
if (res?.error) {
418+
throw new Error(res.error);
419+
}
420+
421+
if (!res) {
422+
throw new Error(`${actionType} request returned empty response.`);
423+
}
414424
415-
if (actionType !== 'analyze_no_images' || !props.meta.isFieldsForAnalizeFromImages) {
416-
responseFlag.value = props.checkboxes.map(() => true);
425+
results[i] = res;
426+
427+
if (actionType !== 'analyze_no_images' || !props.meta.isFieldsForAnalizeFromImages) {
428+
responseFlag.value[i] = true;
429+
}
430+
if (res.result) {
431+
const pk = selected.value[i]?.[primaryKey];
432+
if (pk) {
433+
selected.value[i] = {
434+
...selected.value[i],
435+
...res.result,
436+
isChecked: true,
437+
[primaryKey]: pk,
438+
};
439+
}
440+
}
441+
return { success: true, index: i, data: res };
442+
} catch (e) {
443+
console.error(`Error during ${actionType} for item ${i}:`, e);
444+
hasError = true;
445+
errorMessage = `Failed to ${actionType.replace('_', ' ')}. Please, try to re-run the action.`;
446+
return { success: false, index: i, error: e };
417447
}
418-
} catch (e) {
419-
console.error(`Error during ${actionType}:`, e);
420-
error = `Failed to ${actionType.replace('_', ' ')}. Please, try to re-run the action.`;
421-
}
448+
});
422449
423-
if (res?.error) {
424-
error = res.error;
425-
}
426-
if (!res && !error) {
427-
error = `Error: ${actionType} request returned empty response.`;
428-
}
450+
await Promise.all(tasks);
429451
430-
if (error) {
452+
if (hasError) {
431453
adminforth.alert({
432-
message: error,
454+
message: errorMessage,
433455
variant: 'danger',
434456
timeout: 'unlimited',
435457
});
436458
isError.value = true;
437459
if (actionType === 'generate_images') {
438460
isImageGenerationError.value = true;
439461
}
440-
errorMessage.value = error;
462+
this.errorMessage.value = errorMessage;
441463
return;
442464
}
443-
444-
if (updateOnSuccess) {
445-
res.result.forEach((item: any, idx: number) => {
446-
const pk = selected.value[idx]?.[primaryKey];
447-
if (pk) {
448-
selected.value[idx] = {
449-
...selected.value[idx],
450-
...item,
451-
isChecked: true,
452-
[primaryKey]: pk,
453-
};
454-
}
455-
});
456-
}
457465
}
458466
459-
460467
async function uploadImage(imgBlob, id, fieldName) {
461468
const file = new File([imgBlob], `generated_${fieldName}_${id}.${imgBlob.type.split('/').pop()}`, { type: imgBlob.type });
462469
const { name, size, type } = file;

index.ts

Lines changed: 51 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { PluginOptions } from './types.js';
44
import Handlebars from 'handlebars';
55
import { RateLimiter } from "adminforth";
66

7-
7+
const STUB_MODE = true;
88
export default class BulkAiFlowPlugin extends AdminForthPlugin {
99
options: PluginOptions;
1010
uploadPlugin: AdminForthPlugin;
@@ -140,6 +140,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
140140
isFieldsForAnalizePlain: this.options.fillPlainFields ? Object.keys(this.options.fillPlainFields).length > 0 : false,
141141
isImageGeneration: this.options.generateImages ? Object.keys(this.options.generateImages).length > 0 : false,
142142
isAttachFiles: this.options.attachFiles ? true : false,
143+
disabledWhenNoCheckboxes: true,
143144
}
144145
}
145146

@@ -226,20 +227,23 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
226227
method: 'POST',
227228
path: `/plugin/${this.pluginInstanceId}/analyze`,
228229
handler: async ({ body, adminUser, headers }) => {
229-
const selectedIds = body.selectedIds || [];
230+
const selectedId = body.selectedId || [];
230231
if (typeof(this.options.rateLimits?.fillFieldsFromImages) === 'string'){
231232
if (this.checkRateLimit("fillFieldsFromImages" ,this.options.rateLimits.fillFieldsFromImages, headers)) {
232233
return { error: "Rate limit exceeded" };
233234
}
234235
}
235-
const tasks = selectedIds.map(async (ID) => {
236+
//const tasks = selectedIds.map(async (ID) => {
236237
// Fetch the record using the provided ID
237238
const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
238-
const record = await this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(primaryKeyColumn.name, ID)] );
239+
const record = await this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(primaryKeyColumn.name, selectedId)] );
239240

240241
//recieve image URLs to analyze
241242
const attachmentFiles = await this.options.attachFiles({ record: record });
242-
if (attachmentFiles.length !== 0) {
243+
if (STUB_MODE) {
244+
await new Promise((resolve) => setTimeout(resolve, Math.floor(Math.random() * 8000) + 1000));
245+
return {};
246+
} else if (attachmentFiles.length !== 0) {
243247
//create prompt for OpenAI
244248
const compiledOutputFields = this.compileOutputFieldsTemplates(record);
245249
const prompt = `Analyze the following image(s) and return a single JSON in format like: {'param1': 'value1', 'param2': 'value2'}.
@@ -263,53 +267,52 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
263267

264268
//parse response and update record
265269
const resData = JSON.parse(textOutput);
266-
267-
return resData;
270+
const result = resData;
271+
return { result };
268272
};
269-
});
270-
271-
const result = await Promise.all(tasks);
272-
273-
return { result };
274273
}
275274
});
276275

277276
server.endpoint({
278277
method: 'POST',
279278
path: `/plugin/${this.pluginInstanceId}/analyze_no_images`,
280279
handler: async ({ body, adminUser, headers }) => {
281-
const selectedIds = body.selectedIds || [];
280+
const selectedIds = body.selectedId || [];
282281
if (typeof(this.options.rateLimits?.fillPlainFields) === 'string'){
283282
if (this.checkRateLimit("fillPlainFields", this.options.rateLimits.fillPlainFields, headers)) {
284283
return { error: "Rate limit exceeded" };
285284
}
286285
}
287-
const tasks = selectedIds.map(async (ID) => {
288-
const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
289-
const record = await this.adminforth.resource(this.resourceConfig.resourceId).get( [Filters.EQ(primaryKeyColumn.name, ID)] );
290-
291-
const compiledOutputFields = this.compileOutputFieldsTemplatesNoImage(record);
292-
const prompt = `Analyze the following fields and return a single JSON in format like: {'param1': 'value1', 'param2': 'value2'}.
293-
Do NOT return array of objects. Do NOT include any Markdown, code blocks, explanations, or extra text. Only return valid JSON.
294-
Each object must contain the following fields: ${JSON.stringify(compiledOutputFields)} Use the exact field names.
295-
If it's number field - return only number.`;
296-
//send prompt to OpenAI and get response
297-
const numberOfTokens = this.options.fillPlainFieldsMaxTokens ? this.options.fillPlainFieldsMaxTokens : 1000;
298-
const { content: chatResponse } = await this.options.textCompleteAdapter.complete(prompt, [], numberOfTokens);
299-
300-
const resp: any = (chatResponse as any).response;
301-
const topLevelError = (chatResponse as any).error;
302-
if (topLevelError || resp?.error) {
303-
throw new Error(`ERROR: ${JSON.stringify(topLevelError || resp?.error)}`);
304-
}
305-
const resData = JSON.parse(chatResponse);
286+
//const tasks = selectedIds.map(async (ID) => {
287+
if (STUB_MODE) {
288+
await new Promise((resolve) => setTimeout(resolve, Math.floor(Math.random() * 8000) + 1000));
289+
return {};
290+
} else {
291+
const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
292+
const record = await this.adminforth.resource(this.resourceConfig.resourceId).get( [Filters.EQ(primaryKeyColumn.name, selectedId)] );
306293

307-
return resData;
308-
});
294+
const compiledOutputFields = this.compileOutputFieldsTemplatesNoImage(record);
295+
const prompt = `Analyze the following fields and return a single JSON in format like: {'param1': 'value1', 'param2': 'value2'}.
296+
Do NOT return array of objects. Do NOT include any Markdown, code blocks, explanations, or extra text. Only return valid JSON.
297+
Each object must contain the following fields: ${JSON.stringify(compiledOutputFields)} Use the exact field names.
298+
If it's number field - return only number.`;
299+
//send prompt to OpenAI and get response
300+
const numberOfTokens = this.options.fillPlainFieldsMaxTokens ? this.options.fillPlainFieldsMaxTokens : 1000;
301+
const { content: chatResponse } = await this.options.textCompleteAdapter.complete(prompt, [], numberOfTokens);
309302

310-
const result = await Promise.all(tasks);
303+
const resp: any = (chatResponse as any).response;
304+
const topLevelError = (chatResponse as any).error;
305+
if (topLevelError || resp?.error) {
306+
throw new Error(`ERROR: ${JSON.stringify(topLevelError || resp?.error)}`);
307+
}
308+
const resData = JSON.parse(chatResponse);
309+
310+
//return resData;
311+
const result = resData;
311312

312-
return { result };
313+
return { result };
314+
}
315+
// });
313316
}
314317
});
315318
server.endpoint({
@@ -443,7 +446,6 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
443446
return { error: "Rate limit exceeded" };
444447
}
445448
const start = +new Date();
446-
const STUB_MODE = false;
447449
const record = await this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(this.resourceConfig.columns.find(c => c.primaryKey)?.name, Id)]);
448450
let attachmentFiles
449451
if(!this.options.attachFiles){
@@ -458,7 +460,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
458460
}
459461
if (STUB_MODE) {
460462
await new Promise((resolve) => setTimeout(resolve, 2000));
461-
return `https://picsum.photos/200/300?random=${Math.floor(Math.random() * 1000)}`;
463+
return `https://pic.re/image`;
462464
}
463465

464466
let generationAdapter;
@@ -483,20 +485,21 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
483485
return { images };
484486
}
485487
});
488+
489+
486490
server.endpoint({
487491
method: 'POST',
488492
path: `/plugin/${this.pluginInstanceId}/initial_image_generate`,
489493
handler: async ({ body, headers }) => {
490-
const selectedIds = body.selectedIds || [];
491-
const STUB_MODE = false;
494+
const selectedId = body.selectedId || [];
492495
if (typeof(this.options.rateLimits?.generateImages) === 'string'){
493496
if (this.checkRateLimit("generateImages", this.options.rateLimits.generateImages, headers)) {
494497
return { error: "Rate limit exceeded" };
495498
}
496499
}
497500
const start = +new Date();
498-
const tasks = selectedIds.map(async (ID) => {
499-
const record = await this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(this.resourceConfig.columns.find(c => c.primaryKey)?.name, ID)]);
501+
// const tasks = selectedIds.map(async (ID) => {
502+
const record = await this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ(this.resourceConfig.columns.find(c => c.primaryKey)?.name, selectedId)]);
500503
let attachmentFiles
501504
if(!this.options.attachFiles){
502505
attachmentFiles = [];
@@ -510,8 +513,8 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
510513
return { key, images: [] };
511514
} else {
512515
if (STUB_MODE) {
513-
await new Promise((resolve) => setTimeout(resolve, 2000));
514-
images = `https://picsum.photos/200/300?random=${Math.floor(Math.random() * 1000)}`;
516+
await new Promise((resolve) => setTimeout(resolve, Math.floor(Math.random() * 8000) + 1000));
517+
images = `https://pic.re/image`;
515518
} else {
516519
let generationAdapter;
517520
if (this.options.generateImages[key].adapter) {
@@ -540,16 +543,17 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
540543
recordResult[key] = images;
541544
});
542545

543-
return recordResult;
544-
});
545-
const result = await Promise.all(tasks);
546+
// });
547+
const result = recordResult;
546548

547549
this.totalCalls++;
548550
this.totalDuration += (+new Date() - start) / 1000;
549551

550552
return { result };
551553
}
552554
});
555+
556+
553557
server.endpoint({
554558
method: 'POST',
555559
path: `/plugin/${this.pluginInstanceId}/get_generation_prompts`,

0 commit comments

Comments
 (0)