empowered.house ↗

ai/thinking.ex

Thinking

A reasoning panel that lists its thinking steps as they complete, then settles into a one-line answer.

Use it
# in the module that renders the template
use EHVisuals

<div class="h-96 rounded-xl border border-border">
  <.ai_thinking animated />
</div>
default
isometric
still thinking
five steps · hover · no glow
light surface
isometric · fade out · still

Props

Prop Type Default What it does
title string "Thought for 12 seconds"
steps list ["Read the stock ledger for SKU 4471", "Compare the counts with the WMS", "Find three receipts that were not posted", "Check who owns the receiving step"] Two to five steps of up to 42 characters.
answer string "Three receipts are missing from the ERP." One line of up to 42 characters.
answer_label string "Answer"
thinking boolean false Shows the last step still running and the answer not written yet.
Frame props, shared by every composition
Prop Type Default What it does
animated boolean false Turns motion on. Without it the composition renders in its final, still state.
trigger "mount" | "in-view" | "in-view-repeat" "in-view" When the entrance plays: on page load, once when scrolled into view, or every time it comes back into view.
hover boolean false Enables the hover micro-interactions the composition supports.
glow boolean true The warm incandescent halo behind the composition.
fade_out boolean false Fades the bottom edge into the surface underneath.
isometric boolean false Tilts the composition into the isometric plane of the platform stack.
theme "dark" | "light" "dark" `dark` for the site canvas, `light` for the cream reversed surface.
class string nil Extra classes for the frame.
rest global — Any other HTML attribute, set on the frame.

Source

Two files. Copy them into your app if you would rather own the code than depend on the library.

lib/eh_visuals/ai/thinking.ex
thinking.ex
defmodule EHVisuals.Ai.Thinking do
  @moduledoc """
  A reasoning panel: the steps the model went through, ticked off one by one,
  and the one-line answer they settle into.
  """

  use EHVisuals.Composition,
    name: "ai/thinking",
    title: "Thinking",
    description:
      "A reasoning panel that lists its thinking steps as they complete, then settles into a one-line answer."

  @steps [
    "Read the stock ledger for SKU 4471",
    "Compare the counts with the WMS",
    "Find three receipts that were not posted",
    "Check who owns the receiving step"
  ]

  attr(:title, :string, default: "Thought for 12 seconds")
  attr(:steps, :list, default: @steps, doc: "Two to five steps of up to 42 characters.")

  attr(:answer, :string,
    default: "Three receipts are missing from the ERP.",
    doc: "One line of up to 42 characters."
  )

  attr(:answer_label, :string, default: "Answer")

  attr(:thinking, :boolean,
    default: false,
    doc: "Shows the last step still running and the answer not written yet."
  )

  attr(:label, :string,
    default: "A reasoning panel with four completed thinking steps and a one-line answer"
  )

  frame_attrs()

  @spec ai_thinking(map()) :: Phoenix.LiveView.Rendered.t()
  def ai_thinking(assigns) do
    rows = assigns.steps |> Enum.take(5) |> Enum.with_index()
    count = max(length(rows), 1)
    answer_y = 44 + 16 + count * 24 + 12

    assigns =
      assigns
      |> assign(:rows, rows)
      |> assign(:last, count - 1)
      |> assign(:well_h, 16 + count * 24)
      |> assign(:answer_y, answer_y)
      |> assign(:height, answer_y + 48 + 20)

    ~H"""
    <.frame name="ai/thinking" label={@label} {frame_opts(assigns)}>
      <svg viewBox={"0 0 340 #{@height}"}>
        <g class="ehv-h-lift">
          <rect class="ehv-card ehv-a-rise" x="0.5" y="0.5" width="339" height={@height - 1} rx="12" />
          <g class="ehv-a-fade ehv-i-1">
            <path class="ehv-line-ink ehv-round" d="M22 23.5 l4 4 l4 -4" />
            <text class="ehv-t-2" x="38" y="30" font-size="12.5">{@title}</text>
            <text class="ehv-mono" x="320" y="29.5" font-size="8.5" text-anchor="end">
              {length(@rows)} STEPS
            </text>
          </g>
          <rect
            class="ehv-well ehv-a-fade ehv-i-1"
            x="20.5"
            y="44.5"
            width="299"
            height={@well_h}
            rx="8"
          />
          <g :for={{step, index} <- @rows}>
            <line
              :if={index < @last}
              class={"ehv-line-soft ehv-a-fade ehv-i-#{min(index * 2 + 3, 24)}"}
              x1="37"
              x2="37"
              y1={72 + index * 24}
              y2={80 + index * 24}
            />
            <g class={"ehv-a-fade ehv-i-#{index * 2 + 2}"}>
              <g :if={@thinking and index == @last}>
                <circle class="ehv-ai-thinking__live ehv-a-ping" cx="37" cy={64 + index * 24} r="4" />
                <circle class="ehv-ai-thinking__live ehv-a-pulse" cx="37" cy={64 + index * 24} r="3" />
              </g>
              <path
                :if={!(@thinking and index == @last)}
                class={"ehv-ai-thinking__tick ehv-round ehv-a-draw ehv-i-#{index * 2 + 3}"}
                d={"M33 #{64 + index * 24} l2.8 2.8 l5 -5.6"}
                pathLength="1"
              />
              <text
                class={[!(@thinking and index == @last) && "ehv-t-2"]}
                x="52"
                y={68 + index * 24}
                font-size="11.5"
              >
                {step}
              </text>
            </g>
          </g>
          <g class={"ehv-a-rise ehv-i-#{min(length(@rows) * 2 + 3, 24)}"}>
            <rect
              class={if @thinking, do: "ehv-well ehv-dash", else: "ehv-card-lime"}
              x="20.5"
              y={@answer_y + 0.5}
              width="299"
              height="47"
              rx="8"
            />
            <text
              class={["ehv-mono", if(@thinking, do: "ehv-t-4", else: "ehv-t-lime")]}
              x="34"
              y={@answer_y + 18}
              font-size="8"
            >
              {String.upcase(@answer_label)}
            </text>
            <text :if={!@thinking} class="ehv-t-strong" x="34" y={@answer_y + 35.5} font-size="12.5">
              {@answer}
            </text>
            <rect
              :if={@thinking}
              class="ehv-fill-soft ehv-a-pulse"
              x="34"
              y={@answer_y + 27}
              width="196"
              height="7"
              rx="3.5"
            />
          </g>
        </g>
      </svg>
    </.frame>
    """
  end
end
assets/css/ai/thinking.css
thinking.css
/* ai/thinking */
.ehv-ai-thinking { --ehv-max-w: 370px; --ehv-step: 150ms; }

.ehv-ai-thinking__tick { fill: none; stroke: var(--ehv-ink-2); stroke-width: 1.25; }
.ehv-ai-thinking__live { fill: var(--ehv-lime); }