feat: enhance configuration editor with template schema support and UI improvements (#4267)
- Added support for template schemas in the configuration editor, allowing users to define and manage additional parameters like temperature, top_p, and max_tokens. - Improved UI components in ProviderModelsPanel and ObjectEditor for better user interaction, including new configuration buttons and enhanced input handling. - Updated localization files to include new configuration options.
This commit is contained in:
@@ -1451,7 +1451,32 @@ CONFIG_METADATA_2 = {
|
||||
"description": "自定义请求体参数",
|
||||
"type": "dict",
|
||||
"items": {},
|
||||
"hint": "此处添加的键值对将被合并到发送给 API 的 extra_body 中。值可以是字符串、数字或布尔值。",
|
||||
"hint": "用于在请求时添加额外的参数,如 temperature、top_p、max_tokens 等。",
|
||||
"template_schema": {
|
||||
"temperature": {
|
||||
"name": "Temperature",
|
||||
"description": "温度参数",
|
||||
"hint": "控制输出的随机性,范围通常为 0-2。值越高越随机。",
|
||||
"type": "float",
|
||||
"default": 0.6,
|
||||
"slider": {"min": 0, "max": 2, "step": 0.1},
|
||||
},
|
||||
"top_p": {
|
||||
"name": "Top-p",
|
||||
"description": "Top-p 采样",
|
||||
"hint": "核采样参数,范围通常为 0-1。控制模型考虑的概率质量。",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"slider": {"min": 0, "max": 1, "step": 0.01},
|
||||
},
|
||||
"max_tokens": {
|
||||
"name": "Max Tokens",
|
||||
"description": "最大令牌数",
|
||||
"hint": "生成的最大令牌数。",
|
||||
"type": "int",
|
||||
"default": 8192,
|
||||
},
|
||||
},
|
||||
},
|
||||
"provider": {
|
||||
"type": "string",
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
{{ tm('availability.test') }}
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
icon="mdi-wrench"
|
||||
icon="mdi-connection"
|
||||
size="small"
|
||||
variant="text"
|
||||
:disabled="!entry.provider.enable"
|
||||
@@ -93,6 +93,19 @@
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<v-tooltip location="top" max-width="300">
|
||||
{{ tm('models.configure') }}
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
icon="mdi-cog"
|
||||
size="small"
|
||||
variant="text"
|
||||
v-bind="props"
|
||||
@click.stop="emit('open-provider-edit', entry.provider)"
|
||||
></v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
|
||||
<v-btn icon="mdi-delete" size="small" variant="text" color="error" @click.stop="emit('delete-provider', entry.provider)"></v-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -188,6 +188,7 @@
|
||||
<ObjectEditor
|
||||
v-else-if="itemMeta?.type === 'dict'"
|
||||
:model-value="modelValue"
|
||||
:item-meta="itemMeta"
|
||||
@update:model-value="emitUpdate"
|
||||
class="config-field"
|
||||
/>
|
||||
|
||||
@@ -26,8 +26,9 @@
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pa-4" style="max-height: 400px; overflow-y: auto;">
|
||||
<div v-if="localKeyValuePairs.length > 0">
|
||||
<div v-for="(pair, index) in localKeyValuePairs" :key="index" class="key-value-pair">
|
||||
<!-- Regular key-value pairs (non-template) -->
|
||||
<div v-if="nonTemplatePairs.length > 0">
|
||||
<div v-for="(pair, index) in nonTemplatePairs" :key="index" class="key-value-pair">
|
||||
<v-row no-gutters align="center" class="mb-2">
|
||||
<v-col cols="4">
|
||||
<v-text-field
|
||||
@@ -48,15 +49,29 @@
|
||||
hide-details
|
||||
placeholder="字符串值"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-else-if="pair.type === 'number'"
|
||||
v-model.number="pair.value"
|
||||
type="number"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
placeholder="数值"
|
||||
></v-text-field>
|
||||
<div v-else-if="pair.type === 'number' || pair.type === 'float' || pair.type === 'int'" class="d-flex align-center gap-2 flex-grow-1">
|
||||
<v-slider
|
||||
v-if="pair.slider"
|
||||
:model-value="Number(pair.value) || 0"
|
||||
@update:model-value="pair.value = $event"
|
||||
:min="pair.slider.min"
|
||||
:max="pair.slider.max"
|
||||
:step="pair.slider.step"
|
||||
color="primary"
|
||||
density="compact"
|
||||
hide-details
|
||||
class="flex-grow-1"
|
||||
></v-slider>
|
||||
<v-text-field
|
||||
v-model.number="pair.value"
|
||||
type="number"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
placeholder="数值"
|
||||
:style="pair.slider ? 'max-width: 120px;' : ''"
|
||||
></v-text-field>
|
||||
</div>
|
||||
<v-switch
|
||||
v-else-if="pair.type === 'boolean'"
|
||||
v-model="pair.value"
|
||||
@@ -81,7 +96,7 @@
|
||||
variant="text"
|
||||
size="small"
|
||||
color="error"
|
||||
@click="removeKeyValuePair(index)"
|
||||
@click="removeKeyValuePairByKey(pair.key)"
|
||||
>
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
@@ -89,7 +104,79 @@
|
||||
</v-row>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-center py-8">
|
||||
|
||||
<!-- Template schema fields -->
|
||||
<div v-if="hasTemplateSchema" class="mt-4">
|
||||
<v-divider class="mb-3"></v-divider>
|
||||
<div class="text-caption text-grey mb-2">预设</div>
|
||||
<div v-for="(template, templateKey) in templateSchema" :key="templateKey" class="template-field" :class="{ 'template-field-inactive': !isTemplateKeyAdded(templateKey) }">
|
||||
<v-row no-gutters align="center" class="mb-2">
|
||||
<v-col cols="4">
|
||||
<div class="d-flex flex-column">
|
||||
<span class="text-caption font-weight-medium">{{ template.name || template.description || templateKey }}</span>
|
||||
<span v-if="template.hint" class="text-caption text-grey" style="font-size: 0.7rem;">{{ template.hint }}</span>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="7" class="pl-2 d-flex align-center justify-end">
|
||||
<v-text-field
|
||||
v-if="template.type === 'string'"
|
||||
:model-value="getTemplateValue(templateKey)"
|
||||
@update:model-value="updateTemplateValue(templateKey, $event)"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
placeholder="字符串值"
|
||||
></v-text-field>
|
||||
<div v-else-if="template.type === 'number' || template.type === 'float' || template.type === 'int'" class="d-flex align-center ga-4 flex-grow-1">
|
||||
<v-slider
|
||||
v-if="template.slider"
|
||||
:model-value="Number(getTemplateValue(templateKey)) || 0"
|
||||
@update:model-value="updateTemplateValue(templateKey, $event)"
|
||||
:min="template.slider.min"
|
||||
:max="template.slider.max"
|
||||
:step="template.slider.step"
|
||||
color="primary"
|
||||
density="compact"
|
||||
hide-details
|
||||
class="flex-grow-1"
|
||||
></v-slider>
|
||||
<v-text-field
|
||||
:model-value="getTemplateValue(templateKey)"
|
||||
@update:model-value="updateTemplateValue(templateKey, $event)"
|
||||
type="number"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
placeholder="数值"
|
||||
:style="template.slider ? 'max-width: 120px;' : ''"
|
||||
></v-text-field>
|
||||
</div>
|
||||
<v-switch
|
||||
v-else-if="template.type === 'boolean' || template.type === 'bool'"
|
||||
:model-value="getTemplateValue(templateKey)"
|
||||
@update:model-value="updateTemplateValue(templateKey, $event)"
|
||||
density="compact"
|
||||
hide-details
|
||||
color="primary"
|
||||
></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="1" class="pl-2">
|
||||
<v-btn
|
||||
v-if="isTemplateKeyAdded(templateKey)"
|
||||
icon
|
||||
variant="text"
|
||||
size="small"
|
||||
color="error"
|
||||
@click="removeTemplateKey(templateKey)"
|
||||
>
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="localKeyValuePairs.length === 0 && !hasTemplateSchema" class="text-center py-8">
|
||||
<v-icon size="64" color="grey-lighten-1">mdi-code-json</v-icon>
|
||||
<p class="text-grey mt-4">暂无参数</p>
|
||||
</div>
|
||||
@@ -142,6 +229,10 @@ const props = defineProps({
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
itemMeta: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
buttonText: {
|
||||
type: String,
|
||||
default: '修改'
|
||||
@@ -164,11 +255,25 @@ const originalKeyValuePairs = ref([])
|
||||
const newKey = ref('')
|
||||
const newValueType = ref('string')
|
||||
|
||||
// Template schema support
|
||||
const templateSchema = computed(() => {
|
||||
return props.itemMeta?.template_schema || {}
|
||||
})
|
||||
|
||||
const hasTemplateSchema = computed(() => {
|
||||
return Object.keys(templateSchema.value).length > 0
|
||||
})
|
||||
|
||||
// 计算要显示的键名
|
||||
const displayKeys = computed(() => {
|
||||
return Object.keys(props.modelValue).slice(0, props.maxDisplayItems)
|
||||
})
|
||||
|
||||
// 分离模板字段和普通字段
|
||||
const nonTemplatePairs = computed(() => {
|
||||
return localKeyValuePairs.value.filter(pair => !templateSchema.value[pair.key])
|
||||
})
|
||||
|
||||
// 监听 modelValue 变化,主要用于初始化
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
// This watch is primarily for initialization or external changes
|
||||
@@ -180,10 +285,24 @@ function initializeLocalKeyValuePairs() {
|
||||
for (const [key, value] of Object.entries(props.modelValue)) {
|
||||
let _type = (typeof value) === 'object' ? 'json':(typeof value)
|
||||
let _value = _type === 'json'?JSON.stringify(value):value
|
||||
|
||||
// Check if this key has a template schema
|
||||
const template = templateSchema.value[key]
|
||||
if (template) {
|
||||
// Use template type if available
|
||||
_type = template.type || _type
|
||||
// Use template default if value is missing
|
||||
if (_value === undefined || _value === null) {
|
||||
_value = template.default !== undefined ? template.default : _value
|
||||
}
|
||||
}
|
||||
|
||||
localKeyValuePairs.value.push({
|
||||
key: key,
|
||||
value: _value,
|
||||
type: _type
|
||||
type: _type,
|
||||
slider: template?.slider,
|
||||
template: template
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -239,8 +358,11 @@ function updateJSON(index, newValue) {
|
||||
}
|
||||
}
|
||||
|
||||
function removeKeyValuePair(index) {
|
||||
localKeyValuePairs.value.splice(index, 1)
|
||||
function removeKeyValuePairByKey(key) {
|
||||
const index = localKeyValuePairs.value.findIndex(pair => pair.key === key)
|
||||
if (index >= 0) {
|
||||
localKeyValuePairs.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
function updateKey(index, newKey) {
|
||||
@@ -258,10 +380,83 @@ function updateKey(index, newKey) {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查新键名是否有模板
|
||||
const template = templateSchema.value[newKey]
|
||||
if (template) {
|
||||
// 更新类型和默认值
|
||||
localKeyValuePairs.value[index].type = template.type || localKeyValuePairs.value[index].type
|
||||
if (localKeyValuePairs.value[index].value === undefined || localKeyValuePairs.value[index].value === null || localKeyValuePairs.value[index].value === '') {
|
||||
localKeyValuePairs.value[index].value = template.default !== undefined ? template.default : localKeyValuePairs.value[index].value
|
||||
}
|
||||
localKeyValuePairs.value[index].slider = template.slider
|
||||
localKeyValuePairs.value[index].template = template
|
||||
} else {
|
||||
// 清除模板信息
|
||||
localKeyValuePairs.value[index].slider = undefined
|
||||
localKeyValuePairs.value[index].template = undefined
|
||||
}
|
||||
|
||||
// 更新本地副本
|
||||
localKeyValuePairs.value[index].key = newKey
|
||||
}
|
||||
|
||||
function isTemplateKeyAdded(templateKey) {
|
||||
return localKeyValuePairs.value.some(pair => pair.key === templateKey)
|
||||
}
|
||||
|
||||
function getTemplateValue(templateKey) {
|
||||
const pair = localKeyValuePairs.value.find(pair => pair.key === templateKey)
|
||||
if (pair) {
|
||||
return pair.value
|
||||
}
|
||||
const template = templateSchema.value[templateKey]
|
||||
return template?.default !== undefined ? template.default : getDefaultValueForType(template?.type || 'string')
|
||||
}
|
||||
|
||||
function updateTemplateValue(templateKey, newValue) {
|
||||
const existingIndex = localKeyValuePairs.value.findIndex(pair => pair.key === templateKey)
|
||||
const template = templateSchema.value[templateKey]
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
// 更新现有值
|
||||
localKeyValuePairs.value[existingIndex].value = newValue
|
||||
} else {
|
||||
// 添加新字段
|
||||
let valueType = template?.type || 'string'
|
||||
localKeyValuePairs.value.push({
|
||||
key: templateKey,
|
||||
value: newValue,
|
||||
type: valueType,
|
||||
slider: template?.slider,
|
||||
template: template
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function removeTemplateKey(templateKey) {
|
||||
const index = localKeyValuePairs.value.findIndex(pair => pair.key === templateKey)
|
||||
if (index >= 0) {
|
||||
localKeyValuePairs.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
function getDefaultValueForType(type) {
|
||||
switch (type) {
|
||||
case 'int':
|
||||
case 'float':
|
||||
case 'number':
|
||||
return 0
|
||||
case 'bool':
|
||||
case 'boolean':
|
||||
return false
|
||||
case 'json':
|
||||
return "{}"
|
||||
case 'string':
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDialog() {
|
||||
const updatedValue = {}
|
||||
for (const pair of localKeyValuePairs.value) {
|
||||
@@ -269,12 +464,17 @@ function confirmDialog() {
|
||||
let convertedValue = pair.value
|
||||
// 根据声明的类型进行转换
|
||||
switch (pair.type) {
|
||||
case 'int':
|
||||
convertedValue = parseInt(pair.value) || 0
|
||||
break
|
||||
case 'float':
|
||||
case 'number':
|
||||
// 尝试转换为数字,如果失败则保持原值(或设为默认值0)
|
||||
convertedValue = Number(pair.value)
|
||||
// 可选:检查是否为有效数字,无效则设为0或报错
|
||||
// if (isNaN(convertedValue)) convertedValue = 0;
|
||||
break
|
||||
case 'bool':
|
||||
case 'boolean':
|
||||
// 布尔值通常由 v-switch 正确处理,但为保险起见可以显式转换
|
||||
// 注意:在 JavaScript 中,只有严格的 false, 0, "", null, undefined, NaN 会被转换为 false
|
||||
@@ -307,4 +507,12 @@ function cancelDialog() {
|
||||
.key-value-pair {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.template-field {
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.template-field-inactive {
|
||||
opacity: 0.8;
|
||||
}
|
||||
</style>
|
||||
@@ -129,6 +129,7 @@
|
||||
"manualDialogPreviewLabel": "Display ID (auto generated)",
|
||||
"manualDialogPreviewHint": "Generated as sourceId/modelId",
|
||||
"manualModelRequired": "Please enter a model ID",
|
||||
"manualModelExists": "Model already exists"
|
||||
"manualModelExists": "Model already exists",
|
||||
"configure": "Configure"
|
||||
}
|
||||
}
|
||||
@@ -130,6 +130,7 @@
|
||||
"manualDialogPreviewLabel": "显示 ID(自动生成)",
|
||||
"manualDialogPreviewHint": "生成规则:源ID/模型ID",
|
||||
"manualModelRequired": "请输入模型 ID",
|
||||
"manualModelExists": "该模型已存在"
|
||||
"manualModelExists": "该模型已存在",
|
||||
"configure": "配置"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user