import { Image, Text, View, type CommonEvent } from "@tarojs/components"; import { forwardRef, useEffect, useImperativeHandle, useState, useMemo, useCallback, memo, } from "react"; import { useVoiceRecord } from "@/components/voice-recorder"; import IconShare from "@/images/icon-share.png"; import { chat } from "@/service/character"; import { useCharacterAIStore } from "@/store/characterAIStore"; import { greeting, } from '@/service/character' import type { ICharacter } from "@/types"; import { ECharacterAISTATUS } from "@/types/index"; import { useAudioPlayer } from "@/utils/audio"; import { useBase64AudioPlayer } from "@/utils/audioBase64"; import { handleShare } from "@/utils/share"; import Taro, { useDidHide, useUnload } from "@tarojs/taro"; import { axios } from "taro-axios"; import style from "./index.module.less"; import SharePopup from "@/components/custom-share/share-popup/index"; import SpeakStatus from "./components/speak-status" import IconKeyboard from '@/components/icon/icon-keyboard'; import { AvatarMedia } from "./components/AvatarMedia"; import { setChatSession, getChatSession } from '@/store/chat' let cancelTokenSource = axios.CancelToken.source(); interface Props { character: ICharacter; getSession?: boolean; // 是否发起greeting 请求获取聊天用的 session playGreeting?: boolean; // 是否播放欢迎语 shareButton?: boolean; // 是否显示分享按钮 avatarRenderer?: () => JSX.Element; // 自定义渲染头像 nicknameRenderer?: () => JSX.Element; // 自定义渲染昵称 enableGoPreview?: boolean; // 允许点击头像去预览 buttonSize?: string; // 按钮大小 } let source: AudioBufferSourceNode; export interface IChatAIComponent { stop: () => void; } const ChatAI = memo(forwardRef( ( { character, playGreeting = false, getSession = false, enableGoPreview = false, shareButton = true, avatarRenderer, nicknameRenderer, buttonSize, }: Props, ref ) => { const currentCharacter = character; const [speechStatus, setSpeechStatus] = useState(0); const { start, stop, onVolumeChange, onStop } = useVoiceRecord(); const { stopPlayChunk, onPlayerStatusChanged } = useAudioPlayer(); const { playAudio, stopAudio, onEnded } = useBase64AudioPlayer(); const { setStatus } = useCharacterAIStore(); const [sharePopupVisible, setSharePopupVisible] = useState(false); // console.log( // "currentCharacter:", // currentCharacter?.profileId, // character.profileId, // getSession // ); const handleShareClick = () => { handleShare(); Taro.hideTabBar(); setSharePopupVisible(true); }; const handleSharePopupClosed = () => { Taro.showTabBar(); setSharePopupVisible(false); }; const stopSpeek = () => { setSpeechStatus(0); stop(); }; onEnded(()=> { setStatus(ECharacterAISTATUS.IDLE) }) onPlayerStatusChanged((status: ECharacterAISTATUS) => { setStatus(status) }); const stopPlay = () => { source && source.stop(); stopPlayChunk(); }; const stopPropagation = (e?: CommonEvent)=> { e?.stopPropagation(); } const onLongPress = (e?: CommonEvent) => { e?.stopPropagation(); setSpeechStatus(1); stopPlay(); start(); }; const onTouchEnd = () => { stopSpeek(); }; const handleTouchStart = (e: CommonEvent) => { // fix 解决pc端长按呼出右键菜单 e.preventDefault(); // fix 解决移动端长按依旧穿透 e.stopPropagation(); }; onVolumeChange((_v) => {}); onStop((res) => { const session = getChatSession() console.log("record stop:", res, session); stopSpeek(); if (!res) { return; } console.log(character.profileId, session); if (character?.profileId && session) { setStatus(ECharacterAISTATUS.THINKING); chat( character.profileId, session, res?.tempFilePath || res?.arrayBuffer ); } }); // 打招呼,聊天前都需要先招呼获取聊天所需的 session const showGreeting = async () => { // cancelTokenSource.cancel('Previous showGreeting request canceled') // 重新创建一个新的取消令牌源 if (!getSession) { return; } cancelTokenSource = axios.CancelToken.source(); const res = await greeting(currentCharacter.profileId, { cancelToken: cancelTokenSource.token, }); if(res.code !== 0){ return } if (res?.data?.audio && playGreeting) { setStatus(ECharacterAISTATUS.RESPONDING); const _source = await playAudio(res?.data?.audio); if (_source) { source = _source; } } if (res?.data?.session) { setChatSession(res.data?.session); } }; const handleNav = () => { if (enableGoPreview) { Taro.navigateTo({ url: "/pages/profile/index?profileId=" + character.profileId, }); } }; const gotoChat = () => { Taro.navigateTo({ url: "/pages/chat/index?profileId=" + character.profileId, }); }; // 侦听卡片是否为当前卡片,是的话需要播放欢迎语拿session // 后期是否优化考虑保存住每个卡片的 session useEffect(() => { if (currentCharacter?.profileId === character.profileId) { showGreeting(); } return () => { cancelTokenSource.cancel("Previous showGreeting request canceled"); source && source.stop(); }; }, [currentCharacter?.profileId, character.profileId, getSession]); const stopChatAI = useCallback(() => { // todo: 调用此方法会导致组件重新渲染,需要优化 setStatus(ECharacterAISTATUS.IDLE); stopPlay(); },[]); // 对外暴露 stop 方法 useImperativeHandle(ref, () => { return { stop: stopChatAI, }; }); useDidHide(() => { stopPlay(); stopAudio(); }); useUnload(() => { stopPlay(); stopAudio(); }); // 使用 useCallback 缓存渲染函数 const renderSpeechButton = useMemo(() => { if (process.env.TARO_ENV == "h5") { return <>; } const idle = speechStatus === 0; return ( {gotoChat()}} > 按住对话 ); }, [speechStatus]); const renderNickName = useMemo(() => { if (nicknameRenderer) { return nicknameRenderer(); } return {character?.name}; }, [character?.name, nicknameRenderer]); const renderNameAndAvatar = useMemo(() => { return ( <> {renderNickName} {/* {renderAIStatus} */} {avatarRenderer && avatarRenderer()} {character?.avatar && !avatarRenderer && ( )} ); }, [character?.avatar, avatarRenderer, handleNav, renderNickName]); return ( {renderNameAndAvatar} {renderSpeechButton} {shareButton && ( <> {/* */} 分享 )} ); } )); // 添加显示名称,方便调试 ChatAI.displayName = 'ChatAI'; export default ChatAI;