Spaces:
Running
Running
Upload folder using huggingface_hub
Browse files- AGENTS.md +4 -0
- CLAUDE.md +4 -0
- web_demo/app.py +9 -0
- web_demo/static/feedback/feedback.css +4 -1
- web_demo/static/feedback/feedback.js +5 -1
- web_demo/templates/admin.html +5 -3
- web_demo/templates/feedback.html +11 -0
AGENTS.md
CHANGED
|
@@ -274,6 +274,10 @@ create table public.feedback (
|
|
| 274 |
id uuid primary key default gen_random_uuid(),
|
| 275 |
submitted_at timestamptz not null default now(),
|
| 276 |
kol_email text not null, -- normalized join key
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
received_size text, received_model text, best_fit_finger text,
|
| 278 |
fit_quality text, hand text, photo_url text, notes text
|
| 279 |
);
|
|
|
|
| 274 |
id uuid primary key default gen_random_uuid(),
|
| 275 |
submitted_at timestamptz not null default now(),
|
| 276 |
kol_email text not null, -- normalized join key
|
| 277 |
+
kol_name text, -- also captured: legacy KOLs measured
|
| 278 |
+
-- pre-email-switch have kol_name (not
|
| 279 |
+
-- kol_email) on their measurements row,
|
| 280 |
+
-- so name lets their feedback join back
|
| 281 |
received_size text, received_model text, best_fit_finger text,
|
| 282 |
fit_quality text, hand text, photo_url text, notes text
|
| 283 |
);
|
CLAUDE.md
CHANGED
|
@@ -274,6 +274,10 @@ create table public.feedback (
|
|
| 274 |
id uuid primary key default gen_random_uuid(),
|
| 275 |
submitted_at timestamptz not null default now(),
|
| 276 |
kol_email text not null, -- normalized join key
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
received_size text, received_model text, best_fit_finger text,
|
| 278 |
fit_quality text, hand text, photo_url text, notes text
|
| 279 |
);
|
|
|
|
| 274 |
id uuid primary key default gen_random_uuid(),
|
| 275 |
submitted_at timestamptz not null default now(),
|
| 276 |
kol_email text not null, -- normalized join key
|
| 277 |
+
kol_name text, -- also captured: legacy KOLs measured
|
| 278 |
+
-- pre-email-switch have kol_name (not
|
| 279 |
+
-- kol_email) on their measurements row,
|
| 280 |
+
-- so name lets their feedback join back
|
| 281 |
received_size text, received_model text, best_fit_finger text,
|
| 282 |
fit_quality text, hand text, photo_url text, notes text
|
| 283 |
);
|
web_demo/app.py
CHANGED
|
@@ -651,6 +651,14 @@ def api_fit_feedback():
|
|
| 651 |
kol_email = (request.form.get("kol_email") or "").strip().lower()
|
| 652 |
if not kol_email or "@" not in kol_email:
|
| 653 |
return jsonify({"success": False, "error": "A valid email is required"}), 400
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 654 |
|
| 655 |
def _field(name: str) -> Optional[str]:
|
| 656 |
v = (request.form.get(name) or "").strip()
|
|
@@ -675,6 +683,7 @@ def api_fit_feedback():
|
|
| 675 |
|
| 676 |
record = {
|
| 677 |
"kol_email": kol_email,
|
|
|
|
| 678 |
"received_size": _field("received_size"),
|
| 679 |
"received_model": _field("received_model"),
|
| 680 |
"best_fit_finger": _field("best_fit_finger"),
|
|
|
|
| 651 |
kol_email = (request.form.get("kol_email") or "").strip().lower()
|
| 652 |
if not kol_email or "@" not in kol_email:
|
| 653 |
return jsonify({"success": False, "error": "A valid email is required"}), 400
|
| 654 |
+
# Name is also required: legacy KOLs were measured under the old
|
| 655 |
+
# name-based identity (before the email switch), so their measurements
|
| 656 |
+
# rows have kol_name but no kol_email. Capturing the name here lets their
|
| 657 |
+
# post-shipment feedback join back to those historical rows. See
|
| 658 |
+
# doc/v8/PRD.md.
|
| 659 |
+
kol_name = (request.form.get("kol_name") or "").strip()
|
| 660 |
+
if not kol_name:
|
| 661 |
+
return jsonify({"success": False, "error": "A name is required"}), 400
|
| 662 |
|
| 663 |
def _field(name: str) -> Optional[str]:
|
| 664 |
v = (request.form.get(name) or "").strip()
|
|
|
|
| 683 |
|
| 684 |
record = {
|
| 685 |
"kol_email": kol_email,
|
| 686 |
+
"kol_name": kol_name,
|
| 687 |
"received_size": _field("received_size"),
|
| 688 |
"received_model": _field("received_model"),
|
| 689 |
"best_fit_finger": _field("best_fit_finger"),
|
web_demo/static/feedback/feedback.css
CHANGED
|
@@ -32,7 +32,10 @@
|
|
| 32 |
.controls input[type="file"] {
|
| 33 |
width: 100%;
|
| 34 |
font-size: 1rem;
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
| 36 |
}
|
| 37 |
|
| 38 |
/* Enlarge the native "Choose File" button — iOS Safari renders it small
|
|
|
|
| 32 |
.controls input[type="file"] {
|
| 33 |
width: 100%;
|
| 34 |
font-size: 1rem;
|
| 35 |
+
/* Hide the native status text ("No file chosen" / the picked filename) —
|
| 36 |
+
it's redundant next to the labelled button. The button keeps its own
|
| 37 |
+
color (set on ::file-selector-button below), so it stays visible. */
|
| 38 |
+
color: transparent;
|
| 39 |
}
|
| 40 |
|
| 41 |
/* Enlarge the native "Choose File" button — iOS Safari renders it small
|
web_demo/static/feedback/feedback.js
CHANGED
|
@@ -34,7 +34,7 @@ const REQUIRED = [
|
|
| 34 |
];
|
| 35 |
|
| 36 |
// Every field that can produce a validation error, in DOM order.
|
| 37 |
-
const FIELD_IDS = ["kolEmail", ...REQUIRED.map(([id]) => id)];
|
| 38 |
|
| 39 |
// Inject an inline error slot under each validatable field and clear it
|
| 40 |
// as soon as the user edits that field.
|
|
@@ -80,6 +80,9 @@ function showFormError(msg) {
|
|
| 80 |
|
| 81 |
// Returns [fieldId, message] for the first invalid field, or null.
|
| 82 |
function firstInvalid() {
|
|
|
|
|
|
|
|
|
|
| 83 |
if (!EMAIL_RE.test(val("kolEmail").toLowerCase())) {
|
| 84 |
return ["kolEmail", "Please enter a valid email."];
|
| 85 |
}
|
|
@@ -107,6 +110,7 @@ form.addEventListener("submit", async (event) => {
|
|
| 107 |
}
|
| 108 |
|
| 109 |
const fd = new FormData();
|
|
|
|
| 110 |
fd.append("kol_email", val("kolEmail").toLowerCase());
|
| 111 |
fd.append("received_size", val("receivedSize"));
|
| 112 |
fd.append("received_model", val("receivedModel"));
|
|
|
|
| 34 |
];
|
| 35 |
|
| 36 |
// Every field that can produce a validation error, in DOM order.
|
| 37 |
+
const FIELD_IDS = ["kolName", "kolEmail", ...REQUIRED.map(([id]) => id)];
|
| 38 |
|
| 39 |
// Inject an inline error slot under each validatable field and clear it
|
| 40 |
// as soon as the user edits that field.
|
|
|
|
| 80 |
|
| 81 |
// Returns [fieldId, message] for the first invalid field, or null.
|
| 82 |
function firstInvalid() {
|
| 83 |
+
if (!val("kolName")) {
|
| 84 |
+
return ["kolName", "Please enter your name."];
|
| 85 |
+
}
|
| 86 |
if (!EMAIL_RE.test(val("kolEmail").toLowerCase())) {
|
| 87 |
return ["kolEmail", "Please enter a valid email."];
|
| 88 |
}
|
|
|
|
| 110 |
}
|
| 111 |
|
| 112 |
const fd = new FormData();
|
| 113 |
+
fd.append("kol_name", val("kolName"));
|
| 114 |
fd.append("kol_email", val("kolEmail").toLowerCase());
|
| 115 |
fd.append("received_size", val("receivedSize"));
|
| 116 |
fd.append("received_model", val("receivedModel"));
|
web_demo/templates/admin.html
CHANGED
|
@@ -285,6 +285,7 @@
|
|
| 285 |
<table>
|
| 286 |
<thead>
|
| 287 |
<tr>
|
|
|
|
| 288 |
<th>Email</th>
|
| 289 |
<th>Submitted</th>
|
| 290 |
<th>Size</th>
|
|
@@ -298,7 +299,7 @@
|
|
| 298 |
</tr>
|
| 299 |
</thead>
|
| 300 |
<tbody id="fbTableBody">
|
| 301 |
-
<tr><td colspan="
|
| 302 |
</tbody>
|
| 303 |
</table>
|
| 304 |
</div>
|
|
@@ -526,6 +527,7 @@
|
|
| 526 |
? `<img class="thumb" loading="lazy" src="${esc(r.photo_url)}" onclick="window.open('${esc(r.photo_url)}')" />`
|
| 527 |
: "-";
|
| 528 |
return `<tr data-id="${esc(r.id)}">
|
|
|
|
| 529 |
<td><strong>${esc(r.kol_email) || "-"}</strong></td>
|
| 530 |
<td>${fmtDate(r.submitted_at)}</td>
|
| 531 |
<td>${esc(r.received_size) || "-"}</td>
|
|
@@ -546,7 +548,7 @@
|
|
| 546 |
if (fbCurrentPage < 1) fbCurrentPage = 1;
|
| 547 |
if (total === 0) {
|
| 548 |
fbCountLabel.textContent = "0 feedback rows";
|
| 549 |
-
fbTableBody.innerHTML = '<tr><td colspan="
|
| 550 |
} else {
|
| 551 |
const start = (fbCurrentPage - 1) * PAGE_SIZE;
|
| 552 |
const slice = fbAllRows.slice(start, start + PAGE_SIZE);
|
|
@@ -573,7 +575,7 @@
|
|
| 573 |
fbCurrentPage = 1;
|
| 574 |
renderFbPage();
|
| 575 |
} catch (e) {
|
| 576 |
-
fbTableBody.innerHTML = `<tr><td colspan="
|
| 577 |
}
|
| 578 |
};
|
| 579 |
|
|
|
|
| 285 |
<table>
|
| 286 |
<thead>
|
| 287 |
<tr>
|
| 288 |
+
<th>Name</th>
|
| 289 |
<th>Email</th>
|
| 290 |
<th>Submitted</th>
|
| 291 |
<th>Size</th>
|
|
|
|
| 299 |
</tr>
|
| 300 |
</thead>
|
| 301 |
<tbody id="fbTableBody">
|
| 302 |
+
<tr><td colspan="11" class="empty">Loading...</td></tr>
|
| 303 |
</tbody>
|
| 304 |
</table>
|
| 305 |
</div>
|
|
|
|
| 527 |
? `<img class="thumb" loading="lazy" src="${esc(r.photo_url)}" onclick="window.open('${esc(r.photo_url)}')" />`
|
| 528 |
: "-";
|
| 529 |
return `<tr data-id="${esc(r.id)}">
|
| 530 |
+
<td>${esc(r.kol_name) || "-"}</td>
|
| 531 |
<td><strong>${esc(r.kol_email) || "-"}</strong></td>
|
| 532 |
<td>${fmtDate(r.submitted_at)}</td>
|
| 533 |
<td>${esc(r.received_size) || "-"}</td>
|
|
|
|
| 548 |
if (fbCurrentPage < 1) fbCurrentPage = 1;
|
| 549 |
if (total === 0) {
|
| 550 |
fbCountLabel.textContent = "0 feedback rows";
|
| 551 |
+
fbTableBody.innerHTML = '<tr><td colspan="11" class="empty">No feedback yet</td></tr>';
|
| 552 |
} else {
|
| 553 |
const start = (fbCurrentPage - 1) * PAGE_SIZE;
|
| 554 |
const slice = fbAllRows.slice(start, start + PAGE_SIZE);
|
|
|
|
| 575 |
fbCurrentPage = 1;
|
| 576 |
renderFbPage();
|
| 577 |
} catch (e) {
|
| 578 |
+
fbTableBody.innerHTML = `<tr><td colspan="11" class="empty">Error loading feedback: ${e.message}</td></tr>`;
|
| 579 |
}
|
| 580 |
};
|
| 581 |
|
web_demo/templates/feedback.html
CHANGED
|
@@ -22,6 +22,17 @@
|
|
| 22 |
|
| 23 |
<form class="panel" id="feedbackForm" novalidate>
|
| 24 |
<div class="controls">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
<label>
|
| 26 |
<span>Email</span>
|
| 27 |
<input
|
|
|
|
| 22 |
|
| 23 |
<form class="panel" id="feedbackForm" novalidate>
|
| 24 |
<div class="controls">
|
| 25 |
+
<label>
|
| 26 |
+
<span>Name</span>
|
| 27 |
+
<input
|
| 28 |
+
type="text"
|
| 29 |
+
id="kolName"
|
| 30 |
+
placeholder="Your name"
|
| 31 |
+
autocomplete="name"
|
| 32 |
+
required
|
| 33 |
+
/>
|
| 34 |
+
</label>
|
| 35 |
+
|
| 36 |
<label>
|
| 37 |
<span>Email</span>
|
| 38 |
<input
|