Skip to content
Closed
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
Binary file added backend/temp_requirements.txt
Binary file not shown.
10 changes: 3 additions & 7 deletions docs/backend/backend_python/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1117,14 +1117,9 @@
"in": "query",
"required": false,
"schema": {
"allOf": [
{
"$ref": "#/components/schemas/InputType"
}
],
"$ref": "#/components/schemas/InputType",
"description": "Choose input type: 'path' or 'base64'",
"default": "path",
"title": "Input Type"
"default": "path"
},
"description": "Choose input type: 'path' or 'base64'"
}
Expand Down Expand Up @@ -2204,6 +2199,7 @@
"metadata": {
"anyOf": [
{
"additionalProperties": true,
"type": "object"
},
{
Expand Down
38 changes: 30 additions & 8 deletions 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 @@ -72,18 +74,38 @@ const folderSlice = createSlice({
},

// Set tagging status for folders
setTaggingStatus(state, action: PayloadAction<FolderTaggingInfo[]>) {
const map: Record<string, FolderTaggingInfo> = {};
for (const info of action.payload) {
map[info.folder_id] = info;
}
state.taggingStatus = map;
state.lastUpdatedAt = Date.now();
},
setTaggingStatus(state, action: PayloadAction<FolderTaggingInfo[]>) {
const map: Record<string, FolderTaggingInfo> = {};
const now = Date.now();
const currentFolderIds = new Set<string>();

for (const info of action.payload) {
map[info.folder_id] = info;
currentFolderIds.add(info.folder_id);
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;
}
}

for (const folderId in state.folderStatusTimestamps) {
if (!currentFolderIds.has(folderId)) {
delete state.folderStatusTimestamps[folderId];
}
}

state.taggingStatus = map;
state.lastUpdatedAt = now;
},

// Clear tagging status
clearTaggingStatus(state) {
state.taggingStatus = {};
state.folderStatusTimestamps = {};
state.lastUpdatedAt = undefined;
},
},
Expand Down
217 changes: 143 additions & 74 deletions frontend/src/pages/SettingsPage/components/FolderManagementCard.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
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';
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';
import { FolderDetails } from '@/types/Folder';
import SettingsCard from './SettingsCard';
import { FolderTaggingInfo } from '@/types/FolderStatus';

/**
* Component for managing folder operations in settings
Expand All @@ -28,6 +27,49 @@ const FolderManagementCard: React.FC = () => {
const taggingStatus = useSelector(
(state: RootState) => state.folders.taggingStatus,
);
const folderStatusTimestamps = useSelector(
(state: RootState) => state.folders.folderStatusTimestamps,
);


const calculateEstimatedTime = (status: FolderTaggingInfo) => {
if (!status || status.total_images === 0 || status.tagging_percentage >= 100)
return '';

const processed = status.tagged_images;
const total = status.total_images;
const remaining = total - processed;


const estimatedSeconds = Math.round(remaining * 2);

if (estimatedSeconds < 60) {
return `${estimatedSeconds}s`;
} else {
return `${Math.round(estimatedSeconds / 60)}m`;
}
};


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;
}

return false;
};



return (
<SettingsCard
Expand All @@ -37,84 +79,111 @@ 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}
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 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 className="text-xs text-gray-500 ml-2">
({status.tagged_images}/{status.total_images} images)
</span>

{status.total_images > 50 && status.tagging_percentage < 100 && (
<span className="text-xs text-blue-500 ml-2">
• {calculateEstimatedTime(status)} left
</span>
)}
</span>
</div>

<Progress
value={status.tagging_percentage}
indicatorClassName={
isComplete
? 'bg-green-500 transition-all duration-500'
: 'bg-blue-500 animate-pulse'
}
/>
</>
) : null}
</div>
)}
</div>
);
})}
</div>
) : (
<div className="py-8 text-center">
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/types/FolderStatus.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export interface FolderTaggingInfo {
folder_id: string;
folder_path: string;
total_images: number;
tagged_images: number;
tagging_percentage: number; // 0 - 100
}

Expand Down
6 changes: 4 additions & 2 deletions sync-microservice/app/database/folders.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@


class FolderTaggingInfo(NamedTuple):
"""Represents folder tagging information"""

folder_id: FolderId
folder_path: FolderPath
total_images: int
tagged_images: int
tagging_percentage: float


Expand Down Expand Up @@ -111,6 +111,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
Loading