diff --git a/src/renderer/src/Routes.tsx b/src/renderer/src/Routes.tsx index e0b2740446..80c83048f5 100644 --- a/src/renderer/src/Routes.tsx +++ b/src/renderer/src/Routes.tsx @@ -1,5 +1,5 @@ import { IpcChannel } from '@shared/IpcChannel' -import { AnimatePresence, motion } from 'framer-motion' +import { AnimatePresence, easeInOut, motion } from 'framer-motion' import { useEffect } from 'react' import { Route, Routes, useLocation, useNavigate } from 'react-router-dom' import styled from 'styled-components' @@ -94,9 +94,9 @@ const PageContainer = styled(motion.div)` ` const pageTransition = { - type: 'tween', + type: 'tween' as const, duration: 0.25, - ease: [0.4, 0.0, 0.2, 1] + ease: easeInOut } const pageVariants = { diff --git a/src/renderer/src/hooks/useChat.tsx b/src/renderer/src/hooks/useChat.tsx index cb69eac541..0a2c80404b 100644 --- a/src/renderer/src/hooks/useChat.tsx +++ b/src/renderer/src/hooks/useChat.tsx @@ -1,22 +1,43 @@ import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { useAppDispatch, useAppSelector } from '@renderer/store' -import { setActiveAssistant, setActiveTopic } from '@renderer/store/runtime' import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk' import { Assistant } 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' -export const useChat = () => { - const { assistants } = useAssistants() - const activeAssistant = useAppSelector((state) => state.runtime.chat.activeAssistant) || assistants[0] +interface ChatContextType { + activeAssistant: Assistant + activeTopic: Topic + setActiveAssistant: (assistant: Assistant) => void + setActiveTopic: (topic: Topic) => void +} + +const ChatContext = createContext(null) + +export const ChatProvider = ({ children }) => { + const assistants = useAppSelector((state) => state.assistants.assistants) + const [activeAssistant, setActiveAssistant] = useState(assistants[0]) const topics = useTopicsForAssistant(activeAssistant.id) - const activeTopic = useAppSelector((state) => state.runtime.chat.activeTopic) || topics[0] + const [activeTopic, setActiveTopic] = useState(topics[0]) const { clickAssistantToShowTopic } = useSettings() 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(() => { if (activeTopic) { dispatch(loadTopicMessagesThunk(activeTopic.id)) @@ -24,28 +45,38 @@ export const useChat = () => { } }, [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(() => { if (clickAssistantToShowTopic) { EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR) } }, [clickAssistantToShowTopic, activeAssistant]) - return { - activeAssistant, - activeTopic, - setActiveAssistant: (assistant: Assistant) => { - dispatch(setActiveAssistant(assistant)) - }, - setActiveTopic: (topic: Topic) => { - dispatch(setActiveTopic(topic)) - } - } + useEffect(() => { + const subscriptions = [ + EventEmitter.on(EVENT_NAMES.SET_ASSISTANT, setActiveAssistant), + EventEmitter.on(EVENT_NAMES.SET_TOPIC, setActiveTopic) + ] + return () => subscriptions.forEach((subscription) => subscription()) + }, []) + + const value = useMemo( + () => ({ + activeAssistant, + activeTopic, + setActiveAssistant, + setActiveTopic + }), + [activeAssistant, activeTopic] + ) + + return {children} +} + +export const useChat = () => { + const context = use(ChatContext) + if (!context) { + throw new Error('useChat must be used within ChatProvider') + } + return context } diff --git a/src/renderer/src/hooks/useChatContext.ts b/src/renderer/src/hooks/useChatContext.ts index 2771037d33..d87fca06e7 100644 --- a/src/renderer/src/hooks/useChatContext.ts +++ b/src/renderer/src/hooks/useChatContext.ts @@ -3,7 +3,7 @@ import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { RootState } from '@renderer/store' import { messageBlocksSelectors } from '@renderer/store/messageBlock' 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 { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -27,10 +27,6 @@ export const useChatContext = (activeTopic: Topic) => { return () => unsubscribe() }, [dispatch]) - useEffect(() => { - dispatch(setActiveTopic(activeTopic)) - }, [dispatch, activeTopic]) - const handleToggleMultiSelectMode = useCallback( (value: boolean) => { dispatch(toggleMultiSelectMode(value)) diff --git a/src/renderer/src/pages/apps/App.tsx b/src/renderer/src/pages/apps/App.tsx index 52aae0120e..f03e5f934d 100644 --- a/src/renderer/src/pages/apps/App.tsx +++ b/src/renderer/src/pages/apps/App.tsx @@ -29,7 +29,9 @@ const App: FC = ({ app, onClick, size = 60, isLast }) => { const handleClick = () => { if (!isHome) { - navigate('/') + setTimeout(() => { + navigate('/') + }, 300) } openMinappKeepAlive(app) diff --git a/src/renderer/src/pages/history/HistoryPage.tsx b/src/renderer/src/pages/history/HistoryPage.tsx index d20accfd87..7db6f790af 100644 --- a/src/renderer/src/pages/history/HistoryPage.tsx +++ b/src/renderer/src/pages/history/HistoryPage.tsx @@ -1,4 +1,5 @@ import { HStack } from '@renderer/components/Layout' +import { ChatProvider } from '@renderer/hooks/useChat' import { useAppDispatch } from '@renderer/store' import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk' import { Topic } from '@renderer/types' @@ -72,51 +73,53 @@ const TopicsPage: FC = () => { }, []) return ( - - - 1 ? ( - - - - ) : ( - - - - ) - } - suffix={search.length >= 2 ? : null} - ref={inputRef} - placeholder={t('history.search.placeholder')} - value={search} - onChange={(e) => setSearch(e.target.value.trimStart())} - allowClear - autoFocus - spellCheck={false} - style={{ paddingLeft: 0 }} - variant="borderless" - size="middle" - onPressEnter={onSearch} - /> - - + + + + 1 ? ( + + + + ) : ( + + + + ) + } + suffix={search.length >= 2 ? : null} + ref={inputRef} + placeholder={t('history.search.placeholder')} + value={search} + onChange={(e) => setSearch(e.target.value.trimStart())} + allowClear + autoFocus + spellCheck={false} + style={{ paddingLeft: 0 }} + variant="borderless" + size="middle" + onPressEnter={onSearch} + /> + + - - - - - + + + + + + ) } diff --git a/src/renderer/src/pages/history/components/SearchMessage.tsx b/src/renderer/src/pages/history/components/SearchMessage.tsx index a03249ba35..be2959efc4 100644 --- a/src/renderer/src/pages/history/components/SearchMessage.tsx +++ b/src/renderer/src/pages/history/components/SearchMessage.tsx @@ -1,7 +1,6 @@ import { ArrowRightOutlined } from '@ant-design/icons' import { HStack } from '@renderer/components/Layout' import { MessageEditingProvider } from '@renderer/context/MessageEditingContext' -import { useChat } from '@renderer/hooks/useChat' import { useSettings } from '@renderer/hooks/useSettings' import { getTopicById } from '@renderer/hooks/useTopic' import { default as MessageItem } from '@renderer/pages/home/Messages/Message' @@ -22,7 +21,6 @@ const SearchMessage: FC = ({ message, ...props }) => { const { messageStyle } = useSettings() const { t } = useTranslation() const [topic, setTopic] = useState(null) - const { setActiveAssistant, setActiveTopic } = useChat() useEffect(() => { runAsyncFunction(async () => { @@ -50,13 +48,11 @@ const SearchMessage: FC = ({ message, ...props }) => { type="text" size="middle" style={{ color: 'var(--color-text-3)', position: 'absolute', right: 0, top: 10 }} - onClick={() => locateToMessage({ message, setActiveAssistant, setActiveTopic })} + onClick={() => locateToMessage(message)} icon={} /> - diff --git a/src/renderer/src/pages/history/components/TopicMessages.tsx b/src/renderer/src/pages/history/components/TopicMessages.tsx index 4aa34bb48c..a05e003e97 100644 --- a/src/renderer/src/pages/history/components/TopicMessages.tsx +++ b/src/renderer/src/pages/history/components/TopicMessages.tsx @@ -59,7 +59,7 @@ const TopicMessages: FC = ({ topic, ...props }) => { type="text" size="middle" style={{ color: 'var(--color-text-3)', position: 'absolute', right: 0, top: 5 }} - onClick={() => locateToMessage({ message, setActiveAssistant, setActiveTopic })} + onClick={() => locateToMessage(message)} icon={} /> diff --git a/src/renderer/src/pages/home/HomePage.tsx b/src/renderer/src/pages/home/HomePage.tsx index 6c09f4f43a..527717c7a3 100644 --- a/src/renderer/src/pages/home/HomePage.tsx +++ b/src/renderer/src/pages/home/HomePage.tsx @@ -1,4 +1,5 @@ import { HStack } from '@renderer/components/Layout' +import { ChatProvider } from '@renderer/hooks/useChat' import { useSettings } from '@renderer/hooks/useSettings' import { FC, useEffect } from 'react' import styled from 'styled-components' @@ -19,15 +20,17 @@ const HomePage: FC<{ style?: React.CSSProperties }> = ({ style }) => { }, [showAssistants, showTopics, topicPosition]) return ( - - - - - - - - - + + + + + + + + + + + ) } diff --git a/src/renderer/src/pages/home/MainSidebar/MainSidebar.tsx b/src/renderer/src/pages/home/MainSidebar/MainSidebar.tsx index 827888d922..57c5a22a6f 100644 --- a/src/renderer/src/pages/home/MainSidebar/MainSidebar.tsx +++ b/src/renderer/src/pages/home/MainSidebar/MainSidebar.tsx @@ -52,7 +52,6 @@ import { SubMenu } from './MainSidebarStyles' import OpenedMinappTabs from './OpenedMinapps' -import PinnedApps from './PinnedApps' type Tab = 'assistants' | 'topic' @@ -174,7 +173,7 @@ const MainSidebar: FC = () => { overflow: showAssistants ? 'initial' : 'hidden' }}> - + setIsAppMenuExpanded(!isAppMenuExpanded)}> @@ -200,7 +199,6 @@ const MainSidebar: FC = () => { ))} - )} @@ -299,9 +297,7 @@ const MainContainer = styled.div` ` const AssistantContainer = styled.div` - margin: 0 10px; - margin-top: 4px; - margin-bottom: 4px; + margin: 4px 10px; display: flex; ` diff --git a/src/renderer/src/pages/home/MainSidebar/MainSidebarStyles.tsx b/src/renderer/src/pages/home/MainSidebar/MainSidebarStyles.tsx index 2c8e97e42f..9cec535d1b 100644 --- a/src/renderer/src/pages/home/MainSidebar/MainSidebarStyles.tsx +++ b/src/renderer/src/pages/home/MainSidebar/MainSidebarStyles.tsx @@ -85,7 +85,6 @@ export const TabsContainer = styled.div` export const TabsWrapper = styled(Scrollbar as any)` width: 100%; - padding: 5px 0; max-height: 50vh; ` diff --git a/src/renderer/src/pages/home/MainSidebar/OpenedMinapps.tsx b/src/renderer/src/pages/home/MainSidebar/OpenedMinapps.tsx index d8638c3444..87023187ef 100644 --- a/src/renderer/src/pages/home/MainSidebar/OpenedMinapps.tsx +++ b/src/renderer/src/pages/home/MainSidebar/OpenedMinapps.tsx @@ -1,14 +1,16 @@ +import DragableList from '@renderer/components/DragableList' import MinAppIcon from '@renderer/components/Icons/MinAppIcon' import IndicatorLight from '@renderer/components/IndicatorLight' import { Center } from '@renderer/components/Layout' import { useMinappPopup } from '@renderer/hooks/useMinappPopup' +import { useMinapps } from '@renderer/hooks/useMinapps' import { useRuntime } from '@renderer/hooks/useRuntime' import { useSettings } from '@renderer/hooks/useSettings' import type { MenuProps } from 'antd' import { Empty } from 'antd' import { Dropdown } from 'antd' import { isEmpty } from 'lodash' -import { FC, useEffect } from 'react' +import { FC, useEffect, useMemo } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' @@ -18,7 +20,6 @@ import { MainMenuItemLeft, MainMenuItemRight, MainMenuItemText, - Menus, TabsContainer, TabsWrapper } from './MainSidebarStyles' @@ -27,8 +28,24 @@ const OpenedMinapps: FC = () => { const { minappShow, openedKeepAliveMinapps, currentMinappId } = useRuntime() const { openMinappKeepAlive, hideMinappPopup, closeMinapp, closeAllMinapps } = useMinappPopup() const { showOpenedMinappsInSidebar } = useSettings() + const { pinned, updatePinnedMinapps } = useMinapps() 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) => { if (minappShow && currentMinappId === app.id) { hideMinappPopup() @@ -59,51 +76,80 @@ const OpenedMinapps: FC = () => { container.style.setProperty('--indicator-right', `${indicatorRight}px`) }, [currentMinappId, openedKeepAliveMinapps, minappShow]) - const isShowOpened = showOpenedMinappsInSidebar && openedKeepAliveMinapps.length > 0 + const isShowApps = showOpenedMinappsInSidebar && sortedApps.length > 0 - if (!isShowOpened) return + if (!isShowApps) return return ( - - {openedKeepAliveMinapps.map((app) => { + { + // 只更新固定应用的顺序 + 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'] = [ { - key: 'closeApp', - label: t('minapp.sidebar.close.title'), - onClick: () => closeMinapp(app.id) - }, - { - key: 'closeAllApp', - label: t('minapp.sidebar.closeall.title'), - onClick: () => closeAllMinapps() + key: 'togglePin', + label: isPinned ? t('minapp.sidebar.remove.title') : t('minapp.sidebar.pin.title'), + onClick: () => { + if (isPinned) { + const newPinned = pinned.filter((item) => item.id !== app.id) + updatePinnedMinapps(newPinned) + } else { + 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 ( - handleOnClick(app)}> - - - - - {app.name} - - - - - - - + + handleOnClick(app)}> + + + + + {app.name} + + {isOpened && ( + + + + )} + + ) - })} - {isEmpty(openedKeepAliveMinapps) && ( -
- -
- )} -
+ }} + + {isEmpty(sortedApps) && ( +
+ +
+ )}
diff --git a/src/renderer/src/services/EventService.ts b/src/renderer/src/services/EventService.ts index ee434a79e2..04dd9a3f41 100644 --- a/src/renderer/src/services/EventService.ts +++ b/src/renderer/src/services/EventService.ts @@ -29,5 +29,7 @@ export const EVENT_NAMES = { SHOW_MODEL_SELECTOR: 'SHOW_MODEL_SELECTOR', EDIT_CODE_BLOCK: 'EDIT_CODE_BLOCK', CHANGE_TOPIC: 'CHANGE_TOPIC', - OPEN_MINAPP: 'OPEN_MINAPP' + OPEN_MINAPP: 'OPEN_MINAPP', + SET_ASSISTANT: 'SET_ASSISTANT', + SET_TOPIC: 'SET_TOPIC' } diff --git a/src/renderer/src/services/MessagesService.ts b/src/renderer/src/services/MessagesService.ts index ebb8f2df82..c6b1e5c9c9 100644 --- a/src/renderer/src/services/MessagesService.ts +++ b/src/renderer/src/services/MessagesService.ts @@ -81,15 +81,7 @@ export function isGenerating() { }) } -export async function locateToMessage({ - message, - setActiveAssistant, - setActiveTopic -}: { - message: Message - setActiveAssistant: (assistant: Assistant) => void - setActiveTopic: (topic: Topic) => void -}) { +export async function locateToMessage(message: Message) { await isGenerating() SearchPopup.hide() @@ -100,11 +92,11 @@ export async function locateToMessage({ return } - setActiveAssistant(assistant) - setActiveTopic(topic) + EventEmitter.emit(EVENT_NAMES.SET_ASSISTANT, assistant) + EventEmitter.emit(EVENT_NAMES.SET_TOPIC, topic) 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) } /** diff --git a/src/renderer/src/store/runtime.ts b/src/renderer/src/store/runtime.ts index 79a545e56d..31134878fa 100644 --- a/src/renderer/src/store/runtime.ts +++ b/src/renderer/src/store/runtime.ts @@ -1,13 +1,11 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' 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' export interface ChatState { isMultiSelectMode: boolean selectedMessageIds: string[] - activeTopic: Topic | null - activeAssistant: Assistant | null /** topic ids that are currently being renamed */ renamingTopics: string[] /** topic ids that are newly renamed */ @@ -70,8 +68,6 @@ const initialState: RuntimeState = { chat: { isMultiSelectMode: false, selectedMessageIds: [], - activeTopic: null, - activeAssistant: null, renamingTopics: [], newlyRenamedTopics: [] } @@ -124,12 +120,6 @@ const runtimeSlice = createSlice({ setSelectedMessageIds: (state, action: PayloadAction) => { state.chat.selectedMessageIds = action.payload }, - setActiveTopic: (state, action: PayloadAction) => { - state.chat.activeTopic = action.payload - }, - setActiveAssistant: (state, action: PayloadAction) => { - state.chat.activeAssistant = action.payload - }, setRenamingTopics: (state, action: PayloadAction) => { state.chat.renamingTopics = action.payload }, @@ -154,8 +144,6 @@ export const { // Chat related actions toggleMultiSelectMode, setSelectedMessageIds, - setActiveTopic, - setActiveAssistant, setRenamingTopics, setNewlyRenamedTopics } = runtimeSlice.actions