mirror of
https://github.com/coleam00/Archon.git
synced 2025-12-24 02:39:17 -05:00
Refactoring the UI for consistent styling
This commit is contained in:
8
archon-ui-main/package-lock.json
generated
8
archon-ui-main/package-lock.json
generated
@@ -48,6 +48,7 @@
|
||||
"@testing-library/react": "^14.3.1",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@types/node": "^20.19.0",
|
||||
"@types/prismjs": "^1.26.5",
|
||||
"@types/react": "^18.3.1",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
@@ -4374,6 +4375,13 @@
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/prismjs": {
|
||||
"version": "1.26.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz",
|
||||
"integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/prop-types": {
|
||||
"version": "15.7.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
|
||||
|
||||
@@ -62,10 +62,13 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.2.2",
|
||||
"@tailwindcss/postcss": "4.1.2",
|
||||
"@tailwindcss/vite": "4.1.2",
|
||||
"@testing-library/jest-dom": "^6.4.6",
|
||||
"@testing-library/react": "^14.3.1",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@types/node": "^20.19.0",
|
||||
"@types/prismjs": "^1.26.5",
|
||||
"@types/react": "^18.3.1",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
@@ -78,8 +81,6 @@
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.1",
|
||||
"jsdom": "^24.1.0",
|
||||
"@tailwindcss/postcss": "4.1.2",
|
||||
"@tailwindcss/vite": "4.1.2",
|
||||
"postcss": "latest",
|
||||
"tailwindcss": "4.1.2",
|
||||
"ts-node": "^10.9.1",
|
||||
|
||||
@@ -8,8 +8,8 @@ import { useId, useState } from "react";
|
||||
import { useToast } from "@/features/shared/hooks/useToast";
|
||||
import { Button, Input, Label } from "../../ui/primitives";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "../../ui/primitives/dialog";
|
||||
import { cn } from "../../ui/primitives/styles";
|
||||
import { Tabs, TabsContent } from "../../ui/primitives/tabs";
|
||||
import { cn, glassCard } from "../../ui/primitives/styles";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../ui/primitives/tabs";
|
||||
import { useCrawlUrl, useUploadDocument } from "../hooks";
|
||||
import type { CrawlRequest, UploadMetadata } from "../types";
|
||||
import { KnowledgeTypeSelector } from "./KnowledgeTypeSelector";
|
||||
@@ -134,59 +134,17 @@ export const AddKnowledgeDialog: React.FC<AddKnowledgeDialogProps> = ({
|
||||
</DialogHeader>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as "crawl" | "upload")}>
|
||||
{/* Enhanced Tab Buttons */}
|
||||
<div className="grid grid-cols-2 gap-3 p-2 rounded-xl backdrop-blur-md bg-gradient-to-b from-gray-100/30 via-gray-50/20 to-white/40 dark:from-gray-900/30 dark:via-gray-800/20 dark:to-black/40 border border-gray-200/40 dark:border-gray-700/40">
|
||||
{/* Crawl Website Tab */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab("crawl")}
|
||||
className={cn(
|
||||
"relative flex items-center justify-center gap-3 px-6 py-4 rounded-lg transition-all duration-300",
|
||||
"backdrop-blur-md border-2 font-medium text-sm",
|
||||
activeTab === "crawl"
|
||||
? "bg-gradient-to-b from-cyan-100/70 via-cyan-50/40 to-white/80 dark:from-cyan-900/40 dark:via-cyan-800/25 dark:to-black/50 border-cyan-400/60 text-cyan-700 dark:text-cyan-300 shadow-[0_0_20px_rgba(34,211,238,0.25)]"
|
||||
: "bg-gradient-to-b from-white/40 via-white/30 to-white/60 dark:from-gray-800/40 dark:via-gray-800/30 dark:to-black/60 border-gray-300/40 dark:border-gray-600/40 text-gray-600 dark:text-gray-300 hover:border-cyan-300/50 hover:text-cyan-600 dark:hover:text-cyan-400 hover:shadow-[0_0_15px_rgba(34,211,238,0.15)]",
|
||||
)}
|
||||
>
|
||||
{/* Top accent glow for active state */}
|
||||
{activeTab === "crawl" && (
|
||||
<div className="pointer-events-none absolute inset-x-0 top-0">
|
||||
<div className="mx-2 mt-0.5 h-[2px] rounded-full bg-cyan-500" />
|
||||
<div className="-mt-1 h-8 w-full bg-gradient-to-b from-cyan-500/30 to-transparent blur-md" />
|
||||
</div>
|
||||
)}
|
||||
<Globe className={cn("w-5 h-5", activeTab === "crawl" ? "text-cyan-500" : "text-current")} />
|
||||
<div className="flex flex-col items-start gap-0.5">
|
||||
<span className="font-semibold">Crawl Website</span>
|
||||
<span className="text-xs opacity-80">Scan web pages</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Upload Document Tab */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab("upload")}
|
||||
className={cn(
|
||||
"relative flex items-center justify-center gap-3 px-6 py-4 rounded-lg transition-all duration-300",
|
||||
"backdrop-blur-md border-2 font-medium text-sm",
|
||||
activeTab === "upload"
|
||||
? "bg-gradient-to-b from-purple-100/70 via-purple-50/40 to-white/80 dark:from-purple-900/40 dark:via-purple-800/25 dark:to-black/50 border-purple-400/60 text-purple-700 dark:text-purple-300 shadow-[0_0_20px_rgba(147,51,234,0.25)]"
|
||||
: "bg-gradient-to-b from-white/40 via-white/30 to-white/60 dark:from-gray-800/40 dark:via-gray-800/30 dark:to-black/60 border-gray-300/40 dark:border-gray-600/40 text-gray-600 dark:text-gray-300 hover:border-purple-300/50 hover:text-purple-600 dark:hover:text-purple-400 hover:shadow-[0_0_15px_rgba(147,51,234,0.15)]",
|
||||
)}
|
||||
>
|
||||
{/* Top accent glow for active state */}
|
||||
{activeTab === "upload" && (
|
||||
<div className="pointer-events-none absolute inset-x-0 top-0">
|
||||
<div className="mx-2 mt-0.5 h-[2px] rounded-full bg-purple-500" />
|
||||
<div className="-mt-1 h-8 w-full bg-gradient-to-b from-purple-500/30 to-transparent blur-md" />
|
||||
</div>
|
||||
)}
|
||||
<Upload className={cn("w-5 h-5", activeTab === "upload" ? "text-purple-500" : "text-current")} />
|
||||
<div className="flex flex-col items-start gap-0.5">
|
||||
<span className="font-semibold">Upload Document</span>
|
||||
<span className="text-xs opacity-80">Add local files</span>
|
||||
</div>
|
||||
</button>
|
||||
<div className="flex justify-center">
|
||||
<TabsList>
|
||||
<TabsTrigger value="crawl" color="blue">
|
||||
<Globe className="w-4 h-4 mr-2" />
|
||||
Crawl Website
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="upload" color="purple">
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
Upload Document
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
{/* Crawl Tab */}
|
||||
@@ -207,12 +165,14 @@ export const AddKnowledgeDialog: React.FC<AddKnowledgeDialogProps> = ({
|
||||
value={crawlUrl}
|
||||
onChange={(e) => 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)]"
|
||||
className={cn(
|
||||
"pl-10 h-12",
|
||||
glassCard.blur.md,
|
||||
glassCard.transparency.medium,
|
||||
"border-gray-300/60 dark:border-gray-600/60 focus:border-cyan-400/70",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Enter the URL of a website you want to crawl for knowledge
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
@@ -231,7 +191,13 @@ export const AddKnowledgeDialog: React.FC<AddKnowledgeDialogProps> = ({
|
||||
<Button
|
||||
onClick={handleCrawl}
|
||||
disabled={isProcessing || !crawlUrl}
|
||||
className="w-full bg-gradient-to-r from-cyan-500 to-cyan-600 hover:from-cyan-600 hover:to-cyan-700 backdrop-blur-md border border-cyan-400/50 shadow-[0_0_20px_rgba(6,182,212,0.25)] hover:shadow-[0_0_30px_rgba(6,182,212,0.35)] transition-all duration-200"
|
||||
className={[
|
||||
"w-full bg-gradient-to-r from-cyan-500 to-cyan-600",
|
||||
"hover:from-cyan-600 hover:to-cyan-700",
|
||||
"backdrop-blur-md border border-cyan-400/50",
|
||||
"shadow-[0_0_20px_rgba(6,182,212,0.25)] hover:shadow-[0_0_30px_rgba(6,182,212,0.35)]",
|
||||
"transition-all duration-200",
|
||||
].join(" ")}
|
||||
>
|
||||
{crawlMutation.isPending ? (
|
||||
<>
|
||||
@@ -268,11 +234,11 @@ export const AddKnowledgeDialog: React.FC<AddKnowledgeDialogProps> = ({
|
||||
<div
|
||||
className={cn(
|
||||
"relative h-20 rounded-xl border-2 border-dashed transition-all duration-200",
|
||||
"backdrop-blur-md bg-gradient-to-b from-white/60 via-white/40 to-white/50 dark:from-black/60 dark:via-black/40 dark:to-black/50",
|
||||
"flex flex-col items-center justify-center gap-2 text-center p-4",
|
||||
selectedFile
|
||||
? "border-purple-400/70 bg-gradient-to-b from-purple-50/60 to-white/60 dark:from-purple-900/20 dark:to-black/50"
|
||||
: "border-gray-300/60 dark:border-gray-600/60 hover:border-purple-400/50 hover:bg-gradient-to-b hover:from-purple-50/40 hover:to-white/60 dark:hover:from-purple-900/10 dark:hover:to-black/50",
|
||||
glassCard.blur.md,
|
||||
selectedFile ? glassCard.tints.purple.light : glassCard.transparency.medium,
|
||||
selectedFile ? "border-purple-400/70" : "border-gray-300/60 dark:border-gray-600/60",
|
||||
!selectedFile && "hover:border-purple-400/50",
|
||||
isProcessing && "opacity-50 cursor-not-allowed",
|
||||
)}
|
||||
>
|
||||
@@ -312,7 +278,13 @@ export const AddKnowledgeDialog: React.FC<AddKnowledgeDialogProps> = ({
|
||||
<Button
|
||||
onClick={handleUpload}
|
||||
disabled={isProcessing || !selectedFile}
|
||||
className="w-full bg-gradient-to-r from-purple-500 to-purple-600 hover:from-purple-600 hover:to-purple-700 backdrop-blur-md border border-purple-400/50 shadow-[0_0_20px_rgba(147,51,234,0.25)] hover:shadow-[0_0_30px_rgba(147,51,234,0.35)] transition-all duration-200"
|
||||
className={[
|
||||
"w-full bg-gradient-to-r from-purple-500 to-purple-600",
|
||||
"hover:from-purple-600 hover:to-purple-700",
|
||||
"backdrop-blur-md border border-purple-400/50",
|
||||
"shadow-[0_0_20px_rgba(147,51,234,0.25)] hover:shadow-[0_0_30px_rgba(147,51,234,0.35)]",
|
||||
"transition-all duration-200",
|
||||
].join(" ")}
|
||||
>
|
||||
{uploadMutation.isPending ? (
|
||||
<>
|
||||
|
||||
@@ -1,239 +0,0 @@
|
||||
/**
|
||||
* Document Browser Component
|
||||
* Shows document chunks and code examples for a knowledge item
|
||||
*/
|
||||
|
||||
import { ChevronDown, ChevronRight, Code, FileText, Search } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Input } from "../../ui/primitives";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../../ui/primitives/dialog";
|
||||
import { cn } from "../../ui/primitives/styles";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../ui/primitives/tabs";
|
||||
import { useCodeExamples, useKnowledgeItemChunks } from "../hooks";
|
||||
|
||||
interface DocumentBrowserProps {
|
||||
sourceId: string;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const DocumentBrowser: React.FC<DocumentBrowserProps> = ({ sourceId, open, onOpenChange }) => {
|
||||
const [activeTab, setActiveTab] = useState<"documents" | "code">("documents");
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [expandedChunks, setExpandedChunks] = useState<Set<string>>(new Set());
|
||||
|
||||
const {
|
||||
data: chunksData,
|
||||
isLoading: chunksLoading,
|
||||
isError: chunksError,
|
||||
error: chunksErrorObj,
|
||||
} = useKnowledgeItemChunks(sourceId);
|
||||
const { data: codeData, isLoading: codeLoading, isError: codeError, error: codeErrorObj } = useCodeExamples(sourceId);
|
||||
|
||||
const chunks = chunksData?.chunks || [];
|
||||
const codeExamples = codeData?.code_examples || [];
|
||||
|
||||
// Filter chunks based on search
|
||||
const filteredChunks = chunks.filter(
|
||||
(chunk) =>
|
||||
chunk.content.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
chunk.metadata?.title?.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||
);
|
||||
|
||||
// Filter code examples based on search
|
||||
const filteredCode = codeExamples.filter((example) => {
|
||||
const codeContent = example.code || example.content || "";
|
||||
return (
|
||||
codeContent.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
example.summary?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
example.language?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
});
|
||||
|
||||
const toggleChunk = (chunkId: string) => {
|
||||
setExpandedChunks((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(chunkId)) {
|
||||
next.delete(chunkId);
|
||||
} else {
|
||||
next.add(chunkId);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-4xl h-[80vh] flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Document Browser</DialogTitle>
|
||||
<div className="flex items-center gap-2 mt-4">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search documents and code..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10 bg-black/30 border-white/10 focus:border-cyan-500/50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={(v) => setActiveTab(v as "documents" | "code")}
|
||||
className="flex-1 flex flex-col"
|
||||
>
|
||||
<TabsList className="">
|
||||
<TabsTrigger value="documents" className="data-[state=active]:bg-cyan-500/20">
|
||||
<FileText className="w-4 h-4 mr-2" />
|
||||
Documents ({filteredChunks.length})
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="code" className="data-[state=active]:bg-cyan-500/20">
|
||||
<Code className="w-4 h-4 mr-2" />
|
||||
Code Examples ({filteredCode.length})
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Documents Tab */}
|
||||
<TabsContent value="documents" className="flex-1 overflow-hidden">
|
||||
<div className="h-full overflow-y-auto">
|
||||
{chunksLoading ? (
|
||||
<div className="text-center py-8 text-gray-400">Loading documents...</div>
|
||||
) : chunksError ? (
|
||||
<div className="text-center py-8 text-red-400">
|
||||
Failed to load documents for source {sourceId}.
|
||||
{chunksErrorObj?.message && ` ${chunksErrorObj.message}`}
|
||||
</div>
|
||||
) : filteredChunks.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-400">
|
||||
{searchQuery ? "No documents match your search" : "No documents available"}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3 p-4">
|
||||
{filteredChunks.map((chunk) => {
|
||||
const isExpanded = expandedChunks.has(chunk.id);
|
||||
const preview = chunk.content.substring(0, 200);
|
||||
const needsExpansion = chunk.content.length > 200;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={chunk.id}
|
||||
className="bg-black/30 rounded-lg border border-white/10 p-4 hover:border-cyan-500/30 transition-colors"
|
||||
>
|
||||
{chunk.metadata?.title && (
|
||||
<h4 className="font-medium text-white/90 mb-2 flex items-center gap-2">
|
||||
{needsExpansion && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleChunk(chunk.id)}
|
||||
className="text-gray-400 hover:text-white transition-colors"
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
) : (
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{chunk.metadata.title}
|
||||
</h4>
|
||||
)}
|
||||
|
||||
<div className="text-sm text-gray-300 whitespace-pre-wrap">
|
||||
{isExpanded || !needsExpansion ? (
|
||||
chunk.content
|
||||
) : (
|
||||
<>
|
||||
{preview}...
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleChunk(chunk.id)}
|
||||
className="ml-2 text-cyan-400 hover:text-cyan-300"
|
||||
>
|
||||
Show more
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{chunk.metadata?.tags && chunk.metadata.tags.length > 0 && (
|
||||
<div className="flex items-center gap-2 mt-3 flex-wrap">
|
||||
{chunk.metadata.tags.map((tag: string) => (
|
||||
<span key={tag} className="px-2 py-1 text-xs border border-white/20 rounded bg-black/20">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* Code Examples Tab */}
|
||||
<TabsContent value="code" className="flex-1 overflow-hidden">
|
||||
<div className="h-full overflow-y-auto">
|
||||
{codeLoading ? (
|
||||
<div className="text-center py-8 text-gray-400">Loading code examples...</div>
|
||||
) : codeError ? (
|
||||
<div className="text-center py-8 text-red-400">
|
||||
Failed to load code examples for source {sourceId}.
|
||||
{codeErrorObj?.message && ` ${codeErrorObj.message}`}
|
||||
</div>
|
||||
) : filteredCode.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-400">
|
||||
{searchQuery ? "No code examples match your search" : "No code examples available"}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3 p-4">
|
||||
{filteredCode.map((example) => (
|
||||
<div
|
||||
key={example.id}
|
||||
className="bg-black/30 rounded-lg border border-white/10 overflow-hidden hover:border-cyan-500/30 transition-colors"
|
||||
>
|
||||
<div className="flex items-center justify-between p-3 border-b border-white/10 bg-black/20">
|
||||
<div className="flex items-center gap-2">
|
||||
<Code className="w-4 h-4 text-cyan-400" />
|
||||
{example.language && (
|
||||
<span className="px-2 py-1 text-xs bg-cyan-500/20 text-cyan-400 rounded">
|
||||
{example.language}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{example.file_path && <span className="text-xs text-gray-400">{example.file_path}</span>}
|
||||
</div>
|
||||
|
||||
{example.summary && (
|
||||
<div className="p-3 text-sm text-gray-300 border-b border-white/10">{example.summary}</div>
|
||||
)}
|
||||
|
||||
<pre className="p-4 text-sm overflow-x-auto">
|
||||
<code
|
||||
className={cn(
|
||||
"text-gray-300",
|
||||
example.language === "javascript" && "language-javascript",
|
||||
example.language === "typescript" && "language-typescript",
|
||||
example.language === "python" && "language-python",
|
||||
example.language === "java" && "language-java",
|
||||
)}
|
||||
>
|
||||
{example.code || example.content || ""}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -12,6 +12,7 @@ import { isOptimistic } from "@/features/shared/utils/optimistic";
|
||||
import { KnowledgeCardProgress } from "../../progress/components/KnowledgeCardProgress";
|
||||
import type { ActiveOperation } from "../../progress/types";
|
||||
import { StatPill } from "../../ui/primitives";
|
||||
import { DataCard, DataCardContent, DataCardFooter, DataCardHeader } from "../../ui/primitives/data-card";
|
||||
import { OptimisticIndicator } from "../../ui/primitives/OptimisticIndicator";
|
||||
import { cn } from "../../ui/primitives/styles";
|
||||
import { SimpleTooltip } from "../../ui/primitives/tooltip";
|
||||
@@ -79,37 +80,16 @@ export const KnowledgeCard: React.FC<KnowledgeCardProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const getCardGradient = () => {
|
||||
if (activeOperation) {
|
||||
return "from-cyan-100/60 via-cyan-50/30 to-white/70 dark:from-cyan-900/30 dark:via-cyan-900/15 dark:to-black/40";
|
||||
}
|
||||
if (hasError) {
|
||||
return "from-red-100/50 via-red-50/25 to-white/60 dark:from-red-900/20 dark:via-red-900/10 dark:to-black/30";
|
||||
}
|
||||
if (isProcessing) {
|
||||
return "from-yellow-100/50 via-yellow-50/25 to-white/60 dark:from-yellow-900/20 dark:via-yellow-900/10 dark:to-black/30";
|
||||
}
|
||||
if (isTechnical) {
|
||||
return isUrl
|
||||
? "from-cyan-100/50 via-cyan-50/25 to-white/60 dark:from-cyan-900/20 dark:via-cyan-900/10 dark:to-black/30"
|
||||
: "from-purple-100/50 via-purple-50/25 to-white/60 dark:from-purple-900/20 dark:via-purple-900/10 dark:to-black/30";
|
||||
}
|
||||
return isUrl
|
||||
? "from-blue-100/50 via-blue-50/25 to-white/60 dark:from-blue-900/20 dark:via-blue-900/10 dark:to-black/30"
|
||||
: "from-pink-100/50 via-pink-50/25 to-white/60 dark:from-pink-900/20 dark:via-pink-900/10 dark:to-black/30";
|
||||
// Determine edge color for DataCard primitive
|
||||
const getEdgeColor = (): "cyan" | "purple" | "blue" | "pink" | "red" | "orange" => {
|
||||
if (activeOperation) return "cyan";
|
||||
if (hasError) return "red";
|
||||
if (isProcessing) return "orange";
|
||||
if (isTechnical) return isUrl ? "cyan" : "purple";
|
||||
return isUrl ? "blue" : "pink";
|
||||
};
|
||||
|
||||
const getBorderColor = () => {
|
||||
if (activeOperation) return "border-cyan-600/40 dark:border-cyan-500/50";
|
||||
if (hasError) return "border-red-600/30 dark:border-red-500/30";
|
||||
if (isProcessing) return "border-yellow-600/30 dark:border-yellow-500/30";
|
||||
if (isTechnical) {
|
||||
return isUrl ? "border-cyan-600/30 dark:border-cyan-500/30" : "border-purple-600/30 dark:border-purple-500/30";
|
||||
}
|
||||
return isUrl ? "border-blue-600/30 dark:border-blue-500/30" : "border-pink-600/30 dark:border-pink-500/30";
|
||||
};
|
||||
|
||||
// Accent color used for the top glow bar
|
||||
// Accent color name for title component
|
||||
const getAccentColorName = () => {
|
||||
if (activeOperation) return "cyan" as const;
|
||||
if (hasError) return "red" as const;
|
||||
@@ -118,26 +98,6 @@ export const KnowledgeCard: React.FC<KnowledgeCardProps> = ({
|
||||
return isUrl ? ("blue" as const) : ("pink" as const);
|
||||
};
|
||||
|
||||
const accent = (() => {
|
||||
const name = getAccentColorName();
|
||||
switch (name) {
|
||||
case "cyan":
|
||||
return { bar: "bg-cyan-500", smear: "from-cyan-500/25" };
|
||||
case "purple":
|
||||
return { bar: "bg-purple-500", smear: "from-purple-500/25" };
|
||||
case "blue":
|
||||
return { bar: "bg-blue-500", smear: "from-blue-500/25" };
|
||||
case "pink":
|
||||
return { bar: "bg-pink-500", smear: "from-pink-500/25" };
|
||||
case "red":
|
||||
return { bar: "bg-red-500", smear: "from-red-500/25" };
|
||||
case "yellow":
|
||||
return { bar: "bg-yellow-400", smear: "from-yellow-400/25" };
|
||||
default:
|
||||
return { bar: "bg-cyan-500", smear: "from-cyan-500/25" };
|
||||
}
|
||||
})();
|
||||
|
||||
const getSourceIcon = () => {
|
||||
if (isUrl) return <Globe className="w-5 h-5" />;
|
||||
return <File className="w-5 h-5" />;
|
||||
@@ -146,7 +106,7 @@ export const KnowledgeCard: React.FC<KnowledgeCardProps> = ({
|
||||
return (
|
||||
// biome-ignore lint/a11y/useSemanticElements: Card contains nested interactive elements (buttons, links) - using div to avoid invalid HTML nesting
|
||||
<motion.div
|
||||
className="relative group cursor-pointer"
|
||||
className={cn("relative group cursor-pointer", optimistic && "opacity-80")}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
@@ -161,33 +121,17 @@ export const KnowledgeCard: React.FC<KnowledgeCardProps> = ({
|
||||
whileHover={{ scale: 1.02 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<div
|
||||
<DataCard
|
||||
edgePosition="top"
|
||||
edgeColor={getEdgeColor()}
|
||||
blur="md"
|
||||
className={cn(
|
||||
"relative overflow-hidden transition-all duration-300 rounded-xl",
|
||||
"bg-gradient-to-b backdrop-blur-md border",
|
||||
getCardGradient(),
|
||||
getBorderColor(),
|
||||
"transition-shadow",
|
||||
isHovered && "shadow-[0_0_30px_rgba(6,182,212,0.2)]",
|
||||
"min-h-[240px] flex flex-col",
|
||||
optimistic && "opacity-80 ring-1 ring-cyan-400/30",
|
||||
optimistic && "ring-1 ring-cyan-400/30",
|
||||
)}
|
||||
>
|
||||
{/* Top accent glow tied to type (does not change size) */}
|
||||
<div className="pointer-events-none absolute inset-x-0 top-0">
|
||||
{/* Hairline highlight */}
|
||||
<div className={cn("mx-1 mt-0.5 h-[2px] rounded-full", accent.bar)} />
|
||||
{/* Soft glow smear fading downward */}
|
||||
<div className={cn("-mt-1 h-8 w-full bg-gradient-to-b to-transparent blur-md", accent.smear)} />
|
||||
</div>
|
||||
{/* Glow effect on hover */}
|
||||
{isHovered && (
|
||||
<div className="absolute inset-0 opacity-20 pointer-events-none">
|
||||
<div className="absolute -inset-[100px] bg-[radial-gradient(circle,rgba(6,182,212,0.4)_0%,transparent_70%)] blur-3xl" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Header with Type Badge */}
|
||||
<div className="relative p-4 pb-2">
|
||||
<DataCardHeader>
|
||||
<div className="flex items-start justify-between gap-2 mb-2">
|
||||
{/* Type and Source Badge */}
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -248,7 +192,10 @@ export const KnowledgeCard: React.FC<KnowledgeCardProps> = ({
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="inline-flex items-center gap-1 text-xs text-gray-600 dark:text-gray-400 hover:text-cyan-600 dark:hover:text-cyan-400 transition-colors mt-2"
|
||||
className={[
|
||||
"inline-flex items-center gap-1 text-xs mt-2",
|
||||
"text-gray-600 dark:text-gray-400 hover:text-cyan-600 dark:hover:text-cyan-400 transition-colors",
|
||||
].join(" ")}
|
||||
>
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
<span className="truncate">{extractDomain(item.url)}</span>
|
||||
@@ -273,16 +220,14 @@ export const KnowledgeCard: React.FC<KnowledgeCardProps> = ({
|
||||
>
|
||||
<KnowledgeCardTags sourceId={item.source_id} tags={item.metadata?.tags || []} />
|
||||
</div>
|
||||
</div>
|
||||
</DataCardHeader>
|
||||
|
||||
{/* Spacer to push footer to bottom */}
|
||||
<div className="flex-1" />
|
||||
<DataCardContent>
|
||||
{/* Progress tracking for active operations - using simplified component */}
|
||||
{activeOperation && <KnowledgeCardProgress operation={activeOperation} />}
|
||||
</DataCardContent>
|
||||
|
||||
{/* Progress tracking for active operations - using simplified component */}
|
||||
{activeOperation && <KnowledgeCardProgress operation={activeOperation} />}
|
||||
|
||||
{/* Fixed Footer with Stats */}
|
||||
<div className="px-4 py-3 bg-gray-100/50 dark:bg-black/30 border-t border-gray-200/50 dark:border-white/10">
|
||||
<DataCardFooter>
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
{/* Left: date */}
|
||||
<div className="flex items-center gap-1 text-gray-600 dark:text-gray-400">
|
||||
@@ -342,8 +287,8 @@ export const KnowledgeCard: React.FC<KnowledgeCardProps> = ({
|
||||
</SimpleTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DataCardFooter>
|
||||
</DataCard>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -215,7 +215,10 @@ export const KnowledgeCardTags: React.FC<KnowledgeCardTagsProps> = ({ sourceId,
|
||||
<Badge
|
||||
color="gray"
|
||||
variant="outline"
|
||||
className="flex items-center gap-1 text-[10px] cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors group pr-0.5 px-1.5 py-0.5 h-5"
|
||||
className={[
|
||||
"flex items-center gap-1 text-[10px] cursor-pointer",
|
||||
"hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors group pr-0.5 px-1.5 py-0.5 h-5",
|
||||
].join(" ")}
|
||||
onClick={() => {
|
||||
setIsEditing(true);
|
||||
// Load this specific tag for editing
|
||||
@@ -255,7 +258,10 @@ export const KnowledgeCardTags: React.FC<KnowledgeCardTagsProps> = ({ sourceId,
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAllTags(!showAllTags)}
|
||||
className="flex items-center gap-0.5 text-[10px] text-gray-500 dark:text-gray-400 hover:text-cyan-600 dark:hover:text-cyan-400 transition-colors px-1 py-0.5 rounded"
|
||||
className={[
|
||||
"flex items-center gap-0.5 text-[10px] text-gray-500 dark:text-gray-400",
|
||||
"hover:text-cyan-600 dark:hover:text-cyan-400 transition-colors px-1 py-0.5 rounded",
|
||||
].join(" ")}
|
||||
>
|
||||
{showAllTags ? (
|
||||
<>
|
||||
@@ -322,7 +328,12 @@ export const KnowledgeCardTags: React.FC<KnowledgeCardTagsProps> = ({ sourceId,
|
||||
}
|
||||
}, 0);
|
||||
}}
|
||||
className="flex items-center gap-0.5 px-1.5 py-0.5 text-[10px] rounded border border-gray-300 dark:border-gray-600 text-gray-500 dark:text-gray-400 hover:text-cyan-600 dark:hover:text-cyan-400 hover:border-cyan-400 dark:hover:border-cyan-600 transition-colors h-5"
|
||||
className={[
|
||||
"flex items-center gap-0.5 px-1.5 py-0.5 text-[10px] rounded border h-5",
|
||||
"border-gray-300 dark:border-gray-600 text-gray-500 dark:text-gray-400",
|
||||
"hover:text-cyan-600 dark:hover:text-cyan-400 hover:border-cyan-400 dark:hover:border-cyan-600",
|
||||
"transition-colors",
|
||||
].join(" ")}
|
||||
aria-label="Add tags"
|
||||
>
|
||||
<Plus className="w-2.5 h-2.5" />
|
||||
@@ -338,7 +349,10 @@ export const KnowledgeCardTags: React.FC<KnowledgeCardTagsProps> = ({ sourceId,
|
||||
type="button"
|
||||
onClick={handleSaveTags}
|
||||
disabled={updateMutation.isPending}
|
||||
className="px-2 py-1 text-xs bg-cyan-600 text-white rounded hover:bg-cyan-700 disabled:opacity-50 transition-colors"
|
||||
className={[
|
||||
"px-2 py-1 text-xs bg-cyan-600 dark:bg-cyan-600 text-white",
|
||||
"hover:bg-cyan-700 dark:hover:bg-cyan-700 disabled:opacity-50 transition-colors",
|
||||
].join(" ")}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
@@ -346,7 +360,10 @@ export const KnowledgeCardTags: React.FC<KnowledgeCardTagsProps> = ({ sourceId,
|
||||
type="button"
|
||||
onClick={handleCancelEdit}
|
||||
disabled={updateMutation.isPending}
|
||||
className="px-2 py-1 text-xs bg-gray-500 text-white rounded hover:bg-gray-600 disabled:opacity-50 transition-colors"
|
||||
className={[
|
||||
"px-2 py-1 text-xs bg-gray-500 dark:bg-gray-500 text-white",
|
||||
"hover:bg-gray-600 dark:hover:bg-gray-600 disabled:opacity-50 transition-colors",
|
||||
].join(" ")}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
@@ -72,8 +72,8 @@ export const KnowledgeCardType: React.FC<KnowledgeCardTypeProps> = ({ sourceId,
|
||||
"border-cyan-400 dark:border-cyan-600",
|
||||
"focus:ring-1 focus:ring-cyan-400",
|
||||
isTechnical
|
||||
? "bg-blue-100 text-blue-700 dark:bg-blue-500/10 dark:text-blue-400"
|
||||
: "bg-pink-100 text-pink-700 dark:bg-pink-500/10 dark:text-pink-400",
|
||||
? "bg-cyan-500/10 text-cyan-600 dark:text-cyan-400"
|
||||
: "bg-purple-500/10 text-purple-600 dark:text-purple-400",
|
||||
)}
|
||||
>
|
||||
<SelectValue>
|
||||
@@ -111,8 +111,8 @@ export const KnowledgeCardType: React.FC<KnowledgeCardTypeProps> = ({ sourceId,
|
||||
"flex items-center gap-1.5 px-2 py-1 rounded-md text-xs font-medium cursor-pointer",
|
||||
"hover:ring-1 hover:ring-cyan-400/50 transition-all",
|
||||
isTechnical
|
||||
? "bg-blue-100 text-blue-700 dark:bg-blue-500/10 dark:text-blue-400"
|
||||
: "bg-pink-100 text-pink-700 dark:bg-pink-500/10 dark:text-pink-400",
|
||||
? "bg-cyan-500/10 text-cyan-600 dark:text-cyan-400"
|
||||
: "bg-purple-500/10 text-purple-600 dark:text-purple-400",
|
||||
updateMutation.isPending && "opacity-50 cursor-not-allowed",
|
||||
)}
|
||||
onClick={handleClick}
|
||||
|
||||
@@ -32,8 +32,8 @@ export const KnowledgeHeader: React.FC<KnowledgeHeaderProps> = ({
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 px-6 py-4 border-b border-white/10">
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Left: Title */}
|
||||
{/* Row 1: Title and Add Button (always on same line) */}
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-3 flex-shrink-0">
|
||||
<BookOpen className="h-7 w-7 text-purple-500 filter drop-shadow-[0_0_8px_rgba(168,85,247,0.8)]" />
|
||||
<h1 className="text-2xl font-bold text-white">Knowledge Base</h1>
|
||||
@@ -42,85 +42,89 @@ export const KnowledgeHeader: React.FC<KnowledgeHeaderProps> = ({
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Right: Search, Filters, View toggle, CTA */}
|
||||
<div className="ml-auto flex items-center gap-3">
|
||||
{/* Search on title row */}
|
||||
<div className="relative w-[320px]">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search knowledge base..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
className="pl-10 bg-black/30 border-white/10 focus:border-cyan-500/50"
|
||||
/>
|
||||
</div>
|
||||
{/* Add knowledge button - stays on top line */}
|
||||
<Button variant="knowledge" onClick={onAddKnowledge} className="shadow-lg shadow-purple-500/30 flex-shrink-0">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Knowledge
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Segmented type filters */}
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
size="sm"
|
||||
value={typeFilter}
|
||||
onValueChange={(v) => v && onTypeFilterChange(v as "all" | "technical" | "business")}
|
||||
aria-label="Filter knowledge type"
|
||||
{/* Row 2: Search and Filters (wraps on smaller screens) */}
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
{/* Search */}
|
||||
<div className="relative w-full sm:w-[320px]">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search knowledge base..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
className="pl-10 bg-black/30 dark:bg-black/30 border-white/10 dark:border-white/10 focus:border-cyan-500/50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Segmented type filters */}
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
size="sm"
|
||||
value={typeFilter}
|
||||
onValueChange={(v) => v && onTypeFilterChange(v as "all" | "technical" | "business")}
|
||||
aria-label="Filter knowledge type"
|
||||
>
|
||||
<ToggleGroupItem value="all" aria-label="All" title="All" className="flex items-center justify-center">
|
||||
<Asterisk className="w-4 h-4" aria-hidden="true" />
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem
|
||||
value="technical"
|
||||
aria-label="Technical"
|
||||
title="Technical"
|
||||
className="flex items-center justify-center"
|
||||
>
|
||||
<ToggleGroupItem value="all" aria-label="All" title="All" className="flex items-center justify-center">
|
||||
<Asterisk className="w-4 h-4" aria-hidden="true" />
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem
|
||||
value="technical"
|
||||
aria-label="Technical"
|
||||
title="Technical"
|
||||
className="flex items-center justify-center"
|
||||
>
|
||||
<Terminal className="w-4 h-4" aria-hidden="true" />
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem
|
||||
value="business"
|
||||
aria-label="Business"
|
||||
title="Business"
|
||||
className="flex items-center justify-center"
|
||||
>
|
||||
<Briefcase className="w-4 h-4" aria-hidden="true" />
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
<Terminal className="w-4 h-4" aria-hidden="true" />
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem
|
||||
value="business"
|
||||
aria-label="Business"
|
||||
title="Business"
|
||||
className="flex items-center justify-center"
|
||||
>
|
||||
<Briefcase className="w-4 h-4" aria-hidden="true" />
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
|
||||
{/* View Mode Toggle */}
|
||||
<div className="flex gap-1 p-1 bg-black/30 rounded-lg border border-white/10">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onViewModeChange("grid")}
|
||||
aria-label="Grid view"
|
||||
aria-pressed={viewMode === "grid"}
|
||||
title="Grid view"
|
||||
className={cn(
|
||||
"px-3",
|
||||
viewMode === "grid" ? "bg-cyan-500/20 text-cyan-400" : "text-gray-400 hover:text-white",
|
||||
)}
|
||||
>
|
||||
<Grid className="w-4 h-4" aria-hidden="true" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onViewModeChange("table")}
|
||||
aria-label="Table view"
|
||||
aria-pressed={viewMode === "table"}
|
||||
title="Table view"
|
||||
className={cn(
|
||||
"px-3",
|
||||
viewMode === "table" ? "bg-cyan-500/20 text-cyan-400" : "text-gray-400 hover:text-white",
|
||||
)}
|
||||
>
|
||||
<List className="w-4 h-4" aria-hidden="true" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Add knowledge */}
|
||||
<Button variant="knowledge" onClick={onAddKnowledge} className="shadow-lg shadow-purple-500/30">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Add Knowledge
|
||||
{/* View Mode Toggle */}
|
||||
<div className="flex gap-1 p-1 bg-black/30 rounded-lg border border-white/10">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onViewModeChange("grid")}
|
||||
aria-label="Grid view"
|
||||
aria-pressed={viewMode === "grid"}
|
||||
title="Grid view"
|
||||
className={cn(
|
||||
"px-3",
|
||||
viewMode === "grid"
|
||||
? "bg-cyan-500/20 dark:bg-cyan-500/20 text-cyan-400"
|
||||
: "text-gray-400 hover:text-white",
|
||||
)}
|
||||
>
|
||||
<Grid className="w-4 h-4" aria-hidden="true" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onViewModeChange("table")}
|
||||
aria-label="Table view"
|
||||
aria-pressed={viewMode === "table"}
|
||||
title="Table view"
|
||||
className={cn(
|
||||
"px-3",
|
||||
viewMode === "table"
|
||||
? "bg-cyan-500/20 dark:bg-cyan-500/20 text-cyan-400"
|
||||
: "text-gray-400 hover:text-white",
|
||||
)}
|
||||
>
|
||||
<List className="w-4 h-4" aria-hidden="true" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -107,7 +107,7 @@ export const KnowledgeList: React.FC<KnowledgeListProps> = ({
|
||||
className="flex items-center justify-center py-12"
|
||||
>
|
||||
<div className="text-center max-w-md" role="alert">
|
||||
<div className="inline-flex items-center justify-center w-12 h-12 rounded-full bg-red-500/10 mb-4">
|
||||
<div className="inline-flex items-center justify-center w-12 h-12 rounded-full bg-red-500/10 dark:bg-red-500/10 mb-4">
|
||||
<AlertCircle className="w-6 h-6 text-red-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold mb-2">Failed to Load Knowledge Base</h3>
|
||||
@@ -130,7 +130,7 @@ export const KnowledgeList: React.FC<KnowledgeListProps> = ({
|
||||
className="flex items-center justify-center py-12"
|
||||
>
|
||||
<div className="text-center max-w-md">
|
||||
<div className="inline-flex items-center justify-center w-12 h-12 rounded-full bg-cyan-500/10 mb-4">
|
||||
<div className="inline-flex items-center justify-center w-12 h-12 rounded-full bg-cyan-500/10 dark:bg-cyan-500/10 mb-4">
|
||||
<AlertCircle className="w-6 h-6 text-cyan-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold mb-2">No Knowledge Items</h3>
|
||||
|
||||
@@ -60,150 +60,155 @@ export const KnowledgeTable: React.FC<KnowledgeTableProps> = ({ items, onViewDoc
|
||||
|
||||
const getTypeColor = (type?: string) => {
|
||||
if (type === "technical") {
|
||||
return "text-cyan-400 bg-cyan-500/10 border-cyan-500/20";
|
||||
return "bg-cyan-500/10 text-cyan-600 dark:text-cyan-400";
|
||||
}
|
||||
return "bg-purple-500/10 text-purple-600 dark:text-purple-400";
|
||||
};
|
||||
|
||||
const getHostname = (url: string): string => {
|
||||
try {
|
||||
return new URL(url).hostname;
|
||||
} catch {
|
||||
return url;
|
||||
}
|
||||
};
|
||||
|
||||
const formatCreatedDate = (dateString: string): string => {
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return "N/A";
|
||||
}
|
||||
return formatDistanceToNowStrict(date, { addSuffix: true });
|
||||
} catch {
|
||||
return "N/A";
|
||||
}
|
||||
return "text-blue-400 bg-blue-500/10 border-blue-500/20";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-white/10">
|
||||
<th className="text-left py-3 px-4 text-sm font-medium text-gray-400">Title</th>
|
||||
<th className="text-left py-3 px-4 text-sm font-medium text-gray-400">Type</th>
|
||||
<th className="text-left py-3 px-4 text-sm font-medium text-gray-400">Source</th>
|
||||
<th className="text-left py-3 px-4 text-sm font-medium text-gray-400">Docs</th>
|
||||
<th className="text-left py-3 px-4 text-sm font-medium text-gray-400">Examples</th>
|
||||
<th className="text-left py-3 px-4 text-sm font-medium text-gray-400">Created</th>
|
||||
<th className="text-right py-3 px-4 text-sm font-medium text-gray-400">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((item) => {
|
||||
const isDeleting = deletingIds.has(item.source_id);
|
||||
<div className="w-full">
|
||||
<div className="overflow-x-auto scrollbar-hide">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="bg-gradient-to-r from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 border-b-2 border-gray-200 dark:border-gray-700">
|
||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700 dark:text-gray-300">Title</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700 dark:text-gray-300">Type</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700 dark:text-gray-300">Source</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700 dark:text-gray-300">Docs</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700 dark:text-gray-300">Examples</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700 dark:text-gray-300">Created</th>
|
||||
<th className="px-4 py-3 text-right text-sm font-medium text-gray-700 dark:text-gray-300">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((item, index) => {
|
||||
const isDeleting = deletingIds.has(item.source_id);
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={item.source_id}
|
||||
className={cn(
|
||||
"border-b border-white/5 transition-colors",
|
||||
"hover:bg-white/5",
|
||||
isDeleting && "opacity-50 pointer-events-none",
|
||||
)}
|
||||
>
|
||||
{/* Title */}
|
||||
<td className="py-3 px-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-white/90 font-medium truncate max-w-xs">{item.title}</span>
|
||||
</div>
|
||||
</td>
|
||||
return (
|
||||
<tr
|
||||
key={item.source_id}
|
||||
className={cn(
|
||||
"group transition-all duration-200",
|
||||
index % 2 === 0 ? "bg-white/50 dark:bg-black/50" : "bg-gray-50/80 dark:bg-gray-900/30",
|
||||
"border-b border-gray-200 dark:border-gray-800",
|
||||
isDeleting && "opacity-50 pointer-events-none",
|
||||
)}
|
||||
>
|
||||
{/* Title */}
|
||||
<td className="py-3 px-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-sm text-gray-900 dark:text-white truncate max-w-xs inline-block">
|
||||
{item.title}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{/* Type */}
|
||||
<td className="py-3 px-4">
|
||||
<span
|
||||
className={cn(
|
||||
"px-2 py-1 text-xs rounded inline-flex items-center",
|
||||
getTypeColor(item.metadata?.knowledge_type),
|
||||
)}
|
||||
>
|
||||
{getTypeIcon(item.metadata?.knowledge_type)}
|
||||
<span className="ml-1">{item.metadata?.knowledge_type || "general"}</span>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
{/* Source URL */}
|
||||
<td className="py-3 px-4">
|
||||
<a
|
||||
href={item.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 text-sm text-gray-400 hover:text-cyan-400 transition-colors"
|
||||
>
|
||||
<ExternalLink className="w-3.5 h-3.5" />
|
||||
<span className="truncate max-w-xs">
|
||||
{(() => {
|
||||
try {
|
||||
return new URL(item.url).hostname;
|
||||
} catch {
|
||||
return item.url;
|
||||
}
|
||||
})()}
|
||||
</span>
|
||||
</a>
|
||||
</td>
|
||||
|
||||
{/* Document Count */}
|
||||
<td className="py-3 px-4">
|
||||
<div className="flex items-center gap-1 text-sm text-gray-400">
|
||||
<FileText className="w-3.5 h-3.5" />
|
||||
<span className="font-medium text-white/80">
|
||||
{item.document_count || item.metadata?.document_count || 0}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{/* Code Examples Count */}
|
||||
<td className="py-3 px-4">
|
||||
<div className="flex items-center gap-1 text-sm text-gray-400">
|
||||
<Code className="w-3.5 h-3.5 text-green-400" />
|
||||
<span className="font-medium text-white/80">
|
||||
{item.code_examples_count || item.metadata?.code_examples_count || 0}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{/* Created Date */}
|
||||
<td className="py-3 px-4">
|
||||
<span className="text-sm text-gray-400">
|
||||
{formatDistanceToNowStrict(new Date(item.created_at), { addSuffix: true })}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
{/* Actions */}
|
||||
<td className="py-3 px-4">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onViewDocument(item.source_id)}
|
||||
className="text-gray-400 hover:text-white hover:bg-white/10"
|
||||
{/* Type */}
|
||||
<td className="py-3 px-4">
|
||||
<span
|
||||
className={cn(
|
||||
"px-2 py-1 text-xs rounded inline-flex items-center",
|
||||
getTypeColor(item.metadata?.knowledge_type),
|
||||
)}
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
</Button>
|
||||
{getTypeIcon(item.metadata?.knowledge_type)}
|
||||
<span className="ml-1">{item.metadata?.knowledge_type || "general"}</span>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0 text-gray-400 hover:text-white hover:bg-white/10"
|
||||
>
|
||||
<MoreHorizontal className="w-4 h-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => onViewDocument(item.source_id)}>
|
||||
<Eye className="w-4 h-4 mr-2" />
|
||||
View Documents
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleDelete(item)}
|
||||
className="text-red-400 focus:text-red-400"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
{/* Source URL */}
|
||||
<td className="py-3 px-4 text-sm text-gray-700 dark:text-gray-300 max-w-xs truncate">
|
||||
<a
|
||||
href={item.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 hover:text-cyan-600 dark:hover:text-cyan-400 transition-colors"
|
||||
>
|
||||
<ExternalLink className="w-3.5 h-3.5" />
|
||||
<span className="truncate inline-block">{getHostname(item.url)}</span>
|
||||
</a>
|
||||
</td>
|
||||
|
||||
{/* Document Count */}
|
||||
<td className="py-3 px-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
{item.document_count || item.metadata?.document_count || 0}
|
||||
</td>
|
||||
|
||||
{/* Code Examples Count */}
|
||||
<td className="py-3 px-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
{item.code_examples_count || item.metadata?.code_examples_count || 0}
|
||||
</td>
|
||||
|
||||
{/* Created Date */}
|
||||
<td className="py-3 px-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
{formatCreatedDate(item.created_at)}
|
||||
</td>
|
||||
|
||||
{/* Actions */}
|
||||
<td className="py-3 px-4">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onViewDocument(item.source_id)}
|
||||
className="text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0 text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
|
||||
>
|
||||
<MoreHorizontal className="w-4 h-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => onViewDocument(item.source_id)}>
|
||||
<Eye className="w-4 h-4 mr-2" />
|
||||
View Documents
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleDelete(item)}
|
||||
className="text-red-600 dark:text-red-400 focus:text-red-600 dark:focus:text-red-400"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
*/
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { Briefcase, Check, Terminal } from "lucide-react";
|
||||
import { cn } from "../../ui/primitives/styles";
|
||||
import { Briefcase, Terminal } from "lucide-react";
|
||||
import { cn, glassCard } from "../../ui/primitives/styles";
|
||||
|
||||
interface KnowledgeTypeSelectorProps {
|
||||
value: "technical" | "business";
|
||||
@@ -19,54 +19,24 @@ const TYPES = [
|
||||
label: "Technical",
|
||||
description: "Code, APIs, dev docs",
|
||||
icon: Terminal,
|
||||
gradient: {
|
||||
selected:
|
||||
"from-cyan-100/60 via-cyan-50/30 to-white/70 dark:from-cyan-900/30 dark:via-cyan-900/15 dark:to-black/40",
|
||||
unselected:
|
||||
"from-gray-50/50 via-gray-25/25 to-white/60 dark:from-gray-800/20 dark:via-gray-800/10 dark:to-black/30",
|
||||
},
|
||||
border: {
|
||||
selected: "border-cyan-500/60",
|
||||
unselected: "border-gray-300/50 dark:border-gray-700/50",
|
||||
hover: "hover:border-cyan-400/50",
|
||||
},
|
||||
edgeColor: "cyan" as const,
|
||||
colors: {
|
||||
selected: "text-cyan-700 dark:text-cyan-400",
|
||||
unselected: "text-gray-700 dark:text-gray-300",
|
||||
description: {
|
||||
selected: "text-cyan-600 dark:text-cyan-400",
|
||||
unselected: "text-gray-500 dark:text-gray-400",
|
||||
},
|
||||
icon: "text-cyan-700 dark:text-cyan-400",
|
||||
label: "text-cyan-700 dark:text-cyan-400",
|
||||
description: "text-cyan-600 dark:text-cyan-400",
|
||||
},
|
||||
accent: "bg-cyan-500",
|
||||
smear: "from-cyan-500/25",
|
||||
},
|
||||
{
|
||||
value: "business" as const,
|
||||
label: "Business",
|
||||
description: "Guides, policies, general",
|
||||
icon: Briefcase,
|
||||
gradient: {
|
||||
selected:
|
||||
"from-pink-100/60 via-pink-50/30 to-white/70 dark:from-pink-900/30 dark:via-pink-900/15 dark:to-black/40",
|
||||
unselected:
|
||||
"from-gray-50/50 via-gray-25/25 to-white/60 dark:from-gray-800/20 dark:via-gray-800/10 dark:to-black/30",
|
||||
},
|
||||
border: {
|
||||
selected: "border-pink-500/60",
|
||||
unselected: "border-gray-300/50 dark:border-gray-700/50",
|
||||
hover: "hover:border-pink-400/50",
|
||||
},
|
||||
edgeColor: "purple" as const,
|
||||
colors: {
|
||||
selected: "text-pink-700 dark:text-pink-400",
|
||||
unselected: "text-gray-700 dark:text-gray-300",
|
||||
description: {
|
||||
selected: "text-pink-600 dark:text-pink-400",
|
||||
unselected: "text-gray-500 dark:text-gray-400",
|
||||
},
|
||||
icon: "text-purple-700 dark:text-purple-400",
|
||||
label: "text-purple-700 dark:text-purple-400",
|
||||
description: "text-purple-600 dark:text-purple-400",
|
||||
},
|
||||
accent: "bg-pink-500",
|
||||
smear: "from-pink-500/25",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -94,47 +64,50 @@ export const KnowledgeTypeSelector: React.FC<KnowledgeTypeSelectorProps> = ({
|
||||
onClick={() => !disabled && onValueChange(type.value)}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
"relative w-full h-24 rounded-xl transition-all duration-200 border-2",
|
||||
"relative w-full h-24 rounded-xl transition-all duration-200",
|
||||
"flex flex-col items-center justify-center gap-2 p-4",
|
||||
"backdrop-blur-md",
|
||||
glassCard.base,
|
||||
isSelected
|
||||
? `${type.border.selected} bg-gradient-to-b ${type.gradient.selected}`
|
||||
: `${type.border.unselected} bg-gradient-to-b ${type.gradient.unselected}`,
|
||||
!disabled && !isSelected && type.border.hover,
|
||||
!disabled &&
|
||||
!isSelected &&
|
||||
"hover:shadow-[0_0_15px_rgba(0,0,0,0.05)] dark:hover:shadow-[0_0_15px_rgba(255,255,255,0.05)]",
|
||||
isSelected && "shadow-[0_0_20px_rgba(6,182,212,0.15)]",
|
||||
? glassCard.edgeColors[type.edgeColor].border
|
||||
: "border border-gray-300/50 dark:border-gray-700/50",
|
||||
isSelected ? glassCard.tints[type.edgeColor].light : glassCard.transparency.light,
|
||||
!isSelected && "hover:border-gray-400/60 dark:hover:border-gray-600/60",
|
||||
disabled && "opacity-50 cursor-not-allowed",
|
||||
)}
|
||||
aria-label={`Select ${type.label}: ${type.description}`}
|
||||
>
|
||||
{/* Top accent glow for selected state */}
|
||||
{/* Top edge-lit effect for selected state */}
|
||||
{isSelected && (
|
||||
<div className="pointer-events-none absolute inset-x-0 top-0">
|
||||
<div className={cn("mx-1 mt-0.5 h-[2px] rounded-full", type.accent)} />
|
||||
<div className={cn("-mt-1 h-6 w-full bg-gradient-to-b to-transparent blur-md", type.smear)} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Selection indicator */}
|
||||
{isSelected && (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute -top-1 -right-1 w-5 h-5 rounded-full flex items-center justify-center",
|
||||
type.accent,
|
||||
)}
|
||||
>
|
||||
<Check className="w-3 h-3 text-white" />
|
||||
</div>
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-x-0 top-0 h-[2px] pointer-events-none z-10",
|
||||
glassCard.edgeLit.position.top,
|
||||
glassCard.edgeLit.color[type.edgeColor].line,
|
||||
glassCard.edgeLit.color[type.edgeColor].glow,
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-x-0 top-0 h-16 bg-gradient-to-b to-transparent blur-lg pointer-events-none z-10",
|
||||
glassCard.edgeLit.color[type.edgeColor].gradient.vertical,
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Icon */}
|
||||
<Icon className={cn("w-6 h-6", isSelected ? type.colors.selected : type.colors.unselected)} />
|
||||
<Icon
|
||||
className={cn("w-6 h-6", isSelected ? type.colors.icon : "text-gray-700 dark:text-gray-300")}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
{/* Label */}
|
||||
<div
|
||||
className={cn("text-sm font-semibold", isSelected ? type.colors.selected : type.colors.unselected)}
|
||||
className={cn(
|
||||
"text-sm font-semibold",
|
||||
isSelected ? type.colors.label : "text-gray-700 dark:text-gray-300",
|
||||
)}
|
||||
>
|
||||
{type.label}
|
||||
</div>
|
||||
@@ -143,7 +116,7 @@ export const KnowledgeTypeSelector: React.FC<KnowledgeTypeSelectorProps> = ({
|
||||
<div
|
||||
className={cn(
|
||||
"text-xs text-center leading-tight",
|
||||
isSelected ? type.colors.description.selected : type.colors.description.unselected,
|
||||
isSelected ? type.colors.description : "text-gray-500 dark:text-gray-400",
|
||||
)}
|
||||
>
|
||||
{type.description}
|
||||
@@ -153,11 +126,6 @@ export const KnowledgeTypeSelector: React.FC<KnowledgeTypeSelectorProps> = ({
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Help text */}
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 text-center">
|
||||
Choose the type that best describes your content
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
*/
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { Check, Info } from "lucide-react";
|
||||
import { cn } from "../../ui/primitives/styles";
|
||||
import { SimpleTooltip } from "../../ui/primitives/tooltip";
|
||||
import { Info } from "lucide-react";
|
||||
import { cn, glassCard } from "../../ui/primitives/styles";
|
||||
import { SimpleTooltip, Tooltip, TooltipContent, TooltipTrigger } from "../../ui/primitives/tooltip";
|
||||
|
||||
interface LevelSelectorProps {
|
||||
value: string;
|
||||
@@ -43,36 +43,47 @@ const LEVELS = [
|
||||
|
||||
export const LevelSelector: React.FC<LevelSelectorProps> = ({ value, onValueChange, disabled = false }) => {
|
||||
const tooltipContent = (
|
||||
<div className="space-y-2 text-xs">
|
||||
<div className="font-semibold mb-2">Crawl Depth Level Explanations:</div>
|
||||
<div className="space-y-2 max-w-xs">
|
||||
<div className="font-semibold mb-2 text-sm">Crawl Depth Levels:</div>
|
||||
{LEVELS.map((level) => (
|
||||
<div key={level.value} className="space-y-1">
|
||||
<div className="font-medium">
|
||||
Level {level.value}: "{level.description}"
|
||||
<div key={level.value} className="space-y-0.5">
|
||||
<div className="text-xs font-medium">
|
||||
Level {level.value}: {level.description}
|
||||
</div>
|
||||
<div className="text-gray-300 dark:text-gray-400 pl-2">{level.details}</div>
|
||||
<div className="text-xs text-gray-400 dark:text-gray-500 pl-2">{level.details}</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="mt-3 pt-2 border-t border-gray-600 dark:border-gray-400">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>💡</span>
|
||||
<span className="font-medium">More data isn't always better. Choose based on your needs.</span>
|
||||
</div>
|
||||
<div className="mt-2 pt-2 border-t border-gray-600 dark:border-gray-500 text-xs">
|
||||
💡 More data isn't always better. Choose based on your needs.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-white/90" id="crawl-depth-label">
|
||||
Crawl Depth
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-white/90" id="crawl-depth-label">
|
||||
Crawl Depth
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="text-gray-400 hover:text-cyan-500 transition-colors cursor-help"
|
||||
aria-label="Show crawl depth level details"
|
||||
>
|
||||
<Info className="w-4 h-4" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">{tooltipContent}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Higher levels crawl deeper into the website structure
|
||||
</div>
|
||||
<SimpleTooltip content={tooltipContent}>
|
||||
<Info className="w-4 h-4 text-gray-400 hover:text-cyan-500 transition-colors cursor-help" />
|
||||
</SimpleTooltip>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-3" role="radiogroup" aria-labelledby="crawl-depth-label">
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3" role="radiogroup" aria-labelledby="crawl-depth-label">
|
||||
{LEVELS.map((level) => {
|
||||
const isSelected = value === level.value;
|
||||
|
||||
@@ -98,29 +109,34 @@ export const LevelSelector: React.FC<LevelSelectorProps> = ({ value, onValueChan
|
||||
}}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
"relative w-full h-16 rounded-xl transition-all duration-200 border-2",
|
||||
"relative w-full h-16 rounded-xl transition-all duration-200",
|
||||
"flex flex-col items-center justify-center gap-1",
|
||||
"backdrop-blur-md focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:ring-offset-2",
|
||||
isSelected
|
||||
? "border-cyan-500/60 bg-gradient-to-b from-cyan-100/60 via-cyan-50/30 to-white/70 dark:from-cyan-900/30 dark:via-cyan-900/15 dark:to-black/40"
|
||||
: "border-gray-300/50 dark:border-gray-700/50 bg-gradient-to-b from-gray-50/50 via-gray-25/25 to-white/60 dark:from-gray-800/20 dark:via-gray-800/10 dark:to-black/30",
|
||||
!disabled && "hover:border-cyan-400/50 hover:shadow-[0_0_15px_rgba(6,182,212,0.15)]",
|
||||
glassCard.base,
|
||||
"focus:outline-none focus-visible:ring-2 focus-visible:ring-cyan-500 focus-visible:ring-offset-2",
|
||||
isSelected ? glassCard.edgeColors.cyan.border : "border border-gray-300/50 dark:border-gray-700/50",
|
||||
isSelected ? glassCard.tints.cyan.light : glassCard.transparency.light,
|
||||
!disabled && !isSelected && "hover:border-cyan-400/50",
|
||||
disabled && "opacity-50 cursor-not-allowed",
|
||||
)}
|
||||
>
|
||||
{/* Top accent glow for selected state */}
|
||||
{/* Top edge-lit effect for selected state */}
|
||||
{isSelected && (
|
||||
<div className="pointer-events-none absolute inset-x-0 top-0">
|
||||
<div className="mx-1 mt-0.5 h-[2px] rounded-full bg-cyan-500" />
|
||||
<div className="-mt-1 h-6 w-full bg-gradient-to-b from-cyan-500/25 to-transparent blur-md" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Selection indicator */}
|
||||
{isSelected && (
|
||||
<div className="absolute -top-1 -right-1 w-5 h-5 bg-cyan-500 rounded-full flex items-center justify-center">
|
||||
<Check className="w-3 h-3 text-white" />
|
||||
</div>
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-x-0 top-0 h-[2px] pointer-events-none z-10",
|
||||
glassCard.edgeLit.position.top,
|
||||
glassCard.edgeLit.color.cyan.line,
|
||||
glassCard.edgeLit.color.cyan.glow,
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-x-0 top-0 h-16 bg-gradient-to-b to-transparent blur-lg pointer-events-none z-10",
|
||||
glassCard.edgeLit.color.cyan.gradient.vertical,
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Level number */}
|
||||
@@ -148,11 +164,6 @@ export const LevelSelector: React.FC<LevelSelectorProps> = ({ value, onValueChan
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Help text */}
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 text-center">
|
||||
Higher levels crawl deeper into the website structure
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ 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";
|
||||
import { cn, glassCard } from "../../ui/primitives/styles";
|
||||
|
||||
interface TagInputProps {
|
||||
tags: string[];
|
||||
@@ -75,12 +75,17 @@ export const TagInput: React.FC<TagInputProps> = ({
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-white/90">Tags</div>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-white/90">Tags</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Press Enter or comma to add tags • Backspace to remove last tag
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tag Display */}
|
||||
{tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{tags.map((tag, index) => (
|
||||
{tags.map((tag) => (
|
||||
<motion.div
|
||||
key={tag}
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
@@ -88,9 +93,9 @@ export const TagInput: React.FC<TagInputProps> = ({
|
||||
exit={{ opacity: 0, scale: 0.8 }}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-medium",
|
||||
"backdrop-blur-md bg-gradient-to-r from-blue-100/80 to-blue-50/60 dark:from-blue-900/40 dark:to-blue-800/30",
|
||||
"border border-blue-300/50 dark:border-blue-700/50",
|
||||
"text-blue-700 dark:text-blue-300",
|
||||
glassCard.blur.md,
|
||||
glassCard.tints.blue.medium,
|
||||
"border border-blue-400/30",
|
||||
"transition-all duration-200",
|
||||
)}
|
||||
>
|
||||
@@ -102,7 +107,7 @@ export const TagInput: React.FC<TagInputProps> = ({
|
||||
className="ml-0.5 text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-200 transition-colors"
|
||||
aria-label={`Remove ${tag} tag`}
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
<X className="w-3 h-3" aria-hidden="true" />
|
||||
</button>
|
||||
)}
|
||||
</motion.div>
|
||||
@@ -122,19 +127,21 @@ export const TagInput: React.FC<TagInputProps> = ({
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={tags.length >= 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)]"
|
||||
className={cn(
|
||||
"pl-9",
|
||||
glassCard.blur.md,
|
||||
glassCard.transparency.medium,
|
||||
"border-gray-300/60 dark:border-gray-600/60 focus:border-blue-400/70",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Help Text */}
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 space-y-1">
|
||||
<p>Press Enter or comma to add tags • Backspace to remove last tag</p>
|
||||
{maxTags && (
|
||||
<p>
|
||||
{tags.length}/{maxTags} tags used
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{/* Tag count */}
|
||||
{maxTags && (
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{tags.length}/{maxTags} tags used
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export * from "./AddKnowledgeDialog";
|
||||
export * from "./DocumentBrowser";
|
||||
export * from "./KnowledgeCard";
|
||||
export * from "./KnowledgeList";
|
||||
export * from "./KnowledgeTypeSelector";
|
||||
|
||||
@@ -228,7 +228,7 @@ export function useCrawlUrl() {
|
||||
});
|
||||
|
||||
// Return context for rollback and replacement
|
||||
return { previousSummaries, previousOperations, tempProgressId };
|
||||
return { previousSummaries, previousOperations, tempProgressId, tempItemId: tempProgressId };
|
||||
},
|
||||
onSuccess: (response, _variables, context) => {
|
||||
// Replace temporary IDs with real ones from the server
|
||||
@@ -407,7 +407,7 @@ export function useUploadDocument() {
|
||||
};
|
||||
});
|
||||
|
||||
return { previousSummaries, previousOperations, tempProgressId };
|
||||
return { previousSummaries, previousOperations, tempProgressId, tempItemId: tempProgressId };
|
||||
},
|
||||
onSuccess: (response, _variables, context) => {
|
||||
// Replace temporary IDs with real ones from the server
|
||||
|
||||
@@ -4,9 +4,20 @@
|
||||
*/
|
||||
|
||||
import { Check, Code, Copy, FileText, Layers } from "lucide-react";
|
||||
import Prism from "prismjs";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { Button } from "../../../ui/primitives";
|
||||
import type { InspectorSelectedItem } from "../../types";
|
||||
|
||||
// Import Prism theme and languages
|
||||
import "prismjs/themes/prism-tomorrow.css";
|
||||
import "prismjs/components/prism-javascript";
|
||||
import "prismjs/components/prism-typescript";
|
||||
import "prismjs/components/prism-python";
|
||||
import "prismjs/components/prism-java";
|
||||
import "prismjs/components/prism-bash";
|
||||
import "prismjs/components/prism-json";
|
||||
|
||||
interface ContentViewerProps {
|
||||
selectedItem: InspectorSelectedItem | null;
|
||||
onCopy: (text: string, id: string) => void;
|
||||
@@ -25,6 +36,44 @@ export const ContentViewer: React.FC<ContentViewerProps> = ({ selectedItem, onCo
|
||||
);
|
||||
}
|
||||
|
||||
// Highlight code with Prism
|
||||
const highlightCode = (code: string, language?: string): string => {
|
||||
try {
|
||||
const lang = language?.toLowerCase() || "javascript";
|
||||
const grammar = Prism.languages[lang] || Prism.languages.javascript;
|
||||
return Prism.highlight(code, grammar, lang);
|
||||
} catch (error) {
|
||||
console.error("Prism highlighting failed:", error);
|
||||
// Return original code with basic HTML escaping
|
||||
return code.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
}
|
||||
};
|
||||
|
||||
// Strip leading/trailing backticks from document content
|
||||
const stripOuterBackticks = (content: string) => {
|
||||
let cleaned = content.trim();
|
||||
|
||||
// Remove opening triple backticks (with optional language identifier)
|
||||
if (cleaned.startsWith("```")) {
|
||||
const firstNewline = cleaned.indexOf("\n");
|
||||
if (firstNewline > 0) {
|
||||
cleaned = cleaned.substring(firstNewline + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove closing triple backticks
|
||||
if (cleaned.endsWith("```")) {
|
||||
const lastBackticks = cleaned.lastIndexOf("\n```");
|
||||
if (lastBackticks > 0) {
|
||||
cleaned = cleaned.substring(0, lastBackticks);
|
||||
} else {
|
||||
cleaned = cleaned.substring(0, cleaned.length - 3);
|
||||
}
|
||||
}
|
||||
|
||||
return cleaned.trim();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Content Header - Fixed with proper overflow handling */}
|
||||
@@ -56,7 +105,12 @@ export const ContentViewer: React.FC<ContentViewerProps> = ({ selectedItem, onCo
|
||||
) : (
|
||||
<>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<span className="px-2 py-0.5 bg-green-500/10 text-green-400 text-xs font-mono rounded flex-shrink-0">
|
||||
<span
|
||||
className={[
|
||||
"px-2 py-0.5 bg-green-500/10 text-green-600 dark:text-green-400",
|
||||
"text-xs font-mono rounded flex-shrink-0",
|
||||
].join(" ")}
|
||||
>
|
||||
{selectedItem.type === "code" && selectedItem.metadata && "language" in selectedItem.metadata
|
||||
? selectedItem.metadata.language || "unknown"
|
||||
: "unknown"}
|
||||
@@ -105,19 +159,48 @@ export const ContentViewer: React.FC<ContentViewerProps> = ({ selectedItem, onCo
|
||||
{/* Content Body */}
|
||||
<div className="flex-1 overflow-y-auto min-h-0 p-6 scrollbar-thin">
|
||||
{selectedItem.type === "document" ? (
|
||||
<div className="prose prose-invert max-w-none">
|
||||
<pre className="whitespace-pre-wrap text-sm text-gray-300 font-sans leading-relaxed">
|
||||
{selectedItem.content || "No content available"}
|
||||
</pre>
|
||||
<div className="prose prose-invert prose-sm max-w-none prose-headings:text-cyan-400 prose-a:text-cyan-400 prose-code:text-purple-400 prose-strong:text-white prose-pre:bg-black/30 prose-pre:border prose-pre:border-white/10">
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
p: ({ children }) => <p className="mb-4 leading-relaxed">{children}</p>,
|
||||
h1: ({ children }) => <h1 className="text-xl font-bold mb-3 mt-6">{children}</h1>,
|
||||
h2: ({ children }) => <h2 className="text-lg font-bold mb-3 mt-5">{children}</h2>,
|
||||
h3: ({ children }) => <h3 className="text-base font-semibold mb-2 mt-4">{children}</h3>,
|
||||
ul: ({ children }) => <ul className="list-disc list-inside mb-4 space-y-1">{children}</ul>,
|
||||
ol: ({ children }) => <ol className="list-decimal list-inside mb-4 space-y-1">{children}</ol>,
|
||||
li: ({ children }) => <li className="leading-relaxed">{children}</li>,
|
||||
code: ({ children }) => <code className="px-1.5 py-0.5 rounded bg-black/30">{children}</code>,
|
||||
}}
|
||||
>
|
||||
{stripOuterBackticks(selectedItem.content || "No content available")}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative">
|
||||
<pre className="bg-black/30 border border-white/10 rounded-lg p-4 overflow-x-auto scrollbar-thin scrollbar-thumb-white/10 scrollbar-track-transparent">
|
||||
<code className="text-sm text-gray-300 font-mono">
|
||||
{selectedItem.content || "// No code content available"}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
(() => {
|
||||
// Extract language once
|
||||
const language =
|
||||
selectedItem.metadata && "language" in selectedItem.metadata
|
||||
? selectedItem.metadata.language || "javascript"
|
||||
: "javascript";
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<pre
|
||||
className={[
|
||||
"bg-black/30 dark:bg-black/30 border border-cyan-500/10 rounded-lg p-4",
|
||||
"overflow-x-auto scrollbar-thin scrollbar-thumb-white/10 scrollbar-track-transparent",
|
||||
].join(" ")}
|
||||
>
|
||||
<code
|
||||
className={`language-${language} font-mono text-sm leading-relaxed`}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: highlightCode(selectedItem.content || "// No code content available", language),
|
||||
}}
|
||||
/>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
})()
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -131,16 +214,19 @@ export const ContentViewer: React.FC<ContentViewerProps> = ({ selectedItem, onCo
|
||||
<span className="text-cyan-400">{(selectedItem.metadata.relevance_score * 100).toFixed(0)}%</span>
|
||||
</span>
|
||||
)}
|
||||
{selectedItem.type === "document" && "url" in selectedItem.metadata && selectedItem.metadata.url && (
|
||||
<a
|
||||
href={selectedItem.metadata.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-cyan-400 hover:text-cyan-300 transition-colors underline"
|
||||
>
|
||||
View Source
|
||||
</a>
|
||||
)}
|
||||
{selectedItem.type === "document" &&
|
||||
selectedItem.metadata &&
|
||||
"url" in selectedItem.metadata &&
|
||||
selectedItem.metadata.url && (
|
||||
<a
|
||||
href={selectedItem.metadata.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-cyan-400 hover:text-cyan-300 transition-colors underline"
|
||||
>
|
||||
View Source
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-gray-600">{selectedItem.type === "document" ? "Document Chunk" : "Code Example"}</span>
|
||||
</div>
|
||||
|
||||
@@ -38,8 +38,8 @@ export const InspectorHeader: React.FC<InspectorHeaderProps> = ({
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium",
|
||||
item.source_type === "url"
|
||||
? "bg-blue-500/10 text-blue-400 border border-blue-500/20"
|
||||
: "bg-purple-500/10 text-purple-400 border border-purple-500/20",
|
||||
? "bg-blue-500/10 text-blue-600 dark:text-blue-400 border border-blue-500/20"
|
||||
: "bg-purple-500/10 text-purple-600 dark:text-purple-400 border border-purple-500/20",
|
||||
)}
|
||||
>
|
||||
{item.source_type === "url" ? (
|
||||
@@ -60,8 +60,8 @@ export const InspectorHeader: React.FC<InspectorHeaderProps> = ({
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium",
|
||||
item.knowledge_type === "technical"
|
||||
? "bg-green-500/10 text-green-400 border border-green-500/20"
|
||||
: "bg-orange-500/10 text-orange-400 border border-orange-500/20",
|
||||
? "bg-green-500/10 text-green-600 dark:text-green-400 border border-green-500/20"
|
||||
: "bg-orange-500/10 text-orange-600 dark:text-orange-400 border border-orange-500/20",
|
||||
)}
|
||||
>
|
||||
{item.knowledge_type === "technical" ? (
|
||||
|
||||
@@ -102,9 +102,9 @@ export const InspectorSidebar: React.FC<InspectorSidebarProps> = ({
|
||||
onClick={() => onItemSelect(item)}
|
||||
className={cn(
|
||||
"w-full text-left p-3 rounded-lg mb-1 transition-all",
|
||||
"hover:bg-white/5 focus:outline-none focus:ring-2 focus:ring-cyan-500/50",
|
||||
"hover:bg-white/5 dark:hover:bg-white/5 focus:outline-none focus:ring-2 focus:ring-cyan-500/50",
|
||||
selectedItemId === item.id
|
||||
? "bg-cyan-500/10 border border-cyan-500/30 ring-1 ring-cyan-500/20"
|
||||
? "bg-cyan-500/10 dark:bg-cyan-500/10 border border-cyan-500/30 dark:border-cyan-500/30 ring-1 ring-cyan-500/20"
|
||||
: "border border-transparent",
|
||||
)}
|
||||
role="option"
|
||||
@@ -128,7 +128,7 @@ export const InspectorSidebar: React.FC<InspectorSidebarProps> = ({
|
||||
{getItemTitle(item)}
|
||||
</span>
|
||||
{viewMode === "code" && (item as CodeExample).language && (
|
||||
<span className="px-1.5 py-0.5 bg-green-500/10 text-green-400 text-xs rounded flex-shrink-0">
|
||||
<span className="px-1.5 py-0.5 bg-green-500/10 text-green-600 dark:text-green-400 text-xs rounded flex-shrink-0">
|
||||
{(item as CodeExample).language}
|
||||
</span>
|
||||
)}
|
||||
@@ -157,7 +157,7 @@ export const InspectorSidebar: React.FC<InspectorSidebarProps> = ({
|
||||
size="sm"
|
||||
onClick={onLoadMore}
|
||||
disabled={isFetchingNextPage}
|
||||
className="w-full text-cyan-400 hover:text-white hover:bg-cyan-500/10 transition-all"
|
||||
className="w-full text-cyan-600 dark:text-cyan-400 hover:text-white dark:hover:text-white hover:bg-cyan-500/10 transition-all"
|
||||
aria-label={`Load more ${viewMode}`}
|
||||
>
|
||||
{isFetchingNextPage ? (
|
||||
|
||||
@@ -73,7 +73,7 @@ export const KnowledgeView = () => {
|
||||
// Check if it was an error or success
|
||||
if (op.status === "error" || op.status === "failed") {
|
||||
// Show error message with details
|
||||
const errorMessage = op.message || op.error || "Operation failed";
|
||||
const errorMessage = op.message || "Operation failed";
|
||||
showToast(`❌ ${errorMessage}`, "error", 7000);
|
||||
} else if (op.status === "completed") {
|
||||
// Show success message
|
||||
@@ -141,7 +141,7 @@ export const KnowledgeView = () => {
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-white/90">Active Operations ({activeOperations.length})</h3>
|
||||
<div className="flex items-center gap-2 text-sm text-gray-400">
|
||||
<div className="w-2 h-2 bg-cyan-400 rounded-full animate-pulse" />
|
||||
<div className="w-2 h-2 bg-cyan-400 dark:bg-cyan-400 rounded-full animate-pulse" />
|
||||
Live Updates
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -105,7 +105,21 @@ const switchVariants = {
|
||||
* - Uncontrolled: Pass defaultChecked, component manages own state
|
||||
*/
|
||||
const Switch = React.forwardRef<React.ElementRef<typeof SwitchPrimitives.Root>, SwitchProps>(
|
||||
({ className, size = "md", color = "cyan", icon, iconOn, iconOff, checked, defaultChecked, onCheckedChange, ...props }, ref) => {
|
||||
(
|
||||
{
|
||||
className,
|
||||
size = "md",
|
||||
color = "cyan",
|
||||
icon,
|
||||
iconOn,
|
||||
iconOff,
|
||||
checked,
|
||||
defaultChecked,
|
||||
onCheckedChange,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const sizeStyles = switchVariants.size[size];
|
||||
const colorStyles = switchVariants.color[color];
|
||||
|
||||
@@ -128,7 +142,7 @@ const Switch = React.forwardRef<React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
// Call parent's handler if provided
|
||||
onCheckedChange?.(newChecked);
|
||||
},
|
||||
[isControlled, onCheckedChange]
|
||||
[isControlled, onCheckedChange],
|
||||
);
|
||||
|
||||
const displayIcon = React.useMemo(() => {
|
||||
|
||||
Reference in New Issue
Block a user