From 7d37ef76db9de34b2486b2b575e69f20575c44ce Mon Sep 17 00:00:00 2001 From: DIY Smart Code Date: Tue, 16 Sep 2025 13:20:53 +0200 Subject: [PATCH] feat: Complete UX redesign of Add Knowledge Modal with modern glassmorphism styling (#661) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Complete UX redesign of Add Knowledge Modal with modern glassmorphism styling 🎨 Enhanced Tab Navigation - Replaced basic tabs with large, card-style buttons - Added glassmorphism effects with backdrop blur and gradients - Color-coded themes: Cyan for crawl, Purple for upload - Top accent glow bars for active states matching KnowledgeCard - Two-line layout with descriptive subtitles 🌐 Modern URL Input Enhancement - Added prominent Globe icon with proper visibility - Enhanced glassmorphism styling with gradient backgrounds - Larger input height for better interaction - Improved placeholder text with example URLs - Enhanced focus states with cyan glow effects 📁 Professional File Upload Area - Custom drag & drop zone replacing basic file input - Visual upload area with glassmorphism effects - Dynamic Upload icon with state-based colors - File name and size display when selected - Purple theme colors matching document context 🏷️ Visual Tag Management System - Replaced comma-separated input with modern tag pills - Individual tag removal with X buttons - Enter or comma to add tags (backward compatible) - Tag count display and proper accessibility - Blue accent colors matching knowledge base theme 🎯 Circular Level Selection - Replaced dropdown with visual circular selector - Clear representation of crawl depth (1,2,3,5 levels) - Informative tooltips with detailed explanations - Selection indicators with animations - Info icon with comprehensive guidance 📋 Knowledge Type Selection Enhancement - Replaced dropdown with large visual radio cards - Technical vs Business with distinct styling - Color-coded themes and descriptive icons - Enhanced selection indicators ✨ Technical Improvements - Created reusable LevelSelector, KnowledgeTypeSelector, TagInput components - Updated state management from strings to arrays for tags - Maintained backward compatibility with existing API - Enhanced accessibility with proper ARIA labels - Minimal bundle impact with optimized imports 🚀 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * fix: Improve TagInput race conditions and enhance LevelSelector accessibility 🏷️ TagInput Race Condition Fix: - Fixed race condition in handleInputChange when pasting comma-separated tags - Replaced forEach addTag loop with batched update approach - Use Set for proper deduplication of tags - Enforce maxTags limit on final combined array - Single onTagsChange call prevents multiple re-renders and stale state issues - Prevents duplicates and exceeding maxTags when pasting multiple tags ♿ LevelSelector Accessibility Enhancement: - Added proper radio group semantics with role="radiogroup" - Added aria-labelledby linking to crawl depth label - Each button now has role="radio" and aria-checked state - Implemented roving tabindex (selected=0, others=-1) - Added descriptive aria-label for each level option - Enhanced keyboard support with Enter/Space key handlers - Added proper focus ring styling for keyboard navigation - Improved screen reader experience with semantic structure 🚀 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * fix: Address CodeRabbit feedback - improve plural logic and remove unused dependency - Fix plural logic in LevelSelector.tsx for better readability - Remove unused @radix-ui/themes dependency to reduce bundle size - Update package-lock.json after dependency removal 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --------- Co-authored-by: Claude --- .../components/AddKnowledgeDialog.tsx | 276 +++++++++++------- .../components/KnowledgeTypeSelector.tsx | 162 ++++++++++ .../knowledge/components/LevelSelector.tsx | 166 +++++++++++ .../knowledge/components/TagInput.tsx | 143 +++++++++ .../features/knowledge/components/index.ts | 3 + 5 files changed, 644 insertions(+), 106 deletions(-) create mode 100644 archon-ui-main/src/features/knowledge/components/KnowledgeTypeSelector.tsx create mode 100644 archon-ui-main/src/features/knowledge/components/LevelSelector.tsx create mode 100644 archon-ui-main/src/features/knowledge/components/TagInput.tsx diff --git a/archon-ui-main/src/features/knowledge/components/AddKnowledgeDialog.tsx b/archon-ui-main/src/features/knowledge/components/AddKnowledgeDialog.tsx index 23d43722..26834e98 100644 --- a/archon-ui-main/src/features/knowledge/components/AddKnowledgeDialog.tsx +++ b/archon-ui-main/src/features/knowledge/components/AddKnowledgeDialog.tsx @@ -7,11 +7,14 @@ import { Globe, Loader2, Upload } from "lucide-react"; import { useId, useState } from "react"; import { useToast } from "../../ui/hooks/useToast"; import { Button, Input, Label } from "../../ui/primitives"; +import { cn } from "../../ui/primitives/styles"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "../../ui/primitives/dialog"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../ui/primitives/select"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../ui/primitives/tabs"; import { useCrawlUrl, useUploadDocument } from "../hooks"; import type { CrawlRequest, UploadMetadata } from "../types"; +import { KnowledgeTypeSelector } from "./KnowledgeTypeSelector"; +import { LevelSelector } from "./LevelSelector"; +import { TagInput } from "./TagInput"; interface AddKnowledgeDialogProps { open: boolean; @@ -33,32 +36,27 @@ export const AddKnowledgeDialog: React.FC = ({ // Generate unique IDs for form elements const urlId = useId(); - const typeId = useId(); - const depthId = useId(); - const tagsId = useId(); const fileId = useId(); - const uploadTypeId = useId(); - const uploadTagsId = useId(); // Crawl form state const [crawlUrl, setCrawlUrl] = useState(""); const [crawlType, setCrawlType] = useState<"technical" | "business">("technical"); const [maxDepth, setMaxDepth] = useState("2"); - const [tags, setTags] = useState(""); + const [tags, setTags] = useState([]); // Upload form state const [selectedFile, setSelectedFile] = useState(null); const [uploadType, setUploadType] = useState<"technical" | "business">("technical"); - const [uploadTags, setUploadTags] = useState(""); + const [uploadTags, setUploadTags] = useState([]); const resetForm = () => { setCrawlUrl(""); setCrawlType("technical"); setMaxDepth("2"); - setTags(""); + setTags([]); setSelectedFile(null); setUploadType("technical"); - setUploadTags(""); + setUploadTags([]); }; const handleCrawl = async () => { @@ -72,7 +70,7 @@ export const AddKnowledgeDialog: React.FC = ({ url: crawlUrl, knowledge_type: crawlType, max_depth: parseInt(maxDepth, 10), - tags: tags ? tags.split(",").map((t) => t.trim()) : undefined, + tags: tags.length > 0 ? tags : undefined, }; const response = await crawlMutation.mutateAsync(request); @@ -102,7 +100,7 @@ export const AddKnowledgeDialog: React.FC = ({ try { const metadata: UploadMetadata = { knowledge_type: uploadType, - tags: uploadTags ? uploadTags.split(",").map((t) => t.trim()) : undefined, + tags: uploadTags.length > 0 ? uploadTags : undefined, }; const response = await uploadMutation.mutateAsync({ file: selectedFile, metadata }); @@ -136,76 +134,118 @@ export const AddKnowledgeDialog: React.FC = ({ setActiveTab(v as "crawl" | "upload")}> - - - - Crawl Website - - - - Upload Document - - + {/* Enhanced Tab Buttons */} +
+ {/* Crawl Website Tab */} + + + {/* Upload Document Tab */} + +
{/* Crawl Tab */} - -
- - setCrawlUrl(e.target.value)} + + {/* Enhanced URL Input Section */} +
+ +
+
+ +
+ setCrawlUrl(e.target.value)} + disabled={isProcessing} + className="pl-10 h-12 backdrop-blur-md bg-gradient-to-r from-white/60 to-white/50 dark:from-black/60 dark:to-black/50 border-gray-300/60 dark:border-gray-600/60 focus:border-cyan-400/70 focus:shadow-[0_0_20px_rgba(34,211,238,0.15)]" + /> +
+

+ Enter the URL of a website you want to crawl for knowledge +

+
+ +
+ + +
-
-
- - -
- -
- - -
-
- -
- - setTags(e.target.value)} - disabled={isProcessing} - /> -
+ + + ); + })} +
+ + {/* Help text */} +
+ Choose the type that best describes your content +
+ + ); +}; \ No newline at end of file diff --git a/archon-ui-main/src/features/knowledge/components/LevelSelector.tsx b/archon-ui-main/src/features/knowledge/components/LevelSelector.tsx new file mode 100644 index 00000000..b97ce2b2 --- /dev/null +++ b/archon-ui-main/src/features/knowledge/components/LevelSelector.tsx @@ -0,0 +1,166 @@ +/** + * Level Selection Component + * Circular level selector for crawl depth using radio-like selection + */ + +import { motion } from "framer-motion"; +import { Check, Info } from "lucide-react"; +import { cn } from "../../ui/primitives/styles"; +import { SimpleTooltip } from "../../ui/primitives/tooltip"; + +interface LevelSelectorProps { + value: string; + onValueChange: (value: string) => void; + disabled?: boolean; +} + +const LEVELS = [ + { + value: "1", + label: "1", + description: "Single page only", + details: "1-50 pages • Best for: Single articles, specific pages" + }, + { + value: "2", + label: "2", + description: "Page + immediate links", + details: "10-200 pages • Best for: Documentation sections, blogs" + }, + { + value: "3", + label: "3", + description: "2 levels deep", + details: "50-500 pages • Best for: Entire sites, comprehensive docs" + }, + { + value: "5", + label: "5", + description: "Very deep crawling", + details: "100-1000+ pages • Warning: May include irrelevant content" + }, +]; + +export const LevelSelector: React.FC = ({ + value, + onValueChange, + disabled = false, +}) => { + const tooltipContent = ( +
+
Crawl Depth Level Explanations:
+ {LEVELS.map((level) => ( +
+
Level {level.value}: "{level.description}"
+
+ {level.details} +
+
+ ))} +
+
+ 💡 + More data isn't always better. Choose based on your needs. +
+
+
+ ); + + return ( +
+
+
+ Crawl Depth +
+ + + +
+
+ {LEVELS.map((level) => { + const isSelected = value === level.value; + + return ( + + + + + + ); + })} +
+ + {/* Help text */} +
+ Higher levels crawl deeper into the website structure +
+
+ ); +}; \ No newline at end of file diff --git a/archon-ui-main/src/features/knowledge/components/TagInput.tsx b/archon-ui-main/src/features/knowledge/components/TagInput.tsx new file mode 100644 index 00000000..6123cd02 --- /dev/null +++ b/archon-ui-main/src/features/knowledge/components/TagInput.tsx @@ -0,0 +1,143 @@ +/** + * Tag Input Component + * Visual tag management with add/remove functionality + */ + +import { motion } from "framer-motion"; +import { Plus, X } from "lucide-react"; +import { useState } from "react"; +import { Input } from "../../ui/primitives"; +import { cn } from "../../ui/primitives/styles"; + +interface TagInputProps { + tags: string[]; + onTagsChange: (tags: string[]) => void; + placeholder?: string; + disabled?: boolean; + maxTags?: number; +} + +export const TagInput: React.FC = ({ + tags, + onTagsChange, + placeholder = "Enter a tag and press Enter", + disabled = false, + maxTags = 10, +}) => { + const [inputValue, setInputValue] = useState(""); + + const addTag = (tag: string) => { + const trimmedTag = tag.trim(); + if ( + trimmedTag && + !tags.includes(trimmedTag) && + tags.length < maxTags + ) { + onTagsChange([...tags, trimmedTag]); + setInputValue(""); + } + }; + + const removeTag = (tagToRemove: string) => { + onTagsChange(tags.filter(tag => tag !== tagToRemove)); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" || e.key === ",") { + e.preventDefault(); + addTag(inputValue); + } else if (e.key === "Backspace" && !inputValue && tags.length > 0) { + // Remove last tag when backspace is pressed on empty input + removeTag(tags[tags.length - 1]); + } + }; + + const handleInputChange = (e: React.ChangeEvent) => { + const value = e.target.value; + // Handle comma-separated input for backwards compatibility + if (value.includes(",")) { + // Collect pasted candidates, trim and filter them + const newCandidates = value.split(",") + .map(tag => tag.trim()) + .filter(Boolean); + + // Merge with current tags using Set to dedupe + const combinedTags = new Set([...tags, ...newCandidates]); + const combinedArray = Array.from(combinedTags); + + // Enforce maxTags limit by taking only the first N allowed tags + const finalTags = combinedArray.slice(0, maxTags); + + // Single batched update + onTagsChange(finalTags); + setInputValue(""); + } else { + setInputValue(value); + } + }; + + return ( +
+
+ Tags +
+ + {/* Tag Display */} + {tags.length > 0 && ( +
+ {tags.map((tag, index) => ( + + {tag} + {!disabled && ( + + )} + + ))} +
+ )} + + {/* Tag Input */} +
+
+ +
+ = maxTags ? "Maximum tags reached" : placeholder} + disabled={disabled || tags.length >= maxTags} + className="pl-9 backdrop-blur-md bg-gradient-to-r from-white/60 to-white/50 dark:from-black/60 dark:to-black/50 border-gray-300/60 dark:border-gray-600/60 focus:border-blue-400/70 focus:shadow-[0_0_15px_rgba(59,130,246,0.15)]" + /> +
+ + {/* Help Text */} +
+

Press Enter or comma to add tags • Backspace to remove last tag

+ {maxTags && ( +

{tags.length}/{maxTags} tags used

+ )} +
+
+ ); +}; \ No newline at end of file diff --git a/archon-ui-main/src/features/knowledge/components/index.ts b/archon-ui-main/src/features/knowledge/components/index.ts index 2112f382..e9174d5b 100644 --- a/archon-ui-main/src/features/knowledge/components/index.ts +++ b/archon-ui-main/src/features/knowledge/components/index.ts @@ -2,3 +2,6 @@ export * from "./AddKnowledgeDialog"; export * from "./DocumentBrowser"; export * from "./KnowledgeCard"; export * from "./KnowledgeList"; +export * from "./KnowledgeTypeSelector"; +export * from "./LevelSelector"; +export * from "./TagInput";