mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-18 10:00:40 +08:00
feat:删除v2版本的知识库前端代码;删除i18n相关文件
This commit is contained in:
@@ -1,49 +0,0 @@
|
||||
{
|
||||
"stats": {
|
||||
"totalDocuments": "Total Documents",
|
||||
"totalChunks": "Total Chunks",
|
||||
"totalMedia": "Media Resources",
|
||||
"totalSize": "Total Size"
|
||||
},
|
||||
"actions": {
|
||||
"upload": "Upload Document",
|
||||
"refresh": "Refresh",
|
||||
"view": "View",
|
||||
"delete": "Delete"
|
||||
},
|
||||
"empty": {
|
||||
"noDocuments": "No documents yet. Click the upload button to add documents"
|
||||
},
|
||||
"upload": {
|
||||
"title": "Upload Document",
|
||||
"dropzone": "Drag and drop files here or click to upload",
|
||||
"supportedFormats": "Supported formats: PDF, TXT, MD",
|
||||
"selectedFiles": "Selected files:",
|
||||
"cancel": "Cancel",
|
||||
"upload": "Start Upload",
|
||||
"uploading": "Uploading files..."
|
||||
},
|
||||
"detail": {
|
||||
"fileType": "File Type",
|
||||
"fileSize": "File Size",
|
||||
"chunks": "Document Chunks",
|
||||
"uploadedAt": "Upload Time",
|
||||
"noChunks": "No document chunks",
|
||||
"chunkIndex": "Chunk #{index}",
|
||||
"characters": "characters"
|
||||
},
|
||||
"delete": {
|
||||
"title": "Confirm Delete",
|
||||
"confirmText": "Are you sure you want to delete document \"{name}\"?",
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete"
|
||||
},
|
||||
"messages": {
|
||||
"loadError": "Failed to load document list",
|
||||
"uploadSuccess": "Successfully uploaded {count} document(s)",
|
||||
"uploadPartial": "Upload completed: {success} succeeded, {fail} failed",
|
||||
"deleteSuccess": "Document deleted successfully",
|
||||
"deleteFailed": "Delete failed",
|
||||
"deleteError": "Error deleting document"
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
{
|
||||
"list": {
|
||||
"title": "Native Knowledge Base",
|
||||
"subtitle": "AstrBot's built-in knowledge base management system",
|
||||
"create": "Create Knowledge Base",
|
||||
"sessionConfig": "Session Config",
|
||||
"refresh": "Refresh",
|
||||
"loading": "Loading...",
|
||||
"documents": "documents",
|
||||
"chunks": "chunks"
|
||||
},
|
||||
"empty": {
|
||||
"title": "No Knowledge Bases Yet",
|
||||
"subtitle": "Create your first knowledge base to get started",
|
||||
"create": "Create Knowledge Base"
|
||||
},
|
||||
"createDialog": {
|
||||
"title": "Create Knowledge Base",
|
||||
"nameLabel": "Knowledge Base Name",
|
||||
"namePlaceholder": "Enter knowledge base name",
|
||||
"descriptionLabel": "Description",
|
||||
"descriptionPlaceholder": "Brief description of the knowledge base...",
|
||||
"embeddingModelLabel": "Embedding Model",
|
||||
"rerankModelLabel": "Rerank Model",
|
||||
"providerInfo": "Provider ID: {id} | Dimensions: {dimensions}",
|
||||
"rerankProviderInfo": "Provider ID: {id}",
|
||||
"tips": "Tip: Once you choose an embedding model, do not modify the provider's model or vector dimensions, as it will severely affect recall rate.",
|
||||
"cancel": "Cancel",
|
||||
"create": "Create"
|
||||
},
|
||||
"editDialog": {
|
||||
"title": "Edit Knowledge Base",
|
||||
"save": "Save"
|
||||
},
|
||||
"deleteDialog": {
|
||||
"title": "Confirm Delete",
|
||||
"confirmText": "Are you sure you want to delete knowledge base \"{name}\"?",
|
||||
"warning": "This operation is irreversible. All knowledge base content will be permanently deleted.",
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete"
|
||||
},
|
||||
"detailDialog": {
|
||||
"tabs": {
|
||||
"documents": "Document Management",
|
||||
"search": "Search Test",
|
||||
"settings": "Knowledge Base Settings"
|
||||
}
|
||||
},
|
||||
"emojiPicker": {
|
||||
"title": "Select Emoji",
|
||||
"close": "Close",
|
||||
"categories": {
|
||||
"books": "Books and Documents",
|
||||
"emotions": "Emotions and Expressions",
|
||||
"objects": "Objects and Tools",
|
||||
"symbols": "Symbols and Marks"
|
||||
}
|
||||
},
|
||||
"sessionConfig": {
|
||||
"title": "Knowledge Base Session Configuration"
|
||||
},
|
||||
"messages": {
|
||||
"loadFailed": "Failed to load knowledge base list",
|
||||
"loadError": "Error loading knowledge base list",
|
||||
"nameRequired": "Please enter knowledge base name",
|
||||
"createSuccess": "Knowledge base created successfully",
|
||||
"updateSuccess": "Knowledge base updated successfully",
|
||||
"saveFailed": "Save failed",
|
||||
"saveError": "Error saving",
|
||||
"deleteSuccess": "Knowledge base deleted successfully",
|
||||
"deleteFailed": "Delete failed",
|
||||
"deleteError": "Error deleting"
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"search": {
|
||||
"queryLabel": "Search Query",
|
||||
"queryPlaceholder": "Enter search query...",
|
||||
"topKLabel": "Number of Results (Top-K)",
|
||||
"enableRerankLabel": "Enable Reranking",
|
||||
"search": "Search",
|
||||
"searching": "Searching...",
|
||||
"noResults": "No matching content found",
|
||||
"resultsTitle": "Found {count} result(s)",
|
||||
"searchTime": "Search time: {time}s",
|
||||
"relevance": "Relevance",
|
||||
"chunkInfo": "Chunk #{index} | {chars} characters"
|
||||
},
|
||||
"messages": {
|
||||
"queryRequired": "Please enter search query",
|
||||
"noResults": "No matching content found",
|
||||
"searchFailed": "Search failed",
|
||||
"searchError": "Error searching"
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
{
|
||||
"info": {
|
||||
"title": "What is Session Configuration?",
|
||||
"description": "Session configuration allows you to specify which knowledge bases to use for different platforms or sessions, enabling personalized knowledge enhancement.",
|
||||
"platformLevel": "Platform Level: Applies to the entire platform (e.g., all QQ sessions)",
|
||||
"sessionLevel": "Session Level: Applies to specific sessions (e.g., specific QQ group)"
|
||||
},
|
||||
"list": {
|
||||
"title": "Configuration List",
|
||||
"refresh": "Refresh",
|
||||
"add": "Add Configuration",
|
||||
"noKB": "No knowledge bases configured"
|
||||
},
|
||||
"scope": {
|
||||
"platform": "Platform Level",
|
||||
"session": "Session Level"
|
||||
},
|
||||
"dialog": {
|
||||
"addTitle": "Add Session Configuration",
|
||||
"editTitle": "Edit Session Configuration",
|
||||
"scopeLabel": "Configuration Scope",
|
||||
"platformIdLabel": "Platform ID",
|
||||
"platformIdPlaceholder": "e.g., qq, telegram, discord",
|
||||
"platformIdHint": "Platform identifier, such as qq, telegram, etc.",
|
||||
"sessionIdLabel": "Session ID",
|
||||
"sessionIdPlaceholder": "e.g., qq:group:123456",
|
||||
"sessionIdHint": "Session identifier, format: platform:type:ID",
|
||||
"platformTooltip": "Applies to all sessions on the platform",
|
||||
"sessionTooltip": "Applies only to the specified session",
|
||||
"kbLabel": "Select Knowledge Base",
|
||||
"kbPlaceholder": "Select one or more knowledge bases",
|
||||
"kbHint": "You can select multiple knowledge bases; they will be searched together during retrieval",
|
||||
"advancedSettings": "Advanced Settings (Optional)",
|
||||
"topKLabel": "Number of Results (Top-K)",
|
||||
"topKHint": "Number of results to return during retrieval",
|
||||
"enableRerankLabel": "Enable Reranking",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save"
|
||||
},
|
||||
"empty": {
|
||||
"noConfigs": "No configurations yet. Click the add button to create your first configuration",
|
||||
"createFirst": "Create First Configuration"
|
||||
},
|
||||
"delete": {
|
||||
"title": "Confirm Delete",
|
||||
"confirmText": "Are you sure you want to delete this configuration?",
|
||||
"warning": "After deletion, the platform or session will no longer use the specified knowledge bases.",
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete"
|
||||
},
|
||||
"actions": {
|
||||
"edit": "Edit",
|
||||
"delete": "Delete"
|
||||
},
|
||||
"messages": {
|
||||
"loadFailed": "Failed to load configuration list",
|
||||
"loadError": "Error loading configurations",
|
||||
"scopeIdRequired": "Please enter platform ID or session ID",
|
||||
"kbIdsRequired": "Please select at least one knowledge base",
|
||||
"createSuccess": "Configuration created successfully",
|
||||
"updateSuccess": "Configuration updated successfully",
|
||||
"saveFailed": "Save failed",
|
||||
"saveError": "Error saving configuration",
|
||||
"deleteSuccess": "Configuration deleted successfully",
|
||||
"deleteFailed": "Delete failed",
|
||||
"deleteError": "Error deleting configuration"
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"basic": {
|
||||
"title": "Basic Information",
|
||||
"nameLabel": "Knowledge Base Name",
|
||||
"namePlaceholder": "Enter knowledge base name",
|
||||
"descriptionLabel": "Description",
|
||||
"descriptionPlaceholder": "Brief description of the knowledge base..."
|
||||
},
|
||||
"models": {
|
||||
"title": "Model Configuration",
|
||||
"embeddingLabel": "Embedding Model",
|
||||
"embeddingHint": "Model for generating document vectors",
|
||||
"rerankLabel": "Rerank Model",
|
||||
"rerankHint": "Model for optimizing retrieval result ranking (optional)"
|
||||
},
|
||||
"chunking": {
|
||||
"title": "Chunking Parameters",
|
||||
"chunkSizeLabel": "Chunk Size",
|
||||
"chunkSizeHint": "Number of characters per document chunk (50-2000)",
|
||||
"chunkOverlapLabel": "Chunk Overlap",
|
||||
"chunkOverlapHint": "Number of overlapping characters between adjacent chunks (0-half of chunk size)"
|
||||
},
|
||||
"retrieval": {
|
||||
"title": "Retrieval Parameters",
|
||||
"topKDenseLabel": "Dense Retrieval Top-K",
|
||||
"topKDenseHint": "Number of results from vector retrieval",
|
||||
"topKSparseLabel": "Sparse Retrieval Top-K",
|
||||
"topKSparseHint": "Number of results from keyword retrieval",
|
||||
"topMFinalLabel": "Final Results Top-M",
|
||||
"topMFinalHint": "Number of final results after fusion",
|
||||
"enableRerankLabel": "Enable Reranking"
|
||||
},
|
||||
"actions": {
|
||||
"save": "Save Settings"
|
||||
},
|
||||
"messages": {
|
||||
"nameRequired": "Please enter knowledge base name",
|
||||
"embeddingRequired": "Please select embedding model",
|
||||
"saveSuccess": "Settings saved successfully",
|
||||
"saveFailed": "Save failed",
|
||||
"saveError": "Error saving settings",
|
||||
"loadProvidersError": "Failed to load model providers"
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
{
|
||||
"stats": {
|
||||
"totalDocuments": "总文档数",
|
||||
"totalChunks": "总块数",
|
||||
"totalMedia": "多媒体资源",
|
||||
"totalSize": "总大小"
|
||||
},
|
||||
"actions": {
|
||||
"upload": "上传文档",
|
||||
"refresh": "刷新",
|
||||
"view": "查看",
|
||||
"delete": "删除"
|
||||
},
|
||||
"empty": {
|
||||
"noDocuments": "暂无文档,点击上传按钮添加文档"
|
||||
},
|
||||
"upload": {
|
||||
"title": "上传文档",
|
||||
"dropzone": "拖放文件到这里或点击上传",
|
||||
"supportedFormats": "支持格式: PDF, TXT, MD",
|
||||
"selectedFiles": "已选择文件:",
|
||||
"cancel": "取消",
|
||||
"upload": "开始上传",
|
||||
"uploading": "正在上传文件..."
|
||||
},
|
||||
"detail": {
|
||||
"fileType": "文件类型",
|
||||
"fileSize": "文件大小",
|
||||
"chunks": "文档块",
|
||||
"uploadedAt": "上传时间",
|
||||
"noChunks": "暂无文档块",
|
||||
"chunkIndex": "块 #{index}",
|
||||
"characters": "字符"
|
||||
},
|
||||
"delete": {
|
||||
"title": "确认删除",
|
||||
"confirmText": "确定要删除文档 \"{name}\" 吗?",
|
||||
"cancel": "取消",
|
||||
"delete": "删除"
|
||||
},
|
||||
"messages": {
|
||||
"loadError": "加载文档列表失败",
|
||||
"uploadSuccess": "成功上传 {count} 个文档",
|
||||
"uploadPartial": "上传完成: {success} 成功, {fail} 失败",
|
||||
"deleteSuccess": "文档删除成功",
|
||||
"deleteFailed": "删除失败",
|
||||
"deleteError": "删除文档时出错"
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
{
|
||||
"list": {
|
||||
"title": "原生知识库",
|
||||
"subtitle": "AstrBot 内置的知识库管理系统",
|
||||
"create": "创建知识库",
|
||||
"sessionConfig": "会话配置",
|
||||
"refresh": "刷新",
|
||||
"loading": "加载中...",
|
||||
"documents": "文档",
|
||||
"chunks": "块"
|
||||
},
|
||||
"empty": {
|
||||
"title": "还没有知识库",
|
||||
"subtitle": "创建第一个知识库来开始使用",
|
||||
"create": "创建知识库"
|
||||
},
|
||||
"createDialog": {
|
||||
"title": "创建知识库",
|
||||
"nameLabel": "知识库名称",
|
||||
"namePlaceholder": "输入知识库名称",
|
||||
"descriptionLabel": "描述",
|
||||
"descriptionPlaceholder": "知识库的简短描述...",
|
||||
"embeddingModelLabel": "嵌入模型 (Embedding Model)",
|
||||
"rerankModelLabel": "重排序模型 (Rerank Model)",
|
||||
"providerInfo": "提供商 ID: {id} | 维度: {dimensions}",
|
||||
"rerankProviderInfo": "提供商 ID: {id}",
|
||||
"tips": "提示: 一旦选择了嵌入模型,请不要修改该提供商的模型或向量维度,否则将严重影响召回率。",
|
||||
"cancel": "取消",
|
||||
"create": "创建"
|
||||
},
|
||||
"editDialog": {
|
||||
"title": "编辑知识库",
|
||||
"save": "保存"
|
||||
},
|
||||
"deleteDialog": {
|
||||
"title": "确认删除",
|
||||
"confirmText": "确定要删除知识库 \"{name}\" 吗?",
|
||||
"warning": "此操作不可逆,所有知识库内容将被永久删除。",
|
||||
"cancel": "取消",
|
||||
"delete": "删除"
|
||||
},
|
||||
"detailDialog": {
|
||||
"tabs": {
|
||||
"documents": "文档管理",
|
||||
"search": "搜索测试",
|
||||
"settings": "知识库设置"
|
||||
}
|
||||
},
|
||||
"emojiPicker": {
|
||||
"title": "选择表情",
|
||||
"close": "关闭",
|
||||
"categories": {
|
||||
"books": "书籍和文档",
|
||||
"emotions": "表情和情感",
|
||||
"objects": "物品和工具",
|
||||
"symbols": "符号和标记"
|
||||
}
|
||||
},
|
||||
"sessionConfig": {
|
||||
"title": "知识库会话配置"
|
||||
},
|
||||
"messages": {
|
||||
"loadFailed": "加载知识库列表失败",
|
||||
"loadError": "加载知识库列表时出错",
|
||||
"nameRequired": "请输入知识库名称",
|
||||
"createSuccess": "知识库创建成功",
|
||||
"updateSuccess": "知识库更新成功",
|
||||
"saveFailed": "保存失败",
|
||||
"saveError": "保存时出错",
|
||||
"deleteSuccess": "知识库删除成功",
|
||||
"deleteFailed": "删除失败",
|
||||
"deleteError": "删除时出错"
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"search": {
|
||||
"queryLabel": "搜索查询",
|
||||
"queryPlaceholder": "输入搜索内容...",
|
||||
"topKLabel": "返回结果数 (Top-K)",
|
||||
"enableRerankLabel": "启用重排序",
|
||||
"search": "搜索",
|
||||
"searching": "正在搜索...",
|
||||
"noResults": "没有找到匹配的内容",
|
||||
"resultsTitle": "找到 {count} 条结果",
|
||||
"searchTime": "搜索用时: {time}s",
|
||||
"relevance": "相关度",
|
||||
"chunkInfo": "块 #{index} | {chars} 字符"
|
||||
},
|
||||
"messages": {
|
||||
"queryRequired": "请输入搜索内容",
|
||||
"noResults": "没有找到匹配的内容",
|
||||
"searchFailed": "搜索失败",
|
||||
"searchError": "搜索时出错"
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
{
|
||||
"info": {
|
||||
"title": "什么是会话配置?",
|
||||
"description": "会话配置允许您为不同的平台或会话指定使用的知识库,实现个性化的知识增强。",
|
||||
"platformLevel": "平台级别:对整个平台生效(如所有QQ会话)",
|
||||
"sessionLevel": "会话级别:对特定会话生效(如特定的QQ群)"
|
||||
},
|
||||
"list": {
|
||||
"title": "配置列表",
|
||||
"refresh": "刷新",
|
||||
"add": "新增配置",
|
||||
"noKB": "未配置知识库"
|
||||
},
|
||||
"scope": {
|
||||
"platform": "平台级别",
|
||||
"session": "会话级别"
|
||||
},
|
||||
"dialog": {
|
||||
"addTitle": "新增会话配置",
|
||||
"editTitle": "编辑会话配置",
|
||||
"scopeLabel": "配置范围",
|
||||
"platformIdLabel": "平台 ID",
|
||||
"platformIdPlaceholder": "例如: qq, telegram, discord",
|
||||
"platformIdHint": "平台标识符,如 qq、telegram 等",
|
||||
"sessionIdLabel": "会话 ID",
|
||||
"sessionIdPlaceholder": "例如: qq:group:123456",
|
||||
"sessionIdHint": "会话标识符,格式: 平台:类型:ID",
|
||||
"platformTooltip": "对整个平台的所有会话生效",
|
||||
"sessionTooltip": "仅对指定的会话生效",
|
||||
"kbLabel": "选择知识库",
|
||||
"kbPlaceholder": "选择一个或多个知识库",
|
||||
"kbHint": "可以选择多个知识库,检索时会合并搜索",
|
||||
"advancedSettings": "高级设置(可选)",
|
||||
"topKLabel": "返回结果数 (Top-K)",
|
||||
"topKHint": "检索时返回的结果数量",
|
||||
"enableRerankLabel": "启用重排序",
|
||||
"cancel": "取消",
|
||||
"save": "保存"
|
||||
},
|
||||
"empty": {
|
||||
"noConfigs": "暂无配置,点击新增按钮创建第一个配置",
|
||||
"createFirst": "创建第一个配置"
|
||||
},
|
||||
"delete": {
|
||||
"title": "确认删除",
|
||||
"confirmText": "确定要删除此配置吗?",
|
||||
"warning": "删除后,该平台或会话将不再使用指定的知识库。",
|
||||
"cancel": "取消",
|
||||
"delete": "删除"
|
||||
},
|
||||
"actions": {
|
||||
"edit": "编辑",
|
||||
"delete": "删除"
|
||||
},
|
||||
"messages": {
|
||||
"loadFailed": "加载配置列表失败",
|
||||
"loadError": "加载配置时出错",
|
||||
"scopeIdRequired": "请输入平台 ID 或会话 ID",
|
||||
"kbIdsRequired": "请至少选择一个知识库",
|
||||
"createSuccess": "配置创建成功",
|
||||
"updateSuccess": "配置更新成功",
|
||||
"saveFailed": "保存失败",
|
||||
"saveError": "保存配置时出错",
|
||||
"deleteSuccess": "配置删除成功",
|
||||
"deleteFailed": "删除失败",
|
||||
"deleteError": "删除配置时出错"
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"basic": {
|
||||
"title": "基本信息",
|
||||
"nameLabel": "知识库名称",
|
||||
"namePlaceholder": "输入知识库名称",
|
||||
"descriptionLabel": "描述",
|
||||
"descriptionPlaceholder": "知识库的简短描述..."
|
||||
},
|
||||
"models": {
|
||||
"title": "模型配置",
|
||||
"embeddingLabel": "嵌入模型",
|
||||
"embeddingHint": "用于生成文档向量的模型",
|
||||
"rerankLabel": "重排序模型",
|
||||
"rerankHint": "用于优化检索结果排序的模型(可选)"
|
||||
},
|
||||
"chunking": {
|
||||
"title": "分块参数",
|
||||
"chunkSizeLabel": "块大小",
|
||||
"chunkSizeHint": "每个文档块的字符数 (50-2000)",
|
||||
"chunkOverlapLabel": "块重叠",
|
||||
"chunkOverlapHint": "相邻块之间的重叠字符数 (0-块大小的一半)"
|
||||
},
|
||||
"retrieval": {
|
||||
"title": "检索参数",
|
||||
"topKDenseLabel": "密集检索 Top-K",
|
||||
"topKDenseHint": "向量检索返回的结果数",
|
||||
"topKSparseLabel": "稀疏检索 Top-K",
|
||||
"topKSparseHint": "关键词检索返回的结果数",
|
||||
"topMFinalLabel": "最终结果数 Top-M",
|
||||
"topMFinalHint": "融合后返回的最终结果数",
|
||||
"enableRerankLabel": "启用重排序"
|
||||
},
|
||||
"actions": {
|
||||
"save": "保存设置"
|
||||
},
|
||||
"messages": {
|
||||
"nameRequired": "请输入知识库名称",
|
||||
"embeddingRequired": "请选择嵌入模型",
|
||||
"saveSuccess": "设置保存成功",
|
||||
"saveFailed": "保存失败",
|
||||
"saveError": "保存设置时出错",
|
||||
"loadProvidersError": "加载模型提供商失败"
|
||||
}
|
||||
}
|
||||
@@ -1,686 +0,0 @@
|
||||
<template>
|
||||
<div class="kb-v2-container">
|
||||
<!-- 警告提示 -->
|
||||
<v-alert
|
||||
v-if="showCompatibilityWarning"
|
||||
type="warning"
|
||||
variant="tonal"
|
||||
closable
|
||||
class="mb-4"
|
||||
@click:close="showCompatibilityWarning = false"
|
||||
>
|
||||
<strong>兼容性提示:</strong> 此为 AstrBot 原生知识库系统。如您已安装知识库插件,建议不要同时使用两个知识库系统,以避免配置冲突。
|
||||
</v-alert>
|
||||
|
||||
<!-- 知识库列表视图 -->
|
||||
<div v-if="kbCollections.length > 0 || loading">
|
||||
<div class="d-flex align-center justify-space-between mb-4">
|
||||
<div>
|
||||
<h2>{{ tm('list.title') }}</h2>
|
||||
<small class="text-medium-emphasis">{{ tm('list.subtitle') }}</small>
|
||||
</div>
|
||||
<v-icon
|
||||
size="small"
|
||||
color="grey"
|
||||
@click="openUrl('https://astrbot.app/use/knowledge-base.html')"
|
||||
>
|
||||
mdi-information-outline
|
||||
</v-icon>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮组 -->
|
||||
<div class="action-buttons mb-4">
|
||||
<v-btn prepend-icon="mdi-plus" variant="tonal" color="primary" @click="showCreateDialog = true">
|
||||
{{ tm('list.create') }}
|
||||
</v-btn>
|
||||
<v-btn prepend-icon="mdi-account-cog" variant="tonal" color="info" @click="showSessionConfigDialog = true">
|
||||
{{ tm('list.sessionConfig') }}
|
||||
</v-btn>
|
||||
<v-btn prepend-icon="mdi-refresh" variant="tonal" @click="loadKnowledgeBases" :loading="loading">
|
||||
{{ tm('list.refresh') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<!-- 知识库网格 -->
|
||||
<div v-if="loading && kbCollections.length === 0" class="text-center py-8">
|
||||
<v-progress-circular indeterminate color="primary"></v-progress-circular>
|
||||
<p class="mt-4">{{ tm('list.loading') }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="kb-grid">
|
||||
<v-card
|
||||
v-for="(kb, index) in kbCollections"
|
||||
:key="index"
|
||||
class="kb-card"
|
||||
@click="openKnowledgeBase(kb)"
|
||||
hover
|
||||
>
|
||||
<div class="book-spine"></div>
|
||||
<div class="book-content">
|
||||
<div class="kb-emoji">{{ kb.emoji || '📚' }}</div>
|
||||
<div class="kb-name">{{ kb.kb_name }}</div>
|
||||
<div class="kb-stats">
|
||||
<div class="stat-item">
|
||||
<v-icon size="small">mdi-file-document</v-icon>
|
||||
<span>{{ kb.doc_count || 0 }} {{ tm('list.documents') }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<v-icon size="small">mdi-text-box</v-icon>
|
||||
<span>{{ kb.chunk_count || 0 }} {{ tm('list.chunks') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kb-actions">
|
||||
<v-btn
|
||||
icon
|
||||
variant="text"
|
||||
size="small"
|
||||
color="info"
|
||||
@click.stop="editKnowledgeBase(kb)"
|
||||
>
|
||||
<v-icon>mdi-pencil</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
icon
|
||||
variant="text"
|
||||
size="small"
|
||||
color="error"
|
||||
@click.stop="confirmDelete(kb)"
|
||||
>
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</v-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div
|
||||
v-else
|
||||
class="d-flex align-center justify-center flex-column"
|
||||
style="min-height: 400px"
|
||||
>
|
||||
<v-icon size="80" color="grey-lighten-1">mdi-book-open-variant</v-icon>
|
||||
<h2 class="mt-4">{{ tm('empty.title') }}</h2>
|
||||
<p class="text-medium-emphasis mt-2">{{ tm('empty.subtitle') }}</p>
|
||||
<v-btn
|
||||
class="mt-4"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
prepend-icon="mdi-plus"
|
||||
@click="showCreateDialog = true"
|
||||
>
|
||||
{{ tm('empty.create') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<!-- 创建/编辑知识库对话框 -->
|
||||
<v-dialog v-model="showCreateDialog" max-width="600px">
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center">
|
||||
<span class="text-h5">{{ editingKB ? tm('editDialog.title') : tm('createDialog.title') }}</span>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="plain" icon @click="showCreateDialog = false">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<div class="text-center mb-4">
|
||||
<span class="emoji-display" @click="showEmojiPicker = true">
|
||||
{{ newKB.emoji || '📚' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<v-form @submit.prevent="submitCreateForm">
|
||||
<v-text-field
|
||||
v-model="newKB.kb_name"
|
||||
:label="tm('createDialog.nameLabel')"
|
||||
:placeholder="tm('createDialog.namePlaceholder')"
|
||||
variant="outlined"
|
||||
required
|
||||
class="mb-2"
|
||||
></v-text-field>
|
||||
|
||||
<v-textarea
|
||||
v-model="newKB.description"
|
||||
:label="tm('createDialog.descriptionLabel')"
|
||||
:placeholder="tm('createDialog.descriptionPlaceholder')"
|
||||
variant="outlined"
|
||||
rows="3"
|
||||
class="mb-2"
|
||||
></v-textarea>
|
||||
|
||||
<v-select
|
||||
v-model="newKB.embedding_provider_id"
|
||||
:items="embeddingProviderConfigs"
|
||||
:item-props="embeddingModelProps"
|
||||
:label="tm('createDialog.embeddingModelLabel')"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
class="mb-2"
|
||||
></v-select>
|
||||
|
||||
<v-select
|
||||
v-model="newKB.rerank_provider_id"
|
||||
:items="rerankProviderConfigs"
|
||||
:item-props="rerankModelProps"
|
||||
:label="tm('createDialog.rerankModelLabel')"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
clearable
|
||||
></v-select>
|
||||
|
||||
<small class="text-medium-emphasis">{{ tm('createDialog.tips') }}</small>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="grey" variant="text" @click="showCreateDialog = false">
|
||||
{{ tm('createDialog.cancel') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="text"
|
||||
@click="submitCreateForm"
|
||||
:loading="creating"
|
||||
>
|
||||
{{ editingKB ? tm('editDialog.save') : tm('createDialog.create') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- Emoji 选择器对话框 -->
|
||||
<v-dialog v-model="showEmojiPicker" max-width="500px">
|
||||
<v-card>
|
||||
<v-card-title>{{ tm('emojiPicker.title') }}</v-card-title>
|
||||
<v-card-text>
|
||||
<div class="emoji-picker">
|
||||
<div v-for="(category, catIndex) in emojiCategories" :key="catIndex" class="mb-4">
|
||||
<div class="text-subtitle-2 mb-2">{{ tm(`emojiPicker.categories.${category.key}`) }}</div>
|
||||
<div class="emoji-grid">
|
||||
<div
|
||||
v-for="(emoji, emojiIndex) in category.emojis"
|
||||
:key="emojiIndex"
|
||||
class="emoji-item"
|
||||
@click="selectEmoji(emoji)"
|
||||
>
|
||||
{{ emoji }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="primary" variant="text" @click="showEmojiPicker = false">
|
||||
{{ tm('emojiPicker.close') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 知识库详情对话框 -->
|
||||
<v-dialog v-model="showDetailDialog" max-width="1200px" scrollable>
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center">
|
||||
<div class="emoji-sm me-2">{{ currentKB.emoji || '📚' }}</div>
|
||||
<span>{{ currentKB.kb_name }}</span>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="plain" icon @click="showDetailDialog = false">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-subtitle v-if="currentKB.description">
|
||||
{{ currentKB.description }}
|
||||
</v-card-subtitle>
|
||||
|
||||
<v-card-text>
|
||||
<v-tabs v-model="activeTab">
|
||||
<v-tab value="documents">{{ tm('detailDialog.tabs.documents') }}</v-tab>
|
||||
<v-tab value="search">{{ tm('detailDialog.tabs.search') }}</v-tab>
|
||||
<v-tab value="settings">{{ tm('detailDialog.tabs.settings') }}</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<v-window v-model="activeTab" class="mt-4">
|
||||
<v-window-item value="documents">
|
||||
<DocumentListPanel v-if="currentKB.kb_id" :kb="currentKB" />
|
||||
</v-window-item>
|
||||
|
||||
<v-window-item value="search">
|
||||
<SearchPanel v-if="currentKB.kb_id" :kb="currentKB" />
|
||||
</v-window-item>
|
||||
|
||||
<v-window-item value="settings">
|
||||
<KBSettingsPanel
|
||||
v-if="currentKB.kb_id"
|
||||
:kb="currentKB"
|
||||
@updated="onKBUpdated"
|
||||
/>
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 删除确认对话框 -->
|
||||
<v-dialog v-model="showDeleteDialog" max-width="450px">
|
||||
<v-card>
|
||||
<v-card-title class="text-h5">{{ tm('deleteDialog.title') }}</v-card-title>
|
||||
<v-card-text>
|
||||
<p>{{ tm('deleteDialog.confirmText', { name: deleteTarget.kb_name }) }}</p>
|
||||
<p class="text-error mt-2">{{ tm('deleteDialog.warning') }}</p>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="grey" variant="text" @click="showDeleteDialog = false">
|
||||
{{ tm('deleteDialog.cancel') }}
|
||||
</v-btn>
|
||||
<v-btn color="error" variant="text" @click="deleteKnowledgeBase" :loading="deleting">
|
||||
{{ tm('deleteDialog.delete') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 会话配置对话框 -->
|
||||
<v-dialog v-model="showSessionConfigDialog" max-width="900px" scrollable>
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center">
|
||||
<span>{{ tm('sessionConfig.title') }}</span>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="plain" icon @click="showSessionConfigDialog = false">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<SessionConfigPanel />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 消息提示 -->
|
||||
<v-snackbar v-model="snackbar.show" :color="snackbar.color" :timeout="3000">
|
||||
{{ snackbar.text }}
|
||||
</v-snackbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import { useModuleI18n } from '@/i18n/composables';
|
||||
import DocumentListPanel from './components/DocumentListPanel.vue';
|
||||
import SearchPanel from './components/SearchPanel.vue';
|
||||
import KBSettingsPanel from './components/KBSettingsPanel.vue';
|
||||
import SessionConfigPanel from './components/SessionConfigPanel.vue';
|
||||
|
||||
export default {
|
||||
name: 'KnowledgeBaseV2',
|
||||
components: {
|
||||
DocumentListPanel,
|
||||
SearchPanel,
|
||||
KBSettingsPanel,
|
||||
SessionConfigPanel,
|
||||
},
|
||||
setup() {
|
||||
const { tm } = useModuleI18n('features/alkaid/knowledge-base-v2/index');
|
||||
return { tm };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showCompatibilityWarning: true,
|
||||
kbCollections: [],
|
||||
loading: false,
|
||||
creating: false,
|
||||
deleting: false,
|
||||
showCreateDialog: false,
|
||||
showEmojiPicker: false,
|
||||
showDetailDialog: false,
|
||||
showDeleteDialog: false,
|
||||
showSessionConfigDialog: false,
|
||||
editingKB: null,
|
||||
currentKB: {},
|
||||
deleteTarget: {},
|
||||
activeTab: 'documents',
|
||||
newKB: {
|
||||
kb_name: '',
|
||||
description: '',
|
||||
emoji: '📚',
|
||||
embedding_provider_id: null,
|
||||
rerank_provider_id: null,
|
||||
},
|
||||
embeddingProviderConfigs: [],
|
||||
rerankProviderConfigs: [],
|
||||
emojiCategories: [
|
||||
{
|
||||
key: 'books',
|
||||
emojis: ['📚', '📖', '📕', '📗', '📘', '📙', '📓', '📔', '📒', '📑', '🗂️', '📂', '📁', '🗃️', '🗄️'],
|
||||
},
|
||||
{
|
||||
key: 'emotions',
|
||||
emojis: ['😀', '😃', '😄', '😁', '😆', '😅', '🤣', '😂', '🙂', '🙃', '😉', '😊', '😇', '🥰', '😍'],
|
||||
},
|
||||
{
|
||||
key: 'objects',
|
||||
emojis: ['💡', '🔬', '🔭', '🗿', '🏆', '🎯', '🎓', '🔑', '🔒', '🔓', '🔔', '🔕', '🔨', '🛠️', '⚙️'],
|
||||
},
|
||||
{
|
||||
key: 'symbols',
|
||||
emojis: ['❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', '🤍', '🤎', '⭐', '🌟', '✨', '💫', '⚡', '🔥'],
|
||||
},
|
||||
],
|
||||
snackbar: {
|
||||
show: false,
|
||||
text: '',
|
||||
color: 'success',
|
||||
},
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.loadKnowledgeBases();
|
||||
this.loadProviderConfigs();
|
||||
},
|
||||
methods: {
|
||||
embeddingModelProps(providerConfig) {
|
||||
return {
|
||||
title: providerConfig.embedding_model || providerConfig.id,
|
||||
subtitle: this.tm('createDialog.providerInfo', {
|
||||
id: providerConfig.id,
|
||||
dimensions: providerConfig.embedding_dimensions || 'N/A',
|
||||
}),
|
||||
};
|
||||
},
|
||||
rerankModelProps(providerConfig) {
|
||||
return {
|
||||
title: providerConfig.rerank_model || providerConfig.id,
|
||||
subtitle: this.tm('createDialog.rerankProviderInfo', {
|
||||
id: providerConfig.id,
|
||||
}),
|
||||
};
|
||||
},
|
||||
async loadKnowledgeBases() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await axios.get('/api/kb/list');
|
||||
if (response.data.status === 'ok') {
|
||||
this.kbCollections = response.data.data.items || [];
|
||||
} else {
|
||||
this.showSnackbar(response.data.message || this.tm('messages.loadFailed'), 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading knowledge bases:', error);
|
||||
this.showSnackbar(this.tm('messages.loadError'), 'error');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
async loadProviderConfigs() {
|
||||
try {
|
||||
const response = await axios.get('/api/config/provider/list', {
|
||||
params: { provider_type: 'embedding,rerank' },
|
||||
});
|
||||
if (response.data.status === 'ok') {
|
||||
this.embeddingProviderConfigs = response.data.data.filter(
|
||||
(p) => p.provider_type === 'embedding'
|
||||
);
|
||||
this.rerankProviderConfigs = response.data.data.filter(
|
||||
(p) => p.provider_type === 'rerank'
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading provider configs:', error);
|
||||
}
|
||||
},
|
||||
openKnowledgeBase(kb) {
|
||||
this.currentKB = kb;
|
||||
this.activeTab = 'documents';
|
||||
this.showDetailDialog = true;
|
||||
},
|
||||
editKnowledgeBase(kb) {
|
||||
this.editingKB = kb;
|
||||
this.newKB = {
|
||||
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,
|
||||
};
|
||||
this.showCreateDialog = true;
|
||||
},
|
||||
async submitCreateForm() {
|
||||
if (!this.newKB.kb_name) {
|
||||
this.showSnackbar(this.tm('messages.nameRequired'), 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
this.creating = true;
|
||||
try {
|
||||
const payload = {
|
||||
kb_name: this.newKB.kb_name,
|
||||
description: this.newKB.description,
|
||||
emoji: this.newKB.emoji || '📚',
|
||||
embedding_provider_id: this.newKB.embedding_provider_id,
|
||||
rerank_provider_id: this.newKB.rerank_provider_id,
|
||||
};
|
||||
|
||||
let response;
|
||||
if (this.editingKB) {
|
||||
response = await axios.post('/api/kb/update', {
|
||||
kb_id: this.editingKB.kb_id,
|
||||
...payload,
|
||||
});
|
||||
} else {
|
||||
response = await axios.post('/api/kb/create', payload);
|
||||
}
|
||||
|
||||
if (response.data.status === 'ok') {
|
||||
this.showSnackbar(
|
||||
this.editingKB ? this.tm('messages.updateSuccess') : this.tm('messages.createSuccess')
|
||||
);
|
||||
this.showCreateDialog = false;
|
||||
this.resetNewKB();
|
||||
this.loadKnowledgeBases();
|
||||
} else {
|
||||
this.showSnackbar(response.data.message || this.tm('messages.saveFailed'), 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving knowledge base:', error);
|
||||
this.showSnackbar(this.tm('messages.saveError'), 'error');
|
||||
} finally {
|
||||
this.creating = false;
|
||||
}
|
||||
},
|
||||
confirmDelete(kb) {
|
||||
this.deleteTarget = kb;
|
||||
this.showDeleteDialog = true;
|
||||
},
|
||||
async deleteKnowledgeBase() {
|
||||
if (!this.deleteTarget.kb_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.deleting = true;
|
||||
try {
|
||||
const response = await axios.post('/api/kb/delete', {
|
||||
kb_id: this.deleteTarget.kb_id,
|
||||
});
|
||||
|
||||
if (response.data.status === 'ok') {
|
||||
this.showSnackbar(this.tm('messages.deleteSuccess'));
|
||||
this.showDeleteDialog = false;
|
||||
this.loadKnowledgeBases();
|
||||
} else {
|
||||
this.showSnackbar(response.data.message || this.tm('messages.deleteFailed'), 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting knowledge base:', error);
|
||||
this.showSnackbar(this.tm('messages.deleteError'), 'error');
|
||||
} finally {
|
||||
this.deleting = false;
|
||||
}
|
||||
},
|
||||
selectEmoji(emoji) {
|
||||
this.newKB.emoji = emoji;
|
||||
this.showEmojiPicker = false;
|
||||
},
|
||||
resetNewKB() {
|
||||
this.editingKB = null;
|
||||
this.newKB = {
|
||||
kb_name: '',
|
||||
description: '',
|
||||
emoji: '📚',
|
||||
embedding_provider_id: null,
|
||||
rerank_provider_id: null,
|
||||
};
|
||||
},
|
||||
onKBUpdated() {
|
||||
this.loadKnowledgeBases();
|
||||
this.showDetailDialog = false;
|
||||
},
|
||||
openUrl(url) {
|
||||
window.open(url, '_blank');
|
||||
},
|
||||
showSnackbar(text, color = 'success') {
|
||||
this.snackbar.text = text;
|
||||
this.snackbar.color = color;
|
||||
this.snackbar.show = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.kb-v2-container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.kb-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||
gap: 24px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.kb-card {
|
||||
height: 280px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.kb-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15) !important;
|
||||
}
|
||||
|
||||
.book-spine {
|
||||
width: 12px;
|
||||
background: linear-gradient(180deg, #5c6bc0 0%, #3f51b5 100%);
|
||||
height: 100%;
|
||||
border-radius: 2px 0 0 2px;
|
||||
}
|
||||
|
||||
.book-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
background: linear-gradient(145deg, #f5f7fa 0%, #e4e8f0 100%);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.kb-emoji {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.kb-name {
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
margin-bottom: 12px;
|
||||
text-align: center;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.kb-stats {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.kb-actions {
|
||||
position: absolute;
|
||||
bottom: 12px;
|
||||
right: 12px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.kb-card:hover .kb-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.emoji-display {
|
||||
font-size: 64px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.emoji-display:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.emoji-sm {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.emoji-picker {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.emoji-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(8, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.emoji-item {
|
||||
font-size: 28px;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.emoji-item:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
</style>
|
||||
@@ -1,639 +0,0 @@
|
||||
<template>
|
||||
<div class="document-list-panel">
|
||||
<!-- 文档统计 -->
|
||||
<v-row class="mb-4">
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-card variant="outlined">
|
||||
<v-card-text class="text-center">
|
||||
<div class="text-h5 primary--text">{{ documentStats.total }}</div>
|
||||
<div class="text-caption">{{ tm('stats.totalDocuments') }}</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-card variant="outlined">
|
||||
<v-card-text class="text-center">
|
||||
<div class="text-h5 success--text">{{ documentStats.chunks }}</div>
|
||||
<div class="text-caption">{{ tm('stats.totalChunks') }}</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-card variant="outlined">
|
||||
<v-card-text class="text-center">
|
||||
<div class="text-h5 warning--text">{{ documentStats.media }}</div>
|
||||
<div class="text-caption">{{ tm('stats.totalMedia') }}</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-card variant="outlined">
|
||||
<v-card-text class="text-center">
|
||||
<div class="text-h5 info--text">{{ formatFileSize(documentStats.size) }}</div>
|
||||
<div class="text-caption">{{ tm('stats.totalSize') }}</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 操作栏 -->
|
||||
<div class="d-flex justify-space-between align-center mb-4">
|
||||
<v-btn prepend-icon="mdi-upload" variant="tonal" color="primary" @click="showUploadDialog = true">
|
||||
{{ tm('actions.upload') }}
|
||||
</v-btn>
|
||||
<v-btn prepend-icon="mdi-refresh" variant="text" @click="loadDocuments" :loading="loading">
|
||||
{{ tm('actions.refresh') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<!-- 文档列表 -->
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="documents"
|
||||
:loading="loading"
|
||||
items-per-page="10"
|
||||
class="elevation-1"
|
||||
>
|
||||
<template v-slot:item.doc_name="{ item }">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon :icon="getFileIcon(item.file_type)" class="mr-2"></v-icon>
|
||||
<span>{{ item.doc_name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.file_size="{ item }">
|
||||
{{ formatFileSize(item.file_size) }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.created_at="{ item }">
|
||||
{{ formatDate(item.created_at) }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-btn variant="text" size="small" prepend-icon="mdi-eye" @click="viewDocument(item)">
|
||||
{{ tm('actions.view') }}
|
||||
</v-btn>
|
||||
<v-btn variant="text" size="small" color="error" prepend-icon="mdi-delete" @click="confirmDelete(item)">
|
||||
{{ tm('actions.delete') }}
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<template v-slot:no-data>
|
||||
<div class="text-center py-8">
|
||||
<v-icon size="64" color="grey-lighten-2">mdi-file-document-outline</v-icon>
|
||||
<p class="text-medium-emphasis mt-4">{{ tm('empty.noDocuments') }}</p>
|
||||
</div>
|
||||
</template>
|
||||
</v-data-table>
|
||||
|
||||
<!-- 文档上传对话框 -->
|
||||
<v-dialog v-model="showUploadDialog" max-width="600px">
|
||||
<v-card>
|
||||
<v-card-title>{{ tm('upload.title') }}</v-card-title>
|
||||
<v-card-text>
|
||||
<div class="upload-zone" @click="triggerFileInput" @dragover.prevent @drop.prevent="onFileDrop">
|
||||
<input type="file" ref="fileInput" style="display: none" @change="onFileSelected" multiple />
|
||||
<v-icon size="64" color="primary">mdi-cloud-upload</v-icon>
|
||||
<p class="mt-4">{{ tm('upload.dropzone') }}</p>
|
||||
<p class="text-caption text-medium-emphasis">{{ tm('upload.supportedFormats') }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedFiles.length > 0" class="mt-4">
|
||||
<h4>{{ tm('upload.selectedFiles') }}</h4>
|
||||
<v-list density="compact">
|
||||
<v-list-item v-for="(file, index) in selectedFiles" :key="index">
|
||||
<template v-slot:prepend>
|
||||
<v-icon :icon="getFileIcon(getFileExtension(file.name))"></v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ file.name }}</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ formatFileSize(file.size) }}</v-list-item-subtitle>
|
||||
<template v-slot:append>
|
||||
<v-btn icon variant="text" size="small" @click="removeFile(index)">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</div>
|
||||
|
||||
<div v-if="uploading" class="mt-4">
|
||||
<v-progress-linear indeterminate color="primary"></v-progress-linear>
|
||||
<p class="text-center mt-2">{{ tm('upload.uploading') }}</p>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn @click="showUploadDialog = false">{{ tm('upload.cancel') }}</v-btn>
|
||||
<v-btn color="primary" @click="uploadFiles" :loading="uploading" :disabled="selectedFiles.length === 0">
|
||||
{{ tm('upload.upload') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 文档详情对话框 -->
|
||||
<v-dialog v-model="showDetailDialog" max-width="800px" scrollable>
|
||||
<v-card v-if="currentDocument">
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon :icon="getFileIcon(currentDocument.file_type)" class="mr-2"></v-icon>
|
||||
<span>{{ currentDocument.doc_name }}</span>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="plain" icon @click="showDetailDialog = false">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-tabs v-model="detailTab">
|
||||
<v-tab value="info">{{ tm('detail.tabs.info') || '基本信息' }}</v-tab>
|
||||
<v-tab value="chunks">{{ tm('detail.tabs.chunks') || '文档块' }} ({{ currentDocument.chunk_count || 0 }})</v-tab>
|
||||
<v-tab value="media">{{ tm('detail.tabs.media') || '多媒体' }} ({{ currentDocument.media_count || 0 }})</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<v-card-text>
|
||||
<v-window v-model="detailTab">
|
||||
<!-- 基本信息 Tab -->
|
||||
<v-window-item value="info">
|
||||
<v-row class="mb-4">
|
||||
<v-col cols="6">
|
||||
<div class="text-caption text-medium-emphasis">{{ tm('detail.fileType') }}</div>
|
||||
<div>{{ currentDocument.file_type.toUpperCase() }}</div>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<div class="text-caption text-medium-emphasis">{{ tm('detail.fileSize') }}</div>
|
||||
<div>{{ formatFileSize(currentDocument.file_size) }}</div>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<div class="text-caption text-medium-emphasis">{{ tm('detail.chunks') }}</div>
|
||||
<div>{{ currentDocument.chunk_count }}</div>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<div class="text-caption text-medium-emphasis">{{ tm('detail.uploadedAt') }}</div>
|
||||
<div>{{ formatDate(currentDocument.created_at) }}</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-window-item>
|
||||
|
||||
<!-- 文档块 Tab -->
|
||||
<v-window-item value="chunks">
|
||||
<div v-if="loadingChunks" class="text-center py-4">
|
||||
<v-progress-circular indeterminate color="primary"></v-progress-circular>
|
||||
</div>
|
||||
<div v-else-if="chunks.length === 0" class="text-center py-4 text-medium-emphasis">
|
||||
{{ tm('detail.noChunks') }}
|
||||
</div>
|
||||
<div v-else>
|
||||
<v-card
|
||||
v-for="(chunk, index) in chunks"
|
||||
:key="index"
|
||||
variant="outlined"
|
||||
class="mb-2 chunk-card"
|
||||
>
|
||||
<v-card-text>
|
||||
<div class="d-flex justify-space-between align-center mb-2">
|
||||
<span class="text-caption">{{ tm('detail.chunkIndex', { index: chunk.chunk_index }) }}</span>
|
||||
<div class="d-flex align-center">
|
||||
<span class="text-caption mr-2">{{ chunk.char_count }} {{ tm('detail.characters') }}</span>
|
||||
<v-btn
|
||||
icon
|
||||
variant="text"
|
||||
size="small"
|
||||
color="error"
|
||||
@click.stop="confirmDeleteChunk(chunk)"
|
||||
>
|
||||
<v-icon size="small">mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chunk-content">{{ truncateText(chunk.content, 200) }}</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</v-window-item>
|
||||
|
||||
<!-- 多媒体 Tab -->
|
||||
<v-window-item value="media">
|
||||
<div v-if="loadingMedia" class="text-center py-4">
|
||||
<v-progress-circular indeterminate color="primary"></v-progress-circular>
|
||||
</div>
|
||||
<div v-else-if="mediaList.length === 0" class="text-center py-4 text-medium-emphasis">
|
||||
{{ tm('detail.noMedia') || '暂无多媒体资源' }}
|
||||
</div>
|
||||
<v-list v-else density="compact">
|
||||
<v-list-item v-for="media in mediaList" :key="media.media_id">
|
||||
<template v-slot:prepend>
|
||||
<v-icon :icon="getMediaIcon(media.media_type)"></v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ media.file_name }}</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
{{ media.mime_type }} | {{ formatFileSize(media.file_size) }}
|
||||
</v-list-item-subtitle>
|
||||
<template v-slot:append>
|
||||
<v-btn icon size="small" variant="text" color="error" @click="confirmDeleteMedia(media)">
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 删除确认对话框 -->
|
||||
<v-dialog v-model="showDeleteDialog" max-width="400px">
|
||||
<v-card>
|
||||
<v-card-title>{{ tm('delete.title') }}</v-card-title>
|
||||
<v-card-text>
|
||||
<p>{{ tm('delete.confirmText', { name: deleteTarget.doc_name }) }}</p>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn @click="showDeleteDialog = false">{{ tm('delete.cancel') }}</v-btn>
|
||||
<v-btn color="error" @click="deleteDocument" :loading="deleting">{{ tm('delete.delete') }}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 删除块确认对话框 -->
|
||||
<v-dialog v-model="showDeleteChunkDialog" max-width="400px">
|
||||
<v-card>
|
||||
<v-card-title>{{ tm('delete.chunkTitle') || '删除文档块' }}</v-card-title>
|
||||
<v-card-text>
|
||||
<p>{{ tm('delete.chunkConfirmText', { index: deleteChunkTarget.chunk_index }) || `确定要删除块 #${deleteChunkTarget.chunk_index} 吗?` }}</p>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn @click="showDeleteChunkDialog = false">{{ tm('delete.cancel') }}</v-btn>
|
||||
<v-btn color="error" @click="deleteChunk" :loading="deletingChunk">{{ tm('delete.delete') }}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 删除多媒体确认对话框 -->
|
||||
<v-dialog v-model="showDeleteMediaDialog" max-width="400px">
|
||||
<v-card>
|
||||
<v-card-title>{{ tm('delete.mediaTitle') || '删除多媒体资源' }}</v-card-title>
|
||||
<v-card-text>
|
||||
<p>{{ tm('delete.mediaConfirmText', { name: deleteMediaTarget.file_name }) || `确定要删除 "${deleteMediaTarget.file_name}" 吗?` }}</p>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn @click="showDeleteMediaDialog = false">{{ tm('delete.cancel') }}</v-btn>
|
||||
<v-btn color="error" @click="deleteMedia" :loading="deletingMedia">{{ tm('delete.delete') }}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 消息提示 -->
|
||||
<v-snackbar v-model="snackbar.show" :color="snackbar.color" :timeout="3000">
|
||||
{{ snackbar.text }}
|
||||
</v-snackbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import { useModuleI18n } from '@/i18n/composables';
|
||||
|
||||
export default {
|
||||
name: 'DocumentListPanel',
|
||||
props: {
|
||||
kb: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { tm } = useModuleI18n('features/alkaid/knowledge-base-v2/documents');
|
||||
return { tm };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
documents: [],
|
||||
chunks: [],
|
||||
mediaList: [],
|
||||
loading: false,
|
||||
loadingChunks: false,
|
||||
loadingMedia: false,
|
||||
uploading: false,
|
||||
deleting: false,
|
||||
deletingChunk: false,
|
||||
deletingMedia: false,
|
||||
showUploadDialog: false,
|
||||
showDetailDialog: false,
|
||||
showDeleteDialog: false,
|
||||
showDeleteChunkDialog: false,
|
||||
showDeleteMediaDialog: false,
|
||||
selectedFiles: [],
|
||||
currentDocument: null,
|
||||
deleteTarget: {},
|
||||
deleteChunkTarget: {},
|
||||
deleteMediaTarget: {},
|
||||
detailTab: 'info',
|
||||
documentStats: {
|
||||
total: 0,
|
||||
chunks: 0,
|
||||
media: 0,
|
||||
size: 0,
|
||||
},
|
||||
headers: [
|
||||
{ title: '文件名', key: 'doc_name', width: '35%' },
|
||||
{ title: '类型', key: 'file_type', width: '10%' },
|
||||
{ title: '大小', key: 'file_size', width: '15%' },
|
||||
{ title: '块数量', key: 'chunk_count', width: '10%' },
|
||||
{ title: '上传时间', key: 'created_at', width: '15%' },
|
||||
{ title: '操作', key: 'actions', width: '15%', sortable: false },
|
||||
],
|
||||
snackbar: {
|
||||
show: false,
|
||||
text: '',
|
||||
color: 'success',
|
||||
},
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.loadDocuments();
|
||||
},
|
||||
methods: {
|
||||
async loadDocuments() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await axios.get('/api/kb/document/list', {
|
||||
params: { kb_id: this.kb.kb_id },
|
||||
});
|
||||
if (response.data.status === 'ok') {
|
||||
this.documents = response.data.data.items || [];
|
||||
this.updateStats();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading documents:', error);
|
||||
this.showSnackbar(this.tm('messages.loadError'), 'error');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
updateStats() {
|
||||
this.documentStats = {
|
||||
total: this.documents.length,
|
||||
chunks: this.documents.reduce((sum, doc) => sum + (doc.chunk_count || 0), 0),
|
||||
media: this.documents.reduce((sum, doc) => sum + (doc.media_count || 0), 0),
|
||||
size: this.documents.reduce((sum, doc) => sum + (doc.file_size || 0), 0),
|
||||
};
|
||||
},
|
||||
getFileIcon(fileType) {
|
||||
const iconMap = {
|
||||
pdf: 'mdi-file-pdf-box',
|
||||
docx: 'mdi-file-word-box',
|
||||
doc: 'mdi-file-word-box',
|
||||
txt: 'mdi-file-document-outline',
|
||||
md: 'mdi-language-markdown',
|
||||
markdown: 'mdi-language-markdown',
|
||||
};
|
||||
return iconMap[fileType?.toLowerCase()] || 'mdi-file-outline';
|
||||
},
|
||||
getFileExtension(filename) {
|
||||
return filename.split('.').pop().toLowerCase();
|
||||
},
|
||||
formatFileSize(bytes) {
|
||||
if (!bytes || bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
},
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '';
|
||||
return new Date(dateString).toLocaleString();
|
||||
},
|
||||
truncateText(text, maxLength) {
|
||||
if (!text || text.length <= maxLength) return text;
|
||||
return text.substring(0, maxLength) + '...';
|
||||
},
|
||||
triggerFileInput() {
|
||||
this.$refs.fileInput.click();
|
||||
},
|
||||
onFileSelected(event) {
|
||||
const files = Array.from(event.target.files);
|
||||
this.selectedFiles = [...this.selectedFiles, ...files];
|
||||
},
|
||||
onFileDrop(event) {
|
||||
const files = Array.from(event.dataTransfer.files);
|
||||
this.selectedFiles = [...this.selectedFiles, ...files];
|
||||
},
|
||||
removeFile(index) {
|
||||
this.selectedFiles.splice(index, 1);
|
||||
},
|
||||
async uploadFiles() {
|
||||
if (this.selectedFiles.length === 0) return;
|
||||
|
||||
this.uploading = true;
|
||||
let successCount = 0;
|
||||
let failCount = 0;
|
||||
|
||||
for (const file of this.selectedFiles) {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('kb_id', this.kb.kb_id);
|
||||
|
||||
const response = await axios.post('/api/kb/document/upload', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
|
||||
if (response.data.status === 'ok') {
|
||||
successCount++;
|
||||
} else {
|
||||
failCount++;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error uploading file:', error);
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
|
||||
this.uploading = false;
|
||||
this.selectedFiles = [];
|
||||
this.showUploadDialog = false;
|
||||
|
||||
if (failCount === 0) {
|
||||
this.showSnackbar(this.tm('messages.uploadSuccess', { count: successCount }));
|
||||
} else {
|
||||
this.showSnackbar(
|
||||
this.tm('messages.uploadPartial', { success: successCount, fail: failCount }),
|
||||
'warning'
|
||||
);
|
||||
}
|
||||
|
||||
this.loadDocuments();
|
||||
},
|
||||
async viewDocument(document) {
|
||||
this.currentDocument = document;
|
||||
this.showDetailDialog = true;
|
||||
this.detailTab = 'info'; // 重置为基本信息 Tab
|
||||
this.loadChunks(document.doc_id);
|
||||
this.loadMedia(document.doc_id);
|
||||
},
|
||||
async loadChunks(docId) {
|
||||
this.loadingChunks = true;
|
||||
try {
|
||||
const response = await axios.get('/api/kb/chunk/list', {
|
||||
params: { doc_id: docId },
|
||||
});
|
||||
if (response.data.status === 'ok') {
|
||||
this.chunks = response.data.data.items || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading chunks:', error);
|
||||
} finally {
|
||||
this.loadingChunks = false;
|
||||
}
|
||||
},
|
||||
async loadMedia(docId) {
|
||||
this.loadingMedia = true;
|
||||
try {
|
||||
const response = await axios.get('/api/kb/media/list', {
|
||||
params: { doc_id: docId },
|
||||
});
|
||||
if (response.data.status === 'ok') {
|
||||
this.mediaList = response.data.data.items || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading media:', error);
|
||||
} finally {
|
||||
this.loadingMedia = false;
|
||||
}
|
||||
},
|
||||
confirmDelete(document) {
|
||||
this.deleteTarget = document;
|
||||
this.showDeleteDialog = true;
|
||||
},
|
||||
async deleteDocument() {
|
||||
this.deleting = true;
|
||||
try {
|
||||
const response = await axios.post('/api/kb/document/delete', {
|
||||
doc_id: this.deleteTarget.doc_id,
|
||||
});
|
||||
|
||||
if (response.data.status === 'ok') {
|
||||
this.showSnackbar(this.tm('messages.deleteSuccess'));
|
||||
this.showDeleteDialog = false;
|
||||
this.loadDocuments();
|
||||
} else {
|
||||
this.showSnackbar(response.data.message || this.tm('messages.deleteFailed'), 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting document:', error);
|
||||
this.showSnackbar(this.tm('messages.deleteError'), 'error');
|
||||
} finally {
|
||||
this.deleting = false;
|
||||
}
|
||||
},
|
||||
confirmDeleteChunk(chunk) {
|
||||
this.deleteChunkTarget = chunk;
|
||||
this.showDeleteChunkDialog = true;
|
||||
},
|
||||
async deleteChunk() {
|
||||
this.deletingChunk = true;
|
||||
try {
|
||||
const response = await axios.post('/api/kb/chunk/delete', {
|
||||
chunk_id: this.deleteChunkTarget.chunk_id,
|
||||
});
|
||||
|
||||
if (response.data.status === 'ok') {
|
||||
this.showSnackbar(this.tm('messages.chunkDeleteSuccess') || '块删除成功');
|
||||
this.showDeleteChunkDialog = false;
|
||||
// 重新加载块列表
|
||||
if (this.currentDocument) {
|
||||
await this.loadChunks(this.currentDocument.doc_id);
|
||||
// 更新文档列表以反映新的块数量
|
||||
await this.loadDocuments();
|
||||
}
|
||||
} else {
|
||||
this.showSnackbar(response.data.message || this.tm('messages.chunkDeleteFailed') || '块删除失败', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting chunk:', error);
|
||||
this.showSnackbar(this.tm('messages.chunkDeleteError') || '块删除出错', 'error');
|
||||
} finally {
|
||||
this.deletingChunk = false;
|
||||
}
|
||||
},
|
||||
confirmDeleteMedia(media) {
|
||||
this.deleteMediaTarget = media;
|
||||
this.showDeleteMediaDialog = true;
|
||||
},
|
||||
async deleteMedia() {
|
||||
this.deletingMedia = true;
|
||||
try {
|
||||
const response = await axios.post('/api/kb/media/delete', {
|
||||
media_id: this.deleteMediaTarget.media_id,
|
||||
});
|
||||
|
||||
if (response.data.status === 'ok') {
|
||||
this.showSnackbar(this.tm('messages.mediaDeleteSuccess') || '多媒体删除成功');
|
||||
this.showDeleteMediaDialog = false;
|
||||
// 重新加载多媒体列表
|
||||
if (this.currentDocument) {
|
||||
await this.loadMedia(this.currentDocument.doc_id);
|
||||
// 更新文档列表以反映新的多媒体数量
|
||||
await this.loadDocuments();
|
||||
}
|
||||
} else {
|
||||
this.showSnackbar(response.data.message || this.tm('messages.mediaDeleteFailed') || '多媒体删除失败', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting media:', error);
|
||||
this.showSnackbar(this.tm('messages.mediaDeleteError') || '多媒体删除出错', 'error');
|
||||
} finally {
|
||||
this.deletingMedia = false;
|
||||
}
|
||||
},
|
||||
getMediaIcon(mediaType) {
|
||||
const iconMap = {
|
||||
image: 'mdi-image',
|
||||
video: 'mdi-video',
|
||||
audio: 'mdi-music',
|
||||
};
|
||||
return iconMap[mediaType] || 'mdi-file';
|
||||
},
|
||||
showSnackbar(text, color = 'success') {
|
||||
this.snackbar.text = text;
|
||||
this.snackbar.color = color;
|
||||
this.snackbar.show = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.upload-zone {
|
||||
border: 2px dashed #ccc;
|
||||
border-radius: 8px;
|
||||
padding: 48px 24px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.upload-zone:hover {
|
||||
border-color: #5c6bc0;
|
||||
background-color: rgba(92, 107, 192, 0.05);
|
||||
}
|
||||
|
||||
.chunk-card {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.chunk-card:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
|
||||
.chunk-content {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
@@ -1,310 +0,0 @@
|
||||
<template>
|
||||
<div class="kb-settings-panel">
|
||||
<v-form @submit.prevent="saveSettings">
|
||||
<!-- 基本信息 -->
|
||||
<v-card variant="outlined" class="mb-4">
|
||||
<v-card-title>
|
||||
<v-icon start>mdi-information-outline</v-icon>
|
||||
{{ tm('basic.title') }}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="settings.kb_name"
|
||||
:label="tm('basic.nameLabel')"
|
||||
:placeholder="tm('basic.namePlaceholder')"
|
||||
variant="outlined"
|
||||
class="mb-2"
|
||||
required
|
||||
></v-text-field>
|
||||
|
||||
<v-textarea
|
||||
v-model="settings.description"
|
||||
:label="tm('basic.descriptionLabel')"
|
||||
:placeholder="tm('basic.descriptionPlaceholder')"
|
||||
variant="outlined"
|
||||
rows="3"
|
||||
></v-textarea>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- 模型配置 -->
|
||||
<v-card variant="outlined" class="mb-4">
|
||||
<v-card-title>
|
||||
<v-icon start>mdi-brain</v-icon>
|
||||
{{ tm('models.title') }}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-select
|
||||
v-model="settings.embedding_provider_id"
|
||||
:items="embeddingProviders"
|
||||
:label="tm('models.embeddingLabel')"
|
||||
:hint="tm('models.embeddingHint')"
|
||||
persistent-hint
|
||||
variant="outlined"
|
||||
item-title="embedding_model"
|
||||
item-value="id"
|
||||
class="mb-4"
|
||||
>
|
||||
<template v-slot:item="{ props, item }">
|
||||
<v-list-item v-bind="props">
|
||||
<v-list-item-subtitle>
|
||||
Provider ID: {{ item.raw.id }} | 维度: {{ item.raw.embedding_dimensions }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-select>
|
||||
|
||||
<v-select
|
||||
v-model="settings.rerank_provider_id"
|
||||
:items="rerankProviders"
|
||||
:label="tm('models.rerankLabel')"
|
||||
:hint="tm('models.rerankHint')"
|
||||
persistent-hint
|
||||
variant="outlined"
|
||||
item-title="rerank_model"
|
||||
item-value="id"
|
||||
clearable
|
||||
>
|
||||
<template v-slot:item="{ props, item }">
|
||||
<v-list-item v-bind="props">
|
||||
<v-list-item-subtitle>
|
||||
Provider ID: {{ item.raw.id }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-select>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- 分块参数配置 -->
|
||||
<v-card variant="outlined" class="mb-4">
|
||||
<v-card-title>
|
||||
<v-icon start>mdi-puzzle-outline</v-icon>
|
||||
{{ tm('chunking.title') }}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model.number="settings.chunk_size"
|
||||
:label="tm('chunking.chunkSizeLabel')"
|
||||
:hint="tm('chunking.chunkSizeHint')"
|
||||
persistent-hint
|
||||
type="number"
|
||||
variant="outlined"
|
||||
min="50"
|
||||
max="2000"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model.number="settings.chunk_overlap"
|
||||
:label="tm('chunking.chunkOverlapLabel')"
|
||||
:hint="tm('chunking.chunkOverlapHint')"
|
||||
persistent-hint
|
||||
type="number"
|
||||
variant="outlined"
|
||||
min="0"
|
||||
:max="settings.chunk_size ? settings.chunk_size / 2 : 100"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- 检索参数配置 -->
|
||||
<v-card variant="outlined" class="mb-4">
|
||||
<v-card-title>
|
||||
<v-icon start>mdi-magnify</v-icon>
|
||||
{{ tm('retrieval.title') }}
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field
|
||||
v-model.number="settings.top_k_dense"
|
||||
:label="tm('retrieval.topKDenseLabel')"
|
||||
:hint="tm('retrieval.topKDenseHint')"
|
||||
persistent-hint
|
||||
type="number"
|
||||
variant="outlined"
|
||||
min="1"
|
||||
max="100"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field
|
||||
v-model.number="settings.top_k_sparse"
|
||||
:label="tm('retrieval.topKSparseLabel')"
|
||||
:hint="tm('retrieval.topKSparseHint')"
|
||||
persistent-hint
|
||||
type="number"
|
||||
variant="outlined"
|
||||
min="1"
|
||||
max="100"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field
|
||||
v-model.number="settings.top_m_final"
|
||||
:label="tm('retrieval.topMFinalLabel')"
|
||||
:hint="tm('retrieval.topMFinalHint')"
|
||||
persistent-hint
|
||||
type="number"
|
||||
variant="outlined"
|
||||
min="1"
|
||||
max="50"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-switch
|
||||
v-model="settings.enable_rerank"
|
||||
:label="tm('retrieval.enableRerankLabel')"
|
||||
color="primary"
|
||||
class="mt-2"
|
||||
></v-switch>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="text-center">
|
||||
<v-btn
|
||||
color="primary"
|
||||
type="submit"
|
||||
:loading="saving"
|
||||
prepend-icon="mdi-content-save"
|
||||
size="large"
|
||||
>
|
||||
{{ tm('actions.save') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-form>
|
||||
|
||||
<!-- 消息提示 -->
|
||||
<v-snackbar v-model="snackbar.show" :color="snackbar.color" :timeout="3000">
|
||||
{{ snackbar.text }}
|
||||
</v-snackbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import { useModuleI18n } from '@/i18n/composables';
|
||||
|
||||
export default {
|
||||
name: 'KBSettingsPanel',
|
||||
props: {
|
||||
kb: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { tm } = useModuleI18n('features/alkaid/knowledge-base-v2/settings');
|
||||
return { tm };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
settings: {
|
||||
kb_name: '',
|
||||
description: '',
|
||||
embedding_provider_id: null,
|
||||
rerank_provider_id: null,
|
||||
chunk_size: 512,
|
||||
chunk_overlap: 50,
|
||||
top_k_dense: 50,
|
||||
top_k_sparse: 50,
|
||||
top_m_final: 5,
|
||||
enable_rerank: true,
|
||||
},
|
||||
embeddingProviders: [],
|
||||
rerankProviders: [],
|
||||
saving: false,
|
||||
snackbar: {
|
||||
show: false,
|
||||
text: '',
|
||||
color: 'success',
|
||||
},
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.loadSettings();
|
||||
this.loadProviders();
|
||||
},
|
||||
methods: {
|
||||
loadSettings() {
|
||||
this.settings = {
|
||||
kb_name: this.kb.kb_name,
|
||||
description: this.kb.description || '',
|
||||
embedding_provider_id: this.kb.embedding_provider_id,
|
||||
rerank_provider_id: this.kb.rerank_provider_id,
|
||||
chunk_size: this.kb.chunk_size || 512,
|
||||
chunk_overlap: this.kb.chunk_overlap || 50,
|
||||
top_k_dense: this.kb.top_k_dense || 50,
|
||||
top_k_sparse: this.kb.top_k_sparse || 50,
|
||||
top_m_final: this.kb.top_m_final || 5,
|
||||
enable_rerank: this.kb.enable_rerank !== undefined ? this.kb.enable_rerank : true,
|
||||
};
|
||||
},
|
||||
async loadProviders() {
|
||||
try {
|
||||
const response = await axios.get('/api/config/provider/list', {
|
||||
params: { provider_type: 'embedding,rerank' },
|
||||
});
|
||||
if (response.data.status === 'ok') {
|
||||
this.embeddingProviders = response.data.data.filter((p) => p.provider_type === 'embedding');
|
||||
this.rerankProviders = response.data.data.filter((p) => p.provider_type === 'rerank');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading providers:', error);
|
||||
this.showSnackbar(this.tm('messages.loadProvidersError'), 'error');
|
||||
}
|
||||
},
|
||||
async saveSettings() {
|
||||
// 表单验证
|
||||
if (!this.settings.kb_name || !this.settings.kb_name.trim()) {
|
||||
this.showSnackbar(this.tm('messages.nameRequired'), 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.settings.embedding_provider_id) {
|
||||
this.showSnackbar(this.tm('messages.embeddingRequired'), 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
this.saving = true;
|
||||
try {
|
||||
const response = await axios.post('/api/kb/update', {
|
||||
kb_id: this.kb.kb_id,
|
||||
...this.settings,
|
||||
});
|
||||
|
||||
if (response.data.status === 'ok') {
|
||||
this.showSnackbar(this.tm('messages.saveSuccess'));
|
||||
this.$emit('updated');
|
||||
} else {
|
||||
this.showSnackbar(response.data.message || this.tm('messages.saveFailed'), 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving settings:', error);
|
||||
this.showSnackbar(this.tm('messages.saveError'), 'error');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
showSnackbar(text, color = 'success') {
|
||||
this.snackbar.text = text;
|
||||
this.snackbar.color = color;
|
||||
this.snackbar.show = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.kb-settings-panel {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
@@ -1,194 +0,0 @@
|
||||
<template>
|
||||
<div class="search-panel">
|
||||
<v-card variant="outlined" class="mb-4">
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="searchQuery"
|
||||
:label="tm('search.queryLabel')"
|
||||
:placeholder="tm('search.queryPlaceholder')"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
variant="outlined"
|
||||
clearable
|
||||
@keyup.enter="performSearch"
|
||||
></v-text-field>
|
||||
|
||||
<v-row class="mt-2">
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="topK"
|
||||
:items="[3, 5, 10, 20]"
|
||||
:label="tm('search.topKLabel')"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-switch
|
||||
v-model="enableRerank"
|
||||
:label="tm('search.enableRerankLabel')"
|
||||
color="primary"
|
||||
hide-details
|
||||
></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<div class="text-center mt-4">
|
||||
<v-btn
|
||||
color="primary"
|
||||
prepend-icon="mdi-magnify"
|
||||
@click="performSearch"
|
||||
:loading="searching"
|
||||
:disabled="!searchQuery"
|
||||
>
|
||||
{{ tm('search.search') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- 搜索结果 -->
|
||||
<div v-if="searching" class="text-center py-8">
|
||||
<v-progress-circular indeterminate color="primary"></v-progress-circular>
|
||||
<p class="mt-4">{{ tm('search.searching') }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="searchPerformed && searchResults.length === 0" class="text-center py-8">
|
||||
<v-icon size="64" color="grey-lighten-2">mdi-file-search-outline</v-icon>
|
||||
<p class="text-medium-emphasis mt-4">{{ tm('search.noResults') }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="searchResults.length > 0">
|
||||
<div class="d-flex justify-space-between align-center mb-4">
|
||||
<h4>{{ tm('search.resultsTitle', { count: searchResults.length }) }}</h4>
|
||||
<small class="text-medium-emphasis">{{ tm('search.searchTime', { time: searchTime }) }}</small>
|
||||
</div>
|
||||
|
||||
<v-card
|
||||
v-for="(result, index) in searchResults"
|
||||
:key="index"
|
||||
variant="outlined"
|
||||
class="mb-3 result-card"
|
||||
>
|
||||
<v-card-text>
|
||||
<div class="d-flex justify-space-between align-center mb-2">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="small" color="primary" class="mr-1">mdi-file-document-outline</v-icon>
|
||||
<span class="text-caption">{{ result.doc_name || result.metadata?.source }}</span>
|
||||
</div>
|
||||
<v-chip v-if="result.score" size="small" color="primary" variant="tonal">
|
||||
{{ tm('search.relevance') }}: {{ Math.round(result.score * 100) }}%
|
||||
</v-chip>
|
||||
</div>
|
||||
<div class="result-content">{{ result.content }}</div>
|
||||
<div class="text-caption text-medium-emphasis mt-2">
|
||||
{{ tm('search.chunkInfo', { index: result.metadata?.chunk_index, chars: result.char_count }) }}
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
|
||||
<!-- 消息提示 -->
|
||||
<v-snackbar v-model="snackbar.show" :color="snackbar.color" :timeout="3000">
|
||||
{{ snackbar.text }}
|
||||
</v-snackbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import { useModuleI18n } from '@/i18n/composables';
|
||||
|
||||
export default {
|
||||
name: 'SearchPanel',
|
||||
props: {
|
||||
kb: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { tm } = useModuleI18n('features/alkaid/knowledge-base-v2/search');
|
||||
return { tm };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchQuery: '',
|
||||
topK: 5,
|
||||
enableRerank: true,
|
||||
searching: false,
|
||||
searchPerformed: false,
|
||||
searchResults: [],
|
||||
searchTime: 0,
|
||||
snackbar: {
|
||||
show: false,
|
||||
text: '',
|
||||
color: 'success',
|
||||
},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async performSearch() {
|
||||
if (!this.searchQuery || !this.searchQuery.trim()) {
|
||||
this.showSnackbar(this.tm('messages.queryRequired'), 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
this.searching = true;
|
||||
this.searchPerformed = true;
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const response = await axios.post('/api/kb/retrieve', {
|
||||
query: this.searchQuery,
|
||||
kb_ids: [this.kb.kb_id],
|
||||
top_k: this.topK,
|
||||
enable_rerank: this.enableRerank,
|
||||
});
|
||||
|
||||
if (response.data.status === 'ok') {
|
||||
this.searchResults = response.data.data.results || [];
|
||||
this.searchTime = ((Date.now() - startTime) / 1000).toFixed(2);
|
||||
|
||||
if (this.searchResults.length === 0) {
|
||||
this.showSnackbar(this.tm('messages.noResults'), 'info');
|
||||
}
|
||||
} else {
|
||||
this.showSnackbar(response.data.message || this.tm('messages.searchFailed'), 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error searching knowledge base:', error);
|
||||
this.showSnackbar(this.tm('messages.searchError'), 'error');
|
||||
} finally {
|
||||
this.searching = false;
|
||||
}
|
||||
},
|
||||
showSnackbar(text, color = 'success') {
|
||||
this.snackbar.text = text;
|
||||
this.snackbar.color = color;
|
||||
this.snackbar.show = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.result-card {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.result-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
|
||||
.result-content {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.6;
|
||||
padding: 12px;
|
||||
background-color: rgba(0, 0, 0, 0.02);
|
||||
border-radius: 4px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -1,510 +0,0 @@
|
||||
<template>
|
||||
<div class="session-config-panel">
|
||||
<!-- 说明提示 -->
|
||||
<v-alert type="info" variant="tonal" class="mb-4" border="start">
|
||||
<div class="text-subtitle-2 mb-2">
|
||||
<v-icon start>mdi-information-outline</v-icon>
|
||||
{{ tm('info.title') }}
|
||||
</div>
|
||||
<p class="text-caption">{{ tm('info.description') }}</p>
|
||||
<ul class="text-caption mt-2">
|
||||
<li>{{ tm('info.platformLevel') }}</li>
|
||||
<li>{{ tm('info.sessionLevel') }}</li>
|
||||
</ul>
|
||||
</v-alert>
|
||||
|
||||
<!-- 操作按钮组 -->
|
||||
<div class="d-flex justify-space-between align-center mb-4">
|
||||
<h3>{{ tm('list.title') }}</h3>
|
||||
<div class="d-flex gap-2">
|
||||
<v-btn
|
||||
prepend-icon="mdi-refresh"
|
||||
variant="text"
|
||||
@click="loadConfigs"
|
||||
:loading="loading"
|
||||
>
|
||||
{{ tm('list.refresh') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
prepend-icon="mdi-plus"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
@click="openCreateDialog"
|
||||
>
|
||||
{{ tm('list.add') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 配置列表 -->
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="configs"
|
||||
:loading="loading"
|
||||
items-per-page="10"
|
||||
class="elevation-1"
|
||||
>
|
||||
<template v-slot:item.scope="{ item }">
|
||||
<v-chip
|
||||
:color="item.scope === 'platform' ? 'primary' : 'secondary'"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
>
|
||||
<v-icon start size="small">
|
||||
{{ item.scope === 'platform' ? 'mdi-desktop-classic' : 'mdi-chat' }}
|
||||
</v-icon>
|
||||
{{ item.scope === 'platform' ? tm('scope.platform') : tm('scope.session') }}
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.scope_id="{ item }">
|
||||
<code class="scope-id-code">{{ item.scope_id }}</code>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.kb_ids="{ item }">
|
||||
<div class="kb-list">
|
||||
<v-chip
|
||||
v-for="kbId in (item.kb_ids || [])"
|
||||
:key="kbId"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
class="mr-1 mb-1"
|
||||
>
|
||||
<v-icon start size="small">mdi-book-open-variant</v-icon>
|
||||
{{ getKBName(kbId) }}
|
||||
</v-chip>
|
||||
<span v-if="!item.kb_ids || item.kb_ids.length === 0" class="text-medium-emphasis">
|
||||
{{ tm('list.noKB') }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.created_at="{ item }">
|
||||
{{ formatDate(item.created_at) }}
|
||||
</template>
|
||||
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-btn
|
||||
variant="text"
|
||||
size="small"
|
||||
prepend-icon="mdi-pencil"
|
||||
@click="editConfig(item)"
|
||||
>
|
||||
{{ tm('actions.edit') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="text"
|
||||
size="small"
|
||||
color="error"
|
||||
prepend-icon="mdi-delete"
|
||||
@click="confirmDelete(item)"
|
||||
>
|
||||
{{ tm('actions.delete') }}
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<template v-slot:no-data>
|
||||
<div class="text-center py-8">
|
||||
<v-icon size="64" color="grey-lighten-2">mdi-cog-outline</v-icon>
|
||||
<p class="text-medium-emphasis mt-4">{{ tm('empty.noConfigs') }}</p>
|
||||
<v-btn
|
||||
class="mt-2"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
prepend-icon="mdi-plus"
|
||||
@click="openCreateDialog"
|
||||
>
|
||||
{{ tm('empty.createFirst') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
</v-data-table>
|
||||
|
||||
<!-- 新增/编辑配置对话框 -->
|
||||
<v-dialog v-model="showConfigDialog" max-width="700px" persistent>
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon start>mdi-cog</v-icon>
|
||||
<span>{{ editingConfig ? tm('dialog.editTitle') : tm('dialog.addTitle') }}</span>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="plain" icon @click="closeConfigDialog">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<v-form ref="configForm" @submit.prevent="saveConfig">
|
||||
<!-- 配置范围选择 -->
|
||||
<v-radio-group
|
||||
v-model="configForm.scope"
|
||||
:label="tm('dialog.scopeLabel')"
|
||||
class="mb-2"
|
||||
>
|
||||
<v-radio :label="tm('scope.platform')" value="platform">
|
||||
<template v-slot:label>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon start>mdi-desktop-classic</v-icon>
|
||||
<span>{{ tm('scope.platform') }}</span>
|
||||
<v-tooltip location="top">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-icon v-bind="props" size="small" class="ml-2">
|
||||
mdi-information-outline
|
||||
</v-icon>
|
||||
</template>
|
||||
<span>{{ tm('dialog.platformTooltip') }}</span>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</v-radio>
|
||||
<v-radio :label="tm('scope.session')" value="session">
|
||||
<template v-slot:label>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon start>mdi-chat</v-icon>
|
||||
<span>{{ tm('scope.session') }}</span>
|
||||
<v-tooltip location="top">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-icon v-bind="props" size="small" class="ml-2">
|
||||
mdi-information-outline
|
||||
</v-icon>
|
||||
</template>
|
||||
<span>{{ tm('dialog.sessionTooltip') }}</span>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</v-radio>
|
||||
</v-radio-group>
|
||||
|
||||
<!-- 标识输入 -->
|
||||
<v-text-field
|
||||
v-model="configForm.scope_id"
|
||||
:label="configForm.scope === 'platform' ? tm('dialog.platformIdLabel') : tm('dialog.sessionIdLabel')"
|
||||
:placeholder="configForm.scope === 'platform' ? tm('dialog.platformIdPlaceholder') : tm('dialog.sessionIdPlaceholder')"
|
||||
:hint="configForm.scope === 'platform' ? tm('dialog.platformIdHint') : tm('dialog.sessionIdHint')"
|
||||
persistent-hint
|
||||
variant="outlined"
|
||||
class="mb-4"
|
||||
required
|
||||
></v-text-field>
|
||||
|
||||
<!-- 知识库选择 -->
|
||||
<v-select
|
||||
v-model="configForm.kb_ids"
|
||||
:items="availableKBs"
|
||||
item-title="kb_name"
|
||||
item-value="kb_id"
|
||||
:label="tm('dialog.kbLabel')"
|
||||
:placeholder="tm('dialog.kbPlaceholder')"
|
||||
:hint="tm('dialog.kbHint')"
|
||||
persistent-hint
|
||||
variant="outlined"
|
||||
multiple
|
||||
chips
|
||||
closable-chips
|
||||
class="mb-2"
|
||||
required
|
||||
>
|
||||
<template v-slot:chip="{ props, item }">
|
||||
<v-chip v-bind="props" :text="item.raw.kb_name">
|
||||
<template v-slot:prepend>
|
||||
<span class="mr-1">{{ item.raw.emoji || '📚' }}</span>
|
||||
</template>
|
||||
</v-chip>
|
||||
</template>
|
||||
|
||||
<template v-slot:item="{ props, item }">
|
||||
<v-list-item v-bind="props">
|
||||
<template v-slot:prepend>
|
||||
<span class="emoji-icon">{{ item.raw.emoji || '📚' }}</span>
|
||||
</template>
|
||||
<v-list-item-subtitle>
|
||||
{{ item.raw.doc_count || 0 }} 个文档 | {{ item.raw.chunk_count || 0 }} 个块
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-select>
|
||||
|
||||
<!-- 检索参数(可选) -->
|
||||
<v-expansion-panels class="mt-4">
|
||||
<v-expansion-panel>
|
||||
<v-expansion-panel-title>
|
||||
<v-icon start>mdi-tune</v-icon>
|
||||
{{ tm('dialog.advancedSettings') }}
|
||||
</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model.number="configForm.top_k"
|
||||
:label="tm('dialog.topKLabel')"
|
||||
:hint="tm('dialog.topKHint')"
|
||||
persistent-hint
|
||||
type="number"
|
||||
variant="outlined"
|
||||
min="1"
|
||||
max="50"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-switch
|
||||
v-model="configForm.enable_rerank"
|
||||
:label="tm('dialog.enableRerankLabel')"
|
||||
color="primary"
|
||||
></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn @click="closeConfigDialog">{{ tm('dialog.cancel') }}</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="saveConfig"
|
||||
:loading="saving"
|
||||
>
|
||||
{{ tm('dialog.save') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 删除确认对话框 -->
|
||||
<v-dialog v-model="showDeleteDialog" max-width="450px">
|
||||
<v-card>
|
||||
<v-card-title class="text-h5">{{ tm('delete.title') }}</v-card-title>
|
||||
<v-card-text>
|
||||
<p>{{ tm('delete.confirmText') }}</p>
|
||||
<v-alert type="warning" variant="tonal" class="mt-4" density="compact">
|
||||
{{ tm('delete.warning') }}
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn @click="showDeleteDialog = false">{{ tm('delete.cancel') }}</v-btn>
|
||||
<v-btn color="error" @click="deleteConfig" :loading="deleting">
|
||||
{{ tm('delete.delete') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 消息提示 -->
|
||||
<v-snackbar v-model="snackbar.show" :color="snackbar.color" :timeout="3000">
|
||||
{{ snackbar.text }}
|
||||
</v-snackbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import { useModuleI18n } from '@/i18n/composables';
|
||||
|
||||
export default {
|
||||
name: 'SessionConfigPanel',
|
||||
setup() {
|
||||
const { tm } = useModuleI18n('features/alkaid/knowledge-base-v2/session-config');
|
||||
return { tm };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
configs: [],
|
||||
availableKBs: [],
|
||||
loading: false,
|
||||
saving: false,
|
||||
deleting: false,
|
||||
showConfigDialog: false,
|
||||
showDeleteDialog: false,
|
||||
editingConfig: null,
|
||||
deleteTarget: null,
|
||||
configForm: {
|
||||
scope: 'session',
|
||||
scope_id: '',
|
||||
kb_ids: [],
|
||||
top_k: 5,
|
||||
enable_rerank: true,
|
||||
},
|
||||
headers: [
|
||||
{ title: '范围', key: 'scope', width: '12%' },
|
||||
{ title: '标识', key: 'scope_id', width: '23%' },
|
||||
{ title: '关联知识库', key: 'kb_ids', width: '35%' },
|
||||
{ title: '创建时间', key: 'created_at', width: '15%' },
|
||||
{ title: '操作', key: 'actions', width: '15%', sortable: false },
|
||||
],
|
||||
snackbar: {
|
||||
show: false,
|
||||
text: '',
|
||||
color: 'success',
|
||||
},
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.loadConfigs();
|
||||
this.loadAvailableKBs();
|
||||
},
|
||||
methods: {
|
||||
async loadConfigs() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await axios.get('/api/kb/session/config/list');
|
||||
if (response.data.status === 'ok') {
|
||||
this.configs = response.data.data.items || [];
|
||||
} else {
|
||||
this.showSnackbar(response.data.message || this.tm('messages.loadFailed'), 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading configs:', error);
|
||||
this.showSnackbar(this.tm('messages.loadError'), 'error');
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
async loadAvailableKBs() {
|
||||
try {
|
||||
const response = await axios.get('/api/kb/list');
|
||||
if (response.data.status === 'ok') {
|
||||
this.availableKBs = response.data.data.items || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading KBs:', error);
|
||||
}
|
||||
},
|
||||
getKBName(kbId) {
|
||||
const kb = this.availableKBs.find((k) => k.kb_id === kbId);
|
||||
return kb ? kb.kb_name : kbId;
|
||||
},
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '';
|
||||
return new Date(dateString).toLocaleString();
|
||||
},
|
||||
openCreateDialog() {
|
||||
this.editingConfig = null;
|
||||
this.resetForm();
|
||||
this.showConfigDialog = true;
|
||||
},
|
||||
editConfig(config) {
|
||||
this.editingConfig = config;
|
||||
this.configForm = {
|
||||
scope: config.scope,
|
||||
scope_id: config.scope_id,
|
||||
kb_ids: config.kb_ids || [],
|
||||
top_k: config.top_k || 5,
|
||||
enable_rerank: config.enable_rerank !== undefined ? config.enable_rerank : true,
|
||||
};
|
||||
this.showConfigDialog = true;
|
||||
},
|
||||
closeConfigDialog() {
|
||||
this.showConfigDialog = false;
|
||||
this.resetForm();
|
||||
},
|
||||
async saveConfig() {
|
||||
// 表单验证
|
||||
if (!this.configForm.scope_id || !this.configForm.scope_id.trim()) {
|
||||
this.showSnackbar(this.tm('messages.scopeIdRequired'), 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.configForm.kb_ids || this.configForm.kb_ids.length === 0) {
|
||||
this.showSnackbar(this.tm('messages.kbIdsRequired'), 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
this.saving = true;
|
||||
try {
|
||||
const payload = {
|
||||
scope: this.configForm.scope,
|
||||
scope_id: this.configForm.scope_id,
|
||||
kb_ids: this.configForm.kb_ids,
|
||||
top_k: this.configForm.top_k,
|
||||
enable_rerank: this.configForm.enable_rerank,
|
||||
};
|
||||
|
||||
const response = await axios.post('/api/kb/session/config/set', payload);
|
||||
|
||||
if (response.data.status === 'ok') {
|
||||
this.showSnackbar(
|
||||
this.editingConfig ? this.tm('messages.updateSuccess') : this.tm('messages.createSuccess')
|
||||
);
|
||||
this.closeConfigDialog();
|
||||
this.loadConfigs();
|
||||
} else {
|
||||
this.showSnackbar(response.data.message || this.tm('messages.saveFailed'), 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving config:', error);
|
||||
this.showSnackbar(this.tm('messages.saveError'), 'error');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
confirmDelete(config) {
|
||||
this.deleteTarget = config;
|
||||
this.showDeleteDialog = true;
|
||||
},
|
||||
async deleteConfig() {
|
||||
if (!this.deleteTarget) return;
|
||||
|
||||
this.deleting = true;
|
||||
try {
|
||||
const response = await axios.post('/api/kb/session/config/delete', {
|
||||
scope: this.deleteTarget.scope,
|
||||
scope_id: this.deleteTarget.scope_id,
|
||||
});
|
||||
|
||||
if (response.data.status === 'ok') {
|
||||
this.showSnackbar(this.tm('messages.deleteSuccess'));
|
||||
this.showDeleteDialog = false;
|
||||
this.loadConfigs();
|
||||
} else {
|
||||
this.showSnackbar(response.data.message || this.tm('messages.deleteFailed'), 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting config:', error);
|
||||
this.showSnackbar(this.tm('messages.deleteError'), 'error');
|
||||
} finally {
|
||||
this.deleting = false;
|
||||
}
|
||||
},
|
||||
resetForm() {
|
||||
this.editingConfig = null;
|
||||
this.configForm = {
|
||||
scope: 'session',
|
||||
scope_id: '',
|
||||
kb_ids: [],
|
||||
top_k: 5,
|
||||
enable_rerank: true,
|
||||
};
|
||||
},
|
||||
showSnackbar(text, color = 'success') {
|
||||
this.snackbar.text = text;
|
||||
this.snackbar.color = color;
|
||||
this.snackbar.show = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.kb-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.scope-id-code {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.emoji-icon {
|
||||
font-size: 24px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user