@code-all/harmonyai
v0.1.0
Published
The Harmony AI chat widget for your site — one call, framework agnostic, with React, Vue and Angular adapters.
Downloads
79
Maintainers
Readme
@code-all/harmonyai
The Harmony AI chat widget, as an npm package. One call to load it, a typed API you can use before it has finished loading, and thin adapters for React, Vue and Angular.
npm install @code-all/harmonyaiYou need a widget key. It is in the Harmony merchant panel under Widgets,
and it looks like wgt_…. The key is public — it ends up in your page source
either way, and it grants access to nothing.
Quick start
import { harmony } from "@code-all/harmonyai";
harmony.load({ key: "wgt_YOUR_KEY", locale: "tr" });That is the whole integration. The launcher appears; the chat itself is not loaded until the visitor opens it.
If all you want is a script tag, you do not need this package —
<script src="https://widget.chatharmony.ai/embed.js?key=wgt_…" async></script>
does the same job. What the package adds is a queue, and that matters the
moment your app is a single-page app:
// Somewhere in your login handler, possibly before the widget has loaded.
harmony.setUser({ email: user.email, name: user.name });With a bare script tag that line is window.Harmony?.setUser(…), which does
nothing at all on the page loads where the script was still in flight — and
tells you nothing when it does. Here it is queued and delivered when the widget
arrives.
The API
| | |
|---|---|
| load(options) | puts the widget on the page. Idempotent; a no-op on the server |
| ready() | a promise that resolves when the widget is loaded |
| open() close() toggle() isOpen() | the panel |
| setUser(user \| null) | sign-up prefill — see below |
| setLocale(locale) setTheme(theme) | theme is light, dark or auto |
| on(event, fn) | returns an unsubscribe function |
| getState() subscribe(fn) | the reactive slice: { isReady, isOpen, unread } |
| destroy() | removes the widget entirely |
| config | the resolved boot config, or null before loading |
Events: open, close, ready, and unread with { count }.
const off = harmony.on("unread", ({ count }) => {
document.title = count ? `(${count}) Shop` : "Shop";
});load options
harmony.load({
key: "wgt_YOUR_KEY", // required
locale: "tr",
theme: "auto",
origin: "https://widget.chatharmony.ai", // staging or local, if you need it
});Staging, and your local machine
There is one package, not one per environment — nothing about the environment is
baked into it. origin chooses which Harmony the widget talks to, at runtime:
harmony.load({
key: import.meta.env.VITE_HARMONY_KEY,
origin: import.meta.env.VITE_HARMONY_ORIGIN, // omit for production
});| | |
|---|---|
| production | omit origin |
| staging | https://dev-widget.chatharmony.ai — with a key issued there |
A key belongs to one environment. A production key does not resolve against staging, and the widget removes itself rather than rendering something that cannot work.
setUser is prefill, not sign-in
Only email and name are read. They fill in the sign-up form's initial values
so a visitor already signed in to your site does not retype their address.
They are not an identity. The values come from JavaScript in a browser, where anyone can change them, so nothing is authorized on their basis, they are never tied to an account or a conversation, and they never appear in a URL. If the visitor is already signed in to the widget they are ignored — there is no form to fill.
They are personal data, so do not call this before your consent banner has
been accepted. Pass null on sign-out.
React
import { Harmony, useHarmonyState } from "@code-all/harmonyai/react";
// Once, at the root of your app.
<Harmony
config={{ key: "wgt_YOUR_KEY", locale: "tr" }}
user={user ? { email: user.email, name: user.name } : null}
/>;<Harmony> renders nothing. It loads the widget on mount and calls setUser
when the user prop changes; user={null} clears the prefill, and omitting the
prop entirely means the component does not manage the user at all.
const { isOpen, unread } = useHarmonyState();The hook is for rendering — a badge in your own navigation, a button that
reflects the panel. To call the API, import harmony directly; there is no hook
for that and there does not need to be, because it is a singleton.
There is no Provider, on purpose. Nothing is scoped to your component tree: the
widget renders into document.body and there is exactly one of it.
Server components are fine — the React entry carries "use client", and the
core is SSR-safe on its own.
Unmounting <Harmony> does not remove the widget. It belongs to the page,
not to your component, and React StrictMode remounts components in development —
a destroying cleanup would close an open panel and cut a streaming reply every
time. Call harmony.destroy() when you actually mean it.
Vue 3
<script setup lang="ts">
import { Harmony, useHarmony } from "@code-all/harmonyai/vue";
const { unread, isOpen } = useHarmony();
</script>
<template>
<Harmony :config="{ key: 'wgt_YOUR_KEY', locale: 'tr' }" :user="user" />
</template>useHarmony() returns refs and releases its subscription with the component
scope. Same rules as React: no plugin, no provide/inject, and unmounting
does not destroy the widget.
Angular
import { provideHarmony, HarmonyStore } from "@code-all/harmonyai/angular";
bootstrapApplication(App, {
providers: [provideHarmony({ key: "wgt_YOUR_KEY", locale: "tr" })],
});
export class Nav {
private harmony = inject(HarmonyStore);
unread = this.harmony.unread; // Signal<number>
open = () => this.harmony.api.open();
}state, isReady, isOpen and unread are signals. The adapter ships no
decorators and no templates — there is nothing here for the Angular compiler to
process, which is why the package needs no ng-packagr and does not care which
Angular version you are on beyond signals existing (16+).
Svelte, Solid, and everything else
No adapter, and none is coming — the core is already what you need.
subscribe(fn) calls fn immediately with the current state and again on every
change, which is the Svelte store contract exactly:
<script>
import { harmony } from "@code-all/harmonyai";
harmony.load({ key: "wgt_YOUR_KEY" });
</script>
<span>{$harmony.unread}</span>Anything else binds to subscribe and getState in three lines.
Server-side rendering
load() and every other method are no-ops when there is no window, and
nothing touches document at import time. Next.js, Nuxt and Angular Universal
all work without a dynamic import or a typeof window check on your side.
Consent, and removing the widget
destroy() removes the element, drops every listener and deletes the global.
Calling load() afterwards starts a fresh widget. That is the rollback path for
a visitor who withdraws consent; the safer path is not calling load() until
they have given it.
Content Security Policy
Two directives, one host:
script-src https://widget.chatharmony.ai;
frame-src https://widget.chatharmony.ai;Nothing else — no connect-src, no img-src, no style-src. Everything the
chat does happens inside a cross-origin iframe with its own policy. If your CSP
is nonce-based, it works without changes.
TypeScript
Everything is typed, and the types are the source of truth for the widget itself
rather than a description written after the fact. window.Harmony is declared
globally, so a script-tag installation is typed too.
MIT
