@sales-bot-llm/sdk
v0.2.3
Published
Frontend SDK for the Sales Bot chat API — framework-agnostic core with React, Vue, and vanilla adapters
Readme
@sales-bot-llm/sdk
Frontend SDK for the Sales Bot chat API. A framework-agnostic core (~8 KB) plus thin adapters for React, Vue, a drop-in floating widget, and a plain <script> tag.
Sub-project #2 of 5 in the Sales Bot platform. Consumes the SSE wire format defined by sub-project #1 (sales_bot/).
What you get
| Entry point | Use when |
| --- | --- |
| @sales-bot-llm/sdk/core | Custom UI, Node integration, or any framework not covered below. Returns a SalesBotClient with an async-iterable ask(). |
| @sales-bot-llm/sdk/react | React 18+ apps. useSalesBot() hook returns { ask, messages, isStreaming, error, conversationId, visitorToken, reset }. |
| @sales-bot-llm/sdk/vue | Vue 3.4+ apps. Same shape as React, but state is exposed as Ref<…>. |
| @sales-bot-llm/sdk/widget | Drop-in floating launcher + chat panel rendered into a shadow DOM. createWidget({ embedKey, theme }).mount(). |
| @sales-bot-llm/sdk/vanilla | Plain HTML page. Load dist/vanilla.global.js via <script> → window.SalesBot.default.widget({ … }). |
All entry points talk to the same SalesBotClient and consume the same SSE event stream.
Install
pnpm add @sales-bot-llm/sdk
# or: npm install @sales-bot-llm/sdk / yarn add @sales-bot-llm/sdkReact and Vue are optional peer deps — only install the one(s) you use.
Quickstart
Core (framework-agnostic)
import { SalesBotClient } from '@sales-bot-llm/sdk/core'
const client = new SalesBotClient({
embedKey: 'pk_live_...', // from the Bot config in the admin UI
baseUrl: 'https://api.example.com', // your sales_bot deployment
})
for await (const event of client.ask('What does your trial include?')) {
if (event.event === 'delta') {
process.stdout.write(event.data.content)
} else if (event.event === 'done') {
break
}
}client.ask() is an async iterable over the typed SSE union (turn_started, delta, tool_call_started, tool_call_finished, message_complete, usage, done, error). Each event type maps to a strongly-typed payload — see src/core/types.ts.
The client also offers:
client.identify({ externalId, email, name, traits })— merges into the nextask()call.client.on(eventName, handler)— event-bus subscription, returns an unsubscribe.client.getConversationId()/setConversationId(id | null)— persisted in storage so reloads continue the same conversation.client.loadHistory()— fetch the last 20 messages of the current conversation.client.endConversation()— close the conversation server-side and clear local state.client.getBotConfig()— public bot config (title, greeting, etc.), cached for the client's lifetime.
React
import { useSalesBot } from '@sales-bot-llm/sdk/react'
function Chat() {
const { ask, messages, isStreaming, error } = useSalesBot({
embedKey: 'pk_live_...',
baseUrl: 'https://api.example.com',
})
return (
<>
{messages.map(m => (
<div key={m.id} data-role={m.role}>{m.content}</div>
))}
<button onClick={() => ask('Show me the pricing')} disabled={isStreaming}>
Ask
</button>
{error && <p role="alert">{error.message}</p>}
</>
)
}Vue
<script setup lang="ts">
import { useSalesBot } from '@sales-bot-llm/sdk/vue'
const { ask, messages, isStreaming, error } = useSalesBot({
embedKey: 'pk_live_...',
baseUrl: 'https://api.example.com',
})
</script>
<template>
<div v-for="m in messages" :key="m.id" :data-role="m.role">{{ m.content }}</div>
<button @click="ask('Show me the pricing')" :disabled="isStreaming">Ask</button>
<p v-if="error" role="alert">{{ error.message }}</p>
</template>Floating widget
import { createWidget } from '@sales-bot-llm/sdk/widget'
createWidget({
embedKey: 'pk_live_...',
baseUrl: 'https://api.example.com',
title: 'Ask us anything',
position: 'bottom-right',
theme: {
primary: '#3b82f6',
radius: '16px',
fontFamily: 'Inter, system-ui, sans-serif',
},
}).mount()The widget renders into a shadow root, so it can't be styled (or broken) by host-page CSS. Use the typed theme for common knobs and customCss for anything else. Every theme key maps to a --sb-<kebab-case> CSS variable on the shadow host — see src/core/types.ts for the full list.
Vanilla <script> tag
<script src="https://your-cdn/sales-bot/vanilla.global.js"></script>
<script>
window.SalesBot.default.widget({
embedKey: 'pk_live_...',
baseUrl: 'https://api.example.com',
})
</script>Note the .default.widget indirection — the IIFE bundle exposes the module's exports wrapper, not the module itself. window.SalesBot.default.widget(...) is the recommended path; window.SalesBot.SalesBot.widget(...) also works.
Configuration
All entry points accept the same SalesBotClientOptions:
| Option | Required | Default | Purpose |
| --- | --- | --- | --- |
| embedKey | yes | — | Public bot key, pk_live_…. Created in the admin UI. |
| baseUrl | no | http://localhost:3000 | Sales Bot backend URL. |
| storage | no | localStorage with MemoryStorage fallback | Custom StorageAdapter for the visitor token + conversation id. |
| customHeaders | no | — | Extra headers merged into every request. Useful in Node where the browser-style Origin header must be set manually. |
Widget-only extras (see src/core/types.ts for the full WidgetOptions shape):
container?: HTMLElement— mount target (default:document.body)position?: 'bottom-right' | 'bottom-left'title?,placeholder?theme?: WidgetTheme— typed CSS-variable overrides (colors, sizing, fonts, effects)customCss?: string— raw CSS appended inside the shadow root
Errors
Every error surfaces as a SalesBotError with a typed code you can switch on:
import { SalesBotError } from '@sales-bot-llm/sdk/core'
try {
for await (const ev of client.ask('hi')) { /* … */ }
} catch (e) {
if (e instanceof SalesBotError) {
switch (e.code) {
case 'origin_not_allowed': /* domain not verified for this Bot */
case 'rate_limited': /* back off */
case 'out_of_credits': /* show top-up CTA */
case 'network_error': /* retry if e.retryable */
// …
}
}
}Backend codes mirror the platform error catalog; SDK-only codes are network_error and parse_error.
Origin header — the most common gotcha
The backend validates Origin on every chat request. Browsers send it automatically, but the Bot's Resource must list your origin as a verified domain (e.g. localhost, or your production host). A 403 origin_not_allowed almost always means the Resource isn't configured for the host that's calling the SDK.
In Node (no browser, no automatic Origin), pass customHeaders: { Origin: 'https://yourapp.example' }.
Repo layout
sales_bot_sdk/
├── src/
│ ├── core/ framework-agnostic client, types, transport, SSE parser, storage
│ ├── react/ useSalesBot hook
│ ├── vue/ useSalesBot composable
│ ├── widget/ createWidget — shadow-DOM floating panel
│ └── vanilla/ IIFE entry, exposes window.SalesBot
├── tests/ vitest specs incl. SSE contract, framework adapters, widget
├── example/ Vite + React playground (see example/README.md)
├── tsup.config.ts bundling: ESM + CJS for libs, IIFE for vanilla
├── biome.json lint + format
└── package.json exports map driving the public surfaceDevelopment
Node 20+ and pnpm 10+.
pnpm install
pnpm build # tsup → dist/*.{js,cjs,d.ts} + vanilla.global.js (IIFE)
pnpm dev # tsup --watch
pnpm test # vitest run (unit + SSE-contract + adapter tests)
pnpm test:watch
pnpm test:coverage
pnpm typecheck # tsc --noEmit
pnpm lint # biome check .
pnpm lint:fix
pnpm size # size-limit budgets: core 8KB, react/vue 12KB, widget 25KB, vanilla 20KBThe tests/ tree mirrors the src/ tree, plus a contract/ folder that pins the SSE event taxonomy against the backend's sse-events.contract.spec.ts. If the contract tests fail, do not loosen them — coordinate the change with the backend team. The wire format is the integration boundary.
Run against a local backend
# In ../sales_bot/:
pnpm start:dev # API on :3000
# In sales_bot_sdk/:
pnpm build
pnpm --filter @sales-bot/sdk-example dev # http://localhost:5173See example/README.md for the full set-up, including how to verify a localhost Resource and Bot in the admin UI.
SSE wire contract
The SDK is locked to the backend's SSE event taxonomy. The full list and payload shapes live in src/core/types.ts — search for SalesBotEvent. Any change here is a breaking change for both sides and must be coordinated.
Event names: turn_started, delta, tool_call_started, tool_call_finished, message_complete, usage, done, error.
Bundle outputs
pnpm build produces (in dist/):
| File | Format | Purpose |
| --- | --- | --- |
| core.{js,cjs,d.ts} | ESM / CJS | Framework-agnostic client |
| react.{js,cjs,d.ts} | ESM / CJS | React adapter |
| vue.{js,cjs,d.ts} | ESM / CJS | Vue adapter |
| widget.{js,cjs,d.ts} | ESM / CJS | Floating widget |
| vanilla.global.js | IIFE, minified | <script>-loadable, exposes window.SalesBot |
All ESM/CJS bundles are side-effect-free and tree-shakable.
Publishing
The package is published as @sales-bot-llm/sdk on the public npm registry under access=public.
pnpm build
pnpm test
pnpm size
npm publish # NPM_TOKEN must be set in the environmentIn CI, set NPM_TOKEN as a repo secret; npm picks it up automatically from the .npmrc directive.
License
MIT — see package.json.
