@fe-free/ai 4.1.18 → 4.1.19

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.19
4
+
5
+ ### Patch Changes
6
+
7
+ - feat: ai
8
+ - @fe-free/core@4.1.19
9
+ - @fe-free/icons@4.1.19
10
+ - @fe-free/tool@4.1.19
11
+
3
12
  ## 4.1.18
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.18",
3
+ "version": "4.1.19",
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.18"
22
+ "@fe-free/core": "4.1.19"
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/icons": "4.1.18",
33
- "@fe-free/tool": "4.1.18"
32
+ "@fe-free/tool": "4.1.19",
33
+ "@fe-free/icons": "4.1.19"
34
34
  },
35
35
  "scripts": {
36
36
  "test": "echo \"Error: no test specified\" && exit 1",
@@ -7,7 +7,7 @@ import type { MSenderProps } from './types';
7
7
 
8
8
  function Actions(
9
9
  props: MSenderProps & {
10
- refText: RefObject<HTMLTextAreaElement>;
10
+ refText: RefObject<HTMLTextAreaElement | null>;
11
11
  type: 'input' | 'record';
12
12
  setType: (type: 'input' | 'record') => void;
13
13
  },
@@ -6,7 +6,7 @@ import { Actions } from './actions';
6
6
  import { RecordAction } from './record';
7
7
  import type { MSenderProps, MSenderRef } from './types';
8
8
 
9
- function Text(props: MSenderProps & { refText: RefObject<HTMLTextAreaElement> }) {
9
+ function Text(props: MSenderProps & { refText: RefObject<HTMLTextAreaElement | null> }) {
10
10
  const { value, onChange, placeholder, refText, autoFocus } = props;
11
11
 
12
12
  return (
@@ -3,7 +3,7 @@ import { useEffect, useMemo, useRef } from 'react';
3
3
  import { EnumChatMessageType, type ChatMessage } from '../store/types';
4
4
 
5
5
  interface MessagesProps<AIData> {
6
- refList?: React.RefObject<HTMLDivElement>;
6
+ refList?: React.RefObject<HTMLDivElement | null>;
7
7
  messages?: ChatMessage<AIData>[];
8
8
  /** 含所有 */
9
9
  renderMessage?: (props: { message: ChatMessage<AIData> }) => React.ReactNode;
@@ -42,7 +42,7 @@ function Messages<AIData>(props: MessagesProps<AIData>) {
42
42
  setTimeout(() => {
43
43
  const element = document.querySelector(`[data-uuid="${lastMessage.uuid}"]`);
44
44
  if (element) {
45
- element.scrollIntoView({ behavior: 'smooth' });
45
+ element.scrollIntoView({ behavior: 'smooth', block: 'end' });
46
46
  }
47
47
  }, 100);
48
48
  }, [lastMessage?.uuid]);
@@ -66,7 +66,7 @@ function Messages<AIData>(props: MessagesProps<AIData>) {
66
66
  // 如果最后一个元素可见,则滚动到底部
67
67
  const isVisible = top < listBottom && bottom > listTop;
68
68
  if (isVisible) {
69
- element.scrollIntoView({ behavior: 'smooth' });
69
+ element.scrollIntoView({ behavior: 'smooth', block: 'end' });
70
70
  }
71
71
  }, 100);
72
72
  }, [lastMessage?.updatedAt, lastMessage?.uuid, ref]);
@@ -1,7 +1,7 @@
1
1
  import Icons from '@fe-free/icons';
2
2
  import type { UploadFile } from 'antd';
3
- import { Button, Divider } from 'antd';
4
- import { useCallback, type RefObject } from 'react';
3
+ import { Button } from 'antd';
4
+ import type { RefObject } from 'react';
5
5
  import SendIcon from '../svgs/send.svg?react';
6
6
  import { FileAction } from './files';
7
7
  import { RecordAction } from './record';
@@ -9,65 +9,29 @@ import type { SenderProps } from './types';
9
9
 
10
10
  function Actions(
11
11
  props: SenderProps & {
12
- refText: RefObject<HTMLTextAreaElement>;
13
- refUpload: RefObject<HTMLDivElement>;
12
+ refText: RefObject<HTMLTextAreaElement | null>;
13
+ refUpload: RefObject<HTMLDivElement | null>;
14
14
  isUploading: boolean;
15
15
  fileList: UploadFile[];
16
16
  setFileList: (fileList: UploadFile[]) => void;
17
17
  fileUrls: string[];
18
18
  setFileUrls: (fileUrls: string[]) => void;
19
+ onSubmit: () => Promise<void>;
19
20
  },
20
21
  ) {
21
22
  const {
22
- refText,
23
- loading,
24
- onSubmit,
25
- value,
26
- onChange,
27
23
  refUpload,
28
24
  isUploading,
29
- setFileList,
30
25
  fileUrls,
31
26
  setFileUrls,
32
27
  allowUpload,
33
28
  allowSpeech,
29
+ loading,
30
+ onSubmit,
34
31
  } = props;
35
32
 
36
33
  const isLoading = loading || isUploading;
37
34
 
38
- const handleSubmit = useCallback(async () => {
39
- if (isLoading || allowSpeech?.recording) {
40
- return;
41
- }
42
-
43
- const newValue = {
44
- ...value,
45
- text: value?.text?.trim(),
46
- };
47
-
48
- // 有内容才提交
49
- if (newValue.text || (newValue.files && newValue.files.length > 0)) {
50
- await Promise.resolve(onSubmit?.(newValue));
51
-
52
- // reset
53
- setFileList([]);
54
- setFileUrls([]);
55
- onChange?.({});
56
-
57
- // focus
58
- refText.current?.focus();
59
- }
60
- }, [
61
- isLoading,
62
- allowSpeech?.recording,
63
- value,
64
- onSubmit,
65
- setFileList,
66
- setFileUrls,
67
- onChange,
68
- refText,
69
- ]);
70
-
71
35
  return (
72
36
  <div className="flex items-center gap-2">
73
37
  <div className="flex flex-1 gap-1">
@@ -80,7 +44,7 @@ function Actions(
80
44
  />
81
45
  )}
82
46
  </div>
83
- <Divider type="vertical" />
47
+ {/* <Divider type="vertical" /> */}
84
48
  <div className="flex items-center gap-2">
85
49
  {allowSpeech && <RecordAction {...props} />}
86
50
  <Button
@@ -89,7 +53,7 @@ function Actions(
89
53
  icon={<Icons component={SendIcon} className="!text-lg" />}
90
54
  loading={isLoading}
91
55
  // disabled={loading}
92
- onClick={handleSubmit}
56
+ onClick={onSubmit}
93
57
  />
94
58
  </div>
95
59
  </div>
@@ -11,7 +11,7 @@ import type { SenderProps } from './types';
11
11
 
12
12
  function FileAction(
13
13
  props: SenderProps & {
14
- refUpload: RefObject<HTMLDivElement>;
14
+ refUpload: RefObject<HTMLDivElement | null>;
15
15
  fileUrls: string[];
16
16
  setFileUrls: (fileUrls: string[]) => void;
17
17
  },
@@ -89,7 +89,7 @@ function FileAction(
89
89
 
90
90
  function FileUpload(
91
91
  props: SenderProps & {
92
- refUpload: RefObject<HTMLDivElement>;
92
+ refUpload: RefObject<HTMLDivElement | null>;
93
93
  fileList: UploadFile[];
94
94
  setFileList: (fileList: UploadFile[]) => void;
95
95
  uploadMaxCount?: number;
@@ -9,8 +9,29 @@ import { Actions } from './actions';
9
9
  import { FileUpload, Files } from './files';
10
10
  import type { SenderProps, SenderRef } from './types';
11
11
 
12
- function Text(props: SenderProps & { refText: RefObject<HTMLTextAreaElement> }) {
13
- const { value, onChange, placeholder, refText } = props;
12
+ function Text(
13
+ props: SenderProps & {
14
+ refText: RefObject<HTMLTextAreaElement | null>;
15
+ onSubmit?: () => void;
16
+ },
17
+ ) {
18
+ const { value, onChange, placeholder, refText, onSubmit } = props;
19
+
20
+ const handleKeyDown = useCallback(
21
+ (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
22
+ // Shift + Enter: 换行(默认行为)
23
+ if (e.key === 'Enter' && e.shiftKey) {
24
+ return;
25
+ }
26
+
27
+ // Enter: 提交
28
+ if (e.key === 'Enter' && !e.shiftKey) {
29
+ e.preventDefault();
30
+ onSubmit?.();
31
+ }
32
+ },
33
+ [onSubmit],
34
+ );
14
35
 
15
36
  return (
16
37
  <Input.TextArea
@@ -19,6 +40,7 @@ function Text(props: SenderProps & { refText: RefObject<HTMLTextAreaElement> })
19
40
  onChange={(e) => {
20
41
  onChange?.({ ...value, text: e.target.value });
21
42
  }}
43
+ onKeyDown={handleKeyDown}
22
44
  placeholder={placeholder}
23
45
  autoSize={{ minRows: 2, maxRows: 8 }}
24
46
  className="mb-1 px-1 py-0"
@@ -32,21 +54,22 @@ function Sender(originProps: SenderProps) {
32
54
  const props = useMemo(() => {
33
55
  return {
34
56
  placeholder:
35
- originProps.placeholder ?? t('@fe-free/ai.sender.describeYourQuestion', '描述你的问题'),
57
+ originProps.placeholder ??
58
+ t('@fe-free/ai.sender.describeYourQuestion', '描述你的问题, shift + enter 换行'),
36
59
  ...originProps,
37
60
  };
38
61
  }, [originProps, t]);
39
62
 
40
63
  const refText = useRef<HTMLTextAreaElement>(null);
41
64
 
42
- const { value, onChange, allowUpload } = props;
65
+ const { value, onChange, allowUpload, onSubmit, loading, allowSpeech } = props;
43
66
  const { filesMaxCount } = allowUpload || {};
44
67
 
45
68
  const refContainer = useRef<HTMLDivElement>(null);
46
69
  const refUpload = useRef<HTMLDivElement>(null);
47
70
  const [dragHover, setDragHover] = useState(false);
48
71
 
49
- useDrop(refContainer, {
72
+ useDrop(allowUpload ? refContainer : null, {
50
73
  onDragEnter: () => {
51
74
  setDragHover(true);
52
75
  },
@@ -94,6 +117,41 @@ function Sender(originProps: SenderProps) {
94
117
  return fileList.some((file) => !file.response?.data?.url);
95
118
  }, [fileList]);
96
119
 
120
+ const handleSubmit = useCallback(async () => {
121
+ const isLoading = loading || isUploading;
122
+
123
+ if (isLoading || allowSpeech?.recording) {
124
+ return;
125
+ }
126
+
127
+ const newValue = {
128
+ ...value,
129
+ text: value?.text?.trim(),
130
+ };
131
+
132
+ // 有内容才提交
133
+ if (newValue.text || (newValue.files && newValue.files.length > 0)) {
134
+ await Promise.resolve(onSubmit?.(newValue));
135
+
136
+ // reset
137
+ setFileList([]);
138
+ setFileUrls([]);
139
+ onChange?.({});
140
+
141
+ // focus
142
+ refText.current?.focus();
143
+ }
144
+ }, [
145
+ loading,
146
+ isUploading,
147
+ allowSpeech?.recording,
148
+ value,
149
+ onSubmit,
150
+ setFileList,
151
+ setFileUrls,
152
+ onChange,
153
+ ]);
154
+
97
155
  return (
98
156
  <div className="fea-sender-wrap">
99
157
  <div
@@ -116,7 +174,7 @@ function Sender(originProps: SenderProps) {
116
174
  setFileUrls={setFileUrls}
117
175
  />
118
176
  <div className="flex">
119
- <Text {...props} refText={refText} />
177
+ <Text {...props} refText={refText} onSubmit={handleSubmit} />
120
178
  </div>
121
179
  <Actions
122
180
  {...props}
@@ -127,14 +185,17 @@ function Sender(originProps: SenderProps) {
127
185
  setFileList={setFileList}
128
186
  fileUrls={fileUrls}
129
187
  setFileUrls={setFileUrls}
188
+ onSubmit={handleSubmit}
130
189
  />
131
- <FileUpload
132
- {...props}
133
- refUpload={refUpload}
134
- fileList={fileList}
135
- setFileList={setFileList}
136
- uploadMaxCount={filesMaxCount ? filesMaxCount - fileUrls.length : undefined}
137
- />
190
+ {allowUpload && (
191
+ <FileUpload
192
+ {...props}
193
+ refUpload={refUpload}
194
+ fileList={fileList}
195
+ setFileList={setFileList}
196
+ uploadMaxCount={filesMaxCount ? filesMaxCount - fileUrls.length : undefined}
197
+ />
198
+ )}
138
199
  </div>
139
200
  <div className="mt-1 text-center text-xs text-03">
140
201
  {t(