mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-07-14 12:48:03 +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 });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user