diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index e978362c..030473a2 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -12,6 +12,7 @@ import { ThemeProvider } from './context/ThemeProvider' import AgentsPage from './pages/agents/AgentsPage' import AppsPage from './pages/apps/AppsPage' import FilesPage from './pages/files/FilesPage' +import HistoryPage from './pages/history/HistoryPage' import HomePage from './pages/home/HomePage' import SettingsPage from './pages/settings/SettingsPage' import TranslatePage from './pages/translate/TranslatePage' @@ -31,6 +32,7 @@ function App(): JSX.Element { } /> } /> } /> + } /> } /> diff --git a/src/renderer/src/components/app/Sidebar.tsx b/src/renderer/src/components/app/Sidebar.tsx index c565eb51..4c12316b 100644 --- a/src/renderer/src/components/app/Sidebar.tsx +++ b/src/renderer/src/components/app/Sidebar.tsx @@ -1,4 +1,4 @@ -import { FolderOutlined, TranslationOutlined } from '@ant-design/icons' +import { FileSearchOutlined, FolderOutlined, TranslationOutlined } from '@ant-design/icons' import { isMac } from '@renderer/config/constant' import { isLocalAi, UserAvatar } from '@renderer/config/env' import useAvatar from '@renderer/hooks/useAvatar' @@ -23,6 +23,7 @@ const Sidebar: FC = () => { const { windowStyle } = useSettings() const isRoute = (path: string): string => (pathname === path ? 'active' : '') + const isRoutes = (path: string): string => (pathname.startsWith(path) ? 'active' : '') const onEditUser = () => UserPopup.show() @@ -72,6 +73,11 @@ const Sidebar: FC = () => { + to('/messages')}> + + + + diff --git a/src/renderer/src/hooks/useAssistant.ts b/src/renderer/src/hooks/useAssistant.ts index 0c3a5a4c..bc4007cc 100644 --- a/src/renderer/src/hooks/useAssistant.ts +++ b/src/renderer/src/hooks/useAssistant.ts @@ -71,7 +71,7 @@ export function useDefaultAssistant() { return { defaultAssistant: { ...defaultAssistant, - topics: [getDefaultTopic()] + topics: [getDefaultTopic(defaultAssistant.id)] }, updateDefaultAssistant: (assistant: Assistant) => dispatch(updateDefaultAssistant({ assistant })) } diff --git a/src/renderer/src/hooks/useTopic.ts b/src/renderer/src/hooks/useTopic.ts index 8727c59c..e4e547b9 100644 --- a/src/renderer/src/hooks/useTopic.ts +++ b/src/renderer/src/hooks/useTopic.ts @@ -1,5 +1,6 @@ import db from '@renderer/databases' import { deleteMessageFiles } from '@renderer/services/messages' +import store from '@renderer/store' import { Assistant, Topic } from '@renderer/types' import { find } from 'lodash' import { useEffect, useState } from 'react' @@ -8,9 +9,9 @@ import { useAssistant } from './useAssistant' let _activeTopic: Topic -export function useActiveTopic(_assistant: Assistant) { +export function useActiveTopic(_assistant: Assistant, topic?: Topic) { const { assistant } = useAssistant(_assistant.id) - const [activeTopic, setActiveTopic] = useState(_activeTopic || assistant?.topics[0]) + const [activeTopic, setActiveTopic] = useState(topic || _activeTopic || assistant?.topics[0]) _activeTopic = activeTopic @@ -28,6 +29,14 @@ export function getTopic(assistant: Assistant, topicId: string) { return assistant?.topics.find((topic) => topic.id === topicId) } +export async function getTopicById(topicId: string) { + const assistants = store.getState().assistants.assistants + const topics = assistants.map((assistant) => assistant.topics).flat() + const topic = topics.find((topic) => topic.id === topicId) + const messages = await TopicManager.getTopicMessages(topicId) + return { ...topic, messages } as Topic +} + export class TopicManager { static async getTopic(id: string) { return await db.topics.get(id) diff --git a/src/renderer/src/i18n/en-us.json b/src/renderer/src/i18n/en-us.json index 69c00f88..9c8176ef 100644 --- a/src/renderer/src/i18n/en-us.json +++ b/src/renderer/src/i18n/en-us.json @@ -141,6 +141,14 @@ "tag.system": "System", "tag.user": "Mine" }, + "minapp": { + "title": "MinApp" + }, + "history": { + "title": "Topics Search", + "search.placeholder": "Search topics or messages...", + "continue_chat": "Continue Chatting" + }, "provider": { "nvidia": "Nvidia", "zhinao": "360AI", @@ -290,9 +298,6 @@ "keep_alive_time.placeholder": "Minutes", "keep_alive_time.description": "The time in minutes to keep the connection alive, default is 5 minutes." }, - "minapp": { - "title": "MinApp" - }, "error": { "chat.response": "Something went wrong. Please check if you have set your API key in the Settings > Providers", "backup.file_format": "Backup file format error" diff --git a/src/renderer/src/i18n/zh-cn.json b/src/renderer/src/i18n/zh-cn.json index 85ef60cf..39d4e3c6 100644 --- a/src/renderer/src/i18n/zh-cn.json +++ b/src/renderer/src/i18n/zh-cn.json @@ -141,6 +141,14 @@ "tag.system": "系统", "tag.user": "我的" }, + "minapp": { + "title": "小程序" + }, + "history": { + "title": "话题搜索", + "search.placeholder": "搜索话题或消息...", + "continue_chat": "继续聊天" + }, "provider": { "nvidia": "英伟达", "zhinao": "360智脑", @@ -290,9 +298,6 @@ "keep_alive_time.placeholder": "分钟", "keep_alive_time.description": "对话后模型在内存中保持的时间(默认:5分钟)" }, - "minapp": { - "title": "小程序" - }, "error": { "chat.response": "出错了,如果没有配置 API 密钥,请前往设置 > 模型提供商中配置密钥", "backup.file_format": "备份文件格式错误" diff --git a/src/renderer/src/i18n/zh-tw.json b/src/renderer/src/i18n/zh-tw.json index 476920fe..c000dfc2 100644 --- a/src/renderer/src/i18n/zh-tw.json +++ b/src/renderer/src/i18n/zh-tw.json @@ -141,6 +141,14 @@ "tag.system": "系統", "tag.user": "我的" }, + "minapp": { + "title": "小程序" + }, + "history": { + "title": "搜尋話題", + "search.placeholder": "搜尋話題或訊息...", + "continue_chat": "繼續聊天" + }, "provider": { "nvidia": "輝達", "zhinao": "360智腦", @@ -290,9 +298,6 @@ "keep_alive_time.placeholder": "分鐘", "keep_alive_time.description": "對話後模型在記憶體中保持的時間(預設為 5 分鐘)。" }, - "minapp": { - "title": "小程序" - }, "error": { "chat.response": "出現錯誤。如果尚未配置 API 密鑰,請前往設定 > 模型提供者中配置密鑰", "backup.file_format": "備份文件格式錯誤" diff --git a/src/renderer/src/pages/history/HistoryPage.tsx b/src/renderer/src/pages/history/HistoryPage.tsx new file mode 100644 index 00000000..994cfb1f --- /dev/null +++ b/src/renderer/src/pages/history/HistoryPage.tsx @@ -0,0 +1,157 @@ +import { ArrowLeftOutlined, EnterOutlined, SearchOutlined } from '@ant-design/icons' +import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' +import { Message, Topic } from '@renderer/types' +import { Divider, Input } from 'antd' +import { last } from 'lodash' +import { FC, useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +import SearchMessage from './components/SearchMessage' +import SearchResults from './components/SearchResults' +import TopicMessages from './components/TopicMessages' +import TopicsHistory from './components/TopicsHistory' + +type Route = 'topics' | 'topic' | 'search' | 'message' + +let _search = '' +let _stack: Route[] = ['topics'] +let _topic: Topic | undefined +let _message: Message | undefined + +const TopicsPage: FC = () => { + const { t } = useTranslation() + const [search, setSearch] = useState(_search) + const [stack, setStack] = useState(_stack) + const [topic, setTopic] = useState(_topic) + const [message, setMessage] = useState(_message) + + _search = search + _stack = stack + _topic = topic + _message = message + + const goBack = () => { + const _stack = [...stack] + const route = _stack.pop() + setStack(_stack) + route === 'search' && setSearch('') + route === 'topic' && setTopic(undefined) + route === 'message' && setMessage(undefined) + } + + const onSearch = () => { + setStack(['topics', 'search']) + setTopic(undefined) + } + + const onTopicClick = (topic: Topic) => { + setStack((prev) => [...prev, 'topic']) + setTopic(topic) + } + + const onMessageClick = (message: Message) => { + setStack(['topics', 'search', 'message']) + setMessage(message) + } + + const isShow = (route: Route) => (last(stack) === route ? 'flex' : 'none') + + return ( + + + {t('history.title')} + + + + {stack.length > 1 && ( + + + + + + )} + setSearch(e.target.value.trimStart())} + suffix={search.length >= 2 ? : } + onPressEnter={onSearch} + /> + + + + + + + + + ) +} + +const Container = styled.div` + display: flex; + flex: 1; + flex-direction: column; + height: 100%; +` + +const ContentContainer = styled.div` + display: flex; + flex: 1; + flex-direction: column; + align-items: center; + height: 100%; + overflow: hidden; +` + +const Header = styled.div` + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + padding: 8px 20px; + width: 100%; + position: relative; +` + +const HeaderLeft = styled.div` + display: flex; + flex-direction: row; + align-items: center; + position: absolute; + top: 8px; + left: 15px; +` + +const MenuIcon = styled.div` + cursor: pointer; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + width: 36px; + height: 36px; + border-radius: 50%; + &:hover { + background-color: var(--color-background-mute); + .anticon { + color: var(--color-text-1); + } + } +` + +const SearchInput = styled(Input)` + border-radius: 30px; + width: 800px; + height: 36px; +` + +export default TopicsPage diff --git a/src/renderer/src/pages/history/components/SearchMessage.tsx b/src/renderer/src/pages/history/components/SearchMessage.tsx new file mode 100644 index 00000000..9ad5b5a9 --- /dev/null +++ b/src/renderer/src/pages/history/components/SearchMessage.tsx @@ -0,0 +1,38 @@ +import { default as MessageItem } from '@renderer/pages/home/Messages/Message' +import { Message } from '@renderer/types' +import { FC } from 'react' +import styled from 'styled-components' + +interface Props extends React.HTMLAttributes { + message?: Message +} + +const SearchMessage: FC = ({ message, ...props }) => { + if (!message) { + return null + } + + return ( + + + + + + ) +} + +const MessagesContainer = styled.div` + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + overflow-y: scroll; +` + +const ContainerWrapper = styled.div` + width: 800px; + display: flex; + flex-direction: column; +` + +export default SearchMessage diff --git a/src/renderer/src/pages/history/components/SearchResults.tsx b/src/renderer/src/pages/history/components/SearchResults.tsx new file mode 100644 index 00000000..25bd6017 --- /dev/null +++ b/src/renderer/src/pages/history/components/SearchResults.tsx @@ -0,0 +1,153 @@ +import db from '@renderer/databases' +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 styled from 'styled-components' + +const { Text, Title } = Typography + +interface Props extends React.HTMLAttributes { + keywords: string + onMessageClick: (message: Message) => void + onTopicClick: (topic: Topic) => void +} + +const SearchResults: FC = ({ keywords, onMessageClick, onTopicClick, ...props }) => { + const containerRef = useRef(null) + + const [searchTerms, setSearchTerms] = useState( + keywords + .toLowerCase() + .split(' ') + .filter((term) => term.length > 0) + ) + + const topics = useLiveQuery(() => db.topics.toArray(), []) + + const messages = useMemo( + () => (topics || [])?.map((topic) => topic.messages.filter((message) => message.role !== 'user')).flat(), + [topics] + ) + + const [searchResults, setSearchResults] = useState<{ message: Message; topic: Topic }[]>([]) + const [searchStats, setSearchStats] = useState({ count: 0, time: 0 }) + + const removeMarkdown = (text: string) => { + return text + .replace(/\*\*(.*?)\*\*/g, '$1') + .replace(/\*(.*?)\*/g, '$1') + .replace(/\[(.*?)\]\((.*?)\)/g, '$1') + .replace(/```[\s\S]*?```/g, '') + .replace(/`(.*?)`/g, '$1') + .replace(/#+\s/g, '') + .replace(/<[^>]*>/g, '') + } + + const onSearch = useCallback(async () => { + setSearchResults([]) + const startTime = performance.now() + const results: { message: Message; topic: Topic }[] = [] + const newSearchTerms = keywords + .toLowerCase() + .split(' ') + .filter((term) => term.length > 0) + + for (const message of messages) { + const cleanContent = removeMarkdown(message.content.toLowerCase()) + if (newSearchTerms.every((term) => cleanContent.includes(term))) { + results.push({ message, topic: await getTopicById(message.topicId)! }) + } + } + + const endTime = performance.now() + setSearchResults(results) + setSearchStats({ + count: results.length, + time: (endTime - startTime) / 1000 + }) + setSearchTerms(newSearchTerms) + }, [messages, keywords]) + + const highlightText = (text: string) => { + let highlightedText = removeMarkdown(text) + searchTerms.forEach((term) => { + const regex = new RegExp(term, 'gi') + highlightedText = highlightedText.replace(regex, (match) => `${match}`) + }) + return + } + + useEffect(() => { + onSearch() + }, [onSearch]) + + return ( + + + {searchResults.length > 0 && ( + + Found {searchStats.count} results in {searchStats.time.toFixed(3)} seconds + + )} + { + setTimeout(() => containerRef.current?.scrollTo({ top: 0 }), 0) + } + }} + renderItem={({ message, topic }) => ( + + { + const _topic = await getTopicById(topic.id) + onTopicClick(_topic) + }}> + {topic.name} + + onMessageClick(message)}> + {highlightText(message.content)} + + + {new Date(message.createdAt).toLocaleString()} + + + )} + /> + + + + ) +} + +const Container = styled.div` + width: 100%; + padding: 20px; + overflow-y: auto; + display: flex; + flex-direction: row; + justify-content: center; +` + +const ContainerWrapper = styled.div` + width: 800px; + display: flex; + flex-direction: column; +` + +const SearchStats = styled.div` + font-size: 13px; + color: var(--color-text-3); +` + +const SearchResultTime = styled.div` + margin-top: 10px; +` + +export default memo(SearchResults) diff --git a/src/renderer/src/pages/history/components/TopicMessages.tsx b/src/renderer/src/pages/history/components/TopicMessages.tsx new file mode 100644 index 00000000..2665f2d3 --- /dev/null +++ b/src/renderer/src/pages/history/components/TopicMessages.tsx @@ -0,0 +1,64 @@ +import { getAssistantById } from '@renderer/services/assistant' +import { EVENT_NAMES, EventEmitter } from '@renderer/services/event' +import { Topic } from '@renderer/types' +import { Button, Divider, Empty } from 'antd' +import { t } from 'i18next' +import { FC } from 'react' +import { useNavigate } from 'react-router' +import styled from 'styled-components' + +import { default as MessageItem } from '../../home/Messages/Message' + +interface Props extends React.HTMLAttributes { + topic?: Topic +} + +const TopicMessages: FC = ({ topic, ...props }) => { + const navigate = useNavigate() + const isEmpty = (topic?.messages || []).length === 0 + + if (!topic) { + return null + } + + const onContinueChat = (topic: Topic) => { + const assistant = getAssistantById(topic.assistantId) + navigate('/', { state: { assistant, topic } }) + setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 100) + } + + return ( + + + {topic?.messages.map((message) => ( + + + + + ))} + {isEmpty && } + {!isEmpty && ( + onContinueChat(topic)}> + {t('history.continue_chat')} + + )} + + + ) +} + +const MessagesContainer = styled.div` + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + overflow-y: scroll; +` + +const ContainerWrapper = styled.div` + width: 800px; + display: flex; + flex-direction: column; +` + +export default TopicMessages diff --git a/src/renderer/src/pages/history/components/TopicsHistory.tsx b/src/renderer/src/pages/history/components/TopicsHistory.tsx new file mode 100644 index 00000000..2aa7efff --- /dev/null +++ b/src/renderer/src/pages/history/components/TopicsHistory.tsx @@ -0,0 +1,114 @@ +import { useAssistants } from '@renderer/hooks/useAssistant' +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 styled from 'styled-components' + +type Props = { + keywords: string + onClick: (topic: Topic) => void +} & React.HTMLAttributes + +const GroupedTopics: React.FC = ({ keywords, onClick, ...props }) => { + const { assistants } = useAssistants() + + const topics = orderBy(assistants.map((assistant) => assistant.topics).flat(), 'createdAt', 'desc') + + const filteredTopics = topics.filter((topic) => { + return topic.name.toLowerCase().includes(keywords.toLowerCase()) + }) + + const groupedTopics = groupBy(filteredTopics, (topic) => { + return dayjs(topic.createdAt).format('MM/DD') + }) + + if (isEmpty(filteredTopics)) { + return ( + + + + + + ) + } + + return ( + + + {Object.entries(groupedTopics).map(([date, items]) => ( + + {date} + + {items.map((topic) => ( + { + const _topic = await getTopicById(topic.id) + onClick(_topic) + }}> + {topic.name.substring(0, 50)} + {dayjs(topic.updatedAt).format('HH:mm')} + + ))} + + ))} + + + + ) +} + +GroupedTopics.displayName = 'GroupedTopics' + +const ContainerWrapper = styled.div` + width: 800px; + display: flex; + flex-direction: column; +` + +const ListContainer = styled.div` + display: flex; + flex: 1; + flex-direction: column; + overflow-y: scroll; + width: 100%; + align-items: center; + padding-top: 20px; + padding-bottom: 20px; +` + +const ListItem = styled.div` + display: flex; + flex-direction: column; + margin-bottom: 15px; +` + +const Date = styled.div` + font-size: 26px; + font-weight: bold; + color: var(--color-text-3); +` + +const TopicItem = styled.div` + cursor: pointer; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + height: 30px; +` + +const TopicName = styled.div` + font-size: 14px; + color: var(--color-text); +` + +const TopicDate = styled.div` + font-size: 14px; + color: var(--color-text-3); + margin-left: 10px; +` + +export default GroupedTopics diff --git a/src/renderer/src/pages/home/Assistants.tsx b/src/renderer/src/pages/home/Assistants.tsx index 9a0bc5ce..b8ed5c6d 100644 --- a/src/renderer/src/pages/home/Assistants.tsx +++ b/src/renderer/src/pages/home/Assistants.tsx @@ -63,7 +63,7 @@ const Assistants: FC = ({ key: 'duplicate', icon: , onClick: async () => { - const _assistant: Assistant = { ...assistant, id: uuid(), topics: [getDefaultTopic()] } + const _assistant: Assistant = { ...assistant, id: uuid(), topics: [getDefaultTopic(assistant.id)] } addAssistant(_assistant) setActiveAssistant(_assistant) } diff --git a/src/renderer/src/pages/home/HomePage.tsx b/src/renderer/src/pages/home/HomePage.tsx index bb46db4c..cb7f468e 100644 --- a/src/renderer/src/pages/home/HomePage.tsx +++ b/src/renderer/src/pages/home/HomePage.tsx @@ -3,6 +3,7 @@ import { useShowAssistants } from '@renderer/hooks/useStore' import { useActiveTopic } from '@renderer/hooks/useTopic' import { Assistant } from '@renderer/types' import { FC, useState } from 'react' +import { useLocation } from 'react-router-dom' import styled from 'styled-components' import Chat from './Chat' @@ -13,9 +14,13 @@ let _activeAssistant: Assistant const HomePage: FC = () => { const { assistants } = useAssistants() - const [activeAssistant, setActiveAssistant] = useState(_activeAssistant || assistants[0]) + + const location = useLocation() + const state = location.state + + const [activeAssistant, setActiveAssistant] = useState(state?.assistant || _activeAssistant || assistants[0]) + const { activeTopic, setActiveTopic } = useActiveTopic(activeAssistant, state?.topic) const { showAssistants } = useShowAssistants() - const { activeTopic, setActiveTopic } = useActiveTopic(activeAssistant) _activeAssistant = activeAssistant diff --git a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx index 0c6dd195..3e86975d 100644 --- a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx +++ b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx @@ -123,11 +123,11 @@ const Inputbar: FC = ({ assistant, setActiveTopic }) => { } const addNewTopic = useCallback(() => { - const topic = getDefaultTopic() + const topic = getDefaultTopic(assistant.id) addTopic(topic) setActiveTopic(topic) db.topics.add({ id: topic.id, messages: [] }) - }, [addTopic, setActiveTopic]) + }, [addTopic, assistant.id, setActiveTopic]) const clearTopic = async () => { if (generating) { diff --git a/src/renderer/src/pages/home/Messages/Message.tsx b/src/renderer/src/pages/home/Messages/Message.tsx index 96220ab4..2e8c000c 100644 --- a/src/renderer/src/pages/home/Messages/Message.tsx +++ b/src/renderer/src/pages/home/Messages/Message.tsx @@ -1,23 +1,15 @@ -import UserPopup from '@renderer/components/Popups/UserPopup' import { FONT_FAMILY } from '@renderer/config/constant' -import { APP_NAME, AppLogo, isLocalAi } from '@renderer/config/env' -import { startMinAppById } from '@renderer/config/minapps' -import { getModelLogo } from '@renderer/config/models' -import { useTheme } from '@renderer/context/ThemeProvider' import { useAssistant } from '@renderer/hooks/useAssistant' -import useAvatar from '@renderer/hooks/useAvatar' import { useModel } from '@renderer/hooks/useModel' import { useSettings } from '@renderer/hooks/useSettings' import { Message } from '@renderer/types' -import { firstLetter, removeLeadingEmoji } from '@renderer/utils' -import { Avatar, Divider } from 'antd' -import dayjs from 'dayjs' -import { upperFirst } from 'lodash' -import { FC, memo, useCallback, useMemo } from 'react' +import { Divider } from 'antd' +import { FC, memo, useMemo } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' import MessageContent from './MessageContent' +import MessageHeader from './MessageHeader' import MessageMenubar from './MessageMenubar' import MessgeTokens from './MessageTokens' @@ -26,45 +18,26 @@ interface Props { index?: number total?: number lastMessage?: boolean + showMenu?: boolean onEditMessage?: (message: Message) => void onDeleteMessage?: (message: Message) => void } -const MessageItem: FC = ({ message, index, lastMessage, onEditMessage, onDeleteMessage }) => { - const avatar = useAvatar() +const MessageItem: FC = ({ message, index, lastMessage, showMenu = true, onEditMessage, onDeleteMessage }) => { const { t } = useTranslation() const { assistant, setModel } = useAssistant(message.assistantId) const model = useModel(message.modelId) - const { userName, showMessageDivider, messageFont, fontSize } = useSettings() - const { theme } = useTheme() + const { showMessageDivider, messageFont, fontSize } = useSettings() const isLastMessage = lastMessage || index === 0 const isAssistantMessage = message.role === 'assistant' - const getUserName = useCallback(() => { - if (isLocalAi && message.role !== 'user') return APP_NAME - if (message.role === 'assistant') return upperFirst(model?.name || model?.id) - return userName || t('common.you') - }, [message.role, model?.id, model?.name, t, userName]) - const fontFamily = useMemo(() => { return messageFont === 'serif' ? FONT_FAMILY.replace('sans-serif', 'serif').replace('Ubuntu, ', '') : FONT_FAMILY }, [messageFont]) const messageBorder = showMessageDivider ? undefined : 'none' - const avatarSource = useMemo(() => { - if (isLocalAi) return AppLogo - return message.modelId ? getModelLogo(message.modelId) : undefined - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [message.modelId, theme]) - - const avatarName = useMemo(() => firstLetter(assistant?.name).toUpperCase(), [assistant?.name]) - - const username = useMemo(() => removeLeadingEmoji(getUserName()), [getUserName]) - - const showMiniApp = () => model?.provider && startMinAppById(model?.provider) - if (message.type === 'clear') { return ( @@ -75,38 +48,10 @@ const MessageItem: FC = ({ message, index, lastMessage, onEditMessage, on return ( - - - {isAssistantMessage ? ( - - {avatarName} - - ) : ( - UserPopup.show()} - /> - )} - - {username} - {dayjs(message.createdAt).format('MM/DD HH:mm')} - - - + - {!lastMessage && ( + {!lastMessage && showMenu && ( = ({ assistant, model, message }) => { + const avatar = useAvatar() + const { theme } = useTheme() + const { userName } = useSettings() + const { t } = useTranslation() + + const isAssistantMessage = message.role === 'assistant' + + const avatarSource = useMemo(() => { + if (isLocalAi) return AppLogo + return message.modelId ? getModelLogo(message.modelId) : undefined + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [message.modelId, theme]) + + const getUserName = useCallback(() => { + if (isLocalAi && message.role !== 'user') return APP_NAME + if (message.role === 'assistant') return upperFirst(model?.name || model?.id) + return userName || t('common.you') + }, [message.role, model?.id, model?.name, t, userName]) + + const avatarName = useMemo(() => firstLetter(assistant?.name).toUpperCase(), [assistant?.name]) + + const username = useMemo(() => removeLeadingEmoji(getUserName()), [getUserName]) + + const showMiniApp = () => model?.provider && startMinAppById(model?.provider) + + return ( + + + {isAssistantMessage ? ( + + {avatarName} + + ) : ( + UserPopup.show()} + /> + )} + + {username} + {dayjs(message.createdAt).format('MM/DD HH:mm')} + + + + ) +} + +const Container = styled.div` + margin-right: 10px; + display: flex; + flex-direction: row; + align-items: center; + padding-bottom: 4px; + justify-content: space-between; +` + +const AvatarWrapper = styled.div` + display: flex; + flex-direction: row; + align-items: center; +` + +const UserWrap = styled.div` + display: flex; + flex-direction: column; + justify-content: space-between; + margin-left: 12px; +` + +const UserName = styled.div` + font-size: 14px; + font-weight: 600; +` + +const MessageTime = styled.div` + font-size: 12px; + color: var(--color-text-3); +` + +export default MessageHeader diff --git a/src/renderer/src/pages/home/Messages/Messages.tsx b/src/renderer/src/pages/home/Messages/Messages.tsx index 3d1fa791..f20e6d3a 100644 --- a/src/renderer/src/pages/home/Messages/Messages.tsx +++ b/src/renderer/src/pages/home/Messages/Messages.tsx @@ -164,7 +164,7 @@ const Messages: FC = ({ assistant, topic, setActiveTopic }) => { } as Message) }), EventEmitter.on(EVENT_NAMES.NEW_BRANCH, async (index: number) => { - const newTopic = getDefaultTopic() + const newTopic = getDefaultTopic(assistant.id) newTopic.name = topic.name const branchMessages = take(messages, messages.length - index) diff --git a/src/renderer/src/pages/home/Navbar.tsx b/src/renderer/src/pages/home/Navbar.tsx index 91085c1d..6bf83a2e 100644 --- a/src/renderer/src/pages/home/Navbar.tsx +++ b/src/renderer/src/pages/home/Navbar.tsx @@ -33,13 +33,13 @@ const HeaderNavbar: FC = ({ activeAssistant, setActiveTopic }) => { const { t } = useTranslation() const addNewTopic = useCallback(() => { - const topic = getDefaultTopic() + const topic = getDefaultTopic(assistant.id) addTopic(topic) setActiveTopic(topic) db.topics.add({ id: topic.id, messages: [] }) window.message.success({ content: t('message.topic.added'), key: 'topic-added' }) setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 0) - }, [addTopic, setActiveTopic, t]) + }, [addTopic, assistant.id, setActiveTopic, t]) return ( diff --git a/src/renderer/src/services/assistant.ts b/src/renderer/src/services/assistant.ts index 9cbcd314..a836b114 100644 --- a/src/renderer/src/services/assistant.ts +++ b/src/renderer/src/services/assistant.ts @@ -11,7 +11,7 @@ export function getDefaultAssistant(): Assistant { id: 'default', name: i18n.t('chat.default.name'), prompt: '', - topics: [getDefaultTopic()] + topics: [getDefaultTopic('default')] } } @@ -19,9 +19,10 @@ export function getDefaultAssistantSettings() { return store.getState().assistants.defaultAssistant.settings } -export function getDefaultTopic(): Topic { +export function getDefaultTopic(assistantId: string): Topic { return { id: uuid(), + assistantId, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), name: i18n.t('chat.default.topic.name'), @@ -86,10 +87,12 @@ export const getAssistantSettings = (assistant: Assistant): AssistantSettings => } export function covertAgentToAssistant(agent: Agent): Assistant { + const id = agent.group === 'system' ? uuid() : String(agent.id) return { ...getDefaultAssistant(), ...agent, - id: agent.group === 'system' ? uuid() : String(agent.id), + id, + topics: [getDefaultTopic(id)], name: getAssistantNameWithAgent(agent), settings: getDefaultAssistantSettings() } @@ -129,3 +132,8 @@ export function syncAgentToAssistant(agent: Agent) { ) } } + +export function getAssistantById(id: string) { + const assistants = store.getState().assistants.assistants + return assistants.find((a) => a.id === id) +} diff --git a/src/renderer/src/store/assistants.ts b/src/renderer/src/store/assistants.ts index 78f40b8c..9d8084d1 100644 --- a/src/renderer/src/store/assistants.ts +++ b/src/renderer/src/store/assistants.ts @@ -94,7 +94,7 @@ const assistantsSlice = createSlice({ assistant.topics.forEach((topic) => TopicManager.removeTopic(topic.id)) return { ...assistant, - topics: [getDefaultTopic()] + topics: [getDefaultTopic(assistant.id)] } } return assistant diff --git a/src/renderer/src/store/index.ts b/src/renderer/src/store/index.ts index b93b0c73..d3d57111 100644 --- a/src/renderer/src/store/index.ts +++ b/src/renderer/src/store/index.ts @@ -22,7 +22,7 @@ const persistedReducer = persistReducer( { key: 'cherry-studio', storage, - version: 28, + version: 29, blacklist: ['runtime'], migrate }, diff --git a/src/renderer/src/store/migrate.ts b/src/renderer/src/store/migrate.ts index edfbfbea..6609ed91 100644 --- a/src/renderer/src/store/migrate.ts +++ b/src/renderer/src/store/migrate.ts @@ -489,6 +489,21 @@ const migrateConfig = { ] } } + }, + '29': (state: RootState) => { + return { + ...state, + assistants: { + ...state.assistants, + assistants: state.assistants.assistants.map((assistant) => { + assistant.topics = assistant.topics.map((topic) => ({ + ...topic, + assistantId: assistant.id + })) + return assistant + }) + } + } } } diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index 416fedce..d6b6c54f 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -36,6 +36,7 @@ export type Message = { export type Topic = { id: string + assistantId: string name: string createdAt: string updatedAt: string