mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-26 03:30:36 +00:00
01404ac062
* Shorten session sidebar relative time labels * feat: adaptive session title refresh based on conversation evolution Addresses #869 — the 'Optional' part: adapt session names to current conversation context instead of only generating once from the first exchange. Backend (api/streaming.py): - Add _latest_exchange_snippets() to extract last user+assistant pair - Add _count_exchanges() to count user messages - Add _get_title_refresh_interval() to read the setting - Add _run_background_title_refresh() — refreshes title from latest exchange with LLM, skips if title is unchanged or user manually renamed - Add _maybe_schedule_title_refresh() — checks exchange count and schedules refresh after stream_end (non-blocking) Config (api/config.py): - Add auto_title_refresh_every setting (default '0' = off) - Enum validation: {'0', '5', '10', '20'} Frontend: - Settings UI dropdown (static/index.html) - Wire up load/save in panels.js - i18n keys for all 6 locales (en/ru/es/de/zh/zh-Hant) Default: off. Opt-in via Settings > Conversation > Adaptive title refresh. * test: add 37 tests for adaptive title refresh helpers Covers all five new functions introduced in this PR: _count_exchanges, _latest_exchange_snippets, _get_title_refresh_interval, _run_background_title_refresh, _maybe_schedule_title_refresh Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> * fix(settings): show selected state on theme/skin/font-size picker cards The CSS rule `#mainSettings .theme-pick-btn { border-color: var(--border) !important }` was overriding the inline `style.borderColor = "var(--accent)"` set by `_syncThemePicker()` and siblings — `!important` beats inline styles. Active cards showed no visual highlight. Fix: move to `.active` CSS class with `border-color:var(--accent)!important` so the active rule wins over the base rule, and clear the stale inline borderColor/boxShadow from the sync functions. 5 regression tests added. Closes #1057 * fix: rename test file to match PR number, fix stale issue reference * docs: v0.50.211 release notes and version bump Compact sidebar timestamps, adaptive title refresh (opt-in), settings picker fix. * docs(changelog): correct settings tab for adaptive title refresh The v0.50.211 entry for #1058 said "Settings → Appearance" but the toggle is actually rendered inside settingsPanePreferences (the Preferences tab) per static/index.html:604+. The commit message also had the wrong tab ("Conversation"). Updated CHANGELOG to match the actual UI surface so users can find the toggle. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: create state dir before writing settings file save_settings() called SETTINGS_FILE.write_text() without ensuring the parent directory exists. In fresh environments (CI, first run without HERMES_WEBUI_STATE_DIR set) this raised FileNotFoundError. Add mkdir(parents=True, exist_ok=True) before the write. --------- Co-authored-by: Pavol Biely <biely@webtec.sk> Co-authored-by: bergeouss <bergeouss@users.noreply.github.com> Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com> Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
117 lines
4.6 KiB
Python
117 lines
4.6 KiB
Python
from collections import Counter
|
|
from pathlib import Path
|
|
import re
|
|
|
|
|
|
REPO = Path(__file__).resolve().parent.parent
|
|
|
|
|
|
def read(path: Path) -> str:
|
|
return path.read_text(encoding="utf-8")
|
|
|
|
|
|
def test_russian_locale_block_exists():
|
|
src = read(REPO / "static" / "i18n.js")
|
|
assert "\n ru: {" in src
|
|
assert "_label: 'Русский'" in src
|
|
assert "_speech: 'ru-RU'" in src
|
|
|
|
|
|
def extract_locale_block(src: str, locale_key: str) -> str:
|
|
start_match = re.search(rf"\b{re.escape(locale_key)}\s*:\s*\{{", src)
|
|
assert start_match, f"{locale_key} locale block not found"
|
|
|
|
start = start_match.end() - 1
|
|
depth = 0
|
|
in_single = False
|
|
in_double = False
|
|
in_backtick = False
|
|
escape = False
|
|
|
|
for i in range(start, len(src)):
|
|
ch = src[i]
|
|
|
|
if escape:
|
|
escape = False
|
|
continue
|
|
|
|
if in_single:
|
|
if ch == "\\":
|
|
escape = True
|
|
elif ch == "'":
|
|
in_single = False
|
|
continue
|
|
|
|
if in_double:
|
|
if ch == "\\":
|
|
escape = True
|
|
elif ch == '"':
|
|
in_double = False
|
|
continue
|
|
|
|
if in_backtick:
|
|
if ch == "\\":
|
|
escape = True
|
|
elif ch == "`":
|
|
in_backtick = False
|
|
continue
|
|
|
|
if ch == "'":
|
|
in_single = True
|
|
continue
|
|
if ch == '"':
|
|
in_double = True
|
|
continue
|
|
if ch == "`":
|
|
in_backtick = True
|
|
continue
|
|
|
|
if ch == "{":
|
|
depth += 1
|
|
continue
|
|
if ch == "}":
|
|
depth -= 1
|
|
if depth == 0:
|
|
return src[start + 1 : i]
|
|
|
|
raise AssertionError(f"{locale_key} locale block braces are not balanced")
|
|
|
|
|
|
def test_russian_locale_includes_representative_translations():
|
|
src = read(REPO / "static" / "i18n.js")
|
|
expected = [
|
|
"settings_title: '\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438'",
|
|
"login_title: '\u0412\u0445\u043e\u0434'",
|
|
"approval_heading: '\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u0435'",
|
|
"tab_tasks: '\u0417\u0430\u0434\u0430\u0447\u0438'",
|
|
"tab_profiles: '\u041f\u0440\u043e\u0444\u0438\u043b\u0438'",
|
|
"session_time_bucket_today: '\u0421\u0435\u0433\u043e\u0434\u043d\u044f'",
|
|
"onboarding_title: '\u0414\u043e\u0431\u0440\u043e \u043f\u043e\u0436\u0430\u043b\u043e\u0432\u0430\u0442\u044c \u0432 Hermes Web UI'",
|
|
"onboarding_complete: '\u041f\u0435\u0440\u0432\u0438\u0447\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0430'",
|
|
"profile_default_label: '\u0028\u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e\u0029'",
|
|
"profile_name_placeholder: '\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u043f\u0440\u043e\u0444\u0438\u043b\u044f \u0028\u0441\u0442\u0440\u043e\u0447\u043d\u044b\u0435 \u0431\u0443\u043a\u0432\u044b, a-z, 0-9, \u0434\u0435\u0444\u0438\u0441\u044b\u0029'",
|
|
"profile_clone_label: '\u0421\u043a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0438\u0437 \u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0433\u043e \u043f\u0440\u043e\u0444\u0438\u043b\u044f'",
|
|
"profile_base_url_placeholder: '\u0411\u0430\u0437\u043e\u0432\u044b\u0439 URL \u0028\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 http://localhost:11434\u0029'",
|
|
"profile_api_key_placeholder: 'API-\u043a\u043b\u044e\u0447 \u0028\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0029'",
|
|
]
|
|
for entry in expected:
|
|
assert entry in src
|
|
|
|
|
|
def test_russian_locale_covers_english_keys():
|
|
src = read(REPO / "static" / "i18n.js")
|
|
key_pattern = re.compile(r"^\s{4}([a-zA-Z0-9_]+):", re.MULTILINE)
|
|
en_keys = set(key_pattern.findall(extract_locale_block(src, "en")))
|
|
ru_keys = set(key_pattern.findall(extract_locale_block(src, "ru")))
|
|
|
|
missing = sorted(en_keys - ru_keys)
|
|
assert not missing, f"Russian locale missing keys: {missing}"
|
|
|
|
|
|
def test_russian_locale_has_no_duplicate_keys():
|
|
src = read(REPO / "static" / "i18n.js")
|
|
key_pattern = re.compile(r"^\s{4}([a-zA-Z0-9_]+):", re.MULTILINE)
|
|
keys = key_pattern.findall(extract_locale_block(src, "ru"))
|
|
duplicates = sorted(k for k, count in Counter(keys).items() if count > 1)
|
|
assert not duplicates, f"Russian locale has duplicate keys: {duplicates}"
|