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

teko-assistant-ui

v1.1.1

Published

teko-assistant-ui - AI assistant UI SDK for Teko apps

Readme

teko-assistant-ui

A React chat-panel SDK for embedding an AI assistant into apps. The SDK is a pure chat panel — the host app owns the trigger, container, position, and visibility; the panel fills 100% of its parent. It talks to your BFF over Server-Sent Events using the OpenAI Chat Completions streaming format.

Live Demo · NPM · GitLab

Contents: Install · Quick start · Core concepts · Reacting to tool calls · Suggestions · API reference · BFF protocol

How it works

Host App
 └── <AssistantPanel chatBffUrl="..." />
      │  Server-Sent Events (OpenAI-compatible streaming)
      ▼
     BFF → LLM → Business Services

Install

yarn add teko-assistant-ui
# peer deps
yarn add react react-dom

Quick start

You provide the trigger button and the container; the panel fills its parent. The minimum is appId + chatBffUrl:

import { AssistantPanel } from 'teko-assistant-ui';

// Place the panel in any container you like (sidebar, drawer, modal…)
<div style={{ position: 'fixed', right: 0, top: 0, width: 380, height: '100vh' }}>
  <AssistantPanel
    appId="your-app-id"
    chatBffUrl="https://your-bff.example.com/chat"
    // Auth/context go through HTTP headers; the SDK also adds x-session-id + x-user-id
    getRequestHeaders={async () => ({
      'x-authorization': await getAccessToken(),
    })}
  />
</div>;

That gives you a working streaming chat. Everything below is optional — add it as you need it.

Core concepts

The SDK streams assistant replies and renders them. Beyond plain text, the LLM (via your BFF) can emit tool calls — a request for the app to act, shaped as { name, arguments }. The SDK surfaces each tool call to the host through three primitives:

| Primitive | When it fires | What the host does | | ----------------------- | ---------------------------- | ------------------------------------------------------------------------ | | onToolCall(name, args) | every tool call | Primary, generic — react however you want (open a drawer, navigate, apply a change…) | | tools[name] | a message has tool call name | Render a React component inline in the chat (e.g. a product card) | | sendContext(data) | after a user action | Push context back to the AI (bidirectional) — no need to retype |

Tool calls are dispatched eagerly — each fires as soon as it completes mid-stream, not at the end — and multiple tool calls per turn are supported (accumulated by OpenAI index).

To render clickable suggestion chips, add suggestions[name] = { map, behavior } (map tool args → chips; behavior = 'send' | 'prefill' | custom). Optional — see Suggestions.

Reacting to tool calls

onToolCall is the catch-all for everything the LLM asks the app to do. tools renders an inline component for a specific tool call.

<AssistantPanel
  appId="your-app-id"
  chatBffUrl="https://your-bff.example.com/chat"
  getRequestHeaders={async () => ({ 'x-authorization': await getAccessToken() })}
  // Catch-all: react to any tool call from the LLM
  onToolCall={(name, args) => {
    if (name === 'VIEW_CART') openCartDrawer(args.cart_token);
    if (name === 'APPLY_PATCH') applyPatch(args);
  }}
  // Render an inline component for a specific tool call
  tools={{ PRODUCT_DETAIL: ProductDetailCard }}
/>;

An inline tool component can push context back to the AI after the user acts (bidirectional):

import { useAssistantContext } from 'teko-assistant-ui';

function ProductDetailCard({ args }: { args: Record<string, unknown> }) {
  const { sendContext } = useAssistantContext();
  return (
    <button onClick={() => sendContext({ type: 'added_to_cart', sku: args.sku })}>
      Add to cart
    </button>
  );
}

useAssistantContext() returns { sendContext, sendMessage }. The host can also call these via the panel ref — see AssistantPanelRef.

Suggestions

Render clickable suggestion chips under a message. Each suggestions[name] is a { map, behavior } config — map turns a tool call's args into chips; behavior decides what a click does. Render and click logic live together per tool name.

behavior (default 'send') has 3 modes:

  • 'send' — send option.label as a message immediately.
  • 'prefill' — put option.label into the input for the user to edit/send.
  • function — custom: (option, { sendContext, sendMessage, fillInput }) => void.
type Product = { sku: string; name: string; canonical?: string };

<AssistantPanel
  /* …appId, chatBffUrl, getRequestHeaders… */
  suggestions={{
    INTENT_PRODUCT_SEARCH: {
      // args → chips (return void for none); may be async
      map: (args) => {
        const products = args.products as Product[] | undefined;
        return products?.slice(0, 8).map((p) => ({
          key: p.sku,
          label: p.name,
          payload: { sku: p.sku, canonical: p.canonical },
        }));
      },
      // custom click handler
      behavior: (option, { sendContext, sendMessage }) => {
        sendContext(option.payload ?? {});
        sendMessage(option.label);
      },
    },
    QUICK_REPLIES: {
      map: (args) => (args.replies as string[]).map((r) => ({ key: r, label: r })),
      behavior: 'prefill', // drop the text into the input instead of sending
    },
  }}
/>;

SuggestOption is { key: string; label: string; payload?: Record<string, unknown> }.

API reference

<AssistantPanel> props

Required

| Prop | Type | Description | | ------------ | -------- | ---------------- | | appId | string | App identifier | | chatBffUrl | string | BFF endpoint URL |

Tool handling

| Prop | Type | Description | | ------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | onToolCall | (name: string, args: Record<string, unknown>) => void | Catch-all for every tool call (open a drawer, navigate, apply a change…). | | tools | Record<string, ToolUIComponent> | Render a React component inline when a message has a tool call of that name. | | suggestions | Record<string, SuggestionConfig> | Per-tool-name { map, behavior } → suggestion chips. See Suggestions. | | getRequestHeaders | () => Record<string, string> \| Promise<...> | Headers attached to every request. The SDK adds x-session-id + x-user-id; the host supplies auth. |

Request & state

| Prop | Type | Description | | ----------------- | --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | extraBody | Record<string, unknown> | Extra fields merged into the request body next to messages (e.g. { stream: true }, { model: '…' }). Memoize if passed inline. | | conversationId | string | Controlled conversation identity. Pass a stable id per scope (e.g. page-${id}) for per-page sessions. Changing it resets messages; history is restored per id only when persistHistory is on. | | onLoadingChange | (isStreaming: boolean) => void | Fires true when a turn starts streaming, false when it ends — use it to lock/unlock UI (e.g. disable an editor while generating). | | mapError | (error: StreamError) => string \| undefined | Map a structured BFF stream error to display text. Return undefined → falls back to labels.connectionError. Without it, raw BFF error text is not shown (safe default). |

History

| Prop | Type | Default | Description | | ----------------- | --------- | ------- | ------------------------------------------------------------------------------------------------- | | persistHistory | boolean | false | Persist chat history to localStorage. Survives reload; scroll up to load older messages. | | historyPageSize | number | 10 | Messages loaded per page (on mount and when scrolling up). |

UI & lifecycle

| Prop | Type | Default | Description | | --------------------- | -------------------------- | ----------- | --------------------------------------------------------------------------- | | primaryColor | string | '#1a73e8' | Primary color (hex). | | branding | BrandingConfig | — | Logo, background, status-dot color, sender-name toggle. | | locale | 'vi' \| 'en' | 'vi' | UI language. | | labels | Partial<ChatLabels> | — | Override any user-facing string. | | quickActions | QuickAction[] | — | Fixed action buttons above the input. Omit → nothing renders. | | isOpen | boolean | — | Whether the panel is visible — drives unread counting (see below). | | onUnreadCountChange | (count: number) => void | — | Fires when the unread count changes (for a trigger-button badge). | | onClose | () => void | — | Fires when the user clicks the header close (X). Host unmounts/hides. | | onDebugEvent | (e: DebugEvent) => void | — | Dev only — receive request/response/context debug events. |

loadingPhrases is deprecated — the "typing" state now uses labels.typingText.

BrandingConfig

| Field | Type | Default | Description | | ------------------- | --------- | ------- | ---------------------------------------------------------------------------- | | logoUrl | string | — | Logo in the header + typing indicator. Omit → default avatar. | | backgroundColor | string | white | Panel background (message area + input). | | statusColor | string | — | "Online" status-dot color in the header. | | showMessageSender | boolean | true | Show the sender name next to the timestamp under each message. |

When you set branding.statusColor, override agentStatus to text without the (the SDK renders its own colored dot), e.g. 'Online'.

ChatLabels

Every user-facing string is overridable via labels (merged with the built-in vi/en set). Defaults below are the vi locale.

| Field | vi default | Description | | ---------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------ | | agentName | Tư vấn viên AI | Brand name in the header and under AI messages. | | agentStatus | ● Trực tuyến | Status text under the brand name. | | userName | Bạn | Name shown under the user's messages. | | typingText | Đang soạn tin nhắn | Text next to the logo while the AI is typing (SDK appends an animated — don't add ...). | | inputPlaceholder | Nhập tin nhắn... | Input placeholder. | | connectionError | Đã xảy ra lỗi khi kết nối. Vui lòng thử lại. | Shown on a transport/HTTP error (and stream errors with no mapError). | | reasoningRunning | 🤔 Đang suy nghĩ... | Reasoning toggle — running. | | reasoningDone | 💭 Đã suy nghĩ | Reasoning toggle — done. | | reasoningPlaceholder | Đang xử lý... | Reasoning placeholder content. | | close | Đóng | aria-label of the close button. | | emptyState | Xin chào! Tôi có thể giúp bạn. | Shown when there are no messages. | | loadMoreHint | Cuộn lên để xem thêm | Hint when older history exists (persistHistory). | | loadMoreLoading | Đang tải thêm tin nhắn... | Shown while loading more. |

QuickAction

Fixed buttons above the input; each handles its own behavior via onClick (navigate, call chatRef.sendMessage(...), …).

interface QuickAction {
  key: string;
  label: string;
  onClick: () => void;
}

<AssistantPanel
  quickActions={[
    { key: 'category', label: 'Browse', onClick: () => router.push('/c') },
    { key: 'combo', label: 'Combos', onClick: () => chatRef.current?.sendMessage('Suggest a combo') },
  ]}
/>;

Unread badge

isOpen + onUnreadCountChange let the host show a badge on its trigger button. The count resets to 0 when isOpen is true and the tab is visible, or when markAllRead() is called.

const [chatOpen, setChatOpen] = useState(false);
const [unread, setUnread] = useState(0);

<AssistantPanel isOpen={chatOpen} onUnreadCountChange={setUnread} /* … */ />;

{!chatOpen && (
  <button onClick={() => setChatOpen(true)}>
    Chat{unread > 0 && <span>{unread > 9 ? '9+' : unread}</span>}
  </button>
)}

AssistantPanelRef methods

| Method | Signature | Description | | ------------- | ----------------------------------------- | ----------------------------------------------------------------- | | sendMessage | (text: string) => void | Send a message programmatically. | | sendContext | (data: Record<string, unknown>) => void | Queue context — merged into the next request's system message. | | markAllRead | () => void | Mark all messages read — call when the host opens the panel. |

BFF protocol

The SDK speaks Server-Sent Events in the OpenAI Chat Completions streaming format.

  • Request: POST {chatBffUrl} with Accept: text/event-stream. Body is { messages } (OpenAI format); the host's sendContext data is serialized into a system message. Extra body fields come from extraBody.
  • Response: SSE framing data: {...}\n\n, terminated by data: [DONE]\n\n. Each chunk is an OpenAI-compatible delta:
    • choices[].delta.content → text (rendered in real time).
    • choices[].delta.tool_calls[] → tool calls (accumulated by index; multiple supported; each emitted as soon as it completes).
    • choices[].finish_reason or [DONE] → end of turn.
  • Mid-stream error: a chunk shaped { "error": { "code": 429, "message": "…" } } is passed to mapError (the host maps it to text); without mapError, labels.connectionError is shown. HTTP/parse errors also use connectionError.

Contributing

See DEVELOPMENT.md for dev setup, the Playground, project structure, and the release process.