ForcePilot/web/src/views/DataBaseInfoView.vue

1423 lines
38 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>
<HeaderComponent
:title="database.name || '数据库信息'"
>
<template #description>
<div class="database-info">
<a-tag color="blue" v-if="database.embed_model">{{ database.embed_model }}</a-tag>
<a-tag color="green" v-if="database.dimension">{{ database.dimension }}</a-tag>
<span class="row-count">{{ database.files ? Object.keys(database.files).length : 0 }} 文件 · {{ database.db_id }}</span>
</div>
</template>
<template #actions>
<a-button type="primary" @click="backToDatabase">
<LeftOutlined /> 返回
</a-button>
<a-button type="primary" @click="showEditModal">
<EditOutlined />
</a-button>
<a-button type="primary" danger @click="deleteDatabse">
<DeleteOutlined />
</a-button>
</template>
</HeaderComponent>
<a-alert v-if="configStore.config.embed_model &&database.embed_model != configStore.config.embed_model" message="向量模型不匹配,请重新选择" type="warning" style="margin: 10px 20px;" />
<!-- 添加编辑对话框 -->
<a-modal v-model:open="editModalVisible" title="编辑知识库信息" @ok="handleEditSubmit">
<a-form :model="editForm" :rules="rules" ref="editFormRef" layout="vertical">
<a-form-item label="知识库名称" name="name" required>
<a-input v-model:value="editForm.name" placeholder="请输入知识库名称" />
</a-form-item>
<a-form-item label="知识库描述" name="description">
<a-textarea v-model:value="editForm.description" placeholder="请输入知识库描述" :rows="4" />
</a-form-item>
</a-form>
</a-modal>
<div class="db-main-container">
<a-tabs v-model:activeKey="state.curPage" class="atab-container" type="card">
<a-tab-pane key="files">
<template #tab><span><ReadOutlined />文件列表</span></template>
<div class="db-tab-container">
<div class="actions">
<a-button @click="handleRefresh" :loading="state.refrashing">刷新</a-button>
</div>
<a-table :columns="columns" :data-source="Object.values(database.files || {})" row-key="file_id" class="my-table">
<template #bodyCell="{ column, text, record }">
<template v-if="column.key === 'filename'">
<a-button class="main-btn" type="link" @click="openFileDetail(record)">{{ text }}</a-button>
</template>
<template v-else-if="column.key === 'type'">
<span :class="['span-type', text]">{{ text?.toUpperCase() }}</span>
</template>
<template v-else-if="column.key === 'status' && text === 'done'">
<CheckCircleFilled style="color: #41A317;"/>
</template>
<template v-else-if="column.key === 'status' && text === 'failed'">
<CloseCircleFilled style="color: #FF4D4F ;"/>
</template>
<template v-else-if="column.key === 'status' && text === 'processing'">
<HourglassFilled style="color: #1677FF;"/>
</template>
<template v-else-if="column.key === 'status' && text === 'waiting'">
<ClockCircleFilled style="color: #FFCD43;"/>
</template>
<template v-else-if="column.key === 'action'">
<a-button class="del-btn" type="link"
@click="deleteFile(text)"
:disabled="state.lock || record.status === 'processing' || record.status === 'waiting' "
>删除
</a-button>
</template>
<span v-else-if="column.key === 'created_at'">{{ formatRelativeTime(Math.round(text*1000)) }}</span>
<span v-else>{{ text }}</span>
</template>
</a-table>
<a-drawer
width="50%"
v-model:open="state.drawer"
class="custom-class"
:title="selectedFile?.filename || '文件详情'"
placement="right"
@after-open-change="afterOpenChange"
>
<h2>共 {{ selectedFile?.lines?.length || 0 }} 个片段</h2>
<p v-for="line in selectedFile?.lines || []" :key="line.id" class="line-text">
{{ line.text }}
</p>
</a-drawer>
</div>
</a-tab-pane>
<a-tab-pane key="add">
<template #tab><span><CloudUploadOutlined />添加文件</span></template>
<div class="db-tab-container">
<div class="upload-section">
<div class="upload-sidebar">
<div class="chunking-params">
<div class="params-info">
<p>调整分块参数可以控制文本的切分方式,影响检索质量和文档加载效率。</p>
</div>
<a-form
:model="chunkParams"
name="basic"
autocomplete="off"
layout="vertical"
>
<a-form-item label="Chunk Size" name="chunk_size">
<a-input-number v-model:value="chunkParams.chunk_size" :min="100" :max="10000" />
<p class="param-description">每个文本片段的最大字符数</p>
</a-form-item>
<a-form-item label="Chunk Overlap" name="chunk_overlap">
<a-input-number v-model:value="chunkParams.chunk_overlap" :min="0" :max="1000" />
<p class="param-description">相邻文本片段间的重叠字符数</p>
</a-form-item>
<a-form-item label="使用文件节点解析器" name="use_parser">
<a-switch v-model:checked="chunkParams.use_parser" />
<p class="param-description">启用特定文件格式的智能分析</p>
</a-form-item>
</a-form>
</div>
</div>
<div class="upload-main">
<div class="source-selector">
<a-radio-group v-model:value="uploadMode" button-style="solid" style="margin-bottom: 16px;">
<a-radio-button value="file">
<FileOutlined /> 上传文件
</a-radio-button>
<a-radio-button value="url">
<LinkOutlined /> 输入网址
</a-radio-button>
</a-radio-group>
</div>
<!-- 文件上传区域 -->
<div class="upload" v-if="uploadMode === 'file'">
<a-upload-dragger
class="upload-dragger"
v-model:fileList="fileList"
name="file"
:multiple="true"
:disabled="state.loading"
:action="'/api/data/upload?db_id=' + databaseId"
:headers="getAuthHeaders()"
@change="handleFileUpload"
@drop="handleDrop"
>
<p class="ant-upload-text">点击或者把文件拖拽到这里上传</p>
<p class="ant-upload-hint">
目前仅支持上传文本文件,如 .pdf, .txt, .md。且同名文件无法重复添加
</p>
</a-upload-dragger>
</div>
<!-- URL 输入区域 -->
<div class="url-input" v-else>
<a-form layout="vertical">
<a-form-item label="网页链接 (每行一个URL)">
<a-textarea
v-model:value="urlList"
placeholder="请输入网页链接,每行一个"
:rows="6"
:disabled="state.loading"
/>
</a-form-item>
</a-form>
<p class="url-hint">
支持添加网页内容系统会自动抓取网页文本并进行分块。请确保URL格式正确且可以公开访问。
</p>
</div>
<div class="actions">
<a-button
type="primary"
@click="chunkData"
:loading="state.loading"
:disabled="(uploadMode === 'file' && fileList.length === 0) || (uploadMode === 'url' && !urlList.trim())"
style="margin: 0px 20px 20px 0;"
>
生成分块
</a-button>
</div>
</div>
</div>
<!-- 分块结果预览区域 -->
<div class="chunk-preview" v-if="chunkResults.length > 0">
<div class="preview-header">
<h3>分块预览 (共 {{ chunkResults.length }} 个文件,{{ getTotalChunks() }} 个分块)</h3>
<a-button
type="primary"
@click="addToDatabase"
:loading="state.adding"
>
添加到数据库
</a-button>
</div>
<a-collapse v-model:activeKey="activeFileKeys">
<a-collapse-panel v-for="(file, fileIdx) in chunkResults" :key="fileIdx" :header="file.filename + ' (' + file.nodes.length + ' 个分块)'">
<div id="result-cards" class="result-cards">
<div v-for="(chunk, index) in file.nodes" :key="index" class="chunk">
<p><strong>#{{ index + 1 }}</strong> {{ chunk.text }}</p>
</div>
</div>
</a-collapse-panel>
</a-collapse>
</div>
</div>
</a-tab-pane>
<a-tab-pane key="query-test" force-render>
<template #tab><span><SearchOutlined />检索测试</span></template>
<div class="query-test-container db-tab-container">
<div class="sider">
<div class="sider-top">
<div class="query-params" v-if="state.curPage == 'query-test'">
<!-- <h3 class="params-title">查询参数</h3> -->
<div class="params-group">
<div class="params-item">
<p>检索数量:</p>
<a-input-number size="small" v-model:value="meta.maxQueryCount" :min="1" :max="20" />
</div>
<div class="params-item">
<p>过滤低质量:</p>
<a-switch v-model:checked="meta.filter" />
</div>
<div class="params-item">
<p>筛选 TopK</p>
<a-input-number size="small" v-model:value="meta.topK" :min="1" :max="meta.maxQueryCount" />
</div>
<div class="params-item" v-if="configStore.config.enable_reranker">
<p>排序方式:</p>
<a-radio-group v-model:value="meta.sortBy" button-style="solid" size="small">
<a-radio-button value="rerank_score">重排序分</a-radio-button>
<a-radio-button value="distance">相似度</a-radio-button>
</a-radio-group>
</div>
</div>
<div class="params-group">
<div class="params-item w100" v-if="configStore.config.enable_reranker">
<p>重排序阈值:</p>
<a-slider v-model:value="meta.rerankThreshold" :min="0" :max="1" :step="0.01" />
</div>
<div class="params-item w100">
<p>距离阈值:</p>
<a-slider v-model:value="meta.distanceThreshold" :min="0" :max="1" :step="0.01" />
</div>
</div>
<div class="params-group">
<div class="params-item col">
<p>重写查询<small>(修改后需重新检索)</small></p>
<a-segmented v-model:value="meta.use_rewrite_query" :options="use_rewrite_queryOptions">
<template #label="{ payload }">
<div>
<p style="margin: 4px 0">{{ payload.subTitle }}</p>
</div>
</template>
</a-segmented>
</div>
</div>
</div>
</div>
<div class="sider-bottom">
</div>
</div>
<div class="query-result-container">
<div class="query-action">
<a-textarea
v-model:value="queryText"
placeholder="填写需要查询的句子"
:auto-size="{ minRows: 2, maxRows: 10 }"
/>
<a-button class="btn-query" @click="onQuery" :disabled="queryText.length == 0">
<span v-if="!state.searchLoading"><SearchOutlined /> 检索</span>
<span v-else><LoadingOutlined /></span>
</a-button>
</div>
<!-- 新增示例按钮 -->
<!-- <div class="query-examples-container">
<div class="examples-title">示例查询:</div>
<div class="query-examples">
<a-button v-for="example in queryExamples" :key="example" @click="useQueryExample(example)">
{{ example }}
</a-button>
</div>
</div> -->
<div class="query-test" v-if="queryResult">
<div class="results-overview">
<div class="results-stats">
<span class="stat-item">
<strong>总数:</strong> {{ queryResult.all_results.length }}
</span>
<span class="stat-item">
<strong>过滤后:</strong> {{ filteredResults.length }}
</span>
<span class="stat-item">
<strong>TopK:</strong> {{ meta.topK }}
</span>
<span class="stat-item">
<strong>排序:</strong> {{ meta.sortBy === 'rerank_score' ? '重排序分' : '相似度' }}
</span>
</div>
<div class="rewritten-query" v-if="queryResult.rw_query">
<strong>重写后查询:</strong>
<span class="query-text">{{ queryResult.rw_query }}</span>
</div>
</div>
<div class="query-result-card" v-for="(result, idx) in (filteredResults)" :key="idx">
<p>
<strong>#{{ idx + 1 }}&nbsp;&nbsp;&nbsp;</strong>
<span>{{ result.file.filename }}&nbsp;&nbsp;&nbsp;</span>
<span><strong>相似度</strong>{{ result.distance.toFixed(4) }}&nbsp;&nbsp;&nbsp;</span>
<span v-if="result.rerank_score"><strong>重排序分</strong>{{ result.rerank_score.toFixed(4) }}</span>
</p>
<p class="query-text">{{ result.entity.text }}</p>
</div>
</div>
</div>
</div>
</a-tab-pane>
<!-- <a-tab-pane key="3" tab="Tab 3">Content of Tab Pane 3</a-tab-pane> -->
</a-tabs>
</div>
</div>
</template>
<script setup>
import { onMounted, reactive, ref, watch, toRaw, onUnmounted, computed } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { useRoute, useRouter } from 'vue-router';
import { useConfigStore } from '@/stores/config'
import { useUserStore } from '@/stores/user'
import { knowledgeBaseApi } from '@/apis/admin_api'
import HeaderComponent from '@/components/HeaderComponent.vue';
import {
ReadOutlined,
LeftOutlined,
CheckCircleFilled,
HourglassFilled,
CloseCircleFilled,
ClockCircleFilled,
DeleteOutlined,
CloudUploadOutlined,
SearchOutlined,
LoadingOutlined,
FileOutlined,
LinkOutlined,
EditOutlined,
} from '@ant-design/icons-vue'
const route = useRoute();
const router = useRouter();
const databaseId = ref(route.params.database_id);
const database = ref({});
const fileList = ref([]);
const selectedFile = ref(null);
// 查询测试
const queryText = ref('');
const queryResult = ref(null)
const filteredResults = ref([])
const configStore = useConfigStore()
const state = reactive({
loading: false,
adding: false,
refrashing: false,
searchLoading: false,
lock: false,
drawer: false,
refreshInterval: null,
curPage: "files",
});
const meta = reactive({
mode: 'search',
maxQueryCount: 30,
filter: true,
use_rewrite_query: 'off',
rerankThreshold: 0.1,
distanceThreshold: 0.3,
topK: 10,
sortBy: 'rerank_score',
});
const use_rewrite_queryOptions = ref([
{ value: 'off', payload: { title: 'off', subTitle: '不启用' } },
{ value: 'on', payload: { title: 'on', subTitle: '启用重写' } },
{ value: 'hyde', payload: { title: 'hyde', subTitle: '伪文档生成' } },
])
const filterQueryResults = () => {
if (!queryResult.value || !queryResult.value.all_results) {
return;
}
let results = toRaw(queryResult.value.all_results);
console.log("results", results);
if (meta.filter) {
results = results.filter(r => r.distance >= meta.distanceThreshold);
console.log("before", results);
// 根据排序方式决定排序逻辑
if (configStore.config.enable_reranker) {
// 先过滤掉低于阈值的结果
results = results.filter(r => r.rerank_score >= meta.rerankThreshold);
// 根据选择的排序方式进行排序
if (meta.sortBy === 'rerank_score') {
results = results.sort((a, b) => b.rerank_score - a.rerank_score);
} else {
// 按距离排序 (数值越大表示越相似)
results = results.sort((a, b) => b.distance - a.distance);
}
} else {
// 没有启用重排序时,默认按距离排序
results = results.sort((a, b) => b.distance - a.distance);
}
console.log("after", results);
results = results.slice(0, meta.topK);
}
filteredResults.value = results;
}
const onQuery = () => {
if (database.value.embed_model != configStore.config.embed_model) {
message.error('向量模型不匹配,请重新选择')
return
}
console.log(queryText.value)
state.searchLoading = true
if (!queryText.value.trim()) {
message.error('请输入查询内容')
state.searchLoading = false
return
}
meta.db_id = database.value.db_id
try {
knowledgeBaseApi.queryTest({
query: queryText.value.trim(),
meta: meta
})
.then(data => {
console.log(data)
queryResult.value = data
filterQueryResults()
})
.catch(error => {
console.error(error)
message.error(error.message)
})
.finally(() => {
state.searchLoading = false
})
} catch (error) {
console.error(error)
message.error(error.message)
state.searchLoading = false
}
}
const handleFileUpload = (event) => {
console.log(event)
console.log(fileList.value)
}
const handleDrop = (event) => {
console.log(event)
console.log(fileList.value)
}
const afterOpenChange = (visible) => {
if (!visible) {
selectedFile.value = null
}
}
const backToDatabase = () => {
router.push('/database')
}
const handleRefresh = () => {
state.refrashing = true
getDatabaseInfo().then(() => {
state.refrashing = false
console.log(database.value)
})
}
const deleteDatabse = () => {
Modal.confirm({
title: '删除数据库',
content: '确定要删除该数据库吗?',
okText: '确认',
cancelText: '取消',
onOk: () => {
state.lock = true
knowledgeBaseApi.deleteDatabase(databaseId.value)
.then(data => {
console.log(data)
message.success(data.message || '删除成功')
router.push('/database')
})
.catch(error => {
console.error(error)
message.error(error.message || '删除失败')
})
.finally(() => {
state.lock = false
})
},
onCancel: () => {
console.log('Cancel');
},
});
}
const openFileDetail = (record) => {
state.lock = true
try {
knowledgeBaseApi.getDocumentDetail(databaseId.value, record.file_id)
.then(data => {
console.log(data)
if (data.status == "failed") {
message.error(data.message)
return
}
state.lock = false
selectedFile.value = {
...record,
lines: data.lines || []
}
state.drawer = true
})
.catch(error => {
console.error(error)
message.error(error.message)
})
} catch (error) {
console.error(error)
message.error('获取文件详情失败')
state.lock = false
}
}
const formatRelativeTime = (timestamp) => {
const now = Date.now();
const secondsPast = (now - timestamp) / 1000;
if (secondsPast < 60) {
return Math.round(secondsPast) + ' 秒前';
} else if (secondsPast < 3600) {
return Math.round(secondsPast / 60) + ' 分钟前';
} else if (secondsPast < 86400) {
return Math.round(secondsPast / 3600) + ' 小时前';
} else {
const date = new Date(timestamp);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
return `${year}${month}${day}`;
}
}
const getDatabaseInfo = () => {
const db_id = databaseId.value
if (!db_id) {
return
}
state.lock = true
return new Promise((resolve, reject) => {
knowledgeBaseApi.getDatabaseInfo(db_id)
.then(data => {
database.value = data
resolve(data)
})
.catch(error => {
console.error(error)
message.error(error.message || '获取数据库信息失败')
reject(error)
})
.finally(() => {
state.lock = false
})
})
}
const deleteFile = (fileId) => {
console.log(fileId)
//删除提示
Modal.confirm({
title: '删除文件',
content: '确定要删除该文件吗?',
okText: '确认',
cancelText: '取消',
onOk: () => {
state.lock = true
knowledgeBaseApi.deleteFile(databaseId.value, fileId)
.then(data => {
console.log(data)
message.success(data.message || '删除成功')
getDatabaseInfo()
})
.catch(error => {
console.error(error)
message.error(error.message || '删除失败')
})
.finally(() => {
state.lock = false
})
},
onCancel: () => {
console.log('Cancel');
},
});
}
const chunkParams = ref({
chunk_size: 1000,
chunk_overlap: 200,
use_parser: false,
})
const chunkResults = ref([]);
const activeFileKeys = ref([]);
// 获取所有分块的总数
const getTotalChunks = () => {
return chunkResults.value.reduce((total, file) => total + file.nodes.length, 0);
}
// 分块预览
const chunkFiles = () => {
console.log(fileList.value)
const files = fileList.value.filter(file => file.status === 'done').map(file => file.response.file_path)
console.log(files)
if (files.length === 0) {
message.error('请先上传文件')
return
}
state.loading = true
// 调用file-to-chunk接口获取分块信息
knowledgeBaseApi.fileToChunk({
files: files,
params: chunkParams.value
})
.then(data => {
console.log('文件分块信息:', data)
chunkResults.value = Object.values(data);
activeFileKeys.value = chunkResults.value.length > 0 ? [0] : []; // 默认展开第一个文件
})
.catch(error => {
console.error(error)
message.error(error.message)
})
.finally(() => {
state.loading = false
})
}
// 分块预览
const chunkUrls = () => {
// 分割并过滤URL列表
const urls = urlList.value.split('\n')
.map(url => url.trim())
.filter(url => url.length > 0 && (url.startsWith('http://') || url.startsWith('https://')));
if (urls.length === 0) {
message.error('请输入有效的网页链接必须以http://或https://开头)');
return;
}
state.loading = true;
// 调用url-to-chunk接口获取分块信息
knowledgeBaseApi.urlToChunk({
urls: urls,
params: chunkParams.value
})
.then(data => {
console.log('URL分块信息:', data);
chunkResults.value = Object.values(data);
activeFileKeys.value = chunkResults.value.length > 0 ? [0] : []; // 默认展开第一个
})
.catch(error => {
console.error(error);
message.error(error.message || '处理URL失败');
})
.finally(() => {
state.loading = false;
});
};
// 添加到数据库
const addToDatabase = () => {
if (chunkResults.value.length === 0) {
message.error('没有可添加的分块')
return
}
state.adding = true
state.lock = true
// 转换为API需要的格式
const fileChunks = {};
chunkResults.value.forEach(file => {
fileChunks[file.file_id] = file;
});
// 调用add-by-chunks接口将分块添加到数据库
knowledgeBaseApi.addByChunks({
db_id: databaseId.value,
file_chunks: fileChunks
})
.then(data => {
console.log(data)
if (data.status === 'failed') {
message.error(data.message)
} else {
message.success(data.message)
fileList.value = []
chunkResults.value = []
activeFileKeys.value = []
}
})
.catch(error => {
console.error(error)
message.error(error.message)
})
.finally(() => {
getDatabaseInfo()
state.adding = false
state.lock = false
})
}
const addDocumentByFile = () => {
// 此函数不再需要由chunkFiles和addToDatabase替代
console.log('此功能已被拆分为两个步骤')
}
const columns = [
// { title: '文件ID', dataIndex: 'file_id', key: 'file_id' },
{ title: '文件名', dataIndex: 'filename', key: 'filename' },
{ title: '上传时间', dataIndex: 'created_at', key: 'created_at' },
{ title: '状态', dataIndex: 'status', key: 'status' },
{ title: '类型', dataIndex: 'type', key: 'type' },
{ title: '操作', key: 'action', dataIndex: 'file_id' }
];
watch(() => route.params.database_id, (newId) => {
databaseId.value = newId;
console.log(newId)
clearInterval(state.refreshInterval)
getDatabaseInfo()
}
);
// 检测到 meta 变化时重新查询
watch(() => meta, () => {
filterQueryResults()
}, { deep: true })
// 添加更多示例查询
const queryExamples = ref([
'贾宝玉的丫鬟有哪些?',
'请介绍一下红楼梦的主要人物',
'林黛玉是什么性格?',
'曹雪芹的创作背景',
]);
// 使用示例查询的方法
const useQueryExample = (example) => {
queryText.value = example;
onQuery();
};
onMounted(() => {
getDatabaseInfo();
state.refreshInterval = setInterval(() => {
getDatabaseInfo();
}, 10000);
})
// 添加 onUnmounted 钩子,在组件卸载时清除定时器
onUnmounted(() => {
if (state.refreshInterval) {
clearInterval(state.refreshInterval);
state.refreshInterval = null;
}
})
const uploadMode = ref('file');
const urlList = ref('');
const chunkData = () => {
if (uploadMode.value === 'file') {
chunkFiles();
} else if (uploadMode.value === 'url') {
chunkUrls();
}
}
const getAuthHeaders = () => {
const userStore = useUserStore();
return userStore.getAuthHeaders();
};
// 编辑知识库表单
const editModalVisible = ref(false);
const editFormRef = ref(null);
const editForm = reactive({
name: '',
description: ''
});
const rules = {
name: [{ required: true, message: '请输入知识库名称' }]
};
// 显示编辑对话框
const showEditModal = () => {
editForm.name = database.value.name || '';
editForm.description = database.value.description || '';
editModalVisible.value = true;
};
// 提交编辑表单
const handleEditSubmit = () => {
editFormRef.value.validate().then(() => {
updateDatabaseInfo();
}).catch(err => {
console.error('表单验证失败:', err);
});
};
// 更新知识库信息
const updateDatabaseInfo = async () => {
try {
state.lock = true;
const response = await knowledgeBaseApi.updateDatabaseInfo(databaseId.value, {
name: editForm.name,
description: editForm.description
});
message.success('知识库信息更新成功');
editModalVisible.value = false;
await getDatabaseInfo(); // 刷新数据
} catch (error) {
console.error(error);
message.error(error.message || '更新失败');
} finally {
state.lock = false;
}
};
</script>
<style lang="less" scoped>
.database-info {
margin: 8px 0 0;
}
.db-main-container {
display: flex;
width: 100%;
}
.db-tab-container {
display: flex;
flex-direction: column;
gap: 12px;
}
.query-test-container {
display: flex;
flex-direction: row;
gap: 20px;
.sider {
display: flex;
flex-direction: column;
justify-content: space-between;
width: 325px;
height: 100%;
padding: 0;
flex: 0 0 325px;
.sider-top {
.query-params {
display: flex;
flex-direction: column;
box-sizing: border-box;
font-size: 15px;
gap: 12px;
padding-top: 12px;
padding-right: 16px;
border: 1px solid var(--main-light-3);
background-color: var(--main-light-6);
border-radius: 8px;
padding: 16px;
margin-right: 8px;
.params-title {
margin-top: 0;
margin-bottom: 16px;
color: var(--main-color);
font-size: 18px;
text-align: center;
font-weight: bold;
}
.params-group {
margin-bottom: 16px;
padding-bottom: 16px;
border-bottom: 1px solid var(--main-light-3);
&:last-child {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
}
}
.params-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
&:last-child {
margin-bottom: 0;
}
p {
margin: 0;
color: var(--gray-900);
}
&.col {
align-items: flex-start;
flex-direction: column;
width: 100%;
height: auto;
}
&.w100,
&.col {
& > * {
width: 100%;
}
}
}
.ant-slider {
margin: 6px 0px;
}
}
}
}
.query-result-container {
flex: 1;
padding-bottom: 20px;
}
.query-action {
display: flex;
gap: 8px;
margin-bottom: 20px;
textarea {
padding: 12px 16px;
border: 1px solid var(--main-light-2);
}
button.btn-query {
height: auto;
width: 100px;
box-shadow: none;
border: none;
font-weight: bold;
background: var(--main-light-3);
color: var(--main-color);
&:disabled {
cursor: not-allowed;
background: var(--main-light-4);
color: var(--gray-700);
}
}
}
.query-examples-container {
margin-bottom: 20px;
padding: 12px;
background: var(--main-light-6);
border-radius: 8px;
border: 1px solid var(--main-light-3);
.examples-title {
font-weight: bold;
margin-bottom: 10px;
color: var(--main-color);
}
.query-examples {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 10px 0 0;
.ant-btn {
font-size: 14px;
padding: 4px 12px;
height: auto;
background-color: var(--gray-200);
border: none;
color: var(--gray-800);
&:hover {
color: var(--main-color);
}
}
}
}
.query-test {
display: flex;
flex-direction: column;
border-radius: 12px;
gap: 20px;
.results-overview {
background-color: #fff;
border-radius: 8px;
padding: 16px;
border: 1px solid var(--main-light-3);
.results-stats {
display: flex;
justify-content: flex-start;
.stat-item {
border-radius: 4px;
font-size: 14px;
margin-right: 24px;
padding: 4px 8px;
strong {
color: var(--main-color);
margin-right: 4px;
}
}
}
.rewritten-query {
border-radius: 4px;
font-size: 14px;
padding: 4px 8px;
strong {
color: var(--main-color);
margin-right: 8px;
}
.query-text {
font-style: italic;
color: var(--gray-900);
}
}
}
.query-result-card {
padding: 20px;
border-radius: 8px;
background: #fff;
border: 1px solid var(--main-light-3);
transition: box-shadow 0.3s ease;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
p {
margin-bottom: 8px;
line-height: 1.6;
color: var(--gray-900);
&:last-child {
margin-bottom: 0;
}
}
strong {
color: var(--main-color);
}
.query-text {
font-size: 15px;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--main-light-3);
}
}
}
}
.upload {
margin-bottom: 20px;
.upload-dragger {
margin: 0px;
}
}
.my-table {
button.ant-btn-link {
padding: 0;
}
.span-type {
color: white;
padding: 2px 4px;
border-radius: 4px;
font-size: 10px;
font-weight: bold;
opacity: 0.8;
user-select: none;
background: #005F77;
}
.pdf {
background: #005F77;
}
.txt {
background: #068033;
}
.docx, .doc {
background: #2C59B7;
}
.md {
background: #020817;
}
button.main-btn {
font-weight: bold;
font-size: 14px;
&:hover {
cursor: pointer;
color: var(--main-color);
font-weight: bold;
}
}
button.del-btn {
cursor: pointer;
&:hover {
color: var(--error-color);
}
&:disabled {
cursor: not-allowed;
}
}
}
.custom-class .line-text {
padding: 10px;
border-radius: 4px;
&:hover {
background-color: var(--main-light-4);
}
}
.upload-section {
display: flex;
gap: 20px;
.upload-sidebar {
width: 280px;
padding: 20px;
background-color: var(--main-light-6);
border-radius: 8px;
border: 1px solid var(--main-light-3);
// box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
.chunking-params {
h4 {
margin-top: 0;
margin-bottom: 16px;
color: var(--main-color);
font-size: 18px;
text-align: center;
font-weight: bold;
padding-bottom: 10px;
border-bottom: 1px dashed var(--main-light-3);
}
.params-info {
background-color: var(--main-light-4);
border-radius: 6px;
padding: 10px 12px;
margin-bottom: 16px;
p {
margin: 0;
font-size: 13px;
line-height: 1.5;
color: var(--gray-700);
}
}
.ant-form-item {
margin-bottom: 16px;
.ant-form-item-label {
padding-bottom: 6px;
label {
color: var(--gray-800);
font-weight: 500;
font-size: 15px;
}
}
}
.ant-input-number {
width: 100%;
border-radius: 6px;
&:hover, &:focus {
border-color: var(--main-color);
}
}
.ant-switch {
background-color: var(--gray-400);
&.ant-switch-checked {
background-color: var(--main-color);
}
}
// 添加参数说明
.param-description {
color: var(--gray-600);
font-size: 12px;
margin-top: 4px;
margin-bottom: 0;
}
}
}
.upload-main {
flex: 1;
}
}
.chunk-preview {
margin-top: 20px;
.preview-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
h3 {
margin: 0;
color: var(--main-color);
font-size: 18px;
}
}
.result-cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(600px, 1fr));
gap: 12px;
margin-top: 10px;
}
.chunk {
background-color: var(--main-light-5);
border: 1px solid var(--main-light-3);
border-radius: 8px;
padding: 16px;
word-wrap: break-word;
word-break: break-all;
transition: all 0.2s ease;
&:hover {
background-color: var(--main-light-4);
border-color: var(--main-light-2);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06);
}
p {
margin: 0;
line-height: 1.6;
strong {
color: var(--main-color);
margin-right: 6px;
}
}
}
}
.url-input {
margin-bottom: 20px;
}
.url-input .ant-textarea {
border-color: var(--main-light-3);
background-color: #fff;
font-family: monospace;
resize: vertical;
}
.url-input .ant-textarea:hover,
.url-input .ant-textarea:focus {
border-color: var(--main-color);
}
.url-hint {
font-size: 13px;
color: var(--gray-600);
margin-top: 5px;
line-height: 1.5;
}
</style>
<style lang="less">
.atab-container {
padding: 0;
width: 100%;
max-height: 100%;
overflow: auto;
div.ant-tabs-nav {
background: var(--main-light-5);
padding: 8px 20px;
padding-bottom: 0;
}
.ant-tabs-content-holder {
padding: 0 20px;
}
}
.params-item.col .ant-segmented {
width: 100%;
div.ant-segmented-group {
display: flex;
justify-content: space-around;
}
}
</style>
<style lang="less">
.db-main-container {
.atab-container {
padding: 0;
width: 100%;
max-height: 100%;
overflow: auto;
div.ant-tabs-nav {
background: var(--main-light-5);
padding: 8px 20px;
padding-bottom: 0;
}
.ant-tabs-content-holder {
padding: 0 20px;
}
}
.params-item.col .ant-segmented {
width: 100%;
font-size: smaller;
div.ant-segmented-group {
display: flex;
justify-content: space-around;
}
label.ant-segmented-item {
flex: 1;
text-align: center;
div.ant-segmented-item-label > div > p {
font-size: small;
}
}
}
}
</style>