open-wa logoopen-wa
DocsGuides

Dashboard Pages

Render plugin components inside Neo pages and the normal Chat feed.

Dashboard Pages

Plugins can supply native components inside Neo's shell and extend its existing Chat view. Neo supplies React, shared UI controls, privacy, calling, and requests scoped to your plugin. A browser module runs with the dashboard's trust; install modules only from plugins you trust.

Add a page

Return a root page declaration and serve a self-contained ES module from the plugin's Hono routes. Include the module file in the published package.

// In the object returned by init():
routeAuth: 'host',
pages: [{
  path: '/',
  title: 'Greeting bot',
  icon: '👋',
  module: 'dashboard.js',
}],
routes: () => {
  const app = new Hono();
  app.get('/dashboard.js', async c => c.body(
    await readFile(new URL('./dashboard.js', import.meta.url), 'utf8'),
    200,
    { 'Content-Type': 'text/javascript' },
  ));
  app.get('/health', c => c.json({ status: 'Ready' }));
  return app;
},

Import Hono from hono and readFile from node:fs/promises in your plugin's server entry. The file URL above assumes the browser module sits beside that entry; preserve the corresponding layout in your package.

// dashboard.js — browser module, without bare package imports
export function createPage({ React, ui, json }) {
  const { createElement: h, useEffect, useState } = React;
  return function GreetingPage() {
    const [status, setStatus] = useState('Loading…');
    useEffect(() => {
      const controller = new AbortController();
      json('health', { signal: controller.signal })
        .then(value => setStatus(value.status))
        .catch(() => { if (!controller.signal.aborted) setStatus('Unavailable'); });
      return () => controller.abort();
    }, []);
    return h('div', { className: 'dashboard-page' },
      h(ui.PageHeader, { title: 'Greeting bot' }),
      h('p', { role: 'status' }, status));
  };
}

Open Plugins → Greeting bot, at /dashboard/plugins/greeting-bot. A page without module displays the plugin's status metadata. Use path: '/' for a custom page; nested plugin page routes aren't implemented.

title, icon, description, and optional order describe a page. Use an emoji for the sidebar icon. The current sidebar doesn't apply order sorting.

Extend the normal Chat feed

Return chat: { module: 'chat.js' } from init() and serve that module from your plugin routes. Its createChat(host) factory returns a React component receiving chatId and children. Call children() with a toolbar and feed entries:

export function createChat({ React }) {
  const h = React.createElement;
  return function ChatControls({ chatId, children }) {
    return children({
      toolbar: h('div', { className: 'border-b p-3' },
        'Greeting bot is available in this conversation.'),
      entries: [],
    });
  };
}

A feed entry has id, at (Unix milliseconds), and React content. Add messageId to place it beneath a matching loaded WhatsApp message. Otherwise, Neo inserts it into the timeline in time order. Namespace entry IDs within your plugin and keep them stable across updates. New transcripts update the existing entry rather than duplicating it. Return an empty contribution for chats your plugin doesn't handle. Dispose subscriptions when chatId changes or the component unmounts.

Voxtral uses this interface for voice-note transcripts, call turns, and pause and takeover controls. Its settings and usage stay on its plugin page.

Host helpers

  • React is Neo's own React instance. Don't bundle another copy.
  • ui includes Button, Input, Textarea, PageHeader, DetailSheet, and BoardUI table components.
  • json(path, init) and request(path, init) use the current Easy API connection and its API key header. Paths stay under /plugins/<your-plugin>/.
  • watch(path, receive, signal) consumes event: snapshot SSE events. Handle reconnects in your component and fetch a fresh snapshot; it doesn't replay past events.
  • usePrivacy() and useCalling() expose Neo's shared hooks. Apply privacy redaction to names and content; use the shared call controller for human audio.
  • chatUrl(peer) returns a link to the person's existing Chat view.

Browser modules are fetched with authenticated requests and imported on demand. Keep them self-contained: inline styles or bundle their dependencies ahead of publication. Relative module imports aren't resolved against the plugin route.

routeAuth: 'host' protects plugin routes with X-API-Key when Easy API has a key. Without a key, these routes accept loopback access and reject browser requests from another origin. Remote use needs a configured Easy API key.

Was this helpful?

Your answer includes the page path and docs version.

On this page