mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
feat: auto-create search/tts providers at startup with enable toggle
- Add `enable` column (default false) to search_providers and tts_providers tables - Auto-create default entries for all provider types on startup (disabled by default) - Add enable/disable Switch toggle in frontend for both search and TTS providers - Show green status dot in sidebar for enabled providers, sort enabled first - Filter bot settings dropdowns to only show enabled providers
This commit is contained in:
@@ -477,10 +477,11 @@ const { mutateAsync: deleteBot, isLoading: deleteLoading } = useMutation({
|
||||
|
||||
const models = computed(() => modelData.value ?? [])
|
||||
const providers = computed(() => providerData.value ?? [])
|
||||
const searchProviders = computed(() => searchProviderData.value ?? [])
|
||||
const searchProviders = computed(() => (searchProviderData.value ?? []).filter((p) => p.enable !== false))
|
||||
const memoryProviders = computed(() => memoryProviderData.value ?? [])
|
||||
const ttsProviders = computed(() => ttsProviderData.value ?? [])
|
||||
const ttsModels = computed(() => ttsModelData.value ?? [])
|
||||
const ttsProviders = computed(() => (ttsProviderData.value ?? []).filter((p) => p.enable !== false))
|
||||
const enabledTtsProviderIds = computed(() => new Set(ttsProviders.value.map((p) => p.id)))
|
||||
const ttsModels = computed(() => (ttsModelData.value ?? []).filter((m: Record<string, unknown>) => enabledTtsProviderIds.value.has(m.tts_provider_id as string)))
|
||||
const browserContexts = computed(() => browserContextData.value ?? [])
|
||||
|
||||
// ---- Form ----
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
<template>
|
||||
<div class="p-4">
|
||||
<section class="flex justify-between items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', 'volume-high']"
|
||||
class="size-5"
|
||||
<section class="flex items-center gap-3">
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', 'volume-high']"
|
||||
class="size-5"
|
||||
/>
|
||||
<div class="min-w-0">
|
||||
<h2 class="text-sm font-semibold truncate">
|
||||
{{ curProvider?.name }}
|
||||
</h2>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ currentMeta?.display_name ?? curProvider?.provider }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="ml-auto flex items-center gap-2">
|
||||
<span class="text-xs text-muted-foreground">
|
||||
{{ $t('common.enable') }}
|
||||
</span>
|
||||
<Switch
|
||||
:model-value="curProvider?.enable ?? false"
|
||||
:disabled="!curProvider?.id || enableLoading"
|
||||
@update:model-value="handleToggleEnable"
|
||||
/>
|
||||
<div>
|
||||
<h2 class="text-sm font-semibold">
|
||||
{{ curProvider?.name }}
|
||||
</h2>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ currentMeta?.display_name ?? curProvider?.provider }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<Separator class="mt-4 mb-6" />
|
||||
@@ -152,6 +160,7 @@ import {
|
||||
FormItem,
|
||||
Separator,
|
||||
Label,
|
||||
Switch,
|
||||
} from '@memohai/ui'
|
||||
import ConfirmPopover from '@/components/confirm-popover/index.vue'
|
||||
import LoadingButton from '@/components/loading-button/index.vue'
|
||||
@@ -170,6 +179,7 @@ import type { TtsProviderResponse, TtsProviderMetaResponse, TtsModelInfo } from
|
||||
const { t } = useI18n()
|
||||
const curProvider = inject('curTtsProvider', ref<TtsProviderResponse>())
|
||||
const curProviderId = computed(() => curProvider.value?.id)
|
||||
const enableLoading = ref(false)
|
||||
|
||||
const apiBase = import.meta.env.VITE_API_URL?.trim() || '/api'
|
||||
function authHeaders(): Record<string, string> {
|
||||
@@ -219,6 +229,28 @@ function toggleModel(id: string) {
|
||||
|
||||
const queryCache = useQueryCache()
|
||||
|
||||
async function handleToggleEnable(value: boolean) {
|
||||
if (!curProviderId.value || !curProvider.value) return
|
||||
|
||||
const prev = curProvider.value.enable ?? false
|
||||
curProvider.value = { ...curProvider.value, enable: value }
|
||||
|
||||
enableLoading.value = true
|
||||
try {
|
||||
await putTtsProvidersById({
|
||||
path: { id: curProviderId.value },
|
||||
body: { enable: value },
|
||||
throwOnError: true,
|
||||
})
|
||||
queryCache.invalidateQueries({ key: ['tts-providers'] })
|
||||
} catch {
|
||||
curProvider.value = { ...curProvider.value, enable: prev }
|
||||
toast.error(t('common.saveFailed'))
|
||||
} finally {
|
||||
enableLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const schema = toTypedSchema(z.object({
|
||||
name: z.string().min(1),
|
||||
}))
|
||||
|
||||
@@ -37,7 +37,11 @@ const selectProvider = (name: string) => computed(() => {
|
||||
|
||||
const filteredProviders = computed(() => {
|
||||
if (!Array.isArray(providerData.value)) return []
|
||||
return providerData.value
|
||||
return [...providerData.value].sort((a, b) => {
|
||||
const ae = a.enable !== false ? 1 : 0
|
||||
const be = b.enable !== false ? 1 : 0
|
||||
return be - ae
|
||||
})
|
||||
})
|
||||
|
||||
watch(filteredProviders, (list) => {
|
||||
@@ -76,7 +80,19 @@ const openStatus = reactive({ addOpen: false })
|
||||
:model-value="selectProvider(item.name ?? '').value"
|
||||
@update:model-value="(isSelect) => { if (isSelect) curProvider = item }"
|
||||
>
|
||||
{{ item.name }}
|
||||
<span class="relative shrink-0">
|
||||
<span class="flex size-7 items-center justify-center rounded-full bg-muted">
|
||||
<FontAwesomeIcon
|
||||
:icon="['fas', 'volume-high']"
|
||||
class="size-3.5 text-muted-foreground"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
v-if="item.enable !== false"
|
||||
class="absolute -bottom-0.5 -right-0.5 size-2.5 rounded-full bg-green-500 ring-2 ring-background"
|
||||
/>
|
||||
</span>
|
||||
<span class="truncate">{{ item.name }}</span>
|
||||
</Toggle>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
<template>
|
||||
<div class="p-4">
|
||||
<section class="flex justify-between items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<SearchProviderLogo
|
||||
:provider="curProvider?.provider || ''"
|
||||
size="lg"
|
||||
<section class="flex items-center gap-3">
|
||||
<SearchProviderLogo
|
||||
:provider="curProvider?.provider || ''"
|
||||
size="lg"
|
||||
/>
|
||||
<h2 class="scroll-m-20 text-sm font-semibold tracking-tight min-w-0 truncate">
|
||||
{{ curProvider?.name }}
|
||||
</h2>
|
||||
<div class="ml-auto flex items-center gap-2">
|
||||
<span class="text-xs text-muted-foreground">
|
||||
{{ $t('common.enable') }}
|
||||
</span>
|
||||
<Switch
|
||||
:model-value="curProvider?.enable ?? true"
|
||||
:disabled="!curProvider?.id || enableLoading"
|
||||
@update:model-value="handleToggleEnable"
|
||||
/>
|
||||
<h2 class="scroll-m-20 text-sm font-semibold tracking-tight">
|
||||
{{ curProvider?.name }}
|
||||
</h2>
|
||||
</div>
|
||||
</section>
|
||||
<Separator class="mt-4 mb-6" />
|
||||
@@ -123,6 +131,7 @@ import {
|
||||
FormItem,
|
||||
Separator,
|
||||
Label,
|
||||
Switch,
|
||||
} from '@memohai/ui'
|
||||
import ConfirmPopover from '@/components/confirm-popover/index.vue'
|
||||
import LoadingButton from '@/components/loading-button/index.vue'
|
||||
@@ -146,9 +155,13 @@ import { useForm } from 'vee-validate'
|
||||
import { useMutation, useQueryCache } from '@pinia/colada'
|
||||
import { putSearchProvidersById, deleteSearchProvidersById } from '@memohai/sdk'
|
||||
import type { SearchprovidersGetResponse, SearchprovidersUpdateRequest } from '@memohai/sdk'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
const { t } = useI18n()
|
||||
const curProvider = inject('curSearchProvider', ref<SearchprovidersGetResponse>())
|
||||
const curProviderId = computed(() => curProvider.value?.id)
|
||||
const enableLoading = ref(false)
|
||||
|
||||
const queryCache = useQueryCache()
|
||||
|
||||
@@ -182,6 +195,28 @@ watch(curProvider, (newVal) => {
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
async function handleToggleEnable(value: boolean) {
|
||||
if (!curProviderId.value || !curProvider.value) return
|
||||
|
||||
const prev = curProvider.value.enable ?? true
|
||||
curProvider.value = { ...curProvider.value, enable: value }
|
||||
|
||||
enableLoading.value = true
|
||||
try {
|
||||
await putSearchProvidersById({
|
||||
path: { id: curProviderId.value },
|
||||
body: { enable: value },
|
||||
throwOnError: true,
|
||||
})
|
||||
queryCache.invalidateQueries({ key: ['search-providers'] })
|
||||
} catch {
|
||||
curProvider.value = { ...curProvider.value, enable: prev }
|
||||
toast.error(t('common.saveFailed'))
|
||||
} finally {
|
||||
enableLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ---- mutations ----
|
||||
const { mutate: submitUpdate, isLoading: editLoading } = useMutation({
|
||||
mutation: async (data: SearchprovidersUpdateRequest) => {
|
||||
|
||||
@@ -43,15 +43,27 @@ const curFilterProvider = computed(() => {
|
||||
if (!Array.isArray(providerData.value)) {
|
||||
return []
|
||||
}
|
||||
return providerData.value
|
||||
return [...providerData.value].sort((a, b) => {
|
||||
const ae = a.enable !== false ? 1 : 0
|
||||
const be = b.enable !== false ? 1 : 0
|
||||
return be - ae
|
||||
})
|
||||
})
|
||||
|
||||
watch(curFilterProvider, () => {
|
||||
if (curFilterProvider.value.length > 0) {
|
||||
curProvider.value = curFilterProvider.value[0]
|
||||
} else {
|
||||
watch(curFilterProvider, (providers) => {
|
||||
if (providers.length === 0) {
|
||||
curProvider.value = { id: '' }
|
||||
return
|
||||
}
|
||||
const currentId = curProvider.value?.id
|
||||
if (currentId) {
|
||||
const stillExists = providers.find((p) => p.id === currentId)
|
||||
if (stillExists) {
|
||||
curProvider.value = stillExists
|
||||
return
|
||||
}
|
||||
}
|
||||
curProvider.value = providers[0]
|
||||
}, {
|
||||
immediate: true,
|
||||
})
|
||||
@@ -74,7 +86,10 @@ const openStatus = reactive({
|
||||
class="justify-start py-5! px-4"
|
||||
>
|
||||
<Toggle
|
||||
:class="`py-4 border border-transparent ${curProvider?.name === item.name ? 'border-inherit' : ''}`"
|
||||
:class="[
|
||||
'py-4 border',
|
||||
curProvider?.id === item.id ? 'border-border' : 'border-transparent',
|
||||
]"
|
||||
:model-value="selectProvider(item.name as string).value"
|
||||
@update:model-value="(isSelect) => {
|
||||
if (isSelect) {
|
||||
@@ -82,12 +97,17 @@ const openStatus = reactive({
|
||||
}
|
||||
}"
|
||||
>
|
||||
<SearchProviderLogo
|
||||
:provider="item.provider || ''"
|
||||
size="sm"
|
||||
class="mr-2"
|
||||
/>
|
||||
{{ item.name }}
|
||||
<span class="relative shrink-0">
|
||||
<SearchProviderLogo
|
||||
:provider="item.provider || ''"
|
||||
size="sm"
|
||||
/>
|
||||
<span
|
||||
v-if="item.enable !== false"
|
||||
class="absolute -bottom-0.5 -right-0.5 size-2.5 rounded-full bg-green-500 ring-2 ring-background"
|
||||
/>
|
||||
</span>
|
||||
<span class="truncate">{{ item.name }}</span>
|
||||
</Toggle>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
|
||||
Reference in New Issue
Block a user