gadget/gadget-code/frontend/src/components/ChatSearchResults.tsx
2026-05-19 15:43:24 -04:00

130 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// src/components/ChatSearchResults.tsx
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
// All Rights Reserved
import { useState, useEffect, useCallback } from "react";
import { Search, X, Loader2 } from "lucide-react";
import { searchApi, ISearchResult } from "../lib/api";
interface ChatSearchResultsProps {
query: string;
filters?: { projectId?: string; sessionId?: string; turnId?: string };
onSelect: (result: ISearchResult) => void;
onClose: () => void;
}
export default function ChatSearchResults({
query,
filters,
onSelect,
onClose,
}: ChatSearchResultsProps) {
const [editQuery, setEditQuery] = useState(query);
const [results, setResults] = useState<ISearchResult[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const performSearch = useCallback(async () => {
if (!editQuery.trim()) return;
setLoading(true);
setError(null);
try {
const data = await searchApi.search(editQuery, filters, 20);
setResults(data);
} catch (err) {
setError((err as Error).message || "Search failed");
} finally {
setLoading(false);
}
}, [editQuery, filters]);
useEffect(() => { performSearch(); }, [performSearch]);
const handleSearchKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
e.preventDefault();
performSearch();
}
};
const formatScore = (score: number): string => `${Math.round(score * 100)}%`;
const formatTimestamp = (iso: string): string => {
if (!iso) return "";
try { return new Date(iso).toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric", hour: "2-digit", minute: "2-digit" }); }
catch { return iso; }
};
return (
<div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50"
onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
onKeyDown={(e) => { if (e.key === "Escape") onClose(); }}
>
<div className="bg-bg-secondary border border-border-default rounded-lg w-full max-w-3xl max-h-[80vh] flex flex-col mx-4">
{/* Header with editable search input */}
<div className="flex items-center gap-3 px-6 py-4 border-b border-border-default">
<Search className="text-text-tertiary shrink-0" size={16} />
<input
type="text"
value={editQuery}
onChange={(e) => setEditQuery(e.target.value)}
onKeyDown={handleSearchKeyDown}
className="flex-1 bg-transparent text-text-primary text-sm focus:outline-none placeholder:text-text-tertiary"
placeholder="Edit search query…"
autoFocus
/>
<button
onClick={performSearch}
className="text-text-tertiary hover:text-brand transition-colors shrink-0"
aria-label="Submit search"
>
<Search size={16} />
</button>
<button onClick={onClose} className="text-text-tertiary hover:text-text-primary transition-colors p-1 shrink-0" aria-label="Close">
<X size={18} />
</button>
</div>
{/* Result count bar */}
{!loading && (
<div className="px-6 py-2 text-sm text-text-tertiary border-b border-border-default">
{results.length} result{results.length !== 1 ? "s" : ""} for "{query === editQuery ? query : editQuery}"
</div>
)}
<div className="overflow-y-auto flex-1 p-4">
{loading && (
<div className="flex items-center justify-center py-12">
<Loader2 className="text-brand text-2xl animate-spin" />
<span className="ml-3 text-text-secondary">Searching</span>
</div>
)}
{error && <div className="text-center py-12"><p className="text-red-400">{error}</p></div>}
{!loading && !error && results.length === 0 && <div className="text-center py-12"><p className="text-text-tertiary">No results found</p></div>}
{!loading && !error && results.length > 0 && (
<div className="space-y-2">
{results.map((result) => (
<button key={result.id} onClick={() => onSelect(result)}
className="w-full text-left p-4 rounded-lg bg-bg-primary hover:bg-bg-tertiary border border-border-default hover:border-brand transition-colors group"
>
<div className="flex items-start justify-between gap-3">
<span className="shrink-0 inline-flex items-center px-2 py-0.5 rounded text-xs font-mono bg-brand/20 text-brand">{formatScore(result.score)}</span>
<div className="flex-1 min-w-0"><p className="text-sm text-text-primary line-clamp-3 whitespace-pre-wrap">{result.content}</p></div>
</div>
<div className="flex items-center gap-2 mt-2 text-xs text-text-tertiary">
{result.project?.name && <span>{result.project.name}</span>}
{result.session?.name && <><span></span><span>{result.session.name}</span></>}
<span></span>
<span className="font-mono">{result.turnId.slice(0, 8)}</span>
{result.createdAt && <><span className="mx-1">·</span><span>{formatTimestamp(result.createdAt)}</span></>}
</div>
</button>
))}
</div>
)}
</div>
</div>
</div>
);
}