Coots commited on
Commit
397b7db
ยท
verified ยท
1 Parent(s): 416ce05

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +217 -165
index.html CHANGED
@@ -1,168 +1,220 @@
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"></div>
74
+ </div>
75
+ </div>
76
+ </div>
77
+
78
+ <!-- Main chatbot script -->
79
+ <script>
80
+ const state = {
81
+ step: 'tileType',
82
+ tileType: null,
83
+ area: null,
84
+ tileSize: null
85
+ };
86
+
87
+ const chatArea = document.getElementById('chat-area');
88
+ const userInput = document.getElementById('user-input');
89
+ const recommendations = document.getElementById('recommendations');
90
+ const resetBtn = document.getElementById('reset-btn');
91
+
92
+ resetBtn.addEventListener('click', resetConversation);
93
+ userInput.addEventListener('keypress', (e) => {
94
+ if (e.key === 'Enter') sendMessage();
95
+ });
96
+
97
+ function resetConversation() {
98
+ state.step = 'tileType';
99
+ state.tileType = null;
100
+ state.area = null;
101
+ state.tileSize = null;
102
+
103
+ chatArea.innerHTML = `
104
+ <div class="bot-message bg-gray-100 rounded-lg p-4 max-w-xs mb-3">
105
+ <p>Hello! ๐Ÿ‘‹ I'm your Tile Calculator Assistant. Let's estimate how many tiles you need.</p>
106
+ <p class="mt-2">Are you looking for <span class="font-semibold">floor</span> or <span class="font-semibold">wall</span> tiles?</p>
107
+ <div class="flex gap-2 mt-3">
108
+ <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>
109
+ <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>
110
+ </div>
111
+ </div>
112
+ `;
113
+ recommendations.innerHTML = '';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  }
115
 
116
+ function selectTileType(type) {
117
+ state.tileType = type;
118
+ state.step = 'area';
119
+
120
+ addMessage('user', type === 'floor' ? 'Floor tiles' : 'Wall tiles');
121
+ showTyping();
122
+
123
+ setTimeout(() => {
124
+ hideTyping();
125
+ addMessage('bot', Great choice for ${type} tiles! What's the total area you need to cover (in sq.ft)?);
126
+ }, 1000);
127
+ }
128
 
129
+ function sendMessage() {
130
+ const message = userInput.value.trim();
131
+ if (!message) return;
132
+
133
+ addMessage('user', message);
134
+ userInput.value = '';
135
+
136
+ processUserMessage(message);
137
+ }
138
+
139
+ function processUserMessage(message) {
140
+ showTyping();
141
+
142
+ setTimeout(() => {
143
+ hideTyping();
144
+
145
+ if (state.step === 'area') {
146
+ const area = parseFloat(message);
147
+ if (isNaN(area)) {
148
+ addMessage('bot', 'Please enter a valid number for the area (e.g. 120).');
149
+ return;
150
+ }
151
+
152
+ state.area = area;
153
+ state.step = 'tileSize';
154
+ addMessage('bot', 'Now enter the tile size (e.g. "2x2", "600x600 mm", or "200*200"):');
155
+
156
+ } else if (state.step === 'tileSize') {
157
+ const tileArea = parseTileSize(message);
158
+ if (!tileArea) {
159
+ addMessage('bot', 'I couldn\'t understand that tile size. Try: "2x2", "600x600 mm", or "200*200".');
160
+ return;
161
+ }
162
+
163
+ state.tileSize = tileArea;
164
+ calculateTiles();
165
+ }
166
+ }, 1000);
167
+ }
168
+
169
+ function parseTileSize(input) {
170
+ input = input.toLowerCase().replace(/ร—|into|\*/g, 'x').replace(/ft|feet|mm/g, '').trim();
171
+ if (input.includes('x')) {
172
+ const [a, b] = input.split('x').map(s => parseFloat(s.replace(/[^\d.]/g, '')));
173
+ if (isNaN(a) || isNaN(b)) return null;
174
+ return (a > 20 ? (a * b) / 92903.04 : a * b);
175
+ } else if (/^\d+(\.\d+)?$/.test(input)) {
176
+ const val = parseFloat(input);
177
+ return val > 20 ? (val * val) / 92903.04 : val * val;
178
+ }
179
+ return null;
180
+ }
181
+
182
+ function calculateTiles() {
183
+ const numTiles = Math.ceil((state.area / state.tileSize) * 1.1);
184
+ const numBoxes = Math.ceil(numTiles / 10);
185
+ chatArea.insertAdjacentHTML('beforeend', `
186
+ <div class="bot-message bg-gray-100 rounded-lg p-4 mb-3">
187
+ <p class="font-semibold">Calculation Results:</p>
188
+ <p>๐Ÿงฑ Tile Type: ${state.tileType}</p>
189
+ <p>๐Ÿ“ Area to Cover: ${state.area} sq.ft</p>
190
+ <p>๐Ÿงฎ Tile Size: ${state.tileSize.toFixed(2)} sq.ft per tile</p>
191
+ <p class="mt-2">๐Ÿ”ข <span class="font-bold">Tiles Needed:</span> ${numTiles} (${numBoxes} boxes)</p>
192
+ </div>
193
+ `);
194
+ state.step = 'complete';
195
+ }
196
+
197
+ function addMessage(sender, message) {
198
+ const messageDiv = document.createElement('div');
199
+ 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;
200
+ messageDiv.textContent = message;
201
+ chatArea.appendChild(messageDiv);
202
+ chatArea.scrollTop = chatArea.scrollHeight;
203
+ }
204
+
205
+ function showTyping() {
206
+ const typingDiv = document.createElement('div');
207
+ typingDiv.className = 'typing-indicator bg-gray-100 rounded-lg p-4 max-w-xs mb-3 flex gap-1';
208
+ typingDiv.id = 'typing-indicator';
209
+ 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>';
210
+ chatArea.appendChild(typingDiv);
211
+ chatArea.scrollTop = chatArea.scrollHeight;
212
+ }
213
+
214
+ function hideTyping() {
215
+ const typingIndicator = document.getElementById('typing-indicator');
216
+ if (typingIndicator) typingIndicator.remove();
217
+ }
218
+ </script>
219
+ </body>
220
+ </html>