Agent Avatar
5 by 5 pixel identicon generated from an agent id, tinted with a chart color.
import { AgentAvatar } from "@/components/ai/agent-avatar";
const agents = ["Keeper", "Writer", "Analyst", "Engineer", "Designer"];
export default function Demo() { return ( <div className="flex flex-col gap-6"> <div className="flex items-center gap-5"> {agents.map((name) => ( <div key={name} className="flex flex-col items-center gap-1.5"> <AgentAvatar seed={name.toLowerCase()} label={name} /> <span className="text-xs text-muted-foreground">{name}</span> </div> ))} </div> <div className="flex items-center gap-3.5"> <AgentAvatar seed="keeper" size={20} /> <AgentAvatar seed="keeper" size={28} /> <AgentAvatar seed="keeper" size={40} /> <AgentAvatar seed="keeper" size={56} /> <AgentAvatar seed="keeper" size={28} tile={false} color="chart-2" /> </div> </div> );}<script setup lang="ts">import { AgentAvatar } from "@/components/ai/agent-avatar";
const agents = ["Keeper", "Writer", "Analyst", "Engineer", "Designer"];</script>
<template> <div class="flex flex-col gap-6"> <div class="flex items-center gap-5"> <div v-for="name in agents" :key="name" class="flex flex-col items-center gap-1.5"> <AgentAvatar :seed="name.toLowerCase()" :label="name" /> <span class="text-xs text-muted-foreground">{{ name }}</span> </div> </div> <div class="flex items-center gap-3.5"> <AgentAvatar seed="keeper" :size="20" /> <AgentAvatar seed="keeper" :size="28" /> <AgentAvatar seed="keeper" :size="40" /> <AgentAvatar seed="keeper" :size="56" /> <AgentAvatar seed="keeper" :size="28" :tile="false" color="chart-2" /> </div> </div></template><script lang="ts"> import { AgentAvatar } from "$lib/components/ai/agent-avatar";
const agents = ["Keeper", "Writer", "Analyst", "Engineer", "Designer"];</script>
<div class="flex flex-col gap-6"> <div class="flex items-center gap-5"> {#each agents as name (name)} <div class="flex flex-col items-center gap-1.5"> <AgentAvatar seed={name.toLowerCase()} label={name} /> <span class="text-xs text-muted-foreground">{name}</span> </div> {/each} </div> <div class="flex items-center gap-3.5"> <AgentAvatar seed="keeper" size={20} /> <AgentAvatar seed="keeper" size={28} /> <AgentAvatar seed="keeper" size={40} /> <AgentAvatar seed="keeper" size={56} /> <AgentAvatar seed="keeper" size={28} tile={false} color="chart-2" /> </div></div>Edmi ✦ addition. Built on plain SVG; no other Edmi items.
Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/ai-agent-avatarpnpm dlx shadcn@latest add @edmi-ui/ai-agent-avataryarn dlx shadcn@latest add @edmi-ui/ai-agent-avatarbunx shadcn@latest add @edmi-ui/ai-agent-avatarPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-vue@latest add @edmi-ui/ai-agent-avatarpnpm dlx shadcn-vue@latest add @edmi-ui/ai-agent-avataryarn dlx shadcn-vue@latest add @edmi-ui/ai-agent-avatarbunx shadcn-vue@latest add @edmi-ui/ai-agent-avatarPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-agent-avatar.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-agent-avatar.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-agent-avatar.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-agent-avatar.jsonPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Copy the code from Component source into your project and update the import paths to match your aliases.
import { AgentAvatar } from "@/components/ai/agent-avatar";
<div className="flex flex-col gap-6"> <div className="flex items-center gap-5"> </div> <div className="flex items-center gap-3.5"> <AgentAvatar seed="keeper" size={20} /> <AgentAvatar seed="keeper" size={28} /> <AgentAvatar seed="keeper" size={40} /> <AgentAvatar seed="keeper" size={56} /> <AgentAvatar seed="keeper" size={28} tile={false} color="chart-2" /> </div></div><script setup lang="ts">import { AgentAvatar } from "@/components/ai/agent-avatar";</script>
<template> <div class="flex flex-col gap-6"> <div class="flex items-center gap-5"> <div :key="name" class="flex flex-col items-center gap-1.5"> <AgentAvatar :seed="name.toLowerCase()" :label="name" /> <span class="text-xs text-muted-foreground">{{ name }}</span> </div> </div> <div class="flex items-center gap-3.5"> <AgentAvatar seed="keeper" :size="20" /> <AgentAvatar seed="keeper" :size="28" /> <AgentAvatar seed="keeper" :size="40" /> <AgentAvatar seed="keeper" :size="56" /> <AgentAvatar seed="keeper" :size="28" :tile="false" color="chart-2" /> </div> </div></template><script lang="ts"> import { AgentAvatar } from "$lib/components/ai/agent-avatar";</script>
<div class="flex flex-col gap-6"> <div class="flex items-center gap-5"> {#each agents as name (name)} <div class="flex flex-col items-center gap-1.5"> <AgentAvatar seed={name.toLowerCase()} label={name} /> <span class="text-xs text-muted-foreground">{name}</span> </div> {/each} </div> <div class="flex items-center gap-3.5"> <AgentAvatar seed="keeper" size={20} /> <AgentAvatar seed="keeper" size={28} /> <AgentAvatar seed="keeper" size={40} /> <AgentAvatar seed="keeper" size={56} /> <AgentAvatar seed="keeper" size={28} tile={false} color="chart-2" /> </div></div>Component source
Section titled “Component source”"use client";
import { cn } from "cn";import type { ComponentProps } from "react";import { useMemo } from "react";
// FNV-1a hash + xorshift32: the same seed gives the same mark in every framework port.function seedRandom(seed: string) { let h = 2166136261; for (let i = 0; i < seed.length; i++) { h ^= seed.charCodeAt(i); h = Math.imul(h, 16777619); } let s = h >>> 0 || 1; return () => { s ^= s << 13; s >>>= 0; s ^= s >>> 17; s ^= s << 5; s >>>= 0; return s; };}
const OPACITIES = [0.45, 0.58, 0.71, 0.84, 0.97];
export type AgentAvatarCell = { x: number; y: number; opacity: number };
/** 5x5 mirrored grid: three columns are drawn, the outer two mirror them. */export function agentAvatarCells(seed: string): { cells: AgentAvatarCell[]; chart: number;} { const next = seedRandom(seed); const chart = (next() % 5) + 1; const cells: AgentAvatarCell[] = []; for (let y = 0; y < 5; y++) { for (let x = 0; x < 3; x++) { const v = next() % 8; if (v < 3) continue; const opacity = OPACITIES[v - 3] ?? 0.97; cells.push({ x, y, opacity }); if (x < 2) cells.push({ x: 4 - x, y, opacity }); } } return { cells, chart };}
export type AgentAvatarProps = Omit<ComponentProps<"svg">, "color"> & { /** Agent id: the same seed always draws the same mark. */ seed: string; /** `chart-1` to `chart-5`, or any CSS color. Defaults to a chart token picked from the seed. */ color?: string; /** Pixel size of the square. */ size?: number; /** Muted rounded tile behind the mark (default). `false` draws the bare mark. */ tile?: boolean; /** Accessible name; the mark is decorative when omitted. */ label?: string;};
export function AgentAvatar({ seed, color, size = 40, tile = true, label, className, ...props}: AgentAvatarProps) { const { cells, chart } = useMemo(() => agentAvatarCells(seed), [seed]); const fill = color ? /^chart-[1-5]$/.test(color) ? `var(--${color})` : color : `var(--chart-${chart})`; const inner = tile ? size * 0.62 : size; const offset = tile ? size * 0.19 : 0; const cell = inner * 0.17; const step = (inner - cell) / 4; return ( <svg data-slot="ai-agent-avatar" width={size} height={size} viewBox={`0 0 ${size} ${size}`} role={label ? "img" : undefined} aria-label={label} aria-hidden={label ? undefined : true} className={cn("shrink-0", className)} {...props} > {tile && ( <rect x={0.5} y={0.5} width={size - 1} height={size - 1} rx={size * 0.28} fill="var(--muted)" stroke="var(--border)" /> )} {cells.map((c) => ( <rect key={`${c.x}-${c.y}`} x={offset + c.x * step} y={offset + c.y * step} width={cell} height={cell} rx={cell * 0.17} fill={fill} opacity={c.opacity} /> ))} </svg> );}<script setup lang="ts">import type { HTMLAttributes } from "vue"import { computed } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { agentAvatarCells } from "./identicon"
const props = withDefaults(defineProps<{ class?: HTMLAttributes["class"] /** Agent id: the same seed always draws the same mark. */ seed: string /** `chart-1` to `chart-5`, or any CSS color. Defaults to a chart token picked from the seed. */ color?: string /** Pixel size of the square. */ size?: number /** Muted rounded tile behind the mark (default). `false` draws the bare mark. */ tile?: boolean /** Accessible name; the mark is decorative when omitted. */ label?: string}>(), { size: 40, tile: true,})
const data = computed(() => agentAvatarCells(props.seed))const fill = computed(() => { if (props.color) return /^chart-[1-5]$/.test(props.color) ? `var(--${props.color})` : props.color return `var(--chart-${data.value.chart})`})const inner = computed(() => (props.tile ? props.size * 0.62 : props.size))const offset = computed(() => (props.tile ? props.size * 0.19 : 0))const cell = computed(() => inner.value * 0.17)const step = computed(() => (inner.value - cell.value) / 4)</script>
<template> <svg data-slot="ai-agent-avatar" :width="size" :height="size" :viewBox="`0 0 ${size} ${size}`" :role="label ? 'img' : undefined" :aria-label="label" :aria-hidden="label ? undefined : true" :class="cn('shrink-0', props.class)" > <rect v-if="tile" :x="0.5" :y="0.5" :width="size - 1" :height="size - 1" :rx="size * 0.28" fill="var(--muted)" stroke="var(--border)" /> <rect v-for="c in data.cells" :key="`${c.x}-${c.y}`" :x="offset + c.x * step" :y="offset + c.y * step" :width="cell" :height="cell" :rx="cell * 0.17" :fill="fill" :opacity="c.opacity" /> </svg></template>// FNV-1a hash + xorshift32: the same seed gives the same mark in every framework port.function seedRandom(seed: string) { let h = 2166136261; for (let i = 0; i < seed.length; i++) { h ^= seed.charCodeAt(i); h = Math.imul(h, 16777619); } let s = h >>> 0 || 1; return () => { s ^= s << 13; s >>>= 0; s ^= s >>> 17; s ^= s << 5; s >>>= 0; return s; };}
const OPACITIES = [0.45, 0.58, 0.71, 0.84, 0.97];
export type AgentAvatarCell = { x: number; y: number; opacity: number };
/** 5x5 mirrored grid: three columns are drawn, the outer two mirror them. */export function agentAvatarCells(seed: string): { cells: AgentAvatarCell[]; chart: number;} { const next = seedRandom(seed); const chart = (next() % 5) + 1; const cells: AgentAvatarCell[] = []; for (let y = 0; y < 5; y++) { for (let x = 0; x < 3; x++) { const v = next() % 8; if (v < 3) continue; const opacity = OPACITIES[v - 3] ?? 0.97; cells.push({ x, y, opacity }); if (x < 2) cells.push({ x: 4 - x, y, opacity }); } } return { cells, chart };}export { default as AgentAvatar } from "./AgentAvatar.vue";export * from "./identicon";<script lang="ts"> import { cn } from "$lib/utils.js"; import type { SVGAttributes } from "svelte/elements"; import { agentAvatarCells } from "./identicon.js";
let { ref = $bindable(null), seed, color, size = 40, tile = true, label, class: className, ...restProps }: Omit<SVGAttributes<SVGSVGElement>, "color"> & { ref?: SVGSVGElement | null; /** Agent id: the same seed always draws the same mark. */ seed: string; /** `chart-1` to `chart-5`, or any CSS color. Defaults to a chart token picked from the seed. */ color?: string; /** Pixel size of the square. */ size?: number; /** Muted rounded tile behind the mark (default). `false` draws the bare mark. */ tile?: boolean; /** Accessible name; the mark is decorative when omitted. */ label?: string; } = $props();
const data = $derived(agentAvatarCells(seed)); const fill = $derived( color ? /^chart-[1-5]$/.test(color) ? `var(--${color})` : color : `var(--chart-${data.chart})` ); const inner = $derived(tile ? size * 0.62 : size); const offset = $derived(tile ? size * 0.19 : 0); const cell = $derived(inner * 0.17); const step = $derived((inner - cell) / 4);</script>
<svg bind:this={ref} data-slot="ai-agent-avatar" width={size} height={size} viewBox="0 0 {size} {size}" role={label ? "img" : undefined} aria-label={label} aria-hidden={label ? undefined : true} class={cn("shrink-0", className)} {...restProps}> {#if tile} <rect x={0.5} y={0.5} width={size - 1} height={size - 1} rx={size * 0.28} fill="var(--muted)" stroke="var(--border)" /> {/if} {#each data.cells as c (`${c.x}-${c.y}`)} <rect x={offset + c.x * step} y={offset + c.y * step} width={cell} height={cell} rx={cell * 0.17} {fill} opacity={c.opacity} /> {/each}</svg>// FNV-1a hash + xorshift32: the same seed gives the same mark in every framework port.function seedRandom(seed: string) { let h = 2166136261; for (let i = 0; i < seed.length; i++) { h ^= seed.charCodeAt(i); h = Math.imul(h, 16777619); } let s = h >>> 0 || 1; return () => { s ^= s << 13; s >>>= 0; s ^= s >>> 17; s ^= s << 5; s >>>= 0; return s; };}
const OPACITIES = [0.45, 0.58, 0.71, 0.84, 0.97];
export type AgentAvatarCell = { x: number; y: number; opacity: number };
/** 5x5 mirrored grid: three columns are drawn, the outer two mirror them. */export function agentAvatarCells(seed: string): { cells: AgentAvatarCell[]; chart: number;} { const next = seedRandom(seed); const chart = (next() % 5) + 1; const cells: AgentAvatarCell[] = []; for (let y = 0; y < 5; y++) { for (let x = 0; x < 3; x++) { const v = next() % 8; if (v < 3) continue; const opacity = OPACITIES[v - 3] ?? 0.97; cells.push({ x, y, opacity }); if (x < 2) cells.push({ x: 4 - x, y, opacity }); } } return { cells, chart };}import AgentAvatar from "./agent-avatar.svelte";
export * from "./identicon.js";export { AgentAvatar };| Part | Prop | Type | Default |
|---|---|---|---|
| AgentAvatar | seed |
string (agent id: same seed, same mark) |
|
| AgentAvatar | color |
chart-1 to chart-5 or any CSS color |
picked from the seed |
| AgentAvatar | size |
number (px) |
40 |
| AgentAvatar | tile |
boolean (muted rounded square behind the mark) |
true |
| AgentAvatar | label |
string (accessible name; decorative when omitted) |
Elevation ✦
Section titled “Elevation ✦”Flat only. It is a decorative mark with no depth. It is not the same component as the identicon of agent-card; each keeps its own look.
✦ Edmi additions
Section titled “✦ Edmi additions”- The mark is a 3 by 5 grid mirrored to 5 by 5, so it stays recognisable at 20px. Use it only where several agents appear.