@lobehub/chat 0.118.10 → 0.119.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 (92) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/locales/de-DE/chat.json +10 -0
  3. package/locales/de-DE/common.json +1 -0
  4. package/locales/de-DE/error.json +4 -4
  5. package/locales/en-US/chat.json +10 -0
  6. package/locales/en-US/common.json +1 -0
  7. package/locales/en-US/error.json +1 -1
  8. package/locales/es-ES/chat.json +10 -0
  9. package/locales/es-ES/common.json +1 -0
  10. package/locales/fr-FR/chat.json +10 -0
  11. package/locales/fr-FR/common.json +1 -0
  12. package/locales/fr-FR/error.json +1 -2
  13. package/locales/it-IT/chat.json +10 -0
  14. package/locales/it-IT/common.json +1 -0
  15. package/locales/it-IT/error.json +5 -5
  16. package/locales/ja-JP/chat.json +10 -0
  17. package/locales/ja-JP/common.json +1 -0
  18. package/locales/ko-KR/chat.json +10 -0
  19. package/locales/ko-KR/common.json +1 -0
  20. package/locales/ko-KR/error.json +4 -4
  21. package/locales/nl-NL/chat.json +10 -0
  22. package/locales/nl-NL/common.json +1 -0
  23. package/locales/pl-PL/chat.json +10 -0
  24. package/locales/pl-PL/common.json +1 -0
  25. package/locales/pl-PL/error.json +2 -3
  26. package/locales/pt-BR/chat.json +10 -0
  27. package/locales/pt-BR/common.json +1 -0
  28. package/locales/ru-RU/chat.json +10 -0
  29. package/locales/ru-RU/common.json +1 -0
  30. package/locales/ru-RU/error.json +3 -3
  31. package/locales/tr-TR/chat.json +10 -0
  32. package/locales/tr-TR/common.json +1 -0
  33. package/locales/vi-VN/chat.json +10 -0
  34. package/locales/vi-VN/common.json +1 -0
  35. package/locales/vi-VN/error.json +1 -1
  36. package/locales/zh-CN/chat.json +10 -0
  37. package/locales/zh-CN/common.json +1 -0
  38. package/locales/zh-TW/chat.json +10 -0
  39. package/locales/zh-TW/common.json +1 -0
  40. package/package.json +2 -2
  41. package/src/app/chat/(desktop)/features/Conversation.tsx +17 -0
  42. package/src/app/chat/(desktop)/features/HotKeys.tsx +44 -0
  43. package/src/app/chat/(desktop)/index.tsx +2 -3
  44. package/src/app/chat/features/ChatHeader/ShareButton/Preview.tsx +1 -1
  45. package/src/app/chat/features/SessionListContent/List/Item/Actions.tsx +24 -3
  46. package/src/app/chat/features/TopicListContent/Topic/TopicContent.tsx +43 -2
  47. package/src/database/models/__tests__/message.test.ts +48 -0
  48. package/src/database/models/__tests__/topic.test.ts +135 -0
  49. package/src/database/models/message.ts +35 -0
  50. package/src/database/models/session.ts +11 -1
  51. package/src/database/models/topic.ts +34 -2
  52. package/src/features/ChatInput/ActionBar/Clear.tsx +0 -6
  53. package/src/features/Conversation/Actions/Assistant.tsx +16 -5
  54. package/src/features/Conversation/Actions/Error.tsx +2 -2
  55. package/src/features/Conversation/Actions/Fallback.tsx +4 -3
  56. package/src/features/Conversation/Actions/Function.tsx +3 -3
  57. package/src/features/Conversation/Actions/User.tsx +3 -3
  58. package/src/features/Conversation/Actions/customAction.ts +2 -4
  59. package/src/features/Conversation/Actions/index.ts +50 -45
  60. package/src/features/Conversation/Error/InvalidAccess.tsx +1 -1
  61. package/src/features/Conversation/Error/OpenAPIKey.tsx +1 -1
  62. package/src/features/Conversation/Error/OpenAiBizError.tsx +1 -1
  63. package/src/features/Conversation/Error/Plugin/PluginError.tsx +1 -1
  64. package/src/features/Conversation/Error/Plugin/PluginSettings.tsx +1 -1
  65. package/src/features/Conversation/Error/index.tsx +2 -2
  66. package/src/features/Conversation/Extras/Assistant.tsx +32 -29
  67. package/src/features/Conversation/Extras/User.tsx +2 -1
  68. package/src/features/Conversation/Extras/index.ts +4 -4
  69. package/src/features/Conversation/Messages/index.ts +3 -3
  70. package/src/features/Conversation/components/ChatItem/ActionsBar.tsx +58 -0
  71. package/src/features/Conversation/components/ChatItem/index.tsx +126 -0
  72. package/src/features/Conversation/components/ChatList/HistoryDivider.tsx +6 -3
  73. package/src/features/Conversation/components/ChatList/index.tsx +35 -79
  74. package/src/features/Conversation/hooks/useChatListActionsBar.tsx +43 -31
  75. package/src/features/Conversation/hooks/useInitConversation.ts +4 -1
  76. package/src/features/Conversation/index.tsx +4 -3
  77. package/src/features/Conversation/types/index.tsx +35 -0
  78. package/src/features/PluginStore/PluginItem/EditCustomPlugin.tsx +7 -2
  79. package/src/locales/default/chat.ts +10 -1
  80. package/src/locales/default/common.ts +1 -0
  81. package/src/services/config.ts +22 -0
  82. package/src/services/session.ts +6 -0
  83. package/src/services/topic.ts +4 -0
  84. package/src/store/chat/slices/message/selectors.ts +10 -0
  85. package/src/store/chat/slices/topic/action.ts +22 -0
  86. package/src/store/global/slices/common/action.ts +2 -0
  87. package/src/store/session/slices/session/action.test.ts +136 -0
  88. package/src/store/session/slices/session/action.ts +26 -6
  89. package/src/types/topic.ts +1 -0
  90. package/src/features/Conversation/components/ChatList/ActionsBar.tsx +0 -27
  91. package/src/features/Conversation/components/ChatList/Item.tsx +0 -236
  92. package/src/features/Conversation/container/index.tsx +0 -83
@@ -0,0 +1,136 @@
1
+ import { act, renderHook } from '@testing-library/react';
2
+ import { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime';
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
+
5
+ import { INBOX_SESSION_ID } from '@/const/session';
6
+ import { SESSION_CHAT_URL } from '@/const/url';
7
+ import { sessionService } from '@/services/session';
8
+ import { useSessionStore } from '@/store/session';
9
+ import { LobeSessionType } from '@/types/session';
10
+
11
+ // Mock sessionService 和其他依赖项
12
+ vi.mock('@/services/session', () => ({
13
+ sessionService: {
14
+ removeAllSessions: vi.fn(),
15
+ createNewSession: vi.fn(),
16
+ duplicateSession: vi.fn(),
17
+ updateSessionGroup: vi.fn(),
18
+ removeSession: vi.fn(),
19
+ getSessions: vi.fn(),
20
+ searchSessions: vi.fn(),
21
+ },
22
+ }));
23
+
24
+ // Mock router
25
+ const mockRouterPush = vi.fn();
26
+
27
+ const mockRefresh = vi.fn();
28
+ beforeEach(() => {
29
+ vi.clearAllMocks();
30
+ useSessionStore.setState({
31
+ refreshSessions: mockRefresh,
32
+ router: {
33
+ push: mockRouterPush,
34
+ } as unknown as AppRouterInstance,
35
+ });
36
+ });
37
+
38
+ afterEach(() => {
39
+ vi.restoreAllMocks();
40
+ });
41
+
42
+ describe('SessionAction', () => {
43
+ describe('clearSessions', () => {
44
+ it('should clear all sessions and refresh the list', async () => {
45
+ const { result } = renderHook(() => useSessionStore());
46
+
47
+ await act(async () => {
48
+ await result.current.clearSessions();
49
+ });
50
+
51
+ expect(sessionService.removeAllSessions).toHaveBeenCalled();
52
+ expect(mockRefresh).toHaveBeenCalled(); // 假设 refreshSessions 调用了 getSessions
53
+ });
54
+ });
55
+
56
+ describe('createSession', () => {
57
+ it('should create a new session and switch to it', async () => {
58
+ const { result } = renderHook(() => useSessionStore());
59
+ const newSessionId = 'new-session-id';
60
+ vi.mocked(sessionService.createNewSession).mockResolvedValue(newSessionId);
61
+
62
+ let createdSessionId;
63
+
64
+ await act(async () => {
65
+ createdSessionId = await result.current.createSession({ config: { displayMode: 'docs' } });
66
+ });
67
+
68
+ const call = vi.mocked(sessionService.createNewSession).mock.calls[0];
69
+ expect(call[0]).toEqual(LobeSessionType.Agent);
70
+ expect(call[1]).toMatchObject({ config: { displayMode: 'docs' } });
71
+
72
+ expect(createdSessionId).toBe(newSessionId);
73
+ expect(mockRouterPush).toHaveBeenCalledWith(
74
+ SESSION_CHAT_URL(newSessionId, result.current.isMobile),
75
+ );
76
+ });
77
+ });
78
+
79
+ describe('duplicateSession', () => {
80
+ it('should duplicate a session and switch to the new one', async () => {
81
+ const { result } = renderHook(() => useSessionStore());
82
+ const sessionId = 'session-id';
83
+ const duplicatedSessionId = 'duplicated-session-id';
84
+ vi.mocked(sessionService.duplicateSession).mockResolvedValue(duplicatedSessionId);
85
+
86
+ await act(async () => {
87
+ await result.current.duplicateSession(sessionId);
88
+ });
89
+
90
+ expect(sessionService.duplicateSession).toHaveBeenCalledWith(sessionId, undefined);
91
+ });
92
+ });
93
+
94
+ describe('removeSession', () => {
95
+ it('should remove a session and refresh the list', async () => {
96
+ const { result } = renderHook(() => useSessionStore());
97
+ const sessionId = 'session-id';
98
+
99
+ await act(async () => {
100
+ await result.current.removeSession(sessionId);
101
+ });
102
+
103
+ expect(sessionService.removeSession).toHaveBeenCalledWith(sessionId);
104
+ expect(mockRefresh).toHaveBeenCalled();
105
+ });
106
+ });
107
+
108
+ describe('switchSession', () => {
109
+ it('should switch to the provided session id', async () => {
110
+ const { result } = renderHook(() => useSessionStore());
111
+ const sessionId = 'session-id';
112
+
113
+ act(() => {
114
+ result.current.switchSession(sessionId);
115
+ });
116
+
117
+ expect(result.current.activeId).toBe(sessionId);
118
+ expect(mockRouterPush).toHaveBeenCalledWith(
119
+ SESSION_CHAT_URL(sessionId, result.current.isMobile),
120
+ );
121
+ });
122
+
123
+ it('should switch to the inbox session id if none is provided', async () => {
124
+ const { result } = renderHook(() => useSessionStore());
125
+
126
+ act(() => {
127
+ result.current.switchSession();
128
+ });
129
+
130
+ expect(result.current.activeId).toBe(INBOX_SESSION_ID);
131
+ expect(mockRouterPush).toHaveBeenCalledWith(
132
+ SESSION_CHAT_URL(INBOX_SESSION_ID, result.current.isMobile),
133
+ );
134
+ });
135
+ });
136
+ });
@@ -1,3 +1,5 @@
1
+ import { message } from 'antd';
2
+ import { t } from 'i18next';
1
3
  import useSWR, { SWRResponse, mutate } from 'swr';
2
4
  import { DeepPartial } from 'utility-types';
3
5
  import { StateCreator } from 'zustand/vanilla';
@@ -17,7 +19,9 @@ import {
17
19
  import { merge } from '@/utils/merge';
18
20
  import { setNamespace } from '@/utils/storeDebug';
19
21
 
22
+ import { agentSelectors } from '../agent/selectors';
20
23
  import { initLobeSession } from './initialState';
24
+ import { sessionSelectors } from './selectors';
21
25
 
22
26
  const n = setNamespace('session');
23
27
 
@@ -39,7 +43,7 @@ export interface SessionAction {
39
43
  * @returns sessionId
40
44
  */
41
45
  createSession: (agent?: DeepPartial<LobeAgentSettings>) => Promise<string>;
42
-
46
+ duplicateSession: (id: string) => Promise<void>;
43
47
  /**
44
48
  * Pins or unpins a session.
45
49
  *
@@ -48,20 +52,16 @@ export interface SessionAction {
48
52
  * @returns {Promise<void>} A promise that resolves when the session is successfully pinned or unpinned.
49
53
  */
50
54
  pinSession: (id: string, pinned?: boolean) => Promise<void>;
51
-
52
55
  /**
53
56
  * re-fetch the data
54
57
  */
55
58
  refreshSessions: () => Promise<void>;
56
-
57
59
  /**
58
60
  * remove session
59
61
  * @param id - sessionId
60
62
  */
61
63
  removeSession: (id: string) => void;
62
-
63
64
  switchBackToChat: () => void;
64
-
65
65
  /**
66
66
  * switch session url
67
67
  */
@@ -70,7 +70,6 @@ export interface SessionAction {
70
70
  * A custom hook that uses SWR to fetch sessions data.
71
71
  */
72
72
  useFetchSessions: () => SWRResponse<any>;
73
-
74
73
  useSearchSessions: (keyword?: string) => SWRResponse<any>;
75
74
  }
76
75
 
@@ -109,6 +108,27 @@ export const createSessionSlice: StateCreator<
109
108
  return id;
110
109
  },
111
110
 
111
+ duplicateSession: async (id) => {
112
+ const { switchSession, refreshSessions } = get();
113
+ const session = sessionSelectors.getSessionById(id)(get());
114
+
115
+ if (!session) return;
116
+ const title = agentSelectors.getTitle(session.meta);
117
+
118
+ const newTitle = t('duplicateTitle', { ns: 'chat', title: title });
119
+
120
+ const newId = await sessionService.duplicateSession(id, newTitle);
121
+
122
+ // duplicate Session Error
123
+ if (!newId) {
124
+ message.error('复制失败');
125
+ return;
126
+ }
127
+
128
+ await refreshSessions();
129
+ switchSession(newId);
130
+ },
131
+
112
132
  pinSession: async (sessionId, pinned) => {
113
133
  await sessionService.updateSessionGroup(sessionId, pinned ? 'pinned' : 'default');
114
134
 
@@ -2,6 +2,7 @@ import { BaseDataModel } from '@/types/meta';
2
2
 
3
3
  export interface ChatTopic extends Omit<BaseDataModel, 'meta'> {
4
4
  favorite?: boolean;
5
+ sessionId?: string;
5
6
  title: string;
6
7
  }
7
8
 
@@ -1,27 +0,0 @@
1
- import { ActionIconGroup, type ActionIconGroupProps } from '@lobehub/ui';
2
- import { memo } from 'react';
3
-
4
- import { useChatListActionsBar } from '../../hooks/useChatListActionsBar';
5
-
6
- export interface ActionsBarProps extends ActionIconGroupProps {
7
- text?: {
8
- copy?: string;
9
- delete?: string;
10
- edit?: string;
11
- regenerate?: string;
12
- };
13
- }
14
-
15
- const ActionsBar = memo<ActionsBarProps>(({ text, ...rest }) => {
16
- const { regenerate, edit, copy, divider, del } = useChatListActionsBar(text);
17
- return (
18
- <ActionIconGroup
19
- dropdownMenu={[edit, copy, regenerate, divider, del]}
20
- items={[regenerate, edit]}
21
- type="ghost"
22
- {...rest}
23
- />
24
- );
25
- });
26
-
27
- export default ActionsBar;
@@ -1,236 +0,0 @@
1
- import { type AlertProps, ChatItem, type ChatItemProps } from '@lobehub/ui';
2
- import { ActionEvent, copyToClipboard } from '@lobehub/ui';
3
- import { App } from 'antd';
4
- import { FC, ReactNode, memo, useCallback, useMemo, useState } from 'react';
5
-
6
- import { LLMRoleType } from '@/types/llm';
7
- import { ChatMessage } from '@/types/message';
8
-
9
- import ActionsBar, { type ActionsBarProps } from './ActionsBar';
10
-
11
- export type OnMessageChange = (id: string, content: string) => void;
12
- export type OnActionsClick = (action: ActionEvent, message: ChatMessage) => void;
13
- export type OnAvatatsClick = (role: RenderRole) => ChatItemProps['onAvatarClick'];
14
- export type RenderRole = LLMRoleType | 'default' | string;
15
- export type RenderItem = FC<{ key: string } & ChatMessage & ListItemProps>;
16
- export type RenderMessage = FC<ChatMessage & { editableContent: ReactNode }>;
17
- export type RenderMessageExtra = FC<ChatMessage>;
18
- export interface RenderErrorMessage {
19
- Render?: FC<ChatMessage>;
20
- config?: AlertProps;
21
- }
22
- export type RenderAction = FC<ActionsBarProps & ChatMessage>;
23
-
24
- export interface ListItemProps {
25
- groupNav?: ChatItemProps['avatarAddon'];
26
- loading?: boolean;
27
- /**
28
- * @description 点击操作按钮的回调函数
29
- */
30
- onActionsClick?: OnActionsClick;
31
- onAvatarsClick?: OnAvatatsClick;
32
- /**
33
- * @description 消息变化的回调函数
34
- */
35
- onMessageChange?: OnMessageChange;
36
- renderActions?: {
37
- [actionKey: string]: RenderAction;
38
- };
39
- /**
40
- * @description 渲染错误消息的函数
41
- */
42
- renderErrorMessages?: {
43
- [errorType: 'default' | string]: RenderErrorMessage;
44
- };
45
- renderItems?: {
46
- [role: RenderRole]: RenderItem;
47
- };
48
- /**
49
- * @description 渲染消息的函数
50
- */
51
- renderMessages?: {
52
- [role: RenderRole]: RenderMessage;
53
- };
54
- /**
55
- * @description 渲染消息额外内容的函数
56
- */
57
- renderMessagesExtra?: {
58
- [role: RenderRole]: RenderMessageExtra;
59
- };
60
- /**
61
- * @description 是否显示聊天项的名称
62
- * @default false
63
- */
64
- showTitle?: boolean;
65
- /**
66
- * @description 文本内容
67
- */
68
- text?: ChatItemProps['text'] &
69
- ActionsBarProps['text'] & {
70
- copySuccess?: string;
71
- history?: string;
72
- } & {
73
- [key: string]: string;
74
- };
75
- /**
76
- * @description 聊天列表的类型
77
- * @default 'chat'
78
- */
79
- type?: 'docs' | 'chat';
80
- }
81
-
82
- export type ChatListItemProps = ChatMessage & ListItemProps;
83
-
84
- const Item = memo<ChatListItemProps>((props) => {
85
- const {
86
- renderMessagesExtra,
87
- showTitle,
88
- onActionsClick,
89
- onAvatarsClick,
90
- onMessageChange,
91
- type,
92
- text,
93
- renderMessages,
94
- renderErrorMessages,
95
- renderActions,
96
- loading,
97
- groupNav,
98
- renderItems,
99
- ...item
100
- } = props;
101
-
102
- const [editing, setEditing] = useState(false);
103
-
104
- const { message } = App.useApp();
105
-
106
- const RenderItem = useMemo(() => {
107
- if (!renderItems || !item?.role) return;
108
- let renderFunction;
109
- if (renderItems?.[item.role]) renderFunction = renderItems[item.role];
110
- if (!renderFunction && renderItems?.['default']) renderFunction = renderItems['default'];
111
- if (!renderFunction) return;
112
- return renderFunction;
113
- }, [renderItems?.[item.role]]);
114
-
115
- const RenderMessage = useCallback(
116
- ({ editableContent, data }: { data: ChatMessage; editableContent: ReactNode }) => {
117
- if (!renderMessages || !item?.role) return;
118
- let RenderFunction;
119
- if (renderMessages?.[item.role]) RenderFunction = renderMessages[item.role];
120
- if (!RenderFunction && renderMessages?.['default'])
121
- RenderFunction = renderMessages['default'];
122
- if (!RenderFunction) return;
123
- return <RenderFunction {...data} editableContent={editableContent} />;
124
- },
125
- [renderMessages?.[item.role]],
126
- );
127
-
128
- const MessageExtra = useCallback(
129
- ({ data }: { data: ChatMessage }) => {
130
- if (!renderMessagesExtra || !item?.role) return;
131
- let RenderFunction;
132
- if (renderMessagesExtra?.[item.role]) RenderFunction = renderMessagesExtra[item.role];
133
- if (renderMessagesExtra?.['default']) RenderFunction = renderMessagesExtra['default'];
134
- if (!RenderFunction) return;
135
- return <RenderFunction {...data} />;
136
- },
137
- [renderMessagesExtra?.[item.role]],
138
- );
139
-
140
- const ErrorMessage = useCallback(
141
- ({ data }: { data: ChatMessage }) => {
142
- if (!renderErrorMessages || !item?.error?.type) return;
143
- let RenderFunction;
144
- if (renderErrorMessages?.[item.error.type])
145
- RenderFunction = renderErrorMessages[item.error.type].Render;
146
- if (!RenderFunction && renderErrorMessages?.['default'])
147
- RenderFunction = renderErrorMessages['default'].Render;
148
- if (!RenderFunction) return;
149
- return <RenderFunction {...data} />;
150
- },
151
- [renderErrorMessages?.[item?.error?.type]],
152
- );
153
-
154
- const Actions = useCallback(
155
- ({ data }: { data: ChatMessage }) => {
156
- if (!renderActions || !item?.role) return;
157
- let RenderFunction;
158
- if (renderActions?.[item.role]) RenderFunction = renderActions[item.role];
159
- if (renderActions?.['default']) RenderFunction = renderActions['default'];
160
- if (!RenderFunction) RenderFunction = ActionsBar;
161
-
162
- const handleActionClick: ListItemProps['onActionsClick'] = async (action, data) => {
163
- switch (action.key) {
164
- case 'copy': {
165
- await copyToClipboard(data.content);
166
- message.success(text?.copySuccess || 'Copy Success');
167
- break;
168
- }
169
- case 'edit': {
170
- setEditing(true);
171
- }
172
- }
173
-
174
- onActionsClick?.(action, data);
175
- };
176
-
177
- return (
178
- <RenderFunction
179
- {...data}
180
- onActionClick={(actionKey) => handleActionClick?.(actionKey, data)}
181
- text={text}
182
- />
183
- );
184
- },
185
- [renderActions?.[item.role], text, onActionsClick],
186
- );
187
-
188
- const error = useMemo(() => {
189
- if (!item.error) return;
190
- const message = item.error?.message;
191
- let alertConfig = {};
192
- if (item.error.type && renderErrorMessages?.[item.error.type]) {
193
- alertConfig = renderErrorMessages[item.error.type]?.config as AlertProps;
194
- }
195
- return {
196
- message,
197
- ...alertConfig,
198
- };
199
- }, [renderErrorMessages, item.error]);
200
-
201
- if (RenderItem) return <RenderItem key={item.id} {...props} />;
202
-
203
- return (
204
- <ChatItem
205
- actions={<Actions data={item} />}
206
- avatar={item.meta}
207
- avatarAddon={groupNav}
208
- editing={editing}
209
- error={error}
210
- errorMessage={<ErrorMessage data={item} />}
211
- loading={loading}
212
- message={item.content}
213
- messageExtra={<MessageExtra data={item} />}
214
- onAvatarClick={onAvatarsClick?.(item.role)}
215
- onChange={(value) => onMessageChange?.(item.id, value)}
216
- onDoubleClick={(e) => {
217
- if (item.id === 'default' || item.error) return;
218
- if (item.role && ['assistant', 'user'].includes(item.role) && e.altKey) {
219
- setEditing(true);
220
- }
221
- }}
222
- onEditingChange={setEditing}
223
- placement={type === 'chat' ? (item.role === 'user' ? 'right' : 'left') : 'left'}
224
- primary={item.role === 'user'}
225
- renderMessage={(editableContent) => (
226
- <RenderMessage data={item} editableContent={editableContent} />
227
- )}
228
- showTitle={showTitle}
229
- text={text}
230
- time={item.updateAt || item.createAt}
231
- type={type === 'chat' ? 'block' : 'pure'}
232
- />
233
- );
234
- });
235
-
236
- export default Item;
@@ -1,83 +0,0 @@
1
- import isEqual from 'fast-deep-equal';
2
- import { memo } from 'react';
3
- import { useHotkeys } from 'react-hotkeys-hook';
4
- import { useTranslation } from 'react-i18next';
5
-
6
- import { PREFIX_KEY, REGENERATE_KEY } from '@/const/hotkeys';
7
- import { useChatStore } from '@/store/chat';
8
- import { chatSelectors } from '@/store/chat/selectors';
9
- import { useSessionStore } from '@/store/session';
10
- import { agentSelectors } from '@/store/session/selectors';
11
-
12
- import { renderActions, useActionsClick } from '../Actions';
13
- import { renderErrorMessages } from '../Error';
14
- import { renderMessagesExtra } from '../Extras';
15
- import { renderMessages, useAvatarsClick } from '../Messages';
16
- import ChatList from '../components/ChatList';
17
- import SkeletonList from '../components/SkeletonList';
18
-
19
- const List = memo(() => {
20
- const { t } = useTranslation('common');
21
- const meta = useSessionStore(agentSelectors.currentAgentMeta, isEqual);
22
- const data = useChatStore(chatSelectors.currentChatsWithGuideMessage(meta), isEqual);
23
-
24
- const [init, chatLoadingId, resendMessage, updateMessageContent] = useChatStore((s) => [
25
- s.messagesInit,
26
- s.chatLoadingId,
27
- s.resendMessage,
28
- s.updateMessageContent,
29
- ]);
30
-
31
- const [displayMode, enableHistoryCount, historyCount] = useSessionStore((s) => {
32
- const config = agentSelectors.currentAgentConfig(s);
33
- return [config.displayMode, config.enableHistoryCount, config.historyCount];
34
- });
35
- const onActionsClick = useActionsClick();
36
- const onAvatarsClick = useAvatarsClick();
37
-
38
- const hotkeys = [PREFIX_KEY, REGENERATE_KEY].join('+');
39
-
40
- useHotkeys(
41
- hotkeys,
42
- () => {
43
- const lastMessage = data.at(-1);
44
- if (!lastMessage || lastMessage.id === 'default' || lastMessage.role === 'system') return;
45
- resendMessage(lastMessage.id);
46
- },
47
- {
48
- enableOnFormTags: true,
49
- preventDefault: true,
50
- },
51
- );
52
-
53
- if (!init) return <SkeletonList />;
54
-
55
- return (
56
- <ChatList
57
- data={data as any}
58
- enableHistoryCount={enableHistoryCount}
59
- historyCount={historyCount}
60
- loadingId={chatLoadingId}
61
- onActionsClick={onActionsClick}
62
- onAvatarsClick={onAvatarsClick}
63
- onMessageChange={(id, content) => updateMessageContent(id, content)}
64
- renderActions={renderActions}
65
- renderErrorMessages={renderErrorMessages}
66
- renderMessages={renderMessages}
67
- renderMessagesExtra={renderMessagesExtra}
68
- text={{
69
- cancel: t('cancel'),
70
- confirm: t('ok'),
71
- copy: t('copy'),
72
- copySuccess: t('copySuccess'),
73
- delete: t('delete'),
74
- edit: t('edit'),
75
- history: t('historyRange'),
76
- regenerate: t('regenerate'),
77
- }}
78
- type={displayMode || 'chat'}
79
- />
80
- );
81
- });
82
-
83
- export default List;