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

@xhub-yaah-ai/sdk

v0.1.4

Published

Reusable AI chat SDK with intent-based routing and pluggable actions

Downloads

14

Readme

@teknix/yaah-ai-sdk

Reusable AI chat SDK with intent-based routing and pluggable action handlers.
Designed for the Vietnamese pickleball market — Vietnamese-aware intent patterns, language-agnostic architecture.


Table of Contents


Requirements

| Peer dependency | Version | | --------------- | ------- | | react | ≥ 18 | | react-dom | ≥ 18 | | zustand | ≥ 4 | | @orama/orama | ≥ 2 | | motion | ≥ 11 |


Installation

1. As a workspace package (monorepo / pnpm)

This SDK lives inside the yaah-fe monorepo. Add it to any other package via the workspace protocol:

// package.json of the consumer
{
  "dependencies": {
    "@teknix/yaah-ai-sdk": "workspace:*",
  },
}

Then install:

pnpm install

2. Standalone / published package

# pnpm
pnpm add @teknix/yaah-ai-sdk

# npm
npm install @teknix/yaah-ai-sdk

# yarn
yarn add @teknix/yaah-ai-sdk

Note: You must also install the peer dependencies if they are not already present.

pnpm add react react-dom zustand @orama/orama motion

Quick Start

// app/ai-chat/page.tsx (or any client component)
'use client'

import {
  AiChatProvider,
  ChatShell,
  IntentRegistry,
  type ActionContext,
  type ActionResult,
} from '@teknix/yaah-ai-sdk'

// 1. Build an IntentRegistry and wire up your action handlers
const registry = new IntentRegistry()

registry
  .register(
    'nearby_courts',
    async (_ctx: ActionContext): Promise<ActionResult> => {
      const courts = await fetchNearbyCourts() // your own API call
      return {
        message: `Found ${courts.length} courts near you!`,
        richContent: courts,
        suggestions: ['Book a court', 'View on map'],
      }
    },
    // Optional Orama documents to extend semantic search for this intent
    [{ id: 'nearby-1', intent: 'nearby_courts', tags: ['sân gần đây', 'nearby court'] }]
  )
  .register('reservations', async (_ctx) => {
    const reservations = await fetchMyReservations()
    return {
      message: 'Here are your reservations:',
      richContent: reservations,
      suggestions: ['Cancel booking', 'Book again'],
    }
  })

// 2. Wrap your UI with AiChatProvider
export default function AiChatPage() {
  return (
    <AiChatProvider
      registry={registry}
      welcomeMessage="Xin chào! Tôi có thể giúp gì cho bạn?"
      initialSuggestions={['Tìm sân gần đây', 'Lịch đặt của tôi', 'Hủy đặt sân']}
    >
      <ChatShell
        className="h-full"
        inputPlaceholder="Nhập tin nhắn..."
        renderRichContent={(message) => {
          // Render domain-specific cards based on message.type or richContent
          if (Array.isArray(message.richContent)) {
            return <CourtList courts={message.richContent as Court[]} />
          }
          return null
        }}
      />
    </AiChatProvider>
  )
}

Architecture Overview

┌─────────────────────────────────────────────────┐
│                  Consumer App                   │
│                                                 │
│  ┌───────────────────────────────────────────┐  │
│  │            AiChatProvider                 │  │
│  │  (initializes OramaDB, provides context)  │  │
│  │                                           │  │
│  │  ┌─────────────────────────────────────┐  │  │
│  │  │             ChatShell               │  │  │
│  │  │  ┌──────────┐   ┌───────────────┐  │  │  │
│  │  │  │AiChatList│   │ AiChatInput   │  │  │  │
│  │  │  └──────────┘   └───────────────┘  │  │  │
│  │  └─────────────────────────────────────┘  │  │
│  └───────────────────────────────────────────┘  │
└─────────────────────────────────────────────────┘
         │ uses                   │ uses
         ▼                        ▼
┌─────────────────┐    ┌──────────────────────────┐
│  IntentRegistry │    │       useAiChat           │
│  (handler map)  │    │  (message state machine)  │
└─────────────────┘    └──────────────────────────┘
         │ resolves               │ queries
         ▼                        ▼
┌─────────────────────────────────────────────────┐
│                  Engine Layer                   │
│  ┌───────────────────┐  ┌─────────────────────┐ │
│  │  intent-patterns  │  │   orama-client      │ │
│  │  (regex/rule det) │  │   (semantic search) │ │
│  └───────────────────┘  └─────────────────────┘ │
└─────────────────────────────────────────────────┘

Intent resolution priority (in useAiChat):

  1. Social intent (regex, no registry needed)
  2. Nearby courts (detectNearbyIntent)
  3. Location search (extractLocationSearch)
  4. Match history (detectMatchHistoryIntent)
  5. Cancel reservation (detectCancelIntent)
  6. Reservations history (detectReservationsIntent)
  7. Orama semantic search → registry dispatch
  8. Fallback 'default' handler → generic reply

API Reference

IntentRegistry

Maps intent strings to async ActionHandler functions. Optionally enriches the Orama search index with custom documents per intent.

import { IntentRegistry } from '@teknix/yaah-ai-sdk'

const registry = new IntentRegistry()

Methods

| Method | Signature | Description | | -------------- | ------------------------------------------------------------------------------- | --------------------------------------------- | | register | (intent: string, handler: ActionHandler, documents?: OramaDocument[]) => this | Register an intent → handler pair. Chainable. | | resolve | (intent: string) => ActionHandler \| undefined | Look up the handler for a given intent. | | getDocuments | () => OramaDocument[] | Returns all registered Orama documents. | | getIntents | () => string[] | Returns all registered intent keys. |

Built-in intent keys

| Key | Trigger | | ----------------- | ---------------------------------------- | | nearby_courts | "gần đây", "nearby", "sân gần", … | | location_search | "tìm sân ở Quận 1", "sân tại Thủ Đức", … | | reservations | "lịch sử đặt", "booking history", … | | match_history | "lịch sử đấu", "match history", … | | cancel | "hủy", "cancel", "bỏ đặt", … | | default | Fallback when no other intent matches |

You may register any additional intent key to extend the SDK behavior.


AiChatProvider

React context provider. Initializes the Orama vector DB from the registry documents and makes the context available to all child components.

import { AiChatProvider } from '@teknix/yaah-ai-sdk'

;<AiChatProvider
  registry={registry}
  welcomeMessage="Hello! How can I help you?"
  initialSuggestions={['Search', 'Find nearby', 'View schedule']}
>
  {children}
</AiChatProvider>

Props — AiChatProviderProps

| Prop | Type | Default | Description | | -------------------- | --------------------------------------------- | ------------------------------------ | ------------------------------ | | registry | IntentRegistry | required | The registry instance. | | createDb | (docs: OramaDocument[]) => Promise<OramaDB> | built-in factory | Override the Orama DB factory. | | welcomeMessage | string | 'Hello! How can I help you today?' | Initial AI greeting. | | initialSuggestions | string[] | DEFAULT_SUGGESTIONS_MAP['help'] | Initial chip suggestions. | | children | ReactNode | required | — |

useAiChatContext()

Access the raw context value (db, registry, welcomeMessage, initialSuggestions). Must be called inside <AiChatProvider>.

import { useAiChatContext } from '@teknix/yaah-ai-sdk'

const { db, registry } = useAiChatContext()

ChatShell

A ready-to-use, fully wired chat UI: AiChatList + AiChatInput. Must be rendered inside <AiChatProvider>.

import { ChatShell } from '@teknix/yaah-ai-sdk'

;<ChatShell
  className="h-screen"
  inputPlaceholder="Type a message…"
  renderRichContent={(message) => <MyCard data={message.richContent} />}
/>

Props — ChatShellProps

| Prop | Type | Description | | --------------------- | --------------------------------- | --------------------------------------------------------------------------------- | | className | string | Additional CSS class on the root <div>. | | style | React.CSSProperties | Inline styles on the root <div>. | | renderRichContent | (message: Message) => ReactNode | Render domain-specific cards for messages that carry richContent. | | inputPlaceholder | string | Placeholder text for the default input field. Ignored when renderFooter is set. | | renderFooter | RenderFooter | Replace the default footer (input + chips) with custom UI. | | renderHeader | RenderHeader | Render a custom header above the message list. | | renderMessageBubble | RenderMessageBubble | Replace the default message bubble + action list with custom UI. |


AiChatList

Renders the conversation message list with an optional typing indicator. Can be used standalone if you prefer to manage state manually.

import { AiChatList } from '@teknix/yaah-ai-sdk'

;<AiChatList
  messages={messages}
  isReplying={isReplying}
  onAction={sendMessage}
  renderRichContent={renderRichContent}
  renderMessageBubble={renderMessageBubble}
/>

AiChatInput

The text input bar with suggestion chips and debounced typing suggestions.

import { AiChatInput } from '@teknix/yaah-ai-sdk'

;<AiChatInput
  value={inputValue}
  onChange={setInputValue}
  onSend={sendMessage}
  suggestions={suggestions}
  typingSuggestions={typingSuggestions}
  placeholder="Nhập tin nhắn…"
  disabled={isReplying}
/>

AiChatMessage

Renders a single chat bubble. Used internally by AiChatList.

import { AiChatMessage } from '@teknix/yaah-ai-sdk'

;<AiChatMessage
  message={message}
  onAction={sendMessage}
  renderRichContent={renderRichContent}
  renderMessageBubble={renderMessageBubble}
/>

useAiChat

Core hook that manages the full message state machine. Use this directly when you want custom UI layout instead of ChatShell.

import { useAiChat } from '@teknix/yaah-ai-sdk'
import { useAiChatContext } from '@teknix/yaah-ai-sdk'

function MyCustomChat() {
  const { db, registry, welcomeMessage, initialSuggestions } = useAiChatContext()

  const {
    messages,
    suggestions,
    typingSuggestions,
    inputValue,
    setInputValue,
    isLoading,
    isReplying,
    sendMessage,
    addAiMessage, // Programmatically inject an AI message
    addUserMessage, // Programmatically inject a user message
  } = useAiChat({ registry, db, welcomeMessage, initialSuggestions })
}

Options — UseAiChatOptions

| Option | Type | Description | | -------------------- | ----------------- | ---------------------------------------------------- | | registry | IntentRegistry | required | | db | OramaDB \| null | required — the Orama DB instance (from context). | | welcomeMessage | string | Override the welcome message. | | initialSuggestions | string[] | Override the initial chip suggestions. |


useAiChatStore

Zustand store that backs useAiChat. Useful for reading/mutating chat state from outside the component tree (e.g. resetting the chat session on logout).

import { useAiChatStore } from '@teknix/yaah-ai-sdk'

// Reset conversation
useAiChatStore.getState().reset()

State shape

| Field | Type | Description | | ------------------- | ----------- | -------------------------------------------------------- | | messages | Message[] | Full message history. | | suggestions | string[] | Current chip suggestions. | | typingSuggestions | string[] | Debounced suggestions while user types. | | inputValue | string | Current input field value. | | isReplying | boolean | true while AI is processing. | | dbInitialized | boolean | true after Orama DB is ready and welcome message sent. |


Engine — Intent Patterns

Low-level regex detectors. Import directly if you need them outside the SDK's hook.

import {
  detectSocialIntent,
  detectNearbyIntent,
  detectReservationsIntent,
  detectMatchHistoryIntent,
  detectCancelIntent,
  detectNonCoreIntent,
  detectMultiIntent,
  extractLocationSearch,
  extractTime,
  extractDate,
  extractCourtType,
} from '@teknix/yaah-ai-sdk'

| Export | Returns | Description | | -------------------------------- | -------------------------- | -------------------------------------------------------- | | detectSocialIntent(text) | string \| null | Returns a social reply string if matched, else null. | | detectNearbyIntent(text) | boolean | Matches "gần đây", "nearby", etc. | | detectReservationsIntent(text) | boolean | Matches reservation history queries. | | detectMatchHistoryIntent(text) | boolean | Matches match/game history queries. | | detectCancelIntent(text) | boolean | Matches cancel/delete booking queries. | | detectNonCoreIntent(text) | NonCoreIntentKey \| null | Detects off-topic intents (abuse, complaint, refund, …). | | detectMultiIntent(text) | string[] \| null | Returns [intent1, intent2] for compound queries. | | extractLocationSearch(text) | string \| null | Extracts a location string (e.g. "Quận 1"). | | extractTime(text) | string \| undefined | Extracts a time string (e.g. "18h", "6:30"). | | extractDate(text) | string \| undefined | Extracts a date string (e.g. "ngày 5", "mai"). | | extractCourtType(text) | string \| undefined | Extracts a court type (pickleball, tennis, …). |


Engine — Orama Client

import { createOramaDB, searchWithIntent } from '@teknix/yaah-ai-sdk'

// Initialize the DB (AiChatProvider does this automatically)
const db = await createOramaDB(registry.getDocuments())

// Search
const result = await searchWithIntent(db, 'tìm sân gần Quận 3')
// result: { query, intent: IntentData, suggestions: string[] }

Types

import type {
  Message,
  IntentData,
  SearchResult,
  ActionResult,
  ActionContext,
  ActionHandler,
  OramaDocument,
  // Render prop types
  FooterHandlers,
  HeaderHandlers,
  MessageBubbleHandlers,
  RenderFooter,
  RenderHeader,
  RenderMessageBubble,
} from '@teknix/yaah-ai-sdk'

Message

interface Message {
  id: string
  text: string
  sender: 'user' | 'ai'
  timestamp: Date
  type?: string
  richContent?: unknown // opaque — rendered by renderRichContent
  actions?: string[] // clickable action chips inside the bubble
}

ActionHandler

type ActionHandler = (ctx: ActionContext) => Promise<ActionResult>

interface ActionContext {
  query: string
  intent: IntentData
}

interface ActionResult {
  message: string // AI reply text
  richContent?: unknown // passed through to renderRichContent
  suggestions: string[] // chip suggestions shown after the reply
  actions?: string[] // action chips inside the message bubble
}

OramaDocument

interface OramaDocument {
  id: string
  intent: string
  tags: string[] // Vietnamese + English search tags
}

Constants

import {
  DEFAULT_WELCOME_MESSAGE,
  DEFAULT_SUGGESTIONS,
  DEFAULT_SUGGESTIONS_MAP,
} from '@teknix/yaah-ai-sdk'

DEFAULT_SUGGESTIONS_MAP keys: social, nearby, reservations, help, booking, search, pricing, cancel, match_history, availability, court_info, default.


Intent Routing Flow

User sends message
        │
        ▼
1. detectSocialIntent?  ──yes──► reply social string (no registry)
        │ no
        ▼
2. detectNearbyIntent?  ──yes──► registry.resolve('nearby_courts')
        │ no
        ▼
3. extractLocationSearch?──yes──► registry.resolve('location_search')
        │ no
        ▼
4. detectMatchHistoryIntent?─yes► registry.resolve('match_history')
        │ no
        ▼
5. detectCancelIntent?  ──yes──► registry.resolve('cancel')
        │ no
        ▼
6. detectReservationsIntent?─yes► registry.resolve('reservations')
        │ no
        ▼
7. searchWithIntent (Orama) ──► registry.resolve(intent.action)
        │ no handler
        ▼
8. registry.resolve('default') or generic fallback reply

Registering Custom Intents

You can register any intent key beyond the built-ins. The SDK will call your handler whenever the Orama semantic search maps a query to that intent.

registry.register(
  'court_pricing',
  async ({ query, intent }) => {
    const prices = await fetchPricing({ location: intent.location })
    return {
      message: `Here are the prices for ${intent.location ?? 'courts near you'}:`,
      richContent: prices,
      suggestions: ['Book a court', 'Find cheaper', 'Compare courts'],
      actions: ['View all prices'],
    }
  },
  [
    {
      id: 'pricing-1',
      intent: 'court_pricing',
      tags: ['giá sân', 'bảng giá', 'pricing', 'how much'],
    },
    { id: 'pricing-2', intent: 'court_pricing', tags: ['rẻ nhất', 'cheapest court', 'tìm sân rẻ'] },
  ]
)

Rendering Rich Content

The richContent field is opaque — the SDK passes it through without touching it. You are responsible for rendering it via the renderRichContent prop on ChatShell or AiChatList.

<ChatShell
  renderRichContent={(message) => {
    // Use message.type or inspect richContent shape
    if (message.type === 'reservations') {
      return <ReservationList items={message.richContent as Reservation[]} />
    }

    if (message.type === 'nearby_courts') {
      return <CourtMap courts={message.richContent as Court[]} />
    }

    return null
  }}
/>

Tip: Set message.type inside your ActionHandler by returning it in richContent or by using the actions array to drive UI state.


Custom UI — Render Props

ChatShell exposes three render props that let you replace any part of the default UI while keeping the SDK's state machine intact.

renderFooter

Replaces the entire footer area (input field + suggestion chips).

import type { RenderFooter } from '@teknix/yaah-ai-sdk'

const myFooter: RenderFooter = (handlers, chips) => (
  <div style={{ padding: '1rem', borderTop: '1px solid #333' }}>
    {/* Suggestion chips */}
    {chips.map((chip) => (
      <button key={chip} onClick={() => handlers.onSend(chip)}>
        {chip}
      </button>
    ))}

    {/* Custom input row */}
    <div>
      <input
        value={handlers.value}
        onChange={(e) => handlers.onChange(e.target.value)}
        disabled={handlers.disabled}
        placeholder="Nhập tin nhắn…"
      />
      <button
        onClick={() => handlers.onSend(handlers.value)}
        disabled={handlers.disabled || !handlers.value.trim()}
      >
        Gửi
      </button>
    </div>
  </div>
)

<ChatShell renderFooter={myFooter} />

FooterHandlers

| Field | Type | Description | | ---------- | ------------------------- | ----------------------------------------------- | | onSend | (text: string) => void | Send a message. Pass the text to send directly. | | value | string | Current input value. | | onChange | (value: string) => void | Update the input value. | | disabled | boolean | true while the AI is replying. |

The second argument chips: string[] is the active suggestion list (typing suggestions take priority over post-reply suggestions).


renderHeader

Renders a custom header above the message list.

import type { RenderHeader } from '@teknix/yaah-ai-sdk'

const myHeader: RenderHeader = (handlers) => (
  <div style={{ padding: '0.75rem 1rem', borderBottom: '1px solid #333' }}>
    <span>Yaah AI</span>
    {handlers.isReplying && <span> · đang trả lời…</span>}
  </div>
)

<ChatShell renderHeader={myHeader} />

HeaderHandlers

| Field | Type | Description | | ------------ | --------- | ------------------------------------------ | | isReplying | boolean | true while the AI is processing a reply. |


renderMessageBubble

Replaces the default bubble + action buttons for every message. Return null to fall back to the default rendering for that message.

import type { RenderMessageBubble } from '@teknix/yaah-ai-sdk'

const myBubble: RenderMessageBubble = (message, handlers) => {
  const isUser = message.sender === 'user'

  return (
    <div className={isUser ? 'bubble-user' : 'bubble-ai'}>
      <p>{message.text}</p>

      {/* Render action buttons from the AI response */}
      {message.actions?.map((action) => (
        <button key={action} onClick={() => handlers.onAction(action)}>
          {action}
        </button>
      ))}
    </div>
  )
}

;<ChatShell renderMessageBubble={myBubble} />

Note: renderRichContent is always rendered below the bubble regardless of whether renderMessageBubble is set.

MessageBubbleHandlers

| Field | Type | Description | | ---------- | -------------------------- | ----------------------------------------------------------------------------------------------------- | | onAction | (action: string) => void | Trigger an action. The action string is whatever was set in ActionResult.actions by your handler. |


Combining all three

<ChatShell
  renderHeader={(h) => <MyHeader isReplying={h.isReplying} />}
  renderFooter={(h, chips) => <MyFooter handlers={h} chips={chips} />}
  renderMessageBubble={(msg, h) => <MyBubble message={msg} onAction={h.onAction} />}
  renderRichContent={(msg) => <MyRichCard data={msg.richContent} />}
/>

Building the SDK

# Watch mode (for monorepo development)
pnpm dev

# One-time build (ESM + CJS + .d.ts)
pnpm build

# Type-check only
pnpm typecheck

Output is emitted to dist/. The package exports both ESM (import) and CJS (require) formats with source maps and declaration maps.


Made with ❤️ by Teknix