Compare commits

...

5 Commits

Author SHA1 Message Date
b715b8e859 feat: 扩展 Settings 页面功能
- 优化模型配置表单
- 添加更多设置选项

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 10:47:08 +08:00
afa3026585 feat: 增强 Knowledge 创建流程步骤验证
- 添加步骤有效性验证逻辑
- 支持跳转到已完成步骤
- 优化创建流程用户体验

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 10:47:03 +08:00
44ce7156cf feat: 新增 FormDialog 通用表单对话框组件
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 10:46:48 +08:00
a104046fbd feat: 扩展 Model 设置功能
- 添加 Embedding 模型类型支持
- 优化模型设置表单样式

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 17:19:11 +08:00
0c63c3b44d feat: 支持 Embedding 模型连接测试
- 添加 model_type 字段区分 chat 和 embedding 模型
- 根据 model_type 使用不同的 API 端点和请求格式
- embedding 模型使用 /v1/embeddings 端点

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 17:18:30 +08:00
8 changed files with 477 additions and 272 deletions

View File

@@ -58,6 +58,7 @@ type UpdateModelRequest struct {
type TestModelRequest struct {
Provider string `json:"provider" binding:"required"`
Model string `json:"model" binding:"required"`
ModelType string `json:"model_type" binding:"required"`
APIKey string `json:"api_key" binding:"required"`
BaseURL string `json:"base_url" binding:"required"`
APIEndpoint string `json:"api_endpoint"`

View File

@@ -114,8 +114,6 @@ func (s *ModelService) Delete(id string) error {
// TestConnection 测试模型连接
func (s *ModelService) TestConnection(req model.TestModelRequest) (*model.TestModelResponse, error) {
log.Printf("[TestConnection] 开始测试连接: provider=%s, model=%s, base_url=%s, api_endpoint=%s", req.Provider, req.Model, req.BaseURL, req.APIEndpoint)
// 构建请求 URL
baseURL := req.BaseURL
// 去掉 base_url 末尾的斜杠
@@ -126,31 +124,49 @@ func (s *ModelService) TestConnection(req model.TestModelRequest) (*model.TestMo
apiEndpoint := strings.TrimLeft(req.APIEndpoint, "/")
baseURL = baseURL + "/" + apiEndpoint
} else {
// 默认端点 - 根据不同 provider 设置
switch req.Provider {
case "OpenAI":
baseURL = baseURL + "/v1/chat/completions"
case "Ollama":
baseURL = baseURL + "/api/chat"
case "ali", "Ali", "aliyun", "Aliyun":
// 阿里云 DashScope 兼容 OpenAI 格式,需要添加 /chat/completions
// base_url 格式: https://dashscope.aliyuncs.com/compatible-mode/v1
baseURL = baseURL + "/chat/completions"
// 根据 model_type 确定端点
switch req.ModelType {
case "embedding":
// embedding 模型使用 /v1/embeddings
switch req.Provider {
case "Ollama":
baseURL = baseURL + "/api/embeddings"
default:
baseURL = baseURL + "/v1/embeddings"
}
default:
// 默认使用 OpenAI 兼容格式
baseURL = baseURL + "/v1/chat/completions"
// chat 模型使用 /chat/completions
switch req.Provider {
case "OpenAI":
baseURL = baseURL + "/v1/chat/completions"
case "Ollama":
baseURL = baseURL + "/api/chat"
case "ali", "Ali", "aliyun", "Aliyun":
// 阿里云 DashScope 兼容 OpenAI 格式
baseURL = baseURL + "/chat/completions"
default:
// 默认使用 OpenAI 兼容格式
baseURL = baseURL + "/v1/chat/completions"
}
}
}
log.Printf("[TestConnection] 请求 URL: %s", baseURL)
// 构建请求体
requestBody := map[string]interface{}{
"model": req.Model,
"messages": []map[string]string{
{"role": "user", "content": "Hello"},
},
"max_tokens": 10,
// 构建请求体 - 根据 model_type 使用不同的格式
var requestBody map[string]interface{}
if req.ModelType == "embedding" {
requestBody = map[string]interface{}{
"model": req.Model,
"input": "Hello",
"format": "float",
}
} else {
requestBody = map[string]interface{}{
"model": req.Model,
"messages": []map[string]string{
{"role": "user", "content": "Hello"},
},
"max_tokens": 10,
}
}
body, err := json.Marshal(requestBody)
@@ -191,8 +207,6 @@ func (s *ModelService) TestConnection(req model.TestModelRequest) (*model.TestMo
return &model.TestModelResponse{Success: false, Message: err.Error()}, nil
}
log.Printf("[TestConnection] 响应状态码: %d, 响应体: %s", resp.StatusCode, string(respBody))
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return &model.TestModelResponse{Success: true, Message: "Connection successful"}, nil
}

View File

@@ -0,0 +1,130 @@
<script setup lang="ts">
defineProps<{
modelValue: boolean
title: string
description?: string
icon?: string
iconClass?: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: boolean]
}>()
const close = () => {
emit('update:modelValue', false)
}
</script>
<template>
<Teleport to="body">
<div v-if="modelValue" class="dialog-overlay" @click.self="close">
<div class="dialog-container">
<!-- 头部 -->
<div class="dialog-header">
<div class="flex items-center gap-3">
<div v-if="icon" :class="['dialog-icon', iconClass]">
<i :class="icon"></i>
</div>
<div>
<h3 class="dialog-title">{{ title }}</h3>
<p v-if="description" class="dialog-desc">{{ description }}</p>
</div>
</div>
<button class="btn-icon" @click="close">
<i class="fa-solid fa-xmark text-xl"></i>
</button>
</div>
<!-- 内容 -->
<div class="dialog-content">
<slot></slot>
</div>
<!-- 底部 -->
<div class="dialog-footer">
<slot name="footer"></slot>
</div>
</div>
</div>
</Teleport>
</template>
<style scoped>
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.dialog-container {
background-color: #121218;
border-radius: 12px;
width: 100%;
max-width: 520px;
max-height: 90vh;
overflow: hidden;
display: flex;
flex-direction: column;
border: 1px solid #2a2a3a;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5);
}
.dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px;
border-bottom: 1px solid #2a2a3a;
background-color: rgba(30, 30, 40, 0.5);
}
.dialog-icon {
width: 40px;
height: 40px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
}
.dialog-icon i {
color: white;
font-size: 18px;
}
.dialog-title {
font-size: 18px;
font-weight: 600;
color: white;
}
.dialog-desc {
font-size: 13px;
color: #9ca3af;
margin-top: 2px;
}
.dialog-content {
padding: 20px;
overflow-y: auto;
flex: 1;
}
.dialog-footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 12px;
padding: 16px 20px;
border-top: 1px solid #2a2a3a;
background-color: rgba(30, 30, 40, 0.5);
}
</style>

View File

@@ -1,8 +1,64 @@
<script setup lang="ts">
import { ref } from 'vue'
import { ref, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { useModelSettings } from './settings/useModelSettings'
import './knowledge/knowledge.css'
// 获取已配置的模型列表
const { models, fetchModels } = useModelSettings()
// 步骤验证
const step1Valid = computed(() => !!newKbForm.value.name.trim())
const step2Valid = computed(() => !!embeddingConfig.value.modelId)
const step3Valid = computed(() => true) // Parsing - 暂时默认通过
const step4Valid = computed(() => true) // Storage - 暂时默认通过
// 获取当前步骤是否有效
const isCurrentStepValid = computed(() => {
switch (createStep.value) {
case 1: return step1Valid.value
case 2: return step2Valid.value
case 3: return step3Valid.value
case 4: return step4Valid.value
default: return false
}
})
// 步骤是否可以点击(只能点击当前步骤或已完成的步骤)
const canClickStep = (step: number) => {
if (step === 1) return true
if (step === createStep.value) return true
// 可以点击已完成的前面的步骤
if (step < createStep.value) {
switch (step) {
case 1: return step1Valid.value
case 2: return step2Valid.value
case 3: return step3Valid.value
case 4: return step4Valid.value
default: return false
}
}
return false
}
// 下一步
const nextStep = () => {
if (!isCurrentStepValid.value) {
ElMessage.warning('Please complete the current step first')
return
}
if (createStep.value < 4) {
createStep.value++
}
}
// 上一步
const prevStep = () => {
if (createStep.value > 1) {
createStep.value--
}
}
// 模拟知识库数据
const knowledgeBases = ref([
{
@@ -55,8 +111,7 @@ const newKbForm = ref({
description: '',
})
const embeddingConfig = ref({
provider: '',
model: '',
modelId: '',
})
const parsingConfig = ref({
@@ -71,7 +126,9 @@ const parsingConfig = ref({
const openCreateDialog = () => {
createStep.value = 1
newKbForm.value = { name: '', description: '' }
embeddingConfig.value = { provider: '', model: '' }
embeddingConfig.value = { modelId: '' }
// 获取已配置的模型列表
fetchModels()
parsingConfig.value = {
enablePdf: true,
engine: 'default',
@@ -85,7 +142,7 @@ const openCreateDialog = () => {
const cancelCreate = () => {
newKbForm.value = { name: '', description: '' }
embeddingConfig.value = { provider: '', model: '' }
embeddingConfig.value = { modelId: '' }
parsingConfig.value = {
enablePdf: true,
engine: 'default',
@@ -97,18 +154,6 @@ const cancelCreate = () => {
showCreateDialog.value = false
}
const nextStep = () => {
if (!newKbForm.value.name) {
ElMessage.warning('Please enter knowledge base name')
return
}
createStep.value = 2
}
const prevStep = () => {
createStep.value = 1
}
const createKnowledgeBase = () => {
knowledgeBases.value.push({
id: Date.now().toString(),
@@ -120,7 +165,7 @@ const createKnowledgeBase = () => {
status: 'ready'
})
newKbForm.value = { name: '', description: '' }
embeddingConfig.value = { provider: '', model: '' }
embeddingConfig.value = { modelId: '' }
showCreateDialog.value = false
ElMessage.success('Knowledge base created successfully')
}
@@ -300,11 +345,12 @@ const viewDetail = (kb: any) => {
<span class="menu-desc">Name and description</span>
</div>
<i v-if="createStep === 1" class="fa-solid fa-chevron-right menu-arrow"></i>
<i v-else-if="createStep > 1 && step1Valid" class="fa-solid fa-check-circle menu-check"></i>
</div>
<div
class="menu-item"
:class="{ active: createStep === 2 }"
@click="createStep = 2"
:class="{ active: createStep === 2, disabled: !canClickStep(2) }"
@click="canClickStep(2) && (createStep = 2)"
>
<i class="fa-solid fa-robot menu-icon"></i>
<div class="menu-content">
@@ -312,11 +358,12 @@ const viewDetail = (kb: any) => {
<span class="menu-desc">Embedding & rerank models</span>
</div>
<i v-if="createStep === 2" class="fa-solid fa-chevron-right menu-arrow"></i>
<i v-else-if="createStep > 2 && step2Valid" class="fa-solid fa-check-circle menu-check"></i>
</div>
<div
class="menu-item"
:class="{ active: createStep === 3 }"
@click="createStep = 3"
:class="{ active: createStep === 3, disabled: !canClickStep(3) }"
@click="canClickStep(3) && (createStep = 3)"
>
<i class="fa-solid fa-file-lines menu-icon"></i>
<div class="menu-content">
@@ -324,11 +371,12 @@ const viewDetail = (kb: any) => {
<span class="menu-desc">Document parsing settings</span>
</div>
<i v-if="createStep === 3" class="fa-solid fa-chevron-right menu-arrow"></i>
<i v-else-if="createStep > 3 && step3Valid" class="fa-solid fa-check-circle menu-check"></i>
</div>
<div
class="menu-item"
:class="{ active: createStep === 4 }"
@click="createStep = 4"
:class="{ active: createStep === 4, disabled: !canClickStep(4) }"
@click="canClickStep(4) && (createStep = 4)"
>
<i class="fa-solid fa-hard-drive menu-icon"></i>
<div class="menu-content">
@@ -336,42 +384,7 @@ const viewDetail = (kb: any) => {
<span class="menu-desc">Vector & file storage</span>
</div>
<i v-if="createStep === 4" class="fa-solid fa-chevron-right menu-arrow"></i>
</div>
<div
class="menu-item"
:class="{ active: createStep === 5 }"
@click="createStep = 5"
>
<i class="fa-solid fa-scissors menu-icon"></i>
<div class="menu-content">
<span class="menu-title">Chunking</span>
<span class="menu-desc">Text chunking strategy</span>
</div>
<i v-if="createStep === 5" class="fa-solid fa-chevron-right menu-arrow"></i>
</div>
<div
class="menu-item"
:class="{ active: createStep === 6 }"
@click="createStep = 6"
>
<i class="fa-solid fa-project-diagram menu-icon"></i>
<div class="menu-content">
<span class="menu-title">Knowledge Graph</span>
<span class="menu-desc">Graph extraction settings</span>
</div>
<i v-if="createStep === 6" class="fa-solid fa-chevron-right menu-arrow"></i>
</div>
<div
class="menu-item"
:class="{ active: createStep === 7 }"
@click="createStep = 7"
>
<i class="fa-solid fa-sliders menu-icon"></i>
<div class="menu-content">
<span class="menu-title">Advanced</span>
<span class="menu-desc">Additional settings</span>
</div>
<i v-if="createStep === 7" class="fa-solid fa-chevron-right menu-arrow"></i>
<i v-else-if="createStep > 4 && step4Valid" class="fa-solid fa-check-circle menu-check"></i>
</div>
</div>
@@ -400,31 +413,21 @@ const viewDetail = (kb: any) => {
<span class="section-title">Model Config</span>
</div>
<el-form label-position="top" class="kb-form">
<el-form-item label="Embedding Provider">
<el-select v-model="embeddingConfig.provider" placeholder="Select embedding provider" class="w-full">
<el-option label="OpenAI" value="openai">
<div class="provider-option">
<i class="fa-solid fa-robot"></i>
<span>OpenAI</span>
</div>
</el-option>
<el-option label="Ollama" value="ollama">
<div class="provider-option">
<i class="fa-solid fa-microchip"></i>
<span>Ollama</span>
</div>
</el-option>
<el-option label="Azure OpenAI" value="azure">
<div class="provider-option">
<i class="fa-brands fa-microsoft"></i>
<span>Azure OpenAI</span>
<el-form-item label="Embedding Model">
<el-select v-model="embeddingConfig.modelId" placeholder="Select a configured model" class="w-full" popper-class="dark-select-dropdown">
<el-option
v-for="model in models"
:key="model.id"
:label="model.name"
:value="model.id"
>
<div class="model-option">
<span class="model-name">{{ model.name }}</span>
<span class="model-info">{{ model.provider }} - {{ model.model }}</span>
</div>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="Embedding Model">
<el-input v-model="embeddingConfig.model" placeholder="e.g., text-embedding-ada-002" />
</el-form-item>
</el-form>
</div>
@@ -510,76 +513,25 @@ const viewDetail = (kb: any) => {
</el-form-item>
</el-form>
</div>
<!-- Step 5: Chunking -->
<div v-if="createStep === 5" class="form-content">
<div class="section-header">
<i class="fa-solid fa-scissors section-icon"></i>
<span class="section-title">Chunking</span>
</div>
<el-form label-position="top" class="kb-form">
<el-form-item label="Chunk Size">
<el-input type="number" placeholder="e.g., 500" />
</el-form-item>
<el-form-item label="Chunk Overlap">
<el-input type="number" placeholder="e.g., 50" />
</el-form-item>
</el-form>
</div>
<!-- Step 6: Knowledge Graph -->
<div v-if="createStep === 6" class="form-content">
<div class="section-header">
<i class="fa-solid fa-project-diagram section-icon"></i>
<span class="section-title">Knowledge Graph</span>
</div>
<el-form label-position="top" class="kb-form">
<el-form-item label="Enable Knowledge Graph">
<el-switch />
</el-form-item>
<el-form-item label="Graph Extraction">
<el-select placeholder="Select extraction level" class="w-full">
<el-option label="Entities Only" value="entities" />
<el-option label="Entities & Relations" value="relations" />
</el-select>
</el-form-item>
</el-form>
</div>
<!-- Step 7: Advanced -->
<div v-if="createStep === 7" class="form-content">
<div class="section-header">
<i class="fa-solid fa-sliders section-icon"></i>
<span class="section-title">Advanced</span>
</div>
<el-form label-position="top" class="kb-form">
<el-form-item label="Top K">
<el-input type="number" placeholder="e.g., 5" />
</el-form-item>
<el-form-item label="Score Threshold">
<el-input type="number" placeholder="e.g., 0.7" />
</el-form-item>
</el-form>
</div>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<div class="footer-left">
<span class="step-hint">Step {{ createStep }} of 7</span>
<span class="step-hint">Step {{ createStep }} of 4</span>
</div>
<div class="footer-right">
<el-button @click="cancelCreate" class="cancel-btn">Cancel</el-button>
<el-button v-if="createStep > 1" @click="createStep--" class="prev-btn">
<el-button v-if="createStep > 1" @click="prevStep" class="prev-btn">
<i class="fa-solid fa-arrow-left"></i>
Previous
</el-button>
<el-button v-if="createStep < 7" @click="createStep++" class="next-btn">
<el-button v-if="createStep < 4" :disabled="!isCurrentStepValid" @click="nextStep" class="next-btn">
Next
<i class="fa-solid fa-arrow-right"></i>
</el-button>
<el-button v-if="createStep === 7" @click="createKnowledgeBase" class="confirm-btn">
<el-button v-if="createStep === 4" :disabled="!isCurrentStepValid" @click="createKnowledgeBase" class="confirm-btn">
<i class="fa-solid fa-plus"></i>
Create
</el-button>

View File

@@ -2,6 +2,7 @@
import { ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { useModelSettings } from './settings/useModelSettings'
import FormDialog from '@/components/FormDialog.vue'
import './settings/settings.css'
import './settings/settings-parsing.css'
import './settings/modelSettings.css'
@@ -476,7 +477,7 @@ const saveStorageSettings = () => {
{{ model.model }}
</td>
<td class="text-center">
<span class="model-type-tag">{{ model.model_type }}</span>
<span class="model-type-tag" :class="model.model_type">{{ model.model_type }}</span>
</td>
<td class="text-center text-sm">
{{ model.base_url }}
@@ -498,22 +499,28 @@ const saveStorageSettings = () => {
</table>
<!-- 新增模型弹窗 -->
<el-dialog
v-model="showAddModelForm"
<FormDialog
:model-value="showAddModelForm"
@update:model-value="showAddModelForm = $event"
title="Add New Model"
width="500px"
:close-on-click-modal="false"
class="add-model-dialog"
description="Configure your model settings"
icon="fa-solid fa-brain"
icon-class="bg-gradient-to-br from-primary-orange to-red-500"
>
<p class="dialog-desc">Configure your model settings</p>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Model Name</label>
<input
v-model="newModelForm.name"
type="text"
placeholder="e.g., My GPT-4 Model"
class="input-field"
>
</div>
<el-form label-position="top" class="settings-form">
<el-form-item label="Model Name">
<el-input v-model="newModelForm.name" placeholder="e.g., My GPT-4 Model" />
</el-form-item>
<el-form-item label="Model Type">
<el-select v-model="newModelForm.modelType" placeholder="Select model type">
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Model Type</label>
<el-select v-model="newModelForm.modelType" placeholder="Select model type" class="w-full" size="large" popper-class="dark-select-dropdown">
<el-option
v-for="type in modelTypeOptions"
:key="type.value"
@@ -521,10 +528,11 @@ const saveStorageSettings = () => {
:value="type.value"
/>
</el-select>
</el-form-item>
</div>
<el-form-item label="Provider">
<el-select v-model="newModelForm.provider" placeholder="Select provider">
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Provider</label>
<el-select v-model="newModelForm.provider" placeholder="Select provider" class="w-full" size="large" popper-class="dark-select-dropdown">
<el-option
v-for="provider in providerOptions"
:key="provider.value"
@@ -532,60 +540,86 @@ const saveStorageSettings = () => {
:value="provider.value"
/>
</el-select>
</el-form-item>
<el-form-item label="Model">
<el-input v-model="newModelForm.model" placeholder="e.g., gpt-4o (OpenAI) or llama3 (Ollama)" />
</el-form-item>
<el-form-item label="API Key">
<el-input v-model="newModelForm.apiKey" type="password" placeholder="sk-xxxxx (required for OpenAI)" show-password />
</el-form-item>
<el-form-item label="Base URL">
<el-input v-model="newModelForm.baseUrl" placeholder="https://api.openai.com/v1 (OpenAI) or http://localhost:11434 (Ollama)" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button text @click="cancelAddModel">Cancel</el-button>
<el-button
:loading="testingConnection"
@click="testConnection"
class="test-btn"
>
<i v-if="!testingConnection" class="fa-solid fa-plug"></i>
Test Connection
</el-button>
<el-button type="primary" @click="addModel">Confirm</el-button>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Model</label>
<input
v-model="newModelForm.model"
type="text"
placeholder="e.g., gpt-4o (OpenAI) or llama3 (Ollama)"
class="input-field"
>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">API Key</label>
<input
v-model="newModelForm.apiKey"
type="password"
placeholder="sk-xxxxx (required for OpenAI)"
class="input-field"
>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Base URL</label>
<input
v-model="newModelForm.baseUrl"
type="text"
placeholder="https://api.openai.com/v1 (OpenAI) or http://localhost:11434 (Ollama)"
class="input-field"
>
</div>
<!-- 连接状态提示 -->
<div v-if="connectionStatus === 'success'" class="connection-status success">
<i class="fa-solid fa-check-circle"></i> Connection successful
</div>
<div v-else-if="connectionStatus === 'error'" class="connection-status error">
<i class="fa-solid fa-xmark-circle"></i> Connection failed
</div>
</div>
<template #footer>
<button class="btn-secondary" @click="cancelAddModel">Cancel</button>
<button
:disabled="testingConnection"
class="btn-primary"
@click="testConnection"
>
<i class="fa-solid fa-plug"></i>
Test Connection
</button>
<button class="btn-primary" @click="addModel">
Confirm
</button>
</template>
</el-dialog>
</FormDialog>
<!-- 编辑模型弹窗 -->
<el-dialog
v-model="showEditModelForm"
<FormDialog
:model-value="showEditModelForm"
@update:model-value="showEditModelForm = $event"
title="Edit Model"
width="500px"
:close-on-click-modal="false"
class="add-model-dialog"
description="Update your model settings"
icon="fa-solid fa-pen-to-square"
icon-class="bg-gradient-to-br from-primary-cyan to-blue-500"
>
<p class="dialog-desc">Update your model settings</p>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Model Name</label>
<input
v-model="editForm.name"
type="text"
placeholder="e.g., My GPT-4 Model"
class="input-field"
>
</div>
<el-form label-position="top" class="settings-form">
<el-form-item label="Model Name">
<el-input v-model="editForm.name" placeholder="e.g., My GPT-4 Model" />
</el-form-item>
<el-form-item label="Provider">
<el-select v-model="editForm.provider" placeholder="Select provider">
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Provider</label>
<el-select v-model="editForm.provider" placeholder="Select provider" class="w-full" size="large" popper-class="dark-select-dropdown">
<el-option
v-for="option in providerOptions"
:key="option.value"
@@ -593,14 +627,21 @@ const saveStorageSettings = () => {
:value="option.value"
/>
</el-select>
</el-form-item>
</div>
<el-form-item label="Model">
<el-input v-model="editForm.model" placeholder="e.g., gpt-4, llama2" />
</el-form-item>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Model</label>
<input
v-model="editForm.model"
type="text"
placeholder="e.g., gpt-4, llama2"
class="input-field"
>
</div>
<el-form-item label="Model Type">
<el-select v-model="editForm.modelType" placeholder="Select model type">
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Model Type</label>
<el-select v-model="editForm.modelType" placeholder="Select model type" class="w-full" size="large" popper-class="dark-select-dropdown">
<el-option
v-for="option in modelTypeOptions"
:key="option.value"
@@ -608,38 +649,52 @@ const saveStorageSettings = () => {
:value="option.value"
/>
</el-select>
</el-form-item>
<el-form-item label="API Key">
<el-input v-model="editForm.apiKey" type="password" placeholder="sk-..." show-password />
</el-form-item>
<el-form-item label="Base URL">
<el-input v-model="editForm.baseUrl" placeholder="e.g., https://api.openai.com/v1" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<button class="test-btn" @click="cancelEditModel">Cancel</button>
<el-button
:loading="testingEditConnection"
@click="testConnectionEdit"
class="test-btn"
>
<i v-if="!testingEditConnection" class="fa-solid fa-plug"></i>
Test Connection
</el-button>
<el-button type="primary" @click="updateModel">Confirm</el-button>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">API Key</label>
<input
v-model="editForm.apiKey"
type="password"
placeholder="sk-..."
class="input-field"
>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Base URL</label>
<input
v-model="editForm.baseUrl"
type="text"
placeholder="e.g., https://api.openai.com/v1"
class="input-field"
>
</div>
<!-- 连接状态提示 -->
<div v-if="editConnectionStatus === 'success'" class="connection-status success">
<i class="fa-solid fa-check-circle"></i> Connection successful
</div>
<div v-if="editConnectionStatus === 'error'" class="connection-status error">
<i class="fa-solid fa-xmark-circle"></i> Connection failed
</div>
</div>
<template #footer>
<button class="btn-secondary" @click="cancelEditModel">Cancel</button>
<button
:disabled="testingEditConnection"
class="btn-primary"
@click="testConnectionEdit"
>
<i class="fa-solid fa-plug"></i>
Test Connection
</button>
<button class="btn-primary" @click="updateModel">
Confirm
</button>
</template>
</el-dialog>
</FormDialog>
</div>
</div>
</div>

View File

@@ -31,7 +31,7 @@
}
.search-input:focus {
border-color: #f97316;
border-color: #ffffff;
}
.search-input::placeholder {
@@ -116,7 +116,7 @@
}
.kb-dialog :deep(.el-dialog__headerbtn:hover .el-dialog__close) {
color: #f97316;
color: #ffffff;
}
.kb-dialog :deep(.el-dialog__body) {
@@ -135,8 +135,8 @@
}
.kb-dialog :deep(.el-button--primary) {
background-color: #f97316;
border-color: #f97316;
background-color: #ffffff;
border-color: #ffffff;
}
.kb-dialog :deep(.el-button--primary:hover) {
@@ -226,6 +226,20 @@
border-color: rgba(54, 191, 250, 0.25);
}
.menu-item.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.menu-item.disabled .menu-content {
opacity: 0.5;
}
.menu-check {
color: #22c55e;
font-size: 14px;
}
.menu-icon {
width: 36px;
height: 36px;
@@ -241,9 +255,9 @@
}
.menu-item.active .menu-icon {
background: linear-gradient(135deg, #36bffa 0%, #0ea5e9 100%);
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
color: white;
box-shadow: 0 4px 12px rgba(54, 191, 250, 0.3);
box-shadow: 0 4px 12px rgba(249, 115, 22, 0.3);
}
.menu-content {
@@ -262,7 +276,7 @@
}
.menu-item.active .menu-title {
color: #36bffa;
color: #ffffff;
}
.menu-desc {
@@ -272,7 +286,7 @@
.menu-arrow {
font-size: 12px;
color: #36bffa;
color: #ffffff;
}
/* 右侧内容区 */
@@ -311,11 +325,11 @@
width: 48px;
height: 48px;
border-radius: 14px;
background: linear-gradient(135deg, rgba(54, 191, 250, 0.2) 0%, rgba(14, 165, 233, 0.12) 100%);
background: linear-gradient(135deg, rgba(249, 115, 22, 0.2) 0%, rgba(234, 88, 12, 0.12) 100%);
display: flex;
align-items: center;
justify-content: center;
color: #36bffa;
color: #ffffff;
font-size: 18px;
}
@@ -342,11 +356,11 @@
}
.kb-form :deep(.el-input__wrapper:hover) {
border-color: #36bffa;
border-color: #ffffff;
}
.kb-form :deep(.el-input__wrapper.is-focus) {
border-color: #36bffa;
border-color: #ffffff;
box-shadow: 0 0 0 3px rgba(54, 191, 250, 0.15);
}
@@ -370,11 +384,11 @@
}
.kb-form :deep(.el-textarea__inner:hover) {
border-color: #36bffa;
border-color: #ffffff;
}
.kb-form :deep(.el-textarea__inner:focus) {
border-color: #36bffa;
border-color: #ffffff;
box-shadow: 0 0 0 3px rgba(54, 191, 250, 0.15);
}
@@ -391,11 +405,11 @@
}
.kb-form :deep(.el-select .el-input__wrapper:hover) {
border-color: #36bffa;
border-color: #ffffff;
}
.kb-form :deep(.el-select .el-input__wrapper.is-focus) {
border-color: #36bffa;
border-color: #ffffff;
box-shadow: 0 0 0 3px rgba(54, 191, 250, 0.15);
}
@@ -408,7 +422,24 @@
}
.provider-option i {
color: #36bffa;
color: #ffffff;
}
/* 模型选项 */
.model-option {
display: flex;
flex-direction: column;
gap: 2px;
color: #e8eaed;
}
.model-option .model-name {
font-weight: 500;
}
.model-option .model-info {
font-size: 12px;
color: #9ca3af;
}
/* Parsing 配置样式 */
@@ -454,8 +485,8 @@
/* Switch 样式 */
:deep(.el-switch.is-checked .el-switch__core) {
background-color: #36bffa;
border-color: #36bffa;
background-color: #ffffff;
border-color: #ffffff;
}
.parsing-divider {
@@ -566,7 +597,7 @@
}
.next-btn {
background: linear-gradient(135deg, #36bffa 0%, #0ea5e9 100%);
background: linear-gradient(135deg, #ffffff 0%, #0ea5e9 100%);
border: none;
color: white;
padding: 10px 20px;
@@ -580,7 +611,7 @@
}
.next-btn:hover {
background: linear-gradient(135deg, #4dc3ff 0%, #36bffa 100%);
background: linear-gradient(135deg, #4dc3ff 0%, #ffffff 100%);
box-shadow: 0 6px 16px rgba(54, 191, 250, 0.35);
}

View File

@@ -65,14 +65,33 @@
/* Model Type 标签 */
.model-type-tag {
background-color: rgba(249, 115, 22, 0.2);
color: #f97316;
padding: 4px 8px;
border-radius: 4px;
font-size: 14px;
text-transform: capitalize;
}
/* 不同类型的不同颜色 */
.model-type-tag.chat {
background-color: rgba(59, 130, 246, 0.2);
color: #3b82f6;
}
.model-type-tag.embedding {
background-color: rgba(34, 197, 94, 0.2);
color: #22c55e;
}
.model-type-tag.rerank {
background-color: rgba(168, 85, 247, 0.2);
color: #a855f7;
}
.model-type-tag.vlm {
background-color: rgba(249, 115, 22, 0.2);
color: #f97316;
}
/* 操作按钮 */
.btn-icon {
padding: 6px;

View File

@@ -27,6 +27,8 @@ export function useModelSettings() {
// 默认 Base URL 映射
const defaultBaseUrls: Record<string, string> = {
OpenAI: 'http://localhost:1234/v1',
Ollama: 'http://localhost:11434',
ali: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
}
@@ -120,6 +122,7 @@ export function useModelSettings() {
body: JSON.stringify({
provider: newModelForm.value.provider,
model: newModelForm.value.model,
model_type: newModelForm.value.modelType || 'chat',
api_key: newModelForm.value.apiKey,
base_url: newModelForm.value.baseUrl,
api_endpoint: newModelForm.value.apiEndpoint,