Skip to content
Open
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
3 changes: 3 additions & 0 deletions backend/app/database/face_clusters.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ def db_get_images_by_cluster_id(
i.path as image_path,
i.thumbnailPath as thumbnail_path,
i.metadata,
i.isFavourite,
f.face_id,
f.confidence,
f.bbox
Expand All @@ -321,6 +322,7 @@ def db_get_images_by_cluster_id(
image_path,
thumbnail_path,
metadata,
is_favourite,
face_id,
confidence,
bbox_json,
Expand All @@ -340,6 +342,7 @@ def db_get_images_by_cluster_id(
"image_path": image_path,
"thumbnail_path": thumbnail_path,
"metadata": metadata_dict,
"isFavourite": is_favourite,
"face_id": face_id,
"confidence": confidence,
"bbox": bbox,
Expand Down
50 changes: 25 additions & 25 deletions backend/app/routes/face_clusters.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,56 +153,56 @@ def get_all_clusters():
def get_cluster_images(cluster_id: str):
"""Get all images that contain faces belonging to a specific cluster."""
try:
# Step 1: Validate cluster exists
# Check if cluster exists
cluster = db_get_cluster_by_id(cluster_id)
if not cluster:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ErrorResponse(
success=False,
error="Cluster Not Found",
message=f"Cluster with ID '{cluster_id}' does not exist.",
message=f"Cluster with ID '{cluster_id}' does not exist",
).model_dump(),
)

# Step 2: Get images for this cluster
images_data = db_get_images_by_cluster_id(cluster_id)

# Step 3: Convert to response models
images = [
ImageInCluster(
id=img["image_id"],
path=img["image_path"],
thumbnailPath=img["thumbnail_path"],
metadata=img["metadata"],
face_id=img["face_id"],
confidence=img["confidence"],
bbox=img["bbox"],
)
for img in images_data
]
# Get images for this cluster
images = db_get_images_by_cluster_id(cluster_id)

# Transform the data to match the frontend schema
formatted_images = []
for img in images:
formatted_images.append({
"id": img["image_id"],
"path": img["image_path"],
"thumbnailPath": img["thumbnail_path"],
"metadata": img["metadata"],
"isFavourite": bool(img["isFavourite"]),
"face_id": img["face_id"],
"confidence": img["confidence"],
"bbox": img["bbox"],
})

return GetClusterImagesResponse(
success=True,
message=f"Successfully retrieved {len(images)} image(s) for cluster '{cluster_id}'",
message=f"Successfully retrieved {len(formatted_images)} images for cluster",
data=GetClusterImagesData(
cluster_id=cluster_id,
cluster_name=cluster["cluster_name"],
images=images,
total_images=len(images),
cluster_name=cluster["cluster_name"], # ✅ CHANGE THIS LINE - Remove .get()
images=formatted_images,
total_images=len(formatted_images),
),
)

except HTTPException as e:
# Re-raise HTTPExceptions to preserve the status code and detail
raise e
except Exception as e:
logger.error(f"Error getting cluster images: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=ErrorResponse(
success=False,
error="Internal server error",
message=f"Unable to retrieve images for cluster: {str(e)}",
error="Internal Server Error",
message=f"Failed to retrieve cluster images: {str(e)}",
).model_dump(),
)

Expand Down
1 change: 1 addition & 0 deletions backend/app/schemas/face_clusters.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ class ImageInCluster(BaseModel):
path: str
thumbnailPath: Optional[str] = None
metadata: Optional[Dict[str, Any]] = None
isFavourite: bool = False # Add this field
face_id: int
confidence: Optional[float] = None
bbox: Optional[Dict[str, Union[int, float]]] = None
Expand Down
5 changes: 5 additions & 0 deletions docs/backend/backend_python/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -2212,6 +2212,11 @@
],
"title": "Metadata"
},
"isFavourite": {
"type": "boolean",
"title": "Isfavourite",
"default": false
},
"face_id": {
"type": "integer",
"title": "Face Id"
Expand Down
58 changes: 57 additions & 1 deletion frontend/src/hooks/useToggleFav.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,74 @@
import { usePictoMutation } from '@/hooks/useQueryExtension';
import { useMutationFeedback } from '@/hooks/useMutationFeedback';
import { togglefav } from '@/api/api-functions/togglefav';
import { useQueryClient } from '@tanstack/react-query';

export const useToggleFav = () => {
const queryClient = useQueryClient();

const toggleFavouriteMutation = usePictoMutation({
mutationFn: async (image_id: string) => togglefav(image_id),
autoInvalidateTags: ['images'],
onSuccess: () => {
// Invalidate person-images queries to refetch cluster images
queryClient.invalidateQueries({ queryKey: ['person-images'] });
},
onMutate: async (image_id: string) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['images'] });

// Snapshot the previous value
const previousImages = queryClient.getQueryData(['images']);

// Optimistically update images query
queryClient.setQueryData(['images'], (old: any) => {
if (!old?.data) return old;

return {
...old,
data: old.data.map((img: any) =>
img.id === image_id
? { ...img, isFavourite: !img.isFavourite }
: img
)
};
});

// Optimistically update person-images queries
queryClient.setQueriesData({ queryKey: ['person-images'] }, (old: any) => {
if (!old?.data?.images) return old;

return {
...old,
data: {
...old.data,
images: old.data.images.map((img: any) =>
img.id === image_id
? { ...img, isFavourite: !img.isFavourite }
: img
)
}
};
});

return { previousImages };
},
onError: (err, image_id, context) => {
if (context?.previousImages) {
queryClient.setQueryData(['images'], context.previousImages);
}
// Refetch to restore correct state
queryClient.invalidateQueries({ queryKey: ['person-images'] });
},
});

useMutationFeedback(toggleFavouriteMutation, {
showLoading: false,
showSuccess: false,
});

return {
toggleFavourite: (id: any) => toggleFavouriteMutation.mutate(id),
toggleFavouritePending: toggleFavouriteMutation.isPending,
};
};
};
24 changes: 23 additions & 1 deletion frontend/src/pages/PersonImages/PersonImages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { ROUTES } from '@/constants/routes';
import { Check, Pencil, ArrowLeft } from 'lucide-react';
import { useQueryClient } from '@tanstack/react-query';

export const PersonImages = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
const queryClient = useQueryClient();
const { clusterId } = useParams<{ clusterId: string }>();
const isImageViewOpen = useSelector(selectIsImageViewOpen);
const images = useSelector(selectImages);
Expand All @@ -25,6 +27,7 @@ export const PersonImages = () => {
const { data, isLoading, isSuccess, isError } = usePictoQuery({
queryKey: ['person-images', clusterId],
queryFn: async () => fetchClusterImages({ clusterId: clusterId || '' }),
refetchOnWindowFocus: true,
});

const { mutate: renameClusterMutate } = usePictoMutation({
Expand All @@ -46,6 +49,24 @@ export const PersonImages = () => {
}
}, [data, isSuccess, isError, isLoading, dispatch]);

// Listen to query cache changes and update Redux state
useEffect(() => {
const unsubscribe = queryClient.getQueryCache().subscribe((event) => {
if (
event.type === 'updated' &&
event.query.queryKey[0] === 'person-images' &&
event.query.queryKey[1] === clusterId
) {
const updatedData = event.query.state.data as any;
if (updatedData?.data?.images) {
dispatch(setImages(updatedData.data.images));
}
}
});

return () => unsubscribe();
}, [clusterId, dispatch, queryClient]);

const handleEditName = () => {
setClusterName(clusterName);
setIsEditing(true);
Expand All @@ -66,6 +87,7 @@ export const PersonImages = () => {
handleSaveName();
}
};

return (
<div className="p-6">
<div className="mb-6 flex items-center justify-between">
Expand Down Expand Up @@ -124,4 +146,4 @@ export const PersonImages = () => {
{isImageViewOpen && <MediaView images={images} />}
</div>
);
};
};