npm.io
1.0.3 • Published 3d ago

@eworx-org/ccbot-react

Licence
MIT
Version
1.0.3
Deps
20
Size
167 kB
Vulns
0
Weekly
0

@eworx-org/ccbot-react

A React widget library for the Content Cloud chatbot. Renders a floating, conversational AI chat interface that connects to the Content Cloud chat service.


Contents


Installation

npm install @eworx-org/ccbot-react
# or
yarn add @eworx-org/ccbot-react
# or
pnpm add @eworx-org/ccbot-react
Peer dependencies

React 18 or 19 is required and must be installed in your project:

npm install react react-dom

Quick Start (Minimum Required Props)

The component only requires auth to function.

import { CCbotWidget } from '@eworx-org/ccbot-react';

export default function App() {
  return (
    <CCbotWidget
      auth={{ tokenEndpoint: '/ccbot/api/token' }}
    />
  );
}

Note: config is optional. Provide it only when you want to override widget defaults. The widget mounts inside a Shadow DOM and ships its own styles — there is no separate stylesheet to import.


Props Reference

The component accepts a single CcbotWidgetProps object with the following top-level fields.


apiBaseUrlstring · Optional

Base URL of the Content Cloud chat service the widget talks to. This is integration wiring, so it is a top-level prop (not part of config) and is never sourced from remote config. Use it to point at an internal staging/test service; omit it for the default production URL baked into the package.

storagePrefixstring · Optional · Default 'ccbot'

localStorage namespace for this widget's persisted state. Every key derives from it with a static suffix: <prefix>:messages (conversation), <prefix>:open (open/closed state), <prefix>:config (cached remote config), and <prefix>:anonymous-user-id (stable anonymous identity). Like apiBaseUrl, it is integration wiring: a top-level prop, never sourced from remote config. Set a distinct value if you mount more than one widget on the same origin (e.g. different tenants), so their stored state and cached config don't collide.


configCcbotWidgetConfig · Optional

General configuration for the chat widget's appearance and behaviour. All of these may also be supplied via remote configuration.

Prop Type Default Description
threadWelcomeAvatarLabel string 'CCBot' Deprecated. Use headerTitle instead.
threadWelcomeMessage ReactNode | ComponentType Deprecated. Use welcomeMessage instead.
suggestedQuestionsAllContent string[] [] A list of suggested questions shown when the user is querying across all content.
suggestedQuestionsThisPage string[] [] A list of suggested questions shown when the user is querying within the current page scope.
downloadFilename string | (context) => string 'conversation-YYYY-MM-DD_HH-mm.txt' Controls the filename used when users download a conversation. Pass a string for a fixed filename or a function for full control. .txt is appended automatically if omitted.
pageScope CcbotPageScopeConfig Optional configuration for page-level context. See pageScope below.
defaultOpen boolean false When true, the widget is rendered open on the first visit. See defaultOpen & openBehavior below.
openBehavior 'greet-once' | 'remember' | 'always' 'greet-once' Controls whether the widget auto-opens on subsequent visits. Only takes effect when defaultOpen is true. See defaultOpen & openBehavior below.
theme 'black' | 'white' | 'red' 'black' Visual preset for the header and launcher.
themeColors CcbotThemeColors {} Per-property color overrides on top of the selected theme.
triggerLabel string 'Ask the CCBot' Launcher button label.
headerTitle / headerSubtitle string 'CCBot' / 'AI assistant' Header text in the chat panel.
welcomeTitle string 'Explore the website' Welcome screen heading.
welcomeMessage CcbotWelcomeMessage default message Message shown under the welcome title. Remote config supports strings; React consumers may also pass a React element or component type.
suggestedQuestionsLabel string 'Suggested questions' Heading above suggested questions.
about CcbotAboutConfig built-in sections About-this-assistant dialog content.
Remote configuration (automatic)

When auth is configured, the widget automatically fetches tenant-specific settings from:

GET …/ccbot-config — a sibling of the chat API base (e.g. https://example.com/services/ccbot-config when apiBaseUrl is https://example.com/services/chat)

using the same chat session token as ASK/VOTE. The response is a partial CcbotWidgetConfig JSON object (or {} if nothing is customized). Values from the server are merged on top of the config prop, which in turn overrides built-in defaults.

No extra props or flags are required. Integrators only need apiBaseUrl (or the package default) and auth.

downloadFilename

Controls the filename used by the download conversation action.

By default, downloaded conversations are named:

conversation-YYYY-MM-DD_HH-mm.txt

Pass a string for a fixed filename:

<CCbotWidget
  auth={{ tokenEndpoint: '/ccbot/api/token' }}
  config={{
    downloadFilename: 'support-conversation',
  }}
/>

This downloads as support-conversation.txt.

Pass a function for full control:

<CCbotWidget
  auth={{ tokenEndpoint: '/ccbot/api/token' }}
  config={{
    downloadFilename: ({ timestamp }) => {
      const date = timestamp.toISOString().slice(0, 10);
      return `customer-chat-${date}.txt`;
    },
  }}
/>

Or extend the built-in default filename:

<CCbotWidget
  auth={{ tokenEndpoint: '/ccbot/api/token' }}
  config={{
    downloadFilename: ({ defaultFilename }) => `portal-${defaultFilename}`,
  }}
/>

Filenames are sanitized before download, and .txt is appended automatically when omitted.

defaultOpen & openBehavior

By default the widget renders collapsed to its launcher button. Set defaultOpen: true to render it open on the first visit.

<CCbotWidget
  auth={{ tokenEndpoint: '/ccbot/api/token' }}
  config={{
    defaultOpen: true,
    openBehavior: 'greet-once',
  }}
/>

openBehavior controls what happens on subsequent visits (it has no effect when defaultOpen is false, since nothing ever auto-opens):

Value First visit After the user closes it Subsequent loads
greet-once (default) Open Stays closed Closed
remember Open Restores the last state Restores the last open/closed state the user left it in
always Open Re-opens on next load Always open

The open/closed state is persisted in localStorage under `<storagePrefix>:open` (namespaced and isolated via the top-level storagePrefix prop). It is only written when defaultOpen is enabled. No cookies are used. always persists nothing.

Note: Like all config values, defaultOpen and openBehavior can also be supplied via remote configuration, letting operators control auto-open behavior centrally.

pageScopeCcbotPageScopeConfig · Optional

Controls the ability to narrow the chatbot's knowledge to the content of the current page. When pageId is provided, the widget checks the Content Cloud backend to determine whether that page has been ingested and can be used for page-scoped search.

Prop Type Default Description
pageId string A unique identifier for the current page (e.g. a node ID or slug). Used by the backend to scope results to this page's content.
toggleEnabled boolean When true, a toggle control is shown in the widget allowing the user to switch between querying the current page or all content.
defaultMode 'page' | 'all' 'page' The default scope mode when the widget first opens.

authCcbotAuthConfig · Required

Configures how the widget obtains a Chat Session Token (CST) to authenticate with the Content Cloud chat service. You must provide one of the two options below.

Option A — Token endpoint URL

The widget will call your backend endpoint to retrieve a fresh token automatically.

auth={{ tokenEndpoint: '/ccbot/api/token' }}
Prop Type Description
tokenEndpoint string A URL (relative or absolute) on your backend that returns a Chat Session Token. The widget sends a POST request to this URL and expects a JSON response containing a token string.
Option B — Custom token function

Provide an async function that resolves to a token string. Use this when you need full control over headers, caching, or token refresh logic.

auth={{
  getToken: async () => {
    const res = await fetch('/my/custom/token-endpoint');
    const { token } = await res.json();
    return token;
  }
}}
Prop Type Description
getToken () => Promise<string> An async function that returns a Chat Session Token string.

identityCcbotIdentityConfig · Optional

Optional configuration for associating conversations with a specific user. Useful for personalising responses or tracking usage per user.

Prop Type Description
getUserId () => string A function that returns the current user's unique identifier (e.g. a user ID from your authentication system). Called when a conversation starts.

Example:

identity={{
  getUserId: () => currentUser.id,
}}

themeCcbotTheme · Optional

Controls the widget's color theme. The widget ships built-in light and dark themes; this prop lets your React app switch between them declaratively.

Value Default Description
'light' Light theme.
'dark' Dark theme.
'auto' Follows the user's prefers-color-scheme OS setting and updates live when it changes.

Example — bind to your own theme state:

const [theme, setTheme] = useState<'light' | 'dark' | 'auto'>('auto');

return (
  <CCbotWidget
    auth={{ tokenEndpoint: '/ccbot/api/token' }}
    theme={theme}
  />
);

See Dark mode under Styling for theming details and for the non-React escape hatch.


Full Example

import { CCbotWidget } from '@eworx-org/ccbot-react';

export default function App() {
  return (
    <CCbotWidget
      apiBaseUrl="https://staging.contentcloud.com/services/chat"
      storagePrefix="my_site_ccbot"
      config={{
        threadWelcomeAvatarLabel: 'Ask AI',
        suggestedQuestionsAllContent: [
          'What services do you offer?',
          'How do I get started?',
        ],
        suggestedQuestionsThisPage: [
          'Summarise this page for me',
          'What are the key points here?',
        ],
        pageScope: {
          pageId: '42',
          toggleEnabled: true,
          defaultMode: 'page',
        },
      }}
      auth={{ tokenEndpoint: '/ccbot/api/token' }}
      identity={{ getUserId: () => 'user-123' }}
    />
  );
}

Implementing a Custom Token Endpoint (Non-Drupal Sites)

If your site does not use Drupal (which provides the /ccbot/api/token endpoint out of the box via the CCBot module), you need to implement your own backend endpoint that securely exchanges your Content Cloud API key for a Chat Session Token (CST).

Why a backend endpoint? The API key must never be exposed in client-side code. The token exchange must always happen server-side.


Request Flow

The flow is:

Browser → Your Backend Endpoint → Content Cloud Token Service → Your Backend → Browser
  1. The CCbotWidget (running in the browser) calls your backend endpoint to request a token.
  2. Your backend endpoint calls the Content Cloud Token Service, passing your API key.
  3. The Token Service responds with a short-lived Chat Session Token.
  4. Your backend returns the token to the widget.
  5. The widget uses the token to authenticate chat requests for the duration of the session.

Step 1 — Create a Widget-Facing Endpoint

Create an endpoint on your own backend and pass its URL to the widget:

<CCbotWidget
  auth={{ tokenEndpoint: '/ccbot/api/token' }}
/>

When the widget needs a token, it sends this request to your endpoint:

POST /ccbot/api/token HTTP/1.1
Content-Type: application/json

The widget does not send a request body. Your endpoint should use your existing server-side context, such as the user's session cookie or authentication headers, if you need to decide whether the current visitor is allowed to receive a chat token.

Your endpoint should respond with JSON:

{
  "token": "eyJ..."
}

If the request is not allowed, return an appropriate error status such as 401 or 403. If the token exchange fails, return a 5xx response or another status that matches your application's error handling.


Step 2 — Exchange Your API Key for a Token

From your backend, make an HTTP POST request to:

https://www.contentcloud.com/services/chat-token-service/exchange

The request must include:

  • Method: POST
  • Header: X-Api-Key: YOUR_CONTENT_CLOUD_API_KEY

Your API key is provisioned when you register your site with Content Cloud. Keep it stored securely (e.g. in an environment variable or secrets manager) and never commit it to source control.


Step 3 — Handle the Content Cloud Response

On success, the Token Service returns an HTTP 200 response with a JSON body containing the Chat Session Token:

{
  "token": "eyJ..."
}

Extract the token value from the response. This is the Chat Session Token (CST) you will return to the browser.

If the exchange fails (invalid API key, service unavailable, etc.), the Token Service will return a non-200 status code. Your endpoint should handle these cases gracefully and return an appropriate error response to the client.


Example Endpoint

The exact framework does not matter, but the endpoint should follow this shape:

app.post('/ccbot/api/token', async (req, res) => {
  if (!isAllowedToUseChat(req)) {
    res.status(403).json({ error: 'Not allowed' });
    return;
  }

  const exchangeResponse = await fetch(
    'https://www.contentcloud.com/services/chat-token-service/exchange',
    {
      method: 'POST',
      headers: {
        'X-Api-Key': process.env.CONTENT_CLOUD_API_KEY,
      },
    },
  );

  if (!exchangeResponse.ok) {
    res.status(502).json({ error: 'Failed to fetch chat session token' });
    return;
  }

  const data = await exchangeResponse.json();

  if (!data?.token) {
    res.status(502).json({ error: 'Invalid token response' });
    return;
  }

  res.json({ token: data.token });
});

If your backend framework requires explicit response headers, use Content-Type: application/json.

If you need a different request method, custom request body, custom headers, or a different response shape, use auth.getToken instead of auth.tokenEndpoint. With auth.getToken, you fully control the browser-side fetch and only need to return the token string from that function.


Security Considerations
  • Protect your endpoint. If your chat is intended only for authenticated users, require a valid session or authentication header before issuing a token.
  • Do not cache tokens for too long. Chat Session Tokens are short-lived by design. Issue a fresh token per chat session rather than reusing one across sessions.
  • Store the API key securely. Use environment variables or a secrets manager. Never hard-code it or expose it in client-side bundles.

Styling

The widget is rendered as a floating button fixed to the bottom-right corner of the viewport and opens as a resizable modal. No additional layout changes are required on the host page.

The widget mounts inside a Shadow DOM and bundles its own styles — there is no stylesheet to import. This isolation means the host page's CSS cannot bleed into the widget, and the widget's CSS cannot bleed onto the host.

There are two supported ways to customise the widget's appearance from your application:

  1. CSS custom properties (--ccbot-*) — for theme tokens such as colors, typography, and corner radius.
  2. ::part() selectors — for targeted overrides on specific elements (the launcher button, modal, dialog, etc.).

Both work with plain CSS — no Tailwind, no JavaScript, no special build setup.

The widget renders into a custom element named <ccbot-widget>. All overrides are scoped to that selector.

1. Theme tokens (CSS variables)

Set any of the following variables on the host element (or any ancestor of it — they inherit through the shadow boundary). The widget reads them internally.

ccbot-widget {
  --ccbot-primary: 280 80% 50%;
  --ccbot-primary-foreground: 0 0% 100%;
  --ccbot-radius: 0.25rem;
}
Color value format

Color tokens use the HSL triplet format: three space-separated numbers, without the hsl(...) wrapper, without commas, with % on saturation and lightness:

/* ✓ correct */
--ccbot-primary: 280 80% 50%;

/* ✗ all wrong */
--ccbot-primary: hsl(280 80% 50%);
--ccbot-primary: hsl(280, 80%, 50%);
--ccbot-primary: #7c3aed;
--ccbot-primary: rebeccapurple;

This format is required because the widget uses Tailwind's alpha-shorthand internally (e.g. hover states at reduced opacity). The widget composes the final color as hsl(var(--ccbot-primary) / <alpha>).

Available tokens
Variable Purpose
--ccbot-font-family Primary font-family stack used for all widget text. Monospace elements (code blocks, inline code) continue to use the separate mono stack.
--ccbot-background Widget surface background.
--ccbot-foreground Default text color on the widget surface.
--ccbot-primary Primary action color (launcher button, send button).
--ccbot-primary-foreground Foreground color paired with --ccbot-primary.
--ccbot-secondary Secondary action color.
--ccbot-secondary-foreground Foreground paired with --ccbot-secondary.
--ccbot-muted Muted background (suggested-question cards, etc.).
--ccbot-muted-foreground Foreground paired with --ccbot-muted.
--ccbot-accent Accent background (hover states).
--ccbot-accent-foreground Foreground paired with --ccbot-accent.
--ccbot-popover Modal panel background.
--ccbot-popover-foreground Modal panel text color.
--ccbot-card Card background.
--ccbot-card-foreground Card text color.
--ccbot-destructive Destructive action color.
--ccbot-destructive-foreground Foreground paired with --ccbot-destructive.
--ccbot-border Default border color.
--ccbot-input Input border / surface color.
--ccbot-ring Focus ring color.
--ccbot-radius Base corner radius (length, e.g. 0.5rem).
Dark mode

The widget ships a built-in dark theme. The recommended way to activate it from a React app is the theme prop:

<CCbotWidget
  auth={{ tokenEndpoint: '/ccbot/api/token' }}
  theme="dark"
/>

theme accepts:

Value Behavior
'light' (default) Light theme.
'dark' Dark theme.
'auto' Follows the user's prefers-color-scheme OS setting and updates live when it changes.

You can drive the prop from your own state — for example, a theme toggle in your app:

const [theme, setTheme] = useState<'light' | 'dark' | 'auto'>('auto');

return (
  <CCbotWidget
    auth={{ tokenEndpoint: '/ccbot/api/token' }}
    theme={theme}
  />
);

Under the hood, the prop adds class="dark" to the <ccbot-widget> host element, which switches every --ccbot-* token to its dark-mode value. If you ever need to toggle this manually (e.g. from non-React code), you can still do so directly:

document.querySelector('ccbot-widget').classList.toggle('dark');
2. ::part() selectors

For overrides that go beyond theme tokens — geometry, shadows, custom backgrounds, focus styles — target the widget's named parts. The widget exposes a stable set of part attributes.

ccbot-widget::part(trigger) {
  background: rebeccapurple;
  box-shadow: 0 8px 24px rgba(102, 51, 153, 0.5);
}

ccbot-widget::part(trigger):hover {
  transform: scale(1.15);
}

ccbot-widget::part(anchor) {
  bottom: 2rem;
  left: 2rem;
  right: auto; /* move launcher to bottom-left */
}

ccbot-widget::part(dialog) {
  border: 2px solid black;
  border-radius: 0;
}
Available parts
Part Element
anchor The fixed-position container holding the launcher button.
trigger The launcher button itself (the outer pill that grows to reveal the label).
trigger-badge The badge inside the launcher (the colored teardrop/circle that holds the icon). Style its background, border, and corner radius here.
trigger-icon The bot/close SVG icon rendered inside the badge.
trigger-label The label text that slides out of the launcher on first load, hover, and focus.
modal The popover/dialog wrapper that opens above the trigger.
content Alias on the same element as modal (set together).
dialog The visible chat panel inside the modal.
thread The conversation root.
messages The scrollable messages viewport.
composer The input area at the bottom of the thread.
user-message A message bubble sent by the user.
assistant-message A message bubble sent by the assistant.
button Any button rendered inside the widget.
tooltip The tooltip bubble. Style background/padding/text here. Setting z-index here also propagates to the underlying Radix positioning wrapper.
Styling open vs. closed state

The launcher parts (trigger, trigger-badge, trigger-label) carry a data-state attribute that is "open" while the modal is open and "closed" otherwise. Chain it as an attribute selector to style each state independently — for example, to give the badge a different background once the chat is open:

/* Recolor the badge while the modal is open */
ccbot-widget::part(trigger-badge)[data-state="open"] {
  background: rebeccapurple;
}

/* Restyle the launcher pill background in the open state */
ccbot-widget::part(trigger)[data-state="open"] {
  background: transparent;
}

/* Tweak the slide-out label */
ccbot-widget::part(trigger-label) {
  letter-spacing: 0.02em;
  color: rebeccapurple;
}
Limitations of ::part()

These are CSS spec rules, not widget limitations:

  • You cannot reach inside a part with a descendant combinator: ::part(dialog) .inner is invalid.
  • You cannot combine descendants inside the parens: ::part(anchor trigger) is invalid (use comma-separated rules instead).
  • You can chain pseudo-classes after the part: ::part(trigger):hover, ::part(trigger):focus-visible.
  • You can chain attribute selectors on the part itself: ::part(button)[data-variant="ghost"].

If you need to style something that isn't currently exposed as a part, open an issue — the part list is the public theming API and is the right place to extend.

Example: complete custom theme
/* Brand the widget in your app's global stylesheet */
ccbot-widget {
  --ccbot-font-family: "Inter", system-ui, sans-serif;
  --ccbot-primary: 220 90% 56%;
  --ccbot-primary-foreground: 0 0% 100%;
  --ccbot-radius: 1rem;
}

ccbot-widget::part(trigger) {
  box-shadow: 0 10px 30px hsl(220 90% 56% / 0.4);
}

ccbot-widget::part(dialog) {
  border: 1px solid hsl(220 90% 56% / 0.2);
}

ccbot-widget::part(user-message) {
  font-weight: 500;
}

The font file(s) must be loaded by the host page (via @font-face, Google Fonts <link>, etc.). The variable only supplies the font-family value.