Direct AI-driven product demos for Phoenix apps, right from your Tidewave Web tab.
DemoDirector gives an AI agent (or, eventually, a saved script)
the seams to drive a Phoenix LiveView application as a guided tour:
subtitled explanations appear in an overlay, the next element to
interact with is highlighted, typing is slowed enough to read, and
selectors stay stable via the data-demo-id convention.
Concept
The package is intentionally small. It does not run demos itself —
it produces JavaScript strings that an agent passes to Tidewave's
browser_eval (or any equivalent in-browser evaluator), plus HEEx
components and JS/CSS that render the demo overlay in the page.
Two layers:
- Top-level helpers in this module (
subtitle/1,highlight/1,fill/2,fill_typed/3,click/1,wait/1) return JS source strings. The agent drives the demo by emitting them in sequence. - Overlay components in
DemoDirector.Componentsrender the subtitle bar and highlight ring. Apps mount these once on a layout.
Quick start
# In your layout (Phoenix.Component or LiveView):
import DemoDirector.Components
~H"""
<.demo_director_overlay />
"""
# In your HEEx templates, mark interactive elements:
import DemoDirector.HEEx
~H"""
<button {demo_id("save-prescription")}>Save</button>
"""
# Then the agent emits, in sequence, things like:
DemoDirector.subtitle("First we'll add a diagnosis.")
DemoDirector.highlight("save-prescription")
DemoDirector.fill_typed("notes", "Patient stable.")
DemoDirector.click("save-prescription")Each return value is a JS string. The agent passes it to
browser.eval(...) (Tidewave) or any evaluator with a JavaScript
execution context for the page.
Selectors
All helpers default to data-demo-id lookups. To target an element
the LiveView source doesn't yet expose, the agent should ask before
inventing a CSS selector — fragile selectors (:nth-child, deep
descendant chains) are exactly what data-demo-id exists to avoid.
Summary
Types
A demo-id string. Maps to the value of a data-demo-id attribute
on one or more elements in the rendered page.
Options accepted by typing-driven helpers.
Functions
Clicks the element with the given demo-id.
Fills the element with the given demo-id with value instantly.
Fills the element with the given demo-id one character at a time, dispatching input events between keystrokes.
Highlights the element with the given demo-id.
Sets the subtitle overlay text.
Pauses for ms milliseconds. Useful between steps to let the user
read a subtitle or watch a transition complete.
Types
@type demo_id() :: String.t()
A demo-id string. Maps to the value of a data-demo-id attribute
on one or more elements in the rendered page.
@type type_opts() :: [{:per_char_ms, pos_integer()}]
Options accepted by typing-driven helpers.
:per_char_ms— delay between simulated keystrokes (default: 35).
Functions
Clicks the element with the given demo-id.
Fills the element with the given demo-id with value instantly.
Useful for fields where typing animation would distract — uuids, prefilled fields, anything the user shouldn't be drawn to.
Fills the element with the given demo-id one character at a time, dispatching input events between keystrokes.
Default speed is 35ms per character. Pass per_char_ms: to
override:
DemoDirector.fill_typed("note", "Patient stable.", per_char_ms: 60)
Highlights the element with the given demo-id.
Renders a focus ring around the matching element and scrolls it
into view. Passing nil clears any active highlight.
Sets the subtitle overlay text.
Returns JS that finds the subtitle overlay (rendered by
DemoDirector.Components.demo_director_overlay/1) and
updates its text content.
@spec wait(pos_integer()) :: String.t()
Pauses for ms milliseconds. Useful between steps to let the user
read a subtitle or watch a transition complete.
Returned JS uses await, so the agent must wrap its sequence in
an async function (Tidewave's browser.eval supports this).