From ba742b7b1fd900186b3ead99dce256480b676fb5 Mon Sep 17 00:00:00 2001 From: one Date: Thu, 10 Jul 2025 21:34:01 +0800 Subject: [PATCH] feat: save to knowledge (#7528) * feat: save to knowledge * refactor: simplify checkbox * feat(i18n): add 'Save to Local File' translation key for multiple languages --------- Co-authored-by: suyao --- .../Popups/SaveToKnowledgePopup.tsx | 353 ++++++++++++++++++ src/renderer/src/i18n/locales/en-us.json | 29 ++ src/renderer/src/i18n/locales/ja-jp.json | 31 +- src/renderer/src/i18n/locales/ru-ru.json | 31 +- src/renderer/src/i18n/locales/zh-cn.json | 29 ++ src/renderer/src/i18n/locales/zh-tw.json | 33 +- .../pages/home/Messages/MessageMenubar.tsx | 32 +- src/renderer/src/utils/knowledge.ts | 269 +++++++++++++ 8 files changed, 794 insertions(+), 13 deletions(-) create mode 100644 src/renderer/src/components/Popups/SaveToKnowledgePopup.tsx create mode 100644 src/renderer/src/utils/knowledge.ts diff --git a/src/renderer/src/components/Popups/SaveToKnowledgePopup.tsx b/src/renderer/src/components/Popups/SaveToKnowledgePopup.tsx new file mode 100644 index 0000000000..b6f0577c4f --- /dev/null +++ b/src/renderer/src/components/Popups/SaveToKnowledgePopup.tsx @@ -0,0 +1,353 @@ +import CustomTag from '@renderer/components/CustomTag' +import { TopView } from '@renderer/components/TopView' +import Logger from '@renderer/config/logger' +import { useKnowledge, useKnowledgeBases } from '@renderer/hooks/useKnowledge' +import { Message } from '@renderer/types/newMessage' +import { + analyzeMessageContent, + CONTENT_TYPES, + ContentType, + MessageContentStats, + processMessageContent +} from '@renderer/utils/knowledge' +import { Flex, Form, Modal, Select, Tooltip, Typography } from 'antd' +import { Check, CircleHelp } from 'lucide-react' +import { useEffect, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +const { Text } = Typography + +// 内容类型配置 +const CONTENT_TYPE_CONFIG = { + [CONTENT_TYPES.TEXT]: { + label: 'chat.save.knowledge.content.maintext.title', + description: 'chat.save.knowledge.content.maintext.description' + }, + [CONTENT_TYPES.CODE]: { + label: 'chat.save.knowledge.content.code.title', + description: 'chat.save.knowledge.content.code.description' + }, + [CONTENT_TYPES.THINKING]: { + label: 'chat.save.knowledge.content.thinking.title', + description: 'chat.save.knowledge.content.thinking.description' + }, + [CONTENT_TYPES.TOOL_USE]: { + label: 'chat.save.knowledge.content.tool_use.title', + description: 'chat.save.knowledge.content.tool_use.description' + }, + [CONTENT_TYPES.CITATION]: { + label: 'chat.save.knowledge.content.citation.title', + description: 'chat.save.knowledge.content.citation.description' + }, + [CONTENT_TYPES.TRANSLATION]: { + label: 'chat.save.knowledge.content.translation.title', + description: 'chat.save.knowledge.content.translation.description' + }, + [CONTENT_TYPES.ERROR]: { + label: 'chat.save.knowledge.content.error.title', + description: 'chat.save.knowledge.content.error.description' + }, + [CONTENT_TYPES.FILE]: { + label: 'chat.save.knowledge.content.file.title', + description: 'chat.save.knowledge.content.file.description' + } +} as const + +// Tag 颜色常量 +const TAG_COLORS = { + SELECTED: '#008001', + UNSELECTED: '#8c8c8c' +} as const + +interface ContentTypeOption { + type: ContentType + label: string + count: number + enabled: boolean + description?: string +} + +interface ShowParams { + message: Message + title?: string +} + +interface SaveResult { + success: boolean + savedCount: number +} + +interface Props extends ShowParams { + resolve: (data: SaveResult | null) => void +} + +const PopupContainer: React.FC = ({ message, title, resolve }) => { + const [open, setOpen] = useState(true) + const [loading, setLoading] = useState(false) + const [selectedBaseId, setSelectedBaseId] = useState() + const [selectedTypes, setSelectedTypes] = useState([]) + const [hasInitialized, setHasInitialized] = useState(false) + const { bases } = useKnowledgeBases() + const { addNote, addFiles } = useKnowledge(selectedBaseId || '') + const { t } = useTranslation() + + // 分析消息内容统计 + const contentStats = useMemo(() => analyzeMessageContent(message), [message]) + + // 生成内容类型选项(只显示有内容的类型) + const contentTypeOptions: ContentTypeOption[] = useMemo(() => { + return Object.entries(CONTENT_TYPE_CONFIG) + .map(([type, config]) => { + const contentType = type as ContentType + const count = contentStats[contentType as keyof MessageContentStats] || 0 + return { + type: contentType, + count, + enabled: count > 0, + label: t(config.label), + description: t(config.description) + } + }) + .filter((option) => option.enabled) // 只显示有内容的类型 + }, [contentStats, t]) + + // 知识库选项 + const knowledgeBaseOptions = useMemo( + () => + bases.map((base) => ({ + label: base.name, + value: base.id, + disabled: !base.version // 如果知识库没有配置好就禁用 + })), + [bases] + ) + + // 合并状态计算 + const formState = useMemo(() => { + const hasValidBase = selectedBaseId && bases.find((base) => base.id === selectedBaseId)?.version + const hasContent = contentTypeOptions.length > 0 + const selectedCount = contentTypeOptions + .filter((option) => selectedTypes.includes(option.type)) + .reduce((sum, option) => sum + option.count, 0) + + return { + hasValidBase, + hasContent, + canSubmit: hasValidBase && selectedTypes.length > 0 && hasContent, + selectedCount, + hasNoSelection: selectedTypes.length === 0 && hasContent + } + }, [selectedBaseId, bases, contentTypeOptions, selectedTypes]) + + // 默认选择第一个可用的知识库 + useEffect(() => { + if (!selectedBaseId) { + const firstAvailableBase = bases.find((base) => base.version) + if (firstAvailableBase) { + setSelectedBaseId(firstAvailableBase.id) + } + } + }, [bases, selectedBaseId]) + + // 默认选择所有可用的内容类型(仅在初始化时) + useEffect(() => { + if (!hasInitialized && contentTypeOptions.length > 0) { + const availableTypes = contentTypeOptions.map((option) => option.type) + setSelectedTypes(availableTypes) + setHasInitialized(true) + } + }, [contentTypeOptions, hasInitialized]) + + // 计算UI状态 + const uiState = useMemo(() => { + if (!formState.hasContent) { + return { type: 'empty', message: t('chat.save.knowledge.empty.no_content') } + } + if (bases.length === 0) { + return { type: 'empty', message: t('chat.save.knowledge.empty.no_knowledge_base') } + } + return { type: 'form' } + }, [formState.hasContent, bases.length, t]) + + // 处理内容类型选择切换 + const handleContentTypeToggle = (type: ContentType) => { + setSelectedTypes((prev) => (prev.includes(type) ? prev.filter((t) => t !== type) : [...prev, type])) + } + + const onOk = async () => { + if (!formState.canSubmit) { + return + } + + setLoading(true) + let savedCount = 0 + + try { + const result = processMessageContent(message, selectedTypes) + + // 保存文本内容 + if (result.text.trim() && selectedTypes.some((type) => type !== CONTENT_TYPES.FILE)) { + await addNote(result.text) + savedCount++ + } + + // 保存文件 + if (result.files.length > 0 && selectedTypes.includes(CONTENT_TYPES.FILE)) { + addFiles(result.files) + savedCount += result.files.length + } + + setOpen(false) + resolve({ success: true, savedCount }) + } catch (error) { + Logger.error('[SaveToKnowledgePopup] save failed:', error) + window.message.error(t('chat.save.knowledge.error.save_failed')) + setLoading(false) + } + } + + const onCancel = () => { + setOpen(false) + } + + const onClose = () => { + resolve(null) + } + + // 渲染空状态 + const renderEmptyState = () => ( + + {uiState.message} + + ) + + // 渲染表单内容 + const renderFormContent = () => ( + <> +
+ +