Files
rag-local/ui-src/src/views/DatasetList.vue
T
2026-08-07 13:18:53 +08:00

222 lines
9.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>