@headband/react 0.1.0 → 0.2.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/dist/index.js CHANGED
@@ -32,7 +32,21 @@ __export(index_exports, {
32
32
  SearchBox: () => SearchBox,
33
33
  SortBy: () => SortBy,
34
34
  Stats: () => Stats,
35
+ andFilters: () => andFilters,
36
+ assignExperiment: () => assignExperiment,
37
+ buildExperimentConfigPath: () => buildExperimentConfigPath,
38
+ chooseExperimentVariant: () => chooseExperimentVariant,
39
+ combineFilters: () => combineFilters,
40
+ experimentSearchMetadata: () => experimentSearchMetadata,
41
+ filterEq: () => filterEq,
42
+ filterExists: () => filterExists,
43
+ filterIn: () => filterIn,
44
+ filterNotEq: () => filterNotEq,
45
+ filterRange: () => filterRange,
35
46
  headband: () => headband,
47
+ isRunnableExperiment: () => isRunnableExperiment,
48
+ orFilters: () => orFilters,
49
+ quoteFilterValue: () => quoteFilterValue,
36
50
  useHeadbandContext: () => useHeadbandContext,
37
51
  useHighlight: () => useHighlight,
38
52
  useHits: () => useHits,
@@ -46,6 +60,70 @@ __export(index_exports, {
46
60
  });
47
61
  module.exports = __toCommonJS(index_exports);
48
62
 
63
+ // src/experiments.ts
64
+ function stableHash(input) {
65
+ let hash = 2166136261;
66
+ for (let i = 0; i < input.length; i += 1) {
67
+ hash ^= input.charCodeAt(i);
68
+ hash = Math.imul(hash, 16777619);
69
+ }
70
+ return hash >>> 0;
71
+ }
72
+ function bucket(input, max) {
73
+ if (max <= 0) return 0;
74
+ return stableHash(input) % max;
75
+ }
76
+ function assignmentSeed(config, subjectId, scope) {
77
+ return `${config.assignmentSalt || config.experimentKey}:${config.experimentKey}:${scope}:${subjectId}`;
78
+ }
79
+ function buildExperimentConfigPath(query = {}) {
80
+ const params = new URLSearchParams();
81
+ if (query.projectId) params.set("projectId", query.projectId);
82
+ if (query.indexUid) params.set("indexUid", query.indexUid);
83
+ if (query.experimentKey) params.set("experimentKey", query.experimentKey);
84
+ const search = params.toString();
85
+ return `/v1/experiments/config${search ? `?${search}` : ""}`;
86
+ }
87
+ function isRunnableExperiment(config) {
88
+ return config.status === "active" && !config.killSwitch && config.trafficPercent > 0 && config.variants.length > 0;
89
+ }
90
+ function chooseExperimentVariant(config, subjectId) {
91
+ if (!isRunnableExperiment(config)) return null;
92
+ const trafficBucket = bucket(assignmentSeed(config, subjectId, "traffic"), 1e4) / 100;
93
+ if (trafficBucket >= config.trafficPercent) return null;
94
+ const totalWeight = config.variants.reduce(
95
+ (sum, variant) => sum + Math.max(0, variant.weight),
96
+ 0
97
+ );
98
+ if (totalWeight <= 0) return null;
99
+ const variantBucket = bucket(assignmentSeed(config, subjectId, "variant"), totalWeight);
100
+ let cursor = 0;
101
+ for (const variant of config.variants) {
102
+ cursor += Math.max(0, variant.weight);
103
+ if (variantBucket < cursor) return variant;
104
+ }
105
+ return config.variants[config.variants.length - 1] ?? null;
106
+ }
107
+ function assignExperiment(config, subjectId, options = {}) {
108
+ const variant = chooseExperimentVariant(config, subjectId);
109
+ if (!variant) return null;
110
+ return {
111
+ experimentKey: config.experimentKey,
112
+ variant: variant.key,
113
+ indexUid: variant.indexUid,
114
+ sessionId: options.sessionId ?? subjectId,
115
+ config
116
+ };
117
+ }
118
+ function experimentSearchMetadata(assignment) {
119
+ if (!assignment) return void 0;
120
+ return {
121
+ experimentKey: assignment.experimentKey,
122
+ variant: assignment.variant,
123
+ ...assignment.sessionId ? { sessionId: assignment.sessionId } : {}
124
+ };
125
+ }
126
+
49
127
  // src/client.ts
50
128
  var HeadbandError = class extends Error {
51
129
  constructor(message, status, code) {
@@ -57,22 +135,20 @@ var HeadbandError = class extends Error {
57
135
  };
58
136
  function headband(host, apiKey) {
59
137
  const baseUrl = host.replace(/\/+$/, "");
60
- async function request(path, body) {
138
+ async function request(path, options = {}) {
139
+ const method = options.method ?? "POST";
61
140
  let response;
62
141
  try {
63
142
  response = await fetch(`${baseUrl}${path}`, {
64
- method: "POST",
143
+ method,
65
144
  headers: {
66
- "Content-Type": "application/json",
145
+ ...method === "POST" ? { "Content-Type": "application/json" } : {},
67
146
  Authorization: `Bearer ${apiKey}`
68
147
  },
69
- body: JSON.stringify(body)
148
+ ...method === "POST" ? { body: JSON.stringify(options.body ?? {}) } : {}
70
149
  });
71
150
  } catch (err) {
72
- throw new HeadbandError(
73
- err instanceof Error ? err.message : "Network request failed",
74
- 0
75
- );
151
+ throw new HeadbandError(err instanceof Error ? err.message : "Network request failed", 0);
76
152
  }
77
153
  if (!response.ok) {
78
154
  let errorBody = {};
@@ -91,17 +167,102 @@ function headband(host, apiKey) {
91
167
  return {
92
168
  search(index, query, params) {
93
169
  return request("/v1/search", {
94
- index,
95
- q: query,
96
- ...params
170
+ body: {
171
+ index,
172
+ q: query,
173
+ ...params
174
+ }
97
175
  });
98
176
  },
99
177
  multiSearch(queries) {
100
- return request("/v1/multi-search", { queries });
178
+ return request("/v1/multi-search", { body: { queries } });
179
+ },
180
+ async getExperimentConfigs(query) {
181
+ const response = await request(
182
+ buildExperimentConfigPath(query),
183
+ { method: "GET" }
184
+ );
185
+ return { experiments: response.data?.experiments ?? [] };
186
+ },
187
+ async assignExperiment(query) {
188
+ const response = await request(
189
+ "/v1/experiments/assign",
190
+ { body: query }
191
+ );
192
+ if (!response.data) {
193
+ throw new HeadbandError("Experiment assignment response missing data", 500);
194
+ }
195
+ return response.data;
196
+ },
197
+ async recordExperimentEvent(event) {
198
+ const response = await request("/v1/experiments/events", {
199
+ body: event
200
+ });
201
+ if (!response.data) {
202
+ throw new HeadbandError("Experiment event response missing data", 500);
203
+ }
204
+ return response.data;
205
+ },
206
+ warm(target, opts) {
207
+ return request("/v1/warm", {
208
+ body: { ...target, ...opts?.level ? { level: opts.level } : {} }
209
+ });
101
210
  }
102
211
  };
103
212
  }
104
213
 
214
+ // src/filters.ts
215
+ function quoteFilterValue(value) {
216
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
217
+ return JSON.stringify(value);
218
+ }
219
+ function filterEq(attribute, value) {
220
+ return `${attribute} = ${quoteFilterValue(value)}`;
221
+ }
222
+ function filterNotEq(attribute, value) {
223
+ return `${attribute} != ${quoteFilterValue(value)}`;
224
+ }
225
+ function filterIn(attribute, values) {
226
+ return `${attribute} IN [${values.map(quoteFilterValue).join(", ")}]`;
227
+ }
228
+ function filterExists(attribute) {
229
+ return `${attribute} EXISTS`;
230
+ }
231
+ function filterRange(attribute, range) {
232
+ const parts = [];
233
+ const gte = range.gte ?? range.min;
234
+ const lte = range.lte ?? range.max;
235
+ if (range.gt !== void 0) parts.push(`${attribute} > ${range.gt}`);
236
+ if (gte !== void 0) parts.push(`${attribute} >= ${gte}`);
237
+ if (range.lt !== void 0) parts.push(`${attribute} < ${range.lt}`);
238
+ if (lte !== void 0) parts.push(`${attribute} <= ${lte}`);
239
+ return parts.length ? andFilters(...parts) : void 0;
240
+ }
241
+ function normalizeFilter(input) {
242
+ if (!input) return [];
243
+ return (Array.isArray(input) ? input : [input]).filter((part) => Boolean(part));
244
+ }
245
+ function andFilters(...filters) {
246
+ const parts = filters.flatMap(normalizeFilter);
247
+ if (parts.length === 0) return void 0;
248
+ if (parts.length === 1) return parts[0];
249
+ return parts.map((part) => `(${part})`).join(" AND ");
250
+ }
251
+ function orFilters(...filters) {
252
+ const parts = filters.flatMap(normalizeFilter);
253
+ if (parts.length === 0) return void 0;
254
+ if (parts.length === 1) return parts[0];
255
+ return parts.map((part) => `(${part})`).join(" OR ");
256
+ }
257
+ function combineFilters(baseFilter, generatedFilter) {
258
+ const base = normalizeFilter(baseFilter);
259
+ const generated = normalizeFilter(generatedFilter);
260
+ const combined = [...base, ...generated];
261
+ if (combined.length === 0) return void 0;
262
+ if (combined.length === 1) return combined[0];
263
+ return combined;
264
+ }
265
+
105
266
  // src/provider.tsx
106
267
  var import_react = require("react");
107
268
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -109,9 +270,7 @@ var HeadbandContext = (0, import_react.createContext)(null);
109
270
  function useHeadbandContext() {
110
271
  const ctx = (0, import_react.useContext)(HeadbandContext);
111
272
  if (!ctx) {
112
- throw new Error(
113
- "useHeadbandContext must be used within a <HeadbandProvider>."
114
- );
273
+ throw new Error("useHeadbandContext must be used within a <HeadbandProvider>.");
115
274
  }
116
275
  return ctx;
117
276
  }
@@ -121,18 +280,19 @@ function buildFilterString(refinements, ranges) {
121
280
  if (values.size === 0) return;
122
281
  if (values.size === 1) {
123
282
  const [v] = values;
124
- parts.push(`${attribute} = "${v}"`);
283
+ parts.push(filterEq(attribute, v));
125
284
  } else {
126
- const list = Array.from(values).map((v) => `"${v}"`).join(", ");
127
- parts.push(`${attribute} IN [${list}]`);
285
+ parts.push(filterIn(attribute, Array.from(values)));
128
286
  }
129
287
  });
130
288
  ranges.forEach((range, attribute) => {
131
289
  if (range.min !== void 0) {
132
- parts.push(`${attribute} >= ${range.min}`);
290
+ const generated = filterRange(attribute, { min: range.min });
291
+ if (generated) parts.push(generated);
133
292
  }
134
293
  if (range.max !== void 0) {
135
- parts.push(`${attribute} <= ${range.max}`);
294
+ const generated = filterRange(attribute, { max: range.max });
295
+ if (generated) parts.push(generated);
136
296
  }
137
297
  });
138
298
  return parts.length > 0 ? parts.join(" AND ") : void 0;
@@ -141,90 +301,92 @@ function HeadbandProvider({
141
301
  client,
142
302
  index,
143
303
  initialQuery = "",
304
+ experiment = null,
305
+ baseFilter,
306
+ mode,
144
307
  children
145
308
  }) {
146
309
  const [query, setQueryRaw] = (0, import_react.useState)(initialQuery);
147
310
  const [results, setResults] = (0, import_react.useState)(null);
148
311
  const [isLoading, setIsLoading] = (0, import_react.useState)(false);
149
312
  const [error, setError] = (0, import_react.useState)(null);
150
- const [refinements, setRefinements] = (0, import_react.useState)(
151
- () => /* @__PURE__ */ new Map()
152
- );
313
+ const [refinements, setRefinements] = (0, import_react.useState)(() => /* @__PURE__ */ new Map());
153
314
  const [ranges, setRanges] = (0, import_react.useState)(() => /* @__PURE__ */ new Map());
154
315
  const [sortBy, setSortBy] = (0, import_react.useState)(null);
155
316
  const [page, setPage] = (0, import_react.useState)(0);
156
317
  const [hitsPerPage, setHitsPerPage] = (0, import_react.useState)(20);
157
318
  const [facets, setFacets] = (0, import_react.useState)(() => /* @__PURE__ */ new Set());
158
- const timerRef = (0, import_react.useRef)(null);
159
319
  const requestIdRef = (0, import_react.useRef)(0);
320
+ const generatedFilter = (0, import_react.useMemo)(
321
+ () => buildFilterString(refinements, ranges),
322
+ [refinements, ranges]
323
+ );
324
+ const filter = (0, import_react.useMemo)(
325
+ () => combineFilters(baseFilter, generatedFilter),
326
+ [baseFilter, generatedFilter]
327
+ );
328
+ const facetList = (0, import_react.useMemo)(() => facets.size > 0 ? Array.from(facets) : void 0, [facets]);
329
+ const sortList = (0, import_react.useMemo)(() => sortBy ? [sortBy] : void 0, [sortBy]);
160
330
  const executeSearch = (0, import_react.useCallback)(() => {
161
- const id = ++requestIdRef.current;
331
+ const requestId = ++requestIdRef.current;
162
332
  setIsLoading(true);
163
333
  setError(null);
164
- const filter = buildFilterString(refinements, ranges);
165
- const facetList = facets.size > 0 ? Array.from(facets) : void 0;
166
- const sortList = sortBy ? [sortBy] : void 0;
167
334
  const params = {
168
335
  limit: hitsPerPage,
169
336
  offset: page * hitsPerPage,
170
337
  ...filter ? { filter } : {},
171
338
  ...facetList ? { facets: facetList } : {},
172
339
  ...sortList ? { sort: sortList } : {},
340
+ ...experiment ? experiment : {},
341
+ ...mode ? { mode } : {},
173
342
  attributesToHighlight: ["*"]
174
343
  };
175
344
  client.search(index, query, params).then((res) => {
176
- if (id === requestIdRef.current) {
177
- setResults(res);
178
- setIsLoading(false);
345
+ if (requestId !== requestIdRef.current) {
346
+ return;
179
347
  }
348
+ setResults(res);
349
+ setIsLoading(false);
180
350
  }).catch((err) => {
181
- if (id === requestIdRef.current) {
182
- setError(err instanceof Error ? err : new Error(String(err)));
183
- setIsLoading(false);
351
+ if (requestId !== requestIdRef.current) {
352
+ return;
184
353
  }
354
+ setError(err instanceof Error ? err : new Error(String(err)));
355
+ setIsLoading(false);
185
356
  });
186
- }, [client, index, query, refinements, ranges, sortBy, page, hitsPerPage, facets]);
357
+ }, [client, index, query, filter, facetList, sortList, experiment, mode, page, hitsPerPage]);
187
358
  (0, import_react.useEffect)(() => {
188
- if (timerRef.current) {
189
- clearTimeout(timerRef.current);
190
- }
191
- timerRef.current = setTimeout(executeSearch, 200);
359
+ const timer = setTimeout(executeSearch, 200);
192
360
  return () => {
193
- if (timerRef.current) clearTimeout(timerRef.current);
361
+ clearTimeout(timer);
194
362
  };
195
363
  }, [executeSearch]);
196
364
  const setQuery = (0, import_react.useCallback)((q) => {
197
365
  setQueryRaw(q);
198
366
  setPage(0);
199
367
  }, []);
200
- const toggleRefinement = (0, import_react.useCallback)(
201
- (attribute, value2) => {
202
- setRefinements((prev) => {
203
- const next = new Map(prev);
204
- const current = new Set(next.get(attribute) ?? []);
205
- if (current.has(value2)) {
206
- current.delete(value2);
207
- } else {
208
- current.add(value2);
209
- }
210
- next.set(attribute, current);
211
- return next;
212
- });
213
- setPage(0);
214
- },
215
- []
216
- );
217
- const setRange = (0, import_react.useCallback)(
218
- (attribute, range) => {
219
- setRanges((prev) => {
220
- const next = new Map(prev);
221
- next.set(attribute, range);
222
- return next;
223
- });
224
- setPage(0);
225
- },
226
- []
227
- );
368
+ const toggleRefinement = (0, import_react.useCallback)((attribute, value2) => {
369
+ setRefinements((prev) => {
370
+ const next = new Map(prev);
371
+ const current = new Set(next.get(attribute) ?? []);
372
+ if (current.has(value2)) {
373
+ current.delete(value2);
374
+ } else {
375
+ current.add(value2);
376
+ }
377
+ next.set(attribute, current);
378
+ return next;
379
+ });
380
+ setPage(0);
381
+ }, []);
382
+ const setRange = (0, import_react.useCallback)((attribute, range) => {
383
+ setRanges((prev) => {
384
+ const next = new Map(prev);
385
+ next.set(attribute, range);
386
+ return next;
387
+ });
388
+ setPage(0);
389
+ }, []);
228
390
  const clearRange = (0, import_react.useCallback)((attribute) => {
229
391
  setRanges((prev) => {
230
392
  const next = new Map(prev);
@@ -280,7 +442,18 @@ function HeadbandProvider({
280
442
  unregisterFacet,
281
443
  refresh
282
444
  }),
283
- [setQuery, toggleRefinement, setRange, clearRange, registerFacet, unregisterFacet, refresh]
445
+ [
446
+ setQuery,
447
+ setPage,
448
+ setSortBy,
449
+ setHitsPerPage,
450
+ toggleRefinement,
451
+ setRange,
452
+ clearRange,
453
+ registerFacet,
454
+ unregisterFacet,
455
+ refresh
456
+ ]
284
457
  );
285
458
  const value = (0, import_react.useMemo)(
286
459
  () => ({ client, index, state, actions }),
@@ -342,18 +515,19 @@ function useRefinementList({
342
515
  actions.unregisterFacet(attribute);
343
516
  };
344
517
  }, [attribute, actions]);
345
- const refined = state.refinements.get(attribute) ?? /* @__PURE__ */ new Set();
518
+ const refined = state.refinements.get(attribute);
346
519
  const items = (0, import_react4.useMemo)(() => {
520
+ const activeRefinements = refined ?? /* @__PURE__ */ new Set();
347
521
  const distribution = state.results?.facetDistribution?.[attribute] ?? {};
348
522
  const allValues = /* @__PURE__ */ new Set([
349
523
  ...Object.keys(distribution),
350
- ...refined
524
+ ...activeRefinements
351
525
  ]);
352
526
  let result = Array.from(allValues).map(
353
527
  (value) => ({
354
528
  value,
355
529
  count: distribution[value] ?? 0,
356
- isRefined: refined.has(value)
530
+ isRefined: activeRefinements.has(value)
357
531
  })
358
532
  );
359
533
  if (sortOrder === "alpha") {
@@ -653,7 +827,11 @@ function SearchBox({
653
827
  // src/components/Hits.tsx
654
828
  var import_jsx_runtime3 = require("react/jsx-runtime");
655
829
  function DefaultHit({ hit }) {
656
- const { _formatted, __data, ...rest } = hit;
830
+ const rest = Object.fromEntries(
831
+ Object.entries(hit).filter(
832
+ ([key]) => key !== "_formatted" && key !== "__data"
833
+ )
834
+ );
657
835
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "rounded-lg border border-gray-200 bg-white p-4 text-sm dark:border-gray-700 dark:bg-gray-900", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("pre", { className: "overflow-x-auto whitespace-pre-wrap text-xs text-gray-700 dark:text-gray-300", children: JSON.stringify(rest, null, 2) }) });
658
836
  }
659
837
  function DefaultEmpty() {
@@ -714,7 +892,7 @@ function RefinementList({
714
892
  sortBy
715
893
  });
716
894
  const handleChange = (0, import_react11.useCallback)(
717
- (value) => (_e) => {
895
+ (value) => () => {
718
896
  refine(value);
719
897
  },
720
898
  [refine]
@@ -954,17 +1132,15 @@ function RangeInput({
954
1132
  maxPlaceholder = "Max"
955
1133
  }) {
956
1134
  const { min, max, stats, setRange, clear } = useRange({ attribute });
957
- const [localMin, setLocalMin] = (0, import_react14.useState)(min !== void 0 ? String(min) : "");
958
- const [localMax, setLocalMax] = (0, import_react14.useState)(max !== void 0 ? String(max) : "");
959
- (0, import_react14.useEffect)(() => {
960
- setLocalMin(min !== void 0 ? String(min) : "");
961
- }, [min]);
962
- (0, import_react14.useEffect)(() => {
963
- setLocalMax(max !== void 0 ? String(max) : "");
964
- }, [max]);
1135
+ const defaultMin = min !== void 0 ? String(min) : "";
1136
+ const defaultMax = max !== void 0 ? String(max) : "";
1137
+ const formKey = `${attribute}:${defaultMin}:${defaultMax}`;
965
1138
  const handleSubmit = (0, import_react14.useCallback)(
966
1139
  (e) => {
967
1140
  e.preventDefault();
1141
+ const formData = new FormData(e.currentTarget);
1142
+ const localMin = String(formData.get("min") ?? "");
1143
+ const localMax = String(formData.get("max") ?? "");
968
1144
  const parsedMin = localMin !== "" ? Number(localMin) : void 0;
969
1145
  const parsedMax = localMax !== "" ? Number(localMax) : void 0;
970
1146
  if (parsedMin === void 0 && parsedMax === void 0) {
@@ -976,15 +1152,7 @@ function RangeInput({
976
1152
  });
977
1153
  }
978
1154
  },
979
- [localMin, localMax, setRange, clear]
980
- );
981
- const handleMinChange = (0, import_react14.useCallback)(
982
- (e) => setLocalMin(e.target.value),
983
- []
984
- );
985
- const handleMaxChange = (0, import_react14.useCallback)(
986
- (e) => setLocalMax(e.target.value),
987
- []
1155
+ [setRange, clear]
988
1156
  );
989
1157
  const containerClass = styled ? `space-y-2 ${className ?? ""}`.trim() : className ?? "";
990
1158
  const inputClass = styled ? "w-full rounded border border-gray-200 bg-white px-2.5 py-1.5 text-sm outline-none transition-colors placeholder:text-gray-400 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/20 dark:border-gray-700 dark:bg-gray-900 dark:text-white dark:placeholder:text-gray-500" : "";
@@ -1019,9 +1187,9 @@ function RangeInput({
1019
1187
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1020
1188
  "input",
1021
1189
  {
1190
+ name: "min",
1022
1191
  type: "number",
1023
- value: localMin,
1024
- onChange: handleMinChange,
1192
+ defaultValue: defaultMin,
1025
1193
  placeholder: minPlaceholder,
1026
1194
  "aria-label": `Minimum ${attribute}`,
1027
1195
  className: inputClass
@@ -1038,9 +1206,9 @@ function RangeInput({
1038
1206
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1039
1207
  "input",
1040
1208
  {
1209
+ name: "max",
1041
1210
  type: "number",
1042
- value: localMax,
1043
- onChange: handleMaxChange,
1211
+ defaultValue: defaultMax,
1044
1212
  placeholder: maxPlaceholder,
1045
1213
  "aria-label": `Maximum ${attribute}`,
1046
1214
  className: inputClass
@@ -1049,7 +1217,8 @@ function RangeInput({
1049
1217
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "submit", className: buttonClass, children: "Go" })
1050
1218
  ] })
1051
1219
  ]
1052
- }
1220
+ },
1221
+ formKey
1053
1222
  );
1054
1223
  }
1055
1224
 
@@ -1110,7 +1279,21 @@ function PoweredBy({
1110
1279
  SearchBox,
1111
1280
  SortBy,
1112
1281
  Stats,
1282
+ andFilters,
1283
+ assignExperiment,
1284
+ buildExperimentConfigPath,
1285
+ chooseExperimentVariant,
1286
+ combineFilters,
1287
+ experimentSearchMetadata,
1288
+ filterEq,
1289
+ filterExists,
1290
+ filterIn,
1291
+ filterNotEq,
1292
+ filterRange,
1113
1293
  headband,
1294
+ isRunnableExperiment,
1295
+ orFilters,
1296
+ quoteFilterValue,
1114
1297
  useHeadbandContext,
1115
1298
  useHighlight,
1116
1299
  useHits,