Files
AgentHub/client/src/components/users/UserAvatar.tsx
T
nntrivi2001 7a88b74016 feat: v2.8.0 — Artifacts uploads and plugin links, console tab combos, account avatars
- Artifacts hub: upload a file from the computer; make a skill by uploading a
  Markdown file, files, a folder or a .zip/.skill, or by writing it in the
  dialog; add plugins by picking from the list a marketplace link answers
  (POST /api/artifacts/plugin/inspect); Settings → AI tools → Artifacts Hub.
- Console tabs are combos of up to four split panes (ConsoleTabs, PaneGrid).
- Accounts: an own avatar (PUT/DELETE /api/me/avatar, raster only, ≤ 1 MB,
  <data dir>/avatars/), shown with name and role at the sidebar's foot and on
  the workspace owner groups (GET /api/users/:id/avatar, owner_avatar_at).
- Users page: per-user storage (GET /api/users/storage, measured in a worker)
  and account-pool spend/tokens per account (server/lib/pool-usage.js).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 17:21:33 +07:00

60 lines
1.9 KiB
TypeScript

/**
* @file UserAvatar.tsx
* @description An account's round picture: the signed-in account's own
* (`GET /api/me/avatar`) or, with `userId`, another's (`GET
* /api/users/:id/avatar`, staff only), versioned by `avatarAt`; without one —
* or when the image fails to load — the first letters of its name. Used by the
* sidebar's account row, its workspace owner groups and Settings → Account.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
import { useState } from "react";
import { api } from "../../lib/api";
export function UserAvatar({
name,
avatarAt,
userId,
letters = 2,
className = "h-7 w-7 text-[11px]",
initialsClassName = "bg-accent/15 text-accent",
}: {
name: string;
avatarAt?: string | null;
/** Another account's (staff only); omitted = the signed-in account's own. */
userId?: number;
/** How many letters of the name stand in for a missing picture. */
letters?: number;
/** Size and font size: the parts that differ between call sites. */
className?: string;
/** The initials' colours. */
initialsClassName?: string;
}) {
const url = userId != null ? api.users.avatarUrl(userId, avatarAt) : api.me.avatarUrl(avatarAt);
// A broken image falls back to the initials, per URL.
const [failed, setFailed] = useState<string | null>(null);
const base = `flex flex-shrink-0 items-center justify-center overflow-hidden rounded-full ${className}`;
if (url && failed !== url) {
return (
<img
src={url}
alt=""
aria-hidden
draggable={false}
onError={() => setFailed(url)}
className={`${base} object-cover`}
data-testid="user-avatar-img"
/>
);
}
return (
<span
aria-hidden
className={`${base} ${initialsClassName} font-semibold uppercase`}
data-testid="user-avatar-initials"
>
{name.slice(0, letters)}
</span>
);
}