refactor(ui): implement agent avatar resolution and logo fallback in agent rendering
This commit is contained in:
@@ -138,6 +138,30 @@ export function normalizeAgentLabel(agent: {
|
||||
return agent.name?.trim() || agent.identity?.name?.trim() || agent.id;
|
||||
}
|
||||
|
||||
const AVATAR_URL_RE = /^(https?:\/\/|data:image\/|\/)/i;
|
||||
|
||||
export function resolveAgentAvatarUrl(
|
||||
agent: { identity?: { avatar?: string; avatarUrl?: string } },
|
||||
agentIdentity?: AgentIdentityResult | null,
|
||||
): string | null {
|
||||
const url =
|
||||
agentIdentity?.avatar?.trim() ??
|
||||
agent.identity?.avatarUrl?.trim() ??
|
||||
agent.identity?.avatar?.trim();
|
||||
if (!url) {
|
||||
return null;
|
||||
}
|
||||
if (AVATAR_URL_RE.test(url)) {
|
||||
return url;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function agentLogoUrl(basePath: string): string {
|
||||
const base = basePath?.trim() ? basePath.replace(/\/$/, "") : "";
|
||||
return base ? `${base}/favicon.svg` : "/favicon.svg";
|
||||
}
|
||||
|
||||
function isLikelyEmoji(value: string) {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
@@ -229,7 +253,7 @@ export type AgentContext = {
|
||||
workspace: string;
|
||||
model: string;
|
||||
identityName: string;
|
||||
identityEmoji: string;
|
||||
identityAvatar: string;
|
||||
skillsLabel: string;
|
||||
isDefault: boolean;
|
||||
};
|
||||
@@ -255,14 +279,14 @@ export function buildAgentContext(
|
||||
agent.name?.trim() ||
|
||||
config.entry?.name ||
|
||||
agent.id;
|
||||
const identityEmoji = resolveAgentEmoji(agent, agentIdentity) || "-";
|
||||
const identityAvatar = resolveAgentAvatarUrl(agent, agentIdentity) ? "custom" : "—";
|
||||
const skillFilter = Array.isArray(config.entry?.skills) ? config.entry?.skills : null;
|
||||
const skillCount = skillFilter?.length ?? null;
|
||||
return {
|
||||
workspace,
|
||||
model: modelLabel,
|
||||
identityName,
|
||||
identityEmoji,
|
||||
identityAvatar,
|
||||
skillsLabel: skillFilter ? `${skillCount} selected` : "all skills",
|
||||
isDefault: Boolean(defaultId && agent.id === defaultId),
|
||||
};
|
||||
|
||||
@@ -18,9 +18,10 @@ import { renderAgentTools, renderAgentSkills } from "./agents-panels-tools-skill
|
||||
import {
|
||||
agentAvatarHue,
|
||||
agentBadgeText,
|
||||
agentLogoUrl,
|
||||
buildAgentContext,
|
||||
normalizeAgentLabel,
|
||||
resolveAgentEmoji,
|
||||
resolveAgentAvatarUrl,
|
||||
} from "./agents-utils.ts";
|
||||
|
||||
export type AgentsPanel = "overview" | "files" | "tools" | "skills" | "channels" | "cron";
|
||||
@@ -65,6 +66,7 @@ export type AgentSkillsState = {
|
||||
};
|
||||
|
||||
export type AgentsProps = {
|
||||
basePath: string;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
agentsList: AgentsListResult | null;
|
||||
@@ -174,8 +176,12 @@ export function renderAgents(props: AgentsProps) {
|
||||
`
|
||||
: filteredAgents.map((agent) => {
|
||||
const badge = agentBadgeText(agent.id, defaultId);
|
||||
const emoji = resolveAgentEmoji(agent, props.agentIdentityById[agent.id] ?? null);
|
||||
const avatarUrl = resolveAgentAvatarUrl(
|
||||
agent,
|
||||
props.agentIdentityById[agent.id] ?? null,
|
||||
);
|
||||
const hue = agentAvatarHue(agent.id);
|
||||
const logoUrl = agentLogoUrl(props.basePath);
|
||||
return html`
|
||||
<button
|
||||
type="button"
|
||||
@@ -183,7 +189,11 @@ export function renderAgents(props: AgentsProps) {
|
||||
@click=${() => props.onSelectAgent(agent.id)}
|
||||
>
|
||||
<div class="agent-avatar" style="--agent-hue: ${hue}">
|
||||
${emoji || normalizeAgentLabel(agent).slice(0, 1)}
|
||||
${
|
||||
avatarUrl
|
||||
? html`<img src=${avatarUrl} alt="" class="agent-avatar__img" />`
|
||||
: html`<img src=${logoUrl} alt="" class="agent-avatar__img agent-avatar__logo" />`
|
||||
}
|
||||
</div>
|
||||
<div class="agent-info">
|
||||
<div class="agent-title">${normalizeAgentLabel(agent)}</div>
|
||||
@@ -211,6 +221,7 @@ export function renderAgents(props: AgentsProps) {
|
||||
defaultId,
|
||||
props.agentIdentityById[selectedAgent.id] ?? null,
|
||||
props.onSetDefault,
|
||||
props.basePath,
|
||||
)}
|
||||
${renderAgentTabs(props.activePanel, (panel) => props.onSelectPanel(panel), tabCounts)}
|
||||
${
|
||||
@@ -341,11 +352,12 @@ function renderAgentHeader(
|
||||
defaultId: string | null,
|
||||
agentIdentity: AgentIdentityResult | null,
|
||||
onSetDefault: (agentId: string) => void,
|
||||
basePath: string,
|
||||
) {
|
||||
const badge = agentBadgeText(agent.id, defaultId);
|
||||
const displayName = normalizeAgentLabel(agent);
|
||||
const subtitle = agent.identity?.theme?.trim() || "Agent workspace and routing.";
|
||||
const emoji = resolveAgentEmoji(agent, agentIdentity);
|
||||
const avatarUrl = resolveAgentAvatarUrl(agent, agentIdentity);
|
||||
const hue = agentAvatarHue(agent.id);
|
||||
const isDefault = Boolean(defaultId && agent.id === defaultId);
|
||||
|
||||
@@ -354,11 +366,16 @@ function renderAgentHeader(
|
||||
actionsMenuOpen = false;
|
||||
};
|
||||
|
||||
const logoUrl = agentLogoUrl(basePath);
|
||||
return html`
|
||||
<section class="card agent-header">
|
||||
<div class="agent-header-main">
|
||||
<div class="agent-avatar agent-avatar--lg" style="--agent-hue: ${hue}">
|
||||
${emoji || displayName.slice(0, 1)}
|
||||
${
|
||||
avatarUrl
|
||||
? html`<img src=${avatarUrl} alt="" class="agent-avatar__img" />`
|
||||
: html`<img src=${logoUrl} alt="" class="agent-avatar__img agent-avatar__logo" />`
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
<div class="card-title">${displayName}</div>
|
||||
|
||||
Reference in New Issue
Block a user