param-config-content.tsx 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. 'use client'
  2. import useSWR from 'swr'
  3. import produce from 'immer'
  4. import React, { Fragment } from 'react'
  5. import { usePathname } from 'next/navigation'
  6. import { useTranslation } from 'react-i18next'
  7. import { RiCloseLine } from '@remixicon/react'
  8. import { Listbox, ListboxButton, ListboxOption, ListboxOptions, Transition } from '@headlessui/react'
  9. import { CheckIcon, ChevronDownIcon } from '@heroicons/react/20/solid'
  10. import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
  11. import type { Item } from '@/app/components/base/select'
  12. import { fetchAppVoices } from '@/service/apps'
  13. import Tooltip from '@/app/components/base/tooltip'
  14. import Switch from '@/app/components/base/switch'
  15. import AudioBtn from '@/app/components/base/audio-btn'
  16. import { languages } from '@/i18n/language'
  17. import { TtsAutoPlay } from '@/types/app'
  18. import type { OnFeaturesChange } from '@/app/components/base/features/types'
  19. import classNames from '@/utils/classnames'
  20. type VoiceParamConfigProps = {
  21. onClose: () => void
  22. onChange?: OnFeaturesChange
  23. }
  24. const VoiceParamConfig = ({
  25. onClose,
  26. onChange,
  27. }: VoiceParamConfigProps) => {
  28. const { t } = useTranslation()
  29. const pathname = usePathname()
  30. const matched = pathname.match(/\/app\/([^/]+)/)
  31. const appId = (matched?.length && matched[1]) ? matched[1] : ''
  32. const text2speech = useFeatures(state => state.features.text2speech)
  33. const featuresStore = useFeaturesStore()
  34. let languageItem = languages.find(item => item.value === text2speech?.language)
  35. if (languages && !languageItem)
  36. languageItem = languages[0]
  37. const localLanguagePlaceholder = languageItem?.name || t('common.placeholder.select')
  38. const language = languageItem?.value
  39. const voiceItems = useSWR({ appId, language }, fetchAppVoices).data
  40. let voiceItem = voiceItems?.find(item => item.value === text2speech?.voice)
  41. if (voiceItems && !voiceItem)
  42. voiceItem = voiceItems[0]
  43. const localVoicePlaceholder = voiceItem?.name || t('common.placeholder.select')
  44. const handleChange = (value: Record<string, string>) => {
  45. const {
  46. features,
  47. setFeatures,
  48. } = featuresStore!.getState()
  49. const newFeatures = produce(features, (draft) => {
  50. draft.text2speech = {
  51. ...draft.text2speech,
  52. ...value,
  53. }
  54. })
  55. setFeatures(newFeatures)
  56. if (onChange)
  57. onChange()
  58. }
  59. return (
  60. <>
  61. <div className='mb-4 flex items-center justify-between'>
  62. <div className='system-xl-semibold text-text-primary'>{t('appDebug.voice.voiceSettings.title')}</div>
  63. <div className='cursor-pointer p-1' onClick={onClose}><RiCloseLine className='h-4 w-4 text-text-tertiary' /></div>
  64. </div>
  65. <div className='mb-3'>
  66. <div className='system-sm-semibold mb-1 flex items-center py-1 text-text-secondary'>
  67. {t('appDebug.voice.voiceSettings.language')}
  68. <Tooltip
  69. popupContent={
  70. <div className='w-[180px]'>
  71. {t('appDebug.voice.voiceSettings.resolutionTooltip').split('\n').map(item => (
  72. <div key={item}>{item}
  73. </div>
  74. ))}
  75. </div>
  76. }
  77. />
  78. </div>
  79. <Listbox
  80. value={languageItem}
  81. onChange={(value: Item) => {
  82. handleChange({
  83. language: String(value.value),
  84. })
  85. }}
  86. >
  87. <div className='relative h-8'>
  88. <ListboxButton
  89. 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'}>
  90. <span className={classNames('block truncate text-left text-text-secondary', !languageItem?.name && 'text-text-tertiary')}>
  91. {languageItem?.name ? t(`common.voice.language.${languageItem?.value.replace('-', '')}`) : localLanguagePlaceholder}
  92. </span>
  93. <span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
  94. <ChevronDownIcon
  95. className="h-4 w-4 text-text-tertiary"
  96. aria-hidden="true"
  97. />
  98. </span>
  99. </ListboxButton>
  100. <Transition
  101. as={Fragment}
  102. leave="transition ease-in duration-100"
  103. leaveFrom="opacity-100"
  104. leaveTo="opacity-0"
  105. >
  106. <ListboxOptions
  107. 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">
  108. {languages.map((item: Item) => (
  109. <ListboxOption
  110. key={item.value}
  111. className={
  112. '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'
  113. }
  114. value={item}
  115. disabled={false}
  116. >
  117. {({ /* active, */ selected }) => (
  118. <>
  119. <span
  120. className={classNames('block', selected && 'font-normal')}>{t(`common.voice.language.${(item.value).toString().replace('-', '')}`)}</span>
  121. {(selected || item.value === text2speech?.language) && (
  122. <span
  123. className={classNames(
  124. 'absolute inset-y-0 right-0 flex items-center pr-4 text-text-secondary',
  125. )}
  126. >
  127. <CheckIcon className="h-4 w-4" aria-hidden="true" />
  128. </span>
  129. )}
  130. </>
  131. )}
  132. </ListboxOption>
  133. ))}
  134. </ListboxOptions>
  135. </Transition>
  136. </div>
  137. </Listbox>
  138. </div>
  139. <div className='mb-3'>
  140. <div className='system-sm-semibold mb-1 py-1 text-text-secondary'>
  141. {t('appDebug.voice.voiceSettings.voice')}
  142. </div>
  143. <div className='flex items-center gap-1'>
  144. <Listbox
  145. value={voiceItem ?? {}}
  146. disabled={!languageItem}
  147. onChange={(value: Item) => {
  148. handleChange({
  149. voice: String(value.value),
  150. })
  151. }}
  152. >
  153. <div className={'relative h-8 grow'}>
  154. <ListboxButton
  155. 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'}>
  156. <span
  157. className={classNames('block truncate text-left text-text-secondary', !voiceItem?.name && 'text-text-tertiary')}>{voiceItem?.name ?? localVoicePlaceholder}</span>
  158. <span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
  159. <ChevronDownIcon
  160. className="h-4 w-4 text-text-tertiary"
  161. aria-hidden="true"
  162. />
  163. </span>
  164. </ListboxButton>
  165. <Transition
  166. as={Fragment}
  167. leave="transition ease-in duration-100"
  168. leaveFrom="opacity-100"
  169. leaveTo="opacity-0"
  170. >
  171. <ListboxOptions
  172. 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">
  173. {voiceItems?.map((item: Item) => (
  174. <ListboxOption
  175. key={item.value}
  176. className={
  177. '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'
  178. }
  179. value={item}
  180. disabled={false}
  181. >
  182. {({ /* active, */ selected }) => (
  183. <>
  184. <span className={classNames('block', selected && 'font-normal')}>{item.name}</span>
  185. {(selected || item.value === text2speech?.voice) && (
  186. <span
  187. className={classNames(
  188. 'absolute inset-y-0 right-0 flex items-center pr-4 text-text-secondary',
  189. )}
  190. >
  191. <CheckIcon className="h-4 w-4" aria-hidden="true" />
  192. </span>
  193. )}
  194. </>
  195. )}
  196. </ListboxOption>
  197. ))}
  198. </ListboxOptions>
  199. </Transition>
  200. </div>
  201. </Listbox>
  202. {languageItem?.example && (
  203. <div className='h-8 shrink-0 rounded-lg bg-components-button-tertiary-bg p-1'>
  204. <AudioBtn
  205. value={languageItem?.example}
  206. isAudition
  207. voice={text2speech?.voice}
  208. noCache
  209. />
  210. </div>
  211. )}
  212. </div>
  213. </div>
  214. <div>
  215. <div className='system-sm-semibold mb-1 py-1 text-text-secondary'>
  216. {t('appDebug.voice.voiceSettings.autoPlay')}
  217. </div>
  218. <Switch className='shrink-0'
  219. defaultValue={text2speech?.autoPlay === TtsAutoPlay.enabled}
  220. onChange={(value: boolean) => {
  221. handleChange({
  222. autoPlay: value ? TtsAutoPlay.enabled : TtsAutoPlay.disabled,
  223. })
  224. }}
  225. />
  226. </div>
  227. </>
  228. )
  229. }
  230. export default React.memo(VoiceParamConfig)