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. Have its handler call the server route, which reads the
ordersdependency slot — the tool should never hit a dataset URL directly."
Improve the chat experience:
"Update the openChat instructions to tell the AI which dependency slots the app reads (the
ordersdataset and thepartnerintegration) 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) |
Data Access Configuration
This dashboard reads two data sources: an internal orders dataset and a partner sales integration. Both are declared as typed dependency slots in informer.yaml. The installer (or each slot's defaultBinding) points a slot at a real resource, and a server route refers to it by slot name — no dataset or integration IDs ever appear in the frontend:
dependencies:
orders:
target: dataset
# UUID of your orders dataset — look it up with `GET /api/datasets-list`.
defaultBinding: 7d5a9b1e-0c83-4bde-9e2a-3a4b5c6d7e8f
partner:
target: integration
# UUID of your integration — look it up with `GET /api/integrations-list`.
defaultBinding: 5a6b7c8d-9e0f-1234-5678-9abcdef01234
# The AI copilot endpoints aren't dependency slots — they're raw API paths,
# so they stay in the access allowlist alongside the typed dependencies above.
access:
apis:
- POST /api/models/go_everyday/_chat
- POST /api/models/go_everyday/_completion
- POST /api/models/go_everyday/_object
The typed slots (orders, partner) and the access: apis: allowlist coexist — both are honored. Data access goes through the slots; the AI model routes that power the copilot stay in access.apis. See the informer.yaml reference for the full slot model.
What Claude Will Generate
When you ask Claude to add AI integration, it will implement tool registration, chat opening, and dev mode mocking.
Server Route
The data access lives in a server route that reads the orders and partner slots through the typed context proxy. The frontend never sees a dataset or integration ID — it just calls /api/_server/dashboard:
// server/dashboard.js
export async function POST({ context, request }) {
const { filters = [], params = {} } = request.body || {};
const [orders, partner] = await Promise.all([
// dataset slot → search(esQuery)
context.orders.search({
query: { bool: { filter: filters } },
size: 0,
aggs: {
totalRevenue: { sum: { field: 'amount' } },
orderCount: { value_count: { field: 'id' } }
}
}),
// integration slot → request(opts) — returns the upstream JSON directly
context.partner.request({
method: 'GET',
url: '/v1/sales',
params
})
]);
return {
orders: {
revenue: orders.aggregations.totalRevenue.value,
count: orders.aggregations.orderCount.value
},
partner: {
revenue: partner.total_revenue || 0,
count: partner.count || 0
}
};
}
JavaScript
Here's what the rest of the generated code looks like. The data loaders call the server route instead of hitting dataset/integration URLs directly:
window.informerReady = false;
const theme = window.__INFORMER__?.theme || 'light';
document.documentElement.setAttribute('data-theme', theme);
// Slot names declared in informer.yaml — used only as labels in the
// copilot instructions below. No resource IDs live in the frontend.
const ORDERS_SLOT = 'orders';
const PARTNER_SLOT = 'partner';
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 slot: ${ORDERS_SLOT}) and partner sales (integration slot: ${PARTNER_SLOT}).
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() {
setStatus('ordersStatus', 'loading');
setStatus('partnerStatus', 'loading');
try {
// One call to the server route — it reads the `orders` and `partner`
// slots and returns both result sets. No resource IDs in the frontend.
const dateFilter = buildDateFilter();
const filters = [];
const params = {};
if (dateFilter) {
filters.push({
range: {
orderDate: {
gte: dateFilter.start,
lte: dateFilter.end
}
}
});
params.start_date = dateFilter.start;
params.end_date = dateFilter.end;
}
const response = await fetch('/api/_server/dashboard', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filters, params })
});
if (!response.ok) throw new Error(`Dashboard load failed: ${response.status}`);
const data = await response.json();
// Orders
dashboardData.orders.revenue = data.orders.revenue;
dashboardData.orders.count = data.orders.count;
dashboardData.loaded.orders = true;
document.getElementById('ordersRevenue').textContent = formatCurrency(data.orders.revenue);
document.getElementById('ordersCount').textContent = data.orders.count.toLocaleString();
setStatus('ordersStatus', 'success', 'Loaded');
// Partner sales
dashboardData.partner.revenue = data.partner.revenue;
dashboardData.partner.count = data.partner.count;
dashboardData.loaded.partner = true;
document.getElementById('partnerRevenue').textContent = formatCurrency(data.partner.revenue);
document.getElementById('partnerCount').textContent = data.partner.count.toLocaleString();
setStatus('partnerStatus', 'success', 'Loaded');
} catch (err) {
console.error('Dashboard load error:', err);
setStatus('ordersStatus', 'error', 'Failed');
setStatus('partnerStatus', 'error', 'Failed');
dashboardData.loaded.orders = false;
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;
// Read data through the server route (which reads the `orders` slot)
// or return cached data — never a dataset URL from the frontend.
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.