From 95c13cacec1e4b26d0a2f3127033a8787e63ed97 Mon Sep 17 00:00:00 2001 From: leex279 Date: Sun, 7 Sep 2025 19:36:03 +0200 Subject: [PATCH] refactor: simplify to inline tag editing only and fix tooltip z-index MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../knowledge-base/BulkTagEditor.tsx | 336 ------------------ .../knowledge-base/EditableTags.tsx | 2 +- .../knowledge-base/KnowledgeItemCard.tsx | 37 +- .../knowledge-base/TagSuggestions.tsx | 44 --- archon-ui-main/src/hooks/useTagSuggestions.ts | 79 ---- .../src/pages/KnowledgeBasePage.tsx | 171 +-------- 6 files changed, 10 insertions(+), 659 deletions(-) delete mode 100644 archon-ui-main/src/components/knowledge-base/BulkTagEditor.tsx delete mode 100644 archon-ui-main/src/components/knowledge-base/TagSuggestions.tsx delete mode 100644 archon-ui-main/src/hooks/useTagSuggestions.ts diff --git a/archon-ui-main/src/components/knowledge-base/BulkTagEditor.tsx b/archon-ui-main/src/components/knowledge-base/BulkTagEditor.tsx deleted file mode 100644 index 04df5db9..00000000 --- a/archon-ui-main/src/components/knowledge-base/BulkTagEditor.tsx +++ /dev/null @@ -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 = ({ - selectedItems, - onClose, - onUpdate, -}) => { - const [selectedTag, setSelectedTag] = useState(''); - const [replaceTags, setReplaceTags] = useState([]); - const [isProcessing, setIsProcessing] = useState(false); - const [results, setResults] = useState([]); - 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 => { - 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( - - e.stopPropagation()} - > - {/* Purple accent line at the top */} -
- - -
- {/* Header */} -
-
-

- Bulk Tag Editor -

-

- Editing tags for {selectedItems.length} items -

-
- -
- -
- {/* Tag Operations */} - {!showResults && ( -
- {/* Add/Remove Tags Section */} -
-

- Add or Remove Tags -

- -
-
- -
- - -
-
- - {/* Replace All Tags Section */} -
-

- Replace All Tags -

-

- This will replace all existing tags with the tags you specify below. -

- -
- { - setReplaceTags(tags); - }} - maxVisibleTags={10} - isUpdating={false} - /> -
- - -
-
- )} - - {/* Results Section */} - {showResults && ( -
-
-

- Operation Results -

-
- - - {successCount} Success - - {errorCount > 0 && ( - - - {errorCount} Failed - - )} -
-
- -
- {results.map((result) => ( -
-
- {result.success ? ( - - ) : ( - - )} - - {result.title} - -
- {result.error && ( - - {result.error} - - )} -
- ))} - - {isProcessing && results.length < selectedItems.length && ( -
- - - Processing... ({results.length}/{selectedItems.length}) - -
- )} -
-
- )} -
- - {/* Footer */} -
- {showResults ? ( - - ) : ( - - )} -
-
-
-
-
, - document.body - ); -}; \ No newline at end of file diff --git a/archon-ui-main/src/components/knowledge-base/EditableTags.tsx b/archon-ui-main/src/components/knowledge-base/EditableTags.tsx index bc568900..f9ff19ba 100644 --- a/archon-ui-main/src/components/knowledge-base/EditableTags.tsx +++ b/archon-ui-main/src/components/knowledge-base/EditableTags.tsx @@ -340,7 +340,7 @@ export const EditableTags: React.FC = ({ +{remainingTags.length} more... {showTooltip && ( -
+
Additional Tags:
diff --git a/archon-ui-main/src/components/knowledge-base/KnowledgeItemCard.tsx b/archon-ui-main/src/components/knowledge-base/KnowledgeItemCard.tsx index 1a5422a7..882b4225 100644 --- a/archon-ui-main/src/components/knowledge-base/KnowledgeItemCard.tsx +++ b/archon-ui-main/src/components/knowledge-base/KnowledgeItemCard.tsx @@ -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 = ({ > { - if (isSelectionMode && onToggleSelection) { - e.stopPropagation(); - onToggleSelection(e); - } - }} + className="relative h-full flex flex-col overflow-hidden" > - {/* Checkbox for selection mode */} - {isSelectionMode && ( -
- {}} - className="pointer-events-none" - /> -
- )} {/* Reflection overlay */}
{item.title} - {!isSelectionMode && ( -
+
- )}
{/* Description section - fixed height */} diff --git a/archon-ui-main/src/components/knowledge-base/TagSuggestions.tsx b/archon-ui-main/src/components/knowledge-base/TagSuggestions.tsx deleted file mode 100644 index 442840b2..00000000 --- a/archon-ui-main/src/components/knowledge-base/TagSuggestions.tsx +++ /dev/null @@ -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 = ({ - 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 ( - - ); -}; \ No newline at end of file diff --git a/archon-ui-main/src/hooks/useTagSuggestions.ts b/archon-ui-main/src/hooks/useTagSuggestions.ts deleted file mode 100644 index 411bfb4a..00000000 --- a/archon-ui-main/src/hooks/useTagSuggestions.ts +++ /dev/null @@ -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 => { - 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 = {}; - - 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, - }; -}; \ No newline at end of file diff --git a/archon-ui-main/src/pages/KnowledgeBasePage.tsx b/archon-ui-main/src/pages/KnowledgeBasePage.tsx index e8a46a1a..40c70ef6 100644 --- a/archon-ui-main/src/pages/KnowledgeBasePage.tsx +++ b/archon-ui-main/src/pages/KnowledgeBasePage.tsx @@ -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([]); const [loading, setLoading] = useState(true); - const [totalItems, setTotalItems] = useState(0); const [progressItems, setProgressItemsRaw] = useState([]); const [showCrawlingTab, setShowCrawlingTab] = useState(false); @@ -51,10 +46,6 @@ export const KnowledgeBasePage = () => { }); }; - // Selection state - const [selectedItems, setSelectedItems] = useState>(new Set()); - const [isSelectionMode, setIsSelectionMode] = useState(false); - const [lastSelectedIndex, setLastSelectedIndex] = useState(null); // Document browser state const [documentBrowserSourceId, setDocumentBrowserSourceId] = useState(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 = () => {
- - - -
-
- - - -
-
- - - )} - {/* Active Crawls Tab */} {showCrawlingTab && progressItems.length > 0 && ( @@ -757,7 +610,7 @@ export const KnowledgeBasePage = () => { ))} - {ungroupedItems.map((item, index) => ( + {ungroupedItems.map((item, _index) => ( { onUpdate={loadKnowledgeItems} onRefresh={handleRefreshItem} onBrowseDocuments={handleBrowseDocuments} - isSelectionMode={isSelectionMode} - isSelected={selectedItems.has(item.id)} - onToggleSelection={(e) => toggleItemSelection(item.id, index, e)} /> ))} @@ -797,11 +647,10 @@ export const KnowledgeBasePage = () => { {isGroupModalOpen && ( selectedItems.has(item.id))} + selectedItems={[]} onClose={() => setIsGroupModalOpen(false)} onSuccess={() => { setIsGroupModalOpen(false); - toggleSelectionMode(); loadKnowledgeItems(); }} /> @@ -819,18 +668,6 @@ export const KnowledgeBasePage = () => { /> )} - {isBulkEditModalOpen && ( - selectedItems.has(item.id))} - onClose={() => setIsBulkEditModalOpen(false)} - onUpdate={() => { - setIsBulkEditModalOpen(false); - setSelectedItems(new Set()); - setIsSelectionMode(false); - loadKnowledgeItems(); - }} - /> - )} ); }; \ No newline at end of file