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
17 changes: 16 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,29 @@ 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
179 changes: 107 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;
};
Comment on lines +35 to +51
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Handle missing timestamp more gracefully to avoid indefinite loading.

If status exists but timestamp is missing (line 42), the function returns true (loading), which would show a loading spinner indefinitely. This edge case could occur if state is restored from persistence without timestamps or in rare race conditions.

Consider treating a missing timestamp as "skip grace period" rather than "still loading":

 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 no timestamp, treat as "no grace period needed"
+  const timeSinceUpdate = timestamp ? Date.now() - timestamp : Infinity;

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

   return false;
 };

This way, if the timestamp is missing, timeSinceUpdate is Infinity, so the 3-second grace period check is bypassed and the actual status (empty or progress) is shown.

📝 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
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;
};
const isStatusLoading = (folderId: string, folderHasAITagging: boolean) => {
if (!folderHasAITagging) return false;
const status = taggingStatus[folderId];
if (!status) return true;
const timestamp = folderStatusTimestamps[folderId];
// If no timestamp, treat as "no grace period needed"
const timeSinceUpdate = timestamp ? Date.now() - timestamp : Infinity;
if (status.total_images === 0 && timeSinceUpdate < 3000) {
return true;
}
return false;
};
🤖 Prompt for AI Agents
In frontend/src/pages/SettingsPage/components/FolderManagementCard.tsx around
lines 35 to 51, the function treats a missing timestamp as "still loading" which
can cause an indefinite spinner; change the logic so the 3-second grace period
is only applied when a valid timestamp exists (e.g., if timestamp is missing,
skip the grace check by treating timeSinceUpdate as Infinity or by bypassing the
<3000 check), so status with no timestamp falls back to showing the actual state
instead of staying in loading.


return (
<SettingsCard
Expand All @@ -37,84 +58,98 @@ 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