From ffff3fe1815aeba0fe622dbc3affd053732d6008 Mon Sep 17 00:00:00 2001 From: wizardchen Date: Tue, 28 Jul 2026 11:12:38 +0800 Subject: [PATCH] feat(knowledge): configure tags during upload confirmation --- frontend/src/components/UploadConfirmHost.vue | 1 + .../components/manual-knowledge-editor.vue | 13 +- frontend/src/i18n/locales/en-US.ts | 13 ++ frontend/src/i18n/locales/ko-KR.ts | 13 ++ frontend/src/i18n/locales/ru-RU.ts | 13 ++ frontend/src/i18n/locales/zh-CN.ts | 13 ++ frontend/src/stores/uploadConfirm.ts | 7 + .../src/views/knowledge/KnowledgeBase.vue | 32 +++- .../components/UploadConfirmDialog.test.ts | 32 ++++ .../components/UploadConfirmDialog.vue | 163 +++++++++++++++++- frontend/src/views/platform/index.vue | 8 +- 11 files changed, 284 insertions(+), 24 deletions(-) create mode 100644 frontend/src/views/knowledge/components/UploadConfirmDialog.test.ts diff --git a/frontend/src/components/UploadConfirmHost.vue b/frontend/src/components/UploadConfirmHost.vue index 1e257935a..74f4690e8 100644 --- a/frontend/src/components/UploadConfirmHost.vue +++ b/frontend/src/components/UploadConfirmHost.vue @@ -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" diff --git a/frontend/src/components/manual-knowledge-editor.vue b/frontend/src/components/manual-knowledge-editor.vue index bc52ead1f..5eecf5a2c 100644 --- a/frontend/src/components/manual-knowledge-editor.vue +++ b/frontend/src/components/manual-knowledge-editor.vue @@ -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(null) +const manualTagIds = ref([]) 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 } diff --git a/frontend/src/i18n/locales/en-US.ts b/frontend/src/i18n/locales/en-US.ts index 67a2228a9..554b14a5e 100755 --- a/frontend/src/i18n/locales/en-US.ts +++ b/frontend/src/i18n/locales/en-US.ts @@ -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', diff --git a/frontend/src/i18n/locales/ko-KR.ts b/frontend/src/i18n/locales/ko-KR.ts index 4ef1910d5..f0a10482b 100755 --- a/frontend/src/i18n/locales/ko-KR.ts +++ b/frontend/src/i18n/locales/ko-KR.ts @@ -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: "고급 설정", diff --git a/frontend/src/i18n/locales/ru-RU.ts b/frontend/src/i18n/locales/ru-RU.ts index 5edaed873..0de4c4565 100755 --- a/frontend/src/i18n/locales/ru-RU.ts +++ b/frontend/src/i18n/locales/ru-RU.ts @@ -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: 'Расширенные настройки', diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index f9d316020..a184fa4f7 100755 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -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: "高级设置", diff --git a/frontend/src/stores/uploadConfirm.ts b/frontend/src/stores/uploadConfirm.ts index c8837466d..350bd4ddd 100644 --- a/frontend/src/stores/uploadConfirm.ts +++ b/frontend/src/stores/uploadConfirm.ts @@ -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 = '' diff --git a/frontend/src/views/knowledge/KnowledgeBase.vue b/frontend/src/views/knowledge/KnowledgeBase.vue index da8820985..7f9f44cdb 100644 --- a/frontend/src/views/knowledge/KnowledgeBase.vue +++ b/frontend/src/views/knowledge/KnowledgeBase.vue @@ -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, diff --git a/frontend/src/views/knowledge/components/UploadConfirmDialog.test.ts b/frontend/src/views/knowledge/components/UploadConfirmDialog.test.ts new file mode 100644 index 000000000..2bda7c85e --- /dev/null +++ b/frontend/src/views/knowledge/components/UploadConfirmDialog.test.ts @@ -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\)/) +}) diff --git a/frontend/src/views/knowledge/components/UploadConfirmDialog.vue b/frontend/src/views/knowledge/components/UploadConfirmDialog.vue index 0b1a1be74..e0a445d16 100644 --- a/frontend/src/views/knowledge/components/UploadConfirmDialog.vue +++ b/frontend/src/views/knowledge/components/UploadConfirmDialog.vue @@ -136,6 +136,33 @@ +
+
+
+
+ +

{{ t('uploadConfirm.tagsDescription') }}

+
+
+ +

+ {{ t('uploadConfirm.tagsLoadFailed') }} +

+

+ {{ t('uploadConfirm.tagsEmpty') }} +

+
+
+
+
+
+
+ +

{{ t('knowledgeEditor.advanced.multimodal.descriptionLanguageDescription') }}

+
+
+ + + + + + +
+
+
+
+ +

{{ t('knowledgeEditor.advanced.multimodal.customInstructionsDescription') }}

+
+
+ +
+
@@ -211,6 +270,19 @@

+
+
+ +

{{ t('knowledgeEditor.asr.languageDescription') }}

+
+
+ +
+
@@ -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" />
@@ -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([]) const localFiles = ref([]) const localUrls = ref([]) +const availableTags = ref>([]) +const selectedTagIds = ref([]) +const tagsLoading = ref(false) +const tagsLoadFailed = ref(false) const activeSection = ref('overview') const uiState = ref(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 = { + 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 { diff --git a/frontend/src/views/platform/index.vue b/frontend/src/views/platform/index.vue index 630b1a4a2..7b662caa8 100644 --- a/frontend/src/views/platform/index.vue +++ b/frontend/src/views/platform/index.vue @@ -5,7 +5,6 @@
-
@@ -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 } + })); } // 组件挂载时添加全局事件监听器