@rx-lab/dashboard-searching-ui 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +704 -0
  2. package/dist/components/search/SearchAgent/SearchAgent.d.ts +103 -0
  3. package/dist/components/search/SearchAgent/SearchAgent.d.ts.map +1 -0
  4. package/dist/components/search/SearchAgent/components/FileResultCard.d.ts +39 -0
  5. package/dist/components/search/SearchAgent/components/FileResultCard.d.ts.map +1 -0
  6. package/dist/components/search/SearchAgent/components/MessageActions.d.ts +33 -0
  7. package/dist/components/search/SearchAgent/components/MessageActions.d.ts.map +1 -0
  8. package/dist/components/search/SearchAgent/components/MessageBubble.d.ts +54 -0
  9. package/dist/components/search/SearchAgent/components/MessageBubble.d.ts.map +1 -0
  10. package/dist/components/search/SearchAgent/components/StreamingIndicator.d.ts +23 -0
  11. package/dist/components/search/SearchAgent/components/StreamingIndicator.d.ts.map +1 -0
  12. package/dist/components/search/SearchAgent/components/index.d.ts +5 -0
  13. package/dist/components/search/SearchAgent/components/index.d.ts.map +1 -0
  14. package/dist/components/search/SearchAgent/index.d.ts +3 -0
  15. package/dist/components/search/SearchAgent/index.d.ts.map +1 -0
  16. package/dist/components/search/SearchCommand/SearchCommand.d.ts +99 -0
  17. package/dist/components/search/SearchCommand/SearchCommand.d.ts.map +1 -0
  18. package/dist/components/search/SearchCommand/index.d.ts +2 -0
  19. package/dist/components/search/SearchCommand/index.d.ts.map +1 -0
  20. package/dist/components/search/SearchTrigger/SearchTrigger.d.ts +60 -0
  21. package/dist/components/search/SearchTrigger/SearchTrigger.d.ts.map +1 -0
  22. package/dist/components/search/SearchTrigger/index.d.ts +2 -0
  23. package/dist/components/search/SearchTrigger/index.d.ts.map +1 -0
  24. package/dist/components/ui/badge.d.ts +10 -0
  25. package/dist/components/ui/badge.d.ts.map +1 -0
  26. package/dist/components/ui/button.d.ts +11 -0
  27. package/dist/components/ui/button.d.ts.map +1 -0
  28. package/dist/components/ui/command.d.ts +20 -0
  29. package/dist/components/ui/command.d.ts.map +1 -0
  30. package/dist/components/ui/context-menu.d.ts +26 -0
  31. package/dist/components/ui/context-menu.d.ts.map +1 -0
  32. package/dist/components/ui/dialog.d.ts +16 -0
  33. package/dist/components/ui/dialog.d.ts.map +1 -0
  34. package/dist/components/ui/scroll-area.d.ts +6 -0
  35. package/dist/components/ui/scroll-area.d.ts.map +1 -0
  36. package/dist/components/ui/tooltip.d.ts +8 -0
  37. package/dist/components/ui/tooltip.d.ts.map +1 -0
  38. package/dist/hooks/index.d.ts +2 -0
  39. package/dist/hooks/index.d.ts.map +1 -0
  40. package/dist/hooks/useSearchAgent.d.ts +90 -0
  41. package/dist/hooks/useSearchAgent.d.ts.map +1 -0
  42. package/dist/index.d.ts +13 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/lib/utils.d.ts +3 -0
  45. package/dist/lib/utils.d.ts.map +1 -0
  46. package/dist/package.json +42 -0
  47. package/dist/searching-ui.js +16278 -0
  48. package/dist/searching-ui.js.map +1 -0
  49. package/dist/test/setup.d.ts +1 -0
  50. package/dist/test/setup.d.ts.map +1 -0
  51. package/package.json +90 -0
package/README.md ADDED
@@ -0,0 +1,704 @@
1
+ # searching-ui
2
+
3
+ A React component library for building AI-powered search interfaces with streaming chat support.
4
+
5
+ ## Features
6
+
7
+ - **SearchTrigger** - Customizable search trigger button
8
+ - **SearchCommand** - Command palette dialog with instant search results
9
+ - **SearchAgent** - AI-powered chat interface with streaming responses
10
+ - **useSearchAgent** - React hook for managing chat state
11
+ - Full TypeScript support
12
+ - Tailwind CSS v4 styling
13
+ - shadcn/ui components included
14
+ - Framer Motion animations
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install searching-ui
20
+ # or
21
+ bun add searching-ui
22
+ ```
23
+
24
+ ### Peer Dependencies
25
+
26
+ If you plan to use the AI agent features, you'll also need:
27
+
28
+ ```bash
29
+ npm install @ai-sdk/react ai
30
+ ```
31
+
32
+ ## Quick Start
33
+
34
+ ### Basic Search Command
35
+
36
+ ```tsx
37
+ import { useState } from "react";
38
+ import { SearchTrigger, SearchCommand } from "searching-ui";
39
+
40
+ function App() {
41
+ const [open, setOpen] = useState(false);
42
+
43
+ return (
44
+ <>
45
+ <SearchTrigger onClick={() => setOpen(true)} />
46
+
47
+ <SearchCommand
48
+ open={open}
49
+ onOpenChange={setOpen}
50
+ onSearch={async ({ query, limit }) => {
51
+ // Your search implementation
52
+ const results = await fetch(`/api/search?q=${query}&limit=${limit}`);
53
+ return results.json();
54
+ }}
55
+ onResultSelect={(result) => {
56
+ // Handle result selection
57
+ console.log("Selected:", result);
58
+ }}
59
+ />
60
+ </>
61
+ );
62
+ }
63
+ ```
64
+
65
+ ### With AI Agent Mode
66
+
67
+ ```tsx
68
+ import { useState } from "react";
69
+ import { SearchTrigger, SearchCommand } from "searching-ui";
70
+
71
+ function App() {
72
+ const [open, setOpen] = useState(false);
73
+
74
+ return (
75
+ <>
76
+ <SearchTrigger onClick={() => setOpen(true)} />
77
+
78
+ <SearchCommand
79
+ open={open}
80
+ onOpenChange={setOpen}
81
+ onSearch={async ({ query }) => {
82
+ const results = await fetch(`/api/search?q=${query}`);
83
+ return results.json();
84
+ }}
85
+ enableAgentMode
86
+ agentConfig={{
87
+ apiEndpoint: "/api/search-agent",
88
+ }}
89
+ />
90
+ </>
91
+ );
92
+ }
93
+ ```
94
+
95
+ ## Components
96
+
97
+ ### SearchTrigger
98
+
99
+ A trigger button for opening the search dialog.
100
+
101
+ ```tsx
102
+ import { SearchTrigger } from "searching-ui";
103
+
104
+ // Basic usage
105
+ <SearchTrigger onClick={() => setOpen(true)} />
106
+
107
+ // Custom placeholder
108
+ <SearchTrigger
109
+ onClick={() => setOpen(true)}
110
+ placeholder="Find documents..."
111
+ />
112
+
113
+ // Custom shortcut
114
+ <SearchTrigger
115
+ onClick={() => setOpen(true)}
116
+ shortcut={{ key: "P", modifier: "Ctrl" }}
117
+ />
118
+
119
+ // Completely custom content
120
+ <SearchTrigger onClick={() => setOpen(true)}>
121
+ <span>Custom Search Button</span>
122
+ </SearchTrigger>
123
+ ```
124
+
125
+ #### Props
126
+
127
+ | Prop | Type | Default | Description |
128
+ |------|------|---------|-------------|
129
+ | `onClick` | `() => void` | required | Click handler to open search |
130
+ | `placeholder` | `string` | `"Search files..."` | Placeholder text |
131
+ | `shortcut` | `ShortcutConfig \| null` | `{ key: "K", modifier: "⌘" }` | Keyboard shortcut display |
132
+ | `icon` | `ComponentType` | `Search` | Icon component |
133
+ | `children` | `ReactNode` | - | Custom content (replaces default) |
134
+ | `variant` | `string` | `"outline"` | Button variant |
135
+ | `className` | `string` | - | Additional class name |
136
+
137
+ ### SearchCommand
138
+
139
+ A search command dialog with optional AI agent mode.
140
+
141
+ ```tsx
142
+ import { SearchCommand } from "searching-ui";
143
+
144
+ <SearchCommand
145
+ open={open}
146
+ onOpenChange={setOpen}
147
+ onSearch={async ({ query, searchType, limit }) => {
148
+ const results = await searchAPI(query, { type: searchType, limit });
149
+ return results;
150
+ }}
151
+ onResultSelect={(result) => {
152
+ router.push(`/files/${result.id}`);
153
+ }}
154
+ searchTypes={[
155
+ { id: "all", label: "All" },
156
+ { id: "documents", label: "Documents" },
157
+ { id: "images", label: "Images" },
158
+ ]}
159
+ renderResult={(result, onSelect) => (
160
+ <div onClick={onSelect} className="p-2 cursor-pointer hover:bg-accent">
161
+ <h3>{result.title}</h3>
162
+ <p>{result.snippet}</p>
163
+ </div>
164
+ )}
165
+ enableAgentMode
166
+ agentConfig={{
167
+ apiEndpoint: "/api/search-agent",
168
+ header: { title: "AI Assistant" },
169
+ }}
170
+ />
171
+ ```
172
+
173
+ #### Props
174
+
175
+ | Prop | Type | Default | Description |
176
+ |------|------|---------|-------------|
177
+ | `open` | `boolean` | required | Whether dialog is open |
178
+ | `onOpenChange` | `(open: boolean) => void` | required | Open state handler |
179
+ | `onSearch` | `(params: SearchParams) => Promise<TResult[]>` | required | Search function |
180
+ | `onResultSelect` | `(result: TResult) => void` | - | Result selection handler |
181
+ | `searchTypes` | `SearchTypeConfig[]` | - | Available search types |
182
+ | `debounceMs` | `number` | `300` | Debounce delay |
183
+ | `limit` | `number` | `10` | Max results |
184
+ | `renderResult` | `(result, onSelect) => ReactNode` | - | Custom result renderer |
185
+ | `renderEmpty` | `(query, hasResults) => ReactNode` | - | Custom empty state |
186
+ | `renderLoading` | `() => ReactNode` | - | Custom loading state |
187
+ | `enableAgentMode` | `boolean` | `false` | Enable AI agent mode |
188
+ | `agentConfig` | `Partial<SearchAgentProps>` | - | Agent configuration |
189
+ | `placeholder` | `string` | `"Search..."` | Input placeholder |
190
+
191
+ #### Customizing the AI Loading Indicator
192
+
193
+ When using agent mode, you can customize the loading indicator shown while the AI is processing:
194
+
195
+ ```tsx
196
+ import { Brain } from "lucide-react";
197
+
198
+ <SearchCommand
199
+ open={open}
200
+ onOpenChange={setOpen}
201
+ onSearch={onSearch}
202
+ enableAgentMode
203
+ agentConfig={{
204
+ apiEndpoint: "/api/search-agent",
205
+ input: {
206
+ streamingText: "Thinking...",
207
+ streamingIcon: Brain,
208
+ },
209
+ }}
210
+ />
211
+ ```
212
+
213
+ ### SearchAgent
214
+
215
+ An AI-powered search agent component with streaming chat support.
216
+
217
+ ```tsx
218
+ import { SearchAgent } from "searching-ui";
219
+
220
+ <SearchAgent
221
+ initialQuery="Find all PDF documents"
222
+ apiEndpoint="/api/search-agent"
223
+ onBack={() => setMode("quick")}
224
+ onClose={() => setOpen(false)}
225
+ header={{
226
+ title: "AI Search Assistant",
227
+ showBackButton: true,
228
+ showClearButton: true,
229
+ }}
230
+ toolResultRenderers={{
231
+ display_files: ({ output, onAction }) => (
232
+ <div>
233
+ {output.files.map((file) => (
234
+ <FileCard
235
+ key={file.id}
236
+ file={file}
237
+ onClick={() => onAction({ type: "navigate", payload: `/files/${file.id}` })}
238
+ />
239
+ ))}
240
+ </div>
241
+ ),
242
+ }}
243
+ renderAssistantContent={(content) => (
244
+ <CustomMarkdown>{content}</CustomMarkdown>
245
+ )}
246
+ />
247
+ ```
248
+
249
+ #### Props
250
+
251
+ | Prop | Type | Default | Description |
252
+ |------|------|---------|-------------|
253
+ | `initialQuery` | `string` | `""` | Initial query to send |
254
+ | `initialMessages` | `UIMessage[]` | - | Initial messages |
255
+ | `apiEndpoint` | `string` | `"/api/search-agent"` | Chat API endpoint |
256
+ | `chatInstance` | `Chat<UIMessage>` | - | Custom Chat instance |
257
+ | `toolResultRenderers` | `ToolResultRenderers` | `{}` | Custom tool renderers |
258
+ | `onToolAction` | `(action: ToolAction) => void` | - | Tool action handler |
259
+ | `renderMessage` | `(props) => ReactNode` | - | Custom message renderer |
260
+ | `renderUserContent` | `(content: string) => ReactNode` | - | Custom user content |
261
+ | `renderAssistantContent` | `(content: string) => ReactNode` | - | Custom assistant content |
262
+ | `renderStreamingIndicator` | `() => ReactNode` | - | Custom loading indicator |
263
+ | `onNavigate` | `(path: string) => void` | - | Navigation handler |
264
+ | `onBack` | `() => void` | - | Back button handler |
265
+ | `onClose` | `() => void` | - | Close handler |
266
+ | `onClearHistory` | `() => void` | - | Clear history handler |
267
+ | `header` | `SearchAgentHeaderConfig` | - | Header configuration |
268
+ | `input` | `SearchAgentInputConfig` | - | Input configuration |
269
+ | `enableMessageActions` | `boolean` | `true` | Show action buttons on messages (Copy, Regenerate) |
270
+ | `onMessageRegenerate` | `(messageId, content) => void` | - | Called when user regenerates a message |
271
+
272
+ #### Message Actions
273
+
274
+ By default, action buttons appear when hovering over messages. The buttons are:
275
+
276
+ | Action | Available On | Description |
277
+ |--------|-------------|-------------|
278
+ | **Copy** | All messages | Copy message content to clipboard |
279
+ | **Regenerate** | All messages | For user messages: removes this and all subsequent messages, then resends. For assistant messages: regenerates this response |
280
+
281
+ To disable the action buttons:
282
+
283
+ ```tsx
284
+ <SearchAgent
285
+ apiEndpoint="/api/search-agent"
286
+ enableMessageActions={false}
287
+ />
288
+ ```
289
+
290
+ To handle regenerate events:
291
+
292
+ ```tsx
293
+ <SearchAgent
294
+ apiEndpoint="/api/search-agent"
295
+ onMessageRegenerate={(messageId, content) => {
296
+ console.log("Regenerating:", messageId, content);
297
+ }}
298
+ />
299
+ ```
300
+
301
+ #### Input Configuration
302
+
303
+ The `input` prop accepts an `SearchAgentInputConfig` object:
304
+
305
+ | Option | Type | Default | Description |
306
+ |--------|------|---------|-------------|
307
+ | `placeholder` | `string` | `"Ask a follow-up question..."` | Input placeholder when ready |
308
+ | `placeholderProcessing` | `string` | `"Generating..."` | Input placeholder when processing |
309
+ | `streamingText` | `ReactNode` | `"Searching..."` | Text or component shown in loading indicator |
310
+ | `streamingIcon` | `ComponentType \| null` | `Loader2` | Custom spinner icon, or `null` to hide |
311
+
312
+ **Example with custom streaming indicator:**
313
+
314
+ ```tsx
315
+ import { Brain } from "lucide-react";
316
+
317
+ <SearchAgent
318
+ apiEndpoint="/api/search-agent"
319
+ input={{
320
+ placeholder: "Ask me anything...",
321
+ streamingText: "Thinking...",
322
+ streamingIcon: Brain,
323
+ }}
324
+ />
325
+ ```
326
+
327
+ **Example with fully custom content:**
328
+
329
+ ```tsx
330
+ <SearchAgent
331
+ apiEndpoint="/api/search-agent"
332
+ input={{
333
+ streamingText: (
334
+ <span className="flex items-center gap-1">
335
+ <Brain className="h-3 w-3 animate-pulse" />
336
+ Analyzing your request...
337
+ </span>
338
+ ),
339
+ streamingIcon: null, // Hide default spinner since we include our own
340
+ }}
341
+ />
342
+ ```
343
+
344
+ ## Hooks
345
+
346
+ ### useSearchAgent
347
+
348
+ A hook for managing AI chat state with the Vercel AI SDK.
349
+
350
+ ```tsx
351
+ import { useSearchAgent } from "searching-ui";
352
+
353
+ function CustomChatInterface() {
354
+ const {
355
+ messages,
356
+ isProcessing,
357
+ sendMessage,
358
+ stop,
359
+ clearMessages,
360
+ getTextContent,
361
+ hasContent,
362
+ } = useSearchAgent({
363
+ apiEndpoint: "/api/search-agent",
364
+ onMessagesChange: (messages) => {
365
+ // Persist messages
366
+ sessionStorage.setItem("chat", JSON.stringify(messages));
367
+ },
368
+ onToolCall: ({ toolName, args }) => {
369
+ console.log("Tool called:", toolName, args);
370
+ },
371
+ });
372
+
373
+ return (
374
+ <div>
375
+ {messages.map((message) => (
376
+ <div key={message.id}>
377
+ {getTextContent(message)}
378
+ </div>
379
+ ))}
380
+
381
+ <button onClick={() => sendMessage("Hello!")}>
382
+ Send
383
+ </button>
384
+
385
+ {isProcessing && (
386
+ <button onClick={stop}>Stop</button>
387
+ )}
388
+ </div>
389
+ );
390
+ }
391
+ ```
392
+
393
+ #### Options
394
+
395
+ | Option | Type | Description |
396
+ |--------|------|-------------|
397
+ | `apiEndpoint` | `string` | API endpoint for chat |
398
+ | `initialMessages` | `UIMessage[]` | Initial messages |
399
+ | `chatInstance` | `Chat<UIMessage>` | Custom Chat instance |
400
+ | `onMessagesChange` | `(messages) => void` | Messages change callback |
401
+ | `initialQuery` | `string` | Auto-send query on mount |
402
+ | `onToolCall` | `(toolCall) => void` | Tool call callback |
403
+ | `onFinish` | `(message) => void` | Completion callback |
404
+ | `onError` | `(error) => void` | Error callback |
405
+
406
+ #### Return Value
407
+
408
+ | Property | Type | Description |
409
+ |----------|------|-------------|
410
+ | `messages` | `UIMessage[]` | Current messages |
411
+ | `status` | `string` | Current status |
412
+ | `isProcessing` | `boolean` | Whether processing |
413
+ | `error` | `Error \| undefined` | Current error |
414
+ | `sendMessage` | `(text: string) => void` | Send a message |
415
+ | `stop` | `() => void` | Stop generation |
416
+ | `clearMessages` | `() => void` | Clear messages |
417
+ | `setMessages` | `function` | Set messages directly |
418
+ | `chat` | `Chat<UIMessage>` | Underlying Chat instance |
419
+ | `getTextContent` | `(message) => string` | Extract text content |
420
+ | `getToolCalls` | `(message) => ToolCallInfo[]` | Extract tool calls |
421
+ | `hasContent` | `(message) => boolean` | Check if has content |
422
+
423
+ ## Sub-components
424
+
425
+ ### MessageBubble
426
+
427
+ Renders a single chat message with support for tool results.
428
+
429
+ ```tsx
430
+ import { MessageBubble } from "searching-ui";
431
+
432
+ <MessageBubble
433
+ message={message}
434
+ toolResultRenderers={{
435
+ search_files: MyFileResultRenderer,
436
+ }}
437
+ renderUserContent={(content) => <span>{content}</span>}
438
+ renderAssistantContent={(content) => <Markdown>{content}</Markdown>}
439
+ />
440
+ ```
441
+
442
+ ### StreamingIndicator
443
+
444
+ Shows a loading indicator while the AI is generating.
445
+
446
+ ```tsx
447
+ import { StreamingIndicator } from "searching-ui";
448
+
449
+ // Basic usage
450
+ <StreamingIndicator text="Thinking..." />
451
+
452
+ // Custom icons
453
+ <StreamingIndicator
454
+ text="Processing..."
455
+ botIcon={CustomBotIcon}
456
+ loadingIcon={CustomSpinner}
457
+ />
458
+
459
+ // Hide the spinner (when text includes its own)
460
+ <StreamingIndicator
461
+ text={
462
+ <span className="flex items-center gap-1">
463
+ <Brain className="h-3 w-3 animate-pulse" />
464
+ Analyzing...
465
+ </span>
466
+ }
467
+ loadingIcon={null}
468
+ />
469
+ ```
470
+
471
+ #### Props
472
+
473
+ | Prop | Type | Default | Description |
474
+ |------|------|---------|-------------|
475
+ | `text` | `ReactNode` | `"Searching..."` | Text or component to display |
476
+ | `botIcon` | `ComponentType` | `Bot` | Bot avatar icon |
477
+ | `loadingIcon` | `ComponentType \| null` | `Loader2` | Spinner icon, or `null` to hide |
478
+ | `className` | `string` | - | Additional class name |
479
+
480
+ ### FileResultCard
481
+
482
+ A card component for displaying file results.
483
+
484
+ ```tsx
485
+ import { FileResultCard } from "searching-ui";
486
+
487
+ <FileResultCard
488
+ file={{
489
+ id: "123",
490
+ title: "Document.pdf",
491
+ fileType: "document",
492
+ mimeType: "application/pdf",
493
+ folderName: "My Folder",
494
+ }}
495
+ description="A sample document"
496
+ onClick={() => navigate(`/files/123`)}
497
+ renderIcon={(fileType, mimeType) => (
498
+ <FileIcon type={fileType} />
499
+ )}
500
+ />
501
+ ```
502
+
503
+ ## Custom Rendering
504
+
505
+ The library provides flexible customization options for rendering messages and tool results.
506
+
507
+ ### Custom User Message Rendering
508
+
509
+ Use `renderUserContent` to customize how user messages are displayed:
510
+
511
+ ```tsx
512
+ <SearchAgent
513
+ apiEndpoint="/api/search-agent"
514
+ renderUserContent={(content) => (
515
+ <div className="font-medium text-blue-600">
516
+ {content}
517
+ </div>
518
+ )}
519
+ />
520
+ ```
521
+
522
+ ### Custom Assistant Message Rendering
523
+
524
+ Use `renderAssistantContent` to customize assistant responses (e.g., with a custom Markdown renderer):
525
+
526
+ ```tsx
527
+ import ReactMarkdown from 'react-markdown'
528
+ import remarkGfm from 'remark-gfm'
529
+
530
+ <SearchAgent
531
+ apiEndpoint="/api/search-agent"
532
+ renderAssistantContent={(content) => (
533
+ <ReactMarkdown remarkPlugins={[remarkGfm]}>
534
+ {content}
535
+ </ReactMarkdown>
536
+ )}
537
+ />
538
+ ```
539
+
540
+ ### Custom Tool Result Rendering
541
+
542
+ Use `toolResultRenderers` to provide custom renderers for specific tool outputs. Each renderer receives the tool's output and an action handler:
543
+
544
+ ```tsx
545
+ import { SearchAgent, ToolResultRendererProps, FileResultCard } from 'searching-ui'
546
+
547
+ // Custom renderer for file search results
548
+ const FileResultsRenderer = ({ output, onAction }: ToolResultRendererProps) => {
549
+ const files = output as Array<{ id: string; title: string; fileType: string }>
550
+
551
+ return (
552
+ <div className="grid gap-2">
553
+ {files.map((file) => (
554
+ <FileResultCard
555
+ key={file.id}
556
+ file={file}
557
+ onClick={() => onAction?.({ type: 'open', payload: file })}
558
+ />
559
+ ))}
560
+ </div>
561
+ )
562
+ }
563
+
564
+ // Custom renderer for a different tool
565
+ const WeatherRenderer = ({ output }: ToolResultRendererProps) => {
566
+ const data = output as { temp: number; condition: string }
567
+ return (
568
+ <div className="p-3 rounded-lg bg-blue-50">
569
+ <p>{data.temp}°F - {data.condition}</p>
570
+ </div>
571
+ )
572
+ }
573
+
574
+ <SearchAgent
575
+ apiEndpoint="/api/search-agent"
576
+ toolResultRenderers={{
577
+ display_files: FileResultsRenderer,
578
+ get_weather: WeatherRenderer,
579
+ }}
580
+ onToolAction={(action) => {
581
+ if (action.type === 'open') {
582
+ router.push(`/files/${action.payload.id}`)
583
+ }
584
+ }}
585
+ />
586
+ ```
587
+
588
+ #### ToolResultRendererProps
589
+
590
+ | Prop | Type | Description |
591
+ |------|------|-------------|
592
+ | `output` | `unknown` | The tool's output data |
593
+ | `toolCallId` | `string` | Unique identifier for the tool call |
594
+ | `onAction` | `(action: ToolAction) => void` | Trigger actions to parent |
595
+
596
+ #### ToolAction
597
+
598
+ ```tsx
599
+ interface ToolAction {
600
+ type: string // Action type (e.g., 'open', 'download', 'select')
601
+ payload: unknown // Action data
602
+ }
603
+ ```
604
+
605
+ ### Complete Message Override
606
+
607
+ For full control over message rendering, use `renderMessage` to override the entire message bubble:
608
+
609
+ ```tsx
610
+ import { UIMessage } from 'searching-ui'
611
+
612
+ <SearchAgent
613
+ apiEndpoint="/api/search-agent"
614
+ renderMessage={({ message, getTextContent, getToolCalls }) => {
615
+ const content = getTextContent(message)
616
+ const toolCalls = getToolCalls(message)
617
+
618
+ return (
619
+ <div className={message.role === 'user' ? 'text-right' : 'text-left'}>
620
+ <div className="inline-block p-3 rounded-lg">
621
+ <p>{content}</p>
622
+ {toolCalls.map((tool) => (
623
+ <div key={tool.toolCallId}>
624
+ Tool: {tool.toolName}
625
+ </div>
626
+ ))}
627
+ </div>
628
+ </div>
629
+ )
630
+ }}
631
+ />
632
+ ```
633
+
634
+ ### Custom Icons
635
+
636
+ Customize user and bot avatar icons in `MessageBubble`:
637
+
638
+ ```tsx
639
+ import { User, Sparkles } from 'lucide-react'
640
+
641
+ <MessageBubble
642
+ message={message}
643
+ userIcon={User}
644
+ botIcon={Sparkles}
645
+ />
646
+ ```
647
+
648
+ ### Using with SearchCommand
649
+
650
+ When using `SearchCommand` with agent mode, pass custom renderers through `agentConfig`:
651
+
652
+ ```tsx
653
+ <SearchCommand
654
+ open={open}
655
+ onOpenChange={setOpen}
656
+ onSearch={onSearch}
657
+ enableAgentMode
658
+ agentConfig={{
659
+ apiEndpoint: '/api/search-agent',
660
+ renderUserContent: (content) => <strong>{content}</strong>,
661
+ renderAssistantContent: (content) => <Markdown>{content}</Markdown>,
662
+ toolResultRenderers: {
663
+ display_files: FileResultsRenderer,
664
+ },
665
+ onToolAction: handleToolAction,
666
+ }}
667
+ />
668
+ ```
669
+
670
+ ## Styling
671
+
672
+ The library uses Tailwind CSS v4 and includes shadcn/ui components. Styles are automatically injected when importing components.
673
+
674
+ To customize the theme, add CSS variables to your root:
675
+
676
+ ```css
677
+ :root {
678
+ --background: 0 0% 100%;
679
+ --foreground: 0 0% 3.9%;
680
+ --primary: 0 0% 9%;
681
+ --primary-foreground: 0 0% 98%;
682
+ /* ... other variables */
683
+ }
684
+ ```
685
+
686
+ ## Development
687
+
688
+ ```bash
689
+ # Install dependencies
690
+ bun install
691
+
692
+ # Run tests
693
+ bun run test
694
+
695
+ # Build library
696
+ bun run build
697
+
698
+ # Run example app
699
+ bun run example:dev
700
+ ```
701
+
702
+ ## License
703
+
704
+ MIT