npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cboyke/demotools

v4.0.2

Published

Reusable React components for building commercetools demos

Downloads

265

Readme

@cboyke/demotools

Reusable React components and AI-chat scaffolding for building commercetools demos.

Modules

The package exports three subpaths:

| Import path | Contents | |------------------------------------------|------------------------------------------------| | @cboyke/demotools | UI components (JsonViewer, JsonModal) | | @cboyke/demotools/chat | Chat types, ChatActionChips | | @cboyke/demotools/chat/server | Chat agent loop, route factory |

The chat/server entrypoint is server-only — keep it out of 'use client' files so the LLM driver doesn't end up in the browser bundle.

UI components

<JsonViewer data={...} />

A VS Code-styled, searchable, collapsible JSON tree viewer. Useful for inspecting the live shape of any object (carts, orders, customers, etc.) in a demo UI.

Features:

  • Search (Enter / Shift+Enter to navigate matches)
  • Expand all / collapse all
  • Copy raw JSON to clipboard
  • Auto-expand of paths containing matches
  • VS Code Dark+ color palette

<JsonModal data={...} title="Cart JSON" />

A trigger button that opens a fullscreen modal containing a JsonViewer. Drop-in replacement for hand-rolled "show JSON" buttons in demo pages.

| Prop | Default | Notes | |-------------------|------------------|------------------------------------| | data | (required) | Object to render in the viewer. | | title | "JSON" | Header label inside the modal. | | buttonLabel | "JSON" | Label on the trigger button. | | buttonClassName | small slate pill | Override classes on the trigger. |

Chat scaffolding

A vendor-neutral chat assistant engine extracted from b2b-starter and b2c-starter. It owns the boring/load-bearing parts (agent loop, system-reminder injection, address-detection, voice loop, audio routes, presentational components); the demo owns its own tools, system prompt, context, and branding.

What's shared (4.0.x)

| Layer | Module | |---|---| | Agent loop, system-reminder injection | runChatTurn (server) | | Route factory: /api/chat | makeChatRoute | | Route factories: TTS + STT | makeSpeakRoute, makeTranscribeRoute | | Voice mic loop (VAD + auto-submit) | useVoiceLoop | | /api/chat fetch wrapper | postChatTurn | | Action chips | <ChatActionChips> | | Composer (textarea + send) | <ChatComposer> | | Launcher (round button + "Continue chat" pill) | <ChatLauncher> | | Product tiles (with OOS guard, ref-locked Add) | <ChatProductTile>, <ChatProductRow> | | Cart card | <ChatCartSummary> | | Order confirmation card | <ChatOrderConfirmation> | | Shipping address form (with optional email field) | <ChatAddressForm> | | Types: tools, turns, artifacts, addresses | top-level exports |

All components are headless: i18n labels, formatMoney, routing primitives, and hooks (useChat, useCart, etc.) flow in via props. Each demo wraps the library component with a 10-line shim that wires up the local hooks.

What's NOT shared

Per-demo divergence stays per-demo:

  • Tool implementations (search_products, add_to_cart, etc.) — they bind to each demo's commerce backend (B2B as-associate carts vs. B2C anonymous; BU/store pickers vs. payment forms).
  • System prompt — tone, scope, branding.
  • Domain-specific artifact components — B2B's ChatStorePicker / ChatBusinessUnitPicker and B2C's ChatPaymentForm (saved-card picker) are intentionally per-demo because the underlying data shapes diverge.

Held back for v5

These need API design before locking down:

  • ChatProvider/useChat context — generics over UiAction and artifact extras
  • <ChatPanel> — slot-based composition (header brand, voice status bar, scroller, composer)
  • <ChatMessage> artifact router — pluggable artifact renderers so demos register their own (ChatStorePicker, ChatPaymentForm, etc.)

Why share this layer

Roughly 70% of the chat code in our demos was identical: agent loop, voice loop, Markdown rendering, action chips, OOS guard, ref-locked tile button. Sharing eliminates a real bug class — a fix landed in b2b on 2026-05-02 and silently went un-ported to b2c for a day before this package existed. With v4, fixes flow through npm version patch and a single dependency bump.

Wire-up sketch

A new demo's chat surface is roughly: install @cboyke/demotools, write a tools.ts + system-prompt.ts, then 6 component shims (~10 lines each) that pass demo hooks/i18n into the library components.

/api/chat/route.ts (the explicit form — makeChatRoute is also available):

import OpenAI from 'openai';
import { runChatTurn, type ChatComplete } from '@cboyke/demotools/chat/server';
import { NextResponse } from 'next/server';
import { TOOLS } from '@/lib/chat/tool-defs';
import { executeTool, type ToolContext } from '@/lib/chat/tools';
import { buildSystemPrompt } from '@/lib/chat/system-prompt';
import { getSession } from '@/lib/session';

const openai = new OpenAI();
const chatComplete: ChatComplete = async ({ messages, tools, model }) => {
  const r = await openai.chat.completions.create({
    model: model ?? process.env.CHAT_MODEL ?? 'gpt-4o-mini',
    tools,
    messages: messages as never,
  });
  return { finish_reason: r.choices[0].finish_reason, message: r.choices[0].message as never };
};

const toolRegistry = Object.fromEntries(
  TOOL_NAMES.map((name) => [
    name,
    async (args, ctx) => {
      const r = await executeTool(name, args, ctx as ToolContext);
      return {
        toolPayload: r.toolPayload,
        isError: r.isError,
        setCookies: r.setCookies,
        artifacts: { products: r.products, cart: r.cart, order: r.order /* ... */ },
      };
    },
  ]),
);

export async function POST(request: Request) {
  const session = await getSession();
  const body = await request.json();
  const { setCookies, ...rest } = await runChatTurn({
    messages: body.messages,
    uiActions: body.uiActions ?? [],
    recentProducts: body.recentProducts ?? [],
    language: body.language ?? 'en-US',
    ctx: { session /* ... */ },
    systemPrompt: buildSystemPrompt({ session, language: body.language }),
    tools: TOOLS,
    toolRegistry,
    chatComplete,
  });
  const response = NextResponse.json(rest);
  for (const cookie of setCookies) response.headers.append('set-cookie', cookie);
  return response;
}

/api/chat/speak/route.ts + /transcribe/route.ts — 4 lines each:

import { NextResponse } from 'next/server';
import OpenAI from 'openai';
import { makeSpeakRoute } from '@cboyke/demotools/chat/server';

export const POST = makeSpeakRoute({
  openai: new OpenAI() as never,
  NextResponse: NextResponse as never,
});

Component shims — pass hooks + i18n + formatters to the library component. Same shape across all 7 components:

// site/components/chat/ChatActionChips.tsx
'use client';
import { ChatActionChips as LibChatActionChips } from '@cboyke/demotools/chat';
import { useChat } from '@/context/ChatContext';

export function ChatActionChips({ suggestions }) {
  const { sendMessage, isLoading } = useChat();
  return (
    <LibChatActionChips
      suggestions={suggestions}
      onSelect={(query) => void sendMessage(query)}
      disabled={isLoading}
    />
  );
}
// site/components/chat/ChatProductTile.tsx
'use client';
import Image from 'next/image';
import Link from 'next/link';
import { ChatProductTile as LibChatProductTile } from '@cboyke/demotools/chat';
import { useChat } from '@/context/ChatContext';
import { useCart } from '@/context/CartContext';
import { useFormatters } from '@/hooks/useFormatters';
import { useTranslations } from 'next-intl';

export function ChatProductTile({ product }) {
  const t = useTranslations('chat');
  const { formatMoney } = useFormatters();
  const { addItem } = useCart();
  const { pushUiAction, sendMessage } = useChat();

  return (
    <LibChatProductTile
      product={product}
      formatMoney={formatMoney}
      labels={{ /* 8 strings */ }}
      pdpHref={pdpHref}
      onAdd={async (p) => {
        await addItem(p.id, p.variantId, 1);
        pushUiAction({ type: 'added_to_cart', productId: p.id, productName: p.name, quantity: 1 });
        void sendMessage('');
      }}
      ImageComponent={Image}
      LinkComponent={Link}
    />
  );
}

Reference consumers

Both demos use the package end-to-end:

See src/chat/DESIGN.md for the rationale on what's shared vs. demo-specific and the migration plan for the held-back surface.

Installation

npm install @cboyke/demotools

For local development, link from a sibling checkout:

{ "dependencies": { "@cboyke/demotools": "file:../demotools" } }

Tailwind

The UI components ship as compiled JS with Tailwind utility classes embedded as string literals (e.g. bg-black/60, bg-[#1e1e1e], text-[#9cdcfe]). Tailwind only generates CSS for class names it can see during its content scan, so you must tell Tailwind to scan this package's dist/ — otherwise the JSON modal renders unstyled (no backdrop, no syntax colors, content bleeds onto the page underneath).

Tailwind v3 — add the path to content in tailwind.config.js:

// tailwind.config.js
export default {
  content: [
    './index.html',
    './src/**/*.{js,jsx,ts,tsx}',
    './node_modules/@cboyke/demotools/dist/**/*.js', // ← required
  ],
  // ...
};

Tailwind v4 — add a @source line to your CSS:

@import "tailwindcss";
@source "../node_modules/@cboyke/demotools/dist/**/*.js";

After adding the path, restart the dev server (a hot reload of tailwind.config.js isn't always enough — Vite's PostCSS pipeline can hold a stale content set).

Smoke test

Open the JSON modal in your demo. If you see a proper dark overlay with a search bar and VS Code-style syntax colors, you're good. If the JSON tree appears inline over the page with no backdrop, your content scan is missing the dist/ path.

Versioning

  • 3.0.xJsonViewer + JsonModal only.
  • 3.1.x — adds /chat and /chat/server subpaths (agent loop + ChatActionChips + types). Existing imports unchanged.
  • 4.0.x — adds the bulk of the chat surface: useVoiceLoop, postChatTurn, makeSpeakRoute, makeTranscribeRoute, ChatComposer, ChatLauncher, ChatProductRow, ChatProductTile, ChatCartSummary, ChatOrderConfirmation, ChatAddressForm. New components require label props (i18n strings), hence the major bump.
  • 5.0.0 (planned) — ChatProvider / useChat context with generics over UiAction and artifact extras; slot-based <ChatPanel>; pluggable <ChatMessage> artifact router so demos can register their own renderers (ChatStorePicker, ChatPaymentForm, etc.) under known artifact keys.