mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Fix agent avatars in cards and teams
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user