Skip to main content

React SDK

The @viglet/turing-react-sdk provides headless hooks and unstyled UI components for building custom search experiences on top of the Viglet Turing Semantic Navigation API.

  • Headless architecture: zero built-in styles, full render control via render props
  • Two modes: manual (programmatic) or URL-synced (URL as source of truth)
  • TypeScript-first: full type safety for all hooks, components, and API responses
  • React 18 & 19 compatible

Storybook: Interactive demos and live examples at turing.viglet.org/react-sdk


Installation​

npm install @viglet/turing-react-sdk

Peer dependencies: react (^18 or ^19) and axios (^1.0).


Quick Start​

import { TuringProvider, useTuringSearch } from "@viglet/turing-react-sdk";

function App() {
return (
<TuringProvider config={{ site: "my-site", locale: "en_US" }}>
<SearchPage />
</TuringProvider>
);
}

function SearchPage() {
const { documents, searchQuery, status } = useTuringSearch();

return (
<div>
<input
placeholder="Search..."
onKeyDown={(e) => {
if (e.key === "Enter") searchQuery(e.currentTarget.value);
}}
/>
{status === "loading" && <p>Loading...</p>}
{documents.map((doc) => (
<div key={doc.url}>
<h3>{doc.title}</h3>
<p>{doc.description}</p>
</div>
))}
</div>
);
}

TuringProvider​

The root context provider that makes Turing config and search state available to all hooks.

import { TuringProvider } from "@viglet/turing-react-sdk";

Props​

PropTypeRequiredDescription
configTuringConfigYesSite configuration
urlSync{ searchParams, setSearchParams }NoEnable URL synchronization
childrenReactNodeYesChild components

TuringConfig​

{
site: string; // Turing site name (required)
locale?: string; // Default locale (e.g., "en_US")
sort?: string; // Default sort (e.g., "relevance")
}

URL Sync Mode​

When urlSync is provided, the URL becomes the single source of truth. Changes to the URL trigger automatic searches, and searches update the URL automatically.

import { useSearchParams } from "react-router-dom";

function App() {
const [searchParams, setSearchParams] = useSearchParams();
return (
<TuringProvider
config={{ site: "my-site", locale: "pt_BR" }}
urlSync={{ searchParams, setSearchParams }}
>
<SearchPage />
</TuringProvider>
);
}

Hooks​

useTuringSearch​

Core search hook for manual (non-URL) mode. Manages the full lifecycle: query, results, pagination, facets, locale, and sort.

const {
status, // "idle" | "loading" | "success" | "error"
data, // Raw TurSearchResponse
chat, // AI chat response (null for wildcard queries)
documents, // Resolved documents with mapped default fields
groups, // Resolved groups (when group param used)
error, // Error message
params, // Current search params
structured, // Link-free response (POST-native; authoritative)
request, // The request that will be sent next
search, // Execute with given params
searchRequest, // Execute from an explicit structured request
searchQuery, // New text query (resets page to 1)
toggleFacet, // Toggle a structured facet item, or a result's own facet chip
clearFacet, // Clear one facet
clearAllFilters,
applySpellCheck, // Accept (or refuse) "did you mean"
changeLocale, // Change locale
changeSort, // Change sort
goToPage, // Go to specific page
} = useTuringSearch(initialParams?);

The hook is POST-native by default: it calls the POST-native search endpoint and asks for the link-free response, so structured is authoritative and every action above is a change to request rather than a Turing href to parse. That is what makes a filter value containing &, =, +, #, % or a space safe.

data (the legacy link-bearing body) is null in this mode. Pass { postNative: false } to go back to the GET contract and data, and render its hrefs as real anchors. The deprecated navigate(href) still works on that path but logs a one-time console notice and is scheduled for removal in the next major; the same applies to onNavigate on TuringPagination.

useTuringAutoComplete​

Autocomplete hook that calls the Turing /ac endpoint with built-in debouncing.

const {
suggestions, // string[]: completion suggestions
isLoading, // Fetching state
fetch, // Trigger autocomplete (min 2 chars)
clear, // Clear suggestions
} = useTuringAutoComplete(debounceMs?: number); // default: 300

useTuringFacets​

Provides facet data with toggle/clear actions. Requires urlSync mode.

const {
facetGroups, // Groups with toggle/clear actions
hasSelected, // Any facet selected?
clearAll, // Remove all selections
raw, // Raw API facet data
} = useTuringFacets();

Each facetGroup contains:

{
name: string; // Group identifier
label: string; // Localized label
multivalued: boolean;
hasSelected: boolean;
clear: () => void; // Clear group selections
facets: [{
count: number;
label: string;
selected: boolean;
toggle: () => void; // Toggle this facet
link?: string; // Legacy mode only (see below)
}];
}

link is the server-rendered href for a facet value and exists only in legacy mode (postNative: false). The default POST-native mode returns a link-free response, so it is undefined there: use toggle(), and if you need a crawlable anchor, handle the absence explicitly rather than rendering <a href={facet.link}> (which would link to the current page).

useTuringPagination​

Provides pagination data with navigation helpers. Requires urlSync mode.

const {
pages, // All pages with action helpers
currentPage, // Current page number
pageCount, // Total pages
totalCount, // Total results
hasPages, // Multiple pages?
} = useTuringPagination();

Each page contains:

{
page: number;
label: string; // "1", "2", "Next", etc.
type: string; // PAGE | CURRENT | FIRST | PREVIOUS | NEXT | LAST | ELLIPSIS
isCurrent: boolean;
isEllipsis: boolean;
select: () => void; // Navigate to this page
}

useTuringDocument​

Typed, normalized access to document custom fields, eliminating Array.isArray() checks.

const {
getString, // (field, fallback?) => string
getArray, // (field) => string[]
getNumber, // (field, fallback?) => number
getBoolean, // (field) => boolean
getDate, // (field) => Date | null
getRaw, // <T>(field) => T | undefined
fields, // Direct field access
} = useTuringDocument(document);

useTuringTabs​

Manages tab-based search with filter switching and grouping. Keeps URLs clean using implicit params.

const {
activeIndex, // Active tab index
activeTab, // Active tab definition
tabs, // All tabs with select() action
isGrouped, // Active tab uses grouping?
documents, // Docs for active tab
groups, // Groups (if grouped)
} = useTuringTabs({
attribute: "templateName",
items: [
{ label: "All" },
{ label: "Courses", filter: "templateName:cursos", group: "category" },
{ label: "News", filter: "templateName:noticias", rows: 5 },
],
});

useTuringSortOptions​

Fetches available sort options (built-in + custom from Turing admin console).

const {
sortOptions, // { value: string, label: string }[]
isLoading,
error,
refresh, // Re-fetch options
} = useTuringSortOptions();

useTuringSearchHistory​

Persists search terms in IndexedDB, scoped per site + domain.

const {
history, // Recent terms (newest first, deduplicated)
save, // Save a term
remove, // Remove a term
clear, // Clear all for current site
} = useTuringSearchHistory(maxItems?: number); // default: 50

useTuringUrlSearch​

Returns the shared search store from TuringProvider when urlSync is configured.

const store = useTuringUrlSearch();
// Same shape as TuringProvider's internal store:
// status, structured, data, documents, groups, params, inputValue,
// submitSearch, toggleFacet, goToPage, navigate, setLocale, setSort, showAll, etc.

structured holds the link-free response (the default). data holds the legacy link-bearing one and is null unless you set urlSync.postNative: false. Prefer the derived hooks above (and useSearchView below) over reading either directly.

useSearchView​

Projects whichever response arrived onto one view model, so a component never learns which shape it got. It covers the parts the other hooks do not (the site icon, the locales, the applied-filter row and the spell-check offer) and resolves each action with its state: removing an applied filter subtracts a filter query in the default mode and follows an href on the legacy transport, through the same remove() call.

const view = useSearchView();
if (!view.ready) return <Spinner />; // the render gate, either shape

return (
<>
{view.siteIcon ? <Icon icon={view.siteIcon} /> : <YourLogo />}
{view.appliedFilters.map((filter) => (
<button key={filter.key} onClick={filter.remove}>{filter.label} ✕</button>
))}
{view.spellCheck?.correctedText && (
<button onClick={view.spellCheck.applyCorrection}>
Did you mean {view.spellCheck.correctedTextValue}?
</button>
)}
<FacetRail multiSelect={view.multiSelectFacetItems} />
</>
);
FieldDescription
source"structured" | "legacy" | "none": which shape backed the view
readytrue once a response of either shape arrived
siteIconThe site's Iconify name, or null: fall back to your own logo
locales[{ locale }] the site can be searched in
queryContextCounts, paging and default field names
appliedFilters[{ key, label, remove() }]: the "applied filters" row
spellCheck{ correctedTextValue, applyCorrection(), applyOriginal() } or null
multiSelectFacetItemstrue when the site's facets multi-select (draw checkboxes)

The projection itself is toSearchView(store) in @viglet/turing-sdk, the same function, so a plain-JS host and a React one resolve a response identically. This hook is the React wrapper.


useTuringChat​

Streams a conversation with the site's chat (/sn/{site}/chat/conversation) inside a TuringProvider. It returns messages, status, isStreaming, error and send(text, overrides?), plus stop() and reset().

To answer within the visitor's current search, pass filters (field → values, the shape postNlFilters returns); send's second argument replaces them for one turn. onFilters receives the filters the server read from a list-style question, ahead of the answer:

const { messages, send } = useTuringChat({
filters: { uf: [currentUf] },
onFilters: (parsed) => setSuggestedFq(parsed.fq),
});

await send("Quais são remotas?");
await send("E no Rio?", { filters: { uf: ["RJ"] } }); // this turn only

UI Components​

All components are unstyled (headless). You control the look via render props and CSS.

TuringSearchBar​

Simple search bar with customizable input/button via render props.

<TuringSearchBar
onSearch={(query) => console.log(query)}
placeholder="Search..."
defaultQuery=""
renderInput={(props) => <input {...props} className="my-input" />}
renderButton={({ onClick }) => <button onClick={onClick}>Go</button>}
/>
PropTypeDescription
onSearch(query: string) => voidCallback on Enter or button click
defaultQuerystringPre-fill value
placeholderstringInput placeholder
renderInput(props) => ReactNodeCustom input renderer
renderButton(props) => ReactNodeCustom button renderer

TuringSearchField​

Advanced compound component with autocomplete, search history, and dropdown.

<TuringSearchField turing={urlSearchStore} debounce={300} maxHistory={8}>
<TuringSearchField.Input placeholder="Search..." className="my-input" />
<TuringSearchField.Button className="my-btn">Search</TuringSearchField.Button>
<TuringSearchField.Dropdown
className="my-dropdown"
historyLabel="Recent searches"
clearAllLabel="Clear all"
renderSuggestion={(term, onClick) => (
<div onClick={onClick}>{term}</div>
)}
renderHistory={(term, onClick, onRemove) => (
<div>
<span onClick={onClick}>{term}</span>
<button onClick={onRemove}>x</button>
</div>
)}
/>
</TuringSearchField>

TuringResultList​

Renders search results using the slot/render-prop pattern.

<TuringResultList
documents={documents}
isLoading={status === "loading"}
itemComponent={({ document, index }) => (
<div key={document.url}>
<h3>{document.title}</h3>
<p>{document.description}</p>
</div>
)}
emptyComponent={() => <p>No results found.</p>}
loadingComponent={() => <p>Loading...</p>}
/>
PropTypeDescription
documentsResolvedDocument[]Documents to render
itemComponent(props) => ReactNodeItem renderer (required)
emptyComponent() => ReactNodeNo-results state
loadingComponent() => ReactNodeLoading state
isLoadingbooleanShow loading component
onResultClick(document, index) => voidFires on a row click: wire it to trackResultClick
onToggleFacet(target) => voidToggles a result's facet chip; forwarded to itemComponent
onNavigate(href) => voidLegacy: follows a chip's href; forwarded the same way

TuringDocumentChips​

Renders a result's facet chips (document.raw.metadata) as actions. A chip arrives in one of two shapes (a legacy href or a link-free filterQuery), and this narrows that for you: a filter query becomes an aria-pressed button, an href becomes an anchor that still carries the URL (for middle-click and crawlers) but calls your handler, and a chip you gave no handler for renders as plain text rather than as a control that does nothing.

<TuringResultList
documents={documents}
onToggleFacet={toggleFacet}
itemComponent={({ document, raw, onToggleFacet }) => (
<article>
<h3>{document.title}</h3>
<TuringDocumentChips
chips={raw.metadata}
onToggleFacet={onToggleFacet}
chipClassName="chip"
selectedChipClassName="chip--on"
/>
</article>
)}
/>
PropTypeDescription
chipsTuringDocumentChip[]The document's metadata array, either shape
onToggleFacet(target) => voidOmit it and every chip renders inert
onNavigate(href) => voidLegacy chips only
itemComponent(props) => ReactNodeOwn markup; receives chip, isActionable, isSelected, onClick
chipClassName / selectedChipClassNamestringSkin the default renderer

Also exported from @viglet/turing-react-ui for hosts that consume the headless package directly.

TuringPagination​

Renders pagination items with navigation actions.

<TuringPagination
items={structured?.pagination ?? []}
onSelectPage={(page) => goToPage(page)}
itemComponent={({ item, isCurrent, isEllipsis, onClick, label }) => (
<button
onClick={onClick}
disabled={isCurrent}
style={{ fontWeight: isCurrent ? "bold" : "normal" }}
>
{isEllipsis ? "..." : label}
</button>
)}
/>

Data Types​

ResolvedDocument​

Documents returned by hooks have default fields already mapped:

{
url: string; // Document URL
title: string; // Document title
description: string; // Document description
date: string; // Document date
image: string; // Image URL
text: string; // Full text
raw: TurDocument; // Full document with all custom fields
}

SearchParams​

{
q: string; // Query string
p?: string; // Page number
_setlocale?: string; // Locale
sort?: string; // Sort field
fq?: string[]; // Facet queries (filters)
group?: string; // Group field name
rows?: string; // Results per page
}

Full Example: Search Page with Facets​

import {
TuringProvider,
useTuringUrlSearch,
useTuringFacets,
useTuringPagination,
TuringResultList,
TuringSearchBar,
TuringPagination,
} from "@viglet/turing-react-sdk";
import { useSearchParams } from "react-router-dom";

function App() {
const [searchParams, setSearchParams] = useSearchParams();
return (
<TuringProvider
config={{ site: "my-site", locale: "en_US" }}
urlSync={{ searchParams, setSearchParams }}
>
<SearchPage />
</TuringProvider>
);
}

function SearchPage() {
const store = useTuringUrlSearch();
const { facetGroups, clearAll } = useTuringFacets();
const { pages } = useTuringPagination();

return (
<div>
<TuringSearchBar onSearch={(q) => store.searchQuery(q)} />

<aside>
{facetGroups.map((group) => (
<div key={group.name}>
<h4>{group.label}</h4>
{group.facets.map((facet) => (
<label key={facet.label}>
<input
type="checkbox"
checked={facet.selected}
onChange={facet.toggle}
/>
{facet.label} ({facet.count})
</label>
))}
</div>
))}
</aside>

<main>
<TuringResultList
documents={store.documents}
isLoading={store.status === "loading"}
itemComponent={({ document }) => (
<article>
<h3><a href={document.url}>{document.title}</a></h3>
<p>{document.description}</p>
</article>
)}
emptyComponent={() => <p>No results found.</p>}
/>

<TuringPagination
items={store.structured?.pagination ?? []}
onSelectPage={store.goToPage}
/>
</main>
</div>
);
}


PageDescription
Developer GuideDev environment, stack, contributing
Client ToolsLive tool activity, client tools, generative UI
Reference showcaseAtlas Store: a full app built on this SDK
ChatThe chat surface the hooks drive
REST API ReferenceThe endpoints behind the hooks