Skip to content

Commit 7e45792

Browse files
feat: add centralized agent icon system and fix agent name formatting
1 parent f127b69 commit 7e45792

File tree

1 file changed

+253
-39
lines changed

1 file changed

+253
-39
lines changed

src/frontend/src/utils/agentIconUtils.tsx

Lines changed: 253 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -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';
1223
import { TeamService } from '@/services/TeamService';
1324
import { TaskService } from '@/services';
1425
import { 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(/20Regular$/i, '')
131+
.replace(/Regular$/i, '')
132+
.replace(/20$/i, '')
133+
.replace(/^fluent/i, '')
134+
.replace(/^icon/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

Comments
 (0)