diff --git a/docs/changelog/update.md b/docs/changelog/update.md new file mode 100644 index 00000000..6c81c23f --- /dev/null +++ b/docs/changelog/update.md @@ -0,0 +1,39 @@ +目前已有的开发计划包括: + + +### v2.0 Prerelease + +💭 **Features Todo** +- [x] 知识库使用 LightRAG(GraphRAG 轻量版)重构(🌟🌟🌟🌟🌟) +- [x] 集成 MinerU 处理 PDF 文件。(🌟🌟🌟) +- [x] 优化现有向量模型的逻辑,全局配置的向量模型作为新建知识库的默认模型,但是查询的时候,使用对应数据库的向量模型查询(单例模式)(🌟🌟🌟) +- [x] 多类型知识库支持(🌟🌟🌟🌟🌟) +- [x] 设计问答知识库,支持针对问答对的调优 +- [x] 添加了批量上传文件的脚本,并优化文件信息 + +🐛**BUGs** +- [x] 智能体的工具切换有问题 +- [ ] 知识图谱可视化页面的效果有点问题 +- [x] 处理失败的文件,在特定情况下会一直处于 processing 状态,且无法删除 +- [ ] 与最新的 LightRAG 版本兼容性存在问题 #233 + +# 💯 More: + +下面的功能会放在后续版本实现,暂时未定 + +- [ ] 封装现有工具为 mcp(stdio)调用 +- [ ] 支持额外 mcp 配置(代码端) +- [ ] 添加用户日志与用户反馈模块,可以在 AgentView 中查看信息(🌟🌟) +- [ ] 对话页面支持文档/图片临时上传(🌟🌟🌟🌟) +- [ ] 在 @web/src/components/ToolCallingResult/KnowledgeBaseResult.vue 文件中,添加点击某个片段的时候可以弹出信息预览框(最好可以提供源文件下载的功能),这里需要将DatabaseInfoView 里面的那个文件详情的弹窗给组件化。 + - 先实现 result 中可以按照 file_id 聚合信息;信息的展示以结果卡片的形式展示 + 但是同时也要能够配置,选择显示源文件还是chunk的结果,如果可以的话,最好是在一个文件里面显示所有的chunk,而不是每个chunk都单独显示。不过似乎有点难度,毕竟是有 overlap 的存在,还是分开吧。对于URL链接的这种,可以选择直接跳转就可以。(sub1: sub2:根据 file_id 可以从后端获取文件的基本信息,比如可下载的路径,chunks 等信息;sub3:在前端获取到所有的 chunks 信息之后,能够根据传入的 chunks_idx 高亮被选中的 chunks 区域;sub4:如果可能的话,使用 markdown 展示(但是存在 overlap 拼接的问题。不知道现在知识库里面是否存储的有 fulltext,这时候 start_idx 和 end_idx 就有用了。) +- [ ] 先实现在 DatabaseInfo.vue 中,点击文件详情按钮,能够弹出文件详情的弹窗,显示的方法有两种,一种是以现在的 chunk 的形式展示,一种是以 markdown渲染全文的形式,但是存在 overlap 拼接的问题。不知道现在知识库里面是否存储的有 fulltext,这时候 start_idx 和 end_idx 就有用了。 +- [ ] 各种结果的可视化:知识库检索页面,工具调用检索页面,知识图谱检索页面 + + +根据这个模糊的分析,判断一下这个需求应该如何实现,务必阅读所有相关的文献,包括但不限于当前代码中,保存文本片段的时候所保存的信息是否支撑功能的实现,当某些需求无法实现的时候,能否采用退而求其次的方法实现略差一些的功能(不要过度增加代码的复杂度)。 + +模糊的需求如下:先实现在 DatabaseInfo.vue 中,点击文件详情按钮,能够弹出文件详情的弹窗,显示的方法有两种,一种是以现在的 chunk 的形式展示,一种是以 markdown渲染全文的形式,但是存在 overlap 拼接的问题。不知道现在知识库里面是否存储的有 fulltext,这时候 start_idx 和 end_idx 就有用了。并不对,直接读取源文件的方法是错的,因为源文件并不全是 markdown 格式,可能是 html、pdf 等结构文件,因此,只能想办法拼接,而不是尝试读取文件。项目中已经安装了 md-editor-v3 ,可以参考 AgentMessageComponent.vue 中对于 markdown 的渲染。 + +先告诉我你要如何实现这些功能,重点实现的是 milvus db 和 chroma db ,lightrag db 尽量实现,实现不了也没事。相关的上下文如下 knowledge_router.py knowledge_base.py DataBaseInfoView.vue。 \ No newline at end of file diff --git a/docs/changelog/v0.2.md b/docs/changelog/v0.2.md deleted file mode 100644 index d022693f..00000000 --- a/docs/changelog/v0.2.md +++ /dev/null @@ -1,28 +0,0 @@ -目前已有的开发计划包括: - - -### v2.0 Prerelease - -💭 **Features Todo** -- [x] 知识库使用 LightRAG(GraphRAG 轻量版)重构(🌟🌟🌟🌟🌟) -- [x] 集成 MinerU 处理 PDF 文件。(🌟🌟🌟) -- [x] 优化现有向量模型的逻辑,全局配置的向量模型作为新建知识库的默认模型,但是查询的时候,使用对应数据库的向量模型查询(单例模式)(🌟🌟🌟) -- [x] 多类型知识库支持(🌟🌟🌟🌟🌟) -- [x] 设计问答知识库,支持针对问答对的调优 -- [x] 添加了批量上传文件的脚本,并优化文件信息 - -🐛**BUGs** -- [x] 智能体的工具切换有问题 -- [ ] 知识图谱可视化页面的效果有点问题 -- [ ] 处理失败的文件,在特定情况下会一直处于 processing 状态,且无法删除 - -# 💯 More: - -下面的功能会放在后续版本实现,暂时未定 - -- [ ] 封装现有工具为 mcp(stdio)调用 -- [ ] 支持额外 mcp 配置(代码端) -- [ ] 添加用户日志与用户反馈模块,可以在 AgentView 中查看信息(🌟🌟) -- [ ] 对话页面支持文档/图片临时上传(🌟🌟🌟🌟) -- [ ] 在Prompt中添加对于参考文献的要求,同时要求使用链接的形式,在聊天页面点击该链接后,可以选择跳转或者弹出信息预览框(最好可以提供源文件下载的功能),这里需要将DatabaseInfoView 里面的那个文件详情的弹窗给组件化。但是同时也要能够配置,选择显示源文件还是chunk的结果,如果可以的话,最好是在一个文件里面显示所有的chunk,而不是每个chunk都单独显示。不过似乎有点难度,毕竟是有 overlap 的存在,还是分开吧。对于URL链接的这种,可以选择直接跳转就可以。 -- [ ] 各种结果的可视化:知识库检索页面,工具调用检索页面,知识图谱检索页面 diff --git a/web/src/components/DatabaseHeader.vue b/web/src/components/DatabaseHeader.vue new file mode 100644 index 00000000..21b6b7ef --- /dev/null +++ b/web/src/components/DatabaseHeader.vue @@ -0,0 +1,144 @@ + + + + + \ No newline at end of file diff --git a/web/src/components/FileDetailModal.vue b/web/src/components/FileDetailModal.vue new file mode 100644 index 00000000..6dbcc1b0 --- /dev/null +++ b/web/src/components/FileDetailModal.vue @@ -0,0 +1,186 @@ + + + + + \ No newline at end of file diff --git a/web/src/components/FileTable.vue b/web/src/components/FileTable.vue new file mode 100644 index 00000000..b40c184a --- /dev/null +++ b/web/src/components/FileTable.vue @@ -0,0 +1,459 @@ + + + + + \ No newline at end of file diff --git a/web/src/components/FileUploadModal.vue b/web/src/components/FileUploadModal.vue new file mode 100644 index 00000000..b16a038e --- /dev/null +++ b/web/src/components/FileUploadModal.vue @@ -0,0 +1,541 @@ + + + + + \ No newline at end of file diff --git a/web/src/components/KnowledgeGraphSection.vue b/web/src/components/KnowledgeGraphSection.vue new file mode 100644 index 00000000..b5bfd32f --- /dev/null +++ b/web/src/components/KnowledgeGraphSection.vue @@ -0,0 +1,183 @@ + + + + + \ No newline at end of file diff --git a/web/src/components/KnowledgeGraphViewer.vue b/web/src/components/KnowledgeGraphViewer.vue index 9662debc..ac4566a7 100644 --- a/web/src/components/KnowledgeGraphViewer.vue +++ b/web/src/components/KnowledgeGraphViewer.vue @@ -76,7 +76,7 @@
节点: {{ stats.displayed_nodes || 0 }} 边: {{ stats.displayed_edges || 0 }} - 已截断 +
@@ -419,6 +419,10 @@ const sigmaSettings = { edgeClickTolerance: 10, // 边点击容差,增加点击检测区域 edgeHoverTolerance: 8, // 边悬停容差 zoomToSizeRatioFunction: (x) => x, // 确保缩放时边的厚度保持合理 + eventListenerOptions: { + wheel: { passive: true }, + touchstart: { passive: true }, + }, } // 初始化Sigma.js diff --git a/web/src/components/QuerySection.vue b/web/src/components/QuerySection.vue new file mode 100644 index 00000000..f0d7d844 --- /dev/null +++ b/web/src/components/QuerySection.vue @@ -0,0 +1,323 @@ + + + + + \ No newline at end of file diff --git a/web/src/layouts/AppLayout.vue b/web/src/layouts/AppLayout.vue index fbda6f92..37f6e7a5 100644 --- a/web/src/layouts/AppLayout.vue +++ b/web/src/layouts/AppLayout.vue @@ -56,7 +56,7 @@ const getRemoteConfig = () => { } const getRemoteDatabase = () => { - databaseStore.refreshDatabase() + databaseStore.getDatabaseInfo() } // Fetch GitHub stars count diff --git a/web/src/stores/database.js b/web/src/stores/database.js index 21846553..5e784e31 100644 --- a/web/src/stores/database.js +++ b/web/src/stores/database.js @@ -1,18 +1,372 @@ -import { ref, computed } from 'vue' -import { defineStore } from 'pinia' -import { databaseApi } from '@/apis/knowledge_api' + +import { defineStore } from 'pinia'; +import { ref, reactive } from 'vue'; +import { message, Modal } from 'ant-design-vue'; +import { databaseApi, documentApi, queryApi } from '@/apis/knowledge_api'; +import { useRouter } from 'vue-router'; export const useDatabaseStore = defineStore('database', () => { - const db = ref({}) - function setDatabase(newDatabase) { - db.value = newDatabase + const router = useRouter(); + + // State + const database = ref({}); + const databaseId = ref(null); + const selectedFile = ref(null); + const queryResult = ref(''); + const queryParams = ref([]); + const meta = reactive({}); + const graphStats = ref({ + displayed_nodes: 0, + displayed_edges: 0, + is_truncated: false, + }); + const selectedRowKeys = ref([]); + + const state = reactive({ + databaseLoading: false, + refrashing: false, + searchLoading: false, + lock: false, + fileDetailModalVisible: false, + fileDetailLoading: false, + batchDeleting: false, + chunkLoading: false, + autoRefresh: false, + queryParamsLoading: false, + isGraphMaximized: false, + rightPanelVisible: true, + }); + + let refreshInterval = null; + + // Actions + async function getDatabaseInfo(id) { + const db_id = id || databaseId.value; + if (!db_id) return; + + state.lock = true; + state.databaseLoading = true; + try { + const data = await databaseApi.getDatabaseInfo(db_id); + database.value = data; + await loadQueryParams(db_id); + } catch (error) { + console.error(error); + message.error(error.message || '获取数据库信息失败'); + } finally { + state.lock = false; + state.databaseLoading = false; + } } - async function refreshDatabase() { - const res = await databaseApi.getDatabases() - console.log("database", res) - setDatabase(res.databases) + async function updateDatabaseInfo(formData) { + try { + state.lock = true; + await databaseApi.updateDatabase(databaseId.value, formData); + message.success('知识库信息更新成功'); + await getDatabaseInfo(); + } catch (error) { + console.error(error); + message.error(error.message || '更新失败'); + } finally { + state.lock = false; + } } - return { db, setDatabase, refreshDatabase } -}) \ No newline at end of file + function deleteDatabase() { + Modal.confirm({ + title: '删除数据库', + content: '确定要删除该数据库吗?', + okText: '确认', + cancelText: '取消', + onOk: async () => { + state.lock = true; + try { + const data = await databaseApi.deleteDatabase(databaseId.value); + message.success(data.message || '删除成功'); + router.push('/database'); + } catch (error) { + console.error(error); + message.error(error.message || '删除失败'); + } finally { + state.lock = false; + } + }, + }); + } + + async function deleteFile(fileId) { + state.lock = true; + try { + await documentApi.deleteDocument(databaseId.value, fileId); + await getDatabaseInfo(); + } catch (error) { + console.error(error); + message.error(error.message || '删除失败'); + throw error; + } finally { + state.lock = false; + } + } + + function handleDeleteFile(fileId) { + Modal.confirm({ + title: '删除文件', + content: '确定要删除该文件吗?', + okText: '确认', + cancelText: '取消', + onOk: () => deleteFile(fileId), + }); + } + + function handleBatchDelete() { + const files = database.value.files || {}; + const validFileIds = selectedRowKeys.value.filter(fileId => { + const file = files[fileId]; + return file && !(file.status === 'processing' || file.status === 'waiting'); + }); + + if (validFileIds.length === 0) { + message.info('没有可删除的文件'); + return; + } + + Modal.confirm({ + title: '批量删除文件', + content: `确定要删除选中的 ${validFileIds.length} 个文件吗?`, + okText: '确认', + cancelText: '取消', + onOk: async () => { + state.batchDeleting = true; + let successCount = 0; + let failureCount = 0; + let progressMessage = message.loading(`正在删除文件 0/${validFileIds.length}`, 0); + + try { + for (let i = 0; i < validFileIds.length; i++) { + const fileId = validFileIds[i]; + try { + await deleteFile(fileId); + successCount++; + } catch (error) { + console.error(`删除文件 ${fileId} 失败:`, error); + failureCount++; + } + progressMessage?.(); + if (i + 1 < validFileIds.length) { + progressMessage = message.loading(`正在删除文件 ${i + 1}/${validFileIds.length}`, 0); + } + } + progressMessage?.(); + if (successCount > 0 && failureCount === 0) { + message.success(`成功删除 ${successCount} 个文件`); + } else if (successCount > 0 && failureCount > 0) { + message.warning(`成功删除 ${successCount} 个文件,${failureCount} 个文件删除失败`); + } else if (failureCount > 0) { + message.error(`${failureCount} 个文件删除失败`); + } + selectedRowKeys.value = []; + await getDatabaseInfo(); + } catch (error) { + progressMessage?.(); + console.error('批量删除出错:', error); + message.error('批量删除过程中发生错误'); + } finally { + state.batchDeleting = false; + } + }, + }); + } + + async function addFiles({ items, contentType, params }) { + if (items.length === 0) { + message.error(contentType === 'file' ? '请先上传文件' : '请输入有效的网页链接'); + return; + } + + state.chunkLoading = true; + try { + const data = await documentApi.addDocuments(databaseId.value, items, { ...params, content_type: contentType }); + if (data.status === 'success') { + const itemType = contentType === 'file' ? '文件' : 'URL'; + message.success(data.message || `${itemType}已提交处理,请稍后在列表刷新查看状态`); + await getDatabaseInfo(); + return true; // Indicate success + } else { + message.error(data.message || '处理失败'); + return false; + } + } catch (error) { + console.error(error); + message.error(error.message || '处理请求失败'); + return false; + } finally { + state.chunkLoading = false; + } + } + + async function openFileDetail(record) { + if (record.status !== 'done') { + message.error('文件未处理完成,请稍后再试'); + return; + } + state.fileDetailModalVisible = true; + selectedFile.value = { ...record, lines: [] }; + state.fileDetailLoading = true; + state.lock = true; + + try { + const data = await documentApi.getDocumentInfo(databaseId.value, record.file_id); + if (data.status == "failed") { + message.error(data.message); + state.fileDetailModalVisible = false; + return; + } + selectedFile.value = { ...record, lines: data.lines || [] }; + } catch (error) { + console.error(error); + message.error(error.message); + state.fileDetailModalVisible = false; + } finally { + state.fileDetailLoading = false; + state.lock = false; + } + } + + async function loadQueryParams(id) { + const db_id = id || databaseId.value; + if (!db_id) return; + + state.queryParamsLoading = true; + try { + const response = await queryApi.getKnowledgeBaseQueryParams(db_id); + queryParams.value = response.params?.options || []; + + // Create a set of currently supported parameter keys + const supportedParamKeys = new Set(queryParams.value.map(param => param.key)); + + // Remove unsupported parameters from meta + for (const key in meta) { + if (key !== 'db_id' && !supportedParamKeys.has(key)) { + delete meta[key]; + } + } + + // Add default values for supported parameters that are not in meta + queryParams.value.forEach(param => { + if (!(param.key in meta)) { + meta[param.key] = param.default; + } + }); + } catch (error) { + console.error('Failed to load query params:', error); + message.error('加载查询参数失败'); + } finally { + state.queryParamsLoading = false; + } + } + + async function onQuery(queryText) { + state.searchLoading = true; + queryResult.value = ''; + + // 确保只传递当前知识库类型支持的参数 + const supportedParamKeys = new Set(queryParams.value.map(param => param.key)); + const queryMeta = {}; + + console.log('Supported param keys:', Array.from(supportedParamKeys)); + console.log('All meta params:', meta); + console.log('Database info:', database.value); + + // 遍历 meta 中的参数,只保留当前知识库类型支持的参数 + for (const [key, value] of Object.entries(meta)) { + // 跳过 db_id 参数 + if (key === 'db_id') continue; + + // 只保留当前知识库类型支持的参数 + if (supportedParamKeys.has(key)) { + queryMeta[key] = value; + } else { + console.log(`Skipping unsupported parameter: ${key}`); + } + } + + console.log('Filtered query meta:', queryMeta); + + try { + const data = await queryApi.queryTest(database.value.db_id, queryText, queryMeta); + queryResult.value = data; + } catch (error) { + console.error(error); + message.error(error.message); + } finally { + state.searchLoading = false; + } + } + + function startAutoRefresh() { + if (state.autoRefresh && !refreshInterval) { + refreshInterval = setInterval(() => { + getDatabaseInfo(); + }, 1000); + } + } + + function stopAutoRefresh() { + if (refreshInterval) { + clearInterval(refreshInterval); + refreshInterval = null; + } + } + + function toggleAutoRefresh() { + state.autoRefresh = !state.autoRefresh; + if (state.autoRefresh) { + startAutoRefresh(); + } else { + stopAutoRefresh(); + } + } + + function selectAllFailedFiles() { + const files = Object.values(database.value.files || {}); + const failedFiles = files + .filter(file => file.status === 'failed') + .map(file => file.file_id); + + const newSelectedKeys = [...new Set([...selectedRowKeys.value, ...failedFiles])]; + selectedRowKeys.value = newSelectedKeys; + + if (failedFiles.length > 0) { + message.success(`已选择 ${failedFiles.length} 个失败的文件`); + } else { + message.info('当前没有失败的文件'); + } + } + + return { + database, + databaseId, + selectedFile, + queryResult, + queryParams, + meta, + graphStats, + selectedRowKeys, + state, + getDatabaseInfo, + updateDatabaseInfo, + deleteDatabase, + deleteFile, + handleDeleteFile, + handleBatchDelete, + addFiles, + openFileDetail, + loadQueryParams, + onQuery, + startAutoRefresh, + stopAutoRefresh, + toggleAutoRefresh, + selectAllFailedFiles, + }; +}); diff --git a/web/src/utils/file_utils.js b/web/src/utils/file_utils.js new file mode 100644 index 00000000..e9e466da --- /dev/null +++ b/web/src/utils/file_utils.js @@ -0,0 +1,141 @@ +// 文件相关工具函数 +import { FileTextFilled, FileMarkdownFilled, FilePdfFilled, FileWordFilled, FileExcelFilled, FileImageFilled, FileUnknownFilled } from '@ant-design/icons-vue'; + +// 根据文件扩展名获取文件图标 +export const getFileIcon = (filename) => { + if (!filename) return FileUnknownFilled + + const extension = filename.toLowerCase().split('.').pop() + + const iconMap = { + // 文本文件 + 'txt': FileTextFilled, + 'text': FileTextFilled, + 'log': FileTextFilled, + + // Markdown文件 + 'md': FileMarkdownFilled, + 'markdown': FileMarkdownFilled, + + // PDF文件 + 'pdf': FilePdfFilled, + + // Word文档 + 'doc': FileWordFilled, + 'docx': FileWordFilled, + + // Excel文档 + 'xls': FileExcelFilled, + 'xlsx': FileExcelFilled, + 'csv': FileExcelFilled, + + // 图片文件 + 'jpg': FileImageFilled, + 'jpeg': FileImageFilled, + 'png': FileImageFilled, + 'gif': FileImageFilled, + 'bmp': FileImageFilled, + 'svg': FileImageFilled, + 'webp': FileImageFilled, + } + + return iconMap[extension] || FileUnknownFilled +} + +// 根据文件扩展名获取文件图标颜色 +export const getFileIconColor = (filename) => { + if (!filename) return '#8c8c8c' + + const extension = filename.toLowerCase().split('.').pop() + + const colorMap = { + // 文本文件 - 蓝色 + 'txt': '#1890ff', + 'text': '#1890ff', + 'log': '#1890ff', + + // Markdown文件 - 深蓝色 + 'md': '#0050b3', + 'markdown': '#0050b3', + + // PDF文件 - 红色 + 'pdf': '#ff4d4f', + + // Word文档 - 深蓝色 + 'doc': '#2f54eb', + 'docx': '#2f54eb', + + // Excel文档 - 绿色 + 'xls': '#52c41a', + 'xlsx': '#52c41a', + 'csv': '#52c41a', + + // 图片文件 - 紫色 + 'jpg': '#722ed1', + 'jpeg': '#722ed1', + 'png': '#722ed1', + 'gif': '#722ed1', + 'bmp': '#722ed1', + 'svg': '#722ed1', + 'webp': '#722ed1', + } + + return colorMap[extension] || '#8c8c8c' +} + +// Format relative time with more granularity: days ago, weeks ago, months ago +export const formatRelativeTime = (timestamp, offset = 0) => { + // If you want to adjust to UTC+8, set offset to 8, otherwise 0 + const timezoneOffset = offset * 60 * 60 * 1000; // offset in milliseconds + const adjustedTimestamp = timestamp + timezoneOffset; + + const now = Date.now(); + const secondsPast = (now - adjustedTimestamp) / 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 if (secondsPast < 86400 * 7) { + // Less than 7 days + return Math.round(secondsPast / 86400) + ' 天前'; + } else if (secondsPast < 86400 * 30) { + // Less than 30 days, show in weeks + return Math.round(secondsPast / (86400 * 7)) + ' 周前'; + } else if (secondsPast < 86400 * 365) { + // Less than 1 year, show in months + return Math.round(secondsPast / (86400 * 30)) + ' 月前'; + } else { + // More than 1 year, show full date + const date = new Date(adjustedTimestamp); + const year = date.getFullYear(); + const month = date.getMonth() + 1; + const day = date.getDate(); + return `${year} 年 ${month} 月 ${day} 日`; + } +} + +// 格式化标准时间 +export const formatStandardTime = (timestamp) => { + const date = new Date(timestamp); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hour = String(date.getHours()).padStart(2, '0'); + const minute = String(date.getMinutes()).padStart(2, '0'); + const second = String(date.getSeconds()).padStart(2, '0'); + return `${year}年${month}月${day}日 ${hour}:${minute}:${second}`; +} + +// 获取状态文本 +export const getStatusText = (status) => { + const statusMap = { + 'done': '处理完成', + 'failed': '处理失败', + 'processing': '处理中', + 'waiting': '等待处理' + } + return statusMap[status] || status +} \ No newline at end of file diff --git a/web/src/utils/kb_utils.js b/web/src/utils/kb_utils.js new file mode 100644 index 00000000..ba4c476f --- /dev/null +++ b/web/src/utils/kb_utils.js @@ -0,0 +1,30 @@ + +import { Database, Zap } from 'lucide-vue-next'; +import { ThunderboltOutlined } from '@ant-design/icons-vue'; + +export const getKbTypeLabel = (type) => { + const labels = { + lightrag: 'LightRAG', + chroma: 'Chroma', + milvus: 'Milvus' + }; + return labels[type] || type; +}; + +export const getKbTypeIcon = (type) => { + const icons = { + lightrag: Database, + chroma: Zap, + milvus: ThunderboltOutlined + }; + return icons[type] || Database; +}; + +export const getKbTypeColor = (type) => { + const colors = { + lightrag: 'purple', + chroma: 'orange', + milvus: 'red' + }; + return colors[type] || 'blue'; +}; diff --git a/web/src/views/DataBaseInfoView.vue b/web/src/views/DataBaseInfoView.vue index 9410ddc8..57056b76 100644 --- a/web/src/views/DataBaseInfoView.vue +++ b/web/src/views/DataBaseInfoView.vue @@ -1,231 +1,10 @@ \ No newline at end of file diff --git a/web/src/views/DataBaseView.vue b/web/src/views/DataBaseView.vue index a8e66fe7..880a4427 100644 --- a/web/src/views/DataBaseView.vue +++ b/web/src/views/DataBaseView.vue @@ -72,7 +72,15 @@ 创建 -
+ + +
+ +

正在加载知识库...

+
+ + +
@@ -682,6 +690,15 @@ onMounted(() => { padding: 0; } +.loading-container { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 300px; + gap: 16px; +} + .new-database-modal { h3 { margin-top: 10px;