npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@rx-lab/dashboard-searching-ui

v1.1.2

Published

A React component library for building AI-powered search interfaces with streaming chat support.

Readme

searching-ui

A React component library for building AI-powered search interfaces with streaming chat support.

Features

  • SearchTrigger - Customizable search trigger button
  • SearchCommand - Command palette dialog with instant search results
  • SearchAgent - AI-powered chat interface with streaming responses
  • useSearchAgent - React hook for managing chat state
  • Full TypeScript support
  • Tailwind CSS v4 styling
  • shadcn/ui components included
  • Framer Motion animations

Installation

npm install @rx-lab/dashboard-searching-ui
# or
bun add @rx-lab/dashboard-searching-ui

Peer Dependencies

If you plan to use the AI agent features, you'll also need:

npm install @ai-sdk/react ai

Import Styles

Important: You must import the CSS file at the root of your application to ensure styles are loaded before components render:

// In your app entry point (e.g., _app.tsx, layout.tsx, main.tsx)
import "@rx-lab/dashboard-searching-ui/style.css";

Quick Start

Basic Search Command

import { useState } from "react";
import "@rx-lab/dashboard-searching-ui/style.css";
import { SearchTrigger, SearchCommand } from "@rx-lab/dashboard-searching-ui";

function App() {
  const [open, setOpen] = useState(false);

  return (
    <>
      <SearchTrigger onClick={() => setOpen(true)} />

      <SearchCommand
        open={open}
        onOpenChange={setOpen}
        onSearch={async ({ query, limit }) => {
          // Your search implementation
          const results = await fetch(`/api/search?q=${query}&limit=${limit}`);
          return results.json();
        }}
        onResultSelect={(result) => {
          // Handle result selection
          console.log("Selected:", result);
        }}
      />
    </>
  );
}

With AI Agent Mode

import { useState } from "react";
import "@rx-lab/dashboard-searching-ui/style.css";
import { SearchTrigger, SearchCommand } from "@rx-lab/dashboard-searching-ui";

function App() {
  const [open, setOpen] = useState(false);

  return (
    <>
      <SearchTrigger onClick={() => setOpen(true)} />

      <SearchCommand
        open={open}
        onOpenChange={setOpen}
        onSearch={async ({ query }) => {
          const results = await fetch(`/api/search?q=${query}`);
          return results.json();
        }}
        enableAgentMode
        agentConfig={{
          apiEndpoint: "/api/search-agent",
        }}
      />
    </>
  );
}

Components

SearchTrigger

A trigger button for opening the search dialog.

import { SearchTrigger } from "@rx-lab/dashboard-searching-ui";

// Basic usage
<SearchTrigger onClick={() => setOpen(true)} />

// Custom placeholder
<SearchTrigger
  onClick={() => setOpen(true)}
  placeholder="Find documents..."
/>

// Custom shortcut
<SearchTrigger
  onClick={() => setOpen(true)}
  shortcut={{ key: "P", modifier: "Ctrl" }}
/>

// Completely custom content
<SearchTrigger onClick={() => setOpen(true)}>
  <span>Custom Search Button</span>
</SearchTrigger>

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | onClick | () => void | required | Click handler to open search | | placeholder | string | "Search files..." | Placeholder text | | shortcut | ShortcutConfig \| null | { key: "K", modifier: "⌘" } | Keyboard shortcut display | | icon | ComponentType | Search | Icon component | | children | ReactNode | - | Custom content (replaces default) | | variant | string | "outline" | Button variant | | className | string | - | Additional class name |

SearchCommand

A search command dialog with optional AI agent mode.

import { SearchCommand } from "@rx-lab/dashboard-searching-ui";

<SearchCommand
  open={open}
  onOpenChange={setOpen}
  onSearch={async ({ query, searchType, limit }) => {
    const results = await searchAPI(query, { type: searchType, limit });
    return results;
  }}
  onResultSelect={(result) => {
    router.push(`/files/${result.id}`);
  }}
  searchTypes={[
    { id: "all", label: "All" },
    { id: "documents", label: "Documents" },
    { id: "images", label: "Images" },
  ]}
  renderResult={(result, onSelect) => (
    <div onClick={onSelect} className="p-2 cursor-pointer hover:bg-accent">
      <h3>{result.title}</h3>
      <p>{result.snippet}</p>
    </div>
  )}
  enableAgentMode
  agentConfig={{
    apiEndpoint: "/api/search-agent",
    header: { title: "AI Assistant" },
  }}
/>

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | open | boolean | required | Whether dialog is open | | onOpenChange | (open: boolean) => void | required | Open state handler | | onSearch | (params: SearchParams) => Promise<TResult[]> | required | Search function | | onResultSelect | (result: TResult) => void | - | Result selection handler | | searchTypes | SearchTypeConfig[] | - | Available search types | | debounceMs | number | 300 | Debounce delay | | limit | number | 10 | Max results | | renderResult | (result, onSelect) => ReactNode | - | Custom result renderer | | renderEmpty | (query, hasResults) => ReactNode | - | Custom empty state | | renderLoading | () => ReactNode | - | Custom loading state | | enableAgentMode | boolean | false | Enable AI agent mode | | agentConfig | Partial<SearchAgentProps> | - | Agent configuration | | placeholder | string | "Search..." | Input placeholder |

Customizing the AI Loading Indicator

When using agent mode, you can customize the loading indicator shown while the AI is processing:

import { Brain } from "lucide-react";

<SearchCommand
  open={open}
  onOpenChange={setOpen}
  onSearch={onSearch}
  enableAgentMode
  agentConfig={{
    apiEndpoint: "/api/search-agent",
    input: {
      streamingText: "Thinking...",
      streamingIcon: Brain,
    },
  }}
/>

SearchAgent

An AI-powered search agent component with streaming chat support.

import { SearchAgent } from "@rx-lab/dashboard-searching-ui";

<SearchAgent
  initialQuery="Find all PDF documents"
  apiEndpoint="/api/search-agent"
  onBack={() => setMode("quick")}
  onClose={() => setOpen(false)}
  header={{
    title: "AI Search Assistant",
    showBackButton: true,
    showClearButton: true,
  }}
  toolResultRenderers={{
    display_files: ({ output, onAction }) => (
      <div>
        {output.files.map((file) => (
          <FileCard
            key={file.id}
            file={file}
            onClick={() => onAction({ type: "navigate", payload: `/files/${file.id}` })}
          />
        ))}
      </div>
    ),
  }}
  renderAssistantContent={(content) => (
    <CustomMarkdown>{content}</CustomMarkdown>
  )}
/>

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | initialQuery | string | "" | Initial query to send | | initialMessages | UIMessage[] | - | Initial messages | | apiEndpoint | string | "/api/search-agent" | Chat API endpoint | | chatInstance | Chat<UIMessage> | - | Custom Chat instance | | toolResultRenderers | ToolResultRenderers | {} | Custom tool renderers | | onToolAction | (action: ToolAction) => void | - | Tool action handler | | renderMessage | (props) => ReactNode | - | Custom message renderer | | renderUserContent | (content: string) => ReactNode | - | Custom user content | | renderAssistantContent | (content: string) => ReactNode | - | Custom assistant content | | renderStreamingIndicator | () => ReactNode | - | Custom loading indicator | | onNavigate | (path: string) => void | - | Navigation handler | | onBack | () => void | - | Back button handler | | onClose | () => void | - | Close handler | | onClearHistory | () => void | - | Clear history handler | | header | SearchAgentHeaderConfig | - | Header configuration | | input | SearchAgentInputConfig | - | Input configuration | | enableMessageActions | boolean | true | Show action buttons on messages (Copy, Regenerate) | | onMessageRegenerate | (messageId, content) => void | - | Called when user regenerates a message |

Message Actions

By default, action buttons appear when hovering over messages. The buttons are:

| Action | Available On | Description | |--------|-------------|-------------| | Copy | All messages | Copy message content to clipboard | | Regenerate | All messages | For user messages: removes this and all subsequent messages, then resends. For assistant messages: regenerates this response |

To disable the action buttons:

<SearchAgent
  apiEndpoint="/api/search-agent"
  enableMessageActions={false}
/>

To handle regenerate events:

<SearchAgent
  apiEndpoint="/api/search-agent"
  onMessageRegenerate={(messageId, content) => {
    console.log("Regenerating:", messageId, content);
  }}
/>

Input Configuration

The input prop accepts an SearchAgentInputConfig object:

| Option | Type | Default | Description | |--------|------|---------|-------------| | placeholder | string | "Ask a follow-up question..." | Input placeholder when ready | | placeholderProcessing | string | "Generating..." | Input placeholder when processing | | streamingText | ReactNode | "Searching..." | Text or component shown in loading indicator | | streamingIcon | ComponentType \| null | Loader2 | Custom spinner icon, or null to hide |

Example with custom streaming indicator:

import { Brain } from "lucide-react";

<SearchAgent
  apiEndpoint="/api/search-agent"
  input={{
    placeholder: "Ask me anything...",
    streamingText: "Thinking...",
    streamingIcon: Brain,
  }}
/>

Example with fully custom content:

<SearchAgent
  apiEndpoint="/api/search-agent"
  input={{
    streamingText: (
      <span className="flex items-center gap-1">
        <Brain className="h-3 w-3 animate-pulse" />
        Analyzing your request...
      </span>
    ),
    streamingIcon: null, // Hide default spinner since we include our own
  }}
/>

Hooks

useSearchAgent

A hook for managing AI chat state with the Vercel AI SDK.

import { useSearchAgent } from "@rx-lab/dashboard-searching-ui";

function CustomChatInterface() {
  const {
    messages,
    isProcessing,
    sendMessage,
    stop,
    clearMessages,
    getTextContent,
    hasContent,
  } = useSearchAgent({
    apiEndpoint: "/api/search-agent",
    onMessagesChange: (messages) => {
      // Persist messages
      sessionStorage.setItem("chat", JSON.stringify(messages));
    },
    onToolCall: ({ toolName, args }) => {
      console.log("Tool called:", toolName, args);
    },
  });

  return (
    <div>
      {messages.map((message) => (
        <div key={message.id}>
          {getTextContent(message)}
        </div>
      ))}

      <button onClick={() => sendMessage("Hello!")}>
        Send
      </button>

      {isProcessing && (
        <button onClick={stop}>Stop</button>
      )}
    </div>
  );
}

Options

| Option | Type | Description | |--------|------|-------------| | apiEndpoint | string | API endpoint for chat | | initialMessages | UIMessage[] | Initial messages | | chatInstance | Chat<UIMessage> | Custom Chat instance | | onMessagesChange | (messages) => void | Messages change callback | | initialQuery | string | Auto-send query on mount | | onToolCall | (toolCall) => void | Tool call callback | | onFinish | (message) => void | Completion callback | | onError | (error) => void | Error callback |

Return Value

| Property | Type | Description | |----------|------|-------------| | messages | UIMessage[] | Current messages | | status | string | Current status | | isProcessing | boolean | Whether processing | | error | Error \| undefined | Current error | | sendMessage | (text: string) => void | Send a message | | stop | () => void | Stop generation | | clearMessages | () => void | Clear messages | | setMessages | function | Set messages directly | | chat | Chat<UIMessage> | Underlying Chat instance | | getTextContent | (message) => string | Extract text content | | getToolCalls | (message) => ToolCallInfo[] | Extract tool calls | | hasContent | (message) => boolean | Check if has content |

Sub-components

MessageBubble

Renders a single chat message with support for tool results.

import { MessageBubble } from "@rx-lab/dashboard-searching-ui";

<MessageBubble
  message={message}
  toolResultRenderers={{
    search_files: MyFileResultRenderer,
  }}
  renderUserContent={(content) => <span>{content}</span>}
  renderAssistantContent={(content) => <Markdown>{content}</Markdown>}
/>

StreamingIndicator

Shows a loading indicator while the AI is generating.

import { StreamingIndicator } from "@rx-lab/dashboard-searching-ui";

// Basic usage
<StreamingIndicator text="Thinking..." />

// Custom icons
<StreamingIndicator
  text="Processing..."
  botIcon={CustomBotIcon}
  loadingIcon={CustomSpinner}
/>

// Hide the spinner (when text includes its own)
<StreamingIndicator
  text={
    <span className="flex items-center gap-1">
      <Brain className="h-3 w-3 animate-pulse" />
      Analyzing...
    </span>
  }
  loadingIcon={null}
/>

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | text | ReactNode | "Searching..." | Text or component to display | | botIcon | ComponentType | Bot | Bot avatar icon | | loadingIcon | ComponentType \| null | Loader2 | Spinner icon, or null to hide | | className | string | - | Additional class name |

FileResultCard

A card component for displaying file results.

import { FileResultCard } from "@rx-lab/dashboard-searching-ui";

<FileResultCard
  file={{
    id: "123",
    title: "Document.pdf",
    fileType: "document",
    mimeType: "application/pdf",
    folderName: "My Folder",
  }}
  description="A sample document"
  onClick={() => navigate(`/files/123`)}
  renderIcon={(fileType, mimeType) => (
    <FileIcon type={fileType} />
  )}
/>

Custom Rendering

The library provides flexible customization options for rendering messages and tool results.

Custom User Message Rendering

Use renderUserContent to customize how user messages are displayed:

<SearchAgent
  apiEndpoint="/api/search-agent"
  renderUserContent={(content) => (
    <div className="font-medium text-blue-600">
      {content}
    </div>
  )}
/>

Custom Assistant Message Rendering

Use renderAssistantContent to customize assistant responses (e.g., with a custom Markdown renderer):

import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'

<SearchAgent
  apiEndpoint="/api/search-agent"
  renderAssistantContent={(content) => (
    <ReactMarkdown remarkPlugins={[remarkGfm]}>
      {content}
    </ReactMarkdown>
  )}
/>

Custom Tool Result Rendering

Use toolResultRenderers to provide custom renderers for specific tool outputs. Each renderer receives the tool's output and an action handler:

import { SearchAgent, ToolResultRendererProps, FileResultCard } from '@rx-lab/dashboard-searching-ui'

// Custom renderer for file search results
const FileResultsRenderer = ({ output, onAction }: ToolResultRendererProps) => {
  const files = output as Array<{ id: string; title: string; fileType: string }>

  return (
    <div className="grid gap-2">
      {files.map((file) => (
        <FileResultCard
          key={file.id}
          file={file}
          onClick={() => onAction?.({ type: 'open', payload: file })}
        />
      ))}
    </div>
  )
}

// Custom renderer for a different tool
const WeatherRenderer = ({ output }: ToolResultRendererProps) => {
  const data = output as { temp: number; condition: string }
  return (
    <div className="p-3 rounded-lg bg-blue-50">
      <p>{data.temp}°F - {data.condition}</p>
    </div>
  )
}

<SearchAgent
  apiEndpoint="/api/search-agent"
  toolResultRenderers={{
    display_files: FileResultsRenderer,
    get_weather: WeatherRenderer,
  }}
  onToolAction={(action) => {
    if (action.type === 'open') {
      router.push(`/files/${action.payload.id}`)
    }
  }}
/>

ToolResultRendererProps

| Prop | Type | Description | |------|------|-------------| | output | unknown | The tool's output data | | toolCallId | string | Unique identifier for the tool call | | onAction | (action: ToolAction) => void | Trigger actions to parent |

ToolAction

interface ToolAction {
  type: string    // Action type (e.g., 'open', 'download', 'select')
  payload: unknown // Action data
}

Complete Message Override

For full control over message rendering, use renderMessage to override the entire message bubble:

import { UIMessage } from '@rx-lab/dashboard-searching-ui'

<SearchAgent
  apiEndpoint="/api/search-agent"
  renderMessage={({ message, getTextContent, getToolCalls }) => {
    const content = getTextContent(message)
    const toolCalls = getToolCalls(message)

    return (
      <div className={message.role === 'user' ? 'text-right' : 'text-left'}>
        <div className="inline-block p-3 rounded-lg">
          <p>{content}</p>
          {toolCalls.map((tool) => (
            <div key={tool.toolCallId}>
              Tool: {tool.toolName}
            </div>
          ))}
        </div>
      </div>
    )
  }}
/>

Custom Icons

Customize user and bot avatar icons in MessageBubble:

import { User, Sparkles } from 'lucide-react'

<MessageBubble
  message={message}
  userIcon={User}
  botIcon={Sparkles}
/>

Using with SearchCommand

When using SearchCommand with agent mode, pass custom renderers through agentConfig:

<SearchCommand
  open={open}
  onOpenChange={setOpen}
  onSearch={onSearch}
  enableAgentMode
  agentConfig={{
    apiEndpoint: '/api/search-agent',
    renderUserContent: (content) => <strong>{content}</strong>,
    renderAssistantContent: (content) => <Markdown>{content}</Markdown>,
    toolResultRenderers: {
      display_files: FileResultsRenderer,
    },
    onToolAction: handleToolAction,
  }}
/>

Styling

The library uses Tailwind CSS v4 and includes shadcn/ui components. Import the stylesheet at the root of your application:

import "@rx-lab/dashboard-searching-ui/style.css";

To customize the theme, add CSS variables to your root:

:root {
  --background: 0 0% 100%;
  --foreground: 0 0% 3.9%;
  --primary: 0 0% 9%;
  --primary-foreground: 0 0% 98%;
  /* ... other variables */
}

Development

# Install dependencies
bun install

# Run tests
bun run test

# Build library
bun run build

# Run example app
bun run example:dev

License

MIT