index.tsx 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. 'use client'
  2. import { useEffect, useRef, useState } from 'react'
  3. import { t } from 'i18next'
  4. import { useParams, usePathname } from 'next/navigation'
  5. import s from './style.module.css'
  6. import Tooltip from '@/app/components/base/tooltip'
  7. import { randomString } from '@/utils'
  8. import { textToAudio } from '@/service/share'
  9. import Loading from '@/app/components/base/loading'
  10. type AudioBtnProps = {
  11. value: string
  12. voice?: string
  13. className?: string
  14. isAudition?: boolean
  15. noCache: boolean
  16. }
  17. type AudioState = 'initial' | 'loading' | 'playing' | 'paused' | 'ended'
  18. const AudioBtn = ({
  19. value,
  20. voice,
  21. className,
  22. isAudition,
  23. noCache,
  24. }: AudioBtnProps) => {
  25. const audioRef = useRef<HTMLAudioElement | null>(null)
  26. const [audioState, setAudioState] = useState<AudioState>('initial')
  27. const selector = useRef(`play-tooltip-${randomString(4)}`)
  28. const params = useParams()
  29. const pathname = usePathname()
  30. const removeCodeBlocks = (inputText: any) => {
  31. const codeBlockRegex = /```[\s\S]*?```/g
  32. if (inputText)
  33. return inputText.replace(codeBlockRegex, '')
  34. return ''
  35. }
  36. const loadAudio = async () => {
  37. const formData = new FormData()
  38. formData.append('text', removeCodeBlocks(value))
  39. formData.append('voice', removeCodeBlocks(voice))
  40. if (value !== '') {
  41. setAudioState('loading')
  42. let url = ''
  43. let isPublic = false
  44. if (params.token) {
  45. url = '/text-to-audio'
  46. isPublic = true
  47. }
  48. else if (params.appId) {
  49. if (pathname.search('explore/installed') > -1)
  50. url = `/installed-apps/${params.appId}/text-to-audio`
  51. else
  52. url = `/apps/${params.appId}/text-to-audio`
  53. }
  54. try {
  55. const audioResponse = await textToAudio(url, isPublic, formData)
  56. const blob_bytes = Buffer.from(audioResponse.data, 'latin1')
  57. const blob = new Blob([blob_bytes], { type: 'audio/wav' })
  58. const audioUrl = URL.createObjectURL(blob)
  59. audioRef.current!.src = audioUrl
  60. }
  61. catch (error) {
  62. setAudioState('initial')
  63. console.error('Error playing audio:', error)
  64. }
  65. }
  66. }
  67. const handleToggle = async () => {
  68. if (audioState === 'initial' || noCache) {
  69. await loadAudio()
  70. }
  71. else if (audioRef.current) {
  72. if (audioState === 'playing') {
  73. audioRef.current.pause()
  74. setAudioState('paused')
  75. }
  76. else {
  77. audioRef.current.play()
  78. setAudioState('playing')
  79. }
  80. }
  81. }
  82. useEffect(() => {
  83. const currentAudio = audioRef.current
  84. const handleLoading = () => {
  85. setAudioState('loading')
  86. }
  87. const handlePlay = () => {
  88. currentAudio?.play()
  89. setAudioState('playing')
  90. }
  91. const handleEnded = () => {
  92. setAudioState('ended')
  93. }
  94. currentAudio?.addEventListener('progress', handleLoading)
  95. currentAudio?.addEventListener('canplaythrough', handlePlay)
  96. currentAudio?.addEventListener('ended', handleEnded)
  97. return () => {
  98. currentAudio?.removeEventListener('progress', handleLoading)
  99. currentAudio?.removeEventListener('canplaythrough', handlePlay)
  100. currentAudio?.removeEventListener('ended', handleEnded)
  101. URL.revokeObjectURL(currentAudio?.src || '')
  102. currentAudio?.pause()
  103. currentAudio?.setAttribute('src', '')
  104. }
  105. }, [])
  106. const tooltipContent = {
  107. initial: t('appApi.play'),
  108. ended: t('appApi.play'),
  109. paused: t('appApi.pause'),
  110. playing: t('appApi.playing'),
  111. loading: t('appApi.loading'),
  112. }[audioState]
  113. return (
  114. <div className={`${(audioState === 'loading' || audioState === 'playing') ? 'mr-1' : className}`}>
  115. <Tooltip
  116. selector={selector.current}
  117. content={tooltipContent}
  118. className='z-10'
  119. >
  120. <button
  121. disabled={audioState === 'loading'}
  122. className={`box-border p-0.5 flex items-center justify-center cursor-pointer ${isAudition || '!p-0 rounded-md bg-white'}`}
  123. onClick={handleToggle}
  124. >
  125. {audioState === 'loading'
  126. ? (
  127. <div className='w-6 h-6 rounded-md flex items-center justify-center p-2'>
  128. <Loading />
  129. </div>
  130. )
  131. : (
  132. <div className={`w-6 h-6 rounded-md ${!isAudition ? 'w-4 h-4 hover:bg-gray-50' : 'hover:bg-gray-50'} ${(audioState === 'playing') ? s.pauseIcon : s.playIcon}`}></div>
  133. )}
  134. </button>
  135. </Tooltip>
  136. <audio ref={audioRef} src='' className='hidden' />
  137. </div>
  138. )
  139. }
  140. export default AudioBtn