diff --git a/internal/service/static/main.js b/internal/service/static/main.js index bb6c015..8e88a94 100644 --- a/internal/service/static/main.js +++ b/internal/service/static/main.js @@ -120,6 +120,7 @@ class Odidere { this.#clearAttachments(); this.$textInput.value = ''; this.$textInput.style.height = 'auto'; + this.#updateSendButtonState(); } // ==================== @@ -258,6 +259,7 @@ class Odidere { // Render the active path. const activePath = this.getActivePath(); this.#renderMessages(activePath); + this.#updateSendButtonState(); } catch (e) { console.error('failed to load history:', e); this.messagesMap.clear(); @@ -349,6 +351,7 @@ class Odidere { } this.#saveToStorage(); + this.#updateSendButtonState(); return messages; } @@ -449,6 +452,7 @@ class Odidere { } this.#saveToStorage(); + this.#updateSendButtonState(); } /** @@ -572,6 +576,7 @@ class Odidere { const maxHeight = parseFloat(computed.maxHeight); const newHeight = Math.min(textarea.scrollHeight, maxHeight); textarea.style.height = `${newHeight}px`; + this.#updateSendButtonState(); }; /** @@ -592,6 +597,7 @@ class Odidere { this.attachments.push(file); } this.#renderAttachments(); + this.#updateSendButtonState(); this.$fileInput.value = ''; } @@ -1025,9 +1031,6 @@ class Odidere { */ async #submitText() { const text = this.$textInput.value.trim(); - const hasContent = text || this.attachments.length > 0; - - if (!hasContent) return; if (this.isProcessing) return; this.#stopCurrentAudio(); @@ -1060,20 +1063,25 @@ class Odidere { this.currentController = new AbortController(); try { - // Build the user message with text and attachments. - const userMessage = await this.#buildUserMessage(text, this.attachments); - const hasContent = text || this.attachments.length > 0 || audio; - if (!hasContent) { - this.#setLoadingState(false); - return; - } + const hasNewInput = text || this.attachments.length > 0 || audio; // Build messages array from active path + current user message. const activePath = this.getActivePath(); - const messages = [ - ...activePath.map(({ parent, children, id, meta, ...msg }) => msg), - userMessage, - ]; + let messages; + let userMessage; + + if (hasNewInput) { + userMessage = await this.#buildUserMessage(text, this.attachments); + messages = [ + ...activePath.map(({ parent, children, id, meta, ...msg }) => msg), + userMessage, + ]; + } else { + // Continue conversation: send existing history without a new user message. + messages = activePath.map( + ({ parent, children, id, meta, ...msg }) => msg, + ); + } const payload = { messages, @@ -1091,9 +1099,9 @@ class Odidere { // Clear attachments after building payload (before async operations). this.#clearAttachments(); - // For text-only requests (no audio), add to history and render - // immediately. Use the returned messages (with IDs) for rendering. - if (!audio) { + // For text-only requests with new input, add to history and render + // immediately. Skip when continuing conversation (no new user message). + if (!audio && hasNewInput) { const appended = this.#appendHistory([userMessage]); this.#renderMessages(appended); } @@ -1310,6 +1318,23 @@ class Odidere { // ==================== // STATE // ==================== + /** + * #updateSendButtonState enables the send button when there is text, + * attachments, or at least one message in the conversation history, + * and disables it otherwise. + */ + #updateSendButtonState() { + const hasText = this.$textInput.value.trim().length > 0; + const hasAttachments = this.attachments.length > 0; + const hasHistory = this.messagesMap.size > 0; + const enabled = hasText || hasAttachments || hasHistory; + this.$send.disabled = !enabled; + this.$send.setAttribute( + 'aria-label', + enabled ? 'Send message' : 'Start a conversation to enable send', + ); + } + /** * #setLoadingState updates UI to reflect loading state. * @param {boolean} loading @@ -1813,6 +1838,7 @@ class Odidere { #removeAttachment(index) { this.attachments.splice(index, 1); this.#renderAttachments(); + this.#updateSendButtonState(); } /**