mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-07-16 13:08:02 +00:00
Support mobile enter for queued agent prompts
This commit is contained in:
+135
-7
@@ -53,6 +53,69 @@ window.uiModule = uiModule;
|
|||||||
window.adminModule = adminModule;
|
window.adminModule = adminModule;
|
||||||
window.cookbookModule = cookbookModule;
|
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() {
|
function initForegroundActivityHeartbeat() {
|
||||||
let lastSent = 0;
|
let lastSent = 0;
|
||||||
const minGapMs = 12000;
|
const minGapMs = 12000;
|
||||||
@@ -3226,17 +3289,38 @@ function initializeEventListeners() {
|
|||||||
// Textarea auto-resize
|
// Textarea auto-resize
|
||||||
const textarea = el('message');
|
const textarea = el('message');
|
||||||
if (textarea) {
|
if (textarea) {
|
||||||
|
_syncMobileEnterKeyHint(textarea);
|
||||||
|
window.addEventListener('odysseus:chat-busy-change', () => _syncMobileEnterKeyHint(textarea));
|
||||||
uiModule.autoResize(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);
|
uiModule.autoResize(textarea);
|
||||||
|
_syncMobileEnterKeyHint(textarea);
|
||||||
});
|
});
|
||||||
textarea.addEventListener('paste', () => {
|
textarea.addEventListener('paste', () => {
|
||||||
setTimeout(() => uiModule.autoResize(textarea), 1);
|
setTimeout(() => uiModule.autoResize(textarea), 1);
|
||||||
});
|
});
|
||||||
textarea.addEventListener('keydown', (e) => {
|
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 ghost autocomplete is active, accept the suggestion instead of submitting
|
||||||
if (window._ghostAutocomplete && window._ghostAutocomplete.isActive()) {
|
if (window._ghostAutocomplete && window._ghostAutocomplete.isActive()) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -3249,6 +3333,12 @@ function initializeEventListeners() {
|
|||||||
// Check if already submitting before triggering form submission
|
// Check if already submitting before triggering form submission
|
||||||
const form = el('chat-form');
|
const form = el('chat-form');
|
||||||
if (form) {
|
if (form) {
|
||||||
|
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"]');
|
const submitBtn = form.querySelector('button[type="submit"]');
|
||||||
if (submitBtn) submitBtn.click();
|
if (submitBtn) submitBtn.click();
|
||||||
}
|
}
|
||||||
@@ -3685,10 +3775,31 @@ function startOdysseusApp() {
|
|||||||
return fileHandlerModule.getPendingCount && fileHandlerModule.getPendingCount() > 0;
|
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() {
|
function _updateSendBtnIcon() {
|
||||||
if (!sendBtn) return;
|
if (!sendBtn) return;
|
||||||
// Don't override if streaming (stop button) or recording
|
if (sendBtn.dataset.mode === 'streaming') {
|
||||||
if (sendBtn.dataset.mode === 'streaming' || sendBtn.dataset.mode === 'recording') return;
|
_updateStreamingSubmitButton();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Don't override if recording
|
||||||
|
if (sendBtn.dataset.mode === 'recording') return;
|
||||||
const prevMode = sendBtn.dataset.mode || '';
|
const prevMode = sendBtn.dataset.mode || '';
|
||||||
const hasText = messageInput && messageInput.value.trim().length > 0;
|
const hasText = messageInput && messageInput.value.trim().length > 0;
|
||||||
const hasFiles = _hasAttachments();
|
const hasFiles = _hasAttachments();
|
||||||
@@ -3784,6 +3895,12 @@ function startOdysseusApp() {
|
|||||||
const hasText = messageInput && messageInput.value.trim().length > 0;
|
const hasText = messageInput && messageInput.value.trim().length > 0;
|
||||||
const hasFiles = _hasAttachments();
|
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
|
// New chat mode — empty input, no attachments, no STT
|
||||||
if (!hasText && !hasFiles && sendBtn.dataset.mode === 'newchat') {
|
if (!hasText && !hasFiles && sendBtn.dataset.mode === 'newchat') {
|
||||||
if (sessionModule) {
|
if (sessionModule) {
|
||||||
@@ -3823,9 +3940,10 @@ function startOdysseusApp() {
|
|||||||
// Enter to send (shift+enter for newline), or new chat when empty
|
// Enter to send (shift+enter for newline), or new chat when empty
|
||||||
if (messageInput) {
|
if (messageInput) {
|
||||||
messageInput.addEventListener('keydown', (e) => {
|
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();
|
e.preventDefault();
|
||||||
// Flush the debounced icon update so dataset.mode reflects the current
|
// Flush the debounced icon update so dataset.mode reflects the current
|
||||||
// text state. Without this, a fast type-and-Enter would still see the
|
// text state. Without this, a fast type-and-Enter would still see the
|
||||||
@@ -3836,6 +3954,12 @@ function startOdysseusApp() {
|
|||||||
if (railNew) railNew.click();
|
if (railNew) railNew.click();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (_isForegroundChatBusy() && messageInput.value && messageInput.value.trim()) {
|
||||||
|
if (chatModule && chatModule.queueStreamingComposerRequest && chatModule.queueStreamingComposerRequest()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.__odysseusQueueStreamingSubmit = Date.now();
|
||||||
|
}
|
||||||
handleSubmit(e);
|
handleSubmit(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -3855,7 +3979,11 @@ function startOdysseusApp() {
|
|||||||
_syncModelPickerAutohide();
|
_syncModelPickerAutohide();
|
||||||
messageInput.addEventListener('input', () => {
|
messageInput.addEventListener('input', () => {
|
||||||
_syncModelPickerAutohide();
|
_syncModelPickerAutohide();
|
||||||
|
if (sendBtn && sendBtn.dataset.mode === 'streaming') {
|
||||||
|
_updateSendBtnIcon();
|
||||||
|
} else {
|
||||||
_debouncedUpdateIcon();
|
_debouncedUpdateIcon();
|
||||||
|
}
|
||||||
}, { passive: true });
|
}, { passive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+31
-15
@@ -48,6 +48,7 @@ import { wireArrowUpRecall, getLastUserMessageFromChatHistory } from './composer
|
|||||||
try {
|
try {
|
||||||
window.__odysseusChatBusy = !!active;
|
window.__odysseusChatBusy = !!active;
|
||||||
window.__odysseusChatBusyUntil = active ? Date.now() + 120000 : Date.now() + 1200;
|
window.__odysseusChatBusyUntil = active ? Date.now() + 120000 : Date.now() + 1200;
|
||||||
|
window.dispatchEvent(new CustomEvent('odysseus:chat-busy-change', { detail: { active: !!active } }));
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
let _pendingContinue = null; // Stores the stopped AI element to merge with new response
|
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
|
// arrow out for the stop icon — otherwise the swap happens mid-flight
|
||||||
// and the user sees nothing fly out.
|
// and the user sees nothing fly out.
|
||||||
setTimeout(() => {
|
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');
|
submitBtn.classList.remove('anim-launch');
|
||||||
void submitBtn.offsetWidth;
|
void submitBtn.offsetWidth;
|
||||||
submitBtn.classList.add('anim-land');
|
submitBtn.classList.add('anim-land');
|
||||||
@@ -471,6 +477,24 @@ import { wireArrowUpRecall, getLastUserMessageFromChatHistory } from './composer
|
|||||||
return true;
|
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() {
|
function _drainQueuedAgentRequests() {
|
||||||
if (isStreaming || _sendInFlight || !_queuedAgentRequests.length) return;
|
if (isStreaming || _sendInFlight || !_queuedAgentRequests.length) return;
|
||||||
if (_queuedDrainTimer) return;
|
if (_queuedDrainTimer) return;
|
||||||
@@ -507,21 +531,13 @@ import { wireArrowUpRecall, getLastUserMessageFromChatHistory } from './composer
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If currently streaming, a non-empty composer means "queue this next".
|
// If currently streaming, keyboard Enter can queue a non-empty composer.
|
||||||
// Empty composer keeps the existing Stop behavior.
|
// Clicking the stop icon should still stop normally, even if text exists.
|
||||||
if (isStreaming) {
|
if (isStreaming) {
|
||||||
const queuedInput = uiModule.el('message');
|
const queueRequestedAt = Number(window.__odysseusQueueStreamingSubmit || 0);
|
||||||
const queuedText = (queuedInput && queuedInput.value || '').trim();
|
const shouldQueueStreamingSubmit = queueRequestedAt && Date.now() - queueRequestedAt < 1200;
|
||||||
if (queuedText) {
|
window.__odysseusQueueStreamingSubmit = 0;
|
||||||
if (fileHandlerModule.getPendingCount && fileHandlerModule.getPendingCount()) {
|
if (shouldQueueStreamingSubmit && queueStreamingComposerRequest()) {
|
||||||
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);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (fileHandlerModule.isUploading && fileHandlerModule.isUploading()) {
|
if (fileHandlerModule.isUploading && fileHandlerModule.isUploading()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user