muhalwan commited on
Commit
55acd9b
·
1 Parent(s): 8c20f44

add housing and UI

Browse files
Files changed (5) hide show
  1. app.py +42 -2
  2. index.html +74 -27
  3. requirements.txt +3 -1
  4. static/script.js +76 -53
  5. static/style.css +147 -110
app.py CHANGED
@@ -4,6 +4,7 @@ os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
4
 
5
  import logging
6
  import io
 
7
  from pathlib import Path
8
  from fastapi import FastAPI, File, UploadFile, HTTPException
9
  from fastapi.staticfiles import StaticFiles
@@ -49,16 +50,38 @@ def load_models():
49
  cat_dog_model = None
50
  logging.error(f"Error loading cat & dog model: {e}")
51
 
52
- return tokenizer, sentiment_model, cat_dog_model
 
 
 
 
 
 
 
 
 
 
 
 
53
 
54
  # --- FastAPI App Initialization ---
55
  app = FastAPI()
56
- tokenizer, sentiment_model, cat_dog_model = load_models()
57
  app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
58
 
59
  class SentimentRequest(BaseModel):
60
  text: str
61
 
 
 
 
 
 
 
 
 
 
 
62
  # --- API Endpoints ---
63
  @app.get("/")
64
  async def read_root():
@@ -101,3 +124,20 @@ async def predict_catdog(file: UploadFile = File(...)):
101
  except Exception as e:
102
  logging.error(f"Cat/Dog prediction error: {e}")
103
  raise HTTPException(status_code=500, detail="An error occurred during image classification.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
  import logging
6
  import io
7
+ import pickle
8
  from pathlib import Path
9
  from fastapi import FastAPI, File, UploadFile, HTTPException
10
  from fastapi.staticfiles import StaticFiles
 
50
  cat_dog_model = None
51
  logging.error(f"Error loading cat & dog model: {e}")
52
 
53
+ try:
54
+ xgb_model_path = hf_hub_download(repo_id="muhalwan/california_housing_price_predictor", filename="xgb_model.pkl")
55
+ with open(xgb_model_path, "rb") as f:
56
+ housing_model = pickle.load(f)
57
+ scaler_path = hf_hub_download(repo_id="muhalwan/california_housing_price_predictor", filename="scaler.pkl")
58
+ with open(scaler_path, "rb") as f:
59
+ housing_scaler = pickle.load(f)
60
+ logging.info("Housing price model and scaler loaded successfully.")
61
+ except Exception as e:
62
+ housing_model, housing_scaler = None, None
63
+ logging.error(f"Error loading housing price model: {e}")
64
+
65
+ return tokenizer, sentiment_model, cat_dog_model, housing_model, housing_scaler
66
 
67
  # --- FastAPI App Initialization ---
68
  app = FastAPI()
69
+ tokenizer, sentiment_model, cat_dog_model, housing_model, housing_scaler = load_models()
70
  app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
71
 
72
  class SentimentRequest(BaseModel):
73
  text: str
74
 
75
+ class HousingRequest(BaseModel):
76
+ MedInc: float
77
+ HouseAge: float
78
+ AveRooms: float
79
+ AveBedrms: float
80
+ Population: float
81
+ AveOccup: float
82
+ Latitude: float
83
+ Longitude: float
84
+
85
  # --- API Endpoints ---
86
  @app.get("/")
87
  async def read_root():
 
124
  except Exception as e:
125
  logging.error(f"Cat/Dog prediction error: {e}")
126
  raise HTTPException(status_code=500, detail="An error occurred during image classification.")
127
+
128
+ @app.post("/predict/housing")
129
+ async def predict_housing(request: HousingRequest):
130
+ if not housing_model or not housing_scaler:
131
+ raise HTTPException(status_code=503, detail="Housing model is not available.")
132
+ try:
133
+ input_data = np.array([[
134
+ request.MedInc, request.HouseAge, request.AveRooms, request.AveBedrms,
135
+ request.Population, request.AveOccup, request.Latitude, request.Longitude
136
+ ]])
137
+ data_scaled = housing_scaler.transform(input_data)
138
+ raw_prediction = housing_model.predict(data_scaled)[0]
139
+ final_prediction = raw_prediction * 100000
140
+ return {"prediction": f"${final_prediction:,.2f}"}
141
+ except Exception as e:
142
+ logging.error(f"Housing prediction error: {e}")
143
+ raise HTTPException(status_code=500, detail="An error occurred during housing price prediction.")
index.html CHANGED
@@ -3,7 +3,7 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>ML Model Hub</title>
7
  <link rel="stylesheet" href="/static/style.css">
8
  <link rel="preconnect" href="https://fonts.googleapis.com">
9
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
@@ -12,44 +12,91 @@
12
  <body>
13
  <div class="container">
14
  <header>
15
- <h1>ML Model Hub</h1>
16
- <p>Select a model to get started</p>
17
  </header>
18
 
19
- <div class="model-selector">
20
- <button id="cat-dog-btn" class="model-btn">Cat & Dog Classifier</button>
21
  <button id="sentiment-btn" class="model-btn active">Sentiment Analysis</button>
22
- <button class="model-btn disabled" disabled>California House Price (soon)</button>
23
- </div>
 
24
 
25
- <main id="main-content">
26
- <!-- Sentiment Analysis Interface -->
27
- <div id="sentiment-interface" class="model-interface active">
 
 
28
  <form id="sentiment-form">
29
- <h2>Tweet Sentiment Analysis</h2>
30
- <p>Enter a tweet to analyze its sentiment (Bullish, Bearish, or Neutral).</p>
31
- <label for="tweet-text">Tweet Text</label>
32
- <textarea id="tweet-text" placeholder="Enter tweet here..."></textarea>
33
- <button type="submit" class="predict-btn">Predict</button>
34
  </form>
35
- <div id="sentiment-result" class="result-display"></div>
36
  </div>
37
 
38
- <!-- Cat & Dog Classifier Interface -->
39
- <div id="cat-dog-interface" class="model-interface">
40
- <form id="cat-dog-form">
41
- <h2>Cat & Dog Classifier</h2>
42
- <p>Upload an image to classify if it's a cat or a dog.</p>
43
- <label for="image-upload" class="upload-label">Choose an image...</label>
44
- <input type="file" id="image-upload" accept="image/jpeg, image/png">
45
- <div id="image-preview"></div>
46
- <button type="submit" class="predict-btn">Predict</button>
 
47
  </form>
48
- <div id="cat-dog-result" class="result-display"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  </div>
50
  </main>
51
- </div>
52
 
 
 
 
 
53
  <script src="/static/script.js"></script>
54
  </body>
55
  </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>ML Nexus</title>
7
  <link rel="stylesheet" href="/static/style.css">
8
  <link rel="preconnect" href="https://fonts.googleapis.com">
9
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
 
12
  <body>
13
  <div class="container">
14
  <header>
15
+ <h1>ML Nexus</h1>
16
+ <p>Your one-stop hub for ML model inference.</p>
17
  </header>
18
 
19
+ <nav class="model-selector">
 
20
  <button id="sentiment-btn" class="model-btn active">Sentiment Analysis</button>
21
+ <button id="catdog-btn" class="model-btn">Cat & Dog Classifier</button>
22
+ <button id="housing-btn" class="model-btn">House Price Predictor</button>
23
+ </nav>
24
 
25
+ <main>
26
+ <!-- Sentiment Analysis Section -->
27
+ <div id="sentiment-section" class="model-section active">
28
+ <h2>Sentiment Analysis <span class="beta-tag">Beta</span></h2>
29
+ <p>Enter text to classify its sentiment (Bearish or Bullish). This model is in beta, and its accuracy may vary.</p>
30
  <form id="sentiment-form">
31
+ <textarea id="sentiment-text" name="text" rows="4" placeholder="Enter text here..."></textarea>
32
+ <button type="submit">Predict Sentiment</button>
 
 
 
33
  </form>
34
+ <div id="sentiment-result" class="result"></div>
35
  </div>
36
 
37
+ <!-- Cat & Dog Classifier Section -->
38
+ <div id="catdog-section" class="model-section">
39
+ <h2>Cat & Dog Classifier</h2>
40
+ <p>Upload an image to classify it as a cat or a dog.</p>
41
+ <form id="catdog-form">
42
+ <input type="file" id="catdog-file" name="file" accept="image/*">
43
+ <div class="image-preview-container">
44
+ <img id="image-preview" src="#" alt="Image Preview" class="image-preview"/>
45
+ </div>
46
+ <button type="submit">Classify Image</button>
47
  </form>
48
+ <div id="catdog-result" class="result"></div>
49
+ </div>
50
+
51
+ <!-- California Housing Price Predictor Section -->
52
+ <div id="housing-section" class="model-section">
53
+ <h2>California House Price Predictor</h2>
54
+ <p>Enter the details below to predict the median house value.</p>
55
+ <form id="housing-form">
56
+ <div class="form-grid">
57
+ <div class="form-group">
58
+ <label for="MedInc">Median Income</label>
59
+ <input type="number" step="any" id="MedInc" name="MedInc" placeholder="e.g., 8.3252" required>
60
+ </div>
61
+ <div class="form-group">
62
+ <label for="HouseAge">House Age</label>
63
+ <input type="number" step="any" id="HouseAge" name="HouseAge" placeholder="e.g., 41.0" required>
64
+ </div>
65
+ <div class="form-group">
66
+ <label for="AveRooms">Average Rooms</label>
67
+ <input type="number" step="any" id="AveRooms" name="AveRooms" placeholder="e.g., 6.9841" required>
68
+ </div>
69
+ <div class="form-group">
70
+ <label for="AveBedrms">Average Bedrooms</label>
71
+ <input type="number" step="any" id="AveBedrms" name="AveBedrms" placeholder="e.g., 1.0238" required>
72
+ </div>
73
+ <div class="form-group">
74
+ <label for="Population">Population</label>
75
+ <input type="number" step="any" id="Population" name="Population" placeholder="e.g., 322.0" required>
76
+ </div>
77
+ <div class="form-group">
78
+ <label for="AveOccup">Average Occupancy</label>
79
+ <input type="number" step="any" id="AveOccup" name="AveOccup" placeholder="e.g., 2.5555" required>
80
+ </div>
81
+ <div class="form-group">
82
+ <label for="Latitude">Latitude</label>
83
+ <input type="number" step="any" id="Latitude" name="Latitude" placeholder="e.g., 37.88" required>
84
+ </div>
85
+ <div class="form-group">
86
+ <label for="Longitude">Longitude</label>
87
+ <input type="number" step="any" id="Longitude" name="Longitude" placeholder="e.g., -122.23" required>
88
+ </div>
89
+ </div>
90
+ <button type="submit">Predict Price</button>
91
+ </form>
92
+ <div id="housing-result" class="result"></div>
93
  </div>
94
  </main>
 
95
 
96
+ <footer>
97
+ <p>&copy; 2025 ML Nexus. All rights reserved.</p>
98
+ </footer>
99
+ </div>
100
  <script src="/static/script.js"></script>
101
  </body>
102
  </html>
requirements.txt CHANGED
@@ -10,4 +10,6 @@ Pillow
10
  types-pytz
11
  fastapi
12
  uvicorn[standard]
13
- python-multipart
 
 
 
10
  types-pytz
11
  fastapi
12
  uvicorn[standard]
13
+ python-multipart
14
+ scikit-learn
15
+ xgboost
static/script.js CHANGED
@@ -1,47 +1,49 @@
1
  document.addEventListener('DOMContentLoaded', () => {
 
2
  const sentimentBtn = document.getElementById('sentiment-btn');
3
- const catDogBtn = document.getElementById('cat-dog-btn');
4
- const sentimentInterface = document.getElementById('sentiment-interface');
5
- const catDogInterface = document.getElementById('cat-dog-interface');
6
 
 
 
 
 
 
 
 
 
7
  const sentimentForm = document.getElementById('sentiment-form');
8
- const catDogForm = document.getElementById('cat-dog-form');
9
- const tweetText = document.getElementById('tweet-text');
10
- const imageUpload = document.getElementById('image-upload');
11
- const imagePreview = document.getElementById('image-preview');
12
 
13
  const sentimentResult = document.getElementById('sentiment-result');
14
- const catDogResult = document.getElementById('cat-dog-result');
 
15
 
16
- function switchModelView(activeBtn, activeInterface) {
17
- [sentimentBtn, catDogBtn].forEach(btn => btn.classList.remove('active'));
18
- activeBtn.classList.add('active');
19
-
20
- [sentimentInterface, catDogInterface].forEach(intf => intf.classList.remove('active'));
21
- activeInterface.classList.add('active');
 
 
 
 
 
22
  }
23
 
24
- sentimentBtn.addEventListener('click', () => switchModelView(sentimentBtn, sentimentInterface));
25
- catDogBtn.addEventListener('click', () => switchModelView(catDogBtn, catDogInterface));
 
26
 
27
- imageUpload.addEventListener('change', () => {
28
- const file = imageUpload.files[0];
29
- if (file) {
30
- const reader = new FileReader();
31
- reader.onload = (e) => {
32
- imagePreview.innerHTML = `<img src="${e.target.result}" alt="Image preview">`;
33
- };
34
- reader.readAsDataURL(file);
35
- }
36
- });
37
 
 
38
  sentimentForm.addEventListener('submit', async (e) => {
39
  e.preventDefault();
40
- const text = tweetText.value.trim();
41
- if (!text) return;
42
-
43
- showResult(sentimentResult, 'Analyzing...', 'loading');
44
-
45
  try {
46
  const response = await fetch('/predict/sentiment', {
47
  method: 'POST',
@@ -49,42 +51,63 @@ document.addEventListener('DOMContentLoaded', () => {
49
  body: JSON.stringify({ text })
50
  });
51
  const data = await response.json();
52
- if (!response.ok) throw new Error(data.detail || 'Prediction failed');
53
-
54
- showResult(sentimentResult, `Prediction: ${data.prediction}`, 'success');
55
  } catch (error) {
56
- showResult(sentimentResult, `Error: ${error.message}`, 'error');
57
  }
58
  });
59
 
60
- catDogForm.addEventListener('submit', async (e) => {
 
61
  e.preventDefault();
62
- const file = imageUpload.files[0];
63
- if (!file) return;
64
-
65
- showResult(catDogResult, 'Classifying...', 'loading');
66
-
67
- const formData = new FormData();
68
- formData.append('file', file);
69
-
70
  try {
71
  const response = await fetch('/predict/catdog', {
72
  method: 'POST',
73
  body: formData
74
  });
75
  const data = await response.json();
76
- if (!response.ok) throw new Error(data.detail || 'Classification failed');
 
 
 
 
 
77
 
78
- showResult(catDogResult, `Prediction: ${data.prediction}`, 'success');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  } catch (error) {
80
- showResult(catDogResult, `Error: ${error.message}`, 'error');
81
  }
82
  });
83
 
84
- function showResult(element, message, type) {
85
- element.innerHTML = message;
86
- element.className = 'result-display';
87
- element.classList.add(type);
88
- element.style.display = 'block';
89
- }
 
 
 
 
 
 
 
 
90
  });
 
1
  document.addEventListener('DOMContentLoaded', () => {
2
+ // Model selection buttons and sections
3
  const sentimentBtn = document.getElementById('sentiment-btn');
4
+ const catdogBtn = document.getElementById('catdog-btn');
5
+ const housingBtn = document.getElementById('housing-btn');
 
6
 
7
+ const sentimentSection = document.getElementById('sentiment-section');
8
+ const catdogSection = document.getElementById('catdog-section');
9
+ const housingSection = document.getElementById('housing-section');
10
+
11
+ const sections = [sentimentSection, catdogSection, housingSection];
12
+ const buttons = [sentimentBtn, catdogBtn, housingBtn];
13
+
14
+ // Forms and result displays
15
  const sentimentForm = document.getElementById('sentiment-form');
16
+ const catdogForm = document.getElementById('catdog-form');
17
+ const housingForm = document.getElementById('housing-form');
 
 
18
 
19
  const sentimentResult = document.getElementById('sentiment-result');
20
+ const catdogResult = document.getElementById('catdog-result');
21
+ const housingResult = document.getElementById('housing-result');
22
 
23
+ const imagePreview = document.getElementById('image-preview');
24
+ const catdogFileInput = document.getElementById('catdog-file');
25
+
26
+ // --- Event Listeners for Model Switching ---
27
+ function switchTab(activeIndex) {
28
+ buttons.forEach((button, index) => {
29
+ button.classList.toggle('active', index === activeIndex);
30
+ });
31
+ sections.forEach((section, index) => {
32
+ section.classList.toggle('active', index === activeIndex);
33
+ });
34
  }
35
 
36
+ sentimentBtn.addEventListener('click', () => switchTab(0));
37
+ catdogBtn.addEventListener('click', () => switchTab(1));
38
+ housingBtn.addEventListener('click', () => switchTab(2));
39
 
40
+ // --- Form Submission Handlers ---
 
 
 
 
 
 
 
 
 
41
 
42
+ // Sentiment Form
43
  sentimentForm.addEventListener('submit', async (e) => {
44
  e.preventDefault();
45
+ const text = document.getElementById('sentiment-text').value;
46
+ sentimentResult.textContent = 'Analyzing...';
 
 
 
47
  try {
48
  const response = await fetch('/predict/sentiment', {
49
  method: 'POST',
 
51
  body: JSON.stringify({ text })
52
  });
53
  const data = await response.json();
54
+ if (!response.ok) throw new Error(data.detail || 'An unknown error occurred.');
55
+ sentimentResult.textContent = `Prediction: ${data.prediction}`;
 
56
  } catch (error) {
57
+ sentimentResult.textContent = `Error: ${error.message}`;
58
  }
59
  });
60
 
61
+ // Cat & Dog Form
62
+ catdogForm.addEventListener('submit', async (e) => {
63
  e.preventDefault();
64
+ const formData = new FormData(catdogForm);
65
+ catdogResult.textContent = 'Classifying...';
 
 
 
 
 
 
66
  try {
67
  const response = await fetch('/predict/catdog', {
68
  method: 'POST',
69
  body: formData
70
  });
71
  const data = await response.json();
72
+ if (!response.ok) throw new Error(data.detail || 'An unknown error occurred.');
73
+ catdogResult.textContent = `Prediction: ${data.prediction}`;
74
+ } catch (error) {
75
+ catdogResult.textContent = `Error: ${error.message}`;
76
+ }
77
+ });
78
 
79
+ // Housing Form
80
+ housingForm.addEventListener('submit', async (e) => {
81
+ e.preventDefault();
82
+ const formData = new FormData(housingForm);
83
+ const data = Object.fromEntries(formData.entries());
84
+ housingResult.textContent = 'Predicting...';
85
+ try {
86
+ const response = await fetch('/predict/housing', {
87
+ method: 'POST',
88
+ headers: { 'Content-Type': 'application/json' },
89
+ body: JSON.stringify(data)
90
+ });
91
+ const result = await response.json();
92
+ if (!response.ok) throw new Error(result.detail || 'An unknown error occurred.');
93
+ housingResult.textContent = `Predicted Price: ${result.prediction}`;
94
  } catch (error) {
95
+ housingResult.textContent = `Error: ${error.message}`;
96
  }
97
  });
98
 
99
+ // Image Preview Handler
100
+ catdogFileInput.addEventListener('change', () => {
101
+ const file = catdogFileInput.files[0];
102
+ if (file) {
103
+ const reader = new FileReader();
104
+ reader.onload = (e) => {
105
+ imagePreview.src = e.target.result;
106
+ imagePreview.style.display = 'block';
107
+ };
108
+ reader.readAsDataURL(file);
109
+ } else {
110
+ imagePreview.style.display = 'none';
111
+ }
112
+ });
113
  });
static/style.css CHANGED
@@ -1,194 +1,231 @@
 
1
  body {
2
- font-family: 'Inter', sans-serif;
3
- background-color: #f0f2f5;
4
- color: #333;
5
  margin: 0;
6
- padding: 20px;
7
  display: flex;
8
  justify-content: center;
9
- align-items: flex-start;
10
  min-height: 100vh;
 
11
  }
12
 
13
  .container {
 
 
 
 
14
  width: 100%;
15
  max-width: 800px;
16
- background-color: #ffffff;
17
- border-radius: 8px;
18
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
19
  overflow: hidden;
20
  }
21
 
22
  header {
23
  text-align: center;
24
- padding: 24px;
25
- border-bottom: 1px solid #e8e8e8;
26
  }
27
 
28
  header h1 {
29
- margin: 0 0 8px 0;
30
- font-size: 28px;
31
  font-weight: 700;
32
- color: #1a73e8;
33
  }
34
 
35
  header p {
36
- margin: 0;
37
- color: #666;
 
38
  }
39
 
 
40
  .model-selector {
41
  display: flex;
42
  justify-content: center;
43
- gap: 12px;
44
- padding: 24px;
45
- background-color: #f7f9fc;
46
  }
47
 
48
  .model-btn {
49
- padding: 10px 20px;
50
- font-size: 14px;
 
 
 
51
  font-weight: 600;
52
- border: 1px solid #dcdcdc;
53
- border-radius: 20px;
54
- background-color: #ffffff;
55
- color: #555;
56
  cursor: pointer;
57
- transition: all 0.2s ease-in-out;
58
- }
59
-
60
- .model-btn:hover {
61
- background-color: #f1f3f4;
62
- border-color: #c0c0c0;
63
  }
64
 
65
  .model-btn.active {
66
- background-color: #1a73e8;
67
- color: #ffffff;
68
- border-color: #1a73e8;
69
  }
70
 
71
- .model-btn.disabled {
72
- background-color: #f5f5f5;
73
- color: #b0b0b0;
74
- cursor: not-allowed;
75
- border-color: #e0e0e0;
76
  }
77
 
 
78
  main {
79
- padding: 32px;
80
  }
81
 
82
- .model-interface {
83
  display: none;
 
84
  }
85
 
86
- .model-interface.active {
87
  display: block;
88
  }
89
 
90
- form {
91
- background-color: #ffffff;
92
- border: 1px solid #e8e8e8;
93
- border-radius: 8px;
94
- padding: 24px;
95
- }
96
-
97
- form h2 {
98
  margin-top: 0;
99
- margin-bottom: 8px;
100
- font-size: 20px;
 
 
 
101
  }
102
 
103
- form p {
104
- margin-top: 0;
105
- margin-bottom: 24px;
106
- color: #666;
107
  }
108
 
109
- label {
110
- display: block;
111
- font-weight: 600;
112
- margin-bottom: 8px;
113
  }
114
 
115
- textarea, input[type="file"] {
116
  width: 100%;
117
- box-sizing: border-box;
118
- }
119
-
120
- textarea {
121
  padding: 12px;
122
- border: 1px solid #dcdcdc;
123
- border-radius: 6px;
124
- font-size: 14px;
125
- min-height: 120px;
126
- resize: vertical;
 
 
127
  }
128
 
129
- .predict-btn {
130
- width: 100%;
131
- padding: 12px;
132
- font-size: 16px;
133
- font-weight: 600;
134
  border: none;
135
- border-radius: 6px;
136
- background-color: #28a745;
137
- color: #ffffff;
 
138
  cursor: pointer;
139
- margin-top: 24px;
140
  transition: background-color 0.2s;
 
141
  }
142
 
143
- .predict-btn:hover {
144
- background-color: #218838;
145
  }
146
 
147
- .upload-label {
148
- padding: 12px;
149
- border: 1px dashed #dcdcdc;
150
- border-radius: 6px;
151
  text-align: center;
152
- cursor: pointer;
153
- display: block;
154
- margin-bottom: 16px;
155
  }
156
 
157
- input[type="file"] {
 
 
 
 
158
  display: none;
159
  }
160
 
161
- #image-preview {
162
- margin-top: 16px;
163
- text-align: center;
 
 
164
  }
165
 
166
- #image-preview img {
167
- max-width: 100%;
168
- max-height: 200px;
169
- border-radius: 6px;
 
170
  }
171
 
172
- .result-display {
173
- margin-top: 24px;
174
- padding: 16px;
175
- border-radius: 6px;
176
- background-color: #f7f9fc;
177
- border: 1px solid #e8e8e8;
178
- text-align: center;
179
- font-size: 18px;
180
  font-weight: 600;
181
- display: none;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  }
183
 
184
- .result-display.loading {
185
- color: #555;
 
 
 
 
186
  }
187
 
188
- .result-display.success {
189
- color: #1a73e8;
 
 
 
190
  }
191
 
192
- .result-display.error {
193
- color: #d93025;
 
 
 
 
 
 
 
 
194
  }
 
1
+ /* --- General Layout & Dark Theme --- */
2
  body {
3
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
4
+ background-color: #0b0d11;
5
+ color: #e6edf3;
6
  margin: 0;
 
7
  display: flex;
8
  justify-content: center;
9
+ align-items: center;
10
  min-height: 100vh;
11
+ padding: 20px;
12
  }
13
 
14
  .container {
15
+ background: #1c2128;
16
+ border-radius: 12px;
17
+ border: 1px solid #30363d;
18
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
19
  width: 100%;
20
  max-width: 800px;
 
 
 
21
  overflow: hidden;
22
  }
23
 
24
  header {
25
  text-align: center;
26
+ padding: 30px;
27
+ border-bottom: 1px solid #30363d;
28
  }
29
 
30
  header h1 {
31
+ margin: 0;
32
+ font-size: 2.5rem;
33
  font-weight: 700;
34
+ color: #e6edf3;
35
  }
36
 
37
  header p {
38
+ margin-top: 8px;
39
+ color: #8b949e;
40
+ font-size: 1.1rem;
41
  }
42
 
43
+ /* --- Model Selector Navigation --- */
44
  .model-selector {
45
  display: flex;
46
  justify-content: center;
47
+ padding: 20px;
48
+ background-color: #161b22;
49
+ border-bottom: 1px solid #30363d;
50
  }
51
 
52
  .model-btn {
53
+ background-color: transparent;
54
+ border: none;
55
+ padding: 12px 24px;
56
+ margin: 0 10px;
57
+ font-size: 1rem;
58
  font-weight: 600;
59
+ color: #8b949e;
 
 
 
60
  cursor: pointer;
61
+ border-radius: 8px;
62
+ transition: background-color 0.2s, color 0.2s;
 
 
 
 
63
  }
64
 
65
  .model-btn.active {
66
+ background-color: #ffd866;
67
+ color: #000000;
 
68
  }
69
 
70
+ .model-btn:not(.active):hover {
71
+ background-color: #30363d;
72
+ color: #e6edf3;
 
 
73
  }
74
 
75
+ /* --- Main Content & Model Sections --- */
76
  main {
77
+ padding: 30px;
78
  }
79
 
80
+ .model-section {
81
  display: none;
82
+ animation: fadeIn 0.5s;
83
  }
84
 
85
+ .model-section.active {
86
  display: block;
87
  }
88
 
89
+ .model-section h2 {
 
 
 
 
 
 
 
90
  margin-top: 0;
91
+ font-size: 1.8rem;
92
+ color: #e6edf3;
93
+ border-bottom: 2px solid #ffd866;
94
+ padding-bottom: 10px;
95
+ margin-bottom: 20px;
96
  }
97
 
98
+ .model-section p {
99
+ margin-bottom: 20px;
100
+ line-height: 1.6;
101
+ color: #8b949e;
102
  }
103
 
104
+ /* --- Forms --- */
105
+ form {
106
+ display: flex;
107
+ flex-direction: column;
108
  }
109
 
110
+ textarea, input[type="file"], input[type="number"] {
111
  width: 100%;
 
 
 
 
112
  padding: 12px;
113
+ background-color: #0d1117;
114
+ color: #e6edf3;
115
+ border: 1px solid #30363d;
116
+ border-radius: 8px;
117
+ font-size: 1rem;
118
+ margin-bottom: 15px;
119
+ box-sizing: border-box;
120
  }
121
 
122
+ button[type="submit"] {
123
+ background-color: #ffd866;
124
+ color: #000000;
 
 
125
  border: none;
126
+ padding: 14px 20px;
127
+ font-size: 1.1rem;
128
+ font-weight: 600;
129
+ border-radius: 8px;
130
  cursor: pointer;
 
131
  transition: background-color 0.2s;
132
+ align-self: flex-start;
133
  }
134
 
135
+ button[type="submit"]:hover {
136
+ background-color: #f0c340;
137
  }
138
 
139
+ /* --- Specific Form Styling --- */
140
+ .image-preview-container {
141
+ margin-bottom: 15px;
 
142
  text-align: center;
 
 
 
143
  }
144
 
145
+ .image-preview {
146
+ max-width: 100%;
147
+ max-height: 200px;
148
+ border: 1px solid #30363d;
149
+ border-radius: 8px;
150
  display: none;
151
  }
152
 
153
+ .form-grid {
154
+ display: grid;
155
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
156
+ gap: 20px;
157
+ margin-bottom: 20px;
158
  }
159
 
160
+ .form-group label {
161
+ display: block;
162
+ font-weight: 600;
163
+ margin-bottom: 8px;
164
+ color: #e6edf3;
165
  }
166
 
167
+ .beta-tag {
168
+ background-color: #30363d;
169
+ color: #8b949e;
170
+ font-size: 0.8rem;
 
 
 
 
171
  font-weight: 600;
172
+ padding: 4px 8px;
173
+ border-radius: 12px;
174
+ vertical-align: middle;
175
+ margin-left: 8px;
176
+ }
177
+
178
+ /* --- Result Display --- */
179
+ .result {
180
+ margin-top: 20px;
181
+ padding: 15px;
182
+ background-color: #161b22;
183
+ border-left: 4px solid #ffd866;
184
+ border-radius: 4px;
185
+ font-size: 1.1rem;
186
+ font-weight: 500;
187
+ min-height: 24px;
188
+ }
189
+
190
+ /* --- Footer --- */
191
+ footer {
192
+ text-align: center;
193
+ padding: 20px;
194
+ background-color: #161b22;
195
+ border-top: 1px solid #30363d;
196
+ font-size: 0.9rem;
197
+ color: #8b949e;
198
+ }
199
+
200
+ /* --- Animations --- */
201
+ @keyframes fadeIn {
202
+ from { opacity: 0; }
203
+ to { opacity: 1; }
204
  }
205
 
206
+ /* --- Housing Predictor Form Grid --- */
207
+ .housing-predictor-form-grid {
208
+ display: grid;
209
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
210
+ gap: 20px;
211
+ margin-bottom: 20px;
212
  }
213
 
214
+ .housing-predictor-form-group label {
215
+ display: block;
216
+ font-weight: 600;
217
+ margin-bottom: 8px;
218
+ color: #e6edf3;
219
  }
220
 
221
+ .housing-predictor-form-group input[type="number"] {
222
+ width: 100%;
223
+ padding: 12px;
224
+ background-color: #0d1117;
225
+ color: #e6edf3;
226
+ border: 1px solid #30363d;
227
+ border-radius: 8px;
228
+ font-size: 1rem;
229
+ margin-bottom: 15px;
230
+ box-sizing: border-box;
231
  }