@aintela/bi
v0.2.9
Published
BI visualisation components, dashboard grid, hooks, the dashboard composer (sketch pad + authoring view) and the LLM runtime registry for the AIA platform (extracted from Envision's dashboard app).
Readme
@aintela/bi
React BI components for the AIA platform, extracted from Envision's dashboard app (Issue 1210, plan in
AIA-Solution/Docs/BI/BiLibrary_ExtractionPlan.md §1.2). It pairs with @aintela/chat: the chat library runs the
LLM-emitted TSX, this package is what that TSX imports.
@aintela/bi flat union of every subpath — what LLM code imports
@aintela/bi/data DataResult, defineSchema / InferModel, coercers, formatters, computeDomain/Ticks, trend helpers
@aintela/bi/cards ChartCard, DataTableCard (MUI X Pro), MetricCard, MetricGroupCard, GaugeCard, GaugeGroup, TreeMapCard, MapBubblesCard, MapConnectionsCard
@aintela/bi/grid GridLayout, GridCard, GridCardPlaceHolder, useGridConfigTranslator, BiErrorBoundary
@aintela/bi/inline Chart (PNG artifact card), Metric, MetricGroup, IFrame — the explorer's present_* vocabulary
@aintela/bi/hooks useDashboardData, useDashboardLazyData, DashboardParametersProvider
@aintela/bi/filters VizAutoComplete, VizDateRangePicker, VizDatePicker, VizSelect, VizTextField, VizTriggerButton, createFilterComponent
@aintela/bi/viewer useDashboardBundle (headless), ScriptDashboardViewer, ScriptDashboardViewerVertical
@aintela/bi/runtime registerBiModules, createBiUiTools, loadDashboardBundle / instantiateDashboardBundle, BI_RUNTIME_VERSION
@aintela/bi/theme createBiTheme / withBiPalette, useBiPalette, biSeriesColor, BiThemeBridge, biLightPalette, biDarkPalette
@aintela/bi/client createBiClient, BiClientProvider, useBiClient (+ the Dashboard / DashboardFolder document routes)
@aintela/bi/composer ComposerView, DashboardPad, DashboardPadSketch, ComposerProvider / useComposer, the LLM pad helpers, the environment API
@aintela/bi/style.css the grid + inline + composer stylesheet (token-driven, no colours of its own)Install
npm i @aintela/bi @aintela/chatPeer dependencies (the host installs them, this package never bundles them).
Since 0.2.9 there are no optional peers. There used to be four — framer-motion, zustand and the two
date pickers — and the marking was wrong for all of them: each is loaded EAGERLY, by a static import, from at
least two published entry points. Marking a peer optional tells npm not to install it and not to warn, so the
only thing it bought was a package that throws on its first line:
Cannot find package '<peer>' imported from …/dist/index-*.jsNothing catches that earlier. npm is satisfied (the peer was optional), and tsc is satisfied (types resolve
from the .d.ts; only a real module load pulls the chunk). It cost the AIA agentserver image its whole
dashboard composer — the baked react-tsx-template declared zustand and the date pickers but happened not to
declare framer-motion, so every LLM-authored card test died on its first import { … } from '@aintela/bi'.
Only that one peer surfaced, and only because the other three were declared by luck; a consumer with none of
them installed throws on whichever the bundler resolves first. A developer checkout can never show any of it,
because there the consumer resolves peers from this workspace's own node_modules.
What "optional" was reaching for is real but per ENTRY POINT, which npm cannot express — so it lives in
the table below and in tests/peerReachability.test.ts, which pins it and fails if it moves:
| entry | eagerly needs |
|---|---|
| @aintela/bi (the flat union), /runtime, /composer | framer-motion, zustand, both date pickers |
| /filters, /viewer | both date pickers |
| /cards, /grid, /data, /inline, /hooks, /theme, /client | nothing beyond the always-required peers |
So a consumer that imports only those seven subpaths installs four packages it does not use. That is the
deliberate trade: being asked for something you may not need beats a package that throws on import, and this
package already required one paid MUI X peer (@mui/x-data-grid-pro) unconditionally, so the precedent for
carrying a peer you might not exercise was already set.
| peer | version | note |
|---|---|---|
| react, react-dom | ^18.2 | React 18 — @types/react must be 18 too |
| @mui/material, @mui/icons-material | ^7 | |
| @emotion/react, @emotion/styled | ^11.14 | |
| @mui/x-data-grid-pro | ^8 | Pro — see the licence note |
| @mui/x-date-pickers, @mui/x-date-pickers-pro | ^8 | Pro — eager on 5 of 12 entries (/filters, /viewer and the three below) |
| recharts | ^2.15 | |
| react-grid-layout | ^2.2 | the 2.x major with the /core subpath |
| react-simple-maps | ^3 | |
| dayjs | ^1.11 | |
| zustand | ^5 | eager on the bare entry, /runtime and /composer — the composer creates its store with it |
| framer-motion | ^11.18 || ^12 | eager on the bare entry, /runtime and /composer (the splash ↔ execution morph, the side panel, the sketch chips) |
| @aintela/chat | ^0.4.25 | the runtime subpath (registry, uiTools) and the composer subpath (PromptV2, EnvironmentProvider, the v2 icons) import it at runtime |
Bundler note: dedupe react, react-dom, @mui/material, @mui/material/styles, @mui/icons-material,
@emotion/react, @emotion/styled, zustand across @aintela/bi and @aintela/chat (obtapp's vite.config.ts already
does this for the chat library — add @aintela/bi). Otherwise useTheme() reads a different ThemeContext than the
one you created the theme in.
MUI X licence
DataTableCard is MUI X DataGrid Pro. The licence key is the host's, set once before the first render:
import { LicenseInfo } from '@mui/x-license';
LicenseInfo.setLicenseKey(import.meta.env.VITE_MUI_LICENSE_KEY);This package ships no key. If none is set the grid renders MUI's watermark and the card logs one
[@aintela/bi] MUI X licence key not set … warning per session.
Wiring a host
import { LicenseInfo } from '@mui/x-license';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import { AiChatProvider } from '@aintela/chat';
import { createBiClient, BiClientProvider, createBiTheme, BiThemeBridge, registerBiModules, createBiUiTools } from '@aintela/bi';
import '@aintela/bi/style.css';
LicenseInfo.setLicenseKey(MUI_KEY);
// 1. Make the library resolvable from LLM-generated code (once, at boot).
registerBiModules({ specifiers: ['@aintela/bi'] });
// envision-abi keeps its legacy keys alive forever:
// registerBiModules({ specifiers: ['@aintela/bi', '@envision/ui-library', '@envision/ui-library/components', '@envision/aichat'], extraScope: VizLibraryShim });
// 2. One client per server. Routes default to the legacy WSServer layout; override what differs.
const bi = createBiClient({
baseUrl: import.meta.env.VITE_BI_BASE_URL,
getAuthHeader: () => `Bearer ${getAccessToken()}`,
routes: { artifact: (src, { sessionId }) => `${AGENTS_URL}/artifacts/${sessionId}/${src}` },
});
// 3. present_chart / present_metric / present_metric_group / present_iframe; the host adds the rest.
const uiTools = createBiUiTools({ client: bi })
.render('present_dashboard', (a) => <ScriptDashboardViewer dashboardId={a.id} />)
.build();
// 4. Theme: your theme + the BI palette (light and dark defaults ship; set `palette.bi` to override).
const theme = createBiTheme(createTheme({ palette: { mode: 'light', primary: { main: BRAND } } }));
<ThemeProvider theme={theme}>
<BiThemeBridge> {/* publishes the --bi-* CSS variables the stylesheet reads */}
<BiClientProvider client={bi} sessionId={sessionId}>
<AiChatProvider uiTools={uiTools} …/>
</BiClientProvider>
</BiThemeBridge>
</ThemeProvider>registerBiModules calls the chat library's extendModuleRegistry(specifier, surface) once per specifier,
extendDefaultScope(surface) once, and seeds the dashboard-bundle loader's require() map under the same strings.
BI_RUNTIME_VERSION is the package version — log a mismatch against the server's AIntelA.Bi.Agents at handshake.
BiClientProvider / createBiClient
Every server call in the package goes through one BiClient: useDashboardData, useDashboardLazyData (page,
distinct values, CSV/XLSX export), VizAutoComplete options, useDashboardBundle (dashboard document, bundle code,
sub-agent status) and the inline Chart / IFrame URLs (client.artifactUrl(src, { sessionId?, dashboardId? })).
Nothing reads process.env, import.meta.env or a hard-coded host. fetch is injectable (tests, custom transports);
getAuthHeader may be async.
Data hooks
const { toDataResult, loading } = useDashboardData('<query hash>'); // inside a DashboardParametersProvider
const data = toDataResult({ month: 'date', spend: 'number' });
<ChartCard data={data} type="barV" xAxis="month" series={[{ dataKey: 'spend' }]} loading={loading} />
const lazy = useDashboardLazyData('<view id>', { airline: { type: 'string', values: 'distinct' }, total: 'number' });
<DataTableCard lazyData={lazy} /> // server-side page / sort / filter / exportTheming contract (plan §2.3, decision D8)
No colour literal exists in this package outside src/theme/biPalette.ts. Components read the MUI theme
(theme.palette.divider, text.secondary, action.hover, background.paper, alpha(text.primary, 0.08) for
translucent overlays, error/warning/success for gauge bands); the two stylesheets read --bi-* custom properties
that <BiThemeBridge/> (or useBiCssVars()) derives from the theme; DataGrid Pro is styled through sx tokens.
The series palette is a theme augmentation:
declare module '@mui/material/styles' {
interface Palette { bi: { series: string[]; positive: string; negative: string; neutral: string } }
}createBiTheme(theme) / withBiPalette(theme) fill palette.bi from biLightPalette or biDarkPalette by
palette.mode when the host did not set it; useBiPalette() and biSeriesColor(theme, i) read it;
ChartCardDefaultsProvider palette={…} is the per-dashboard override.
Enforced three ways: ESLint no-restricted-syntax on #hex / rgb(a) / hsl(a) literals in .ts/.tsx
(src/theme/biPalette.ts is the only override), stylelint color-no-hex + color-named: never +
function-disallowed-list on .css, and tests/theme/colourLiterals.test.ts, which walks src/ and fails on any
literal outside the palette file.
Composer (@aintela/bi/composer)
The authoring UI extracted from the dashboard app's components/dashboardPad/** + views/composer/** (plan §1.2, Phase 4
item 20): the sketch pad the user draws a dashboard on, the composer screen around it, and the DOM "environment" the
dashboard agent drives through action tools and live TSX.
Providers and what the host still owns
import { AiChatProvider, AiChatV2Pane, RunProgressPanelV2, RunProgressStripV2, EnvironmentProvider, LiveTsxConfigProvider } from '@aintela/chat/components';
import { BiClientProvider, createBiClient, createBiUiTools } from '@aintela/bi';
import { ComposerProvider, ComposerView, ComposerChatBinding, useComposerChatBridge, useComposerEnvironment } from '@aintela/bi/composer';
function ComposerScreen({ routeSessionId, navigate }) {
return (
<BiClientProvider client={bi}>
<ComposerProvider> {/* one zustand store per instance — no module singleton */}
<ComposerHost routeSessionId={routeSessionId} navigate={navigate} />
</ComposerProvider>
</BiClientProvider>
);
}
function ComposerHost({ routeSessionId, navigate }) {
const bridge = useComposerChatBridge(); // onRenderPrompt / onTurnOpen / onTurnComplete / ingestToolEvent / environment
const uiTools = useMemo(() => createBiUiTools({ client: bi, environment: bridge.environment })
.render('present_dashboard', (a) => <ScriptDashboardViewer dashboardId={a.id} />)
.build(), [bridge.environment]);
return (
<LiveTsxConfigProvider maxCharsForFull={10000}>
<AiChatProvider
key={routeSessionId ?? 'new'}
agentContext={{ agentConfig: { id: DASHBOARD_AGENT_ID, name: 'Dashboard Agent' }, customer }} // the host picks the agent
serverBaseUrl={SERVER} sessionId={routeSessionId} user={user} storageService={storage} eventBusService={bus}
uiTools={uiTools} hidePromptInput autoCreateSession
onRenderPrompt={bridge.onRenderPrompt} onTurnOpen={bridge.onTurnOpen} onTurnComplete={bridge.onTurnComplete}
>
<EnvironmentProvider value={bridge.environment}>
<ComposerChatBinding /> {/* mirrors the live session id + forwards gateway tool events */}
<ComposerView
routeSessionId={routeSessionId}
sessionId={liveSessionId} {/* or let ComposerChatBinding set it in the store */}
chatPane={<AiChatV2Pane hideHeader />}
renderProgressPanel={({ hasDashboard, dashboardCollapsed, turnOpen }) => (
<RunProgressPanelV2 width={hasDashboard ? '22%' : '30%'} defaultCollapsed={(!dashboardCollapsed || !turnOpen) && hasDashboard} />
)}
progressStrip={<RunProgressStripV2 />}
onSelectSession={(sid) => navigate(`/composer/${sid}`)}
onDashboardNameChange={setBreadcrumb}
datasetControls={isAdmin && <DatasetParameterizationControls orientation="column" datasets={datasets} agents={agents} agentId={agentId} onAgentChange={pickAgent} />}
/>
</EnvironmentProvider>
</AiChatProvider>
</LiveTsxConfigProvider>
);
}What the host owns, deliberately: AiChatProvider / AiChatV2Pane / the progress panel (the chat is not inside the
package); the agent ids (agentContext) — the legacy DEFAULT_DASHBOARD_AGENT_ID and the customer/Envision agent pair
are gone, DatasetParameterizationControls takes agents + agentId + onAgentChange; routing (routeSessionId in,
onSelectSession / onSearchAllSessions out); the breadcrumb (onDashboardNameChange); isAdmin (render
datasetControls or not); LiveTsxConfigProvider; the pt-BR dayjs locale (the composer never calls dayjs.locale);
the "Abrir" picker body on DashboardPad (renderDashboardPicker); Markdown for card descriptions (renderDescription
— default is plain text).
Components
| export | what it is | key props |
|---|---|---|
| ComposerView | splash → execution layout, drafts, guided tour | chatPane (required), sessionId, routeSessionId, renderProgressPanel, progressStrip, onBeforeSend, resolveGuideElement, suggestions, templates, recentSessions, onSelectSession, onSearchAllSessions, onDashboardNameChange, datasetControls, renderDescription, sidePanelWidth, sidePanel |
| DashboardPad | the V1 pad: toolbar (Sketch/Dashboard, ToolBox, Novo/Salvar/Atualizar/Abrir/Compose) + sketch or viewer | sessionId, dashboardId, turnOpen, onCompose, onChange, onBack, renderDashboardPicker, showDebug, headerExtra, renderDescription |
| DashboardPadSketch | the grid of CardSketch cells + filter chips (ref: DashboardPadHandle = addGenericCell / addTypedCell / handleAddFilter) | hideInternalHeader, hideFiltersInSketch, zoomLevel, onAddCard, headerExtra, renderDescription |
| CardSketch (alias CardScketch), FilterSketch, TitleSketch (aliases TextSketh, TitleSkath), ToolBox, HoverButton, ClonedComponent | the pad's pieces | |
| DashboardPadEditor | tabbed shell (DashboardPadEditorHandle) | renderContent, addMenuItems, tabs, onTab* |
| DatasetParameterizationControls | admin block: "DataSet Dinâmico" toggle + default dataset + agent picker | orientation, datasets, defaultTemplateDataset, agents, agentId, recommendedAgentId, onAgentChange |
| DrawablePrompt | PromptV2 with the sketch in its topSlot (rail + pad, the sketch chips, the queued-changes chip) | onCreateSketch, onBeforeSend, sketchExpanded, onSketchExpandedChange, seedDraft, compact, zoomLevel, datasetControls |
| EntryEmptyState, SpotlightOverlay, DashboardSidePanel, DashboardPin | the splash, the tour, the two dashboard surfaces | suggestions/templates/onSearchAllSessions; steps/resolveElement; width/defaultCollapsed |
| SessionVisualizationsPanel | side panel listing the session's charts/KPIs that have scrolled OUT of the transcript | width, title, footerAction, headerActions |
| CardTypeIconsProvider / useCardTypeIcons | swap the pad's icons (category / specificType / filterType maps; MUI defaults) | |
Data that used to be hardcoded is now a default you can replace: defaultComposerSuggestions ({ label, prompt }[]),
defaultComposerTemplates (+ seedFromTemplate, TEMPLATE_KIND_TO_SKETCH), CARD_TYPE_CATALOG, FILTER_TYPE_CATALOG —
all Portuguese travel-BI text.
The side panel is a SLOT
ComposerView's sidePanel prop takes a node, or a function of { hasDashboard, turnOpen }. Omit it and you get
DashboardSidePanel, which is what every host had before the prop existed; pass null for no panel at all.
It is a slot rather than a 'dashboard' | 'visualizations' enum on purpose. A host changing its layout or its
colours must not need a change in here — an enum makes every new panel a release of this package, which is the
coupling that rule exists to avoid. The same reasoning is why SessionVisualizationsPanel reads a PUBLIC hook: a
host that wants a different panel writes one against useSessionVisualizations() and never forks this file.
Session visualizations (@aintela/bi/runtime)
What the conversation has DRAWN, and which of it is currently on screen. The rule the panel implements: the charts live in the chat, and the panel lists only the ones that have scrolled out of view — listing all of them would be a second copy of the transcript, listing none would leave the column empty.
| export | what it is |
|---|---|
| SessionVisualizationsProvider | holds the registry for one conversation. Mount it around the transcript AND whatever reads the list |
| VisualizationFrame | wraps one drawn thing: registers it, stamps data-viz="<callId>", watches its own intersection with the scroller. Adds no styling of its own |
| useSessionVisualizations() | { items, offscreen, scrollTo(id), setScroller(el) } |
createBiUiTools already wraps present_chart, present_metric, present_metric_group and present_iframe in a
frame, with the chart's own image as the panel's thumbnail. Outside a provider every piece is inert and the frame
renders its child unchanged, so a host that does not want the feature sees no difference.
ComposerView finds the transcript's scroller inside the chatPane you gave it and publishes it, so the
composer needs no wiring beyond the provider. A flash is drawn by the host: the frame sets data-viz-focus for
two seconds and styling it is a CSS rule, not a prop.
Store — ComposerProvider, useComposer()
ComposerProvider creates one zustand store (createComposerStore) per mount; useComposer(selector) subscribes,
useComposerStore() gives the API for getState() in handlers, useComposerEnvironment() returns the
DashboardEnvironment. The store keeps the legacy semantics: dashboardDefinition (live), publishedDashboard (what the
viewer renders — advances VIEWER_COMMIT_DEBOUNCE_MS = 3 s after the last edit, never mid-turn, immediately on
commitForSend() / load / dashboardGenerated / refreshDashboard), lastSentDashboard (the queued-changes chip's
baseline), renderGeneration (bundle CODE changed → codeGeneration), llmCounter (→ llmRenderCount), agentPulseTick,
todosByCardKey, recordSketchChange / clearClientSideChanges (the per-turn hint the prompt carries), the guide state,
and the autosave (DRAFT_SAVE_DEBOUNCE_MS = 800 ms, POST /Dashboard through the BiClient; client={null} disables it).
The environment object
DashboardEnvironment = highlightComponent(target, { durationMs, scrollIntoView }), cloneComponent(target, { includeHidden }),
createDashboardPad(def), updateDashboardPad(def), refreshDashboard(), showGuide(steps, options), dismissGuide().
Targets resolve against [data-env-id], [data-env-key], [data-env-name], #id, .class or a raw selector
(resolveEnvironmentElements, resolveGuideElement). Pass it to EnvironmentProvider (live TSX calls useEnvironment()),
and to createBiUiTools({ environment }), which then also registers the agent's three ACTION tools: refresh_dashboard,
present_dashboard_pad ({ definitionJson, mode: 'create' | 'update' }) and present_guide ({ stepsJson }).
getBiModuleSurface() exposes the whole subpath, so LLM code reaches ExamplePad, ExampleCardSketh (legacy spelling kept
as an alias of ExampleCardSketch), PrototypePad, useDashboardPad, c, dateRange, datePicker, autoComplete,
EnvironmentProvider, useEnvironment under every registered specifier.
Theming
Same contract as the rest of the package. composer.css reads --bi-* only; the bridge gained --bi-secondary,
--bi-secondary-soft, --bi-sketch-dash, --bi-sketch-dash-faint, --bi-sketch-selection (alpha(primary, .12)),
--bi-scrim, --bi-highlight-outline, --bi-highlight-glow. The v2 chat theme's extra tokens (palette.surface.raised,
palette.surface.hover, palette.border.soft, typography.fontFamilyMono) are read when present and fall back to
background.paper / action.hover / divider / a monospace stack (resolveComposerSurfaces). Framer-motion timings are
unchanged; every animated colour is a theme token.
What changed from the dashboard app
- Providers now required:
BiClientProvider(or aclientprop) for every hook / viewer / inline component;DashboardParametersProvideraround cards (as before);BiThemeBridgefor the stylesheets to have colours. - Removed exports:
CHART_COLORS,chartColor,chartShade(fromdata;chartShadelives incards, built on MUIalpha()),GEO_URL/DEFAULT_GEO_LAYERS(nowGEO_URL_WORLD/worldGeoLayers/brazilGeoLayers+GeoLayersProvider), theVizLibrarycompat pieces,ContainTable,DataAnalysis. - Signature changes:
loadDashboardBundle(client, sessionId, id)/instantiateDashboardBundle(client, …)take the client first;Chartlostpartial,runaround,showHeader,eventBusService,parentComponentand gainedonExpand;IFramegainedonExpand/height;VizTriggerButtonrequiresonTrigger;VizDatePicker/VizSelect/VizTextFieldreadDashboardParametersContext(the PythonDashboardContextis gone). - Viewer:
ScriptDashboardViewertakesonExportPdf,onSchedule,onEdit,onLayoutCommit,datasetOverride,isAdmin,datasetOverrideControl,resolveCardIcon,isPrintPreviewinstead of reading the composer / app stores,react-router,react-js-cron,useCustomersand the PDF endpoint.headerRenderis unchanged.dashboardWaitinggainedsessionId. - Map cards default to the world base layer only; pass
geoUrl={brazilGeoLayers}(or aGeoLayersProvider) for the legacy world + Brazil-states pair. loadingis optional on every card (defaultfalse).Metric.descriptionrenders as plain text (the app rendered Markdown).- Portuguese UI strings are kept as they were.
- Composer:
useComposerViewStore(module singleton,devtools) → a per-ComposerProviderstore;DashboardService→BiClient(listDashboards,getDashboardBySession,upsertDashboard,updateDashboard,deleteDashboard,dashboardAction,listFolders/getFolder/createFolder/updateFolder/deleteFolder/folderAction,getDashboardHierarchy; the draft envelope lives intoDraftPayload/fromUpsertResponse);ComposerViewV2→ComposerView+useComposerChatBridge(noAiChatProvider, no router, nouseDashboardAppStore);ExampleFilterSketchandTestCardwere not carried over (unused);CardSketchrenders the description as plain text unlessrenderDescriptionis given (wasreact-markdown);DashboardPad's debug panel is a<pre>(wasMarkdownExtended); the "Buscar todas as sessões" link firesonSearchAllSessionsinstead of clicking[data-v2-search-trigger];SketchCardItem.muiIconNamenow stores the icon's export name (was the constant'SvgIcon').
Scripts
npm run build (vite lib + .d.ts) · npm test · npm run lint (eslint + stylelint) · npm run typecheck.
