Package Exports
- intentmap
- intentmap/react
Readme
intentmap
Map UI events and text to semantic user intents — locally, offline, zero dependencies.
intentmap sits between your event handlers and business logic. You define intents with example phrases — intentmap matches incoming text to them in real-time using local vector similarity. No API keys. No network calls. No cold starts.
user types: "I want to finish my purchase" → { intent: "checkout", confidence: 0.84 }Install
npm install intentmap
# or
pnpm add intentmap
# or
yarn add intentmapPlayground
Try the live demo before installing:
- Open CodeSandbox Playground
- Local demo: run
examples/demo.tsin this repo to try ranked matches and explain mode
Quick start
import { createIntentMap, defineIntent } from 'intentmap'
const im = createIntentMap({
intents: {
checkout: defineIntent([
'buy now',
'proceed to checkout',
'place order',
'complete purchase',
]),
search: defineIntent([
'search for',
'find product',
'look up',
'show me results',
]),
cancel: defineIntent([
'cancel order',
'go back',
'never mind',
'abort',
], { threshold: 0.35 }),
},
defaultThreshold: 0.25,
})
// One-shot matching
const result = im.match('I want to complete my purchase')
// { matched: true, intent: 'checkout', confidence: 0.82, scores: {...} }
// Ranked alternatives + explain mode
const ranked = im.matchTopK('buy something now', { limit: 3, explain: true })
// ranked.alternatives -> top 3 intents
// ranked.explanation -> matchedPattern, keywordHits, topSignals
// Event-driven
im.on('checkout', (result) => console.log('checkout:', result.confidence))
im.on('cancel', (result) => console.log('cancel:', result.confidence))
im.on('*', (result) => console.log('any match:', result.intent))
// Emit manually
im.emit(im.match('never mind'))API
createIntentMap(config)
Creates a new IntentMap instance.
const im = createIntentMap({
intents: Record<string, IntentDefinition>,
defaultThreshold?: number, // default: 0.25
caseSensitive?: boolean, // default: false
debug?: boolean, // default: false
})defineIntent(patterns, options?)
Helper to define an intent with patterns and optional config.
defineIntent(
['buy now', 'add to cart'],
{ threshold: 0.3, meta: { route: '/checkout' } }
)im.match(input, options?)
Synchronously scores input against all intents.
const result: MatchResult = im.match('look up sneakers')
// {
// matched: true,
// intent: 'search',
// confidence: 0.76,
// scores: { search: 0.76, checkout: 0.02, cancel: 0.01 },
// input: 'look up sneakers'
// }Enable explanation metadata when you need to debug or inspect why a result won:
const explained = im.match('buy now', { explain: true })
// {
// intent: 'checkout',
// explanation: {
// matchedPattern: 'buy now',
// keywordHits: ['buy', 'now'],
// topSignals: ['keyword overlap', 'cosine similarity', 'threshold pass']
// }
// }im.matchTopK(input, options?)
Returns the normal top result plus ranked alternatives.
const ranked = im.matchTopK('buy something now', { limit: 3 })
// {
// intent: 'checkout',
// confidence: 0.71,
// alternatives: [
// { intent: 'checkout', confidence: 0.71, threshold: 0.25, matched: true },
// { intent: 'search', confidence: 0.22, threshold: 0.25, matched: false },
// { intent: 'cancel', confidence: 0.08, threshold: 0.35, matched: false }
// ]
// }im.on(intent, handler) → unsubscribe()
Register a handler that fires when intent is matched. Use '*' to catch all results.
const off = im.on('checkout', (result, event) => { ... })
off() // unsubscribeim.off(intent, handler)
Remove a specific handler.
im.emit(result, event?)
Manually trigger handlers for a match result.
im.bind(element, options?) → unbind()
Attach intentmap to a DOM element. Fires matching handlers on every event.
const unbind = im.bind(searchInput, {
on: 'input', // event type(s)
extractor: (e) => e.target.value, // how to extract text
filter: (result) => result.confidence > 0.5, // optional gate
})
unbind() // clean upim.addIntent(name, definition)
Dynamically add an intent after creation.
im.addIntent('navigate', {
patterns: ['go to', 'open page', 'navigate to'],
threshold: 0.3,
})im.removeIntent(name)
Remove an intent and its handlers.
im.train(intent, examples)
Add more example phrases to sharpen matching for an intent.
im.train('checkout', ['ready to pay', 'confirm my order'])train() updates the current IntentMap instance in memory only. It affects future matches immediately, but it does not persist across page reloads, process restarts, or new instances created later.
im.getIntents() → string[]
List all registered intent names.
im.destroy()
Unbind all DOM listeners, clear all handlers and intents. Call on component unmount.
React
import { defineIntent } from 'intentmap'
import { useIntentMap, useIntent } from 'intentmap/react'
function SearchBar() {
const im = useIntentMap({
intents: {
search: defineIntent(['search for', 'find', 'look up']),
checkout: defineIntent(['buy', 'purchase', 'add to cart']),
},
})
useIntent(im, 'search', (result) => console.log('searching!', result.confidence))
useIntent(im, 'checkout', (result) => console.log('checkout!', result.confidence))
return (
<input
onChange={(e) => im.emit(im.match(e.target.value))}
placeholder="Type to search or buy..."
/>
)
}How it works
intentmap uses TF-IDF-inspired vector similarity with bigram tokenisation:
- Each pattern is tokenised and converted into a weighted term-frequency vector
- Intent vectors are averaged across all their patterns
- On
match(), the input is vectorised and cosine similarity is computed against each intent - The highest-scoring intent above
thresholdwins
Everything runs synchronously in ~1ms. No WASM, no model files, no network.
What It Is Good At
intentmap works best when:
- intents are expressed with recognizable keyword overlap
- you can provide 3-10 representative example phrases per intent
- you want fast local matching, not a large semantic model
- you care more about deterministic behavior than "magical" generalization
Examples that usually work well:
"I want to finish my purchase"->checkout"look up red sneakers"->search"cancel my order"->cancel
Where It Needs More Examples
intentmap is not an LLM. It does not deeply infer intent from very indirect language unless you teach that language through examples.
For example:
"wrap this up"may not matchcheckoutunless you add similar checkout phrases"let's go"is likely too vague on its own- domain slang, abbreviations, and product-specific language should usually be added explicitly
If matching feels weak, the first fix is usually to add better patterns, not to increase complexity.
Threshold Tuning
Confidence is a relative score from the matcher, not a calibrated probability. A score of 0.84 means "strong match by this engine", not "84% chance this is objectively correct."
Useful starting ranges:
0.15-0.25: loose, better recall, more false positives0.25-0.40: balanced default range for many UIs0.40+: strict, better precision, fewer accidental matches
Practical advice:
- lower the threshold when users phrase the same intent in many ways
- raise the threshold when different intents share a lot of vocabulary
- use
matchTopK()when you want to inspect close alternatives before committing to a single action - use
explain: truewhen tuning patterns and thresholds
Notes On Training
im.train() appends new example phrases to an existing intent on the current instance.
That means:
- it is in-memory only
- it changes future matches immediately
- it does not write to disk or npm package state
- adding many phrases increases the work done during matching, though the library is still designed for small local intent sets
For production use, treat train() as runtime enrichment for the current session, not as long-term model storage.
Benchmarks
| Input length | Intents | Match time |
|---|---|---|
| short (1–5 words) | 10 | ~0.3ms |
| medium (5–20 words) | 10 | ~0.6ms |
| long (20–50 words) | 20 | ~1.1ms |
Bundle size
| Export | Size (minzipped) |
|---|---|
intentmap (core) |
~3.2 kB |
intentmap/react |
~1.1 kB |
License
MIT