feat(knowledge): configure tags during upload confirmation

This commit is contained in:
wizardchen
2026-07-28 11:23:25 +08:00
committed by lyingbug
parent e88564a7db
commit ffff3fe181
11 changed files with 284 additions and 24 deletions
@@ -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
}
+13
View File
@@ -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',
+13
View File
@@ -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: "고급 설정",
+13
View File
@@ -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: 'Расширенные настройки',
+13
View File
@@ -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: "高级设置",
+7
View File
@@ -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 = ''
+26 -6
View File
@@ -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 {
+3 -5
View File
@@ -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 }
}));
}
// 组件挂载时添加全局事件监听器