@fe-free/ai 4.1.11 → 4.1.13

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,23 @@
1
1
  # @fe-free/ai
2
2
 
3
+ ## 4.1.13
4
+
5
+ ### Patch Changes
6
+
7
+ - feat: ai
8
+ - @fe-free/core@4.1.13
9
+ - @fe-free/icons@4.1.13
10
+ - @fe-free/tool@4.1.13
11
+
12
+ ## 4.1.12
13
+
14
+ ### Patch Changes
15
+
16
+ - feat: ai
17
+ - @fe-free/core@4.1.12
18
+ - @fe-free/icons@4.1.12
19
+ - @fe-free/tool@4.1.12
20
+
3
21
  ## 4.1.11
4
22
 
5
23
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fe-free/ai",
3
- "version": "4.1.11",
3
+ "version": "4.1.13",
4
4
  "description": "",
5
5
  "main": "./src/index.ts",
6
6
  "author": "",
@@ -10,15 +10,18 @@
10
10
  "registry": "https://registry.npmjs.org/"
11
11
  },
12
12
  "dependencies": {
13
+ "@ant-design/plots": "^2.5.0",
14
+ "@ant-design/x-markdown": "^2.1.3",
15
+ "@ant-design/x-sdk": "^2.1.3",
16
+ "@ant-design/x": "^2.1.3",
13
17
  "ahooks": "^3.7.8",
14
18
  "classnames": "^2.5.1",
15
19
  "lodash-es": "^4.17.21",
16
20
  "uuid": "^13.0.0",
17
21
  "zustand": "^4.5.7",
18
- "@fe-free/core": "4.1.11"
22
+ "@fe-free/core": "4.1.13"
19
23
  },
20
24
  "peerDependencies": {
21
- "@ant-design/x-sdk": "^2.1.3",
22
25
  "antd": "^5.27.1",
23
26
  "dayjs": "~1.11.10",
24
27
  "i18next": "^25.7.2",
@@ -26,8 +29,8 @@
26
29
  "i18next-icu": "^2.4.1",
27
30
  "react": "^19.2.0",
28
31
  "react-i18next": "^16.4.0",
29
- "@fe-free/icons": "4.1.11",
30
- "@fe-free/tool": "4.1.11"
32
+ "@fe-free/icons": "4.1.13",
33
+ "@fe-free/tool": "4.1.13"
31
34
  },
32
35
  "scripts": {
33
36
  "test": "echo \"Error: no test specified\" && exit 1",
@@ -11,7 +11,6 @@ import {
11
11
  } from '@fe-free/ai';
12
12
  import { sleep } from '@fe-free/tool';
13
13
  import type { Meta } from '@storybook/react-vite';
14
- import { useDebounce, useUpdateEffect } from 'ahooks';
15
14
  import { Button, Divider } from 'antd';
16
15
  import { set } from 'lodash-es';
17
16
  import { useCallback, useEffect, useMemo } from 'react';
@@ -54,8 +53,6 @@ function Component() {
54
53
  const updateMessage = useChatStore((state) => state.updateMessage);
55
54
  const { chatStatus } = useChatStoreComputed();
56
55
 
57
- const debounceCacheMessages = useDebounce(messages, { wait: 500 });
58
-
59
56
  // init from cache
60
57
  useEffect(() => {
61
58
  const cacheMessages = localStorage.getItem('chatMessages');
@@ -63,48 +60,47 @@ function Component() {
63
60
  setMessages(JSON.parse(cacheMessages));
64
61
  }
65
62
  }, []);
66
- // cache
67
- useUpdateEffect(() => {
68
- localStorage.setItem('chatMessages', JSON.stringify(debounceCacheMessages));
69
- }, [debounceCacheMessages]);
70
63
 
71
64
  const loading =
72
65
  chatStatus === EnumChatMessageStatus.PENDING || chatStatus === EnumChatMessageStatus.STREAMING;
73
66
 
74
- const handleSubmit = useCallback((v) => {
75
- console.log('onSubmit', v);
76
-
77
- const message: ChatMessage<AIData> = {
78
- uuid: generateUUID(),
79
- status: EnumChatMessageStatus.PENDING,
80
- user: {
81
- ...v,
82
- },
83
- };
84
-
85
- addMessage(message);
86
-
87
- // fake
88
- fakeFetchStream(v, {
89
- onUpdate: ({ event, data }) => {
90
- if (event === 'message') {
91
- message.status = EnumChatMessageStatus.STREAMING;
92
-
93
- const preText = message.ai?.data?.text || '';
94
- set(message, 'ai.data.text', preText + data);
95
-
96
- // 假设有 session_id
97
- set(message, 'ai.session_id', '123');
98
-
99
- updateMessage(message);
100
- }
101
- if (event === 'done') {
102
- message.status = EnumChatMessageStatus.DONE;
103
- updateMessage(message);
104
- }
105
- },
106
- });
107
- }, []);
67
+ const handleSubmit = useCallback(
68
+ (v) => {
69
+ console.log('onSubmit', v);
70
+
71
+ const message: ChatMessage<AIData> = {
72
+ uuid: generateUUID(),
73
+ status: EnumChatMessageStatus.PENDING,
74
+ user: {
75
+ ...v,
76
+ },
77
+ };
78
+
79
+ addMessage(message);
80
+
81
+ // fake
82
+ fakeFetchStream(v, {
83
+ onUpdate: ({ event, data }) => {
84
+ if (event === 'message') {
85
+ message.status = EnumChatMessageStatus.STREAMING;
86
+
87
+ const preText = message.ai?.data?.text || '';
88
+ set(message, 'ai.data.text', preText + data);
89
+
90
+ // 假设有 session_id
91
+ set(message, 'ai.session_id', '123');
92
+
93
+ updateMessage(message);
94
+ }
95
+ if (event === 'done') {
96
+ message.status = EnumChatMessageStatus.DONE;
97
+ updateMessage(message);
98
+ }
99
+ },
100
+ });
101
+ },
102
+ [addMessage, updateMessage],
103
+ );
108
104
 
109
105
  return (
110
106
  <div>
@@ -1,11 +1,15 @@
1
1
  import { PageLayout } from '@fe-free/core';
2
2
 
3
3
  function Chat({
4
+ start,
5
+ startClassName,
4
6
  end,
5
7
  endClassName,
6
8
  children,
7
9
  childrenClassName,
8
10
  }: {
11
+ start?: React.ReactNode;
12
+ startClassName?: string;
9
13
  end?: React.ReactNode;
10
14
  endClassName?: string;
11
15
  children?: React.ReactNode;
@@ -14,6 +18,8 @@ function Chat({
14
18
  return (
15
19
  <PageLayout
16
20
  direction="vertical"
21
+ start={start}
22
+ startClassName={startClassName}
17
23
  end={end}
18
24
  endClassName={endClassName}
19
25
  childrenClassName={childrenClassName}
package/src/helper.tsx CHANGED
@@ -30,7 +30,7 @@ function RecordLoading({
30
30
  style={{
31
31
  height: '4px',
32
32
  width: '2px',
33
- animation: `sender-rectangle-${color} infinite 1s ease-in-out ${getAnimationDelay(index)}s`,
33
+ animation: `fea-sender-rectangle-${color} infinite 1s ease-in-out ${getAnimationDelay(index)}s`,
34
34
  }}
35
35
  />
36
36
  ))}
package/src/index.ts CHANGED
@@ -4,8 +4,10 @@ export { FileView, FileViewList } from './files';
4
4
  export { generateUUID } from './helper';
5
5
  export { MSender } from './m_sender';
6
6
  export type { MSenderProps, MSenderRef } from './m_sender';
7
- export { MessageActions, Messages } from './messages';
8
- export type { MessagesProps } from './messages';
7
+ export { CustomMarkdown, Markdown } from './markdown';
8
+ export type { CustomMarkdownProps, MarkdownProps } from './markdown';
9
+ export { MessageActions, MessageThink, Messages } from './messages';
10
+ export type { MessageThinkProps, MessagesProps } from './messages';
9
11
  export { Sender } from './sender';
10
12
  export type { SenderProps, SenderRef } from './sender';
11
13
  export { createChatStore } from './store';
@@ -55,6 +55,9 @@ function MSender(originProps: MSenderProps) {
55
55
  className={classNames(
56
56
  'fea-m-sender relative flex items-end rounded-xl border border-01 bg-white p-2.5',
57
57
  )}
58
+ style={{
59
+ boxShadow: '0px 2px 12px 0px #00000014',
60
+ }}
58
61
  >
59
62
  <div className="flex flex-1">
60
63
  <Text {...props} refText={refText} />
@@ -0,0 +1,318 @@
1
+ import { Column, Line, Pie, Scatter } from '@ant-design/plots';
2
+ import React, { useMemo } from 'react';
3
+
4
+ // 类型定义
5
+ interface ChartData {
6
+ columns: string[];
7
+ rows: (string | number)[][];
8
+ }
9
+
10
+ interface ChartConfigBase {
11
+ chart_type: 'bar' | 'pie' | 'table' | 'scatter';
12
+ x_field?: string;
13
+ y_field?: string;
14
+ angle_field?: string;
15
+ color_field?: string;
16
+ title: string;
17
+ }
18
+
19
+ interface LineChartConfig {
20
+ chart_type: 'line';
21
+ x_field?: string;
22
+ y_field: string | string[];
23
+ angle_field?: string;
24
+ color_field?: string;
25
+ title: string;
26
+ }
27
+
28
+ // @ts-ignore
29
+ interface ChartConfig extends ChartConfigBase, ChartConfigLine {}
30
+
31
+ // 错误处理组件
32
+ function ChartError(props: { children?: React.ReactNode }) {
33
+ const { children } = props;
34
+ return (
35
+ <div className="fea-markdown-body-block-chart">
36
+ <div style={{ textAlign: 'center', padding: '20px' }}>{children || '图表发生错误'}</div>
37
+ </div>
38
+ );
39
+ }
40
+
41
+ class ErrorBoundary extends React.Component {
42
+ state = { hasError: false };
43
+
44
+ static getDerivedStateFromError(error) {
45
+ console.error('ErrorBoundary:', error);
46
+ return { hasError: true };
47
+ }
48
+
49
+ componentDidCatch(error, info) {
50
+ console.error('Error caught:', error, info);
51
+ }
52
+
53
+ render() {
54
+ if (this.state.hasError) {
55
+ return <ChartError />;
56
+ }
57
+ return this.props.children;
58
+ }
59
+ }
60
+
61
+ // 图表容器组件
62
+ function ChartContainer(props: { title: string; children: React.ReactNode }) {
63
+ const { title, children } = props;
64
+ return (
65
+ <div className="fea-markdown-body-block-chart">
66
+ <div className="fea-markdown-body-block-chart-title">{title}</div>
67
+ {children}
68
+ </div>
69
+ );
70
+ }
71
+
72
+ // 饼图组件
73
+ function PieChart(props: { data: ChartData; chart: ChartConfig }) {
74
+ const { data, chart } = props;
75
+ const { columns, rows } = data;
76
+ const { angle_field, color_field } = chart;
77
+
78
+ if (!angle_field || !color_field) {
79
+ return <ChartError />;
80
+ }
81
+
82
+ const angleIndex = columns.indexOf(angle_field);
83
+ const colorIndex = columns.indexOf(color_field);
84
+
85
+ if (angleIndex === -1 || colorIndex === -1) {
86
+ return <ChartError />;
87
+ }
88
+
89
+ // 转换数据格式为 Ant Design Charts 需要的格式
90
+ const chartData = rows.map((row) => ({
91
+ [color_field]: row[colorIndex],
92
+ [angle_field]: Number(row[angleIndex]),
93
+ }));
94
+
95
+ const config = {
96
+ data: chartData,
97
+ angleField: angle_field,
98
+ colorField: color_field,
99
+ label: {
100
+ text: angle_field,
101
+ style: {
102
+ fontWeight: 'bold',
103
+ },
104
+ },
105
+ legend: {
106
+ color: {
107
+ title: false,
108
+ position: 'right',
109
+ rowPadding: 5,
110
+ },
111
+ },
112
+ };
113
+
114
+ return <Pie {...config} />;
115
+ }
116
+
117
+ // 折线图组件
118
+ function LineChart(props: { data: ChartData; chart: LineChartConfig }) {
119
+ const { data, chart } = props;
120
+ const { columns, rows } = data;
121
+ const { x_field, y_field } = chart;
122
+
123
+ if (!x_field || !y_field) {
124
+ return <ChartError />;
125
+ }
126
+
127
+ const xIndex = columns.indexOf(x_field);
128
+ if (xIndex === -1) {
129
+ return <ChartError />;
130
+ }
131
+
132
+ // 处理 y_field 为数组的情况
133
+ if (Array.isArray(y_field)) {
134
+ // 验证所有 y_field 是否存在于 columns 中
135
+ const yIndices = y_field.map((field) => columns.indexOf(field));
136
+ if (yIndices.some((index) => index === -1)) {
137
+ return <ChartError />;
138
+ }
139
+
140
+ // 转换数据格式为 Ant Design Charts 需要的格式(长数据格式)
141
+ const chartData: any[] = [];
142
+ rows.forEach((row) => {
143
+ y_field.forEach((field, index) => {
144
+ const value = row[yIndices[index]];
145
+ if (value !== null && value !== undefined) {
146
+ chartData.push({
147
+ [x_field]: row[xIndex],
148
+ type: field,
149
+ value: Number(value),
150
+ });
151
+ }
152
+ });
153
+ });
154
+
155
+ const config = {
156
+ data: chartData,
157
+ xField: x_field,
158
+ yField: 'value',
159
+ seriesField: 'type',
160
+ };
161
+
162
+ console.log('config', config);
163
+
164
+ return <Line {...config} />;
165
+ } else {
166
+ // 处理单个 y_field 的情况(保持向后兼容)
167
+ const yIndex = columns.indexOf(y_field);
168
+ if (yIndex === -1) {
169
+ return <ChartError />;
170
+ }
171
+
172
+ // 转换数据格式为 Ant Design Charts 需要的格式
173
+ const chartData = rows.map((row) => ({
174
+ [x_field]: row[xIndex],
175
+ [y_field]: Number(row[yIndex]),
176
+ }));
177
+
178
+ const config = {
179
+ data: chartData,
180
+ xField: x_field,
181
+ yField: y_field,
182
+ };
183
+
184
+ return <Line {...config} />;
185
+ }
186
+ }
187
+
188
+ // 柱状图组件
189
+ function BarChart(props: { data: ChartData; chart: ChartConfig }) {
190
+ const { data, chart } = props;
191
+ const { columns, rows } = data;
192
+ const { x_field, y_field } = chart;
193
+
194
+ if (!x_field || !y_field) {
195
+ return <ChartError />;
196
+ }
197
+
198
+ const xIndex = columns.indexOf(x_field);
199
+ const yIndex = columns.indexOf(y_field);
200
+
201
+ if (xIndex === -1 || yIndex === -1) {
202
+ return <ChartError />;
203
+ }
204
+
205
+ // 转换数据格式为 Ant Design Charts 需要的格式
206
+ const chartData = rows.map((row) => ({
207
+ [x_field]: row[xIndex],
208
+ [y_field]: Number(row[yIndex]),
209
+ }));
210
+
211
+ const config = {
212
+ data: chartData,
213
+ xField: x_field,
214
+ yField: y_field,
215
+ };
216
+
217
+ return <Column {...config} />;
218
+ }
219
+
220
+ function ScatterChart(props: { data: ChartData; chart: ChartConfig }) {
221
+ const { data, chart } = props;
222
+ const { columns, rows } = data;
223
+ const { x_field, y_field } = chart;
224
+
225
+ if (!x_field || !y_field) {
226
+ return <ChartError />;
227
+ }
228
+
229
+ const xIndex = columns.indexOf(x_field);
230
+ const yIndex = columns.indexOf(y_field);
231
+
232
+ if (xIndex === -1 || yIndex === -1) {
233
+ return <ChartError />;
234
+ }
235
+
236
+ const chartData = rows.map((row) => ({
237
+ [x_field]: row[xIndex],
238
+ [y_field]: row[yIndex],
239
+ }));
240
+
241
+ const config = {
242
+ data: chartData,
243
+ xField: x_field,
244
+ yField: y_field,
245
+ };
246
+
247
+ return <Scatter {...config} />;
248
+ }
249
+
250
+ // 主 ChartBlock 组件
251
+ function ChartBlockBase(props: any) {
252
+ const { children } = props;
253
+
254
+ const chartData = useMemo(() => {
255
+ try {
256
+ return JSON.parse(children);
257
+ } catch (error) {
258
+ console.error('Failed to parse chart data:', error);
259
+ return null;
260
+ }
261
+ }, [children]);
262
+
263
+ if (!chartData) {
264
+ return <ChartError />;
265
+ }
266
+
267
+ const { data, chart } = chartData;
268
+ const { chart_type, title } = chart;
269
+
270
+ switch (chart_type) {
271
+ case 'pie':
272
+ return (
273
+ <ChartContainer title={title}>
274
+ <PieChart data={data} chart={chart} />
275
+ </ChartContainer>
276
+ );
277
+ case 'line':
278
+ return (
279
+ <ChartContainer title={title}>
280
+ <LineChart data={data} chart={chart} />
281
+ </ChartContainer>
282
+ );
283
+ case 'bar':
284
+ return (
285
+ <ChartContainer title={title}>
286
+ <BarChart data={data} chart={chart} />
287
+ </ChartContainer>
288
+ );
289
+ case 'scatter':
290
+ return (
291
+ <ChartContainer title={title}>
292
+ <ScatterChart data={data} chart={chart} />
293
+ </ChartContainer>
294
+ );
295
+ case 'table':
296
+ // 表格类型暂不处理
297
+ return null;
298
+ default:
299
+ return <ChartError>不支持的图表类型:{chart_type}</ChartError>;
300
+ }
301
+ }
302
+
303
+ function ChartBlock(props: any) {
304
+ const { children } = props;
305
+
306
+ // 大模型会返回一些奇怪字符,需要去掉
307
+ // 不间断空格
308
+ // eslint-disable-next-line no-irregular-whitespace
309
+ const content = children?.replace(/ /g, '');
310
+
311
+ return (
312
+ <ErrorBoundary>
313
+ <ChartBlockBase>{content}</ChartBlockBase>
314
+ </ErrorBoundary>
315
+ );
316
+ }
317
+
318
+ export { ChartBlock };
@@ -0,0 +1,29 @@
1
+ import { CodeHighlighter, Mermaid } from '@ant-design/x';
2
+ import type { ComponentProps } from '@ant-design/x-markdown';
3
+ import { memo } from 'react';
4
+ import { ChartBlock } from './chart';
5
+ import { HMChartBlock } from './hm_chart';
6
+
7
+ const CodeComponent = memo((props: ComponentProps) => {
8
+ const { className, children } = props;
9
+
10
+ const lang = className?.match(/language-(\w+)/)?.[1] || '';
11
+
12
+ if (typeof children !== 'string') return null;
13
+
14
+ if (lang === 'mermaid') {
15
+ return <Mermaid>{children}</Mermaid>;
16
+ }
17
+
18
+ if (lang === 'chart') {
19
+ return <ChartBlock>{children}</ChartBlock>;
20
+ }
21
+
22
+ if (lang === 'hmchart') {
23
+ return <HMChartBlock>{children}</HMChartBlock>;
24
+ }
25
+
26
+ return <CodeHighlighter lang={lang}>{children}</CodeHighlighter>;
27
+ });
28
+
29
+ export { CodeComponent };