/**
* FaceGallery.jsx — Enrolled person management.
*
* Services:
* /social/enrollment/list_persons (ListPersons.srv)
* /social/enrollment/enroll_person (EnrollPerson.srv)
* /social/enrollment/delete_person (DeletePerson.srv)
*
* Topics (live detections):
* /social/face_detections (FaceDetectionArray) — optional, shows live bboxes
*/
import { useEffect, useState, useCallback } from 'react';
function PersonCard({ person, onDelete, liveDetection }) {
const [deleting, setDeleting] = useState(false);
const enrolledAt = person.enrolled_at
? new Date(person.enrolled_at.sec * 1000).toLocaleDateString()
: '—';
const handleDelete = async () => {
if (!confirm(`Delete ${person.person_name}?`)) return;
setDeleting(true);
try {
await onDelete(person.person_id);
} finally {
setDeleting(false);
}
};
const isActive = liveDetection?.face_id === person.person_id;
return (
{/* Avatar placeholder */}
{/* Info */}
{person.person_name}
ID: {person.person_id}
Samples: {person.sample_count}
Enrolled: {enrolledAt}
{/* Delete */}
);
}
export function FaceGallery({ subscribe, callService }) {
const [persons, setPersons] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [enrolling, setEnrolling] = useState(false);
const [newName, setNewName] = useState('');
const [newSamples, setNewSamples] = useState(5);
const [liveDetections, setLiveDetections] = useState([]);
const loadPersons = useCallback(async () => {
setLoading(true);
setError(null);
try {
const result = await callService(
'/social/enrollment/list_persons',
'saltybot_social_msgs/srv/ListPersons',
{}
);
setPersons(result?.persons ?? []);
} catch (e) {
setError('Service unavailable: ' + (e?.message ?? e));
} finally {
setLoading(false);
}
}, [callService]);
useEffect(() => {
loadPersons();
}, [loadPersons]);
// Live face detections
useEffect(() => {
const unsub = subscribe(
'/social/face_detections',
'saltybot_social_msgs/FaceDetectionArray',
(msg) => setLiveDetections(msg.faces ?? [])
);
return unsub;
}, [subscribe]);
const handleEnroll = async () => {
if (!newName.trim()) return;
setEnrolling(true);
setError(null);
try {
const result = await callService(
'/social/enrollment/enroll_person',
'saltybot_social_msgs/srv/EnrollPerson',
{ name: newName.trim(), mode: 'capture', n_samples: newSamples }
);
if (result?.success) {
setNewName('');
await loadPersons();
} else {
setError(result?.message ?? 'Enrollment failed');
}
} catch (e) {
setError('Enroll error: ' + (e?.message ?? e));
} finally {
setEnrolling(false);
}
};
const handleDelete = async (personId) => {
try {
await callService(
'/social/enrollment/delete_person',
'saltybot_social_msgs/srv/DeletePerson',
{ person_id: personId }
);
await loadPersons();
} catch (e) {
setError('Delete error: ' + (e?.message ?? e));
}
};
return (
{/* Enroll new person */}
ENROLL NEW PERSON
{enrolling && (
Capturing face samples — look at the camera…
)}
{/* Error */}
{error && (
{error}
)}
{/* Gallery header */}
ENROLLED PERSONS ({persons.length})
{liveDetections.length > 0 && (
{liveDetections.length} detected
)}
{/* Grid */}
{loading && persons.length === 0 ? (
Loading…
) : persons.length === 0 ? (
No enrolled persons
) : (
{persons.map((p) => (
d.face_id === p.person_id)}
/>
))}
)}
);
}