@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.
- package/.i18nrc.js +1 -1
- package/CHANGELOG.md +50 -0
- package/README.md +38 -4
- package/README.zh-CN.md +41 -9
- package/__mocks__/zustand/traditional.ts +4 -4
- package/locales/en_US/chat.json +11 -1
- package/locales/en_US/error.json +6 -0
- package/locales/en_US/setting.json +40 -1
- package/locales/ja_JP/chat.json +11 -1
- package/locales/ja_JP/error.json +6 -0
- package/locales/ja_JP/setting.json +40 -1
- package/locales/ko_KR/chat.json +11 -1
- package/locales/ko_KR/error.json +6 -0
- package/locales/ko_KR/setting.json +40 -1
- package/locales/ru_RU/chat.json +11 -1
- package/locales/ru_RU/error.json +6 -0
- package/locales/ru_RU/setting.json +40 -1
- package/locales/zh_CN/chat.json +10 -1
- package/locales/zh_CN/error.json +6 -0
- package/locales/zh_CN/setting.json +36 -1
- package/locales/zh_TW/chat.json +11 -1
- package/locales/zh_TW/error.json +6 -0
- package/locales/zh_TW/setting.json +40 -1
- package/next.config.mjs +2 -20
- package/package.json +4 -3
- package/src/app/api/openai/chat/route.ts +5 -41
- package/src/app/api/openai/{createAzureOpenai.ts → createBizOpenAI/createAzureOpenai.ts} +1 -1
- package/src/app/api/openai/{createOpenai.ts → createBizOpenAI/createOpenai.ts} +1 -1
- package/src/app/api/openai/createBizOpenAI/index.ts +46 -0
- package/src/app/api/openai/models/route.ts +1 -1
- package/src/app/api/openai/stt/route.ts +29 -0
- package/src/app/api/openai/tts/route.ts +17 -0
- package/src/app/api/tts/edge-speech/route.ts +9 -0
- package/src/app/api/tts/microsoft-speech/route.ts +9 -0
- package/src/app/chat/(desktop)/features/ChatHeader.tsx +3 -2
- package/src/app/chat/(desktop)/features/ChatInput/Footer.tsx +62 -0
- package/src/app/chat/(desktop)/features/ChatInput/index.tsx +0 -1
- package/src/app/chat/(desktop)/features/SessionHeader.tsx +2 -2
- package/src/app/chat/(desktop)/features/SideBar.tsx +7 -1
- package/src/app/chat/(mobile)/features/ChatHeader.tsx +29 -11
- package/src/app/chat/(mobile)/features/ChatInput/Mobile.tsx +20 -4
- package/src/app/chat/features/ChatHeader/ShareButton/index.tsx +23 -15
- package/src/app/chat/features/ChatInput/ActionBar/config.ts +16 -6
- package/src/app/chat/features/ChatInput/ActionBar/index.tsx +40 -16
- package/src/app/chat/features/ChatInput/STT/index.tsx +146 -0
- package/src/app/chat/features/ChatInput/Topic/index.tsx +1 -3
- package/src/app/chat/features/Conversation/ChatList/Actions/Assistant.tsx +2 -2
- package/src/app/chat/features/Conversation/ChatList/Actions/User.tsx +2 -2
- package/src/app/chat/features/Conversation/ChatList/Actions/customAction.ts +9 -2
- package/src/app/chat/features/Conversation/ChatList/Actions/index.ts +8 -1
- package/src/app/chat/features/Conversation/ChatList/Extras/Assistant.tsx +33 -27
- package/src/app/chat/features/Conversation/ChatList/Extras/AudioPlayer.tsx +139 -0
- package/src/app/chat/features/Conversation/ChatList/Extras/ExtraContainer.tsx +13 -0
- package/src/app/chat/features/Conversation/ChatList/Extras/TTS.tsx +122 -0
- package/src/app/chat/features/Conversation/ChatList/Extras/User.tsx +18 -11
- package/src/app/chat/settings/features/HeaderContent.tsx +6 -4
- package/src/app/chat/settings/features/SubmitAgentButton/index.tsx +2 -3
- package/src/app/settings/features/SideBar/List.tsx +2 -1
- package/src/app/settings/features/SideBar/index.tsx +1 -1
- package/src/app/settings/features/UpgradeAlert.tsx +1 -1
- package/src/app/settings/tts/TTS/index.tsx +73 -0
- package/src/app/settings/tts/TTS/options.ts +30 -0
- package/src/app/settings/tts/index.tsx +21 -0
- package/src/app/settings/tts/layout.tsx +1 -0
- package/src/app/settings/tts/page.tsx +3 -0
- package/src/components/HotKeys/index.tsx +4 -7
- package/src/const/layoutTokens.ts +3 -0
- package/src/const/settings.ts +22 -1
- package/src/features/AgentSetting/AgentTTS/SelectWithTTSPreview.tsx +119 -0
- package/src/features/AgentSetting/AgentTTS/index.tsx +115 -0
- package/src/features/AgentSetting/AgentTTS/options.ts +16 -0
- package/src/features/AgentSetting/index.tsx +2 -1
- package/src/hooks/useSTT.ts +61 -0
- package/src/hooks/useTTS.ts +87 -0
- package/src/locales/default/chat.ts +10 -1
- package/src/locales/default/error.ts +6 -0
- package/src/locales/default/setting.ts +35 -1
- package/src/services/_url.ts +7 -0
- package/src/store/global/initialState.ts +1 -0
- package/src/store/global/selectors/settings.test.ts +32 -0
- package/src/store/global/selectors/settings.ts +9 -1
- package/src/store/session/slices/agentConfig/selectors.ts +9 -0
- package/src/store/session/slices/chat/actions/translate.ts +24 -1
- package/src/store/session/slices/chat/selectors/utils.test.ts +16 -0
- package/src/store/session/slices/session/reducers/session.test.ts +24 -0
- package/src/types/chatMessage.ts +6 -0
- package/src/types/session.ts +17 -0
- package/src/types/settings.ts +13 -2
- 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
|
-
<
|
|
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
|
-
<
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
</div>
|
|
25
|
+
<ExtraContainer>
|
|
26
|
+
<Translate id={id} {...extra?.translate} loading={loading} />
|
|
27
|
+
</ExtraContainer>
|
|
21
28
|
)}
|
|
22
|
-
</
|
|
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 {
|
|
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
|
|
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 {
|
|
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={
|
|
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>
|
|
@@ -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';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { createStyles } from 'antd-style';
|
|
4
|
-
import {
|
|
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
|
-
<
|
|
60
|
-
<
|
|
61
|
-
|
|
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;
|
package/src/const/settings.ts
CHANGED
|
@@ -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
|
};
|