feat(ui): show waiting state during AI extraction

Hide input controls and display a spinner with "Waiting for
response..." while LLM extraction requests are in-flight. Controls
are restored on completion or failure. Applied consistently across
scan, roaster, and roast forms.
This commit is contained in:
Jon Seager 2026-02-03 19:25:19 +00:00
parent 3e3c57a07f
commit e3db350609
No known key found for this signature in database
3 changed files with 33 additions and 15 deletions

View file

@ -32,9 +32,11 @@
if (_extracting) return; if (_extracting) return;
_extracting = true; _extracting = true;
var errorEl = document.getElementById(formId + '-extract-error'); var errorEl = document.getElementById(formId + '-extract-error');
var spinnerEl = document.getElementById(formId + '-extract-spinner'); var controlsEl = document.getElementById(formId + '-extract-controls');
var waitingEl = document.getElementById(formId + '-extract-waiting');
errorEl.classList.add('hidden'); errorEl.classList.add('hidden');
spinnerEl.classList.remove('hidden'); controlsEl.classList.add('hidden');
waitingEl.classList.remove('hidden');
try { try {
var resp = await fetch(endpoint, { var resp = await fetch(endpoint, {
@ -53,7 +55,8 @@
errorEl.textContent = 'Extraction failed: ' + e.message; errorEl.textContent = 'Extraction failed: ' + e.message;
errorEl.classList.remove('hidden'); errorEl.classList.remove('hidden');
} finally { } finally {
spinnerEl.classList.add('hidden'); waitingEl.classList.add('hidden');
controlsEl.classList.remove('hidden');
_extracting = false; _extracting = false;
} }
} }
@ -114,7 +117,7 @@
> >
{% if has_ai_extract %} {% if has_ai_extract %}
<div class="relative border-b border-amber-200 pb-4"> <div class="relative border-b border-amber-200 pb-4">
<div class="flex flex-wrap items-center gap-3"> <div id="roaster-form-extract-controls" class="flex flex-wrap items-center gap-3">
<button <button
type="button" type="button"
onclick="triggerPhotoExtract('roaster-form', '/api/v1/extract-roaster')" onclick="triggerPhotoExtract('roaster-form', '/api/v1/extract-roaster')"
@ -142,10 +145,13 @@
Go Go
</button> </button>
</div> </div>
<svg id="roaster-form-extract-spinner" class="hidden h-4 w-4 animate-spin text-amber-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" aria-hidden="true"> </div>
<div id="roaster-form-extract-waiting" class="hidden flex items-center gap-3 text-sm text-amber-700">
<svg class="h-4 w-4 animate-spin text-amber-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" aria-hidden="true">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path> <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg> </svg>
Waiting for response&hellip;
</div> </div>
<p id="roaster-form-extract-error" class="hidden mt-2 text-sm text-red-600"></p> <p id="roaster-form-extract-error" class="hidden mt-2 text-sm text-red-600"></p>
</div> </div>

View file

@ -32,9 +32,11 @@
if (_extracting) return; if (_extracting) return;
_extracting = true; _extracting = true;
var errorEl = document.getElementById(formId + '-extract-error'); var errorEl = document.getElementById(formId + '-extract-error');
var spinnerEl = document.getElementById(formId + '-extract-spinner'); var controlsEl = document.getElementById(formId + '-extract-controls');
var waitingEl = document.getElementById(formId + '-extract-waiting');
errorEl.classList.add('hidden'); errorEl.classList.add('hidden');
spinnerEl.classList.remove('hidden'); controlsEl.classList.add('hidden');
waitingEl.classList.remove('hidden');
try { try {
var resp = await fetch(endpoint, { var resp = await fetch(endpoint, {
@ -53,7 +55,8 @@
errorEl.textContent = 'Extraction failed: ' + e.message; errorEl.textContent = 'Extraction failed: ' + e.message;
errorEl.classList.remove('hidden'); errorEl.classList.remove('hidden');
} finally { } finally {
spinnerEl.classList.add('hidden'); waitingEl.classList.add('hidden');
controlsEl.classList.remove('hidden');
_extracting = false; _extracting = false;
} }
} }
@ -143,7 +146,7 @@
> >
{% if has_ai_extract %} {% if has_ai_extract %}
<div class="relative border-b border-amber-200 pb-4"> <div class="relative border-b border-amber-200 pb-4">
<div class="flex flex-wrap items-center gap-3"> <div id="roast-form-extract-controls" class="flex flex-wrap items-center gap-3">
<button <button
type="button" type="button"
onclick="triggerPhotoExtract('roast-form', '/api/v1/extract-roast')" onclick="triggerPhotoExtract('roast-form', '/api/v1/extract-roast')"
@ -171,10 +174,13 @@
Go Go
</button> </button>
</div> </div>
<svg id="roast-form-extract-spinner" class="hidden h-4 w-4 animate-spin text-amber-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" aria-hidden="true"> </div>
<div id="roast-form-extract-waiting" class="hidden flex items-center gap-3 text-sm text-amber-700">
<svg class="h-4 w-4 animate-spin text-amber-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" aria-hidden="true">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path> <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg> </svg>
Waiting for response&hellip;
</div> </div>
<p id="roast-form-extract-error" class="hidden mt-2 text-sm text-red-600"></p> <p id="roast-form-extract-error" class="hidden mt-2 text-sm text-red-600"></p>
</div> </div>

View file

@ -32,9 +32,11 @@
if (_extracting) return; if (_extracting) return;
_extracting = true; _extracting = true;
var errorEl = document.getElementById('scan-extract-error'); var errorEl = document.getElementById('scan-extract-error');
var spinnerEl = document.getElementById('scan-extract-spinner'); var controlsEl = document.getElementById('scan-extract-controls');
var waitingEl = document.getElementById('scan-extract-waiting');
errorEl.classList.add('hidden'); errorEl.classList.add('hidden');
spinnerEl.classList.remove('hidden'); controlsEl.classList.add('hidden');
waitingEl.classList.remove('hidden');
try { try {
var resp = await fetch('/api/v1/extract-bag-scan', { var resp = await fetch('/api/v1/extract-bag-scan', {
@ -55,7 +57,8 @@
errorEl.textContent = 'Extraction failed: ' + e.message; errorEl.textContent = 'Extraction failed: ' + e.message;
errorEl.classList.remove('hidden'); errorEl.classList.remove('hidden');
} finally { } finally {
spinnerEl.classList.add('hidden'); waitingEl.classList.add('hidden');
controlsEl.classList.remove('hidden');
_extracting = false; _extracting = false;
} }
} }
@ -136,7 +139,7 @@
<!-- Input section: photo or text --> <!-- Input section: photo or text -->
<div id="scan-input-section" class="mt-6 rounded-lg border border-amber-300 bg-amber-100/80 p-5 shadow-sm"> <div id="scan-input-section" class="mt-6 rounded-lg border border-amber-300 bg-amber-100/80 p-5 shadow-sm">
<div class="flex flex-wrap items-center gap-3"> <div id="scan-extract-controls" class="flex flex-wrap items-center gap-3">
<button <button
type="button" type="button"
onclick="triggerScanPhoto()" onclick="triggerScanPhoto()"
@ -164,10 +167,13 @@
Go Go
</button> </button>
</div> </div>
<svg id="scan-extract-spinner" class="hidden h-5 w-5 animate-spin text-amber-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" aria-hidden="true"> </div>
<div id="scan-extract-waiting" class="hidden flex items-center gap-3 text-sm text-amber-700">
<svg class="h-5 w-5 animate-spin text-amber-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" aria-hidden="true">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path> <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg> </svg>
Waiting for response&hellip;
</div> </div>
<p id="scan-extract-error" class="hidden mt-2 text-sm text-red-600"></p> <p id="scan-extract-error" class="hidden mt-2 text-sm text-red-600"></p>
</div> </div>