@cms.ai/brand_brain
v0.1.0
Published
Client-side SDK for building CMS.ai Brand Brain (Guide) experiences into your own website UI.
Downloads
256
Readme
@cms.ai/brand_brain
Client-side SDK for building CMS.ai Brand Brain (Guide) experiences directly into your own website UI — your own chat, your own layout — instead of the drop-in overlay. It handles the visitor session, streaming chat across every guide skill, structured deliverables (documents, recommendations, how-tos, playlists, business cases, solution designs, assessments), conversation history, and lead capture, all against your branded CMS.ai host.
Framework-agnostic core + an optional React binding.
Machine-readable reference:
llms.txtships in this package — point your AI coding tool at it for the full wire contract.
Install
npm install @cms.ai/brand_brainQuick start (React)
import { useGuide } from "@cms.ai/brand_brain/react";
function Chat() {
const { messages, skillTurn, gate, ask, submitGate, retryLastAsk, isReady, isStreaming } = useGuide({
baseUrl: "https://guide.yourcompany.com", // your branded CMS.ai host
slug: "default", // which guide to load
});
return (
<div>
{messages.map((m) => (
<p key={m.id} data-role={m.role}>
{m.content}
</p>
))}
{/* Structured deliverable for the current turn — see "Rendering skills" */}
{skillTurn && skillTurn.documents.length > 0 && (
<ul>
{skillTurn.documents.map((d) => (
<li key={d.id}>
<a href={d.url}>{d.title}</a>
</li>
))}
</ul>
)}
{/* The guide may ask for an email before finishing a turn */}
{gate && (
<form
onSubmit={async (e) => {
e.preventDefault();
const email = new FormData(e.currentTarget).get("email") as string;
const result = await submitGate(email);
if (result.success && !result.needsVerification) await retryLastAsk();
}}
>
<input name="email" type="email" required placeholder="Work email" />
<button type="submit">Continue</button>
</form>
)}
<button disabled={!isReady || isStreaming} onClick={() => ask("What can you help me with?")}>
Ask
</button>
</div>
);
}useGuide bootstraps the client, streams replies into messages, folds every structured skill event into skillTurn, surfaces email gates on gate, and cleans the session up on unmount. Pass onEvent in the config to observe every raw stream event.
Quick start (vanilla JS/TS)
import { createSkillTurn, initGuide, reduceSkillEvent } from "@cms.ai/brand_brain";
const guide = await initGuide({
baseUrl: "https://guide.yourcompany.com",
slug: "default",
});
let turn = createSkillTurn();
for await (const event of guide.ask("How does pricing work?")) {
if (event.type === "text") {
appendToChatBubble(event.delta); // your render fn
} else if (event.type === "skill") {
turn = reduceSkillEvent(turn, event); // fold structured payloads into renderable state
renderSkill(turn);
} else if (event.type === "gate") {
const email = await promptForEmail(); // your UI
await guide.submitGate(email);
}
}baseUrl and where this runs
baseUrl should be your branded CMS.ai host on your own root domain (e.g. guide.yourcompany.com, the CNAME we provision). Same-site requests mean the visitor identity cookies attach in every browser with no third-party-cookie caveats.
If your app runs on a different site than the guide host (e.g. a preview on *.lovable.app talking to *.guides.navless.ai), everything still works — chat, skills, lead capture — but Safari and other browsers that block third-party cookies will treat the visitor as anonymous on each page load (no cross-visit identity, no conversation resume). Build the UI so nothing depends on the visitor being remembered, and move to a same-site branded host for production.
The ask() event stream
ask() returns an async iterator of GuideStreamEvents:
| event.type | Meaning |
| ---------------------------------------- | ---------------------------------------------------------------------------------------------- |
| run-started / run-finished | Turn lifecycle |
| message-start / text / message-end | The streamed chat reply (text.delta is the token chunk) |
| skill | A structured skill payload, fully typed and discriminated by name — see below |
| gate | The guide needs an email before continuing — submitGate(email), then re-send the message |
| error | A run or skill error — { message, code?, retryable?, retryHint? } |
| skill-unknown | Forward-compat escape hatch for structured events newer than this SDK version — safe to ignore |
Every turn runs exactly one skill (plain answers are the answer skill). Skill routing is automatic — the server picks the skill from the message. Force one with guide.ask(text, { forcedSkill: SkillResponseType.HowTo }).
A turn's skill events always follow the same lifecycle:
skill_start { skill, messageId } ← which skill is running
…payload events (see per-skill list)…
skill_end { skillInstanceId? } ← deliverable persisted; id usable with guide.skills.*You rarely need to handle the events by hand: reduceSkillEvent (used internally by useGuide) folds them into a GuideSkillTurn — one flat, renderable state object.
Rendering skills
What fills in on GuideSkillTurn depends on skillTurn.skill. Everything not listed stays at its empty default.
answer — grounded Q&A (always enabled)
The reply streams as text events into the chat bubble. Alongside it:
documents: GuideDocument[]— the sources behind the answer. Render as source/citation cards:title, optionalurl,thumbnailUrl, andcitationIndexmatching[N]marks in the reply text.followUpRecommendations: SkillRecommendation[]— follow-up pills. On click:ask(rec.prompt, { forcedSkill: rec.skill }).ctas: SkillCTA[]— render the first CTA below the response. Discriminated onkind:url(button openinghref),form(in-guide formformId),product(card linking tohref),scheduler(button opening theurlmeeting link).
howto — step-by-step guide
howToOutlinearrives first:{ title, steps: [{ id, title }] }. Render immediately as a numbered skeleton.howToStepsthen replaces the skeleton with detailed steps:{ id, title, description, icon?, content? }.contentis an optional source card (title,url,excerpt,thumbnailUrl).
Suggested UI: numbered checklist with step cards; a progress affordance (steps completed) works well. The persisted instance (skillInstanceId) supports saving progress via guide.skills.update.
recommend — content recommendations
contentRecommendations: ContentRecommendation[] — cards with title, rationale, and confidence (high/medium/low). On click: ask(rec.prompt, { forcedSkill: rec.targetSkill }).
playlist — curated content playlist
playlistHeader: { title, description }playlistItems: PlaylistItem[]— content cards:title,description,contentType(e.g.video_youtube,pdf,url),url,thumbnailUrl.rationalestreams in late per item — render it when it appears.
diagnose — self-assessment
diagnoseHeader: { targetTopic, subtitle, questionCount, subtype? }diagnoseQuestionsappend one at a time — render a stepper (questionCountsizes it). Each question hascategory,questionText, and single-choiceoptions(labelA–D,text, optionalscore).
There is no correct answer — it's a self-assessment. In standalone mode, use score per option to compute per-category results for a summary chart. In context_gathering mode (subtype), after the user answers, send a follow-up message summarizing their answers (see followUpSkill).
businesscase — generated business case
businessCaseFormats+businessCaseDraftsarrive: render the formats (label,description,icon) as a chooser.- When the user picks one, send its label as a new
ask(...). The draft then streams intobusinessCaseContent[subType]as markdown — render with your markdown component.
solutiondesign — architecture diagram
solutionDesignHeader: { title, summary }solutionDesignNodes/solutionDesignEdges— a flowchart: nodes havelabel,description,nodeType(process,decision,datastore,external,trigger,start,end), and a Lucideiconname; edges connect node ids with optional labels. Render with any diagram library — or fall back to a grouped list of nodes with their descriptions.
The email gate
Guides can be configured to require an email before delivering a skill. Mid-turn you'll get a gate event (the stream then ends), and in React gate becomes non-null. Flow:
- Render an email form.
await submitGate(email)— the pending gate's context is attached automatically.- If
result.needsVerificationis true, tell the user to check their inbox (magic link). Otherwise callretryLastAsk()to re-send the message and finish the turn.
Theming
guide.theme (a BrandKitTheme) carries the brand's design tokens as CSS color strings (hex or oklch(...)), named shadcn-style: primary, primaryForeground, secondary, secondaryForeground, background, foreground, muted, mutedForeground, border, input, ring. All optional — absent tokens mean "keep your default".
const { guide } = useGuide({ baseUrl, slug: "default" });
const style = guide?.theme
? ({
"--primary": guide.theme.primary,
"--primary-foreground": guide.theme.primaryForeground,
"--background": guide.theme.background,
"--foreground": guide.theme.foreground,
} as React.CSSProperties)
: undefined;
return <div style={style}>…</div>;Also on guide: logoUrl, faviconUrl, heroImageUrl, companyName, and customization (per-guide copy overrides — welcomeMessage, chatInputPlaceholders, suggestionsTitle, …). Use customization.welcomeMessage as the empty-state greeting and chatInputPlaceholders as rotating input placeholders. skillIconStyle customizes skill icon colors (mode: default | mono | per_skill).
API
initGuide(config) → GuideClient. Config: baseUrl, slug, optional domain (defaults to the baseUrl hostname, which is the registered account domain), consent ("auto" | { functional?, analytical? } | false), trackLoad (default true), fetch.
GuideClient:
guide— resolved metadata (id,accountId,theme,customization, …)ask(message, { forcedSkill?, signal? })— stream a reply (see above)messages/reset()— in-memory conversation historysetConsent({ functional?, analytical? })/getStatus()submitGate(email, options?)— capture a lead (magic link when verification is required)track(eventType, properties?)/trackPageView(url?)— analytics (calltrackPageViewon SPA route changes)endSession()— end the visit (call onpagehide;useGuidedoes this on unmount)conversations—getOrCreate(),list(),get(id, { cursor?, limit? }),delete(id)skills—listHistory(),get(id),update(id, data),selectDraft(id),markShared(id),importShared(id)
Core helpers: createSkillTurn() / reduceSkillEvent(turn, event) fold skill events into a GuideSkillTurn outside React.
useGuide(config) (from @cms.ai/brand_brain/react) returns { client, guide, messages, skillTurn, gate, isReady, isStreaming, error, ask, submitGate, retryLastAsk, reset }. Config additionally accepts onEvent(event).
Notes
- The SDK calls the existing public CMS.ai API — it introduces no new endpoints.
@navless/*internals are bundled in; the published package has no@navless/*runtime dependencies.react/react-domare optional peers used only by@cms.ai/brand_brain/react.
