|
import { useState } from "react"; |
|
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 { Search, Loader2 } from "lucide-react"; |
|
import { type SearchRequest } from "@shared/schema"; |
|
|
|
interface SearchInterfaceProps { |
|
onSearch: (request: SearchRequest) => void; |
|
isLoading?: boolean; |
|
} |
|
|
|
export default function SearchInterface({ onSearch, isLoading }: SearchInterfaceProps) { |
|
const [query, setQuery] = useState(""); |
|
const [searchType, setSearchType] = useState<"semantic" | "keyword" | "hybrid">("semantic"); |
|
const [sourceTypes, setSourceTypes] = useState<string[]>(["pdf", "web", "academic", "code"]); |
|
|
|
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(""); |
|
} |
|
}; |
|
|
|
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"> |
|
<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 to find relevant documents... (Press Enter to search, Esc to clear)" |
|
value={query} |
|
onChange={(e) => setQuery(e.target.value)} |
|
onKeyDown={handleKeyDown} |
|
className="pl-11 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" /> |
|
</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"> |
|
<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> |
|
</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> |
|
|
|
{} |
|
<div className="mt-4 pt-4 border-t border-slate-200 dark:border-slate-700"> |
|
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3"> |
|
<div> |
|
<h3 className="text-sm font-medium text-slate-700 dark:text-slate-300">AI Development Tools</h3> |
|
<p className="text-xs text-slate-500 dark:text-slate-400">Access external AI platforms and services</p> |
|
</div> |
|
<div className="flex flex-wrap gap-2"> |
|
<Button |
|
type="button" |
|
variant="outline" |
|
size="sm" |
|
onClick={() => window.open('https://studio.nebius.com/', '_blank')} |
|
className="text-xs hover:bg-blue-50 hover:border-blue-300 dark:hover:bg-blue-900/20" |
|
> |
|
π Nebius Studio |
|
</Button> |
|
<Button |
|
type="button" |
|
variant="outline" |
|
size="sm" |
|
onClick={() => window.open('https://platform.openai.com/playground', '_blank')} |
|
className="text-xs hover:bg-green-50 hover:border-green-300 dark:hover:bg-green-900/20" |
|
> |
|
π€ OpenAI Playground |
|
</Button> |
|
<Button |
|
type="button" |
|
variant="outline" |
|
size="sm" |
|
onClick={() => window.open('https://huggingface.co/spaces', '_blank')} |
|
className="text-xs hover:bg-orange-50 hover:border-orange-300 dark:hover:bg-orange-900/20" |
|
> |
|
π€ HuggingFace Spaces |
|
</Button> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
); |
|
} |
|
|