@@ -7,12 +7,216 @@ import {
77 Search20Regular ,
88 Globe20Regular ,
99 Database20Regular ,
10- Wrench20Regular
10+ Wrench20Regular ,
11+ DocumentData20Regular ,
12+ ChartMultiple20Regular ,
13+ Bot20Regular ,
14+ DataUsage20Regular ,
15+ TableSimple20Regular ,
16+ DataTrending20Regular , // Replace Analytics20Regular with DataTrending20Regular
17+ Settings20Regular ,
18+ Brain20Regular ,
19+ Target20Regular ,
20+ Flash20Regular ,
21+ Shield20Regular
1122} from '@fluentui/react-icons' ;
1223import { TeamService } from '@/services/TeamService' ;
1324import { TaskService } from '@/services' ;
1425import { iconMap } from '@/models/homeInput' ;
1526
27+ // Extended icon mapping for user-uploaded string icons
28+ const fluentIconMap : Record < string , React . ComponentType < any > > = {
29+ 'Desktop20Regular' : Desktop20Regular ,
30+ 'Code20Regular' : Code20Regular ,
31+ 'Building20Regular' : Building20Regular ,
32+ 'Organization20Regular' : Organization20Regular ,
33+ 'Search20Regular' : Search20Regular ,
34+ 'Globe20Regular' : Globe20Regular ,
35+ 'Database20Regular' : Database20Regular ,
36+ 'Wrench20Regular' : Wrench20Regular ,
37+ 'DocumentData20Regular' : DocumentData20Regular ,
38+ 'ChartMultiple20Regular' : ChartMultiple20Regular ,
39+ 'Bot20Regular' : Bot20Regular ,
40+ 'DataUsage20Regular' : DataUsage20Regular ,
41+ 'TableSimple20Regular' : TableSimple20Regular ,
42+ 'DataTrending20Regular' : DataTrending20Regular , // Updated
43+ 'Analytics20Regular' : DataTrending20Regular , // Keep Analytics as alias
44+ 'Settings20Regular' : Settings20Regular ,
45+ 'Brain20Regular' : Brain20Regular ,
46+ 'Target20Regular' : Target20Regular ,
47+ 'Flash20Regular' : Flash20Regular ,
48+ 'Shield20Regular' : Shield20Regular ,
49+ // Add common variations and aliases
50+ 'desktop' : Desktop20Regular ,
51+ 'code' : Code20Regular ,
52+ 'building' : Building20Regular ,
53+ 'organization' : Organization20Regular ,
54+ 'search' : Search20Regular ,
55+ 'globe' : Globe20Regular ,
56+ 'database' : Database20Regular ,
57+ 'wrench' : Wrench20Regular ,
58+ 'document' : DocumentData20Regular ,
59+ 'chart' : ChartMultiple20Regular ,
60+ 'bot' : Bot20Regular ,
61+ 'data' : DataUsage20Regular ,
62+ 'table' : TableSimple20Regular ,
63+ 'analytics' : DataTrending20Regular ,
64+ 'trending' : DataTrending20Regular ,
65+ 'settings' : Settings20Regular ,
66+ 'brain' : Brain20Regular ,
67+ 'target' : Target20Regular ,
68+ 'flash' : Flash20Regular ,
69+ 'shield' : Shield20Regular
70+ } ;
71+
72+ // Icon pool for unique assignment (excluding Person20Regular)
73+ const AGENT_ICON_POOL = [
74+ Bot20Regular ,
75+ DataTrending20Regular , // Updated
76+ TableSimple20Regular ,
77+ ChartMultiple20Regular ,
78+ DataUsage20Regular ,
79+ DocumentData20Regular ,
80+ Settings20Regular ,
81+ Brain20Regular ,
82+ Target20Regular ,
83+ Flash20Regular ,
84+ Shield20Regular ,
85+ Code20Regular ,
86+ Search20Regular ,
87+ Globe20Regular ,
88+ Building20Regular ,
89+ Organization20Regular ,
90+ Wrench20Regular ,
91+ Database20Regular ,
92+ Desktop20Regular
93+ ] ;
94+
95+ // Cache for agent icon assignments to ensure consistency
96+ const agentIconAssignments : Record < string , React . ComponentType < any > > = { } ;
97+
98+ /**
99+ * Generate a consistent hash from a string for icon assignment
100+ */
101+ const generateHash = ( str : string ) : number => {
102+ let hash = 0 ;
103+ for ( let i = 0 ; i < str . length ; i ++ ) {
104+ const char = str . charCodeAt ( i ) ;
105+ hash = ( ( hash << 5 ) - hash ) + char ;
106+ hash = hash & hash ; // Convert to 32bit integer
107+ }
108+ return Math . abs ( hash ) ;
109+ } ;
110+
111+ /**
112+ * Match user-uploaded string icon to Fluent UI component
113+ */
114+ const matchStringToFluentIcon = ( iconString : string ) : React . ComponentType < any > | null => {
115+ if ( ! iconString || typeof iconString !== 'string' ) return null ;
116+
117+ // Try exact match first
118+ if ( fluentIconMap [ iconString ] ) {
119+ return fluentIconMap [ iconString ] ;
120+ }
121+
122+ // Try case-insensitive match
123+ const lowerIconString = iconString . toLowerCase ( ) ;
124+ if ( fluentIconMap [ lowerIconString ] ) {
125+ return fluentIconMap [ lowerIconString ] ;
126+ }
127+
128+ // Try removing common suffixes and prefixes
129+ const cleanedIconString = iconString
130+ . replace ( / 2 0 R e g u l a r $ / i, '' )
131+ . replace ( / R e g u l a r $ / i, '' )
132+ . replace ( / 2 0 $ / i, '' )
133+ . replace ( / ^ f l u e n t / i, '' )
134+ . replace ( / ^ i c o n / i, '' )
135+ . toLowerCase ( )
136+ . trim ( ) ;
137+
138+ if ( fluentIconMap [ cleanedIconString ] ) {
139+ return fluentIconMap [ cleanedIconString ] ;
140+ }
141+
142+ return null ;
143+ } ;
144+
145+ /**
146+ * Get deterministic icon for agent based on name pattern matching
147+ * This ensures agents with the same name always get the same icon
148+ */
149+ const getDeterministicAgentIcon = ( cleanName : string ) : React . ComponentType < any > => {
150+ // Pattern-based assignment - deterministic based on agent name
151+ if ( cleanName . includes ( 'data' ) && cleanName . includes ( 'order' ) ) {
152+ return TableSimple20Regular ;
153+ } else if ( cleanName . includes ( 'data' ) && cleanName . includes ( 'customer' ) ) {
154+ return DataUsage20Regular ;
155+ } else if ( cleanName . includes ( 'analysis' ) || cleanName . includes ( 'recommend' ) || cleanName . includes ( 'insight' ) ) {
156+ return DataTrending20Regular ;
157+ } else if ( cleanName . includes ( 'proxy' ) || cleanName . includes ( 'interface' ) ) {
158+ return Bot20Regular ;
159+ } else if ( cleanName . includes ( 'brain' ) || cleanName . includes ( 'ai' ) || cleanName . includes ( 'intelligence' ) ) {
160+ return Brain20Regular ;
161+ } else if ( cleanName . includes ( 'security' ) || cleanName . includes ( 'protect' ) || cleanName . includes ( 'guard' ) ) {
162+ return Shield20Regular ;
163+ } else if ( cleanName . includes ( 'target' ) || cleanName . includes ( 'goal' ) || cleanName . includes ( 'objective' ) ) {
164+ return Target20Regular ;
165+ } else if ( cleanName . includes ( 'fast' ) || cleanName . includes ( 'quick' ) || cleanName . includes ( 'speed' ) ) {
166+ return Flash20Regular ;
167+ } else if ( cleanName . includes ( 'code' ) || cleanName . includes ( 'dev' ) || cleanName . includes ( 'program' ) ) {
168+ return Code20Regular ;
169+ } else if ( cleanName . includes ( 'search' ) || cleanName . includes ( 'find' ) || cleanName . includes ( 'lookup' ) ) {
170+ return Search20Regular ;
171+ } else if ( cleanName . includes ( 'web' ) || cleanName . includes ( 'internet' ) || cleanName . includes ( 'online' ) ) {
172+ return Globe20Regular ;
173+ } else if ( cleanName . includes ( 'business' ) || cleanName . includes ( 'company' ) || cleanName . includes ( 'enterprise' ) ) {
174+ return Building20Regular ;
175+ } else if ( cleanName . includes ( 'hr' ) || cleanName . includes ( 'human' ) || cleanName . includes ( 'people' ) ) {
176+ return Organization20Regular ;
177+ } else if ( cleanName . includes ( 'tool' ) || cleanName . includes ( 'utility' ) || cleanName . includes ( 'helper' ) ) {
178+ return Wrench20Regular ;
179+ } else if ( cleanName . includes ( 'document' ) || cleanName . includes ( 'file' ) || cleanName . includes ( 'report' ) ) {
180+ return DocumentData20Regular ;
181+ } else if ( cleanName . includes ( 'config' ) || cleanName . includes ( 'setting' ) || cleanName . includes ( 'manage' ) ) {
182+ return Settings20Regular ;
183+ } else if ( cleanName . includes ( 'data' ) || cleanName . includes ( 'database' ) ) {
184+ return Database20Regular ;
185+ } else {
186+ // Use hash-based assignment for consistent selection across identical names
187+ const hash = generateHash ( cleanName ) ;
188+ const iconIndex = hash % AGENT_ICON_POOL . length ;
189+ return AGENT_ICON_POOL [ iconIndex ] ;
190+ }
191+ } ;
192+
193+ /**
194+ * Get unique icon for an agent based on their name and context
195+ * Ensures agents with identical names get identical icons
196+ */
197+ const getUniqueAgentIcon = (
198+ agentName : string ,
199+ allAgentNames : string [ ] ,
200+ iconStyle : React . CSSProperties
201+ ) : React . ReactNode => {
202+ const cleanName = TaskService . cleanTextToSpaces ( agentName ) . toLowerCase ( ) ;
203+
204+ // If we already assigned an icon to this agent, use it
205+ if ( agentIconAssignments [ cleanName ] ) {
206+ const IconComponent = agentIconAssignments [ cleanName ] ;
207+ return React . createElement ( IconComponent , { style : iconStyle } ) ;
208+ }
209+
210+ // Get deterministic icon based on agent name patterns
211+ // This ensures same names always get the same icon regardless of assignment order
212+ const selectedIcon = getDeterministicAgentIcon ( cleanName ) ;
213+
214+ // Cache the assignment for future lookups
215+ agentIconAssignments [ cleanName ] = selectedIcon ;
216+
217+ return React . createElement ( selectedIcon , { style : iconStyle } ) ;
218+ } ;
219+
16220/**
17221 * Comprehensive utility function to get agent icon from multiple data sources
18222 * with consistent fallback patterns across all components
@@ -25,7 +229,33 @@ export const getAgentIcon = (
25229) : React . ReactNode => {
26230 const iconStyle = { fontSize : '16px' , color : iconColor } ;
27231
28- // 1. First priority: Get from stored team configuration (TeamService)
232+ // 1. First priority: Get from uploaded team configuration in planData
233+ if ( planData ?. team ?. agents ) {
234+ const cleanAgentName = TaskService . cleanTextToSpaces ( agentName ) ;
235+
236+ const agent = planData . team . agents . find ( ( a : any ) =>
237+ TaskService . cleanTextToSpaces ( a . name || '' ) . toLowerCase ( ) . includes ( cleanAgentName . toLowerCase ( ) ) ||
238+ TaskService . cleanTextToSpaces ( a . type || '' ) . toLowerCase ( ) . includes ( cleanAgentName . toLowerCase ( ) ) ||
239+ TaskService . cleanTextToSpaces ( a . input_key || '' ) . toLowerCase ( ) . includes ( cleanAgentName . toLowerCase ( ) )
240+ ) ;
241+
242+ if ( agent ?. icon ) {
243+ // Try to match string to Fluent icon component first
244+ const FluentIconComponent = matchStringToFluentIcon ( agent . icon ) ;
245+ if ( FluentIconComponent ) {
246+ return React . createElement ( FluentIconComponent , { style : iconStyle } ) ;
247+ }
248+
249+ // Fallback: check if it's in the existing iconMap
250+ if ( iconMap [ agent . icon ] ) {
251+ return React . cloneElement ( iconMap [ agent . icon ] as React . ReactElement , {
252+ style : iconStyle
253+ } ) ;
254+ }
255+ }
256+ }
257+
258+ // 2. Second priority: Get from stored team configuration (TeamService)
29259 const storedTeam = TeamService . getStoredTeam ( ) ;
30260 if ( storedTeam ?. agents ) {
31261 const cleanAgentName = TaskService . cleanTextToSpaces ( agentName ) ;
@@ -43,7 +273,7 @@ export const getAgentIcon = (
43273 }
44274 }
45275
46- // 2. Second priority: Get from participant_descriptions in planApprovalRequest
276+ // 3. Third priority: Get from participant_descriptions in planApprovalRequest
47277 if ( planApprovalRequest ?. context ?. participant_descriptions ) {
48278 const participantDesc = planApprovalRequest . context . participant_descriptions [ agentName ] ;
49279 if ( participantDesc ?. icon && iconMap [ participantDesc . icon ] ) {
@@ -53,44 +283,28 @@ export const getAgentIcon = (
53283 }
54284 }
55285
56- // 3. Third priority: Get from planData team configuration
57- if ( planData ?. team ?. agents ) {
58- const cleanAgentName = TaskService . cleanTextToSpaces ( agentName ) ;
59-
60- const agent = planData . team . agents . find ( ( a : any ) =>
61- TaskService . cleanTextToSpaces ( a . name ) . toLowerCase ( ) . includes ( cleanAgentName . toLowerCase ( ) ) ||
62- a . type . toLowerCase ( ) . includes ( cleanAgentName . toLowerCase ( ) ) ||
63- a . input_key . toLowerCase ( ) . includes ( cleanAgentName . toLowerCase ( ) )
64- ) ;
65-
66- if ( agent ?. icon && iconMap [ agent . icon ] ) {
67- return React . cloneElement ( iconMap [ agent . icon ] as React . ReactElement , {
68- style : iconStyle
69- } ) ;
70- }
71- }
72-
73- // 4. Fallback: Pattern-based icon assignment based on agent name
74- const cleanName = agentName . toLowerCase ( ) ;
75-
76- if ( cleanName . includes ( 'coder' ) || cleanName . includes ( 'coding' ) || cleanName . includes ( 'executor' ) ) {
77- return < Code20Regular style = { iconStyle } /> ;
78- } else if ( cleanName . includes ( 'research' ) || cleanName . includes ( 'data' ) || cleanName . includes ( 'analyst' ) ) {
79- return < Database20Regular style = { iconStyle } /> ;
80- } else if ( cleanName . includes ( 'websurfer' ) || cleanName . includes ( 'web' ) || cleanName . includes ( 'browser' ) ) {
81- return < Globe20Regular style = { iconStyle } /> ;
82- } else if ( cleanName . includes ( 'search' ) || cleanName . includes ( 'kb' ) || cleanName . includes ( 'knowledge' ) ) {
83- return < Search20Regular style = { iconStyle } /> ;
84- } else if ( cleanName . includes ( 'hr' ) || cleanName . includes ( 'human' ) || cleanName . includes ( 'manager' ) ) {
85- return < Organization20Regular style = { iconStyle } /> ;
86- } else if ( cleanName . includes ( 'marketing' ) || cleanName . includes ( 'business' ) || cleanName . includes ( 'sales' ) ) {
87- return < Building20Regular style = { iconStyle } /> ;
88- } else if ( cleanName . includes ( 'custom' ) || cleanName . includes ( 'tool' ) || cleanName . includes ( 'utility' ) ) {
89- return < Wrench20Regular style = { iconStyle } /> ;
286+ // 4. Deterministic icon assignment - ensures same names get same icons
287+ // Get all agent names from current context for unique assignment
288+ let allAgentNames : string [ ] = [ ] ;
289+
290+ if ( planApprovalRequest ?. team ) {
291+ allAgentNames = planApprovalRequest . team ;
292+ } else if ( planData ?. team ?. agents ) {
293+ allAgentNames = planData . team . agents . map ( ( a : any ) => a . name || a . type || '' ) ;
294+ } else if ( storedTeam ?. agents ) {
295+ allAgentNames = storedTeam . agents . map ( a => a . name ) ;
90296 }
297+
298+ return getUniqueAgentIcon ( agentName , allAgentNames , iconStyle ) ;
299+ } ;
91300
92- // 5. Final fallback: Desktop icon for AI agents
93- return < Desktop20Regular style = { iconStyle } /> ;
301+ /**
302+ * Clear agent icon assignments (useful when switching teams/contexts)
303+ */
304+ export const clearAgentIconAssignments = ( ) : void => {
305+ Object . keys ( agentIconAssignments ) . forEach ( key => {
306+ delete agentIconAssignments [ key ] ;
307+ } ) ;
94308} ;
95309
96310/**
0 commit comments