@adminforth/bulk-ai-flow 2.0.3 → 2.1.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.
- package/build.log +7 -6
- package/custom/GenerationSetup.vue +148 -0
- package/custom/ImageLightbox.vue +45 -0
- package/custom/RecordDetail.vue +276 -0
- package/custom/ReviewQueue.vue +253 -0
- package/custom/VisionAction.vue +266 -878
- package/custom/package-lock.json +1 -331
- package/custom/package.json +0 -3
- package/custom/pnpm-lock.yaml +0 -214
- package/custom/reviewModel.ts +124 -0
- package/dist/custom/GenerationSetup.vue +148 -0
- package/dist/custom/ImageLightbox.vue +45 -0
- package/dist/custom/RecordDetail.vue +276 -0
- package/dist/custom/ReviewQueue.vue +253 -0
- package/dist/custom/VisionAction.vue +266 -878
- package/dist/custom/package-lock.json +1 -331
- package/dist/custom/package.json +0 -3
- package/dist/custom/pnpm-lock.yaml +0 -214
- package/dist/custom/reviewModel.ts +124 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +34 -39
- package/dist/types.d.ts +2 -2
- package/index.ts +39 -46
- package/package.json +2 -2
- package/types.ts +2 -2
- package/custom/ImageCompare.vue +0 -185
- package/custom/ImageGenerationCarousel.vue +0 -327
- package/custom/Swiper.vue +0 -76
- package/custom/VisionTable.vue +0 -575
- package/dist/custom/ImageCompare.vue +0 -185
- package/dist/custom/ImageGenerationCarousel.vue +0 -327
- package/dist/custom/Swiper.vue +0 -76
- package/dist/custom/VisionTable.vue +0 -575
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
export type RecordStatus = 'pending' | 'processing' | 'completed' | 'failed';
|
|
2
|
+
export type GenerationAction = 'analyze' | 'analyze_no_images' | 'generate_images';
|
|
3
|
+
export type PromptCategory = 'imageFieldsPrompts' | 'plainFieldsPrompts' | 'generateImages';
|
|
4
|
+
export type FieldKind = 'fromImage' | 'text' | 'image';
|
|
5
|
+
export type FieldChange = 'new' | 'changed' | 'same';
|
|
6
|
+
|
|
7
|
+
export type RecordState = {
|
|
8
|
+
id: string | number;
|
|
9
|
+
status: RecordStatus;
|
|
10
|
+
isChecked: boolean;
|
|
11
|
+
label: string;
|
|
12
|
+
data: Record<string, any>;
|
|
13
|
+
oldData: Record<string, any>;
|
|
14
|
+
// links to the stored images of the image fields, null for an empty field
|
|
15
|
+
oldImagePreviews: Record<string, string | null>;
|
|
16
|
+
// source images returned by attachFiles
|
|
17
|
+
images: string[];
|
|
18
|
+
aiStatus: {
|
|
19
|
+
generatedImages: boolean;
|
|
20
|
+
analyzedImages: boolean;
|
|
21
|
+
analyzedNoImages: boolean;
|
|
22
|
+
};
|
|
23
|
+
imageGenerationErrorMessage: string;
|
|
24
|
+
imageGenerationFailed: boolean;
|
|
25
|
+
imageToTextErrorMessages: Record<string, string>;
|
|
26
|
+
textToTextErrorMessages: Record<string, string>;
|
|
27
|
+
regeneratingFieldsStatus: Record<string, boolean>;
|
|
28
|
+
// image fields which kept their stored image, because values are not overwritten
|
|
29
|
+
listOfImageNotGenerated: Record<string, { originalImage: string }>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const PROMPT_CATEGORY: Record<GenerationAction, PromptCategory> = {
|
|
33
|
+
analyze: 'imageFieldsPrompts',
|
|
34
|
+
analyze_no_images: 'plainFieldsPrompts',
|
|
35
|
+
generate_images: 'generateImages',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const CATEGORY_KIND: Record<PromptCategory, FieldKind> = {
|
|
39
|
+
imageFieldsPrompts: 'fromImage',
|
|
40
|
+
plainFieldsPrompts: 'text',
|
|
41
|
+
generateImages: 'image',
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export function fieldKind(meta: any, field: string): FieldKind {
|
|
45
|
+
if (meta.outputImageFields.includes(field)) {
|
|
46
|
+
return 'image';
|
|
47
|
+
}
|
|
48
|
+
return meta.outputFieldsForAnalizeFromImages.includes(field) ? 'fromImage' : 'text';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// keys of the plugin options may differ in case from the column names
|
|
52
|
+
export function fieldLabel(meta: any, field: string): string {
|
|
53
|
+
const labels: Record<string, string> = meta.columnLabels;
|
|
54
|
+
const key = Object.keys(labels).find((name) => name.toLowerCase() === field.toLowerCase());
|
|
55
|
+
return key ? labels[key] : field;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function isFieldGenerating(record: RecordState, meta: any, field: string): boolean {
|
|
59
|
+
const kind = fieldKind(meta, field);
|
|
60
|
+
if (kind === 'image') {
|
|
61
|
+
return !record.aiStatus.generatedImages;
|
|
62
|
+
}
|
|
63
|
+
const isDone = kind === 'fromImage' ? record.aiStatus.analyzedImages : record.aiStatus.analyzedNoImages;
|
|
64
|
+
return !isDone || record.regeneratingFieldsStatus[field] === true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// images and single fields of a record regenerate in parallel, its status settles once all of them finished
|
|
68
|
+
export function isRegenerating(record: RecordState): boolean {
|
|
69
|
+
return !record.aiStatus.generatedImages || Object.values(record.regeneratingFieldsStatus).some(Boolean);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function isRecordBusy(record: RecordState): boolean {
|
|
73
|
+
return record.status === 'pending' || record.status === 'processing' || isRegenerating(record);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function fieldError(record: RecordState, meta: any, field: string): string {
|
|
77
|
+
const kind = fieldKind(meta, field);
|
|
78
|
+
if (kind === 'image') {
|
|
79
|
+
return record.imageGenerationFailed ? record.imageGenerationErrorMessage : '';
|
|
80
|
+
}
|
|
81
|
+
return (kind === 'fromImage' ? record.imageToTextErrorMessages : record.textToTextErrorMessages)[field] ?? '';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function recordError(record: RecordState): string {
|
|
85
|
+
return [
|
|
86
|
+
record.imageGenerationFailed ? record.imageGenerationErrorMessage : '',
|
|
87
|
+
...Object.values(record.imageToTextErrorMessages),
|
|
88
|
+
...Object.values(record.textToTextErrorMessages),
|
|
89
|
+
].find(Boolean) ?? '';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// the image a field will have after saving: the generated one, or the stored one while nothing new was made
|
|
93
|
+
export function generatedImageUrl(record: RecordState, meta: any, field: string): string | null {
|
|
94
|
+
return fieldChange(record, meta, field) === 'same' ? record.oldImagePreviews[field] ?? null : record.data[field];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isEmpty(value: any): boolean {
|
|
98
|
+
return value === null || value === undefined || value === '';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function isSameValue(a: any, b: any): boolean {
|
|
102
|
+
if (isEmpty(a) || isEmpty(b)) {
|
|
103
|
+
return isEmpty(a) && isEmpty(b);
|
|
104
|
+
}
|
|
105
|
+
return typeof a === 'object' || typeof b === 'object'
|
|
106
|
+
? JSON.stringify(a) === JSON.stringify(b)
|
|
107
|
+
: String(a) === String(b);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function fieldChange(record: RecordState, meta: any, field: string): FieldChange {
|
|
111
|
+
const isKeptImage = fieldKind(meta, field) === 'image' && field in record.listOfImageNotGenerated;
|
|
112
|
+
if (isKeptImage || isSameValue(record.data[field], record.oldData[field])) {
|
|
113
|
+
return 'same';
|
|
114
|
+
}
|
|
115
|
+
return isEmpty(record.oldData[field]) ? 'new' : 'changed';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function changedFieldsCount(record: RecordState, meta: any): number {
|
|
119
|
+
return meta.outputFields.filter((field: string) => fieldChange(record, meta, field) !== 'same').length;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function isRecordChanged(record: RecordState, meta: any): boolean {
|
|
123
|
+
return record.status !== 'pending' && changedFieldsCount(record, meta) > 0;
|
|
124
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -57,6 +57,11 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
57
57
|
* way the core show route drops them.
|
|
58
58
|
*/
|
|
59
59
|
private scopeRecordForFrontend;
|
|
60
|
+
/**
|
|
61
|
+
* Links to the stored images of the generated image columns, built by the Upload plugin of each
|
|
62
|
+
* column the same way its own previews are, so the dialog shows the current image next to the new one.
|
|
63
|
+
*/
|
|
64
|
+
private imagePreviewUrls;
|
|
60
65
|
/**
|
|
61
66
|
* The row as the caller is allowed to read it: columns which are not declared on the resource and
|
|
62
67
|
* columns whose `backendOnly` resolves true for this request are dropped, exactly the way the
|
package/dist/index.js
CHANGED
|
@@ -48,10 +48,6 @@ const jobStatusBodySchema = z.object({
|
|
|
48
48
|
const updateRateLimitsBodySchema = z.object({
|
|
49
49
|
actionType: z.string().nullish(),
|
|
50
50
|
}).strict();
|
|
51
|
-
const compileOldImageLinkBodySchema = z.object({
|
|
52
|
-
image: z.string().nullish(),
|
|
53
|
-
columnName: z.string().nullish(),
|
|
54
|
-
}).strict();
|
|
55
51
|
const getFilteredIdsBodySchema = z.object({
|
|
56
52
|
filters: z.any(),
|
|
57
53
|
}).passthrough();
|
|
@@ -687,10 +683,18 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
687
683
|
outputImagesPluginInstanceIds[key] = plugin.pluginInstanceId;
|
|
688
684
|
}
|
|
689
685
|
}
|
|
690
|
-
const outputFields =
|
|
686
|
+
const outputFields = [
|
|
687
|
+
...outputFieldsForAnalizeFromImages,
|
|
688
|
+
...outputPlainFields,
|
|
689
|
+
...outputImageFields,
|
|
690
|
+
];
|
|
691
691
|
const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
|
|
692
692
|
// Map of column technical names to their display labels from resource config
|
|
693
693
|
const columnLabels = Object.fromEntries((this.resourceConfig.columns || []).map((c) => [c.name, c.label || c.name]));
|
|
694
|
+
// the review dialog picks an editor for a generated value by the type of its column
|
|
695
|
+
const columnTypes = Object.fromEntries(this.resourceConfig.columns.filter((c) => outputFields.includes(c.name)).map((c) => [c.name, c.type]));
|
|
696
|
+
// only the prompts go to the browser: an entry of generateImages may hold an adapter with its API key
|
|
697
|
+
const imageGenerationPrompts = Object.fromEntries(Object.entries(this.options.generateImages || {}).map(([key, value]) => [key, value.prompt]));
|
|
694
698
|
const pageInjection = {
|
|
695
699
|
file: this.componentPath('VisionAction.vue'),
|
|
696
700
|
meta: {
|
|
@@ -699,6 +703,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
699
703
|
actionName: this.options.actionName,
|
|
700
704
|
columnEnums: columnEnums,
|
|
701
705
|
columnLabels: columnLabels,
|
|
706
|
+
columnTypes: columnTypes,
|
|
702
707
|
outputImageFields: outputImageFields,
|
|
703
708
|
outputFieldsForAnalizeFromImages: outputFieldsForAnalizeFromImages,
|
|
704
709
|
outputPlainFields: outputPlainFields,
|
|
@@ -722,7 +727,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
722
727
|
generationPrompts: {
|
|
723
728
|
plainFieldsPrompts: this.options.fillPlainFields || {},
|
|
724
729
|
imageFieldsPrompts: this.options.fillFieldsFromImages || {},
|
|
725
|
-
imageGenerationPrompts:
|
|
730
|
+
imageGenerationPrompts: imageGenerationPrompts,
|
|
726
731
|
},
|
|
727
732
|
pageSize: this.options.pageSize
|
|
728
733
|
}
|
|
@@ -985,6 +990,24 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
985
990
|
return scoped;
|
|
986
991
|
});
|
|
987
992
|
}
|
|
993
|
+
/**
|
|
994
|
+
* Links to the stored images of the generated image columns, built by the Upload plugin of each
|
|
995
|
+
* column the same way its own previews are, so the dialog shows the current image next to the new one.
|
|
996
|
+
*/
|
|
997
|
+
imagePreviewUrls(record) {
|
|
998
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
999
|
+
var _a;
|
|
1000
|
+
const previews = {};
|
|
1001
|
+
for (const key of Object.keys(this.options.generateImages || {})) {
|
|
1002
|
+
const uploadPlugin = this.adminforth.activatedPlugins.find(p => p.resourceConfig.resourceId === this.resourceConfig.resourceId &&
|
|
1003
|
+
p.pluginOptions.pathColumnName === key);
|
|
1004
|
+
const holder = { [key]: record[key] };
|
|
1005
|
+
yield uploadPlugin.genPreviewUrl(holder);
|
|
1006
|
+
previews[key] = (_a = holder[`previewUrl_${uploadPlugin.pluginInstanceId}`]) !== null && _a !== void 0 ? _a : null;
|
|
1007
|
+
}
|
|
1008
|
+
return previews;
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
988
1011
|
/**
|
|
989
1012
|
* The row as the caller is allowed to read it: columns which are not declared on the resource and
|
|
990
1013
|
* columns whose `backendOnly` resolves true for this request are dropped, exactly the way the
|
|
@@ -1022,7 +1045,11 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
1022
1045
|
if (error) {
|
|
1023
1046
|
return { ok: false, error };
|
|
1024
1047
|
}
|
|
1025
|
-
return {
|
|
1048
|
+
return {
|
|
1049
|
+
ok: true,
|
|
1050
|
+
record: yield this.scopeRecordForFrontend(record, ctx),
|
|
1051
|
+
imagePreviews: yield this.imagePreviewUrls(record),
|
|
1052
|
+
};
|
|
1026
1053
|
})
|
|
1027
1054
|
});
|
|
1028
1055
|
server.endpoint({
|
|
@@ -1325,38 +1352,6 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
1325
1352
|
return { ok: true, sessionId };
|
|
1326
1353
|
})
|
|
1327
1354
|
});
|
|
1328
|
-
server.endpoint({
|
|
1329
|
-
method: 'POST',
|
|
1330
|
-
path: `/plugin/${this.pluginInstanceId}/compile_old_image_link`,
|
|
1331
|
-
agent: { hiddenFromAgents: true },
|
|
1332
|
-
request_schema: compileOldImageLinkBodySchema,
|
|
1333
|
-
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, headers, response }) {
|
|
1334
|
-
var _b, _c;
|
|
1335
|
-
const data = body;
|
|
1336
|
-
const image = data.image;
|
|
1337
|
-
const columnName = data.columnName;
|
|
1338
|
-
if (!image) {
|
|
1339
|
-
return { ok: false, error: "Can't find image url" };
|
|
1340
|
-
}
|
|
1341
|
-
if (!columnName) {
|
|
1342
|
-
return { ok: false, error: "Can't find column name" };
|
|
1343
|
-
}
|
|
1344
|
-
try {
|
|
1345
|
-
if ((_b = this.options) === null || _b === void 0 ? void 0 : _b.generateImages) {
|
|
1346
|
-
const plugin = this.adminforth.activatedPlugins.find(p => p.resourceConfig.resourceId === this.resourceConfig.resourceId &&
|
|
1347
|
-
p.pluginOptions.pathColumnName === columnName);
|
|
1348
|
-
if ((_c = plugin === null || plugin === void 0 ? void 0 : plugin.pluginOptions) === null || _c === void 0 ? void 0 : _c.preview) {
|
|
1349
|
-
const compiledPreviewUrl = plugin.pluginOptions.preview.previewUrl({ filePath: image });
|
|
1350
|
-
return { ok: true, previewUrl: compiledPreviewUrl };
|
|
1351
|
-
}
|
|
1352
|
-
return { ok: false, error: "Can't find plugin for column" };
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1355
|
-
catch (e) {
|
|
1356
|
-
return { ok: false, error: "Error compiling preview url" };
|
|
1357
|
-
}
|
|
1358
|
-
})
|
|
1359
|
-
});
|
|
1360
1355
|
server.endpoint({
|
|
1361
1356
|
method: 'POST',
|
|
1362
1357
|
path: `/plugin/${this.pluginInstanceId}/get_filtered_ids`,
|
package/dist/types.d.ts
CHANGED
|
@@ -131,8 +131,8 @@ export interface PluginOptions extends PluginsCommonOptions {
|
|
|
131
131
|
everyRecords: number;
|
|
132
132
|
})[];
|
|
133
133
|
/**
|
|
134
|
-
* Number of
|
|
135
|
-
* If omitted, the
|
|
134
|
+
* Number of records per page in the list of the review dialog.
|
|
135
|
+
* If omitted, the list shows 10 records per page.
|
|
136
136
|
*/
|
|
137
137
|
pageSize?: number;
|
|
138
138
|
}
|
package/index.ts
CHANGED
|
@@ -50,11 +50,6 @@ const updateRateLimitsBodySchema = z.object({
|
|
|
50
50
|
actionType: z.string().nullish(),
|
|
51
51
|
}).strict();
|
|
52
52
|
|
|
53
|
-
const compileOldImageLinkBodySchema = z.object({
|
|
54
|
-
image: z.string().nullish(),
|
|
55
|
-
columnName: z.string().nullish(),
|
|
56
|
-
}).strict();
|
|
57
|
-
|
|
58
53
|
const getFilteredIdsBodySchema = z.object({
|
|
59
54
|
filters: z.any(),
|
|
60
55
|
}).passthrough();
|
|
@@ -694,18 +689,26 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
694
689
|
}
|
|
695
690
|
}
|
|
696
691
|
|
|
697
|
-
const outputFields =
|
|
698
|
-
...
|
|
699
|
-
...
|
|
700
|
-
...
|
|
701
|
-
|
|
692
|
+
const outputFields = [
|
|
693
|
+
...outputFieldsForAnalizeFromImages,
|
|
694
|
+
...outputPlainFields,
|
|
695
|
+
...outputImageFields,
|
|
696
|
+
];
|
|
702
697
|
|
|
703
698
|
const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
|
|
704
699
|
// Map of column technical names to their display labels from resource config
|
|
705
700
|
const columnLabels: Record<string, string> = Object.fromEntries(
|
|
706
701
|
(this.resourceConfig.columns || []).map((c: any) => [c.name, c.label || c.name])
|
|
707
702
|
);
|
|
708
|
-
|
|
703
|
+
// the review dialog picks an editor for a generated value by the type of its column
|
|
704
|
+
const columnTypes: Record<string, string> = Object.fromEntries(
|
|
705
|
+
this.resourceConfig.columns.filter((c) => outputFields.includes(c.name)).map((c) => [c.name, c.type])
|
|
706
|
+
);
|
|
707
|
+
// only the prompts go to the browser: an entry of generateImages may hold an adapter with its API key
|
|
708
|
+
const imageGenerationPrompts: Record<string, string> = Object.fromEntries(
|
|
709
|
+
Object.entries(this.options.generateImages || {}).map(([key, value]) => [key, value.prompt])
|
|
710
|
+
);
|
|
711
|
+
|
|
709
712
|
const pageInjection = {
|
|
710
713
|
file: this.componentPath('VisionAction.vue'),
|
|
711
714
|
meta: {
|
|
@@ -714,6 +717,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
714
717
|
actionName: this.options.actionName,
|
|
715
718
|
columnEnums: columnEnums,
|
|
716
719
|
columnLabels: columnLabels,
|
|
720
|
+
columnTypes: columnTypes,
|
|
717
721
|
outputImageFields: outputImageFields,
|
|
718
722
|
outputFieldsForAnalizeFromImages: outputFieldsForAnalizeFromImages,
|
|
719
723
|
outputPlainFields: outputPlainFields,
|
|
@@ -737,7 +741,7 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
737
741
|
generationPrompts: {
|
|
738
742
|
plainFieldsPrompts: this.options.fillPlainFields || {},
|
|
739
743
|
imageFieldsPrompts: this.options.fillFieldsFromImages || {},
|
|
740
|
-
imageGenerationPrompts:
|
|
744
|
+
imageGenerationPrompts: imageGenerationPrompts,
|
|
741
745
|
},
|
|
742
746
|
pageSize: this.options.pageSize
|
|
743
747
|
}
|
|
@@ -1036,6 +1040,24 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
1036
1040
|
return scoped;
|
|
1037
1041
|
}
|
|
1038
1042
|
|
|
1043
|
+
/**
|
|
1044
|
+
* Links to the stored images of the generated image columns, built by the Upload plugin of each
|
|
1045
|
+
* column the same way its own previews are, so the dialog shows the current image next to the new one.
|
|
1046
|
+
*/
|
|
1047
|
+
private async imagePreviewUrls(record: any): Promise<Record<string, string | null>> {
|
|
1048
|
+
const previews: Record<string, string | null> = {};
|
|
1049
|
+
for (const key of Object.keys(this.options.generateImages || {})) {
|
|
1050
|
+
const uploadPlugin = this.adminforth.activatedPlugins.find(p =>
|
|
1051
|
+
p.resourceConfig!.resourceId === this.resourceConfig.resourceId &&
|
|
1052
|
+
p.pluginOptions.pathColumnName === key
|
|
1053
|
+
) as any;
|
|
1054
|
+
const holder = { [key]: record[key] };
|
|
1055
|
+
await uploadPlugin.genPreviewUrl(holder);
|
|
1056
|
+
previews[key] = holder[`previewUrl_${uploadPlugin.pluginInstanceId}`] ?? null;
|
|
1057
|
+
}
|
|
1058
|
+
return previews;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1039
1061
|
/**
|
|
1040
1062
|
* The row as the caller is allowed to read it: columns which are not declared on the resource and
|
|
1041
1063
|
* columns whose `backendOnly` resolves true for this request are dropped, exactly the way the
|
|
@@ -1072,7 +1094,11 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
1072
1094
|
if (error) {
|
|
1073
1095
|
return { ok: false, error };
|
|
1074
1096
|
}
|
|
1075
|
-
return {
|
|
1097
|
+
return {
|
|
1098
|
+
ok: true,
|
|
1099
|
+
record: await this.scopeRecordForFrontend(record, ctx),
|
|
1100
|
+
imagePreviews: await this.imagePreviewUrls(record),
|
|
1101
|
+
};
|
|
1076
1102
|
}
|
|
1077
1103
|
});
|
|
1078
1104
|
|
|
@@ -1402,39 +1428,6 @@ export default class BulkAiFlowPlugin extends AdminForthPlugin {
|
|
|
1402
1428
|
});
|
|
1403
1429
|
|
|
1404
1430
|
|
|
1405
|
-
server.endpoint({
|
|
1406
|
-
method: 'POST',
|
|
1407
|
-
path: `/plugin/${this.pluginInstanceId}/compile_old_image_link`,
|
|
1408
|
-
agent: { hiddenFromAgents: true },
|
|
1409
|
-
request_schema: compileOldImageLinkBodySchema,
|
|
1410
|
-
handler: async ({ body, adminUser, headers, response }) => {
|
|
1411
|
-
const data = body as z.infer<typeof compileOldImageLinkBodySchema>;
|
|
1412
|
-
const image = data.image;
|
|
1413
|
-
const columnName = data.columnName;
|
|
1414
|
-
if (!image) {
|
|
1415
|
-
return { ok: false, error: "Can't find image url" };
|
|
1416
|
-
}
|
|
1417
|
-
if (!columnName) {
|
|
1418
|
-
return { ok: false, error: "Can't find column name" };
|
|
1419
|
-
}
|
|
1420
|
-
try {
|
|
1421
|
-
if (this.options?.generateImages) {
|
|
1422
|
-
const plugin = this.adminforth.activatedPlugins.find(p =>
|
|
1423
|
-
p.resourceConfig!.resourceId === this.resourceConfig.resourceId &&
|
|
1424
|
-
p.pluginOptions.pathColumnName === columnName
|
|
1425
|
-
);
|
|
1426
|
-
if (plugin?.pluginOptions?.preview) {
|
|
1427
|
-
const compiledPreviewUrl = plugin.pluginOptions.preview.previewUrl({ filePath: image });
|
|
1428
|
-
return { ok: true, previewUrl: compiledPreviewUrl };
|
|
1429
|
-
}
|
|
1430
|
-
return { ok: false, error: "Can't find plugin for column" };
|
|
1431
|
-
}
|
|
1432
|
-
} catch (e) {
|
|
1433
|
-
return { ok: false, error: "Error compiling preview url" };
|
|
1434
|
-
}
|
|
1435
|
-
}
|
|
1436
|
-
});
|
|
1437
|
-
|
|
1438
1431
|
server.endpoint({
|
|
1439
1432
|
method: 'POST',
|
|
1440
1433
|
path: `/plugin/${this.pluginInstanceId}/get_filtered_ids`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adminforth/bulk-ai-flow",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"zod": "^4.3.6"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"adminforth": "^4.
|
|
43
|
+
"adminforth": "^4.7.3"
|
|
44
44
|
},
|
|
45
45
|
"release": {
|
|
46
46
|
"plugins": [
|
package/types.ts
CHANGED
|
@@ -154,8 +154,8 @@ export interface PluginOptions extends PluginsCommonOptions {
|
|
|
154
154
|
askConfirmation?: ({ afterRecords: number } | { everyRecords: number })[]
|
|
155
155
|
|
|
156
156
|
/**
|
|
157
|
-
* Number of
|
|
158
|
-
* If omitted, the
|
|
157
|
+
* Number of records per page in the list of the review dialog.
|
|
158
|
+
* If omitted, the list shows 10 records per page.
|
|
159
159
|
*/
|
|
160
160
|
pageSize?: number;
|
|
161
161
|
|
package/custom/ImageCompare.vue
DELETED
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<!-- Popup Overlay -->
|
|
3
|
-
<div class="fixed inset-0 z-40 flex items-center justify-center bg-lightScrim dark:bg-darkScrim" @click.self="closePopup">
|
|
4
|
-
<div class="image-compare-container shadow-sm p-4 border-white dark:border-black max-w-4xl max-h-[90vh] overflow-y-auto rounded-lg bg-white dark:bg-gray-800">
|
|
5
|
-
<!-- Close Button -->
|
|
6
|
-
<div class="flex justify-end mb-4">
|
|
7
|
-
<button type="button"
|
|
8
|
-
@click="closePopup"
|
|
9
|
-
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white" >
|
|
10
|
-
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
|
11
|
-
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
|
|
12
|
-
</svg>
|
|
13
|
-
<span class="sr-only">{{ $t('Close modal') }}</span>
|
|
14
|
-
</button>
|
|
15
|
-
</div>
|
|
16
|
-
<div class="flex gap-4 items-start justify-between">
|
|
17
|
-
<h3 class="text-sm font-medium text-gray-700 dark:text-white mb-2">{{ $t('Old Image') }}</h3>
|
|
18
|
-
<h3 class="text-sm font-medium text-gray-700 dark:text-white mb-2">{{ $t('New Image') }}</h3>
|
|
19
|
-
</div>
|
|
20
|
-
<div class="flex gap-4 items-center">
|
|
21
|
-
<!-- Old Image -->
|
|
22
|
-
<div class="flex-1">
|
|
23
|
-
<div class="relative">
|
|
24
|
-
<img
|
|
25
|
-
v-if="isValidUrl(compiledOldImage)"
|
|
26
|
-
ref="oldImg"
|
|
27
|
-
:src="compiledOldImage"
|
|
28
|
-
alt="Old image"
|
|
29
|
-
class="w-full max-w-sm h-auto object-cover rounded-lg cursor-pointer border hover:border-blue-500 transition-colors duration-200"
|
|
30
|
-
/>
|
|
31
|
-
<div v-else class="w-full max-w-sm h-48 bg-gray-100 rounded-lg flex items-center justify-center">
|
|
32
|
-
<p class="text-gray-500">{{ $t('No old image') }}</p>
|
|
33
|
-
</div>
|
|
34
|
-
</div>
|
|
35
|
-
</div>
|
|
36
|
-
|
|
37
|
-
<!-- Comparison Arrow -->
|
|
38
|
-
<div class="flex items-center justify-center">
|
|
39
|
-
<div class="w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center dark:bg-gray-500">
|
|
40
|
-
<svg class="w-4 h-4 text-blue-600 dark:text-black" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
41
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
|
42
|
-
</svg>
|
|
43
|
-
</div>
|
|
44
|
-
</div>
|
|
45
|
-
|
|
46
|
-
<!-- New Image -->
|
|
47
|
-
<div class="flex-1">
|
|
48
|
-
<div class="relative">
|
|
49
|
-
<img
|
|
50
|
-
v-if="isValidUrl(newImage)"
|
|
51
|
-
ref="newImg"
|
|
52
|
-
:src="newImage"
|
|
53
|
-
alt="New image"
|
|
54
|
-
class="w-full max-w-sm h-auto object-cover rounded-lg cursor-pointer border hover:border-blue-500 transition-colors duration-200"
|
|
55
|
-
/>
|
|
56
|
-
<div v-else class="w-full max-w-sm h-48 bg-gray-100 rounded-lg flex items-center justify-center">
|
|
57
|
-
<p class="text-gray-500">{{ $t('No new image') }}</p>
|
|
58
|
-
</div>
|
|
59
|
-
</div>
|
|
60
|
-
</div>
|
|
61
|
-
</div>
|
|
62
|
-
</div>
|
|
63
|
-
</div>
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
</template>
|
|
67
|
-
|
|
68
|
-
<script setup lang="ts">
|
|
69
|
-
import { ref, onMounted, watch, nextTick } from 'vue'
|
|
70
|
-
import mediumZoom from 'medium-zoom'
|
|
71
|
-
import { callAdminForthApi } from '@/utils';
|
|
72
|
-
|
|
73
|
-
const props = defineProps<{
|
|
74
|
-
oldImage: string
|
|
75
|
-
newImage: string
|
|
76
|
-
meta: any
|
|
77
|
-
columnName: string
|
|
78
|
-
}>()
|
|
79
|
-
|
|
80
|
-
const emit = defineEmits<{
|
|
81
|
-
close: []
|
|
82
|
-
}>()
|
|
83
|
-
|
|
84
|
-
const oldImg = ref<HTMLImageElement | null>(null)
|
|
85
|
-
const newImg = ref<HTMLImageElement | null>(null)
|
|
86
|
-
const oldZoom = ref<any>(null)
|
|
87
|
-
const newZoom = ref<any>(null)
|
|
88
|
-
const compiledOldImage = ref<string>('')
|
|
89
|
-
|
|
90
|
-
async function compileOldImage() {
|
|
91
|
-
try {
|
|
92
|
-
const res = await callAdminForthApi({
|
|
93
|
-
path: `/plugin/${props.meta.pluginInstanceId}/compile_old_image_link`,
|
|
94
|
-
method: 'POST',
|
|
95
|
-
body: {
|
|
96
|
-
image: props.oldImage,
|
|
97
|
-
columnName: props.columnName,
|
|
98
|
-
},
|
|
99
|
-
});
|
|
100
|
-
compiledOldImage.value = res.previewUrl;
|
|
101
|
-
} catch (e) {
|
|
102
|
-
console.error("Error compiling old image:", e)
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function closePopup() {
|
|
108
|
-
emit('close')
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function isValidUrl(str: string): boolean {
|
|
112
|
-
if (!str) return false
|
|
113
|
-
try {
|
|
114
|
-
new URL(str)
|
|
115
|
-
return true
|
|
116
|
-
} catch {
|
|
117
|
-
return false
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function initializeZoom() {
|
|
122
|
-
// Clean up existing zoom instances
|
|
123
|
-
if (oldZoom.value) {
|
|
124
|
-
oldZoom.value.detach()
|
|
125
|
-
}
|
|
126
|
-
if (newZoom.value) {
|
|
127
|
-
newZoom.value.detach()
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Initialize zoom for old image
|
|
131
|
-
if (oldImg.value && isValidUrl(compiledOldImage.value)) {
|
|
132
|
-
oldZoom.value = mediumZoom(oldImg.value, {
|
|
133
|
-
margin: 24,
|
|
134
|
-
background: 'rgba(0, 0, 0, 0.8)',
|
|
135
|
-
scrollOffset: 150
|
|
136
|
-
})
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Initialize zoom for new image
|
|
140
|
-
if (newImg.value && isValidUrl(props.newImage)) {
|
|
141
|
-
newZoom.value = mediumZoom(newImg.value, {
|
|
142
|
-
margin: 24,
|
|
143
|
-
background: 'rgba(0, 0, 0, 0.8)',
|
|
144
|
-
scrollOffset: 150
|
|
145
|
-
})
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
onMounted(async () => {
|
|
150
|
-
await compileOldImage()
|
|
151
|
-
await nextTick()
|
|
152
|
-
initializeZoom()
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
// Re-initialize zoom when images change
|
|
156
|
-
watch([() => props.oldImage, () => props.newImage, () => compiledOldImage.value], async () => {
|
|
157
|
-
await nextTick()
|
|
158
|
-
initializeZoom()
|
|
159
|
-
})
|
|
160
|
-
</script>
|
|
161
|
-
|
|
162
|
-
<style scoped>
|
|
163
|
-
.medium-zoom-image {
|
|
164
|
-
z-index: 999999;
|
|
165
|
-
background: rgba(0, 0, 0, 0.8);
|
|
166
|
-
border: none;
|
|
167
|
-
border-radius: 0;
|
|
168
|
-
}
|
|
169
|
-
.medium-zoom-overlay {
|
|
170
|
-
z-index: 99999;
|
|
171
|
-
background: rgba(0, 0, 0, 0.8);
|
|
172
|
-
}
|
|
173
|
-
html.dark .medium-zoom-overlay {
|
|
174
|
-
background: rgba(17, 24, 39, 0.8);
|
|
175
|
-
}
|
|
176
|
-
body.medium-zoom--opened aside {
|
|
177
|
-
filter: grayscale(1);
|
|
178
|
-
}
|
|
179
|
-
.fade-enter-active, .fade-leave-active {
|
|
180
|
-
transition: opacity 0.3s ease;
|
|
181
|
-
}
|
|
182
|
-
.fade-enter-from, .fade-leave-to {
|
|
183
|
-
opacity: 0;
|
|
184
|
-
}
|
|
185
|
-
</style>
|