@fe-free/ai 4.1.19 → 4.1.20

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/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # @fe-free/ai
2
2
 
3
+ ## 4.1.20
4
+
5
+ ### Patch Changes
6
+
7
+ - feat: ai
8
+ - @fe-free/core@4.1.20
9
+ - @fe-free/icons@4.1.20
10
+ - @fe-free/tool@4.1.20
11
+
3
12
  ## 4.1.19
4
13
 
5
14
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fe-free/ai",
3
- "version": "4.1.19",
3
+ "version": "4.1.20",
4
4
  "description": "",
5
5
  "main": "./src/index.ts",
6
6
  "author": "",
@@ -19,7 +19,7 @@
19
19
  "lodash-es": "^4.17.21",
20
20
  "uuid": "^13.0.0",
21
21
  "zustand": "^4.5.7",
22
- "@fe-free/core": "4.1.19"
22
+ "@fe-free/core": "4.1.20"
23
23
  },
24
24
  "peerDependencies": {
25
25
  "antd": "^5.27.1",
@@ -29,8 +29,8 @@
29
29
  "i18next-icu": "^2.4.1",
30
30
  "react": "^19.2.0",
31
31
  "react-i18next": "^16.4.0",
32
- "@fe-free/tool": "4.1.19",
33
- "@fe-free/icons": "4.1.19"
32
+ "@fe-free/icons": "4.1.20",
33
+ "@fe-free/tool": "4.1.20"
34
34
  },
35
35
  "scripts": {
36
36
  "test": "echo \"Error: no test specified\" && exit 1",
@@ -121,7 +121,7 @@ function Component() {
121
121
  Add New Session
122
122
  </Button>
123
123
  </div>
124
- <div className="h-[800px] w-[500px] border border-red-500">
124
+ <div className="h-[500px] w-[500px] border border-red-500">
125
125
  <Chat
126
126
  end={
127
127
  <div
package/src/helper.tsx CHANGED
@@ -42,4 +42,24 @@ function generateUUID() {
42
42
  return uuidv4();
43
43
  }
44
44
 
45
- export { generateUUID, RecordLoading };
45
+ function getScrollbarWidth() {
46
+ // 创建一个不可见的 div 元素
47
+ const outer = document.createElement('div');
48
+ outer.style.visibility = 'hidden';
49
+ outer.style.overflow = 'scroll'; // 强制显示滚动条
50
+ document.body.appendChild(outer);
51
+
52
+ // 创建一个内部 div,宽度为 100%
53
+ const inner = document.createElement('div');
54
+ outer.appendChild(inner);
55
+
56
+ // 滚动条宽度 = 外部容器的宽度 - 内部内容的宽度
57
+ const scrollbarWidth = outer.offsetWidth - inner.offsetWidth;
58
+
59
+ // 清理 DOM
60
+ document.body.removeChild(outer);
61
+
62
+ return scrollbarWidth;
63
+ }
64
+
65
+ export { generateUUID, getScrollbarWidth, RecordLoading };
@@ -1,5 +1,9 @@
1
1
  import { PageLayout } from '@fe-free/core';
2
- import { useEffect, useMemo, useRef } from 'react';
2
+ import { AngleLeftOutlined } from '@fe-free/icons';
3
+ import { useMemoizedFn } from 'ahooks';
4
+ import { Button } from 'antd';
5
+ import { useEffect, useMemo, useRef, useState } from 'react';
6
+ import { getScrollbarWidth } from '../helper';
3
7
  import { EnumChatMessageType, type ChatMessage } from '../store/types';
4
8
 
5
9
  interface MessagesProps<AIData> {
@@ -15,6 +19,37 @@ interface MessagesProps<AIData> {
15
19
  renderMessageOfAI?: (props: { message: ChatMessage<AIData> }) => React.ReactNode;
16
20
  }
17
21
 
22
+ function useScrollWidth() {
23
+ const width = useMemo(() => {
24
+ return getScrollbarWidth();
25
+ }, []);
26
+
27
+ return width;
28
+ }
29
+
30
+ function useScrollToBottom({ ref }) {
31
+ const [isNearBottom, setIsNearBottom] = useState(false);
32
+
33
+ useEffect(() => {
34
+ if (!ref.current) {
35
+ return;
36
+ }
37
+
38
+ const handleScroll = () => {
39
+ const isNearBottom =
40
+ ref.current?.scrollTop + ref.current?.clientHeight >= ref.current?.scrollHeight - 200;
41
+ setIsNearBottom(isNearBottom);
42
+ };
43
+ ref.current.addEventListener('scroll', handleScroll);
44
+
45
+ return () => {
46
+ ref.current.removeEventListener('scroll', handleScroll);
47
+ };
48
+ }, []);
49
+
50
+ return isNearBottom;
51
+ }
52
+
18
53
  function Messages<AIData>(props: MessagesProps<AIData>) {
19
54
  const {
20
55
  refList,
@@ -32,8 +67,7 @@ function Messages<AIData>(props: MessagesProps<AIData>) {
32
67
  return messages?.[messages.length - 1];
33
68
  }, [messages]);
34
69
 
35
- // 首次和更新时滚动到最新消息
36
- useEffect(() => {
70
+ const scrollToBottom = useMemoizedFn(() => {
37
71
  if (!lastMessage?.uuid) {
38
72
  return;
39
73
  }
@@ -45,7 +79,12 @@ function Messages<AIData>(props: MessagesProps<AIData>) {
45
79
  element.scrollIntoView({ behavior: 'smooth', block: 'end' });
46
80
  }
47
81
  }, 100);
48
- }, [lastMessage?.uuid]);
82
+ });
83
+
84
+ // 首次和更新时滚动到最新消息
85
+ useEffect(() => {
86
+ scrollToBottom();
87
+ }, [scrollToBottom]);
49
88
 
50
89
  // 数据更新是,如果 dom 处于可视区域,则滚动
51
90
  useEffect(() => {
@@ -66,17 +105,34 @@ function Messages<AIData>(props: MessagesProps<AIData>) {
66
105
  // 如果最后一个元素可见,则滚动到底部
67
106
  const isVisible = top < listBottom && bottom > listTop;
68
107
  if (isVisible) {
69
- element.scrollIntoView({ behavior: 'smooth', block: 'end' });
108
+ scrollToBottom();
70
109
  }
71
110
  }, 100);
72
- }, [lastMessage?.updatedAt, lastMessage?.uuid, ref]);
111
+ }, [lastMessage?.updatedAt, lastMessage?.uuid, ref, scrollToBottom]);
112
+
113
+ const scrollWidth = useScrollWidth();
114
+
115
+ const isNearBottom = useScrollToBottom({ ref });
73
116
 
74
117
  return (
75
118
  <PageLayout>
76
- <div ref={ref} className="flex h-full flex-col overflow-y-auto">
119
+ <div
120
+ ref={ref}
121
+ className="fea-messages-scroll relative flex h-full flex-col overflow-y-auto overflow-x-hidden"
122
+ style={{
123
+ transform: `translateZ(0)`,
124
+ }}
125
+ >
77
126
  {messages?.map((message) => {
78
127
  return (
79
- <div key={message.uuid} data-uuid={message.uuid} className="flex flex-col">
128
+ <div
129
+ key={message.uuid}
130
+ data-uuid={message.uuid}
131
+ className="flex flex-col"
132
+ style={{
133
+ marginRight: `-${scrollWidth}px`,
134
+ }}
135
+ >
80
136
  {renderMessage ? (
81
137
  renderMessage?.({ message })
82
138
  ) : (
@@ -97,6 +153,19 @@ function Messages<AIData>(props: MessagesProps<AIData>) {
97
153
  </div>
98
154
  );
99
155
  })}
156
+ <div className="sticky bottom-2 left-0 right-0 flex justify-center">
157
+ <Button
158
+ shape="circle"
159
+ icon={<AngleLeftOutlined rotate={-90} />}
160
+ onClick={() => {
161
+ scrollToBottom();
162
+ }}
163
+ className="bg-white shadow-lg"
164
+ style={{
165
+ transform: `translateY(${isNearBottom ? 30 : 0}px) scale(${isNearBottom ? 0.1 : 1})`,
166
+ }}
167
+ />
168
+ </div>
100
169
  </div>
101
170
  </PageLayout>
102
171
  );