From d340b6611e9c79c3f711c3af6f5ec1ae2905fdf2 Mon Sep 17 00:00:00 2001 From: yaroslav8765 Date: Mon, 25 Aug 2025 11:45:04 +0300 Subject: [PATCH 01/26] feat: implement image generation functionality at the API level --- custom/visionAction.vue | 13 +++++- index.ts | 87 ++++++++++++++++++++++++++++++++++++++++- types.ts | 44 ++++++++++++++++++++- 3 files changed, 140 insertions(+), 4 deletions(-) diff --git a/custom/visionAction.vue b/custom/visionAction.vue index 350e47d..070bca4 100644 --- a/custom/visionAction.vue +++ b/custom/visionAction.vue @@ -81,7 +81,8 @@ const openDialog = async () => { customFieldNames.value = tableHeaders.value.slice(3).map(h => h.fieldName); setSelected(); - analyzeFields(); + //analyzeFields(); + generateImages(); } // watch(selected, (val) => { @@ -247,4 +248,14 @@ async function saveData() { console.error('Error saving data:', res); } } + +async function generateImages() { + const res = await callAdminForthApi({ + path: `/plugin/${props.meta.pluginInstanceId}/generate_images`, + method: 'POST', + body: { + selectedIds: props.checkboxes, + }, + }); +} \ No newline at end of file diff --git a/index.ts b/index.ts index e8e0b3e..7000768 100644 --- a/index.ts +++ b/index.ts @@ -3,6 +3,7 @@ import type { IAdminForth, IHttpServer, AdminForthResourcePages, AdminForthResou import type { PluginOptions } from './types.js'; import { json } from "stream/consumers"; import Handlebars from 'handlebars'; +import { RateLimiter } from "adminforth"; export default class BulkAiFlowPlugin extends AdminForthPlugin { @@ -27,6 +28,20 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin { } return compiled; } + + // Compile Handlebars templates in generateImage using record fields as context + private compileImageGenerationFieldsTemplates(record: any): Record { + const compiled: Record = {}; + for (const [key, templateStr] of Object.entries(this.options.generateImages.fields)) { + try { + const tpl = Handlebars.compile(String(templateStr)); + compiled[key] = tpl(record); + } catch { + compiled[key] = String(templateStr); + } + } + return compiled; + } async modifyResourceConfig(adminforth: IAdminForth, resourceConfig: AdminForthResource) { super.modifyResourceConfig(adminforth, resourceConfig); @@ -51,7 +66,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin { //check if Upload plugin is installed on all attachment fields if (this.options.generateImages) { - for (const [key, value] of Object.entries(this.options.generateImages)) { + for (const [key, value] of Object.entries(this.options.generateImages.fields)) { const column = columns.find(c => c.name.toLowerCase() === key.toLowerCase()); if (!column) { throw new Error(`⚠️ No column found for key "${key}"`); @@ -201,6 +216,76 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin { return { ok: true }; } }); + + server.endpoint({ + method: 'POST', + path: `/plugin/${this.pluginInstanceId}/generate_images`, + handler: async ({ body, headers }) => { + const selectedIds = body.selectedIds || []; + const tasks = selectedIds.map(async (ID) => { + if (this.options.generateImages.rateLimit?.limit) { + // rate limit + const { error } = RateLimiter.checkRateLimit( + this.pluginInstanceId, + this.options.generateImages.rateLimit?.limit, + this.adminforth.auth.getClientIp(headers), + ); + if (error) { + return { error: this.options.generateImages.rateLimit.errorMessage }; + } + } + + let error: string | undefined = undefined; + + const STUB_MODE = true; + + const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey); + const record = await this.adminforth.resource(this.resourceConfig.resourceId).get( [Filters.EQ(primaryKeyColumn.name, ID)] ); + const compiledOutputFields = this.compileImageGenerationFieldsTemplates(record); + const attachmentFiles = await this.options.attachFiles({ record: record }); + + for (const [key, value] of Object.entries(compiledOutputFields)) { + const prompt = `${value}`; + const images = await Promise.all( + (new Array(this.options.generateImages.countToGenerate)).fill(0).map(async () => { + if (STUB_MODE) { + await new Promise((resolve) => setTimeout(resolve, 2000)); + return `https://picsum.photos/200/300?random=${Math.floor(Math.random() * 1000)}`; + } + const start = +new Date(); + const resp = await this.options.generateImages.adapter.generate( + { + prompt, + inputFiles: attachmentFiles, + n: 1, + size: this.options.generateImages.outputSize, + } + ) + + if (resp.error) { + console.error('Error generating image', resp.error); + error = resp.error; + return; + } + + // this.totalCalls++; + // this.totalDuration += (+new Date() - start) / 1000; + + return resp.imageURLs[0]; + + }) + ); + console.log('Generated images', images); + return { error, images }; + } + }); + + + + const result = await Promise.all(tasks); + return { result }; + } + }); } } diff --git a/types.ts b/types.ts index cbb06e8..66ea35d 100644 --- a/types.ts +++ b/types.ts @@ -1,12 +1,52 @@ -import { ImageVisionAdapter, AdminUser, IAdminForth, StorageAdapter } from "adminforth"; +import { ImageVisionAdapter, AdminUser, IAdminForth, StorageAdapter, ImageGenerationAdapter } from "adminforth"; export interface PluginOptions { actionName: string, visionAdapter: ImageVisionAdapter, fillFieldsFromImages?: Record, // can analyze what is on image and fill fields, typical tasks "find dominant color", "describe what is on image", "clasify to one enum item, e.g. what is on image dog/cat/plant" - generateImages?: Record, // can generate from images or just from another fields, e.g. "remove text from images", "improve image quality", "turn image into ghibli style" attachFiles?: ({ record }: { record: any, }) => string[] | Promise, + + generateImages?: { // can generate from images or just from another fields, e.g. "remove text from images", "improve image quality", "turn image into ghibli style" + fields: Record + + adapter: ImageGenerationAdapter, + + /** + * The size of the generated image. + */ + outputSize?: string, + + /** + * Fields for conetext which will be used to generate the image. + * If specified, the plugin will use fields from the record to provide additional context to the AI model. + */ + fieldsForContext?: string[], + + /** + * The number of images to generate + * in one request + */ + countToGenerate: number, + + /** + * Since AI generation can be expensive, we can limit the number of requests per IP. + */ + rateLimit?: { + + /** + * E.g. 5/1d - 5 requests per day + * 3/1h - 3 requests per hour + */ + limit: string, + + /** + * !Not used now + * Message shown to user when rate limit is reached + */ + errorMessage: string, + }, + } } From 573be48ccffc70bbba0ee90ffde15a5747cc3175 Mon Sep 17 00:00:00 2001 From: yaroslav8765 Date: Mon, 25 Aug 2025 12:10:50 +0300 Subject: [PATCH 02/26] fix: update generateImages handling in visionAction and BulkAiFlowPlugin to manage AI response state and output image fields --- custom/visionAction.vue | 5 ++++- index.ts | 11 +++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/custom/visionAction.vue b/custom/visionAction.vue index 070bca4..cf19a15 100644 --- a/custom/visionAction.vue +++ b/custom/visionAction.vue @@ -146,7 +146,7 @@ function setSelected() { selected.value = records.value.map(() => ({})); records.value.forEach((record, index) => { for (const key in props.meta.outputFields) { - if(isInColumnEnum(key)){ + if (isInColumnEnum(key)) { const colEnum = props.meta.columnEnums.find(c => c.name === key); const object = colEnum.enum.find(item => item.value === record[key]); selected.value[index][key] = object ? record[key] : null; @@ -250,6 +250,7 @@ async function saveData() { } async function generateImages() { + isAiResponseReceived.value = props.checkboxes.map(() => false); const res = await callAdminForthApi({ path: `/plugin/${props.meta.pluginInstanceId}/generate_images`, method: 'POST', @@ -257,5 +258,7 @@ async function generateImages() { selectedIds: props.checkboxes, }, }); + isAiResponseReceived.value = props.checkboxes.map(() => true); + } \ No newline at end of file diff --git a/index.ts b/index.ts index 7000768..a8a4a11 100644 --- a/index.ts +++ b/index.ts @@ -64,6 +64,14 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin { } } + let outputImageFields = []; + if (this.options.generateImages) { + for (const [key, value] of Object.entries(this.options.generateImages.fields)) { + // console.log('Checking image generation field', key, value); + outputImageFields.push(key); + } + } + //check if Upload plugin is installed on all attachment fields if (this.options.generateImages) { for (const [key, value] of Object.entries(this.options.generateImages.fields)) { @@ -90,6 +98,8 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin { } + //const outputFields = this.options.fillFieldsFromImages + (this.options.generateImages?.fields || {}); + //console.log('outputFields', outputFields); const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey); @@ -100,6 +110,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin { outputFields: this.options.fillFieldsFromImages, actionName: this.options.actionName, columnEnums: columnEnums, + outputImageFields: outputImageFields, primaryKey: primaryKeyColumn.name, } } From 8ebf7b6a137745401552fc319593cbf6471b5258 Mon Sep 17 00:00:00 2001 From: yaroslav8765 Date: Mon, 25 Aug 2025 15:54:00 +0300 Subject: [PATCH 03/26] feat: enhance image generation handling in vision components and update plugin options --- custom/visionAction.vue | 53 +++++++++++++++++++++--------- custom/visionTable.vue | 35 +++++++++++++++++++- index.ts | 73 ++++++++++++++++++++--------------------- types.ts | 6 ---- 4 files changed, 106 insertions(+), 61 deletions(-) diff --git a/custom/visionAction.vue b/custom/visionAction.vue index cf19a15..3ecb444 100644 --- a/custom/visionAction.vue +++ b/custom/visionAction.vue @@ -25,6 +25,7 @@ :tableColumnsIndexes="tableColumnsIndexes" :selected="selected" :isAiResponseReceived="isAiResponseReceived" + :isAiResponseReceivedImage="isAiResponseReceivedImage" :primaryKey="primaryKey" />