@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/dist/react/index.cjs
CHANGED
|
@@ -1387,7 +1387,11 @@ const SSEMessageType = {
|
|
|
1387
1387
|
TextMessageEnd: "TEXT_MESSAGE_END",
|
|
1388
1388
|
Custom: "CUSTOM"
|
|
1389
1389
|
};
|
|
1390
|
-
/**
|
|
1390
|
+
/**
|
|
1391
|
+
* The `name` on `CUSTOM` guide events (skill payloads). `SkillGate` and
|
|
1392
|
+
* `SkillError` never surface as `skill` stream events — the SDK re-maps them to
|
|
1393
|
+
* the dedicated `gate` / `error` {@link GuideStreamEvent} variants.
|
|
1394
|
+
*/
|
|
1391
1395
|
const GuideEvent = {
|
|
1392
1396
|
SkillStart: "skill_start",
|
|
1393
1397
|
SkillContent: "skill_content",
|
|
@@ -1407,8 +1411,17 @@ const GuideEvent = {
|
|
|
1407
1411
|
SkillSolutionDesignDiagram: "skill_solutiondesign_diagram",
|
|
1408
1412
|
SkillEnd: "skill_end",
|
|
1409
1413
|
SkillError: "skill_error",
|
|
1410
|
-
SkillGate: "skill_gate"
|
|
1411
|
-
|
|
1414
|
+
SkillGate: "skill_gate"
|
|
1415
|
+
};
|
|
1416
|
+
/** The guide skill types. `answer` is always on; the rest are enabled per-guide. */
|
|
1417
|
+
const SkillResponseType = {
|
|
1418
|
+
Answer: "answer",
|
|
1419
|
+
HowTo: "howto",
|
|
1420
|
+
Diagnose: "diagnose",
|
|
1421
|
+
Recommend: "recommend",
|
|
1422
|
+
Playlist: "playlist",
|
|
1423
|
+
BusinessCase: "businesscase",
|
|
1424
|
+
SolutionDesign: "solutiondesign"
|
|
1412
1425
|
};
|
|
1413
1426
|
/** Analytics events fired by the SDK bootstrap. */
|
|
1414
1427
|
const EmbedTrackingEvent = {
|
|
@@ -1445,7 +1458,13 @@ async function* streamConversation(params) {
|
|
|
1445
1458
|
body: JSON.stringify(body),
|
|
1446
1459
|
signal
|
|
1447
1460
|
});
|
|
1448
|
-
if (!response.ok || !response.body)
|
|
1461
|
+
if (!response.ok || !response.body) {
|
|
1462
|
+
let detail = "";
|
|
1463
|
+
try {
|
|
1464
|
+
detail = (await response.text()).trim();
|
|
1465
|
+
} catch {}
|
|
1466
|
+
throw new Error(`[brand_brain] Conversation request failed with status ${response.status}${detail ? `: ${detail}` : ""}`);
|
|
1467
|
+
}
|
|
1449
1468
|
const reader = response.body.getReader();
|
|
1450
1469
|
const decoder = new TextDecoder();
|
|
1451
1470
|
let buffer = "";
|
|
@@ -1504,6 +1523,13 @@ function mapFrame(frame) {
|
|
|
1504
1523
|
default: return null;
|
|
1505
1524
|
}
|
|
1506
1525
|
}
|
|
1526
|
+
/**
|
|
1527
|
+
* Names that surface as typed `skill` events. `SkillGate`/`SkillError` are
|
|
1528
|
+
* re-mapped to the `gate`/`error` variants below, so they are excluded here;
|
|
1529
|
+
* any name outside this set is a server event this SDK version predates and
|
|
1530
|
+
* flows through as `skill-unknown`.
|
|
1531
|
+
*/
|
|
1532
|
+
const SKILL_EVENT_NAMES = new Set(Object.values(GuideEvent).filter((name) => name !== GuideEvent.SkillGate && name !== GuideEvent.SkillError));
|
|
1507
1533
|
function mapCustomFrame(frame) {
|
|
1508
1534
|
if (!frame.name) return null;
|
|
1509
1535
|
const value = frame.value ?? {};
|
|
@@ -1516,20 +1542,42 @@ function mapCustomFrame(frame) {
|
|
|
1516
1542
|
if (frame.name === GuideEvent.SkillError) return {
|
|
1517
1543
|
type: "error",
|
|
1518
1544
|
message: value.message ?? "Skill error",
|
|
1519
|
-
code: value.errorType
|
|
1545
|
+
code: value.errorType,
|
|
1546
|
+
retryable: value.retryable,
|
|
1547
|
+
retryHint: value.retryHint
|
|
1520
1548
|
};
|
|
1521
|
-
return {
|
|
1549
|
+
if (SKILL_EVENT_NAMES.has(frame.name)) return {
|
|
1522
1550
|
type: "skill",
|
|
1523
1551
|
name: frame.name,
|
|
1524
1552
|
value
|
|
1525
1553
|
};
|
|
1554
|
+
return {
|
|
1555
|
+
type: "skill-unknown",
|
|
1556
|
+
name: frame.name,
|
|
1557
|
+
value
|
|
1558
|
+
};
|
|
1526
1559
|
}
|
|
1527
1560
|
//#endregion
|
|
1528
1561
|
//#region src/GuideClient.ts
|
|
1562
|
+
/**
|
|
1563
|
+
* The account is resolved by the branded host the SDK talks to, and that host
|
|
1564
|
+
* must be same-site with `baseUrl` for the visitor cookies to attach — so the
|
|
1565
|
+
* `baseUrl` hostname IS the registered account domain. Derive it from there
|
|
1566
|
+
* rather than the host page's `window.location`, which is an unrelated origin
|
|
1567
|
+
* for cross-site embeds and produces a "Domain not found" 404 on resolve.
|
|
1568
|
+
*/
|
|
1529
1569
|
function resolveDomain(config) {
|
|
1530
1570
|
if (config.domain) return config.domain;
|
|
1531
|
-
|
|
1532
|
-
|
|
1571
|
+
return hostnameFromBaseUrl(config.baseUrl);
|
|
1572
|
+
}
|
|
1573
|
+
/**
|
|
1574
|
+
* Normalize `baseUrl` to the bare host the server stores: lowercased, no port,
|
|
1575
|
+
* no leading `www.` (the domain write-path strips it, so a `www.` host can
|
|
1576
|
+
* never match). Accepts a bare host too, in case `baseUrl` omits the protocol.
|
|
1577
|
+
*/
|
|
1578
|
+
function hostnameFromBaseUrl(baseUrl) {
|
|
1579
|
+
const withProtocol = /^[a-z][a-z0-9+.-]*:\/\//i.test(baseUrl) ? baseUrl : `https://${baseUrl}`;
|
|
1580
|
+
return new URL(withProtocol).hostname.toLowerCase().replace(/^www\./, "");
|
|
1533
1581
|
}
|
|
1534
1582
|
/**
|
|
1535
1583
|
* The client returned by {@link initGuide}. Holds the resolved guide, the
|
|
@@ -1583,7 +1631,9 @@ var GuideClient = class GuideClient {
|
|
|
1583
1631
|
this.#api = createApiClient(this.apiBaseUrl, this.fetchImpl);
|
|
1584
1632
|
}
|
|
1585
1633
|
static async init(config) {
|
|
1586
|
-
const
|
|
1634
|
+
const globalFetch = typeof globalThis.fetch === "function" ? globalThis.fetch.bind(globalThis) : void 0;
|
|
1635
|
+
const fetchImpl = config.fetch ?? globalFetch;
|
|
1636
|
+
if (!fetchImpl) throw new Error("[brand_brain] No `fetch` is available — pass `config.fetch` when running without a global fetch.");
|
|
1587
1637
|
const apiBaseUrl = toApiBaseUrl(config.baseUrl);
|
|
1588
1638
|
const domain = resolveDomain(config);
|
|
1589
1639
|
const guide = await createApiClient(apiBaseUrl, fetchImpl).guides.resolve({
|
|
@@ -1722,26 +1772,181 @@ async function initGuide(config) {
|
|
|
1722
1772
|
return GuideClient.init(config);
|
|
1723
1773
|
}
|
|
1724
1774
|
//#endregion
|
|
1775
|
+
//#region src/skill-state.ts
|
|
1776
|
+
/** A fresh, empty turn state. */
|
|
1777
|
+
function createSkillTurn() {
|
|
1778
|
+
return {
|
|
1779
|
+
skill: null,
|
|
1780
|
+
status: "streaming",
|
|
1781
|
+
messageId: null,
|
|
1782
|
+
diagnoseSubtype: null,
|
|
1783
|
+
followUpSkill: null,
|
|
1784
|
+
content: "",
|
|
1785
|
+
documents: [],
|
|
1786
|
+
contentRecommendations: [],
|
|
1787
|
+
followUpRecommendations: [],
|
|
1788
|
+
ctas: [],
|
|
1789
|
+
diagnoseHeader: null,
|
|
1790
|
+
diagnoseQuestions: [],
|
|
1791
|
+
playlistHeader: null,
|
|
1792
|
+
playlistItems: [],
|
|
1793
|
+
howToOutline: null,
|
|
1794
|
+
howToSteps: [],
|
|
1795
|
+
businessCaseFormats: [],
|
|
1796
|
+
businessCaseDrafts: {},
|
|
1797
|
+
businessCaseContent: {},
|
|
1798
|
+
solutionDesignHeader: null,
|
|
1799
|
+
solutionDesignNodes: [],
|
|
1800
|
+
solutionDesignEdges: [],
|
|
1801
|
+
skillInstanceId: null
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
/** A recommendations payload is content cards iff its entries carry `targetSkill`. */
|
|
1805
|
+
function isContentRecommendations(recommendations) {
|
|
1806
|
+
return recommendations.length > 0 && "targetSkill" in recommendations[0];
|
|
1807
|
+
}
|
|
1808
|
+
/**
|
|
1809
|
+
* Fold one `skill` stream event into the turn state. Pure and immutable — the
|
|
1810
|
+
* returned object is a new reference whenever anything changed, so the result
|
|
1811
|
+
* can back React state directly:
|
|
1812
|
+
*
|
|
1813
|
+
* ```ts
|
|
1814
|
+
* let turn = createSkillTurn();
|
|
1815
|
+
* for await (const event of client.ask(message)) {
|
|
1816
|
+
* if (event.type === "skill") turn = reduceSkillEvent(turn, event);
|
|
1817
|
+
* }
|
|
1818
|
+
* ```
|
|
1819
|
+
*/
|
|
1820
|
+
function reduceSkillEvent(turn, event) {
|
|
1821
|
+
switch (event.name) {
|
|
1822
|
+
case GuideEvent.SkillStart: return {
|
|
1823
|
+
...event.value.skill === SkillResponseType.BusinessCase && turn.skill === SkillResponseType.BusinessCase ? {
|
|
1824
|
+
...createSkillTurn(),
|
|
1825
|
+
businessCaseFormats: turn.businessCaseFormats,
|
|
1826
|
+
businessCaseDrafts: turn.businessCaseDrafts,
|
|
1827
|
+
businessCaseContent: turn.businessCaseContent
|
|
1828
|
+
} : createSkillTurn(),
|
|
1829
|
+
skill: event.value.skill,
|
|
1830
|
+
messageId: event.value.messageId,
|
|
1831
|
+
diagnoseSubtype: event.value.diagnoseSubtype ?? null,
|
|
1832
|
+
followUpSkill: event.value.followUpSkill ?? null
|
|
1833
|
+
};
|
|
1834
|
+
case GuideEvent.SkillContent: return {
|
|
1835
|
+
...turn,
|
|
1836
|
+
content: turn.content + event.value.delta
|
|
1837
|
+
};
|
|
1838
|
+
case GuideEvent.SkillDocuments: return {
|
|
1839
|
+
...turn,
|
|
1840
|
+
documents: event.value.documents
|
|
1841
|
+
};
|
|
1842
|
+
case GuideEvent.SkillRecommendations: return isContentRecommendations(event.value.recommendations) ? {
|
|
1843
|
+
...turn,
|
|
1844
|
+
contentRecommendations: event.value.recommendations
|
|
1845
|
+
} : {
|
|
1846
|
+
...turn,
|
|
1847
|
+
followUpRecommendations: event.value.recommendations
|
|
1848
|
+
};
|
|
1849
|
+
case GuideEvent.SkillCTAs: return {
|
|
1850
|
+
...turn,
|
|
1851
|
+
ctas: event.value.ctas
|
|
1852
|
+
};
|
|
1853
|
+
case GuideEvent.SkillDiagnoseHeader: return {
|
|
1854
|
+
...turn,
|
|
1855
|
+
diagnoseHeader: event.value.header
|
|
1856
|
+
};
|
|
1857
|
+
case GuideEvent.SkillDiagnoseQuestion: return {
|
|
1858
|
+
...turn,
|
|
1859
|
+
diagnoseQuestions: [...turn.diagnoseQuestions, event.value.question]
|
|
1860
|
+
};
|
|
1861
|
+
case GuideEvent.SkillPlaylistHeader: return {
|
|
1862
|
+
...turn,
|
|
1863
|
+
playlistHeader: event.value.header
|
|
1864
|
+
};
|
|
1865
|
+
case GuideEvent.SkillPlaylistItems: return {
|
|
1866
|
+
...turn,
|
|
1867
|
+
playlistItems: event.value.items
|
|
1868
|
+
};
|
|
1869
|
+
case GuideEvent.SkillPlaylistRationale: return {
|
|
1870
|
+
...turn,
|
|
1871
|
+
playlistItems: turn.playlistItems.map((item) => item.id === event.value.itemId ? {
|
|
1872
|
+
...item,
|
|
1873
|
+
rationale: event.value.rationale
|
|
1874
|
+
} : item)
|
|
1875
|
+
};
|
|
1876
|
+
case GuideEvent.SkillHowToOutline: return {
|
|
1877
|
+
...turn,
|
|
1878
|
+
howToOutline: event.value.outline,
|
|
1879
|
+
skillInstanceId: event.value.skillInstanceId ?? turn.skillInstanceId
|
|
1880
|
+
};
|
|
1881
|
+
case GuideEvent.SkillHowToSteps: return {
|
|
1882
|
+
...turn,
|
|
1883
|
+
howToSteps: event.value.steps
|
|
1884
|
+
};
|
|
1885
|
+
case GuideEvent.SkillBusinessCaseChooser: return {
|
|
1886
|
+
...turn,
|
|
1887
|
+
businessCaseFormats: event.value.formats,
|
|
1888
|
+
businessCaseDrafts: event.value.drafts
|
|
1889
|
+
};
|
|
1890
|
+
case GuideEvent.SkillBusinessCaseDelta: {
|
|
1891
|
+
const existing = turn.businessCaseContent[event.value.subType] ?? "";
|
|
1892
|
+
return {
|
|
1893
|
+
...turn,
|
|
1894
|
+
businessCaseContent: {
|
|
1895
|
+
...turn.businessCaseContent,
|
|
1896
|
+
[event.value.subType]: existing + event.value.delta
|
|
1897
|
+
}
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1900
|
+
case GuideEvent.SkillSolutionDesignHeader: return {
|
|
1901
|
+
...turn,
|
|
1902
|
+
solutionDesignHeader: event.value.header
|
|
1903
|
+
};
|
|
1904
|
+
case GuideEvent.SkillSolutionDesignDiagram: return {
|
|
1905
|
+
...turn,
|
|
1906
|
+
solutionDesignNodes: event.value.nodes,
|
|
1907
|
+
solutionDesignEdges: event.value.edges
|
|
1908
|
+
};
|
|
1909
|
+
case GuideEvent.SkillEnd: return {
|
|
1910
|
+
...turn,
|
|
1911
|
+
status: "complete",
|
|
1912
|
+
skillInstanceId: event.value.skillInstanceId ?? event.value.skillResponseId ?? turn.skillInstanceId
|
|
1913
|
+
};
|
|
1914
|
+
default: return event;
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
//#endregion
|
|
1725
1918
|
//#region src/react/useGuide.ts
|
|
1726
1919
|
/**
|
|
1727
1920
|
* React binding over {@link initGuide}. Bootstraps once per `baseUrl`+`slug`,
|
|
1728
|
-
* mirrors the streamed reply into
|
|
1921
|
+
* mirrors the streamed reply into `messages`, folds structured skill events
|
|
1922
|
+
* into `skillTurn`, surfaces email gates, and ends the session on unmount —
|
|
1729
1923
|
* encapsulating the streaming/cleanup patterns so generated UIs stay correct.
|
|
1730
1924
|
*/
|
|
1731
1925
|
function useGuide(config) {
|
|
1732
1926
|
const [client, setClient] = (0, react.useState)(null);
|
|
1733
1927
|
const [messages, setMessages] = (0, react.useState)([]);
|
|
1928
|
+
const [skillTurn, setSkillTurn] = (0, react.useState)(null);
|
|
1929
|
+
const [gate, setGate] = (0, react.useState)(null);
|
|
1734
1930
|
const [isStreaming, setIsStreaming] = (0, react.useState)(false);
|
|
1735
1931
|
const [error, setError] = (0, react.useState)(null);
|
|
1736
1932
|
const { baseUrl, slug } = config;
|
|
1737
1933
|
const configRef = (0, react.useRef)(config);
|
|
1738
1934
|
configRef.current = config;
|
|
1935
|
+
const turnRef = (0, react.useRef)(null);
|
|
1936
|
+
const lastAskRef = (0, react.useRef)(null);
|
|
1739
1937
|
(0, react.useEffect)(() => {
|
|
1740
1938
|
let active = true;
|
|
1741
1939
|
let created = null;
|
|
1940
|
+
setClient(null);
|
|
1941
|
+
setMessages([]);
|
|
1942
|
+
setSkillTurn(null);
|
|
1943
|
+
setGate(null);
|
|
1944
|
+
setError(null);
|
|
1945
|
+
turnRef.current = null;
|
|
1946
|
+
lastAskRef.current = null;
|
|
1742
1947
|
initGuide(configRef.current).then((instance) => {
|
|
1743
1948
|
if (!active) {
|
|
1744
|
-
instance.endSession();
|
|
1949
|
+
instance.endSession().catch(() => {});
|
|
1745
1950
|
return;
|
|
1746
1951
|
}
|
|
1747
1952
|
created = instance;
|
|
@@ -1752,15 +1957,32 @@ function useGuide(config) {
|
|
|
1752
1957
|
});
|
|
1753
1958
|
return () => {
|
|
1754
1959
|
active = false;
|
|
1755
|
-
created?.endSession();
|
|
1960
|
+
created?.endSession().catch(() => {});
|
|
1756
1961
|
};
|
|
1757
1962
|
}, [baseUrl, slug]);
|
|
1758
1963
|
const ask = (0, react.useCallback)(async (message, options) => {
|
|
1759
1964
|
if (!client) return;
|
|
1965
|
+
lastAskRef.current = {
|
|
1966
|
+
message,
|
|
1967
|
+
options
|
|
1968
|
+
};
|
|
1760
1969
|
setIsStreaming(true);
|
|
1761
1970
|
setError(null);
|
|
1971
|
+
setGate(null);
|
|
1762
1972
|
try {
|
|
1763
|
-
for await (const event of client.ask(message, options))
|
|
1973
|
+
for await (const event of client.ask(message, options)) {
|
|
1974
|
+
configRef.current.onEvent?.(event);
|
|
1975
|
+
if (event.type === "text" || event.type === "message-start") setMessages([...client.messages]);
|
|
1976
|
+
else if (event.type === "skill") {
|
|
1977
|
+
turnRef.current = reduceSkillEvent(turnRef.current ?? createSkillTurn(), event);
|
|
1978
|
+
setSkillTurn(turnRef.current);
|
|
1979
|
+
} else if (event.type === "gate") setGate({
|
|
1980
|
+
skill: event.skill,
|
|
1981
|
+
prompt: event.prompt,
|
|
1982
|
+
skillInstanceId: event.skillInstanceId
|
|
1983
|
+
});
|
|
1984
|
+
else if (event.type === "error") setError(new Error(event.message));
|
|
1985
|
+
}
|
|
1764
1986
|
setMessages([...client.messages]);
|
|
1765
1987
|
} catch (err) {
|
|
1766
1988
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
@@ -1768,18 +1990,44 @@ function useGuide(config) {
|
|
|
1768
1990
|
setIsStreaming(false);
|
|
1769
1991
|
}
|
|
1770
1992
|
}, [client]);
|
|
1993
|
+
const submitGate = (0, react.useCallback)(async (email, options) => {
|
|
1994
|
+
if (!client) throw new Error("[brand_brain] Guide is not ready yet — wait for isReady before submitting the gate.");
|
|
1995
|
+
const pending = gate;
|
|
1996
|
+
const result = await client.submitGate(email, {
|
|
1997
|
+
...pending && {
|
|
1998
|
+
skill: pending.skill,
|
|
1999
|
+
prompt: pending.prompt,
|
|
2000
|
+
skillInstanceId: pending.skillInstanceId
|
|
2001
|
+
},
|
|
2002
|
+
...options
|
|
2003
|
+
});
|
|
2004
|
+
if (result.success) setGate(null);
|
|
2005
|
+
return result;
|
|
2006
|
+
}, [client, gate]);
|
|
2007
|
+
const retryLastAsk = (0, react.useCallback)(async () => {
|
|
2008
|
+
const last = lastAskRef.current;
|
|
2009
|
+
if (last) await ask(last.message, last.options);
|
|
2010
|
+
}, [ask]);
|
|
1771
2011
|
const reset = (0, react.useCallback)(() => {
|
|
1772
2012
|
client?.reset();
|
|
1773
2013
|
setMessages([]);
|
|
2014
|
+
setSkillTurn(null);
|
|
2015
|
+
setGate(null);
|
|
2016
|
+
turnRef.current = null;
|
|
2017
|
+
lastAskRef.current = null;
|
|
1774
2018
|
}, [client]);
|
|
1775
2019
|
return {
|
|
1776
2020
|
client,
|
|
1777
2021
|
guide: client?.guide ?? null,
|
|
1778
2022
|
messages,
|
|
2023
|
+
skillTurn,
|
|
2024
|
+
gate,
|
|
1779
2025
|
isReady: client !== null,
|
|
1780
2026
|
isStreaming,
|
|
1781
2027
|
error,
|
|
1782
2028
|
ask,
|
|
2029
|
+
submitGate,
|
|
2030
|
+
retryLastAsk,
|
|
1783
2031
|
reset
|
|
1784
2032
|
};
|
|
1785
2033
|
}
|