Spaces:
Running
Running
structured email; smooth cooldown
Browse files- backend/server.py +51 -29
- frontend/js/vehicles.js +22 -16
- frontend/vehicles.html +4 -4
backend/server.py
CHANGED
|
@@ -200,7 +200,7 @@ FEEDBACK_PATH = Path(tempfile.gettempdir()) / "urbanflow_feedback.json"
|
|
| 200 |
def send_feedback_email(api_key, feedback):
|
| 201 |
try:
|
| 202 |
resend.api_key = api_key
|
| 203 |
-
fb_type =
|
| 204 |
rating = feedback.get('rating', 0)
|
| 205 |
details = feedback.get('details') or "No text provided"
|
| 206 |
usecase = feedback.get('usecase') or "Not specified"
|
|
@@ -208,42 +208,64 @@ def send_feedback_email(api_key, feedback):
|
|
| 208 |
emojis = feedback.get('emojis', {})
|
| 209 |
priorities = feedback.get('priorities', [])
|
| 210 |
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
|
| 220 |
priority_section = ""
|
| 221 |
if priorities:
|
| 222 |
-
priority_section = "<p
|
|
|
|
| 223 |
for p in priorities:
|
| 224 |
-
priority_section += f"<
|
| 225 |
-
priority_section += "</
|
| 226 |
|
| 227 |
html_body = f"""
|
| 228 |
-
<div style="font-family: sans-serif; color: #333; max-width: 600px; border: 1px solid #
|
| 229 |
-
<
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
<tr><td style="padding: 5px 0;"><strong>Rating:</strong></td><td>{'β
' * rating}{'β' * (5-rating)} ({rating}/5)</td></tr>
|
| 234 |
-
<tr><td style="padding: 5px 0;"><strong>Usecase:</strong></td><td>{usecase}</td></tr>
|
| 235 |
-
</table>
|
| 236 |
-
|
| 237 |
-
<hr style="border: 0; border-top: 1px solid #eee; margin: 20px 0;">
|
| 238 |
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
|
| 245 |
-
<hr style="border: 0; border-top: 1px solid #eee; margin:
|
| 246 |
-
<p style="font-size:
|
| 247 |
</div>
|
| 248 |
"""
|
| 249 |
|
|
|
|
| 200 |
def send_feedback_email(api_key, feedback):
|
| 201 |
try:
|
| 202 |
resend.api_key = api_key
|
| 203 |
+
fb_type = feedback.get('type') or 'General'
|
| 204 |
rating = feedback.get('rating', 0)
|
| 205 |
details = feedback.get('details') or "No text provided"
|
| 206 |
usecase = feedback.get('usecase') or "Not specified"
|
|
|
|
| 208 |
emojis = feedback.get('emojis', {})
|
| 209 |
priorities = feedback.get('priorities', [])
|
| 210 |
|
| 211 |
+
def get_emoji_row(label, choice):
|
| 212 |
+
options = ["Poor", "Fair", "Good", "Great"]
|
| 213 |
+
# Map choice to title case for comparison
|
| 214 |
+
c = (choice or "").title()
|
| 215 |
+
|
| 216 |
+
row = f"<div style='margin-bottom: 12px;'><span style='font-size: 11px; font-weight: bold; color: #777; text-transform: uppercase;'>{label}</span><br><div style='margin-top: 4px;'>"
|
| 217 |
+
for opt in options:
|
| 218 |
+
if c == opt:
|
| 219 |
+
row += f"<span style='display: inline-block; background: #c89a6c; color: #000; font-size: 10px; font-weight: bold; padding: 3px 10px; border-radius: 4px; margin-right: 6px;'>{opt}</span>"
|
| 220 |
+
else:
|
| 221 |
+
row += f"<span style='display: inline-block; background: #f5f5f5; color: #bbb; font-size: 10px; padding: 2px 9px; border-radius: 4px; border: 1px solid #eee; margin-right: 6px;'>{opt}</span>"
|
| 222 |
+
row += "</div></div>"
|
| 223 |
+
return row
|
| 224 |
+
|
| 225 |
+
metrics_html = ""
|
| 226 |
+
metrics_html += get_emoji_row("Recommend Product", emojis.get("fb-recommend"))
|
| 227 |
+
metrics_html += get_emoji_row("Security Assessment", emojis.get("fb-security"))
|
| 228 |
+
metrics_html += get_emoji_row("Integration Ready", emojis.get("fb-integration"))
|
| 229 |
+
metrics_html += get_emoji_row("Ease of Use", emojis.get("fb-ease"))
|
| 230 |
|
| 231 |
priority_section = ""
|
| 232 |
if priorities:
|
| 233 |
+
priority_section = "<p style='font-size: 11px; font-weight: bold; color: #777; text-transform: uppercase; margin-bottom: 8px;'>Feature Prioritization:</p>"
|
| 234 |
+
priority_section += "<div style='display: flex; flex-wrap: wrap; gap: 6px;'>"
|
| 235 |
for p in priorities:
|
| 236 |
+
priority_section += f"<span style='display: inline-block; background: #eee; border-left: 3px solid #c89a6c; padding: 6px 12px; font-size: 12px; font-weight: 600; margin-bottom: 6px;'>{p}</span> "
|
| 237 |
+
priority_section += "</div>"
|
| 238 |
|
| 239 |
html_body = f"""
|
| 240 |
+
<div style="font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; color: #333; max-width: 600px; border: 1px solid #ddd; padding: 40px; border-radius: 16px; line-height: 1.6;">
|
| 241 |
+
<div style="text-align: center; margin-bottom: 30px;">
|
| 242 |
+
<h2 style="color: #8b5e3c; margin: 0; font-size: 24px; letter-spacing: -0.5px;">UrbanFlow Intelligence</h2>
|
| 243 |
+
<p style="color: #999; font-size: 12px; text-transform: uppercase; tracking: 1px;">Session Feedback Artifact</p>
|
| 244 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
|
| 246 |
+
<div style="background: #fafafa; padding: 20px; border-radius: 12px; margin-bottom: 30px;">
|
| 247 |
+
<table style="width: 100%; font-size: 14px;">
|
| 248 |
+
<tr><td style="color: #777; width: 40%; padding: 4px 0;">Primary Use Case:</td><td style="font-weight: 600;">{usecase}</td></tr>
|
| 249 |
+
<tr><td style="color: #777; padding: 4px 0;">Feedback Category:</td><td style="font-weight: 600;">{fb_type}</td></tr>
|
| 250 |
+
<tr><td style="color: #777; padding: 4px 0;">Overall Rating:</td><td style="font-weight: 600; color: #c89a6c;">{'β
' * rating}{'β' * (5-rating)} ({rating}/5)</td></tr>
|
| 251 |
+
</table>
|
| 252 |
+
</div>
|
| 253 |
+
|
| 254 |
+
<div style="margin-bottom: 30px;">
|
| 255 |
+
{metrics_html}
|
| 256 |
+
</div>
|
| 257 |
+
|
| 258 |
+
<div style="margin-bottom: 30px;">
|
| 259 |
+
{priority_section}
|
| 260 |
+
</div>
|
| 261 |
+
|
| 262 |
+
<div style="margin-bottom: 10px;">
|
| 263 |
+
<p style="font-size: 11px; font-weight: bold; color: #777; text-transform: uppercase;">Detailed Word Feedback:</p>
|
| 264 |
+
<div style="background: #fff; border: 1px solid #eee; padding: 20px; border-radius: 8px; font-size: 14px; color: #444; white-space: pre-wrap; line-height: 1.8;">{details}</div>
|
| 265 |
+
</div>
|
| 266 |
|
| 267 |
+
<hr style="border: 0; border-top: 1px solid #eee; margin: 40px 0;">
|
| 268 |
+
<p style="font-size: 10px; color: #aaa; text-align: center; font-style: italic;">Automated transmission from UrbanFlow Inference Engine v1.0</p>
|
| 269 |
</div>
|
| 270 |
"""
|
| 271 |
|
frontend/js/vehicles.js
CHANGED
|
@@ -145,13 +145,17 @@
|
|
| 145 |
}
|
| 146 |
|
| 147 |
async function submitFeedback() {
|
| 148 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
const text = document.getElementById('fb-text').value.trim();
|
| 150 |
-
const usecase = document.getElementById('fb-usecase').value;
|
| 151 |
|
| 152 |
const priorities = [];
|
| 153 |
document.querySelectorAll('#fb-priorities .fb-chip.active').forEach(c => {
|
| 154 |
-
priorities.push(c.
|
| 155 |
});
|
| 156 |
|
| 157 |
if (!text && _fbRating === 0 && !_fbEmojis['fb-recommend'] && !_fbEmojis['fb-security'] && !_fbEmojis['fb-integration'] && !_fbEmojis['fb-ease']) {
|
|
@@ -162,8 +166,8 @@
|
|
| 162 |
const payload = {
|
| 163 |
rating: _fbRating,
|
| 164 |
emojis: _fbEmojis,
|
| 165 |
-
type:
|
| 166 |
-
usecase:
|
| 167 |
priorities: priorities,
|
| 168 |
details: text,
|
| 169 |
timestamp: new Date().toISOString()
|
|
@@ -188,11 +192,10 @@
|
|
| 188 |
_fbRating = 0;
|
| 189 |
setRating(0);
|
| 190 |
|
| 191 |
-
// Start Cooldown (60 seconds)
|
| 192 |
const btn = document.getElementById('fb-submit-btn');
|
| 193 |
const wrap = document.getElementById('fb-cooldown-wrap');
|
| 194 |
const bar = document.getElementById('fb-cooldown-bar');
|
| 195 |
-
const timerEl = document.getElementById('fb-cooldown-timer');
|
| 196 |
|
| 197 |
btn.disabled = true;
|
| 198 |
btn.style.opacity = '0.5';
|
|
@@ -200,15 +203,17 @@
|
|
| 200 |
btn.innerText = 'Feedback Received';
|
| 201 |
wrap.classList.remove('hidden');
|
| 202 |
|
| 203 |
-
|
| 204 |
-
const
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
|
|
|
| 209 |
|
| 210 |
-
if (
|
| 211 |
-
|
|
|
|
| 212 |
btn.disabled = false;
|
| 213 |
btn.style.opacity = '1';
|
| 214 |
btn.style.cursor = 'pointer';
|
|
@@ -216,7 +221,8 @@
|
|
| 216 |
wrap.classList.add('hidden');
|
| 217 |
bar.style.width = '100%';
|
| 218 |
}
|
| 219 |
-
}
|
|
|
|
| 220 |
} else {
|
| 221 |
showToast('Failed to submit β please try again', 'error');
|
| 222 |
}
|
|
|
|
| 145 |
}
|
| 146 |
|
| 147 |
async function submitFeedback() {
|
| 148 |
+
const typeEl = document.getElementById('fb-type');
|
| 149 |
+
const typeText = typeEl.selectedIndex >= 0 ? typeEl.options[typeEl.selectedIndex].text : "";
|
| 150 |
+
|
| 151 |
+
const usecaseEl = document.getElementById('fb-usecase');
|
| 152 |
+
const usecaseText = usecaseEl.selectedIndex >= 0 ? usecaseEl.options[usecaseEl.selectedIndex].text : "";
|
| 153 |
+
|
| 154 |
const text = document.getElementById('fb-text').value.trim();
|
|
|
|
| 155 |
|
| 156 |
const priorities = [];
|
| 157 |
document.querySelectorAll('#fb-priorities .fb-chip.active').forEach(c => {
|
| 158 |
+
priorities.push(c.innerText);
|
| 159 |
});
|
| 160 |
|
| 161 |
if (!text && _fbRating === 0 && !_fbEmojis['fb-recommend'] && !_fbEmojis['fb-security'] && !_fbEmojis['fb-integration'] && !_fbEmojis['fb-ease']) {
|
|
|
|
| 166 |
const payload = {
|
| 167 |
rating: _fbRating,
|
| 168 |
emojis: _fbEmojis,
|
| 169 |
+
type: typeText,
|
| 170 |
+
usecase: usecaseText,
|
| 171 |
priorities: priorities,
|
| 172 |
details: text,
|
| 173 |
timestamp: new Date().toISOString()
|
|
|
|
| 192 |
_fbRating = 0;
|
| 193 |
setRating(0);
|
| 194 |
|
| 195 |
+
// Start Smooth Cooldown (60 seconds)
|
| 196 |
const btn = document.getElementById('fb-submit-btn');
|
| 197 |
const wrap = document.getElementById('fb-cooldown-wrap');
|
| 198 |
const bar = document.getElementById('fb-cooldown-bar');
|
|
|
|
| 199 |
|
| 200 |
btn.disabled = true;
|
| 201 |
btn.style.opacity = '0.5';
|
|
|
|
| 203 |
btn.innerText = 'Feedback Received';
|
| 204 |
wrap.classList.remove('hidden');
|
| 205 |
|
| 206 |
+
const startTime = Date.now();
|
| 207 |
+
const duration = 60000;
|
| 208 |
+
|
| 209 |
+
function updateSmoothBar() {
|
| 210 |
+
const elapsed = Date.now() - startTime;
|
| 211 |
+
const remaining = Math.max(0, duration - elapsed);
|
| 212 |
+
bar.style.width = (remaining / duration * 100) + '%';
|
| 213 |
|
| 214 |
+
if (remaining > 0) {
|
| 215 |
+
requestAnimationFrame(updateSmoothBar);
|
| 216 |
+
} else {
|
| 217 |
btn.disabled = false;
|
| 218 |
btn.style.opacity = '1';
|
| 219 |
btn.style.cursor = 'pointer';
|
|
|
|
| 221 |
wrap.classList.add('hidden');
|
| 222 |
bar.style.width = '100%';
|
| 223 |
}
|
| 224 |
+
}
|
| 225 |
+
requestAnimationFrame(updateSmoothBar);
|
| 226 |
} else {
|
| 227 |
showToast('Failed to submit β please try again', 'error');
|
| 228 |
}
|
frontend/vehicles.html
CHANGED
|
@@ -833,11 +833,11 @@
|
|
| 833 |
</button>
|
| 834 |
|
| 835 |
<!-- Cooldown Progress Bar -->
|
| 836 |
-
<div id="fb-cooldown-wrap" class="w-full mt-
|
| 837 |
-
<div class="w-full h-1 bg-neutral-900 rounded-full overflow-hidden">
|
| 838 |
-
<div id="fb-cooldown-bar" class="h-full w-full
|
| 839 |
</div>
|
| 840 |
-
<p class="text-[9px] font-bold text-center mt-2 uppercase tracking-[0.
|
| 841 |
</div>
|
| 842 |
|
| 843 |
</div>
|
|
|
|
| 833 |
</button>
|
| 834 |
|
| 835 |
<!-- Cooldown Progress Bar -->
|
| 836 |
+
<div id="fb-cooldown-wrap" class="w-full mt-5 hidden">
|
| 837 |
+
<div class="w-full h-[1.5px] bg-neutral-900 rounded-full overflow-hidden">
|
| 838 |
+
<div id="fb-cooldown-bar" class="h-full w-full rounded-full" style="background:var(--cocoa-l)"></div>
|
| 839 |
</div>
|
| 840 |
+
<p class="text-[9px] font-bold text-center mt-2.5 uppercase tracking-[0.25em]" style="color:#444">Ready to send again in a moment...</p>
|
| 841 |
</div>
|
| 842 |
|
| 843 |
</div>
|