streaming-ui-script · support agent ← Back to docs
Live demo · async tools

An AI triage agent for a customer support inbox

A queue of incoming tickets on the left, and the same component renders the triage workspace on the right whenever you select one. The triage_ticket tool simulates an LLM round-trip (with a 600 ms delay) and returns a draft reply, severity, and routing suggestion. Use the action buttons to mutate the queue.

Triage workspace

The same <streaming-ui-script> instance drives both panes — the inbox shows tickets via list_tickets, and selecting one calls get_ticket + triage_ticket to render the details. Replying or escalating fires a Mutation that updates the in-memory store.

$status = "open"
$selectedId = "T-1042"
$reply = ""

inbox = Query("list_tickets", {status: $status}, {rows: []})
detail = Query("get_ticket", {id: $selectedId}, {id: "", subject: "", from: "", channel: "", body: "", waiting: 0, plan: ""})
triage = Query("triage_ticket", {id: $selectedId}, {priority: "", category: "", confidence: 0, draft: "", routing: "", tags: []})

statusFilter = FormControl("Inbox", Select("status", [
  SelectItem("open", "Open"),
  SelectItem("waiting", "Waiting on customer"),
  SelectItem("resolved", "Resolved")
], "open", $status))

ticketRow = @Each(inbox.rows, "t",
  Card([
    Stack([Tag(t.priority, null, "sm", t.priorityVariant), TextContent(t.waiting + "h ago", "small", "muted")], "row", "s", "center", "between"),
    TextContent(t.subject, "body-heavy"),
    TextContent(t.from + " · " + t.channel, "small", "muted"),
    Buttons([Button(t.id == $selectedId ? "Selected" : "Open", Action([@Set($selectedId, t.id)]), t.id == $selectedId ? "primary" : "secondary", "normal", "small")])
  ], "outlined")
)

empty = TextContent("Inbox zero. Take a coffee break.", "small", "muted")
inboxList = @Count(inbox.rows) > 0 ? Stack(ticketRow, "column", "s") : empty

inboxStats = Stack([
  StatCard("Open",     "" + @Count(inbox.rows)),
  StatCard("Avg wait", "" + @Round(@Avg(inbox.rows.waiting)) + "h"),
  StatCard("SLA risk", "" + @Count(inbox.rows.slaRisk))
], "row", "m", "stretch", "start", true)

leftPane = Card([
  CardHeader("Inbox", "Filter by status, then click a card to triage"),
  statusFilter,
  inboxStats,
  inboxList
])

confidenceTag = Tag("Confidence " + @Round(triage.confidence * 100) + "%", null, "sm", triage.confidence >= 0.7 ? "success" : triage.confidence >= 0.4 ? "warning" : "danger")
priorityTag = Tag(triage.priority, null, "sm", triage.priority == "Urgent" ? "danger" : triage.priority == "High" ? "warning" : "info")

triageHeader = Stack([priorityTag, Tag(triage.category, null, "sm", "primary"), confidenceTag], "row", "s", "center", "start", true)

routingCallout = Callout(triage.priority == "Urgent" ? "danger" : "info", "Routing suggestion", triage.routing)

draftField = FormControl("Suggested reply", TextArea("reply", triage.draft, $reply))

ticketHeader = Card([
  Stack([Tag(detail.plan, null, "sm", detail.plan == "Enterprise" ? "primary" : detail.plan == "Pro" ? "success" : "neutral"), TextContent("Waiting " + detail.waiting + "h", "small", "muted")], "row", "s", "center", "between"),
  TextContent(detail.subject, "large-heavy"),
  TextContent(detail.from + " · " + detail.channel, "small", "muted"),
  Separator("horizontal", true),
  TextContent(detail.body, "body")
], "elevated")

actions = Buttons([
  Button("Send reply",  Action([@Run(reply), @Run(inbox)]), "primary"),
  Button("Escalate",    Action([@Run(escalate), @Run(inbox)]), "secondary"),
  Button("Mark waiting",Action([@Run(markWaiting), @Run(inbox)]), "ghost"),
  Button("Resolve",     Action([@Run(resolve), @Run(inbox)]), "ghost")
])

reply = Mutation("send_reply", {id: $selectedId, body: $reply})
escalate = Mutation("escalate", {id: $selectedId})
markWaiting = Mutation("set_status", {id: $selectedId, status: "waiting"})
resolve = Mutation("set_status", {id: $selectedId, status: "resolved"})

tagsRow = TagBlock(triage.tags)

rightPane = Card([
  CardHeader("Triage · " + detail.id, "AI suggestion based on history, plan, and content"),
  triageHeader,
  routingCallout,
  ticketHeader,
  tagsRow,
  draftField,
  actions
])

root = Stack([leftPane, rightPane], "row", "l", "stretch")

How the AI is mocked

triage_ticket returns a deterministic-but-realistic suggestion per ticket. Swap in a real LLM call (or a queue of cached completions) and the UI doesn't change. The 600 ms delay simulates network latency so you can see the reactive cards re-render.

el.setTools({
  list_tickets:  ({ status }) => ({ rows: queue.filter((t) => t.status === status) }),
  get_ticket:    ({ id })     => queue.find((t) => t.id === id),
  triage_ticket: async ({ id }) => {
    await sleep(600);
    return triageFor(id);  // returns { priority, category, confidence, draft, routing, tags }
  },
  send_reply:    ({ id, body }) => { archive(id); return { ok: true }; },
  escalate:      ({ id })       => { mark(id, "escalated"); return { ok: true }; },
  set_status:    ({ id, status }) => { mark(id, status); return { ok: true }; },
});