123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316 |
- 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 (
- <View className={`${idle ? style.speechButton : style.speechButtonActive} ${buttonSize === 'small' ? style.buttonSmall : ''}`}>
- <View className={idle ? style.speechButtonInner : "hidden"}>
- <View className={style.keyboardButton} onLongPress={stopPropagation}
- onTouchStart={stopPropagation}
- onTouchEnd={stopPropagation}
- onClick={()=> {gotoChat()}}
- >
- <IconKeyboard></IconKeyboard>
- </View>
- <Text className={`font-medium ${style.speechText}`}>按住对话</Text>
- </View>
- </View>
- );
- }, [speechStatus]);
- const renderNickName = useMemo(() => {
- if (nicknameRenderer) {
- return nicknameRenderer();
- }
- return <View className="truncate">{character?.name}</View>;
- }, [character?.name, nicknameRenderer]);
- const renderNameAndAvatar = useMemo(() => {
- return (
- <>
- <View
- className={`flex items-center justify-center ${style.nickname} truncate`}
- onClick={handleNav}
- id="user-name"
- >
- {renderNickName}
- </View>
- <View className={style.circleOurter} onClick={handleNav}>
- <View className={style.circleInner}>
- {/* {renderAIStatus} */}
- <SpeakStatus></SpeakStatus>
- {avatarRenderer && avatarRenderer()}
- {character?.avatar && !avatarRenderer && (
- <AvatarMedia character={character}></AvatarMedia>
- )}
- </View>
- </View>
- </>
- );
- }, [character?.avatar, avatarRenderer, handleNav, renderNickName]);
- return (
- <View
- className={`flex flex-col items-center relative w-full ${style.wrapper}`}
- >
- <View
- className={`${style.cardContainer}`}
- style={character.customStyled}
- >
- {renderNameAndAvatar}
- <View
- className={`flex flex-col items-center justify-center pt-42 gap-12`}
- onLongPress={onLongPress}
- onTouchStart={handleTouchStart}
- onTouchEnd={onTouchEnd}
- >
- {renderSpeechButton}
- {shareButton && (
- <>
- <View className={style.shareButton} onClick={handleShareClick}>
- <Image src={IconShare} className="w-20 h-20" />
- {/* <Button openType={"share"} className="share-button"></Button> */}
- <Text>分享</Text>
- </View>
- <SharePopup
- character={character}
- visible={sharePopupVisible}
- onClose={handleSharePopupClosed}
- ></SharePopup>
- </>
- )}
-
- </View>
- </View>
- </View>
- );
- }
- ));
- // 添加显示名称,方便调试
- ChatAI.displayName = 'ChatAI';
- export default ChatAI;
|