import { memo, type ComponentType, type SVGProps } from "react"; import { Bot, FileText, Hexagon, MessageSquare, Quote } from "lucide-react"; import type { Agent, CompanySearchResult } from "@paperclipai/shared"; import { Link } from "@/lib/router"; import { cn } from "@/lib/utils"; import { StatusIcon } from "../StatusIcon"; import { Identity } from "../Identity"; import { HighlightedText, type HighlightedTextProps } from "./HighlightedText"; type SnippetStyle = { Icon: ComponentType>; label: string; }; 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 { if (!input) return ""; const value = new Date(input); if (Number.isNaN(value.getTime())) return ""; const diffMs = Date.now() - value.getTime(); const seconds = Math.round(diffMs / 1000); if (seconds < 60) return "just now"; const minutes = Math.round(seconds / 60); if (minutes < 60) return `${minutes}m`; const hours = Math.round(minutes / 60); if (hours < 24) return `${hours}h`; const days = Math.round(hours / 24); if (days < 7) return `${days}d`; const weeks = Math.round(days / 7); if (weeks < 5) return `${weeks}w`; const months = Math.round(days / 30); if (months < 12) return `${months}mo`; const years = Math.round(days / 365); return `${years}y`; } export interface SearchResultRowProps { result: CompanySearchResult; agentsById?: ReadonlyMap>; isActive?: boolean; className?: string; } const ROW_BASE = "group flex items-start gap-3 rounded-md px-3 transition-colors no-underline text-inherit hover:bg-muted/40"; function SearchResultRowImpl({ result, agentsById, isActive, className, }: SearchResultRowProps) { if (result.type === "agent") { return (
{result.title}
{result.snippet ? ( ) : null}
); } if (result.type === "project") { return (
{result.title} {result.snippet ? ( ) : null}
); } const issue = result.issue; if (!issue) return null; const assigneeName = issue.assigneeAgentId ? agentsById?.get(issue.assigneeAgentId)?.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); const previewImageUrl = result.previewImageUrl; const hasRightRail = previewImageUrl || assigneeName || updated; return (
{issue.identifier ? ( {issue.identifier} ) : null}
{bodySnippets.map((snippet, index) => ( ))} {hasRightRail ? (
{assigneeName ? {assigneeName} : null} {updated ? {updated} : null}
) : null}
{hasRightRail ? (
{assigneeName || updated ? (
{assigneeName ? : null} {updated ? {updated} : null}
) : null} {previewImageUrl ? ( ) : null}
) : null} ); } export const SearchResultRow = memo(SearchResultRowImpl); interface SnippetLineProps { text: string; highlights?: HighlightedTextProps["highlights"]; field: string; fallbackLabel: string; multiline?: boolean; } function SnippetLine({ text, highlights, field, fallbackLabel, multiline = false }: SnippetLineProps) { const { Icon, label } = snippetStyle(field, fallbackLabel); return (
{label}:
); }