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;