{# AI Intent + AI Change Summary section — shared include for watch edit and tag/group edit. Required template context: llm_configured — bool: LLM provider is configured in settings form — the WTForms form (must have .llm_intent and .llm_change_summary fields) Group/tag edit only: llm_group_edit — bool: True when rendering the tag/group edit page. This is the ONLY way to tell the two contexts apart — the tags blueprint also passes the tag dict as `watch`, so `watch` is truthy in both. form.llm_backend_profile is ternary here (On / Off / Leave it to each watch) and is the group's only AI control; on a watch it is a checkbox. Watch edit only: watch — the Watch object (for processor check and prefilter display) llm_group_overrides — dict returned by _resolve_llm_group_overrides(): {'llm_intent': {'value': str, 'group_name': str} | None, 'llm_change_summary': {'value': str, 'group_name': str} | None, 'llm_backend_profile': {'value': bool, 'group_name': str} | None} The two prompt entries are non-None only when the watch has no own value AND a linked group set to "On" has one — that is what puts "From group '': " in the placeholder. llm_backend_profile is non-None when a linked group has taken the on/off decision (On or Off, not "leave it to each watch") — #4204. Usage (both): {% include "edit/include_llm_intent.html" %} #} {% from '_helpers.html' import render_field, render_checkbox_field, render_ternary_field %} {% set llm_group_mode = llm_group_edit|default(false) %} {# Processor check only applies in watch-edit context. #} {# In tag/group edit context the AI section is always visible. #} {# Processors whose edit form carries the AI Intent / Change Summary fields (i.e. those whose form inherits processor_text_json_diff_form). text_json_diff + restock_diff today. #} {% if llm_group_mode %} {% set show_ai_section = true %} {% else %} {% set show_ai_section = not watch.get('processor') or watch.get('processor') in ['text_json_diff', 'restock_diff'] %} {% endif %} {# An unrendered switch is absent from the POST, and WTForms reads that as off. So whenever we do NOT render the AI switch (no LLM provider configured, or a processor whose form has no AI fields) we carry its saved state through in a hidden input — otherwise merely saving the page would silently switch AI off. #} {% if not (show_ai_section and llm_configured) %} {% if llm_group_mode %} {# Group: ternary, so preserve whichever of the three states it is in #} {% if form.llm_backend_profile.data is not none %}{% endif %} {% elif form.llm_backend_profile.data %} {% endif %} {% endif %} {% if show_ai_section %} {# ── Configured: show the intent + summary fields ────────────────── #} {% if llm_configured %}
{% if llm_group_mode %} {# The group's single AI control. "On" is also what makes the prompts below cascade to the watches (tag_llm_applies_to_watches), so there is no second "does this override?" flag. #}
{{ render_ternary_field(form.llm_backend_profile) }} {{ _('On – every watch in this group uses the AI settings below, unless it fills in its own. Off – no AI for any watch in this group. Leave it to each watch – this group has no say; each watch uses its own AI settings.')|safe }}
{# The prompts below only mean something in the "On" state, so grey them out otherwise — the radio counterpart of the toggleOpacity cue used by #overrides_watch on the restock tab. #} {% else %} {# Per-watch AI on/off (#4204). Resolution is global settings → group → watch, so a group that has taken the decision (its AI setting is On or Off rather than "leave it to each watch") owns this control: we disable it, show the state the group decided, and name the group. The watch's own preference is not lost — because the field isn't user-writable in this state, edit.py ignores whatever the POST says for it and keeps the stored value. #} {% set profile_group = llm_group_overrides.llm_backend_profile if llm_group_overrides is defined else none %}
{# Only the checkbox is dimmed — the explanation of *why* has to stay readable. #} {% if profile_group %} {# Show what the group decided, not this watch's now-inert own value. #} {% set dummy = form.llm_backend_profile.__setattr__('checked', profile_group.value) %} {{ render_checkbox_field(form.llm_backend_profile, disabled=True) }} {% else %} {{ render_checkbox_field(form.llm_backend_profile) }} {% endif %}
{% if profile_group %} {# The group name links to its edit page so the decision can be changed where it lives. Built as escaped markup and passed into the sentence, so translators keep one string. #} {%- set group_link -%} {{ profile_group.group_name }} {%- endset -%} {% if profile_group.value %} {{ _("Group %(name)s decides this: AI is ON for every watch in that group.", name=group_link) | safe }} {% else %} {{ _("Group %(name)s decides this: AI is OFF for every watch in that group.", name=group_link) | safe }} {% endif %} {% endif %}
{% endif %}

{% if not llm_group_mode %} {{ _('Describe what you care about. The AI evaluates every detected change against this and only notifies you when it matches.') }} {% else %} {{ _('Set a change intent for all watches in this tag/group. Each watch can override with its own.') }} {% endif %}

{% if not llm_group_mode and llm_group_overrides is defined and llm_group_overrides.llm_intent %} {% set intent_placeholder = _("From group '%(name)s': %(value)s", name=llm_group_overrides.llm_intent.group_name, value=llm_group_overrides.llm_intent.value) %} {% elif not llm_group_mode %} {% set intent_placeholder = _('e.g. Alert me when the price drops below $300, or a new product is launched. Ignore footer and navigation changes.') %} {% else %} {% set intent_placeholder = _('e.g. Flag price changes or new product launches across all watches in this group') %} {% endif %} {{ render_field(form.llm_intent, placeholder=intent_placeholder, rows=5, class="pure-input-1") }}
{% if not llm_group_mode %}
{{ _('Examples:') }}
  • {{ _('Only notify if the price drops below $200, or a limited-time deal is added') }}
  • {{ _('Alert when a new recall, safety notice, or product withdrawal is published') }}
  • {{ _('Notify when a new grant round opens or an application deadline is announced') }}
  • {{ _('Only important if package versions change or a CVE is mentioned') }}
{% if watch is defined and watch.get('llm_prefilter') %}
{{ _('AI pre-filter active: %(filter)s — narrows content scope before evaluation', filter=watch.get('llm_prefilter')|e) | safe }}
{% endif %} {% endif %}
{# — AI Change Summary — #}

{% if not llm_group_mode %} {{ _('When a change is detected, the AI describes it according to your instructions and replaces %(diff)s in your notification. Use %(raw_diff)s if you still want the original diff.', diff='{{diff}}', raw_diff='{{raw_diff}}') | safe }} {% else %} {{ _('Describe how changes should be summarised in notifications for all watches in this group.') }} {% endif %}

{% if not llm_group_mode and llm_group_overrides is defined and llm_group_overrides.llm_change_summary %} {% set summary_placeholder = _("From group '%(name)s': %(value)s", name=llm_group_overrides.llm_change_summary.group_name, value=llm_group_overrides.llm_change_summary.value) %} {% else %} {% set summary_placeholder = form.llm_change_summary.render_kw['placeholder'] %} {% endif %} {{ render_field(form.llm_change_summary, placeholder=summary_placeholder, rows=5, class="pure-input-1") }} {{ _('Modify default prompt') }}
{{ render_field(form.llm_change_summary_mode) }} {% if not llm_group_mode %} {{ _('Appending keeps the prompt inherited from the group or from global settings and adds your text after it, so later edits to that prompt still reach this watch.') }} {% else %} {{ _('Appending keeps the prompt inherited from global settings and adds your text after it, so later edits to that prompt still reach this group.') }} {% endif %}
{% if not llm_group_mode %}
{{ _('Examples:') }}
  • {{ _('List each new item added with its name and price. Translate to English.') }}
  • {{ _('Summarise what events were added or cancelled. Two sentences maximum.') }}
  • {{ _('Describe the price change: old price, new price, percentage difference.') }}
{% endif %}
{# ── Not configured: greyed-out prompt to configure ──────────────── #} {% else %}

✨ {{ _('AI') }}

{% if not llm_group_mode %} {{ _('Configure an AI / LLM provider in Settings → AI / LLM to enable AI Change Intent and AI Change Summary.', url=url_for('settings.settings_page') + '#ai') | safe }} {% else %} {{ _('Configure an AI / LLM provider in Settings → AI / LLM to enable AI features for this group.', url=url_for('settings.settings_page') + '#ai') | safe }} {% endif %}

{% endif %} {% endif %}{# show_ai_section #}