yt-chat-components 2.0.5 → 2.0.7

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.
@@ -214,6 +214,7 @@ const ChatWindow = forwardRef<ChatWindowRef, ChatWindowProps>(({
214
214
  // 滚动事件处理,选择文件时,文件内容超出显示框时,显示左右箭头
215
215
  const messageContainerRef = useRef<HTMLDivElement>(null);
216
216
  const [isUserScrolledUp, setIsUserScrolledUp] = useState(false);
217
+ const isUserScrolledUpRef = useRef(false);
217
218
 
218
219
  useImperativeHandle(ref, () => ({
219
220
  handleCallButtonClick:handleCallButtonClick
@@ -234,6 +235,10 @@ const ChatWindow = forwardRef<ChatWindowRef, ChatWindowProps>(({
234
235
  }
235
236
  };
236
237
 
238
+ useEffect(() => {
239
+ isUserScrolledUpRef.current = isUserScrolledUp;
240
+ }, [isUserScrolledUp]);
241
+
237
242
  /**
238
243
  * 监听用户滚动标志,一旦出现滚动事件就会执行判断
239
244
  * 若处于底部且之前激活向上滚动,则使向上滚动标志失效
@@ -245,37 +250,43 @@ const ChatWindow = forwardRef<ChatWindowRef, ChatWindowProps>(({
245
250
 
246
251
  const handleScroll = () => {
247
252
  const { scrollTop, scrollHeight, clientHeight } = container;
248
- const isAtBottom = scrollHeight - scrollTop - clientHeight <= 10; // <= 更稳妥
249
- console.log("与底部距离===", scrollHeight - scrollTop - clientHeight)
250
- console.log("此前是否向上滚动", isUserScrolledUp)
251
- if (!isAtBottom && !isUserScrolledUp) {
252
- setIsUserScrolledUp(true);
253
- } else if (isAtBottom && isUserScrolledUp) {
254
- setIsUserScrolledUp(false);
253
+ const isAtBottom = scrollHeight - scrollTop - clientHeight <= 10;
254
+
255
+ if (!isAtBottom) {
256
+ // 用户向上滚动了,标记为“已干预”
257
+ if (!isUserScrolledUpRef.current) {
258
+ setIsUserScrolledUp(true);
259
+ }
260
+ } else {
261
+ // 只有在底部时才允许重置“已干预”状态
262
+ if (isUserScrolledUpRef.current) {
263
+ setIsUserScrolledUp(false);
264
+ }
255
265
  }
256
266
  };
257
267
 
258
268
  container.addEventListener('scroll', handleScroll);
259
269
  return () => container.removeEventListener('scroll', handleScroll);
260
- }, [isUserScrolledUp]);
270
+ // ✅ 依赖为空,靠 useRef 获取最新状态
271
+ }, []);
261
272
 
262
273
 
263
274
  /**
264
275
  * 判断是否需要滚动到底部
265
276
  * */
266
- const judgeToScrollEnd = (isForce) => {
277
+ const judgeToScrollEnd = (isForce = false) => {
267
278
  const container = messageContainerRef.current;
268
279
  if (!container) return;
269
280
 
270
- const { scrollTop, scrollHeight, clientHeight } = container;
271
- const isCurrentlyAtBottom = scrollHeight - scrollTop - clientHeight < 120; //
272
281
  if (isForce) {
273
- container.scrollTop = scrollHeight;
274
- // setIsUserScrolledUp(false); // 强制滚动后认为用户在底部
282
+ setIsUserScrolledUp(false);
283
+ container.scrollTop = container.scrollHeight;
275
284
  return;
276
285
  }
277
- if (isCurrentlyAtBottom) {
278
- container.scrollTop = scrollHeight;
286
+
287
+ // 👉 核心改进:只要用户没有主动向上滚动,就自动滚动到底
288
+ if (!isUserScrolledUpRef.current) {
289
+ container.scrollTop = container.scrollHeight;
279
290
  }
280
291
  };
281
292
 
@@ -366,7 +377,7 @@ const ChatWindow = forwardRef<ChatWindowRef, ChatWindowProps>(({
366
377
  // 流式输出消息,实时显示(token为流式输出内容,end为结束输出,整体输出一次)
367
378
  const handleMessageContent = (event, data) => {
368
379
  // console.log(`--- event = ${event}, content_ns = ${content_ns}, content_id = ${content_id}, data = `, data)
369
-
380
+ setTimeout(() => judgeToScrollEnd(), 0)
370
381
  if (event == 'add_message' && data['sender'] == 'Machine') {
371
382
  getHistoryList();
372
383
  }
@@ -413,10 +424,6 @@ const ChatWindow = forwardRef<ChatWindowRef, ChatWindowProps>(({
413
424
  updateMessageItem({updateSrc: "3", chunk, loadingMessage: loading_message || ''})
414
425
  }
415
426
  }
416
-
417
- if (lastMessage.current) {
418
- setTimeout(()=>judgeToScrollEnd(),100)
419
- }
420
427
  }
421
428
  else if (event == 't_token') {
422
429
  let { chunk, id, r_id, ns, name, icon, loading_message } = data
@@ -430,10 +437,6 @@ const ChatWindow = forwardRef<ChatWindowRef, ChatWindowProps>(({
430
437
  }else {
431
438
  updateMessageItem({updateSrc: "4", chunk, status:null, isThinkChunk:true, loadingMessage: loading_message || ''})
432
439
  }
433
-
434
- if (lastMessage.current) {
435
- setTimeout(()=>judgeToScrollEnd(),100)
436
- }
437
440
  }
438
441
  else if (event == 'status') {
439
442
  // 更新状态
@@ -0,0 +1,389 @@
1
+ import { Avatar, Button, Input, Layout, List, Modal, Tooltip, Typography, Dropdown } from 'antd';
2
+ import {
3
+ AlertOutlined,
4
+ BellOutlined,
5
+ FolderOutlined, LeftOutlined,
6
+ MessageOutlined,
7
+ PlusOutlined,
8
+ QuestionCircleOutlined, RightOutlined,
9
+ SearchOutlined,
10
+ TeamOutlined,
11
+ UserOutlined,
12
+ } from '@ant-design/icons';
13
+ import { useEffect, useState } from "react";
14
+ import { isEmpty, isFunction } from "lodash";
15
+ const { Sider } = Layout;
16
+ const { Text } = Typography;
17
+ const { confirm } = Modal;
18
+
19
+ const displayStyle = { display: 'none' }
20
+
21
+ const LeftMenu = ({
22
+ onNewChat,
23
+ onSelectSession,
24
+ activeId,
25
+ appId,
26
+ flow,
27
+ handleLoadHistoryList,
28
+ userInfo,
29
+ sceneInfo = {},
30
+ allFlowList = [],
31
+ addToScene,
32
+ removeFromScene
33
+ }) => {
34
+ const [collapsed, setCollapsed] = useState(false);
35
+ const [historyList, setHistoryList] = useState([]);
36
+ const [appList, setAppList] = useState([]);
37
+ const [filter, setFilter] = useState('');
38
+ const [isShowSearch, setIsShowSearch] = useState(false);
39
+ const [isShowAllFlowList, setIsShowAllFlowList] = useState(false);
40
+ useEffect(() => {
41
+ }, [appId])
42
+
43
+ const handleDelete = (item) => {
44
+ confirm({
45
+ centered: true,
46
+ title: '确认删除该流程?',
47
+ content: `删除后仍可以添加到该场景,是否删除?`,
48
+ okText: '删除',
49
+ okType: 'danger',
50
+ cancelText: '取消',
51
+ onOk: () => {
52
+ removeFromScene && removeFromScene(item);
53
+ },
54
+ });
55
+ };
56
+
57
+ return (
58
+ <>
59
+ <Sider
60
+ width={300}
61
+ collapsible={true}
62
+ defaultCollapsed={false}
63
+ collapsed={collapsed}
64
+ collapsedWidth={0}
65
+ trigger={<Button
66
+ icon={collapsed ? <RightOutlined/> : <LeftOutlined/>}
67
+ style={{ borderRadius: '20px', position: 'absolute', top: '100px', right: '25px' }}
68
+ onClick={() => setCollapsed(!collapsed)}
69
+ />}
70
+ zeroWidthTriggerStyle={{ backgroundColor: 'transparent' }}
71
+ >
72
+ <div style={{
73
+ display: 'flex',
74
+ flexDirection: 'row',
75
+ position: 'relative',
76
+ }}
77
+ >
78
+ {
79
+ !collapsed && <div style={{
80
+ width: '64px',
81
+ display: 'flex',
82
+ alignItems: 'center',
83
+ flexDirection: 'column',
84
+ backgroundColor: '#FFFFFF',
85
+ borderRight: '1px solid #E5E5E5'
86
+ }}>
87
+ <div style={{ marginTop: '16px', marginBottom: '64px' }}>
88
+ <Avatar size={32}
89
+ style={{
90
+ background: '#ff4d4f',
91
+ fontSize: '20px',
92
+ width: '40px',
93
+ height: '40px',
94
+ borderRadius: '6px',
95
+ cursor: 'pointer'
96
+ }}>
97
+ {userInfo?.name.substring(0, 1) || '我'}
98
+ </Avatar>
99
+ </div>
100
+ <div style={{ display: 'flex', flexDirection: 'column', flex: 1, alignItems: 'center', gap: 12 }}>
101
+ <MessageOutlined title={'聊天'} style={{
102
+ color: '#000000',
103
+ fontSize: '20px',
104
+ padding: '12px', ...displayStyle
105
+ }}/>
106
+ <FolderOutlined title={'资源'} style={{
107
+ color: '#000000',
108
+ fontSize: '20px',
109
+ padding: '12px', ...displayStyle
110
+ }}/>
111
+ <TeamOutlined title={'团队'}
112
+ style={{ color: '#000000', fontSize: '20px', padding: '12px', ...displayStyle }}/>
113
+ <AlertOutlined title={'专家'} style={{
114
+ color: '#000000',
115
+ fontSize: '20px',
116
+ padding: '12px', ...displayStyle
117
+ }}/>
118
+ </div>
119
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 12 }}>
120
+ <SearchOutlined title={'搜索'} style={{
121
+ color: '#000000',
122
+ fontSize: '20px',
123
+ padding: '12px', ...displayStyle
124
+ }}/>
125
+ <QuestionCircleOutlined title={'帮助'} style={{
126
+ color: '#000000',
127
+ fontSize: '20px',
128
+ padding: '12px', ...displayStyle
129
+ }}/>
130
+ <BellOutlined title={'通知'}
131
+ style={{ color: '#000000', fontSize: '20px', padding: '12px', ...displayStyle }}/>
132
+ </div>
133
+ <div style={{ margin: '16px 0' }}>
134
+ {/*<Avatar size={32}*/}
135
+ {/* style={{*/}
136
+ {/* background: '#ff4d4f',*/}
137
+ {/* fontSize: '20px',*/}
138
+ {/* width: '40px',*/}
139
+ {/* height: '40px',*/}
140
+ {/* }}>*/}
141
+ {/* 你*/}
142
+ {/*</Avatar>*/}
143
+ </div>
144
+ </div>
145
+ }
146
+ {
147
+ !collapsed && <div style={{
148
+ width: '266px',
149
+ height: '100vh',
150
+ background: '#fff',
151
+ borderRight: '1px solid #E5E5E5',
152
+ display: 'flex',
153
+ flexDirection: 'column',
154
+ }}>
155
+ <div
156
+ style={{
157
+ padding: '22px 16px 14px',
158
+ borderBottom: '#E5E5E5 solid 1px',
159
+ display: 'flex',
160
+ alignItems: 'center',
161
+ justifyContent: 'space-between',
162
+ }}
163
+ >
164
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
165
+ <div style={{ display: 'flex', flexDirection: 'column' }}>
166
+ <Text strong>聊天</Text>
167
+ {/*<Text type="secondary" style={{ fontSize: 12 }}>*/}
168
+ {/* {flow.name}*/}
169
+ {/*</Text>*/}
170
+ </div>
171
+ </div>
172
+ <Button
173
+ type="primary"
174
+ style={{display: 'flex', alignItems: 'center'}}
175
+ shape="circle"
176
+ icon={<PlusOutlined style={{
177
+ fontSize: 20,
178
+ width: 27,
179
+ height: 27,
180
+ display: 'flex',
181
+ justifyContent: 'center'
182
+ }}/>}
183
+ size="small"
184
+ title={'添加助手'}
185
+ onClick={() => setIsShowAllFlowList(true)}
186
+ />
187
+ </div>
188
+
189
+ {/* 会话 / 菜单列表 */}
190
+ <div
191
+ style={{
192
+ flex: 1,
193
+ overflowY: 'auto',
194
+ padding: '0 8px 8px',
195
+ }}
196
+ >
197
+ <div style={{ padding: '8px 8px 8px' }}>
198
+ <Input
199
+ placeholder="搜索智能体"
200
+ prefix={<SearchOutlined/>}
201
+ allowClear
202
+ size="small"
203
+ onChange={(e) => setFilter(e.target.value)}
204
+ />
205
+ </div>
206
+ <div style={{ marginBottom: 16 }}>
207
+ <List
208
+ size="small"
209
+ dataSource={sceneInfo.flows?.filter(item => item.name.indexOf(filter) > -1)}
210
+ renderItem={item => {
211
+ const isActive = activeId === item.id;
212
+
213
+ return (
214
+ <Dropdown
215
+ trigger={['contextMenu']}
216
+ menu={{
217
+ items: [
218
+ {
219
+ key: 'delete',
220
+ label: '删除',
221
+ danger: true,
222
+ onClick: () => handleDelete(item),
223
+ },
224
+ ],
225
+ }}
226
+ >
227
+ <List.Item
228
+ style={{
229
+ padding: '8px 8px',
230
+ borderRadius: 8,
231
+ margin: '2px 0',
232
+ cursor: 'pointer',
233
+ background: isActive ? 'rgba(84,52,255,0.06)' : 'transparent',
234
+ }}
235
+ onClick={() => onSelectSession && onSelectSession(item)}
236
+ >
237
+ <List.Item.Meta
238
+ style={{ display: 'flex', alignItems: 'center' }}
239
+ avatar={
240
+ <img
241
+ src={item.icon}
242
+ style={{ width: 28, height: 28, borderRadius: 4 }}
243
+ />
244
+ }
245
+ title={
246
+ <div
247
+ style={{
248
+ display: 'flex',
249
+ justifyContent: 'space-between',
250
+ alignItems: 'center',
251
+ }}
252
+ >
253
+ <span
254
+ style={{
255
+ fontWeight: isActive ? 600 : 400,
256
+ }}
257
+ >
258
+ {item.name}
259
+ </span>
260
+ </div>
261
+ }
262
+ description={
263
+ item.content ? (
264
+ <Tooltip title={item.content} placement={'bottomRight'}>
265
+ <Text
266
+ type="secondary"
267
+ style={{ fontSize: 12 }}
268
+ ellipsis
269
+ >
270
+ {item.content}
271
+ </Text>
272
+ </Tooltip>
273
+ ) : null
274
+ }
275
+ />
276
+ </List.Item>
277
+ </Dropdown>
278
+ );
279
+ }}
280
+ />
281
+ </div>
282
+ </div>
283
+
284
+ {/* 底部用户/设置区 */}
285
+ <div
286
+ style={{
287
+ borderTop: '1px solid #E5E5E5',
288
+ padding: '8px 12px',
289
+ display: 'none',
290
+ alignItems: 'center',
291
+ justifyContent: 'space-between',
292
+ }}
293
+ >
294
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
295
+ <Avatar size={28} icon={<UserOutlined/>}/>
296
+ <div style={{ display: 'flex', flexDirection: 'column' }}>
297
+ <Text style={{ fontSize: 13 }}>招生咨询老师</Text>
298
+ <Text type="secondary" style={{ fontSize: 11 }}>
299
+ 在线
300
+ </Text>
301
+ </div>
302
+ </div>
303
+ <Button size="small" type="link" style={{ padding: 0 }}>
304
+ 设置
305
+ </Button>
306
+ </div>
307
+ </div>
308
+ }
309
+ </div>
310
+ </Sider>
311
+ <Modal
312
+ title={null}
313
+ open={isShowAllFlowList}
314
+ onCancel={() => setIsShowAllFlowList(false)}
315
+ footer={null}
316
+ width={1200}
317
+ centered={true}
318
+ // style={{ top: 0, height: '100%' }}
319
+ >
320
+ <List
321
+ size="large"
322
+ dataSource={allFlowList.filter(flow => sceneInfo.flow_ids.indexOf(flow.id) === -1)}
323
+ renderItem={item => {
324
+ const isActive = activeId === item.id;
325
+ return (
326
+ <List.Item
327
+ style={{
328
+ padding: '8px 8px',
329
+ borderRadius: 8,
330
+ margin: '2px 0',
331
+ cursor: 'pointer',
332
+ background: isActive ? 'rgba(84,52,255,0.06)' : 'transparent',
333
+ }}
334
+ actions={[
335
+ <a
336
+ size="small"
337
+ style={{ padding: 0,width: 80 }}
338
+ onClick={() => {
339
+ if(isFunction(addToScene)){
340
+ addToScene(item)
341
+ }
342
+ }}
343
+ >添加</a>
344
+ ]}
345
+ >
346
+ <List.Item.Meta
347
+ style={{ display: 'flex', alignItems: 'center' }}
348
+ avatar={
349
+ <img
350
+ src={item.icon}
351
+ style={{ width: 28, height: 28, borderRadius: 4, }}/>
352
+ }
353
+ title={
354
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', }}>
355
+ <span
356
+ style={{
357
+ fontWeight: isActive ? 600 : 400,
358
+ }}
359
+ >
360
+ {item.name}
361
+ </span>
362
+ </div>
363
+ }
364
+ description={
365
+ item.content ? (
366
+ <Tooltip title={item.content} placement={'bottomRight'}>
367
+ <Text
368
+ type="secondary"
369
+ style={{ fontSize: 12 }}
370
+ ellipsis
371
+ >
372
+ {item.content}
373
+ </Text>
374
+ </Tooltip>
375
+ ) : null
376
+ }
377
+ />
378
+ </List.Item>
379
+ );
380
+ }}
381
+ >
382
+ </List>
383
+ </Modal>
384
+ </>
385
+ )
386
+ ;
387
+ }
388
+
389
+ export default LeftMenu;