Skip to content

Commit 069661b

Browse files
authored
Merge pull request #12 from devforth/feature/AdminForth/1281/let's-add-hard-limit-to-transl
Feature/admin forth/1281/let's add hard limit to transl
2 parents 86acd4b + 17657cb commit 069661b

6 files changed

Lines changed: 574 additions & 141 deletions

File tree

custom/BulkActionButton.vue

Lines changed: 59 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
:buttons="[
55
{
66
label: 'Translate',
7-
onclick: (dialog) => { runTranslation(); dialog.hide(); } ,
7+
onclick: async (dialog) => { await runTranslation(); dialog.hide(); } ,
88
options: {
99
disabled: noneChecked
1010
}
@@ -19,20 +19,21 @@
1919
]"
2020
>
2121
<template #trigger>
22-
<button
23-
v-if="checkboxes.length > 0"
24-
class="flex gap-1 items-center py-1 px-3 text-sm font-medium text-lightListViewButtonText focus:outline-none bg-lightListViewButtonBackground rounded-default border border-lightListViewButtonBorder hover:bg-lightListViewButtonBackgroundHover hover:text-lightListViewButtonTextHover focus:z-10 focus:ring-4 focus:ring-lightListViewButtonFocusRing dark:focus:ring-darkListViewButtonFocusRing dark:bg-darkListViewButtonBackground dark:text-darkListViewButtonText dark:border-darkListViewButtonBorder dark:hover:text-darkListViewButtonTextHover dark:hover:bg-darkListViewButtonBackgroundHover"
25-
>
26-
<IconLanguageOutline class="w-5 h-5" />
27-
{{ t('Translate Selected') }} {{ `(${checkboxes.length})` }}
28-
<div class="text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-purple-300 dark:focus:ring-purple-800
29-
font-medium rounded-sm text-xs px-1 ml-1 text-center ">
30-
AI
22+
<button class="flex items-center justify-center w-full">
23+
<IconLanguageOutline class="text-gray-500 dark:text-gray-400 w-5 h-5" />
24+
<div class="flex items-end justify-start gap-2 cursor-pointer">
25+
<p class="text-justify max-h-[18px] truncate max-w-[60vw] md:max-w-none">{{ t('Translate filtered') }}</p>
26+
<div class="flex items-center justify-center text-white bg-gradient-to-r h-[18px] from-purple-500 via-purple-600 to-purple-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-purple-300 dark:focus:ring-purple-800 font-medium rounded-md text-sm px-1 text-center">
27+
{{t('AI')}}
28+
</div>
3129
</div>
3230
</button>
3331
</template>
3432

3533
<div class="af-i18n-translations-selector grid grid-cols-2 gap-1 w-full">
34+
<div v-if="isLoading" class="top-0 left-0 z-10 absolute bg-black/30 w-full h-full rounded-lg flex items-center justify-center">
35+
<Spinner class="w-10 h-10" />
36+
</div>
3637
<Button @click="selectAll" :disabled="allChecked">{{ t('Select All') }}</Button>
3738
<Button @click="uncheckAll" :disabled="noneChecked">{{ t('Uncheck All') }}</Button>
3839
<div class="col-span-2 grid grid-cols-3 gap-1 mt-4">
@@ -52,14 +53,18 @@
5253
<script setup lang="ts">
5354
import { IconLanguageOutline } from '@iconify-prerendered/vue-flowbite';
5455
import { useI18n } from 'vue-i18n';
55-
import { Dialog, Button, Checkbox } from '@/afcl';
56-
import { computed, onMounted, ref, watch } from 'vue';
56+
import { Dialog, Button, Checkbox, Spinner } from '@/afcl';
57+
import { computed, onMounted, ref, onUnmounted } from 'vue';
5758
import { callAdminForthApi } from '@/utils';
5859
import { useAdminforth } from '@/adminforth';
5960
import { getCountryCodeFromLangCode } from './langCommon';
6061
import { getName, overwrite } from 'country-list';
6162
import ISO6391 from 'iso-639-1';
6263
import 'flag-icon-css/css/flag-icons.min.css';
64+
import websocket from '@/websocket';
65+
import { useFiltersStore } from '@/stores/filters';
66+
67+
const filtersStore = useFiltersStore();
6368
6469
const { t } = useI18n();
6570
const adminforth = useAdminforth();
@@ -80,14 +85,22 @@
8085
}>();
8186
8287
const checkedLanguages = ref<Record<string, boolean>>({});
88+
const isLoading = ref(false);
8389
const allChecked = computed(() => Object.values(checkedLanguages.value).every(Boolean));
8490
const noneChecked = computed(() => Object.values(checkedLanguages.value).every(value => !value));
8591
8692
onMounted(() => {
93+
websocket.subscribe('/translation_progress', (data) => {
94+
adminforth.list.refresh();
95+
});
8796
for (const lang of props.meta.supportedLanguages) {
8897
checkedLanguages.value[lang] = true;
8998
}
9099
});
100+
101+
onUnmounted( () => {
102+
websocket.unsubscribe('/translation_progress');
103+
} )
91104
92105
function selectAll() {
93106
for (const lang of props.meta.supportedLanguages) {
@@ -106,27 +119,58 @@
106119
}
107120
108121
async function runTranslation() {
122+
isLoading.value = true;
123+
const listOfIds = await getListOfIds();
109124
try {
110125
const res = await callAdminForthApi({
111126
path: `/plugin/${props.meta.pluginInstanceId}/translate-selected-to-languages`,
112127
method: 'POST',
113128
body: {
114-
selectedIds: props.checkboxes,
129+
selectedIds: listOfIds,
115130
selectedLanguages: Object.keys(checkedLanguages.value).filter(lang => checkedLanguages.value[lang]),
116131
},
117132
silentError: true,
118133
});
119-
adminforth.list.refresh();
120134
props.clearCheckboxes();
121135
if (res.ok) {
122-
adminforth.alert({ message: res.successMessage, variant: 'success' });
136+
adminforth.alert({ message: `Running translation job`, variant: 'success' });
137+
console.log('Received record IDs for filtered selector:', res);
138+
const jobId = res.jobId;
139+
if (jobId) {
140+
console.log('Opening job info popup for jobId:', jobId);
141+
//@ts-ignore
142+
window.OpenJobInfoPopup(jobId);
143+
}
123144
} else {
124145
adminforth.alert({ message: res.errorMessage || t('Failed to translate selected items. Please, try again.'), variant: 'danger' });
125146
}
126147
} catch (e) {
127148
console.error('Failed to translate selected items:', e);
128149
adminforth.alert({ message: t('Failed to translate selected items. Please, try again.'), variant: 'danger' });
129150
}
151+
isLoading.value = false;
152+
}
153+
154+
async function getListOfIds() {
155+
const filters = filtersStore.getFilters();
156+
let res;
157+
try {
158+
res = await callAdminForthApi({
159+
path: `/plugin/${props.meta.pluginInstanceId}/get_filtered_ids`,
160+
method: 'POST',
161+
body: { filters },
162+
silentError: true,
163+
});
164+
} catch (e) {
165+
console.error('Failed to get records for filtered selector:', e);
166+
return [];
167+
}
168+
if (!res?.ok || !res?.recordIds) {
169+
console.error('Failed to get records for filtered selector, response error:', res);
170+
return [];
171+
}
172+
return res.recordIds;
130173
}
174+
131175
132176
</script>
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
2+
<template>
3+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 my-3">
4+
<div class="flex items-center space-x-1">
5+
<span class=" text-gray-500">{{ t('Total tokens will be used for translation:') }}</span>
6+
<span class="text-lg font-semibold text-gray-900 dark:text-gray-100">{{ new Number(props.job.state?.totalTranslationTokenCost).toLocaleString() || 0 }}</span>
7+
</div>
8+
<div class="flex items-center space-x-1">
9+
<span class=" text-gray-500">{{ t('Total translation token used:') }}</span>
10+
<span class="text-lg font-semibold text-gray-900 dark:text-gray-100">{{ new Number(props.job.state?.totalUsedTokens).toLocaleString() || 0 }}</span>
11+
</div>
12+
</div>
13+
<div class="grid grid-cols-3 gap-2">
14+
<div class="bg-gray-50 hover:bg-gray-100 transition-all px-2 py-2 rounded-md border max-w-64 w-full flex items-center gap-2" v-for="(task, index) in translationTasks" :key="index">
15+
{{ task.state?.taskName }} to
16+
<span class="flag-icon"
17+
:class="`flag-icon-${getCountryCodeFromLangCode(task.state?.lang)}`"
18+
></span>
19+
<component
20+
:is="getCustomComponent({file: '@@/plugins/BackgroundJobsPlugin/StateToIcon.vue'})"
21+
:status="task.status"
22+
/>
23+
</div>
24+
</div>
25+
</template>
26+
27+
28+
<script setup lang="ts">
29+
import { AdminForthComponentDeclarationFull } from 'adminforth';
30+
import { useI18n } from 'vue-i18n'
31+
import { onMounted, onUnmounted, ref } from 'vue';
32+
import websocket from '@/websocket';
33+
import { getCountryCodeFromLangCode } from './langCommon';
34+
import { getCustomComponent } from '@/utils';
35+
36+
const { t } = useI18n();
37+
38+
const translationTasks = ref<{state: Record<string, any>, status: string}[]>([]);
39+
const currentPaginationWindow = ref({limit: 25, offset: 0});
40+
41+
const props = defineProps<{
42+
meta: any;
43+
getJobTasks: (limit?: number, offset?: number) => Promise<
44+
{tasks: {state: Record<string, any>, status: string}[], total: number}>;
45+
job: {
46+
id: string;
47+
name: string;
48+
status: 'IN_PROGRESS' | 'DONE' | 'DONE_WITH_ERRORS' | 'CANCELLED';
49+
state: {
50+
totalTranslationTokenCost: number;
51+
totalUsedTokens: number;
52+
};
53+
progress: number; // 0 to 100
54+
createdAt: Date;
55+
customComponent?: AdminForthComponentDeclarationFull;
56+
};
57+
}>();
58+
59+
onMounted(async () => {
60+
const {tasks, total} = await props.getJobTasks(currentPaginationWindow.value.limit, currentPaginationWindow.value.offset);
61+
translationTasks.value = tasks;
62+
63+
websocket.subscribe(`/background-jobs-task-update/${props.job.id}`, (data: { taskIndex: number, status?: string, state?: Record<string, any> }) => {
64+
if ( data.taskIndex <= currentPaginationWindow.value.offset + currentPaginationWindow.value.limit && data.taskIndex >= currentPaginationWindow.value.offset ) {
65+
if (data.state) {
66+
translationTasks.value[data.taskIndex].state = data.state;
67+
}
68+
if (data.status) {
69+
translationTasks.value[data.taskIndex].status = data.status;
70+
}
71+
}
72+
});
73+
74+
75+
});
76+
77+
onUnmounted(() => {
78+
websocket.unsubscribe(`/background-jobs-task-update/${props.job.id}`);
79+
});
80+
81+
82+
</script>

0 commit comments

Comments
 (0)