@cms.ai/brand_brain 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,47 +1,26 @@
1
1
  # @cms.ai/brand_brain
2
2
 
3
- 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, conversation history, and lead capture, all against your branded CMS.ai host.
3
+ 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.
4
4
 
5
5
  Framework-agnostic core + an optional React binding.
6
6
 
7
+ > Machine-readable reference: [`llms.txt`](./llms.txt) ships in this package — point your AI coding tool at it for the full wire contract.
8
+
7
9
  ## Install
8
10
 
9
11
  ```bash
10
12
  npm install @cms.ai/brand_brain
11
13
  ```
12
14
 
13
- ## Quick start (vanilla JS/TS)
14
-
15
- ```ts
16
- import { initGuide } from "@cms.ai/brand_brain";
17
-
18
- const guide = await initGuide({
19
- baseUrl: "https://guide.yourcompany.com", // your branded CMS.ai host
20
- slug: "default", // which guide to load
21
- });
22
-
23
- // Stream a reply into your own UI:
24
- for await (const event of guide.ask("How does pricing work?")) {
25
- if (event.type === "text") {
26
- appendToChatBubble(event.delta); // your render fn
27
- } else if (event.type === "gate") {
28
- // The guide is asking for an email before continuing.
29
- await guide.submitGate("visitor@acme.com");
30
- }
31
- }
32
- ```
33
-
34
- > **`baseUrl` must be your branded CMS.ai host on your own root domain** (e.g. `guide.yourcompany.com`, the CNAME we provision). That keeps the visitor cookies same-site, so identity + analytics work in every browser without any third-party-cookie caveats.
35
-
36
15
  ## Quick start (React)
37
16
 
38
17
  ```tsx
39
18
  import { useGuide } from "@cms.ai/brand_brain/react";
40
19
 
41
20
  function Chat() {
42
- const { messages, ask, isStreaming, isReady } = useGuide({
43
- baseUrl: "https://guide.yourcompany.com",
44
- slug: "default",
21
+ const { messages, skillTurn, gate, ask, submitGate, retryLastAsk, isReady, isStreaming } = useGuide({
22
+ baseUrl: "https://guide.yourcompany.com", // your branded CMS.ai host
23
+ slug: "default", // which guide to load
45
24
  });
46
25
 
47
26
  return (
@@ -51,6 +30,33 @@ function Chat() {
51
30
  {m.content}
52
31
  </p>
53
32
  ))}
33
+
34
+ {/* Structured deliverable for the current turn — see "Rendering skills" */}
35
+ {skillTurn && skillTurn.documents.length > 0 && (
36
+ <ul>
37
+ {skillTurn.documents.map((d) => (
38
+ <li key={d.id}>
39
+ <a href={d.url}>{d.title}</a>
40
+ </li>
41
+ ))}
42
+ </ul>
43
+ )}
44
+
45
+ {/* The guide may ask for an email before finishing a turn */}
46
+ {gate && (
47
+ <form
48
+ onSubmit={async (e) => {
49
+ e.preventDefault();
50
+ const email = new FormData(e.currentTarget).get("email") as string;
51
+ const result = await submitGate(email);
52
+ if (result.success && !result.needsVerification) await retryLastAsk();
53
+ }}
54
+ >
55
+ <input name="email" type="email" required placeholder="Work email" />
56
+ <button type="submit">Continue</button>
57
+ </form>
58
+ )}
59
+
54
60
  <button disabled={!isReady || isStreaming} onClick={() => ask("What can you help me with?")}>
55
61
  Ask
56
62
  </button>
@@ -59,23 +65,140 @@ function Chat() {
59
65
  }
60
66
  ```
61
67
 
68
+ `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.
69
+
70
+ ## Quick start (vanilla JS/TS)
71
+
72
+ ```ts
73
+ import { createSkillTurn, initGuide, reduceSkillEvent } from "@cms.ai/brand_brain";
74
+
75
+ const guide = await initGuide({
76
+ baseUrl: "https://guide.yourcompany.com",
77
+ slug: "default",
78
+ });
79
+
80
+ let turn = createSkillTurn();
81
+ for await (const event of guide.ask("How does pricing work?")) {
82
+ if (event.type === "text") {
83
+ appendToChatBubble(event.delta); // your render fn
84
+ } else if (event.type === "skill") {
85
+ turn = reduceSkillEvent(turn, event); // fold structured payloads into renderable state
86
+ renderSkill(turn);
87
+ } else if (event.type === "gate") {
88
+ const email = await promptForEmail(); // your UI
89
+ await guide.submitGate(email);
90
+ }
91
+ }
92
+ ```
93
+
94
+ ## `baseUrl` and where this runs
95
+
96
+ **`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.
97
+
98
+ 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.
99
+
62
100
  ## The `ask()` event stream
63
101
 
64
- `ask()` returns an async iterator of `GuideStreamEvent`s. The common case is `text`; `skill` carries every structured deliverable (documents, recommendations, how-to steps, diagnose questions, business cases, solution designs, …), discriminated by its `name`.
102
+ `ask()` returns an async iterator of `GuideStreamEvent`s:
103
+
104
+ | `event.type` | Meaning |
105
+ | ---------------------------------------- | ---------------------------------------------------------------------------------------------- |
106
+ | `run-started` / `run-finished` | Turn lifecycle |
107
+ | `message-start` / `text` / `message-end` | The streamed chat reply (`text.delta` is the token chunk) |
108
+ | `skill` | A structured skill payload, fully typed and discriminated by `name` — see below |
109
+ | `gate` | The guide needs an email before continuing — `submitGate(email)`, then re-send the message |
110
+ | `error` | A run or skill error — `{ message, code?, retryable?, retryHint? }` |
111
+ | `skill-unknown` | Forward-compat escape hatch for structured events newer than this SDK version — safe to ignore |
112
+
113
+ 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 })`.
114
+
115
+ A turn's skill events always follow the same lifecycle:
116
+
117
+ ```text
118
+ skill_start { skill, messageId } ← which skill is running
119
+ …payload events (see per-skill list)…
120
+ skill_end { skillInstanceId? } ← deliverable persisted; id usable with guide.skills.*
121
+ ```
122
+
123
+ You rarely need to handle the events by hand: `reduceSkillEvent` (used internally by `useGuide`) folds them into a `GuideSkillTurn` — one flat, renderable state object.
124
+
125
+ ## Rendering skills
126
+
127
+ What fills in on `GuideSkillTurn` depends on `skillTurn.skill`. Everything not listed stays at its empty default.
128
+
129
+ ### `answer` — grounded Q&A (always enabled)
130
+
131
+ The reply streams as `text` events into the chat bubble. Alongside it:
132
+
133
+ - `documents: GuideDocument[]` — the sources behind the answer. Render as source/citation cards: `title`, optional `url`, `thumbnailUrl`, and `citationIndex` matching `[N]` marks in the reply text.
134
+ - `followUpRecommendations: SkillRecommendation[]` — follow-up pills. On click: `ask(rec.prompt, { forcedSkill: rec.skill })`.
135
+ - `ctas: SkillCTA[]` — render the **first** CTA below the response. Discriminated on `kind`: `url` (button opening `href`), `form` (in-guide form `formId`), `product` (card linking to `href`), `scheduler` (button opening the `url` meeting link).
136
+
137
+ ### `howto` — step-by-step guide
138
+
139
+ 1. `howToOutline` arrives first: `{ title, steps: [{ id, title }] }`. Render immediately as a numbered skeleton.
140
+ 2. `howToSteps` then replaces the skeleton with detailed steps: `{ id, title, description, icon?, content? }`. `content` is an optional source card (`title`, `url`, `excerpt`, `thumbnailUrl`).
141
+
142
+ 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`.
143
+
144
+ ### `recommend` — content recommendations
145
+
146
+ `contentRecommendations: ContentRecommendation[]` — cards with `title`, `rationale`, and `confidence` (`high`/`medium`/`low`). On click: `ask(rec.prompt, { forcedSkill: rec.targetSkill })`.
65
147
 
66
- | `event.type` | Meaning |
67
- | ---------------------------------------- | ------------------------------------------------------------------------------ |
68
- | `run-started` / `run-finished` | Turn lifecycle |
69
- | `message-start` / `text` / `message-end` | The streamed chat reply (`text.delta` is the token chunk) |
70
- | `skill` | A structured skill payload — `{ name, value }`, where `name` is a `GuideEvent` |
71
- | `gate` | The guide needs an email before continuing — call `guide.submitGate(email)` |
72
- | `error` | A run or skill error — `{ message, code? }` |
148
+ ### `playlist` — curated content playlist
73
149
 
74
- Skill routing is automatic (the server picks the skill from the message). Force one with `guide.ask(text, { forcedSkill: SkillResponseType.HowTo })`.
150
+ - `playlistHeader: { title, description }`
151
+ - `playlistItems: PlaylistItem[]` — content cards: `title`, `description`, `contentType` (e.g. `video_youtube`, `pdf`, `url`), `url`, `thumbnailUrl`. `rationale` streams in **late** per item — render it when it appears.
152
+
153
+ ### `diagnose` — self-assessment
154
+
155
+ - `diagnoseHeader: { targetTopic, subtitle, questionCount, subtype? }`
156
+ - `diagnoseQuestions` **append one at a time** — render a stepper (`questionCount` sizes it). Each question has `category`, `questionText`, and single-choice `options` (`label` A–D, `text`, optional `score`).
157
+
158
+ 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`).
159
+
160
+ ### `businesscase` — generated business case
161
+
162
+ 1. `businessCaseFormats` + `businessCaseDrafts` arrive: render the formats (`label`, `description`, `icon`) as a chooser.
163
+ 2. When the user picks one, send its label as a new `ask(...)`. The draft then streams into `businessCaseContent[subType]` as markdown — render with your markdown component.
164
+
165
+ ### `solutiondesign` — architecture diagram
166
+
167
+ - `solutionDesignHeader: { title, summary }`
168
+ - `solutionDesignNodes` / `solutionDesignEdges` — a flowchart: nodes have `label`, `description`, `nodeType` (`process`, `decision`, `datastore`, `external`, `trigger`, `start`, `end`), and a Lucide `icon` name; edges connect node ids with optional labels. Render with any diagram library — or fall back to a grouped list of nodes with their descriptions.
169
+
170
+ ## The email gate
171
+
172
+ 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:
173
+
174
+ 1. Render an email form.
175
+ 2. `await submitGate(email)` — the pending gate's context is attached automatically.
176
+ 3. If `result.needsVerification` is true, tell the user to check their inbox (magic link). Otherwise call `retryLastAsk()` to re-send the message and finish the turn.
177
+
178
+ ## Theming
179
+
180
+ `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".
181
+
182
+ ```tsx
183
+ const { guide } = useGuide({ baseUrl, slug: "default" });
184
+
185
+ const style = guide?.theme
186
+ ? ({
187
+ "--primary": guide.theme.primary,
188
+ "--primary-foreground": guide.theme.primaryForeground,
189
+ "--background": guide.theme.background,
190
+ "--foreground": guide.theme.foreground,
191
+ } as React.CSSProperties)
192
+ : undefined;
193
+
194
+ return <div style={style}>…</div>;
195
+ ```
196
+
197
+ 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`).
75
198
 
76
199
  ## API
77
200
 
78
- `initGuide(config)` → `GuideClient`. Config: `baseUrl`, `slug`, optional `domain` (defaults to `window.location.hostname`), `consent` (`"auto"` | `{ functional?, analytical? }` | `false`), `trackLoad` (default `true`), `fetch`.
201
+ `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`.
79
202
 
80
203
  `GuideClient`:
81
204
 
@@ -85,10 +208,14 @@ Skill routing is automatic (the server picks the skill from the message). Force
85
208
  - `setConsent({ functional?, analytical? })` / `getStatus()`
86
209
  - `submitGate(email, options?)` — capture a lead (magic link when verification is required)
87
210
  - `track(eventType, properties?)` / `trackPageView(url?)` — analytics (call `trackPageView` on SPA route changes)
88
- - `endSession()` — end the visit (call on `pagehide`)
211
+ - `endSession()` — end the visit (call on `pagehide`; `useGuide` does this on unmount)
89
212
  - `conversations` — `getOrCreate()`, `list()`, `get(id, { cursor?, limit? })`, `delete(id)`
90
213
  - `skills` — `listHistory()`, `get(id)`, `update(id, data)`, `selectDraft(id)`, `markShared(id)`, `importShared(id)`
91
214
 
215
+ Core helpers: `createSkillTurn()` / `reduceSkillEvent(turn, event)` fold skill events into a `GuideSkillTurn` outside React.
216
+
217
+ `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)`.
218
+
92
219
  ## Notes
93
220
 
94
221
  - The SDK calls the existing public CMS.ai API — it introduces no new endpoints.
package/dist/index.cjs CHANGED
@@ -1386,7 +1386,11 @@ const SSEMessageType = {
1386
1386
  TextMessageEnd: "TEXT_MESSAGE_END",
1387
1387
  Custom: "CUSTOM"
1388
1388
  };
1389
- /** The `name` on `CUSTOM` guide events (skill payloads). */
1389
+ /**
1390
+ * The `name` on `CUSTOM` guide events (skill payloads). `SkillGate` and
1391
+ * `SkillError` never surface as `skill` stream events — the SDK re-maps them to
1392
+ * the dedicated `gate` / `error` {@link GuideStreamEvent} variants.
1393
+ */
1390
1394
  const GuideEvent = {
1391
1395
  SkillStart: "skill_start",
1392
1396
  SkillContent: "skill_content",
@@ -1406,8 +1410,7 @@ const GuideEvent = {
1406
1410
  SkillSolutionDesignDiagram: "skill_solutiondesign_diagram",
1407
1411
  SkillEnd: "skill_end",
1408
1412
  SkillError: "skill_error",
1409
- SkillGate: "skill_gate",
1410
- InputSuggestions: "input_suggestions"
1413
+ SkillGate: "skill_gate"
1411
1414
  };
1412
1415
  /** The guide skill types. `answer` is always on; the rest are enabled per-guide. */
1413
1416
  const SkillResponseType = {
@@ -1433,6 +1436,74 @@ const EmbedTrackingEvent = {
1433
1436
  EmbedSideTabClicked: "embed_side_tab_clicked",
1434
1437
  EmbedSideTabDismissed: "embed_side_tab_dismissed"
1435
1438
  };
1439
+ /** How a brand kit colors the guide's skill icons. */
1440
+ const SkillIconColorMode = {
1441
+ Default: "default",
1442
+ Mono: "mono",
1443
+ PerSkill: "per_skill"
1444
+ };
1445
+ /** Kind of content a document/playlist item points at. */
1446
+ const ContentType = {
1447
+ PDF: "pdf",
1448
+ DOCX: "docx",
1449
+ PPTX: "pptx",
1450
+ Image: "image",
1451
+ VideoNative: "video_native",
1452
+ VideoYouTube: "video_youtube",
1453
+ VideoWistia: "video_wistia",
1454
+ Document: "document",
1455
+ URL: "url",
1456
+ Text: "text",
1457
+ Markdown: "markdown"
1458
+ };
1459
+ /** Confidence level attached to a content recommendation. */
1460
+ const Confidence = {
1461
+ High: "high",
1462
+ Medium: "medium",
1463
+ Low: "low"
1464
+ };
1465
+ /** The render kind of a surfaced call-to-action. */
1466
+ const SkillCtaKind = {
1467
+ Url: "url",
1468
+ Form: "form",
1469
+ Product: "product",
1470
+ Scheduler: "scheduler"
1471
+ };
1472
+ /** Where a surfaced CTA renders. One slot today: below the skill response. */
1473
+ const CtaPlacement = { ResponseFooter: "response_footer" };
1474
+ /** Which scheduler embed a scheduler CTA loads. */
1475
+ const SchedulerProvider = {
1476
+ HubSpot: "hubspot",
1477
+ Calendly: "calendly"
1478
+ };
1479
+ /** Diagnose skill mode. */
1480
+ const DiagnoseSubtype = {
1481
+ Standalone: "standalone",
1482
+ ContextGathering: "context_gathering"
1483
+ };
1484
+ /** Answer option labels for diagnose questions. */
1485
+ const DiagnoseOptionLabel = {
1486
+ A: "A",
1487
+ B: "B",
1488
+ C: "C",
1489
+ D: "D"
1490
+ };
1491
+ /** Business case format sub-types. */
1492
+ const BusinessCaseSubType = {
1493
+ RoiAnalysis: "roi-analysis",
1494
+ StakeholderBrief: "stakeholder-brief",
1495
+ VendorComparison: "vendor-comparison"
1496
+ };
1497
+ /** Node types in a solution design diagram. */
1498
+ const SolutionDesignNodeType = {
1499
+ Process: "process",
1500
+ Decision: "decision",
1501
+ Datastore: "datastore",
1502
+ External: "external",
1503
+ Trigger: "trigger",
1504
+ Start: "start",
1505
+ End: "end"
1506
+ };
1436
1507
  //#endregion
1437
1508
  //#region src/chat-stream.ts
1438
1509
  const DATA_PREFIX = "data:";
@@ -1460,7 +1531,13 @@ async function* streamConversation(params) {
1460
1531
  body: JSON.stringify(body),
1461
1532
  signal
1462
1533
  });
1463
- if (!response.ok || !response.body) throw new Error(`[brand_brain] Conversation request failed with status ${response.status}`);
1534
+ if (!response.ok || !response.body) {
1535
+ let detail = "";
1536
+ try {
1537
+ detail = (await response.text()).trim();
1538
+ } catch {}
1539
+ throw new Error(`[brand_brain] Conversation request failed with status ${response.status}${detail ? `: ${detail}` : ""}`);
1540
+ }
1464
1541
  const reader = response.body.getReader();
1465
1542
  const decoder = new TextDecoder();
1466
1543
  let buffer = "";
@@ -1519,6 +1596,13 @@ function mapFrame(frame) {
1519
1596
  default: return null;
1520
1597
  }
1521
1598
  }
1599
+ /**
1600
+ * Names that surface as typed `skill` events. `SkillGate`/`SkillError` are
1601
+ * re-mapped to the `gate`/`error` variants below, so they are excluded here;
1602
+ * any name outside this set is a server event this SDK version predates and
1603
+ * flows through as `skill-unknown`.
1604
+ */
1605
+ const SKILL_EVENT_NAMES = new Set(Object.values(GuideEvent).filter((name) => name !== GuideEvent.SkillGate && name !== GuideEvent.SkillError));
1522
1606
  function mapCustomFrame(frame) {
1523
1607
  if (!frame.name) return null;
1524
1608
  const value = frame.value ?? {};
@@ -1531,20 +1615,42 @@ function mapCustomFrame(frame) {
1531
1615
  if (frame.name === GuideEvent.SkillError) return {
1532
1616
  type: "error",
1533
1617
  message: value.message ?? "Skill error",
1534
- code: value.errorType
1618
+ code: value.errorType,
1619
+ retryable: value.retryable,
1620
+ retryHint: value.retryHint
1535
1621
  };
1536
- return {
1622
+ if (SKILL_EVENT_NAMES.has(frame.name)) return {
1537
1623
  type: "skill",
1538
1624
  name: frame.name,
1539
1625
  value
1540
1626
  };
1627
+ return {
1628
+ type: "skill-unknown",
1629
+ name: frame.name,
1630
+ value
1631
+ };
1541
1632
  }
1542
1633
  //#endregion
1543
1634
  //#region src/GuideClient.ts
1635
+ /**
1636
+ * The account is resolved by the branded host the SDK talks to, and that host
1637
+ * must be same-site with `baseUrl` for the visitor cookies to attach — so the
1638
+ * `baseUrl` hostname IS the registered account domain. Derive it from there
1639
+ * rather than the host page's `window.location`, which is an unrelated origin
1640
+ * for cross-site embeds and produces a "Domain not found" 404 on resolve.
1641
+ */
1544
1642
  function resolveDomain(config) {
1545
1643
  if (config.domain) return config.domain;
1546
- if (typeof window !== "undefined") return window.location.hostname;
1547
- throw new Error("[brand_brain] `domain` is required when running without a browser `window`.");
1644
+ return hostnameFromBaseUrl(config.baseUrl);
1645
+ }
1646
+ /**
1647
+ * Normalize `baseUrl` to the bare host the server stores: lowercased, no port,
1648
+ * no leading `www.` (the domain write-path strips it, so a `www.` host can
1649
+ * never match). Accepts a bare host too, in case `baseUrl` omits the protocol.
1650
+ */
1651
+ function hostnameFromBaseUrl(baseUrl) {
1652
+ const withProtocol = /^[a-z][a-z0-9+.-]*:\/\//i.test(baseUrl) ? baseUrl : `https://${baseUrl}`;
1653
+ return new URL(withProtocol).hostname.toLowerCase().replace(/^www\./, "");
1548
1654
  }
1549
1655
  /**
1550
1656
  * The client returned by {@link initGuide}. Holds the resolved guide, the
@@ -1598,7 +1704,9 @@ var GuideClient = class GuideClient {
1598
1704
  this.#api = createApiClient(this.apiBaseUrl, this.fetchImpl);
1599
1705
  }
1600
1706
  static async init(config) {
1601
- const fetchImpl = config.fetch ?? globalThis.fetch.bind(globalThis);
1707
+ const globalFetch = typeof globalThis.fetch === "function" ? globalThis.fetch.bind(globalThis) : void 0;
1708
+ const fetchImpl = config.fetch ?? globalFetch;
1709
+ if (!fetchImpl) throw new Error("[brand_brain] No `fetch` is available — pass `config.fetch` when running without a global fetch.");
1602
1710
  const apiBaseUrl = toApiBaseUrl(config.baseUrl);
1603
1711
  const domain = resolveDomain(config);
1604
1712
  const guide = await createApiClient(apiBaseUrl, fetchImpl).guides.resolve({
@@ -1737,12 +1845,167 @@ async function initGuide(config) {
1737
1845
  return GuideClient.init(config);
1738
1846
  }
1739
1847
  //#endregion
1848
+ //#region src/skill-state.ts
1849
+ /** A fresh, empty turn state. */
1850
+ function createSkillTurn() {
1851
+ return {
1852
+ skill: null,
1853
+ status: "streaming",
1854
+ messageId: null,
1855
+ diagnoseSubtype: null,
1856
+ followUpSkill: null,
1857
+ content: "",
1858
+ documents: [],
1859
+ contentRecommendations: [],
1860
+ followUpRecommendations: [],
1861
+ ctas: [],
1862
+ diagnoseHeader: null,
1863
+ diagnoseQuestions: [],
1864
+ playlistHeader: null,
1865
+ playlistItems: [],
1866
+ howToOutline: null,
1867
+ howToSteps: [],
1868
+ businessCaseFormats: [],
1869
+ businessCaseDrafts: {},
1870
+ businessCaseContent: {},
1871
+ solutionDesignHeader: null,
1872
+ solutionDesignNodes: [],
1873
+ solutionDesignEdges: [],
1874
+ skillInstanceId: null
1875
+ };
1876
+ }
1877
+ /** A recommendations payload is content cards iff its entries carry `targetSkill`. */
1878
+ function isContentRecommendations(recommendations) {
1879
+ return recommendations.length > 0 && "targetSkill" in recommendations[0];
1880
+ }
1881
+ /**
1882
+ * Fold one `skill` stream event into the turn state. Pure and immutable — the
1883
+ * returned object is a new reference whenever anything changed, so the result
1884
+ * can back React state directly:
1885
+ *
1886
+ * ```ts
1887
+ * let turn = createSkillTurn();
1888
+ * for await (const event of client.ask(message)) {
1889
+ * if (event.type === "skill") turn = reduceSkillEvent(turn, event);
1890
+ * }
1891
+ * ```
1892
+ */
1893
+ function reduceSkillEvent(turn, event) {
1894
+ switch (event.name) {
1895
+ case GuideEvent.SkillStart: return {
1896
+ ...event.value.skill === SkillResponseType.BusinessCase && turn.skill === SkillResponseType.BusinessCase ? {
1897
+ ...createSkillTurn(),
1898
+ businessCaseFormats: turn.businessCaseFormats,
1899
+ businessCaseDrafts: turn.businessCaseDrafts,
1900
+ businessCaseContent: turn.businessCaseContent
1901
+ } : createSkillTurn(),
1902
+ skill: event.value.skill,
1903
+ messageId: event.value.messageId,
1904
+ diagnoseSubtype: event.value.diagnoseSubtype ?? null,
1905
+ followUpSkill: event.value.followUpSkill ?? null
1906
+ };
1907
+ case GuideEvent.SkillContent: return {
1908
+ ...turn,
1909
+ content: turn.content + event.value.delta
1910
+ };
1911
+ case GuideEvent.SkillDocuments: return {
1912
+ ...turn,
1913
+ documents: event.value.documents
1914
+ };
1915
+ case GuideEvent.SkillRecommendations: return isContentRecommendations(event.value.recommendations) ? {
1916
+ ...turn,
1917
+ contentRecommendations: event.value.recommendations
1918
+ } : {
1919
+ ...turn,
1920
+ followUpRecommendations: event.value.recommendations
1921
+ };
1922
+ case GuideEvent.SkillCTAs: return {
1923
+ ...turn,
1924
+ ctas: event.value.ctas
1925
+ };
1926
+ case GuideEvent.SkillDiagnoseHeader: return {
1927
+ ...turn,
1928
+ diagnoseHeader: event.value.header
1929
+ };
1930
+ case GuideEvent.SkillDiagnoseQuestion: return {
1931
+ ...turn,
1932
+ diagnoseQuestions: [...turn.diagnoseQuestions, event.value.question]
1933
+ };
1934
+ case GuideEvent.SkillPlaylistHeader: return {
1935
+ ...turn,
1936
+ playlistHeader: event.value.header
1937
+ };
1938
+ case GuideEvent.SkillPlaylistItems: return {
1939
+ ...turn,
1940
+ playlistItems: event.value.items
1941
+ };
1942
+ case GuideEvent.SkillPlaylistRationale: return {
1943
+ ...turn,
1944
+ playlistItems: turn.playlistItems.map((item) => item.id === event.value.itemId ? {
1945
+ ...item,
1946
+ rationale: event.value.rationale
1947
+ } : item)
1948
+ };
1949
+ case GuideEvent.SkillHowToOutline: return {
1950
+ ...turn,
1951
+ howToOutline: event.value.outline,
1952
+ skillInstanceId: event.value.skillInstanceId ?? turn.skillInstanceId
1953
+ };
1954
+ case GuideEvent.SkillHowToSteps: return {
1955
+ ...turn,
1956
+ howToSteps: event.value.steps
1957
+ };
1958
+ case GuideEvent.SkillBusinessCaseChooser: return {
1959
+ ...turn,
1960
+ businessCaseFormats: event.value.formats,
1961
+ businessCaseDrafts: event.value.drafts
1962
+ };
1963
+ case GuideEvent.SkillBusinessCaseDelta: {
1964
+ const existing = turn.businessCaseContent[event.value.subType] ?? "";
1965
+ return {
1966
+ ...turn,
1967
+ businessCaseContent: {
1968
+ ...turn.businessCaseContent,
1969
+ [event.value.subType]: existing + event.value.delta
1970
+ }
1971
+ };
1972
+ }
1973
+ case GuideEvent.SkillSolutionDesignHeader: return {
1974
+ ...turn,
1975
+ solutionDesignHeader: event.value.header
1976
+ };
1977
+ case GuideEvent.SkillSolutionDesignDiagram: return {
1978
+ ...turn,
1979
+ solutionDesignNodes: event.value.nodes,
1980
+ solutionDesignEdges: event.value.edges
1981
+ };
1982
+ case GuideEvent.SkillEnd: return {
1983
+ ...turn,
1984
+ status: "complete",
1985
+ skillInstanceId: event.value.skillInstanceId ?? event.value.skillResponseId ?? turn.skillInstanceId
1986
+ };
1987
+ default: return event;
1988
+ }
1989
+ }
1990
+ //#endregion
1991
+ exports.BusinessCaseSubType = BusinessCaseSubType;
1992
+ exports.Confidence = Confidence;
1740
1993
  exports.ConsentTier = ConsentTier;
1994
+ exports.ContentType = ContentType;
1995
+ exports.CtaPlacement = CtaPlacement;
1996
+ exports.DiagnoseOptionLabel = DiagnoseOptionLabel;
1997
+ exports.DiagnoseSubtype = DiagnoseSubtype;
1741
1998
  exports.EmbedTrackingEvent = EmbedTrackingEvent;
1742
1999
  exports.GuideClient = GuideClient;
1743
2000
  exports.GuideEvent = GuideEvent;
1744
2001
  exports.SSEMessageType = SSEMessageType;
2002
+ exports.SchedulerProvider = SchedulerProvider;
2003
+ exports.SkillCtaKind = SkillCtaKind;
2004
+ exports.SkillIconColorMode = SkillIconColorMode;
1745
2005
  exports.SkillResponseType = SkillResponseType;
2006
+ exports.SolutionDesignNodeType = SolutionDesignNodeType;
2007
+ exports.createSkillTurn = createSkillTurn;
1746
2008
  exports.initGuide = initGuide;
2009
+ exports.reduceSkillEvent = reduceSkillEvent;
1747
2010
 
1748
2011
  //# sourceMappingURL=index.cjs.map