The widget
The orb, your own trigger, or your own UI entirely.
The widget is a convenience, not a requirement. Pick one of three levels.
The examples are React; without React, mountWidget gives you the same widget
as a web component, with the same options. See the JavaScript
SDK.
1. The orb
<AssistantWidget />A circle in the corner of your app. Tapping it opens a small menu of chat and the voice modes you allow, or opens chat straight away when voice is off. Chat opens the conversation panel. Voice stays in the orb, so your page stays visible while the assistant navigates and changes it: a ring shows it listening, thinking or speaking, and a confirmation card pops out of the orb when an action needs one.
| Prop | Default | Meaning |
|---|---|---|
form | "orb" | "classic" for a launcher that opens the panel, with voice inside it. |
draggable | false | Let the user drag the orb to either side. It remembers where. |
corner | "bottom-right" | Or "bottom-left", where it sits until dragged. |
voiceModes | both | Which of "hands-free" and "hold" the menu offers, when voice is on. |
showLauncher | true | Render the orb, or the classic launcher. Turn it off when you have your own trigger. |
defaultOpen | false | Open the panel on first mount. |
launcherIcon | your uploaded icon, or the Mihe mark | Any element, drawn inside the circle. |
title | the assistant's name from the dashboard | The panel's header. |
placeholder | — | The input's placeholder. |
style | — | Merged over the panel's own styles. |
form, draggable, corner and the voice modes are also settings, under
Widget and Voice in the app's settings in the dashboard. They reach the
widget when it connects, so changing one needs no deploy. A prop in your code
overrides the setting, except voiceModes, which can only narrow it: a mode
switched off in the dashboard is refused by the runtime, so the widget never
offers it.
The widget is styled inline, so it neither leaks into your page's CSS nor picks it up.
2. Your trigger, our panel
Anything can open the assistant. useAssistantTrigger() returns the props that
make an element a proper button, including role, tabIndex and the keyboard
handler, since a div with only an onClick is invisible to keyboards and
screen readers:
function AskButton() {
const trigger = useAssistantTrigger();
if (!trigger) return null; // no assistant configured here
return <div {...trigger}>Ask the assistant</div>;
}
<AskButton />
<AssistantWidget showLauncher={false} />For a keyboard shortcut or a command palette, use the controls directly:
const { open, close, toggle, isOpen } = useAssistant();For suggestion chips and help links, useAssistantPrompt sends a message and
opens the panel in one call:
const ask = useAssistantPrompt("How do I add a product?");
return ask && <button onClick={ask}>How do I add a product?</button>;Both hooks return null when there is no assistant, so shared navigation can
render nothing rather than a control that does not work.
3. Your panel too
AssistantPanel is the conversation with no launcher and no fixed position.
Put it in a sidebar, a drawer or a tab:
<aside style={{ width: 360 }}>
<AssistantPanel style={{ height: "100%" }} />
</aside>Or build your own entirely. Everything the panel renders is on
useAssistant(): messages, proposals, busy, connection, lastError,
send and respondToProposal.
Render confirmation cards from proposals
If you build your own UI, render each card from the proposals payload,
never from the assistant's text. Its label, description and input come
from your registry, which is what stops text smuggled into your data from
changing what a user thinks they are approving. High-risk actions need that
tap, and a typed "yes" is refused for them.
The launcher icon
The launcher shows the Mihe mark until you upload your own under Widget
icon in the app's settings. The upload reaches the widget when it connects,
so changing it needs no deploy. launcherIcon in code overrides both.
If your app sends a Content-Security-Policy, an uploaded icon needs the storage
domain in img-src (*.digitaloceanspaces.com). Without it the image is
blocked, and the launcher falls back to the Mihe mark. MiheMark is exported
for a trigger of your own that should match.