mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-08-02 03:28:36 -04:00
172 lines
6.1 KiB
JavaScript
172 lines
6.1 KiB
JavaScript
/**
|
|
* ArrowUp on the composer recalls previous user messages from this chat.
|
|
*/
|
|
|
|
/**
|
|
* User bubbles in the active chat surface (#chat-history), newest first, using
|
|
* dataset.raw (same source as resend/regenerate in chat.js).
|
|
*
|
|
* @param {Document | Element} [root=document]
|
|
* @returns {string[]}
|
|
*/
|
|
export function getUserMessagesFromChatHistory(root = document) {
|
|
const chatBox =
|
|
root && root.id === 'chat-history' && typeof root.querySelectorAll === 'function'
|
|
? root
|
|
: (root.getElementById ? root.getElementById('chat-history') : null);
|
|
if (!chatBox) return [];
|
|
|
|
const users = chatBox.querySelectorAll('.msg-user');
|
|
const prompts = [];
|
|
for (let i = users.length - 1; i >= 0; i--) {
|
|
const msg = users[i];
|
|
const bodyEl = msg.querySelector('.body');
|
|
const text = msg.dataset?.raw || (bodyEl ? bodyEl.textContent : '') || '';
|
|
if (text) prompts.push(text);
|
|
}
|
|
return prompts;
|
|
}
|
|
|
|
/**
|
|
* Last user bubble in the active chat surface (#chat-history).
|
|
*
|
|
* @param {Document | Element} [root=document]
|
|
* @returns {string}
|
|
*/
|
|
export function getLastUserMessageFromChatHistory(root = document) {
|
|
return getUserMessagesFromChatHistory(root)[0] || '';
|
|
}
|
|
|
|
/**
|
|
* @param {HTMLTextAreaElement} composer
|
|
* @param {() => string|string[]} getUserMessages
|
|
* @param {{ autoResize?: (el: HTMLTextAreaElement) => void }} [options]
|
|
* @returns {boolean} true when wired (or already wired)
|
|
*/
|
|
export function wireArrowUpRecall(composer, getUserMessages, options = {}) {
|
|
if (!composer) return false;
|
|
if (composer._arrowUpRecallWired) return true;
|
|
composer._arrowUpRecallWired = true;
|
|
|
|
const { autoResize } = options;
|
|
let recallIndex = -1;
|
|
let applyingRecall = false;
|
|
let lastRecalledValue = '';
|
|
let recallHistory = [];
|
|
|
|
const readHistory = () => {
|
|
const value = getUserMessages?.();
|
|
if (Array.isArray(value)) return value.filter(Boolean);
|
|
return value ? [value] : [];
|
|
};
|
|
const norm = (value) => String(value || '').replace(/\r\n/g, '\n').trimEnd();
|
|
const debug = (...args) => {
|
|
try {
|
|
if (localStorage.getItem('odysseusArrowRecallDebug') === '1') {
|
|
console.debug('[arrow-recall]', ...args);
|
|
}
|
|
} catch (_) {}
|
|
};
|
|
|
|
composer.addEventListener('input', () => {
|
|
if (applyingRecall) return;
|
|
if (norm(composer.value) === norm(lastRecalledValue)) return;
|
|
recallIndex = -1;
|
|
lastRecalledValue = '';
|
|
recallHistory = [];
|
|
try { delete composer.dataset.odysseusRecallIndex; } catch (_) {}
|
|
});
|
|
|
|
composer.addEventListener('keydown', (e) => {
|
|
// Prompt history: ArrowUp walks older, ArrowDown walks newer/back to blank.
|
|
if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return;
|
|
if (e.shiftKey || e.altKey || e.ctrlKey || e.metaKey) return;
|
|
if (e.isComposing) return;
|
|
if (typeof window !== 'undefined' && window._ghostAutocomplete?.isActive?.()) return;
|
|
|
|
const freshHistory = readHistory();
|
|
const history = freshHistory.length ? freshHistory : recallHistory;
|
|
if (!history.length) {
|
|
debug('skip:no-history', { value: composer.value });
|
|
return;
|
|
}
|
|
|
|
const rawCurrentValue = String(composer.value || '');
|
|
const currentValue = norm(rawCurrentValue);
|
|
const recalledValue = norm(lastRecalledValue);
|
|
let currentIndex = rawCurrentValue === ''
|
|
? -1
|
|
: history.findIndex((item) => norm(item) === currentValue);
|
|
if (currentIndex < 0 && currentValue && currentValue === recalledValue) {
|
|
currentIndex = recallIndex;
|
|
}
|
|
if (currentIndex < 0 && currentValue) {
|
|
const markedIndex = Number(composer.dataset?.odysseusRecallIndex);
|
|
if (Number.isInteger(markedIndex) && markedIndex >= 0 && markedIndex < history.length) {
|
|
currentIndex = markedIndex;
|
|
}
|
|
}
|
|
if (rawCurrentValue !== '' && currentIndex < 0) {
|
|
debug('skip:draft-in-progress', { value: composer.value });
|
|
return;
|
|
}
|
|
e.preventDefault();
|
|
e.stopPropagation?.();
|
|
e.stopImmediatePropagation?.();
|
|
if (e.key === 'ArrowDown') {
|
|
if (currentIndex < 0) return;
|
|
const nextIndex = currentIndex - 1;
|
|
if (nextIndex < 0) {
|
|
recallIndex = -1;
|
|
recallHistory = history;
|
|
applyingRecall = true;
|
|
lastRecalledValue = '';
|
|
try { delete composer.dataset.odysseusRecallIndex; } catch (_) {}
|
|
composer.value = '';
|
|
try { composer.selectionStart = composer.selectionEnd = 0; } catch (_) {}
|
|
if (autoResize) autoResize(composer);
|
|
debug('handled-down-clear', { historyLength: history.length });
|
|
setTimeout(() => { applyingRecall = false; }, 0);
|
|
return;
|
|
}
|
|
const recalled = history[nextIndex];
|
|
recallIndex = nextIndex;
|
|
recallHistory = history;
|
|
applyingRecall = true;
|
|
lastRecalledValue = recalled;
|
|
try { composer.dataset.odysseusRecallIndex = String(nextIndex); } catch (_) {}
|
|
composer.value = recalled;
|
|
try { composer.selectionStart = composer.selectionEnd = recalled.length; } catch (_) {}
|
|
if (autoResize) autoResize(composer);
|
|
debug('handled-down', { nextIndex, recalled, historyLength: history.length });
|
|
setTimeout(() => { applyingRecall = false; }, 0);
|
|
return;
|
|
}
|
|
|
|
// ArrowUp owns prompt history in the chat composer. If the current text
|
|
// is not already a recalled prompt, start from newest instead of letting
|
|
// the browser move the caret inside the textarea.
|
|
const nextIndex = currentIndex >= 0 ? Math.min(currentIndex + 1, history.length - 1) : 0;
|
|
const recalled = history[nextIndex];
|
|
if (!recalled) {
|
|
debug('skip:no-recalled', { nextIndex, history });
|
|
return;
|
|
}
|
|
|
|
recallIndex = nextIndex;
|
|
recallHistory = history;
|
|
applyingRecall = true;
|
|
lastRecalledValue = recalled;
|
|
try { composer.dataset.odysseusRecallIndex = String(nextIndex); } catch (_) {}
|
|
composer.value = recalled;
|
|
try {
|
|
composer.selectionStart = composer.selectionEnd = recalled.length;
|
|
} catch (_) {}
|
|
if (autoResize) autoResize(composer);
|
|
debug('handled', { nextIndex, recalled, historyLength: history.length });
|
|
setTimeout(() => { applyingRecall = false; }, 0);
|
|
}, true);
|
|
|
|
return true;
|
|
}
|