mirror of
https://github.com/Tencent/WeKnora.git
synced 2026-10-29 18:49:20 +08:00
feat: Revamp ModelEditorDialog and ModelSettings for improved UI and validation
- Replaced the dialog component with a custom modal layout for better user experience. - Enhanced form structure with custom styles and validation for required fields. - Updated model source selection and added dynamic placeholders based on model type. - Improved API validation logic for model saving in both ModelEditorDialog and ModelSettings. - Refactored styles for better alignment and responsiveness across components.
This commit is contained in:
@@ -1,39 +1,36 @@
|
||||
<template>
|
||||
<t-dialog
|
||||
v-model:visible="dialogVisible"
|
||||
:header="isEdit ? '编辑模型' : '添加模型'"
|
||||
:width="600"
|
||||
:on-confirm="handleConfirm"
|
||||
:on-cancel="handleCancel"
|
||||
:confirm-btn="{ content: '保存', loading: saving }"
|
||||
>
|
||||
<div class="model-editor-form">
|
||||
<t-form ref="formRef" :data="formData" :rules="rules" layout="vertical">
|
||||
<!-- 模型名称 -->
|
||||
<t-form-item label="模型名称" name="name">
|
||||
<t-input v-model="formData.name" placeholder="为模型取个名字,如:GPT-4" />
|
||||
</t-form-item>
|
||||
<Teleport to="body">
|
||||
<Transition name="modal">
|
||||
<div v-if="dialogVisible" class="model-editor-overlay" @click.self="handleCancel">
|
||||
<div class="model-editor-modal">
|
||||
<!-- 关闭按钮 -->
|
||||
<button class="close-btn" @click="handleCancel" aria-label="关闭">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path d="M15 5L5 15M5 5L15 15" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- 标题区域 -->
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">{{ isEdit ? '编辑模型' : '添加模型' }}</h2>
|
||||
<p class="modal-desc">{{ getModalDescription() }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 表单内容区域 -->
|
||||
<div class="modal-body">
|
||||
<t-form ref="formRef" :data="formData" :rules="rules" layout="vertical">
|
||||
<!-- 模型来源 -->
|
||||
<t-form-item label="模型来源" name="source">
|
||||
<div class="form-item">
|
||||
<label class="form-label required">模型来源</label>
|
||||
<t-radio-group v-model="formData.source">
|
||||
<t-radio value="local">Ollama (本地)</t-radio>
|
||||
<t-radio value="remote">Remote API (远程)</t-radio>
|
||||
</t-radio-group>
|
||||
</t-form-item>
|
||||
|
||||
<!-- VLLM 专用:接口类型(在来源选择之后) -->
|
||||
<template v-if="modelType === 'vllm' && formData.source === 'local'">
|
||||
<t-form-item label="接口类型" name="interfaceType">
|
||||
<t-radio-group v-model="formData.interfaceType">
|
||||
<t-radio value="ollama">Ollama</t-radio>
|
||||
<t-radio value="openai">OpenAI 兼容接口</t-radio>
|
||||
</t-radio-group>
|
||||
</t-form-item>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Ollama 本地模型选择器 -->
|
||||
<t-form-item v-if="formData.source === 'local' && modelType !== 'vllm'" label="模型标识" name="modelName">
|
||||
<div v-if="formData.source === 'local'" class="form-item">
|
||||
<label class="form-label required">模型名称</label>
|
||||
<t-select
|
||||
v-model="formData.modelName"
|
||||
:loading="loadingOllamaModels"
|
||||
@@ -92,35 +89,39 @@
|
||||
<t-icon name="refresh" />
|
||||
刷新列表
|
||||
</t-button>
|
||||
</t-form-item>
|
||||
</div>
|
||||
|
||||
<!-- Remote API 和 VLLM 保持原有的 input -->
|
||||
<t-form-item v-else label="模型标识" name="modelName">
|
||||
<div v-else class="form-item">
|
||||
<label class="form-label required">模型名称</label>
|
||||
<t-input
|
||||
v-model="formData.modelName"
|
||||
:placeholder="formData.source === 'local' ? '如:llava:latest' : '如:gpt-4, claude-3-opus'"
|
||||
:placeholder="getModelNamePlaceholder()"
|
||||
/>
|
||||
</t-form-item>
|
||||
</div>
|
||||
|
||||
<!-- Remote API 配置或 VLLM OpenAI 兼容接口 -->
|
||||
<template v-if="formData.source === 'remote' || (modelType === 'vllm' && formData.interfaceType === 'openai')">
|
||||
<t-form-item label="Base URL" name="baseUrl">
|
||||
<!-- Remote API 配置 -->
|
||||
<template v-if="formData.source === 'remote'">
|
||||
<div class="form-item">
|
||||
<label class="form-label required">Base URL</label>
|
||||
<t-input
|
||||
v-model="formData.baseUrl"
|
||||
:placeholder="modelType === 'vllm' ? '如:http://localhost:11434/v1' : '如:https://api.openai.com/v1'"
|
||||
/>
|
||||
</t-form-item>
|
||||
</div>
|
||||
|
||||
<t-form-item label="API Key (可选)" name="apiKey">
|
||||
<div class="form-item">
|
||||
<label class="form-label">API Key (可选)</label>
|
||||
<t-input
|
||||
v-model="formData.apiKey"
|
||||
type="password"
|
||||
placeholder="输入 API Key"
|
||||
/>
|
||||
</t-form-item>
|
||||
</div>
|
||||
|
||||
<!-- Remote API 校验 -->
|
||||
<t-form-item label="连接测试">
|
||||
<div class="form-item">
|
||||
<label class="form-label">连接测试</label>
|
||||
<div class="api-test-section">
|
||||
<t-button
|
||||
variant="outline"
|
||||
@@ -146,26 +147,40 @@
|
||||
{{ remoteMessage }}
|
||||
</span>
|
||||
</div>
|
||||
</t-form-item>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Embedding 专用:维度 -->
|
||||
<t-form-item v-if="modelType === 'embedding'" label="向量维度" name="dimension">
|
||||
<div v-if="modelType === 'embedding'" class="form-item">
|
||||
<label class="form-label">向量维度</label>
|
||||
<t-input-number
|
||||
v-model="formData.dimension"
|
||||
:min="128"
|
||||
:max="4096"
|
||||
placeholder="如:1536"
|
||||
/>
|
||||
</t-form-item>
|
||||
</div>
|
||||
|
||||
<!-- 设为默认 -->
|
||||
<t-form-item label=" " name="isDefault">
|
||||
<div class="form-item">
|
||||
<t-checkbox v-model="formData.isDefault">设为默认模型</t-checkbox>
|
||||
</t-form-item>
|
||||
</div>
|
||||
</t-form>
|
||||
</div>
|
||||
</t-dialog>
|
||||
</div>
|
||||
|
||||
<!-- 底部按钮区域 -->
|
||||
<div class="modal-footer">
|
||||
<t-button theme="default" variant="outline" @click="handleCancel">
|
||||
取消
|
||||
</t-button>
|
||||
<t-button theme="primary" @click="handleConfirm" :loading="saving">
|
||||
保存
|
||||
</t-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -239,25 +254,78 @@ const formData = ref<ModelFormData>({
|
||||
})
|
||||
|
||||
const rules = {
|
||||
name: [{ required: true, message: '请输入模型名称' }],
|
||||
modelName: [{ required: true, message: '请输入模型标识' }],
|
||||
modelName: [
|
||||
{ required: true, message: '请输入模型名称' },
|
||||
{
|
||||
validator: (val: string) => {
|
||||
if (!val || !val.trim()) {
|
||||
return { result: false, message: '模型名称不能为空' }
|
||||
}
|
||||
if (val.trim().length > 100) {
|
||||
return { result: false, message: '模型名称不能超过100个字符' }
|
||||
}
|
||||
return { result: true }
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
baseUrl: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入 Base URL',
|
||||
trigger: 'blur'
|
||||
},
|
||||
{
|
||||
validator: (val: string) => {
|
||||
if (!val || !val.trim()) {
|
||||
return { result: false, message: 'Base URL 不能为空' }
|
||||
}
|
||||
// 简单的 URL 格式校验
|
||||
try {
|
||||
new URL(val.trim())
|
||||
return { result: true }
|
||||
} catch {
|
||||
return { result: false, message: 'Base URL 格式不正确,请输入有效的 URL' }
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 获取弹窗描述文字
|
||||
const getModalDescription = () => {
|
||||
const typeDesc = {
|
||||
chat: '配置用于对话的大语言模型',
|
||||
embedding: '配置用于文本向量化的嵌入模型',
|
||||
rerank: '配置用于结果重排序的模型',
|
||||
vllm: '配置用于视觉理解和多模态的视觉语言模型'
|
||||
}
|
||||
return typeDesc[props.modelType] || '配置模型信息'
|
||||
}
|
||||
|
||||
// 获取模型名称占位符
|
||||
const getModelNamePlaceholder = () => {
|
||||
if (props.modelType === 'vllm') {
|
||||
return formData.value.source === 'local' ? '如:llava:latest' : '如:gpt-4-vision-preview'
|
||||
}
|
||||
return formData.value.source === 'local' ? '如:llama2:latest' : '如:gpt-4, claude-3-opus'
|
||||
}
|
||||
|
||||
// 监听 visible 变化,初始化表单
|
||||
watch(() => props.visible, (val) => {
|
||||
if (val) {
|
||||
// 锁定背景滚动
|
||||
document.body.style.overflow = 'hidden'
|
||||
|
||||
if (props.modelData) {
|
||||
formData.value = { ...props.modelData }
|
||||
} else {
|
||||
resetForm()
|
||||
}
|
||||
} else {
|
||||
// 恢复背景滚动
|
||||
document.body.style.overflow = ''
|
||||
}
|
||||
})
|
||||
|
||||
@@ -265,13 +333,13 @@ watch(() => props.visible, (val) => {
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
id: generateId(),
|
||||
name: '',
|
||||
name: '', // 保留字段但不使用,保存时用 modelName
|
||||
source: 'local',
|
||||
modelName: '',
|
||||
baseUrl: '',
|
||||
apiKey: '',
|
||||
dimension: props.modelType === 'embedding' ? 1536 : undefined,
|
||||
interfaceType: props.modelType === 'vllm' ? 'ollama' : undefined,
|
||||
interfaceType: undefined,
|
||||
isDefault: false
|
||||
}
|
||||
modelChecked.value = false
|
||||
@@ -314,6 +382,7 @@ const handleModelFilter = (filterWords: string) => {
|
||||
|
||||
// 加载 Ollama 模型列表
|
||||
const loadOllamaModels = async () => {
|
||||
// 只在选择 local 来源时加载
|
||||
if (formData.value.source !== 'local') return
|
||||
|
||||
loadingOllamaModels.value = true
|
||||
@@ -455,6 +524,34 @@ const checkRemoteAPI = async () => {
|
||||
// 确认保存
|
||||
const handleConfirm = async () => {
|
||||
try {
|
||||
// 手动校验必填字段
|
||||
if (!formData.value.modelName || !formData.value.modelName.trim()) {
|
||||
MessagePlugin.warning('请输入模型名称')
|
||||
return
|
||||
}
|
||||
|
||||
if (formData.value.modelName.trim().length > 100) {
|
||||
MessagePlugin.warning('模型名称不能超过100个字符')
|
||||
return
|
||||
}
|
||||
|
||||
// 如果是 remote 类型,必须填写 baseUrl
|
||||
if (formData.value.source === 'remote') {
|
||||
if (!formData.value.baseUrl || !formData.value.baseUrl.trim()) {
|
||||
MessagePlugin.warning('Remote API 类型必须填写 Base URL')
|
||||
return
|
||||
}
|
||||
|
||||
// 校验 Base URL 格式
|
||||
try {
|
||||
new URL(formData.value.baseUrl.trim())
|
||||
} catch {
|
||||
MessagePlugin.warning('Base URL 格式不正确,请输入有效的 URL')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 执行表单验证
|
||||
await formRef.value?.validate()
|
||||
saving.value = true
|
||||
|
||||
@@ -465,7 +562,7 @@ const handleConfirm = async () => {
|
||||
|
||||
emit('confirm', { ...formData.value })
|
||||
dialogVisible.value = false
|
||||
MessagePlugin.success(isEdit.value ? '模型已更新' : '模型已添加')
|
||||
// 移除此处的成功提示,由父组件统一处理
|
||||
} catch (error) {
|
||||
console.error('表单验证失败:', error)
|
||||
} finally {
|
||||
@@ -580,10 +677,88 @@ const handleCancel = () => {
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.model-editor-form {
|
||||
padding: 8px 0;
|
||||
max-height: 60vh;
|
||||
// 遮罩层
|
||||
.model-editor-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
backdrop-filter: blur(4px);
|
||||
overflow: hidden; // 防止背景滚动
|
||||
}
|
||||
|
||||
// 弹窗主体
|
||||
.model-editor-modal {
|
||||
position: relative;
|
||||
width: 90vw;
|
||||
max-width: 600px;
|
||||
max-height: 85vh;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// 关闭按钮
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
background: #f5f5f5;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #666;
|
||||
transition: all 0.2s ease;
|
||||
z-index: 10;
|
||||
|
||||
&:hover {
|
||||
background: #e5e5e5;
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
|
||||
// 标题区域
|
||||
.modal-header {
|
||||
padding: 24px 32px 20px;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
margin: 0 0 8px 0;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #000000e6;
|
||||
}
|
||||
|
||||
.modal-desc {
|
||||
margin: 0;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 14px;
|
||||
color: #00000066;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
// 内容区域
|
||||
.modal-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 24px 32px;
|
||||
|
||||
// 自定义滚动条
|
||||
&::-webkit-scrollbar {
|
||||
@@ -604,118 +779,110 @@ const handleCancel = () => {
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.t-form-item) {
|
||||
margin-bottom: 18px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
:deep(.t-form) {
|
||||
.t-form-item {
|
||||
display: none; // 隐藏 t-form-item,使用自定义的 form-item
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.t-form-item__label) {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #333333;
|
||||
padding-bottom: 6px;
|
||||
// 表单项样式
|
||||
.form-item {
|
||||
margin-bottom: 20px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
:deep(.t-input),
|
||||
:deep(.t-select),
|
||||
:deep(.t-textarea),
|
||||
:deep(.t-input-number) {
|
||||
font-size: 13px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: #07C05F;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&.t-is-focused {
|
||||
background: #ffffff;
|
||||
border-color: #07C05F;
|
||||
box-shadow: 0 0 0 2px rgba(7, 192, 95, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.t-input__inner) {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
:deep(.t-radio-group) {
|
||||
.t-radio {
|
||||
margin-right: 16px;
|
||||
font-size: 13px;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.t-radio__label {
|
||||
font-size: 13px;
|
||||
color: #333333;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.t-checkbox) {
|
||||
font-size: 13px;
|
||||
|
||||
.t-checkbox__label {
|
||||
font-size: 13px;
|
||||
color: #333333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.model-input-with-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
:deep(.t-input) {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.available {
|
||||
color: #07C05F;
|
||||
}
|
||||
|
||||
&.unavailable {
|
||||
color: #e34d59;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.download-hint {
|
||||
margin-top: -10px;
|
||||
.form-label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-family: "PingFang SC";
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #000000e6;
|
||||
|
||||
:deep(.t-alert) {
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
border-radius: 6px;
|
||||
background: #fff7e6;
|
||||
border-color: #ffa940;
|
||||
&.required::after {
|
||||
content: '*';
|
||||
color: #FA5151;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
// 输入框样式
|
||||
:deep(.t-input),
|
||||
:deep(.t-select),
|
||||
:deep(.t-textarea),
|
||||
:deep(.t-input-number) {
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
|
||||
.t-input__inner,
|
||||
input {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
// 单选按钮组
|
||||
:deep(.t-radio-group) {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
|
||||
.t-radio {
|
||||
margin-right: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.t-radio__label {
|
||||
font-size: 14px;
|
||||
color: #000000e6;
|
||||
}
|
||||
}
|
||||
|
||||
// 复选框
|
||||
:deep(.t-checkbox) {
|
||||
font-size: 14px;
|
||||
|
||||
.t-checkbox__label {
|
||||
font-size: 14px;
|
||||
color: #000000e6;
|
||||
}
|
||||
}
|
||||
|
||||
// 底部按钮区域
|
||||
.modal-footer {
|
||||
padding: 16px 32px;
|
||||
border-top: 1px solid #e5e5e5;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
// 过渡动画
|
||||
.modal-enter-active,
|
||||
.modal-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-enter-from,
|
||||
.modal-leave-to {
|
||||
opacity: 0;
|
||||
|
||||
.model-editor-modal {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
// API 测试区域
|
||||
.api-test-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.test-message {
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
|
||||
&.success {
|
||||
@@ -730,52 +897,17 @@ const handleCancel = () => {
|
||||
:deep(.t-button) {
|
||||
min-width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
// 优化对话框样式
|
||||
:deep(.t-dialog) {
|
||||
border-radius: 12px;
|
||||
|
||||
.t-dialog__header {
|
||||
padding: 20px 24px 16px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
|
||||
.t-dialog__header-content {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
.status-icon {
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.available {
|
||||
color: #07C05F;
|
||||
}
|
||||
}
|
||||
|
||||
.t-dialog__body {
|
||||
padding: 20px 24px;
|
||||
}
|
||||
|
||||
.t-dialog__footer {
|
||||
padding: 12px 24px 20px;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
|
||||
.t-button {
|
||||
font-size: 13px;
|
||||
padding: 6px 16px;
|
||||
border-radius: 6px;
|
||||
|
||||
&.t-button--theme-primary {
|
||||
background: #07C05F;
|
||||
border-color: #07C05F;
|
||||
|
||||
&:hover {
|
||||
background: #05a34e;
|
||||
border-color: #05a34e;
|
||||
}
|
||||
}
|
||||
|
||||
&.t-button--variant-outline {
|
||||
&:hover {
|
||||
border-color: #07C05F;
|
||||
color: #07C05F;
|
||||
}
|
||||
}
|
||||
&.unavailable {
|
||||
color: #e34d59;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,10 @@
|
||||
<p>配置用于对话的大语言模型</p>
|
||||
</div>
|
||||
<t-button size="small" theme="primary" @click="openAddDialog('chat')">
|
||||
<t-icon name="add" />添加模型
|
||||
<template #icon>
|
||||
<t-icon name="add" />
|
||||
</template>
|
||||
添加模型
|
||||
</t-button>
|
||||
</div>
|
||||
|
||||
@@ -59,7 +62,10 @@
|
||||
<p>配置用于文本向量化的嵌入模型</p>
|
||||
</div>
|
||||
<t-button size="small" theme="primary" @click="openAddDialog('embedding')">
|
||||
<t-icon name="add" />添加模型
|
||||
<template #icon>
|
||||
<t-icon name="add" />
|
||||
</template>
|
||||
添加模型
|
||||
</t-button>
|
||||
</div>
|
||||
|
||||
@@ -106,7 +112,10 @@
|
||||
<p>配置用于结果重排序的模型</p>
|
||||
</div>
|
||||
<t-button size="small" theme="primary" @click="openAddDialog('rerank')">
|
||||
<t-icon name="add" />添加模型
|
||||
<template #icon>
|
||||
<t-icon name="add" />
|
||||
</template>
|
||||
添加模型
|
||||
</t-button>
|
||||
</div>
|
||||
|
||||
@@ -152,7 +161,10 @@
|
||||
<p>配置用于视觉理解和多模态的视觉语言模型</p>
|
||||
</div>
|
||||
<t-button size="small" theme="primary" @click="openAddDialog('vllm')">
|
||||
<t-icon name="add" />添加模型
|
||||
<template #icon>
|
||||
<t-icon name="add" />
|
||||
</template>
|
||||
添加模型
|
||||
</t-button>
|
||||
</div>
|
||||
|
||||
@@ -164,9 +176,7 @@
|
||||
<t-tag v-if="model.isDefault" theme="success" size="small">默认</t-tag>
|
||||
</div>
|
||||
<div class="model-meta">
|
||||
<span class="source-tag">
|
||||
{{ model.interfaceType === 'ollama' ? 'Ollama' : 'OpenAI兼容' }}
|
||||
</span>
|
||||
<span class="source-tag">{{ model.source === 'local' ? 'Ollama' : 'OpenAI兼容' }}</span>
|
||||
<!-- <span class="model-id">{{ model.modelName }}</span> -->
|
||||
</div>
|
||||
</div>
|
||||
@@ -259,7 +269,6 @@ function convertToLegacyFormat(model: ModelConfig) {
|
||||
baseUrl: model.parameters.base_url || '',
|
||||
apiKey: model.parameters.api_key || '',
|
||||
dimension: model.parameters.embedding_parameters?.dimension,
|
||||
interfaceType: model.parameters.interface_type,
|
||||
isDefault: model.is_default || false
|
||||
}
|
||||
}
|
||||
@@ -276,8 +285,7 @@ function deduplicateModels(models: any[]) {
|
||||
modelName: model.modelName,
|
||||
baseUrl: model.baseUrl,
|
||||
apiKey: model.apiKey,
|
||||
dimension: model.dimension,
|
||||
interfaceType: model.interfaceType
|
||||
dimension: model.dimension
|
||||
})
|
||||
|
||||
if (seen.has(signature)) {
|
||||
@@ -327,23 +335,55 @@ const editModel = (type: 'chat' | 'embedding' | 'rerank' | 'vllm', model: any) =
|
||||
// 保存模型
|
||||
const handleModelSave = async (modelData: any) => {
|
||||
try {
|
||||
// 字段校验
|
||||
if (!modelData.modelName || !modelData.modelName.trim()) {
|
||||
MessagePlugin.warning('模型名称不能为空')
|
||||
return
|
||||
}
|
||||
|
||||
if (modelData.modelName.trim().length > 100) {
|
||||
MessagePlugin.warning('模型名称不能超过100个字符')
|
||||
return
|
||||
}
|
||||
|
||||
// Remote 类型必须填写 baseUrl
|
||||
if (modelData.source === 'remote') {
|
||||
if (!modelData.baseUrl || !modelData.baseUrl.trim()) {
|
||||
MessagePlugin.warning('Remote API 类型必须填写 Base URL')
|
||||
return
|
||||
}
|
||||
|
||||
// 校验 Base URL 格式
|
||||
try {
|
||||
new URL(modelData.baseUrl.trim())
|
||||
} catch {
|
||||
MessagePlugin.warning('Base URL 格式不正确,请输入有效的 URL')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Embedding 模型必须填写维度
|
||||
if (currentModelType.value === 'embedding') {
|
||||
if (!modelData.dimension || modelData.dimension < 128 || modelData.dimension > 4096) {
|
||||
MessagePlugin.warning('Embedding 模型必须填写有效的向量维度(128-4096)')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 将前端格式转换为后端格式
|
||||
const apiModelData: ModelConfig = {
|
||||
name: modelData.modelName || modelData.name,
|
||||
name: modelData.modelName.trim(), // 使用 modelName 作为 name,并去除首尾空格
|
||||
type: getModelType(currentModelType.value),
|
||||
source: modelData.source,
|
||||
description: '',
|
||||
parameters: {
|
||||
base_url: modelData.baseUrl,
|
||||
api_key: modelData.apiKey,
|
||||
base_url: modelData.baseUrl?.trim() || '',
|
||||
api_key: modelData.apiKey?.trim() || '',
|
||||
...(currentModelType.value === 'embedding' && modelData.dimension ? {
|
||||
embedding_parameters: {
|
||||
dimension: modelData.dimension,
|
||||
truncate_prompt_tokens: 0
|
||||
}
|
||||
} : {}),
|
||||
...(currentModelType.value === 'vllm' && modelData.interfaceType ? {
|
||||
interface_type: modelData.interfaceType
|
||||
} : {})
|
||||
},
|
||||
is_default: modelData.isDefault || false
|
||||
@@ -514,18 +554,6 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
// 确保按钮内的图标垂直居中
|
||||
:deep(.t-button) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
.t-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.model-list-container {
|
||||
|
||||
@@ -163,6 +163,8 @@ type UpdateModelRequest struct {
|
||||
Description string `json:"description"`
|
||||
Parameters types.ModelParameters `json:"parameters"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
Source types.ModelSource `json:"source"`
|
||||
Type types.ModelType `json:"type"`
|
||||
}
|
||||
|
||||
// UpdateModel handles the HTTP request to update an existing model
|
||||
@@ -206,13 +208,13 @@ func (h *ModelHandler) UpdateModel(c *gin.Context) {
|
||||
if req.Name != "" {
|
||||
model.Name = req.Name
|
||||
}
|
||||
if req.Description != "" {
|
||||
model.Description = req.Description
|
||||
}
|
||||
model.Description = req.Description
|
||||
if req.Parameters != (types.ModelParameters{}) {
|
||||
model.Parameters = req.Parameters
|
||||
}
|
||||
model.IsDefault = req.IsDefault
|
||||
model.Source = req.Source
|
||||
model.Type = req.Type
|
||||
|
||||
logger.Infof(ctx, "Updating model, ID: %s, Name: %s", id, model.Name)
|
||||
if err := h.service.UpdateModel(ctx, model); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user