diff --git a/archon-ui-main/src/features/knowledge/components/AddKnowledgeDialog.tsx b/archon-ui-main/src/features/knowledge/components/AddKnowledgeDialog.tsx index 23d4372268..26834e9878 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 - - - - {/* Crawl Tab */} - -
- - setCrawlUrl(e.target.value)} - disabled={isProcessing} - /> -
+ {/* Enhanced Tab Buttons */} +
+ {/* Crawl Website Tab */} + -
-
- - + {/* Upload Document Tab */} + +
-
- - + {/* Crawl Tab */} + + {/* 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)} +
+ + +
+ + + + ); + })} +
+ + {/* 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 0000000000..b97ce2b234 --- /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 0000000000..6123cd0240 --- /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 2112f382fc..e9174d5b13 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";