Skip to content
Merged
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/api/api-functions/folders.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { foldersEndpoints } from '../apiEndpoints';
import { apiClient } from '../axiosConfig';
import { apiClient, syncApiClient } from '../axiosConfig';
import { APIResponse } from '@/types/API';
import { FolderTaggingStatusResponse } from '@/types/FolderStatus';

// Request Types
export interface AddFolderRequest {
Expand Down Expand Up @@ -69,3 +70,15 @@ export const deleteFolders = async (
);
return response.data;
};

export const getFoldersTaggingStatus = async (): Promise<APIResponse> => {
const response = await syncApiClient.get<FolderTaggingStatusResponse>(
foldersEndpoints.getTaggingStatus,
);
const res = response.data;
return {
data: res.data as any,
success: res.status === 'success',
message: res.message,
};
};
1 change: 1 addition & 0 deletions frontend/src/api/apiEndpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const foldersEndpoints = {
disableAITagging: '/folders/disable-ai-tagging',
deleteFolders: '/folders/delete-folders',
syncFolder: '/folders/sync-folder',
getTaggingStatus: '/folders/status',
};

export const userPreferencesEndpoints = {
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/app/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import imageReducer from '@/features/imageSlice';
import faceClustersReducer from '@/features/faceClustersSlice';
import infoDialogReducer from '@/features/infoDialogSlice';
import folderReducer from '@/features/folderSlice';
import taggingStatusReducer from '@/features/taggingStatusSlice';

export const store = configureStore({
reducer: {
Expand All @@ -16,6 +17,7 @@ export const store = configureStore({
infoDialog: infoDialogReducer,
folders: folderReducer,
search: searchReducer,
taggingStatus: taggingStatusReducer,
},
});
// Infer the `RootState` and `AppDispatch` types from the store itself
Expand Down
14 changes: 11 additions & 3 deletions frontend/src/components/ui/progress.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,31 @@ import * as ProgressPrimitive from '@radix-ui/react-progress';

import { cn } from '@/lib/utils';

type ProgressProps = React.ComponentProps<typeof ProgressPrimitive.Root> & {
indicatorClassName?: string;
};

function Progress({
className,
value,
indicatorClassName,
...props
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
}: ProgressProps) {
return (
<ProgressPrimitive.Root
data-slot="progress"
className={cn(
'bg-primary/20 relative h-2 w-full overflow-hidden rounded-full',
'bg-white relative h-2 w-full overflow-hidden rounded-full',
className,
)}
{...props}
>
<ProgressPrimitive.Indicator
data-slot="progress-indicator"
className="bg-primary h-full w-full flex-1 transition-all"
className={cn(
'bg-primary h-full w-full flex-1 will-change-transform transition-transform duration-700 ease-in-out',
indicatorClassName,
)}
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
Expand Down
39 changes: 39 additions & 0 deletions frontend/src/features/taggingStatusSlice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { FolderTaggingInfo } from '@/types/FolderStatus';

interface TaggingStatusState {
byFolderId: Record<string, FolderTaggingInfo>;
lastUpdatedAt?: number;
}

const initialState: TaggingStatusState = {
byFolderId: {},
};

const taggingStatusSlice = createSlice({
name: 'taggingStatus',
initialState,
reducers: {
setTaggingStatus(
state,
action: PayloadAction<FolderTaggingInfo[]>,
) {
const map: Record<string, FolderTaggingInfo> = {};
for (const info of action.payload) {
map[info.folder_id] = info;
}
state.byFolderId = map;
state.lastUpdatedAt = Date.now();
},
clearTaggingStatus(state) {
state.byFolderId = {};
state.lastUpdatedAt = undefined;
},
},
});

export const { setTaggingStatus, clearTaggingStatus } = taggingStatusSlice.actions;
export default taggingStatusSlice.reducer;



19 changes: 19 additions & 0 deletions frontend/src/hooks/useFolderOperations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { selectAllFolders } from '@/features/folderSelectors';
import { setFolders } from '@/features/folderSlice';
import { FolderDetails } from '@/types/Folder';
import { useMutationFeedback } from './useMutationFeedback';
import { getFoldersTaggingStatus } from '@/api/api-functions/folders';
import { setTaggingStatus } from '@/features/taggingStatusSlice';

/**
* Custom hook for folder operations
Expand All @@ -26,6 +28,15 @@ export const useFolderOperations = () => {
queryFn: getAllFolders,
});

const taggingStatusQuery = usePictoQuery({
queryKey: ['folders', 'tagging-status'],
queryFn: getFoldersTaggingStatus,
staleTime: 1000,
refetchInterval: 1000,
refetchIntervalInBackground: true,
enabled: folders.some((f) => f.AI_Tagging),
});

// Apply feedback to the folders query
useMutationFeedback(
{
Expand All @@ -51,6 +62,14 @@ export const useFolderOperations = () => {
}
}, [foldersQuery.data, dispatch]);

// Update Redux store with tagging status on each poll
useEffect(() => {
const raw = taggingStatusQuery.data?.data as any;
if (Array.isArray(raw)) {
dispatch(setTaggingStatus(raw));
}
}, [taggingStatusQuery.data, dispatch]);

// Enable AI tagging mutation
const enableAITaggingMutation = usePictoMutation({
mutationFn: async (folder_id: string) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import React from 'react';
import { Folder, Trash2 } from 'lucide-react';
import { Folder, Trash2, Check } from 'lucide-react';

import { Switch } from '@/components/ui/switch';
import { Button } from '@/components/ui/button';
import { Progress } from '@/components/ui/progress';
import { useSelector } from 'react-redux';
import { RootState } from '@/app/store';
import FolderPicker from '@/components/FolderPicker/FolderPicker';

import { useFolderOperations } from '@/hooks/useFolderOperations';
Expand All @@ -22,6 +25,10 @@ const FolderManagementCard: React.FC = () => {
deleteFolderPending,
} = useFolderOperations();

const taggingStatus = useSelector(
(state: RootState) => state.taggingStatus.byFolderId,
);

return (
<SettingsCard
icon={Folder}
Expand All @@ -43,6 +50,38 @@ const FolderManagementCard: React.FC = () => {
{folder.folder_path}
</span>
</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
? 'text-green-500 flex items-center gap-1'
: 'text-muted-foreground'
}
>
{(taggingStatus[folder.folder_id]?.tagging_percentage ?? 0) >= 100 && (
<Check className="h-3 w-3" />
)}
{Math.round(
taggingStatus[folder.folder_id]?.tagging_percentage ?? 0,
)}
%
</span>
</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>

<div className="ml-4 flex items-center gap-4">
Expand Down Expand Up @@ -92,4 +131,4 @@ const FolderManagementCard: React.FC = () => {
);
};

export default FolderManagementCard;
export default FolderManagementCard;
15 changes: 15 additions & 0 deletions frontend/src/types/FolderStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export interface FolderTaggingInfo {
folder_id: string;
folder_path: string;
tagging_percentage: number; // 0 - 100
}

export interface FolderTaggingStatusResponse {
status: 'success' | 'error';
data: FolderTaggingInfo[];
total_folders: number;
message?: string;
}