feat: add topics history
This commit is contained in:
parent
2da3a3f010
commit
4cc140e4f2
@ -12,6 +12,7 @@ import { ThemeProvider } from './context/ThemeProvider'
|
|||||||
import AgentsPage from './pages/agents/AgentsPage'
|
import AgentsPage from './pages/agents/AgentsPage'
|
||||||
import AppsPage from './pages/apps/AppsPage'
|
import AppsPage from './pages/apps/AppsPage'
|
||||||
import FilesPage from './pages/files/FilesPage'
|
import FilesPage from './pages/files/FilesPage'
|
||||||
|
import HistoryPage from './pages/history/HistoryPage'
|
||||||
import HomePage from './pages/home/HomePage'
|
import HomePage from './pages/home/HomePage'
|
||||||
import SettingsPage from './pages/settings/SettingsPage'
|
import SettingsPage from './pages/settings/SettingsPage'
|
||||||
import TranslatePage from './pages/translate/TranslatePage'
|
import TranslatePage from './pages/translate/TranslatePage'
|
||||||
@ -31,6 +32,7 @@ function App(): JSX.Element {
|
|||||||
<Route path="/agents" element={<AgentsPage />} />
|
<Route path="/agents" element={<AgentsPage />} />
|
||||||
<Route path="/translate" element={<TranslatePage />} />
|
<Route path="/translate" element={<TranslatePage />} />
|
||||||
<Route path="/apps" element={<AppsPage />} />
|
<Route path="/apps" element={<AppsPage />} />
|
||||||
|
<Route path="/messages/*" element={<HistoryPage />} />
|
||||||
<Route path="/settings/*" element={<SettingsPage />} />
|
<Route path="/settings/*" element={<SettingsPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
|
|||||||
@ -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 { isMac } from '@renderer/config/constant'
|
||||||
import { isLocalAi, UserAvatar } from '@renderer/config/env'
|
import { isLocalAi, UserAvatar } from '@renderer/config/env'
|
||||||
import useAvatar from '@renderer/hooks/useAvatar'
|
import useAvatar from '@renderer/hooks/useAvatar'
|
||||||
@ -23,6 +23,7 @@ const Sidebar: FC = () => {
|
|||||||
const { windowStyle } = useSettings()
|
const { windowStyle } = useSettings()
|
||||||
|
|
||||||
const isRoute = (path: string): string => (pathname === path ? 'active' : '')
|
const isRoute = (path: string): string => (pathname === path ? 'active' : '')
|
||||||
|
const isRoutes = (path: string): string => (pathname.startsWith(path) ? 'active' : '')
|
||||||
|
|
||||||
const onEditUser = () => UserPopup.show()
|
const onEditUser = () => UserPopup.show()
|
||||||
|
|
||||||
@ -72,6 +73,11 @@ const Sidebar: FC = () => {
|
|||||||
<FolderOutlined />
|
<FolderOutlined />
|
||||||
</Icon>
|
</Icon>
|
||||||
</StyledLink>
|
</StyledLink>
|
||||||
|
<StyledLink onClick={() => to('/messages')}>
|
||||||
|
<Icon className={isRoutes('/messages')}>
|
||||||
|
<FileSearchOutlined />
|
||||||
|
</Icon>
|
||||||
|
</StyledLink>
|
||||||
</Menus>
|
</Menus>
|
||||||
</MainMenus>
|
</MainMenus>
|
||||||
<Menus onClick={MinApp.onClose}>
|
<Menus onClick={MinApp.onClose}>
|
||||||
|
|||||||
@ -71,7 +71,7 @@ export function useDefaultAssistant() {
|
|||||||
return {
|
return {
|
||||||
defaultAssistant: {
|
defaultAssistant: {
|
||||||
...defaultAssistant,
|
...defaultAssistant,
|
||||||
topics: [getDefaultTopic()]
|
topics: [getDefaultTopic(defaultAssistant.id)]
|
||||||
},
|
},
|
||||||
updateDefaultAssistant: (assistant: Assistant) => dispatch(updateDefaultAssistant({ assistant }))
|
updateDefaultAssistant: (assistant: Assistant) => dispatch(updateDefaultAssistant({ assistant }))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import db from '@renderer/databases'
|
import db from '@renderer/databases'
|
||||||
import { deleteMessageFiles } from '@renderer/services/messages'
|
import { deleteMessageFiles } from '@renderer/services/messages'
|
||||||
|
import store from '@renderer/store'
|
||||||
import { Assistant, Topic } from '@renderer/types'
|
import { Assistant, Topic } from '@renderer/types'
|
||||||
import { find } from 'lodash'
|
import { find } from 'lodash'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
@ -8,9 +9,9 @@ import { useAssistant } from './useAssistant'
|
|||||||
|
|
||||||
let _activeTopic: Topic
|
let _activeTopic: Topic
|
||||||
|
|
||||||
export function useActiveTopic(_assistant: Assistant) {
|
export function useActiveTopic(_assistant: Assistant, topic?: Topic) {
|
||||||
const { assistant } = useAssistant(_assistant.id)
|
const { assistant } = useAssistant(_assistant.id)
|
||||||
const [activeTopic, setActiveTopic] = useState(_activeTopic || assistant?.topics[0])
|
const [activeTopic, setActiveTopic] = useState(topic || _activeTopic || assistant?.topics[0])
|
||||||
|
|
||||||
_activeTopic = activeTopic
|
_activeTopic = activeTopic
|
||||||
|
|
||||||
@ -28,6 +29,14 @@ export function getTopic(assistant: Assistant, topicId: string) {
|
|||||||
return assistant?.topics.find((topic) => topic.id === topicId)
|
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 {
|
export class TopicManager {
|
||||||
static async getTopic(id: string) {
|
static async getTopic(id: string) {
|
||||||
return await db.topics.get(id)
|
return await db.topics.get(id)
|
||||||
|
|||||||
@ -141,6 +141,14 @@
|
|||||||
"tag.system": "System",
|
"tag.system": "System",
|
||||||
"tag.user": "Mine"
|
"tag.user": "Mine"
|
||||||
},
|
},
|
||||||
|
"minapp": {
|
||||||
|
"title": "MinApp"
|
||||||
|
},
|
||||||
|
"history": {
|
||||||
|
"title": "Topics Search",
|
||||||
|
"search.placeholder": "Search topics or messages...",
|
||||||
|
"continue_chat": "Continue Chatting"
|
||||||
|
},
|
||||||
"provider": {
|
"provider": {
|
||||||
"nvidia": "Nvidia",
|
"nvidia": "Nvidia",
|
||||||
"zhinao": "360AI",
|
"zhinao": "360AI",
|
||||||
@ -290,9 +298,6 @@
|
|||||||
"keep_alive_time.placeholder": "Minutes",
|
"keep_alive_time.placeholder": "Minutes",
|
||||||
"keep_alive_time.description": "The time in minutes to keep the connection alive, default is 5 minutes."
|
"keep_alive_time.description": "The time in minutes to keep the connection alive, default is 5 minutes."
|
||||||
},
|
},
|
||||||
"minapp": {
|
|
||||||
"title": "MinApp"
|
|
||||||
},
|
|
||||||
"error": {
|
"error": {
|
||||||
"chat.response": "Something went wrong. Please check if you have set your API key in the Settings > Providers",
|
"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"
|
"backup.file_format": "Backup file format error"
|
||||||
|
|||||||
@ -141,6 +141,14 @@
|
|||||||
"tag.system": "系统",
|
"tag.system": "系统",
|
||||||
"tag.user": "我的"
|
"tag.user": "我的"
|
||||||
},
|
},
|
||||||
|
"minapp": {
|
||||||
|
"title": "小程序"
|
||||||
|
},
|
||||||
|
"history": {
|
||||||
|
"title": "话题搜索",
|
||||||
|
"search.placeholder": "搜索话题或消息...",
|
||||||
|
"continue_chat": "继续聊天"
|
||||||
|
},
|
||||||
"provider": {
|
"provider": {
|
||||||
"nvidia": "英伟达",
|
"nvidia": "英伟达",
|
||||||
"zhinao": "360智脑",
|
"zhinao": "360智脑",
|
||||||
@ -290,9 +298,6 @@
|
|||||||
"keep_alive_time.placeholder": "分钟",
|
"keep_alive_time.placeholder": "分钟",
|
||||||
"keep_alive_time.description": "对话后模型在内存中保持的时间(默认:5分钟)"
|
"keep_alive_time.description": "对话后模型在内存中保持的时间(默认:5分钟)"
|
||||||
},
|
},
|
||||||
"minapp": {
|
|
||||||
"title": "小程序"
|
|
||||||
},
|
|
||||||
"error": {
|
"error": {
|
||||||
"chat.response": "出错了,如果没有配置 API 密钥,请前往设置 > 模型提供商中配置密钥",
|
"chat.response": "出错了,如果没有配置 API 密钥,请前往设置 > 模型提供商中配置密钥",
|
||||||
"backup.file_format": "备份文件格式错误"
|
"backup.file_format": "备份文件格式错误"
|
||||||
|
|||||||
@ -141,6 +141,14 @@
|
|||||||
"tag.system": "系統",
|
"tag.system": "系統",
|
||||||
"tag.user": "我的"
|
"tag.user": "我的"
|
||||||
},
|
},
|
||||||
|
"minapp": {
|
||||||
|
"title": "小程序"
|
||||||
|
},
|
||||||
|
"history": {
|
||||||
|
"title": "搜尋話題",
|
||||||
|
"search.placeholder": "搜尋話題或訊息...",
|
||||||
|
"continue_chat": "繼續聊天"
|
||||||
|
},
|
||||||
"provider": {
|
"provider": {
|
||||||
"nvidia": "輝達",
|
"nvidia": "輝達",
|
||||||
"zhinao": "360智腦",
|
"zhinao": "360智腦",
|
||||||
@ -290,9 +298,6 @@
|
|||||||
"keep_alive_time.placeholder": "分鐘",
|
"keep_alive_time.placeholder": "分鐘",
|
||||||
"keep_alive_time.description": "對話後模型在記憶體中保持的時間(預設為 5 分鐘)。"
|
"keep_alive_time.description": "對話後模型在記憶體中保持的時間(預設為 5 分鐘)。"
|
||||||
},
|
},
|
||||||
"minapp": {
|
|
||||||
"title": "小程序"
|
|
||||||
},
|
|
||||||
"error": {
|
"error": {
|
||||||
"chat.response": "出現錯誤。如果尚未配置 API 密鑰,請前往設定 > 模型提供者中配置密鑰",
|
"chat.response": "出現錯誤。如果尚未配置 API 密鑰,請前往設定 > 模型提供者中配置密鑰",
|
||||||
"backup.file_format": "備份文件格式錯誤"
|
"backup.file_format": "備份文件格式錯誤"
|
||||||
|
|||||||
157
src/renderer/src/pages/history/HistoryPage.tsx
Normal file
157
src/renderer/src/pages/history/HistoryPage.tsx
Normal file
@ -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<Route[]>(_stack)
|
||||||
|
const [topic, setTopic] = useState<Topic | undefined>(_topic)
|
||||||
|
const [message, setMessage] = useState<Message | undefined>(_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 (
|
||||||
|
<Container>
|
||||||
|
<Navbar>
|
||||||
|
<NavbarCenter style={{ borderRight: 'none', justifyContent: 'flex-start' }}>{t('history.title')} </NavbarCenter>
|
||||||
|
</Navbar>
|
||||||
|
<ContentContainer id="content-container">
|
||||||
|
<Header>
|
||||||
|
{stack.length > 1 && (
|
||||||
|
<HeaderLeft>
|
||||||
|
<MenuIcon onClick={goBack}>
|
||||||
|
<ArrowLeftOutlined />
|
||||||
|
</MenuIcon>
|
||||||
|
</HeaderLeft>
|
||||||
|
)}
|
||||||
|
<SearchInput
|
||||||
|
placeholder={t('history.search.placeholder')}
|
||||||
|
type="search"
|
||||||
|
value={search}
|
||||||
|
allowClear
|
||||||
|
onChange={(e) => setSearch(e.target.value.trimStart())}
|
||||||
|
suffix={search.length >= 2 ? <EnterOutlined /> : <SearchOutlined />}
|
||||||
|
onPressEnter={onSearch}
|
||||||
|
/>
|
||||||
|
</Header>
|
||||||
|
<Divider style={{ margin: 0 }} />
|
||||||
|
<TopicsHistory keywords={search} onClick={onTopicClick as any} style={{ display: isShow('topics') }} />
|
||||||
|
<TopicMessages topic={topic} style={{ display: isShow('topic') }} />
|
||||||
|
<SearchResults
|
||||||
|
keywords={search}
|
||||||
|
onMessageClick={onMessageClick}
|
||||||
|
onTopicClick={onTopicClick}
|
||||||
|
style={{ display: isShow('search') }}
|
||||||
|
/>
|
||||||
|
<SearchMessage message={message} style={{ display: isShow('message') }} />
|
||||||
|
</ContentContainer>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
38
src/renderer/src/pages/history/components/SearchMessage.tsx
Normal file
38
src/renderer/src/pages/history/components/SearchMessage.tsx
Normal file
@ -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<HTMLDivElement> {
|
||||||
|
message?: Message
|
||||||
|
}
|
||||||
|
|
||||||
|
const SearchMessage: FC<Props> = ({ message, ...props }) => {
|
||||||
|
if (!message) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MessagesContainer {...props}>
|
||||||
|
<ContainerWrapper style={{ paddingTop: 30, paddingBottom: 30 }}>
|
||||||
|
<MessageItem message={message} showMenu={false} />
|
||||||
|
</ContainerWrapper>
|
||||||
|
</MessagesContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
153
src/renderer/src/pages/history/components/SearchResults.tsx
Normal file
153
src/renderer/src/pages/history/components/SearchResults.tsx
Normal file
@ -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<HTMLDivElement> {
|
||||||
|
keywords: string
|
||||||
|
onMessageClick: (message: Message) => void
|
||||||
|
onTopicClick: (topic: Topic) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const SearchResults: FC<Props> = ({ keywords, onMessageClick, onTopicClick, ...props }) => {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
const [searchTerms, setSearchTerms] = useState<string[]>(
|
||||||
|
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) => `<mark>${match}</mark>`)
|
||||||
|
})
|
||||||
|
return <span dangerouslySetInnerHTML={{ __html: highlightedText }} />
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onSearch()
|
||||||
|
}, [onSearch])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container ref={containerRef} {...props}>
|
||||||
|
<ContainerWrapper>
|
||||||
|
{searchResults.length > 0 && (
|
||||||
|
<SearchStats>
|
||||||
|
Found {searchStats.count} results in {searchStats.time.toFixed(3)} seconds
|
||||||
|
</SearchStats>
|
||||||
|
)}
|
||||||
|
<List
|
||||||
|
itemLayout="vertical"
|
||||||
|
dataSource={searchResults}
|
||||||
|
pagination={{
|
||||||
|
pageSize: 10,
|
||||||
|
onChange: () => {
|
||||||
|
setTimeout(() => containerRef.current?.scrollTo({ top: 0 }), 0)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
renderItem={({ message, topic }) => (
|
||||||
|
<List.Item>
|
||||||
|
<Title
|
||||||
|
level={5}
|
||||||
|
style={{ color: 'var(--color-primary)', cursor: 'pointer' }}
|
||||||
|
onClick={async () => {
|
||||||
|
const _topic = await getTopicById(topic.id)
|
||||||
|
onTopicClick(_topic)
|
||||||
|
}}>
|
||||||
|
{topic.name}
|
||||||
|
</Title>
|
||||||
|
<div style={{ cursor: 'pointer' }} onClick={() => onMessageClick(message)}>
|
||||||
|
<Text>{highlightText(message.content)}</Text>
|
||||||
|
</div>
|
||||||
|
<SearchResultTime>
|
||||||
|
<Text type="secondary">{new Date(message.createdAt).toLocaleString()}</Text>
|
||||||
|
</SearchResultTime>
|
||||||
|
</List.Item>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div style={{ minHeight: 30 }}></div>
|
||||||
|
</ContainerWrapper>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
64
src/renderer/src/pages/history/components/TopicMessages.tsx
Normal file
64
src/renderer/src/pages/history/components/TopicMessages.tsx
Normal file
@ -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<HTMLDivElement> {
|
||||||
|
topic?: Topic
|
||||||
|
}
|
||||||
|
|
||||||
|
const TopicMessages: FC<Props> = ({ 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 (
|
||||||
|
<MessagesContainer {...props}>
|
||||||
|
<ContainerWrapper style={{ paddingTop: 30, paddingBottom: 30 }}>
|
||||||
|
{topic?.messages.map((message) => (
|
||||||
|
<div key={message.id}>
|
||||||
|
<MessageItem message={message} showMenu={false} />
|
||||||
|
<Divider style={{ margin: '10px auto' }} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{isEmpty && <Empty />}
|
||||||
|
{!isEmpty && (
|
||||||
|
<Button type="link" onClick={() => onContinueChat(topic)}>
|
||||||
|
{t('history.continue_chat')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ContainerWrapper>
|
||||||
|
</MessagesContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
114
src/renderer/src/pages/history/components/TopicsHistory.tsx
Normal file
114
src/renderer/src/pages/history/components/TopicsHistory.tsx
Normal file
@ -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<HTMLDivElement>
|
||||||
|
|
||||||
|
const GroupedTopics: React.FC<Props> = ({ 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 (
|
||||||
|
<ListContainer {...props}>
|
||||||
|
<ContainerWrapper>
|
||||||
|
<Empty />
|
||||||
|
</ContainerWrapper>
|
||||||
|
</ListContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListContainer {...props}>
|
||||||
|
<ContainerWrapper>
|
||||||
|
{Object.entries(groupedTopics).map(([date, items]) => (
|
||||||
|
<ListItem key={date}>
|
||||||
|
<Date>{date}</Date>
|
||||||
|
<Divider style={{ margin: '5px 0' }} />
|
||||||
|
{items.map((topic) => (
|
||||||
|
<TopicItem
|
||||||
|
key={topic.id}
|
||||||
|
onClick={async () => {
|
||||||
|
const _topic = await getTopicById(topic.id)
|
||||||
|
onClick(_topic)
|
||||||
|
}}>
|
||||||
|
<TopicName>{topic.name.substring(0, 50)}</TopicName>
|
||||||
|
<TopicDate>{dayjs(topic.updatedAt).format('HH:mm')}</TopicDate>
|
||||||
|
</TopicItem>
|
||||||
|
))}
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
<div style={{ minHeight: 30 }}></div>
|
||||||
|
</ContainerWrapper>
|
||||||
|
</ListContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
@ -63,7 +63,7 @@ const Assistants: FC<Props> = ({
|
|||||||
key: 'duplicate',
|
key: 'duplicate',
|
||||||
icon: <CopyIcon />,
|
icon: <CopyIcon />,
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
const _assistant: Assistant = { ...assistant, id: uuid(), topics: [getDefaultTopic()] }
|
const _assistant: Assistant = { ...assistant, id: uuid(), topics: [getDefaultTopic(assistant.id)] }
|
||||||
addAssistant(_assistant)
|
addAssistant(_assistant)
|
||||||
setActiveAssistant(_assistant)
|
setActiveAssistant(_assistant)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { useShowAssistants } from '@renderer/hooks/useStore'
|
|||||||
import { useActiveTopic } from '@renderer/hooks/useTopic'
|
import { useActiveTopic } from '@renderer/hooks/useTopic'
|
||||||
import { Assistant } from '@renderer/types'
|
import { Assistant } from '@renderer/types'
|
||||||
import { FC, useState } from 'react'
|
import { FC, useState } from 'react'
|
||||||
|
import { useLocation } from 'react-router-dom'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import Chat from './Chat'
|
import Chat from './Chat'
|
||||||
@ -13,9 +14,13 @@ let _activeAssistant: Assistant
|
|||||||
|
|
||||||
const HomePage: FC = () => {
|
const HomePage: FC = () => {
|
||||||
const { assistants } = useAssistants()
|
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 { showAssistants } = useShowAssistants()
|
||||||
const { activeTopic, setActiveTopic } = useActiveTopic(activeAssistant)
|
|
||||||
|
|
||||||
_activeAssistant = activeAssistant
|
_activeAssistant = activeAssistant
|
||||||
|
|
||||||
|
|||||||
@ -123,11 +123,11 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const addNewTopic = useCallback(() => {
|
const addNewTopic = useCallback(() => {
|
||||||
const topic = getDefaultTopic()
|
const topic = getDefaultTopic(assistant.id)
|
||||||
addTopic(topic)
|
addTopic(topic)
|
||||||
setActiveTopic(topic)
|
setActiveTopic(topic)
|
||||||
db.topics.add({ id: topic.id, messages: [] })
|
db.topics.add({ id: topic.id, messages: [] })
|
||||||
}, [addTopic, setActiveTopic])
|
}, [addTopic, assistant.id, setActiveTopic])
|
||||||
|
|
||||||
const clearTopic = async () => {
|
const clearTopic = async () => {
|
||||||
if (generating) {
|
if (generating) {
|
||||||
|
|||||||
@ -1,23 +1,15 @@
|
|||||||
import UserPopup from '@renderer/components/Popups/UserPopup'
|
|
||||||
import { FONT_FAMILY } from '@renderer/config/constant'
|
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 { useAssistant } from '@renderer/hooks/useAssistant'
|
||||||
import useAvatar from '@renderer/hooks/useAvatar'
|
|
||||||
import { useModel } from '@renderer/hooks/useModel'
|
import { useModel } from '@renderer/hooks/useModel'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { Message } from '@renderer/types'
|
import { Message } from '@renderer/types'
|
||||||
import { firstLetter, removeLeadingEmoji } from '@renderer/utils'
|
import { Divider } from 'antd'
|
||||||
import { Avatar, Divider } from 'antd'
|
import { FC, memo, useMemo } from 'react'
|
||||||
import dayjs from 'dayjs'
|
|
||||||
import { upperFirst } from 'lodash'
|
|
||||||
import { FC, memo, useCallback, useMemo } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import MessageContent from './MessageContent'
|
import MessageContent from './MessageContent'
|
||||||
|
import MessageHeader from './MessageHeader'
|
||||||
import MessageMenubar from './MessageMenubar'
|
import MessageMenubar from './MessageMenubar'
|
||||||
import MessgeTokens from './MessageTokens'
|
import MessgeTokens from './MessageTokens'
|
||||||
|
|
||||||
@ -26,45 +18,26 @@ interface Props {
|
|||||||
index?: number
|
index?: number
|
||||||
total?: number
|
total?: number
|
||||||
lastMessage?: boolean
|
lastMessage?: boolean
|
||||||
|
showMenu?: boolean
|
||||||
onEditMessage?: (message: Message) => void
|
onEditMessage?: (message: Message) => void
|
||||||
onDeleteMessage?: (message: Message) => void
|
onDeleteMessage?: (message: Message) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const MessageItem: FC<Props> = ({ message, index, lastMessage, onEditMessage, onDeleteMessage }) => {
|
const MessageItem: FC<Props> = ({ message, index, lastMessage, showMenu = true, onEditMessage, onDeleteMessage }) => {
|
||||||
const avatar = useAvatar()
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { assistant, setModel } = useAssistant(message.assistantId)
|
const { assistant, setModel } = useAssistant(message.assistantId)
|
||||||
const model = useModel(message.modelId)
|
const model = useModel(message.modelId)
|
||||||
const { userName, showMessageDivider, messageFont, fontSize } = useSettings()
|
const { showMessageDivider, messageFont, fontSize } = useSettings()
|
||||||
const { theme } = useTheme()
|
|
||||||
|
|
||||||
const isLastMessage = lastMessage || index === 0
|
const isLastMessage = lastMessage || index === 0
|
||||||
const isAssistantMessage = message.role === 'assistant'
|
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(() => {
|
const fontFamily = useMemo(() => {
|
||||||
return messageFont === 'serif' ? FONT_FAMILY.replace('sans-serif', 'serif').replace('Ubuntu, ', '') : FONT_FAMILY
|
return messageFont === 'serif' ? FONT_FAMILY.replace('sans-serif', 'serif').replace('Ubuntu, ', '') : FONT_FAMILY
|
||||||
}, [messageFont])
|
}, [messageFont])
|
||||||
|
|
||||||
const messageBorder = showMessageDivider ? undefined : 'none'
|
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') {
|
if (message.type === 'clear') {
|
||||||
return (
|
return (
|
||||||
<Divider dashed style={{ padding: '0 20px' }} plain>
|
<Divider dashed style={{ padding: '0 20px' }} plain>
|
||||||
@ -75,38 +48,10 @@ const MessageItem: FC<Props> = ({ message, index, lastMessage, onEditMessage, on
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<MessageContainer key={message.id} className="message">
|
<MessageContainer key={message.id} className="message">
|
||||||
<MessageHeader>
|
<MessageHeader message={message} assistant={assistant} model={model} />
|
||||||
<AvatarWrapper>
|
|
||||||
{isAssistantMessage ? (
|
|
||||||
<Avatar
|
|
||||||
src={avatarSource}
|
|
||||||
size={35}
|
|
||||||
style={{
|
|
||||||
borderRadius: '20%',
|
|
||||||
cursor: 'pointer',
|
|
||||||
border: isLocalAi ? '1px solid var(--color-border-soft)' : 'none',
|
|
||||||
filter: theme === 'dark' ? 'invert(0.05)' : undefined
|
|
||||||
}}
|
|
||||||
onClick={showMiniApp}>
|
|
||||||
{avatarName}
|
|
||||||
</Avatar>
|
|
||||||
) : (
|
|
||||||
<Avatar
|
|
||||||
src={avatar}
|
|
||||||
size={35}
|
|
||||||
style={{ borderRadius: '20%', cursor: 'pointer' }}
|
|
||||||
onClick={() => UserPopup.show()}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<UserWrap>
|
|
||||||
<UserName>{username}</UserName>
|
|
||||||
<MessageTime>{dayjs(message.createdAt).format('MM/DD HH:mm')}</MessageTime>
|
|
||||||
</UserWrap>
|
|
||||||
</AvatarWrapper>
|
|
||||||
</MessageHeader>
|
|
||||||
<MessageContentContainer style={{ fontFamily, fontSize }}>
|
<MessageContentContainer style={{ fontFamily, fontSize }}>
|
||||||
<MessageContent message={message} model={model} />
|
<MessageContent message={message} model={model} />
|
||||||
{!lastMessage && (
|
{!lastMessage && showMenu && (
|
||||||
<MessageFooter style={{ border: messageBorder, flexDirection: isLastMessage ? 'row-reverse' : undefined }}>
|
<MessageFooter style={{ border: messageBorder, flexDirection: isLastMessage ? 'row-reverse' : undefined }}>
|
||||||
<MessgeTokens message={message} />
|
<MessgeTokens message={message} />
|
||||||
<MessageMenubar
|
<MessageMenubar
|
||||||
@ -145,38 +90,6 @@ const MessageContainer = styled.div`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const MessageHeader = 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);
|
|
||||||
`
|
|
||||||
|
|
||||||
const MessageContentContainer = styled.div`
|
const MessageContentContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
114
src/renderer/src/pages/home/Messages/MessageHeader.tsx
Normal file
114
src/renderer/src/pages/home/Messages/MessageHeader.tsx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import UserPopup from '@renderer/components/Popups/UserPopup'
|
||||||
|
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 useAvatar from '@renderer/hooks/useAvatar'
|
||||||
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
|
import { Assistant, Message, Model } from '@renderer/types'
|
||||||
|
import { firstLetter, removeLeadingEmoji } from '@renderer/utils'
|
||||||
|
import { Avatar } from 'antd'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { upperFirst } from 'lodash'
|
||||||
|
import { FC, useCallback, useMemo } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
message: Message
|
||||||
|
assistant: Assistant
|
||||||
|
model?: Model
|
||||||
|
}
|
||||||
|
|
||||||
|
const MessageHeader: FC<Props> = ({ 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 (
|
||||||
|
<Container>
|
||||||
|
<AvatarWrapper>
|
||||||
|
{isAssistantMessage ? (
|
||||||
|
<Avatar
|
||||||
|
src={avatarSource}
|
||||||
|
size={35}
|
||||||
|
style={{
|
||||||
|
borderRadius: '20%',
|
||||||
|
cursor: 'pointer',
|
||||||
|
border: isLocalAi ? '1px solid var(--color-border-soft)' : 'none',
|
||||||
|
filter: theme === 'dark' ? 'invert(0.05)' : undefined
|
||||||
|
}}
|
||||||
|
onClick={showMiniApp}>
|
||||||
|
{avatarName}
|
||||||
|
</Avatar>
|
||||||
|
) : (
|
||||||
|
<Avatar
|
||||||
|
src={avatar}
|
||||||
|
size={35}
|
||||||
|
style={{ borderRadius: '20%', cursor: 'pointer' }}
|
||||||
|
onClick={() => UserPopup.show()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<UserWrap>
|
||||||
|
<UserName>{username}</UserName>
|
||||||
|
<MessageTime>{dayjs(message.createdAt).format('MM/DD HH:mm')}</MessageTime>
|
||||||
|
</UserWrap>
|
||||||
|
</AvatarWrapper>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
@ -164,7 +164,7 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
|
|||||||
} as Message)
|
} as Message)
|
||||||
}),
|
}),
|
||||||
EventEmitter.on(EVENT_NAMES.NEW_BRANCH, async (index: number) => {
|
EventEmitter.on(EVENT_NAMES.NEW_BRANCH, async (index: number) => {
|
||||||
const newTopic = getDefaultTopic()
|
const newTopic = getDefaultTopic(assistant.id)
|
||||||
newTopic.name = topic.name
|
newTopic.name = topic.name
|
||||||
const branchMessages = take(messages, messages.length - index)
|
const branchMessages = take(messages, messages.length - index)
|
||||||
|
|
||||||
|
|||||||
@ -33,13 +33,13 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveTopic }) => {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const addNewTopic = useCallback(() => {
|
const addNewTopic = useCallback(() => {
|
||||||
const topic = getDefaultTopic()
|
const topic = getDefaultTopic(assistant.id)
|
||||||
addTopic(topic)
|
addTopic(topic)
|
||||||
setActiveTopic(topic)
|
setActiveTopic(topic)
|
||||||
db.topics.add({ id: topic.id, messages: [] })
|
db.topics.add({ id: topic.id, messages: [] })
|
||||||
window.message.success({ content: t('message.topic.added'), key: 'topic-added' })
|
window.message.success({ content: t('message.topic.added'), key: 'topic-added' })
|
||||||
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 0)
|
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 0)
|
||||||
}, [addTopic, setActiveTopic, t])
|
}, [addTopic, assistant.id, setActiveTopic, t])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Navbar>
|
<Navbar>
|
||||||
|
|||||||
@ -11,7 +11,7 @@ export function getDefaultAssistant(): Assistant {
|
|||||||
id: 'default',
|
id: 'default',
|
||||||
name: i18n.t('chat.default.name'),
|
name: i18n.t('chat.default.name'),
|
||||||
prompt: '',
|
prompt: '',
|
||||||
topics: [getDefaultTopic()]
|
topics: [getDefaultTopic('default')]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -19,9 +19,10 @@ export function getDefaultAssistantSettings() {
|
|||||||
return store.getState().assistants.defaultAssistant.settings
|
return store.getState().assistants.defaultAssistant.settings
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDefaultTopic(): Topic {
|
export function getDefaultTopic(assistantId: string): Topic {
|
||||||
return {
|
return {
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
|
assistantId,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
name: i18n.t('chat.default.topic.name'),
|
name: i18n.t('chat.default.topic.name'),
|
||||||
@ -86,10 +87,12 @@ export const getAssistantSettings = (assistant: Assistant): AssistantSettings =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function covertAgentToAssistant(agent: Agent): Assistant {
|
export function covertAgentToAssistant(agent: Agent): Assistant {
|
||||||
|
const id = agent.group === 'system' ? uuid() : String(agent.id)
|
||||||
return {
|
return {
|
||||||
...getDefaultAssistant(),
|
...getDefaultAssistant(),
|
||||||
...agent,
|
...agent,
|
||||||
id: agent.group === 'system' ? uuid() : String(agent.id),
|
id,
|
||||||
|
topics: [getDefaultTopic(id)],
|
||||||
name: getAssistantNameWithAgent(agent),
|
name: getAssistantNameWithAgent(agent),
|
||||||
settings: getDefaultAssistantSettings()
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@ -94,7 +94,7 @@ const assistantsSlice = createSlice({
|
|||||||
assistant.topics.forEach((topic) => TopicManager.removeTopic(topic.id))
|
assistant.topics.forEach((topic) => TopicManager.removeTopic(topic.id))
|
||||||
return {
|
return {
|
||||||
...assistant,
|
...assistant,
|
||||||
topics: [getDefaultTopic()]
|
topics: [getDefaultTopic(assistant.id)]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return assistant
|
return assistant
|
||||||
|
|||||||
@ -22,7 +22,7 @@ const persistedReducer = persistReducer(
|
|||||||
{
|
{
|
||||||
key: 'cherry-studio',
|
key: 'cherry-studio',
|
||||||
storage,
|
storage,
|
||||||
version: 28,
|
version: 29,
|
||||||
blacklist: ['runtime'],
|
blacklist: ['runtime'],
|
||||||
migrate
|
migrate
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -36,6 +36,7 @@ export type Message = {
|
|||||||
|
|
||||||
export type Topic = {
|
export type Topic = {
|
||||||
id: string
|
id: string
|
||||||
|
assistantId: string
|
||||||
name: string
|
name: string
|
||||||
createdAt: string
|
createdAt: string
|
||||||
updatedAt: string
|
updatedAt: string
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user