Integration at a glance
- Provision the Streamlit deployment for each persona and expose it through your SSL-terminated domain.
- Drop the launcher button and iframe shell into your host app and pin the widget origin.
- Wait for
chat-embed-ready
, postchat-embed-config
, and keep the URL fallback active. - Style the launcher, enforce persona metadata, and verify the flow across all environments.
1. Provision the Streamlit widget
The static JavaScript bundle has been retired. Run the Streamlit client from
agent_project/clients/streamlit_client
and publish one deployment per persona behind your HTTPS
reverse proxy. Keep a fourth CHAT_APP_VARIANT=test
instance for diagnostics.
https://chat-public.jasserknows.online/
→ anonymous visitor support (CHAT_APP_VARIANT=public
).https://chat-customer.jasserknows.online/
→ authenticated customer workflows (CHAT_APP_VARIANT=customer
).https://chat-employee.jasserknows.online/
→ internal enablement (CHAT_APP_VARIANT=employee
).https://chat-test.jasserknows.online/
(or an equivalent subdomain) → full control surface for QA (CHAT_APP_VARIANT=test
).
Terminate TLS at your proxy (for example NGINX), forward requests to the Streamlit process, and store
agent_api_key
and related secrets server-side through environment variables or .streamlit/secrets.toml
.
Each persona runs as an independent service so you can roll updates without interrupting the others.
streamlit run app.py --server.port 8501
and proxy the root domain through NGINX so iframe URLs stay stable across environments.
2. Scaffold the host markup
Add the launcher button and iframe shell to your layout. Record the canonical origin in a
data-origin
attribute so your script can enforce postMessage checks and rebuild the URL for the
fallback path.
<button id="chat-launcher" class="chat-launcher" type="button" aria-expanded="false">
Open Customer Chat
</button>
<div id="chat-frame" class="chat-frame" hidden>
<iframe
id="chat-iframe"
src="https://chat-customer.jasserknows.online"
data-origin="https://chat-customer.jasserknows.online"
allow="clipboard-write"
title="Customer Support"
loading="lazy"
></iframe>
</div>
min(420px, 100vw - 2rem)
and increase the bottom offset on mobile so launchers never overlap the device’s gesture area.
3. Implement the handshake
Listen for chat-embed-ready
before posting configuration, persist the last user ID, and always
rebuild the iframe URL with the same parameters. The fallback keeps the widget functional if
postMessage
is delayed or blocked by browser policies.
const chatIframe = document.getElementById('chat-iframe');
const defaultOrigin = chatIframe.dataset.origin || chatIframe.src;
const JASSER_KNOWS_URL = window.CHAT_WIDGET_ORIGIN || defaultOrigin;
chatIframe.src = JASSER_KNOWS_URL;
const WIDGET_ORIGIN = new URL(JASSER_KNOWS_URL, window.location.href).origin;
const launcher = document.getElementById('chat-launcher');
const frameWrapper = document.getElementById('chat-frame');
let configSent = false;
let iframeReady = false;
let lastValidUserId = null;
function buildUrlWithParams(baseUrl, params) {
const url = new URL(baseUrl, window.location.href);
Object.entries(params).forEach(([k, v]) => {
if (v !== undefined && v !== null) url.searchParams.set(k, String(v));
});
return url.toString();
}
function sendConfig(userId) {
if (configSent || !iframeReady || !chatIframe.contentWindow) {
if (!iframeReady) {
console.debug('Config not sent: iframe is not ready.');
}
return;
}
const numericId = Number(userId);
if (!Number.isInteger(numericId) || numericId <= 0) {
console.debug(`Config not sent: user_id must be a positive integer (received "${userId}").`);
return;
}
const payload = {
mode: 'customer',
user_id: numericId,
require_user_id: true,
allow_generated_id: false,
direction: document.documentElement.dir || 'ltr'
};
chatIframe.contentWindow.postMessage(
{ type: 'chat-embed-config', payload },
WIDGET_ORIGIN
);
configSent = true;
console.debug(`Sent chat-embed-config payload with user_id=${numericId}`);
}
launcher.addEventListener('click', () => {
const isOpen = !frameWrapper.hasAttribute('hidden');
if (isOpen) {
frameWrapper.setAttribute('hidden', '');
launcher.setAttribute('aria-expanded', 'false');
launcher.textContent = 'Open Customer Chat';
configSent = false;
} else {
const userId = window.currentCustomerId;
if (!userId || userId <= 0) {
console.error('Cannot open chat: valid user_id is required');
return;
}
lastValidUserId = userId;
frameWrapper.removeAttribute('hidden');
launcher.setAttribute('aria-expanded', 'true');
launcher.textContent = 'Close Chat';
// URL fallback: reload iframe with configuration in query params
try {
const urlWithParams = buildUrlWithParams(JASSER_KNOWS_URL, {
mode: 'customer',
user_id: userId,
require_user_id: true,
allow_generated_id: false,
direction: document.documentElement.dir || 'ltr',
v: Date.now()
});
chatIframe.src = urlWithParams;
console.debug('Applied URL fallback with user_id to iframe src');
} catch (e) {
console.error('Failed to apply URL fallback: ' + e.message);
}
sendConfig(userId);
}
});
window.addEventListener('message', (event) => {
if (event.origin !== WIDGET_ORIGIN) return;
if (event.data?.type === 'chat-embed-ready') {
console.debug('Widget is ready to receive configuration.');
iframeReady = true;
const isOpen = !frameWrapper.hasAttribute('hidden');
if (isOpen && !configSent && lastValidUserId) {
console.debug('Sending deferred config payload.');
sendConfig(lastValidUserId);
}
}
if (event.data?.type === 'chat-embed-ack') {
console.debug('Widget acknowledged configuration payload');
}
});
event.origin
, keep the fallback origin in
sync with your reverse proxy route, and validate user_id values before sending them to prevent unauthorized access.
4. Beginner configuration walkthrough
New to embeds? Follow these explicit steps to wire Jasser Knows into a plain HTML page. Once the walkthrough succeeds, move the same markup and script into your product's template or component framework (React, Vue, Angular, Blade, etc.).
-
Create a scratch HTML file (for example
jasser-knows-demo.html
) in your workspace and paste the launcher and iframe markup from step 2 inside the<body>
tag. -
Under the markup, add a
<script>
block that contains the handshake code from step 3. Setwindow.currentCustomerId
to a test numeric value (e.g.,12345
) to validate the flow. -
Serve the file through your usual front-end dev server (for example
python3 -m http.server 8080
) or open it directly in the browser. When you click the launcher you should see the iframe open, the URL parameters applied as fallback, and then receive achat-embed-ready
event in the console, followed by thechat-embed-ack
confirmation. If readiness or ack never arrives, confirm the widget URL is correct, check for blocked mixed-content requests, and review the browser console for CSP or CORS errors. -
Replace the test
window.currentCustomerId
with the real identifier from your application. In a server-rendered site you can inline it via a template variable; in a SPA you can set it before the script runs or fetch it from your session API:
// Example: pulling from an API response
fetch('/api/session').then(res => res.json()).then(({ accountId }) => {
window.currentCustomerId = accountId;
});
-
Reload and test again. The console logs should show the specific
user_id
you passed in both the postMessage payload and the fallback URL parameters. The widget should acknowledge the configuration and be ready to accept messages. Ship only after you verify the correct value reaches the Agent API in request payloads.
chat-embed-ready
and chat-embed-ack
in the console, and confirm the rebuilt iframe URL carries
the same user_id
parameters in the "Network" tab. Check that iframeReady
becomes true before configuration is sent.
5. Persona payload matrix
Align the persona you embed with the metadata your host page supplies. The hosted widgets already lock the
correct CHAT_APP_VARIANT
; your job is to send a matching mode
plus any identity fields in the
handshake payload. Always wait for the chat-embed-ready
event before sending configuration.
-
Public: use
{ mode: 'public', require_user_id: false, allow_generated_id: true }
. Theuser_id
is optional but can include an analytics fingerprint for visitor telemetry correlation. -
Customer: send the authenticated account id with
{ mode: 'customer', user_id, require_user_id: true, allow_generated_id: false }
. Theuser_id
must be a positive integer. Display name and org metadata can ride alongside in future payload iterations. -
Employee: supply the SSO or HR identifier with
{ mode: 'employee', user_id, require_user_id: true, allow_generated_id: false }
. Theuser_id
must be a positive integer for audit trails and access reviews.
direction: 'rtl'
for Arabic layouts. The widget will
auto-adjust typography while keeping English available from the UI toggle.
6. Styling & UX
Extend your design system with launcher and frame rules that mirror the demo.
.chat-launcher {
position: fixed;
right: 1.5rem;
bottom: 1.5rem;
z-index: 1000;
padding: 0.85rem 1.75rem;
border-radius: 999px;
border: none;
background: linear-gradient(135deg, #2563eb, #1d4ed8);
color: #fff;
font-weight: 600;
cursor: pointer;
box-shadow: 0 24px 60px rgba(15, 23, 42, 0.25);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.chat-launcher:hover {
transform: translateY(-2px);
box-shadow: 0 28px 70px rgba(15, 23, 42, 0.32);
}
.chat-launcher:focus-visible {
outline: 3px solid #bfdbfe;
outline-offset: 3px;
}
.chat-frame {
position: fixed;
right: 1.5rem;
bottom: 5rem;
width: min(420px, calc(100vw - 2rem));
height: 560px;
box-shadow: 0 25px 60px rgba(15, 23, 42, 0.3);
border-radius: 16px;
overflow: hidden;
background: #fff;
z-index: 999;
}
.chat-frame[hidden] {
display: none;
}
.chat-frame iframe {
width: 100%;
height: 100%;
border: 0;
}
@media (max-width: 640px) {
.chat-launcher {
right: 1rem;
bottom: 1rem;
}
.chat-frame {
right: 1rem;
bottom: 4rem;
width: calc(100vw - 2rem);
height: calc(100vh - 6rem);
}
}
7. Security, observability, and SLAs
- Whitelist trusted embed origins inside the Streamlit app before acknowledging
chat-embed-config
. - Enable CSP headers on your product:
frame-src https://chat-public.jasserknows.online https://chat-customer.jasserknows.online https://chat-employee.jasserknows.online https://chat-test.jasserknows.online
. - Validate
user_id
values on both client and server to prevent unauthorized access. - Rate-limit launcher usage if embedding on high-traffic landing pages.
- Subscribe to widget logs via browser dev tools or a custom telemetry hook.
- Instrument the Agent API with request ids that match your
user_id
field. - Capture
chat-embed-ack
timing to monitor handshake latency. - Track
iframeReady
state to diagnose configuration delays.
8. Verification checklist
- Open the host page and confirm the iframe stays hidden until the launcher is pressed.
- Click the launcher and verify the
chat-embed-ready
event is received in the console. - Capture the posted payload in your browser dev tools and ensure
user_id
matches your auth context. - Wait for
chat-embed-ack
in the console and verify the response time stays under one second. - Verify the iframe URL includes the same configuration parameters as the postMessage payload (fallback mechanism).
- Send a message and confirm the Agent API receives the locked persona and metadata.
- Close and reopen the widget to ensure a fresh payload is sent,
configSent
resets, and configs apply correctly.
Resources & next steps
- Review the overview page for interactive telemetry logs.
- Share the public, customer, and employee demos with stakeholders.
- Document your final payload schema in configuration management to keep personas aligned with back-end routing.