AI-Enhanced Dashboard
Add AI capabilities to your dashboard with tools and chat integration, enabling bidirectional communication between the app and AI.
What You'll Build
A dashboard that:
- Registers tools that AI can call to get live app state
- Opens an AI chat with context from the app
- Demonstrates the app bridge architecture (bidirectional communication)
- Uses
__INFORMER__.registerTool()and__INFORMER__.openChat()
Prerequisites
- Completed API Mashup
- Understanding of the previous dashboard patterns
Example Prompts for Claude Code
Add AI chat integration:
"Add an 'Ask AI' button to the dashboard header. When clicked, open an AI chat using
__INFORMER__.openChat(). Pass the current dashboard metrics as context and tell the AI to use the Informer API to analyze trends. Add dev mode mocking so it works locally."
Register a tool:
"Register a tool called 'getContext' using
__INFORMER__.registerTool(). It should return the current filters, all visible metrics, and data load status. The AI can call this tool during the conversation to see the latest dashboard state."
Advanced tool:
"Add a second tool called 'getTopCustomers' that accepts a limit parameter and returns the top N customers by revenue for the current date range. Query the dataset when the tool is called."
Improve the chat experience:
"Update the openChat instructions to tell the AI which datasets and integrations are available, and suggest specific analysis it can perform (trends, anomalies, category breakdown)."
The App Bridge
The app bridge enables bidirectional communication between apps and AI:
- App → AI -
openChat()sends initial context and instructions - AI → App - AI calls registered tools to get fresh data on-demand
This is more powerful than static context because the AI can ask the app for its current state at any point during the conversation.
Activating the Copilot
The copilot button is hidden by default for apps. It activates automatically when tools are registered via registerTool(), so most apps that use the bridge get the button for free. You can also activate it explicitly or paint your own:
// Automatic: registering tools activates the copilot button
__INFORMER__.registerTool({ name: 'getContext', ... }); // button appears
// Explicit: show without registering tools
window.__INFORMER__?.showCopilot();
// Custom: use your own button
document.querySelector('#my-ai-btn').addEventListener('click', () => {
__INFORMER__.openChat({ prompt: 'Analyze the data' });
});
Register Tools
Tools must be registered before openChat() is called. Register them during initialization:
__INFORMER__.registerTool({
name: 'getContext',
description: 'Returns the current dashboard state including active filters, selected data, and summary metrics.',
schema: {
type: 'object',
properties: {},
additionalProperties: false
},
handler: () => {
return {
filters: getCurrentFilters(),
metrics: getSummaryMetrics(),
dataLoaded: getLoadedDataSources()
};
}
});
| Option | Type | Description |
|---|---|---|
name | string | Tool name - exposed to AI as report_<name> |
description | string | What the tool does (AI reads this to decide when to call it) |
schema | object | JSON Schema for input parameters |
handler | function | Called when AI invokes the tool - returns data (or Promise) |
What Claude Will Generate
When you ask Claude to add AI integration, it will implement tool registration, chat opening, and dev mode mocking. Here's what the generated code looks like:
window.informerReady = false;
const theme = window.__INFORMER__?.theme || 'light';
document.documentElement.setAttribute('data-theme', theme);
const ORDERS_DATASET = 'admin:orders';
const PARTNER_INTEGRATION = 'partner-api';
let currentFilters = { dateRange: '30' };
let dashboardData = {
orders: { revenue: 0, count: 0 },
partner: { revenue: 0, count: 0 },
loaded: { orders: false, partner: false }
};
async function init() {
try {
// Register AI tool BEFORE loading data
registerAITools();
// Wire up controls
document.getElementById('refresh').addEventListener('click', loadAllData);
document.getElementById('dateRange').addEventListener('change', (e) => {
currentFilters.dateRange = e.target.value;
});
// Add "Ask AI" button handler
document.getElementById('askAI').addEventListener('click', openAIChat);
// Load data
await loadAllData();
window.informerReady = true;
} catch (err) {
console.error('Initialization failed:', err);
}
}
function registerAITools() {
// Mock in dev mode
if (!window.__INFORMER__?.registerTool) {
window.__INFORMER__ = window.__INFORMER__ || {};
window.__INFORMER__.registerTool = (def) => console.log('registerTool (dev):', def.name);
}
// Register the getContext tool
__INFORMER__.registerTool({
name: 'getContext',
description: 'Get the current dashboard state: active filters, revenue metrics, and data load status.',
schema: {
type: 'object',
properties: {},
additionalProperties: false
},
handler: () => {
return {
filters: {
dateRange: currentFilters.dateRange,
dateRangeLabel: document.getElementById('dateRange').selectedOptions[0].text
},
metrics: {
orders: {
revenue: dashboardData.orders.revenue,
count: dashboardData.orders.count
},
partner: {
revenue: dashboardData.partner.revenue,
count: dashboardData.partner.count
},
total: {
revenue: dashboardData.orders.revenue + dashboardData.partner.revenue,
count: dashboardData.orders.count + dashboardData.partner.count
}
},
dataLoaded: {
orders: dashboardData.loaded.orders,
partner: dashboardData.loaded.partner
}
};
}
});
}
function openAIChat() {
// Mock in dev mode
if (!window.__INFORMER__?.openChat) {
window.__INFORMER__ = window.__INFORMER__ || {};
window.__INFORMER__.openChat = (opts) => {
console.log('openChat (dev):', opts);
alert('AI chat would open here. This only works in Informer GO.');
};
}
__INFORMER__.openChat({
prompt: 'Analyze the current revenue data and identify trends.',
context: {
totalRevenue: dashboardData.orders.revenue + dashboardData.partner.revenue,
dateRange: currentFilters.dateRange
},
instructions: `You are viewing an executive dashboard with revenue data from two sources:
internal orders (dataset: ${ORDERS_DATASET}) and partner sales (integration: ${PARTNER_INTEGRATION}).
Use the report_getContext tool to see the current dashboard state including filters and metrics.
Then use the Informer API to:
1. Query the orders dataset for detailed breakdown (searchDataset tool)
2. Compare current metrics to historical trends
3. Identify anomalies or notable patterns
Provide actionable insights based on the data.`
});
}
async function loadAllData() {
await Promise.allSettled([
loadOrders(),
loadPartnerSales()
]);
}
async function loadOrders() {
setStatus('ordersStatus', 'loading');
try {
const dateFilter = buildDateFilter();
const filters = [];
if (dateFilter) {
filters.push({
range: {
orderDate: {
gte: dateFilter.start,
lte: dateFilter.end
}
}
});
}
const response = await fetch(`/api/datasets/${ORDERS_DATASET}/_search`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: { bool: { filter: filters } },
size: 0,
aggs: {
totalRevenue: { sum: { field: 'amount' } },
orderCount: { value_count: { field: 'id' } }
}
})
});
if (!response.ok) throw new Error(`Orders query failed: ${response.status}`);
const result = await response.json();
const aggs = result.aggregations;
dashboardData.orders.revenue = aggs.totalRevenue.value;
dashboardData.orders.count = aggs.orderCount.value;
dashboardData.loaded.orders = true;
document.getElementById('ordersRevenue').textContent = formatCurrency(aggs.totalRevenue.value);
document.getElementById('ordersCount').textContent = aggs.orderCount.value.toLocaleString();
setStatus('ordersStatus', 'success', 'Loaded');
} catch (err) {
console.error('Orders error:', err);
setStatus('ordersStatus', 'error', 'Failed');
dashboardData.loaded.orders = false;
}
}
async function loadPartnerSales() {
setStatus('partnerStatus', 'loading');
try {
const dateFilter = buildDateFilter();
const params = dateFilter ? { start_date: dateFilter.start, end_date: dateFilter.end } : {};
const response = await fetch(`/api/integrations/${PARTNER_INTEGRATION}/request`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url: '/v1/sales',
method: 'GET',
params
})
});
if (!response.ok) throw new Error(`Integration request failed: ${response.status}`);
const result = await response.json();
if (result.error) {
throw new Error(`Partner API error: ${result.status}`);
}
const revenue = result.data.total_revenue || 0;
const count = result.data.count || 0;
dashboardData.partner.revenue = revenue;
dashboardData.partner.count = count;
dashboardData.loaded.partner = true;
document.getElementById('partnerRevenue').textContent = formatCurrency(revenue);
document.getElementById('partnerCount').textContent = count.toLocaleString();
setStatus('partnerStatus', 'success', 'Loaded');
} catch (err) {
console.error('Partner API error:', err);
setStatus('partnerStatus', 'error', 'Failed');
dashboardData.loaded.partner = false;
}
}
function buildDateFilter() {
if (currentFilters.dateRange === 'all') return null;
const days = parseInt(currentFilters.dateRange);
const endDate = new Date();
const startDate = new Date();
startDate.setDate(startDate.getDate() - days);
return {
start: startDate.toISOString().split('T')[0],
end: endDate.toISOString().split('T')[0]
};
}
function setStatus(elementId, state, text) {
const el = document.getElementById(elementId);
el.className = `status ${state}`;
el.textContent = text || state;
}
function formatCurrency(val) {
if (isNaN(val)) return '--';
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 0
}).format(val);
}
init();
HTML for Ask AI Button
Claude will add the button to the header:
<header>
<div>
<h1>Executive Dashboard</h1>
<p class="subtitle">Real-time data from multiple sources</p>
</div>
<button id="askAI" class="btn-secondary">Ask AI</button>
</header>
Button Styles
Claude will add styling for the AI button:
header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
}
.btn-secondary {
padding: 10px 20px;
background: var(--bg-card);
color: var(--primary);
border: 2px solid var(--primary);
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.btn-secondary:hover {
background: var(--primary);
color: white;
}
@media print {
.btn-secondary { display: none; }
}
How It Works
1. Tool Registration (on page load)
registerAITools(); // Registers report_getContext
The tool metadata (name, description, schema) is sent to Informer GO. The handler stays in the app.
2. User Clicks "Ask AI"
openAIChat();
This sends:
- prompt - Initial user message
- context - Static snapshot of current state
- instructions - Tell AI which APIs to call and what to focus on
The chat opens in Informer GO with:
- The Informer API skill automatically enabled (
apiCallandsearchRoutestools) report_getContextavailable as a tool- The app's identity (id, name, url) automatically included
3. AI Conversation
The AI can:
- Call
report_getContextto get fresh dashboard state - Call
searchDataset(from Informer API skill) to query the orders dataset - Call
makeApiCallto hit other Informer APIs
Example AI flow:
User: "Why did revenue spike last week?"
AI thinks:
1. Call report_getContext to see current filters/metrics
2. Call searchDataset on admin:orders with date filter for last week
3. Group by category to find which category drove the spike
4. Respond with insights
4. Bidirectional Communication
- App → AI - Initial context via
openChat() - AI → App - Calls
report_getContexton-demand - AI → Informer API - Queries datasets, executes queries, calls integrations
Tool with Parameters
You can also register tools that accept parameters:
__INFORMER__.registerTool({
name: 'getTopCustomers',
description: 'Get the top N customers by revenue for the current date range.',
schema: {
type: 'object',
properties: {
limit: {
type: 'number',
description: 'Number of customers to return (default: 10)'
}
},
required: []
},
handler: (args) => {
const limit = args.limit || 10;
// Query dataset or return cached data
return fetchTopCustomers(limit);
}
});
The AI can call:
report_getTopCustomers({ limit: 5 })
Context vs Instructions vs Tools
| Mechanism | When It's Read | Best For |
|---|---|---|
context | Once, when chat opens | Static snapshot (selected row, current total) |
instructions | Once, when chat opens | Guidance on which APIs to call, what to analyze |
tools (via registerTool) | On-demand, during conversation | Live state that changes (filters, visible data) |
Use context for - Values that won't change during the conversation (e.g., "user clicked on Q4 2024")
Use instructions for - Telling AI which datasets/APIs to use (e.g., "query admin:orders dataset")
Use tools for - State that might change or is expensive to send upfront (e.g., all visible rows)
Dev Mode Mocking
The Vite plugin doesn't provide registerTool(), openChat(), or showCopilot() since there's no parent GO app. Mock them for testing:
if (!window.__INFORMER__?.registerTool) {
window.__INFORMER__ = window.__INFORMER__ || {};
window.__INFORMER__.registerTool = (def) => console.log('registerTool (dev):', def.name);
}
if (!window.__INFORMER__?.openChat) {
window.__INFORMER__ = window.__INFORMER__ || {};
window.__INFORMER__.openChat = (opts) => {
console.log('openChat (dev):', opts);
alert('AI chat only works in Informer GO');
};
}
if (!window.__INFORMER__?.showCopilot) {
window.__INFORMER__ = window.__INFORMER__ || {};
window.__INFORMER__.showCopilot = () => console.log('showCopilot (dev): copilot button would appear');
}
Test in Informer GO
- Deploy the app:
npm run deploy - Open Informer GO (desktop or mobile app)
- Navigate to the app
- Click "Ask AI"
- Verify chat opens with context
- Ask the AI to "show me the current dashboard state" - it should call
report_getContext
Next Steps
Continue to Smart Summaries to learn how to use AI completion endpoints for inline text generation and structured insights.