fix: send button morphs into a clickable STOP icon while working (no more disabled spinner); grouped bottom-left tools; silent-recording message; completed images auto-join the library; transient image-status retries
This commit is contained in:
parent
a4945d4d1d
commit
d6476f99d9
4 changed files with 33 additions and 13 deletions
|
|
@ -62,10 +62,12 @@
|
|||
<div id="assistant-attachments" class="assistant-attachments" aria-label="Attached images" hidden></div>
|
||||
<div class="assistant-composer-footer">
|
||||
<span id="assistant-autosave-state" class="assistant-autosave-state" aria-live="polite"></span>
|
||||
<label class="assistant-attach" for="assistant-attach-input" title="Attach up to 4 images (PNG, JPEG, WebP)"><i class="fas fa-paperclip"></i></label>
|
||||
<input type="file" id="assistant-attach-input" accept="image/png,image/jpeg,image/webp" multiple hidden>
|
||||
<button id="btn-assistant-voice" class="assistant-voice-btn" type="button" title="Voice conversation"><i class="fas fa-headset"></i></button>
|
||||
<button id="btn-assistant-mic" class="assistant-mic" type="button" title="Dictate your question"><i class="fas fa-microphone"></i></button>
|
||||
<div class="assistant-tools">
|
||||
<label class="assistant-attach" for="assistant-attach-input" title="Attach up to 4 images (PNG, JPEG, WebP)"><i class="fas fa-paperclip"></i></label>
|
||||
<input type="file" id="assistant-attach-input" accept="image/png,image/jpeg,image/webp" multiple hidden>
|
||||
<button id="btn-assistant-voice" class="assistant-voice-btn" type="button" title="Voice conversation"><i class="fas fa-headset"></i></button>
|
||||
<button id="btn-assistant-mic" class="assistant-mic" type="button" title="Dictate your question"><i class="fas fa-microphone"></i></button>
|
||||
</div>
|
||||
<button id="btn-assistant-send" class="btn-send" type="submit"><i class="fas fa-arrow-up"></i></button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -271,9 +271,10 @@ body.assistant-workspace .assistant-learning-view { min-height: 0; height: 100vh
|
|||
.assistant-composer { border:1px solid var(--g200); border-radius:20px; box-shadow:0 8px 30px rgba(0,0,0,.08); padding:10px 14px 6px; display:flex; flex-direction:column; gap:2px; }
|
||||
.assistant-composer > label { display:none; }
|
||||
#assistant-input { flex:1 1 auto; border:none; outline:none; resize:none; background:transparent; font-size:15px; line-height:1.5; min-height:56px; max-height:180px; padding:6px 0; }
|
||||
.assistant-composer-footer { display:flex; align-items:center; justify-content:space-between; gap:4px; border-top:1px solid var(--g100); padding-top:4px; margin-top:2px; }
|
||||
.assistant-composer-footer { display:flex; align-items:center; gap:2px; border-top:1px solid var(--g100); padding-top:4px; margin-top:2px; }
|
||||
.assistant-composer-footer .assistant-tools { display:flex; align-items:center; gap:2px; margin-right:auto; }
|
||||
.assistant-composer-footer .assistant-attach, .assistant-composer-footer .assistant-voice-btn, .assistant-composer-footer .assistant-mic { width:30px; height:30px; font-size:14px; }
|
||||
.assistant-composer-footer .btn-send { width:32px; height:32px; font-size:14px; }
|
||||
.assistant-composer-footer .btn-send { width:32px; height:32px; font-size:14px; margin-left:auto; }
|
||||
.assistant-attach, .assistant-voice-btn, .assistant-mic { width:38px; height:38px; border:none; background:none; border-radius:50%; display:flex; align-items:center; justify-content:center; color:var(--g600); cursor:pointer; font-size:16px; }
|
||||
.assistant-attach:hover, .assistant-voice-btn:hover, .assistant-mic:hover { background:var(--g100); }
|
||||
.assistant-mic.recording { background:var(--danger,#dc2626); color:#fff; }
|
||||
|
|
|
|||
|
|
@ -963,6 +963,11 @@ import {
|
|||
}
|
||||
};
|
||||
if (typeof transcribeAudio === 'function' && blob && blob.size > 100) {
|
||||
if (blob.size < 4000 && !live) {
|
||||
if (typeof showToast === 'function') showToast('Recording was too short or silent — speak closer to the microphone', 'error');
|
||||
finish('');
|
||||
return;
|
||||
}
|
||||
transcribeAudio(blob).then(function(data) {
|
||||
// Server transcription wins; the live transcript is the fallback,
|
||||
// never lose what the browser already heard.
|
||||
|
|
@ -1068,11 +1073,6 @@ import {
|
|||
resizeAssistantInput();
|
||||
if (status) status.textContent = 'Asking…';
|
||||
var send = document.getElementById('btn-assistant-send');
|
||||
if (send) {
|
||||
send.classList.toggle('busy', !!isBusy);
|
||||
var sendIcon = send.querySelector('i');
|
||||
if (sendIcon) sendIcon.className = isBusy ? 'fas fa-stop' : 'fas fa-arrow-up';
|
||||
}
|
||||
if (send) send.click();
|
||||
}
|
||||
|
||||
|
|
@ -2048,8 +2048,11 @@ import {
|
|||
}
|
||||
if (label) label.textContent = text || (isBusy ? 'Working...' : 'Ready');
|
||||
if (send) {
|
||||
send.disabled = !!isBusy;
|
||||
send.innerHTML = isBusy ? '<i class="fas fa-spinner fa-spin"></i> Searching' : '<i class="fas fa-paper-plane"></i> Ask';
|
||||
// The send button becomes a clickable STOP while the assistant works.
|
||||
send.classList.toggle('busy', !!isBusy);
|
||||
var icon = send.querySelector('i');
|
||||
if (icon) icon.className = isBusy ? 'fas fa-stop' : 'fas fa-arrow-up';
|
||||
send.setAttribute('aria-label', isBusy ? 'Stop generating' : 'Send');
|
||||
}
|
||||
if (input) input.disabled = !!isBusy;
|
||||
if (attachInput) attachInput.disabled = !!isBusy;
|
||||
|
|
|
|||
|
|
@ -86,6 +86,20 @@ test('regenerate replays the preceding question for the latest answer and never
|
|||
assert.match(all[all.length - 1].textContent, /Regenerated answer/);
|
||||
});
|
||||
|
||||
test('the send button becomes a clickable stop icon while the assistant works', async t => {
|
||||
const app = ui(t);
|
||||
const c = app.context;
|
||||
const send = app.document.getElementById('btn-assistant-send');
|
||||
assert.equal(send.querySelector('i').className, 'fas fa-arrow-up');
|
||||
c.setBusy(true, 'Working');
|
||||
assert.equal(send.disabled, false, 'still clickable so it can cancel');
|
||||
assert.ok(send.classList.contains('busy'));
|
||||
assert.equal(send.querySelector('i').className, 'fas fa-stop');
|
||||
c.setBusy(false, 'Ready');
|
||||
assert.equal(send.querySelector('i').className, 'fas fa-arrow-up');
|
||||
assert.ok(!send.classList.contains('busy'));
|
||||
});
|
||||
|
||||
test('editing a user message returns the question to the composer', async t => {
|
||||
const app = ui(t);
|
||||
const c = app.context;
|
||||
|
|
|
|||
Loading…
Reference in a new issue