@lobehub/chat 0.101.7 → 0.102.1

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 (87) 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/index.tsx +0 -1
  37. package/src/app/chat/(desktop)/features/SessionHeader.tsx +2 -2
  38. package/src/app/chat/(mobile)/features/ChatHeader.tsx +29 -11
  39. package/src/app/chat/(mobile)/features/ChatInput/Mobile.tsx +20 -4
  40. package/src/app/chat/features/ChatHeader/ShareButton/index.tsx +23 -15
  41. package/src/app/chat/features/ChatInput/ActionBar/config.ts +16 -6
  42. package/src/app/chat/features/ChatInput/ActionBar/index.tsx +40 -16
  43. package/src/app/chat/features/ChatInput/STT/index.tsx +146 -0
  44. package/src/app/chat/features/ChatInput/Topic/index.tsx +1 -3
  45. package/src/app/chat/features/Conversation/ChatList/Actions/Assistant.tsx +2 -2
  46. package/src/app/chat/features/Conversation/ChatList/Actions/User.tsx +2 -2
  47. package/src/app/chat/features/Conversation/ChatList/Actions/customAction.ts +9 -2
  48. package/src/app/chat/features/Conversation/ChatList/Actions/index.ts +8 -1
  49. package/src/app/chat/features/Conversation/ChatList/Extras/Assistant.tsx +33 -27
  50. package/src/app/chat/features/Conversation/ChatList/Extras/AudioPlayer.tsx +139 -0
  51. package/src/app/chat/features/Conversation/ChatList/Extras/ExtraContainer.tsx +13 -0
  52. package/src/app/chat/features/Conversation/ChatList/Extras/TTS.tsx +122 -0
  53. package/src/app/chat/features/Conversation/ChatList/Extras/User.tsx +18 -11
  54. package/src/app/chat/settings/features/HeaderContent.tsx +6 -4
  55. package/src/app/chat/settings/features/SubmitAgentButton/index.tsx +2 -3
  56. package/src/app/settings/features/SideBar/List.tsx +2 -1
  57. package/src/app/settings/features/SideBar/index.tsx +1 -1
  58. package/src/app/settings/features/UpgradeAlert.tsx +1 -1
  59. package/src/app/settings/tts/TTS/index.tsx +73 -0
  60. package/src/app/settings/tts/TTS/options.ts +30 -0
  61. package/src/app/settings/tts/index.tsx +21 -0
  62. package/src/app/settings/tts/layout.tsx +1 -0
  63. package/src/app/settings/tts/page.tsx +3 -0
  64. package/src/components/HotKeys/index.tsx +4 -7
  65. package/src/const/layoutTokens.ts +3 -0
  66. package/src/const/settings.ts +22 -1
  67. package/src/features/AgentSetting/AgentTTS/SelectWithTTSPreview.tsx +119 -0
  68. package/src/features/AgentSetting/AgentTTS/index.tsx +115 -0
  69. package/src/features/AgentSetting/AgentTTS/options.ts +16 -0
  70. package/src/features/AgentSetting/index.tsx +2 -1
  71. package/src/hooks/useSTT.ts +61 -0
  72. package/src/hooks/useTTS.ts +87 -0
  73. package/src/locales/default/chat.ts +10 -1
  74. package/src/locales/default/error.ts +6 -0
  75. package/src/locales/default/setting.ts +35 -1
  76. package/src/services/_url.ts +7 -0
  77. package/src/store/global/initialState.ts +1 -0
  78. package/src/store/global/selectors/settings.test.ts +32 -0
  79. package/src/store/global/selectors/settings.ts +9 -1
  80. package/src/store/session/slices/agentConfig/selectors.ts +9 -0
  81. package/src/store/session/slices/chat/actions/translate.ts +24 -1
  82. package/src/store/session/slices/chat/selectors/utils.test.ts +16 -0
  83. package/src/store/session/slices/session/reducers/session.test.ts +24 -0
  84. package/src/types/chatMessage.ts +6 -0
  85. package/src/types/session.ts +17 -0
  86. package/src/types/settings.ts +13 -2
  87. package/src/app/chat/(mobile)/features/ChatInput/style.mobile.ts +0 -19
@@ -0,0 +1,13 @@
1
+ import { Divider } from 'antd';
2
+ import { PropsWithChildren, memo } from 'react';
3
+
4
+ const ExtraContainer = memo<PropsWithChildren>(({ children }) => {
5
+ return (
6
+ <div>
7
+ <Divider style={{ margin: '8px 0' }} />
8
+ {children}
9
+ </div>
10
+ );
11
+ });
12
+
13
+ export default ExtraContainer;
@@ -0,0 +1,122 @@
1
+ import { AudioPlayer } from '@lobehub/tts/react';
2
+ import { ActionIcon, Alert, Highlighter } from '@lobehub/ui';
3
+ import { Button } from 'antd';
4
+ import { TrashIcon } from 'lucide-react';
5
+ import { memo, useCallback, useEffect, useState } from 'react';
6
+ import { useTranslation } from 'react-i18next';
7
+ import { Flexbox } from 'react-layout-kit';
8
+
9
+ import { useTTS } from '@/hooks/useTTS';
10
+ import { useSessionStore } from '@/store/session';
11
+ import { ChatMessageError, ChatTTS } from '@/types/chatMessage';
12
+ import { getMessageError } from '@/utils/fetch';
13
+
14
+ interface TTSProps extends ChatTTS {
15
+ content: string;
16
+ id: string;
17
+ loading?: boolean;
18
+ }
19
+
20
+ const TTS = memo<TTSProps>(({ id, init, content }) => {
21
+ const [isStart, setIsStart] = useState(false);
22
+ const [error, setError] = useState<ChatMessageError>();
23
+ const { t } = useTranslation('chat');
24
+
25
+ const [ttsMessage, clearTTS] = useSessionStore((s) => [s.ttsMessage, s.clearTTS]);
26
+
27
+ const setDefaultError = useCallback(
28
+ (err?: any) => {
29
+ setError({ body: err, message: t('tts.responseError', { ns: 'error' }), type: 500 });
30
+ },
31
+ [t],
32
+ );
33
+
34
+ const { isGlobalLoading, audio, start, stop, response } = useTTS(content, {
35
+ onError: (err) => {
36
+ stop();
37
+ setDefaultError(err);
38
+ },
39
+ onErrorRetry: (err) => {
40
+ stop();
41
+ setDefaultError(err);
42
+ },
43
+ onSuccess: async () => {
44
+ if (!response) return;
45
+ if (response.status === 200) return ttsMessage(id, true);
46
+ const message = await getMessageError(response);
47
+ if (message) {
48
+ setError(message);
49
+ } else {
50
+ setDefaultError();
51
+ }
52
+ stop();
53
+ },
54
+ });
55
+
56
+ const handleInitStart = useCallback(() => {
57
+ if (isStart) return;
58
+ start();
59
+ setIsStart(true);
60
+ }, [isStart]);
61
+
62
+ const handleDelete = useCallback(() => {
63
+ stop();
64
+ clearTTS(id);
65
+ }, [stop, id]);
66
+
67
+ const handleRetry = useCallback(() => {
68
+ setError(undefined);
69
+ start();
70
+ }, [start]);
71
+
72
+ useEffect(() => {
73
+ if (init) return;
74
+ handleInitStart();
75
+ }, [init]);
76
+
77
+ return (
78
+ <Flexbox align={'center'} horizontal style={{ minWidth: 160, width: '100%' }}>
79
+ {error ? (
80
+ <Alert
81
+ action={
82
+ <Button onClick={handleRetry} size={'small'} type={'primary'}>
83
+ {t('retry', { ns: 'common' })}
84
+ </Button>
85
+ }
86
+ closable
87
+ extra={
88
+ error.body && (
89
+ <Highlighter copyButtonSize={'small'} language={'json'} type={'pure'}>
90
+ {JSON.stringify(error.body, null, 2)}
91
+ </Highlighter>
92
+ )
93
+ }
94
+ message={error.message}
95
+ onClose={handleDelete}
96
+ style={{ alignItems: 'center', width: '100%' }}
97
+ type="error"
98
+ />
99
+ ) : (
100
+ <>
101
+ <AudioPlayer
102
+ audio={audio}
103
+ buttonSize={'small'}
104
+ isLoading={isGlobalLoading}
105
+ onInitPlay={handleInitStart}
106
+ onLoadingStop={stop}
107
+ timeRender={'tag'}
108
+ timeStyle={{ margin: 0 }}
109
+ />
110
+ <ActionIcon
111
+ icon={TrashIcon}
112
+ onClick={handleDelete}
113
+ size={'small'}
114
+ title={t('tts.clear')}
115
+ />
116
+ </>
117
+ )}
118
+ </Flexbox>
119
+ );
120
+ });
121
+
122
+ export default TTS;
@@ -1,24 +1,31 @@
1
1
  import { RenderMessageExtra } from '@lobehub/ui';
2
- import { Divider } from 'antd';
3
2
  import { memo } from 'react';
4
- import { Flexbox } from 'react-layout-kit';
5
3
 
6
4
  import { useSessionStore } from '@/store/session';
5
+ import { ChatMessage } from '@/types/chatMessage';
7
6
 
7
+ import ExtraContainer from './ExtraContainer';
8
+ import TTS from './TTS';
8
9
  import Translate from './Translate';
9
10
 
10
- export const UserMessageExtra: RenderMessageExtra = memo(({ extra, id }) => {
11
- const hasTranslate = !!extra?.translate;
12
-
11
+ export const UserMessageExtra: RenderMessageExtra = memo<ChatMessage>(({ extra, id, content }) => {
13
12
  const loading = useSessionStore((s) => s.chatLoadingId === id);
13
+
14
+ const showExtra = extra?.translate || extra?.tts;
15
+ if (!showExtra) return;
16
+
14
17
  return (
15
- <Flexbox gap={8} style={{ marginTop: hasTranslate ? 8 : 0 }}>
18
+ <div style={{ marginTop: 8 }}>
19
+ {extra?.tts && (
20
+ <ExtraContainer>
21
+ <TTS content={content} id={id} loading={loading} {...extra?.tts} />
22
+ </ExtraContainer>
23
+ )}
16
24
  {extra?.translate && (
17
- <div>
18
- <Divider style={{ margin: '12px 0' }} />
19
- <Translate id={id} {...extra.translate} loading={loading} />
20
- </div>
25
+ <ExtraContainer>
26
+ <Translate id={id} {...extra?.translate} loading={loading} />
27
+ </ExtraContainer>
21
28
  )}
22
- </Flexbox>
29
+ </div>
23
30
  );
24
31
  });
@@ -5,7 +5,7 @@ import { HardDriveDownload } from 'lucide-react';
5
5
  import { memo, useMemo } from 'react';
6
6
  import { useTranslation } from 'react-i18next';
7
7
 
8
- import { MOBILE_HEADER_ICON_SIZE } from '@/const/layoutTokens';
8
+ import { HEADER_ICON_SIZE } from '@/const/layoutTokens';
9
9
  import { exportSingleAgent, exportSingleSession } from '@/helpers/export';
10
10
  import { useSessionStore } from '@/store/session';
11
11
 
@@ -41,13 +41,15 @@ export const HeaderContent = memo<{ mobile?: boolean }>(() => {
41
41
  [],
42
42
  );
43
43
 
44
- const size = mobile ? MOBILE_HEADER_ICON_SIZE : { fontSize: 24 };
45
-
46
44
  return (
47
45
  <>
48
46
  <SubmitAgentButton />
49
47
  <Dropdown arrow={false} menu={{ items }} trigger={['click']}>
50
- <ActionIcon icon={HardDriveDownload} size={size} title={t('export', { ns: 'common' })} />
48
+ <ActionIcon
49
+ icon={HardDriveDownload}
50
+ size={HEADER_ICON_SIZE(mobile)}
51
+ title={t('export', { ns: 'common' })}
52
+ />
51
53
  </Dropdown>
52
54
  </>
53
55
  );
@@ -4,7 +4,7 @@ import { Share2 } from 'lucide-react';
4
4
  import { memo, useState } from 'react';
5
5
  import { useTranslation } from 'react-i18next';
6
6
 
7
- import { MOBILE_HEADER_ICON_SIZE } from '@/const/layoutTokens';
7
+ import { HEADER_ICON_SIZE } from '@/const/layoutTokens';
8
8
 
9
9
  import Inner from './Inner';
10
10
 
@@ -12,13 +12,12 @@ const SubmitAgentButton = memo(() => {
12
12
  const { t } = useTranslation('setting');
13
13
  const { mobile } = useResponsive();
14
14
  const [isModalOpen, setIsModalOpen] = useState(false);
15
- const size = mobile ? MOBILE_HEADER_ICON_SIZE : { fontSize: 24 };
16
15
  return (
17
16
  <>
18
17
  <ActionIcon
19
18
  icon={Share2}
20
19
  onClick={() => setIsModalOpen(true)}
21
- size={size}
20
+ size={HEADER_ICON_SIZE(mobile)}
22
21
  title={t('submitAgentModal.tooltips')}
23
22
  />
24
23
  <Modal
@@ -1,5 +1,5 @@
1
1
  import { useResponsive } from 'antd-style';
2
- import { Bot, Settings2, Webhook } from 'lucide-react';
2
+ import { Bot, Mic2, Settings2, Webhook } from 'lucide-react';
3
3
  import Link from 'next/link';
4
4
  import { memo } from 'react';
5
5
  import { useTranslation } from 'react-i18next';
@@ -17,6 +17,7 @@ const List = memo(() => {
17
17
  const items = [
18
18
  { icon: Settings2, label: t('tab.common'), value: SettingsTabs.Common },
19
19
  { icon: Webhook, label: t('tab.llm'), value: SettingsTabs.LLM },
20
+ { icon: Mic2, label: t('tab.tts'), value: SettingsTabs.TTS },
20
21
  { icon: Bot, label: t('tab.agent'), value: SettingsTabs.Agent },
21
22
  ];
22
23
 
@@ -30,8 +30,8 @@ const SideBar = memo(() => {
30
30
  <Logo className={styles.logo} extra={'Settings'} size={36} type={'text'} />
31
31
  </div>
32
32
  </Flexbox>
33
- <UpgradeAlert />
34
33
  <Flexbox gap={2} style={{ paddingInline: 8 }}>
34
+ <UpgradeAlert />
35
35
  <List />
36
36
  </Flexbox>
37
37
  </DraggablePanelBody>
@@ -21,10 +21,10 @@ const UpgradeAlert = memo(() => {
21
21
  </Button>
22
22
  </Link>
23
23
  }
24
- banner
25
24
  closable
26
25
  message={`✨ ${t('upgradeVersion.newVersion', { version: latestVersion })}`}
27
26
  showIcon={false}
27
+ style={{ marginBottom: 6 }}
28
28
  type={'info'}
29
29
  />
30
30
  )
@@ -0,0 +1,73 @@
1
+ import { Form, type ItemGroup } from '@lobehub/ui';
2
+ import { Form as AntForm, Select, Switch } from 'antd';
3
+ import isEqual from 'fast-deep-equal';
4
+ import { debounce } from 'lodash-es';
5
+ import { Mic, Webhook } from 'lucide-react';
6
+ import { memo } from 'react';
7
+ import { useTranslation } from 'react-i18next';
8
+
9
+ import { FORM_STYLE } from '@/const/layoutTokens';
10
+ import { settingsSelectors, useGlobalStore } from '@/store/global';
11
+
12
+ import { opeanaiSTTOptions, opeanaiTTSOptions, sttOptions } from './options';
13
+
14
+ type SettingItemGroup = ItemGroup;
15
+
16
+ const TTS_SETTING_KEY = 'tts';
17
+
18
+ const TTS = memo(() => {
19
+ const { t } = useTranslation('setting');
20
+ const [form] = AntForm.useForm();
21
+ const settings = useGlobalStore(settingsSelectors.currentSettings, isEqual);
22
+ const [setSettings] = useGlobalStore((s) => [s.setSettings]);
23
+
24
+ const stt: SettingItemGroup = {
25
+ children: [
26
+ {
27
+ children: <Select options={sttOptions} />,
28
+ desc: t('settingTTS.sttService.desc'),
29
+ label: t('settingTTS.sttService.title'),
30
+ name: [TTS_SETTING_KEY, 'sttServer'],
31
+ },
32
+ {
33
+ children: <Switch />,
34
+ desc: t('settingTTS.sttAutoStop.desc'),
35
+ label: t('settingTTS.sttAutoStop.title'),
36
+ minWidth: undefined,
37
+ name: [TTS_SETTING_KEY, 'sttAutoStop'],
38
+ valuePropName: 'checked',
39
+ },
40
+ ],
41
+ icon: Mic,
42
+ title: t('settingTTS.stt'),
43
+ };
44
+
45
+ const openai: SettingItemGroup = {
46
+ children: [
47
+ {
48
+ children: <Select options={opeanaiTTSOptions} />,
49
+ label: t('settingTTS.openai.ttsModel'),
50
+ name: [TTS_SETTING_KEY, 'openAI', 'ttsModel'],
51
+ },
52
+ {
53
+ children: <Select options={opeanaiSTTOptions} />,
54
+ label: t('settingTTS.openai.sttModel'),
55
+ name: [TTS_SETTING_KEY, 'openAI', 'sttModel'],
56
+ },
57
+ ],
58
+ icon: Webhook,
59
+ title: t('llm.OpenAI.title'),
60
+ };
61
+
62
+ return (
63
+ <Form
64
+ form={form}
65
+ initialValues={settings}
66
+ items={[stt, openai]}
67
+ onValuesChange={debounce(setSettings, 100)}
68
+ {...FORM_STYLE}
69
+ />
70
+ );
71
+ });
72
+
73
+ export default TTS;
@@ -0,0 +1,30 @@
1
+ import { SelectProps } from 'antd';
2
+
3
+ export const opeanaiTTSOptions: SelectProps['options'] = [
4
+ {
5
+ label: 'tts-1',
6
+ value: 'tts-1',
7
+ },
8
+ {
9
+ label: 'tts-1-hd',
10
+ value: 'tts-1-hd',
11
+ },
12
+ ];
13
+
14
+ export const opeanaiSTTOptions: SelectProps['options'] = [
15
+ {
16
+ label: 'whisper-1',
17
+ value: 'whisper-1',
18
+ },
19
+ ];
20
+
21
+ export const sttOptions: SelectProps['options'] = [
22
+ {
23
+ label: 'OpenAI',
24
+ value: 'openai',
25
+ },
26
+ {
27
+ label: 'Browser',
28
+ value: 'browser',
29
+ },
30
+ ];
@@ -0,0 +1,21 @@
1
+ 'use client';
2
+
3
+ import { memo } from 'react';
4
+ import { useTranslation } from 'react-i18next';
5
+
6
+ import PageTitle from '@/components/PageTitle';
7
+ import { useSwitchSideBarOnInit } from '@/store/global/hooks/useSwitchSettingsOnInit';
8
+ import { SettingsTabs } from '@/store/global/initialState';
9
+
10
+ import TTS from './TTS';
11
+
12
+ export default memo(() => {
13
+ useSwitchSideBarOnInit(SettingsTabs.TTS);
14
+ const { t } = useTranslation('setting');
15
+ return (
16
+ <>
17
+ <PageTitle title={t('tab.llm')} />
18
+ <TTS />
19
+ </>
20
+ );
21
+ });
@@ -0,0 +1 @@
1
+ export { default } from '../layout.server';
@@ -0,0 +1,3 @@
1
+ import Index from './index';
2
+
3
+ export default () => <Index />;
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { createStyles } from 'antd-style';
4
- import { Fragment, memo, useEffect, useState } from 'react';
4
+ import { memo, useEffect, useState } from 'react';
5
5
  import { Flexbox } from 'react-layout-kit';
6
6
 
7
7
  import { CLEAN_MESSAGE_KEY, PREFIX_KEY } from '@/const/hotkeys';
@@ -56,12 +56,9 @@ const HotKeys = memo<HotKeysProps>(({ keys, desc }) => {
56
56
  const content = (
57
57
  <Flexbox align={'center'} className={styles} gap={2} horizontal>
58
58
  {keysGroup.map((key, index) => (
59
- <Fragment key={index}>
60
- <kbd>
61
- <span style={{ visibility }}>{key.toUpperCase()}</span>
62
- </kbd>
63
- {index + 1 < keysGroup.length && <span>+</span>}
64
- </Fragment>
59
+ <kbd key={index}>
60
+ <span style={{ visibility }}>{key.toUpperCase()}</span>
61
+ </kbd>
65
62
  ))}
66
63
  </Flexbox>
67
64
  );
@@ -14,3 +14,6 @@ export const FORM_STYLE: FormProps = {
14
14
  style: { maxWidth: MAX_WIDTH, width: '100%' },
15
15
  };
16
16
  export const MOBILE_HEADER_ICON_SIZE = { blockSize: 36, fontSize: 22 };
17
+ export const DESKTOP_HEADER_ICON_SIZE = { fontSize: 24 };
18
+ export const HEADER_ICON_SIZE = (mobile?: boolean) =>
19
+ mobile ? MOBILE_HEADER_ICON_SIZE : DESKTOP_HEADER_ICON_SIZE;
@@ -2,12 +2,13 @@ import { getClientConfig } from '@/config/client';
2
2
  import { DEFAULT_OPENAI_MODEL_LIST } from '@/const/llm';
3
3
  import { DEFAULT_AGENT_META } from '@/const/meta';
4
4
  import { LanguageModel } from '@/types/llm';
5
- import { LobeAgentConfig } from '@/types/session';
5
+ import { LobeAgentConfig, LobeAgentTTSConfig } from '@/types/session';
6
6
  import {
7
7
  GlobalBaseSettings,
8
8
  GlobalDefaultAgent,
9
9
  GlobalLLMConfig,
10
10
  GlobalSettings,
11
+ GlobalTTSConfig,
11
12
  } from '@/types/settings';
12
13
 
13
14
  export const DEFAULT_BASE_SETTINGS: GlobalBaseSettings = {
@@ -18,6 +19,15 @@ export const DEFAULT_BASE_SETTINGS: GlobalBaseSettings = {
18
19
  themeMode: 'auto',
19
20
  };
20
21
 
22
+ export const DEFAUTT_AGENT_TTS_CONFIG: LobeAgentTTSConfig = {
23
+ showAllLocaleVoice: false,
24
+ sttLocale: 'auto',
25
+ ttsService: 'openai',
26
+ voice: {
27
+ openai: 'alloy',
28
+ },
29
+ };
30
+
21
31
  export const VISION_MODEL_DEFAULT_MAX_TOKENS = 1000;
22
32
 
23
33
  export const DEFAULT_AGENT_CONFIG: LobeAgentConfig = {
@@ -32,6 +42,7 @@ export const DEFAULT_AGENT_CONFIG: LobeAgentConfig = {
32
42
  },
33
43
  plugins: [],
34
44
  systemRole: '',
45
+ tts: DEFAUTT_AGENT_TTS_CONFIG,
35
46
  };
36
47
 
37
48
  export const DEFAULT_LLM_CONFIG: GlobalLLMConfig = {
@@ -48,8 +59,18 @@ export const DEFAULT_AGENT: GlobalDefaultAgent = {
48
59
  meta: DEFAULT_AGENT_META,
49
60
  };
50
61
 
62
+ export const DEFAULT_TTS_CONFIG: GlobalTTSConfig = {
63
+ openAI: {
64
+ sttModel: 'whisper-1',
65
+ ttsModel: 'tts-1',
66
+ },
67
+ sttAutoStop: true,
68
+ sttServer: 'openai',
69
+ };
70
+
51
71
  export const DEFAULT_SETTINGS: GlobalSettings = {
52
72
  defaultAgent: DEFAULT_AGENT,
53
73
  languageModel: DEFAULT_LLM_CONFIG,
74
+ tts: DEFAULT_TTS_CONFIG,
54
75
  ...DEFAULT_BASE_SETTINGS,
55
76
  };
@@ -0,0 +1,119 @@
1
+ import { AudioPlayer } from '@lobehub/tts/react';
2
+ import { Alert, Highlighter } from '@lobehub/ui';
3
+ import { Button, RefSelectProps, Select, SelectProps } from 'antd';
4
+ import { useTheme } from 'antd-style';
5
+ import { forwardRef, useCallback, useState } from 'react';
6
+ import { useTranslation } from 'react-i18next';
7
+ import { Flexbox } from 'react-layout-kit';
8
+
9
+ import { useTTS } from '@/hooks/useTTS';
10
+ import { ChatMessageError } from '@/types/chatMessage';
11
+ import { TTSServer } from '@/types/session';
12
+ import { getMessageError } from '@/utils/fetch';
13
+
14
+ interface SelectWithTTSPreviewProps extends SelectProps {
15
+ server: TTSServer;
16
+ }
17
+
18
+ const SelectWithTTSPreview = forwardRef<RefSelectProps, SelectWithTTSPreviewProps>(
19
+ ({ value, options, server, onSelect, ...rest }, ref) => {
20
+ const [error, setError] = useState<ChatMessageError>();
21
+ const [voice, setVoice] = useState<string>(value);
22
+ const { t } = useTranslation('welcome');
23
+ const theme = useTheme();
24
+ const PREVIEW_TEXT = ['Lobe Chat', t('slogan.title'), t('slogan.desc1')].join('. ');
25
+
26
+ const setDefaultError = useCallback(
27
+ (err?: any) => {
28
+ setError({ body: err, message: t('tts.responseError', { ns: 'error' }), type: 500 });
29
+ },
30
+ [t],
31
+ );
32
+
33
+ const { isGlobalLoading, audio, stop, start, response, setText } = useTTS(PREVIEW_TEXT, {
34
+ onError: (err) => {
35
+ stop();
36
+ setDefaultError(err);
37
+ },
38
+ onErrorRetry: (err) => {
39
+ stop();
40
+ setDefaultError(err);
41
+ },
42
+ onSuccess: async () => {
43
+ if (!response) return;
44
+ if (response.status === 200) return;
45
+ const message = await getMessageError(response);
46
+ if (message) {
47
+ setError(message);
48
+ } else {
49
+ setDefaultError();
50
+ }
51
+ stop();
52
+ },
53
+ server,
54
+ voice,
55
+ });
56
+
57
+ const handleCloseError = useCallback(() => {
58
+ setError(undefined);
59
+ stop();
60
+ }, [stop]);
61
+
62
+ const handleRetry = useCallback(() => {
63
+ setError(undefined);
64
+ stop();
65
+ start();
66
+ }, [stop, start]);
67
+
68
+ const handleSelect: SelectProps['onSelect'] = (value, option) => {
69
+ stop();
70
+ setVoice(value as string);
71
+ setText([PREVIEW_TEXT, option?.label].join(' - '));
72
+ onSelect?.(value, option);
73
+ };
74
+ return (
75
+ <Flexbox gap={8}>
76
+ <Flexbox align={'center'} gap={8} horizontal style={{ width: '100%' }}>
77
+ <Select onSelect={handleSelect} options={options} ref={ref} value={value} {...rest} />
78
+ <AudioPlayer
79
+ allowPause={false}
80
+ audio={audio}
81
+ buttonActive
82
+ buttonSize={{ blockSize: 36, fontSize: 16 }}
83
+ buttonStyle={{ border: `1px solid ${theme.colorBorder}` }}
84
+ isLoading={isGlobalLoading}
85
+ onInitPlay={start}
86
+ onLoadingStop={stop}
87
+ showSlider={false}
88
+ showTime={false}
89
+ style={{ flex: 'none', padding: 0, width: 'unset' }}
90
+ title={t('settingTTS.voice.preview', { ns: 'setting' })}
91
+ />
92
+ </Flexbox>
93
+ {error && (
94
+ <Alert
95
+ action={
96
+ <Button onClick={handleRetry} size={'small'} type={'primary'}>
97
+ {t('retry', { ns: 'common' })}
98
+ </Button>
99
+ }
100
+ closable
101
+ extra={
102
+ error.body && (
103
+ <Highlighter copyButtonSize={'small'} language={'json'} type={'pure'}>
104
+ {JSON.stringify(error.body, null, 2)}
105
+ </Highlighter>
106
+ )
107
+ }
108
+ message={error.message}
109
+ onClose={handleCloseError}
110
+ style={{ alignItems: 'center', width: '100%' }}
111
+ type="error"
112
+ />
113
+ )}
114
+ </Flexbox>
115
+ );
116
+ },
117
+ );
118
+
119
+ export default SelectWithTTSPreview;