@adminforth/bulk-ai-flow 2.0.4 → 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 CHANGED
@@ -4,15 +4,16 @@
4
4
 
5
5
  sending incremental file list
6
6
  custom/
7
- custom/ImageCompare.vue
8
- custom/ImageGenerationCarousel.vue
9
- custom/Swiper.vue
7
+ custom/GenerationSetup.vue
8
+ custom/ImageLightbox.vue
9
+ custom/RecordDetail.vue
10
+ custom/ReviewQueue.vue
10
11
  custom/VisionAction.vue
11
- custom/VisionTable.vue
12
12
  custom/package-lock.json
13
13
  custom/package.json
14
14
  custom/pnpm-lock.yaml
15
+ custom/reviewModel.ts
15
16
  custom/tsconfig.json
16
17
 
17
- sent 130,404 bytes received 191 bytes 261,190.00 bytes/sec
18
- total size is 129,681 speedup is 0.99
18
+ sent 83,073 bytes received 210 bytes 166,566.00 bytes/sec
19
+ total size is 82,285 speedup is 0.99
@@ -0,0 +1,148 @@
1
+ <template>
2
+ <div class="af-confirmation-panel flex flex-col gap-4">
3
+ <section class="af-configure-prompts-card flex flex-col gap-1.5">
4
+ <h4 class="text-sm font-medium text-lightDialogHeaderText dark:text-darkDialogHeaderText">{{ t('Fields to generate') }}</h4>
5
+ <ul class="af-settings-panel overflow-hidden rounded-lg border border-lightFormBorder divide-y divide-lightFormBorder dark:border-darkFormBorder dark:divide-darkFormBorder">
6
+ <li
7
+ v-for="row in rows"
8
+ :key="row.field"
9
+ :class="openFields.has(row.field) ? 'bg-lightFormHeading dark:bg-darkFormHeading' : 'bg-lightForm dark:bg-darkForm'"
10
+ >
11
+ <div class="flex items-center gap-3 px-3.5 py-2.5">
12
+ <div class="flex-1 min-w-0">
13
+ <p class="text-sm font-medium text-lightDialogHeaderText dark:text-darkDialogHeaderText">{{ fieldLabel(meta, row.field) }}</p>
14
+ <p v-if="!openFields.has(row.field)" class="truncate text-xs text-lightInputIconColor dark:text-darkInputIconColor">
15
+ {{ prompts[row.category][row.field] }}
16
+ </p>
17
+ </div>
18
+ <span
19
+ v-if="isUnsaved(row)"
20
+ class="inline-flex h-5 shrink-0 items-center rounded-full border px-2 text-xs font-medium border-lightWarningBorder bg-lightWarningSoft text-lightWarning dark:border-darkWarningBorder dark:bg-darkWarningSoft dark:text-darkWarning"
21
+ >
22
+ {{ t('Unsaved') }}
23
+ </span>
24
+ <span class="inline-flex h-5 shrink-0 items-center rounded-full px-2 text-xs font-medium bg-lightSecondaryDarken text-lightInputIconColor dark:bg-darkSecondaryLighten dark:text-darkInputIconColor">
25
+ {{ kindLabels[CATEGORY_KIND[row.category]] }}
26
+ </span>
27
+ <Button
28
+ type="button"
29
+ variant="ghost"
30
+ size="sm"
31
+ iconOnly
32
+ :aria-label="openFields.has(row.field) ? t('Hide prompt') : t('Edit prompt')"
33
+ :aria-expanded="openFields.has(row.field)"
34
+ :aria-controls="`${idPrefix}-${row.field}`"
35
+ @click="toggle(row.field)"
36
+ >
37
+ <IconChevronUp v-if="openFields.has(row.field)" aria-hidden="true" />
38
+ <IconPencil v-else aria-hidden="true" />
39
+ </Button>
40
+ </div>
41
+ <div v-if="openFields.has(row.field)" :id="`${idPrefix}-${row.field}`" class="flex flex-col items-start gap-2 px-3.5 pb-3">
42
+ <Textarea
43
+ v-model="prompts[row.category][row.field]"
44
+ rows="4"
45
+ class="resize-y"
46
+ :placeholder="t('Enter prompt instructions...')"
47
+ />
48
+ <div class="flex w-full flex-wrap items-center justify-between gap-2">
49
+ <Button
50
+ type="button"
51
+ variant="ghost"
52
+ size="sm"
53
+ :disabled="prompts[row.category][row.field] === defaultPrompts[row.category][row.field]"
54
+ @click="prompts[row.category][row.field] = defaultPrompts[row.category][row.field]"
55
+ >
56
+ <IconRotateCcw class="w-3.5 h-3.5" aria-hidden="true" />
57
+ {{ t('Reset to default') }}
58
+ </Button>
59
+ <Button
60
+ type="button"
61
+ variant="secondary"
62
+ size="sm"
63
+ :disabled="!isUnsaved(row)"
64
+ @click="emit('save', row.category, row.field)"
65
+ >
66
+ <IconSave class="w-3.5 h-3.5" aria-hidden="true" />
67
+ {{ t('Save prompt') }}
68
+ </Button>
69
+ </div>
70
+ </div>
71
+ </li>
72
+ </ul>
73
+ <p class="text-xs text-lightInputIconColor dark:text-darkInputIconColor">
74
+ {{ t('Saved prompts are kept in this browser, unsaved changes apply to this run only.') }}
75
+ </p>
76
+ </section>
77
+
78
+ <div class="af-overwrite-settings-card rounded-lg border border-lightFormBorder bg-lightForm px-3.5 py-3 dark:border-darkFormBorder dark:bg-darkForm">
79
+ <Toggle v-model="overwrite" class="w-full flex-row-reverse justify-between gap-3 [&>label]:ms-0">
80
+ <span class="flex flex-col">
81
+ <span class="font-medium text-lightDialogHeaderText dark:text-darkDialogHeaderText">{{ t('Overwrite existing values') }}</span>
82
+ <span class="text-xs text-lightInputIconColor dark:text-darkInputIconColor">
83
+ {{ overwrite ? t('AI-generated data will replace existing values.') : t('AI fills only empty fields.') }}
84
+ </span>
85
+ </span>
86
+ </Toggle>
87
+ </div>
88
+
89
+ <div
90
+ v-if="overwrite"
91
+ class="flex items-start gap-2.5 rounded-default border px-3 py-2.5 text-sm border-lightWarningBorder bg-lightWarningSoft text-lightWarning dark:border-darkWarningBorder dark:bg-darkWarningSoft dark:text-darkWarning"
92
+ >
93
+ <IconTriangleAlert class="mt-0.5 w-4 h-4 shrink-0" aria-hidden="true" />
94
+ <p>{{ t('Existing values will be replaced when you save.') }}</p>
95
+ </div>
96
+ </div>
97
+ </template>
98
+
99
+ <script setup lang="ts">
100
+ import { computed, reactive, useId } from 'vue';
101
+ import { useI18n } from 'vue-i18n';
102
+ import { Button, Textarea, Toggle } from '@/afcl';
103
+ import { IconChevronUp, IconPencil, IconRotateCcw, IconSave, IconTriangleAlert } from '@iconify-prerendered/vue-lucide';
104
+ import { CATEGORY_KIND, fieldLabel, type FieldKind, type PromptCategory } from './reviewModel';
105
+
106
+ type Prompts = Record<PromptCategory, Record<string, string>>;
107
+ type PromptRow = { category: PromptCategory, field: string };
108
+
109
+ const props = defineProps<{
110
+ meta: any,
111
+ defaultPrompts: Prompts,
112
+ savedPrompts: Prompts,
113
+ }>();
114
+
115
+ const emit = defineEmits<{
116
+ save: [category: PromptCategory, field: string],
117
+ }>();
118
+
119
+ const prompts = defineModel<Prompts>('prompts', { required: true });
120
+ const overwrite = defineModel<boolean>('overwrite', { required: true });
121
+
122
+ const { t } = useI18n();
123
+ const idPrefix = useId();
124
+ const openFields = reactive(new Set<string>());
125
+
126
+ const kindLabels = computed<Record<FieldKind, string>>(() => ({
127
+ fromImage: t('From image'),
128
+ text: t('Text'),
129
+ image: t('Image'),
130
+ }));
131
+
132
+ // in the order of the columns in the review
133
+ const rows = computed<PromptRow[]>(() => (Object.keys(CATEGORY_KIND) as PromptCategory[]).flatMap((category) =>
134
+ Object.keys(prompts.value[category]).map((field) => ({ category, field })),
135
+ ));
136
+
137
+ function isUnsaved(row: PromptRow): boolean {
138
+ return prompts.value[row.category][row.field] !== props.savedPrompts[row.category][row.field];
139
+ }
140
+
141
+ function toggle(field: string) {
142
+ if (openFields.has(field)) {
143
+ openFields.delete(field);
144
+ } else {
145
+ openFields.add(field);
146
+ }
147
+ }
148
+ </script>
@@ -0,0 +1,45 @@
1
+ <template>
2
+ <Teleport to="body">
3
+ <div
4
+ v-if="src"
5
+ class="af-image-lightbox fixed inset-0 z-[100] flex items-center justify-center bg-black/90 cursor-zoom-out"
6
+ @click="src = null"
7
+ >
8
+ <img :src="src" alt="" class="max-w-[90vw] max-h-[90vh] rounded-lg object-contain select-none" />
9
+ <button
10
+ type="button"
11
+ class="absolute top-4 right-4 flex items-center justify-center w-9 h-9 rounded-full bg-white/10 text-white/70 hover:bg-white/20 hover:text-white focus:outline-none focus-visible:ring-[3px] focus-visible:ring-white/40 transition-colors"
12
+ :aria-label="t('Close')"
13
+ >
14
+ <IconX class="w-4 h-4" aria-hidden="true" />
15
+ </button>
16
+ </div>
17
+ </Teleport>
18
+ </template>
19
+
20
+ <script setup lang="ts">
21
+ import { onBeforeUnmount, watch } from 'vue';
22
+ import { useI18n } from 'vue-i18n';
23
+ import { IconX } from '@iconify-prerendered/vue-lucide';
24
+
25
+ // a photo is dark in both themes, so the lightbox keeps black and white tints instead of tokens
26
+ const src = defineModel<string | null>({ required: true });
27
+
28
+ const { t } = useI18n();
29
+
30
+ function onKeydown(event: KeyboardEvent) {
31
+ if (event.key === 'Escape') {
32
+ src.value = null;
33
+ }
34
+ }
35
+
36
+ watch(src, (value) => {
37
+ if (value) {
38
+ document.addEventListener('keydown', onKeydown);
39
+ } else {
40
+ document.removeEventListener('keydown', onKeydown);
41
+ }
42
+ });
43
+
44
+ onBeforeUnmount(() => document.removeEventListener('keydown', onKeydown));
45
+ </script>
@@ -0,0 +1,276 @@
1
+ <template>
2
+ <div class="af-review-record @container/detail flex min-h-0 flex-col">
3
+ <div class="flex flex-wrap items-center gap-x-3 gap-y-2 border-b border-lightFormBorder px-4 py-3 dark:border-darkFormBorder">
4
+ <div class="min-w-0 flex-1 basis-full @md/detail:basis-0">
5
+ <h4 class="truncate text-sm font-semibold text-lightDialogHeaderText dark:text-darkDialogHeaderText">{{ label }}</h4>
6
+ <p class="text-xs text-lightInputIconColor dark:text-darkInputIconColor">
7
+ {{ t('Record {position} of {total}', { position, total }) }}
8
+ <i18n-t keypath="· {up} {down} to move, {space} to include" tag="span" class="hidden @3xl/review:inline">
9
+ <template #up><kbd :class="KBD_CLASSES">↑</kbd></template>
10
+ <template #down><kbd :class="KBD_CLASSES">↓</kbd></template>
11
+ <template #space><kbd :class="KBD_CLASSES">Space</kbd></template>
12
+ </i18n-t>
13
+ </p>
14
+ </div>
15
+ <Checkbox v-model="record.isChecked">{{ t('Save this record') }}</Checkbox>
16
+ <Button type="button" variant="secondary" size="sm" :disabled="!isSettled" @click="emit('regenerateRecord')">
17
+ <IconRefreshCw class="w-3.5 h-3.5" aria-hidden="true" />
18
+ {{ t('Regenerate') }}
19
+ </Button>
20
+ <div class="flex gap-1">
21
+ <Button type="button" variant="secondary" size="sm" iconOnly :disabled="position === 1" :aria-label="t('Previous record')" @click="emit('prev')">
22
+ <IconChevronUp aria-hidden="true" />
23
+ </Button>
24
+ <Button type="button" variant="secondary" size="sm" iconOnly :disabled="position === total" :aria-label="t('Next record')" @click="emit('next')">
25
+ <IconChevronDown aria-hidden="true" />
26
+ </Button>
27
+ </div>
28
+ </div>
29
+
30
+ <div class="min-h-0 flex-1 overflow-y-auto">
31
+ <div v-if="record.status === 'pending'" class="flex h-full flex-col items-center justify-center gap-2 p-6 text-center">
32
+ <IconClock class="w-6 h-6 text-lightInputPlaceholderText dark:text-darkInputPlaceholderText" aria-hidden="true" />
33
+ <p class="text-sm text-lightInputIconColor dark:text-darkInputIconColor">{{ t('This record is waiting for its turn.') }}</p>
34
+ </div>
35
+
36
+ <template v-else>
37
+ <div class="hidden gap-3 border-b border-lightFormBorder bg-lightFormHeading px-4 py-2 text-xs font-medium text-lightInputIconColor dark:border-darkFormBorder dark:bg-darkFormHeading dark:text-darkInputIconColor @lg/detail:grid @lg/detail:grid-cols-[minmax(0,1fr)_1rem_minmax(0,1fr)]">
38
+ <span>{{ t('Current') }}</span>
39
+ <span></span>
40
+ <span>{{ t('Generated') }}</span>
41
+ </div>
42
+
43
+ <div v-if="meta.isAttachFiles" class="flex flex-col gap-2 border-b border-lightFormBorder px-4 py-3 dark:border-darkFormBorder">
44
+ <span class="text-sm font-medium text-lightDialogHeaderText dark:text-darkDialogHeaderText">{{ t('Source images') }}</span>
45
+ <div v-if="record.images.length" class="flex flex-wrap gap-2">
46
+ <button
47
+ v-for="(image, index) in record.images"
48
+ :key="image"
49
+ type="button"
50
+ :class="IMAGE_BUTTON_CLASSES"
51
+ :aria-label="t('Open source image {number}', { number: index + 1 })"
52
+ @click="emit('zoom', image)"
53
+ >
54
+ <img :src="image" alt="" class="h-16 w-24 object-cover" />
55
+ </button>
56
+ </div>
57
+ <Skeleton v-else-if="record.status === 'processing'" type="image" class="h-16 w-24 rounded-default" />
58
+ <p v-else class="text-sm text-lightInputPlaceholderText dark:text-darkInputPlaceholderText">{{ t('No source images') }}</p>
59
+ </div>
60
+
61
+ <div
62
+ v-for="field in meta.outputFields"
63
+ :key="field"
64
+ class="flex flex-col gap-2 border-b border-lightFormBorder px-4 py-3 last:border-b-0 dark:border-darkFormBorder"
65
+ >
66
+ <div class="flex items-center gap-2">
67
+ <span class="text-sm font-medium text-lightDialogHeaderText dark:text-darkDialogHeaderText">{{ fieldLabel(meta, field) }}</span>
68
+ <span v-if="!isFieldGenerating(record, meta, field)" :class="[BADGE_CLASSES, badgeClasses(field)]">{{ badgeLabel(field) }}</span>
69
+ <span class="flex-1"></span>
70
+ <Tooltip>
71
+ <Button
72
+ type="button"
73
+ variant="ghost"
74
+ size="sm"
75
+ iconOnly
76
+ :loader="record.regeneratingFieldsStatus[field] === true"
77
+ :disabled="!isSettled || isFieldGenerating(record, meta, field)"
78
+ :aria-label="t('Regenerate {field}', { field: fieldLabel(meta, field) })"
79
+ @click="regenerate(field)"
80
+ >
81
+ <IconRefreshCw aria-hidden="true" />
82
+ </Button>
83
+ <template #tooltip>{{ t('Regenerate') }}</template>
84
+ </Tooltip>
85
+ </div>
86
+
87
+ <div class="grid grid-cols-1 items-start gap-2 @lg/detail:grid-cols-[minmax(0,1fr)_1rem_minmax(0,1fr)] @lg/detail:gap-3">
88
+ <div class="flex min-w-0 flex-col gap-1">
89
+ <span class="text-xs text-lightInputIconColor dark:text-darkInputIconColor @lg/detail:hidden">{{ t('Current') }}</span>
90
+ <template v-if="fieldKind(meta, field) === 'image'">
91
+ <button
92
+ v-if="record.oldImagePreviews[field]"
93
+ type="button"
94
+ :class="IMAGE_BUTTON_CLASSES"
95
+ :aria-label="t('Open current image')"
96
+ @click="emit('zoom', record.oldImagePreviews[field])"
97
+ >
98
+ <img :src="record.oldImagePreviews[field]" alt="" class="aspect-[16/10] w-full object-cover" />
99
+ </button>
100
+ <div v-else :class="IMAGE_PLACEHOLDER_CLASSES">
101
+ <IconImageOff class="w-5 h-5" aria-hidden="true" />
102
+ {{ t('No image') }}
103
+ </div>
104
+ </template>
105
+ <div
106
+ v-else
107
+ class="min-h-[2.125rem] max-h-40 overflow-y-auto whitespace-pre-wrap break-words rounded-default bg-lightSecondaryDarken px-2.5 py-1.5 text-sm leading-5 text-lightInputIconColor dark:bg-darkSecondaryLighten dark:text-darkInputIconColor"
108
+ >
109
+ <span v-if="isEmpty(record.oldData[field])" class="text-lightInputPlaceholderText dark:text-darkInputPlaceholderText">-</span>
110
+ <template v-else>{{ displayValue(field, record.oldData[field]) }}</template>
111
+ </div>
112
+ </div>
113
+
114
+ <IconArrowRight class="mt-2.5 hidden w-4 h-4 text-lightInputPlaceholderText dark:text-darkInputPlaceholderText @lg/detail:block" aria-hidden="true" />
115
+
116
+ <div class="flex min-w-0 flex-col gap-1">
117
+ <span class="text-xs text-lightInputIconColor dark:text-darkInputIconColor @lg/detail:hidden">{{ t('Generated') }}</span>
118
+ <template v-if="fieldKind(meta, field) === 'image'">
119
+ <Skeleton v-if="isFieldGenerating(record, meta, field)" type="image" class="aspect-[16/10] w-full rounded-lg" />
120
+ <button
121
+ v-else-if="generatedImageUrl(record, meta, field)"
122
+ type="button"
123
+ :class="IMAGE_BUTTON_CLASSES"
124
+ :aria-label="t('Open generated image')"
125
+ @click="emit('zoom', generatedImageUrl(record, meta, field))"
126
+ >
127
+ <img :src="generatedImageUrl(record, meta, field)" alt="" class="aspect-[16/10] w-full object-cover" />
128
+ </button>
129
+ <div v-else :class="IMAGE_PLACEHOLDER_CLASSES">
130
+ <IconImageOff class="w-5 h-5" aria-hidden="true" />
131
+ {{ t('Not generated') }}
132
+ </div>
133
+ </template>
134
+ <Skeleton
135
+ v-else-if="isFieldGenerating(record, meta, field)"
136
+ type="input"
137
+ class="w-full"
138
+ :class="editorKind(field) === 'multiline' ? 'h-24' : 'h-[2.125rem]'"
139
+ />
140
+ <Select
141
+ v-else-if="editorKind(field) === 'enum'"
142
+ v-model="record.data[field]"
143
+ class="w-full"
144
+ :options="enumOptions(field)"
145
+ teleportToTop
146
+ />
147
+ <div v-else-if="editorKind(field) === 'boolean'" class="flex h-[2.125rem] items-center">
148
+ <Toggle v-model="record.data[field]" />
149
+ </div>
150
+ <Input v-else-if="editorKind(field) === 'number'" v-model="record.data[field]" type="number" fullWidth />
151
+ <Input v-else-if="editorKind(field) === 'line'" v-model="record.data[field]" type="text" fullWidth />
152
+ <Textarea
153
+ v-else
154
+ :modelValue="record.data[field] ?? ''"
155
+ rows="4"
156
+ class="resize-y"
157
+ @update:modelValue="(value) => record.data[field] = value"
158
+ />
159
+ </div>
160
+ </div>
161
+
162
+ <p v-if="fieldError(record, meta, field)" class="flex items-start gap-1.5 text-xs text-lightDanger dark:text-darkDanger">
163
+ <IconCircleAlert class="mt-px w-3.5 h-3.5 shrink-0" aria-hidden="true" />
164
+ {{ fieldError(record, meta, field) }}
165
+ </p>
166
+ </div>
167
+ </template>
168
+ </div>
169
+ </div>
170
+ </template>
171
+
172
+ <script setup lang="ts">
173
+ import { computed } from 'vue';
174
+ import { useI18n } from 'vue-i18n';
175
+ import { Button, Checkbox, Input, Select, Skeleton, Textarea, Toggle, Tooltip } from '@/afcl';
176
+ import {
177
+ IconArrowRight, IconChevronDown, IconChevronUp, IconCircleAlert, IconClock, IconImageOff, IconRefreshCw,
178
+ } from '@iconify-prerendered/vue-lucide';
179
+ import {
180
+ fieldChange, fieldError, fieldKind, fieldLabel, generatedImageUrl, isFieldGenerating, isRecordBusy, type RecordState,
181
+ } from './reviewModel';
182
+
183
+ type EditorKind = 'enum' | 'boolean' | 'number' | 'line' | 'multiline';
184
+
185
+ const NUMBER_TYPES = ['integer', 'decimal', 'float'];
186
+ const KBD_CLASSES = 'rounded border border-lightFormBorder px-1 font-mono text-[11px] dark:border-darkFormBorder';
187
+ const BADGE_CLASSES = 'inline-flex h-5 items-center rounded-full border px-2 text-xs font-medium';
188
+ const IMAGE_BUTTON_CLASSES = 'block overflow-hidden rounded-lg border border-lightFormBorder cursor-zoom-in hover:border-lightPrimary focus:outline-none focus-visible:ring-[3px] focus-visible:ring-lightFocusRing dark:border-darkFormBorder dark:hover:border-darkPrimary dark:focus-visible:ring-darkFocusRing transition-colors';
189
+ const IMAGE_PLACEHOLDER_CLASSES = 'flex aspect-[16/10] w-full flex-col items-center justify-center gap-1 rounded-lg border border-dashed border-lightFormBorder bg-lightFormHeading text-xs text-lightInputPlaceholderText dark:border-darkFormBorder dark:bg-darkFormHeading dark:text-darkInputPlaceholderText';
190
+
191
+ const props = defineProps<{
192
+ record: RecordState,
193
+ meta: any,
194
+ label: string,
195
+ position: number,
196
+ total: number,
197
+ }>();
198
+
199
+ const emit = defineEmits<{
200
+ prev: [],
201
+ next: [],
202
+ regenerateRecord: [],
203
+ regenerateImages: [],
204
+ regenerateField: [field: string],
205
+ zoom: [url: string],
206
+ }>();
207
+
208
+ const { t } = useI18n();
209
+
210
+ const isSettled = computed(() => !isRecordBusy(props.record));
211
+
212
+ function columnEnum(field: string) {
213
+ return props.meta.columnEnums.find((column: any) => column.name === field);
214
+ }
215
+
216
+ function editorKind(field: string): EditorKind {
217
+ if (columnEnum(field)) {
218
+ return 'enum';
219
+ }
220
+ const type = props.meta.columnTypes[field];
221
+ if (type === 'boolean') {
222
+ return 'boolean';
223
+ }
224
+ if (NUMBER_TYPES.includes(type)) {
225
+ return 'number';
226
+ }
227
+ return type === 'string' ? 'line' : 'multiline';
228
+ }
229
+
230
+ function enumOptions(field: string) {
231
+ return columnEnum(field).enum.map((item: any) => ({ label: item.label, value: item.value }));
232
+ }
233
+
234
+ function isEmpty(value: any) {
235
+ return value === null || value === undefined || value === '';
236
+ }
237
+
238
+ function displayValue(field: string, value: any): string {
239
+ const option = columnEnum(field)?.enum.find((item: any) => item.value === value);
240
+ if (option) {
241
+ return option.label;
242
+ }
243
+ if (typeof value === 'boolean') {
244
+ return value ? t('Yes') : t('No');
245
+ }
246
+ return typeof value === 'object' ? JSON.stringify(value) : String(value);
247
+ }
248
+
249
+ function badgeLabel(field: string): string {
250
+ if (fieldError(props.record, props.meta, field)) {
251
+ return t('Failed');
252
+ }
253
+ return {
254
+ new: t('New'),
255
+ changed: t('Changed'),
256
+ same: t('Unchanged'),
257
+ }[fieldChange(props.record, props.meta, field)];
258
+ }
259
+
260
+ function badgeClasses(field: string): string {
261
+ if (fieldError(props.record, props.meta, field)) {
262
+ return 'border-lightDangerBorder bg-lightDangerSoft text-lightDanger dark:border-darkDangerBorder dark:bg-darkDangerSoft dark:text-darkDanger';
263
+ }
264
+ return fieldChange(props.record, props.meta, field) === 'same'
265
+ ? 'border-transparent bg-lightSecondaryDarken text-lightInputIconColor dark:bg-darkSecondaryLighten dark:text-darkInputIconColor'
266
+ : 'border-lightPrimary/30 bg-lightPrimaryOpacity text-lightPrimary dark:border-darkPrimary/40 dark:bg-darkPrimaryOpacity dark:text-darkPrimary';
267
+ }
268
+
269
+ function regenerate(field: string) {
270
+ if (fieldKind(props.meta, field) === 'image') {
271
+ emit('regenerateImages');
272
+ } else {
273
+ emit('regenerateField', field);
274
+ }
275
+ }
276
+ </script>