@huskel/sdk 0.3.1 → 0.3.3
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.d.mts +56 -3
- package/dist/index.d.ts +56 -3
- package/dist/index.js +307 -21
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +303 -19
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -53,6 +53,7 @@ interface HuskelConfig {
|
|
|
53
53
|
siteId?: string;
|
|
54
54
|
apiUrl?: string;
|
|
55
55
|
apiToken?: string;
|
|
56
|
+
shopperId?: string;
|
|
56
57
|
}
|
|
57
58
|
interface SearchRequest {
|
|
58
59
|
query: string;
|
|
@@ -82,11 +83,21 @@ declare class HuskelAPI {
|
|
|
82
83
|
private apiUrl;
|
|
83
84
|
private siteId;
|
|
84
85
|
private apiToken;
|
|
85
|
-
|
|
86
|
+
private getShopperId?;
|
|
87
|
+
private getSessionId?;
|
|
88
|
+
constructor(apiUrl: string, siteId: string, apiToken: string, getShopperId?: (() => string | undefined) | undefined, getSessionId?: (() => string | undefined) | undefined);
|
|
86
89
|
private post;
|
|
87
90
|
ingest(product: Product): Promise<IngestResponse>;
|
|
88
91
|
ingestBatch(products: Product[]): Promise<IngestResponse>;
|
|
89
92
|
search(query: string, limit?: number): Promise<SearchResponse>;
|
|
93
|
+
searchVector(query: string, limit?: number): Promise<SearchResponse>;
|
|
94
|
+
chat(query: string, history?: Array<{
|
|
95
|
+
role: 'user' | 'assistant';
|
|
96
|
+
content: string;
|
|
97
|
+
}>): Promise<{
|
|
98
|
+
answer: string;
|
|
99
|
+
sources: any[];
|
|
100
|
+
}>;
|
|
90
101
|
}
|
|
91
102
|
|
|
92
103
|
declare class HuskelClient {
|
|
@@ -95,7 +106,17 @@ declare class HuskelClient {
|
|
|
95
106
|
private ingestTimer;
|
|
96
107
|
private ingestedUrls;
|
|
97
108
|
private onlineHandler;
|
|
109
|
+
private shopperId?;
|
|
110
|
+
private sessionId;
|
|
111
|
+
private static INGEST_CACHE_KEY;
|
|
112
|
+
private static INGEST_CACHE_TTL;
|
|
113
|
+
private loadIngestedCache;
|
|
114
|
+
private saveIngestedCache;
|
|
98
115
|
constructor(config: HuskelConfig);
|
|
116
|
+
setShopperId(id: string | undefined): void;
|
|
117
|
+
getShopperId(): string | undefined;
|
|
118
|
+
getSessionId(): string;
|
|
119
|
+
private initSession;
|
|
99
120
|
destroy(): void;
|
|
100
121
|
queueIngest(rawProduct: RawProductInput): Promise<void>;
|
|
101
122
|
queueIngestBatch(rawProducts: RawProductInput[]): Promise<void>;
|
|
@@ -149,6 +170,29 @@ declare function useIngest(): UseIngestReturn;
|
|
|
149
170
|
*/
|
|
150
171
|
declare function usePageIngest(product: RawProductInput | null | undefined): void;
|
|
151
172
|
|
|
173
|
+
interface ChatMessage {
|
|
174
|
+
role: 'user' | 'assistant';
|
|
175
|
+
content: string;
|
|
176
|
+
}
|
|
177
|
+
interface ChatSource {
|
|
178
|
+
id?: string;
|
|
179
|
+
name: string;
|
|
180
|
+
price?: string;
|
|
181
|
+
currency?: string;
|
|
182
|
+
category?: string;
|
|
183
|
+
url?: string;
|
|
184
|
+
image?: string;
|
|
185
|
+
}
|
|
186
|
+
interface UseChatReturn {
|
|
187
|
+
messages: ChatMessage[];
|
|
188
|
+
sources: ChatSource[];
|
|
189
|
+
loading: boolean;
|
|
190
|
+
error: string | null;
|
|
191
|
+
send: (query: string) => Promise<void>;
|
|
192
|
+
reset: () => void;
|
|
193
|
+
}
|
|
194
|
+
declare function useChat(): UseChatReturn;
|
|
195
|
+
|
|
152
196
|
interface SearchBarProps {
|
|
153
197
|
placeholder?: string;
|
|
154
198
|
limit?: number;
|
|
@@ -169,9 +213,18 @@ interface SparkleProps {
|
|
|
169
213
|
}
|
|
170
214
|
declare function Sparkle({ productName, limit, onResult, className }: SparkleProps): react_jsx_runtime.JSX.Element;
|
|
171
215
|
|
|
216
|
+
interface ChatWidgetProps {
|
|
217
|
+
placeholder?: string;
|
|
218
|
+
title?: string;
|
|
219
|
+
className?: string;
|
|
220
|
+
/** Called when user clicks a product link */
|
|
221
|
+
onSelectSource?: (source: ChatSource) => void;
|
|
222
|
+
}
|
|
223
|
+
declare function ChatWidget({ placeholder, title, className, onSelectSource }: ChatWidgetProps): react_jsx_runtime.JSX.Element;
|
|
224
|
+
|
|
172
225
|
interface HuskelProviderProps extends HuskelConfig {
|
|
173
226
|
children: React.ReactNode;
|
|
174
227
|
}
|
|
175
|
-
declare function HuskelProvider({ siteId, apiUrl, apiToken, children }: HuskelProviderProps): react_jsx_runtime.JSX.Element;
|
|
228
|
+
declare function HuskelProvider({ siteId, apiUrl, apiToken, shopperId, children }: HuskelProviderProps): react_jsx_runtime.JSX.Element;
|
|
176
229
|
|
|
177
|
-
export { HuskelAPI, HuskelClient, type HuskelConfig, type HuskelError, HuskelProvider, type IngestResponse, type Product, type RawProductInput, SearchBar, type SearchRequest, type SearchResponse, type SearchResult, Sparkle, getHuskelClient, initHuskel, useHuskel, useIngest, usePageIngest, useSearch };
|
|
230
|
+
export { type ChatMessage, type ChatSource, ChatWidget, HuskelAPI, HuskelClient, type HuskelConfig, type HuskelError, HuskelProvider, type IngestResponse, type Product, type RawProductInput, SearchBar, type SearchRequest, type SearchResponse, type SearchResult, Sparkle, getHuskelClient, initHuskel, useChat, useHuskel, useIngest, usePageIngest, useSearch };
|
package/dist/index.d.ts
CHANGED
|
@@ -53,6 +53,7 @@ interface HuskelConfig {
|
|
|
53
53
|
siteId?: string;
|
|
54
54
|
apiUrl?: string;
|
|
55
55
|
apiToken?: string;
|
|
56
|
+
shopperId?: string;
|
|
56
57
|
}
|
|
57
58
|
interface SearchRequest {
|
|
58
59
|
query: string;
|
|
@@ -82,11 +83,21 @@ declare class HuskelAPI {
|
|
|
82
83
|
private apiUrl;
|
|
83
84
|
private siteId;
|
|
84
85
|
private apiToken;
|
|
85
|
-
|
|
86
|
+
private getShopperId?;
|
|
87
|
+
private getSessionId?;
|
|
88
|
+
constructor(apiUrl: string, siteId: string, apiToken: string, getShopperId?: (() => string | undefined) | undefined, getSessionId?: (() => string | undefined) | undefined);
|
|
86
89
|
private post;
|
|
87
90
|
ingest(product: Product): Promise<IngestResponse>;
|
|
88
91
|
ingestBatch(products: Product[]): Promise<IngestResponse>;
|
|
89
92
|
search(query: string, limit?: number): Promise<SearchResponse>;
|
|
93
|
+
searchVector(query: string, limit?: number): Promise<SearchResponse>;
|
|
94
|
+
chat(query: string, history?: Array<{
|
|
95
|
+
role: 'user' | 'assistant';
|
|
96
|
+
content: string;
|
|
97
|
+
}>): Promise<{
|
|
98
|
+
answer: string;
|
|
99
|
+
sources: any[];
|
|
100
|
+
}>;
|
|
90
101
|
}
|
|
91
102
|
|
|
92
103
|
declare class HuskelClient {
|
|
@@ -95,7 +106,17 @@ declare class HuskelClient {
|
|
|
95
106
|
private ingestTimer;
|
|
96
107
|
private ingestedUrls;
|
|
97
108
|
private onlineHandler;
|
|
109
|
+
private shopperId?;
|
|
110
|
+
private sessionId;
|
|
111
|
+
private static INGEST_CACHE_KEY;
|
|
112
|
+
private static INGEST_CACHE_TTL;
|
|
113
|
+
private loadIngestedCache;
|
|
114
|
+
private saveIngestedCache;
|
|
98
115
|
constructor(config: HuskelConfig);
|
|
116
|
+
setShopperId(id: string | undefined): void;
|
|
117
|
+
getShopperId(): string | undefined;
|
|
118
|
+
getSessionId(): string;
|
|
119
|
+
private initSession;
|
|
99
120
|
destroy(): void;
|
|
100
121
|
queueIngest(rawProduct: RawProductInput): Promise<void>;
|
|
101
122
|
queueIngestBatch(rawProducts: RawProductInput[]): Promise<void>;
|
|
@@ -149,6 +170,29 @@ declare function useIngest(): UseIngestReturn;
|
|
|
149
170
|
*/
|
|
150
171
|
declare function usePageIngest(product: RawProductInput | null | undefined): void;
|
|
151
172
|
|
|
173
|
+
interface ChatMessage {
|
|
174
|
+
role: 'user' | 'assistant';
|
|
175
|
+
content: string;
|
|
176
|
+
}
|
|
177
|
+
interface ChatSource {
|
|
178
|
+
id?: string;
|
|
179
|
+
name: string;
|
|
180
|
+
price?: string;
|
|
181
|
+
currency?: string;
|
|
182
|
+
category?: string;
|
|
183
|
+
url?: string;
|
|
184
|
+
image?: string;
|
|
185
|
+
}
|
|
186
|
+
interface UseChatReturn {
|
|
187
|
+
messages: ChatMessage[];
|
|
188
|
+
sources: ChatSource[];
|
|
189
|
+
loading: boolean;
|
|
190
|
+
error: string | null;
|
|
191
|
+
send: (query: string) => Promise<void>;
|
|
192
|
+
reset: () => void;
|
|
193
|
+
}
|
|
194
|
+
declare function useChat(): UseChatReturn;
|
|
195
|
+
|
|
152
196
|
interface SearchBarProps {
|
|
153
197
|
placeholder?: string;
|
|
154
198
|
limit?: number;
|
|
@@ -169,9 +213,18 @@ interface SparkleProps {
|
|
|
169
213
|
}
|
|
170
214
|
declare function Sparkle({ productName, limit, onResult, className }: SparkleProps): react_jsx_runtime.JSX.Element;
|
|
171
215
|
|
|
216
|
+
interface ChatWidgetProps {
|
|
217
|
+
placeholder?: string;
|
|
218
|
+
title?: string;
|
|
219
|
+
className?: string;
|
|
220
|
+
/** Called when user clicks a product link */
|
|
221
|
+
onSelectSource?: (source: ChatSource) => void;
|
|
222
|
+
}
|
|
223
|
+
declare function ChatWidget({ placeholder, title, className, onSelectSource }: ChatWidgetProps): react_jsx_runtime.JSX.Element;
|
|
224
|
+
|
|
172
225
|
interface HuskelProviderProps extends HuskelConfig {
|
|
173
226
|
children: React.ReactNode;
|
|
174
227
|
}
|
|
175
|
-
declare function HuskelProvider({ siteId, apiUrl, apiToken, children }: HuskelProviderProps): react_jsx_runtime.JSX.Element;
|
|
228
|
+
declare function HuskelProvider({ siteId, apiUrl, apiToken, shopperId, children }: HuskelProviderProps): react_jsx_runtime.JSX.Element;
|
|
176
229
|
|
|
177
|
-
export { HuskelAPI, HuskelClient, type HuskelConfig, type HuskelError, HuskelProvider, type IngestResponse, type Product, type RawProductInput, SearchBar, type SearchRequest, type SearchResponse, type SearchResult, Sparkle, getHuskelClient, initHuskel, useHuskel, useIngest, usePageIngest, useSearch };
|
|
230
|
+
export { type ChatMessage, type ChatSource, ChatWidget, HuskelAPI, HuskelClient, type HuskelConfig, type HuskelError, HuskelProvider, type IngestResponse, type Product, type RawProductInput, SearchBar, type SearchRequest, type SearchResponse, type SearchResult, Sparkle, getHuskelClient, initHuskel, useChat, useHuskel, useIngest, usePageIngest, useSearch };
|
package/dist/index.js
CHANGED
|
@@ -38,6 +38,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
38
38
|
// src/index.ts
|
|
39
39
|
var index_exports = {};
|
|
40
40
|
__export(index_exports, {
|
|
41
|
+
ChatWidget: () => ChatWidget,
|
|
41
42
|
HuskelAPI: () => HuskelAPI,
|
|
42
43
|
HuskelClient: () => HuskelClient,
|
|
43
44
|
HuskelProvider: () => HuskelProvider,
|
|
@@ -45,6 +46,7 @@ __export(index_exports, {
|
|
|
45
46
|
Sparkle: () => Sparkle,
|
|
46
47
|
getHuskelClient: () => getHuskelClient,
|
|
47
48
|
initHuskel: () => initHuskel,
|
|
49
|
+
useChat: () => useChat,
|
|
48
50
|
useHuskel: () => useHuskel,
|
|
49
51
|
useIngest: () => useIngest,
|
|
50
52
|
usePageIngest: () => usePageIngest,
|
|
@@ -65,21 +67,33 @@ async function sleep(ms) {
|
|
|
65
67
|
return new Promise((r) => setTimeout(r, ms));
|
|
66
68
|
}
|
|
67
69
|
var HuskelAPI = class {
|
|
68
|
-
constructor(apiUrl, siteId, apiToken) {
|
|
70
|
+
constructor(apiUrl, siteId, apiToken, getShopperId, getSessionId) {
|
|
69
71
|
this.apiUrl = apiUrl;
|
|
70
72
|
this.siteId = siteId;
|
|
71
73
|
this.apiToken = apiToken;
|
|
74
|
+
this.getShopperId = getShopperId;
|
|
75
|
+
this.getSessionId = getSessionId;
|
|
72
76
|
}
|
|
73
77
|
async post(path, body, attempt = 0) {
|
|
78
|
+
var _a, _b;
|
|
74
79
|
const url = `${this.apiUrl}${path}`;
|
|
75
80
|
try {
|
|
81
|
+
const headers = {
|
|
82
|
+
"Content-Type": "application/json",
|
|
83
|
+
"X-Huskel-Token": this.apiToken,
|
|
84
|
+
"X-Huskel-Site": this.siteId
|
|
85
|
+
};
|
|
86
|
+
const shopperId = (_a = this.getShopperId) == null ? void 0 : _a.call(this);
|
|
87
|
+
if (shopperId) {
|
|
88
|
+
headers["X-Huskel-Shopper-Id"] = shopperId;
|
|
89
|
+
}
|
|
90
|
+
const sessionId = (_b = this.getSessionId) == null ? void 0 : _b.call(this);
|
|
91
|
+
if (sessionId) {
|
|
92
|
+
headers["X-Huskel-Session-Id"] = sessionId;
|
|
93
|
+
}
|
|
76
94
|
const res = await fetch(url, {
|
|
77
95
|
method: "POST",
|
|
78
|
-
headers
|
|
79
|
-
"Content-Type": "application/json",
|
|
80
|
-
"X-Huskel-Token": this.apiToken,
|
|
81
|
-
"X-Huskel-Site": this.siteId
|
|
82
|
-
},
|
|
96
|
+
headers,
|
|
83
97
|
body: JSON.stringify(body)
|
|
84
98
|
});
|
|
85
99
|
if (!res.ok) {
|
|
@@ -122,6 +136,15 @@ var HuskelAPI = class {
|
|
|
122
136
|
log("info", "search query", query);
|
|
123
137
|
return this.post("/search", { query, siteId: this.siteId, limit });
|
|
124
138
|
}
|
|
139
|
+
// Pure vector search — no LLM, instant results. This is what the SearchBar uses.
|
|
140
|
+
async searchVector(query, limit = 10) {
|
|
141
|
+
return this.post("/search/vector", { query, siteId: this.siteId, limit });
|
|
142
|
+
}
|
|
143
|
+
// LLM chat — conversational search with history context.
|
|
144
|
+
async chat(query, history = []) {
|
|
145
|
+
log("info", "chat query", query);
|
|
146
|
+
return this.post("/chat", { query, siteId: this.siteId, history });
|
|
147
|
+
}
|
|
125
148
|
};
|
|
126
149
|
|
|
127
150
|
// src/client.ts
|
|
@@ -205,19 +228,39 @@ function mapRawProduct(input) {
|
|
|
205
228
|
slug
|
|
206
229
|
};
|
|
207
230
|
}
|
|
208
|
-
|
|
231
|
+
function generateUUID() {
|
|
232
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
233
|
+
return crypto.randomUUID();
|
|
234
|
+
}
|
|
235
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
236
|
+
const r = Math.random() * 16 | 0;
|
|
237
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
238
|
+
return v.toString(16);
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
var _HuskelClient = class _HuskelClient {
|
|
209
242
|
constructor(config) {
|
|
210
243
|
this.ingestQueue = [];
|
|
211
244
|
this.ingestTimer = null;
|
|
212
245
|
this.ingestedUrls = /* @__PURE__ */ new Set();
|
|
213
246
|
this.onlineHandler = null;
|
|
247
|
+
this.sessionId = "";
|
|
214
248
|
const siteId = config.siteId || getEnvVar("NEXT_PUBLIC_HUSKEL_SITE_ID") || "";
|
|
215
249
|
const apiUrl = config.apiUrl || getEnvVar("NEXT_PUBLIC_HUSKEL_API_URL") || "";
|
|
216
250
|
const apiToken = config.apiToken || getEnvVar("NEXT_PUBLIC_HUSKEL_API_TOKEN") || "";
|
|
217
251
|
if (!siteId) console.error('[Huskel] Missing siteId. Set it via <HuskelProvider siteId="..."> or NEXT_PUBLIC_HUSKEL_SITE_ID.');
|
|
218
252
|
if (!apiUrl) console.error('[Huskel] Missing apiUrl. Set it via <HuskelProvider apiUrl="..."> or NEXT_PUBLIC_HUSKEL_API_URL.');
|
|
219
253
|
if (!apiToken) console.error('[Huskel] Missing apiToken. Set it via <HuskelProvider apiToken="..."> or NEXT_PUBLIC_HUSKEL_API_TOKEN.');
|
|
220
|
-
this.
|
|
254
|
+
this.shopperId = config.shopperId;
|
|
255
|
+
this.initSession();
|
|
256
|
+
this.loadIngestedCache();
|
|
257
|
+
this.api = new HuskelAPI(
|
|
258
|
+
apiUrl,
|
|
259
|
+
siteId,
|
|
260
|
+
apiToken,
|
|
261
|
+
() => this.shopperId,
|
|
262
|
+
() => this.sessionId
|
|
263
|
+
);
|
|
221
264
|
instance = this;
|
|
222
265
|
if (typeof window !== "undefined") {
|
|
223
266
|
this.onlineHandler = () => {
|
|
@@ -227,6 +270,55 @@ var HuskelClient = class {
|
|
|
227
270
|
window.addEventListener("online", this.onlineHandler);
|
|
228
271
|
}
|
|
229
272
|
}
|
|
273
|
+
// 24h
|
|
274
|
+
loadIngestedCache() {
|
|
275
|
+
if (typeof window === "undefined") return;
|
|
276
|
+
try {
|
|
277
|
+
const raw = localStorage.getItem(_HuskelClient.INGEST_CACHE_KEY);
|
|
278
|
+
if (!raw) return;
|
|
279
|
+
const { ts, urls } = JSON.parse(raw);
|
|
280
|
+
if (Date.now() - ts > _HuskelClient.INGEST_CACHE_TTL) {
|
|
281
|
+
localStorage.removeItem(_HuskelClient.INGEST_CACHE_KEY);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
this.ingestedUrls = new Set(urls);
|
|
285
|
+
} catch (e) {
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
saveIngestedCache() {
|
|
289
|
+
if (typeof window === "undefined") return;
|
|
290
|
+
try {
|
|
291
|
+
localStorage.setItem(
|
|
292
|
+
_HuskelClient.INGEST_CACHE_KEY,
|
|
293
|
+
JSON.stringify({ ts: Date.now(), urls: [...this.ingestedUrls] })
|
|
294
|
+
);
|
|
295
|
+
} catch (e) {
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
setShopperId(id) {
|
|
299
|
+
this.shopperId = id;
|
|
300
|
+
}
|
|
301
|
+
getShopperId() {
|
|
302
|
+
return this.shopperId;
|
|
303
|
+
}
|
|
304
|
+
getSessionId() {
|
|
305
|
+
return this.sessionId;
|
|
306
|
+
}
|
|
307
|
+
initSession() {
|
|
308
|
+
if (typeof window !== "undefined" && window.sessionStorage) {
|
|
309
|
+
try {
|
|
310
|
+
let sid = window.sessionStorage.getItem("huskel_session_id");
|
|
311
|
+
if (!sid) {
|
|
312
|
+
sid = generateUUID();
|
|
313
|
+
window.sessionStorage.setItem("huskel_session_id", sid);
|
|
314
|
+
}
|
|
315
|
+
this.sessionId = sid;
|
|
316
|
+
return;
|
|
317
|
+
} catch (e) {
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
this.sessionId = generateUUID();
|
|
321
|
+
}
|
|
230
322
|
destroy() {
|
|
231
323
|
if (typeof window !== "undefined" && this.onlineHandler) {
|
|
232
324
|
window.removeEventListener("online", this.onlineHandler);
|
|
@@ -245,6 +337,7 @@ var HuskelClient = class {
|
|
|
245
337
|
return;
|
|
246
338
|
}
|
|
247
339
|
this.ingestedUrls.add(product.url);
|
|
340
|
+
this.saveIngestedCache();
|
|
248
341
|
this.ingestQueue.push(product);
|
|
249
342
|
this.scheduleFlush();
|
|
250
343
|
}
|
|
@@ -259,6 +352,7 @@ var HuskelClient = class {
|
|
|
259
352
|
this.ingestQueue.push(product);
|
|
260
353
|
});
|
|
261
354
|
if (this.ingestQueue.length > 0) {
|
|
355
|
+
this.saveIngestedCache();
|
|
262
356
|
this.scheduleFlush();
|
|
263
357
|
}
|
|
264
358
|
}
|
|
@@ -290,6 +384,9 @@ var HuskelClient = class {
|
|
|
290
384
|
}
|
|
291
385
|
}
|
|
292
386
|
};
|
|
387
|
+
_HuskelClient.INGEST_CACHE_KEY = "huskel_ingested_v1";
|
|
388
|
+
_HuskelClient.INGEST_CACHE_TTL = 24 * 60 * 60 * 1e3;
|
|
389
|
+
var HuskelClient = _HuskelClient;
|
|
293
390
|
var instance = null;
|
|
294
391
|
function initHuskel(config) {
|
|
295
392
|
instance = new HuskelClient(config);
|
|
@@ -327,11 +424,15 @@ var import_react3 = require("react");
|
|
|
327
424
|
var import_react2 = require("react");
|
|
328
425
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
329
426
|
var HuskelContext = (0, import_react2.createContext)(null);
|
|
330
|
-
function HuskelProvider({ siteId, apiUrl, apiToken, children }) {
|
|
427
|
+
function HuskelProvider({ siteId, apiUrl, apiToken, shopperId, children }) {
|
|
331
428
|
const clientRef = (0, import_react2.useRef)(null);
|
|
332
429
|
if (!clientRef.current) {
|
|
333
|
-
clientRef.current = new HuskelClient({ siteId, apiUrl, apiToken });
|
|
430
|
+
clientRef.current = new HuskelClient({ siteId, apiUrl, apiToken, shopperId });
|
|
334
431
|
}
|
|
432
|
+
(0, import_react2.useEffect)(() => {
|
|
433
|
+
var _a;
|
|
434
|
+
(_a = clientRef.current) == null ? void 0 : _a.setShopperId(shopperId);
|
|
435
|
+
}, [shopperId]);
|
|
335
436
|
(0, import_react2.useEffect)(() => {
|
|
336
437
|
return () => {
|
|
337
438
|
var _a;
|
|
@@ -366,7 +467,7 @@ function useSearch() {
|
|
|
366
467
|
setLoading(true);
|
|
367
468
|
setError(null);
|
|
368
469
|
try {
|
|
369
|
-
const res = await client.api.
|
|
470
|
+
const res = await client.api.searchVector(query, limit);
|
|
370
471
|
setResults((_b = res.results) != null ? _b : []);
|
|
371
472
|
} catch (e) {
|
|
372
473
|
setError((_c = e.message) != null ? _c : "Search failed");
|
|
@@ -432,8 +533,47 @@ function usePageIngest(product) {
|
|
|
432
533
|
}, [(_a = product == null ? void 0 : product.url) != null ? _a : product == null ? void 0 : product.name]);
|
|
433
534
|
}
|
|
434
535
|
|
|
435
|
-
// src/
|
|
536
|
+
// src/hooks/useChat.ts
|
|
436
537
|
var import_react6 = require("react");
|
|
538
|
+
function useChat() {
|
|
539
|
+
const client = useHuskelContext();
|
|
540
|
+
const [messages, setMessages] = (0, import_react6.useState)([]);
|
|
541
|
+
const [sources, setSources] = (0, import_react6.useState)([]);
|
|
542
|
+
const [loading, setLoading] = (0, import_react6.useState)(false);
|
|
543
|
+
const [error, setError] = (0, import_react6.useState)(null);
|
|
544
|
+
const abortRef = (0, import_react6.useRef)(null);
|
|
545
|
+
const send = (0, import_react6.useCallback)(async (query) => {
|
|
546
|
+
var _a, _b, _c;
|
|
547
|
+
if (!query.trim() || loading) return;
|
|
548
|
+
(_a = abortRef.current) == null ? void 0 : _a.abort();
|
|
549
|
+
abortRef.current = new AbortController();
|
|
550
|
+
const userMsg = { role: "user", content: query };
|
|
551
|
+
setMessages((prev) => [...prev, userMsg]);
|
|
552
|
+
setLoading(true);
|
|
553
|
+
setError(null);
|
|
554
|
+
try {
|
|
555
|
+
const history = messages.map((m) => ({ role: m.role, content: m.content }));
|
|
556
|
+
const res = await client.api.chat(query, history);
|
|
557
|
+
const assistantMsg = { role: "assistant", content: res.answer };
|
|
558
|
+
setMessages((prev) => [...prev, assistantMsg]);
|
|
559
|
+
setSources((_b = res.sources) != null ? _b : []);
|
|
560
|
+
} catch (e) {
|
|
561
|
+
setError((_c = e == null ? void 0 : e.message) != null ? _c : "Chat request failed");
|
|
562
|
+
setMessages((prev) => prev.slice(0, -1));
|
|
563
|
+
} finally {
|
|
564
|
+
setLoading(false);
|
|
565
|
+
}
|
|
566
|
+
}, [client, messages, loading]);
|
|
567
|
+
const reset = (0, import_react6.useCallback)(() => {
|
|
568
|
+
setMessages([]);
|
|
569
|
+
setSources([]);
|
|
570
|
+
setError(null);
|
|
571
|
+
}, []);
|
|
572
|
+
return { messages, sources, loading, error, send, reset };
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// src/components/SearchBar.tsx
|
|
576
|
+
var import_react7 = require("react");
|
|
437
577
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
438
578
|
var S = `
|
|
439
579
|
.hsk-wrap{position:relative;width:100%;font-family:inherit}
|
|
@@ -457,12 +597,12 @@ function SearchBar({
|
|
|
457
597
|
dropdownClassName,
|
|
458
598
|
renderResult
|
|
459
599
|
}) {
|
|
460
|
-
const [query, setQuery] = (0,
|
|
461
|
-
const [open, setOpen] = (0,
|
|
600
|
+
const [query, setQuery] = (0, import_react7.useState)("");
|
|
601
|
+
const [open, setOpen] = (0, import_react7.useState)(false);
|
|
462
602
|
const { results, loading, search, clear } = useSearch();
|
|
463
|
-
const timer = (0,
|
|
464
|
-
const wrap = (0,
|
|
465
|
-
(0,
|
|
603
|
+
const timer = (0, import_react7.useRef)();
|
|
604
|
+
const wrap = (0, import_react7.useRef)(null);
|
|
605
|
+
(0, import_react7.useEffect)(() => {
|
|
466
606
|
clearTimeout(timer.current);
|
|
467
607
|
if (!query.trim()) {
|
|
468
608
|
clear();
|
|
@@ -475,7 +615,7 @@ function SearchBar({
|
|
|
475
615
|
}, debounceMs);
|
|
476
616
|
return () => clearTimeout(timer.current);
|
|
477
617
|
}, [query, search, clear, limit, debounceMs]);
|
|
478
|
-
(0,
|
|
618
|
+
(0, import_react7.useEffect)(() => {
|
|
479
619
|
const handler = (e) => {
|
|
480
620
|
if (wrap.current && !wrap.current.contains(e.target)) setOpen(false);
|
|
481
621
|
};
|
|
@@ -530,7 +670,7 @@ function SearchBar({
|
|
|
530
670
|
}
|
|
531
671
|
|
|
532
672
|
// src/components/Sparkle.tsx
|
|
533
|
-
var
|
|
673
|
+
var import_react8 = require("react");
|
|
534
674
|
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
535
675
|
var S2 = `
|
|
536
676
|
.hsk-sparkle{display:inline-flex;align-items:center;gap:5px;padding:4px 10px;font-size:12px;font-weight:600;background:#f47c3c;color:#fff;border:none;border-radius:20px;cursor:pointer;transition:opacity .2s,transform .15s}
|
|
@@ -539,11 +679,11 @@ var S2 = `
|
|
|
539
679
|
`;
|
|
540
680
|
function Sparkle({ productName, limit = 5, onResult, className }) {
|
|
541
681
|
const client = useHuskelContext();
|
|
542
|
-
const [loading, setLoading] = (0,
|
|
682
|
+
const [loading, setLoading] = (0, import_react8.useState)(false);
|
|
543
683
|
const handleClick = async () => {
|
|
544
684
|
setLoading(true);
|
|
545
685
|
try {
|
|
546
|
-
const res = await client.api.
|
|
686
|
+
const res = await client.api.searchVector(productName, limit);
|
|
547
687
|
onResult == null ? void 0 : onResult(res.results);
|
|
548
688
|
} catch (e) {
|
|
549
689
|
console.error("[Huskel Sparkle]", e);
|
|
@@ -559,8 +699,153 @@ function Sparkle({ productName, limit = 5, onResult, className }) {
|
|
|
559
699
|
] })
|
|
560
700
|
] });
|
|
561
701
|
}
|
|
702
|
+
|
|
703
|
+
// src/components/ChatWidget.tsx
|
|
704
|
+
var import_react9 = require("react");
|
|
705
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
706
|
+
var S3 = `
|
|
707
|
+
.hsk-chat-widget{display:flex;flex-direction:column;height:100%;min-height:320px;font-family:inherit;background:#0f0f10;border:1px solid #2a2a2d;border-radius:12px;overflow:hidden}
|
|
708
|
+
.hsk-chat-header{display:flex;align-items:center;gap:10px;padding:14px 16px;border-bottom:1px solid #1e1e1f;background:#111112;flex-shrink:0}
|
|
709
|
+
.hsk-chat-title{font-size:14px;font-weight:600;color:#f3f3f2}
|
|
710
|
+
.hsk-chat-badge{font-size:10px;font-weight:700;letter-spacing:0.08em;text-transform:uppercase;color:#ff6a33;background:#ff6a3315;border:1px solid #ff6a3330;padding:2px 8px;border-radius:20px}
|
|
711
|
+
.hsk-chat-messages{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:12px;scroll-behavior:smooth}
|
|
712
|
+
.hsk-chat-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:8px;color:#555;font-size:13px;text-align:center;padding:24px}
|
|
713
|
+
.hsk-chat-empty-icon{font-size:28px;margin-bottom:4px}
|
|
714
|
+
.hsk-msg-row{display:flex;gap:8px;align-items:flex-start}
|
|
715
|
+
.hsk-msg-row.user{flex-direction:row-reverse}
|
|
716
|
+
.hsk-msg-avatar{width:28px;height:28px;border-radius:50%;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:700}
|
|
717
|
+
.hsk-msg-avatar.ai{background:#ff6a3320;border:1px solid #ff6a3340;color:#ff6a33}
|
|
718
|
+
.hsk-msg-avatar.user{background:#2a2a2d;color:#9a9aa1}
|
|
719
|
+
.hsk-msg-bubble{max-width:78%;padding:10px 14px;border-radius:12px;font-size:13px;line-height:1.6}
|
|
720
|
+
.hsk-msg-bubble.ai{background:#171718;border:1px solid #2a2a2d;color:#e8e8e7;border-radius:4px 12px 12px 12px}
|
|
721
|
+
.hsk-msg-bubble.user{background:#ff6a33;color:#fff;border-radius:12px 4px 12px 12px}
|
|
722
|
+
.hsk-sources{margin-top:10px;display:flex;flex-direction:column;gap:6px}
|
|
723
|
+
.hsk-source-card{display:flex;align-items:center;gap:10px;padding:8px 10px;background:#1a1a1b;border:1px solid #252527;border-radius:8px;cursor:pointer;transition:border-color 0.15s}
|
|
724
|
+
.hsk-source-card:hover{border-color:#ff6a3360}
|
|
725
|
+
.hsk-source-img{width:36px;height:36px;object-fit:cover;border-radius:4px;background:#fff}
|
|
726
|
+
.hsk-source-name{font-size:12px;font-weight:500;color:#e8e8e7;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
727
|
+
.hsk-source-price{font-size:11px;color:#ff6a33;font-weight:700;margin-top:2px}
|
|
728
|
+
.hsk-typing{display:flex;gap:4px;align-items:center;padding:10px 14px;background:#171718;border:1px solid #2a2a2d;border-radius:4px 12px 12px 12px;width:fit-content}
|
|
729
|
+
.hsk-typing-dot{width:6px;height:6px;background:#ff6a33;border-radius:50%;animation:hsk-chat-bounce 1.2s infinite}
|
|
730
|
+
.hsk-typing-dot:nth-child(2){animation-delay:0.2s}
|
|
731
|
+
.hsk-typing-dot:nth-child(3){animation-delay:0.4s}
|
|
732
|
+
@keyframes hsk-chat-bounce{0%,100%{opacity:0.3;transform:translateY(0)}50%{opacity:1;transform:translateY(-4px)}}
|
|
733
|
+
.hsk-chat-input-area{display:flex;align-items:center;gap:8px;padding:12px 14px;border-top:1px solid #1e1e1f;background:#111112;flex-shrink:0}
|
|
734
|
+
.hsk-chat-input{flex:1;background:#1a1a1b;border:1px solid #2a2a2d;border-radius:8px;padding:9px 14px;font-size:13px;color:#f3f3f2;outline:none;font-family:inherit;transition:border-color 0.2s;resize:none;min-height:38px;max-height:120px;line-height:1.5}
|
|
735
|
+
.hsk-chat-input::placeholder{color:#555}
|
|
736
|
+
.hsk-chat-input:focus{border-color:#ff6a33}
|
|
737
|
+
.hsk-chat-send{width:34px;height:34px;border-radius:8px;background:#ff6a33;border:none;color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:16px;transition:opacity 0.15s,transform 0.1s}
|
|
738
|
+
.hsk-chat-send:hover{opacity:0.88}
|
|
739
|
+
.hsk-chat-send:active{transform:scale(0.93)}
|
|
740
|
+
.hsk-chat-send:disabled{opacity:0.4;cursor:not-allowed}
|
|
741
|
+
.hsk-chat-reset{font-size:11px;color:#555;cursor:pointer;padding:0 4px;transition:color 0.15s;background:none;border:none;font-family:inherit}
|
|
742
|
+
.hsk-chat-reset:hover{color:#ff6a33}
|
|
743
|
+
`;
|
|
744
|
+
function SourceCard({ source, onSelect }) {
|
|
745
|
+
var _a;
|
|
746
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "hsk-source-card", onClick: () => onSelect == null ? void 0 : onSelect(source), children: [
|
|
747
|
+
source.image && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("img", { src: source.image, alt: source.name, className: "hsk-source-img" }),
|
|
748
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { flex: 1, minWidth: 0 }, children: [
|
|
749
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "hsk-source-name", children: source.name }),
|
|
750
|
+
source.price && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "hsk-source-price", children: [
|
|
751
|
+
(_a = source.currency) != null ? _a : "KES",
|
|
752
|
+
" ",
|
|
753
|
+
source.price
|
|
754
|
+
] })
|
|
755
|
+
] })
|
|
756
|
+
] });
|
|
757
|
+
}
|
|
758
|
+
function ChatWidget({ placeholder = "Ask about anything in our store\u2026", title = "AI Shopping Assistant", className, onSelectSource }) {
|
|
759
|
+
const { messages, sources, loading, error, send, reset } = useChat();
|
|
760
|
+
const [input, setInput] = (0, import_react9.useState)("");
|
|
761
|
+
const bottomRef = (0, import_react9.useRef)(null);
|
|
762
|
+
const textareaRef = (0, import_react9.useRef)(null);
|
|
763
|
+
(0, import_react9.useEffect)(() => {
|
|
764
|
+
var _a;
|
|
765
|
+
(_a = bottomRef.current) == null ? void 0 : _a.scrollIntoView({ behavior: "smooth" });
|
|
766
|
+
}, [messages, loading]);
|
|
767
|
+
const handleSend = async () => {
|
|
768
|
+
const q = input.trim();
|
|
769
|
+
if (!q || loading) return;
|
|
770
|
+
setInput("");
|
|
771
|
+
if (textareaRef.current) textareaRef.current.style.height = "auto";
|
|
772
|
+
await send(q);
|
|
773
|
+
};
|
|
774
|
+
const handleKey = (e) => {
|
|
775
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
776
|
+
e.preventDefault();
|
|
777
|
+
handleSend();
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
const handleInput = (e) => {
|
|
781
|
+
setInput(e.target.value);
|
|
782
|
+
const t = e.target;
|
|
783
|
+
t.style.height = "auto";
|
|
784
|
+
t.style.height = Math.min(t.scrollHeight, 120) + "px";
|
|
785
|
+
};
|
|
786
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
|
|
787
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("style", { children: S3 }),
|
|
788
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: `hsk-chat-widget ${className != null ? className : ""}`, children: [
|
|
789
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "hsk-chat-header", children: [
|
|
790
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { fontSize: 16, color: "#ff6a33" }, children: "\u2726" }),
|
|
791
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "hsk-chat-title", children: title }),
|
|
792
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "hsk-chat-badge", children: "AI" }),
|
|
793
|
+
messages.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: "hsk-chat-reset", onClick: reset, style: { marginLeft: "auto" }, children: "Clear" })
|
|
794
|
+
] }),
|
|
795
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "hsk-chat-messages", children: [
|
|
796
|
+
messages.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "hsk-chat-empty", children: [
|
|
797
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "hsk-chat-empty-icon", children: "\u2726" }),
|
|
798
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { children: "Ask me anything about our products" }),
|
|
799
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontSize: 12, color: "#444", marginTop: 4 }, children: '"Find me headphones under KSh 5,000" \xB7 "Gift ideas for a chef"' })
|
|
800
|
+
] }) : messages.map((msg, idx) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { children: [
|
|
801
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: `hsk-msg-row ${msg.role}`, children: [
|
|
802
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: `hsk-msg-avatar ${msg.role === "assistant" ? "ai" : "user"}`, children: msg.role === "assistant" ? "\u2726" : "\u2191" }),
|
|
803
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: `hsk-msg-bubble ${msg.role === "assistant" ? "ai" : "user"}`, children: msg.content })
|
|
804
|
+
] }),
|
|
805
|
+
msg.role === "assistant" && idx === messages.length - 1 && sources.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { marginLeft: 36 }, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "hsk-sources", children: sources.map((src, si) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(SourceCard, { source: src, onSelect: onSelectSource }, si)) }) })
|
|
806
|
+
] }, idx)),
|
|
807
|
+
loading && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "hsk-msg-row", children: [
|
|
808
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "hsk-msg-avatar ai", children: "\u2726" }),
|
|
809
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "hsk-typing", children: [
|
|
810
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "hsk-typing-dot" }),
|
|
811
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "hsk-typing-dot" }),
|
|
812
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "hsk-typing-dot" })
|
|
813
|
+
] })
|
|
814
|
+
] }),
|
|
815
|
+
error && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontSize: 12, color: "#ef4444", textAlign: "center", padding: 8 }, children: error }),
|
|
816
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { ref: bottomRef })
|
|
817
|
+
] }),
|
|
818
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "hsk-chat-input-area", children: [
|
|
819
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
820
|
+
"textarea",
|
|
821
|
+
{
|
|
822
|
+
ref: textareaRef,
|
|
823
|
+
className: "hsk-chat-input",
|
|
824
|
+
value: input,
|
|
825
|
+
onChange: handleInput,
|
|
826
|
+
onKeyDown: handleKey,
|
|
827
|
+
placeholder,
|
|
828
|
+
rows: 1,
|
|
829
|
+
disabled: loading
|
|
830
|
+
}
|
|
831
|
+
),
|
|
832
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
833
|
+
"button",
|
|
834
|
+
{
|
|
835
|
+
className: "hsk-chat-send",
|
|
836
|
+
onClick: handleSend,
|
|
837
|
+
disabled: !input.trim() || loading,
|
|
838
|
+
"aria-label": "Send",
|
|
839
|
+
children: "\u2191"
|
|
840
|
+
}
|
|
841
|
+
)
|
|
842
|
+
] })
|
|
843
|
+
] })
|
|
844
|
+
] });
|
|
845
|
+
}
|
|
562
846
|
// Annotate the CommonJS export names for ESM import in node:
|
|
563
847
|
0 && (module.exports = {
|
|
848
|
+
ChatWidget,
|
|
564
849
|
HuskelAPI,
|
|
565
850
|
HuskelClient,
|
|
566
851
|
HuskelProvider,
|
|
@@ -568,6 +853,7 @@ function Sparkle({ productName, limit = 5, onResult, className }) {
|
|
|
568
853
|
Sparkle,
|
|
569
854
|
getHuskelClient,
|
|
570
855
|
initHuskel,
|
|
856
|
+
useChat,
|
|
571
857
|
useHuskel,
|
|
572
858
|
useIngest,
|
|
573
859
|
usePageIngest,
|