wineater-bot 3.9.3 → 4.0.0-beta.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/.prettierignore +9 -0
- package/.prettierrc +16 -0
- package/README.md +86 -0
- package/dist/wineater-chatbot.css +1 -0
- package/dist/wineater-chatbot.mjs +6494 -5123
- package/dist/wineater-chatbot.umd.js +40 -985
- package/docs/SVELTE.md +146 -0
- package/e2e/demo.test.ts +6 -0
- package/eslint.config.js +41 -0
- package/index.html +11 -3
- package/package.json +30 -5
- package/playwright.config.ts +9 -0
- package/pnpm-workspace.yaml +2 -0
- package/src/App.svelte +394 -0
- package/src/App.svelte.test.ts +13 -0
- package/src/components/Footer.svelte +131 -0
- package/src/components/Header.svelte +92 -0
- package/src/components/Loader.svelte +62 -0
- package/src/components/Search.svelte +188 -0
- package/src/components/WinePopup.svelte +192 -0
- package/src/components/WineTile.svelte +331 -0
- package/src/demo.spec.ts +13 -0
- package/src/index.js +4 -36
- package/src/services/analytics/analytics.ts +103 -0
- package/src/{js/widget/widget-events.js → services/analytics/constants.ts} +0 -2
- package/src/services/api/api.ts +112 -0
- package/src/services/api/deserialize.ts +102 -0
- package/src/services/i18n/i18n.ts +134 -0
- package/src/services/i18n/translations.json +521 -0
- package/src/services/store/app-store.svelte.ts +84 -0
- package/tsconfig.json +23 -0
- package/vite.config.js +69 -19
- package/vitest-setup-client.ts +2 -0
- package/src/js/analytics.js +0 -117
- package/src/js/api.js +0 -138
- package/src/js/i18n.js +0 -703
- package/src/js/widget/widget-language.js +0 -107
- package/src/js/widget/widget-search.js +0 -68
- package/src/js/widget/widget-styles.js +0 -855
- package/src/js/widget/widget-ui.js +0 -186
- package/src/js/widget.js +0 -227
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { wineaterApi } from "../api/api";
|
|
2
|
+
import type { CurrencyCode, LanguageCode } from "../i18n/i18n";
|
|
3
|
+
|
|
4
|
+
export type StoreConfig = {
|
|
5
|
+
id: string;
|
|
6
|
+
lang: LanguageCode;
|
|
7
|
+
available_languages: LanguageCode[];
|
|
8
|
+
open_ai_key: string | null;
|
|
9
|
+
currency: CurrencyCode;
|
|
10
|
+
[key: string]: any;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type AppState = {
|
|
14
|
+
storeConfig: StoreConfig | null;
|
|
15
|
+
currentLanguage: LanguageCode;
|
|
16
|
+
isConfigLoaded: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
let state = $state<AppState>({
|
|
20
|
+
storeConfig: null,
|
|
21
|
+
currentLanguage: 'en',
|
|
22
|
+
isConfigLoaded: false,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export const appStore = {
|
|
26
|
+
getLanguageFromURL(): LanguageCode | null {
|
|
27
|
+
const params = new URLSearchParams(window.location.search);
|
|
28
|
+
const lang = params.get('lang');
|
|
29
|
+
return lang as LanguageCode | null;
|
|
30
|
+
},
|
|
31
|
+
// app language -- the one used in the UI
|
|
32
|
+
getAppLanguage(): LanguageCode {
|
|
33
|
+
return state?.currentLanguage ?? 'en';
|
|
34
|
+
},
|
|
35
|
+
// store default language from API
|
|
36
|
+
getStoreLanguage(): LanguageCode | null {
|
|
37
|
+
return state.storeConfig?.lang ?? null;
|
|
38
|
+
},
|
|
39
|
+
getApiKey(): string | null {
|
|
40
|
+
return state.storeConfig?.open_ai_key ?? null;
|
|
41
|
+
},
|
|
42
|
+
getStoreId(): string | null {
|
|
43
|
+
return state.storeConfig?.id ?? null;
|
|
44
|
+
},
|
|
45
|
+
setStoreLanguage(lang: LanguageCode) {
|
|
46
|
+
if (state.storeConfig && state.storeConfig.available_languages.includes(lang)) {
|
|
47
|
+
state.currentLanguage = lang;
|
|
48
|
+
const params = new URLSearchParams(window.location.search);
|
|
49
|
+
params.set('lang', lang);
|
|
50
|
+
const newUrl = `${window.location.pathname}?${params.toString()}`;
|
|
51
|
+
window.history.replaceState({}, '', newUrl);
|
|
52
|
+
} else {
|
|
53
|
+
throw new Error(`Language ${lang} is not available in store configuration.`);
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
getSupportedLanguages(): LanguageCode[] {
|
|
57
|
+
return state.storeConfig?.available_languages ?? ['en'];
|
|
58
|
+
},
|
|
59
|
+
getCurrency(): CurrencyCode {
|
|
60
|
+
return state.storeConfig?.currency ?? 'USD';
|
|
61
|
+
},
|
|
62
|
+
async initializeStore() {
|
|
63
|
+
try {
|
|
64
|
+
const config = await wineaterApi.getStoreConfig();
|
|
65
|
+
state.storeConfig = config;
|
|
66
|
+
state.currentLanguage = config.lang as LanguageCode;
|
|
67
|
+
const urlLang = this.getLanguageFromURL();
|
|
68
|
+
if (urlLang && config.available_languages.includes(urlLang)) {
|
|
69
|
+
this.setStoreLanguage(urlLang);
|
|
70
|
+
} else {
|
|
71
|
+
// fallback to store default language
|
|
72
|
+
this.setStoreLanguage(config.lang as LanguageCode);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
state.isConfigLoaded = true;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error('Failed to initialize store:', error);
|
|
78
|
+
state.isConfigLoaded = false;
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
isInitialized(): boolean {
|
|
82
|
+
return state.isConfigLoaded;
|
|
83
|
+
},
|
|
84
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"module": "ESNext",
|
|
4
|
+
"target": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"allowImportingTsExtensions": true,
|
|
7
|
+
"allowArbitraryExtensions": true,
|
|
8
|
+
"resolveJsonModule": true,
|
|
9
|
+
"allowSyntheticDefaultImports": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"strict": false,
|
|
13
|
+
"forceConsistentCasingInFileNames": true,
|
|
14
|
+
"isolatedModules": true,
|
|
15
|
+
"noEmit": true
|
|
16
|
+
},
|
|
17
|
+
"include": [
|
|
18
|
+
"src/**/*"
|
|
19
|
+
],
|
|
20
|
+
"exclude": [
|
|
21
|
+
"node_modules"
|
|
22
|
+
]
|
|
23
|
+
}
|
package/vite.config.js
CHANGED
|
@@ -1,21 +1,71 @@
|
|
|
1
|
-
import
|
|
1
|
+
import devtoolsJson from 'vite-plugin-devtools-json';
|
|
2
|
+
import { defineConfig } from 'vitest/config';
|
|
3
|
+
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
|
2
4
|
|
|
3
5
|
export default defineConfig({
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
6
|
+
server: {
|
|
7
|
+
port: 3001 // Customize the port if necessary
|
|
8
|
+
},
|
|
9
|
+
build: {
|
|
10
|
+
lib: {
|
|
11
|
+
entry: 'src/index.js',
|
|
12
|
+
name: 'WineaterChatbot',
|
|
13
|
+
fileName: () => 'wineater-chatbot' // base name only
|
|
14
|
+
},
|
|
15
|
+
rollupOptions: {
|
|
16
|
+
output: [
|
|
17
|
+
{
|
|
18
|
+
format: 'es',
|
|
19
|
+
entryFileNames: 'wineater-chatbot.mjs',
|
|
20
|
+
assetFileNames: (assetInfo) => {
|
|
21
|
+
if (assetInfo.name && assetInfo.name.endsWith('.css')) {
|
|
22
|
+
return 'wineater-chatbot.css'; // your desired name
|
|
23
|
+
}
|
|
24
|
+
return assetInfo.name || '[name].[ext]';
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
format: 'umd',
|
|
29
|
+
name: 'WineaterChatbot',
|
|
30
|
+
entryFileNames: 'wineater-chatbot.umd.js',
|
|
31
|
+
assetFileNames: (assetInfo) => {
|
|
32
|
+
if (assetInfo.name && assetInfo.name.endsWith('.css')) {
|
|
33
|
+
return 'wineater-chatbot.css';
|
|
34
|
+
}
|
|
35
|
+
return assetInfo.name || '[name].[ext]';
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
test: {
|
|
42
|
+
expect: { requireAssertions: true },
|
|
43
|
+
projects: [
|
|
44
|
+
{
|
|
45
|
+
extends: './vite.config.js',
|
|
46
|
+
test: {
|
|
47
|
+
name: 'client',
|
|
48
|
+
environment: 'browser',
|
|
49
|
+
browser: {
|
|
50
|
+
enabled: true,
|
|
51
|
+
provider: 'playwright',
|
|
52
|
+
instances: [{ browser: 'chromium' }]
|
|
53
|
+
},
|
|
54
|
+
include: ['src/**/*.svelte.{test,spec}.{js,ts}'],
|
|
55
|
+
exclude: ['src/lib/server/**'],
|
|
56
|
+
setupFiles: ['./vitest-setup-client.ts']
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
extends: './vite.config.js',
|
|
61
|
+
test: {
|
|
62
|
+
name: 'server',
|
|
63
|
+
environment: 'node',
|
|
64
|
+
include: ['src/**/*.{test,spec}.{js,ts}'],
|
|
65
|
+
exclude: ['src/**/*.svelte.{test,spec}.{js,ts}']
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
},
|
|
70
|
+
plugins: [devtoolsJson(), svelte()]
|
|
71
|
+
});
|
package/src/js/analytics.js
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
// analytics.js
|
|
2
|
-
|
|
3
|
-
// Function to generate a UUID
|
|
4
|
-
function generateUUID() {
|
|
5
|
-
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
6
|
-
const r = Math.random() * 16 | 0,
|
|
7
|
-
v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
8
|
-
return v.toString(16);
|
|
9
|
-
});
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
// Function to set a cookie
|
|
13
|
-
function setCookie(name, value, days) {
|
|
14
|
-
console.log(name, days);
|
|
15
|
-
let expires = "";
|
|
16
|
-
if (days) {
|
|
17
|
-
const date = new Date();
|
|
18
|
-
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
|
|
19
|
-
expires = "; expires=" + date.toUTCString();
|
|
20
|
-
}
|
|
21
|
-
document.cookie = name + "=" + (value || "") + expires + "; path=/; secure; samesite=strict";
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Function to get a cookie by name
|
|
25
|
-
function getCookie(name) {
|
|
26
|
-
const nameEQ = name + "=";
|
|
27
|
-
const ca = document.cookie.split(';');
|
|
28
|
-
for (let i = 0; i < ca.length; i++) {
|
|
29
|
-
let c = ca[i];
|
|
30
|
-
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
|
|
31
|
-
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
|
|
32
|
-
}
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function deleteCookie(name) {
|
|
37
|
-
document.cookie = name + '=; Max-Age=-99999999;';
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Function to set a unique user identifier cookie
|
|
41
|
-
function setUniqueUserCookie() {
|
|
42
|
-
const cookieName = 'uniqueUserID';
|
|
43
|
-
let userId = getCookie(cookieName);
|
|
44
|
-
|
|
45
|
-
if (!userId) {
|
|
46
|
-
userId = generateUUID();
|
|
47
|
-
setCookie(cookieName, userId, 365); // Set cookie to expire in 1 year
|
|
48
|
-
}
|
|
49
|
-
return userId;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function trackWidgetEvent(event_type, store_id, data = {}, meta = {}) {
|
|
53
|
-
const endpoint = 'https://admin-api.wineater.com/api/widget-events';
|
|
54
|
-
|
|
55
|
-
// Ensure we have a unique user ID
|
|
56
|
-
let user_id = getCookie('uniqueUserID');
|
|
57
|
-
if (!user_id) {
|
|
58
|
-
user_id = setUniqueUserCookie();
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Get device info
|
|
62
|
-
const device_info = {
|
|
63
|
-
user_agent: navigator.userAgent,
|
|
64
|
-
screen_width: window.screen.width,
|
|
65
|
-
screen_height: window.screen.height,
|
|
66
|
-
language: navigator.language,
|
|
67
|
-
platform: navigator.platform
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
// Get current timestamp
|
|
71
|
-
const event_time = new Date().toISOString();
|
|
72
|
-
|
|
73
|
-
// Generate session ID if not exists
|
|
74
|
-
let session_id = getCookie('sessionID');
|
|
75
|
-
if (!session_id) {
|
|
76
|
-
session_id = generateUUID();
|
|
77
|
-
setCookie('sessionID', session_id, 1); // Session cookie expires in 1 day
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const payload = {
|
|
81
|
-
event_type,
|
|
82
|
-
store_id,
|
|
83
|
-
session_id,
|
|
84
|
-
event_time,
|
|
85
|
-
data,
|
|
86
|
-
meta: {
|
|
87
|
-
...meta,
|
|
88
|
-
user_id
|
|
89
|
-
},
|
|
90
|
-
device_info,
|
|
91
|
-
region: navigator.language.split('-')[1] || null,
|
|
92
|
-
language: navigator.language.split('-')[0] || 'en'
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
return fetch(endpoint, {
|
|
96
|
-
method: 'POST',
|
|
97
|
-
headers: {
|
|
98
|
-
'Content-Type': 'application/json'
|
|
99
|
-
},
|
|
100
|
-
body: JSON.stringify(payload)
|
|
101
|
-
})
|
|
102
|
-
.then(response => {
|
|
103
|
-
if (!response.ok) {
|
|
104
|
-
return response.json().then(err => {
|
|
105
|
-
throw new Error(err.message || 'Failed to track widget event');
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
return response.json();
|
|
109
|
-
})
|
|
110
|
-
.catch(error => {
|
|
111
|
-
console.error('Widget event tracking error:', error);
|
|
112
|
-
throw error;
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export { setCookie, getCookie, setUniqueUserCookie, deleteCookie, trackWidgetEvent };
|
|
117
|
-
|
package/src/js/api.js
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
// api.js
|
|
2
|
-
|
|
3
|
-
const stores = [];
|
|
4
|
-
const DEMO_TOKEN='HeUw6IXuhvz6tRuXdPEGEbj3SGQ3Q2YtEc1Z8jd41Kg9LtyPb4Epu0to31SYIadG';
|
|
5
|
-
|
|
6
|
-
export class Api {
|
|
7
|
-
constructor(apiEndpoint) {
|
|
8
|
-
// Allow users of the library to specify their own endpoint if needed
|
|
9
|
-
// this.baseURL = 'http://localhost:3000/';
|
|
10
|
-
this.baseURL = 'https://wineater-backend.vercel.app/';
|
|
11
|
-
this.store = stores.find(value => window.location.href.includes(value));
|
|
12
|
-
this.token = window?.wineaterData?.CLIENT_TOKEN || DEMO_TOKEN;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
async getStoreConfig(storeName) {
|
|
16
|
-
console.log(storeName);
|
|
17
|
-
try {
|
|
18
|
-
const response = await fetch(`${this.baseURL}stores/store`, {
|
|
19
|
-
method: 'POST',
|
|
20
|
-
headers: {
|
|
21
|
-
'Content-Type': 'application/json',
|
|
22
|
-
'Authorization': `Bearer ${this.token}`
|
|
23
|
-
},
|
|
24
|
-
body: JSON.stringify({ store: storeName })
|
|
25
|
-
});
|
|
26
|
-
console.log(response);
|
|
27
|
-
if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
|
|
28
|
-
return await response.json();
|
|
29
|
-
} catch (error) {
|
|
30
|
-
console.error('Error fetching store configuration:', error);
|
|
31
|
-
throw error;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async getWineRecommendations(text) {
|
|
36
|
-
console.log(window.location.href);
|
|
37
|
-
try {
|
|
38
|
-
const response = await fetch(`${this.baseURL}dish/recommendations`, {
|
|
39
|
-
method: 'POST',
|
|
40
|
-
headers: {
|
|
41
|
-
'Content-Type': 'application/json',
|
|
42
|
-
'Authorization': `Bearer ${this.token}`
|
|
43
|
-
},
|
|
44
|
-
body: JSON.stringify({ text: text, store: this.store ? (this.store.toString().charAt(0).toUpperCase() + this.store.slice(1)) : undefined})
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
const data = await response.json();
|
|
48
|
-
|
|
49
|
-
if (!response.ok) {
|
|
50
|
-
// Providing more detailed error information
|
|
51
|
-
throw new Error(`API call failed with status ${response.status}: ${data.message || 'No additional error information'}`);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return data;
|
|
55
|
-
} catch (error) {
|
|
56
|
-
// Log and rethrow to allow consumer of the API to handle or display the error appropriately
|
|
57
|
-
console.error('Fetching wine recommendations failed:', error.message);
|
|
58
|
-
throw new Error(`Error in getWineRecommendations: ${error.message}`);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async getWineRecommendationsSearch(text) {
|
|
63
|
-
console.log(window.location.href);
|
|
64
|
-
try {
|
|
65
|
-
const response = await fetch(`${this.baseURL}dish/recommendations/search`, {
|
|
66
|
-
method: 'POST',
|
|
67
|
-
headers: {
|
|
68
|
-
'Content-Type': 'application/json',
|
|
69
|
-
'Authorization': `Bearer ${this.token}`
|
|
70
|
-
},
|
|
71
|
-
body: JSON.stringify({ text: text})
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
const data = await response.json();
|
|
75
|
-
|
|
76
|
-
if (!response.ok) {
|
|
77
|
-
// Providing more detailed error information
|
|
78
|
-
throw new Error(`API call failed with status ${response.status}: ${data.message || 'No additional error information'}`);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return data;
|
|
82
|
-
} catch (error) {
|
|
83
|
-
// Log and rethrow to allow consumer of the API to handle or display the error appropriately
|
|
84
|
-
console.error('Fetching wine recommendations failed:', error.message);
|
|
85
|
-
throw new Error(`Error in getWineRecommendations: ${error.message}`);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
async getPossibleDishesWithMeta(metaTitle, metaDescription) {
|
|
90
|
-
try {
|
|
91
|
-
const response = await fetch(`${this.baseURL}dish/possible_dishes`, {
|
|
92
|
-
method: 'POST',
|
|
93
|
-
headers: {
|
|
94
|
-
'Authorization': `Bearer ${this.token}`,
|
|
95
|
-
'Content-Type': 'application/json'
|
|
96
|
-
},
|
|
97
|
-
body: JSON.stringify({
|
|
98
|
-
title: metaTitle,
|
|
99
|
-
description: metaDescription
|
|
100
|
-
})
|
|
101
|
-
});
|
|
102
|
-
const data = await response.json();
|
|
103
|
-
console.log(data);
|
|
104
|
-
if (!response.ok) {
|
|
105
|
-
throw new Error(`API call failed with status ${response.status}: ${data.message || 'No additional error information'}`);
|
|
106
|
-
}
|
|
107
|
-
return data; // Return the list of dishes
|
|
108
|
-
} catch (error) {
|
|
109
|
-
console.error('Error fetching possible dishes:', error);
|
|
110
|
-
throw error;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
async getInstantRecommendations() {
|
|
114
|
-
console.log('Fetching instant recommendations...');
|
|
115
|
-
try {
|
|
116
|
-
const response = await fetch(`${this.baseURL}dish/recommendations/instant`, {
|
|
117
|
-
method: 'POST',
|
|
118
|
-
headers: {
|
|
119
|
-
'Content-Type': 'application/json',
|
|
120
|
-
'Authorization': `Bearer ${this.token}`
|
|
121
|
-
},
|
|
122
|
-
body: JSON.stringify({ store: this.store})
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
const data = await response.json();
|
|
126
|
-
|
|
127
|
-
if (!response.ok) {
|
|
128
|
-
throw new Error(`API call failed with status ${response.status}: ${data.message || 'No additional error information'}`);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
console.log('Instant recommendations:', data);
|
|
132
|
-
return data;
|
|
133
|
-
} catch (error) {
|
|
134
|
-
console.error('Fetching instant recommendations failed:', error.message);
|
|
135
|
-
throw new Error(`Error in getInstantRecommendations: ${error.message}`);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|