useAIFocus
The useAIFocus
hook allows users to explicitly mark which items (documents, notes, files, etc.) should be prioritized and included as context in AI conversations. Unlike ambient context that’s always present, focus items are user-controlled and explicit.
Syntax
function useAIFocus(): {
// Actions
setFocus: (id: string, item: FocusItem) => void;
clearFocus: (id: string) => void;
clearAllFocus: () => void;
getFocus: (id: string) => FocusItem | undefined;
// Reactive State
focusedIds: string[];
allFocusItems: FocusItem[];
hasFocusedItems: boolean;
focusItemsRecord: Record<string, FocusItem>;
}
Return Value
Actions
Method | Parameters | Description |
---|---|---|
setFocus(id, item) | id: string , item: FocusItem | Add an item to focus |
clearFocus(id) | id: string | Remove a specific item from focus by ID |
clearAllFocus() | None | Remove all items from focus |
getFocus(id) | id: string | Retrieve a specific focused item by ID |
Reactive State
Property | Type | Description |
---|---|---|
focusedIds | string[] | Array of focused item IDs - re-renders when focus changes |
allFocusItems | FocusItem[] | Array of all focused items - re-renders when focus changes |
hasFocusedItems | boolean | Whether any items are currently focused |
focusItemsRecord | Record<string, FocusItem> | Object mapping IDs to focus items |
FocusItem Interface
interface FocusItem {
id: string; // Unique identifier
label?: string; // Display label for UI (chips, badges)
description?: string; // Semantic description to help the AI understand context
data?: Record<string, unknown>; // Structured payload sent to the AI model
}
Examples
Basic Usage
import React from "react";
import { ChatContainer, useAIChat, useAIFocus } from "ai-chat-bootstrap";
function DocumentSelector({ documents }: { documents: Document[] }) {
const { setFocus, clearFocus, focusedIds } = useAIFocus();
const handleDocumentToggle = (doc: Document) => {
if (focusedIds.includes(doc.id)) {
clearFocus(doc.id);
} else {
setFocus(doc.id, {
id: doc.id,
label: doc.title,
description: `Document: ${doc.title}`,
data: {
type: 'document',
title: doc.title,
content: doc.content,
lastModified: doc.lastModified
}
});
}
};
return (
<div className="space-y-2">
{documents.map((doc) => (
<button
key={doc.id}
onClick={() => handleDocumentToggle(doc)}
className={`p-3 rounded-lg border ${
focusedIds.includes(doc.id)
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-gray-300'
}`}
>
{doc.title}
</button>
))}
</div>
);
}
Complete Implementation
export function ChatWithFocus() {
const { allFocusItems } = useAIFocus();
const chat = useAIChat({
api: "/api/chat",
systemPrompt: "You are a helpful assistant with access to the user's focused documents."
});
return (
<div className="grid grid-cols-2 gap-4">
<DocumentSelector documents={myDocuments} />
<ChatContainer
chat={chat}
header={{ title: "AI Assistant", subtitle: `${allFocusItems.length} items in focus` }}
/>
</div>
);
}
Focus Management Actions
function FocusControls() {
const { focusedIds, clearAllFocus, clearFocus, allFocusItems } = useAIFocus();
return (
<div className="space-y-2">
<div className="flex justify-between items-center">
<span>{focusedIds.length} items focused</span>
<button onClick={clearAllFocus}>Clear All</button>
</div>
{allFocusItems.map((item) => (
<div key={item.id} className="flex justify-between items-center">
<span>{item.label || item.id}</span>
<button onClick={() => clearFocus(item.id)}>Remove</button>
</div>
))}
</div>
);
}
Different Item Types
function MultiTypeSelector() {
const { setFocus, clearFocus, focusedIds } = useAIFocus();
const focusDocument = (doc) => {
setFocus(doc.id, {
id: doc.id,
label: doc.title,
description: `Document: ${doc.title}`,
data: {
type: 'document',
title: doc.title,
content: doc.content,
wordCount: doc.content.split(' ').length
}
});
};
const focusImage = (img) => {
setFocus(img.id, {
id: img.id,
label: img.filename,
description: `Image: ${img.filename}`,
data: {
type: 'image',
filename: img.filename,
url: img.url,
alt: img.alt,
dimensions: img.dimensions
}
});
};
const focusNote = (note) => {
setFocus(note.id, {
id: note.id,
label: note.title,
description: `Note: ${note.title}`,
data: {
type: 'note',
title: note.title,
content: note.content,
tags: note.tags,
createdAt: note.createdAt
}
});
};
return <div>...</div>;
}
Backend Integration
Focus items are included in the client-built enrichedSystemPrompt
, so you should not rebuild or concatenate them again server-side:
import { createAzure } from '@ai-sdk/azure';
import { convertToModelMessages, streamText } from 'ai';
const azure = createAzure({
resourceName: process.env.AZURE_RESOURCE_NAME!,
apiKey: process.env.AZURE_API_KEY!,
apiVersion: process.env.AZURE_API_VERSION ?? 'preview',
});
const model = azure(process.env.AZURE_DEPLOYMENT_ID!);
export async function POST(request: Request) {
const { messages, enrichedSystemPrompt, tools } = await request.json();
const result = await streamText({
model,
messages: [
{ role: 'system', content: enrichedSystemPrompt },
...convertToModelMessages(messages),
],
tools,
});
return result.toUIMessageStreamResponse();
}
Note: Avoid concatenating focus items again; duplication wastes tokens and can degrade model performance.
How Focus Works
- User Selection: Users actively choose which items to focus on through your UI
- Visual Feedback: Focused items are typically shown as chips, badges, or highlighted elements
- AI Context: Focused items are automatically included in chat requests to provide relevant context
- Dynamic Management: Users can add/remove items from focus at any time
Best Practices
1. Meaningful Labels and Descriptions
setFocus(doc.id, {
id: doc.id,
label: doc.title, // User-friendly display name
description: `Document: ${doc.title}`, // Context for the AI
data: { ... } // Full structured data
});
2. Include Relevant Data
// Good: Include the data the AI needs
setFocus(note.id, {
id: note.id,
label: note.title,
data: {
type: 'note',
title: note.title,
content: note.content,
tags: note.tags,
createdAt: note.createdAt
}
});
// Less helpful: Minimal data
setFocus(note.id, {
id: note.id,
label: note.title
});
3. Visual Feedback
Always provide clear visual indicators for focused items:
- Chips/badges showing focused items with remove buttons
- Different styling for focused vs unfocused items in lists
- Counter showing number of focused items
4. Avoiding Re-render Loops
Follow the Zustand patterns:
// ✅ Correct - stable dependencies
const setFocus = useAIFocus(state => state.setFocus);
useEffect(() => {
if (shouldFocusDocument) {
setFocus(docId, focusItem);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [docId, shouldFocusDocument]); // Only data dependencies
Common Use Cases
- Document Editor: Users select which documents are relevant to their current editing task
- Note-Taking App: Users focus on specific notes when asking questions or generating content
- File Explorer: Users mark files they’re currently working with for AI assistance
- Research Tool: Users focus on specific sources or references for contextual analysis
TypeScript Types
interface FocusItem {
id: string;
label?: string;
description?: string;
data?: Record<string, unknown>;
}
interface UseAIFocusReturn {
// Actions
setFocus: (id: string, item: FocusItem) => void;
clearFocus: (id: string) => void;
clearAllFocus: () => void;
getFocus: (id: string) => FocusItem | undefined;
// Reactive State
focusedIds: string[];
allFocusItems: FocusItem[];
hasFocusedItems: boolean;
focusItemsRecord: Record<string, FocusItem>;
}
See Also
- Focus Items guide - Complete tutorial with live examples
- useAIContext - For automatic context sharing
- useAIChat - Main chat hook
useAIFocusItem (Reactive Focus)
useAIFocusItem
is a companion hook that provides a reactive way to keep a focus item synchronized with changing data (title/content/etc.) while it is focused. It feels similar to useAIContext
, but remains explicitly user‑curated: the item only stays in focus while you choose to keep it there.
When to Use
Use Case | Prefer |
---|---|
User explicitly selects an item once and wants a stable snapshot | setFocus (manual) |
Item content changes as the user edits and AI should see latest | useAIFocusItem |
Need historical reproducibility of the exact original data | Manual snapshot |
Low‑friction always-fresh focus for an open editor | useAIFocusItem |
Performance sensitive; avoid rapid updates | Debounced useAIFocusItem or manual |
API
useAIFocusItem(
id: string,
build: () => Omit<FocusItem, 'id'> | null | undefined,
deps?: unknown[]
): void
Parameters
Name | Type | Description |
---|---|---|
id | string | Stable focus item identifier |
build | () => Omit<FocusItem,'id'> | null | Function returning the latest focus payload; return null to remove focus |
deps | unknown[] | Dependency array that triggers re-evaluation of build |
Behavior
- Runs on mount and whenever any dependency changes.
- If
build()
returns an object →setFocus(id, {...})
is called. - If
build()
returnsnull
/undefined
→clearFocus(id)
is called. - Does NOT automatically clear on unmount (so navigating between list + editor does not implicitly drop focus). Explicitly return
null
or callclearFocus
when you want to remove it.
Basic Example
import { useAIFocusItem } from 'ai-chat-bootstrap';
function SourceEditor({ source, isFocused }: { source: Source; isFocused: boolean }) {
useAIFocusItem(
source.id,
() => {
if (!isFocused) return null; // remove from focus when toggle off
return {
label: source.title || '(Untitled)',
data: {
type: 'source',
excerpt: source.body.slice(0, 500),
origin: source.origin,
}
};
},
[isFocused, source.title, source.body, source.origin]
);
return <Editor value={source.body} /* ... */ />;
}
Integrating with a Toggle (Checkbox)
function FocusableRow({ source }: { source: Source }) {
const { focusItemsRecord } = useAIFocus();
const [manual, setManual] = React.useState(!!focusItemsRecord[source.id]);
useAIFocusItem(
source.id,
() => (manual ? {
label: source.title || '(Untitled)',
data: { type: 'source', excerpt: source.body.slice(0, 500) }
} : null),
[manual, source.title, source.body]
);
return (
<div className="flex items-center gap-2">
<span className="truncate flex-1">{source.title || '(Untitled)'}</span>
<Checkbox checked={manual} onCheckedChange={c => setManual(!!c)} />
</div>
);
}
Debouncing Updates (Optional)
If the underlying data changes very rapidly (e.g. on each keystroke) you may want to debounce the dependencies upstream:
const debouncedBody = useDebouncedValue(source.body, 250);
useAIFocusItem(
source.id,
() => ({
label: source.title || '(Untitled)',
data: { type: 'source', excerpt: debouncedBody.slice(0, 500) }
}),
[source.title, debouncedBody]
);
Migration Guidance
Old Pattern | New Reactive Equivalent |
---|---|
setFocus(id, item) after each edit | One useAIFocusItem with deps |
Clear on component unmount | Return null when condition false |
Manual re-sync button | Remove; reactive keeps it fresh |
Best Practices
- Keep
deps
minimal; pass only values that influence the built focus payload. - Return
null
early for cheap pruning if not focused. - Avoid recreating large objects unnecessarily—derive only the fields the model needs.
- For auditability, snapshot
allFocusItems
at send time if you need an immutable record.
Anti-Patterns
Anti-Pattern | Why | Fix |
---|---|---|
Rebuilding huge payloads every keystroke | Performance churn | Debounce or narrow data |
Returning new objects when nothing semantically changed | Noise | Add a shallow hash check |
Depending on unstable inline functions in deps | Infinite updates risk | Memoize helpers |
Combining Snapshot + Reactive
You can mix both approaches: use useAIFocusItem
during active editing, then at the moment of “commit” (e.g. sending a chat) copy current focus items into a per-message snapshot for traceability.
const { allFocusItems } = useAIFocus();
sendMessage({ focus: [...allFocusItems] }); // immutable snapshot
Summary
useAIFocusItem
reduces boilerplate and keeps focused data fresh while preserving deliberate user control. Use it when freshness matters; fall back to manual setFocus
for intentional snapshot semantics or strict reproducibility.