streaming-ui-script · docs portal ← Back to live examples
Live demo · knowledge base

A working help center & knowledge base, from one program

A documentation portal: SearchBar with keyboard hint, a Tree of categories, a rich Markdown article body, a Rating-driven "was this helpful?" block, a related-articles MediaCard grid, and a contact-support panel. Filtering by category or jumping to an article is one @Set.

Live preview

Search for "stream", "theme", or "rate", pick a category on the left, or click a related article to swap the body. The "Was this helpful" rating fires a follow-up message to the assistant.

$category = "all"
$article = "stream-llm"
$query = ""
$helpful = ""
$rating = 0
$contactReason = "general"

$articles = [
  {id: "quickstart",
   title: "Quick start: drop the script tag",
   category: "getting-started",
   tags: ["install", "html", "5 min"],
   readMin: 3,
   updated: "Updated May 12 · v2.3",
   image: "https://images.unsplash.com/photo-1517694712202-14dd9538aa97?w=480&h=240&fit=crop",
   excerpt: "Add one script tag and one `` element tag. That's it.",
   body: "Streaming UI Script ships as a single web component. Add the ESM bundle and the tag — every framework treats it like a native HTML element. See the [framework integration recipes](#frameworks) for React, Vue, Angular, Svelte, and plain HTML walkthroughs."},

  {id: "stream-llm",
   title: "Stream LLM tokens straight into the renderer",
   category: "getting-started",
   tags: ["streaming", "llm", "tools", "10 min"],
   readMin: 8,
   updated: "Updated May 11 · v2.3",
   image: "https://images.unsplash.com/photo-1517511620798-cec17d428bc0?w=480&h=240&fit=crop",
   excerpt: "Set streaming = true, pipe tokens into appendChunk(), set streaming = false. The renderer handles partial parses.",
   body: "**Streaming-first design.** The parser commits every fully-streamed line, so users see the page shell before the body has finished rendering. Set the `streaming` attribute on the element so banners and parse errors are suppressed mid-stream:\n\n1. `el.streaming = true` before the first chunk.\n2. Call `el.appendChunk(chunk)` for each incoming token.\n3. `el.streaming = false` when the stream finishes.\n\nIf you need progressive results without a real LLM, set `el.setResponse(...)` with a full program — every statement is committed in order."},

  {id: "themes",
   title: "Themes, tokens, and runtime customisation",
   category: "guides",
   tags: ["theming", "tokens", "css", "6 min"],
   readMin: 5,
   updated: "Updated May 09 · v2.3",
   image: "https://images.unsplash.com/photo-1503424886307-b090341d25d1?w=480&h=240&fit=crop",
   excerpt: "Seven built-in themes plus full token overrides. Authored programs never hard-code colours.",
   body: "**Pick a theme** via the `theme` attribute or pass a partial token map. Authored Streaming UI Script must work across every theme — use semantic `tone` props (\"primary\", \"success\", \"warning\", \"danger\") and let the runtime resolve the colour.\n\nThemes are CSS custom properties under the hood, so you can also style the host element from the outside:\n\n```css\nstreaming-ui-script {\n  --rui-color-primary: #16a34a;\n  --rui-radius-md: 14px;\n}\n```"},

  {id: "rate-limits",
   title: "Rate limits, batching, and retries",
   category: "api",
   tags: ["api", "rate-limit", "tools", "4 min"],
   readMin: 4,
   updated: "Updated May 08 · v2.3",
   image: "https://images.unsplash.com/photo-1507925921958-8a62f3d1a50d?w=480&h=240&fit=crop",
   excerpt: "Default limits, retry-with-backoff strategy, and how the tool layer handles failure.",
   body: "The renderer does not call your APIs directly — tool functions own that. Use **exponential backoff** with jitter for 429 / 503 responses, and surface a `Banner` to the user if retries exhaust. The `Query` re-runs automatically when any of its `$variable` args changes."},

  {id: "billing-cycle",
   title: "Understanding your billing cycle",
   category: "billing",
   tags: ["billing", "invoice", "plan", "3 min"],
   readMin: 3,
   updated: "Updated May 02 · v2.2",
   image: "https://images.unsplash.com/photo-1556742049-0cfed4f6a45d?w=480&h=240&fit=crop",
   excerpt: "Plans bill on the calendar month; trials end at midnight UTC on the next renewal anchor.",
   body: "Every plan bills on the **anchor day** you signed up on. Mid-cycle plan changes prorate the next invoice. Trials never auto-charge — you'll get an email five days before the renewal anchor."},

  {id: "refund-policy",
   title: "Refund policy and prorations",
   category: "billing",
   tags: ["billing", "refund", "policy", "2 min"],
   readMin: 2,
   updated: "Updated May 02 · v2.2",
   image: "https://images.unsplash.com/photo-1554224155-6726b3ff858f?w=480&h=240&fit=crop",
   excerpt: "Full refund inside 14 days, prorated otherwise. Annual plans refundable within 60 days.",
   body: "Inside the first 14 days, you can request a full refund — no questions asked. After that, monthly plans prorate the unused window. Annual plans refund the remaining months at the monthly rate up to 60 days."},

  {id: "errors-blank",
   title: "Nothing renders — what to check first",
   category: "troubleshooting",
   tags: ["errors", "shadow-dom", "html", "5 min"],
   readMin: 5,
   updated: "Updated May 12 · v2.3",
   image: "https://images.unsplash.com/photo-1505816014357-96b5ff457e9a?w=480&h=240&fit=crop",
   excerpt: "Walks through the four most common reasons the element appears blank.",
   body: "When nothing renders, check in this order:\n\n1. The `script` tag is `type=\"module\"` and the URL points to a valid bundle.\n2. The first non-blank line of the program is `root = Stack(...)`.\n3. `el.streaming` is `false` after the last chunk (otherwise the banner is suppressed).\n4. Open the host page's console — every parse error is also emitted as a custom `error` event you can log."},

  {id: "errors-tools",
   title: "Why is my tool called with undefined args?",
   category: "troubleshooting",
   tags: ["tools", "query", "mutation", "3 min"],
   readMin: 3,
   updated: "Updated May 11 · v2.3",
   image: "https://images.unsplash.com/photo-1556761175-b413da4baf72?w=480&h=240&fit=crop",
   excerpt: "Args reflect the call site verbatim — interpolate to empty strings, never to undefined.",
   body: "Tools receive whatever object the call site emits. If the LLM emits `{q: \"\" + $search}` and `$search` is empty, the tool sees `{ q: \"\" }`. Always default inside the tool with `?? \"\"` rather than relying on absence."}
]

categoryRows = $category == "all" ? $articles : @Filter($articles, "category", "==", $category)
visibleRows = $query == "" ? categoryRows : @Filter(categoryRows, "title", "contains", $query)
visibleCount = @Count(visibleRows)
totalCount = @Count($articles)

current = @First(@Filter($articles, "id", "==", $article))
currentExists = @Count(@Filter($articles, "id", "==", $article))

related = $query == "" ? @Filter($articles, "category", "==", current.category) : visibleRows
relatedFiltered = @Filter(related, "id", "!=", $article)

categoryLabel = $category == "getting-started" ? "Getting started" : ($category == "guides" ? "Guides" : ($category == "api" ? "API reference" : ($category == "billing" ? "Billing & plans" : ($category == "troubleshooting" ? "Troubleshooting" : "All categories"))))

navTree = Tree([
  TreeNode("All articles",        null, "book-open",      $category == "all",             $category == "all",             "" + totalCount,                                            Action([@Set($category, "all")])),
  TreeNode("Getting started",     null, "rocket",         $category == "getting-started", $category == "getting-started", "" + @Count(@Filter($articles, "category", "==", "getting-started")), Action([@Set($category, "getting-started")])),
  TreeNode("Guides",              null, "compass",        $category == "guides",          $category == "guides",          "" + @Count(@Filter($articles, "category", "==", "guides")),          Action([@Set($category, "guides")])),
  TreeNode("API reference",       null, "plug",           $category == "api",             $category == "api",             "" + @Count(@Filter($articles, "category", "==", "api")),             Action([@Set($category, "api")])),
  TreeNode("Billing & plans",     null, "credit-card",    $category == "billing",         $category == "billing",         "" + @Count(@Filter($articles, "category", "==", "billing")),         Action([@Set($category, "billing")])),
  TreeNode("Troubleshooting",     null, "screwdriver-wrench", $category == "troubleshooting", $category == "troubleshooting", "" + @Count(@Filter($articles, "category", "==", "troubleshooting")), Action([@Set($category, "troubleshooting")]))
])

quickLinksCard = Card([
  SectionHeader("Trending"),
  List([
    ListItem("Stream LLM tokens into the renderer", "+312 reads", "fire"),
    ListItem("Themes, tokens, and customisation",   "+182 reads", "fire"),
    ListItem("Quick start: drop the script tag",     "+91 reads",  "fire"),
    ListItem("Nothing renders — what to check first","+68 reads",  "fire")
  ])
])

contactCard = Card([
  SectionHeader("Still stuck?", "We answer in 2 hours on business days."),
  FormControl("What's it about?", Select("contactReason", [
    SelectItem("general",  "General question"),
    SelectItem("bug",      "Bug or unexpected behaviour"),
    SelectItem("billing",  "Billing or invoices"),
    SelectItem("feature",  "Feature request")
  ], null, null, $contactReason)),
  Buttons([
    Button("Open live chat",
           Action([@ToAssistant("Open live chat for " + $contactReason)]),
           "primary"),
    Button("Email support",
           Action([@OpenUrl("mailto:support@example.com")]),
           "secondary"),
    Button("Community Slack",
           Action([@OpenUrl("https://slack.example.com")]),
           "ghost")
  ])
])

sidePane = Stack([
  Card([
    SectionHeader("Categories"),
    navTree
  ]),
  quickLinksCard,
  contactCard
], "column", "m")

searchToolbar = Card([
  SearchBar("docs-q", "Search the docs…", $query, "/"),
  Stack([
    TagBlock(["streaming", "themes", "tools", "routing", "javascript", "components"]),
    Spacer(),
    TextContent("Press / to search", "small", "muted")
  ], "row", "m", "center")
])

resultListEmpty = EmptyState(
  "No articles match",
  "Try a different search or clear the category filter.",
  "magnifying-glass",
  Button("Reset filters",
         Action([@Set($category, "all"), @Reset($query)]),
         "secondary")
)

searchResultsRow = @Each(visibleRows, "a",
  Notification(
    a.title,
    a.excerpt,
    "" + a.readMin + " min · " + a.updated,
    "book",
    null,
    "info",
    false,
    [Button("Read",
            Action([@Set($article, a.id), @Reset($query)]),
            "secondary",
            "button",
            "small")]
  )
)

searchResults = $query == "" ? null : Card([
  SectionHeader("Search results", "" + visibleCount + " matches for \"" + $query + "\""),
  visibleCount == 0 ? resultListEmpty : Stack(searchResultsRow, "column", "s")
])

articleHeader = currentExists == 0 ? EmptyState("Pick an article", "Choose a tile on the left to read.", "book", null) : Stack([
  Breadcrumb([BreadcrumbItem("Help center", "#"), BreadcrumbItem(categoryLabel, "#"), BreadcrumbItem(current.title)]),
  Cover(
    current.title,
    current.image,
    current.excerpt,
    categoryLabel,
    current.readMin + " min read · " + current.updated,
    [
      Button("Print",         Action([@ToAssistant("Print " + current.title)]),         "secondary"),
      Button("Share article", Action([@OpenUrl("https://example.com/help/" + current.id)]), "ghost")
    ],
    "primary"
  ),
  TagBlock(current.tags)
], "column", "m")

articleBody = currentExists == 0 ? null : Card([
  Markdown(current.body),
  Separator("horizontal", true),
  SectionHeader("Was this helpful?", "Your feedback shapes which articles we prioritise."),
  Stack([
    Rating($rating, 5, null, null, "md", true),
    Spacer(),
    Buttons([
      Button("👍 Yes",
             Action([@Set($helpful, "yes"), @ToAssistant("User found the article useful: " + current.title)]),
             $helpful == "yes" ? "primary" : "secondary"),
      Button("👎 No",
             Action([@Set($helpful, "no"), @ToAssistant("User didn't find the article useful: " + current.title)]),
             $helpful == "no" ? "danger" : "secondary")
    ])
  ], "row", "m", "center"),
  $helpful == "no" ? Note("Thanks — your feedback opens a short improvement ticket on this article.", "info", "circle-info") : null,
  $helpful == "yes" ? Note("Glad it helped! Star the article to find it later.", "success", "circle-check") : null
])

relatedHeader = currentExists == 0 ? null : SectionHeader("Related articles", "Hand-picked next reads in " + categoryLabel, null, Tag("" + @Count(relatedFiltered) + " items", null, "sm", "info"))

relatedGrid = currentExists == 0 ? null : Grid(
  @Each(relatedFiltered, "r",
    MediaCard(
      r.title,
      r.image,
      r.excerpt,
      r.tags,
      "" + r.readMin + " min · " + r.updated,
      [Button("Read",
              Action([@Set($article, r.id)]),
              "secondary",
              "button",
              "small")]
    )
  ),
  3, "m"
)

contentCard = currentExists == 0 ? Card([articleHeader]) : Stack([articleHeader, articleBody, relatedHeader, relatedGrid], "column", "l")

popularFaqs = Card([
  SectionHeader("Popular FAQs", "Quick answers without leaving the page"),
  Accordion([
    AccordionItem("How do I switch themes at runtime?",
                  [Markdown("Pass a theme name to `el.setTheme(\"dark\")` or a partial token map. Themes are CSS custom properties under the hood — values propagate immediately without re-rendering.")],
                  true),
    AccordionItem("Where does the system prompt live?",
                  [Markdown("Two flavours ship in `dist/`: `system_prompt.txt` (full) and `system_prompt_chat.txt` (chat). Fetch one and prepend it to every model call. Programmatic prompts: `el.getSystemPrompt({ tools: [...] })`.")]),
    AccordionItem("Can I add my own components?",
                  [Markdown("Yes — call `el.registerComponents([...])`. Each spec contributes a name, description, props, and a `render(node, props, helpers)` function. The next `getSystemPrompt()` call automatically advertises the new component.")]),
    AccordionItem("How do I keep Tabs state across re-renders?",
                  [Markdown("Use a stable `defaultValue` derived from a `$variable`. Internal renderer state is keyed by tree path, so the active tab persists as long as the surrounding shape doesn't change.")])
  ])
])

splitLayout = SplitView([contentCard], [sidePane], "1.5fr")

header = PageHeader(
  "Help center",
  "" + totalCount + " articles · last updated " + (currentExists == 0 ? "today" : current.updated),
  Breadcrumb([BreadcrumbItem("Product", "#"), BreadcrumbItem("Help center")]),
  [
    Button("Status",
           Action([@OpenUrl("https://status.example.com")]),
           "ghost"),
    Button("Contact support",
           Action([@ToAssistant("Open the contact form")]),
           "primary")
  ],
  Badge("Live", "success")
)

stats = Stats([
  {label: "Articles",  value: "" + totalCount,                    hint: "across 5 categories", tone: "primary"},
  {label: "Reading time", value: currentExists == 0 ? "—" : ("" + current.readMin + " min"), hint: "current article", tone: "info"},
  {label: "Helpful",   value: "94%",                              hint: "rated useful",         tone: "success"},
  {label: "Response",  value: "2 hours",                          hint: "support reply time",   tone: "default"}
])

followUps = FollowUpBlock([
  FollowUpItem("Find articles about streaming"),
  FollowUpItem("Explain the theme token map"),
  FollowUpItem("Open the API rate-limit guide")
], "Need something specific?")

root = Stack([header, searchToolbar, stats, searchResults, splitLayout, popularFaqs, followUps], "column", "l")

Everything is in the program

This demo doesn't register any tools at all — the article corpus lives in $articles and every navigation is a declarative @Set. Plug your own tools in to fetch articles from a CMS, and the surface above won't have to change.

el.setResponse(document.getElementById("src-docs").textContent);