mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-04 11:49:02 +08:00
refactor(ChatProvider, useChat): implement context for chat state management and enhance event handling
- Introduced ChatProvider to manage active assistant and topic state using React context. - Updated useChat hook to utilize context for accessing chat state and actions. - Refactored message navigation to emit events for setting active assistant and topic. - Improved topic selection logic to ensure active topic is valid when topics change. - Integrated ChatProvider into HomePage and HistoryPage for consistent chat state management.
This commit is contained in:
parent
58f3edb352
commit
cdfa2ac13a
@ -1,5 +1,5 @@
|
|||||||
import { IpcChannel } from '@shared/IpcChannel'
|
import { IpcChannel } from '@shared/IpcChannel'
|
||||||
import { AnimatePresence, motion } from 'framer-motion'
|
import { AnimatePresence, easeInOut, motion } from 'framer-motion'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom'
|
import { Route, Routes, useLocation, useNavigate } from 'react-router-dom'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
@ -94,9 +94,9 @@ const PageContainer = styled(motion.div)`
|
|||||||
`
|
`
|
||||||
|
|
||||||
const pageTransition = {
|
const pageTransition = {
|
||||||
type: 'tween',
|
type: 'tween' as const,
|
||||||
duration: 0.25,
|
duration: 0.25,
|
||||||
ease: [0.4, 0.0, 0.2, 1]
|
ease: easeInOut
|
||||||
}
|
}
|
||||||
|
|
||||||
const pageVariants = {
|
const pageVariants = {
|
||||||
|
|||||||
@ -1,22 +1,43 @@
|
|||||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||||
import { setActiveAssistant, setActiveTopic } from '@renderer/store/runtime'
|
|
||||||
import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
|
import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
|
||||||
import { Assistant } from '@renderer/types'
|
import { Assistant } from '@renderer/types'
|
||||||
import { Topic } from '@renderer/types'
|
import { Topic } from '@renderer/types'
|
||||||
import { useEffect } from 'react'
|
import { use, useEffect, useMemo, useState } from 'react'
|
||||||
|
import { createContext } from 'react'
|
||||||
|
|
||||||
import { useAssistants, useTopicsForAssistant } from './useAssistant'
|
import { useTopicsForAssistant } from './useAssistant'
|
||||||
import { useSettings } from './useSettings'
|
import { useSettings } from './useSettings'
|
||||||
|
|
||||||
export const useChat = () => {
|
interface ChatContextType {
|
||||||
const { assistants } = useAssistants()
|
activeAssistant: Assistant
|
||||||
const activeAssistant = useAppSelector((state) => state.runtime.chat.activeAssistant) || assistants[0]
|
activeTopic: Topic
|
||||||
|
setActiveAssistant: (assistant: Assistant) => void
|
||||||
|
setActiveTopic: (topic: Topic) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChatContext = createContext<ChatContextType | null>(null)
|
||||||
|
|
||||||
|
export const ChatProvider = ({ children }) => {
|
||||||
|
const assistants = useAppSelector((state) => state.assistants.assistants)
|
||||||
|
const [activeAssistant, setActiveAssistant] = useState<Assistant>(assistants[0])
|
||||||
const topics = useTopicsForAssistant(activeAssistant.id)
|
const topics = useTopicsForAssistant(activeAssistant.id)
|
||||||
const activeTopic = useAppSelector((state) => state.runtime.chat.activeTopic) || topics[0]
|
const [activeTopic, setActiveTopic] = useState<Topic>(topics[0])
|
||||||
const { clickAssistantToShowTopic } = useSettings()
|
const { clickAssistantToShowTopic } = useSettings()
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
|
console.log('activeAssistant', activeAssistant)
|
||||||
|
console.log('activeTopic', activeTopic)
|
||||||
|
|
||||||
|
// 当 topics 变化时,如果当前 activeTopic 不在 topics 中,设置第一个 topic
|
||||||
|
useEffect(() => {
|
||||||
|
if (!topics.find((topic) => topic.id === activeTopic?.id)) {
|
||||||
|
const firstTopic = topics[0]
|
||||||
|
firstTopic && setActiveTopic(firstTopic)
|
||||||
|
}
|
||||||
|
}, [topics, activeTopic?.id])
|
||||||
|
|
||||||
|
// 当 activeTopic 变化时加载消息
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeTopic) {
|
if (activeTopic) {
|
||||||
dispatch(loadTopicMessagesThunk(activeTopic.id))
|
dispatch(loadTopicMessagesThunk(activeTopic.id))
|
||||||
@ -24,28 +45,38 @@ export const useChat = () => {
|
|||||||
}
|
}
|
||||||
}, [activeTopic, dispatch])
|
}, [activeTopic, dispatch])
|
||||||
|
|
||||||
useEffect(() => {
|
// 处理点击助手显示话题侧边栏
|
||||||
if (topics.find((topic) => topic.id === activeTopic?.id)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const firstTopic = topics[0]
|
|
||||||
firstTopic && dispatch(setActiveTopic(firstTopic))
|
|
||||||
}, [activeAssistant, activeTopic?.id, dispatch, topics])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (clickAssistantToShowTopic) {
|
if (clickAssistantToShowTopic) {
|
||||||
EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)
|
EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)
|
||||||
}
|
}
|
||||||
}, [clickAssistantToShowTopic, activeAssistant])
|
}, [clickAssistantToShowTopic, activeAssistant])
|
||||||
|
|
||||||
return {
|
useEffect(() => {
|
||||||
activeAssistant,
|
const subscriptions = [
|
||||||
activeTopic,
|
EventEmitter.on(EVENT_NAMES.SET_ASSISTANT, setActiveAssistant),
|
||||||
setActiveAssistant: (assistant: Assistant) => {
|
EventEmitter.on(EVENT_NAMES.SET_TOPIC, setActiveTopic)
|
||||||
dispatch(setActiveAssistant(assistant))
|
]
|
||||||
},
|
return () => subscriptions.forEach((subscription) => subscription())
|
||||||
setActiveTopic: (topic: Topic) => {
|
}, [])
|
||||||
dispatch(setActiveTopic(topic))
|
|
||||||
}
|
const value = useMemo(
|
||||||
}
|
() => ({
|
||||||
|
activeAssistant,
|
||||||
|
activeTopic,
|
||||||
|
setActiveAssistant,
|
||||||
|
setActiveTopic
|
||||||
|
}),
|
||||||
|
[activeAssistant, activeTopic]
|
||||||
|
)
|
||||||
|
|
||||||
|
return <ChatContext value={value}>{children}</ChatContext>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useChat = () => {
|
||||||
|
const context = use(ChatContext)
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useChat must be used within ChatProvider')
|
||||||
|
}
|
||||||
|
return context
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
|||||||
import { RootState } from '@renderer/store'
|
import { RootState } from '@renderer/store'
|
||||||
import { messageBlocksSelectors } from '@renderer/store/messageBlock'
|
import { messageBlocksSelectors } from '@renderer/store/messageBlock'
|
||||||
import { selectMessagesForTopic } from '@renderer/store/newMessage'
|
import { selectMessagesForTopic } from '@renderer/store/newMessage'
|
||||||
import { setActiveTopic, setSelectedMessageIds, toggleMultiSelectMode } from '@renderer/store/runtime'
|
import { setSelectedMessageIds, toggleMultiSelectMode } from '@renderer/store/runtime'
|
||||||
import { Topic } from '@renderer/types'
|
import { Topic } from '@renderer/types'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@ -27,10 +27,6 @@ export const useChatContext = (activeTopic: Topic) => {
|
|||||||
return () => unsubscribe()
|
return () => unsubscribe()
|
||||||
}, [dispatch])
|
}, [dispatch])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
dispatch(setActiveTopic(activeTopic))
|
|
||||||
}, [dispatch, activeTopic])
|
|
||||||
|
|
||||||
const handleToggleMultiSelectMode = useCallback(
|
const handleToggleMultiSelectMode = useCallback(
|
||||||
(value: boolean) => {
|
(value: boolean) => {
|
||||||
dispatch(toggleMultiSelectMode(value))
|
dispatch(toggleMultiSelectMode(value))
|
||||||
|
|||||||
@ -29,7 +29,9 @@ const App: FC<Props> = ({ app, onClick, size = 60, isLast }) => {
|
|||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
if (!isHome) {
|
if (!isHome) {
|
||||||
navigate('/')
|
setTimeout(() => {
|
||||||
|
navigate('/')
|
||||||
|
}, 300)
|
||||||
}
|
}
|
||||||
|
|
||||||
openMinappKeepAlive(app)
|
openMinappKeepAlive(app)
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { HStack } from '@renderer/components/Layout'
|
import { HStack } from '@renderer/components/Layout'
|
||||||
|
import { ChatProvider } from '@renderer/hooks/useChat'
|
||||||
import { useAppDispatch } from '@renderer/store'
|
import { useAppDispatch } from '@renderer/store'
|
||||||
import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
|
import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
|
||||||
import { Topic } from '@renderer/types'
|
import { Topic } from '@renderer/types'
|
||||||
@ -72,51 +73,53 @@ const TopicsPage: FC = () => {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<ChatProvider>
|
||||||
<HStack style={{ padding: '0 12px', marginTop: 8 }}>
|
<Container>
|
||||||
<Input
|
<HStack style={{ padding: '0 12px', marginTop: 8 }}>
|
||||||
prefix={
|
<Input
|
||||||
stack.length > 1 ? (
|
prefix={
|
||||||
<SearchIcon className="back-icon" onClick={goBack}>
|
stack.length > 1 ? (
|
||||||
<ChevronLeft size={16} />
|
<SearchIcon className="back-icon" onClick={goBack}>
|
||||||
</SearchIcon>
|
<ChevronLeft size={16} />
|
||||||
) : (
|
</SearchIcon>
|
||||||
<SearchIcon>
|
) : (
|
||||||
<Search size={15} />
|
<SearchIcon>
|
||||||
</SearchIcon>
|
<Search size={15} />
|
||||||
)
|
</SearchIcon>
|
||||||
}
|
)
|
||||||
suffix={search.length >= 2 ? <CornerDownLeft size={16} /> : null}
|
}
|
||||||
ref={inputRef}
|
suffix={search.length >= 2 ? <CornerDownLeft size={16} /> : null}
|
||||||
placeholder={t('history.search.placeholder')}
|
ref={inputRef}
|
||||||
value={search}
|
placeholder={t('history.search.placeholder')}
|
||||||
onChange={(e) => setSearch(e.target.value.trimStart())}
|
value={search}
|
||||||
allowClear
|
onChange={(e) => setSearch(e.target.value.trimStart())}
|
||||||
autoFocus
|
allowClear
|
||||||
spellCheck={false}
|
autoFocus
|
||||||
style={{ paddingLeft: 0 }}
|
spellCheck={false}
|
||||||
variant="borderless"
|
style={{ paddingLeft: 0 }}
|
||||||
size="middle"
|
variant="borderless"
|
||||||
onPressEnter={onSearch}
|
size="middle"
|
||||||
/>
|
onPressEnter={onSearch}
|
||||||
</HStack>
|
/>
|
||||||
<Divider style={{ margin: 0, marginTop: 4, borderBlockStartWidth: 0.5 }} />
|
</HStack>
|
||||||
|
<Divider style={{ margin: 0, marginTop: 4, borderBlockStartWidth: 0.5 }} />
|
||||||
|
|
||||||
<TopicsHistory
|
<TopicsHistory
|
||||||
keywords={search}
|
keywords={search}
|
||||||
onClick={onTopicClick as any}
|
onClick={onTopicClick as any}
|
||||||
onSearch={onSearch}
|
onSearch={onSearch}
|
||||||
style={{ display: isShow('topics') }}
|
style={{ display: isShow('topics') }}
|
||||||
/>
|
/>
|
||||||
<TopicMessages topic={topic} style={{ display: isShow('topic') }} />
|
<TopicMessages topic={topic} style={{ display: isShow('topic') }} />
|
||||||
<SearchResults
|
<SearchResults
|
||||||
keywords={isShow('search') ? searchKeywords : ''}
|
keywords={isShow('search') ? searchKeywords : ''}
|
||||||
onMessageClick={onMessageClick}
|
onMessageClick={onMessageClick}
|
||||||
onTopicClick={onTopicClick}
|
onTopicClick={onTopicClick}
|
||||||
style={{ display: isShow('search') }}
|
style={{ display: isShow('search') }}
|
||||||
/>
|
/>
|
||||||
<SearchMessage message={message} style={{ display: isShow('message') }} />
|
<SearchMessage message={message} style={{ display: isShow('message') }} />
|
||||||
</Container>
|
</Container>
|
||||||
|
</ChatProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { ArrowRightOutlined } from '@ant-design/icons'
|
import { ArrowRightOutlined } from '@ant-design/icons'
|
||||||
import { HStack } from '@renderer/components/Layout'
|
import { HStack } from '@renderer/components/Layout'
|
||||||
import { MessageEditingProvider } from '@renderer/context/MessageEditingContext'
|
import { MessageEditingProvider } from '@renderer/context/MessageEditingContext'
|
||||||
import { useChat } from '@renderer/hooks/useChat'
|
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { getTopicById } from '@renderer/hooks/useTopic'
|
import { getTopicById } from '@renderer/hooks/useTopic'
|
||||||
import { default as MessageItem } from '@renderer/pages/home/Messages/Message'
|
import { default as MessageItem } from '@renderer/pages/home/Messages/Message'
|
||||||
@ -22,7 +21,6 @@ const SearchMessage: FC<Props> = ({ message, ...props }) => {
|
|||||||
const { messageStyle } = useSettings()
|
const { messageStyle } = useSettings()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [topic, setTopic] = useState<Topic | null>(null)
|
const [topic, setTopic] = useState<Topic | null>(null)
|
||||||
const { setActiveAssistant, setActiveTopic } = useChat()
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
runAsyncFunction(async () => {
|
runAsyncFunction(async () => {
|
||||||
@ -50,13 +48,11 @@ const SearchMessage: FC<Props> = ({ message, ...props }) => {
|
|||||||
type="text"
|
type="text"
|
||||||
size="middle"
|
size="middle"
|
||||||
style={{ color: 'var(--color-text-3)', position: 'absolute', right: 0, top: 10 }}
|
style={{ color: 'var(--color-text-3)', position: 'absolute', right: 0, top: 10 }}
|
||||||
onClick={() => locateToMessage({ message, setActiveAssistant, setActiveTopic })}
|
onClick={() => locateToMessage(message)}
|
||||||
icon={<ArrowRightOutlined />}
|
icon={<ArrowRightOutlined />}
|
||||||
/>
|
/>
|
||||||
<HStack mt="10px" justifyContent="center">
|
<HStack mt="10px" justifyContent="center">
|
||||||
<Button
|
<Button onClick={() => locateToMessage(message)} icon={<ArrowRightOutlined />}>
|
||||||
onClick={() => locateToMessage({ message, setActiveAssistant, setActiveTopic })}
|
|
||||||
icon={<ArrowRightOutlined />}>
|
|
||||||
{t('history.locate.message')}
|
{t('history.locate.message')}
|
||||||
</Button>
|
</Button>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|||||||
@ -59,7 +59,7 @@ const TopicMessages: FC<Props> = ({ topic, ...props }) => {
|
|||||||
type="text"
|
type="text"
|
||||||
size="middle"
|
size="middle"
|
||||||
style={{ color: 'var(--color-text-3)', position: 'absolute', right: 0, top: 5 }}
|
style={{ color: 'var(--color-text-3)', position: 'absolute', right: 0, top: 5 }}
|
||||||
onClick={() => locateToMessage({ message, setActiveAssistant, setActiveTopic })}
|
onClick={() => locateToMessage(message)}
|
||||||
icon={<ArrowRightOutlined />}
|
icon={<ArrowRightOutlined />}
|
||||||
/>
|
/>
|
||||||
<Divider style={{ margin: '8px auto 15px' }} variant="dashed" />
|
<Divider style={{ margin: '8px auto 15px' }} variant="dashed" />
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { HStack } from '@renderer/components/Layout'
|
import { HStack } from '@renderer/components/Layout'
|
||||||
|
import { ChatProvider } from '@renderer/hooks/useChat'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { FC, useEffect } from 'react'
|
import { FC, useEffect } from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
@ -19,15 +20,17 @@ const HomePage: FC<{ style?: React.CSSProperties }> = ({ style }) => {
|
|||||||
}, [showAssistants, showTopics, topicPosition])
|
}, [showAssistants, showTopics, topicPosition])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HStack style={{ display: 'flex', flex: 1 }} id="home-page">
|
<ChatProvider>
|
||||||
<MainSidebar />
|
<HStack style={{ display: 'flex', flex: 1 }} id="home-page">
|
||||||
<Container style={style}>
|
<MainSidebar />
|
||||||
<ChatNavbar />
|
<Container style={style}>
|
||||||
<ContentContainer id="content-container">
|
<ChatNavbar />
|
||||||
<Chat />
|
<ContentContainer id="content-container">
|
||||||
</ContentContainer>
|
<Chat />
|
||||||
</Container>
|
</ContentContainer>
|
||||||
</HStack>
|
</Container>
|
||||||
|
</HStack>
|
||||||
|
</ChatProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -52,7 +52,6 @@ import {
|
|||||||
SubMenu
|
SubMenu
|
||||||
} from './MainSidebarStyles'
|
} from './MainSidebarStyles'
|
||||||
import OpenedMinappTabs from './OpenedMinapps'
|
import OpenedMinappTabs from './OpenedMinapps'
|
||||||
import PinnedApps from './PinnedApps'
|
|
||||||
|
|
||||||
type Tab = 'assistants' | 'topic'
|
type Tab = 'assistants' | 'topic'
|
||||||
|
|
||||||
@ -174,7 +173,7 @@ const MainSidebar: FC = () => {
|
|||||||
overflow: showAssistants ? 'initial' : 'hidden'
|
overflow: showAssistants ? 'initial' : 'hidden'
|
||||||
}}>
|
}}>
|
||||||
<MainNavbar />
|
<MainNavbar />
|
||||||
<MainMenu>
|
<MainMenu style={{ marginBottom: 4 }}>
|
||||||
<MainMenuItem active={isAppMenuExpanded} onClick={() => setIsAppMenuExpanded(!isAppMenuExpanded)}>
|
<MainMenuItem active={isAppMenuExpanded} onClick={() => setIsAppMenuExpanded(!isAppMenuExpanded)}>
|
||||||
<MainMenuItemLeft>
|
<MainMenuItemLeft>
|
||||||
<MainMenuItemIcon>
|
<MainMenuItemIcon>
|
||||||
@ -200,7 +199,6 @@ const MainSidebar: FC = () => {
|
|||||||
</MainMenuItemLeft>
|
</MainMenuItemLeft>
|
||||||
</MainMenuItem>
|
</MainMenuItem>
|
||||||
))}
|
))}
|
||||||
<PinnedApps />
|
|
||||||
</SubMenu>
|
</SubMenu>
|
||||||
)}
|
)}
|
||||||
<OpenedMinappTabs />
|
<OpenedMinappTabs />
|
||||||
@ -299,9 +297,7 @@ const MainContainer = styled.div`
|
|||||||
`
|
`
|
||||||
|
|
||||||
const AssistantContainer = styled.div`
|
const AssistantContainer = styled.div`
|
||||||
margin: 0 10px;
|
margin: 4px 10px;
|
||||||
margin-top: 4px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|||||||
@ -85,7 +85,6 @@ export const TabsContainer = styled.div`
|
|||||||
|
|
||||||
export const TabsWrapper = styled(Scrollbar as any)`
|
export const TabsWrapper = styled(Scrollbar as any)`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 5px 0;
|
|
||||||
max-height: 50vh;
|
max-height: 50vh;
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|||||||
@ -1,14 +1,16 @@
|
|||||||
|
import DragableList from '@renderer/components/DragableList'
|
||||||
import MinAppIcon from '@renderer/components/Icons/MinAppIcon'
|
import MinAppIcon from '@renderer/components/Icons/MinAppIcon'
|
||||||
import IndicatorLight from '@renderer/components/IndicatorLight'
|
import IndicatorLight from '@renderer/components/IndicatorLight'
|
||||||
import { Center } from '@renderer/components/Layout'
|
import { Center } from '@renderer/components/Layout'
|
||||||
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
||||||
|
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import type { MenuProps } from 'antd'
|
import type { MenuProps } from 'antd'
|
||||||
import { Empty } from 'antd'
|
import { Empty } from 'antd'
|
||||||
import { Dropdown } from 'antd'
|
import { Dropdown } from 'antd'
|
||||||
import { isEmpty } from 'lodash'
|
import { isEmpty } from 'lodash'
|
||||||
import { FC, useEffect } from 'react'
|
import { FC, useEffect, useMemo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
@ -18,7 +20,6 @@ import {
|
|||||||
MainMenuItemLeft,
|
MainMenuItemLeft,
|
||||||
MainMenuItemRight,
|
MainMenuItemRight,
|
||||||
MainMenuItemText,
|
MainMenuItemText,
|
||||||
Menus,
|
|
||||||
TabsContainer,
|
TabsContainer,
|
||||||
TabsWrapper
|
TabsWrapper
|
||||||
} from './MainSidebarStyles'
|
} from './MainSidebarStyles'
|
||||||
@ -27,8 +28,24 @@ const OpenedMinapps: FC = () => {
|
|||||||
const { minappShow, openedKeepAliveMinapps, currentMinappId } = useRuntime()
|
const { minappShow, openedKeepAliveMinapps, currentMinappId } = useRuntime()
|
||||||
const { openMinappKeepAlive, hideMinappPopup, closeMinapp, closeAllMinapps } = useMinappPopup()
|
const { openMinappKeepAlive, hideMinappPopup, closeMinapp, closeAllMinapps } = useMinappPopup()
|
||||||
const { showOpenedMinappsInSidebar } = useSettings()
|
const { showOpenedMinappsInSidebar } = useSettings()
|
||||||
|
const { pinned, updatePinnedMinapps } = useMinapps()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
// 合并并排序应用列表
|
||||||
|
const sortedApps = useMemo(() => {
|
||||||
|
// 分离已打开但未固定的应用
|
||||||
|
const openedNotPinned = openedKeepAliveMinapps.filter((app) => !pinned.find((p) => p.id === app.id))
|
||||||
|
|
||||||
|
// 获取固定应用列表(保持原有顺序)
|
||||||
|
const pinnedApps = pinned.map((app) => {
|
||||||
|
const openedApp = openedKeepAliveMinapps.find((o) => o.id === app.id)
|
||||||
|
return openedApp || app
|
||||||
|
})
|
||||||
|
|
||||||
|
// 把已启动但未固定的放到列表下面
|
||||||
|
return [...pinnedApps, ...openedNotPinned]
|
||||||
|
}, [openedKeepAliveMinapps, pinned])
|
||||||
|
|
||||||
const handleOnClick = (app) => {
|
const handleOnClick = (app) => {
|
||||||
if (minappShow && currentMinappId === app.id) {
|
if (minappShow && currentMinappId === app.id) {
|
||||||
hideMinappPopup()
|
hideMinappPopup()
|
||||||
@ -59,51 +76,80 @@ const OpenedMinapps: FC = () => {
|
|||||||
container.style.setProperty('--indicator-right', `${indicatorRight}px`)
|
container.style.setProperty('--indicator-right', `${indicatorRight}px`)
|
||||||
}, [currentMinappId, openedKeepAliveMinapps, minappShow])
|
}, [currentMinappId, openedKeepAliveMinapps, minappShow])
|
||||||
|
|
||||||
const isShowOpened = showOpenedMinappsInSidebar && openedKeepAliveMinapps.length > 0
|
const isShowApps = showOpenedMinappsInSidebar && sortedApps.length > 0
|
||||||
|
|
||||||
if (!isShowOpened) return <TabsContainer className="TabsContainer" />
|
if (!isShowApps) return <TabsContainer className="TabsContainer" />
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TabsContainer className="TabsContainer">
|
<TabsContainer className="TabsContainer">
|
||||||
<Divider />
|
<Divider />
|
||||||
<TabsWrapper>
|
<TabsWrapper>
|
||||||
<Menus>
|
<DragableList
|
||||||
{openedKeepAliveMinapps.map((app) => {
|
list={sortedApps}
|
||||||
|
onUpdate={(newList) => {
|
||||||
|
// 只更新固定应用的顺序
|
||||||
|
const newPinned = newList.filter((app) => pinned.find((p) => p.id === app.id))
|
||||||
|
updatePinnedMinapps(newPinned)
|
||||||
|
}}
|
||||||
|
listStyle={{ margin: '4px 0' }}>
|
||||||
|
{(app) => {
|
||||||
|
const isPinned = pinned.find((p) => p.id === app.id)
|
||||||
|
const isOpened = openedKeepAliveMinapps.find((o) => o.id === app.id)
|
||||||
|
|
||||||
const menuItems: MenuProps['items'] = [
|
const menuItems: MenuProps['items'] = [
|
||||||
{
|
{
|
||||||
key: 'closeApp',
|
key: 'togglePin',
|
||||||
label: t('minapp.sidebar.close.title'),
|
label: isPinned ? t('minapp.sidebar.remove.title') : t('minapp.sidebar.pin.title'),
|
||||||
onClick: () => closeMinapp(app.id)
|
onClick: () => {
|
||||||
},
|
if (isPinned) {
|
||||||
{
|
const newPinned = pinned.filter((item) => item.id !== app.id)
|
||||||
key: 'closeAllApp',
|
updatePinnedMinapps(newPinned)
|
||||||
label: t('minapp.sidebar.closeall.title'),
|
} else {
|
||||||
onClick: () => closeAllMinapps()
|
updatePinnedMinapps([...pinned, app])
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if (isOpened) {
|
||||||
|
menuItems.push(
|
||||||
|
{
|
||||||
|
key: 'closeApp',
|
||||||
|
label: t('minapp.sidebar.close.title'),
|
||||||
|
onClick: () => closeMinapp(app.id)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'closeAllApp',
|
||||||
|
label: t('minapp.sidebar.closeall.title'),
|
||||||
|
onClick: () => closeAllMinapps()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainMenuItem key={app.id} onClick={() => handleOnClick(app)}>
|
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']} overlayStyle={{ zIndex: 10000 }}>
|
||||||
<MainMenuItemLeft>
|
<MainMenuItem key={app.id} onClick={() => handleOnClick(app)}>
|
||||||
<MainMenuItemIcon>
|
<MainMenuItemLeft>
|
||||||
<MinAppIcon size={22} app={app} style={{ borderRadius: 6 }} sidebar />
|
<MainMenuItemIcon>
|
||||||
</MainMenuItemIcon>
|
<MinAppIcon size={22} app={app} style={{ borderRadius: 6 }} sidebar />
|
||||||
<MainMenuItemText>{app.name}</MainMenuItemText>
|
</MainMenuItemIcon>
|
||||||
</MainMenuItemLeft>
|
<MainMenuItemText>{app.name}</MainMenuItemText>
|
||||||
<MainMenuItemRight style={{ marginRight: 4 }}>
|
</MainMenuItemLeft>
|
||||||
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']} overlayStyle={{ zIndex: 10000 }}>
|
{isOpened && (
|
||||||
<IndicatorLight color="var(--color-primary)" shadow={false} animation={false} size={5} />
|
<MainMenuItemRight style={{ marginRight: 4 }}>
|
||||||
</Dropdown>
|
<IndicatorLight color="var(--color-primary)" shadow={false} animation={false} size={5} />
|
||||||
</MainMenuItemRight>
|
</MainMenuItemRight>
|
||||||
</MainMenuItem>
|
)}
|
||||||
|
</MainMenuItem>
|
||||||
|
</Dropdown>
|
||||||
)
|
)
|
||||||
})}
|
}}
|
||||||
{isEmpty(openedKeepAliveMinapps) && (
|
</DragableList>
|
||||||
<Center>
|
{isEmpty(sortedApps) && (
|
||||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
<Center>
|
||||||
</Center>
|
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||||
)}
|
</Center>
|
||||||
</Menus>
|
)}
|
||||||
</TabsWrapper>
|
</TabsWrapper>
|
||||||
<Divider />
|
<Divider />
|
||||||
</TabsContainer>
|
</TabsContainer>
|
||||||
|
|||||||
@ -29,5 +29,7 @@ export const EVENT_NAMES = {
|
|||||||
SHOW_MODEL_SELECTOR: 'SHOW_MODEL_SELECTOR',
|
SHOW_MODEL_SELECTOR: 'SHOW_MODEL_SELECTOR',
|
||||||
EDIT_CODE_BLOCK: 'EDIT_CODE_BLOCK',
|
EDIT_CODE_BLOCK: 'EDIT_CODE_BLOCK',
|
||||||
CHANGE_TOPIC: 'CHANGE_TOPIC',
|
CHANGE_TOPIC: 'CHANGE_TOPIC',
|
||||||
OPEN_MINAPP: 'OPEN_MINAPP'
|
OPEN_MINAPP: 'OPEN_MINAPP',
|
||||||
|
SET_ASSISTANT: 'SET_ASSISTANT',
|
||||||
|
SET_TOPIC: 'SET_TOPIC'
|
||||||
}
|
}
|
||||||
|
|||||||
@ -81,15 +81,7 @@ export function isGenerating() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function locateToMessage({
|
export async function locateToMessage(message: Message) {
|
||||||
message,
|
|
||||||
setActiveAssistant,
|
|
||||||
setActiveTopic
|
|
||||||
}: {
|
|
||||||
message: Message
|
|
||||||
setActiveAssistant: (assistant: Assistant) => void
|
|
||||||
setActiveTopic: (topic: Topic) => void
|
|
||||||
}) {
|
|
||||||
await isGenerating()
|
await isGenerating()
|
||||||
|
|
||||||
SearchPopup.hide()
|
SearchPopup.hide()
|
||||||
@ -100,11 +92,11 @@ export async function locateToMessage({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setActiveAssistant(assistant)
|
EventEmitter.emit(EVENT_NAMES.SET_ASSISTANT, assistant)
|
||||||
setActiveTopic(topic)
|
EventEmitter.emit(EVENT_NAMES.SET_TOPIC, topic)
|
||||||
|
|
||||||
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 0)
|
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 0)
|
||||||
setTimeout(() => EventEmitter.emit(EVENT_NAMES.LOCATE_MESSAGE + ':' + message.id), 500)
|
setTimeout(() => EventEmitter.emit(EVENT_NAMES.LOCATE_MESSAGE + ':' + message.id), 200)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,13 +1,11 @@
|
|||||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||||
import { AppLogo, UserAvatar } from '@renderer/config/env'
|
import { AppLogo, UserAvatar } from '@renderer/config/env'
|
||||||
import type { Assistant, MinAppType, Topic } from '@renderer/types'
|
import type { MinAppType } from '@renderer/types'
|
||||||
import type { UpdateInfo } from 'builder-util-runtime'
|
import type { UpdateInfo } from 'builder-util-runtime'
|
||||||
|
|
||||||
export interface ChatState {
|
export interface ChatState {
|
||||||
isMultiSelectMode: boolean
|
isMultiSelectMode: boolean
|
||||||
selectedMessageIds: string[]
|
selectedMessageIds: string[]
|
||||||
activeTopic: Topic | null
|
|
||||||
activeAssistant: Assistant | null
|
|
||||||
/** topic ids that are currently being renamed */
|
/** topic ids that are currently being renamed */
|
||||||
renamingTopics: string[]
|
renamingTopics: string[]
|
||||||
/** topic ids that are newly renamed */
|
/** topic ids that are newly renamed */
|
||||||
@ -70,8 +68,6 @@ const initialState: RuntimeState = {
|
|||||||
chat: {
|
chat: {
|
||||||
isMultiSelectMode: false,
|
isMultiSelectMode: false,
|
||||||
selectedMessageIds: [],
|
selectedMessageIds: [],
|
||||||
activeTopic: null,
|
|
||||||
activeAssistant: null,
|
|
||||||
renamingTopics: [],
|
renamingTopics: [],
|
||||||
newlyRenamedTopics: []
|
newlyRenamedTopics: []
|
||||||
}
|
}
|
||||||
@ -124,12 +120,6 @@ const runtimeSlice = createSlice({
|
|||||||
setSelectedMessageIds: (state, action: PayloadAction<string[]>) => {
|
setSelectedMessageIds: (state, action: PayloadAction<string[]>) => {
|
||||||
state.chat.selectedMessageIds = action.payload
|
state.chat.selectedMessageIds = action.payload
|
||||||
},
|
},
|
||||||
setActiveTopic: (state, action: PayloadAction<Topic>) => {
|
|
||||||
state.chat.activeTopic = action.payload
|
|
||||||
},
|
|
||||||
setActiveAssistant: (state, action: PayloadAction<Assistant>) => {
|
|
||||||
state.chat.activeAssistant = action.payload
|
|
||||||
},
|
|
||||||
setRenamingTopics: (state, action: PayloadAction<string[]>) => {
|
setRenamingTopics: (state, action: PayloadAction<string[]>) => {
|
||||||
state.chat.renamingTopics = action.payload
|
state.chat.renamingTopics = action.payload
|
||||||
},
|
},
|
||||||
@ -154,8 +144,6 @@ export const {
|
|||||||
// Chat related actions
|
// Chat related actions
|
||||||
toggleMultiSelectMode,
|
toggleMultiSelectMode,
|
||||||
setSelectedMessageIds,
|
setSelectedMessageIds,
|
||||||
setActiveTopic,
|
|
||||||
setActiveAssistant,
|
|
||||||
setRenamingTopics,
|
setRenamingTopics,
|
||||||
setNewlyRenamedTopics
|
setNewlyRenamedTopics
|
||||||
} = runtimeSlice.actions
|
} = runtimeSlice.actions
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user