Merge branch 'main' into refactor/projects-ui

Merged in PR #776 (refactor/knowledge-ui) from main.
No conflicts - different features.
This commit is contained in:
Developer
2025-10-10 17:08:05 -04:00
20 changed files with 608 additions and 787 deletions

View File

@@ -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",

View File

@@ -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",

View File

@@ -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 ? (
<>

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>

View File

@@ -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}

View File

@@ -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>

View File

@@ -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>

View File

@@ -60,150 +60,171 @@ 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 isSafeProtocol = (url: string): boolean => {
try {
const protocol = new URL(url).protocol;
return protocol === "http:" || protocol === "https:";
} catch {
return false;
}
};
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">
{isSafeProtocol(item.url) ? (
<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>
) : (
<span className="inline-flex items-center gap-1">
<ExternalLink className="w-3.5 h-3.5" />
<span className="truncate inline-block">{getHostname(item.url)}</span>
</span>
)}
</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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -1,5 +1,4 @@
export * from "./AddKnowledgeDialog";
export * from "./DocumentBrowser";
export * from "./KnowledgeCard";
export * from "./KnowledgeList";
export * from "./KnowledgeTypeSelector";

View File

@@ -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

View File

@@ -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,51 @@ export const ContentViewer: React.FC<ContentViewerProps> = ({ selectedItem, onCo
);
}
// Highlight code with Prism
const highlightCode = (code: string, language?: string): string => {
try {
// Escape HTML entities FIRST per Prism documentation requirement
// Prism expects pre-escaped input to prevent XSS
const escaped = code
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
const lang = language?.toLowerCase() || "javascript";
const grammar = Prism.languages[lang] || Prism.languages.javascript;
return Prism.highlight(escaped, grammar, lang);
} catch (error) {
console.error("Prism highlighting failed:", error);
// Return escaped code on error
return code.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
};
// 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 +112,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 +166,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 +221,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>

View File

@@ -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" ? (

View File

@@ -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 ? (

View File

@@ -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>

View File

@@ -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(() => {