@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.
Files changed (33) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/configs/_common/.github/workflows/general-check.yml +1 -1
  3. package/dist/configs/_common/.github/workflows/release.yml +2 -2
  4. package/dist/index.cjs +1 -1
  5. package/dist/index.js +1 -1
  6. package/dist/templates/react-app/__tests__/src/uikit/components/chatMessage/ChatRoot.test.tsx +274 -0
  7. package/dist/templates/react-app/config/IOCIdentifier.ts +3 -0
  8. package/dist/templates/react-app/config/Identifier/components/component.chatMessage.ts +56 -0
  9. package/dist/templates/react-app/config/Identifier/components/component.messageBaseList.ts +103 -0
  10. package/dist/templates/react-app/config/Identifier/pages/index.ts +1 -0
  11. package/dist/templates/react-app/config/Identifier/pages/page.message.ts +20 -0
  12. package/dist/templates/react-app/config/app.router.ts +23 -0
  13. package/dist/templates/react-app/config/i18n/chatMessageI18n.ts +17 -0
  14. package/dist/templates/react-app/config/i18n/messageBaseListI18n.ts +22 -0
  15. package/dist/templates/react-app/config/i18n/messageI18n.ts +14 -0
  16. package/dist/templates/react-app/docs/en/components/chat-message-component.md +314 -0
  17. package/dist/templates/react-app/docs/en/components/chat-message-refactor.md +270 -0
  18. package/dist/templates/react-app/docs/en/components/message-base-list-component.md +172 -0
  19. package/dist/templates/react-app/docs/zh/components/chat-message-component.md +314 -0
  20. package/dist/templates/react-app/docs/zh/components/chat-message-refactor.md +270 -0
  21. package/dist/templates/react-app/docs/zh/components/message-base-list-component.md +172 -0
  22. package/dist/templates/react-app/playwright.config.ts +6 -6
  23. package/dist/templates/react-app/public/locales/en/common.json +44 -1
  24. package/dist/templates/react-app/public/locales/zh/common.json +44 -1
  25. package/dist/templates/react-app/src/pages/base/MessagePage.tsx +40 -0
  26. package/dist/templates/react-app/src/uikit/components/MessageBaseList.tsx +240 -0
  27. package/dist/templates/react-app/src/uikit/components/chatMessage/ChatMessageBridge.ts +176 -0
  28. package/dist/templates/react-app/src/uikit/components/chatMessage/ChatRoot.tsx +21 -0
  29. package/dist/templates/react-app/src/uikit/components/chatMessage/FocusBar.tsx +106 -0
  30. package/dist/templates/react-app/src/uikit/components/chatMessage/MessageApi.ts +271 -0
  31. package/dist/templates/react-app/src/uikit/components/chatMessage/MessageItem.tsx +102 -0
  32. package/dist/templates/react-app/src/uikit/components/chatMessage/MessagesList.tsx +86 -0
  33. 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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qlover/create-app",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Create a new app with a single command",
5
5
  "private": false,
6
6
  "type": "module",