1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
|
# Using the WebX11 Chat Widget
## 1. Files
```
widget.html the widget (HTML + CSS + JS, self-contained)
config.example.js copy to config.js and edit
```
Both files must sit in the same directory, because `widget.html` loads
`config.js` with a relative `<script src="config.js">`. If `config.js` is
missing the widget still renders, but it will refuse to send and prints
“not configured” in the panel.
## 2. Configure
```bash
cp config.example.js config.js
```
Open `config.js` and set at least these:
```js
window.WX_WIDGET_CONFIG = {
endpoint: 'https://YOUR-HOST/ollama', // your gateway, /api/generate is appended
model: 'llama3.2:latest',
title: 'Chat with us',
accentColor: '#00e5a0',
welcomeMessage: 'Hi! How can I help you today?',
systemPrompt: 'You are a helpful assistant for …'
};
```
Notes:
- `endpoint` may be a base URL (`https://YOUR-HOST/ollama`) or the full
generate URL (`https://YOUR-HOST/ollama/api/generate`); the widget appends
`/api/generate` only when it is not already there.
- A relative endpoint such as `/ollama` works if the widget and the gateway
share an origin.
- `accentColor` accepts `#rgb` or `#rrggbb`. Bubble/button text switches
between dark and light automatically for contrast.
- `chips` is an array of `[label, message]` pairs. An empty array hides the row.
- `promptUrl` is optional. If set, the widget fetches the system prompt from
that plain-text URL at load, retries once, and falls back to the inline
`systemPrompt` on failure.
### About the API key — read this once
The widget is browser code. Every byte of `config.js` and `widget.html` is
visible to any visitor, so **no key, token or password may ever be placed in
them**. Your gateway is responsible for authentication: it holds the key in
a server-side secret (environment variable or root-owned file) and adds it to
the upstream request. The browser only ever talks to your gateway.
## 3. Embed in an existing site
`widget.html` is a working demo page. To embed, copy two blocks out of it into
your own template:
1. The `<div id="wx-chat-widget"> … </div>` block (the markup).
2. The `<style> … </style>` block, and the `<script>` block that follows it.
Then add one tag before them:
```html
<script src="/path/to/config.js"></script>
```
Order matters: `config.js` must be loaded before the widget script runs, and
the widget script must run after the DOM nodes exist (place it at the end of
`<body>`, or wrap your own init in `DOMContentLoaded`).
Minimal page:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>My site</title>
<!-- paste the widget <style> block here, or link it from a .css file -->
</head>
<body>
<h1>My site</h1>
<!-- paste <div id="wx-chat-widget"> … </div> here -->
<!-- paste the widget <script> block here -->
<script src="/path/to/config.js"></script>
</body>
</html>
```
The widget uses `wx-` prefixed ids and classes and all CSS is scoped to
`#wx-chat-widget`, so it will not collide with your site's styles. It sets
`z-index: 2147483000`; lower it in your CSS if you have a cookie banner that
must sit above the bubble.
## 4. The endpoint contract
The widget sends:
```
POST <endpoint>
Content-Type: application/json
{"model":"<model>","prompt":"<systemPrompt>\n\nUser Question: <text>","stream":true}
```
and expects newline-delimited JSON (Ollama `/api/generate` format), where each
line has a `response` string:
```
{"model":"llama3.2:latest","response":"Hello","done":false}
{"model":"llama3.2:latest","response":" there","done":false}
{"model":"llama3.2:latest","response":"","done":true}
```
Server-sent events (`data: {...}`) and a non-streaming JSON reply with a
`response` field are also accepted, so most OpenAI-style shims work if they
emit one of those shapes.
## 5. Gateway examples
nginx reverse proxy in front of a local Ollama:
```nginx
location /ollama/ {
proxy_pass http://127.0.0.1:11434/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_buffering off; # streaming
proxy_read_timeout 300s;
# your key is added here, server side, e.g.
# proxy_set_header Authorization "..." ; (kept out of this doc on purpose)
}
```
Ollama itself refuses cross-origin browser calls unless the origin is allowed:
```bash
# systemd drop-in or shell environment for the ollama service
OLLAMA_ORIGINS="https://www.example.com,https://example.com"
```
Caddy:
```caddy
handle_path /ollama/* {
reverse_proxy 127.0.0.1:11434
}
```
## 6. Troubleshooting
**Nothing happens when I open the page.**
Check the browser console. A 404 on `config.js` means the file is not next to
`widget.html` or the path in the `<script src>` is wrong.
**“This chat is not configured yet.”**
`endpoint` is empty in `config.js`, or `config.js` failed to load.
**“Sorry — I could not reach the chat service.” with `TypeError: Failed to fetch`.**
This is almost always CORS. The widget runs on *your* domain, so the endpoint
must return `Access-Control-Allow-Origin` for that exact origin, and must
answer the `OPTIONS` preflight (the request carries
`Content-Type: application/json`, which is not a “simple” request).
The endpoint must allow the customer's origin — every origin that embeds the
widget, including `http://localhost:8000` while testing:
```
Access-Control-Allow-Origin: https://www.example.com
Access-Control-Allow-Methods: POST, OPTIONS
Access-Control-Allow-Headers: Content-Type
```
`Access-Control-Allow-Origin: *` also works and is fine for a public widget.
Do not combine `*` with credentials; the widget sends no credentials.
Quick check from a terminal:
```bash
curl -i -X POST https://YOUR-HOST/ollama/api/generate \
-H 'Origin: https://www.example.com' \
-H 'Content-Type: application/json' \
-d '{"model":"llama3.2:latest","prompt":"hi","stream":false}'
```
If the response has no `Access-Control-Allow-Origin` header, the browser will
block it no matter what the widget does.
**HTTP 401 / 403.**
The gateway is rejecting the request. The key belongs on the gateway, not in
`config.js`. Fix the server-side secret.
**HTTP 404.**
Wrong path. Confirm the gateway exposes `/api/generate` (the widget appends it
to whatever base you configured).
**The reply appears all at once instead of streaming.**
A proxy is buffering. In nginx set `proxy_buffering off;` for that location.
Cloudflare and similar CDNs may also buffer unless the response is chunked.
**The model name is rejected.**
`model` must match exactly what your backend lists (`ollama list`). If you use
a cloud/tagged model, include the tag.
**Nothing is remembered between messages.**
By design in 1.0.0 — see the limitation note in `README.md`. Each request
contains the system prompt plus the current question only.
**Styles look wrong inside my site.**
Your page probably sets `* { box-sizing }` or global button styles that reach
into the widget. All widget rules are scoped to `#wx-chat-widget`, but global
rules with higher specificity can still win; add `!important` overrides
targeting `#wx-chat-widget` rather than editing the widget source.
**The bubble overlaps my cookie banner or WhatsApp button.**
Change `position` in `config.js`, or override in your CSS:
```css
#wx-chat-widget { bottom: 96px; }
```
## 7. Support
Thirty days of setup help are included with the Setup Package. When asking for
help, include: the page URL, the browser console error, the failing request's
status code, and the `endpoint` value with the hostname replaced by
`YOUR-HOST` (never send a key).
|