agents/run.ex
Agent run
One agent run, step by step, with the tool each step uses and the point where it asks a person.
# in the module that renders the template
use EHVisuals
<div class="h-96 rounded-xl border border-border">
<.agents_run animated />
</div>
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
agent |
string | "Intake agent" |
|
run |
string | "Run 771 · 00:42" |
|
status |
string | "Running" |
|
steps |
list | [%{state: "done", text: "Read the purchase order", tool: "mail.read"}, %{state: "done", ...}, ...] |
Up to five `%{text, tool, state}`. State is `done`, `running`, `waiting` or `pending`. |
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/agents/run.ex
defmodule EHVisuals.Agents.Run do
@moduledoc """
One agent run, step by step: what is done, what is running, and where it
stops to ask a person.
"""
use EHVisuals.Composition,
name: "agents/run",
title: "Agent run",
description:
"One agent run, step by step, with the tool each step uses and the point where it asks a person."
@steps [
%{text: "Read the purchase order", tool: "mail.read", state: "done"},
%{text: "Match it to the contract", tool: "wiki.search", state: "done"},
%{text: "Draft the ERP entry", tool: "erp.write", state: "running"},
%{text: "Ask Marta to approve", tool: "people", state: "pending"}
]
attr(:agent, :string, default: "Intake agent")
attr(:run, :string, default: "Run 771 · 00:42")
attr(:status, :string, default: "Running")
attr(:steps, :list,
default: @steps,
doc: "Up to five `%{text, tool, state}`. State is `done`, `running`, `waiting` or `pending`."
)
attr(:label, :string, default: "An agent run with two steps done and the third running")
frame_attrs()
@spec agents_run(map()) :: Phoenix.LiveView.Rendered.t()
def agents_run(assigns) do
rows = assigns.steps |> Enum.take(5) |> Enum.with_index()
assigns =
assigns
|> assign(:rows, rows)
|> assign(:last, length(rows) - 1)
|> assign(:pill_w, 28 + String.length(assigns.status) * 6.4)
|> assign(:height, 74 + length(rows) * 38 + 6)
~H"""
<.frame name="agents/run" 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">
<rect class="ehv-card-3" x="20" y="17" width="28" height="28" rx="8" />
<rect class="ehv-fill-lime" x="29" y="26" width="10" height="10" rx="3" />
<text class="ehv-t-strong" x="58" y="29" font-size="13">{@agent}</text>
<text class="ehv-mono" x="58" y="43" font-size="8.5">{String.upcase(@run)}</text>
<rect
class="ehv-fill-lime-soft"
x={320 - @pill_w}
y="21"
width={@pill_w}
height="20"
rx="6"
/>
<circle class="ehv-fill-lime ehv-a-pulse" cx={320 - @pill_w + 11} cy="31" r="2.5" />
<text class="ehv-mono ehv-t-lime" x="311" y="34.5" font-size="8.5" text-anchor="end">
{String.upcase(@status)}
</text>
</g>
<line class="ehv-line-faint" x1="20" x2="320" y1="60.5" y2="60.5" />
<g :for={{step, index} <- @rows}>
<line
:if={index < @last}
class={"ehv-line-soft ehv-a-fade ehv-i-#{index + 3}"}
x1="30"
x2="30"
y1={98 + index * 38}
y2={112 + index * 38}
/>
<g class={"ehv-a-rise ehv-i-#{index + 2}"}>
<g :if={step.state == "done"}>
<circle class="ehv-fill-lime-soft" cx="30" cy={86 + index * 38} r="9" />
<path
class={"ehv-line-lime ehv-round ehv-a-draw ehv-i-#{index + 4}"}
d={"M26 #{86 + index * 38} l2.8 2.8 l5 -5.6"}
pathLength="1"
/>
</g>
<g :if={step.state == "running"} class="ehv-agents-run__spin">
<circle class="ehv-line-soft" cx="30" cy={86 + index * 38} r="8" />
<path class="ehv-line-lime ehv-round" d={"M30 #{78 + index * 38} a8 8 0 0 1 8 8"} />
</g>
<g :if={step.state == "waiting"}>
<circle class="ehv-fill-lime ehv-a-ping" cx="30" cy={86 + index * 38} r="6" />
<circle class="ehv-node-lime" cx="30" cy={86 + index * 38} r="8" />
<rect class="ehv-fill-lime" x="27" y={83 + index * 38} width="6" height="6" rx="2" />
</g>
<circle
:if={step.state not in ["done", "running", "waiting"]}
class="ehv-node-soft"
cx="30"
cy={86 + index * 38}
r="8"
/>
<text
class={[step.state not in ["done", "running", "waiting"] && "ehv-t-3"]}
x="50"
y={90.5 + index * 38}
font-size="12.5"
>
{step.text}
</text>
<text
class={["ehv-mono", step.state == "waiting" && "ehv-t-lime"]}
x="320"
y={90 + index * 38}
font-size="8.5"
text-anchor="end"
>
{String.upcase(step.tool)}
</text>
</g>
</g>
</g>
</svg>
</.frame>
"""
end
end
assets/css/agents/run.css
/* agents/run */
.ehv-agents-run { --ehv-max-w: 360px; --ehv-step: 130ms; }
/* The spinner turns around the centre of its own track circle. */
@keyframes ehv-agents-run-spin { to { transform: rotate(360deg); } }
.ehv-agents-run__spin { transform-box: fill-box; transform-origin: center; }
.ehv--animated .ehv-agents-run__spin { animation: ehv-agents-run-spin 1.1s linear infinite; }