@lobehub/chat 0.19.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/package.json +1 -1
- package/src/locales/default/common.ts +2 -2
- package/src/pages/chat/[id]/Config/SideBar.tsx +9 -5
- package/src/pages/chat/[id]/Config/Topic.tsx +27 -0
- package/src/pages/chat/[id]/Config/TopicItem.tsx +80 -0
- package/src/pages/chat/[id]/Conversation/Input/index.tsx +5 -2
- package/src/pages/chat/[id]/Header.tsx +14 -10
- package/src/pages/chat/layout.tsx +10 -4
- package/src/prompts/chat.ts +19 -0
- package/src/store/session/selectors.ts +1 -1
- package/src/store/session/slices/agentConfig/selectors.ts +4 -0
- package/src/store/session/slices/chat/actions/index.ts +21 -0
- package/src/store/session/slices/chat/{action.ts → actions/message.ts} +33 -23
- package/src/store/session/slices/chat/actions/topic.ts +98 -0
- package/src/store/session/slices/chat/index.ts +1 -1
- package/src/store/session/slices/chat/initialState.ts +2 -5
- package/src/store/session/slices/chat/reducers/message.test.ts +92 -0
- package/src/store/session/slices/chat/reducers/message.ts +16 -1
- package/src/store/session/slices/chat/reducers/topic.test.ts +179 -0
- package/src/store/session/slices/chat/reducers/topic.ts +59 -0
- package/src/store/session/slices/chat/selectors/chat.ts +8 -4
- package/src/store/session/slices/chat/selectors/index.ts +5 -0
- package/src/store/session/slices/chat/selectors/topic.ts +16 -0
- package/src/store/session/slices/chat/selectors/utils.test.ts +81 -1
- package/src/store/session/slices/chat/selectors/utils.ts +8 -2
- package/src/store/session/slices/session/reducers/session.ts +25 -1
- package/src/types/topic.ts +0 -1
- package/src/pages/chat/[id]/Config/ConfigCell.tsx +0 -68
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
## [Version 0.20.0](https://github.com/lobehub/lobe-chat/compare/v0.19.0...v0.20.0)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2023-07-25**</sup>
|
|
8
|
+
|
|
9
|
+
#### ✨ Features
|
|
10
|
+
|
|
11
|
+
- **misc**: 实现话题模块.
|
|
12
|
+
|
|
13
|
+
<br/>
|
|
14
|
+
|
|
15
|
+
<details>
|
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
17
|
+
|
|
18
|
+
#### What's improved
|
|
19
|
+
|
|
20
|
+
- **misc**: 实现话题模块, closes [#16](https://github.com/lobehub/lobe-chat/issues/16) ([64fd6ee](https://github.com/lobehub/lobe-chat/commit/64fd6ee))
|
|
21
|
+
|
|
22
|
+
</details>
|
|
23
|
+
|
|
24
|
+
<div align="right">
|
|
25
|
+
|
|
26
|
+
[](#readme-top)
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
|
|
5
30
|
## [Version 0.19.0](https://github.com/lobehub/lobe-chat/compare/v0.18.2...v0.19.0)
|
|
6
31
|
|
|
7
32
|
<sup>Released on **2023-07-24**</sup>
|
package/package.json
CHANGED
|
@@ -27,8 +27,8 @@ export default {
|
|
|
27
27
|
share: '分享',
|
|
28
28
|
tokenDetail: '系统设定: {{systemRoleToken}} 历史消息: {{chatsToken}}',
|
|
29
29
|
topic: {
|
|
30
|
-
saveCurrentMessages: '
|
|
31
|
-
searchPlaceholder: '
|
|
30
|
+
saveCurrentMessages: '将当前会话保存为话题',
|
|
31
|
+
searchPlaceholder: '搜索话题...',
|
|
32
32
|
},
|
|
33
33
|
updateAgent: '更新助理信息',
|
|
34
34
|
updatePrompt: '更新提示词',
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { ActionIcon, DraggablePanelBody, EditableMessage, SearchBar } from '@lobehub/ui';
|
|
2
2
|
import { createStyles } from 'antd-style';
|
|
3
|
-
import {
|
|
3
|
+
import { Maximize2Icon } from 'lucide-react';
|
|
4
4
|
import { memo, useState } from 'react';
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
|
6
6
|
import { Flexbox } from 'react-layout-kit';
|
|
7
7
|
import { shallow } from 'zustand/shallow';
|
|
8
8
|
|
|
9
|
+
import { Topic } from '@/pages/chat/[id]/Config/Topic';
|
|
9
10
|
import { agentSelectors, useSessionStore } from '@/store/session';
|
|
10
11
|
|
|
11
12
|
import Header from './Header';
|
|
@@ -42,8 +43,10 @@ const useStyles = createStyles(({ css, token }) => ({
|
|
|
42
43
|
const SideBar = memo(() => {
|
|
43
44
|
const [openModal, setOpenModal] = useState(false);
|
|
44
45
|
const { styles } = useStyles();
|
|
45
|
-
const [updateAgentConfig] = useSessionStore(
|
|
46
|
-
|
|
46
|
+
const [systemRole, updateAgentConfig] = useSessionStore(
|
|
47
|
+
(s) => [agentSelectors.currentAgentSystemRole(s), s.updateAgentConfig],
|
|
48
|
+
shallow,
|
|
49
|
+
);
|
|
47
50
|
|
|
48
51
|
const { t } = useTranslation('common');
|
|
49
52
|
return (
|
|
@@ -51,7 +54,7 @@ const SideBar = memo(() => {
|
|
|
51
54
|
<Header
|
|
52
55
|
actions={
|
|
53
56
|
<ActionIcon
|
|
54
|
-
icon={
|
|
57
|
+
icon={Maximize2Icon}
|
|
55
58
|
onClick={() => setOpenModal(true)}
|
|
56
59
|
size="small"
|
|
57
60
|
title={t('edit')}
|
|
@@ -76,8 +79,9 @@ const SideBar = memo(() => {
|
|
|
76
79
|
}}
|
|
77
80
|
value={systemRole}
|
|
78
81
|
/>
|
|
79
|
-
<Flexbox style={{ padding: 16 }}>
|
|
82
|
+
<Flexbox gap={12} style={{ padding: 16 }}>
|
|
80
83
|
<SearchBar placeholder={t('topic.searchPlaceholder')} type={'block'} />
|
|
84
|
+
<Topic />
|
|
81
85
|
</Flexbox>
|
|
82
86
|
</DraggablePanelBody>
|
|
83
87
|
);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Flexbox } from 'react-layout-kit';
|
|
2
|
+
import { shallow } from 'zustand/shallow';
|
|
3
|
+
|
|
4
|
+
import { topicSelectors, useSessionStore } from '@/store/session';
|
|
5
|
+
|
|
6
|
+
import TopicItem from './TopicItem';
|
|
7
|
+
|
|
8
|
+
export const Topic = () => {
|
|
9
|
+
const topics = useSessionStore(topicSelectors.currentTopics);
|
|
10
|
+
const [activeTopicId] = useSessionStore((s) => [s.activeTopicId], shallow);
|
|
11
|
+
return (
|
|
12
|
+
<Flexbox gap={8}>
|
|
13
|
+
<TopicItem active={!activeTopicId} fav={false} title={'默认话题'} />
|
|
14
|
+
|
|
15
|
+
{topics.map(({ id, favorite, title }) => (
|
|
16
|
+
<TopicItem
|
|
17
|
+
active={activeTopicId === id}
|
|
18
|
+
fav={favorite}
|
|
19
|
+
id={id}
|
|
20
|
+
key={id}
|
|
21
|
+
showFav
|
|
22
|
+
title={title}
|
|
23
|
+
/>
|
|
24
|
+
))}
|
|
25
|
+
</Flexbox>
|
|
26
|
+
);
|
|
27
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { StarFilled, StarOutlined } from '@ant-design/icons';
|
|
2
|
+
import { ActionIcon } from '@lobehub/ui';
|
|
3
|
+
import { createStyles } from 'antd-style';
|
|
4
|
+
import { LucideIcon } from 'lucide-react';
|
|
5
|
+
import { memo } from 'react';
|
|
6
|
+
import { Flexbox } from 'react-layout-kit';
|
|
7
|
+
import { shallow } from 'zustand/shallow';
|
|
8
|
+
|
|
9
|
+
import { useSessionStore } from '@/store/session';
|
|
10
|
+
|
|
11
|
+
const useStyles = createStyles(({ css, token }) => ({
|
|
12
|
+
active: css`
|
|
13
|
+
background: ${token.colorFill};
|
|
14
|
+
|
|
15
|
+
&:hover {
|
|
16
|
+
background: ${token.colorFill};
|
|
17
|
+
}
|
|
18
|
+
`,
|
|
19
|
+
container: css`
|
|
20
|
+
cursor: pointer;
|
|
21
|
+
background: ${token.colorFillTertiary};
|
|
22
|
+
border-radius: 6px;
|
|
23
|
+
|
|
24
|
+
&:hover {
|
|
25
|
+
background: ${token.colorFillSecondary};
|
|
26
|
+
}
|
|
27
|
+
`,
|
|
28
|
+
split: css`
|
|
29
|
+
border-bottom: 1px solid ${token.colorSplit};
|
|
30
|
+
`,
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
export interface ConfigCellProps {
|
|
34
|
+
active?: boolean;
|
|
35
|
+
fav?: boolean;
|
|
36
|
+
id?: string;
|
|
37
|
+
showFav?: boolean;
|
|
38
|
+
title: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const TopicItem = memo<ConfigCellProps>(({ title, active, id, showFav, fav }) => {
|
|
42
|
+
const { styles, theme, cx } = useStyles();
|
|
43
|
+
|
|
44
|
+
const [dispatchTopic, toggleTopic] = useSessionStore(
|
|
45
|
+
(s) => [s.dispatchTopic, s.toggleTopic],
|
|
46
|
+
shallow,
|
|
47
|
+
);
|
|
48
|
+
const starIcon = (fav ? StarFilled : StarOutlined) as LucideIcon;
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<Flexbox
|
|
52
|
+
align={'center'}
|
|
53
|
+
className={cx(styles.container, active && styles.active)}
|
|
54
|
+
distribution={'space-between'}
|
|
55
|
+
horizontal
|
|
56
|
+
onClick={() => {
|
|
57
|
+
toggleTopic(id);
|
|
58
|
+
}}
|
|
59
|
+
padding={'10px 12px'}
|
|
60
|
+
>
|
|
61
|
+
{title}
|
|
62
|
+
{!showFav ? undefined : (
|
|
63
|
+
<ActionIcon
|
|
64
|
+
icon={starIcon}
|
|
65
|
+
onClick={() => {
|
|
66
|
+
if (!id) return;
|
|
67
|
+
|
|
68
|
+
dispatchTopic({ id, key: 'favorite', type: 'updateChatTopic', value: !fav });
|
|
69
|
+
}}
|
|
70
|
+
size={'small'}
|
|
71
|
+
style={{
|
|
72
|
+
color: fav ? theme.yellow : undefined,
|
|
73
|
+
}}
|
|
74
|
+
/>
|
|
75
|
+
)}
|
|
76
|
+
</Flexbox>
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
export default TopicItem;
|
|
@@ -18,12 +18,15 @@ const ChatInput = () => {
|
|
|
18
18
|
const [text, setText] = useState('');
|
|
19
19
|
|
|
20
20
|
const [inputHeight] = useSettings((s) => [s.inputHeight], shallow);
|
|
21
|
-
const [sendMessage] = useSessionStore(
|
|
21
|
+
const [sendMessage, saveToTopic] = useSessionStore(
|
|
22
|
+
(s) => [s.createOrSendMsg, s.saveToTopic],
|
|
23
|
+
shallow,
|
|
24
|
+
);
|
|
22
25
|
|
|
23
26
|
const footer = useMemo(
|
|
24
27
|
() => (
|
|
25
28
|
<Tooltip title={t('topic.saveCurrentMessages')}>
|
|
26
|
-
<Button icon={<Icon icon={LucideGalleryVerticalEnd} />} />
|
|
29
|
+
<Button icon={<Icon icon={LucideGalleryVerticalEnd} />} onClick={saveToTopic} />
|
|
27
30
|
</Tooltip>
|
|
28
31
|
),
|
|
29
32
|
[],
|
|
@@ -12,10 +12,18 @@ import { agentSelectors, useSessionStore } from '@/store/session';
|
|
|
12
12
|
const Header = memo(() => {
|
|
13
13
|
const { t } = useTranslation('common');
|
|
14
14
|
|
|
15
|
-
const [
|
|
16
|
-
(s) => [
|
|
15
|
+
const [title, description, avatar, backgroundColor, id, model] = useSessionStore(
|
|
16
|
+
(s) => [
|
|
17
|
+
agentSelectors.currentAgentTitle(s),
|
|
18
|
+
agentSelectors.currentAgentDescription(s),
|
|
19
|
+
agentSelectors.currentAgentAvatar(s),
|
|
20
|
+
agentSelectors.currentAgentBackgroundColor(s),
|
|
21
|
+
s.activeId,
|
|
22
|
+
agentSelectors.currentAgentModel(s),
|
|
23
|
+
],
|
|
17
24
|
shallow,
|
|
18
25
|
);
|
|
26
|
+
|
|
19
27
|
const [showAgentSettings, toggleConfig] = useSessionStore(
|
|
20
28
|
(s) => [s.showAgentSettings, s.toggleConfig],
|
|
21
29
|
shallow,
|
|
@@ -26,20 +34,16 @@ const Header = memo(() => {
|
|
|
26
34
|
left={
|
|
27
35
|
<>
|
|
28
36
|
<Avatar
|
|
29
|
-
avatar={
|
|
30
|
-
background={
|
|
37
|
+
avatar={avatar}
|
|
38
|
+
background={backgroundColor}
|
|
31
39
|
onClick={() => {
|
|
32
40
|
Router.push(`/chat/${id}/edit`);
|
|
33
41
|
}}
|
|
34
42
|
size={40}
|
|
35
43
|
style={{ cursor: 'pointer' }}
|
|
36
|
-
title={
|
|
37
|
-
/>
|
|
38
|
-
<HeaderTitle
|
|
39
|
-
desc={meta?.description || t('noDescription')}
|
|
40
|
-
tag={<Tag>{modle}</Tag>}
|
|
41
|
-
title={meta?.title || t('defaultAgent')}
|
|
44
|
+
title={title}
|
|
42
45
|
/>
|
|
46
|
+
<HeaderTitle desc={description} tag={<Tag>{model}</Tag>} title={title} />
|
|
43
47
|
</>
|
|
44
48
|
}
|
|
45
49
|
right={
|
|
@@ -17,8 +17,8 @@ const ChatLayout = memo<PropsWithChildren>(({ children }) => {
|
|
|
17
17
|
initI18n.finally();
|
|
18
18
|
}, []);
|
|
19
19
|
|
|
20
|
-
const [activeSession] = useSessionStore((s) => {
|
|
21
|
-
return [s.activeSession];
|
|
20
|
+
const [activeSession, toggleTopic] = useSessionStore((s) => {
|
|
21
|
+
return [s.activeSession, s.toggleTopic];
|
|
22
22
|
}, shallow);
|
|
23
23
|
|
|
24
24
|
const router = useRouter();
|
|
@@ -26,10 +26,16 @@ const ChatLayout = memo<PropsWithChildren>(({ children }) => {
|
|
|
26
26
|
|
|
27
27
|
useEffect(() => {
|
|
28
28
|
const hasRehydrated = useSessionStore.persist.hasHydrated();
|
|
29
|
-
//
|
|
30
|
-
if (
|
|
29
|
+
// 只有当水合完毕后再开始做操作
|
|
30
|
+
if (!hasRehydrated) return;
|
|
31
|
+
|
|
32
|
+
// 1. 正常激活会话
|
|
33
|
+
if (typeof id === 'string') {
|
|
31
34
|
activeSession(id);
|
|
32
35
|
}
|
|
36
|
+
|
|
37
|
+
// 将话题重置为默认值
|
|
38
|
+
toggleTopic();
|
|
33
39
|
}, [id]);
|
|
34
40
|
|
|
35
41
|
useEffect(() => {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { OpenAIChatMessage, OpenAIStreamPayload } from '@/types/openai';
|
|
2
|
+
|
|
3
|
+
export const promptSummaryTitle = (
|
|
4
|
+
messages: OpenAIChatMessage[],
|
|
5
|
+
): Partial<OpenAIStreamPayload> => ({
|
|
6
|
+
messages: [
|
|
7
|
+
{
|
|
8
|
+
content:
|
|
9
|
+
'你是一名擅长会话的助理,你需要将用户的会话总结为 10 个字以内的标题,不需要包含标点符号',
|
|
10
|
+
role: 'system',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
content: `${messages.map((message) => `${message.role}: ${message.content}`).join('\n')}
|
|
14
|
+
|
|
15
|
+
请总结上述对话为10个字以内的标题,不需要包含标点符号`,
|
|
16
|
+
role: 'user',
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
});
|
|
@@ -17,6 +17,9 @@ const currentAgentMeta = (s: SessionStore): MetaData => {
|
|
|
17
17
|
|
|
18
18
|
const currentAgentTitle = (s: SessionStore) => currentAgentMeta(s)?.title || t('defaultSession');
|
|
19
19
|
|
|
20
|
+
const currentAgentDescription = (s: SessionStore) =>
|
|
21
|
+
currentAgentMeta(s)?.description || t('noDescription');
|
|
22
|
+
|
|
20
23
|
const currentAgentBackgroundColor = (s: SessionStore) => {
|
|
21
24
|
const session = sessionSelectors.currentSession(s);
|
|
22
25
|
if (!session) return DEFAULT_BACKGROUND_COLOR;
|
|
@@ -62,6 +65,7 @@ export const agentSelectors = {
|
|
|
62
65
|
currentAgentBackgroundColor,
|
|
63
66
|
currentAgentConfig,
|
|
64
67
|
currentAgentConfigSafe,
|
|
68
|
+
currentAgentDescription,
|
|
65
69
|
currentAgentMeta,
|
|
66
70
|
currentAgentModel,
|
|
67
71
|
currentAgentSystemRole,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { StateCreator } from 'zustand/vanilla';
|
|
2
|
+
|
|
3
|
+
import { SessionStore } from '@/store/session';
|
|
4
|
+
|
|
5
|
+
import { ChatMessageAction, chatMessage } from './message';
|
|
6
|
+
import { ChatTopicAction, chatTopic } from './topic';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 聊天操作
|
|
10
|
+
*/
|
|
11
|
+
export interface ChatAction extends ChatTopicAction, ChatMessageAction {}
|
|
12
|
+
|
|
13
|
+
export const createChatSlice: StateCreator<
|
|
14
|
+
SessionStore,
|
|
15
|
+
[['zustand/devtools', never]],
|
|
16
|
+
[],
|
|
17
|
+
ChatAction
|
|
18
|
+
> = (...params) => ({
|
|
19
|
+
...chatTopic(...params),
|
|
20
|
+
...chatMessage(...params),
|
|
21
|
+
});
|
|
@@ -7,14 +7,14 @@ import { fetchSSE } from '@/utils/fetch';
|
|
|
7
7
|
import { isFunctionMessage } from '@/utils/message';
|
|
8
8
|
import { nanoid } from '@/utils/uuid';
|
|
9
9
|
|
|
10
|
-
import { MessageDispatch, messagesReducer } from '
|
|
10
|
+
import { MessageDispatch, messagesReducer } from '../reducers/message';
|
|
11
11
|
|
|
12
12
|
const LOADING_FLAT = '...';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* 聊天操作
|
|
16
16
|
*/
|
|
17
|
-
export interface
|
|
17
|
+
export interface ChatMessageAction {
|
|
18
18
|
/**
|
|
19
19
|
* 清除消息
|
|
20
20
|
*/
|
|
@@ -57,6 +57,7 @@ export interface ChatAction {
|
|
|
57
57
|
* @param id - 消息 ID
|
|
58
58
|
*/
|
|
59
59
|
resendMessage: (id: string) => Promise<void>;
|
|
60
|
+
|
|
60
61
|
/**
|
|
61
62
|
* 发送消息
|
|
62
63
|
* @param text - 消息文本
|
|
@@ -64,14 +65,20 @@ export interface ChatAction {
|
|
|
64
65
|
sendMessage: (text: string) => Promise<void>;
|
|
65
66
|
}
|
|
66
67
|
|
|
67
|
-
export const
|
|
68
|
+
export const chatMessage: StateCreator<
|
|
68
69
|
SessionStore,
|
|
69
70
|
[['zustand/devtools', never]],
|
|
70
71
|
[],
|
|
71
|
-
|
|
72
|
+
ChatMessageAction
|
|
72
73
|
> = (set, get) => ({
|
|
73
74
|
clearMessage: () => {
|
|
74
|
-
|
|
75
|
+
const { dispatchMessage, activeTopicId, dispatchTopic } = get();
|
|
76
|
+
|
|
77
|
+
dispatchMessage({ topicId: activeTopicId, type: 'resetMessages' });
|
|
78
|
+
|
|
79
|
+
if (activeTopicId) {
|
|
80
|
+
dispatchTopic({ id: activeTopicId, type: 'deleteChatTopic' });
|
|
81
|
+
}
|
|
75
82
|
},
|
|
76
83
|
|
|
77
84
|
createOrSendMsg: async (message) => {
|
|
@@ -155,7 +162,7 @@ export const createChatSlice: StateCreator<
|
|
|
155
162
|
},
|
|
156
163
|
|
|
157
164
|
realFetchAIResponse: async (messages, userMessageId) => {
|
|
158
|
-
const { dispatchMessage, generateMessage } = get();
|
|
165
|
+
const { dispatchMessage, generateMessage, activeTopicId } = get();
|
|
159
166
|
|
|
160
167
|
// 添加 systemRole
|
|
161
168
|
const { systemRole, model } = agentSelectors.currentAgentConfigSafe(get());
|
|
@@ -165,49 +172,46 @@ export const createChatSlice: StateCreator<
|
|
|
165
172
|
|
|
166
173
|
// 再添加一个空的信息用于放置 ai 响应,注意顺序不能反
|
|
167
174
|
// 因为如果顺序反了,messages 中将包含新增的 ai message
|
|
168
|
-
const
|
|
175
|
+
const mid = nanoid();
|
|
169
176
|
|
|
170
177
|
dispatchMessage({
|
|
171
|
-
id:
|
|
178
|
+
id: mid,
|
|
172
179
|
message: LOADING_FLAT,
|
|
173
180
|
parentId: userMessageId,
|
|
174
181
|
role: 'assistant',
|
|
175
182
|
type: 'addMessage',
|
|
176
183
|
});
|
|
177
184
|
|
|
185
|
+
// 如果有 activeTopicId,则添加 topicId
|
|
186
|
+
if (activeTopicId) {
|
|
187
|
+
dispatchMessage({ id: mid, key: 'topicId', type: 'updateMessage', value: activeTopicId });
|
|
188
|
+
}
|
|
189
|
+
|
|
178
190
|
// 为模型添加 fromModel 的额外信息
|
|
179
|
-
dispatchMessage({
|
|
180
|
-
id: assistantId,
|
|
181
|
-
key: 'fromModel',
|
|
182
|
-
type: 'updateMessageExtra',
|
|
183
|
-
value: model,
|
|
184
|
-
});
|
|
191
|
+
dispatchMessage({ id: mid, key: 'fromModel', type: 'updateMessageExtra', value: model });
|
|
185
192
|
|
|
186
193
|
// 生成 ai message
|
|
187
|
-
const { output, isFunctionCall } = await generateMessage(messages,
|
|
194
|
+
const { output, isFunctionCall } = await generateMessage(messages, mid);
|
|
188
195
|
|
|
189
196
|
// 如果是 function,则发送函数调用方法
|
|
190
197
|
if (isFunctionCall) {
|
|
191
198
|
const { function_call } = JSON.parse(output);
|
|
192
199
|
|
|
193
200
|
dispatchMessage({
|
|
194
|
-
id:
|
|
201
|
+
id: mid,
|
|
195
202
|
key: 'function_call',
|
|
196
203
|
type: 'updateMessage',
|
|
197
204
|
value: function_call,
|
|
198
205
|
});
|
|
199
206
|
|
|
200
207
|
await generateMessage(
|
|
201
|
-
[
|
|
202
|
-
|
|
203
|
-
{ content: '', function_call, id: assistantId, role: 'assistant' } as ChatMessage,
|
|
204
|
-
],
|
|
205
|
-
assistantId,
|
|
208
|
+
[...messages, { content: '', function_call, id: mid, role: 'assistant' } as ChatMessage],
|
|
209
|
+
mid,
|
|
206
210
|
true,
|
|
207
211
|
);
|
|
208
212
|
|
|
209
213
|
dispatchMessage({
|
|
210
|
-
id:
|
|
214
|
+
id: mid,
|
|
211
215
|
key: 'role',
|
|
212
216
|
type: 'updateMessage',
|
|
213
217
|
value: 'assistant',
|
|
@@ -243,13 +247,19 @@ export const createChatSlice: StateCreator<
|
|
|
243
247
|
},
|
|
244
248
|
|
|
245
249
|
sendMessage: async (message) => {
|
|
246
|
-
const { dispatchMessage, realFetchAIResponse, autocompleteSessionAgentMeta } =
|
|
250
|
+
const { dispatchMessage, realFetchAIResponse, autocompleteSessionAgentMeta, activeTopicId } =
|
|
251
|
+
get();
|
|
247
252
|
const session = sessionSelectors.currentSession(get());
|
|
248
253
|
if (!session || !message) return;
|
|
249
254
|
|
|
250
255
|
const userId = nanoid();
|
|
251
256
|
dispatchMessage({ id: userId, message, role: 'user', type: 'addMessage' });
|
|
252
257
|
|
|
258
|
+
// 如果有 activeTopicId,则添加 topicId
|
|
259
|
+
if (activeTopicId) {
|
|
260
|
+
dispatchMessage({ id: userId, key: 'topicId', type: 'updateMessage', value: activeTopicId });
|
|
261
|
+
}
|
|
262
|
+
|
|
253
263
|
// 先拿到当前的 messages
|
|
254
264
|
const messages = chatSelectors.currentChats(get());
|
|
255
265
|
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { StateCreator } from 'zustand/vanilla';
|
|
2
|
+
|
|
3
|
+
import { promptSummaryTitle } from '@/prompts/chat';
|
|
4
|
+
import { SessionStore, chatSelectors, sessionSelectors } from '@/store/session';
|
|
5
|
+
import { fetchPresetTaskResult } from '@/utils/fetch';
|
|
6
|
+
import { nanoid } from '@/utils/uuid';
|
|
7
|
+
|
|
8
|
+
import { ChatTopicDispatch, topicReducer } from '../reducers/topic';
|
|
9
|
+
|
|
10
|
+
export interface ChatTopicAction {
|
|
11
|
+
/**
|
|
12
|
+
* 分发主题
|
|
13
|
+
* @param payload - 要分发的主题
|
|
14
|
+
*/
|
|
15
|
+
dispatchTopic: (payload: ChatTopicDispatch) => void;
|
|
16
|
+
/**
|
|
17
|
+
* 将当前消息保存为主题
|
|
18
|
+
*/
|
|
19
|
+
saveToTopic: () => void;
|
|
20
|
+
/**
|
|
21
|
+
* 切换主题
|
|
22
|
+
* @param id - 要切换的主题的 ID
|
|
23
|
+
*/
|
|
24
|
+
toggleTopic: (id?: string) => void;
|
|
25
|
+
/**
|
|
26
|
+
* 更新主题加载状态
|
|
27
|
+
* @param id - 要更新的主题的 ID
|
|
28
|
+
*/
|
|
29
|
+
updateTopicLoading: (id?: string) => void;
|
|
30
|
+
}
|
|
31
|
+
export const chatTopic: StateCreator<
|
|
32
|
+
SessionStore,
|
|
33
|
+
[['zustand/devtools', never]],
|
|
34
|
+
[],
|
|
35
|
+
ChatTopicAction
|
|
36
|
+
> = (set, get) => ({
|
|
37
|
+
dispatchTopic: (payload) => {
|
|
38
|
+
const { activeId } = get();
|
|
39
|
+
const session = sessionSelectors.currentSession(get());
|
|
40
|
+
if (!activeId || !session) return;
|
|
41
|
+
|
|
42
|
+
const topics = topicReducer(session.topics || {}, payload);
|
|
43
|
+
|
|
44
|
+
get().dispatchSession({ id: activeId, topics, type: 'updateSessionTopic' });
|
|
45
|
+
},
|
|
46
|
+
saveToTopic: () => {
|
|
47
|
+
const session = sessionSelectors.currentSession(get());
|
|
48
|
+
if (!session) return;
|
|
49
|
+
|
|
50
|
+
const { dispatchTopic, dispatchMessage, updateTopicLoading } = get();
|
|
51
|
+
// 获取当前的 messages
|
|
52
|
+
const messages = chatSelectors.currentChats(get());
|
|
53
|
+
|
|
54
|
+
const topicId = nanoid();
|
|
55
|
+
|
|
56
|
+
const defaultTitle = '默认话题';
|
|
57
|
+
const newTopic = {
|
|
58
|
+
createAt: Date.now(),
|
|
59
|
+
id: topicId,
|
|
60
|
+
title: defaultTitle,
|
|
61
|
+
updateAt: Date.now(),
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
dispatchTopic({
|
|
65
|
+
topic: newTopic,
|
|
66
|
+
type: 'addChatTopic',
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// 为所有 message 添加 topicId
|
|
70
|
+
for (const m of messages) {
|
|
71
|
+
dispatchMessage({ id: m.id, key: 'topicId', type: 'updateMessage', value: topicId });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let output = '';
|
|
75
|
+
|
|
76
|
+
// 自动总结话题标题
|
|
77
|
+
fetchPresetTaskResult({
|
|
78
|
+
onError: () => {
|
|
79
|
+
dispatchTopic({ id: topicId, key: 'title', type: 'updateChatTopic', value: defaultTitle });
|
|
80
|
+
},
|
|
81
|
+
onLoadingChange: (loading) => {
|
|
82
|
+
updateTopicLoading(loading ? topicId : undefined);
|
|
83
|
+
},
|
|
84
|
+
onMessageHandle: (x) => {
|
|
85
|
+
output += x;
|
|
86
|
+
dispatchTopic({ id: topicId, key: 'title', type: 'updateChatTopic', value: output });
|
|
87
|
+
},
|
|
88
|
+
params: promptSummaryTitle(messages),
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
toggleTopic: (id) => {
|
|
92
|
+
set({ activeTopicId: id });
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
updateTopicLoading: (id) => {
|
|
96
|
+
set({ topicLoadingId: id });
|
|
97
|
+
},
|
|
98
|
+
});
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
export interface ChatState {
|
|
2
|
+
activeTopicId?: string;
|
|
2
3
|
chatLoading: boolean;
|
|
3
|
-
|
|
4
|
+
topicLoadingId?: string;
|
|
4
5
|
}
|
|
5
6
|
|
|
6
7
|
export const initialChatState: ChatState = {
|
|
7
8
|
chatLoading: false,
|
|
8
|
-
|
|
9
|
-
// activeId: null,
|
|
10
|
-
// searchKeywords: '',
|
|
11
|
-
//
|
|
12
9
|
};
|
|
@@ -116,6 +116,30 @@ describe('messagesReducer', () => {
|
|
|
116
116
|
quotaId: 'message2',
|
|
117
117
|
});
|
|
118
118
|
});
|
|
119
|
+
|
|
120
|
+
it('should use the provided parentId and quotaId when adding a new message', () => {
|
|
121
|
+
const payload: MessageDispatch = {
|
|
122
|
+
type: 'addMessage',
|
|
123
|
+
message: 'New Message',
|
|
124
|
+
id: 'message3',
|
|
125
|
+
role: 'user',
|
|
126
|
+
parentId: 'message1',
|
|
127
|
+
quotaId: 'message2',
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const newState = messagesReducer(initialState, payload);
|
|
131
|
+
|
|
132
|
+
expect(newState.message3).toEqual({
|
|
133
|
+
id: 'message3',
|
|
134
|
+
content: 'New Message',
|
|
135
|
+
meta: {},
|
|
136
|
+
createAt: expect.any(Number),
|
|
137
|
+
updateAt: expect.any(Number),
|
|
138
|
+
role: 'user',
|
|
139
|
+
parentId: 'message1',
|
|
140
|
+
quotaId: 'message2',
|
|
141
|
+
});
|
|
142
|
+
});
|
|
119
143
|
});
|
|
120
144
|
|
|
121
145
|
describe('deleteMessage', () => {
|
|
@@ -142,6 +166,17 @@ describe('messagesReducer', () => {
|
|
|
142
166
|
|
|
143
167
|
expect(newState).toEqual(initialState);
|
|
144
168
|
});
|
|
169
|
+
|
|
170
|
+
it('should not modify the state if the specified message does not exist', () => {
|
|
171
|
+
const payload: MessageDispatch = {
|
|
172
|
+
type: 'deleteMessage',
|
|
173
|
+
id: 'nonexistentMessage',
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const newState = messagesReducer(initialState, payload);
|
|
177
|
+
|
|
178
|
+
expect(newState).toEqual(initialState);
|
|
179
|
+
});
|
|
145
180
|
});
|
|
146
181
|
|
|
147
182
|
describe('updateMessage', () => {
|
|
@@ -171,6 +206,19 @@ describe('messagesReducer', () => {
|
|
|
171
206
|
|
|
172
207
|
expect(newState).toEqual(initialState);
|
|
173
208
|
});
|
|
209
|
+
|
|
210
|
+
it('should not modify the state if the specified message does not exist', () => {
|
|
211
|
+
const payload: MessageDispatch = {
|
|
212
|
+
type: 'updateMessage',
|
|
213
|
+
id: 'nonexistentMessage',
|
|
214
|
+
key: 'content',
|
|
215
|
+
value: 'Updated Message',
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const newState = messagesReducer(initialState, payload);
|
|
219
|
+
|
|
220
|
+
expect(newState).toEqual(initialState);
|
|
221
|
+
});
|
|
174
222
|
});
|
|
175
223
|
|
|
176
224
|
describe('updateMessageExtra', () => {
|
|
@@ -200,6 +248,19 @@ describe('messagesReducer', () => {
|
|
|
200
248
|
|
|
201
249
|
expect(newState).toEqual(initialState);
|
|
202
250
|
});
|
|
251
|
+
|
|
252
|
+
it('should not modify the state if the specified message does not exist', () => {
|
|
253
|
+
const payload: MessageDispatch = {
|
|
254
|
+
type: 'updateMessageExtra',
|
|
255
|
+
id: 'nonexistentMessage',
|
|
256
|
+
key: 'translate',
|
|
257
|
+
value: { target: 'en', to: 'zh' },
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const newState = messagesReducer(initialState, payload);
|
|
261
|
+
|
|
262
|
+
expect(newState).toEqual(initialState);
|
|
263
|
+
});
|
|
203
264
|
});
|
|
204
265
|
|
|
205
266
|
describe('resetMessages', () => {
|
|
@@ -212,6 +273,37 @@ describe('messagesReducer', () => {
|
|
|
212
273
|
|
|
213
274
|
expect(newState).toEqual({});
|
|
214
275
|
});
|
|
276
|
+
|
|
277
|
+
it('should delete messages with the specified topicId', () => {
|
|
278
|
+
const initialState = {
|
|
279
|
+
message1: {
|
|
280
|
+
id: 'message1',
|
|
281
|
+
content: 'Hello World',
|
|
282
|
+
createAt: 1629264000000,
|
|
283
|
+
updateAt: 1629264000000,
|
|
284
|
+
role: 'user',
|
|
285
|
+
topicId: 'topic1',
|
|
286
|
+
},
|
|
287
|
+
message2: {
|
|
288
|
+
id: 'message2',
|
|
289
|
+
content: 'How are you?',
|
|
290
|
+
createAt: 1629264000000,
|
|
291
|
+
updateAt: 1629264000000,
|
|
292
|
+
role: 'system',
|
|
293
|
+
},
|
|
294
|
+
} as unknown as ChatMessageMap;
|
|
295
|
+
|
|
296
|
+
const payload: MessageDispatch = {
|
|
297
|
+
type: 'resetMessages',
|
|
298
|
+
topicId: 'topic1',
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const newState = messagesReducer(initialState, payload);
|
|
302
|
+
|
|
303
|
+
expect(Object.keys(newState)).toHaveLength(1);
|
|
304
|
+
expect(newState).not.toHaveProperty('message1');
|
|
305
|
+
expect(newState).toHaveProperty('message2');
|
|
306
|
+
});
|
|
215
307
|
});
|
|
216
308
|
|
|
217
309
|
describe('unimplemented type', () => {
|
|
@@ -21,6 +21,7 @@ interface DeleteMessage {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
interface ResetMessages {
|
|
24
|
+
topicId?: string;
|
|
24
25
|
type: 'resetMessages';
|
|
25
26
|
}
|
|
26
27
|
|
|
@@ -101,7 +102,21 @@ export const messagesReducer = (
|
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
case 'resetMessages': {
|
|
104
|
-
return {
|
|
105
|
+
return produce(state, (draftState) => {
|
|
106
|
+
const { topicId } = payload;
|
|
107
|
+
|
|
108
|
+
const messages = Object.values(draftState).filter((message) => {
|
|
109
|
+
// 如果没有 topicId,说明是清空默认对话里的消息
|
|
110
|
+
if (!topicId) return !message.topicId;
|
|
111
|
+
|
|
112
|
+
return message.topicId === topicId;
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// 删除上述找到的消息
|
|
116
|
+
for (const message of messages) {
|
|
117
|
+
delete draftState[message.id];
|
|
118
|
+
}
|
|
119
|
+
});
|
|
105
120
|
}
|
|
106
121
|
|
|
107
122
|
default: {
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { produce } from 'immer';
|
|
2
|
+
|
|
3
|
+
import { ChatTopic, ChatTopicMap } from '@/types/topic';
|
|
4
|
+
|
|
5
|
+
import { ChatTopicDispatch, topicReducer } from './topic';
|
|
6
|
+
|
|
7
|
+
describe('topicReducer', () => {
|
|
8
|
+
let state: ChatTopicMap;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
state = {};
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe('addChatTopic', () => {
|
|
15
|
+
it('should add a new ChatTopic object to state', () => {
|
|
16
|
+
const payload: ChatTopicDispatch = {
|
|
17
|
+
type: 'addChatTopic',
|
|
18
|
+
topic: {
|
|
19
|
+
id: '1',
|
|
20
|
+
title: 'Test Topic',
|
|
21
|
+
createAt: Date.now(),
|
|
22
|
+
updateAt: Date.now(),
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const newState = topicReducer(state, payload);
|
|
27
|
+
|
|
28
|
+
expect(newState).toMatchObject({
|
|
29
|
+
'1': payload.topic,
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should add a ChatTopic object with correct id', () => {
|
|
34
|
+
const payload: ChatTopicDispatch = {
|
|
35
|
+
type: 'addChatTopic',
|
|
36
|
+
topic: {
|
|
37
|
+
id: '1',
|
|
38
|
+
title: 'Test Topic',
|
|
39
|
+
createAt: Date.now(),
|
|
40
|
+
updateAt: Date.now(),
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const newState = topicReducer(state, payload);
|
|
45
|
+
|
|
46
|
+
expect(newState['1']).toBeDefined();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should add a ChatTopic object with correct properties', () => {
|
|
50
|
+
const payload: ChatTopicDispatch = {
|
|
51
|
+
type: 'addChatTopic',
|
|
52
|
+
topic: {
|
|
53
|
+
id: '1',
|
|
54
|
+
title: 'Test Topic',
|
|
55
|
+
createAt: Date.now(),
|
|
56
|
+
updateAt: Date.now(),
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const newState = topicReducer(state, payload);
|
|
61
|
+
|
|
62
|
+
expect(newState['1']).toMatchObject(payload.topic);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('updateChatTopic', () => {
|
|
67
|
+
it('should update the ChatTopic object in state', () => {
|
|
68
|
+
const topic: ChatTopic = {
|
|
69
|
+
id: '1',
|
|
70
|
+
title: 'Test Topic',
|
|
71
|
+
createAt: Date.now(),
|
|
72
|
+
updateAt: Date.now(),
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
state['1'] = topic;
|
|
76
|
+
|
|
77
|
+
const payload: ChatTopicDispatch = {
|
|
78
|
+
type: 'updateChatTopic',
|
|
79
|
+
id: '1',
|
|
80
|
+
key: 'title',
|
|
81
|
+
value: 'Updated Topic',
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const newState = topicReducer(state, payload);
|
|
85
|
+
|
|
86
|
+
expect(newState['1'].title).toBe('Updated Topic');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should update the ChatTopic object with correct properties', () => {
|
|
90
|
+
const topic: ChatTopic = {
|
|
91
|
+
id: '1',
|
|
92
|
+
title: 'Test Topic',
|
|
93
|
+
createAt: Date.now() - 1,
|
|
94
|
+
updateAt: Date.now() - 1, // 设定比当前时间前面一点
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
state['1'] = topic;
|
|
98
|
+
|
|
99
|
+
const payload: ChatTopicDispatch = {
|
|
100
|
+
type: 'updateChatTopic',
|
|
101
|
+
id: '1',
|
|
102
|
+
key: 'title',
|
|
103
|
+
value: 'Updated Topic',
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const newState = topicReducer(state, payload);
|
|
107
|
+
|
|
108
|
+
expect(newState['1'].updateAt).toBeGreaterThan(topic.updateAt);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('deleteChatTopic', () => {
|
|
113
|
+
it('should delete the specified ChatTopic object from state', () => {
|
|
114
|
+
const topic: ChatTopic = {
|
|
115
|
+
id: '1',
|
|
116
|
+
title: 'Test Topic',
|
|
117
|
+
createAt: Date.now(),
|
|
118
|
+
updateAt: Date.now(),
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
state['1'] = topic;
|
|
122
|
+
|
|
123
|
+
const payload: ChatTopicDispatch = {
|
|
124
|
+
type: 'deleteChatTopic',
|
|
125
|
+
id: '1',
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const newState = topicReducer(state, payload);
|
|
129
|
+
|
|
130
|
+
expect(newState['1']).toBeUndefined();
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('default', () => {
|
|
135
|
+
it('should return the original state object', () => {
|
|
136
|
+
const payload = {
|
|
137
|
+
type: 'unknown',
|
|
138
|
+
} as unknown as ChatTopicDispatch;
|
|
139
|
+
|
|
140
|
+
const newState = topicReducer(state, payload);
|
|
141
|
+
|
|
142
|
+
expect(newState).toBe(state);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('produce', () => {
|
|
147
|
+
it('should generate immutable state object', () => {
|
|
148
|
+
const payload: ChatTopicDispatch = {
|
|
149
|
+
type: 'addChatTopic',
|
|
150
|
+
topic: {
|
|
151
|
+
id: '1',
|
|
152
|
+
title: 'Test Topic',
|
|
153
|
+
createAt: Date.now(),
|
|
154
|
+
updateAt: Date.now(),
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const newState = topicReducer(state, payload);
|
|
159
|
+
|
|
160
|
+
expect(newState).not.toBe(state);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should not modify the original state object', () => {
|
|
164
|
+
const payload: ChatTopicDispatch = {
|
|
165
|
+
type: 'addChatTopic',
|
|
166
|
+
topic: {
|
|
167
|
+
id: '1',
|
|
168
|
+
title: 'Test Topic',
|
|
169
|
+
createAt: Date.now(),
|
|
170
|
+
updateAt: Date.now(),
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const newState = topicReducer(state, payload);
|
|
175
|
+
|
|
176
|
+
expect(state).toMatchObject({});
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { produce } from 'immer';
|
|
2
|
+
|
|
3
|
+
import { ChatTopic, ChatTopicMap } from '@/types/topic';
|
|
4
|
+
|
|
5
|
+
interface AddChatTopicAction {
|
|
6
|
+
topic: ChatTopic;
|
|
7
|
+
type: 'addChatTopic';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface UpdateChatTopicAction {
|
|
11
|
+
id: string;
|
|
12
|
+
key: keyof ChatTopic;
|
|
13
|
+
type: 'updateChatTopic';
|
|
14
|
+
value: any;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface DeleteChatTopicAction {
|
|
18
|
+
id: string;
|
|
19
|
+
type: 'deleteChatTopic';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type ChatTopicDispatch = AddChatTopicAction | UpdateChatTopicAction | DeleteChatTopicAction;
|
|
23
|
+
|
|
24
|
+
export const topicReducer = (state: ChatTopicMap, payload: ChatTopicDispatch): ChatTopicMap => {
|
|
25
|
+
switch (payload.type) {
|
|
26
|
+
case 'addChatTopic': {
|
|
27
|
+
return produce(state, (draftState) => {
|
|
28
|
+
draftState[payload.topic.id] = payload.topic;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
case 'updateChatTopic': {
|
|
33
|
+
return produce(state, (draftState) => {
|
|
34
|
+
const { key, value, id } = payload;
|
|
35
|
+
|
|
36
|
+
if (!draftState[id]) return;
|
|
37
|
+
|
|
38
|
+
const topic = draftState[id];
|
|
39
|
+
|
|
40
|
+
if (value !== undefined) {
|
|
41
|
+
// @ts-ignore
|
|
42
|
+
topic[key] = value;
|
|
43
|
+
|
|
44
|
+
topic.updateAt = Date.now();
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
case 'deleteChatTopic': {
|
|
50
|
+
return produce(state, (draftState) => {
|
|
51
|
+
delete draftState[payload.id];
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
default: {
|
|
56
|
+
return state;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
@@ -12,10 +12,14 @@ export const currentChats = (s: SessionStore): ChatMessage[] => {
|
|
|
12
12
|
const session = sessionSelectors.currentSession(s);
|
|
13
13
|
if (!session) return [];
|
|
14
14
|
|
|
15
|
-
return organizeChats(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
return organizeChats(
|
|
16
|
+
session,
|
|
17
|
+
{
|
|
18
|
+
assistant: agentSelectors.currentAgentAvatar(s),
|
|
19
|
+
user: useSettings.getState().settings.avatar || DEFAULT_USER_AVATAR,
|
|
20
|
+
},
|
|
21
|
+
s.activeTopicId,
|
|
22
|
+
);
|
|
19
23
|
};
|
|
20
24
|
|
|
21
25
|
export const systemRoleSel = (s: SessionStore): string => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { currentChats } from './chat';
|
|
2
2
|
import { chatsTokenCount, systemRoleTokenCount, totalTokenCount } from './token';
|
|
3
|
+
import { currentTopics } from './topic';
|
|
3
4
|
|
|
4
5
|
export const chatSelectors = {
|
|
5
6
|
chatsTokenCount,
|
|
@@ -7,3 +8,7 @@ export const chatSelectors = {
|
|
|
7
8
|
systemRoleTokenCount,
|
|
8
9
|
totalTokenCount,
|
|
9
10
|
};
|
|
11
|
+
|
|
12
|
+
export const topicSelectors = {
|
|
13
|
+
currentTopics,
|
|
14
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// 展示在聊天框中的消息
|
|
2
|
+
import { SessionStore, sessionSelectors } from '@/store/session';
|
|
3
|
+
import { ChatTopic } from '@/types/topic';
|
|
4
|
+
|
|
5
|
+
export const currentTopics = (s: SessionStore): ChatTopic[] => {
|
|
6
|
+
const session = sessionSelectors.currentSession(s);
|
|
7
|
+
if (!session) return [];
|
|
8
|
+
|
|
9
|
+
const topics = Object.values(session.topics || {});
|
|
10
|
+
|
|
11
|
+
// 按时间倒序
|
|
12
|
+
const favTopics = topics.filter((t) => t.favorite).sort((a, b) => b.updateAt - a.updateAt);
|
|
13
|
+
const defaultTopics = topics.filter((t) => !t.favorite).sort((a, b) => b.updateAt - a.updateAt);
|
|
14
|
+
|
|
15
|
+
return [...favTopics, ...defaultTopics];
|
|
16
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { beforeEach } from 'vitest';
|
|
1
|
+
import { beforeEach, describe } from 'vitest';
|
|
2
2
|
|
|
3
|
+
import { ChatMessage } from '@/types/chatMessage';
|
|
3
4
|
import { LobeAgentSession } from '@/types/session';
|
|
4
5
|
|
|
5
6
|
import { organizeChats } from './utils';
|
|
@@ -261,4 +262,83 @@ describe('organizeChats', () => {
|
|
|
261
262
|
},
|
|
262
263
|
]);
|
|
263
264
|
});
|
|
265
|
+
|
|
266
|
+
const avatar = {
|
|
267
|
+
assistant: 'assistant-avatar',
|
|
268
|
+
user: 'user-avatar',
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
it('should organize chats in chronological order when topicId is not provided', () => {
|
|
272
|
+
const result = organizeChats(session, avatar);
|
|
273
|
+
|
|
274
|
+
expect(result.length).toBe(3);
|
|
275
|
+
expect(result[0].id).toBe('1');
|
|
276
|
+
expect(result[1].id).toBe('2');
|
|
277
|
+
expect(result[2].id).toBe('3');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('should only return chats with specified topicId when topicId is provided', () => {
|
|
281
|
+
const result = organizeChats(session, avatar, 'topic-id');
|
|
282
|
+
|
|
283
|
+
expect(result.length).toBe(0);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
describe('user Meta', () => {
|
|
287
|
+
it('should return correct meta for user role', () => {
|
|
288
|
+
const result = organizeChats(session, avatar);
|
|
289
|
+
const meta = result[1].meta;
|
|
290
|
+
|
|
291
|
+
expect(meta.avatar).toBe(avatar.user);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('should return correct meta for assistant role', () => {
|
|
295
|
+
const result = organizeChats(session, avatar);
|
|
296
|
+
const meta = result[0].meta;
|
|
297
|
+
|
|
298
|
+
expect(meta.avatar).toBe(avatar.assistant);
|
|
299
|
+
expect(meta.title).toBeUndefined();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
describe('should return correct meta for function role', () => {
|
|
303
|
+
it('找不到插件', () => {
|
|
304
|
+
const message = {
|
|
305
|
+
id: '4',
|
|
306
|
+
createAt: 1927785600004,
|
|
307
|
+
updateAt: 1927785600004,
|
|
308
|
+
role: 'function',
|
|
309
|
+
function_call: {
|
|
310
|
+
name: 'plugin-name',
|
|
311
|
+
},
|
|
312
|
+
} as ChatMessage;
|
|
313
|
+
|
|
314
|
+
session.chats[message.id] = message;
|
|
315
|
+
|
|
316
|
+
const result = organizeChats(session, avatar);
|
|
317
|
+
const meta = result[3].meta;
|
|
318
|
+
|
|
319
|
+
expect(meta.avatar).toBe('🧩');
|
|
320
|
+
expect(meta.title).toBe('plugin-unknown');
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('找到的插件', () => {
|
|
324
|
+
const message = {
|
|
325
|
+
id: '4',
|
|
326
|
+
createAt: 1927785600004,
|
|
327
|
+
updateAt: 1927785600004,
|
|
328
|
+
role: 'function',
|
|
329
|
+
function_call: {
|
|
330
|
+
name: 'realtimeWeather',
|
|
331
|
+
},
|
|
332
|
+
} as ChatMessage;
|
|
333
|
+
|
|
334
|
+
session.chats[message.id] = message;
|
|
335
|
+
|
|
336
|
+
const result = organizeChats(session, avatar);
|
|
337
|
+
const meta = result[3].meta;
|
|
338
|
+
|
|
339
|
+
expect(meta.avatar).toBe('☂️');
|
|
340
|
+
expect(meta.title).toBe('realtimeWeather');
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
});
|
|
264
344
|
});
|
|
@@ -5,6 +5,7 @@ import { LobeAgentSession } from '@/types/session';
|
|
|
5
5
|
export const organizeChats = (
|
|
6
6
|
session: LobeAgentSession,
|
|
7
7
|
avatar: { assistant: string; user: string },
|
|
8
|
+
topicId?: string,
|
|
8
9
|
) => {
|
|
9
10
|
const getMeta = (message: ChatMessage) => {
|
|
10
11
|
switch (message.role) {
|
|
@@ -38,8 +39,13 @@ export const organizeChats = (
|
|
|
38
39
|
const basic = Object.values<ChatMessage>(session.chats)
|
|
39
40
|
// 首先按照时间顺序排序,越早的在越前面
|
|
40
41
|
.sort((pre, next) => pre.createAt - next.createAt)
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
.filter((m) => {
|
|
43
|
+
// 过滤掉包含 topicId 的消息,有主题的消息不应该出现在聊天框中
|
|
44
|
+
if (!topicId) return !m.topicId;
|
|
45
|
+
|
|
46
|
+
// 或者当话题 id 一致时,再展示话题
|
|
47
|
+
return m.topicId === topicId;
|
|
48
|
+
})
|
|
43
49
|
// 映射头像关系
|
|
44
50
|
.map((m) => {
|
|
45
51
|
return {
|
|
@@ -3,6 +3,7 @@ import { produce } from 'immer';
|
|
|
3
3
|
import { ChatMessageMap } from '@/types/chatMessage';
|
|
4
4
|
import { MetaData } from '@/types/meta';
|
|
5
5
|
import { LobeAgentConfig, LobeAgentSession, LobeSessions } from '@/types/session';
|
|
6
|
+
import { ChatTopicMap } from '@/types/topic';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* @title 添加会话
|
|
@@ -37,6 +38,19 @@ interface UpdateSessionChat {
|
|
|
37
38
|
type: 'updateSessionChat';
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
/**
|
|
42
|
+
* @title 更新会话聊天上下文
|
|
43
|
+
*/
|
|
44
|
+
interface UpdateSessionTopic {
|
|
45
|
+
/**
|
|
46
|
+
* 会话 ID
|
|
47
|
+
*/
|
|
48
|
+
id: string;
|
|
49
|
+
topics: ChatTopicMap;
|
|
50
|
+
|
|
51
|
+
type: 'updateSessionTopic';
|
|
52
|
+
}
|
|
53
|
+
|
|
40
54
|
interface UpdateSessionMeta {
|
|
41
55
|
id: string;
|
|
42
56
|
key: keyof MetaData;
|
|
@@ -55,7 +69,8 @@ export type SessionDispatch =
|
|
|
55
69
|
| UpdateSessionChat
|
|
56
70
|
| RemoveSession
|
|
57
71
|
| UpdateSessionMeta
|
|
58
|
-
| UpdateSessionAgentConfig
|
|
72
|
+
| UpdateSessionAgentConfig
|
|
73
|
+
| UpdateSessionTopic;
|
|
59
74
|
|
|
60
75
|
export const sessionsReducer = (state: LobeSessions, payload: SessionDispatch): LobeSessions => {
|
|
61
76
|
switch (payload.type) {
|
|
@@ -96,6 +111,15 @@ export const sessionsReducer = (state: LobeSessions, payload: SessionDispatch):
|
|
|
96
111
|
});
|
|
97
112
|
}
|
|
98
113
|
|
|
114
|
+
case 'updateSessionTopic': {
|
|
115
|
+
return produce(state, (draft) => {
|
|
116
|
+
const chat = draft[payload.id];
|
|
117
|
+
if (!chat) return;
|
|
118
|
+
|
|
119
|
+
chat.topics = payload.topics;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
99
123
|
case 'updateSessionConfig': {
|
|
100
124
|
return produce(state, (draft) => {
|
|
101
125
|
const { id, config } = payload;
|
package/src/types/topic.ts
CHANGED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { Icon } from '@lobehub/ui';
|
|
2
|
-
import { createStyles } from 'antd-style';
|
|
3
|
-
import { LucideChevronRight, LucideIcon } from 'lucide-react';
|
|
4
|
-
import { memo } from 'react';
|
|
5
|
-
import { Flexbox } from 'react-layout-kit';
|
|
6
|
-
|
|
7
|
-
const useStyles = createStyles(({ css, token }) => ({
|
|
8
|
-
container: css`
|
|
9
|
-
background: ${token.colorFillQuaternary};
|
|
10
|
-
border-radius: 6px;
|
|
11
|
-
`,
|
|
12
|
-
split: css`
|
|
13
|
-
border-bottom: 1px solid ${token.colorSplit};
|
|
14
|
-
`,
|
|
15
|
-
}));
|
|
16
|
-
export interface ConfigItem {
|
|
17
|
-
icon: LucideIcon;
|
|
18
|
-
label: string;
|
|
19
|
-
value?: string | number;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export type ConfigCellProps = ConfigItem;
|
|
23
|
-
|
|
24
|
-
export const ConfigCell = memo<ConfigCellProps>(({ icon, label, value }) => {
|
|
25
|
-
const { styles } = useStyles();
|
|
26
|
-
return (
|
|
27
|
-
<Flexbox
|
|
28
|
-
className={styles.container}
|
|
29
|
-
distribution={'space-between'}
|
|
30
|
-
horizontal
|
|
31
|
-
padding={'10px 12px'}
|
|
32
|
-
>
|
|
33
|
-
<Flexbox gap={8} horizontal>
|
|
34
|
-
<Icon icon={icon} />
|
|
35
|
-
<Flexbox>{label}</Flexbox>
|
|
36
|
-
</Flexbox>
|
|
37
|
-
{value ?? <Icon icon={LucideChevronRight} />}
|
|
38
|
-
</Flexbox>
|
|
39
|
-
);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
export interface CellGroupProps {
|
|
43
|
-
items: ConfigItem[];
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export const ConfigCellGroup = memo<CellGroupProps>(({ items }) => {
|
|
47
|
-
const { styles } = useStyles();
|
|
48
|
-
|
|
49
|
-
return (
|
|
50
|
-
<Flexbox className={styles.container}>
|
|
51
|
-
{items.map(({ label, icon, value }, index) => (
|
|
52
|
-
<Flexbox
|
|
53
|
-
className={items.length === index + 1 ? undefined : styles.split}
|
|
54
|
-
distribution={'space-between'}
|
|
55
|
-
horizontal
|
|
56
|
-
key={label}
|
|
57
|
-
padding={'10px 12px'}
|
|
58
|
-
>
|
|
59
|
-
<Flexbox gap={8} horizontal>
|
|
60
|
-
<Icon icon={icon} />
|
|
61
|
-
<Flexbox>{label}</Flexbox>
|
|
62
|
-
</Flexbox>
|
|
63
|
-
{value ?? <Icon icon={LucideChevronRight} />}
|
|
64
|
-
</Flexbox>
|
|
65
|
-
))}
|
|
66
|
-
</Flexbox>
|
|
67
|
-
);
|
|
68
|
-
});
|