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 %} +| Token | @@ -70,17 +75,69 @@Notes | Usage | Last updated | +Actions | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| {{ override.token }} | -{{ override.pronunciation or "—" }} | -{{ override.voice or "—" }} | -{{ override.notes or "" }} | +{{ override.token }} | ++ + | ++ + | ++ + | {{ override.usage_count }} | {{ override.updated_at_label }} | ++ + |