Add stop button

This commit is contained in:
dwrz
2026-06-09 01:32:49 +00:00
parent ad58cd78ff
commit 6c8c04c804
5 changed files with 127 additions and 17 deletions

View File

@@ -29,12 +29,14 @@ class Odidere {
this.isRecording = false;
this.isMuted = false;
this.isResetPending = false;
this.isStopPending = false;
this.mediaRecorder = null;
// DOM Elements
this.$attach = document.getElementById('attach');
this.$attachments = document.getElementById('attachments');
this.$chat = document.getElementById('chat');
this.$chatLoading = document.getElementById('chat-loading');
this.$fileInput = document.getElementById('file-input');
this.$model = document.getElementById('model');
this.$ptt = document.getElementById('ptt');
@@ -141,8 +143,9 @@ class Odidere {
this.$textInput.addEventListener('keydown', this.#handleTextareaKeyDown);
this.$textInput.addEventListener('input', this.#handleTextareaInput);
// Send button
this.$send.addEventListener('click', () => this.#submitText());
// Send button: when loading, acts as stop button with two-press safety
this.$send.addEventListener('click', () => this.#handleSendClick());
this.document.addEventListener('click', (e) => this.#handleStopCancel(e));
// Reset button: two-press safety
this.$reset.addEventListener('click', () => this.#handleResetClick());
@@ -356,6 +359,58 @@ class Odidere {
this.$reset.classList.remove('footer__toolbar-btn--pending');
}
/**
* #handleSendClick routes to either send or stop based on loading state.
*/
#handleSendClick() {
if (this.$send.classList.contains('loading')) {
this.#handleStopClick();
} else {
this.#submitText();
}
}
/**
* #handleStopClick toggles the pending stop state on first press,
* aborts the current request on second press.
*/
#handleStopClick() {
if (this.isStopPending) {
this.isStopPending = false;
this.$send.classList.remove('pending');
this.#stopConversation();
} else {
this.isStopPending = true;
this.$send.classList.add('pending');
}
}
/**
* #handleStopCancel clears the pending stop state when the user
* clicks anywhere outside the send button.
* @param {MouseEvent} event
*/
#handleStopCancel(event) {
if (!this.isStopPending) return;
if (this.$send.contains(event.target)) return;
this.isStopPending = false;
this.$send.classList.remove('pending');
}
/**
* #stopConversation aborts the current streaming request and audio.
*/
#stopConversation() {
this.#stopCurrentAudio();
if (this.currentController) {
this.currentController.abort();
this.currentController = null;
}
this.#setLoadingState(false);
}
// ====================
// AUDIO RECORDING
// ====================
@@ -887,8 +942,17 @@ class Odidere {
#setLoadingState(loading) {
this.isProcessing = loading;
this.$send.classList.toggle('loading', loading);
this.$send.disabled = loading;
this.$send.setAttribute('aria-label', loading ? 'Stop' : 'Send message');
// Show/hide the spinner in the chat area.
// Positioning is handled by #renderMessages to ensure it's always at the end.
this.$chatLoading.hidden = !loading;
// Keep the send button enabled during loading so it can act as a stop button.
this.$ptt.disabled = loading;
// Clear any pending stop state when loading ends.
if (!loading && this.isStopPending) {
this.isStopPending = false;
this.$send.classList.remove('pending');
}
}
/**
@@ -1182,6 +1246,11 @@ class Odidere {
this.#renderAssistantMessage(msg, msg.meta ?? meta, toolResults);
}
// Ensure the loading spinner is always at the end of the chat.
if (!this.$chatLoading.hidden) {
this.$chat.appendChild(this.$chatLoading);
}
}
/**