From b11e0e42bf4d13fbe3c90f32d63f108e5743e8db Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 15 Oct 2024 09:43:40 +0530 Subject: [PATCH 01/28] Pick until 3 --- web/packages/new/photos/services/ml/people.ts | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/web/packages/new/photos/services/ml/people.ts b/web/packages/new/photos/services/ml/people.ts index b20988c074..60636847ba 100644 --- a/web/packages/new/photos/services/ml/people.ts +++ b/web/packages/new/photos/services/ml/people.ts @@ -406,24 +406,25 @@ export const suggestionsForPerson = async (person: CGroupPerson) => { const fileByID = new Map(files.map((f) => [f.id, f])); const suggestions = suggestedClusters.map((cluster) => { - const previewFaces = cluster.faces - .slice(0, 3) - .map((faceID) => { - const fileID = fileIDFromFaceID(faceID); - if (!fileID) { - assertionFailed(); - return undefined; - } - const file = fileByID.get(fileID); - if (!file) { - // TODO-Cluster: This might be a hidden/trash file, so this - // assert is not appropriate, we instead need a "until 3". - // assertionFailed(); - return undefined; - } - return { file, faceID }; - }) - .filter((f) => !!f); + const previewFaces: PreviewableFace[] = []; + for (const faceID of cluster.faces) { + const fileID = fileIDFromFaceID(faceID); + if (!fileID) { + assertionFailed(); + continue; + } + + const file = fileByID.get(fileID); + if (!file) { + // This might be a hidden/trash file, and it is thus not + // appropriate to use it as a preview file anyway. + continue; + } + + previewFaces.push({ file, faceID }); + + if (previewFaces.length == 3) break; + } const id = cluster.id; return { id, cluster, previewFaces }; From 74bbdd5e7208efaf86c66b368c99f46ea8ca6b37 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 15 Oct 2024 09:47:47 +0530 Subject: [PATCH 02/28] Random sampling --- web/packages/new/photos/services/ml/people.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/web/packages/new/photos/services/ml/people.ts b/web/packages/new/photos/services/ml/people.ts index 60636847ba..79320650a4 100644 --- a/web/packages/new/photos/services/ml/people.ts +++ b/web/packages/new/photos/services/ml/people.ts @@ -1,6 +1,7 @@ import { assertionFailed } from "@/base/assert"; import log from "@/base/log"; import type { EnteFile } from "@/media/file"; +import { shuffled } from "@/utils/array"; import { getLocalFiles } from "../files"; import { savedCGroups, type CGroup } from "../user-entity"; import type { FaceCluster } from "./cluster"; @@ -371,6 +372,12 @@ export const suggestionsForPerson = async (person: CGroupPerson) => { .flat() .filter((e) => !!e); + // Randomly sample faces to limit the O(n^2) cost. + const sampledPersonFaceEmbeddings = shuffled(personFaceEmbeddings).slice( + 0, + 100, + ); + const suggestedClusters: FaceCluster[] = []; for (const cluster of clusters) { const { id, faces } = cluster; @@ -384,7 +391,7 @@ export const suggestionsForPerson = async (person: CGroupPerson) => { for (const fi of faces) { const ei = embeddingByFaceID.get(fi); if (!ei) continue; - for (const ej of personFaceEmbeddings) { + for (const ej of sampledPersonFaceEmbeddings) { const csim = dotProduct(ei, ej); if (csim >= 0.6) { suggest = true; From bf5f9b6af5af365de9fe01cf8715c27c9a1f9315 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 15 Oct 2024 09:51:52 +0530 Subject: [PATCH 03/28] Avatars 1 --- web/packages/new/photos/components/gallery/PeopleHeader.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/web/packages/new/photos/components/gallery/PeopleHeader.tsx b/web/packages/new/photos/components/gallery/PeopleHeader.tsx index de9b2c4264..a2b6f331d7 100644 --- a/web/packages/new/photos/components/gallery/PeopleHeader.tsx +++ b/web/packages/new/photos/components/gallery/PeopleHeader.tsx @@ -51,6 +51,7 @@ import React, { useEffect, useReducer } from "react"; import { useAppContext } from "../../types/context"; import { AddPersonDialog } from "../AddPersonDialog"; import { SpaceBetweenFlex } from "../mui"; +import { SuggestionFaceList } from "../PeopleList"; import { SingleInputDialog } from "../SingleInputForm"; import type { GalleryBarImplProps } from "./BarImpl"; import { GalleryItemsHeaderAdapter, GalleryItemsSummary } from "./ListHeader"; @@ -515,7 +516,10 @@ const SuggestionsList: React.FC = ({ }} key={suggestion.id} > - {`${suggestion.previewFaces.length} faces ntaoheu naoehtu aosnehu asoenuh aoenuht`} + + + {`${suggestion.previewFaces.length} faces ntaoheu naoehtu aosnehu asoenuh aoenuht`} + Date: Tue, 15 Oct 2024 10:32:23 +0530 Subject: [PATCH 04/28] Subtitle --- .../new/photos/components/gallery/PeopleHeader.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/web/packages/new/photos/components/gallery/PeopleHeader.tsx b/web/packages/new/photos/components/gallery/PeopleHeader.tsx index a2b6f331d7..2f04adc692 100644 --- a/web/packages/new/photos/components/gallery/PeopleHeader.tsx +++ b/web/packages/new/photos/components/gallery/PeopleHeader.tsx @@ -435,10 +435,13 @@ const SuggestionsDialog: React.FC = ({ fullScreen={isSmallWidth} PaperProps={{ sx: { minHeight: "80svh" } }} > - - {person.name && pt(`${person.name}?`)} - - + + + {pt("Review suggestions")} + + {person.name ?? " "} + + {state.activity == "fetching" ? ( @@ -507,7 +510,7 @@ const SuggestionsList: React.FC = ({ markedSuggestionIDs, onMarkSuggestion, }) => ( - + {suggestions.map((suggestion) => ( Date: Tue, 15 Oct 2024 10:39:45 +0530 Subject: [PATCH 05/28] Avatar fit --- .../new/photos/components/PeopleList.tsx | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/web/packages/new/photos/components/PeopleList.tsx b/web/packages/new/photos/components/PeopleList.tsx index 2fb88b4983..d72af38787 100644 --- a/web/packages/new/photos/components/PeopleList.tsx +++ b/web/packages/new/photos/components/PeopleList.tsx @@ -211,12 +211,12 @@ export const SuggestionFaceList: React.FC = ({ return ( {faces.map(({ file, faceID }) => ( - + - + ))} ); @@ -225,7 +225,19 @@ export const SuggestionFaceList: React.FC = ({ const SuggestionFaceList_ = styled("div")` display: flex; flex-wrap: wrap; - gap: 5px; + gap: 2px; + border: 1px solid tomato; +`; + +const SuggestionFace = styled("div")` + width: 87px; + height: 87px; + border-radius: 50%; + overflow: hidden; + & > img { + width: 100%; + height: 100%; + } `; type FaceCropImageViewProps = PreviewableFace & { From 8e350682711682115e52abc0a7e153d0e1d10793 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 15 Oct 2024 11:00:20 +0530 Subject: [PATCH 06/28] Count --- web/packages/new/photos/components/PeopleList.tsx | 3 +-- .../new/photos/components/gallery/PeopleHeader.tsx | 13 ++++++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/web/packages/new/photos/components/PeopleList.tsx b/web/packages/new/photos/components/PeopleList.tsx index d72af38787..5493fc7339 100644 --- a/web/packages/new/photos/components/PeopleList.tsx +++ b/web/packages/new/photos/components/PeopleList.tsx @@ -225,8 +225,7 @@ export const SuggestionFaceList: React.FC = ({ const SuggestionFaceList_ = styled("div")` display: flex; flex-wrap: wrap; - gap: 2px; - border: 1px solid tomato; + gap: 6px; `; const SuggestionFace = styled("div")` diff --git a/web/packages/new/photos/components/gallery/PeopleHeader.tsx b/web/packages/new/photos/components/gallery/PeopleHeader.tsx index 2f04adc692..edc8d02646 100644 --- a/web/packages/new/photos/components/gallery/PeopleHeader.tsx +++ b/web/packages/new/photos/components/gallery/PeopleHeader.tsx @@ -510,18 +510,25 @@ const SuggestionsList: React.FC = ({ markedSuggestionIDs, onMarkSuggestion, }) => ( - + {suggestions.map((suggestion) => ( - + + + {/* Use the face count as as stand-in for the photo count */} + {t("photos_count", { + count: suggestion.cluster.faces.length, + })} + - {`${suggestion.previewFaces.length} faces ntaoheu naoehtu aosnehu asoenuh aoenuht`} Date: Tue, 15 Oct 2024 11:00:49 +0530 Subject: [PATCH 07/28] Flip --- web/packages/new/photos/components/gallery/PeopleHeader.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/packages/new/photos/components/gallery/PeopleHeader.tsx b/web/packages/new/photos/components/gallery/PeopleHeader.tsx index edc8d02646..f9976aa835 100644 --- a/web/packages/new/photos/components/gallery/PeopleHeader.tsx +++ b/web/packages/new/photos/components/gallery/PeopleHeader.tsx @@ -541,12 +541,12 @@ const SuggestionsList: React.FC = ({ ) } > - - - + + + ))} From 78a87ad6d499035ba78fedf63e080bc679dc5787 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 15 Oct 2024 11:33:14 +0530 Subject: [PATCH 08/28] Increase preview count --- web/packages/new/photos/services/ml/people.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/packages/new/photos/services/ml/people.ts b/web/packages/new/photos/services/ml/people.ts index 79320650a4..a372b175e8 100644 --- a/web/packages/new/photos/services/ml/people.ts +++ b/web/packages/new/photos/services/ml/people.ts @@ -430,7 +430,7 @@ export const suggestionsForPerson = async (person: CGroupPerson) => { previewFaces.push({ file, faceID }); - if (previewFaces.length == 3) break; + if (previewFaces.length == 4) break; } const id = cluster.id; From f9ad4c36a6387a3d3601f6868b4dcc492e675b08 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 15 Oct 2024 11:51:18 +0530 Subject: [PATCH 09/28] RestoreIcon --- .../components/gallery/PeopleHeader.tsx | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/web/packages/new/photos/components/gallery/PeopleHeader.tsx b/web/packages/new/photos/components/gallery/PeopleHeader.tsx index f9976aa835..eda19d8a29 100644 --- a/web/packages/new/photos/components/gallery/PeopleHeader.tsx +++ b/web/packages/new/photos/components/gallery/PeopleHeader.tsx @@ -32,7 +32,9 @@ import ClearIcon from "@mui/icons-material/Clear"; import EditIcon from "@mui/icons-material/Edit"; import ListAltOutlined from "@mui/icons-material/ListAltOutlined"; import MoreHoriz from "@mui/icons-material/MoreHoriz"; +import RestoreIcon from "@mui/icons-material/Restore"; import { + Box, Dialog, DialogActions, DialogContent, @@ -435,12 +437,27 @@ const SuggestionsDialog: React.FC = ({ fullScreen={isSmallWidth} PaperProps={{ sx: { minHeight: "80svh" } }} > - - - {pt("Review suggestions")} - - {person.name ?? " "} - + + + + {pt("Review suggestions")} + + + {person.name ?? " "} + + + + + + {state.activity == "fetching" ? ( @@ -516,7 +533,6 @@ const SuggestionsList: React.FC = ({ sx={{ paddingInline: 0, paddingBlockEnd: "24px", - justifyContent: "space-between", }} key={suggestion.id} From a27310b80ce4b421ab1a3cd366fd70039a52255b Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 15 Oct 2024 12:09:58 +0530 Subject: [PATCH 10/28] Toggle --- .../components/gallery/PeopleHeader.tsx | 60 ++++++++++++------- 1 file changed, 40 insertions(+), 20 deletions(-) diff --git a/web/packages/new/photos/components/gallery/PeopleHeader.tsx b/web/packages/new/photos/components/gallery/PeopleHeader.tsx index eda19d8a29..03edf6b5ce 100644 --- a/web/packages/new/photos/components/gallery/PeopleHeader.tsx +++ b/web/packages/new/photos/components/gallery/PeopleHeader.tsx @@ -30,11 +30,11 @@ import AddIcon from "@mui/icons-material/Add"; import CheckIcon from "@mui/icons-material/Check"; import ClearIcon from "@mui/icons-material/Clear"; import EditIcon from "@mui/icons-material/Edit"; -import ListAltOutlined from "@mui/icons-material/ListAltOutlined"; -import MoreHoriz from "@mui/icons-material/MoreHoriz"; +import ChecklistRtlIcon from '@mui/icons-material/ChecklistRtl'; +import ListAltOutlinedIcon from "@mui/icons-material/ListAltOutlined"; +import MoreHorizIcon from "@mui/icons-material/MoreHoriz"; import RestoreIcon from "@mui/icons-material/Restore"; import { - Box, Dialog, DialogActions, DialogContent, @@ -155,7 +155,7 @@ const CGroupPersonHeader: React.FC = ({ /> } + triggerButtonIcon={} > } @@ -173,7 +173,7 @@ const CGroupPersonHeader: React.FC = ({ {process.env.NEXT_PUBLIC_ENTE_WIP_CL /* TODO-Cluster */ && ( } + startIcon={} centerAlign onClick={showSuggestions} > @@ -235,7 +235,7 @@ const ClusterPersonHeader: React.FC = ({ } + triggerButtonIcon={} > } @@ -270,6 +270,10 @@ interface SuggestionsDialogState { * True if "fetching" failed. */ fetchFailed: boolean; + /** + * True if we should show the existing clusters. + */ + showSavedSuggestions: boolean; /** * List of clusters (suitably augmented for the UI display) which might * belong to the person, and being offered to the user as suggestions. @@ -290,12 +294,14 @@ type SuggestionsDialogAction = | { type: "fetched"; personID: string; suggestions: PersonSuggestion[] } | { type: "mark"; suggestion: PersonSuggestion; value: SuggestionMark } | { type: "save" } + | { type: "toggleHistory" } | { type: "close" }; const initialSuggestionsDialogState: SuggestionsDialogState = { activity: undefined, personID: undefined, fetchFailed: false, + showSavedSuggestions: false, suggestions: [], markedSuggestionIDs: new Map(), }; @@ -310,6 +316,7 @@ const suggestionsDialogReducer = ( activity: "fetching", personID: action.personID, fetchFailed: false, + showSavedSuggestions: false, suggestions: [], markedSuggestionIDs: new Map(), }; @@ -336,6 +343,11 @@ const suggestionsDialogReducer = ( markedSuggestionIDs, }; } + case "toggleHistory": + return { + ...state, + showSavedSuggestions: !state.showSavedSuggestions, + }; case "save": return { ...state, activity: "saving" }; case "close": @@ -416,6 +428,8 @@ const SuggestionsDialog: React.FC = ({ const handleMark = (suggestion: PersonSuggestion, value: SuggestionMark) => dispatch({ type: "mark", suggestion, value }); + const handleToggleHistory = () => dispatch({ type: "toggleHistory" }); + const handleSave = async () => { try { // TODO-Cluster @@ -437,27 +451,33 @@ const SuggestionsDialog: React.FC = ({ fullScreen={isSmallWidth} PaperProps={{ sx: { minHeight: "80svh" } }} > - + - {pt("Review suggestions")} + {state.showSavedSuggestions + ? pt("Saved suggestions") + : pt("Review suggestions")} {person.name ?? " "} - - - - + + + {state.showSavedSuggestions ? ( + + ) : ( + + )} + + + {state.activity == "fetching" ? ( From 050347762a1b3e147216abdd870cc891e457e8f9 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 15 Oct 2024 12:34:03 +0530 Subject: [PATCH 11/28] Alt --- .../components/gallery/PeopleHeader.tsx | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/web/packages/new/photos/components/gallery/PeopleHeader.tsx b/web/packages/new/photos/components/gallery/PeopleHeader.tsx index 03edf6b5ce..7f7e836326 100644 --- a/web/packages/new/photos/components/gallery/PeopleHeader.tsx +++ b/web/packages/new/photos/components/gallery/PeopleHeader.tsx @@ -30,7 +30,6 @@ import AddIcon from "@mui/icons-material/Add"; import CheckIcon from "@mui/icons-material/Check"; import ClearIcon from "@mui/icons-material/Clear"; import EditIcon from "@mui/icons-material/Edit"; -import ChecklistRtlIcon from '@mui/icons-material/ChecklistRtl'; import ListAltOutlinedIcon from "@mui/icons-material/ListAltOutlined"; import MoreHorizIcon from "@mui/icons-material/MoreHoriz"; import RestoreIcon from "@mui/icons-material/Restore"; @@ -462,21 +461,21 @@ const SuggestionsDialog: React.FC = ({ {person.name ?? " "} - theme.colors.fill.muted + : "transparent", + }} > - - {state.showSavedSuggestions ? ( - - ) : ( - - )} - - + + {state.activity == "fetching" ? ( From 3121462829f3975431b9cbf28fa2d1eb63462b1a Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 15 Oct 2024 13:11:25 +0530 Subject: [PATCH 12/28] wip checkpoint --- web/packages/new/photos/services/ml/people.ts | 66 +++++++++++++------ 1 file changed, 46 insertions(+), 20 deletions(-) diff --git a/web/packages/new/photos/services/ml/people.ts b/web/packages/new/photos/services/ml/people.ts index a372b175e8..2ec52df157 100644 --- a/web/packages/new/photos/services/ml/people.ts +++ b/web/packages/new/photos/services/ml/people.ts @@ -2,9 +2,10 @@ import { assertionFailed } from "@/base/assert"; import log from "@/base/log"; import type { EnteFile } from "@/media/file"; import { shuffled } from "@/utils/array"; +import { ensure } from "@/utils/ensure"; import { getLocalFiles } from "../files"; import { savedCGroups, type CGroup } from "../user-entity"; -import type { FaceCluster } from "./cluster"; +import { type FaceCluster } from "./cluster"; import { savedFaceClusters, savedFaceIndexes } from "./db"; import { fileIDFromFaceID } from "./face"; import { dotProduct } from "./math"; @@ -323,28 +324,44 @@ export const filterNamedPeople = (people: Person[]): NamedPerson[] => { return namedPeople; }; -export interface PersonSuggestion { +export type PreviewableCluster = FaceCluster & { /** - * The ID of the suggestion. This is the same as the cluster's ID, - * duplicated here for the UI's convenience. - */ - id: string; - /** - * The underlying {@link FaceCluster} that is being offered as the - * suggestion. - */ - cluster: FaceCluster; - /** - * A list of up to 3 "preview" faces for this cluster, each annotated with + * A list of up to 3 "preview" faces for the cluster, each annotated with * the corresponding {@link EnteFile} that contains them. */ previewFaces: PreviewableFace[]; +}; + +interface PersonSuggestionsAndChoices { + /** + * Previously saved choices. + * + * The first entry is always the primary cluster, and will have the + * {@link isPrimary} flag set. + * + * Rest of the entries are clusters (sorted by size) that the user had + * previously merged or explicitly ignored from the person under + * consideration. + * + * The ignored flag will be true for the entries that correspond to ignored + * clusters. + */ + choices: (PreviewableCluster & { + isPrimary?: boolean; + ignored?: boolean; + })[]; + /** + * New suggestions to offer to the user. + */ + suggestions: PreviewableCluster[]; } /** - * Returns suggestions for the given person. + * Returns suggestions and existing choices for the given person. */ -export const suggestionsForPerson = async (person: CGroupPerson) => { +export const suggestionsAndChoicesForPerson = async ( + person: CGroupPerson, +): Promise => { const startTime = Date.now(); const personClusters = person.cgroup.data.assigned; @@ -412,7 +429,7 @@ export const suggestionsForPerson = async (person: CGroupPerson) => { const files = await getLocalFiles("normal"); const fileByID = new Map(files.map((f) => [f.id, f])); - const suggestions = suggestedClusters.map((cluster) => { + const toPreviewable = (cluster: FaceCluster) => { const previewFaces: PreviewableFace[] = []; for (const faceID of cluster.faces) { const fileID = fileIDFromFaceID(faceID); @@ -433,13 +450,22 @@ export const suggestionsForPerson = async (person: CGroupPerson) => { if (previewFaces.length == 4) break; } - const id = cluster.id; - return { id, cluster, previewFaces }; - }); + return { ...cluster, previewFaces }; + }; + + const choices = [ + { ...toPreviewable(ensure(personClusters[0])), isPrimary: true }, + ...personClusters.slice(1).map(toPreviewable), + ...ignoredClusters + .map(toPreviewable) + .map((p) => ({ ...p, ignored: true })), + ]; + + const suggestions = suggestedClusters.map(toPreviewable); log.info( `Generated ${suggestions.length} suggestions for ${person.id} (${Date.now() - startTime} ms)`, ); - return suggestions; + return { choices, suggestions }; }; From f89c03318ab7094f9021cb430958a5ba3939520b Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 15 Oct 2024 13:21:32 +0530 Subject: [PATCH 13/28] wip checkpoint --- .../components/gallery/PeopleHeader.tsx | 77 ++++++++++--------- web/packages/new/photos/services/ml/people.ts | 2 +- 2 files changed, 42 insertions(+), 37 deletions(-) diff --git a/web/packages/new/photos/components/gallery/PeopleHeader.tsx b/web/packages/new/photos/components/gallery/PeopleHeader.tsx index 7f7e836326..14c5a2d40d 100644 --- a/web/packages/new/photos/components/gallery/PeopleHeader.tsx +++ b/web/packages/new/photos/components/gallery/PeopleHeader.tsx @@ -12,16 +12,15 @@ import { import { useIsSmallWidth } from "@/base/hooks"; import { pt } from "@/base/i18n"; import log from "@/base/log"; +import { deleteCGroup, renameCGroup } from "@/new/photos/services/ml"; import { - deleteCGroup, - renameCGroup, - suggestionsForPerson, -} from "@/new/photos/services/ml"; -import { + suggestionsAndChoicesForPerson, type CGroupPerson, type ClusterPerson, type Person, type PersonSuggestion, + type PersonSuggestionsAndChoices, + type PreviewableCluster, } from "@/new/photos/services/ml/people"; import { wait } from "@/utils/promise"; import OverflowMenu from "@ente/shared/components/OverflowMenu/menu"; @@ -270,28 +269,37 @@ interface SuggestionsDialogState { */ fetchFailed: boolean; /** - * True if we should show the existing clusters. + * True if we should show the previously saved choice view. */ - showSavedSuggestions: boolean; + showSavedChoices: boolean; /** * List of clusters (suitably augmented for the UI display) which might * belong to the person, and being offered to the user as suggestions. */ suggestions: PersonSuggestion[]; /** - * An entry corresponding to each clusters (suggestions) that the user has - * either explicitly accepted or rejected. + * List of previously saved choices (suitabley augmented for UI display). */ - markedSuggestionIDs: Map>; + savedChoices: PersonSuggestionsAndChoices["choices"]; + /** + * An entry corresponding to each + * - suggestions that the user has either explicitly accepted or rejected. + * - saved choice for which the user has changed their mind. + */ + markedClusterIDs: Map>; } -type SuggestionMark = "yes" | "no" | undefined; +type ClusterMark = "yes" | "no" | undefined; type SuggestionsDialogAction = | { type: "fetch"; personID: string } | { type: "fetchFailed"; personID: string } - | { type: "fetched"; personID: string; suggestions: PersonSuggestion[] } - | { type: "mark"; suggestion: PersonSuggestion; value: SuggestionMark } + | { + type: "fetched"; + personID: string; + suggestionsAndChoices: PersonSuggestionsAndChoices; + } + | { type: "mark"; suggestion: PreviewableCluster; value: ClusterMark } | { type: "save" } | { type: "toggleHistory" } | { type: "close" }; @@ -300,9 +308,10 @@ const initialSuggestionsDialogState: SuggestionsDialogState = { activity: undefined, personID: undefined, fetchFailed: false, - showSavedSuggestions: false, + showSavedChoices: false, + savedChoices: [], suggestions: [], - markedSuggestionIDs: new Map(), + markedClusterIDs: new Map(), }; const suggestionsDialogReducer = ( @@ -312,12 +321,9 @@ const suggestionsDialogReducer = ( switch (action.type) { case "fetch": return { + ...initialSuggestionsDialogState, activity: "fetching", personID: action.personID, - fetchFailed: false, - showSavedSuggestions: false, - suggestions: [], - markedSuggestionIDs: new Map(), }; case "fetchFailed": if (action.personID != state.personID) return state; @@ -327,25 +333,23 @@ const suggestionsDialogReducer = ( return { ...state, activity: undefined, - suggestions: action.suggestions, + suggestions: action.suggestionsAndChoices.suggestions, + savedChoices: action.suggestionsAndChoices.choices, }; case "mark": { - const markedSuggestionIDs = new Map(state.markedSuggestionIDs); + const markedClusterIDs = new Map(state.markedClusterIDs); const id = action.suggestion.id; if (action.value == "yes" || action.value == "no") { - markedSuggestionIDs.set(id, action.value); + markedClusterIDs.set(id, action.value); } else { - markedSuggestionIDs.delete(id); + markedClusterIDs.delete(id); } - return { - ...state, - markedSuggestionIDs, - }; + return { ...state, markedClusterIDs }; } case "toggleHistory": return { ...state, - showSavedSuggestions: !state.showSavedSuggestions, + showSavedChoices: !state.showSavedChoices, }; case "save": return { ...state, activity: "saving" }; @@ -374,7 +378,7 @@ const SuggestionsDialog: React.FC = ({ const isSmallWidth = useIsSmallWidth(); - const hasUnsavedChanges = state.markedSuggestionIDs.size > 0; + const hasUnsavedChanges = state.markedClusterIDs.size > 0; const resetPersonAndClose = () => { dispatch({ type: "close" }); @@ -394,8 +398,9 @@ const SuggestionsDialog: React.FC = ({ const go = async () => { try { - const suggestions = await suggestionsForPerson(person); - dispatch({ type: "fetched", personID, suggestions }); + const { choices, suggestions } = + await suggestionsAndChoicesForPerson(person); + dispatch({ type: "fetched", personID, choices, suggestions }); } catch (e) { log.error("Failed to generate suggestions", e); dispatch({ type: "fetchFailed", personID }); @@ -424,7 +429,7 @@ const SuggestionsDialog: React.FC = ({ resetPersonAndClose(); }; - const handleMark = (suggestion: PersonSuggestion, value: SuggestionMark) => + const handleMark = (suggestion: PersonSuggestion, value: ClusterMark) => dispatch({ type: "mark", suggestion, value }); const handleToggleHistory = () => dispatch({ type: "toggleHistory" }); @@ -453,7 +458,7 @@ const SuggestionsDialog: React.FC = ({ - {state.showSavedSuggestions + {state.showSavedChoices ? pt("Saved suggestions") : pt("Review suggestions")} @@ -464,12 +469,12 @@ const SuggestionsDialog: React.FC = ({ theme.colors.fill.muted : "transparent", }} @@ -537,7 +542,7 @@ type SuggestionsListProps = Pick< */ onMarkSuggestion: ( suggestion: PersonSuggestion, - value: SuggestionMark, + value: ClusterMark, ) => void; }; diff --git a/web/packages/new/photos/services/ml/people.ts b/web/packages/new/photos/services/ml/people.ts index 2ec52df157..05e9056af2 100644 --- a/web/packages/new/photos/services/ml/people.ts +++ b/web/packages/new/photos/services/ml/people.ts @@ -332,7 +332,7 @@ export type PreviewableCluster = FaceCluster & { previewFaces: PreviewableFace[]; }; -interface PersonSuggestionsAndChoices { +export interface PersonSuggestionsAndChoices { /** * Previously saved choices. * From a14db238736b9fef6d75b825f1439fac2dce79f8 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 15 Oct 2024 13:28:23 +0530 Subject: [PATCH 14/28] wip fin --- .../components/gallery/PeopleHeader.tsx | 53 ++++++++----------- 1 file changed, 22 insertions(+), 31 deletions(-) diff --git a/web/packages/new/photos/components/gallery/PeopleHeader.tsx b/web/packages/new/photos/components/gallery/PeopleHeader.tsx index 14c5a2d40d..50798161a5 100644 --- a/web/packages/new/photos/components/gallery/PeopleHeader.tsx +++ b/web/packages/new/photos/components/gallery/PeopleHeader.tsx @@ -18,9 +18,7 @@ import { type CGroupPerson, type ClusterPerson, type Person, - type PersonSuggestion, type PersonSuggestionsAndChoices, - type PreviewableCluster, } from "@/new/photos/services/ml/people"; import { wait } from "@/utils/promise"; import OverflowMenu from "@ente/shared/components/OverflowMenu/menu"; @@ -276,7 +274,7 @@ interface SuggestionsDialogState { * List of clusters (suitably augmented for the UI display) which might * belong to the person, and being offered to the user as suggestions. */ - suggestions: PersonSuggestion[]; + suggestions: PersonSuggestionsAndChoices["suggestions"]; /** * List of previously saved choices (suitabley augmented for UI display). */ @@ -299,7 +297,7 @@ type SuggestionsDialogAction = personID: string; suggestionsAndChoices: PersonSuggestionsAndChoices; } - | { type: "mark"; suggestion: PreviewableCluster; value: ClusterMark } + | { type: "mark"; clusterID: string; value: ClusterMark } | { type: "save" } | { type: "toggleHistory" } | { type: "close" }; @@ -338,7 +336,7 @@ const suggestionsDialogReducer = ( }; case "mark": { const markedClusterIDs = new Map(state.markedClusterIDs); - const id = action.suggestion.id; + const id = action.clusterID; if (action.value == "yes" || action.value == "no") { markedClusterIDs.set(id, action.value); } else { @@ -398,11 +396,11 @@ const SuggestionsDialog: React.FC = ({ const go = async () => { try { - const { choices, suggestions } = + const suggestionsAndChoices = await suggestionsAndChoicesForPerson(person); - dispatch({ type: "fetched", personID, choices, suggestions }); + dispatch({ type: "fetched", personID, suggestionsAndChoices }); } catch (e) { - log.error("Failed to generate suggestions", e); + log.error("Failed to fetch suggestions and choices", e); dispatch({ type: "fetchFailed", personID }); } }; @@ -429,10 +427,8 @@ const SuggestionsDialog: React.FC = ({ resetPersonAndClose(); }; - const handleMark = (suggestion: PersonSuggestion, value: ClusterMark) => - dispatch({ type: "mark", suggestion, value }); - - const handleToggleHistory = () => dispatch({ type: "toggleHistory" }); + const handleMark = (clusterID: string, value: ClusterMark) => + dispatch({ type: "mark", clusterID, value }); const handleSave = async () => { try { @@ -467,7 +463,7 @@ const SuggestionsDialog: React.FC = ({ dispatch({ type: "toggleHistory" })} aria-label={ !state.showSavedChoices ? pt("Saved suggestions") @@ -505,8 +501,8 @@ const SuggestionsDialog: React.FC = ({ ) : ( )} @@ -534,24 +530,21 @@ const SuggestionsDialog: React.FC = ({ type SuggestionsListProps = Pick< SuggestionsDialogState, - "suggestions" | "markedSuggestionIDs" + "suggestions" | "markedClusterIDs" > & { /** - * Callback invoked when the user toggles the value associated with the - * given suggestion. + * Callback invoked when the user changes the value associated with the + * given cluster. */ - onMarkSuggestion: ( - suggestion: PersonSuggestion, - value: ClusterMark, - ) => void; + onMarkCluster: (clusterID: string, value: ClusterMark) => void; }; const SuggestionsList: React.FC = ({ suggestions, - markedSuggestionIDs, - onMarkSuggestion, + markedClusterIDs, + onMarkCluster, }) => ( - + {suggestions.map((suggestion) => ( = ({ {/* Use the face count as as stand-in for the photo count */} - {t("photos_count", { - count: suggestion.cluster.faces.length, - })} + {t("photos_count", { count: suggestion.faces.length })} - onMarkSuggestion( - suggestion, + onMarkCluster( + suggestion.id, // Dance for TypeScript to recognize the type. v == "yes" ? "yes" : v == "no" ? "no" : undefined, ) From c629d66d6722fb54adb445f5e16d0a0c7204c0a9 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 15 Oct 2024 13:53:47 +0530 Subject: [PATCH 15/28] List order is not reflective of primary --- web/packages/new/photos/services/ml/people.ts | 47 +++++++++++-------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/web/packages/new/photos/services/ml/people.ts b/web/packages/new/photos/services/ml/people.ts index 05e9056af2..a0d5a42c94 100644 --- a/web/packages/new/photos/services/ml/people.ts +++ b/web/packages/new/photos/services/ml/people.ts @@ -336,20 +336,20 @@ export interface PersonSuggestionsAndChoices { /** * Previously saved choices. * - * The first entry is always the primary cluster, and will have the - * {@link isPrimary} flag set. - * - * Rest of the entries are clusters (sorted by size) that the user had - * previously merged or explicitly ignored from the person under - * consideration. + * These are clusters (sorted by size) that the user had previously merged + * or explicitly ignored from the person under consideration. * * The ignored flag will be true for the entries that correspond to ignored * clusters. + * + * This array is guaranteed to be non-empty, and it is guaranteed that the + * first item is a merged cluster (i.e. a cluster for which the ignored flag + * is not set), even if there exists an ignored cluster with a larger size. + * The rest of the entries are intermixed and sorted by size normally. + * + * The first entry will have the {@link fixed} flag set. */ - choices: (PreviewableCluster & { - isPrimary?: boolean; - ignored?: boolean; - })[]; + choices: (PreviewableCluster & { fixed?: boolean, ignored?: boolean })[]; /** * New suggestions to offer to the user. */ @@ -421,8 +421,6 @@ export const suggestionsAndChoicesForPerson = async ( if (suggest) suggestedClusters.push(cluster); } - suggestedClusters.sort((a, b) => b.faces.length - a.faces.length); - // Annotate the clusters with the information that the UI needs to show its // preview faces. @@ -453,14 +451,25 @@ export const suggestionsAndChoicesForPerson = async ( return { ...cluster, previewFaces }; }; - const choices = [ - { ...toPreviewable(ensure(personClusters[0])), isPrimary: true }, - ...personClusters.slice(1).map(toPreviewable), - ...ignoredClusters - .map(toPreviewable) - .map((p) => ({ ...p, ignored: true })), - ]; + const sortBySize = (entries: { faces: unknown[] }[]) => + entries.sort((a, b) => b.faces.length - a.faces.length); + const acceptedChoices = personClusters.map(toPreviewable); + sortBySize(acceptedChoices); + + const ignoredChoices = ignoredClusters + .map(toPreviewable) + .map((p) => ({ ...p, ignored: true })); + + // Ensure that the first item in the choices is not an ignored one, even if + // that is what we'd have ended up with if we sorted by size. + const firstChoice = {...ensure(acceptedChoices[0]), fixed: true}; + const restChoices = acceptedChoices.concat(ignoredChoices); + sortBySize(restChoices); + + const choices = [firstChoice, ...restChoices]; + + sortBySize(suggestedClusters); const suggestions = suggestedClusters.map(toPreviewable); log.info( From 3298cb6c14adab869fdda511d2ba066ec7759a9a Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 15 Oct 2024 14:01:15 +0530 Subject: [PATCH 16/28] wip checkpoint --- web/packages/new/photos/services/ml/people.ts | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/web/packages/new/photos/services/ml/people.ts b/web/packages/new/photos/services/ml/people.ts index a0d5a42c94..7d0bf5613d 100644 --- a/web/packages/new/photos/services/ml/people.ts +++ b/web/packages/new/photos/services/ml/people.ts @@ -339,17 +339,18 @@ export interface PersonSuggestionsAndChoices { * These are clusters (sorted by size) that the user had previously merged * or explicitly ignored from the person under consideration. * - * The ignored flag will be true for the entries that correspond to ignored - * clusters. + * The {@link accepted} flag will true for the entries that correspond to + * accepted clusters, and false for those that the user had ignored. * * This array is guaranteed to be non-empty, and it is guaranteed that the - * first item is a merged cluster (i.e. a cluster for which the ignored flag - * is not set), even if there exists an ignored cluster with a larger size. - * The rest of the entries are intermixed and sorted by size normally. + * first item is a merged cluster (i.e. a cluster for which accepted is + * true), even if there exists an ignored cluster with a larger size. The + * rest of the entries are intermixed and sorted by size normally. * - * The first entry will have the {@link fixed} flag set. + * For convenience of the UI, teh first entry will have also have the + * {@link fixed} flag set. */ - choices: (PreviewableCluster & { fixed?: boolean, ignored?: boolean })[]; + choices: (PreviewableCluster & { fixed?: boolean; accepted: boolean })[]; /** * New suggestions to offer to the user. */ @@ -454,16 +455,20 @@ export const suggestionsAndChoicesForPerson = async ( const sortBySize = (entries: { faces: unknown[] }[]) => entries.sort((a, b) => b.faces.length - a.faces.length); - const acceptedChoices = personClusters.map(toPreviewable); + const acceptedChoices = personClusters + .map(toPreviewable) + .map((p) => ({ ...p, accepted: true })); + sortBySize(acceptedChoices); const ignoredChoices = ignoredClusters .map(toPreviewable) - .map((p) => ({ ...p, ignored: true })); + .map((p) => ({ ...p, accepted: false })); // Ensure that the first item in the choices is not an ignored one, even if // that is what we'd have ended up with if we sorted by size. - const firstChoice = {...ensure(acceptedChoices[0]), fixed: true}; + + const firstChoice = { ...ensure(acceptedChoices[0]), fixed: true }; const restChoices = acceptedChoices.concat(ignoredChoices); sortBySize(restChoices); From ebab2e0387a99d33f8dcf50da63b5984074f5dad Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 15 Oct 2024 14:11:39 +0530 Subject: [PATCH 17/28] wip checkpoint --- .../components/gallery/PeopleHeader.tsx | 64 +++++++++---------- web/packages/new/photos/services/ml/people.ts | 3 +- 2 files changed, 34 insertions(+), 33 deletions(-) diff --git a/web/packages/new/photos/components/gallery/PeopleHeader.tsx b/web/packages/new/photos/components/gallery/PeopleHeader.tsx index 50798161a5..8977cd6a2c 100644 --- a/web/packages/new/photos/components/gallery/PeopleHeader.tsx +++ b/web/packages/new/photos/components/gallery/PeopleHeader.tsx @@ -19,6 +19,7 @@ import { type ClusterPerson, type Person, type PersonSuggestionsAndChoices, + type PreviewableCluster, } from "@/new/photos/services/ml/people"; import { wait } from "@/utils/promise"; import OverflowMenu from "@ente/shared/components/OverflowMenu/menu"; @@ -306,8 +307,8 @@ const initialSuggestionsDialogState: SuggestionsDialogState = { activity: undefined, personID: undefined, fetchFailed: false, - showSavedChoices: false, - savedChoices: [], + showChoices: false, + choices: [], suggestions: [], markedClusterIDs: new Map(), }; @@ -528,58 +529,57 @@ const SuggestionsDialog: React.FC = ({ ); }; -type SuggestionsListProps = Pick< - SuggestionsDialogState, - "suggestions" | "markedClusterIDs" -> & { +interface MarkedClusterListProps { + clusters: (PreviewableCluster & { fixed?: boolean; mark: ClusterMark })[]; /** * Callback invoked when the user changes the value associated with the * given cluster. */ onMarkCluster: (clusterID: string, value: ClusterMark) => void; -}; +} -const SuggestionsList: React.FC = ({ - suggestions, - markedClusterIDs, +const MarkedClusterList: React.FC = ({ + clusters, onMarkCluster, }) => ( - {suggestions.map((suggestion) => ( + {clusters.map((cluster) => ( {/* Use the face count as as stand-in for the photo count */} - {t("photos_count", { count: suggestion.faces.length })} + {t("photos_count", { count: cluster.faces.length })} - + - - onMarkCluster( - suggestion.id, - // Dance for TypeScript to recognize the type. - v == "yes" ? "yes" : v == "no" ? "no" : undefined, - ) - } - > - - - - - - - + {!cluster.fixed && ( + + onMarkCluster(cluster.id, toClusterMark(v)) + } + > + + + + + + + + )} ))} ); + +// Dance for TypeScript to recognize the type. +const toClusterMark = (v: unknown) => + v == "yes" ? "yes" : v == "no" ? "no" : undefined; diff --git a/web/packages/new/photos/services/ml/people.ts b/web/packages/new/photos/services/ml/people.ts index 7d0bf5613d..1f23243f27 100644 --- a/web/packages/new/photos/services/ml/people.ts +++ b/web/packages/new/photos/services/ml/people.ts @@ -475,7 +475,8 @@ export const suggestionsAndChoicesForPerson = async ( const choices = [firstChoice, ...restChoices]; sortBySize(suggestedClusters); - const suggestions = suggestedClusters.map(toPreviewable); + // Limit to the number of suggestions shown in a single go. + const suggestions = suggestedClusters.slice(0, 80).map(toPreviewable); log.info( `Generated ${suggestions.length} suggestions for ${person.id} (${Date.now() - startTime} ms)`, From 520777083bb7c7f709ff2f509d2ff4a1ec060212 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 15 Oct 2024 14:48:16 +0530 Subject: [PATCH 18/28] Use --- .../components/gallery/PeopleHeader.tsx | 83 ++++++++++++------- 1 file changed, 55 insertions(+), 28 deletions(-) diff --git a/web/packages/new/photos/components/gallery/PeopleHeader.tsx b/web/packages/new/photos/components/gallery/PeopleHeader.tsx index 8977cd6a2c..04f3b5de1f 100644 --- a/web/packages/new/photos/components/gallery/PeopleHeader.tsx +++ b/web/packages/new/photos/components/gallery/PeopleHeader.tsx @@ -268,18 +268,19 @@ interface SuggestionsDialogState { */ fetchFailed: boolean; /** - * True if we should show the previously saved choice view. + * True if we should show the previously saved choice view instead of the + * new suggestions. */ - showSavedChoices: boolean; + showChoices: boolean; /** - * List of clusters (suitably augmented for the UI display) which might - * belong to the person, and being offered to the user as suggestions. + * The underlying data we fetched. */ - suggestions: PersonSuggestionsAndChoices["suggestions"]; + suggestionsAndChoices: PersonSuggestionsAndChoices | undefined; /** - * List of previously saved choices (suitabley augmented for UI display). + * The list of markable clusters derived from {@link suggestionsAndChoices} + * and other UI state. */ - savedChoices: PersonSuggestionsAndChoices["choices"]; + markableClusters: MarkableCluster[]; /** * An entry corresponding to each * - suggestions that the user has either explicitly accepted or rejected. @@ -290,6 +291,11 @@ interface SuggestionsDialogState { type ClusterMark = "yes" | "no" | undefined; +type MarkableCluster = PreviewableCluster & { + fixed?: boolean; + mark: ClusterMark; +}; + type SuggestionsDialogAction = | { type: "fetch"; personID: string } | { type: "fetchFailed"; personID: string } @@ -308,8 +314,8 @@ const initialSuggestionsDialogState: SuggestionsDialogState = { personID: undefined, fetchFailed: false, showChoices: false, - choices: [], - suggestions: [], + suggestionsAndChoices: undefined, + markableClusters: [], markedClusterIDs: new Map(), }; @@ -317,6 +323,29 @@ const suggestionsDialogReducer = ( state: SuggestionsDialogState, action: SuggestionsDialogAction, ): SuggestionsDialogState => { + const updatingMarkableClusters = (s: SuggestionsDialogState) => { + let markableClusters: MarkableCluster[]; + if (s.showChoices) { + markableClusters = (s.suggestionsAndChoices?.suggestions ?? []).map( + (c) => { + return { ...c, mark: s.markedClusterIDs.get(c.id) }; + }, + ); + } else { + markableClusters = (s.suggestionsAndChoices?.choices ?? []).map( + (c) => { + return { + ...c, + mark: + s.markedClusterIDs.get(c.id) ?? + (c.accepted ? "yes" : "no"), + }; + }, + ); + } + return { ...s, markableClusters }; + }; + switch (action.type) { case "fetch": return { @@ -329,12 +358,11 @@ const suggestionsDialogReducer = ( return { ...state, activity: undefined, fetchFailed: true }; case "fetched": if (action.personID != state.personID) return state; - return { + return updatingMarkableClusters({ ...state, activity: undefined, - suggestions: action.suggestionsAndChoices.suggestions, - savedChoices: action.suggestionsAndChoices.choices, - }; + suggestionsAndChoices: action.suggestionsAndChoices, + }); case "mark": { const markedClusterIDs = new Map(state.markedClusterIDs); const id = action.clusterID; @@ -343,13 +371,13 @@ const suggestionsDialogReducer = ( } else { markedClusterIDs.delete(id); } - return { ...state, markedClusterIDs }; + return updatingMarkableClusters({ ...state, markedClusterIDs }); } case "toggleHistory": - return { + return updatingMarkableClusters({ ...state, - showSavedChoices: !state.showSavedChoices, - }; + showChoices: !state.showChoices, + }); case "save": return { ...state, activity: "saving" }; case "close": @@ -455,7 +483,7 @@ const SuggestionsDialog: React.FC = ({ - {state.showSavedChoices + {state.showChoices ? pt("Saved suggestions") : pt("Review suggestions")} @@ -466,12 +494,12 @@ const SuggestionsDialog: React.FC = ({ dispatch({ type: "toggleHistory" })} aria-label={ - !state.showSavedChoices + !state.showChoices ? pt("Saved suggestions") : pt("Review suggestions") } sx={{ - backgroundColor: state.showSavedChoices + backgroundColor: state.showChoices ? (theme) => theme.colors.fill.muted : "transparent", }} @@ -490,7 +518,7 @@ const SuggestionsDialog: React.FC = ({ - ) : state.suggestions.length == 0 ? ( + ) : !state.showChoices && state.markableClusters.length == 0 ? ( = ({ ) : ( - )} @@ -529,16 +556,16 @@ const SuggestionsDialog: React.FC = ({ ); }; -interface MarkedClusterListProps { +interface MarkableClusterListProps { clusters: (PreviewableCluster & { fixed?: boolean; mark: ClusterMark })[]; /** * Callback invoked when the user changes the value associated with the * given cluster. */ - onMarkCluster: (clusterID: string, value: ClusterMark) => void; + onMarkCluster: (id: string, value: ClusterMark) => void; } -const MarkedClusterList: React.FC = ({ +const MarkableClusterList: React.FC = ({ clusters, onMarkCluster, }) => ( @@ -561,7 +588,7 @@ const MarkedClusterList: React.FC = ({ {!cluster.fixed && ( onMarkCluster(cluster.id, toClusterMark(v)) From b21639059e63b1eadc65f17307783c8799365349 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 15 Oct 2024 15:14:56 +0530 Subject: [PATCH 19/28] Rework wip --- .../components/gallery/PeopleHeader.tsx | 134 ++++++++---------- 1 file changed, 57 insertions(+), 77 deletions(-) diff --git a/web/packages/new/photos/components/gallery/PeopleHeader.tsx b/web/packages/new/photos/components/gallery/PeopleHeader.tsx index 04f3b5de1f..9191b0257f 100644 --- a/web/packages/new/photos/components/gallery/PeopleHeader.tsx +++ b/web/packages/new/photos/components/gallery/PeopleHeader.tsx @@ -272,29 +272,21 @@ interface SuggestionsDialogState { * new suggestions. */ showChoices: boolean; - /** - * The underlying data we fetched. - */ - suggestionsAndChoices: PersonSuggestionsAndChoices | undefined; - /** - * The list of markable clusters derived from {@link suggestionsAndChoices} - * and other UI state. - */ - markableClusters: MarkableCluster[]; + /** Fetched choices. */ + choices: SCItem[]; + /** Fetched suggestions. */ + suggestions: SCItem[]; /** * An entry corresponding to each - * - suggestions that the user has either explicitly accepted or rejected. * - saved choice for which the user has changed their mind. + * - suggestion that the user has either explicitly accepted or rejected. */ - markedClusterIDs: Map>; + marks: Map; } -type ClusterMark = "yes" | "no" | undefined; +type SCItem = PreviewableCluster & { fixed?: boolean; accepted?: boolean }; -type MarkableCluster = PreviewableCluster & { - fixed?: boolean; - mark: ClusterMark; -}; +type SCMark = "yes" | "no" | undefined; type SuggestionsDialogAction = | { type: "fetch"; personID: string } @@ -304,7 +296,7 @@ type SuggestionsDialogAction = personID: string; suggestionsAndChoices: PersonSuggestionsAndChoices; } - | { type: "mark"; clusterID: string; value: ClusterMark } + | { type: "mark"; item: SCItem; value: SCMark } | { type: "save" } | { type: "toggleHistory" } | { type: "close" }; @@ -314,42 +306,22 @@ const initialSuggestionsDialogState: SuggestionsDialogState = { personID: undefined, fetchFailed: false, showChoices: false, - suggestionsAndChoices: undefined, - markableClusters: [], - markedClusterIDs: new Map(), + choices: [], + suggestions: [], + marks: new Map(), }; const suggestionsDialogReducer = ( state: SuggestionsDialogState, action: SuggestionsDialogAction, ): SuggestionsDialogState => { - const updatingMarkableClusters = (s: SuggestionsDialogState) => { - let markableClusters: MarkableCluster[]; - if (s.showChoices) { - markableClusters = (s.suggestionsAndChoices?.suggestions ?? []).map( - (c) => { - return { ...c, mark: s.markedClusterIDs.get(c.id) }; - }, - ); - } else { - markableClusters = (s.suggestionsAndChoices?.choices ?? []).map( - (c) => { - return { - ...c, - mark: - s.markedClusterIDs.get(c.id) ?? - (c.accepted ? "yes" : "no"), - }; - }, - ); - } - return { ...s, markableClusters }; - }; - switch (action.type) { case "fetch": return { ...initialSuggestionsDialogState, + choices: [], + suggestions: [], + marks: new Map(), activity: "fetching", personID: action.personID, }; @@ -358,26 +330,26 @@ const suggestionsDialogReducer = ( return { ...state, activity: undefined, fetchFailed: true }; case "fetched": if (action.personID != state.personID) return state; - return updatingMarkableClusters({ + return { ...state, activity: undefined, - suggestionsAndChoices: action.suggestionsAndChoices, - }); + choices: action.suggestionsAndChoices.choices, + suggestions: action.suggestionsAndChoices.suggestions, + }; case "mark": { - const markedClusterIDs = new Map(state.markedClusterIDs); - const id = action.clusterID; - if (action.value == "yes" || action.value == "no") { - markedClusterIDs.set(id, action.value); + const marks = new Map(state.marks); + const { item, value } = action; + // If this was a new suggestion, prune off marks created as a result + // of the user toggling the item back to its original unset state. + if (item.accepted === undefined && value === undefined) { + marks.delete(item.id); } else { - markedClusterIDs.delete(id); + marks.set(item.id, value); } - return updatingMarkableClusters({ ...state, markedClusterIDs }); + return { ...state, marks }; } case "toggleHistory": - return updatingMarkableClusters({ - ...state, - showChoices: !state.showChoices, - }); + return { ...state, showChoices: !state.showChoices }; case "save": return { ...state, activity: "saving" }; case "close": @@ -405,7 +377,7 @@ const SuggestionsDialog: React.FC = ({ const isSmallWidth = useIsSmallWidth(); - const hasUnsavedChanges = state.markedClusterIDs.size > 0; + const hasUnsavedChanges = state.marks.size > 0; const resetPersonAndClose = () => { dispatch({ type: "close" }); @@ -456,8 +428,8 @@ const SuggestionsDialog: React.FC = ({ resetPersonAndClose(); }; - const handleMark = (clusterID: string, value: ClusterMark) => - dispatch({ type: "mark", clusterID, value }); + const handleMark = (item: SCItem, value: SCMark) => + dispatch({ type: "mark", item, value }); const handleSave = async () => { try { @@ -518,7 +490,13 @@ const SuggestionsDialog: React.FC = ({ - ) : !state.showChoices && state.markableClusters.length == 0 ? ( + ) : state.showChoices ? ( + + ) : state.suggestions.length == 0 ? ( = ({ ) : ( - )} @@ -556,23 +535,24 @@ const SuggestionsDialog: React.FC = ({ ); }; -interface MarkableClusterListProps { - clusters: (PreviewableCluster & { fixed?: boolean; mark: ClusterMark })[]; +interface SuggestionOrChoiceListProps { + items: SCItem[]; + marks: Map; /** * Callback invoked when the user changes the value associated with the - * given cluster. + * given suggestion or choice. */ - onMarkCluster: (id: string, value: ClusterMark) => void; + onMarkItem: (item: SCItem, value: SCMark) => void; } -const MarkableClusterList: React.FC = ({ - clusters, - onMarkCluster, +const SuggestionOrChoiceList: React.FC = ({ + items, + onMarkItem: onMarkCluster, }) => ( - {clusters.map((cluster) => ( + {items.map((item) => ( = ({ {/* Use the face count as as stand-in for the photo count */} - {t("photos_count", { count: cluster.faces.length })} + {t("photos_count", { count: item.faces.length })} - + - {!cluster.fixed && ( + {!item.fixed && ( - onMarkCluster(cluster.id, toClusterMark(v)) + onMarkCluster(item.id, toClusterMark(v)) } > From 865b5f3c8f460c66584922bf5fe20901afcc2174 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 15 Oct 2024 15:30:22 +0530 Subject: [PATCH 20/28] rework fin --- .../new/photos/components/gallery/PeopleHeader.tsx | 14 ++++++++------ web/packages/new/photos/services/ml/people.ts | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/web/packages/new/photos/components/gallery/PeopleHeader.tsx b/web/packages/new/photos/components/gallery/PeopleHeader.tsx index 9191b0257f..e973e1396d 100644 --- a/web/packages/new/photos/components/gallery/PeopleHeader.tsx +++ b/web/packages/new/photos/components/gallery/PeopleHeader.tsx @@ -547,7 +547,8 @@ interface SuggestionOrChoiceListProps { const SuggestionOrChoiceList: React.FC = ({ items, - onMarkItem: onMarkCluster, + marks, + onMarkItem, }) => ( {items.map((item) => ( @@ -568,11 +569,9 @@ const SuggestionOrChoiceList: React.FC = ({ {!item.fixed && ( - onMarkCluster(item.id, toClusterMark(v)) - } + onChange={(_, v) => onMarkItem(item, toClusterMark(v))} > @@ -587,6 +586,9 @@ const SuggestionOrChoiceList: React.FC = ({ ); -// Dance for TypeScript to recognize the type. +const toItemValue = (accepted?: boolean) => + accepted ? "yes" : accepted === false ? "no" : undefined; + const toClusterMark = (v: unknown) => + // This dance is needed for TypeScript to recognize the type. v == "yes" ? "yes" : v == "no" ? "no" : undefined; diff --git a/web/packages/new/photos/services/ml/people.ts b/web/packages/new/photos/services/ml/people.ts index 1f23243f27..23f86483b5 100644 --- a/web/packages/new/photos/services/ml/people.ts +++ b/web/packages/new/photos/services/ml/people.ts @@ -469,7 +469,7 @@ export const suggestionsAndChoicesForPerson = async ( // that is what we'd have ended up with if we sorted by size. const firstChoice = { ...ensure(acceptedChoices[0]), fixed: true }; - const restChoices = acceptedChoices.concat(ignoredChoices); + const restChoices = acceptedChoices.slice(1).concat(ignoredChoices); sortBySize(restChoices); const choices = [firstChoice, ...restChoices]; From 6b05782446981c8016c0a56fdee262a01121235b Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 15 Oct 2024 15:37:01 +0530 Subject: [PATCH 21/28] Show option only if there is something to revert --- .../components/gallery/PeopleHeader.tsx | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/web/packages/new/photos/components/gallery/PeopleHeader.tsx b/web/packages/new/photos/components/gallery/PeopleHeader.tsx index e973e1396d..809da0aef3 100644 --- a/web/packages/new/photos/components/gallery/PeopleHeader.tsx +++ b/web/packages/new/photos/components/gallery/PeopleHeader.tsx @@ -463,21 +463,23 @@ const SuggestionsDialog: React.FC = ({ {person.name ?? " "} - dispatch({ type: "toggleHistory" })} - aria-label={ - !state.showChoices - ? pt("Saved suggestions") - : pt("Review suggestions") - } - sx={{ - backgroundColor: state.showChoices - ? (theme) => theme.colors.fill.muted - : "transparent", - }} - > - - + {state.choices.length > 1 && ( + dispatch({ type: "toggleHistory" })} + aria-label={ + !state.showChoices + ? pt("Saved suggestions") + : pt("Review suggestions") + } + sx={{ + backgroundColor: state.showChoices + ? (theme) => theme.colors.fill.muted + : "transparent", + }} + > + + + )} {state.activity == "fetching" ? ( From a745b69ebe09ecf1501d9964740dc04f873c7aa0 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 15 Oct 2024 15:50:52 +0530 Subject: [PATCH 22/28] State updates --- .../components/gallery/PeopleHeader.tsx | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/web/packages/new/photos/components/gallery/PeopleHeader.tsx b/web/packages/new/photos/components/gallery/PeopleHeader.tsx index 809da0aef3..a72b131473 100644 --- a/web/packages/new/photos/components/gallery/PeopleHeader.tsx +++ b/web/packages/new/photos/components/gallery/PeopleHeader.tsx @@ -281,13 +281,11 @@ interface SuggestionsDialogState { * - saved choice for which the user has changed their mind. * - suggestion that the user has either explicitly accepted or rejected. */ - marks: Map; + marks: Map; } type SCItem = PreviewableCluster & { fixed?: boolean; accepted?: boolean }; -type SCMark = "yes" | "no" | undefined; - type SuggestionsDialogAction = | { type: "fetch"; personID: string } | { type: "fetchFailed"; personID: string } @@ -296,7 +294,7 @@ type SuggestionsDialogAction = personID: string; suggestionsAndChoices: PersonSuggestionsAndChoices; } - | { type: "mark"; item: SCItem; value: SCMark } + | { type: "mark"; item: SCItem; value: boolean | undefined } | { type: "save" } | { type: "toggleHistory" } | { type: "close" }; @@ -339,9 +337,13 @@ const suggestionsDialogReducer = ( case "mark": { const marks = new Map(state.marks); const { item, value } = action; - // If this was a new suggestion, prune off marks created as a result - // of the user toggling the item back to its original unset state. if (item.accepted === undefined && value === undefined) { + // If this was a suggestion, prune marks created as a result of + // the user toggling the item back to its original unset state. + marks.delete(item.id); + } else if (item.accepted && value === item.accepted) { + // If this is a choice, prune marks which match the choice's + // accepted state. marks.delete(item.id); } else { marks.set(item.id, value); @@ -428,7 +430,7 @@ const SuggestionsDialog: React.FC = ({ resetPersonAndClose(); }; - const handleMark = (item: SCItem, value: SCMark) => + const handleMark = (item: SCItem, value: boolean | undefined) => dispatch({ type: "mark", item, value }); const handleSave = async () => { @@ -539,12 +541,12 @@ const SuggestionsDialog: React.FC = ({ interface SuggestionOrChoiceListProps { items: SCItem[]; - marks: Map; + marks: Map; /** * Callback invoked when the user changes the value associated with the * given suggestion or choice. */ - onMarkItem: (item: SCItem, value: SCMark) => void; + onMarkItem: (item: SCItem, value: boolean | undefined) => void; } const SuggestionOrChoiceList: React.FC = ({ @@ -571,9 +573,9 @@ const SuggestionOrChoiceList: React.FC = ({ {!item.fixed && ( onMarkItem(item, toClusterMark(v))} + onChange={(_, v) => onMarkItem(item, toItemValue(v))} > @@ -588,9 +590,16 @@ const SuggestionOrChoiceList: React.FC = ({ ); -const toItemValue = (accepted?: boolean) => - accepted ? "yes" : accepted === false ? "no" : undefined; +const fromItemValue = ( + item: SCItem, + marks: Map, +) => { + // Use the in-memory state if available. + if (marks.has(item.id)) return marks.get(item.id); + // Use the original state of the choice (when applicable). + return item.accepted ? "yes" : item.accepted === false ? "no" : undefined; +}; -const toClusterMark = (v: unknown) => +const toItemValue = (v: unknown) => // This dance is needed for TypeScript to recognize the type. - v == "yes" ? "yes" : v == "no" ? "no" : undefined; + v == "yes" ? true : v == "no" ? false : undefined; From 2c91eb82bcff39f8e7451af021cea648a4b52d59 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 15 Oct 2024 16:25:12 +0530 Subject: [PATCH 23/28] Use a disambiguating background --- .../new/photos/components/gallery/PeopleHeader.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/web/packages/new/photos/components/gallery/PeopleHeader.tsx b/web/packages/new/photos/components/gallery/PeopleHeader.tsx index a72b131473..6af2c8e65c 100644 --- a/web/packages/new/photos/components/gallery/PeopleHeader.tsx +++ b/web/packages/new/photos/components/gallery/PeopleHeader.tsx @@ -454,11 +454,18 @@ const SuggestionsDialog: React.FC = ({ fullScreen={isSmallWidth} PaperProps={{ sx: { minHeight: "80svh" } }} > - + theme.colors.fill.faint + : "transparent", + }} + > {state.showChoices - ? pt("Saved suggestions") + ? pt("Saved choices") : pt("Review suggestions")} From e0b8999696fd68fa77e03b527993790f625dfa13 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 15 Oct 2024 16:35:35 +0530 Subject: [PATCH 24/28] Reset scroll pos --- .../new/photos/components/gallery/PeopleHeader.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/web/packages/new/photos/components/gallery/PeopleHeader.tsx b/web/packages/new/photos/components/gallery/PeopleHeader.tsx index 6af2c8e65c..840bec6f35 100644 --- a/web/packages/new/photos/components/gallery/PeopleHeader.tsx +++ b/web/packages/new/photos/components/gallery/PeopleHeader.tsx @@ -474,6 +474,7 @@ const SuggestionsDialog: React.FC = ({ {state.choices.length > 1 && ( dispatch({ type: "toggleHistory" })} aria-label={ !state.showChoices @@ -490,7 +491,11 @@ const SuggestionsDialog: React.FC = ({ )} - + {state.activity == "fetching" ? ( From 358f741d7d95dd5faa50ac28adbafffe03d9ff2b Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 15 Oct 2024 16:41:32 +0530 Subject: [PATCH 25/28] Use via worker --- web/packages/new/photos/services/ml/index.ts | 4 ++-- web/packages/new/photos/services/ml/worker.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/web/packages/new/photos/services/ml/index.ts b/web/packages/new/photos/services/ml/index.ts index c19f32abe0..f3469a7dd0 100644 --- a/web/packages/new/photos/services/ml/index.ts +++ b/web/packages/new/photos/services/ml/index.ts @@ -794,5 +794,5 @@ export const deleteCGroup = async ({ id }: CGroup) => { * * The suggestion computation happens in a web worker. */ -export const suggestionsForPerson = async (person: CGroupPerson) => - worker().then((w) => w.suggestionsForPerson(person)); +export const suggestionsAndChoicesForPerson = async (person: CGroupPerson) => + worker().then((w) => w.suggestionsAndChoicesForPerson(person)); diff --git a/web/packages/new/photos/services/ml/worker.ts b/web/packages/new/photos/services/ml/worker.ts index 906a7a2771..5507c0875b 100644 --- a/web/packages/new/photos/services/ml/worker.ts +++ b/web/packages/new/photos/services/ml/worker.ts @@ -44,7 +44,7 @@ import { type RawRemoteMLData, type RemoteMLData, } from "./ml-data"; -import { suggestionsForPerson, type CGroupPerson } from "./people"; +import { suggestionsAndChoicesForPerson, type CGroupPerson } from "./people"; import type { CLIPMatches, MLWorkerDelegate } from "./worker-types"; /** @@ -341,10 +341,10 @@ export class MLWorker { } /** - * Return suggestions for the given cgroup {@link person}. + * Return suggestions and choices for the given cgroup {@link person}. */ - async suggestionsForPerson(person: CGroupPerson) { - return suggestionsForPerson(person); + async suggestionsAndChoicesForPerson(person: CGroupPerson) { + return suggestionsAndChoicesForPerson(person); } } From b2b649d203fb37e0f59cb84620f07d9dedf76308 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 15 Oct 2024 16:44:51 +0530 Subject: [PATCH 26/28] Disambiguate implementations --- .../new/photos/components/gallery/PeopleHeader.tsx | 5 ++++- web/packages/new/photos/services/ml/clip.ts | 2 +- web/packages/new/photos/services/ml/cluster.ts | 2 +- web/packages/new/photos/services/ml/people.ts | 2 +- web/packages/new/photos/services/ml/worker.ts | 12 ++++++------ 5 files changed, 13 insertions(+), 10 deletions(-) diff --git a/web/packages/new/photos/components/gallery/PeopleHeader.tsx b/web/packages/new/photos/components/gallery/PeopleHeader.tsx index 840bec6f35..3eafe3f1cd 100644 --- a/web/packages/new/photos/components/gallery/PeopleHeader.tsx +++ b/web/packages/new/photos/components/gallery/PeopleHeader.tsx @@ -12,9 +12,12 @@ import { import { useIsSmallWidth } from "@/base/hooks"; import { pt } from "@/base/i18n"; import log from "@/base/log"; -import { deleteCGroup, renameCGroup } from "@/new/photos/services/ml"; import { + deleteCGroup, + renameCGroup, suggestionsAndChoicesForPerson, +} from "@/new/photos/services/ml"; +import { type CGroupPerson, type ClusterPerson, type Person, diff --git a/web/packages/new/photos/services/ml/clip.ts b/web/packages/new/photos/services/ml/clip.ts index 773621602f..a3c6a22487 100644 --- a/web/packages/new/photos/services/ml/clip.ts +++ b/web/packages/new/photos/services/ml/clip.ts @@ -133,7 +133,7 @@ const normalized = (embedding: Float32Array) => { * The result can also be `undefined`, which indicates that the download for the * ML model is still in progress (trying again later should succeed). */ -export const clipMatches = async ( +export const _clipMatches = async ( searchPhrase: string, electron: ElectronMLWorker, ): Promise => { diff --git a/web/packages/new/photos/services/ml/cluster.ts b/web/packages/new/photos/services/ml/cluster.ts index fab47652ce..36a39b5e09 100644 --- a/web/packages/new/photos/services/ml/cluster.ts +++ b/web/packages/new/photos/services/ml/cluster.ts @@ -62,7 +62,7 @@ export type ClusterFace = Omit & { * other interactions with the worker (where this code runs) do not get stalled * while clustering is in progress. */ -export const clusterFaces = async ( +export const _clusterFaces = async ( faceIndexes: FaceIndex[], localFiles: EnteFile[], onProgress: (progress: ClusteringProgress) => void, diff --git a/web/packages/new/photos/services/ml/people.ts b/web/packages/new/photos/services/ml/people.ts index 23f86483b5..16b36a0232 100644 --- a/web/packages/new/photos/services/ml/people.ts +++ b/web/packages/new/photos/services/ml/people.ts @@ -360,7 +360,7 @@ export interface PersonSuggestionsAndChoices { /** * Returns suggestions and existing choices for the given person. */ -export const suggestionsAndChoicesForPerson = async ( +export const _suggestionsAndChoicesForPerson = async ( person: CGroupPerson, ): Promise => { const startTime = Date.now(); diff --git a/web/packages/new/photos/services/ml/worker.ts b/web/packages/new/photos/services/ml/worker.ts index 5507c0875b..d8ebea44c0 100644 --- a/web/packages/new/photos/services/ml/worker.ts +++ b/web/packages/new/photos/services/ml/worker.ts @@ -18,14 +18,14 @@ import { type ImageBitmapAndData, } from "./blob"; import { + _clipMatches, clearCachedCLIPIndexes, clipIndexingVersion, - clipMatches, indexCLIP, type CLIPIndex, } from "./clip"; import { - clusterFaces, + _clusterFaces, reconcileClusters, type ClusteringProgress, } from "./cluster"; @@ -44,7 +44,7 @@ import { type RawRemoteMLData, type RemoteMLData, } from "./ml-data"; -import { suggestionsAndChoicesForPerson, type CGroupPerson } from "./people"; +import { _suggestionsAndChoicesForPerson, type CGroupPerson } from "./people"; import type { CLIPMatches, MLWorkerDelegate } from "./worker-types"; /** @@ -207,7 +207,7 @@ export class MLWorker { * Find {@link CLIPMatches} for a given normalized {@link searchPhrase}. */ async clipMatches(searchPhrase: string): Promise { - return clipMatches(searchPhrase, ensure(this.electron)); + return _clipMatches(searchPhrase, ensure(this.electron)); } private async tick() { @@ -326,7 +326,7 @@ export class MLWorker { * cgroups if needed. */ async clusterFaces(masterKey: Uint8Array) { - const clusters = await clusterFaces( + const clusters = await _clusterFaces( await savedFaceIndexes(), await getAllLocalFiles(), (progress) => this.updateClusteringProgress(progress), @@ -344,7 +344,7 @@ export class MLWorker { * Return suggestions and choices for the given cgroup {@link person}. */ async suggestionsAndChoicesForPerson(person: CGroupPerson) { - return suggestionsAndChoicesForPerson(person); + return _suggestionsAndChoicesForPerson(person); } } From 9c4d734b2607df70f3fefd77e64a5ff798003e8c Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 15 Oct 2024 16:57:14 +0530 Subject: [PATCH 27/28] Fix --- .../new/photos/components/gallery/PeopleHeader.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/web/packages/new/photos/components/gallery/PeopleHeader.tsx b/web/packages/new/photos/components/gallery/PeopleHeader.tsx index 3eafe3f1cd..0a9a7976cb 100644 --- a/web/packages/new/photos/components/gallery/PeopleHeader.tsx +++ b/web/packages/new/photos/components/gallery/PeopleHeader.tsx @@ -609,10 +609,10 @@ const fromItemValue = ( item: SCItem, marks: Map, ) => { - // Use the in-memory state if available. - if (marks.has(item.id)) return marks.get(item.id); - // Use the original state of the choice (when applicable). - return item.accepted ? "yes" : item.accepted === false ? "no" : undefined; + // Use the in-memory state if available. For choices, fallback to their + // original state. + const resolved = marks.has(item.id) ? marks.get(item.id) : item.accepted; + return resolved ? "yes" : resolved === false ? "no" : undefined; }; const toItemValue = (v: unknown) => From a0b601c847b552bcb7d26c854d367b3d74e82fdd Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 15 Oct 2024 17:08:20 +0530 Subject: [PATCH 28/28] Enable for internal users --- .../new/photos/components/gallery/PeopleHeader.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/web/packages/new/photos/components/gallery/PeopleHeader.tsx b/web/packages/new/photos/components/gallery/PeopleHeader.tsx index 0a9a7976cb..713b3f965c 100644 --- a/web/packages/new/photos/components/gallery/PeopleHeader.tsx +++ b/web/packages/new/photos/components/gallery/PeopleHeader.tsx @@ -49,7 +49,8 @@ import { Typography, } from "@mui/material"; import { t } from "i18next"; -import React, { useEffect, useReducer } from "react"; +import React, { useEffect, useReducer, useState } from "react"; +import { isInternalUser } from "../../services/feature-flags"; import { useAppContext } from "../../types/context"; import { AddPersonDialog } from "../AddPersonDialog"; import { SpaceBetweenFlex } from "../mui"; @@ -118,12 +119,17 @@ const CGroupPersonHeader: React.FC = ({ const cgroup = person.cgroup; const { showMiniDialog } = useAppContext(); + const [showReviewOption, setShowReviewOption] = useState(false); const { show: showNameInput, props: nameInputVisibilityProps } = useModalVisibility(); const { show: showSuggestions, props: suggestionsVisibilityProps } = useModalVisibility(); + useEffect(() => { + void isInternalUser().then((b) => setShowReviewOption(b)); + }, []); + const handleRename = (name: string) => renameCGroup(cgroup, name); const handleReset = () => @@ -171,7 +177,7 @@ const CGroupPersonHeader: React.FC = ({ > {pt("Reset")} - {process.env.NEXT_PUBLIC_ENTE_WIP_CL /* TODO-Cluster */ && ( + {showReviewOption /* TODO-Cluster */ && ( } centerAlign @@ -547,7 +553,7 @@ const SuggestionsDialog: React.FC = ({ color={"accent"} onClick={handleSave} > - {t("save")} + {hasUnsavedChanges ? pt("TODO Not impl") : t("save")}