Drop one script on your site. The chat UI lives in a Shadow DOM; the Socket.IO API and widget bundle share the same origin you host Echo on.
Put this near the end of <body> (before
</body>). Replace the host with your deployed Echo URL.
<script async src="https://YOUR-ECHO-HOST/widget/v1/widget.js"></script>
Example production host: https://echo.solopress.ai. The
/v1/ segment is the stable contract; a future breaking change
would ship as /v2/ without touching your tag until you
choose to upgrade.
document.body inside a closed Shadow
root — your site’s CSS does not affect it, and its styles do not leak
out.
localStorage keys solopoint_conversation_id
and solopoint_server_offset:<id>, so a refresh can
resume the same thread.
Set window.SolopointEchoConfig before the
widget <script> tag if you need overrides:
<script>
window.SolopointEchoConfig = {
// Socket.IO + API origin (your Echo base URL)
serverUrl: 'https://YOUR-ECHO-HOST',
// Skip auto-mount; call open() or mount() yourself
autoMount: false,
// Which corner of the panel users grab to resize.
// 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'none'
resizeCorner: 'top-left',
// Where the floating launcher sits on the viewport. See "Launcher placement" below.
position: 'bottom-right',
// Allow the visitor to drag the launcher to a new spot. Persists across reloads.
draggable: false
};
</script>
<script async src="https://YOUR-ECHO-HOST/widget/v1/widget.js"></script>
Use serverUrl when the page is served from a different domain
than Echo (typical for marketing sites). Use autoMount: false
if you only want the widget after navigation or a feature flag — then call
window.SolopointEcho.open() yourself.
resizeCorner: 'none' hides the resize affordance entirely.
Two knobs control where the floating launcher lives on the viewport and
whether visitors can move it: position and
draggable. Both are properties of
window.SolopointEchoConfig.
position pins the launcher to one of eight perimeter
anchors. Defaults to 'bottom-right'. Unknown values fall
back to the default.
| Value | Anchor | Panel grows toward |
|---|---|---|
'bottom-right' (default) | Bottom-right corner | Up + left |
'bottom-left' | Bottom-left corner | Up + right |
'bottom-center' | Bottom edge, centred | Up |
'top-right' | Top-right corner | Down + left |
'top-left' | Top-left corner | Down + right |
'top-center' | Top edge, centred | Down |
'center-right' | Right edge, vertically centred | Left |
'center-left' | Left edge, vertically centred | Right |
Set draggable: true to let visitors reposition the launcher
with a pointer drag (mouse, touch, pen). The implementation:
touch-action: none on
the launcher prevents the page from scrolling during a drag on mobile.
grab when idle,
grabbing while dragging.
When the panel opens from a dragged launcher, it always grows
upward from the launcher’s bottom-left or bottom-right
corner, matching the natural feel of the default
bottom-right preset. The launcher itself does not move on
open or close — the panel sits anchored to the same viewport corner the
launcher occupies.
With draggable: true the final coordinates are written to
the visitor’s localStorage under these keys:
solopoint_launcher_bottom — distance from the bottom of the viewport (px).solopoint_launcher_x — distance from the left edge (set when the launcher is anchored to the left half).solopoint_launcher_right — distance from the right edge (set when anchored to the right half).
On reload, saved coordinates override position. If
draggable is later set to false, the declared
position wins and saved coords are ignored. To reset a
visitor to defaults: localStorage.removeItem('solopoint_launcher_bottom')
(plus _x / _right).
The panel is resizable from the corner set by resizeCorner
(default 'top-left'; 'none' disables resizing).
On a draggable launcher this corner automatically mirrors to the
opposite side based on which half of the viewport the launcher is in,
so the grab corner is always on the side the panel grows toward. Last
chosen width and height are stored as
solopoint_widget_width /
solopoint_widget_height.
On mount the widget fetches
GET /widget/v1/config from the same Echo origin. Configure
on the server with environment variables (no embed tag changes):
WIDGET_ANNOUNCEMENT_TEXT=Maintenance scheduled for tonight at 10 PM. WIDGET_SUGGESTED_PROMPTS=["How do I track my order?","Speak to an agent"]
WIDGET_ANNOUNCEMENT_TEXT — optional banner above the header;
omit or leave empty to hide. WIDGET_SUGGESTED_PROMPTS — JSON
array or comma-separated list (max 8, 80 chars each). Chips appear only
until the visitor sends their first message; built-in defaults apply when
unset. For local demos, SolopointEchoConfig.announcement and
suggestedPrompts apply only if the fetch fails.
After the bundle loads, window.SolopointEcho exposes:
| Method | Behaviour |
|---|---|
open() |
Opens the panel and focuses the message field. Mounts first if needed. |
close() |
Closes the panel (same animation as the header control). |
toggle() |
Open ↔ close. Mounts first if needed. |
isOpen() |
Returns true / false. |
mount() |
Idempotent manual mount when autoMount: false. |
<button type="button" id="chat-cta">Chat with us</button>
<script>
document.getElementById('chat-cta').addEventListener('click', function () {
window.SolopointEcho.open();
});
</script>
document emits solopoint-echo:open and
solopoint-echo:close after transitions — handy for syncing a
CTA label. On iOS, the keyboard appears most reliably when
open() runs from a real user tap.
Your Echo operator must allow your site’s origin in two places:
CORS_ORIGIN must list every
domain that embeds the widget (include https://www. and apex
separately if both exist). This gates the Socket.IO handshake.
This page is served from your Echo instance. For deeper ops (deploy, health checks, Docker), see the project README in the repository.