@lobehub/chat 0.147.21 → 0.148.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 (52) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/locales/ar/setting.json +4 -0
  3. package/locales/bg-BG/setting.json +4 -0
  4. package/locales/de-DE/setting.json +4 -0
  5. package/locales/en-US/setting.json +4 -0
  6. package/locales/es-ES/setting.json +4 -0
  7. package/locales/fr-FR/setting.json +4 -0
  8. package/locales/it-IT/setting.json +4 -0
  9. package/locales/ja-JP/setting.json +4 -0
  10. package/locales/ko-KR/setting.json +4 -0
  11. package/locales/nl-NL/setting.json +4 -0
  12. package/locales/pl-PL/setting.json +4 -0
  13. package/locales/pt-BR/setting.json +4 -0
  14. package/locales/ru-RU/setting.json +4 -0
  15. package/locales/tr-TR/setting.json +4 -0
  16. package/locales/vi-VN/setting.json +4 -0
  17. package/locales/zh-CN/setting.json +4 -0
  18. package/locales/zh-TW/setting.json +4 -0
  19. package/package.json +3 -2
  20. package/public/favicon-32x32.ico +0 -0
  21. package/public/favicon.ico +0 -0
  22. package/public/icons/apple-touch-icon.png +0 -0
  23. package/src/app/api/chat/[provider]/route.test.ts +5 -7
  24. package/src/app/api/chat/[provider]/route.ts +13 -7
  25. package/src/app/api/chat/agentRuntime.test.ts +195 -451
  26. package/src/app/api/chat/agentRuntime.ts +197 -280
  27. package/src/app/api/chat/models/[provider]/route.ts +2 -2
  28. package/src/app/chat/features/TopicListContent/Topic/TopicContent.tsx +2 -2
  29. package/src/app/metadata.ts +3 -5
  30. package/src/app/settings/llm/components/ProviderConfig/index.tsx +23 -1
  31. package/src/app/settings/llm/index.tsx +2 -2
  32. package/src/app/settings/llm/page.tsx +1 -5
  33. package/src/features/ChatInput/Topic/index.tsx +6 -2
  34. package/src/features/Conversation/components/ChatItem/index.tsx +8 -3
  35. package/src/libs/agent-runtime/AgentRuntime.test.ts +400 -0
  36. package/src/libs/agent-runtime/AgentRuntime.ts +192 -0
  37. package/src/libs/agent-runtime/index.ts +1 -0
  38. package/src/libs/swr/index.ts +9 -0
  39. package/src/locales/default/setting.ts +4 -0
  40. package/src/services/__tests__/chat.test.ts +287 -1
  41. package/src/services/chat.ts +148 -2
  42. package/src/store/chat/slices/message/action.ts +80 -42
  43. package/src/store/chat/slices/message/initialState.ts +1 -1
  44. package/src/store/chat/slices/message/reducer.ts +32 -1
  45. package/src/store/chat/slices/topic/action.test.ts +25 -2
  46. package/src/store/chat/slices/topic/action.ts +24 -7
  47. package/src/store/chat/slices/topic/reducer.test.ts +141 -0
  48. package/src/store/chat/slices/topic/reducer.ts +67 -0
  49. package/src/store/global/slices/settings/selectors/modelConfig.ts +13 -0
  50. package/src/store/session/slices/session/action.ts +4 -5
  51. package/src/types/settings/modelProvider.ts +4 -0
  52. package/vercel.json +1 -1
@@ -1,6 +1,7 @@
1
1
  import isEqual from 'fast-deep-equal';
2
2
  import { produce } from 'immer';
3
3
 
4
+ import { CreateMessageParams } from '@/services/message';
4
5
  import { ChatMessage } from '@/types/message';
5
6
  import { merge } from '@/utils/merge';
6
7
 
@@ -10,6 +11,15 @@ interface UpdateMessage {
10
11
  type: 'updateMessage';
11
12
  value: ChatMessage[keyof ChatMessage];
12
13
  }
14
+ interface CreateMessage {
15
+ id: string;
16
+ type: 'createMessage';
17
+ value: CreateMessageParams;
18
+ }
19
+ interface DeleteMessage {
20
+ id: string;
21
+ type: 'deleteMessage';
22
+ }
13
23
 
14
24
  interface UpdatePluginState {
15
25
  id: string;
@@ -24,7 +34,12 @@ interface UpdateMessageExtra {
24
34
  value: any;
25
35
  }
26
36
 
27
- export type MessageDispatch = UpdateMessage | UpdatePluginState | UpdateMessageExtra;
37
+ export type MessageDispatch =
38
+ | CreateMessage
39
+ | UpdateMessage
40
+ | UpdatePluginState
41
+ | UpdateMessageExtra
42
+ | DeleteMessage;
28
43
 
29
44
  export const messagesReducer = (state: ChatMessage[], payload: MessageDispatch): ChatMessage[] => {
30
45
  switch (payload.type) {
@@ -76,6 +91,22 @@ export const messagesReducer = (state: ChatMessage[], payload: MessageDispatch):
76
91
  });
77
92
  }
78
93
 
94
+ case 'createMessage': {
95
+ return produce(state, (draftState) => {
96
+ const { value, id } = payload;
97
+
98
+ draftState.push({ ...value, createdAt: Date.now(), id, meta: {}, updatedAt: Date.now() });
99
+ });
100
+ }
101
+ case 'deleteMessage': {
102
+ return produce(state, (draft) => {
103
+ const { id } = payload;
104
+
105
+ const index = draft.findIndex((m) => m.id === id);
106
+
107
+ if (index >= 0) draft.splice(index, 1);
108
+ });
109
+ }
79
110
  default: {
80
111
  throw new Error('暂未实现的 type,请检查 reducer');
81
112
  }
@@ -149,7 +149,9 @@ describe('topic action', () => {
149
149
  });
150
150
 
151
151
  // Check if mutate has been called with the active session ID
152
- expect(mutate).toHaveBeenCalledWith(['SWR_USE_FETCH_TOPIC', activeId]);
152
+ expect(mutate).toHaveBeenCalledWith(['SWR_USE_FETCH_TOPIC', activeId], undefined, {
153
+ populateCache: false,
154
+ });
153
155
  });
154
156
 
155
157
  it('should handle errors during refreshing topics', async () => {
@@ -314,7 +316,7 @@ describe('topic action', () => {
314
316
  const activeId = 'test-session-id';
315
317
 
316
318
  await act(async () => {
317
- useChatStore.setState({ activeId });
319
+ useChatStore.setState({ activeId, activeTopicId: topicId });
318
320
  });
319
321
 
320
322
  const refreshTopicSpy = vi.spyOn(result.current, 'refreshTopic');
@@ -329,6 +331,27 @@ describe('topic action', () => {
329
331
  expect(refreshTopicSpy).toHaveBeenCalled();
330
332
  expect(switchTopicSpy).toHaveBeenCalled();
331
333
  });
334
+ it('should remove a specific topic and its messages, then not refresh the topic list', async () => {
335
+ const topicId = 'topic-1';
336
+ const { result } = renderHook(() => useChatStore());
337
+ const activeId = 'test-session-id';
338
+
339
+ await act(async () => {
340
+ useChatStore.setState({ activeId });
341
+ });
342
+
343
+ const refreshTopicSpy = vi.spyOn(result.current, 'refreshTopic');
344
+ const switchTopicSpy = vi.spyOn(result.current, 'switchTopic');
345
+
346
+ await act(async () => {
347
+ await result.current.removeTopic(topicId);
348
+ });
349
+
350
+ expect(messageService.removeMessages).toHaveBeenCalledWith(activeId, topicId);
351
+ expect(topicService.removeTopic).toHaveBeenCalledWith(topicId);
352
+ expect(refreshTopicSpy).toHaveBeenCalled();
353
+ expect(switchTopicSpy).not.toHaveBeenCalled();
354
+ });
332
355
  });
333
356
  describe('removeUnstarredTopic', () => {
334
357
  it('should remove unstarred topics and refresh the topic list', async () => {
@@ -9,11 +9,11 @@ import { StateCreator } from 'zustand/vanilla';
9
9
  import { chainSummaryTitle } from '@/chains/summaryTitle';
10
10
  import { LOADING_FLAT } from '@/const/message';
11
11
  import { TraceNameMap } from '@/const/trace';
12
- import { useClientDataSWR } from '@/libs/swr';
12
+ import { SWRefreshMethod, useClientDataSWR } from '@/libs/swr';
13
13
  import { chatService } from '@/services/chat';
14
14
  import { messageService } from '@/services/message';
15
15
  import { topicService } from '@/services/topic';
16
- import { ChatStore } from '@/store/chat';
16
+ import type { ChatStore } from '@/store/chat';
17
17
  import { ChatMessage } from '@/types/message';
18
18
  import { ChatTopic } from '@/types/topic';
19
19
  import { setNamespace } from '@/utils/storeDebug';
@@ -29,7 +29,7 @@ const SWR_USE_SEARCH_TOPIC = 'SWR_USE_SEARCH_TOPIC';
29
29
  export interface ChatTopicAction {
30
30
  favoriteTopic: (id: string, favState: boolean) => Promise<void>;
31
31
  openNewTopicOrSaveTopic: () => Promise<void>;
32
- refreshTopic: () => Promise<void>;
32
+ refreshTopic: SWRefreshMethod<ChatTopic[]>;
33
33
  removeAllTopics: () => Promise<void>;
34
34
  removeSessionTopics: () => Promise<void>;
35
35
  removeTopic: (id: string) => Promise<void>;
@@ -78,6 +78,17 @@ export const chatTopic: StateCreator<
78
78
  messages: messages.map((m) => m.id),
79
79
  });
80
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
+ // });
81
92
 
82
93
  // 2. auto summary topic Title
83
94
  // we don't need to wait for summary, just let it run async
@@ -189,9 +200,10 @@ export const chatTopic: StateCreator<
189
200
  await refreshTopic();
190
201
  },
191
202
  removeTopic: async (id) => {
192
- const { activeId, switchTopic, refreshTopic } = get();
203
+ const { activeId, activeTopicId, switchTopic, refreshTopic } = get();
193
204
 
194
205
  // remove messages in the topic
206
+ // TODO: Need to remove because server service don't need to call it
195
207
  await messageService.removeMessages(activeId, id);
196
208
 
197
209
  // remove topic
@@ -199,7 +211,7 @@ export const chatTopic: StateCreator<
199
211
  await refreshTopic();
200
212
 
201
213
  // switch bach to default topic
202
- switchTopic();
214
+ if (activeTopicId === id) switchTopic();
203
215
  },
204
216
  removeUnstarredTopic: async () => {
205
217
  const { refreshTopic, switchTopic } = get();
@@ -226,7 +238,12 @@ export const chatTopic: StateCreator<
226
238
  updateTopicLoading: (id) => {
227
239
  set({ topicLoadingId: id }, false, n('updateTopicLoading'));
228
240
  },
229
- refreshTopic: async () => {
230
- await mutate([SWR_USE_FETCH_TOPIC, get().activeId]);
241
+ // TODO: I don't know why this ts error, so have to ignore it
242
+ // @ts-ignore
243
+ refreshTopic: async (params) => {
244
+ return mutate([SWR_USE_FETCH_TOPIC, get().activeId], params?.action, {
245
+ optimisticData: params?.optimisticData,
246
+ populateCache: false,
247
+ });
231
248
  },
232
249
  });
@@ -0,0 +1,141 @@
1
+ import { produce } from 'immer';
2
+ import { expect } from 'vitest';
3
+
4
+ import { ChatTopic } from '@/types/topic';
5
+
6
+ import { ChatTopicDispatch, topicReducer } from './reducer';
7
+
8
+ describe('topicReducer', () => {
9
+ let state: ChatTopic[];
10
+
11
+ beforeEach(() => {
12
+ state = [];
13
+ });
14
+
15
+ describe('addTopic', () => {
16
+ it('should add a new ChatTopic object to state', () => {
17
+ const payload: ChatTopicDispatch = {
18
+ type: 'addTopic',
19
+ value: {
20
+ title: 'Test Topic',
21
+ sessionId: '',
22
+ },
23
+ };
24
+
25
+ const newState = topicReducer(state, payload);
26
+
27
+ expect(newState[0].id).toBeDefined();
28
+ });
29
+ });
30
+
31
+ describe('updateTopic', () => {
32
+ it('should update the ChatTopic object in state', () => {
33
+ const topic: ChatTopic = {
34
+ id: '1',
35
+ title: 'Test Topic',
36
+ createdAt: Date.now(),
37
+ updatedAt: Date.now(),
38
+ };
39
+
40
+ state.push(topic);
41
+
42
+ const payload: ChatTopicDispatch = {
43
+ type: 'updateTopic',
44
+ id: '1',
45
+ key: 'title',
46
+ value: 'Updated Topic',
47
+ };
48
+
49
+ const newState = topicReducer(state, payload);
50
+
51
+ expect(newState[0].title).toBe('Updated Topic');
52
+ });
53
+
54
+ it('should update the ChatTopic object with correct properties', () => {
55
+ const topic: ChatTopic = {
56
+ id: '1',
57
+ title: 'Test Topic',
58
+ createdAt: Date.now() - 1,
59
+ updatedAt: Date.now() - 1, // 设定比当前时间前面一点
60
+ };
61
+
62
+ state.push(topic);
63
+
64
+ const payload: ChatTopicDispatch = {
65
+ type: 'updateTopic',
66
+ id: '1',
67
+ key: 'title',
68
+ value: 'Updated Topic',
69
+ };
70
+
71
+ const newState = topicReducer(state, payload);
72
+
73
+ expect(newState[0].updatedAt).toBeGreaterThan(topic.updatedAt);
74
+ });
75
+ });
76
+
77
+ describe('deleteTopic', () => {
78
+ it('should delete the specified ChatTopic object from state', () => {
79
+ const topic: ChatTopic = {
80
+ id: '1',
81
+ title: 'Test Topic',
82
+ createdAt: Date.now(),
83
+ updatedAt: Date.now(),
84
+ };
85
+
86
+ state.push(topic);
87
+
88
+ const payload: ChatTopicDispatch = {
89
+ type: 'deleteTopic',
90
+ id: '1',
91
+ };
92
+
93
+ const newState = topicReducer(state, payload);
94
+
95
+ expect(newState).toEqual([]);
96
+ });
97
+ });
98
+
99
+ describe('default', () => {
100
+ it('should return the original state object', () => {
101
+ const payload = {
102
+ type: 'unknown',
103
+ } as unknown as ChatTopicDispatch;
104
+
105
+ const newState = topicReducer(state, payload);
106
+
107
+ expect(newState).toBe(state);
108
+ });
109
+ });
110
+
111
+ describe('produce', () => {
112
+ it('should generate immutable state object', () => {
113
+ const payload: ChatTopicDispatch = {
114
+ type: 'addTopic',
115
+ value: {
116
+ title: 'Test Topic',
117
+ sessionId: '1',
118
+ },
119
+ };
120
+
121
+ const newState = topicReducer(state, payload);
122
+
123
+ expect(newState).not.toBe(state);
124
+ });
125
+
126
+ it('should not modify the original state object', () => {
127
+ const payload: ChatTopicDispatch = {
128
+ type: 'addTopic',
129
+ value: {
130
+ title: 'Test Topic',
131
+
132
+ sessionId: '123',
133
+ },
134
+ };
135
+
136
+ const newState = topicReducer(state, payload);
137
+
138
+ expect(state).toEqual([]);
139
+ });
140
+ });
141
+ });
@@ -0,0 +1,67 @@
1
+ import { produce } from 'immer';
2
+
3
+ import { CreateTopicParams } from '@/services/topic/type';
4
+ import { ChatTopic } from '@/types/topic';
5
+
6
+ interface AddChatTopicAction {
7
+ type: 'addTopic';
8
+ value: CreateTopicParams;
9
+ }
10
+
11
+ interface UpdateChatTopicAction {
12
+ id: string;
13
+ key: keyof ChatTopic;
14
+ type: 'updateTopic';
15
+ value: any;
16
+ }
17
+
18
+ interface DeleteChatTopicAction {
19
+ id: string;
20
+ type: 'deleteTopic';
21
+ }
22
+
23
+ export type ChatTopicDispatch = AddChatTopicAction | UpdateChatTopicAction | DeleteChatTopicAction;
24
+
25
+ export const topicReducer = (state: ChatTopic[] = [], payload: ChatTopicDispatch): ChatTopic[] => {
26
+ switch (payload.type) {
27
+ case 'addTopic': {
28
+ return produce(state, (draftState) => {
29
+ draftState.unshift({
30
+ ...payload.value,
31
+ createdAt: Date.now(),
32
+ id: Date.now().toString(),
33
+ sessionId: payload.value.sessionId ? payload.value.sessionId : undefined,
34
+ updatedAt: Date.now(),
35
+ });
36
+ });
37
+ }
38
+
39
+ case 'updateTopic': {
40
+ return produce(state, (draftState) => {
41
+ const { key, value, id } = payload;
42
+ const topicIndex = draftState.findIndex((topic) => topic.id === id);
43
+
44
+ if (topicIndex !== -1) {
45
+ const updatedTopic = { ...draftState[topicIndex] };
46
+ // @ts-ignore
47
+ updatedTopic[key] = value;
48
+ draftState[topicIndex] = updatedTopic;
49
+ updatedTopic.updatedAt = Date.now();
50
+ }
51
+ });
52
+ }
53
+
54
+ case 'deleteTopic': {
55
+ return produce(state, (draftState) => {
56
+ const topicIndex = draftState.findIndex((topic) => topic.id === payload.id);
57
+ if (topicIndex !== -1) {
58
+ draftState.splice(topicIndex, 1);
59
+ }
60
+ });
61
+ }
62
+
63
+ default: {
64
+ return state;
65
+ }
66
+ }
67
+ };
@@ -6,6 +6,16 @@ import { currentLLMSettings, getProviderConfigById } from './settings';
6
6
  const isProviderEnabled = (provider: GlobalLLMProviderKey) => (s: GlobalStore) =>
7
7
  getProviderConfigById(provider)(s)?.enabled || false;
8
8
 
9
+ const isProviderEndpointNotEmpty = (provider: GlobalLLMProviderKey | string) => (s: GlobalStore) =>
10
+ !!getProviderConfigById(provider)(s)?.endpoint;
11
+
12
+ const isProviderFetchOnClient = (provider: GlobalLLMProviderKey | string) => (s: GlobalStore) => {
13
+ const config = getProviderConfigById(provider)(s);
14
+ if (typeof config?.fetchOnClient !== 'undefined') return config?.fetchOnClient;
15
+
16
+ return isProviderEndpointNotEmpty(provider)(s);
17
+ };
18
+
9
19
  const getCustomModelCard =
10
20
  ({ id, provider }: { id?: string; provider?: string }) =>
11
21
  (s: GlobalStore) => {
@@ -46,6 +56,9 @@ export const modelConfigSelectors = {
46
56
  isAutoFetchModelsEnabled,
47
57
  isAzureEnabled,
48
58
  isProviderEnabled,
59
+ isProviderEndpointNotEmpty,
60
+ isProviderFetchOnClient,
61
+
49
62
  ollamaConfig,
50
63
 
51
64
  openAIConfig,
@@ -6,7 +6,7 @@ import { StateCreator } from 'zustand/vanilla';
6
6
 
7
7
  import { message } from '@/components/AntdStaticMethods';
8
8
  import { INBOX_SESSION_ID } from '@/const/session';
9
- import { useClientDataSWR } from '@/libs/swr';
9
+ import { SWRRefreshParams, useClientDataSWR } from '@/libs/swr';
10
10
  import { sessionService } from '@/services/session';
11
11
  import { useGlobalStore } from '@/store/global';
12
12
  import { settingsSelectors } from '@/store/global/selectors';
@@ -51,10 +51,7 @@ export interface SessionAction {
51
51
  /**
52
52
  * re-fetch the data
53
53
  */
54
- refreshSessions: (params?: {
55
- action: () => Promise<void>;
56
- optimisticData?: (data: ChatSessionList) => ChatSessionList;
57
- }) => Promise<void>;
54
+ refreshSessions: (params?: SWRRefreshParams<ChatSessionList>) => Promise<void>;
58
55
 
59
56
  /**
60
57
  * remove session
@@ -145,6 +142,8 @@ export const createSessionSlice: StateCreator<
145
142
  },
146
143
  // 乐观更新
147
144
  optimisticData: produce((draft) => {
145
+ if (!draft) return;
146
+
148
147
  const session = draft.all.find((i) => i.id === sessionId);
149
148
  if (!session) return;
150
149
 
@@ -16,6 +16,10 @@ export interface GeneralModelProviderConfig {
16
16
  */
17
17
  enabledModels?: string[] | null;
18
18
  endpoint?: string;
19
+ /**
20
+ * whether fetch on client
21
+ */
22
+ fetchOnClient?: boolean;
19
23
  /**
20
24
  * the latest fetch model list time
21
25
  */
package/vercel.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "installCommand": "npx bun@1.1.3 install"
2
+ "installCommand": "bun install"
3
3
  }