param-config-content.tsx 8.4 KB

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