@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.
- package/CHANGELOG.md +42 -0
- package/locales/ar/chat.json +4 -1
- package/locales/ar/modelProvider.json +1 -3
- package/locales/bg-BG/chat.json +4 -1
- package/locales/bg-BG/modelProvider.json +1 -3
- package/locales/de-DE/chat.json +4 -1
- package/locales/de-DE/modelProvider.json +1 -3
- package/locales/en-US/chat.json +7 -4
- package/locales/en-US/modelProvider.json +1 -3
- package/locales/es-ES/chat.json +4 -1
- package/locales/es-ES/modelProvider.json +1 -3
- package/locales/fr-FR/chat.json +7 -4
- package/locales/fr-FR/modelProvider.json +1 -3
- package/locales/it-IT/chat.json +4 -1
- package/locales/it-IT/modelProvider.json +16 -17
- package/locales/ja-JP/chat.json +4 -1
- package/locales/ja-JP/modelProvider.json +14 -16
- package/locales/ko-KR/chat.json +4 -1
- package/locales/ko-KR/modelProvider.json +1 -3
- package/locales/nl-NL/chat.json +4 -1
- package/locales/nl-NL/modelProvider.json +1 -3
- package/locales/pl-PL/chat.json +4 -1
- package/locales/pl-PL/modelProvider.json +1 -3
- package/locales/pt-BR/chat.json +4 -1
- package/locales/pt-BR/modelProvider.json +1 -3
- package/locales/ru-RU/chat.json +4 -1
- package/locales/ru-RU/modelProvider.json +1 -3
- package/locales/tr-TR/chat.json +4 -1
- package/locales/tr-TR/modelProvider.json +1 -3
- package/locales/vi-VN/chat.json +4 -1
- package/locales/vi-VN/modelProvider.json +1 -3
- package/locales/zh-CN/chat.json +4 -1
- package/locales/zh-CN/modelProvider.json +1 -3
- package/locales/zh-TW/chat.json +4 -1
- package/locales/zh-TW/modelProvider.json +1 -3
- package/package.json +1 -1
- package/src/app/chat/(desktop)/features/ChatInput/Footer/SendMore.tsx +117 -0
- package/src/app/chat/(desktop)/features/ChatInput/Footer/index.tsx +9 -56
- package/src/app/chat/(desktop)/features/ChatInput/TextArea.tsx +1 -1
- package/src/app/chat/(desktop)/features/HotKeys.tsx +3 -3
- package/src/app/chat/features/TopicListContent/Topic/TopicContent.tsx +15 -6
- package/src/components/HotKeys/index.tsx +14 -18
- package/src/const/hotkeys.ts +1 -1
- package/src/features/ChatInput/ActionBar/Clear.tsx +3 -3
- package/src/features/ChatInput/Topic/index.tsx +3 -3
- package/src/features/Conversation/Actions/Assistant.tsx +1 -1
- package/src/features/Conversation/Error/OllamaBizError/SetupGuide.tsx +159 -70
- package/src/locales/default/chat.ts +4 -1
- package/src/locales/default/modelProvider.ts +1 -3
- package/src/store/chat/slices/message/action.ts +15 -0
- package/src/store/chat/slices/topic/action.test.ts +8 -4
- package/src/store/chat/slices/topic/action.ts +70 -28
- package/src/store/chat/slices/topic/initialState.ts +2 -1
- package/src/store/chat/slices/topic/reducer.test.ts +3 -5
- 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 {
|
|
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 = [
|
|
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
|
);
|
|
@@ -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
|
-
<
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
{
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
<Link href={'https://ollama.com/download'}
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
63
|
-
|
|
133
|
+
),
|
|
134
|
+
status: 'process',
|
|
135
|
+
title: t('ollama.setup.install.title'),
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
description: (
|
|
64
139
|
<Flexbox gap={8}>
|
|
65
|
-
{t('ollama.setup.
|
|
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
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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
|
-
|
|
386
|
-
|
|
389
|
+
act(() => {
|
|
390
|
+
result.current.internal_updateTopicLoading('loading-id', true);
|
|
387
391
|
});
|
|
388
392
|
|
|
389
|
-
expect(result.current.
|
|
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,
|
|
79
|
+
const { activeId, summaryTopicTitle, internal_createTopic } = get();
|
|
73
80
|
|
|
74
81
|
// 1. create topic and bind these messages
|
|
75
|
-
const topicId = await
|
|
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,
|
|
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
|
|
132
|
+
await get().internal_updateTopic(topicId, { title: text });
|
|
129
133
|
},
|
|
130
134
|
onLoadingChange: (loading) => {
|
|
131
|
-
|
|
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
|
|
144
|
-
await get().refreshTopic();
|
|
146
|
+
await get().internal_updateTopic(id, { favorite });
|
|
145
147
|
},
|
|
146
148
|
|
|
147
149
|
updateTopicTitle: async (id, title) => {
|
|
148
|
-
await
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|