mirror of https://github.com/langgenius/dify.git
254 lines
10 KiB
TypeScript
254 lines
10 KiB
TypeScript
'use client'
|
|
import type { OnFeaturesChange } from '@/app/components/base/features/types'
|
|
import type { Item } from '@/app/components/base/select'
|
|
import { Listbox, ListboxButton, ListboxOption, ListboxOptions, Transition } from '@headlessui/react'
|
|
import { produce } from 'immer'
|
|
import * as React from 'react'
|
|
import { Fragment } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { replace } from 'string-ts'
|
|
import AudioBtn from '@/app/components/base/audio-btn'
|
|
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
|
|
import Switch from '@/app/components/base/switch'
|
|
import Tooltip from '@/app/components/base/tooltip'
|
|
import { languages } from '@/i18n-config/language'
|
|
import { usePathname } from '@/next/navigation'
|
|
import { useAppVoices } from '@/service/use-apps'
|
|
import { TtsAutoPlay } from '@/types/app'
|
|
import { cn } from '@/utils/classnames'
|
|
|
|
type VoiceParamConfigProps = {
|
|
onClose: () => void
|
|
onChange?: OnFeaturesChange
|
|
}
|
|
const VoiceParamConfig = ({
|
|
onClose,
|
|
onChange,
|
|
}: VoiceParamConfigProps) => {
|
|
const { t } = useTranslation()
|
|
const pathname = usePathname()
|
|
const matched = /\/app\/([^/]+)/.exec(pathname)
|
|
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('placeholder.select', { ns: 'common' })
|
|
|
|
const language = languageItem?.value
|
|
const { data: voiceItems } = useAppVoices(appId, language)
|
|
let voiceItem = voiceItems?.find(item => item.value === text2speech?.voice)
|
|
if (voiceItems && !voiceItem)
|
|
voiceItem = voiceItems[0]
|
|
const localVoicePlaceholder = voiceItem?.name || t('placeholder.select', { ns: 'common' })
|
|
|
|
const handleChange = (value: Record<string, string>) => {
|
|
const {
|
|
features,
|
|
setFeatures,
|
|
} = featuresStore!.getState()
|
|
|
|
const newFeatures = produce(features, (draft) => {
|
|
draft.text2speech = {
|
|
...draft.text2speech,
|
|
...value,
|
|
}
|
|
})
|
|
|
|
setFeatures(newFeatures)
|
|
if (onChange)
|
|
onChange()
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<div className="mb-4 flex items-center justify-between">
|
|
<div className="text-text-primary system-xl-semibold">{t('voice.voiceSettings.title', { ns: 'appDebug' })}</div>
|
|
<div
|
|
className="cursor-pointer p-1"
|
|
role="button"
|
|
tabIndex={0}
|
|
aria-label={t('appDebug:voice.voiceSettings.close')}
|
|
onClick={onClose}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
e.preventDefault()
|
|
onClose()
|
|
}
|
|
}}
|
|
>
|
|
<span className="i-ri-close-line h-4 w-4 text-text-tertiary" />
|
|
</div>
|
|
</div>
|
|
<div className="mb-3">
|
|
<div className="mb-1 flex items-center py-1 text-text-secondary system-sm-semibold">
|
|
{t('voice.voiceSettings.language', { ns: 'appDebug' })}
|
|
<Tooltip
|
|
popupContent={(
|
|
<div className="w-[180px]">
|
|
{t('voice.voiceSettings.resolutionTooltip', { ns: 'appDebug' }).split('\n').map(item => (
|
|
<div key={item}>
|
|
{item}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
/>
|
|
</div>
|
|
<Listbox
|
|
value={languageItem}
|
|
onChange={(value: Item) => {
|
|
handleChange({
|
|
language: String(value.value),
|
|
})
|
|
}}
|
|
>
|
|
<div className="relative h-8">
|
|
<ListboxButton
|
|
className="h-full w-full cursor-pointer rounded-lg border-0 bg-components-input-bg-normal py-1.5 pl-3 pr-10 focus-visible:bg-state-base-hover focus-visible:outline-none group-hover:bg-state-base-hover sm:text-sm sm:leading-6"
|
|
>
|
|
<span className={cn('block truncate text-left text-text-secondary', !languageItem?.name && 'text-text-tertiary')}>
|
|
{languageItem?.name
|
|
? t(`voice.language.${replace(languageItem?.value ?? '', '-', '')}`, languageItem?.name, { ns: 'common' as const })
|
|
: localLanguagePlaceholder}
|
|
</span>
|
|
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
|
<span className="i-heroicons-chevron-down-20-solid h-4 w-4 text-text-tertiary" aria-hidden="true" />
|
|
</span>
|
|
</ListboxButton>
|
|
<Transition
|
|
as={Fragment}
|
|
leave="transition ease-in duration-100"
|
|
leaveFrom="opacity-100"
|
|
leaveTo="opacity-0"
|
|
>
|
|
|
|
<ListboxOptions
|
|
className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md border-[0.5px] border-components-panel-border bg-components-panel-bg px-1 py-1 text-base shadow-lg focus:outline-none sm:text-sm"
|
|
>
|
|
{languages.map(item => (
|
|
<ListboxOption
|
|
key={item.value}
|
|
className="relative cursor-pointer select-none rounded-lg py-2 pl-3 pr-9 text-text-secondary hover:bg-state-base-hover data-[active]:bg-state-base-active"
|
|
value={item}
|
|
disabled={false}
|
|
>
|
|
{({ /* active, */ selected }) => (
|
|
<>
|
|
<span
|
|
className={cn('block', selected && 'font-normal')}
|
|
>
|
|
{t(`voice.language.${replace((item.value), '-', '')}`, item.name, { ns: 'common' as const })}
|
|
</span>
|
|
{(selected || item.value === text2speech?.language) && (
|
|
<span
|
|
className={cn('absolute inset-y-0 right-0 flex items-center pr-4 text-text-secondary')}
|
|
>
|
|
<span className="i-heroicons-check-20-solid h-4 w-4" aria-hidden="true" />
|
|
</span>
|
|
)}
|
|
</>
|
|
)}
|
|
</ListboxOption>
|
|
))}
|
|
</ListboxOptions>
|
|
</Transition>
|
|
</div>
|
|
</Listbox>
|
|
</div>
|
|
<div className="mb-3">
|
|
<div className="mb-1 py-1 text-text-secondary system-sm-semibold">
|
|
{t('voice.voiceSettings.voice', { ns: 'appDebug' })}
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<Listbox
|
|
value={voiceItem}
|
|
disabled={!languageItem}
|
|
onChange={(value: Item) => {
|
|
handleChange({
|
|
voice: String(value.value),
|
|
})
|
|
}}
|
|
>
|
|
<div className="relative h-8 grow">
|
|
<ListboxButton
|
|
className="h-full w-full cursor-pointer rounded-lg border-0 bg-components-input-bg-normal py-1.5 pl-3 pr-10 focus-visible:bg-state-base-hover focus-visible:outline-none group-hover:bg-state-base-hover sm:text-sm sm:leading-6"
|
|
>
|
|
<span
|
|
className={cn('block truncate text-left text-text-secondary', !voiceItem?.name && 'text-text-tertiary')}
|
|
>
|
|
{voiceItem?.name ?? localVoicePlaceholder}
|
|
</span>
|
|
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
|
<span className="i-heroicons-chevron-down-20-solid h-4 w-4 text-text-tertiary" aria-hidden="true" />
|
|
</span>
|
|
</ListboxButton>
|
|
<Transition
|
|
as={Fragment}
|
|
leave="transition ease-in duration-100"
|
|
leaveFrom="opacity-100"
|
|
leaveTo="opacity-0"
|
|
>
|
|
|
|
<ListboxOptions
|
|
className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md border-[0.5px] border-components-panel-border bg-components-panel-bg px-1 py-1 text-base shadow-lg focus:outline-none sm:text-sm"
|
|
>
|
|
{voiceItems?.map((item: Item) => (
|
|
<ListboxOption
|
|
key={item.value}
|
|
className="relative cursor-pointer select-none rounded-lg py-2 pl-3 pr-9 text-text-secondary hover:bg-state-base-hover data-[active]:bg-state-base-active"
|
|
value={item}
|
|
disabled={false}
|
|
>
|
|
{({ /* active, */ selected }) => (
|
|
<>
|
|
<span className={cn('block', selected && 'font-normal')}>{item.name}</span>
|
|
{(selected || item.value === text2speech?.voice) && (
|
|
<span
|
|
className={cn('absolute inset-y-0 right-0 flex items-center pr-4 text-text-secondary')}
|
|
>
|
|
<span className="i-heroicons-check-20-solid h-4 w-4" aria-hidden="true" />
|
|
</span>
|
|
)}
|
|
</>
|
|
)}
|
|
</ListboxOption>
|
|
))}
|
|
</ListboxOptions>
|
|
</Transition>
|
|
</div>
|
|
</Listbox>
|
|
{languageItem?.example && (
|
|
<div className="h-8 shrink-0 rounded-lg bg-components-button-tertiary-bg p-1" data-testid="audition-button">
|
|
<AudioBtn
|
|
value={languageItem?.example}
|
|
isAudition
|
|
voice={text2speech?.voice}
|
|
noCache
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div className="mb-1 py-1 text-text-secondary system-sm-semibold">
|
|
{t('voice.voiceSettings.autoPlay', { ns: 'appDebug' })}
|
|
</div>
|
|
<Switch
|
|
className="shrink-0"
|
|
value={text2speech?.autoPlay === TtsAutoPlay.enabled}
|
|
onChange={(value: boolean) => {
|
|
handleChange({
|
|
autoPlay: value ? TtsAutoPlay.enabled : TtsAutoPlay.disabled,
|
|
})
|
|
}}
|
|
/>
|
|
</div>
|
|
</>
|
|
)
|
|
}
|
|
|
|
export default React.memo(VoiceParamConfig)
|