feat: enhance provider selection with a new drawer interface and localization updates
This commit is contained in:
@@ -14,8 +14,20 @@
|
||||
<!-- Provider Selection Dialog -->
|
||||
<v-dialog v-model="dialog" max-width="600px">
|
||||
<v-card>
|
||||
<v-card-title class="text-h3 py-4" style="font-weight: normal;">
|
||||
{{ tm('providerSelector.dialogTitle') }}
|
||||
<v-card-title
|
||||
class="text-h3 py-4 d-flex align-center justify-space-between gap-4 flex-wrap"
|
||||
style="font-weight: normal;"
|
||||
>
|
||||
<span>{{ tm('providerSelector.dialogTitle') }}</span>
|
||||
<v-btn
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
prepend-icon="mdi-plus"
|
||||
@click="openProviderDrawer"
|
||||
>
|
||||
{{ tm('providerSelector.createProvider') }}
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pa-0" style="max-height: 400px; overflow-y: auto;">
|
||||
@@ -79,12 +91,33 @@
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-overlay
|
||||
v-model="providerDrawer"
|
||||
class="provider-drawer-overlay"
|
||||
location="right"
|
||||
transition="slide-x-reverse-transition"
|
||||
:scrim="true"
|
||||
@click:outside="closeProviderDrawer"
|
||||
>
|
||||
<v-card class="provider-drawer-card" elevation="12">
|
||||
<div class="provider-drawer-header">
|
||||
<v-btn icon variant="text" @click="closeProviderDrawer">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<div class="provider-drawer-content">
|
||||
<ProviderPage :default-tab="defaultTab" />
|
||||
</div>
|
||||
</v-card>
|
||||
</v-overlay>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import axios from 'axios'
|
||||
import { useModuleI18n } from '@/i18n/composables'
|
||||
import ProviderPage from '@/views/ProviderPage.vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
@@ -112,12 +145,26 @@ const dialog = ref(false)
|
||||
const providerList = ref([])
|
||||
const loading = ref(false)
|
||||
const selectedProvider = ref('')
|
||||
const providerDrawer = ref(false)
|
||||
|
||||
const defaultTab = computed(() => {
|
||||
if (props.providerType === 'agent_runner' && props.providerSubtype) {
|
||||
return `select_agent_runner_provider:${props.providerSubtype}`
|
||||
}
|
||||
return props.providerType || 'chat_completion'
|
||||
})
|
||||
|
||||
// 监听 modelValue 变化,同步到 selectedProvider
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
selectedProvider.value = newValue || ''
|
||||
}, { immediate: true })
|
||||
|
||||
watch(providerDrawer, (isOpen, wasOpen) => {
|
||||
if (!isOpen && wasOpen) {
|
||||
loadProviders()
|
||||
}
|
||||
})
|
||||
|
||||
async function openDialog() {
|
||||
selectedProvider.value = props.modelValue || ''
|
||||
dialog.value = true
|
||||
@@ -170,6 +217,14 @@ function cancelSelection() {
|
||||
selectedProvider.value = props.modelValue || ''
|
||||
dialog.value = false
|
||||
}
|
||||
|
||||
function openProviderDrawer() {
|
||||
providerDrawer.value = true
|
||||
}
|
||||
|
||||
function closeProviderDrawer() {
|
||||
providerDrawer.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -184,4 +239,35 @@ function cancelSelection() {
|
||||
.v-list-item.v-list-item--active {
|
||||
background-color: rgba(var(--v-theme-primary), 0.08);
|
||||
}
|
||||
|
||||
.provider-drawer-overlay {
|
||||
align-items: stretch;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.provider-drawer-card {
|
||||
width: clamp(360px, 70vw, 1200px);
|
||||
height: calc(100vh - 32px);
|
||||
margin: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.provider-drawer-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px 12px 20px;
|
||||
}
|
||||
|
||||
.provider-drawer-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.provider-drawer-content > * {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -40,6 +40,8 @@
|
||||
"cancelSelection": "Cancel",
|
||||
"clearSelection": "None",
|
||||
"clearSelectionSubtitle": "Clear current selection",
|
||||
"unknownType": "Unknown type"
|
||||
"unknownType": "Unknown type",
|
||||
"createProvider": "Create Provider",
|
||||
"manageProviders": "Provider Management"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,8 @@
|
||||
"cancelSelection": "取消",
|
||||
"clearSelection": "不选择",
|
||||
"clearSelectionSubtitle": "清除当前选择",
|
||||
"unknownType": "未知类型"
|
||||
"unknownType": "未知类型",
|
||||
"createProvider": "创建提供商",
|
||||
"manageProviders": "提供商管理"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
<div class="d-flex align-center justify-space-between px-4 pt-4 pb-2">
|
||||
<div class="d-flex align-center ga-2">
|
||||
<h3 class="mb-0">{{ tm('providerSources.title') }}</h3>
|
||||
<v-chip size="x-small" color="primary" variant="tonal">{{ displayedProviderSources.length }}</v-chip>
|
||||
</div>
|
||||
<v-menu>
|
||||
<template v-slot:activator="{ props }">
|
||||
@@ -126,7 +125,6 @@
|
||||
<div class="mt-4">
|
||||
<div class="d-flex align-center ga-2 mb-2">
|
||||
<h3 class="text-h5 font-weight-bold mb-0">{{ tm('models.configured') }}</h3>
|
||||
<!-- <v-chip color="success" variant="tonal" size="small">{{ displayedChatProviders.length }}</v-chip> -->
|
||||
<small style="color: grey;" v-if="availableModels.length">{{ tm('models.available') }} {{
|
||||
availableModels.length }}</small>
|
||||
<v-spacer></v-spacer>
|
||||
@@ -359,6 +357,13 @@ import ItemCard from '@/components/shared/ItemCard.vue'
|
||||
import AddNewProvider from '@/components/provider/AddNewProvider.vue'
|
||||
import { getProviderIcon } from '@/utils/providerUtils'
|
||||
|
||||
const props = defineProps({
|
||||
defaultTab: {
|
||||
type: String,
|
||||
default: 'chat_completion'
|
||||
}
|
||||
})
|
||||
|
||||
const { tm } = useModuleI18n('features/provider')
|
||||
const router = useRouter()
|
||||
|
||||
@@ -367,7 +372,7 @@ const config = ref({})
|
||||
const metadata = ref({})
|
||||
const providerSources = ref([])
|
||||
const providers = ref([])
|
||||
const selectedProviderType = ref('chat_completion')
|
||||
const selectedProviderType = ref(resolveDefaultTab(props.defaultTab))
|
||||
const selectedProviderSource = ref(null)
|
||||
const selectedProviderSourceOriginalId = ref(null)
|
||||
const editableProviderSource = ref(null)
|
||||
@@ -607,6 +612,32 @@ function isTypeMatchingProviderType(type, providerType) {
|
||||
return type && type.includes(providerType)
|
||||
}
|
||||
|
||||
function resolveDefaultTab(value) {
|
||||
const normalized = (value || '').toLowerCase()
|
||||
|
||||
if (normalized.startsWith('select_agent_runner_provider') || normalized === 'agent_runner') {
|
||||
return 'agent_runner'
|
||||
}
|
||||
|
||||
if (normalized === 'select_provider_stt' || normalized === 'speech_to_text' || normalized.includes('stt')) {
|
||||
return 'speech_to_text'
|
||||
}
|
||||
|
||||
if (normalized === 'select_provider_tts' || normalized === 'text_to_speech' || normalized.includes('tts')) {
|
||||
return 'text_to_speech'
|
||||
}
|
||||
|
||||
if (normalized.includes('embedding')) {
|
||||
return 'embedding'
|
||||
}
|
||||
|
||||
if (normalized.includes('rerank')) {
|
||||
return 'rerank'
|
||||
}
|
||||
|
||||
return 'chat_completion'
|
||||
}
|
||||
|
||||
function resolveSourceIcon(source) {
|
||||
if (!source) return ''
|
||||
|
||||
@@ -959,6 +990,10 @@ onMounted(async () => {
|
||||
await loadProviderTemplate()
|
||||
})
|
||||
|
||||
watch(() => props.defaultTab, (val) => {
|
||||
selectedProviderType.value = resolveDefaultTab(val)
|
||||
})
|
||||
|
||||
// 跟踪编辑中的 provider source 是否被修改
|
||||
watch(editableProviderSource, () => {
|
||||
if (suppressSourceWatch) return
|
||||
|
||||
Reference in New Issue
Block a user