@qlover/create-app 0.8.0 → 0.9.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.
- package/CHANGELOG.md +17 -0
- package/dist/configs/_common/.github/workflows/general-check.yml +1 -1
- package/dist/configs/_common/.github/workflows/release.yml +2 -2
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/templates/react-app/__tests__/src/uikit/components/chatMessage/ChatRoot.test.tsx +274 -0
- package/dist/templates/react-app/config/IOCIdentifier.ts +3 -0
- package/dist/templates/react-app/config/Identifier/components/component.chatMessage.ts +56 -0
- package/dist/templates/react-app/config/Identifier/components/component.messageBaseList.ts +103 -0
- package/dist/templates/react-app/config/Identifier/pages/index.ts +1 -0
- package/dist/templates/react-app/config/Identifier/pages/page.message.ts +20 -0
- package/dist/templates/react-app/config/app.router.ts +23 -0
- package/dist/templates/react-app/config/i18n/chatMessageI18n.ts +17 -0
- package/dist/templates/react-app/config/i18n/messageBaseListI18n.ts +22 -0
- package/dist/templates/react-app/config/i18n/messageI18n.ts +14 -0
- package/dist/templates/react-app/docs/en/components/chat-message-component.md +314 -0
- package/dist/templates/react-app/docs/en/components/chat-message-refactor.md +270 -0
- package/dist/templates/react-app/docs/en/components/message-base-list-component.md +172 -0
- package/dist/templates/react-app/docs/zh/components/chat-message-component.md +314 -0
- package/dist/templates/react-app/docs/zh/components/chat-message-refactor.md +270 -0
- package/dist/templates/react-app/docs/zh/components/message-base-list-component.md +172 -0
- package/dist/templates/react-app/playwright.config.ts +6 -6
- package/dist/templates/react-app/public/locales/en/common.json +44 -1
- package/dist/templates/react-app/public/locales/zh/common.json +44 -1
- package/dist/templates/react-app/src/pages/base/MessagePage.tsx +40 -0
- package/dist/templates/react-app/src/uikit/components/MessageBaseList.tsx +240 -0
- package/dist/templates/react-app/src/uikit/components/chatMessage/ChatMessageBridge.ts +176 -0
- package/dist/templates/react-app/src/uikit/components/chatMessage/ChatRoot.tsx +21 -0
- package/dist/templates/react-app/src/uikit/components/chatMessage/FocusBar.tsx +106 -0
- package/dist/templates/react-app/src/uikit/components/chatMessage/MessageApi.ts +271 -0
- package/dist/templates/react-app/src/uikit/components/chatMessage/MessageItem.tsx +102 -0
- package/dist/templates/react-app/src/uikit/components/chatMessage/MessagesList.tsx +86 -0
- package/package.json +1 -1
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ThreadUtil,
|
|
3
|
+
MessageStatus,
|
|
4
|
+
ChatMessageRole
|
|
5
|
+
} from '@qlover/corekit-bridge';
|
|
6
|
+
import { random } from 'lodash';
|
|
7
|
+
import type {
|
|
8
|
+
MessageStoreMsg,
|
|
9
|
+
ChatMessageStore,
|
|
10
|
+
MessageGetwayInterface,
|
|
11
|
+
GatewayOptions
|
|
12
|
+
} from '@qlover/corekit-bridge';
|
|
13
|
+
|
|
14
|
+
export class MessageApi implements MessageGetwayInterface {
|
|
15
|
+
constructor(protected messagesStore: ChatMessageStore<unknown>) {
|
|
16
|
+
this.messagesStore = messagesStore;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Send message (supports both normal and streaming modes)
|
|
21
|
+
* - If options.stream === true, use streaming mode (progressive output)
|
|
22
|
+
* - If options provided but stream !== true, use interruptible normal mode
|
|
23
|
+
* - If no options, use fast normal mode (non-interruptible)
|
|
24
|
+
*/
|
|
25
|
+
async sendMessage<M extends MessageStoreMsg<string>>(
|
|
26
|
+
message: M,
|
|
27
|
+
options?: GatewayOptions<M>
|
|
28
|
+
): Promise<M> {
|
|
29
|
+
const messageContent = message.content ?? '';
|
|
30
|
+
|
|
31
|
+
// Check if error simulation is needed
|
|
32
|
+
if (messageContent.includes('Failed') || messageContent.includes('error')) {
|
|
33
|
+
const error = new Error('Failed to send message');
|
|
34
|
+
await options?.onError?.(error);
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Determine which mode to use
|
|
39
|
+
if (options?.stream === true) {
|
|
40
|
+
// Streaming mode: progressive output
|
|
41
|
+
return this.sendStreamMode(message, options);
|
|
42
|
+
} else if (options) {
|
|
43
|
+
// Interruptible normal mode: one-time return, but supports stop
|
|
44
|
+
return this.sendInterruptibleMode(message, options);
|
|
45
|
+
} else {
|
|
46
|
+
// Fast normal mode: non-interruptible
|
|
47
|
+
return this.sendNormalMode(message);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Streaming send mode
|
|
53
|
+
* - Simulates character-by-character output
|
|
54
|
+
*/
|
|
55
|
+
private async sendStreamMode<M extends MessageStoreMsg<string>>(
|
|
56
|
+
message: M,
|
|
57
|
+
options: GatewayOptions<M>
|
|
58
|
+
): Promise<M> {
|
|
59
|
+
const messageContent = message.content ?? '';
|
|
60
|
+
|
|
61
|
+
// Simulate generated reply content
|
|
62
|
+
const responseText = `Hello! You sent: ${messageContent}. This is a streaming response that will be sent word by word to simulate real streaming behavior.`;
|
|
63
|
+
const words = responseText.split(' ');
|
|
64
|
+
|
|
65
|
+
// Create initial assistant message
|
|
66
|
+
const assistantMessageId =
|
|
67
|
+
ChatMessageRole.ASSISTANT + message.id + Date.now();
|
|
68
|
+
let accumulatedContent = '';
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
// Simulate connection establishment - call onConnected
|
|
72
|
+
await ThreadUtil.sleep(random(200, 500));
|
|
73
|
+
|
|
74
|
+
// Note: Framework will automatically intercept this call, trigger plugin system, then call user's original callback
|
|
75
|
+
await options.onConnected?.();
|
|
76
|
+
|
|
77
|
+
// Send word by word
|
|
78
|
+
for (let i = 0; i < words.length; i++) {
|
|
79
|
+
// Check if cancelled
|
|
80
|
+
if (options.signal?.aborted) {
|
|
81
|
+
throw new DOMException('Request aborted', 'AbortError');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Accumulate content
|
|
85
|
+
accumulatedContent += (i > 0 ? ' ' : '') + words[i];
|
|
86
|
+
|
|
87
|
+
// Create message object for current chunk
|
|
88
|
+
const chunkMessage = this.messagesStore.createMessage({
|
|
89
|
+
...message,
|
|
90
|
+
id: assistantMessageId,
|
|
91
|
+
role: ChatMessageRole.ASSISTANT,
|
|
92
|
+
content: accumulatedContent,
|
|
93
|
+
error: null,
|
|
94
|
+
loading: true,
|
|
95
|
+
status: MessageStatus.SENDING,
|
|
96
|
+
startTime: message.startTime,
|
|
97
|
+
endTime: 0,
|
|
98
|
+
result: null
|
|
99
|
+
}) as unknown as M;
|
|
100
|
+
|
|
101
|
+
// Call onChunk callback
|
|
102
|
+
await options.onChunk?.(chunkMessage);
|
|
103
|
+
|
|
104
|
+
// Calculate progress
|
|
105
|
+
const progress = ((i + 1) / words.length) * 100;
|
|
106
|
+
await options.onProgress?.(progress);
|
|
107
|
+
|
|
108
|
+
// Simulate network delay (random delay)
|
|
109
|
+
await ThreadUtil.sleep(random(100, 300));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Create final completed message
|
|
113
|
+
const finalMessage = this.messagesStore.createMessage({
|
|
114
|
+
...message,
|
|
115
|
+
id: assistantMessageId,
|
|
116
|
+
role: ChatMessageRole.ASSISTANT,
|
|
117
|
+
content: accumulatedContent,
|
|
118
|
+
error: null,
|
|
119
|
+
loading: false,
|
|
120
|
+
status: MessageStatus.SENT,
|
|
121
|
+
startTime: message.startTime,
|
|
122
|
+
endTime: Date.now(),
|
|
123
|
+
result: null
|
|
124
|
+
}) as unknown as M;
|
|
125
|
+
|
|
126
|
+
// Call completion callback
|
|
127
|
+
await options.onComplete?.(finalMessage);
|
|
128
|
+
|
|
129
|
+
return finalMessage;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
// Check if it's a stop error
|
|
132
|
+
if (
|
|
133
|
+
error instanceof DOMException &&
|
|
134
|
+
error.name === 'AbortError' &&
|
|
135
|
+
typeof options.onAborted === 'function'
|
|
136
|
+
) {
|
|
137
|
+
// Call onAborted callback
|
|
138
|
+
// Note: The message here contains accumulated content
|
|
139
|
+
const abortedMessage = this.messagesStore.createMessage({
|
|
140
|
+
...message,
|
|
141
|
+
id: assistantMessageId,
|
|
142
|
+
role: ChatMessageRole.ASSISTANT,
|
|
143
|
+
content: accumulatedContent,
|
|
144
|
+
error: null,
|
|
145
|
+
loading: false,
|
|
146
|
+
status: MessageStatus.STOPPED,
|
|
147
|
+
startTime: message.startTime,
|
|
148
|
+
endTime: Date.now(),
|
|
149
|
+
result: null
|
|
150
|
+
}) as unknown as M;
|
|
151
|
+
|
|
152
|
+
await options.onAborted(abortedMessage);
|
|
153
|
+
} else {
|
|
154
|
+
// Other errors, call onError callback
|
|
155
|
+
await options.onError?.(error);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Interruptible normal mode
|
|
164
|
+
* - Returns complete message at once (no character-by-character output)
|
|
165
|
+
* - Supports stop control (via signal)
|
|
166
|
+
* - Supports event callbacks (onComplete, onAborted, onError)
|
|
167
|
+
*/
|
|
168
|
+
private async sendInterruptibleMode<M extends MessageStoreMsg<string>>(
|
|
169
|
+
message: M,
|
|
170
|
+
options: GatewayOptions<M>
|
|
171
|
+
): Promise<M> {
|
|
172
|
+
const messageContent = message.content ?? '';
|
|
173
|
+
|
|
174
|
+
// Simulate connection establishment - call onConnected
|
|
175
|
+
await options.onConnected?.();
|
|
176
|
+
|
|
177
|
+
// Simulate random delay
|
|
178
|
+
const times = random(200, 1000);
|
|
179
|
+
|
|
180
|
+
// Check if cancelled during delay
|
|
181
|
+
const startTime = Date.now();
|
|
182
|
+
while (Date.now() - startTime < times) {
|
|
183
|
+
if (options.signal?.aborted) {
|
|
184
|
+
// Cancelled, create stopped message
|
|
185
|
+
const abortedMessage = this.messagesStore.createMessage({
|
|
186
|
+
...message,
|
|
187
|
+
id: ChatMessageRole.ASSISTANT + message.id,
|
|
188
|
+
role: ChatMessageRole.ASSISTANT,
|
|
189
|
+
content: '', // Content not generated yet
|
|
190
|
+
error: null,
|
|
191
|
+
loading: false,
|
|
192
|
+
status: MessageStatus.STOPPED,
|
|
193
|
+
endTime: Date.now(),
|
|
194
|
+
result: null
|
|
195
|
+
}) as unknown as M;
|
|
196
|
+
|
|
197
|
+
// Call onAborted
|
|
198
|
+
await options.onAborted?.(abortedMessage);
|
|
199
|
+
throw new DOMException('Request aborted', 'AbortError');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Check every 50ms
|
|
203
|
+
await ThreadUtil.sleep(50);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Check if error simulation is needed
|
|
207
|
+
if (messageContent.includes('Failed') || messageContent.includes('error')) {
|
|
208
|
+
const error = new Error('Failed to send message');
|
|
209
|
+
await options.onError?.(error);
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (times % 5 === 0) {
|
|
214
|
+
const error = new Error(`Network error(${times})`);
|
|
215
|
+
await options.onError?.(error);
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const endTime = Date.now();
|
|
220
|
+
const finalMessage = this.messagesStore.createMessage({
|
|
221
|
+
...message,
|
|
222
|
+
id: ChatMessageRole.ASSISTANT + message.id,
|
|
223
|
+
role: ChatMessageRole.ASSISTANT,
|
|
224
|
+
content: `(${endTime - message.startTime}ms) Hello! You sent: ${message.content}`,
|
|
225
|
+
error: null,
|
|
226
|
+
loading: false,
|
|
227
|
+
status: MessageStatus.SENT,
|
|
228
|
+
endTime: endTime,
|
|
229
|
+
result: null
|
|
230
|
+
}) as unknown as M;
|
|
231
|
+
|
|
232
|
+
// Call onComplete
|
|
233
|
+
await options.onComplete?.(finalMessage);
|
|
234
|
+
|
|
235
|
+
return finalMessage;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Fast normal mode
|
|
240
|
+
* - Returns complete message at once
|
|
241
|
+
* - Non-interruptible (doesn't check signal)
|
|
242
|
+
*/
|
|
243
|
+
private async sendNormalMode<M extends MessageStoreMsg<string>>(
|
|
244
|
+
message: M
|
|
245
|
+
): Promise<M> {
|
|
246
|
+
const times = random(200, 1000);
|
|
247
|
+
await ThreadUtil.sleep(times);
|
|
248
|
+
|
|
249
|
+
const messageContent = message.content ?? '';
|
|
250
|
+
if (messageContent.includes('Failed') || messageContent.includes('error')) {
|
|
251
|
+
throw new Error('Failed to send message');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (times % 5 === 0) {
|
|
255
|
+
throw new Error(`Network error(${times})`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const endTime = Date.now();
|
|
259
|
+
return this.messagesStore.createMessage({
|
|
260
|
+
...message,
|
|
261
|
+
id: ChatMessageRole.ASSISTANT + message.id,
|
|
262
|
+
role: ChatMessageRole.ASSISTANT,
|
|
263
|
+
content: `(${endTime - message.startTime}ms) Hello! You sent: ${message.content}`,
|
|
264
|
+
error: null,
|
|
265
|
+
loading: false,
|
|
266
|
+
status: MessageStatus.SENT,
|
|
267
|
+
endTime: endTime,
|
|
268
|
+
result: null
|
|
269
|
+
}) as unknown as M;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { ReloadOutlined } from '@ant-design/icons';
|
|
2
|
+
import { ChatMessageRole } from '@qlover/corekit-bridge';
|
|
3
|
+
import { Spin, Button } from 'antd';
|
|
4
|
+
import { clsx } from 'clsx';
|
|
5
|
+
import { useMemo } from 'react';
|
|
6
|
+
import type { ChatMessageI18nInterface } from '@config/i18n/chatMessageI18n';
|
|
7
|
+
import type {
|
|
8
|
+
ChatMessage,
|
|
9
|
+
ChatMessageBridgeInterface
|
|
10
|
+
} from '@qlover/corekit-bridge';
|
|
11
|
+
|
|
12
|
+
export interface MessageItemProps<T, MessageType extends ChatMessage<T>> {
|
|
13
|
+
message: MessageType;
|
|
14
|
+
index: number;
|
|
15
|
+
bridge?: ChatMessageBridgeInterface<T>;
|
|
16
|
+
tt: ChatMessageI18nInterface;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function MessageItem<T, MessageType extends ChatMessage<T>>({
|
|
20
|
+
message,
|
|
21
|
+
bridge,
|
|
22
|
+
index,
|
|
23
|
+
tt
|
|
24
|
+
}: MessageItemProps<T, MessageType>) {
|
|
25
|
+
const messageText = useMemo(() => {
|
|
26
|
+
return message.content as string;
|
|
27
|
+
}, [message.content]);
|
|
28
|
+
|
|
29
|
+
const duration = useMemo(() => {
|
|
30
|
+
return message.endTime ? message.endTime - message.startTime : 0;
|
|
31
|
+
}, [message.endTime, message.startTime]);
|
|
32
|
+
|
|
33
|
+
const errorMessage = useMemo(() => {
|
|
34
|
+
return message.error instanceof Error ? message.error.message : null;
|
|
35
|
+
}, [message.error]);
|
|
36
|
+
|
|
37
|
+
const isUserMessage = message.role === ChatMessageRole.USER;
|
|
38
|
+
const isAssistant = message.role === ChatMessageRole.ASSISTANT;
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div
|
|
42
|
+
data-testid="MessageItem"
|
|
43
|
+
data-message-id={message.id}
|
|
44
|
+
data-message-role={message.role}
|
|
45
|
+
data-message-index={index}
|
|
46
|
+
className={clsx('w-full flex mb-3', {
|
|
47
|
+
'justify-end': isUserMessage,
|
|
48
|
+
'justify-start': !isUserMessage
|
|
49
|
+
})}
|
|
50
|
+
>
|
|
51
|
+
<div
|
|
52
|
+
className={clsx('flex flex-col px-4 py-3 rounded-xl shadow-sm', {
|
|
53
|
+
'max-w-[80%] bg-blue-500 text-white': isUserMessage,
|
|
54
|
+
'max-w-[85%] bg-secondary border border-primary': isAssistant
|
|
55
|
+
})}
|
|
56
|
+
>
|
|
57
|
+
{/* Message content */}
|
|
58
|
+
<div className="flex items-start gap-2">
|
|
59
|
+
{isUserMessage && !message.loading && (
|
|
60
|
+
<Button
|
|
61
|
+
type="text"
|
|
62
|
+
size="small"
|
|
63
|
+
icon={<ReloadOutlined className="text-white" />}
|
|
64
|
+
disabled={message.loading}
|
|
65
|
+
onClick={() => bridge?.send(message)}
|
|
66
|
+
className="flex-shrink-0 hover:bg-blue-600"
|
|
67
|
+
title={tt.retry}
|
|
68
|
+
/>
|
|
69
|
+
)}
|
|
70
|
+
|
|
71
|
+
<div
|
|
72
|
+
data-testid="MessageContent"
|
|
73
|
+
className={clsx('flex-1 whitespace-pre-wrap break-words', {
|
|
74
|
+
'text-white': isUserMessage,
|
|
75
|
+
'text-text': isAssistant
|
|
76
|
+
})}
|
|
77
|
+
>
|
|
78
|
+
{message.loading && <Spin size="small" className="mr-2" />}
|
|
79
|
+
{messageText}
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
{/* Duration for user messages */}
|
|
84
|
+
{isUserMessage && duration > 0 && (
|
|
85
|
+
<div className="text-right text-xs mt-1 opacity-80">
|
|
86
|
+
{tt.duration}: {duration}ms
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
89
|
+
|
|
90
|
+
{/* Error message */}
|
|
91
|
+
{errorMessage && (
|
|
92
|
+
<div
|
|
93
|
+
data-testid="MessageError"
|
|
94
|
+
className="mt-2 text-sm p-2 bg-red-100 text-red-700 rounded border border-red-200"
|
|
95
|
+
>
|
|
96
|
+
❌ {errorMessage}
|
|
97
|
+
</div>
|
|
98
|
+
)}
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { useStore } from '@brain-toolkit/react-kit';
|
|
2
|
+
import { useCallback, type ComponentType } from 'react';
|
|
3
|
+
import type { ChatMessageI18nInterface } from '@config/i18n/chatMessageI18n';
|
|
4
|
+
import { MessageItem, type MessageItemProps } from './MessageItem';
|
|
5
|
+
import type {
|
|
6
|
+
ChatMessage,
|
|
7
|
+
ChatMessageBridgeInterface
|
|
8
|
+
} from '@qlover/corekit-bridge';
|
|
9
|
+
|
|
10
|
+
export type MessageComponentType<
|
|
11
|
+
T,
|
|
12
|
+
MessageType extends ChatMessage<T>
|
|
13
|
+
> = ComponentType<MessageItemProps<T, MessageType>>;
|
|
14
|
+
|
|
15
|
+
export type GetMessageComponent<T, MessageType extends ChatMessage<T>> = (
|
|
16
|
+
props: MessageItemProps<T, MessageType>
|
|
17
|
+
) => MessageComponentType<T, MessageType>;
|
|
18
|
+
|
|
19
|
+
export interface MessagesListProps<T, MessageType extends ChatMessage<T>> {
|
|
20
|
+
bridge: ChatMessageBridgeInterface<T>;
|
|
21
|
+
tt: ChatMessageI18nInterface;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get message component
|
|
25
|
+
*
|
|
26
|
+
* @default `MessageItem`
|
|
27
|
+
*
|
|
28
|
+
* @type `ComponentType<MessageItemProps<MessageType>>`
|
|
29
|
+
*/
|
|
30
|
+
getMessageComponent?: GetMessageComponent<T, MessageType>;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get message unique identifier
|
|
34
|
+
*
|
|
35
|
+
* @default `(message.id ?? '') + (message.startTime ?? 0) + index`
|
|
36
|
+
*
|
|
37
|
+
* @param message Message
|
|
38
|
+
* @param index Message index
|
|
39
|
+
* @returns Message unique identifier
|
|
40
|
+
*/
|
|
41
|
+
getMessageKey?(message: MessageType, index: number): string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function MessagesList<T = string>({
|
|
45
|
+
bridge,
|
|
46
|
+
tt,
|
|
47
|
+
getMessageComponent,
|
|
48
|
+
getMessageKey
|
|
49
|
+
}: MessagesListProps<T, ChatMessage<T>>) {
|
|
50
|
+
const messagesStore = bridge.getMessageStore();
|
|
51
|
+
const messages = useStore(messagesStore, (state) => state.messages);
|
|
52
|
+
|
|
53
|
+
const _getMessageKey = useCallback(
|
|
54
|
+
(message: ChatMessage<T>, index: number): string => {
|
|
55
|
+
return (
|
|
56
|
+
getMessageKey?.(message, index) ||
|
|
57
|
+
String((message.id ?? '') + (message.startTime ?? 0) + index)
|
|
58
|
+
);
|
|
59
|
+
},
|
|
60
|
+
[getMessageKey]
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div
|
|
65
|
+
data-testid="MessagesList"
|
|
66
|
+
className="flex-1 overflow-y-auto p-4 space-y-2"
|
|
67
|
+
>
|
|
68
|
+
{messages.length === 0 ? (
|
|
69
|
+
<div className="flex items-center justify-center h-full">
|
|
70
|
+
<div className="text-center text-text-secondary">
|
|
71
|
+
<p className="text-base mb-1">{tt.empty}</p>
|
|
72
|
+
<p className="text-sm">{tt.start}</p>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
) : (
|
|
76
|
+
messages.map((message, index) => {
|
|
77
|
+
const key = _getMessageKey(message, index);
|
|
78
|
+
const props = { message, index, bridge, tt };
|
|
79
|
+
const Component = getMessageComponent?.(props) || MessageItem;
|
|
80
|
+
|
|
81
|
+
return <Component data-testid="MessageItem" key={key} {...props} />;
|
|
82
|
+
})
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|