Jasser Knows

Integrate the Jasser Knows chat widget

Follow the Streamlit-based approach for embedding the production widget, listening for the chat-embed-ready event, and falling back to URL parameters so your application keeps working even when postMessage is blocked.

Integration at a glance

  1. Provision the Streamlit deployment for each persona and expose it through your SSL-terminated domain.
  2. Drop the launcher button and iframe shell into your host app and pin the widget origin.
  3. Wait for chat-embed-ready, post chat-embed-config, and keep the URL fallback active.
  4. Style the launcher, enforce persona metadata, and verify the flow across all environments.
Tip: keep this page open while you implement; each section expands on the checklist above.

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.

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.

Deploy hint: run 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>
Responsive detail: clamp the iframe width to 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');
  }
});
Security reminder: guard the 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.).

  1. 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.
  2. Under the markup, add a <script> block that contains the handshake code from step 3. Set window.currentCustomerId to a test numeric value (e.g., 12345) to validate the flow.
  3. 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 a chat-embed-ready event in the console, followed by the chat-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.
  4. 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;
});
  1. 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.
Debug helper: keep the browser dev tools open while testing. Watch for 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.

Locale support: pass 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.
Disaster recovery: fall back to an escalation link (mailto or support form) if the iframe fails to load within five seconds—swap the launcher label dynamically based on load events.

8. Verification checklist

  1. Open the host page and confirm the iframe stays hidden until the launcher is pressed.
  2. Click the launcher and verify the chat-embed-ready event is received in the console.
  3. Capture the posted payload in your browser dev tools and ensure user_id matches your auth context.
  4. Wait for chat-embed-ack in the console and verify the response time stays under one second.
  5. Verify the iframe URL includes the same configuration parameters as the postMessage payload (fallback mechanism).
  6. Send a message and confirm the Agent API receives the locked persona and metadata.
  7. Close and reopen the widget to ensure a fresh payload is sent, configSent resets, and configs apply correctly.

Resources & next steps