diff --git a/web/app/components/base/features/new-feature-panel/text-to-speech.tsx b/web/app/components/base/features/new-feature-panel/text-to-speech/index.tsx similarity index 87% rename from web/app/components/base/features/new-feature-panel/text-to-speech.tsx rename to web/app/components/base/features/new-feature-panel/text-to-speech/index.tsx index 5924bb769e..9f101a3ab0 100644 --- a/web/app/components/base/features/new-feature-panel/text-to-speech.tsx +++ b/web/app/components/base/features/new-feature-panel/text-to-speech/index.tsx @@ -5,6 +5,7 @@ import { RiEqualizer2Line } from '@remixicon/react' import { TextToAudio } from '@/app/components/base/icons/src/vender/features' import FeatureCard from '@/app/components/base/features/new-feature-panel/feature-card' import Button from '@/app/components/base/button' +import VoiceSettings from '@/app/components/base/features/new-feature-panel/text-to-speech/voice-settings' import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks' import type { OnFeaturesChange } from '@/app/components/base/features/types' import { FeatureEnum } from '@/app/components/base/features/types' @@ -21,6 +22,7 @@ const TextToSpeech = ({ const { t } = useTranslation() const textToSpeech = useFeatures(s => s.features.text2speech) // .language .voice .autoPlay const languageInfo = languages.find(i => i.value === textToSpeech?.language) + const [modalOpen, setModalOpen] = useState(false) const [isHovering, setIsHovering] = useState(false) const features = useFeatures(s => s.features) const featuresStore = useFeaturesStore() @@ -61,7 +63,7 @@ const TextToSpeech = ({ )} {!!features.text2speech?.enabled && ( <> - {!isHovering && ( + {!isHovering && !modalOpen && (
{t('appDebug.voice.voiceSettings.language')}
@@ -79,11 +81,13 @@ const TextToSpeech = ({
)} - {isHovering && ( - + {(isHovering || modalOpen) && ( + + + )} )} diff --git a/web/app/components/base/features/new-feature-panel/text-to-speech/param-config-content.tsx b/web/app/components/base/features/new-feature-panel/text-to-speech/param-config-content.tsx new file mode 100644 index 0000000000..bd279b5cf3 --- /dev/null +++ b/web/app/components/base/features/new-feature-panel/text-to-speech/param-config-content.tsx @@ -0,0 +1,242 @@ +'use client' +import useSWR from 'swr' +import produce from 'immer' +import React, { Fragment } from 'react' +import { usePathname } from 'next/navigation' +import { useTranslation } from 'react-i18next' +import { RiCloseLine } from '@remixicon/react' +import { Listbox, Transition } from '@headlessui/react' +import { CheckIcon, ChevronDownIcon } from '@heroicons/react/20/solid' +import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks' +import type { Item } from '@/app/components/base/select' +import { fetchAppVoices } from '@/service/apps' +import Tooltip from '@/app/components/base/tooltip' +import Switch from '@/app/components/base/switch' +import AudioBtn from '@/app/components/base/audio-btn' +import { languages } from '@/i18n/language' +import { TtsAutoPlay } from '@/types/app' +import type { OnFeaturesChange } from '@/app/components/base/features/types' +import classNames from '@/utils/classnames' + +type VoiceParamConfigProps = { + onClose: () => void + onChange?: OnFeaturesChange +} +const VoiceParamConfig = ({ + onClose, + onChange, +}: VoiceParamConfigProps) => { + const { t } = useTranslation() + const pathname = usePathname() + const matched = pathname.match(/\/app\/([^/]+)/) + const appId = (matched?.length && matched[1]) ? matched[1] : '' + const text2speech = useFeatures(state => state.features.text2speech) + const featuresStore = useFeaturesStore() + + let languageItem = languages.find(item => item.value === text2speech?.language) + if (languages && !languageItem) + languageItem = languages[0] + const localLanguagePlaceholder = languageItem?.name || t('common.placeholder.select') + + const language = languageItem?.value + const voiceItems = useSWR({ appId, language }, fetchAppVoices).data + let voiceItem = voiceItems?.find(item => item.value === text2speech?.voice) + if (voiceItems && !voiceItem) + voiceItem = voiceItems[0] + const localVoicePlaceholder = voiceItem?.name || t('common.placeholder.select') + + const handleChange = (value: Record) => { + const { + features, + setFeatures, + } = featuresStore!.getState() + + const newFeatures = produce(features, (draft) => { + draft.text2speech = { + ...draft.text2speech, + ...value, + } + }) + + setFeatures(newFeatures) + if (onChange) + onChange(newFeatures) + } + + return ( + <> +
+
{t('appDebug.voice.voiceSettings.title')}
+
+
+
+
+ {t('appDebug.voice.voiceSettings.language')} + + {t('appDebug.voice.voiceSettings.resolutionTooltip').split('\n').map(item => ( +
{item} +
+ ))} +
+ } + /> +
+ { + handleChange({ + language: String(value.value), + }) + }} + > +
+ + + {languageItem?.name ? t(`common.voice.language.${languageItem?.value.replace('-', '')}`) : localLanguagePlaceholder} + + + + + + + + {languages.map((item: Item) => ( + + `relative cursor-pointer select-none py-2 pl-3 pr-9 rounded-lg hover:bg-gray-100 text-gray-700 ${active ? 'bg-gray-100' : '' + }` + } + value={item} + disabled={false} + > + {({ /* active, */ selected }) => ( + <> + {t(`common.voice.language.${(item.value).toString().replace('-', '')}`)} + {(selected || item.value === text2speech?.language) && ( + + + )} + + )} + + ))} + + +
+
+ +
+
+ {t('appDebug.voice.voiceSettings.voice')} +
+
+ { + handleChange({ + voice: String(value.value), + }) + }} + > +
+ + {voiceItem?.name ?? localVoicePlaceholder} + + + + + + + {voiceItems?.map((item: Item) => ( + + `relative cursor-pointer select-none py-2 pl-3 pr-9 rounded-lg hover:bg-gray-100 text-gray-700 ${active ? 'bg-gray-100' : '' + }` + } + value={item} + disabled={false} + > + {({ /* active, */ selected }) => ( + <> + {item.name} + {(selected || item.value === text2speech?.voice) && ( + + + )} + + )} + + ))} + + +
+
+ {languageItem?.example && ( +
+ +
+ )} +
+
+
+
+ {t('appDebug.voice.voiceSettings.autoPlay')} +
+ { + handleChange({ + autoPlay: value ? TtsAutoPlay.enabled : TtsAutoPlay.disabled, + }) + }} + /> +
+ + ) +} + +export default React.memo(VoiceParamConfig) diff --git a/web/app/components/base/features/new-feature-panel/text-to-speech/voice-settings.tsx b/web/app/components/base/features/new-feature-panel/text-to-speech/voice-settings.tsx new file mode 100644 index 0000000000..076f06e6e7 --- /dev/null +++ b/web/app/components/base/features/new-feature-panel/text-to-speech/voice-settings.tsx @@ -0,0 +1,47 @@ +'use client' +import { memo } from 'react' +import { + PortalToFollowElem, + PortalToFollowElemContent, + PortalToFollowElemTrigger, +} from '@/app/components/base/portal-to-follow-elem' +import ParamConfigContent from '@/app/components/base/features/new-feature-panel/text-to-speech/param-config-content' +import type { OnFeaturesChange } from '@/app/components/base/features/types' + +type VoiceSettingsProps = { + open: boolean + onOpen: (state: any) => void + onChange?: OnFeaturesChange + disabled?: boolean + children?: React.ReactNode + placementLeft?: boolean +} +const VoiceSettings = ({ + open, + onOpen, + onChange, + disabled, + children, + placementLeft = true, +}: VoiceSettingsProps) => { + return ( + + !disabled && onOpen((open: boolean) => !open)}> + {children} + + +
+ onOpen(false)} onChange={onChange} /> +
+
+
+ ) +} +export default memo(VoiceSettings) diff --git a/web/app/components/workflow/features.tsx b/web/app/components/workflow/features.tsx index 80421dfba1..4ce6c045b0 100644 --- a/web/app/components/workflow/features.tsx +++ b/web/app/components/workflow/features.tsx @@ -28,39 +28,6 @@ const Features = () => { onChange={handleFeaturesChange} onClose={() => setShowFeaturesPanel(false)} /> - //
- //
- // {t('workflow.common.features')} - //
- // { - // isChatMode && ( - // <> - // - //
- // - // ) - // } - //
setShowFeaturesPanel(false)} - // > - // - //
- //
- //
- //
- // {}, - // }} - // /> - //
- //
) }