diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index a7a52d1ac3..0a6ffbf92f 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -885,6 +885,9 @@ }, "history": { "continue_chat": "Continue Chatting", + "error": { + "topic_not_found": "Topic not found" + }, "locate": { "message": "Locate the message" }, diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 953213fdbf..f35e31231a 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -885,6 +885,9 @@ }, "history": { "continue_chat": "チャットを続ける", + "error": { + "topic_not_found": "トピックが見つかりません" + }, "locate": { "message": "メッセージを探す" }, diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index a251854c34..69b64ce26f 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -885,6 +885,9 @@ }, "history": { "continue_chat": "Продолжить чат", + "error": { + "topic_not_found": "Топик не найден" + }, "locate": { "message": "Найти сообщение" }, diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 8b55525a9c..3164f068ab 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -885,6 +885,9 @@ }, "history": { "continue_chat": "继续聊天", + "error": { + "topic_not_found": "话题不存在" + }, "locate": { "message": "定位到消息" }, diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 2e2babab9c..df5500fc71 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -885,6 +885,9 @@ }, "history": { "continue_chat": "繼續聊天", + "error": { + "topic_not_found": "話題不存在" + }, "locate": { "message": "定位到訊息" }, 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 && (