Skip to Content
APIHooksuseAIFocus

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

MethodParametersDescription
setFocus(id, item)id: string, item: FocusItemAdd an item to focus
clearFocus(id)id: stringRemove a specific item from focus by ID
clearAllFocus()NoneRemove all items from focus
getFocus(id)id: stringRetrieve a specific focused item by ID

Reactive State

PropertyTypeDescription
focusedIdsstring[]Array of focused item IDs - re-renders when focus changes
allFocusItemsFocusItem[]Array of all focused items - re-renders when focus changes
hasFocusedItemsbooleanWhether any items are currently focused
focusItemsRecordRecord<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

  1. User Selection: Users actively choose which items to focus on through your UI
  2. Visual Feedback: Focused items are typically shown as chips, badges, or highlighted elements
  3. AI Context: Focused items are automatically included in chat requests to provide relevant context
  4. 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


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 CasePrefer
User explicitly selects an item once and wants a stable snapshotsetFocus (manual)
Item content changes as the user edits and AI should see latestuseAIFocusItem
Need historical reproducibility of the exact original dataManual snapshot
Low‑friction always-fresh focus for an open editoruseAIFocusItem
Performance sensitive; avoid rapid updatesDebounced useAIFocusItem or manual

API

useAIFocusItem( id: string, build: () => Omit<FocusItem, 'id'> | null | undefined, deps?: unknown[] ): void

Parameters

NameTypeDescription
idstringStable focus item identifier
build() => Omit<FocusItem,'id'> | nullFunction returning the latest focus payload; return null to remove focus
depsunknown[]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() returns null / undefinedclearFocus(id) is called.
  • Does NOT automatically clear on unmount (so navigating between list + editor does not implicitly drop focus). Explicitly return null or call clearFocus 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 PatternNew Reactive Equivalent
setFocus(id, item) after each editOne useAIFocusItem with deps
Clear on component unmountReturn null when condition false
Manual re-sync buttonRemove; reactive keeps it fresh

Best Practices

  1. Keep deps minimal; pass only values that influence the built focus payload.
  2. Return null early for cheap pruning if not focused.
  3. Avoid recreating large objects unnecessarily—derive only the fields the model needs.
  4. For auditability, snapshot allFocusItems at send time if you need an immutable record.

Anti-Patterns

Anti-PatternWhyFix
Rebuilding huge payloads every keystrokePerformance churnDebounce or narrow data
Returning new objects when nothing semantically changedNoiseAdd a shallow hash check
Depending on unstable inline functions in depsInfinite updates riskMemoize 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.

Last updated on