|  | @@ -1,11 +1,12 @@
 | 
	
		
			
				|  |  |  'use client'
 | 
	
		
			
				|  |  | -import { useRef, useState } from 'react'
 | 
	
		
			
				|  |  | +import { useEffect, useRef, useState } from 'react'
 | 
	
		
			
				|  |  |  import { t } from 'i18next'
 | 
	
		
			
				|  |  |  import { useParams, usePathname } from 'next/navigation'
 | 
	
		
			
				|  |  |  import s from './style.module.css'
 | 
	
		
			
				|  |  |  import Tooltip from '@/app/components/base/tooltip'
 | 
	
		
			
				|  |  |  import { randomString } from '@/utils'
 | 
	
		
			
				|  |  |  import { textToAudio } from '@/service/share'
 | 
	
		
			
				|  |  | +import Loading from '@/app/components/base/loading'
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  type AudioBtnProps = {
 | 
	
		
			
				|  |  |    value: string
 | 
	
	
		
			
				|  | @@ -14,6 +15,8 @@ type AudioBtnProps = {
 | 
	
		
			
				|  |  |    isAudition?: boolean
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +type AudioState = 'initial' | 'loading' | 'playing' | 'paused' | 'ended'
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |  const AudioBtn = ({
 | 
	
		
			
				|  |  |    value,
 | 
	
		
			
				|  |  |    voice,
 | 
	
	
		
			
				|  | @@ -21,9 +24,8 @@ const AudioBtn = ({
 | 
	
		
			
				|  |  |    isAudition,
 | 
	
		
			
				|  |  |  }: AudioBtnProps) => {
 | 
	
		
			
				|  |  |    const audioRef = useRef<HTMLAudioElement | null>(null)
 | 
	
		
			
				|  |  | -  const [isPlaying, setIsPlaying] = useState(false)
 | 
	
		
			
				|  |  | -  const [isPause, setPause] = useState(false)
 | 
	
		
			
				|  |  | -  const [hasEnded, setHasEnded] = useState(false)
 | 
	
		
			
				|  |  | +  const [audioState, setAudioState] = useState<AudioState>('initial')
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |    const selector = useRef(`play-tooltip-${randomString(4)}`)
 | 
	
		
			
				|  |  |    const params = useParams()
 | 
	
		
			
				|  |  |    const pathname = usePathname()
 | 
	
	
		
			
				|  | @@ -34,9 +36,11 @@ const AudioBtn = ({
 | 
	
		
			
				|  |  |      return ''
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -  const playAudio = async () => {
 | 
	
		
			
				|  |  | +  const loadAudio = async () => {
 | 
	
		
			
				|  |  |      const formData = new FormData()
 | 
	
		
			
				|  |  |      if (value !== '') {
 | 
	
		
			
				|  |  | +      setAudioState('loading')
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |        formData.append('text', removeCodeBlocks(value))
 | 
	
		
			
				|  |  |        formData.append('voice', removeCodeBlocks(voice))
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -59,67 +63,80 @@ const AudioBtn = ({
 | 
	
		
			
				|  |  |          const blob_bytes = Buffer.from(audioResponse.data, 'latin1')
 | 
	
		
			
				|  |  |          const blob = new Blob([blob_bytes], { type: 'audio/wav' })
 | 
	
		
			
				|  |  |          const audioUrl = URL.createObjectURL(blob)
 | 
	
		
			
				|  |  | -        const audio = new Audio(audioUrl)
 | 
	
		
			
				|  |  | -        audioRef.current = audio
 | 
	
		
			
				|  |  | -        audio.play().then(() => {}).catch(() => {
 | 
	
		
			
				|  |  | -          setIsPlaying(false)
 | 
	
		
			
				|  |  | -          URL.revokeObjectURL(audioUrl)
 | 
	
		
			
				|  |  | -        })
 | 
	
		
			
				|  |  | -        audio.onended = () => {
 | 
	
		
			
				|  |  | -          setHasEnded(true)
 | 
	
		
			
				|  |  | -          setIsPlaying(false)
 | 
	
		
			
				|  |  | -        }
 | 
	
		
			
				|  |  | +        audioRef.current!.src = audioUrl
 | 
	
		
			
				|  |  |        }
 | 
	
		
			
				|  |  |        catch (error) {
 | 
	
		
			
				|  |  | -        setIsPlaying(false)
 | 
	
		
			
				|  |  | +        setAudioState('initial')
 | 
	
		
			
				|  |  |          console.error('Error playing audio:', error)
 | 
	
		
			
				|  |  |        }
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  | -  const togglePlayPause = () => {
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  const handleToggle = () => {
 | 
	
		
			
				|  |  | +    if (audioState === 'initial')
 | 
	
		
			
				|  |  | +      loadAudio()
 | 
	
		
			
				|  |  |      if (audioRef.current) {
 | 
	
		
			
				|  |  | -      if (isPlaying) {
 | 
	
		
			
				|  |  | -        if (!hasEnded) {
 | 
	
		
			
				|  |  | -          setPause(false)
 | 
	
		
			
				|  |  | -          audioRef.current.play()
 | 
	
		
			
				|  |  | -        }
 | 
	
		
			
				|  |  | -        if (!isPause) {
 | 
	
		
			
				|  |  | -          setPause(true)
 | 
	
		
			
				|  |  | -          audioRef.current.pause()
 | 
	
		
			
				|  |  | -        }
 | 
	
		
			
				|  |  | +      if (audioState === 'playing') {
 | 
	
		
			
				|  |  | +        audioRef.current.pause()
 | 
	
		
			
				|  |  | +        setAudioState('paused')
 | 
	
		
			
				|  |  |        }
 | 
	
		
			
				|  |  | -      else if (!isPlaying) {
 | 
	
		
			
				|  |  | -        if (isPause) {
 | 
	
		
			
				|  |  | -          setPause(false)
 | 
	
		
			
				|  |  | -          audioRef.current.play()
 | 
	
		
			
				|  |  | -        }
 | 
	
		
			
				|  |  | -        else {
 | 
	
		
			
				|  |  | -          setHasEnded(false)
 | 
	
		
			
				|  |  | -          playAudio().then()
 | 
	
		
			
				|  |  | -        }
 | 
	
		
			
				|  |  | +      else if (audioState === 'paused' || audioState === 'ended') {
 | 
	
		
			
				|  |  | +        audioRef.current.play()
 | 
	
		
			
				|  |  | +        setAudioState('playing')
 | 
	
		
			
				|  |  |        }
 | 
	
		
			
				|  |  | -      setIsPlaying(prevIsPlaying => !prevIsPlaying)
 | 
	
		
			
				|  |  | -    }
 | 
	
		
			
				|  |  | -    else {
 | 
	
		
			
				|  |  | -      setIsPlaying(true)
 | 
	
		
			
				|  |  | -      if (!isPlaying)
 | 
	
		
			
				|  |  | -        playAudio().then()
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +  useEffect(() => {
 | 
	
		
			
				|  |  | +    const currentAudio = audioRef.current
 | 
	
		
			
				|  |  | +    const handleLoading = () => {
 | 
	
		
			
				|  |  | +      setAudioState('loading')
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +    const handlePlay = () => {
 | 
	
		
			
				|  |  | +      currentAudio?.play()
 | 
	
		
			
				|  |  | +      setAudioState('playing')
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +    const handleEnded = () => {
 | 
	
		
			
				|  |  | +      setAudioState('ended')
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +    currentAudio?.addEventListener('progress', handleLoading)
 | 
	
		
			
				|  |  | +    currentAudio?.addEventListener('canplaythrough', handlePlay)
 | 
	
		
			
				|  |  | +    currentAudio?.addEventListener('ended', handleEnded)
 | 
	
		
			
				|  |  | +    return () => {
 | 
	
		
			
				|  |  | +      if (currentAudio) {
 | 
	
		
			
				|  |  | +        currentAudio.removeEventListener('progress', handleLoading)
 | 
	
		
			
				|  |  | +        currentAudio.removeEventListener('canplaythrough', handlePlay)
 | 
	
		
			
				|  |  | +        currentAudio.removeEventListener('ended', handleEnded)
 | 
	
		
			
				|  |  | +        URL.revokeObjectURL(currentAudio.src)
 | 
	
		
			
				|  |  | +        currentAudio.src = ''
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  }, [])
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  const tooltipContent = {
 | 
	
		
			
				|  |  | +    initial: t('appApi.play'),
 | 
	
		
			
				|  |  | +    ended: t('appApi.play'),
 | 
	
		
			
				|  |  | +    paused: t('appApi.pause'),
 | 
	
		
			
				|  |  | +    playing: t('appApi.playing'),
 | 
	
		
			
				|  |  | +    loading: t('appApi.loading'),
 | 
	
		
			
				|  |  | +  }[audioState]
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |    return (
 | 
	
		
			
				|  |  | -    <div className={`${(isPlaying && !hasEnded) ? 'mr-1' : className}`}>
 | 
	
		
			
				|  |  | +    <div className={`${(audioState === 'loading' || audioState === 'playing') ? 'mr-1' : className}`}>
 | 
	
		
			
				|  |  |        <Tooltip
 | 
	
		
			
				|  |  |          selector={selector.current}
 | 
	
		
			
				|  |  | -        content={(!isPause ? ((isPlaying && !hasEnded) ? t('appApi.playing') : t('appApi.play')) : t('appApi.pause')) as string}
 | 
	
		
			
				|  |  | +        content={tooltipContent}
 | 
	
		
			
				|  |  |          className='z-10'
 | 
	
		
			
				|  |  |        >
 | 
	
		
			
				|  |  | -        <div
 | 
	
		
			
				|  |  | +        <button
 | 
	
		
			
				|  |  | +          disabled={audioState === 'loading'}
 | 
	
		
			
				|  |  |            className={`box-border p-0.5 flex items-center justify-center cursor-pointer ${isAudition || '!p-0 rounded-md bg-white'}`}
 | 
	
		
			
				|  |  | -          onClick={togglePlayPause}>
 | 
	
		
			
				|  |  | -          <div className={`w-6 h-6 rounded-md ${!isAudition ? 'w-4 h-4 hover:bg-gray-50' : 'hover:bg-gray-50'} ${(isPlaying && !hasEnded) ? s.pauseIcon : s.playIcon}`}></div>
 | 
	
		
			
				|  |  | -        </div>
 | 
	
		
			
				|  |  | +          onClick={handleToggle}>
 | 
	
		
			
				|  |  | +          {audioState === 'loading' && <div className='w-6 h-6 rounded-md flex items-center justify-center p-2'><Loading /></div>}
 | 
	
		
			
				|  |  | +          {audioState !== 'loading' && <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>}
 | 
	
		
			
				|  |  | +        </button>
 | 
	
		
			
				|  |  |        </Tooltip>
 | 
	
		
			
				|  |  | +      <audio ref={audioRef} src='' className='hidden' />
 | 
	
		
			
				|  |  |      </div>
 | 
	
		
			
				|  |  |    )
 | 
	
		
			
				|  |  |  }
 |