diff --git a/package.json b/package.json index 950d46a82..258bf6b72 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "CherryStudio", - "version": "0.7.11", + "version": "0.7.12", "private": true, "description": "A powerful AI assistant for producer.", "main": "./out/main/index.js", diff --git a/src/renderer/src/assets/styles/ant.scss b/src/renderer/src/assets/styles/ant.scss index 36f2244e9..71e8c58a1 100644 --- a/src/renderer/src/assets/styles/ant.scss +++ b/src/renderer/src/assets/styles/ant.scss @@ -41,6 +41,9 @@ } .segmented-tab { + .ant-segmented-item { + overflow: hidden; + } .ant-segmented-item-selected { background-color: var(--color-background-mute); } diff --git a/src/renderer/src/context/AntdProvider.tsx b/src/renderer/src/context/AntdProvider.tsx index 281a1d606..22c14f520 100644 --- a/src/renderer/src/context/AntdProvider.tsx +++ b/src/renderer/src/context/AntdProvider.tsx @@ -19,7 +19,10 @@ const AntdProvider: FC = ({ children }) => { Segmented: { trackBg: 'transparent', itemSelectedBg: isDarkTheme ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.05)', - boxShadowTertiary: undefined + boxShadowTertiary: undefined, + borderRadiusLG: 12, + borderRadiusSM: 12, + borderRadiusXS: 12 }, Menu: { activeBarBorderWidth: 0, diff --git a/src/renderer/src/hooks/useScrollPosition.ts b/src/renderer/src/hooks/useScrollPosition.ts new file mode 100644 index 000000000..ddcd54028 --- /dev/null +++ b/src/renderer/src/hooks/useScrollPosition.ts @@ -0,0 +1,20 @@ +import { throttle } from 'lodash' +import { useEffect, useRef } from 'react' + +export default function useScrollPosition(key: string) { + const containerRef = useRef(null) + const scrollKey = `scroll:${key}` + + const handleScroll = throttle(() => { + const position = containerRef.current?.scrollTop ?? 0 + window.keyv.set(scrollKey, position) + }, 100) + + useEffect(() => { + const scroll = () => containerRef.current?.scrollTo({ top: window.keyv.get(scrollKey) || 0 }) + scroll() + setTimeout(scroll, 50) + }, [scrollKey]) + + return { containerRef, handleScroll } +} diff --git a/src/renderer/src/i18n/en-us.json b/src/renderer/src/i18n/en-us.json index 9c8176ef6..1884c4f17 100644 --- a/src/renderer/src/i18n/en-us.json +++ b/src/renderer/src/i18n/en-us.json @@ -147,7 +147,9 @@ "history": { "title": "Topics Search", "search.placeholder": "Search topics or messages...", - "continue_chat": "Continue Chatting" + "continue_chat": "Continue Chatting", + "search.topics.empty": "No topics found, press Enter to search all messages", + "locate.message": "Locate the message" }, "provider": { "nvidia": "Nvidia", diff --git a/src/renderer/src/i18n/zh-cn.json b/src/renderer/src/i18n/zh-cn.json index 39d4e3c6a..b9c2860d9 100644 --- a/src/renderer/src/i18n/zh-cn.json +++ b/src/renderer/src/i18n/zh-cn.json @@ -147,7 +147,9 @@ "history": { "title": "话题搜索", "search.placeholder": "搜索话题或消息...", - "continue_chat": "继续聊天" + "continue_chat": "继续聊天", + "search.topics.empty": "没有找到相关话题, 点击回车键搜索所有消息", + "locate.message": "定位到消息" }, "provider": { "nvidia": "英伟达", diff --git a/src/renderer/src/i18n/zh-tw.json b/src/renderer/src/i18n/zh-tw.json index c000dfc2f..580dc8957 100644 --- a/src/renderer/src/i18n/zh-tw.json +++ b/src/renderer/src/i18n/zh-tw.json @@ -147,7 +147,9 @@ "history": { "title": "搜尋話題", "search.placeholder": "搜尋話題或訊息...", - "continue_chat": "繼續聊天" + "continue_chat": "繼續聊天", + "search.topics.empty": "沒有找到相關話題, 點擊回車鍵搜尋所有訊息", + "locate.message": "定位到訊息" }, "provider": { "nvidia": "輝達", diff --git a/src/renderer/src/pages/history/HistoryPage.tsx b/src/renderer/src/pages/history/HistoryPage.tsx index 994cfb1f9..8b716e590 100644 --- a/src/renderer/src/pages/history/HistoryPage.tsx +++ b/src/renderer/src/pages/history/HistoryPage.tsx @@ -109,7 +109,7 @@ const ContentContainer = styled.div` flex-direction: column; align-items: center; height: 100%; - overflow: hidden; + overflow-y: scroll; ` const Header = styled.div` diff --git a/src/renderer/src/pages/history/components/SearchMessage.tsx b/src/renderer/src/pages/history/components/SearchMessage.tsx index ccc263aba..816c86085 100644 --- a/src/renderer/src/pages/history/components/SearchMessage.tsx +++ b/src/renderer/src/pages/history/components/SearchMessage.tsx @@ -1,12 +1,12 @@ -import { getTopicById } from '@renderer/hooks/useTopic' +import { ArrowRightOutlined } from '@ant-design/icons' +import { HStack } from '@renderer/components/Layout' import { default as MessageItem } from '@renderer/pages/home/Messages/Message' -import { getAssistantById } from '@renderer/services/assistant' -import { EVENT_NAMES, EventEmitter } from '@renderer/services/event' +import { locateToMessage } from '@renderer/services/messages' import { Message } from '@renderer/types' import { Button } from 'antd' import { FC } from 'react' import { useTranslation } from 'react-i18next' -import { useNavigate } from 'react-router-dom' +import { useNavigate } from 'react-router' import styled from 'styled-components' interface Props extends React.HTMLAttributes { @@ -14,27 +14,29 @@ interface Props extends React.HTMLAttributes { } const SearchMessage: FC = ({ message, ...props }) => { - const { t } = useTranslation() const navigate = useNavigate() + const { t } = useTranslation() if (!message) { return null } - const onContinueChat = async (message: Message) => { - const assistant = getAssistantById(message.assistantId) - const topic = await getTopicById(message.topicId) - navigate('/', { state: { assistant, topic } }) - setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 100) - } - return ( - + - + + ) @@ -52,6 +54,9 @@ const ContainerWrapper = styled.div` width: 800px; display: flex; flex-direction: column; + .message { + padding: 0; + } ` export default SearchMessage diff --git a/src/renderer/src/pages/history/components/SearchResults.tsx b/src/renderer/src/pages/history/components/SearchResults.tsx index 25bd60172..0705bd976 100644 --- a/src/renderer/src/pages/history/components/SearchResults.tsx +++ b/src/renderer/src/pages/history/components/SearchResults.tsx @@ -1,9 +1,10 @@ import db from '@renderer/databases' +import useScrollPosition from '@renderer/hooks/useScrollPosition' import { getTopicById } from '@renderer/hooks/useTopic' import { Message, Topic } from '@renderer/types' import { List, Typography } from 'antd' import { useLiveQuery } from 'dexie-react-hooks' -import { FC, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { FC, memo, useCallback, useEffect, useMemo, useState } from 'react' import styled from 'styled-components' const { Text, Title } = Typography @@ -15,7 +16,7 @@ interface Props extends React.HTMLAttributes { } const SearchResults: FC = ({ keywords, onMessageClick, onTopicClick, ...props }) => { - const containerRef = useRef(null) + const { handleScroll, containerRef } = useScrollPosition('SearchResults') const [searchTerms, setSearchTerms] = useState( keywords @@ -84,7 +85,7 @@ const SearchResults: FC = ({ keywords, onMessageClick, onTopicClick, ...p }, [onSearch]) return ( - + {searchResults.length > 0 && ( diff --git a/src/renderer/src/pages/history/components/TopicMessages.tsx b/src/renderer/src/pages/history/components/TopicMessages.tsx index 2665f2d31..c8a34191d 100644 --- a/src/renderer/src/pages/history/components/TopicMessages.tsx +++ b/src/renderer/src/pages/history/components/TopicMessages.tsx @@ -1,5 +1,9 @@ +import { ArrowRightOutlined, MessageOutlined } from '@ant-design/icons' +import { HStack } from '@renderer/components/Layout' +import useScrollPosition from '@renderer/hooks/useScrollPosition' import { getAssistantById } from '@renderer/services/assistant' import { EVENT_NAMES, EventEmitter } from '@renderer/services/event' +import { locateToMessage } from '@renderer/services/messages' import { Topic } from '@renderer/types' import { Button, Divider, Empty } from 'antd' import { t } from 'i18next' @@ -15,6 +19,8 @@ interface Props extends React.HTMLAttributes { const TopicMessages: FC = ({ topic, ...props }) => { const navigate = useNavigate() + const { handleScroll, containerRef } = useScrollPosition('TopicMessages') + const isEmpty = (topic?.messages || []).length === 0 if (!topic) { @@ -28,19 +34,28 @@ const TopicMessages: FC = ({ topic, ...props }) => { } return ( - + {topic?.messages.map((message) => ( -
+
- +
))} {isEmpty && } {!isEmpty && ( - + + + )} @@ -59,6 +74,9 @@ const ContainerWrapper = styled.div` width: 800px; display: flex; flex-direction: column; + .message { + padding: 0; + } ` export default TopicMessages diff --git a/src/renderer/src/pages/history/components/TopicsHistory.tsx b/src/renderer/src/pages/history/components/TopicsHistory.tsx index 2aa7efffb..34d0a4d06 100644 --- a/src/renderer/src/pages/history/components/TopicsHistory.tsx +++ b/src/renderer/src/pages/history/components/TopicsHistory.tsx @@ -1,9 +1,11 @@ import { useAssistants } from '@renderer/hooks/useAssistant' +import useScrollPosition from '@renderer/hooks/useScrollPosition' import { getTopicById } from '@renderer/hooks/useTopic' import { Topic } from '@renderer/types' import { Divider, Empty } from 'antd' import dayjs from 'dayjs' import { groupBy, isEmpty, orderBy } from 'lodash' +import { useTranslation } from 'react-i18next' import styled from 'styled-components' type Props = { @@ -11,8 +13,10 @@ type Props = { onClick: (topic: Topic) => void } & React.HTMLAttributes -const GroupedTopics: React.FC = ({ keywords, onClick, ...props }) => { +const TopicsHistory: React.FC = ({ keywords, onClick, ...props }) => { const { assistants } = useAssistants() + const { t } = useTranslation() + const { handleScroll, containerRef } = useScrollPosition('TopicsHistory') const topics = orderBy(assistants.map((assistant) => assistant.topics).flat(), 'createdAt', 'desc') @@ -28,14 +32,14 @@ const GroupedTopics: React.FC = ({ keywords, onClick, ...props }) => { return ( - + ) } return ( - + {Object.entries(groupedTopics).map(([date, items]) => ( @@ -60,8 +64,6 @@ const GroupedTopics: React.FC = ({ keywords, onClick, ...props }) => { ) } -GroupedTopics.displayName = 'GroupedTopics' - const ContainerWrapper = styled.div` width: 800px; display: flex; @@ -111,4 +113,4 @@ const TopicDate = styled.div` margin-left: 10px; ` -export default GroupedTopics +export default TopicsHistory diff --git a/src/renderer/src/pages/home/Assistants.tsx b/src/renderer/src/pages/home/Assistants.tsx index fe0c149af..e87ccabb9 100644 --- a/src/renderer/src/pages/home/Assistants.tsx +++ b/src/renderer/src/pages/home/Assistants.tsx @@ -222,7 +222,7 @@ const AssistantItem = styled.div` display: flex; flex-direction: row; justify-content: space-between; - padding: 7px 10px; + padding: 7px 12px; position: relative; border-radius: 17px; margin: 0 10px; diff --git a/src/renderer/src/pages/home/Markdown/CodeBlock.tsx b/src/renderer/src/pages/home/Markdown/CodeBlock.tsx index 1bb12a140..2c5404cf2 100644 --- a/src/renderer/src/pages/home/Markdown/CodeBlock.tsx +++ b/src/renderer/src/pages/home/Markdown/CodeBlock.tsx @@ -81,7 +81,7 @@ const CodeHeader = styled.div` color: var(--color-text); font-size: 14px; font-weight: bold; - background-color: var(--color-code-background); + /* background-color: var(--color-code-background); */ height: 36px; padding: 0 10px; border-top-left-radius: 8px; diff --git a/src/renderer/src/pages/home/Messages/Message.tsx b/src/renderer/src/pages/home/Messages/Message.tsx index 2e8c000c1..1b84abea3 100644 --- a/src/renderer/src/pages/home/Messages/Message.tsx +++ b/src/renderer/src/pages/home/Messages/Message.tsx @@ -2,9 +2,10 @@ import { FONT_FAMILY } from '@renderer/config/constant' import { useAssistant } from '@renderer/hooks/useAssistant' import { useModel } from '@renderer/hooks/useModel' import { useSettings } from '@renderer/hooks/useSettings' +import { EVENT_NAMES, EventEmitter } from '@renderer/services/event' import { Message } from '@renderer/types' import { Divider } from 'antd' -import { FC, memo, useMemo } from 'react' +import { FC, memo, useEffect, useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' @@ -28,6 +29,7 @@ const MessageItem: FC = ({ message, index, lastMessage, showMenu = true, const { assistant, setModel } = useAssistant(message.assistantId) const model = useModel(message.modelId) const { showMessageDivider, messageFont, fontSize } = useSettings() + const messageRef = useRef(null) const isLastMessage = lastMessage || index === 0 const isAssistantMessage = message.role === 'assistant' @@ -38,6 +40,23 @@ const MessageItem: FC = ({ message, index, lastMessage, showMenu = true, const messageBorder = showMessageDivider ? undefined : 'none' + useEffect(() => { + const unsubscribes = [ + EventEmitter.on(EVENT_NAMES.LOCATE_MESSAGE + ':' + message.id, () => { + if (messageRef.current) { + messageRef.current.scrollIntoView({ behavior: 'smooth' }) + setTimeout(() => { + messageRef.current?.classList.add('message-highlight') + setTimeout(() => { + messageRef.current?.classList.remove('message-highlight') + }, 2500) + }, 500) + } + }) + ] + return () => unsubscribes.forEach((unsub) => unsub()) + }, [message]) + if (message.type === 'clear') { return ( @@ -47,7 +66,7 @@ const MessageItem: FC = ({ message, index, lastMessage, showMenu = true, } return ( - + @@ -74,8 +93,12 @@ const MessageItem: FC = ({ message, index, lastMessage, showMenu = true, const MessageContainer = styled.div` display: flex; flex-direction: column; - padding: 0 20px; + padding: 15px 20px 0 20px; position: relative; + transition: background-color 0.3s ease; + &.message-highlight { + background-color: var(--color-primary-mute); + } .menubar { opacity: 0; transition: opacity 0.2s ease; @@ -105,7 +128,7 @@ const MessageFooter = styled.div` justify-content: space-between; align-items: center; padding: 2px 0; - margin: 2px 0 8px 0; + margin-top: 2px; border-top: 0.5px dashed var(--color-border); ` diff --git a/src/renderer/src/pages/home/Messages/Prompt.tsx b/src/renderer/src/pages/home/Messages/Prompt.tsx index 49505a80f..3c1582124 100644 --- a/src/renderer/src/pages/home/Messages/Prompt.tsx +++ b/src/renderer/src/pages/home/Messages/Prompt.tsx @@ -28,7 +28,7 @@ const Container = styled.div` padding: 10px 20px; background-color: var(--color-background-soft); margin-bottom: 20px; - margin: 0 20px 20px 20px; + margin: 0 20px 0 20px; border-radius: 6px; cursor: pointer; ` diff --git a/src/renderer/src/pages/home/RightSidebar.tsx b/src/renderer/src/pages/home/RightSidebar.tsx index 600dcc584..d7b54a8aa 100644 --- a/src/renderer/src/pages/home/RightSidebar.tsx +++ b/src/renderer/src/pages/home/RightSidebar.tsx @@ -103,6 +103,7 @@ const RightSidebar: FC = ({ activeAssistant, activeTopic, setActiveAssist borderRadius: 0, padding: '10px 0', margin: '0 10px', + paddingBottom: 10, borderBottom: '0.5px solid var(--color-border)', gap: 2 }} diff --git a/src/renderer/src/pages/home/Topics.tsx b/src/renderer/src/pages/home/Topics.tsx index 5e29a6461..d082aa562 100644 --- a/src/renderer/src/pages/home/Topics.tsx +++ b/src/renderer/src/pages/home/Topics.tsx @@ -182,7 +182,7 @@ const Container = styled.div` ` const TopicListItem = styled.div` - padding: 7px 10px; + padding: 7px 12px; margin: 0 10px; border-radius: 17px; font-family: Ubuntu; diff --git a/src/renderer/src/services/event.ts b/src/renderer/src/services/event.ts index 2fe1c8b79..a4a673083 100644 --- a/src/renderer/src/services/event.ts +++ b/src/renderer/src/services/event.ts @@ -18,5 +18,6 @@ export const EVENT_NAMES = { SWITCH_TOPIC_SIDEBAR: 'SWITCH_TOPIC_SIDEBAR', NEW_CONTEXT: 'NEW_CONTEXT', NEW_BRANCH: 'NEW_BRANCH', - EXPORT_TOPIC_IMAGE: 'EXPORT_TOPIC_IMAGE' + EXPORT_TOPIC_IMAGE: 'EXPORT_TOPIC_IMAGE', + LOCATE_MESSAGE: 'LOCATE_MESSAGE' } diff --git a/src/renderer/src/services/messages.ts b/src/renderer/src/services/messages.ts index 29a3a8ff7..48dad09c9 100644 --- a/src/renderer/src/services/messages.ts +++ b/src/renderer/src/services/messages.ts @@ -1,7 +1,11 @@ import { DEFAULT_CONEXTCOUNT } from '@renderer/config/constant' +import { getTopicById } from '@renderer/hooks/useTopic' import { Assistant, Message } from '@renderer/types' import { isEmpty, takeRight } from 'lodash' +import { NavigateFunction } from 'react-router' +import { getAssistantById } from './assistant' +import { EVENT_NAMES, EventEmitter } from './event' import FileManager from './file' export const filterMessages = (messages: Message[]) => { @@ -36,3 +40,11 @@ export function getContextCount(assistant: Assistant, messages: Message[]) { export function deleteMessageFiles(message: Message) { message.files && FileManager.deleteFiles(message.files.map((f) => f.id)) } + +export async function locateToMessage(navigate: NavigateFunction, message: Message) { + const assistant = getAssistantById(message.assistantId) + const topic = await getTopicById(message.topicId) + navigate('/', { state: { assistant, topic } }) + setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 0) + setTimeout(() => EventEmitter.emit(EVENT_NAMES.LOCATE_MESSAGE + ':' + message.id), 300) +}