diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml index 2c15302bee..82058ec2a9 100644 --- a/.github/workflows/pr-ci.yml +++ b/.github/workflows/pr-ci.yml @@ -45,8 +45,14 @@ jobs: - name: Install Dependencies run: yarn install - - name: Build Check - run: yarn build:check - - name: Lint Check run: yarn test:lint + + - name: Type Check + run: yarn typecheck + + - name: i18n Check + run: yarn check:i18n + + - name: Test + run: yarn test diff --git a/src/main/config.ts b/src/main/config.ts index dce2013199..5a6f667d18 100644 --- a/src/main/config.ts +++ b/src/main/config.ts @@ -1,7 +1,7 @@ +import { isDev, isWin } from '@main/constant' import { app } from 'electron' import { getDataPath } from './utils' -import { isWin, isDev } from '@main/constant' if (isDev) { app.setPath('userData', app.getPath('userData') + 'Dev') diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 8765ca0025..2277299dc6 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -72,7 +72,7 @@ const dxtService = new DxtService() export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { const appUpdater = new AppUpdater() - const notificationService = new NotificationService(mainWindow) + const notificationService = new NotificationService() // Initialize Python service with main window pythonService.setMainWindow(mainWindow) diff --git a/src/main/services/NotificationService.ts b/src/main/services/NotificationService.ts index 5ba0d82ce4..2ceb12ee40 100644 --- a/src/main/services/NotificationService.ts +++ b/src/main/services/NotificationService.ts @@ -1,14 +1,9 @@ -import { BrowserWindow, Notification as ElectronNotification } from 'electron' +import { Notification as ElectronNotification } from 'electron' import { Notification } from 'src/renderer/src/types/notification' +import { windowService } from './WindowService' + class NotificationService { - private window: BrowserWindow - - constructor(window: BrowserWindow) { - // Initialize the service - this.window = window - } - public async sendNotification(notification: Notification) { // 使用 Electron Notification API const electronNotification = new ElectronNotification({ @@ -17,8 +12,8 @@ class NotificationService { }) electronNotification.on('click', () => { - this.window.show() - this.window.webContents.send('notification-click', notification) + windowService.getMainWindow()?.show() + windowService.getMainWindow()?.webContents.send('notification-click', notification) }) electronNotification.show() diff --git a/src/renderer/src/assets/styles/ant.scss b/src/renderer/src/assets/styles/ant.scss index 227f2d84fa..ad6d1e0eaf 100644 --- a/src/renderer/src/assets/styles/ant.scss +++ b/src/renderer/src/assets/styles/ant.scss @@ -1,5 +1,26 @@ @use './container.scss'; +/* Modal 关闭按钮不应该可拖拽,以确保点击正常 */ +.ant-modal-close { + -webkit-app-region: no-drag; +} + +/* 普通 Drawer 内容不应该可拖拽 */ +.ant-drawer-content { + -webkit-app-region: no-drag; +} + +/* minapp-drawer 有自己的拖拽规则 */ + +/* 下拉菜单和弹出框内容不应该可拖拽 */ +.ant-dropdown, +.ant-dropdown-menu, +.ant-popover-content, +.ant-tooltip-content, +.ant-popconfirm { + -webkit-app-region: no-drag; +} + #inputbar { resize: none; } @@ -66,6 +87,7 @@ } .ant-drawer-header { + /* 普通 drawer header 不应该可拖拽,除非被 minapp-drawer 覆盖 */ -webkit-app-region: no-drag; } @@ -148,6 +170,7 @@ border-radius: 10px; } .ant-modal-body { + /* 保持 body 在视口内,使用标准的最大高度 */ max-height: 80vh; overflow-y: auto; padding: 0 16px 0 16px; diff --git a/src/renderer/src/components/MarkdownShadowDOMRenderer.tsx b/src/renderer/src/components/MarkdownShadowDOMRenderer.tsx index 9972d52139..3eb6ac40ea 100644 --- a/src/renderer/src/components/MarkdownShadowDOMRenderer.tsx +++ b/src/renderer/src/components/MarkdownShadowDOMRenderer.tsx @@ -47,7 +47,7 @@ const ShadowDOMRenderer: React.FC = ({ children }) => { } return ( -
+
{createPortal( diff --git a/src/renderer/src/components/Tab/TabContainer.tsx b/src/renderer/src/components/Tab/TabContainer.tsx index 4fdeff7e72..e3fd8291d3 100644 --- a/src/renderer/src/components/Tab/TabContainer.tsx +++ b/src/renderer/src/components/Tab/TabContainer.tsx @@ -206,8 +206,16 @@ const TabsBar = styled.div<{ $isFullscreen: boolean }>` gap: 5px; padding-left: ${({ $isFullscreen }) => (!$isFullscreen && isMac ? '75px' : '15px')}; padding-right: ${({ $isFullscreen }) => ($isFullscreen ? '12px' : isWin ? '140px' : isLinux ? '120px' : '12px')}; - -webkit-app-region: drag; height: var(--navbar-height); + position: relative; + -webkit-app-region: drag; + + /* 确保交互元素在拖拽区域之上 */ + > * { + position: relative; + z-index: 1; + -webkit-app-region: no-drag; + } ` const Tab = styled.div<{ active?: boolean }>` @@ -220,7 +228,6 @@ const Tab = styled.div<{ active?: boolean }>` border-radius: var(--list-item-border-radius); cursor: pointer; user-select: none; - -webkit-app-region: none; height: 30px; min-width: 90px; transition: background 0.2s; @@ -273,7 +280,6 @@ const AddTabButton = styled.div` height: 30px; cursor: pointer; color: var(--color-text-2); - -webkit-app-region: none; border-radius: var(--list-item-border-radius); &.active { background: var(--color-list-item); @@ -298,7 +304,6 @@ const ThemeButton = styled.div` height: 30px; cursor: pointer; color: var(--color-text); - -webkit-app-region: none; &:hover { background: var(--color-list-item); @@ -314,7 +319,6 @@ const SettingsButton = styled.div<{ $active: boolean }>` height: 30px; cursor: pointer; color: var(--color-text); - -webkit-app-region: none; border-radius: 8px; background: ${(props) => (props.$active ? 'var(--color-list-item)' : 'transparent')}; &:hover { diff --git a/src/renderer/src/hooks/useAssistant.ts b/src/renderer/src/hooks/useAssistant.ts index 02a327c345..9527764f81 100644 --- a/src/renderer/src/hooks/useAssistant.ts +++ b/src/renderer/src/hooks/useAssistant.ts @@ -7,10 +7,10 @@ import { MODEL_SUPPORTED_REASONING_EFFORT } from '@renderer/config/models' import { db } from '@renderer/databases' -import { getDefaultTopic } from '@renderer/services/AssistantService' +import { getDefaultAssistant, getDefaultTopic } from '@renderer/services/AssistantService' import { useAppDispatch, useAppSelector } from '@renderer/store' import { - addAssistant, + addAssistant as _addAssistant, addTopic, insertAssistant, removeAllTopics, @@ -27,6 +27,7 @@ import { import { setDefaultModel, setQuickModel, setTranslateModel } from '@renderer/store/llm' import { Assistant, AssistantSettings, Model, ThinkingOption, Topic } from '@renderer/types' import { uuid } from '@renderer/utils' +import { formatErrorMessage } from '@renderer/utils/error' import { useCallback, useEffect, useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' @@ -38,10 +39,25 @@ export function useAssistants() { const dispatch = useAppDispatch() const logger = loggerService.withContext('useAssistants') + /** + * 添加一个新的助手 + * @param assistant - 要添加的助手对象 + * @throws {Error} 如果添加助手失败会抛出错误 + */ + const addAssistant = (assistant: Assistant) => { + try { + dispatch(_addAssistant(assistant)) + } catch (e) { + logger.error('Failed to add assistant', e as Error) + window.message.error(t('assistants.error.add' + ': ' + formatErrorMessage(e))) + throw e + } + } + return { assistants, updateAssistants: (assistants: Assistant[]) => dispatch(updateAssistants(assistants)), - addAssistant: (assistant: Assistant) => dispatch(addAssistant(assistant)), + addAssistant, insertAssistant: (index: number, assistant: Assistant) => dispatch(insertAssistant({ index, assistant })), copyAssistant: (assistant: Assistant): Assistant | undefined => { if (!assistant) { @@ -52,7 +68,7 @@ export function useAssistants() { const _assistant: Assistant = { ...assistant, id: uuid(), topics: [getDefaultTopic(assistant.id)] } if (index === -1) { logger.warn("Origin assistant's id not found. Fallback to addAssistant.") - dispatch(addAssistant(_assistant)) + addAssistant(_assistant) } else { // 插入到后面 try { @@ -74,7 +90,22 @@ export function useAssistants() { } export function useAssistant(id: string) { - const assistant = useAppSelector((state) => state.assistants.assistants.find((a) => a.id === id) as Assistant) + let assistant = useAppSelector((state) => state.assistants.assistants.find((a) => a.id === id)) + const { addAssistant } = useAssistants() + const { t } = useTranslation() + + if (!assistant) { + window.message.warning(t('warning.missing_assistant')) + const newAssistant = { ...getDefaultAssistant(), id } + try { + addAssistant(newAssistant) + assistant = newAssistant + } catch (e) { + window.message.warning(t('warning.fallback.deafult_assistant')) + assistant = getDefaultAssistant() + } + } + const dispatch = useAppDispatch() const { defaultModel } = useDefaultModel() @@ -88,7 +119,7 @@ export function useAssistant(id: string) { const settingsRef = useRef(assistant?.settings) useEffect(() => { - settingsRef.current = assistant.settings + settingsRef.current = assistant?.settings }, [assistant?.settings]) const updateAssistantSettings = useCallback( diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 71a168fa04..035c906d9b 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -148,6 +148,9 @@ "edit": { "title": "Edit Assistant" }, + "error": { + "add": "Failed to add assistant" + }, "icon": { "type": "Assistant Icon" }, @@ -885,6 +888,9 @@ }, "history": { "continue_chat": "Continue Chatting", + "error": { + "topic_not_found": "Topic not found" + }, "locate": { "message": "Locate the message" }, @@ -3803,6 +3809,10 @@ "title": "Update" }, "warning": { + "fallback": { + "deafult_assistant": "Reverted to default assistant, which may cause issues" + }, + "missing_assistant": "Assistant does not exist", "missing_provider": "The supplier does not exist; reverted to the default supplier {{provider}}. This may cause issues." }, "words": { diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index a2157c4a21..3361adafe7 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -148,6 +148,9 @@ "edit": { "title": "アシスタントを編集" }, + "error": { + "add": "アシスタントの追加に失敗しました" + }, "icon": { "type": "アシスタントアイコン" }, @@ -885,6 +888,9 @@ }, "history": { "continue_chat": "チャットを続ける", + "error": { + "topic_not_found": "トピックが見つかりません" + }, "locate": { "message": "メッセージを探す" }, @@ -3803,6 +3809,10 @@ "title": "更新" }, "warning": { + "fallback": { + "deafult_assistant": "既定のアシスタントに戻されました。これにより問題が発生する可能性があります。" + }, + "missing_assistant": "アシスタントが存在しません", "missing_provider": "サプライヤーが存在しないため、デフォルトのサプライヤー {{provider}} にロールバックされました。これにより問題が発生する可能性があります。" }, "words": { diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index b6e6c26590..22ce9cfce8 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -148,6 +148,9 @@ "edit": { "title": "Редактировать ассистента" }, + "error": { + "add": "Не удалось добавить помощника" + }, "icon": { "type": "Иконка ассистента" }, @@ -885,6 +888,9 @@ }, "history": { "continue_chat": "Продолжить чат", + "error": { + "topic_not_found": "Топик не найден" + }, "locate": { "message": "Найти сообщение" }, @@ -3803,6 +3809,10 @@ "title": "Обновление" }, "warning": { + "fallback": { + "deafult_assistant": "Возвращено к помощнику по умолчанию, что может привести к проблемам" + }, + "missing_assistant": "Ассистент не существует", "missing_provider": "Поставщик не существует, возвращение к поставщику по умолчанию {{provider}}. Это может привести к проблемам." }, "words": { diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 743c1d7621..3b3d763f8b 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -148,6 +148,9 @@ "edit": { "title": "编辑助手" }, + "error": { + "add": "添加助手失败" + }, "icon": { "type": "助手图标" }, @@ -885,6 +888,9 @@ }, "history": { "continue_chat": "继续聊天", + "error": { + "topic_not_found": "话题不存在" + }, "locate": { "message": "定位到消息" }, @@ -3803,6 +3809,10 @@ "title": "更新提示" }, "warning": { + "fallback": { + "deafult_assistant": "已回退到默认助手,这可能导致问题" + }, + "missing_assistant": "助手不存在", "missing_provider": "供应商不存在,已回退到默认供应商 {{provider}}。这可能导致问题。" }, "words": { diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 9f8f75b301..d86862508f 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -148,6 +148,9 @@ "edit": { "title": "編輯助手" }, + "error": { + "add": "添加助手失敗" + }, "icon": { "type": "助手圖示" }, @@ -885,6 +888,9 @@ }, "history": { "continue_chat": "繼續聊天", + "error": { + "topic_not_found": "話題不存在" + }, "locate": { "message": "定位到訊息" }, @@ -3803,6 +3809,10 @@ "title": "更新提示" }, "warning": { + "fallback": { + "deafult_assistant": "已回退到預設助手,這可能導致問題" + }, + "missing_assistant": "助手不存在", "missing_provider": "供應商不存在,已回退到預設供應商 {{provider}}。這可能導致問題。" }, "words": { diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index 015558658b..0b7cf845b0 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -148,6 +148,9 @@ "edit": { "title": "Επεξεργασία βοηθού" }, + "error": { + "add": "Αποτυχία προσθήκης βοηθού" + }, "icon": { "type": "Εικόνα Βοηθού" }, @@ -2688,7 +2691,7 @@ "title": "Αυτόματη ενημέρωση" }, "avatar": { - "builtin": "Ενσωματωμένο αναγνωριστικό προφίλ", + "builtin": "ενσωματωμένο avatar", "reset": "Επαναφορά εικονιδίου" }, "backup": { @@ -3803,6 +3806,10 @@ "title": "Ενημέρωση" }, "warning": { + "fallback": { + "deafult_assistant": "Επαναφέρθηκε στον προεπιλεγμένο βοηθό, γεγονός που ενδέχεται να προκαλέσει προβλήματα" + }, + "missing_assistant": "Ο βοηθός δεν υπάρχει", "missing_provider": "Ο προμηθευτής δεν υπάρχει, έγινε επαναφορά στον προεπιλεγμένο προμηθευτή {{provider}}. Αυτό μπορεί να προκαλέσει προβλήματα." }, "words": { diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index f6ba2ce7a9..e592e00cea 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -148,6 +148,9 @@ "edit": { "title": "Editar Asistente" }, + "error": { + "add": "Error al agregar asistente" + }, "icon": { "type": "Ícono del Asistente" }, @@ -3803,6 +3806,10 @@ "title": "Actualización" }, "warning": { + "fallback": { + "deafult_assistant": "Se ha revertido al asistente predeterminado, lo que podría causar problemas" + }, + "missing_assistant": "El asistente no existe", "missing_provider": "El proveedor no existe, se ha revertido al proveedor predeterminado {{provider}}. Esto podría causar problemas." }, "words": { diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index 454e03f305..01b46cc95d 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -148,6 +148,9 @@ "edit": { "title": "Modifier l'Aide" }, + "error": { + "add": "Échec de l'ajout de l'assistant" + }, "icon": { "type": "Icône de l'assistant" }, @@ -3803,6 +3806,10 @@ "title": "Mise à jour" }, "warning": { + "fallback": { + "deafult_assistant": "Revenu à l'assistant par défaut, ce qui pourrait entraîner des problèmes" + }, + "missing_assistant": "L'assistant n'existe pas", "missing_provider": "Le fournisseur n’existe pas, retour au fournisseur par défaut {{provider}}. Cela peut entraîner des problèmes." }, "words": { diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index b655984b86..900585d154 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -148,6 +148,9 @@ "edit": { "title": "Editar Assistente" }, + "error": { + "add": "Falha ao adicionar assistente" + }, "icon": { "type": "Ícone do Assistente" }, @@ -3803,6 +3806,10 @@ "title": "Atualização" }, "warning": { + "fallback": { + "deafult_assistant": "Voltou ao assistente padrão, o que pode causar problemas" + }, + "missing_assistant": "O assistente não existe", "missing_provider": "O fornecedor não existe; foi revertido para o fornecedor predefinido {{provider}}. Isto pode causar problemas." }, "words": { diff --git a/src/renderer/src/pages/history/HistoryPage.tsx b/src/renderer/src/pages/history/HistoryPage.tsx index d20accfd87..7d0857f2e9 100644 --- a/src/renderer/src/pages/history/HistoryPage.tsx +++ b/src/renderer/src/pages/history/HistoryPage.tsx @@ -22,7 +22,7 @@ let _stack: Route[] = ['topics'] let _topic: Topic | undefined let _message: Message | undefined -const TopicsPage: FC = () => { +const HistoryPage: FC = () => { const { t } = useTranslation() const [search, setSearch] = useState(_search) const [searchKeywords, setSearchKeywords] = useState(_search) @@ -52,7 +52,12 @@ const TopicsPage: FC = () => { setTopic(undefined) } - const onTopicClick = (topic: Topic) => { + // topic 不包含 messages,用到的时候才会获取 + const onTopicClick = (topic: Topic | null | undefined) => { + if (!topic) { + window.message.error(t('history.error.topic_not_found')) + return + } setStack((prev) => [...prev, 'topic']) setTopic(topic) } @@ -86,7 +91,7 @@ const TopicsPage: FC = () => { ) } - suffix={search.length >= 2 ? : null} + suffix={search.length ? : null} ref={inputRef} placeholder={t('history.search.placeholder')} value={search} @@ -146,4 +151,4 @@ const SearchIcon = styled.div` } ` -export default TopicsPage +export default HistoryPage diff --git a/src/renderer/src/pages/history/components/SearchResults.tsx b/src/renderer/src/pages/history/components/SearchResults.tsx index c33d8e56e0..a88e97dd0e 100644 --- a/src/renderer/src/pages/history/components/SearchResults.tsx +++ b/src/renderer/src/pages/history/components/SearchResults.tsx @@ -1,16 +1,23 @@ +import { LoadingIcon } from '@renderer/components/Icons' import db from '@renderer/databases' import useScrollPosition from '@renderer/hooks/useScrollPosition' -import { useTimer } from '@renderer/hooks/useTimer' -import { getTopicById } from '@renderer/hooks/useTopic' +import { selectTopicsMap } from '@renderer/store/assistants' import { Topic } from '@renderer/types' import { type Message, MessageBlockType } from '@renderer/types/newMessage' -import { List, Typography } from 'antd' +import { List, Spin, Typography } from 'antd' import { useLiveQuery } from 'dexie-react-hooks' -import { FC, memo, useCallback, useEffect, useState } from 'react' +import { FC, memo, useCallback, useEffect, useRef, useState } from 'react' +import { useSelector } from 'react-redux' import styled from 'styled-components' const { Text, Title } = Typography +type SearchResult = { + message: Message + topic: Topic + content: string +} + interface Props extends React.HTMLAttributes { keywords: string onMessageClick: (message: Message) => void @@ -19,7 +26,7 @@ interface Props extends React.HTMLAttributes { const SearchResults: FC = ({ keywords, onMessageClick, onTopicClick, ...props }) => { const { handleScroll, containerRef } = useScrollPosition('SearchResults') - const { setTimeoutTimer } = useTimer() + const observerRef = useRef(null) const [searchTerms, setSearchTerms] = useState( keywords @@ -29,9 +36,12 @@ const SearchResults: FC = ({ keywords, onMessageClick, onTopicClick, ...p ) const topics = useLiveQuery(() => db.topics.toArray(), []) + // FIXME: db 中没有 topic.name 等信息,只能从 store 获取 + const storeTopicsMap = useSelector(selectTopicsMap) - const [searchResults, setSearchResults] = useState<{ message: Message; topic: Topic; content: string }[]>([]) + const [searchResults, setSearchResults] = useState([]) const [searchStats, setSearchStats] = useState({ count: 0, time: 0 }) + const [isLoading, setIsLoading] = useState(false) const removeMarkdown = (text: string) => { return text @@ -46,33 +56,40 @@ const SearchResults: FC = ({ keywords, onMessageClick, onTopicClick, ...p const onSearch = useCallback(async () => { setSearchResults([]) + setIsLoading(true) if (keywords.length === 0) { setSearchStats({ count: 0, time: 0 }) setSearchTerms([]) + setIsLoading(false) return } const startTime = performance.now() - const results: { message: Message; topic: Topic; content: string }[] = [] const newSearchTerms = keywords .toLowerCase() .split(' ') .filter((term) => term.length > 0) + const searchRegexes = newSearchTerms.map((term) => new RegExp(term, 'i')) - const blocksArray = await db.message_blocks.toArray() - const blocks = blocksArray + const blocks = (await db.message_blocks.toArray()) .filter((block) => block.type === MessageBlockType.MAIN_TEXT) - .filter((block) => newSearchTerms.some((term) => block.content.toLowerCase().includes(term))) + .filter((block) => searchRegexes.some((regex) => regex.test(block.content))) - const messages = topics?.map((topic) => topic.messages).flat() + const messages = topics?.flatMap((topic) => topic.messages) - for (const block of blocks) { - const message = messages?.find((message) => message.id === block.messageId) - if (message) { - results.push({ message, topic: await getTopicById(message.topicId)!, content: block.content }) - } - } + const results = await Promise.all( + blocks.map(async (block) => { + const message = messages?.find((message) => message.id === block.messageId) + if (message) { + const topic = storeTopicsMap.get(message.topicId) + if (topic) { + return { message, topic, content: block.content } + } + } + return null + }) + ).then((results) => results.filter(Boolean) as SearchResult[]) const endTime = performance.now() setSearchResults(results) @@ -81,7 +98,8 @@ const SearchResults: FC = ({ keywords, onMessageClick, onTopicClick, ...p time: (endTime - startTime) / 1000 }) setSearchTerms(newSearchTerms) - }, [keywords, topics]) + setIsLoading(false) + }, [keywords, storeTopicsMap, topics]) const highlightText = (text: string) => { let highlightedText = removeMarkdown(text) @@ -100,9 +118,24 @@ const SearchResults: FC = ({ keywords, onMessageClick, onTopicClick, ...p onSearch() }, [onSearch]) + useEffect(() => { + if (!containerRef.current) return + + observerRef.current = new MutationObserver(() => { + containerRef.current?.scrollTo({ top: 0, behavior: 'smooth' }) + }) + + observerRef.current.observe(containerRef.current, { + childList: true, + subtree: true + }) + + return () => observerRef.current?.disconnect() + }, [containerRef]) + return ( - + }> {searchResults.length > 0 && ( Found {searchStats.count} results in {searchStats.time.toFixed(3)} seconds @@ -113,19 +146,15 @@ const SearchResults: FC = ({ keywords, onMessageClick, onTopicClick, ...p dataSource={searchResults} pagination={{ pageSize: 10, - onChange: () => { - setTimeoutTimer('scroll', () => containerRef.current?.scrollTo({ top: 0 }), 0) - } + hideOnSinglePage: true }} + style={{ opacity: isLoading ? 0 : 1 }} renderItem={({ message, topic, content }) => ( { - const _topic = await getTopicById(topic.id) - onTopicClick(_topic) - }}> + onClick={() => onTopicClick(topic)}> {topic.name}
onMessageClick(message)}> @@ -138,24 +167,17 @@ const SearchResults: FC = ({ keywords, onMessageClick, onTopicClick, ...p )} />
- + ) } const Container = styled.div` width: 100%; - padding: 20px; + height: 100%; + padding: 20px 36px; overflow-y: auto; display: flex; - flex-direction: row; - justify-content: center; -` - -const ContainerWrapper = styled.div` - width: 100%; - padding: 0 16px; - display: flex; flex-direction: column; ` @@ -166,6 +188,7 @@ const SearchStats = styled.div` const SearchResultTime = styled.div` margin-top: 10px; + text-align: right; ` export default memo(SearchResults) diff --git a/src/renderer/src/pages/history/components/TopicMessages.tsx b/src/renderer/src/pages/history/components/TopicMessages.tsx index 917a110b5d..9f5111a254 100644 --- a/src/renderer/src/pages/history/components/TopicMessages.tsx +++ b/src/renderer/src/pages/history/components/TopicMessages.tsx @@ -5,18 +5,17 @@ import { MessageEditingProvider } from '@renderer/context/MessageEditingContext' import useScrollPosition from '@renderer/hooks/useScrollPosition' import { useSettings } from '@renderer/hooks/useSettings' import { useTimer } from '@renderer/hooks/useTimer' +import { getTopicById } from '@renderer/hooks/useTopic' import { getAssistantById } from '@renderer/services/AssistantService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { isGenerating, locateToMessage } from '@renderer/services/MessagesService' import NavigationService from '@renderer/services/NavigationService' -import { useAppDispatch } from '@renderer/store' -import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk' import { Topic } from '@renderer/types' -import { classNames } from '@renderer/utils' +import { classNames, runAsyncFunction } from '@renderer/utils' import { Button, Divider, Empty } from 'antd' import { t } from 'i18next' import { Forward } from 'lucide-react' -import { FC, useEffect } from 'react' +import { FC, useEffect, useState } from 'react' import styled from 'styled-components' import { default as MessageItem } from '../../home/Messages/Message' @@ -25,16 +24,22 @@ interface Props extends React.HTMLAttributes { topic?: Topic } -const TopicMessages: FC = ({ topic, ...props }) => { +const TopicMessages: FC = ({ topic: _topic, ...props }) => { const navigate = NavigationService.navigate! const { handleScroll, containerRef } = useScrollPosition('TopicMessages') - const dispatch = useAppDispatch() const { messageStyle } = useSettings() const { setTimeoutTimer } = useTimer() + const [topic, setTopic] = useState(_topic) + useEffect(() => { - topic && dispatch(loadTopicMessagesThunk(topic.id)) - }, [dispatch, topic]) + if (!_topic) return + + runAsyncFunction(async () => { + const topic = await getTopicById(_topic.id) + setTopic(topic) + }) + }, [_topic, topic]) const isEmpty = (topic?.messages || []).length === 0 diff --git a/src/renderer/src/pages/history/components/TopicsHistory.tsx b/src/renderer/src/pages/history/components/TopicsHistory.tsx index 2051d536bf..37113891f2 100644 --- a/src/renderer/src/pages/history/components/TopicsHistory.tsx +++ b/src/renderer/src/pages/history/components/TopicsHistory.tsx @@ -1,14 +1,14 @@ import { SearchOutlined } from '@ant-design/icons' import { VStack } from '@renderer/components/Layout' -import { useAssistants } from '@renderer/hooks/useAssistant' import useScrollPosition from '@renderer/hooks/useScrollPosition' -import { getTopicById } from '@renderer/hooks/useTopic' +import { selectAllTopics } from '@renderer/store/assistants' import { Topic } from '@renderer/types' import { Button, Divider, Empty, Segmented } from 'antd' import dayjs from 'dayjs' import { groupBy, isEmpty, orderBy } from 'lodash' import { useState } from 'react' import { useTranslation } from 'react-i18next' +import { useSelector } from 'react-redux' import styled from 'styled-components' type SortType = 'createdAt' | 'updatedAt' @@ -20,18 +20,18 @@ type Props = { } & React.HTMLAttributes const TopicsHistory: React.FC = ({ keywords, onClick, onSearch, ...props }) => { - const { assistants } = useAssistants() const { t } = useTranslation() const { handleScroll, containerRef } = useScrollPosition('TopicsHistory') const [sortType, setSortType] = useState('createdAt') - const topics = orderBy(assistants.map((assistant) => assistant.topics).flat(), sortType, 'desc') + // FIXME: db 中没有 topic.name 等信息,只能从 store 获取 + const topics = useSelector(selectAllTopics) const filteredTopics = topics.filter((topic) => { return topic.name.toLowerCase().includes(keywords.toLowerCase()) }) - const groupedTopics = groupBy(filteredTopics, (topic) => { + const groupedTopics = groupBy(orderBy(filteredTopics, sortType, 'desc'), (topic) => { return dayjs(topic[sortType]).format('MM/DD') }) @@ -66,19 +66,14 @@ const TopicsHistory: React.FC = ({ keywords, onClick, onSearch, ...props {date} {items.map((topic) => ( - { - const _topic = await getTopicById(topic.id) - onClick(_topic) - }}> + onClick(topic)}> {topic.name.substring(0, 50)} {dayjs(topic[sortType]).format('HH:mm')} ))} ))} - {keywords.length >= 2 && ( + {keywords && (