mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 08:50:15 +01:00
✨ feat: auto send text slider (#3312)
* feat: convert main component to float * feat: convert the remaining components * feat: use `recoilState` instead of `recoilValue` * feat: replaced `AutoSendTextSwitch` to `AutoSendTextSelector` * feat: use `autoSendText` in the `useSpeechToTextExternal` hook * fix: `autoSendText` timeout
This commit is contained in:
parent
d5d188eebf
commit
d5782ac66c
14 changed files with 737 additions and 749 deletions
|
|
@ -38,8 +38,8 @@ const ChatForm = ({ index = 0 }) => {
|
||||||
const submitButtonRef = useRef<HTMLButtonElement>(null);
|
const submitButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
const textAreaRef = useRef<HTMLTextAreaElement | null>(null);
|
const textAreaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||||
|
|
||||||
const SpeechToText = useRecoilValue(store.speechToText);
|
const SpeechToText = useRecoilState<boolean>(store.speechToText);
|
||||||
const TextToSpeech = useRecoilValue(store.textToSpeech);
|
const TextToSpeech = useRecoilState<boolean>(store.textToSpeech);
|
||||||
const automaticPlayback = useRecoilValue(store.automaticPlayback);
|
const automaticPlayback = useRecoilValue(store.automaticPlayback);
|
||||||
|
|
||||||
const [showStopButton, setShowStopButton] = useRecoilState(store.showStopButtonByIndex(index));
|
const [showStopButton, setShowStopButton] = useRecoilState(store.showStopButtonByIndex(index));
|
||||||
|
|
|
||||||
|
|
@ -10,15 +10,15 @@ export default function ConversationModeSwitch({
|
||||||
}) {
|
}) {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const [conversationMode, setConversationMode] = useRecoilState<boolean>(store.conversationMode);
|
const [conversationMode, setConversationMode] = useRecoilState<boolean>(store.conversationMode);
|
||||||
const [speechToText] = useRecoilState<boolean>(store.speechToText);
|
const speechToText = useRecoilState<boolean>(store.speechToText);
|
||||||
const [textToSpeech] = useRecoilState<boolean>(store.textToSpeech);
|
const textToSpeech = useRecoilState<boolean>(store.textToSpeech);
|
||||||
const [, setAutoSendText] = useRecoilState<boolean>(store.autoSendText);
|
const [, setAutoSendText] = useRecoilState(store.autoSendText);
|
||||||
const [, setDecibelValue] = useRecoilState(store.decibelValue);
|
const [, setDecibelValue] = useRecoilState(store.decibelValue);
|
||||||
const [, setAutoTranscribeAudio] = useRecoilState<boolean>(store.autoTranscribeAudio);
|
const [, setAutoTranscribeAudio] = useRecoilState<boolean>(store.autoTranscribeAudio);
|
||||||
|
|
||||||
const handleCheckedChange = (value: boolean) => {
|
const handleCheckedChange = (value: boolean) => {
|
||||||
setAutoTranscribeAudio(value);
|
setAutoTranscribeAudio(value);
|
||||||
setAutoSendText(value);
|
setAutoSendText(3);
|
||||||
setDecibelValue(-45);
|
setDecibelValue(-45);
|
||||||
setConversationMode(value);
|
setConversationMode(value);
|
||||||
if (onCheckedChange) {
|
if (onCheckedChange) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { useRecoilState } from 'recoil';
|
||||||
|
import { cn, defaultTextProps, optionText } from '~/utils/';
|
||||||
|
import { Slider, InputNumber } from '~/components/ui';
|
||||||
|
import { useLocalize } from '~/hooks';
|
||||||
|
import store from '~/store';
|
||||||
|
|
||||||
|
export default function AutoSendTextSelector() {
|
||||||
|
const localize = useLocalize();
|
||||||
|
|
||||||
|
const speechToText = useRecoilState<boolean>(store.speechToText);
|
||||||
|
const [autoSendText, setAutoSendText] = useRecoilState(store.autoSendText);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>{localize('com_nav_auto_send_text')}</div>
|
||||||
|
<div className="w-2" />
|
||||||
|
<small className="opacity-40">({localize('com_nav_auto_send_text_disabled')})</small>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Slider
|
||||||
|
value={[autoSendText ?? -1]}
|
||||||
|
onValueChange={(value) => setAutoSendText(value[0])}
|
||||||
|
doubleClickHandler={() => setAutoSendText(-1)}
|
||||||
|
min={-1}
|
||||||
|
max={60}
|
||||||
|
step={1}
|
||||||
|
className="ml-4 flex h-4 w-24"
|
||||||
|
disabled={!speechToText}
|
||||||
|
/>
|
||||||
|
<div className="w-2" />
|
||||||
|
<InputNumber
|
||||||
|
value={`${autoSendText} s`}
|
||||||
|
disabled={!speechToText}
|
||||||
|
onChange={(value) => setAutoSendText(value ? value[0] : 0)}
|
||||||
|
min={-1}
|
||||||
|
max={60}
|
||||||
|
className={cn(
|
||||||
|
defaultTextProps,
|
||||||
|
cn(
|
||||||
|
optionText,
|
||||||
|
'reset-rc-number-input reset-rc-number-input-text-right h-auto w-12 border-0 group-hover/temp:border-gray-200',
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
import { useRecoilState } from 'recoil';
|
|
||||||
import { Switch } from '~/components/ui';
|
|
||||||
import { useLocalize } from '~/hooks';
|
|
||||||
import store from '~/store';
|
|
||||||
|
|
||||||
export default function AutoSendTextSwitch({
|
|
||||||
onCheckedChange,
|
|
||||||
}: {
|
|
||||||
onCheckedChange?: (value: boolean) => void;
|
|
||||||
}) {
|
|
||||||
const localize = useLocalize();
|
|
||||||
const [autoSendText, setAutoSendText] = useRecoilState<boolean>(store.autoSendText);
|
|
||||||
const [SpeechToText] = useRecoilState<boolean>(store.speechToText);
|
|
||||||
|
|
||||||
const handleCheckedChange = (value: boolean) => {
|
|
||||||
setAutoSendText(value);
|
|
||||||
if (onCheckedChange) {
|
|
||||||
onCheckedChange(value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>{localize('com_nav_auto_send_text')}</div>
|
|
||||||
<Switch
|
|
||||||
id="AutoSendText"
|
|
||||||
checked={autoSendText}
|
|
||||||
onCheckedChange={handleCheckedChange}
|
|
||||||
className="ml-4"
|
|
||||||
data-testid="AutoSendText"
|
|
||||||
disabled={!SpeechToText}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -12,7 +12,7 @@ export default function AutoTranscribeAudioSwitch({
|
||||||
const [autoTranscribeAudio, setAutoTranscribeAudio] = useRecoilState<boolean>(
|
const [autoTranscribeAudio, setAutoTranscribeAudio] = useRecoilState<boolean>(
|
||||||
store.autoTranscribeAudio,
|
store.autoTranscribeAudio,
|
||||||
);
|
);
|
||||||
const [speechToText] = useRecoilState<boolean>(store.speechToText);
|
const speechToText = useRecoilState<boolean>(store.speechToText);
|
||||||
|
|
||||||
const handleCheckedChange = (value: boolean) => {
|
const handleCheckedChange = (value: boolean) => {
|
||||||
setAutoTranscribeAudio(value);
|
setAutoTranscribeAudio(value);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
import { Slider, InputNumber } from '~/components/ui';
|
import { Slider, InputNumber } from '~/components/ui';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
@ -7,7 +7,7 @@ import { cn, defaultTextProps, optionText } from '~/utils/';
|
||||||
|
|
||||||
export default function DecibelSelector() {
|
export default function DecibelSelector() {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const speechToText = useRecoilValue(store.speechToText);
|
const speechToText = useRecoilState<boolean>(store.speechToText);
|
||||||
const [decibelValue, setDecibelValue] = useRecoilState(store.decibelValue);
|
const [decibelValue, setDecibelValue] = useRecoilState(store.decibelValue);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import '@testing-library/jest-dom/extend-expect';
|
|
||||||
import { render, fireEvent } from 'test/layout-test-utils';
|
|
||||||
import AutoSendTextSwitch from '../AutoSendTextSwitch';
|
|
||||||
import { RecoilRoot } from 'recoil';
|
|
||||||
|
|
||||||
describe('AutoSendTextSwitch', () => {
|
|
||||||
/**
|
|
||||||
* Mock function to set the auto-send-text state.
|
|
||||||
*/
|
|
||||||
let mockSetAutoSendText: jest.Mock<void, [boolean]> | ((value: boolean) => void) | undefined;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
mockSetAutoSendText = jest.fn();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders correctly', () => {
|
|
||||||
const { getByTestId } = render(
|
|
||||||
<RecoilRoot>
|
|
||||||
<AutoSendTextSwitch />
|
|
||||||
</RecoilRoot>,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(getByTestId('AutoSendText')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('calls onCheckedChange when the switch is toggled', () => {
|
|
||||||
const { getByTestId } = render(
|
|
||||||
<RecoilRoot>
|
|
||||||
<AutoSendTextSwitch onCheckedChange={mockSetAutoSendText} />
|
|
||||||
</RecoilRoot>,
|
|
||||||
);
|
|
||||||
const switchElement = getByTestId('AutoSendText');
|
|
||||||
fireEvent.click(switchElement);
|
|
||||||
|
|
||||||
expect(mockSetAutoSendText).toHaveBeenCalledWith(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
export { default as AutoSendTextSwitch } from './AutoSendTextSwitch';
|
export { default as AutoSendTextSelector } from './AutoSendTextSelector';
|
||||||
export { default as SpeechToTextSwitch } from './SpeechToTextSwitch';
|
export { default as SpeechToTextSwitch } from './SpeechToTextSwitch';
|
||||||
export { default as EngineSTTDropdown } from './EngineSTTDropdown';
|
export { default as EngineSTTDropdown } from './EngineSTTDropdown';
|
||||||
export { default as DecibelSelector } from './DecibelSelector';
|
export { default as DecibelSelector } from './DecibelSelector';
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import {
|
||||||
AutoTranscribeAudioSwitch,
|
AutoTranscribeAudioSwitch,
|
||||||
LanguageSTTDropdown,
|
LanguageSTTDropdown,
|
||||||
SpeechToTextSwitch,
|
SpeechToTextSwitch,
|
||||||
AutoSendTextSwitch,
|
AutoSendTextSelector,
|
||||||
EngineSTTDropdown,
|
EngineSTTDropdown,
|
||||||
DecibelSelector,
|
DecibelSelector,
|
||||||
} from './STT';
|
} from './STT';
|
||||||
|
|
@ -220,8 +220,8 @@ function Speech() {
|
||||||
<DecibelSelector />
|
<DecibelSelector />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="border-b last-of-type:border-b-0 dark:border-gray-700">
|
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
|
||||||
<AutoSendTextSwitch />
|
<AutoSendTextSelector />
|
||||||
</div>
|
</div>
|
||||||
<div className="h-px bg-black/20 bg-white/20" role="none" />
|
<div className="h-px bg-black/20 bg-white/20" role="none" />
|
||||||
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
|
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-700">
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ const useSpeechToTextExternal = (onTranscriptionComplete: (text: string) => void
|
||||||
const { externalSpeechToText } = useGetAudioSettings();
|
const { externalSpeechToText } = useGetAudioSettings();
|
||||||
const [speechToText] = useRecoilState<boolean>(store.speechToText);
|
const [speechToText] = useRecoilState<boolean>(store.speechToText);
|
||||||
const [autoTranscribeAudio] = useRecoilState<boolean>(store.autoTranscribeAudio);
|
const [autoTranscribeAudio] = useRecoilState<boolean>(store.autoTranscribeAudio);
|
||||||
const [autoSendText] = useRecoilState<boolean>(store.autoSendText);
|
const [autoSendText] = useRecoilState(store.autoSendText);
|
||||||
const [text, setText] = useState<string>('');
|
const [text, setText] = useState<string>('');
|
||||||
const [isListening, setIsListening] = useState(false);
|
const [isListening, setIsListening] = useState(false);
|
||||||
const [permission, setPermission] = useState(false);
|
const [permission, setPermission] = useState(false);
|
||||||
|
|
@ -27,10 +27,11 @@ const useSpeechToTextExternal = (onTranscriptionComplete: (text: string) => void
|
||||||
const extractedText = data.text;
|
const extractedText = data.text;
|
||||||
setText(extractedText);
|
setText(extractedText);
|
||||||
setIsRequestBeingMade(false);
|
setIsRequestBeingMade(false);
|
||||||
if (autoSendText && speechToText && extractedText.length > 0) {
|
|
||||||
|
if (autoSendText > -1 && speechToText && extractedText.length > 0) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
onTranscriptionComplete(extractedText);
|
onTranscriptionComplete(extractedText);
|
||||||
}, 3000);
|
}, autoSendText * 1000);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
|
|
|
||||||
|
|
@ -628,7 +628,8 @@ export default {
|
||||||
com_nav_delete_warning: 'WARNING: This will permanently delete your account.',
|
com_nav_delete_warning: 'WARNING: This will permanently delete your account.',
|
||||||
com_nav_delete_data_info: 'All your data will be deleted.',
|
com_nav_delete_data_info: 'All your data will be deleted.',
|
||||||
com_nav_conversation_mode: 'Conversation Mode',
|
com_nav_conversation_mode: 'Conversation Mode',
|
||||||
com_nav_auto_send_text: 'Auto send text (after 3 sec)',
|
com_nav_auto_send_text: 'Auto send text',
|
||||||
|
com_nav_auto_send_text_disabled: 'set -1 to disable',
|
||||||
com_nav_auto_transcribe_audio: 'Auto transcribe audio',
|
com_nav_auto_transcribe_audio: 'Auto transcribe audio',
|
||||||
com_nav_db_sensitivity: 'Decibel sensitivity',
|
com_nav_db_sensitivity: 'Decibel sensitivity',
|
||||||
com_nav_playback_rate: 'Audio Playback Rate',
|
com_nav_playback_rate: 'Audio Playback Rate',
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -45,7 +45,7 @@ const localStorageAtoms = {
|
||||||
languageSTT: atomWithLocalStorage('languageSTT', ''),
|
languageSTT: atomWithLocalStorage('languageSTT', ''),
|
||||||
autoTranscribeAudio: atomWithLocalStorage('autoTranscribeAudio', false),
|
autoTranscribeAudio: atomWithLocalStorage('autoTranscribeAudio', false),
|
||||||
decibelValue: atomWithLocalStorage('decibelValue', -45),
|
decibelValue: atomWithLocalStorage('decibelValue', -45),
|
||||||
autoSendText: atomWithLocalStorage('autoSendText', false),
|
autoSendText: atomWithLocalStorage('autoSendText', -1),
|
||||||
|
|
||||||
textToSpeech: atomWithLocalStorage('textToSpeech', true),
|
textToSpeech: atomWithLocalStorage('textToSpeech', true),
|
||||||
engineTTS: atomWithLocalStorage('engineTTS', 'browser'),
|
engineTTS: atomWithLocalStorage('engineTTS', 'browser'),
|
||||||
|
|
|
||||||
|
|
@ -304,7 +304,7 @@ const speechTab = z
|
||||||
languageSTT: z.string().optional(),
|
languageSTT: z.string().optional(),
|
||||||
autoTranscribeAudio: z.boolean().optional(),
|
autoTranscribeAudio: z.boolean().optional(),
|
||||||
decibelValue: z.number().optional(),
|
decibelValue: z.number().optional(),
|
||||||
autoSendText: z.boolean().optional(),
|
autoSendText: z.number().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue