Coots commited on
Commit
9f9b6af
·
verified ·
1 Parent(s): 75e2b50

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +311 -168
index.html CHANGED
@@ -1,168 +1,311 @@
1
- from flask import Flask, request, jsonify, send_file
2
- from flask_cors import CORS
3
- import joblib
4
- import xgboost as xgb
5
- import numpy as np
6
- import json
7
- import math
8
- import os
9
- import logging
10
-
11
- app = Flask(__name__)
12
- CORS(app)
13
- logging.basicConfig(level=logging.INFO)
14
-
15
- # Load models
16
- try:
17
- rf = joblib.load("rf_model.pkl")
18
- xgb_model = xgb.Booster()
19
- xgb_model.load_model("xgb_model.json")
20
- app.logger.info("✅ Models loaded successfully.")
21
- except Exception as e:
22
- app.logger.error(f"❌ Error loading models: {e}")
23
- raise e
24
-
25
- # Load tile metadata
26
- with open("tile_catalog.json", "r", encoding="utf-8") as f:
27
- tile_catalog = json.load(f)
28
-
29
- with open("tile_sizes.json", "r", encoding="utf-8") as f:
30
- tile_sizes = json.load(f)
31
-
32
- @app.route("/")
33
- def home():
34
- return send_file("index.html")
35
-
36
- @app.route('/recommend', methods=['POST'])
37
- def recommend():
38
- try:
39
- data = request.get_json()
40
-
41
- required = ['tile_type', 'coverage', 'area', 'price_range']
42
- if not all(k in data for k in required):
43
- return jsonify({"error": "Missing required fields"}), 400
44
-
45
- tile_type = data['tile_type'].lower()
46
- if tile_type not in ['floor', 'wall']:
47
- return jsonify({"error": "Invalid tile type"}), 400
48
-
49
- validate_positive_number(data['coverage'], "coverage")
50
- validate_positive_number(data['area'], "area")
51
-
52
- pr = data['price_range']
53
- if (not isinstance(pr, list) or len(pr) != 2 or pr[0] < 0 or pr[1] <= 0 or pr[0] >= pr[1]):
54
- return jsonify({"error": "Invalid price range"}), 400
55
-
56
- features = prepare_features(data)
57
- xgb_pred = float(xgb_model.predict(xgb.DMatrix(features))[0])
58
- rf_pred = float(rf.predict_proba(features)[0][1])
59
- combined_score = (xgb_pred + rf_pred) / 2
60
-
61
- recommended = filter_products(
62
- tile_type=tile_type,
63
- min_price=pr[0],
64
- max_price=pr[1],
65
- preferred_sizes=data.get("preferred_sizes", []),
66
- min_score=0.5
67
- )
68
-
69
- return jsonify({
70
- "recommendation_score": round(combined_score, 3),
71
- "total_matches": len(recommended),
72
- "recommended_products": recommended[:5],
73
- "calculation": calculate_requirements(data['area'], data['coverage'])
74
- })
75
-
76
- except Exception as e:
77
- app.logger.error(f"Error in /recommend: {e}")
78
- return jsonify({"error": "Internal server error"}), 500
79
-
80
- @app.route('/calculate', methods=['POST'])
81
- def calculate():
82
- try:
83
- data = request.get_json()
84
-
85
- for k in ['tile_type', 'area', 'tile_size']:
86
- if k not in data:
87
- return jsonify({"error": f"Missing field: {k}"}), 400
88
-
89
- tile_type = data['tile_type'].lower()
90
- if tile_type not in ['floor', 'wall']:
91
- return jsonify({"error": "Invalid tile type"}), 400
92
-
93
- tile_size_key = data['tile_size']
94
- if tile_size_key not in tile_sizes:
95
- return jsonify({"error": "Invalid tile size"}), 400
96
-
97
- validate_positive_number(data['area'], "area")
98
-
99
- tile_info = tile_sizes[tile_size_key]
100
- area_per_tile = tile_info['length'] * tile_info['width']
101
- tiles_needed = math.ceil((data['area'] / area_per_tile) * 1.1)
102
- tiles_per_box = tile_info.get('tiles_per_box', 10)
103
- boxes_needed = math.ceil(tiles_needed / tiles_per_box)
104
-
105
- matching = [
106
- p for p in tile_catalog
107
- if p['type'].lower() == tile_type and p['size'] == tile_size_key
108
- ]
109
-
110
- return jsonify({
111
- "tile_type": tile_type,
112
- "area": data['area'],
113
- "tile_size": tile_size_key,
114
- "tiles_needed": tiles_needed,
115
- "boxes_needed": boxes_needed,
116
- "matching_products": matching[:3],
117
- "total_matches": len(matching)
118
- })
119
-
120
- except Exception as e:
121
- app.logger.error(f"Error in /calculate: {e}")
122
- return jsonify({"error": "Internal server error"}), 500
123
-
124
- def prepare_features(data):
125
- tile_type_num = 0 if data['tile_type'] == 'floor' else 1
126
- price_per_sqft = data['price_range'][1] / data['coverage']
127
- budget_efficiency = data['coverage'] / data['price_range'][1]
128
-
129
- return np.array([[
130
- tile_type_num,
131
- data['area'],
132
- data['coverage'],
133
- data['price_range'][0],
134
- data['price_range'][1],
135
- price_per_sqft,
136
- budget_efficiency
137
- ]])
138
-
139
- def filter_products(tile_type, min_price, max_price, preferred_sizes, min_score=0.5):
140
- results = []
141
- for p in tile_catalog:
142
- if (
143
- p['type'].lower() == tile_type and
144
- min_price <= p['price'] <= max_price and
145
- (not preferred_sizes or p['size'] in preferred_sizes)
146
- ):
147
- price_score = 1 - ((p['price'] - min_price) / (max_price - min_price + 1e-6))
148
- size_score = 1 if not preferred_sizes or p['size'] in preferred_sizes else 0.5
149
- score = (price_score + size_score) / 2
150
- if score >= min_score:
151
- results.append({**p, "recommendation_score": round(score, 2)})
152
- return sorted(results, key=lambda x: x['recommendation_score'], reverse=True)
153
-
154
- def calculate_requirements(area, coverage):
155
- min_tiles = math.ceil(area / coverage)
156
- suggested = math.ceil(min_tiles * 1.1)
157
- return {
158
- "minimum_tiles": min_tiles,
159
- "suggested_tiles": suggested,
160
- "estimated_cost_range": [round(area * 3, 2), round(area * 10, 2)]
161
- }
162
-
163
- def validate_positive_number(value, field):
164
- if not isinstance(value, (int, float)) or value <= 0:
165
- raise ValueError(f"{field} must be a positive number")
166
-
167
- if __name__ == '__main__':
168
- app.run(host="0.0.0.0", port=5000, debug=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>