diff --git a/frontend/src/api/knowledge-base/index.ts b/frontend/src/api/knowledge-base/index.ts index f1d422fd9..ebe646cb0 100644 --- a/frontend/src/api/knowledge-base/index.ts +++ b/frontend/src/api/knowledge-base/index.ts @@ -333,6 +333,35 @@ export function getKnowledgeDetailsCon(id: string, page: number) { return get(`/api/v1/chunks/${id}?page=${page}&page_size=25`); } +export interface ChunkEditPayload { + content?: string; + is_enabled?: boolean; + expected_revision?: number; +} + +export function updateDocumentChunk(knowledgeId: string, chunkId: string, data: ChunkEditPayload) { + return put(`/api/v1/chunks/${knowledgeId}/${chunkId}`, data); +} + +export function listChunkRevisions(knowledgeId: string, chunkId: string) { + return get(`/api/v1/chunks/${knowledgeId}/${chunkId}/revisions`); +} + +export function revertDocumentChunk(knowledgeId: string, chunkId: string, revision: number, expectedRevision: number) { + return post(`/api/v1/chunks/${knowledgeId}/${chunkId}/revert`, { + revision, + expected_revision: expectedRevision, + }); +} + +export function updateKnowledgeMetadata(knowledgeId: string, customMetadata: Record) { + return put(`/api/v1/knowledge/${knowledgeId}`, { custom_metadata: customMetadata }); +} + +export function regenerateKnowledgeSummary(knowledgeId: string) { + return post(`/api/v1/knowledge/${knowledgeId}/regenerate-summary`, {}); +} + // Get chunk by chunk_id only (new endpoint - to be added to backend) export function getChunkByIdOnly(chunkId: string) { return get(`/api/v1/chunks/by-id/${chunkId}`); @@ -343,6 +372,17 @@ export function deleteGeneratedQuestion(chunkId: string, questionId: string) { return del(`/api/v1/chunks/by-id/${chunkId}/questions`, { question_id: questionId }); } +export function upsertGeneratedQuestion(chunkId: string, question: string, questionId?: string) { + return put(`/api/v1/chunks/by-id/${chunkId}/questions`, { + question_id: questionId || '', + question, + }); +} + +export function regenerateGeneratedQuestions(chunkId: string) { + return post(`/api/v1/chunks/by-id/${chunkId}/questions/regenerate`, {}); +} + export function listKnowledgeTags( kbId: string, params?: { page?: number; page_size?: number; keyword?: string }, diff --git a/frontend/src/components/doc-content.vue b/frontend/src/components/doc-content.vue index 4a78607f0..df8e9211e 100644 --- a/frontend/src/components/doc-content.vue +++ b/frontend/src/components/doc-content.vue @@ -8,7 +8,11 @@ import hljs from "highlight.js"; import "highlight.js/styles/github.css"; import mermaid from "mermaid"; import { onMounted, ref, nextTick, onUnmounted, watch, computed } from "vue"; -import { downKnowledgeDetails, deleteGeneratedQuestion, getChunkByIdOnly, previewKnowledgeFile } from "@/api/knowledge-base/index"; +import { + downKnowledgeDetails, deleteGeneratedQuestion, getChunkByIdOnly, previewKnowledgeFile, + updateDocumentChunk, listChunkRevisions, revertDocumentChunk, updateKnowledgeMetadata, + regenerateKnowledgeSummary, upsertGeneratedQuestion, regenerateGeneratedQuestions, +} from "@/api/knowledge-base/index"; import { MessagePlugin, DialogPlugin } from "tdesign-vue-next"; import { sanitizeHTML, safeMarkdownToHTML, createSafeImage, isValidImageURL, hydrateProtectedFileImages, isValidURL } from '@/utils/security'; import { normalizeSpuriousTablePrefixes } from '@/utils/markdownTableNormalize'; @@ -30,6 +34,49 @@ const canDeleteGeneratedQuestion = computed(() => { if (props.canEditKB === true) return true; return authStore.hasRole('admin'); }); +const canEditContent = canDeleteGeneratedQuestion; + +const metadataEditing = ref(false); +const metadataDraft = ref('{}'); +const metadataSaving = ref(false); +const summaryRefreshing = ref(false); + +const syncMetadataDraft = () => { + metadataDraft.value = JSON.stringify(props.details?.custom_metadata || {}, null, 2); +}; + +const saveMetadata = async () => { + try { + const value = JSON.parse(metadataDraft.value || '{}'); + if (!value || Array.isArray(value) || typeof value !== 'object') throw new Error(t('knowledgeBase.metadataObjectRequired')); + metadataSaving.value = true; + await updateKnowledgeMetadata(props.details.id, value); + props.details.custom_metadata = value; + metadataEditing.value = false; + props.details.summary_status = props.details.description ? 'pending' : props.details.summary_status; + MessagePlugin.success(t('common.saveSuccess')); + } catch (error: any) { + MessagePlugin.error(error?.message || t('common.saveFailed')); + } finally { + metadataSaving.value = false; + } +}; + +const refreshSummary = async () => { + summaryRefreshing.value = true; + try { + const result: any = await regenerateKnowledgeSummary(props.details.id); + if (result?.data) { + props.details.description = result.data.description || ''; + props.details.summary_status = result.data.summary_status || 'completed'; + } + MessagePlugin.success(t('knowledgeBase.summaryRefreshed')); + } catch (error: any) { + MessagePlugin.error(error?.message || t('common.error')); + } finally { + summaryRefreshing.value = false; + } +}; const detailTags = computed(() => { const tags = props.details?.tags; @@ -86,6 +133,7 @@ mermaid.initialize({ }); const props = defineProps(["visible", "details", "knowledgeType", "sourceInfo", "canEditKB", "canDownloadKB", "parse_status", "kbId"]); const emit = defineEmits(["closeDoc", "getDoc", "questionDeleted"]); +watch(() => props.details?.id, syncMetadataDraft, { immediate: true }); const hasTimelineSpans = ref(false); const timelineDrawerVisible = ref(false); @@ -892,6 +940,149 @@ const toggleQuestions = (index: number) => { const isExpanded = (index: number) => expandedChunks.value.has(index); +const editingChunkId = ref(''); +const chunkDraft = ref(''); +const savingChunkId = ref(''); + +const startChunkEdit = (item: any) => { + editingChunkId.value = item.id; + chunkDraft.value = item.content || ''; +}; + +const saveChunkEdit = async (item: any) => { + if (!chunkDraft.value.trim()) { + MessagePlugin.warning(t('knowledgeBase.chunkContentRequired')); + return; + } + savingChunkId.value = item.id; + try { + const result: any = await updateDocumentChunk(props.details.id, item.id, { + content: chunkDraft.value, + expected_revision: item.content_revision || 0, + }); + Object.assign(item, result.data); + editingChunkId.value = ''; + props.details.summary_status = props.details.description ? 'pending' : props.details.summary_status; + MessagePlugin.success(t('common.saveSuccess')); + } catch (error: any) { + MessagePlugin.error(error?.message || t('common.saveFailed')); + } finally { + savingChunkId.value = ''; + } +}; + +const toggleChunkEnabled = async (item: any) => { + try { + const result: any = await updateDocumentChunk(props.details.id, item.id, { + is_enabled: !item.is_enabled, + expected_revision: item.content_revision || 0, + }); + Object.assign(item, result.data); + } catch (error: any) { + MessagePlugin.error(error?.message || t('common.error')); + } +}; + +const retryChunkIndex = async (item: any) => { + try { + const result: any = await updateDocumentChunk(props.details.id, item.id, { + expected_revision: item.content_revision || 0, + }); + Object.assign(item, result.data); + if (item.index_status === 'failed') throw new Error(t('knowledgeBase.indexFailed')); + MessagePlugin.success(t('knowledgeBase.indexRetrySuccess')); + } catch (error: any) { + MessagePlugin.error(error?.message || t('common.error')); + } +}; + +const showChunkHistory = async (item: any) => { + try { + const result: any = await listChunkRevisions(props.details.id, item.id); + const revisions = result?.data || []; + const body = revisions.length + ? revisions.map((r: any) => `v${r.revision} · ${new Date(r.edited_at).toLocaleString()}\n${r.content.slice(0, 240)}`).join('\n\n') + : t('knowledgeBase.noChunkHistory'); + if (!revisions.length) { + DialogPlugin.alert({ header: t('knowledgeBase.chunkHistory'), body }); + return; + } + const dialog = DialogPlugin.confirm({ + header: t('knowledgeBase.chunkHistory'), body, + confirmBtn: t('knowledgeBase.revertRevision'), cancelBtn: t('common.close'), + onConfirm: async () => { + const value = window.prompt(t('knowledgeBase.enterRevision'), String(revisions[0].revision)); + if (value !== null && Number.isInteger(Number(value))) await revertChunk(item, Number(value)); + dialog.hide(); + }, + onClose: () => dialog.hide(), + }); + } catch (error: any) { + MessagePlugin.error(error?.message || t('common.error')); + } +}; + +const revertChunk = async (item: any, revision: number) => { + try { + const result: any = await revertDocumentChunk(props.details.id, item.id, revision, item.content_revision || 0); + Object.assign(item, result.data); + MessagePlugin.success(t('knowledgeBase.chunkReverted')); + } catch (error: any) { + MessagePlugin.error(error?.message || t('common.error')); + } +}; + +const questionDrafts = ref>({}); +const savingQuestionChunk = ref(''); +const regeneratingQuestionChunk = ref(''); + +const addQuestion = async (item: any) => { + const question = (questionDrafts.value[item.id] || '').trim(); + if (!question) return; + savingQuestionChunk.value = item.id; + try { + const result: any = await upsertGeneratedQuestion(item.id, question); + questionDrafts.value[item.id] = ''; + const metadata = typeof item.metadata === 'string' ? JSON.parse(item.metadata || '{}') : (item.metadata || {}); + metadata.generated_questions = [...(metadata.generated_questions || []), result.data]; + metadata.generated_questions_revision = item.content_revision || 0; + item.metadata = metadata; + MessagePlugin.success(t('common.saveSuccess')); + } catch (error: any) { + MessagePlugin.error(error?.message || t('common.error')); + } finally { + savingQuestionChunk.value = ''; + } +}; + +const editQuestion = async (item: any, question: GeneratedQuestion) => { + const value = window.prompt(t('knowledgeBase.editGeneratedQuestion'), question.question); + if (!value || value.trim() === question.question) return; + try { + await upsertGeneratedQuestion(item.id, value.trim(), question.id); + question.question = value.trim(); + MessagePlugin.success(t('common.saveSuccess')); + } catch (error: any) { + MessagePlugin.error(error?.message || t('common.error')); + } +}; + +const regenerateQuestions = async (item: any) => { + regeneratingQuestionChunk.value = item.id; + try { + const result: any = await regenerateGeneratedQuestions(item.id); + const metadata = typeof item.metadata === 'string' ? JSON.parse(item.metadata || '{}') : (item.metadata || {}); + metadata.generated_questions = result?.data || []; + metadata.generated_questions_revision = item.content_revision || 0; + item.metadata = metadata; + MessagePlugin.success(t('knowledgeBase.questionsRegenerated')); + } catch (error: any) { + MessagePlugin.error(error?.message || t('common.error')); + } finally { + regeneratingQuestionChunk.value = ''; + } +}; + // 删除中的状态 const deletingQuestion = ref<{ chunkIndex: number; questionId: string } | null>(null); @@ -1174,6 +1365,27 @@ const handleDetailsScroll = () => { + @@ -1190,7 +1402,12 @@ const handleDetailsScroll = () => {
-

{{ $t('knowledgeBase.documentSummary') }}

+
+

{{ $t('knowledgeBase.documentSummary') }}

+ + {{ $t('knowledgeBase.regenerateSummary') }} + +
@@ -1267,9 +1484,24 @@ const handleDetailsScroll = () => { {{ $t('knowledgeBase.questions') }} {{ chunk.questions.length }} {{ chunk.meta }} + {{ $t('knowledgeBase.retryIndex') }} +
-
+
+ +
+ {{ $t('common.save') }} + {{ $t('common.cancel') }} +
+
+
@@ -1294,6 +1526,10 @@ const handleDetailsScroll = () => {
{{ question.question }} + + + @@ -1302,6 +1538,18 @@ const handleDetailsScroll = () => {
+
+ + {{ $t('common.add') }} + {{ $t('knowledgeBase.regenerateQuestions') }} +
+
+ +
+
+ + {{ $t('common.add') }} + {{ $t('knowledgeBase.regenerateQuestions') }}
@@ -1322,6 +1570,25 @@ const handleDetailsScroll = () => {