|
@@ -1,316 +0,0 @@
|
|
|
-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;
|