mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-25 11:10:18 +00:00
feat: add opt-in session endless scroll
This commit is contained in:
committed by
nesquena-hermes
parent
596c6b314d
commit
ea8aca2818
@@ -3682,6 +3682,7 @@ _SETTINGS_DEFAULTS = {
|
||||
"theme": "dark", # light | dark | system
|
||||
"skin": "default", # accent color skin: default | ares | mono | slate | poseidon | sisyphus | charizard
|
||||
"font_size": "default", # small | default | large
|
||||
"session_endless_scroll": False, # auto-load older transcript pages while scrolling upward
|
||||
"language": "en", # UI locale code; must match a key in static/i18n.js LOCALES
|
||||
"bot_name": os.getenv(
|
||||
"HERMES_WEBUI_BOT_NAME", "Hermes"
|
||||
@@ -3810,6 +3811,7 @@ _SETTINGS_BOOL_KEYS = {
|
||||
"show_thinking",
|
||||
"simplified_tool_calling",
|
||||
"api_redact_enabled",
|
||||
"session_endless_scroll",
|
||||
}
|
||||
# Language codes are validated as short alphanumeric BCP-47-like tags (e.g. 'en', 'zh', 'fr')
|
||||
_SETTINGS_LANG_RE = __import__("re").compile(r"^[a-zA-Z]{2,10}(-[a-zA-Z0-9]{2,8})?$")
|
||||
|
||||
@@ -1303,6 +1303,7 @@ function applyBotName(){
|
||||
window._simplifiedToolCalling=s.simplified_tool_calling!==false;
|
||||
window._sidebarDensity=(s.sidebar_density==='detailed'?'detailed':'compact');
|
||||
window._busyInputMode=(s.busy_input_mode||'queue');
|
||||
window._sessionEndlessScrollEnabled=!!s.session_endless_scroll;
|
||||
window._botName=s.bot_name||'Hermes';
|
||||
if(s.default_model) window._defaultModel=s.default_model;
|
||||
// Persist default workspace so the blank new-chat page can show it
|
||||
@@ -1337,6 +1338,7 @@ function applyBotName(){
|
||||
window._simplifiedToolCalling=true;
|
||||
window._sidebarDensity='compact';
|
||||
window._busyInputMode='queue';
|
||||
window._sessionEndlessScrollEnabled=false;
|
||||
window._botName='Hermes';
|
||||
_bootSettings={check_for_updates:false};
|
||||
if(typeof setLocale==='function'){
|
||||
|
||||
@@ -421,6 +421,10 @@ const LOCALES = {
|
||||
settings_update_check_failed: 'Update check failed',
|
||||
settings_label_workspace_panel_open: 'Keep workspace panel open by default',
|
||||
settings_desc_workspace_panel_open: 'When enabled, the workspace / file browser panel opens automatically with each new session. You can still close it manually at any time.',
|
||||
|
||||
settings_label_session_endless_scroll: 'Load older messages while scrolling up',
|
||||
|
||||
settings_desc_session_endless_scroll: 'When enabled, older messages load automatically as you scroll upward. When disabled, use the older-messages button.',
|
||||
open_in_browser: 'Open in browser',
|
||||
settings_dropdown_conversation: 'Conversation',
|
||||
settings_dropdown_appearance: 'Appearance',
|
||||
@@ -1442,6 +1446,10 @@ const LOCALES = {
|
||||
settings_updates_disabled: 'アップデート確認は無効です',
|
||||
settings_label_workspace_panel_open: 'ワークスペースパネルをデフォルトで開いておく',
|
||||
settings_desc_workspace_panel_open: '有効にすると、新しいセッションごとにワークスペース/ファイルブラウザパネルが自動で開きます。手動でいつでも閉じられます。',
|
||||
|
||||
settings_label_session_endless_scroll: '上スクロールで古いメッセージを読み込む',
|
||||
|
||||
settings_desc_session_endless_scroll: '有効にすると、上にスクロールしたとき古いメッセージを自動で読み込みます。無効の場合は古いメッセージボタンを使います。',
|
||||
open_in_browser: 'ブラウザで開く',
|
||||
settings_dropdown_conversation: '会話',
|
||||
settings_dropdown_appearance: '外観',
|
||||
@@ -2880,6 +2888,10 @@ const LOCALES = {
|
||||
settings_update_check_failed: 'Ошибка проверки обновлений',
|
||||
settings_label_workspace_panel_open: 'Открывать панель рабочей области по умолчанию',
|
||||
settings_desc_workspace_panel_open: 'При включении панель файлов будет открываться автоматически в каждой новой сессии.',
|
||||
|
||||
settings_label_session_endless_scroll: 'Загружать старые сообщения при прокрутке вверх',
|
||||
|
||||
settings_desc_session_endless_scroll: 'Если включено, старые сообщения загружаются автоматически при прокрутке вверх. Если выключено, используйте кнопку загрузки старых сообщений.',
|
||||
open_in_browser: 'Открыть в браузере',
|
||||
settings_section_system_title: 'System',
|
||||
settings_tab_appearance: 'Appearance',
|
||||
@@ -3823,6 +3835,10 @@ const LOCALES = {
|
||||
settings_update_check_failed: 'Error al comprobar actualizaciones',
|
||||
settings_label_workspace_panel_open: 'Mantener panel de espacio abierto',
|
||||
settings_desc_workspace_panel_open: 'Al activar, el panel de archivos se abre automáticamente en cada nueva sesión. Aún puedes cerrarlo manualmente.',
|
||||
|
||||
settings_label_session_endless_scroll: 'Cargar mensajes antiguos al desplazarse hacia arriba',
|
||||
|
||||
settings_desc_session_endless_scroll: 'Si está activado, los mensajes antiguos se cargan automáticamente al desplazarte hacia arriba. Si está desactivado, usa el botón de mensajes antiguos.',
|
||||
open_in_browser: 'Abrir en el navegador',
|
||||
settings_section_system_title: 'System',
|
||||
settings_tab_appearance: 'Appearance',
|
||||
@@ -4512,6 +4528,10 @@ const LOCALES = {
|
||||
settings_label_workspace_panel_open: 'Arbeitsbereich-Panel standardmäßig öffnen',
|
||||
settings_desc_workspace_panel_open: 'Wenn aktiviert, wird der Datei-Browser bei jeder neuen Sitzung automatisch geöffnet. Er kann jederzeit manuell geschlossen werden.',
|
||||
|
||||
settings_label_session_endless_scroll: 'Ältere Nachrichten beim Hochscrollen laden',
|
||||
|
||||
settings_desc_session_endless_scroll: 'Wenn aktiviert, werden ältere Nachrichten beim Hochscrollen automatisch geladen. Wenn deaktiviert, nutzt du den Button für ältere Nachrichten.',
|
||||
|
||||
workspace_drag_hint: 'Ziehen zum Neuordnen',
|
||||
workspace_reorder_failed: 'Neuordnen fehlgeschlagen',
|
||||
open_in_browser: 'Im Browser öffnen',
|
||||
@@ -5730,6 +5750,10 @@ const LOCALES = {
|
||||
settings_update_check_failed: '更新检查失败',
|
||||
settings_label_workspace_panel_open: '默认保持工作区面板打开',
|
||||
settings_desc_workspace_panel_open: '启用后,工作区/文件浏览器面板会在每次新会话时自动打开。您仍可随时手动关闭。',
|
||||
|
||||
settings_label_session_endless_scroll: '向上滚动时加载更早的消息',
|
||||
|
||||
settings_desc_session_endless_scroll: '启用后,向上滚动时会自动加载更早的消息。禁用时请使用加载更早消息按钮。',
|
||||
open_in_browser: '在浏览器中打开',
|
||||
settings_section_system_title: 'System',
|
||||
settings_tab_appearance: 'Appearance',
|
||||
@@ -6136,6 +6160,10 @@ const LOCALES = {
|
||||
settings_update_check_failed: '更新檢查失敗',
|
||||
settings_label_workspace_panel_open: '預設保持工作區面板開啓',
|
||||
settings_desc_workspace_panel_open: '啟用後,工作區/檔案瀏覽器面板會在每次新會話時自動開啓。您仍可隨時手動關閉。',
|
||||
|
||||
settings_label_session_endless_scroll: '向上捲動時載入較早訊息',
|
||||
|
||||
settings_desc_session_endless_scroll: '啟用後,向上捲動時會自動載入較早訊息。停用時請使用載入較早訊息按鈕。',
|
||||
open_in_browser: '在瀏覽器中開啓',
|
||||
settings_dropdown_conversation: '對話',
|
||||
settings_dropdown_appearance: '外觀',
|
||||
@@ -7170,6 +7198,10 @@ const LOCALES = {
|
||||
settings_update_check_failed: 'Falha ao verificar updates',
|
||||
settings_label_workspace_panel_open: 'Manter painel workspace aberto por padrão',
|
||||
settings_desc_workspace_panel_open: 'Quando ativo, o painel workspace abre automaticamente com cada nova sessão.',
|
||||
|
||||
settings_label_session_endless_scroll: 'Carregar mensagens antigas ao rolar para cima',
|
||||
|
||||
settings_desc_session_endless_scroll: 'Quando ativado, mensagens antigas carregam automaticamente ao rolar para cima. Quando desativado, use o botão de mensagens antigas.',
|
||||
open_in_browser: 'Abrir no navegador',
|
||||
settings_dropdown_conversation: 'Conversa',
|
||||
settings_dropdown_appearance: 'Aparência',
|
||||
@@ -8089,6 +8121,10 @@ const LOCALES = {
|
||||
settings_update_check_failed: 'Update check failed',
|
||||
settings_label_workspace_panel_open: '기본으로 워크스페이스 패널 열기',
|
||||
settings_desc_workspace_panel_open: '활성화하면 새 세션마다 워크스페이스/파일 브라우저 패널이 자동으로 열립니다. 언제든지 수동으로 닫을 수 있습니다.',
|
||||
|
||||
settings_label_session_endless_scroll: '위로 스크롤할 때 이전 메시지 불러오기',
|
||||
|
||||
settings_desc_session_endless_scroll: '활성화하면 위로 스크롤할 때 이전 메시지를 자동으로 불러옵니다. 비활성화하면 이전 메시지 버튼을 사용합니다.',
|
||||
open_in_browser: '브라우저에서 열기',
|
||||
settings_dropdown_conversation: '대화',
|
||||
settings_dropdown_appearance: '외형',
|
||||
|
||||
@@ -863,6 +863,13 @@
|
||||
</label>
|
||||
<div style="font-size:11px;color:var(--muted);margin-top:4px" data-i18n="settings_desc_workspace_panel_open">When enabled, the workspace / file browser panel opens automatically with each new session. You can still close it manually at any time.</div>
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
||||
<input type="checkbox" id="settingsSessionEndlessScroll" style="width:15px;height:15px;accent-color:var(--accent)">
|
||||
<span data-i18n="settings_label_session_endless_scroll">Load older messages while scrolling up</span>
|
||||
</label>
|
||||
<div style="font-size:11px;color:var(--muted);margin-top:4px" data-i18n="settings_desc_session_endless_scroll">When enabled, older messages load automatically as you scroll upward. When disabled, use the older-messages button.</div>
|
||||
</div>
|
||||
<div id="settingsAppearanceAutosaveStatus" class="settings-autosave-status" aria-live="polite"></div>
|
||||
</div>
|
||||
<div class="settings-pane" id="settingsPanePreferences">
|
||||
|
||||
@@ -4251,6 +4251,7 @@ function _appearancePayloadFromUi(){
|
||||
theme: ($('settingsTheme')||{}).value || localStorage.getItem('hermes-theme') || 'dark',
|
||||
skin: ($('settingsSkin')||{}).value || localStorage.getItem('hermes-skin') || 'default',
|
||||
font_size: ($('settingsFontSize')||{}).value || localStorage.getItem('hermes-font-size') || 'default',
|
||||
session_endless_scroll: !!($('settingsSessionEndlessScroll')||{}).checked,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4298,6 +4299,7 @@ async function _autosaveAppearanceSettings(payload){
|
||||
if(saved&&saved.font_size){
|
||||
localStorage.setItem('hermes-font-size',saved.font_size);
|
||||
}
|
||||
window._sessionEndlessScrollEnabled=!!(saved&&saved.session_endless_scroll);
|
||||
_setAppearanceAutosaveStatus('saved');
|
||||
}catch(e){
|
||||
console.warn('[settings] appearance autosave failed', e);
|
||||
@@ -4470,6 +4472,15 @@ async function loadSettingsPanel(){
|
||||
else if(!open&&_workspacePanelMode!=='closed') toggleWorkspacePanel(false);
|
||||
};
|
||||
}
|
||||
const endlessScrollCb=$('settingsSessionEndlessScroll');
|
||||
if(endlessScrollCb){
|
||||
endlessScrollCb.checked=!!settings.session_endless_scroll;
|
||||
window._sessionEndlessScrollEnabled=endlessScrollCb.checked;
|
||||
endlessScrollCb.onchange=function(){
|
||||
window._sessionEndlessScrollEnabled=this.checked;
|
||||
_scheduleAppearanceAutosave();
|
||||
};
|
||||
}
|
||||
const resolvedLanguage=(typeof resolvePreferredLocale==='function')
|
||||
? resolvePreferredLocale(settings.language, localStorage.getItem('hermes-lang'))
|
||||
: (settings.language || localStorage.getItem('hermes-lang') || 'en');
|
||||
@@ -5126,6 +5137,7 @@ function _applySavedSettingsUi(saved, body, opts){
|
||||
window._simplifiedToolCalling=body.simplified_tool_calling!==false;
|
||||
window._sidebarDensity=sidebarDensity==='detailed'?'detailed':'compact';
|
||||
window._busyInputMode=body.busy_input_mode||'queue';
|
||||
window._sessionEndlessScrollEnabled=!!body.session_endless_scroll;
|
||||
window._botName=body.bot_name||'Hermes';
|
||||
if(typeof applyBotName==='function') applyBotName();
|
||||
if(typeof setLocale==='function') setLocale(language);
|
||||
@@ -5221,6 +5233,7 @@ async function saveSettings(andClose){
|
||||
body.theme=theme;
|
||||
body.skin=skin;
|
||||
body.font_size=fontSize;
|
||||
body.session_endless_scroll=!!($('settingsSessionEndlessScroll')||{}).checked;
|
||||
body.language=language;
|
||||
body.show_token_usage=showTokenUsage;
|
||||
body.show_tps=showTps;
|
||||
|
||||
+8
-2
@@ -185,6 +185,9 @@ function _messageRenderableMessageCount(){
|
||||
function _messageHiddenBeforeCount(){
|
||||
return Math.max(0,_messageRenderableMessageCount()-_currentMessageRenderWindowSize());
|
||||
}
|
||||
function _isSessionEndlessScrollEnabled(){
|
||||
return window._sessionEndlessScrollEnabled===true;
|
||||
}
|
||||
function _wireMessageWindowLoadEarlierButton(){
|
||||
const indicator=$('loadOlderIndicator');
|
||||
if(!indicator) return;
|
||||
@@ -1574,8 +1577,11 @@ if (typeof window !== 'undefined') window._resetScrollDirectionTracker = _resetS
|
||||
} // #1360
|
||||
const btn=$('scrollToBottomBtn');
|
||||
if(btn) btn.style.display=_scrollPinned?'none':'flex';
|
||||
// Load older messages when scrolled near the top
|
||||
if(el.scrollTop<80 && typeof _messagesTruncated!=='undefined' && _messagesTruncated && typeof _loadOlderMessages==='function'){
|
||||
// Prefetch older messages before the reader hits the hard top. Prepending
|
||||
// then preserving scrollTop is seamless only if there is runway left for
|
||||
// the user's continued upward wheel/touch movement.
|
||||
const olderPrefetchPx=Math.max(600,el.clientHeight*1.5);
|
||||
if(_isSessionEndlessScrollEnabled()&&el.scrollTop<olderPrefetchPx && typeof _messagesTruncated!=='undefined' && _messagesTruncated && typeof _loadOlderMessages==='function'){
|
||||
_loadOlderMessages();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -427,10 +427,11 @@ class TestMessagePaginationFrontend:
|
||||
assert "async function _ensureAllMessagesLoaded" in SESSIONS_JS
|
||||
|
||||
def test_scroll_to_top_triggers_loading(self):
|
||||
"""Scroll event handler must trigger _loadOlderMessages near top."""
|
||||
"""Scroll event handler must trigger _loadOlderMessages near top when opt-in is enabled."""
|
||||
UI_JS = (REPO / "static" / "ui.js").read_text(encoding="utf-8")
|
||||
|
||||
assert "el.scrollTop<80" in UI_JS
|
||||
assert "const olderPrefetchPx=Math.max(600,el.clientHeight*1.5)" in UI_JS
|
||||
assert "_isSessionEndlessScrollEnabled()&&el.scrollTop<olderPrefetchPx" in UI_JS
|
||||
assert "_loadOlderMessages" in UI_JS
|
||||
|
||||
def test_load_older_indicator_in_render(self):
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
CONFIG_PY = (ROOT / "api" / "config.py").read_text(encoding="utf-8")
|
||||
BOOT_JS = (ROOT / "static" / "boot.js").read_text(encoding="utf-8")
|
||||
INDEX_HTML = (ROOT / "static" / "index.html").read_text(encoding="utf-8")
|
||||
PANELS_JS = (ROOT / "static" / "panels.js").read_text(encoding="utf-8")
|
||||
UI_JS = (ROOT / "static" / "ui.js").read_text(encoding="utf-8")
|
||||
I18N_JS = (ROOT / "static" / "i18n.js").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_endless_scroll_is_opt_in_setting():
|
||||
assert '"session_endless_scroll": False' in CONFIG_PY
|
||||
assert '"session_endless_scroll"' in CONFIG_PY
|
||||
assert 'id="settingsSessionEndlessScroll"' in INDEX_HTML
|
||||
assert 'data-i18n="settings_label_session_endless_scroll"' in INDEX_HTML
|
||||
assert 'data-i18n="settings_desc_session_endless_scroll"' in INDEX_HTML
|
||||
assert "session_endless_scroll: !!($('settingsSessionEndlessScroll')||{}).checked" in PANELS_JS
|
||||
assert "window._sessionEndlessScrollEnabled=!!s.session_endless_scroll" in BOOT_JS
|
||||
assert "window._sessionEndlessScrollEnabled=false" in BOOT_JS
|
||||
|
||||
|
||||
def test_scroll_listener_prefetches_older_messages_only_when_enabled():
|
||||
assert "function _isSessionEndlessScrollEnabled" in UI_JS
|
||||
assert "const olderPrefetchPx=Math.max(600,el.clientHeight*1.5)" in UI_JS
|
||||
assert "_isSessionEndlessScrollEnabled()&&el.scrollTop<olderPrefetchPx" in UI_JS
|
||||
assert "el.scrollTop<80 && typeof _messagesTruncated" not in UI_JS
|
||||
|
||||
|
||||
def test_endless_scroll_i18n_keys_exist_for_each_locale():
|
||||
assert I18N_JS.count("settings_label_session_endless_scroll") == I18N_JS.count("settings_label_workspace_panel_open")
|
||||
assert I18N_JS.count("settings_desc_session_endless_scroll") == I18N_JS.count("settings_desc_workspace_panel_open")
|
||||
Reference in New Issue
Block a user