Spaces:
Running
Running
Upload folder using huggingface_hub
Browse files- web_demo/static/app.js +1 -1
- web_demo/templates/admin.html +55 -111
- web_demo/templates/index.html +1 -1
web_demo/static/app.js
CHANGED
|
@@ -21,7 +21,7 @@ const failReasonMessageMap = {
|
|
| 21 |
card_near_edge:
|
| 22 |
"Card appears cropped. Place the entire card within the photo frame.",
|
| 23 |
hand_not_detected:
|
| 24 |
-
"Hand not detected. Place your hand flat on a plain
|
| 25 |
finger_isolation_failed:
|
| 26 |
"Could not isolate the selected finger. Keep one target finger extended and separated.",
|
| 27 |
finger_not_fully_visible:
|
|
|
|
| 21 |
card_near_edge:
|
| 22 |
"Card appears cropped. Place the entire card within the photo frame.",
|
| 23 |
hand_not_detected:
|
| 24 |
+
"Hand not detected. Place your hand flat on a plain white background (e.g. a sheet of paper), and spread your fingers naturally.",
|
| 25 |
finger_isolation_failed:
|
| 26 |
"Could not isolate the selected finger. Keep one target finger extended and separated.",
|
| 27 |
finger_not_fully_visible:
|
web_demo/templates/admin.html
CHANGED
|
@@ -67,32 +67,12 @@
|
|
| 67 |
tr:hover td { background: #faf7f2; }
|
| 68 |
.thumb { width: 48px; height: 48px; object-fit: cover; border-radius: 4px; cursor: pointer; }
|
| 69 |
.fail { color: var(--accent); font-weight: 500; }
|
| 70 |
-
.gt-input {
|
| 71 |
-
width: 48px; padding: 2px 4px; border: 1px solid var(--border);
|
| 72 |
-
border-radius: 4px; font-size: 12px; text-align: center;
|
| 73 |
-
}
|
| 74 |
-
.gt-select {
|
| 75 |
-
padding: 2px 4px; border: 1px solid var(--border);
|
| 76 |
-
border-radius: 4px; font-size: 12px;
|
| 77 |
-
}
|
| 78 |
-
.gt-notes-input {
|
| 79 |
-
width: 100px; padding: 2px 4px; border: 1px solid var(--border);
|
| 80 |
-
border-radius: 4px; font-size: 12px;
|
| 81 |
-
}
|
| 82 |
-
.save-btn {
|
| 83 |
-
padding: 2px 8px; font-size: 11px; cursor: pointer;
|
| 84 |
-
border: 1px solid #2a7; border-radius: 4px;
|
| 85 |
-
background: #fff; color: #2a7;
|
| 86 |
-
}
|
| 87 |
-
.save-btn:hover { background: #2a7; color: #fff; }
|
| 88 |
-
.save-btn.saved { border-color: #2a7; color: #2a7; }
|
| 89 |
.del-btn {
|
| 90 |
padding: 2px 8px; font-size: 11px; cursor: pointer;
|
| 91 |
border: 1px solid var(--accent); border-radius: 4px;
|
| 92 |
-
background: #fff; color: var(--accent);
|
| 93 |
}
|
| 94 |
.del-btn:hover { background: var(--accent); color: #fff; }
|
| 95 |
-
.ai-peek { color: var(--accent); cursor: pointer; text-decoration: underline; font-size: 12px; }
|
| 96 |
.finger-cell { font-size: 12px; }
|
| 97 |
.finger-cell .size { font-weight: 600; }
|
| 98 |
.finger-cell .detail { color: var(--ink-soft); }
|
|
@@ -120,6 +100,8 @@
|
|
| 120 |
<a href="/">Back to Demo</a>
|
| 121 |
<a id="exportCsvLink" href="#" download>Export CSV</a>
|
| 122 |
<button id="refreshBtn">Refresh</button>
|
|
|
|
|
|
|
| 123 |
<span class="count" id="countLabel">Loading...</span>
|
| 124 |
</div>
|
| 125 |
|
|
@@ -130,26 +112,16 @@
|
|
| 130 |
<th>KOL</th>
|
| 131 |
<th>Date</th>
|
| 132 |
<th>Photo</th>
|
| 133 |
-
<th>Mode</th>
|
| 134 |
-
<th>Size</th>
|
| 135 |
-
<th>Range</th>
|
| 136 |
<th>Index</th>
|
| 137 |
<th>Middle</th>
|
| 138 |
<th>Ring</th>
|
| 139 |
<th>Conf</th>
|
| 140 |
<th>Fail</th>
|
| 141 |
-
<th>AI Explain</th>
|
| 142 |
-
<th>Ring Fit</th>
|
| 143 |
-
<th>Best Finger</th>
|
| 144 |
-
<th>GT Idx</th>
|
| 145 |
-
<th>GT Mid</th>
|
| 146 |
-
<th>GT Ring</th>
|
| 147 |
-
<th>GT Notes</th>
|
| 148 |
<th></th>
|
| 149 |
</tr>
|
| 150 |
</thead>
|
| 151 |
<tbody id="tableBody">
|
| 152 |
-
<tr><td colspan="
|
| 153 |
</tbody>
|
| 154 |
</table>
|
| 155 |
</div>
|
|
@@ -163,6 +135,12 @@
|
|
| 163 |
const loginError = document.getElementById("loginError");
|
| 164 |
const tbody = document.getElementById("tableBody");
|
| 165 |
const countLabel = document.getElementById("countLabel");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
|
| 167 |
let adminToken = sessionStorage.getItem("admin_token") || "";
|
| 168 |
|
|
@@ -200,7 +178,7 @@
|
|
| 200 |
const fmtDate = (iso) => {
|
| 201 |
if (!iso) return "";
|
| 202 |
const d = new Date(iso);
|
| 203 |
-
return d.toLocaleDateString("en-CA") + " " + d.toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
|
| 204 |
};
|
| 205 |
|
| 206 |
const fmtFinger = (pf, name) => {
|
|
@@ -213,6 +191,42 @@
|
|
| 213 |
return `<span class="size">${size}</span> <span class="detail">${diam} ${conf}</span>`;
|
| 214 |
};
|
| 215 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 216 |
const loadData = async () => {
|
| 217 |
try {
|
| 218 |
const resp = await fetch(`/api/admin/measurements?token=${encodeURIComponent(adminToken)}`);
|
|
@@ -223,82 +237,11 @@
|
|
| 223 |
loginError.textContent = "Session expired. Please log in again.";
|
| 224 |
return;
|
| 225 |
}
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
tbody.innerHTML = '<tr><td colspan="17" class="empty">No measurements yet</td></tr>';
|
| 230 |
-
return;
|
| 231 |
-
}
|
| 232 |
-
tbody.innerHTML = rows.map((r) => {
|
| 233 |
-
const pf = r.per_finger || {};
|
| 234 |
-
const photoThumb = r.photo_url
|
| 235 |
-
? `<img class="thumb" src="${r.photo_url}" onclick="window.open('${r.photo_url}')" />`
|
| 236 |
-
: "-";
|
| 237 |
-
return `<tr data-id="${r.id}">
|
| 238 |
-
<td><strong>${r.kol_name || "-"}</strong></td>
|
| 239 |
-
<td>${fmtDate(r.created_at)}</td>
|
| 240 |
-
<td>${photoThumb}</td>
|
| 241 |
-
<td>${r.mode || "-"}</td>
|
| 242 |
-
<td><strong>${r.overall_best_size != null ? r.overall_best_size : "-"}</strong></td>
|
| 243 |
-
<td>${r.overall_range_min != null ? r.overall_range_min + "\u2013" + r.overall_range_max : "-"}</td>
|
| 244 |
-
<td class="finger-cell">${fmtFinger(pf, "index")}</td>
|
| 245 |
-
<td class="finger-cell">${fmtFinger(pf, "middle")}</td>
|
| 246 |
-
<td class="finger-cell">${fmtFinger(pf, "ring")}</td>
|
| 247 |
-
<td>${r.confidence != null ? (r.confidence * 100).toFixed(0) + "%" : "-"}</td>
|
| 248 |
-
<td>${r.fail_reason ? '<span class="fail">' + r.fail_reason + "</span>" : ""}</td>
|
| 249 |
-
<td>${(r.result_json && r.result_json.ai_explanation) ? '<span class="ai-peek" title="Click to view" onclick="alert(this.dataset.full)" data-full="' + r.result_json.ai_explanation.replace(/"/g, '"') + '">View</span>' : "-"}</td>
|
| 250 |
-
<td><select class="gt-select" data-field="ring_fit">
|
| 251 |
-
<option value="">-</option>
|
| 252 |
-
<option value="fits_well" ${r.ring_fit === "fits_well" ? "selected" : ""}>Fits well</option>
|
| 253 |
-
<option value="too_tight" ${r.ring_fit === "too_tight" ? "selected" : ""}>Too tight</option>
|
| 254 |
-
<option value="too_loose" ${r.ring_fit === "too_loose" ? "selected" : ""}>Too loose</option>
|
| 255 |
-
</select></td>
|
| 256 |
-
<td><select class="gt-select" data-field="gt_best_finger">
|
| 257 |
-
<option value="">-</option>
|
| 258 |
-
<option value="index" ${r.gt_best_finger === "index" ? "selected" : ""}>Index</option>
|
| 259 |
-
<option value="middle" ${r.gt_best_finger === "middle" ? "selected" : ""}>Middle</option>
|
| 260 |
-
<option value="ring" ${r.gt_best_finger === "ring" ? "selected" : ""}>Ring</option>
|
| 261 |
-
</select></td>
|
| 262 |
-
<td><input class="gt-input" data-field="gt_index_size" type="number" min="4" max="15" value="${r.gt_index_size ?? ""}" /></td>
|
| 263 |
-
<td><input class="gt-input" data-field="gt_middle_size" type="number" min="4" max="15" value="${r.gt_middle_size ?? ""}" /></td>
|
| 264 |
-
<td><input class="gt-input" data-field="gt_ring_size" type="number" min="4" max="15" value="${r.gt_ring_size ?? ""}" /></td>
|
| 265 |
-
<td><input class="gt-notes-input" data-field="gt_notes" type="text" value="${r.gt_notes || ""}" placeholder="notes" /></td>
|
| 266 |
-
<td><button class="save-btn" onclick="saveGT(this)">Save</button> <button class="del-btn" onclick="deleteRow(this)">Delete</button></td>
|
| 267 |
-
</tr>`;
|
| 268 |
-
}).join("");
|
| 269 |
} catch (e) {
|
| 270 |
-
tbody.innerHTML = `<tr><td colspan="
|
| 271 |
-
}
|
| 272 |
-
};
|
| 273 |
-
|
| 274 |
-
window.saveGT = async (btn) => {
|
| 275 |
-
const tr = btn.closest("tr");
|
| 276 |
-
const id = tr.dataset.id;
|
| 277 |
-
const data = {};
|
| 278 |
-
tr.querySelectorAll("[data-field]").forEach((el) => {
|
| 279 |
-
const field = el.dataset.field;
|
| 280 |
-
if (el.type === "number") {
|
| 281 |
-
data[field] = el.value ? parseInt(el.value, 10) : null;
|
| 282 |
-
} else {
|
| 283 |
-
data[field] = el.value || null;
|
| 284 |
-
}
|
| 285 |
-
});
|
| 286 |
-
try {
|
| 287 |
-
const resp = await fetch(`/api/admin/measurements/${id}/ground-truth?token=${encodeURIComponent(adminToken)}`, {
|
| 288 |
-
method: "POST",
|
| 289 |
-
headers: { "Content-Type": "application/json" },
|
| 290 |
-
body: JSON.stringify(data),
|
| 291 |
-
});
|
| 292 |
-
const result = await resp.json();
|
| 293 |
-
if (result.success) {
|
| 294 |
-
btn.textContent = "Saved";
|
| 295 |
-
btn.classList.add("saved");
|
| 296 |
-
setTimeout(() => { btn.textContent = "Save"; btn.classList.remove("saved"); }, 2000);
|
| 297 |
-
} else {
|
| 298 |
-
alert("Save failed: " + (result.error || "unknown"));
|
| 299 |
-
}
|
| 300 |
-
} catch (e) {
|
| 301 |
-
alert("Network error: " + e.message);
|
| 302 |
}
|
| 303 |
};
|
| 304 |
|
|
@@ -312,9 +255,8 @@
|
|
| 312 |
});
|
| 313 |
const result = await resp.json();
|
| 314 |
if (result.success) {
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
countLabel.textContent = `${rows.length} records`;
|
| 318 |
} else {
|
| 319 |
alert("Delete failed: " + (result.error || "unknown"));
|
| 320 |
}
|
|
@@ -323,6 +265,8 @@
|
|
| 323 |
}
|
| 324 |
};
|
| 325 |
|
|
|
|
|
|
|
| 326 |
document.getElementById("refreshBtn").addEventListener("click", loadData);
|
| 327 |
</script>
|
| 328 |
</body>
|
|
|
|
| 67 |
tr:hover td { background: #faf7f2; }
|
| 68 |
.thumb { width: 48px; height: 48px; object-fit: cover; border-radius: 4px; cursor: pointer; }
|
| 69 |
.fail { color: var(--accent); font-weight: 500; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
.del-btn {
|
| 71 |
padding: 2px 8px; font-size: 11px; cursor: pointer;
|
| 72 |
border: 1px solid var(--accent); border-radius: 4px;
|
| 73 |
+
background: #fff; color: var(--accent);
|
| 74 |
}
|
| 75 |
.del-btn:hover { background: var(--accent); color: #fff; }
|
|
|
|
| 76 |
.finger-cell { font-size: 12px; }
|
| 77 |
.finger-cell .size { font-weight: 600; }
|
| 78 |
.finger-cell .detail { color: var(--ink-soft); }
|
|
|
|
| 100 |
<a href="/">Back to Demo</a>
|
| 101 |
<a id="exportCsvLink" href="#" download>Export CSV</a>
|
| 102 |
<button id="refreshBtn">Refresh</button>
|
| 103 |
+
<button id="prevPageBtn" disabled>‹ Prev</button>
|
| 104 |
+
<button id="nextPageBtn" disabled>Next ›</button>
|
| 105 |
<span class="count" id="countLabel">Loading...</span>
|
| 106 |
</div>
|
| 107 |
|
|
|
|
| 112 |
<th>KOL</th>
|
| 113 |
<th>Date</th>
|
| 114 |
<th>Photo</th>
|
|
|
|
|
|
|
|
|
|
| 115 |
<th>Index</th>
|
| 116 |
<th>Middle</th>
|
| 117 |
<th>Ring</th>
|
| 118 |
<th>Conf</th>
|
| 119 |
<th>Fail</th>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
<th></th>
|
| 121 |
</tr>
|
| 122 |
</thead>
|
| 123 |
<tbody id="tableBody">
|
| 124 |
+
<tr><td colspan="9" class="empty">Loading...</td></tr>
|
| 125 |
</tbody>
|
| 126 |
</table>
|
| 127 |
</div>
|
|
|
|
| 135 |
const loginError = document.getElementById("loginError");
|
| 136 |
const tbody = document.getElementById("tableBody");
|
| 137 |
const countLabel = document.getElementById("countLabel");
|
| 138 |
+
const prevPageBtn = document.getElementById("prevPageBtn");
|
| 139 |
+
const nextPageBtn = document.getElementById("nextPageBtn");
|
| 140 |
+
|
| 141 |
+
const PAGE_SIZE = 50;
|
| 142 |
+
let allRows = [];
|
| 143 |
+
let currentPage = 1;
|
| 144 |
|
| 145 |
let adminToken = sessionStorage.getItem("admin_token") || "";
|
| 146 |
|
|
|
|
| 178 |
const fmtDate = (iso) => {
|
| 179 |
if (!iso) return "";
|
| 180 |
const d = new Date(iso);
|
| 181 |
+
return d.toLocaleDateString("en-CA") + " " + d.toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
| 182 |
};
|
| 183 |
|
| 184 |
const fmtFinger = (pf, name) => {
|
|
|
|
| 191 |
return `<span class="size">${size}</span> <span class="detail">${diam} ${conf}</span>`;
|
| 192 |
};
|
| 193 |
|
| 194 |
+
const rowHtml = (r) => {
|
| 195 |
+
const pf = r.per_finger || {};
|
| 196 |
+
const photoThumb = r.photo_url
|
| 197 |
+
? `<img class="thumb" loading="lazy" src="${r.photo_url}" onclick="window.open('${r.photo_url}')" />`
|
| 198 |
+
: "-";
|
| 199 |
+
return `<tr data-id="${r.id}">
|
| 200 |
+
<td><strong>${r.kol_name || "-"}</strong></td>
|
| 201 |
+
<td>${fmtDate(r.created_at)}</td>
|
| 202 |
+
<td>${photoThumb}</td>
|
| 203 |
+
<td class="finger-cell">${fmtFinger(pf, "index")}</td>
|
| 204 |
+
<td class="finger-cell">${fmtFinger(pf, "middle")}</td>
|
| 205 |
+
<td class="finger-cell">${fmtFinger(pf, "ring")}</td>
|
| 206 |
+
<td>${r.confidence != null ? (r.confidence * 100).toFixed(0) + "%" : "-"}</td>
|
| 207 |
+
<td>${r.fail_reason ? '<span class="fail">' + r.fail_reason + "</span>" : ""}</td>
|
| 208 |
+
<td><button class="del-btn" onclick="deleteRow(this)">Delete</button></td>
|
| 209 |
+
</tr>`;
|
| 210 |
+
};
|
| 211 |
+
|
| 212 |
+
const renderPage = () => {
|
| 213 |
+
const total = allRows.length;
|
| 214 |
+
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
| 215 |
+
if (currentPage > totalPages) currentPage = totalPages;
|
| 216 |
+
if (currentPage < 1) currentPage = 1;
|
| 217 |
+
if (total === 0) {
|
| 218 |
+
countLabel.textContent = "0 records";
|
| 219 |
+
tbody.innerHTML = '<tr><td colspan="9" class="empty">No measurements yet</td></tr>';
|
| 220 |
+
} else {
|
| 221 |
+
const start = (currentPage - 1) * PAGE_SIZE;
|
| 222 |
+
const slice = allRows.slice(start, start + PAGE_SIZE);
|
| 223 |
+
countLabel.textContent = `${total} records · page ${currentPage}/${totalPages}`;
|
| 224 |
+
tbody.innerHTML = slice.map(rowHtml).join("");
|
| 225 |
+
}
|
| 226 |
+
prevPageBtn.disabled = currentPage <= 1;
|
| 227 |
+
nextPageBtn.disabled = currentPage >= totalPages;
|
| 228 |
+
};
|
| 229 |
+
|
| 230 |
const loadData = async () => {
|
| 231 |
try {
|
| 232 |
const resp = await fetch(`/api/admin/measurements?token=${encodeURIComponent(adminToken)}`);
|
|
|
|
| 237 |
loginError.textContent = "Session expired. Please log in again.";
|
| 238 |
return;
|
| 239 |
}
|
| 240 |
+
allRows = await resp.json();
|
| 241 |
+
currentPage = 1;
|
| 242 |
+
renderPage();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
} catch (e) {
|
| 244 |
+
tbody.innerHTML = `<tr><td colspan="9" class="empty">Error loading data: ${e.message}</td></tr>`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
}
|
| 246 |
};
|
| 247 |
|
|
|
|
| 255 |
});
|
| 256 |
const result = await resp.json();
|
| 257 |
if (result.success) {
|
| 258 |
+
allRows = allRows.filter((r) => r.id !== id);
|
| 259 |
+
renderPage();
|
|
|
|
| 260 |
} else {
|
| 261 |
alert("Delete failed: " + (result.error || "unknown"));
|
| 262 |
}
|
|
|
|
| 265 |
}
|
| 266 |
};
|
| 267 |
|
| 268 |
+
prevPageBtn.addEventListener("click", () => { currentPage--; renderPage(); });
|
| 269 |
+
nextPageBtn.addEventListener("click", () => { currentPage++; renderPage(); });
|
| 270 |
document.getElementById("refreshBtn").addEventListener("click", loadData);
|
| 271 |
</script>
|
| 272 |
</body>
|
web_demo/templates/index.html
CHANGED
|
@@ -15,7 +15,7 @@
|
|
| 15 |
<p class="hero-kicker">Femometer Smart Ring Sizer</p>
|
| 16 |
<h1>Upload a photo to quickly measure ring size</h1>
|
| 17 |
<p class="hero-sub">
|
| 18 |
-
|
| 19 |
</p>
|
| 20 |
</div>
|
| 21 |
<div class="hero-card">
|
|
|
|
| 15 |
<p class="hero-kicker">Femometer Smart Ring Sizer</p>
|
| 16 |
<h1>Upload a photo to quickly measure ring size</h1>
|
| 17 |
<p class="hero-sub">
|
| 18 |
+
Using a photo of your hand with a card of standard credit card size for scale.
|
| 19 |
</p>
|
| 20 |
</div>
|
| 21 |
<div class="hero-card">
|