feng-x commited on
Commit
b6cb9cf
·
verified ·
1 Parent(s): 58f432a

Upload folder using huggingface_hub

Browse files
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, 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:
 
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); margin-left: 4px;
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="19" class="empty">Loading...</td></tr>
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
- const rows = await resp.json();
227
- countLabel.textContent = `${rows.length} records`;
228
- if (rows.length === 0) {
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, '&quot;') + '">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="17" class="empty">Error loading data: ${e.message}</td></tr>`;
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
- tr.remove();
316
- const rows = tbody.querySelectorAll("tr");
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>&lsaquo; Prev</button>
104
+ <button id="nextPageBtn" disabled>Next &rsaquo;</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
- Use a hand photo with a card of standard credit card dimensions for scale.
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">