-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.html
More file actions
442 lines (418 loc) · 18.9 KB
/
index.html
File metadata and controls
442 lines (418 loc) · 18.9 KB
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
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Bridle embed — integration examples</title>
<script src="https://cdn.tailwindcss.com"></script>
<style type="text/tailwindcss">
@layer base {
body { @apply bg-slate-50 text-slate-900 antialiased; }
code { @apply rounded bg-slate-100 px-1 py-0.5 font-mono text-[0.85em] text-slate-800; }
pre code { @apply bg-transparent p-0 text-slate-100; }
}
</style>
</head>
<body>
<div class="mx-auto max-w-4xl px-6 py-12">
<!-- Header: explains what this page is and the current token status. -->
<header class="mb-12 text-center">
<p class="text-xs font-semibold uppercase tracking-widest text-indigo-600">CleanSlice · Bridle SDK</p>
<h1 class="mt-2 text-4xl font-bold tracking-tight">Bridle integration examples</h1>
<p class="mx-auto mt-4 max-w-xl text-slate-600">
Four ways to drop the chat widget onto a site — from a single
<code><script></code> in HEAD to programmatic init with a
dynamic token.
</p>
<p class="mx-auto mt-3 max-w-xl text-xs text-slate-500">
Latest:
interactive forms — radio / checkbox / select (v0.12.0) ·
empty-state suggestions (v0.11.0) ·
<code>data-greeting</code> (v0.10.0) ·
image attachments — drag / paste / paperclip (v0.9.0) ·
<code>data-fab-icon</code> (v0.8.2) ·
<code>data-custom-css</code> (v0.8.0)
</p>
<p id="status" class="mt-4 text-xs text-slate-500">Minting token from the backend…</p>
</header>
<!-- 1. BASIC: one <script> tag with data-* attributes. -->
<!-- The SDK registers the custom element and mounts a floating button. -->
<section class="mb-10 rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
<span class="text-xs font-semibold uppercase tracking-widest text-indigo-600">01 · basic</span>
<h2 class="mt-1 text-2xl font-semibold">Basic — one <code><script></code> tag</h2>
<p class="mt-2 text-slate-600">
The shortest path. The SDK reads <code>data-*</code> from its own
script tag, mounts a floating bubble in the corner of the page, and
opens the WebSocket itself. The Ranch API key stays on the server —
only a short-lived JWT reaches the browser via <code>data-token</code>.
</p>
<pre class="mt-4 overflow-x-auto rounded-lg bg-slate-900 p-4 text-sm"><code><script
src="https://bridle.cleanslice.org/sdk/latest.js"
data-api-url="https://hub.example.com"
data-agent-id="agent-31a6fbd1-…"
data-token="<jwt>"
<!-- v0.11.0: rich empty state with avatar, headline, suggestion chips -->
data-empty-avatar="/avatars/agent.png"
data-empty-title="How can I help?"
data-empty-subtitle="Ask anything, or pick a prompt below."
data-suggestions="Compare plans|Book a demo|Open a support ticket"
<!-- v0.10.0: pre-seed the first bubble on an empty chat -->
data-greeting="Hi! Drop a screenshot or ask me anything."
data-greeting-delay="2500"
<!-- v0.8.2: replace the default FAB glyph -->
data-fab-icon="/icons/chat.svg"
></script></code></pre>
<p class="mt-3 text-xs text-slate-500">
↘ live demo — floating button in the bottom-right corner of this page.
Open it: you'll see the typing dots for ~2.5 s, then the greeting
appears. Drag an image onto the panel, paste a screenshot, or click
the paperclip to attach it before sending.
</p>
</section>
<!-- 2. INLINE: chat is mounted inside a specific container. -->
<!-- Good for support pages or dashboard panels — no floating bubble. -->
<section class="mb-10 rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
<span class="text-xs font-semibold uppercase tracking-widest text-indigo-600">02 · inline</span>
<h2 class="mt-1 text-2xl font-semibold">Inline — embed into the page</h2>
<p class="mt-2 text-slate-600">
The <code>inline</code> mode mounts the chat right inside the given
<code>mount</code> container instead of a floating bubble. The chat
size is defined by the container size.
</p>
<div class="mt-4 grid gap-4 md:grid-cols-2">
<pre class="overflow-x-auto rounded-lg bg-slate-900 p-4 text-xs"><code>Bridle.init({
apiUrl,
agentId,
token,
mode: 'inline',
mount: '#chat-inline',
title: 'Support chat',
})</code></pre>
<div id="chat-inline" class="h-[520px]"></div>
</div>
</section>
<!-- 3. STYLES: theme + CSS variables + custom CSS injected into shadow root. -->
<!-- themeVars forward any --bridle-* CSS custom property. -->
<!-- customCss is injected straight into the shadow root — needed to style -->
<!-- internal classes (.bridle__panel, etc.) that host CSS can't reach. -->
<section class="mb-10 rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
<span class="text-xs font-semibold uppercase tracking-widest text-indigo-600">03 · styles</span>
<h2 class="mt-1 text-2xl font-semibold">Styles — theme, variables and custom CSS</h2>
<p class="mt-2 text-slate-600">
Built-in palettes (<code>default</code> / <code>cleanslice</code>),
overrides via CSS custom properties, and your own CSS injected into
the shadow root for the widget's internal classes.
</p>
<div class="mt-4 grid gap-4 md:grid-cols-2">
<pre class="overflow-x-auto rounded-lg bg-slate-900 p-4 text-xs"><code>Bridle.init({
apiUrl,
agentId,
token,
mode: 'inline',
mount: '#chat-styled',
theme: 'cleanslice',
colorMode: 'light',
themeVars: {
'--bridle-primary': '#6366f1',
'--bridle-radius': '14px',
},
customCss: `
.bridle__panel {
border: 1px solid #c7d2fe;
box-shadow: 0 8px 24px rgba(99,102,241,0.18);
border-radius: 14px;
}
`,
})</code></pre>
<div id="chat-styled" class="h-[520px]"></div>
</div>
</section>
<!-- 4. AUTHENTICATOR: token is passed as a function instead of a string. -->
<!-- The SDK awaits it on mount (and can call it again to refresh). -->
<!-- RANCH_API_KEY never reaches the browser — our backend fetches the token. -->
<section class="mb-10 rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
<span class="text-xs font-semibold uppercase tracking-widest text-indigo-600">04 · authenticator</span>
<h2 class="mt-1 text-2xl font-semibold">Authenticator — token as a function</h2>
<p class="mt-2 text-slate-600">
Instead of a string in <code>token</code>, pass a function: the SDK
will call it and await the <code>Promise<string></code>. Under
the hood the function hits your own backend for a fresh JWT — the
Ranch API key stays server-side.
</p>
<div class="mt-4 grid gap-4 md:grid-cols-2">
<pre class="overflow-x-auto rounded-lg bg-slate-900 p-4 text-xs"><code>Bridle.init({
apiUrl,
agentId,
// called before connecting —
// re-mint the token here if needed
token: async () => {
const r = await fetch('/embed/token?sub=user-42')
if (!r.ok) throw new Error(`token ${r.status}`)
const { token } = await r.json()
return token
},
mode: 'inline',
mount: '#chat-authn',
onReady: () => console.log('[bridle] ready'),
onError: (e) => console.error('[bridle] error', e),
})</code></pre>
<div id="chat-authn" class="h-[520px]"></div>
</div>
</section>
<!-- 5. ONBOARDING (v0.11.0): empty state with avatar + headline + chips. -->
<!-- All four fields are independent — set any subset to enable it. -->
<!-- Hidden the moment messages start arriving or the agent types. -->
<section class="mb-10 rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
<span class="text-xs font-semibold uppercase tracking-widest text-indigo-600">05 · onboarding</span>
<h2 class="mt-1 text-2xl font-semibold">Empty state — avatar, headline, suggestion chips</h2>
<p class="mt-2 text-slate-600">
Replace the default <em>Start a conversation</em> placeholder with
a real onboarding panel. Click a chip and it sends as a user
message; the whole block disappears the moment the conversation
begins. Stacks naturally on top of <code>greeting</code> and
<code>prompt</code>.
</p>
<div class="mt-4 grid gap-4 md:grid-cols-2">
<pre class="overflow-x-auto rounded-lg bg-slate-900 p-4 text-xs"><code>Bridle.init({
apiUrl,
agentId,
token,
mode: 'inline',
mount: '#chat-onboarding',
emptyAvatar: '/avatars/agent.png',
emptyTitle: 'How can I help?',
emptySubtitle: 'Ask anything, or pick a prompt.',
suggestions: [
'Compare plans',
'Book a demo',
'How does theming work?',
],
})</code></pre>
<div id="chat-onboarding" class="h-[520px]"></div>
</div>
</section>
<!-- 6. INTERACTIVE FORMS (v0.12.0): agent → browser `ui` part renders -->
<!-- a form inside the bubble; visitor → agent `ui_submit` part ships -->
<!-- the values back. Capability-gated on `msg.capabilities.includes('ui')` -->
<!-- so non-Bridle channels (Telegram, etc.) fall back to plain text. -->
<section class="mb-10 rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
<span class="text-xs font-semibold uppercase tracking-widest text-indigo-600">06 · forms</span>
<h2 class="mt-1 text-2xl font-semibold">Interactive forms — radio, checkbox, select</h2>
<p class="mt-2 text-slate-600">
The agent renders a form <em>inside its chat bubble</em> and gets
the answers back as a regular user message — wire-level it's just
one new <code>ui</code> part agent→browser and <code>ui_submit</code>
browser→agent. Used end-to-end this looks like a one-off settings
flow inside the conversation.
</p>
<div class="mt-4 grid gap-4 md:grid-cols-2">
<pre class="overflow-x-auto rounded-lg bg-slate-900 p-4 text-xs"><code>// In your agent runtime:
import { buildUiForm } from '@cleanslice/bridle/runtime'
bridle.onMessage(async (msg) => {
if (!msg.capabilities?.includes('ui')) {
// Telegram / old SDK fallback
await bridle.send(msg.from,
'Reply with basic, pro, or team')
return
}
const form = buildUiForm([
{ type: 'heading', text: 'Pick a plan' },
{
type: 'radio', name: 'plan',
required: true, default: 'basic',
options: [
{ value: 'basic', label: 'Basic — $0' },
{ value: 'pro', label: 'Pro — $10' },
{ value: 'team', label: 'Team — $30' },
],
},
{
type: 'checkbox', name: 'newsletter',
label: 'Weekly updates',
},
], { uiId: 'plan-2026-05', submitLabel: 'Continue' })
await bridle.send(msg.from,
'Quick one before we get you set up:',
[{ type: 'text', text: 'Quick one before we get you set up:' }, form])
})
// Next onMessage carries the answers:
bridle.onMessage(async (msg) => {
for (const part of msg.parts) {
if (part.type === 'ui_submit'
&& part.uiId === 'plan-2026-05') {
const { plan, newsletter } = part.values
// provision + ack...
}
}
})</code></pre>
<div class="flex flex-col gap-2">
<button
type="button"
id="form-trigger"
class="self-start rounded-lg border border-indigo-200 bg-indigo-50 px-3 py-1.5 text-xs font-semibold text-indigo-700 transition hover:bg-indigo-100 disabled:opacity-50"
disabled
>Send <code>/form</code> to agent</button>
<div id="chat-forms" class="h-[520px]"></div>
</div>
</div>
</section>
<footer class="mt-12 border-t border-slate-200 pt-6 text-center text-xs text-slate-500">
SDK v<span id="sdk-version">?</span> ·
<a class="underline hover:text-slate-700" href="https://bridle.cleanslice.org">documentation</a>
</footer>
</div>
<script>
// Loading flow:
// 1) Fetch tokens from our own /embed/token (the backend talks to Ranch).
// 2) Insert the exact <script data-*> an integrator would put on their
// site. The SDK loads, the IIFE exposes window.Bridle and auto-mounts
// the first widget from its own dataset.
// 3) On its onload, mount the remaining 3 widgets via Bridle.init().
//
// Each token uses a different `sub` so the four sockets don't compete
// for the same clientId in the hub.
(async () => {
const status = document.getElementById('status');
const setError = (msg) => {
status.textContent = msg;
status.classList.remove('text-slate-500');
status.classList.add('text-rose-600');
};
try {
const [basic, inline, styled, onboarding, forms] = await Promise.all(
['demo-basic', 'demo-inline', 'demo-styled', 'demo-onboarding', 'demo-forms'].map(mintToken),
);
// 01 · basic — REAL <script data-*>. This is exactly what an
// integrator drops onto their site: src + data-api-url +
// data-agent-id + data-token. The SDK reads its dataset and mounts
// a floating button in the corner of the page.
const sdk = document.createElement('script');
sdk.src = '/sdk/latest.js';
sdk.dataset.apiUrl = basic.apiUrl;
sdk.dataset.agentId = basic.agentId;
sdk.dataset.token = basic.token;
sdk.dataset.title = 'Chat';
// v0.11.0 — rich empty state: visitor sees suggestion chips
// before they type. Clicking a chip fires the prompt for them.
sdk.dataset.emptyTitle = 'How can I help?';
sdk.dataset.emptySubtitle = 'Ask anything, or pick a prompt below.';
sdk.dataset.suggestions =
'Compare plans|Book a demo|How does theming work?';
// v0.10.0 — typing dots for 2.5s, then this message lands as the
// first assistant bubble. Cancels if the user types first.
sdk.dataset.greeting =
'Hi! Drop a screenshot or ask me anything.';
sdk.dataset.greetingDelay = '2500';
sdk.onerror = () => setError('Failed to load /sdk/latest.js');
sdk.onload = () => {
document.getElementById('sdk-version').textContent = window.Bridle.version;
status.textContent = 'Ready — 6 widgets mounted.';
// 02 · inline — mount inside #chat-inline.
Bridle.init({
apiUrl: inline.apiUrl,
agentId: inline.agentId,
token: inline.token,
mode: 'inline',
mount: '#chat-inline',
title: 'Support chat',
});
// 03 · styles — cleanslice theme + CSS variables + CSS injection
// into the shadow root.
Bridle.init({
apiUrl: styled.apiUrl,
agentId: styled.agentId,
token: styled.token,
mode: 'inline',
mount: '#chat-styled',
title: 'Helper',
theme: 'cleanslice',
colorMode: 'light',
themeVars: {
'--bridle-primary': '#6366f1',
'--bridle-radius': '14px',
},
customCss: `
.bridle__panel {
border: 1px solid #c7d2fe;
box-shadow: 0 8px 24px rgba(99,102,241,0.18);
border-radius: 14px;
}
`,
});
// 04 · authenticator — token as a function; the SDK calls it itself.
// apiUrl/agentId are reused (it's the same agent).
Bridle.init({
apiUrl: basic.apiUrl,
agentId: basic.agentId,
mode: 'inline',
mount: '#chat-authn',
title: 'Auth via token()',
token: async () => {
const r = await fetch('/embed/token?sub=demo-authn');
console.log('r', r)
if (!r.ok) throw new Error(`token ${r.status}`);
const { token } = await r.json();
console.log('token', token)
return token;
},
onReady: () => console.log('[bridle] authn ready'),
onError: (e) => console.error('[bridle] authn error', e),
});
// 05 · onboarding — rich empty state with avatar, headline,
// sub-line and pre-set suggestion chips. Click a chip and it
// fires as a regular user message; the panel hides itself.
Bridle.init({
apiUrl: onboarding.apiUrl,
agentId: onboarding.agentId,
token: onboarding.token,
mode: 'inline',
mount: '#chat-onboarding',
title: 'Welcome desk',
emptyTitle: 'How can I help?',
emptySubtitle: 'Ask anything, or pick a prompt below.',
suggestions: [
'Compare plans',
'Book a demo',
'How does theming work?',
],
});
// 06 · forms — same agent; the "Send /form" button below uses
// the returned instance's sendMessage() to fire the trigger
// text on the visitor's behalf. The agent must answer `/form`
// with a `ui` part for the renderer to kick in — see the
// snippet on the left of this section.
const formsChat = Bridle.init({
apiUrl: forms.apiUrl,
agentId: forms.agentId,
token: forms.token,
mode: 'inline',
mount: '#chat-forms',
title: 'Forms demo',
emptyTitle: 'Try the form flow',
emptySubtitle: 'Send /form (or type freely) to see the agent emit a ui part.',
});
const formTrigger = document.getElementById('form-trigger');
if (formTrigger) {
formTrigger.disabled = false;
formTrigger.addEventListener('click', () => {
formsChat.sendMessage('/form');
});
}
};
document.head.appendChild(sdk);
} catch (err) {
console.error(err);
setError(`Init failed: ${err.message}`);
}
})();
async function mintToken(sub) {
const r = await fetch(`/embed/token?sub=${encodeURIComponent(sub)}`);
if (!r.ok) {
const body = await r.json().catch(() => ({}));
throw new Error(body.error || `token endpoint ${r.status}`);
}
return r.json(); // { token, apiUrl, agentId, expiresAt }
}
</script>
</body>
</html>