{% load common_extras %} {% comment %} Step 3 — the message (SPEC §13.1, ROADMAP line 43). A plain HTMX/Alpine form that writes a single-node graph_json. No React embed: the canvas belongs to the flow builder and a broadcast is one message. What IS reused is the config panels' data shapes — apps/flows/schema/nodes.py's message_block, message_button and quick_reply fragments — so the same validator, the same sanitiser and the same engine node serve both. **Not one platform name appears below.** Every affordance is a question put to `composer`, which apps/broadcasts/composer.py assembles out of the capability table, the policy table, the template registry and the segment counter: composer.blocks which block kinds this platform renders composer.allows_buttons max_buttons > 0 composer.tag_choices policy.outside_window being a NeedsTag composer.tag_allowed_use_text Meta's own copy, displayed verbatim (SPEC §6.4) composer.templates approved_templates_for + variable_schema composer.cost_hint the workspace's own per-category estimates composer.counts_segments whether the SMS segment preview applies An `if` comparing a platform name here would be the first crack in contract 4's promise that a Layer-5 platform costs one module and one registry line, and test_composer.py scans these templates for exactly that comparison. Which is why the prose above does not spell one out — the scan is a plain substring search on purpose, the way templates/contacts/_filter_bar_script.html keeps a literal script tag out of its own comment for its extractor. {% endcomment %}
{% csrf_token %}

Write the message

What everyone in the audience receives. The preview shows it as it lands in the chat.

{% if composer.templates %}
What kind of message?

Outside the {{ composer.window_hours }}-hour messaging window this channel accepts approved templates only.

{% endif %} {# ── Composed message ─────────────────────────────────────── #}
Message {% if composer.counts_segments %} {% comment %} SPEC §6.6's segment-count preview, served by L5-D's own endpoint — which its docstring names this composer as a caller of. It counts through apps/channels/segments.py and multiplies by the workspace's `per_segment_cost`, so neither the arithmetic nor the price exists twice. Whether to offer it at all is the capability table's `counts_segments`, not a platform name. {% endcomment %} {% endif %}
{# ── Buttons and quick replies ────────────────────────────── #} {% comment %} Buttons, and deliberately not quick replies. SPEC §13.1 writes a broadcast's content as "send_message [+ buttons]" and issue #23 repeats it; the config payload still carries `allows_quick_replies` and `max_quick_replies` because they are the composer's honest reading of the capability table, and a later issue that wants them has the numbers. {% endcomment %}
Buttons A reply button parks the contact in a waiting run until they answer.
{# ── Approved template ────────────────────────────────────── #}
Approved template
{# ── Message tag (SPEC §6.4) ──────────────────────────────── #} {% if composer.tag_choices %}
Message tag Required for contacts outside the {{ composer.window_hours }}-hour window.
{% comment %} Meta's own allowed-use text, carried from the PlatformPolicy row and displayed verbatim (SPEC §6.4). Paraphrasing a compliance obligation is how a page gets disabled, so this is escaped ordinary text and never edited here. {% endcomment %}

{{ composer.tag_allowed_use_text }}

{% endif %} {# ── Live preview ─────────────────────────────────────────── #} {% comment %} The message as it lands in the chat (HANDOFF §3: "message editor with live preview"), drawn from the same Alpine state the form serialises — so it cannot show something other than what will be saved. Text only for media: the picture is a library id or a link here, and fetching it to draw a thumbnail is the detail page's job, not a keystroke's. {% endcomment %}

Preview

{% include "broadcasts/_step_back.html" %}