feat(ui): 자매 프로필 사진 통합

- OfficeFloor: 4자매 책상에 SisterAvatar (60px gradient frame)
- SubTaskTree: 노드 row 에 작은 SisterAvatar (20px) 표시
- SubTaskDetailDrawer: 헤더에 큰 SisterAvatar (40px) + role badge 함께

이미 있는 백엔드 /api/sisters/:name/avatar 엔드포인트 재활용
(SSH 로 ~/.openclaw/avatar.png 가져오는 AvatarService)
This commit is contained in:
2026-04-10 18:14:39 +09:00
parent 5ec1287792
commit f8da7331ce
4 changed files with 26 additions and 13 deletions

View File

@@ -1 +1 @@
1775811234
1775812327

View File

@@ -3,6 +3,7 @@
import React, { useMemo } from 'react';
import styled, { keyframes } from 'styled-components';
import type { RailsSubTaskNode, RailsPipelineSummary } from '@/lib/useRailsSocket';
import SisterAvatar from '@/components/common/SisterAvatar';
const SISTERS = [
{ key: 'harang', label: '하랑', role: 'Planner', color: '#3b82f6', accent: '#60a5fa', x: 0, y: 0 },
@@ -114,17 +115,15 @@ const SisterBadge = styled.div<{ $color: string }>`
gap: 12px;
`;
const Avatar = styled.div<{ $color: string; $running: boolean }>`
width: 56px;
height: 56px;
border-radius: 14px;
background: linear-gradient(135deg, ${({ $color }) => $color}, ${({ $color }) => `${$color}80`});
const AvatarFrame = styled.div<{ $color: string; $running: boolean }>`
width: 60px;
height: 60px;
border-radius: 50%;
padding: 3px;
background: linear-gradient(135deg, ${({ $color }) => $color}, ${({ $color }) => `${$color}60`});
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
font-weight: 700;
color: #fff;
flex-shrink: 0;
box-shadow: ${({ $color, $running }) =>
$running ? `0 4px 16px ${$color}60` : `0 2px 8px ${$color}30`};
@@ -309,9 +308,9 @@ export default function OfficeFloor({
>
<DeskHead>
<SisterBadge $color={sister.color}>
<Avatar $color={sister.color} $running={running}>
{sister.label[0]}
</Avatar>
<AvatarFrame $color={sister.color} $running={running}>
<SisterAvatar name={sister.key} size={54} />
</AvatarFrame>
<NameBlock>
<Name>{sister.label}</Name>
<Role>{sister.role}</Role>

View File

@@ -3,6 +3,9 @@
import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import { API_URL } from '@/lib/config';
import SisterAvatar from '@/components/common/SisterAvatar';
const SISTER_NAMES = new Set(['harang', 'narang', 'darang', 'erang']);
interface SubTaskEvent {
id: number;
@@ -375,7 +378,12 @@ export default function SubTaskDetailDrawer({ subTaskId, onClose }: Props) {
<>
<Header>
<TopRow>
<RoleBadge $role={detail.role}>{detail.role}</RoleBadge>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
{SISTER_NAMES.has(detail.agentName) && (
<SisterAvatar name={detail.agentName} size={40} />
)}
<RoleBadge $role={detail.role}>{detail.role}</RoleBadge>
</div>
<Close onClick={onClose}> ESC</Close>
</TopRow>
<Title>{detail.title}</Title>

View File

@@ -3,6 +3,9 @@
import React from 'react';
import styled from 'styled-components';
import type { RailsSubTaskNode } from '@/lib/useRailsSocket';
import SisterAvatar from '@/components/common/SisterAvatar';
const SISTER_NAMES = new Set(['harang', 'narang', 'darang', 'erang']);
const Wrap = styled.div`
font-family: var(--font-sans);
@@ -160,6 +163,9 @@ function NodeRow({
<Node $state={node.state} onClick={() => onSelect?.(node.id)}>
<Row>
<StateDot $state={node.state} />
{SISTER_NAMES.has(node.agentName) && (
<SisterAvatar name={node.agentName} size={20} style={{ marginRight: 8 }} />
)}
<RoleBadge $role={node.role}>{node.role}</RoleBadge>
<Title>{node.title.slice(0, 100)}</Title>
<Model>{node.model || '—'}</Model>