Coots commited on
Commit
3ea94bc
·
verified ·
1 Parent(s): d12096d

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +171 -292
index.html CHANGED
@@ -1,311 +1,190 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Tile Calculator AI</title>
7
- <script src="https://cdn.tailwindcss.com"></script>
8
- <style>
9
- .chat-container {
10
- height: 80vh;
11
- background-image: radial-gradient(circle at 1px 1px, #e5e7eb 1px, transparent 0);
12
- background-size: 20px 20px;
13
- }
14
- .typing-indicator span {
15
- animation: bounce 1.5s infinite ease-in-out;
16
- }
17
- @keyframes bounce {
18
- 0%, 60%, 100% { transform: translateY(0); }
19
- 30% { transform: translateY(-5px); }
20
- }
21
- </style>
22
- </head>
23
- <body class="bg-gray-100">
24
- <div class="max-w-4xl mx-auto p-4">
25
- <div class="bg-white rounded-xl shadow-lg overflow-hidden">
26
- <!-- Header -->
27
- <div class="bg-indigo-600 text-white p-4 flex justify-between items-center">
28
- <div class="flex items-center space-x-3">
29
- <div class="w-10 h-10 bg-white rounded-full flex items-center justify-center">
30
- <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-indigo-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
31
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
32
- </svg>
33
- </div>
34
- <div>
35
- <h1 class="font-bold text-xl">Tile Calculator AI</h1>
36
- <p class="text-xs opacity-80">Powered by your models</p>
37
- </div>
38
  </div>
39
- <button id="reset-btn" class="p-2 rounded-full hover:bg-indigo-700 transition">
40
- <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
41
- <path fill-rule="evenodd" d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z" clip-rule="evenodd" />
42
- </svg>
43
- </button>
44
  </div>
 
 
 
45
 
46
- <!-- Chat Area -->
47
- <div class="chat-container p-4 overflow-y-auto" id="chat-area">
48
- <div class="bot-message bg-gray-100 rounded-lg p-4 max-w-xs mb-3">
49
- <p>Hello! 👋 I'm your Tile Calculator Assistant. Let's estimate how many tiles you need.</p>
50
- <p class="mt-2">Are you looking for <span class="font-semibold">floor</span> or <span class="font-semibold">wall</span> tiles?</p>
51
- <div class="flex gap-2 mt-3">
52
- <button onclick="selectTileType('floor')" class="quick-reply bg-white text-indigo-600 border border-indigo-600 px-4 py-2 rounded-full font-medium hover:bg-indigo-50">Floor</button>
53
- <button onclick="selectTileType('wall')" class="quick-reply bg-white text-indigo-600 border border-indigo-600 px-4 py-2 rounded-full font-medium hover:bg-indigo-50">Wall</button>
54
- </div>
55
- </div>
56
- </div>
57
 
58
- <!-- Input Area -->
59
- <div class="border-t p-4 bg-white">
60
- <div class="flex gap-2">
61
- <input type="text" id="user-input" placeholder="Type your message..." class="flex-1 border rounded-full px-4 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500">
62
- <button onclick="sendMessage()" class="bg-indigo-600 text-white rounded-full p-2 hover:bg-indigo-700 transition">
63
- <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
64
- <path d="M10.894 2.553a1 1 0 00-1.788 0l-7 14a1 1 0 001.169 1.409l5-1.429A1 1 0 009 15.571V11a1 1 0 112 0v4.571a1 1 0 00.725.962l5 1.428a1 1 0 001.17-1.408l-7-14z" />
65
- </svg>
66
- </button>
67
- </div>
68
- </div>
69
 
70
- <!-- Recommendations Section -->
71
- <div class="bg-gray-50 p-4 border-t">
72
- <h3 class="font-semibold text-gray-800 mb-3">Recommended Products</h3>
73
- <div class="grid grid-cols-2 md:grid-cols-4 gap-4" id="recommendations">
74
- <!-- Products will be loaded here -->
75
- </div>
76
- </div>
77
- </div>
78
- </div>
79
 
80
- <script>
81
- // Conversation state
82
- const state = {
83
- step: 'tileType',
84
- tileType: null,
85
- area: null,
86
- tileSize: null
87
- };
88
 
89
- // DOM elements
90
- const chatArea = document.getElementById('chat-area');
91
- const userInput = document.getElementById('user-input');
92
- const recommendations = document.getElementById('recommendations');
93
- const resetBtn = document.getElementById('reset-btn');
94
 
95
- // Event listeners
96
- resetBtn.addEventListener('click', resetConversation);
97
- userInput.addEventListener('keypress', (e) => {
98
- if (e.key === 'Enter') sendMessage();
99
- });
100
 
101
- // Reset conversation
102
- function resetConversation() {
103
- state.step = 'tileType';
104
- state.tileType = null;
105
- state.area = null;
106
- state.tileSize = null;
107
-
108
- chatArea.innerHTML = `
109
- <div class="bot-message bg-gray-100 rounded-lg p-4 max-w-xs mb-3">
110
- <p>Hello! 👋 I'm your Tile Calculator Assistant. Let's estimate how many tiles you need.</p>
111
- <p class="mt-2">Are you looking for <span class="font-semibold">floor</span> or <span class="font-semibold">wall</span> tiles?</p>
112
- <div class="flex gap-2 mt-3">
113
- <button onclick="selectTileType('floor')" class="quick-reply bg-white text-indigo-600 border border-indigo-600 px-4 py-2 rounded-full font-medium hover:bg-indigo-50">Floor</button>
114
- <button onclick="selectTileType('wall')" class="quick-reply bg-white text-indigo-600 border border-indigo-600 px-4 py-2 rounded-full font-medium hover:bg-indigo-50">Wall</button>
115
- </div>
116
- </div>
117
- `;
118
- recommendations.innerHTML = '';
119
- }
120
-
121
- // Select tile type
122
- function selectTileType(type) {
123
- state.tileType = type;
124
- state.step = 'area';
125
-
126
- addMessage('user', type === 'floor' ? 'Floor tiles' : 'Wall tiles');
127
- showTyping();
128
-
129
- setTimeout(() => {
130
- hideTyping();
131
- addMessage('bot', Great choice for ${type} tiles! What's the total area you need to cover (in sq.ft)?);
132
- }, 1000);
133
- }
134
 
135
- // Send message handler
136
- function sendMessage() {
137
- const message = userInput.value.trim();
138
- if (!message) return;
139
-
140
- addMessage('user', message);
141
- userInput.value = '';
142
-
143
- processUser Message(message);
144
- }
145
 
146
- // Process user message based on current step
147
- function processUser Message(message) {
148
- showTyping();
149
-
150
- setTimeout(() => {
151
- hideTyping();
152
-
153
- if (state.step === 'area') {
154
- const area = parseFloat(message);
155
- if (isNaN(area)) {
156
- addMessage('bot', 'Please enter a valid number for the area (e.g. 120).');
157
- return;
158
- }
159
-
160
- state.area = area;
161
- state.step = 'tileSize';
162
- addMessage('bot', 'Now enter the tile size (e.g. "2x2", "600x600 mm", or "200*200"):');
163
-
164
- } else if (state.step === 'tileSize') {
165
- const tileArea = parseTileSize(message);
166
- if (!tileArea) {
167
- addMessage('bot', 'I couldn\'t understand that tile size. Try: "2x2", "600x600 mm", or "200*200".');
168
- return;
169
- }
170
-
171
- state.tileSize = tileArea;
172
- calculateTiles();
173
  }
174
- }, 1000);
175
- }
176
 
177
- // Parse tile size input
178
- function parseTileSize(input) {
179
- input = input.toLowerCase()
180
- .replace('×', 'x')
181
- .replace('into', 'x')
182
- .replace('*', 'x')
183
- .replace('ft', '')
184
- .replace('feet', '')
185
- .replace('mm', '')
186
- .trim();
187
-
188
- if (input.includes('x')) {
189
- const parts = input.split('x');
190
- if (parts.length === 2) {
191
- const val1 = parseFloat(parts[0].replace(/[^\d.]/g, ''));
192
- const val2 = parseFloat(parts[1].replace(/[^\d.]/g, ''));
193
-
194
- if (isNaN(val1) || isNaN(val2)) return null;
195
-
196
- // If values > 20, assume mm, else feet
197
- return val1 > 20 ?
198
- (val1 * val2) / 92903.04 : // mm² to ft²
199
- val1 * val2; // ft²
200
  }
201
- } else if (/^\d+(\.\d+)?$/.test(input)) {
202
- const val = parseFloat(input);
203
- return val > 20 ?
204
- (val * val) / 92903.04 : // mm² to ft²
205
- val * val; // ft²
206
- }
207
-
208
- return null;
209
- }
210
 
211
- // Calculate tiles and show results
212
- function calculateTiles() {
213
- const numTiles = Math.ceil((state.area / state.tileSize) * 1.1); // 10% buffer
214
- const numBoxes = Math.ceil(numTiles / 10); // Assuming 10 tiles per box
215
-
216
- let result = `
217
- <div class="bot-message bg-gray-100 rounded-lg p-4 mb-3">
218
- <p class="font-semibold">Calculation Results:</p>
219
- <p>🧱 Tile Type: ${state.tileType}</p>
220
- <p>📐 Area to Cover: ${state.area} sq.ft</p>
221
- <p>🧮 Tile Size: ${state.tileSize.toFixed(2)} sq.ft per tile</p>
222
- <p class="mt-2">🔢 <span class="font-bold">Tiles Needed:</span> ${numTiles} (${numBoxes} boxes)</p>
223
- </div>
224
- `;
225
-
226
- chatArea.insertAdjacentHTML('beforeend', result);
227
- state.step = 'complete';
228
-
229
- // Get recommendations
230
- fetchRecommendations();
231
- }
232
-
233
- // Fetch product recommendations from Flask backend
234
- function fetchRecommendations() {
235
- fetch('/recommend', {
236
- method: 'POST',
237
- headers: {
238
- 'Content-Type': 'application/json',
239
- },
240
- body: JSON.stringify({
241
- tile_type: state.tileType,
242
- coverage: state.tileSize,
243
- area: state.area,
244
- price_range: [3, 10] // Example price range
245
- })
246
- })
247
- .then(response => response.json())
248
- .then(data => {
249
- if (data.recommended_products && data.recommended_products.length > 0) {
250
- loadProductRecommendations(data.recommended_products);
251
- } else {
252
- recommendations.innerHTML = `
253
- <div class="col-span-4 text-center py-4 text-gray-500">
254
- No strong recommendations available for these parameters.
255
- </div>
256
- `;
257
- }
258
- })
259
- .catch(error => {
260
- console.error('Error fetching recommendations:', error);
261
- });
262
- }
263
-
264
- // Load product recommendations
265
- function loadProductRecommendations(products) {
266
- recommendations.innerHTML = '';
267
-
268
- products.slice(0, 4).forEach(product => {
269
- const productCard = document.createElement('div');
270
- productCard.className = 'bg-white rounded-lg overflow-hidden shadow-sm border border-gray-100';
271
- productCard.innerHTML = `
272
- <div class="h-32 bg-gray-200 flex items-center justify-center">
273
- <svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
274
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
275
- </svg>
276
- </div>
277
- <div class="p-3">
278
- <h4 class="font-medium text-gray-800">${product.name || 'Tile Product'}</h4>
279
- <p class="text-sm text-gray-600 mt-1">${product.price || '$0.00'}/box</p>
280
- <button class="mt-2 w-full bg-indigo-600 text-white py-1 rounded text-sm hover:bg-indigo-700 transition">View Details</button>
281
- </div>
282
- `;
283
- recommendations.appendChild(productCard);
284
- });
285
  }
286
 
287
- // Helper functions
288
- function addMessage(sender, message) {
289
- const messageDiv = document.createElement('div');
290
- messageDiv.className = ${sender}-message ${sender === 'user' ? 'ml-auto bg-indigo-600 text-white' : 'bg-gray-100'} rounded-lg p-4 max-w-xs mb-3;
291
- messageDiv.textContent = message;
292
- chatArea.appendChild(messageDiv);
293
- chatArea.scrollTop = chatArea.scrollHeight;
294
- }
295
 
296
- function showTyping() {
297
- const typingDiv = document.createElement('div');
298
- typingDiv.className = 'typing-indicator bg-gray-100 rounded-lg p-4 max-w-xs mb-3 flex gap-1';
299
- typingDiv.id = 'typing-indicator';
300
- typingDiv.innerHTML = '<span class="w-2 h-2 bg-gray-400 rounded-full"></span><span class="w-2 h-2 bg-gray-400 rounded-full"></span><span class="w-2 h-2 bg-gray-400 rounded-full"></span>';
301
- chatArea.appendChild(typingDiv);
302
- chatArea.scrollTop = chatArea.scrollHeight;
303
- }
304
 
305
- function hideTyping() {
306
- const typingIndicator = document.getElementById('typing-indicator');
307
- if (typingIndicator) typingIndicator.remove();
308
- }
309
- </script>
310
- </body>
311
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script>
2
+ const state = {
3
+ step: 'tileType',
4
+ tileType: null,
5
+ area: null,
6
+ tileSize: null
7
+ };
8
+
9
+ const chatArea = document.getElementById('chat-area');
10
+ const userInput = document.getElementById('user-input');
11
+ const recommendations = document.getElementById('recommendations');
12
+ const resetBtn = document.getElementById('reset-btn');
13
+
14
+ resetBtn.addEventListener('click', resetConversation);
15
+ userInput.addEventListener('keypress', (e) => {
16
+ if (e.key === 'Enter') sendMessage();
17
+ });
18
+
19
+ function resetConversation() {
20
+ state.step = 'tileType';
21
+ state.tileType = null;
22
+ state.area = null;
23
+ state.tileSize = null;
24
+
25
+ chatArea.innerHTML = `
26
+ <div class="bot-message bg-gray-100 rounded-lg p-4 max-w-xs mb-3">
27
+ <p>Hello! 👋 I'm your Tile Calculator Assistant. Let's estimate how many tiles you need.</p>
28
+ <p class="mt-2">Are you looking for <span class="font-semibold">floor</span> or <span class="font-semibold">wall</span> tiles?</p>
29
+ <div class="flex gap-2 mt-3">
30
+ <button onclick="selectTileType('floor')" class="quick-reply bg-white text-indigo-600 border border-indigo-600 px-4 py-2 rounded-full font-medium hover:bg-indigo-50">Floor</button>
31
+ <button onclick="selectTileType('wall')" class="quick-reply bg-white text-indigo-600 border border-indigo-600 px-4 py-2 rounded-full font-medium hover:bg-indigo-50">Wall</button>
 
 
 
 
 
 
32
  </div>
 
 
 
 
 
33
  </div>
34
+ `;
35
+ recommendations.innerHTML = '';
36
+ }
37
 
38
+ function selectTileType(type) {
39
+ state.tileType = type;
40
+ state.step = 'area';
 
 
 
 
 
 
 
 
41
 
42
+ addMessage('user', type === 'floor' ? 'Floor tiles' : 'Wall tiles');
43
+ showTyping();
 
 
 
 
 
 
 
 
 
44
 
45
+ setTimeout(() => {
46
+ hideTyping();
47
+ addMessage('bot', `Great choice for ${type} tiles! What's the total area you need to cover (in sq.ft)?`);
48
+ }, 1000);
49
+ }
 
 
 
 
50
 
51
+ function sendMessage() {
52
+ const message = userInput.value.trim();
53
+ if (!message) return;
 
 
 
 
 
54
 
55
+ addMessage('user', message);
56
+ userInput.value = '';
 
 
 
57
 
58
+ processUserMessage(message);
59
+ }
 
 
 
60
 
61
+ function processUserMessage(message) {
62
+ showTyping();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
 
64
+ setTimeout(() => {
65
+ hideTyping();
 
 
 
 
 
 
 
 
66
 
67
+ if (state.step === 'area') {
68
+ const area = parseFloat(message);
69
+ if (isNaN(area)) {
70
+ addMessage('bot', 'Please enter a valid number for the area (e.g. 120).');
71
+ return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  }
 
 
73
 
74
+ state.area = area;
75
+ state.step = 'tileSize';
76
+ addMessage('bot', 'Now enter the tile size (e.g. "2x2", "600x600 mm", or "200*200"):');
77
+ } else if (state.step === 'tileSize') {
78
+ const tileArea = parseTileSize(message);
79
+ if (!tileArea) {
80
+ addMessage('bot', 'I couldn\'t understand that tile size. Try: "2x2", "600x600 mm", or "200*200".');
81
+ return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  }
 
 
 
 
 
 
 
 
 
83
 
84
+ state.tileSize = tileArea;
85
+ calculateTiles();
86
+ }
87
+ }, 1000);
88
+ }
89
+
90
+ function parseTileSize(input) {
91
+ input = input.toLowerCase()
92
+ .replace(/×|into|\*/g, 'x')
93
+ .replace(/ft|feet|mm/g, '')
94
+ .trim();
95
+
96
+ if (input.includes('x')) {
97
+ const [a, b] = input.split('x').map(s => parseFloat(s.replace(/[^\d.]/g, '')));
98
+ if (isNaN(a) || isNaN(b)) return null;
99
+ return (a > 20 ? (a * b) / 92903.04 : a * b);
100
+ } else if (/^\d+(\.\d+)?$/.test(input)) {
101
+ const val = parseFloat(input);
102
+ return val > 20 ? (val * val) / 92903.04 : val * val;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  }
104
 
105
+ return null;
106
+ }
 
 
 
 
 
 
107
 
108
+ function calculateTiles() {
109
+ const numTiles = Math.ceil((state.area / state.tileSize) * 1.1);
110
+ const numBoxes = Math.ceil(numTiles / 10);
 
 
 
 
 
111
 
112
+ const result = `
113
+ <div class="bot-message bg-gray-100 rounded-lg p-4 mb-3">
114
+ <p class="font-semibold">Calculation Results:</p>
115
+ <p>🧱 Tile Type: ${state.tileType}</p>
116
+ <p>📐 Area to Cover: ${state.area} sq.ft</p>
117
+ <p>🧮 Tile Size: ${state.tileSize.toFixed(2)} sq.ft per tile</p>
118
+ <p class="mt-2">🔢 <span class="font-bold">Tiles Needed:</span> ${numTiles} (${numBoxes} boxes)</p>
119
+ </div>
120
+ `;
121
+ chatArea.insertAdjacentHTML('beforeend', result);
122
+ state.step = 'complete';
123
+ fetchRecommendations();
124
+ }
125
+
126
+ function fetchRecommendations() {
127
+ fetch('/recommend', {
128
+ method: 'POST',
129
+ headers: { 'Content-Type': 'application/json' },
130
+ body: JSON.stringify({
131
+ tile_type: state.tileType,
132
+ coverage: state.tileSize,
133
+ area: state.area,
134
+ price_range: [3, 10]
135
+ })
136
+ })
137
+ .then(res => res.json())
138
+ .then(data => {
139
+ if (data.recommended_products?.length > 0) {
140
+ loadProductRecommendations(data.recommended_products);
141
+ } else {
142
+ recommendations.innerHTML = `<div class="col-span-4 text-center py-4 text-gray-500">No strong recommendations available for these parameters.</div>`;
143
+ }
144
+ })
145
+ .catch(err => console.error('Error fetching recommendations:', err));
146
+ }
147
+
148
+ function loadProductRecommendations(products) {
149
+ recommendations.innerHTML = '';
150
+ products.slice(0, 4).forEach(product => {
151
+ const card = document.createElement('div');
152
+ card.className = 'bg-white rounded-lg overflow-hidden shadow-sm border border-gray-100';
153
+ card.innerHTML = `
154
+ <div class="h-32 bg-gray-200 flex items-center justify-center">
155
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
156
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
157
+ </svg>
158
+ </div>
159
+ <div class="p-3">
160
+ <h4 class="font-medium text-gray-800">${product.name || 'Tile Product'}</h4>
161
+ <p class="text-sm text-gray-600 mt-1">${product.price || '$0.00'}/box</p>
162
+ <button class="mt-2 w-full bg-indigo-600 text-white py-1 rounded text-sm hover:bg-indigo-700 transition">View Details</button>
163
+ </div>
164
+ `;
165
+ recommendations.appendChild(card);
166
+ });
167
+ }
168
+
169
+ function addMessage(sender, message) {
170
+ const messageDiv = document.createElement('div');
171
+ messageDiv.className = `${sender}-message ${sender === 'user' ? 'ml-auto bg-indigo-600 text-white' : 'bg-gray-100'} rounded-lg p-4 max-w-xs mb-3`;
172
+ messageDiv.textContent = message;
173
+ chatArea.appendChild(messageDiv);
174
+ chatArea.scrollTop = chatArea.scrollHeight;
175
+ }
176
+
177
+ function showTyping() {
178
+ const typingDiv = document.createElement('div');
179
+ typingDiv.className = 'typing-indicator bg-gray-100 rounded-lg p-4 max-w-xs mb-3 flex gap-1';
180
+ typingDiv.id = 'typing-indicator';
181
+ typingDiv.innerHTML = '<span class="w-2 h-2 bg-gray-400 rounded-full"></span><span class="w-2 h-2 bg-gray-400 rounded-full"></span><span class="w-2 h-2 bg-gray-400 rounded-full"></span>';
182
+ chatArea.appendChild(typingDiv);
183
+ chatArea.scrollTop = chatArea.scrollHeight;
184
+ }
185
+
186
+ function hideTyping() {
187
+ const typingIndicator = document.getElementById('typing-indicator');
188
+ if (typingIndicator) typingIndicator.remove();
189
+ }
190
+ </script>