feat: ollama setting pages now has validating animation (#523)

This commit is contained in:
MisakaKumomi 御坂云见
2025-08-29 03:16:07 +08:00
committed by GitHub
parent a81711137b
commit 698bfb065f
4 changed files with 99 additions and 46 deletions
@@ -20,6 +20,7 @@ const { t } = useI18n()
const router = useRouter()
const providersStore = useProvidersStore()
const { providers } = storeToRefs(providersStore) as { providers: RemovableRef<Record<string, any>> }
const loading = ref(0)
// Get provider metadata
const providerId = 'ollama'
@@ -74,6 +75,11 @@ watch(headers, (headers) => {
})
async function refetch() {
loading.value++
// service startup time
const startValidationTimestamp = performance.now()
let finalValidationMessage = ''
try {
const validationResult = await providerMetadata.value.validators.validateProviderConfig({
baseUrl: baseUrl.value,
@@ -84,16 +90,25 @@ async function refetch() {
})
if (!validationResult.valid) {
validationMessage.value = t('settings.dialogs.onboarding.validationError', {
finalValidationMessage = t('settings.dialogs.onboarding.validationError', {
error: validationResult.reason,
})
}
else {
finalValidationMessage = ''
}
}
catch (error) {
validationMessage.value = t('settings.dialogs.onboarding.validationError', {
finalValidationMessage = t('settings.dialogs.onboarding.validationError', {
error: error instanceof Error ? error.message : String(error),
})
}
finally {
setTimeout(() => {
loading.value--
validationMessage.value = finalValidationMessage
}, 500 - (performance.now() - startValidationTimestamp))
}
}
watch([baseUrl, headers], refetch, { immediate: true })
@@ -122,47 +137,59 @@ function handleResetSettings() {
</script>
<template>
<Alert v-if="validationMessage" type="error">
<template #title>
{{ t('settings.dialogs.onboarding.validationFailed') }}
</template>
<template v-if="validationMessage" #content>
<div class="whitespace-pre-wrap break-all">
{{ validationMessage }}
</div>
</template>
</Alert>
<ProviderSettingsLayout
:provider-name="providerMetadata?.localizedName"
:provider-icon="providerMetadata?.icon"
:on-back="() => router.back()"
>
<ProviderSettingsContainer>
<ProviderBasicSettings
:title="t('settings.pages.providers.common.section.basic.title')"
:description="t('settings.pages.providers.common.section.basic.description')"
:on-reset="handleResetSettings"
>
<ProviderBaseUrlInput
v-model="baseUrl"
:placeholder="providerMetadata?.defaultOptions?.().baseUrl as string || ''"
required
/>
</ProviderBasicSettings>
<div class="flex flex-col gap-4">
<Alert v-if="!!loading" type="loading">
<template #title>
{{ t('settings.pages.providers.provider.common.status.validating') }}
</template>
</Alert>
<Alert v-else-if="!validationMessage" type="success">
<template #title>
{{ t('settings.pages.providers.provider.common.status.valid') }}
</template>
</Alert>
<Alert v-else-if="validationMessage" type="error">
<template #title>
{{ t('settings.dialogs.onboarding.validationFailed') }}
</template>
<template v-if="validationMessage" #content>
<div class="whitespace-pre-wrap break-all">
{{ validationMessage }}
</div>
</template>
</Alert>
<ProviderSettingsLayout
:provider-name="providerMetadata?.localizedName"
:provider-icon="providerMetadata?.icon"
:on-back="() => router.back()"
>
<ProviderSettingsContainer>
<ProviderBasicSettings
:title="t('settings.pages.providers.common.section.basic.title')"
:description="t('settings.pages.providers.common.section.basic.description')"
:on-reset="handleResetSettings"
>
<ProviderBaseUrlInput
v-model="baseUrl"
:placeholder="providerMetadata?.defaultOptions?.().baseUrl as string || ''"
required
/>
</ProviderBasicSettings>
<ProviderAdvancedSettings :title="t('settings.pages.providers.common.section.advanced.title')">
<FieldKeyValues
v-model="headers"
:label="t('settings.pages.providers.common.section.advanced.fields.field.headers.label')"
:description="t('settings.pages.providers.common.section.advanced.fields.field.headers.description')"
:key-placeholder="t('settings.pages.providers.common.section.advanced.fields.field.headers.key.placeholder')"
:value-placeholder="t('settings.pages.providers.common.section.advanced.fields.field.headers.value.placeholder')"
@add="(key: string, value: string) => addKeyValue(headers, key, value)"
@remove="(index: number) => removeKeyValue(index, headers)"
/>
</ProviderAdvancedSettings>
</ProviderSettingsContainer>
</ProviderSettingsLayout>
<ProviderAdvancedSettings :title="t('settings.pages.providers.common.section.advanced.title')">
<FieldKeyValues
v-model="headers"
:label="t('settings.pages.providers.common.section.advanced.fields.field.headers.label')"
:description="t('settings.pages.providers.common.section.advanced.fields.field.headers.description')"
:key-placeholder="t('settings.pages.providers.common.section.advanced.fields.field.headers.key.placeholder')"
:value-placeholder="t('settings.pages.providers.common.section.advanced.fields.field.headers.value.placeholder')"
@add="(key: string, value: string) => addKeyValue(headers, key, value)"
@remove="(index: number) => removeKeyValue(index, headers)"
/>
</ProviderAdvancedSettings>
</ProviderSettingsContainer>
</ProviderSettingsLayout>
</div>
</template>
<route lang="yaml">
@@ -334,6 +334,9 @@ pages:
placeholder: Input Cloudflare API Key
title: Cloudflare Workers AI
common:
status:
validating: Validating
valid: Configuration seems to be valid.
fields:
field:
pitch:
@@ -305,6 +305,9 @@ pages:
placeholder: 请输入 Cloudflare Workers AI 的 API Key
title: Workers AI
common:
status:
validating: 正在验证配置
valid: 配置似乎有效。
fields:
field:
pitch:
@@ -1,16 +1,24 @@
<script setup lang="ts">
import { computed } from 'vue'
import { computed, useSlots } from 'vue'
const props = defineProps<{
type?: 'error' | 'warning'
type?: 'error' | 'warning' | 'success' | 'info' | 'loading'
}>()
const slots = useSlots()
const containerClass = computed(() => {
switch (props.type) {
case 'error':
return 'border-solid border-2 border-red-200 bg-red-50 dark:border-red-800/30 dark:bg-red-900/20'
case 'warning':
return 'border-solid border-2 border-amber-200 bg-amber-50 dark:border-amber-800/30 dark:bg-amber-900/20'
case 'success':
return 'border-solid border-2 border-green-200 bg-green-50 dark:border-green-800/30 text-green-700 dark:bg-green-900/30 dark:text-green-300'
case 'info':
return 'border-solid border-2 border-blue-200 bg-blue-50 dark:border-blue-800/30 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300'
case 'loading':
return 'border-solid border-2 border-blue-200 bg-blue-50 dark:border-blue-800/30 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300'
}
return ''
})
@@ -21,6 +29,12 @@ const iconClass = computed(() => {
return 'i-solar:close-circle-bold-duotone text-red-500 dark:text-red-400'
case 'warning':
return 'i-solar:danger-circle-bold-duotone text-amber-500 dark:text-amber-400'
case 'success':
return 'i-solar:check-circle-bold-duotone text-green-500 dark:text-green-400'
case 'info':
return 'i-solar:info-circle-bold-duotone text-blue-500 dark:text-blue-400'
case 'loading':
return 'i-svg-spinners:3-dots-fade text-blue-500 dark:text-blue-400'
}
return ''
})
@@ -31,20 +45,26 @@ const titleClass = computed(() => {
return 'text-red-500 dark:text-red-400'
case 'warning':
return 'text-amber-500 dark:text-amber-400'
case 'success':
return 'text-green-500 dark:text-green-400'
case 'info':
return 'text-blue-500 dark:text-blue-400'
case 'loading':
return 'text-blue-500 dark:text-blue-400'
}
return ''
})
</script>
<template>
<div class="flex flex-col gap-3 rounded-xl px-2 pb-3 pt-2" :class="containerClass">
<div class="flex flex-col gap-3 rounded-xl p-2" :class="containerClass">
<div class="flex items-center gap-1.5 font-medium">
<div class="text-2xl" :class="iconClass" />
<div :class="titleClass">
<slot name="title" />
</div>
</div>
<div class="px-1 text-sm">
<div v-if="slots.content" class="px-1 text-sm">
<slot name="content" />
</div>
</div>