|
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(""); |
|
} |
|
}; |
|
|
|
|
|
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(); |
|
}, |
|
}); |
|
|
|
|
|
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(); |
|
}, |
|
}); |
|
|
|
|
|
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(); |
|
}, |
|
}); |
|
|
|
|
|
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> |
|
|
|
{} |
|
<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> |
|
|
|
{} |
|
{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> |
|
)} |
|
|
|
{} |
|
{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 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> |
|
|
|
{} |
|
{(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> |
|
); |
|
} |