Compare commits
19 Commits
features/s
...
url2kb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ff2d6c537 | ||
|
|
2125197cab | ||
|
|
5bdd492ddb | ||
|
|
347d3eb4a5 | ||
|
|
1b0cf53a92 | ||
|
|
81b6f20451 | ||
|
|
6f104d713f | ||
|
|
bc8799229a | ||
|
|
917bd401dd | ||
|
|
2878d6000a | ||
|
|
dda8dc2f83 | ||
|
|
5e7835c793 | ||
|
|
55f5e256b2 | ||
|
|
75beb7e7b9 | ||
|
|
89982cfbb5 | ||
|
|
a55f51678c | ||
|
|
3bc3027fc7 | ||
|
|
0dd3a43462 | ||
|
|
d15ed94b9e |
@@ -75,7 +75,8 @@
|
||||
"usage": "Usage: Enter \"/kb use {name}\" in the chat page",
|
||||
"tabs": {
|
||||
"upload": "Upload Files",
|
||||
"search": "Search Content"
|
||||
"search": "Search Content",
|
||||
"fromURL": "From URL"
|
||||
}
|
||||
},
|
||||
"upload": {
|
||||
@@ -132,5 +133,26 @@
|
||||
"deleteFailed": "Deletion failed",
|
||||
"deleteKnowledgeBaseFailed": "Failed to delete knowledge base",
|
||||
"getEmbeddingModelListFailed": "Failed to get embedding model list"
|
||||
},
|
||||
"importFromUrl": {
|
||||
"title": "Import from URL",
|
||||
"urlLabel": "Web Page URL",
|
||||
"urlPlaceholder": "Enter the URL of the web page to extract knowledge from",
|
||||
"optionsTitle": "Import Options",
|
||||
"tooltip": "These options control how text is extracted and processed from the URL content.\nLeave blank to use the plugin's default settings.\nEnabling LLM text repair and summary may take a long time.",
|
||||
"useLlmRepairLabel": "Enable LLM Text Repair",
|
||||
"useClusteringSummaryLabel": "Enable Clustering Summary",
|
||||
"repairLlmProviderIdLabel": "Text Repair Model",
|
||||
"summarizeLlmProviderIdLabel": "Summarize Model",
|
||||
"embeddingProviderIdLabel": "Embedding Model",
|
||||
"chunkSizeLabel": "Chunk Size",
|
||||
"chunkOverlapLabel": "Chunk Overlap",
|
||||
"startImport": "Start Import",
|
||||
"importing": "Importing...",
|
||||
"importSuccess": "Import Successful",
|
||||
"importFailed": "Import Failed",
|
||||
"uploadingChunks": "Content extracted successfully, uploading chunks...",
|
||||
"preRequisite": "Hint: Please go to the plugin market to install astrbot_plugin_url_2_knowledge_base and follow the instructions in the plugin documentation to complete the playwright installation before using this feature.",
|
||||
"allChunksUploaded": "All chunks uploaded successfully"
|
||||
}
|
||||
}
|
||||
@@ -75,7 +75,8 @@
|
||||
"usage": "使用方式: 在聊天页中输入 \"/kb use {name}\"",
|
||||
"tabs": {
|
||||
"upload": "上传文件",
|
||||
"search": "搜索内容"
|
||||
"search": "搜索内容",
|
||||
"fromURL": "从URL导入"
|
||||
}
|
||||
},
|
||||
"upload": {
|
||||
@@ -132,5 +133,26 @@
|
||||
"deleteFailed": "删除失败",
|
||||
"deleteKnowledgeBaseFailed": "删除知识库失败",
|
||||
"getEmbeddingModelListFailed": "获取嵌入模型列表失败"
|
||||
},
|
||||
"importFromUrl": {
|
||||
"title": "从 URL 导入",
|
||||
"urlLabel": "网页 URL",
|
||||
"urlPlaceholder": "请输入要提取知识的网页地址",
|
||||
"optionsTitle": "导入选项",
|
||||
"tooltip": "这些选项控制如何从URL内容中提取和处理文本。\n留空将使用插件的默认设置。\n启用LLM文本修复和摘要后可能花费时间较长。",
|
||||
"useLlmRepairLabel": "启用LLM文本修复",
|
||||
"useClusteringSummaryLabel": "启用聚类摘要",
|
||||
"repairLlmProviderIdLabel": "文本修复模型",
|
||||
"summarizeLlmProviderIdLabel": "摘要模型",
|
||||
"embeddingProviderIdLabel": "嵌入模型",
|
||||
"chunkSizeLabel": "分片长度",
|
||||
"chunkOverlapLabel": "重叠长度",
|
||||
"startImport": "开始导入",
|
||||
"importing": "正在导入...",
|
||||
"importSuccess": "导入成功",
|
||||
"importFailed": "导入失败",
|
||||
"uploadingChunks": "内容提取成功,正在上传分片...",
|
||||
"preRequisite": "提示:请先前往插件市场安装 astrbot_plugin_url_2_knowledge_base 并根据插件文档内的指示完成 playwright 安装后才可使用本功能",
|
||||
"allChunksUploaded": "所有分片上传成功"
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,9 @@
|
||||
:loading="installing">
|
||||
{{ tm('notInstalled.install') }}
|
||||
</v-btn>
|
||||
<ConsoleDisplayer v-show="installing" style="background-color: #fff; max-height: 300px; margin-top: 16px; max-width: 100%" :show-level-btns="false"></ConsoleDisplayer>
|
||||
<ConsoleDisplayer v-show="installing"
|
||||
style="background-color: #fff; max-height: 300px; margin-top: 16px; max-width: 100%"
|
||||
:show-level-btns="false"></ConsoleDisplayer>
|
||||
</div>
|
||||
<div v-else-if="kbCollections.length == 0" class="d-flex align-center justify-center flex-column"
|
||||
style="flex-grow: 1; width: 100%; height: 100%;">
|
||||
@@ -75,13 +77,16 @@
|
||||
<v-form @submit.prevent="submitCreateForm">
|
||||
|
||||
|
||||
<v-text-field variant="outlined" v-model="newKB.name" :label="tm('createDialog.nameLabel')" required></v-text-field>
|
||||
<v-text-field variant="outlined" v-model="newKB.name" :label="tm('createDialog.nameLabel')"
|
||||
required></v-text-field>
|
||||
|
||||
<v-textarea v-model="newKB.description" :label="tm('createDialog.descriptionLabel')" variant="outlined" :placeholder="tm('createDialog.descriptionPlaceholder')"
|
||||
<v-textarea v-model="newKB.description" :label="tm('createDialog.descriptionLabel')"
|
||||
variant="outlined" :placeholder="tm('createDialog.descriptionPlaceholder')"
|
||||
rows="3"></v-textarea>
|
||||
|
||||
<v-select v-model="newKB.embedding_provider_id" :items="embeddingProviderConfigs"
|
||||
:item-props="embeddingModelProps" :label="tm('createDialog.embeddingModelLabel')" variant="outlined" class="mt-2">
|
||||
:item-props="embeddingModelProps" :label="tm('createDialog.embeddingModelLabel')"
|
||||
variant="outlined" class="mt-2">
|
||||
</v-select>
|
||||
|
||||
<small>{{ tm('createDialog.tips') }}</small>
|
||||
@@ -89,8 +94,10 @@
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="error" variant="text" @click="showCreateDialog = false">{{ tm('createDialog.cancel') }}</v-btn>
|
||||
<v-btn color="primary" variant="text" @click="submitCreateForm">{{ tm('createDialog.create') }}</v-btn>
|
||||
<v-btn color="error" variant="text" @click="showCreateDialog = false">{{ tm('createDialog.cancel')
|
||||
}}</v-btn>
|
||||
<v-btn color="primary" variant="text" @click="submitCreateForm">{{ tm('createDialog.create')
|
||||
}}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
@@ -114,7 +121,8 @@
|
||||
</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-btn color="primary" variant="text" @click="showEmojiPicker = false">{{ tm('emojiPicker.close')
|
||||
}}</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
@@ -134,89 +142,177 @@
|
||||
<div v-if="currentKB._embedding_provider_config" class="px-6 py-2">
|
||||
<v-chip class="mr-2" color="primary" variant="tonal" size="small" rounded="sm">
|
||||
<v-icon start size="small">mdi-database</v-icon>
|
||||
{{ tm('contentDialog.embeddingModel') }}: {{ currentKB._embedding_provider_config.embedding_model }}
|
||||
{{ tm('contentDialog.embeddingModel') }}: {{
|
||||
currentKB._embedding_provider_config.embedding_model }}
|
||||
</v-chip>
|
||||
<v-chip color="secondary" variant="tonal" size="small" rounded="sm">
|
||||
<v-icon start size="small">mdi-vector-point</v-icon>
|
||||
{{ tm('contentDialog.vectorDimension') }}: {{ currentKB._embedding_provider_config.embedding_dimensions }}
|
||||
{{ tm('contentDialog.vectorDimension') }}: {{
|
||||
currentKB._embedding_provider_config.embedding_dimensions }}
|
||||
</v-chip>
|
||||
<small style="margin-left: 8px;">💡 使用方式: 在聊天页中输入 “/kb use {{ currentKB.collection_name }}”</small>
|
||||
</div>
|
||||
|
||||
<v-card-text>
|
||||
<v-tabs v-model="activeTab">
|
||||
<v-tab value="upload">{{ tm('contentDialog.tabs.upload') }}</v-tab>
|
||||
<v-tab value="import">导入数据</v-tab>
|
||||
<v-tab value="search">{{ tm('contentDialog.tabs.search') }}</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<v-window v-model="activeTab" class="mt-4">
|
||||
<!-- 上传文件标签页 -->
|
||||
<v-window-item value="upload">
|
||||
<div class="upload-container pa-4">
|
||||
<div class="text-center mb-4">
|
||||
<h3>{{ tm('upload.title') }}</h3>
|
||||
<p class="text-subtitle-1">{{ tm('upload.subtitle') }}</p>
|
||||
<!-- 导入数据标签页 -->
|
||||
<v-window-item value="import">
|
||||
<div class="import-container pa-4">
|
||||
<div class="mb-8">
|
||||
<h2>导入数据</h2>
|
||||
<p class="text-subtitle-1">选择数据源并导入内容到知识库</p>
|
||||
</div>
|
||||
|
||||
<div class="upload-zone" @dragover.prevent @drop.prevent="onFileDrop"
|
||||
@click="triggerFileInput">
|
||||
<input type="file" ref="fileInput" style="display: none" @change="onFileSelected" />
|
||||
<v-icon size="48" color="primary">mdi-cloud-upload</v-icon>
|
||||
<p class="mt-2">{{ tm('upload.dropzone') }}</p>
|
||||
</div>
|
||||
<!-- 数据源选择下拉列表 -->
|
||||
<v-select
|
||||
v-model="dataSource"
|
||||
:items="dataSourceOptions"
|
||||
:label="'数据源选择'"
|
||||
variant="outlined"
|
||||
item-title="title"
|
||||
item-value="value"
|
||||
prepend-inner-icon="mdi-database"
|
||||
></v-select>
|
||||
|
||||
<!-- 优化后的分片长度和重叠长度设置 -->
|
||||
<v-card class="mt-4 chunk-settings-card" variant="outlined" color="grey-lighten-4">
|
||||
<v-card-title class="pa-4 pb-0 d-flex align-center">
|
||||
<v-icon color="primary" class="mr-2">mdi-puzzle-outline</v-icon>
|
||||
<span class="text-subtitle-1 font-weight-bold">{{ tm('upload.chunkSettings.title') }}</span>
|
||||
<v-tooltip location="top">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-icon v-bind="props" class="ml-2" size="small" color="grey">
|
||||
mdi-information-outline
|
||||
</v-icon>
|
||||
</template>
|
||||
<span>
|
||||
{{ tm('upload.chunkSettings.tooltip') }}
|
||||
</span>
|
||||
</v-tooltip>
|
||||
</v-card-title>
|
||||
<v-card-text class="pa-4 pt-2">
|
||||
<div class="d-flex flex-wrap" style="gap: 8px">
|
||||
<v-text-field v-model="chunkSize" :label="tm('upload.chunkSettings.chunkSizeLabel')" type="number"
|
||||
:hint="tm('upload.chunkSettings.chunkSizeHint')" persistent-hint variant="outlined"
|
||||
density="comfortable" class="flex-grow-1 chunk-field"
|
||||
prepend-inner-icon="mdi-text-box-outline" min="50"></v-text-field>
|
||||
|
||||
<v-text-field v-model="overlap" :label="tm('upload.chunkSettings.overlapLabel')" type="number"
|
||||
:hint="tm('upload.chunkSettings.overlapHint')" persistent-hint variant="outlined"
|
||||
density="comfortable" class="flex-grow-1 chunk-field"
|
||||
prepend-inner-icon="mdi-vector-intersection" min="0"></v-text-field>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<div class="selected-files mt-4" v-if="selectedFile">
|
||||
<div type="info" variant="tonal" class="d-flex align-center">
|
||||
<div>
|
||||
<v-icon class="me-2">{{ getFileIcon(selectedFile.name) }}</v-icon>
|
||||
<span style="font-weight: 1000;">{{ selectedFile.name }}</span>
|
||||
</div>
|
||||
<v-btn size="small" color="error" variant="text" @click="selectedFile = null">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
<!-- 从文件导入 -->
|
||||
<div v-if="dataSource === 'file'" class="mt-4">
|
||||
<div class="upload-zone" @dragover.prevent @drop.prevent="onFileDrop"
|
||||
@click="triggerFileInput">
|
||||
<input type="file" ref="fileInput" style="display: none" @change="onFileSelected" />
|
||||
<v-icon size="48" color="primary">mdi-cloud-upload</v-icon>
|
||||
<p class="mt-2">{{ tm('upload.dropzone') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-4">
|
||||
<v-btn color="primary" variant="elevated" :loading="uploading"
|
||||
:disabled="!selectedFile" @click="uploadFile">
|
||||
{{ tm('upload.upload') }}
|
||||
</v-btn>
|
||||
<!-- 分片长度和重叠长度设置 -->
|
||||
<v-card class="mt-4 chunk-settings-card" variant="outlined" color="grey-lighten-4">
|
||||
<v-card-title class="pa-4 pb-0 d-flex align-center">
|
||||
<v-icon color="primary" class="mr-2">mdi-puzzle-outline</v-icon>
|
||||
<span class="text-subtitle-1 font-weight-bold">{{
|
||||
tm('upload.chunkSettings.title') }}</span>
|
||||
<v-tooltip location="top">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-icon v-bind="props" class="ml-2" size="small" color="grey">
|
||||
mdi-information-outline
|
||||
</v-icon>
|
||||
</template>
|
||||
<span>
|
||||
{{ tm('upload.chunkSettings.tooltip') }}
|
||||
</span>
|
||||
</v-tooltip>
|
||||
</v-card-title>
|
||||
<v-card-text class="pa-4 pt-2">
|
||||
<div class="d-flex flex-wrap" style="gap: 8px">
|
||||
<v-text-field v-model="chunkSize"
|
||||
:label="tm('upload.chunkSettings.chunkSizeLabel')" type="number"
|
||||
:hint="tm('upload.chunkSettings.chunkSizeHint')" persistent-hint
|
||||
variant="outlined" density="comfortable" class="flex-grow-1 chunk-field"
|
||||
prepend-inner-icon="mdi-text-box-outline" min="50"></v-text-field>
|
||||
|
||||
<v-text-field v-model="overlap"
|
||||
:label="tm('upload.chunkSettings.overlapLabel')" type="number"
|
||||
:hint="tm('upload.chunkSettings.overlapHint')" persistent-hint
|
||||
variant="outlined" density="comfortable" class="flex-grow-1 chunk-field"
|
||||
prepend-inner-icon="mdi-vector-intersection" min="0"></v-text-field>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<div class="selected-files mt-4" v-if="selectedFile">
|
||||
<div type="info" variant="tonal" class="d-flex align-center">
|
||||
<div>
|
||||
<v-icon class="me-2">{{ getFileIcon(selectedFile.name) }}</v-icon>
|
||||
<span style="font-weight: 1000;">{{ selectedFile.name }}</span>
|
||||
</div>
|
||||
<v-btn size="small" color="error" variant="text" @click="selectedFile = null">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-4">
|
||||
<v-btn color="primary" variant="elevated" :loading="uploading"
|
||||
:disabled="!selectedFile" @click="uploadFile">
|
||||
{{ tm('upload.upload') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="upload-progress mt-4" v-if="uploading">
|
||||
<v-progress-linear indeterminate color="primary"></v-progress-linear>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="upload-progress mt-4" v-if="uploading">
|
||||
<v-progress-linear indeterminate color="primary"></v-progress-linear>
|
||||
<!-- 从URL导入 -->
|
||||
<div v-if="dataSource === 'url'" class="from-url-container">
|
||||
<v-alert type="info" variant="tonal" class="mb-4" border>
|
||||
{{ tm('importFromUrl.preRequisite') }}
|
||||
</v-alert>
|
||||
<v-text-field v-model="importUrl" :label="tm('importFromUrl.urlLabel')"
|
||||
:placeholder="tm('importFromUrl.urlPlaceholder')" variant="outlined" class="mb-4" hide-details></v-text-field>
|
||||
|
||||
<v-card class="mb-4" variant="outlined" color="grey-lighten-4">
|
||||
<v-card-title class="pa-4 pb-0 d-flex align-center">
|
||||
<v-icon color="primary" class="mr-2">mdi-cog-outline</v-icon>
|
||||
<span class="text-subtitle-1 font-weight-bold">{{ tm('importFromUrl.optionsTitle') }}</span>
|
||||
<v-tooltip location="top">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-icon v-bind="props" class="ml-2" size="small" color="grey">mdi-information-outline</v-icon>
|
||||
</template>
|
||||
<span>{{ tm('importFromUrl.tooltip') }}</span>
|
||||
</v-tooltip>
|
||||
</v-card-title>
|
||||
<v-card-text class="pa-4 pt-2">
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-switch hide-details v-model="importOptions.use_llm_repair" :label="tm('importFromUrl.useLlmRepairLabel')"
|
||||
color="primary" inset></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-switch v-model="importOptions.use_clustering_summary" hide-details
|
||||
:label="tm('importFromUrl.useClusteringSummaryLabel')" color="primary" inset></v-switch>
|
||||
</v-col>
|
||||
<v-row class="pa-4">
|
||||
<!-- Optional Repair Selector -->
|
||||
<v-col v-if="importOptions.use_llm_repair" :md="optionalSelectorColWidth" cols="12">
|
||||
<v-select v-model="importOptions.repair_llm_provider_id" :items="llmProviderConfigs" item-value="id"
|
||||
:item-props="llmModelProps" :label="tm('importFromUrl.repairLlmProviderIdLabel')" variant="outlined"
|
||||
clearable hide-details></v-select>
|
||||
</v-col>
|
||||
|
||||
<!-- Optional Summary Selector -->
|
||||
<v-col v-if="importOptions.use_clustering_summary" :md="optionalSelectorColWidth" cols="12">
|
||||
<v-select v-model="importOptions.summarize_llm_provider_id" :items="llmProviderConfigs" item-value="id"
|
||||
:item-props="llmModelProps" :label="tm('importFromUrl.summarizeLlmProviderIdLabel')" variant="outlined"
|
||||
clearable hide-details></v-select>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-select v-model="importOptions.embedding_provider_id" :items="embeddingProviderConfigs" item-value="id"
|
||||
:item-props="embeddingModelProps" :label="tm('importFromUrl.embeddingProviderIdLabel')"
|
||||
variant="outlined" clearable hide-details></v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-text-field v-model="importOptions.chunk_size" :label="tm('importFromUrl.chunkSizeLabel')" type="number"
|
||||
variant="outlined" clearable hide-details></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="3">
|
||||
<v-text-field v-model="importOptions.chunk_overlap" :label="tm('importFromUrl.chunkOverlapLabel')"
|
||||
type="number" variant="outlined" clearable hide-details></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<div class="text-center">
|
||||
<v-btn color="primary" variant="elevated" :loading="importing" :disabled="!importUrl" @click="startImportFromUrl">
|
||||
{{ tm('importFromUrl.startImport') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-window-item>
|
||||
@@ -225,12 +321,13 @@
|
||||
<v-window-item value="search">
|
||||
<div class="search-container pa-4">
|
||||
<v-form @submit.prevent="searchKnowledgeBase" class="d-flex align-center">
|
||||
<v-text-field v-model="searchQuery" :label="tm('search.queryLabel')" append-icon="mdi-magnify"
|
||||
variant="outlined" class="flex-grow-1 me-2" @click:append="searchKnowledgeBase"
|
||||
@keyup.enter="searchKnowledgeBase" :placeholder="tm('search.queryPlaceholder')"
|
||||
hide-details></v-text-field>
|
||||
<v-text-field v-model="searchQuery" :label="tm('search.queryLabel')"
|
||||
append-icon="mdi-magnify" variant="outlined" class="flex-grow-1 me-2"
|
||||
@click:append="searchKnowledgeBase" @keyup.enter="searchKnowledgeBase"
|
||||
:placeholder="tm('search.queryPlaceholder')" hide-details></v-text-field>
|
||||
|
||||
<v-select v-model="topK" :items="[3, 5, 10, 20]" :label="tm('search.resultCountLabel')" variant="outlined"
|
||||
<v-select v-model="topK" :items="[3, 5, 10, 20]"
|
||||
:label="tm('search.resultCountLabel')" variant="outlined"
|
||||
style="max-width: 120px;" hide-details></v-select>
|
||||
</v-form>
|
||||
|
||||
@@ -253,7 +350,8 @@
|
||||
<v-spacer></v-spacer>
|
||||
<v-chip v-if="result.score" size="small" color="primary"
|
||||
variant="tonal">
|
||||
{{ tm('search.relevance') }}: {{ Math.round(result.score * 100) }}%
|
||||
{{ tm('search.relevance') }}: {{ Math.round(result.score * 100)
|
||||
}}%
|
||||
</v-chip>
|
||||
</div>
|
||||
<div class="search-content">{{ result.content }}</div>
|
||||
@@ -284,8 +382,11 @@
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="grey-darken-1" 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-btn color="grey-darken-1" 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>
|
||||
@@ -360,7 +461,12 @@ export default {
|
||||
collection_name: '',
|
||||
emoji: ''
|
||||
},
|
||||
activeTab: 'upload',
|
||||
activeTab: 'import',
|
||||
dataSource: 'file',
|
||||
dataSourceOptions: [
|
||||
{ title: '从文件', value: 'file', icon: 'mdi-file-upload' },
|
||||
{ title: '从URL', value: 'url', icon: 'mdi-web' }
|
||||
],
|
||||
selectedFile: null,
|
||||
chunkSize: null,
|
||||
overlap: null,
|
||||
@@ -375,14 +481,72 @@ export default {
|
||||
collection_name: ''
|
||||
},
|
||||
deleting: false,
|
||||
embeddingProviderConfigs: []
|
||||
embeddingProviderConfigs: [],
|
||||
llmProviderConfigs: [],
|
||||
// URL导入相关数据
|
||||
importUrl: '',
|
||||
importOptions: {
|
||||
use_llm_repair: true,
|
||||
use_clustering_summary: false,
|
||||
repair_llm_provider_id: null,
|
||||
summarize_llm_provider_id: null,
|
||||
embedding_provider_id: null,
|
||||
chunk_size: 300,
|
||||
chunk_overlap: 50,
|
||||
},
|
||||
importing: false,
|
||||
pollingInterval: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
optionalSelectorColWidth() {
|
||||
const repairOn = this.importOptions.use_llm_repair;
|
||||
const summaryOn = this.importOptions.use_clustering_summary;
|
||||
if (repairOn && summaryOn) {
|
||||
return 6; // Both on, each takes half
|
||||
}
|
||||
return 12; // Only one is on, it takes full width
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
llmProviderConfigs: {
|
||||
handler(newVal) {
|
||||
if (newVal && newVal.length > 0) {
|
||||
if (!this.importOptions.repair_llm_provider_id) {
|
||||
this.importOptions.repair_llm_provider_id = newVal[0].id;
|
||||
}
|
||||
if (!this.importOptions.summarize_llm_provider_id) {
|
||||
this.importOptions.summarize_llm_provider_id = newVal[0].id;
|
||||
}
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
deep: true
|
||||
},
|
||||
embeddingProviderConfigs: {
|
||||
handler(newVal) {
|
||||
if (newVal && newVal.length > 0) {
|
||||
if (!this.importOptions.embedding_provider_id) {
|
||||
this.importOptions.embedding_provider_id = newVal[0].id;
|
||||
}
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.checkPlugin();
|
||||
this.getEmbeddingProviderList();
|
||||
this.getLlmProviderList();
|
||||
},
|
||||
methods: {
|
||||
llmModelProps(providerConfig) {
|
||||
return {
|
||||
title: providerConfig.llm_model || providerConfig.id,
|
||||
subtitle: `Provider ID: ${providerConfig.id}`,
|
||||
}
|
||||
},
|
||||
embeddingModelProps(providerConfig) {
|
||||
return {
|
||||
title: providerConfig.embedding_model,
|
||||
@@ -500,7 +664,8 @@ export default {
|
||||
},
|
||||
|
||||
resetContentDialog() {
|
||||
this.activeTab = 'upload';
|
||||
this.activeTab = 'import';
|
||||
this.dataSource = 'file';
|
||||
this.selectedFile = null;
|
||||
this.searchQuery = '';
|
||||
this.searchResults = [];
|
||||
@@ -508,6 +673,13 @@ export default {
|
||||
// 重置分片长度和重叠长度参数
|
||||
this.chunkSize = null;
|
||||
this.overlap = null;
|
||||
// 重置URL导入相关数据
|
||||
this.importUrl = '';
|
||||
this.importing = false;
|
||||
if (this.pollingInterval) {
|
||||
clearInterval(this.pollingInterval);
|
||||
this.pollingInterval = null;
|
||||
}
|
||||
},
|
||||
|
||||
triggerFileInput() {
|
||||
@@ -704,8 +876,178 @@ export default {
|
||||
|
||||
openUrl(url) {
|
||||
window.open(url, '_blank');
|
||||
},
|
||||
|
||||
getLlmProviderList() {
|
||||
axios.get('/api/config/provider/list', {
|
||||
params: {
|
||||
provider_type: 'chat_completion'
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (response.data.status === 'ok') {
|
||||
this.llmProviderConfigs = response.data.data || [];
|
||||
} else {
|
||||
this.showSnackbar(response.data.message || 'Failed to get LLM provider list', 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching LLM providers:', error);
|
||||
this.showSnackbar('Failed to get LLM provider list', 'error');
|
||||
});
|
||||
},
|
||||
|
||||
// URL导入相关方法
|
||||
async startImportFromUrl() {
|
||||
if (!this.importUrl) {
|
||||
this.showSnackbar('Please enter a URL', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
this.importing = true;
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
url: this.importUrl,
|
||||
...Object.fromEntries(Object.entries(this.importOptions).filter(([_, v]) => v !== '' && v !== null && v !== undefined))
|
||||
};
|
||||
|
||||
console.log('Starting URL import with payload:', JSON.stringify(payload, null, 2));
|
||||
const addTaskResponse = await axios.post('/api/plug/url_2_kb/add', payload);
|
||||
|
||||
if (!addTaskResponse.data.task_id) {
|
||||
throw new Error(addTaskResponse.data.message || 'Failed to start import task: No task_id received.');
|
||||
}
|
||||
|
||||
const taskId = addTaskResponse.data.task_id;
|
||||
this.pollTaskStatus(taskId);
|
||||
|
||||
} catch (error) {
|
||||
const errorMessage = error.response?.data?.message || error.message || 'An unknown error occurred.';
|
||||
this.showSnackbar(`Error: ${errorMessage}`, 'error');
|
||||
this.importing = false;
|
||||
}
|
||||
},
|
||||
|
||||
pollTaskStatus(taskId) {
|
||||
this.pollingInterval = setInterval(async () => {
|
||||
try {
|
||||
const statusResponse = await axios.post(`/api/plug/url_2_kb/status`, { task_id: taskId });
|
||||
|
||||
const taskData = statusResponse.data;
|
||||
const taskStatus = taskData.status;
|
||||
|
||||
if (taskStatus === 'completed') {
|
||||
clearInterval(this.pollingInterval);
|
||||
this.pollingInterval = null;
|
||||
this.showSnackbar(this.tm('importFromUrl.uploadingChunks'), 'info');
|
||||
this.handleImportResult(taskData);
|
||||
} else if (taskStatus === 'failed') {
|
||||
clearInterval(this.pollingInterval);
|
||||
this.pollingInterval = null;
|
||||
const failureReason = taskData.result || 'Unknown reason.';
|
||||
this.showSnackbar(`${this.tm('importFromUrl.importFailed')}: ${failureReason}`, 'error');
|
||||
this.importing = false;
|
||||
}
|
||||
} catch (error) {
|
||||
clearInterval(this.pollingInterval);
|
||||
this.pollingInterval = null;
|
||||
const errorMessage = error.response?.data?.message || error.message || 'An unknown error occurred during polling.';
|
||||
this.showSnackbar(`Polling Error: ${errorMessage}`, 'error');
|
||||
this.importing = false;
|
||||
}
|
||||
}, 3000);
|
||||
},
|
||||
|
||||
async handleImportResult(data) {
|
||||
const chunks = [];
|
||||
const result = data.result;
|
||||
|
||||
// 1. Handle overall summary
|
||||
if (result.overall_summary) {
|
||||
chunks.push({ content: result.overall_summary, filename: 'overall_summary.txt' });
|
||||
}
|
||||
|
||||
// 2. Handle topic summaries
|
||||
if (result.topics && result.topics.length > 0) {
|
||||
result.topics.forEach(topic => {
|
||||
if (topic.topic_summary) {
|
||||
chunks.push({ content: topic.topic_summary, filename: `topic_${topic.topic_id}_summary.txt` });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Handle noise points
|
||||
if (result.noise_points && result.noise_points.length > 0) {
|
||||
result.noise_points.forEach((point, index) => {
|
||||
const content = typeof point === 'object' && point.text ? point.text : point;
|
||||
chunks.push({ content: content, filename: `noise_${index + 1}.txt` });
|
||||
});
|
||||
}
|
||||
|
||||
if (chunks.length === 0) {
|
||||
this.showSnackbar('URL processed, but no text chunks were extracted.', 'info');
|
||||
this.importing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
let successCount = 0;
|
||||
let failCount = 0;
|
||||
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
const chunk = chunks[i];
|
||||
try {
|
||||
await this.uploadChunkAsFile(chunk.content, chunk.filename);
|
||||
successCount++;
|
||||
} catch (error) {
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (failCount === 0) {
|
||||
this.showSnackbar(this.tm('importFromUrl.allChunksUploaded'), 'success');
|
||||
} else if (successCount > 0) {
|
||||
this.showSnackbar('Import partially complete. See console for details.', 'warning');
|
||||
} else {
|
||||
this.showSnackbar('Import failed. No chunks were uploaded.', 'error');
|
||||
}
|
||||
|
||||
this.importing = false;
|
||||
this.getKBCollections();
|
||||
},
|
||||
|
||||
async uploadChunkAsFile(content, filename) {
|
||||
const blob = new Blob([content], { type: 'text/plain' });
|
||||
const file = new File([blob], filename, { type: 'text/plain' });
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('collection_name', this.currentKB.collection_name);
|
||||
|
||||
if (this.importOptions.chunk_size && this.importOptions.chunk_size > 0) {
|
||||
formData.append('chunk_size', this.importOptions.chunk_size);
|
||||
}
|
||||
if (this.importOptions.chunk_overlap && this.importOptions.chunk_overlap >= 0) {
|
||||
formData.append('chunk_overlap', this.importOptions.chunk_overlap);
|
||||
}
|
||||
|
||||
const response = await axios.post('/api/plug/alkaid/kb/collection/add_file', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data.status !== 'ok') {
|
||||
throw new Error(response.data.message || 'Chunk upload failed');
|
||||
}
|
||||
return response.data;
|
||||
},
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.pollingInterval) {
|
||||
clearInterval(this.pollingInterval);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -898,4 +1240,13 @@ export default {
|
||||
.chunk-field:focus-within :deep(.v-field__prepend-inner) {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.import-container,
|
||||
.from-url-container {
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.data-source-select :deep(.v-field__prepend-inner) {
|
||||
padding-right: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user