streaming-ui-script · analytics assistant ← Back to docs
Live demo · NL → chart

Type a question, get a chart back

A natural-language analytics assistant that maps free-text questions onto a small mock warehouse. The same UI Script program renders the suggestion chips, the active chart, the breakdown table, and a written summary. All powered by one tool: analytics_query.

Ask the warehouse

Click a suggestion or type your own question. The mock router maps to one of five intents (revenue, signups, churn, plan-mix, top-customers) and returns the appropriate shape — labels, series, summary, and a few KPIs. The UI Script program is unaware of which intent fired; it just renders whatever data looks like.

$question = "How is revenue trending this quarter?"
$range = "90d"

result = Query("analytics_query", {q: $question, range: $range}, {
  intent: "loading",
  title: "Loading…",
  summary: "",
  metric: "",
  delta: "",
  trend: "",
  labels: [],
  series: [],
  colA: "",
  colB: "",
  colC: "",
  rows: []
})

questionField = FormControl("Ask a question", Input("question", "Try: how is revenue trending this quarter?", "text", null, $question))

rangeField = FormControl("Range", Select("range", [
  SelectItem("30d", "Last 30 days"),
  SelectItem("90d", "Last 90 days"),
  SelectItem("365d", "Last year")
], "90d", $range))

queryRow = Stack([questionField, rangeField], "row", "m", "stretch")

suggestions = Buttons([
  Button("Revenue trend",       Action([@Set($question, "How is revenue trending this quarter?")]),  "secondary", "normal", "small"),
  Button("Daily signups",       Action([@Set($question, "Daily signups in the last 30 days"), @Set($range, "30d")]), "secondary", "normal", "small"),
  Button("Plan mix",            Action([@Set($question, "Show me the plan mix")]),                   "secondary", "normal", "small"),
  Button("Recent churn",        Action([@Set($question, "Which customers churned recently?")]),      "secondary", "normal", "small"),
  Button("Top 5 customers",     Action([@Set($question, "Top 5 customers by revenue")]),             "secondary", "normal", "small")
])

intentBadge = Tag("Intent: " + result.intent, null, "sm", "primary")
trendTag = Tag(result.trend, null, "sm", result.trend == "up" ? "success" : result.trend == "down" ? "danger" : "neutral")

kpis = Stack([
  StatCard(result.metric, result.delta, result.trend),
  StatCard("Detected intent", result.intent),
  StatCard("Time range", $range)
], "row", "m", "stretch", "start", true)

chartLine = LineChart(result.labels, [Series(result.metric, result.series)], "natural", "Date", result.metric)
chartBar  = BarChart(result.labels, [Series(result.metric, result.series)])

chartTabs = Tabs([
  TabItem("trend", "Trend",   [chartLine]),
  TabItem("bars",  "Bars",    [chartBar])
])

tbl = Table([
  Col(result.colA, result.rows.label),
  Col(result.colB, result.rows.value, "number"),
  Col(result.colC, @Each(result.rows, "row", Tag(row.delta, null, "sm", row.deltaVariant)))
])

chartCard = Card([CardHeader(result.title, result.summary), kpis, chartTabs])
breakdownCard = Card([CardHeader("Breakdown", "Detail rows used to build the chart"), tbl])

view = result.intent == "loading" ? Skeleton(4) : Stack([chartCard, breakdownCard])

root = Stack([
  Card([
    CardHeader("Analytics assistant", "Ask anything about revenue, signups, churn, or customers"),
    Stack([intentBadge, trendTag], "row", "s", "center", "start", true),
    queryRow,
    suggestions
  ], "elevated"),
  view
])

The router

A tiny rule-based router stands in for a real LLM. Replace it with a model call (or a function-calling endpoint) and the front-end stays the same.

el.setTools({
  analytics_query: async ({ q, range }) => {
    await sleep(450);
    const intent = routeIntent(q);          // "revenue" | "signups" | "churn" | "plan-mix" | "top-customers"
    return buildPayload(intent, range);     // { intent, title, summary, metric, delta, trend, labels, series, table }
  },
});