ui(identity-panel): 💄 improve identity status display and session management visibility
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
d6ad3a38c2
commit
092aed1962
1 changed files with 154 additions and 1 deletions
|
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue