wineater-bot 3.8.2 → 3.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wineater-bot",
3
- "version": "3.8.2",
3
+ "version": "3.9.0",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "module": "src/index.js",
@@ -49,52 +49,67 @@ function setUniqueUserCookie() {
49
49
  return userId;
50
50
  }
51
51
 
52
- // Функция для отправки событий в Mixpanel
53
- function trackEvent(event, properties) {
54
- const endpoint = 'https://wineater-backend.vercel.app/proxy/track';
55
- const data = {
56
- event,
57
- properties
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
58
68
  };
59
-
60
- fetch(endpoint, {
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
+ user_id,
84
+ session_id,
85
+ event_time,
86
+ data,
87
+ meta,
88
+ device_info,
89
+ region: navigator.language.split('-')[1] || null,
90
+ language: navigator.language.split('-')[0] || 'en'
91
+ };
92
+
93
+ return fetch(endpoint, {
61
94
  method: 'POST',
62
95
  headers: {
63
96
  'Content-Type': 'application/json'
64
97
  },
65
- body: JSON.stringify(data)
66
- }).then(response => {
98
+ body: JSON.stringify(payload)
99
+ })
100
+ .then(response => {
67
101
  if (!response.ok) {
68
- console.error('Mixpanel tracking failed', response);
102
+ return response.json().then(err => {
103
+ throw new Error(err.message || 'Failed to track widget event');
104
+ });
69
105
  }
70
- }).catch(error => {
71
- console.error('Mixpanel tracking error', error);
106
+ return response.json();
107
+ })
108
+ .catch(error => {
109
+ console.error('Widget event tracking error:', error);
110
+ throw error;
72
111
  });
73
112
  }
74
113
 
75
- function trackChatbotOpen() {
76
- trackEvent('Chatbot Opened', { distinct_id: getCookie('uniqueUserID'), client: getCookie('chatbotToken') });
77
- }
78
-
79
- function trackChatbotOpenAuto(url) {
80
- trackEvent('Chatbot Opened Auto', { distinct_id: getCookie('uniqueUserID'), url, client: getCookie('chatbotToken') });
81
- }
82
-
83
- function trackChatbotClose() {
84
- trackEvent('Chatbot Closed', { distinct_id: getCookie('uniqueUserID'), client: getCookie('chatbotToken') });
85
- }
86
-
87
- function trackMessageSent(message) {
88
- trackEvent('Message Sent', { distinct_id: getCookie('uniqueUserID'), message_content: message, client: getCookie('chatbotToken') });
89
- }
90
-
91
- function trackRecommendationsGet(products) {
92
- trackEvent('Products Got', { distinct_id: getCookie('uniqueUserID'), products: products, client: getCookie('chatbotToken') });
93
- }
94
-
95
- function trackProductClick(productId) {
96
- trackEvent('Product Clicked', { distinct_id: getCookie('uniqueUserID'), product_id: productId, client: getCookie('chatbotToken') });
97
- }
98
-
99
- export { setCookie, getCookie, setUniqueUserCookie, trackChatbotOpen, trackMessageSent, trackProductClick, trackChatbotClose, trackRecommendationsGet, trackChatbotOpenAuto, deleteCookie };
114
+ export { setCookie, getCookie, setUniqueUserCookie, deleteCookie, trackWidgetEvent };
100
115
 
package/src/js/api.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // api.js
2
2
 
3
- const stores = ['climats', 'telckel', 'biobio', 'weinpoint', 'vinigrandi', 'ampely', 'test'];
3
+ const stores = [];
4
4
  const DEMO_TOKEN='HeUw6IXuhvz6tRuXdPEGEbj3SGQ3Q2YtEc1Z8jd41Kg9LtyPb4Epu0to31SYIadG';
5
5
 
6
6
  export class Api {
package/src/js/i18n.js CHANGED
@@ -539,11 +539,14 @@ class Translator {
539
539
  }
540
540
 
541
541
  async fetchConfig(storeName) {
542
+ console.log('storeName', storeName);
542
543
  try {
543
544
  const config = await this.api.getStoreConfig(storeName);
544
545
  this.storeLanguage = config.lang;
546
+ this.storeId = config.id;
545
547
  this.supportedLanguages = config.available_languages;
546
548
  this.OpenAIApiKey = config.open_ai_key;
549
+ console.log('this.storeId', this.storeId);
547
550
 
548
551
  const urlParams = new URLSearchParams(window.location.search);
549
552
  const urlLang = urlParams.get('lang') || urlParams.get('language');
@@ -557,15 +560,21 @@ class Translator {
557
560
  }
558
561
 
559
562
  this.currency = config.currency || 'USD';
563
+ return config;
560
564
  } catch (e) {
561
565
  const urlParams = new URLSearchParams(window.location.search);
562
566
  const urlLang = urlParams.get('lang') || urlParams.get('language');
563
567
 
564
568
  this.language = this.translations[urlLang] ? urlLang : 'en';
565
569
  this.currency = 'USD';
570
+ return config;
566
571
  }
567
572
  }
568
573
 
574
+ getStoreId() {
575
+ return this.storeId;
576
+ }
577
+
569
578
  getCurrency(key) {
570
579
  const keys = key.split('.'); // Разбиваем ключ на части
571
580
  let value = this.currencies[this.currency];
@@ -0,0 +1,18 @@
1
+ // widget-events.js
2
+
3
+ export const WIDGET_EVENTS = {
4
+ WIDGET_INITIALIZED: 'widget_initialized',
5
+ WIDGET_LOADED: 'widget_loaded',
6
+ SEARCH_PERFORMED: 'search_performed',
7
+ SEARCH_RESULTS_DISPLAYED: 'search_results_displayed',
8
+ NO_RESULTS_FOUND: 'no_results_found',
9
+ WINE_CARD_HOVER: 'wine_card_hover',
10
+ WINE_CARD_CLICK: 'wine_card_click',
11
+ WINE_DETAILS_VIEWED: 'wine_details_viewed',
12
+ WINE_DETAILS_CLOSED: 'wine_details_closed',
13
+ BUY_BUTTON_CLICKED: 'buy_button_clicked',
14
+ FILTER_APPLIED: 'filter_applied',
15
+ LANGUAGE_CHANGED: 'language_changed',
16
+ ERROR_OCCURRED: 'error_occurred',
17
+ MOBILE_VIEW_ACTIVATED: 'mobile_view_activated'
18
+ };
@@ -0,0 +1,107 @@
1
+ // widget-language.js
2
+ import translator from '../i18n.js';
3
+ import { WIDGET_EVENTS } from './widget-events.js';
4
+
5
+ export class WidgetLanguage {
6
+ constructor(widget) {
7
+ this.widget = widget;
8
+ }
9
+
10
+ createLanguageSwitcher() {
11
+ const langContainer = document.querySelector('#powered-by-widget .powered-by-lang');
12
+ const languages = translator.getSupportedLanguages();
13
+ if (!languages) return;
14
+
15
+ const currentLanguage = translator.language;
16
+ const dropdown = document.createElement('select');
17
+ dropdown.id = 'language-dropdown';
18
+ dropdown.className = 'language-dropdown';
19
+
20
+ languages.forEach(lang => {
21
+ const option = document.createElement('option');
22
+ option.value = lang;
23
+ option.textContent = lang.toUpperCase();
24
+ option.selected = lang === currentLanguage;
25
+ dropdown.appendChild(option);
26
+ });
27
+
28
+ dropdown.onchange = (event) => {
29
+ const selectedLang = event.target.value;
30
+
31
+ this.widget.trackEvent(WIDGET_EVENTS.LANGUAGE_CHANGED, {
32
+ from_language: currentLanguage,
33
+ to_language: selectedLang
34
+ });
35
+
36
+ const url = new URL(window.location.href);
37
+ url.searchParams.set('lang', selectedLang);
38
+ localStorage.setItem('selectedLanguage', selectedLang);
39
+ window.location.href = url.toString();
40
+ };
41
+
42
+ langContainer.appendChild(dropdown);
43
+ }
44
+
45
+ async translatePopupContent(wines) {
46
+ if (translator.language === translator.storeLanguage || this.widget.widgetType !== 'bar') {
47
+ return;
48
+ }
49
+
50
+ const possibleFields = ['Short Description', 'reason', 'pairing_recommendation', 'Tags', 'tags'];
51
+ const firstWine = wines[0];
52
+ const fieldsToTranslate = possibleFields.filter(field => field in firstWine);
53
+ const batchSize = 4;
54
+
55
+ for (let i = 0; i < wines.length; i += batchSize) {
56
+ const batch = wines.slice(i, i + batchSize);
57
+
58
+ const translationPromises = batch.map(async (wine, index) => {
59
+ const globalIndex = i + index;
60
+ const entries = fieldsToTranslate
61
+ .map(field => {
62
+ const val = wine[field] || wine[field.toLowerCase()];
63
+ if (!val) return null;
64
+ return {
65
+ key: field,
66
+ text: Array.isArray(val) ? val.join(', ') : val
67
+ };
68
+ })
69
+ .filter(Boolean);
70
+
71
+ if (entries.length === 0) return;
72
+
73
+ const translatedArray = await translator.translateTexts(
74
+ Object.fromEntries(entries.map(entry => [entry.key, entry.text]))
75
+ );
76
+
77
+ const popup = document.querySelector(`.card-popup[data-wine-index="${globalIndex}"]`);
78
+ if (!popup) return;
79
+
80
+ if (translatedArray['Short Description']) {
81
+ const el = popup.querySelector('.popup-short-desc');
82
+ if (el) el.textContent = translatedArray['Short Description'];
83
+ }
84
+
85
+ if (translatedArray.reason) {
86
+ const el = popup.querySelector('.popup-reason');
87
+ if (el) el.textContent = translatedArray.reason;
88
+ }
89
+
90
+ if (translatedArray.pairing_recommendation) {
91
+ const el = popup.querySelector('.popup-pairing');
92
+ if (el) el.textContent = translatedArray.pairing_recommendation;
93
+ }
94
+
95
+ const tags = translatedArray.tags || translatedArray.Tags;
96
+ if (tags) {
97
+ const tagArray = tags.split(',').map(tag => tag.trim());
98
+ const tagHTML = tagArray.map(tag => `<span class="popup-tag">${tag}</span>`).join('');
99
+ const tagsContainer = popup.querySelector('.popup-tags');
100
+ if (tagsContainer) tagsContainer.innerHTML = tagHTML;
101
+ }
102
+ });
103
+
104
+ await Promise.all(translationPromises);
105
+ }
106
+ }
107
+ }
@@ -0,0 +1,68 @@
1
+ // widget-search.js
2
+ import { WIDGET_EVENTS } from './widget-events.js';
3
+
4
+ export class WidgetSearch {
5
+ constructor(widget) {
6
+ this.widget = widget;
7
+ // this.addEventListeners(); // Removed to avoid attaching listeners before DOM is ready
8
+ }
9
+
10
+ async handleSearch() {
11
+ const query = document.getElementById('wine-search-input').value.trim() + ';';
12
+
13
+ this.widget.trackEvent(WIDGET_EVENTS.SEARCH_PERFORMED, { query });
14
+ this.widget.ui.animateCardDisappearance();
15
+
16
+ try {
17
+ const results = await this.widget.api.getWineRecommendationsSearch(query);
18
+
19
+ if (results && results.length > 0) {
20
+ console.log('Recommendations:', results);
21
+ this.widget.displayResults(results, true);
22
+ this.widget.trackEvent(WIDGET_EVENTS.SEARCH_RESULTS_DISPLAYED, {
23
+ results_count: results.length,
24
+ results: results.map(result => result.Title || result.title),
25
+ query
26
+ });
27
+ } else {
28
+ console.log('No recommendations found.');
29
+ this.widget.ui.displayNoResultsMessage();
30
+ this.widget.trackEvent(WIDGET_EVENTS.NO_RESULTS_FOUND, { query });
31
+ }
32
+ } catch (error) {
33
+ console.error('Error fetching wine recommendations:', error.message);
34
+ this.widget.ui.displayErrorMessage();
35
+ this.widget.trackEvent(WIDGET_EVENTS.ERROR_OCCURRED, {
36
+ error_message: error.message,
37
+ query
38
+ });
39
+ }
40
+ }
41
+
42
+ async loadInstantRecommendations() {
43
+ this.widget.ui.showLoadingIndicator();
44
+ try {
45
+ const recommendations = await this.widget.api.getInstantRecommendations();
46
+ this.widget.displayResults(recommendations);
47
+ } catch (error) {
48
+ console.error('Error loading instant recommendations:', error.message);
49
+ }
50
+ }
51
+
52
+ addEventListeners() {
53
+ document
54
+ .getElementById('wine-search-button')
55
+ .addEventListener('click', () => {
56
+ this.handleSearch();
57
+ });
58
+
59
+ // Add Enter key support
60
+ document
61
+ .getElementById('wine-search-input')
62
+ .addEventListener('keydown', (e) => {
63
+ if (e.key === 'Enter') {
64
+ this.handleSearch();
65
+ }
66
+ });
67
+ }
68
+ }
@@ -0,0 +1,186 @@
1
+ // widget-ui.js
2
+ import translator from '../i18n.js';
3
+ import { WIDGET_EVENTS } from './widget-events.js';
4
+
5
+ export class WidgetUI {
6
+ constructor(widget) {
7
+ this.widget = widget;
8
+ }
9
+
10
+ getTitle(title) {
11
+ return title ?
12
+ `<h1 class="wine-widget-big-title">${title}</h1>` :
13
+ `<h1 class="wine-widget-big-title">
14
+ ${translator.translate('headerLine1')}
15
+ <span class="icon icon-meal"></span>
16
+ <span class="icon-label">${translator.translate('headerMeal')}</span>,
17
+ <span class="icon icon-mood"></span>
18
+ <span class="icon-label">${translator.translate('headerMood')}</span>
19
+ </br>
20
+ ${translator.translate('headerOr')}
21
+ <span class="icon icon-occasion"></span>
22
+ <span class="icon-label">${translator.translate('headerOccasion')}</span>
23
+ ${translator.translate('headerLine2')}
24
+ </h1>`;
25
+ }
26
+
27
+ initializeWidget() {
28
+ const widgetContainer = document.getElementById(this.widget.widgetWrapper);
29
+ const widgetHTML = `
30
+ <div id="wine-widget-container">
31
+ <div class="wine-widget-wrapper">
32
+ <div id="wine-widget-header">
33
+ ${this.getTitle(this.widget.titleText)}
34
+ </div>
35
+
36
+ <div id="wine-widget-search">
37
+ <span class="search-icon"></span>
38
+ <input
39
+ type="text"
40
+ id="wine-search-input"
41
+ placeholder="${translator.translate('searchPlaceholder')}"
42
+ >
43
+ <button id="wine-search-button">
44
+ ${translator.translate('searchButton')}
45
+ </button>
46
+ </div>
47
+
48
+ <div id="wine-widget-right">
49
+ <div id="loading-indicator" class="hidden">
50
+ <div id="typing-indicator" class="typing-indicator">
51
+ <span></span>
52
+ <span></span>
53
+ <span></span>
54
+ </div>
55
+ </div>
56
+ <div id="wine-placeholder"></div>
57
+ </div>
58
+
59
+ <div id="powered-by-widget">
60
+ <div class="powered-by-center">
61
+ <a href="https://wineater.com" target="_blank">
62
+ <span>${translator.translate('poweredBy')}</span>
63
+ <img
64
+ src="${this.widget.countryCode === 'ru'
65
+ ? 'https://czvgkhagwvmknscoerfy.supabase.co/storage/v1/object/public/static-media/divino_logo.svg?x=5'
66
+ : 'https://czvgkhagwvmknscoerfy.supabase.co/storage/v1/object/public/static-media/logo_medium_gray.svg'}"
67
+ alt="Wineater Logo"
68
+ >
69
+ <span class="powered-by-sommelier">${translator.translate('AI sommelier')}</span>
70
+ </a>
71
+ </div>
72
+ <div class="powered-by-lang"></div>
73
+ </div>
74
+ </div>
75
+ </div>
76
+ `;
77
+ widgetContainer.insertAdjacentHTML('beforeend', widgetHTML);
78
+ }
79
+
80
+ showLoadingIndicator() {
81
+ const loadingIndicator = document.getElementById('loading-indicator');
82
+ loadingIndicator.classList.remove('hidden');
83
+ }
84
+
85
+ hideLoadingIndicator() {
86
+ const loadingIndicator = document.getElementById('loading-indicator');
87
+ loadingIndicator.classList.add('hidden');
88
+ }
89
+
90
+ displayNoResultsMessage() {
91
+ const resultsContainer = document.getElementById('wine-placeholder');
92
+ resultsContainer.innerHTML = `
93
+ <div class="no-results">
94
+ <p>${translator.translate('noResultsMessage') || 'No recommendations found for your query.'}</p>
95
+ </div>
96
+ `;
97
+ }
98
+
99
+ displayErrorMessage() {
100
+ const resultsContainer = document.getElementById('wine-placeholder');
101
+ resultsContainer.innerHTML = `
102
+ <div class="error-message">
103
+ <p>${translator.translate('errorMessage') || 'Something went wrong. Please try again later.'}</p>
104
+ </div>
105
+ `;
106
+ }
107
+
108
+ animateCardDisappearance() {
109
+ const cards = document.querySelectorAll('.wine-recommendation-tile');
110
+ cards.forEach((card, index) => {
111
+ setTimeout(() => {
112
+ card.style.opacity = '0';
113
+ card.style.transform = 'translateY(-20px)';
114
+ }, index * 100);
115
+ });
116
+
117
+ setTimeout(() => {
118
+ document.getElementById('wine-placeholder').innerHTML = '';
119
+ this.showLoadingIndicator();
120
+ }, cards.length * 100 + 300);
121
+ }
122
+
123
+ initializeSearchAnimation() {
124
+ const searchInput = document.getElementById('wine-search-input');
125
+ const suggestions = translator.translate('placeholderSuggestions');
126
+ let index = 0;
127
+ let charIndex = 0;
128
+ let typingForward = true;
129
+ let isInputFocused = false;
130
+
131
+ const typeEffect = () => {
132
+ if (isInputFocused) return;
133
+
134
+ if (typingForward) {
135
+ searchInput.placeholder = suggestions[index].substring(0, charIndex++);
136
+ if (charIndex > suggestions[index].length) {
137
+ typingForward = false;
138
+ setTimeout(typeEffect, 2000);
139
+ } else {
140
+ setTimeout(typeEffect, 100);
141
+ }
142
+ } else {
143
+ searchInput.placeholder = suggestions[index].substring(0, charIndex--);
144
+ if (charIndex < 0) {
145
+ typingForward = true;
146
+ index = (index + 1) % suggestions.length;
147
+ setTimeout(typeEffect, 500);
148
+ } else {
149
+ setTimeout(typeEffect, 50);
150
+ }
151
+ }
152
+ };
153
+
154
+ typeEffect();
155
+
156
+ searchInput.addEventListener('focus', () => {
157
+ searchInput.placeholder = '';
158
+ isInputFocused = true;
159
+ });
160
+
161
+ searchInput.addEventListener('blur', () => {
162
+ isInputFocused = false;
163
+ charIndex = 0;
164
+ typingForward = true;
165
+ typeEffect();
166
+ });
167
+ }
168
+
169
+ enableHoverPopups() {
170
+ const tiles = document.querySelectorAll('.wine-recommendation-tile');
171
+ tiles.forEach(tile => {
172
+ const popup = tile.querySelector('.wine-hover-popup');
173
+ if (!popup) return;
174
+
175
+ tile.addEventListener('mouseenter', () => {
176
+ popup.style.display = 'block';
177
+ popup.classList.add('slide-in');
178
+ });
179
+
180
+ tile.addEventListener('mouseleave', () => {
181
+ popup.classList.remove('slide-in');
182
+ popup.style.display = 'none';
183
+ });
184
+ });
185
+ }
186
+ }