Skip to main content

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 orders dependency 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 orders dataset and the partner integration) and suggest specific analysis it can perform (trends, anomalies, category breakdown)."

The App Bridge

The app bridge enables bidirectional communication between apps and AI:

  1. App → AI - openChat() sends initial context and instructions
  2. 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()
};
}
});
OptionTypeDescription
namestringTool name - exposed to AI as report_<name>
descriptionstringWhat the tool does (AI reads this to decide when to call it)
schemaobjectJSON Schema for input parameters
handlerfunctionCalled 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 (apiCall and searchRoutes tools)
  • report_getContext available as a tool
  • The app's identity (id, name, url) automatically included

3. AI Conversation

The AI can:

  • Call report_getContext to get fresh dashboard state
  • Call searchDataset (from Informer API skill) to query the orders dataset
  • Call makeApiCall to 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_getContext on-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

MechanismWhen It's ReadBest For
contextOnce, when chat opensStatic snapshot (selected row, current total)
instructionsOnce, when chat opensGuidance on which APIs to call, what to analyze
tools (via registerTool)On-demand, during conversationLive 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

  1. Deploy the app: npm run deploy
  2. Open Informer GO (desktop or mobile app)
  3. Navigate to the app
  4. Click "Ask AI"
  5. Verify chat opens with context
  6. 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.