Widget
Install snippet, attribute table, bubble vs inline, theming, CSP.
Embed <harnix-chat> with one script tag and one custom element:
<script src="https://api.harnix.vn/v2/widget/v1/widget.js" defer></script>
<harnix-chat pk="pk_live_..." title="Ask us"></harnix-chat>The widget has no runtime dependency and reads nothing from the host page
beyond the attributes you set on <harnix-chat>.
Attribute table
Exactly one of pk, token-url, or token is required.
| Attribute | Required | Description |
|---|---|---|
pk | One of three | The organization's anonymous publishable key. |
token-url | One of three | A URL on your backend that returns a chat session token. |
token | One of three | A chat session token you fetched yourself beforehand. |
end-user-id | No | Your end-user's id, used with user-hash and expires-at to sign pk. |
user-hash | No | HMAC signature of end-user-id + expires-at with the identity secret. |
expires-at | No | Unix timestamp the user-hash signature expires at. |
api-url | No | Overrides the API URL (defaults to the URL the widget script was loaded from). |
agent | No | Picks a specific agent when the organization has more than one. |
mode | No | bubble (default) or inline. |
position | No | right (default) or left — bubble mode only. |
theme | No | auto (default), light, or dark. |
primary-color | No | A #rgb/#rrggbb color for the widget's accent. |
logo | No | A logo image URL shown on the widget. |
title | No | The title shown at the top of the widget. |
greeting | No | The default greeting shown when the widget opens. |
strings | No | A JSON string overriding visible labels — e.g. to localize them. |
Overriding labels
You can override widget labels through the strings attribute. The available keys are:
| Key | Default |
|---|---|
| rateLimited | Too many messages, try again in {n} s |
| queueFull | Assistant is busy, try again in {n} s |
| budgetExceeded | Daily limit reached, please try again later |
| approvalTitle | Confirm this action |
| approvalPlaceholder | Confirm the action above to continue |
| approve | Approve |
| approveRemember | Approve for this conversation |
| deny | Decline |
| approvalExpiresIn | Expires in {time} |
| approvedLine | Approved · {tool} |
| approvedRememberLine | Approved for this conversation · {tool} |
| deniedLine | Declined · {tool} |
| expiredLine | Expired · {tool} |
| questionPlaceholder | Pick above or type your answer… |
| questionOther | Other |
| questionOtherPlaceholder | Type your own answer… |
| questionMultiHint | Choose one or more |
| questionSubmit | Send |
| questionExpiresIn | Expires in {time} |
| questionExpiredLine | Question expired |
In rateLimited and queueFull, {n} is the seconds left; in approvalExpiresIn and questionExpiresIn, {time} is the time left as "M:SS"; {tool} is the name of the tool being confirmed.
An invalid enum value (e.g. position="up") falls back to the design's
default instead of breaking the whole widget.
Confirming an action
A connector tool may change data — create an order, send an email, update a ticket. Such tools never run silently. When the agent picks one to call, the widget shows a confirmation card listing the connector's name, the tool's name, and the real arguments the agent is about to send:
- Approve calls the tool right away with those arguments.
- Approve for this conversation calls the tool and remembers this decision for the whole conversation — the same tool won't ask again later in this conversation.
- Decline skips the tool; the assistant tells the user and the conversation continues normally.
A countdown at the top shows "Expires in 4:32" and counts down; if you don't answer within 5 minutes, the action is treated as declined. The message input is locked while waiting — you cannot send another message until the confirmation is resolved. If you reload the page, the widget restores the card from the server and reconnects the stream to keep receiving the answer.
Answering a question
When an agent calls the ask_user tool to clarify user intent, the widget renders a question card inline within the message stream:
- 1–4 questions: Each question shows a header chip, the question text, and selectable options (radio for single-select, checkbox for multi-select with a "Choose one or more" hint).
- Typing your own answer (Other): Each question includes an Other row with a text input allowing the end-user to type a custom answer instead of picking a predefined option.
- Tap to send: If there is only one single-select question and the "Other" field is empty, tapping an option submits the answer immediately without requiring an extra click on Send.
- Send button: For multi-select or multi-question cards, the Send button enables only when every question has an answer (either a selected option or a non-empty Other response).
- Typing in the chat composer: While a question card is open, the message input at the bottom remains unlocked (with the placeholder "Pick above or type your answer…"). The user can simply type an answer in the composer and send it as normal; the server records this message as the answer to the waiting question, the card disappears, and the assistant stream continues within the same run.
- Countdown and expiry: The footer shows a countdown timer "Expires in M:SS" (default 10 minutes). If the time expires without an answer, the card collapses into a single "Question expired" line and the model continues with existing context.
- Reload restoration: If the page is reloaded while a question is pending, the widget restores the question card from the server and reopens the stream so the response can stream in.