222 lines
9.9 KiB
Vue
222 lines
9.9 KiB
Vue
<template>
|
||
<div class="ds-page">
|
||
<div class="ds-head">
|
||
<el-button type="primary" @click="openCreate">新建数据集</el-button>
|
||
</div>
|
||
<el-table :data="datasets" v-loading="loading" empty-text="暂无数据集">
|
||
<el-table-column prop="name" label="名称" min-width="160" show-overflow-tooltip />
|
||
<el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip />
|
||
<el-table-column label="向量模型" width="200">
|
||
<template #default="{ row }">
|
||
<span v-if="row.embedding_cfg_id">{{ embeddingName(row.embedding_cfg_id) }}</span>
|
||
<el-tag v-else size="small" type="info">未设置</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="分块" width="210">
|
||
<template #default="{ row }">
|
||
{{ row.chunk_size || 800 }} / {{ row.chunk_overlap ?? 150 }}<template v-if="row.unit_pattern">(自动识别:{{ humanizePattern(row.unit_pattern) }})</template>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="created_at" label="创建时间" width="170" />
|
||
<el-table-column label="操作" width="190" fixed="right">
|
||
<template #default="{ row }">
|
||
<el-button link type="primary" @click="$router.push('/datasets/' + row.id)">详情</el-button>
|
||
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
|
||
<el-button link type="danger" @click="remove(row)">删除</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<el-dialog v-model="dialogVisible" :title="editing ? '编辑数据集' : '新建数据集'" width="480px">
|
||
<el-form :model="form" label-width="90px">
|
||
<el-form-item label="名称" required>
|
||
<el-input v-model="form.name" placeholder="如:产品手册" />
|
||
</el-form-item>
|
||
<el-form-item label="描述">
|
||
<el-input v-model="form.description" type="textarea" :rows="2" />
|
||
</el-form-item>
|
||
<el-form-item label="向量模型" required>
|
||
<el-select v-model="form.embedding_cfg_id" placeholder="请选择向量模型" style="width: 100%">
|
||
<el-option v-for="m in embedders" :key="m.id" :label="m.name" :value="m.id" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="分块大小">
|
||
<el-input-number v-model="form.chunk_size" :min="50" :max="5000" :step="50" style="width: 100%" />
|
||
<div class="ds-tip">每块最大字数。系统自动组合分块策略:优先识别文档结构(条文/章节/编号等)按结构切分,无结构时依次按标题感知、语义、递归切分</div>
|
||
</el-form-item>
|
||
<el-form-item label="重叠字数">
|
||
<el-input-number v-model="form.chunk_overlap" :min="0" :max="500" :step="10" style="width: 100%" />
|
||
<div class="ds-tip">相邻分块间的重叠字数,用于保持上下文连贯(语义切分路径自动忽略)</div>
|
||
</el-form-item>
|
||
<el-form-item label="智能体轮次">
|
||
<el-input-number v-model="form.react_rounds" :min="0" :max="10" :step="1" style="width: 100%" />
|
||
<div class="ds-tip">0=关闭(单次检索问答);1~10=启用 ReAct 智能体模式,模型可自主多次调用检索工具,此为轮次上限</div>
|
||
</el-form-item>
|
||
<el-form-item label="向量召回数">
|
||
<el-input-number v-model="form.vec_top_k" :min="-1" :max="50" :step="1" style="width: 100%" />
|
||
<div class="ds-tip">向量检索原始召回数。0=全局默认(20),-1=尽量多(上限100),正数=固定值</div>
|
||
</el-form-item>
|
||
<el-form-item label="全文召回数">
|
||
<el-input-number v-model="form.fts_top_k" :min="-1" :max="50" :step="1" style="width: 100%" />
|
||
<div class="ds-tip">全文检索(BM25)原始召回数。0=全局默认(20),-1=尽量多(上限100),正数=固定值</div>
|
||
</el-form-item>
|
||
<el-form-item label="重排候选数">
|
||
<el-input-number v-model="form.rerank_top_k" :min="-1" :max="50" :step="1" style="width: 100%" />
|
||
<div class="ds-tip">融合后喂给 LLM 重排的候选数,需 ≥ 最终返回数。0=全局默认(10),-1=尽量多(上限60)</div>
|
||
</el-form-item>
|
||
<el-form-item label="最终返回数">
|
||
<el-input-number v-model="form.recall_top_k" :min="-1" :max="50" :step="1" style="width: 100%" />
|
||
<div class="ds-tip">重排后最终返回给回答的引用条数。0=全局默认(5),-1=全部召回(不限制条数,达到相关性门槛的全部返回,仅受总字符物理保护),正数=固定条数</div>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="dialogVisible = false">取消</el-button>
|
||
<el-button type="primary" :loading="saving" @click="save">保存</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { onMounted, ref } from 'vue'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import { listDatasets, saveDataset, deleteDataset } from '../api/dataset.js'
|
||
import { listModelConfigs } from '../api/model_config.js'
|
||
import { getSettings } from '../api/settings.js'
|
||
|
||
// 把识别出的结构正则转成可读名称(第[一二三四五六七八九十百千]+条 → 条文)
|
||
function humanizePattern(p) {
|
||
if (!p) return ''
|
||
if (p.includes('条')) return '条文'
|
||
if (p.includes('回')) return '章回'
|
||
if (p.includes('节')) return '小节'
|
||
if (p.includes('章')) return '章节'
|
||
if (p.includes('篇') || p.includes('部')) return '篇章'
|
||
if (p.includes('(') || p.includes('(')) return '序号'
|
||
if (/\d/.test(p)) return '编号'
|
||
return '结构单元'
|
||
}
|
||
|
||
const datasets = ref([])
|
||
const embedders = ref([])
|
||
const defaults = ref({ chunk_size: 800, chunk_overlap: 150, react_rounds: 0, vec_top_k: 0, fts_top_k: 0, rerank_top_k: 0, recall_top_k: 0 })
|
||
const loading = ref(false)
|
||
const saving = ref(false)
|
||
const dialogVisible = ref(false)
|
||
const editing = ref(false)
|
||
let editingRowCfgId = 0
|
||
let editingRowChunk = { chunk_size: 0, chunk_overlap: 0 }
|
||
const form = ref({ id: 0, name: '', description: '', embedding_cfg_id: 0, chunk_size: 800, chunk_overlap: 150, react_rounds: 0, vec_top_k: 0, fts_top_k: 0, rerank_top_k: 0, recall_top_k: 0 })
|
||
|
||
onMounted(async () => {
|
||
await load()
|
||
try {
|
||
const s = await getSettings()
|
||
if (s) {
|
||
defaults.value.chunk_size = s.chunk_size || 800
|
||
defaults.value.chunk_overlap = s.chunk_overlap ?? 150
|
||
defaults.value.react_rounds = s.react_rounds ?? 0
|
||
}
|
||
} catch { /* 忽略 */ }
|
||
try {
|
||
const m = await listModelConfigs('embedding')
|
||
if (m && m.list) embedders.value = m.list
|
||
} catch { /* 忽略 */ }
|
||
})
|
||
|
||
async function load() {
|
||
loading.value = true
|
||
try {
|
||
const d = await listDatasets()
|
||
if (d && d.list) datasets.value = d.list
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
function embeddingName(id) {
|
||
const m = embedders.value.find(x => x.id === id)
|
||
return m ? m.name : '(已删除的配置)'
|
||
}
|
||
|
||
function openCreate() {
|
||
editing.value = false
|
||
editingRowCfgId = 0
|
||
editingRowChunk = { chunk_size: 0, chunk_overlap: 0 }
|
||
// 默认选中默认向量模型,其次第一个;分块大小/重叠/智能体轮次带出全局设置值
|
||
const def = embedders.value.find(x => x.is_default === 1) || embedders.value[0]
|
||
form.value = { id: 0, name: '', description: '', embedding_cfg_id: def?.id || 0, chunk_size: defaults.value.chunk_size, chunk_overlap: defaults.value.chunk_overlap, react_rounds: defaults.value.react_rounds, vec_top_k: defaults.value.vec_top_k, fts_top_k: defaults.value.fts_top_k, rerank_top_k: defaults.value.rerank_top_k, recall_top_k: defaults.value.recall_top_k }
|
||
dialogVisible.value = true
|
||
}
|
||
|
||
function openEdit(row) {
|
||
editing.value = true
|
||
editingRowCfgId = row.embedding_cfg_id
|
||
editingRowChunk = { chunk_size: row.chunk_size || 800, chunk_overlap: row.chunk_overlap ?? 150 }
|
||
form.value = {
|
||
id: row.id, name: row.name, description: row.description, embedding_cfg_id: row.embedding_cfg_id,
|
||
chunk_size: editingRowChunk.chunk_size, chunk_overlap: editingRowChunk.chunk_overlap,
|
||
react_rounds: row.react_rounds ?? 0,
|
||
vec_top_k: row.vec_top_k ?? 0, fts_top_k: row.fts_top_k ?? 0,
|
||
rerank_top_k: row.rerank_top_k ?? 0, recall_top_k: row.recall_top_k ?? 0,
|
||
}
|
||
dialogVisible.value = true
|
||
}
|
||
|
||
async function save() {
|
||
if (!form.value.name.trim()) {
|
||
ElMessage.warning('请输入数据集名称')
|
||
return
|
||
}
|
||
if (!form.value.embedding_cfg_id) {
|
||
ElMessage.warning('请选择向量模型(数据集必须绑定向量模型)')
|
||
return
|
||
}
|
||
const modelChanged = editing.value && form.value.embedding_cfg_id !== editingRowCfgId
|
||
const chunkChanged = editing.value && (form.value.chunk_size !== editingRowChunk.chunk_size || form.value.chunk_overlap !== editingRowChunk.chunk_overlap)
|
||
if (modelChanged || chunkChanged) {
|
||
const changed = []
|
||
if (modelChanged) changed.push('向量模型')
|
||
if (chunkChanged) changed.push('分块配置')
|
||
try {
|
||
await ElMessageBox.confirm(`${changed.join('与')}已变更,保存后将重新处理该数据集下所有文档(异步进行,可在文档列表查看进度)。是否继续?`, '重新处理文档', { type: 'warning' })
|
||
} catch {
|
||
return
|
||
}
|
||
}
|
||
saving.value = true
|
||
try {
|
||
await saveDataset(form.value)
|
||
ElMessage.success(modelChanged || chunkChanged ? '已保存,正在重新处理全部文档' : '保存成功')
|
||
dialogVisible.value = false
|
||
await load()
|
||
} finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
|
||
async function remove(row) {
|
||
try {
|
||
await ElMessageBox.confirm(`删除数据集「${row.name}」?`, '删除确认', { type: 'warning' })
|
||
} catch {
|
||
return
|
||
}
|
||
try {
|
||
await deleteDataset(row.id)
|
||
ElMessage.success('已删除')
|
||
await load()
|
||
} catch { /* request.js 已提示 */ }
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.ds-head {
|
||
margin-bottom: 12px;
|
||
}
|
||
.ds-tip {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
line-height: 1.4;
|
||
}
|
||
</style>
|