Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
15 changes: 14 additions & 1 deletion frontend/src/features/folderSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import { FolderTaggingInfo } from '@/types/FolderStatus';
interface FolderState {
folders: FolderDetails[];
taggingStatus: Record<string, FolderTaggingInfo>;
folderStatusTimestamps: Record<string, number>;
lastUpdatedAt?: number;
}

const initialState: FolderState = {
folders: [],
taggingStatus: {},
folderStatusTimestamps: {},
};

const folderSlice = createSlice({
Expand Down Expand Up @@ -74,16 +76,27 @@ const folderSlice = createSlice({
// Set tagging status for folders
setTaggingStatus(state, action: PayloadAction<FolderTaggingInfo[]>) {
const map: Record<string, FolderTaggingInfo> = {};
const now = Date.now();

for (const info of action.payload) {
map[info.folder_id] = info;

const existingStatus = state.taggingStatus[info.folder_id];
if (!existingStatus ||
existingStatus.total_images !== info.total_images ||
existingStatus.tagged_images !== info.tagged_images) {
state.folderStatusTimestamps[info.folder_id] = now;
}
}

state.taggingStatus = map;
state.lastUpdatedAt = Date.now();
state.lastUpdatedAt = now;
},

// Clear tagging status
clearTaggingStatus(state) {
state.taggingStatus = {};
state.folderStatusTimestamps = {};
state.lastUpdatedAt = undefined;
},
},
Expand Down
176 changes: 104 additions & 72 deletions frontend/src/pages/SettingsPage/components/FolderManagementCard.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { Folder, Trash2, Check } from 'lucide-react';
import { Folder, Trash2, Check, Loader2, AlertCircle } from 'lucide-react';

import { Switch } from '@/components/ui/switch';
import { Button } from '@/components/ui/button';
Expand Down Expand Up @@ -28,6 +28,27 @@ const FolderManagementCard: React.FC = () => {
const taggingStatus = useSelector(
(state: RootState) => state.folders.taggingStatus,
);
const folderStatusTimestamps = useSelector(
(state: RootState) => state.folders.folderStatusTimestamps,
);

const isStatusLoading = (folderId: string, folderHasAITagging: boolean) => {
if (!folderHasAITagging) return false;

const status = taggingStatus[folderId];
if (!status) return true;

const timestamp = folderStatusTimestamps[folderId];
if (!timestamp) return true;

const timeSinceUpdate = Date.now() - timestamp;

if (status.total_images === 0 && timeSinceUpdate < 3000) {
return true;
}

return false;
};

return (
<SettingsCard
Expand All @@ -37,84 +58,95 @@ const FolderManagementCard: React.FC = () => {
>
{folders.length > 0 ? (
<div className="space-y-3">
{folders.map((folder: FolderDetails, index: number) => (
<div
key={index}
className="group border-border bg-background/50 relative rounded-lg border p-4 transition-all hover:border-gray-300 hover:shadow-sm dark:hover:border-gray-600"
>
<div className="flex items-center justify-between">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-3">
<Folder className="h-4 w-4 flex-shrink-0 text-gray-500 dark:text-gray-400" />
<span className="text-foreground truncate">
{folder.folder_path}
</span>
</div>
</div>
{folders.map((folder: FolderDetails, index: number) => {
const status = taggingStatus[folder.folder_id];
const loading = isStatusLoading(folder.folder_id, folder.AI_Tagging);
const hasImages = status && status.total_images > 0;
const isEmpty = status && status.total_images === 0 && !loading;
const isComplete = status && status.tagging_percentage >= 100;

<div className="ml-4 flex items-center gap-4">
<div className="flex items-center gap-3">
<span className="text-muted-foreground text-sm">
AI Tagging
</span>
<Switch
className="cursor-pointer"
checked={folder.AI_Tagging}
onCheckedChange={() => toggleAITagging(folder)}
disabled={
enableAITaggingPending || disableAITaggingPending
}
/>
return (
<div
key={index}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Use folder.folder_id as key instead of array index.

Using array index as the key prop is a React anti-pattern that can cause rendering issues when folders are reordered, added, or removed. React may incorrectly recycle component instances, leading to stale state or incorrect UI updates.

Apply this diff:

-            key={index}
+            key={folder.folder_id}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
key={index}
key={folder.folder_id}
🤖 Prompt for AI Agents
In frontend/src/pages/SettingsPage/components/FolderManagementCard.tsx around
line 73, the component uses the array index as the React key which can cause
incorrect re-renders; replace key={index} with key={folder.folder_id} (or
another stable unique id from the folder object) to provide a stable and unique
key for each item, and if folder.folder_id might be undefined add a safe
fallback (e.g., a generated unique id stored on the folder) to ensure keys
remain stable across renders.

className="group border-border bg-background/50 relative rounded-lg border p-4 transition-all hover:border-gray-300 hover:shadow-sm dark:hover:border-gray-600"
>
<div className="flex items-center justify-between">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-3">
<Folder className="h-4 w-4 flex-shrink-0 text-gray-500 dark:text-gray-400" />
<span className="text-foreground truncate">
{folder.folder_path}
</span>
</div>
</div>

<Button
onClick={() => deleteFolder(folder.folder_id)}
variant="outline"
size="sm"
className="h-8 w-8 cursor-pointer text-gray-500 hover:border-red-300 hover:text-red-600 dark:text-gray-400 dark:hover:text-red-400"
disabled={deleteFolderPending}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
<div className="ml-4 flex items-center gap-4">
<div className="flex items-center gap-3">
<span className="text-muted-foreground text-sm">
AI Tagging
</span>
<Switch
className="cursor-pointer"
checked={folder.AI_Tagging}
onCheckedChange={() => toggleAITagging(folder)}
disabled={
enableAITaggingPending || disableAITaggingPending
}
/>
</div>

{folder.AI_Tagging && (
<div className="mt-3">
<div className="text-muted-foreground mb-1 flex items-center justify-between text-xs">
<span>AI Tagging Progress</span>
<span
className={
(taggingStatus[folder.folder_id]?.tagging_percentage ??
0) >= 100
? 'flex items-center gap-1 text-green-500'
: 'text-muted-foreground'
}
<Button
onClick={() => deleteFolder(folder.folder_id)}
variant="outline"
size="sm"
className="h-8 w-8 cursor-pointer text-gray-500 hover:border-red-300 hover:text-red-600 dark:text-gray-400 dark:hover:text-red-400"
disabled={deleteFolderPending}
>
{(taggingStatus[folder.folder_id]?.tagging_percentage ??
0) >= 100 && <Check className="h-3 w-3" />}
{Math.round(
taggingStatus[folder.folder_id]?.tagging_percentage ??
0,
)}
%
</span>
<Trash2 className="h-4 w-4" />
</Button>
</div>
<Progress
value={
taggingStatus[folder.folder_id]?.tagging_percentage ?? 0
}
indicatorClassName={
(taggingStatus[folder.folder_id]?.tagging_percentage ??
0) >= 100
? 'bg-green-500'
: 'bg-blue-500'
}
/>
</div>
)}
</div>
))}

{folder.AI_Tagging && (
<div className="mt-3">
{loading ? (
<div className="text-muted-foreground flex items-center gap-2 text-xs">
<Loader2 className="h-3 w-3 animate-spin" />
<span>Loading status...</span>
</div>
) : isEmpty ? (
<div className="text-muted-foreground flex items-center gap-2 text-xs">
<AlertCircle className="h-3 w-3" />
<span>No images found in this folder</span>
</div>
) : hasImages ? (
<>
<div className="text-muted-foreground mb-1 flex items-center justify-between text-xs">
<span>AI Tagging Progress</span>
<span
className={
isComplete
? 'flex items-center gap-1 text-green-500'
: 'text-muted-foreground'
}
>
{isComplete && <Check className="h-3 w-3" />}
{Math.round(status.tagging_percentage)}%
</span>
</div>
<Progress
value={status.tagging_percentage}
indicatorClassName={
isComplete ? 'bg-green-500' : 'bg-blue-500'
}
/>
</>
) : null}
</div>
)}
</div>
);
})}
</div>
) : (
<div className="py-8 text-center">
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/types/FolderStatus.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
export interface FolderTaggingInfo {
folder_id: string;
folder_path: string;
tagging_percentage: number; // 0 - 100
total_images: number;
tagged_images: number;
tagging_percentage: number;
}

export interface FolderTaggingStatusResponse {
Expand Down
5 changes: 4 additions & 1 deletion sync-microservice/app/database/folders.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ class FolderTaggingInfo(NamedTuple):

folder_id: FolderId
folder_path: FolderPath
total_images: int
tagged_images: int
tagging_percentage: float


Expand Down Expand Up @@ -101,7 +103,6 @@ def db_get_tagging_progress() -> List[FolderTaggingInfo]:

folder_info_list = []
for folder_id, folder_path, total_images, tagged_images in results:
# Calculate percentage, handle division by zero
if total_images > 0:
tagging_percentage = (tagged_images / total_images) * 100
else:
Expand All @@ -111,6 +112,8 @@ def db_get_tagging_progress() -> List[FolderTaggingInfo]:
FolderTaggingInfo(
folder_id=folder_id,
folder_path=folder_path,
total_images=total_images,
tagged_images=tagged_images,
tagging_percentage=round(tagging_percentage, 2),
)
)
Expand Down
2 changes: 2 additions & 0 deletions sync-microservice/app/routes/folders.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ def get_folders_tagging_status():
FolderTaggingInfo(
folder_id=folder.folder_id,
folder_path=folder.folder_path,
total_images=folder.total_images,
tagged_images=folder.tagged_images,
tagging_percentage=folder.tagging_percentage,
)
for folder in tagging_progress
Expand Down
2 changes: 2 additions & 0 deletions sync-microservice/app/schemas/folders.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ class FolderTaggingInfo(BaseModel):

folder_id: str = Field(..., description="Unique identifier for the folder")
folder_path: str = Field(..., description="Path to the folder")
total_images: int = Field(..., ge=0, description="Total number of images in folder")
tagged_images: int = Field(..., ge=0, description="Number of tagged images")
tagging_percentage: float = Field(
...,
ge=0,
Expand Down
Loading