@lobehub/chat 0.101.7 → 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 (88) hide show
  1. package/.i18nrc.js +1 -1
  2. package/CHANGELOG.md +25 -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/(mobile)/features/ChatHeader.tsx +29 -11
  40. package/src/app/chat/(mobile)/features/ChatInput/Mobile.tsx +20 -4
  41. package/src/app/chat/features/ChatHeader/ShareButton/index.tsx +23 -15
  42. package/src/app/chat/features/ChatInput/ActionBar/config.ts +16 -6
  43. package/src/app/chat/features/ChatInput/ActionBar/index.tsx +40 -16
  44. package/src/app/chat/features/ChatInput/STT/index.tsx +146 -0
  45. package/src/app/chat/features/ChatInput/Topic/index.tsx +1 -3
  46. package/src/app/chat/features/Conversation/ChatList/Actions/Assistant.tsx +2 -2
  47. package/src/app/chat/features/Conversation/ChatList/Actions/User.tsx +2 -2
  48. package/src/app/chat/features/Conversation/ChatList/Actions/customAction.ts +9 -2
  49. package/src/app/chat/features/Conversation/ChatList/Actions/index.ts +8 -1
  50. package/src/app/chat/features/Conversation/ChatList/Extras/Assistant.tsx +33 -27
  51. package/src/app/chat/features/Conversation/ChatList/Extras/AudioPlayer.tsx +139 -0
  52. package/src/app/chat/features/Conversation/ChatList/Extras/ExtraContainer.tsx +13 -0
  53. package/src/app/chat/features/Conversation/ChatList/Extras/TTS.tsx +122 -0
  54. package/src/app/chat/features/Conversation/ChatList/Extras/User.tsx +18 -11
  55. package/src/app/chat/settings/features/HeaderContent.tsx +6 -4
  56. package/src/app/chat/settings/features/SubmitAgentButton/index.tsx +2 -3
  57. package/src/app/settings/features/SideBar/List.tsx +2 -1
  58. package/src/app/settings/features/SideBar/index.tsx +1 -1
  59. package/src/app/settings/features/UpgradeAlert.tsx +1 -1
  60. package/src/app/settings/tts/TTS/index.tsx +73 -0
  61. package/src/app/settings/tts/TTS/options.ts +30 -0
  62. package/src/app/settings/tts/index.tsx +21 -0
  63. package/src/app/settings/tts/layout.tsx +1 -0
  64. package/src/app/settings/tts/page.tsx +3 -0
  65. package/src/components/HotKeys/index.tsx +4 -7
  66. package/src/const/layoutTokens.ts +3 -0
  67. package/src/const/settings.ts +22 -1
  68. package/src/features/AgentSetting/AgentTTS/SelectWithTTSPreview.tsx +119 -0
  69. package/src/features/AgentSetting/AgentTTS/index.tsx +115 -0
  70. package/src/features/AgentSetting/AgentTTS/options.ts +16 -0
  71. package/src/features/AgentSetting/index.tsx +2 -1
  72. package/src/hooks/useSTT.ts +61 -0
  73. package/src/hooks/useTTS.ts +87 -0
  74. package/src/locales/default/chat.ts +10 -1
  75. package/src/locales/default/error.ts +6 -0
  76. package/src/locales/default/setting.ts +35 -1
  77. package/src/services/_url.ts +7 -0
  78. package/src/store/global/initialState.ts +1 -0
  79. package/src/store/global/selectors/settings.test.ts +32 -0
  80. package/src/store/global/selectors/settings.ts +9 -1
  81. package/src/store/session/slices/agentConfig/selectors.ts +9 -0
  82. package/src/store/session/slices/chat/actions/translate.ts +24 -1
  83. package/src/store/session/slices/chat/selectors/utils.test.ts +16 -0
  84. package/src/store/session/slices/session/reducers/session.test.ts +24 -0
  85. package/src/types/chatMessage.ts +6 -0
  86. package/src/types/session.ts +17 -0
  87. package/src/types/settings.ts +13 -2
  88. package/src/app/chat/(mobile)/features/ChatInput/style.mobile.ts +0 -19
@@ -0,0 +1,139 @@
1
+ import { ActionIcon, ActionIconProps, Icon, Tag } from '@lobehub/ui';
2
+ import { Dropdown, Slider } from 'antd';
3
+ import { Download, PauseCircle, Play, StopCircle } from 'lucide-react';
4
+ import React, { memo, useCallback, useMemo } from 'react';
5
+ import { Flexbox } from 'react-layout-kit';
6
+
7
+ const secondsToMinutesAndSeconds = (num: number) => Math.floor(num);
8
+ export interface AudioProps {
9
+ currentTime: number;
10
+ download: () => void;
11
+ duration: number;
12
+ isPlaying: boolean;
13
+ pause: () => void;
14
+ play: () => void;
15
+ setTime: (time: number) => void;
16
+ stop: () => void;
17
+ }
18
+
19
+ export interface AudioPlayerProps {
20
+ allowPause?: boolean;
21
+ audio: AudioProps;
22
+ buttonSize?: ActionIconProps['size'];
23
+ className?: string;
24
+ isLoading?: boolean;
25
+ onInitPlay?: () => void;
26
+ onPause?: () => void;
27
+ onPlay?: () => void;
28
+ onStop?: () => void;
29
+ showSlider?: boolean;
30
+ style?: React.CSSProperties;
31
+ timeRender?: 'tag' | 'text';
32
+ timeStyle?: React.CSSProperties;
33
+ timeType?: 'left' | 'current' | 'combine';
34
+ }
35
+
36
+ const AudioPlayer = memo<AudioPlayerProps>(
37
+ ({
38
+ isLoading,
39
+ style,
40
+ timeStyle,
41
+ buttonSize,
42
+ className,
43
+ audio,
44
+ allowPause = true,
45
+ timeType = 'left',
46
+ showSlider = true,
47
+ timeRender = 'text',
48
+ onInitPlay,
49
+ onPause,
50
+ onStop,
51
+ onPlay,
52
+ }) => {
53
+ const { isPlaying, play, stop, pause, duration, setTime, currentTime, download } = audio;
54
+
55
+ const formatedLeftTime = secondsToMinutesAndSeconds(duration - currentTime);
56
+ const formatedCurrentTime = secondsToMinutesAndSeconds(currentTime);
57
+ const formatedDuration = secondsToMinutesAndSeconds(duration);
58
+
59
+ const Time = useMemo(
60
+ () => (timeRender === 'tag' ? Tag : (props: any) => <div {...props} />),
61
+ [timeRender],
62
+ );
63
+
64
+ const handlePlay = useCallback(() => {
65
+ if ((!duration || duration === 0) && !isLoading) {
66
+ onInitPlay?.();
67
+ } else {
68
+ play?.();
69
+ onPlay?.();
70
+ }
71
+ }, [play, duration]);
72
+
73
+ const handlePause = useCallback(() => {
74
+ pause?.();
75
+ onPause?.();
76
+ }, [pause]);
77
+
78
+ const handleStop = useCallback(() => {
79
+ stop?.();
80
+ onStop?.();
81
+ }, [stop]);
82
+
83
+ return (
84
+ <Flexbox
85
+ align={'center'}
86
+ className={className}
87
+ gap={8}
88
+ horizontal
89
+ style={{ paddingRight: 8, width: '100%', ...style }}
90
+ >
91
+ <ActionIcon
92
+ icon={isPlaying ? (allowPause ? PauseCircle : StopCircle) : Play}
93
+ loading={isLoading}
94
+ onClick={isPlaying ? (allowPause ? handlePause : handleStop) : handlePlay}
95
+ size={buttonSize || { blockSize: 32, fontSize: 16 }}
96
+ style={{ flex: 'none' }}
97
+ />
98
+ {showSlider && (
99
+ <Slider
100
+ disabled={duration === 0}
101
+ max={duration}
102
+ min={0}
103
+ onChange={(e) => setTime(e)}
104
+ step={0.01}
105
+ style={{ flex: 1 }}
106
+ tooltip={{ formatter: secondsToMinutesAndSeconds as any }}
107
+ value={currentTime}
108
+ />
109
+ )}
110
+ <Dropdown
111
+ disabled={duration === 0}
112
+ menu={{
113
+ items: [
114
+ {
115
+ key: 'download',
116
+ label: <Icon icon={Download} size={{ fontSize: 16 }} />,
117
+ onClick: download,
118
+ },
119
+ ],
120
+ }}
121
+ placement="top"
122
+ >
123
+ <Time style={{ cursor: 'pointer', flex: 'none', ...timeStyle }}>
124
+ {timeType === 'left' && formatedLeftTime}
125
+ {timeType === 'current' && formatedCurrentTime}
126
+ {timeType === 'combine' && (
127
+ <span>
128
+ {formatedCurrentTime}
129
+ <span style={{ opacity: 0.66 }}>{` / ${formatedDuration}`}</span>
130
+ </span>
131
+ )}
132
+ </Time>
133
+ </Dropdown>
134
+ </Flexbox>
135
+ );
136
+ },
137
+ );
138
+
139
+ export default AudioPlayer;
@@ -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
  };