From 7ed6e58f8e80b94be6f5fb960327d8d83a41de61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=A2=E5=A5=8B=E7=8C=AB?= Date: Wed, 2 Jul 2025 17:34:19 +0800 Subject: [PATCH] refactor: new knowledge base ui layout (#7748) --- .../src/pages/knowledge/KnowledgeContent.tsx | 728 ++++-------------- .../src/pages/knowledge/KnowledgePage.tsx | 6 +- .../knowledge/items/KnowledgeDirectories.tsx | 128 +++ .../pages/knowledge/items/KnowledgeFiles.tsx | 194 +++++ .../pages/knowledge/items/KnowledgeNotes.tsx | 107 +++ .../knowledge/items/KnowledgeSitemaps.tsx | 142 ++++ .../pages/knowledge/items/KnowledgeUrls.tsx | 190 +++++ 7 files changed, 913 insertions(+), 582 deletions(-) create mode 100644 src/renderer/src/pages/knowledge/items/KnowledgeDirectories.tsx create mode 100644 src/renderer/src/pages/knowledge/items/KnowledgeFiles.tsx create mode 100644 src/renderer/src/pages/knowledge/items/KnowledgeNotes.tsx create mode 100644 src/renderer/src/pages/knowledge/items/KnowledgeSitemaps.tsx create mode 100644 src/renderer/src/pages/knowledge/items/KnowledgeUrls.tsx diff --git a/src/renderer/src/pages/knowledge/KnowledgeContent.tsx b/src/renderer/src/pages/knowledge/KnowledgeContent.tsx index 7d5d251660..71e0125c8d 100644 --- a/src/renderer/src/pages/knowledge/KnowledgeContent.tsx +++ b/src/renderer/src/pages/knowledge/KnowledgeContent.tsx @@ -1,233 +1,90 @@ -import { CopyOutlined, DeleteOutlined, EditOutlined, RedoOutlined } from '@ant-design/icons' +import { RedoOutlined } from '@ant-design/icons' import CustomTag from '@renderer/components/CustomTag' -import Ellipsis from '@renderer/components/Ellipsis' import { HStack } from '@renderer/components/Layout' -import PromptPopup from '@renderer/components/Popups/PromptPopup' -import TextEditPopup from '@renderer/components/Popups/TextEditPopup' -import Scrollbar from '@renderer/components/Scrollbar' -import Logger from '@renderer/config/logger' import { useKnowledge } from '@renderer/hooks/useKnowledge' -import FileManager from '@renderer/services/FileManager' +import { NavbarIcon } from '@renderer/pages/home/Navbar' import { getProviderName } from '@renderer/services/ProviderService' -import { FileType, FileTypes, KnowledgeBase, KnowledgeItem } from '@renderer/types' -import { formatFileSize } from '@renderer/utils' -import { bookExts, documentExts, textExts, thirdPartyApplicationExts } from '@shared/config/constant' -import { Alert, Button, Dropdown, Empty, message, Tag, Tooltip, Upload } from 'antd' -import dayjs from 'dayjs' -import { ChevronsDown, ChevronsUp, Plus, Search, Settings2 } from 'lucide-react' -import VirtualList from 'rc-virtual-list' +import { KnowledgeBase } from '@renderer/types' +import { Button, Empty, Tabs, Tag, Tooltip } from 'antd' +import { Book, Folder, Globe, Link, Notebook, Search, Settings } from 'lucide-react' import { FC, useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' -import CustomCollapse from '../../components/CustomCollapse' -import FileItem from '../files/FileItem' -import { NavbarIcon } from '../home/Navbar' import KnowledgeSearchPopup from './components/KnowledgeSearchPopup' import KnowledgeSettingsPopup from './components/KnowledgeSettingsPopup' -import StatusIcon from './components/StatusIcon' - -const { Dragger } = Upload +import KnowledgeDirectories from './items/KnowledgeDirectories' +import KnowledgeFiles from './items/KnowledgeFiles' +import KnowledgeNotes from './items/KnowledgeNotes' +import KnowledgeSitemaps from './items/KnowledgeSitemaps' +import KnowledgeUrls from './items/KnowledgeUrls' interface KnowledgeContentProps { selectedBase: KnowledgeBase } -const fileTypes = [...bookExts, ...thirdPartyApplicationExts, ...documentExts, ...textExts] - -const getDisplayTime = (item: KnowledgeItem) => { - const timestamp = item.updated_at && item.updated_at > item.created_at ? item.updated_at : item.created_at - return dayjs(timestamp).format('MM-DD HH:mm') -} - const KnowledgeContent: FC = ({ selectedBase }) => { const { t } = useTranslation() - const [expandAll, setExpandAll] = useState(false) - - const { - base, - noteItems, - fileItems, - urlItems, - sitemapItems, - directoryItems, - addFiles, - updateNoteContent, - refreshItem, - addUrl, - addSitemap, - removeItem, - getProcessingStatus, - getDirectoryProcessingPercent, - addNote, - addDirectory, - updateItem - } = useKnowledge(selectedBase.id || '') + const { base, urlItems, fileItems, directoryItems, noteItems, sitemapItems } = useKnowledge(selectedBase.id || '') + const [activeKey, setActiveKey] = useState('files') const providerName = getProviderName(base?.model.provider || '') - const disabled = !base?.version || !providerName + + const knowledgeItems = [ + { + key: 'files', + title: t('files.title'), + icon: activeKey === 'files' ? : , + items: fileItems, + content: + }, + { + key: 'notes', + title: t('knowledge.notes'), + icon: activeKey === 'notes' ? : , + items: noteItems, + content: + }, + { + key: 'directories', + title: t('knowledge.directories'), + icon: activeKey === 'directories' ? : , + items: directoryItems, + content: + }, + { + key: 'urls', + title: t('knowledge.urls'), + icon: activeKey === 'urls' ? : , + items: urlItems, + content: + }, + { + key: 'sitemaps', + title: t('knowledge.sitemaps'), + icon: activeKey === 'sitemaps' ? : , + items: sitemapItems, + content: + } + ] if (!base) { return null } - const getProgressingPercentForItem = (itemId: string) => getDirectoryProcessingPercent(itemId) - - const handleAddFile = () => { - if (disabled) { - return - } - const input = document.createElement('input') - input.type = 'file' - input.multiple = true - input.accept = fileTypes.join(',') - input.onchange = (e) => { - const files = (e.target as HTMLInputElement).files - files && handleDrop(Array.from(files)) - } - input.click() - } - - const handleDrop = async (files: File[]) => { - if (disabled) { - return - } - - if (files) { - const _files: FileType[] = files - .map((file) => ({ - id: file.name, - name: file.name, - path: window.api.file.getPathForFile(file), - size: file.size, - ext: `.${file.name.split('.').pop()}`.toLowerCase(), - count: 1, - origin_name: file.name, - type: file.type as FileTypes, - created_at: new Date().toISOString() - })) - .filter(({ ext }) => fileTypes.includes(ext)) - const uploadedFiles = await FileManager.uploadFiles(_files) - addFiles(uploadedFiles) - } - } - - const handleAddUrl = async () => { - if (disabled) { - return - } - - const urlInput = await PromptPopup.show({ - title: t('knowledge.add_url'), - message: '', - inputPlaceholder: t('knowledge.url_placeholder'), - inputProps: { - rows: 10, - onPressEnter: () => {} - } - }) - - if (urlInput) { - // Split input by newlines and filter out empty lines - const urls = urlInput.split('\n').filter((url) => url.trim()) - - for (const url of urls) { - try { - new URL(url.trim()) - if (!urlItems.find((item) => item.content === url.trim())) { - addUrl(url.trim()) - } else { - message.success(t('knowledge.url_added')) - } - } catch (e) { - // Skip invalid URLs silently - continue - } - } - } - } - - const handleAddSitemap = async () => { - if (disabled) { - return - } - - const url = await PromptPopup.show({ - title: t('knowledge.add_sitemap'), - message: '', - inputPlaceholder: t('knowledge.sitemap_placeholder'), - inputProps: { - maxLength: 1000, - rows: 1 - } - }) - - if (url) { - try { - new URL(url) - if (sitemapItems.find((item) => item.content === url)) { - message.success(t('knowledge.sitemap_added')) - return - } - addSitemap(url) - } catch (e) { - console.error('Invalid Sitemap URL:', url) - } - } - } - - const handleAddNote = async () => { - if (disabled) { - return - } - - const note = await TextEditPopup.show({ text: '', textareaProps: { rows: 20 } }) - note && addNote(note) - } - - const handleEditNote = async (note: any) => { - if (disabled) { - return - } - - const editedText = await TextEditPopup.show({ text: note.content as string, textareaProps: { rows: 20 } }) - editedText && updateNoteContent(note.id, editedText) - } - - const handleAddDirectory = async () => { - if (disabled) { - return - } - - const path = await window.api.file.selectFolder() - Logger.log('[KnowledgeContent] Selected directory:', path) - path && addDirectory(path) - } - - const handleEditRemark = async (item: KnowledgeItem) => { - if (disabled) { - return - } - - const editedRemark: string | undefined = await PromptPopup.show({ - title: t('knowledge.edit_remark'), - message: '', - inputPlaceholder: t('knowledge.edit_remark_placeholder'), - defaultValue: item.remark || '', - inputProps: { - maxLength: 100, - rows: 1 - } - }) - - if (editedRemark !== undefined && editedRemark !== null) { - updateItem({ - ...item, - remark: editedRemark, - updated_at: Date.now() - }) - } - } + const tabItems = knowledgeItems.map((item) => ({ + key: item.key, + label: ( + + {item.icon} + {item.title} + 0 ? '#00b96b' : '#cccccc'}> + {item.items.length} + + + ), + children: {item.content} + })) return ( @@ -235,7 +92,7 @@ const KnowledgeContent: FC = ({ selectedBase }) => { - }> - handleDrop([file as File])} - multiple={true} - accept={fileTypes.join(',')} - style={{ marginTop: 10, background: 'transparent' }}> -

{t('knowledge.drag_file')}

-

- {t('knowledge.file_hint', { file_types: 'TXT, MD, HTML, PDF, DOCX, PPTX, XLSX, EPUB...' })} -

-
- - - {fileItems.length === 0 ? ( - - ) : ( - 5 ? 400 : fileItems.length * 75} - itemHeight={75} - itemKey="id" - styles={{ - verticalScrollBar: { - width: 6 - }, - verticalScrollBarThumb: { - background: 'var(--color-scrollbar-thumb)' - } - }}> - {(item) => { - const file = item.content as FileType - return ( -
- window.api.file.openPath(FileManager.getFilePath(file))}> - - {file.origin_name} - - - ), - ext: file.ext, - extra: `${getDisplayTime(item)} · ${formatFileSize(file.size)}`, - actions: ( - - {item.uniqueId && ( -
- ) - }} -
- )} -
- - - } - defaultActiveKey={[]} - activeKey={expandAll ? ['1'] : undefined} - extra={ - - }> - - {directoryItems.length === 0 && } - {directoryItems.reverse().map((item) => ( - window.api.file.openPath(item.content as string)}> - - {item.content as string} - - - ), - ext: '.folder', - extra: getDisplayTime(item), - actions: ( - - {item.uniqueId && - }> - - {urlItems.length === 0 && } - {urlItems.reverse().map((item) => ( - , - label: t('knowledge.edit_remark'), - onClick: () => handleEditRemark(item) - }, - { - key: 'copy', - icon: , - label: t('common.copy'), - onClick: () => { - navigator.clipboard.writeText(item.content as string) - message.success(t('message.copied')) - } - } - ] - }} - trigger={['contextMenu']}> - - - - - {item.remark || (item.content as string)} - - - - - - ), - ext: '.url', - extra: getDisplayTime(item), - actions: ( - - {item.uniqueId && - }> - - {sitemapItems.length === 0 && } - {sitemapItems.reverse().map((item) => ( - - - - - {item.content as string} - - - - - ), - ext: '.sitemap', - extra: getDisplayTime(item), - actions: ( - - {item.uniqueId && - }> - - {noteItems.length === 0 && } - {noteItems.reverse().map((note) => ( - handleEditNote(note)}>{(note.content as string).slice(0, 50)}..., - ext: '.txt', - extra: getDisplayTime(note), - actions: ( - - + + + {directoryItems.length === 0 && } + {directoryItems.reverse().map((item) => ( + window.api.file.openPath(item.content as string)}> + + {item.content as string} + + + ), + ext: '.folder', + extra: getDisplayTime(item), + actions: ( + + {item.uniqueId && + + + + handleDrop([file as File])} + multiple={true} + accept={fileTypes.join(',')}> +

{t('knowledge.drag_file')}

+

+ {t('knowledge.file_hint', { file_types: 'TXT, MD, HTML, PDF, DOCX, PPTX, XLSX, EPUB...' })} +

+
+ {fileItems.length === 0 ? ( + + ) : ( + + {(item) => { + const file = item.content as FileType + return ( +
+ window.api.file.openPath(FileManager.getFilePath(file))}> + + {file.origin_name} + + + ), + ext: file.ext, + extra: `${getDisplayTime(item)} · ${formatFileSize(file.size)}`, + actions: ( + + {item.uniqueId && ( +
+ ) + }} +
+ )} +
+ + ) +} + +const ItemFlexColumn = styled.div` + display: flex; + flex-direction: column; + padding: 20px 16px; + gap: 10px; +` + +export default KnowledgeFiles diff --git a/src/renderer/src/pages/knowledge/items/KnowledgeNotes.tsx b/src/renderer/src/pages/knowledge/items/KnowledgeNotes.tsx new file mode 100644 index 0000000000..ca3f8c4d17 --- /dev/null +++ b/src/renderer/src/pages/knowledge/items/KnowledgeNotes.tsx @@ -0,0 +1,107 @@ +import { DeleteOutlined, EditOutlined } from '@ant-design/icons' +import TextEditPopup from '@renderer/components/Popups/TextEditPopup' +import Scrollbar from '@renderer/components/Scrollbar' +import { useKnowledge } from '@renderer/hooks/useKnowledge' +import FileItem from '@renderer/pages/files/FileItem' +import { getProviderName } from '@renderer/services/ProviderService' +import { KnowledgeBase, KnowledgeItem } from '@renderer/types' +import { Button } from 'antd' +import dayjs from 'dayjs' +import { Plus } from 'lucide-react' +import { FC } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +import StatusIcon from '../components/StatusIcon' +import { FlexAlignCenter, ItemContainer, ItemHeader, KnowledgeEmptyView, StatusIconWrapper } from '../KnowledgeContent' + +interface KnowledgeContentProps { + selectedBase: KnowledgeBase +} + +const getDisplayTime = (item: KnowledgeItem) => { + const timestamp = item.updated_at && item.updated_at > item.created_at ? item.updated_at : item.created_at + return dayjs(timestamp).format('MM-DD HH:mm') +} + +const KnowledgeNotes: FC = ({ selectedBase }) => { + const { t } = useTranslation() + + const { base, noteItems, updateNoteContent, removeItem, getProcessingStatus, addNote } = useKnowledge( + selectedBase.id || '' + ) + + const providerName = getProviderName(base?.model.provider || '') + const disabled = !base?.version || !providerName + + if (!base) { + return null + } + + const handleAddNote = async () => { + if (disabled) { + return + } + + const note = await TextEditPopup.show({ text: '', textareaProps: { rows: 20 } }) + note && addNote(note) + } + + const handleEditNote = async (note: any) => { + if (disabled) { + return + } + + const editedText = await TextEditPopup.show({ text: note.content as string, textareaProps: { rows: 20 } }) + editedText && updateNoteContent(note.id, editedText) + } + + return ( + + + + + + {noteItems.length === 0 && } + {noteItems.reverse().map((note) => ( + handleEditNote(note)}>{(note.content as string).slice(0, 50)}..., + ext: '.txt', + extra: getDisplayTime(note), + actions: ( + + + + + {sitemapItems.length === 0 && } + {sitemapItems.reverse().map((item) => ( + + + + + {item.content as string} + + + + + ), + ext: '.sitemap', + extra: getDisplayTime(item), + actions: ( + + {item.uniqueId && + + + {urlItems.length === 0 && } + {urlItems.reverse().map((item) => ( + , + label: t('knowledge.edit_remark'), + onClick: () => handleEditRemark(item) + }, + { + key: 'copy', + icon: , + label: t('common.copy'), + onClick: () => { + navigator.clipboard.writeText(item.content as string) + message.success(t('message.copied')) + } + } + ] + }} + trigger={['contextMenu']}> + + + + + {item.remark || (item.content as string)} + + + + + + ), + ext: '.url', + extra: getDisplayTime(item), + actions: ( + + {item.uniqueId &&