@lobehub/chat 0.101.6 → 0.102.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 (89) hide show
  1. package/.i18nrc.js +1 -1
  2. package/CHANGELOG.md +50 -0
  3. package/README.md +38 -4
  4. package/README.zh-CN.md +41 -9
  5. package/__mocks__/zustand/traditional.ts +4 -4
  6. package/locales/en_US/chat.json +11 -1
  7. package/locales/en_US/error.json +6 -0
  8. package/locales/en_US/setting.json +40 -1
  9. package/locales/ja_JP/chat.json +11 -1
  10. package/locales/ja_JP/error.json +6 -0
  11. package/locales/ja_JP/setting.json +40 -1
  12. package/locales/ko_KR/chat.json +11 -1
  13. package/locales/ko_KR/error.json +6 -0
  14. package/locales/ko_KR/setting.json +40 -1
  15. package/locales/ru_RU/chat.json +11 -1
  16. package/locales/ru_RU/error.json +6 -0
  17. package/locales/ru_RU/setting.json +40 -1
  18. package/locales/zh_CN/chat.json +10 -1
  19. package/locales/zh_CN/error.json +6 -0
  20. package/locales/zh_CN/setting.json +36 -1
  21. package/locales/zh_TW/chat.json +11 -1
  22. package/locales/zh_TW/error.json +6 -0
  23. package/locales/zh_TW/setting.json +40 -1
  24. package/next.config.mjs +2 -20
  25. package/package.json +4 -3
  26. package/src/app/api/openai/chat/route.ts +5 -41
  27. package/src/app/api/openai/{createAzureOpenai.ts → createBizOpenAI/createAzureOpenai.ts} +1 -1
  28. package/src/app/api/openai/{createOpenai.ts → createBizOpenAI/createOpenai.ts} +1 -1
  29. package/src/app/api/openai/createBizOpenAI/index.ts +46 -0
  30. package/src/app/api/openai/models/route.ts +1 -1
  31. package/src/app/api/openai/stt/route.ts +29 -0
  32. package/src/app/api/openai/tts/route.ts +17 -0
  33. package/src/app/api/tts/edge-speech/route.ts +9 -0
  34. package/src/app/api/tts/microsoft-speech/route.ts +9 -0
  35. package/src/app/chat/(desktop)/features/ChatHeader.tsx +3 -2
  36. package/src/app/chat/(desktop)/features/ChatInput/Footer.tsx +62 -0
  37. package/src/app/chat/(desktop)/features/ChatInput/index.tsx +0 -1
  38. package/src/app/chat/(desktop)/features/SessionHeader.tsx +2 -2
  39. package/src/app/chat/(desktop)/features/SideBar.tsx +7 -1
  40. package/src/app/chat/(mobile)/features/ChatHeader.tsx +29 -11
  41. package/src/app/chat/(mobile)/features/ChatInput/Mobile.tsx +20 -4
  42. package/src/app/chat/features/ChatHeader/ShareButton/index.tsx +23 -15
  43. package/src/app/chat/features/ChatInput/ActionBar/config.ts +16 -6
  44. package/src/app/chat/features/ChatInput/ActionBar/index.tsx +40 -16
  45. package/src/app/chat/features/ChatInput/STT/index.tsx +146 -0
  46. package/src/app/chat/features/ChatInput/Topic/index.tsx +1 -3
  47. package/src/app/chat/features/Conversation/ChatList/Actions/Assistant.tsx +2 -2
  48. package/src/app/chat/features/Conversation/ChatList/Actions/User.tsx +2 -2
  49. package/src/app/chat/features/Conversation/ChatList/Actions/customAction.ts +9 -2
  50. package/src/app/chat/features/Conversation/ChatList/Actions/index.ts +8 -1
  51. package/src/app/chat/features/Conversation/ChatList/Extras/Assistant.tsx +33 -27
  52. package/src/app/chat/features/Conversation/ChatList/Extras/AudioPlayer.tsx +139 -0
  53. package/src/app/chat/features/Conversation/ChatList/Extras/ExtraContainer.tsx +13 -0
  54. package/src/app/chat/features/Conversation/ChatList/Extras/TTS.tsx +122 -0
  55. package/src/app/chat/features/Conversation/ChatList/Extras/User.tsx +18 -11
  56. package/src/app/chat/settings/features/HeaderContent.tsx +6 -4
  57. package/src/app/chat/settings/features/SubmitAgentButton/index.tsx +2 -3
  58. package/src/app/settings/features/SideBar/List.tsx +2 -1
  59. package/src/app/settings/features/SideBar/index.tsx +1 -1
  60. package/src/app/settings/features/UpgradeAlert.tsx +1 -1
  61. package/src/app/settings/tts/TTS/index.tsx +73 -0
  62. package/src/app/settings/tts/TTS/options.ts +30 -0
  63. package/src/app/settings/tts/index.tsx +21 -0
  64. package/src/app/settings/tts/layout.tsx +1 -0
  65. package/src/app/settings/tts/page.tsx +3 -0
  66. package/src/components/HotKeys/index.tsx +4 -7
  67. package/src/const/layoutTokens.ts +3 -0
  68. package/src/const/settings.ts +22 -1
  69. package/src/features/AgentSetting/AgentTTS/SelectWithTTSPreview.tsx +119 -0
  70. package/src/features/AgentSetting/AgentTTS/index.tsx +115 -0
  71. package/src/features/AgentSetting/AgentTTS/options.ts +16 -0
  72. package/src/features/AgentSetting/index.tsx +2 -1
  73. package/src/hooks/useSTT.ts +61 -0
  74. package/src/hooks/useTTS.ts +87 -0
  75. package/src/locales/default/chat.ts +10 -1
  76. package/src/locales/default/error.ts +6 -0
  77. package/src/locales/default/setting.ts +35 -1
  78. package/src/services/_url.ts +7 -0
  79. package/src/store/global/initialState.ts +1 -0
  80. package/src/store/global/selectors/settings.test.ts +32 -0
  81. package/src/store/global/selectors/settings.ts +9 -1
  82. package/src/store/session/slices/agentConfig/selectors.ts +9 -0
  83. package/src/store/session/slices/chat/actions/translate.ts +24 -1
  84. package/src/store/session/slices/chat/selectors/utils.test.ts +16 -0
  85. package/src/store/session/slices/session/reducers/session.test.ts +24 -0
  86. package/src/types/chatMessage.ts +6 -0
  87. package/src/types/session.ts +17 -0
  88. package/src/types/settings.ts +13 -2
  89. package/src/app/chat/(mobile)/features/ChatInput/style.mobile.ts +0 -19
@@ -1,7 +1,8 @@
1
- import { ActionIcon, MobileNavBar, MobileNavBarTitle } from '@lobehub/ui';
2
- import { Clock3, Settings } from 'lucide-react';
1
+ import { ActionIcon, Icon, MobileNavBar, MobileNavBarTitle } from '@lobehub/ui';
2
+ import { Dropdown, MenuProps } from 'antd';
3
+ import { Clock3, MoreHorizontal, Settings, Share2 } from 'lucide-react';
3
4
  import { useRouter } from 'next/navigation';
4
- import { memo } from 'react';
5
+ import { memo, useState } from 'react';
5
6
  import { useTranslation } from 'react-i18next';
6
7
 
7
8
  import { MOBILE_HEADER_ICON_SIZE } from '@/const/layoutTokens';
@@ -15,6 +16,7 @@ import ShareButton from '../../features/ChatHeader/ShareButton';
15
16
  const MobileHeader = memo(() => {
16
17
  const { t } = useTranslation('chat');
17
18
  const router = useRouter();
19
+ const [open, setOpen] = useState(false);
18
20
 
19
21
  const [isInbox, title] = useSessionStore((s) => [
20
22
  sessionSelectors.isInboxSession(s),
@@ -25,21 +27,37 @@ const MobileHeader = memo(() => {
25
27
 
26
28
  const displayTitle = isInbox ? t('inbox.title') : title;
27
29
 
30
+ const items: MenuProps['items'] = [
31
+ {
32
+ icon: <Icon icon={Share2} />,
33
+ key: 'share',
34
+ label: t('share', { ns: 'common' }),
35
+ onClick: () => setOpen(true),
36
+ },
37
+ !isInbox && {
38
+ icon: <Icon icon={Settings} />,
39
+ key: 'settings',
40
+ label: t('header.session', { ns: 'setting' }),
41
+ onClick: () => router.push(pathString('/chat/settings', { hash: location.hash })),
42
+ },
43
+ ].filter(Boolean) as MenuProps['items'];
44
+
28
45
  return (
29
46
  <MobileNavBar
30
47
  center={<MobileNavBarTitle title={displayTitle} />}
31
48
  onBackClick={() => router.push('/chat')}
32
49
  right={
33
50
  <>
34
- <ShareButton />
35
51
  <ActionIcon icon={Clock3} onClick={() => toggleConfig()} size={MOBILE_HEADER_ICON_SIZE} />
36
- {!isInbox && (
37
- <ActionIcon
38
- icon={Settings}
39
- onClick={() => router.push(pathString('/chat/settings', { hash: location.hash }))}
40
- size={MOBILE_HEADER_ICON_SIZE}
41
- />
42
- )}
52
+ <ShareButton mobile open={open} setOpen={setOpen} />
53
+ <Dropdown
54
+ menu={{
55
+ items,
56
+ }}
57
+ trigger={['click']}
58
+ >
59
+ <ActionIcon icon={MoreHorizontal} />
60
+ </Dropdown>
43
61
  </>
44
62
  }
45
63
  showBackButton
@@ -1,20 +1,36 @@
1
+ import { createStyles } from 'antd-style';
2
+ import { rgba } from 'polished';
1
3
  import { memo } from 'react';
2
4
  import { Flexbox } from 'react-layout-kit';
3
5
 
4
6
  import ActionBar from '@/app/chat/features/ChatInput/ActionBar';
5
7
  import InputAreaInner from '@/app/chat/features/ChatInput/InputAreaInner';
8
+ import STT from '@/app/chat/features/ChatInput/STT';
6
9
  import SaveTopic from '@/app/chat/features/ChatInput/Topic';
7
10
 
8
11
  import SendButton from './SendButton';
9
- import { useStyles } from './style.mobile';
12
+
13
+ const useStyles = createStyles(({ css, token }) => {
14
+ return {
15
+ container: css`
16
+ padding: 12px 0;
17
+ background: ${token.colorBgLayout};
18
+ border-top: 1px solid ${rgba(token.colorBorder, 0.25)};
19
+ `,
20
+ inner: css`
21
+ padding: 0 8px;
22
+ `,
23
+ };
24
+ });
10
25
 
11
26
  const ChatInputArea = memo(() => {
12
- const { cx, styles } = useStyles();
27
+ const { styles } = useStyles();
13
28
 
14
29
  return (
15
- <Flexbox className={cx(styles.container)} gap={12}>
16
- <ActionBar rightAreaStartRender={<SaveTopic />} />
30
+ <Flexbox className={styles.container} gap={12}>
31
+ <ActionBar mobile padding={'0 8px'} rightAreaStartRender={<SaveTopic mobile />} />
17
32
  <Flexbox className={styles.inner} gap={8} horizontal>
33
+ <STT mobile />
18
34
  <InputAreaInner mobile />
19
35
  <SendButton />
20
36
  </Flexbox>
@@ -1,32 +1,40 @@
1
1
  import { ActionIcon, Modal } from '@lobehub/ui';
2
- import { useResponsive } from 'antd-style';
3
2
  import { Share2 } from 'lucide-react';
4
3
  import dynamic from 'next/dynamic';
5
- import { memo, useState } from 'react';
4
+ import { memo } from 'react';
6
5
  import { useTranslation } from 'react-i18next';
6
+ import useMergeState from 'use-merge-value';
7
7
 
8
- import { MOBILE_HEADER_ICON_SIZE } from '@/const/layoutTokens';
8
+ import { DESKTOP_HEADER_ICON_SIZE } from '@/const/layoutTokens';
9
9
  import { useSessionStore } from '@/store/session';
10
10
 
11
11
  const Inner = dynamic(() => import('./Inner'));
12
+ interface ShareButtonProps {
13
+ mobile?: boolean;
14
+ open?: boolean;
15
+ setOpen?: (open: boolean) => void;
16
+ }
12
17
 
13
- const ShareButton = memo(() => {
14
- const [isModalOpen, setIsModalOpen] = useState(false);
18
+ const ShareButton = memo<ShareButtonProps>(({ mobile, setOpen, open }) => {
19
+ const [isModalOpen, setIsModalOpen] = useMergeState(false, {
20
+ defaultValue: false,
21
+ onChange: setOpen,
22
+ value: open,
23
+ });
15
24
  const { t } = useTranslation('common');
16
25
  const [shareLoading] = useSessionStore((s) => [s.shareLoading]);
17
- const { mobile } = useResponsive();
18
-
19
- const size = mobile ? MOBILE_HEADER_ICON_SIZE : { fontSize: 24 };
20
26
 
21
27
  return (
22
28
  <>
23
- <ActionIcon
24
- icon={Share2}
25
- loading={shareLoading}
26
- onClick={() => setIsModalOpen(true)}
27
- size={size}
28
- title={t('share')}
29
- />
29
+ {!mobile && (
30
+ <ActionIcon
31
+ icon={Share2}
32
+ loading={shareLoading}
33
+ onClick={() => setIsModalOpen(true)}
34
+ size={DESKTOP_HEADER_ICON_SIZE}
35
+ title={t('share')}
36
+ />
37
+ )}
30
38
  <Modal
31
39
  centered={false}
32
40
  footer={null}
@@ -1,5 +1,4 @@
1
- import { FC } from 'react';
2
-
1
+ import STT from '../STT';
3
2
  import Clear from './Clear';
4
3
  import FileUpload from './FileUpload';
5
4
  import History from './History';
@@ -7,15 +6,26 @@ import ModelSwitch from './ModelSwitch';
7
6
  import Temperature from './Temperature';
8
7
  import Token from './Token';
9
8
 
10
- export const actionMap: Record<string, FC> = {
9
+ export const actionMap = {
11
10
  clear: Clear,
12
11
  fileUpload: FileUpload,
13
12
  history: History,
14
13
  model: ModelSwitch,
14
+ stt: STT,
15
15
  temperature: Temperature,
16
16
  token: Token,
17
- };
17
+ } as const;
18
+
19
+ type ActionMap = typeof actionMap;
20
+
21
+ export type ActionKeys = keyof ActionMap;
22
+
23
+ type getActionList = (mobile?: boolean) => ActionKeys[];
18
24
 
19
25
  // we can make these action lists configurable in the future
20
- export const leftActionList = ['model', 'fileUpload', 'temperature', 'history', 'token'];
21
- export const rightActionList = ['clear'];
26
+ export const getLeftActionList: getActionList = (mobile) =>
27
+ ['model', 'fileUpload', 'temperature', 'history', !mobile && 'stt', 'token'].filter(
28
+ Boolean,
29
+ ) as ActionKeys[];
30
+
31
+ export const getRightActionList: getActionList = () => ['clear'].filter(Boolean) as ActionKeys[];
@@ -1,9 +1,9 @@
1
- import { ReactNode, memo } from 'react';
1
+ import { ReactNode, memo, useMemo } from 'react';
2
2
  import { Flexbox } from 'react-layout-kit';
3
3
 
4
- import { actionMap, leftActionList, rightActionList } from './config';
4
+ import { ActionKeys, actionMap, getLeftActionList, getRightActionList } from './config';
5
5
 
6
- const RenderActionList = ({ dataSource }: { dataSource: string[] }) => (
6
+ const RenderActionList = ({ dataSource }: { dataSource: ActionKeys[] }) => (
7
7
  <>
8
8
  {dataSource.map((key) => {
9
9
  const Render = actionMap[key];
@@ -13,23 +13,47 @@ const RenderActionList = ({ dataSource }: { dataSource: string[] }) => (
13
13
  );
14
14
 
15
15
  export interface ActionBarProps {
16
+ leftAreaEndRender?: ReactNode;
17
+ leftAreaStartRender?: ReactNode;
18
+ mobile?: boolean;
19
+ padding?: number | string;
16
20
  rightAreaEndRender?: ReactNode;
17
21
  rightAreaStartRender?: ReactNode;
18
22
  }
19
23
 
20
- const ActionBar = memo<ActionBarProps>(({ rightAreaStartRender, rightAreaEndRender }) => {
21
- return (
22
- <Flexbox align={'center'} flex={'none'} horizontal justify={'space-between'} padding={'0 16px'}>
23
- <Flexbox align={'center'} flex={1} gap={4} horizontal>
24
- <RenderActionList dataSource={leftActionList} />
25
- </Flexbox>
26
- <Flexbox align={'center'} flex={0} gap={4} horizontal justify={'flex-end'}>
27
- {rightAreaStartRender}
28
- <RenderActionList dataSource={rightActionList} />
29
- {rightAreaEndRender}
24
+ const ActionBar = memo<ActionBarProps>(
25
+ ({
26
+ padding = '0 16px',
27
+ mobile,
28
+ rightAreaStartRender,
29
+ rightAreaEndRender,
30
+ leftAreaStartRender,
31
+ leftAreaEndRender,
32
+ }) => {
33
+ const leftActionList = useMemo(() => getLeftActionList(mobile), [mobile]);
34
+ const rightActionList = useMemo(() => getRightActionList(mobile), [mobile]);
35
+
36
+ return (
37
+ <Flexbox
38
+ align={'center'}
39
+ flex={'none'}
40
+ horizontal
41
+ justify={'space-between'}
42
+ padding={padding}
43
+ >
44
+ <Flexbox align={'center'} flex={1} gap={4} horizontal>
45
+ {leftAreaStartRender}
46
+ <RenderActionList dataSource={leftActionList} />
47
+ {leftAreaEndRender}
48
+ </Flexbox>
49
+ <Flexbox align={'center'} flex={0} gap={4} horizontal justify={'flex-end'}>
50
+ {rightAreaStartRender}
51
+ <RenderActionList dataSource={rightActionList} />
52
+ {rightAreaEndRender}
53
+ </Flexbox>
30
54
  </Flexbox>
31
- </Flexbox>
32
- );
33
- });
55
+ );
56
+ },
57
+ );
34
58
 
35
59
  export default ActionBar;
@@ -0,0 +1,146 @@
1
+ import { ActionIcon, Alert, Highlighter, Icon } from '@lobehub/ui';
2
+ import { Button, Dropdown } from 'antd';
3
+ import { createStyles } from 'antd-style';
4
+ import { Mic, MicOff } from 'lucide-react';
5
+ import { memo, useCallback, useState } from 'react';
6
+ import { useTranslation } from 'react-i18next';
7
+ import { Flexbox } from 'react-layout-kit';
8
+
9
+ import { useSTT } from '@/hooks/useSTT';
10
+ import { useSessionStore } from '@/store/session';
11
+ import { ChatMessageError } from '@/types/chatMessage';
12
+ import { getMessageError } from '@/utils/fetch';
13
+
14
+ const useStyles = createStyles(({ css, token }) => ({
15
+ recording: css`
16
+ width: 8px;
17
+ height: 8px;
18
+ background: ${token.colorError};
19
+ border-radius: 50%;
20
+ `,
21
+ }));
22
+
23
+ const STT = memo<{ mobile?: boolean }>(({ mobile }) => {
24
+ const [error, setError] = useState<ChatMessageError>();
25
+ const { t } = useTranslation('chat');
26
+ const { styles } = useStyles();
27
+
28
+ const [loading, updateInputMessage] = useSessionStore((s) => [
29
+ !!s.chatLoadingId,
30
+ s.updateInputMessage,
31
+ ]);
32
+
33
+ const setDefaultError = useCallback(
34
+ (err?: any) => {
35
+ setError({ body: err, message: t('stt.responseError', { ns: 'error' }), type: 500 });
36
+ },
37
+ [t],
38
+ );
39
+
40
+ const { start, isLoading, stop, formattedTime, time, response, isRecording } = useSTT({
41
+ onError: (err) => {
42
+ stop();
43
+ setDefaultError(err);
44
+ },
45
+ onErrorRetry: (err) => {
46
+ stop();
47
+ setDefaultError(err);
48
+ },
49
+ onSuccess: async () => {
50
+ if (!response) return;
51
+ if (response.status === 200) return;
52
+ const message = await getMessageError(response);
53
+ if (message) {
54
+ setError(message);
55
+ } else {
56
+ setDefaultError();
57
+ }
58
+ stop();
59
+ },
60
+ onTextChange: (text) => {
61
+ if (loading) stop();
62
+ if (text) updateInputMessage(text);
63
+ },
64
+ });
65
+
66
+ const icon = isLoading ? MicOff : Mic;
67
+ const Render: any = !mobile ? ActionIcon : Button;
68
+ const iconRender: any = !mobile ? icon : <Icon icon={icon} />;
69
+ const desc = t('stt.action');
70
+
71
+ const handleTriggerStartStop = useCallback(() => {
72
+ if (loading) return;
73
+ if (!isLoading) {
74
+ start();
75
+ } else {
76
+ stop();
77
+ }
78
+ }, [loading, isLoading, start, stop]);
79
+
80
+ const handleCloseError = useCallback(() => {
81
+ setError(undefined);
82
+ stop();
83
+ }, [stop]);
84
+
85
+ const handleRetry = useCallback(() => {
86
+ setError(undefined);
87
+ start();
88
+ }, [start]);
89
+
90
+ return (
91
+ <Dropdown
92
+ dropdownRender={
93
+ error
94
+ ? () => (
95
+ <Alert
96
+ action={
97
+ <Button onClick={handleRetry} size={'small'} type={'primary'}>
98
+ {t('retry', { ns: 'common' })}
99
+ </Button>
100
+ }
101
+ closable
102
+ extra={
103
+ error.body && (
104
+ <Highlighter copyButtonSize={'small'} language={'json'} type={'pure'}>
105
+ {JSON.stringify(error.body, null, 2)}
106
+ </Highlighter>
107
+ )
108
+ }
109
+ message={error.message}
110
+ onClose={handleCloseError}
111
+ style={{ alignItems: 'center' }}
112
+ type="error"
113
+ />
114
+ )
115
+ : undefined
116
+ }
117
+ menu={{
118
+ activeKey: 'time',
119
+ items: [
120
+ {
121
+ key: 'time',
122
+ label: (
123
+ <Flexbox align={'center'} gap={8} horizontal>
124
+ <div className={styles.recording} />
125
+ {time > 0 ? formattedTime : t(isRecording ? 'stt.loading' : 'stt.prettifying')}
126
+ </Flexbox>
127
+ ),
128
+ },
129
+ ],
130
+ }}
131
+ open={!!error || isRecording || isLoading}
132
+ placement={mobile ? 'topRight' : 'top'}
133
+ trigger={['click']}
134
+ >
135
+ <Render
136
+ icon={iconRender}
137
+ onClick={handleTriggerStartStop}
138
+ placement={'bottom'}
139
+ style={{ flex: 'none' }}
140
+ title={desc}
141
+ />
142
+ </Dropdown>
143
+ );
144
+ });
145
+
146
+ export default STT;
@@ -1,6 +1,5 @@
1
1
  import { ActionIcon, Icon, Tooltip } from '@lobehub/ui';
2
2
  import { Button } from 'antd';
3
- import { useResponsive } from 'antd-style';
4
3
  import { LucideGalleryVerticalEnd, LucideMessageSquarePlus } from 'lucide-react';
5
4
  import { memo } from 'react';
6
5
  import { useHotkeys } from 'react-hotkeys-hook';
@@ -10,13 +9,12 @@ import HotKeys from '@/components/HotKeys';
10
9
  import { PREFIX_KEY, SAVE_TOPIC_KEY } from '@/const/hotkeys';
11
10
  import { useSessionStore } from '@/store/session';
12
11
 
13
- const SaveTopic = memo(() => {
12
+ const SaveTopic = memo<{ mobile?: boolean }>(({ mobile }) => {
14
13
  const { t } = useTranslation('chat');
15
14
  const [hasTopic, openNewTopicOrSaveTopic] = useSessionStore((s) => [
16
15
  !!s.activeTopicId,
17
16
  s.openNewTopicOrSaveTopic,
18
17
  ]);
19
- const { mobile } = useResponsive();
20
18
 
21
19
  const icon = hasTopic ? LucideMessageSquarePlus : LucideGalleryVerticalEnd;
22
20
  const Render = mobile ? ActionIcon : Button;
@@ -6,14 +6,14 @@ import { useCustomActions } from './customAction';
6
6
 
7
7
  export const AssistantActionsBar: RenderAction = memo(({ text, id, onActionClick, error }) => {
8
8
  const { regenerate, edit, copy, divider, del } = useChatListActionsBar(text);
9
- const { translate } = useCustomActions();
9
+ const { translate, tts } = useCustomActions();
10
10
  if (id === 'default') return;
11
11
 
12
12
  if (error) return <ErrorActionsBar onActionClick={onActionClick} text={text} />;
13
13
 
14
14
  return (
15
15
  <ActionIconGroup
16
- dropdownMenu={[edit, copy, regenerate, divider, translate, divider, del]}
16
+ dropdownMenu={[edit, copy, regenerate, divider, tts, translate, divider, del]}
17
17
  items={[regenerate, copy]}
18
18
  onActionClick={onActionClick}
19
19
  type="ghost"
@@ -5,11 +5,11 @@ import { useCustomActions } from './customAction';
5
5
 
6
6
  export const UserActionsBar: RenderAction = memo(({ text, onActionClick }) => {
7
7
  const { regenerate, edit, copy, divider, del } = useChatListActionsBar(text);
8
- const { translate } = useCustomActions();
8
+ const { translate, tts } = useCustomActions();
9
9
 
10
10
  return (
11
11
  <ActionIconGroup
12
- dropdownMenu={[edit, copy, regenerate, divider, translate, divider, del]}
12
+ dropdownMenu={[edit, copy, regenerate, divider, tts, translate, divider, del]}
13
13
  items={[regenerate, edit]}
14
14
  onActionClick={onActionClick}
15
15
  type="ghost"
@@ -1,5 +1,5 @@
1
1
  import { ActionIconGroupItems } from '@lobehub/ui/es/ActionIconGroup';
2
- import { LanguagesIcon } from 'lucide-react';
2
+ import { LanguagesIcon, Play } from 'lucide-react';
3
3
  import { useTranslation } from 'react-i18next';
4
4
 
5
5
  import { localeOptions } from '@/locales/options';
@@ -14,10 +14,17 @@ export const useCustomActions = () => {
14
14
  })),
15
15
  icon: LanguagesIcon,
16
16
  key: 'translate',
17
- label: t('translateTo'),
17
+ label: t('translate.action'),
18
+ } as ActionIconGroupItems;
19
+
20
+ const tts = {
21
+ icon: Play,
22
+ key: 'tts',
23
+ label: t('tts.action'),
18
24
  } as ActionIconGroupItems;
19
25
 
20
26
  return {
21
27
  translate,
28
+ tts,
22
29
  };
23
30
  };
@@ -20,10 +20,11 @@ interface ActionsClick {
20
20
  }
21
21
 
22
22
  export const useActionsClick = (): ChatListProps['onActionsClick'] => {
23
- const [deleteMessage, resendMessage, translateMessage] = useSessionStore((s) => [
23
+ const [deleteMessage, resendMessage, translateMessage, ttsMessage] = useSessionStore((s) => [
24
24
  s.deleteMessage,
25
25
  s.resendMessage,
26
26
  s.translateMessage,
27
+ s.ttsMessage,
27
28
  ]);
28
29
 
29
30
  return (action, { id, error }) => {
@@ -42,6 +43,12 @@ export const useActionsClick = (): ChatListProps['onActionsClick'] => {
42
43
  },
43
44
  trigger: action.key === 'regenerate',
44
45
  },
46
+ {
47
+ onClick: () => {
48
+ ttsMessage(id);
49
+ },
50
+ trigger: action.key === 'tts',
51
+ },
45
52
  {
46
53
  onClick: () => {
47
54
  /**
@@ -1,39 +1,45 @@
1
1
  import { SiOpenai } from '@icons-pack/react-simple-icons';
2
2
  import { RenderMessageExtra, Tag } from '@lobehub/ui';
3
- import { Divider } from 'antd';
4
3
  import { memo } from 'react';
5
4
  import { Flexbox } from 'react-layout-kit';
6
5
 
7
6
  import { useSessionStore } from '@/store/session';
8
7
  import { agentSelectors } from '@/store/session/selectors';
8
+ import { ChatMessage } from '@/types/chatMessage';
9
9
 
10
+ import ExtraContainer from './ExtraContainer';
11
+ import TTS from './TTS';
10
12
  import Translate from './Translate';
11
13
 
12
- export const AssistantMessageExtra: RenderMessageExtra = memo(({ extra, id }) => {
13
- const model = useSessionStore(agentSelectors.currentAgentModel);
14
-
15
- const showModelTag = extra?.fromModel && model !== extra?.fromModel;
16
- const hasTranslate = !!extra?.translate;
17
-
18
- const showExtra = showModelTag || hasTranslate;
19
-
20
- const loading = useSessionStore((s) => s.chatLoadingId === id);
21
-
22
- if (!showExtra) return;
23
-
24
- return (
25
- <Flexbox gap={8} style={{ marginTop: 8 }}>
26
- {showModelTag && (
27
- <div>
28
- <Tag icon={<SiOpenai size={'1em'} />}>{extra?.fromModel as string}</Tag>
29
- </div>
30
- )}
31
- {extra.translate && (
14
+ export const AssistantMessageExtra: RenderMessageExtra = memo<ChatMessage>(
15
+ ({ extra, id, content }) => {
16
+ const model = useSessionStore(agentSelectors.currentAgentModel);
17
+ const loading = useSessionStore((s) => s.chatLoadingId === id);
18
+
19
+ const showModelTag = extra?.fromModel && model !== extra?.fromModel;
20
+ const showExtra = extra?.showModelTag || extra?.translate || extra?.tts;
21
+ if (!showExtra) return;
22
+
23
+ return (
24
+ <Flexbox gap={8} style={{ marginTop: 8 }}>
25
+ {showModelTag && (
26
+ <div>
27
+ <Tag icon={<SiOpenai size={'1em'} />}>{extra?.fromModel as string}</Tag>
28
+ </div>
29
+ )}
32
30
  <div>
33
- <Divider style={{ margin: '12px 0' }} />
34
- <Translate id={id} loading={loading} {...extra.translate} />
31
+ {extra?.tts && (
32
+ <ExtraContainer>
33
+ <TTS content={content} id={id} loading={loading} {...extra?.tts} />
34
+ </ExtraContainer>
35
+ )}
36
+ {extra?.translate && (
37
+ <ExtraContainer>
38
+ <Translate id={id} loading={loading} {...extra?.translate} />
39
+ </ExtraContainer>
40
+ )}
35
41
  </div>
36
- )}
37
- </Flexbox>
38
- );
39
- });
42
+ </Flexbox>
43
+ );
44
+ },
45
+ );