Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed src/web-ui/public/panda_1.png
Binary file not shown.
Binary file removed src/web-ui/public/panda_2.png
Binary file not shown.
Binary file removed src/web-ui/public/panda_wink.png
Binary file not shown.
767 changes: 477 additions & 290 deletions src/web-ui/src/app/components/AboutDialog/AboutDialog.scss

Large diffs are not rendered by default.

541 changes: 354 additions & 187 deletions src/web-ui/src/app/components/AboutDialog/AboutDialog.tsx

Large diffs are not rendered by default.

8 changes: 3 additions & 5 deletions src/web-ui/src/app/components/AboutDialog/appearance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,18 @@ export const aboutDialogAppearanceDescriptor: AppearanceSurfaceDescriptor = {
{ id: 'root', visualRole: 'content', continuityGroup: 'about-dialog' },
{ id: 'hero', visualRole: 'content', continuityGroup: 'about-dialog' },
{ id: 'title', propertyProfile: 'paint', visualRole: 'content' },
{ id: 'version', propertyProfile: 'paint', visualRole: 'content' },
{ id: 'decoration', propertyProfile: 'paint', visualRole: 'decoration' },
{ id: 'content', visualRole: 'content', continuityGroup: 'about-dialog' },
{ id: 'updateCard', visualRole: 'card' },
{ id: 'updateHeader', visualRole: 'toolbar' },
{ id: 'channelBadge', propertyProfile: 'paint', visualRole: 'content' },
{ id: 'updateCard', visualRole: 'content' },
{ id: 'updateFeedback', propertyProfile: 'paint', visualRole: 'content' },
{ id: 'updateActions', visualRole: 'toolbar' },
{ id: 'progress', propertyProfile: 'control', visualRole: 'control' },
{ id: 'progressFill', propertyProfile: 'paint', visualRole: 'decoration' },
{ id: 'infoCard', visualRole: 'card' },
{ id: 'infoRow', visualRole: 'content' },
{ id: 'infoLabel', propertyProfile: 'paint', visualRole: 'content' },
{ id: 'infoValue', propertyProfile: 'paint', visualRole: 'content' },
{ id: 'copyButton', propertyProfile: 'control', visualRole: 'control' },
{ id: 'starCallout', visualRole: 'toolbar', continuityGroup: 'about-dialog' },
{ id: 'footer', visualRole: 'toolbar', continuityGroup: 'about-dialog' },
{ id: 'license', propertyProfile: 'paint', visualRole: 'content' },
{ id: 'copyright', propertyProfile: 'paint', visualRole: 'content' },
Expand Down
52 changes: 17 additions & 35 deletions src/web-ui/src/app/components/AssistantAvatar/AssistantAvatar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,52 +10,30 @@
height: var(--assistant-avatar-size);
place-items: center;
overflow: visible;
border: 1px solid color-mix(in srgb, var(--bf-appearance-token-color-accent-500) 38%, var(--bf-appearance-token-border-subtle));
border: 1px solid var(--bf-appearance-token-color-overlay-black-12);
border-radius: 50%;
background:
radial-gradient(circle at 32% 24%, color-mix(in srgb, var(--bf-appearance-token-color-purple-500) 24%, transparent), transparent 44%),
color-mix(in srgb, var(--bf-appearance-token-color-accent-500) 13%, var(--bf-appearance-token-element-bg-base));
color: var(--bf-appearance-token-color-accent-500);
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--bf-appearance-token-color-bg-primary) 30%, transparent);
background: var(--bf-appearance-token-color-static-white);
color: var(--bf-appearance-token-color-text-secondary);
box-shadow: 0 2px 8px var(--bf-appearance-token-color-overlay-black-08);
isolation: isolate;

&[data-bf-palette='violet'] {
border-color: color-mix(in srgb, var(--bf-appearance-token-color-purple-500) 38%, var(--bf-appearance-token-border-subtle));
background:
radial-gradient(circle at 32% 24%, color-mix(in srgb, var(--bf-appearance-token-color-accent-500) 24%, transparent), transparent 44%),
color-mix(in srgb, var(--bf-appearance-token-color-purple-500) 13%, var(--bf-appearance-token-element-bg-base));
color: var(--bf-appearance-token-color-purple-500);
}

&__secondary-fill {
fill: var(--bf-appearance-token-color-purple-500);
}

&__secondary-stroke {
stroke: var(--bf-appearance-token-color-purple-500);
}

&[data-bf-palette='violet'] &__secondary-fill {
fill: var(--bf-appearance-token-color-accent-500);
}

&[data-bf-palette='violet'] &__secondary-stroke {
stroke: var(--bf-appearance-token-color-accent-500);
}

&__art {
display: grid;
width: 100%;
height: 100%;
place-items: center;
overflow: hidden;
border-radius: inherit;
}

svg {
display: block;
width: 78%;
height: 78%;
}
&__image {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
pointer-events: none;
user-select: none;
-webkit-user-drag: none;
}

&__emoji {
Expand All @@ -70,6 +48,10 @@
color: var(--bf-appearance-token-color-text-secondary);
}

&.is-image {
border-color: var(--bf-appearance-token-color-overlay-black-12);
}

&.is-active {
border-color: color-mix(in srgb, var(--bf-appearance-token-color-accent-500) 72%, var(--bf-appearance-token-border-medium));
box-shadow:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,17 @@ describe('AssistantAvatar', () => {
container.remove();
});

it('renders a selected built-in preset as a stable circular identity', () => {
it('maps a retired SVG preset id to the new Claw artwork', () => {
act(() => {
root.render(<AssistantAvatar presetId="orbit-nova" emoji="🧭" size={28} />);
});

const avatar = container.querySelector('[data-bf-component="assistant-avatar"]');
expect(avatar?.getAttribute('data-bf-preset')).toBe('orbit-nova');
expect(avatar?.getAttribute('data-bf-family')).toBe('orbit');
const image = container.querySelector('.assistant-avatar__image') as HTMLImageElement;
expect(avatar?.getAttribute('data-bf-preset')).toBe('claw-orbit');
expect(avatar?.getAttribute('data-bf-family')).toBe('clawOrbit');
expect(avatar?.getAttribute('style')).toContain('28px');
expect(image.getAttribute('src')).toBe('/assets/assistant/claw-avatar-alt.webp');
});

it('keeps the legacy emoji when a preset id is missing or unknown', () => {
Expand All @@ -44,10 +46,27 @@ describe('AssistantAvatar', () => {
expect(avatar?.textContent).toContain('🧭');
});

it('uses a deterministic built-in fallback when no identity marker exists', () => {
it('uses a deterministic new Claw fallback when no identity marker exists', () => {
expect(resolveAssistantAvatarPreset(undefined, 'assistant-1').id)
.toBe(resolveAssistantAvatarPreset(undefined, 'assistant-1').id);
expect(resolveAssistantAvatarPreset(undefined, 'assistant-1').id)
.not.toBe('');
expect(resolveAssistantAvatarPreset(undefined).id).toBe('claw');
});

it('renders the new default avatar without discarding the accessible identity', () => {
act(() => {
root.render(
<AssistantAvatar
name="Claw"
decorative={false}
/>,
);
});

const avatar = container.querySelector('[data-bf-component="assistant-avatar"]');
const image = container.querySelector('.assistant-avatar__image') as HTMLImageElement;
expect(avatar?.getAttribute('data-bf-family')).toBe('claw');
expect(avatar?.getAttribute('data-bf-preset')).toBe('claw');
expect(avatar?.getAttribute('aria-label')).toBe('Claw avatar');
expect(image.getAttribute('src')).toBe('/assets/assistant/claw-avatar.webp');
});
});
68 changes: 9 additions & 59 deletions src/web-ui/src/app/components/AssistantAvatar/AssistantAvatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React from 'react';
import {
getAssistantAvatarPreset,
resolveAssistantAvatarPreset,
type AssistantAvatarFamily,
} from './assistantAvatarPresets';
import { firstAvatarGrapheme } from './assistantAvatarValue';
import './AssistantAvatar.scss';
Expand All @@ -21,60 +20,6 @@ export interface AssistantAvatarProps {
className?: string;
}

const PresetArtwork: React.FC<{
family: AssistantAvatarFamily;
variant: number;
}> = ({ family, variant }) => {
if (family === 'signal') {
return (
<svg viewBox="0 0 32 32" focusable="false" aria-hidden="true">
<circle cx="16" cy="16" r={variant === 1 ? 3.4 : 2.8} />
<path d="M10.5 11.2a7 7 0 0 0 0 9.6" fill="none" stroke="currentColor" strokeWidth="2.6" strokeLinecap="round" />
<path className="assistant-avatar__secondary-stroke" d="M7.2 8.2a11.2 11.2 0 0 0 0 15.6" fill="none" strokeWidth="2.1" strokeLinecap="round" />
<path d="M21.5 11.2a7 7 0 0 1 0 9.6" fill="none" stroke="currentColor" strokeWidth="2.6" strokeLinecap="round" />
<path className="assistant-avatar__secondary-stroke" d="M24.8 8.2a11.2 11.2 0 0 1 0 15.6" fill="none" strokeWidth="2.1" strokeLinecap="round" />
</svg>
);
}

if (family === 'orbit') {
return (
<svg viewBox="0 0 32 32" focusable="false" aria-hidden="true">
<circle className="assistant-avatar__secondary-fill" cx="16" cy="16" r="5.2" />
<ellipse cx="16" cy="16" rx="12" ry="6.2" fill="none" stroke="currentColor" strokeWidth="2" transform={variant === 1 ? 'rotate(-24 16 16)' : 'rotate(28 16 16)'} />
<circle cx={variant === 1 ? 26 : 7} cy={variant === 1 ? 12 : 11} r="2.2" />
<circle className="assistant-avatar__secondary-fill" cx={variant === 1 ? 9 : 23} cy={variant === 1 ? 23 : 24} r="1.4" />
</svg>
);
}

if (family === 'mosaic') {
return (
<svg viewBox="0 0 32 32" focusable="false" aria-hidden="true">
<rect x="6" y="6" width="9" height="9" rx="2.4" />
<rect className="assistant-avatar__secondary-fill" x="17" y="6" width="9" height={variant === 1 ? 14 : 8} rx="2.4" />
<rect className="assistant-avatar__secondary-fill" x="6" y="17" width={variant === 1 ? 14 : 8} height="9" rx="2.4" />
<rect x={variant === 1 ? 22 : 16} y={variant === 1 ? 22 : 16} width={variant === 1 ? 4 : 10} height={variant === 1 ? 4 : 10} rx="2" />
</svg>
);
}

return (
<svg viewBox="0 0 32 32" focusable="false" aria-hidden="true">
<path
d={variant === 1
? 'M8 13.2C8 8.7 11.5 6 16 6s8 2.7 8 7.2v5.6C24 23.3 20.5 26 16 26s-8-2.7-8-7.2v-5.6Z'
: 'M7 14c0-5 4-8 9-8s9 3 9 8v4c0 5-4 8-9 8s-9-3-9-8v-4Z'}
className="assistant-avatar__secondary-fill"
/>
<circle cx="13" cy="16" r="1.8" />
<circle cx="19" cy="16" r="1.8" />
<path d={variant === 1 ? 'M13 21c1.8 1 4.2 1 6 0' : 'M13.2 21h5.6'} fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
{variant === 1 ? <path d="m21.5 8.5 2-2 2 2-2 2-2-2Z" /> : null}
</svg>
);
};

const AssistantAvatar: React.FC<AssistantAvatarProps> = ({
presetId,
emoji,
Expand All @@ -88,11 +33,11 @@ const AssistantAvatar: React.FC<AssistantAvatarProps> = ({
}) => {
const displayedEmoji = firstAvatarGrapheme(emoji ?? '');
const explicitPreset = getAssistantAvatarPreset(presetId);
const preset = explicitPreset ?? resolveAssistantAvatarPreset(undefined, stableKey || name);
const preset = explicitPreset ?? resolveAssistantAvatarPreset(undefined, stableKey);
const usesPreset = Boolean(explicitPreset) || !displayedEmoji;
const classes = [
'assistant-avatar',
usesPreset ? 'is-preset' : 'is-emoji',
usesPreset ? 'is-image' : 'is-emoji',
active && 'is-active',
status !== 'idle' && `is-${status}`,
className,
Expand All @@ -106,7 +51,6 @@ const AssistantAvatar: React.FC<AssistantAvatarProps> = ({
data-bf-part="root"
data-bf-family={usesPreset ? preset.family : 'emoji'}
data-bf-preset={usesPreset ? preset.id : undefined}
data-bf-palette={usesPreset ? preset.palette : undefined}
data-bf-state={[active && 'active', status !== 'idle' && status].filter(Boolean).join(' ') || undefined}
style={{ '--assistant-avatar-size': `${size}px` } as React.CSSProperties}
role={decorative ? undefined : 'img'}
Expand All @@ -115,7 +59,13 @@ const AssistantAvatar: React.FC<AssistantAvatarProps> = ({
>
<span className="assistant-avatar__art" aria-hidden="true">
{usesPreset ? (
<PresetArtwork family={preset.family} variant={preset.variant} />
<img
className="assistant-avatar__image"
src={preset.imageSrc}
alt=""
decoding="async"
draggable={false}
/>
) : (
<span className="assistant-avatar__emoji">{displayedEmoji}</span>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
export const ASSISTANT_AVATAR_PRESETS = [
{ id: 'signal-pulse', family: 'signal', variant: 1, palette: 'accent' },
{ id: 'signal-wave', family: 'signal', variant: 2, palette: 'violet' },
{ id: 'orbit-nova', family: 'orbit', variant: 1, palette: 'violet' },
{ id: 'orbit-loop', family: 'orbit', variant: 2, palette: 'accent' },
{ id: 'mosaic-grid', family: 'mosaic', variant: 1, palette: 'accent' },
{ id: 'mosaic-stack', family: 'mosaic', variant: 2, palette: 'violet' },
{ id: 'companion-spark', family: 'companion', variant: 1, palette: 'violet' },
{ id: 'companion-calm', family: 'companion', variant: 2, palette: 'accent' },
{
id: 'claw',
family: 'claw',
imageSrc: '/assets/assistant/claw-avatar.webp',
},
{
id: 'claw-orbit',
family: 'clawOrbit',
imageSrc: '/assets/assistant/claw-avatar-alt.webp',
},
] as const;

export type AssistantAvatarPreset = typeof ASSISTANT_AVATAR_PRESETS[number];
Expand All @@ -17,6 +19,19 @@ const PRESETS_BY_ID = new Map<string, AssistantAvatarPreset>(
ASSISTANT_AVATAR_PRESETS.map((preset) => [preset.id, preset]),
);

// Persisted identity documents may still contain one of the retired SVG preset
// ids. Keep reading them, but resolve every legacy id to the new Claw artwork.
const LEGACY_PRESET_ALIASES = new Map<string, AssistantAvatarPresetId>([
['signal-pulse', 'claw'],
['signal-wave', 'claw-orbit'],
['orbit-nova', 'claw-orbit'],
['orbit-loop', 'claw'],
['mosaic-grid', 'claw'],
['mosaic-stack', 'claw-orbit'],
['companion-spark', 'claw-orbit'],
['companion-calm', 'claw'],
]);

function stableHash(value: string): number {
let hash = 2166136261;
for (let index = 0; index < value.length; index += 1) {
Expand All @@ -28,7 +43,10 @@ function stableHash(value: string): number {

export function getAssistantAvatarPreset(value?: string | null): AssistantAvatarPreset | null {
const normalized = value?.trim();
return normalized ? PRESETS_BY_ID.get(normalized) ?? null : null;
if (!normalized) return null;

const canonicalId = LEGACY_PRESET_ALIASES.get(normalized) ?? normalized;
return PRESETS_BY_ID.get(canonicalId) ?? null;
}

export function resolveAssistantAvatarPreset(
Expand All @@ -38,6 +56,8 @@ export function resolveAssistantAvatarPreset(
const explicitPreset = getAssistantAvatarPreset(value);
if (explicitPreset) return explicitPreset;

const normalizedKey = stableKey?.trim() || 'bitfun-primary-assistant';
const normalizedKey = stableKey?.trim();
if (!normalizedKey) return ASSISTANT_AVATAR_PRESETS[0];

return ASSISTANT_AVATAR_PRESETS[stableHash(normalizedKey) % ASSISTANT_AVATAR_PRESETS.length];
}
15 changes: 5 additions & 10 deletions src/web-ui/src/app/components/NavPanel/NavPanel.scss
Original file line number Diff line number Diff line change
Expand Up @@ -886,6 +886,8 @@ $_section-header-height: 24px;
display: flex;
align-items: center;
justify-content: flex-end;
gap: $size-gap-1;
flex: 0 0 auto;
margin-left: auto;
min-width: 0;
}
Expand All @@ -908,10 +910,6 @@ $_section-header-height: 24px;
filter $motion-fast $easing-standard,
opacity $motion-fast $easing-standard;

& + & {
margin-left: -8px;
}

&:not(.is-active):not(.bitfun-nav-panel__miniapp-bubble--more) {
filter: saturate(0.72);
opacity: 0.9;
Expand All @@ -927,8 +925,7 @@ $_section-header-height: 24px;
&.is-active {
filter: saturate(1);
opacity: 1;
box-shadow: 0 8px 18px rgba(var(--bf-appearance-token-color-static-black-rgb), 0.22),
0 0 0 3px color-mix(in srgb, var(--bf-appearance-token-color-accent-500) 28%, transparent);
box-shadow: none;
}

&.is-customizing {
Expand All @@ -937,9 +934,7 @@ $_section-header-height: 24px;
}

&.is-active.is-customizing {
box-shadow: 0 8px 18px rgba(var(--bf-appearance-token-color-static-black-rgb), 0.22),
0 0 0 3px color-mix(in srgb, var(--bf-appearance-token-color-accent-500) 28%, transparent),
0 0 0 5px color-mix(in srgb, var(--bf-appearance-token-color-accent-500) 28%, transparent);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--bf-appearance-token-color-accent-500) 42%, transparent);
}

&-customize-dot {
Expand Down Expand Up @@ -3313,7 +3308,7 @@ $_section-header-height: 24px;
filter 120ms ease;

&:hover {
transform: none;
transform: scale(1.05);
box-shadow: none;
}

Expand Down
Loading
Loading