@demystify/tools-ui
v0.4.2
Published
Design tokens + the shared Demystify Tools floating panel (search, filter pills, per-host recents, per-app pinned grid). Mode A: first-party React component, no iframe.
Maintainers
Readme
@demystify/tools-ui
Design tokens and the shared Demystify Tools floating panel — search, filter pills, per-host "recently used" memory, and a per-app pinned grid. Mode A: a first-party React component you embed via npm (no iframe), themed by your host tokens.
pnpm add @demystify/tools-ui
# peers: react >=18, react-dom >=18Quick start (e.g. Finokit)
"use client";
import { ToolsPanel, type PanelTool } from "@demystify/tools-ui";
import configs from "@demystify/tools-ui/widget-configs.json"; // per-app pins (Finokit, ASHR, …)
import "@demystify/tools-ui/panel.css"; // required
import "@demystify/tools-ui/tokens.css"; // optional Demystify theme
// Your tool list — from @demystify/tools-registry, or fetch the hosted search index once:
// const tools = (await fetch("https://tools.demystifysystem.com/search/index.json")).json()
const tools: PanelTool[] = /* [{ id, name, desc?, category?, suite?, keywords?, url? }, …] */ [];
export function FinokitToolsButton() {
return (
<ToolsPanel
tools={tools}
product="finocket" // resolves pinned_top + placement from widget-configs.json
configs={configs}
context={{ module: "invoicing", screen: "new-invoice" }} // optional: pre-filters relevant tools
onOpenTool={(tool) => openInYourSlideOver(tool)} // omit to open the hosted page in a new tab
/>
);
}That renders a draggable brand puck (and Cmd/Ctrl+K) — drag it anywhere; it remembers its spot
per host and snaps to the nearest side. Opening it shows Finokit's 8 pinned tools, a search box over
the whole catalog, filter pills (Pinned · Most used · Recent · your context · top categories), and
per-host recents + usage. No iframe, no network on mount, no tracking.
The puck
- Draggable & repositionable (iOS-style): drag to move, release to snap to the nearest edge,
position saved in
localStorageper host. Setdraggable={false}to pin it to a corner. - Branded icon: defaults to the hosted Demystify mark, with an inline SVG fallback if the image
can't load (so it works offline). Override with
logoUrl="…". - Local usage tracking: every open increments a per-tool count in
localStorage(namespaced per product) and powers the Most used pill. Read it yourself withreadUsage(namespace).
What you control
| Prop | Purpose |
|---|---|
| tools | the catalog to search/pin over (host supplies it) |
| product + configs | resolve pinned grid + placement from widget-configs.json |
| config / pinnedTop | supply a config object or pin ids directly instead |
| context | { module, screen } — adds a relevant filter pill and biases results |
| ai | in-panel Mysty chat: { enabled, send? , endpoint?, token?, model?, greeting?, suggestions? } (see below) |
| onOpenTool(tool) | the key seam — open the tool in your own slide-over/embed/route; omit → new tab |
| baseUrl | origin for tool URLs + footer links (default tools.demystifysystem.com) |
| draggable · logoUrl | drag+remember the puck (default on) · puck icon (default Demystify mark) |
| placement · trigger · hotkey | bottom-right … · fab | cmdk | fab+cmdk | none · Cmd/Ctrl+K |
| onEvent | aggregate-only telemetry (open/search/open_tool …), never PII |
| labels | i18n string overrides |
Ask Mysty — the in-panel AI chat
Pass an ai config to add an Ask Mysty tab next to Tools. It's provider-agnostic — the host
decides how it's authorized, so keys/tokens never live in this component. The tab only appears when
ai.enabled and a way to send is wired.
BYOK (the user's own key — Miatz students, "add a key", or the tools app's /api/ai proxy) — give a send:
<ToolsPanel
tools={tools} product="finocket" configs={configs}
ai={{
enabled: true,
name: "Mysty",
greeting: "Hi, I'm Mysty. Ask me about GST, invoices, anything.",
suggestions: ["Explain GST input credit", "Draft a payment reminder"],
send: async (messages, { signal }) => {
const key = getUserKey(); // from your local BYOK store
const res = await fetch("https://api.provider.com/v1/chat", {
method: "POST", signal,
headers: { authorization: `Bearer ${key}`, "content-type": "application/json" },
body: JSON.stringify({ messages }),
});
return (await res.json()).choices[0].message.content;
},
}}
/>Host-token (Finokit / ashr.work entitlement) — give an endpoint + the platform token; the
built-in adapter POSTs { messages, model } with Authorization: Bearer <token>:
ai={{ enabled: true, endpoint: "https://mysty.demystipai.com/chat", token: sessionToken, model: "mysty-1" }}The adapter reads the reply from {reply} / {content} / {message} / OpenAI-style choices[0].message.content.
Chat history is kept locally per host (localStorage, cleared with the Clear button). Nothing is
sent until the user hits Send.
Not wired yet? Leave
aioff (default) and the panel is tools-only. Setai.enabledwithout asend/endpointand the tab stays hidden until you connect one — a safe seam.
BYOK AI toolkit — one shared, self-updating catalog
The package ships the whole "bring your own key" stack so every app is consistent and models update in one place:
AI_PROVIDERS— a typed catalog of ~16 providers (free: Groq · Google Gemini · OpenRouter · Cerebras · NVIDIA · Mistral · Cohere · Together · self-hosted; paid: OpenAI · Anthropic · xAI · DeepSeek · Perplexity · Fireworks) with base URLs, key-origin links, and current models. Also served live athttps://tools.demystifysystem.com/ai-models.json(CORS) so new models roll out centrally without a release — fetch it, fall back to the bundle.<KeyManager />— a drop-in "Bring your own key" settings panel. Renders on your settings page; the user picks a provider (free ones grouped first), pastes a key, picks a model. Saved to this origin's localStorage only.- key store —
readKeys/saveKey/removeKey/activeKey/hasAnyKey/setSelectedProvider. makeBrowserSend()— anAiConfig.sendbacked by that store; the browser calls the user's chosen provider directly (OpenAI-compatible + Anthropic). Their key never touches a Demystify server.
Wire the chat to BYOK with one flag — no send needed:
import { ToolsPanel, KeyManager } from "@demystify/tools-ui";
// settings page:
<KeyManager />
// anywhere (the puck):
<ToolsPanel tools={tools} product="finocket" configs={configs}
ai={{ enabled: true, byok: true }} /> // uses the local key; if none, the chat says "add a key in Settings"Per-module AI: give each module its own
recentsKey/namespace and (if you want different models per screen) pass a per-moduleai.sendthat reads that module's chosen key. The defaultbyok:trueshares one key across the app.Free to start: tell users to grab a free key from Groq or Google AI Studio (instant, no card) —
KeyManagerlinks straight to each provider's key page.CORS note: Groq, Google, OpenRouter, Cerebras, xAI, Anthropic and self-hosted allow direct browser calls. A few providers block browser CORS — route those through a proxy (
ai.endpoint) or pick a browser-friendly one.
Theming
The panel reads a --dmt-* token bridge, falling back to Demystify --color-* tokens (if you import
tokens.css), then sane light/dark defaults. Map your host tokens once to restyle everything:
.dmt-root { --dmt-accent: var(--brand); --dmt-surface: var(--card); --dmt-radius: 10px; }Dark mode follows prefers-color-scheme; pin it with [data-dmt-theme="light"|"dark"] on any ancestor.
Recents
Stored locally per host (localStorage, namespaced by product/recentsKey). Each app keeps its own
recents — partitioned by design, not shared across products.
Also exported
rank / score (the pure ranker), readRecents / pushRecent, readUsage / recordUsage /
mostUsedIds, readPosition / writePosition / snapToEdge / clampToViewport, resolveConfig /
resolvePinned / deriveFilters / toolUrl, the AI toolkit (AI_PROVIDERS, KeyManager,
makeBrowserSend, sendWithKey, key-store fns), and all types — so you can build a custom surface on
the same core. Data-only import: @demystify/tools-ui/providers.
Notes
widget-configs.jsonis generated at build from the repo SSOTregistry/widget-configs.json.- This ships the launcher. Running a tool in-process (engines rendered inside the panel) is the
next layer; the
onOpenToolseam is where it plugs in — until then, open the hosted tool or an embed. - License: Apache-2.0.
