param-config-content.tsx 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. 'use client'
  2. import useSWR from 'swr'
  3. import produce from 'immer'
  4. import React, { Fragment } from 'react'
  5. import classNames from 'classnames'
  6. import {
  7. RiQuestionLine,
  8. } from '@remixicon/react'
  9. import { usePathname } from 'next/navigation'
  10. import { useTranslation } from 'react-i18next'
  11. import { Listbox, Transition } from '@headlessui/react'
  12. import { CheckIcon, ChevronDownIcon } from '@heroicons/react/20/solid'
  13. import {
  14. useFeatures,
  15. useFeaturesStore,
  16. } from '../../hooks'
  17. import type { OnFeaturesChange } from '../../types'
  18. import type { Item } from '@/app/components/base/select'
  19. import { fetchAppVoices } from '@/service/apps'
  20. import Tooltip from '@/app/components/base/tooltip'
  21. import { languages } from '@/i18n/language'
  22. type VoiceParamConfigProps = {
  23. onChange?: OnFeaturesChange
  24. }
  25. const VoiceParamConfig = ({
  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. const languageItem = languages.find(item => item.value === text2speech.language)
  35. const localLanguagePlaceholder = languageItem?.name || t('common.placeholder.select')
  36. const language = languageItem?.value
  37. const voiceItems = useSWR({ appId, language }, fetchAppVoices).data
  38. const voiceItem = voiceItems?.find(item => item.value === text2speech.voice)
  39. const localVoicePlaceholder = voiceItem?.name || t('common.placeholder.select')
  40. const handleChange = (value: Record<string, string>) => {
  41. const {
  42. features,
  43. setFeatures,
  44. } = featuresStore!.getState()
  45. const newFeatures = produce(features, (draft) => {
  46. draft.text2speech = {
  47. ...draft.text2speech,
  48. ...value,
  49. }
  50. })
  51. setFeatures(newFeatures)
  52. if (onChange)
  53. onChange(newFeatures)
  54. }
  55. return (
  56. <div>
  57. <div>
  58. <div className='leading-6 text-base font-semibold text-gray-800'>{t('appDebug.voice.voiceSettings.title')}</div>
  59. <div className='pt-3 space-y-6'>
  60. <div>
  61. <div className='mb-2 flex items-center space-x-1'>
  62. <div className='leading-[18px] text-[13px] font-semibold text-gray-800'>{t('appDebug.voice.voiceSettings.language')}</div>
  63. <Tooltip htmlContent={<div className='w-[180px]' >
  64. {t('appDebug.voice.voiceSettings.resolutionTooltip').split('\n').map(item => (
  65. <div key={item}>{item}</div>
  66. ))}
  67. </div>} selector='config-resolution-tooltip'>
  68. <RiQuestionLine className='w-[14px] h-[14px] text-gray-400' />
  69. </Tooltip>
  70. </div>
  71. <Listbox
  72. value={languageItem}
  73. onChange={(value: Item) => {
  74. handleChange({
  75. language: String(value.value),
  76. })
  77. }}
  78. >
  79. <div className={'relative h-9'}>
  80. <Listbox.Button className={'w-full h-full rounded-lg border-0 bg-gray-100 py-1.5 pl-3 pr-10 sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-gray-200 group-hover:bg-gray-200 cursor-pointer'}>
  81. <span className={classNames('block truncate text-left', !languageItem?.name && 'text-gray-400')}>
  82. {languageItem?.name ? t(`common.voice.language.${languageItem?.value.replace('-', '')}`) : localLanguagePlaceholder}
  83. </span>
  84. <span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
  85. <ChevronDownIcon
  86. className="h-5 w-5 text-gray-400"
  87. aria-hidden="true"
  88. />
  89. </span>
  90. </Listbox.Button>
  91. <Transition
  92. as={Fragment}
  93. leave="transition ease-in duration-100"
  94. leaveFrom="opacity-100"
  95. leaveTo="opacity-0"
  96. >
  97. <Listbox.Options className="absolute z-10 mt-1 px-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg border-gray-200 border-[0.5px] focus:outline-none sm:text-sm">
  98. {languages.map((item: Item) => (
  99. <Listbox.Option
  100. key={item.value}
  101. className={({ active }) =>
  102. `relative cursor-pointer select-none py-2 pl-3 pr-9 rounded-lg hover:bg-gray-100 text-gray-700 ${active ? 'bg-gray-100' : ''
  103. }`
  104. }
  105. value={item}
  106. disabled={false}
  107. >
  108. {({ /* active, */ selected }) => (
  109. <>
  110. <span
  111. className={classNames('block', selected && 'font-normal')}>{t(`common.voice.language.${(item.value).toString().replace('-', '')}`)}</span>
  112. {(selected || item.value === text2speech.language) && (
  113. <span
  114. className={classNames(
  115. 'absolute inset-y-0 right-0 flex items-center pr-4 text-gray-700',
  116. )}
  117. >
  118. <CheckIcon className="h-5 w-5" aria-hidden="true" />
  119. </span>
  120. )}
  121. </>
  122. )}
  123. </Listbox.Option>
  124. ))}
  125. </Listbox.Options>
  126. </Transition>
  127. </div>
  128. </Listbox>
  129. </div>
  130. <div>
  131. <div className='mb-2 leading-[18px] text-[13px] font-semibold text-gray-800'>{t('appDebug.voice.voiceSettings.voice')}</div>
  132. <Listbox
  133. value={voiceItem}
  134. disabled={!languageItem}
  135. onChange={(value: Item) => {
  136. handleChange({
  137. voice: String(value.value),
  138. })
  139. }}
  140. >
  141. <div className={'relative h-9'}>
  142. <Listbox.Button className={'w-full h-full rounded-lg border-0 bg-gray-100 py-1.5 pl-3 pr-10 sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-gray-200 group-hover:bg-gray-200 cursor-pointer'}>
  143. <span className={classNames('block truncate text-left', !voiceItem?.name && 'text-gray-400')}>{voiceItem?.name ?? localVoicePlaceholder}</span>
  144. <span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
  145. <ChevronDownIcon
  146. className="h-5 w-5 text-gray-400"
  147. aria-hidden="true"
  148. />
  149. </span>
  150. </Listbox.Button>
  151. <Transition
  152. as={Fragment}
  153. leave="transition ease-in duration-100"
  154. leaveFrom="opacity-100"
  155. leaveTo="opacity-0"
  156. >
  157. <Listbox.Options className="absolute z-10 mt-1 px-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg border-gray-200 border-[0.5px] focus:outline-none sm:text-sm">
  158. {voiceItems?.map((item: Item) => (
  159. <Listbox.Option
  160. key={item.value}
  161. className={({ active }) =>
  162. `relative cursor-pointer select-none py-2 pl-3 pr-9 rounded-lg hover:bg-gray-100 text-gray-700 ${active ? 'bg-gray-100' : ''
  163. }`
  164. }
  165. value={item}
  166. disabled={false}
  167. >
  168. {({ /* active, */ selected }) => (
  169. <>
  170. <span className={classNames('block', selected && 'font-normal')}>{item.name}</span>
  171. {(selected || item.value === text2speech.voice) && (
  172. <span
  173. className={classNames(
  174. 'absolute inset-y-0 right-0 flex items-center pr-4 text-gray-700',
  175. )}
  176. >
  177. <CheckIcon className="h-5 w-5" aria-hidden="true" />
  178. </span>
  179. )}
  180. </>
  181. )}
  182. </Listbox.Option>
  183. ))}
  184. </Listbox.Options>
  185. </Transition>
  186. </div>
  187. </Listbox>
  188. </div>
  189. </div>
  190. </div>
  191. </div>
  192. )
  193. }
  194. export default React.memo(VoiceParamConfig)