@skawr/search 0.2.0 → 0.3.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/README.md CHANGED
@@ -39,6 +39,43 @@ results.results.forEach((item) => {
39
39
  });
40
40
  ```
41
41
 
42
+ ### Why a result matched
43
+
44
+ Every result can carry `match_reasons` — structured reasons, not rendered
45
+ phrases, so they survive translation and restyling. `toChips` turns them into
46
+ readable labels plus two style flags, and you draw them however your UI draws
47
+ small tags:
48
+
49
+ ```typescript
50
+ import { toChips } from '@skawr/search';
51
+
52
+ for (const item of results.results) {
53
+ // Pass the query too: the synonym chip names the word the shopper typed.
54
+ for (const chip of toChips(item.match_reasons, undefined, 'phone')) {
55
+ // chip.label — the text
56
+ // chip.tinted — accent treatment (synonym, model-authored reason)
57
+ // chip.soft — the "matched by meaning" chip, which admits no word matched
58
+ }
59
+ }
60
+ ```
61
+
62
+ Pass your own label map as the second argument to localize the computed chips:
63
+
64
+ ```typescript
65
+ toChips(item.match_reasons, {
66
+ reasonLiteral: '{t} · {f}',
67
+ reasonVariant: '{t} · {f} · صيغة الكلمة',
68
+ reasonSynonym: '{t} — مرادف لـ «{q}»',
69
+ reasonVector: 'طابق في المعنى',
70
+ fieldTitle: 'العنوان',
71
+ fieldDescription: 'الوصف',
72
+ }, query);
73
+ ```
74
+
75
+ A reason type the SDK does not recognise renders as nothing rather than as a
76
+ guess — the vocabulary is open and more types may follow, and a guessed chip
77
+ would be the one that is not evidence-backed.
78
+
42
79
  ### Suggestions
43
80
 
44
81
  ```typescript
@@ -65,6 +102,7 @@ console.log(suggestions); // ['laptop', 'laptop case', 'laptop stand']
65
102
  | `publicKey` | `string` | *required* | Your SKAWR public/search key |
66
103
  | `baseUrl` | `string` | `https://api.skawr.com` | API base URL |
67
104
  | `timeout` | `number` | `10000` | Request timeout in ms |
105
+ | `preview` | `boolean` | `false` | Set on a surface that is **not** the merchant's storefront — an onboarding playground, a shared store link, a demo. The API treats the first search from a storefront as the moment that store went live; a preview client does not trigger that. |
68
106
 
69
107
  ## Error Handling
70
108
 
package/dist/index.d.ts CHANGED
@@ -2,6 +2,15 @@ interface SkawrSearchConfig {
2
2
  publicKey: string;
3
3
  baseUrl?: string;
4
4
  timeout?: number;
5
+ /**
6
+ * True when this client does not run on the merchant's own storefront — an
7
+ * onboarding playground, a shared store link, a demo page.
8
+ *
9
+ * The API treats the first search from a storefront as the moment that store
10
+ * went live. Set this and it won't: every request carries `X-Skawr-Preview`.
11
+ * Leave it unset on a real storefront, which is the default.
12
+ */
13
+ preview?: boolean;
5
14
  }
6
15
  interface SearchFilters {
7
16
  min_price?: number;
@@ -18,10 +27,36 @@ interface SearchOptions {
18
27
  per_page?: number;
19
28
  sort_by?: SortBy;
20
29
  highlight_matches?: boolean;
30
+ /**
31
+ * Ask for query suggestions alongside the results, returned as
32
+ * `SearchResponse.suggestions`. Off at the API unless requested, because
33
+ * computing them costs a second pass.
34
+ */
35
+ include_suggestions?: boolean;
21
36
  result_format?: 'standard' | 'minimal' | 'detailed';
22
37
  /** Anonymous shopper ID for personalization. Generated and persisted automatically by the SDK. */
23
38
  anonymous_id?: string;
24
39
  }
40
+ /**
41
+ * Why a result matched, as the API sends it (skawr-search#507 / #522 / #528).
42
+ *
43
+ * Structured, not a rendered phrase: `{type, field, term}` for a computed
44
+ * reason and `{type: "semantic", text}` for the model-authored one. A
45
+ * pre-rendered English string would not survive translation or restyling, so
46
+ * the wording belongs to whoever draws the chip.
47
+ *
48
+ * The vocabulary is open — more types may follow — so a renderer that meets an
49
+ * unfamiliar `type` should draw nothing rather than invent a label. A guessed
50
+ * chip is the one kind that is not evidence-backed, which defeats the point.
51
+ */
52
+ interface MatchReason {
53
+ type: string;
54
+ field?: string;
55
+ term?: string;
56
+ /** Present on `type: "semantic"` — the model's own sentence, shown verbatim. */
57
+ text?: string;
58
+ [key: string]: unknown;
59
+ }
25
60
  interface SearchResult {
26
61
  id: string;
27
62
  title: string;
@@ -36,6 +71,22 @@ interface SearchResult {
36
71
  score?: number;
37
72
  highlighted_title?: string;
38
73
  highlighted_description?: string;
74
+ /**
75
+ * Why this matched. Sent whenever the account has match chips enabled, which
76
+ * is the default in production.
77
+ *
78
+ * It was arriving all along and only reachable through the index signature
79
+ * below — so every consumer that wanted to explain a result had to know the
80
+ * field name and its shape without the SDK saying either. Typing it is what
81
+ * makes "show why it matched" a thing the SDK offers rather than a thing you
82
+ * have to already know about.
83
+ */
84
+ match_reasons?: MatchReason[];
85
+ /** True when the query matched this product's text literally. */
86
+ exact_match?: boolean;
87
+ brand?: string;
88
+ /** Ranking contribution from the shopper's own history, when personalised. */
89
+ personalization_boost?: number;
39
90
  [key: string]: unknown;
40
91
  }
41
92
  interface FacetCount {
@@ -77,6 +128,7 @@ declare class SkawrSearch {
77
128
  private readonly baseUrl;
78
129
  private readonly publicKey;
79
130
  private readonly timeout;
131
+ private readonly preview;
80
132
  constructor(config: SkawrSearchConfig);
81
133
  search(query: string, options?: SearchOptions): Promise<SearchResponse>;
82
134
  suggest(query: string, options?: SuggestOptions): Promise<SuggestionsResponse>;
@@ -92,4 +144,62 @@ declare class SearchError extends Error {
92
144
  constructor(message: string, status: number, detail?: string);
93
145
  }
94
146
 
95
- export { type AutocompleteOptions, type AutocompleteResponse, type Facet, type FacetCount, SearchError, type SearchFilters, type SearchOptions, type SearchResponse, type SearchResult, SkawrSearch, type SkawrSearchConfig, type SortBy, type SuggestOptions, type SuggestionsResponse };
147
+ /**
148
+ * Turning a match reason into a chip a shopper can read
149
+ * (skawr-search#507 / #522 / #528).
150
+ *
151
+ * The API sends `{ type, field, term }` (computed) or `{ type: "semantic", text }`
152
+ * (model-authored) and no display string. That is the right shape — a structured
153
+ * reason survives translation and restyling, a pre-rendered English phrase does
154
+ * not — but it means the wording has to live somewhere. It lives here, in the
155
+ * SDK, so every surface that renders results renders the *same* explanation:
156
+ * the marketplace, the hosted store page, the embeddable widget and anything a
157
+ * customer builds. Before this, the only implementation was inside skawr-web,
158
+ * which is why two of our own surfaces shipped bare grids while the marketplace
159
+ * explained itself (skawr-sdks#12).
160
+ *
161
+ * No DOM, no framework: this returns text and two style flags, and the caller
162
+ * draws them. Callers pass a `Strings` map for the computed labels (localized);
163
+ * `EN_REASON_LABELS` is the default for surfaces without their own i18n.
164
+ *
165
+ * Two rules the copy has to hold:
166
+ *
167
+ * 1. An unknown `type` renders as nothing rather than as something invented.
168
+ * The vocabulary is open on purpose and more types may follow. A chip that
169
+ * guessed at an unfamiliar type would be the one kind of reason that is not
170
+ * evidence-backed, which defeats the point of having them.
171
+ *
172
+ * 2. `vector` says plainly that nothing in the query appeared in the product's
173
+ * text. It is the chip that explains a surprising result, so it must not be
174
+ * dressed up to look as confident as a literal match.
175
+ */
176
+
177
+ interface ReasonChip {
178
+ /** Text to display, already localized. */
179
+ label: string;
180
+ /** True for the reason types that carry the accent treatment. */
181
+ tinted: boolean;
182
+ /** True for `vector`, which is styled apart from the confident types. */
183
+ soft: boolean;
184
+ }
185
+ /**
186
+ * Label templates, keyed by name. Typed loosely on purpose: a caller's i18n
187
+ * module is a `Record<string, string>` and pretending otherwise would only move
188
+ * the looseness out of sight. Missing keys render as an empty span of text
189
+ * rather than throwing — `EN_REASON_LABELS` below names every key in use.
190
+ */
191
+ type Strings = Record<string, string>;
192
+ /** Default English labels, for a surface with no localized `Strings`. */
193
+ declare const EN_REASON_LABELS: Strings;
194
+ /**
195
+ * One chip, or null when the reason cannot be rendered honestly.
196
+ *
197
+ * `query` is needed only by the synonym chip, which names the word the shopper
198
+ * actually typed so the cross-language jump is legible: seeing `جوال` next to
199
+ * a search for `phone` is confusing without it.
200
+ */
201
+ declare function toChip(reason: MatchReason, t?: Strings, query?: string): ReasonChip | null;
202
+ /** Every renderable chip for one result, in the order the API ranked them. */
203
+ declare function toChips(reasons: MatchReason[] | undefined, t?: Strings, query?: string): ReasonChip[];
204
+
205
+ export { type AutocompleteOptions, type AutocompleteResponse, EN_REASON_LABELS, type Facet, type FacetCount, type MatchReason, type ReasonChip, SearchError, type SearchFilters, type SearchOptions, type SearchResponse, type SearchResult, SkawrSearch, type SkawrSearchConfig, type SortBy, type Strings, type SuggestOptions, type SuggestionsResponse, toChip, toChips };
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- var o=class extends Error{status;detail;constructor(t,e,r){super(t),this.name="SearchError",this.status=e,this.detail=r??t;}};var g="https://api.skawr.com",l=1e4,p="skawr_anon_id";function d(){return typeof crypto<"u"&&crypto.randomUUID?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,n=>{let t=Math.random()*16|0;return (n==="x"?t:t&3|8).toString(16)})}function m(){if(!(typeof window>"u"||typeof localStorage>"u"))try{let n=localStorage.getItem(p);return n||(n=d(),localStorage.setItem(p,n)),n}catch{return}}var c=class{baseUrl;publicKey;timeout;constructor(t){if(!t.publicKey)throw new Error("publicKey is required");this.baseUrl=(t.baseUrl??g).replace(/\/+$/,""),this.publicKey=t.publicKey,this.timeout=t.timeout??l;}async search(t,e){let r=e?.anonymous_id??m();return this.post("/api/v1/search",{query:t,filters:e?.filters,page:e?.page,per_page:e?.per_page,sort_by:e?.sort_by,highlight_matches:e?.highlight_matches??true,result_format:e?.result_format,enable_personalization:true,anonymous_id:r})}async suggest(t,e){let r={q:t};return e?.limit!==void 0&&(r.limit=String(e.limit)),e?.include_trending!==void 0&&(r.include_trending=String(e.include_trending)),this.get("/api/v1/search/suggestions",r)}async autocomplete(t,e){let r={q:t};return e?.limit!==void 0&&(r.limit=String(e.limit)),this.get("/api/v1/autocomplete",r)}async get(t,e){let r=`${this.baseUrl}${t}`;if(e){let s=new URLSearchParams(e).toString();s&&(r+=`?${s}`);}return this.request(r,{method:"GET"})}async post(t,e){return this.request(`${this.baseUrl}${t}`,{method:"POST",body:JSON.stringify(e)})}async request(t,e){let r={"X-API-Key":this.publicKey,Accept:"application/json"};e.body&&(r["Content-Type"]="application/json");let s;try{s=await fetch(t,{...e,headers:r,signal:AbortSignal.timeout(this.timeout)});}catch(i){throw i instanceof DOMException&&i.name==="TimeoutError"?new o("Request timed out",0,"Request timed out"):i}if(!s.ok){let i=await s.text(),a;try{let u=JSON.parse(i);a=typeof u.detail=="string"?u.detail:i;}catch{a=i||s.statusText;}throw new o(a,s.status,a)}return await s.json()}};
2
- export{o as SearchError,c as SkawrSearch};//# sourceMappingURL=index.js.map
1
+ var o=class extends Error{status;detail;constructor(e,t,r){super(e),this.name="SearchError",this.status=t,this.detail=r??e;}};var f="https://api.skawr.com",m=1e4,g="skawr_anon_id";function h(){return typeof crypto<"u"&&crypto.randomUUID?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,n=>{let e=Math.random()*16|0;return (n==="x"?e:e&3|8).toString(16)})}function y(){if(!(typeof window>"u"||typeof localStorage>"u"))try{let n=localStorage.getItem(g);return n||(n=h(),localStorage.setItem(g,n)),n}catch{return}}var l=class{baseUrl;publicKey;timeout;preview;constructor(e){if(!e.publicKey)throw new Error("publicKey is required");this.baseUrl=(e.baseUrl??f).replace(/\/+$/,""),this.publicKey=e.publicKey,this.timeout=e.timeout??m,this.preview=e.preview??false;}async search(e,t){let r=t?.anonymous_id??y();return this.post("/api/v1/search",{query:e,filters:t?.filters,page:t?.page,per_page:t?.per_page,sort_by:t?.sort_by,highlight_matches:t?.highlight_matches??true,include_suggestions:t?.include_suggestions,result_format:t?.result_format,enable_personalization:true,anonymous_id:r,shopper_id:r})}async suggest(e,t){let r={q:e};return t?.limit!==void 0&&(r.limit=String(t.limit)),t?.include_trending!==void 0&&(r.include_trending=String(t.include_trending)),this.get("/api/v1/search/suggestions",r)}async autocomplete(e,t){let r={q:e};return t?.limit!==void 0&&(r.limit=String(t.limit)),this.get("/api/v1/autocomplete",r)}async get(e,t){let r=`${this.baseUrl}${e}`;if(t){let s=new URLSearchParams(t).toString();s&&(r+=`?${s}`);}return this.request(r,{method:"GET"})}async post(e,t){return this.request(`${this.baseUrl}${e}`,{method:"POST",body:JSON.stringify(t)})}async request(e,t){let r={"X-API-Key":this.publicKey,Accept:"application/json"};this.preview&&(r["X-Skawr-Preview"]="1"),t.body&&(r["Content-Type"]="application/json");let s;try{s=await fetch(e,{...t,headers:r,signal:AbortSignal.timeout(this.timeout)});}catch(i){throw i instanceof DOMException&&i.name==="TimeoutError"?new o("Request timed out",0,"Request timed out"):i}if(!s.ok){let i=await s.text(),a;try{let p=JSON.parse(i);a=typeof p.detail=="string"?p.detail:i;}catch{a=i||s.statusText;}throw new o(a,s.status,a)}return await s.json()}};var u={reasonLiteral:"{t} \xB7 {f}",reasonVariant:"{t} \xB7 {f} \xB7 word form",reasonSynonym:"{t} \u2014 synonym of \u201C{q}\u201D",reasonVector:"matched by meaning",fieldTitle:"title",fieldDescription:"description"};function x(n,e){return n==="title"?e.fieldTitle:n==="description"?e.fieldDescription:n??""}function c(n,e){return n.replace(/\{(\w)\}/g,(t,r)=>e[r]??"")}function d(n,e=u,t=""){let r=(n.term??"").trim(),s=x(n.field,e);switch(n.type){case "literal":return r?{label:c(e.reasonLiteral,{t:r,f:s}),tinted:false,soft:false}:null;case "variant":return r?{label:c(e.reasonVariant,{t:r,f:s}),tinted:false,soft:false}:null;case "synonym":{if(!r)return null;let i=t.trim().split(/\s+/)[0]??"";return {label:c(e.reasonSynonym,{t:r,q:i}),tinted:true,soft:false}}case "vector":return {label:e.reasonVector,tinted:false,soft:true};case "semantic":{let i=(n.text??"").trim();return i?{label:i,tinted:true,soft:false}:null}default:return null}}function S(n,e=u,t=""){return Array.isArray(n)?n.map(r=>d(r,e,t)).filter(r=>r!==null):[]}
2
+ export{u as EN_REASON_LABELS,o as SearchError,l as SkawrSearch,d as toChip,S as toChips};//# sourceMappingURL=index.js.map
3
3
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/errors.ts","../src/client.ts"],"names":["SearchError","message","status","detail","DEFAULT_BASE_URL","DEFAULT_TIMEOUT","ANON_ID_KEY","generateAnonId","c","r","getOrCreateAnonId","id","SkawrSearch","config","query","options","anonId","params","path","url","qs","body","init","headers","response","error","text","parsed"],"mappings":"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","file":"index.js","sourcesContent":["export class SearchError extends Error {\n readonly status: number;\n readonly detail: string;\n\n constructor(message: string, status: number, detail?: string) {\n super(message);\n this.name = 'SearchError';\n this.status = status;\n this.detail = detail ?? message;\n }\n}\n","import { SearchError } from './errors.js';\nimport type {\n SkawrSearchConfig,\n SearchOptions,\n SearchResponse,\n SuggestOptions,\n SuggestionsResponse,\n AutocompleteOptions,\n AutocompleteResponse,\n} from './types.js';\n\nconst DEFAULT_BASE_URL = 'https://api.skawr.com';\nconst DEFAULT_TIMEOUT = 10_000;\nconst ANON_ID_KEY = 'skawr_anon_id';\n\n/** Generate a UUID v4 using crypto.randomUUID when available, fallback to Math.random. */\nfunction generateAnonId(): string {\n if (typeof crypto !== 'undefined' && crypto.randomUUID) return crypto.randomUUID();\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0;\n const v = c === 'x' ? r : (r & 0x3) | 0x8;\n return v.toString(16);\n });\n}\n\n/** Get or create a persistent anonymous ID for this browser/device. */\nfunction getOrCreateAnonId(): string | undefined {\n if (typeof window === 'undefined' || typeof localStorage === 'undefined') return undefined;\n try {\n let id = localStorage.getItem(ANON_ID_KEY);\n if (!id) {\n id = generateAnonId();\n localStorage.setItem(ANON_ID_KEY, id);\n }\n return id;\n } catch {\n // localStorage disabled (private mode, etc.)\n return undefined;\n }\n}\n\nexport class SkawrSearch {\n private readonly baseUrl: string;\n private readonly publicKey: string;\n private readonly timeout: number;\n\n constructor(config: SkawrSearchConfig) {\n if (!config.publicKey) {\n throw new Error('publicKey is required');\n }\n\n this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, '');\n this.publicKey = config.publicKey;\n this.timeout = config.timeout ?? DEFAULT_TIMEOUT;\n }\n\n async search(query: string, options?: SearchOptions): Promise<SearchResponse> {\n const anonId = options?.anonymous_id ?? getOrCreateAnonId();\n return this.post<SearchResponse>('/api/v1/search', {\n query,\n filters: options?.filters,\n page: options?.page,\n per_page: options?.per_page,\n sort_by: options?.sort_by,\n highlight_matches: options?.highlight_matches ?? true,\n result_format: options?.result_format,\n enable_personalization: true,\n anonymous_id: anonId,\n });\n }\n\n async suggest(query: string, options?: SuggestOptions): Promise<SuggestionsResponse> {\n const params: Record<string, string> = { q: query };\n if (options?.limit !== undefined) params.limit = String(options.limit);\n if (options?.include_trending !== undefined) params.include_trending = String(options.include_trending);\n\n return this.get<SuggestionsResponse>('/api/v1/search/suggestions', params);\n }\n\n async autocomplete(query: string, options?: AutocompleteOptions): Promise<AutocompleteResponse> {\n const params: Record<string, string> = { q: query };\n if (options?.limit !== undefined) params.limit = String(options.limit);\n\n return this.get<AutocompleteResponse>('/api/v1/autocomplete', params);\n }\n\n private async get<T>(path: string, params?: Record<string, string>): Promise<T> {\n let url = `${this.baseUrl}${path}`;\n if (params) {\n const qs = new URLSearchParams(params).toString();\n if (qs) url += `?${qs}`;\n }\n\n return this.request<T>(url, { method: 'GET' });\n }\n\n private async post<T>(path: string, body: unknown): Promise<T> {\n return this.request<T>(`${this.baseUrl}${path}`, {\n method: 'POST',\n body: JSON.stringify(body),\n });\n }\n\n private async request<T>(url: string, init: RequestInit): Promise<T> {\n const headers: Record<string, string> = {\n 'X-API-Key': this.publicKey,\n 'Accept': 'application/json',\n };\n\n if (init.body) {\n headers['Content-Type'] = 'application/json';\n }\n\n let response: Response;\n try {\n response = await fetch(url, {\n ...init,\n headers,\n signal: AbortSignal.timeout(this.timeout),\n });\n } catch (error) {\n if (error instanceof DOMException && error.name === 'TimeoutError') {\n throw new SearchError('Request timed out', 0, 'Request timed out');\n }\n throw error;\n }\n\n if (!response.ok) {\n const text = await response.text();\n let detail: string;\n try {\n const parsed = JSON.parse(text);\n detail = typeof parsed.detail === 'string' ? parsed.detail : text;\n } catch {\n detail = text || response.statusText;\n }\n throw new SearchError(detail, response.status, detail);\n }\n\n return (await response.json()) as T;\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/errors.ts","../src/client.ts","../src/matchReasons.ts"],"names":["SearchError","message","status","detail","DEFAULT_BASE_URL","DEFAULT_TIMEOUT","ANON_ID_KEY","generateAnonId","c","r","getOrCreateAnonId","id","SkawrSearch","config","query","options","anonId","params","path","url","qs","body","init","headers","response","error","text","parsed","EN_REASON_LABELS","fieldLabel","field","t","fill","template","values","_","k","toChip","reason","term","q","toChips","reasons"],"mappings":"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","file":"index.js","sourcesContent":["export class SearchError extends Error {\n readonly status: number;\n readonly detail: string;\n\n constructor(message: string, status: number, detail?: string) {\n super(message);\n this.name = 'SearchError';\n this.status = status;\n this.detail = detail ?? message;\n }\n}\n","import { SearchError } from './errors.js';\nimport type {\n SkawrSearchConfig,\n SearchOptions,\n SearchResponse,\n SuggestOptions,\n SuggestionsResponse,\n AutocompleteOptions,\n AutocompleteResponse,\n} from './types.js';\n\nconst DEFAULT_BASE_URL = 'https://api.skawr.com';\nconst DEFAULT_TIMEOUT = 10_000;\nconst ANON_ID_KEY = 'skawr_anon_id';\n\n/** Generate a UUID v4 using crypto.randomUUID when available, fallback to Math.random. */\nfunction generateAnonId(): string {\n if (typeof crypto !== 'undefined' && crypto.randomUUID) return crypto.randomUUID();\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0;\n const v = c === 'x' ? r : (r & 0x3) | 0x8;\n return v.toString(16);\n });\n}\n\n/** Get or create a persistent anonymous ID for this browser/device. */\nfunction getOrCreateAnonId(): string | undefined {\n if (typeof window === 'undefined' || typeof localStorage === 'undefined') return undefined;\n try {\n let id = localStorage.getItem(ANON_ID_KEY);\n if (!id) {\n id = generateAnonId();\n localStorage.setItem(ANON_ID_KEY, id);\n }\n return id;\n } catch {\n // localStorage disabled (private mode, etc.)\n return undefined;\n }\n}\n\nexport class SkawrSearch {\n private readonly baseUrl: string;\n private readonly publicKey: string;\n private readonly timeout: number;\n private readonly preview: boolean;\n\n constructor(config: SkawrSearchConfig) {\n if (!config.publicKey) {\n throw new Error('publicKey is required');\n }\n\n this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, '');\n this.publicKey = config.publicKey;\n this.timeout = config.timeout ?? DEFAULT_TIMEOUT;\n this.preview = config.preview ?? false;\n }\n\n async search(query: string, options?: SearchOptions): Promise<SearchResponse> {\n const anonId = options?.anonymous_id ?? getOrCreateAnonId();\n return this.post<SearchResponse>('/api/v1/search', {\n query,\n filters: options?.filters,\n page: options?.page,\n per_page: options?.per_page,\n sort_by: options?.sort_by,\n highlight_matches: options?.highlight_matches ?? true,\n include_suggestions: options?.include_suggestions,\n result_format: options?.result_format,\n enable_personalization: true,\n anonymous_id: anonId,\n // Same id, second purpose. The indexer keys its served-set on\n // `shopper_id` to make numbered pages dense and de-duplicated; without it\n // a bare ?page=N takes the legacy grid, where each page blends\n // page-specific text hits with a constant vector set. Measured at ~20% of\n // slots repeating across adjacent pages, which also means products that\n // should have appeared never do (skawr-sdks#11, skawr-search#542).\n //\n // Sent separately from `anonymous_id` rather than reusing it server-side:\n // they mean different things — one identifies a shopper for\n // personalization, the other scopes a pagination session — and collapsing\n // them would tie de-duplication to having personalization enabled.\n shopper_id: anonId,\n });\n }\n\n async suggest(query: string, options?: SuggestOptions): Promise<SuggestionsResponse> {\n const params: Record<string, string> = { q: query };\n if (options?.limit !== undefined) params.limit = String(options.limit);\n if (options?.include_trending !== undefined) params.include_trending = String(options.include_trending);\n\n return this.get<SuggestionsResponse>('/api/v1/search/suggestions', params);\n }\n\n async autocomplete(query: string, options?: AutocompleteOptions): Promise<AutocompleteResponse> {\n const params: Record<string, string> = { q: query };\n if (options?.limit !== undefined) params.limit = String(options.limit);\n\n return this.get<AutocompleteResponse>('/api/v1/autocomplete', params);\n }\n\n private async get<T>(path: string, params?: Record<string, string>): Promise<T> {\n let url = `${this.baseUrl}${path}`;\n if (params) {\n const qs = new URLSearchParams(params).toString();\n if (qs) url += `?${qs}`;\n }\n\n return this.request<T>(url, { method: 'GET' });\n }\n\n private async post<T>(path: string, body: unknown): Promise<T> {\n return this.request<T>(`${this.baseUrl}${path}`, {\n method: 'POST',\n body: JSON.stringify(body),\n });\n }\n\n private async request<T>(url: string, init: RequestInit): Promise<T> {\n const headers: Record<string, string> = {\n 'X-API-Key': this.publicKey,\n 'Accept': 'application/json',\n };\n\n // A preview surface is one that is not the merchant's own storefront: an\n // onboarding playground, a shared store link, a demo. The API records the\n // first search from a real storefront as \"this store is live\"\n // (skawr-search#177/#785), so without this header the first person to open\n // a shared link would flip a merchant's store live on their behalf.\n if (this.preview) {\n headers['X-Skawr-Preview'] = '1';\n }\n\n if (init.body) {\n headers['Content-Type'] = 'application/json';\n }\n\n let response: Response;\n try {\n response = await fetch(url, {\n ...init,\n headers,\n signal: AbortSignal.timeout(this.timeout),\n });\n } catch (error) {\n if (error instanceof DOMException && error.name === 'TimeoutError') {\n throw new SearchError('Request timed out', 0, 'Request timed out');\n }\n throw error;\n }\n\n if (!response.ok) {\n const text = await response.text();\n let detail: string;\n try {\n const parsed = JSON.parse(text);\n detail = typeof parsed.detail === 'string' ? parsed.detail : text;\n } catch {\n detail = text || response.statusText;\n }\n throw new SearchError(detail, response.status, detail);\n }\n\n return (await response.json()) as T;\n }\n}\n","/**\n * Turning a match reason into a chip a shopper can read\n * (skawr-search#507 / #522 / #528).\n *\n * The API sends `{ type, field, term }` (computed) or `{ type: \"semantic\", text }`\n * (model-authored) and no display string. That is the right shape — a structured\n * reason survives translation and restyling, a pre-rendered English phrase does\n * not — but it means the wording has to live somewhere. It lives here, in the\n * SDK, so every surface that renders results renders the *same* explanation:\n * the marketplace, the hosted store page, the embeddable widget and anything a\n * customer builds. Before this, the only implementation was inside skawr-web,\n * which is why two of our own surfaces shipped bare grids while the marketplace\n * explained itself (skawr-sdks#12).\n *\n * No DOM, no framework: this returns text and two style flags, and the caller\n * draws them. Callers pass a `Strings` map for the computed labels (localized);\n * `EN_REASON_LABELS` is the default for surfaces without their own i18n.\n *\n * Two rules the copy has to hold:\n *\n * 1. An unknown `type` renders as nothing rather than as something invented.\n * The vocabulary is open on purpose and more types may follow. A chip that\n * guessed at an unfamiliar type would be the one kind of reason that is not\n * evidence-backed, which defeats the point of having them.\n *\n * 2. `vector` says plainly that nothing in the query appeared in the product's\n * text. It is the chip that explains a surprising result, so it must not be\n * dressed up to look as confident as a literal match.\n */\n\nimport type { MatchReason } from './types.js';\n\nexport interface ReasonChip {\n /** Text to display, already localized. */\n label: string;\n /** True for the reason types that carry the accent treatment. */\n tinted: boolean;\n /** True for `vector`, which is styled apart from the confident types. */\n soft: boolean;\n}\n\n/**\n * Label templates, keyed by name. Typed loosely on purpose: a caller's i18n\n * module is a `Record<string, string>` and pretending otherwise would only move\n * the looseness out of sight. Missing keys render as an empty span of text\n * rather than throwing — `EN_REASON_LABELS` below names every key in use.\n */\nexport type Strings = Record<string, string>;\n\n/** Default English labels, for a surface with no localized `Strings`. */\nexport const EN_REASON_LABELS: Strings = {\n reasonLiteral: '{t} · {f}',\n reasonVariant: '{t} · {f} · word form',\n reasonSynonym: '{t} — synonym of “{q}”',\n reasonVector: 'matched by meaning',\n fieldTitle: 'title',\n fieldDescription: 'description',\n};\n\n/**\n * Field names the API uses, in the reader's language. An unknown field passes\n * through as-is rather than being dropped — a field we have not translated is\n * still more useful shown than hidden.\n */\nfunction fieldLabel(field: string | undefined, t: Strings): string {\n if (field === 'title') return t.fieldTitle;\n if (field === 'description') return t.fieldDescription;\n return field ?? '';\n}\n\nfunction fill(template: string, values: Record<string, string>): string {\n return template.replace(/\\{(\\w)\\}/g, (_, k: string) => values[k] ?? '');\n}\n\n/**\n * One chip, or null when the reason cannot be rendered honestly.\n *\n * `query` is needed only by the synonym chip, which names the word the shopper\n * actually typed so the cross-language jump is legible: seeing `جوال` next to\n * a search for `phone` is confusing without it.\n */\nexport function toChip(\n reason: MatchReason,\n t: Strings = EN_REASON_LABELS,\n query = '',\n): ReasonChip | null {\n const term = (reason.term ?? '').trim();\n const field = fieldLabel(reason.field, t);\n\n switch (reason.type) {\n case 'literal':\n if (!term) return null;\n return { label: fill(t.reasonLiteral, { t: term, f: field }), tinted: false, soft: false };\n\n case 'variant':\n if (!term) return null;\n return { label: fill(t.reasonVariant, { t: term, f: field }), tinted: false, soft: false };\n\n case 'synonym': {\n if (!term) return null;\n // The first word of the query is the one that most likely produced the\n // synonym edge. Imperfect on a multi-word query, and better than naming\n // the whole query, which reads as if all of it matched.\n const q = query.trim().split(/\\s+/)[0] ?? '';\n return { label: fill(t.reasonSynonym, { t: term, q }), tinted: true, soft: false };\n }\n\n case 'vector':\n return { label: t.reasonVector, tinted: false, soft: true };\n\n case 'semantic': {\n // Model-authored reason (skawr-search#522): already a full sentence in the\n // shopper's language, so it is shown verbatim rather than templated. Tinted\n // like the confident types — it is an affirmative \"why this matched\", not\n // the apologetic vector note. Empty text renders nothing rather than a\n // blank chip.\n const text = (reason.text ?? '').trim();\n if (!text) return null;\n return { label: text, tinted: true, soft: false };\n }\n\n default:\n // An unfamiliar type renders as nothing rather than as a guess.\n return null;\n }\n}\n\n/** Every renderable chip for one result, in the order the API ranked them. */\nexport function toChips(\n reasons: MatchReason[] | undefined,\n t: Strings = EN_REASON_LABELS,\n query = '',\n): ReasonChip[] {\n if (!Array.isArray(reasons)) return [];\n return reasons\n .map((r) => toChip(r, t, query))\n .filter((c): c is ReasonChip => c !== null);\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skawr/search",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Official SKAWR browser search SDK — lightweight, search-only client",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",