Skip to content

Feat: Bulk AI Tagging & Progress Tracking for Folder Management#747

Closed
hrshdas wants to merge 1 commit intoAOSSIE-Org:mainfrom
hrshdas:feat,bulk-ai-tagging-progress-tracking
Closed

Feat: Bulk AI Tagging & Progress Tracking for Folder Management#747
hrshdas wants to merge 1 commit intoAOSSIE-Org:mainfrom
hrshdas:feat,bulk-ai-tagging-progress-tracking

Conversation

@hrshdas
Copy link
Copy Markdown

@hrshdas hrshdas commented Dec 13, 2025

Summary

This PR introduces Bulk AI Tagging and Progress Tracking to enhance the folder management workflow. Users can now select multiple files/folders and apply AI-generated tags in one go, with real-time progress updates to ensure a smooth and transparent tagging experience.

What’s New

  1. Bulk AI Tagging

Added support for sending multiple items to the AI tagging engine.

Implemented batching logic to prevent blocking or timeouts.

Improved response handling for multi-file metadata generation.

  1. Real-Time Progress Tracking

Added progress indicators for:

Total items processed

Current tagging status

Completed vs pending items

Updated UI to display live tagging updates.

  1. Folder Management Enhancements

Integrated tagging results back into folder structure.

Improved state updates and error handling for bulk operations.

Added checks for already-tagged files to prevent reprocessing.

Technical Changes

New API method: POST /tag/bulk

Introduced utility processBulkTagging()

Updated components/services:

FolderView / FileList

AI Tagging Service

Progress Overlay / Toast Notifications

Improved error recovery for partial tagging failures.

Testing

Added tests for:

Multi-file tagging flows

Progress state updates

Failure handling (network, AI errors)

Manually verified on folders containing 5–100+ files.

Why This Matters

This feature significantly improves user experience when working with large folders. Instead of tagging files one by one, users can now automate the entire process with AI—saving time and increasing accuracy.

Impact

Faster content organization

Better UX for large datasets

More reliable AI tagging performance

Summary by CodeRabbit

  • New Features
    • Bulk AI tagging operations: enable or disable AI tagging for multiple folders simultaneously.
    • Reorganized folder management with collapsible sections (Completed, In Progress, Pending).
    • Folder selection capabilities with select all and clear options.
    • Progress tracking with visual progress bar for tagging operations.

✏️ Tip: You can customize this high-level summary in your review settings.

@github-actions
Copy link
Copy Markdown
Contributor

⚠️ No issue was linked in the PR description.
Please make sure to link an issue (e.g., 'Fixes #issue_number')

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Dec 13, 2025

Walkthrough

Changes introduce bulk AI tagging capabilities to folder management by adding batch mutation operations in the hook layer and refactoring the UI to a sectioned, collapsible interface with selection mechanics. OpenAPI schema structure is updated to standardize parameter definitions.

Changes

Cohort / File(s) Summary
OpenAPI Schema Refactoring
docs/backend/backend_python/openapi.json
Updated InputType parameter schema to use allOf wrapper with title "Input Type" and default "path"; removed explicit additionalProperties declaration from ImageInCluster.metadata.
Bulk Operations Hook Enhancement
frontend/src/hooks/useFolderOperations.tsx
Added bulkEnableMutation and bulkDisableMutation for batch folder AI tagging operations, exposing two new public functions (bulkEnableAITagging, bulkDisableAITagging) with configurable chunk batching and a bulkPending state flag.
Folder Management UI Refactoring
frontend/src/pages/SettingsPage/components/FolderManagementCard.tsx
Refactored from flat list to sectioned, collapsible interface with Completed/In Progress/Pending sections; added folder selection mechanics (select, toggle, clear, select all visible), bulk action controls (AI Tag All, Tag Selected, Disable AI for Selected), folder grouping logic based on taggingStatus, localStorage persistence for collapsed states, and per-row inline controls replacing previous per-folder controls.

Sequence Diagram

sequenceDiagram
    actor User
    participant FolderMgmtCard as FolderManagementCard
    participant Hook as useFolderOperations
    participant Mutation as GraphQL Mutation
    participant Backend as Backend API
    
    User->>FolderMgmtCard: Select folders & click "Tag Selected"
    FolderMgmtCard->>Hook: bulkEnableAITagging(folderIds)
    Hook->>Hook: Batch folderIds into chunks (size: 20)
    loop For each batch
        Hook->>Mutation: Execute bulkEnableMutation(batchIds)
        Mutation->>Backend: POST /folder-tagging/bulk-enable
        Backend->>Backend: Process batch
        Backend-->>Mutation: Success response
    end
    Mutation-->>Hook: All mutations complete
    Hook-->>FolderMgmtCard: Promise resolved
    FolderMgmtCard->>FolderMgmtCard: Update folder state & UI sections
    FolderMgmtCard-->>User: Display updated folder status
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • FolderManagementCard: Dense logic combining selection state management, folder grouping/categorization by taggingStatus, localStorage persistence, and bulk action coordination—requires careful verification of section filtering, selection tracking, and integration with hook mutations.
  • useFolderOperations: Verify batching logic (chunking algorithm, async Promise handling) and mutation state aggregation (bulkPending flag correctness).
  • OpenAPI schema: Confirm schema changes don't break client generation or alter validation behavior unexpectedly; evaluate impact of removing additionalProperties.

Possibly related PRs

Suggested labels

enhancement, frontend, backend

Suggested reviewers

  • rahulharpal1603

Poem

🐰 A rabbit hops through folders bright,
Selecting, batching, tagging with delight!
From flat lists to sections neatly arranged,
Bulk operations make the workflows changed—
Progress bars and localStorage cheer,
The folder management magic is here! ✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main changes: bulk AI tagging functionality and progress tracking for folder management, matching the core objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

⚠️ No issue was linked in the PR description.
Please make sure to link an issue (e.g., 'Fixes #issue_number')

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/backend/backend_python/openapi.json (1)

1114-1130: Input_type parameter uses inconsistent schema pattern with redundant allOf wrapper.

The input_type parameter (lines 1120–1127) wraps an InputType reference in an allOf composition, unlike all other parameters in the spec. Compare against show_hidden (uses direct type: boolean), album_id (uses direct type: string), and cluster_id (uses direct type: string). The allOf is the only such usage in the entire OpenAPI specification.

This pattern is both inconsistent and redundant—a single-item allOf adds unnecessary complexity without benefit. Additionally, the description appears twice: inside the schema (line 1126) and at the parameter level (line 1130).

Consider unwrapping the schema to match the pattern used throughout the spec:

"schema": {
  "$ref": "#/components/schemas/InputType",
  "description": "Choose input type: 'path' or 'base64'",
  "default": "path",
  "title": "Input Type"
}

Verify this matches your intended schema structure and confirm client code generation is not negatively impacted by the current pattern.

🧹 Nitpick comments (7)
frontend/src/hooks/useFolderOperations.tsx (2)

145-155: Avoid re-fetch thrash from autoInvalidateTags on every bulk batch
Right now each batch triggers invalidateQueries(['folders']), which can cause repeated refetches (and UI churn) for large selections. Consider invalidating once after the loop (or using a larger staleTime/debounced invalidate) and/or disabling auto-invalidate for the bulk mutations.

Example approach (invalidate once after batching):

+import { useQueryClient } from '@tanstack/react-query';
...
 export const useFolderOperations = () => {
   const dispatch = useDispatch();
+  const queryClient = useQueryClient();
...
   const bulkEnableMutation = usePictoMutation({
     mutationFn: async (folder_ids: string[]) => enableAITagging({ folder_ids }),
-    autoInvalidateTags: ['folders'],
+    // invalidate once after all batches complete
   });
...
   const bulkDisableMutation = usePictoMutation({
     mutationFn: async (folder_ids: string[]) => disableAITagging({ folder_ids }),
-    autoInvalidateTags: ['folders'],
+    // invalidate once after all batches complete
   });

(Then call queryClient.invalidateQueries({ queryKey: ['folders'] }) once at the end of each bulk function.)


210-218: bulkPending may under-report during sequential batches
Because batching is sequential, bulkPending depends on isPending being set/unset per batch; some UIs expect “pending” for the whole bulk operation. Consider tracking an explicit isBulkRunning state inside the hook (set true before loop, false in finally) and expose that instead (or OR it with mutation pending).

frontend/src/pages/SettingsPage/components/FolderManagementCard.tsx (5)

35-62: Persisted collapse state: good defensive init; consider namespacing the storage key
The lazy initializer + try/catch is solid. Minor: folder-sections-collapsed is fairly generic; consider prefixing with app/feature to avoid collisions.


63-95: Progress math likely misleads (only counts “completed”, ignores in-progress %)
progressPct is based on completed/total, so UI can show 0% even when many folders are mid-tag. If you want “real progress”, compute an average of per-folder percentages (clamped 0–100), or weight by item counts if available.


239-257: “Select all pending” checkbox should be controlled (and ideally indeterminate)
Right now it doesn’t reflect current selection state. Also the label isn’t associated with the input.

Sketch:

  • Compute pendingIds, allPendingSelected, somePendingSelected.
  • Use checked={allPendingSelected} and set ref.current.indeterminate = somePendingSelected && !allPendingSelected.
  • Wrap with <label> or add aria-label.

293-313: Section header button: add aria-expanded + aria-controls
This will improve accessibility for the collapsible regions.


315-387: Add accessible labels for row checkbox + delete button
The row checkbox has no label, and the icon-only delete button should have an aria-label.

-  <input
+  <input
     type="checkbox"
     className="mt-1 h-4 w-4 cursor-pointer accent-blue-600"
     checked={checked}
     onChange={onCheckedChange}
     disabled={disableAll}
+    aria-label={`Select folder ${folder.folder_path}`}
   />
...
   <Button
     onClick={deleteFolder}
     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={disableAll}
+    aria-label={`Delete folder ${folder.folder_path}`}
   >
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d07d817 and d42fde1.

📒 Files selected for processing (3)
  • docs/backend/backend_python/openapi.json (1 hunks)
  • frontend/src/hooks/useFolderOperations.tsx (3 hunks)
  • frontend/src/pages/SettingsPage/components/FolderManagementCard.tsx (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
frontend/src/hooks/useFolderOperations.tsx (2)
frontend/src/hooks/useQueryExtension.ts (1)
  • usePictoMutation (26-78)
frontend/src/api/api-functions/folders.ts (2)
  • enableAITagging (44-52)
  • disableAITagging (54-62)
frontend/src/pages/SettingsPage/components/FolderManagementCard.tsx (3)
frontend/src/hooks/useFolderOperations.tsx (1)
  • useFolderOperations (20-219)
frontend/src/components/ui/progress.tsx (1)
  • Progress (37-37)
frontend/src/components/ui/switch.tsx (1)
  • Switch (29-29)
🔇 Additional comments (1)
docs/backend/backend_python/openapi.json (1)

2204-2213: Verify metadata schema doesn't need explicit property constraints.

The ImageInCluster.metadata field (lines 2204–2213) now omits explicit additionalProperties definition. While JSON Schema defaults to allowing additional properties (no-op functionally), this makes the schema less explicit and may indicate reduced validation strictness.

Confirm that:

  1. The backend allows arbitrary properties on metadata objects (which the current schema permits).
  2. The removal was intentional and not an unintended loss of validation constraints.
  3. Any downstream code that relies on metadata structure is compatible with this permissiveness.

Comment on lines +174 to +200
/**
* Enable AI tagging for many folders with batching to handle rate limits.
*/
const bulkEnableAITagging = async (
folderIds: string[],
batchSize: number = 20,
) => {
const ids = Array.from(new Set(folderIds));
for (let i = 0; i < ids.length; i += batchSize) {
const batch = ids.slice(i, i + batchSize);
await bulkEnableMutation.mutateAsync(batch);
}
};

/**
* Disable AI tagging for many folders with batching.
*/
const bulkDisableAITagging = async (
folderIds: string[],
batchSize: number = 20,
) => {
const ids = Array.from(new Set(folderIds));
for (let i = 0; i < ids.length; i += batchSize) {
const batch = ids.slice(i, i + batchSize);
await bulkDisableMutation.mutateAsync(batch);
}
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Guard batchSize and decide on partial-failure behavior (stop vs continue)

  • If batchSize <= 0, the for loop never progresses (i += batchSize) → infinite loop.
  • Current behavior aborts on the first failed batch; if you want “partial failure recovery”, catch per-batch, collect failures, and continue.
   const bulkEnableAITagging = async (
     folderIds: string[],
     batchSize: number = 20,
   ) => {
+    if (!Number.isFinite(batchSize) || batchSize <= 0) {
+      throw new Error('batchSize must be a positive number');
+    }
     const ids = Array.from(new Set(folderIds));
     for (let i = 0; i < ids.length; i += batchSize) {
       const batch = ids.slice(i, i + batchSize);
       await bulkEnableMutation.mutateAsync(batch);
     }
   };

If partial recovery is desired:

+    const failed: string[] = [];
     for (let i = 0; i < ids.length; i += batchSize) {
       const batch = ids.slice(i, i + batchSize);
-      await bulkEnableMutation.mutateAsync(batch);
+      try {
+        await bulkEnableMutation.mutateAsync(batch);
+      } catch {
+        failed.push(...batch);
+      }
     }
+    if (failed.length) throw new Error(`Failed to enable AI tagging for ${failed.length} folder(s).`);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Enable AI tagging for many folders with batching to handle rate limits.
*/
const bulkEnableAITagging = async (
folderIds: string[],
batchSize: number = 20,
) => {
const ids = Array.from(new Set(folderIds));
for (let i = 0; i < ids.length; i += batchSize) {
const batch = ids.slice(i, i + batchSize);
await bulkEnableMutation.mutateAsync(batch);
}
};
/**
* Disable AI tagging for many folders with batching.
*/
const bulkDisableAITagging = async (
folderIds: string[],
batchSize: number = 20,
) => {
const ids = Array.from(new Set(folderIds));
for (let i = 0; i < ids.length; i += batchSize) {
const batch = ids.slice(i, i + batchSize);
await bulkDisableMutation.mutateAsync(batch);
}
};
/**
* Enable AI tagging for many folders with batching to handle rate limits.
*/
const bulkEnableAITagging = async (
folderIds: string[],
batchSize: number = 20,
) => {
if (!Number.isFinite(batchSize) || batchSize <= 0) {
throw new Error('batchSize must be a positive number');
}
const ids = Array.from(new Set(folderIds));
for (let i = 0; i < ids.length; i += batchSize) {
const batch = ids.slice(i, i + batchSize);
await bulkEnableMutation.mutateAsync(batch);
}
};
/**
* Disable AI tagging for many folders with batching.
*/
const bulkDisableAITagging = async (
folderIds: string[],
batchSize: number = 20,
) => {
if (!Number.isFinite(batchSize) || batchSize <= 0) {
throw new Error('batchSize must be a positive number');
}
const ids = Array.from(new Set(folderIds));
for (let i = 0; i < ids.length; i += batchSize) {
const batch = ids.slice(i, i + batchSize);
await bulkDisableMutation.mutateAsync(batch);
}
};
Suggested change
/**
* Enable AI tagging for many folders with batching to handle rate limits.
*/
const bulkEnableAITagging = async (
folderIds: string[],
batchSize: number = 20,
) => {
const ids = Array.from(new Set(folderIds));
for (let i = 0; i < ids.length; i += batchSize) {
const batch = ids.slice(i, i + batchSize);
await bulkEnableMutation.mutateAsync(batch);
}
};
/**
* Disable AI tagging for many folders with batching.
*/
const bulkDisableAITagging = async (
folderIds: string[],
batchSize: number = 20,
) => {
const ids = Array.from(new Set(folderIds));
for (let i = 0; i < ids.length; i += batchSize) {
const batch = ids.slice(i, i + batchSize);
await bulkDisableMutation.mutateAsync(batch);
}
};
/**
* Enable AI tagging for many folders with batching to handle rate limits.
*/
const bulkEnableAITagging = async (
folderIds: string[],
batchSize: number = 20,
) => {
if (!Number.isFinite(batchSize) || batchSize <= 0) {
throw new Error('batchSize must be a positive number');
}
const ids = Array.from(new Set(folderIds));
const failed: string[] = [];
for (let i = 0; i < ids.length; i += batchSize) {
const batch = ids.slice(i, i + batchSize);
try {
await bulkEnableMutation.mutateAsync(batch);
} catch {
failed.push(...batch);
}
}
if (failed.length) throw new Error(`Failed to enable AI tagging for ${failed.length} folder(s).`);
};
/**
* Disable AI tagging for many folders with batching.
*/
const bulkDisableAITagging = async (
folderIds: string[],
batchSize: number = 20,
) => {
if (!Number.isFinite(batchSize) || batchSize <= 0) {
throw new Error('batchSize must be a positive number');
}
const ids = Array.from(new Set(folderIds));
const failed: string[] = [];
for (let i = 0; i < ids.length; i += batchSize) {
const batch = ids.slice(i, i + batchSize);
try {
await bulkDisableMutation.mutateAsync(batch);
} catch {
failed.push(...batch);
}
}
if (failed.length) throw new Error(`Failed to disable AI tagging for ${failed.length} folder(s).`);
};
🤖 Prompt for AI Agents
In frontend/src/hooks/useFolderOperations.tsx around lines 174 to 200, the
bulkEnableAITagging and bulkDisableAITagging functions don't guard against
invalid batchSize (<= 0) which can cause an infinite loop and they
unconditionally await batch mutations which abort the whole operation on the
first failed batch; to fix this, validate batchSize at the top (throw or coerce
to a safe default > 0) and decide desired failure policy: for fail-fast keep
current awaits and rethrow on any mutation error, or for partial-recovery wrap
each await in try/catch, collect failed batch ids (or errors) into an array,
continue processing remaining batches, and return a result object summarizing
successes and failures so callers can handle partial failures.

Comment on lines +114 to +136
// Bulk actions
const handleTagAll = async () => {
// Preserve user intent: only enable for folders that are not already enabled/completed
const toEnable = pending.map((f) => f.folder_id);
if (toEnable.length === 0) return;
await bulkEnableAITagging(toEnable);
};

const handleTagSelected = async () => {
if (selectedIds.length === 0) return;
await bulkEnableAITagging(selectedIds);
clearSelection();
};

const handleDisableSelected = async () => {
if (selectedIds.length === 0) return;
await bulkDisableAITagging(selectedIds);
clearSelection();
};

const anyMutationPending =
enableAITaggingPending || disableAITaggingPending || deleteFolderPending || bulkPending;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Handle bulk action errors to avoid unhandled promise rejections
If bulkEnableAITagging() rejects, this will bubble out of the click handler and can become an unhandled rejection (and selection won’t clear). Wrap in try/catch (and show feedback if needed).

   const handleTagSelected = async () => {
     if (selectedIds.length === 0) return;
-    await bulkEnableAITagging(selectedIds);
-    clearSelection();
+    try {
+      await bulkEnableAITagging(selectedIds);
+      clearSelection();
+    } catch {
+      // keep selection so user can retry / adjust
+    }
   };

Same for handleTagAll and handleDisableSelected.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Bulk actions
const handleTagAll = async () => {
// Preserve user intent: only enable for folders that are not already enabled/completed
const toEnable = pending.map((f) => f.folder_id);
if (toEnable.length === 0) return;
await bulkEnableAITagging(toEnable);
};
const handleTagSelected = async () => {
if (selectedIds.length === 0) return;
await bulkEnableAITagging(selectedIds);
clearSelection();
};
const handleDisableSelected = async () => {
if (selectedIds.length === 0) return;
await bulkDisableAITagging(selectedIds);
clearSelection();
};
const anyMutationPending =
enableAITaggingPending || disableAITaggingPending || deleteFolderPending || bulkPending;
// Bulk actions
const handleTagAll = async () => {
// Preserve user intent: only enable for folders that are not already enabled/completed
const toEnable = pending.map((f) => f.folder_id);
if (toEnable.length === 0) return;
await bulkEnableAITagging(toEnable);
};
const handleTagSelected = async () => {
if (selectedIds.length === 0) return;
try {
await bulkEnableAITagging(selectedIds);
clearSelection();
} catch {
// keep selection so user can retry / adjust
}
};
const handleDisableSelected = async () => {
if (selectedIds.length === 0) return;
await bulkDisableAITagging(selectedIds);
clearSelection();
};
const anyMutationPending =
enableAITaggingPending || disableAITaggingPending || deleteFolderPending || bulkPending;
🤖 Prompt for AI Agents
In frontend/src/pages/SettingsPage/components/FolderManagementCard.tsx around
lines 114 to 136, the bulk action handlers call async functions without catching
rejections which can produce unhandled promise rejections and prevent selection
clearing; wrap the awaits in try/catch and ensure selection is cleared in a
finally block (or after a successful call) so selection is always reset even if
the API fails, and optionally surface an error/notification to the user inside
the catch for feedback; apply this pattern to handleTagAll, handleTagSelected,
and handleDisableSelected.

Comment on lines 197 to +270
{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>

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

<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="space-y-4">
{/* Completed Section */}
<Section
title={`Completed (${completed.length})`}
collapsed={collapsed.completed}
onToggle={() => setCollapsed((c) => ({ ...c, completed: !c.completed }))}
>
{completed.map((folder: FolderDetails) => (
<FolderRow
key={folder.folder_id}
folder={folder}
pct={Math.round(taggingStatus[folder.folder_id]?.tagging_percentage ?? 100)}
checked={isSelected(folder.folder_id)}
onCheckedChange={() => toggleSelect(folder.folder_id)}
toggleAITagging={() => toggleAITagging(folder)}
deleteFolder={() => deleteFolder(folder.folder_id)}
disableAll={anyMutationPending}
/>
))}
</Section>

{/* In Progress Section */}
<Section
title={`In Progress (${inProgress.length})`}
collapsed={collapsed.inProgress}
onToggle={() => setCollapsed((c) => ({ ...c, inProgress: !c.inProgress }))}
>
{inProgress.map((folder: FolderDetails) => (
<FolderRow
key={folder.folder_id}
folder={folder}
pct={Math.round(taggingStatus[folder.folder_id]?.tagging_percentage ?? 0)}
checked={isSelected(folder.folder_id)}
onCheckedChange={() => toggleSelect(folder.folder_id)}
toggleAITagging={() => toggleAITagging(folder)}
deleteFolder={() => deleteFolder(folder.folder_id)}
disableAll={anyMutationPending}
/>
))}
</Section>

{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'
}
>
{(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>
)}
{/* Pending Section */}
<Section
title={`Pending (${pending.length})`}
collapsed={collapsed.pending}
onToggle={() => setCollapsed((c) => ({ ...c, pending: !c.pending }))}
>
<div className="mb-2 flex items-center gap-2">
<input
type="checkbox"
className="h-4 w-4 cursor-pointer accent-blue-600"
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
selectAllVisible(
pending.map((f) => f.folder_id),
e.currentTarget.checked,
)
}
/>
<span className="text-sm text-muted-foreground">Select all pending</span>
</div>
))}
{pending.map((folder: FolderDetails) => (
<FolderRow
key={folder.folder_id}
folder={folder}
pct={Math.round(taggingStatus[folder.folder_id]?.tagging_percentage ?? 0)}
checked={isSelected(folder.folder_id)}
onCheckedChange={() => toggleSelect(folder.folder_id)}
toggleAITagging={() => toggleAITagging(folder)}
deleteFolder={() => deleteFolder(folder.folder_id)}
disableAll={anyMutationPending}
/>
))}
</Section>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

“Completed” rows can display <100% despite being in Completed section
Completed classification includes folder.taggingCompleted, but you pass pct={Math.round(taggingStatus[...] ?? 100)}; if status exists and is <100, UI shows e.g. 95% in “Completed”. Clamp to 100 for completed rows.

 {completed.map((folder: FolderDetails) => (
   <FolderRow
     key={folder.folder_id}
     folder={folder}
-    pct={Math.round(taggingStatus[folder.folder_id]?.tagging_percentage ?? 100)}
+    pct={100}
     checked={isSelected(folder.folder_id)}
     onCheckedChange={() => toggleSelect(folder.folder_id)}
     toggleAITagging={() => toggleAITagging(folder)}
     deleteFolder={() => deleteFolder(folder.folder_id)}
     disableAll={anyMutationPending}
   />
 ))}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{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>
<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>
<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="space-y-4">
{/* Completed Section */}
<Section
title={`Completed (${completed.length})`}
collapsed={collapsed.completed}
onToggle={() => setCollapsed((c) => ({ ...c, completed: !c.completed }))}
>
{completed.map((folder: FolderDetails) => (
<FolderRow
key={folder.folder_id}
folder={folder}
pct={Math.round(taggingStatus[folder.folder_id]?.tagging_percentage ?? 100)}
checked={isSelected(folder.folder_id)}
onCheckedChange={() => toggleSelect(folder.folder_id)}
toggleAITagging={() => toggleAITagging(folder)}
deleteFolder={() => deleteFolder(folder.folder_id)}
disableAll={anyMutationPending}
/>
))}
</Section>
{/* In Progress Section */}
<Section
title={`In Progress (${inProgress.length})`}
collapsed={collapsed.inProgress}
onToggle={() => setCollapsed((c) => ({ ...c, inProgress: !c.inProgress }))}
>
{inProgress.map((folder: FolderDetails) => (
<FolderRow
key={folder.folder_id}
folder={folder}
pct={Math.round(taggingStatus[folder.folder_id]?.tagging_percentage ?? 0)}
checked={isSelected(folder.folder_id)}
onCheckedChange={() => toggleSelect(folder.folder_id)}
toggleAITagging={() => toggleAITagging(folder)}
deleteFolder={() => deleteFolder(folder.folder_id)}
disableAll={anyMutationPending}
/>
))}
</Section>
{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'
}
>
{(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>
)}
{/* Pending Section */}
<Section
title={`Pending (${pending.length})`}
collapsed={collapsed.pending}
onToggle={() => setCollapsed((c) => ({ ...c, pending: !c.pending }))}
>
<div className="mb-2 flex items-center gap-2">
<input
type="checkbox"
className="h-4 w-4 cursor-pointer accent-blue-600"
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
selectAllVisible(
pending.map((f) => f.folder_id),
e.currentTarget.checked,
)
}
/>
<span className="text-sm text-muted-foreground">Select all pending</span>
</div>
))}
{pending.map((folder: FolderDetails) => (
<FolderRow
key={folder.folder_id}
folder={folder}
pct={Math.round(taggingStatus[folder.folder_id]?.tagging_percentage ?? 0)}
checked={isSelected(folder.folder_id)}
onCheckedChange={() => toggleSelect(folder.folder_id)}
toggleAITagging={() => toggleAITagging(folder)}
deleteFolder={() => deleteFolder(folder.folder_id)}
disableAll={anyMutationPending}
/>
))}
</Section>
{folders.length > 0 ? (
<div className="space-y-4">
{/* Completed Section */}
<Section
title={`Completed (${completed.length})`}
collapsed={collapsed.completed}
onToggle={() => setCollapsed((c) => ({ ...c, completed: !c.completed }))}
>
{completed.map((folder: FolderDetails) => (
<FolderRow
key={folder.folder_id}
folder={folder}
pct={100}
checked={isSelected(folder.folder_id)}
onCheckedChange={() => toggleSelect(folder.folder_id)}
toggleAITagging={() => toggleAITagging(folder)}
deleteFolder={() => deleteFolder(folder.folder_id)}
disableAll={anyMutationPending}
/>
))}
</Section>
{/* In Progress Section */}
<Section
title={`In Progress (${inProgress.length})`}
collapsed={collapsed.inProgress}
onToggle={() => setCollapsed((c) => ({ ...c, inProgress: !c.inProgress }))}
>
{inProgress.map((folder: FolderDetails) => (
<FolderRow
key={folder.folder_id}
folder={folder}
pct={Math.round(taggingStatus[folder.folder_id]?.tagging_percentage ?? 0)}
checked={isSelected(folder.folder_id)}
onCheckedChange={() => toggleSelect(folder.folder_id)}
toggleAITagging={() => toggleAITagging(folder)}
deleteFolder={() => deleteFolder(folder.folder_id)}
disableAll={anyMutationPending}
/>
))}
</Section>
{/* Pending Section */}
<Section
title={`Pending (${pending.length})`}
collapsed={collapsed.pending}
onToggle={() => setCollapsed((c) => ({ ...c, pending: !c.pending }))}
>
<div className="mb-2 flex items-center gap-2">
<input
type="checkbox"
className="h-4 w-4 cursor-pointer accent-blue-600"
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
selectAllVisible(
pending.map((f) => f.folder_id),
e.currentTarget.checked,
)
}
/>
<span className="text-sm text-muted-foreground">Select all pending</span>
</div>
{pending.map((folder: FolderDetails) => (
<FolderRow
key={folder.folder_id}
folder={folder}
pct={Math.round(taggingStatus[folder.folder_id]?.tagging_percentage ?? 0)}
checked={isSelected(folder.folder_id)}
onCheckedChange={() => toggleSelect(folder.folder_id)}
toggleAITagging={() => toggleAITagging(folder)}
deleteFolder={() => deleteFolder(folder.folder_id)}
disableAll={anyMutationPending}
/>
))}
</Section>
🤖 Prompt for AI Agents
In frontend/src/pages/SettingsPage/components/FolderManagementCard.tsx around
lines 197-270, the Completed section passes
pct={Math.round(taggingStatus[folder.folder_id]?.tagging_percentage ?? 100)}
which can render <100% for folders marked completed; change the pct prop for
Completed rows to ensure it always shows 100 (either set pct={100}
unconditionally for completed folders or clamp the computed value with
Math.max(Math.round(...), 100) so any existing tagging_status <100 is coerced to
100 while keeping the 100 fallback).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants