yt-chat-components 2.0.2 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yt-chat-components",
3
- "version": "2.0.2",
3
+ "version": "2.0.3",
4
4
  "main": "build/static/js/bundle.min.js",
5
5
  "module": "build/static/js/bundle.min.js",
6
6
  "types": "build/static/js/index.d.ts",
@@ -196,16 +196,13 @@ const ChatWindow = forwardRef<ChatWindowRef, ChatWindowProps>(({
196
196
  const [receivingMessage, setReceivingMessage] = useState(false);
197
197
  const receivingMessageRef = useRef(receivingMessage);
198
198
  const abortControllerRef = useRef(new AbortController());
199
- const [fileList, setFileList] = useState<
200
- { file: File; fileUrl: string; fileType: string; fileId: string, fileName: string }[]
201
- >([]);
202
- const scrollContainerRef = useRef(null);
199
+ const [fileList, setFileList] = useState<{ file: File; fileUrl: string; fileType: string; fileId: string, fileName: string }[]>([]);
200
+ const fileScrollContainerRef = useRef(null);
203
201
  const sessionIdRef = useRef(null);
204
202
  const [showLeftArrow, setShowLeftArrow] = useState(false); // 控制左侧箭头显示
205
203
  const [showRightArrow, setShowRightArrow] = useState(false); // 控制右侧箭头显示
206
204
  const isStream = true;//是否流式输出(手动开关)
207
205
  const [recordState, setRecordState] = useState(false); // 录音状态。true为正在录音,false为停止录音
208
- const [tagList, setTagList] = useState([]); // 问题标签列表
209
206
  const {isTitleSideIcon, logoWidth, agentUrl, stopMessageUrl, sendMessageUrl, speakUrl, callUrl, smartBoyUrl, wonderBoyUrl, isHideTabSelector, customAiToldYou} = baseConfig;
210
207
  const [inputContainerHeight, setInputContainerHeight] = useState('120px')
211
208
  let content_id: string = null
@@ -216,14 +213,15 @@ const ChatWindow = forwardRef<ChatWindowRef, ChatWindowProps>(({
216
213
  let voiceChunks = []; // 临时存储录制的语音片段
217
214
  // 滚动事件处理,选择文件时,文件内容超出显示框时,显示左右箭头
218
215
  const messageContainerRef = useRef<HTMLDivElement>(null);
216
+ const [isUserScrolledUp, setIsUserScrolledUp] = useState(false);
219
217
 
220
218
  useImperativeHandle(ref, () => ({
221
219
  handleCallButtonClick:handleCallButtonClick
222
220
  }))
223
221
 
224
- const handleScroll = () => {
225
- if (scrollContainerRef.current) {
226
- const { scrollLeft, clientWidth, scrollWidth } = scrollContainerRef.current;
222
+ const handleFileScroll = () => {
223
+ if (fileScrollContainerRef.current) {
224
+ const { scrollLeft, clientWidth, scrollWidth } = fileScrollContainerRef.current;
227
225
 
228
226
  // 判断内容是否超出容器
229
227
  const isContentOverflowed = scrollWidth > clientWidth;
@@ -236,6 +234,50 @@ const ChatWindow = forwardRef<ChatWindowRef, ChatWindowProps>(({
236
234
  }
237
235
  };
238
236
 
237
+ /**
238
+ * 监听用户滚动标志,一旦出现滚动事件就会执行判断
239
+ * 若处于底部且之前激活向上滚动,则使向上滚动标志失效
240
+ * 若不处于底部且未向上滚动,则激活向上滚动标志
241
+ * */
242
+ useEffect(() => {
243
+ const container = messageContainerRef.current;
244
+ if (!container) return;
245
+
246
+ const handleScroll = () => {
247
+ const { scrollTop, scrollHeight, clientHeight } = container;
248
+ const isAtBottom = scrollHeight - scrollTop <= clientHeight + 10; // <= 更稳妥
249
+
250
+ if (!isAtBottom && !isUserScrolledUp) {
251
+ setIsUserScrolledUp(true);
252
+ } else if (isAtBottom && isUserScrolledUp) {
253
+ setIsUserScrolledUp(false);
254
+ }
255
+ };
256
+
257
+ container.addEventListener('scroll', handleScroll);
258
+ return () => container.removeEventListener('scroll', handleScroll);
259
+ }, [isUserScrolledUp]);
260
+
261
+
262
+ /**
263
+ * 判断是否需要滚动到底部
264
+ * */
265
+ const judgeToScrollEnd = (isForce) => {
266
+ const container = messageContainerRef.current;
267
+ if (!container) return;
268
+
269
+ const { scrollTop, scrollHeight, clientHeight } = container;
270
+ const isCurrentlyAtBottom = scrollHeight - scrollTop - clientHeight < 120; //
271
+ if (isForce) {
272
+ container.scrollTop = scrollHeight;
273
+ setIsUserScrolledUp(false); // 强制滚动后认为用户在底部
274
+ return;
275
+ }
276
+ if (isCurrentlyAtBottom) {
277
+ container.scrollTop = scrollHeight;
278
+ }
279
+ };
280
+
239
281
  // 处理延时队列,因为后端返回的是 token token form token token.....会导致form渲染在token前面,所以用这个方式
240
282
  // 当处理token的时候,form的信息放到这里,然后使用 delayMessageTimer 处理这个延时队列
241
283
  // 处理逻辑是:收到 token 的时候,重置timer,由于timer一直被重置所以不会处理 delayMessageList,直到 1.5内没有新 token 才会处理延时队列
@@ -372,7 +414,7 @@ const ChatWindow = forwardRef<ChatWindowRef, ChatWindowProps>(({
372
414
  }
373
415
 
374
416
  if (lastMessage.current) {
375
- lastMessage.current.scrollIntoView({ behavior: 'smooth' });
417
+ setTimeout(()=>judgeToScrollEnd(),100)
376
418
  }
377
419
  }
378
420
  else if (event == 't_token') {
@@ -389,7 +431,7 @@ const ChatWindow = forwardRef<ChatWindowRef, ChatWindowProps>(({
389
431
  }
390
432
 
391
433
  if (lastMessage.current) {
392
- lastMessage.current.scrollIntoView({ behavior: 'smooth' });
434
+ setTimeout(()=>judgeToScrollEnd(),100)
393
435
  }
394
436
  }
395
437
  else if (event == 'status') {
@@ -627,7 +669,7 @@ const ChatWindow = forwardRef<ChatWindowRef, ChatWindowProps>(({
627
669
  });
628
670
  }
629
671
  setFileList([]);
630
- handleScroll();
672
+ handleFileScroll();
631
673
  sendMessage(
632
674
  input_value_type,
633
675
  embedAppExtend,
@@ -711,7 +753,7 @@ const ChatWindow = forwardRef<ChatWindowRef, ChatWindowProps>(({
711
753
  setTimeout(() => {
712
754
  if (lastMessage.current) {
713
755
  console.error('开始滚动最后一条消息到可视区域')
714
- lastMessage.current.scrollIntoView({ behavior: 'smooth' });
756
+ judgeToScrollEnd();
715
757
  }
716
758
  }, 600);
717
759
  };
@@ -721,9 +763,9 @@ const ChatWindow = forwardRef<ChatWindowRef, ChatWindowProps>(({
721
763
  * @param file File 对象
722
764
  */
723
765
  const getFileType = (file) => {
724
- if (!(file instanceof File)) {
725
- throw new Error('Input must be a File object');
726
- }
766
+ // if (!(file instanceof File)) {
767
+ // throw new Error('Input must be a File object');
768
+ // }
727
769
 
728
770
  const mimeType = file.type;
729
771
 
@@ -787,7 +829,7 @@ const ChatWindow = forwardRef<ChatWindowRef, ChatWindowProps>(({
787
829
  .then(res => {
788
830
  setFileList(fileList.filter((item, i) => i !== index));
789
831
  })
790
- handleScroll();
832
+ handleFileScroll();
791
833
  };
792
834
 
793
835
  /**
@@ -862,7 +904,7 @@ const ChatWindow = forwardRef<ChatWindowRef, ChatWindowProps>(({
862
904
  : item
863
905
  )
864
906
  );
865
- handleScroll();
907
+ handleFileScroll();
866
908
  })
867
909
  .catch((e) => {
868
910
  setFileList((prevFileList) =>
@@ -889,12 +931,12 @@ const ChatWindow = forwardRef<ChatWindowRef, ChatWindowProps>(({
889
931
  // 向右滚动方法
890
932
  const handleScrollRight = (direction) => {
891
933
  if (direction == 'right') {
892
- if (scrollContainerRef.current) {
893
- scrollContainerRef.current.scrollLeft += 200;
934
+ if (fileScrollContainerRef.current) {
935
+ fileScrollContainerRef.current.scrollLeft += 200;
894
936
  }
895
937
  } else if (direction == 'left') {
896
- if (scrollContainerRef.current) {
897
- scrollContainerRef.current.scrollLeft -= 200;
938
+ if (fileScrollContainerRef.current) {
939
+ fileScrollContainerRef.current.scrollLeft -= 200;
898
940
  }
899
941
  }
900
942
  };
@@ -1006,21 +1048,12 @@ const ChatWindow = forwardRef<ChatWindowRef, ChatWindowProps>(({
1006
1048
  };
1007
1049
 
1008
1050
  /**
1009
- * 输出消息时,滚动到底部
1010
- */
1011
- useEffect(() => {
1012
- if (lastMessage.current) lastMessage.current.scrollIntoView({ behavior: 'smooth' });
1013
- }, [nowAIContentList]);
1014
-
1015
- /* Refocus the User input whenever a new response is returned from the LLM */
1016
-
1051
+ * 当前对话历史消息发生变化时,强制滚动到底部
1052
+ * */
1017
1053
  useEffect(() => {
1018
- // after a slight delay
1019
- setTagList(tags);
1020
- setTimeout(() => {
1021
- // inputRef.current?.focus();
1022
- }, 100);
1023
- }, [messages, open, tags]);
1054
+ judgeToScrollEnd(true);
1055
+ setIsUserScrolledUp(false);
1056
+ }, [messages]);
1024
1057
 
1025
1058
  const fetchChatHistory = async () => {
1026
1059
  try {
@@ -1063,14 +1096,22 @@ const ChatWindow = forwardRef<ChatWindowRef, ChatWindowProps>(({
1063
1096
  }
1064
1097
 
1065
1098
  // 处理最后的 ChatMessageType
1066
- let latestMessage: ChatMessageType= finalChatHistoryList[finalChatHistoryList.length - 1]
1099
+ let latestMessage: ChatMessageType= finalChatHistoryList[finalChatHistoryList.length - 1];
1067
1100
  latestMessage.messageItemList.push({
1068
1101
  id: id,
1069
1102
  name: sender_name,
1070
1103
  message: text,
1071
1104
  thinkMessage: reasoning_content,
1072
1105
  // rawInfo: data,
1073
- rawInfo: {files: files},
1106
+ rawInfo: {
1107
+ files: files.map((fileItem) => ({
1108
+ fileUrl: fileItem.url,
1109
+ name: fileItem.name,
1110
+ size: fileItem.size,
1111
+ fileType: getFileType(fileItem)
1112
+ }))
1113
+ },
1114
+
1074
1115
  type: MessageType.text,
1075
1116
  });
1076
1117
 
@@ -1337,7 +1378,7 @@ const ChatWindow = forwardRef<ChatWindowRef, ChatWindowProps>(({
1337
1378
  }
1338
1379
 
1339
1380
  const inputNode = <div style={input_container_style} className="cl-input_container">
1340
- <div className="w_file_preview" ref={scrollContainerRef} onScroll={handleScroll}>
1381
+ <div className="w_file_preview" ref={fileScrollContainerRef} onScroll={handleFileScroll}>
1341
1382
  <div className="w_toLeftBox" style={{display: showLeftArrow ? 'flex' : 'none'}}>
1342
1383
  <div className="w_toLeft" onClick={() => handleScrollRight('left')}>
1343
1384
  <img src={toLeftPng}/>
@@ -1506,7 +1547,7 @@ const ChatWindow = forwardRef<ChatWindowRef, ChatWindowProps>(({
1506
1547
  abortControllerRef.current.abort('disconnect');
1507
1548
  abortControllerRef.current = new AbortController();
1508
1549
  } else {
1509
- handleSendMessage()
1550
+ handleSendMessage();
1510
1551
  }
1511
1552
  }}
1512
1553
  >
@@ -1527,55 +1568,7 @@ const ChatWindow = forwardRef<ChatWindowRef, ChatWindowProps>(({
1527
1568
  }
1528
1569
  return (
1529
1570
  <>
1530
- <div
1531
- className="w_tagListClass"
1532
- style={isMobile ?
1533
- {
1534
- display: tagList?.length > 0 ? '' : 'none',
1535
- zIndex: tagList?.length > 0 ? '2' : '-1',
1536
- maxWidth: 'calc(100vw - 3.2rem - 24px)',
1537
- flexWrap: "nowrap",
1538
- overflowX: "auto",
1539
- WebkitOverflowScrolling: 'touch',
1540
- scrollbarWidth: "none",
1541
- overflowY: "hidden",
1542
- background: 'transparent',
1543
- padding: '8px 0 8px 0',
1544
- marginLeft: 10,
1545
- } : {
1546
- // bottom: fileList.length > 0 ? '130px' : '80px',
1547
- display: tagList?.length > 0 ? '' : 'none',
1548
- zIndex: tagList?.length > 0 ? '2' : '-1',
1549
- }}
1550
- >
1551
- {
1552
- tagList.map((item, index) => (
1553
- <div
1554
- key={index}
1555
- className="w_tagItemBox"
1556
- onClick={() => {
1557
- if (receivingMessage) {
1558
- return
1559
- }
1560
- handleSendMessage(item?.name);
1561
- setFileList([]);
1562
- }}
1563
- >
1564
- <div>
1565
- <img
1566
- style={item?.img ? {} : {display: 'none'}}
1567
- src={'https://trans-from-yuntu-resourse.oss-cn-beijing.aliyuncs.com/' + item?.img}
1568
- className="w_tagImgh"
1569
- alt="Image"
1570
- />
1571
- </div>
1572
- <div className="w_tagItemText"
1573
- style={isMobile ? {wordBreak: "keep-all", whiteSpace: "nowrap",} : {}}>{item?.name}</div>
1574
- </div>
1575
- ))
1576
- }
1577
- {inputNode}
1578
- </div>
1571
+
1579
1572
  </>
1580
1573
  )
1581
1574
  }