balibabu commited on
Commit
eedc75d
·
1 Parent(s): 27634f0

feat: Regenerate chat message #2088 (#2166)

Browse files

### What problem does this PR solve?

feat: Regenerate chat message #2088
### Type of change

- [x] New Feature (non-breaking change which adds functionality)

web/src/components/message-item/group-button.tsx CHANGED
@@ -8,8 +8,9 @@ import {
8
  SoundOutlined,
9
  SyncOutlined,
10
  } from '@ant-design/icons';
11
- import { Radio } from 'antd';
12
  import { useCallback } from 'react';
 
13
  import SvgIcon from '../svg-icon';
14
  import FeedbackModal from './feedback-modal';
15
  import { useRemoveMessage, useSendFeedback } from './hooks';
@@ -33,6 +34,7 @@ export const AssistantGroupButton = ({
33
  hideModal: hidePromptModal,
34
  showModal: showPromptModal,
35
  } = useSetModalState();
 
36
 
37
  const handleLike = useCallback(() => {
38
  onFeedbackOk({ thumbup: true });
@@ -45,7 +47,9 @@ export const AssistantGroupButton = ({
45
  <CopyToClipboard text={content}></CopyToClipboard>
46
  </Radio.Button>
47
  <Radio.Button value="b">
48
- <SoundOutlined />
 
 
49
  </Radio.Button>
50
  <Radio.Button value="c" onClick={handleLike}>
51
  <LikeOutlined />
@@ -81,27 +85,41 @@ export const AssistantGroupButton = ({
81
  interface UserGroupButtonProps extends IRemoveMessageById {
82
  messageId: string;
83
  content: string;
 
 
84
  }
85
 
86
  export const UserGroupButton = ({
87
  content,
88
  messageId,
 
89
  removeMessageById,
 
90
  }: UserGroupButtonProps) => {
91
  const { onRemoveMessage, loading } = useRemoveMessage(
92
  messageId,
93
  removeMessageById,
94
  );
 
 
95
  return (
96
  <Radio.Group size="small">
97
  <Radio.Button value="a">
98
  <CopyToClipboard text={content}></CopyToClipboard>
99
  </Radio.Button>
100
- <Radio.Button value="b">
101
- <SyncOutlined />
 
 
 
 
 
 
102
  </Radio.Button>
103
  <Radio.Button value="c" onClick={onRemoveMessage} disabled={loading}>
104
- <DeleteOutlined spin={loading} />
 
 
105
  </Radio.Button>
106
  </Radio.Group>
107
  );
 
8
  SoundOutlined,
9
  SyncOutlined,
10
  } from '@ant-design/icons';
11
+ import { Radio, Tooltip } from 'antd';
12
  import { useCallback } from 'react';
13
+ import { useTranslation } from 'react-i18next';
14
  import SvgIcon from '../svg-icon';
15
  import FeedbackModal from './feedback-modal';
16
  import { useRemoveMessage, useSendFeedback } from './hooks';
 
34
  hideModal: hidePromptModal,
35
  showModal: showPromptModal,
36
  } = useSetModalState();
37
+ const { t } = useTranslation();
38
 
39
  const handleLike = useCallback(() => {
40
  onFeedbackOk({ thumbup: true });
 
47
  <CopyToClipboard text={content}></CopyToClipboard>
48
  </Radio.Button>
49
  <Radio.Button value="b">
50
+ <Tooltip title={t('chat.read')}>
51
+ <SoundOutlined />
52
+ </Tooltip>
53
  </Radio.Button>
54
  <Radio.Button value="c" onClick={handleLike}>
55
  <LikeOutlined />
 
85
  interface UserGroupButtonProps extends IRemoveMessageById {
86
  messageId: string;
87
  content: string;
88
+ regenerateMessage(): void;
89
+ sendLoading: boolean;
90
  }
91
 
92
  export const UserGroupButton = ({
93
  content,
94
  messageId,
95
+ sendLoading,
96
  removeMessageById,
97
+ regenerateMessage,
98
  }: UserGroupButtonProps) => {
99
  const { onRemoveMessage, loading } = useRemoveMessage(
100
  messageId,
101
  removeMessageById,
102
  );
103
+ const { t } = useTranslation();
104
+
105
  return (
106
  <Radio.Group size="small">
107
  <Radio.Button value="a">
108
  <CopyToClipboard text={content}></CopyToClipboard>
109
  </Radio.Button>
110
+ <Radio.Button
111
+ value="b"
112
+ onClick={regenerateMessage}
113
+ disabled={sendLoading}
114
+ >
115
+ <Tooltip title={t('chat.regenerate')}>
116
+ <SyncOutlined spin={sendLoading} />
117
+ </Tooltip>
118
  </Radio.Button>
119
  <Radio.Button value="c" onClick={onRemoveMessage} disabled={loading}>
120
+ <Tooltip title={t('common.delete')}>
121
+ <DeleteOutlined spin={loading} />
122
+ </Tooltip>
123
  </Radio.Button>
124
  </Radio.Group>
125
  );
web/src/components/message-item/index.tsx CHANGED
@@ -11,7 +11,7 @@ import {
11
  useFetchDocumentInfosByIds,
12
  useFetchDocumentThumbnailsByIds,
13
  } from '@/hooks/document-hooks';
14
- import { IRemoveMessageById } from '@/hooks/logic-hooks';
15
  import { IMessage } from '@/pages/chat/interface';
16
  import MarkdownContent from '@/pages/chat/markdown-content';
17
  import { getExtension, isImage } from '@/utils/document-util';
@@ -24,10 +24,11 @@ import styles from './index.less';
24
 
25
  const { Text } = Typography;
26
 
27
- interface IProps extends IRemoveMessageById {
28
  item: IMessage;
29
  reference: IReference;
30
  loading?: boolean;
 
31
  nickname?: string;
32
  avatar?: string;
33
  clickDocumentButton?: (documentId: string, chunk: IChunk) => void;
@@ -39,9 +40,11 @@ const MessageItem = ({
39
  reference,
40
  loading = false,
41
  avatar = '',
 
42
  clickDocumentButton,
43
  index,
44
  removeMessageById,
 
45
  }: IProps) => {
46
  const isAssistant = item.role === MessageType.Assistant;
47
  const isUser = item.role === MessageType.User;
@@ -73,6 +76,10 @@ const MessageItem = ({
73
  [showModal],
74
  );
75
 
 
 
 
 
76
  useEffect(() => {
77
  const ids = item?.doc_ids ?? [];
78
  if (ids.length) {
@@ -128,6 +135,8 @@ const MessageItem = ({
128
  content={item.content}
129
  messageId={item.id}
130
  removeMessageById={removeMessageById}
 
 
131
  ></UserGroupButton>
132
  )}
133
 
 
11
  useFetchDocumentInfosByIds,
12
  useFetchDocumentThumbnailsByIds,
13
  } from '@/hooks/document-hooks';
14
+ import { IRegenerateMessage, IRemoveMessageById } from '@/hooks/logic-hooks';
15
  import { IMessage } from '@/pages/chat/interface';
16
  import MarkdownContent from '@/pages/chat/markdown-content';
17
  import { getExtension, isImage } from '@/utils/document-util';
 
24
 
25
  const { Text } = Typography;
26
 
27
+ interface IProps extends IRemoveMessageById, IRegenerateMessage {
28
  item: IMessage;
29
  reference: IReference;
30
  loading?: boolean;
31
+ sendLoading?: boolean;
32
  nickname?: string;
33
  avatar?: string;
34
  clickDocumentButton?: (documentId: string, chunk: IChunk) => void;
 
40
  reference,
41
  loading = false,
42
  avatar = '',
43
+ sendLoading = false,
44
  clickDocumentButton,
45
  index,
46
  removeMessageById,
47
+ regenerateMessage,
48
  }: IProps) => {
49
  const isAssistant = item.role === MessageType.Assistant;
50
  const isUser = item.role === MessageType.User;
 
76
  [showModal],
77
  );
78
 
79
+ const handleRegenerateMessage = useCallback(() => {
80
+ regenerateMessage(item);
81
+ }, [regenerateMessage, item]);
82
+
83
  useEffect(() => {
84
  const ids = item?.doc_ids ?? [];
85
  if (ids.length) {
 
135
  content={item.content}
136
  messageId={item.id}
137
  removeMessageById={removeMessageById}
138
+ regenerateMessage={handleRegenerateMessage}
139
+ sendLoading={sendLoading}
140
  ></UserGroupButton>
141
  )}
142
 
web/src/hooks/logic-hooks.ts CHANGED
@@ -2,7 +2,7 @@ import { Authorization } from '@/constants/authorization';
2
  import { LanguageTranslationMap } from '@/constants/common';
3
  import { Pagination } from '@/interfaces/common';
4
  import { ResponseType } from '@/interfaces/database/base';
5
- import { IAnswer } from '@/interfaces/database/chat';
6
  import { IKnowledgeFile } from '@/interfaces/database/knowledge';
7
  import { IChangeParserConfigRequestBody } from '@/interfaces/request/document';
8
  import { IClientConversation } from '@/pages/chat/interface';
@@ -23,6 +23,7 @@ import {
23
  } from 'react';
24
  import { useTranslation } from 'react-i18next';
25
  import { useDispatch } from 'umi';
 
26
  import { useSetModalState, useTranslate } from './common-hooks';
27
  import { useSetDocumentParser } from './document-hooks';
28
  import { useSetPaginationParams } from './route-hook';
@@ -336,6 +337,77 @@ export const useRemoveMessageById = (
336
  return { removeMessageById };
337
  };
338
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
339
  // #endregion
340
 
341
  /**
 
2
  import { LanguageTranslationMap } from '@/constants/common';
3
  import { Pagination } from '@/interfaces/common';
4
  import { ResponseType } from '@/interfaces/database/base';
5
+ import { IAnswer, Message } from '@/interfaces/database/chat';
6
  import { IKnowledgeFile } from '@/interfaces/database/knowledge';
7
  import { IChangeParserConfigRequestBody } from '@/interfaces/request/document';
8
  import { IClientConversation } from '@/pages/chat/interface';
 
23
  } from 'react';
24
  import { useTranslation } from 'react-i18next';
25
  import { useDispatch } from 'umi';
26
+ import { v4 as uuid } from 'uuid';
27
  import { useSetModalState, useTranslate } from './common-hooks';
28
  import { useSetDocumentParser } from './document-hooks';
29
  import { useSetPaginationParams } from './route-hook';
 
337
  return { removeMessageById };
338
  };
339
 
340
+ export const useRemoveMessagesAfterCurrentMessage = (
341
+ setCurrentConversation: (
342
+ callback: (state: IClientConversation) => IClientConversation,
343
+ ) => void,
344
+ ) => {
345
+ const removeMessagesAfterCurrentMessage = useCallback(
346
+ (messageId: string) => {
347
+ setCurrentConversation((pre) => {
348
+ const index = pre.message?.findIndex((x) => x.id === messageId);
349
+ if (index !== -1) {
350
+ let nextMessages = pre.message?.slice(0, index + 2) ?? [];
351
+ const latestMessage = nextMessages.at(-1);
352
+ nextMessages = latestMessage
353
+ ? [
354
+ ...nextMessages.slice(0, -1),
355
+ {
356
+ ...latestMessage,
357
+ content: '',
358
+ reference: undefined,
359
+ prompt: undefined,
360
+ },
361
+ ]
362
+ : nextMessages;
363
+ return {
364
+ ...pre,
365
+ message: nextMessages,
366
+ };
367
+ }
368
+ return pre;
369
+ });
370
+ },
371
+ [setCurrentConversation],
372
+ );
373
+
374
+ return { removeMessagesAfterCurrentMessage };
375
+ };
376
+
377
+ export interface IRegenerateMessage {
378
+ regenerateMessage(message: Message): void;
379
+ }
380
+
381
+ export const useRegenerateMessage = ({
382
+ removeMessagesAfterCurrentMessage,
383
+ sendMessage,
384
+ messages,
385
+ }: {
386
+ removeMessagesAfterCurrentMessage(messageId: string): void;
387
+ sendMessage({ message }: { message: Message; messages?: Message[] }): void;
388
+ messages: Message[];
389
+ }) => {
390
+ const regenerateMessage = useCallback(
391
+ async (message: Message) => {
392
+ if (message.id) {
393
+ removeMessagesAfterCurrentMessage(message.id);
394
+ const index = messages.findIndex((x) => x.id === message.id);
395
+ let nextMessages;
396
+ if (index !== -1) {
397
+ nextMessages = messages.slice(0, index);
398
+ }
399
+ sendMessage({
400
+ message: { ...message, id: uuid() },
401
+ messages: nextMessages,
402
+ });
403
+ }
404
+ },
405
+ [removeMessagesAfterCurrentMessage, sendMessage, messages],
406
+ );
407
+
408
+ return { regenerateMessage };
409
+ };
410
+
411
  // #endregion
412
 
413
  /**
web/src/locales/en.ts CHANGED
@@ -425,6 +425,8 @@ The above is the content you need to summarize.`,
425
  parsing: 'Parsing',
426
  uploading: 'Uploading',
427
  uploadFailed: 'Upload failed',
 
 
428
  },
429
  setting: {
430
  profile: 'Profile',
 
425
  parsing: 'Parsing',
426
  uploading: 'Uploading',
427
  uploadFailed: 'Upload failed',
428
+ regenerate: 'Regenerate',
429
+ read: 'Read content',
430
  },
431
  setting: {
432
  profile: 'Profile',
web/src/locales/zh-traditional.ts CHANGED
@@ -395,6 +395,8 @@ export default {
395
  parsing: '解析中',
396
  uploading: '上傳中',
397
  uploadFailed: '上傳失敗',
 
 
398
  },
399
  setting: {
400
  profile: '概述',
 
395
  parsing: '解析中',
396
  uploading: '上傳中',
397
  uploadFailed: '上傳失敗',
398
+ regenerate: '重新生成',
399
+ read: '朗讀內容',
400
  },
401
  setting: {
402
  profile: '概述',
web/src/locales/zh.ts CHANGED
@@ -412,6 +412,8 @@ export default {
412
  parsing: '解析中',
413
  uploading: '上传中',
414
  uploadFailed: '上传失败',
 
 
415
  },
416
  setting: {
417
  profile: '概要',
 
412
  parsing: '解析中',
413
  uploading: '上传中',
414
  uploadFailed: '上传失败',
415
+ regenerate: '重新生成',
416
+ read: '朗读内容',
417
  },
418
  setting: {
419
  profile: '概要',
web/src/pages/chat/chat-container/index.tsx CHANGED
@@ -28,17 +28,20 @@ const ChatContainer = () => {
28
  conversationId,
29
  loading,
30
  removeMessageById,
 
31
  } = useFetchConversationOnMount();
32
  const {
33
  handleInputChange,
34
  handlePressEnter,
35
  value,
36
  loading: sendLoading,
 
37
  } = useSendMessage(
38
  conversation,
39
  addNewestConversation,
40
  removeLatestMessage,
41
  addNewestAnswer,
 
42
  );
43
  const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } =
44
  useClickDrawer();
@@ -71,6 +74,8 @@ const ChatContainer = () => {
71
  clickDocumentButton={clickDocumentButton}
72
  index={i}
73
  removeMessageById={removeMessageById}
 
 
74
  ></MessageItem>
75
  );
76
  })}
 
28
  conversationId,
29
  loading,
30
  removeMessageById,
31
+ removeMessagesAfterCurrentMessage,
32
  } = useFetchConversationOnMount();
33
  const {
34
  handleInputChange,
35
  handlePressEnter,
36
  value,
37
  loading: sendLoading,
38
+ regenerateMessage,
39
  } = useSendMessage(
40
  conversation,
41
  addNewestConversation,
42
  removeLatestMessage,
43
  addNewestAnswer,
44
+ removeMessagesAfterCurrentMessage,
45
  );
46
  const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } =
47
  useClickDrawer();
 
74
  clickDocumentButton={clickDocumentButton}
75
  index={i}
76
  removeMessageById={removeMessageById}
77
+ regenerateMessage={regenerateMessage}
78
+ sendLoading={sendLoading}
79
  ></MessageItem>
80
  );
81
  })}
web/src/pages/chat/hooks.ts CHANGED
@@ -18,7 +18,9 @@ import {
18
  useTranslate,
19
  } from '@/hooks/common-hooks';
20
  import {
 
21
  useRemoveMessageById,
 
22
  useSendMessageWithSse,
23
  } from '@/hooks/logic-hooks';
24
  import {
@@ -255,6 +257,8 @@ export const useSelectCurrentConversation = () => {
255
  const { data: dialog } = useFetchNextDialog();
256
  const { conversationId, dialogId } = useGetChatSearchParams();
257
  const { removeMessageById } = useRemoveMessageById(setCurrentConversation);
 
 
258
 
259
  // Show the entered message in the conversation immediately after sending the message
260
  const addNewestConversation = useCallback(
@@ -353,6 +357,7 @@ export const useSelectCurrentConversation = () => {
353
  removeLatestMessage,
354
  addNewestAnswer,
355
  removeMessageById,
 
356
  loading,
357
  };
358
  };
@@ -382,6 +387,7 @@ export const useFetchConversationOnMount = () => {
382
  addNewestAnswer,
383
  loading,
384
  removeMessageById,
 
385
  } = useSelectCurrentConversation();
386
  const ref = useScrollToBottom(currentConversation);
387
 
@@ -394,6 +400,7 @@ export const useFetchConversationOnMount = () => {
394
  conversationId,
395
  loading,
396
  removeMessageById,
 
397
  };
398
  };
399
 
@@ -418,6 +425,7 @@ export const useSendMessage = (
418
  addNewestConversation: (message: Message, answer?: string) => void,
419
  removeLatestMessage: () => void,
420
  addNewestAnswer: (answer: IAnswer) => void,
 
421
  ) => {
422
  const { setConversation } = useSetConversation();
423
  const { conversationId } = useGetChatSearchParams();
@@ -427,16 +435,18 @@ export const useSendMessage = (
427
  const { send, answer, done, setDone } = useSendMessageWithSse();
428
 
429
  const sendMessage = useCallback(
430
- async (message: Message, documentIds: string[], id?: string) => {
 
 
 
 
 
 
 
 
431
  const res = await send({
432
- conversation_id: id ?? conversationId,
433
- messages: [
434
- ...(conversation?.message ?? []),
435
- {
436
- ...message,
437
- doc_ids: documentIds,
438
- },
439
- ],
440
  });
441
 
442
  if (res && (res?.response.status !== 200 || res?.data?.retcode !== 0)) {
@@ -445,10 +455,10 @@ export const useSendMessage = (
445
  console.info('removeLatestMessage111');
446
  removeLatestMessage();
447
  } else {
448
- if (id) {
449
  console.info('111');
450
  // new conversation
451
- handleClickConversation(id);
452
  } else {
453
  console.info('222');
454
  // fetchConversation(conversationId);
@@ -466,20 +476,26 @@ export const useSendMessage = (
466
  );
467
 
468
  const handleSendMessage = useCallback(
469
- async (message: Message, documentIds: string[]) => {
470
  if (conversationId !== '') {
471
- sendMessage(message, documentIds);
472
  } else {
473
  const data = await setConversation(message.content);
474
  if (data.retcode === 0) {
475
  const id = data.data.id;
476
- sendMessage(message, documentIds, id);
477
  }
478
  }
479
  },
480
  [conversationId, setConversation, sendMessage],
481
  );
482
 
 
 
 
 
 
 
483
  useEffect(() => {
484
  // #1289
485
  if (answer.answer && answer?.conversationId === conversationId) {
@@ -507,10 +523,12 @@ export const useSendMessage = (
507
  });
508
  if (done) {
509
  setValue('');
510
- handleSendMessage(
511
- { id, content: value.trim(), role: MessageType.User },
512
- documentIds,
513
- );
 
 
514
  }
515
  },
516
  [addNewestConversation, handleSendMessage, done, setValue, value],
@@ -521,6 +539,7 @@ export const useSendMessage = (
521
  handleInputChange,
522
  value,
523
  setValue,
 
524
  loading: !done,
525
  };
526
  };
 
18
  useTranslate,
19
  } from '@/hooks/common-hooks';
20
  import {
21
+ useRegenerateMessage,
22
  useRemoveMessageById,
23
+ useRemoveMessagesAfterCurrentMessage,
24
  useSendMessageWithSse,
25
  } from '@/hooks/logic-hooks';
26
  import {
 
257
  const { data: dialog } = useFetchNextDialog();
258
  const { conversationId, dialogId } = useGetChatSearchParams();
259
  const { removeMessageById } = useRemoveMessageById(setCurrentConversation);
260
+ const { removeMessagesAfterCurrentMessage } =
261
+ useRemoveMessagesAfterCurrentMessage(setCurrentConversation);
262
 
263
  // Show the entered message in the conversation immediately after sending the message
264
  const addNewestConversation = useCallback(
 
357
  removeLatestMessage,
358
  addNewestAnswer,
359
  removeMessageById,
360
+ removeMessagesAfterCurrentMessage,
361
  loading,
362
  };
363
  };
 
387
  addNewestAnswer,
388
  loading,
389
  removeMessageById,
390
+ removeMessagesAfterCurrentMessage,
391
  } = useSelectCurrentConversation();
392
  const ref = useScrollToBottom(currentConversation);
393
 
 
400
  conversationId,
401
  loading,
402
  removeMessageById,
403
+ removeMessagesAfterCurrentMessage,
404
  };
405
  };
406
 
 
425
  addNewestConversation: (message: Message, answer?: string) => void,
426
  removeLatestMessage: () => void,
427
  addNewestAnswer: (answer: IAnswer) => void,
428
+ removeMessagesAfterCurrentMessage: (messageId: string) => void,
429
  ) => {
430
  const { setConversation } = useSetConversation();
431
  const { conversationId } = useGetChatSearchParams();
 
435
  const { send, answer, done, setDone } = useSendMessageWithSse();
436
 
437
  const sendMessage = useCallback(
438
+ async ({
439
+ message,
440
+ currentConversationId,
441
+ messages,
442
+ }: {
443
+ message: Message;
444
+ currentConversationId?: string;
445
+ messages?: Message[];
446
+ }) => {
447
  const res = await send({
448
+ conversation_id: currentConversationId ?? conversationId,
449
+ messages: [...(messages ?? conversation?.message ?? []), message],
 
 
 
 
 
 
450
  });
451
 
452
  if (res && (res?.response.status !== 200 || res?.data?.retcode !== 0)) {
 
455
  console.info('removeLatestMessage111');
456
  removeLatestMessage();
457
  } else {
458
+ if (currentConversationId) {
459
  console.info('111');
460
  // new conversation
461
+ handleClickConversation(currentConversationId);
462
  } else {
463
  console.info('222');
464
  // fetchConversation(conversationId);
 
476
  );
477
 
478
  const handleSendMessage = useCallback(
479
+ async (message: Message) => {
480
  if (conversationId !== '') {
481
+ sendMessage({ message });
482
  } else {
483
  const data = await setConversation(message.content);
484
  if (data.retcode === 0) {
485
  const id = data.data.id;
486
+ sendMessage({ message, currentConversationId: id });
487
  }
488
  }
489
  },
490
  [conversationId, setConversation, sendMessage],
491
  );
492
 
493
+ const { regenerateMessage } = useRegenerateMessage({
494
+ removeMessagesAfterCurrentMessage,
495
+ sendMessage,
496
+ messages: conversation.message,
497
+ });
498
+
499
  useEffect(() => {
500
  // #1289
501
  if (answer.answer && answer?.conversationId === conversationId) {
 
523
  });
524
  if (done) {
525
  setValue('');
526
+ handleSendMessage({
527
+ id,
528
+ content: value.trim(),
529
+ role: MessageType.User,
530
+ doc_ids: documentIds,
531
+ });
532
  }
533
  },
534
  [addNewestConversation, handleSendMessage, done, setValue, value],
 
539
  handleInputChange,
540
  value,
541
  setValue,
542
+ regenerateMessage,
543
  loading: !done,
544
  };
545
  };
web/src/utils/chat.ts CHANGED
@@ -16,8 +16,8 @@ export const buildMessageUuid = (message: Partial<Message | IMessage>) => {
16
  return uuid();
17
  };
18
 
19
- export const getMessagePureId = (id: string) => {
20
- const strings = id.split('_');
21
  if (strings.length > 0) {
22
  return strings.at(-1);
23
  }
 
16
  return uuid();
17
  };
18
 
19
+ export const getMessagePureId = (id?: string) => {
20
+ const strings = id?.split('_') ?? [];
21
  if (strings.length > 0) {
22
  return strings.at(-1);
23
  }