Skip to content

About

Mindra is an experimental knowledge layer for AI-native applications, exploring how software can use context and intelligence to deliver more adaptive, personalized user experiences.

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

mindra

Your interface knows what your user has already learned.

MIT 5.4 kB gzipped no runtime dependencies no backend CI


Every product ships the same interface to a first-time user and to someone who has used it daily for three years. The usual fix is a scatter of booleans — hasSeenExportTooltip, dismissedOnboarding — one per feature, never cleaned up, with no notion of degree and no notion of struggle.

Mindra replaces them with one primitive: how familiar is this user with this specific element, right now?

const { expertise, friction, suggestion } = useAdaptive('export-btn')

No signup. No API key. No backend. Nothing leaves the browser.

Install

npm install @mindra.dev/react

Quick start

import { AdaptiveProvider, Adaptive } from '@mindra.dev/react'

// 1. Wrap your app once
<AdaptiveProvider appId="my-app">{children}</AdaptiveProvider>

// 2. Let an element speak differently to people who know it
<Adaptive
  id="export"
  novice="Export this project as a PDF"
  expert="Export"
>
  <button>Export</button>
</Adaptive>

That is the whole setup. The runtime watches hovers, clicks, focus, hesitation and abandonment, scores each element, and swaps the copy as familiarity grows.

What you get back

const state = useAdaptive('export-btn')

{
  familiarity: 0.63,          // 0–1, rises asymptotically with use
  friction:    0.21,          // 0–1, from hesitation, abandonment and errors
  confidence:  0.50,          // (1 − friction) × familiarity
  expertise:  'proficient',   // novice | learning | proficient | expert
  suggestion: 'show_shortcut' // show_tutorial | inline_details | show_shortcut | silent
}

Branch on it however you like:

const { expertise } = useAdaptive('delete-item')

const handleDelete = () => {
  if (expertise === 'novice') return openConfirmationModal()
  deleteNow()
  showUndoToast()          // experts get speed, with a way back
}

How the scoring works

Metric Definition
Familiarity 1 − e^(−clicks / λ), λ = 8 by default. Asymptotic, never quite reaches 1.
Hesitation Time between pointer entering the element and activating it, clamped to 8s.
Friction 0.4·hesitation + 0.4·abandonment + 0.2·errorRate
Confidence (1 − friction) × familiarity

Tier thresholds sit at 0.28 / 0.58 / 0.85 familiarity. Tune λ via <AdaptiveProvider config={{ lambda: 12 }}>.

Tier variants

<Adaptive> resolves content in this order: the variant for the user's current tier → AI-generated copy if you enabled it → fallback → the child untouched.

Variants walk down to the nearest one you supplied, so partial specification behaves sensibly — give only novice and expert, and a proficient user keeps the novice copy. The interface does not simplify until you say it may.

It also sets data-adaptive-tier on the element, so you can drive transitions from your own stylesheet instead of having a library write inline styles.

AI is optional

The engine is deterministic and needs no model. If you want copy generated instead of hand-written, bring your own key — it is never proxied through us, because there is no "us" in the request path.

<AdaptiveProvider
  appId="my-app"
  ai={{ provider: 'ollama', endpoint: 'http://localhost:11434', model: 'llama3' }}
/>

Supported: ollama, openai, gemini, window.ai, a custom runner, or server pointed at your own endpoint. Generated copy is cached per element and tier, so a model is consulted once per variation, not once per interaction.

Privacy

  • Collected: interaction timing and counts only — hovers, clicks, focus, hesitation, abandonment, and errors you report. Never keystrokes, never input values, never page content.
  • Stored: localStorage, under mindra_stats_<appId>, capped at 500 elements with coldest-first eviction.
  • Transmitted: nothing. There is no default network call. Data leaves the browser only if you pass your own onSync handler or enable a cloud AI provider.
  • Identifiers derived from the DOM are scrubbed of emails, UUIDs, long hashes and digit runs before storage, so order-1048577 is recorded as order-#.

You are still the data controller for your own site. If your jurisdiction requires consent for non-essential local storage, gate <AdaptiveProvider> behind it.

What this does not do

Being direct about the edges, because they matter more than the features:

  • No cross-device sync, but there is a seam for it. State lives in this browser. Pass userId to keep two people sharing a machine apart, and use initialStats / exportStats() to carry familiarity across devices through storage you already own. Mindra ships no backend and never will — see Carrying state across devices.
  • No backend, dashboard or cohort analytics. Bring your own onSync if you want aggregation.
  • It does not move your UI around. Mindra emits state; you decide what changes. Only elements you explicitly wrap are ever touched.
  • Clicks are not comprehension. A user who clicks the same button thirty times may be an expert or may be stuck. Read friction alongside familiarity.

Carrying state across devices

Familiarity is stored per browser. If you want it to follow a person, you supply the storage — the library gives you both ends and stays out of the middle.

<AdaptiveProvider
  appId="my-app"
  userId={user.id}                          // separate profiles on a shared machine
  initialStats={statsLoadedFromYourApi}     // what you saved last time
  mergeInitialStats                         // combine with this device's history
  onSync={(events) => saveToYourApi(events)}
/>

onPersist fires whenever the state settles, handing you exactly what to save:

<AdaptiveProvider
  appId="my-app"
  userId={user.id}
  initialStats={statsLoadedFromYourApi}
  mergeInitialStats
  onPersist={(stats) => fetch("/api/mindra", {
    method: "PUT",
    body: JSON.stringify(stats),
  })}
/>

On your side that is one row per user with a JSON column, and two endpoints. On ours it is nothing — Mindra still stores no user data anywhere.

Prefer onPersist over onSync for this. onSync hands you raw events, so persisting them means reimplementing the scoring accumulator in your own code; onPersist hands you the state the runtime already computed.

For finer control, useMindraRuntime() returns the runtime itself:

const runtime = useMindraRuntime()
const stats = runtime?.exportStats()   // plain JSON, store it wherever

Two details worth knowing:

  • userId is hashed before it becomes a storage key, so passing an email or an account id does not write that value into localStorage.
  • Writes are coalesced, and hydration does not echo back. A burst of clicks produces one onPersist call, and loading a profile does not immediately write it straight out again.
  • Merging takes the larger counter rather than summing. Hydrating the same history twice cannot inflate a score, so re-hydration is always safe and familiarity never moves backwards.

Without userId, everyone using the browser shares one profile — which is usually wrong on a shared machine.

Packages

Package
@mindra.dev/core Framework-agnostic runtime, scoring, storage
@mindra.dev/react Provider, hooks, <Adaptive>
packages/extension Chrome DevTools panel, declares no permissions

Contributing

npm install
npm run build     # dual ESM/CJS, asserts the "use client" directive survives bundling
npm test          # 57 tests, including assertions that the runtime makes no network calls
npm run typecheck

License

MIT © Sidhanshu Monga

About

Mindra is an experimental knowledge layer for AI-native applications, exploring how software can use context and intelligence to deliver more adaptive, personalized user experiences.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages