JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 28
  • Score
    100M100P100Q78654F
  • License MIT

Map UI events and text to semantic user intents — locally, offline, with zero dependencies.

Package Exports

  • intentmap
  • intentmap/react

Readme

intentmap

Map UI events and text to semantic user intents — locally, offline, zero dependencies.

npm version bundle size license CI Playground

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 intentmap

Playground

Try the live demo before installing:


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() // unsubscribe

im.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 up

im.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:

  1. Each pattern is tokenised and converted into a weighted term-frequency vector
  2. Intent vectors are averaged across all their patterns
  3. On match(), the input is vectorised and cosine similarity is computed against each intent
  4. The highest-scoring intent above threshold wins

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 match checkout unless 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 positives
  • 0.25-0.40: balanced default range for many UIs
  • 0.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: true when 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