JavaScript SDK
Mihe in any web app, without React — Vue, Svelte, Angular, server-rendered pages, or a page with no build step.
@mihe-dev/js is the framework-free SDK. The React SDK is built on it, so the
two behave the same; this page is the same integration with function calls
instead of hooks.
Install
npm install @mihe-dev/jsES modules with TypeScript types, and no dependencies beyond
@mihe-dev/protocol. It is safe to import during a server render: nothing
touches the page until you create an assistant in the browser.
No build step? One script tag defines window.Mihe:
<script src="https://cdn.jsdelivr.net/npm/@mihe-dev/js@0.1.0/dist/mihe.iife.min.js"></script>
<script>
const mihe = Mihe.createMihe({ /* as below */ });
Mihe.mountWidget(mihe);
</script>Pin the version in the URL, or copy the file into your site; it needs nothing
beside it. dist/mihe.min.js is the same file as an ES module.
Create the assistant
Once, when your app starts and the user is signed in:
import { createMihe, mountWidget } from "@mihe-dev/js";
const mihe = createMihe({
url: "wss://runtime.mihe.dev/v1/ws",
publicKey: "pk_live_…",
// Your backend signs this for the signed-in user. See User tokens.
getUserToken: () => fetch("/api/mihe-token").then((r) => r.text()),
// Your router. Without it, the assistant navigates with a full page load.
navigate: (route) => router.push(route),
// Refetch what the page shows after the assistant changes data.
onActionComplete: () => refresh(),
});
mountWidget(mihe);| Option | Required | Meaning |
|---|---|---|
url | yes | The runtime's session address. |
publicKey | yes | The app's public key. Safe in a browser. |
getUserToken | yes | Fetches a token your backend signed. Called on every connect, so it can be fresh. User tokens |
navigate | no | Takes the user to a route, with your router. |
openModal | no | Opens a dialog by key. Return false for a key you do not have. |
onActionComplete | no | Called when an approved action finishes, whatever the outcome. |
debug | no | Logs what each screen costs in tokens. For building, not production. |
createMihe connects at once. mihe.destroy() disconnects for good, such as
when the user signs out.
Tell it what the user sees
Each call takes the same declaration as its React hook (see Screens and components) and returns a function that removes it. Call that when the user leaves the page. Calling a method again with the same id updates the declaration rather than adding a second one, so a page can re-declare whenever its data changes.
function renderOrders(orders) {
const leave = [
mihe.setScreen({
screenId: "orders",
route: "/orders",
title: "Orders",
visibleData: { open: orders.length },
}),
mihe.registerTable({
id: "orders.table",
label: "Orders",
columns: [
{ key: "number", label: "Order" },
{ key: "total", label: "Total" },
],
rows: orders,
rowId: (order) => order.id,
rowIdName: "orderId",
}),
mihe.registerComponent(document.querySelector("#new-order"), "orders.new", {
type: "button",
label: "New order",
}),
];
return () => leave.forEach((undo) => undo());
}An undo is tied to its own declaration, not just the id. If the next page has already declared a table with the same id, a late undo from the page you left does nothing, so cleanup order never removes the wrong thing.
| Method | Declares |
|---|---|
setScreen(screen) | Where the user is. |
registerComponent(element, key, meta) | Something the assistant can point at. Marks element so it can be found; pass null for one it can describe but not highlight. |
registerTabs(tabs) | Tabs it can see and switch. |
registerTable(table) | A table it can read, and search with query. |
registerChart(chart) | A chart it can explain. |
setValues(key, values) | Extra figures for the screen's visibleData. |
Actions
const remove = mihe.registerAction("show_unpaid_orders", (input) => {
setFilter("unpaid");
return { shown: unpaidCount };
});The action itself (its risk, permissions and whether it needs approval) is in your registry, as on Actions. The handler returns quickly: for anything that needs the user, such as a confirmation dialog or a password, open it, return what you opened, and let them finish there.
The widget
mountWidget puts <mihe-assistant> on the page: the orb by default, in its
own shadow root, so your page's CSS cannot reach it and its CSS cannot leak
into your page.
mountWidget(mihe); // the orb
mountWidget(mihe, { form: "classic" }); // a launcher and panel
mountWidget(mihe, { layout: "inline", container: sidebar }); // the panel, in your layout| Option | Default | Meaning |
|---|---|---|
layout | "floating" | "inline" for the panel alone, filling container. |
container | document.body | Where the element goes. |
form | the dashboard's setting, else "orb" | "classic" for a launcher that opens the panel. |
corner | the dashboard's setting | "bottom-right" or "bottom-left". |
draggable | the dashboard's setting | Let the user drag the orb to either side. |
voiceModes | the dashboard's setting | Narrows the voice modes offered; never widens them. |
offset | { bottom: 20, side: 20 } | Distance from the corner, in pixels. Lift Mihe above another widget there, such as a support chat. |
showLauncher | true | Off when you have your own trigger. |
defaultOpen | false | Open the panel once, on mount. |
title | the assistant's name | The panel's header. |
placeholder | — | The input's placeholder. |
showClose | false (inline) | A close button in an inline panel's header. |
launcherIcon | your uploaded icon, or the Mihe mark | An element drawn in the orb. |
mountWidget returns { element, unmount }.
In HTML, place the element yourself and connect it. Options are
attributes: layout, form, corner, draggable, voice-modes,
offset-bottom, offset-side, show-launcher, default-open, show-close,
placeholder, and heading for the title.
<mihe-assistant form="classic" corner="bottom-left"></mihe-assistant>
<script type="module">
import { createMihe, defineMiheAssistant } from "@mihe-dev/js";
defineMiheAssistant();
document.querySelector("mihe-assistant").mihe = createMihe({ /* … */ });
</script>Theming is through custom properties on the element, the one styling hook a shadow root lets through:
mihe-assistant {
--mihe-accent: #0a7d5a;
--mihe-radius: 10px;
--mihe-font: "Inter", system-ui, sans-serif;
--mihe-z: 2147482000;
}Your own trigger, or your own UI
mihe.bindTrigger(element) makes any element open and close the assistant,
with the keyboard and screen-reader support a plain div lacks. It returns
the function that unbinds it.
To draw the conversation yourself, read the state and subscribe to it.
getState() returns the same object until something changes, so comparing
it is enough to know whether to redraw:
mihe.subscribe(() => render(mihe.getState()));
mihe.send("How many orders are open?");getState() | { connection, messages, proposals, busy, lastError, isOpen, assistantName, launcherIconUrl, widgetSettings, voice } |
subscribe(listener) | Called after every change. Returns the unsubscribe. |
send(text) | Sends a message, and opens the assistant so the reply is visible. |
respondToProposal(id, confirmed) | Answers an approval card. Draw cards only from proposals, never from message text: it is what keeps text the model reads from changing what a user approves. |
open(), close(), toggle() | The assistant's open state, shared with the widget. |
prompt(text) | A function that sends text, for suggestion chips. |
voice | start({ handsFree }), stop(), press(), release(), and onLevel(listener) for an animation. |
update(options) | Replaces options. A new url, publicKey or debug reconnects. |
destroy() | Disconnects and drops every listener. |