feat: 增强 Knowledge 创建流程步骤验证

- 添加步骤有效性验证逻辑
- 支持跳转到已完成步骤
- 优化创建流程用户体验

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 10:47:03 +08:00
parent 44ce7156cf
commit afa3026585
2 changed files with 140 additions and 157 deletions

View File

@@ -1,8 +1,64 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref, computed } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { useModelSettings } from './settings/useModelSettings'
import './knowledge/knowledge.css' 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([ const knowledgeBases = ref([
{ {
@@ -55,8 +111,7 @@ const newKbForm = ref({
description: '', description: '',
}) })
const embeddingConfig = ref({ const embeddingConfig = ref({
provider: '', modelId: '',
model: '',
}) })
const parsingConfig = ref({ const parsingConfig = ref({
@@ -71,7 +126,9 @@ const parsingConfig = ref({
const openCreateDialog = () => { const openCreateDialog = () => {
createStep.value = 1 createStep.value = 1
newKbForm.value = { name: '', description: '' } newKbForm.value = { name: '', description: '' }
embeddingConfig.value = { provider: '', model: '' } embeddingConfig.value = { modelId: '' }
// 获取已配置的模型列表
fetchModels()
parsingConfig.value = { parsingConfig.value = {
enablePdf: true, enablePdf: true,
engine: 'default', engine: 'default',
@@ -85,7 +142,7 @@ const openCreateDialog = () => {
const cancelCreate = () => { const cancelCreate = () => {
newKbForm.value = { name: '', description: '' } newKbForm.value = { name: '', description: '' }
embeddingConfig.value = { provider: '', model: '' } embeddingConfig.value = { modelId: '' }
parsingConfig.value = { parsingConfig.value = {
enablePdf: true, enablePdf: true,
engine: 'default', engine: 'default',
@@ -97,18 +154,6 @@ const cancelCreate = () => {
showCreateDialog.value = false 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 = () => { const createKnowledgeBase = () => {
knowledgeBases.value.push({ knowledgeBases.value.push({
id: Date.now().toString(), id: Date.now().toString(),
@@ -120,7 +165,7 @@ const createKnowledgeBase = () => {
status: 'ready' status: 'ready'
}) })
newKbForm.value = { name: '', description: '' } newKbForm.value = { name: '', description: '' }
embeddingConfig.value = { provider: '', model: '' } embeddingConfig.value = { modelId: '' }
showCreateDialog.value = false showCreateDialog.value = false
ElMessage.success('Knowledge base created successfully') ElMessage.success('Knowledge base created successfully')
} }
@@ -300,11 +345,12 @@ const viewDetail = (kb: any) => {
<span class="menu-desc">Name and description</span> <span class="menu-desc">Name and description</span>
</div> </div>
<i v-if="createStep === 1" class="fa-solid fa-chevron-right menu-arrow"></i> <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>
<div <div
class="menu-item" class="menu-item"
:class="{ active: createStep === 2 }" :class="{ active: createStep === 2, disabled: !canClickStep(2) }"
@click="createStep = 2" @click="canClickStep(2) && (createStep = 2)"
> >
<i class="fa-solid fa-robot menu-icon"></i> <i class="fa-solid fa-robot menu-icon"></i>
<div class="menu-content"> <div class="menu-content">
@@ -312,11 +358,12 @@ const viewDetail = (kb: any) => {
<span class="menu-desc">Embedding & rerank models</span> <span class="menu-desc">Embedding & rerank models</span>
</div> </div>
<i v-if="createStep === 2" class="fa-solid fa-chevron-right menu-arrow"></i> <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>
<div <div
class="menu-item" class="menu-item"
:class="{ active: createStep === 3 }" :class="{ active: createStep === 3, disabled: !canClickStep(3) }"
@click="createStep = 3" @click="canClickStep(3) && (createStep = 3)"
> >
<i class="fa-solid fa-file-lines menu-icon"></i> <i class="fa-solid fa-file-lines menu-icon"></i>
<div class="menu-content"> <div class="menu-content">
@@ -324,11 +371,12 @@ const viewDetail = (kb: any) => {
<span class="menu-desc">Document parsing settings</span> <span class="menu-desc">Document parsing settings</span>
</div> </div>
<i v-if="createStep === 3" class="fa-solid fa-chevron-right menu-arrow"></i> <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>
<div <div
class="menu-item" class="menu-item"
:class="{ active: createStep === 4 }" :class="{ active: createStep === 4, disabled: !canClickStep(4) }"
@click="createStep = 4" @click="canClickStep(4) && (createStep = 4)"
> >
<i class="fa-solid fa-hard-drive menu-icon"></i> <i class="fa-solid fa-hard-drive menu-icon"></i>
<div class="menu-content"> <div class="menu-content">
@@ -336,42 +384,7 @@ const viewDetail = (kb: any) => {
<span class="menu-desc">Vector & file storage</span> <span class="menu-desc">Vector & file storage</span>
</div> </div>
<i v-if="createStep === 4" class="fa-solid fa-chevron-right menu-arrow"></i> <i v-if="createStep === 4" class="fa-solid fa-chevron-right menu-arrow"></i>
</div> <i v-else-if="createStep > 4 && step4Valid" class="fa-solid fa-check-circle menu-check"></i>
<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>
</div> </div>
</div> </div>
@@ -400,31 +413,21 @@ const viewDetail = (kb: any) => {
<span class="section-title">Model Config</span> <span class="section-title">Model Config</span>
</div> </div>
<el-form label-position="top" class="kb-form"> <el-form label-position="top" class="kb-form">
<el-form-item label="Embedding Provider"> <el-form-item label="Embedding Model">
<el-select v-model="embeddingConfig.provider" placeholder="Select embedding provider" class="w-full"> <el-select v-model="embeddingConfig.modelId" placeholder="Select a configured model" class="w-full" popper-class="dark-select-dropdown">
<el-option label="OpenAI" value="openai"> <el-option
<div class="provider-option"> v-for="model in models"
<i class="fa-solid fa-robot"></i> :key="model.id"
<span>OpenAI</span> :label="model.name"
</div> :value="model.id"
</el-option> >
<el-option label="Ollama" value="ollama"> <div class="model-option">
<div class="provider-option"> <span class="model-name">{{ model.name }}</span>
<i class="fa-solid fa-microchip"></i> <span class="model-info">{{ model.provider }} - {{ model.model }}</span>
<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>
</div> </div>
</el-option> </el-option>
</el-select> </el-select>
</el-form-item> </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> </el-form>
</div> </div>
@@ -510,76 +513,25 @@ const viewDetail = (kb: any) => {
</el-form-item> </el-form-item>
</el-form> </el-form>
</div> </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>
</div> </div>
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
<div class="footer-left"> <div class="footer-left">
<span class="step-hint">Step {{ createStep }} of 7</span> <span class="step-hint">Step {{ createStep }} of 4</span>
</div> </div>
<div class="footer-right"> <div class="footer-right">
<el-button @click="cancelCreate" class="cancel-btn">Cancel</el-button> <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> <i class="fa-solid fa-arrow-left"></i>
Previous Previous
</el-button> </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 Next
<i class="fa-solid fa-arrow-right"></i> <i class="fa-solid fa-arrow-right"></i>
</el-button> </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> <i class="fa-solid fa-plus"></i>
Create Create
</el-button> </el-button>

View File

@@ -31,7 +31,7 @@
} }
.search-input:focus { .search-input:focus {
border-color: #f97316; border-color: #ffffff;
} }
.search-input::placeholder { .search-input::placeholder {
@@ -116,7 +116,7 @@
} }
.kb-dialog :deep(.el-dialog__headerbtn:hover .el-dialog__close) { .kb-dialog :deep(.el-dialog__headerbtn:hover .el-dialog__close) {
color: #f97316; color: #ffffff;
} }
.kb-dialog :deep(.el-dialog__body) { .kb-dialog :deep(.el-dialog__body) {
@@ -135,8 +135,8 @@
} }
.kb-dialog :deep(.el-button--primary) { .kb-dialog :deep(.el-button--primary) {
background-color: #f97316; background-color: #ffffff;
border-color: #f97316; border-color: #ffffff;
} }
.kb-dialog :deep(.el-button--primary:hover) { .kb-dialog :deep(.el-button--primary:hover) {
@@ -226,6 +226,20 @@
border-color: rgba(54, 191, 250, 0.25); 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 { .menu-icon {
width: 36px; width: 36px;
height: 36px; height: 36px;
@@ -241,9 +255,9 @@
} }
.menu-item.active .menu-icon { .menu-item.active .menu-icon {
background: linear-gradient(135deg, #36bffa 0%, #0ea5e9 100%); background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
color: white; 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 { .menu-content {
@@ -262,7 +276,7 @@
} }
.menu-item.active .menu-title { .menu-item.active .menu-title {
color: #36bffa; color: #ffffff;
} }
.menu-desc { .menu-desc {
@@ -272,7 +286,7 @@
.menu-arrow { .menu-arrow {
font-size: 12px; font-size: 12px;
color: #36bffa; color: #ffffff;
} }
/* 右侧内容区 */ /* 右侧内容区 */
@@ -311,11 +325,11 @@
width: 48px; width: 48px;
height: 48px; height: 48px;
border-radius: 14px; 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; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: #36bffa; color: #ffffff;
font-size: 18px; font-size: 18px;
} }
@@ -342,11 +356,11 @@
} }
.kb-form :deep(.el-input__wrapper:hover) { .kb-form :deep(.el-input__wrapper:hover) {
border-color: #36bffa; border-color: #ffffff;
} }
.kb-form :deep(.el-input__wrapper.is-focus) { .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); box-shadow: 0 0 0 3px rgba(54, 191, 250, 0.15);
} }
@@ -370,11 +384,11 @@
} }
.kb-form :deep(.el-textarea__inner:hover) { .kb-form :deep(.el-textarea__inner:hover) {
border-color: #36bffa; border-color: #ffffff;
} }
.kb-form :deep(.el-textarea__inner:focus) { .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); 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) { .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) { .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); box-shadow: 0 0 0 3px rgba(54, 191, 250, 0.15);
} }
@@ -408,7 +422,24 @@
} }
.provider-option i { .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 配置样式 */ /* Parsing 配置样式 */
@@ -454,8 +485,8 @@
/* Switch 样式 */ /* Switch 样式 */
:deep(.el-switch.is-checked .el-switch__core) { :deep(.el-switch.is-checked .el-switch__core) {
background-color: #36bffa; background-color: #ffffff;
border-color: #36bffa; border-color: #ffffff;
} }
.parsing-divider { .parsing-divider {
@@ -566,7 +597,7 @@
} }
.next-btn { .next-btn {
background: linear-gradient(135deg, #36bffa 0%, #0ea5e9 100%); background: linear-gradient(135deg, #ffffff 0%, #0ea5e9 100%);
border: none; border: none;
color: white; color: white;
padding: 10px 20px; padding: 10px 20px;
@@ -580,7 +611,7 @@
} }
.next-btn:hover { .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); box-shadow: 0 6px 16px rgba(54, 191, 250, 0.35);
} }