diff --git a/src/main/services/FileStorage.ts b/src/main/services/FileStorage.ts index 887a351d..d7139eeb 100644 --- a/src/main/services/FileStorage.ts +++ b/src/main/services/FileStorage.ts @@ -55,6 +55,8 @@ class FileStorage { const storedFilePath = path.join(this.storageDir, file) const storedStats = fs.statSync(storedFilePath) + console.debug('storedFilePath', storedFilePath) + if (storedStats.size === fileSize) { const [originalHash, storedHash] = await Promise.all([ this.getFileHash(filePath), diff --git a/src/main/services/KnowledgeService.ts b/src/main/services/KnowledgeService.ts index 92e1017f..3b87d370 100644 --- a/src/main/services/KnowledgeService.ts +++ b/src/main/services/KnowledgeService.ts @@ -11,7 +11,6 @@ import { WebLoader } from '@llm-tools/embedjs-loader-web' import { OpenAiEmbeddings } from '@llm-tools/embedjs-openai' import { FileType, RagAppRequestParams } from '@types' import { app } from 'electron' -import Logger from 'electron-log' class KnowledgeService { private storageDir = path.join(app.getPath('userData'), 'Data', 'KnowledgeBase') @@ -27,7 +26,6 @@ class KnowledgeService { } private getRagApplication = async ({ id, model, apiKey, baseURL }: RagAppRequestParams): Promise => { - Logger.log('getRagApplication', { id, model, apiKey, baseURL }) return new RAGApplicationBuilder() .setModel('NO_MODEL') .setEmbeddingModel( @@ -82,7 +80,7 @@ class KnowledgeService { return await ragApplication.addLoader(new DocxLoader({ filePathOrUrl: data.path }) as any) } - if (data.ext === '.md') { + if (data.ext === '.md' || data.ext === '.mdx') { return await ragApplication.addLoader(new MarkdownLoader({ filePathOrUrl: data.path }) as any) } diff --git a/src/main/services/WindowService.ts b/src/main/services/WindowService.ts index 11c13010..e133505a 100644 --- a/src/main/services/WindowService.ts +++ b/src/main/services/WindowService.ts @@ -123,6 +123,10 @@ export class WindowService { private setupWebContentsHandlers(mainWindow: BrowserWindow) { mainWindow.webContents.on('will-navigate', (event, url) => { + if (url.includes('localhost:5173')) { + return + } + event.preventDefault() shell.openExternal(url) }) diff --git a/src/renderer/src/components/app/Sidebar.tsx b/src/renderer/src/components/app/Sidebar.tsx index 709cd812..f181505a 100644 --- a/src/renderer/src/components/app/Sidebar.tsx +++ b/src/renderer/src/components/app/Sidebar.tsx @@ -1,4 +1,4 @@ -import { BookOutlined, FolderOutlined, PictureOutlined, TranslationOutlined } from '@ant-design/icons' +import { FileSearchOutlined, FolderOutlined, PictureOutlined, TranslationOutlined } from '@ant-design/icons' import { isMac } from '@renderer/config/constant' import { isLocalAi, UserAvatar } from '@renderer/config/env' import { useTheme } from '@renderer/context/ThemeProvider' @@ -91,7 +91,7 @@ const Sidebar: FC = () => { to('/knowledge')}> - + diff --git a/src/renderer/src/hooks/useknowledge.ts b/src/renderer/src/hooks/useknowledge.ts index a5f44703..c4ab65f3 100644 --- a/src/renderer/src/hooks/useknowledge.ts +++ b/src/renderer/src/hooks/useknowledge.ts @@ -1,20 +1,21 @@ /* eslint-disable react-hooks/rules-of-hooks */ import { db } from '@renderer/databases/index' -import { RootState, useAppSelector } from '@renderer/store' +import KnowledgeQueue from '@renderer/queue/KnowledgeQueue' +import FileManager from '@renderer/services/FileManager' +import { getRagAppRequestParams } from '@renderer/services/KnowledgeService' +import { RootState } from '@renderer/store' import { + addBase, addItem, - addProcessingItem, - clearAllItems, - clearCompletedItems, + clearAllProcessing, + clearCompletedProcessing, + deleteBase, removeItem as removeItemAction, - removeProcessingItem, renameBase, - selectProcessingItemBySource, - selectProcessingItemsByType, updateBase, updateFiles as updateFilesAction, - updateNotes, - updateProcessingStatus + updateItemProcessingStatus, + updateNotes } from '@renderer/store/knowledge' import { FileType, KnowledgeBase, ProcessingStatus } from '@renderer/types' import { KnowledgeItem } from '@renderer/types' @@ -26,7 +27,6 @@ import { v4 as uuidv4 } from 'uuid' export const useKnowledge = (baseId: string) => { const dispatch = useDispatch() const base = useSelector((state: RootState) => state.knowledge.bases.find((b) => b.id === baseId)) - const knowledgeState = useAppSelector((state: RootState) => state.knowledge) // 重命名知识库 const renameKnowledgeBase = (name: string) => { @@ -41,27 +41,37 @@ export const useKnowledge = (baseId: string) => { // 添加文件列表 const addFiles = (files: FileType[]) => { for (const file of files) { - const newItem = { + const newItem: KnowledgeItem = { id: uuidv4(), type: 'file' as const, content: file, created_at: Date.now(), - updated_at: Date.now() + updated_at: Date.now(), + processingStatus: 'pending', + processingProgress: 0, + processingError: '', + retryCount: 0 } dispatch(addItem({ baseId, item: newItem })) } + setTimeout(() => KnowledgeQueue.checkAllBases(), 0) } // 添加URL const addUrl = (url: string) => { - const newUrlItem = { + const newUrlItem: KnowledgeItem = { id: uuidv4(), type: 'url' as const, content: url, created_at: Date.now(), - updated_at: Date.now() + updated_at: Date.now(), + processingStatus: 'pending', + processingProgress: 0, + processingError: '', + retryCount: 0 } dispatch(addItem({ baseId, item: newUrlItem })) + setTimeout(() => KnowledgeQueue.checkAllBases(), 0) } // 添加笔记 @@ -85,10 +95,15 @@ export const useKnowledge = (baseId: string) => { type: 'note', content: '', // store中不需要存储实际内容 created_at: Date.now(), - updated_at: Date.now() + updated_at: Date.now(), + processingStatus: 'pending', + processingProgress: 0, + processingError: '', + retryCount: 0 } dispatch(updateNotes({ baseId, item: noteRef })) + setTimeout(() => KnowledgeQueue.checkAllBases(), 0) } // 更新文件列表 @@ -101,6 +116,7 @@ export const useKnowledge = (baseId: string) => { updated_at: Date.now() })) dispatch(updateFilesAction({ baseId, items: newItems })) + setTimeout(() => KnowledgeQueue.checkAllBases(), 0) } // 更新笔记内容 @@ -115,6 +131,7 @@ export const useKnowledge = (baseId: string) => { await db.knowledge_notes.put(updatedNote) dispatch(updateNotes({ baseId, item: updatedNote })) } + setTimeout(() => KnowledgeQueue.checkAllBases(), 0) } // 获取笔记内容 @@ -123,47 +140,23 @@ export const useKnowledge = (baseId: string) => { } // 移除项目 - const removeItem = (item: KnowledgeItem) => { + const removeItem = async (item: KnowledgeItem) => { dispatch(removeItemAction({ baseId, item })) - } - - // 添加文件到处理队列 - const addFileToQueue = (itemId: string) => { - dispatch( - addProcessingItem({ - baseId, - type: 'file', - sourceId: itemId - }) - ) - } - - // 添加URL到处理队列 - const addUrlToQueue = (itemId: string) => { - dispatch( - addProcessingItem({ - baseId, - type: 'url', - sourceId: itemId - }) - ) - } - - // 添加笔记到处理队列 - const addNoteToQueue = (itemId: string) => { - dispatch( - addProcessingItem({ - baseId, - type: 'note', - sourceId: itemId - }) - ) + if (base) { + const config = getRagAppRequestParams(base) + if (item?.uniqueId) { + await window.api.knowledgeBase.remove({ uniqueId: item.uniqueId, config }) + } + if (item.type === 'file' && typeof item.content === 'object') { + await FileManager.deleteFile(item.content.id) + } + } } // 更新处理状态 const updateItemStatus = (itemId: string, status: ProcessingStatus, progress?: number, error?: string) => { dispatch( - updateProcessingStatus({ + updateItemProcessingStatus({ baseId, itemId, status, @@ -173,38 +166,46 @@ export const useKnowledge = (baseId: string) => { ) } - // 获取特定源的处理状态 - const getProcessingStatus = (sourceId: string) => { - return selectProcessingItemBySource(knowledgeState, baseId, sourceId) + // 获取特定项目的处理状态 + const getProcessingStatus = (itemId: string) => { + return base?.items.find((item) => item.id === itemId)?.processingStatus } // 获取特定类型的所有处理项 const getProcessingItemsByType = (type: 'file' | 'url' | 'note') => { - return selectProcessingItemsByType(knowledgeState, baseId, type) - } - - // 从队列中移除项目 - const removeFromQueue = (itemId: string) => { - dispatch( - removeProcessingItem({ - baseId, - itemId - }) - ) + return base?.items.filter((item) => item.type === type && item.processingStatus !== undefined) || [] } // 清除已完成的项目 const clearCompleted = () => { - dispatch(clearCompletedItems({ baseId })) + dispatch(clearCompletedProcessing({ baseId })) } - // 清除所有队列项目 + // 清除所有处理状态 const clearAll = () => { - dispatch(clearAllItems({ baseId })) + dispatch(clearAllProcessing({ baseId })) + } + + // 添加 Sitemap + const addSitemap = (url: string) => { + const newSitemapItem: KnowledgeItem = { + id: uuidv4(), + type: 'sitemap' as const, + content: url, + created_at: Date.now(), + updated_at: Date.now(), + processingStatus: 'pending', + processingProgress: 0, + processingError: '', + retryCount: 0 + } + dispatch(addItem({ baseId, item: newSitemapItem })) + setTimeout(() => KnowledgeQueue.checkAllBases(), 0) } const fileItems = base?.items.filter((item) => item.type === 'file') || [] const urlItems = base?.items.filter((item) => item.type === 'url') || [] + const sitemapItems = base?.items.filter((item) => item.type === 'sitemap') || [] const [noteItems, setNoteItems] = useState([]) useEffect(() => { @@ -224,24 +225,46 @@ export const useKnowledge = (baseId: string) => { base, fileItems, urlItems, + sitemapItems, noteItems, renameKnowledgeBase, updateKnowledgeBase, addFiles, addUrl, + addSitemap, addNote, updateFiles, updateNoteContent, getNoteContent, - addFileToQueue, - addUrlToQueue, - addNoteToQueue, updateItemStatus, getProcessingStatus, getProcessingItemsByType, - removeFromQueue, clearCompleted, clearAll, removeItem } } + +export const useKnowledgeBases = () => { + const dispatch = useDispatch() + const bases = useSelector((state: RootState) => state.knowledge.bases) + + const addKnowledgeBase = (base: KnowledgeBase) => { + dispatch(addBase(base)) + } + + const renameKnowledgeBase = (baseId: string, name: string) => { + dispatch(renameBase({ baseId, name })) + } + + const deleteKnowledgeBase = (baseId: string) => { + dispatch(deleteBase({ baseId })) + } + + return { + bases, + addKnowledgeBase, + renameKnowledgeBase, + deleteKnowledgeBase + } +} diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index a76f7a29..483aae36 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -555,11 +555,13 @@ "status_completed": "Completed", "status_failed": "Failed", "url_added": "URL added", - "query": "Query", "search_placeholder": "Enter text to search", "add_note": "Add Note", "no_bases": "No knowledge bases available", - "clear_selection": "Clear selection" + "clear_selection": "Clear selection", + "delete_confirm": "Are you sure you want to delete this knowledge base?", + "sitemaps": "Site Maps", + "add_sitemap": "Add Site Map" } } } diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index dab0141a..d4d7d41a 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -555,11 +555,13 @@ "status_completed": "Завершено", "status_failed": "Ошибка", "url_added": "URL добавлен", - "query": "Поиск", "search_placeholder": "Введите текст для поиска", "add_note": "Добавить запись", "no_bases": "База знаний не найдена", - "clear_selection": "Очистить выбор" + "clear_selection": "Очистить выбор", + "delete_confirm": "Вы уверены, что хотите удалить эту базу знаний?", + "sitemaps": "Карта сайта", + "add_sitemap": "Добавить карту сайта" } } } diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 7813cb5d..52bcd8da 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -529,7 +529,7 @@ "notes_placeholder": "输入此知识库的附加信息或上下文...", "delete": "删除", "rename": "重命名", - "urls": "网站", + "urls": "网址", "add_url": "添加网址", "url_placeholder": "请输入网址", "invalid_url": "无效的网址", @@ -544,11 +544,13 @@ "status_completed": "已完成", "status_failed": "失败", "url_added": "网址已添加", - "query": "查询", "search_placeholder": "输入查询内容", "add_note": "添加笔记", "no_bases": "暂无知识库", - "clear_selection": "清除选择" + "clear_selection": "清除选择", + "delete_confirm": "确定要删除此知识库吗?", + "sitemaps": "站点地图", + "add_sitemap": "添加站点地图" } } } diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 2ead2970..a07e5e44 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -543,11 +543,13 @@ "status_completed": "已完成", "status_failed": "失敗", "url_added": "網址已添加", - "query": "查詢", "search_placeholder": "輸入查詢內容", "add_note": "添加筆記", "no_bases": "暫無知識庫", - "clear_selection": "清除選擇" + "clear_selection": "清除選擇", + "delete_confirm": "確定要刪除此知識庫嗎?", + "sitemaps": "站點地圖", + "add_sitemap": "添加站點地圖" } } } diff --git a/src/renderer/src/pages/home/Inputbar/AttachmentButton.tsx b/src/renderer/src/pages/home/Inputbar/AttachmentButton.tsx index 93b3dfe0..ac1418e9 100644 --- a/src/renderer/src/pages/home/Inputbar/AttachmentButton.tsx +++ b/src/renderer/src/pages/home/Inputbar/AttachmentButton.tsx @@ -11,9 +11,10 @@ interface Props { files: FileType[] setFiles: (files: FileType[]) => void ToolbarButton: any + disabled?: boolean } -const AttachmentButton: FC = ({ model, files, setFiles, ToolbarButton }) => { +const AttachmentButton: FC = ({ model, files, setFiles, ToolbarButton, disabled }) => { const { t } = useTranslation() const extensions = isVisionModel(model) ? [...imageExts, ...documentExts, ...textExts] @@ -37,7 +38,7 @@ const AttachmentButton: FC = ({ model, files, setFiles, ToolbarButton }) return ( - + diff --git a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx index c7dee77b..4416a466 100644 --- a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx +++ b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx @@ -49,7 +49,7 @@ interface Props { let _text = '' let _files: FileType[] = [] -let _base: KnowledgeBase +let _base: KnowledgeBase | undefined const Inputbar: FC = ({ assistant: _assistant, setActiveTopic }) => { const [text, setText] = useState(_text) @@ -80,7 +80,7 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic }) => { const [spaceClickCount, setSpaceClickCount] = useState(0) const spaceClickTimer = useRef() const [isTranslating, setIsTranslating] = useState(false) - const [selectedKnowledgeBase, setSelectedKnowledgeBase] = useState(_base) + const [selectedKnowledgeBase, setSelectedKnowledgeBase] = useState(_base) const isVision = useMemo(() => isVisionModel(model), [model]) const supportExts = useMemo(() => [...textExts, ...documentExts, ...(isVision ? imageExts : [])], [isVision]) @@ -450,8 +450,19 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic }) => { - - + 0} + /> + diff --git a/src/renderer/src/pages/home/Inputbar/KnowledgeBaseButton.tsx b/src/renderer/src/pages/home/Inputbar/KnowledgeBaseButton.tsx index 76865717..2ef33916 100644 --- a/src/renderer/src/pages/home/Inputbar/KnowledgeBaseButton.tsx +++ b/src/renderer/src/pages/home/Inputbar/KnowledgeBaseButton.tsx @@ -1,4 +1,4 @@ -import { BookOutlined } from '@ant-design/icons' +import { FileSearchOutlined } from '@ant-design/icons' import { useAppSelector } from '@renderer/store' import { KnowledgeBase } from '@renderer/types' import { Button, Popover, Tooltip } from 'antd' @@ -9,6 +9,8 @@ import styled from 'styled-components' interface Props { selectedBase?: KnowledgeBase onSelect: (base?: KnowledgeBase) => void + disabled?: boolean + ToolbarButton?: any } const KnowledgeBaseSelector: FC = ({ selectedBase, onSelect }) => { @@ -42,14 +44,14 @@ const KnowledgeBaseSelector: FC = ({ selectedBase, onSelect }) => { ) } -const KnowledgeBaseButton: FC = ({ selectedBase, onSelect }) => { +const KnowledgeBaseButton: FC = ({ selectedBase, onSelect, disabled, ToolbarButton }) => { const { t } = useTranslation() if (selectedBase) { return ( onSelect(undefined)}> - + ) @@ -61,8 +63,8 @@ const KnowledgeBaseButton: FC = ({ selectedBase, onSelect }) => { placement="top" content={} trigger="click"> - selectedBase && onSelect(undefined)}> - + selectedBase && onSelect(undefined)} disabled={disabled}> + @@ -78,40 +80,4 @@ const EmptyMessage = styled.div` padding: 8px; ` -const ToolbarButton = styled(Button)` - width: 30px; - height: 30px; - font-size: 17px; - border-radius: 50%; - transition: all 0.3s ease; - color: var(--color-icon); - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - padding: 0; - &.anticon, - &.iconfont { - transition: all 0.3s ease; - color: var(--color-icon); - } - &:hover { - background-color: var(--color-background-soft); - .anticon, - .iconfont { - color: var(--color-text-1); - } - } - &.active { - background-color: var(--color-primary) !important; - .anticon, - .iconfont { - color: var(--color-white-soft); - } - &:hover { - background-color: var(--color-primary); - } - } -` - export default KnowledgeBaseButton diff --git a/src/renderer/src/pages/home/Messages/MessageTokens.tsx b/src/renderer/src/pages/home/Messages/MessageTokens.tsx index 030175fe..8a782447 100644 --- a/src/renderer/src/pages/home/Messages/MessageTokens.tsx +++ b/src/renderer/src/pages/home/Messages/MessageTokens.tsx @@ -29,18 +29,20 @@ const MessgeTokens: React.FC<{ message: Message; isLastMessage: boolean }> = ({ if (message.role === 'assistant') { let metrixs = '' + let hasMetrics = false if (message?.metrics?.completion_tokens && message?.metrics?.time_completion_millsec) { + hasMetrics = true metrixs = t('settings.messages.metrics', { time_first_token_millsec: message?.metrics?.time_first_token_millsec, token_speed: (message?.metrics?.completion_tokens / (message?.metrics?.time_completion_millsec / 1000)).toFixed( - 2 + 0 ) }) } return ( - + {metrixs} Tokens: {message?.usage?.total_tokens} ↑{message?.usage?.prompt_tokens} ↓{message?.usage?.completion_tokens} @@ -68,7 +70,7 @@ const MessageMetadata = styled.div` display: block; } - &:hover { + &.has-metrics:hover { .metrics { display: block; } diff --git a/src/renderer/src/pages/knowledge/KnowledgeContent.tsx b/src/renderer/src/pages/knowledge/KnowledgeContent.tsx index 80131c7c..5fd5e37e 100644 --- a/src/renderer/src/pages/knowledge/KnowledgeContent.tsx +++ b/src/renderer/src/pages/knowledge/KnowledgeContent.tsx @@ -2,10 +2,10 @@ import { DeleteOutlined, EditOutlined, FileTextOutlined, + GlobalOutlined, LinkOutlined, PlusOutlined, - SearchOutlined, - StopOutlined + SearchOutlined } from '@ant-design/icons' import PromptPopup from '@renderer/components/Popups/PromptPopup' import TextEditPopup from '@renderer/components/Popups/TextEditPopup' @@ -18,8 +18,8 @@ import { FC } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' -import KnowledgeSearchPopup from './KnowledgeSearchPopup' -import StatusIcon from './StatusIcon' +import KnowledgeSearchPopup from './components/KnowledgeSearchPopup' +import StatusIcon from './components/StatusIcon' const { Dragger } = Upload const { Title } = Typography @@ -35,15 +35,13 @@ const KnowledgeContent: FC = ({ selectedBase }) => { noteItems, fileItems, urlItems, + sitemapItems, addFiles, updateNoteContent, addUrl, + addSitemap, removeItem, getProcessingStatus, - addFileToQueue, - addNoteToQueue, - addUrlToQueue, - clearAll, addNote } = useKnowledge(selectedBase.id || '') @@ -109,23 +107,36 @@ const KnowledgeContent: FC = ({ selectedBase }) => { } } + const handleAddSitemap = async () => { + const url = await PromptPopup.show({ + title: t('knowledge_base.add_sitemap'), + message: '', + inputPlaceholder: t('knowledge_base.sitemap_placeholder'), + inputProps: { + maxLength: 1000, + rows: 1 + } + }) + + if (url) { + try { + new URL(url) + if (sitemapItems.find((item) => item.content === url)) { + message.success(t('knowledge_base.sitemap_added')) + return + } + addSitemap(url) + } catch (e) { + console.error('Invalid Sitemap URL:', url) + } + } + } + const handleAddNote = async () => { const note = await TextEditPopup.show({ text: '', textareaProps: { rows: 20 } }) note && addNote(note) } - const handleIndexAll = async () => { - fileItems.forEach((item) => !item.uniqueId && addFileToQueue(item.id)) - urlItems.forEach((item) => !item.uniqueId && addUrlToQueue(item.id)) - noteItems.forEach((note) => !note.uniqueId && addNoteToQueue(note.id)) - message.success(t('knowledge_base.index_started')) - } - - const handleCancelIndex = () => { - clearAll() - message.success(t('knowledge_base.index_cancelled')) - } - const handleEditNote = async (note: any) => { const editedText = await TextEditPopup.show({ text: note.content as string, textareaProps: { rows: 20 } }) editedText && updateNoteContent(note.id, editedText) @@ -198,6 +209,33 @@ const KnowledgeContent: FC = ({ selectedBase }) => { + + + {t('knowledge_base.sitemaps')} + + +
+ {sitemapItems.map((item) => ( + + + + + + {item.content as string} + + +
+ +
+
+
+ ))} +
+
+ {t('knowledge_base.notes')} @@ -222,19 +260,9 @@ const KnowledgeContent: FC = ({ selectedBase }) => { ))} - - - -
@@ -321,7 +349,7 @@ const ItemInfo = styled.div` const IndexSection = styled.div` margin-top: 20px; display: flex; - justify-content: flex-end; + justify-content: center; ` export default KnowledgeContent diff --git a/src/renderer/src/pages/knowledge/KnowledgePage.tsx b/src/renderer/src/pages/knowledge/KnowledgePage.tsx index f500d11e..cd77a87b 100644 --- a/src/renderer/src/pages/knowledge/KnowledgePage.tsx +++ b/src/renderer/src/pages/knowledge/KnowledgePage.tsx @@ -3,35 +3,35 @@ import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' import ListItem from '@renderer/components/ListItem' import PromptPopup from '@renderer/components/Popups/PromptPopup' import Scrollbar from '@renderer/components/Scrollbar' -import { RootState } from '@renderer/store' -import { deleteBase, renameBase } from '@renderer/store/knowledge' +import { useKnowledgeBases } from '@renderer/hooks/useknowledge' import { KnowledgeBase } from '@renderer/types' import { Dropdown, Empty, MenuProps } from 'antd' import { FC, useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useDispatch, useSelector } from 'react-redux' import styled from 'styled-components' -import AddKnowledgePopup from './AddKnowledgePopup' +import AddKnowledgePopup from './components/AddKnowledgePopup' import KnowledgeContent from './KnowledgeContent' const KnowledgePage: FC = () => { const { t } = useTranslation() - const { bases } = useSelector((state: RootState) => state.knowledge) + const { bases, renameKnowledgeBase, deleteKnowledgeBase } = useKnowledgeBases() const [selectedBase, setSelectedBase] = useState() - const dispatch = useDispatch() const handleAddKnowledge = async () => { - await AddKnowledgePopup.show({ - title: t('knowledge_base.add.title') - }) + await AddKnowledgePopup.show({ title: t('knowledge_base.add.title') }) } useEffect(() => { if (bases.length > 0) { - setSelectedBase(bases[0]) + if (!selectedBase) { + return setSelectedBase(bases[0]) + } + if (selectedBase && !bases.includes(selectedBase)) { + return setSelectedBase(bases[0]) + } } - }, [bases]) + }, [bases, selectedBase]) const getMenuItems = useCallback( (base: KnowledgeBase) => { @@ -47,7 +47,7 @@ const KnowledgePage: FC = () => { defaultValue: base.name || '' }) if (name && base.name !== name) { - dispatch(renameBase({ baseId: base.id, name })) + renameKnowledgeBase(base.id, name) } } }, @@ -58,14 +58,20 @@ const KnowledgePage: FC = () => { key: 'delete', icon: , onClick: () => { - dispatch(deleteBase({ baseId: base.id })) + window.modal.confirm({ + title: t('knowledge_base.delete_confirm'), + centered: true, + onOk: () => { + deleteKnowledgeBase(base.id) + } + }) } } ] return menus }, - [dispatch, t] + [deleteKnowledgeBase, renameKnowledgeBase, t] ) return ( diff --git a/src/renderer/src/pages/knowledge/AddKnowledgePopup.tsx b/src/renderer/src/pages/knowledge/components/AddKnowledgePopup.tsx similarity index 94% rename from src/renderer/src/pages/knowledge/AddKnowledgePopup.tsx rename to src/renderer/src/pages/knowledge/components/AddKnowledgePopup.tsx index 142bea6c..caa031be 100644 --- a/src/renderer/src/pages/knowledge/AddKnowledgePopup.tsx +++ b/src/renderer/src/pages/knowledge/components/AddKnowledgePopup.tsx @@ -1,16 +1,15 @@ import { TopView } from '@renderer/components/TopView' import { isEmbeddingModel } from '@renderer/config/models' +import { useKnowledgeBases } from '@renderer/hooks/useknowledge' import { useProviders } from '@renderer/hooks/useProvider' import { getRagAppRequestParams } from '@renderer/services/KnowledgeService' import { getModelUniqId } from '@renderer/services/ModelService' -import { addBase } from '@renderer/store/knowledge' import { Model } from '@renderer/types' import { Form, Input, Modal, Select } from 'antd' import { find, sortBy } from 'lodash' import { nanoid } from 'nanoid' import { useState } from 'react' import { useTranslation } from 'react-i18next' -import { useDispatch } from 'react-redux' interface ShowParams { title: string @@ -29,8 +28,8 @@ const PopupContainer: React.FC = ({ title, resolve }) => { const [open, setOpen] = useState(true) const [form] = Form.useForm() const { t } = useTranslation() - const dispatch = useDispatch() const { providers } = useProviders() + const { addKnowledgeBase } = useKnowledgeBases() const allModels = providers .map((p) => p.models) .flat() @@ -61,14 +60,13 @@ const PopupContainer: React.FC = ({ title, resolve }) => { name: values.name, model: selectedModel, items: [], - processingQueue: [], created_at: Date.now(), updated_at: Date.now() } await window.api.knowledgeBase.create(getRagAppRequestParams(newBase)) - dispatch(addBase(newBase as any)) + addKnowledgeBase(newBase as any) setOpen(false) resolve(newBase) } diff --git a/src/renderer/src/pages/knowledge/KnowledgeSearchPopup.tsx b/src/renderer/src/pages/knowledge/components/KnowledgeSearchPopup.tsx similarity index 100% rename from src/renderer/src/pages/knowledge/KnowledgeSearchPopup.tsx rename to src/renderer/src/pages/knowledge/components/KnowledgeSearchPopup.tsx diff --git a/src/renderer/src/pages/knowledge/StatusIcon.tsx b/src/renderer/src/pages/knowledge/components/StatusIcon.tsx similarity index 88% rename from src/renderer/src/pages/knowledge/StatusIcon.tsx rename to src/renderer/src/pages/knowledge/components/StatusIcon.tsx index c156ca5d..e9015e1f 100644 --- a/src/renderer/src/pages/knowledge/StatusIcon.tsx +++ b/src/renderer/src/pages/knowledge/components/StatusIcon.tsx @@ -1,6 +1,6 @@ import { CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons' import { Center } from '@renderer/components/Layout' -import { KnowledgeBase, ProcessingItem } from '@renderer/types' +import { KnowledgeBase, ProcessingStatus } from '@renderer/types' import { Tooltip } from 'antd' import { FC } from 'react' import { useTranslation } from 'react-i18next' @@ -9,13 +9,14 @@ import styled from 'styled-components' interface StatusIconProps { sourceId: string base: KnowledgeBase - getProcessingStatus: (sourceId: string) => ProcessingItem | undefined + getProcessingStatus: (sourceId: string) => ProcessingStatus | undefined } const StatusIcon: FC = ({ sourceId, base, getProcessingStatus }) => { const { t } = useTranslation() const status = getProcessingStatus(sourceId) const item = base.items.find((item) => item.id === sourceId) + const errorText = item?.processingError if (!status) { if (item?.uniqueId) { @@ -34,7 +35,7 @@ const StatusIcon: FC = ({ sourceId, base, getProcessingStatus } ) } - switch (status.status) { + switch (status) { case 'pending': return ( @@ -55,7 +56,7 @@ const StatusIcon: FC = ({ sourceId, base, getProcessingStatus } ) case 'failed': return ( - + ) diff --git a/src/renderer/src/pages/settings/AboutSettings.tsx b/src/renderer/src/pages/settings/AboutSettings.tsx index 65751e5c..1081f877 100644 --- a/src/renderer/src/pages/settings/AboutSettings.tsx +++ b/src/renderer/src/pages/settings/AboutSettings.tsx @@ -1,4 +1,4 @@ -import { GithubOutlined, TwitterOutlined } from '@ant-design/icons' +import { GithubOutlined, XOutlined } from '@ant-design/icons' import { FileProtectOutlined, GlobalOutlined, MailOutlined, SendOutlined, SoundOutlined } from '@ant-design/icons' import IndicatorLight from '@renderer/components/IndicatorLight' import { HStack } from '@renderer/components/Layout' @@ -208,7 +208,7 @@ const AboutSettings: FC = () => { - X + X