diff --git a/.dumirc.ts b/.dumirc.ts index 54794177..86ffcaf8 100644 --- a/.dumirc.ts +++ b/.dumirc.ts @@ -8,4 +8,5 @@ export default defineConfig({ }, outputPath: 'docs-dist', html2sketch: {}, + extraBabelPlugins: ['antd-style'], }); diff --git a/package.json b/package.json index 94894169..1c6e67b6 100644 --- a/package.json +++ b/package.json @@ -70,13 +70,16 @@ "dayjs": "^1", "emoji-regex": "^10", "fast-deep-equal": "^3", + "gpt-tokenizer": "^2.1.2", "immer": "^10", "lodash-es": "^4", "lucide-react": "^0.288.0", + "nanoid": "^5", "polished": "^4", "prism-react-renderer": "^2", "re-resizable": "^6", "react-error-boundary": "^4", + "react-intersection-observer": "^9.5.2", "react-layout-kit": "^1.7.1", "react-markdown": "^8", "react-rnd": "^10", @@ -102,6 +105,7 @@ "@umijs/lint": "^4", "@vitest/coverage-v8": "latest", "antd-style": "^3", + "babel-plugin-antd-style": "^1.0.4", "commitlint": "^17", "commitlint-config-gitmoji": "^2", "conventional-changelog-gitmoji-config": "^1", diff --git a/src/ChatItem/components/Avatar.tsx b/src/ChatItem/components/Avatar.tsx index 342f8f83..135d8348 100644 --- a/src/ChatItem/components/Avatar.tsx +++ b/src/ChatItem/components/Avatar.tsx @@ -21,7 +21,6 @@ const Avatar = memo(({ loading, avatar, placement, addon, onClick, const avatarContent = (
[global(theme), antdOverride(theme)]); - -export default GlobalStyle; diff --git a/src/Img/index.tsx b/src/Img/index.tsx deleted file mode 100644 index 23985e85..00000000 --- a/src/Img/index.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { ImageProps } from 'antd'; -import { ElementType, createElement, forwardRef, useContext, useMemo } from 'react'; - -import { ConfigContext } from '@/ConfigProvider'; -import { ImgProps } from '@/types'; - -const createContainer = (as: ElementType) => - forwardRef((props: any, ref) => createElement(as, { ...props, ref })); - -const Img = forwardRef((props, ref) => { - const config = useContext(ConfigContext); - - const ImgContainer = useMemo(() => createContainer(config?.imgAs || 'img'), [config]); - - return ; -}); - -export default Img; diff --git a/src/ProChat/components/ChatList/Extras/Assistant.tsx b/src/ProChat/components/ChatList/Extras/Assistant.tsx index d4e0b0dd..1b1d5d55 100644 --- a/src/ProChat/components/ChatList/Extras/Assistant.tsx +++ b/src/ProChat/components/ChatList/Extras/Assistant.tsx @@ -4,11 +4,10 @@ import { RenderMessageExtra } from '@/index'; import { memo } from 'react'; import { Flexbox } from 'react-layout-kit'; -import { useChatStore } from '@/ProChat/store'; -import { agentSelectors } from '@/ProChat/store/selectors'; +import { useStore } from '@/ProChat/store'; export const AssistantMessageExtra: RenderMessageExtra = memo(({ extra }) => { - const model = useChatStore(agentSelectors.currentAgentModel); + const model = useStore((s) => s.config.model); const showModelTag = extra?.fromModel && model !== extra?.fromModel; const hasTranslate = !!extra?.translate; diff --git a/src/ProChat/components/ChatList/index.tsx b/src/ProChat/components/ChatList/index.tsx index 2435f60b..a35b8922 100644 --- a/src/ProChat/components/ChatList/index.tsx +++ b/src/ProChat/components/ChatList/index.tsx @@ -2,20 +2,22 @@ import { ChatList } from '@lobehub/ui'; import isEqual from 'fast-deep-equal'; import { memo } from 'react'; -import { useChatStore, useSessionChatInit } from '@/ProChat/store'; -import { agentSelectors, chatSelectors } from '@/ProChat/store/selectors'; +import { useStore } from '@/ProChat/store'; +import { chatSelectors } from '@/ProChat/store/selectors'; import { renderActions } from './Actions'; import { renderMessagesExtra } from './Extras'; import { renderMessages } from './Messages'; import SkeletonList from './SkeletonList'; -const List = memo(() => { - const init = useSessionChatInit(); - - const data = useChatStore(chatSelectors.currentChatsWithGuideMessage, isEqual); +interface ListProps { + showTitle?: boolean; +} +const List = memo(({ showTitle }) => { + const data = useStore(chatSelectors.currentChatsWithGuideMessage, isEqual); const [ + init, displayMode, enableHistoryCount, historyCount, @@ -23,18 +25,18 @@ const List = memo(() => { deleteMessage, resendMessage, dispatchMessage, - translateMessage, - ] = useChatStore((s) => { - const config = agentSelectors.currentAgentConfig(s); + ] = useStore((s) => { + const config = s.config; + return [ - config.displayMode, + s.init, + s.displayMode, config.enableHistoryCount, config.historyCount, s.chatLoadingId, s.deleteMessage, s.resendMessage, s.dispatchMessage, - s.translateMessage, ]; }); @@ -42,6 +44,8 @@ const List = memo(() => { return ( { } } - // click the menu item with translate item, the result is: - // key: 'en-US' - // keyPath: ['en-US','translate'] - if (action.keyPath.at(-1) === 'translate') { - const lang = action.keyPath[0]; - translateMessage(id, lang); - } + // TODO: need a custom callback }} onMessageChange={(id, content) => dispatchMessage({ id, key: 'content', type: 'updateMessage', value: content }) diff --git a/src/ProChat/components/ScrollAnchor/index.tsx b/src/ProChat/components/ScrollAnchor/index.tsx index d3dc5fb7..c93a7b3d 100644 --- a/src/ProChat/components/ScrollAnchor/index.tsx +++ b/src/ProChat/components/ScrollAnchor/index.tsx @@ -1,12 +1,12 @@ import { memo, useEffect } from 'react'; import { useInView } from 'react-intersection-observer'; -import { useChatStore } from '@/ProChat/store'; +import { useStore } from '@/ProChat/store'; import { useAtBottom } from './useAtBottom'; const ChatScrollAnchor = memo(() => { - const trackVisibility = useChatStore((s) => !!s.chatLoadingId); + const trackVisibility = useStore((s) => !!s.chatLoadingId); const isAtBottom = useAtBottom(); const { ref, entry, inView } = useInView({ diff --git a/src/ProChat/const/fetch.ts b/src/ProChat/const/fetch.ts deleted file mode 100644 index 70281ec0..00000000 --- a/src/ProChat/const/fetch.ts +++ /dev/null @@ -1,20 +0,0 @@ -export const OPENAI_END_POINT = 'X-openai-end-point'; -export const OPENAI_API_KEY_HEADER_KEY = 'X-openai-api-key'; - -export const USE_AZURE_OPENAI = 'X-use-azure-openai'; - -export const AZURE_OPENAI_API_VERSION = 'X-azure-openai-api-version'; - -export const LOBE_CHAT_ACCESS_CODE = 'X-lobe-chat-access-code'; - -export const getOpenAIAuthFromRequest = (req: Request) => { - const apiKey = req.headers.get(OPENAI_API_KEY_HEADER_KEY); - const endpoint = req.headers.get(OPENAI_END_POINT); - const accessCode = req.headers.get(LOBE_CHAT_ACCESS_CODE); - const useAzureStr = req.headers.get(USE_AZURE_OPENAI); - const apiVersion = req.headers.get(AZURE_OPENAI_API_VERSION); - - const useAzure = !!useAzureStr; - - return { accessCode, apiKey, apiVersion, endpoint, useAzure }; -}; diff --git a/src/ProChat/const/hotkeys.ts b/src/ProChat/const/hotkeys.ts deleted file mode 100644 index b30d027c..00000000 --- a/src/ProChat/const/hotkeys.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const PREFIX_KEY = 'alt'; -export const SAVE_TOPIC_KEY = 'n'; -export const CLEAN_MESSAGE_KEY = 'backspace'; -export const REGENERATE_KEY = 'r'; diff --git a/src/ProChat/const/layoutTokens.ts b/src/ProChat/const/layoutTokens.ts deleted file mode 100644 index 82a23cf9..00000000 --- a/src/ProChat/const/layoutTokens.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { FormProps } from '@lobehub/ui'; - -export const HEADER_HEIGHT = 64; -export const MOBILE_NABBAR_HEIGHT = 44; -export const MOBILE_TABBAR_HEIGHT = 48; -export const CHAT_TEXTAREA_HEIGHT = 200; -export const CHAT_TEXTAREA_HEIGHT_MOBILE = 108; -export const CHAT_SIDEBAR_WIDTH = 280; -export const MARKET_SIDEBAR_WIDTH = 400; -export const FOLDER_WIDTH = 256; -export const MAX_WIDTH = 1024; -export const FORM_STYLE: FormProps = { - itemMinWidth: 'max(30%,240px)', - style: { maxWidth: MAX_WIDTH, width: '100%' }, -}; -export const MOBILE_HEADER_ICON_SIZE = { blockSize: 36, fontSize: 22 }; diff --git a/src/ProChat/const/llm.ts b/src/ProChat/const/llm.ts deleted file mode 100644 index da849775..00000000 --- a/src/ProChat/const/llm.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * A white list of language models that are allowed to display and be used in the app. - */ -export const LanguageModelWhiteList = [ - // OpenAI - 'gpt-3.5-turbo', - 'gpt-3.5-turbo-16k', - 'gpt-4', - 'gpt-4-32k', -]; - -export const DEFAULT_OPENAI_MODEL_LIST = [ - 'gpt-3.5-turbo', - 'gpt-3.5-turbo-16k', - 'gpt-4', - 'gpt-4-32k', -]; diff --git a/src/ProChat/const/locale.ts b/src/ProChat/const/locale.ts deleted file mode 100644 index 4e8ced3a..00000000 --- a/src/ProChat/const/locale.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { localeOptions } from '@/locales/options'; -import { Locales } from '@/locales/resources'; - -export const DEFAULT_LANG = 'en-US'; -export const LOBE_LOCALE_COOKIE = 'LOBE_LOCALE'; - -export const checkLang = (lang: Locales) => { - return lang === DEFAULT_LANG || !localeOptions.map((o) => o.value).includes(lang); -}; diff --git a/src/ProChat/const/market.ts b/src/ProChat/const/market.ts deleted file mode 100644 index 71dafbbd..00000000 --- a/src/ProChat/const/market.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { DEFAULT_AGENT_CONFIG } from '@/const/settings'; -import { AgentsMarketItem } from '@/types/market'; - -import { DEFAULT_AGENT_META } from './meta'; - -export const DEFAULT_AGENTS_MARKET_ITEM: AgentsMarketItem = { - author: '', - config: DEFAULT_AGENT_CONFIG, - createAt: Date.now().toString(), - homepage: '', - identifier: '', - manifest: '', - meta: DEFAULT_AGENT_META, - schemaVersion: 1, -}; diff --git a/src/ProChat/const/message.ts b/src/ProChat/const/message.ts index 71871d86..9927101c 100644 --- a/src/ProChat/const/message.ts +++ b/src/ProChat/const/message.ts @@ -2,5 +2,3 @@ export const LOADING_FLAT = '...'; // 只要 start with 这个,就可以判断为 function message export const FUNCTION_MESSAGE_FLAG = '{"function'; - -export const FUNCTION_LOADING = 'FUNCTION_LOADING'; diff --git a/src/ProChat/const/meta.ts b/src/ProChat/const/meta.ts index 7efcc442..1cac24f8 100644 --- a/src/ProChat/const/meta.ts +++ b/src/ProChat/const/meta.ts @@ -1,9 +1,2 @@ -import { MetaData } from '@/types/meta'; - export const DEFAULT_AVATAR = '🤖'; export const DEFAULT_USER_AVATAR = '😀'; -export const DEFAULT_BACKGROUND_COLOR = 'rgba(0,0,0,0)'; -export const DEFAULT_AGENT_META: MetaData = {}; -export const DEFAULT_INBOX_AVATAR = '🤯'; -export const DEFAULT_USER_AVATAR_URL = - 'https://registry.npmmirror.com/@lobehub/assets-logo/1.1.0/files/assets/logo-3d.webp'; diff --git a/src/ProChat/const/settings.ts b/src/ProChat/const/settings.ts deleted file mode 100644 index 41fb22f6..00000000 --- a/src/ProChat/const/settings.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { DEFAULT_OPENAI_MODEL_LIST } from '@/const/llm'; -import { DEFAULT_AGENT_META } from '@/const/meta'; -import { LanguageModel } from '@/types/llm'; -import { LobeAgentConfig } from '@/types/session'; -import { - GlobalBaseSettings, - GlobalDefaultAgent, - GlobalLLMConfig, - GlobalSettings, -} from '@/types/settings'; - -export const DEFAULT_BASE_SETTINGS: GlobalBaseSettings = { - avatar: '', - fontSize: 14, - language: 'auto', - password: '', - themeMode: 'auto', -}; - -export const DEFAULT_AGENT_CONFIG: LobeAgentConfig = { - displayMode: 'chat', - historyCount: 1, - model: LanguageModel.GPT3_5, - params: { - frequency_penalty: 0, - presence_penalty: 0, - temperature: 0.6, - top_p: 1, - }, - plugins: [], - systemRole: '', -}; - -export const DEFAULT_LLM_CONFIG: GlobalLLMConfig = { - openAI: { - OPENAI_API_KEY: '', - models: DEFAULT_OPENAI_MODEL_LIST, - }, -}; - -export const DEFAULT_AGENT: GlobalDefaultAgent = { - config: DEFAULT_AGENT_CONFIG, - meta: DEFAULT_AGENT_META, -}; - -export const DEFAULT_SETTINGS: GlobalSettings = { - defaultAgent: DEFAULT_AGENT, - languageModel: DEFAULT_LLM_CONFIG, - ...DEFAULT_BASE_SETTINGS, -}; diff --git a/src/ProChat/container/App.tsx b/src/ProChat/container/App.tsx new file mode 100644 index 00000000..db57e7a7 --- /dev/null +++ b/src/ProChat/container/App.tsx @@ -0,0 +1,44 @@ +import BackBottom from '@/BackBottom'; +import { createStyles } from 'antd-style'; +import { ReactNode, memo, useRef } from 'react'; +import { Flexbox } from 'react-layout-kit'; + +import { useOverrideStyles } from '@/ProChat/container/OverrideStyle'; +import ChatList from '../components/ChatList'; +import ChatScrollAnchor from '../components/ScrollAnchor'; + +const useStyles = createStyles( + ({ css, responsive, stylish }) => css` + overflow: hidden scroll; + height: 100%; + ${responsive.mobile} { + ${stylish.noScrollbar} + width: 100vw; + } + `, +); + +interface ConversationProps { + chatInput?: ReactNode; + showTitle?: boolean; +} + +const App = memo(({ chatInput, showTitle }) => { + const ref = useRef(null); + const { styles } = useStyles(); + const { styles: override } = useOverrideStyles(); + return ( + +
+
+ + +
+ +
+ {chatInput} +
+ ); +}); + +export default App; diff --git a/src/GlobalStyle/antdOverride.ts b/src/ProChat/container/OverrideStyle/antdOverride.ts similarity index 54% rename from src/GlobalStyle/antdOverride.ts rename to src/ProChat/container/OverrideStyle/antdOverride.ts index f9a3f1ce..32eb926b 100644 --- a/src/GlobalStyle/antdOverride.ts +++ b/src/ProChat/container/OverrideStyle/antdOverride.ts @@ -1,24 +1,11 @@ -import { Theme, css } from 'antd-style'; -import { readableColor } from 'polished'; +import { FullToken, css } from 'antd-style'; -export default (token: Theme) => css` - .${token.prefixCls}-btn { +export default (token: FullToken, prefixCls: string) => css` + .${prefixCls}-btn { box-shadow: none; } - .${token.prefixCls}-btn-primary { - color: ${readableColor(token.colorPrimary)} !important; - - &:hover { - color: ${readableColor(token.colorPrimary)} !important; - } - - &:active { - color: ${readableColor(token.colorPrimaryActive)} !important; - } - } - - .${token.prefixCls}-tooltip-inner { + .${prefixCls}-tooltip-inner { display: flex; align-items: center; justify-content: center; @@ -32,18 +19,18 @@ export default (token: Theme) => css` border-radius: ${token.borderRadiusSM}px !important; } - .${token.prefixCls}-tooltip-arrow { + .${prefixCls}-tooltip-arrow { &::before, &::after { background: ${token.colorText} !important; } } - .${token.prefixCls}-switch-handle::before { + .${prefixCls}-switch-handle::before { background: ${token.colorBgContainer} !important; } - .${token.prefixCls}-alert { + .${prefixCls}-alert { span[role='img'] { align-self: flex-start; width: 16px; @@ -51,16 +38,16 @@ export default (token: Theme) => css` margin-top: 3px; } - .${token.prefixCls}-alert-description { + .${prefixCls}-alert-description { word-break: break-all; word-wrap: break-word; } - &.${token.prefixCls}-alert-with-description { + &.${prefixCls}-alert-with-description { padding-block: 12px; padding-inline: 12px; - .${token.prefixCls}-alert-message { + .${prefixCls}-alert-message { font-size: 14px; font-weight: 600; word-break: break-all; @@ -70,7 +57,7 @@ export default (token: Theme) => css` } @media (max-width: 575px) { - .${token.prefixCls}-tooltip { + .${prefixCls}-tooltip { display: none !important; } } diff --git a/src/GlobalStyle/global.ts b/src/ProChat/container/OverrideStyle/global.ts similarity index 67% rename from src/GlobalStyle/global.ts rename to src/ProChat/container/OverrideStyle/global.ts index d8741c4d..4094f184 100644 --- a/src/GlobalStyle/global.ts +++ b/src/ProChat/container/OverrideStyle/global.ts @@ -1,45 +1,16 @@ -import { Theme, css } from 'antd-style'; +import { FullToken, css } from 'antd-style'; -export default (token: Theme) => css` - html, - body { - --font-settings: 'cv01', 'tnum', 'kern'; - --font-variations: 'opsz' auto, tabular-nums; - - overflow-x: hidden; - overflow-y: auto; - - margin: 0; - padding: 0; - - font-family: ${token.fontFamily}; - font-size: ${token.fontSize}px; - font-feature-settings: var(--font-settings); - font-variation-settings: var(--font-variations); +export default (token: FullToken) => css` line-height: 1; - color: ${token.colorTextBase}; text-size-adjust: none; text-rendering: optimizelegibility; vertical-align: baseline; - color-scheme: dark; - background-color: ${token.colorBgLayout}; - -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -webkit-overflow-scrolling: touch; -webkit-tap-highlight-color: transparent; } - - body { - overflow-x: hidden; - height: 100vh; - } - - #root { - min-height: 100vh; - } - code { font-family: ${token.fontFamilyCode} !important; diff --git a/src/ProChat/container/OverrideStyle/index.ts b/src/ProChat/container/OverrideStyle/index.ts new file mode 100644 index 00000000..4818ce48 --- /dev/null +++ b/src/ProChat/container/OverrideStyle/index.ts @@ -0,0 +1,7 @@ +import { createStyles } from 'antd-style'; +import antdOverride from './antdOverride'; +import global from './global'; + +export const useOverrideStyles = createStyles(({ token, prefixCls, cx }) => ({ + container: cx(`${prefixCls}-pro-chat`, global(token), antdOverride(token, prefixCls)), +})); diff --git a/src/ProChat/container/Provider.tsx b/src/ProChat/container/Provider.tsx new file mode 100644 index 00000000..72b32da1 --- /dev/null +++ b/src/ProChat/container/Provider.tsx @@ -0,0 +1,28 @@ +import { memo, ReactNode } from 'react'; +import { DevtoolsOptions } from 'zustand/middleware'; +import { ChatProps, createStore, Provider, useStoreApi } from '../store'; + +interface ProChatProviderProps extends ChatProps { + children: ReactNode; + devtoolOptions?: boolean | DevtoolsOptions; +} + +export const ProChatProvider = memo( + ({ children, devtoolOptions, ...props }) => { + let isWrapped = true; + + const Content = <>{children}; + + try { + useStoreApi(); + } catch (e) { + isWrapped = false; + } + + if (isWrapped) { + return Content; + } + + return createStore(props, devtoolOptions)}>{Content}; + }, +); diff --git a/src/ProChat/container/StoreUpdater.tsx b/src/ProChat/container/StoreUpdater.tsx new file mode 100644 index 00000000..7a4f0f4a --- /dev/null +++ b/src/ProChat/container/StoreUpdater.tsx @@ -0,0 +1,31 @@ +import { memo } from 'react'; +import { createStoreUpdater } from 'zustand-utils'; + +import { ChatProps, ChatState, useStoreApi } from '../store'; + +export type StoreUpdaterProps = Partial< + Pick +> & + Pick; + +const StoreUpdater = memo( + ({ init, onChatsChange, userMeta, assistantMeta, helloMessage, chats, config }) => { + const storeApi = useStoreApi(); + const useStoreUpdater = createStoreUpdater(storeApi); + + useStoreUpdater('init', init); + + useStoreUpdater('userMeta', userMeta); + useStoreUpdater('assistantMeta', assistantMeta); + + useStoreUpdater('helloMessage', helloMessage); + useStoreUpdater('config', config); + + useStoreUpdater('chats', chats); + useStoreUpdater('onChatsChange', onChatsChange); + + return null; + }, +); + +export default StoreUpdater; diff --git a/src/ProChat/container/index.tsx b/src/ProChat/container/index.tsx new file mode 100644 index 00000000..cfe5086a --- /dev/null +++ b/src/ProChat/container/index.tsx @@ -0,0 +1,56 @@ +import { App as Container } from 'antd'; +import { ReactNode, memo } from 'react'; + +import App from './App'; + +import { DevtoolsOptions } from 'zustand/middleware'; +import { ChatProps } from '../store'; +import { ProChatProvider } from './Provider'; +import StoreUpdater from './StoreUpdater'; + +export interface ProChatProps extends ChatProps { + renderInput?: ReactNode; + __PRO_CHAT_STORE_DEVTOOLS__?: boolean | DevtoolsOptions; + showTitle?: boolean; +} + +export const ProChat = memo( + ({ + renderInput, + __PRO_CHAT_STORE_DEVTOOLS__, + chats, + onChatsChange, + initialChats, + loading, + helloMessage, + userMeta, + assistantMeta, + showTitle, + ...props + }) => { + return ( + + + + + + + ); + }, +); diff --git a/src/ProChat/demos/default.tsx b/src/ProChat/demos/default.tsx new file mode 100644 index 00000000..13474ff8 --- /dev/null +++ b/src/ProChat/demos/default.tsx @@ -0,0 +1,17 @@ +/** + * compact: true + */ +import { ProChat } from '@ant-design/pro-chat'; + +import { useTheme } from 'antd-style'; +import { Flexbox } from 'react-layout-kit'; +import { example } from '../mocks/basic'; + +export default () => { + const theme = useTheme(); + return ( + + + + ); +}; diff --git a/src/ProChat/demos/helloMessage.tsx b/src/ProChat/demos/helloMessage.tsx new file mode 100644 index 00000000..d59d6380 --- /dev/null +++ b/src/ProChat/demos/helloMessage.tsx @@ -0,0 +1,20 @@ +/** + * compact: true + */ +import { ProChat } from '@ant-design/pro-chat'; + +import { useTheme } from 'antd-style'; +import { Flexbox } from 'react-layout-kit'; + +export default () => { + const theme = useTheme(); + return ( + + + + ); +}; diff --git a/src/ProChat/demos/initialChats.tsx b/src/ProChat/demos/initialChats.tsx new file mode 100644 index 00000000..13474ff8 --- /dev/null +++ b/src/ProChat/demos/initialChats.tsx @@ -0,0 +1,17 @@ +/** + * compact: true + */ +import { ProChat } from '@ant-design/pro-chat'; + +import { useTheme } from 'antd-style'; +import { Flexbox } from 'react-layout-kit'; +import { example } from '../mocks/basic'; + +export default () => { + const theme = useTheme(); + return ( + + + + ); +}; diff --git a/src/ProChat/demos/loading.tsx b/src/ProChat/demos/loading.tsx new file mode 100644 index 00000000..4781485d --- /dev/null +++ b/src/ProChat/demos/loading.tsx @@ -0,0 +1,38 @@ +/** + * title: 设置 loading 添加数据获取态 + * compact: true + */ +import { ProChat } from '@ant-design/pro-chat'; +import { Button, Divider } from 'antd'; +import { useTheme } from 'antd-style'; +import { useState } from 'react'; +import { Flexbox } from 'react-layout-kit'; + +export default () => { + const [loading, setLoading] = useState(true); + const theme = useTheme(); + + return ( + + + + + + + + + ); +}; diff --git a/src/ProChat/demos/meta.tsx b/src/ProChat/demos/meta.tsx new file mode 100644 index 00000000..0cd5b998 --- /dev/null +++ b/src/ProChat/demos/meta.tsx @@ -0,0 +1,19 @@ +import { chats } from '@/ProChat/mocks/threebody'; +import { ProChat } from '@ant-design/pro-chat'; +import { useTheme } from 'antd-style'; +import { Flexbox } from 'react-layout-kit'; + +export default () => { + const theme = useTheme(); + + return ( + + + + ); +}; diff --git a/src/ProChat/demos/modal.tsx b/src/ProChat/demos/modal.tsx new file mode 100644 index 00000000..d54ebbeb --- /dev/null +++ b/src/ProChat/demos/modal.tsx @@ -0,0 +1,8 @@ +/** + * iframe + */ +import { ProChat } from '@ant-design/pro-chat'; + +export default () => { + return ; +}; diff --git a/src/ProChat/index.md b/src/ProChat/index.md new file mode 100644 index 00000000..b3f6b68c --- /dev/null +++ b/src/ProChat/index.md @@ -0,0 +1,44 @@ +--- +nav: Components +group: Chat +title: ProChat +description: a Chat Solution +--- + +## Default + +搭配 `request` 指定接口即可使用: + + + +## 设定初始会话消息 + +使用 `initialChats` 设定初始会话消息。 + + + +## 设定打招呼消息 + +通过 `helloMessage` 设定打招呼消息。 + + + +## 加载中 + +使用 loading 字段控制加载中状态。适用于第一次加载数据时使用。 + + + +## 指定会话双方的头像、名称 + +ProChat使用 `meta` 来表意会话双方的头像、名称等信息。设定助手的头像、名称使用 `assistantMeta`,设定用户的头像、名称使用 `userMeta`。 使用 `showTitle` 显示名称。 + + + +## 🚧 悬浮窗使用 + +将 `ProChat` 组件作为会话解决方案 + +## 🚧 程序化控制消息发送 + +## APIs diff --git a/src/ProChat/index.tsx b/src/ProChat/index.tsx index ac2420e3..4737113e 100644 --- a/src/ProChat/index.tsx +++ b/src/ProChat/index.tsx @@ -1,42 +1 @@ -import BackBottom from '@/BackBottom'; -import { createStyles } from 'antd-style'; -import { ReactNode, memo, useRef } from 'react'; -import { Flexbox } from 'react-layout-kit'; - -import ChatList from './components/ChatList'; -import ChatScrollAnchor from './components/ScrollAnchor'; - -const useStyles = createStyles( - ({ css, responsive, stylish }) => css` - overflow: hidden scroll; - height: 100%; - ${responsive.mobile} { - ${stylish.noScrollbar} - width: 100vw; - } - `, -); - -interface ConversationProps { - chatInput: ReactNode; -} - -const Conversation = memo(({ chatInput }) => { - const ref = useRef(null); - const { styles } = useStyles(); - - return ( - -
-
- - -
- -
- {chatInput} -
- ); -}); - -export default Conversation; +export { ProChat } from './container'; diff --git a/src/ProChat/mocks/basic.ts b/src/ProChat/mocks/basic.ts new file mode 100644 index 00000000..b1c90dd2 --- /dev/null +++ b/src/ProChat/mocks/basic.ts @@ -0,0 +1,30 @@ +export const example = { + chats: { + ZGxiX2p4: { + content: '昨天的当天是明天的什么?', + createAt: 1697862242452, + id: 'ZGxiX2p4', + role: 'user', + updateAt: 1697862243540, + }, + Sb5pAzLL: { + content: '昨天的当天是明天的昨天。', + createAt: 1697862247302, + id: 'Sb5pAzLL', + parentId: 'ZGxiX2p4', + role: 'assistant', + updateAt: 1697862249387, + model: 'gpt-3.5-turbo', + }, + }, + config: { + model: 'gpt-3.5-turbo', + params: { + frequency_penalty: 0, + presence_penalty: 0, + temperature: 0.6, + top_p: 1, + }, + systemRole: '', + }, +}; diff --git a/src/ProChat/mocks/threebody.ts b/src/ProChat/mocks/threebody.ts new file mode 100644 index 00000000..b7883bd4 --- /dev/null +++ b/src/ProChat/mocks/threebody.ts @@ -0,0 +1,30 @@ +export const chats = { + chats: { + ZGxiX2p4: { + content: '我对三体世界说话。', + createAt: 1697862242452, + id: 'ZGxiX2p4', + role: 'user', + updateAt: 1697862243540, + }, + Sb5pAzLL: { + content: '闭嘴', + createAt: 1697862247302, + id: 'Sb5pAzLL', + parentId: 'ZGxiX2p4', + role: 'assistant', + updateAt: 1697862249387, + model: 'gpt-3.5-turbo', + }, + }, + config: { + model: 'gpt-3.5-turbo', + params: { + frequency_penalty: 0, + presence_penalty: 0, + temperature: 0.6, + top_p: 1, + }, + systemRole: '', + }, +}; diff --git a/src/ProChat/store/slices/chat/action.ts b/src/ProChat/store/action.ts similarity index 77% rename from src/ProChat/store/slices/chat/action.ts rename to src/ProChat/store/action.ts index 6370b24b..b5d63bc9 100644 --- a/src/ProChat/store/slices/chat/action.ts +++ b/src/ProChat/store/action.ts @@ -3,18 +3,16 @@ import { StateCreator } from 'zustand/vanilla'; import { LOADING_FLAT } from '@/ProChat/const/message'; import { fetchChatModel } from '@/ProChat/services/chatModel'; -import { SessionStore } from '@/ProChat/store'; +import { ChatStore } from '@/ProChat/store/index'; +import { ChatMessage } from '@/ProChat/types/message'; import { fetchSSE } from '@/ProChat/utils/fetch'; import { isFunctionMessage } from '@/ProChat/utils/message'; import { setNamespace } from '@/ProChat/utils/storeDebug'; import { nanoid } from '@/ProChat/utils/uuid'; -import { ChatMessage } from '@/types/chatMessage'; -import { agentSelectors } from '../agentConfig/selectors'; -import { sessionSelectors } from '../session/selectors'; +import { getSlicedMessagesWithConfig } from '../utils/message'; import { MessageDispatch, messagesReducer } from './reducers/message'; import { chatSelectors } from './selectors'; -import { getSlicedMessagesWithConfig } from './utils'; const t = setNamespace('chat/message'); @@ -72,18 +70,16 @@ export interface ChatAction { ) => AbortController | undefined; } -export const createChatSlice: StateCreator< - SessionStore, - [['zustand/devtools', never]], - [], - ChatAction -> = (set, get) => ({ +export const chatAction: StateCreator = ( + set, + get, +) => ({ clearMessage: () => { - const { dispatchMessage, activeTopicId } = get(); + const { dispatchMessage } = get(); - dispatchMessage({ topicId: activeTopicId, type: 'resetMessages' }); + dispatchMessage({ type: 'resetMessages' }); - // TODO: need a topic callback + // TODO: need callback after reset }, deleteMessage: (id) => { @@ -91,16 +87,14 @@ export const createChatSlice: StateCreator< }, dispatchMessage: (payload) => { - const { activeId } = get(); - const session = sessionSelectors.currentSession(get()); - if (!activeId || !session) return; + const { chats } = get(); - const chats = messagesReducer(session.chats, payload); + const nextChats = messagesReducer(chats, payload); - get().dispatchSession({ chats, id: activeId, type: 'updateSessionChat' }); + set({ chats: nextChats }, false, t('dispatchMessage')); }, generateMessage: async (messages, assistantId) => { - const { dispatchMessage, toggleChatLoading } = get(); + const { dispatchMessage, toggleChatLoading, config } = get(); const abortController = toggleChatLoading( true, @@ -108,8 +102,6 @@ export const createChatSlice: StateCreator< t('generateMessage(start)', { assistantId, messages }) as string, ); - const config = agentSelectors.currentAgentConfig(get()); - const compiler = template(config.inputTemplate, { interpolate: /{{([\S\s]+?)}}/g }); // ========================== // @@ -146,7 +138,6 @@ export const createChatSlice: StateCreator< messages: postMessages, model: config.model, ...config.params, - plugins: config.plugins, }, { signal: abortController?.signal }, ); @@ -182,9 +173,7 @@ export const createChatSlice: StateCreator< }, realFetchAIResponse: async (messages, userMessageId) => { - const { dispatchMessage, generateMessage, activeTopicId } = get(); - - const { model } = agentSelectors.currentAgentConfig(get()); + const { dispatchMessage, generateMessage, config } = get(); // 添加一个空的信息用于放置 ai 响应,注意顺序不能反 // 因为如果顺序反了,messages 中将包含新增的 ai message @@ -198,25 +187,19 @@ export const createChatSlice: StateCreator< type: 'addMessage', }); - // 如果有 activeTopicId,则添加 topicId - if (activeTopicId) { - dispatchMessage({ id: mid, key: 'topicId', type: 'updateMessage', value: activeTopicId }); - } + // TODO: need a callback before generate message // 为模型添加 fromModel 的额外信息 - dispatchMessage({ id: mid, key: 'fromModel', type: 'updateMessageExtra', value: model }); + // TODO: 此处需要model 信息 + dispatchMessage({ id: mid, key: 'fromModel', type: 'updateMessageExtra', value: config.model }); // 生成 ai message await generateMessage(messages, mid); - // todo: need fc callback + // todo: need callback after generate message }, resendMessage: async (messageId) => { - const session = sessionSelectors.currentSession(get()); - - if (!session) return; - // 1. 构造所有相关的历史记录 const chats = chatSelectors.currentChats(get()); @@ -255,24 +238,21 @@ export const createChatSlice: StateCreator< }, sendMessage: async (message) => { - const { dispatchMessage, realFetchAIResponse, activeTopicId } = get(); - const session = sessionSelectors.currentSession(get()); - if (!session || !message) return; + const { dispatchMessage, realFetchAIResponse } = get(); + + if (!message) return; const userId = nanoid(); dispatchMessage({ id: userId, message, role: 'user', type: 'addMessage' }); - // if there is activeTopicId,then add topicId to message - if (activeTopicId) { - dispatchMessage({ id: userId, key: 'topicId', type: 'updateMessage', value: activeTopicId }); - } + // Todo: need a callback before send message // Get the current messages to generate AI response const messages = chatSelectors.currentChats(get()); await realFetchAIResponse(messages, userId); - // TODO: need a topic callback + // TODO: need a callback after send }, stopGenerateMessage: () => { diff --git a/src/ProChat/store/hooks/useEffectAfterHydrated.ts b/src/ProChat/store/hooks/useEffectAfterHydrated.ts index dfb308f4..e9449deb 100644 --- a/src/ProChat/store/hooks/useEffectAfterHydrated.ts +++ b/src/ProChat/store/hooks/useEffectAfterHydrated.ts @@ -1,26 +1,26 @@ import { useEffect } from 'react'; -import { SessionStore, useChatStore } from '../store'; +import { ChatStore, useStore } from '../store'; export const useEffectAfterSessionHydrated = ( - fn: (session: typeof useChatStore, store: SessionStore) => void, + fn: (session: typeof useStore, store: ChatStore) => void, deps: any[] = [], ) => { // const hasTrigger = useRef(false); useEffect(() => { - const hasRehydrated = useChatStore.persist.hasHydrated(); + const hasRehydrated = useStore.persist.hasHydrated(); if (hasRehydrated) { // equal useEffect triggered multi-time - fn(useChatStore, useChatStore.getState()); + fn(useStore, useStore.getState()); } else { // keep onFinishHydration just are triggered only once // if (hasTrigger.current) return; // // hasTrigger.current = true; // equal useEffect first trigger - useChatStore.persist.onFinishHydration(() => { - fn(useChatStore, useChatStore.getState()); + useStore.persist.onFinishHydration(() => { + fn(useStore, useStore.getState()); }); } }, deps); diff --git a/src/ProChat/store/hooks/useOnFinishHydrationSession.ts b/src/ProChat/store/hooks/useOnFinishHydrationSession.ts index efe2e4c2..dacc6e5f 100644 --- a/src/ProChat/store/hooks/useOnFinishHydrationSession.ts +++ b/src/ProChat/store/hooks/useOnFinishHydrationSession.ts @@ -1,19 +1,19 @@ import { useEffect } from 'react'; import { StoreApi, UseBoundStore } from 'zustand'; -import { SessionStore, useChatStore } from '../store'; +import { ChatStore, useStore } from '../store'; /** * 当 Session 水合完毕后才会执行的 useEffect * @param fn */ export const useOnFinishHydrationSession = ( - fn: (state: SessionStore, store: UseBoundStore>) => void, + fn: (state: ChatStore, store: UseBoundStore>) => void, ) => { useEffect(() => { // 只有当水合完毕后再开始做操作 - useChatStore.persist.onFinishHydration(() => { - fn(useChatStore.getState(), useChatStore); + useStore.persist.onFinishHydration(() => { + fn(useStore.getState(), useStore); }); }, []); }; diff --git a/src/ProChat/store/hooks/useSessionChatInit.ts b/src/ProChat/store/hooks/useSessionChatInit.ts index 5cab66d9..31637e56 100644 --- a/src/ProChat/store/hooks/useSessionChatInit.ts +++ b/src/ProChat/store/hooks/useSessionChatInit.ts @@ -1,4 +1,4 @@ -import { useChatStore } from '../store'; +import { useStore } from '../store'; import { useSessionHydrated } from './useSessionHydrated'; /** @@ -6,7 +6,8 @@ import { useSessionHydrated } from './useSessionHydrated'; */ export const useSessionChatInit = () => { const sessionHydrated = useSessionHydrated(); - const [hasActive] = useChatStore((s) => [!!s.activeId]); + // TODO: mark when session is active + const [hasActive] = useStore(() => [true]); return sessionHydrated && hasActive; }; diff --git a/src/ProChat/store/hooks/useSessionHydrated.ts b/src/ProChat/store/hooks/useSessionHydrated.ts index 5ad0497f..b823c0d3 100644 --- a/src/ProChat/store/hooks/useSessionHydrated.ts +++ b/src/ProChat/store/hooks/useSessionHydrated.ts @@ -1,11 +1,11 @@ import { useState } from 'react'; -import { useChatStore } from '../store'; +import { useStore } from '../store'; import { useEffectAfterSessionHydrated } from './useEffectAfterHydrated'; export const useSessionHydrated = () => { - // 根据 sessions 是否有值来判断是否已经初始化 - const hasInited = !!Object.values(useChatStore.getState().sessions).length; + // TODO: 根据 config 是否有值来判断是否已经初始化 + const hasInited = !!Object.values(useStore.getState().config).length; const [isInit, setInit] = useState(hasInited); diff --git a/src/ProChat/store/index.ts b/src/ProChat/store/index.ts index ae8544d8..367df469 100644 --- a/src/ProChat/store/index.ts +++ b/src/ProChat/store/index.ts @@ -1,3 +1,8 @@ -export * from './hooks'; -export { useChatStore } from './store'; -export type { SessionStore } from './store'; +import { StoreApi } from 'zustand'; +import { createContext } from 'zustand-utils'; +import { ChatStore } from './store'; + +export type { ChatState } from './initialState'; +export * from './store'; + +export const { useStore, useStoreApi, Provider } = createContext>(); diff --git a/src/ProChat/store/initialState.ts b/src/ProChat/store/initialState.ts index 6bbd22b1..e13ed33c 100644 --- a/src/ProChat/store/initialState.ts +++ b/src/ProChat/store/initialState.ts @@ -1,12 +1,55 @@ -import { ChatState, initialChatState } from './slices/chat/initialState'; -import { SessionState, initialSessionState } from './slices/session/initialState'; +import { DEFAULT_AVATAR, DEFAULT_USER_AVATAR } from '@/ProChat/const/meta'; +import { ModelConfig } from '@/ProChat/types/config'; +import { ChatMessageMap } from '@/ProChat/types/message'; +import { MetaData } from '@/ProChat/types/meta'; +import { LanguageModel } from '@/types'; -export type SessionStoreState = SessionState & ChatState; +export interface ChatPropsState { + /** + * 语言模型角色设定 + */ + config: ModelConfig; + /** + * 聊天记录 + */ + chats: ChatMessageMap; + onChatsChange?: (chats: ChatMessageMap) => void; + displayMode: 'chat' | 'docs'; + userMeta: MetaData; + assistantMeta: MetaData; + /** + * 帮助消息 + */ + helloMessage?: string; +} -export const initialState: SessionStoreState = { - ...initialSessionState, - ...initialChatState, +export interface ChatState extends ChatPropsState { + init?: boolean; + abortController?: AbortController; + chatLoadingId?: string; +} + +export const initialLobeAgentConfig: ModelConfig = { + historyCount: 1, + model: LanguageModel.GPT3_5, + params: { + frequency_penalty: 0, + presence_penalty: 0, + temperature: 0.6, + top_p: 1, + }, + systemRole: '', }; -export { initialLobeAgentConfig } from './slices/agentConfig'; -export { initLobeSession } from './slices/session/initialState'; +export const initialState: ChatState = { + chats: {}, + init: true, + displayMode: 'chat', + userMeta: { + avatar: DEFAULT_USER_AVATAR, + }, + assistantMeta: { + avatar: DEFAULT_AVATAR, + }, + config: initialLobeAgentConfig, +}; diff --git a/src/ProChat/store/slices/chat/reducers/message.test.ts b/src/ProChat/store/reducers/message.test.ts similarity index 99% rename from src/ProChat/store/slices/chat/reducers/message.test.ts rename to src/ProChat/store/reducers/message.test.ts index 6ac51984..11ad6493 100644 --- a/src/ProChat/store/slices/chat/reducers/message.test.ts +++ b/src/ProChat/store/reducers/message.test.ts @@ -1,4 +1,4 @@ -import { ChatMessageMap } from '@/types/chatMessage'; +import { ChatMessageMap } from '@/ProChat/types/message'; import { MessageDispatch, messagesReducer } from './message'; diff --git a/src/ProChat/store/slices/chat/reducers/message.ts b/src/ProChat/store/reducers/message.ts similarity index 83% rename from src/ProChat/store/slices/chat/reducers/message.ts rename to src/ProChat/store/reducers/message.ts index 1d7be225..cd28acda 100644 --- a/src/ProChat/store/slices/chat/reducers/message.ts +++ b/src/ProChat/store/reducers/message.ts @@ -1,9 +1,9 @@ import { produce } from 'immer'; -import { ChatMessage, ChatMessageMap } from '@/types/chatMessage'; +import { ChatMessage, ChatMessageMap } from '@/ProChat/types/message'; +import { nanoid } from '@/ProChat/utils/uuid'; import { LLMRoleType } from '@/types/llm'; import { MetaData } from '@/types/meta'; -import { nanoid } from '@/utils/uuid'; interface AddMessage { id?: string; @@ -58,9 +58,7 @@ export const messagesReducer = ( content: payload.message, createAt: Date.now(), id: mid, - meta: payload.meta || {}, parentId: payload.parentId, - quotaId: payload.quotaId, role: payload.role, updateAt: Date.now(), }; @@ -103,14 +101,7 @@ export const messagesReducer = ( case 'resetMessages': { return produce(state, (draftState) => { - const { topicId } = payload; - - const messages = Object.values(draftState).filter((message) => { - // 如果没有 topicId,说明是清空默认对话里的消息 - if (!topicId) return !message.topicId; - - return message.topicId === topicId; - }); + const messages = Object.values(draftState); // 删除上述找到的消息 for (const message of messages) { diff --git a/src/ProChat/store/selectors.ts b/src/ProChat/store/selectors.ts deleted file mode 100644 index e7adb388..00000000 --- a/src/ProChat/store/selectors.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { agentSelectors } from './slices/agentConfig/selectors'; -export { chatSelectors, topicSelectors } from './slices/chat/selectors'; -export { sessionSelectors } from './slices/session/selectors'; diff --git a/src/ProChat/store/selectors/chat.ts b/src/ProChat/store/selectors/chat.ts new file mode 100644 index 00000000..5c86b242 --- /dev/null +++ b/src/ProChat/store/selectors/chat.ts @@ -0,0 +1,99 @@ +import { ChatMessage } from '@/ProChat/types/message'; + +import { MetaData } from '@/ProChat/types/meta'; +import { getSlicedMessagesWithConfig } from '../../utils/message'; +import type { ChatStore } from '../store'; + +// 当前激活的消息列表 +export const currentChats = (s: ChatStore): ChatMessage[] => { + if (Object.keys(s.chats).length === 0) return []; + + const getMeta = (message: ChatMessage): MetaData => { + const user = s.userMeta; + const assistant = s.assistantMeta; + switch (message.role) { + case 'user': { + return { + avatar: user?.avatar, + title: user?.title, + }; + } + + case 'system': { + return assistant; + } + + case 'assistant': { + return { + avatar: assistant?.avatar, + backgroundColor: assistant?.backgroundColor, + title: assistant?.title, + }; + } + } + + return {}; + }; + + const basic = Object.values(s.chats) + // 首先按照时间顺序排序,越早的在越前面 + .sort((pre, next) => pre.createAt - next.createAt) + // 映射头像关系 + .map((m) => { + return { + ...m, + meta: getMeta(m), + }; + }); + + const finalList: ChatMessage[] = []; + + const addItem = (item: ChatMessage) => { + const isExist = finalList.findIndex((i) => item.id === i.id) > -1; + if (!isExist) { + finalList.push(item); + } + }; + + // 基于添加逻辑进行重排序 + for (const item of basic) { + // 先判存在与否,不存在就加入 + addItem(item); + + for (const another of basic) { + if (another.parentId === item.id) { + addItem(another); + } + } + } + + return finalList; +}; + +// 针对新助手添加初始化时的自定义消息 +export const currentChatsWithGuideMessage = (s: ChatStore): ChatMessage[] => { + const data = currentChats(s); + // TODO: need topic inject + + const isBrandNewChat = data.length === 0; + + if (!isBrandNewChat) return data; + + const emptyInboxGuideMessage = { + content: s.helloMessage ?? '让我们开始对话吧', + createAt: Date.now(), + extra: {}, + id: 'default', + meta: s.assistantMeta, + role: 'assistant', + updateAt: Date.now(), + } as ChatMessage; + + return [emptyInboxGuideMessage]; +}; + +export const currentChatsWithHistoryConfig = (s: ChatStore): ChatMessage[] => { + const chats = currentChats(s); + + return getSlicedMessagesWithConfig(chats, s.config); +}; diff --git a/src/ProChat/store/selectors/index.ts b/src/ProChat/store/selectors/index.ts new file mode 100644 index 00000000..38fc3c44 --- /dev/null +++ b/src/ProChat/store/selectors/index.ts @@ -0,0 +1,7 @@ +import { currentChats, currentChatsWithGuideMessage, currentChatsWithHistoryConfig } from './chat'; + +export const chatSelectors = { + currentChats, + currentChatsWithGuideMessage, + currentChatsWithHistoryConfig, +}; diff --git a/src/ProChat/store/slices/agentConfig/action.ts b/src/ProChat/store/slices/agentConfig/action.ts deleted file mode 100644 index 762e946c..00000000 --- a/src/ProChat/store/slices/agentConfig/action.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { produce } from 'immer'; -import { StateCreator } from 'zustand/vanilla'; - -import { MetaData } from '@/types/meta'; -import { LobeAgentConfig } from '@/types/session'; - -import { SessionStore } from '../../store'; -import { sessionSelectors } from '../session/selectors'; - -/** - * 助手接口 - */ -export interface AgentAction { - removePlugin: (id: string) => void; - /** - * 更新代理配置 - * @param config - 部分 LobeAgentConfig 的配置 - */ - updateAgentConfig: (config: Partial) => void; - updateAgentMeta: (meta: Partial) => void; -} - -export const createAgentSlice: StateCreator< - SessionStore, - [['zustand/devtools', never]], - [], - AgentAction -> = (set, get) => ({ - removePlugin: (id) => { - const { activeId } = get(); - const session = sessionSelectors.currentSession(get()); - if (!activeId || !session) return; - - const config = produce(session.config, (draft) => { - draft.plugins = draft.plugins?.filter((i) => i !== id) || []; - }); - - get().dispatchSession({ config, id: activeId, type: 'updateSessionConfig' }); - }, - - updateAgentConfig: (config) => { - const { activeId } = get(); - const session = sessionSelectors.currentSession(get()); - if (!activeId || !session) return; - - get().dispatchSession({ config, id: activeId, type: 'updateSessionConfig' }); - }, - - updateAgentMeta: (meta) => { - const { activeId } = get(); - const session = sessionSelectors.currentSession(get()); - if (!activeId || !session) return; - - for (const [key, value] of Object.entries(meta)) { - if (value !== undefined) { - get().dispatchSession({ - id: activeId, - key: key as keyof MetaData, - type: 'updateSessionMeta', - value, - }); - } - } - }, -}); diff --git a/src/ProChat/store/slices/agentConfig/index.ts b/src/ProChat/store/slices/agentConfig/index.ts deleted file mode 100644 index 6bdd1f4b..00000000 --- a/src/ProChat/store/slices/agentConfig/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './action'; -export * from './initialState'; -export * from './selectors'; diff --git a/src/ProChat/store/slices/agentConfig/initialState.ts b/src/ProChat/store/slices/agentConfig/initialState.ts deleted file mode 100644 index 1c79a8c0..00000000 --- a/src/ProChat/store/slices/agentConfig/initialState.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { DEFAULT_AGENT_CONFIG } from '@/const/settings'; -import { LobeAgentConfig } from '@/types/session'; - -export const initialLobeAgentConfig: LobeAgentConfig = DEFAULT_AGENT_CONFIG; diff --git a/src/ProChat/store/slices/agentConfig/selectors.ts b/src/ProChat/store/slices/agentConfig/selectors.ts deleted file mode 100644 index 6a8f8999..00000000 --- a/src/ProChat/store/slices/agentConfig/selectors.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { t } from 'i18next'; - -import { DEFAULT_OPENAI_MODEL_LIST } from '@/const/llm'; -import { DEFAULT_AVATAR, DEFAULT_BACKGROUND_COLOR } from '@/const/meta'; -import { SessionStore } from '@/ProChat/store'; -import { LanguageModel } from '@/types/llm'; -import { MetaData } from '@/types/meta'; -import { merge } from '@/utils/merge'; - -import { sessionSelectors } from '../session/selectors'; -import { initialLobeAgentConfig } from './initialState'; - -const currentAgentMeta = (s: SessionStore): MetaData => { - const session = sessionSelectors.currentSession(s); - - return { avatar: DEFAULT_AVATAR, backgroundColor: DEFAULT_BACKGROUND_COLOR, ...session?.meta }; -}; - -const currentAgentTitle = (s: SessionStore) => currentAgentMeta(s)?.title || t('defaultSession'); - -const currentAgentConfig = (s: SessionStore) => { - const session = sessionSelectors.currentSession(s); - return merge(initialLobeAgentConfig, session?.config); -}; - -const currentAgentSystemRole = (s: SessionStore) => { - return currentAgentConfig(s).systemRole; -}; - -const currentAgentDescription = (s: SessionStore) => - currentAgentMeta(s)?.description || currentAgentSystemRole(s) || t('noDescription'); - -const currentAgentBackgroundColor = (s: SessionStore) => { - const session = sessionSelectors.currentSession(s); - if (!session) return DEFAULT_BACKGROUND_COLOR; - return session.meta.backgroundColor || DEFAULT_BACKGROUND_COLOR; -}; - -const currentAgentAvatar = (s: SessionStore) => { - const session = sessionSelectors.currentSession(s); - if (!session) return DEFAULT_AVATAR; - - return session.meta.avatar || DEFAULT_AVATAR; -}; - -const currentAgentModel = (s: SessionStore): LanguageModel | string => { - const config = currentAgentConfig(s); - - return config?.model || LanguageModel.GPT3_5; -}; - -const currentAgentPlugins = (s: SessionStore) => { - const config = currentAgentConfig(s); - - return config?.plugins || []; -}; - -const hasSystemRole = (s: SessionStore) => { - const config = currentAgentConfig(s); - - return !!config.systemRole; -}; - -const getAvatar = (s: MetaData) => s.avatar || DEFAULT_AVATAR; -const getTitle = (s: MetaData) => s.title || t('defaultSession', { ns: 'common' }); - -export const getDescription = (s: MetaData) => - s.description || t('noDescription', { ns: 'common' }); - -const showTokenTag = (s: SessionStore) => { - const model = currentAgentModel(s); - - return DEFAULT_OPENAI_MODEL_LIST.includes(model); -}; - -export const agentSelectors = { - currentAgentAvatar, - currentAgentBackgroundColor, - currentAgentConfig, - currentAgentDescription, - currentAgentMeta, - currentAgentModel, - currentAgentPlugins, - currentAgentSystemRole, - currentAgentTitle, - getAvatar, - getDescription, - getTitle, - hasSystemRole, - showTokenTag, -}; diff --git a/src/ProChat/store/slices/chat/initialState.ts b/src/ProChat/store/slices/chat/initialState.ts deleted file mode 100644 index 2875ca48..00000000 --- a/src/ProChat/store/slices/chat/initialState.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface ChatState { - abortController?: AbortController; - chatLoadingId?: string; -} - -export const initialChatState: ChatState = {}; diff --git a/src/ProChat/store/slices/chat/selectors/chat.ts b/src/ProChat/store/slices/chat/selectors/chat.ts deleted file mode 100644 index 7077ac95..00000000 --- a/src/ProChat/store/slices/chat/selectors/chat.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { t } from 'i18next'; - -import { FunctionCallProps } from '@/app/chat/features/Conversation/ChatList/Plugins/FunctionCall'; -import { DEFAULT_INBOX_AVATAR, DEFAULT_USER_AVATAR } from '@/const/meta'; -import { useGlobalStore } from '@/store/global'; -import { ChatMessage } from '@/types/chatMessage'; - -import type { SessionStore } from '../../../store'; -import { agentSelectors } from '../../agentConfig'; -import { sessionSelectors } from '../../session/selectors'; -import { getSlicedMessagesWithConfig } from '../utils'; -import { organizeChats } from './utils'; - -export const getChatsById = - (id: string) => - (s: SessionStore): ChatMessage[] => { - const session = sessionSelectors.getSessionById(id)(s); - - if (!session) return []; - - return organizeChats(session, { - meta: { - assistant: { - avatar: agentSelectors.currentAgentAvatar(s), - backgroundColor: agentSelectors.currentAgentBackgroundColor(s), - }, - user: { - avatar: - // TODO: need props config - useGlobalStore.getState().settings.avatar || DEFAULT_USER_AVATAR, - }, - }, - topicId: s.activeTopicId, - }); - }; - -// 当前激活的消息列表 -export const currentChats = (s: SessionStore): ChatMessage[] => { - if (!s.activeId) return []; - - return getChatsById(s.activeId)(s); -}; - -// 针对新助手添加初始化时的自定义消息 -export const currentChatsWithGuideMessage = (s: SessionStore): ChatMessage[] => { - const data = currentChats(s); - // TODO: need topic inject - - const isBrandNewChat = data.length === 0; - - if (!isBrandNewChat) return data; - - const [activeId] = [s.activeId]; - const meta = agentSelectors.currentAgentMeta(s); - - const agentSystemRoleMsg = t('agentDefaultMessageWithSystemRole', { - name: meta.title || t('defaultAgent'), - ns: 'chat', - systemRole: meta.description, - }); - const agentMsg = t('agentDefaultMessage', { - id: activeId, - name: meta.title || t('defaultAgent'), - ns: 'chat', - }); - - const emptyInboxGuideMessage = { - content: !!meta.description ? agentSystemRoleMsg : agentMsg, - createAt: Date.now(), - extra: {}, - id: 'default', - meta: meta || { - avatar: DEFAULT_INBOX_AVATAR, - }, - role: 'assistant', - updateAt: Date.now(), - } as ChatMessage; - - return [emptyInboxGuideMessage]; -}; - -export const currentChatsWithHistoryConfig = (s: SessionStore): ChatMessage[] => { - const chats = currentChats(s); - const config = agentSelectors.currentAgentConfig(s); - - return getSlicedMessagesWithConfig(chats, config); -}; - -export const chatsMessageString = (s: SessionStore): string => { - const chats = currentChatsWithHistoryConfig(s); - return chats.map((m) => m.content).join(''); -}; - -export const getFunctionMessageParams = - ( - s: SessionStore, - ): (( - props: Pick, - ) => FunctionCallProps) => - ({ plugin, function_call, content, id }) => { - const itemId = plugin?.identifier || function_call?.name; - const command = plugin ?? function_call; - const args = command?.arguments; - - return { - arguments: args, - command, - content, - id: itemId, - loading: id === s.chatLoadingId, - }; - }; diff --git a/src/ProChat/store/slices/chat/selectors/index.ts b/src/ProChat/store/slices/chat/selectors/index.ts deleted file mode 100644 index c2e2ab55..00000000 --- a/src/ProChat/store/slices/chat/selectors/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { - chatsMessageString, - currentChats, - currentChatsWithGuideMessage, - currentChatsWithHistoryConfig, - getChatsById, - getFunctionMessageParams, -} from './chat'; - -export const chatSelectors = { - chatsMessageString, - currentChats, - currentChatsWithGuideMessage, - currentChatsWithHistoryConfig, - getChatsById, - getFunctionMessageParams, -}; diff --git a/src/ProChat/store/slices/chat/selectors/utils.test.ts b/src/ProChat/store/slices/chat/selectors/utils.test.ts deleted file mode 100644 index ed3ef529..00000000 --- a/src/ProChat/store/slices/chat/selectors/utils.test.ts +++ /dev/null @@ -1,365 +0,0 @@ -import { beforeEach, describe } from 'vitest'; - -import { ChatMessage } from '@/types/chatMessage'; -import { LobeAgentSession } from '@/types/session'; - -import { organizeChats } from './utils'; - -let session: LobeAgentSession; - -beforeEach(() => { - session = { - chats: { - '1': { - id: '1', - createAt: 1639440000000, - updateAt: 1639440000000, - meta: {}, - content: 'Message 1', - role: 'assistant', - }, - '2': { - id: '2', - createAt: 1639450000000, - updateAt: 1639450000000, - meta: {}, - content: 'Message 2', - role: 'user', - }, - '3': { - id: '3', - createAt: 1639460000000, - updateAt: 1639460000000, - meta: {}, - content: 'Message 3', - role: 'assistant', - }, - }, - config: { - model: 'gpt-4', - params: { - temperature: 0.6, - }, - systemRole: '', - }, - type: 'agent', - createAt: 1690110700808, - id: 'abc', - meta: {}, - updateAt: 1690110700808, - } as LobeAgentSession; -}); - -describe('organizeChats', () => { - it('should return an array of chat messages', () => { - const result = organizeChats(session); - expect(Array.isArray(result)).toBe(true); - expect(result.length).toBe(3); - expect(result[0].id).toBe('1'); - expect(result[1].id).toBe('2'); - expect(result[2].id).toBe('3'); - }); - - it('should sort chat messages in ascending order of createAt', () => { - const result = organizeChats(session); - expect(result[0].id).toBe('1'); - expect(result[1].id).toBe('2'); - expect(result[2].id).toBe('3'); - }); - - it('should filter out archived messages', () => { - session.chats['2'].topicId = '123'; - const result = organizeChats(session); - expect(result.length).toBe(2); - expect(result[0].id).toBe('1'); - expect(result[1].id).toBe('3'); - }); - - it('should map avatars correctly', () => { - const avatar = 'https://example.com/avatar.png'; - const settings = { - avatar: 'https://example.com/user-avatar.png', - }; - - const result = organizeChats(session, { - meta: { - user: { avatar: settings.avatar }, - assistant: { avatar }, - }, - }); - - expect(result[0].meta.avatar).toBe(avatar); - expect(result[1].meta.avatar).toBe(settings.avatar); - expect(result[2].meta.avatar).toBe(avatar); - }); - - it('should reorder messages based on parent-child relationship', () => { - session.chats['2'].parentId = '1'; - session.chats['3'].parentId = '2'; - const result = organizeChats(session); - expect(result.length).toBe(3); - expect(result[0].id).toBe('1'); - expect(result[1].id).toBe('2'); - expect(result[2].id).toBe('3'); - }); - - it('should remove duplicate messages', () => { - session.chats['2'].parentId = '1'; - session.chats['3'].parentId = '2'; - session.chats['3'].id = '2'; - const result = organizeChats(session); - expect(result.length).toBe(2); - expect(result[0].id).toBe('1'); - expect(result[1].id).toBe('2'); - }); - - it('should return an empty array for empty session', () => { - const emptySession = { - chats: {}, - config: {}, - type: 'agent', - } as LobeAgentSession; - const result = organizeChats(emptySession); - expect(Array.isArray(result)).toBe(true); - expect(result.length).toBe(0); - }); - - it('should handle large number of chat messages', () => { - const largeSession = { - chats: {}, - config: {}, - type: 'agent', - meta: {}, - } as LobeAgentSession; - - const numMessages = 1000; - - for (let i = 1; i <= numMessages; i++) { - largeSession.chats[i.toString()] = { - id: i.toString(), - createAt: i, - updateAt: i, - meta: {}, - content: `Message ${i}`, - role: 'assistant', - }; - } - - const result = organizeChats(largeSession); - expect(result.length).toBe(numMessages); - expect(result[0].id).toBe('1'); - expect(result[numMessages - 1].id).toBe(numMessages.toString()); - }); - - it('按照预期进行排序', () => { - const realSession = { - chats: { - Ftei28dF: { - content: '鲁迅为何暴打周树人', - createAt: 1690111354731, - id: 'Ftei28dF', - meta: {}, - role: 'user', - updateAt: 1690111354731, - }, - '9bQW9hTs': { - content: - '这是一种误解。鲁迅和周树人是同一人,指的都是现代文学家周樟寿。"鲁迅"是他的笔名,"周树人"则是他的原名。所以,鲁迅并没有暴打周树人,这只是一种说法上的误解。', - createAt: 1690111354734, - id: '9bQW9hTs', - meta: {}, - parentId: 'Ftei28dF', - role: 'assistant', - updateAt: 1690111361514, - extra: { - fromModel: 'gpt-4', - }, - }, - HRQGSszU: { - content: - '这个问题基于一个误解。实际上,鲁迅和周树人是同一个人,这是中国现代文学的开山鼻祖周树人的笔名。他的本名是周树人,鲁迅则是他在30岁时开始使用的笔名。因此,鲁迅不能暴打周树人,因为他们是同一个人。', - createAt: 1690111364344, - id: 'HRQGSszU', - meta: {}, - parentId: 'Ftei28dF', - role: 'assistant', - updateAt: 1690111369519, - extra: { - fromModel: 'gpt-4', - }, - }, - '981qr9n0': { - content: - '这是一个误解。鲁迅和周树人是同一个人,是中国现代文学的奠基人。鲁迅是他的笔名,周树人是他的本名。所以,鲁迅不可能暴打周树人。这种说法可能是源于一些误解或误传。', - createAt: 1690111375456, - id: '981qr9n0', - meta: {}, - parentId: 'Ftei28dF', - role: 'assistant', - updateAt: 1690111381458, - extra: { - fromModel: 'gpt-4', - }, - }, - ddd: { - content: '鲁迅是谁', - createAt: 1690211354731, - id: 'ddd', - meta: {}, - role: 'user', - updateAt: 1690211354731, - }, - }, - config: { - model: 'gpt-4', - params: { - temperature: 0.6, - }, - systemRole: '', - }, - createAt: 1690110700808, - id: '1515e861-0c64-49a3-bb85-2b24d65a19d6', - meta: {}, - type: 'agent', - updateAt: 1690110700808, - } as LobeAgentSession; - - const result = organizeChats(realSession); - - expect( - result.map((i) => ({ id: i.id, content: i.content, role: i.role, createAt: i.createAt })), - ).toEqual([ - { - content: '鲁迅为何暴打周树人', - createAt: 1690111354731, - id: 'Ftei28dF', - role: 'user', - }, - { - content: - '这是一种误解。鲁迅和周树人是同一人,指的都是现代文学家周樟寿。"鲁迅"是他的笔名,"周树人"则是他的原名。所以,鲁迅并没有暴打周树人,这只是一种说法上的误解。', - createAt: 1690111354734, - id: '9bQW9hTs', - role: 'assistant', - }, - { - content: - '这个问题基于一个误解。实际上,鲁迅和周树人是同一个人,这是中国现代文学的开山鼻祖周树人的笔名。他的本名是周树人,鲁迅则是他在30岁时开始使用的笔名。因此,鲁迅不能暴打周树人,因为他们是同一个人。', - createAt: 1690111364344, - id: 'HRQGSszU', - role: 'assistant', - }, - { - content: - '这是一个误解。鲁迅和周树人是同一个人,是中国现代文学的奠基人。鲁迅是他的笔名,周树人是他的本名。所以,鲁迅不可能暴打周树人。这种说法可能是源于一些误解或误传。', - createAt: 1690111375456, - id: '981qr9n0', - role: 'assistant', - }, - { - content: '鲁迅是谁', - createAt: 1690211354731, - id: 'ddd', - role: 'user', - }, - ]); - }); - - const params = { - meta: { - user: { avatar: 'user-avatar' }, - assistant: { avatar: 'assistant-avatar' }, - }, - }; - - it('should organize chats in chronological order when topicId is not provided', () => { - const result = organizeChats(session, params); - - expect(result.length).toBe(3); - expect(result[0].id).toBe('1'); - expect(result[1].id).toBe('2'); - expect(result[2].id).toBe('3'); - }); - - it('should only return chats with specified topicId when topicId is provided', () => { - const result = organizeChats(session, { ...params, topicId: 'topic-id' }); - - expect(result.length).toBe(0); - }); - - describe('user Meta', () => { - it('should return correct meta for user role', () => { - const result = organizeChats(session, params); - const meta = result[1].meta; - - expect(meta.avatar).toBe(params.meta.user.avatar); - }); - - it('should return correct meta for assistant role', () => { - const result = organizeChats(session, params); - const meta = result[0].meta; - - expect(meta.avatar).toBe('assistant-avatar'); - expect(meta.title).toBeUndefined(); - }); - - describe('should return correct meta for function role', () => { - it('找不到插件', () => { - const message = { - id: '4', - createAt: 1927785600004, - updateAt: 1927785600004, - role: 'function', - function_call: { - name: 'plugin-name', - }, - } as ChatMessage; - - session.chats[message.id] = message; - - const result = organizeChats(session, params); - const meta = result[3].meta; - - expect(meta.avatar).toBe('🧩'); - expect(meta.title).toBe('plugin-unknown'); - }); - - it('找到的插件', () => { - const message = { - id: '4', - createAt: 1927785600004, - updateAt: 1927785600004, - role: 'function', - function_call: { - name: 'realtimeWeather', - }, - name: 'realtimeWeather', - } as ChatMessage; - - session.chats[message.id] = message; - - const result = organizeChats(session, { - ...params, - pluginList: [ - { - identifier: 'realtimeWeather', - author: '123', - meta: { - avatar: '🧩', - title: '天气预报', - }, - createAt: 'abc', - manifest: '', - homepage: '', - schemaVersion: 1, - }, - ], - }); - const meta = result[3].meta; - - expect(meta.avatar).toBe('🧩'); - expect(meta.title).toBe('realtimeWeather'); - }); - }); - }); -}); diff --git a/src/ProChat/store/slices/chat/selectors/utils.ts b/src/ProChat/store/slices/chat/selectors/utils.ts deleted file mode 100644 index a90d4bb1..00000000 --- a/src/ProChat/store/slices/chat/selectors/utils.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { LobeChatPluginMeta } from '@lobehub/chat-plugin-sdk'; - -import { ChatMessage } from '@/types/chatMessage'; -import { MetaData } from '@/types/meta'; -import { LobeAgentSession } from '@/types/session'; - -interface OrganizeParams { - meta?: { - assistant?: MetaData; - user?: MetaData; - }; - pluginList?: LobeChatPluginMeta[]; - topicId?: string; -} - -export const organizeChats = ( - session: LobeAgentSession, - { topicId, meta, pluginList }: OrganizeParams = {}, -) => { - const getMeta = (message: ChatMessage) => { - switch (message.role) { - case 'user': { - return { - avatar: meta?.user?.avatar, - }; - } - - case 'system': { - return message.meta; - } - - case 'assistant': { - return { - avatar: meta?.assistant?.avatar, - backgroundColor: meta?.assistant?.backgroundColor, - title: meta?.assistant?.title || session.meta.title, - }; - } - - case 'function': { - const plugin = (pluginList || []).find((m) => m.identifier === message.name); - - return { - avatar: '🧩', - title: plugin?.identifier || 'plugin-unknown', - }; - } - } - }; - - const basic = Object.values(session.chats) - // 首先按照时间顺序排序,越早的在越前面 - .sort((pre, next) => pre.createAt - next.createAt) - .filter((m) => { - // 过滤掉包含 topicId 的消息,有主题的消息不应该出现在聊天框中 - if (!topicId) return !m.topicId; - - // 或者当话题 id 一致时,再展示话题 - return m.topicId === topicId; - }) - // 映射头像关系 - .map((m) => { - return { - ...m, - meta: getMeta(m), - }; - }); - - const finalList: ChatMessage[] = []; - - const addItem = (item: ChatMessage) => { - const isExist = finalList.findIndex((i) => item.id === i.id) > -1; - if (!isExist) { - finalList.push(item); - } - }; - - // 基于添加逻辑进行重排序 - for (const item of basic) { - // 先判存在与否,不存在就加入 - addItem(item); - - for (const another of basic) { - if (another.parentId === item.id) { - addItem(another); - } - } - } - - return finalList; -}; diff --git a/src/ProChat/store/slices/chat/utils.ts b/src/ProChat/store/slices/chat/utils.ts deleted file mode 100644 index c37367d1..00000000 --- a/src/ProChat/store/slices/chat/utils.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ChatMessage } from '@/types/chatMessage'; -import { LobeAgentConfig } from '@/types/session'; - -export const getSlicedMessagesWithConfig = ( - messages: ChatMessage[], - config: LobeAgentConfig, -): ChatMessage[] => { - // 如果没有开启历史消息数限制,或者限制为 0,则直接返回 - if (!config.enableHistoryCount || !config.historyCount) return messages; - - // 如果开启了,则返回尾部的N条消息 - return messages.reverse().slice(0, config.historyCount).reverse(); -}; diff --git a/src/ProChat/store/slices/session/action.ts b/src/ProChat/store/slices/session/action.ts deleted file mode 100644 index 0e876418..00000000 --- a/src/ProChat/store/slices/session/action.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { produce } from 'immer'; -import { merge } from 'lodash-es'; -import { DeepPartial } from 'utility-types'; -import { StateCreator } from 'zustand/vanilla'; - -import { SessionStore } from '@/ProChat/store'; -import { INBOX_SESSION_ID } from '@/const/session'; -import { SESSION_CHAT_URL } from '@/const/url'; -import { useGlobalStore } from '@/store/global'; -import { LobeAgentSession, LobeAgentSettings, LobeSessions } from '@/types/session'; -import { setNamespace } from '@/utils/storeDebug'; -import { uuid } from '@/utils/uuid'; - -import { initInbox, initLobeSession } from './initialState'; -import { SessionDispatch, sessionsReducer } from './reducers/session'; - -const t = setNamespace('session'); - -export interface SessionAction { - activeSession: (sessionId: string) => void; - clearSessions: () => void; - /** - * @title 添加会话 - * @param session - 会话信息 - * @returns void - */ - createSession: (agent?: DeepPartial) => Promise; - /** - * 变更session - * @param payload - 聊天记录 - */ - dispatchSession: (payload: SessionDispatch) => void; - importInbox: (inbox: LobeAgentSession) => void; - /** - * 导入会话 - * @param sessions - */ - importSessions: (sessions: LobeSessions) => void; - - /** - * 置顶会话 - * @param sessionId - */ - pinSession: (sessionId: string, pinned?: boolean) => void; - - /** - * 生成压缩后的消息 - * @returns 压缩后的消息 - */ - // genShareUrl: () => string; - /** - * @title 删除会话 - * @param index - 会话索引 - * @returns void - */ - removeSession: (sessionId: string) => void; - - switchBackToChat: () => void; - - /** - * @title 切换会话 - * @param sessionId - 会话索引 - * @returns void - */ - switchSession: (sessionId?: string) => void; -} - -export const createSessionSlice: StateCreator< - SessionStore, - [['zustand/devtools', never]], - [], - SessionAction -> = (set, get) => ({ - activeSession: (sessionId) => { - set({ activeId: sessionId, activeTopicId: undefined }, false, t('activeSession')); - }, - - clearSessions: () => { - set({ inbox: initInbox, sessions: {} }, false, t('clearSessions')); - }, - - createSession: async (agent) => { - const { dispatchSession, switchSession } = get(); - - const timestamp = Date.now(); - - // 合并 settings 里的 defaultAgent - const globalDefaultAgent = useGlobalStore.getState().settings.defaultAgent; - const newSession: LobeAgentSession = merge({}, initLobeSession, globalDefaultAgent, { - ...agent, - createAt: timestamp, - id: uuid(), - updateAt: timestamp, - }); - - dispatchSession({ session: newSession, type: 'addSession' }); - - switchSession(newSession.id); - - return newSession.id; - }, - - dispatchSession: (payload) => { - const { type, ...res } = payload; - - // 如果是 inbox 类型的 session - if ('id' in res && res.id === INBOX_SESSION_ID) { - const nextInbox = sessionsReducer({ inbox: get().inbox }, payload) as { - inbox: LobeAgentSession; - }; - set({ inbox: nextInbox.inbox }, false, t(`dispatchInbox/${type}`, res)); - } else { - // 常规类型的session - set( - { sessions: sessionsReducer(get().sessions, payload) }, - false, - t(`dispatchSessions/${type}`, res), - ); - } - }, - // TODO:暂时先不实现导入 inbox 的功能 - importInbox: () => {}, - importSessions: (importSessions) => { - const { sessions } = get(); - set( - { - sessions: produce(sessions, (draft) => { - for (const [id, session] of Object.entries(importSessions)) { - // 如果已经存在,则跳过 - if (draft[id]) continue; - - draft[id] = session; - } - }), - }, - false, - t('importSessions', importSessions), - ); - }, - - pinSession: (sessionId, pinned) => { - const nextValue = typeof pinned === 'boolean' ? pinned : !get().sessions[sessionId].pinned; - - get().dispatchSession({ id: sessionId, pinned: nextValue, type: 'toggleSessionPinned' }); - }, - - removeSession: (sessionId) => { - get().dispatchSession({ id: sessionId, type: 'removeSession' }); - - if (sessionId === get().activeId) { - get().switchSession(); - } - }, - - switchBackToChat: () => { - const { activeId, router } = get(); - - const id = activeId || INBOX_SESSION_ID; - - get().activeSession(id); - - router?.push(SESSION_CHAT_URL(id, get().isMobile)); - }, - switchSession: (sessionId = INBOX_SESSION_ID) => { - const { isMobile, router } = get(); - // mobile also should switch session due to chat mobile route is different - // fix https://github.com/lobehub/lobe-chat/issues/163 - if (!isMobile && get().activeId === sessionId) return; - - get().activeSession(sessionId); - - router?.push(SESSION_CHAT_URL(sessionId, isMobile)); - }, -}); diff --git a/src/ProChat/store/slices/session/initialState.ts b/src/ProChat/store/slices/session/initialState.ts deleted file mode 100644 index 612dc3fb..00000000 --- a/src/ProChat/store/slices/session/initialState.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { merge } from 'lodash-es'; - -import { DEFAULT_AGENT_META, DEFAULT_INBOX_AVATAR } from '@/ProChat/const/meta'; -import { LobeAgentConfig, LobeAgentSession, LobeSessionType } from '@/ProChat/types/session'; - -import { initialLobeAgentConfig } from '../agentConfig/initialState'; - -export interface SessionState { - /** - * @title 当前活动的会话 - * @description 当前正在编辑或查看的会话 - */ - activeId: string | undefined; - // 默认会话 - inbox: LobeAgentSession; - isMobile?: boolean; - searchKeywords: string; - sessions: Record; - topicSearchKeywords: string; -} - -export const initLobeSession: LobeAgentSession = { - chats: {}, - config: initialLobeAgentConfig, - createAt: Date.now(), - id: '', - meta: DEFAULT_AGENT_META, - type: LobeSessionType.Agent, - updateAt: Date.now(), -}; - -export const initInbox = merge({}, initLobeSession, { - config: { - systemRole: '', - } as LobeAgentConfig, - id: 'inbox', - meta: { - avatar: DEFAULT_INBOX_AVATAR, - }, -} as Partial); - -export const initialSessionState: SessionState = { - activeId: 'inbox', - inbox: initInbox, - isMobile: false, - searchKeywords: '', - sessions: {}, - topicSearchKeywords: '', -}; diff --git a/src/ProChat/store/slices/session/reducers/session.test.ts b/src/ProChat/store/slices/session/reducers/session.test.ts deleted file mode 100644 index 1cc5ccf7..00000000 --- a/src/ProChat/store/slices/session/reducers/session.test.ts +++ /dev/null @@ -1,456 +0,0 @@ -import { produce } from 'immer'; - -import { ChatMessage } from '@/types/chatMessage'; -import { LanguageModel } from '@/types/llm'; -import { MetaData } from '@/types/meta'; -import { LobeAgentConfig, LobeAgentSession, LobeSessionType, LobeSessions } from '@/types/session'; - -import { SessionDispatch, sessionsReducer } from './session'; - -describe('sessionsReducer', () => { - describe('addSession', () => { - it('should add session to state when valid session and type are provided', () => { - const state: LobeSessions = {}; - const session = { - id: 'session-id', - config: { - model: LanguageModel.GPT3_5, - params: {}, - systemRole: 'system-role', - }, - type: LobeSessionType.Agent, - } as LobeAgentSession; - const payload: SessionDispatch = { - type: 'addSession', - session, - }; - - const newState = sessionsReducer(state, payload); - - expect(newState).toEqual({ - 'session-id': session, - }); - }); - - it('should not change state when invalid session and type are provided', () => { - const state: LobeSessions = {}; - const session = undefined as unknown as LobeAgentSession; - const payload: SessionDispatch = { - type: 'addSession', - session, - }; - - const newState = sessionsReducer(state, payload); - - expect(newState).toEqual(state); - }); - }); - - describe('removeSession', () => { - it('should remove session from state when valid id and type are provided', () => { - const state = { - 'session-id': { - id: 'session-id', - config: { - model: 'gpt-3.5-turbo', - params: {}, - systemRole: 'system-role', - }, - type: 'agent', - } as LobeAgentSession, - } as LobeSessions; - const id = 'session-id'; - const payload: SessionDispatch = { - type: 'removeSession', - id, - }; - - const newState = sessionsReducer(state, payload); - - expect(newState).toEqual({}); - }); - - it('should not change state when invalid id and type are provided', () => { - const state: LobeSessions = { - 'session-id': { - id: 'session-id', - config: { - model: 'gpt-3.5-turbo', - params: {}, - systemRole: 'system-role', - }, - type: 'agent', - } as LobeAgentSession, - }; - const id = 'non-existent-id'; - const payload: SessionDispatch = { - type: 'removeSession', - id, - }; - - const newState = sessionsReducer(state, payload); - - expect(newState).toEqual(state); - }); - }); - - describe('updateSessionMeta', () => { - it('should update session meta when valid id, key, and value are provided', () => { - const state: LobeSessions = { - 'session-id': { - id: 'session-id', - config: { - model: 'gpt-3.5-turbo', - params: {}, - systemRole: 'system-role', - }, - type: 'agent', - meta: { - avatar: 'avatar-url', - backgroundColor: 'background-color', - description: 'description', - tags: ['tag1', 'tag2'], - title: 'title', - }, - } as LobeAgentSession, - }; - const id = 'session-id'; - const key: keyof MetaData = 'title'; - const value = 'new-title'; - const payload: SessionDispatch = { - type: 'updateSessionMeta', - id, - key, - value, - }; - - const newState = sessionsReducer(state, payload); - - expect(newState).toEqual({ - 'session-id': { - ...state['session-id'], - meta: { - ...state['session-id'].meta, - title: 'new-title', - }, - }, - }); - }); - - it('should not change state when invalid id, key, and value are provided', () => { - const state: LobeSessions = { - 'session-id': { - id: 'session-id', - config: { - model: 'gpt-3.5-turbo', - params: {}, - systemRole: 'system-role', - }, - type: 'agent', - meta: { - avatar: 'avatar-url', - backgroundColor: 'background-color', - description: 'description', - tags: ['tag1', 'tag2'], - title: 'title', - }, - } as LobeAgentSession, - }; - const id = 'non-existent-id'; - const key = 'invalid-key' as keyof MetaData; - const value = 'new-value'; - const payload: SessionDispatch = { - type: 'updateSessionMeta', - id, - key, - value, - }; - - const newState = sessionsReducer(state, payload); - - expect(newState).toEqual(state); - }); - - it('should not change state when valid id, invalid key, and value are provided', () => { - const state: LobeSessions = { - 'session-id': { - id: 'session-id', - config: { - model: 'gpt-3.5-turbo', - params: {}, - systemRole: 'system-role', - }, - type: 'agent', - meta: { - avatar: 'avatar-url', - backgroundColor: 'background-color', - description: 'description', - tags: ['tag1', 'tag2'], - title: 'title', - }, - } as LobeAgentSession, - }; - const id = 'session-id'; - const key = 'invalid-key' as keyof MetaData; - const value = 'new-value'; - const payload: SessionDispatch = { - type: 'updateSessionMeta', - id, - key, - value, - }; - - const newState = sessionsReducer(state, payload); - - expect(newState).toEqual(state); - }); - }); - - describe('updateSessionConfig', () => { - it('should update session config when valid id and partial config are provided', () => { - const state: LobeSessions = { - 'session-id': { - id: 'session-id', - config: { - model: 'gpt-3.5-turbo', - params: {}, - systemRole: 'system-role', - }, - type: 'agent', - meta: { - avatar: 'avatar-url', - backgroundColor: 'background-color', - description: 'description', - tags: ['tag1', 'tag2'], - title: 'title', - }, - } as LobeAgentSession, - }; - const id = 'session-id'; - const config: Partial = { - model: LanguageModel.GPT4, - }; - const payload: SessionDispatch = { - type: 'updateSessionConfig', - id, - config, - }; - - const newState = sessionsReducer(state, payload); - - expect(newState).toEqual({ - 'session-id': { - ...state['session-id'], - config: { - ...state['session-id'].config, - model: 'gpt-4', - }, - }, - }); - }); - - it('should update session agent config correctly', () => { - const state: LobeSessions = { - session1: { - id: 'session1', - config: { - model: 'gpt-3.5-turbo', - params: {}, - systemRole: 'system', - }, - } as LobeAgentSession, - session2: { - id: 'session2', - config: { - model: 'gpt-3.5-turbo', - params: {}, - systemRole: 'system', - }, - } as LobeAgentSession, - }; - - const payload: SessionDispatch = { - type: 'updateSessionConfig', - id: 'session1', - config: { - model: LanguageModel.GPT4, - params: {}, - }, - }; - - const expectedState = produce(state, (draft) => { - draft.session1.config = { - model: LanguageModel.GPT4, - params: {}, - systemRole: 'system', - }; - }); - - const newState = sessionsReducer(state, payload); - - expect(newState).toEqual(expectedState); - }); - - it('should not change state when invalid session ID is provided for updating session agent config', () => { - const state: LobeSessions = { - session1: { - id: 'session1', - config: { - model: 'gpt-3.5-turbo', - params: {}, - systemRole: 'system', - }, - } as LobeAgentSession, - }; - - const payload: SessionDispatch = { - type: 'updateSessionConfig', - id: 'session2', - config: { - model: LanguageModel.GPT4, - params: {}, - }, - }; - - const newState = sessionsReducer(state, payload); - - expect(newState).toEqual(state); - }); - - it.skip('should not change state when invalid session agent config is provided for updating session agent config', () => { - const state: LobeSessions = { - session1: { - id: 'session1', - config: { - model: 'gpt-3.5-turbo', - params: {}, - systemRole: 'system', - }, - } as LobeAgentSession, - }; - - const payload: SessionDispatch = { - type: 'updateSessionConfig', - id: 'session1', - config: { - model: LanguageModel.GPT4, - params: {}, - invalidKey: 'invalidValue', - } as unknown as LobeAgentConfig, - }; - - const newState = sessionsReducer(state, payload); - - expect(newState).toEqual(state); - }); - }); - - test('should not change state when invalid operation type is provided', () => { - const state: LobeSessions = { - session1: { - id: 'session1', - config: { - model: 'gpt-3.5-turbo', - params: {}, - systemRole: 'system', - }, - } as LobeAgentSession, - }; - - const payload = { - type: 'invalidOperation', - } as unknown as SessionDispatch; - - const newState = sessionsReducer(state, payload); - - expect(newState).toEqual(state); - }); - - describe('updateSessionChat', () => { - it('should update session chat correctly', () => { - const state: LobeSessions = { - session1: { - id: 'session1', - chats: {}, - } as LobeAgentSession, - session2: { - id: 'session2', - chats: {}, - } as LobeAgentSession, - }; - - const payload: SessionDispatch = { - type: 'updateSessionChat', - id: 'session1', - chats: { - message1: { - id: 'message1', - content: 'Hello', - } as ChatMessage, - }, - }; - - const expectedState = produce(state, (draft) => { - draft.session1.chats = { - message1: { - id: 'message1', - content: 'Hello', - } as ChatMessage, - }; - }); - - const newState = sessionsReducer(state, payload); - - expect(newState).toEqual(expectedState); - }); - - it('should not change state when invalid session ID is provided for updating session chat', () => { - const state: LobeSessions = { - session1: { - id: 'session1', - chats: {}, - } as LobeAgentSession, - }; - - const payload: SessionDispatch = { - type: 'updateSessionChat', - id: 'session2', - chats: { - message1: { - id: 'message1', - content: 'Hello', - } as ChatMessage, - }, - }; - - const newState = sessionsReducer(state, payload); - - expect(newState).toEqual(state); - }); - - it.skip('should not change state when invalid chat data is provided for updating session chat', () => { - const state: LobeSessions = { - session1: { - id: 'session1', - chats: {}, - } as LobeAgentSession, - }; - - const payload: SessionDispatch = { - type: 'updateSessionChat', - id: 'session1', - chats: { - message1: { - id: 'message1', - content: 'Hello', - invalidKey: 'invalidValue', - } as unknown as ChatMessage, - }, - }; - - const newState = sessionsReducer(state, payload); - - expect(newState).toEqual(state); - }); - }); -}); diff --git a/src/ProChat/store/slices/session/reducers/session.ts b/src/ProChat/store/slices/session/reducers/session.ts deleted file mode 100644 index a1483b3a..00000000 --- a/src/ProChat/store/slices/session/reducers/session.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { produce } from 'immer'; - -import { ChatMessageMap } from '@/types/chatMessage'; -import { MetaData } from '@/types/meta'; -import { LobeAgentConfig, LobeAgentSession, LobeSessions } from '@/types/session'; -import { ChatTopicMap } from '@/types/topic'; - -/** - * @title 添加会话 - */ -interface AddSession { - /** - * @param session - 会话信息 - */ - session: LobeAgentSession; - /** - * @param type - 操作类型 - * @default 'addChat' - */ - type: 'addSession'; -} - -interface RemoveSession { - id: string; - type: 'removeSession'; -} - -/** - * @title 更新会话聊天上下文 - */ -interface UpdateSessionChat { - chats: ChatMessageMap; - /** - * 会话 ID - */ - id: string; - - type: 'updateSessionChat'; -} - -/** - * @title 更新会话聊天上下文 - */ -interface UpdateSessionTopic { - /** - * 会话 ID - */ - id: string; - topics: ChatTopicMap; - - type: 'updateSessionTopic'; -} - -interface UpdateSessionMeta { - id: string; - key: keyof MetaData; - type: 'updateSessionMeta'; - value: any; -} - -interface UpdateSessionAgentConfig { - config: Partial; - id: string; - type: 'updateSessionConfig'; -} -interface ToggleSessionPinned { - id: string; - pinned: boolean; - type: 'toggleSessionPinned'; -} - -export type SessionDispatch = - | AddSession - | UpdateSessionChat - | RemoveSession - | UpdateSessionMeta - | UpdateSessionAgentConfig - | UpdateSessionTopic - | ToggleSessionPinned; - -export const sessionsReducer = (state: LobeSessions, payload: SessionDispatch): LobeSessions => { - switch (payload.type) { - case 'addSession': { - return produce(state, (draft) => { - const { session } = payload; - if (!session) return; - - draft[session.id] = session; - }); - } - - case 'removeSession': { - return produce(state, (draft) => { - delete draft[payload.id]; - }); - } - - case 'toggleSessionPinned': { - return produce(state, (draft) => { - const { pinned, id } = payload; - const session = draft[id]; - if (!session) return; - - session.pinned = pinned; - }); - } - - case 'updateSessionMeta': { - return produce(state, (draft) => { - const chat = draft[payload.id]; - if (!chat) return; - - const { key, value } = payload; - - const validKeys = ['avatar', 'backgroundColor', 'description', 'tags', 'title']; - - if (validKeys.includes(key)) chat.meta[key] = value; - }); - } - - case 'updateSessionChat': { - return produce(state, (draft) => { - const chat = draft[payload.id]; - if (!chat) return; - - chat.chats = payload.chats; - }); - } - - case 'updateSessionTopic': { - return produce(state, (draft) => { - const chat = draft[payload.id]; - if (!chat) return; - - chat.topics = payload.topics; - }); - } - - case 'updateSessionConfig': { - return produce(state, (draft) => { - const { id, config } = payload; - const chat = draft[id]; - if (!chat) return; - - chat.config = { - ...chat.config, - ...config, - params: { ...chat.config.params, ...config.params }, - }; - }); - } - - default: { - return produce(state, () => {}); - } - } -}; diff --git a/src/ProChat/store/slices/session/selectors/export.ts b/src/ProChat/store/slices/session/selectors/export.ts deleted file mode 100644 index cef451f4..00000000 --- a/src/ProChat/store/slices/session/selectors/export.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { transform } from 'lodash-es'; - -import { SessionStore } from '@/ProChat/store'; -import { ConfigStateAgents, ConfigStateSessions } from '@/types/exportConfig'; -import { LobeAgentSession, LobeSessions } from '@/types/session'; - -import { getSessionById } from './list'; - -export const exportSessions = (s: SessionStore): ConfigStateSessions => ({ - inbox: s.inbox, - sessions: s.sessions, -}); - -export const exportAgents = (s: SessionStore): ConfigStateAgents => { - return { - sessions: transform(s.sessions, (result: LobeSessions, value, key) => { - // 移除 chats 和 topics - result[key] = { ...value, chats: {}, topics: {} } as LobeAgentSession; - }), - }; -}; - -export const getExportAgent = - (id: string) => - (s: SessionStore): LobeAgentSession => { - const session = getSessionById(id)(s); - return { ...session, chats: {}, topics: {} }; - }; diff --git a/src/ProChat/store/slices/session/selectors/index.ts b/src/ProChat/store/slices/session/selectors/index.ts deleted file mode 100644 index dc803d50..00000000 --- a/src/ProChat/store/slices/session/selectors/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { INBOX_SESSION_ID } from '@/const/session'; -import { SessionStore } from '@/ProChat/store'; - -import { exportAgents, exportSessions, getExportAgent } from './export'; -import { - currentSession, - currentSessionSafe, - getSessionById, - getSessionMetaById, - hasSessionList, - sessionList, -} from './list'; - -const isInboxSession = (s: SessionStore) => s.activeId === INBOX_SESSION_ID; - -export const sessionSelectors = { - currentSession, - currentSessionSafe, - exportAgents, - exportSessions, - getExportAgent, - getSessionById, - getSessionMetaById, - hasSessionList, - isInboxSession, - sessionList, -}; diff --git a/src/ProChat/store/slices/session/selectors/list.test.ts b/src/ProChat/store/slices/session/selectors/list.test.ts deleted file mode 100644 index a16db895..00000000 --- a/src/ProChat/store/slices/session/selectors/list.test.ts +++ /dev/null @@ -1,303 +0,0 @@ -import type { SessionStore } from '@/ProChat/store'; -import { LanguageModel } from '@/types/llm'; -import { LobeAgentSession, LobeSessionType } from '@/types/session'; - -import { initLobeSession } from '../initialState'; -import { - currentSession, - currentSessionSafe, - getSessionById, - getSessionMetaById, - hasSessionList, - sessionList, -} from './list'; - -describe('currentSession', () => { - const s = { - activeId: '1', - sessions: { - '1': { - id: '1', - config: { - model: LanguageModel.GPT3_5, - params: {}, - systemRole: 'system-role', - }, - type: LobeSessionType.Agent, - } as LobeAgentSession, - '2': { - id: '2', - config: { - model: LanguageModel.GPT3_5, - params: {}, - systemRole: 'system-role', - }, - type: LobeSessionType.Agent, - } as LobeAgentSession, - }, - inbox: { - id: 'inbox', - config: { - model: LanguageModel.GPT3_5, - params: {}, - systemRole: 'system-role', - }, - type: LobeSessionType.Agent, - } as LobeAgentSession, - } as unknown as SessionStore; - - it('should return undefined when s.activeId is not defined', () => { - expect(currentSession({ sessions: {} } as any)).toBeUndefined(); - }); - - it('should return s.inbox when s.activeId is equal to INBOX_SESSION_ID', () => { - expect(currentSession({ ...s, activeId: 'inbox' })).toEqual(s.inbox); - }); - - it('should return s.sessions[s.activeId] when s.activeId is not equal to INBOX_SESSION_ID', () => { - expect(currentSession(s)).toEqual(s.sessions['1']); - }); -}); - -describe('currentSessionSafe', () => { - const s = { - activeId: '1', - sessions: { - '1': { - id: '1', - config: { - model: LanguageModel.GPT3_5, - params: {}, - systemRole: 'system-role', - }, - type: LobeSessionType.Agent, - } as LobeAgentSession, - '2': { - id: '2', - config: { - model: LanguageModel.GPT3_5, - params: {}, - systemRole: 'system-role', - }, - type: LobeSessionType.Agent, - } as LobeAgentSession, - }, - inbox: { - id: 'inbox', - config: { - model: LanguageModel.GPT3_5, - params: {}, - systemRole: 'system-role', - }, - type: LobeSessionType.Agent, - } as LobeAgentSession, - } as unknown as SessionStore; - - it('should return initLobeSession when currentSession(s) returns undefined', () => { - expect(currentSessionSafe({ sessions: {} } as any)).toEqual(initLobeSession); - }); - - it('should return the result of currentSession(s) when it returns a non-undefined value', () => { - expect(currentSessionSafe(s)).toEqual(s.sessions['1']); - }); -}); - -describe('sessionList', () => { - const s = { - sessions: { - '1': { - pinned: true, - updateAt: 100, - }, - '2': { - pinned: false, - updateAt: 200, - }, - '3': { - pinned: true, - updateAt: 300, - }, - }, - } as unknown as SessionStore; - - it('should return an array of sessions sorted by pinned and updateAt fields', () => { - expect(sessionList(s)).toEqual([s.sessions['3'], s.sessions['1'], s.sessions['2']]); - }); - - it('should return an empty array when s.sessions is an empty object', () => { - const result = sessionList({ - sessions: {}, - } as any); - expect(result).toEqual([]); - }); - - it('should return a sorted session list when s.sessions is not empty object', () => { - const session1 = { - id: '1', - pinned: true, - updateAt: 1635705600000, - meta: {}, - }; - const session2 = { - id: '2', - pinned: false, - updateAt: 1635705600000, - meta: {}, - }; - const session3 = { - id: '3', - pinned: true, - updateAt: 1635705700000, - meta: {}, - }; - const s = { - sessions: { - '1': session1, - '2': session2, - '3': session3, - }, - } as unknown as SessionStore; - const result = sessionList(s); - expect(result).toEqual([session3, session1, session2]); - }); - - it('should return a sorted session list when s.searchKeywords is not an empty string', () => { - const session1 = { - id: '1', - pinned: true, - meta: { title: 'yword' }, - - updateAt: 1635705600000, - }; - const session2 = { - id: '2', - pinned: false, - meta: { title: 'keyword' }, - updateAt: 1635705600000, - }; - const session3 = { - id: '3', - pinned: true, - meta: { title: 'kyword' }, - updateAt: 1635705800000, - }; - const s = { - sessions: { - '1': session1, - '2': session2, - '3': session3, - }, - searchKeywords: 'k', - } as unknown as SessionStore; - - const result = sessionList(s); - expect(result).toEqual([session3, session2]); - }); - - it('should return an empty array when s.sessions is an empty object and s.searchKeywords is not an empty string', () => { - const result = sessionList({ - sessions: {}, - searchKeywords: 'keyword', - } as unknown as SessionStore); - expect(result).toEqual([]); - }); -}); - -describe('hasSessionList', () => { - it('should return true when sessionList(s) returns a non-empty array', () => { - const s = { - sessions: { - '1': {}, - }, - } as unknown as SessionStore; - expect(hasSessionList(s)).toBe(true); - }); - - it('should return false when sessionList(s) returns an empty array', () => { - const s = { - sessions: {}, - } as unknown as SessionStore; - expect(hasSessionList(s)).toBe(false); - }); -}); - -describe('getSessionById', () => { - const s = { - activeId: '1', - sessions: { - '1': { - id: '1', - config: { - model: LanguageModel.GPT3_5, - params: {}, - systemRole: 'system-role', - }, - type: LobeSessionType.Agent, - } as LobeAgentSession, - '2': { - id: '2', - config: { - model: LanguageModel.GPT3_5, - params: {}, - systemRole: 'system-role', - }, - type: LobeSessionType.Agent, - } as LobeAgentSession, - }, - inbox: { - id: 'inbox', - config: { - model: LanguageModel.GPT3_5, - params: {}, - systemRole: 'system-role', - }, - type: LobeSessionType.Agent, - } as LobeAgentSession, - } as unknown as SessionStore; - - it('should return s.inbox when id is equal to INBOX_SESSION_ID', () => { - expect(getSessionById('inbox')(s)).toEqual(s.inbox); - }); - - it('should return the session with the specified id when id is not equal to INBOX_SESSION_ID', () => { - expect(getSessionById('1')(s)).toEqual(s.sessions['1']); - }); - - it('should return initLobeSession when the session with the specified id does not exist', () => { - expect(getSessionById('3')(s)).toEqual(initLobeSession); - }); -}); - -describe('getSessionMetaById', () => { - const s: SessionStore = { - sessions: { - '1': { - meta: { - title: 'Session 1', - }, - }, - '2': { - meta: { - title: 'Session 2', - }, - }, - }, - inbox: { - meta: { - title: 'Inbox', - }, - }, - } as unknown as SessionStore; - - it('should return the meta data of the session with the specified id', () => { - expect(getSessionMetaById('1')(s)).toEqual({ title: 'Session 1' }); - }); - - it('should return the meta data of the inbox session when id is equal to INBOX_SESSION_ID', () => { - expect(getSessionMetaById('inbox')(s)).toEqual({ title: 'Inbox' }); - }); - - it('should return an empty object when the session with the specified id does not exist', () => { - expect(getSessionMetaById('3')(s)).toEqual({}); - }); -}); diff --git a/src/ProChat/store/slices/session/selectors/list.ts b/src/ProChat/store/slices/session/selectors/list.ts deleted file mode 100644 index a28ecb82..00000000 --- a/src/ProChat/store/slices/session/selectors/list.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { INBOX_SESSION_ID } from '@/const/session'; -import { MetaData } from '@/types/meta'; -import { LobeAgentSession } from '@/types/session'; -import { filterWithKeywords } from '@/utils/filter'; - -import { SessionStore } from '../../../store'; -import { initLobeSession } from '../initialState'; - -export const currentSession = (s: SessionStore): LobeAgentSession | undefined => { - if (!s.activeId) return; - - if (s.activeId === INBOX_SESSION_ID) return s.inbox; - - return s.sessions[s.activeId]; -}; - -export const currentSessionSafe = (s: SessionStore): LobeAgentSession => { - return currentSession(s) || initLobeSession; -}; - -export const sessionList = (s: SessionStore) => { - const filterChats = filterWithKeywords(s.sessions, s.searchKeywords, (item) => [ - Object.values(item.chats || {}) - .map((c) => c.content) - .join(''), - ]); - // 先按照 `pinned` 字段进行排序,置顶的会排在前面。然后,根据 `updateAt` 字段进行倒序排序。 - return Object.values(filterChats).sort((a, b) => { - if (a.pinned && !b.pinned) { - return -1; - } - if (!a.pinned && b.pinned) { - return 1; - } - if (a.updateAt && b.updateAt) { - return b.updateAt - a.updateAt; - } - return 0; - }); -}; - -export const hasSessionList = (s: SessionStore) => { - const list = sessionList(s); - return list?.length > 0; -}; - -export const getSessionById = - (id: string) => - (s: SessionStore): LobeAgentSession => { - if (id === INBOX_SESSION_ID) return s.inbox; - - const session = s.sessions[id]; - - if (!session) return initLobeSession; - return session; - }; - -export const getSessionMetaById = - (id: string) => - (s: SessionStore): MetaData => { - const session = getSessionById(id)(s); - - if (!session) return {}; - return session.meta; - }; - -// export const sessionTreeSel = (s: SessionStore) => { -// const sessionTree: SessionTree = [ -// { -// agentId: 'default', -// chats: chatListSel(s) -// .filter((s) => !s.agentId) -// .map((c) => c.id), -// }, -// ]; -// -// Object.values(s.agents).forEach((agent) => { -// const chats = Object.values(s.chats).filter((s) => s.agentId === agent.id); -// -// sessionTree.push({ -// agentId: agent.id, -// chats: chats.map((c) => c.id), -// }); -// }); -// -// return sessionTree; -// }; diff --git a/src/ProChat/store/store.ts b/src/ProChat/store/store.ts index eebdd4b9..e0a10d87 100644 --- a/src/ProChat/store/store.ts +++ b/src/ProChat/store/store.ts @@ -1,53 +1,64 @@ -import { PersistOptions, devtools, persist } from 'zustand/middleware'; -import { shallow } from 'zustand/shallow'; -import { createWithEqualityFn } from 'zustand/traditional'; import { StateCreator } from 'zustand/vanilla'; -import { SessionStoreState, initialState } from './initialState'; -import { AgentAction, createAgentSlice } from './slices/agentConfig/action'; -import { ChatAction, createChatSlice } from './slices/chat/action'; -import { SessionAction, createSessionSlice } from './slices/session/action'; +import { MetaData } from '@/ProChat/types/meta'; +import isEqual from 'fast-deep-equal'; +import { merge } from 'lodash-es'; +import { optionalDevtools } from 'zustand-utils'; +import { DevtoolsOptions } from 'zustand/middleware'; +import { createWithEqualityFn } from 'zustand/traditional'; +import { ChatAction, chatAction } from './action'; +import { ChatPropsState, ChatState, initialState } from './initialState'; -const isDev = process.env.NODE_ENV === 'development'; +export interface ChatProps extends Partial { + // init + loading?: boolean; + initialChats?: ChatPropsState['chats']; + userMeta?: MetaData; + assistantMeta?: MetaData; +} // =============== 聚合 createStoreFn ============ // -export type SessionStore = SessionAction & AgentAction & ChatAction & SessionStoreState; -const createStore: StateCreator = (...parameters) => ({ - ...initialState, - ...createAgentSlice(...parameters), - ...createSessionSlice(...parameters), - ...createChatSlice(...parameters), -}); +export type ChatStore = ChatAction & ChatState; + +const vanillaStore = + ({ + loading, + initialChats, + chats, + ...props + }: ChatProps): StateCreator => + (...parameters) => { + // initState = innerState + props + + const state = merge({}, initialState, { + init: !loading, + chats: chats ?? initialChats, + ...props, + } as ChatState); -// =============== persist 本地缓存中间件配置 ============ // + return { + ...state, + ...chatAction(...parameters), + }; + }; +// + +// =============== 封装 createStore ============ // const PRO_CHAT = 'PRO_CHAT'; -const persistOptions: PersistOptions = { - name: PRO_CHAT, +const isDev = process.env.NODE_ENV === 'development'; + +export const createStore = (props: ChatProps, options: boolean | DevtoolsOptions = false) => { + const devtools = optionalDevtools(options !== false); - // 手动控制 Hydration ,避免 ssr 报错 - skipHydration: true, + const devtoolOptions = + options === false + ? undefined + : options === true + ? { name: PRO_CHAT + (isDev ? '_DEV' : '') } + : options; - // storage: createHyperStorage({ - // localStorage: { - // dbName: 'LobeHub', - // mode: 'indexedDB', - // selectors: ['inbox', 'sessions'], - // }, - // }), - version: 0, + return createWithEqualityFn()(devtools(vanillaStore(props), devtoolOptions), isEqual); }; - -// =============== 实装 useStore ============ // - -export const useChatStore = createWithEqualityFn()( - persist( - devtools(createStore, { - name: PRO_CHAT + (isDev ? '_DEV' : ''), - }), - persistOptions, - ), - shallow, -); diff --git a/src/ProChat/types/chat.ts b/src/ProChat/types/chat.ts index 078bdcab..cc0fdfa3 100644 --- a/src/ProChat/types/chat.ts +++ b/src/ProChat/types/chat.ts @@ -14,7 +14,7 @@ export interface OpenAIChatMessage { * 角色 * @description 消息发送者的角色 */ - role: LLMRoleType; + role: LLMRoleType | string; } /** @@ -68,26 +68,3 @@ export interface ChatStreamPayload { */ top_p?: number; } - -export interface ChatCompletionFunctions { - /** - * The description of what the function does. - * @type {string} - * @memberof ChatCompletionFunctions - */ - description?: string; - /** - * The name of the function to be called. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 64. - * @type {string} - * @memberof ChatCompletionFunctions - */ - name: string; - /** - * The parameters the functions accepts, described as a JSON Schema object. See the [guide](/docs/guides/gpt/function-calling) for examples, and the [JSON Schema reference](https://json-schema.org/understanding-json-schema/) for documentation about the format. - * @type {{ [key: string]: any }} - * @memberof ChatCompletionFunctions - */ - parameters?: { - [key: string]: any; - }; -} diff --git a/src/ProChat/types/chatMessage.ts b/src/ProChat/types/chatMessage.ts deleted file mode 100644 index ac4b768c..00000000 --- a/src/ProChat/types/chatMessage.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { PluginRequestPayload } from '@lobehub/chat-plugin-sdk'; - -import { ErrorType } from '@/types/fetch'; - -import { LLMRoleType } from './llm'; -import { BaseDataModel } from './meta'; - -/** - * 聊天消息错误对象 - */ -export interface ChatMessageError { - body?: any; - message: string; - type: ErrorType; -} -export interface OpenAIFunctionCall { - arguments?: string; - name: string; -} - -export interface ChatMessage extends BaseDataModel { - /** - * @title 内容 - * @description 消息内容 - */ - content: string; - error?: any; - // 扩展字段 - extra?: { - fromModel?: string; - // 翻译 - translate?: ChatTranslate; - } & Record; - - /** - * replace with plugin - * @deprecated - */ - function_call?: OpenAIFunctionCall; - name?: string; - - parentId?: string; - - plugin?: PluginRequestPayload; - - // 引用 - quotaId?: string; - /** - * 角色 - * @description 消息发送者的角色 - */ - role: LLMRoleType; - /** - * 保存到主题的消息 - */ - topicId?: string; -} - -export type ChatMessageMap = Record; diff --git a/src/ProChat/types/config.ts b/src/ProChat/types/config.ts new file mode 100644 index 00000000..6563c525 --- /dev/null +++ b/src/ProChat/types/config.ts @@ -0,0 +1,70 @@ +// 语言模型的设置参数 +export interface ModelParams { + /** + * 控制生成文本中的惩罚系数,用于减少重复性 + * @default 0 + */ + frequency_penalty?: number; + /** + * 生成文本的最大长度 + */ + max_tokens?: number; + /** + * 控制生成文本中的惩罚系数,用于减少主题的变化 + * @default 0 + */ + presence_penalty?: number; + /** + * 生成文本的随机度量,用于控制文本的创造性和多样性 + * @default 0.6 + */ + temperature?: number; + /** + * 控制生成文本中最高概率的单个 token + * @default 1 + */ + top_p?: number; +} + +export type ModelRoleType = 'user' | 'system' | 'assistant' | 'function'; + +export interface LLMMessage { + content: string; + role: ModelRoleType; +} + +export type LLMFewShots = LLMMessage[]; + +export interface ModelConfig { + compressThreshold?: number; + /** + * 历史消息长度压缩阈值 + */ + enableCompressThreshold?: boolean; + /** + * 开启历史记录条数 + */ + enableHistoryCount?: boolean; + enableMaxTokens?: boolean; + /** + * 语言模型示例 + */ + fewShots?: LLMFewShots; + /** + * 历史消息条数 + */ + historyCount?: number; + inputTemplate?: string; + /** + * 角色所使用的语言模型 + */ + model: string; + /** + * 语言模型参数 + */ + params: ModelParams; + /** + * 系统角色 + */ + systemRole: string; +} diff --git a/src/ProChat/types/llm.ts b/src/ProChat/types/llm.ts deleted file mode 100644 index 3a2d8628..00000000 --- a/src/ProChat/types/llm.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * LLM 模型 - */ -export enum LanguageModel { - /** - * GPT 3.5 Turbo - */ - GPT3_5 = 'gpt-3.5-turbo', - GPT3_5_16K = 'gpt-3.5-turbo-16k', - /** - * GPT 4 - */ - GPT4 = 'gpt-4', - GPT4_32K = 'gpt-4-32k', -} - -// 语言模型的设置参数 -export interface LLMParams { - /** - * 控制生成文本中的惩罚系数,用于减少重复性 - * @default 0 - */ - frequency_penalty?: number; - /** - * 生成文本的最大长度 - */ - max_tokens?: number; - /** - * 控制生成文本中的惩罚系数,用于减少主题的变化 - * @default 0 - */ - presence_penalty?: number; - /** - * 生成文本的随机度量,用于控制文本的创造性和多样性 - * @default 0.6 - */ - temperature?: number; - /** - * 控制生成文本中最高概率的单个 token - * @default 1 - */ - top_p?: number; -} - -export type LLMRoleType = 'user' | 'system' | 'assistant' | 'function'; - -export interface LLMMessage { - content: string; - role: LLMRoleType; -} - -export type LLMExample = LLMMessage[]; diff --git a/src/ProChat/types/message.ts b/src/ProChat/types/message.ts new file mode 100644 index 00000000..42c2869f --- /dev/null +++ b/src/ProChat/types/message.ts @@ -0,0 +1,33 @@ +import { ModelRoleType } from '@/ProChat/types/config'; + +/** + * 聊天消息错误对象 + */ +export interface ChatMessageError { + body?: any; + message: string; + type: string | number; +} + +export interface ChatMessage { + /** + * @title 内容 + * @description 消息内容 + */ + content: string; + error?: any; + model?: string; + name?: string; + parentId?: string; + /** + * 角色 + * @description 消息发送者的角色 + */ + role: ModelRoleType | string; + createAt: number; + id: string; + updateAt: number; + extra?: Record; +} + +export type ChatMessageMap = Record; diff --git a/src/ProChat/types/meta.ts b/src/ProChat/types/meta.ts index 0464ce37..065be8cb 100644 --- a/src/ProChat/types/meta.ts +++ b/src/ProChat/types/meta.ts @@ -9,18 +9,9 @@ export interface MetaData { * @description 可选参数,如果不传则使用默认背景色 */ backgroundColor?: string; - description?: string; - tags?: string[]; /** * 名称 * @description 可选参数,如果不传则使用默认名称 */ title?: string; } - -export interface BaseDataModel { - createAt: number; - id: string; - meta: MetaData; - updateAt: number; -} diff --git a/src/ProChat/types/session.ts b/src/ProChat/types/session.ts deleted file mode 100644 index 4526aa7d..00000000 --- a/src/ProChat/types/session.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { ChatMessageMap } from './chatMessage'; -import { LLMExample, LLMParams, LanguageModel } from './llm'; -import { BaseDataModel, MetaData } from './meta'; - -export enum LobeSessionType { - /** - * 角色 - */ - Agent = 'agent', - /** - * 群聊 - */ - Group = 'group', -} - -interface LobeSessionBase extends BaseDataModel { - /** - * 聊天记录 - */ - chats: ChatMessageMap; - /** - * 置顶 - */ - pinned?: boolean; - - /** - * 每个会话的类别 - */ - type: LobeSessionType; -} - -export interface LobeAgentConfig { - compressThreshold?: number; - displayMode?: 'chat' | 'docs'; - /** - * 历史消息长度压缩阈值 - */ - enableCompressThreshold?: boolean; - /** - * 开启历史记录条数 - */ - enableHistoryCount?: boolean; - enableMaxTokens?: boolean; - /** - * 语言模型示例 - */ - example?: LLMExample; - /** - * 历史消息条数 - */ - historyCount?: number; - inputTemplate?: string; - /** - * 角色所使用的语言模型 - * @default gpt-3.5-turbo - */ - model: LanguageModel | string; - /** - * 语言模型参数 - */ - params: LLMParams; - /** - * 启用的插件 - */ - plugins?: string[]; - /** - * 系统角色 - */ - systemRole: string; -} - -/** - * Lobe Agent会话 - */ -export interface LobeAgentSession extends LobeSessionBase { - /** - * 语言模型角色设定 - */ - config: LobeAgentConfig; - type: LobeSessionType.Agent; -} - -export interface LobeAgentSettings { - /** - * 语言模型角色设定 - */ - config: LobeAgentConfig; - meta: MetaData; -} -export type LobeSessions = Record; - -export type LobeAgentConfigKeys = - | keyof LobeAgentConfig - | ['params', keyof LobeAgentConfig['params']]; diff --git a/src/ProChat/utils/VersionController.test.ts b/src/ProChat/utils/VersionController.test.ts deleted file mode 100644 index b75d471b..00000000 --- a/src/ProChat/utils/VersionController.test.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { Migration, MigrationData, VersionController } from './VersionController'; - -class TestMigration0 implements Migration { - version = 0; - - migrate(data: MigrationData): MigrationData { - return data; - } -} -class TestMigration1 implements Migration { - version = 1; - - migrate(data: MigrationData): MigrationData { - return { - state: { - ...data.state, - value1: data.state.value * 2, - }, - version: this.version, - }; - } -} - -class TestMigration2 implements Migration { - version = 2; - - migrate(data: MigrationData): MigrationData { - return { - state: { - ...data.state, - value2: data.state.value1 * 2, - }, - version: this.version, - }; - } -} - -describe('VersionController', () => { - let migrations; - let versionController: VersionController; - - beforeEach(() => { - migrations = [TestMigration0, TestMigration1, TestMigration2]; - versionController = new VersionController(migrations); - }); - - it('should instantiate with sorted migrations', () => { - expect(versionController['migrations'][0].version).toBe(0); - expect(versionController['migrations'][1].version).toBe(1); - expect(versionController['migrations'][2].version).toBe(2); - }); - - it('should throw error if data version is undefined', () => { - const data = { - state: { value: 10 }, - }; - - expect(() => versionController.migrate(data as any)).toThrow( - '导入数据缺少版本号,请检查文件后重试', - ); - }); - - it('should migrate data correctly through multiple versions', () => { - const data: MigrationData = { - state: { value: 10 }, - version: 0, - }; - - const migratedData = versionController.migrate(data); - - expect(migratedData).toEqual({ - state: { value: 10, value1: 20, value2: 40 }, - version: 3, - }); - }); - - it('should migrate data correctly if starting from a specific version', () => { - const data: MigrationData = { - state: { value: 10, value1: 20 }, - version: 1, - }; - - const migratedData = versionController.migrate(data); - - expect(migratedData).toEqual({ - state: { value: 10, value1: 20, value2: 40 }, - version: 3, - }); - }); -}); diff --git a/src/ProChat/utils/VersionController.ts b/src/ProChat/utils/VersionController.ts deleted file mode 100644 index e03de38e..00000000 --- a/src/ProChat/utils/VersionController.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * 迁移接口 - * @template T - 状态类型 - */ -export interface Migration { - /** - * 迁移数据 - * @param data - 迁移数据 - * @returns 迁移后的数据 - */ - migrate(data: MigrationData): MigrationData; - /** - * 迁移版本号 - */ - version: number; -} - -/** - * 迁移数据接口 - * @template T - 状态类型 - */ -export interface MigrationData { - /** - * 状态数据 - */ - state: T; - /** - * 迁移版本号 - */ - version: number; -} -export class VersionController { - private migrations: Migration[]; - targetVersion: number; - - constructor(migrations: any[], targetVersion: number = migrations.length) { - this.migrations = migrations - .map((cls) => { - return new cls() as Migration; - }) - .sort((a, b) => a.version - b.version); - - this.targetVersion = targetVersion; - } - - migrate(data: MigrationData): MigrationData { - let nextData = data; - const targetVersion = this.targetVersion || this.migrations.length; - if (data.version === undefined) throw new Error('导入数据缺少版本号,请检查文件后重试'); - const currentVersion = data.version; - - for (let i = currentVersion || 0; i < targetVersion; i++) { - const migration = this.migrations.find((m) => m.version === i); - if (!migration) throw new Error('程序出错'); - - nextData = migration.migrate(nextData); - - nextData.version += 1; - console.debug('迁移器:', migration, '数据:', nextData, '迁移后版本:', nextData.version); - } - - return nextData; - } -} diff --git a/src/ProChat/utils/fetch.ts b/src/ProChat/utils/fetch.ts index 02121183..8ca61545 100644 --- a/src/ProChat/utils/fetch.ts +++ b/src/ProChat/utils/fetch.ts @@ -1,27 +1,12 @@ -import { t } from 'i18next'; - -import { fetchChatModel } from '@/services/chatModel'; import { ChatMessageError } from '@/types/chatMessage'; -import { ErrorResponse, ErrorType } from '@/types/fetch'; export const getMessageError = async (response: Response) => { let chatMessageError: ChatMessageError; - // 尝试取一波业务错误语义 - try { - const data = (await response.json()) as ErrorResponse; - chatMessageError = { - body: data.body, - message: t(`response.${data.errorType}`), - type: data.errorType, - }; - } catch { - // 如果无法正常返回,说明是常规报错 - chatMessageError = { - message: t(`response.${response.status}`), - type: response.status as ErrorType, - }; - } + chatMessageError = { + message: `response error, status: ${response.statusText}`, + type: response.status as any, + }; return chatMessageError; }; @@ -41,6 +26,7 @@ export const fetchSSE = async (fetchFn: () => Promise, options: FetchS // 如果不 ok 说明有请求错误 if (!response.ok) { + // TODO: need a message error custom parser const chatMessageError = await getMessageError(response); options.onErrorHandle?.(chatMessageError); @@ -68,59 +54,3 @@ export const fetchSSE = async (fetchFn: () => Promise, options: FetchS return returnRes; }; - -interface FetchAITaskResultParams { - abortController?: AbortController; - /** - * 错误处理函数 - */ - onError?: (e: Error, rawError?: any) => void; - /** - * 加载状态变化处理函数 - * @param loading - 是否处于加载状态 - */ - onLoadingChange?: (loading: boolean) => void; - /** - * 消息处理函数 - * @param text - 消息内容 - */ - onMessageHandle?: (text: string) => void; - - /** - * 请求对象 - */ - params: T; -} - -export const fetchAIFactory = - (fetcher: (params: T, options: { signal?: AbortSignal }) => Promise) => - async ({ - params, - onMessageHandle, - onError, - onLoadingChange, - abortController, - }: FetchAITaskResultParams) => { - const errorHandle = (error: Error, errorContent?: any) => { - onLoadingChange?.(false); - if (abortController?.signal.aborted) { - return; - } - onError?.(error, errorContent); - }; - - onLoadingChange?.(true); - - const data = await fetchSSE(() => fetcher(params, { signal: abortController?.signal }), { - onErrorHandle: (error) => { - errorHandle(new Error(error.message), error); - }, - onMessageHandle, - }).catch(errorHandle); - - onLoadingChange?.(false); - - return await data?.text(); - }; - -export const fetchPresetTaskResult = fetchAIFactory(fetchChatModel); diff --git a/src/ProChat/utils/message.ts b/src/ProChat/utils/message.ts index 32912756..6f1dcf89 100644 --- a/src/ProChat/utils/message.ts +++ b/src/ProChat/utils/message.ts @@ -1,5 +1,18 @@ import { FUNCTION_MESSAGE_FLAG } from '@/ProChat/const/message'; +import { ModelConfig } from '@/ProChat/types/config'; +import { ChatMessage } from '@/ProChat/types/message'; export const isFunctionMessage = (content: string) => { return content.startsWith(FUNCTION_MESSAGE_FLAG); }; + +export const getSlicedMessagesWithConfig = ( + messages: ChatMessage[], + config: ModelConfig, +): ChatMessage[] => { + // 如果没有开启历史消息数限制,或者限制为 0,则直接返回 + if (!config.enableHistoryCount || !config.historyCount) return messages; + + // 如果开启了,则返回尾部的N条消息 + return messages.reverse().slice(0, config.historyCount).reverse(); +}; diff --git a/src/ProChat/utils/parseMarkdown.ts b/src/ProChat/utils/parseMarkdown.ts deleted file mode 100644 index 54cf68a0..00000000 --- a/src/ProChat/utils/parseMarkdown.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { remark } from 'remark'; -import remarkGfm from 'remark-gfm'; -import remarkHtml from 'remark-html'; - -export const parseMarkdown = async (content: string) => { - const file = await remark().use(remarkGfm).use(remarkHtml).process(content.trim()); - - return String(file); -}; diff --git a/src/ProChat/utils/responsive.ts b/src/ProChat/utils/responsive.ts deleted file mode 100644 index 0434bd3b..00000000 --- a/src/ProChat/utils/responsive.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { headers } from 'next/headers'; -import { UAParser } from 'ua-parser-js'; - -/** - * check mobile device in server - */ -export const isMobileDevice = () => { - if (typeof process === 'undefined') { - throw new Error('[Server method] you are importing a server-only module outside of server'); - } - - const { get } = headers(); - const ua = get('user-agent'); - - // console.debug(ua); - const device = new UAParser(ua || '').getDevice(); - - return device.type === 'mobile'; -}; diff --git a/src/ProChat/utils/switchLang.ts b/src/ProChat/utils/switchLang.ts deleted file mode 100644 index 15e95104..00000000 --- a/src/ProChat/utils/switchLang.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { changeLanguage } from 'i18next'; - -import { LocaleMode } from '@/types/locale'; - -export const switchLang = (locale: LocaleMode) => { - const lang = locale === 'auto' ? navigator.language : locale; - - changeLanguage(lang); - document.documentElement.lang = lang; -}; diff --git a/src/ProChat/utils/uploadFIle.ts b/src/ProChat/utils/uploadFIle.ts deleted file mode 100644 index 2eb48296..00000000 --- a/src/ProChat/utils/uploadFIle.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const createUploadImageHandler = - (onUploadImage: (base64: string) => void) => (file: any) => { - const reader = new FileReader(); - reader.readAsDataURL(file); - reader.addEventListener('load', () => { - onUploadImage(String(reader.result)); - }); - }; diff --git a/src/ProChat/utils/url.test.ts b/src/ProChat/utils/url.test.ts deleted file mode 100644 index 2d618fc9..00000000 --- a/src/ProChat/utils/url.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { pathString } from './url'; - -describe('pathString', () => { - it('基本情况', () => { - const result = pathString('/home'); - expect(result).toBe('/home'); - }); - - it('包含查询参数的情况', () => { - const result = pathString('/home', { search: 'id=1&name=test' }); - expect(result).toBe('/home?id=1&name=test'); - }); - - it('包含哈希值的情况', () => { - const result = pathString('/home', { hash: 'top' }); - expect(result).toBe('/home#top'); - - const result2 = pathString('/home', { hash: '#hash=abc' }); - expect(result2).toBe('/home#hash=abc'); - }); - - it('path 参数包含相对路径的情况', () => { - const result = pathString('./home'); - expect(result).toBe('/home'); - }); - - it('path 参数包含绝对路径的情况', () => { - const result = pathString('/home'); - expect(result).toBe('/home'); - }); - - it('path 参数包含协议的情况', () => { - const result = pathString('https://www.example.com/home'); - expect(result).toBe('https://www.example.com/home'); - }); - - it('path 参数包含主机名的情况', () => { - const result = pathString('//www.example.com/home'); - expect(result).toBe('https://www.example.com/home'); - }); - - it('path 参数包含端口号的情况', () => { - const result = pathString('//www.example.com:8080/home'); - expect(result).toBe('https://www.example.com:8080/home'); - }); - - it('path 参数包含特殊字符的情况', () => { - const result = pathString('/home/测试'); - expect(result).toBe('/home/%E6%B5%8B%E8%AF%95'); - }); -}); diff --git a/src/ProChat/utils/url.ts b/src/ProChat/utils/url.ts deleted file mode 100644 index 46ed3863..00000000 --- a/src/ProChat/utils/url.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Build a path string from a path and a hash/search object - * @param path - * @param hash - * @param search - */ -export const pathString = ( - path: string, - { - hash = '', - search = '', - }: { - hash?: string; - search?: string; - } = {}, -) => { - const tempBase = 'https://a.com'; - const url = new URL(path, tempBase); - - if (hash) url.hash = hash; - if (search) url.search = search; - return url.toString().replace(tempBase, ''); -}; diff --git a/src/ProChat/utils/uuid.ts b/src/ProChat/utils/uuid.ts index 3c9d63e6..8cc31f3c 100644 --- a/src/ProChat/utils/uuid.ts +++ b/src/ProChat/utils/uuid.ts @@ -5,5 +5,3 @@ export const nanoid = customAlphabet( '1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', 8, ); - -export { v4 as uuid } from 'uuid'; diff --git a/src/components/Avatar/index.tsx b/src/components/Avatar/index.tsx index c01f16c7..7b852208 100644 --- a/src/components/Avatar/index.tsx +++ b/src/components/Avatar/index.tsx @@ -7,7 +7,6 @@ import { getEmoji } from '@/utils/getEmojiByCharacter'; import { useStyles } from './style'; export interface AvatarProps extends AntAvatarProps { - animation?: boolean; /** * @description The URL or base64 data of the avatar image */ @@ -37,7 +36,6 @@ const Avatar = memo( className, avatar, title, - animation, size = 40, shape = 'circle', background = 'rgba(0,0,0,0)', @@ -71,11 +69,7 @@ const Avatar = memo( /> ) : ( - {emoji ? ( - - ) : ( - text?.toUpperCase().slice(0, 2) - )} + {emoji ? : text?.toUpperCase().slice(0, 2)} ); }, diff --git a/src/index.ts b/src/index.ts index e6c8a8ae..5ff6ac70 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,6 +15,7 @@ export type { RenderMessageExtra, } from './ChatList'; export { default as ActionsBar, type ActionsBarProps } from './ChatList/ActionsBar'; +export { ProChat } from './ProChat'; export { default as DraggablePanel, type DraggablePanelProps } from './DraggablePanel'; export { @@ -53,7 +54,6 @@ export { default as Markdown, type MarkdownProps } from './Markdown'; export { default as MessageInput, type MessageInputProps } from './MessageInput'; export { default as MessageModal, type MessageModalProps } from './MessageModal'; -export { default as GlobalStyle } from './GlobalStyle'; export { default as Snippet, type SnippetProps } from './Snippet'; export { default as TokenTag, type TokenTagProps } from './TokenTag'; export { useChatListActionsBar } from './hooks/useChatListActionsBar'; diff --git a/src/styles/stylish.ts b/src/styles/stylish.ts new file mode 100644 index 00000000..99ba13e1 --- /dev/null +++ b/src/styles/stylish.ts @@ -0,0 +1,238 @@ +import { createStylish, keyframes } from 'antd-style'; + +export const useStylish = createStylish(({ css, token, isDarkMode }) => { + const gradient = keyframes` + 0% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } + 100% { + background-position: 0% 50%; + } + `; + + const cyanColor = isDarkMode ? token.cyan9A : token.cyan11A; + const cyanBackground = isDarkMode ? token.cyan2A : token.cyan6A; + + return { + blur: css` + backdrop-filter: saturate(180%) blur(10px); + `, + blurStrong: css` + backdrop-filter: blur(36px); + `, + bottomScrollbar: css` + ::-webkit-scrollbar { + width: 0; + height: 4px; + background-color: transparent; + + &-thumb { + background-color: ${token.colorFill}; + border-radius: 4px; + transition: background-color 500ms ${token.motionEaseOut}; + } + + &-corner { + display: none; + width: 0; + height: 0; + } + } + `, + gradientAnimation: css` + background-image: linear-gradient( + -45deg, + ${token.gold}, + ${token.magenta}, + ${token.geekblue}, + ${token.cyan} + ); + background-size: 400% 400%; + border-radius: inherit; + animation: 5s ${gradient} 5s ease infinite; + `, + markdown: css` + color: ${token.colorText}; + + h1, + h2, + h3, + h4, + h5 { + font-weight: 600; + } + + p { + margin-block-start: 0; + margin-block-end: 0; + + font-size: 14px; + line-height: 1.8; + color: ${token.colorText}; + word-break: break-all; + word-wrap: break-word; + + + * { + margin-block-end: 0.5em; + } + } + + > *:last-child { + margin-bottom: 0 !important; + } + + blockquote { + margin: 16px 0; + padding: 0 12px; + + p { + font-style: italic; + color: ${token.colorTextDescription}; + } + } + + p:not(:last-child) { + margin-bottom: 1em; + } + + a { + color: ${token.colorLink}; + + &:hover { + color: ${token.colorLinkHover}; + } + + &:active { + color: ${token.colorLinkActive}; + } + } + + img { + max-width: 100%; + } + + pre, + [data-code-type='highlighter'] { + border: none; + border-radius: ${token.borderRadius}px; + + > code { + padding: 0 !important; + border: none !important; + } + } + + > :not([data-code-type='highlighter']) code { + padding: 2px 6px; + + font-size: ${token.fontSizeSM}px; + color: ${cyanColor}; + + background: ${cyanBackground}; + border: 1px solid ${isDarkMode ? token.cyan1A : token.cyan6A}; + border-radius: ${token.borderRadiusSM}px; + } + + table { + border-spacing: 0; + + width: 100%; + margin-block-start: 1em; + margin-block-end: 1em; + margin-inline-start: 0; + margin-inline-end: 0; + padding: 8px; + + border: 1px solid ${token.colorBorderSecondary}; + border-radius: ${token.borderRadius}px; + + code { + display: inline-flex; + } + } + + th, + td { + padding-block-start: 10px; + padding-block-end: 10px; + padding-inline-start: 16px; + padding-inline-end: 16px; + } + + thead { + tr { + th { + background: ${token.colorFillTertiary}; + + &:first-child { + border-top-left-radius: ${token.borderRadius}px; + border-bottom-left-radius: ${token.borderRadius}px; + } + + &:last-child { + border-top-right-radius: ${token.borderRadius}px; + border-bottom-right-radius: ${token.borderRadius}px; + } + } + } + } + + > ol > li::marker { + color: ${isDarkMode ? token.cyan9A : token.cyan10A} !important; + } + + > ul > li { + line-height: 1.8; + list-style-type: disc; + + &::marker { + color: ${isDarkMode ? token.cyan9A : token.cyan10A} !important; + } + } + + ol, + ul { + > li::marker { + color: ${token.colorTextDescription}; + } + } + + details { + margin-bottom: 1em; + padding: 12px 16px; + + background: ${token.colorFillTertiary}; + border: 1px solid ${token.colorBorderSecondary}; + border-radius: ${token.borderRadiusLG}px; + + transition: all 400ms ${token.motionEaseOut}; + } + + details[open] { + summary { + padding-bottom: 12px; + border-bottom: 1px solid ${token.colorBorder}; + } + } + `, + noScrollbar: css` + ::-webkit-scrollbar { + display: none; + width: 0; + height: 0; + background-color: transparent; + } + `, + resetLinkColor: css` + cursor: pointer; + color: ${token.colorTextSecondary}; + + &:hover { + color: ${token.colorText}; + } + `, + }; +}); diff --git a/src/types/meta.ts b/src/types/meta.ts index 0464ce37..3a6b2408 100644 --- a/src/types/meta.ts +++ b/src/types/meta.ts @@ -9,8 +9,6 @@ export interface MetaData { * @description 可选参数,如果不传则使用默认背景色 */ backgroundColor?: string; - description?: string; - tags?: string[]; /** * 名称 * @description 可选参数,如果不传则使用默认名称