import React, { useCallback, useEffect, useMemo, useRef } from 'react' import { addAskInfo, mergeAskInfo, setAutoScroll, setAutoTranslate, setCheckAutoScroll, setFoldAll, setNeedScroll, setSearchText, setSegmentFold, setTempData } from '../redux/envReducer' import { useAppDispatch, useAppSelector } from '../hooks/redux' import { AiOutlineAim, AiOutlineCloseCircle, FaRegArrowAltCircleDown, IoWarning, MdExpand, RiTranslate } from 'react-icons/all' import classNames from 'classnames' import toast from 'react-hot-toast' import SegmentCard from './SegmentCard' import { ASK_ENABLED_DEFAULT, HEADER_HEIGHT, PAGE_SETTINGS, SEARCH_BAR_HEIGHT, SUMMARIZE_ALL_THRESHOLD, TITLE_HEIGHT } from '../consts/const' import { FaClipboardList } from 'react-icons/fa' import useTranslate from '../hooks/useTranslate' import { openUrl } from '@kky002/kky-util' import useKeyService from '../hooks/useKeyService' import Ask from './Ask' import { v4 } from 'uuid' import RateExtension from '../components/RateExtension' import ApiKeyReminder from './ApiKeyReminder' const Body = () => { const dispatch = useAppDispatch() const inputting = useAppSelector(state => state.env.inputting) const noVideo = useAppSelector(state => state.env.noVideo) const autoTranslate = useAppSelector(state => state.env.autoTranslate) const autoScroll = useAppSelector(state => state.env.autoScroll) const segments = useAppSelector(state => state.env.segments) const foldAll = useAppSelector(state => state.env.foldAll) const envData = useAppSelector(state => state.env.envData) const compact = useAppSelector(state => state.env.tempData.compact) const floatKeyPointsSegIdx = useAppSelector(state => state.env.floatKeyPointsSegIdx) const translateEnable = useAppSelector(state => state.env.envData.translateEnable) const summarizeEnable = useAppSelector(state => state.env.envData.summarizeEnable) const { addSummarizeTask, addAskTask } = useTranslate() // const infos = useAppSelector(state => state.env.infos) const bodyRef = useRef() const curOffsetTop = useAppSelector(state => state.env.curOffsetTop) const checkAutoScroll = useAppSelector(state => state.env.checkAutoScroll) const needScroll = useAppSelector(state => state.env.needScroll) const totalHeight = useAppSelector(state => state.env.totalHeight) const curSummaryType = useAppSelector(state => state.env.tempData.curSummaryType) // const title = useAppSelector(state => state.env.title) // const fontSize = useAppSelector(state => state.env.envData.fontSize) const searchText = useAppSelector(state => state.env.searchText) const asks = useAppSelector(state => state.env.asks) // const recommendIdx = useMemo(() => random(0, 3), []) const showSearchInput = useMemo(() => { return (segments != null && segments.length > 0) && (envData.searchEnabled ? envData.searchEnabled : (envData.askEnabled ?? ASK_ENABLED_DEFAULT)) }, [envData.askEnabled, envData.searchEnabled, segments]) const searchPlaceholder = useMemo(() => { let placeholder = '' if (envData.searchEnabled) { if (envData.askEnabled ?? ASK_ENABLED_DEFAULT) { placeholder = '搜索或提问字幕内容(按Enter提问)' } else { placeholder = '搜索字幕内容' } } else { if (envData.askEnabled ?? ASK_ENABLED_DEFAULT) { placeholder = '提问字幕内容' } } return placeholder }, [envData.askEnabled, envData.searchEnabled]) const normalCallback = useCallback(() => { dispatch(setTempData({ compact: false })) }, [dispatch]) const compactCallback = useCallback(() => { dispatch(setTempData({ compact: true })) }, [dispatch]) const posCallback = useCallback(() => { dispatch(setNeedScroll(true)) }, [dispatch]) const onSummarizeAll = useCallback(() => { const apiKey = envData.aiType === 'gemini' ? envData.geminiApiKey : envData.apiKey if (!apiKey) { toast.error('请先在选项页面设置ApiKey!') return } const segments_ = [] for (const segment of segments ?? []) { const summary = segment.summaries[curSummaryType] if (!summary || summary.status === 'init' || (summary.status === 'done' && summary.error)) { segments_.push(segment) } } if (segments_.length === 0) { toast.error('没有可总结的段落!') return } if (segments_.length < SUMMARIZE_ALL_THRESHOLD || confirm(`确定总结${segments_.length}个段落?`)) { for (const segment of segments_) { addSummarizeTask(curSummaryType, segment).catch(console.error) } toast.success(`已添加${segments_.length}个总结任务!`) } }, [addSummarizeTask, curSummaryType, dispatch, envData.aiType, envData.apiKey, envData.geminiApiKey, segments]) const onFoldAll = useCallback(() => { dispatch(setFoldAll(!foldAll)) for (const ask of asks) { dispatch(mergeAskInfo({ id: ask.id, fold: !foldAll })) } for (const segment of segments ?? []) { dispatch(setSegmentFold({ segmentStartIdx: segment.startIdx, fold: !foldAll })) } }, [asks, dispatch, foldAll, segments]) const toggleAutoTranslateCallback = useCallback(() => { const apiKey = envData.aiType === 'gemini' ? envData.geminiApiKey : envData.apiKey if (apiKey) { dispatch(setAutoTranslate(!autoTranslate)) } else { toast.error('请先在选项页面设置ApiKey!') } }, [autoTranslate, dispatch, envData.aiType, envData.apiKey, envData.geminiApiKey]) const onEnableAutoScroll = useCallback(() => { dispatch(setAutoScroll(true)) dispatch(setNeedScroll(true)) }, [dispatch]) const onWheel = useCallback(() => { if (autoScroll) { dispatch(setAutoScroll(false)) } }, [autoScroll, dispatch]) // const onCopy = useCallback(() => { // const [success, content] = getSummarize(title, segments, curSummaryType) // if (success) { // navigator.clipboard.writeText(content).then(() => { // toast.success('复制成功') // }).catch(console.error) // } // }, [curSummaryType, segments, title]) const onSearchTextChange = useCallback((e: any) => { const searchText = e.target.value dispatch(setSearchText(searchText)) }, [dispatch]) const onClearSearchText = useCallback(() => { dispatch(setSearchText('')) }, [dispatch]) const onAsk = useCallback(() => { if ((envData.askEnabled ?? ASK_ENABLED_DEFAULT) && searchText) { const apiKey = envData.aiType === 'gemini' ? envData.geminiApiKey : envData.apiKey if (apiKey) { if (segments != null && segments.length > 0) { const id = v4() addAskTask(id, segments[0], searchText).catch(console.error) // 添加ask dispatch(addAskInfo({ id, question: searchText, status: 'pending', })) } } else { toast.error('请先在选项页面设置ApiKey!') } } }, [addAskTask, dispatch, envData.aiType, envData.apiKey, envData.askEnabled, envData.geminiApiKey, searchText, segments]) // service useKeyService() // 自动滚动 useEffect(() => { if (checkAutoScroll && curOffsetTop && autoScroll && !needScroll) { if (bodyRef.current.scrollTop <= curOffsetTop - bodyRef.current.offsetTop - (totalHeight - 160) + (floatKeyPointsSegIdx != null ? 100 : 0) || bodyRef.current.scrollTop >= curOffsetTop - bodyRef.current.offsetTop - 40 - 10 ) { dispatch(setNeedScroll(true)) dispatch(setCheckAutoScroll(false)) console.debug('need scroll') } } }, [autoScroll, checkAutoScroll, curOffsetTop, dispatch, floatKeyPointsSegIdx, needScroll, totalHeight]) return
{/* title */}
{segments != null && segments.length > 0 && }
列表视图 文章视图
{translateEnable &&
} {summarizeEnable &&
} {noVideo &&
}
{/* search */} {showSearchInput &&
{ // enter if (e.key === 'Enter') { if (!inputting) { e.preventDefault() e.stopPropagation() onAsk() dispatch(setSearchText('')) } } }} /> {searchText && }
} {/* auto scroll btn */} {!autoScroll &&
} {/* body */}
{/* asks */} {asks.map(ask => )} {/* segments */} {segments?.map((segment, segmentIdx) => )} {/* tip */}
快捷键提示
  • 单击字幕跳转,双击字幕跳转+切换暂停。
  • alt+单击字幕复制单条字幕。
  • 上下方向键来移动当前字幕(可先点击字幕使焦点在字幕列表内)。
{/*
*/} {/*
💡提示💡
*/} {/*
可以尝试将概览生成的内容粘贴到视频评论里,发布后看看有什么效果🥳 */} {/*
*/} {/* {(segments?.length ?? 0) > 0 && } */} {/*
*/}
{/*
*/} {/*
BibiGPT logoBibiGPT */} {/*
*/} {/*
这是网页版的字幕列表,支持任意视频提取字幕总结(包括没有字幕的视频) */} {/*
*/} {/* */} {/*
*/} {/* */}
{/* recommend */} {/*
*/} {/* {recommendIdx === 0 &&
*/} {/* { */} {/* e.preventDefault() */} {/* openUrl('https://bibigpt.co/r/bilibili') */} {/* }}>BibiGPT logo✨ BibiGPT ✨ */} {/* 支持任意视频的网页版总结。 */} {/*
} */} {/* {recommendIdx === 1 &&
*/} {/* { */} {/* e.preventDefault() */} {/* openUrl('https://chromewebstore.google.com/detail/fiaeclpicddpifeflpmlgmbjgaedladf') */} {/* }}>youtube caption logoYouTube Caption */} {/* YouTube版的字幕列表。 */} {/*
} */} {/* {recommendIdx === 2 &&
*/} {/* { */} {/* e.preventDefault() */} {/* openUrl('https://chromewebstore.google.com/detail/nanlpakfialleijdidafldapoifndngn') */} {/* }}>My Article Summarizer logoMy Article Summarizer */} {/* 网页文章总结。 */} {/*
} */} {/* {recommendIdx === 3 &&
*/} {/* { */} {/* e.preventDefault() */} {/* openUrl('https://api.openai-up.com/register?aff=varM') */} {/* }}>Openai Up logoOpenai代理 */} {/* 目前价格不到官方的6折 */} {/*
} */} {/*
*/}
} export default Body