feat: ollama setting pages now has validating animation (#523)
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user