@lobehub/chat 0.149.0 → 0.149.2

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 (55) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/locales/ar/chat.json +4 -1
  3. package/locales/ar/modelProvider.json +1 -3
  4. package/locales/bg-BG/chat.json +4 -1
  5. package/locales/bg-BG/modelProvider.json +1 -3
  6. package/locales/de-DE/chat.json +4 -1
  7. package/locales/de-DE/modelProvider.json +1 -3
  8. package/locales/en-US/chat.json +7 -4
  9. package/locales/en-US/modelProvider.json +1 -3
  10. package/locales/es-ES/chat.json +4 -1
  11. package/locales/es-ES/modelProvider.json +1 -3
  12. package/locales/fr-FR/chat.json +7 -4
  13. package/locales/fr-FR/modelProvider.json +1 -3
  14. package/locales/it-IT/chat.json +4 -1
  15. package/locales/it-IT/modelProvider.json +16 -17
  16. package/locales/ja-JP/chat.json +4 -1
  17. package/locales/ja-JP/modelProvider.json +14 -16
  18. package/locales/ko-KR/chat.json +4 -1
  19. package/locales/ko-KR/modelProvider.json +1 -3
  20. package/locales/nl-NL/chat.json +4 -1
  21. package/locales/nl-NL/modelProvider.json +1 -3
  22. package/locales/pl-PL/chat.json +4 -1
  23. package/locales/pl-PL/modelProvider.json +1 -3
  24. package/locales/pt-BR/chat.json +4 -1
  25. package/locales/pt-BR/modelProvider.json +1 -3
  26. package/locales/ru-RU/chat.json +4 -1
  27. package/locales/ru-RU/modelProvider.json +1 -3
  28. package/locales/tr-TR/chat.json +4 -1
  29. package/locales/tr-TR/modelProvider.json +1 -3
  30. package/locales/vi-VN/chat.json +4 -1
  31. package/locales/vi-VN/modelProvider.json +1 -3
  32. package/locales/zh-CN/chat.json +4 -1
  33. package/locales/zh-CN/modelProvider.json +1 -3
  34. package/locales/zh-TW/chat.json +4 -1
  35. package/locales/zh-TW/modelProvider.json +1 -3
  36. package/package.json +1 -1
  37. package/src/app/chat/(desktop)/features/ChatInput/Footer/SendMore.tsx +117 -0
  38. package/src/app/chat/(desktop)/features/ChatInput/Footer/index.tsx +9 -56
  39. package/src/app/chat/(desktop)/features/ChatInput/TextArea.tsx +1 -1
  40. package/src/app/chat/(desktop)/features/HotKeys.tsx +3 -3
  41. package/src/app/chat/features/TopicListContent/Topic/TopicContent.tsx +15 -6
  42. package/src/components/HotKeys/index.tsx +14 -18
  43. package/src/const/hotkeys.ts +1 -1
  44. package/src/features/ChatInput/ActionBar/Clear.tsx +3 -3
  45. package/src/features/ChatInput/Topic/index.tsx +3 -3
  46. package/src/features/Conversation/Actions/Assistant.tsx +1 -1
  47. package/src/features/Conversation/Error/OllamaBizError/SetupGuide.tsx +159 -70
  48. package/src/locales/default/chat.ts +4 -1
  49. package/src/locales/default/modelProvider.ts +1 -3
  50. package/src/store/chat/slices/message/action.ts +15 -0
  51. package/src/store/chat/slices/topic/action.test.ts +8 -4
  52. package/src/store/chat/slices/topic/action.ts +70 -28
  53. package/src/store/chat/slices/topic/initialState.ts +2 -1
  54. package/src/store/chat/slices/topic/reducer.test.ts +3 -5
  55. package/src/store/chat/slices/topic/reducer.ts +9 -9
@@ -6,7 +6,7 @@ import { useHotkeys } from 'react-hotkeys-hook';
6
6
  import { useTranslation } from 'react-i18next';
7
7
 
8
8
  import HotKeys from '@/components/HotKeys';
9
- import { PREFIX_KEY, SAVE_TOPIC_KEY } from '@/const/hotkeys';
9
+ import { ALT_KEY, SAVE_TOPIC_KEY } from '@/const/hotkeys';
10
10
  import { useActionSWR } from '@/libs/swr';
11
11
  import { useChatStore } from '@/store/chat';
12
12
 
@@ -24,7 +24,7 @@ const SaveTopic = memo<{ mobile?: boolean }>(({ mobile }) => {
24
24
  const iconRender: any = mobile ? icon : <Icon icon={icon} />;
25
25
  const desc = t(hasTopic ? 'topic.openNewTopic' : 'topic.saveCurrentMessages');
26
26
 
27
- const hotkeys = [PREFIX_KEY, SAVE_TOPIC_KEY].join('+');
27
+ const hotkeys = [ALT_KEY, SAVE_TOPIC_KEY].join('+');
28
28
 
29
29
  useHotkeys(hotkeys, () => mutate(), {
30
30
  enableOnFormTags: true,
@@ -32,7 +32,7 @@ const SaveTopic = memo<{ mobile?: boolean }>(({ mobile }) => {
32
32
  });
33
33
 
34
34
  return (
35
- <Tooltip title={<HotKeys desc={desc} keys={hotkeys} />}>
35
+ <Tooltip title={<HotKeys desc={desc} inverseTheme keys={hotkeys} />}>
36
36
  <Render aria-label={desc} icon={iconRender} loading={isValidating} onClick={() => mutate()} />
37
37
  </Tooltip>
38
38
  );
@@ -27,7 +27,7 @@ export const AssistantActionsBar: RenderAction = memo(({ id, onActionClick, erro
27
27
  delAndRegenerate,
28
28
  del,
29
29
  ]}
30
- items={[regenerate, copy]}
30
+ items={[edit, copy]}
31
31
  onActionClick={onActionClick}
32
32
  type="ghost"
33
33
  />
@@ -1,19 +1,28 @@
1
- import { Highlighter, Snippet } from '@lobehub/ui';
2
- import { Tab, Tabs } from '@lobehub/ui/mdx';
1
+ import { Highlighter, Snippet, TabsNav } from '@lobehub/ui';
3
2
  import { Steps } from 'antd';
4
3
  import { createStyles } from 'antd-style';
5
4
  import Link from 'next/link';
5
+ import { readableColor } from 'polished';
6
6
  import { memo } from 'react';
7
7
  import { Trans, useTranslation } from 'react-i18next';
8
8
  import { Flexbox } from 'react-layout-kit';
9
9
 
10
- const useStyles = createStyles(({ css, prefixCls }) => ({
10
+ const useStyles = createStyles(({ css, prefixCls, token }) => ({
11
11
  steps: css`
12
+ margin-top: 32px;
12
13
  &.${prefixCls}-steps-small .${prefixCls}-steps-item-title {
13
14
  margin-bottom: 16px;
14
15
  font-size: 16px;
15
16
  font-weight: bold;
16
17
  }
18
+
19
+ .${prefixCls}-steps-item-description {
20
+ margin-bottom: 24px;
21
+ }
22
+
23
+ .${prefixCls}-steps-icon {
24
+ color: ${readableColor(token.colorPrimary)} !important;
25
+ }
17
26
  `,
18
27
  }));
19
28
 
@@ -21,29 +30,91 @@ const SetupGuide = memo(() => {
21
30
  const { styles } = useStyles();
22
31
  const { t } = useTranslation('modelProvider');
23
32
  return (
24
- <Flexbox paddingBlock={8}>
25
- <Steps
26
- className={styles.steps}
27
- direction={'vertical'}
28
- items={[
29
- {
30
- description: (
31
- <Flexbox>
32
- {t('ollama.setup.install.description')}
33
- <Tabs items={['macOS', t('ollama.setup.install.windowsTab'), 'Linux', 'Docker']}>
34
- <Tab>
35
- <Trans i18nKey={'ollama.setup.install.macos'} ns={'modelProvider'}>
36
- <Link href={'https://ollama.com/download'}>下载 Ollama for macOS</Link>
37
- 并解压。
33
+ <TabsNav
34
+ items={[
35
+ {
36
+ children: (
37
+ <Steps
38
+ className={styles.steps}
39
+ direction={'vertical'}
40
+ items={[
41
+ {
42
+ description: (
43
+ <Trans i18nKey={'ollama.setup.install.description'} ns={'modelProvider'}>
44
+ 请确认你已经开启 Ollama ,如果没有安装 Ollama ,请前往官网
45
+ <Link href={'https://ollama.com/download'}>下载</Link>
38
46
  </Trans>
39
- </Tab>
40
- <Tab>
41
- <Trans i18nKey={'ollama.setup.install.windows'} ns={'modelProvider'}>
42
- <Link href={'https://ollama.com/download'}>下载 Ollama for macOS</Link>
43
- 并解压。
47
+ ),
48
+ status: 'process',
49
+ title: t('ollama.setup.install.title'),
50
+ },
51
+ {
52
+ description: (
53
+ <Flexbox gap={8}>
54
+ {t('ollama.setup.cors.description')}
55
+
56
+ <Flexbox gap={8}>
57
+ {t('ollama.setup.cors.macos')}
58
+ <Snippet language={'bash'}>
59
+ {/* eslint-disable-next-line react/no-unescaped-entities */}
60
+ launchctl setenv OLLAMA_ORIGINS "*"
61
+ </Snippet>
62
+ {t('ollama.setup.cors.reboot')}
63
+ </Flexbox>
64
+ </Flexbox>
65
+ ),
66
+ status: 'process',
67
+ title: t('ollama.setup.cors.title'),
68
+ },
69
+ ]}
70
+ size={'small'}
71
+ />
72
+ ),
73
+ key: 'macos',
74
+ label: 'macOS',
75
+ },
76
+ {
77
+ children: (
78
+ <Steps
79
+ className={styles.steps}
80
+ direction={'vertical'}
81
+ items={[
82
+ {
83
+ description: (
84
+ <Trans i18nKey={'ollama.setup.install.description'} ns={'modelProvider'}>
85
+ 请确认你已经开启 Ollama ,如果没有安装 Ollama ,请前往官网
86
+ <Link href={'https://ollama.com/download'}>下载</Link>
44
87
  </Trans>
45
- </Tab>
46
- <Tab>
88
+ ),
89
+ status: 'process',
90
+ title: t('ollama.setup.install.title'),
91
+ },
92
+ {
93
+ description: (
94
+ <Flexbox gap={8}>
95
+ {t('ollama.setup.cors.description')}
96
+ <div>{t('ollama.setup.cors.windows')}</div>
97
+ <div>{t('ollama.setup.cors.reboot')}</div>
98
+ </Flexbox>
99
+ ),
100
+ status: 'process',
101
+ title: t('ollama.setup.cors.title'),
102
+ },
103
+ ]}
104
+ size={'small'}
105
+ />
106
+ ),
107
+ key: 'windows',
108
+ label: t('ollama.setup.install.windowsTab'),
109
+ },
110
+ {
111
+ children: (
112
+ <Steps
113
+ className={styles.steps}
114
+ direction={'vertical'}
115
+ items={[
116
+ {
117
+ description: (
47
118
  <Flexbox gap={8}>
48
119
  {t('ollama.setup.install.linux.command')}
49
120
  <Snippet language={'bash'}>
@@ -59,43 +130,16 @@ const SetupGuide = memo(() => {
59
130
  </Trans>
60
131
  </div>
61
132
  </Flexbox>
62
- </Tab>
63
- <Tab>
133
+ ),
134
+ status: 'process',
135
+ title: t('ollama.setup.install.title'),
136
+ },
137
+ {
138
+ description: (
64
139
  <Flexbox gap={8}>
65
- {t('ollama.setup.install.docker')}
66
- <Snippet language={'bash'}>docker pull ollama/ollama</Snippet>
67
- </Flexbox>
68
- </Tab>
69
- </Tabs>
70
- </Flexbox>
71
- ),
72
- status: 'process',
73
- title: t('ollama.setup.install.title'),
74
- },
75
- {
76
- description: (
77
- <Flexbox>
78
- {t('ollama.setup.cors.description')}
140
+ <div>{t('ollama.setup.cors.description')}</div>
79
141
 
80
- <Tabs items={['macOS', t('ollama.setup.install.windowsTab'), 'Linux']}>
81
- <Tab>
82
- <Flexbox gap={8}>
83
- {t('ollama.setup.cors.macos')}
84
- {/* eslint-disable-next-line react/no-unescaped-entities */}
85
- <Snippet language={'bash'}>launchctl setenv OLLAMA_ORIGINS "*"</Snippet>
86
- {t('ollama.setup.cors.reboot')}
87
- </Flexbox>
88
- </Tab>
89
- <Tab>
90
- <Flexbox gap={8}>
91
- <div>{t('ollama.setup.cors.windows')}</div>
92
- <div>{t('ollama.setup.cors.reboot')}</div>
93
- </Flexbox>
94
- </Tab>
95
- <Tab>
96
- {' '}
97
- <Flexbox gap={8}>
98
- {t('ollama.setup.cors.linux.systemd')}
142
+ <div>{t('ollama.setup.cors.linux.systemd')}</div>
99
143
  {/* eslint-disable-next-line react/no-unescaped-entities */}
100
144
  <Snippet language={'bash'}> sudo systemctl edit ollama.service</Snippet>
101
145
  {t('ollama.setup.cors.linux.env')}
@@ -111,17 +155,62 @@ Environment="OLLAMA_ORIGINS=*"`}
111
155
  />
112
156
  {t('ollama.setup.cors.linux.reboot')}
113
157
  </Flexbox>
114
- </Tab>
115
- </Tabs>
116
- </Flexbox>
117
- ),
118
- status: 'process',
119
- title: t('ollama.setup.cors.title'),
120
- },
121
- ]}
122
- size={'small'}
123
- />
124
- </Flexbox>
158
+ ),
159
+ status: 'process',
160
+ title: t('ollama.setup.cors.title'),
161
+ },
162
+ ]}
163
+ size={'small'}
164
+ />
165
+ ),
166
+ key: 'linux',
167
+ label: 'Linux',
168
+ },
169
+ {
170
+ children: (
171
+ <Steps
172
+ className={styles.steps}
173
+ direction={'vertical'}
174
+ items={[
175
+ {
176
+ description: (
177
+ <Flexbox gap={8}>
178
+ {t('ollama.setup.install.description')}
179
+ <div>{t('ollama.setup.install.docker')}</div>
180
+ <Snippet language={'bash'}>docker pull ollama/ollama</Snippet>
181
+ </Flexbox>
182
+ ),
183
+ status: 'process',
184
+ title: t('ollama.setup.install.title'),
185
+ },
186
+ {
187
+ description: (
188
+ <Flexbox gap={8}>
189
+ {t('ollama.setup.cors.description')}
190
+ <Highlighter
191
+ fileName={'ollama.service'}
192
+ fullFeatured
193
+ language={'bash'}
194
+ showLanguage
195
+ >
196
+ {/* eslint-disable-next-line react/no-unescaped-entities */}
197
+ docker run -d --gpus=all -v ollama:/root/.ollama -e OLLAMA_ORIGINS="*" -p
198
+ 11434:11434 --name ollama ollama/ollama
199
+ </Highlighter>
200
+ </Flexbox>
201
+ ),
202
+ status: 'process',
203
+ title: t('ollama.setup.cors.title'),
204
+ },
205
+ ]}
206
+ size={'small'}
207
+ />
208
+ ),
209
+ key: 'docker',
210
+ label: 'Docker',
211
+ },
212
+ ]}
213
+ />
125
214
  );
126
215
  });
127
216
 
@@ -27,8 +27,9 @@ export default {
27
27
  title: '随便聊聊',
28
28
  },
29
29
  input: {
30
+ addAi: '添加一条 AI 消息',
31
+ addUser: '添加一条用户消息',
30
32
  more: '更多',
31
- onlyAdd: '仅添加消息',
32
33
  send: '发送',
33
34
  sendWithCmdEnter: '按 {{meta}} + Enter 键发送',
34
35
  sendWithEnter: '按 Enter 键发送',
@@ -103,6 +104,8 @@ export default {
103
104
  confirmRemoveTopic: '即将删除该话题,删除后将不可恢复,请谨慎操作。',
104
105
  confirmRemoveUnstarred: '即将删除未收藏话题,删除后将不可恢复,请谨慎操作。',
105
106
  defaultTitle: '默认话题',
107
+ duplicateLoading: '话题复制中...',
108
+ duplicateSuccess: '话题复制成功',
106
109
  guide: {
107
110
  desc: '点击发送左侧按钮可将当前会话保存为历史话题,并开启新一轮会话',
108
111
  title: '话题列表',
@@ -150,16 +150,14 @@ export default {
150
150
  '在 Windows 上,点击「控制面板」,进入编辑系统环境变量。为您的用户账户新建名为 「OLLAMA_ORIGINS」 的环境变量,值为 * ,点击 「OK/应用」 保存',
151
151
  },
152
152
  install: {
153
- description: '请确认你已经开启 Ollama ,如果没有下载 Ollama ,请前往官网下载',
153
+ description: '请确认你已经开启 Ollama ,如果没有下载 Ollama ,请前往官网<1>下载</1>',
154
154
  docker:
155
155
  '如果你更倾向于使用 Docker,Ollama 也提供了官方 Docker 镜像,你可以通过以下命令拉取:',
156
156
  linux: {
157
157
  command: '通过以下命令安装:',
158
158
  manual: '或者,你也可以参考 <1>Linux 手动安装指南</1> 自行安装',
159
159
  },
160
- macos: '<0>下载 macOS 版 Ollama</0>,解压并安装',
161
160
  title: '在本地安装并开启 Ollama 应用',
162
- windows: '<0>下载 Windows 版 Ollama</0>,解压并安装',
163
161
  windowsTab: 'Windows (预览版)',
164
162
  },
165
163
  },
@@ -38,6 +38,7 @@ interface SendMessageParams {
38
38
  export interface ChatMessageAction {
39
39
  // create
40
40
  sendMessage: (params: SendMessageParams) => Promise<void>;
41
+ addAIMessage: () => Promise<void>;
41
42
  /**
42
43
  * regenerate message
43
44
  * trace enabled
@@ -214,7 +215,21 @@ export const chatMessage: StateCreator<
214
215
  if (id) switchTopic(id);
215
216
  }
216
217
  },
218
+ addAIMessage: async () => {
219
+ const { internalCreateMessage, updateInputMessage, activeTopicId, activeId, inputMessage } =
220
+ get();
221
+ if (!activeId) return;
222
+
223
+ await internalCreateMessage({
224
+ content: inputMessage,
225
+ role: 'assistant',
226
+ sessionId: activeId,
227
+ // if there is activeTopicId,then add topicId to message
228
+ topicId: activeTopicId,
229
+ });
217
230
 
231
+ updateInputMessage('');
232
+ },
218
233
  copyMessage: async (id, content) => {
219
234
  await copyToClipboard(content);
220
235
 
@@ -379,14 +379,18 @@ describe('topic action', () => {
379
379
  describe('updateTopicLoading', () => {
380
380
  it('should call update topicLoadingId', async () => {
381
381
  const { result } = renderHook(() => useChatStore());
382
- expect(result.current.topicLoadingId).toBeUndefined();
382
+ act(() => {
383
+ useChatStore.setState({ topicLoadingIds: [] });
384
+ });
385
+
386
+ expect(result.current.topicLoadingIds).toHaveLength(0);
383
387
 
384
388
  // Call the action with the topicId and newTitle
385
- await act(async () => {
386
- await result.current.updateTopicLoading('loading-id');
389
+ act(() => {
390
+ result.current.internal_updateTopicLoading('loading-id', true);
387
391
  });
388
392
 
389
- expect(result.current.topicLoadingId).toEqual('loading-id');
393
+ expect(result.current.topicLoadingIds).toEqual(['loading-id']);
390
394
  });
391
395
  });
392
396
  describe('summaryTopicTitle', () => {
@@ -7,18 +7,21 @@ import useSWR, { SWRResponse, mutate } from 'swr';
7
7
  import { StateCreator } from 'zustand/vanilla';
8
8
 
9
9
  import { chainSummaryTitle } from '@/chains/summaryTitle';
10
+ import { message } from '@/components/AntdStaticMethods';
10
11
  import { LOADING_FLAT } from '@/const/message';
11
12
  import { TraceNameMap } from '@/const/trace';
12
13
  import { useClientDataSWR } from '@/libs/swr';
13
14
  import { chatService } from '@/services/chat';
14
15
  import { messageService } from '@/services/message';
15
16
  import { topicService } from '@/services/topic';
17
+ import { CreateTopicParams } from '@/services/topic/type';
16
18
  import type { ChatStore } from '@/store/chat';
17
19
  import { ChatMessage } from '@/types/message';
18
20
  import { ChatTopic } from '@/types/topic';
19
21
  import { setNamespace } from '@/utils/storeDebug';
20
22
 
21
23
  import { chatSelectors } from '../message/selectors';
24
+ import { ChatTopicDispatch, topicReducer } from './reducer';
22
25
  import { topicSelectors } from './selectors';
23
26
 
24
27
  const n = setNamespace('topic');
@@ -40,10 +43,14 @@ export interface ChatTopicAction {
40
43
  summaryTopicTitle: (topicId: string, messages: ChatMessage[]) => Promise<void>;
41
44
  switchTopic: (id?: string) => Promise<void>;
42
45
  updateTopicTitleInSummary: (id: string, title: string) => void;
43
- updateTopicLoading: (id?: string) => void;
44
46
  updateTopicTitle: (id: string, title: string) => Promise<void>;
45
47
  useFetchTopics: (sessionId: string) => SWRResponse<ChatTopic[]>;
46
48
  useSearchTopics: (keywords?: string, sessionId?: string) => SWRResponse<ChatTopic[]>;
49
+
50
+ internal_updateTopicLoading: (id: string, loading: boolean) => void;
51
+ internal_createTopic: (params: CreateTopicParams) => Promise<string>;
52
+ internal_updateTopic: (id: string, data: Partial<ChatTopic>) => Promise<void>;
53
+ internal_dispatchTopic: (payload: ChatTopicDispatch, action?: any) => void;
47
54
  }
48
55
 
49
56
  export const chatTopic: StateCreator<
@@ -69,27 +76,16 @@ export const chatTopic: StateCreator<
69
76
  const messages = chatSelectors.currentChats(get());
70
77
  if (messages.length === 0) return;
71
78
 
72
- const { activeId, summaryTopicTitle, refreshTopic } = get();
79
+ const { activeId, summaryTopicTitle, internal_createTopic } = get();
73
80
 
74
81
  // 1. create topic and bind these messages
75
- const topicId = await topicService.createTopic({
82
+ const topicId = await internal_createTopic({
76
83
  sessionId: activeId,
77
84
  title: t('topic.defaultTitle', { ns: 'chat' }),
78
85
  messages: messages.map((m) => m.id),
79
86
  });
80
- await refreshTopic();
81
- // TODO: 优化为乐观更新
82
- // const params: CreateTopicParams = {
83
- // sessionId: activeId,
84
- // title: t('topic.defaultTitle', { ns: 'chat' }),
85
- // messages: messages.map((m) => m.id),
86
- // };
87
-
88
- // const topicId = await refreshTopic({
89
- // action: async () => topicService.createTopic(params),
90
- // optimisticData: (data) => topicReducer(data, { type: 'addTopic', value: params }),
91
- // });
92
87
 
88
+ get().internal_updateTopicLoading(topicId, true);
93
89
  // 2. auto summary topic Title
94
90
  // we don't need to wait for summary, just let it run async
95
91
  summaryTopicTitle(topicId, messages);
@@ -104,14 +100,22 @@ export const chatTopic: StateCreator<
104
100
 
105
101
  const newTitle = t('duplicateTitle', { ns: 'chat', title: topic?.title });
106
102
 
103
+ message.loading({
104
+ content: t('topic.duplicateLoading', { ns: 'chat' }),
105
+ key: 'duplicateTopic',
106
+ duration: 0,
107
+ });
108
+
107
109
  const newTopicId = await topicService.cloneTopic(id, newTitle);
108
110
  await refreshTopic();
111
+ message.destroy('duplicateTopic');
112
+ message.success(t('topic.duplicateSuccess', { ns: 'chat' }));
109
113
 
110
- switchTopic(newTopicId);
114
+ await switchTopic(newTopicId);
111
115
  },
112
116
  // update
113
117
  summaryTopicTitle: async (topicId, messages) => {
114
- const { updateTopicTitleInSummary, updateTopicLoading, refreshTopic } = get();
118
+ const { updateTopicTitleInSummary, internal_updateTopicLoading } = get();
115
119
  const topic = topicSelectors.getTopicById(topicId)(get());
116
120
  if (!topic) return;
117
121
 
@@ -125,10 +129,10 @@ export const chatTopic: StateCreator<
125
129
  updateTopicTitleInSummary(topicId, topic.title);
126
130
  },
127
131
  onFinish: async (text) => {
128
- await topicService.updateTopic(topicId, { title: text });
132
+ await get().internal_updateTopic(topicId, { title: text });
129
133
  },
130
134
  onLoadingChange: (loading) => {
131
- updateTopicLoading(loading ? topicId : undefined);
135
+ internal_updateTopicLoading(topicId, loading);
132
136
  },
133
137
  onMessageHandle: (x) => {
134
138
  output += x;
@@ -137,23 +141,23 @@ export const chatTopic: StateCreator<
137
141
  params: await chainSummaryTitle(messages),
138
142
  trace: get().getCurrentTracePayload({ traceName: TraceNameMap.SummaryTopicTitle, topicId }),
139
143
  });
140
- await refreshTopic();
141
144
  },
142
145
  favoriteTopic: async (id, favorite) => {
143
- await topicService.updateTopic(id, { favorite });
144
- await get().refreshTopic();
146
+ await get().internal_updateTopic(id, { favorite });
145
147
  },
146
148
 
147
149
  updateTopicTitle: async (id, title) => {
148
- await topicService.updateTopic(id, { title });
149
- await get().refreshTopic();
150
+ await get().internal_updateTopic(id, { title });
150
151
  },
151
152
 
152
153
  autoRenameTopicTitle: async (id) => {
153
- const { activeId: sessionId, summaryTopicTitle } = get();
154
+ const { activeId: sessionId, summaryTopicTitle, internal_updateTopicLoading } = get();
155
+
156
+ internal_updateTopicLoading(id, true);
154
157
  const messages = await messageService.getMessages(sessionId, id);
155
158
 
156
159
  await summaryTopicTitle(id, messages);
160
+ internal_updateTopicLoading(id, false);
157
161
  },
158
162
 
159
163
  // query
@@ -235,10 +239,48 @@ export const chatTopic: StateCreator<
235
239
 
236
240
  set({ topics }, false, n(`updateTopicTitleInSummary`, { id, title }));
237
241
  },
238
- updateTopicLoading: (id) => {
239
- set({ topicLoadingId: id }, false, n('updateTopicLoading'));
240
- },
241
242
  refreshTopic: async () => {
242
243
  return mutate([SWR_USE_FETCH_TOPIC, get().activeId]);
243
244
  },
245
+
246
+ internal_updateTopicLoading: (id, loading) => {
247
+ set(
248
+ (state) => {
249
+ if (loading) return { topicLoadingIds: [...state.topicLoadingIds, id] };
250
+
251
+ return { topicLoadingIds: state.topicLoadingIds.filter((i) => i !== id) };
252
+ },
253
+ false,
254
+ n('updateTopicLoading'),
255
+ );
256
+ },
257
+
258
+ internal_updateTopic: async (id, data) => {
259
+ get().internal_dispatchTopic({ type: 'updateTopic', id, value: data });
260
+
261
+ get().internal_updateTopicLoading(id, true);
262
+ await topicService.updateTopic(id, data);
263
+ await get().refreshTopic();
264
+ get().internal_updateTopicLoading(id, false);
265
+ },
266
+ internal_createTopic: async (params) => {
267
+ const tmpId = Date.now().toString();
268
+ get().internal_dispatchTopic({ type: 'addTopic', value: { ...params, id: tmpId } });
269
+
270
+ get().internal_updateTopicLoading(tmpId, true);
271
+ const topicId = await topicService.createTopic(params);
272
+ get().internal_updateTopicLoading(tmpId, false);
273
+
274
+ get().internal_updateTopicLoading(topicId, true);
275
+ await get().refreshTopic();
276
+ get().internal_updateTopicLoading(topicId, false);
277
+
278
+ return topicId;
279
+ },
280
+
281
+ internal_dispatchTopic: (payload, action) => {
282
+ const nextTopics = topicReducer(get().topics, payload);
283
+
284
+ set({ topics: nextTopics }, false, action);
285
+ },
244
286
  });
@@ -4,7 +4,7 @@ export interface ChatTopicState {
4
4
  activeTopicId?: string;
5
5
  isSearchingTopic: boolean;
6
6
  searchTopics: ChatTopic[];
7
- topicLoadingId?: string;
7
+ topicLoadingIds: string[];
8
8
  topicRenamingId?: string;
9
9
  topicSearchKeywords: string;
10
10
  topics: ChatTopic[];
@@ -17,6 +17,7 @@ export interface ChatTopicState {
17
17
  export const initialTopicState: ChatTopicState = {
18
18
  isSearchingTopic: false,
19
19
  searchTopics: [],
20
+ topicLoadingIds: [],
20
21
  topicSearchKeywords: '',
21
22
  topics: [],
22
23
  topicsInit: false,
@@ -42,8 +42,7 @@ describe('topicReducer', () => {
42
42
  const payload: ChatTopicDispatch = {
43
43
  type: 'updateTopic',
44
44
  id: '1',
45
- key: 'title',
46
- value: 'Updated Topic',
45
+ value: { title: 'Updated Topic' },
47
46
  };
48
47
 
49
48
  const newState = topicReducer(state, payload);
@@ -64,13 +63,12 @@ describe('topicReducer', () => {
64
63
  const payload: ChatTopicDispatch = {
65
64
  type: 'updateTopic',
66
65
  id: '1',
67
- key: 'title',
68
- value: 'Updated Topic',
66
+ value: { title: 'Updated Topic' },
69
67
  };
70
68
 
71
69
  const newState = topicReducer(state, payload);
72
70
 
73
- expect(newState[0].updatedAt).toBeGreaterThan(topic.updatedAt);
71
+ expect((newState[0].updatedAt as unknown as Date).valueOf()).toBeGreaterThan(topic.updatedAt);
74
72
  });
75
73
  });
76
74