@baishuyun/chat-sdk 0.0.9 → 0.0.10

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.
Files changed (51) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/chat-sdk.js +13923 -13642
  3. package/dist/chat-sdk.js.map +1 -1
  4. package/dist/chat-sdk.umd.cjs +159 -159
  5. package/dist/chat-sdk.umd.cjs.map +1 -1
  6. package/dist/index.css +1 -1
  7. package/package.json +4 -4
  8. package/src/chat.tsx +9 -0
  9. package/src/components/biz-comp/FakeBotMsg.tsx +6 -1
  10. package/src/components/biz-comp/chat-client.tsx +1 -0
  11. package/src/components/biz-comp/conversation.tsx +24 -45
  12. package/src/components/biz-comp/dash-widget-icon.tsx +17 -0
  13. package/src/components/biz-comp/field-icon.tsx +7 -2
  14. package/src/components/biz-comp/highlight-msg.tsx +3 -0
  15. package/src/components/biz-comp/messages.tsx +5 -4
  16. package/src/components/biz-comp/multi-modal-input/clear-btn.tsx +1 -0
  17. package/src/components/biz-comp/multi-modal-input/index.tsx +7 -3
  18. package/src/components/biz-comp/suggestions.tsx +5 -5
  19. package/src/components/bs-ui/border-color-animation.tsx +4 -1
  20. package/src/components/bs-ui/bs-icons.tsx +70 -0
  21. package/src/components/bs-ui/card.tsx +8 -10
  22. package/src/components/bs-ui/chat-area-header.tsx +5 -3
  23. package/src/components/bs-ui/collapsible-txt-msg.tsx +21 -6
  24. package/src/components/bs-ui/fields-generating-indicator.tsx +76 -31
  25. package/src/components/bs-ui/fields-previewer.tsx +4 -0
  26. package/src/components/bs-ui/form-info-editor.tsx +21 -7
  27. package/src/components/bs-ui/icon-btn.tsx +7 -2
  28. package/src/components/bs-ui/primary-confirm-btn.tsx +14 -7
  29. package/src/components/bs-ui/primary-entry-btn.tsx +14 -4
  30. package/src/components/bs-ui/scroll-to-bottom-btn.tsx +28 -0
  31. package/src/const/index.ts +26 -2
  32. package/src/hooks/use-frame-mode.ts +1 -0
  33. package/src/hooks/use-scroll-to-bottom.ts +127 -0
  34. package/src/plugins/mcp-form-builder-plugin/components/create-form-confirm.tsx +1 -1
  35. package/src/plugins/mcp-form-builder-plugin/index.ts +1 -0
  36. package/src/plugins/report-query-plugin/components/query-msg-part.tsx +30 -3
  37. package/src/plugins/report-query-plugin/components/result-cards/DataTableCard.tsx +12 -3
  38. package/src/plugins/report-query-plugin/components/result-cards/DataTableFields.tsx +1 -1
  39. package/src/plugins/report-query-plugin/components/result-cards/FilterCondition.tsx +11 -11
  40. package/src/plugins/report-query-plugin/index.ts +4 -0
  41. package/src/plugins/report-query-plugin/utils/field-enhance.ts +39 -0
  42. package/src/plugins/report-query-plugin/utils/get-field-group.ts +691 -0
  43. package/src/plugins/report-query-plugin/utils/get-field-icon.ts +26 -0
  44. package/src/plugins/report-query-plugin/utils/get-group-rule.ts +21 -0
  45. package/src/plugins/report-query-plugin/utils.tsx +40 -50
  46. package/src/stories/DashWidgetIcon.stories.tsx +132 -0
  47. package/src/stories/IconBtn.stories.tsx +14 -8
  48. package/src/stories/{PrimaryConfirmBtn.stories.ts → PrimaryConfirmBtn.stories.tsx} +16 -0
  49. package/src/stories/ScrollToBottom.stories.tsx +24 -0
  50. package/src/stories/SplitLine.stories.tsx +4 -4
  51. package/src/stories/fields-generating.stories.tsx +19 -0
@@ -0,0 +1,28 @@
1
+ import { cn } from '@/lib/utils';
2
+ import { transCls } from '@/const/ui';
3
+ import { ScrollArrowDownIcon } from './bs-icons';
4
+
5
+ export interface ScrollToBottomProps {
6
+ onClick?: () => void;
7
+ className?: string;
8
+ }
9
+
10
+ export const ScrollToBottomBtn = ({ onClick, className }: ScrollToBottomProps) => {
11
+ return (
12
+ <button
13
+ type="button"
14
+ onClick={onClick}
15
+ className={cn(
16
+ 'inline-flex items-center justify-center',
17
+ 'w-[36px] h-[36px] rounded-full',
18
+ 'bg-white text-[#666] shadow-[0px_0px_14px_0px_rgba(0,0,0,0.1)]',
19
+ 'hover:shadow-[0px_14px_14px_0px_rgba(0,0,0,0.14)]',
20
+ 'cursor-pointer hover:text-[#333] aspect-square',
21
+ transCls,
22
+ className
23
+ )}
24
+ >
25
+ <ScrollArrowDownIcon />
26
+ </button>
27
+ );
28
+ };
@@ -1,4 +1,4 @@
1
- import { FieldType } from '@baishuyun/types';
1
+ import { FieldType, IDashWidgetType } from '@baishuyun/types';
2
2
 
3
3
  export const FieldTypeNameMap: Record<FieldType, string> = {
4
4
  text: '输入框',
@@ -30,7 +30,7 @@ export const FieldTypeNameMap: Record<FieldType, string> = {
30
30
  subform: '子表单',
31
31
  };
32
32
 
33
- export const FIELD_ICON_CODE_MAP: Record<FieldType, string> = {
33
+ export const FIELD_ICON_CODE_MAP: Record<FieldType & 'formula', string> = {
34
34
  address: '&#xe881;',
35
35
  subform: '&#xe847;',
36
36
  password: '&#xe854;', // text
@@ -59,3 +59,27 @@ export const FIELD_ICON_CODE_MAP: Record<FieldType, string> = {
59
59
  deptgroup: '&#xe852;',
60
60
  unknown: '&#xe848;',
61
61
  };
62
+
63
+ export const DASH_WIDGET_ICON_CODE_MAP: Record<IDashWidgetType, string> = {
64
+ metric_table: '&#xe88c;', // 指标图
65
+ gauge_chart: '&#xe88d;', // 仪表盘
66
+ column_chart: '&#xe892;', // 柱形图
67
+ funnel_chart: '&#xe89c;', // 漏斗图
68
+ line_chart: '&#xe88b;', // 折线图
69
+ pie_chart: '&#xe89e;', // 饼图
70
+ area_chart: '&#xe898;', // 面积图
71
+ radar_chart: '&#xe893;', // 雷达图
72
+ bar_chart: '&#xe896;', // 条形图
73
+ gantt: '&#xe899;', // 甘特图
74
+ multi_axes_chart: '&#xe88f;', // 双轴图
75
+ data_table: '&#xe894;', // 数据表
76
+ background_table: '&#xe894;', // 数据表(同 data_table)
77
+ pivot_table: '&#xe891;', // 透视图
78
+ map_chart: '&#xe89d;', // 地图
79
+ newMap: '&#xe89d;', // 地图
80
+ card: '&#xe8f4;', // 卡片
81
+ rili: '&#xe849;', // 日历(日期)
82
+ description: '&#xe88e;', // 文本组件
83
+ image: '&#xe890;', // 图片组件
84
+ gauge_imageMore: '&#xe88d;', // 仪表盘(同 gauge_chart)
85
+ };
@@ -10,6 +10,7 @@ export const useChatStatus = () => {
10
10
  return {
11
11
  status,
12
12
  setStatus,
13
+ isHide: status === 'hide',
13
14
  isVisible: status !== 'hide',
14
15
  isFloat: status === 'float',
15
16
  isDock: status === 'dock',
@@ -0,0 +1,127 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+
3
+ export function useScrollToBottom() {
4
+ const containerRef = useRef<HTMLDivElement>(null);
5
+ const endRef = useRef<HTMLDivElement>(null);
6
+ const [isAtBottom, setIsAtBottom] = useState(true);
7
+ const isAtBottomRef = useRef(true);
8
+ const isUserScrollingRef = useRef(false);
9
+
10
+ // Keep ref in sync with state
11
+ useEffect(() => {
12
+ isAtBottomRef.current = isAtBottom;
13
+ }, [isAtBottom]);
14
+
15
+ const checkIfAtBottom = useCallback(() => {
16
+ if (!containerRef.current) {
17
+ return true;
18
+ }
19
+ const { scrollTop, scrollHeight, clientHeight } = containerRef.current;
20
+ return scrollTop + clientHeight >= scrollHeight - 100;
21
+ }, []);
22
+
23
+ const scrollToBottom = useCallback((behavior: ScrollBehavior = 'smooth') => {
24
+ if (!containerRef.current) {
25
+ return;
26
+ }
27
+ containerRef.current.scrollTo({
28
+ top: containerRef.current.scrollHeight,
29
+ behavior,
30
+ });
31
+ }, []);
32
+
33
+ // Handle user scroll events
34
+ useEffect(() => {
35
+ const container = containerRef.current;
36
+ if (!container) {
37
+ return;
38
+ }
39
+
40
+ let scrollTimeout: ReturnType<typeof setTimeout>;
41
+
42
+ const handleScroll = () => {
43
+ // Mark as user scrolling
44
+ isUserScrollingRef.current = true;
45
+ clearTimeout(scrollTimeout);
46
+
47
+ // Update isAtBottom state
48
+ const atBottom = checkIfAtBottom();
49
+ setIsAtBottom(atBottom);
50
+ isAtBottomRef.current = atBottom;
51
+
52
+ // Reset user scrolling flag after scroll ends
53
+ scrollTimeout = setTimeout(() => {
54
+ isUserScrollingRef.current = false;
55
+ }, 150);
56
+ };
57
+
58
+ container.addEventListener('scroll', handleScroll, { passive: true });
59
+ return () => {
60
+ container.removeEventListener('scroll', handleScroll);
61
+ clearTimeout(scrollTimeout);
62
+ };
63
+ }, [checkIfAtBottom]);
64
+
65
+ // Auto-scroll when content changes
66
+ useEffect(() => {
67
+ const container = containerRef.current;
68
+ if (!container) {
69
+ return;
70
+ }
71
+
72
+ const scrollIfNeeded = () => {
73
+ // Only auto-scroll if user was at bottom and isn't actively scrolling
74
+ if (isAtBottomRef.current && !isUserScrollingRef.current) {
75
+ requestAnimationFrame(() => {
76
+ container.scrollTo({
77
+ top: container.scrollHeight,
78
+ behavior: 'instant',
79
+ });
80
+ setIsAtBottom(true);
81
+ isAtBottomRef.current = true;
82
+ });
83
+ }
84
+ };
85
+
86
+ // Watch for DOM changes
87
+ const mutationObserver = new MutationObserver(scrollIfNeeded);
88
+ mutationObserver.observe(container, {
89
+ childList: true,
90
+ subtree: true,
91
+ characterData: true,
92
+ });
93
+
94
+ // Watch for size changes
95
+ const resizeObserver = new ResizeObserver(scrollIfNeeded);
96
+ resizeObserver.observe(container);
97
+
98
+ // Also observe children for size changes
99
+ for (const child of Array.from(container.children)) {
100
+ resizeObserver.observe(child);
101
+ }
102
+
103
+ return () => {
104
+ mutationObserver.disconnect();
105
+ resizeObserver.disconnect();
106
+ };
107
+ }, []);
108
+
109
+ function onViewportEnter() {
110
+ setIsAtBottom(true);
111
+ isAtBottomRef.current = true;
112
+ }
113
+
114
+ function onViewportLeave() {
115
+ setIsAtBottom(false);
116
+ isAtBottomRef.current = false;
117
+ }
118
+
119
+ return {
120
+ containerRef,
121
+ endRef,
122
+ isAtBottom,
123
+ scrollToBottom,
124
+ onViewportEnter,
125
+ onViewportLeave,
126
+ };
127
+ }
@@ -51,7 +51,7 @@ export const CreateFormConfirm = memo(
51
51
  <FakeBotMessage>
52
52
  {success ? (
53
53
  <>
54
- 已为你创建{designInfo.formName}
54
+ <span className="text-[#4d609f]">已为你创建{designInfo.formName}</span>
55
55
  <FormInfoEditor
56
56
  onConfirm={(name, icon) => {
57
57
  if (!formRef.current) {
@@ -43,6 +43,7 @@ export class McpFormBuilderPlugin extends FormBuilderBasePlugin<McpFormBuilderPl
43
43
  onBeforeChatRender: () => {
44
44
  return {
45
45
  appendLoadingIndicator: true,
46
+ placeholder: '描述你的需求,如:搭建一张供应商管理表',
46
47
  };
47
48
  },
48
49
  };
@@ -7,18 +7,45 @@ import { usePluginCtx } from '@/hooks/use-plugin-ctx';
7
7
  import { ReportQueryPlugin } from '@/index';
8
8
  import { PLUGIN_NAME } from '../const';
9
9
  import { IReportQueryContext } from '../types';
10
+ import { FakeBotMessage } from '@/components/biz-comp/FakeBotMsg';
11
+ import { HighlightMsg } from '@/components/biz-comp/highlight-msg';
12
+ import { CollapsibleTxtMsg } from '@/components/bs-ui/collapsible-txt-msg';
13
+ import { AggregateTableIcon } from '@/components/bs-ui/bs-icons';
10
14
 
11
15
  export const QueryMsgPart = ({ part, ...rest }: MsgPartCompProps) => {
12
16
  const textPart = part as TextUIPart;
13
17
  const ctx = usePluginCtx<IReportQueryContext, ReportQueryPlugin>(PLUGIN_NAME);
14
18
 
19
+ if (!ctx) {
20
+ return null;
21
+ }
22
+
15
23
  if (IsQueryResultPart(part)) {
16
24
  const result = GetQueryResult(part);
17
- if (ctx && result) {
18
- ctx.queryResult = result;
19
- ctx.queryResult.appID = ctx.appInfo?.appId || '';
25
+
26
+ if (!result) {
27
+ return null;
28
+ }
29
+
30
+ if (result.isAggregate) {
31
+ return (
32
+ <FakeBotMessage headless className="w-full">
33
+ <HighlightMsg>已为你创建聚合表</HighlightMsg>
34
+ <CollapsibleTxtMsg
35
+ icon={
36
+ <span className="text-[#0265ff]">
37
+ <AggregateTableIcon />
38
+ </span>
39
+ }
40
+ title={result.title!}
41
+ disableExpand
42
+ />
43
+ </FakeBotMessage>
44
+ );
20
45
  }
21
46
 
47
+ ctx.queryResult = result;
48
+ ctx.queryResult.appID = ctx.appInfo?.appId || '';
22
49
  return <DataTableCard {...(result || {})}></DataTableCard>;
23
50
  }
24
51
 
@@ -10,6 +10,7 @@ import { ConditionField, DataSourceField, DimensionField, MetricField } from './
10
10
  import { CardFieldsWrapper } from '@/components/bs-ui/card-field';
11
11
  import { FilterCondition } from './FilterCondition';
12
12
  import { SplitLine } from '@/components/bs-ui/split-line';
13
+ import { DashWidgetIcon } from '@/components/biz-comp/dash-widget-icon';
13
14
 
14
15
  export const DataTableCard = (props: IQueryResult) => {
15
16
  const ctx = usePluginCtx<IReportQueryContext, ReportQueryPlugin>(PLUGIN_NAME);
@@ -28,20 +29,28 @@ export const DataTableCard = (props: IQueryResult) => {
28
29
  const hasMetrics = props.metrics && props.metrics.length > 0;
29
30
 
30
31
  return (
31
- <CollapsibleTxtMsg title={props?.title || ''} icon={<DataTableIcon />} defaultOpen>
32
+ <CollapsibleTxtMsg
33
+ title={props?.title || ''}
34
+ icon={<DashWidgetIcon type={props.type} />}
35
+ defaultOpen
36
+ >
32
37
  <CardFieldsWrapper>
33
38
  {name && <DataSourceField>{name}</DataSourceField>}
34
39
 
35
40
  {props.fields?.length ? (
36
- <DimensionField>{props.fields.map((field) => field.title).join('、')}</DimensionField>
41
+ <DimensionField>{props.fields?.map((field) => field.title).join('、')}</DimensionField>
37
42
  ) : null}
38
43
 
39
44
  {props.xFields?.length ? (
40
45
  <DimensionField isCol>
41
- {props.xFields.map((field) => field.title).join('、')}
46
+ {props.xFields?.map((field) => field.title).join('、')}
42
47
  </DimensionField>
43
48
  ) : null}
44
49
 
50
+ {props.yFields?.length ? (
51
+ <DimensionField>{props.yFields.map((field) => field.title).join('、')}</DimensionField>
52
+ ) : null}
53
+
45
54
  {props.filter?.conds?.length ? (
46
55
  <ConditionField>
47
56
  <FilterCondition {...props.filter} />
@@ -35,7 +35,7 @@ export const MetricField = ({
35
35
  return (
36
36
  <div className={cn('flex items-center justify-between', className)}>
37
37
  <div className="flex min-w-0 items-center gap-[6px]">
38
- <span className="shrink-0 text-[#0265FF]">
38
+ <span className="shrink-0 text-[#6f829e]">
39
39
  <FieldIcon type={metric.type} />
40
40
  </span>
41
41
  <span className="truncate text-sm leading-normal text-[#030303]">{metric.title}</span>
@@ -333,24 +333,24 @@ const filterToDesc = (field: IFilterField): ReactNode => {
333
333
  // 为空/不为空 不需要显示值
334
334
  if (method === 'empty' || method === 'not_empty') {
335
335
  return (
336
- <div className="leading-[normal]">
336
+ <span className="leading-[normal]">
337
337
  <span className="text-[#030303]">{title}</span>
338
- <span className="text-[#4d609f]">&nbsp;&nbsp;</span>
338
+ <span className="text-[#4d609f]">&nbsp;</span>
339
339
  {renderMethodLabel(methodLabel)}
340
- </div>
340
+ </span>
341
341
  );
342
342
  }
343
343
 
344
344
  return (
345
- <div className="leading-[normal]">
345
+ <span className="leading-[normal]">
346
346
  <span className="text-[#030303]">{title}</span>
347
- <span className="text-[#4d609f]">&nbsp;&nbsp;</span>
347
+ <span className="text-[#4d609f]">&nbsp;</span>
348
348
  {prefix && <span className="text-[#666]">{prefix}</span>}
349
349
  {renderMethodLabel(methodLabel)}
350
- <span className="text-[#4d609f]">&nbsp;&nbsp;</span>
350
+ <span className="text-[#4d609f]">&nbsp;</span>
351
351
  <span className="text-[#0265ff]">{valueStr}</span>
352
352
  {suffix && <span className="text-[#666]">&nbsp;{suffix}</span>}
353
- </div>
353
+ </span>
354
354
  );
355
355
  };
356
356
 
@@ -362,13 +362,13 @@ export const FilterCondition = ({ conds, rel }: IQueryFilter): ReactNode => {
362
362
  const relLabel = rel === 'or' ? '或' : '且';
363
363
 
364
364
  return (
365
- <div className="font-['Alibaba_PuHuiTi_2.0:55_Regular',sans-serif] text-[14px] text-[#030303] whitespace-pre-wrap">
365
+ <span className="font-['Alibaba_PuHuiTi_2.0:55_Regular',sans-serif] text-[14px] text-[#030303] whitespace-pre-wrap">
366
366
  {conds.map((field, index) => (
367
- <div key={`${field.name}-${index}`} className="leading-[normal]">
367
+ <span key={`${field.name}-${index}`} className="leading-[normal]">
368
368
  {filterToDesc(field)}
369
369
  {index < conds.length - 1 && <span className="text-[#999]">&nbsp;{relLabel}&nbsp;</span>}
370
- </div>
370
+ </span>
371
371
  ))}
372
- </div>
372
+ </span>
373
373
  );
374
374
  };
@@ -68,6 +68,10 @@ export class ReportQueryPlugin extends ChatAreaPlugin<IReportQueryContext> {
68
68
  };
69
69
 
70
70
  public setForms = (forms: Array<IDashForm>) => {
71
+ if (!forms || forms.length === 0) {
72
+ return;
73
+ }
74
+
71
75
  const formMap = new Map<string, IDashForm>();
72
76
  forms.forEach((form) => {
73
77
  formMap.set(form.entryId, form);
@@ -0,0 +1,39 @@
1
+ import {
2
+ FieldType,
3
+ IDashWidgetType,
4
+ IQueryResultMetric,
5
+ IQueryResultXField,
6
+ } from '@baishuyun/types';
7
+ import { GetXFieldGroupStr } from './get-field-group';
8
+ import { getFieldIcon } from './get-field-icon';
9
+ import { getGroupRule } from './get-group-rule';
10
+
11
+ export const XFieldEnhance = (
12
+ field: IQueryResultXField,
13
+ widgetType: IDashWidgetType,
14
+ index: number
15
+ ) => {
16
+ return {
17
+ ...field,
18
+ id: index,
19
+ icon: getFieldIcon(field.type as FieldType),
20
+ group: GetXFieldGroupStr(widgetType, field),
21
+ groupRule: getGroupRule(field.type as FieldType, widgetType),
22
+ };
23
+ };
24
+
25
+ export const MetricEnhance = (
26
+ field: IQueryResultMetric,
27
+ widgetType: IDashWidgetType,
28
+ index: number
29
+ ) => {
30
+ return {
31
+ ...field,
32
+ id: index,
33
+ icon: getFieldIcon(field.type as FieldType),
34
+ group: GetXFieldGroupStr(widgetType, {
35
+ type: field.type,
36
+ name: field.name || field.title,
37
+ }),
38
+ };
39
+ };