From 3e54007baac569d3801a66808582f2903981f89e Mon Sep 17 00:00:00 2001
From: JB
Date: Mon, 13 Oct 2025 06:57:09 -0700
Subject: [PATCH] feat: Add status messaging and override management
functionality with improved UI elements
---
abogen/web/routes.py | 69 ++++++++++++++++++++++++++++++
abogen/web/static/styles.css | 65 ++++++++++++++++++++++++++++
abogen/web/templates/entities.html | 67 ++++++++++++++++++++++++++---
3 files changed, 196 insertions(+), 5 deletions(-)
diff --git a/abogen/web/routes.py b/abogen/web/routes.py
index 35eaa2b..4e3edb7 100644
--- a/abogen/web/routes.py
+++ b/abogen/web/routes.py
@@ -2151,6 +2151,10 @@ def entities_page() -> ResponseReturnValue:
raw_language = (request.args.get("lang") or settings.get("language") or "a").strip().lower()
language = raw_language if raw_language in languages_map else "a"
+ status_code = (request.args.get("status") or "").strip().lower()
+ status_token = (request.args.get("token") or "").strip()
+ status_error = (request.args.get("error") or "").strip()
+
query = (request.args.get("q") or "").strip()
voice_filter = (request.args.get("voice") or "all").strip().lower()
pronunciation_filter = (request.args.get("pronunciation") or "all").strip().lower()
@@ -2207,6 +2211,12 @@ def entities_page() -> ResponseReturnValue:
{"value": "without-pronunciation", "label": "No pronunciation"},
]
+ status_message = ""
+ if status_code == "saved":
+ status_message = f"Updated override for {status_token or 'override'}."
+ elif status_code == "deleted":
+ status_message = f"Deleted override for {status_token or 'override'}."
+
context = {
"options": options,
"language": language,
@@ -2220,10 +2230,69 @@ def entities_page() -> ResponseReturnValue:
"limit": limit_value,
"overrides": display_rows,
"stats": stats,
+ "status_message": status_message,
+ "status_error": status_error,
}
return render_template("entities.html", **context)
+@web_bp.post("/entities/override")
+def entities_override_update() -> ResponseReturnValue:
+ options = _template_options()
+ languages_map = options.get("languages", {})
+
+ raw_language = (request.form.get("lang") or "").strip().lower()
+ language = raw_language if raw_language in languages_map else "a"
+
+ token_value = (request.form.get("token") or "").strip()
+ action = (request.form.get("action") or "save").strip().lower()
+ pronunciation_value = (request.form.get("pronunciation") or "").strip()
+ voice_value = (request.form.get("voice") or "").strip()
+ notes_value = (request.form.get("notes") or "").strip()
+
+ redirect_params: Dict[str, Any] = {"lang": language}
+ state_mappings = (
+ ("state_voice", "voice"),
+ ("state_pronunciation", "pronunciation"),
+ ("state_limit", "limit"),
+ ("state_query", "q"),
+ )
+ for form_key, query_key in state_mappings:
+ value = (request.form.get(form_key) or "").strip()
+ if value:
+ redirect_params[query_key] = value
+
+ if not token_value:
+ redirect_params["status"] = "error"
+ redirect_params["error"] = "Missing override token."
+ return redirect(url_for("web.entities_page", **redirect_params))
+
+ status_code = "saved"
+ try:
+ if action == "delete":
+ delete_pronunciation_override(language=language, token=token_value)
+ status_code = "deleted"
+ else:
+ save_pronunciation_override(
+ language=language,
+ token=token_value,
+ pronunciation=pronunciation_value or None,
+ voice=voice_value or None,
+ notes=notes_value or None,
+ context=None,
+ )
+ status_code = "saved"
+ except Exception as exc: # pragma: no cover - defensive logging
+ current_app.logger.exception("Failed to %s override for token %s", action, token_value)
+ redirect_params["status"] = "error"
+ redirect_params["error"] = "Failed to update override."
+ return redirect(url_for("web.entities_page", **redirect_params))
+
+ redirect_params["status"] = status_code
+ redirect_params["token"] = token_value
+ return redirect(url_for("web.entities_page", **redirect_params))
+
+
@web_bp.route("/speakers", methods=["GET", "POST"])
def speaker_configs_page() -> ResponseReturnValue:
options = _template_options()
diff --git a/abogen/web/static/styles.css b/abogen/web/static/styles.css
index f512edd..c434357 100644
--- a/abogen/web/static/styles.css
+++ b/abogen/web/static/styles.css
@@ -25,6 +25,27 @@ body {
flex-direction: column;
}
+.notice {
+ margin: 0 0 1.5rem;
+ padding: 0.9rem 1.1rem;
+ border-radius: 16px;
+ border: 1px solid rgba(56, 189, 248, 0.32);
+ background: rgba(14, 165, 233, 0.18);
+ color: var(--text);
+ font-size: 0.95rem;
+ box-shadow: 0 12px 30px rgba(8, 15, 32, 0.22);
+}
+
+.notice--success {
+ border-color: rgba(52, 211, 153, 0.42);
+ background: rgba(34, 197, 94, 0.18);
+}
+
+.notice--error {
+ border-color: rgba(248, 113, 113, 0.48);
+ background: rgba(248, 113, 113, 0.18);
+}
+
.top-bar {
backdrop-filter: blur(20px);
background: linear-gradient(90deg, rgba(15, 23, 42, 0.9), rgba(30, 41, 59, 0.7));
@@ -1194,6 +1215,50 @@ button.step-indicator__item:focus-visible {
background: rgba(56, 189, 248, 0.08);
}
+.overrides-table__token {
+ font-weight: 600;
+ letter-spacing: 0.02em;
+}
+
+.overrides-table__input,
+.overrides-table__select {
+ width: 100%;
+ box-sizing: border-box;
+ border-radius: 12px;
+ border: 1px solid rgba(148, 163, 184, 0.2);
+ background: rgba(15, 23, 42, 0.45);
+ color: var(--text);
+ padding: 0.6rem 0.75rem;
+ font-size: 0.95rem;
+ transition: border 0.2s ease, box-shadow 0.2s ease;
+}
+
+.overrides-table__input:focus,
+.overrides-table__select:focus {
+ outline: none;
+ border-color: rgba(56, 189, 248, 0.55);
+ box-shadow: 0 0 0 3px rgba(56, 189, 248, 0.18);
+}
+
+.overrides-table__select {
+ appearance: none;
+}
+
+.overrides-table__actions-heading {
+ text-align: right;
+}
+
+.overrides-table__actions {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+ gap: 0.5rem;
+}
+
+.overrides-table__form {
+ margin: 0;
+}
+
.progress-bar {
width: 100%;
height: 9px;
diff --git a/abogen/web/templates/entities.html b/abogen/web/templates/entities.html
index 63a284c..ac06d81 100644
--- a/abogen/web/templates/entities.html
+++ b/abogen/web/templates/entities.html
@@ -59,9 +59,14 @@
{{ stats.with_pronunciation }} with pronunciations · {{ stats.with_voice }} with assigned voices
+ {% if status_message or status_error %}
+
+ {{ status_error or status_message }}
+
+ {% endif %}
{% if overrides %}