mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-05-26 03:30:26 -04:00
feat: In-app AI Provider Configuration (#7650)
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
<br>
|
||||
<DocLink
|
||||
class="mt-2"
|
||||
link="/documentation/getting-started/installation/open-ai"
|
||||
link="/documentation/getting-started/installation/ai-providers"
|
||||
/>
|
||||
</BaseCardSectionTitle>
|
||||
</v-container>
|
||||
@@ -17,6 +17,36 @@
|
||||
<div>
|
||||
<v-card-text>
|
||||
<v-container class="pa-0">
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-if="groups"
|
||||
v-model="selectedGroupId"
|
||||
:items="groups"
|
||||
item-title="name"
|
||||
item-value="id"
|
||||
:label="$t('group.group')"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
clearable
|
||||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="selectedProviderId"
|
||||
:items="groupProviders"
|
||||
item-title="name"
|
||||
item-value="id"
|
||||
:label="$t('group.ai-provider-settings.ai-provider')"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
clearable
|
||||
hide-details
|
||||
:disabled="!selectedGroupId"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col
|
||||
cols="auto"
|
||||
@@ -61,6 +91,7 @@
|
||||
<v-card-actions>
|
||||
<BaseButton
|
||||
type="submit"
|
||||
:disabled="!selectedProviderId"
|
||||
:text="$t('admin.run-test')"
|
||||
:icon="$globals.icons.check"
|
||||
:loading="loading"
|
||||
@@ -85,7 +116,9 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAdminApi } from "~/composables/api";
|
||||
import { useGroups } from "~/composables/use-groups";
|
||||
import { alert } from "~/composables/use-toast";
|
||||
import type { AIProviderSummary } from "~/lib/api/types/group";
|
||||
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
@@ -106,10 +139,24 @@ const uploadedImage = ref<Blob | File>();
|
||||
const uploadedImageName = ref<string>("");
|
||||
const uploadedImagePreviewUrl = ref<string>();
|
||||
|
||||
function uploadImage(fileObject: File) {
|
||||
uploadedImage.value = fileObject;
|
||||
uploadedImageName.value = fileObject.name;
|
||||
uploadedImagePreviewUrl.value = URL.createObjectURL(fileObject);
|
||||
// Group + provider selection
|
||||
const { groups } = useGroups();
|
||||
const selectedGroupId = ref<string | null>(null);
|
||||
const groupProviders = ref<AIProviderSummary[]>([]);
|
||||
const selectedProviderId = ref<string | null>(null);
|
||||
|
||||
watch(selectedGroupId, (id) => {
|
||||
groupProviders.value = [];
|
||||
selectedProviderId.value = null;
|
||||
if (!id) return;
|
||||
const group = groups.value?.find(g => g.id === id);
|
||||
groupProviders.value = group?.aiProviderSettings?.providers ?? [];
|
||||
});
|
||||
|
||||
function uploadImage(fileObject: unknown) {
|
||||
uploadedImage.value = fileObject as File;
|
||||
uploadedImageName.value = (fileObject as File).name;
|
||||
uploadedImagePreviewUrl.value = URL.createObjectURL(fileObject as File);
|
||||
}
|
||||
|
||||
function clearImage() {
|
||||
@@ -119,10 +166,15 @@ function clearImage() {
|
||||
}
|
||||
|
||||
async function testOpenAI() {
|
||||
if (!selectedProviderId.value) {
|
||||
alert.error("Please select a provider");
|
||||
return;
|
||||
}
|
||||
|
||||
response.value = "";
|
||||
|
||||
loading.value = true;
|
||||
const { data } = await api.debug.debugOpenAI(uploadedImage.value);
|
||||
const { data } = await api.debug.debugOpenAI(selectedProviderId.value, uploadedImage.value);
|
||||
loading.value = false;
|
||||
|
||||
if (!data) {
|
||||
|
||||
@@ -33,6 +33,13 @@
|
||||
v-if="group.preferences"
|
||||
v-model="group.preferences"
|
||||
/>
|
||||
<GroupAIProviderSettingsEditor
|
||||
v-if="group.aiProviderSettings"
|
||||
v-model="group.aiProviderSettings"
|
||||
@create="handleCreateProvider"
|
||||
@update="handleUpdateProvider"
|
||||
@delete="handleDeleteProvider"
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<div class="d-flex pa-2">
|
||||
@@ -50,8 +57,10 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import GroupPreferencesEditor from "~/components/Domain/Group/GroupPreferencesEditor.vue";
|
||||
import GroupAIProviderSettingsEditor from "~/components/Domain/Group/GroupAIProviderSettingsEditor.vue";
|
||||
import { useAdminApi } from "~/composables/api";
|
||||
import { alert } from "~/composables/use-toast";
|
||||
import type { AIProviderCreate, AIProviderUpdate } from "~/lib/api/types/group";
|
||||
import type { VForm } from "vuetify/components";
|
||||
|
||||
definePageMeta({
|
||||
@@ -72,7 +81,7 @@ const adminApi = useAdminApi();
|
||||
|
||||
const userError = ref(false);
|
||||
|
||||
const { data: group } = useLazyAsyncData(`get-household-${groupId.value}`, async () => {
|
||||
const { data: group, refresh } = useLazyAsyncData(`get-household-${groupId.value}`, async () => {
|
||||
if (!groupId.value) {
|
||||
return null;
|
||||
}
|
||||
@@ -86,7 +95,7 @@ const { data: group } = useLazyAsyncData(`get-household-${groupId.value}`, async
|
||||
}, { watch: [groupId] });
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!refGroupEditForm.value?.validate() || group.value === null) {
|
||||
if (!refGroupEditForm.value?.validate() || !group.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -103,4 +112,40 @@ async function handleSubmit() {
|
||||
alert.error(i18n.t("settings.settings-update-failed"));
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCreateProvider(data: AIProviderCreate) {
|
||||
if (!group.value) return;
|
||||
const result = await adminApi.aiProviders.createProvider(group.value.id, data);
|
||||
if (result.data) {
|
||||
await refresh();
|
||||
alert.success(i18n.t("group.ai-provider-settings.provider-created"));
|
||||
}
|
||||
else {
|
||||
alert.error(i18n.t("group.ai-provider-settings.provider-create-failed"));
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUpdateProvider(id: string, data: AIProviderUpdate) {
|
||||
if (!group.value) return;
|
||||
const result = await adminApi.aiProviders.updateProvider(group.value.id, id, data);
|
||||
if (result.data) {
|
||||
await refresh();
|
||||
alert.success(i18n.t("group.ai-provider-settings.provider-updated"));
|
||||
}
|
||||
else {
|
||||
alert.error(i18n.t("group.ai-provider-settings.provider-update-failed"));
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteProvider(id: string) {
|
||||
if (!group.value) return;
|
||||
const result = await adminApi.aiProviders.deleteProvider(group.value.id, id);
|
||||
if (result.data) {
|
||||
await refresh();
|
||||
alert.success(i18n.t("group.ai-provider-settings.provider-deleted"));
|
||||
}
|
||||
else {
|
||||
alert.error(i18n.t("group.ai-provider-settings.provider-delete-failed"));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -45,6 +45,14 @@
|
||||
:title="$t('settings.site-settings')"
|
||||
/>
|
||||
<v-divider />
|
||||
<v-stepper-item
|
||||
:value="Pages.AI_PROVIDERS"
|
||||
:icon="$globals.icons.robot"
|
||||
:complete="currentPage > Pages.AI_PROVIDERS"
|
||||
:color="getStepperColor(currentPage, Pages.AI_PROVIDERS)"
|
||||
:title="$t('group.ai-provider-settings.ai-providers')"
|
||||
/>
|
||||
<v-divider />
|
||||
<v-stepper-item
|
||||
:value="Pages.CONFIRM"
|
||||
:icon="$globals.icons.chefHat"
|
||||
@@ -173,6 +181,43 @@
|
||||
</v-stepper-actions>
|
||||
</v-stepper-window-item>
|
||||
|
||||
<!-- AI PROVIDERS -->
|
||||
<v-stepper-window-item :value="Pages.AI_PROVIDERS">
|
||||
<v-container max-width="880">
|
||||
<v-card-title class="headline pa-0">
|
||||
{{ $t('group.ai-provider-settings.ai-providers') }}
|
||||
</v-card-title>
|
||||
<v-card-subtitle class="px-0 py-2 text-wrap">
|
||||
{{ $t('group.ai-provider-settings.ai-providers-description') }}
|
||||
</v-card-subtitle>
|
||||
<GroupAIProviderSettingsEditor
|
||||
v-if="group?.aiProviderSettings"
|
||||
v-model="group.aiProviderSettings"
|
||||
hide-header
|
||||
class="mt-4"
|
||||
@create="handleCreateProvider"
|
||||
@update="handleUpdateProvider"
|
||||
@delete="handleDeleteProvider"
|
||||
/>
|
||||
</v-container>
|
||||
<v-stepper-actions
|
||||
:disabled="isSubmitting"
|
||||
prev-text="general.back"
|
||||
@click:prev="onPrev"
|
||||
>
|
||||
<template #next>
|
||||
<v-btn
|
||||
variant="flat"
|
||||
color="success"
|
||||
:disabled="isSubmitting"
|
||||
:loading="isSubmitting"
|
||||
:text="$t('general.next')"
|
||||
@click="onNext"
|
||||
/>
|
||||
</template>
|
||||
</v-stepper-actions>
|
||||
</v-stepper-window-item>
|
||||
|
||||
<!-- CONFIRMATION -->
|
||||
<v-stepper-window-item :value="Pages.CONFIRM">
|
||||
<v-container max-width="880">
|
||||
@@ -252,7 +297,11 @@ import { useLocales } from "~/composables/use-locales";
|
||||
import { alert } from "~/composables/use-toast";
|
||||
import { useUserRegistrationForm } from "~/composables/use-users/user-registration-form";
|
||||
import { useCommonSettingsForm } from "~/composables/use-setup/common-settings-form";
|
||||
import { useGroupSelf } from "~/composables/use-groups";
|
||||
import { useAIProviders } from "~/composables/use-ai-providers";
|
||||
import UserRegistrationForm from "~/components/Domain/User/UserRegistrationForm.vue";
|
||||
import GroupAIProviderSettingsEditor from "~/components/Domain/Group/GroupAIProviderSettingsEditor.vue";
|
||||
import type { AIProviderCreate, AIProviderUpdate } from "~/lib/api/types/group";
|
||||
|
||||
definePageMeta({
|
||||
layout: "blank",
|
||||
@@ -267,6 +316,42 @@ const userApi = useUserApi();
|
||||
const adminApi = useAdminApi();
|
||||
|
||||
const groupSlug = computed(() => auth.user.value?.groupSlug);
|
||||
|
||||
const { group, actions: groupActions } = useGroupSelf();
|
||||
const { createOne, updateOne, deleteOne } = useAIProviders();
|
||||
|
||||
async function handleCreateProvider(data: AIProviderCreate) {
|
||||
const result = await createOne(data);
|
||||
if (result.data) {
|
||||
await groupActions.refresh();
|
||||
alert.success(i18n.t("group.ai-provider-settings.provider-created"));
|
||||
}
|
||||
else {
|
||||
alert.error(i18n.t("group.ai-provider-settings.provider-create-failed"));
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUpdateProvider(id: string, data: AIProviderUpdate) {
|
||||
const result = await updateOne(id, data);
|
||||
if (result.data) {
|
||||
await groupActions.refresh();
|
||||
alert.success(i18n.t("group.ai-provider-settings.provider-updated"));
|
||||
}
|
||||
else {
|
||||
alert.error(i18n.t("group.ai-provider-settings.provider-update-failed"));
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteProvider(id: string) {
|
||||
const result = await deleteOne(id);
|
||||
if (result.data) {
|
||||
await groupActions.refresh();
|
||||
alert.success(i18n.t("group.ai-provider-settings.provider-deleted"));
|
||||
}
|
||||
else {
|
||||
alert.error(i18n.t("group.ai-provider-settings.provider-delete-failed"));
|
||||
}
|
||||
}
|
||||
const { locale } = useLocales();
|
||||
const router = useRouter();
|
||||
const isSubmitting = ref(false);
|
||||
@@ -281,8 +366,9 @@ enum Pages {
|
||||
LANDING = 1,
|
||||
USER_INFO = 2,
|
||||
PAGE_2 = 3,
|
||||
CONFIRM = 4,
|
||||
END = 5,
|
||||
AI_PROVIDERS = 4,
|
||||
CONFIRM = 5,
|
||||
END = 6,
|
||||
}
|
||||
|
||||
function getStepperColor(currentPage: Pages, page: Pages) {
|
||||
@@ -475,6 +561,7 @@ async function submitAll() {
|
||||
const tasks = [
|
||||
submitRegistration(),
|
||||
submitCommonSettings(),
|
||||
groupActions.updateAIProviderSettings(),
|
||||
];
|
||||
|
||||
await Promise.all(tasks);
|
||||
|
||||
@@ -284,7 +284,6 @@ const appConfig = ref<CheckApp>({
|
||||
isUpToDate: false,
|
||||
ldapReady: false,
|
||||
oidcReady: false,
|
||||
enableOpenai: false,
|
||||
});
|
||||
function isLocalHostOrHttps() {
|
||||
return window.location.hostname === "localhost" || window.location.protocol === "https:";
|
||||
@@ -351,15 +350,6 @@ const simpleChecks = computed<SimpleCheck[]>(() => {
|
||||
color: appConfig.value.oidcReady ? goodColor : warningColor,
|
||||
icon: appConfig.value.oidcReady ? goodIcon : warningIcon,
|
||||
},
|
||||
{
|
||||
id: "openai-ready",
|
||||
text: appConfig.value.enableOpenai ? i18n.t("settings.openai-ready") : i18n.t("settings.openai-not-ready"),
|
||||
status: appConfig.value.enableOpenai,
|
||||
errorText: i18n.t("settings.openai-ready-error-text"),
|
||||
successText: i18n.t("settings.openai-ready-success-text"),
|
||||
color: appConfig.value.enableOpenai ? goodColor : warningColor,
|
||||
icon: appConfig.value.enableOpenai ? goodIcon : warningIcon,
|
||||
},
|
||||
];
|
||||
return data;
|
||||
});
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { MenuItem } from "~/components/global/BaseOverflowButton.vue";
|
||||
import AdvancedOnly from "~/components/global/AdvancedOnly.vue";
|
||||
import { useGroupSelf } from "~/composables/use-groups";
|
||||
|
||||
definePageMeta({
|
||||
middleware: ["group-only"],
|
||||
@@ -52,7 +53,8 @@ definePageMeta({
|
||||
|
||||
const i18n = useI18n();
|
||||
const auth = useMealieAuth();
|
||||
const { $appInfo, $globals } = useNuxtApp();
|
||||
const { $globals } = useNuxtApp();
|
||||
const { group } = useGroupSelf();
|
||||
|
||||
useSeoMeta({
|
||||
title: i18n.t("general.create"),
|
||||
@@ -78,7 +80,7 @@ const subpages = computed<MenuItem[]>(() => [
|
||||
icon: $globals.icons.fileImage,
|
||||
text: i18n.t("recipe.create-from-images"),
|
||||
value: "image",
|
||||
hide: !$appInfo.enableOpenaiImageServices,
|
||||
hide: !group.value?.aiProviderSettings?.imageProviderEnabled,
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.edit,
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
persistent-hint
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-card-text v-if="$appInfo.enableOpenai">
|
||||
<v-card-text v-if="group?.aiProviderSettings?.aiEnabled">
|
||||
{{ $t('recipe.recipe-debugger-use-openai-description') }}
|
||||
<v-checkbox
|
||||
v-model="state.useOpenAI"
|
||||
@@ -69,6 +69,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { useGroupSelf } from "~/composables/use-groups";
|
||||
import { validators } from "~/composables/use-validators";
|
||||
import type { Recipe } from "~/lib/api/types/recipe";
|
||||
|
||||
@@ -80,6 +81,7 @@ const state = reactive({
|
||||
const api = useUserApi();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { group } = useGroupSelf();
|
||||
|
||||
const recipeUrl = computed({
|
||||
set(recipe_import_url: string | null) {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<v-card-text>
|
||||
<v-card-text class="pa-0">
|
||||
<p>{{ $t('recipe.scrape-recipe-description') }}</p>
|
||||
<p v-if="$appInfo.enableOpenaiTranscriptionServices">
|
||||
<p v-if="group?.aiProviderSettings?.audioProviderEnabled">
|
||||
{{ $t('recipe.scrape-recipe-description-transcription') }}
|
||||
</p>
|
||||
</v-card-text>
|
||||
@@ -145,6 +145,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { AxiosResponse } from "axios";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { useGroupSelf } from "~/composables/use-groups";
|
||||
import { useTagStore } from "~/composables/store/use-tag-store";
|
||||
import { useNewRecipeOptions } from "~/composables/use-new-recipe-options";
|
||||
import { validators } from "~/composables/use-validators";
|
||||
@@ -162,6 +163,7 @@ const auth = useMealieAuth();
|
||||
const api = useUserApi();
|
||||
const route = useRoute();
|
||||
const groupSlug = computed(() => route.params.groupSlug as string || auth.user.value?.groupSlug || "");
|
||||
const { group } = useGroupSelf();
|
||||
|
||||
const router = useRouter();
|
||||
const tags = useTagStore();
|
||||
|
||||
@@ -17,25 +17,52 @@
|
||||
</template>
|
||||
{{ $t("profile.group-description") }}
|
||||
</BasePageTitle>
|
||||
<v-form ref="refGroupEditForm" @submit.prevent="handleSubmit">
|
||||
<v-card variant="outlined" style="border-color: lightgray;">
|
||||
<v-card-text>
|
||||
<GroupPreferencesEditor v-if="group.preferences" v-model="group.preferences" />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<div class="d-flex pa-2">
|
||||
<BaseButton type="submit" edit class="ml-auto">
|
||||
{{ $t("general.update") }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</v-form>
|
||||
|
||||
<div class="mb-10">
|
||||
<v-form ref="refGroupPrefsEditForm" @submit.prevent="handlePrefsSubmit">
|
||||
<v-card variant="outlined" style="border-color: lightgray;">
|
||||
<v-card-text>
|
||||
<GroupPreferencesEditor v-if="group.preferences" v-model="group.preferences" />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<div class="d-flex pa-2">
|
||||
<BaseButton type="submit" edit class="ml-auto">
|
||||
{{ $t("general.update") }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</v-form>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<v-form ref="refGroupAISettingsForm" @submit.prevent="handleAISettingsSubmit">
|
||||
<v-card variant="outlined" style="border-color: lightgray;">
|
||||
<v-card-text>
|
||||
<GroupAIProviderSettingsEditor
|
||||
v-if="group.aiProviderSettings"
|
||||
v-model="group.aiProviderSettings"
|
||||
@create="handleCreateProvider"
|
||||
@update="handleUpdateProvider"
|
||||
@delete="handleDeleteProvider"
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<div class="d-flex pa-2">
|
||||
<BaseButton type="submit" edit class="ml-auto">
|
||||
{{ $t("general.update") }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</v-form>
|
||||
</div>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import GroupPreferencesEditor from "~/components/Domain/Group/GroupPreferencesEditor.vue";
|
||||
import GroupAIProviderSettingsEditor from "~/components/Domain/Group/GroupAIProviderSettingsEditor.vue";
|
||||
import { useGroupSelf } from "~/composables/use-groups";
|
||||
import { useAIProviders } from "~/composables/use-ai-providers";
|
||||
import { alert } from "~/composables/use-toast";
|
||||
import type { AIProviderCreate, AIProviderUpdate } from "~/lib/api/types/group";
|
||||
import type { VForm } from "~/types/auto-forms";
|
||||
|
||||
definePageMeta({
|
||||
@@ -49,10 +76,11 @@ useSeoMeta({
|
||||
title: i18n.t("group.group"),
|
||||
});
|
||||
|
||||
const refGroupEditForm = ref<VForm | null>(null);
|
||||
const refGroupPrefsEditForm = ref<VForm | null>(null);
|
||||
const refGroupAISettingsForm = ref<VForm | null>(null);
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!refGroupEditForm.value?.validate() || !group.value?.preferences) {
|
||||
async function handlePrefsSubmit() {
|
||||
if (!refGroupPrefsEditForm.value?.validate() || !group.value?.preferences) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -64,6 +92,55 @@ async function handleSubmit() {
|
||||
alert.error(i18n.t("settings.settings-update-failed"));
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAISettingsSubmit() {
|
||||
if (!refGroupAISettingsForm.value?.validate() || !group.value?.aiProviderSettings) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await groupActions.updateAIProviderSettings();
|
||||
if (data) {
|
||||
alert.success(i18n.t("settings.settings-updated"));
|
||||
}
|
||||
else {
|
||||
alert.error(i18n.t("settings.settings-update-failed"));
|
||||
}
|
||||
}
|
||||
|
||||
const { createOne, updateOne, deleteOne } = useAIProviders();
|
||||
|
||||
async function handleCreateProvider(data: AIProviderCreate) {
|
||||
const result = await createOne(data);
|
||||
if (result.data) {
|
||||
await groupActions.refresh();
|
||||
alert.success(i18n.t("group.ai-provider-settings.provider-created"));
|
||||
}
|
||||
else {
|
||||
alert.error(i18n.t("group.ai-provider-settings.provider-create-failed"));
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUpdateProvider(id: string, data: AIProviderUpdate) {
|
||||
const result = await updateOne(id, data);
|
||||
if (result.data) {
|
||||
await groupActions.refresh();
|
||||
alert.success(i18n.t("group.ai-provider-settings.provider-updated"));
|
||||
}
|
||||
else {
|
||||
alert.error(i18n.t("group.ai-provider-settings.provider-update-failed"));
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteProvider(id: string) {
|
||||
const result = await deleteOne(id);
|
||||
if (result.data) {
|
||||
await groupActions.refresh();
|
||||
alert.success(i18n.t("group.ai-provider-settings.provider-deleted"));
|
||||
}
|
||||
else {
|
||||
alert.error(i18n.t("group.ai-provider-settings.provider-delete-failed"));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css">
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
sm="12"
|
||||
md="12"
|
||||
>
|
||||
<v-card variant="outlined" style="border-color: lightgray;" class="mt-4">
|
||||
<v-card variant="outlined" style="border-color: lightgray;" class="mt-4 pa-2">
|
||||
<v-card-title class="text-h6 pb-0">
|
||||
{{ $t('profile.household-statistics') }}
|
||||
</v-card-title>
|
||||
|
||||
Reference in New Issue
Block a user