diff --git a/ui/src/components/search/SearchResultRow.tsx b/ui/src/components/search/SearchResultRow.tsx index 9a341bf022a..11bb0e3da33 100644 --- a/ui/src/components/search/SearchResultRow.tsx +++ b/ui/src/components/search/SearchResultRow.tsx @@ -1,37 +1,26 @@ -import { useMemo } from "react"; -import { Bot, Hexagon } from "lucide-react"; +import type { ComponentType, SVGProps } from "react"; +import { Bot, FileText, Hexagon, MessageSquare, Quote } from "lucide-react"; import type { Agent, CompanySearchResult, Project } from "@paperclipai/shared"; import { Link } from "@/lib/router"; import { cn } from "@/lib/utils"; import { StatusIcon } from "../StatusIcon"; import { StatusBadge } from "../StatusBadge"; import { Identity } from "../Identity"; -import { HighlightedText } from "./HighlightedText"; -import { MatchSourceChip, type MatchSourceChipKind } from "./MatchSourceChip"; +import { HighlightedText, type HighlightedTextProps } from "./HighlightedText"; -const SNIPPET_FIELD_LABEL: Record = { - comment: "COMMENT", - document: "DOC", - identifier: "IDENTIFIER", - description: "DESCRIPTION", - title: "TITLE", +type SnippetStyle = { + Icon: ComponentType>; + label: string; }; -function deriveChips(matchedFields: string[]): Array<{ kind: MatchSourceChipKind; count?: number }> { - const counts = new Map(); - for (const field of matchedFields) { - let kind: MatchSourceChipKind | null = null; - if (field === "title" || field === "description") kind = "title"; - else if (field === "identifier") kind = "identifier"; - else if (field === "comment") kind = "comment"; - else if (field === "document") kind = "document"; - if (!kind) continue; - counts.set(kind, (counts.get(kind) ?? 0) + 1); - } - const order: MatchSourceChipKind[] = ["title", "identifier", "comment", "document"]; - return order - .filter((kind) => counts.has(kind)) - .map((kind) => ({ kind, count: counts.get(kind) ?? 1 })); +const SNIPPET_STYLES: Record = { + comment: { Icon: MessageSquare, label: "Comment" }, + document: { Icon: FileText, label: "Doc" }, + description: { Icon: Quote, label: "Description" }, +}; + +function snippetStyle(field: string, fallbackLabel: string): SnippetStyle { + return SNIPPET_STYLES[field] ?? { Icon: Quote, label: fallbackLabel }; } function formatRelativeTime(input: string | null): string { @@ -70,20 +59,18 @@ export function SearchResultRow({ isActive, className, }: SearchResultRowProps) { - const chips = useMemo(() => deriveChips(result.matchedFields), [result.matchedFields]); - if (result.type === "agent") { return ( - +
@@ -91,16 +78,12 @@ export function SearchResultRow({ {result.title}
{result.snippet ? ( -
- - {result.sourceLabel ?? "Agent"} - - -
+ ) : null} @@ -112,7 +95,7 @@ export function SearchResultRow({ {result.title} {result.snippet ? ( -
- - {result.sourceLabel ?? "Project"} - - -
+ ) : null} @@ -146,72 +125,57 @@ export function SearchResultRow({ const projectName = issue.projectId ? projectsById?.get(issue.projectId)?.name ?? null : null; const updated = formatRelativeTime(result.updatedAt ?? issue.updatedAt); const titleHighlights = result.snippets.find((snippet) => snippet.field === "title")?.highlights; + const bodySnippets = result.snippets.filter((snippet) => snippet.field !== "title").slice(0, 2); return ( -
+
-
+
{issue.identifier ? ( - + {issue.identifier} ) : null} -
+
{assigneeName ? : null} {projectName ? ( - + {projectName} ) : null} {updated ? ( - {updated} + {updated} ) : null}
- {chips.length > 0 ? ( -
- {chips.map((chip) => ( - - ))} -
- ) : null} - {result.snippets - .filter((snippet) => snippet.field !== "title") - .slice(0, 2) - .map((snippet, index) => ( -
- - {SNIPPET_FIELD_LABEL[snippet.field] ?? snippet.label.toUpperCase()} - - -
- ))} -
+ {bodySnippets.map((snippet, index) => ( + + ))} +
{assigneeName ? {assigneeName} : null} {projectName ? · {projectName} : null} @@ -221,3 +185,25 @@ export function SearchResultRow({ ); } + +interface SnippetLineProps { + text: string; + highlights?: HighlightedTextProps["highlights"]; + field: string; + fallbackLabel: string; +} + +function SnippetLine({ text, highlights, field, fallbackLabel }: SnippetLineProps) { + const { Icon, label } = snippetStyle(field, fallbackLabel); + return ( +
+ + {label}: + +
+ ); +} diff --git a/ui/src/pages/Search.tsx b/ui/src/pages/Search.tsx index 458af4c24c6..1331345a56a 100644 --- a/ui/src/pages/Search.tsx +++ b/ui/src/pages/Search.tsx @@ -13,6 +13,7 @@ import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; import { Badge } from "@/components/ui/badge"; +import { cn } from "@/lib/utils"; import { useNavigate, useSearchParams } from "@/lib/router"; import { useCompany } from "../context/CompanyContext"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; @@ -593,16 +594,20 @@ function SearchTabContent({ return (
-
+
{totalResults === 1 ? "1 result" : `${totalResults} results`} · sorted by relevance - {isFetching ? Updating… : null} + {isFetching ? Updating… : null}
{scope === "all" ? ( - subgroups.map((group) => ( -
+ subgroups.map((group, groupIndex) => ( +
0 && "mt-3 border-t border-border")} + > } - className="border-b border-border bg-muted/30" + className="px-4 pt-2 pb-1 text-[11px] tracking-wider text-muted-foreground" /> -
+
{group.results.map((result) => ( )) ) : ( -
+
{subgroups .flatMap((group) => group.results) .map((result) => ( diff --git a/ui/storybook/stories/search.stories.tsx b/ui/storybook/stories/search.stories.tsx index 40b1bfb2aa8..3bf2507cdc7 100644 --- a/ui/storybook/stories/search.stories.tsx +++ b/ui/storybook/stories/search.stories.tsx @@ -263,7 +263,7 @@ function SearchPagePreview({ {state === "results" ? (
-
+
{response.results.length} results · sorted by relevance
@@ -274,9 +274,9 @@ function SearchPagePreview({ {fixtureResults.length} } - className="border-b border-border bg-muted/30" + className="px-4 pt-2 pb-1 text-[11px] tracking-wider text-muted-foreground" /> -
+
{fixtureResults.map((result) => (
-
+
} - className="border-b border-border bg-muted/30" + className="px-4 pt-2 pb-1 text-[11px] tracking-wider text-muted-foreground" /> -
+
{fixtureAgents.map((result) => ( ))}
-
+
} - className="border-b border-border bg-muted/30" + className="px-4 pt-2 pb-1 text-[11px] tracking-wider text-muted-foreground" /> -
+
{fixtureProjects.map((result) => ( ))}