Skip to content

@askable-ui/react

React bindings for askable-ui. Requires React 17+.

Install

bash
npm install @askable-ui/react @askable-ui/core

<Askable>

Renders a wrapper element with data-askable managed reactively from the meta prop.

tsx
import { Askable } from '@askable-ui/react';

<Askable meta={{ metric: 'revenue', delta: '-12%' }}>
  <RevenueChart />
</Askable>

<Askable meta={{ metric: 'revenue' }} scope="analytics">
  <RevenueChart />
</Askable>

<Askable meta={{ metric: 'pipeline' }} events={['hover']}>
  <PipelineChart />
</Askable>

<Askable meta={{ metric: 'revenue' }} events="manual">
  <RevenueCard />
</Askable>

<Askable meta="main navigation" as="nav">
  <NavLinks />
</Askable>

Props:

PropTypeDefaultDescription
metaRecord<string, unknown> | stringValue for data-askable attribute
scopestringOptional category written to data-askable-scope for scoped prompt/history queries
eventsAskableEvent[] | 'manual'Optional per-component activation override. Use a subset like ['hover']/['click'] to narrow passive activation or 'manual' to disable passive activation entirely
askeyof JSX.IntrinsicElements"div"HTML element to render
refRef<HTMLElement>Forwarded to the underlying element
...restAll other props forwarded to the element

Use events on <Askable> when one component should behave differently from the surrounding shared context. Typical patterns are hover-only cards, click-only cards, or fully manual cards that are activated through ctx.select().

tsx
function MixedDashboard() {
  const cardRef = useRef<HTMLDivElement>(null);
  const { ctx } = useAskable();

  return (
    <>
      <Askable meta={{ widget: 'pipeline' }} events={['hover']}>
        <PipelineCard />
      </Askable>

      <Askable meta={{ widget: 'revenue' }} events={['click']}>
        <RevenueCard />
      </Askable>

      <Askable ref={cardRef} meta={{ widget: 'account-summary' }} events="manual">
        <AccountSummary />
        <button onClick={() => cardRef.current && ctx.select(cardRef.current)}>
          Ask AI
        </button>
      </Askable>
    </>
  );
}

AskableInspector(props?)

Declarative React wrapper around createAskableInspector().

tsx
import { AskableInspector } from '@askable-ui/react';

// Match a click-only shared React context
<AskableInspector events={['click']} />

Props:

  • all core AskableInspectorOptions props such as position, highlight, tools, promptOptions, and sourcePreview
  • ctx?: AskableContext — reuse an explicit context
  • name?: string — match a named shared React context
  • events?: AskableEvent[] — match a shared React event configuration
  • viewport?: boolean — match a shared viewport-aware React context

When you are already using useAskable({ events: [...] }), pass the same events (or the same ctx) to <AskableInspector /> so the dev panel tracks the same context instead of silently falling back to the default click/hover/focus observer.

tsx
function DashboardDevTools() {
  useAskable({ events: ['click'] });

  return (
    <>
      {/* app UI */}
      {process.env.NODE_ENV === 'development' && (
        <AskableInspector events={['click']} position="bottom-left" />
      )}
    </>
  );
}

Set sourcePreview to include registered source data in the inspector preview and Copy output:

tsx
<AskableInspector
  ctx={ctx}
  sourcePreview={{ sources: [{ id: 'accounts', mode: 'summary' }] }}
/>

useAskable(options?)

Hook that provides reactive access to a shared AskableContext for the requested events configuration. Observation starts after mount; additional consumers with the same events reuse the existing observer instead of re-observing the document. Differing events configurations get isolated shared contexts, each destroyed when its last consumer unmounts. Pass name to scope that shared lifecycle to a specific UI region (for example table vs chart).

ts
import { useAskable } from '@askable-ui/react';

const { focus, promptContext, ctx } = useAskable();

Options:

OptionTypeDescription
namestringOptional shared context name for region-scoped context reuse
viewportbooleanEnable viewport-aware context tracking for this hook's context
eventsAskableEvent[]Trigger events. Default: ['click', 'hover', 'focus']
ctxAskableContextProvide a custom context instead of the shared singleton

Returns:

ValueTypeDescription
focusAskableFocus | nullCurrent focused element data
promptContextstringNatural-language context string
ctxAskableContextFull context instance — select(), clear(), getHistory(), toHistoryContext(), etc.

Examples:

ts
// Basic usage
const { focus, promptContext, ctx } = useAskable();

// Click-only activation
const { focus: clickFocus } = useAskable({ events: ['click'] });

// Hover-only activation
const { focus: hoverFocus } = useAskable({ events: ['hover'] });

// Focus-only activation
const { focus: focusOnly } = useAskable({ events: ['focus'] });

// Named shared context for one surface
const { focus: chartFocus } = useAskable({
  name: 'chart',
  events: ['click', 'hover'],
});

// Private context options for one hook instance
const privateAskable = useAskable({
  maxHistory: 10,
  sanitizeText: (text) => text.trim(),
  sanitizeMeta: (meta) => {
    const { internalId, ...safe } = meta;
    return safe;
  },
  textExtractor: (el) => el.getAttribute('aria-label') ?? el.textContent ?? '',
});

// Custom context (multiple independent AI surfaces)
import { createAskableContext } from '@askable-ui/core';
const myCtx = createAskableContext();
myCtx.observe(panelEl, { events: ['hover'] });

const { focus: panelFocus } = useAskable({ ctx: myCtx });

useAskableSource(id, source, options?)

Lifecycle-managed registration for app-owned context sources. Use this when the assistant needs data that is not fully rendered in the DOM: paginated tables, virtualized lists, documents, charts, maps, calendars, canvases, file trees, or custom state.

The hook registers the source after mount, keeps the latest resolver functions current across rerenders, and unregisters automatically on unmount.

tsx
import { useEffect } from 'react';
import { useAskableSource } from '@askable-ui/react';

function AccountsContextSource({ table, filters, sort }) {
  const accounts = useAskableSource('accounts', {
    kind: 'collection',
    describe: 'Customer accounts matching the active filters',
    getState: () => ({
      filters,
      sort,
      page: table.getState().pagination.pageIndex + 1,
      pageSize: table.getState().pagination.pageSize,
      totalCount: table.options.meta?.totalCount,
    }),
    resolve: async ({ mode, maxItems }) => {
      if (mode === 'visible') return table.getRowModel().rows.map((row) => row.original);
      return summarizeAccounts({ filters, sort, maxItems });
    },
    sanitize: (source) => ({
      ...source,
      data: redactAccountFields(source.data),
    }),
  });

  async function ask(question: string) {
    const promptContext = await accounts.toPromptContext({
      source: { mode: 'summary', maxItems: 20, timeoutMs: 750 },
      sourceErrorMode: 'include',
    });

    return sendToAgent({ question, promptContext });
  }

  useEffect(() => {
    return table.onStateChange(() => {
      accounts.notifyChanged();
    });
  }, [accounts.notifyChanged, table]);

  return null;
}

Call notifyChanged() when source data changes without a DOM focus change, such as pagination, filters, selected rows, or query-cache updates. Async subscribers created with ctx.subscribeAsync() re-resolve matching sources.

Options:

OptionTypeDescription
enabledbooleanRegister while true. Defaults to true
name / events / viewport / ctxsame as useAskable()Choose the context that owns the source

Returns:

ValueTypeDescription
ctxAskableContextContext instance that owns the source
sourceIdstringTrimmed registered source id
resolve(request?)Promise<AskableResolvedContextSource>Resolve this source directly
toPromptContext(options?)Promise<string>Serialize focus plus this source
notifyChanged()() => voidRe-resolve matching async subscribers after source data changes
unregister()() => voidManually unregister before unmount when needed

Practical option patterns

Shared default hook

tsx
function ChatInput() {
  const { promptContext } = useAskable();
  return <textarea defaultValue={promptContext} />;
}

Private hook instance with sanitization

When you pass context-creation options like maxHistory, sanitizeMeta, sanitizeText, or textExtractor without name or ctx, React creates a private context for that hook instance.

tsx
function SafeSupportPanel() {
  const { promptContext } = useAskable({
    maxHistory: 5,
    sanitizeMeta: ({ internalId, ...safe }) => safe,
    sanitizeText: (text) => text.replace(/\s+/g, ' ').trim(),
    textExtractor: (el) => el.getAttribute('data-askable-text') ?? el.textContent ?? '',
  });

  return <textarea defaultValue={promptContext} />;
}

Pre-created custom context

tsx
import { createAskableContext } from '@askable-ui/core';

function AnalyticsPanel({ panelEl }: { panelEl: HTMLElement }) {
  const ctx = useMemo(() => {
    const next = createAskableContext({ maxHistory: 20 });
    next.observe(panelEl, { events: ['click'] });
    return next;
  }, [panelEl]);

  const { promptContext } = useAskable({ ctx });
  return <textarea defaultValue={promptContext} />;
}

Explicit "Ask AI" button flow with ctx.select()

tsx
function RevenueCard() {
  const cardRef = useRef<HTMLDivElement>(null);
  const { ctx } = useAskable({ events: ['click'] });

  return (
    <Askable ref={cardRef} meta={{ widget: 'revenue-card' }}>
      <RevenueChart />
      <button
        onClick={() => {
          if (cardRef.current) ctx.select(cardRef.current);
          openChat();
        }}
      >
        Ask AI
      </button>
    </Askable>
  );
}

Touch-device note

On touch-like devices, Askable maps hover to tap/click by default. That means useAskable({ events: ['hover'] }) still works on mobile-style environments, but the trigger comes from a tap rather than a pointer hover.

Shared vs custom contexts

useAskable() has three common modes in React:

  • Default shared contextuseAskable() with no ctx or name reuses one shared document observer for the same events + viewport configuration.
  • Named shared contextuseAskable({ name: 'chart' }) reuses a separate shared context for one UI region or surface.
  • Custom contextuseAskable({ ctx }) attaches to an explicitly created AskableContext that you observe/configure yourself.

Use a shared context when multiple components on the same page should agree on the same focus/history. Provide a custom ctx when you need isolation, a custom root element, or different lifecycle control.

tsx
function SharedChatInput() {
  const { promptContext } = useAskable({ events: ['hover'] });
  return <textarea defaultValue={promptContext} />;
}

function PrivatePanel({ panelEl }: { panelEl: HTMLElement }) {
  const ctx = useMemo(() => {
    const next = createAskableContext();
    next.observe(panelEl, { events: ['click'] });
    return next;
  }, [panelEl]);

  const { promptContext } = useAskable({ ctx });
  return <textarea defaultValue={promptContext} />;
}

useAskableRegionCapture(options?)

React hook for explicit region, circle, and lasso capture. It mounts a temporary browser overlay, tracks active state, and stores the last captured Context packet.

tsx
import { useAskable, useAskableRegionCapture } from '@askable-ui/react';

function DashboardCapture() {
  const { ctx } = useAskable({ viewport: true });
  const capture = useAskableRegionCapture({
    ctx,
    includeViewport: true,
    theme: {
      lassoStrokeWidth: 4,
      lassoGlowRadius: 12,
    },
    onCapture(packet) {
      sendToAgent(packet);
    },
  });

  return (
    <>
      <button onClick={() => capture.start({ shape: 'region' })}>
        Select region
      </button>
      <button onClick={() => capture.start({ shape: 'circle' })}>
        Circle area
      </button>
      <button onClick={() => capture.start({ shape: 'lasso' })}>
        Lasso area
      </button>
      {capture.active && <button onClick={capture.cancel}>Cancel</button>}
    </>
  );
}

Options:

OptionTypeDescription
shape'region' | 'circle' | 'lasso'Default shape for start()
minSizenumberMinimum accepted width/height in CSS pixels
oncebooleanRemove the overlay after the first accepted capture
themePartial<AskableRegionCaptureTheme>Override overlay colors, selection fill/stroke, and lasso gradient/glow styling
onCapture(packet, selection) => voidCalled when the user completes a selection
onCancel() => voidCalled when selection is cancelled
ctxAskableContextReuse an existing context
includeViewportbooleanInclude visible annotated elements in the emitted packet
source, intent, privacy, provenancecontext packet optionsPassed through to packet serialization

Returns:

ValueTypeDescription
ctxAskableContextContext used for packet serialization
activebooleanWhether the overlay is currently active
lastPacketWebContextPacket | nullLast captured packet
lastSelectionAskableRegionCaptureSelection | nullLast captured geometry; lasso includes point path metadata
start(overrides?)functionStart capture, optionally overriding shape/intent/etc.
cancel()functionCancel the active overlay
destroy()functionRemove the overlay without firing cancel
isActive()functionRead active state from the live capture handle

For persistent capture tools, pass once: false. The overlay and active state stay on after each accepted capture until the user cancels or the component unmounts.


useAskableTextSelectionCapture(options?)

React hook for highlighted text capture. It can listen to browser selection changes or capture the current selection on demand.

tsx
import { useAskableTextSelectionCapture } from '@askable-ui/react';

function TextSelectionCapture() {
  const selection = useAskableTextSelectionCapture({
    includeViewport: true,
    intent: 'answer using this highlighted text',
    onCapture(packet) {
      sendToAgent(packet);
    },
  });

  return (
    <>
      <button onClick={() => selection.start()}>Watch selection</button>
      <button onClick={() => selection.captureNow()}>Send selected text</button>
      {selection.active && <button onClick={selection.cancel}>Cancel</button>}
    </>
  );
}

Options: root, minLength, debounce, once, dedupe, onCapture, onCancel, ctx, and packet options such as includeViewport, source, intent, privacy, and provenance.

Returns: ctx, active, lastPacket, lastSelection, start(overrides?), captureNow(overrides?), cancel(), destroy(), and isActive().

Released under the MIT License.