index.tsx 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. import { View, ScrollView, Video } from "@tarojs/components";
  2. import NavBarNormal from "@/components/NavBarNormal/index";
  3. import PageCustom from "@/components/page-custom/index";
  4. import Taro, { useDidShow, useRouter, useUnload } from "@tarojs/taro";
  5. import ChatMessage from "@/components/chat-message";
  6. import InputBar from "./components/input-bar";
  7. import { useEffect, useState, useRef, useMemo } from "react";
  8. import { useTextChat } from "@/store/textChat";
  9. import { TRobotMessage, TMessage, EContentType, EChatRole } from "@/types/bot";
  10. import ChatGreeting from "./components/ChatGreeting";
  11. import IconArrowLeftWhite24 from "@/components/icon/IconArrowLeftWhite24";
  12. import PersonalCard from "./components/personal-card";
  13. import { useAgentStore } from "@/store/agentStore";
  14. import { useLoadMoreInfinite, createKey } from "@/utils/loadMoreInfinite";
  15. import { getMessageHistories } from "@/service/bot";
  16. import RecommendQuestions from './components/RecommendQuestions'
  17. import {useKeyboard} from './components/keyboard'
  18. import { saveMessageToServer } from "./components/input-bar/message";
  19. import { generateUUID, getLoginId } from '@/utils/index'
  20. export default function Index() {
  21. const router = useRouter();
  22. const { agentId, isVisitor } = router.params;
  23. if (!agentId) {
  24. return <View>没有相应的智能体</View>;
  25. }
  26. const { fetchAgent, fetchAgentProfile, } = useAgentStore()
  27. const agent = useAgentStore((state) => {
  28. if(isVisitor === 'true'){
  29. return state.agentProfile
  30. }
  31. return state.agent
  32. });
  33. const scrollViewRef = useRef<any>(null);
  34. const messageList = useTextChat((state) => state.list);
  35. const {keyboardHeight, marginTopOffset, triggerHeightUpdate,} = useKeyboard(scrollViewRef, '#messageList', '#scrollView')
  36. const { destroy, setScrollTop, genSessionId, sessionId, setAutoScroll } = useTextChat();
  37. const scrollTop = useTextChat((state) => state.scrollTop);
  38. // const autoScroll = useTextChat((state) => state.autoScroll);
  39. const fetcher = async ([_url,{ nextId, pageSize}]) => {
  40. const _nextId = nextId ? decodeURIComponent(nextId) : nextId;
  41. const res = await getMessageHistories({
  42. agentId,
  43. startId: _nextId,
  44. pageSize,
  45. });
  46. return res.data;
  47. };
  48. const { list, loadMore, pageIndex, mutate } = useLoadMoreInfinite<TMessage[]|TRobotMessage[]>(
  49. createKey(`messeagehistories${isVisitor}${agentId}`),
  50. fetcher,
  51. );
  52. const parsedList = list.map((item: TMessage|TRobotMessage) => {
  53. if(item.contentType == EContentType.AiseekQA){
  54. try{
  55. const contentJson = JSON.parse(item.content as string)
  56. item.content = contentJson.answer.text
  57. // 把消息详情放入统一 body 中
  58. item.body = {...item, content: contentJson, contentType: EContentType.AiseekQA}
  59. }catch(e){
  60. // console.error(e)
  61. }
  62. }
  63. return item
  64. })
  65. const allMessages = useMemo(()=> [...[...parsedList].reverse(), ...messageList], [parsedList, messageList])
  66. const messagesLength = useMemo(() => allMessages.length, [allMessages.length]);
  67. const prevLengthRef = useRef(messagesLength);
  68. const [showWelcome, setShowWelcome] = useState(!list.length);
  69. // 加载更多
  70. const onScrollToUpper = () => {
  71. console.log("onscroll");
  72. loadMore();
  73. };
  74. const handleTouchMove = ()=> {
  75. console.log('set auto scroll false')
  76. setAutoScroll(false)
  77. }
  78. useDidShow(()=> {
  79. mutate(undefined,{revalidate: true})
  80. })
  81. useEffect(() => {
  82. if(agentId){
  83. if(isVisitor){
  84. fetchAgentProfile(agentId)
  85. }else{
  86. fetchAgent(agentId);
  87. }
  88. }
  89. }, [agentId, isVisitor]);
  90. // 是否显示欢迎 ui
  91. useEffect(() => {
  92. setShowWelcome(!messageList.length && !list.length);
  93. }, [list, messageList]);
  94. // 将 greeting 开场白存入聊天记录里
  95. // useEffect(()=> {
  96. // console.log(showWelcome, agent)
  97. // if(!showWelcome){
  98. // return;
  99. // }
  100. // const loginId = getLoginId();
  101. // if (!loginId || !agent?.agentId || !agent?.greeting || !sessionId) {
  102. // return;
  103. // }
  104. // saveMessageToServer({
  105. // loginId,
  106. // messages: [{
  107. // content: agent.greeting,
  108. // contentType: EContentType.TextPlain,
  109. // role: EChatRole.Assistant,
  110. // saveStatus: 2,
  111. // isStreaming: false,
  112. // msgUk: generateUUID(),
  113. // }],
  114. // agentId: agent.agentId,
  115. // sessionId,
  116. // })
  117. // }, [showWelcome, sessionId])
  118. // 首次进入界面滚动到底
  119. useEffect(() => {
  120. if (pageIndex === 1) {
  121. setTimeout(() => {
  122. setScrollTop();
  123. }, 300);
  124. }
  125. }, [pageIndex]);
  126. // 首次进入聊天生成 session id
  127. useEffect(() => {
  128. genSessionId();
  129. }, []);
  130. // 监听消息列表变化,触发键盘高度重新计算
  131. useEffect(() => {
  132. // 只在长度真正变化时才触发
  133. if (prevLengthRef.current !== messagesLength) {
  134. prevLengthRef.current = messagesLength;
  135. // 使用 setTimeout 确保 DOM 更新完成后再计算高度
  136. const timer = setTimeout(() => {
  137. triggerHeightUpdate();
  138. }, 100);
  139. return () => clearTimeout(timer);
  140. }
  141. }, [messagesLength, triggerHeightUpdate]);
  142. useUnload(() => {
  143. destroy();
  144. });
  145. const renderNavLeft = () => {
  146. return (
  147. <View
  148. className="flex items-center gap-8"
  149. onClick={() => Taro.navigateBack()}
  150. >
  151. <IconArrowLeftWhite24 />
  152. <PersonalCard agent={agent} size="mini" />
  153. </View>
  154. );
  155. };
  156. // 大背景可以是视频,也可以是图片
  157. const getBgContent = ()=> {
  158. if(!agent?.avatarUrl || !!!agent?.enabledChatBg){
  159. return ''
  160. }
  161. return agent?.avatarUrl
  162. }
  163. useEffect(()=> {
  164. Taro.setNavigationBarColor({
  165. frontColor: '#ffffff',
  166. backgroundColor: 'transparent'
  167. })
  168. return ()=> {
  169. Taro.setNavigationBarColor({
  170. frontColor: '#000000',
  171. backgroundColor: 'transparent'
  172. })
  173. }
  174. })
  175. return (
  176. <PageCustom fullPage style={{ overflow: "hidden" }} styleBg={getBgContent()}>
  177. <NavBarNormal blur leftColumn={renderNavLeft}>
  178. {/* <>{`${scrollTop}`}--{autoScroll ? 'true': 'false'}</> */}
  179. </NavBarNormal>
  180. <View
  181. className="flex flex-col w-full h-full relative z-10 flex-1"
  182. style={{ top: `${marginTopOffset}px` }}
  183. >
  184. <ScrollView
  185. ref={scrollViewRef}
  186. scrollY
  187. id="scrollView"
  188. style={{
  189. flex: 1,
  190. height: "1px", // 高度自适应
  191. }}
  192. scrollTop={scrollTop}
  193. scrollWithAnimation
  194. onScrollToUpper={onScrollToUpper}
  195. onScrollToLower={()=> setAutoScroll(true)}
  196. >
  197. <View id="messageList" className="flex flex-col gap-16 px-18" onTouchMove={handleTouchMove}>
  198. {showWelcome && <ChatGreeting agent={agent} />}
  199. {/* 复制 histories 再 reverse 否则会影响 state */}
  200. {allMessages.map((message) => {
  201. const reasoningContent = (message as any).reasoningContent || '';
  202. return (
  203. <ChatMessage
  204. key={message.msgUk}
  205. textReasoning={reasoningContent}
  206. agent={agent}
  207. role={message.role}
  208. text={message.content}
  209. message={message}
  210. ></ChatMessage>
  211. );
  212. })}
  213. </View>
  214. <View className="pb-40 pt-8">
  215. {(agent) && <RecommendQuestions agent={agent} />}
  216. </View>
  217. </ScrollView>
  218. <View className="w-full h-54">
  219. {/* 输入框高度占位块,todo: 改成动态获取输入框块高度 */}
  220. </View>
  221. <View
  222. className="bottom-bar px-16 pt-12 z-50"
  223. style={{
  224. bottom: `${keyboardHeight}px`,
  225. }}
  226. >
  227. <View className="bg-[#F5FAFF]">
  228. {agent && (
  229. <InputBar
  230. agent={agent}
  231. histories={list}
  232. setShowWelcome={setShowWelcome}
  233. ></InputBar>
  234. )}
  235. </View>
  236. </View>
  237. </View>
  238. </PageCustom>
  239. );
  240. }