Skip to content

Commit 22ad8ce

Browse files
fix: display proper status for empty folders in AI tagging
- Show 'No images found' message for empty folders instead of 0% progress - Add image counts and estimated time for better UX - Implement smart loading states with 3-second grace period - Enhanced progress display with animations
1 parent c37d8df commit 22ad8ce

File tree

8 files changed

+195
-95
lines changed

8 files changed

+195
-95
lines changed

backend/temp_requirements.txt

2.64 KB
Binary file not shown.

docs/backend/backend_python/openapi.json

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1117,14 +1117,9 @@
11171117
"in": "query",
11181118
"required": false,
11191119
"schema": {
1120-
"allOf": [
1121-
{
1122-
"$ref": "#/components/schemas/InputType"
1123-
}
1124-
],
1120+
"$ref": "#/components/schemas/InputType",
11251121
"description": "Choose input type: 'path' or 'base64'",
1126-
"default": "path",
1127-
"title": "Input Type"
1122+
"default": "path"
11281123
},
11291124
"description": "Choose input type: 'path' or 'base64'"
11301125
}
@@ -2204,6 +2199,7 @@
22042199
"metadata": {
22052200
"anyOf": [
22062201
{
2202+
"additionalProperties": true,
22072203
"type": "object"
22082204
},
22092205
{

frontend/src/features/folderSlice.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ import { FolderTaggingInfo } from '@/types/FolderStatus';
55
interface FolderState {
66
folders: FolderDetails[];
77
taggingStatus: Record<string, FolderTaggingInfo>;
8+
folderStatusTimestamps: Record<string, number>;
89
lastUpdatedAt?: number;
910
}
1011

1112
const initialState: FolderState = {
1213
folders: [],
1314
taggingStatus: {},
15+
folderStatusTimestamps: {},
1416
};
1517

1618
const folderSlice = createSlice({
@@ -74,16 +76,26 @@ const folderSlice = createSlice({
7476
// Set tagging status for folders
7577
setTaggingStatus(state, action: PayloadAction<FolderTaggingInfo[]>) {
7678
const map: Record<string, FolderTaggingInfo> = {};
79+
const now = Date.now();
7780
for (const info of action.payload) {
7881
map[info.folder_id] = info;
82+
const existingStatus = state.taggingStatus[info.folder_id];
83+
if (
84+
!existingStatus ||
85+
existingStatus.total_images !== info.total_images ||
86+
existingStatus.tagged_images !== info.tagged_images
87+
) {
88+
state.folderStatusTimestamps[info.folder_id] = now;
89+
}
7990
}
8091
state.taggingStatus = map;
81-
state.lastUpdatedAt = Date.now();
92+
state.lastUpdatedAt = now;
8293
},
8394

8495
// Clear tagging status
8596
clearTaggingStatus(state) {
8697
state.taggingStatus = {};
98+
state.folderStatusTimestamps = {};
8799
state.lastUpdatedAt = undefined;
88100
},
89101
},

frontend/src/pages/SettingsPage/components/FolderManagementCard.tsx

Lines changed: 143 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
import React from 'react';
2-
import { Folder, Trash2, Check } from 'lucide-react';
3-
2+
import { Folder, Trash2, Check, Loader2, AlertCircle } from 'lucide-react';
43
import { Switch } from '@/components/ui/switch';
54
import { Button } from '@/components/ui/button';
65
import { Progress } from '@/components/ui/progress';
76
import { useSelector } from 'react-redux';
87
import { RootState } from '@/app/store';
98
import FolderPicker from '@/components/FolderPicker/FolderPicker';
10-
119
import { useFolderOperations } from '@/hooks/useFolderOperations';
1210
import { FolderDetails } from '@/types/Folder';
1311
import SettingsCard from './SettingsCard';
12+
import { FolderTaggingInfo } from '@/types/FolderStatus';
1413

1514
/**
1615
* Component for managing folder operations in settings
@@ -28,6 +27,49 @@ const FolderManagementCard: React.FC = () => {
2827
const taggingStatus = useSelector(
2928
(state: RootState) => state.folders.taggingStatus,
3029
);
30+
const folderStatusTimestamps = useSelector(
31+
(state: RootState) => state.folders.folderStatusTimestamps,
32+
);
33+
34+
35+
const calculateEstimatedTime = (status: FolderTaggingInfo) => {
36+
if (!status || status.total_images === 0 || status.tagging_percentage >= 100)
37+
return '';
38+
39+
const processed = status.tagged_images;
40+
const total = status.total_images;
41+
const remaining = total - processed;
42+
43+
44+
const estimatedSeconds = Math.round(remaining * 2);
45+
46+
if (estimatedSeconds < 60) {
47+
return `${estimatedSeconds}s`;
48+
} else {
49+
return `${Math.round(estimatedSeconds / 60)}m`;
50+
}
51+
};
52+
53+
54+
const isStatusLoading = (folderId: string, folderHasAITagging: boolean) => {
55+
if (!folderHasAITagging) return false;
56+
57+
const status = taggingStatus[folderId];
58+
if (!status) return true;
59+
60+
const timestamp = folderStatusTimestamps[folderId];
61+
62+
const timeSinceUpdate = timestamp ? Date.now() - timestamp : Infinity;
63+
64+
65+
if (status.total_images === 0 && timeSinceUpdate < 3000) {
66+
return true;
67+
}
68+
69+
return false;
70+
};
71+
72+
3173

3274
return (
3375
<SettingsCard
@@ -37,84 +79,111 @@ const FolderManagementCard: React.FC = () => {
3779
>
3880
{folders.length > 0 ? (
3981
<div className="space-y-3">
40-
{folders.map((folder: FolderDetails, index: number) => (
41-
<div
42-
key={index}
43-
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"
44-
>
45-
<div className="flex items-center justify-between">
46-
<div className="min-w-0 flex-1">
47-
<div className="flex items-center gap-3">
48-
<Folder className="h-4 w-4 flex-shrink-0 text-gray-500 dark:text-gray-400" />
49-
<span className="text-foreground truncate">
50-
{folder.folder_path}
51-
</span>
52-
</div>
53-
</div>
82+
{folders.map((folder: FolderDetails, index: number) => {
83+
const status = taggingStatus[folder.folder_id];
84+
const loading = isStatusLoading(
85+
folder.folder_id,
86+
folder.AI_Tagging,
87+
);
88+
const hasImages = status && status.total_images > 0;
89+
const isEmpty = status && status.total_images === 0 && !loading;
90+
const isComplete = status && status.tagging_percentage >= 100;
5491

55-
<div className="ml-4 flex items-center gap-4">
56-
<div className="flex items-center gap-3">
57-
<span className="text-muted-foreground text-sm">
58-
AI Tagging
59-
</span>
60-
<Switch
61-
className="cursor-pointer"
62-
checked={folder.AI_Tagging}
63-
onCheckedChange={() => toggleAITagging(folder)}
64-
disabled={
65-
enableAITaggingPending || disableAITaggingPending
66-
}
67-
/>
92+
return (
93+
<div
94+
key={index}
95+
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"
96+
>
97+
<div className="flex items-center justify-between">
98+
<div className="min-w-0 flex-1">
99+
<div className="flex items-center gap-3">
100+
<Folder className="h-4 w-4 shrink-0 text-gray-500 dark:text-gray-400" />
101+
<span className="text-foreground truncate">
102+
{folder.folder_path}
103+
</span>
104+
</div>
68105
</div>
69106

70-
<Button
71-
onClick={() => deleteFolder(folder.folder_id)}
72-
variant="outline"
73-
size="sm"
74-
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"
75-
disabled={deleteFolderPending}
76-
>
77-
<Trash2 className="h-4 w-4" />
78-
</Button>
79-
</div>
80-
</div>
107+
<div className="ml-4 flex items-center gap-4">
108+
<div className="flex items-center gap-3">
109+
<span className="text-muted-foreground text-sm">
110+
AI Tagging
111+
</span>
112+
<Switch
113+
className="cursor-pointer"
114+
checked={folder.AI_Tagging}
115+
onCheckedChange={() => toggleAITagging(folder)}
116+
disabled={
117+
enableAITaggingPending || disableAITaggingPending
118+
}
119+
/>
120+
</div>
81121

82-
{folder.AI_Tagging && (
83-
<div className="mt-3">
84-
<div className="text-muted-foreground mb-1 flex items-center justify-between text-xs">
85-
<span>AI Tagging Progress</span>
86-
<span
87-
className={
88-
(taggingStatus[folder.folder_id]?.tagging_percentage ??
89-
0) >= 100
90-
? 'flex items-center gap-1 text-green-500'
91-
: 'text-muted-foreground'
92-
}
122+
<Button
123+
onClick={() => deleteFolder(folder.folder_id)}
124+
variant="outline"
125+
size="sm"
126+
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"
127+
disabled={deleteFolderPending}
93128
>
94-
{(taggingStatus[folder.folder_id]?.tagging_percentage ??
95-
0) >= 100 && <Check className="h-3 w-3" />}
96-
{Math.round(
97-
taggingStatus[folder.folder_id]?.tagging_percentage ??
98-
0,
99-
)}
100-
%
101-
</span>
129+
<Trash2 className="h-4 w-4" />
130+
</Button>
102131
</div>
103-
<Progress
104-
value={
105-
taggingStatus[folder.folder_id]?.tagging_percentage ?? 0
106-
}
107-
indicatorClassName={
108-
(taggingStatus[folder.folder_id]?.tagging_percentage ??
109-
0) >= 100
110-
? 'bg-green-500'
111-
: 'bg-blue-500'
112-
}
113-
/>
114132
</div>
115-
)}
116-
</div>
117-
))}
133+
134+
{folder.AI_Tagging && (
135+
<div className="mt-3">
136+
{loading ? (
137+
<div className="text-muted-foreground flex items-center gap-2 text-xs">
138+
<Loader2 className="h-3 w-3 animate-spin" />
139+
<span>Loading status...</span>
140+
</div>
141+
) : isEmpty ? (
142+
<div className="text-muted-foreground flex items-center gap-2 text-xs">
143+
<AlertCircle className="h-3 w-3" />
144+
<span>No images found in this folder</span>
145+
</div>
146+
) : hasImages ? (
147+
<>
148+
<div className="text-muted-foreground mb-1 flex items-center justify-between text-xs">
149+
<span>AI Tagging Progress</span>
150+
<span
151+
className={
152+
isComplete
153+
? 'flex items-center gap-1 text-green-500'
154+
: 'text-muted-foreground'
155+
}
156+
>
157+
{isComplete && <Check className="h-3 w-3" />}
158+
{Math.round(status.tagging_percentage)}%
159+
160+
<span className="text-xs text-gray-500 ml-2">
161+
({status.tagged_images}/{status.total_images} images)
162+
</span>
163+
164+
{status.total_images > 50 && status.tagging_percentage < 100 && (
165+
<span className="text-xs text-blue-500 ml-2">
166+
{calculateEstimatedTime(status)} left
167+
</span>
168+
)}
169+
</span>
170+
</div>
171+
172+
<Progress
173+
value={status.tagging_percentage}
174+
indicatorClassName={
175+
isComplete
176+
? 'bg-green-500 transition-all duration-500'
177+
: 'bg-blue-500 animate-pulse'
178+
}
179+
/>
180+
</>
181+
) : null}
182+
</div>
183+
)}
184+
</div>
185+
);
186+
})}
118187
</div>
119188
) : (
120189
<div className="py-8 text-center">

frontend/src/types/FolderStatus.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
export interface FolderTaggingInfo {
22
folder_id: string;
33
folder_path: string;
4+
total_images: number;
5+
tagged_images: number;
46
tagging_percentage: number; // 0 - 100
57
}
68

sync-microservice/app/database/folders.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@
1212

1313

1414
class FolderTaggingInfo(NamedTuple):
15-
"""Represents folder tagging information"""
16-
1715
folder_id: FolderId
1816
folder_path: FolderPath
17+
total_images: int
18+
tagged_images: int
1919
tagging_percentage: float
2020

2121

@@ -111,6 +111,8 @@ def db_get_tagging_progress() -> List[FolderTaggingInfo]:
111111
FolderTaggingInfo(
112112
folder_id=folder_id,
113113
folder_path=folder_path,
114+
total_images=total_images,
115+
tagged_images=tagged_images,
114116
tagging_percentage=round(tagging_percentage, 2),
115117
)
116118
)

0 commit comments

Comments
 (0)