refactor: simplify to inline tag editing only and fix tooltip z-index

Based on user feedback, simplified the implementation to focus on core
inline tag editing functionality by removing:

- BulkTagEditor component and bulk selection UI
- TagSuggestions component and autocomplete functionality
- useTagSuggestions hook and related caching
- Selection mode and bulk operations
- PRP files from repository (development artifacts)

Fixed tooltip z-index issue where "+N more..." tooltip appeared behind
other UI elements like the Recrawl button.

Kept essential features:
-  Inline tag editing (click to edit, Enter/Escape shortcuts)
-  Add/remove tags with "+" and "×" buttons
-  Tag editing in EditKnowledgeItemModal
-  Input validation and error handling
-  Toast notifications for success/error states
-  Proper tooltip layering (z-index: 100)

This maintains the core user experience while significantly reducing
complexity and removing features that weren't needed.

Resolves #538

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
leex279
2025-09-07 19:36:03 +02:00
parent a925efd2ac
commit 95c13cacec
6 changed files with 10 additions and 659 deletions

View File

@@ -1,336 +0,0 @@
import React, { useState, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { motion } from 'framer-motion';
import { X, Plus, Minus, Replace, RefreshCw, CheckCircle, AlertCircle } from 'lucide-react';
import { Card } from '../ui/Card';
import { Button } from '../ui/Button';
import { KnowledgeItem, knowledgeBaseService } from '../../services/knowledgeBaseService';
import { TagSuggestions } from './TagSuggestions';
import { EditableTags } from './EditableTags';
import { useTagSuggestions } from '../../hooks/useTagSuggestions';
interface BulkTagEditorProps {
selectedItems: KnowledgeItem[];
onClose: () => void;
onUpdate: () => void;
}
interface BulkOperationResult {
sourceId: string;
title: string;
success: boolean;
error?: string;
}
export const BulkTagEditor: React.FC<BulkTagEditorProps> = ({
selectedItems,
onClose,
onUpdate,
}) => {
const [selectedTag, setSelectedTag] = useState('');
const [replaceTags, setReplaceTags] = useState<string[]>([]);
const [isProcessing, setIsProcessing] = useState(false);
const [results, setResults] = useState<BulkOperationResult[]>([]);
const [showResults, setShowResults] = useState(false);
const { data: tagSuggestions = [], isLoading: isLoadingSuggestions, error: suggestionsError } = useTagSuggestions();
// Handle escape key to close modal
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape' && !isProcessing) onClose();
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [onClose, isProcessing]);
const performBulkOperation = async (
operation: 'add' | 'remove' | 'replace',
tagsToProcess: string[]
) => {
if (tagsToProcess.length === 0) return;
setIsProcessing(true);
setResults([]);
setShowResults(true);
// Process items in batches of 5 for better performance
const batchSize = 5;
const batches: KnowledgeItem[][] = [];
for (let i = 0; i < selectedItems.length; i += batchSize) {
batches.push(selectedItems.slice(i, i + batchSize));
}
const allResults: BulkOperationResult[] = [];
try {
for (const batch of batches) {
const batchPromises = batch.map(async (item): Promise<BulkOperationResult> => {
try {
const currentTags = item.metadata.tags || [];
let newTags: string[] = [];
switch (operation) {
case 'add':
// Add tags that don't already exist
newTags = [...new Set([...currentTags, ...tagsToProcess])];
break;
case 'remove':
// Remove specified tags
newTags = currentTags.filter(tag => !tagsToProcess.includes(tag));
break;
case 'replace':
// Replace all tags with new ones
newTags = [...tagsToProcess];
break;
}
await knowledgeBaseService.updateKnowledgeItemTags(item.source_id, newTags);
return {
sourceId: item.source_id,
title: item.title,
success: true,
};
} catch (error) {
return {
sourceId: item.source_id,
title: item.title,
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
});
const batchResults = await Promise.all(batchPromises);
allResults.push(...batchResults);
setResults([...allResults]); // Update results incrementally
}
} catch (error) {
console.error('Bulk operation failed:', error);
} finally {
setIsProcessing(false);
onUpdate(); // Refresh the parent component
}
};
const handleAddTags = () => {
if (selectedTag.trim()) {
performBulkOperation('add', [selectedTag.trim()]);
setSelectedTag('');
}
};
const handleRemoveTags = () => {
if (selectedTag.trim()) {
performBulkOperation('remove', [selectedTag.trim()]);
setSelectedTag('');
}
};
const handleReplaceTags = async () => {
performBulkOperation('replace', replaceTags);
};
const successCount = results.filter(r => r.success).length;
const errorCount = results.filter(r => !r.success).length;
return createPortal(
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 flex items-center justify-center z-50 bg-black/60 backdrop-blur-sm"
onClick={!isProcessing ? onClose : undefined}
>
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
className="relative w-full max-w-2xl max-h-[90vh] overflow-hidden"
onClick={(e) => e.stopPropagation()}
>
{/* Purple accent line at the top */}
<div className="absolute top-0 left-0 right-0 h-[2px] bg-gradient-to-r from-purple-500 to-pink-500 shadow-[0_0_20px_5px_rgba(168,85,247,0.5)] z-10 rounded-t-xl"></div>
<Card className="relative overflow-hidden h-full">
<div className="flex flex-col h-full max-h-[85vh]">
{/* Header */}
<div className="flex items-center justify-between mb-6 flex-shrink-0">
<div>
<h2 className="text-xl font-semibold text-gray-800 dark:text-white">
Bulk Tag Editor
</h2>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
Editing tags for {selectedItems.length} items
</p>
</div>
<button
onClick={!isProcessing ? onClose : undefined}
disabled={isProcessing}
className="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition-colors disabled:opacity-50"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="flex-1 overflow-y-auto space-y-6">
{/* Tag Operations */}
{!showResults && (
<div className="space-y-6">
{/* Add/Remove Tags Section */}
<div className="space-y-4">
<h3 className="text-lg font-medium text-gray-800 dark:text-white">
Add or Remove Tags
</h3>
<div className="flex gap-2">
<div className="flex-1">
<TagSuggestions
suggestions={tagSuggestions || []}
onSelect={setSelectedTag}
placeholder="Select or type a tag..."
isLoading={isLoadingSuggestions}
allowCustomValue={true}
/>
</div>
<Button
onClick={handleAddTags}
disabled={!selectedTag.trim() || isProcessing}
className="flex items-center gap-2 bg-green-600 hover:bg-green-700 text-white"
>
<Plus className="w-4 h-4" />
Add to All
</Button>
<Button
onClick={handleRemoveTags}
disabled={!selectedTag.trim() || isProcessing}
className="flex items-center gap-2 bg-red-600 hover:bg-red-700 text-white"
>
<Minus className="w-4 h-4" />
Remove from All
</Button>
</div>
</div>
{/* Replace All Tags Section */}
<div className="space-y-4">
<h3 className="text-lg font-medium text-gray-800 dark:text-white">
Replace All Tags
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">
This will replace all existing tags with the tags you specify below.
</p>
<div className="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<EditableTags
tags={replaceTags}
onTagsUpdate={async (tags) => {
setReplaceTags(tags);
}}
maxVisibleTags={10}
isUpdating={false}
/>
</div>
<Button
onClick={handleReplaceTags}
disabled={isProcessing}
className="flex items-center gap-2 bg-orange-600 hover:bg-orange-700 text-white"
>
<Replace className="w-4 h-4" />
Replace All Tags
</Button>
</div>
</div>
)}
{/* Results Section */}
{showResults && (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-medium text-gray-800 dark:text-white">
Operation Results
</h3>
<div className="flex gap-4 text-sm">
<span className="flex items-center gap-1 text-green-600">
<CheckCircle className="w-4 h-4" />
{successCount} Success
</span>
{errorCount > 0 && (
<span className="flex items-center gap-1 text-red-600">
<AlertCircle className="w-4 h-4" />
{errorCount} Failed
</span>
)}
</div>
</div>
<div className="space-y-2 max-h-60 overflow-y-auto">
{results.map((result) => (
<div
key={result.sourceId}
className={`flex items-center justify-between p-3 rounded-lg border ${
result.success
? 'bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800'
: 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800'
}`}
>
<div className="flex items-center gap-2">
{result.success ? (
<CheckCircle className="w-4 h-4 text-green-600" />
) : (
<AlertCircle className="w-4 h-4 text-red-600" />
)}
<span className="font-medium text-sm">
{result.title}
</span>
</div>
{result.error && (
<span className="text-xs text-red-600">
{result.error}
</span>
)}
</div>
))}
{isProcessing && results.length < selectedItems.length && (
<div className="flex items-center justify-center p-3">
<RefreshCw className="w-4 h-4 animate-spin mr-2" />
<span className="text-sm text-gray-600 dark:text-gray-400">
Processing... ({results.length}/{selectedItems.length})
</span>
</div>
)}
</div>
</div>
)}
</div>
{/* Footer */}
<div className="flex justify-end gap-3 pt-4 mt-6 border-t border-gray-200 dark:border-gray-700 flex-shrink-0">
{showResults ? (
<Button
onClick={onClose}
disabled={isProcessing}
accentColor="purple"
>
Done
</Button>
) : (
<Button
onClick={onClose}
variant="outline"
disabled={isProcessing}
>
Cancel
</Button>
)}
</div>
</div>
</Card>
</motion.div>
</motion.div>,
document.body
);
};

View File

@@ -340,7 +340,7 @@ export const EditableTags: React.FC<EditableTagsProps> = ({
+{remainingTags.length} more...
</Badge>
{showTooltip && (
<div className="absolute top-full mt-2 left-1/2 transform -translate-x-1/2 bg-black dark:bg-zinc-800 text-white text-xs rounded-lg py-2 px-3 shadow-lg z-50 whitespace-nowrap max-w-xs">
<div className="absolute top-full mt-2 left-1/2 transform -translate-x-1/2 bg-black dark:bg-zinc-800 text-white text-xs rounded-lg py-2 px-3 shadow-lg z-[100] whitespace-nowrap max-w-xs">
<div className="font-semibold text-purple-300 mb-1">
Additional Tags:
</div>

View File

@@ -2,7 +2,6 @@ import { useState } from 'react';
import { Link as LinkIcon, Upload, Trash2, RefreshCw, Code, FileText, Brain, BoxIcon, Pencil } from 'lucide-react';
import { Card } from '../ui/Card';
import { Badge } from '../ui/Badge';
import { Checkbox } from '../ui/Checkbox';
import { KnowledgeItem, knowledgeBaseService } from '../../services/knowledgeBaseService';
import { useCardTilt } from '../../hooks/useCardTilt';
import { CodeViewerModal, CodeExample } from '../code/CodeViewerModal';
@@ -73,9 +72,6 @@ interface KnowledgeItemCardProps {
onUpdate?: () => void;
onRefresh?: (sourceId: string) => void;
onBrowseDocuments?: (sourceId: string) => void;
isSelectionMode?: boolean;
isSelected?: boolean;
onToggleSelection?: (event: React.MouseEvent) => void;
}
export const KnowledgeItemCard = ({
@@ -84,9 +80,6 @@ export const KnowledgeItemCard = ({
onUpdate,
onRefresh,
onBrowseDocuments,
isSelectionMode = false,
isSelected = false,
onToggleSelection
}: KnowledgeItemCardProps) => {
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [showCodeModal, setShowCodeModal] = useState(false);
@@ -142,10 +135,10 @@ export const KnowledgeItemCard = ({
const sourceIconColor = getSourceIconColor();
const typeIconColor = getTypeIconColor();
// Use the tilt effect hook - disable in selection mode
// Use the tilt effect hook
const { cardRef, tiltStyles, handlers } = useCardTilt({
max: isSelectionMode ? 0 : 10,
scale: isSelectionMode ? 1 : 1.02,
max: 10,
scale: 1.02,
perspective: 1200,
});
@@ -241,26 +234,8 @@ export const KnowledgeItemCard = ({
>
<Card
accentColor={accentColor}
className={`relative h-full flex flex-col overflow-hidden ${
isSelected ? 'ring-2 ring-blue-500 dark:ring-blue-400' : ''
} ${isSelectionMode ? 'cursor-pointer' : ''}`}
onClick={(e) => {
if (isSelectionMode && onToggleSelection) {
e.stopPropagation();
onToggleSelection(e);
}
}}
className="relative h-full flex flex-col overflow-hidden"
>
{/* Checkbox for selection mode */}
{isSelectionMode && (
<div className="absolute top-3 right-3 z-20">
<Checkbox
checked={isSelected}
onChange={() => {}}
className="pointer-events-none"
/>
</div>
)}
{/* Reflection overlay */}
<div
@@ -306,8 +281,7 @@ export const KnowledgeItemCard = ({
<h3 className="text-gray-800 dark:text-white font-medium flex-1 line-clamp-1 truncate min-w-0">
{item.title}
</h3>
{!isSelectionMode && (
<div className="flex items-center gap-1 flex-shrink-0">
<div className="flex items-center gap-1 flex-shrink-0">
<button
onClick={(e) => {
e.stopPropagation();
@@ -329,7 +303,6 @@ export const KnowledgeItemCard = ({
<Trash2 className="w-3 h-3" />
</button>
</div>
)}
</div>
{/* Description section - fixed height */}

View File

@@ -1,44 +0,0 @@
import React from 'react';
import { ComboBox, ComboBoxOption } from '../../features/ui/primitives/combobox';
interface TagSuggestionsProps {
suggestions: string[];
onSelect: (tag: string) => void;
placeholder?: string;
allowCustomValue?: boolean;
className?: string;
isLoading?: boolean;
}
export const TagSuggestions: React.FC<TagSuggestionsProps> = ({
suggestions = [],
onSelect,
placeholder = 'Search or create tag...',
allowCustomValue = true,
className,
isLoading = false,
}) => {
// Convert string suggestions to ComboBox options
const options: ComboBoxOption[] = suggestions.map((tag) => ({
value: tag,
label: tag,
description: undefined,
}));
const handleValueChange = (value: string) => {
onSelect(value);
};
return (
<ComboBox
options={options}
onValueChange={handleValueChange}
placeholder={placeholder}
searchPlaceholder="Type to search tags..."
emptyMessage="No tags found"
allowCustomValue={allowCustomValue}
isLoading={isLoading}
className={className}
/>
);
};

View File

@@ -1,79 +0,0 @@
import { useQuery } from "@tanstack/react-query";
import { knowledgeBaseService } from "../services/knowledgeBaseService";
interface TagSuggestionsResult {
data?: string[];
isLoading: boolean;
error: Error | null;
isError: boolean;
}
/**
* Hook to fetch and manage tag suggestions from knowledge base
* Uses TanStack Query for caching and deduplication
*/
export const useTagSuggestions = (): TagSuggestionsResult => {
const queryResult = useQuery({
queryKey: ["knowledge-base", "tags", "suggestions"],
queryFn: async (): Promise<string[]> => {
try {
// Get all knowledge items to extract tags
const response = await knowledgeBaseService.getKnowledgeItems({ per_page: 1000 });
// Extract all tags from all items
const allTags: string[] = [];
const tagFrequency: Record<string, number> = {};
response.items.forEach(item => {
if (item.metadata.tags && Array.isArray(item.metadata.tags)) {
item.metadata.tags.forEach(tag => {
if (typeof tag === 'string' && tag.trim()) {
const cleanTag = tag.trim();
allTags.push(cleanTag);
tagFrequency[cleanTag] = (tagFrequency[cleanTag] || 0) + 1;
}
});
}
});
// Deduplicate and sort by frequency (most used first)
const uniqueTags = Array.from(new Set(allTags));
const sortedTags = uniqueTags.sort((a, b) => {
const freqA = tagFrequency[a] || 0;
const freqB = tagFrequency[b] || 0;
// Sort by frequency (descending), then alphabetically if same frequency
if (freqA !== freqB) {
return freqB - freqA;
}
return a.toLowerCase().localeCompare(b.toLowerCase());
});
// eslint-disable-next-line no-console
console.log(`📋 [TagSuggestions] Found ${sortedTags.length} unique tags from ${response.items.length} items`);
// eslint-disable-next-line no-console
console.log(`📋 [TagSuggestions] Top tags:`, sortedTags.slice(0, 10));
return sortedTags;
} catch (error) {
const errorMessage = error instanceof Error
? `Failed to fetch tag suggestions: ${error.message}`
: 'Failed to fetch tag suggestions: Unknown error occurred';
console.error('❌ [TagSuggestions] Error details:', error);
throw new Error(errorMessage); // Let TanStack Query handle the error state
}
},
staleTime: 5 * 60 * 1000, // 5 minutes
refetchOnWindowFocus: false, // Don't refetch on window focus
retry: 2, // Retry failed requests twice
retryDelay: 1000, // Wait 1 second between retries
});
return {
data: queryResult.data,
isLoading: queryResult.isLoading,
error: queryResult.error,
isError: queryResult.isError,
};
};

View File

@@ -1,10 +1,8 @@
import { useEffect, useState, useRef, useMemo } from 'react';
import { Search, Grid, Plus, Filter, BoxIcon, List, BookOpen, CheckSquare, Brain } from 'lucide-react';
import { useEffect, useState, useMemo } from 'react';
import { Search, Grid, Plus, Filter, BoxIcon, List, BookOpen, Brain } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { Card } from '../components/ui/Card';
import { Button } from '../components/ui/Button';
import { Input } from '../components/ui/Input';
import { Badge } from '../components/ui/Badge';
import { useStaggeredEntrance } from '../hooks/useStaggeredEntrance';
import { useToast } from '../contexts/ToastContext';
import { knowledgeBaseService, KnowledgeItem, KnowledgeItemMetadata } from '../services/knowledgeBaseService';
@@ -17,7 +15,6 @@ import { GroupCreationModal } from '../components/knowledge-base/GroupCreationMo
import { AddKnowledgeModal } from '../components/knowledge-base/AddKnowledgeModal';
import { CrawlingTab } from '../components/knowledge-base/CrawlingTab';
import { DocumentBrowser } from '../components/knowledge-base/DocumentBrowser';
import { BulkTagEditor } from '../components/knowledge-base/BulkTagEditor';
interface GroupedKnowledgeItem {
id: string;
@@ -34,11 +31,9 @@ export const KnowledgeBasePage = () => {
const [searchQuery, setSearchQuery] = useState('');
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
const [isGroupModalOpen, setIsGroupModalOpen] = useState(false);
const [isBulkEditModalOpen, setIsBulkEditModalOpen] = useState(false);
const [typeFilter, setTypeFilter] = useState<'all' | 'technical' | 'business'>('all');
const [knowledgeItems, setKnowledgeItems] = useState<KnowledgeItem[]>([]);
const [loading, setLoading] = useState(true);
const [totalItems, setTotalItems] = useState(0);
const [progressItems, setProgressItemsRaw] = useState<CrawlProgressData[]>([]);
const [showCrawlingTab, setShowCrawlingTab] = useState(false);
@@ -51,10 +46,6 @@ export const KnowledgeBasePage = () => {
});
};
// Selection state
const [selectedItems, setSelectedItems] = useState<Set<string>>(new Set());
const [isSelectionMode, setIsSelectionMode] = useState(false);
const [lastSelectedIndex, setLastSelectedIndex] = useState<number | null>(null);
// Document browser state
const [documentBrowserSourceId, setDocumentBrowserSourceId] = useState<string | null>(null);
@@ -71,7 +62,6 @@ export const KnowledgeBasePage = () => {
per_page: 100
});
setKnowledgeItems(response.items);
setTotalItems(response.total);
} catch (error) {
console.error('Failed to load knowledge items:', error);
showToast('Failed to load knowledge items', 'error');
@@ -280,96 +270,7 @@ export const KnowledgeBasePage = () => {
setIsDocumentBrowserOpen(true);
};
const toggleSelectionMode = () => {
setIsSelectionMode(!isSelectionMode);
if (isSelectionMode) {
setSelectedItems(new Set());
setLastSelectedIndex(null);
}
};
const toggleItemSelection = (itemId: string, index: number, event: React.MouseEvent) => {
const newSelected = new Set(selectedItems);
if (event.shiftKey && lastSelectedIndex !== null) {
const start = Math.min(lastSelectedIndex, index);
const end = Math.max(lastSelectedIndex, index);
for (let i = start; i <= end; i++) {
if (filteredItems[i]) {
newSelected.add(filteredItems[i].id);
}
}
} else if (event.ctrlKey || event.metaKey) {
if (newSelected.has(itemId)) {
newSelected.delete(itemId);
} else {
newSelected.add(itemId);
}
} else {
if (newSelected.has(itemId)) {
newSelected.delete(itemId);
} else {
newSelected.add(itemId);
}
}
setSelectedItems(newSelected);
setLastSelectedIndex(index);
};
const selectAll = () => {
const allIds = new Set(filteredItems.map(item => item.id));
setSelectedItems(allIds);
};
const deselectAll = () => {
setSelectedItems(new Set());
setLastSelectedIndex(null);
};
const deleteSelectedItems = async () => {
if (selectedItems.size === 0) return;
const count = selectedItems.size;
const confirmed = window.confirm(`Are you sure you want to delete ${count} selected item${count > 1 ? 's' : ''}?`);
if (!confirmed) return;
try {
const deletePromises = Array.from(selectedItems).map(itemId =>
knowledgeBaseService.deleteKnowledgeItem(itemId)
);
await Promise.all(deletePromises);
setKnowledgeItems(prev => prev.filter(item => !selectedItems.has(item.id)));
setSelectedItems(new Set());
setIsSelectionMode(false);
showToast(`Successfully deleted ${count} item${count > 1 ? 's' : ''}`, 'success');
} catch (error) {
console.error('Failed to delete selected items:', error);
showToast('Failed to delete some items', 'error');
}
};
// Keyboard shortcuts
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'a' && isSelectionMode) {
e.preventDefault();
selectAll();
}
if (e.key === 'Escape' && isSelectionMode) {
toggleSelectionMode();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isSelectionMode, filteredItems]);
const handleRefreshItem = async (sourceId: string) => {
try {
@@ -656,16 +557,6 @@ export const KnowledgeBasePage = () => {
</button>
</div>
<Button
onClick={toggleSelectionMode}
variant={isSelectionMode ? "secondary" : "ghost"}
accentColor="blue"
className={isSelectionMode ? "bg-blue-500/10 border-blue-500/40" : ""}
>
<CheckSquare className="w-4 h-4 mr-2 inline" />
<span>{isSelectionMode ? 'Cancel' : 'Select'}</span>
</Button>
<Button
onClick={handleAddKnowledge}
variant="primary"
@@ -678,44 +569,6 @@ export const KnowledgeBasePage = () => {
</motion.div>
</motion.div>
{/* Selection Toolbar */}
<AnimatePresence>
{isSelectionMode && selectedItems.size > 0 && (
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="mb-6"
>
<Card className="p-4 bg-gradient-to-r from-blue-500/10 to-purple-500/10 border-blue-500/20">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
{selectedItems.size} item{selectedItems.size > 1 ? 's' : ''} selected
</span>
<Button onClick={selectAll} variant="ghost" size="sm" accentColor="blue">
Select All
</Button>
<Button onClick={deselectAll} variant="ghost" size="sm" accentColor="gray">
Clear Selection
</Button>
</div>
<div className="flex items-center gap-2">
<Button onClick={() => setIsBulkEditModalOpen(true)} variant="secondary" size="sm" accentColor="purple">
Edit Tags
</Button>
<Button onClick={() => setIsGroupModalOpen(true)} variant="secondary" size="sm" accentColor="blue">
Create Group
</Button>
<Button onClick={deleteSelectedItems} variant="secondary" size="sm" accentColor="pink">
Delete Selected
</Button>
</div>
</div>
</Card>
</motion.div>
)}
</AnimatePresence>
{/* Active Crawls Tab */}
{showCrawlingTab && progressItems.length > 0 && (
@@ -757,7 +610,7 @@ export const KnowledgeBasePage = () => {
</motion.div>
))}
{ungroupedItems.map((item, index) => (
{ungroupedItems.map((item, _index) => (
<motion.div key={item.id} variants={contentItemVariants}>
<KnowledgeItemCard
item={item}
@@ -765,9 +618,6 @@ export const KnowledgeBasePage = () => {
onUpdate={loadKnowledgeItems}
onRefresh={handleRefreshItem}
onBrowseDocuments={handleBrowseDocuments}
isSelectionMode={isSelectionMode}
isSelected={selectedItems.has(item.id)}
onToggleSelection={(e) => toggleItemSelection(item.id, index, e)}
/>
</motion.div>
))}
@@ -797,11 +647,10 @@ export const KnowledgeBasePage = () => {
{isGroupModalOpen && (
<GroupCreationModal
selectedItems={knowledgeItems.filter(item => selectedItems.has(item.id))}
selectedItems={[]}
onClose={() => setIsGroupModalOpen(false)}
onSuccess={() => {
setIsGroupModalOpen(false);
toggleSelectionMode();
loadKnowledgeItems();
}}
/>
@@ -819,18 +668,6 @@ export const KnowledgeBasePage = () => {
/>
)}
{isBulkEditModalOpen && (
<BulkTagEditor
selectedItems={knowledgeItems.filter(item => selectedItems.has(item.id))}
onClose={() => setIsBulkEditModalOpen(false)}
onUpdate={() => {
setIsBulkEditModalOpen(false);
setSelectedItems(new Set());
setIsSelectionMode(false);
loadKnowledgeItems();
}}
/>
)}
</div>
);
};