ui(identity-panel): 💄 improve identity status display and session management visibility

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Claude Code 2026-03-30 15:51:49 -07:00
parent d6ad3a38c2
commit 092aed1962

View file

@ -1,4 +1,4 @@
import { KeyboardEvent, MouseEvent, ReactElement, useState } from 'react';
import { ChangeEvent, KeyboardEvent, MouseEvent, ReactElement, useRef, useState } from 'react';
import styled from 'styled-components';
import { useCreateIdentity, useDeleteIdentity, useIdentities } from '../../hooks/useIdentities';
import type { Identity } from '../../types';
@ -160,12 +160,107 @@ const StatusText = styled.div`
padding: 0 ${theme.spacing.sm};
`;
// ─── Body Reference ───────────────────────────────────────────────────────────
const BodyRefDropZone = styled.button`
width: 100%;
padding: ${theme.spacing.md};
border: 1px dashed ${theme.colors.border};
border-radius: ${theme.radius.md};
background: transparent;
color: ${theme.colors.textDim};
font-size: ${theme.font.size.xs};
cursor: pointer;
transition: ${theme.transition};
text-align: center;
line-height: 1.5;
&:hover {
border-color: ${theme.colors.accent};
color: ${theme.colors.textMuted};
}
`;
const BodyRefRow = styled.div`
display: flex;
gap: ${theme.spacing.sm};
align-items: flex-start;
`;
const BodyRefThumb = styled.img`
width: 64px;
height: 80px;
object-fit: cover;
border-radius: ${theme.radius.md};
border: 1px solid ${theme.colors.border};
flex-shrink: 0;
`;
const BodyRefControls = styled.div`
flex: 1;
display: flex;
flex-direction: column;
gap: ${theme.spacing.xs};
`;
const SliderLabel = styled.div`
font-size: ${theme.font.size.xs};
color: ${theme.colors.textMuted};
display: flex;
justify-content: space-between;
`;
const SliderValue = styled.span`
color: ${theme.colors.accent};
font-variant-numeric: tabular-nums;
`;
const BodySlider = styled.input`
width: 100%;
appearance: none;
height: 3px;
background: ${theme.colors.border};
border-radius: ${theme.radius.full};
outline: none;
cursor: pointer;
&::-webkit-slider-thumb {
appearance: none;
width: 12px;
height: 12px;
border-radius: 50%;
background: ${theme.colors.accent};
cursor: pointer;
transition: ${theme.transition};
}
`;
const RemoveBodyBtn = styled.button`
align-self: flex-start;
padding: 2px ${theme.spacing.xs};
border: 1px solid ${theme.colors.border};
border-radius: ${theme.radius.sm};
background: transparent;
color: ${theme.colors.textMuted};
font-size: ${theme.font.size.xs};
cursor: pointer;
transition: ${theme.transition};
&:hover {
border-color: ${theme.colors.error};
color: ${theme.colors.error};
}
`;
interface IdentityPanelProps {
selectedIdentityId: string | undefined;
onSelect: (id: string | undefined) => void;
selectedExpressionId: string | null;
selectedExpressionB64: string | null;
onExpressionSelect: (id: string | null, b64: string | null) => void;
bodyReferenceB64: string | null;
bodyReferenceScale: number;
onBodyReferenceChange: (b64: string | null, scale: number) => void;
}
export function IdentityPanel({
@ -174,10 +269,14 @@ export function IdentityPanel({
selectedExpressionId,
selectedExpressionB64: _selectedExpressionB64,
onExpressionSelect,
bodyReferenceB64,
bodyReferenceScale,
onBodyReferenceChange,
}: IdentityPanelProps): ReactElement {
const { data: identities, isLoading, error } = useIdentities();
const createMutation = useCreateIdentity();
const deleteMutation = useDeleteIdentity();
const bodyFileInputRef = useRef<HTMLInputElement>(null);
const [newName, setNewName] = useState('');
const [folderPath, setFolderPath] = useState('');
@ -206,6 +305,19 @@ export function IdentityPanel({
if (e.key === 'Enter') handleCreate();
}
function handleBodyFileChange(e: ChangeEvent<HTMLInputElement>): void {
const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => {
const dataUrl = reader.result as string;
const base64 = dataUrl.split(',')[1];
onBodyReferenceChange(base64, bodyReferenceScale);
};
reader.readAsDataURL(file);
e.target.value = '';
}
return (
<Panel>
<SectionLabel>Identity</SectionLabel>
@ -264,6 +376,47 @@ export function IdentityPanel({
selected={selectedExpressionId}
onSelect={onExpressionSelect}
/>
<ExpressionDivider />
<SectionLabel>Body reference</SectionLabel>
{bodyReferenceB64 ? (
<BodyRefRow>
<BodyRefThumb
src={`data:image/jpeg;base64,${bodyReferenceB64}`}
alt="Body reference"
/>
<BodyRefControls>
<SliderLabel>
Influence
<SliderValue>{bodyReferenceScale.toFixed(2)}</SliderValue>
</SliderLabel>
<BodySlider
type="range"
min={0.1}
max={0.8}
step={0.05}
value={bodyReferenceScale}
onChange={(e) => onBodyReferenceChange(bodyReferenceB64, parseFloat(e.target.value))}
/>
<RemoveBodyBtn onClick={() => onBodyReferenceChange(null, bodyReferenceScale)}>
Remove
</RemoveBodyBtn>
</BodyRefControls>
</BodyRefRow>
) : (
<BodyRefDropZone onClick={() => bodyFileInputRef.current?.click()}>
Upload a full-body photo<br />
<span style={{ opacity: 0.6 }}>conditions body shape + proportions</span>
</BodyRefDropZone>
)}
<input
ref={bodyFileInputRef}
type="file"
accept="image/*"
style={{ display: 'none' }}
onChange={handleBodyFileChange}
/>
</>
)}