Files
qiming/qimingclaw/crates/agent-electron-client/src/renderer/components/pages/KnowledgePage.tsx
2026-07-14 16:39:40 +08:00

2085 lines
64 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import {
Button,
Empty,
Form,
Input,
Modal,
Select,
Spin,
Table,
Tabs,
Tag,
Tooltip,
message,
} from "antd";
import {
ArrowLeftOutlined,
CheckOutlined,
CloudUploadOutlined,
DatabaseOutlined,
DeleteOutlined,
DownloadOutlined,
EditOutlined,
FileAddOutlined,
FileTextOutlined,
PictureOutlined,
PlusOutlined,
ReloadOutlined,
SearchOutlined,
TeamOutlined,
UploadOutlined,
} from "@ant-design/icons";
import type { ColumnsType } from "antd/es/table";
import ImageWithFallback from "../common/ImageWithFallback";
import { getCurrentAuth } from "../../services/core/auth";
import {
AiAgentContext,
AiManualComponent,
addAiAgentKnowledgeComponent,
enableAiAgentKnowledgeForChat,
fetchAiAgentComponents,
isAiKnowledgeChatSelectable,
resolveAiAgentContext,
} from "../../services/core/aiChat";
import {
fetchPublishedWorkflows,
fetchSpaces,
PublishedWorkflow,
SpaceInfo,
} from "../../services/core/marketplace";
import {
KnowledgeComponent,
KnowledgeConfigSaveParams,
KnowledgeDocumentInfo,
KnowledgeInfo,
KnowledgeModelOption,
KnowledgeQaInfo,
KnowledgeRawSegmentInfo,
addCustomKnowledgeDocument,
addKnowledgeDocuments,
addKnowledgeQa,
addKnowledgeRawSegment,
createKnowledgeConfig,
deleteKnowledgeConfig,
deleteKnowledgeDocument,
deleteKnowledgeQa,
deleteKnowledgeRawSegment,
downloadKnowledgeQaTemplate,
fetchEmbeddingModels,
fetchKnowledgeComponents,
fetchKnowledgeDetail,
fetchKnowledgeDocuments,
fetchKnowledgeQaList,
fetchKnowledgeRawSegments,
generateKnowledgeDocumentEmbeddings,
generateKnowledgeDocumentQa,
getKnowledgeId,
importKnowledgeQaExcel,
queryKnowledgeDocumentStatus,
retryKnowledgeDocumentTask,
updateKnowledgeConfig,
updateKnowledgeDocumentName,
updateKnowledgeQa,
updateKnowledgeRawSegment,
uploadKnowledgeAsset,
uploadKnowledgeFile,
validateKnowledgeFile,
} from "../../services/core/knowledge";
import styles from "../../styles/components/KnowledgePage.module.css";
const DOCUMENT_PAGE_SIZE = 48;
const SEGMENT_PAGE_SIZE = 30;
const QA_PAGE_SIZE = 20;
const KNOWLEDGE_ICON_MAX_SIZE = 2 * 1024 * 1024;
type DetailTabKey = "documents" | "segments" | "qa" | "graph";
interface KnowledgeFormValues {
name: string;
description?: string;
embeddingModelId: number;
dataParsingMethod?: "default" | "workflow";
workflowId?: number;
}
interface CustomDocumentValues {
name: string;
fileContent: string;
}
interface RenameDocumentValues {
name: string;
}
interface SegmentFormValues {
rawTxt: string;
sortIndex?: number;
}
interface QaFormValues {
question: string;
answer: string;
}
function formatDateTime(value?: string): string {
if (!value) return "-";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
return date.toLocaleString("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
}
function formatBytes(value?: number): string {
if (!value) return "0 B";
if (value >= 1024 * 1024 * 1024) {
return `${(value / 1024 / 1024 / 1024).toFixed(1)} GB`;
}
if (value >= 1024 * 1024) {
return `${(value / 1024 / 1024).toFixed(1)} MB`;
}
if (value >= 1024) return `${(value / 1024).toFixed(1)} KB`;
return `${value} B`;
}
function getInitial(name?: string): string {
const trimmed = (name || "").trim();
return trimmed ? trimmed.slice(0, 1).toUpperCase() : "知";
}
function getDocumentStatus(doc: KnowledgeDocumentInfo): {
color: string;
text: string;
} {
const code = Number(doc.docStatusCode || 0);
if (code === 2) return { color: "success", text: "分析成功" };
if (code === 10) return { color: "error", text: "分析失败" };
if ([1, 3, 4, 5].includes(code)) {
return { color: "processing", text: doc.docStatusDesc || "处理中" };
}
return {
color: "default",
text: doc.docStatusDesc || doc.docStatus || "未知",
};
}
function isKnowledgeComponent(component: AiManualComponent): boolean {
return String(component.type || "").toLowerCase() === "knowledge";
}
function getPublishedWorkflowId(workflow: PublishedWorkflow): number {
return Number(workflow.targetId || workflow.id);
}
export default function KnowledgePage() {
const [currentUserId, setCurrentUserId] = useState<number | null>(null);
const [spaces, setSpaces] = useState<SpaceInfo[]>([]);
const [selectedSpaceId, setSelectedSpaceId] = useState<number | null>(null);
const [knowledges, setKnowledges] = useState<KnowledgeComponent[]>([]);
const [loadingKnowledges, setLoadingKnowledges] = useState(false);
const [knowledgeKeyword, setKnowledgeKeyword] = useState("");
const [knowledgeError, setKnowledgeError] = useState<string | null>(null);
const [selectedKnowledgeId, setSelectedKnowledgeId] = useState<number | null>(
null,
);
const [knowledgeInfo, setKnowledgeInfo] = useState<KnowledgeInfo | null>(
null,
);
const [loadingDetail, setLoadingDetail] = useState(false);
const [activeTab, setActiveTab] = useState<DetailTabKey>("documents");
const [documents, setDocuments] = useState<KnowledgeDocumentInfo[]>([]);
const [documentTotal, setDocumentTotal] = useState(0);
const [documentKeyword, setDocumentKeyword] = useState("");
const [loadingDocuments, setLoadingDocuments] = useState(false);
const [selectedDocument, setSelectedDocument] =
useState<KnowledgeDocumentInfo | null>(null);
const [uploadingDocs, setUploadingDocs] = useState(false);
const [segments, setSegments] = useState<KnowledgeRawSegmentInfo[]>([]);
const [segmentTotal, setSegmentTotal] = useState(0);
const [loadingSegments, setLoadingSegments] = useState(false);
const [segmentModalOpen, setSegmentModalOpen] = useState(false);
const [editingSegment, setEditingSegment] =
useState<KnowledgeRawSegmentInfo | null>(null);
const [qaList, setQaList] = useState<KnowledgeQaInfo[]>([]);
const [qaTotal, setQaTotal] = useState(0);
const [qaKeyword, setQaKeyword] = useState("");
const [loadingQa, setLoadingQa] = useState(false);
const [qaModalOpen, setQaModalOpen] = useState(false);
const [editingQa, setEditingQa] = useState<KnowledgeQaInfo | null>(null);
const [models, setModels] = useState<KnowledgeModelOption[]>([]);
const [loadingModels, setLoadingModels] = useState(false);
const [parsingWorkflows, setParsingWorkflows] = useState<PublishedWorkflow[]>(
[],
);
const [loadingParsingWorkflows, setLoadingParsingWorkflows] = useState(false);
const [iconUrl, setIconUrl] = useState("");
const [uploadingIcon, setUploadingIcon] = useState(false);
const [knowledgeModalOpen, setKnowledgeModalOpen] = useState(false);
const [editingKnowledge, setEditingKnowledge] =
useState<KnowledgeInfo | null>(null);
const [savingKnowledge, setSavingKnowledge] = useState(false);
const [customDocModalOpen, setCustomDocModalOpen] = useState(false);
const [savingCustomDoc, setSavingCustomDoc] = useState(false);
const [renameDocModalOpen, setRenameDocModalOpen] = useState(false);
const [renamingDoc, setRenamingDoc] = useState<KnowledgeDocumentInfo | null>(
null,
);
const [savingRename, setSavingRename] = useState(false);
const [agentContext, setAgentContext] = useState<AiAgentContext | null>(null);
const [joinedKnowledgeMap, setJoinedKnowledgeMap] = useState<
Map<number, AiManualComponent>
>(new Map());
const [joiningIds, setJoiningIds] = useState<Set<number>>(() => new Set());
const [knowledgeForm] = Form.useForm<KnowledgeFormValues>();
const [customDocForm] = Form.useForm<CustomDocumentValues>();
const [renameDocForm] = Form.useForm<RenameDocumentValues>();
const [segmentForm] = Form.useForm<SegmentFormValues>();
const [qaForm] = Form.useForm<QaFormValues>();
const dataParsingMethod =
Form.useWatch("dataParsingMethod", knowledgeForm) || "default";
const iconFileInputRef = useRef<HTMLInputElement | null>(null);
const documentFileInputRef = useRef<HTMLInputElement | null>(null);
const qaFileInputRef = useRef<HTMLInputElement | null>(null);
const filteredKnowledges = useMemo(() => {
const keyword = knowledgeKeyword.trim().toLowerCase();
if (!keyword) return knowledges;
return knowledges.filter((item) =>
[item.name, item.description]
.filter(Boolean)
.some((value) => String(value).toLowerCase().includes(keyword)),
);
}, [knowledgeKeyword, knowledges]);
const selectedKnowledgeComponent = useMemo(
() =>
selectedKnowledgeId
? knowledges.find(
(item) => getKnowledgeId(item) === selectedKnowledgeId,
) || null
: null,
[knowledges, selectedKnowledgeId],
);
const isKnowledgeOwner = useCallback(
(knowledge?: KnowledgeComponent | KnowledgeInfo | null) => {
if (!knowledge) return false;
const creator = (knowledge as KnowledgeComponent).creator;
const creatorId = Number(knowledge.creatorId || creator?.id || 0);
return Boolean(currentUserId && creatorId && currentUserId === creatorId);
},
[currentUserId],
);
const canManageSelectedKnowledge = isKnowledgeOwner(
knowledgeInfo || selectedKnowledgeComponent,
);
const ensureCanManageSelectedKnowledge = useCallback(() => {
if (canManageSelectedKnowledge) return true;
message.warning("该知识库由其他用户创建,仅支持查看和加入对话");
return false;
}, [canManageSelectedKnowledge]);
const loadAgentComponents = useCallback(async () => {
try {
const context = await resolveAiAgentContext();
setAgentContext(context);
const components = await fetchAiAgentComponents(context.agentId);
const map = new Map<number, AiManualComponent>();
components.filter(isKnowledgeComponent).forEach((component) => {
const targetId = Number(component.targetId || 0);
if (targetId) map.set(targetId, component);
});
setJoinedKnowledgeMap(map);
} catch (error) {
console.warn("[KnowledgePage] Failed to load agent components:", error);
setAgentContext(null);
setJoinedKnowledgeMap(new Map());
}
}, []);
const loadSpaces = useCallback(async (): Promise<SpaceInfo[]> => {
try {
const result = await fetchSpaces();
const nextSpaces = result || [];
setSpaces(nextSpaces);
setSelectedSpaceId((prev) => prev || nextSpaces[0]?.id || null);
return nextSpaces;
} catch (error) {
console.warn("[KnowledgePage] Failed to load spaces:", error);
return [];
}
}, []);
const ensureDefaultSpaceId = useCallback(
async (showError = true): Promise<number | null> => {
if (selectedSpaceId) return selectedSpaceId;
const list = spaces.length > 0 ? spaces : await loadSpaces();
const defaultSpaceId = list[0]?.id || null;
if (defaultSpaceId) {
setSelectedSpaceId(defaultSpaceId);
return defaultSpaceId;
}
if (showError) {
message.error("无法获取默认工作区,暂时不能创建知识库");
}
return null;
},
[loadSpaces, selectedSpaceId, spaces],
);
const loadKnowledges = useCallback(async () => {
if (!selectedSpaceId) {
setKnowledges([]);
return;
}
setLoadingKnowledges(true);
setKnowledgeError(null);
try {
const list = await fetchKnowledgeComponents(selectedSpaceId);
setKnowledges(list);
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : "知识库加载失败";
setKnowledgeError(errorMessage);
setKnowledges([]);
} finally {
setLoadingKnowledges(false);
}
}, [selectedSpaceId]);
const loadKnowledgeDetail = useCallback(
async (knowledgeId = selectedKnowledgeId) => {
if (!knowledgeId) return;
setLoadingDetail(true);
try {
const detail = await fetchKnowledgeDetail(knowledgeId);
setKnowledgeInfo(detail);
} catch (error) {
message.error(
error instanceof Error ? error.message : "知识库详情加载失败",
);
} finally {
setLoadingDetail(false);
}
},
[selectedKnowledgeId],
);
const loadDocuments = useCallback(
async (keyword = documentKeyword) => {
if (!selectedKnowledgeId) return;
setLoadingDocuments(true);
try {
const result = await fetchKnowledgeDocuments({
kbId: selectedKnowledgeId,
name: keyword.trim() || undefined,
current: 1,
pageSize: DOCUMENT_PAGE_SIZE,
});
const records = result.records || [];
setDocuments(records);
setDocumentTotal(result.total || 0);
setSelectedDocument((prev) => {
if (prev && records.some((item) => item.id === prev.id)) return prev;
return records[0] || null;
});
} catch (error) {
message.error(
error instanceof Error ? error.message : "文档列表加载失败",
);
setDocuments([]);
setDocumentTotal(0);
setSelectedDocument(null);
} finally {
setLoadingDocuments(false);
}
},
[documentKeyword, selectedKnowledgeId],
);
const loadSegments = useCallback(async () => {
if (!selectedDocument) {
setSegments([]);
setSegmentTotal(0);
return;
}
setLoadingSegments(true);
try {
const result = await fetchKnowledgeRawSegments({
docId: selectedDocument.id,
current: 1,
pageSize: SEGMENT_PAGE_SIZE,
});
setSegments(result.records || []);
setSegmentTotal(result.total || 0);
} catch (error) {
message.error(error instanceof Error ? error.message : "分段加载失败");
setSegments([]);
setSegmentTotal(0);
} finally {
setLoadingSegments(false);
}
}, [selectedDocument]);
const loadQaList = useCallback(
async (keyword = qaKeyword) => {
if (!selectedKnowledgeId) return;
setLoadingQa(true);
try {
const result = await fetchKnowledgeQaList({
kbId: selectedKnowledgeId,
spaceId: selectedSpaceId || undefined,
question: keyword.trim() || undefined,
current: 1,
pageSize: QA_PAGE_SIZE,
});
setQaList(result.records || []);
setQaTotal(result.total || 0);
} catch (error) {
message.error(
error instanceof Error ? error.message : "QA 列表加载失败",
);
setQaList([]);
setQaTotal(0);
} finally {
setLoadingQa(false);
}
},
[qaKeyword, selectedKnowledgeId, selectedSpaceId],
);
const loadEmbeddingModels = useCallback(async (spaceId?: number) => {
setLoadingModels(true);
try {
const result = await fetchEmbeddingModels(spaceId);
setModels(result || []);
} catch (error) {
message.error(
error instanceof Error ? error.message : "嵌入模型加载失败",
);
setModels([]);
} finally {
setLoadingModels(false);
}
}, []);
const loadParsingWorkflows = useCallback(async (spaceId?: number) => {
setLoadingParsingWorkflows(true);
try {
const result = await fetchPublishedWorkflows({
page: 1,
pageSize: 80,
category: "all",
spaceId,
});
setParsingWorkflows(result.records || []);
} catch (error) {
message.error(
error instanceof Error ? error.message : "解析工作流加载失败",
);
setParsingWorkflows([]);
} finally {
setLoadingParsingWorkflows(false);
}
}, []);
useEffect(() => {
loadSpaces();
loadAgentComponents();
void getCurrentAuth().then((auth) => {
setCurrentUserId(auth.userInfo?.userId || null);
});
}, [loadAgentComponents, loadSpaces]);
useEffect(() => {
loadKnowledges();
}, [loadKnowledges]);
useEffect(() => {
if (!selectedKnowledgeId) return;
loadKnowledgeDetail(selectedKnowledgeId);
loadDocuments("");
}, [loadDocuments, loadKnowledgeDetail, selectedKnowledgeId]);
useEffect(() => {
if (activeTab === "segments") loadSegments();
}, [activeTab, loadSegments]);
useEffect(() => {
if (activeTab === "qa") loadQaList();
}, [activeTab, loadQaList]);
useEffect(() => {
if (!knowledgeModalOpen || dataParsingMethod !== "workflow") return;
if (loadingParsingWorkflows || parsingWorkflows.length > 0) return;
void (async () => {
const spaceId = selectedSpaceId || (await ensureDefaultSpaceId(false));
await loadParsingWorkflows(spaceId || undefined);
})();
}, [
dataParsingMethod,
ensureDefaultSpaceId,
knowledgeModalOpen,
loadParsingWorkflows,
loadingParsingWorkflows,
parsingWorkflows.length,
selectedSpaceId,
]);
const openKnowledgeForm = async (
knowledge?: KnowledgeComponent | KnowledgeInfo,
) => {
if (knowledge && !isKnowledgeOwner(knowledge)) {
message.warning("该知识库由其他用户创建,不能编辑");
return;
}
setKnowledgeModalOpen(true);
void (async () => {
const spaceId =
(knowledge as KnowledgeInfo | undefined)?.spaceId ||
selectedSpaceId ||
(await ensureDefaultSpaceId(false));
await loadEmbeddingModels(spaceId || undefined);
})();
if (knowledge) {
try {
const detail =
"embeddingModelId" in knowledge && knowledge.embeddingModelId
? (knowledge as KnowledgeInfo)
: await fetchKnowledgeDetail(getKnowledgeId(knowledge));
setEditingKnowledge(detail);
setIconUrl(detail.icon || "");
knowledgeForm.setFieldsValue({
name: detail.name,
description: detail.description,
embeddingModelId: detail.embeddingModelId,
dataParsingMethod: detail.workflowId ? "workflow" : "default",
workflowId: detail.workflowId ? Number(detail.workflowId) : undefined,
});
if (detail.workflowId) {
await loadParsingWorkflows(
detail.spaceId || selectedSpaceId || undefined,
);
setParsingWorkflows((prev) => {
const workflowId = Number(detail.workflowId);
if (
prev.some((item) => getPublishedWorkflowId(item) === workflowId)
) {
return prev;
}
return [
{
id: workflowId,
targetId: workflowId,
name: detail.workflowName || `工作流 ${workflowId}`,
description: detail.workflowDescription,
icon: detail.workflowIcon,
} as PublishedWorkflow,
...prev,
];
});
}
} catch (error) {
message.error(
error instanceof Error ? error.message : "知识库详情加载失败",
);
setKnowledgeModalOpen(false);
}
} else {
setEditingKnowledge(null);
setIconUrl("");
knowledgeForm.resetFields();
knowledgeForm.setFieldsValue({ dataParsingMethod: "default" });
}
};
const saveKnowledge = async () => {
if (editingKnowledge && !isKnowledgeOwner(editingKnowledge)) {
message.warning("该知识库由其他用户创建,不能编辑");
return;
}
let values: KnowledgeFormValues;
try {
values = await knowledgeForm.validateFields();
} catch {
return;
}
if (values.dataParsingMethod === "workflow" && !values.workflowId) {
message.error("请选择解析工作流");
return;
}
const spaceId =
editingKnowledge?.spaceId ||
selectedSpaceId ||
(await ensureDefaultSpaceId());
if (!spaceId) return;
setSavingKnowledge(true);
try {
const params: KnowledgeConfigSaveParams = {
id: editingKnowledge?.id,
spaceId,
name: values.name,
description: values.description || "",
embeddingModelId: values.embeddingModelId,
icon: iconUrl || "",
dataType: 1,
workflowId:
values.dataParsingMethod === "workflow" && values.workflowId
? values.workflowId
: null,
};
if (editingKnowledge?.id) {
await updateKnowledgeConfig(params);
message.success("知识库已更新");
await loadKnowledgeDetail(editingKnowledge.id);
} else {
const id = await createKnowledgeConfig(params);
message.success("知识库已创建");
setSelectedKnowledgeId(Number(id));
setActiveTab("documents");
}
setKnowledgeModalOpen(false);
await loadKnowledges();
} catch (error) {
if (error instanceof Error) message.error(error.message);
} finally {
setSavingKnowledge(false);
}
};
const confirmDeleteKnowledge = (knowledge: KnowledgeComponent) => {
if (!isKnowledgeOwner(knowledge)) {
message.warning("该知识库由其他用户创建,不能删除");
return;
}
const knowledgeId = getKnowledgeId(knowledge);
Modal.confirm({
title: "删除知识库",
content: `确认删除「${knowledge.name}」吗?该操作不可恢复。`,
okText: "删除",
okButtonProps: { danger: true },
cancelText: "取消",
onOk: async () => {
await deleteKnowledgeConfig(knowledgeId);
message.success("知识库已删除");
if (selectedKnowledgeId === knowledgeId) {
setSelectedKnowledgeId(null);
setKnowledgeInfo(null);
}
await loadKnowledges();
},
});
};
const joinKnowledgeToChat = async (
knowledge: KnowledgeComponent | KnowledgeInfo,
) => {
const knowledgeId = getKnowledgeId(knowledge);
const joinedComponent = joinedKnowledgeMap.get(knowledgeId);
if (joinedComponent && isAiKnowledgeChatSelectable(joinedComponent)) {
message.info("该知识库已加入对话");
return;
}
const context = agentContext || (await resolveAiAgentContext());
setAgentContext(context);
setJoiningIds((prev) => new Set(prev).add(knowledgeId));
try {
let componentId = joinedComponent?.id;
if (!componentId) {
componentId = await addAiAgentKnowledgeComponent({
agentId: context.agentId,
knowledgeId,
});
}
const resolvedComponentId = componentId;
const components = await fetchAiAgentComponents(context.agentId);
const component =
components.find((item) => item.id === resolvedComponentId) ||
components.find(
(item) =>
String(item.type || "").toLowerCase() === "knowledge" &&
Number(item.targetId || 0) === knowledgeId,
) ||
({
id: resolvedComponentId,
type: "Knowledge",
targetId: knowledgeId,
name: knowledge.name || "知识库",
icon: knowledge.icon,
description: knowledge.description,
} as AiManualComponent);
const enabledComponent = await enableAiAgentKnowledgeForChat(component);
setJoinedKnowledgeMap((prev) => {
const next = new Map(prev);
next.set(knowledgeId, enabledComponent);
return next;
});
message.success("知识库已加入 AI 对话");
await loadAgentComponents();
} catch (error) {
message.error(error instanceof Error ? error.message : "加入对话失败");
} finally {
setJoiningIds((prev) => {
const next = new Set(prev);
next.delete(knowledgeId);
return next;
});
}
};
const openKnowledgeDetail = (knowledge: KnowledgeComponent) => {
setSelectedKnowledgeId(getKnowledgeId(knowledge));
setKnowledgeInfo(null);
setSelectedDocument(null);
setDocuments([]);
setSegments([]);
setQaList([]);
setDocumentKeyword("");
setQaKeyword("");
setActiveTab("documents");
};
const handleIconFile = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
event.target.value = "";
if (!file) return;
const isValidImage =
file.type === "image/jpeg" ||
file.type === "image/jpg" ||
file.type === "image/png";
if (!isValidImage) {
message.error("图标仅支持 JPG、JPEG、PNG 格式");
return;
}
if (file.size > KNOWLEDGE_ICON_MAX_SIZE) {
message.error("图标不能超过 2MB");
return;
}
setUploadingIcon(true);
try {
const url = await uploadKnowledgeAsset(file);
setIconUrl(url);
message.success("图标已上传");
} catch (error) {
message.error(error instanceof Error ? error.message : "图标上传失败");
} finally {
setUploadingIcon(false);
}
};
const handleDocumentFiles = async (
event: React.ChangeEvent<HTMLInputElement>,
) => {
const files = Array.from(event.target.files || []);
event.target.value = "";
if (!files.length || !selectedKnowledgeId) return;
if (!ensureCanManageSelectedKnowledge()) return;
const validationError = files.map(validateKnowledgeFile).find(Boolean);
if (validationError) {
message.error(validationError);
return;
}
setUploadingDocs(true);
try {
const uploaded = await Promise.all(files.map(uploadKnowledgeFile));
await addKnowledgeDocuments({
kbId: selectedKnowledgeId,
files: uploaded,
});
message.success("文档已添加,后台将继续分析处理");
await loadDocuments();
} catch (error) {
message.error(error instanceof Error ? error.message : "文档上传失败");
} finally {
setUploadingDocs(false);
}
};
const saveCustomDocument = async () => {
if (!selectedKnowledgeId) return;
if (!ensureCanManageSelectedKnowledge()) return;
const values = await customDocForm.validateFields();
setSavingCustomDoc(true);
try {
await addCustomKnowledgeDocument({
kbId: selectedKnowledgeId,
name: values.name,
fileContent: values.fileContent,
});
message.success("自定义文本已添加");
setCustomDocModalOpen(false);
customDocForm.resetFields();
await loadDocuments();
} catch (error) {
message.error(error instanceof Error ? error.message : "添加文本失败");
} finally {
setSavingCustomDoc(false);
}
};
const openRenameDocument = (doc: KnowledgeDocumentInfo) => {
if (!ensureCanManageSelectedKnowledge()) return;
setRenamingDoc(doc);
renameDocForm.setFieldsValue({ name: doc.name });
setRenameDocModalOpen(true);
};
const saveDocumentName = async () => {
if (!renamingDoc) return;
if (!ensureCanManageSelectedKnowledge()) return;
const values = await renameDocForm.validateFields();
setSavingRename(true);
try {
await updateKnowledgeDocumentName({
docId: renamingDoc.id,
name: values.name,
});
message.success("文档名称已更新");
setRenameDocModalOpen(false);
await loadDocuments();
} catch (error) {
message.error(error instanceof Error ? error.message : "文档重命名失败");
} finally {
setSavingRename(false);
}
};
const refreshDocumentStatus = async () => {
if (documents.length === 0) return;
try {
const statusList = await queryKnowledgeDocumentStatus(
documents.map((doc) => doc.id),
);
const statusMap = new Map(statusList.map((item) => [item.id, item]));
setDocuments((prev) =>
prev.map((doc) => ({
...doc,
...(statusMap.get(doc.id) || {}),
})),
);
message.success("文档状态已刷新");
} catch {
await loadDocuments();
}
};
const confirmDeleteDocument = (doc: KnowledgeDocumentInfo) => {
if (!ensureCanManageSelectedKnowledge()) return;
Modal.confirm({
title: "删除文档",
content: `确认删除「${doc.name}」吗?`,
okText: "删除",
okButtonProps: { danger: true },
cancelText: "取消",
onOk: async () => {
await deleteKnowledgeDocument(doc.id);
message.success("文档已删除");
await loadDocuments();
},
});
};
const runDocumentAction = async (
doc: KnowledgeDocumentInfo,
action: "qa" | "embedding" | "retry",
) => {
if (!ensureCanManageSelectedKnowledge()) return;
try {
if (action === "qa") {
await generateKnowledgeDocumentQa(doc.id);
message.success("已提交生成 QA 任务");
} else if (action === "embedding") {
await generateKnowledgeDocumentEmbeddings(doc.id);
message.success("已提交向量化任务");
} else {
await retryKnowledgeDocumentTask(doc.id);
message.success("已提交重试任务");
}
await loadDocuments();
} catch (error) {
message.error(error instanceof Error ? error.message : "操作失败");
}
};
const openSegmentModal = (segment?: KnowledgeRawSegmentInfo) => {
if (!ensureCanManageSelectedKnowledge()) return;
setEditingSegment(segment || null);
segmentForm.setFieldsValue({
rawTxt: segment?.rawTxt || "",
sortIndex: segment?.sortIndex || segments.length,
});
setSegmentModalOpen(true);
};
const saveSegment = async () => {
if (!ensureCanManageSelectedKnowledge()) return;
const spaceId =
selectedDocument?.spaceId || knowledgeInfo?.spaceId || selectedSpaceId;
if (!selectedDocument || !spaceId) return;
const values = await segmentForm.validateFields();
try {
if (editingSegment) {
await updateKnowledgeRawSegment({
spaceId,
rawSegmentId: editingSegment.id,
docId: selectedDocument.id,
rawTxt: values.rawTxt,
sortIndex: values.sortIndex,
});
message.success("分段已更新");
} else {
await addKnowledgeRawSegment({
spaceId,
docId: selectedDocument.id,
rawTxt: values.rawTxt,
sortIndex: values.sortIndex,
});
message.success("分段已新增");
}
setSegmentModalOpen(false);
await loadSegments();
} catch (error) {
message.error(error instanceof Error ? error.message : "保存分段失败");
}
};
const confirmDeleteSegment = (segment: KnowledgeRawSegmentInfo) => {
if (!ensureCanManageSelectedKnowledge()) return;
Modal.confirm({
title: "删除分段",
content: "确认删除该分段吗?",
okText: "删除",
okButtonProps: { danger: true },
cancelText: "取消",
onOk: async () => {
await deleteKnowledgeRawSegment(segment.id);
message.success("分段已删除");
await loadSegments();
},
});
};
const openQaModal = (qa?: KnowledgeQaInfo) => {
if (!ensureCanManageSelectedKnowledge()) return;
setEditingQa(qa || null);
qaForm.setFieldsValue({
question: qa?.question || "",
answer: qa?.answer || "",
});
setQaModalOpen(true);
};
const saveQa = async () => {
if (!selectedKnowledgeId) return;
if (!ensureCanManageSelectedKnowledge()) return;
const values = await qaForm.validateFields();
try {
if (editingQa) {
await updateKnowledgeQa({
id: editingQa.id,
question: values.question,
answer: values.answer,
});
message.success("QA 已更新");
} else {
await addKnowledgeQa({
kbId: selectedKnowledgeId,
spaceId: selectedSpaceId || undefined,
question: values.question,
answer: values.answer,
});
message.success("QA 已新增");
}
setQaModalOpen(false);
await loadQaList();
} catch (error) {
message.error(error instanceof Error ? error.message : "保存 QA 失败");
}
};
const confirmDeleteQa = (qa: KnowledgeQaInfo) => {
if (!ensureCanManageSelectedKnowledge()) return;
Modal.confirm({
title: "删除 QA",
content: "确认删除该问答吗?",
okText: "删除",
okButtonProps: { danger: true },
cancelText: "取消",
onOk: async () => {
await deleteKnowledgeQa(qa.id);
message.success("QA 已删除");
await loadQaList();
},
});
};
const handleQaImport = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
event.target.value = "";
if (!file || !selectedKnowledgeId) return;
if (!ensureCanManageSelectedKnowledge()) return;
try {
await importKnowledgeQaExcel({ kbId: selectedKnowledgeId, file });
message.success("QA 已导入");
await loadQaList();
} catch (error) {
message.error(error instanceof Error ? error.message : "QA 导入失败");
}
};
const handleDownloadQaTemplate = async () => {
try {
const blob = await downloadKnowledgeQaTemplate();
const url = URL.createObjectURL(blob);
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = "知识库QA导入模板.xlsx";
anchor.click();
URL.revokeObjectURL(url);
} catch (error) {
message.error(error instanceof Error ? error.message : "模板下载失败");
}
};
const documentColumns: ColumnsType<KnowledgeDocumentInfo> = [
{
title: "文档",
dataIndex: "name",
ellipsis: true,
render: (name: string, record) => (
<button
type="button"
className={styles.linkButton}
onClick={() => {
setSelectedDocument(record);
setActiveTab("segments");
}}
>
<FileTextOutlined />
<span>{name}</span>
</button>
),
},
{
title: "状态",
width: 120,
render: (_, record) => {
const status = getDocumentStatus(record);
return <Tag color={status.color}>{status.text}</Tag>;
},
},
{
title: "QA",
width: 82,
render: (_, record) => (record.hasQa ? "已生成" : "未生成"),
},
{
title: "向量",
width: 82,
render: (_, record) => (record.hasEmbedding ? "已完成" : "未完成"),
},
{
title: "更新时间",
width: 170,
render: (_, record) => formatDateTime(record.modified || record.created),
},
{
title: "操作",
width: canManageSelectedKnowledge ? 260 : 120,
fixed: "right",
render: (_, record) => (
<div className={styles.tableActions}>
<Button
type="link"
size="small"
onClick={() => {
setSelectedDocument(record);
setActiveTab("segments");
}}
>
</Button>
{canManageSelectedKnowledge && (
<>
<Button
type="link"
size="small"
onClick={() => runDocumentAction(record, "qa")}
>
QA
</Button>
<Button
type="link"
size="small"
onClick={() => runDocumentAction(record, "embedding")}
>
</Button>
<Button
type="link"
size="small"
onClick={() => runDocumentAction(record, "retry")}
>
</Button>
<Button
type="link"
size="small"
onClick={() => openRenameDocument(record)}
>
</Button>
</>
)}
{record.docUrl && (
<Button
type="link"
size="small"
onClick={() => window.open(record.docUrl, "_blank")}
>
</Button>
)}
{canManageSelectedKnowledge && (
<Button
type="link"
size="small"
danger
onClick={() => confirmDeleteDocument(record)}
>
</Button>
)}
</div>
),
},
];
const segmentColumns: ColumnsType<KnowledgeRawSegmentInfo> = [
{
title: "序号",
dataIndex: "sortIndex",
width: 90,
render: (value, _, index) => value ?? index + 1,
},
{
title: "分段内容",
dataIndex: "rawTxt",
render: (value: string) => <p className={styles.segmentText}>{value}</p>,
},
{
title: "更新时间",
width: 170,
render: (_, record) => formatDateTime(record.modified || record.created),
},
...(canManageSelectedKnowledge
? [
{
title: "操作",
width: 120,
render: (_: unknown, record: KnowledgeRawSegmentInfo) => (
<div className={styles.tableActions}>
<Button
type="link"
size="small"
onClick={() => openSegmentModal(record)}
>
</Button>
<Button
type="link"
size="small"
danger
onClick={() => confirmDeleteSegment(record)}
>
</Button>
</div>
),
},
]
: []),
];
const qaColumns: ColumnsType<KnowledgeQaInfo> = [
{
title: "问题",
dataIndex: "question",
width: "34%",
render: (value: string) => <p className={styles.qaText}>{value}</p>,
},
{
title: "答案",
dataIndex: "answer",
render: (value: string) => <p className={styles.qaText}>{value}</p>,
},
{
title: "更新时间",
width: 170,
render: (_, record) => formatDateTime(record.modified || record.created),
},
...(canManageSelectedKnowledge
? [
{
title: "操作",
width: 120,
render: (_: unknown, record: KnowledgeQaInfo) => (
<div className={styles.tableActions}>
<Button
type="link"
size="small"
onClick={() => openQaModal(record)}
>
</Button>
<Button
type="link"
size="small"
danger
onClick={() => confirmDeleteQa(record)}
>
</Button>
</div>
),
},
]
: []),
];
const renderKnowledgeIcon = (
knowledge: KnowledgeComponent | KnowledgeInfo | null,
) => (
<ImageWithFallback
className={styles.knowledgeIconImage}
src={knowledge?.icon}
alt=""
fallback={
<span className={styles.knowledgeIconText}>
{getInitial(knowledge?.name)}
</span>
}
/>
);
const renderCreateButton = (variant: "header" | "toolbar" | "empty") => (
<Button
type="primary"
icon={<PlusOutlined />}
className={variant === "toolbar" ? styles.toolbarCreateButton : undefined}
onClick={() => openKnowledgeForm()}
>
</Button>
);
const renderList = () => (
<>
<header className={styles.header}>
<div>
<div className={styles.breadcrumb}>
<span>/</span> <strong></strong>
</div>
<div className={styles.titleRow}>
<h1></h1>
<p> QA AI </p>
</div>
</div>
{renderCreateButton("header")}
</header>
<main className={styles.content}>
<div className={styles.toolbar}>
<Input
allowClear
prefix={<SearchOutlined />}
placeholder="搜索知识库"
value={knowledgeKeyword}
onChange={(event) => setKnowledgeKeyword(event.target.value)}
className={styles.search}
/>
{renderCreateButton("toolbar")}
<div className={styles.count}>
<span>{filteredKnowledges.length}</span>
</div>
</div>
<div className={styles.listArea}>
{loadingKnowledges ? (
<div className={styles.centerState}>
<Spin />
<span>...</span>
</div>
) : knowledgeError ? (
<div className={styles.centerState}>
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={knowledgeError}
/>
<Button icon={<ReloadOutlined />} onClick={loadKnowledges}>
</Button>
</div>
) : filteredKnowledges.length === 0 ? (
<div className={styles.centerState}>
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description="暂无知识库"
/>
{renderCreateButton("empty")}
</div>
) : (
<div className={styles.grid}>
{filteredKnowledges.map((knowledge) => {
const knowledgeId = getKnowledgeId(knowledge);
const owned = isKnowledgeOwner(knowledge);
const creatorName =
knowledge.creator?.nickName ||
knowledge.creator?.userName ||
knowledge.creatorName ||
"未知创建人";
const joinedComponent = joinedKnowledgeMap.get(knowledgeId);
const joined =
!!joinedComponent &&
isAiKnowledgeChatSelectable(joinedComponent);
return (
<article
key={knowledgeId}
className={styles.card}
onClick={() => openKnowledgeDetail(knowledge)}
>
<div className={styles.cardHeader}>
<div className={styles.knowledgeIcon}>
{renderKnowledgeIcon(knowledge)}
</div>
<div className={styles.cardTitle}>
<h3>{knowledge.name}</h3>
<div className={styles.cardOwnerRow}>
<Tag
color={owned ? "cyan" : "default"}
className={styles.ownerTag}
>
{owned ? "我的知识库" : "只读"}
</Tag>
{!owned && (
<span title={creatorName}>
{creatorName}
</span>
)}
</div>
</div>
</div>
<p className={styles.cardDescription}>
{knowledge.description || "暂无描述"}
</p>
<div className={styles.cardMeta}>
<span>
{formatBytes(Number(knowledge.fileSize || 0))}
</span>
<span
title={formatDateTime(
knowledge.modified || knowledge.created,
)}
>
{" "}
{formatDateTime(
knowledge.modified || knowledge.created,
)}
</span>
</div>
<div className={styles.cardActions}>
<Button
type="primary"
size="small"
icon={joined ? <CheckOutlined /> : <TeamOutlined />}
loading={joiningIds.has(knowledgeId)}
onClick={(event) => {
event.stopPropagation();
joinKnowledgeToChat(knowledge);
}}
>
{joined ? "已加入" : "加入对话"}
</Button>
{owned && (
<div className={styles.cardOwnerActions}>
<Tooltip title="编辑知识库">
<Button
size="small"
icon={<EditOutlined />}
aria-label="编辑知识库"
onClick={(event) => {
event.stopPropagation();
openKnowledgeForm(knowledge);
}}
/>
</Tooltip>
<Tooltip title="删除知识库">
<Button
size="small"
danger
icon={<DeleteOutlined />}
aria-label="删除知识库"
onClick={(event) => {
event.stopPropagation();
confirmDeleteKnowledge(knowledge);
}}
/>
</Tooltip>
</div>
)}
</div>
</article>
);
})}
</div>
)}
</div>
</main>
</>
);
const renderDocuments = () => (
<div className={styles.tabPane}>
<div className={styles.tabToolbar}>
<Input.Search
allowClear
placeholder="搜索文档"
value={documentKeyword}
onChange={(event) => {
const value = event.target.value;
setDocumentKeyword(value);
if (!value) void loadDocuments("");
}}
onSearch={(value) => loadDocuments(value)}
className={styles.innerSearch}
/>
<input
ref={documentFileInputRef}
type="file"
multiple
className={styles.hiddenInput}
onChange={handleDocumentFiles}
/>
{canManageSelectedKnowledge && (
<>
<Button
icon={<CloudUploadOutlined />}
loading={uploadingDocs}
onClick={() => documentFileInputRef.current?.click()}
>
</Button>
<Button
icon={<FileAddOutlined />}
onClick={() => {
customDocForm.resetFields();
setCustomDocModalOpen(true);
}}
>
</Button>
</>
)}
<Tooltip title="刷新文档状态">
<Button icon={<ReloadOutlined />} onClick={refreshDocumentStatus} />
</Tooltip>
</div>
<Table
rowKey="id"
size="middle"
loading={loadingDocuments}
columns={documentColumns}
dataSource={documents}
scroll={{ x: 980 }}
pagination={{
pageSize: DOCUMENT_PAGE_SIZE,
total: documentTotal,
showSizeChanger: false,
}}
locale={{
emptyText: (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description="暂无文档"
/>
),
}}
/>
</div>
);
const renderSegments = () => (
<div className={styles.tabPane}>
<div className={styles.tabToolbar}>
<Select
value={selectedDocument?.id}
placeholder="选择文档查看分段"
className={styles.documentSelect}
onChange={(id) => {
setSelectedDocument(documents.find((doc) => doc.id === id) || null);
}}
options={documents.map((doc) => ({ label: doc.name, value: doc.id }))}
/>
{canManageSelectedKnowledge && (
<Button
icon={<PlusOutlined />}
disabled={!selectedDocument}
onClick={() => openSegmentModal()}
>
</Button>
)}
<Button
icon={<ReloadOutlined />}
disabled={!selectedDocument}
onClick={loadSegments}
>
</Button>
<span className={styles.mutedText}> {segmentTotal} </span>
</div>
{!selectedDocument ? (
<div className={styles.centerState}>
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description="请先选择文档"
/>
</div>
) : (
<Table
rowKey="id"
size="middle"
loading={loadingSegments}
columns={segmentColumns}
dataSource={segments}
pagination={{
pageSize: SEGMENT_PAGE_SIZE,
total: segmentTotal,
showSizeChanger: false,
}}
locale={{
emptyText: (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description="暂无分段"
/>
),
}}
/>
)}
</div>
);
const renderQa = () => (
<div className={styles.tabPane}>
<div className={styles.tabToolbar}>
<Input.Search
allowClear
placeholder="搜索问题"
value={qaKeyword}
onChange={(event) => {
const value = event.target.value;
setQaKeyword(value);
if (!value) void loadQaList("");
}}
onSearch={(value) => loadQaList(value)}
className={styles.innerSearch}
/>
{canManageSelectedKnowledge && (
<Button icon={<PlusOutlined />} onClick={() => openQaModal()}>
QA
</Button>
)}
<input
ref={qaFileInputRef}
type="file"
accept=".xls,.xlsx"
className={styles.hiddenInput}
onChange={handleQaImport}
/>
{canManageSelectedKnowledge && (
<Button
icon={<CloudUploadOutlined />}
onClick={() => qaFileInputRef.current?.click()}
>
Excel
</Button>
)}
<Button icon={<DownloadOutlined />} onClick={handleDownloadQaTemplate}>
</Button>
<Button icon={<ReloadOutlined />} onClick={() => loadQaList()}>
</Button>
<span className={styles.mutedText}> {qaTotal} QA</span>
</div>
<Table
rowKey="id"
size="middle"
loading={loadingQa}
columns={qaColumns}
dataSource={qaList}
scroll={{ x: 900 }}
pagination={{
pageSize: QA_PAGE_SIZE,
total: qaTotal,
showSizeChanger: false,
}}
locale={{
emptyText: (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无 QA" />
),
}}
/>
</div>
);
const renderGraphPlaceholder = () => (
<div className={styles.graphPlaceholder}>
<DatabaseOutlined />
<h3></h3>
<p></p>
</div>
);
const renderDetail = () => {
const joined =
selectedKnowledgeId !== null &&
!!joinedKnowledgeMap.get(selectedKnowledgeId) &&
isAiKnowledgeChatSelectable(joinedKnowledgeMap.get(selectedKnowledgeId)!);
const joining =
selectedKnowledgeId !== null && joiningIds.has(selectedKnowledgeId);
const knowledgeForAction = knowledgeInfo || selectedKnowledgeComponent;
const owned = isKnowledgeOwner(knowledgeForAction);
const creator = (knowledgeForAction as KnowledgeComponent | null)?.creator;
const creatorName =
creator?.nickName ||
creator?.userName ||
knowledgeForAction?.creatorName ||
"未知创建人";
return (
<>
<header className={styles.detailHeader}>
<Button
type="text"
icon={<ArrowLeftOutlined />}
onClick={() => setSelectedKnowledgeId(null)}
>
</Button>
<div className={styles.detailTitleBlock}>
<div className={styles.detailIcon}>
{renderKnowledgeIcon(knowledgeForAction)}
</div>
<div>
<div className={styles.breadcrumb}>
<span>/</span>{" "}
<strong>{knowledgeForAction?.name || "知识库详情"}</strong>
</div>
<h1>{knowledgeForAction?.name || "知识库详情"}</h1>
<div className={styles.detailOwnerRow}>
<Tag color={owned ? "cyan" : "default"}>
{owned ? "我的知识库" : "只读知识库"}
</Tag>
{!owned && <span>{creatorName}</span>}
</div>
<p>{knowledgeForAction?.description || "暂无描述"}</p>
</div>
</div>
<div className={styles.detailActions}>
<Button
icon={joined ? <CheckOutlined /> : <TeamOutlined />}
loading={joining}
disabled={!knowledgeForAction}
onClick={() =>
knowledgeForAction && joinKnowledgeToChat(knowledgeForAction)
}
>
{joined ? "已加入对话" : "加入对话"}
</Button>
{owned && (
<Button
icon={<EditOutlined />}
disabled={!knowledgeForAction}
onClick={() =>
knowledgeForAction && openKnowledgeForm(knowledgeForAction)
}
>
</Button>
)}
</div>
</header>
<main className={styles.detailContent}>
{loadingDetail && !knowledgeInfo ? (
<div className={styles.centerState}>
<Spin />
<span>...</span>
</div>
) : (
<>
<div className={styles.statRow}>
<div className={styles.statCard}>
<span></span>
<strong>{documentTotal}</strong>
</div>
<div className={styles.statCard}>
<span></span>
<strong>
{formatBytes(Number(knowledgeInfo?.fileSize || 0))}
</strong>
</div>
<div className={styles.statCard}>
<span></span>
<strong>{knowledgeInfo?.pubStatus || "Published"}</strong>
</div>
<div className={styles.statCard}>
<span></span>
<strong>
{formatDateTime(
knowledgeInfo?.modified || knowledgeInfo?.created,
)}
</strong>
</div>
</div>
<Tabs
activeKey={activeTab}
onChange={(key) => setActiveTab(key as DetailTabKey)}
items={[
{
key: "documents",
label: "文档",
children: renderDocuments(),
},
{
key: "segments",
label: "分段",
children: renderSegments(),
},
{
key: "qa",
label: "QA",
children: renderQa(),
},
{
key: "graph",
label: "知识图谱",
children: renderGraphPlaceholder(),
},
]}
/>
</>
)}
</main>
</>
);
};
return (
<div className={styles.page}>
{selectedKnowledgeId ? renderDetail() : renderList()}
<Modal
open={knowledgeModalOpen}
title={editingKnowledge ? "编辑知识库" : "新建知识库"}
okText={editingKnowledge ? "保存" : "创建"}
cancelText="取消"
confirmLoading={savingKnowledge}
onOk={saveKnowledge}
onCancel={() => setKnowledgeModalOpen(false)}
destroyOnClose
width={620}
>
<input
ref={iconFileInputRef}
type="file"
accept=".jpg,.jpeg,.png,image/jpeg,image/png"
className={styles.hiddenInput}
onChange={handleIconFile}
/>
<Form
form={knowledgeForm}
layout="vertical"
preserve={false}
initialValues={{ dataParsingMethod: "default" }}
>
<Form.Item
name="name"
label="知识库名称"
rules={[
{ required: true, message: "请输入知识库名称" },
{ max: 100, message: "知识库名称不能超过 100 个字符" },
]}
>
<Input placeholder="请输入知识库名称" showCount maxLength={100} />
</Form.Item>
<Form.Item name="description" label="描述">
<Input.TextArea
placeholder="请输入知识库描述"
showCount
maxLength={10000}
autoSize={{ minRows: 3, maxRows: 5 }}
/>
</Form.Item>
<Form.Item
name="embeddingModelId"
label="嵌入模型"
rules={[{ required: true, message: "请选择嵌入模型" }]}
tooltip="用于文档向量化和检索召回,请选择 Embeddings 类型模型"
>
<Select
loading={loadingModels}
placeholder="请选择嵌入模型"
allowClear
showSearch
optionFilterProp="label"
notFoundContent={
loadingModels ? (
<Spin size="small" />
) : (
"暂无向量模型,请先配置 Embeddings 模型"
)
}
options={models.map((model) => ({
label: model.name || model.model || `模型 ${model.id}`,
value: model.id,
}))}
/>
</Form.Item>
<Form.Item
name="dataParsingMethod"
label="文档解析方式"
tooltip="系统默认解析适合常规文档;自定义工作流解析适合需要专门抽取、清洗或结构化处理的文档"
>
<Select
placeholder="请选择文档解析方式"
options={[
{ label: "系统默认", value: "default" },
{ label: "自定义工作流", value: "workflow" },
]}
onChange={(value) => {
if (value === "default") {
knowledgeForm.setFieldValue("workflowId", undefined);
return;
}
void (async () => {
const spaceId =
selectedSpaceId || (await ensureDefaultSpaceId(false));
await loadParsingWorkflows(spaceId || undefined);
})();
}}
/>
</Form.Item>
{dataParsingMethod === "workflow" && (
<Form.Item
name="workflowId"
label="解析工作流"
rules={[{ required: true, message: "请选择解析工作流" }]}
>
<Select
loading={loadingParsingWorkflows}
placeholder="请选择用于文档解析的工作流"
allowClear
showSearch
optionFilterProp="label"
notFoundContent={
loadingParsingWorkflows ? (
<Spin size="small" />
) : (
"暂无可用工作流,请先发布工作流"
)
}
options={parsingWorkflows.map((workflow) => ({
label:
workflow.name ||
`工作流 ${getPublishedWorkflowId(workflow)}`,
value: getPublishedWorkflowId(workflow),
}))}
/>
</Form.Item>
)}
<Form.Item label="图标">
<div className={styles.iconUploadRow}>
<button
type="button"
className={styles.iconUploadCard}
disabled={uploadingIcon}
onClick={() => iconFileInputRef.current?.click()}
>
<span className={styles.iconPreview}>
<ImageWithFallback
src={iconUrl}
alt=""
fallback={<PictureOutlined />}
/>
</span>
<span className={styles.iconUploadText}>
<strong>
{uploadingIcon
? "图标上传中..."
: iconUrl
? "已上传图标"
: "默认知识库图标"}
</strong>
<span> JPGPNG 2MB</span>
</span>
<UploadOutlined />
</button>
<Button
disabled={!iconUrl || uploadingIcon}
onClick={() => setIconUrl("")}
>
</Button>
</div>
</Form.Item>
</Form>
</Modal>
<Modal
open={customDocModalOpen}
title="新增自定义文本"
okText="添加"
cancelText="取消"
confirmLoading={savingCustomDoc}
onOk={saveCustomDocument}
onCancel={() => setCustomDocModalOpen(false)}
destroyOnClose
>
<Form form={customDocForm} layout="vertical" preserve={false}>
<Form.Item
name="name"
label="文档名称"
rules={[{ required: true, message: "请输入文档名称" }]}
>
<Input placeholder="请输入文档名称" />
</Form.Item>
<Form.Item
name="fileContent"
label="文本内容"
rules={[{ required: true, message: "请输入文本内容" }]}
>
<Input.TextArea
placeholder="请输入文本内容"
autoSize={{ minRows: 8, maxRows: 14 }}
/>
</Form.Item>
</Form>
</Modal>
<Modal
open={renameDocModalOpen}
title="重命名文档"
okText="保存"
cancelText="取消"
confirmLoading={savingRename}
onOk={saveDocumentName}
onCancel={() => setRenameDocModalOpen(false)}
destroyOnClose
>
<Form form={renameDocForm} layout="vertical" preserve={false}>
<Form.Item
name="name"
label="文档名称"
rules={[{ required: true, message: "请输入文档名称" }]}
>
<Input placeholder="请输入文档名称" />
</Form.Item>
</Form>
</Modal>
<Modal
open={segmentModalOpen}
title={editingSegment ? "编辑分段" : "新增分段"}
okText="保存"
cancelText="取消"
onOk={saveSegment}
onCancel={() => setSegmentModalOpen(false)}
destroyOnClose
width={720}
>
<Form form={segmentForm} layout="vertical" preserve={false}>
<Form.Item name="sortIndex" label="排序">
<Input type="number" placeholder="排序序号" />
</Form.Item>
<Form.Item
name="rawTxt"
label="分段内容"
rules={[{ required: true, message: "请输入分段内容" }]}
>
<Input.TextArea
placeholder="请输入分段内容"
autoSize={{ minRows: 8, maxRows: 16 }}
/>
</Form.Item>
</Form>
</Modal>
<Modal
open={qaModalOpen}
title={editingQa ? "编辑 QA" : "新增 QA"}
okText="保存"
cancelText="取消"
onOk={saveQa}
onCancel={() => setQaModalOpen(false)}
destroyOnClose
width={720}
>
<Form form={qaForm} layout="vertical" preserve={false}>
<Form.Item
name="question"
label="问题"
rules={[{ required: true, message: "请输入问题" }]}
>
<Input.TextArea
placeholder="请输入问题"
autoSize={{ minRows: 3, maxRows: 6 }}
/>
</Form.Item>
<Form.Item
name="answer"
label="答案"
rules={[{ required: true, message: "请输入答案" }]}
>
<Input.TextArea
placeholder="请输入答案"
autoSize={{ minRows: 6, maxRows: 12 }}
/>
</Form.Item>
</Form>
</Modal>
</div>
);
}