Spaces:
Runtime error
Runtime error
| 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> | |
| ); | |
| } |