From e1f0810df5e236c1272c22fc97b13fc11d6fe35b Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Fri, 15 May 2026 19:33:34 -0700 Subject: [PATCH] Personalized pipeline: UI multi-select picker for kinds + variants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The action was registered + the block declared, but the automation builder's per-action config renderer didn't have a case for `personalized_pipeline` so users only saw the bare card with the generic delay-minutes input — no way to select which playlists to sync. This commit adds the multi-select picker. Backend: - `core/personalized/api.list_kinds(manager=...)` now optionally takes a manager and includes the resolved variant list per kind (calls each spec's variant_resolver(deps) when present). Singleton kinds get an empty `variants` list. Variant-bearing kinds (time_machine / genre_playlist / daily_mix / seasonal_mix) get their full enumerated set. - `web_server.py` `/api/personalized/kinds` route now passes a built manager so the variants list lands in the response. Frontend: - `webui/static/stats-automations.js` `_renderBlockConfigFields` gains a `personalized_pipeline` branch that renders a scrollable multi-select picker: - Singletons (Hidden Gems, Discovery Shuffle, Popular Picks, Fresh Tape, The Archives) = one checkbox row per kind - Variant kinds = a section header + one checkbox row per variant (e.g. Time Machine: 1960s/1970s/.../2020s; Seasonal: halloween/ christmas/valentines/summer/spring/autumn) - Pre-checks rows that match the existing `kinds` config on edit - New `_autoLoadPersonalizedKinds(slotKey)` fetches `/api/personalized/kinds` (cached after first load), renders the picker DOM, and pre-checks saved selections via `data-kind` / `data-variant` attributes on the checkboxes. - `_renderBuilderCanvas` calls the loader for any `cfg-*-kinds-picker` it finds in the freshly-rendered slots. - The save-time `_collectActionConfig` walks the picker's checked inputs (matched by `data-kind` attribute) and emits `{kinds: [{kind, variant?}, ...], refresh_first, skip_wishlist}` in the same shape the handler expects. Tests: - `tests/automation/test_automation_blocks.py::_FIELD_TYPES` adds 'personalized_playlist_select' so the block-shape regression test accepts the new field type. (Test was failing because it whitelists every field type used across all blocks.) - 189 automation + personalized API tests pass; full suite intact. --- core/personalized/api.py | 22 +++- tests/automation/test_automation_blocks.py | 3 +- web_server.py | 7 +- webui/static/stats-automations.js | 129 +++++++++++++++++++++ 4 files changed, 153 insertions(+), 8 deletions(-) diff --git a/core/personalized/api.py b/core/personalized/api.py index 1fc6fef8..bfa9de60 100644 --- a/core/personalized/api.py +++ b/core/personalized/api.py @@ -62,23 +62,35 @@ def _track_to_dict(track: Track) -> Dict[str, Any]: } -def list_kinds(registry: Optional[PlaylistKindRegistry] = None) -> Dict[str, Any]: +def list_kinds( + registry: Optional[PlaylistKindRegistry] = None, + manager: Optional[PersonalizedPlaylistManager] = None, +) -> Dict[str, Any]: """Return every registered playlist kind with metadata. UI uses this to render the "available playlists" picker. Each - kind reports whether it requires a variant and the resolved - variant set so the UI can render variant choices when relevant.""" + kind reports whether it requires a variant; when a manager is + supplied AND the kind has a variant_resolver, the resolved + variant list is also included so the UI can render variant + checkboxes without a second round-trip per kind.""" reg = registry or get_registry() out = [] for spec in reg.all(): - out.append({ + entry = { 'kind': spec.kind, 'name_template': spec.name_template, 'description': spec.description, 'requires_variant': spec.requires_variant, 'tags': list(spec.tags), 'default_config': spec.default_config.to_json_dict(), - }) + 'variants': [], + } + if manager is not None and spec.variant_resolver is not None: + try: + entry['variants'] = list(spec.variant_resolver(manager.deps) or []) + except Exception: + entry['variants'] = [] + out.append(entry) return {'success': True, 'kinds': out} diff --git a/tests/automation/test_automation_blocks.py b/tests/automation/test_automation_blocks.py index 64d95cfa..151725bb 100644 --- a/tests/automation/test_automation_blocks.py +++ b/tests/automation/test_automation_blocks.py @@ -34,7 +34,8 @@ def _shape_check(items, allowed_types): _FIELD_TYPES = { 'number', 'select', 'time', 'multi_select', 'checkbox', 'text', - 'mirrored_playlist_select', 'signal_input', 'script_select', + 'mirrored_playlist_select', 'personalized_playlist_select', + 'signal_input', 'script_select', } diff --git a/web_server.py b/web_server.py index 86269fa0..25bec6c7 100644 --- a/web_server.py +++ b/web_server.py @@ -26856,9 +26856,12 @@ def _build_personalized_manager(): @app.route('/api/personalized/kinds', methods=['GET']) def personalized_list_kinds(): - """List every registered personalized-playlist kind.""" + """List every registered personalized-playlist kind. Includes the + resolved variant list per kind that supports variants so the UI + can render kind+variant checkboxes without per-kind round-trips.""" try: - return jsonify(_personalized_api.list_kinds()) + manager = _build_personalized_manager() + return jsonify(_personalized_api.list_kinds(manager=manager)) except Exception as e: logger.error(f"Personalized kinds list error: {e}") return jsonify({"success": False, "error": str(e)}), 500 diff --git a/webui/static/stats-automations.js b/webui/static/stats-automations.js index 7df4ab4b..922d6607 100644 --- a/webui/static/stats-automations.js +++ b/webui/static/stats-automations.js @@ -5754,6 +5754,18 @@ function _renderBuilderCanvas() { canvas.innerHTML = html; // Load mirrored playlist selects if any are present _autoLoadMirroredSelects(); + // Personalized-pipeline kind pickers (one per slot that uses it) + ['when', 'do'].forEach(sk => { + if (document.getElementById('cfg-' + sk + '-kinds-picker')) { + _autoLoadPersonalizedKinds(sk); + } + }); + _autoBuilder.then.forEach((item, i) => { + const sk = 'then-' + i; + if (document.getElementById('cfg-' + sk + '-kinds-picker')) { + _autoLoadPersonalizedKinds(sk); + } + }); // Set up checkbox state for refresh_mirrored ['when', 'do'].forEach(sk => { const allCb = document.getElementById('cfg-' + sk + '-all'); @@ -5961,6 +5973,29 @@ function _renderBlockConfigFields(slotKey, blockType, config) {