@adminforth/bulk-ai-flow 1.0.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.
@@ -0,0 +1,254 @@
1
+ <template>
2
+ <div @click="openDialog">
3
+ <p class="">{{ props.meta.actionName }}</p>
4
+ </div>
5
+ <Dialog ref="confirmDialog">
6
+ <div
7
+ class="fixed inset-0 z-50 flex items-center justify-center bg-black/40"
8
+ @click="closeDialog"
9
+ >
10
+ <div
11
+ class="relative max-w-[95vw] max-h-[90vh] bg-white dark:bg-gray-900 rounded-md shadow-2xl overflow-hidden"
12
+ @click.stop
13
+ >
14
+ <div class="flex flex-col items-end justify-evenly gap-4 w-full h-full p-6 overflow-y-auto">
15
+ <VisionTable
16
+ v-if="records && props.checkboxes.length"
17
+ :checkbox="props.checkboxes"
18
+ :records="records"
19
+ :index="0"
20
+ :meta="props.meta"
21
+ :images="images"
22
+ :tableHeaders="tableHeaders"
23
+ :tableColumns="tableColumns"
24
+ :customFieldNames="customFieldNames"
25
+ :tableColumnsIndexes="tableColumnsIndexes"
26
+ :selected="selected"
27
+ :isAiResponseReceived="isAiResponseReceived"
28
+ />
29
+ <Button
30
+ class="w-64"
31
+ @click="saveData"
32
+ >
33
+ {{ props.checkboxes.length > 1 ? 'Save fields' : 'Save field' }}
34
+ </Button>
35
+ </div>
36
+ </div>
37
+ </div>
38
+ </Dialog>
39
+ </template>
40
+
41
+ <script lang="ts" setup>
42
+ import { callAdminForthApi } from '@/utils';
43
+ import { ref, watch } from 'vue'
44
+ import { Dialog, Button } from '@/afcl';
45
+ import VisionTable from './visionTable.vue'
46
+
47
+ const props = defineProps<{
48
+ checkboxes: any,
49
+ meta: any,
50
+ resource: any,
51
+ adminUser: any,
52
+ updateList: {
53
+ type: Function,
54
+ required: true
55
+ },
56
+ clearCheckboxes: {
57
+ type: Function
58
+ }
59
+ }>();
60
+
61
+ const confirmDialog = ref(null);
62
+ const records = ref<any[]>([]);
63
+ const images = ref<any[]>([]);
64
+ const tableHeaders = ref([]);
65
+ const tableColumns = ref([]);
66
+ const tableColumnsIndexes = ref([]);
67
+ const customFieldNames = ref([]);
68
+ const selected = ref<any[]>([]);
69
+ const isAiResponseReceived = ref([]);
70
+
71
+ const openDialog = async () => {
72
+ confirmDialog.value.open();
73
+ await getRecords();
74
+ await getImages();
75
+ tableHeaders.value = generateTableHeaders(props.meta.outputFields);
76
+ const result = generateTableColumns();
77
+ tableColumns.value = result.tableData;
78
+ tableColumnsIndexes.value = result.indexes;
79
+
80
+ customFieldNames.value = tableHeaders.value.slice(3).map(h => h.fieldName);
81
+ setSelected();
82
+ analyzeFields();
83
+ }
84
+
85
+ // watch(selected, (val) => {
86
+ // console.log('Selected changed:', val);
87
+ // }, { deep: true });
88
+
89
+ const closeDialog = () => {
90
+ confirmDialog.value.close();
91
+ isAiResponseReceived.value = [];
92
+ }
93
+
94
+ function formatLabel(str) {
95
+ return str
96
+ .split('_')
97
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
98
+ .join(' ');
99
+ }
100
+
101
+ function generateTableHeaders(outputFields) {
102
+ const headers = [];
103
+
104
+ headers.push({ label: 'Checkboxes', fieldName: 'checkboxes' });
105
+ headers.push({ label: 'Field name', fieldName: 'label' });
106
+ headers.push({ label: 'Source Images', fieldName: 'images' });
107
+
108
+ if (outputFields.length > 0) {
109
+ const sampleField = outputFields[0];
110
+ for (const key in sampleField) {
111
+ headers.push({
112
+ label: formatLabel(key),
113
+ fieldName: key,
114
+ });
115
+ }
116
+ }
117
+ return headers;
118
+ }
119
+
120
+ function generateTableColumns() {
121
+ const fields = [];
122
+ const tableData = [];
123
+ const indexes = [];
124
+ for (const field of tableHeaders.value) {
125
+ fields.push( field.fieldName );
126
+ }
127
+ for (const [index, checkbox] of props.checkboxes.entries()) {
128
+ const record = records.value[index];
129
+ let reqFields: any = {};
130
+ for (const field of fields) {
131
+ reqFields[field] = record[field] || '';
132
+ }
133
+ reqFields.label = record._label;
134
+ reqFields.images = images.value[index];
135
+ reqFields.id = record.id;
136
+ indexes.push({
137
+ id: record.id,
138
+ label: record._label,
139
+ });
140
+ tableData.push(reqFields);
141
+ }
142
+ return { tableData, indexes };
143
+ }
144
+
145
+ function setSelected() {
146
+ selected.value = records.value.map(() => ({}));
147
+
148
+ records.value.forEach((record, index) => {
149
+ props.meta.outputFields.forEach((fieldObj, i) => {
150
+ for (const key in fieldObj) {
151
+ if(isInColumnEnum(key)){
152
+ const colEnum = props.meta.columnEnums.find(c => c.name === key);
153
+ const object = colEnum.enum.find(item => item.value === record[key]);
154
+ selected.value[index][key] = object ? record[key] : null;
155
+ } else {
156
+ selected.value[index][key] = record[key];
157
+ }
158
+ }
159
+ selected.value[index].isChecked = true;
160
+ selected.value[index].id = record.id;
161
+ isAiResponseReceived.value[index] = true;
162
+ });
163
+ });
164
+ }
165
+
166
+ function isInColumnEnum(key: string): boolean {
167
+ const colEnum = props.meta.columnEnums?.find(c => c.name === key);
168
+ if (!colEnum) {
169
+ return false;
170
+ }
171
+ return true;
172
+ }
173
+
174
+ async function getRecords() {
175
+ const res = await callAdminForthApi({
176
+ path: `/plugin/${props.meta.pluginInstanceId}/get_records`,
177
+ method: 'POST',
178
+ body: {
179
+ record: props.checkboxes,
180
+ },
181
+ });
182
+ records.value = res.records;
183
+ }
184
+
185
+ async function getImages() {
186
+ const res = await callAdminForthApi({
187
+ path: `/plugin/${props.meta.pluginInstanceId}/get_images`,
188
+ method: 'POST',
189
+ body: {
190
+ record: records.value,
191
+ },
192
+ });
193
+
194
+ images.value = res.images;
195
+ }
196
+
197
+ function prepareDataForSave() {
198
+ const checkedItems = selected.value
199
+ .filter(item => item.isChecked === true)
200
+ .map(item => {
201
+ const { isChecked, id, ...itemWithoutIsCheckedAndId } = item;
202
+ return itemWithoutIsCheckedAndId;
203
+ });
204
+ const checkedItemsIDs = selected.value
205
+ .filter(item => item.isChecked === true)
206
+ .map(item => item.id);
207
+ return [checkedItemsIDs, checkedItems];
208
+ }
209
+
210
+ async function analyzeFields() {
211
+ isAiResponseReceived.value = props.checkboxes.map(() => false);
212
+
213
+ const res = await callAdminForthApi({
214
+ path: `/plugin/${props.meta.pluginInstanceId}/analyze`,
215
+ method: 'POST',
216
+ body: {
217
+ selectedIds: props.checkboxes,
218
+ },
219
+ });
220
+
221
+ isAiResponseReceived.value = props.checkboxes.map(() => true);
222
+
223
+ selected.value.splice(
224
+ 0,
225
+ selected.value.length,
226
+ ...res.result.map((item, idx) => ({
227
+ ...item,
228
+ isChecked: true,
229
+ id: selected.value[idx]?.id,
230
+ }))
231
+ )
232
+ }
233
+
234
+ async function saveData() {
235
+ const [checkedItemsIDs, reqData] = prepareDataForSave();
236
+
237
+ const res = await callAdminForthApi({
238
+ path: `/plugin/${props.meta.pluginInstanceId}/update_fields`,
239
+ method: 'POST',
240
+ body: {
241
+ selectedIds: checkedItemsIDs,
242
+ fields: reqData,
243
+ },
244
+ });
245
+
246
+ if(res.ok) {
247
+ confirmDialog.value.close();
248
+ props.updateList();
249
+ props.clearCheckboxes();
250
+ } else {
251
+ console.error('Error saving data:', res);
252
+ }
253
+ }
254
+ </script>
@@ -0,0 +1,140 @@
1
+ <template>
2
+ <div>
3
+ <Table
4
+ :columns="tableHeaders"
5
+ :data="tableColumns"
6
+ :pageSize="8"
7
+ >
8
+ <!-- HEADER TEMPLATE -->
9
+ <template #header:checkboxes="{ item }">
10
+ MARK FOR SAVE
11
+ </template>
12
+ <!-- CHECKBOX CELL TEMPLATE -->
13
+ <template #cell:checkboxes="{ item }">
14
+ <div class="flex items-center justify-center">
15
+ <Checkbox
16
+ v-model="selected[tableColumnsIndexes.findIndex(el => el.label === item.label)].isChecked"
17
+ />
18
+ </div>
19
+ </template>
20
+ <!-- IMAGE CELL TEMPLATE -->
21
+ <template #cell:images="{item}">
22
+ <div class="flex flex-shrink-0 gap-2">
23
+ <div v-for="image in item.images" :key="image">
24
+ <div class="mt-2 flex items-center justify-center gap-2">
25
+ <img
26
+ :src="image"
27
+ class="w-20 h-20 object-cover rounded cursor-pointer border hover:border-blue-500 transition"
28
+ @click="zoomImage(image)"
29
+ />
30
+ </div>
31
+ <div
32
+ v-if="zoomedImage"
33
+ class="fixed inset-0 z-50 flex items-center justify-center bg-black/60"
34
+ @click.self="closeZoom"
35
+ >
36
+ <img
37
+ :src="zoomedImage"
38
+ ref="zoomedImg"
39
+ class="max-w-full max-h-full rounded-lg object-contain cursor-grab z-75"
40
+ />
41
+ </div>
42
+ </div>
43
+ </div>
44
+ </template>
45
+ <!-- CUSTOM FIELD TEMPLATES -->
46
+ <template v-for="n in customFieldNames" :key="n" #[`cell:${n}`]="{ item, column }">
47
+ <div v-if="isAiResponseReceived[tableColumnsIndexes.findIndex(el => el.id === item.id)]">
48
+ <div v-if="isInColumnEnum(n)">
49
+ <Select
50
+ :options="convertColumnEnumToSelectOptions(props.meta.columnEnums, n)"
51
+ v-model="selected[tableColumnsIndexes.findIndex(el => el.id === item.id)][n]"
52
+ >
53
+ </Select>
54
+ </div>
55
+ <div v-else-if="typeof selected[tableColumnsIndexes.findIndex(el => el.id === item.id)][n] === 'string' || typeof selected[tableColumnsIndexes.findIndex(el => el.id === item.id)][n] === 'object'">
56
+ <Textarea
57
+ class="w-full h-full"
58
+ type="text"
59
+ v-model="selected[tableColumnsIndexes.findIndex(el => el.id === item.id)][n]"
60
+ >
61
+ </Textarea>
62
+ </div>
63
+ <div v-else-if="typeof selected[tableColumnsIndexes.findIndex(el => el.id === item.id)][n] === 'boolean'">
64
+ <Toggle
65
+ v-model="selected[tableColumnsIndexes.findIndex(el => el.id === item.id)][n]"
66
+ >
67
+ </Toggle>
68
+ </div>
69
+ <div v-else>
70
+ <Input
71
+ type="number"
72
+ v-model="selected[tableColumnsIndexes.findIndex(el => el.id === item.id)][n]"
73
+ class="w-full "
74
+ :fullWidth="true"
75
+ />
76
+ </div>
77
+ </div>
78
+ <div v-else>
79
+ <Skeleton class="w-full h-6" />
80
+ </div>
81
+ </template>
82
+ </Table>
83
+ </div>
84
+ </template>
85
+
86
+ <script lang="ts" setup>
87
+ import { ref, nextTick, watch } from 'vue'
88
+ import mediumZoom from 'medium-zoom'
89
+ import { Select, Input, Textarea, Table, Checkbox, Skeleton, Toggle } from '@/afcl'
90
+
91
+ const props = defineProps<{
92
+ meta: any,
93
+ tableHeaders: any,
94
+ tableColumns: any,
95
+ customFieldNames: any,
96
+ tableColumnsIndexes: any,
97
+ selected: any,
98
+ isAiResponseReceived: boolean[]
99
+ }>();
100
+
101
+ const zoomedImage = ref(null)
102
+ const zoomedImg = ref(null)
103
+
104
+ function zoomImage(img) {
105
+ zoomedImage.value = img
106
+ }
107
+
108
+ function closeZoom() {
109
+ zoomedImage.value = null
110
+ }
111
+
112
+ watch(zoomedImage, async (val) => {
113
+ await nextTick()
114
+ if (val && zoomedImg.value) {
115
+ mediumZoom(zoomedImg.value, {
116
+ margin: 24,
117
+ background: 'rgba(0, 0, 0, 0.9)',
118
+ scrollOffset: 150
119
+ }).show()
120
+ }
121
+ })
122
+
123
+ function isInColumnEnum(key: string): boolean {
124
+ const colEnum = props.meta.columnEnums?.find(c => c.name === key);
125
+ if (!colEnum) {
126
+ return false;
127
+ }
128
+ return true;
129
+ }
130
+
131
+ function convertColumnEnumToSelectOptions(columnEnumArray: any[], key: string) {
132
+ const col = columnEnumArray.find(c => c.name === key);
133
+ if (!col) return [];
134
+ return col.enum.map(item => ({
135
+ label: item.label,
136
+ value: item.value
137
+ }));
138
+ }
139
+
140
+ </script>
package/dist/index.js ADDED
@@ -0,0 +1,166 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { AdminForthPlugin, Filters } from "adminforth";
11
+ import Handlebars from 'handlebars';
12
+ export default class BulkVisionPlugin extends AdminForthPlugin {
13
+ constructor(options) {
14
+ super(options, import.meta.url);
15
+ this.options = options;
16
+ }
17
+ // Compile Handlebars templates in outputFields using record fields as context
18
+ compileOutputFieldsTemplates(record) {
19
+ return this.options.outputFields.map((fieldObj) => {
20
+ const compiled = {};
21
+ for (const [key, templateStr] of Object.entries(fieldObj)) {
22
+ try {
23
+ const tpl = Handlebars.compile(String(templateStr));
24
+ compiled[key] = tpl(record);
25
+ }
26
+ catch (_a) {
27
+ compiled[key] = String(templateStr);
28
+ }
29
+ }
30
+ return compiled;
31
+ });
32
+ }
33
+ modifyResourceConfig(adminforth, resourceConfig) {
34
+ const _super = Object.create(null, {
35
+ modifyResourceConfig: { get: () => super.modifyResourceConfig }
36
+ });
37
+ return __awaiter(this, void 0, void 0, function* () {
38
+ _super.modifyResourceConfig.call(this, adminforth, resourceConfig);
39
+ //check if options names are provided
40
+ const columns = this.resourceConfig.columns;
41
+ let columnEnums = [];
42
+ for (const field of this.options.outputFields) {
43
+ for (const [key, value] of Object.entries(field)) {
44
+ const column = columns.find(c => c.name.toLowerCase() === key.toLowerCase());
45
+ if (column) {
46
+ if (column.enum) {
47
+ field[key] = `${value} Select ${key} from the list (USE ONLY VALUE FIELD. USE ONLY VALUES FROM THIS LIST): ${JSON.stringify(column.enum)}`;
48
+ columnEnums.push({
49
+ name: key,
50
+ enum: column.enum,
51
+ });
52
+ }
53
+ }
54
+ else {
55
+ throw new Error(`⚠️ No column found for key "${key}"`);
56
+ }
57
+ }
58
+ }
59
+ const pageInjection = {
60
+ file: this.componentPath('visionAction.vue'),
61
+ meta: {
62
+ pluginInstanceId: this.pluginInstanceId,
63
+ outputFields: this.options.outputFields,
64
+ actionName: this.options.actionName,
65
+ confirmMessage: this.options.confirmMessage,
66
+ columnEnums: columnEnums,
67
+ }
68
+ };
69
+ if (!this.resourceConfig.options.pageInjections) {
70
+ this.resourceConfig.options.pageInjections = {};
71
+ }
72
+ if (!this.resourceConfig.options.pageInjections.list) {
73
+ this.resourceConfig.options.pageInjections.list = {};
74
+ }
75
+ this.resourceConfig.options.pageInjections.list.threeDotsDropdownItems = [pageInjection];
76
+ });
77
+ }
78
+ validateConfigAfterDiscover(adminforth, resourceConfig) {
79
+ // optional method where you can safely check field types after database discovery was performed
80
+ }
81
+ instanceUniqueRepresentation(pluginOptions) {
82
+ // optional method to return unique string representation of plugin instance.
83
+ // Needed if plugin can have multiple instances on one resource
84
+ return `single`;
85
+ }
86
+ setupEndpoints(server) {
87
+ server.endpoint({
88
+ method: 'POST',
89
+ path: `/plugin/${this.pluginInstanceId}/analyze`,
90
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, headers }) {
91
+ const selectedIds = body.selectedIds || [];
92
+ const tasks = selectedIds.map((ID) => __awaiter(this, void 0, void 0, function* () {
93
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
94
+ // Fetch the record using the provided ID
95
+ const record = yield this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ('id', ID)]);
96
+ //recieve image URLs to analyze
97
+ const attachmentFiles = yield this.options.attachFiles({ record: record });
98
+ //create prompt for OpenAI
99
+ const compiledOutputFields = this.compileOutputFieldsTemplates(record);
100
+ const prompt = `Analyze the following image(s) and return a single JSON in format like: {'param1': 'value1', 'param2': 'value2'}.
101
+ Do NOT return array of objects. Do NOT include any Markdown, code blocks, explanations, or extra text. Only return valid JSON.
102
+ Each object must contain the following fields: ${JSON.stringify(compiledOutputFields)} Use the exact field names. If it's number field - return only number.
103
+ Image URLs:`;
104
+ //send prompt to OpenAI and get response
105
+ const chatResponse = yield this.options.adapter.generate({ prompt, inputFileUrls: attachmentFiles });
106
+ const resp = chatResponse.response;
107
+ const topLevelError = chatResponse.error;
108
+ if (topLevelError || (resp === null || resp === void 0 ? void 0 : resp.error)) {
109
+ throw new Error(`ERROR: ${JSON.stringify(topLevelError || (resp === null || resp === void 0 ? void 0 : resp.error))}`);
110
+ }
111
+ const textOutput = (_f = (_e = (_d = (_c = (_b = (_a = resp === null || resp === void 0 ? void 0 : resp.output) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.content) === null || _c === void 0 ? void 0 : _c[0]) === null || _d === void 0 ? void 0 : _d.text) !== null && _e !== void 0 ? _e : resp === null || resp === void 0 ? void 0 : resp.output_text) !== null && _f !== void 0 ? _f : (_j = (_h = (_g = resp === null || resp === void 0 ? void 0 : resp.choices) === null || _g === void 0 ? void 0 : _g[0]) === null || _h === void 0 ? void 0 : _h.message) === null || _j === void 0 ? void 0 : _j.content;
112
+ if (!textOutput || typeof textOutput !== 'string') {
113
+ throw new Error('Unexpected AI response format');
114
+ }
115
+ //parse response and update record
116
+ const resData = JSON.parse(textOutput);
117
+ return resData;
118
+ }));
119
+ const result = yield Promise.all(tasks);
120
+ return { result };
121
+ })
122
+ });
123
+ server.endpoint({
124
+ method: 'POST',
125
+ path: `/plugin/${this.pluginInstanceId}/get_records`,
126
+ handler: (body) => __awaiter(this, void 0, void 0, function* () {
127
+ let records = [];
128
+ for (const record of body.body.record) {
129
+ records.push(yield this.adminforth.resource(this.resourceConfig.resourceId).get([Filters.EQ('id', record)]));
130
+ records[records.length - 1]._label = this.resourceConfig.recordLabel(records[records.length - 1]);
131
+ }
132
+ return {
133
+ records,
134
+ };
135
+ })
136
+ });
137
+ server.endpoint({
138
+ method: 'POST',
139
+ path: `/plugin/${this.pluginInstanceId}/get_images`,
140
+ handler: (body) => __awaiter(this, void 0, void 0, function* () {
141
+ let images = [];
142
+ if (body.body.record) {
143
+ for (const record of body.body.record) {
144
+ images.push(yield this.options.attachFiles({ record: record }));
145
+ }
146
+ }
147
+ return {
148
+ images,
149
+ };
150
+ })
151
+ });
152
+ server.endpoint({
153
+ method: 'POST',
154
+ path: `/plugin/${this.pluginInstanceId}/update_fields`,
155
+ handler: (body) => __awaiter(this, void 0, void 0, function* () {
156
+ const selectedIds = body.body.selectedIds || [];
157
+ const fieldsToUpdate = body.body.fields || {};
158
+ const updates = selectedIds.map((ID, idx) => this.adminforth
159
+ .resource(this.resourceConfig.resourceId)
160
+ .update(ID, fieldsToUpdate[idx]));
161
+ yield Promise.all(updates);
162
+ return { ok: true };
163
+ })
164
+ });
165
+ }
166
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};