11import React from 'react' ;
2- import { Folder , Trash2 , Check } from 'lucide-react' ;
3-
2+ import { Folder , Trash2 , Check , Loader2 , AlertCircle } from 'lucide-react' ;
43import { Switch } from '@/components/ui/switch' ;
54import { Button } from '@/components/ui/button' ;
65import { Progress } from '@/components/ui/progress' ;
76import { useSelector } from 'react-redux' ;
87import { RootState } from '@/app/store' ;
98import FolderPicker from '@/components/FolderPicker/FolderPicker' ;
10-
119import { useFolderOperations } from '@/hooks/useFolderOperations' ;
1210import { FolderDetails } from '@/types/Folder' ;
1311import 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" >
0 commit comments