feat: added debouncing for onboarding, fixed validation if the user i… (#392)
This commit is contained in:
@@ -18,7 +18,7 @@ dialogs:
|
||||
baseUrlHelp: API endpoint URL (use default if unsure)
|
||||
accountId: Account ID
|
||||
validating: Validating configuration...
|
||||
validationSuccess: Configuration is valid!
|
||||
validationSuccess: Server has response, Configuration may valid.
|
||||
validationFailed: Configuration validation failed
|
||||
validationError: 'Validation error: {error}'
|
||||
skipForNow: Skip for now
|
||||
|
||||
@@ -18,7 +18,6 @@ dialogs:
|
||||
baseUrlHelp: URL del endpoint de la API (usa el predeterminado si no estás seguro)
|
||||
accountId: ID de Cuenta
|
||||
validating: Validando configuración...
|
||||
validationSuccess: ¡La configuración es válida!
|
||||
validationFailed: La validación de la configuración falló
|
||||
validationError: 'Error de validación: {error}'
|
||||
skipForNow: Omitir por ahora
|
||||
|
||||
@@ -16,7 +16,7 @@ dialogs:
|
||||
baseUrlHelp: API 端点 URL(如果不确定请使用默认值)
|
||||
accountId: 账户 ID
|
||||
validating: 正在验证配置...
|
||||
validationSuccess: 配置有效!
|
||||
validationSuccess: 初步测试服务器有响应,配置或许有效。
|
||||
validationFailed: 配置验证失败
|
||||
validationError: 验证错误:{error}
|
||||
skipForNow: 暂时跳过
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
<script setup lang="ts" xmlns:i-solar="http://www.w3.org/1999/xhtml">
|
||||
import { FieldInput } from '@proj-airi/ui'
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
@@ -21,6 +21,8 @@ interface Emits {
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const debounceTime = 500
|
||||
|
||||
const step = ref(1)
|
||||
const direction = ref<'next' | 'previous'>('next')
|
||||
|
||||
@@ -55,8 +57,8 @@ const selectedProvider = computed(() => {
|
||||
return allChatProvidersMetadata.value.find(p => p.id === selectedProviderId.value) || null
|
||||
})
|
||||
|
||||
// Validation state
|
||||
const isValidating = ref(false)
|
||||
// Validation state (animation)
|
||||
const isValidating = ref(0)
|
||||
const isValid = ref(false)
|
||||
const validationMessage = ref('')
|
||||
|
||||
@@ -134,8 +136,11 @@ async function validateConfiguration() {
|
||||
if (!selectedProvider.value)
|
||||
return
|
||||
|
||||
isValidating.value = true
|
||||
isValidating.value++
|
||||
// service startup time
|
||||
const startValidationTimestamp = performance.now()
|
||||
validationMessage.value = t('settings.dialogs.onboarding.validating')
|
||||
let finalValidationMessage = ''
|
||||
|
||||
try {
|
||||
// Prepare config object
|
||||
@@ -155,20 +160,23 @@ async function validateConfiguration() {
|
||||
isValid.value = validationResult.valid
|
||||
|
||||
if (isValid.value) {
|
||||
validationMessage.value = t('settings.dialogs.onboarding.validationSuccess')
|
||||
finalValidationMessage = t('settings.dialogs.onboarding.validationSuccess')
|
||||
}
|
||||
else {
|
||||
validationMessage.value = validationResult.reason
|
||||
finalValidationMessage = validationResult.reason
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
isValid.value = false
|
||||
validationMessage.value = t('settings.dialogs.onboarding.validationError', {
|
||||
finalValidationMessage = t('settings.dialogs.onboarding.validationError', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
}
|
||||
finally {
|
||||
isValidating.value = false
|
||||
setTimeout(() => {
|
||||
isValidating.value--
|
||||
validationMessage.value = finalValidationMessage
|
||||
}, Math.max(0, debounceTime - (performance.now() - startValidationTimestamp)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,7 +192,7 @@ const debouncedValidateConfiguration = useDebounceFn(() => {
|
||||
return
|
||||
|
||||
validateConfiguration()
|
||||
}, 500)
|
||||
}, debounceTime)
|
||||
|
||||
// Watch for changes and validate
|
||||
watch([apiKey, baseUrl, accountId], () => {
|
||||
@@ -394,7 +402,7 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<!-- Validation Status -->
|
||||
<Alert v-if="!isValid && validationMessage" type="error">
|
||||
<Alert v-if="!isValid && isValidating === 0 && validationMessage" type="error">
|
||||
<template #title>
|
||||
{{ t('settings.dialogs.onboarding.validationFailed') }}
|
||||
</template>
|
||||
@@ -404,17 +412,17 @@ onMounted(() => {
|
||||
</div>
|
||||
</template>
|
||||
</Alert>
|
||||
<div v-if="(isValid || isValidating) && validationMessage" class="mt-4">
|
||||
<div v-if="(isValid || isValidating > 0) && validationMessage" class="mt-4">
|
||||
<div
|
||||
class="flex items-center rounded-lg p-3" :class="[
|
||||
isValidating
|
||||
(isValidating > 0)
|
||||
? 'bg-blue-50 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300'
|
||||
: 'bg-green-50 text-green-700 dark:bg-green-900/30 dark:text-green-300',
|
||||
]"
|
||||
>
|
||||
<div
|
||||
class="mr-2 text-lg" :class="[
|
||||
isValidating
|
||||
(isValidating > 0)
|
||||
? 'i-svg-spinners:3-dots-fade'
|
||||
: 'i-solar:check-circle-bold-duotone',
|
||||
]"
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { isAbsoluteUrl } from '../utils/string'
|
||||
import { models as elevenLabsModels } from './providers/elevenlabs/list-models'
|
||||
|
||||
export interface ProviderMetadata {
|
||||
@@ -159,6 +160,11 @@ export interface VoiceInfo {
|
||||
export const useProvidersStore = defineStore('providers', () => {
|
||||
const providerCredentials = useLocalStorage<Record<string, Record<string, unknown>>>('settings/credentials/providers', {})
|
||||
const { t } = useI18n()
|
||||
const notBaseUrlError = computed(() => ({
|
||||
errors: [new Error('Base URL is not absolute')],
|
||||
reason: 'Base URL is not absolute. Check your input.',
|
||||
valid: false,
|
||||
}))
|
||||
|
||||
// Helper function to fetch OpenRouter models manually
|
||||
async function fetchOpenRouterModels(config: Record<string, unknown>): Promise<ModelInfo[]> {
|
||||
@@ -217,6 +223,10 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
!config.baseUrl && new Error('Base URL is required'),
|
||||
].filter(Boolean)
|
||||
|
||||
if (!!config.baseUrl && !isAbsoluteUrl(config.baseUrl as string)) {
|
||||
return notBaseUrlError.value
|
||||
}
|
||||
|
||||
return {
|
||||
errors,
|
||||
reason: errors.filter(e => e).map(e => String(e)).join(', ') || '',
|
||||
@@ -494,6 +504,10 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
}
|
||||
}
|
||||
|
||||
if (!isAbsoluteUrl(config.baseUrl as string)) {
|
||||
return notBaseUrlError.value
|
||||
}
|
||||
|
||||
// Check if the Ollama server is reachable
|
||||
return fetch(`${(config.baseUrl as string).trim()}models`, { headers: (config.headers as HeadersInit) || undefined })
|
||||
.then((response) => {
|
||||
@@ -556,6 +570,10 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
}
|
||||
}
|
||||
|
||||
if (!isAbsoluteUrl(config.baseUrl as string)) {
|
||||
return notBaseUrlError.value
|
||||
}
|
||||
|
||||
// Check if the Ollama server is reachable
|
||||
return fetch(`${(config.baseUrl as string).trim()}models`, { headers: (config.headers as HeadersInit) || undefined })
|
||||
.then((response) => {
|
||||
@@ -647,6 +665,10 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
}
|
||||
}
|
||||
|
||||
if (!isAbsoluteUrl(config.baseUrl as string)) {
|
||||
return notBaseUrlError.value
|
||||
}
|
||||
|
||||
// Check if the vLLM is reachable
|
||||
return fetch(`${(config.baseUrl as string).trim()}models`, { headers: (config.headers as HeadersInit) || undefined })
|
||||
.then((response) => {
|
||||
@@ -778,6 +800,10 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
!config.baseUrl && new Error('Base URL is required. Default to https://api.openai.com/v1/ for official OpenAI API.'),
|
||||
].filter(Boolean)
|
||||
|
||||
if (!!config.baseUrl && !isAbsoluteUrl(config.baseUrl as string)) {
|
||||
return notBaseUrlError.value
|
||||
}
|
||||
|
||||
return {
|
||||
errors,
|
||||
reason: errors.filter(e => e).map(e => String(e)).join(', ') || '',
|
||||
@@ -891,6 +917,10 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
!config.baseUrl && new Error('Base URL is required. Default to https://api.openai.com/v1/ for official OpenAI API.'),
|
||||
].filter(Boolean)
|
||||
|
||||
if (!!config.baseUrl && !isAbsoluteUrl(config.baseUrl as string)) {
|
||||
return notBaseUrlError.value
|
||||
}
|
||||
|
||||
return {
|
||||
errors,
|
||||
reason: errors.filter(e => e).map(e => String(e)).join(', ') || '',
|
||||
@@ -934,6 +964,10 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
!config.baseUrl && new Error('Base URL is required. Default to https://api.openai.com/v1/ for official OpenAI API.'),
|
||||
].filter(Boolean)
|
||||
|
||||
if (!!config.baseUrl && !isAbsoluteUrl(config.baseUrl as string)) {
|
||||
return notBaseUrlError.value
|
||||
}
|
||||
|
||||
return {
|
||||
errors,
|
||||
reason: errors.filter(e => e).map(e => String(e)).join(', ') || '',
|
||||
@@ -1065,6 +1099,10 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
!config.baseUrl && new Error('Base URL is required. Default to https://api.anthropic.com/v1/ for official Claude API with OpenAI compatibility.'),
|
||||
].filter(Boolean)
|
||||
|
||||
if (!!config.baseUrl && !isAbsoluteUrl(config.baseUrl as string)) {
|
||||
return notBaseUrlError.value
|
||||
}
|
||||
|
||||
return {
|
||||
errors,
|
||||
reason: errors.filter(e => e).map(e => String(e)).join(', ') || '',
|
||||
@@ -1109,6 +1147,10 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
!config.baseUrl && new Error('Base URL is required. Default to https://generativelanguage.googleapis.com/v1beta/openai/ for official Google Gemini API with OpenAI compatibility.'),
|
||||
].filter(Boolean)
|
||||
|
||||
if (!!config.baseUrl && !isAbsoluteUrl(config.baseUrl as string)) {
|
||||
return notBaseUrlError.value
|
||||
}
|
||||
|
||||
return {
|
||||
errors,
|
||||
reason: errors.filter(e => e).map(e => String(e)).join(', ') || '',
|
||||
@@ -1194,6 +1236,10 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
!config.baseUrl && new Error('Base URL is required.'),
|
||||
].filter(Boolean)
|
||||
|
||||
if (!!config.baseUrl && !isAbsoluteUrl(config.baseUrl as string)) {
|
||||
return notBaseUrlError.value
|
||||
}
|
||||
|
||||
return {
|
||||
errors,
|
||||
reason: errors.filter(e => e).map(e => String(e)).join(', ') || '',
|
||||
@@ -1274,6 +1320,10 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
!config.baseUrl && new Error('Base URL is required.'),
|
||||
].filter(Boolean)
|
||||
|
||||
if (!!config.baseUrl && !isAbsoluteUrl(config.baseUrl as string)) {
|
||||
return notBaseUrlError.value
|
||||
}
|
||||
|
||||
return {
|
||||
errors,
|
||||
reason: errors.filter(e => e).map(e => String(e)).join(', ') || '',
|
||||
@@ -1334,6 +1384,10 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
!config.baseUrl && new Error('Base URL is required.'),
|
||||
].filter(Boolean)
|
||||
|
||||
if (!!config.baseUrl && !isAbsoluteUrl(config.baseUrl as string)) {
|
||||
return notBaseUrlError.value
|
||||
}
|
||||
|
||||
return {
|
||||
errors,
|
||||
reason: errors.filter(e => e).map(e => String(e)).join(', ') || '',
|
||||
@@ -1391,6 +1445,10 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
!config.baseUrl && new Error('Base URL is required. Default to http://localhost:11996/tts for Index-TTS.'),
|
||||
].filter(Boolean)
|
||||
|
||||
if (!!config.baseUrl && !isAbsoluteUrl(config.baseUrl as string)) {
|
||||
return notBaseUrlError.value
|
||||
}
|
||||
|
||||
return {
|
||||
errors,
|
||||
reason: errors.filter(e => e).map(e => String(e)).join(', ') || '',
|
||||
@@ -1459,6 +1517,10 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
!config.baseUrl && new Error('Base URL is required.'),
|
||||
].filter(Boolean)
|
||||
|
||||
if (!!config.baseUrl && !isAbsoluteUrl(config.baseUrl as string)) {
|
||||
return notBaseUrlError.value
|
||||
}
|
||||
|
||||
return {
|
||||
errors,
|
||||
reason: errors.filter(e => e).map(e => String(e)).join(', ') || '',
|
||||
@@ -1520,6 +1582,10 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
!((config.app as any)?.appId) && new Error('App ID is required.'),
|
||||
].filter(Boolean)
|
||||
|
||||
if (!!config.baseUrl && !isAbsoluteUrl(config.baseUrl as string)) {
|
||||
return notBaseUrlError.value
|
||||
}
|
||||
|
||||
return {
|
||||
errors,
|
||||
reason: errors.filter(e => e).map(e => String(e)).join(', ') || '',
|
||||
@@ -1687,6 +1753,10 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
!config.baseUrl && new Error('Base URL is required.'),
|
||||
].filter(Boolean)
|
||||
|
||||
if (!!config.baseUrl && !isAbsoluteUrl(config.baseUrl as string)) {
|
||||
return notBaseUrlError.value
|
||||
}
|
||||
|
||||
return {
|
||||
errors,
|
||||
reason: errors.filter(e => e).map(e => String(e)).join(', ') || '',
|
||||
@@ -1786,6 +1856,10 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
!config.baseUrl && new Error('Base URL is required.'),
|
||||
].filter(Boolean)
|
||||
|
||||
if (!!config.baseUrl && !isAbsoluteUrl(config.baseUrl as string)) {
|
||||
return notBaseUrlError.value
|
||||
}
|
||||
|
||||
return {
|
||||
errors,
|
||||
reason: errors.filter(e => e).map(e => String(e)).join(', ') || '',
|
||||
@@ -1910,6 +1984,10 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
}
|
||||
}
|
||||
|
||||
if (!isAbsoluteUrl(config.baseUrl as string)) {
|
||||
return notBaseUrlError.value
|
||||
}
|
||||
|
||||
// Check if the local running Player 2 is reachable
|
||||
return await fetch(`${(config.baseUrl as string).endsWith('/') ? (config.baseUrl as string).slice(0, -1) : config.baseUrl}/health`, {
|
||||
method: 'GET',
|
||||
@@ -2015,6 +2093,10 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
}
|
||||
}
|
||||
|
||||
if (!isAbsoluteUrl(config.baseUrl as string)) {
|
||||
return notBaseUrlError.value
|
||||
}
|
||||
|
||||
return {
|
||||
errors: [],
|
||||
reason: '',
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export function isAbsoluteUrl(url: string) {
|
||||
try {
|
||||
// This could be the most reliable way to check so using side-effect is acceptable
|
||||
// eslint-disable-next-line no-new
|
||||
new URL(url)
|
||||
return true
|
||||
}
|
||||
catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user