@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.
- package/.woodpecker/buildRelease.sh +13 -0
- package/.woodpecker/buildSlackNotify.sh +44 -0
- package/.woodpecker/release.yml +40 -0
- package/LICENSE +21 -0
- package/README.md +1 -0
- package/build.log +12 -0
- package/custom/tsconfig.json +19 -0
- package/custom/visionAction.vue +254 -0
- package/custom/visionTable.vue +140 -0
- package/dist/custom/tsconfig.json +19 -0
- package/dist/custom/visionAction.vue +254 -0
- package/dist/custom/visionTable.vue +140 -0
- package/dist/index.js +166 -0
- package/dist/types.js +1 -0
- package/index.ts +179 -0
- package/package.json +26 -0
- package/tsconfig.json +13 -0
- package/types.ts +12 -0
|
@@ -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 {};
|