@baishuyun/ai-design 2.0.26 → 2.0.28

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
  # @baishuyun/ai-design
2
2
 
3
+ ## 2.0.28
4
+
5
+ ### Patch Changes
6
+
7
+ - fix tree entry
8
+ - Updated dependencies
9
+ - @baishuyun/types@3.0.24
10
+ - @baishuyun/ai-feat-utils@2.0.26
11
+
12
+ ## 2.0.27
13
+
14
+ ### Patch Changes
15
+
16
+ - update agent selector
17
+ - Updated dependencies
18
+ - @baishuyun/ai-feat-utils@2.0.25
19
+ - @baishuyun/types@3.0.23
20
+
3
21
  ## 2.0.26
4
22
 
5
23
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@baishuyun/ai-design",
3
- "version": "2.0.26",
3
+ "version": "2.0.28",
4
4
  "description": "AI 功能 UI 组件库 - 所有 AI 功能的 UI 表现唯一真相来源",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -60,8 +60,8 @@
60
60
  "tw-animate-css": "^1.4.0",
61
61
  "vaul": "^1.1.2",
62
62
  "zod": "^4.1.13",
63
- "@baishuyun/types": "3.0.22",
64
- "@baishuyun/ai-feat-utils": "2.0.24"
63
+ "@baishuyun/ai-feat-utils": "2.0.26",
64
+ "@baishuyun/types": "3.0.24"
65
65
  },
66
66
  "peerDependencies": {
67
67
  "react": "^19.0.0",
@@ -83,7 +83,7 @@
83
83
  "typescript": "^5.5.0",
84
84
  "vite": "^5.1.4",
85
85
  "vite-plugin-dts": "^4.5.4",
86
- "@baishuyun/types": "3.0.22",
86
+ "@baishuyun/types": "3.0.24",
87
87
  "@baishuyun/tailwind-config": "3.0.9",
88
88
  "@baishuyun/typescript-config": "2.0.9"
89
89
  },
@@ -81,21 +81,11 @@ function useTriggerDescription(helper: IAgentHelper, formService?: IBsFormServic
81
81
  }
82
82
  }
83
83
 
84
- function buildAgentInfo(agentName?: string, agentIcon?: string): IAgentInfo | undefined {
85
- if (!agentName && !agentIcon) {
86
- return undefined;
87
- }
88
- return {
89
- id: '',
90
- name: agentName || '',
91
- icon: agentIcon || '',
92
- };
93
- }
94
-
95
84
  export interface AgentHelperCardProps {
96
85
  helper: IAgentHelper;
97
86
  agentName?: string;
98
87
  agentIcon?: string;
88
+ getAgent: (agentId: string) => IAgentInfo | undefined;
99
89
  className?: string;
100
90
  onEnabledChange?: (enabled: boolean) => void;
101
91
  onViewLogs?: () => void;
@@ -108,18 +98,17 @@ export interface AgentHelperCardProps {
108
98
 
109
99
  export const AgentHelperCard = ({
110
100
  helper,
111
- agentName,
112
- agentIcon,
113
101
  className,
114
102
  onEnabledChange,
115
103
  onViewLogs,
116
104
  onEdit,
105
+ getAgent,
117
106
  onCopy,
118
107
  onDelete,
119
108
  formService,
120
109
  }: AgentHelperCardProps) => {
121
110
  const description = useTriggerDescription(helper, formService);
122
- const agent = buildAgentInfo(agentName, agentIcon);
111
+ const agent = getAgent(helper.agentId);
123
112
 
124
113
  return (
125
114
  <div
@@ -1,12 +1,13 @@
1
1
  import { cn } from '../../../lib/utils';
2
2
  import { AgentHelperCard } from '../agent-helper-card';
3
- import type { IAgentHelper, IBsFormService } from '@baishuyun/types';
3
+ import type { IAgentHelper, IAgentInfo, IBsFormService } from '@baishuyun/types';
4
4
 
5
5
  export interface AgentHelperCardListProps {
6
6
  items: Array<IAgentHelper>;
7
7
  className?: string;
8
8
  getAgentName?: (agentId: string) => string | undefined;
9
9
  getAgentIcon?: (agentId: string) => string | undefined;
10
+ getAgent: (agentId: string) => (IAgentInfo | undefined);
10
11
  onEnabledChange?: (helperId: string, enabled: boolean) => void;
11
12
  onViewLogs?: (helperId: string) => void;
12
13
  onEdit?: (helperId: string) => void;
@@ -18,8 +19,7 @@ export interface AgentHelperCardListProps {
18
19
  export const AgentHelperCardList = ({
19
20
  items,
20
21
  className,
21
- getAgentName,
22
- getAgentIcon,
22
+ getAgent,
23
23
  onEnabledChange,
24
24
  onViewLogs,
25
25
  onEdit,
@@ -39,8 +39,7 @@ export const AgentHelperCardList = ({
39
39
  formService={formService}
40
40
  key={helper.id}
41
41
  helper={helper}
42
- agentName={getAgentName?.(helper.agentId)}
43
- agentIcon={getAgentIcon?.(helper.agentId)}
42
+ getAgent={getAgent}
44
43
  className="w-[335px] shrink-0"
45
44
  onEnabledChange={
46
45
  onEnabledChange
@@ -145,9 +145,14 @@ const setTriggerRef = useCallback((node: HTMLButtonElement | null) => {
145
145
  </>
146
146
  );
147
147
 
148
+ const s = showSlider ? slider : null;
149
+ if (!s && !agents.length) {
150
+ return null;
151
+ }
152
+
148
153
  return (
149
154
  <div className={cn('flex items-center gap-2.5 w-fit max-w-full min-w-0', className)}>
150
- {showSlider ? slider : null}
155
+ {s}
151
156
  {agents.length > 0 && (
152
157
  <Popover open={popoverOpen} onOpenChange={setPopoverOpen}>
153
158
  <PopoverTrigger asChild>
@@ -155,7 +160,7 @@ const setTriggerRef = useCallback((node: HTMLButtonElement | null) => {
155
160
  ref={setTriggerRef}
156
161
  type="button"
157
162
  className={cn(
158
- 'flex items-center gap-[6px] shrink-0 h-[34px]',
163
+ 'flex items-center gap-[6px] shrink-0 h-[34px] w-[34px]',
159
164
  'px-[10px] py-[5px] rounded-[4px]',
160
165
  'text-[14px] text-[#121111] leading-[normal]',
161
166
  'hover:bg-[#EFF0F6]',
@@ -166,7 +171,7 @@ const setTriggerRef = useCallback((node: HTMLButtonElement | null) => {
166
171
  )}
167
172
  >
168
173
  <MenuLinesIcon />
169
- <span>技能</span>
174
+ {/* <span>技能</span> */}
170
175
  </button>
171
176
  </PopoverTrigger>
172
177
  <PopoverContent
@@ -10,6 +10,7 @@ export interface AgentSelectProps {
10
10
  agents: Array<AgentSelectAgent>;
11
11
  showAllItems?: boolean;
12
12
  showSearch?: boolean;
13
+ className?: string;
13
14
  activeAgent?: { id: string } | null;
14
15
  onSelect: (agent: AgentSelectAgent) => void | Promise<void>;
15
16
  }
@@ -25,6 +26,7 @@ export const AgentSelect = ({
25
26
  activeAgent,
26
27
  onSelect,
27
28
  showAllItems,
29
+ className,
28
30
  }: AgentSelectProps) => {
29
31
  const [searchQuery, setSearchQuery] = useState('');
30
32
 
@@ -56,7 +58,7 @@ export const AgentSelect = ({
56
58
  })
57
59
 
58
60
  return (
59
- <div className="flex flex-col">
61
+ <div className={classNames('flex flex-col', 'rounded-[4px]', className)}>
60
62
  {showSearch && (
61
63
  <div className="px-1.5 pt-1 pb-0.5">
62
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]">
@@ -9,10 +9,10 @@ import {
9
9
  import { createPortal } from 'react-dom';
10
10
  import { cn } from '../../lib/utils';
11
11
  import { transCls } from '../../const/ui';
12
- import { ScrollSlider } from './scroll-slider';
12
+ import { FlatNode, ScrollSlider } from './scroll-slider';
13
13
  import { EntryPopover } from './entry-popover';
14
- import { buildEntryTreeFromAliasMap } from './build-tree';
15
- import type { BsEntrySelectProps, BsEntrySelectChangeResult } from './types';
14
+ import { buildEntryTreeFromAliasMap, buildFlowCenterTree } from './build-tree';
15
+ import type { BsEntrySelectProps, BsEntrySelectChangeResult, EntryTreeItem } from './types';
16
16
  import { MenuLinesIcon } from '../bs-icons';
17
17
  import { useSelectPopoverPosition } from '../../hooks/use-select-popover-position';
18
18
 
@@ -163,7 +163,27 @@ export const BsEntrySelect = forwardRef<HTMLDivElement, BsEntrySelectProps>(
163
163
 
164
164
  const currentAppTree = useMemo(() => {
165
165
  if (!currentAppId || !appDetails[currentAppId]) return [];
166
- return buildEntryTreeFromAliasMap(appDetails[currentAppId], searchQuery);
166
+
167
+ const showFlowCenter = (appDetails[currentAppId ?? '']?.isFlow ?? false);
168
+
169
+ const flowCenterNodes: FlatNode[] = [];
170
+
171
+ // Flow center — first tab in tabs mode for flow-type apps
172
+ if (showFlowCenter) {
173
+ const flowTree = buildFlowCenterTree();
174
+ flowCenterNodes.push(
175
+ ...flowTree.map((node) => ({
176
+ node,
177
+ appId: currentAppId ?? '',
178
+ section: 'current' as const,
179
+ })),
180
+ );
181
+ }
182
+
183
+ const flowCenterItems: EntryTreeItem[] = flowCenterNodes.map(n => n.node);
184
+ const result = buildEntryTreeFromAliasMap(appDetails[currentAppId], searchQuery);
185
+
186
+ return flowCenterItems.concat(result);
167
187
  }, [
168
188
  appDetails,
169
189
  currentAppId,
@@ -1,5 +1,6 @@
1
1
  import type { IAppDetail, IEntryAliasItem, IEntryDetail, IEntryRef } from '@baishuyun/types';
2
2
  import type { EntryTreeItem, BsEntrySelectChangeResult } from './types';
3
+ import { FLOW_CENTER_GROUP_ID, FLOW_CENTER_ENTRIES } from './types';
3
4
  import { FormSelectItem } from '../agent-helper/form-select';
4
5
 
5
6
  export const DEPTH_PADDING: Record<number, string> = {
@@ -110,6 +111,7 @@ export function buildEntryTreeFromAliasMap(
110
111
  for (const rootId of rootParents) {
111
112
  result.push(...buildRecursive(rootId, 0));
112
113
  }
114
+
113
115
  return result;
114
116
  }
115
117
 
@@ -213,3 +215,51 @@ export function findSelected(
213
215
  }
214
216
  return null;
215
217
  }
218
+
219
+ /** Create a virtual IEntryDetail for a flow center entry */
220
+ export function createFlowEntryDetail(entryId: string, name: string): IEntryDetail {
221
+ return {
222
+ _id: `${FLOW_CENTER_GROUP_ID}_${entryId}`,
223
+ entryId,
224
+ hasCoop: false,
225
+ hasFlow: true,
226
+ icon: 0,
227
+ isEvent: false,
228
+ isFavorite: false,
229
+ name,
230
+ parent: FLOW_CENTER_GROUP_ID,
231
+ seq: 0,
232
+ type: 'flow',
233
+ };
234
+ }
235
+
236
+ /** Build the synthetic "流程中心" group tree with 4 flow entries */
237
+ export function buildFlowCenterTree(): EntryTreeItem[] {
238
+ const children: EntryTreeItem[] = FLOW_CENTER_ENTRIES.map((item) => ({
239
+ // Use entryId directly as the node id so selected-state matching
240
+ // (selectedEntryId === node.id) works like real entries
241
+ id: item.entryId,
242
+ name: item.name,
243
+ icon: 0,
244
+ iconClass: `sdk-icon ${item.iconClass} text-[#0265ff]`,
245
+ type: 'flow',
246
+ children: [],
247
+ // depth 0: the group popover renders these as a flat list — no tree indent
248
+ depth: 0,
249
+ isSelectable: true,
250
+ entry: createFlowEntryDetail(item.entryId, item.name),
251
+ }));
252
+
253
+ const groupNode: EntryTreeItem = {
254
+ id: FLOW_CENTER_GROUP_ID,
255
+ name: '流程中心',
256
+ icon: 0,
257
+ type: 'group',
258
+ depth: 0,
259
+ isSelectable: false,
260
+ entry: null,
261
+ children,
262
+ };
263
+
264
+ return [groupNode];
265
+ }
@@ -28,9 +28,11 @@ export function EntryItem({
28
28
  <span className="flex items-center gap-[6px] min-w-0 flex-1">
29
29
  <i
30
30
  className={
31
- item.entry
32
- ? getEntryIconClass(item.entry)
33
- : 'node-icon icon-bsy-form icon-new-bsy-form-25'
31
+ item.iconClass
32
+ ? item.iconClass
33
+ : item.entry
34
+ ? getEntryIconClass(item.entry)
35
+ : 'node-icon icon-bsy-form icon-new-bsy-form-25'
34
36
  }
35
37
  style={{ fontSize: 16, lineHeight: 1 }}
36
38
  />
@@ -4,16 +4,18 @@ import {
4
4
  useEffect,
5
5
  useCallback,
6
6
  useMemo,
7
+ Fragment,
7
8
  } from 'react';
8
9
  import { createPortal } from 'react-dom';
9
10
  import { cn } from '../../lib/utils';
10
11
  import { transCls } from '../../const/ui';
11
12
  import { IconBtn } from '../icon-btn';
12
- import { LeftArrowIcon, SolidArrowDownIcon } from '../bs-icons';
13
+ import { LeftArrowIcon, SolidArrowDownIcon, DividerIcon } from '../bs-icons';
13
14
  import {
14
15
  buildEntryTreeFromAliasMap,
15
16
  getEntryIconClass,
16
17
  filterTreeBySearch,
18
+ buildFlowCenterTree,
17
19
  } from './build-tree';
18
20
  import { SearchInput } from './search-input';
19
21
  import { TreeItemNode } from './tree-item-node';
@@ -30,7 +32,7 @@ const ChevronRightIcon = ({ size }: { size?: number }) => (
30
32
  <SolidArrowDownIcon className="-rotate-90" size={size} />
31
33
  );
32
34
 
33
- interface FlatNode {
35
+ export interface FlatNode {
34
36
  node: EntryTreeItem;
35
37
  appId: string;
36
38
  section: 'current' | 'other';
@@ -73,10 +75,27 @@ export function ScrollSlider({
73
75
  { estimatedHeight: 340 },
74
76
  );
75
77
 
78
+ const hoverCloseTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
79
+
76
80
  const isTabs = variant === 'tabs';
81
+ const showFlowCenter = isTabs && (appDetails[currentAppId ?? '']?.isFlow ?? false);
77
82
 
78
83
  // Build first-level nodes from all apps (no search filter)
79
84
  const flatNodes = useMemo<FlatNode[]>(() => {
85
+ const flowCenterNodes: FlatNode[] = [];
86
+
87
+ // Flow center — first tab in tabs mode for flow-type apps
88
+ if (showFlowCenter) {
89
+ const flowTree = buildFlowCenterTree();
90
+ flowCenterNodes.push(
91
+ ...flowTree.map((node) => ({
92
+ node,
93
+ appId: currentAppId ?? '',
94
+ section: 'current' as const,
95
+ })),
96
+ );
97
+ }
98
+
80
99
  const appIds = Object.keys(appDetails);
81
100
  const currentFirst: FlatNode[] = [];
82
101
  const otherNodes: FlatNode[] = [];
@@ -93,12 +112,13 @@ export function ScrollSlider({
93
112
  }
94
113
  }
95
114
 
96
- const merged = [...currentFirst, ...otherNodes];
97
- return showCurrentAppEntriesOnly ? currentFirst : merged;
115
+ const merged = [...flowCenterNodes, ...currentFirst, ...otherNodes];
116
+ return showCurrentAppEntriesOnly ? [...flowCenterNodes, ...currentFirst] : merged;
98
117
  }, [
99
118
  appDetails,
100
119
  currentAppId,
101
120
  showCurrentAppEntriesOnly,
121
+ showFlowCenter,
102
122
  Object.keys(appDetails)
103
123
  .map((id) => (appDetails[id]?.entryList?.length ?? 0))
104
124
  .join(','),
@@ -110,6 +130,17 @@ export function ScrollSlider({
110
130
  return filterTreeBySearch(activeGroup.node.children, groupSearchQuery);
111
131
  }, [activeGroup, groupSearchQuery]);
112
132
 
133
+ // Map from group pill node ID → FlatNode, for native hover delegation
134
+ const groupFlatNodeById = useMemo(() => {
135
+ const map = new Map<string, FlatNode>();
136
+ for (const fn of flatNodes) {
137
+ if (!fn.node.isSelectable) {
138
+ map.set(fn.node.id, fn);
139
+ }
140
+ }
141
+ return map;
142
+ }, [flatNodes]);
143
+
113
144
  const checkScroll = useCallback(() => {
114
145
  const el = scrollRef.current;
115
146
  if (!el) return;
@@ -163,9 +194,52 @@ export function ScrollSlider({
163
194
  ) as HTMLElement | null;
164
195
  }
165
196
  }
166
- if (pill) {
167
- pill.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });
168
- }
197
+ if (!pill) return;
198
+
199
+ const container = scrollRef.current;
200
+ const PAD = 12; // matches scrollPaddingInline, covers corner flares
201
+ const start = performance.now();
202
+ let rafId = 0;
203
+
204
+ // Ease toward the pill with the target recomputed EVERY frame. A one-shot
205
+ // scrollIntoView({behavior: 'smooth'}) computes its destination upfront —
206
+ // the selection triggers a host navigation whose follow-up layout changes
207
+ // (content reload, arrow show/hide) shift the container mid-animation and
208
+ // the pill lands clipped. Live recomputation keeps this one continuous
209
+ // ease-out motion that always converges, with no visible second jump.
210
+ const step = () => {
211
+ const containerRect = container.getBoundingClientRect();
212
+ const pillRect = pill.getBoundingClientRect();
213
+ let delta = 0;
214
+ if (pillRect.left < containerRect.left + PAD) {
215
+ delta = pillRect.left - containerRect.left - PAD;
216
+ } else if (pillRect.right > containerRect.right - PAD) {
217
+ delta = pillRect.right - containerRect.right + PAD;
218
+ }
219
+ if (Math.abs(delta) < 1) return; // arrived
220
+ container.scrollLeft += delta * 0.3;
221
+ if (performance.now() - start < 600) {
222
+ rafId = requestAnimationFrame(step);
223
+ }
224
+ };
225
+ rafId = requestAnimationFrame(step);
226
+
227
+ // Stop animating if the user takes over scrolling
228
+ const cancelOnUserScroll = () => cancelAnimationFrame(rafId);
229
+ container.addEventListener('wheel', cancelOnUserScroll, {
230
+ once: true,
231
+ passive: true,
232
+ });
233
+ container.addEventListener('touchstart', cancelOnUserScroll, {
234
+ once: true,
235
+ passive: true,
236
+ });
237
+
238
+ return () => {
239
+ cancelAnimationFrame(rafId);
240
+ container.removeEventListener('wheel', cancelOnUserScroll);
241
+ container.removeEventListener('touchstart', cancelOnUserScroll);
242
+ };
169
243
  }, [selectedEntryId, descendantToGroupId]);
170
244
 
171
245
  useEffect(() => {
@@ -195,46 +269,168 @@ export function ScrollSlider({
195
269
  activeGroupIdRef.current = null;
196
270
  setActiveGroup(null);
197
271
  setGroupSearchQuery('');
272
+ if (hoverCloseTimerRef.current) {
273
+ clearTimeout(hoverCloseTimerRef.current);
274
+ hoverCloseTimerRef.current = null;
275
+ }
198
276
  }, []);
199
277
 
200
- const openGroupPopover = useCallback((flatNode: FlatNode, event: React.MouseEvent) => {
201
- event.stopPropagation();
202
- if (activeGroup?.node.id === flatNode.node.id) {
203
- activeGroupIdRef.current = null;
204
- setActiveGroup(null);
205
- setGroupSearchQuery('');
206
- return;
278
+ const cancelCloseTimer = useCallback(() => {
279
+ if (hoverCloseTimerRef.current) {
280
+ clearTimeout(hoverCloseTimerRef.current);
281
+ hoverCloseTimerRef.current = null;
207
282
  }
208
- setGroupSearchQuery('');
209
- activeGroupIdRef.current = flatNode.node.id;
210
- setActiveGroup(flatNode);
211
-
212
- // Check Shadow DOM portal
213
- const triggerEl = groupPillRefs.current.get(flatNode.node.id);
214
- if (triggerEl) {
215
- const rootNode = triggerEl.getRootNode();
216
- if (rootNode instanceof ShadowRoot) {
217
- const existing = rootNode.querySelector<HTMLElement>(
218
- '[data-bs-entry-select-group-portal]',
219
- );
220
- if (existing) {
221
- setGroupPortalContainer(existing);
283
+ }, []);
284
+
285
+ // Delayed close — cancelled when the mouse re-enters the pill or popover.
286
+ // The :hover guard prevents closing when a transient leave fired but the
287
+ // cursor is still over the pill or popover (e.g. layout-shift artifacts).
288
+ const scheduleClosePopover = useCallback(() => {
289
+ cancelCloseTimer();
290
+ hoverCloseTimerRef.current = setTimeout(() => {
291
+ const pillEl = activeGroupIdRef.current
292
+ ? groupPillRefs.current.get(activeGroupIdRef.current)
293
+ : null;
294
+ if (pillEl?.matches(':hover') || popoverRef.current?.matches(':hover')) {
295
+ return;
296
+ }
297
+ closeGroupPopover();
298
+ }, 200);
299
+ }, [cancelCloseTimer, closeGroupPopover]);
300
+
301
+ // Core open logic shared by click and hover paths
302
+ const openGroupPopoverCore = useCallback(
303
+ (flatNode: FlatNode) => {
304
+ // Clear any pending close timer
305
+ cancelCloseTimer();
306
+
307
+ // Skip if already open for this group (hover keeps it open)
308
+ if (activeGroup?.node.id === flatNode.node.id) return;
309
+
310
+ setGroupSearchQuery('');
311
+ activeGroupIdRef.current = flatNode.node.id;
312
+ setActiveGroup(flatNode);
313
+
314
+ // Check Shadow DOM portal
315
+ const triggerEl = groupPillRefs.current.get(flatNode.node.id);
316
+ if (triggerEl) {
317
+ const rootNode = triggerEl.getRootNode();
318
+ if (rootNode instanceof ShadowRoot) {
319
+ const existing = rootNode.querySelector<HTMLElement>(
320
+ '[data-bs-entry-select-group-portal]',
321
+ );
322
+ if (existing) {
323
+ setGroupPortalContainer(existing);
324
+ } else {
325
+ const container = document.createElement('div');
326
+ container.setAttribute('data-bs-entry-select-group-portal', 'true');
327
+ rootNode.appendChild(container);
328
+ setGroupPortalContainer(container);
329
+ }
222
330
  } else {
223
- const container = document.createElement('div');
224
- container.setAttribute('data-bs-entry-select-group-portal', 'true');
225
- rootNode.appendChild(container);
226
- setGroupPortalContainer(container);
331
+ setGroupPortalContainer(null);
227
332
  }
228
- } else {
229
- setGroupPortalContainer(null);
230
333
  }
231
- }
232
334
 
233
- // Position the popover after the next frame
234
- requestAnimationFrame(() => {
335
+ // Position synchronously — the pill is already mounted, so its rect is
336
+ // available immediately. Batching this with setActiveGroup ensures the
337
+ // popover mounts at the correct position in the same commit; a deferred
338
+ // (rAF) position would render one frame in-flow at the end of the body,
339
+ // shifting layout and firing a spurious mouseleave on the pill.
235
340
  updatePopoverPosition();
236
- });
237
- }, [activeGroup, updatePopoverPosition]);
341
+ },
342
+ [activeGroup, cancelCloseTimer, updatePopoverPosition],
343
+ );
344
+
345
+ const openGroupPopover = useCallback(
346
+ (flatNode: FlatNode, event: React.MouseEvent) => {
347
+ event.stopPropagation();
348
+ if (activeGroup?.node.id === flatNode.node.id) {
349
+ activeGroupIdRef.current = null;
350
+ setActiveGroup(null);
351
+ setGroupSearchQuery('');
352
+ return;
353
+ }
354
+ openGroupPopoverCore(flatNode);
355
+ },
356
+ [activeGroup, openGroupPopoverCore],
357
+ );
358
+
359
+ // Hover handlers for tabs mode.
360
+ //
361
+ // NOTE: these are driven by NATIVE mouseover/mouseout listeners (attached in
362
+ // the effects below), not React's onMouseEnter/onMouseLeave. React's
363
+ // synthetic enter/leave events do not fire when the pointer crosses from
364
+ // light DOM into a shadow root — which is exactly what happens when the
365
+ // mouse enters a tab from the host content area below the tab bar.
366
+ const handleNativeOver = useCallback(
367
+ (e: MouseEvent) => {
368
+ const t = e.target as HTMLElement | null;
369
+ if (!t) return;
370
+ if (popoverRef.current?.contains(t)) {
371
+ cancelCloseTimer();
372
+ return;
373
+ }
374
+ const pillEl = t.closest?.('[data-entry-id]') as HTMLElement | null;
375
+ const flatNode = pillEl
376
+ ? groupFlatNodeById.get(pillEl.getAttribute('data-entry-id') ?? '')
377
+ : undefined;
378
+ if (flatNode) {
379
+ cancelCloseTimer();
380
+ openGroupPopoverCore(flatNode);
381
+ } else {
382
+ // Hovering a leaf pill or empty slider area — close any open group popover
383
+ scheduleClosePopover();
384
+ }
385
+ },
386
+ [cancelCloseTimer, groupFlatNodeById, openGroupPopoverCore, scheduleClosePopover],
387
+ );
388
+
389
+ const handleNativeOut = useCallback(
390
+ (e: MouseEvent) => {
391
+ const to = e.relatedTarget as HTMLElement | null;
392
+ if (to && scrollRef.current?.contains(to)) return;
393
+ if (to && popoverRef.current?.contains(to)) return;
394
+ scheduleClosePopover();
395
+ },
396
+ [scheduleClosePopover],
397
+ );
398
+
399
+ // Keep latest handlers in refs so listeners stay attached across re-renders
400
+ const handleNativeOverRef = useRef(handleNativeOver);
401
+ const handleNativeOutRef = useRef(handleNativeOut);
402
+ useEffect(() => {
403
+ handleNativeOverRef.current = handleNativeOver;
404
+ handleNativeOutRef.current = handleNativeOut;
405
+ });
406
+
407
+ // Native hover listeners on the scroll container (delegated to group pills)
408
+ useEffect(() => {
409
+ const el = scrollRef.current;
410
+ if (!el || !isTabs) return;
411
+ const over = (e: MouseEvent) => handleNativeOverRef.current(e);
412
+ const out = (e: MouseEvent) => handleNativeOutRef.current(e);
413
+ el.addEventListener('mouseover', over);
414
+ el.addEventListener('mouseout', out);
415
+ return () => {
416
+ el.removeEventListener('mouseover', over);
417
+ el.removeEventListener('mouseout', out);
418
+ };
419
+ }, [isTabs]);
420
+
421
+ // Native hover listeners on the group popover (rendered in a portal subtree)
422
+ useEffect(() => {
423
+ const pop = popoverRef.current;
424
+ if (!isTabs || !activeGroup || !pop) return;
425
+ const over = (e: MouseEvent) => handleNativeOverRef.current(e);
426
+ const out = (e: MouseEvent) => handleNativeOutRef.current(e);
427
+ pop.addEventListener('mouseover', over);
428
+ pop.addEventListener('mouseout', out);
429
+ return () => {
430
+ pop.removeEventListener('mouseover', over);
431
+ pop.removeEventListener('mouseout', out);
432
+ };
433
+ }, [isTabs, activeGroup]);
238
434
 
239
435
  const handleGroupPopoverSelect = useCallback(
240
436
  (result: BsEntrySelectChangeResult) => {
@@ -302,19 +498,53 @@ export function ScrollSlider({
302
498
 
303
499
  <div
304
500
  ref={scrollRef}
501
+ // scroll-padding: scrollIntoView aligns pills this far from the
502
+ // container edge, keeping the selected tab's outward corner flares
503
+ // (protruding 9px) fully visible instead of clipped
504
+ style={isTabs ? { scrollPaddingInline: 12 } : undefined}
305
505
  className={cn(
306
506
  'flex-1 overflow-x-auto scrollbar-hide flex min-w-0',
307
507
  isTabs
308
- ? 'items-end gap-0 overflow-y-hidden border-b-[#e0e0e0] before:content-[""] before:absolute before:bottom-[0px] before:left-[-200px] before:right-0 before:h-[1px] before:w-[100vw] before:bg-[#e0e0e0] h-full'
508
+ // px compensates -mx: keeps pills at the same visual position
509
+ // while letting bottom corner flares paint without being clipped
510
+ ? 'items-end gap-0 overflow-y-hidden px-[10px] -mx-[10px] border-b-[#e0e0e0] before:content-[""] before:absolute before:bottom-[0px] before:left-[-200px] before:right-0 before:h-[1px] before:w-[200vw] before:bg-[#e0e0e0] h-full'
309
511
  : 'items-center gap-2.5',
310
512
  )}
311
513
  >
312
- {flatNodes.map((flatNode) => {
514
+ {flatNodes.map((flatNode, index) => {
313
515
  const { node, appId, section } = flatNode;
314
516
  const isLeaf = node.isSelectable;
315
517
  const isSelected = isLeaf && selectedEntryId === node.id;
316
- const isGroupActive = activeGroup?.node.id === node.id
317
- || node?.children.findIndex(c => c.id === selectedEntryId) >= 0;
518
+ // Active tab styling applies only when a descendant entry is
519
+ // selected — hovering/clicking to open the popover must NOT make
520
+ // the group look active (that would show two active tabs at once)
521
+ const isGroupActive = node?.children.findIndex(c => c.id === selectedEntryId) >= 0;
522
+ // Popover-open is a separate, transient state — drives the chevron
523
+ const isGroupPopoverOpen = activeGroup?.node.id === node.id;
524
+
525
+ // Divider between inactive tabs: always rendered in tabs mode so
526
+ // the strip width never shifts on selection change — only its
527
+ // visibility toggles (hidden next to the active tab, which has
528
+ // its own borders)
529
+ const isNodeActive = (fn: FlatNode | null): boolean => {
530
+ if (!fn) return false;
531
+ return fn.node.isSelectable
532
+ ? selectedEntryId === fn.node.id
533
+ : fn.node.children.findIndex((c) => c.id === selectedEntryId) >= 0;
534
+ };
535
+ const dividerVisible =
536
+ !isNodeActive(flatNode) && !isNodeActive(flatNodes[index - 1]);
537
+ const divider = isTabs && index > 0 ? (
538
+ <span
539
+ aria-hidden
540
+ className={cn(
541
+ 'self-center shrink-0 flex items-center justify-center w-[1px]',
542
+ !dividerVisible && 'invisible',
543
+ )}
544
+ >
545
+ <DividerIcon />
546
+ </span>
547
+ ) : null;
318
548
 
319
549
  const setPillRef = (el: HTMLButtonElement | null) => {
320
550
  if (el) {
@@ -326,8 +556,9 @@ export function ScrollSlider({
326
556
 
327
557
  if (isLeaf) {
328
558
  return (
329
- <button
330
- key={node.id}
559
+ <Fragment key={node.id}>
560
+ {divider}
561
+ <button
331
562
  data-entry-id={node.id}
332
563
  type="button"
333
564
  onClick={() => {
@@ -336,65 +567,197 @@ export function ScrollSlider({
336
567
  }
337
568
  }}
338
569
  className={cn(
339
- 'flex items-center gap-[6px] shrink-0 max-w-[240px]',
570
+ 'flex items-center shrink-0 max-w-[240px]',
340
571
  transCls,
341
572
  isTabs
342
573
  ? [
343
- 'px-[16px] py-[8px] relative z-10 border-b-none h-full',
344
- isSelected
345
- ? 'border-r border-t border-l border-[#e0e0e0] bg-[#f6f7fa] text-[#0265FF] rounded-t-[10px] rounded-b-none '
346
- : 'text-[#030303] hover:bg-[#F0F6FF] rounded-t-[8px]',
574
+ // Button stays h-full so text keeps its Y position;
575
+ // the visual tab box is the decorative span below
576
+ 'group px-[10px] py-[8px] relative z-10 h-full',
577
+ isSelected ? 'text-[#0265FF]' : 'text-[#030303]',
347
578
  ]
348
579
  : [
349
- 'px-[10px] py-[7px] rounded-[4px]',
580
+ 'px-[10px] py-[7px] rounded-[4px] gap-[6px]',
350
581
  isSelected
351
582
  ? 'border border-[#F0F6FF] bg-[#F0F6FF] text-[#0265FF]'
352
583
  : 'border border-[#E0E0E0] text-[#030303] hover:bg-[#F0F6FF]',
353
584
  ],
354
585
  )}
355
586
  >
356
- <i
357
- className={
358
- node.entry
359
- ? getEntryIconClass(node.entry)
360
- : 'node-icon icon-bsy-form icon-new-bsy-form-25'
361
- }
362
- style={{ fontSize: 16, lineHeight: 1 }}
363
- />
364
- <span className="text-[14px] leading-[normal] truncate">
365
- {node.name}
587
+ {/* Visual tab box — side borders stop 10px above the bar so
588
+ the concave corner arcs can continue them; transparent
589
+ borders when unselected (decorative only, no layout shift).
590
+ No transCls here: the fill strip and corner flares below
591
+ appear/disappear instantly on selection change, so this
592
+ box must switch instantly too — otherwise its fading
593
+ background leaves a ghost image on the deselected tab */}
594
+ {isTabs && (
595
+ <span
596
+ aria-hidden
597
+ className={cn(
598
+ 'pointer-events-none absolute inset-x-0 top-[6px] bottom-[10px] border-r border-t border-l border-b-none rounded-t-[10px] rounded-b-none',
599
+ isSelected
600
+ ? 'border-[#e0e0e0] bg-[#f6f7fa]'
601
+ : 'border-transparent',
602
+ )}
603
+ />
604
+ )}
605
+ {isTabs && isSelected && (
606
+ <>
607
+ {/* Fill below the tab body — covers the bar line under the tab */}
608
+ <span
609
+ aria-hidden
610
+ className="pointer-events-none absolute inset-x-0 bottom-0 h-[10px] bg-[#f6f7fa]"
611
+ />
612
+ {/* Concave corner flares. The square is shifted 1px
613
+ inward (left/right-[-9px]) so the full border ring —
614
+ including its tangent junctions with the side border
615
+ (vertical tangent) and the bar line (horizontal
616
+ tangent) — fits inside the element and paints above
617
+ the bottom fill strip */}
618
+ <span
619
+ aria-hidden
620
+ className="pointer-events-none absolute left-[-9px] bottom-0 w-[10px] h-[10px]"
621
+ style={{
622
+ background:
623
+ 'radial-gradient(circle at 0% 0%, transparent 9px, #e0e0e0 9px, #e0e0e0 10px, #f6f7fa 10px)',
624
+ }}
625
+ />
626
+ <span
627
+ aria-hidden
628
+ className="pointer-events-none absolute right-[-9px] bottom-0 w-[10px] h-[10px]"
629
+ style={{
630
+ background:
631
+ 'radial-gradient(circle at 100% 0%, transparent 9px, #e0e0e0 9px, #e0e0e0 10px, #f6f7fa 10px)',
632
+ }}
633
+ />
634
+ </>
635
+ )}
636
+ {/* In tabs mode, hover highlights only this icon+text chip —
637
+ the tab background itself stays unchanged. No transCls:
638
+ on selection the hover bg must clear instantly, otherwise
639
+ it fades out slowly and leaves a chip-shaped ghost on the
640
+ newly selected tab */}
641
+ <span
642
+ className={cn(
643
+ 'flex items-center gap-[6px] min-w-0',
644
+ isTabs &&
645
+ cn(
646
+ 'relative rounded-[6px] px-[20px] py-[6px]',
647
+ !isSelected && 'group-hover:bg-[#F0F6FF]',
648
+ ),
649
+ )}
650
+ >
651
+ <i
652
+ className={
653
+ node.iconClass
654
+ ? node.iconClass
655
+ : node.entry
656
+ ? getEntryIconClass(node.entry)
657
+ : 'node-icon icon-bsy-form icon-new-bsy-form-25'
658
+ }
659
+ style={{ fontSize: 16, lineHeight: 1 }}
660
+ />
661
+ <span className="text-[14px] leading-[normal] truncate">
662
+ {node.name}
663
+ </span>
366
664
  </span>
367
665
  </button>
666
+ </Fragment>
368
667
  );
369
668
  }
370
669
 
371
670
  // Group pill (has descendants)
372
671
  return (
672
+ <Fragment key={node.id}>
673
+ {divider}
373
674
  <button
374
- key={node.id}
375
675
  data-entry-id={node.id}
376
676
  ref={setPillRef}
377
677
  type="button"
378
- onClick={(e) => openGroupPopover(flatNode, e)}
678
+ {...(!isTabs
679
+ ? {
680
+ onClick: (e: React.MouseEvent) => openGroupPopover(flatNode, e),
681
+ }
682
+ : {})}
379
683
  className={cn(
380
- 'flex items-center gap-[10px] shrink-0 max-w-[240px]',
684
+ 'flex items-center shrink-0 max-w-[240px]',
381
685
  transCls,
382
686
  isTabs
383
687
  ? [
384
- 'px-[16px] py-[8px] relative z-10 h-full',
385
- isGroupActive
386
- ? 'border-r border-t border-l border-[#e0e0e0] bg-[#f6f7fa] text-[#0265FF] rounded-t-[10px] rounded-b-none '
387
- : 'text-[#030303] hover:bg-[#F0F6FF] rounded-t-[8px]',
688
+ // Button stays h-full so text keeps its Y position;
689
+ // the visual tab box is the decorative span below
690
+ 'group gap-[4px] px-[10px] py-[8px] relative z-10 h-full',
691
+ isGroupActive ? 'text-[#0265FF]' : 'text-[#030303]',
388
692
  ]
389
693
  : [
390
- 'px-[10px] py-[7px] rounded-[4px]',
694
+ 'gap-[10px] px-[10px] py-[7px] rounded-[4px]',
391
695
  isGroupActive
392
696
  ? 'border border-[#0265FF] text-[#030303]'
393
697
  : 'border border-[#E0E0E0] text-[#030303] hover:bg-[#F0F6FF]',
394
698
  ],
395
699
  )}
396
700
  >
397
- <span className="flex items-center gap-[6px] min-w-0 flex-1">
701
+ {/* Visual tab box — side borders stop 10px above the bar so
702
+ the concave corner arcs can continue them; transparent
703
+ borders when inactive (decorative only, no layout shift) */}
704
+ {isTabs && (
705
+ <span
706
+ aria-hidden
707
+ className={cn(
708
+ 'pointer-events-none absolute inset-x-0 top-[6px] bottom-[10px] border-r border-t border-l border-b-none rounded-t-[10px] rounded-b-none',
709
+ isGroupActive
710
+ ? 'border-[#e0e0e0] bg-[#f6f7fa]'
711
+ : 'border-transparent',
712
+ )}
713
+ />
714
+ )}
715
+ {isTabs && isGroupActive && (
716
+ <>
717
+ {/* Fill below the tab body — covers the bar line under the tab */}
718
+ <span
719
+ aria-hidden
720
+ className="pointer-events-none absolute inset-x-0 bottom-0 h-[10px] bg-[#f6f7fa]"
721
+ />
722
+ {/* Concave corner flares. The square is shifted 1px
723
+ inward (left/right-[-9px]) so the full border ring —
724
+ including its tangent junctions with the side border
725
+ (vertical tangent) and the bar line (horizontal
726
+ tangent) — fits inside the element and paints above
727
+ the bottom fill strip */}
728
+ <span
729
+ aria-hidden
730
+ className="pointer-events-none absolute left-[-9px] bottom-0 w-[10px] h-[10px]"
731
+ style={{
732
+ background:
733
+ 'radial-gradient(circle at 0% 0%, transparent 9px, #e0e0e0 9px, #e0e0e0 10px, #f6f7fa 10px)',
734
+ }}
735
+ />
736
+ <span
737
+ aria-hidden
738
+ className="pointer-events-none absolute right-[-9px] bottom-0 w-[10px] h-[10px]"
739
+ style={{
740
+ background:
741
+ 'radial-gradient(circle at 100% 0%, transparent 9px, #e0e0e0 9px, #e0e0e0 10px, #f6f7fa 10px)',
742
+ }}
743
+ />
744
+ </>
745
+ )}
746
+ {/* In tabs mode, hover highlights only this icon+text chip —
747
+ the tab background itself stays unchanged. No transCls:
748
+ on activation the hover bg must clear instantly, otherwise
749
+ it fades out slowly and leaves a chip-shaped ghost on the
750
+ newly activated tab */}
751
+ <span
752
+ className={cn(
753
+ 'flex items-center gap-[6px] min-w-0 flex-1',
754
+ isTabs &&
755
+ cn(
756
+ 'relative rounded-[6px] px-[20px] py-[6px]',
757
+ !isGroupActive && 'group-hover:bg-[#F0F6FF]',
758
+ ),
759
+ )}
760
+ >
398
761
  <i
399
762
  className="icon-bsy-folder-close"
400
763
  style={{ fontSize: 16, lineHeight: 1 }}
@@ -405,14 +768,15 @@ export function ScrollSlider({
405
768
  </span>
406
769
  <span
407
770
  className={cn(
408
- 'shrink-0 flex items-center justify-center w-[16px] h-[16px] text-[#666666]',
771
+ 'relative shrink-0 flex items-center justify-center w-[16px] h-[16px] text-[#666666]',
409
772
  transCls,
410
- isGroupActive && 'rotate-90',
773
+ isGroupPopoverOpen && 'rotate-90',
411
774
  )}
412
775
  >
413
776
  <ChevronRightIcon size={16} />
414
777
  </span>
415
778
  </button>
779
+ </Fragment>
416
780
  );
417
781
  })}
418
782
  </div>
@@ -463,6 +827,14 @@ export function ScrollSlider({
463
827
  transCls,
464
828
  )}
465
829
  >
830
+ {/* Transparent hover bridges: keep the mouse "inside" the popover
831
+ while crossing the gap between pill and popover */}
832
+ {isTabs && (
833
+ <>
834
+ <div className="absolute left-0 right-0 -top-[6px] h-[6px]" />
835
+ <div className="absolute left-0 right-0 -bottom-[6px] h-[6px]" />
836
+ </>
837
+ )}
466
838
  <SearchInput
467
839
  placeholder='搜索表单名称'
468
840
  value={groupSearchQuery}
@@ -6,6 +6,16 @@ export interface BsEntrySelectChangeResult {
6
6
  section: 'current' | 'other';
7
7
  }
8
8
 
9
+ /** Flow center — synthetic group tab for flow-type apps in tabs mode */
10
+ export const FLOW_CENTER_GROUP_ID = '__flow_center__';
11
+
12
+ export const FLOW_CENTER_ENTRIES = [
13
+ { entryId: 'todo', name: '待办任务', iconClass: 'icon-new-bsy-todo' },
14
+ { entryId: 'cc', name: '抄送我的', iconClass: 'icon-new-bsy-cc' },
15
+ { entryId: 'transactors', name: '我处理的', iconClass: 'icon-new-bsy-transactors' },
16
+ { entryId: 'create', name: '我发起的', iconClass: 'icon-new-bsy-create' },
17
+ ] as const;
18
+
9
19
  export type BsEntrySelectSliderVariant = 'default' | 'tabs';
10
20
 
11
21
  export interface BsEntrySelectProps {
@@ -31,4 +41,6 @@ export interface EntryTreeItem {
31
41
  depth: number;
32
42
  isSelectable: boolean;
33
43
  entry: IEntryDetail | null;
44
+ /** Optional custom icon CSS class — used for synthetic entries like flow center */
45
+ iconClass?: string;
34
46
  }
@@ -17,7 +17,7 @@ export const FakeBotMessage = ({
17
17
  <div>
18
18
  <MessageContent
19
19
  className={cn(
20
- 'w-fit wrap-break-words rounded-2 bg-transparent px-0 py-0 text-left',
20
+ 'wrap-break-words rounded-2 bg-transparent px-0 py-0 text-left w-full',
21
21
  className
22
22
  )}
23
23
  data-testid="message-content"
@@ -203,8 +203,8 @@ export const FieldsPreviewer = forwardRef<HTMLDivElement, FieldsPreviewerProps>(
203
203
  <AbsFullscreenGradientBg
204
204
  zIndex={zIndex}
205
205
  padding={isFields ? p : 0}
206
- disableGradient={fullscreen || !isFields}
207
- bgColor={isFields ? '#fff' : '#F5F7FA'}
206
+ disableGradient
207
+ bgColor="#f6f7fa"
208
208
  ref={setAbsContainer}
209
209
  >
210
210
  {fullscreen ? contentJsx : content}
@@ -0,0 +1,94 @@
1
+ export const EmptyAgentIcon = () => (
2
+ <svg xmlns="http://www.w3.org/2000/svg" width="155" height="97" viewBox="0 0 155 97" fill="none">
3
+ <mask id="mask0_33687_60435" style={{maskType: 'alpha'}} maskUnits="userSpaceOnUse" x="0" y="71" width="155" height="19">
4
+ <rect x="0.6875" y="71.3262" width="153.628" height="17.8319" fill="white"/>
5
+ </mask>
6
+ <g mask="url(#mask0_33687_60435)">
7
+ <ellipse cx="77.5017" cy="85.9947" rx="76.8142" ry="14.6686" fill="url(#paint0_linear_33687_60435)"/>
8
+ </g>
9
+ <ellipse cx="123.117" cy="60.3534" rx="6.51549" ry="8.91593" fill="url(#paint1_linear_33687_60435)"/>
10
+ <path d="M122.891 79.2946V52.6367L123.672 79.2946H122.891Z" fill="#D5DDF4"/>
11
+ <path d="M123.003 67.5213L118.875 63.7448L123.003 66.2995V67.5213Z" fill="#D5DDF4"/>
12
+ <path d="M125.011 59.7461L123.003 61.9676V62.7451L125.011 59.7461Z" fill="#D5DDF4"/>
13
+ <path d="M123.016 78.6367L103.15 79.296H123.016V78.6367Z" fill="url(#paint2_linear_33687_60435)"/>
14
+ <ellipse cx="38.4748" cy="66.1104" rx="3.77212" ry="4.80088" transform="rotate(-4.32208 38.4748 66.1104)" fill="url(#paint3_linear_33687_60435)"/>
15
+ <path d="M39.065 75.831L38.1016 63.084L38.1402 75.7356L39.065 75.831Z" fill="#D5DDF4"/>
16
+ <path d="M38.5854 70.1526L40.4091 68.2122L38.5415 69.5727L38.5854 70.1526Z" fill="#D5DDF4"/>
17
+ <path d="M37.3534 66.5346L38.3862 67.5168L38.414 67.8858L37.3534 66.5346Z" fill="#D5DDF4"/>
18
+ <path d="M38.9297 75.1816L17.0783 77.329L38.9794 75.839L38.9297 75.1816Z" fill="url(#paint4_linear_33687_60435)"/>
19
+ <path d="M96.0119 69.9553L100.847 50.027C101.266 48.3002 99.9583 46.6367 98.1814 46.6367H55.7188L59.1062 69.9553H96.0119Z" fill="#90ACF8"/>
20
+ <path d="M96.0119 69.9553L100.847 50.027C101.266 48.3002 99.9583 46.6367 98.1814 46.6367H55.7188L59.1062 69.9553H96.0119Z" fill="#90ACF8" fill-opacity="0.2"/>
21
+ <rect x="76.3438" y="32.0664" width="24.0044" height="28.9571" rx="1.37168" transform="rotate(14.142 76.3438 32.0664)" fill="url(#paint5_linear_33687_60435)" fill-opacity="0.2"/>
22
+ <rect x="68.1953" y="38.3086" width="23.6252" height="23.6724" rx="1.37168" transform="rotate(14.142 68.1953 38.3086)" fill="#90ACF8" fill-opacity="0.2"/>
23
+ <g filter="url(#filter3_d_33687_60435)">
24
+ <path d="M56.6096 69.7845L53.8535 42.3508C53.7521 41.3412 54.5448 40.4648 55.5595 40.4648H62.7559C63.2633 40.4648 63.7446 40.6895 64.0703 41.0785L66.468 43.941C66.9893 44.5633 67.7593 44.9228 68.5711 44.9228H92.1169C93.0009 44.9228 93.7401 45.595 93.8238 46.4752L96.0078 69.4508C96.1035 70.4573 95.3119 71.3277 94.3009 71.3277H58.3156C57.435 71.3277 56.6976 70.6606 56.6096 69.7845Z" fill="#F0F4FE"/>
25
+ </g>
26
+ <path d="M56.6096 69.7845L53.8535 42.3508C53.7521 41.3412 54.5448 40.4648 55.5595 40.4648H62.7559C63.2633 40.4648 63.7446 40.6895 64.0703 41.0785L66.468 43.941C66.9893 44.5633 67.7593 44.9228 68.5711 44.9228H92.1169C93.0009 44.9228 93.7401 45.595 93.8238 46.4752L96.0078 69.4508C96.1035 70.4573 95.3119 71.3277 94.3009 71.3277H58.3156C57.435 71.3277 56.6976 70.6606 56.6096 69.7845Z" fill="#F0F4FE" fill-opacity="0.2"/>
27
+ <mask id="mask1_33687_60435" style={{maskType: 'alpha'}} maskUnits="userSpaceOnUse" x="53" y="40" width="44" height="32">
28
+ <path d="M56.6096 69.7845L53.8535 42.3508C53.7521 41.3412 54.5448 40.4648 55.5595 40.4648H62.7559C63.2633 40.4648 63.7446 40.6895 64.0703 41.0785L66.468 43.941C66.9893 44.5633 67.7593 44.9228 68.5711 44.9228H92.1169C93.0009 44.9228 93.7401 45.595 93.8238 46.4752L96.0078 69.4508C96.1035 70.4573 95.3119 71.3277 94.3009 71.3277H58.3156C57.435 71.3277 56.6976 70.6606 56.6096 69.7845Z" fill="#F0F4FE"/>
29
+ </mask>
30
+ <g mask="url(#mask1_33687_60435)">
31
+ <g filter="url(#filter5_f_33687_60435)">
32
+ <circle cx="59.322" cy="40.8084" r="15.4314" fill="#B9FAFF"/>
33
+ </g>
34
+ </g>
35
+ <rect x="61.7266" y="53.4961" width="18.5177" height="2.74336" rx="0.685841" fill="#CEDCF6"/>
36
+ <rect x="62.4062" y="60.3555" width="11.6593" height="2.74336" rx="0.685841" fill="#CEDCF6"/>
37
+ <path d="M59.0767 34.0097C58.2476 33.5999 56.7207 33.7515 56.7207 33.7515C56.7207 33.7515 57.5181 35.3518 58.4504 35.8866C59.2981 36.3729 60.9114 36.3533 60.9114 36.3533C60.9114 36.3533 60.1207 34.5257 59.0767 34.0097Z" fill="url(#paint6_linear_33687_60435)"/>
38
+ <path d="M63.2906 34.3064C63.3572 33.7574 62.9649 32.928 62.9649 32.928C62.9649 32.928 62.2156 33.698 62.0988 34.3302C61.9925 34.905 62.3271 35.8094 62.3271 35.8094C62.3271 35.8094 63.2067 34.9976 63.2906 34.3064Z" fill="url(#paint7_linear_33687_60435)"/>
39
+ <path d="M108.994 42.7749C110.54 41.9201 111.955 39.3643 111.955 39.3643C111.955 39.3643 108.562 38.8796 106.707 39.7606C105.02 40.5616 103.304 43.1097 103.304 43.1097C103.304 43.1097 107.048 43.851 108.994 42.7749Z" fill="url(#paint8_linear_33687_60435)"/>
40
+ <defs>
41
+ <filter id="filter3_d_33687_60435" x="53.8457" y="40.4648" width="46.285" height="34.9783" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
42
+ <feFlood flood-opacity="0" result="BackgroundImageFix"/>
43
+ <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
44
+ <feOffset dx="2.05752" dy="2.05752"/>
45
+ <feGaussianBlur stdDeviation="1.02876"/>
46
+ <feComposite in2="hardAlpha" operator="out"/>
47
+ <feColorMatrix type="matrix" values="0 0 0 0 0.564706 0 0 0 0 0.67451 0 0 0 0 0.972549 0 0 0 0.2 0"/>
48
+ <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_33687_60435"/>
49
+ <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_33687_60435" result="shape"/>
50
+ </filter>
51
+ <filter id="filter5_f_33687_60435" x="-0.00318146" y="-18.5169" width="118.651" height="118.651" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
52
+ <feFlood flood-opacity="0" result="BackgroundImageFix"/>
53
+ <feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
54
+ <feGaussianBlur stdDeviation="21.9469" result="effect1_foregroundBlur_33687_60435"/>
55
+ </filter>
56
+ <linearGradient id="paint0_linear_33687_60435" x1="77.5017" y1="71.3262" x2="77.5017" y2="86.1595" gradientUnits="userSpaceOnUse">
57
+ <stop stop-color="#E6EEFF"/>
58
+ <stop offset="1" stop-color="white" stop-opacity="0"/>
59
+ </linearGradient>
60
+ <linearGradient id="paint1_linear_33687_60435" x1="123.117" y1="51.4375" x2="123.117" y2="69.2694" gradientUnits="userSpaceOnUse">
61
+ <stop stop-color="#E7EFFF"/>
62
+ <stop offset="1" stop-color="#F3F7FD"/>
63
+ </linearGradient>
64
+ <linearGradient id="paint2_linear_33687_60435" x1="123.016" y1="78.9663" x2="103.15" y2="78.9663" gradientUnits="userSpaceOnUse">
65
+ <stop stop-color="#D5DDF4"/>
66
+ <stop offset="1" stop-color="#EDF2FF"/>
67
+ </linearGradient>
68
+ <linearGradient id="paint3_linear_33687_60435" x1="38.4748" y1="61.3095" x2="38.4748" y2="70.9113" gradientUnits="userSpaceOnUse">
69
+ <stop stop-color="#E7EFFF"/>
70
+ <stop offset="1" stop-color="#F3F7FD"/>
71
+ </linearGradient>
72
+ <linearGradient id="paint4_linear_33687_60435" x1="38.8078" y1="75.3561" x2="17.0534" y2="77.0003" gradientUnits="userSpaceOnUse">
73
+ <stop stop-color="#D5DDF4"/>
74
+ <stop offset="1" stop-color="#EDF2FF"/>
75
+ </linearGradient>
76
+ <linearGradient id="paint5_linear_33687_60435" x1="88.346" y1="32.0664" x2="88.346" y2="61.0235" gradientUnits="userSpaceOnUse">
77
+ <stop stop-color="#E5FFFF"/>
78
+ <stop offset="1" stop-color="#90ACF8"/>
79
+ </linearGradient>
80
+ <linearGradient id="paint6_linear_33687_60435" x1="58.5152" y1="35.9539" x2="59.1557" y2="34.0174" gradientUnits="userSpaceOnUse">
81
+ <stop stop-color="#DBE6FF"/>
82
+ <stop offset="1" stop-color="#F1F5FF"/>
83
+ </linearGradient>
84
+ <linearGradient id="paint7_linear_33687_60435" x1="62.0735" y1="34.3802" x2="63.3021" y2="34.3556" gradientUnits="userSpaceOnUse">
85
+ <stop stop-color="#DBE6FF"/>
86
+ <stop offset="1" stop-color="#F1F5FF"/>
87
+ </linearGradient>
88
+ <linearGradient id="paint8_linear_33687_60435" x1="106.53" y1="39.7891" x2="108.875" y2="42.9064" gradientUnits="userSpaceOnUse">
89
+ <stop stop-color="#DBE6FF"/>
90
+ <stop offset="1" stop-color="#F1F5FF"/>
91
+ </linearGradient>
92
+ </defs>
93
+ </svg>
94
+ )
@@ -105,6 +105,7 @@ function findFieldByName(fields: OutputFieldOption[], fieldName: string): Output
105
105
  const VALID_TYPE_MAP: Partial<Record<FieldType, Set<FieldType>>> = {
106
106
  'text': new Set(['text', 'textarea']),
107
107
  'number': new Set(['text', 'textarea']),
108
+ 'radio': new Set(['radio', 'radiogroup']),
108
109
  }
109
110
 
110
111
  const EMPTY_SET = new Set();
package/src/index.ts CHANGED
@@ -20,6 +20,7 @@ export type { FormIconOption, LegacySlotRenderer } from './types';
20
20
 
21
21
  // Icons
22
22
  export { CalendarIcon } from './components/icons/calendar-icon';
23
+ export { EmptyAgentIcon } from './components/icons/empty-agent-icon';
23
24
 
24
25
  // UI primitives (shadcn)
25
26
  export * from './components/ui/button';