mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 20:05:46 -06:00
fix(ui): center settings help glyph, shrink custodian thinking mascot, add user avatar error fallback (#118337)
This commit is contained in:
committed by
GitHub
parent
18c9f27e2d
commit
899be356ac
@@ -86,6 +86,22 @@ describe("renderChatAvatar", () => {
|
||||
expect(textAvatar?.tagName).toBe("DIV");
|
||||
expect(textAvatar?.textContent?.trim()).toBe("AB");
|
||||
});
|
||||
|
||||
it("swaps a failing local user image to initials instead of a broken image", () => {
|
||||
const container = document.createElement("div");
|
||||
render(
|
||||
renderChatAvatar("user", undefined, { name: "Buns", avatar: "/avatar/user" }),
|
||||
container,
|
||||
);
|
||||
const slot = container.querySelector<HTMLElement>(".chat-avatar-slot");
|
||||
const image = slot?.querySelector("img");
|
||||
expect(image?.getAttribute("src")).toBe("/avatar/user");
|
||||
expect(slot?.classList.contains("is-fallback")).toBe(false);
|
||||
|
||||
image?.dispatchEvent(new Event("error"));
|
||||
expect(slot?.classList.contains("is-fallback")).toBe(true);
|
||||
expect(slot?.querySelector(".chat-avatar--sender-initials")?.textContent?.trim()).toBe("B");
|
||||
});
|
||||
});
|
||||
|
||||
describe("refreshChatAvatar", () => {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
identityAvatarClass,
|
||||
renderIdentityAvatarImage,
|
||||
resolveIdentityAvatarView,
|
||||
type IdentityAvatarView,
|
||||
} from "../../components/identity-avatar-view.ts";
|
||||
import type { AssistantIdentity } from "../../lib/assistant-identity.ts";
|
||||
import {
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
import { normalizeRoleForGrouping } from "../../lib/chat/message-normalizer.ts";
|
||||
import type { SenderIdentity } from "../../lib/chat/sender-label.ts";
|
||||
import { formatSenderLabel } from "../../lib/chat/sender-label.ts";
|
||||
import { resolveAvatarInitials } from "../../lib/identity-avatar.ts";
|
||||
import {
|
||||
DEFAULT_AGENT_ID,
|
||||
isUiGlobalSessionKey,
|
||||
@@ -41,29 +43,7 @@ export function renderChatAvatar(
|
||||
// Attributed multi-user messages show the author's own avatar (profile
|
||||
// upload → gateway Gravatar proxy → initials), not the local viewer's.
|
||||
if (normalized === "user" && sender) {
|
||||
const label = formatSenderLabel(sender) ?? "";
|
||||
const view = resolveIdentityAvatarView(sender);
|
||||
const initialsAvatar = html`<div
|
||||
class="chat-avatar user chat-avatar--sender-initials"
|
||||
style=${`background: hsl(${view.fallback.colorSeed % 360} 48% 42%)`}
|
||||
aria-label="${label}"
|
||||
>
|
||||
${view.fallback.initials}
|
||||
</div>`;
|
||||
if (!view.imageUrl) {
|
||||
return initialsAvatar;
|
||||
}
|
||||
// The derived route may 404 (no upload, no Gravatar); swap to initials
|
||||
// instead of a broken image. Lit reuses DOM parts, so a load must clear a
|
||||
// prior sender's error state.
|
||||
return html`<span class=${identityAvatarClass("chat-avatar-slot", view)}>
|
||||
${renderIdentityAvatarImage({
|
||||
view,
|
||||
fallbackSelector: ".chat-avatar-slot",
|
||||
className: "chat-avatar user",
|
||||
alt: label,
|
||||
})}${initialsAvatar}
|
||||
</span>`;
|
||||
return renderUserAvatarSlot(resolveIdentityAvatarView(sender), formatSenderLabel(sender) ?? "");
|
||||
}
|
||||
const assistantName = assistant?.name?.trim() || "Assistant";
|
||||
const assistantAvatar = assistant?.avatar?.trim() || "";
|
||||
@@ -119,7 +99,14 @@ export function renderChatAvatar(
|
||||
: "other";
|
||||
|
||||
if (normalized === "user" && userAvatarUrl) {
|
||||
return html`<img class="chat-avatar ${className}" src="${userAvatarUrl}" alt="${userName}" />`;
|
||||
return renderUserAvatarSlot(
|
||||
{
|
||||
fallback: resolveAvatarInitials({ name: userName }),
|
||||
imageUrl: userAvatarUrl,
|
||||
pending: false,
|
||||
},
|
||||
userName,
|
||||
);
|
||||
}
|
||||
|
||||
if (normalized === "user" && userAvatarText) {
|
||||
@@ -166,6 +153,32 @@ export function renderChatAvatar(
|
||||
return html`<div class="chat-avatar ${className}">${initial}</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The avatar URL may 404 or be unreachable (missing upload, dead Gravatar,
|
||||
* stale configured URL); swap to initials instead of a broken image. Lit
|
||||
* reuses DOM parts, so a load must clear a prior identity's error state.
|
||||
*/
|
||||
function renderUserAvatarSlot(view: IdentityAvatarView, label: string) {
|
||||
const initialsAvatar = html`<div
|
||||
class="chat-avatar user chat-avatar--sender-initials"
|
||||
style=${`background: hsl(${view.fallback.colorSeed % 360} 48% 42%)`}
|
||||
aria-label="${label}"
|
||||
>
|
||||
${view.fallback.initials}
|
||||
</div>`;
|
||||
if (!view.imageUrl) {
|
||||
return initialsAvatar;
|
||||
}
|
||||
return html`<span class=${identityAvatarClass("chat-avatar-slot", view)}>
|
||||
${renderIdentityAvatarImage({
|
||||
view,
|
||||
fallbackSelector: ".chat-avatar-slot",
|
||||
className: "chat-avatar user",
|
||||
alt: label,
|
||||
})}${initialsAvatar}
|
||||
</span>`;
|
||||
}
|
||||
|
||||
function isAvatarUrl(value: string): boolean {
|
||||
const trimmed = value.trim();
|
||||
return trimmed.startsWith("blob:") || isRenderableControlUiAvatarUrl(trimmed);
|
||||
|
||||
@@ -177,7 +177,7 @@ class CustodianSurface extends OpenClawLightDomElement {
|
||||
${store.sending
|
||||
? html`<div class="chat-group assistant custodian__thinking-row" role="status">
|
||||
<div class="chat-avatar assistant custodian__mascot-avatar" aria-hidden="true">
|
||||
<openclaw-mascot mood="thinking" .size=${32}></openclaw-mascot>
|
||||
<openclaw-mascot mood="thinking" .size=${26}></openclaw-mascot>
|
||||
</div>
|
||||
<div class="chat-group-messages custodian__thinking">
|
||||
<span></span><span></span><span></span>
|
||||
|
||||
@@ -160,6 +160,13 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.settings-section__help-button > span {
|
||||
/* SF's ascent+descent exceed 1em, so a centered line box parks the glyph
|
||||
below the circle's midline; trim to cap bounds for true centering. */
|
||||
display: block;
|
||||
text-box: trim-both cap alphabetic;
|
||||
}
|
||||
|
||||
.settings-section__help-button:hover,
|
||||
.settings-section__help-button:focus-visible {
|
||||
border-color: color-mix(in srgb, var(--accent) 55%, var(--border));
|
||||
|
||||
Reference in New Issue
Block a user