123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337 |
- 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 <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 [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 (
- <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]);
- 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 (
- <PageCustom
- fullPage
- style={{ overflow: "hidden" }}
- styleBg={getBgContent()}
- >
- <NavBarNormal blur leftColumn={renderNavLeft}>
- {/* <>{`${scrollTop}`}--{autoScroll ? 'true': 'false'}</> */}
- </NavBarNormal>
- <View
- className="flex flex-col w-full h-full relative z-10 flex-1"
- style={{ top: `${marginTopOffset}px` }}
- >
- <ScrollView
- ref={scrollViewRef}
- scrollY
- id="scrollView"
- style={{
- flex: 1,
- height: "1px", // 高度自适应
- }}
- scrollTop={scrollTop}
- scrollWithAnimation
- onScrollToUpper={onScrollToUpper}
- onScrollToLower={() => setAutoScroll(true)}
- >
- <View
- id="messageList"
- className="flex flex-col gap-16 px-18"
- onTouchMove={handleTouchMove}
- >
- {showWelcome && <ChatGreeting agent={agent} />}
- {result.map((group) => {
- return (
- <>
- <View className={`text-12 leading-20 block text-center w-full ${(agent?.enabledChatBg && !!agent?.avatarUrl?.length) ? 'text-white-70' : 'text-black-25'}`}>
- {formatMessageTime(group.dt)}
- </View>
- {group.list.map((message) => {
- const reasoningContent =
- (message as any).reasoningContent || "";
- return (
- <ChatMessage
- key={message.msgUk}
- textReasoning={reasoningContent}
- agent={agent}
- role={message.role}
- text={message.content}
- message={message}
- ></ChatMessage>
- );
- })}
- </>
- );
- })}
- </View>
- <View className="pb-40 pt-8">
- {agent && <RecommendQuestions enableOutputAudioStream={streamVoiceEnable} agent={agent} />}
- </View>
- </ScrollView>
- <View className="w-full h-60" style={{
- height: inputContainerHeight,
- }}>
- </View>
- <View
- className="bottom-bar px-16 pt-12 z-50"
- id="inputContainer"
- style={{
- bottom: `${keyboardHeight + inputContainerBottomOffset}px`,
- }}
- >
- <View className="bg-[#F5FAFF]">
- {agent &&
- <View className="flex flex-col w-full gap-8">
- <InputBar
- enableOutputAudioStream={streamVoiceEnable}
- agent={agent}
- histories={list}
- setShowWelcome={setShowWelcome}
- ></InputBar>
- <View>
- <ButtonEnableStreamVoice setEnable={setStreamVoiceEnable} enable={streamVoiceEnable}></ButtonEnableStreamVoice>
- </View>
- </View>
- }
- </View>
- </View>
- </View>
- </PageCustom>
- );
- }
|