React Guide
Install
npm install @askable-ui/react @askable-ui/coreQuick start
import { Askable, useAskable } from '@askable-ui/react';
function Dashboard({ data }) {
const { promptContext } = useAskable();
return (
<Askable meta={{ metric: 'revenue', value: data.revenue, period: 'Q3' }}>
<RevenueChart data={data} />
</Askable>
);
}
function ChatInput() {
const { ctx } = useAskable();
async function submit(question: string) {
await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(await ctx.toAgentRequest(question, {
history: 3,
packet: true,
})),
});
}
}For very small integrations you can still inject promptContext directly as a system message:
function ChatInput() {
const { promptContext } = useAskable();
async function submit(question: string) {
await fetch('/api/chat', {
method: 'POST',
body: JSON.stringify({
messages: [
{ role: 'system', content: `UI context: ${promptContext}` },
{ role: 'user', content: question },
],
}),
});
}
// ...
}<Askable>
Renders a wrapper element with data-askable set from the meta prop. Defaults to a div.
// Object meta
<Askable meta={{ widget: 'churn-rate', value: '4.2%' }}>
<ChurnChart />
</Askable>
// String meta
<Askable meta="pricing page hero" as="section">
<HeroSection />
</Askable>
// Forward a ref to the underlying element
const ref = useRef<HTMLDivElement>(null);
<Askable meta={data} ref={ref}>...</Askable>Props:
| Prop | Type | Default | Description |
|---|---|---|---|
meta | Record<string, unknown> | string | — | Metadata attached as data-askable |
as | keyof JSX.IntrinsicElements | "div" | HTML element to render |
ref | Ref<HTMLElement> | — | Forwarded to the underlying element |
| ...rest | — | — | All other props forwarded to the element |
useAskable(options?)
Hook that connects to a shared context for the requested events configuration. Observation starts after mount, consumers with the same events reuse one observer lifecycle, and each shared context stops when its last consumer unmounts.
const { focus, promptContext, ctx } = useAskable();
// Click only
const { focus } = useAskable({ events: ['click'] });
// Inline context options — creates a private context (not the singleton)
const { focus } = useAskable({ maxHistory: 10 });
const { focus } = useAskable({ sanitizeMeta: ({ secret, ...rest }) => rest });
// Scoped to a pre-created context instance
const { focus } = useAskable({ ctx: myCtx });When any AskableContextOptions are provided (maxHistory, sanitizeMeta, sanitizeText, textExtractor), a private context is created for that component instead of sharing the per-events context.
Options:
| Option | Type | Description |
|---|---|---|
events | AskableEvent[] | Which events trigger updates. Defaults to ['click', 'hover', 'focus']. |
maxHistory | number | History buffer size. Defaults to 50. Set to 0 to disable. |
sanitizeMeta | (meta) => meta | Redact sensitive metadata keys before storage. |
sanitizeText | (text) => string | Redact sensitive text content before storage. |
textExtractor | (el) => string | Custom text extraction function. |
ctx | AskableContext | Use a pre-created context (ignores all options above). |
Returns:
| Value | Type | Description |
|---|---|---|
focus | AskableFocus | null | Current focused element data |
promptContext | string | Natural-language context string, ready for LLM injection |
ctx | AskableContext | Underlying context for advanced use |
"Ask AI" button pattern
Use ctx.select() to explicitly set context when a user clicks a button, rather than relying on passive hover or focus:
function MetricCard({ data }) {
const { ctx } = useAskable();
const cardRef = useRef<HTMLDivElement>(null);
return (
<Askable meta={data} ref={cardRef}>
<RevenueChart data={data} />
<button
onClick={() => {
ctx.select(cardRef.current!);
openChatPanel();
}}
>
Ask AI ✦
</button>
</Askable>
);
}See Ask AI Button for a full working example.
Region, circle, and lasso capture
For visual "send this part of the page" flows, use useAskableRegionCapture() with the same context that powers the rest of your React UI.
import { useAskable, useAskableRegionCapture } from '@askable-ui/react';
function RegionTools() {
const { ctx } = useAskable({ viewport: true });
const capture = useAskableRegionCapture({
ctx,
includeViewport: true,
selectionAffordance: {
label: 'Selected context',
prompt: {
placeholder: 'Ask about this area...',
onSubmit(question, packet) {
sendToAgent({ question, context: packet });
},
},
},
theme: {
lassoStrokeWidth: 4,
lassoGlowRadius: 12,
},
onCapture(packet) {
sendToAgent(packet);
},
});
return (
<div>
<button onClick={() => capture.start({ shape: 'region' })}>
Select region
</button>
<button onClick={() => capture.start({ shape: 'square' })}>
Select square
</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>}
</div>
);
}Region and square packets use capture.mode: 'region'; square packets also set target.metadata.shape: 'square'. Circle packets use capture.mode: 'circle' and include center/radius metadata. Lasso packets use capture.mode: 'lasso' and include freehand path points. The default lasso overlay uses the core ASKABLE_REGION_CAPTURE_THEME; pass theme when you need brand-specific overlay colors, line styling, or selected-state defaults. Use selectionAffordance to keep the selected area visible after capture and optionally attach a small prompt input to it.
Text selection capture
For "send this highlighted copy" flows, use useAskableTextSelectionCapture().
import { useAskableTextSelectionCapture } from '@askable-ui/react';
function TextSelectionTools() {
const selection = useAskableTextSelectionCapture({
includeViewport: true,
intent: 'answer using the highlighted text',
selectionAffordance: {
label: 'Selected text',
prompt: {
placeholder: 'Ask about this text...',
onSubmit(question, packet) {
sendToAgent({ question, context: packet });
},
},
},
onCapture(packet) {
sendToAgent(packet);
},
});
return (
<div>
<button onClick={() => selection.start()}>Watch selection</button>
<button onClick={() => selection.captureNow()}>Send selected text</button>
{selection.active && <button onClick={selection.cancel}>Cancel</button>}
</div>
);
}Selection packets use capture.mode: 'text-selection' and include the highlighted text in target.text. Use selectionAffordance to keep the highlight visible after capture and optionally attach a small prompt input to the selected range.
History-aware context
Feed multi-step interaction history into your LLM instead of just the current focus:
function ChatInput() {
const { ctx } = useAskable();
async function submit(question: string) {
// Include last 5 interactions
const historyContext = ctx.toHistoryContext(5);
await fetch('/api/chat', {
method: 'POST',
body: JSON.stringify({
messages: [
{ role: 'system', content: `Recent UI interactions:\n${historyContext}` },
{ role: 'user', content: question },
],
}),
});
}
}Sanitization
Pass sanitizers inline to strip sensitive fields before they're stored or sent to an LLM:
function App() {
const { promptContext } = useAskable({
sanitizeMeta: ({ password, token, ...safe }) => safe,
sanitizeText: (text) => text.replace(/\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g, '[card]'),
});
// ...
}When sanitizeMeta or sanitizeText are provided, a private context is created for that component. For app-wide sanitization shared across all components, create a context manually:
import { createAskableContext } from '@askable-ui/core';
// Create once at module level (or with useMemo)
const safeCtx = createAskableContext({
sanitizeMeta: ({ password, token, ...safe }) => safe,
});
function App() {
const { promptContext } = useAskable({ ctx: safeCtx });
}See Annotating → Sanitization for details.
Next.js / App Router
useAskable() is safe in Server Components tree — it observes the DOM only on the client, inside a useEffect. No special configuration needed.
// app/dashboard/page.tsx — this is a Server Component
import { Dashboard } from './Dashboard'; // Dashboard uses useAskable() internally
export default function Page() {
return <Dashboard />;
}For 'use client' boundaries, just ensure the component using useAskable() is a Client Component:
'use client';
import { useAskable } from '@askable-ui/react';
// ...