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 ChatMessage from "@/components/chat-message";
import InputBar from "./components/input-bar";
import { useEffect, useState, useRef, useMemo } from "react";
import { useTextChat } from "@/store/textChat";
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 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';
export default function Index() {
const router = useRouter();
const { agentId, isVisitor } = router.params;
if (!agentId) {
return 没有相应的智能体;
}
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(null);
const messageList = useTextChat((state) => state.list);
const [inputContainerHeight,setInputContainerHeight]= useState(0)
const [streamVoiceEnable, setStreamVoiceEnable]= usePersistentState('streamVoiceEnable', false)
const { keyboardHeight, marginTopOffset, triggerHeightUpdate } = useKeyboard(
scrollViewRef,
"#messageList",
"#scrollView"
);
// 输入框容器
// 针对没有 safeArea?.bottom 的手机,需要额外增加 12 高度
let inputContainerBottomOffset = 0
if(bottomSafeHeight <= 0){
inputContainerBottomOffset = 12
}
const { destroy, setScrollTop, genSessionId, setAutoScroll } = useTextChat();
const scrollTop = useTextChat((state) => state.scrollTop);
// 放在组件里
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 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(), 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]);
useUnload(() => {
destroy();
});
const renderNavLeft = () => {
return (
Taro.navigateBack()}
>
{haveBg ? : }
);
};
// 大背景可以是视频,也可以是图片
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]);
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])
return (
{/* <>{`${scrollTop}`}--{autoScroll ? 'true': 'false'}> */}
setAutoScroll(true)}
>
{showWelcome && }
{result.map((group) => {
return (
<>
{formatMessageTime(group.dt)}
{group.list.map((message) => {
const reasoningContent =
(message as any).reasoningContent || "";
return (
);
})}
>
);
})}
{agent && }
{agent &&
}
);
}