@zehnmindai/desk-sdk
v0.2.1
Published
In-app support for zehn-desk: a Support screen, a launcher bubble and screenshot reports for any web app.
Maintainers
Readme
@zehnmindai/desk-sdk
In-app support for zehn-desk: a Support screen, a floating launcher and screenshot reports your users send from inside your web app. Every report lands in the same desk as your Telegram groups — AI triage, dedup, SLA, replies, analytics — with the device, the screen, the last actions and a masked screenshot attached.
- Looks like part of your app: one brand colour (in code, or set in the desk console with no code at all) becomes the whole palette; it takes your page's font and follows your own dark-mode switch.
- Framework-free core, Preact UI in a Shadow DOM (your CSS stays out, ours stays in), React bindings.
- Identity: anonymous per browser, or your signed JWT — no account creation on our side.
- Screenshot of the current screen, sensitive fields masked (
data-zd-mask, passwords, card fields), annotate before sending. - Replies show up in-app (polling); AI answers with 👍/👎; a "did this solve it?" once resolved.
- Deflection: "Search for help" on the home screen and matching published answers while the person types.
- A floating panel, a full-height drawer for portals, or mounted into your own Support page; full screen on phones.
- ≤ 1 KB loader, ~37 KB widget (gzip), four UI languages (en · ru · uz · uz-Cyrl).
5-minute quickstart
Script tag (no build step):
<script async src="https://desk.zehnmind.com/sdk/v1/zehn-desk.js" data-app-key="zd_app_…" data-api-url="https://desk-api.zehnmind.com"></script>
<script>
// a queue that works before the SDK has loaded (and after)
window.ZehnDesk = window.ZehnDesk || function () { (window.ZehnDesk.q = window.ZehnDesk.q || []).push(arguments); };
ZehnDesk("identify", "<jwt from your backend>");
</script>npm:
npm i @zehnmindai/desk-sdkimport { ZehnDesk } from "@zehnmindai/desk-sdk";
const desk = ZehnDesk.init({
appKey: "zd_app_…", // Console → Apps
apiUrl: "https://desk-api.zehnmind.com",
appVersion: "2.3.1",
locale: "auto", // en | ru | uz | uz-Cyrl | auto
mask: { selectors: [".secret"] }, // besides data-zd-mask, password and card inputs
});
desk.identify(userJwt); // when the person signs in (see "Identity")
desk.open(); // desk.open("new") · desk.open({ ticketId })
desk.on("unread", ({ messages }) => badge.textContent = messages || "");React:
import { ZehnDeskProvider, ZehnDeskSupport, ZehnDeskButton } from "@zehnmindai/desk-sdk/react";
<ZehnDeskProvider config={{ appKey, apiUrl, user: { jwt }, launcher: false }}>
<App />
<ZehnDeskButton className="help">Help</ZehnDeskButton> {/* unread in <span data-zd-unread> */}
<Route path="/support" element={<ZehnDeskSupport />} /> {/* the Support screen inline */}
</ZehnDeskProvider>Get the app key from the desk console (Console → Apps → New app); the same page shows these snippets with your values filled in.
Blend it into your app
Without code. In the desk console, Console → Apps → Appearance sets the brand colour, a logo, the greeting (per language), the reply time shown on the home screen, the header style and where the launcher sits. Every installed widget picks it up on its next session. Anything below, passed in code, wins over the console.
One colour. theme.primary takes any CSS colour, including your own variable — var(--brand) is followed live, so if your dark mode changes it, the widget follows. The text on the brand (white or dark), a brand text colour that stays legible in both themes, tints and the header gradient are worked out for you.
ZehnDesk.init({
appKey: "zd_app_…",
theme: {
primary: "var(--brand)", // or "#2563EB"; primaryContrast forces the text colour on it
mode: "auto", // default: your page's own dark switch first (a .dark class, data-theme, data-bs-theme…), then the system
font: "inherit", // default: your page's font · "system" · or a font-family
radius: 12, // panel corners; cards ¾, chips ½
},
panel: { placement: "drawer", header: "compact" }, // floating (default) | drawer · hero (default) | compact
launcher: { position: "bottom-left", offset: { y: 72 }, variant: "label", label: "Help" }, // or false
});
desk.setTheme({ mode: "dark" }); // merges over what init gotFramework recipes for primary: Bootstrap "var(--bs-primary)", shadcn/ui "hsl(var(--primary))", Tailwind v4 "var(--color-primary)", MUI (CSS variables) "var(--mui-palette-primary-main)".
Script tag, still no JavaScript: data-primary="#2563EB", data-theme="auto|system|light|dark", data-position="bottom-left", data-offset-x / data-offset-y, data-launcher="icon|label|false", data-placement="drawer", data-header="compact", data-locale.
Tokens and parts. Every colour is a CSS custom property on the zehn-desk element (--zd-primary, --zd-bg, --zd-surface, --zd-text, --zd-muted, --zd-border, --zd-success, --zd-warning, --zd-danger, --zd-radius, --zd-font, --zd-font-size, --zd-panel-width, --zd-z, …) and the widget exposes ::part() hooks (launcher, badge, panel, header, body, card, button, input, bubble, footer) — see docs/integration.md.
Identity
Anonymous users are keyed by a random device id kept in localStorage. To attach reports to a signed-in person, mint a short-lived HS256 JWT on your backend with the app's JWT secret (shown once when the app is created, rotatable):
# Python
jwt.encode({"sub": user.id, "name": user.name, "email": user.email, "iat": now, "exp": now + 3600}, APP_JWT_SECRET, algorithm="HS256")// Node (jose)
await new SignJWT({ name, email }).setProtectedHeader({ alg: "HS256" }).setSubject(user.id).setIssuedAt().setExpirationTime("1h").sign(new TextEncoder().encode(APP_JWT_SECRET));sub is required and must be stable; exp ≤ 24 h. Pass it as user: { jwt } at init or call desk.identify(jwt) later (a string, or { jwt }) — tickets the person opened anonymously on that device are merged into their account, and the home screen greets them by first name.
Screenshots and privacy
By default the widget re-renders the current page into an image when the report form opens (nothing leaves the browser until Send). Elements matching [data-zd-mask], input[type=password], [autocomplete^=cc-] and [autocomplete=one-time-code] are painted over; add your own with mask.selectors. Cross-origin images that taint the canvas produce a grey placeholder frame instead. Options: screenshot: { mode: "dom" | "display" | "off", auto: false } — display asks the browser for a real screen capture (permission prompt); screenshot.provider hands the widget your own capture (a canvas-heavy app, a native wrapper) — masking is then yours.
What a report carries: the text, the images, app version, browser/OS, viewport, locale, timezone, the current path (no query string), and the last 50 breadcrumbs (route changes, button clicks, request paths + status codes — never bodies or query strings). breadcrumbs: false turns the trail off; beforeSend lets you edit or veto every report.
Content-Security-Policy
script-src https://desk.zehnmind.com;
connect-src https://desk-api.zehnmind.com;
img-src blob: data: https://desk-api.zehnmind.com https://<your MinIO host>; // the console logo is served by the desk APIAPI
ZehnDesk.init(config) → handle · ZehnDesk.mount(el, config) → the Support screen inside your element · ZehnDesk.preview(el, options) → the real widget over sample data, no network (docs, design reviews) · handle: open(view | { ticketId }), close(), toggle(), identify(jwt | { jwt }), logout(), setContext({...}), addBreadcrumb(name, data), setLocale(l), setTheme({ mode, primary, font, radius, tokens, zIndex }), on(event, fn) (ready · open · close · unread · ticket:created · message:received · deflected · appearance · error), unreadCount, state, destroy(). The script-tag build keeps window.ZehnDesk callable after it loads: ZehnDesk("open"), ZehnDesk("identify", jwt).
Keyboard: Escape closes the panel and returns focus to where it came from; the floating panel and the drawer are non-modal, the phone full-screen panel traps focus. Enter sends a reply on a keyboard (Shift+Enter for a new line); on a phone, Enter is a new line.
Development
npm i
npm run dev # demo host app on :3011 against http://localhost:8011 (VITE_APP_KEY in demo/.env.local)
# /demo/gallery.html: every screen over sample data, no backend needed
npm run check # typecheck · lint · locales · tests · build · size-limitsrc/core/i18n/uz-Cyrl.ts is generated from uz.ts (npm run gen:uz-cyrl); never edit it by hand. Releases: conventional commits → release-please PR → tag → npm publish --provenance (CI). Pre-1.0 a feat: bumps the patch (bump-patch-for-minor-pre-major): a minor release needs a Release-As: X.Y.0 footer on its commit.
