wineater-bot 3.2.0 → 3.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/src/js/widget.js CHANGED
@@ -2,7 +2,6 @@
2
2
  import './widget-styles.js';
3
3
  import { Api } from './api.js';
4
4
  import translator from './i18n.js';
5
- import { barConfig } from './barConfig.js';
6
5
 
7
6
  export class WineWidget {
8
7
  constructor() {
@@ -21,24 +20,12 @@ export class WineWidget {
21
20
  intensity: null,
22
21
  };
23
22
 
24
- // Если bar — берем название и список блюд из barConfig
25
- if (this.widgetType === 'bar') {
26
- this.barName = barConfig.barName; // "LENEZ"
27
- this.barDishes = barConfig.dishes; // Список блюд из barConfig
28
- } else {
29
- this.barName = '';
30
- this.barDishes = [];
31
- }
32
-
33
23
  // Инициализируем виджет
34
24
  this.initializeWidget();
35
25
  this.initializeSearchAnimation();
36
26
  this.sendEvent('widgetInitialized', { widgetType: this.widgetType });
27
+ this.loadInstantRecommendations();
37
28
 
38
- // Если нужна мгновенная загрузка рекомендаций только в store
39
- if (this.widgetType === 'store') {
40
- this.loadInstantRecommendations();
41
- }
42
29
  }
43
30
 
44
31
  sendEvent(eventType, data) {
@@ -65,7 +52,6 @@ export class WineWidget {
65
52
 
66
53
  initializeWidget() {
67
54
  const widgetContainer = document.getElementById(this.widgetWrapper);
68
- if (this.widgetType === 'store') {
69
55
  const widgetHTML = `
70
56
  <div id="wine-widget-container">
71
57
  <div class="wine-widget-wrapper">
@@ -108,57 +94,6 @@ export class WineWidget {
108
94
  `;
109
95
  widgetContainer.insertAdjacentHTML('beforeend', widgetHTML);
110
96
  this.addEventListenersStoreVersion();
111
- } else {
112
- const barHTML = `
113
- <div id="wine-widget-container" class="bar-widget">
114
- <div id="wine-widget-right">
115
- ${barConfig.dishGroups.map(group => `
116
- <!-- Обёртка группы -->
117
- <div class="dish-group">
118
- <!-- Заголовок группы, если нужен -->
119
- <!-- Сетка блюд конкретной группы -->
120
- <div class="dish-grid">
121
- ${group.dishes.map(dish => `
122
- <div class="dish-tile" data-dish="${dish}">
123
- ${dish}
124
- </div>
125
- `).join('')}
126
- </div>
127
- </div>
128
- `).join('')}
129
-
130
- <div id="wine-placeholder"></div>
131
-
132
- <div id="loading-indicator" class="hidden">
133
- <div id="typing-indicator" class="typing-indicator">
134
- <span></span><span></span><span></span>
135
- </div>
136
- </div>
137
-
138
- <!-- Кнопки для отправки и сброса -->
139
- <div id="bar-controls">
140
- <input
141
- type="text"
142
- id="bar-notes-input"
143
- placeholder="${translator.translate('barNotesPlaceholder')}"
144
- />
145
- <button id="bar-send-button">
146
- ${translator.translate('barSendButton')}
147
- </button>
148
- <button id="bar-reset-button" class="hidden">
149
- ${translator.translate('barResetButton')}
150
- </button>
151
- </div>
152
-
153
- </div>
154
- </div>
155
- `;
156
- widgetContainer.insertAdjacentHTML('beforeend', barHTML);
157
-
158
- // Привязка обработчиков "плиток" и кнопки
159
- this.addEventListenersBarVersion();
160
- }
161
- this.initializeSearchAnimation();
162
97
  }
163
98
 
164
99
  addEventListenersStoreVersion() {
@@ -267,122 +202,142 @@ export class WineWidget {
267
202
  }
268
203
 
269
204
  displayResults(wines, isAnimated = false) {
270
- const resultsContainer = document.getElementById('wine-placeholder');
271
- this.hideLoadingIndicator();
272
- resultsContainer.innerHTML = '';
273
-
274
- this.sendEvent('resultsDisplayed', { resultsCount: wines.length });
275
-
276
- wines.forEach((wine, index) => {
277
-
205
+ const resultsContainer = document.getElementById('wine-placeholder');
206
+ this.hideLoadingIndicator();
207
+ resultsContainer.innerHTML = '';
208
+
209
+ this.sendEvent('resultsDisplayed', { resultsCount: wines.length });
210
+
211
+ const getValue = (wine, key) => wine[key] || wine[key.charAt(0).toUpperCase() + key.slice(1)];
212
+ const joinParams = (wine, keys) =>
213
+ keys.map(key => getValue(wine, key)).filter(Boolean).join(' • ');
214
+
215
+ const createTagHTML = tags =>
216
+ Array.isArray(tags)
217
+ ? tags.map(tag => `<span class="popup-tag">${tag}</span>`).join('')
218
+ : '';
219
+
220
+ const createPopupHTML = (wine) => {
221
+ const tags = wine.tags || wine.Tags;
222
+ const text = wine.reason || wine['Short Description'];
223
+ if (!text && !tags) return '';
224
+ return `
225
+ <div class="wine-hover-popup">
226
+ ${text ? `<div class="popup-reason">${text}</div>` : ''}
227
+ ${tags ? `<div class="popup-tags">${createTagHTML(tags)}</div>` : ''}
228
+ </div>`;
229
+ };
230
+
231
+ wines.forEach((wine, index) => {
232
+ const title = getValue(wine, 'title');
233
+ const image = getValue(wine, 'image');
234
+ const price = getValue(wine, 'total');
235
+ const url = getValue(wine, 'url');
236
+ const tags = wine.tags || wine.Tags;
237
+ const line1 = joinParams(wine, ['type', 'country', 'region', 'grape']);
238
+ const tagsHTML = tags ? createTagHTML(tags) : '';
239
+ const reasonPopupHTML = createPopupHTML(wine);
240
+ const shortDescription = wine['Short Description'];
241
+ const pairing = wine.pairing_recommendation;
242
+ const isSomm = wine.isSommSelection;
243
+
244
+ const wineHTML = `
245
+ <div class="wine-recommendation-tile ${isSomm ? 'somm-selection' : ''}">
246
+ ${isSomm ? `<div class="somm-selection-badge">${translator.translate('wineDetails.sommSelection')}</div>` : ''}
247
+
248
+ <div class="tile-top">
249
+ ${image ? `<img src="${image}" alt="${title}" class="wine-image" />` : ''}
250
+ <div class="wine-price">${price} ${translator.getCurrency('label')}</div>
251
+ <h3 class="wine-title">${title}</h3>
252
+ </div>
253
+
254
+ ${line1 ? `<div class="wine-params">${line1}</div>` : ''}
255
+
256
+ <div class="bottom-row">
257
+ ${url
258
+ ? `<a class="buy-btn" href="${url}" target="_blank">${translator.translate('wineDetails.buy')}</a>`
259
+ : `<button class="buy-btn" data-toggle-info>${translator.translate('moreInfo')}</button>`
260
+ }
261
+ </div>
262
+
263
+ <div class="card-popup hidden">
264
+ <div class="card-popup-header">
265
+ <h2>${title}</h2>
266
+ <span class="card-popup-close">&times;</span>
267
+ </div>
268
+ ${shortDescription ? `<p>${shortDescription}</p>` : ''}
269
+ ${wine.reason ? `<div class="popup-reason">${wine.reason}</div>` : pairing ? `<p>${pairing}</p>` : ''}
270
+ ${tagsHTML ? `<div class="popup-tags">${tagsHTML}</div>` : ''}
271
+ </div>
272
+
273
+ ${reasonPopupHTML}
274
+ </div>
275
+ `;
276
+
277
+ resultsContainer.insertAdjacentHTML('beforeend', wineHTML);
278
+ const currentCard = resultsContainer.children[index];
279
+
280
+ if (!url) {
281
+ const buyBtn = currentCard.querySelector('[data-toggle-info]');
282
+ const popup = currentCard.querySelector('.card-popup');
283
+ const closeBtn = currentCard.querySelector('.card-popup-close');
284
+
285
+ closeBtn?.addEventListener('click', (e) => {
286
+ e.stopPropagation();
287
+ popup.classList.add('hidden');
288
+ });
278
289
 
279
- // А потом при генерации чипса:
280
- const paramChips = [];
281
- if (wine.type || wine.Type) {
282
- paramChips.push(wine.type || wine.Type);
290
+ document.addEventListener('click', (e) => {
291
+ const isInsideAnyPopup =
292
+ e.target.closest('.card-popup') ||
293
+ e.target.closest('.mobile-fullscreen-popup') ||
294
+ e.target.closest('[data-toggle-info]');
295
+
296
+ if (!isInsideAnyPopup) {
297
+ document.querySelectorAll('.card-popup').forEach(p => p.classList.add('hidden'));
298
+ document.querySelectorAll('.mobile-fullscreen-popup').forEach(p => {
299
+ const wrapper = p.closest('.wine-widget-context');
300
+ if (wrapper) wrapper.remove();
301
+ else p.remove();
302
+ });
283
303
  }
284
- if (wine.country || wine.Country) {
285
- paramChips.push(wine.country || wine.Country);
304
+ });
305
+
306
+ buyBtn?.addEventListener('click', (e) => {
307
+ e.stopPropagation();
308
+
309
+ // Закрыть все открытые popup'ы
310
+ document.querySelectorAll('.card-popup').forEach(p => p.classList.add('hidden'));
311
+ document.querySelectorAll('.mobile-fullscreen-wrapper').forEach(p => p.remove());
312
+
313
+ if (window.innerWidth <= 768) {
314
+ const popupContent = popup.innerHTML;
315
+
316
+ const overlay = document.createElement('div');
317
+ overlay.className = 'mobile-fullscreen-wrapper';
318
+ overlay.innerHTML = `
319
+ <div class="mobile-fullscreen-overlay"></div>
320
+ <div class="card-popup mobile-fullscreen-popup">${popupContent}</div>
321
+ `;
322
+
323
+ overlay.querySelector('.card-popup-close')?.addEventListener('click', () => overlay.remove());
324
+
325
+ document.body.appendChild(overlay);
326
+ } else {
327
+ popup.classList.remove('hidden');
286
328
  }
287
- if (wine.region || wine.Region) {
288
- paramChips.push(wine.region || wine.Region);
289
- };
290
- if (wine.grape || wine.Grape) {
291
- paramChips.push(wine.grape || wine.Grape);
292
- }
293
-
294
- const paramList = [];
295
- if (wine.type || wine.Type) paramList.push(wine.type || wine.Type);
296
- if (wine.country || wine.Country) paramList.push(wine.country || wine.Country);
297
- // if (wine.region || wine.Region) paramList.push(wine.region || wine.Region);
298
- // if (wine.grape || wine.Grape) paramList.push(wine.grape || wine.Grape);
299
-
300
- // Допустим, первые 3 идут в одну строку, а если есть 4‑й,
301
- // мы выведем его на вторую строку. Или всё в одну строку — на вкус.
302
- const line1 = paramList.join(' • ');
303
- // const line2 = paramList.slice(3).join(' • ');
304
- const tags = wine.tags || wine.Tags;
305
- const reasonPopupHTML = (wine.reason || tags)
306
- ? `
307
- <div class="wine-hover-popup">
308
- ${wine.reason ? `<div class="popup-reason">${wine.reason}</div>` : ''}
309
- ${
310
- tags
311
- ? `<div class="popup-tags">${
312
- Array.isArray(tags)
313
- // Для каждого тега делаем отдельный span
314
- ? tags.map(tag => `<span class="popup-tag">${tag}</span>`).join('')
315
- : tags
316
- }
317
- </div>`
318
- : ''
319
- }
320
- </div>`
321
- : '';
322
-
323
- const wineHTML = `
324
- <div class="wine-recommendation-tile ${wine.isSommSelection ? 'somm-selection' : ''}">
325
- ${
326
- wine.isSommSelection
327
- ? `<div class="somm-selection-badge">
328
- ${translator.translate('wineDetails.sommSelection')}
329
- </div>`
330
- : ''
331
- }
332
-
333
- <div class="tile-top">
334
- <!-- Большое изображение по центру -->
335
- ${
336
- (wine.image || wine.Image)
337
- ? `<img src="${wine.image || wine.Image}"
338
- alt="${wine.title || wine.Title}"
339
- class="wine-image" />`
340
- : ''
341
- }
342
-
343
- <!-- Цена над названием -->
344
- <div class="wine-price">
345
- ${wine.price || wine.Price}
346
- </div>
347
-
348
- <!-- Название вина (больше шрифт) -->
349
- <h2 class="wine-title">
350
- ${wine.title || wine.Title}
351
- </h2>
352
- </div>
353
-
354
- <!-- Параметры через точки -->
355
- ${
356
- line1
357
- ? `<div class="wine-params">${line1}</div>`
358
- : ''
329
+ });
359
330
  }
360
-
361
-
362
- <!-- Кнопка Buy, видна только при наведении -->
363
- <div class="bottom-row">
364
- <a class="buy-btn" href="${wine.url || wine.Url}" target="_blank">
365
- ${translator.translate('wineDetails.buy')}
366
- </a>
367
- </div>
368
-
369
- <!-- Добавляем HTML попапа -->
370
- ${reasonPopupHTML}
371
- </div>
372
- `;
373
- resultsContainer.insertAdjacentHTML('beforeend', wineHTML);
374
331
 
375
- // Найти только что добавленную карточку
376
- const currentCard = resultsContainer.children[index];
332
+ setTimeout(() => {
333
+ currentCard.style.opacity = '1';
334
+ currentCard.style.transform = 'translateY(0)';
335
+ }, isAnimated ? index * 200 : 0);
336
+ });
377
337
 
378
- // Анимация появления
379
- setTimeout(() => {
380
- currentCard.style.opacity = '1';
381
- currentCard.style.transform = 'translateY(0)';
382
- }, isAnimated ? index * 200 : 0); // Задержка между карточками
383
- });
384
-
338
+ if (this.widgetType === 'store') {
385
339
  this.enableHoverPopups();
340
+ }
386
341
  }
387
342
 
388
343
  animateCardDisappearance() {
@@ -437,102 +392,6 @@ export class WineWidget {
437
392
  console.error('Error loading instant recommendations:', error.message);
438
393
  }
439
394
  }
440
- addEventListenersBarVersion() {
441
- // Теперь плитки блюд появятся только после renderDishGroups()
442
- const tileElements = document.querySelectorAll('.dish-tile');
443
- tileElements.forEach(tile => {
444
- tile.addEventListener('click', () => {
445
- tile.classList.toggle('selected');
446
- });
447
- });
448
-
449
- // Собираем все группы (или все .dish-grid)
450
- const dishGroups = document.querySelectorAll('.dish-group');
451
-
452
- // Находим элементы управления
453
- const sendButton = document.getElementById('bar-send-button');
454
- const resetButton = document.getElementById('bar-reset-button');
455
- const notesInput = document.getElementById('bar-notes-input');
456
- const winePlaceholder = document.getElementById('wine-placeholder');
457
-
458
- if (!sendButton || !resetButton || !notesInput || !winePlaceholder) return;
459
-
460
- // Нажатие "Отправить"
461
- sendButton.addEventListener('click', async () => {
462
- dishGroups.forEach(g => g.classList.add('hidden'));
463
- this.showLoadingIndicator();
464
-
465
- // 1) Собираем выбранные блюда
466
- const selectedDishes = [];
467
- document.querySelectorAll('.dish-tile.selected').forEach(tile => {
468
- selectedDishes.push(tile.dataset.dish);
469
- });
470
-
471
- // 2) Берём пожелания (notes)
472
- const notes = notesInput.value.trim(); // не стираем и не скрываем поле
473
-
474
- // 3) Формируем запрос
475
- const queryParts = [];
476
- if (selectedDishes.length > 0) {
477
- queryParts.push(`Dishes: ${selectedDishes.join(', ')}`);
478
- }
479
- if (notes) {
480
- queryParts.push(`Notes: ${notes}`);
481
- }
482
- const finalQuery = queryParts.join('; ');
483
-
484
- console.log('Final query =>', finalQuery);
485
-
486
- this.sendEvent('barSearchPerformed', { query: finalQuery, selectedDishes, notes });
487
-
488
- try {
489
- // 4) Делаете запрос к API
490
- const results = await this.api.getWineRecommendationsSearch(finalQuery);
491
-
492
- // 5) Скрываем группы блюд (если хотите, чтобы больше не мешались)
493
-
494
- // 6) Отображаем результаты
495
- if (results && results.length > 0) {
496
- this.displayResults(results, true);
497
- } else {
498
- this.displayNoResultsMessage();
499
- }
500
-
501
- // 7) Показываем кнопку «Сбросить»
502
- resetButton.classList.remove('hidden');
503
-
504
- // 8) Кнопку «Отправить» НЕ скрываем — оставляем!
505
- // ...никакого sendButton.classList.add('hidden')
506
- } catch (error) {
507
- console.error('Error while fetching bar recommendations:', error);
508
- this.displayErrorMessage();
509
- } finally {
510
- this.hideLoadingIndicator();
511
- }
512
- });
513
-
514
- // Нажатие "Сбросить"
515
- resetButton.addEventListener('click', () => {
516
- // 1) Показываем все группы (блюда)
517
- dishGroups.forEach(g => g.classList.remove('hidden'));
518
-
519
- // 2) Снимаем выделение с плиток
520
- tileElements.forEach(tile => tile.classList.remove('selected'));
521
-
522
- // 3) Очищаем результаты
523
- winePlaceholder.innerHTML = '';
524
-
525
- // 4) Прячем кнопку «Сбросить»
526
- resetButton.classList.add('hidden');
527
-
528
- notesInput.value = '';
529
-
530
- // 5) Инпут с пожеланиями НЕ трогаем — пусть остается, пользователь может менять
531
- // notesInput.value = ''; // Если захотите — можете раскомментить, чтобы тоже сбрасывать
532
-
533
- // 6) «Отправить» и дальше на месте, не меняется
534
- });
535
- }
536
395
 
537
396
  initializeSearchAnimation() {
538
397
  const searchInput = document.getElementById('wine-search-input');
@@ -1,239 +0,0 @@
1
- @font-face {
2
- font-family: 'Poppins';
3
- src: url('https://fonts.googleapis.com/css2?family=Poppins:wght@400&display=swap');
4
- }
5
-
6
- /* Overall chatbot styles */
7
- #chatbot-container {
8
- position: fixed;
9
- bottom: 20px;
10
- right: 20px;
11
- font-family: 'Poppins', sans-serif; /* Use Poppins as the main font */
12
- z-index: 1000; /* Ensure chat is above other content */
13
- }
14
-
15
- /* Styling the chat icon */
16
- #chat-icon {
17
- display: flex; /* Use flexbox for alignment */
18
- align-items: center; /* Center items vertically */
19
- justify-content: center; /* Center items horizontally */
20
- padding: 8px; /* Add some padding around the text and image */
21
- border-radius: 20px; /* Rounded corners for the button */
22
- background-color: #7E27ED; /* Background color from your style guide */
23
- cursor: pointer;
24
- box-shadow: 0 2px 10px rgba(0,0,0,0.2); /* Optional: add shadow for depth */
25
-
26
- }
27
-
28
- #chat-icon img {
29
- width: 80%; /* Adjust icon size as needed */
30
- height: auto;
31
- margin-right: 8px;
32
- }
33
-
34
- #chat-icon span {
35
- color: white; /* Text color */
36
- font-family: 'Poppins', sans-serif; /* Font family from your style guide */
37
- /* Add more text styles if needed */
38
- }
39
-
40
- /* Chat window styles */
41
- #chat-window {
42
- width: 350px; /* Width of the chat window */
43
- background-color: white;
44
- border-radius: 8px;
45
- box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
46
- position: absolute;
47
- bottom: 80px; /* Positioning from the bottom */
48
- right: 0;
49
- display: none; /* Initially hidden */
50
- }
51
-
52
- /* Header of the chat window */
53
- #chat-header {
54
- background-color: #7E27ED;
55
- color: white;
56
- padding: 16px;
57
- border-top-left-radius: 8px;
58
- border-top-right-radius: 8px;
59
- display: flex;
60
- justify-content: space-between;
61
- align-items: center;
62
- position: relative; /* Needed for absolute positioning of the close button */
63
- }
64
-
65
-
66
- /* Style for the close button */
67
- #close-btn {
68
- font-size: 24px; /* Size of the close button */
69
- cursor: pointer;
70
- padding: 0 10px;
71
- line-height: 24px;
72
- }
73
-
74
- /* Container for messages */
75
- #message-container {
76
- max-height: 300px;
77
- overflow-y: auto;
78
- padding: 15px;
79
- background: #f7f7f7; /* Light grey background for the message area */
80
- }
81
-
82
- /* Input area */
83
- #input-container {
84
- border-top: 2px solid #ececec; /* Light border for input area */
85
- background-color: white; /* White background for input area */
86
- padding: 12px;
87
- display: flex;
88
- align-items: center; /* Align button and input vertically */
89
- gap: 10px; /* Adds space between input and button */
90
- }
91
-
92
- #input-container input {
93
- flex-grow: 1;
94
- border: 2px solid #B928EB; /* Input border color based on style guide */
95
- padding: 10px;
96
- border-radius: 4px;
97
- font-family: 'Poppins', sans-serif; /* Use Poppins font */
98
- }
99
-
100
- #input-container button {
101
- padding: 10px 20px; /* Larger padding for a larger button */
102
- background-color: #5F82FF; /* Button background color from style guide */
103
- border: none;
104
- color: white;
105
- border-radius: 4px;
106
- font-family: 'Poppins', sans-serif; /* Use Poppins font */
107
- font-size: 1rem; /* Adjust font size as needed */
108
- cursor: pointer;
109
- box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); /* Add subtle shadow for depth */
110
- transition: background-color 0.3s ease; /* Smooth transition for background color */
111
- }
112
-
113
- #input-container button:hover {
114
- background-color: #2FC0BF; /* Hover color based on style guide */
115
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); /* Darker shadow for hover state */
116
- }
117
-
118
- /* Message bubble styling */
119
- .message {
120
- margin-bottom: 8px;
121
- padding: 12px;
122
- background-color: #e0e0e0; /* Light grey for message bubbles */
123
- border-radius: 16px;
124
- font-size: 14px;
125
- line-height: 1.4;
126
- }
127
-
128
- .message.user {
129
- background-color: #B928EB; /* User message bubble color */
130
- color: white;
131
- align-self: flex-end;
132
- }
133
-
134
- .message.api {
135
- background-color: #e0e0e0; /* API message bubble color */
136
- color: black;
137
- align-self: flex-start;
138
- }
139
-
140
- /* Wine item styling */
141
- .wine-item {
142
- display: flex;
143
- align-items: center;
144
- border: 1px solid #ddd;
145
- border-radius: 8px;
146
- padding: 10px;
147
- margin-top: 10px;
148
- background: white; /* White background for wine items */
149
- }
150
-
151
- .wine-item img {
152
- width: 80px; /* Smaller wine image size */
153
- height: auto;
154
- border-radius: 4px; /* Rounded corners for images */
155
- margin-right: 12px;
156
- }
157
-
158
- .wine-item div {
159
- font-size: 0.9rem; /* Smaller font size for wine info */
160
- }
161
-
162
- /* Ensure the chat window is visible when the 'visible' class is added */
163
- #chat-window.visible {
164
- display: block;
165
- }
166
-
167
- /* Loader Styles */
168
- .loader {
169
- display: none; /* Loader is hidden by default */
170
- border: 4px solid #f3f3f3; /* Light grey */
171
- border-top: 4px solid #3498db; /* Blue, could be your primary color */
172
- border-radius: 50%;
173
- width: 40px;
174
- height: 40px;
175
- animation: spin 2s linear infinite;
176
- margin: 20px auto; /* Centered horizontally */
177
- }
178
-
179
- .loader.visible {
180
- display: block;
181
- }
182
-
183
- @keyframes spin {
184
- 0% { transform: rotate(0deg); }
185
- 100% { transform: rotate(360deg); }
186
- }
187
-
188
- /* Typing indicator styles */
189
- .typing-indicator {
190
- display: flex;
191
- justify-content: center;
192
- align-items: center;
193
- height: 0; /* Adjust as needed */
194
- visibility: hidden; /* Start off invisible */
195
- }
196
-
197
- .typing-indicator span {
198
- display: block;
199
- width: 8px;
200
- height: 8px;
201
- margin: 0 4px;
202
- background-color: #3498db; /* Or use a color from your style guide */
203
- border-radius: 50%;
204
- opacity: 0.4;
205
- animation: typing 1.5s infinite;
206
- }
207
-
208
- /* Animation keyframes */
209
- @keyframes typing {
210
- 0% { transform: translateY(0); }
211
- 20% { transform: translateY(-10px); }
212
- 40% { transform: translateY(0); }
213
- 60% { transform: translateY(-10px); }
214
- 80% { transform: translateY(0); }
215
- 100% { transform: translateY(0); }
216
- }
217
-
218
- /* To stagger the animation for each dot */
219
- .typing-indicator span:nth-child(1) {
220
- animation-delay: 0s;
221
- }
222
- .typing-indicator span:nth-child(2) {
223
- animation-delay: 0.2s;
224
- }
225
- .typing-indicator span:nth-child(3) {
226
- animation-delay: 0.4s;
227
- }
228
-
229
- /* Show the typing indicator */
230
- .typing-indicator.visible {
231
- visibility: visible;
232
- height: 50px;
233
- }
234
-
235
- #message-container {
236
- overflow-y: auto;
237
- height: 300px; /* adjust based on your UI needs */
238
- padding: 10px;
239
- }