|
@@ -1,33 +1,30 @@
|
|
|
import { View, ScrollView } from "@tarojs/components";
|
|
|
import NavBarNormal from "@/components/NavBarNormal/index";
|
|
|
import PageCustom from "@/components/page-custom/index";
|
|
|
-import Taro, { useDidShow, useRouter, useUnload } from "@tarojs/taro";
|
|
|
+import { useDidShow, useRouter, useUnload } from "@tarojs/taro";
|
|
|
import ChatMessage from "@/components/chat-message";
|
|
|
-import InputBar from "./components/input-bar";
|
|
|
-import { useEffect, useState, useRef, useMemo } from "react";
|
|
|
-import { useTextChat } from "@/store/textChat";
|
|
|
+import InputBar from "./components/InputBar";
|
|
|
+import { useEffect, useState } from "react";
|
|
|
+import { useTextChat } from "@/store/textChatStore";
|
|
|
import { formatMessageTime } from "@/utils/timeUtils";
|
|
|
-import { formatMessageItem } from "@/utils/messageUtils";
|
|
|
-import {
|
|
|
- TRobotMessage,
|
|
|
- TMessage,
|
|
|
- TAnyMessage,
|
|
|
-} from "@/types/bot";
|
|
|
|
|
|
import ChatGreeting from "./components/ChatGreeting";
|
|
|
import IconArrowLeftWhite24 from "@/components/icon/IconArrowLeftWhite24";
|
|
|
import IconArrowLeft from "@/components/icon/icon-arrow-left";
|
|
|
-import PersonalCard from "./components/personal-card";
|
|
|
+import PersonalCard from "./components/PersonalCard";
|
|
|
import ButtonEnableStreamVoice from "./components/OptionButtons/ButtonEnableStreamVoice";
|
|
|
-import { useAgentStore } from "@/store/agentStore";
|
|
|
-
|
|
|
-import { useLoadMoreInfinite, createKey } from "@/utils/loadMoreInfinite";
|
|
|
-import { getMessageHistories } from "@/service/chat";
|
|
|
|
|
|
import RecommendQuestions from "./components/RecommendQuestions";
|
|
|
-import { useKeyboard } from "./components/keyboard";
|
|
|
-import { useAppStore } from "@/store/appStore";
|
|
|
-import { usePersistentState } from '@/hooks/usePersistentState';
|
|
|
+import { usePersistentState } from "@/hooks/usePersistentState";
|
|
|
+
|
|
|
+// 导入我们抽离的 hooks 和常量
|
|
|
+import {
|
|
|
+ useChatMessages,
|
|
|
+ useChatScrollManager,
|
|
|
+ useChatUI,
|
|
|
+ useChatAgent,
|
|
|
+} from "./hooks";
|
|
|
+import { useChatInput } from "./components/InputBar/useChatInput";
|
|
|
|
|
|
export default function Index() {
|
|
|
const router = useRouter();
|
|
@@ -37,217 +34,84 @@ export default function Index() {
|
|
|
return <View>没有相应的智能体</View>;
|
|
|
}
|
|
|
|
|
|
- const { fetchAgent, fetchAgentProfile } = useAgentStore();
|
|
|
-
|
|
|
- const bottomSafeHeight = useAppStore( state => state.bottomSafeHeight)
|
|
|
-
|
|
|
- const agent = useAgentStore((state) => {
|
|
|
- if (isVisitor === "true") {
|
|
|
- return state.agentProfile;
|
|
|
- }
|
|
|
- return state.agent;
|
|
|
- });
|
|
|
- const scrollViewRef = useRef<any>(null);
|
|
|
- const messageList = useTextChat((state) => state.list);
|
|
|
- const autoScroll = useTextChat((state) => state.autoScroll);
|
|
|
- const [inputContainerHeight,setInputContainerHeight]= useState(0)
|
|
|
- const [streamVoiceEnable, setStreamVoiceEnable]= usePersistentState('streamVoiceEnable', false)
|
|
|
-
|
|
|
- const { keyboardHeight, marginTopOffset, triggerHeightUpdate } = useKeyboard(
|
|
|
+ // 使用抽离的 hooks
|
|
|
+ const { agent } = useChatAgent(agentId, isVisitor);
|
|
|
+
|
|
|
+ const {
|
|
|
+ historyList,
|
|
|
+ groupedMessages,
|
|
|
+ messagesLength,
|
|
|
+ loadMore,
|
|
|
+ pageIndex,
|
|
|
+ mutate,
|
|
|
+ } = useChatMessages(agentId, isVisitor);
|
|
|
+
|
|
|
+ // 获取原始历史消息列表长度(用于UI状态判断)
|
|
|
+ const rawHistoryListLength = historyList.length;
|
|
|
+
|
|
|
+ const {
|
|
|
scrollViewRef,
|
|
|
- "#messageList",
|
|
|
- "#scrollView"
|
|
|
+ scrollTop,
|
|
|
+ keyboardHeight,
|
|
|
+ marginTopOffset,
|
|
|
+ handleScrollToUpper,
|
|
|
+ handleTouchMove,
|
|
|
+ handleScrollToLower,
|
|
|
+ } = useChatScrollManager(messagesLength, pageIndex, loadMore);
|
|
|
+
|
|
|
+ // 获取当前消息列表长度
|
|
|
+ const currentMessageListLength = useTextChat((state) => state.list.length);
|
|
|
+
|
|
|
+ const {
|
|
|
+ showWelcome,
|
|
|
+ haveBg,
|
|
|
+ inputContainerHeight,
|
|
|
+ inputContainerBottomOffset,
|
|
|
+ setShowWelcome,
|
|
|
+ getBgContent,
|
|
|
+ createNavLeftRenderer,
|
|
|
+ } = useChatUI(agent, rawHistoryListLength, currentMessageListLength);
|
|
|
+
|
|
|
+ const [streamVoiceEnable, setStreamVoiceEnable] = usePersistentState(
|
|
|
+ "streamVoiceEnable",
|
|
|
+ false
|
|
|
);
|
|
|
|
|
|
- // 输入框容器
|
|
|
- // 针对没有 safeArea?.bottom 的手机,需要额外增加 12 高度
|
|
|
- let inputContainerBottomOffset = 0
|
|
|
- if(bottomSafeHeight <= 0){
|
|
|
- inputContainerBottomOffset = 12
|
|
|
- }
|
|
|
+ // InputBar 相关状态
|
|
|
+ const [isVoice, setIsVoice] = useState(false);
|
|
|
+ const [disabled, setDisabled] = useState(false);
|
|
|
|
|
|
- const { destroy, setScrollTop, genSessionId, setAutoScroll } = useTextChat();
|
|
|
- const scrollTop = useTextChat((state) => state.scrollTop);
|
|
|
+ const { destroy, genSessionId } = useTextChat();
|
|
|
|
|
|
- // 放在组件里
|
|
|
-const initialScrolledRef = useRef(false);
|
|
|
-
|
|
|
- const fetcher = async ([_url, { nextId, pageSize }]) => {
|
|
|
- const _nextId = nextId ? decodeURIComponent(nextId) : nextId;
|
|
|
- const res = await getMessageHistories({
|
|
|
- agentId,
|
|
|
- startId: _nextId,
|
|
|
- pageSize,
|
|
|
- });
|
|
|
- return res.data;
|
|
|
- };
|
|
|
-
|
|
|
- // 获取历史聊天记录
|
|
|
- const { list, loadMore, pageIndex, mutate } = useLoadMoreInfinite<
|
|
|
- TMessage[] | TRobotMessage[]
|
|
|
- >(createKey(`messeagehistories${isVisitor}${agentId}`), fetcher);
|
|
|
-
|
|
|
- // 解析消息体 content
|
|
|
- const parsedList = list.filter((item)=> !!item.content.length).map(formatMessageItem);
|
|
|
- // 1. 按 sessionId 分组,并记录每组的最早时间
|
|
|
- const resultMap = useMemo(() => {
|
|
|
- const allMessages = [...[...parsedList].reverse(), ...messageList];
|
|
|
- return allMessages.reduce((acc, item) => {
|
|
|
- const { sessionId, msgTime } = item;
|
|
|
- if (!sessionId || !msgTime) {
|
|
|
- return acc;
|
|
|
- }
|
|
|
- let _msgTime = msgTime.replace(/\-/g, "/");
|
|
|
- if (!acc[sessionId]) {
|
|
|
- acc[sessionId] = {
|
|
|
- dt: _msgTime, // 初始化当前组的最早时间
|
|
|
- list: [item], // 初始化当前组的记录列表
|
|
|
- };
|
|
|
- } else {
|
|
|
- // 更新最早时间(如果当前记录的 msgTime 更早)
|
|
|
- if (new Date(_msgTime) < new Date(acc[sessionId].dt)) {
|
|
|
- acc[sessionId].dt = msgTime;
|
|
|
- console.log('yoyoyo')
|
|
|
- }
|
|
|
- // 将记录添加到当前组
|
|
|
- acc[sessionId].list.push(item);
|
|
|
- }
|
|
|
- return acc;
|
|
|
- }, {}) as {
|
|
|
- dt: string;
|
|
|
- list: TAnyMessage[];
|
|
|
- }[];
|
|
|
- }, [parsedList, messageList]);
|
|
|
-
|
|
|
- // 2. 转换为最终数组格式
|
|
|
- const result = Object.values(resultMap);
|
|
|
-
|
|
|
- const messagesLength = useMemo(() => result.length, [result.length]);
|
|
|
- const prevLengthRef = useRef(messagesLength);
|
|
|
-
|
|
|
- const [showWelcome, setShowWelcome] = useState(!list.length);
|
|
|
- const haveBg = (!!agent?.enabledChatBg && !!agent.avatarUrl?.length)
|
|
|
-
|
|
|
- // 加载更多
|
|
|
- const onScrollToUpper = () => {
|
|
|
- console.log("onscroll");
|
|
|
- loadMore();
|
|
|
- };
|
|
|
+ // 统一的聊天输入逻辑 - 只在主组件中实例化一次
|
|
|
+ const chatInputActions = useChatInput({
|
|
|
+ agent,
|
|
|
+ enableOutputAudioStream: streamVoiceEnable,
|
|
|
+ setShowWelcome,
|
|
|
+ setIsVoice,
|
|
|
+ setDisabled,
|
|
|
+ });
|
|
|
|
|
|
- const handleTouchMove = () => {
|
|
|
- console.log("set auto scroll false");
|
|
|
- setAutoScroll(false);
|
|
|
- };
|
|
|
-
|
|
|
+ // 页面显示时刷新数据
|
|
|
useDidShow(() => {
|
|
|
mutate();
|
|
|
});
|
|
|
|
|
|
- useEffect(() => {
|
|
|
- if (agentId) {
|
|
|
- if (isVisitor) {
|
|
|
- fetchAgentProfile(agentId);
|
|
|
- } else {
|
|
|
- fetchAgent(agentId);
|
|
|
- }
|
|
|
- }
|
|
|
- }, [agentId, isVisitor]);
|
|
|
-
|
|
|
- // 是否显示欢迎 ui
|
|
|
- useEffect(() => {
|
|
|
- setShowWelcome(!messageList.length && !list.length);
|
|
|
- }, [list, messageList]);
|
|
|
-
|
|
|
- // 首次进入界面滚动到底
|
|
|
- // 已有:messagesLength 是一个 number(来源于 result.length)
|
|
|
- useEffect(() => {
|
|
|
- // 仅首次 pageIndex === 1 且已经有消息时滚动一次
|
|
|
- if (pageIndex === 1 && messagesLength > 0 && !initialScrolledRef.current) {
|
|
|
- initialScrolledRef.current = true;
|
|
|
-
|
|
|
- // 下1秒再滚,确保 DOM 已完成渲染
|
|
|
- setTimeout(() => {
|
|
|
- setScrollTop()
|
|
|
- console.log("首次进入,滚动到底部");
|
|
|
- }, 1000);
|
|
|
- }
|
|
|
-}, [pageIndex, messagesLength]);
|
|
|
-
|
|
|
// 首次进入聊天生成 session id
|
|
|
useEffect(() => {
|
|
|
genSessionId();
|
|
|
- }, []);
|
|
|
-
|
|
|
- // 监听消息列表变化,触发键盘高度重新计算
|
|
|
- useEffect(() => {
|
|
|
- // 只在长度真正变化时才触发
|
|
|
- if (prevLengthRef.current !== messagesLength) {
|
|
|
- prevLengthRef.current = messagesLength;
|
|
|
-
|
|
|
- // 使用 setTimeout 确保 DOM 更新完成后再计算高度
|
|
|
- const timer = setTimeout(() => {
|
|
|
- triggerHeightUpdate();
|
|
|
- }, 100);
|
|
|
-
|
|
|
- return () => clearTimeout(timer);
|
|
|
- }
|
|
|
- }, [messagesLength, triggerHeightUpdate]);
|
|
|
+ }, [genSessionId]);
|
|
|
|
|
|
+ // 页面卸载时清理
|
|
|
useUnload(() => {
|
|
|
destroy();
|
|
|
});
|
|
|
|
|
|
- const renderNavLeft = () => {
|
|
|
-
|
|
|
- return (
|
|
|
- <View
|
|
|
- className="flex items-center gap-8"
|
|
|
- onClick={() => Taro.navigateBack()}
|
|
|
- >
|
|
|
- {haveBg ? <IconArrowLeftWhite24 /> : <IconArrowLeft />}
|
|
|
-
|
|
|
- <PersonalCard agent={agent} haveBg={haveBg} />
|
|
|
- </View>
|
|
|
- );
|
|
|
- };
|
|
|
-
|
|
|
- // 大背景可以是视频,也可以是图片
|
|
|
- const getBgContent = () => {
|
|
|
- if (!agent?.avatarUrl || !!!agent?.enabledChatBg) {
|
|
|
- return "";
|
|
|
- }
|
|
|
- return agent?.avatarUrl;
|
|
|
- };
|
|
|
-
|
|
|
- useEffect(() => {
|
|
|
- if(haveBg){
|
|
|
- Taro.setNavigationBarColor({
|
|
|
- frontColor: "#ffffff",
|
|
|
- backgroundColor: "transparent",
|
|
|
- });
|
|
|
- }
|
|
|
- return () => {
|
|
|
- Taro.setNavigationBarColor({
|
|
|
- frontColor: "#000000",
|
|
|
- backgroundColor: "transparent",
|
|
|
- });
|
|
|
- };
|
|
|
- }, [haveBg]);
|
|
|
+ // 加载更多的处理函数(已经在 useChatScrollManager 中处理)
|
|
|
+ const onScrollToUpper = handleScrollToUpper;
|
|
|
|
|
|
- useEffect(()=> {
|
|
|
- const query = Taro.createSelectorQuery();
|
|
|
- // 输入框高度
|
|
|
- query
|
|
|
- .select('#inputContainer')
|
|
|
- .boundingClientRect((rect: any) => {
|
|
|
- if (rect) {
|
|
|
-
|
|
|
- // console.log('ScrollView height:', rect.height)
|
|
|
- setInputContainerHeight(rect.height - bottomSafeHeight);
|
|
|
- }
|
|
|
- })
|
|
|
- .exec();
|
|
|
- }, [agent])
|
|
|
+ // 使用工厂函数创建导航栏左侧渲染器
|
|
|
+ const renderNavLeft = createNavLeftRenderer(PersonalCard, IconArrowLeftWhite24, IconArrowLeft);
|
|
|
|
|
|
return (
|
|
|
<PageCustom
|
|
@@ -273,18 +137,24 @@ const initialScrolledRef = useRef(false);
|
|
|
scrollTop={scrollTop}
|
|
|
scrollWithAnimation
|
|
|
onScrollToUpper={onScrollToUpper}
|
|
|
- onScrollToLower={() => setAutoScroll(true)}
|
|
|
+ onScrollToLower={handleScrollToLower}
|
|
|
>
|
|
|
<View
|
|
|
id="messageList"
|
|
|
className="flex flex-col gap-16 px-18"
|
|
|
onTouchMove={handleTouchMove}
|
|
|
>
|
|
|
- {showWelcome && <ChatGreeting agent={agent} />}
|
|
|
- {result.map((group) => {
|
|
|
+ {showWelcome && <ChatGreeting agent={agent} chatInputActions={chatInputActions} />}
|
|
|
+ {groupedMessages.map((group, groupIndex) => {
|
|
|
return (
|
|
|
- <>
|
|
|
- <View className={`text-12 leading-20 block text-center w-full ${(agent?.enabledChatBg && !!agent?.avatarUrl?.length) ? 'text-white-70' : 'text-black-25'}`}>
|
|
|
+ <View key={groupIndex} className="flex flex-col gap-16">
|
|
|
+ <View
|
|
|
+ className={`text-12 leading-20 block text-center w-full ${
|
|
|
+ haveBg
|
|
|
+ ? "text-white-70"
|
|
|
+ : "text-black-25"
|
|
|
+ }`}
|
|
|
+ >
|
|
|
{formatMessageTime(group.dt)}
|
|
|
</View>
|
|
|
{group.list.map((message) => {
|
|
@@ -298,21 +168,29 @@ const initialScrolledRef = useRef(false);
|
|
|
role={message.role}
|
|
|
text={message.content}
|
|
|
message={message}
|
|
|
- ></ChatMessage>
|
|
|
+ />
|
|
|
);
|
|
|
})}
|
|
|
- </>
|
|
|
+ </View>
|
|
|
);
|
|
|
})}
|
|
|
</View>
|
|
|
<View className="pb-40 pt-8">
|
|
|
- {agent && <RecommendQuestions enableOutputAudioStream={streamVoiceEnable} agent={agent} />}
|
|
|
+ {agent && (
|
|
|
+ <RecommendQuestions
|
|
|
+ enableOutputAudioStream={streamVoiceEnable}
|
|
|
+ agent={agent}
|
|
|
+ chatInputActions={chatInputActions}
|
|
|
+ />
|
|
|
+ )}
|
|
|
</View>
|
|
|
</ScrollView>
|
|
|
- <View className="w-full h-60" style={{
|
|
|
- height: inputContainerHeight,
|
|
|
- }}>
|
|
|
- </View>
|
|
|
+ <View
|
|
|
+ className="w-full h-60"
|
|
|
+ style={{
|
|
|
+ height: inputContainerHeight,
|
|
|
+ }}
|
|
|
+ ></View>
|
|
|
<View
|
|
|
className="bottom-bar px-16 pt-12 z-50"
|
|
|
id="inputContainer"
|
|
@@ -321,19 +199,26 @@ const initialScrolledRef = useRef(false);
|
|
|
}}
|
|
|
>
|
|
|
<View className="bg-[#F5FAFF]">
|
|
|
- {agent &&
|
|
|
+ {agent && (
|
|
|
<View className="flex flex-col w-full gap-8">
|
|
|
<InputBar
|
|
|
enableOutputAudioStream={streamVoiceEnable}
|
|
|
agent={agent}
|
|
|
- histories={list}
|
|
|
+ // histories={historyList}
|
|
|
setShowWelcome={setShowWelcome}
|
|
|
- ></InputBar>
|
|
|
+ chatInputActions={chatInputActions}
|
|
|
+ isVoice={isVoice}
|
|
|
+ setIsVoice={setIsVoice}
|
|
|
+ disabled={disabled}
|
|
|
+ />
|
|
|
<View>
|
|
|
- <ButtonEnableStreamVoice setEnable={setStreamVoiceEnable} enable={streamVoiceEnable}></ButtonEnableStreamVoice>
|
|
|
+ <ButtonEnableStreamVoice
|
|
|
+ setEnable={setStreamVoiceEnable}
|
|
|
+ enable={streamVoiceEnable}
|
|
|
+ />
|
|
|
</View>
|
|
|
</View>
|
|
|
- }
|
|
|
+ )}
|
|
|
</View>
|
|
|
</View>
|
|
|
</View>
|