Jelajahi Sumber

refactor: 声音列表请求优化

王晓东 1 bulan lalu
induk
melakukan
03118e7fcb

+ 3 - 38
project.private.config.json

@@ -9,46 +9,11 @@
     "miniprogram": {
       "list": [
         {
-          "name": "pages/profile/index",
-          "pathName": "pages/profile/index",
-          "query": "agentId=p_2e73c9d7efaYfDo2-agent_1485",
+          "name": "pages/agent/index",
+          "pathName": "pages/agent/index",
+          "query": "agentId=p_2e73c9d7efaYfDo2-agent_1016",
           "scene": null,
           "launchMode": "default"
-        },
-        {
-          "name": "pages/component-library/index",
-          "pathName": "pages/component-library/index",
-          "query": "",
-          "launchMode": "default",
-          "scene": null
-        },
-        {
-          "name": "pages/knowledge/index",
-          "pathName": "pages/knowledge/index",
-          "query": "",
-          "launchMode": "default",
-          "scene": null
-        },
-        {
-          "name": "pages/dashboard/index",
-          "pathName": "pages/dashboard/index",
-          "query": "",
-          "launchMode": "default",
-          "scene": null
-        },
-        {
-          "name": "pages/contact/index",
-          "pathName": "pages/contact/index",
-          "query": "",
-          "launchMode": "default",
-          "scene": null
-        },
-        {
-          "name": "pages/dashboard/index",
-          "pathName": "pages/dashboard/index",
-          "query": "",
-          "launchMode": "default",
-          "scene": null
         }
       ]
     }

+ 43 - 5
src/components/chat-message/MessageRobot.tsx

@@ -5,8 +5,7 @@ import IconDislike from "@/components/icon/IconDislike";
 import IconDislikeBlue from "@/components/icon/IconDislikeBlue";
 import IconLike from "@/components/icon/IconLike";
 import IconLikeBlue from "@/components/icon/IconLikeBlue";
-import IconSpeaker from "@/components/icon/icon-speaker";
-import IconCopyDashboard from '@/images/svgs/dashboard/IconCopy.svg'
+import IconSpeaker from "@/components/icon/IconSpeaker";
 import RenderMedia from "@/components/RenderContent/RenderMedia";
 import RenderLinks from "@/components/RenderContent/RenderLinks";
 
@@ -19,6 +18,9 @@ import { EContentType, TMessage } from "@/types/bot";
 import { getLoginId, isSuccess } from "@/utils";
 import { useState } from "react";
 import { AvatarMedia } from "../AvatarMedia";
+import { useTextChat } from "@/store/textChat";
+import { useTextToSpeech } from './textToSpeech'
+
 interface Props {
   agent?: TAgentDetail | null;
   text: string;
@@ -29,7 +31,11 @@ interface Props {
 export default ({ agent, text, message, showUser=false, textReasoning = "" }: Props) => {
   const [isDislike, setIsDislike] = useState(message.isDislike);
   const [isLike, setIsLike] = useState(message.isLike);
-  // console.log('helloworld: ', message)
+  const [isSpeaking, setIsSpeaking] = useState(false);
+  const loginId = getLoginId();
+  const { setMessageRespeaking } = useTextChat()
+  const { startSpeech, stopSpeech, onPlayerStatusChanged } = useTextToSpeech()
+
   const handleCopy = (e: any, textStr: string) => {
     e.stopPropagation();
     // 手动复制并 toast 提示
@@ -52,7 +58,36 @@ export default ({ agent, text, message, showUser=false, textReasoning = "" }: Pr
       });
     }
   };
-  const loginId = getLoginId();
+
+
+  onPlayerStatusChanged((status)=> {
+    const isSpeaking = status === 'playing';
+    setMessageRespeaking(isSpeaking)
+    setIsSpeaking(isSpeaking)
+  })
+  
+  const handlePlay = async (e:any, text: string) => {
+    e.stopPropagation();
+
+    if(isSpeaking){
+      stopSpeech()
+      setIsSpeaking(false)
+      return;
+    }
+    if(!agent?.agentId){
+      return;
+    }
+
+    startSpeech({
+      agentId: agent.agentId,
+      msgUk: message.msgUk,
+      text: text,
+      loginId: loginId,
+    })
+    
+    setIsSpeaking(true)
+    setMessageRespeaking(true)
+  }
   const handleDislike = async () => {
     if (!agent?.agentId) {
       return;
@@ -108,7 +143,7 @@ export default ({ agent, text, message, showUser=false, textReasoning = "" }: Pr
   // 渲染消息主体
   const renderMessageBody = () => {
     const body = message.body;
-    // console.log(body?.contentType, body)
+    console.log('render:', body?.contentType, body)
     // 渲染 QA 回答
     if (body?.contentType === EContentType.AiseekQA) {
       const payload = body?.content?.answer?.payload ?? {};
@@ -157,6 +192,9 @@ export default ({ agent, text, message, showUser=false, textReasoning = "" }: Pr
           <View onClick={(e) => handleCopy(e, text)}>
             <IconCopy />
           </View>
+          <View onClick={(e) => handlePlay(e, text)}>
+            <IconSpeaker color={isSpeaking} />
+          </View>
           {/* <IconSpeaker></IconSpeaker> */}
           <View onClick={() => handleDislike()}>
             {isDislike ? <IconDislikeBlue /> : <IconDislike />}

+ 66 - 0
src/components/chat-message/textToSpeech.ts

@@ -0,0 +1,66 @@
+import { requestTextToSpeech } from "@/service/chat";
+import { useAudioStreamPlayer } from "@/utils/audioStreamPlayer";
+
+export const useTextToSpeech = () => {
+  // 聊天消息流式音频播报
+  const { setFistChunk, pushBase64ToQuene, playChunk, stopPlayChunk, setEndChunk, onPlayerStatusChanged } =
+    useAudioStreamPlayer();
+
+  
+  let stopCurrentChunk:any, currentReqTask:any
+  const startSpeech = (currentTextToSpeech: {
+    agentId: string,
+    loginId: string,
+    msgUk: string,
+    text: string
+  }) => {
+    if(!currentTextToSpeech){
+      return
+    }
+    let isFirstChunk = true;
+    const { reqTask, stopChunk } = requestTextToSpeech({
+      params: currentTextToSpeech,
+      onStart: () => {
+        isFirstChunk = true;
+      },
+      // 音频输出单独处理
+      onAudioParsed: (m) => {
+        const audioStr = m?.audio ?? "";
+        console.log(22222, audioStr)
+        if (isFirstChunk) {
+          isFirstChunk = false;
+          setFistChunk(audioStr, reqTask);
+        } else {
+          pushBase64ToQuene(audioStr);
+        }
+        playChunk();
+      },
+      onReceived: (m) => {},
+      onFinished: async (m) => {
+        
+      },
+      onComplete: async () => {
+        isFirstChunk = true;
+        // chunk 接收已结束
+        setEndChunk()
+      },
+      onError: () => {},
+    });
+    stopCurrentChunk = stopChunk
+    currentReqTask = reqTask
+  }
+  
+
+  const stopSpeech = ()=> {
+    stopCurrentChunk?.();
+    stopPlayChunk?.();
+  }
+
+  
+
+  return {
+    startSpeech,
+    stopSpeech,
+    onPlayerStatusChanged,
+  };
+};

+ 12 - 0
src/components/icon/IconSpeaker/index.tsx

@@ -0,0 +1,12 @@
+import { Image } from '@tarojs/components'
+import Icon from '@/images/svgs/IconSpeaker.svg'
+import IconColor from '@/images/svgs/IconSpeakerColor.svg'
+interface IProps {
+  color?: boolean
+}
+export default ({color}: IProps) => {
+  if(color){
+    return <Image src={IconColor} mode="widthFix" style={{width: '20px', height: '20px'}}></Image>
+  }
+  return <Image src={Icon} mode="widthFix" style={{width: '20px', height: '20px'}}></Image>
+}

+ 0 - 7
src/components/icon/icon-speaker/index.tsx

@@ -1,7 +0,0 @@
-import { Image } from '@tarojs/components'
-import Icon from '@/images/svgs/icon-speaker.svg'
-export default () => {
-  return (
-    <Image src={Icon} mode="widthFix" style={{width: '20px', height: '20px'}}></Image>
-  )
-}

+ 1 - 1
src/components/wemeta-tabs/index.tsx

@@ -31,7 +31,7 @@ export default function Index({
           return (
             <View
               className={
-                `${item.key === tabIndex ? style.visible : style.invisible} h-full`
+                `${item.key === tabIndex ? style.visible : style.invisible} flex flex-col h-full`
               }
             >
               {item.children}

+ 0 - 0
src/images/svgs/icon-speaker.svg → src/images/svgs/IconSpeaker.svg


+ 3 - 0
src/images/svgs/IconSpeakerColor.svg

@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M5.99706 6.70004L6.34853 6.34857L8.93431 3.76279C9.43829 3.25881 10.3 3.61575 10.3 4.32847V15.6716C10.3 16.3843 9.43829 16.7413 8.93431 16.2373L6.34853 13.6515L5.99706 13.3H5.5H4.5C4.05817 13.3 3.7 12.9419 3.7 12.5V7.50004C3.7 7.05822 4.05817 6.70004 4.5 6.70004H5.5H5.99706ZM5.5 14.5L8.08579 17.0858C9.34572 18.3458 11.5 17.4534 11.5 15.6716V4.32847C11.5 2.54666 9.34571 1.65433 8.08578 2.91426L5.5 5.50004H4.5C3.39543 5.50004 2.5 6.39547 2.5 7.50004V12.5C2.5 13.6046 3.39543 14.5 4.5 14.5H5.5ZM14.6478 6.38007C14.8112 6.0918 15.1774 5.99057 15.4657 6.15398C16.2267 6.58536 16.8612 7.20885 17.3059 7.96218C17.7505 8.71551 17.9898 9.57232 17.9997 10.447C18.0096 11.3218 17.7899 12.1838 17.3624 12.947C16.9349 13.7102 16.3147 14.3479 15.5637 14.7965C15.2792 14.9664 14.9108 14.8735 14.7409 14.589C14.571 14.3045 14.6639 13.9361 14.9484 13.7662C15.5192 13.4253 15.9906 12.9407 16.3154 12.3606C16.6403 11.7806 16.8073 11.1254 16.7998 10.4606C16.7922 9.79586 16.6104 9.14469 16.2725 8.57216C15.9345 7.99963 15.4523 7.52577 14.8739 7.19793C14.5856 7.03452 14.4844 6.66835 14.6478 6.38007ZM13.7328 8.32887C13.4446 8.16546 13.0784 8.26668 12.915 8.55496C12.7516 8.84324 12.8528 9.2094 13.1411 9.37281C13.3389 9.48497 13.5039 9.64708 13.6195 9.84294C13.7351 10.0388 13.7973 10.2616 13.7999 10.489C13.8025 10.7164 13.7454 10.9406 13.6342 11.139C13.5231 11.3374 13.3618 11.5032 13.1666 11.6199C12.8821 11.7898 12.7892 12.1581 12.9591 12.4426C13.129 12.7271 13.4974 12.82 13.7819 12.6501C14.1574 12.4258 14.4675 12.107 14.6812 11.7254C14.8949 11.3438 15.0048 10.9128 14.9998 10.4754C14.9949 10.038 14.8753 9.60963 14.6529 9.23297C14.4306 8.8563 14.1133 8.54456 13.7328 8.32887Z" fill="#317CFA"/>
+</svg>

+ 0 - 3
src/pages/chat/components/input-bar/chat.ts

@@ -1,6 +1,4 @@
 import { useEffect, useState } from "react";
-import TextInputBar from "./TextInputBar";
-import VoiceInputBar from "./VoiceInputBar";
 import { requestTextToChat } from "@/service/chat";
 import { useTextChat } from "@/store/textChat";
 import { TAgentDetail } from "@/types/agent";
@@ -12,7 +10,6 @@ import { EChatRole, EContentType } from "@/types/bot";
 import { usePostMessage, saveAgentChatContentToServer } from "./message";
 
 import { getRecommendPrompt } from "@/service/chat";
-import { useDidHide, useUnload } from "@tarojs/taro";
 
 interface Props {
   agent: TAgentDetail | null;

+ 3 - 2
src/pages/chat/index.tsx

@@ -66,7 +66,8 @@ export default function Index() {
 
   const { destroy, setScrollTop, genSessionId, setAutoScroll } = useTextChat();
   const scrollTop = useTextChat((state) => state.scrollTop);
-  // const autoScroll = useTextChat((state) => state.autoScroll);
+
+  
 
   const fetcher = async ([_url, { nextId, pageSize }]) => {
     const _nextId = nextId ? decodeURIComponent(nextId) : nextId;
@@ -134,7 +135,7 @@ export default function Index() {
     console.log("set auto scroll false");
     setAutoScroll(false);
   };
-
+  
   useDidShow(() => {
     mutate();
   });

+ 151 - 115
src/pages/contact/index.tsx

@@ -6,7 +6,7 @@ import SearchBar from "@/components/search-bar/index";
 import { useEffect, useState } from "react";
 import ContactCard from "./components/contact-card/index";
 import { getContactList, setContactToTop } from "@/service/contact";
-import { useLoadMoreInfinite } from "@/utils/loadMoreInfinite";
+import { useLoadMoreInfinite, createKey } from "@/utils/loadMoreInfinite";
 import PageCustom from "@/components/page-custom/index";
 import { TContactItem } from "@/types/contact";
 import { isSuccess } from "@/utils";
@@ -16,21 +16,16 @@ import BlurContainer from "@/components/BlurContainer";
 import SliderAction from "@/components/SliderAction";
 import PageTitle from '@/components/PageTitle'
 
+import {
+  getVoices,
+  cloneVoice as _cloneVoice,
+  
+} from "@/service/voice";
+
 export default function Index() {
   const [searchValue, setSearchValue] = useState("");
 
-  const fetcher = async ([_url, params, keyword]) => {
-    params = params || {};
-    console.log("fetcher", _url, params);
-    const res = await getContactList({
-      startId: params.nextId,
-      pageSize: params.pageSize,
-      keyword: keyword,
-    });
-    return res.data;
-  };
-  const [totalCount, setTotalCount] = useState<number>(0);
-  const getKey = (pageIndex: number, previousPageData) => {
+const getKey = (pageIndex: number, previousPageData) => {
     if (pageIndex === 0)
       return ["/my/contacts", { nextId: undefined, pageSize: 10 }, searchValue];
     if (previousPageData && previousPageData.nextId) {
@@ -42,117 +37,158 @@ export default function Index() {
     }
     return null;
   };
+  const fetcher = async ([url, {pageSize, pageIndex}, [entId]])=> {
+    console.log([url, {pageSize, pageIndex}], entId)
+    const res = await getVoices({ pageSize, pageIndex, entId})
+    return res.data;
+  }
+  // const fetcher = async ([_url, params, keyword]) => {
+  //   params = params || {};
+  //   console.log("fetcher", _url, params);
+  //   const res = await getContactList({
+  //     startId: params.nextId,
+  //     pageSize: params.pageSize,
+  //     keyword: keyword,
+  //   });
+  //   return res.data;
+  // };
   const { data, list, setSize, mutate, pageIndex } = useLoadMoreInfinite<
     TContactItem[]
-  >(getKey, fetcher);
+  >(createKey(`helloworld`, 10, [0]), fetcher);
+  // const {list, loadMore, mutate} = useLoadMoreInfinite(
+  //   getKey,
+  //   fetcher
+  // )
+  useEffect(()=> {
+    // mutate()
+    // console.log(111)
+  }, [])
 
-  const handleSearchBarChanged = (v: string) => {
-    setSearchValue(v);
-  };
+  
+  // const [totalCount, setTotalCount] = useState<number>(0);
+  // const getKey = (pageIndex: number, previousPageData) => {
+  //   if (pageIndex === 0)
+  //     return ["/my/contacts", { nextId: undefined, pageSize: 10 }, searchValue];
+  //   if (previousPageData && previousPageData.nextId) {
+  //     return [
+  //       "/my/contacts",
+  //       { pageSize: 10, nextId: previousPageData.nextId },
+  //       searchValue,
+  //     ];
+  //   }
+  //   return null;
+  // };
+  // const { data, list, setSize, mutate, pageIndex } = useLoadMoreInfinite<
+  //   TContactItem[]
+  // >(getKey, fetcher);
 
-  const handleClear = () => {
-    setSearchValue("");
-  };
+  // const handleSearchBarChanged = (v: string) => {
+  //   setSearchValue(v);
+  // };
 
-  const onScrollToUpper = () => {
-    // 加载更多数据
-    // 如果搜索框中有数据由不加载更多数据
-    if (searchValue.length) {
-      return;
-    }
-    setSize((prevSize) => prevSize + 1);
-  };
+  // const handleClear = () => {
+  //   setSearchValue("");
+  // };
 
-  const handleDelete = (contactId: string | number) => {
-    Taro.showModal({
-      content: "😭 确认删除该联系人吗?",
-      async success(result) {
-        if (result.confirm) {
-          const response = await delContact(contactId);
-          if (isSuccess(response.status)) {
-            Taro.showToast({
-              title: "删除成功",
-              icon: "none",
-            });
-            mutate();
-          }
-        }
-      },
-    });
-  };
+  // const onScrollToUpper = () => {
+  //   // 加载更多数据
+  //   // 如果搜索框中有数据由不加载更多数据
+  //   if (searchValue.length) {
+  //     return;
+  //   }
+  //   setSize((prevSize) => prevSize + 1);
+  // };
 
-  const handlePin = async (isTop: boolean, contactId: string | number) => {
-    const reseponse = await setContactToTop({
-      isTop: !isTop,
-      contactId: contactId,
-    });
-    if (isSuccess(reseponse.status)) {
-      mutate();
-    }
-  };
+  // const handleDelete = (contactId: string | number) => {
+  //   Taro.showModal({
+  //     content: "😭 确认删除该联系人吗?",
+  //     async success(result) {
+  //       if (result.confirm) {
+  //         const response = await delContact(contactId);
+  //         if (isSuccess(response.status)) {
+  //           Taro.showToast({
+  //             title: "删除成功",
+  //             icon: "none",
+  //           });
+  //           mutate();
+  //         }
+  //       }
+  //     },
+  //   });
+  // };
 
-  useDidShow(() => {
-    mutate();
-  });
-  const onLoginEnd = () => {
-    mutate();
-  };
+  // const handlePin = async (isTop: boolean, contactId: string | number) => {
+  //   const reseponse = await setContactToTop({
+  //     isTop: !isTop,
+  //     contactId: contactId,
+  //   });
+  //   if (isSuccess(reseponse.status)) {
+  //     mutate();
+  //   }
+  // };
 
-  useEffect(() => {
-    if (data && pageIndex === 1) {
-      setTotalCount(data?.[0].totalCount || 0);
-    }
-  }, [data, pageIndex]);
+  // useDidShow(() => {
+  //   mutate();
+  // });
+  // const onLoginEnd = () => {
+  //   mutate();
+  // };
 
-  const createSliderButtons = (item: TContactItem) => {
-    const onTopText = item.isTop ? (
-      <View>
-        <View>取消</View>
-        <View>置顶</View>
-      </View>
-    ) : (
-      "置顶"
-    );
-    return [
-      {
-        text: "删除",
-        color: "#FF8200",
-        onClick: () => {
-          handleDelete(item.contactId);
-        },
-      },
-      {
-        text: onTopText,
-        color: `${item.isTop ? "#FF4747" : "#327BF9"}`,
-        onClick: () => {
-          handlePin(item.isTop, item.contactId);
-        },
-      },
-    ];
-  };
+  // useEffect(() => {
+  //   if (data && pageIndex === 1) {
+  //     setTotalCount(data?.[0].totalCount || 0);
+  //   }
+  // }, [data, pageIndex]);
 
-  // 有联系人列表,或者搜索栏有值,说明是搜索结果,搜索结果有可能是 0 条记录
-  const showSearchBar = totalCount > 0 || !!searchValue.length
+  // const createSliderButtons = (item: TContactItem) => {
+  //   const onTopText = item.isTop ? (
+  //     <View>
+  //       <View>取消</View>
+  //       <View>置顶</View>
+  //     </View>
+  //   ) : (
+  //     "置顶"
+  //   );
+  //   return [
+  //     {
+  //       text: "删除",
+  //       color: "#FF8200",
+  //       onClick: () => {
+  //         handleDelete(item.contactId);
+  //       },
+  //     },
+  //     {
+  //       text: onTopText,
+  //       color: `${item.isTop ? "#FF4747" : "#327BF9"}`,
+  //       onClick: () => {
+  //         handlePin(item.isTop, item.contactId);
+  //       },
+  //     },
+  //   ];
+  // };
 
-  const renderContent = () => {
-    if (list?.length) {
-      return list.map((item) => (
-        <View className={`rounded-12 overflow-hidden truncate`}>
-          <SliderAction actions={createSliderButtons(item)}>
-            <ContactCard
-              refresh={mutate}
-              deleteable={true}
-              key={item.contactId}
-              data={item}
-              fromContact
-              className={`${item.isTop ? "bg-[#EDF1FF]" : "bg-white"}`}
-            ></ContactCard>
-          </SliderAction>
-        </View>
-      ));
-    }
-    return <EmptyData text="暂无联系人" type={"search"} />;
-  };
+  // // 有联系人列表,或者搜索栏有值,说明是搜索结果,搜索结果有可能是 0 条记录
+  // const showSearchBar = totalCount > 0 || !!searchValue.length
+
+  // const renderContent = () => {
+  //   if (list?.length) {
+  //     return list.map((item) => (
+  //       <View className={`rounded-12 overflow-hidden truncate`}>
+  //         <SliderAction actions={createSliderButtons(item)}>
+  //           <ContactCard
+  //             refresh={mutate}
+  //             deleteable={true}
+  //             key={item.contactId}
+  //             data={item}
+  //             fromContact
+  //             className={`${item.isTop ? "bg-[#EDF1FF]" : "bg-white"}`}
+  //           ></ContactCard>
+  //         </SliderAction>
+  //       </View>
+  //     ));
+  //   }
+  //   return <EmptyData text="暂无联系人" type={"search"} />;
+  // };
 
   return (
     <PageCustom fullPage isTabPage isflex>
@@ -161,7 +197,7 @@ export default function Index() {
         leftColumn={() => <PageTitle>联系人</PageTitle>}
       ></NavBarNormal>
       <View className="w-full h-full flex flex-col overflow-hidden">
-        <View className="px-16 pb-14">
+        {/* <View className="px-16 pb-14">
           {showSearchBar && (
             <SearchBar
               value={searchValue}
@@ -187,9 +223,9 @@ export default function Index() {
           </ScrollView>
           </View>
           
-        </BlurContainer>
+        </BlurContainer> */}
       </View>
-      <CheckLoginPopup onEnd={onLoginEnd}></CheckLoginPopup>
+      {/* <CheckLoginPopup onEnd={onLoginEnd}></CheckLoginPopup> */}
     </PageCustom>
   );
 }

+ 2 - 2
src/pages/knowledge/components/CompanyTab/components/ScrollListChat.tsx

@@ -22,13 +22,13 @@ import type { TKnowledgeStreamResponseData } from "@/types/knowledge";
 import { DEFAULT_AGENT } from "@/config";
 import { useLoadMoreInfinite, createKey } from "@/utils/loadMoreInfinite";
 
-export interface Iprops {
+export interface IProps {
   entId: string | number;
   assistantOnly?: boolean;
   setTotalCount: (count: number) => void;
 }
 
-const Index = ({ entId, assistantOnly, setTotalCount }: Iprops) => {
+const Index = ({ entId, assistantOnly, setTotalCount }: IProps) => {
   const [scrollTop, setScrollTop] = useState(9999);
   const { whoami } = useUserStore();
 

+ 150 - 78
src/pages/voice/components/MyVoiceList/index.tsx

@@ -1,16 +1,18 @@
+import { Image, View, ScrollView } from "@tarojs/components";
 import EmptyData from "@/components/EmptyData";
 import CardListItem from "@/components/list/card-list-item/index";
 
 import WemetaRadio from "@/components/WemetaRadio/index";
 import IconWave from "@/images/icon-wave-20.png";
 
-import { Image, View } from "@tarojs/components";
+
 import { useEffect, useRef, useState } from "react";
 import Popup from "@/components/popup/popup";
 import PopupRecorder, { ECloneStatus } from "./components/popup-recorder/index";
+import SliderAction from "@/components/SliderAction";
 import style from "./index.module.less";
 import { useVoiceStore } from "@/store/voiceStore";
-import { EVoiceStatus, TVoiceItem } from "@/types/voice";
+import { EVoiceStatus, EVoiceType, TVoiceItem } from "@/types/voice";
 import { TAgentDetail } from "@/types/agent";
 
 import { deleteVoice, getVoiceStatus } from "@/service/voice";
@@ -19,9 +21,16 @@ import { useModalStore } from "@/store/modalStore";
 import { isSuccess } from "@/utils";
 import Taro from "@tarojs/taro";
 
+import {
+  getVoices,
+  cloneVoice as _cloneVoice,  
+} from "@/service/voice";
+import { useLoadMoreInfinite, createKey } from "@/utils/loadMoreInfinite";
+
 interface Props {
   agent: TAgentDetail | null;
-  onPlay?: (voice: any) => void;
+  onPlay: (voice: any) => void;
+  onSelect: (voice: any) => void;
 }
 
 type TTask = {
@@ -30,13 +39,22 @@ type TTask = {
   taskId: string;
 };
 
-export default ({ onPlay, agent }: Props) => {
+export default ({ onPlay, onSelect, agent }: Props) => {
   const intervalRef = useRef<NodeJS.Timeout | null>(null);
   const [show, setShow] = useState(false);
-  const {showModal} = useModalStore()  
+  const { showModal } = useModalStore();
 
-  const { getVoices } = useVoiceStore();
-  const myVoices = useVoiceStore((state) => state.myVoices);
+  const fetcher = async ([url, {pageSize, pageIndex}, [entId]])=> {
+    console.log([url, {pageSize, pageIndex}], entId)
+    const res = await getVoices({ pageSize, pageIndex, entId, type: EVoiceType.MINE})
+    return res.data;
+  }
+  const { list, mutate, loadMore } = useLoadMoreInfinite<
+    TVoiceItem[]
+  >(createKey(`api/getvoices/${0}`, 10, [0]), fetcher);
+  
+  // const myVoices = useVoiceStore((state) => state.myVoices);
+  
   const voices = useVoiceStore((state) => state.voices);
 
   // {
@@ -44,59 +62,67 @@ export default ({ onPlay, agent }: Props) => {
   //   taskId: 'abc',
   //   status: 'processing'
   // }
-  const [cloning, setCloning] = useState<TTask|null>();
+  const [cloning, setCloning] = useState<TTask | null>();
 
   //获取一个默认声音
-  const getDefaultVoice = ()=> {
+  const getDefaultVoice = () => {
     // 优先使用我的声音
-    if(myVoices.length){
-      return myVoices[0]
+    if (list.length) {
+      return list[0];
     }
-    // 其次使用系统声音
-    const systemVoices = voices.filter((item)=> item.isSystem)
-    if(systemVoices.length){
-      return systemVoices[0] 
+    // // 其次使用系统声音
+    const systemVoices = voices.filter((item) => item.isSystem);
+    if (systemVoices.length) {
+      return systemVoices[0];
     }
-    return null
-  }
+    return null;
+  };
 
   // 检查需要不要改变默认声音
-  const syncDefaultVoice = (item: TVoiceItem)=> {
+  const syncDefaultVoice = (item: TVoiceItem) => {
     // 说明删除的声音是当前使用的声音
-    if(agent?.voiceId === item.voiceId){
+    if (agent?.voiceId === item.voiceId) {
       // 需要默认找一个声音
-      const defaultVoice = getDefaultVoice()
-      if(defaultVoice){
-        handleSelect(defaultVoice)
+      const defaultVoice = getDefaultVoice();
+      if (defaultVoice) {
+        handleSelect(defaultVoice);
       }
     }
-    
-  }
+  };
 
   const handleSelect = (item: TVoiceItem) => {
+    if (item.status == EVoiceStatus.DONE) {
+      if (item.voiceName) {
+        onSelect && onSelect(item);
+      }
+    }
+  };
+
+  const handleTry = (e: any, item: TVoiceItem) => {
+    e.stopPropagation();
     if (item.status == EVoiceStatus.DONE) {
       if (item.voiceName) {
         onPlay && onPlay(item);
       }
     }
   };
-  const handleLongPress = (item: TVoiceItem) => {
-    if(voices.length <= 1){
+  const handleDeleteItem = (item: TVoiceItem) => {
+    if (voices.length <= 1) {
       return;
     }
-    if(!item.canDel){
-      Taro.showToast({title: '该声音无法删除'})
+    if (!item.canDel) {
+      Taro.showToast({ title: "该声音无法删除" });
     }
     showModal({
-      content: '确认删除该声音?',
+      content: "确认删除该声音?",
       onConfirm: async () => {
-        const response = await deleteVoice(item.voiceId)
-        if(isSuccess(response.status)){
-          await getVoices();
+        const response = await deleteVoice(item.voiceId);
+        if (isSuccess(response.status)) {
+          await mutate();
           syncDefaultVoice(item);
         }
-      }
-    })
+      },
+    });
   };
 
   // 克隆按钮状态
@@ -126,7 +152,7 @@ export default ({ onPlay, agent }: Props) => {
         taskId: response.data.taskId,
       });
       stopTimer();
-      await getVoices();
+      await mutate();
       setCloning(null);
     }
   };
@@ -144,9 +170,11 @@ export default ({ onPlay, agent }: Props) => {
     }
   };
 
-  useEffect(() => {
-    getVoices();
-  }, []);
+  const onScrollToLower = ()=> {
+    loadMore()
+  }
+
+  
 
   // 清除定时器
   useEffect(() => {
@@ -155,11 +183,29 @@ export default ({ onPlay, agent }: Props) => {
     };
   }, []);
 
+  const createSliderButtons = (item: TVoiceItem) => {
+    return [
+      {
+        text: "删除",
+        color: "#FF8200",
+        onClick: () => {
+          handleDeleteItem(item);
+        },
+      },
+    ];
+  };
+
   // 克隆列表右侧操作栏
   const renderRightColumn = (item: TVoiceItem) => {
     if (item.status == EVoiceStatus.DONE) {
       return (
         <View className="flex items-center h-full">
+          <View
+            className="text-primary px-12 text-12"
+            onClick={(e) => handleTry(e, item)}
+          >
+            试听
+          </View>
           <WemetaRadio checked={item.voiceId === agent?.voiceId}></WemetaRadio>
         </View>
       );
@@ -167,7 +213,7 @@ export default ({ onPlay, agent }: Props) => {
 
     return <></>;
   };
-  
+
   // 克隆状态栏
   const renderCloneStatus = (item: TTask) => {
     if (item.status === "success") {
@@ -177,66 +223,82 @@ export default ({ onPlay, agent }: Props) => {
     }
     if (item.status === "processing") {
       return (
-        <View className={`text-12 leading-20 text-primary`}>{item.message}<ThinkingAnimation /></View>
+        <View className={`text-12 leading-20 text-primary`}>
+          {item.message}
+          <ThinkingAnimation />
+        </View>
       );
     }
 
     if (item.status === "process_fail") {
-      return <View className={`text-12 leading-20 text-orange`}>{item.message}</View>;
+      return (
+        <View className={`text-12 leading-20 text-orange`}>{item.message}</View>
+      );
     }
-    console.log(item.status)
+    console.log(item.status);
     return <></>;
   };
 
   const renderItem = (item) => {
     if (item.taskId) {
-      return <CardListItem
-        underline
-        leftRenderer={() => {
-          return (
-            <View className={style.listIcon}>
-              <Image src={IconWave} className={style.iconImage}></Image>
-            </View>
-          );
-        }}
-      >
-        <View className="flex items-center h-full py-16">{renderCloneStatus(item)}</View>
-      </CardListItem>;
+      return (
+        <CardListItem
+          underline
+          className="px-16"
+          leftRenderer={() => {
+            return (
+              <View className={style.listIcon}>
+                <Image src={IconWave} className={style.iconImage}></Image>
+              </View>
+            );
+          }}
+        >
+          <View className="flex items-center h-full py-16">
+            {renderCloneStatus(item)}
+          </View>
+        </CardListItem>
+      );
     }
 
     return (
-      <CardListItem
-        underline
-        leftRenderer={() => {
-          return (
-            <View className={style.listIcon}>
-              <Image src={IconWave} className={style.iconImage}></Image>
+      <SliderAction actions={createSliderButtons(item)}>
+        <CardListItem
+          underline
+          className="px-16"
+          leftRenderer={() => {
+            return (
+              <View className={style.listIcon}>
+                <Image src={IconWave} className={style.iconImage}></Image>
+              </View>
+            );
+          }}
+          rightRenderer={() => {
+            return renderRightColumn(item);
+          }}
+          onClick={() => handleSelect(item)}
+        >
+          <View className="flex flex-col gap-4 py-16">
+            <View className="flex items-center leading-22">
+              {item.voiceName}
             </View>
-          );
-        }}
-        rightRenderer={() => {
-          return renderRightColumn(item);
-        }}
-        onLongPress={()=> handleLongPress(item)}
-        onClick={() => handleSelect(item)}
-      >
-        <View className="flex flex-col gap-4 py-16">
-          <View className="flex items-center leading-22">{item.voiceName}</View>
-          <View className="text-12 text-gray-45 leading-20">{item.createTime.slice(0,7)} 创建</View>
-        </View>
-      </CardListItem>
+            <View className="text-12 text-gray-45 leading-20">
+              {item.createTime.slice(0, 7)} 创建
+            </View>
+          </View>
+        </CardListItem>
+      </SliderAction>
     );
   };
 
   // 渲染克隆列表
   const renderCloneList = () => {
     //
-    const voices = cloning ? [cloning, ...myVoices] : myVoices;
+    const voices = cloning ? [cloning, ...list] : list;
     if (!voices.length) {
-      return <EmptyData type={'search'}></EmptyData>;
+      return <EmptyData type={"search"}></EmptyData>;
     }
     return (
-      <View className="px-16 flex flex-col w-full">
+      <View className="flex flex-col w-full">
         {voices.map((item, _index) => {
           return renderItem(item);
         })}
@@ -245,9 +307,19 @@ export default ({ onPlay, agent }: Props) => {
   };
 
   return (
-    <View className={`flex flex-col`}>
-      {renderCloneList()}
+    <View className={`flex flex-col h-full`}>
+      <ScrollView
+        scrollY
+        onScrollToLower={onScrollToLower}
+        style={{
+          flex: 1,
+          height: "100%", // 高度自适应
+        }}
+      >
+        {renderCloneList()}
+      </ScrollView>
       
+
       <View
         className={style.addButton}
         onClick={() => {

+ 63 - 0
src/pages/voice/components/ScrollVoiceList/index.tsx

@@ -0,0 +1,63 @@
+import { ScrollView, View } from "@tarojs/components";
+import { TAgentDetail } from "@/types/agent";
+import { EVoiceStatus, EVoiceType, TGender, TVoiceItem } from "@/types/voice";
+import VoiceList from '../VoiceList'
+import {
+  getVoices,
+  cloneVoice as _cloneVoice,  
+} from "@/service/voice";
+import { useLoadMoreInfinite, createKey } from "@/utils/loadMoreInfinite";
+import { useVoiceStore } from "@/store/voiceStore";
+
+export default function Index({
+  onPlay,
+  onSelect,
+  agent,
+  key,
+  type,
+  gender,
+}: {
+  onPlay: (voice: any) => void;
+  onSelect: (voice: any) => void;
+  agent: TAgentDetail | null;
+  key: string
+  type?: EVoiceType
+  gender?: TGender
+}) {
+  
+  const {entId} = useVoiceStore()
+
+  const fetcher = async ([_url, {pageSize, pageIndex}, [entId, type, gender]])=> {
+    const res = await getVoices({ pageSize, pageIndex, entId, type, gender})
+    return res.data;
+  }
+  const { list, loadMore } = useLoadMoreInfinite<
+    TVoiceItem[]
+  >(createKey(`api/${key}/${entId}${type}${gender}`, 20, [entId, type, gender]), fetcher);  
+  const onScrollToLower = ()=> {
+    loadMore()
+  }
+  return (
+    <View className={`flex flex-col h-full`}>
+      <ScrollView
+        scrollY
+        onScrollToLower={onScrollToLower}
+        style={{
+          flex: 1,
+          height: "100%", // 高度自适应
+        }}
+      >
+        <View className="px-16 py-12">
+          <VoiceList
+          agent={agent}
+          list={list}
+          onSelect={onSelect}
+          onPlay={onPlay}
+          >
+
+          </VoiceList>
+        </View>
+      </ScrollView>
+    </View>
+  );
+}

+ 14 - 22
src/pages/voice/components/VoiceList/index.tsx

@@ -8,44 +8,33 @@ import EmptyData from "@/components/EmptyData";
 
 export default function Index({
   onPlay,
+  onSelect,
   list,
   agent,
 }: {
   onPlay: (voice: any) => void;
+  onSelect: (voice: any) => void;
   list: TVoiceItem[];
   agent: TAgentDetail | null;
 }) {
-  const initPage = async () => {
-    // console.log("profileId: ", profileId);
-    // const res = await getSysVoiceList();
-    // if (res.code === 0 && res.data) {
-    //   const r = res.data.map((item) => {
-    //     item.checked = character?.defaultSystemVoice === item.voice;
-    //     return item;
-    //   }) as ExtendedTVoiceItem[];
-    //   const checkedVoice = r.find((item) => !!item.checked);
-    //   if (checkedVoice) {
-    //     setSysVoice(checkedVoice);
-    //   }
-    //   setList(r);
-    // }
-  };
+  
 
   const handleSelect = async (voiceItem: TVoiceItem) => {
-    console.log("system voice select");
+    onSelect(voiceItem);
+  };
 
+  const handleTry = (e:any, voiceItem: TVoiceItem)=> {
+    e.stopPropagation()
     onPlay && onPlay(voiceItem);
-  };
+  }
 
-  useEffect(() => {
-    initPage();
-  }, []);
+  
 
   return (
     <View className="w-full">
       <View className="flex flex-col w-full gap-28">
         <View>
-          <View className="flex flex-col gap-12">
+          <View className="flex flex-col">
             {!list.length && <EmptyData type={'search'}></EmptyData>}
             {list.map((item: TVoiceItem, index) => {
               // 与克隆语音是否相同
@@ -55,7 +44,10 @@ export default function Index({
                   underline
                   rightRenderer={() => {
                     return (
-                      <View className="flex items-center">
+                      <View className="flex items-center" onClick={() => {
+                        handleSelect(item);
+                      }}>
+                        <View className="text-primary px-12 text-12" onClick={(e)=> handleTry(e, item)}>试听</View>
                         <WemetaRadio
                           checked={isEqualToClonedVoice}
                         ></WemetaRadio>

+ 7 - 3
src/pages/voice/index.module.less

@@ -5,6 +5,9 @@ page {
   padding: 0px 16px 16px 16px;
   position: relative;
   width: 100%;
+  display: flex;
+  flex-direction: column;
+  height: 100%;
 }
 
 .playContainer {
@@ -14,11 +17,12 @@ page {
 
 
 .voiceTab{
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  flex: 1;
   margin-top: 12px;
   border-radius: 12px;
   overflow: hidden;
   background-color: white;
 }
-.tabContent{
-  height: calc(100vh - 310px);
-}

+ 52 - 104
src/pages/voice/index.tsx

@@ -1,8 +1,8 @@
 import WemetaTabs from "@/components/wemeta-tabs/index";
 import VoicePlayerBar, { IVoicePlayerBar } from "@/components/voice-player-bar";
-import VoiceList from "./components/VoiceList";
+import ScrollVoiceList from "./components/ScrollVoiceList";
 import MyVoiceList from "./components/MyVoiceList/index";
-import { View, ScrollView } from "@tarojs/components";
+import { View } from "@tarojs/components";
 import React, { useEffect, useRef, useState } from "react";
 import NavBarNormal from "@/components/NavBarNormal/index";
 import style from "./index.module.less";
@@ -11,6 +11,7 @@ import { useVoiceStore } from "@/store/voiceStore";
 import { TVoiceItem } from "@/types/voice";
 import { useAgentStore } from "@/store/agentStore";
 
+
 interface Props {}
 
 const VoiceTabs: React.FC<Props> = ({}) => {
@@ -22,57 +23,16 @@ const VoiceTabs: React.FC<Props> = ({}) => {
   const {agent, agentCharacter, editAgentCharacter} = useAgentStore()
 
   const {
-    voices,
-    femaleVoices,
-    femalePagination,
-    maleVoices,
-    malePagination,
-    isLoading,
-    pagination,
-    setEntId,
     getVoices,
-    getFemaleVoices,
-    getMaleVoices,
-    setPagination,
-    setMalePagination,
-    setFemalePagination,
-    
   } = useVoiceStore();
 
-  
-  const handleScrollEnd = () => {
-    if (isLoading) {
-      return;
-    }
-    setPagination({ pageIndex: pagination.pageIndex + 1 });
-  };
 
-  useEffect(() => {
-    getVoices();
-  }, [agent]);
 
   // 获取女声列表
   useEffect(() => {
-    getFemaleVoices();
+    getVoices()
   }, [agent]);
 
-  // 获取男声列表
-  useEffect(() => {
-    getMaleVoices();
-  }, [agent]);
-
-  // 处理滚动加载更多
-  const handleFemaleScrollEnd = () => {
-    if (isLoading) return;
-    setFemalePagination({ pageIndex: femalePagination.pageIndex + 1 });
-  };
-
-  // 处理滚动加载更多
-  const handleMaleScrollEnd = () => {
-    if (isLoading) return;
-    setMalePagination({ pageIndex: malePagination.pageIndex + 1 });
-  };
-
   // 保存默认 voiceId
   const saveAgentVoiceId = async (voiceItem: TVoiceItem) => {
     if(!agent?.agentId || !voiceItem?.voiceId || !agentCharacter){
@@ -87,26 +47,33 @@ const VoiceTabs: React.FC<Props> = ({}) => {
 
   // 设置当前播放器的播放音频信息
   const setPlayerItem = async (voiceItem: TVoiceItem, type: "system" | "cloned") => {
+    setSysVoice(voiceItem);
+    saveAgentVoiceId(voiceItem)
+  };
+  const onPlay = async (voiceItem: TVoiceItem, type: "system" | "cloned") => {
     if(playerRef.current){
+      setSysVoice(voiceItem)
       playerRef.current.stop();
       playerRef.current.play(voiceItem.voiceUrl)
     }
-    setSysVoice(voiceItem);
-    saveAgentVoiceId(voiceItem)
   };
 
+
+
   const tabList = [
     {
       label: "我的",
       key: "1",
       children: (
-        <View className={style.tabContent}>
+        <View className="h-full">
           <MyVoiceList
           agent={agent}
-          onPlay={(item) => {
-            console.log(item,'cloned')
+          onSelect={(item) => {
             setPlayerItem(item, "cloned");
           }}
+          onPlay={(item) => {
+            onPlay(item, "cloned");
+          }}
           ></MyVoiceList>
         </View>
       ),
@@ -115,26 +82,19 @@ const VoiceTabs: React.FC<Props> = ({}) => {
       label: "全部",
       key: "2",
       children: (
-        <View className={style.tabContent}>
-          <ScrollView
-            scrollY
-            id="scrollView"
-            onScrollEnd={handleScrollEnd}
-            style={{
-              flex: 1,
-              height: "100%", // 高度自适应
+        <View className="h-full">
+          <ScrollVoiceList
+            agent={agent}
+            key="ScrollVoiceList"
+            onSelect={(item) => {
+              setPlayerItem(item, "system");
+            }}
+            onPlay={(item) => {
+              onPlay(item, "system");
             }}
           >
-            <View className="px-16 py-12">
-              <VoiceList
-                agent={agent}
-                list={voices}
-                onPlay={(item) => {
-                  setPlayerItem(item, "system");
-                }}
-              ></VoiceList>
-            </View>
-          </ScrollView>
+
+          </ScrollVoiceList>
         </View>
       ),
     },
@@ -142,26 +102,20 @@ const VoiceTabs: React.FC<Props> = ({}) => {
       label: "女声",
       key: "3",
       children: (
-        <View className={style.tabContent}>
-          <ScrollView
-            scrollY
-            id="scrollView"
-            onScrollEnd={handleFemaleScrollEnd}
-            style={{
-              flex: 1,
-              height: "100%", // 高度自适应
+        <View className="h-full">
+          <ScrollVoiceList
+            agent={agent}
+            key="femaleVoice"
+            gender="female"
+            onSelect={(item) => {
+              setPlayerItem(item, "system");
+            }}
+            onPlay={(item) => {
+              onPlay(item, "system");
             }}
           >
-            <View className="px-16 py-12">
-              <VoiceList
-                agent={agent}
-                list={femaleVoices}
-                onPlay={(item) => {
-                  setPlayerItem(item, "system");
-                }}
-              ></VoiceList>
-            </View>
-          </ScrollView>
+
+          </ScrollVoiceList>
         </View>
       ),
     },
@@ -169,25 +123,20 @@ const VoiceTabs: React.FC<Props> = ({}) => {
       label: "男声",
       key: "4",
       children: (
-        <View className={style.tabContent}>
-          <ScrollView
-            scrollY
-            onScrollEnd={handleMaleScrollEnd}
-            style={{
-              flex: 1,
-              height: "100%", // 高度自适应
+        <View className="h-full">
+          <ScrollVoiceList
+            agent={agent}
+            key="maleVoice"
+            gender="male"
+            onSelect={(item) => {
+              setPlayerItem(item, "system");
+            }}
+            onPlay={(item) => {
+              onPlay(item, "system");
             }}
           >
-            <View className="px-16 py-12">
-              <VoiceList
-                agent={agent}
-                list={maleVoices}
-                onPlay={(item) => {
-                  setPlayerItem(item, "system");
-                }}
-              ></VoiceList>
-            </View>
-          </ScrollView>
+
+          </ScrollVoiceList>
         </View>
       ),
     },
@@ -196,7 +145,7 @@ const VoiceTabs: React.FC<Props> = ({}) => {
   
 
   return (
-    <PageCustom>
+    <PageCustom fullPage isflex >
       <NavBarNormal backText="声音"></NavBarNormal>
       <View className={style.container}>
         <View className={style.playContainer}>
@@ -207,7 +156,6 @@ const VoiceTabs: React.FC<Props> = ({}) => {
         </View>
         <View className={style.voiceTab}>
           <WemetaTabs
-            full
             list={tabList}
             current="1"
             tabStyle="outline"

+ 72 - 0
src/service/chat.ts

@@ -71,6 +71,78 @@ export const speechToText = async (agentId: string, tempFilePath: string) => {
 }
 
 
+export type TTextToSpeechParams = {
+  params: {
+    agentId: string,
+    loginId: string,
+    msgUk: string,
+    text: string
+  };
+  onStart: () => void;
+  onReceived: (m: ICompleteCallback) => void;
+  onAudioParsed: (m: {audio: string}) => void;
+  onFinished: (m: ICompleteCallback) => void;
+  onComplete?: () => void; // 无论失败或成功都会执行
+  onError: () => void;
+};
+
+export const requestTextToSpeech = ({
+  params,
+  onStart,
+  onReceived,
+  onAudioParsed,
+  onFinished,
+  onComplete,
+  onError,
+}: TTextToSpeechParams) => {
+  onStart();
+
+  let reqTask: Taro.RequestTask<any>|null = null;
+  const jsonParser = new JsonChunkParser();
+  jsonParser.onParseComplete((m) => {
+    onFinished(m);
+  });
+  
+  const onChunkReceived = (chunk: any) => {
+    const uint8Array = new Uint8Array(chunk.data);
+    var string = new TextDecoder("utf-8").decode(uint8Array);
+    jsonParser.parseChunk(string, onReceived, onAudioParsed);
+  };
+  const header = getSimpleHeader()
+  
+  try {
+    const url = `${bluebookAiAgent}api/v1/chat/text/speech`;
+    reqTask = Taro.request({
+      url: url,
+      data: params,
+      enableChunked: true,
+      method: "POST",
+      header: {
+        ...header
+      },
+      responseType: "arraybuffer",
+      success: function (res) {
+        console.log("text to speech 服务端响应 >>", res);
+        onComplete?.()
+      },
+      complete: function(res) {
+        onComplete?.()
+      }
+    });
+
+    // reqTask.
+    reqTask.onChunkReceived(onChunkReceived);
+  } catch (e) {
+    onComplete?.()
+    onError();
+  }
+
+  const stopChunk = () => {
+    reqTask?.offChunkReceived(onChunkReceived);
+  };
+
+  return {reqTask: reqTask, stopChunk};
+};
 
 
 

+ 2 - 1
src/service/voice.ts

@@ -28,7 +28,8 @@ export const getVoiceStatus = (taskId: string) => {
 
 // 获取个人录音音色库
 export const getVoices = (data:TGetMyVoicesParams) => {
-  return request.get<TPaginatedVoiceResponse>(`${bluebookAiAgent}api/v1/my/voices/`, {
+  console.log(data,11111)
+  return request.get(`${bluebookAiAgent}api/v1/my/voices/`, {
     params: data
   })
 }

+ 12 - 0
src/store/textChat.ts

@@ -15,6 +15,12 @@ type TRobotMessageWithOptionalId = Omit<TRobotMessage, "msgUk"> & {
   reasoningContent?: string; // 添加 reasoningContent
 };
 
+type TCurrentTextToSpeech = {
+  agentId: string,
+  loginId: string,
+  msgUk: string,
+  text: string
+}
 
 const INIT_CURRENT_ROBOT_MSG_UK = ''
 
@@ -25,6 +31,7 @@ export interface TextChat {
   list: TAnyMessage[];
   questions: string[]; //推荐问题
   sessionId: string|null
+  messageRespeaking: boolean; // 当前正在重放回复消息内容 text to speech
   // 显示聊天历史
   setAutoScroll: (b: boolean) => void // 是否自动滚动
   genSessionId: () => void // 进入聊天界面后,本次的 sessionId
@@ -44,6 +51,7 @@ export interface TextChat {
   // 清空
   destroy: () => void;
   fetchMessageHistories: (data: TGetMessageHistoriesParams) => void
+  setMessageRespeaking: (respeaking: boolean)=> void
 }
 
 // 新messageId 为 index 加 1
@@ -58,6 +66,10 @@ export const useTextChat = create<TextChat>((set, get) => ({
   list: [],
   sessionId: null,
   questions: [],
+  messageRespeaking: false,
+  setMessageRespeaking: (respeaking)=> {
+    set({messageRespeaking: respeaking})
+  },
   setAutoScroll: (b)=> {
     set({autoScroll: b})
   },

+ 6 - 96
src/store/voiceStore.ts

@@ -1,6 +1,6 @@
 import { create } from "zustand";
 
-import { TGetMyVoicesParams, TVoiceItem, TPageination, TGender } from '@/types/voice'
+import { TGetMyVoicesParams, TVoiceItem, } from '@/types/voice'
 
 import {
   getVoices as _getVoices,
@@ -16,19 +16,8 @@ export interface StorageStoreState {
   entId: number|string
   voices: TVoiceItem[];
   myVoices: TVoiceItem[];
-  maleVoices: TVoiceItem[];
-  femaleVoices: TVoiceItem[];
-  pagination: TPageination;
-  malePagination: TPageination;
-  femalePagination: TPageination;
-  isLoading: boolean,
   setEntId: (entId: number|string)=> void
   getVoices: (params?: TVoiceRequestParam) => Promise<boolean>;
-  getMaleVoices: (params?: TVoiceRequestParam) => Promise<boolean>;
-  getFemaleVoices: (params?: TVoiceRequestParam) => Promise<boolean>;
-  setPagination: (params:any) => Promise<void>;
-  setMalePagination: (params:any) => Promise<void>;
-  setFemalePagination: (params:any) => Promise<void>;
   cloneVoice: (params: {
     sourceUrl: string // 源语音地址 ,
     voiceText?: string // 录音文案
@@ -39,8 +28,6 @@ export const useVoiceStore = create<StorageStoreState>((set, get) => ({
   entId: 0,
   voices: [],
   myVoices: [],
-  maleVoices: [],
-  femaleVoices: [],
   pagination: {
     pageIndex: 1,
     pageSize: 10,
@@ -48,100 +35,23 @@ export const useVoiceStore = create<StorageStoreState>((set, get) => ({
     searchKey: '',
     extData: null,
   },
-  malePagination: {
-    pageIndex: 1,
-    pageSize: 10,
-    totalCount: 0,
-    searchKey: '',
-    extData: null,
-  },
-  femalePagination: {
-    pageIndex: 1,
-    pageSize: 10,
-    totalCount: 0,
-    searchKey: '',
-    extData: null,
-  },
-  isLoading: false,
   setEntId: (entId)=> {
     set({entId: entId})
   },
+  // 获取第一页全部声音
   getVoices: async (params = {}) => {
-    set({ isLoading: true });
-    console.log(get().entId, 8888)
-    // 合并当前分页参数和传入参数
-    const currentPagination = get().pagination;
-    const requestParams = { ...currentPagination, entId: get().entId, ...params };
-
+    const requestParams = {
+      pageIndex: 1,
+      pageSize: 10,
+      entId: get().entId, ...params };
     const response = await _getVoices(requestParams)
 
-    // todo: 我的克隆声音后期需要单独,现只是从全部列表中过滤出
     // 更新状态
     set({
       voices: response.data.data,
-      myVoices: response.data.data.filter(item => !item.isSystem).reverse(),
-      pagination: {
-        ...requestParams,
-        totalCount: response.data.totalCount,
-      },
-      isLoading: false
-    });
-    return isSuccess(response.status)
-  },
-  getMaleVoices: async (params = {}) => {
-    set({ isLoading: true });
-    const currentPagination = get().malePagination;
-    const requestParams = { ...currentPagination, entId: get().entId,  ...params, gender: 'male' as TGender };
-
-    const response = await _getVoices(requestParams)
-
-    set({
-      maleVoices: response.data.data,
-      malePagination: {
-        ...requestParams,
-        totalCount: response.data.totalCount,
-      },
-      isLoading: false
     });
     return isSuccess(response.status)
   },
-  getFemaleVoices: async (params = {}) => {
-    set({ isLoading: true });
-    const currentPagination = get().femalePagination;
-    const requestParams = { ...currentPagination, entId: get().entId,  ...params, gender: 'female' as TGender };
-
-    const response = await _getVoices(requestParams)
-
-    set({
-      femaleVoices: response.data.data,
-      femalePagination: {
-        ...requestParams,
-        totalCount: response.data.totalCount,
-      },
-      isLoading: false
-    });
-    return isSuccess(response.status)
-  },
-  // 更新分页参数
-  setPagination: async (params:any) => {
-    set((state) => ({
-      pagination: { ...state.pagination, ...params },
-    }));
-    // 可选:自动触发请求
-    get().getVoices();
-  },
-  setMalePagination: async (params:any) => {
-    set((state) => ({
-      malePagination: { ...state.malePagination, ...params },
-    }));
-    get().getMaleVoices();
-  },
-  setFemalePagination: async (params:any) => {
-    set((state) => ({
-      femalePagination: { ...state.femalePagination, ...params },
-    }));
-    get().getFemaleVoices();
-  },
   cloneVoice: async (params) => {
     await _cloneVoice(params)
   }

+ 24 - 2
src/utils/audioStreamPlayer.ts

@@ -41,17 +41,23 @@ let chunks: ArrayBuffer[] = []; // 流式播放 chunks
 let requestTask = null;
 // let audioBase64 = ''
 const WAV_HEADER_LENGTH = 44; // wav 头长度
-
+export type  TPlayStatus = 'playing'|'stop'
+let playStatusChangedCallback:(status: TPlayStatus)=>void;
 export const useAudioStreamPlayer = () => {
   let totalLength = WAV_HEADER_LENGTH;
   let playing = false;
   let wavHeader: ArrayBuffer;
+  let endChunk = false;
+  const changeStatus = (status: TPlayStatus)=> {
+    playStatusChangedCallback && playStatusChangedCallback(status)
+  }
 
   const setFistChunk = (base64Str: string, _requestTask?: any) => {
     if (_requestTask) {
       requestTask = _requestTask;
     }
     enablePlay = true;
+    endChunk = false
     // audioBase64 = base64Str;
     let chunk = decode(base64Str);
     emptyQuene();
@@ -59,6 +65,7 @@ export const useAudioStreamPlayer = () => {
     wavHeader = chunk.slice(0, WAV_HEADER_LENGTH);
     const firstChunkData = chunk.slice(WAV_HEADER_LENGTH);
     pushChunk2Quene(firstChunkData);
+    changeStatus('playing')
   };
 
   const pushBase64ToQuene = (base64Str: string) => {
@@ -72,6 +79,11 @@ export const useAudioStreamPlayer = () => {
     totalLength += chunk.byteLength;
   };
 
+  // 标定是否为最后一个 chunk 
+  const setEndChunk = () => {
+    endChunk = true;
+  }
+
   const emptyQuene = () => {
     chunks = [];
     // audioBase64 = ''
@@ -108,7 +120,11 @@ export const useAudioStreamPlayer = () => {
         source.onended = () => {
           console.info("play end");
           playing = false;
-          playChunk();
+          if(!chunks.length && endChunk){
+            changeStatus('stop')
+          }else{
+            playChunk();
+          }          
           // console.log('finally', audioBase64)
         };
         source.start(0);
@@ -119,6 +135,9 @@ export const useAudioStreamPlayer = () => {
       }
     );
   };
+  const onPlayerStatusChanged = (callback: (status:  TPlayStatus)=>void) => {
+    playStatusChangedCallback = callback
+  }
 
   const stopPlayChunk = () => {
     // 如果有请求任务取消
@@ -132,13 +151,16 @@ export const useAudioStreamPlayer = () => {
     emptyQuene();
 
     enablePlay = false;
+    changeStatus('stop')
   };
 
   return {
     pushChunk2Quene,
     pushBase64ToQuene,
+    setEndChunk,
     playChunk,
     stopPlayChunk,
     setFistChunk,
+    onPlayerStatusChanged,
   };
 };

+ 12 - 3
src/utils/jsonChunkParser.ts

@@ -23,6 +23,11 @@ data: data:{"content":"","contentType":"text/plain","last":true,"payload":{"usag
 
 data:{"content":{"answer":{"payload":{},"text":"你好,我是饭饭,很高兴你能使用QA"},"qaId":"qa_9605"},"contentType":"aiseek/qa","last":true,"payload":{},"role":"assistant"}
 
+data:{"content":{"audio": "", sentenceBegin: ""},"contentType":"aiseek/audio_chunk","last":true,"payload":{},"role":"assistant"}
+
+// 仅是文本转音频流时的结构
+data:{"audio": ""}
+
 
 
 
@@ -36,11 +41,12 @@ export interface ICompleteCallback {
   body: null| (TJsonMessage & Record<string,any>)
 }
 export interface IAudioPared {
-  body: null| (TJsonMessage & Record<string,any>)
+  body?: null| (TJsonMessage & Record<string,any>)
+  audio?:string
 }
 
 type TContentType = "text/plain" | "aiseek/qa" | 'application/json' | 'aiseek/audio_chunk' | 'aiseek/thinking'  |  'aiseek/function_call' | 'aiseek/multimodal'
-type TJsonMessage = {contentType: TContentType, content: string | any, reasoningContent: string}
+type TJsonMessage = {contentType: TContentType, content: string | any, reasoningContent: string, audio?: string} 
 
 
 export default class JsonChunkParser {
@@ -93,7 +99,7 @@ export default class JsonChunkParser {
             const jsonStr = line.substring(5).trim(); // 移除 "data:" 前缀并去除空格
             const json: TJsonMessage = JSON.parse(jsonStr);
             receivedJsonBody = json
-            // console.log(11111, receivedJsonBody)
+            console.log(11111, receivedJsonBody)
             // 文本回复
             if(json.contentType === "text/plain") {
               if (json.content) {
@@ -121,6 +127,9 @@ export default class JsonChunkParser {
                 onAudioParsed({body: json  }); // 合并并输出
               }
             }
+            else if(json.audio?.length){
+              onAudioParsed(json); // 音频流 base64 片断
+            }
           }
         } catch (error) {
           // 如果解析失败,说明当前行不是完整的 JSON(通常是最后一行)

+ 1 - 0
src/utils/loadMoreInfinite.ts

@@ -11,6 +11,7 @@ export type TResponseData<D> = {
 
 export const createKey = (query: string, pageSize: number = 10, extra: Record<string, any> = []) => {
   return (pageIndex: number, previousPageData) => {
+    // console.log(query, pageSize, extra, pageIndex, previousPageData)
     // previousPageData 接口返回的数据结果
     // pageIndex 后端要求是从 1 开始
     if (pageIndex === 0)