mirror of
https://github.com/Tencent/WeKnora.git
synced 2026-09-24 16:29:01 +08:00
feat(knowledge): configure tags during upload confirmation
This commit is contained in:
@@ -20,6 +20,7 @@ const handleCancel = () => {
|
||||
:kb-info="uploadConfirmStore.kbInfo"
|
||||
:files="uploadConfirmStore.files"
|
||||
:urls="uploadConfirmStore.urls"
|
||||
:tag-ids="uploadConfirmStore.tagIds"
|
||||
:manual-preview="uploadConfirmStore.manual"
|
||||
:reparse-preview="uploadConfirmStore.reparse"
|
||||
:accept-file-types="uploadConfirmStore.acceptFileTypes"
|
||||
|
||||
@@ -29,6 +29,7 @@ interface KnowledgeDetailResponse {
|
||||
file_name?: string
|
||||
metadata?: any
|
||||
parse_status?: string
|
||||
tags?: Array<{ id: string }>
|
||||
}
|
||||
|
||||
type ManualStatus = 'draft' | 'publish'
|
||||
@@ -69,6 +70,7 @@ const visible = computed({
|
||||
const mode = computed(() => uiStore.manualEditorMode)
|
||||
const knowledgeId = computed(() => uiStore.manualEditorKnowledgeId)
|
||||
const currentKnowledgeId = ref<string | null>(null)
|
||||
const manualTagIds = ref<string[]>([])
|
||||
|
||||
const form = reactive({
|
||||
kbId: '' as string,
|
||||
@@ -528,6 +530,7 @@ const loadKnowledgeContent = async () => {
|
||||
''
|
||||
form.content = meta?.content || uiStore.manualEditorInitialContent || ''
|
||||
form.status = resolveManualKnowledgeStatus(meta?.status, data.parse_status)
|
||||
manualTagIds.value = (data.tags || []).map(tag => String(tag.id))
|
||||
if (meta?.updatedAt) {
|
||||
lastUpdatedAt.value = meta.updatedAt
|
||||
}
|
||||
@@ -555,6 +558,7 @@ const resetForm = () => {
|
||||
activeTab.value = 'edit'
|
||||
lastUpdatedAt.value = ''
|
||||
initialLoaded.value = false
|
||||
manualTagIds.value = mode.value === 'create' ? [...uiStore.selectedTagIds] : []
|
||||
selectionRange.start = 0
|
||||
selectionRange.end = 0
|
||||
}
|
||||
@@ -613,7 +617,6 @@ const handleSave = async (targetStatus: ManualStatus) => {
|
||||
saving.value = true
|
||||
savingAction.value = targetStatus
|
||||
try {
|
||||
const tagIdsToUpload = uiStore.selectedTagIds.length > 0 ? [...uiStore.selectedTagIds] : undefined
|
||||
const payload: {
|
||||
title: string
|
||||
content: string
|
||||
@@ -625,9 +628,7 @@ const handleSave = async (targetStatus: ManualStatus) => {
|
||||
content: form.content,
|
||||
status: targetStatus,
|
||||
}
|
||||
if (tagIdsToUpload && tagIdsToUpload.length > 0) {
|
||||
payload.tag_ids = tagIdsToUpload
|
||||
}
|
||||
payload.tag_ids = [...manualTagIds.value]
|
||||
|
||||
if (targetStatus === 'publish') {
|
||||
let kbInfo: any
|
||||
@@ -651,10 +652,12 @@ const handleSave = async (targetStatus: ManualStatus) => {
|
||||
knowledgeId: currentKnowledgeId.value || undefined,
|
||||
title: payload.title,
|
||||
content: payload.content,
|
||||
tagIds: tagIdsToUpload,
|
||||
tagIds: [...manualTagIds.value],
|
||||
},
|
||||
})
|
||||
payload.process_config = confirmResult.processConfig
|
||||
manualTagIds.value = [...(confirmResult.tagIds || [])]
|
||||
payload.tag_ids = [...manualTagIds.value]
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -739,20 +739,30 @@ export default {
|
||||
summaryModel: 'Model',
|
||||
summaryQuestionCount: 'Questions per chunk',
|
||||
summaryQuestionCountValue: '{count}',
|
||||
summaryTableInstructionsSet: 'Table instructions set',
|
||||
summaryTableInstructionsDefault: 'Default table instructions',
|
||||
summaryGraphTags: 'Relation types',
|
||||
summaryGraphTagsValue: '{count}',
|
||||
navChunkingSummary: 'Chunk {size}',
|
||||
statusOn: 'On',
|
||||
statusOff: 'Off',
|
||||
notSet: 'Not set',
|
||||
summaryNoTags: 'Not set',
|
||||
summaryTagsCount: '{count} tags',
|
||||
confirm: 'Upload and parse',
|
||||
cancel: 'Cancel',
|
||||
uploading: 'Uploading {current}/{total}',
|
||||
tabParser: 'Parser',
|
||||
tabTags: 'Document tags',
|
||||
tagsDescription: 'Select one or more tags for every document in this import batch',
|
||||
tagsPlaceholder: 'Select tags (multiple allowed)',
|
||||
tagsEmpty: 'This knowledge base has no tags. You can create them in tag management after upload.',
|
||||
tagsLoadFailed: 'Failed to load tags. You can set them later from the document list.',
|
||||
tabChunking: 'Chunking',
|
||||
tabMultimodal: 'Multimodal',
|
||||
tabAsr: 'Audio',
|
||||
tabQuestion: 'Question generation',
|
||||
tabEnhancement: 'Content enhancement',
|
||||
tabGraph: 'Knowledge graph',
|
||||
noFiles: 'Keep at least one file to upload',
|
||||
noItems: 'Add at least one file or URL',
|
||||
@@ -3166,6 +3176,9 @@ export default {
|
||||
modelLabel: 'ASR Model',
|
||||
modelDescription: 'Speech-to-text model for audio (e.g. OpenAI Whisper)',
|
||||
modelPlaceholder: 'Select an ASR model',
|
||||
languageLabel: 'Audio language hint',
|
||||
languageDescription: 'Optional. A primary language hint can improve some ASR models; leave empty for auto-detection.',
|
||||
languagePlaceholder: 'For example zh or en; empty means auto-detect',
|
||||
},
|
||||
advanced: {
|
||||
title: 'Advanced Settings',
|
||||
|
||||
@@ -744,20 +744,30 @@ export default {
|
||||
summaryModel: "모델",
|
||||
summaryQuestionCount: "청크당 질문 수",
|
||||
summaryQuestionCountValue: "{count}개",
|
||||
summaryTableInstructionsSet: "테이블 지침 설정됨",
|
||||
summaryTableInstructionsDefault: "기본 테이블 지침",
|
||||
summaryGraphTags: "관계 유형",
|
||||
summaryGraphTagsValue: "{count}개",
|
||||
navChunkingSummary: "청크 {size}",
|
||||
statusOn: "켜짐",
|
||||
statusOff: "꺼짐",
|
||||
notSet: "미설정",
|
||||
summaryNoTags: "미설정",
|
||||
summaryTagsCount: "태그 {count}개",
|
||||
confirm: "업로드 및 파싱 확인",
|
||||
cancel: "취소",
|
||||
uploading: "업로드 중 {current}/{total}",
|
||||
tabParser: "파서",
|
||||
tabTags: "문서 태그",
|
||||
tagsDescription: "이 배치로 가져오는 모든 문서에 적용할 태그를 여러 개 선택할 수 있습니다",
|
||||
tagsPlaceholder: "태그 선택(복수 선택 가능)",
|
||||
tagsEmpty: "현재 지식 베이스에 태그가 없습니다. 업로드 후 태그 관리에서 만들 수 있습니다.",
|
||||
tagsLoadFailed: "태그를 불러오지 못했습니다. 나중에 문서 목록에서 설정할 수 있습니다.",
|
||||
tabChunking: "청킹",
|
||||
tabMultimodal: "멀티모달",
|
||||
tabAsr: "오디오",
|
||||
tabQuestion: "질문 생성",
|
||||
tabEnhancement: "콘텐츠 향상",
|
||||
tabGraph: "지식 그래프",
|
||||
noFiles: "업로드할 파일을 하나 이상 남겨 두세요",
|
||||
noItems: "파일 또는 URL을 하나 이상 추가하세요",
|
||||
@@ -4413,6 +4423,9 @@ export default {
|
||||
modelLabel: "ASR 모델",
|
||||
modelDescription: "오디오의 음성을 텍스트로 전사하는 모델 (예: OpenAI Whisper)",
|
||||
modelPlaceholder: "ASR 모델을 선택해주세요",
|
||||
languageLabel: "오디오 언어 힌트",
|
||||
languageDescription: "선택 사항입니다. 기본 언어를 지정하면 일부 ASR 모델의 인식이 향상되며, 비워 두면 자동 감지합니다.",
|
||||
languagePlaceholder: "예: zh, en; 비워 두면 자동 감지",
|
||||
},
|
||||
advanced: {
|
||||
title: "고급 설정",
|
||||
|
||||
@@ -708,20 +708,30 @@ export default {
|
||||
summaryModel: 'Модель',
|
||||
summaryQuestionCount: 'Вопросов на чанк',
|
||||
summaryQuestionCountValue: '{count}',
|
||||
summaryTableInstructionsSet: 'Инструкции таблиц заданы',
|
||||
summaryTableInstructionsDefault: 'Инструкции таблиц по умолчанию',
|
||||
summaryGraphTags: 'Типы связей',
|
||||
summaryGraphTagsValue: '{count}',
|
||||
navChunkingSummary: 'Чанк {size}',
|
||||
statusOn: 'Вкл.',
|
||||
statusOff: 'Выкл.',
|
||||
notSet: 'Не задано',
|
||||
summaryNoTags: 'Не задано',
|
||||
summaryTagsCount: 'Тегов: {count}',
|
||||
confirm: 'Загрузить и обработать',
|
||||
cancel: 'Отмена',
|
||||
uploading: 'Загрузка {current}/{total}',
|
||||
tabParser: 'Парсер',
|
||||
tabTags: 'Теги документов',
|
||||
tagsDescription: 'Выберите один или несколько тегов для всех документов в этой партии',
|
||||
tagsPlaceholder: 'Выберите теги (можно несколько)',
|
||||
tagsEmpty: 'В этой базе знаний пока нет тегов. Их можно создать после загрузки.',
|
||||
tagsLoadFailed: 'Не удалось загрузить теги. Их можно назначить позже в списке документов.',
|
||||
tabChunking: 'Разбиение',
|
||||
tabMultimodal: 'Мультимодальность',
|
||||
tabAsr: 'Аудио',
|
||||
tabQuestion: 'Генерация вопросов',
|
||||
tabEnhancement: 'Обогащение контента',
|
||||
tabGraph: 'Граф знаний',
|
||||
noFiles: 'Оставьте хотя бы один файл для загрузки',
|
||||
noItems: 'Добавьте хотя бы один файл или URL',
|
||||
@@ -3866,6 +3876,9 @@ export default {
|
||||
modelLabel: 'Модель ASR',
|
||||
modelDescription: 'Модель распознавания речи для транскрибации аудио (например, OpenAI Whisper)',
|
||||
modelPlaceholder: 'Выберите модель ASR',
|
||||
languageLabel: 'Подсказка языка аудио',
|
||||
languageDescription: 'Необязательно. Основной язык может улучшить распознавание некоторыми ASR-моделями; пустое значение включает автоопределение.',
|
||||
languagePlaceholder: 'Например zh или en; пусто — автоопределение',
|
||||
},
|
||||
advanced: {
|
||||
title: 'Расширенные настройки',
|
||||
|
||||
@@ -742,20 +742,30 @@ export default {
|
||||
summaryModel: "模型",
|
||||
summaryQuestionCount: "每块问题数",
|
||||
summaryQuestionCountValue: "{count} 个",
|
||||
summaryTableInstructionsSet: "表格指令已设置",
|
||||
summaryTableInstructionsDefault: "表格指令默认",
|
||||
summaryGraphTags: "关系类型",
|
||||
summaryGraphTagsValue: "{count} 个",
|
||||
navChunkingSummary: "分块 {size}",
|
||||
statusOn: "已开启",
|
||||
statusOff: "未开启",
|
||||
notSet: "未设置",
|
||||
summaryNoTags: "未设置",
|
||||
summaryTagsCount: "{count} 个标签",
|
||||
confirm: "确认上传并解析",
|
||||
cancel: "取消",
|
||||
uploading: "正在上传 {current}/{total}",
|
||||
tabParser: "解析引擎",
|
||||
tabTags: "文档标签",
|
||||
tagsDescription: "为本批次导入的所有文档选择标签,可多选",
|
||||
tagsPlaceholder: "请选择标签(可多选)",
|
||||
tagsEmpty: "当前知识库暂无标签,可上传后在标签管理中创建",
|
||||
tagsLoadFailed: "标签加载失败,可稍后在文档列表中设置",
|
||||
tabChunking: "分块",
|
||||
tabMultimodal: "多模态",
|
||||
tabAsr: "音频",
|
||||
tabQuestion: "问题生成",
|
||||
tabEnhancement: "内容增强",
|
||||
tabGraph: "图谱",
|
||||
noFiles: "请至少保留一个待上传文件",
|
||||
noItems: "请至少添加一个文件或 URL",
|
||||
@@ -4460,6 +4470,9 @@ export default {
|
||||
modelLabel: "ASR 模型",
|
||||
modelDescription: "用于音频中语音转文本的识别模型(如 OpenAI Whisper)",
|
||||
modelPlaceholder: "请选择 ASR 模型",
|
||||
languageLabel: "音频语言提示",
|
||||
languageDescription: "可选;指定主要语种可提高部分 ASR 模型的识别稳定性,留空时自动检测",
|
||||
languagePlaceholder: "例如 zh、en,留空自动检测",
|
||||
},
|
||||
advanced: {
|
||||
title: "高级设置",
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface UploadConfirmReparseSource {
|
||||
export interface UploadConfirmResult {
|
||||
processConfig: KnowledgeProcessOverrides
|
||||
mode: UploadConfirmMode
|
||||
tagIds?: string[]
|
||||
files?: File[]
|
||||
urls?: string[]
|
||||
manual?: UploadConfirmManualSource
|
||||
@@ -30,6 +31,7 @@ export interface UploadConfirmResult {
|
||||
export interface OpenUploadConfirmOptions {
|
||||
mode: UploadConfirmMode
|
||||
kbInfo: any
|
||||
tagIds?: string[]
|
||||
files?: File[]
|
||||
urls?: string[]
|
||||
manual?: UploadConfirmManualSource
|
||||
@@ -45,6 +47,7 @@ export const useUploadConfirmStore = defineStore('uploadConfirm', {
|
||||
kbInfo: null as any,
|
||||
files: [] as File[],
|
||||
urls: [] as string[],
|
||||
tagIds: [] as string[],
|
||||
manual: null as UploadConfirmManualSource | null,
|
||||
reparse: null as UploadConfirmReparseSource | null,
|
||||
acceptFileTypes: '',
|
||||
@@ -61,6 +64,9 @@ export const useUploadConfirmStore = defineStore('uploadConfirm', {
|
||||
this.kbInfo = options.kbInfo
|
||||
this.files = options.files ? [...options.files] : []
|
||||
this.urls = options.urls ? [...options.urls] : []
|
||||
this.tagIds = options.tagIds
|
||||
? [...options.tagIds]
|
||||
: [...(options.manual?.tagIds || [])]
|
||||
this.manual = options.manual || null
|
||||
this.reparse = options.reparse || null
|
||||
this.acceptFileTypes = options.acceptFileTypes || ''
|
||||
@@ -86,6 +92,7 @@ export const useUploadConfirmStore = defineStore('uploadConfirm', {
|
||||
this.kbInfo = null
|
||||
this.files = []
|
||||
this.urls = []
|
||||
this.tagIds = []
|
||||
this.manual = null
|
||||
this.reparse = null
|
||||
this.acceptFileTypes = ''
|
||||
|
||||
@@ -993,6 +993,16 @@ const handleOpenURLImportDialog = (event: CustomEvent) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Global file drops are captured by the platform shell. Route them back into
|
||||
// the same confirmation flow as the page upload button so tags and per-batch
|
||||
// processing settings are never skipped.
|
||||
const handleKnowledgeFileDrop = (event: CustomEvent) => {
|
||||
const eventKbId = event.detail?.kbId;
|
||||
const files = Array.isArray(event.detail?.files) ? event.detail.files : [];
|
||||
if (eventKbId !== kbId.value || isFAQ.value || files.length === 0) return;
|
||||
handleUploadSourceFiles(files);
|
||||
};
|
||||
|
||||
// Auto-open document detail when navigated with ?knowledge_id=xxx.
|
||||
// Note: this runs both when the KB page mounts with a query param AND when a
|
||||
// subsequent in-page navigation (e.g. from the global command palette) only
|
||||
@@ -1046,12 +1056,14 @@ onMounted(() => {
|
||||
|
||||
window.addEventListener('knowledgeFileUploaded', handleFileUploaded as EventListener);
|
||||
window.addEventListener('openURLImportDialog', handleOpenURLImportDialog as EventListener);
|
||||
window.addEventListener('weknora:knowledge-file-drop', handleKnowledgeFileDrop as EventListener);
|
||||
window.addEventListener('weknora:open-knowledge', handleOpenKnowledgeEvent as EventListener);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('knowledgeFileUploaded', handleFileUploaded as EventListener);
|
||||
window.removeEventListener('openURLImportDialog', handleOpenURLImportDialog as EventListener);
|
||||
window.removeEventListener('weknora:knowledge-file-drop', handleKnowledgeFileDrop as EventListener);
|
||||
window.removeEventListener('weknora:open-knowledge', handleOpenKnowledgeEvent as EventListener);
|
||||
stopMovePoll();
|
||||
if (timeout !== null) {
|
||||
@@ -1407,14 +1419,16 @@ const showUploadResultMessages = (
|
||||
|
||||
const executeUploadBatch = async (
|
||||
files: File[],
|
||||
options: { processConfig?: KnowledgeProcessOverrides } = {},
|
||||
options: { processConfig?: KnowledgeProcessOverrides; tagIds?: string[] } = {},
|
||||
) => {
|
||||
const targetKbId = kbId.value;
|
||||
if (!targetKbId || files.length === 0) {
|
||||
return { successCount: 0, failCount: files.length };
|
||||
}
|
||||
|
||||
const tagIdsToUpload = selectedTagIds.value.length > 0 ? [...selectedTagIds.value] : undefined;
|
||||
const tagIdsToUpload = options.tagIds && options.tagIds.length > 0
|
||||
? [...options.tagIds]
|
||||
: undefined;
|
||||
let successCount = 0;
|
||||
let failCount = 0;
|
||||
const totalCount = files.length;
|
||||
@@ -1479,14 +1493,18 @@ const executeUploadBatch = async (
|
||||
return { successCount, failCount };
|
||||
};
|
||||
|
||||
const executeUrlImport = async (url: string, processConfig?: KnowledgeProcessOverrides) => {
|
||||
const executeUrlImport = async (
|
||||
url: string,
|
||||
processConfig?: KnowledgeProcessOverrides,
|
||||
tagIds?: string[],
|
||||
) => {
|
||||
const targetKbId = kbId.value;
|
||||
if (!targetKbId) {
|
||||
MessagePlugin.error(t('error.missingKbId'));
|
||||
return;
|
||||
}
|
||||
|
||||
const tagIdsToUpload = selectedTagIds.value.length > 0 ? [...selectedTagIds.value] : undefined;
|
||||
const tagIdsToUpload = tagIds && tagIds.length > 0 ? [...tagIds] : undefined;
|
||||
try {
|
||||
const responseData: any = await createKnowledgeFromURL(targetKbId, {
|
||||
url,
|
||||
@@ -1528,6 +1546,7 @@ const handleUploadConfirmResult = async (result: UploadConfirmResult) => {
|
||||
const files = result.files || [];
|
||||
const urls = result.urls || [];
|
||||
const processConfig = result.processConfig;
|
||||
const tagIds = result.tagIds || [];
|
||||
|
||||
if (files.length > 0) {
|
||||
const hasFolderPaths = files.some((file) => {
|
||||
@@ -1537,11 +1556,11 @@ const handleUploadConfirmResult = async (result: UploadConfirmResult) => {
|
||||
if (hasFolderPaths) {
|
||||
MessagePlugin.info(t('knowledgeBase.uploadingFolder', { total: files.length }));
|
||||
}
|
||||
await executeUploadBatch(files, { processConfig });
|
||||
await executeUploadBatch(files, { processConfig, tagIds });
|
||||
}
|
||||
|
||||
for (const url of urls) {
|
||||
await executeUrlImport(url, processConfig);
|
||||
await executeUrlImport(url, processConfig, tagIds);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1552,6 +1571,7 @@ const openUploadConfirmDialog = async (files: File[], urls: string[] = []) => {
|
||||
const result = await uploadConfirmStore.open({
|
||||
mode: 'file',
|
||||
kbInfo: kbInfo.value,
|
||||
tagIds: [...selectedTagIds.value],
|
||||
files,
|
||||
urls,
|
||||
acceptFileTypes: acceptFileTypes.value,
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
import { readFileSync } from 'node:fs'
|
||||
|
||||
const dialog = readFileSync(new URL('./UploadConfirmDialog.vue', import.meta.url), 'utf8')
|
||||
const host = readFileSync(new URL('../../../components/UploadConfirmHost.vue', import.meta.url), 'utf8')
|
||||
const knowledgeBase = readFileSync(new URL('../KnowledgeBase.vue', import.meta.url), 'utf8')
|
||||
const platform = readFileSync(new URL('../../platform/index.vue', import.meta.url), 'utf8')
|
||||
|
||||
test('selects multiple document tags and returns them with the confirmation result', () => {
|
||||
assert.match(host, /:tag-ids="uploadConfirmStore\.tagIds"/)
|
||||
assert.match(dialog, /v-model="selectedTagIds"/)
|
||||
assert.match(dialog, /multiple/)
|
||||
assert.match(dialog, /listKnowledgeTags\(kbId, \{ page: 1, page_size: 1000 \}\)/)
|
||||
assert.match(dialog, /tagIds: \[\.\.\.selectedTagIds\.value\]/)
|
||||
})
|
||||
|
||||
test('uses confirmed tags for file and URL imports instead of reading the list filter at upload time', () => {
|
||||
assert.match(knowledgeBase, /const tagIds = result\.tagIds \|\| \[\]/)
|
||||
assert.match(knowledgeBase, /executeUploadBatch\(files, \{ processConfig, tagIds \}\)/)
|
||||
assert.match(knowledgeBase, /executeUrlImport\(url, processConfig, tagIds\)/)
|
||||
assert.doesNotMatch(
|
||||
knowledgeBase,
|
||||
/const tagIdsToUpload = selectedTagIds\.value\.length > 0 \? \[\.\.\.selectedTagIds\.value\] : undefined/,
|
||||
)
|
||||
})
|
||||
|
||||
test('routes global knowledge file drops through the upload confirmation flow', () => {
|
||||
assert.match(platform, /weknora:knowledge-file-drop/)
|
||||
assert.match(knowledgeBase, /handleKnowledgeFileDrop/)
|
||||
assert.match(knowledgeBase, /handleUploadSourceFiles\(files\)/)
|
||||
})
|
||||
@@ -136,6 +136,33 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="activeSection === 'tags'" class="section">
|
||||
<div class="kb-embedded-settings">
|
||||
<div class="setting-row setting-row--field setting-row--tags">
|
||||
<div class="setting-info">
|
||||
<label>{{ t('uploadConfirm.tabTags') }}</label>
|
||||
<p class="desc">{{ t('uploadConfirm.tagsDescription') }}</p>
|
||||
</div>
|
||||
<div class="setting-control setting-control--full">
|
||||
<t-select
|
||||
v-model="selectedTagIds"
|
||||
:options="tagOptions"
|
||||
:loading="tagsLoading"
|
||||
multiple
|
||||
filterable
|
||||
clearable
|
||||
:placeholder="t('uploadConfirm.tagsPlaceholder')"
|
||||
/>
|
||||
<p v-if="tagsLoadFailed" class="field-error field-error--muted">
|
||||
{{ t('uploadConfirm.tagsLoadFailed') }}
|
||||
</p>
|
||||
<p v-else-if="!tagsLoading && tagOptions.length === 0" class="field-hint">
|
||||
{{ t('uploadConfirm.tagsEmpty') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="activeSection === 'chunking'" class="section">
|
||||
<KBChunkingSettings
|
||||
embedded
|
||||
@@ -176,6 +203,38 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="uiState.multimodalConfig.enabled" class="setting-row setting-row--field">
|
||||
<div class="setting-info">
|
||||
<label>{{ t('knowledgeEditor.advanced.multimodal.descriptionLanguageLabel') }}</label>
|
||||
<p class="desc">{{ t('knowledgeEditor.advanced.multimodal.descriptionLanguageDescription') }}</p>
|
||||
</div>
|
||||
<div class="setting-control setting-control--full">
|
||||
<t-select
|
||||
v-model="uiState.multimodalConfig.descriptionLanguage"
|
||||
clearable
|
||||
:placeholder="t('knowledgeEditor.advanced.multimodal.descriptionLanguageAuto')"
|
||||
>
|
||||
<t-option value="Chinese" :label="t('language.zhCN')" />
|
||||
<t-option value="English" :label="t('language.enUS')" />
|
||||
<t-option value="Korean" :label="t('language.koKR')" />
|
||||
<t-option value="Russian" :label="t('language.ruRU')" />
|
||||
</t-select>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="uiState.multimodalConfig.enabled" class="setting-row setting-row--field">
|
||||
<div class="setting-info">
|
||||
<label>{{ t('knowledgeEditor.advanced.multimodal.customInstructionsLabel') }}</label>
|
||||
<p class="desc">{{ t('knowledgeEditor.advanced.multimodal.customInstructionsDescription') }}</p>
|
||||
</div>
|
||||
<div class="setting-control setting-control--full">
|
||||
<t-textarea
|
||||
v-model="uiState.multimodalConfig.customInstructions"
|
||||
:placeholder="t('knowledgeEditor.advanced.multimodal.customInstructionsPlaceholder')"
|
||||
:maxlength="4000"
|
||||
:autosize="{ minRows: 3, maxRows: 8 }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="activeSection === 'asr'" class="section">
|
||||
@@ -211,6 +270,19 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="uiState.asrConfig.enabled" class="setting-row setting-row--field">
|
||||
<div class="setting-info">
|
||||
<label>{{ t('knowledgeEditor.asr.languageLabel') }}</label>
|
||||
<p class="desc">{{ t('knowledgeEditor.asr.languageDescription') }}</p>
|
||||
</div>
|
||||
<div class="setting-control setting-control--full">
|
||||
<t-input
|
||||
v-model="uiState.asrConfig.language"
|
||||
clearable
|
||||
:placeholder="t('knowledgeEditor.asr.languagePlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="activeSection === 'question'" class="section">
|
||||
@@ -219,7 +291,9 @@
|
||||
:question-generation="uiState.questionGenerationConfig"
|
||||
:rag-enabled="ragEnabled"
|
||||
:all-models="allModels"
|
||||
:table-metadata-instructions="uiState.chunkingConfig.tableMetadataInstructions"
|
||||
@update:question-generation="handleQuestionGenerationUpdate"
|
||||
@update:table-metadata-instructions="handleTableMetadataInstructionsUpdate"
|
||||
/>
|
||||
</div>
|
||||
<div v-show="activeSection === 'graph'" class="section">
|
||||
@@ -263,6 +337,7 @@ import { useChatResourcesStore } from '@/stores/chatResources'
|
||||
import { useUIStore } from '@/stores/ui'
|
||||
import { formatFileSize, getFileIcon } from '@/utils/files'
|
||||
import { getUploadFileKey } from '../utils/uploadSources'
|
||||
import { listKnowledgeTags } from '@/api/knowledge-base'
|
||||
import KbUploadSourceDropdown from './KbUploadSourceDropdown.vue'
|
||||
import type { KnowledgeProcessOverrides } from '@/types/knowledgeProcess'
|
||||
import type {
|
||||
@@ -312,6 +387,7 @@ const props = withDefaults(defineProps<{
|
||||
mode?: UploadConfirmMode
|
||||
files?: File[]
|
||||
urls?: string[]
|
||||
tagIds?: string[]
|
||||
manualPreview?: UploadConfirmManualSource | null
|
||||
reparsePreview?: UploadConfirmReparseSource | null
|
||||
tagId?: string
|
||||
@@ -321,6 +397,7 @@ const props = withDefaults(defineProps<{
|
||||
mode: 'file',
|
||||
files: () => [],
|
||||
urls: () => [],
|
||||
tagIds: () => [],
|
||||
manualPreview: null,
|
||||
reparsePreview: null,
|
||||
acceptFileTypes: '',
|
||||
@@ -340,6 +417,10 @@ const uiStore = useUIStore()
|
||||
const allModels = ref<any[]>([])
|
||||
const localFiles = ref<File[]>([])
|
||||
const localUrls = ref<string[]>([])
|
||||
const availableTags = ref<Array<{ id: string; name: string }>>([])
|
||||
const selectedTagIds = ref<string[]>([])
|
||||
const tagsLoading = ref(false)
|
||||
const tagsLoadFailed = ref(false)
|
||||
const activeSection = ref('overview')
|
||||
const uiState = ref<UploadUIState>(createDefaultUIState())
|
||||
|
||||
@@ -495,13 +576,39 @@ const chunkingOverviewValue = computed(() => {
|
||||
return parts.join(' · ')
|
||||
})
|
||||
|
||||
const tagOptions = computed(() => availableTags.value.map(tag => ({
|
||||
label: tag.name,
|
||||
value: tag.id,
|
||||
})))
|
||||
|
||||
const tagsOverviewValue = computed(() => {
|
||||
if (selectedTagIds.value.length === 0) return t('uploadConfirm.summaryNoTags')
|
||||
const nameById = new Map(availableTags.value.map(tag => [tag.id, tag.name]))
|
||||
const names = selectedTagIds.value.map(id => nameById.get(id)).filter(Boolean) as string[]
|
||||
if (names.length > 0 && names.length <= 3) return names.join('、')
|
||||
return t('uploadConfirm.summaryTagsCount', { count: selectedTagIds.value.length })
|
||||
})
|
||||
|
||||
const enhancementOverviewValue = computed(() => {
|
||||
const question = uiState.value.questionGenerationConfig
|
||||
const questionValue = question.enabled
|
||||
? t('uploadConfirm.summaryQuestionCountValue', { count: question.questionCount })
|
||||
: t('uploadConfirm.statusOff')
|
||||
const tableValue = uiState.value.chunkingConfig.tableMetadataInstructions?.trim()
|
||||
? t('uploadConfirm.summaryTableInstructionsSet')
|
||||
: t('uploadConfirm.summaryTableInstructionsDefault')
|
||||
return `${questionValue} · ${tableValue}`
|
||||
})
|
||||
|
||||
const overviewLines = computed(() => {
|
||||
const mm = uiState.value.multimodalConfig
|
||||
const asr = uiState.value.asrConfig
|
||||
const qg = uiState.value.questionGenerationConfig
|
||||
const graph = uiState.value.nodeExtractConfig
|
||||
|
||||
return [
|
||||
...(props.mode === 'reparse'
|
||||
? []
|
||||
: [{ key: 'tags', title: t('uploadConfirm.tabTags'), value: tagsOverviewValue.value }]),
|
||||
{ key: 'parser', title: t('uploadConfirm.tabParser'), value: parserOverviewValue.value },
|
||||
{ key: 'chunking', title: t('uploadConfirm.tabChunking'), value: chunkingOverviewValue.value },
|
||||
{
|
||||
@@ -520,10 +627,8 @@ const overviewLines = computed(() => {
|
||||
},
|
||||
{
|
||||
key: 'question',
|
||||
title: t('uploadConfirm.tabQuestion'),
|
||||
value: qg.enabled
|
||||
? t('uploadConfirm.summaryQuestionCountValue', { count: qg.questionCount })
|
||||
: t('uploadConfirm.statusOff'),
|
||||
title: t('uploadConfirm.tabEnhancement'),
|
||||
value: enhancementOverviewValue.value,
|
||||
},
|
||||
{
|
||||
key: 'graph',
|
||||
@@ -538,11 +643,12 @@ const overviewLines = computed(() => {
|
||||
})
|
||||
|
||||
const sectionMeta: Record<string, { titleKey: string; descKey?: string }> = {
|
||||
tags: { titleKey: 'uploadConfirm.tabTags', descKey: 'uploadConfirm.tagsDescription' },
|
||||
parser: { titleKey: 'uploadConfirm.tabParser', descKey: 'kbSettings.parser.description' },
|
||||
chunking: { titleKey: 'uploadConfirm.tabChunking', descKey: 'knowledgeEditor.chunking.description' },
|
||||
multimodal: { titleKey: 'uploadConfirm.tabMultimodal', descKey: 'knowledgeEditor.multimodal.description' },
|
||||
asr: { titleKey: 'uploadConfirm.tabAsr', descKey: 'knowledgeEditor.asr.description' },
|
||||
question: { titleKey: 'uploadConfirm.tabQuestion', descKey: 'knowledgeEditor.advanced.questionGeneration.description' },
|
||||
question: { titleKey: 'uploadConfirm.tabEnhancement', descKey: 'knowledgeEditor.advanced.description' },
|
||||
graph: { titleKey: 'uploadConfirm.tabGraph', descKey: 'graphSettings.description' },
|
||||
}
|
||||
|
||||
@@ -829,18 +935,41 @@ async function loadModels() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTags() {
|
||||
const kbId = props.kbInfo?.id
|
||||
availableTags.value = []
|
||||
tagsLoadFailed.value = false
|
||||
if (!kbId || props.mode === 'reparse') return
|
||||
|
||||
tagsLoading.value = true
|
||||
try {
|
||||
const response: any = await listKnowledgeTags(kbId, { page: 1, page_size: 1000 })
|
||||
const tags = response?.data?.data || []
|
||||
availableTags.value = tags.map((tag: any) => ({
|
||||
id: String(tag.id),
|
||||
name: String(tag.name || ''),
|
||||
}))
|
||||
} catch {
|
||||
tagsLoadFailed.value = true
|
||||
} finally {
|
||||
tagsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (!visible) return
|
||||
localFiles.value = props.mode === 'file' ? [...(props.files || [])] : []
|
||||
localUrls.value = props.mode === 'file' ? [...(props.urls || [])] : []
|
||||
selectedTagIds.value = props.mode === 'reparse' ? [] : [...(props.tagIds || [])]
|
||||
initFromKbInfo(props.kbInfo)
|
||||
if (props.mode === 'reparse') {
|
||||
applyOverridesToState(props.reparsePreview?.processOverrides)
|
||||
}
|
||||
activeSection.value = 'overview'
|
||||
loadModels()
|
||||
loadTags()
|
||||
},
|
||||
)
|
||||
|
||||
@@ -904,10 +1033,14 @@ const handleAddASRModel = () => {
|
||||
uiStore.openSettings('models', 'asr')
|
||||
}
|
||||
|
||||
const handleQuestionGenerationUpdate = (config: { enabled: boolean; questionCount: number }) => {
|
||||
const handleQuestionGenerationUpdate = (config: { enabled: boolean; questionCount: number; customInstructions?: string }) => {
|
||||
uiState.value.questionGenerationConfig = { ...config }
|
||||
}
|
||||
|
||||
const handleTableMetadataInstructionsUpdate = (value: string) => {
|
||||
uiState.value.chunkingConfig.tableMetadataInstructions = value
|
||||
}
|
||||
|
||||
const handleNodeExtractUpdate = (config: UploadUIState['nodeExtractConfig']) => {
|
||||
uiState.value.nodeExtractConfig = { ...config }
|
||||
// GraphSettings is the only graph switch in the upload dialog. Update both
|
||||
@@ -957,13 +1090,19 @@ const handleConfirm = () => {
|
||||
|
||||
const processConfig = buildProcessOverrides()
|
||||
if (props.mode === 'manual' && props.manualPreview) {
|
||||
emit('confirm', { processConfig, mode: 'manual', manual: { ...props.manualPreview } })
|
||||
emit('confirm', {
|
||||
processConfig,
|
||||
mode: 'manual',
|
||||
tagIds: [...selectedTagIds.value],
|
||||
manual: { ...props.manualPreview, tagIds: [...selectedTagIds.value] },
|
||||
})
|
||||
} else if (props.mode === 'reparse' && props.reparsePreview) {
|
||||
emit('confirm', { processConfig, mode: 'reparse', reparse: { ...props.reparsePreview } })
|
||||
} else {
|
||||
emit('confirm', {
|
||||
processConfig,
|
||||
mode: 'file',
|
||||
tagIds: [...selectedTagIds.value],
|
||||
files: [...localFiles.value],
|
||||
urls: [...localUrls.value],
|
||||
})
|
||||
@@ -1342,6 +1481,14 @@ const handleConfirm = () => {
|
||||
line-height: 1.4;
|
||||
color: var(--td-error-color);
|
||||
}
|
||||
|
||||
.field-error--muted,
|
||||
.field-hint {
|
||||
margin: 6px 0 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
color: var(--td-text-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.edit-section--embedded {
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<RouterView />
|
||||
</div>
|
||||
<div class="upload-mask" v-show="ismask">
|
||||
<input type="file" style="display: none" ref="uploadInput" accept=".pdf,.docx,.doc,.pptx,.ppt,.epub,.mhtml,.txt,.md,.jpg,.jpeg,.png,.csv,.xls,.xlsx" />
|
||||
<UploadMask></UploadMask>
|
||||
</div>
|
||||
<!-- 全局设置模态框,供所有 platform 子路由使用 -->
|
||||
@@ -23,7 +22,6 @@
|
||||
import Menu from '@/components/menu.vue'
|
||||
import { ref, onMounted, onUnmounted, nextTick, provide, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import useKnowledgeBase from '@/hooks/useKnowledgeBase'
|
||||
import UploadMask from '@/components/upload-mask.vue'
|
||||
import Settings from '@/views/settings/Settings.vue'
|
||||
import GlobalCommandPalette from '@/components/GlobalCommandPalette.vue'
|
||||
@@ -35,12 +33,10 @@ import { getKnowledgeBaseById } from '@/api/knowledge-base/index'
|
||||
import { MessagePlugin } from 'tdesign-vue-next'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
let { requestMethod } = useKnowledgeBase()
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const commandPaletteStore = useCommandPaletteStore();
|
||||
let ismask = ref(false)
|
||||
let uploadInput = ref();
|
||||
const { t } = useI18n();
|
||||
|
||||
const isRouterAlive = ref(true)
|
||||
@@ -198,7 +194,9 @@ const handleGlobalDrop = async (event: DragEvent) => {
|
||||
return;
|
||||
}
|
||||
|
||||
droppedFiles.forEach(file => requestMethod(file, uploadInput));
|
||||
window.dispatchEvent(new CustomEvent('weknora:knowledge-file-drop', {
|
||||
detail: { kbId: getCurrentKbId(), files: droppedFiles }
|
||||
}));
|
||||
}
|
||||
|
||||
// 组件挂载时添加全局事件监听器
|
||||
|
||||
Reference in New Issue
Block a user