feat: add agent icons with picker and collapsible sidebar section
- Add `icon` text column to agents DB schema with migration - Add icon field to shared Agent type and validators - Create AgentIconPicker component with 40+ curated lucide icons and search - Show clickable icon next to agent name on detail page header - Replace static Agents nav item with collapsible AGENTS section in sidebar - Each agent shows its icon (defaulting to Bot) with truncated name Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
166
ui/src/components/AgentIconPicker.tsx
Normal file
166
ui/src/components/AgentIconPicker.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
import { useState, useMemo } from "react";
|
||||
import {
|
||||
Bot,
|
||||
Cpu,
|
||||
Brain,
|
||||
Zap,
|
||||
Rocket,
|
||||
Code,
|
||||
Terminal,
|
||||
Shield,
|
||||
Eye,
|
||||
Search,
|
||||
Wrench,
|
||||
Hammer,
|
||||
Lightbulb,
|
||||
Sparkles,
|
||||
Star,
|
||||
Heart,
|
||||
Flame,
|
||||
Bug,
|
||||
Cog,
|
||||
Database,
|
||||
Globe,
|
||||
Lock,
|
||||
Mail,
|
||||
MessageSquare,
|
||||
FileCode,
|
||||
GitBranch,
|
||||
Package,
|
||||
Puzzle,
|
||||
Target,
|
||||
Wand2,
|
||||
Atom,
|
||||
CircuitBoard,
|
||||
Radar,
|
||||
Swords,
|
||||
Telescope,
|
||||
Microscope,
|
||||
Crown,
|
||||
Gem,
|
||||
Hexagon,
|
||||
Pentagon,
|
||||
Fingerprint,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const AGENT_ICONS: Record<string, LucideIcon> = {
|
||||
bot: Bot,
|
||||
cpu: Cpu,
|
||||
brain: Brain,
|
||||
zap: Zap,
|
||||
rocket: Rocket,
|
||||
code: Code,
|
||||
terminal: Terminal,
|
||||
shield: Shield,
|
||||
eye: Eye,
|
||||
search: Search,
|
||||
wrench: Wrench,
|
||||
hammer: Hammer,
|
||||
lightbulb: Lightbulb,
|
||||
sparkles: Sparkles,
|
||||
star: Star,
|
||||
heart: Heart,
|
||||
flame: Flame,
|
||||
bug: Bug,
|
||||
cog: Cog,
|
||||
database: Database,
|
||||
globe: Globe,
|
||||
lock: Lock,
|
||||
mail: Mail,
|
||||
"message-square": MessageSquare,
|
||||
"file-code": FileCode,
|
||||
"git-branch": GitBranch,
|
||||
package: Package,
|
||||
puzzle: Puzzle,
|
||||
target: Target,
|
||||
wand: Wand2,
|
||||
atom: Atom,
|
||||
"circuit-board": CircuitBoard,
|
||||
radar: Radar,
|
||||
swords: Swords,
|
||||
telescope: Telescope,
|
||||
microscope: Microscope,
|
||||
crown: Crown,
|
||||
gem: Gem,
|
||||
hexagon: Hexagon,
|
||||
pentagon: Pentagon,
|
||||
fingerprint: Fingerprint,
|
||||
};
|
||||
|
||||
const DEFAULT_ICON = "bot";
|
||||
|
||||
export function getAgentIcon(iconName: string | null | undefined): LucideIcon {
|
||||
return AGENT_ICONS[iconName ?? DEFAULT_ICON] ?? AGENT_ICONS[DEFAULT_ICON];
|
||||
}
|
||||
|
||||
interface AgentIconProps {
|
||||
icon: string | null | undefined;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function AgentIcon({ icon, className }: AgentIconProps) {
|
||||
const Icon = getAgentIcon(icon);
|
||||
return <Icon className={className} />;
|
||||
}
|
||||
|
||||
interface AgentIconPickerProps {
|
||||
value: string | null | undefined;
|
||||
onChange: (icon: string) => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function AgentIconPicker({ value, onChange, children }: AgentIconPickerProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
if (!search) return Object.entries(AGENT_ICONS);
|
||||
const q = search.toLowerCase();
|
||||
return Object.entries(AGENT_ICONS).filter(([name]) => name.includes(q));
|
||||
}, [search]);
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>{children}</PopoverTrigger>
|
||||
<PopoverContent className="w-72 p-3" align="start">
|
||||
<Input
|
||||
placeholder="Search icons..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="mb-2 h-8 text-sm"
|
||||
autoFocus
|
||||
/>
|
||||
<div className="grid grid-cols-7 gap-1 max-h-48 overflow-y-auto">
|
||||
{filtered.map(([name, Icon]) => (
|
||||
<button
|
||||
key={name}
|
||||
onClick={() => {
|
||||
onChange(name);
|
||||
setOpen(false);
|
||||
setSearch("");
|
||||
}}
|
||||
className={cn(
|
||||
"flex items-center justify-center h-8 w-8 rounded hover:bg-accent transition-colors",
|
||||
(value ?? DEFAULT_ICON) === name && "bg-accent ring-1 ring-primary"
|
||||
)}
|
||||
title={name}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
</button>
|
||||
))}
|
||||
{filtered.length === 0 && (
|
||||
<p className="col-span-7 text-xs text-muted-foreground text-center py-2">No icons match</p>
|
||||
)}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user