React Antfly
UI components for building search applications with React and Antfly
React Antfly provides declarative React components for building search interfaces powered by Antfly. It offers a simple way to create search applications with faceted search, filters, customizable result displays, and AI-powered answers and chat.
Installation
Install the package along with its peer dependencies. @antfly/components does not bundle @antfly/sdk, react, or react-dom, so install them explicitly:
npm install @antfly/components @antfly/sdk react react-dom
# or
yarn add @antfly/components @antfly/sdk react react-dom
Then import the stylesheet once, at your app's entry point:
import '@antfly/components/styles';
Without it, components render as unstyled markup: autosuggest dropdowns won't position over the input, pagination and facet lists lose their layout, and so on. See Styling for how the shipped stylesheet works and how to override it.
Quick Start
Here's a basic example of a search interface using React Antfly:
import React from 'react';
import '@antfly/components/styles';
import {
Antfly,
QueryBox,
Facet,
Results
} from '@antfly/components';
const MySearchApp = () => (
<Antfly url="http://127.0.0.1:8080/db/v1" table="movies">
<QueryBox id="mainSearch" mode="live" placeholder="Search movies..." />
<Facet id="actors" fields={["actors"]} />
<Facet id="releasedYear" fields={["releasedYear"]} />
<Results
id="results"
searchBoxId="mainSearch"
fields={["title", "description"]}
items={data =>
data.map(item => (
<div key={item._id}>
<h3>{item._source.title}</h3>
<p>{item._source.description}</p>
</div>
))
}
/>
</Antfly>
);
Core Components
Antfly Provider
The <Antfly> component is the root provider that manages the search state and coordinates all child components. It also mounts the internal Listener that batches and runs queries, so you never render Listener yourself.
<Antfly
url="http://127.0.0.1:8080/db/v1"
table="your_table"
headers={{
Authorization: "Bearer your-token"
}}
onChange={(values) => {
// Handle state changes
}}
>
{/* Your search components */}
</Antfly>
Props:
url(string, required): The Antfly API base URL, including the/db/v1path, e.g.http://127.0.0.1:8080/db/v1table(string, required): Default table used by every widget (individual widgets can override it with their owntableprop)headers(object): Custom headers for API requestsonChange(function): Callback fired with the current widget state whenever a query-relevant value changes
QueryBox
The <QueryBox> component provides a unified text input for both search and question-answering interfaces. It supports two modes: live search (updates as you type) and submit mode (updates on form submission).
<QueryBox
id="main"
mode="live"
placeholder="Search..."
onInputChange={(value) => console.log('Input:', value)}
/>
{/* Submit mode for Q&A */}
<QueryBox
id="question"
mode="submit"
placeholder="Ask a question..."
buttonLabel="Ask"
onSubmit={(value) => console.log('Submitted:', value)}
/>
Props:
id(string, required): Unique identifier for the componentmode("live" | "submit"): Search mode (default: "live")"live": Updates results as user types (traditional search)"submit": Requires explicit submission (Q&A, complex queries)
placeholder(string): Input placeholder textbuttonLabel(string): Submit button text (submit mode only, default: "Submit")initialValue(string): Initial search valueautoSubmit(boolean): In submit mode, automatically submit wheninitialValueis set or changes (used to feed a follow-up question back into the box)submitSignal(string | number): Bump this alongsideinitialValueto force a re-submit of the same textclearOnSubmit(boolean): Clear the input after a submit-mode submission (default: false)onSubmit(function): Callback when query is submittedonInputChange(function): Callback when input value changesonEscape(function): Custom escape key handler; returntrueto prevent the default clear behaviorrenderInput(function): Replace the default<input>with a custom component (receivesvalue,onChange,onSubmit,onKeyDown, and more — seeCustomInputProps)children(ReactNode): Child components (e.g., Autosuggest)
Autosuggest
The <Autosuggest> component provides autocomplete suggestions. It can be used standalone or nested inside a <QueryBox> for integrated search-as-you-type.
Standalone usage:
<Autosuggest
fields={["title", "name"]}
returnFields={["title", "description"]}
limit={10}
minChars={2}
renderSuggestion={(hit) => (
<div>
<strong>{hit._source.title}</strong>
<p>{hit._source.description}</p>
</div>
)}
onSuggestionSelect={(hit) => {
console.log('Selected:', hit);
}}
/>
Nested in QueryBox:
<QueryBox id="search" mode="live" placeholder="Search...">
<Autosuggest
fields={["title"]}
returnFields={["title", "description"]}
limit={5}
/>
</QueryBox>
Props:
fields(array): Fields to search for suggestionsreturnFields(array): Fields to return in results (defaults tofields)limit(number): Maximum number of suggestions (default: 10)minChars(number): Minimum characters to trigger suggestions (default: 2)debounceMs(number): Debounce delay before querying (default: 300, 0 to disable)renderSuggestion(function): Custom render function for suggestions (legacy mode, no children)customQuery(function): Custom query transformation function,(value?, fields?) => querysemanticIndexes(array): Vector indexes for semantic searchtable(string): Optional table overridefilterQuery(object): Query to constrain suggestionsexclusionQuery(object): Query to exclude matchesonSuggestionSelect(function): Callback when a suggestion is selected (legacy mode, no children)layout("vertical" | "horizontal" | "grid" | "custom"): Layout mode (default: "vertical")children(ReactNode): Composable children (AutosuggestResults,AutosuggestFacets)
Composable Autosuggest
Passing children switches <Autosuggest> into composable mode: it stops rendering its own list and instead provides context that <AutosuggestResults> and <AutosuggestFacets> consume.
<QueryBox id="search" mode="live">
<Autosuggest
fields={["title", "content"]}
returnFields={["title", "category"]}
layout="horizontal"
>
<AutosuggestResults
limit={5}
renderItem={(hit, index) => (
<div className="suggestion-item">
<strong>{hit._source.title}</strong>
<span className="category">{hit._source.category}</span>
</div>
)}
header={(count) => <h4>{count} results</h4>}
/>
<AutosuggestFacets
field="category"
size={10}
label="Categories"
clickable={true}
onSelect={(facet) => console.log('Selected category:', facet)}
/>
</Autosuggest>
</QueryBox>
AutosuggestResults
Renders the matched documents inside a composable <Autosuggest>. Must be a descendant of <Autosuggest> (it calls useAutosuggestContext internally).
Props:
limit(number): Cap the number of items shownrenderItem(function):(hit, index) => ReactNodeonSelect(function): Called when an item is clicked or selected via Enterfilter(function):(hit) => booleanto exclude itemsclassName,itemClassName,selectedItemClassName(string): CSS hooksheader(ReactNode | function): Static node, or(count) => ReactNodefooter(ReactNode)emptyMessage(ReactNode): Shown when there are no results after filtering
<AutosuggestResults
renderItem={(hit) => <span>{hit._source.title}</span>}
emptyMessage={<em>No matches</em>}
/>
AutosuggestFacets
Renders one aggregation field's top terms inside a composable <Autosuggest>. Add one <AutosuggestFacets field="..."> per field you want to aggregate on.
Props:
field(string, required): Field to aggregatesize(number): Max terms to show (default: 5)label(string): Section labelorder("count" | "term" | "reverse_count" | "reverse_term"): Sort order (default: "count")renderItem(function):(facet, index) => ReactNoderenderSection(function):(field, label, terms) => ReactNode, replaces the whole sectiononSelect(function): Called when a term is clickedclickable(boolean): Whether terms respond to click/Enter (default: true)filter(function):(facet) => booleanclassName,itemClassName,sectionClassName(string): CSS hooksheader(ReactNode | function),footer(ReactNode),emptyMessage(ReactNode)
<AutosuggestFacets field="category" label="Categories" size={5} />
Facet Filters
The <Facet> component creates filterable facets for categorical data.
<Facet
id="category"
fields={["category.keyword"]}
itemsPerBlock={10}
placeholder="Filter categories..."
seeMore="Show more"
/>
Props:
id(string, required): Unique identifierfields(array, required): Fields to create facets fromitemsPerBlock(number): Number of items to show initiallyplaceholder(string): Search placeholder within facetseeMore(string): Text for "see more" button
Custom Facet Rendering
You can customize how facet items are rendered:
<Facet
id="custom"
fields={["category"]}
items={(data, { handleChange, isChecked }) => {
return data.map((item) => (
<label key={item.key}>
<input
type="checkbox"
checked={isChecked(item)}
onChange={() => handleChange(item, !isChecked(item))}
/>
{item.key} ({item.doc_count})
</label>
));
}}
/>
Results Display
The <Results> component displays search results with pagination.
<Results
id="results"
searchBoxId="search"
initialPage={1}
itemsPerPage={20}
items={(data) =>
data.map(({ _source, _score, _id }) => (
<div key={_id}>
<h3>{_source.title}</h3>
<span>Score: {_score}</span>
</div>
))
}
pagination={(total, itemsPerPage, page, setPage) => (
<CustomPagination total={total} itemsPerPage={itemsPerPage} page={page} onChange={setPage} />
)}
/>
Props:
id(string, required): Unique identifiersearchBoxId(string): ID of theQueryBoxthat provides the search value (omit to just consume results from other widgets)fields(array): Fields to search whensearchBoxIdis set and nosemanticIndexesare givensemanticIndexes(array): Vector indexes to searchcustomQuery(function):(query?) => unknown, overrides the default query built fromfieldslimit(number): Retrieval limit for semantic queries (default: 10)itemsPerPage(number): Results per page (default: 10)initialPage(number): Starting page number (default: 1)items(function, required):(data) => ReactNode, render function for result itemspagination(function):(total, itemsPerPage, page, setPage) => ReactNode, replaces the defaultPaginationstats(function):(total) => ReactNode, replaces the default "N results" textonResults(function):(data, total) => void, called whenever new results arrivesort,table,filterQuery,exclusionQuery: query overrides, same shapes as elsewhere in the library
Pagination
The control <Results> renders by default for keyword (non-semantic) result sets. Pass pagination on <Results> to replace it, or render <Pagination> directly for a fully custom results flow.
Props:
onChange(function, required):(page: number) => voidtotal(number, required): Total number of resultsitemsPerPage(number, required)page(number, required): Current page
<Pagination page={page} total={total} itemsPerPage={10} onChange={setPage} />
Active Filters
<ActiveFilters> reads every widget with a non-empty value and lets you clear them:
<ActiveFilters
items={(filters, removeFilter) => (
<div>
{filters.map(filter => (
<span key={filter.key}>
{filter.key}: {filter.value}
<button onClick={() => removeFilter(filter.key)}>×</button>
</span>
))}
</div>
)}
/>
Without an items render prop it falls back to a plain <ul> of key: value pairs with a remove button.
Props:
items(function):(activeFilters, removeFilter) => ReactNode, where each filter is{ key, value }andremoveFilter(key)clears that widget
CustomWidget
Extensibility point for widgets that need direct access to the shared search state without importing useSharedContext themselves. It clones its children and injects ctx (the current shared state) and dispatch as props.
Props:
children(ReactNode, required)
function RawTableLabel({ ctx }) {
return <span>Table: {ctx?.table}</span>;
}
<CustomWidget>
<RawTableLabel />
</CustomWidget>
Calling useSharedContext() directly from a component anywhere under <Antfly> achieves the same thing; CustomWidget exists for components that would rather receive ctx/dispatch as props.
Listener
<Antfly> mounts Listener internally around your tree — you never render it yourself. It watches every registered widget, batches their queries with a 15ms debounce, runs them through multiquery(), and writes results back into the shared state. It's exported for advanced testing and typing scenarios only.
AI Answers (Retrieval Agent)
Antfly's Retrieval Agent handles query classification, retrieval, reasoning, generation, confidence scoring, and follow-up questions in one streamed request. <AnswerResults> is the React component for it.
AnswerResults
<AnswerResults> links to a <QueryBox> (usually mode="submit") and streams the agent's response.
import { QueryBox, AnswerResults } from '@antfly/components';
const generator = {
provider: "ollama",
model: "qwen2.5:7b"
};
<QueryBox id="question" mode="submit" placeholder="Ask a question..." />
<AnswerResults
id="answer"
searchBoxId="question"
generator={generator}
systemPrompt="You are a helpful research assistant."
fields={["content", "title"]}
semanticIndexes={["embeddings"]}
showReasoning={true}
showFollowUpQuestions={true}
/>
Props:
id(string, required): Unique identifiersearchBoxId(string, required): ID of theQueryBoxthat provides the search valuegenerator(object, required): LLM configuration —{ provider: "antfly" | "ollama" | "openai" | "gemini" | "vertex", model, api_key? }agentKnowledge(string): Additional domain context passed to the agentsystemPrompt(string): Custom system prompt for the LLMgenerationContext(string): Additional generation-step contexttable(string): Optional table override (inherits fromQueryBox/Antflyif not specified)filterQuery(object): Query to constrain search resultsexclusionQuery(object): Query to exclude matchesfields(array): Fields to search for contextsemanticIndexes(array): Vector indexes to searchlimit(number): Results limit per search (default: 10)followUpCount(number): Number of follow-up questions to requesteval(object): Inline evaluation config (EvalConfigfrom@antfly/sdk) to score the answer as it streams
Visibility Controls:
showClassification(boolean): Display query classification (default: false)showReasoning(boolean): Display reasoning process (default: false)showFollowUpQuestions(boolean): Display follow-up questions (default: true)showConfidence(boolean): Display confidence assessment (default: false)showHits(boolean): Display search results used for context (default: false)
Custom Renderers:
renderLoading(function): Custom loading staterenderEmpty(function): Custom empty staterenderClassification(function):(data) => ReactNoderenderReasoning(function):(reasoning, isStreaming) => ReactNoderenderAnswer(function):(answer, isStreaming, hits?) => ReactNoderenderConfidence(function):(confidence) => ReactNoderenderFollowUpQuestions(function):(questions) => ReactNoderenderHits(function):(hits) => ReactNoderenderEvalResult(function):(evalResult) => ReactNode
Callbacks:
-
onStreamStart/onStreamEnd(function): Lifecycle callbacks -
onError(function):(error: string) => void -
onClassification,onHit,onGenerationChunk,onConfidence,onFollowup: fine-grained streaming callbacks, one call per event -
children(ReactNode): Rendered inside the same context — this is how<AnswerFeedback>(below) reads the live answer
Example with custom renderers:
<AnswerResults
id="answer"
searchBoxId="question"
generator={generator}
showReasoning={true}
showFollowUpQuestions={true}
renderAnswer={(answer, isStreaming, hits) => (
<div className="custom-answer">
<Streamdown>{answer}</Streamdown>
{isStreaming && <span>Generating...</span>}
</div>
)}
renderFollowUpQuestions={(questions) => (
<div className="follow-up">
<h4>Related Questions</h4>
<ul>
{questions.map((q, idx) => (
<li key={idx}>
<button onClick={() => handleQuestionClick(q)}>{q}</button>
</li>
))}
</ul>
</div>
)}
/>
Custom Answer Rendering
For production applications, we recommend using streamdown.ai for rendering markdown with beautiful, streaming-aware components:
import Streamdown from 'streamdown';
import { replaceCitations } from '@antfly/components';
<AnswerResults
id="answer"
searchBoxId="question"
generator={generator}
renderAnswer={(answer, isStreaming, hits) => {
// Convert citations to numbered links pointing at the hit anchors
const withLinks = replaceCitations(answer, {
renderCitation: (ids, allIds) =>
ids
.map((id) => `[${allIds.indexOf(id) + 1}](#source-${allIds.indexOf(id) + 1})`)
.join(', '),
});
return (
<div className="answer">
<Streamdown>{withLinks}</Streamdown>
{isStreaming && <span className="streaming-indicator">Generating...</span>}
</div>
);
}}
/>
AnswerFeedback
Collect user feedback on AI-generated answers with configurable rating systems. It must be rendered as a child of <AnswerResults> — it reads the live answer via useAnswerResultsContext internally.
import { AnswerFeedback, renderThumbsUpDown } from '@antfly/components';
<AnswerResults id="answer" searchBoxId="question" generator={generator}>
<AnswerFeedback
scale={1}
renderRating={renderThumbsUpDown}
onFeedback={({ feedback, result, query }) => {
// Store feedback
console.log('Rating:', feedback.rating); // 0 or 1
console.log('Query:', query);
console.log('Comment:', feedback.comment);
}}
/>
</AnswerResults>
Built-in Renderers:
import {
renderThumbsUpDown, // Binary: 👍/👎 (scale=1)
renderStars, // 5-star: ⭐⭐⭐⭐⭐ (scale=4)
renderNumeric // Numeric: 0,1,2,3 (any scale)
} from '@antfly/components';
// Thumbs up/down
<AnswerFeedback
scale={1}
renderRating={renderThumbsUpDown}
onFeedback={handleFeedback}
/>
// 5-star rating
<AnswerFeedback
scale={4}
renderRating={renderStars}
onFeedback={handleFeedback}
/>
// Numeric scale (0-3)
<AnswerFeedback
scale={3}
renderRating={(rating, onRate) => renderNumeric(rating, onRate, 3)}
onFeedback={handleFeedback}
/>
Custom Renderer:
const customRender = (currentRating, onRate) => {
const options = [
{ emoji: "😞", label: "Poor", value: 0 },
{ emoji: "🙂", label: "Good", value: 2 },
{ emoji: "🤩", label: "Excellent", value: 4 }
];
return (
<div>
{options.map(opt => (
<button
key={opt.value}
onClick={() => onRate(opt.value)}
className={currentRating === opt.value ? 'active' : ''}
>
{opt.emoji} {opt.label}
</button>
))}
</div>
);
};
<AnswerFeedback
scale={4}
renderRating={customRender}
renderComment={(comment, setComment) => (
<textarea value={comment} onChange={(e) => setComment(e.target.value)} placeholder="Tell us more..." />
)}
onFeedback={handleFeedback}
/>
Props:
scale(number, required): Maximum rating value (e.g., 1 for binary, 4 for 5-star)renderRating(function): Render function(currentRating, onRate) => ReactNoderenderComment(function):(comment, setComment) => ReactNode, shown after a rating is pickedrenderSubmit(function):(onSubmit) => ReactNoderenderSubmitted(function):() => ReactNode, shown after submission instead of the default "Thank you" messageonFeedback(function, required): Callback with{ feedback, result, query, context? }
Feedback Data:
{
feedback: {
rating: number, // 0 to scale
scale: number, // Max value
comment?: string // Optional text, only present if a comment was entered
},
result: RetrievalAgentResult, // from @antfly/sdk — includes generation, hits, followup_questions, etc.
query: string,
context?: {
classification?: { route_type: "question" | "search", confidence: number },
reasoning?: string,
agentKnowledge?: string
}
}
Chat
<ChatBar> wraps the Retrieval Agent for multi-turn conversations, keeping a full turn history instead of a single answer.
ChatBar
import { Antfly, ChatBar } from '@antfly/components';
const generator = { provider: "ollama", model: "gemma3:4b" };
<Antfly url="http://127.0.0.1:8080/db/v1" table="docs">
<ChatBar
id="support-chat"
generator={generator}
semanticIndexes={["body_embeddings"]}
fields={["title", "body"]}
showHits
/>
</Antfly>
<ChatBar> renders a ChatMessages log and a ChatInput composer for you, and accepts all of their props directly (ChatBarProps extends both).
Props:
id(string, required): Unique identifiergenerator(object, required): Same shape asAnswerResults.generatortable,semanticIndexes,fields,filterQuery,exclusionQuery: retrieval overrides, same shapes asAnswerResultsagentKnowledge,systemPrompt,limit,followUpCount: same meaning asAnswerResultsmaxInternalIterations(number): Cap on agentic tool-call iterations per turntools(object):ChatToolsConfigfrom@antfly/sdk, enables agentic tool usesteps(object):RetrievalAgentStepsoverride for full control of the pipelineonStreamStart,onStreamEnd,onError: lifecycle callbackschildren(ReactNode): rendered after the built-in messages/input, inside the same chat context- plus every prop from
ChatMessagesandChatInput
ChatInput
Renders the message composer. Configure it by passing its props directly on <ChatBar>; for a fully custom composer, call useChatContext() yourself instead of rendering <ChatInput>.
Props:
placeholder(string): default"Type a message..."renderInput(function):({ value, onChange, onSubmit, isStreaming, placeholder, abort }) => ReactNode
<ChatBar id="chat" generator={generator} placeholder="Ask about the docs…" />
ChatMessages
Renders the turn log. Configure it via the same-named props on <ChatBar>.
Props:
showHits,showFollowUpQuestions,showConfidence(boolean): visibility toggles, mirroringAnswerResultsrenderUserMessage(function):(message, turn) => ReactNoderenderAssistantMessage(function):(message, isStreaming, turn) => ReactNoderenderHits(function):(hits, turn) => ReactNoderenderFollowUpQuestions(function):(questions, onSelect, turn) => ReactNoderenderClarification(function):(clarification, onRespond, turn) => ReactNode, for agent clarifying questionsrenderConfidence(function):(confidence, turn) => ReactNoderenderStreamingIndicator(function):() => ReactNoderenderError(function):(error, turn) => ReactNode
<ChatBar id="chat" generator={generator} showHits showConfidence />
Citation Utilities
The @antfly/components package exports helper functions for parsing and rendering citations in AI-generated answers. Citations appear in the text as [resource_id 1, 2] or the shorthand [1, 2].
Available Functions:
import {
parseCitations,
replaceCitations,
renderAsMarkdownLinks,
renderAsSequentialLinks,
getCitedDocumentIds,
getCitedResourceIds
} from '@antfly/components';
parseCitations(text: string): Citation[]
Parses inline citations from the text. Each Citation is { originalText, ids, startIndex, endIndex }.
const citations = parseCitations("The system uses consensus [resource_id 1] and replication [2, 3].");
// [
// { originalText: "[resource_id 1]", ids: ["1"], startIndex: 27, endIndex: 42 },
// { originalText: "[2, 3]", ids: ["2", "3"], startIndex: 61, endIndex: 67 }
// ]
replaceCitations(text: string, options: { renderCitation: (ids, allCitationIds) => string })
Replaces every citation in the text with the string your renderCitation function returns.
const formatted = replaceCitations(
"This is a fact [doc123].",
{ renderCitation: (ids) => ids.map((id) => `<sup><a href="#${id}">[${id}]</a></sup>`).join(', ') }
);
renderAsMarkdownLinks(ids: string[]): string
Renders a citation's IDs as markdown links to #hit-<id> anchors. Pair it with replaceCitations:
const withLinks = replaceCitations(answer, {
renderCitation: (ids) => renderAsMarkdownLinks(ids),
});
renderAsSequentialLinks(ids: string[], allCitationIds: string[]): string
Same as above, but numbers citations by first-appearance order instead of using the raw IDs:
const numbered = replaceCitations(answer, {
renderCitation: (ids, allIds) => renderAsSequentialLinks(ids, allIds),
});
// "This is a fact [[1]](#hit-doc123). Another fact [[2]](#hit-doc456)."
getCitedDocumentIds(text: string) / getCitedResourceIds(text: string)
Extract all cited document/resource IDs from the text, in order of first appearance (getCitedDocumentIds is a deprecated alias for getCitedResourceIds). Useful for filtering which source documents to display.
// renderAnswer's third argument is the hit list used to generate the answer,
// so no extra wiring is needed to cross-reference citations against sources.
<AnswerResults
id="answer"
searchBoxId="question"
generator={generator}
renderAnswer={(answer, isStreaming, hits = []) => {
const citedIds = getCitedResourceIds(answer);
const citedHits = hits.filter(hit => citedIds.includes(hit._id));
return (
<div>
<Streamdown>{answer}</Streamdown>
<div className="sources">
<h4>Sources</h4>
{citedHits.map(hit => (
<div key={hit._id} id={hit._id}>{hit._source.title}</div>
))}
</div>
</div>
);
}}
/>
Advanced: Custom Citation Click Handlers
Create interactive citations that scroll to source documents by giving each source card a real id and linking to it:
import Streamdown from 'streamdown';
import { replaceCitations } from '@antfly/components';
<AnswerResults
id="answer"
searchBoxId="question"
generator={generator}
showHits
renderAnswer={(answer, isStreaming, hits) => {
const withClickableCitations = replaceCitations(answer, {
renderCitation: (ids) =>
ids
.map((id) => {
const index = hits.findIndex((h) => h._id === id);
return index >= 0 ? `[${index + 1}](#source-${index + 1})` : `[${id}]`;
})
.join(', '),
});
return (
<div>
<Streamdown>{withClickableCitations}</Streamdown>
<div className="sources">
{hits.map((hit, idx) => (
<div key={hit._id} id={`source-${idx + 1}`}>
<strong>[{idx + 1}]</strong> {hit._source.title}
</div>
))}
</div>
</div>
);
}}
/>
Hooks
React Antfly provides React hooks for common search, chat, and AI-answer functionality.
useSearchHistory
Manage search history with localStorage persistence.
import { useSearchHistory } from '@antfly/components';
function MyComponent() {
const { history, isReady, upsertSearch, clearHistory } = useSearchHistory(10);
// Save a search result (upserts by id)
upsertSearch({
id: "search-123",
query: "how does raft work",
timestamp: Date.now(),
summary: "Raft is a consensus algorithm...",
hits: [],
citations: [{ id: "doc1", score: 0.95 }]
});
// Clear all history
clearHistory();
// Display history
return (
<ul>
{history.map((result) => (
<li key={result.id}>
<strong>{result.query}</strong>
<p>{result.summary}</p>
</li>
))}
</ul>
);
}
Parameters:
maxResults(number): Maximum number of search results to store (default: 10, 0 to disable)
Returns:
history: array ofSearchResult(most recent first)isReady: alwaystrueonce mounted (history loads synchronously from localStorage)upsertSearch: insert or update a result byidsaveSearch: alias forupsertSearchclearHistory: clear all history
useAnswerStream
Stream Retrieval Agent responses with state management, independent of the <QueryBox>/<AnswerResults> widget wiring. This is what <AnswerResults> uses internally.
import { useAnswerStream } from '@antfly/components';
function MyComponent() {
const {
answer,
reasoning,
classification,
hits,
followUpQuestions,
isStreaming,
error,
startStream,
stopStream,
reset
} = useAnswerStream();
// Start streaming
const handleAsk = () => {
startStream({
url: 'http://127.0.0.1:8080/db/v1',
request: {
query: 'how does raft work',
queries: [{
table: 'docs',
semantic_search: 'how does raft work',
fields: ['content'],
indexes: ['embeddings']
}],
generator: {
provider: 'ollama',
model: 'qwen2.5:7b'
},
steps: {
generation: {},
classification: { with_reasoning: true },
followup: {}
}
},
headers: { 'X-API-Key': 'key' }
});
};
return (
<div>
<button onClick={handleAsk}>Ask</button>
{isStreaming && <p>Loading...</p>}
{error && <p>Error: {error.message}</p>}
{answer && <p>{answer}</p>}
{followUpQuestions.map((q, i) => <li key={i}>{q}</li>)}
</div>
);
}
Returns:
answer,reasoning: streaming textclassification: query classification datahits: search result hitsfollowUpQuestions: array of follow-up question stringsisStreaming: boolean streaming statuserror:Errorobject if anystartStream:({ url, request, headers }) => Promise<void>stopStream: abort the current streamreset: reset all state
useChatStream
The lower-level, multi-turn hook behind <ChatBar>. It manages its own turns array and streaming state, so it works outside <Antfly> entirely.
import { useChatStream } from '@antfly/components';
function MyChat() {
const { turns, isStreaming, sendMessage, abort, reset } = useChatStream();
const ask = (text) =>
sendMessage(text, {
url: 'http://127.0.0.1:8080/db/v1',
table: 'docs',
generator: { provider: 'ollama', model: 'gemma3:4b' },
});
return (
<div>
{turns.map((turn) => (
<p key={turn.id}>{turn.assistantMessage}</p>
))}
</div>
);
}
Returns:
turns: array ofChatTurn(userMessage,assistantMessage,hits,followUpQuestions,classification,confidence,clarification,steps,error,isStreaming, ...)isStreaming: whether any turn is currently streamingsendMessage:(text, config: ChatConfig) => Promise<void>abort: abort the current streamreset: clear all turns
useCitations
Parse and render citations in RAG and Retrieval Agent responses.
import { useCitations } from '@antfly/components';
function MyComponent() {
const {
parseCitations,
highlightCitations,
extractCitationUrls,
renderAsMarkdown,
renderAsSequential
} = useCitations();
// Parse citations from answer text
const citations = parseCitations("See docs [resource_id 1, 2]");
// Get IDs of cited resources
const citedIds = extractCitationUrls(answer);
const citedHits = hits.filter(hit => citedIds.includes(hit._id));
// Render with sequential numbering
const formatted = renderAsSequential(['1', '2'], citedIds);
return <div>{formatted}</div>;
}
Returns:
parseCitations: parse citation objects from texthighlightCitations:(text, options: CitationRenderOptions) => stringextractCitationUrls: extract cited document/resource IDsrenderAsMarkdown:(ids) => string, markdown links from raw IDsrenderAsSequential:(ids, allCitationIds) => string, markdown links numbered by first appearance
useSharedContext
The low-level hook powering every widget: returns the [state, dispatch] tuple from the <Antfly> provider. Throws if called outside <Antfly>. This is what CustomWidget wraps.
const [{ url, table, widgets }, dispatch] = useSharedContext();
useAnswerResultsContext
Returns the live AnswerResultsContextValue (query, answer, reasoning, hits, classification, confidence, followUpQuestions, isStreaming, result, evalResult) for a component rendered inside <AnswerResults>. This is how <AnswerFeedback> reads the current answer; call it directly to build custom feedback or citation UI.
useAutosuggestContext
Returns the AutosuggestContextValue (query, results, facetData, selectedIndex, handleSelect, isLoading, registerItem, unregisterItem, fields) for a component rendered inside composable <Autosuggest> children. This is what <AutosuggestResults> and <AutosuggestFacets> use internally.
useChatContext
Returns the ChatContextValue (turns, isStreaming, sendMessage, sendFollowUp, respondToClarification, abort, reset, config) for a component rendered inside <ChatBar>. This is what <ChatInput> and <ChatMessages> use internally.
Advanced Features
Streamdown.ai Integration
For production-quality answer and chat UIs with markdown rendering, we recommend integrating streamdown.ai, which provides streaming-aware markdown rendering optimized for AI-generated content.
Installation:
npm install streamdown
# or
yarn add streamdown
Complete Example:
import React from 'react';
import { QueryBox, AnswerResults, replaceCitations } from '@antfly/components';
import Streamdown from 'streamdown';
const generator = {
provider: "ollama",
model: "gemma3:4b"
};
const AnswerWithStreamdown = () => {
return (
<div>
<QueryBox
id="question"
mode="submit"
placeholder="Ask a question..."
/>
<AnswerResults
id="answer"
searchBoxId="question"
generator={generator}
systemPrompt="You are a helpful assistant. Cite sources using [resource_id] format."
fields={["content", "title"]}
semanticIndexes={["embeddings"]}
showHits
renderAnswer={(answer, isStreaming, hits) => {
// Convert citations to clickable numbered links
const withClickableCitations = replaceCitations(answer, {
renderCitation: (ids) =>
ids
.map((id) => {
const index = hits.findIndex((h) => h._id === id);
return index >= 0 ? `[${index + 1}](#source-${index + 1})` : `[${id}]`;
})
.join(', '),
});
return (
<div className="answer">
{/* Render markdown with streaming support */}
<Streamdown>{withClickableCitations}</Streamdown>
{isStreaming && (
<span className="streaming-indicator">Generating...</span>
)}
{/* Display source documents */}
{!isStreaming && hits.length > 0 && (
<div className="sources">
<h4>Sources</h4>
{hits.map((hit, idx) => (
<div key={hit._id} id={`source-${idx + 1}`} className="source-card">
<div className="source-number">[{idx + 1}]</div>
<div className="source-content">
<h5>{hit._source.title || 'Untitled'}</h5>
<p>{hit._source.content?.substring(0, 200)}...</p>
</div>
</div>
))}
</div>
)}
</div>
);
}}
/>
</div>
);
};
export default AnswerWithStreamdown;
Why Streamdown?
- Streaming-aware: Renders markdown smoothly as tokens arrive
- Syntax highlighting: Built-in code block highlighting
- Customizable: Full control over styling and components
- Production-ready: Battle-tested in production RAG applications
URL State Management
Sync search state with URL parameters for shareable searches:
import {
toUrlQueryString,
fromUrlQueryString
} from '@antfly/components';
const MyApp = () => {
const [queryString, setQueryString] = useState("");
const initialValues = fromUrlQueryString(window.location.search);
return (
<Antfly
url="http://127.0.0.1:8080/db/v1"
table="data"
onChange={(values) => {
const qs = toUrlQueryString(values);
setQueryString(qs);
window.history.pushState({}, '', `?${qs}`);
}}
>
<QueryBox
id="main"
mode="live"
initialValue={initialValues.get("main")}
/>
<Results
id="results"
searchBoxId="main"
initialPage={initialValues.get("resultsPage")}
items={/* ... */}
/>
</Antfly>
);
};
Custom Queries
Implement custom query logic for complex search requirements. customQuery is a prop on <Results> and <Autosuggest>, not on <QueryBox>:
const customQuery = (query, fields) => {
return {
bool: {
should: fields.map(field => ({
match: {
[field]: {
query: query,
boost: field === "title" ? 2 : 1
}
}
}))
}
};
};
<Autosuggest
fields={["title", "content"]}
customQuery={customQuery}
/>
Styling
@antfly/components ships a baseline stylesheet at @antfly/components/styles (dist/components.css). It's not purely cosmetic: it also carries layout-critical CSS such as autosuggest dropdown positioning, pagination and facet list layout, and chat message spacing. Import it once and then override with your own CSS — every element carries a stable react-af-* class name (e.g. react-af-results, react-af-autosuggest-dropdown, react-af-chat-messages) you can target:
import '@antfly/components/styles';
<Results
id="results"
searchBoxId="search"
className="my-results-container"
items={(data) =>
data.map(({ _source, _id }) => (
<div key={_id} className="result-item">
{/* Your styled content */}
</div>
))
}
/>
TypeScript Support
React Antfly includes TypeScript definitions. Import types as needed:
import {
Antfly,
QueryBox,
type FacetProps,
type ResultsProps,
type AnswerResultsProps,
type ChatBarProps
} from '@antfly/components';
const MyFacet: React.FC<Partial<FacetProps>> = (props) => {
return <Facet {...props} />;
};
Complete Example
Here's a complete example of a movie search application:
import React from 'react';
import {
Antfly,
QueryBox,
Autosuggest,
Facet,
Results,
ActiveFilters
} from '@antfly/components';
const MovieSearch = () => {
return (
<Antfly url="http://127.0.0.1:8080/db/v1" table="movies">
<div className="search-container">
<header>
<QueryBox id="search" mode="live" placeholder="Search movies...">
<Autosuggest
fields={["title", "tagline"]}
returnFields={["title", "release_date", "vote_average"]}
limit={5}
/>
</QueryBox>
</header>
<div className="layout">
<aside className="filters">
<h3>Filters</h3>
<Facet
id="genre"
fields={["genres.keyword"]}
placeholder="Filter genres"
/>
<Facet
id="year"
fields={["release_year"]}
placeholder="Filter by year"
/>
<Facet
id="rating"
fields={["vote_average_range"]}
placeholder="Filter by rating"
/>
</aside>
<main className="results">
<ActiveFilters />
<Results
id="results"
searchBoxId="search"
itemsPerPage={20}
items={(data) =>
data.map(({ _source: movie, _id }) => (
<div key={_id} className="movie-card">
<img
src={movie.poster_path}
alt={movie.title}
/>
<div className="movie-info">
<h2>{movie.title}</h2>
<p className="tagline">{movie.tagline}</p>
<p className="overview">{movie.overview}</p>
<div className="meta">
<span>⭐ {movie.vote_average}</span>
<span>{movie.release_date}</span>
</div>
</div>
</div>
))
}
/>
</main>
</div>
</div>
</Antfly>
);
};
export default MovieSearch;
Performance Tips
- Debounce Search Input: The library automatically debounces queries (15ms), but you can add custom debouncing for other operations
- Lazy Load Facets: Load facets on demand for better initial performance
- Virtualize Long Result Lists: Use libraries like
react-windowfor large result sets - Cache Results: The library includes built-in caching, but you can implement additional caching strategies
Browser Support
React Antfly supports all modern browsers, targeting browsers with >0.03% global usage share.
Resources
- GitHub Repository
- NPM Package
- Storybook Examples
- Streamdown.ai - Recommended markdown renderer for AI answers and chat
License
React Antfly is released under the Apache 2.0 License.