<commit_before>import classNames from 'classnames'
import type { Episode, TranscriptRow } from 'podverse-shared'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { playerGetPosition, playerSeekTo } from '~/services/player/player'
import { getEpisodeProxyTranscript } from '~/services/transcript'
import { MainContentSection, SearchBarFilter } from '..'

type Props = {
  episode?: Episode
}

let scrollToPositionIntervalId: any = null
let playbackPositionIntervalId: any = null
const playbackPositionIntervalTime = 0.5 // seconds

export const Transcripts = ({ episode }: Props) => {
  const { t } = useTranslation()
  const [autoScrollOn, setAutoScrollOn] = useState<boolean>(false)
  const [currentPlaybackPosition, setCurrentPlaybackPosition] = useState<number>(0)
  const [transcriptRows, setTranscriptRows] = useState<TranscriptRow[]>([])
  const [transcriptRowsLoading, setTranscriptRowsLoading] = useState<boolean>(false)
  const [transcriptSearchRows, setTranscriptSearchRows] = useState<TranscriptRow[]>([])

  useEffect(() => {
    const transcriptTag = episode?.transcript && episode?.transcript[0]

    ;(async () => {
      if (transcriptTag) {
        setTranscriptRowsLoading(true)
        const parsedTranscriptRows = await getEpisodeProxyTranscript(episode.id)
        setTranscriptRows(parsedTranscriptRows)
        setTranscriptRowsLoading(false)
      }
    })()

    if (transcriptTag) {
      playbackPositionIntervalId = setInterval(() => {
        const playbackPosition = playerGetPosition()
        setCurrentPlaybackPosition(playbackPosition)
      }, playbackPositionIntervalTime * 1000)
    }

    return () => {
      if (playbackPositionIntervalId) {
        clearInterval(playbackPositionIntervalId)
      }
      if (scrollToPositionIntervalId) {
        clearInterval(scrollToPositionIntervalId)
      }
    }
  }, [])

  const handleAutoScrollButton = () => {
    const newAutoScrollOn = !autoScrollOn
    setAutoScrollOn(newAutoScrollOn)
    if (newAutoScrollOn) {
      scrollToPositionIntervalId = setInterval(() => {
        const currentlyPlayingRow = document.querySelector(
          '.transcripts .transcripts-wrapper .transcript-row.currently-playing'
        ) as any
        if (currentlyPlayingRow) {
          const rowHeight = currentlyPlayingRow.offsetHeight
          const topPos =
            currentlyPlayingRow.offsetTop >= rowHeight
              ? currentlyPlayingRow.offsetTop - rowHeight * 2
              : currentlyPlayingRow.offsetTop
          const transcriptsWrapper = document.querySelector('.transcripts-wrapper')
          if (transcriptsWrapper) {
            transcriptsWrapper.scrollTop = topPos
          }
        }
      }, 100)
    } else if (scrollToPositionIntervalId) {
      clearInterval(scrollToPositionIntervalId)
    }
  }

  const handleRowClick = (skipToTime: number) => {
    playerSeekTo(skipToTime)
  }

  const _handleSearchClear = () => {
    setTranscriptSearchRows([])
  }

  const _handleSearchSubmit = (searchText?: string) => {
    if (!searchText || searchText?.length === 0) {
      _handleSearchClear()
    } else {
      const searchResults = transcriptRows.filter((item: Record<string, any>) => {
        return item?.text?.toLowerCase().includes(searchText?.toLowerCase())
      })

      setAutoScrollOn(false)
      if (scrollToPositionIntervalId) {
        clearInterval(scrollToPositionIntervalId)
      }

      if (searchResults.length === 0) {
        searchResults.push({
          text: t('No results found')
        } as any)
      }

      setTranscriptSearchRows(searchResults)
    }
  }

  const generateSingleLineTranscriptNode = (transcriptRow: TranscriptRow) => {
    return (
      <div className='transcript-row'>
        <div className='transcript-row__text'>{transcriptRow.text}</div>
      </div>
    )
  }

  const generateTranscriptRowNode = (transcriptRow: TranscriptRow) => {
    if (!transcriptRow) return null

    const rowClassName = classNames('transcript-row', {
      'currently-playing':
        (currentPlaybackPosition < 1 && Math.floor(transcriptRow.endTime) <= 1) ||
        (currentPlaybackPosition >= transcriptRow.startTime && currentPlaybackPosition < transcriptRow.endTime)
    })

    return (
      <div className={rowClassName} onClick={() => handleRowClick(transcriptRow.startTime)}>
        <div className='transcript-row__text'>{transcriptRow.text}</div>
        <div className='transcript-row__time'>{transcriptRow.startTimeHHMMSS}</div>
      </div>
    )
  }

  const generateTranscriptRowNodes = () => {
    let transcriptRowNodes = []
    if (transcriptSearchRows?.length > 0) {
      transcriptRowNodes = transcriptSearchRows?.map(generateTranscriptRowNode) || []
    } else {
      if (transcriptRows?.length === 1) {
        transcriptRowNodes = transcriptRows.map(generateSingleLineTranscriptNode) || []
      } else {
        transcriptRowNodes = transcriptRows.map(generateTranscriptRowNode) || []
      }
    }

    return transcriptRowNodes
  }

  const transcriptRowNodes = generateTranscriptRowNodes()

  return (
    <div className='transcripts'>
      <MainContentSection
        handleAutoScrollButton={handleAutoScrollButton}
        headerText={t('Transcript')}
        isAutoScrollOn={autoScrollOn}
        isLoading={transcriptRowsLoading}
      >
        {transcriptRowNodes?.length > 1 && (
          <SearchBarFilter
            handleClear={_handleSearchClear}
            handleSubmit={_handleSearchSubmit}
            placeholder={t('Transcript search')}
            smaller
          />
        )}
        <div className='transcripts-wrapper'>{transcriptRowNodes}</div>
      </MainContentSection>
      <hr />
    </div>
  )
}<commit_msg>Fix "transcript search bar disappears when no search results" bug #1034<commit_after>import classNames from 'classnames'
import type { Episode, TranscriptRow } from 'podverse-shared'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { playerGetPosition, playerSeekTo } from '~/services/player/player'
import { getEpisodeProxyTranscript } from '~/services/transcript'
import { MainContentSection, SearchBarFilter } from '..'

type Props = {
  episode?: Episode
}

let scrollToPositionIntervalId: any = null
let playbackPositionIntervalId: any = null
const playbackPositionIntervalTime = 0.5 // seconds

export const Transcripts = ({ episode }: Props) => {
  const { t } = useTranslation()
  const [autoScrollOn, setAutoScrollOn] = useState<boolean>(false)
  const [currentPlaybackPosition, setCurrentPlaybackPosition] = useState<number>(0)
  const [transcriptRows, setTranscriptRows] = useState<TranscriptRow[]>([])
  const [transcriptRowsLoading, setTranscriptRowsLoading] = useState<boolean>(false)
  const [transcriptSearchRows, setTranscriptSearchRows] = useState<TranscriptRow[]>([])
  const [hasMultilineTranscript, setHasMultilineTranscript] = useState<boolean>(false)

  useEffect(() => {
    const transcriptTag = episode?.transcript && episode?.transcript[0]

    ;(async () => {
      if (transcriptTag) {
        setTranscriptRowsLoading(true)
        const parsedTranscriptRows = await getEpisodeProxyTranscript(episode.id)
        setTranscriptRows(parsedTranscriptRows)
        setHasMultilineTranscript(parsedTranscriptRows.length > 1)
        setTranscriptRowsLoading(false)
      }
    })()

    if (transcriptTag) {
      playbackPositionIntervalId = setInterval(() => {
        const playbackPosition = playerGetPosition()
        setCurrentPlaybackPosition(playbackPosition)
      }, playbackPositionIntervalTime * 1000)
    }

    return () => {
      if (playbackPositionIntervalId) {
        clearInterval(playbackPositionIntervalId)
      }
      if (scrollToPositionIntervalId) {
        clearInterval(scrollToPositionIntervalId)
      }
    }
  }, [])

  const handleAutoScrollButton = () => {
    const newAutoScrollOn = !autoScrollOn
    setAutoScrollOn(newAutoScrollOn)
    if (newAutoScrollOn) {
      scrollToPositionIntervalId = setInterval(() => {
        const currentlyPlayingRow = document.querySelector(
          '.transcripts .transcripts-wrapper .transcript-row.currently-playing'
        ) as any
        if (currentlyPlayingRow) {
          const rowHeight = currentlyPlayingRow.offsetHeight
          const topPos =
            currentlyPlayingRow.offsetTop >= rowHeight
              ? currentlyPlayingRow.offsetTop - rowHeight * 2
              : currentlyPlayingRow.offsetTop
          const transcriptsWrapper = document.querySelector('.transcripts-wrapper')
          if (transcriptsWrapper) {
            transcriptsWrapper.scrollTop = topPos
          }
        }
      }, 100)
    } else if (scrollToPositionIntervalId) {
      clearInterval(scrollToPositionIntervalId)
    }
  }

  const handleRowClick = (skipToTime: number) => {
    playerSeekTo(skipToTime)
  }

  const _handleSearchClear = () => {
    setTranscriptSearchRows([])
  }

  const _handleSearchSubmit = (searchText?: string) => {
    if (!searchText || searchText?.length === 0) {
      _handleSearchClear()
    } else {
      const searchResults = transcriptRows.filter((item: Record<string, any>) => {
        return item?.text?.toLowerCase().includes(searchText?.toLowerCase())
      })

      setAutoScrollOn(false)
      if (scrollToPositionIntervalId) {
        clearInterval(scrollToPositionIntervalId)
      }

      if (searchResults.length === 0) {
        searchResults.push({
          text: t('No results found')
        } as any)
      }

      setTranscriptSearchRows(searchResults)
    }
  }

  const generateSingleLineTranscriptNode = (transcriptRow: TranscriptRow) => {
    return (
      <div className='transcript-row'>
        <div className='transcript-row__text'>{transcriptRow.text}</div>
      </div>
    )
  }

  const generateTranscriptRowNode = (transcriptRow: TranscriptRow) => {
    if (!transcriptRow) return null

    const rowClassName = classNames('transcript-row', {
      'currently-playing':
        (currentPlaybackPosition < 1 && Math.floor(transcriptRow.endTime) <= 1) ||
        (currentPlaybackPosition >= transcriptRow.startTime && currentPlaybackPosition < transcriptRow.endTime)
    })

    return (
      <div className={rowClassName} onClick={() => handleRowClick(transcriptRow.startTime)}>
        <div className='transcript-row__text'>{transcriptRow.text}</div>
        <div className='transcript-row__time'>{transcriptRow.startTimeHHMMSS}</div>
      </div>
    )
  }

  const generateTranscriptRowNodes = () => {
    let transcriptRowNodes = []
    if (transcriptSearchRows?.length > 0) {
      transcriptRowNodes = transcriptSearchRows?.map(generateTranscriptRowNode) || []
    } else {
      if (transcriptRows?.length === 1) {
        transcriptRowNodes = transcriptRows.map(generateSingleLineTranscriptNode) || []
      } else {
        transcriptRowNodes = transcriptRows.map(generateTranscriptRowNode) || []
      }
    }

    return transcriptRowNodes
  }

  const transcriptRowNodes = generateTranscriptRowNodes()

  return (
    <div className='transcripts'>
      <MainContentSection
        handleAutoScrollButton={handleAutoScrollButton}
        headerText={t('Transcript')}
        isAutoScrollOn={autoScrollOn}
        isLoading={transcriptRowsLoading}
      >
        {hasMultilineTranscript && (
          <SearchBarFilter
            handleClear={_handleSearchClear}
            handleSubmit={_handleSearchSubmit}
            placeholder={t('Transcript search')}
            smaller
          />
        )}
        <div className='transcripts-wrapper'>{transcriptRowNodes}</div>
      </MainContentSection>
      <hr />
    </div>
  )
}
{'fraction_non_alphanumeric': 0.10787321063394684, 'fraction_numerical': 0.003919563735514656, 'mean_word_length': 3.3008427995602787, 'pattern_counts': {'":': 0, '<': 58, '<?xml version=': 0, '>': 90, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 8, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3858979', 'n_tokens_mistral': 3312, 'n_tokens_neox': 3123, 'n_words': 565}