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;
Comment on lines 77 to +95
},

// Clear tagging status
clearTaggingStatus(state) {
state.taggingStatus = {};
state.folderStatusTimestamps = {};
state.lastUpdatedAt = undefined;
},
},
Expand Down
177 changes: 105 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,25 @@ 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];
const timeSinceUpdate = timestamp ? Date.now() - timestamp : Infinity;

if (status.total_images === 0 && timeSinceUpdate < 3000) {
return true;
Comment on lines +34 to +45
Comment on lines +42 to +45
}

return false;
};

return (
<SettingsCard
Expand All @@ -37,84 +56,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={folder.folder_id || folder.folder_path || 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>

<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}
Comment on lines +111 to +145
</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