feat:添加Beta 版本的知识库管理器前端页面;添加i18n相关文件内容。
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
"console": "Console",
|
||||
"alkaid": "Alkaid Lab",
|
||||
"knowledgeBase": "Knowledge Base",
|
||||
"knowledgeBaseBeta": "Knowledge Base (Beta)",
|
||||
"about": "About",
|
||||
"settings": "Settings",
|
||||
"documentation": "Documentation",
|
||||
|
||||
@@ -6,13 +6,11 @@
|
||||
"title": "The Alkaid Project.",
|
||||
"subtitle": "AstrBot Alpha Project",
|
||||
"navigation": {
|
||||
"knowledgeBaseV2": "Native Knowledge Base",
|
||||
"knowledgeBase": "Knowledge Base (Plugin)",
|
||||
"longTermMemory": "Long-term Memory",
|
||||
"other": "..."
|
||||
}
|
||||
},
|
||||
"knowledgeBaseV2": "Native Knowledge Base",
|
||||
"features": {
|
||||
"knowledgeBase": "Knowledge Base",
|
||||
"longTermMemory": "Long-term Memory",
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"title": "Knowledge Base Details",
|
||||
"backToList": "Back to List",
|
||||
"tabs": {
|
||||
"overview": "Overview",
|
||||
"documents": "Documents",
|
||||
"sessions": "Sessions",
|
||||
"settings": "Settings"
|
||||
},
|
||||
"overview": {
|
||||
"title": "Basic Information",
|
||||
"name": "Name",
|
||||
"description": "Description",
|
||||
"emoji": "Icon",
|
||||
"createdAt": "Created At",
|
||||
"updatedAt": "Updated At",
|
||||
"stats": "Statistics",
|
||||
"docCount": "Documents",
|
||||
"chunkCount": "Chunks",
|
||||
"embeddingModel": "Embedding Model",
|
||||
"rerankModel": "Rerank Model",
|
||||
"notSet": "Not Set"
|
||||
},
|
||||
"documents": {
|
||||
"title": "Documents",
|
||||
"upload": "Upload Document",
|
||||
"empty": "No documents",
|
||||
"name": "Name",
|
||||
"type": "Type",
|
||||
"size": "Size",
|
||||
"chunks": "Chunks",
|
||||
"createdAt": "Uploaded At",
|
||||
"actions": "Actions",
|
||||
"view": "View",
|
||||
"delete": "Delete",
|
||||
"deleteConfirm": "Are you sure you want to delete document '{name}'?",
|
||||
"deleteWarning": "This will delete the document and all its chunks. This action cannot be undone.",
|
||||
"uploading": "Uploading...",
|
||||
"uploadSuccess": "Document uploaded successfully",
|
||||
"uploadFailed": "Failed to upload document",
|
||||
"deleteSuccess": "Document deleted successfully",
|
||||
"deleteFailed": "Failed to delete document"
|
||||
},
|
||||
"upload": {
|
||||
"title": "Upload Document",
|
||||
"selectFile": "Select File",
|
||||
"dropzone": "Drop files here or click to select",
|
||||
"supportedFormats": "Supported formats: TXT, PDF, Markdown",
|
||||
"maxSize": "Max file size: 50MB",
|
||||
"chunkSettings": "Chunk Settings",
|
||||
"chunkSize": "Chunk Size",
|
||||
"chunkSizeHint": "Number of characters per chunk (default: 512)",
|
||||
"chunkOverlap": "Chunk Overlap",
|
||||
"chunkOverlapHint": "Overlapping characters between chunks (default: 50)",
|
||||
"cancel": "Cancel",
|
||||
"submit": "Upload",
|
||||
"fileRequired": "Please select a file to upload"
|
||||
},
|
||||
"sessions": {
|
||||
"title": "Session Configuration",
|
||||
"subtitle": "Configure which sessions can use this knowledge base",
|
||||
"empty": "No session configurations",
|
||||
"add": "Add Configuration",
|
||||
"scope": "Scope",
|
||||
"scopeId": "Identifier",
|
||||
"topK": "Top K Results",
|
||||
"enableRerank": "Enable Rerank",
|
||||
"actions": "Actions",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"scopeSession": "Session Level",
|
||||
"scopePlatform": "Platform Level",
|
||||
"deleteConfirm": "Are you sure you want to delete this configuration?",
|
||||
"addSuccess": "Configuration added successfully",
|
||||
"addFailed": "Failed to add configuration",
|
||||
"deleteSuccess": "Configuration deleted successfully",
|
||||
"deleteFailed": "Failed to delete configuration"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Knowledge Base Settings",
|
||||
"basic": "Basic Settings",
|
||||
"retrieval": "Retrieval Settings",
|
||||
"chunkSize": "Chunk Size",
|
||||
"chunkOverlap": "Chunk Overlap",
|
||||
"topKDense": "Dense Retrieval Count",
|
||||
"topKSparse": "Sparse Retrieval Count",
|
||||
"topMFinal": "Final Result Count",
|
||||
"enableRerank": "Enable Rerank",
|
||||
"embeddingProvider": "Embedding Provider",
|
||||
"rerankProvider": "Rerank Provider",
|
||||
"save": "Save Settings",
|
||||
"saveSuccess": "Settings saved successfully",
|
||||
"saveFailed": "Failed to save settings",
|
||||
"tips": "Tip: Modifying retrieval settings will affect subsequent knowledge base queries."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"title": "Document Details",
|
||||
"backToKB": "Back to Knowledge Base",
|
||||
"info": {
|
||||
"title": "Document Information",
|
||||
"name": "Document Name",
|
||||
"type": "File Type",
|
||||
"size": "File Size",
|
||||
"chunkCount": "Chunk Count",
|
||||
"createdAt": "Uploaded At"
|
||||
},
|
||||
"chunks": {
|
||||
"title": "Chunks",
|
||||
"empty": "No chunks",
|
||||
"index": "Index",
|
||||
"content": "Content",
|
||||
"charCount": "Characters",
|
||||
"actions": "Actions",
|
||||
"view": "View",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"preview": "Preview",
|
||||
"search": "Search Chunks",
|
||||
"searchPlaceholder": "Enter keywords to search chunks..."
|
||||
},
|
||||
"edit": {
|
||||
"title": "Edit Chunk",
|
||||
"content": "Chunk Content",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"saveSuccess": "Chunk saved successfully",
|
||||
"saveFailed": "Failed to save chunk"
|
||||
},
|
||||
"delete": {
|
||||
"title": "Delete Chunk",
|
||||
"confirmText": "Are you sure you want to delete this chunk?",
|
||||
"warning": "This action cannot be undone and may affect knowledge base retrieval performance.",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Delete",
|
||||
"deleteSuccess": "Chunk deleted successfully",
|
||||
"deleteFailed": "Failed to delete chunk"
|
||||
},
|
||||
"view": {
|
||||
"title": "Chunk Details",
|
||||
"index": "Index",
|
||||
"content": "Content",
|
||||
"charCount": "Characters",
|
||||
"vecDocId": "Vector ID",
|
||||
"close": "Close"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"title": "Knowledge Base Management",
|
||||
"subtitle": "Manage and query knowledge base contents",
|
||||
"list": {
|
||||
"title": "My Knowledge Bases",
|
||||
"subtitle": "Manage all your knowledge base collections",
|
||||
"create": "Create Knowledge Base",
|
||||
"refresh": "Refresh List",
|
||||
"empty": "No knowledge bases",
|
||||
"loading": "Loading...",
|
||||
"documents": "Documents",
|
||||
"chunks": "Chunks",
|
||||
"sessionConfig": "Session Config"
|
||||
},
|
||||
"card": {
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"open": "Open",
|
||||
"docCount": "{count} Documents",
|
||||
"chunkCount": "{count} Chunks"
|
||||
},
|
||||
"create": {
|
||||
"title": "Create Knowledge Base",
|
||||
"nameLabel": "Name",
|
||||
"namePlaceholder": "Enter knowledge base name",
|
||||
"descriptionLabel": "Description",
|
||||
"descriptionPlaceholder": "Describe the purpose of this knowledge base...",
|
||||
"emojiLabel": "Icon",
|
||||
"embeddingModelLabel": "Embedding Model",
|
||||
"rerankModelLabel": "Rerank Model (Optional)",
|
||||
"providerInfo": "Provider: {id} | Dimensions: {dimensions}",
|
||||
"rerankProviderInfo": "Provider: {id}",
|
||||
"tips": "Tip: Once you select an embedding model, do not modify the model or vector dimensions, as this will severely affect recall rate.",
|
||||
"cancel": "Cancel",
|
||||
"submit": "Create",
|
||||
"nameRequired": "Please enter knowledge base name"
|
||||
},
|
||||
"edit": {
|
||||
"title": "Edit Knowledge Base",
|
||||
"submit": "Save"
|
||||
},
|
||||
"delete": {
|
||||
"title": "Delete Knowledge Base",
|
||||
"confirmText": "Are you sure you want to delete knowledge base '{name}'?",
|
||||
"warning": "This action is irreversible. All documents, chunks, and associated configurations will be permanently deleted.",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Delete"
|
||||
},
|
||||
"emoji": {
|
||||
"title": "Select Icon",
|
||||
"close": "Close",
|
||||
"categories": {
|
||||
"books": "Books & Documents",
|
||||
"emotions": "Emotions & Faces",
|
||||
"objects": "Objects & Tools",
|
||||
"symbols": "Symbols & Signs"
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
"createSuccess": "Knowledge base created successfully",
|
||||
"createFailed": "Failed to create",
|
||||
"updateSuccess": "Knowledge base updated successfully",
|
||||
"updateFailed": "Failed to update",
|
||||
"deleteSuccess": "Knowledge base deleted successfully",
|
||||
"deleteFailed": "Failed to delete",
|
||||
"loadError": "Failed to load knowledge base list"
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
"console": "控制台",
|
||||
"alkaid": "Alkaid",
|
||||
"knowledgeBase": "知识库",
|
||||
"knowledgeBaseBeta": "知识库 (Beta)",
|
||||
"about": "关于",
|
||||
"settings": "设置",
|
||||
"documentation": "官方文档",
|
||||
|
||||
@@ -6,13 +6,11 @@
|
||||
"title": "The Alkaid Project.",
|
||||
"subtitle": "AstrBot Alpha 项目",
|
||||
"navigation": {
|
||||
"knowledgeBaseV2": "原生知识库",
|
||||
"knowledgeBase": "知识库 (插件)",
|
||||
"longTermMemory": "长期记忆层",
|
||||
"other": "..."
|
||||
}
|
||||
},
|
||||
"knowledgeBaseV2": "原生知识库",
|
||||
"features": {
|
||||
"knowledgeBase": "知识库",
|
||||
"longTermMemory": "长期记忆",
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"title": "知识库详情",
|
||||
"backToList": "返回列表",
|
||||
"tabs": {
|
||||
"overview": "概览",
|
||||
"documents": "文档管理",
|
||||
"sessions": "会话配置",
|
||||
"settings": "设置"
|
||||
},
|
||||
"overview": {
|
||||
"title": "基本信息",
|
||||
"name": "名称",
|
||||
"description": "描述",
|
||||
"emoji": "图标",
|
||||
"createdAt": "创建时间",
|
||||
"updatedAt": "更新时间",
|
||||
"stats": "统计信息",
|
||||
"docCount": "文档数量",
|
||||
"chunkCount": "分块数量",
|
||||
"embeddingModel": "嵌入模型",
|
||||
"rerankModel": "重排序模型",
|
||||
"notSet": "未设置"
|
||||
},
|
||||
"documents": {
|
||||
"title": "文档列表",
|
||||
"upload": "上传文档",
|
||||
"empty": "暂无文档",
|
||||
"name": "文档名称",
|
||||
"type": "类型",
|
||||
"size": "大小",
|
||||
"chunks": "分块数",
|
||||
"createdAt": "上传时间",
|
||||
"actions": "操作",
|
||||
"view": "查看",
|
||||
"delete": "删除",
|
||||
"deleteConfirm": "确定要删除文档「{name}」吗?",
|
||||
"deleteWarning": "此操作将删除文档及其所有分块,不可恢复。",
|
||||
"uploading": "正在上传...",
|
||||
"uploadSuccess": "文档上传成功",
|
||||
"uploadFailed": "文档上传失败",
|
||||
"deleteSuccess": "文档删除成功",
|
||||
"deleteFailed": "文档删除失败"
|
||||
},
|
||||
"upload": {
|
||||
"title": "上传文档",
|
||||
"selectFile": "选择文件",
|
||||
"dropzone": "拖放文件到这里或点击选择",
|
||||
"supportedFormats": "支持的格式: TXT, PDF, Markdown",
|
||||
"maxSize": "最大文件大小: 50MB",
|
||||
"chunkSettings": "分块设置",
|
||||
"chunkSize": "分块大小",
|
||||
"chunkSizeHint": "每个文本块的字符数 (默认: 512)",
|
||||
"chunkOverlap": "分块重叠",
|
||||
"chunkOverlapHint": "相邻文本块之间的重叠字符数 (默认: 50)",
|
||||
"cancel": "取消",
|
||||
"submit": "上传",
|
||||
"fileRequired": "请选择要上传的文件"
|
||||
},
|
||||
"sessions": {
|
||||
"title": "会话配置",
|
||||
"subtitle": "配置哪些会话可以使用此知识库",
|
||||
"empty": "暂无会话配置",
|
||||
"add": "添加配置",
|
||||
"scope": "范围",
|
||||
"scopeId": "标识",
|
||||
"topK": "返回结果数",
|
||||
"enableRerank": "启用重排序",
|
||||
"actions": "操作",
|
||||
"edit": "编辑",
|
||||
"delete": "删除",
|
||||
"scopeSession": "会话级别",
|
||||
"scopePlatform": "平台级别",
|
||||
"deleteConfirm": "确定要删除此配置吗?",
|
||||
"addSuccess": "配置添加成功",
|
||||
"addFailed": "配置添加失败",
|
||||
"deleteSuccess": "配置删除成功",
|
||||
"deleteFailed": "配置删除失败"
|
||||
},
|
||||
"settings": {
|
||||
"title": "知识库设置",
|
||||
"basic": "基本设置",
|
||||
"retrieval": "检索设置",
|
||||
"chunkSize": "分块大小",
|
||||
"chunkOverlap": "分块重叠",
|
||||
"topKDense": "稠密检索数量",
|
||||
"topKSparse": "稀疏检索数量",
|
||||
"topMFinal": "最终返回数量",
|
||||
"enableRerank": "启用重排序",
|
||||
"embeddingProvider": "嵌入模型提供商",
|
||||
"rerankProvider": "重排序模型提供商",
|
||||
"save": "保存设置",
|
||||
"saveSuccess": "设置保存成功",
|
||||
"saveFailed": "设置保存失败",
|
||||
"tips": "提示: 修改检索设置后,将影响后续的知识库查询效果。"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"title": "文档详情",
|
||||
"backToKB": "返回知识库",
|
||||
"info": {
|
||||
"title": "文档信息",
|
||||
"name": "文档名称",
|
||||
"type": "文件类型",
|
||||
"size": "文件大小",
|
||||
"chunkCount": "分块数量",
|
||||
"createdAt": "上传时间"
|
||||
},
|
||||
"chunks": {
|
||||
"title": "分块列表",
|
||||
"empty": "暂无分块",
|
||||
"index": "序号",
|
||||
"content": "内容",
|
||||
"charCount": "字符数",
|
||||
"actions": "操作",
|
||||
"view": "查看",
|
||||
"edit": "编辑",
|
||||
"delete": "删除",
|
||||
"preview": "预览",
|
||||
"search": "搜索分块",
|
||||
"searchPlaceholder": "输入关键词搜索分块内容..."
|
||||
},
|
||||
"edit": {
|
||||
"title": "编辑分块",
|
||||
"content": "分块内容",
|
||||
"cancel": "取消",
|
||||
"save": "保存",
|
||||
"saveSuccess": "分块保存成功",
|
||||
"saveFailed": "分块保存失败"
|
||||
},
|
||||
"delete": {
|
||||
"title": "删除分块",
|
||||
"confirmText": "确定要删除此分块吗?",
|
||||
"warning": "删除后将无法恢复,可能影响知识库检索效果。",
|
||||
"cancel": "取消",
|
||||
"confirm": "删除",
|
||||
"deleteSuccess": "分块删除成功",
|
||||
"deleteFailed": "分块删除失败"
|
||||
},
|
||||
"view": {
|
||||
"title": "分块详情",
|
||||
"index": "序号",
|
||||
"content": "内容",
|
||||
"charCount": "字符数",
|
||||
"vecDocId": "向量ID",
|
||||
"close": "关闭"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"title": "知识库管理",
|
||||
"subtitle": "统一管理和查询知识库内容",
|
||||
"list": {
|
||||
"title": "我的知识库",
|
||||
"subtitle": "管理您的所有知识库集合",
|
||||
"create": "创建知识库",
|
||||
"refresh": "刷新列表",
|
||||
"empty": "暂无知识库",
|
||||
"loading": "正在加载...",
|
||||
"documents": "文档",
|
||||
"chunks": "分块",
|
||||
"sessionConfig": "会话配置"
|
||||
},
|
||||
"card": {
|
||||
"edit": "编辑",
|
||||
"delete": "删除",
|
||||
"open": "打开",
|
||||
"docCount": "{count} 个文档",
|
||||
"chunkCount": "{count} 个分块"
|
||||
},
|
||||
"create": {
|
||||
"title": "创建知识库",
|
||||
"nameLabel": "知识库名称",
|
||||
"namePlaceholder": "为知识库起个名字",
|
||||
"descriptionLabel": "描述",
|
||||
"descriptionPlaceholder": "简单描述这个知识库的用途...",
|
||||
"emojiLabel": "图标",
|
||||
"embeddingModelLabel": "嵌入模型 (Embedding Model)",
|
||||
"rerankModelLabel": "重排序模型 (Rerank Model, 可选)",
|
||||
"providerInfo": "提供商: {id} | 维度: {dimensions}",
|
||||
"rerankProviderInfo": "提供商: {id}",
|
||||
"tips": "提示: 一旦选择了嵌入模型,请不要修改该提供商的模型或向量维度,否则将严重影响召回率。",
|
||||
"cancel": "取消",
|
||||
"submit": "创建",
|
||||
"nameRequired": "请输入知识库名称"
|
||||
},
|
||||
"edit": {
|
||||
"title": "编辑知识库",
|
||||
"submit": "保存"
|
||||
},
|
||||
"delete": {
|
||||
"title": "删除知识库",
|
||||
"confirmText": "确定要删除知识库「{name}」吗?",
|
||||
"warning": "此操作不可逆,所有文档、分块和关联配置都将被永久删除。",
|
||||
"cancel": "取消",
|
||||
"confirm": "删除"
|
||||
},
|
||||
"emoji": {
|
||||
"title": "选择图标",
|
||||
"close": "关闭",
|
||||
"categories": {
|
||||
"books": "书籍与文档",
|
||||
"emotions": "表情与情感",
|
||||
"objects": "物品与工具",
|
||||
"symbols": "符号与标志"
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
"createSuccess": "知识库创建成功",
|
||||
"createFailed": "创建失败",
|
||||
"updateSuccess": "知识库更新成功",
|
||||
"updateFailed": "更新失败",
|
||||
"deleteSuccess": "知识库删除成功",
|
||||
"deleteFailed": "删除失败",
|
||||
"loadError": "加载知识库列表失败"
|
||||
}
|
||||
}
|
||||
@@ -25,11 +25,9 @@ import zhCNDashboard from './locales/zh-CN/features/dashboard.json';
|
||||
import zhCNAlkaidIndex from './locales/zh-CN/features/alkaid/index.json';
|
||||
import zhCNAlkaidKnowledgeBase from './locales/zh-CN/features/alkaid/knowledge-base.json';
|
||||
import zhCNAlkaidMemory from './locales/zh-CN/features/alkaid/memory.json';
|
||||
import zhCNAlkaidKBV2Index from './locales/zh-CN/features/alkaid/knowledge-base-v2/index.json';
|
||||
import zhCNAlkaidKBV2Documents from './locales/zh-CN/features/alkaid/knowledge-base-v2/documents.json';
|
||||
import zhCNAlkaidKBV2Search from './locales/zh-CN/features/alkaid/knowledge-base-v2/search.json';
|
||||
import zhCNAlkaidKBV2Settings from './locales/zh-CN/features/alkaid/knowledge-base-v2/settings.json';
|
||||
import zhCNAlkaidKBV2SessionConfig from './locales/zh-CN/features/alkaid/knowledge-base-v2/session-config.json';
|
||||
import zhCNKnowledgeBaseIndex from './locales/zh-CN/features/knowledge-base/index.json';
|
||||
import zhCNKnowledgeBaseDetail from './locales/zh-CN/features/knowledge-base/detail.json';
|
||||
import zhCNKnowledgeBaseDocument from './locales/zh-CN/features/knowledge-base/document.json';
|
||||
import zhCNPersona from './locales/zh-CN/features/persona.json';
|
||||
import zhCNMigration from './locales/zh-CN/features/migration.json';
|
||||
|
||||
@@ -61,11 +59,9 @@ import enUSDashboard from './locales/en-US/features/dashboard.json';
|
||||
import enUSAlkaidIndex from './locales/en-US/features/alkaid/index.json';
|
||||
import enUSAlkaidKnowledgeBase from './locales/en-US/features/alkaid/knowledge-base.json';
|
||||
import enUSAlkaidMemory from './locales/en-US/features/alkaid/memory.json';
|
||||
import enUSAlkaidKBV2Index from './locales/en-US/features/alkaid/knowledge-base-v2/index.json';
|
||||
import enUSAlkaidKBV2Documents from './locales/en-US/features/alkaid/knowledge-base-v2/documents.json';
|
||||
import enUSAlkaidKBV2Search from './locales/en-US/features/alkaid/knowledge-base-v2/search.json';
|
||||
import enUSAlkaidKBV2Settings from './locales/en-US/features/alkaid/knowledge-base-v2/settings.json';
|
||||
import enUSAlkaidKBV2SessionConfig from './locales/en-US/features/alkaid/knowledge-base-v2/session-config.json';
|
||||
import enUSKnowledgeBaseIndex from './locales/en-US/features/knowledge-base/index.json';
|
||||
import enUSKnowledgeBaseDetail from './locales/en-US/features/knowledge-base/detail.json';
|
||||
import enUSKnowledgeBaseDocument from './locales/en-US/features/knowledge-base/document.json';
|
||||
import enUSPersona from './locales/en-US/features/persona.json';
|
||||
import enUSMigration from './locales/en-US/features/migration.json';
|
||||
|
||||
@@ -101,14 +97,12 @@ export const translations = {
|
||||
alkaid: {
|
||||
index: zhCNAlkaidIndex,
|
||||
'knowledge-base': zhCNAlkaidKnowledgeBase,
|
||||
memory: zhCNAlkaidMemory,
|
||||
'knowledge-base-v2': {
|
||||
index: zhCNAlkaidKBV2Index,
|
||||
documents: zhCNAlkaidKBV2Documents,
|
||||
search: zhCNAlkaidKBV2Search,
|
||||
settings: zhCNAlkaidKBV2Settings,
|
||||
'session-config': zhCNAlkaidKBV2SessionConfig
|
||||
}
|
||||
memory: zhCNAlkaidMemory
|
||||
},
|
||||
'knowledge-base': {
|
||||
index: zhCNKnowledgeBaseIndex,
|
||||
detail: zhCNKnowledgeBaseDetail,
|
||||
document: zhCNKnowledgeBaseDocument
|
||||
},
|
||||
persona: zhCNPersona,
|
||||
migration: zhCNMigration
|
||||
@@ -145,14 +139,12 @@ export const translations = {
|
||||
alkaid: {
|
||||
index: enUSAlkaidIndex,
|
||||
'knowledge-base': enUSAlkaidKnowledgeBase,
|
||||
memory: enUSAlkaidMemory,
|
||||
'knowledge-base-v2': {
|
||||
index: enUSAlkaidKBV2Index,
|
||||
documents: enUSAlkaidKBV2Documents,
|
||||
search: enUSAlkaidKBV2Search,
|
||||
settings: enUSAlkaidKBV2Settings,
|
||||
'session-config': enUSAlkaidKBV2SessionConfig
|
||||
}
|
||||
memory: enUSAlkaidMemory
|
||||
},
|
||||
'knowledge-base': {
|
||||
index: enUSKnowledgeBaseIndex,
|
||||
detail: enUSKnowledgeBaseDetail,
|
||||
document: enUSKnowledgeBaseDocument
|
||||
},
|
||||
persona: enUSPersona,
|
||||
migration: enUSMigration
|
||||
|
||||
@@ -53,6 +53,14 @@ const sidebarItem: menu[] = [
|
||||
icon: 'mdi-text-box-search',
|
||||
to: '/alkaid/knowledge-base',
|
||||
},
|
||||
{
|
||||
title: 'core.navigation.knowledgeBaseBeta',
|
||||
icon: 'mdi-book-open-variant',
|
||||
to: '/knowledge-base',
|
||||
chip: 'Beta',
|
||||
chipColor: 'primary',
|
||||
chipVariant: 'tonal',
|
||||
},
|
||||
{
|
||||
title: 'core.navigation.config',
|
||||
icon: 'mdi-cog',
|
||||
|
||||
@@ -66,6 +66,30 @@ const MainRoutes = {
|
||||
path: '/console',
|
||||
component: () => import('@/views/ConsolePage.vue')
|
||||
},
|
||||
{
|
||||
name: 'NativeKnowledgeBase',
|
||||
path: '/knowledge-base',
|
||||
component: () => import('@/views/knowledge-base/index.vue'),
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'NativeKBList',
|
||||
component: () => import('@/views/knowledge-base/KBList.vue')
|
||||
},
|
||||
{
|
||||
path: ':kbId',
|
||||
name: 'NativeKBDetail',
|
||||
component: () => import('@/views/knowledge-base/KBDetail.vue'),
|
||||
props: true
|
||||
},
|
||||
{
|
||||
path: ':kbId/document/:docId',
|
||||
name: 'NativeDocumentDetail',
|
||||
component: () => import('@/views/knowledge-base/DocumentDetail.vue'),
|
||||
props: true
|
||||
}
|
||||
]
|
||||
},
|
||||
// {
|
||||
// name: 'Alkaid',
|
||||
// path: '/alkaid',
|
||||
@@ -93,11 +117,6 @@ const MainRoutes = {
|
||||
path: '/alkaid',
|
||||
component: () => import('@/views/AlkaidPage.vue'),
|
||||
children: [
|
||||
{
|
||||
path: 'knowledge-base-v2',
|
||||
name: 'KnowledgeBaseV2',
|
||||
component: () => import('@/views/alkaid/knowledge-base-v2/KnowledgeBaseV2.vue')
|
||||
},
|
||||
{
|
||||
path: 'knowledge-base',
|
||||
name: 'KnowledgeBase',
|
||||
|
||||
@@ -8,12 +8,6 @@
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 8px; margin-bottom: 16px; flex-wrap: wrap;">
|
||||
<v-btn size="large" :variant="isActive('knowledge-base-v2') ? 'flat' : 'tonal'"
|
||||
:color="isActive('knowledge-base-v2') ? '#9b72cb' : ''" rounded="lg"
|
||||
@click="navigateTo('knowledge-base-v2')">
|
||||
<v-icon start>mdi-book-open-page-variant</v-icon>
|
||||
{{ tm('page.navigation.knowledgeBaseV2') }}
|
||||
</v-btn>
|
||||
<v-btn size="large" :variant="isActive('knowledge-base') ? 'flat' : 'tonal'"
|
||||
:color="isActive('knowledge-base') ? '#9b72cb' : ''" rounded="lg"
|
||||
@click="navigateTo('knowledge-base')">
|
||||
@@ -75,9 +69,9 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 如果在根路径 /alkaid,默认跳转到原生知识库页面
|
||||
// 如果在根路径 /alkaid,默认跳转到知识库页面
|
||||
if (this.$route.path === '/alkaid') {
|
||||
this.navigateTo('knowledge-base-v2');
|
||||
this.navigateTo('knowledge-base');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,591 @@
|
||||
<template>
|
||||
<div class="document-detail-page">
|
||||
<!-- 页面头部 -->
|
||||
<div class="page-header">
|
||||
<v-btn
|
||||
icon="mdi-arrow-left"
|
||||
variant="text"
|
||||
@click="$router.push({ name: 'NativeKBDetail', params: { kbId } })"
|
||||
/>
|
||||
<div class="header-content">
|
||||
<h1 class="text-h4">{{ document.doc_name }}</h1>
|
||||
<p class="text-subtitle-1 text-medium-emphasis mt-2">{{ t('title') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading-container">
|
||||
<v-progress-circular indeterminate color="primary" size="64" />
|
||||
</div>
|
||||
|
||||
<!-- 主内容 -->
|
||||
<div v-else class="document-content">
|
||||
<!-- 文档信息卡片 -->
|
||||
<v-card elevation="2" class="mb-6">
|
||||
<v-card-title>{{ t('info.title') }}</v-card-title>
|
||||
<v-divider />
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12" md="3">
|
||||
<div class="info-item">
|
||||
<v-icon start>mdi-label</v-icon>
|
||||
<div>
|
||||
<div class="text-caption text-medium-emphasis">{{ t('info.name') }}</div>
|
||||
<div class="text-body-1">{{ document.doc_name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="12" md="2">
|
||||
<div class="info-item">
|
||||
<v-icon start :color="getFileColor(document.file_type)">
|
||||
{{ getFileIcon(document.file_type) }}
|
||||
</v-icon>
|
||||
<div>
|
||||
<div class="text-caption text-medium-emphasis">{{ t('info.type') }}</div>
|
||||
<div class="text-body-1">{{ document.file_type || '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="12" md="2">
|
||||
<div class="info-item">
|
||||
<v-icon start>mdi-file-chart</v-icon>
|
||||
<div>
|
||||
<div class="text-caption text-medium-emphasis">{{ t('info.size') }}</div>
|
||||
<div class="text-body-1">{{ formatFileSize(document.file_size) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="12" md="2">
|
||||
<div class="info-item">
|
||||
<v-icon start>mdi-text-box</v-icon>
|
||||
<div>
|
||||
<div class="text-caption text-medium-emphasis">{{ t('info.chunkCount') }}</div>
|
||||
<div class="text-body-1">{{ document.chunk_count || 0 }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<div class="info-item">
|
||||
<v-icon start>mdi-calendar</v-icon>
|
||||
<div>
|
||||
<div class="text-caption text-medium-emphasis">{{ t('info.createdAt') }}</div>
|
||||
<div class="text-body-1">{{ formatDate(document.created_at) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- 分块列表 -->
|
||||
<v-card elevation="2">
|
||||
<v-card-title class="d-flex align-center pa-4">
|
||||
<span>{{ t('chunks.title') }}</span>
|
||||
<v-chip class="ml-2" size="small" variant="tonal">
|
||||
{{ chunks.length }} {{ t('chunks.title') }}
|
||||
</v-chip>
|
||||
<v-spacer />
|
||||
<v-text-field
|
||||
v-model="searchQuery"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
:placeholder="t('chunks.searchPlaceholder')"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details
|
||||
clearable
|
||||
style="max-width: 300px"
|
||||
/>
|
||||
</v-card-title>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-card-text class="pa-0">
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="filteredChunks"
|
||||
:loading="loadingChunks"
|
||||
:items-per-page="10"
|
||||
>
|
||||
<template #item.chunk_index="{ item }">
|
||||
<v-chip size="small" variant="tonal" color="primary">
|
||||
#{{ item.chunk_index + 1 }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<template #item.content="{ item }">
|
||||
<div class="chunk-content-preview">
|
||||
{{ item.content }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #item.char_count="{ item }">
|
||||
<v-chip size="small" variant="outlined">
|
||||
{{ item.char_count }} 字符
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<template #item.actions="{ item }">
|
||||
<v-btn
|
||||
icon="mdi-eye"
|
||||
variant="text"
|
||||
size="small"
|
||||
color="info"
|
||||
@click="viewChunk(item)"
|
||||
/>
|
||||
<v-btn
|
||||
icon="mdi-pencil"
|
||||
variant="text"
|
||||
size="small"
|
||||
color="primary"
|
||||
@click="editChunk(item)"
|
||||
/>
|
||||
<v-btn
|
||||
icon="mdi-delete"
|
||||
variant="text"
|
||||
size="small"
|
||||
color="error"
|
||||
@click="confirmDeleteChunk(item)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #no-data>
|
||||
<div class="text-center py-8">
|
||||
<v-icon size="64" color="grey-lighten-2">mdi-text-box-outline</v-icon>
|
||||
<p class="mt-4 text-medium-emphasis">{{ t('chunks.empty') }}</p>
|
||||
</div>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
|
||||
<!-- 查看分块对话框 -->
|
||||
<v-dialog v-model="showViewDialog" max-width="800px" scrollable>
|
||||
<v-card>
|
||||
<v-card-title class="pa-4">
|
||||
<span>{{ t('view.title') }}</span>
|
||||
<v-spacer />
|
||||
<v-btn icon="mdi-close" variant="text" @click="showViewDialog = false" />
|
||||
</v-card-title>
|
||||
<v-divider />
|
||||
<v-card-text class="pa-6">
|
||||
<v-list density="comfortable">
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon>mdi-pound</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ t('view.index') }}</v-list-item-title>
|
||||
<v-list-item-subtitle>#{{ (selectedChunk?.chunk_index || 0) + 1 }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon>mdi-text</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ t('view.charCount') }}</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ selectedChunk?.char_count || 0 }} 字符</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon>mdi-key</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ t('view.vecDocId') }}</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ selectedChunk?.vec_doc_id || '-' }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<v-divider class="my-4" />
|
||||
|
||||
<div class="text-caption text-medium-emphasis mb-2">{{ t('view.content') }}</div>
|
||||
<div class="chunk-content-view">
|
||||
{{ selectedChunk?.content }}
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-divider />
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer />
|
||||
<v-btn variant="text" @click="showViewDialog = false">
|
||||
{{ t('view.close') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 编辑分块对话框 -->
|
||||
<v-dialog v-model="showEditDialog" max-width="800px" persistent scrollable>
|
||||
<v-card>
|
||||
<v-card-title class="pa-4">
|
||||
<span>{{ t('edit.title') }}</span>
|
||||
<v-spacer />
|
||||
<v-btn icon="mdi-close" variant="text" @click="closeEditDialog" />
|
||||
</v-card-title>
|
||||
<v-divider />
|
||||
<v-card-text class="pa-6">
|
||||
<v-textarea
|
||||
v-model="editForm.content"
|
||||
:label="t('edit.content')"
|
||||
variant="outlined"
|
||||
rows="15"
|
||||
auto-grow
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-divider />
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer />
|
||||
<v-btn variant="text" @click="closeEditDialog">
|
||||
{{ t('edit.cancel') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
@click="saveChunk"
|
||||
:loading="saving"
|
||||
>
|
||||
{{ t('edit.save') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 删除确认对话框 -->
|
||||
<v-dialog v-model="showDeleteDialog" max-width="450px">
|
||||
<v-card>
|
||||
<v-card-title class="pa-4 text-h6">{{ t('delete.title') }}</v-card-title>
|
||||
<v-divider />
|
||||
<v-card-text class="pa-6">
|
||||
<p>{{ t('delete.confirmText') }}</p>
|
||||
<v-alert type="warning" variant="tonal" density="compact" class="mt-4">
|
||||
{{ t('delete.warning') }}
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
<v-divider />
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer />
|
||||
<v-btn variant="text" @click="showDeleteDialog = false">
|
||||
{{ t('delete.cancel') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="error"
|
||||
variant="elevated"
|
||||
@click="deleteChunk"
|
||||
:loading="deleting"
|
||||
>
|
||||
{{ t('delete.confirm') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 消息提示 -->
|
||||
<v-snackbar v-model="snackbar.show" :color="snackbar.color">
|
||||
{{ snackbar.text }}
|
||||
</v-snackbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import axios from 'axios'
|
||||
import { useModuleI18n } from '@/i18n/composables'
|
||||
|
||||
const { tm: t } = useModuleI18n('features/knowledge-base/document')
|
||||
const route = useRoute()
|
||||
|
||||
const kbId = ref(route.params.kbId as string)
|
||||
const docId = ref(route.params.docId as string)
|
||||
|
||||
// 状态
|
||||
const loading = ref(true)
|
||||
const loadingChunks = ref(false)
|
||||
const saving = ref(false)
|
||||
const deleting = ref(false)
|
||||
const document = ref<any>({})
|
||||
const chunks = ref<any[]>([])
|
||||
const searchQuery = ref('')
|
||||
const showViewDialog = ref(false)
|
||||
const showEditDialog = ref(false)
|
||||
const showDeleteDialog = ref(false)
|
||||
const selectedChunk = ref<any>(null)
|
||||
const deleteTarget = ref<any>(null)
|
||||
|
||||
const snackbar = ref({
|
||||
show: false,
|
||||
text: '',
|
||||
color: 'success'
|
||||
})
|
||||
|
||||
const showSnackbar = (text: string, color: string = 'success') => {
|
||||
snackbar.value.text = text
|
||||
snackbar.value.color = color
|
||||
snackbar.value.show = true
|
||||
}
|
||||
|
||||
// 编辑表单
|
||||
const editForm = ref({
|
||||
content: ''
|
||||
})
|
||||
|
||||
// 表格列
|
||||
const headers = [
|
||||
{ title: t('chunks.index'), key: 'chunk_index', width: 100 },
|
||||
{ title: t('chunks.content'), key: 'content', sortable: false },
|
||||
{ title: t('chunks.charCount'), key: 'char_count', width: 150 },
|
||||
{ title: t('chunks.actions'), key: 'actions', sortable: false, align: 'end', width: 150 }
|
||||
]
|
||||
|
||||
// 过滤分块
|
||||
const filteredChunks = computed(() => {
|
||||
if (!searchQuery.value) return chunks.value
|
||||
const query = searchQuery.value.toLowerCase()
|
||||
return chunks.value.filter(chunk =>
|
||||
chunk.content.toLowerCase().includes(query)
|
||||
)
|
||||
})
|
||||
|
||||
// 加载文档详情
|
||||
const loadDocument = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await axios.get('/api/kb/document/get', {
|
||||
params: { doc_id: docId.value }
|
||||
})
|
||||
if (response.data.status === 'ok') {
|
||||
document.value = response.data.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load document:', error)
|
||||
showSnackbar('加载文档详情失败', 'error')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载分块列表
|
||||
const loadChunks = async () => {
|
||||
loadingChunks.value = true
|
||||
try {
|
||||
const response = await axios.get('/api/kb/chunk/list', {
|
||||
params: { doc_id: docId.value }
|
||||
})
|
||||
if (response.data.status === 'ok') {
|
||||
chunks.value = response.data.data.items || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load chunks:', error)
|
||||
showSnackbar('加载分块列表失败', 'error')
|
||||
} finally {
|
||||
loadingChunks.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 查看分块
|
||||
const viewChunk = (chunk: any) => {
|
||||
selectedChunk.value = chunk
|
||||
showViewDialog.value = true
|
||||
}
|
||||
|
||||
// 编辑分块
|
||||
const editChunk = (chunk: any) => {
|
||||
selectedChunk.value = chunk
|
||||
editForm.value.content = chunk.content
|
||||
showEditDialog.value = true
|
||||
}
|
||||
|
||||
// 关闭编辑对话框
|
||||
const closeEditDialog = () => {
|
||||
showEditDialog.value = false
|
||||
selectedChunk.value = null
|
||||
editForm.value.content = ''
|
||||
}
|
||||
|
||||
// 保存分块
|
||||
const saveChunk = async () => {
|
||||
if (!selectedChunk.value) return
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
const response = await axios.post('/api/kb/chunk/update', {
|
||||
chunk_id: selectedChunk.value.chunk_id,
|
||||
content: editForm.value.content
|
||||
})
|
||||
|
||||
if (response.data.status === 'ok') {
|
||||
showSnackbar(t('edit.saveSuccess'))
|
||||
closeEditDialog()
|
||||
await loadChunks()
|
||||
} else {
|
||||
showSnackbar(response.data.message || t('edit.saveFailed'), 'error')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save chunk:', error)
|
||||
showSnackbar(t('edit.saveFailed'), 'error')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 确认删除分块
|
||||
const confirmDeleteChunk = (chunk: any) => {
|
||||
deleteTarget.value = chunk
|
||||
showDeleteDialog.value = true
|
||||
}
|
||||
|
||||
// 删除分块
|
||||
const deleteChunk = async () => {
|
||||
if (!deleteTarget.value) return
|
||||
|
||||
deleting.value = true
|
||||
try {
|
||||
const response = await axios.post('/api/kb/chunk/delete', {
|
||||
chunk_id: deleteTarget.value.chunk_id
|
||||
})
|
||||
|
||||
if (response.data.status === 'ok') {
|
||||
showSnackbar(t('delete.deleteSuccess'))
|
||||
showDeleteDialog.value = false
|
||||
await loadChunks()
|
||||
await loadDocument()
|
||||
} else {
|
||||
showSnackbar(response.data.message || t('delete.deleteFailed'), 'error')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete chunk:', error)
|
||||
showSnackbar(t('delete.deleteFailed'), 'error')
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
const getFileIcon = (fileType: string) => {
|
||||
const type = fileType?.toLowerCase() || ''
|
||||
if (type.includes('pdf')) return 'mdi-file-pdf-box'
|
||||
if (type.includes('md')) return 'mdi-language-markdown'
|
||||
if (type.includes('txt')) return 'mdi-file-document-outline'
|
||||
return 'mdi-file'
|
||||
}
|
||||
|
||||
const getFileColor = (fileType: string) => {
|
||||
const type = fileType?.toLowerCase() || ''
|
||||
if (type.includes('pdf')) return 'error'
|
||||
if (type.includes('md')) return 'info'
|
||||
if (type.includes('txt')) return 'success'
|
||||
return 'grey'
|
||||
}
|
||||
|
||||
const formatFileSize = (bytes: number) => {
|
||||
if (!bytes) return '-'
|
||||
const units = ['B', 'KB', 'MB', 'GB']
|
||||
let size = bytes
|
||||
let unitIndex = 0
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024
|
||||
unitIndex++
|
||||
}
|
||||
return `${size.toFixed(2)} ${units[unitIndex]}`
|
||||
}
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
if (!dateStr) return '-'
|
||||
return new Date(dateStr).toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadDocument()
|
||||
loadChunks()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.document-detail-page {
|
||||
padding: 24px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.document-content {
|
||||
animation: slideUp 0.4s ease;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.chunk-content-preview {
|
||||
max-width: 400px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.chunk-content-view {
|
||||
padding: 16px;
|
||||
background: rgba(var(--v-theme-surface-variant), 0.3);
|
||||
border-radius: 8px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
line-height: 1.6;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.document-detail-page {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,359 @@
|
||||
<template>
|
||||
<div class="kb-detail-page">
|
||||
<!-- 页面头部 -->
|
||||
<div class="page-header">
|
||||
<v-btn
|
||||
icon="mdi-arrow-left"
|
||||
variant="text"
|
||||
@click="$router.push({ name: 'NativeKBList' })"
|
||||
/>
|
||||
<div class="header-content">
|
||||
<div class="kb-title">
|
||||
<span class="kb-emoji">{{ kb.emoji || '📚' }}</span>
|
||||
<h1 class="text-h4">{{ kb.kb_name }}</h1>
|
||||
</div>
|
||||
<p v-if="kb.description" class="text-subtitle-1 text-medium-emphasis mt-2">
|
||||
{{ kb.description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading-container">
|
||||
<v-progress-circular indeterminate color="primary" size="64" />
|
||||
</div>
|
||||
|
||||
<!-- 主内容 -->
|
||||
<div v-else class="kb-content">
|
||||
<!-- 标签页 -->
|
||||
<v-tabs v-model="activeTab" class="mb-6" color="primary">
|
||||
<v-tab value="overview">
|
||||
<v-icon start>mdi-information-outline</v-icon>
|
||||
{{ t('tabs.overview') }}
|
||||
</v-tab>
|
||||
<v-tab value="documents">
|
||||
<v-icon start>mdi-file-document-multiple</v-icon>
|
||||
{{ t('tabs.documents') }}
|
||||
<v-chip class="ml-2" size="small" variant="tonal">{{ kb.doc_count || 0 }}</v-chip>
|
||||
</v-tab>
|
||||
<v-tab value="sessions">
|
||||
<v-icon start>mdi-account-multiple</v-icon>
|
||||
{{ t('tabs.sessions') }}
|
||||
</v-tab>
|
||||
<v-tab value="settings">
|
||||
<v-icon start>mdi-cog</v-icon>
|
||||
{{ t('tabs.settings') }}
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<!-- 标签页内容 -->
|
||||
<v-window v-model="activeTab">
|
||||
<!-- 概览 -->
|
||||
<v-window-item value="overview">
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-card elevation="2">
|
||||
<v-card-title>{{ t('overview.title') }}</v-card-title>
|
||||
<v-divider />
|
||||
<v-card-text>
|
||||
<v-list density="comfortable">
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon>mdi-label</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ t('overview.name') }}</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ kb.kb_name }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item v-if="kb.description">
|
||||
<template #prepend>
|
||||
<v-icon>mdi-text</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ t('overview.description') }}</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ kb.description }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon>mdi-emoticon</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ t('overview.emoji') }}</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ kb.emoji || '📚' }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon>mdi-calendar-plus</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ t('overview.createdAt') }}</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ formatDate(kb.created_at) }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon>mdi-calendar-edit</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ t('overview.updatedAt') }}</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ formatDate(kb.updated_at) }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-card elevation="2" class="mb-4">
|
||||
<v-card-title>{{ t('overview.stats') }}</v-card-title>
|
||||
<v-divider />
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="6">
|
||||
<div class="stat-box">
|
||||
<v-icon size="48" color="primary">mdi-file-document</v-icon>
|
||||
<div class="stat-value">{{ kb.doc_count || 0 }}</div>
|
||||
<div class="stat-label">{{ t('overview.docCount') }}</div>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<div class="stat-box">
|
||||
<v-icon size="48" color="secondary">mdi-text-box</v-icon>
|
||||
<div class="stat-value">{{ kb.chunk_count || 0 }}</div>
|
||||
<div class="stat-label">{{ t('overview.chunkCount') }}</div>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<v-card elevation="2">
|
||||
<v-card-title>{{ t('overview.embeddingModel') }}</v-card-title>
|
||||
<v-divider />
|
||||
<v-card-text>
|
||||
<v-list density="comfortable">
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon>mdi-vector-point</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ t('overview.embeddingModel') }}</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ kb.embedding_provider_id || t('overview.notSet') }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon>mdi-sort-ascending</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ t('overview.rerankModel') }}</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ kb.rerank_provider_id || t('overview.notSet') }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-window-item>
|
||||
|
||||
<!-- 文档管理 -->
|
||||
<v-window-item value="documents">
|
||||
<DocumentsTab :kb-id="kbId" :kb="kb" @refresh="loadKB" />
|
||||
</v-window-item>
|
||||
|
||||
<!-- 会话配置 -->
|
||||
<v-window-item value="sessions">
|
||||
<SessionsTab :kb-id="kbId" />
|
||||
</v-window-item>
|
||||
|
||||
<!-- 设置 -->
|
||||
<v-window-item value="settings">
|
||||
<SettingsTab :kb="kb" @updated="loadKB" />
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
</div>
|
||||
|
||||
<!-- 消息提示 -->
|
||||
<v-snackbar v-model="snackbar.show" :color="snackbar.color">
|
||||
{{ snackbar.text }}
|
||||
</v-snackbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import axios from 'axios'
|
||||
import { useModuleI18n } from '@/i18n/composables'
|
||||
import DocumentsTab from './components/DocumentsTab.vue'
|
||||
import SessionsTab from './components/SessionsTab.vue'
|
||||
import SettingsTab from './components/SettingsTab.vue'
|
||||
|
||||
const { tm: t } = useModuleI18n('features/knowledge-base/detail')
|
||||
const route = useRoute()
|
||||
|
||||
const kbId = ref(route.params.kbId as string)
|
||||
const loading = ref(true)
|
||||
const activeTab = ref('overview')
|
||||
const kb = ref<any>({})
|
||||
|
||||
const snackbar = ref({
|
||||
show: false,
|
||||
text: '',
|
||||
color: 'success'
|
||||
})
|
||||
|
||||
const showSnackbar = (text: string, color: string = 'success') => {
|
||||
snackbar.value.text = text
|
||||
snackbar.value.color = color
|
||||
snackbar.value.show = true
|
||||
}
|
||||
|
||||
// 加载知识库详情
|
||||
const loadKB = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await axios.get('/api/kb/get', {
|
||||
params: { kb_id: kbId.value }
|
||||
})
|
||||
if (response.data.status === 'ok') {
|
||||
kb.value = response.data.data
|
||||
} else {
|
||||
showSnackbar(response.data.message || '加载失败', 'error')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load knowledge base:', error)
|
||||
showSnackbar('加载知识库详情失败', 'error')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr: string) => {
|
||||
if (!dateStr) return '-'
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadKB()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.kb-detail-page {
|
||||
padding: 24px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.kb-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.kb-emoji {
|
||||
font-size: 48px;
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-8px); }
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.kb-content {
|
||||
animation: slideUp 0.4s ease;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.stat-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
border-radius: 12px;
|
||||
background: rgba(var(--v-theme-surface-variant), 0.3);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.stat-box:hover {
|
||||
transform: translateY(-4px);
|
||||
background: rgba(var(--v-theme-surface-variant), 0.5);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
margin-top: 8px;
|
||||
color: rgb(var(--v-theme-on-surface));
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.875rem;
|
||||
color: rgb(var(--v-theme-on-surface-variant));
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.kb-detail-page {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.kb-title {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.kb-emoji {
|
||||
font-size: 36px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,686 @@
|
||||
<template>
|
||||
<div class="kb-list-page">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="text-h4 mb-2">{{ t('list.title') }}</h1>
|
||||
<p class="text-subtitle-1 text-medium-emphasis">{{ t('list.subtitle') }}</p>
|
||||
</div>
|
||||
<v-btn
|
||||
icon="mdi-information-outline"
|
||||
variant="text"
|
||||
size="small"
|
||||
color="grey"
|
||||
href="https://astrbot.app/use/knowledge-base.html"
|
||||
target="_blank"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮栏 -->
|
||||
<div class="action-bar mb-6">
|
||||
<v-btn
|
||||
prepend-icon="mdi-plus"
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
@click="showCreateDialog = true"
|
||||
>
|
||||
{{ t('list.create') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
prepend-icon="mdi-refresh"
|
||||
variant="tonal"
|
||||
@click="loadKnowledgeBases"
|
||||
:loading="loading"
|
||||
>
|
||||
{{ t('list.refresh') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<!-- 知识库网格 -->
|
||||
<div v-if="loading && kbList.length === 0" class="loading-container">
|
||||
<v-progress-circular indeterminate color="primary" size="64" />
|
||||
<p class="mt-4 text-medium-emphasis">{{ t('list.loading') }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="kbList.length > 0" class="kb-grid">
|
||||
<v-card
|
||||
v-for="kb in kbList"
|
||||
:key="kb.kb_id"
|
||||
class="kb-card"
|
||||
elevation="2"
|
||||
hover
|
||||
@click="navigateToDetail(kb.kb_id)"
|
||||
>
|
||||
<div class="kb-card-content">
|
||||
<div class="kb-emoji">{{ kb.emoji || '📚' }}</div>
|
||||
<h3 class="kb-name">{{ kb.kb_name }}</h3>
|
||||
<p class="kb-description text-medium-emphasis">{{ kb.description || '暂无描述' }}</p>
|
||||
|
||||
<div class="kb-stats mt-4">
|
||||
<div class="stat-item">
|
||||
<v-icon size="small" color="primary">mdi-file-document</v-icon>
|
||||
<span>{{ kb.doc_count || 0 }} {{ t('list.documents') }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<v-icon size="small" color="secondary">mdi-text-box</v-icon>
|
||||
<span>{{ kb.chunk_count || 0 }} {{ t('list.chunks') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kb-actions">
|
||||
<v-btn
|
||||
icon="mdi-pencil"
|
||||
size="small"
|
||||
variant="text"
|
||||
color="info"
|
||||
@click.stop="editKB(kb)"
|
||||
/>
|
||||
<v-btn
|
||||
icon="mdi-delete"
|
||||
size="small"
|
||||
variant="text"
|
||||
color="error"
|
||||
@click.stop="confirmDelete(kb)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</v-card>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-else class="empty-state">
|
||||
<v-icon size="100" color="grey-lighten-2">mdi-book-open-variant</v-icon>
|
||||
<h2 class="mt-4">{{ t('list.empty') }}</h2>
|
||||
<v-btn
|
||||
class="mt-6"
|
||||
prepend-icon="mdi-plus"
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
size="large"
|
||||
@click="showCreateDialog = true"
|
||||
>
|
||||
{{ t('list.create') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<!-- 创建/编辑对话框 -->
|
||||
<v-dialog v-model="showCreateDialog" max-width="600px" persistent>
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center pa-4">
|
||||
<span class="text-h5">{{ editingKB ? t('edit.title') : t('create.title') }}</span>
|
||||
<v-spacer />
|
||||
<v-btn icon="mdi-close" variant="text" @click="closeCreateDialog" />
|
||||
</v-card-title>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-card-text class="pa-6">
|
||||
<!-- Emoji 选择器 -->
|
||||
<div class="text-center mb-6">
|
||||
<div class="emoji-display" @click="showEmojiPicker = true">
|
||||
{{ formData.emoji }}
|
||||
</div>
|
||||
<p class="text-caption text-medium-emphasis mt-2">{{ t('create.emojiLabel') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 表单 -->
|
||||
<v-form ref="formRef" @submit.prevent="submitForm">
|
||||
<v-text-field
|
||||
v-model="formData.kb_name"
|
||||
:label="t('create.nameLabel')"
|
||||
:placeholder="t('create.namePlaceholder')"
|
||||
variant="outlined"
|
||||
:rules="[v => !!v || t('create.nameRequired')]"
|
||||
required
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<v-textarea
|
||||
v-model="formData.description"
|
||||
:label="t('create.descriptionLabel')"
|
||||
:placeholder="t('create.descriptionPlaceholder')"
|
||||
variant="outlined"
|
||||
rows="3"
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<v-select
|
||||
v-model="formData.embedding_provider_id"
|
||||
:items="embeddingProviders"
|
||||
:item-title="item => item.embedding_model || item.id"
|
||||
:item-value="'id'"
|
||||
:label="t('create.embeddingModelLabel')"
|
||||
variant="outlined"
|
||||
class="mb-4"
|
||||
>
|
||||
<template #item="{ props, item }">
|
||||
<v-list-item v-bind="props">
|
||||
<template #subtitle>
|
||||
{{ t('create.providerInfo', {
|
||||
id: item.raw.id,
|
||||
dimensions: item.raw.embedding_dimensions || 'N/A'
|
||||
}) }}
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-select>
|
||||
|
||||
<v-select
|
||||
v-model="formData.rerank_provider_id"
|
||||
:items="rerankProviders"
|
||||
:item-title="item => item.rerank_model || item.id"
|
||||
:item-value="'id'"
|
||||
:label="t('create.rerankModelLabel')"
|
||||
variant="outlined"
|
||||
clearable
|
||||
class="mb-2"
|
||||
>
|
||||
<template #item="{ props, item }">
|
||||
<v-list-item v-bind="props">
|
||||
<template #subtitle>
|
||||
{{ t('create.rerankProviderInfo', { id: item.raw.id }) }}
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-select>
|
||||
|
||||
<v-alert type="info" variant="tonal" density="compact" class="mt-4">
|
||||
{{ t('create.tips') }}
|
||||
</v-alert>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer />
|
||||
<v-btn variant="text" @click="closeCreateDialog">
|
||||
{{ t('create.cancel') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
@click="submitForm"
|
||||
:loading="saving"
|
||||
>
|
||||
{{ editingKB ? t('edit.submit') : t('create.submit') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- Emoji 选择器对话框 -->
|
||||
<v-dialog v-model="showEmojiPicker" max-width="500px">
|
||||
<v-card>
|
||||
<v-card-title class="pa-4">{{ t('emoji.title') }}</v-card-title>
|
||||
<v-divider />
|
||||
<v-card-text class="pa-4">
|
||||
<div v-for="category in emojiCategories" :key="category.key" class="mb-4">
|
||||
<p class="text-subtitle-2 mb-2">{{ t(`emoji.categories.${category.key}`) }}</p>
|
||||
<div class="emoji-grid">
|
||||
<div
|
||||
v-for="emoji in category.emojis"
|
||||
:key="emoji"
|
||||
class="emoji-item"
|
||||
@click="selectEmoji(emoji)"
|
||||
>
|
||||
{{ emoji }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-divider />
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer />
|
||||
<v-btn variant="text" @click="showEmojiPicker = false">
|
||||
{{ t('emoji.close') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 删除确认对话框 -->
|
||||
<v-dialog v-model="showDeleteDialog" max-width="450px" persistent>
|
||||
<v-card>
|
||||
<v-card-title class="pa-4 text-h6">{{ t('delete.title') }}</v-card-title>
|
||||
<v-divider />
|
||||
<v-card-text class="pa-6">
|
||||
<p>{{ t('delete.confirmText', { name: deleteTarget?.kb_name || '' }) }}</p>
|
||||
<v-alert type="error" variant="tonal" density="compact" class="mt-4">
|
||||
{{ t('delete.warning') }}
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
<v-divider />
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer />
|
||||
<v-btn variant="text" @click="cancelDelete">
|
||||
{{ t('delete.cancel') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="error"
|
||||
variant="elevated"
|
||||
@click="deleteKB"
|
||||
:loading="deleting"
|
||||
>
|
||||
{{ t('delete.confirm') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 消息提示 -->
|
||||
<v-snackbar v-model="snackbar.show" :color="snackbar.color">
|
||||
{{ snackbar.text }}
|
||||
</v-snackbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import axios from 'axios'
|
||||
import { useModuleI18n } from '@/i18n/composables'
|
||||
|
||||
const { tm: t } = useModuleI18n('features/knowledge-base/index')
|
||||
const router = useRouter()
|
||||
|
||||
// 状态
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const deleting = ref(false)
|
||||
const kbList = ref<any[]>([])
|
||||
const embeddingProviders = ref<any[]>([])
|
||||
const rerankProviders = ref<any[]>([])
|
||||
|
||||
// 对话框
|
||||
const showCreateDialog = ref(false)
|
||||
const showEmojiPicker = ref(false)
|
||||
const showDeleteDialog = ref(false)
|
||||
|
||||
// Snackbar 通知
|
||||
const snackbar = ref({
|
||||
show: false,
|
||||
text: '',
|
||||
color: 'success'
|
||||
})
|
||||
|
||||
// 表单
|
||||
const formRef = ref()
|
||||
const editingKB = ref<any>(null)
|
||||
const deleteTarget = ref<any>(null)
|
||||
const formData = ref({
|
||||
kb_name: '',
|
||||
description: '',
|
||||
emoji: '📚',
|
||||
embedding_provider_id: null,
|
||||
rerank_provider_id: null
|
||||
})
|
||||
|
||||
// Emoji 分类
|
||||
const emojiCategories = [
|
||||
{
|
||||
key: 'books',
|
||||
emojis: ['📚', '📖', '📕', '📗', '📘', '📙', '📓', '📔', '📒', '📑', '🗂️', '📂', '📁', '🗃️', '🗄️']
|
||||
},
|
||||
{
|
||||
key: 'emotions',
|
||||
emojis: ['😀', '😃', '😄', '😁', '😆', '😅', '🤣', '😂', '🙂', '🙃', '😉', '😊', '😇', '🥰', '😍']
|
||||
},
|
||||
{
|
||||
key: 'objects',
|
||||
emojis: ['💡', '🔬', '🔭', '🗿', '🏆', '🎯', '🎓', '🔑', '🔒', '🔓', '🔔', '🔕', '🔨', '🛠️', '⚙️']
|
||||
},
|
||||
{
|
||||
key: 'symbols',
|
||||
emojis: ['❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', '🤍', '🤎', '⭐', '🌟', '✨', '💫', '⚡', '🔥']
|
||||
}
|
||||
]
|
||||
|
||||
// 加载知识库列表
|
||||
const loadKnowledgeBases = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await axios.get('/api/kb/list')
|
||||
if (response.data.status === 'ok') {
|
||||
kbList.value = response.data.data.items || []
|
||||
} else {
|
||||
showSnackbar(response.data.message || t('messages.loadError'), 'error')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load knowledge bases:', error)
|
||||
showSnackbar(t('messages.loadError'), 'error')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载提供商配置
|
||||
const loadProviders = async () => {
|
||||
try {
|
||||
const response = await axios.get('/api/config/provider/list', {
|
||||
params: { provider_type: 'embedding,rerank' }
|
||||
})
|
||||
if (response.data.status === 'ok') {
|
||||
embeddingProviders.value = response.data.data.filter(
|
||||
(p: any) => p.provider_type === 'embedding'
|
||||
)
|
||||
rerankProviders.value = response.data.data.filter(
|
||||
(p: any) => p.provider_type === 'rerank'
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load providers:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 导航到详情页
|
||||
const navigateToDetail = (kbId: string) => {
|
||||
router.push({ name: 'NativeKBDetail', params: { kbId } })
|
||||
}
|
||||
|
||||
// 编辑知识库
|
||||
const editKB = (kb: any) => {
|
||||
editingKB.value = kb
|
||||
formData.value = {
|
||||
kb_name: kb.kb_name,
|
||||
description: kb.description || '',
|
||||
emoji: kb.emoji || '📚',
|
||||
embedding_provider_id: kb.embedding_provider_id,
|
||||
rerank_provider_id: kb.rerank_provider_id
|
||||
}
|
||||
showCreateDialog.value = true
|
||||
}
|
||||
|
||||
// 确认删除
|
||||
const confirmDelete = (kb: any) => {
|
||||
deleteTarget.value = kb
|
||||
showDeleteDialog.value = true
|
||||
}
|
||||
|
||||
// 取消删除
|
||||
const cancelDelete = () => {
|
||||
showDeleteDialog.value = false
|
||||
deleteTarget.value = null
|
||||
}
|
||||
|
||||
// 删除知识库
|
||||
const deleteKB = async () => {
|
||||
if (!deleteTarget.value) return
|
||||
|
||||
deleting.value = true
|
||||
try {
|
||||
const response = await axios.post('/api/kb/delete', {
|
||||
kb_id: deleteTarget.value.kb_id
|
||||
})
|
||||
|
||||
console.log('Delete response:', response.data) // 调试日志
|
||||
|
||||
if (response.data.status === 'ok') {
|
||||
showSnackbar(t('messages.deleteSuccess'))
|
||||
// 先刷新列表,再关闭对话框
|
||||
await loadKnowledgeBases()
|
||||
showDeleteDialog.value = false
|
||||
deleteTarget.value = null
|
||||
} else {
|
||||
showSnackbar(response.data.message || t('messages.deleteFailed'), 'error')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete knowledge base:', error)
|
||||
showSnackbar(t('messages.deleteFailed'), 'error')
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const submitForm = async () => {
|
||||
const { valid } = await formRef.value.validate()
|
||||
if (!valid) return
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
const payload = {
|
||||
kb_name: formData.value.kb_name,
|
||||
description: formData.value.description,
|
||||
emoji: formData.value.emoji,
|
||||
embedding_provider_id: formData.value.embedding_provider_id,
|
||||
rerank_provider_id: formData.value.rerank_provider_id
|
||||
}
|
||||
|
||||
let response
|
||||
if (editingKB.value) {
|
||||
response = await axios.post('/api/kb/update', {
|
||||
kb_id: editingKB.value.kb_id,
|
||||
...payload
|
||||
})
|
||||
} else {
|
||||
response = await axios.post('/api/kb/create', payload)
|
||||
}
|
||||
|
||||
if (response.data.status === 'ok') {
|
||||
showSnackbar(editingKB.value ? t('messages.updateSuccess') : t('messages.createSuccess'))
|
||||
closeCreateDialog()
|
||||
await loadKnowledgeBases()
|
||||
} else {
|
||||
showSnackbar(response.data.message || (editingKB.value ? t('messages.updateFailed') : t('messages.createFailed')), 'error')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save knowledge base:', error)
|
||||
showSnackbar(editingKB.value ? t('messages.updateFailed') : t('messages.createFailed'), 'error')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭创建对话框
|
||||
const closeCreateDialog = () => {
|
||||
showCreateDialog.value = false
|
||||
editingKB.value = null
|
||||
formData.value = {
|
||||
kb_name: '',
|
||||
description: '',
|
||||
emoji: '📚',
|
||||
embedding_provider_id: null,
|
||||
rerank_provider_id: null
|
||||
}
|
||||
formRef.value?.reset()
|
||||
}
|
||||
|
||||
// 选择 emoji
|
||||
const selectEmoji = (emoji: string) => {
|
||||
formData.value.emoji = emoji
|
||||
showEmojiPicker.value = false
|
||||
}
|
||||
|
||||
// 显示通知
|
||||
const showSnackbar = (text: string, color: string = 'success') => {
|
||||
snackbar.value.text = text
|
||||
snackbar.value.color = color
|
||||
snackbar.value.show = true
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadKnowledgeBases()
|
||||
loadProviders()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.kb-list-page {
|
||||
padding: 24px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* 知识库网格 */
|
||||
.kb-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.kb-card {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.kb-card:hover {
|
||||
transform: translateY(-8px);
|
||||
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15) !important;
|
||||
}
|
||||
|
||||
.kb-card-content {
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
min-height: 260px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.kb-emoji {
|
||||
font-size: 56px;
|
||||
margin-bottom: 16px;
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-10px); }
|
||||
}
|
||||
|
||||
.kb-name {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
color: rgb(var(--v-theme-on-surface));
|
||||
}
|
||||
|
||||
.kb-description {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
max-height: 3em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.kb-stats {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 0.875rem;
|
||||
color: rgb(var(--v-theme-on-surface-variant));
|
||||
}
|
||||
|
||||
.kb-actions {
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.kb-card:hover .kb-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 400px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
/* Emoji 显示和选择器 */
|
||||
.emoji-display {
|
||||
font-size: 72px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
display: inline-block;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
background: rgba(var(--v-theme-primary), 0.05);
|
||||
}
|
||||
|
||||
.emoji-display:hover {
|
||||
transform: scale(1.1);
|
||||
background: rgba(var(--v-theme-primary), 0.1);
|
||||
}
|
||||
|
||||
.emoji-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(8, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.emoji-item {
|
||||
font-size: 32px;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.emoji-item:hover {
|
||||
background: rgba(var(--v-theme-primary), 0.1);
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.kb-list-page {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.kb-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.emoji-grid {
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,469 @@
|
||||
<template>
|
||||
<div class="documents-tab">
|
||||
<!-- 操作栏 -->
|
||||
<div class="action-bar mb-4">
|
||||
<v-btn
|
||||
prepend-icon="mdi-upload"
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
@click="showUploadDialog = true"
|
||||
>
|
||||
{{ t('documents.upload') }}
|
||||
</v-btn>
|
||||
<v-text-field
|
||||
v-model="searchQuery"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
:placeholder="'搜索文档...'"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details
|
||||
clearable
|
||||
style="max-width: 300px"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 文档列表 -->
|
||||
<v-card elevation="2">
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="documents"
|
||||
:loading="loading"
|
||||
:search="searchQuery"
|
||||
:items-per-page="10"
|
||||
>
|
||||
<template #item.doc_name="{ item }">
|
||||
<div class="d-flex align-center gap-2">
|
||||
<v-icon :color="getFileColor(item.file_type)">
|
||||
{{ getFileIcon(item.file_type) }}
|
||||
</v-icon>
|
||||
<span class="font-weight-medium">{{ item.doc_name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #item.file_size="{ item }">
|
||||
{{ formatFileSize(item.file_size) }}
|
||||
</template>
|
||||
|
||||
<template #item.created_at="{ item }">
|
||||
{{ formatDate(item.created_at) }}
|
||||
</template>
|
||||
|
||||
<template #item.actions="{ item }">
|
||||
<v-btn
|
||||
icon="mdi-eye"
|
||||
variant="text"
|
||||
size="small"
|
||||
color="info"
|
||||
@click="viewDocument(item)"
|
||||
/>
|
||||
<v-btn
|
||||
icon="mdi-delete"
|
||||
variant="text"
|
||||
size="small"
|
||||
color="error"
|
||||
@click="confirmDelete(item)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #no-data>
|
||||
<div class="text-center py-8">
|
||||
<v-icon size="64" color="grey-lighten-2">mdi-file-document-outline</v-icon>
|
||||
<p class="mt-4 text-medium-emphasis">{{ t('documents.empty') }}</p>
|
||||
</div>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card>
|
||||
|
||||
<!-- 上传对话框 -->
|
||||
<v-dialog v-model="showUploadDialog" max-width="600px" persistent>
|
||||
<v-card>
|
||||
<v-card-title class="pa-4">
|
||||
<span class="text-h5">{{ t('upload.title') }}</span>
|
||||
<v-spacer />
|
||||
<v-btn icon="mdi-close" variant="text" @click="closeUploadDialog" />
|
||||
</v-card-title>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-card-text class="pa-6">
|
||||
<!-- 文件选择 -->
|
||||
<div
|
||||
class="upload-dropzone"
|
||||
:class="{ 'dragover': isDragging }"
|
||||
@drop.prevent="handleDrop"
|
||||
@dragover.prevent="isDragging = true"
|
||||
@dragleave="isDragging = false"
|
||||
@click="$refs.fileInput.click()"
|
||||
>
|
||||
<v-icon size="64" color="primary">mdi-cloud-upload</v-icon>
|
||||
<p class="mt-4 text-h6">{{ t('upload.dropzone') }}</p>
|
||||
<p class="text-caption text-medium-emphasis mt-2">{{ t('upload.supportedFormats') }}</p>
|
||||
<p class="text-caption text-medium-emphasis">{{ t('upload.maxSize') }}</p>
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
hidden
|
||||
accept=".txt,.md,.pdf"
|
||||
@change="handleFileSelect"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedFile" class="mt-4 pa-4 rounded bg-surface-variant">
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<div class="d-flex align-center gap-2">
|
||||
<v-icon>{{ getFileIcon(selectedFile.name) }}</v-icon>
|
||||
<div>
|
||||
<div class="font-weight-medium">{{ selectedFile.name }}</div>
|
||||
<div class="text-caption">{{ formatFileSize(selectedFile.size) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<v-btn icon="mdi-close" variant="text" size="small" @click="selectedFile = null" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分块设置 -->
|
||||
<v-expansion-panels class="mt-4">
|
||||
<v-expansion-panel>
|
||||
<v-expansion-panel-title>
|
||||
<v-icon start>mdi-cog</v-icon>
|
||||
{{ t('upload.chunkSettings') }}
|
||||
</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>
|
||||
<v-text-field
|
||||
v-model.number="uploadSettings.chunk_size"
|
||||
:label="t('upload.chunkSize')"
|
||||
:hint="t('upload.chunkSizeHint')"
|
||||
type="number"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
class="mb-2"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model.number="uploadSettings.chunk_overlap"
|
||||
:label="t('upload.chunkOverlap')"
|
||||
:hint="t('upload.chunkOverlapHint')"
|
||||
type="number"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
/>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer />
|
||||
<v-btn variant="text" @click="closeUploadDialog">
|
||||
{{ t('upload.cancel') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
@click="uploadDocument"
|
||||
:loading="uploading"
|
||||
:disabled="!selectedFile"
|
||||
>
|
||||
{{ t('upload.submit') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 删除确认对话框 -->
|
||||
<v-dialog v-model="showDeleteDialog" max-width="450px">
|
||||
<v-card>
|
||||
<v-card-title class="pa-4 text-h6">{{ t('documents.delete') }}</v-card-title>
|
||||
<v-divider />
|
||||
<v-card-text class="pa-6">
|
||||
<p>{{ t('documents.deleteConfirm', { name: deleteTarget?.doc_name || '' }) }}</p>
|
||||
<v-alert type="error" variant="tonal" density="compact" class="mt-4">
|
||||
{{ t('documents.deleteWarning') }}
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
<v-divider />
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer />
|
||||
<v-btn variant="text" @click="showDeleteDialog = false">取消</v-btn>
|
||||
<v-btn color="error" variant="elevated" @click="deleteDocument" :loading="deleting">
|
||||
删除
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 消息提示 -->
|
||||
<v-snackbar v-model="snackbar.show" :color="snackbar.color">
|
||||
{{ snackbar.text }}
|
||||
</v-snackbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import axios from 'axios'
|
||||
import { useModuleI18n } from '@/i18n/composables'
|
||||
|
||||
const { tm: t } = useModuleI18n('features/knowledge-base/detail')
|
||||
const router = useRouter()
|
||||
|
||||
const props = defineProps<{
|
||||
kbId: string
|
||||
kb: any
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['refresh'])
|
||||
|
||||
// 状态
|
||||
const loading = ref(false)
|
||||
const uploading = ref(false)
|
||||
const deleting = ref(false)
|
||||
const documents = ref<any[]>([])
|
||||
const searchQuery = ref('')
|
||||
const showUploadDialog = ref(false)
|
||||
const showDeleteDialog = ref(false)
|
||||
const selectedFile = ref<File | null>(null)
|
||||
const deleteTarget = ref<any>(null)
|
||||
const isDragging = ref(false)
|
||||
|
||||
const snackbar = ref({
|
||||
show: false,
|
||||
text: '',
|
||||
color: 'success'
|
||||
})
|
||||
|
||||
const showSnackbar = (text: string, color: string = 'success') => {
|
||||
snackbar.value.text = text
|
||||
snackbar.value.color = color
|
||||
snackbar.value.show = true
|
||||
}
|
||||
|
||||
// 上传设置
|
||||
const uploadSettings = ref({
|
||||
chunk_size: null,
|
||||
chunk_overlap: null
|
||||
})
|
||||
|
||||
// 表格列
|
||||
const headers = [
|
||||
{ title: t('documents.name'), key: 'doc_name', sortable: true },
|
||||
{ title: t('documents.type'), key: 'file_type', sortable: true },
|
||||
{ title: t('documents.size'), key: 'file_size', sortable: true },
|
||||
{ title: t('documents.chunks'), key: 'chunk_count', sortable: true },
|
||||
{ title: t('documents.createdAt'), key: 'created_at', sortable: true },
|
||||
{ title: t('documents.actions'), key: 'actions', sortable: false, align: 'end' }
|
||||
]
|
||||
|
||||
// 加载文档列表
|
||||
const loadDocuments = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await axios.get('/api/kb/document/list', {
|
||||
params: { kb_id: props.kbId }
|
||||
})
|
||||
if (response.data.status === 'ok') {
|
||||
documents.value = response.data.data.items || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load documents:', error)
|
||||
showSnackbar('加载文档列表失败', 'error')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 文件选择
|
||||
const handleFileSelect = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
if (target.files && target.files[0]) {
|
||||
selectedFile.value = target.files[0]
|
||||
}
|
||||
}
|
||||
|
||||
// 拖放上传
|
||||
const handleDrop = (event: DragEvent) => {
|
||||
isDragging.value = false
|
||||
if (event.dataTransfer?.files && event.dataTransfer.files[0]) {
|
||||
selectedFile.value = event.dataTransfer.files[0]
|
||||
}
|
||||
}
|
||||
|
||||
// 上传文档
|
||||
const uploadDocument = async () => {
|
||||
if (!selectedFile.value) {
|
||||
showSnackbar(t('upload.fileRequired'), 'warning')
|
||||
return
|
||||
}
|
||||
|
||||
uploading.value = true
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('file', selectedFile.value)
|
||||
formData.append('kb_id', props.kbId)
|
||||
if (uploadSettings.value.chunk_size) {
|
||||
formData.append('chunk_size', uploadSettings.value.chunk_size.toString())
|
||||
}
|
||||
if (uploadSettings.value.chunk_overlap) {
|
||||
formData.append('chunk_overlap', uploadSettings.value.chunk_overlap.toString())
|
||||
}
|
||||
|
||||
const response = await axios.post('/api/kb/document/upload', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
|
||||
if (response.data.status === 'ok') {
|
||||
showSnackbar(t('documents.uploadSuccess'))
|
||||
closeUploadDialog()
|
||||
await loadDocuments()
|
||||
emit('refresh')
|
||||
} else {
|
||||
showSnackbar(response.data.message || t('documents.uploadFailed'), 'error')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to upload document:', error)
|
||||
showSnackbar(t('documents.uploadFailed'), 'error')
|
||||
} finally {
|
||||
uploading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭上传对话框
|
||||
const closeUploadDialog = () => {
|
||||
showUploadDialog.value = false
|
||||
selectedFile.value = null
|
||||
uploadSettings.value = { chunk_size: null, chunk_overlap: null }
|
||||
}
|
||||
|
||||
// 查看文档
|
||||
const viewDocument = (doc: any) => {
|
||||
router.push({
|
||||
name: 'NativeDocumentDetail',
|
||||
params: { kbId: props.kbId, docId: doc.doc_id }
|
||||
})
|
||||
}
|
||||
|
||||
// 确认删除
|
||||
const confirmDelete = (doc: any) => {
|
||||
deleteTarget.value = doc
|
||||
showDeleteDialog.value = true
|
||||
}
|
||||
|
||||
// 删除文档
|
||||
const deleteDocument = async () => {
|
||||
if (!deleteTarget.value) return
|
||||
|
||||
deleting.value = true
|
||||
try {
|
||||
const response = await axios.post('/api/kb/document/delete', {
|
||||
doc_id: deleteTarget.value.doc_id
|
||||
})
|
||||
|
||||
if (response.data.status === 'ok') {
|
||||
showSnackbar(t('documents.deleteSuccess'))
|
||||
showDeleteDialog.value = false
|
||||
await loadDocuments()
|
||||
emit('refresh')
|
||||
} else {
|
||||
showSnackbar(response.data.message || t('documents.deleteFailed'), 'error')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete document:', error)
|
||||
showSnackbar(t('documents.deleteFailed'), 'error')
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
const getFileIcon = (fileType: string) => {
|
||||
const type = fileType?.toLowerCase() || ''
|
||||
if (type.includes('pdf')) return 'mdi-file-pdf-box'
|
||||
if (type.includes('md') || type.includes('markdown')) return 'mdi-language-markdown'
|
||||
if (type.includes('txt')) return 'mdi-file-document-outline'
|
||||
return 'mdi-file'
|
||||
}
|
||||
|
||||
const getFileColor = (fileType: string) => {
|
||||
const type = fileType?.toLowerCase() || ''
|
||||
if (type.includes('pdf')) return 'error'
|
||||
if (type.includes('md')) return 'info'
|
||||
if (type.includes('txt')) return 'success'
|
||||
return 'grey'
|
||||
}
|
||||
|
||||
const formatFileSize = (bytes: number) => {
|
||||
if (!bytes) return '-'
|
||||
const units = ['B', 'KB', 'MB', 'GB']
|
||||
let size = bytes
|
||||
let unitIndex = 0
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024
|
||||
unitIndex++
|
||||
}
|
||||
return `${size.toFixed(2)} ${units[unitIndex]}`
|
||||
}
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
if (!dateStr) return '-'
|
||||
return new Date(dateStr).toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadDocuments()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.documents-tab {
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.upload-dropzone {
|
||||
border: 2px dashed rgba(var(--v-theme-primary), 0.3);
|
||||
border-radius: 12px;
|
||||
padding: 48px 24px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
background: rgba(var(--v-theme-surface-variant), 0.3);
|
||||
}
|
||||
|
||||
.upload-dropzone:hover,
|
||||
.upload-dropzone.dragover {
|
||||
border-color: rgb(var(--v-theme-primary));
|
||||
background: rgba(var(--v-theme-primary), 0.05);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.action-bar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.action-bar > * {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,320 @@
|
||||
<template>
|
||||
<div class="sessions-tab">
|
||||
<v-card elevation="2">
|
||||
<v-card-title class="d-flex align-center pa-4">
|
||||
<span>{{ t('sessions.title') }}</span>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
prepend-icon="mdi-plus"
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
size="small"
|
||||
@click="showAddDialog = true"
|
||||
>
|
||||
{{ t('sessions.add') }}
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-subtitle class="px-4 pb-4">
|
||||
{{ t('sessions.subtitle') }}
|
||||
</v-card-subtitle>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-card-text class="pa-0">
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="sessions"
|
||||
:loading="loading"
|
||||
>
|
||||
<template #item.scope="{ item }">
|
||||
<v-chip :color="item.scope === 'session' ? 'primary' : 'secondary'" size="small" variant="tonal">
|
||||
{{ item.scope === 'session' ? t('sessions.scopeSession') : t('sessions.scopePlatform') }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<template #item.enable_rerank="{ item }">
|
||||
<v-icon :color="item.enable_rerank ? 'success' : 'grey'">
|
||||
{{ item.enable_rerank ? 'mdi-check-circle' : 'mdi-close-circle' }}
|
||||
</v-icon>
|
||||
</template>
|
||||
|
||||
<template #item.actions="{ item }">
|
||||
<v-btn
|
||||
icon="mdi-delete"
|
||||
variant="text"
|
||||
size="small"
|
||||
color="error"
|
||||
@click="confirmDelete(item)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #no-data>
|
||||
<div class="text-center py-8">
|
||||
<v-icon size="64" color="grey-lighten-2">mdi-account-multiple-outline</v-icon>
|
||||
<p class="mt-4 text-medium-emphasis">{{ t('sessions.empty') }}</p>
|
||||
</div>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- 添加配置对话框 -->
|
||||
<v-dialog v-model="showAddDialog" max-width="500px" persistent>
|
||||
<v-card>
|
||||
<v-card-title class="pa-4">
|
||||
<span class="text-h6">{{ t('sessions.add') }}</span>
|
||||
<v-spacer />
|
||||
<v-btn icon="mdi-close" variant="text" @click="closeAddDialog" />
|
||||
</v-card-title>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-card-text class="pa-6">
|
||||
<v-form ref="formRef">
|
||||
<v-select
|
||||
v-model="formData.scope"
|
||||
:items="scopeOptions"
|
||||
:label="t('sessions.scope')"
|
||||
variant="outlined"
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
v-model="formData.scope_id"
|
||||
:label="t('sessions.scopeId')"
|
||||
:placeholder="formData.scope === 'session' ? 'platform:xxx:session_id' : 'platform_id'"
|
||||
variant="outlined"
|
||||
required
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
v-model.number="formData.top_k"
|
||||
:label="t('sessions.topK')"
|
||||
type="number"
|
||||
variant="outlined"
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<v-checkbox
|
||||
v-model="formData.enable_rerank"
|
||||
:label="t('sessions.enableRerank')"
|
||||
color="primary"
|
||||
/>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer />
|
||||
<v-btn variant="text" @click="closeAddDialog">取消</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
@click="addSession"
|
||||
:loading="saving"
|
||||
>
|
||||
添加
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 删除确认对话框 -->
|
||||
<v-dialog v-model="showDeleteDialog" max-width="400px">
|
||||
<v-card>
|
||||
<v-card-title class="pa-4 text-h6">确认删除</v-card-title>
|
||||
<v-divider />
|
||||
<v-card-text class="pa-6">
|
||||
<p>{{ t('sessions.deleteConfirm') }}</p>
|
||||
</v-card-text>
|
||||
<v-divider />
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer />
|
||||
<v-btn variant="text" @click="showDeleteDialog = false">取消</v-btn>
|
||||
<v-btn
|
||||
color="error"
|
||||
variant="elevated"
|
||||
@click="deleteSession"
|
||||
:loading="deleting"
|
||||
>
|
||||
删除
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 消息提示 -->
|
||||
<v-snackbar v-model="snackbar.show" :color="snackbar.color">
|
||||
{{ snackbar.text }}
|
||||
</v-snackbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import axios from 'axios'
|
||||
import { useModuleI18n } from '@/i18n/composables'
|
||||
|
||||
const { tm: t } = useModuleI18n('features/knowledge-base/detail')
|
||||
|
||||
const props = defineProps<{
|
||||
kbId: string
|
||||
}>()
|
||||
|
||||
// 状态
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const deleting = ref(false)
|
||||
const sessions = ref<any[]>([])
|
||||
const showAddDialog = ref(false)
|
||||
const showDeleteDialog = ref(false)
|
||||
const deleteTarget = ref<any>(null)
|
||||
const formRef = ref()
|
||||
|
||||
const snackbar = ref({
|
||||
show: false,
|
||||
text: '',
|
||||
color: 'success'
|
||||
})
|
||||
|
||||
const showSnackbar = (text: string, color: string = 'success') => {
|
||||
snackbar.value.text = text
|
||||
snackbar.value.color = color
|
||||
snackbar.value.show = true
|
||||
}
|
||||
|
||||
// 表单数据
|
||||
const formData = ref({
|
||||
scope: 'session',
|
||||
scope_id: '',
|
||||
top_k: 5,
|
||||
enable_rerank: true
|
||||
})
|
||||
|
||||
// 表格列
|
||||
const headers = [
|
||||
{ title: t('sessions.scope'), key: 'scope' },
|
||||
{ title: t('sessions.scopeId'), key: 'scope_id' },
|
||||
{ title: t('sessions.topK'), key: 'top_k' },
|
||||
{ title: t('sessions.enableRerank'), key: 'enable_rerank' },
|
||||
{ title: t('sessions.actions'), key: 'actions', sortable: false, align: 'end' }
|
||||
]
|
||||
|
||||
// 范围选项
|
||||
const scopeOptions = [
|
||||
{ title: t('sessions.scopeSession'), value: 'session' },
|
||||
{ title: t('sessions.scopePlatform'), value: 'platform' }
|
||||
]
|
||||
|
||||
// 加载会话配置
|
||||
const loadSessions = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await axios.get('/api/kb/session/config/list')
|
||||
if (response.data.status === 'ok') {
|
||||
// 过滤出使用当前知识库的配置
|
||||
sessions.value = response.data.data.items.filter((item: any) => {
|
||||
const kbIds = JSON.parse(item.kb_ids || '[]')
|
||||
return kbIds.includes(props.kbId)
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load session configs:', error)
|
||||
showSnackbar('加载会话配置失败', 'error')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 添加会话配置
|
||||
const addSession = async () => {
|
||||
const { valid } = await formRef.value.validate()
|
||||
if (!valid) return
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
const response = await axios.post('/api/kb/session/config', {
|
||||
scope: formData.value.scope,
|
||||
scope_id: formData.value.scope_id,
|
||||
kb_ids: [props.kbId],
|
||||
top_k: formData.value.top_k,
|
||||
enable_rerank: formData.value.enable_rerank
|
||||
})
|
||||
|
||||
if (response.data.status === 'ok') {
|
||||
showSnackbar(t('sessions.addSuccess'))
|
||||
closeAddDialog()
|
||||
await loadSessions()
|
||||
} else {
|
||||
showSnackbar(response.data.message || t('sessions.addFailed'), 'error')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to add session config:', error)
|
||||
showSnackbar(t('sessions.addFailed'), 'error')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 确认删除
|
||||
const confirmDelete = (session: any) => {
|
||||
deleteTarget.value = session
|
||||
showDeleteDialog.value = true
|
||||
}
|
||||
|
||||
// 删除会话配置
|
||||
const deleteSession = async () => {
|
||||
if (!deleteTarget.value) return
|
||||
|
||||
deleting.value = true
|
||||
try {
|
||||
const response = await axios.post('/api/kb/session/config/delete', {
|
||||
scope: deleteTarget.value.scope,
|
||||
scope_id: deleteTarget.value.scope_id
|
||||
})
|
||||
|
||||
if (response.data.status === 'ok') {
|
||||
showSnackbar(t('sessions.deleteSuccess'))
|
||||
showDeleteDialog.value = false
|
||||
await loadSessions()
|
||||
} else {
|
||||
showSnackbar(response.data.message || t('sessions.deleteFailed'), 'error')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete session config:', error)
|
||||
showSnackbar(t('sessions.deleteFailed'), 'error')
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭添加对话框
|
||||
const closeAddDialog = () => {
|
||||
showAddDialog.value = false
|
||||
formData.value = {
|
||||
scope: 'session',
|
||||
scope_id: '',
|
||||
top_k: 5,
|
||||
enable_rerank: true
|
||||
}
|
||||
formRef.value?.reset()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadSessions()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sessions-tab {
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,261 @@
|
||||
<template>
|
||||
<div class="settings-tab">
|
||||
<v-card elevation="2">
|
||||
<v-card-title class="pa-4">{{ t('settings.title') }}</v-card-title>
|
||||
<v-divider />
|
||||
|
||||
<v-card-text class="pa-6">
|
||||
<v-form ref="formRef">
|
||||
<!-- 基本设置 -->
|
||||
<h3 class="text-h6 mb-4">{{ t('settings.basic') }}</h3>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model.number="formData.chunk_size"
|
||||
:label="t('settings.chunkSize')"
|
||||
type="number"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model.number="formData.chunk_overlap"
|
||||
:label="t('settings.chunkOverlap')"
|
||||
type="number"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 检索设置 -->
|
||||
<h3 class="text-h6 mb-4 mt-6">{{ t('settings.retrieval') }}</h3>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field
|
||||
v-model.number="formData.top_k_dense"
|
||||
:label="t('settings.topKDense')"
|
||||
type="number"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field
|
||||
v-model.number="formData.top_k_sparse"
|
||||
:label="t('settings.topKSparse')"
|
||||
type="number"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field
|
||||
v-model.number="formData.top_m_final"
|
||||
:label="t('settings.topMFinal')"
|
||||
type="number"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-checkbox
|
||||
v-model="formData.enable_rerank"
|
||||
:label="t('settings.enableRerank')"
|
||||
color="primary"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 模型设置 -->
|
||||
<h3 class="text-h6 mb-4 mt-6">{{ t('settings.embeddingProvider') }}</h3>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="formData.embedding_provider_id"
|
||||
:items="embeddingProviders"
|
||||
:item-title="item => item.embedding_model || item.id"
|
||||
:item-value="'id'"
|
||||
:label="t('settings.embeddingProvider')"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
disabled
|
||||
hint="嵌入模型创建后不可修改"
|
||||
persistent-hint
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="formData.rerank_provider_id"
|
||||
:items="rerankProviders"
|
||||
:item-title="item => item.rerank_model || item.id"
|
||||
:item-value="'id'"
|
||||
:label="t('settings.rerankProvider')"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
clearable
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-alert type="info" variant="tonal" class="mt-4">
|
||||
{{ t('settings.tips') }}
|
||||
</v-alert>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
prepend-icon="mdi-content-save"
|
||||
@click="saveSettings"
|
||||
:loading="saving"
|
||||
>
|
||||
{{ t('settings.save') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
|
||||
<!-- 消息提示 -->
|
||||
<v-snackbar v-model="snackbar.show" :color="snackbar.color">
|
||||
{{ snackbar.text }}
|
||||
</v-snackbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import axios from 'axios'
|
||||
import { useModuleI18n } from '@/i18n/composables'
|
||||
|
||||
const { tm: t } = useModuleI18n('features/knowledge-base/detail')
|
||||
|
||||
const props = defineProps<{
|
||||
kb: any
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['updated'])
|
||||
|
||||
// 状态
|
||||
const saving = ref(false)
|
||||
const formRef = ref()
|
||||
const embeddingProviders = ref<any[]>([])
|
||||
const rerankProviders = ref<any[]>([])
|
||||
|
||||
const snackbar = ref({
|
||||
show: false,
|
||||
text: '',
|
||||
color: 'success'
|
||||
})
|
||||
|
||||
const showSnackbar = (text: string, color: string = 'success') => {
|
||||
snackbar.value.text = text
|
||||
snackbar.value.color = color
|
||||
snackbar.value.show = true
|
||||
}
|
||||
|
||||
// 表单数据
|
||||
const formData = ref({
|
||||
chunk_size: 512,
|
||||
chunk_overlap: 50,
|
||||
top_k_dense: 50,
|
||||
top_k_sparse: 50,
|
||||
top_m_final: 5,
|
||||
enable_rerank: true,
|
||||
embedding_provider_id: '',
|
||||
rerank_provider_id: ''
|
||||
})
|
||||
|
||||
// 监听 kb 变化,更新表单
|
||||
watch(() => props.kb, (kb) => {
|
||||
if (kb) {
|
||||
formData.value = {
|
||||
chunk_size: kb.chunk_size || 512,
|
||||
chunk_overlap: kb.chunk_overlap || 50,
|
||||
top_k_dense: kb.top_k_dense || 50,
|
||||
top_k_sparse: kb.top_k_sparse || 50,
|
||||
top_m_final: kb.top_m_final || 5,
|
||||
enable_rerank: kb.enable_rerank !== false,
|
||||
embedding_provider_id: kb.embedding_provider_id || '',
|
||||
rerank_provider_id: kb.rerank_provider_id || ''
|
||||
}
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// 加载提供商列表
|
||||
const loadProviders = async () => {
|
||||
try {
|
||||
const response = await axios.get('/api/config/provider/list', {
|
||||
params: { provider_type: 'embedding,rerank' }
|
||||
})
|
||||
if (response.data.status === 'ok') {
|
||||
embeddingProviders.value = response.data.data.filter(
|
||||
(p: any) => p.provider_type === 'embedding'
|
||||
)
|
||||
rerankProviders.value = response.data.data.filter(
|
||||
(p: any) => p.provider_type === 'rerank'
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load providers:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存设置
|
||||
const saveSettings = async () => {
|
||||
const { valid } = await formRef.value.validate()
|
||||
if (!valid) return
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
const response = await axios.post('/api/kb/update', {
|
||||
kb_id: props.kb.kb_id,
|
||||
chunk_size: formData.value.chunk_size,
|
||||
chunk_overlap: formData.value.chunk_overlap,
|
||||
top_k_dense: formData.value.top_k_dense,
|
||||
top_k_sparse: formData.value.top_k_sparse,
|
||||
top_m_final: formData.value.top_m_final,
|
||||
enable_rerank: formData.value.enable_rerank,
|
||||
rerank_provider_id: formData.value.rerank_provider_id
|
||||
})
|
||||
|
||||
if (response.data.status === 'ok') {
|
||||
showSnackbar(t('settings.saveSuccess'))
|
||||
emit('updated')
|
||||
} else {
|
||||
showSnackbar(response.data.message || t('settings.saveFailed'), 'error')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save settings:', error)
|
||||
showSnackbar(t('settings.saveFailed'), 'error')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadProviders()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.settings-tab {
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div class="kb-container">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="kb-fade" mode="out-in">
|
||||
<component :is="Component" :key="$route.fullPath" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 主容器组件,提供路由出口和页面切换动画
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.kb-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 页面切换动画 */
|
||||
.kb-fade-enter-active,
|
||||
.kb-fade-leave-active {
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
|
||||
.kb-fade-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
.kb-fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user