Fix agent avatars in cards and teams

This commit is contained in:
klopez4212
2026-06-22 16:01:50 +01:00
parent c5cdd4cd31
commit 9c740c0dda
4 changed files with 116 additions and 35 deletions
@@ -1,6 +1,7 @@
import type { ReactNode } from "react";
import { Link, Users } from "lucide-react";
import { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar";
import type { AgentPersona } from "@/shared/api/types";
import { Card } from "@/shared/ui/card";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
@@ -127,40 +128,83 @@ function TeamAvatarCluster({
role="img"
>
{items.map((item, index) => (
<div
data-team-cluster-item="avatar"
<TeamClusterItem
index={index}
isMasked={index < items.length - 1}
item={item}
key={item.kind === "persona" ? item.persona.id : "overflow"}
style={{
height: size,
marginLeft: index > 0 ? -overlap : 0,
width: size,
zIndex: index + 1,
...(index < items.length - 1
? {
WebkitMask: `radial-gradient(circle ${maskRadius}px at calc(100% + ${maskOffset}px) 50%, transparent 99%, #fff 100%)`,
mask: `radial-gradient(circle ${maskRadius}px at calc(100% + ${maskOffset}px) 50%, transparent 99%, #fff 100%)`,
}
: null),
}}
>
{item.kind === "persona" ? (
<IdentityInitialsAvatar
colorIndex={index}
label={item.persona.displayName}
size={size}
/>
) : (
<span className="flex h-full w-full items-center justify-center rounded-full border-[3px] border-background bg-card text-base font-semibold text-muted-foreground shadow-sm">
+{item.count}
</span>
)}
</div>
maskOffset={maskOffset}
maskRadius={maskRadius}
overlap={overlap}
size={size}
/>
))}
</div>
</div>
);
}
function TeamClusterItem({
index,
isMasked,
item,
maskOffset,
maskRadius,
overlap,
size,
}: {
index: number;
isMasked: boolean;
item: ClusterItem;
maskOffset: number;
maskRadius: number;
overlap: number;
size: number;
}) {
const avatarUrl =
item.kind === "persona" ? (item.persona.avatarUrl?.trim() ?? null) : null;
return (
<div
data-team-cluster-item="avatar"
style={{
height: size,
marginLeft: index > 0 ? -overlap : 0,
width: size,
zIndex: index + 1,
...(isMasked
? {
WebkitMask: `radial-gradient(circle ${maskRadius}px at calc(100% + ${maskOffset}px) 50%, transparent 99%, #fff 100%)`,
mask: `radial-gradient(circle ${maskRadius}px at calc(100% + ${maskOffset}px) 50%, transparent 99%, #fff 100%)`,
}
: null),
}}
>
{item.kind === "persona" ? (
avatarUrl ? (
<ProfileAvatar
avatarUrl={avatarUrl}
className="h-full w-full border-[3px] border-background bg-muted shadow-sm"
iconClassName="h-8 w-8"
label={item.persona.displayName}
testId={`team-member-avatar-${item.persona.id}`}
/>
) : (
<IdentityInitialsAvatar
colorIndex={index}
label={item.persona.displayName}
size={size}
/>
)
) : (
<span className="flex h-full w-full items-center justify-center rounded-full border-[3px] border-background bg-card text-base font-semibold text-muted-foreground shadow-sm">
+{item.count}
</span>
)}
</div>
);
}
function buildClusterItems(
personas: AgentPersona[],
memberCount: number,
@@ -252,7 +252,11 @@ function AgentPersonaCard({
const modelLabel = formatAgentModelLabel(agent?.model ?? persona.model);
const profileQuery = useUserProfileQuery(agent?.pubkey);
const avatarUrl = agent
? (profileQuery.data?.avatarUrl ?? agent.avatarUrl ?? persona.avatarUrl)
? firstAvatarUrl(
profileQuery.data?.avatarUrl,
agent.avatarUrl,
persona.avatarUrl,
)
: persona.avatarUrl;
return (
@@ -286,7 +290,7 @@ function StandaloneAgentCard({
return (
<AgentIdentityCard
ariaLabel={`${title} agent profile`}
avatarUrl={profileQuery.data?.avatarUrl ?? agent.avatarUrl}
avatarUrl={firstAvatarUrl(profileQuery.data?.avatarUrl, agent.avatarUrl)}
dataTestId={`managed-agent-${agent.pubkey}`}
label={title}
modelLabel={formatAgentModelLabel(agent.model)}
@@ -300,6 +304,16 @@ function formatAgentModelLabel(model: string | null | undefined) {
return trimmed && trimmed.length > 0 ? trimmed : "Auto";
}
function firstAvatarUrl(
...candidates: Array<string | null | undefined>
): string | null {
for (const candidate of candidates) {
const trimmed = candidate?.trim();
if (trimmed) return trimmed;
}
return null;
}
function SectionHeader({
fileInputRef,
handleFileChange,
@@ -77,6 +77,7 @@ import {
resolveProfileDisplayName,
type UserProfilePanelProps,
useRetainedPersona,
withProfileAvatarFallback,
} from "@/features/profile/ui/UserProfilePanelUtils";
import { useUserStatusQuery } from "@/features/user-status/hooks";
import { useAgentSession } from "@/shared/context/AgentSessionContext";
@@ -224,16 +225,15 @@ export function UserProfilePanel({
const unfollowMutation = useUnfollowMutation(currentPubkey);
const { onOpenAgentSession } = useAgentSession();
const { goChannel } = useAppNavigation();
const profile = React.useMemo(() => {
const baseProfile =
profileQuery.data ??
(resolvedPersona ? buildPersonaDraftProfile(resolvedPersona) : undefined);
const fallbackAvatarUrl =
managedAgent?.avatarUrl ?? resolvedPersona?.avatarUrl ?? null;
return baseProfile && !baseProfile.avatarUrl && fallbackAvatarUrl
? { ...baseProfile, avatarUrl: fallbackAvatarUrl }
: baseProfile;
return withProfileAvatarFallback(
baseProfile,
managedAgent?.avatarUrl,
resolvedPersona?.avatarUrl,
);
}, [managedAgent?.avatarUrl, profileQuery.data, resolvedPersona]);
const presenceStatus = pubkeyLower
? presenceQuery.data?.[pubkeyLower]
@@ -135,6 +135,29 @@ export function buildPersonaDraftProfile(persona: AgentPersona): Profile {
};
}
export function resolveProfileAvatarUrl(
...candidates: Array<string | null | undefined>
): string | null {
for (const candidate of candidates) {
const trimmed = candidate?.trim();
if (trimmed) return trimmed;
}
return null;
}
export function withProfileAvatarFallback(
profile: Profile | undefined,
...fallbackAvatarUrls: Array<string | null | undefined>
): Profile | undefined {
const avatarUrl = resolveProfileAvatarUrl(
profile?.avatarUrl,
...fallbackAvatarUrls,
);
return profile && avatarUrl !== profile.avatarUrl
? { ...profile, avatarUrl }
: profile;
}
export function resolveProfileDisplayName({
persona,
profile,