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")