KnowledgeBridge / client /src /components /knowledge-base /enhanced-search-interface.tsx
fazeel007's picture
initial commit
7c012de
import { useState } from "react";
import { useMutation } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Checkbox } from "@/components/ui/checkbox";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Textarea } from "@/components/ui/textarea";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import {
Search,
Loader2,
Brain,
Sparkles,
Target,
FileText,
Lightbulb,
ChevronDown,
ChevronUp,
Wand2,
AlertCircle
} from "lucide-react";
import { type SearchRequest } from "@shared/schema";
interface SearchInterfaceProps {
onSearch: (request: SearchRequest) => void;
onAISearch?: (query: string) => void;
isLoading?: boolean;
onDocumentSelect?: (documentId: number) => void;
}
interface EnhancedSearchResult {
results: any[];
enhancedQuery?: {
enhancedQuery: string;
intent: string;
keywords: string[];
suggestions: string[];
};
searchInsights?: {
totalResults: number;
avgRelevanceScore: number;
modalResultsCount: number;
localResultsCount: number;
};
}
export default function EnhancedSearchInterface({ onSearch, onAISearch, isLoading, onDocumentSelect }: SearchInterfaceProps) {
const [query, setQuery] = useState("");
const [searchType, setSearchType] = useState<"semantic" | "keyword" | "hybrid">("semantic");
const [sourceTypes, setSourceTypes] = useState<string[]>(["pdf", "web", "academic", "code"]);
const [showAITools, setShowAITools] = useState(false);
const [analysisText, setAnalysisText] = useState("");
const [selectedDocuments, setSelectedDocuments] = useState<number[]>([]);
const [useMarkdown, setUseMarkdown] = useState(true);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!query.trim()) return;
onSearch({
query: query.trim(),
searchType,
filters: {
sourceTypes: sourceTypes.length > 0 ? sourceTypes : undefined,
},
limit: 10,
offset: 0,
});
};
const handleSourceTypeChange = (sourceType: string, checked: boolean) => {
setSourceTypes(prev =>
checked
? [...prev, sourceType]
: prev.filter(type => type !== sourceType)
);
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSubmit(e);
} else if (e.key === "Escape") {
setQuery("");
}
};
// Enhanced AI Search
const aiSearchMutation = useMutation({
mutationFn: async (searchQuery: string): Promise<EnhancedSearchResult> => {
const response = await fetch("/api/ai-search", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query: searchQuery,
maxResults: 10,
useQueryEnhancement: true
}),
});
if (!response.ok) throw new Error("Enhanced search failed");
return response.json();
},
});
// Query Enhancement
const queryEnhancementMutation = useMutation({
mutationFn: async (originalQuery: string) => {
const response = await fetch("/api/enhance-query", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query: originalQuery }),
});
if (!response.ok) throw new Error("Query enhancement failed");
return response.json();
},
});
// Document Analysis
const documentAnalysisMutation = useMutation({
mutationFn: async ({ content, analysisType, useMarkdown }: { content: string; analysisType: string; useMarkdown?: boolean }) => {
const response = await fetch("/api/analyze-document", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content, analysisType, useMarkdown }),
});
if (!response.ok) throw new Error("Document analysis failed");
return response.json();
},
});
// Generate Embeddings
const embeddingsMutation = useMutation({
mutationFn: async (input: string) => {
const response = await fetch("/api/embeddings", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ input }),
});
if (!response.ok) throw new Error("Embedding generation failed");
return response.json();
},
});
const handleEnhancedSearch = () => {
if (!query.trim()) return;
aiSearchMutation.mutate(query);
if (onAISearch) onAISearch(query);
};
const handleQueryEnhancement = () => {
if (!query.trim()) return;
queryEnhancementMutation.mutate(query);
};
const handleDocumentAnalysis = (analysisType: string) => {
if (!analysisText.trim()) return;
documentAnalysisMutation.mutate({
content: analysisText,
analysisType,
useMarkdown
});
};
const handleGenerateEmbeddings = () => {
if (!query.trim()) return;
embeddingsMutation.mutate(query);
};
const applyEnhancedQuery = (enhancedQuery: string) => {
setQuery(enhancedQuery);
onSearch({
query: enhancedQuery,
searchType,
filters: {
sourceTypes: sourceTypes.length > 0 ? sourceTypes : undefined,
},
limit: 10,
offset: 0,
});
};
return (
<div className="bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700 p-6 mb-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<Brain className="w-5 h-5 text-blue-600" />
<h2 className="text-lg font-semibold text-slate-900 dark:text-slate-100">AI-Enhanced Search</h2>
<Badge variant="secondary" className="text-xs">Powered by Nebius & Modal</Badge>
</div>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => setShowAITools(!showAITools)}
className="flex items-center gap-1"
>
<Wand2 className="w-4 h-4" />
AI Tools
{showAITools ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
</Button>
</div>
<form onSubmit={handleSubmit}>
<div className="flex flex-col lg:flex-row gap-4">
<div className="flex-1">
<Label htmlFor="knowledge-search" className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Search Knowledge Base
</Label>
<div className="relative">
<Input
id="knowledge-search"
type="text"
placeholder="Enter your query for AI-enhanced search... (Press Enter to search, Esc to clear)"
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={handleKeyDown}
className="pl-11 pr-12 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
disabled={isLoading}
aria-label="Search knowledge base"
/>
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-slate-400 w-4 h-4" />
{query && (
<Button
type="button"
variant="ghost"
size="sm"
onClick={handleQueryEnhancement}
disabled={queryEnhancementMutation.isPending}
className="absolute right-2 top-1/2 transform -translate-y-1/2 h-8 w-8 p-0"
title="Enhance query with AI"
>
{queryEnhancementMutation.isPending ? (
<Loader2 className="w-3 h-3 animate-spin" />
) : (
<Sparkles className="w-3 h-3 text-purple-500" />
)}
</Button>
)}
</div>
</div>
<div className="lg:w-auto">
<Label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
Search Type
</Label>
<Select value={searchType} onValueChange={(value: any) => setSearchType(value)}>
<SelectTrigger className="w-full lg:w-40">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="semantic">Semantic Search</SelectItem>
<SelectItem value="keyword">Keyword Search</SelectItem>
<SelectItem value="hybrid">Hybrid Search</SelectItem>
</SelectContent>
</Select>
</div>
<div className="lg:w-auto flex items-end gap-2">
<Button
type="submit"
disabled={!query.trim() || isLoading}
className="px-6 py-3 bg-blue-600 hover:bg-blue-700 focus:ring-2 focus:ring-blue-600 focus:ring-offset-2"
aria-label={isLoading ? "Searching knowledge base" : "Search knowledge base"}
>
{isLoading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" aria-hidden="true" />
Searching...
</>
) : (
<>
<Search className="w-4 h-4 mr-2" aria-hidden="true" />
Search
</>
)}
</Button>
<Button
type="button"
variant="outline"
onClick={handleEnhancedSearch}
disabled={!query.trim() || aiSearchMutation.isPending}
className="px-4 py-3 border-purple-300 text-purple-700 hover:bg-purple-50 dark:border-purple-600 dark:text-purple-300 dark:hover:bg-purple-900/20"
title="AI-Enhanced Search"
>
{aiSearchMutation.isPending ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Brain className="w-4 h-4" />
)}
</Button>
</div>
</div>
{/* Search Filters */}
<div className="mt-4 pt-4 border-t border-slate-200 dark:border-slate-700">
<div className="flex flex-wrap gap-6">
{[
{ id: "pdf", label: "PDFs" },
{ id: "web", label: "Web Pages" },
{ id: "academic", label: "Academic Papers" },
{ id: "code", label: "Code Repositories" }
].map(({ id, label }) => (
<div key={id} className="flex items-center space-x-2">
<Checkbox
id={`filter-${id}`}
checked={sourceTypes.includes(id)}
onCheckedChange={(checked) => handleSourceTypeChange(id, !!checked)}
/>
<Label
htmlFor={`filter-${id}`}
className="text-sm text-slate-600 dark:text-slate-400 cursor-pointer"
>
{label}
</Label>
</div>
))}
</div>
</div>
</form>
{/* Query Enhancement Results */}
{queryEnhancementMutation.data && (
<div className="mt-4">
<Card className="bg-purple-50 dark:bg-purple-900/20 border-purple-200 dark:border-purple-800">
<CardContent className="pt-4">
<div className="flex items-center justify-between mb-3">
<h4 className="font-semibold text-purple-900 dark:text-purple-100 flex items-center gap-2">
<Sparkles className="w-4 h-4" />
Enhanced Query Suggestion
</h4>
<Button
size="sm"
onClick={() => applyEnhancedQuery(queryEnhancementMutation.data.enhancedQuery)}
className="bg-purple-600 hover:bg-purple-700"
>
Use This Query
</Button>
</div>
<p className="text-sm mb-3 font-mono bg-white dark:bg-gray-800 p-3 rounded border">
{queryEnhancementMutation.data.enhancedQuery}
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-xs">
<div>
<span className="font-medium text-purple-800 dark:text-purple-200">Intent:</span>
<span className="ml-2">{queryEnhancementMutation.data.intent}</span>
</div>
<div>
<span className="font-medium text-purple-800 dark:text-purple-200">Keywords:</span>
<div className="flex flex-wrap gap-1 mt-1">
{queryEnhancementMutation.data.keywords.map((keyword: string, i: number) => (
<Badge key={i} variant="outline" className="text-xs">
{keyword}
</Badge>
))}
</div>
</div>
</div>
</CardContent>
</Card>
</div>
)}
{/* AI Enhanced Search Results */}
{aiSearchMutation.data && (
<div className="mt-4">
<Card className="bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800">
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-lg">
<Brain className="w-5 h-5 text-green-600" />
AI-Enhanced Results
{aiSearchMutation.data.searchInsights && (
<Badge variant="secondary">
{aiSearchMutation.data.searchInsights.totalResults} results
</Badge>
)}
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{aiSearchMutation.data.searchInsights && (
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 p-3 bg-white dark:bg-gray-800 rounded-lg text-sm">
<div>
<span className="font-medium">Avg Relevance:</span>
<span className="ml-1 text-green-600">
{(aiSearchMutation.data.searchInsights.avgRelevanceScore * 100).toFixed(1)}%
</span>
</div>
<div>
<span className="font-medium">Modal Results:</span>
<span className="ml-1">{aiSearchMutation.data.searchInsights.modalResultsCount}</span>
</div>
<div>
<span className="font-medium">Local Results:</span>
<span className="ml-1">{aiSearchMutation.data.searchInsights.localResultsCount}</span>
</div>
<div>
<span className="font-medium">Total:</span>
<span className="ml-1">{aiSearchMutation.data.searchInsights.totalResults}</span>
</div>
</div>
)}
<div className="text-sm text-green-700 dark:text-green-300">
✨ AI-enhanced search completed. Results are ranked by semantic relevance and include additional context.
</div>
</CardContent>
</Card>
</div>
)}
{/* Collapsible AI Tools */}
<Collapsible open={showAITools} onOpenChange={setShowAITools}>
<CollapsibleContent className="mt-4 pt-4 border-t border-slate-200 dark:border-slate-700">
<Tabs defaultValue="analysis" className="w-full">
<TabsList className="grid grid-cols-3 w-full mb-4">
<TabsTrigger value="analysis" className="flex items-center gap-2">
<FileText className="w-4 h-4" />
Analysis
</TabsTrigger>
<TabsTrigger value="embeddings" className="flex items-center gap-2">
<Sparkles className="w-4 h-4" />
Embeddings
</TabsTrigger>
<TabsTrigger value="tools" className="flex items-center gap-2">
<Target className="w-4 h-4" />
External Tools
</TabsTrigger>
</TabsList>
{/* Document Analysis Tab */}
<TabsContent value="analysis" className="space-y-4">
<div className="space-y-3">
<Label className="text-sm font-medium">Document Analysis</Label>
<Textarea
placeholder="Paste document content for AI analysis..."
value={analysisText}
onChange={(e) => setAnalysisText(e.target.value)}
className="min-h-24"
/>
{/* Formatting Option */}
<div className="flex items-center space-x-2 p-2 bg-gray-50 dark:bg-gray-800 rounded">
<Checkbox
id="use-markdown"
checked={useMarkdown}
onCheckedChange={(checked) => setUseMarkdown(!!checked)}
/>
<Label htmlFor="use-markdown" className="text-sm cursor-pointer">
Use markdown formatting (**bold**, bullet points, etc.)
</Label>
</div>
<div className="flex gap-2 flex-wrap">
{['summary', 'classification', 'key_points', 'quality_score'].map((type) => (
<Button
key={type}
variant="outline"
size="sm"
onClick={() => handleDocumentAnalysis(type)}
disabled={!analysisText.trim() || documentAnalysisMutation.isPending}
>
{documentAnalysisMutation.isPending ? (
<Loader2 className="w-3 h-3 animate-spin mr-1" />
) : (
<FileText className="w-3 h-3 mr-1" />
)}
{type.replace('_', ' ').toUpperCase()}
</Button>
))}
</div>
</div>
{documentAnalysisMutation.data && (
<Card className="bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800">
<CardHeader>
<CardTitle className="text-lg flex items-center gap-2">
<FileText className="w-5 h-5 text-blue-600" />
Analysis Result
</CardTitle>
</CardHeader>
<CardContent>
<div className="whitespace-pre-wrap text-sm">
{documentAnalysisMutation.data.analysis}
</div>
</CardContent>
</Card>
)}
</TabsContent>
{/* Embeddings Tab */}
<TabsContent value="embeddings" className="space-y-4">
<div className="space-y-3">
<Label className="text-sm font-medium">Generate Embeddings</Label>
<div className="flex gap-2">
<Input
placeholder="Text to generate embeddings..."
value={query}
onChange={(e) => setQuery(e.target.value)}
className="flex-1"
/>
<Button
onClick={handleGenerateEmbeddings}
disabled={!query.trim() || embeddingsMutation.isPending}
>
{embeddingsMutation.isPending ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Sparkles className="w-4 h-4" />
)}
</Button>
</div>
</div>
{embeddingsMutation.data && (
<Card className="bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-800">
<CardContent className="pt-4 space-y-3">
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="font-medium">Model:</span>
<span className="ml-2">{embeddingsMutation.data.model}</span>
</div>
<div>
<span className="font-medium">Dimensions:</span>
<span className="ml-2">{embeddingsMutation.data.data[0].embedding.length}</span>
</div>
</div>
<div>
<span className="font-medium text-sm">Vector (first 10 dimensions):</span>
<div className="font-mono text-xs bg-white dark:bg-gray-800 p-2 rounded mt-1 overflow-x-auto">
[{embeddingsMutation.data.data[0].embedding.slice(0, 10).map((val: number) => val.toFixed(4)).join(', ')}...]
</div>
</div>
</CardContent>
</Card>
)}
</TabsContent>
{/* External Tools Tab */}
<TabsContent value="tools" className="space-y-4">
<div>
<Label className="text-sm font-medium mb-3 block">AI Development Platforms</Label>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
<Button
variant="outline"
className="h-auto p-4 hover:bg-blue-50 hover:border-blue-300 dark:hover:bg-blue-900/20"
onClick={() => window.open('https://studio.nebius.com/', '_blank')}
>
<div className="text-center">
<div className="text-2xl mb-1">πŸš€</div>
<div className="font-medium">Nebius Studio</div>
<div className="text-xs text-muted-foreground">AI model training & deployment</div>
</div>
</Button>
<Button
variant="outline"
className="h-auto p-4 hover:bg-green-50 hover:border-green-300 dark:hover:bg-green-900/20"
onClick={() => window.open('https://platform.openai.com/playground', '_blank')}
>
<div className="text-center">
<div className="text-2xl mb-1">πŸ€–</div>
<div className="font-medium">OpenAI Playground</div>
<div className="text-xs text-muted-foreground">Test and tune prompts</div>
</div>
</Button>
<Button
variant="outline"
className="h-auto p-4 hover:bg-orange-50 hover:border-orange-300 dark:hover:bg-orange-900/20"
onClick={() => window.open('https://huggingface.co/spaces', '_blank')}
>
<div className="text-center">
<div className="text-2xl mb-1">πŸ€—</div>
<div className="font-medium">HuggingFace</div>
<div className="text-xs text-muted-foreground">Open source AI models</div>
</div>
</Button>
</div>
</div>
</TabsContent>
</Tabs>
</CollapsibleContent>
</Collapsible>
{/* Error States */}
{(aiSearchMutation.error || documentAnalysisMutation.error || embeddingsMutation.error || queryEnhancementMutation.error) && (
<div className="mt-4">
<Card className="border-red-200 dark:border-red-800 bg-red-50 dark:bg-red-900/20">
<CardContent className="pt-4">
<div className="flex items-center gap-2 text-red-700 dark:text-red-300">
<AlertCircle className="w-4 h-4" />
<span className="font-medium">AI Operation Error</span>
</div>
<p className="text-sm text-red-600 dark:text-red-400 mt-1">
{(aiSearchMutation.error || documentAnalysisMutation.error || embeddingsMutation.error || queryEnhancementMutation.error)?.message}
</p>
</CardContent>
</Card>
</div>
)}
</div>
);
}