hh-shipments 0.3.6 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/public/app.js +54 -10
- package/public/index.html +4 -4
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -111,7 +111,10 @@ async function loadDash() {
|
|
|
111
111
|
async function runQuick(text, targetEl, preset) {
|
|
112
112
|
targetEl.innerHTML = '<p class="muted">Extracting…</p>';
|
|
113
113
|
try {
|
|
114
|
-
const r = await
|
|
114
|
+
const r = await Promise.race([
|
|
115
|
+
api.post('/api/intake/quick', { text, seller_name: preset && preset.seller, device: preset && preset.device }),
|
|
116
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error('extract timed out — check your connection and try again')), 45000)),
|
|
117
|
+
]);
|
|
115
118
|
let html = '';
|
|
116
119
|
r.added.forEach((a) => { html += `<div class="review">✅ Added <b>${esc(a.number)}</b> (${esc(a.carrier.toUpperCase())})${r.simulated ? ' <span class="sim-badge">SIMULATED</span>' : ''}</div>`; });
|
|
117
120
|
r.duplicates.forEach((n) => { html += `<div class="review">⛔ <b>${esc(n)}</b> is already tracked — skipped.</div>`; });
|
|
@@ -169,12 +172,18 @@ async function ocrFile(file, statusEl) {
|
|
|
169
172
|
}
|
|
170
173
|
|
|
171
174
|
async function handlePhotoFile(f) {
|
|
175
|
+
const target = $('photo-result');
|
|
172
176
|
try {
|
|
177
|
+
f = await shrinkImage(f);
|
|
178
|
+
const url = URL.createObjectURL(f);
|
|
179
|
+
target.innerHTML = `<div class="review"><img src="${url}" style="max-width:100%;border-radius:10px"><p class="muted">📖 Reading screenshot…</p></div>`;
|
|
173
180
|
const text = await ocrFile(f, $('ocr-status'));
|
|
174
181
|
// Upload original for reference, then run the quick flow on OCR text
|
|
175
182
|
const fd = new FormData();
|
|
176
183
|
fd.append('image', f);
|
|
177
|
-
|
|
184
|
+
let up;
|
|
185
|
+
try { up = await fetchTimeout('/api/intake/image', { method: 'POST', body: fd }, 60000); }
|
|
186
|
+
catch (e) { throw new Error('upload timed out — check your connection and try again'); }
|
|
178
187
|
if (!up.ok) throw new Error('upload failed');
|
|
179
188
|
const r = await runQuick(text, $('photo-result'));
|
|
180
189
|
if (r && !r.added.length && !r.needs_review.length && !r.duplicates.length) showOcrRead($('photo-result'), text);
|
|
@@ -203,25 +212,56 @@ function pastedImageFiles(e) {
|
|
|
203
212
|
return files;
|
|
204
213
|
}
|
|
205
214
|
|
|
206
|
-
//
|
|
207
|
-
//
|
|
215
|
+
// Shrink huge phone photos before OCR/upload: much faster to read and send,
|
|
216
|
+
// and less exposed to a stalled connection. Falls back to the original file.
|
|
217
|
+
async function shrinkImage(file, maxDim = 1600) {
|
|
218
|
+
try {
|
|
219
|
+
const bmp = await createImageBitmap(file);
|
|
220
|
+
const scale = Math.min(1, maxDim / Math.max(bmp.width, bmp.height));
|
|
221
|
+
if (scale >= 1) { bmp.close(); return file; }
|
|
222
|
+
const canvas = document.createElement('canvas');
|
|
223
|
+
canvas.width = Math.round(bmp.width * scale);
|
|
224
|
+
canvas.height = Math.round(bmp.height * scale);
|
|
225
|
+
canvas.getContext('2d').drawImage(bmp, 0, 0, canvas.width, canvas.height);
|
|
226
|
+
bmp.close();
|
|
227
|
+
const blob = await new Promise((res) => canvas.toBlob(res, 'image/jpeg', 0.85));
|
|
228
|
+
return blob ? new File([blob], 'label.jpg', { type: 'image/jpeg' }) : file;
|
|
229
|
+
} catch (e) { return file; }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function fetchTimeout(url, opts = {}, ms = 60000) {
|
|
233
|
+
const c = new AbortController();
|
|
234
|
+
const t = setTimeout(() => c.abort(), ms);
|
|
235
|
+
try { return await fetch(url, { ...opts, signal: c.signal }); }
|
|
236
|
+
finally { clearTimeout(t); }
|
|
237
|
+
}
|
|
208
238
|
async function handlePastedImageFile(f) {
|
|
239
|
+
const target = $('quick-result');
|
|
209
240
|
try {
|
|
241
|
+
// Instant feedback: show a thumbnail + progress the moment the image lands
|
|
242
|
+
f = await shrinkImage(f);
|
|
243
|
+
const url = URL.createObjectURL(f);
|
|
244
|
+
target.innerHTML = `<div class="review"><img src="${url}" style="max-width:100%;border-radius:10px"><p class="muted" id="ocr-live">📖 Reading screenshot…</p></div>`;
|
|
210
245
|
const text = await ocrFile(f, $('ocr-status-paste'));
|
|
246
|
+
const live = $('ocr-live');
|
|
247
|
+
if (live) live.textContent = '⬆️ Uploading…';
|
|
211
248
|
const fd = new FormData();
|
|
212
249
|
fd.append('image', f);
|
|
213
|
-
|
|
250
|
+
let up;
|
|
251
|
+
try { up = await fetchTimeout('/api/intake/image', { method: 'POST', body: fd }, 60000); }
|
|
252
|
+
catch (e) { throw new Error('upload timed out — check your connection and try again'); }
|
|
214
253
|
if (!up.ok) throw new Error('upload failed');
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
254
|
+
if (live) live.textContent = '🔍 Finding tracking number…';
|
|
255
|
+
const r = await runQuick(text, target);
|
|
256
|
+
if (r && !r.added.length && !r.needs_review.length && !r.duplicates.length) showOcrRead(target, text);
|
|
257
|
+
} catch (err) { $('ocr-status-paste').textContent = ''; target.innerHTML = `<p class="error">${esc(err.message)}</p>`; }
|
|
218
258
|
}
|
|
219
259
|
|
|
220
260
|
$('tab-paste').addEventListener('paste', async (e) => {
|
|
221
261
|
const files = pastedImageFiles(e);
|
|
222
262
|
if (!files.length) return; // text paste — let it land in the textarea
|
|
223
263
|
e.preventDefault();
|
|
224
|
-
$('paste-box').innerHTML = '📋 <span class="muted">or tap here
|
|
264
|
+
$('paste-box').innerHTML = '📋 <span class="muted">or tap here and paste a screenshot</span>';
|
|
225
265
|
for (const f of files) await handlePastedImageFile(f);
|
|
226
266
|
});
|
|
227
267
|
|
|
@@ -383,7 +423,11 @@ let searchT;
|
|
|
383
423
|
$('search').addEventListener('input', () => { clearTimeout(searchT); searchT = setTimeout(loadDash, 400); });
|
|
384
424
|
let searchAT;
|
|
385
425
|
$('search-arch').addEventListener('input', () => { clearTimeout(searchAT); searchAT = setTimeout(loadArchive, 400); });
|
|
386
|
-
$('btn-quick').onclick = () =>
|
|
426
|
+
$('btn-quick').onclick = () => {
|
|
427
|
+
const t = $('paste-text').value.trim();
|
|
428
|
+
if (!t) { $('quick-result').innerHTML = '<p class="muted">Paste the seller\'s text above first — or tap <b>📋 Paste screenshot</b>, screenshots extract on their own.</p>'; return; }
|
|
429
|
+
runQuick(t, $('quick-result'));
|
|
430
|
+
};
|
|
387
431
|
$('btn-manual').onclick = async () => {
|
|
388
432
|
$('manual-err').textContent = '';
|
|
389
433
|
try {
|
package/public/index.html
CHANGED
|
@@ -61,11 +61,11 @@
|
|
|
61
61
|
<button class="tab" data-tab="manual">Manual</button>
|
|
62
62
|
</div>
|
|
63
63
|
<div id="tab-paste" class="tabpanel">
|
|
64
|
-
<textarea id="paste-text" rows="
|
|
65
|
-
<div
|
|
66
|
-
<div class="
|
|
64
|
+
<textarea id="paste-text" rows="4" placeholder="Paste the seller's message here…"></textarea>
|
|
65
|
+
<div class="row" style="margin:8px 0"><button id="btn-clipboard" class="primary">📋 Paste screenshot — extracts automatically</button></div>
|
|
66
|
+
<div id="paste-box" class="pastebox" contenteditable="true" spellcheck="false">📋 <span class="muted">or tap here and paste a screenshot</span></div>
|
|
67
67
|
<div id="ocr-status-paste" class="muted"></div>
|
|
68
|
-
<button id="btn-quick" class="
|
|
68
|
+
<button id="btn-quick" class="secondary">Extract from text above</button>
|
|
69
69
|
<div id="quick-result"></div>
|
|
70
70
|
</div>
|
|
71
71
|
<div id="tab-photo" class="tabpanel" hidden>
|