@lobehub/chat 0.18.2 → 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.
Files changed (31) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/package.json +1 -1
  3. package/src/locales/default/common.ts +4 -2
  4. package/src/pages/chat/[id]/Config/SideBar.tsx +10 -6
  5. package/src/pages/chat/[id]/Config/Topic.tsx +27 -0
  6. package/src/pages/chat/[id]/Config/TopicItem.tsx +80 -0
  7. package/src/pages/chat/[id]/Conversation/Input/index.tsx +7 -4
  8. package/src/pages/chat/[id]/Header.tsx +14 -10
  9. package/src/pages/chat/layout.tsx +10 -4
  10. package/src/prompts/chat.ts +19 -0
  11. package/src/store/session/selectors.ts +1 -1
  12. package/src/store/session/slices/agentConfig/selectors.ts +4 -0
  13. package/src/store/session/slices/chat/actions/index.ts +21 -0
  14. package/src/store/session/slices/chat/{action.ts → actions/message.ts} +34 -32
  15. package/src/store/session/slices/chat/actions/topic.ts +98 -0
  16. package/src/store/session/slices/chat/index.ts +1 -1
  17. package/src/store/session/slices/chat/initialState.ts +2 -5
  18. package/src/store/session/slices/chat/{messageReducer.test.ts → reducers/message.test.ts} +93 -1
  19. package/src/store/session/slices/chat/{messageReducer.ts → reducers/message.ts} +16 -1
  20. package/src/store/session/slices/chat/reducers/topic.test.ts +179 -0
  21. package/src/store/session/slices/chat/reducers/topic.ts +59 -0
  22. package/src/store/session/slices/chat/selectors/chat.ts +8 -4
  23. package/src/store/session/slices/chat/selectors/index.ts +5 -0
  24. package/src/store/session/slices/chat/selectors/topic.ts +16 -0
  25. package/src/store/session/slices/chat/selectors/utils.test.ts +82 -2
  26. package/src/store/session/slices/chat/selectors/utils.ts +8 -2
  27. package/src/store/session/slices/session/reducers/session.ts +25 -1
  28. package/src/types/chatMessage.ts +4 -2
  29. package/src/types/session.ts +2 -1
  30. package/src/types/topic.ts +8 -0
  31. package/src/pages/chat/[id]/Config/ConfigCell.tsx +0 -68
package/CHANGELOG.md CHANGED
@@ -2,6 +2,65 @@
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
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
30
+ ## [Version 0.19.0](https://github.com/lobehub/lobe-chat/compare/v0.18.2...v0.19.0)
31
+
32
+ <sup>Released on **2023-07-24**</sup>
33
+
34
+ #### ♻ Code Refactoring
35
+
36
+ - **misc**: 将 message reducer 提取到独立文件夹中, 清理无用代码实现.
37
+
38
+ #### ✨ Features
39
+
40
+ - **misc**: 数据结构层完成 topic 模型改造.
41
+
42
+ <br/>
43
+
44
+ <details>
45
+ <summary><kbd>Improvements and Fixes</kbd></summary>
46
+
47
+ #### Code refactoring
48
+
49
+ - **misc**: 将 message reducer 提取到独立文件夹中 ([64f40ca](https://github.com/lobehub/lobe-chat/commit/64f40ca))
50
+ - **misc**: 清理无用代码实现 ([3655b60](https://github.com/lobehub/lobe-chat/commit/3655b60))
51
+
52
+ #### What's improved
53
+
54
+ - **misc**: 数据结构层完成 topic 模型改造 ([99fa2a6](https://github.com/lobehub/lobe-chat/commit/99fa2a6))
55
+
56
+ </details>
57
+
58
+ <div align="right">
59
+
60
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
61
+
62
+ </div>
63
+
5
64
  ### [Version 0.18.2](https://github.com/lobehub/lobe-chat/compare/v0.18.1...v0.18.2)
6
65
 
7
66
  <sup>Released on **2023-07-24**</sup>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "0.18.2",
3
+ "version": "0.20.0",
4
4
  "description": "Lobe Chat is an open-source chatbot client using LangChain, Typescript and Next.js",
5
5
  "keywords": [
6
6
  "chatbot",
@@ -4,8 +4,6 @@ export default {
4
4
  agentModel: '模型',
5
5
  agentProfile: '助手信息',
6
6
  archive: '归档',
7
- archiveCurrentMessages: '归档当前会话',
8
- archiveSearchPlaceholder: '搜索归档对话...',
9
7
  autoGenerate: '自动补全',
10
8
  autoGenerateTooltip: '基于提示词自动补全助手描述',
11
9
  cancel: '取消',
@@ -28,6 +26,10 @@ export default {
28
26
  setting: '设置',
29
27
  share: '分享',
30
28
  tokenDetail: '系统设定: {{systemRoleToken}} 历史消息: {{chatsToken}}',
29
+ topic: {
30
+ saveCurrentMessages: '将当前会话保存为话题',
31
+ searchPlaceholder: '搜索话题...',
32
+ },
31
33
  updateAgent: '更新助理信息',
32
34
  updatePrompt: '更新提示词',
33
35
  };
@@ -1,11 +1,12 @@
1
1
  import { ActionIcon, DraggablePanelBody, EditableMessage, SearchBar } from '@lobehub/ui';
2
2
  import { createStyles } from 'antd-style';
3
- import { ChevronRight } from 'lucide-react';
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((s) => [s.updateAgentConfig], shallow);
46
- const systemRole = useSessionStore(agentSelectors.currentAgentSystemRole, shallow);
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={ChevronRight}
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 }}>
80
- <SearchBar placeholder={t('archiveSearchPlaceholder')} />
82
+ <Flexbox gap={12} style={{ padding: 16 }}>
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;
@@ -1,6 +1,6 @@
1
1
  import { ChatInputArea, DraggablePanel, Icon, Tooltip } from '@lobehub/ui';
2
2
  import { Button } from 'antd';
3
- import { Archive } from 'lucide-react';
3
+ import { LucideGalleryVerticalEnd } from 'lucide-react';
4
4
  import { memo, useMemo, useState } from 'react';
5
5
  import { useTranslation } from 'react-i18next';
6
6
  import { shallow } from 'zustand/shallow';
@@ -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((s) => [s.createOrSendMsg], shallow);
21
+ const [sendMessage, saveToTopic] = useSessionStore(
22
+ (s) => [s.createOrSendMsg, s.saveToTopic],
23
+ shallow,
24
+ );
22
25
 
23
26
  const footer = useMemo(
24
27
  () => (
25
- <Tooltip title={t('archiveCurrentMessages')}>
26
- <Button icon={<Icon icon={Archive} />} />
28
+ <Tooltip title={t('topic.saveCurrentMessages')}>
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 [meta, id, modle] = useSessionStore(
16
- (s) => [agentSelectors.currentAgentMeta(s), s.activeId, agentSelectors.currentAgentModel(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={meta?.avatar}
30
- background={meta?.backgroundColor}
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={meta?.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 (typeof id === 'string' && hasRehydrated) {
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
+ });
@@ -1,3 +1,3 @@
1
1
  export { agentSelectors } from './slices/agentConfig';
2
- export { chatSelectors } from './slices/chat';
2
+ export { chatSelectors, topicSelectors } from './slices/chat';
3
3
  export { sessionSelectors } from './slices/session';
@@ -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 './messageReducer';
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 ChatAction {
17
+ export interface ChatMessageAction {
18
18
  /**
19
19
  * 清除消息
20
20
  */
@@ -44,11 +44,7 @@ export interface ChatAction {
44
44
  assistantMessageId: string,
45
45
  withPlugin?: boolean,
46
46
  ) => Promise<{ isFunctionCall: boolean; output: string }>;
47
- /**
48
- * 处理消息编辑
49
- * @param messageId - 消息 ID,可选
50
- */
51
- handleMessageEditing: (messageId: string | undefined) => void;
47
+
52
48
  /**
53
49
  * 实际获取 AI 响应
54
50
  *
@@ -61,6 +57,7 @@ export interface ChatAction {
61
57
  * @param id - 消息 ID
62
58
  */
63
59
  resendMessage: (id: string) => Promise<void>;
60
+
64
61
  /**
65
62
  * 发送消息
66
63
  * @param text - 消息文本
@@ -68,14 +65,20 @@ export interface ChatAction {
68
65
  sendMessage: (text: string) => Promise<void>;
69
66
  }
70
67
 
71
- export const createChatSlice: StateCreator<
68
+ export const chatMessage: StateCreator<
72
69
  SessionStore,
73
70
  [['zustand/devtools', never]],
74
71
  [],
75
- ChatAction
72
+ ChatMessageAction
76
73
  > = (set, get) => ({
77
74
  clearMessage: () => {
78
- get().dispatchMessage({ type: 'resetMessages' });
75
+ const { dispatchMessage, activeTopicId, dispatchTopic } = get();
76
+
77
+ dispatchMessage({ topicId: activeTopicId, type: 'resetMessages' });
78
+
79
+ if (activeTopicId) {
80
+ dispatchTopic({ id: activeTopicId, type: 'deleteChatTopic' });
81
+ }
79
82
  },
80
83
 
81
84
  createOrSendMsg: async (message) => {
@@ -158,12 +161,8 @@ export const createChatSlice: StateCreator<
158
161
  return { isFunctionCall, output };
159
162
  },
160
163
 
161
- handleMessageEditing: (messageId) => {
162
- set({ editingMessageId: messageId });
163
- },
164
-
165
164
  realFetchAIResponse: async (messages, userMessageId) => {
166
- const { dispatchMessage, generateMessage } = get();
165
+ const { dispatchMessage, generateMessage, activeTopicId } = get();
167
166
 
168
167
  // 添加 systemRole
169
168
  const { systemRole, model } = agentSelectors.currentAgentConfigSafe(get());
@@ -173,49 +172,46 @@ export const createChatSlice: StateCreator<
173
172
 
174
173
  // 再添加一个空的信息用于放置 ai 响应,注意顺序不能反
175
174
  // 因为如果顺序反了,messages 中将包含新增的 ai message
176
- const assistantId = nanoid();
175
+ const mid = nanoid();
177
176
 
178
177
  dispatchMessage({
179
- id: assistantId,
178
+ id: mid,
180
179
  message: LOADING_FLAT,
181
180
  parentId: userMessageId,
182
181
  role: 'assistant',
183
182
  type: 'addMessage',
184
183
  });
185
184
 
185
+ // 如果有 activeTopicId,则添加 topicId
186
+ if (activeTopicId) {
187
+ dispatchMessage({ id: mid, key: 'topicId', type: 'updateMessage', value: activeTopicId });
188
+ }
189
+
186
190
  // 为模型添加 fromModel 的额外信息
187
- dispatchMessage({
188
- id: assistantId,
189
- key: 'fromModel',
190
- type: 'updateMessageExtra',
191
- value: model,
192
- });
191
+ dispatchMessage({ id: mid, key: 'fromModel', type: 'updateMessageExtra', value: model });
193
192
 
194
193
  // 生成 ai message
195
- const { output, isFunctionCall } = await generateMessage(messages, assistantId);
194
+ const { output, isFunctionCall } = await generateMessage(messages, mid);
196
195
 
197
196
  // 如果是 function,则发送函数调用方法
198
197
  if (isFunctionCall) {
199
198
  const { function_call } = JSON.parse(output);
200
199
 
201
200
  dispatchMessage({
202
- id: assistantId,
201
+ id: mid,
203
202
  key: 'function_call',
204
203
  type: 'updateMessage',
205
204
  value: function_call,
206
205
  });
207
206
 
208
207
  await generateMessage(
209
- [
210
- ...messages,
211
- { content: '', function_call, id: assistantId, role: 'assistant' } as ChatMessage,
212
- ],
213
- assistantId,
208
+ [...messages, { content: '', function_call, id: mid, role: 'assistant' } as ChatMessage],
209
+ mid,
214
210
  true,
215
211
  );
216
212
 
217
213
  dispatchMessage({
218
- id: assistantId,
214
+ id: mid,
219
215
  key: 'role',
220
216
  type: 'updateMessage',
221
217
  value: 'assistant',
@@ -251,13 +247,19 @@ export const createChatSlice: StateCreator<
251
247
  },
252
248
 
253
249
  sendMessage: async (message) => {
254
- const { dispatchMessage, realFetchAIResponse, autocompleteSessionAgentMeta } = get();
250
+ const { dispatchMessage, realFetchAIResponse, autocompleteSessionAgentMeta, activeTopicId } =
251
+ get();
255
252
  const session = sessionSelectors.currentSession(get());
256
253
  if (!session || !message) return;
257
254
 
258
255
  const userId = nanoid();
259
256
  dispatchMessage({ id: userId, message, role: 'user', type: 'addMessage' });
260
257
 
258
+ // 如果有 activeTopicId,则添加 topicId
259
+ if (activeTopicId) {
260
+ dispatchMessage({ id: userId, key: 'topicId', type: 'updateMessage', value: activeTopicId });
261
+ }
262
+
261
263
  // 先拿到当前的 messages
262
264
  const messages = chatSelectors.currentChats(get());
263
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,3 +1,3 @@
1
- export * from './action';
1
+ export * from './actions';
2
2
  export * from './initialState';
3
3
  export * from './selectors';