mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-26 11:40:26 +00:00
feat: add opt-in session jump buttons
This commit is contained in:
committed by
nesquena-hermes
parent
596c6b314d
commit
df1ba9fde8
@@ -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_jump_buttons": False, # show Start/End transcript jump pills
|
||||
"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_jump_buttons",
|
||||
}
|
||||
# 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})?$")
|
||||
|
||||
@@ -1308,6 +1308,7 @@ function applyBotName(){
|
||||
// Persist default workspace so the blank new-chat page can show it
|
||||
// and workspace actions (New file/folder) work before the first session (#804).
|
||||
if(s.default_workspace) S._profileDefaultWorkspace=s.default_workspace;
|
||||
window._sessionJumpButtonsEnabled=!!s.session_jump_buttons;
|
||||
const appearance=_normalizeAppearance(s.theme,s.skin);
|
||||
localStorage.setItem('hermes-theme',appearance.theme);
|
||||
_applyTheme(appearance.theme);
|
||||
@@ -1335,6 +1336,7 @@ function applyBotName(){
|
||||
window._notificationsEnabled=false;
|
||||
window._showThinking=true;
|
||||
window._simplifiedToolCalling=true;
|
||||
window._sessionJumpButtonsEnabled=false;
|
||||
window._sidebarDensity='compact';
|
||||
window._busyInputMode='queue';
|
||||
window._botName='Hermes';
|
||||
|
||||
@@ -125,6 +125,10 @@ const LOCALES = {
|
||||
untitled: 'Untitled',
|
||||
n_messages: (n) => `${n} messages`,
|
||||
load_older_messages: '↑ Scroll up or click to load older messages',
|
||||
session_jump_start: 'Start',
|
||||
session_jump_start_label: 'Jump to beginning of session',
|
||||
session_jump_end: 'End',
|
||||
session_jump_end_label: 'Jump to end of session',
|
||||
queued_label: 'Sends after response',
|
||||
queued_count: (n) => n === 1 ? '1 queued' : `${n} queued`,
|
||||
queued_cancel: 'Cancel queued message',
|
||||
@@ -421,6 +425,8 @@ 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_jump_buttons: 'Show session jump buttons',
|
||||
settings_desc_session_jump_buttons: 'Show floating Start and End buttons while reading long session histories.',
|
||||
open_in_browser: 'Open in browser',
|
||||
settings_dropdown_conversation: 'Conversation',
|
||||
settings_dropdown_appearance: 'Appearance',
|
||||
@@ -1146,6 +1152,10 @@ const LOCALES = {
|
||||
untitled: '無題',
|
||||
n_messages: (n) => `${n} 件のメッセージ`,
|
||||
load_older_messages: '↑ 上にスクロール、またはクリックして過去のメッセージを読み込む',
|
||||
session_jump_start: 'Start',
|
||||
session_jump_start_label: 'Jump to beginning of session',
|
||||
session_jump_end: 'End',
|
||||
session_jump_end_label: 'Jump to end of session',
|
||||
queued_label: '応答後に送信',
|
||||
queued_count: (n) => `${n} 件キュー中`,
|
||||
queued_cancel: 'キューに入れたメッセージをキャンセル',
|
||||
@@ -1442,6 +1452,8 @@ const LOCALES = {
|
||||
settings_updates_disabled: 'アップデート確認は無効です',
|
||||
settings_label_workspace_panel_open: 'ワークスペースパネルをデフォルトで開いておく',
|
||||
settings_desc_workspace_panel_open: '有効にすると、新しいセッションごとにワークスペース/ファイルブラウザパネルが自動で開きます。手動でいつでも閉じられます。',
|
||||
settings_label_session_jump_buttons: 'Show session jump buttons',
|
||||
settings_desc_session_jump_buttons: 'Show floating Start and End buttons while reading long session histories.',
|
||||
open_in_browser: 'ブラウザで開く',
|
||||
settings_dropdown_conversation: '会話',
|
||||
settings_dropdown_appearance: '外観',
|
||||
@@ -2145,6 +2157,10 @@ const LOCALES = {
|
||||
untitled: 'Без названия',
|
||||
n_messages: (n) => `${n} сообщений`,
|
||||
load_older_messages: '↑ Прокрутите вверх или нажмите, чтобы загрузить ранние сообщения',
|
||||
session_jump_start: 'Start',
|
||||
session_jump_start_label: 'Jump to beginning of session',
|
||||
session_jump_end: 'End',
|
||||
session_jump_end_label: 'Jump to end of session',
|
||||
queued_label: 'Отправить после ответа',
|
||||
queued_count: (n) => n === 1 ? '1 в очереди' : `${n} в очереди`,
|
||||
queued_cancel: 'Отменить сообщение',
|
||||
@@ -2880,6 +2896,8 @@ const LOCALES = {
|
||||
settings_update_check_failed: 'Ошибка проверки обновлений',
|
||||
settings_label_workspace_panel_open: 'Открывать панель рабочей области по умолчанию',
|
||||
settings_desc_workspace_panel_open: 'При включении панель файлов будет открываться автоматически в каждой новой сессии.',
|
||||
settings_label_session_jump_buttons: 'Show session jump buttons',
|
||||
settings_desc_session_jump_buttons: 'Show floating Start and End buttons while reading long session histories.',
|
||||
open_in_browser: 'Открыть в браузере',
|
||||
settings_section_system_title: 'System',
|
||||
settings_tab_appearance: 'Appearance',
|
||||
@@ -3102,6 +3120,10 @@ const LOCALES = {
|
||||
untitled: 'Sin título',
|
||||
n_messages: (n) => `${n} mensajes`,
|
||||
load_older_messages: '↑ Desplázate hacia arriba o haz clic para cargar mensajes anteriores',
|
||||
session_jump_start: 'Start',
|
||||
session_jump_start_label: 'Jump to beginning of session',
|
||||
session_jump_end: 'End',
|
||||
session_jump_end_label: 'Jump to end of session',
|
||||
queued_label: 'Enviar después de la respuesta',
|
||||
queued_count: (n) => n === 1 ? '1 en cola' : `${n} en cola`,
|
||||
queued_cancel: 'Cancelar mensaje en cola',
|
||||
@@ -3823,6 +3845,8 @@ 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_jump_buttons: 'Show session jump buttons',
|
||||
settings_desc_session_jump_buttons: 'Show floating Start and End buttons while reading long session histories.',
|
||||
open_in_browser: 'Abrir en el navegador',
|
||||
settings_section_system_title: 'System',
|
||||
settings_tab_appearance: 'Appearance',
|
||||
@@ -4055,6 +4079,10 @@ const LOCALES = {
|
||||
untitled: 'Unbenannt',
|
||||
n_messages: (n) => `${n} Nachrichten`,
|
||||
load_older_messages: '↑ Nach oben scrollen oder klicken, um ältere Nachrichten zu laden',
|
||||
session_jump_start: 'Start',
|
||||
session_jump_start_label: 'Jump to beginning of session',
|
||||
session_jump_end: 'End',
|
||||
session_jump_end_label: 'Jump to end of session',
|
||||
queued_label: 'Wird nach Antwort gesendet',
|
||||
queued_count: (n) => n === 1 ? '1 in Warteschlange' : `${n} in Warteschlange`,
|
||||
queued_cancel: 'Nachricht abbrechen',
|
||||
@@ -4511,6 +4539,8 @@ const LOCALES = {
|
||||
settings_update_check_failed: 'Update-Prüfung fehlgeschlagen',
|
||||
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_jump_buttons: 'Show session jump buttons',
|
||||
settings_desc_session_jump_buttons: 'Show floating Start and End buttons while reading long session histories.',
|
||||
|
||||
workspace_drag_hint: 'Ziehen zum Neuordnen',
|
||||
workspace_reorder_failed: 'Neuordnen fehlgeschlagen',
|
||||
@@ -5012,6 +5042,10 @@ const LOCALES = {
|
||||
untitled: '\u672a\u547d\u540d',
|
||||
n_messages: (n) => `${n} \u6761\u6d88\u606f`,
|
||||
load_older_messages: '↑ 向上滚动或点击加载更早的消息',
|
||||
session_jump_start: 'Start',
|
||||
session_jump_start_label: 'Jump to beginning of session',
|
||||
session_jump_end: 'End',
|
||||
session_jump_end_label: 'Jump to end of session',
|
||||
queued_label: '响应后发送',
|
||||
queued_count: (n) => n === 1 ? '1 条排队' : `${n} 条排队`,
|
||||
queued_cancel: '取消排队消息',
|
||||
@@ -5730,6 +5764,8 @@ const LOCALES = {
|
||||
settings_update_check_failed: '更新检查失败',
|
||||
settings_label_workspace_panel_open: '默认保持工作区面板打开',
|
||||
settings_desc_workspace_panel_open: '启用后,工作区/文件浏览器面板会在每次新会话时自动打开。您仍可随时手动关闭。',
|
||||
settings_label_session_jump_buttons: 'Show session jump buttons',
|
||||
settings_desc_session_jump_buttons: 'Show floating Start and End buttons while reading long session histories.',
|
||||
open_in_browser: '在浏览器中打开',
|
||||
settings_section_system_title: 'System',
|
||||
settings_tab_appearance: 'Appearance',
|
||||
@@ -5964,6 +6000,10 @@ const LOCALES = {
|
||||
untitled: '\u672a\u547d\u540d',
|
||||
n_messages: (n) => `${n} \u689d\u8a0a\u606f`,
|
||||
load_older_messages: '↑ 向上捲動或點擊以載入較早的訊息',
|
||||
session_jump_start: 'Start',
|
||||
session_jump_start_label: 'Jump to beginning of session',
|
||||
session_jump_end: 'End',
|
||||
session_jump_end_label: 'Jump to end of session',
|
||||
model_unavailable: '\uff08\u4e0d\u53ef\u7528\uff09',
|
||||
model_unavailable_title: '\u6b64\u6a21\u578b\u5df2\u7d93\u4e0d\u5728\u7576\u524d provider \u5217\u8868\u4e2d',
|
||||
provider_mismatch_warning: (m,p)=>`\"${m}\" \u53ef\u80fd\u7121\u6cd5\u5728\u7576\u524d\u914d\u7f6e\u7684\u63d0\u4f9b\u8005 (${p}) \u4e0b\u904b\u4f5c\u3002\u5c1a\u9001\uff0c\u6216\u5728\u7d42\u7aef\u57f7\u884c \`hermes model\` \u5207\u63db\u3002`,
|
||||
@@ -6136,6 +6176,8 @@ const LOCALES = {
|
||||
settings_update_check_failed: '更新檢查失敗',
|
||||
settings_label_workspace_panel_open: '預設保持工作區面板開啓',
|
||||
settings_desc_workspace_panel_open: '啟用後,工作區/檔案瀏覽器面板會在每次新會話時自動開啓。您仍可隨時手動關閉。',
|
||||
settings_label_session_jump_buttons: 'Show session jump buttons',
|
||||
settings_desc_session_jump_buttons: 'Show floating Start and End buttons while reading long session histories.',
|
||||
open_in_browser: '在瀏覽器中開啓',
|
||||
settings_dropdown_conversation: '對話',
|
||||
settings_dropdown_appearance: '外觀',
|
||||
@@ -6288,6 +6330,10 @@ const LOCALES = {
|
||||
downloading: (filename) => `正在下載 ${filename}…`,
|
||||
n_messages: (n) => `${n} 則訊息`,
|
||||
load_older_messages: '↑ 向上捲動或點擊以載入較早的訊息',
|
||||
session_jump_start: 'Start',
|
||||
session_jump_start_label: 'Jump to beginning of session',
|
||||
session_jump_end: 'End',
|
||||
session_jump_end_label: 'Jump to end of session',
|
||||
onboarding_api_key_help_prefix: '\u900f\u904e\u4ee5\u4e0b\u65b9\u5f0f\u5132\u5b58\u70ba Hermes .env \u6a94\u6848\u4e2d\u7684\u6a5f\u5bc6',
|
||||
onboarding_api_key_label: 'API \u91d1\u9470',
|
||||
onboarding_api_key_placeholder: '\u7559\u7a7a\u4ee5\u4fdd\u7559\u5df2\u5132\u5b58\u7684\u91d1\u9470',
|
||||
@@ -6906,6 +6952,10 @@ const LOCALES = {
|
||||
untitled: 'Sem título',
|
||||
n_messages: (n) => `${n} mensagens`,
|
||||
load_older_messages: '↑ Role para cima ou clique para carregar mensagens mais antigas',
|
||||
session_jump_start: 'Start',
|
||||
session_jump_start_label: 'Jump to beginning of session',
|
||||
session_jump_end: 'End',
|
||||
session_jump_end_label: 'Jump to end of session',
|
||||
queued_label: 'Envia após a resposta',
|
||||
queued_count: (n) => n === 1 ? '1 na fila' : `${n} na fila`,
|
||||
queued_cancel: 'Cancelar mensagem na fila',
|
||||
@@ -7170,6 +7220,8 @@ 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_jump_buttons: 'Show session jump buttons',
|
||||
settings_desc_session_jump_buttons: 'Show floating Start and End buttons while reading long session histories.',
|
||||
open_in_browser: 'Abrir no navegador',
|
||||
settings_dropdown_conversation: 'Conversa',
|
||||
settings_dropdown_appearance: 'Aparência',
|
||||
@@ -7802,6 +7854,10 @@ const LOCALES = {
|
||||
untitled: '제목 없음',
|
||||
n_messages: (n) => `${n}개 메시지`,
|
||||
load_older_messages: '↑ 위로 스크롤하거나 클릭하여 이전 메시지 불러오기',
|
||||
session_jump_start: 'Start',
|
||||
session_jump_start_label: 'Jump to beginning of session',
|
||||
session_jump_end: 'End',
|
||||
session_jump_end_label: 'Jump to end of session',
|
||||
queued_label: 'Sends after response',
|
||||
queued_count: (n) => n === 1 ? '1 queued' : `${n} queued`,
|
||||
queued_cancel: 'Cancel queued message',
|
||||
@@ -8089,6 +8145,8 @@ const LOCALES = {
|
||||
settings_update_check_failed: 'Update check failed',
|
||||
settings_label_workspace_panel_open: '기본으로 워크스페이스 패널 열기',
|
||||
settings_desc_workspace_panel_open: '활성화하면 새 세션마다 워크스페이스/파일 브라우저 패널이 자동으로 열립니다. 언제든지 수동으로 닫을 수 있습니다.',
|
||||
settings_label_session_jump_buttons: 'Show session jump buttons',
|
||||
settings_desc_session_jump_buttons: 'Show floating Start and End buttons while reading long session histories.',
|
||||
open_in_browser: '브라우저에서 열기',
|
||||
settings_dropdown_conversation: '대화',
|
||||
settings_dropdown_appearance: '외형',
|
||||
@@ -8832,6 +8890,11 @@ function applyLocaleToDOM() {
|
||||
const val = t(key);
|
||||
if (val && val !== key) el.placeholder = val;
|
||||
});
|
||||
document.querySelectorAll('[data-i18n-aria-label]').forEach(el => {
|
||||
const key = el.getAttribute('data-i18n-aria-label');
|
||||
const val = t(key);
|
||||
if (val && val !== key) el.setAttribute('aria-label', val);
|
||||
});
|
||||
if (typeof syncAppTitlebar === 'function') syncAppTitlebar();
|
||||
}
|
||||
|
||||
|
||||
+9
-1
@@ -297,7 +297,8 @@
|
||||
<main class="main">
|
||||
<div id="mainChat" class="main-view">
|
||||
<div class="messages" id="messages">
|
||||
<button id="scrollToBottomBtn" class="scroll-to-bottom-btn" aria-label="Scroll to bottom" onclick="scrollToBottom()" style="display:none">↓</button>
|
||||
<button id="jumpToSessionStartBtn" class="session-jump-btn session-jump-btn--start" aria-label="Jump to beginning of session" data-i18n-aria-label="session_jump_start_label" data-i18n-title="session_jump_start_label" onclick="jumpToSessionStart()" style="display:none"><span aria-hidden="true">↑</span><span data-i18n="session_jump_start">Start</span></button>
|
||||
<button id="scrollToBottomBtn" class="scroll-to-bottom-btn" style="display:none" onclick="scrollToBottom()" aria-label="Scroll to bottom" data-i18n-aria-label="session_jump_end_label" data-i18n-title="session_jump_end_label"><span aria-hidden="true">↓</span><span class="session-jump-btn__text" data-i18n="session_jump_end">End</span></button>
|
||||
<div class="empty-state" id="emptyState">
|
||||
<div class="empty-logo"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="80" height="80" aria-label="Hermes caduceus">
|
||||
<defs>
|
||||
@@ -863,6 +864,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="settingsSessionJumpButtons" style="width:15px;height:15px;accent-color:var(--accent)">
|
||||
<span data-i18n="settings_label_session_jump_buttons">Show session jump buttons</span>
|
||||
</label>
|
||||
<div style="font-size:11px;color:var(--muted);margin-top:4px" data-i18n="settings_desc_session_jump_buttons">Show floating Start and End buttons while reading long session histories.</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_jump_buttons: !!($('settingsSessionJumpButtons')||{}).checked,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4298,6 +4299,10 @@ async function _autosaveAppearanceSettings(payload){
|
||||
if(saved&&saved.font_size){
|
||||
localStorage.setItem('hermes-font-size',saved.font_size);
|
||||
}
|
||||
if(saved){
|
||||
window._sessionJumpButtonsEnabled=!!saved.session_jump_buttons;
|
||||
if(typeof _applySessionNavigationPrefs==='function') _applySessionNavigationPrefs();
|
||||
}
|
||||
_setAppearanceAutosaveStatus('saved');
|
||||
}catch(e){
|
||||
console.warn('[settings] appearance autosave failed', e);
|
||||
@@ -4454,6 +4459,17 @@ async function loadSettingsPanel(){
|
||||
const fontSizeSel=$('settingsFontSize');
|
||||
if(fontSizeSel) fontSizeSel.value=fontSizeVal;
|
||||
if(typeof _syncFontSizePicker==='function') _syncFontSizePicker(fontSizeVal);
|
||||
const jumpButtonsCb=$('settingsSessionJumpButtons');
|
||||
if(jumpButtonsCb){
|
||||
jumpButtonsCb.checked=!!settings.session_jump_buttons;
|
||||
window._sessionJumpButtonsEnabled=jumpButtonsCb.checked;
|
||||
jumpButtonsCb.onchange=function(){
|
||||
window._sessionJumpButtonsEnabled=this.checked;
|
||||
if(typeof _applySessionNavigationPrefs==='function') _applySessionNavigationPrefs();
|
||||
_scheduleAppearanceAutosave();
|
||||
};
|
||||
}
|
||||
if(typeof _applySessionNavigationPrefs==='function') _applySessionNavigationPrefs();
|
||||
// Workspace panel default-open toggle (localStorage-backed)
|
||||
// Uses a separate key (hermes-webui-workspace-panel-pref) so that
|
||||
// closing the panel via toolbar X does not clear the user's preference.
|
||||
@@ -5124,6 +5140,8 @@ function _applySavedSettingsUi(saved, body, opts){
|
||||
window._notificationsEnabled=body.notifications_enabled;
|
||||
window._showThinking=body.show_thinking!==false;
|
||||
window._simplifiedToolCalling=body.simplified_tool_calling!==false;
|
||||
window._sessionJumpButtonsEnabled=!!body.session_jump_buttons;
|
||||
if(typeof _applySessionNavigationPrefs==='function') _applySessionNavigationPrefs();
|
||||
window._sidebarDensity=sidebarDensity==='detailed'?'detailed':'compact';
|
||||
window._busyInputMode=body.busy_input_mode||'queue';
|
||||
window._botName=body.bot_name||'Hermes';
|
||||
@@ -5221,6 +5239,7 @@ async function saveSettings(andClose){
|
||||
body.theme=theme;
|
||||
body.skin=skin;
|
||||
body.font_size=fontSize;
|
||||
body.session_jump_buttons=!!($('settingsSessionJumpButtons')||{}).checked;
|
||||
body.language=language;
|
||||
body.show_token_usage=showTokenUsage;
|
||||
body.show_tps=showTps;
|
||||
|
||||
+9
-2
@@ -772,9 +772,16 @@
|
||||
.workspace-toggle-btn:disabled{opacity:.38;cursor:not-allowed;}
|
||||
.chip.model{color:var(--accent-text);border-color:var(--accent-bg-strong);background:var(--accent-bg);}
|
||||
.messages{flex:1;overflow-y:auto;display:flex;flex-direction:column;min-height:0;position:relative;z-index:0;-webkit-overflow-scrolling:touch;touch-action:pan-y;overscroll-behavior-y:contain;overflow-anchor:none;}
|
||||
/* sticky-first-child: button is first child of .messages so its natural position is above viewport; sticky+bottom:16px pins it there when visible */
|
||||
.scroll-to-bottom-btn{position:sticky;bottom:16px;align-self:flex-end;margin-right:20px;width:32px;height:32px;border-radius:50%;border:1px solid var(--border2);background:var(--code-bg);color:var(--muted);font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 8px rgba(0,0,0,.25);z-index:10;transition:color .12s,border-color .12s,background .12s;}
|
||||
/* sticky-first-child: button is early in .messages so its natural position is above viewport; sticky+bottom pins it there when visible */
|
||||
.scroll-to-bottom-btn{position:sticky;bottom:16px;align-self:flex-end;margin-right:20px;width:32px;height:32px;border-radius:50%;border:1px solid var(--border2);background:var(--code-bg);color:var(--muted);font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 8px rgba(0,0,0,.25);z-index:10;transition:color .12s,border-color .12s,background .12s,transform .12s;}
|
||||
.scroll-to-bottom-btn:hover{color:var(--text);border-color:var(--border);background:var(--hover-bg);}
|
||||
.session-jump-btn__text{display:none;}
|
||||
.session-jump-btn{position:sticky;align-self:flex-end;flex:0 0 32px;min-height:32px;margin-right:20px;height:32px;border-radius:999px;border:1px solid var(--border2);background:var(--code-bg);color:var(--muted);font-size:12px;font-weight:600;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:5px;padding:0 11px;box-shadow:0 2px 8px rgba(0,0,0,.25);z-index:11;transition:color .12s,border-color .12s,background .12s,opacity .12s,transform .12s;}
|
||||
.session-jump-btn:hover{color:var(--text);border-color:var(--border);background:var(--hover-bg);transform:translateY(-1px);}
|
||||
.session-jump-btn--start{top:16px;margin-bottom:-36px;}
|
||||
.messages.session-nav-enabled .scroll-to-bottom-btn{width:auto;min-width:32px;border-radius:999px;font-size:12px;font-weight:600;gap:5px;padding:0 11px;}
|
||||
.messages.session-nav-enabled .scroll-to-bottom-btn:hover{transform:translateY(-1px);}
|
||||
.messages.session-nav-enabled .session-jump-btn__text{display:inline;}
|
||||
.messages-inner{margin:0 auto;width:100%;padding:20px 24px 32px;display:flex;flex-direction:column;}
|
||||
@media(min-width:1400px){.messages-inner{max-width:1100px;}}
|
||||
@media(min-width:1800px){.messages-inner{max-width:1200px;}}
|
||||
|
||||
@@ -205,6 +205,48 @@ function _showEarlierRenderedMessages(){
|
||||
}
|
||||
_scrollPinned=false;
|
||||
}
|
||||
function _isSessionJumpButtonsEnabled(){
|
||||
return window._sessionJumpButtonsEnabled===true;
|
||||
}
|
||||
function _applySessionNavigationPrefs(){
|
||||
const container=$('messages');
|
||||
if(container) container.classList.toggle('session-nav-enabled',_isSessionJumpButtonsEnabled());
|
||||
_updateSessionStartJumpButton();
|
||||
}
|
||||
function _updateSessionStartJumpButton(){
|
||||
const btn=$('jumpToSessionStartBtn');
|
||||
const container=$('messages');
|
||||
if(!btn||!container) return;
|
||||
if(!_isSessionJumpButtonsEnabled()){
|
||||
btn.style.display='none';
|
||||
return;
|
||||
}
|
||||
const hasSession=!!(S&&S.session&&S.messages&&S.messages.length);
|
||||
const awayFromStart=container.scrollTop>Math.max(240,container.clientHeight*0.35);
|
||||
const hasScrollableHistory=container.scrollHeight>container.clientHeight+Math.max(240,container.clientHeight*0.35);
|
||||
const canRevealStart=hasScrollableHistory||_messageHiddenBeforeCount()>0||!!(typeof _messagesTruncated!=='undefined'&&_messagesTruncated);
|
||||
btn.style.display=(hasSession&&canRevealStart&&awayFromStart)?'flex':'none';
|
||||
}
|
||||
async function jumpToSessionStart(){
|
||||
const container=$('messages');
|
||||
if(!container||!S.session) return;
|
||||
_scrollPinned=false;
|
||||
_messageUserUnpinned=true;
|
||||
_programmaticScroll=true;
|
||||
try{
|
||||
if(typeof _ensureAllMessagesLoaded==='function') await _ensureAllMessagesLoaded();
|
||||
_messageRenderWindowSize=Math.max(_currentMessageRenderWindowSize(),_messageRenderableMessageCount());
|
||||
renderMessages({ preserveScroll:true });
|
||||
requestAnimationFrame(()=>{
|
||||
container.scrollTop=0;
|
||||
_updateSessionStartJumpButton();
|
||||
requestAnimationFrame(()=>{ _programmaticScroll=false; });
|
||||
});
|
||||
}catch(e){
|
||||
console.warn('jumpToSessionStart failed:',e);
|
||||
_programmaticScroll=false;
|
||||
}
|
||||
}
|
||||
|
||||
const DASHBOARD_STATUS_TTL_MS=60000;
|
||||
let _dashboardStatusCache=null;
|
||||
@@ -1574,6 +1616,7 @@ if (typeof window !== 'undefined') window._resetScrollDirectionTracker = _resetS
|
||||
} // #1360
|
||||
const btn=$('scrollToBottomBtn');
|
||||
if(btn) btn.style.display=_scrollPinned?'none':'flex';
|
||||
if(typeof _updateSessionStartJumpButton==='function') _updateSessionStartJumpButton();
|
||||
// Load older messages when scrolled near the top
|
||||
if(el.scrollTop<80 && typeof _messagesTruncated!=='undefined' && _messagesTruncated && typeof _loadOlderMessages==='function'){
|
||||
_loadOlderMessages();
|
||||
@@ -1913,6 +1956,7 @@ function scrollToBottom(){
|
||||
_settleMessageScrollToBottom(true);
|
||||
const btn=$('scrollToBottomBtn');
|
||||
if(btn) btn.style.display='none';
|
||||
if(typeof _updateSessionStartJumpButton==='function') _updateSessionStartJumpButton();
|
||||
}
|
||||
|
||||
function _fmtOllamaLabel(mid){
|
||||
@@ -4566,6 +4610,7 @@ function renderMessages(options){
|
||||
inner.innerHTML=cached.html;
|
||||
_sessionHtmlCacheSid=sid;
|
||||
_wireMessageWindowLoadEarlierButton();
|
||||
if(typeof _applySessionNavigationPrefs==='function') _applySessionNavigationPrefs();
|
||||
_scrollAfterMessageRender(preserveScroll);
|
||||
requestAnimationFrame(()=>{highlightCode();addCopyButtons();loadDiffInline();loadCsvInline();loadExcalidrawInline();loadPdfInline();loadHtmlInline();renderMermaidBlocks();renderKatexBlocks();});
|
||||
requestAnimationFrame(()=>{highlightCode();addCopyButtons();initTreeViews();loadPdfInline();loadHtmlInline();renderMermaidBlocks();renderKatexBlocks();});
|
||||
@@ -4626,6 +4671,7 @@ function renderMessages(options){
|
||||
const renderVisWithIdx=visWithIdx.slice(windowStart);
|
||||
const firstRenderedRawIdx=renderVisWithIdx.length?renderVisWithIdx[0].rawIdx:Infinity;
|
||||
const hasServerOlder=!!(typeof _messagesTruncated!=='undefined' && _messagesTruncated && S.messages.length>0);
|
||||
if(typeof _applySessionNavigationPrefs==='function') _applySessionNavigationPrefs();
|
||||
if(hiddenBeforeCount>0 || hasServerOlder){
|
||||
const indicator=document.createElement('button');
|
||||
indicator.type='button';
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
UI_JS = (REPO / "static" / "ui.js").read_text(encoding="utf-8")
|
||||
BOOT_JS = (REPO / "static" / "boot.js").read_text(encoding="utf-8")
|
||||
INDEX_HTML = (REPO / "static" / "index.html").read_text(encoding="utf-8")
|
||||
STYLE_CSS = (REPO / "static" / "style.css").read_text(encoding="utf-8")
|
||||
I18N_JS = (REPO / "static" / "i18n.js").read_text(encoding="utf-8")
|
||||
PANELS_JS = (REPO / "static" / "panels.js").read_text(encoding="utf-8")
|
||||
CONFIG_PY = (REPO / "api" / "config.py").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _function_body(src: str, signature: str) -> str:
|
||||
start = src.index(signature)
|
||||
brace = src.index("{", start)
|
||||
depth = 0
|
||||
for i in range(brace, len(src)):
|
||||
if src[i] == "{":
|
||||
depth += 1
|
||||
elif src[i] == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return src[start : i + 1]
|
||||
raise AssertionError(f"function body not found: {signature}")
|
||||
|
||||
|
||||
def test_session_jump_buttons_are_opt_in_and_keep_existing_bottom_button():
|
||||
assert '"session_jump_buttons": False' in CONFIG_PY
|
||||
assert '"session_jump_buttons"' in CONFIG_PY
|
||||
assert "window._sessionJumpButtonsEnabled=!!s.session_jump_buttons" in BOOT_JS
|
||||
assert "window._sessionJumpButtonsEnabled=false" in BOOT_JS
|
||||
assert "session_jump_buttons: !!($('settingsSessionJumpButtons')||{}).checked" in PANELS_JS
|
||||
|
||||
scroll_listener = UI_JS[UI_JS.index("el.addEventListener('scroll'") : UI_JS.index("})();", UI_JS.index("el.addEventListener('scroll'"))]
|
||||
assert "if(btn) btn.style.display=_scrollPinned?'none':'flex'" in scroll_listener
|
||||
assert "!_isSessionJumpButtonsEnabled()||_scrollPinned" not in UI_JS
|
||||
|
||||
|
||||
def test_jump_to_session_start_button_loads_full_history_and_scrolls_top():
|
||||
jump = _function_body(UI_JS, "async function jumpToSessionStart")
|
||||
update = _function_body(UI_JS, "function _updateSessionStartJumpButton")
|
||||
|
||||
assert 'id="jumpToSessionStartBtn"' in INDEX_HTML
|
||||
assert 'class="session-jump-btn session-jump-btn--start"' in INDEX_HTML
|
||||
assert "data-i18n=\"session_jump_start\"" in INDEX_HTML
|
||||
assert "data-i18n=\"session_jump_end\"" in INDEX_HTML
|
||||
assert "data-i18n-aria-label=\"session_jump_start_label\"" in INDEX_HTML
|
||||
assert "data-i18n-aria-label=\"session_jump_end_label\"" in INDEX_HTML
|
||||
|
||||
assert "_ensureAllMessagesLoaded" in jump
|
||||
assert "_messageRenderWindowSize=Math.max(_currentMessageRenderWindowSize(),_messageRenderableMessageCount())" in jump
|
||||
assert "renderMessages({ preserveScroll:true })" in jump
|
||||
assert "container.scrollTop=0" in jump
|
||||
assert "btn.style.display=(hasSession&&canRevealStart&&awayFromStart)?'flex':'none'" in update
|
||||
|
||||
|
||||
def test_session_jump_buttons_match_pill_layout_without_regressing_default_arrow():
|
||||
assert ".session-jump-btn" in STYLE_CSS
|
||||
assert ".session-jump-btn--start{top:16px" in STYLE_CSS
|
||||
assert ".session-jump-btn__text{display:none" in STYLE_CSS
|
||||
assert ".messages.session-nav-enabled .scroll-to-bottom-btn" in STYLE_CSS
|
||||
assert ".messages.session-nav-enabled .session-jump-btn__text{display:inline" in STYLE_CSS
|
||||
assert "classList.toggle('session-nav-enabled',_isSessionJumpButtonsEnabled())" in UI_JS
|
||||
|
||||
|
||||
def test_session_jump_buttons_are_i18n_localized_in_text_tooltip_and_aria():
|
||||
for key in [
|
||||
"session_jump_start",
|
||||
"session_jump_start_label",
|
||||
"session_jump_end",
|
||||
"session_jump_end_label",
|
||||
"settings_label_session_jump_buttons",
|
||||
"settings_desc_session_jump_buttons",
|
||||
]:
|
||||
assert I18N_JS.count(f"{key}:") >= 8, f"missing locale entries for {key}"
|
||||
assert "document.querySelectorAll('[data-i18n-aria-label]')" in I18N_JS
|
||||
assert "el.setAttribute('aria-label', val)" in I18N_JS
|
||||
Reference in New Issue
Block a user