@indxsearch/intrface 3.12.0 → 3.13.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.13.0
4
+
5
+ ### Minor Changes
6
+
7
+ - SearchResults takes `showScore`, to show or hide each result's score from code. Given, it wins over the toggle in SearchSettingsPanel; left out, the toggle decides, as before.
8
+
9
+ ### Patch Changes
10
+
11
+ - bac9b80: SearchField has a built-in Clear button. It shows while the field holds text and puts the focus back in the field. Turn it off with `clearable={false}`. SearchInput keeps its own Clear, which also resets the filters.
12
+ - Updated dependencies [ffda712]
13
+ - Updated dependencies
14
+ - Updated dependencies [bac9b80]
15
+ - Updated dependencies
16
+ - Updated dependencies [f1bb38b]
17
+ - @indxsearch/systm@2.21.0
18
+
3
19
  ## 3.12.0
4
20
 
5
21
  ### Minor Changes
@@ -0,0 +1,43 @@
1
+ import { default as React } from 'react';
2
+ import { AskSource, AskStatus, AskFilters } from '../hooks/useAsk';
3
+ export interface AskPanelProps {
4
+ /** The question to answer. A new value asks it and starts a new conversation; null or empty
5
+ * shows nothing. To ask the same text again, give the panel a new `key`. */
6
+ question: string | null;
7
+ /** Lets the visitor ask follow-up questions under the answer. Each is answered with the
8
+ * conversation so far (the server uses the last three turns), and the earlier turns stay on
9
+ * screen above it. */
10
+ followUps?: boolean;
11
+ /** Called when the visitor picks a cited source. Look the record up by its documentKey. */
12
+ onSourceClick?: (source: AskSource) => void;
13
+ /** The label over each answer. Default "Answer". */
14
+ label?: string;
15
+ /** Renders the answer text, for a page with a markdown renderer of its own. `cite(n)` gives the
16
+ * citation mark for source n. Default: intrface's own markdown (paragraphs, headings, lists,
17
+ * code blocks, inline code, bold, italics, links), with [n] as citation marks. */
18
+ renderAnswer?: (answer: string, cite: (n: number) => React.ReactNode) => React.ReactNode;
19
+ /** Shown under the answer as "Ask a follow-up" when `followUps` is off: e.g. back to the
20
+ * search field. With `followUps` on, that button goes to the follow-up field instead. */
21
+ onFollowUp?: () => void;
22
+ /** On a catalogue, called with the filters a description was searched with, as soon as they are
23
+ * known: give them to `useFilterSuggestion().apply` so the page's list shows the same set. */
24
+ onFilters?: (filters: AskFilters) => void;
25
+ /** How a filter reads on its chip. Default: a text value as itself, a yes/no by its field's
26
+ * name, a range by its bounds. */
27
+ filterLabel?: (filter: AskFilters['filters'][number]) => string;
28
+ /** Told when the answer starts and stops streaming, e.g. to show a Stop button elsewhere. */
29
+ onStatusChange?: (status: AskStatus) => void;
30
+ /** Fills the height its container gives it, for a panel beside the results: the conversation
31
+ * scrolls on its own, the follow-up field stays at the bottom, and the view follows an answer
32
+ * as it streams while the visitor is at the end of it. Give the container a height (or make it
33
+ * a flex child that has one). Without it the panel is as tall as its content. */
34
+ fill?: boolean;
35
+ className?: string;
36
+ }
37
+ /**
38
+ * An answer to a question, written from the dataset's records by Search or ask: the question, the
39
+ * answer as it streams, the sources it cites, and what to do when it fails or is limited. With
40
+ * `followUps`, a conversation: a field under the answer asks the next question with the earlier
41
+ * ones as context. Pair it with `SearchInput`'s `onAsk`, or `useSearchOrAsk` (when to offer).
42
+ */
43
+ export declare const AskPanel: React.FC<AskPanelProps>;
@@ -1,5 +1,6 @@
1
1
  import { default as React } from 'react';
2
2
  import { InputSize } from '@indxsearch/systm';
3
+ import { SearchOrAskState } from '../hooks/useSearchOrAsk';
3
4
  export interface SearchInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
4
5
  inputSize?: InputSize;
5
6
  /** One Clear button that resets the query and the filters, shown whenever there is either. */
@@ -8,5 +9,16 @@ export interface SearchInputProps extends React.InputHTMLAttributes<HTMLInputEle
8
9
  /** Chips that say what the search is doing: the number of active filters, "Nearest" when no
9
10
  * result reached the coverage bar, and "No results". */
10
11
  showStatus?: boolean;
12
+ /** Search or ask: when given, and the dataset has it switched on, an offer to ask appears in
13
+ * the chip row while the visitor types something an answer would serve. A strong offer also
14
+ * makes Enter ask. Called with the text to ask; show the answer with `AskPanel`. */
15
+ onAsk?: (text: string) => void;
16
+ /** The offer, when the page runs `useSearchOrAsk` itself (to show it elsewhere too, or to use
17
+ * `describes`): the field then shows this one and does not judge again. */
18
+ searchOrAsk?: SearchOrAskState;
19
+ /** Where the offer to ask shows. `chip` (default): a button in the field's chip row. `below`: a
20
+ * row of its own under the field, as on the Indx docs: a full-size button and an Enter hint.
21
+ * The row keeps its height while Search or ask is on, so an offer appearing moves nothing under it. */
22
+ askOffer?: 'chip' | 'below';
11
23
  }
12
24
  export declare const SearchInput: React.FC<SearchInputProps>;
@@ -1,5 +1,6 @@
1
1
  import { default as React } from 'react';
2
2
  import { SearchResult as SearchResultData } from '../context/SearchContext';
3
+ import { SearchOrAskState } from '../hooks/useSearchOrAsk';
3
4
  export interface SearchResultsProps {
4
5
  fields?: string[];
5
6
  resultsPerPage?: number;
@@ -16,6 +17,17 @@ export interface SearchResultsProps {
16
17
  * result, so the dataset's statistics get click-through and click position.
17
18
  */
18
19
  children: (item: Record<string, any>, meta: SearchResultMeta) => React.ReactNode;
20
+ /**
21
+ * The offer, when the page runs `useSearchOrAsk`: while it is strong and the dataset has the
22
+ * rule on, the list shows coverage hits only (up to the truncation point), and nothing when
23
+ * nothing is confirmed, so the offer stands alone. Without it the list is never cut.
24
+ */
25
+ searchOrAsk?: SearchOrAskState;
26
+ /**
27
+ * Shows each result's score beside it. Given, it wins over the `showScore` search setting (the
28
+ * toggle in SearchSettingsPanel); left out, that setting decides, and it is on by default.
29
+ */
30
+ showScore?: boolean;
19
31
  }
20
32
  export interface SearchResultMeta {
21
33
  result: SearchResultData;
@@ -63,6 +63,11 @@ export interface SearchContextType {
63
63
  authErrorInfo?: SearchErrorInfo;
64
64
  retry?: () => void;
65
65
  allowEmptySearch: boolean;
66
+ facetsEnabled: boolean;
67
+ datasetBounds: Record<string, {
68
+ min: number;
69
+ max: number;
70
+ }>;
66
71
  url: string;
67
72
  team: string;
68
73
  dataset: string;
@@ -82,7 +87,19 @@ export interface SearchContextType {
82
87
  fetchMoreResults: (newMax: number) => void;
83
88
  selectResult?: (result: SearchResult) => Promise<void>;
84
89
  sessionId?: string;
90
+ source?: string;
91
+ count?: boolean;
92
+ /** What the search runs on: `state.query`, or what `searchTextOf` or a registered rewrite made of it. */
93
+ searchText?: string;
94
+ /** Searches the typed text as it is, this once: the way back from a rewrite ("Search for your exact words"). */
95
+ searchAsTyped?: () => void;
96
+ /** Registers what the search runs on for what is typed, for a hook inside the provider
97
+ * (useSearchOrAsk's Ask phrases); null takes it off. The provider's own `searchTextOf` wins. */
98
+ setSearchRewrite?: (rewrite: ((typed: string) => string) | null) => void;
85
99
  }
100
+ /** The field's placeholder unless the page sets one. SearchInput says "Search or ask" instead
101
+ * while asking is on, so it must know this one from a page's own. */
102
+ export declare const DEFAULT_PLACEHOLDER = "Type to search";
86
103
  export declare const SearchContext: React.Context<SearchContextType | undefined>;
87
104
  export declare const SearchProvider: React.FC<{
88
105
  children: React.ReactNode;
@@ -101,5 +118,10 @@ export declare const SearchProvider: React.FC<{
101
118
  preAuthenticatedToken?: string;
102
119
  source?: string;
103
120
  count?: boolean;
121
+ /** The text to search for what is typed, when it is not the typed text itself: the field keeps
122
+ * showing what the visitor typed, and the search runs on this. Search or ask's Ask phrases use
123
+ * it ("help me find the movie about the green ogre" searches "green ogre"). Called on every
124
+ * render with the typed text; return it unchanged to search it as typed. */
125
+ searchTextOf?: (typed: string) => string;
104
126
  }>;
105
127
  export declare const useSearchContext: () => SearchContextType;
@@ -15,9 +15,11 @@ export interface UseSearchExecutionOptions {
15
15
  sessionId: string;
16
16
  source?: string;
17
17
  count: boolean;
18
+ /** The text to search for what is typed (SearchProvider's `searchTextOf`). Absent: the typed text. */
19
+ searchTextOf?: (typed: string) => string;
18
20
  }
19
21
  export interface UseSearchExecutionResult {
20
22
  /** Runs the last search again, after a failure. */
21
23
  retrySearch: () => void;
22
24
  }
23
- export declare function useSearchExecution({ state, setState, authenticatedFetch, auth, url, team, dataset, allowEmptySearch, facetsEnabled, enableDebugLogs, shouldFetchMore, sessionId, source, count, }: UseSearchExecutionOptions): UseSearchExecutionResult;
25
+ export declare function useSearchExecution({ state, setState, authenticatedFetch, auth, url, team, dataset, allowEmptySearch, facetsEnabled, enableDebugLogs, shouldFetchMore, sessionId, source, count, searchTextOf, }: UseSearchExecutionOptions): UseSearchExecutionResult;
@@ -0,0 +1,89 @@
1
+ /** A record the answer may use, numbered as the answer cites it ([n]). */
2
+ export interface AskSource {
3
+ n: number;
4
+ documentKey: number;
5
+ title: string;
6
+ }
7
+ /** An earlier question and its answer, sent with a follow-up so the answer can build on it. */
8
+ export interface AskTurn {
9
+ question: string;
10
+ answer: string;
11
+ /** On a catalogue, the list that turn put on the page: a follow-up is about it. */
12
+ shown?: {
13
+ filters: AskFilters['filters'];
14
+ text: string;
15
+ sort?: AskFilters['sort'];
16
+ };
17
+ }
18
+ export type AskStatus = 'idle' | 'searching' | 'writing' | 'answered' | 'error' | 'limited';
19
+ export interface AskState {
20
+ question: string;
21
+ status: AskStatus;
22
+ /** The answer as written so far, citing sources as [n]. */
23
+ answer: string;
24
+ sources: AskSource[];
25
+ /** Why it failed, when status is 'error': the server's code (nothingFound, upstreamBusy, failed,
26
+ * or an HTTP problem code) and a sentence for the page. */
27
+ error?: {
28
+ code: string;
29
+ message: string;
30
+ };
31
+ /** When status is 'limited': seconds until asking again may succeed. */
32
+ retryAfterSeconds?: number;
33
+ /** The server's id for this ask (Indx-Ask-Id). A source opened is reported against it. */
34
+ askId?: string;
35
+ /** On a catalogue, a description's filters, as the server searched with them, and what it found. */
36
+ filters?: AskFilters;
37
+ /** What the agent has done so far ("Searching: Folkehøyskole, by Elevtall"), while it works. */
38
+ steps: string[];
39
+ }
40
+ /** What a catalogue's description was searched with: filters, words no filter covers, and the count. */
41
+ export interface AskFilters {
42
+ filters: {
43
+ field: string;
44
+ value?: string | null;
45
+ min?: number | null;
46
+ max?: number | null;
47
+ }[];
48
+ text: string;
49
+ /** An order the description asked for ("the largest"): a Sortable field. */
50
+ sort?: {
51
+ field: string;
52
+ descending: boolean;
53
+ } | null;
54
+ found: number;
55
+ /** True when the count is a cap: there are more. */
56
+ more: boolean;
57
+ }
58
+ /**
59
+ * Asks the dataset a question (`search-or-ask/ask`) and follows the answer as it streams: the
60
+ * sources first, then the text, then done. One question at a time; asking again, or `stop`, ends
61
+ * the one in flight and keeps what had arrived. The answer uses the dataset's records and nothing
62
+ * else, and says so when they do not hold it.
63
+ */
64
+ export declare function useAsk(): {
65
+ ask: (question: string, history?: AskTurn[]) => Promise<void>;
66
+ stop: () => void | undefined;
67
+ reset: () => void;
68
+ openSource: (source: AskSource, ofAsk?: string) => Promise<void>;
69
+ busy: boolean;
70
+ question: string;
71
+ status: AskStatus;
72
+ /** The answer as written so far, citing sources as [n]. */
73
+ answer: string;
74
+ sources: AskSource[];
75
+ /** Why it failed, when status is 'error': the server's code (nothingFound, upstreamBusy, failed,
76
+ * or an HTTP problem code) and a sentence for the page. */
77
+ error?: {
78
+ code: string;
79
+ message: string;
80
+ };
81
+ /** When status is 'limited': seconds until asking again may succeed. */
82
+ retryAfterSeconds?: number;
83
+ /** The server's id for this ask (Indx-Ask-Id). A source opened is reported against it. */
84
+ askId?: string;
85
+ /** On a catalogue, a description's filters, as the server searched with them, and what it found. */
86
+ filters?: AskFilters;
87
+ /** What the agent has done so far ("Searching: Folkehøyskole, by Elevtall"), while it works. */
88
+ steps: string[];
89
+ };
@@ -0,0 +1,42 @@
1
+ import { SearchOrAskState } from './useSearchOrAsk';
2
+ /** One filter a description asked for: a value (text, or "true"/"false") or a number range. */
3
+ export interface FilterSuggestion {
4
+ field: string;
5
+ value?: string | null;
6
+ min?: number | null;
7
+ max?: number | null;
8
+ }
9
+ export interface FilterSuggestionResult {
10
+ filters: FilterSuggestion[];
11
+ /** What no filter covers, to search for; empty when the filters cover it all. */
12
+ text: string;
13
+ /** An order the description asked for, on a Sortable field. */
14
+ sort?: {
15
+ field: string;
16
+ descending: boolean;
17
+ } | null;
18
+ }
19
+ export declare function localFilters(text: string, facets: Record<string, {
20
+ key: string;
21
+ }[]> | null | undefined): FilterSuggestionResult | null;
22
+ export interface UseFilterSuggestionOptions {
23
+ /** The offer from `useSearchOrAsk`. With it, a description is turned into filters while the
24
+ * visitor types, the way the dataset's `filtersWhileTyping` says: facet values at once, the
25
+ * model for the rest, or nothing until Ask. */
26
+ auto?: SearchOrAskState;
27
+ }
28
+ /**
29
+ * Search or ask on a catalogue: turns a description of what to list ("private videregående i
30
+ * bergen") into filters on the dataset's own fields (`search-or-ask/filters`), and applies them
31
+ * to the search, where ActiveFiltersPanel shows them and the visitor can remove each. Ask for it
32
+ * when `useSearchOrAsk` says `describes`, or hand it the offer (`auto`) and it acts on its own.
33
+ */
34
+ export declare function useFilterSuggestion(options?: UseFilterSuggestionOptions): {
35
+ suggestion: FilterSuggestionResult | null;
36
+ forText: string;
37
+ loading: boolean;
38
+ suggest: (text: string) => Promise<void>;
39
+ apply: (result: FilterSuggestionResult) => void;
40
+ clear: () => void;
41
+ appliedText: string | null;
42
+ };
@@ -0,0 +1,52 @@
1
+ import { CutRule, Offer } from '../utils/searchOrAsk';
2
+ export interface UseSearchOrAskOptions {
3
+ /** false keeps the hook quiet: no settings read, no judgement. Default true. */
4
+ enabled?: boolean;
5
+ /** The text to judge. Defaults to the search query of the surrounding SearchProvider. */
6
+ text?: string;
7
+ /** How long the visitor must pause before the server is asked. Default 300 ms. */
8
+ pauseMs?: number;
9
+ }
10
+ export interface SearchOrAskState {
11
+ /** True once the dataset's settings say Search or ask is on and the instance can run it. Until
12
+ * then, and for good when it is off, the offer stays 'none' and nothing is sent. */
13
+ available: boolean;
14
+ /** What to show: nothing, a quiet "ask about this", or a strong offer where Enter asks. */
15
+ offer: Offer;
16
+ /** The latest probability from the server, for a readout; null before the first judgement. */
17
+ p: number | null;
18
+ /** The judge's reading of whether the top results could answer what is typed; null when it did not say. */
19
+ answerable: number | null;
20
+ /** The text the offer is about: what would be asked. */
21
+ text: string;
22
+ /** On a catalogue: the probability the visitor describes what to list. Null elsewhere. */
23
+ criteria: number | null;
24
+ /** On a catalogue: the text describes what to list, so it can become filters (useFilterSuggestion). */
25
+ describes: boolean;
26
+ /** The list should show coverage hits only: the offer is strong and the dataset has the rule on
27
+ * (`cutWhileAsking`). SearchResults applies it when handed this state; `visibleResults` is the rule. */
28
+ cutResults: boolean;
29
+ /** The cut's two lines, the dataset's, for `visibleResults`. */
30
+ cutRule: CutRule;
31
+ /** The page's searches run with whole-word coverage: the offer is strong and the dataset has
32
+ * that rule on. The hook sets and clears it on the search context itself. */
33
+ strictCoverage: boolean;
34
+ /** On a catalogue, what to do with a description while typing (the dataset's `filtersWhileTyping`):
35
+ * 'off' leaves the list until Ask, 'facets' applies typed facet values at once, 'model' also asks
36
+ * the filters route for the rest. `useFilterSuggestion({ auto })` acts on it. */
37
+ filtersWhileTyping: 'off' | 'facets' | 'model';
38
+ /** The offer is for what is in the field now: the field still extends the text it was judged on.
39
+ * Typing on from a question keeps it (the hook does not ask again while the text only grows);
40
+ * backspacing into it, or changing it, does not, until the judge has read the new text. Show an
41
+ * offer only while this holds, or it blinks out on every keystroke or lingers after a change. */
42
+ stands: boolean;
43
+ }
44
+ /**
45
+ * Decides, while the visitor types, whether to offer an answer beside the results. Reads the
46
+ * dataset's Search or ask settings once, then on each typing pause asks the server's judge
47
+ * (`search-or-ask/intent`) when the rules allow, keeps only the newest answer, and applies the
48
+ * dataset's thresholds with hysteresis. It never asks again while the text only grows and the
49
+ * offer is already strong. A failure of any kind leaves the offer at 'none': the search itself is
50
+ * never affected.
51
+ */
52
+ export declare function useSearchOrAsk(options?: UseSearchOrAskOptions): SearchOrAskState;