@baishuyun/ai-design 2.0.47 → 2.0.49

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 (36) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/package.json +4 -4
  3. package/src/components/agent-drawer.tsx +40 -6
  4. package/src/components/agent-helper/agent-helper-card/index.tsx +3 -3
  5. package/src/components/agent-helper/app-select/index.tsx +7 -7
  6. package/src/components/agent-helper/condition-item/index.tsx +1 -0
  7. package/src/components/agent-helper/field-value-input/dept-select.tsx +447 -0
  8. package/src/components/agent-helper/field-value-input/index.tsx +3 -2
  9. package/src/components/agent-helper/field-value-input/picker-inputs.tsx +1 -1
  10. package/src/components/agent-helper/form-select/index.tsx +8 -5
  11. package/src/components/agent-helper/relational-operator-select/index.tsx +37 -23
  12. package/src/components/agent-helper/table/index.tsx +6 -2
  13. package/src/components/agent-select.tsx +115 -35
  14. package/src/components/card.tsx +19 -6
  15. package/src/components/fields-portal.tsx +2 -2
  16. package/src/components/fields-previewer.tsx +1 -0
  17. package/src/components/legacy-slot.tsx +11 -16
  18. package/src/components/output-config-editor.tsx +32 -19
  19. package/src/components/shadow-content.tsx +19 -0
  20. package/src/components/shadow-portal.tsx +23 -0
  21. package/src/components/ui/dialog.tsx +4 -1
  22. package/src/components/ui/drawer.tsx +3 -1
  23. package/src/components/ui/popover.tsx +3 -2
  24. package/src/components/ui/select.tsx +3 -2
  25. package/src/components/ui/tooltip.tsx +3 -2
  26. package/src/const/shadow.ts +12 -0
  27. package/src/hooks/use-portal-container.ts +6 -3
  28. package/src/hooks/use-shadow-boundary.ts +64 -0
  29. package/src/hooks/use-shadow.ts +24 -7
  30. package/src/index.ts +2 -0
  31. package/src/stories/AppSelect.stories.tsx +44 -0
  32. package/src/stories/FieldValueInput.stories.tsx +6 -3
  33. package/src/stories/FormSelect.stories.tsx +40 -0
  34. package/src/stories/RelationalOperatorSelect.stories.tsx +27 -0
  35. package/src/style.css +28 -1
  36. package/style.css +153 -126
@@ -13,6 +13,8 @@ import { transCls } from '../../../const/ui';
13
13
  import { SolidArrowDownIcon, SearchIcon } from '../../bs-icons';
14
14
  import { useSelectPopoverPosition } from '../../../hooks/use-select-popover-position';
15
15
 
16
+ const POPOVER_MAX_HEIGHT = 300;
17
+
16
18
  const FolderIcon = () => (
17
19
  <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
18
20
  <path d="M13.9004 15H2.65039C2.25257 15 1.87104 14.842 1.58973 14.5607C1.30843 14.2794 1.15039 13.8978 1.15039 13.5V4.5C1.15039 4.10218 1.30843 3.72064 1.58973 3.43934C1.87104 3.15804 2.25257 3 2.65039 3H7.15039L8.65039 4.5H13.9004C14.0974 4.5 14.2924 4.5388 14.4744 4.61418C14.6564 4.68956 14.8218 4.80005 14.9611 4.93934C15.1003 5.07863 15.2108 5.24399 15.2862 5.42597C15.3616 5.60796 15.4004 5.80302 15.4004 6H2.65039V13.5L4.25539 7.5H16.0754C16.1927 7.49595 16.3093 7.51947 16.4158 7.56867C16.5223 7.61788 16.6158 7.69139 16.6888 7.78331C16.7617 7.87523 16.8121 7.98298 16.8358 8.0979C16.8595 8.21282 16.856 8.33171 16.8254 8.445L15.3704 13.875C15.2865 14.1999 15.0961 14.4873 14.8296 14.6913C14.5631 14.8952 14.2359 15.0039 13.9004 15Z" fill="#6F81BD"/>
@@ -307,11 +309,12 @@ function FormPopover({
307
309
  <div
308
310
  className={cn(
309
311
  'bg-white rounded-[10px] shadow-[0px_0px_5px_rgba(0,0,0,0.04)] p-[4px]',
310
- 'flex flex-col gap-[4px]',
312
+ 'flex min-h-0 flex-col gap-[4px] overflow-hidden',
311
313
  )}
314
+ style={{ maxHeight: POPOVER_MAX_HEIGHT }}
312
315
  >
313
316
  {/* Search input */}
314
- <div className="flex items-center gap-[6px] border border-[#E0E0E0] rounded-[4px] h-[30px] px-[10px] py-[5px]">
317
+ <div className="flex shrink-0 items-center gap-[6px] border border-[#E0E0E0] rounded-[4px] h-[30px] px-[10px] py-[5px]">
315
318
  <SearchIcon />
316
319
  <input
317
320
  type="text"
@@ -337,7 +340,7 @@ function FormPopover({
337
340
  )}
338
341
 
339
342
  {/* Form list */}
340
- <div className="flex flex-col gap-0 max-h-[400px] overflow-y-auto">
343
+ <div className="flex min-h-0 flex-col gap-0 overflow-y-auto">
341
344
  {tree.length > 0 ? (
342
345
  tree.map((node) => (
343
346
  <FormTreeItemNode
@@ -391,7 +394,7 @@ export const FormSelect = forwardRef<HTMLDivElement, FormSelectProps>(
391
394
  const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(null);
392
395
 
393
396
  const { popoverStyle, updatePopoverPosition } =
394
- useSelectPopoverPosition(triggerRef);
397
+ useSelectPopoverPosition(triggerRef, { estimatedHeight: POPOVER_MAX_HEIGHT });
395
398
 
396
399
  const resolvedValue = controlledValue ?? internalValue;
397
400
 
@@ -709,7 +712,7 @@ export const FormSelect = forwardRef<HTMLDivElement, FormSelectProps>(
709
712
  {/* Popover */}
710
713
  {isOpen &&
711
714
  createPortal(
712
- <div ref={popoverRef} style={popoverStyle}>
715
+ <div ref={popoverRef} className="flex flex-col" style={popoverStyle}>
713
716
  <FormPopover
714
717
  searchQuery={searchQuery}
715
718
  onSearchChange={setSearchQuery}
@@ -1,27 +1,27 @@
1
1
  import { useState, useCallback, useMemo } from 'react';
2
- import type { relationalOperator } from '@baishuyun/types';
3
- import { OPERATOR_LABEL_MAP } from '@baishuyun/ai-feat-utils';
2
+ import type { FieldType, relationalOperator } from '@baishuyun/types';
3
+ import { getOperatorLabel } from '@baishuyun/ai-feat-utils';
4
4
  import { Popover, PopoverTrigger, PopoverContent } from '../../ui/popover';
5
5
  import { cn } from '../../../lib/utils';
6
6
  import { transCls } from '../../../const/ui';
7
7
 
8
- const ALL_OPTIONS: Array<{ value: relationalOperator; label: string }> = [
9
- { value: 'eq', label: OPERATOR_LABEL_MAP.eq },
10
- { value: 'neq', label: OPERATOR_LABEL_MAP.neq },
11
- { value: 'gt', label: OPERATOR_LABEL_MAP.gt },
12
- { value: 'lt', label: OPERATOR_LABEL_MAP.lt },
13
- { value: 'gte', label: OPERATOR_LABEL_MAP.gte },
14
- { value: 'lte', label: OPERATOR_LABEL_MAP.lte },
15
- { value: 'contains', label: OPERATOR_LABEL_MAP.contains },
16
- { value: 'not_contains', label: OPERATOR_LABEL_MAP.not_contains },
17
- { value: 'in', label: OPERATOR_LABEL_MAP.in },
18
- { value: 'nin', label: OPERATOR_LABEL_MAP.nin },
19
- { value: 'all', label: OPERATOR_LABEL_MAP.all },
20
- { value: 'empty', label: OPERATOR_LABEL_MAP.empty },
21
- { value: 'not_empty', label: OPERATOR_LABEL_MAP.not_empty },
22
- { value: 'range', label: OPERATOR_LABEL_MAP.range },
23
- { value: 'like', label: OPERATOR_LABEL_MAP.like },
24
- { value: 'unlike', label: OPERATOR_LABEL_MAP.unlike },
8
+ const ALL_OPERATORS: relationalOperator[] = [
9
+ 'eq',
10
+ 'neq',
11
+ 'gt',
12
+ 'lt',
13
+ 'gte',
14
+ 'lte',
15
+ 'contains',
16
+ 'not_contains',
17
+ 'in',
18
+ 'nin',
19
+ 'all',
20
+ 'empty',
21
+ 'not_empty',
22
+ 'range',
23
+ 'like',
24
+ 'unlike',
25
25
  ];
26
26
 
27
27
  export interface IRelationalOperatorSelectProps {
@@ -29,6 +29,13 @@ export interface IRelationalOperatorSelectProps {
29
29
  defaultValue?: relationalOperator;
30
30
  onChange?: (value: relationalOperator) => void;
31
31
  operators?: relationalOperator[];
32
+ /**
33
+ * 字段类型:同一操作符在不同字段类型下描述不同
34
+ * (如部门多选的 in 为「包含任意一个」),未提供时使用默认文案
35
+ */
36
+ fieldType?: FieldType;
37
+ /** 逐操作符的文案覆盖,优先级高于 fieldType 推导 */
38
+ operatorLabels?: Partial<Record<relationalOperator, string>>;
32
39
  borderless?: boolean;
33
40
  disabled?: boolean;
34
41
  className?: string;
@@ -39,6 +46,8 @@ export const RelationalOperatorSelect = ({
39
46
  defaultValue = 'eq',
40
47
  onChange,
41
48
  operators,
49
+ fieldType,
50
+ operatorLabels,
42
51
  borderless = false,
43
52
  disabled = false,
44
53
  className,
@@ -48,10 +57,15 @@ export const RelationalOperatorSelect = ({
48
57
  const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(null);
49
58
 
50
59
  const options = useMemo(() => {
51
- if (!operators || operators.length === 0) return ALL_OPTIONS;
52
- const set = new Set(operators);
53
- return ALL_OPTIONS.filter((o) => set.has(o.value));
54
- }, [operators]);
60
+ const list =
61
+ !operators || operators.length === 0
62
+ ? ALL_OPERATORS
63
+ : ALL_OPERATORS.filter((o) => new Set(operators).has(o));
64
+ return list.map((value) => ({
65
+ value,
66
+ label: operatorLabels?.[value] ?? getOperatorLabel(value, fieldType),
67
+ }));
68
+ }, [operators, fieldType, operatorLabels]);
55
69
 
56
70
  const resolvedValue = (() => {
57
71
  const v = controlledValue !== undefined ? controlledValue : internalValue;
@@ -47,9 +47,13 @@ export function DataTable<TData>({
47
47
  return (
48
48
  <div
49
49
  className={cn(
50
- 'rounded-[10px] border border-[#e0e0e0] overflow-hidden bg-white',
50
+ 'rounded-[10px] border border-[#e0e0e0] overflow-auto bg-white',
51
51
  className,
52
52
  )}
53
+ // 抽屉(vaul/Radix Dialog)经 react-remove-scroll 在 document 级 preventDefault
54
+ // wheel 事件;shadow DOM 下其 contains 判定失效,内部滚动会被误杀。
55
+ // 阻断冒泡使其不可见,恢复触摸板滚动(同 form-select 的处理先例)。
56
+ onWheel={(e) => e.stopPropagation()}
53
57
  >
54
58
  <table className="w-full h-full border-collapse">
55
59
  <thead>
@@ -59,7 +63,7 @@ export function DataTable<TData>({
59
63
  <th
60
64
  key={header.id}
61
65
  className={cn(
62
- 'bg-[#f5f7fa] h-[34px] px-[10px] text-left text-[14px] font-normal text-[#030303] border-b border-[#e0e0e0] align-middle whitespace-nowrap',
66
+ 'sticky top-0 z-[1] bg-[#f5f7fa] h-[34px] px-[10px] text-left text-[14px] font-normal text-[#030303] border-b border-[#e0e0e0] align-middle whitespace-nowrap',
63
67
  index !== 0 && 'border-l border-[#e0e0e0]',
64
68
  )}
65
69
  style={{
@@ -13,6 +13,11 @@ export interface AgentSelectProps {
13
13
  className?: string;
14
14
  activeAgent?: { id: string } | null;
15
15
  onSelect: (agent: AgentSelectAgent) => void | Promise<void>;
16
+ /**
17
+ * 大规格行(移动端设计稿 40434:32068):行高 40、图标 20、名称 16px、
18
+ * 分组标题 14px。默认 false,不影响现有 PC 样式
19
+ */
20
+ large?: boolean;
16
21
  }
17
22
 
18
23
  interface AgentGroup {
@@ -27,6 +32,7 @@ export const AgentSelect = ({
27
32
  onSelect,
28
33
  showAllItems,
29
34
  className,
35
+ large = false,
30
36
  }: AgentSelectProps) => {
31
37
  const [searchQuery, setSearchQuery] = useState('');
32
38
 
@@ -60,37 +66,67 @@ export const AgentSelect = ({
60
66
  return (
61
67
  <div className={classNames('flex flex-col', 'rounded-[4px]', className)}>
62
68
  {showSearch && (
63
- <div className="px-1.5 pt-1 pb-0.5">
64
- <div className="flex h-7.5 items-center gap-1.5 rounded-[4px] bg-white px-2.5 py-1.25 border border-[#E0E0E0]">
65
- <svg
66
- width="14"
67
- height="14"
68
- viewBox="0 0 14 14"
69
- fill="none"
70
- xmlns="http://www.w3.org/2000/svg"
71
- className="shrink-0 text-[#999999]"
72
- >
73
- <path
74
- d="M6.41667 11.0833C8.994 11.0833 11.0833 8.994 11.0833 6.41667C11.0833 3.83934 8.994 1.75 6.41667 1.75C3.83934 1.75 1.75 3.83934 1.75 6.41667C1.75 8.994 3.83934 11.0833 6.41667 11.0833Z"
75
- stroke="currentColor"
76
- strokeWidth="1.2"
77
- strokeLinecap="round"
78
- strokeLinejoin="round"
79
- />
80
- <path
81
- d="M12.25 12.25L9.7125 9.7125"
82
- stroke="currentColor"
83
- strokeWidth="1.2"
84
- strokeLinecap="round"
85
- strokeLinejoin="round"
86
- />
87
- </svg>
69
+ // 大规格(移动端抽屉设计稿 40654:39905):搜索框高 46、圆角 100、左右 12 边距、
70
+ // 图标 18、文字 16px、占位「请输入智能体名称」
71
+ <div className={large ? 'px-[12px] pb-[10px]' : 'px-1.5 pt-1 pb-0.5'}>
72
+ <div
73
+ className={cn(
74
+ 'flex items-center bg-white border border-[#E0E0E0]',
75
+ // shadow 内 --spacing=3.5px,数值类会缩小,设计稿像素一律用任意值
76
+ large
77
+ ? 'h-[46px] gap-[6px] px-[10px] py-[5px] rounded-[100px]'
78
+ : 'h-7.5 gap-1.5 px-2.5 py-1.25 rounded-[4px]'
79
+ )}
80
+ >
81
+ {large ? (
82
+ <svg
83
+ width="18"
84
+ height="18"
85
+ viewBox="0 0 18 18"
86
+ fill="none"
87
+ xmlns="http://www.w3.org/2000/svg"
88
+ className="shrink-0"
89
+ aria-hidden="true"
90
+ >
91
+ <path
92
+ d="M16.7608 15.6971L13.8144 12.6563C14.7965 11.3794 15.3849 9.90789 15.3849 8.14051C15.3849 4.21426 12.146 0.976513 8.21976 0.976513C4.29463 0.975388 1.05688 4.21426 1.05688 8.13939C1.05688 12.0656 4.29576 15.3056 8.22088 15.3056C9.88926 15.3056 11.4609 14.7161 12.7366 13.734L15.6808 16.6781C15.7786 16.875 15.9744 16.875 16.1713 16.875C16.3532 16.8711 16.5276 16.8012 16.6618 16.6781C16.9565 16.4824 16.9565 15.9919 16.7608 15.6971ZM2.52951 8.23839C2.5307 6.72901 3.13083 5.28179 4.19812 4.2145C5.26541 3.14721 6.71263 2.54708 8.22201 2.54589C9.73139 2.54708 11.1786 3.14721 12.2459 4.2145C13.3132 5.28179 13.9133 6.72901 13.9145 8.23839C13.9133 9.74777 13.3132 11.195 12.2459 12.2623C11.1786 13.3296 9.73139 13.9297 8.22201 13.9309C6.71263 13.9297 5.26541 13.3296 4.19812 12.2623C3.13083 11.195 2.5307 9.74777 2.52951 8.23839Z"
93
+ fill="#666666"
94
+ />
95
+ </svg>
96
+ ) : (
97
+ <svg
98
+ width="14"
99
+ height="14"
100
+ viewBox="0 0 14 14"
101
+ fill="none"
102
+ xmlns="http://www.w3.org/2000/svg"
103
+ className="shrink-0 text-[#999999]"
104
+ >
105
+ <path
106
+ d="M6.41667 11.0833C8.994 11.0833 11.0833 8.994 11.0833 6.41667C11.0833 3.83934 8.994 1.75 6.41667 1.75C3.83934 1.75 1.75 3.83934 1.75 6.41667C1.75 8.994 3.83934 11.0833 6.41667 11.0833Z"
107
+ stroke="currentColor"
108
+ strokeWidth="1.2"
109
+ strokeLinecap="round"
110
+ strokeLinejoin="round"
111
+ />
112
+ <path
113
+ d="M12.25 12.25L9.7125 9.7125"
114
+ stroke="currentColor"
115
+ strokeWidth="1.2"
116
+ strokeLinecap="round"
117
+ strokeLinejoin="round"
118
+ />
119
+ </svg>
120
+ )}
88
121
  <input
89
122
  type="text"
90
- placeholder="搜索智能体助手"
123
+ placeholder={large ? '请输入智能体名称' : '搜索智能体助手'}
91
124
  value={searchQuery}
92
125
  onChange={(e) => setSearchQuery(e.target.value)}
93
- className="h-full flex-1 bg-transparent text-[12px] text-[#121111] placeholder:text-[#999999] outline-none"
126
+ className={cn(
127
+ 'h-full flex-1 bg-transparent text-[#121111] placeholder:text-[#999999] outline-none',
128
+ large ? 'text-[16px]' : 'text-[12px]'
129
+ )}
94
130
  />
95
131
  </div>
96
132
  </div>
@@ -101,29 +137,48 @@ export const AgentSelect = ({
101
137
  <div key={group.label}>
102
138
  <div
103
139
  className={cn(
104
- 'flex h-8 items-start px-2.5 pt-2.25',
140
+ 'flex items-start',
141
+ large ? 'h-[40px] px-[12px] pt-[9px]' : 'h-8 px-2.5 pt-2.25',
105
142
  groupIndex > 0 && 'mt-0.5'
106
143
  )}
107
144
  >
108
- <div className="flex w-full flex-col gap-1.5">
109
- <span className="text-[12px] font-medium text-[#666666] leading-4.25">
145
+ <div
146
+ className={cn(
147
+ 'flex w-full flex-col gap-1.5',
148
+ // 设计稿分割线钉在分组头底部(justify-between)
149
+ large && 'h-full justify-between'
150
+ )}
151
+ >
152
+ <span
153
+ className={cn(
154
+ 'font-medium text-[#666666]',
155
+ large ? 'text-[14px] leading-[20px]' : 'text-[12px] leading-4.25'
156
+ )}
157
+ >
110
158
  {group.label}
111
159
  </span>
112
160
  <div className="h-0 w-full border-b border-[#E0E0E0]" />
113
161
  </div>
114
162
  </div>
115
163
 
116
- <div className="px-1">
164
+ {/* 大规格:全宽行(设计稿行高 46、左右 10) */}
165
+ <div className={large ? 'px-0' : 'px-1'}>
117
166
  {group.agents.map((agent) => {
118
167
  const isActive = activeAgent?.id === agent.id;
119
168
  return (
120
169
  <button
121
170
  key={agent.id}
122
171
  className={cn(
123
- 'flex h-8 w-full cursor-pointer items-center gap-1.5 rounded-[4px] px-2.5 py-1.5 text-left',
172
+ 'flex w-full cursor-pointer items-center rounded-[4px] text-left',
173
+ large
174
+ ? 'h-[46px] px-[12px] py-[10px] gap-[6px]'
175
+ : 'h-8 px-2.5 py-1.5 gap-1.5',
124
176
  transCls,
177
+ // 选中态:移动端蓝字 + 勾选(无底色高亮,设计稿 40654:39918);PC 浅蓝底高亮
125
178
  isActive
126
- ? 'bg-[#F0F6FF] text-[#0265FF]'
179
+ ? large
180
+ ? 'text-[#0265FF]'
181
+ : 'bg-[#F0F6FF] text-[#0265FF]'
127
182
  : 'text-[#121111] hover:bg-[#EFF0F6]'
128
183
  )}
129
184
  onClick={() => handleSelect(agent)}
@@ -131,11 +186,36 @@ export const AgentSelect = ({
131
186
  <img
132
187
  src={agent.icon}
133
188
  alt={agent.name}
134
- className="h-4 w-4 rounded-[4px] object-cover shrink-0"
189
+ className={cn(
190
+ 'rounded-[4px] object-cover shrink-0',
191
+ large ? 'size-[20px]' : 'h-4 w-4'
192
+ )}
135
193
  />
136
- <span className="truncate text-[14px] leading-5">
194
+ <span
195
+ className={cn(
196
+ 'truncate',
197
+ large && 'flex-1',
198
+ large ? 'text-[16px] leading-[22px]' : 'text-[14px] leading-5'
199
+ )}
200
+ >
137
201
  {agent.name}
138
202
  </span>
203
+ {/* 选中勾选(仅移动端 large) */}
204
+ {large && isActive && (
205
+ <svg
206
+ width="20"
207
+ height="20"
208
+ viewBox="0 0 20 20"
209
+ fill="none"
210
+ className="shrink-0"
211
+ aria-hidden="true"
212
+ >
213
+ <path
214
+ d="M15.7613 5.42387C15.5414 5.22244 15.2506 5.11654 14.9527 5.12943C14.6548 5.14232 14.3742 5.27295 14.1725 5.49262L8.23378 11.9676L5.91628 8.72387L5.82753 8.61387C5.63315 8.40442 5.36635 8.27678 5.08129 8.25687C4.79623 8.23696 4.51428 8.32628 4.29267 8.50668C4.07106 8.68708 3.92641 8.94506 3.88808 9.22823C3.84975 9.5114 3.92061 9.79855 4.08628 10.0314L7.21128 14.4064L7.30628 14.5226C7.41228 14.6356 7.54046 14.7254 7.6828 14.7865C7.82514 14.8477 7.97856 14.8787 8.13346 14.8778C8.28836 14.8768 8.4414 14.8439 8.58299 14.7811C8.72457 14.7183 8.85166 14.6269 8.95628 14.5126L15.8313 7.01262L15.9263 6.89387C16.0811 6.67029 16.1497 6.3982 16.1193 6.12794C16.089 5.85769 15.9618 5.60757 15.7613 5.42387Z"
215
+ fill="#0265FF"
216
+ />
217
+ </svg>
218
+ )}
139
219
  </button>
140
220
  );
141
221
  })}
@@ -9,22 +9,35 @@ export interface CardProps {
9
9
  icon?: ReactNode;
10
10
  iconCode?: string;
11
11
  onClick?: (title: string, iconCode?: string) => void;
12
+ /** 追加到根节点的类名(如移动端满宽 max-w-none) */
13
+ className?: string;
14
+ /**
15
+ * 紧凑模式(移动端推荐卡片,对齐 Figma 40040:30035):
16
+ * 标题行去内边距、高 26,标题与描述间距 6,卡片总高 92。
17
+ * 默认 false,不影响现有 PC 样式
18
+ */
19
+ compact?: boolean;
12
20
  }
13
21
 
14
- export const Card = ({ title, desc, index, icon, onClick, iconCode }: CardProps) => {
22
+ export const Card = ({ title, desc, index, icon, onClick, iconCode, className, compact }: CardProps) => {
15
23
  return (
16
24
  <div
17
25
  className={cn(
18
- 'bs-card group flex flex-col cursor-pointer p-2.5 gap-2.5 rounded-lg max-w-[350px] border border-[#e0e0e0] bg-white',
19
- {
20
- transCls,
21
- }
26
+ 'bs-card group flex flex-col cursor-pointer p-2.5 rounded-lg max-w-[350px] border border-[#e0e0e0] bg-white',
27
+ compact ? 'gap-1.5' : 'gap-2.5',
28
+ transCls,
29
+ className
22
30
  )}
23
31
  onClick={() => {
24
32
  onClick?.(title, iconCode);
25
33
  }}
26
34
  >
27
- <div className="px-2.5 flex items-center gap-1.5 rounded-[4px] h-[34px] bg-white">
35
+ <div
36
+ className={cn('flex items-center gap-1.5 rounded-[4px] bg-white', {
37
+ 'h-[26px]': compact,
38
+ 'px-2.5 h-[34px]': !compact,
39
+ })}
40
+ >
28
41
  {index === undefined ? null : <div>{index}</div>}
29
42
  {icon === undefined ? null : <div className="text-[#0265FF]">{icon}</div>}
30
43
  {/* make title text ellipsis */}
@@ -8,11 +8,11 @@ export interface FieldsPortalProps extends FieldsPreviewerProps {
8
8
  children?: React.ReactNode;
9
9
  }
10
10
 
11
- export const FieldsPortal = ({ portalEl, children, ...rest }: FieldsPortalProps): ReactPortal => {
11
+ export const FieldsPortal = ({ portalEl, children, ...rest }: FieldsPortalProps): ReactPortal | null => {
12
12
  const { shadowContainer: shadowEntry } = useShadow({
13
13
  rootEl: portalEl,
14
14
  id: 'chat-sdk-fields-portal',
15
15
  });
16
16
 
17
- return createPortal(<FieldsPreviewer {...rest}>{children}</FieldsPreviewer>, shadowEntry!);
17
+ return shadowEntry ? createPortal(<FieldsPreviewer {...rest}>{children}</FieldsPreviewer>, shadowEntry) : null;
18
18
  };
@@ -117,6 +117,7 @@ export const FieldsPreviewer = forwardRef<HTMLDivElement, FieldsPreviewerProps>(
117
117
 
118
118
  const { shadowContainer } = useShadow({
119
119
  id: 'dialog-container',
120
+ enabled: !disableShadowContainer,
120
121
  rootEl: dialogContainer,
121
122
  style: containerStyle,
122
123
  wrapperStyle: {
@@ -1,5 +1,7 @@
1
1
  import { useEffect, useRef } from 'react';
2
2
  import { LegacySlotRenderer } from '../types';
3
+ import { syncHostStylesIntoShadow } from '@baishuyun/ai-feat-utils';
4
+ import { useInjectedShadowStyle } from '../hooks/use-shadow-boundary';
3
5
 
4
6
  export interface LegacySlotProps {
5
7
  renderLegacy: LegacySlotRenderer;
@@ -10,8 +12,6 @@ export interface LegacySlotProps {
10
12
  onSetRef?: (ref: HTMLDivElement) => void;
11
13
  }
12
14
 
13
- const STYLE_INJECTED_ATTR = 'data-style-injected';
14
-
15
15
  const injectStylesIntoShadow = (currentElement: Node) => {
16
16
  if (!currentElement) return;
17
17
 
@@ -31,20 +31,8 @@ const injectStylesIntoShadow = (currentElement: Node) => {
31
31
  }
32
32
 
33
33
  if (!shadowRoot) return;
34
- if (shadowRoot.querySelector(`style[${STYLE_INJECTED_ATTR}]`)) return;
35
-
36
- document.head.querySelectorAll('style').forEach((styleEl) => {
37
- const cloned = styleEl.cloneNode(true) as HTMLElement;
38
- cloned.setAttribute(STYLE_INJECTED_ATTR, '');
39
- shadowRoot!.appendChild(cloned);
40
- });
41
-
42
34
  // if style is loaded by link tag, we also need to copy it into shadow DOM
43
- document.head.querySelectorAll('link[rel="stylesheet"]').forEach((linkEl) => {
44
- const cloned = linkEl.cloneNode(true) as HTMLElement;
45
- cloned.setAttribute(STYLE_INJECTED_ATTR, '');
46
- shadowRoot!.appendChild(cloned);
47
- });
35
+ return syncHostStylesIntoShadow(shadowRoot, shadowRoot.host as HTMLElement);
48
36
  };
49
37
 
50
38
  export function LegacySlot({
@@ -66,13 +54,14 @@ export function LegacySlot({
66
54
  const host = hostRef.current;
67
55
  onSetRef?.(host);
68
56
  let target: HTMLElement | ShadowRoot = host;
57
+ let disposeStyles: (() => void) | undefined;
69
58
 
70
59
  if (shadow) {
71
60
  const shadowRoot = host.shadowRoot || host.attachShadow({ mode: 'open' });
72
61
  target = shadowRoot;
73
62
 
74
63
  if (withHostStyles) {
75
- injectStylesIntoShadow(host);
64
+ host.setAttribute('data-bs-style-mode', 'host');
76
65
  }
77
66
  if (typeof injectedStyles === 'string') {
78
67
  const styleEl = document.createElement('style');
@@ -86,12 +75,16 @@ export function LegacySlot({
86
75
 
87
76
  const mountPoint = document.createElement('div');
88
77
  mountPoint.style.display = 'contents';
78
+ mountPoint.setAttribute('data-bs-shadow-content', '');
89
79
  target.appendChild(mountPoint);
90
80
  mountPointRef.current = mountPoint;
81
+ if (shadow && withHostStyles) disposeStyles = injectStylesIntoShadow(host);
91
82
 
92
83
  cleanupRef.current = renderLegacy(mountPoint, props as Record<string, any>);
93
84
 
94
85
  return () => {
86
+ disposeStyles?.();
87
+ host.shadowRoot?.querySelector('style[data-legacy-injected-raw]')?.remove();
95
88
  cleanupRef.current?.();
96
89
  if (mountPointRef.current) {
97
90
  mountPointRef.current.remove();
@@ -101,6 +94,8 @@ export function LegacySlot({
101
94
  };
102
95
  }, []);
103
96
 
97
+ useInjectedShadowStyle(hostRef, injectedStyles, 'data-legacy-injected-raw');
98
+
104
99
  useEffect(() => {
105
100
  if (!mountPointRef.current) return;
106
101
  cleanupRef.current = renderLegacy(mountPointRef.current, props as Record<string, any>);
@@ -1,4 +1,4 @@
1
- import { useState, useCallback } from 'react';
1
+ import { useState, useCallback, useMemo } from 'react';
2
2
  import type { FieldType, IAgentHelperOutputConfig, OutputParam, IField, ISubformField } from '@baishuyun/types';
3
3
  import { cn } from '../lib/utils';
4
4
  import { transCls } from '../const/ui';
@@ -111,17 +111,16 @@ const VALID_TYPE_MAP: Partial<Record<FieldType, Set<FieldType>>> = {
111
111
  'image': new Set(['signature', 'image', 'upload']),
112
112
  }
113
113
 
114
- const EMPTY_SET = new Set();
114
+ function isCompatibleType(sourceType?: FieldType | '', targetType?: FieldType | ''): boolean {
115
+ return !!sourceType && !!targetType && sourceType !== 'subform' && targetType !== 'subform' &&
116
+ (sourceType === targetType || !!VALID_TYPE_MAP[sourceType]?.has(targetType));
117
+ }
115
118
 
116
- function filterFieldsByType(fields: OutputFieldOption[], targetType: FieldType | ''): OutputFieldOption[] {
119
+ function filterFieldsByType(fields: OutputFieldOption[], sourceType?: FieldType | ''): OutputFieldOption[] {
117
120
  return fields.reduce<OutputFieldOption[]>((acc, f) => {
118
- const selfMatches = f.type === targetType;
119
- const validTypeSet = VALID_TYPE_MAP[targetType as FieldType] ?? EMPTY_SET;
120
- const generalMatches = validTypeSet.has(f.type);
121
-
122
- const filteredChildren = f.children ? filterFieldsByType(f.children, targetType) : [];
123
- if (selfMatches || generalMatches || filteredChildren.length > 0) {
124
- acc.push({ ...f, children: filteredChildren.length > 0 ? filteredChildren : f.children });
121
+ const filteredChildren = f.children ? filterFieldsByType(f.children, sourceType) : [];
122
+ if (filteredChildren.length > 0 || (!f.children?.length && isCompatibleType(sourceType, f.type))) {
123
+ acc.push({ ...f, children: filteredChildren.length > 0 ? filteredChildren : undefined });
125
124
  }
126
125
  return acc;
127
126
  }, []);
@@ -134,8 +133,8 @@ export function OutputConfigEditor({ value, onChange, sourceFields, targetFields
134
133
 
135
134
  const { portalContainer, setTriggerRef } = usePortalContainer('data-bs-output-config-portal');
136
135
 
137
- const sourceOptions = sourceFields ? outputParamsToOptions(sourceFields) : [];
138
- const targetOptions = targetFields ? fieldsToOptions(targetFields) : [];
136
+ const sourceOptions = useMemo(() => outputParamsToOptions(sourceFields ?? []), [sourceFields]);
137
+ const targetOptions = useMemo(() => fieldsToOptions(targetFields ?? []), [targetFields]);
139
138
 
140
139
  const handleDropdownOpenChange = useCallback((index: number, field: 'source' | 'target', open: boolean) => {
141
140
  if (open) {
@@ -162,7 +161,7 @@ export function OutputConfigEditor({ value, onChange, sourceFields, targetFields
162
161
  const newItem = { ...newValue[index], sourceField: { fieldName: field.fieldName, fieldDesc: field.fieldDesc } };
163
162
  if (newItem.targetField.fieldName) {
164
163
  const targetOption = findFieldByName(targetOptions, newItem.targetField.fieldName);
165
- if (targetOption && targetOption.type !== field.type) {
164
+ if (!isCompatibleType(field.type, targetOption?.type)) {
166
165
  newItem.targetField = { fieldName: '', fieldDesc: '' };
167
166
  }
168
167
  }
@@ -173,12 +172,21 @@ export function OutputConfigEditor({ value, onChange, sourceFields, targetFields
173
172
  }, [value, onChange, targetOptions]);
174
173
 
175
174
  const handleSelectTargetField = useCallback((index: number, field: OutputFieldOption) => {
175
+ const sourceOption = flattenSourceFields(sourceOptions).find(
176
+ option => option.fieldName === value[index].sourceField.fieldName
177
+ );
178
+ if (!isCompatibleType(sourceOption?.type, field.type)) {
179
+ if (import.meta.env.DEV) {
180
+ console.warn('[OutputConfigEditor] 不兼容的输出映射', { sourceType: sourceOption?.type, targetType: field.type });
181
+ }
182
+ return;
183
+ }
176
184
  const newValue = [...value];
177
185
  newValue[index] = { ...newValue[index], targetField: { fieldName: field.fieldName, fieldDesc: field.fieldDesc } };
178
186
  onChange?.(newValue);
179
187
  setDropdownTarget(null);
180
188
  setSearchText('');
181
- }, [value, onChange]);
189
+ }, [value, onChange, sourceOptions]);
182
190
 
183
191
  const toggleParent = useCallback((fieldName: string) => {
184
192
  setCollapsedParents(prev => {
@@ -210,6 +218,10 @@ export function OutputConfigEditor({ value, onChange, sourceFields, targetFields
210
218
  (f) => (matchSearch(f.fieldDesc) || matchSearch(f.fieldName)) && f.type !== 'subform'
211
219
  );
212
220
 
221
+ const usedSourceFieldNames = new Set(
222
+ value.map(item => item.sourceField.fieldName).filter(Boolean)
223
+ );
224
+
213
225
  const searchedTargetFields = filterFields(targetOptions);
214
226
 
215
227
  const fieldRowCls = cn(
@@ -254,9 +266,10 @@ export function OutputConfigEditor({ value, onChange, sourceFields, targetFields
254
266
  const selectedSourceType = item.sourceField.fieldName
255
267
  ? flatSourceFields.find(f => f.fieldName === item.sourceField.fieldName)?.type
256
268
  : undefined;
257
- const rowTargetFields = selectedSourceType
258
- ? filterFieldsByType(searchedTargetFields, selectedSourceType)
259
- : searchedTargetFields;
269
+ const rowSourceFields = filteredSourceFields.filter(
270
+ f => !usedSourceFieldNames.has(f.fieldName) || f.fieldName === item.sourceField.fieldName
271
+ );
272
+ const rowTargetFields = filterFieldsByType(searchedTargetFields, selectedSourceType);
260
273
  return (
261
274
  <div key={index} className="flex gap-[10px] items-center">
262
275
  {/* Source field selector (JSON output field) */}
@@ -304,7 +317,7 @@ export function OutputConfigEditor({ value, onChange, sourceFields, targetFields
304
317
  />
305
318
  </div>
306
319
  <div className="max-h-[210px] overflow-auto">
307
- {filteredSourceFields.map((field) => (
320
+ {rowSourceFields.map((field) => (
308
321
  <button
309
322
  key={field.fieldName}
310
323
  type="button"
@@ -320,7 +333,7 @@ export function OutputConfigEditor({ value, onChange, sourceFields, targetFields
320
333
  <span className="truncate">{field.fieldDesc}</span>
321
334
  </button>
322
335
  ))}
323
- {filteredSourceFields.length === 0 && (
336
+ {rowSourceFields.length === 0 && (
324
337
  <div className="px-[10px] py-[7px] text-[14px] text-[#999] leading-normal">
325
338
  无匹配结果
326
339
  </div>
@@ -0,0 +1,19 @@
1
+ import type { ReactNode } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import { useShadowBoundary } from '../hooks/use-shadow-boundary';
4
+ import type { ShadowStyle, ShadowStyleMode } from '../hooks/use-shadow-boundary';
5
+
6
+ export interface ShadowContentProps {
7
+ children: ReactNode;
8
+ mode?: ShadowStyleMode;
9
+ styles?: ShadowStyle;
10
+ }
11
+
12
+ export const ShadowContent = ({ children, mode = 'sdk', styles }: ShadowContentProps) => {
13
+ const { setHostRef, container } = useShadowBoundary(mode, styles);
14
+ return (
15
+ <div ref={setHostRef} data-bs-style-mode={mode} style={{ display: 'contents' }}>
16
+ {container && createPortal(children, container)}
17
+ </div>
18
+ );
19
+ };