@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
- Installation
- Quick Start
- Architecture Overview
- API Reference
- Intent Routing Flow
- Registering Custom Intents
- Rendering Rich Content
- Custom UI — Render Props
- Building the SDK
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 install2. Standalone / published package
# pnpm
pnpm add @teknix/yaah-ai-sdk
# npm
npm install @teknix/yaah-ai-sdk
# yarn
yarn add @teknix/yaah-ai-sdkNote: You must also install the peer dependencies if they are not already present.
pnpm add react react-dom zustand @orama/orama motionQuick 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):
- Social intent (regex, no registry needed)
- Nearby courts (
detectNearbyIntent) - Location search (
extractLocationSearch) - Match history (
detectMatchHistoryIntent) - Cancel reservation (
detectCancelIntent) - Reservations history (
detectReservationsIntent) - Orama semantic search → registry dispatch
- 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 replyRegistering 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.typeinside yourActionHandlerby returning it inrichContentor by using theactionsarray 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:
renderRichContentis always rendered below the bubble regardless of whetherrenderMessageBubbleis 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 typecheckOutput is emitted to dist/. The package exports both ESM (import) and CJS (require) formats with source maps and declaration maps.
Made with ❤️ by Teknix
