@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 +165 -38
- package/dist/index.cjs +272 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +560 -16
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +560 -16
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +261 -10
- package/dist/index.mjs.map +1 -1
- package/dist/react/index.cjs +261 -13
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +579 -20
- package/dist/react/index.d.cts.map +1 -1
- package/dist/react/index.d.mts +579 -20
- package/dist/react/index.d.mts.map +1 -1
- package/dist/react/index.mjs +261 -13
- package/dist/react/index.mjs.map +1 -1
- package/llms.txt +115 -0
- package/package.json +6 -5
- package/src/GuideClient.test.ts +28 -0
- package/src/GuideClient.ts +24 -3
- package/src/chat-stream.test.ts +28 -0
- package/src/chat-stream.ts +28 -3
- package/src/index.ts +2 -0
- package/src/react/index.ts +1 -1
- package/src/react/useGuide.ts +107 -6
- package/src/skill-state.test.ts +144 -0
- package/src/skill-state.ts +206 -0
- package/src/types.ts +504 -17
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 `
|
|
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
|
-
/**
|
|
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)
|
|
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
|
-
|
|
1547
|
-
|
|
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
|
|
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
|