@qlover/create-app 0.8.0 → 0.10.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 (85) hide show
  1. package/CHANGELOG.md +32 -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/next-app/.env.template +9 -10
  7. package/dist/templates/next-app/eslint.config.mjs +5 -0
  8. package/dist/templates/next-app/next.config.ts +1 -1
  9. package/dist/templates/next-app/src/app/[locale]/login/page.tsx +1 -1
  10. package/dist/templates/next-app/src/app/[locale]/page.tsx +1 -1
  11. package/dist/templates/next-app/src/app/[locale]/register/page.tsx +1 -1
  12. package/dist/templates/next-app/src/app/api/locales/json/route.ts +2 -1
  13. package/dist/templates/next-app/src/i18n/request.ts +2 -2
  14. package/dist/templates/react-app/__tests__/__mocks__/{MockAppConfit.ts → MockAppConfig.ts} +1 -1
  15. package/dist/templates/react-app/__tests__/__mocks__/components/TestApp.tsx +10 -17
  16. package/dist/templates/react-app/__tests__/__mocks__/components/TestBootstrapsProvider.tsx +27 -8
  17. package/dist/templates/react-app/__tests__/__mocks__/createMockGlobals.ts +1 -1
  18. package/dist/templates/react-app/__tests__/__mocks__/i18nextHttpBackend.ts +110 -0
  19. package/dist/templates/react-app/__tests__/__mocks__/testIOC/TestIOCRegister.ts +3 -2
  20. package/dist/templates/react-app/__tests__/setup/setupGlobal.ts +13 -0
  21. package/dist/templates/react-app/__tests__/src/base/services/I18nService.test.ts +3 -1
  22. package/dist/templates/react-app/__tests__/src/uikit/components/chatMessage/ChatRoot.test.tsx +274 -0
  23. package/dist/templates/react-app/config/IOCIdentifier.ts +9 -3
  24. package/dist/templates/react-app/config/Identifier/components/component.chatMessage.ts +56 -0
  25. package/dist/templates/react-app/config/Identifier/components/component.messageBaseList.ts +103 -0
  26. package/dist/templates/react-app/config/Identifier/pages/index.ts +1 -0
  27. package/dist/templates/react-app/config/Identifier/pages/page.message.ts +20 -0
  28. package/dist/templates/react-app/config/app.router.ts +23 -0
  29. package/dist/templates/react-app/config/common.ts +38 -0
  30. package/dist/templates/react-app/config/feapi.mock.json +5 -12
  31. package/dist/templates/react-app/config/i18n/chatMessageI18n.ts +17 -0
  32. package/dist/templates/react-app/config/i18n/messageBaseListI18n.ts +22 -0
  33. package/dist/templates/react-app/config/i18n/messageI18n.ts +14 -0
  34. package/dist/templates/react-app/docs/en/components/chat-message-component.md +314 -0
  35. package/dist/templates/react-app/docs/en/components/chat-message-refactor.md +270 -0
  36. package/dist/templates/react-app/docs/en/components/message-base-list-component.md +172 -0
  37. package/dist/templates/react-app/docs/zh/components/chat-message-component.md +314 -0
  38. package/dist/templates/react-app/docs/zh/components/chat-message-refactor.md +270 -0
  39. package/dist/templates/react-app/docs/zh/components/message-base-list-component.md +172 -0
  40. package/dist/templates/react-app/eslint.config.mjs +6 -5
  41. package/dist/templates/react-app/package.json +1 -1
  42. package/dist/templates/react-app/playwright.config.ts +6 -6
  43. package/dist/templates/react-app/public/locales/en/common.json +44 -1
  44. package/dist/templates/react-app/public/locales/zh/common.json +44 -1
  45. package/dist/templates/react-app/src/base/apis/userApi/UserApi.ts +22 -13
  46. package/dist/templates/react-app/src/base/apis/userApi/UserApiBootstarp.ts +3 -3
  47. package/dist/templates/react-app/src/base/apis/userApi/UserApiType.ts +17 -12
  48. package/dist/templates/react-app/src/base/cases/I18nKeyErrorPlugin.ts +19 -2
  49. package/dist/templates/react-app/src/base/port/RouteServiceInterface.ts +2 -4
  50. package/dist/templates/react-app/src/base/port/UserServiceInterface.ts +15 -9
  51. package/dist/templates/react-app/src/base/services/BaseLayoutService.ts +55 -0
  52. package/dist/templates/react-app/src/base/services/I18nService.ts +1 -0
  53. package/dist/templates/react-app/src/base/services/UserBootstrap.ts +43 -0
  54. package/dist/templates/react-app/src/base/services/UserGatewayPlugin.ts +16 -0
  55. package/dist/templates/react-app/src/base/services/UserService.ts +51 -80
  56. package/dist/templates/react-app/src/core/bootstraps/BootstrapClient.ts +8 -3
  57. package/dist/templates/react-app/src/core/bootstraps/BootstrapsRegistry.ts +6 -6
  58. package/dist/templates/react-app/src/core/bootstraps/SaveAppInfo.ts +28 -0
  59. package/dist/templates/react-app/src/core/clientIoc/ClientIOCRegister.ts +24 -18
  60. package/dist/templates/react-app/src/core/globals.ts +10 -11
  61. package/dist/templates/react-app/src/main.tsx +1 -1
  62. package/dist/templates/react-app/src/pages/auth/Layout.tsx +4 -4
  63. package/dist/templates/react-app/src/pages/auth/RegisterPage.tsx +1 -1
  64. package/dist/templates/react-app/src/pages/base/Layout.tsx +3 -3
  65. package/dist/templates/react-app/src/pages/base/MessagePage.tsx +40 -0
  66. package/dist/templates/react-app/src/uikit/components/BaseLayoutProvider.tsx +44 -0
  67. package/dist/templates/react-app/src/uikit/components/LogoutButton.tsx +1 -3
  68. package/dist/templates/react-app/src/uikit/components/MessageBaseList.tsx +240 -0
  69. package/dist/templates/react-app/src/uikit/components/chatMessage/ChatMessageBridge.ts +176 -0
  70. package/dist/templates/react-app/src/uikit/components/chatMessage/ChatRoot.tsx +21 -0
  71. package/dist/templates/react-app/src/uikit/components/chatMessage/FocusBar.tsx +106 -0
  72. package/dist/templates/react-app/src/uikit/components/chatMessage/MessageApi.ts +271 -0
  73. package/dist/templates/react-app/src/uikit/components/chatMessage/MessageItem.tsx +102 -0
  74. package/dist/templates/react-app/src/uikit/components/chatMessage/MessagesList.tsx +86 -0
  75. package/dist/templates/react-app/src/uikit/hooks/useNavigateBridge.ts +9 -0
  76. package/dist/templates/react-app/src/uikit/hooks/{useI18nGuard.ts → useRouterI18nGuard.ts} +7 -4
  77. package/dist/templates/react-app/tsconfig.app.json +4 -2
  78. package/dist/templates/react-app/tsconfig.node.json +4 -0
  79. package/dist/templates/react-app/tsconfig.test.json +3 -1
  80. package/package.json +3 -3
  81. package/dist/templates/react-app/__tests__/src/base/cases/AppError.test.ts +0 -102
  82. package/dist/templates/react-app/__tests__/src/main.integration.test.tsx +0 -61
  83. package/dist/templates/react-app/src/base/services/ProcesserExecutor.ts +0 -57
  84. package/dist/templates/react-app/src/uikit/components/ProcessExecutorProvider.tsx +0 -28
  85. package/dist/templates/react-app/src/uikit/components/UserAuthProvider.tsx +0 -16
@@ -14,8 +14,8 @@ import { RequestStatusCatcher } from '@/base/cases/RequestStatusCatcher';
14
14
  import type { IocRegisterOptions } from '@/base/port/IOCInterface';
15
15
  import type { UserServiceInterface } from '@/base/port/UserServiceInterface';
16
16
  import { I18nService } from '@/base/services/I18nService';
17
- import { ProcesserExecutor } from '@/base/services/ProcesserExecutor';
18
17
  import { RouteService } from '@/base/services/RouteService';
18
+ import { UserGatewayPlugin } from '@/base/services/UserGatewayPlugin';
19
19
  import { UserService } from '@/base/services/UserService';
20
20
  import { ExecutorPageBridge } from '@/uikit/bridges/ExecutorPageBridge';
21
21
  import { JSONStoragePageBridge } from '@/uikit/bridges/JSONStoragePageBridge';
@@ -28,7 +28,6 @@ import type {
28
28
  IOCContainerInterface,
29
29
  IOCRegisterInterface
30
30
  } from '@qlover/corekit-bridge';
31
- import type { SyncStorageInterface } from '@qlover/fe-corekit';
32
31
  import type { LoggerInterface } from '@qlover/logger';
33
32
 
34
33
  export class ClientIOCRegister
@@ -71,30 +70,34 @@ export class ClientIOCRegister
71
70
  */
72
71
  protected registerImplement(ioc: IOCContainerInterface): void {
73
72
  ioc.bind(I.I18nServiceInterface, new I18nService(this.options.pathname));
74
- ioc.bind(
75
- I.RouteServiceInterface,
76
- new RouteService(
77
- ioc.get(NavigateBridge),
78
- ioc.get(I.I18nServiceInterface),
79
- {
80
- routes: useLocaleRoutes ? baseRoutes : baseNoLocaleRoutes,
81
- logger: ioc.get(I.Logger),
82
- hasLocalRoutes: useLocaleRoutes,
83
- routerPrefix: routerPrefix
84
- }
85
- )
73
+
74
+ const routeService = new RouteService(
75
+ ioc.get(NavigateBridge),
76
+ ioc.get(I.I18nServiceInterface),
77
+ {
78
+ routes: useLocaleRoutes ? baseRoutes : baseNoLocaleRoutes,
79
+ logger: ioc.get(I.Logger),
80
+ hasLocalRoutes: useLocaleRoutes,
81
+ routerPrefix: routerPrefix
82
+ }
86
83
  );
84
+ ioc.bind(I.RouteServiceInterface, routeService);
87
85
  ioc.bind(
88
86
  I.ThemeService,
89
87
  new ThemeService({
90
88
  ...(themeConfig as unknown as ThemeServiceProps),
91
- storage: ioc.get<SyncStorageInterface<string, string>>(I.LocalStorage)
89
+ storage: ioc.get(I.LocalStorage)
92
90
  })
93
91
  );
94
92
 
95
93
  ioc.bind(I.I18nKeyErrorPlugin, ioc.get(I18nKeyErrorPlugin));
96
- ioc.bind(I.ProcesserExecutorInterface, ioc.get(ProcesserExecutor));
97
- ioc.bind(I.UserServiceInterface, ioc.get(UserService));
94
+
95
+ ioc.bind(
96
+ I.UserServiceInterface,
97
+ ioc
98
+ .get<UserServiceInterface>(UserService)
99
+ .use(new UserGatewayPlugin(routeService))
100
+ );
98
101
  ioc.bind(I.RequestCatcherInterface, ioc.get(RequestStatusCatcher));
99
102
  ioc.bind(I.ExecutorPageBridgeInterface, ioc.get(ExecutorPageBridge));
100
103
  ioc.bind(I.JSONStoragePageInterface, ioc.get(JSONStoragePageBridge));
@@ -111,7 +114,10 @@ export class ClientIOCRegister
111
114
  token: () =>
112
115
  ioc.get<UserServiceInterface>(I.UserServiceInterface).getToken()
113
116
  });
114
- const apiMockPlugin = new ApiMockPlugin(mockDataJson, logger);
117
+ const apiMockPlugin = new ApiMockPlugin({
118
+ mockData: mockDataJson,
119
+ logger: logger
120
+ });
115
121
  const apiCatchPlugin = new ApiCatchPlugin(
116
122
  logger,
117
123
  ioc.get(RequestStatusCatcher)
@@ -1,17 +1,13 @@
1
1
  // ! global variables, don't import any dependencies and don't have side effects
2
2
  import { loggerStyles } from '@config/common';
3
- import {
4
- ColorFormatter,
5
- ConsoleHandler,
6
- CookieStorage,
7
- Logger
8
- } from '@qlover/corekit-bridge';
3
+ import { ColorFormatter, CookieStorage } from '@qlover/corekit-bridge';
9
4
  import {
10
5
  Base64Serializer,
11
6
  JSONSerializer,
12
7
  ObjectStorage,
13
8
  SyncStorage
14
9
  } from '@qlover/fe-corekit';
10
+ import { Logger, ConsoleHandler } from '@qlover/logger';
15
11
  import { AppConfig } from '@/base/cases/AppConfig';
16
12
  import { DialogHandler } from '@/base/cases/DialogHandler';
17
13
  import type { SyncStorageInterface } from '@qlover/fe-corekit';
@@ -37,11 +33,14 @@ export const JSON = new JSONSerializer();
37
33
  /**
38
34
  * Override localStorage to use the global local storage
39
35
  */
40
- export const localStorage = new SyncStorage(new ObjectStorage(), [
41
- JSON,
42
- new Base64Serializer(),
43
- window.localStorage as unknown as SyncStorageInterface<string>
44
- ]);
36
+ export const localStorage = new SyncStorage<string>(
37
+ new ObjectStorage(),
38
+ [
39
+ JSON,
40
+ appConfig.isProduction ? new Base64Serializer() : undefined,
41
+ window.localStorage as unknown as SyncStorageInterface<string>
42
+ ].filter(Boolean) as any[]
43
+ );
45
44
 
46
45
  export const localStorageEncrypt = localStorage;
47
46
 
@@ -4,7 +4,7 @@ import { StrictMode } from 'react';
4
4
  import { createRoot } from 'react-dom/client';
5
5
  import App from './App.tsx';
6
6
  import { BootstrapClient } from './core/bootstraps/BootstrapClient';
7
- import { clientIOC } from './core/clientIoc/ClientIOC.ts';
7
+ import { clientIOC } from './core/clientIoc/ClientIOC';
8
8
 
9
9
  BootstrapClient.main({
10
10
  root: window,
@@ -1,18 +1,18 @@
1
1
  import { useStore } from '@brain-toolkit/react-kit/hooks/useStore';
2
2
  import { IOCIdentifier } from '@config/IOCIdentifier';
3
3
  import { Navigate, Outlet } from 'react-router-dom';
4
- import { useI18nGuard } from '@/uikit/hooks/useI18nGuard';
5
4
  import { useIOC } from '@/uikit/hooks/useIOC';
5
+ import { useRouterI18nGuard } from '@/uikit/hooks/useRouterI18nGuard';
6
6
  import { BaseHeader } from '../../uikit/components/BaseHeader';
7
7
 
8
8
  export default function Layout() {
9
9
  const userService = useIOC(IOCIdentifier.UserServiceInterface);
10
- useStore(userService.store);
10
+ const state = useStore(userService.getStore());
11
11
 
12
- useI18nGuard();
12
+ useRouterI18nGuard();
13
13
 
14
14
  // If user is authenticated, redirect to home page
15
- if (userService.isAuthenticated()) {
15
+ if (userService.isAuthenticated() && userService.isUserInfo(state.result)) {
16
16
  return <Navigate data-testid="Layout" to="/" replace />;
17
17
  }
18
18
 
@@ -3,7 +3,7 @@ import { register18n } from '@config/i18n/register18n';
3
3
  import { IOCIdentifier } from '@config/IOCIdentifier';
4
4
  import { Form, Input, Button, Checkbox } from 'antd';
5
5
  import { useState } from 'react';
6
- import type { RegisterFormData } from '@/base/services/UserService';
6
+ import type { RegisterFormData } from '@/base/apis/userApi/UserApiType';
7
7
  import { useI18nInterface } from '@/uikit/hooks/useI18nInterface';
8
8
  import { useIOC } from '@/uikit/hooks/useIOC';
9
9
 
@@ -1,10 +1,10 @@
1
1
  import { Outlet } from 'react-router-dom';
2
- import { ProcessExecutorProvider } from '@/uikit/components/ProcessExecutorProvider';
2
+ import { BaseLayoutProvider } from '@/uikit/components/BaseLayoutProvider';
3
3
  import { BaseHeader } from '../../uikit/components/BaseHeader';
4
4
 
5
5
  export default function Layout() {
6
6
  return (
7
- <ProcessExecutorProvider data-testid="Layout">
7
+ <BaseLayoutProvider data-testid="Layout">
8
8
  <div
9
9
  data-testid="basic-layout"
10
10
  className="text-base min-h-screen bg-primary"
@@ -15,6 +15,6 @@ export default function Layout() {
15
15
  <Outlet />
16
16
  </div>
17
17
  </div>
18
- </ProcessExecutorProvider>
18
+ </BaseLayoutProvider>
19
19
  );
20
20
  }
@@ -0,0 +1,40 @@
1
+ import { useFactory } from '@brain-toolkit/react-kit';
2
+ import { chatMessageI18n } from '@config/i18n/chatMessageI18n';
3
+ import {
4
+ ChatMessageStore,
5
+ ChatSenderStrategy,
6
+ SendFailureStrategy
7
+ } from '@qlover/corekit-bridge';
8
+ import { useState } from 'react';
9
+ import { logger } from '@/core/globals';
10
+ import { ChatMessageBridge } from '@/uikit/components/chatMessage/ChatMessageBridge';
11
+ import { ChatRoot } from '@/uikit/components/chatMessage/ChatRoot';
12
+ import { MessageApi } from '@/uikit/components/chatMessage/MessageApi';
13
+ import { MessageBaseList } from '@/uikit/components/MessageBaseList';
14
+ import { useI18nInterface } from '@/uikit/hooks/useI18nInterface';
15
+
16
+ export default function MessagePage() {
17
+ const tt = useI18nInterface(chatMessageI18n);
18
+ const messagesStore = useFactory(ChatMessageStore<string>);
19
+ const messageApi = useFactory(MessageApi, messagesStore);
20
+
21
+ const [bridge] = useState(() => {
22
+ return new ChatMessageBridge<string>(messagesStore, {
23
+ gateway: messageApi,
24
+ logger: logger,
25
+ senderName: 'ChatSender',
26
+ gatewayOptions: { stream: true }
27
+ }).use(new ChatSenderStrategy(SendFailureStrategy.KEEP_FAILED, logger));
28
+ });
29
+
30
+ return (
31
+ <div
32
+ data-testid="MessagePage"
33
+ className="min-h-screen bg-primary py-8 px-4 sm:px-6 lg:px-8"
34
+ >
35
+ <MessageBaseList />
36
+
37
+ <ChatRoot bridge={bridge} tt={tt} />
38
+ </div>
39
+ );
40
+ }
@@ -0,0 +1,44 @@
1
+ import { useStore } from '@brain-toolkit/react-kit';
2
+ import { I } from '@config/IOCIdentifier';
3
+ import { BaseLayoutService } from '@/base/services/BaseLayoutService';
4
+ import { Loading } from './Loading';
5
+ import { useIOC } from '../hooks/useIOC';
6
+ import { useNavigateBridge } from '../hooks/useNavigateBridge';
7
+ import { useRouterI18nGuard } from '../hooks/useRouterI18nGuard';
8
+ import { useStrictEffect } from '../hooks/useStrictEffect';
9
+
10
+ /**
11
+ * BaseLayoutProvider
12
+ *
13
+ * /pages/base/Layout.tsx 布局组件的服务提供者
14
+ *
15
+ * - 验证用户信息渲染组件,如果用户未登录则渲染 loading
16
+ *
17
+ * @param children - The children to render
18
+ * @returns
19
+ */
20
+ export function BaseLayoutProvider({
21
+ children
22
+ }: {
23
+ children: React.ReactNode;
24
+ }) {
25
+ const ioc = useIOC();
26
+ const baseLayoutService = ioc(BaseLayoutService);
27
+ const userService = ioc(I.UserServiceInterface);
28
+
29
+ useStore(userService.getStore());
30
+
31
+ useRouterI18nGuard();
32
+
33
+ useNavigateBridge();
34
+
35
+ useStrictEffect(() => {
36
+ baseLayoutService.use(ioc(I.I18nKeyErrorPlugin)).starup(ioc);
37
+ }, []);
38
+
39
+ if (!userService.isAuthenticated()) {
40
+ return <Loading data-testid="BaseLayoutProviderLoading" fullscreen />;
41
+ }
42
+
43
+ return children;
44
+ }
@@ -29,9 +29,7 @@ export function LogoutButton() {
29
29
  'data-testid': 'LogoutButton-CancelButton'
30
30
  },
31
31
  content: tContent,
32
- onOk: () => {
33
- userService.logout();
34
- }
32
+ onOk: () => userService.logout()
35
33
  });
36
34
  }, [tTitle, tContent]);
37
35
 
@@ -0,0 +1,240 @@
1
+ import { useFactory, useStore } from '@brain-toolkit/react-kit';
2
+ import { messageBaseListI18n } from '@config/i18n/messageBaseListI18n';
3
+ import { I } from '@config/IOCIdentifier';
4
+ import {
5
+ MessageSender,
6
+ MessagesStore,
7
+ MessageStatus,
8
+ SenderStrategyPlugin,
9
+ SendFailureStrategy,
10
+ ThreadUtil
11
+ } from '@qlover/corekit-bridge';
12
+ import { Button, Input } from 'antd';
13
+ import { random } from 'lodash';
14
+ import { useCallback, useMemo, useState } from 'react';
15
+ import { useI18nInterface } from '../hooks/useI18nInterface';
16
+ import { useIOC } from '../hooks/useIOC';
17
+ import type {
18
+ MessageGetwayInterface,
19
+ MessagesStateInterface,
20
+ MessageStoreMsg
21
+ } from '@qlover/corekit-bridge';
22
+
23
+ interface MessageBaseMsg extends MessageStoreMsg<string, unknown> {}
24
+
25
+ function createMessagesState(): MessagesStateInterface<MessageBaseMsg> {
26
+ return {
27
+ messages: []
28
+ };
29
+ }
30
+
31
+ class MessageBaseApi implements MessageGetwayInterface {
32
+ async sendMessage<M extends MessageStoreMsg<string>>(
33
+ message: M
34
+ ): Promise<unknown> {
35
+ const times = random(200, 1000);
36
+
37
+ await ThreadUtil.sleep(times);
38
+
39
+ const messageContent = message.content ?? '';
40
+ if (messageContent.includes('Failed') || messageContent.includes('error')) {
41
+ throw new Error('Failed to send message');
42
+ }
43
+
44
+ if (times % 5 === 0) {
45
+ throw new Error(`Network error(${times})`);
46
+ }
47
+
48
+ // Return object response to demonstrate formatting
49
+ return {
50
+ status: 'success',
51
+ timestamp: new Date().toISOString(),
52
+ delay: `${times}ms`,
53
+ echo: message.content,
54
+ data: {
55
+ message: 'Message received successfully',
56
+ processed: true,
57
+ metadata: {
58
+ length: message.content?.length || 0,
59
+ type: 'text'
60
+ }
61
+ }
62
+ };
63
+ }
64
+ }
65
+
66
+ export function MessageBaseList() {
67
+ const [inputValue, setInputValue] = useState('');
68
+ const logger = useIOC(I.Logger);
69
+ const tt = useI18nInterface(messageBaseListI18n);
70
+ const messagesStore = useFactory(
71
+ MessagesStore<MessageBaseMsg>,
72
+ createMessagesState
73
+ );
74
+ const messageBaseApi = useFactory(MessageBaseApi);
75
+
76
+ const [messagesSender] = useState(() =>
77
+ new MessageSender<MessageBaseMsg>(messagesStore, {
78
+ gateway: messageBaseApi,
79
+ logger
80
+ }).use(new SenderStrategyPlugin(SendFailureStrategy.KEEP_FAILED))
81
+ );
82
+
83
+ const messages = useStore(messagesStore, (state) => state.messages);
84
+
85
+ const loadingMessage = useMemo(() => {
86
+ return messages.find((message) => message.loading);
87
+ }, [messages]);
88
+
89
+ const onSend = useCallback(() => {
90
+ messagesSender.send({
91
+ content: inputValue
92
+ });
93
+ setInputValue('');
94
+ }, [inputValue, messagesSender]);
95
+
96
+ /**
97
+ * Render message result with proper formatting
98
+ * - For objects/arrays: display as formatted JSON
99
+ * - For strings: display as plain text
100
+ */
101
+ const renderResult = (result: unknown) => {
102
+ if (!result) return null;
103
+
104
+ // Check if result is an object or array
105
+ if (typeof result === 'object') {
106
+ try {
107
+ const jsonString = JSON.stringify(result, null, 2);
108
+ return (
109
+ <pre
110
+ data-testid="MessageResultJson"
111
+ className="text-xs font-mono bg-green-100 p-2 rounded mt-1 overflow-x-auto whitespace-pre-wrap"
112
+ >
113
+ {jsonString}
114
+ </pre>
115
+ );
116
+ } catch {
117
+ return String(result);
118
+ }
119
+ }
120
+
121
+ // For strings and other primitives
122
+ return <span data-testid="MessageResultText">{String(result)}</span>;
123
+ };
124
+
125
+ return (
126
+ <div data-testid="MessageBaseList" className="max-w-4xl mx-auto">
127
+ {/* Page Title */}
128
+ <div className="mb-6">
129
+ <h1 className="text-2xl font-bold text-text mb-2">{tt.title}</h1>
130
+ <p className="text-sm text-text-secondary">{tt.description}</p>
131
+ </div>
132
+
133
+ {/* Messages Container */}
134
+ <div className="bg-secondary rounded-lg shadow-sm border border-primary mb-4">
135
+ <div
136
+ data-testid="MessageBaseListItems"
137
+ className="flex flex-col gap-3 p-4 max-h-96 min-h-64 overflow-y-auto"
138
+ >
139
+ {messages.length === 0 ? (
140
+ <div className="flex items-center justify-center h-full text-text-secondary">
141
+ <div className="text-center">
142
+ <p className="text-base mb-1">{tt.noMessages}</p>
143
+ <p className="text-sm">{tt.getStarted}</p>
144
+ </div>
145
+ </div>
146
+ ) : (
147
+ messages.map((message) => (
148
+ <div
149
+ key={message.id}
150
+ data-testid="MessageBaseListItem"
151
+ className="space-y-2"
152
+ >
153
+ {/* User Message */}
154
+ <div className="flex justify-end">
155
+ <div className="max-w-[70%] bg-blue-500 text-white rounded-lg px-4 py-2 shadow-sm">
156
+ <div className="text-sm font-medium mb-1">{tt.user}</div>
157
+ <div className="wrap-break-word">{message.content}</div>
158
+ </div>
159
+ </div>
160
+
161
+ {/* Gateway Response */}
162
+ <div className="flex justify-start">
163
+ <div className="max-w-[70%]">
164
+ {message.loading ? (
165
+ <div className="bg-base rounded-lg px-4 py-2 shadow-sm border border-primary">
166
+ <div className="text-sm font-medium text-text-secondary mb-1">
167
+ {tt.gateway}
168
+ </div>
169
+ <div className="flex items-center gap-2 text-text-secondary">
170
+ <div className="flex gap-1">
171
+ <span
172
+ className="animate-bounce inline-block w-1.5 h-1.5 bg-text-secondary rounded-full"
173
+ style={{ animationDelay: '0ms' }}
174
+ ></span>
175
+ <span
176
+ className="animate-bounce inline-block w-1.5 h-1.5 bg-text-secondary rounded-full"
177
+ style={{ animationDelay: '150ms' }}
178
+ ></span>
179
+ <span
180
+ className="animate-bounce inline-block w-1.5 h-1.5 bg-text-secondary rounded-full"
181
+ style={{ animationDelay: '300ms' }}
182
+ ></span>
183
+ </div>
184
+ <span className="text-sm">{tt.processing}</span>
185
+ </div>
186
+ </div>
187
+ ) : message.status === MessageStatus.FAILED ? (
188
+ <div className="bg-red-50 border border-red-200 rounded-lg px-4 py-2 shadow-sm">
189
+ <div className="text-sm font-medium text-red-800 mb-1">
190
+ {tt.gatewayFailed}
191
+ </div>
192
+ <div className="text-red-600 text-sm">
193
+ ❌ {(message as any).error?.message || tt.sendFailed}
194
+ </div>
195
+ </div>
196
+ ) : message.result ? (
197
+ <div className="bg-green-50 border border-green-200 rounded-lg px-4 py-2 shadow-sm">
198
+ <div className="text-sm font-medium text-green-800 mb-1">
199
+ ✓ {tt.gatewayResponse}
200
+ </div>
201
+ <div className="text-green-900">
202
+ {renderResult(message.result)}
203
+ </div>
204
+ </div>
205
+ ) : null}
206
+ </div>
207
+ </div>
208
+ </div>
209
+ ))
210
+ )}
211
+ </div>
212
+ </div>
213
+
214
+ {/* Input Area */}
215
+ <div className="bg-secondary rounded-lg shadow-sm border border-primary p-4">
216
+ <div className="flex gap-2">
217
+ <Input
218
+ value={inputValue}
219
+ onPressEnter={onSend}
220
+ onChange={(e) => setInputValue(e.target.value)}
221
+ placeholder={tt.inputPlaceholder}
222
+ size="large"
223
+ className="flex-1"
224
+ disabled={loadingMessage?.loading}
225
+ />
226
+ <Button
227
+ disabled={!inputValue || loadingMessage?.loading}
228
+ loading={loadingMessage?.loading}
229
+ type="primary"
230
+ size="large"
231
+ onClick={onSend}
232
+ >
233
+ {tt.sendButton}
234
+ </Button>
235
+ </div>
236
+ <div className="mt-2 text-xs text-text-secondary">{tt.errorTip}</div>
237
+ </div>
238
+ </div>
239
+ );
240
+ }