mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-07-10 12:17:11 +00:00
Support mobile enter for queued agent prompts
This commit is contained in:
+138
-10
@@ -53,6 +53,69 @@ window.uiModule = uiModule;
|
||||
window.adminModule = adminModule;
|
||||
window.cookbookModule = cookbookModule;
|
||||
|
||||
function _isMobileChatInput() {
|
||||
return window.innerWidth <= 768 || (window.matchMedia && window.matchMedia('(pointer: coarse)').matches);
|
||||
}
|
||||
|
||||
function _isForegroundChatBusy() {
|
||||
const sendBtn = document.querySelector('.send-btn');
|
||||
return !!window.__odysseusChatBusy
|
||||
|| Date.now() < (window.__odysseusChatBusyUntil || 0)
|
||||
|| !!document.querySelector('.send-btn[data-mode="streaming"], .send-btn.send-pending')
|
||||
|| (sendBtn && (sendBtn.title || '').toLowerCase().includes('stop'));
|
||||
}
|
||||
|
||||
function _shouldQueueFromMobileEnter(e, input) {
|
||||
return e.key === 'Enter'
|
||||
&& !e.shiftKey
|
||||
&& !e.ctrlKey
|
||||
&& !e.metaKey
|
||||
&& !e.altKey
|
||||
&& !e.isComposing
|
||||
&& _isMobileChatInput()
|
||||
&& _isForegroundChatBusy()
|
||||
&& !!(input && input.value && input.value.trim());
|
||||
}
|
||||
|
||||
function _shouldQueueFromMobileLineBreak(input) {
|
||||
return _isMobileChatInput()
|
||||
&& _isForegroundChatBusy()
|
||||
&& !!(input && input.value && input.value.trim());
|
||||
}
|
||||
|
||||
function _isLineBreakInputEvent(e) {
|
||||
return e
|
||||
&& (e.inputType === 'insertLineBreak'
|
||||
|| e.inputType === 'insertParagraph'
|
||||
|| e.data === '\n');
|
||||
}
|
||||
|
||||
function _submitMobileQueuedInput(input) {
|
||||
if (!input || !_shouldQueueFromMobileLineBreak(input)) return false;
|
||||
const now = Date.now();
|
||||
const last = Number(input.dataset.mobileQueueSubmitAt || 0);
|
||||
if (now - last < 300) return true;
|
||||
input.dataset.mobileQueueSubmitAt = String(now);
|
||||
if (chatModule && chatModule.queueStreamingComposerRequest && chatModule.queueStreamingComposerRequest()) {
|
||||
return true;
|
||||
}
|
||||
window.__odysseusQueueStreamingSubmit = now;
|
||||
const form = document.getElementById('chat-form');
|
||||
const submitBtn = form && form.querySelector('button[type="submit"]');
|
||||
if (submitBtn) submitBtn.click();
|
||||
else if (form) form.requestSubmit ? form.requestSubmit() : form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
|
||||
return true;
|
||||
}
|
||||
|
||||
function _syncMobileEnterKeyHint(input) {
|
||||
if (!input) return;
|
||||
input.setAttribute('enterkeyhint', (_isMobileChatInput() && _isForegroundChatBusy()) ? 'send' : 'enter');
|
||||
}
|
||||
|
||||
function _countLineBreaks(s) {
|
||||
return ((s || '').match(/\n/g) || []).length;
|
||||
}
|
||||
|
||||
function initForegroundActivityHeartbeat() {
|
||||
let lastSent = 0;
|
||||
const minGapMs = 12000;
|
||||
@@ -3226,17 +3289,38 @@ function initializeEventListeners() {
|
||||
// Textarea auto-resize
|
||||
const textarea = el('message');
|
||||
if (textarea) {
|
||||
_syncMobileEnterKeyHint(textarea);
|
||||
window.addEventListener('odysseus:chat-busy-change', () => _syncMobileEnterKeyHint(textarea));
|
||||
uiModule.autoResize(textarea);
|
||||
textarea.addEventListener('input', () => {
|
||||
let previousTextareaValue = textarea.value || '';
|
||||
textarea.addEventListener('beforeinput', (e) => {
|
||||
if (_isLineBreakInputEvent(e) && _shouldQueueFromMobileLineBreak(textarea)) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
_submitMobileQueuedInput(textarea);
|
||||
}
|
||||
});
|
||||
textarea.addEventListener('input', (e) => {
|
||||
const currentValue = textarea.value || '';
|
||||
const insertedLineBreak = _isLineBreakInputEvent(e)
|
||||
|| _countLineBreaks(currentValue) > _countLineBreaks(previousTextareaValue);
|
||||
if (insertedLineBreak && _shouldQueueFromMobileLineBreak(textarea)) {
|
||||
textarea.value = currentValue.replace(/\n+$/g, '');
|
||||
previousTextareaValue = textarea.value || '';
|
||||
_submitMobileQueuedInput(textarea);
|
||||
return;
|
||||
}
|
||||
previousTextareaValue = currentValue;
|
||||
uiModule.autoResize(textarea);
|
||||
_syncMobileEnterKeyHint(textarea);
|
||||
});
|
||||
textarea.addEventListener('paste', () => {
|
||||
setTimeout(() => uiModule.autoResize(textarea), 1);
|
||||
});
|
||||
textarea.addEventListener('keydown', (e) => {
|
||||
const isMobile = window.innerWidth <= 768
|
||||
const isMobile = _isMobileChatInput();
|
||||
|
||||
if (e.key === 'Enter' && !e.shiftKey && !e.isComposing && !isMobile) {
|
||||
if (_shouldQueueFromMobileEnter(e, textarea) || (e.key === 'Enter' && !e.shiftKey && !e.isComposing && !isMobile)) {
|
||||
// If ghost autocomplete is active, accept the suggestion instead of submitting
|
||||
if (window._ghostAutocomplete && window._ghostAutocomplete.isActive()) {
|
||||
e.preventDefault();
|
||||
@@ -3249,8 +3333,14 @@ function initializeEventListeners() {
|
||||
// Check if already submitting before triggering form submission
|
||||
const form = el('chat-form');
|
||||
if (form) {
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
if (submitBtn) submitBtn.click();
|
||||
if (_isForegroundChatBusy() && textarea.value && textarea.value.trim()) {
|
||||
if (chatModule && chatModule.queueStreamingComposerRequest && chatModule.queueStreamingComposerRequest()) {
|
||||
return;
|
||||
}
|
||||
window.__odysseusQueueStreamingSubmit = Date.now();
|
||||
}
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
if (submitBtn) submitBtn.click();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -3685,10 +3775,31 @@ function startOdysseusApp() {
|
||||
return fileHandlerModule.getPendingCount && fileHandlerModule.getPendingCount() > 0;
|
||||
}
|
||||
|
||||
function _updateStreamingSubmitButton() {
|
||||
if (!sendBtn || sendBtn.dataset.mode !== 'streaming') return false;
|
||||
const hasText = messageInput && messageInput.value.trim().length > 0;
|
||||
const nextPhase = hasText ? 'queue' : 'processing';
|
||||
if (sendBtn.dataset.phase === nextPhase) return true;
|
||||
sendBtn.dataset.phase = nextPhase;
|
||||
sendBtn.classList.remove('mic-mode', 'newchat-mode', 'newchat-expanded', 'anim-spin', 'anim-launch', 'anim-land');
|
||||
if (hasText) {
|
||||
sendBtn.innerHTML = _sendIcon;
|
||||
sendBtn.title = 'Queue message';
|
||||
} else {
|
||||
sendBtn.innerHTML = _stopIcon;
|
||||
sendBtn.title = 'Stop generation';
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function _updateSendBtnIcon() {
|
||||
if (!sendBtn) return;
|
||||
// Don't override if streaming (stop button) or recording
|
||||
if (sendBtn.dataset.mode === 'streaming' || sendBtn.dataset.mode === 'recording') return;
|
||||
if (sendBtn.dataset.mode === 'streaming') {
|
||||
_updateStreamingSubmitButton();
|
||||
return;
|
||||
}
|
||||
// Don't override if recording
|
||||
if (sendBtn.dataset.mode === 'recording') return;
|
||||
const prevMode = sendBtn.dataset.mode || '';
|
||||
const hasText = messageInput && messageInput.value.trim().length > 0;
|
||||
const hasFiles = _hasAttachments();
|
||||
@@ -3784,6 +3895,12 @@ function startOdysseusApp() {
|
||||
const hasText = messageInput && messageInput.value.trim().length > 0;
|
||||
const hasFiles = _hasAttachments();
|
||||
|
||||
if (sendBtn.dataset.mode === 'streaming') {
|
||||
if (hasText) window.__odysseusQueueStreamingSubmit = Date.now();
|
||||
handleSubmit(e);
|
||||
return;
|
||||
}
|
||||
|
||||
// New chat mode — empty input, no attachments, no STT
|
||||
if (!hasText && !hasFiles && sendBtn.dataset.mode === 'newchat') {
|
||||
if (sessionModule) {
|
||||
@@ -3823,9 +3940,10 @@ function startOdysseusApp() {
|
||||
// Enter to send (shift+enter for newline), or new chat when empty
|
||||
if (messageInput) {
|
||||
messageInput.addEventListener('keydown', (e) => {
|
||||
const isMobile = window.innerWidth <= 768
|
||||
if (e.defaultPrevented) return;
|
||||
const isMobile = _isMobileChatInput();
|
||||
|
||||
if (e.key === 'Enter' && !e.shiftKey && !e.isComposing && !isMobile) {
|
||||
if (_shouldQueueFromMobileEnter(e, messageInput) || (e.key === 'Enter' && !e.shiftKey && !e.isComposing && !isMobile)) {
|
||||
e.preventDefault();
|
||||
// Flush the debounced icon update so dataset.mode reflects the current
|
||||
// text state. Without this, a fast type-and-Enter would still see the
|
||||
@@ -3836,6 +3954,12 @@ function startOdysseusApp() {
|
||||
if (railNew) railNew.click();
|
||||
return;
|
||||
}
|
||||
if (_isForegroundChatBusy() && messageInput.value && messageInput.value.trim()) {
|
||||
if (chatModule && chatModule.queueStreamingComposerRequest && chatModule.queueStreamingComposerRequest()) {
|
||||
return;
|
||||
}
|
||||
window.__odysseusQueueStreamingSubmit = Date.now();
|
||||
}
|
||||
handleSubmit(e);
|
||||
}
|
||||
});
|
||||
@@ -3855,7 +3979,11 @@ function startOdysseusApp() {
|
||||
_syncModelPickerAutohide();
|
||||
messageInput.addEventListener('input', () => {
|
||||
_syncModelPickerAutohide();
|
||||
_debouncedUpdateIcon();
|
||||
if (sendBtn && sendBtn.dataset.mode === 'streaming') {
|
||||
_updateSendBtnIcon();
|
||||
} else {
|
||||
_debouncedUpdateIcon();
|
||||
}
|
||||
}, { passive: true });
|
||||
}
|
||||
|
||||
|
||||
+31
-15
@@ -48,6 +48,7 @@ import { wireArrowUpRecall, getLastUserMessageFromChatHistory } from './composer
|
||||
try {
|
||||
window.__odysseusChatBusy = !!active;
|
||||
window.__odysseusChatBusyUntil = active ? Date.now() + 120000 : Date.now() + 1200;
|
||||
window.dispatchEvent(new CustomEvent('odysseus:chat-busy-change', { detail: { active: !!active } }));
|
||||
} catch (_) {}
|
||||
}
|
||||
let _pendingContinue = null; // Stores the stopped AI element to merge with new response
|
||||
@@ -326,7 +327,12 @@ import { wireArrowUpRecall, getLastUserMessageFromChatHistory } from './composer
|
||||
// arrow out for the stop icon — otherwise the swap happens mid-flight
|
||||
// and the user sees nothing fly out.
|
||||
setTimeout(() => {
|
||||
submitBtn.innerHTML = _stopSvg;
|
||||
if (submitBtn.dataset.mode !== 'streaming') return;
|
||||
const msgInput = uiModule.el('message');
|
||||
const hasQueuedText = !!(msgInput && msgInput.value && msgInput.value.trim());
|
||||
submitBtn.innerHTML = hasQueuedText && icons ? icons.send : _stopSvg;
|
||||
submitBtn.dataset.phase = hasQueuedText ? 'queue' : 'processing';
|
||||
submitBtn.title = hasQueuedText ? 'Queue message' : 'Stop generation';
|
||||
submitBtn.classList.remove('anim-launch');
|
||||
void submitBtn.offsetWidth;
|
||||
submitBtn.classList.add('anim-land');
|
||||
@@ -471,6 +477,24 @@ import { wireArrowUpRecall, getLastUserMessageFromChatHistory } from './composer
|
||||
return true;
|
||||
}
|
||||
|
||||
export function queueStreamingComposerRequest() {
|
||||
if (!isStreaming) return false;
|
||||
const queuedInput = uiModule.el('message');
|
||||
const queuedText = (queuedInput && queuedInput.value || '').trim();
|
||||
if (!queuedText) return false;
|
||||
if (fileHandlerModule.getPendingCount && fileHandlerModule.getPendingCount()) {
|
||||
try { uiModule.showError && uiModule.showError('Finish the current response before queueing messages with attachments.'); } catch (_) {}
|
||||
return true;
|
||||
}
|
||||
if (_queueAgentRequest(queuedText)) {
|
||||
queuedInput.value = '';
|
||||
queuedInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
if (uiModule.autoResize) uiModule.autoResize(queuedInput);
|
||||
try { window._updateSendBtnIcon && window._updateSendBtnIcon(); } catch (_) {}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function _drainQueuedAgentRequests() {
|
||||
if (isStreaming || _sendInFlight || !_queuedAgentRequests.length) return;
|
||||
if (_queuedDrainTimer) return;
|
||||
@@ -507,21 +531,13 @@ import { wireArrowUpRecall, getLastUserMessageFromChatHistory } from './composer
|
||||
return;
|
||||
}
|
||||
|
||||
// If currently streaming, a non-empty composer means "queue this next".
|
||||
// Empty composer keeps the existing Stop behavior.
|
||||
// If currently streaming, keyboard Enter can queue a non-empty composer.
|
||||
// Clicking the stop icon should still stop normally, even if text exists.
|
||||
if (isStreaming) {
|
||||
const queuedInput = uiModule.el('message');
|
||||
const queuedText = (queuedInput && queuedInput.value || '').trim();
|
||||
if (queuedText) {
|
||||
if (fileHandlerModule.getPendingCount && fileHandlerModule.getPendingCount()) {
|
||||
try { uiModule.showError && uiModule.showError('Finish the current response before queueing messages with attachments.'); } catch (_) {}
|
||||
return;
|
||||
}
|
||||
if (_queueAgentRequest(queuedText)) {
|
||||
queuedInput.value = '';
|
||||
queuedInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
if (uiModule.autoResize) uiModule.autoResize(queuedInput);
|
||||
}
|
||||
const queueRequestedAt = Number(window.__odysseusQueueStreamingSubmit || 0);
|
||||
const shouldQueueStreamingSubmit = queueRequestedAt && Date.now() - queueRequestedAt < 1200;
|
||||
window.__odysseusQueueStreamingSubmit = 0;
|
||||
if (shouldQueueStreamingSubmit && queueStreamingComposerRequest()) {
|
||||
return;
|
||||
}
|
||||
if (fileHandlerModule.isUploading && fileHandlerModule.isUploading()) {
|
||||
|
||||
Reference in New Issue
Block a user