Skip to content

Implement a Live Chat Widget

Overview

The Primethink Live Chat Widget is a floating support chat interface that embeds Primethink public chat sessions into any website. It provides a seamless way to offer AI-powered support to your users without leaving your page.

What It Does

  • Floating Button: A customizable circular button that floats in the corner of your page
  • Chat Interface: A draggable, resizable chat window that opens when the button is clicked
  • Session Management: Automatically creates and persists chat sessions across page refreshes
  • iframe Integration: Loads Primethink chat sessions in a secure, sandboxed iframe
  • Local Storage: Maintains session continuity using browser localStorage

Key Features

  • ✅ Session Persistence - Chat sessions survive page refreshes and navigation
  • ✅ Draggable Interface - Users can move the chat window anywhere on screen
  • ✅ Minimize/Maximize - Collapse the chat to title bar only
  • ✅ Mobile Responsive - Adapts to mobile screens automatically
  • ✅ No Dependencies - Pure HTML, CSS, and JavaScript
  • ✅ Easy Configuration - Just update the chat ID to use your own chat

How It Works

Flow Diagram

User Action              Widget Action                    Backend Action
───────────              ─────────────                    ──────────────

1. Click Button    →     Check localStorage
                         ├─ Has session? → Load saved URL
                         └─ No session?  → Continue to step 2

2. Show Preview    →     GET /preview                    → Return agent name,
                         (optional, creates nothing)       greeting, upload settings

3. Create Session  →     POST /public-session            → Generate session ID
   (on first message)                                     → Return session ID

4. Receive Response ←    Read session_id from the JSON object
                         Build URL: /live/{chat-id}/public?sid=abc-123

5. Load Chat       →     Set iframe.src = URL
                         Save URL to localStorage

6. Chat Ready      →     User interacts with chat

7. Page Refresh    →     Check localStorage
                         Load saved session URL
                         Restore chat window

Technical Flow

  1. Initialization: Widget loads and checks localStorage for existing session
  2. Button Click: User clicks the floating button
  3. Preview (optional): GET request to https://app.primethink.ai/api/v1/public/chats/{chat-id}/preview to learn the agent's name, greeting and upload settings without creating anything
  4. API Call: POST request to https://app.primethink.ai/api/v1/public/chats/{chat-id}/public-session, best deferred until the visitor sends their first message
  5. Session Creation: Backend returns a session ID (e.g., "00136af9-a102-42e1-9ca1-e24ba6c30b4a")
  6. URL Construction: Widget builds URL: https://app.primethink.ai/live/{chat-id}/public?sid={session-id}
  7. Load Chat: iframe loads the chat with session ID
  8. Persist Session: URL saved to localStorage
  9. Restore on Refresh: Next page load/refresh restores the saved session

The simple widgets later on this page skip the preview and create the session when the button is clicked, which is fine for a button a visitor has deliberately pressed. Fetch the preview instead when the chat surface is visible without any deliberate action, or when you want to show the agent's name or greeting in your own UI.


Minimal Implementation

Step 1: Basic HTML Structure

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Support Chat Example</title>
    <style>
        /* Styles will go here */
    </style>
</head>
<body>
    <!-- Your page content -->
    <h1>My Website</h1>

    <!-- Floating Chat Button -->
    <button id="chat-button">💬</button>

    <!-- Chat Frame -->
    <div id="chat-frame">
        <div class="title-bar">
            <h3>Support</h3>
            <button id="close-btn">×</button>
        </div>
        <div class="chat-content">
            <iframe id="support-iframe"></iframe>
        </div>
    </div>

    <script>
        /* JavaScript will go here */
    </script>
</body>
</html>

Step 2: Essential Styles

/* Floating Button */
#chat-button {
    position: fixed;
    bottom: 30px;
    right: 30px;
    width: 60px;
    height: 60px;
    border-radius: 50%;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    border: none;
    color: white;
    font-size: 24px;
    cursor: pointer;
    box-shadow: 0 4px 12px rgba(0,0,0,0.3);
    z-index: 1000;
}

#chat-button:hover {
    transform: scale(1.1);
}

#chat-button.hidden {
    display: none;
}

/* Chat Frame */
#chat-frame {
    position: fixed;
    bottom: 100px;
    right: 30px;
    width: 380px;
    height: 600px;
    background: white;
    border-radius: 12px;
    box-shadow: 0 8px 24px rgba(0,0,0,0.3);
    display: none;
    flex-direction: column;
    z-index: 1001;
    overflow: hidden;
}

#chat-frame.open {
    display: flex;
}

/* Title Bar */
.title-bar {
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
    padding: 16px 20px;
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.title-bar button {
    background: rgba(255,255,255,0.2);
    border: none;
    color: white;
    width: 28px;
    height: 28px;
    border-radius: 50%;
    cursor: pointer;
    font-size: 20px;
}

/* Chat Content */
.chat-content {
    flex: 1;
    position: relative;
}

#support-iframe {
    width: 100%;
    height: 100%;
    border: none;
}

Step 3: Core JavaScript

class SupportChat {
    constructor() {
        // DOM elements
        this.chatButton = document.getElementById('chat-button');
        this.chatFrame = document.getElementById('chat-frame');
        this.closeBtn = document.getElementById('close-btn');
        this.iframe = document.getElementById('support-iframe');

        // Configuration - CHANGE THESE VALUES
        this.chatId = 'f6a7b8c9-d0e1-4234-9123-456789012345';
        this.baseUrl = `https://app.primethink.ai/live/${this.chatId}/public`;
        this.apiUrl = `https://app.primethink.ai/api/v1/public/chats/${this.chatId}/public-session`;

        // Storage keys
        this.STORAGE_KEY = 'support_chat_session_url';

        this.init();
    }

    init() {
        // Event listeners
        this.chatButton.addEventListener('click', () => this.openChat());
        this.closeBtn.addEventListener('click', () => this.closeChat());

        // Restore saved session
        this.restoreSession();
    }

    async openChat() {
        // Show chat
        this.chatFrame.classList.add('open');
        this.chatButton.classList.add('hidden');

        // Check for saved session
        const savedUrl = this.readSavedSession();
        if (savedUrl) {
            this.iframe.src = savedUrl;
            return;
        }

        // Create new session
        try {
            const response = await fetch(this.apiUrl, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({})
            });

            if (response.status === 429) {
                const retryAfter = response.headers.get('Retry-After');
                throw new Error(`Too many requests. Try again in ${retryAfter ?? 'a few'} seconds.`);
            }
            if (!response.ok) {
                throw new Error(`Session creation failed with status ${response.status}`);
            }

            // The endpoint returns { session_id, scoped_token } — not a bare string.
            const { session_id: sessionId } = await response.json();
            const sessionUrl = `${this.baseUrl}?sid=${sessionId}`;

            this.iframe.src = sessionUrl;
            localStorage.setItem(this.STORAGE_KEY, sessionUrl);
        } catch (error) {
            console.error('Error creating session:', error);
            this.iframe.srcdoc = '<p role="alert">Chat is temporarily unavailable. Please wait and try again.</p>';
        }
    }

    closeChat() {
        this.chatFrame.classList.remove('open');
        this.chatButton.classList.remove('hidden');
        this.iframe.src = '';
        localStorage.removeItem(this.STORAGE_KEY);
    }

    restoreSession() {
        const savedUrl = this.readSavedSession();
        if (savedUrl) {
            this.chatFrame.classList.add('open');
            this.chatButton.classList.add('hidden');
            this.iframe.src = savedUrl;
        }
    }

    // A visitor who used the chat before the URL changed still has the old
    // address in localStorage. Keep their session id, but rebuild the URL from
    // the current baseUrl so they are not sent to the deprecated path.
    readSavedSession() {
        const savedUrl = localStorage.getItem(this.STORAGE_KEY);
        if (!savedUrl) return null;

        let sid = null;
        try {
            sid = new URL(savedUrl, window.location.origin).searchParams.get('sid');
        } catch {
            // Not a URL we can read — drop it and start a fresh session.
            localStorage.removeItem(this.STORAGE_KEY);
            return null;
        }
        if (!sid) {
            localStorage.removeItem(this.STORAGE_KEY);
            return null;
        }

        const currentUrl = `${this.baseUrl}?sid=${sid}`;
        if (currentUrl !== savedUrl) localStorage.setItem(this.STORAGE_KEY, currentUrl);
        return currentUrl;
    }
}

// Initialize
new SupportChat();

Configuration

Required Values

To use your own Primethink chat, you need:

  1. Chat ID: Your public chat UUID
  2. Base URL: The public chat page URL
  3. API URL: The endpoint to create sessions
// Example for chat ID: abc-123-def-456
this.chatId = 'abc-123-def-456';
this.baseUrl = `https://app.primethink.ai/live/${this.chatId}/public`;
this.apiUrl = `https://app.primethink.ai/api/v1/public/chats/${this.chatId}/public-session`;

Finding Your Chat ID

Turn on Make Public in the chat's Settings tab and the full public chat URL appears underneath the switch, with a button to copy it, a button to open it, and a QR code beside it for pointing a phone at. Copying it from there is the reliable way to get the address; the chat ID is the segment in the middle of it.

Your Primethink public chat URL looks like:

https://app.primethink.ai/live/f6a7b8c9-d0e1-4234-9123-456789012345/public
                               └─────────────────┬────────────────┘
                                       This is your Chat ID

The older /public/chats/{chat-id} address still works

Public chats used to be served at https://app.primethink.ai/public/chats/{chat-id}. Links already shared with that address are forwarded to the new one, carrying their query string with them, so an existing session and an embedder's UI overrides both survive the hop. Use the /live/{chat-id}/public form in anything new — the older path is kept only for links that are already out in the world.

Note that the session endpoint is unchanged: it is still POST /api/v1/public/chats/{chat-id}/public-session.

Enabling File Uploads

By default, public chats do not allow file uploads. To enable file upload functionality, you need to:

  1. Enable Documents & Collections in the chat settings (required for any file upload to work)
  2. Add the following Extra setting to either the Task or the Chat (the values true, "true", and "1" are all accepted):
PUBLIC_CHAT_UPLOAD_FILES = true

Where to set this:

  1. On the Task: Go to the task settings and add this to the Extra field — chats created from the task inherit it
  2. On the Chat: Go to the chat settings and add this to the Extra field

When enabled, users in the public chat will see a file upload button and can attach files to their messages.


UI Customization Parameters

You can customize the appearance of an embedded public chat by appending query parameters to the chat URL. These parameters override the default UI for that session.

URL Format

https://app.primethink.ai/live/{chat-id}/public?sid={session-id}&ui_chat_title=My+Bot&ui_theme=dark

Available Parameters

Parameter Description Accepted Values Example
ui_chat_title Overrides the chat name shown in the top bar Any string ui_chat_title=Sales+Assistant
ui_chat_footer_msg Displays a message below the chat input (e.g. a disclaimer) Any string ui_chat_footer_msg=AI+may+make+mistakes
ui_theme Forces light or dark mode for the chat UI light, dark ui_theme=dark
ui_bg_color Sets the background behind the messages Hex code or color name ui_bg_color=%23f5f5f5
ui_agent_msg_color Sets the color accent for agent (bot) messages Hex code or color name ui_agent_msg_color=teal
ui_guest_msg_color Sets the color accent for guest (user) messages Hex code or color name ui_guest_msg_color=%23667eea

All parameters are optional. When omitted, the chat falls back to its default appearance.

Color Values

Color parameters (ui_bg_color, ui_agent_msg_color, ui_guest_msg_color) accept:

  • Hex codes: #rgb, #rgba, #rrggbb and #rrggbbaa, with or without the # prefix. The 4- and 8-digit forms carry an alpha channel and are accepted, but read it as CSS does — see the note below if you are moving a link over from the old chat. Five- and seven-digit values are not hex codes in CSS and are ignored.
  • Named colors: any CSS color keyword — red, teal, indigo, rebeccapurple, transparent and so on
  • Functional notation: rgb(...), rgba(...), hsl(...), hsla(...)

Note: When using hex codes in a URL, encode the # character as %23. For example: ui_bg_color=%23f0f0f0

A value that is not recognised is ignored and that part of the UI keeps its default, so a typo cannot break the page.

Changed: the public chat is now served by the app runner rather than by the main app, and colors are resolved as CSS rather than as Material colors. Two things follow:

  • Material-only names — amber, deeppurple, deeporange, bluegrey / bluegray — are no longer recognised. Use a hex code, or the nearest CSS keyword.
  • The 4- and 8-digit hex forms still work, but they no longer mean the same thing: CSS puts the alpha channel last (#rgba, #rrggbbaa) where Material put it first (#argb, #aarrggbb). A value written for the old chat is therefore read differently — #8000 was semi-transparent black and is now fully transparent red. Rewrite those with the alpha last, or use the 3- or 6-digit form, which means the same in both.

Names shared by both — red, teal, indigo and the rest — keep working, though the exact shade shifts a little: CSS red is #ff0000 where Material red was #f44336.

Example: Fully Customized Embed

const chatId = 'abc-123-def-456';
const sessionId = 'session-id-here';

const params = new URLSearchParams({
    sid: sessionId,
    ui_chat_title: 'Acme Support',
    ui_chat_footer_msg: 'AI-generated responses may not always be accurate.',
    ui_theme: 'light',
    ui_bg_color: '#f8f9fa',
    ui_agent_msg_color: 'indigo',
    ui_guest_msg_color: '#28a745',
});

const chatUrl = `https://app.primethink.ai/live/${chatId}/public?${params}`;
iframe.src = chatUrl;

Behavior Notes

  • Parameters are preserved across redirects (e.g. when a session is created and the URL changes).
  • ui_chat_title overrides the chat name everywhere it appears (top bar and browser tab title).
  • ui_chat_footer_msg is the only source of the footer line: the public chat shows nothing there unless the link asks for it.
  • ui_theme overrides the user's system theme preference for the chat window only.

API Details

Preview Endpoint

URL: GET https://app.primethink.ai/api/v1/public/chats/{chat-id}/preview

Read-only. It describes what a visitor is about to walk into — the agent's name, whether uploads are available, and the greeting the conversation will open with — without creating anything. Call it as often as you like; no session, no anonymous visitor, and no conversation is created by it.

No authentication is required, and the same public-chat rate limits apply as to every other endpoint on this page.

Response:

{
  "assistant_name": "Support",
  "documents_and_collections_enabled": true,
  "extra": {
    "PUBLIC_CHAT_UPLOAD_FILES": true
  },
  "greeting": "Hello! How can I help you today?",
  "starts_immediately": false
}

Field Meaning
assistant_name The name of the agent answering this chat, or null if none is set. Useful for branding the widget header before the visitor says anything.
documents_and_collections_enabled Whether documents and collections are turned on for the chat. Uploads require this to be true.
extra The chat's extra settings, passed through as configured. This is where PUBLIC_CHAT_UPLOAD_FILES appears when uploads have been enabled.
greeting The opening message the conversation will start with, taken from the task behind the chat, or null when there is no task or the task has no initial prompt. A task that has been deleted counts as no task.
starts_immediately true when the task is set to speak first. Open the session right away rather than waiting for the visitor to type.

Example with cURL:

curl "https://app.primethink.ai/api/v1/public/chats/<chat_share_id>/preview"

Preview on load, create the session on the first message

Creating a session on page load leaves behind an empty conversation and an anonymous visitor for everybody who merely opens the page. Show the preview when the widget opens, and only call the session endpoint once the visitor actually sends something — unless starts_immediately is true, in which case the agent speaks first and the session is needed straight away.

Create Session Endpoint

URL: POST https://app.primethink.ai/api/v1/public/chats/{chat-id}/public-session

Headers:

Content-Type: application/json
Accept: */*

Request Body:

{}

Response: JSON object carrying the session ID

{
  "session_id": "00136af9-a102-42e1-9ca1-e24ba6c30b4a",
  "scoped_token": "..."
}

Read session_id from it. scoped_token is the chat page's own Socket.IO credential — the embedded page obtains its own, so a widget does not need it.

Usage: Append to URL as ?sid= parameter

https://app.primethink.ai/live/{chat-id}/public?sid={session-id}

When a link cannot be used: both the preview and the session endpoint refuse a link that is not open to visitors, and they refuse it the same way. The session endpoint checks before it creates anything, so a refused request leaves no visitor and no conversation behind.

Status Meaning
404 No chat matches that ID.
400 The chat exists but has not been made public. Every chat has a shareable ID from the moment it is created, so an ID resolving is not the same as the chat being published.
403 The chat has been archived by its owner, so it is no longer taking visitors.

Example with cURL

curl -X POST "https://app.primethink.ai/api/v1/public/chats/<chat_share_id>/public-session" \
     -H "Content-Type: application/json" \
     -H "Accept: */*" \
     -d '{}'

Response:

{
  "session_id": "00ba58bb-220e-40b8-8959-19294265b17a",
  "scoped_token": "..."
}

Public Chat Rate Limits

Every public-chat API request consumes two independent request budgets:

  • A per-visitor budget for each public chat, identified by the share ID and client IP. The default is 20 requests per 60 seconds.
  • A per-chat budget shared by all visitors to that public chat. The default is 200 requests per 60 seconds.

These defaults can be changed by the PrimeThink deployment. The limits apply to public session creation, chat details, messages, message editing, socket-token creation, and streaming requests.

When either budget is exhausted, the API returns HTTP 429 Too Many Requests:

{
  "detail": "Rate limit exceeded"
}

Use the response headers rather than parsing the message text:

  • Retry-After — seconds until the fixed window resets.
  • X-RateLimit-Limit — the ceiling for the budget that was exhausted.
  • X-RateLimit-Remaining — 0 for the rejected request.

Wait for the indicated Retry-After interval before allowing another attempt; do not immediately retry or hard-code the default 60-second window.


LocalStorage Structure

Keys Used

The key names are up to your implementation — what matters is persisting the session URL (with its sid parameter) so the session survives page reloads. The two examples in this guide use:

// Minimal single-chat widget
'support_chat_session_url' = 'https://app.primethink.ai/live/abc-123/public?sid=def-456'

// Multi-chat widget
'chat_session_url'   = 'https://app.primethink.ai/live/abc-123/public?sid=def-456'
'chat_selected_city' = 'london'
'chat_is_open'       = 'true' | 'false'

Session Lifecycle

  1. Create: When user opens chat for first time

    localStorage.setItem('support_chat_session_url', 'chat-url?sid=session-id');
    

  2. Restore: On page load/refresh

    const savedUrl = localStorage.getItem('support_chat_session_url');
    if (savedUrl) {
        iframe.src = savedUrl;
    }
    

  3. Clear: When user closes chat

    localStorage.removeItem('support_chat_session_url');
    

What the Chat Page Does With sid

The embedded chat page reads the session from the sid query parameter and from nowhere else. It does not store the session itself, deliberately: a shared or kiosk browser must not hand the next visitor the previous one's conversation. Two consequences for an embedder:

  • A link without sid always starts a new conversation. Keeping the session across reloads is your job, which is what the storage above is for.
  • A sid the server will not accept is replaced, not reported. If the conversation behind it was deleted, or the public chat has since been unpublished or archived, the page drops the parameter and offers a fresh conversation instead of showing the visitor an error they cannot get past. A session URL you stored can therefore stop resuming without anything appearing to fail.

Loading the page does not create anything either. It fetches the preview, shows the greeting, and waits — the conversation is created when the visitor sends their first message, or immediately if the task is set to speak first. So an embedded chat that is visible but never used costs nothing.


Advanced Features

1. Loading Indicator

Add a loading spinner while the chat loads:

<div class="chat-content">
    <div class="loading-spinner" id="spinner">Loading...</div>
    <iframe id="support-iframe"></iframe>
</div>
iframe.addEventListener('load', () => {
    document.getElementById('spinner').style.display = 'none';
});

2. Minimize Feature

Allow users to minimize the chat:

toggleMinimize() {
    this.chatFrame.classList.toggle('minimized');
}
#chat-frame.minimized {
    height: 60px;
}
#chat-frame.minimized .chat-content {
    display: none;
}

3. Draggable Window

Make the chat draggable:

dragStart(e) {
    this.isDragging = true;
    const rect = this.chatFrame.getBoundingClientRect();
    this.offsetX = e.clientX - rect.left;
    this.offsetY = e.clientY - rect.top;
}

drag(e) {
    if (!this.isDragging) return;
    this.chatFrame.style.left = (e.clientX - this.offsetX) + 'px';
    this.chatFrame.style.top = (e.clientY - this.offsetY) + 'px';
}

4. Mobile Responsive

Adjust for mobile devices:

@media (max-width: 768px) {
    #chat-frame {
        width: calc(100vw - 40px);
        height: calc(100vh - 120px);
        right: 20px;
        bottom: 90px;
    }
}

Multiple Chats Example

Use Case: City-Specific Support

If you want to offer different chat sessions based on user selection (e.g., London vs Sydney support):

HTML Structure

<!-- Floating Button -->
<button id="chat-button">💬</button>

<!-- Chat Frame -->
<div id="chat-frame">
    <div class="title-bar">
        <h3>Support</h3>
        <button id="close-btn">×</button>
    </div>

    <!-- Welcome Screen - Choose Location -->
    <div id="welcome-screen" class="chat-content">
        <h2>Select Your Location</h2>
        <button class="city-btn" data-city="london">🇬🇧 London</button>
        <button class="city-btn" data-city="sydney">🇦🇺 Sydney</button>
    </div>

    <!-- Chat Screen - After Selection -->
    <div id="chat-screen" class="chat-content" style="display: none;">
        <div class="chat-header">
            <span id="city-badge"></span>
            <button id="back-btn">← Back</button>
        </div>
        <iframe id="support-iframe"></iframe>
    </div>
</div>

JavaScript Implementation

class MultiChatSupport {
    constructor() {
        this.chatButton = document.getElementById('chat-button');
        this.chatFrame = document.getElementById('chat-frame');
        this.closeBtn = document.getElementById('close-btn');
        this.welcomeScreen = document.getElementById('welcome-screen');
        this.chatScreen = document.getElementById('chat-screen');
        this.iframe = document.getElementById('support-iframe');
        this.cityBadge = document.getElementById('city-badge');
        this.backBtn = document.getElementById('back-btn');

        // Configuration - Multiple Chats
        this.chats = {
            london: {
                chatId: 'f6a7b8c9-d0e1-4234-9123-456789012345',
                name: 'London Support'
            },
            sydney: {
                chatId: 'a7b8c9d0-e1f2-4345-8234-567890123456',
                name: 'Sydney Support'
            }
        };

        this.STORAGE_KEYS = {
            SESSION_URL: 'chat_session_url',
            SELECTED_CITY: 'chat_selected_city',
            IS_OPEN: 'chat_is_open'
        };

        this.init();
    }

    init() {
        // Button event listeners
        this.chatButton.addEventListener('click', () => this.openChat());
        this.closeBtn.addEventListener('click', () => this.closeChat());
        this.backBtn.addEventListener('click', () => this.showWelcome());

        // City selection buttons
        document.querySelectorAll('.city-btn').forEach(btn => {
            btn.addEventListener('click', (e) => {
                const city = e.target.dataset.city;
                this.selectCity(city);
            });
        });

        // Restore session
        this.restoreSession();
    }

    openChat() {
        this.chatFrame.classList.add('open');
        this.chatButton.classList.add('hidden');
        localStorage.setItem(this.STORAGE_KEYS.IS_OPEN, 'true');
    }

    closeChat() {
        this.chatFrame.classList.remove('open');
        this.chatButton.classList.remove('hidden');
        this.iframe.src = '';

        // Clear session
        localStorage.removeItem(this.STORAGE_KEYS.SESSION_URL);
        localStorage.removeItem(this.STORAGE_KEYS.SELECTED_CITY);
        localStorage.setItem(this.STORAGE_KEYS.IS_OPEN, 'false');

        // Show welcome screen
        this.showWelcome();
    }

    showWelcome() {
        this.welcomeScreen.style.display = 'flex';
        this.chatScreen.style.display = 'none';
    }

    async selectCity(city) {
        const chatConfig = this.chats[city];
        if (!chatConfig) return;

        // Update UI
        this.cityBadge.textContent = chatConfig.name;
        this.welcomeScreen.style.display = 'none';
        this.chatScreen.style.display = 'flex';

        // Create session
        try {
            const apiUrl = `https://app.primethink.ai/api/v1/public/chats/${chatConfig.chatId}/public-session`;

            const response = await fetch(apiUrl, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({})
            });

            if (response.status === 429) {
                const retryAfter = response.headers.get('Retry-After');
                throw new Error(`Too many requests. Try again in ${retryAfter ?? 'a few'} seconds.`);
            }
            if (!response.ok) {
                throw new Error(`Session creation failed with status ${response.status}`);
            }

            // The endpoint returns { session_id, scoped_token } — not a bare string.
            const { session_id: sessionId } = await response.json();
            const sessionUrl = `https://app.primethink.ai/live/${chatConfig.chatId}/public?sid=${sessionId}`;

            // Load chat
            this.iframe.src = sessionUrl;

            // Save session
            localStorage.setItem(this.STORAGE_KEYS.SESSION_URL, sessionUrl);
            localStorage.setItem(this.STORAGE_KEYS.SELECTED_CITY, city);

        } catch (error) {
            console.error('Error creating session:', error);
            this.iframe.srcdoc = '<p role="alert">Chat is temporarily unavailable. Please wait and try again.</p>';
        }
    }

    restoreSession() {
        const savedUrl = localStorage.getItem(this.STORAGE_KEYS.SESSION_URL);
        const selectedCity = localStorage.getItem(this.STORAGE_KEYS.SELECTED_CITY);
        const wasOpen = localStorage.getItem(this.STORAGE_KEYS.IS_OPEN) === 'true';

        if (savedUrl && selectedCity && wasOpen) {
            const chatConfig = this.chats[selectedCity];

            // A session stored before the chat URL changed still points at the
            // deprecated path. Keep the session id, rebuild the rest.
            let sid = null;
            try {
                sid = new URL(savedUrl, window.location.origin).searchParams.get('sid');
            } catch { /* unreadable — fall through and start fresh */ }
            if (!sid) {
                localStorage.removeItem(this.STORAGE_KEYS.SESSION_URL);
                return;
            }
            const currentUrl =
                `https://app.primethink.ai/live/${chatConfig.chatId}/public?sid=${sid}`;
            if (currentUrl !== savedUrl) {
                localStorage.setItem(this.STORAGE_KEYS.SESSION_URL, currentUrl);
            }

            // Restore chat state
            this.chatFrame.classList.add('open');
            this.chatButton.classList.add('hidden');
            this.welcomeScreen.style.display = 'none';
            this.chatScreen.style.display = 'flex';

            // Restore content
            this.cityBadge.textContent = chatConfig.name;
            this.iframe.src = currentUrl;
        }
    }
}

// Initialize
new MultiChatSupport();

CSS for Multiple Chats

/* Welcome Screen */
#welcome-screen {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 40px;
}

#welcome-screen h2 {
    font-size: 18px;
    margin-bottom: 30px;
    text-align: center;
    color: #333;
}

.city-btn {
    width: 100%;
    max-width: 250px;
    padding: 16px 24px;
    margin: 8px 0;
    border: 2px solid #667eea;
    background: white;
    color: #667eea;
    border-radius: 8px;
    font-size: 16px;
    font-weight: 600;
    cursor: pointer;
    transition: all 0.3s ease;
}

.city-btn:hover {
    background: #667eea;
    color: white;
    transform: translateY(-2px);
    box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}

/* Chat Screen */
#chat-screen {
    flex-direction: column;
}

.chat-header {
    padding: 16px 20px;
    background: #f8f9fa;
    border-bottom: 1px solid #e0e0e0;
    display: flex;
    justify-content: space-between;
    align-items: center;
}

#city-badge {
    background: #667eea;
    color: white;
    padding: 4px 12px;
    border-radius: 12px;
    font-size: 12px;
    font-weight: 600;
}

#back-btn {
    padding: 6px 16px;
    background: white;
    border: 1px solid #ddd;
    border-radius: 6px;
    color: #666;
    cursor: pointer;
    font-size: 13px;
}

#back-btn:hover {
    border-color: #667eea;
    color: #667eea;
}

#chat-screen iframe {
    flex: 1;
    width: 100%;
    border: none;
}

Multiple Chats Flow

1. User clicks button → Welcome screen shows

2. User selects "London" →
   - POST /chats/london-id/public-session
   - Get session ID
   - Load London chat with ?sid=session-id
   - Save "london" + session URL to localStorage

3. User refreshes page →
   - Check localStorage
   - Find "london" + saved URL
   - Restore London chat automatically

4. User clicks "Back" →
   - Return to welcome screen
   - Keep session in localStorage (optional)

5. User closes chat →
   - Clear all localStorage
   - Reset to initial state

Best Practices

1. Error Handling

Always provide fallbacks:

try {
    const response = await fetch(apiUrl, requestOptions);

    if (response.status === 429) {
        const retryAfter = response.headers.get('Retry-After');
        showTemporaryError(`Too many requests. Try again in ${retryAfter ?? 'a few'} seconds.`);
        return; // Wait for user action; do not retry immediately.
    }
    if (!response.ok) {
        throw new Error(`Session creation failed with status ${response.status}`);
    }

    // The endpoint returns { session_id, scoped_token } — not a bare string.
    const { session_id: sessionId } = await response.json();
    iframe.src = `${baseUrl}?sid=${sessionId}`;
} catch (error) {
    console.error('Session creation failed:', error);
    showTemporaryError('Chat is temporarily unavailable. Please try again later.');
}

2. CORS Considerations

Ensure your domain is allowed to make requests to Primethink API. If you encounter CORS errors, the Primethink backend needs to allow your domain.

3. Security

  • Use the sandbox attribute on the iframe for security
  • Don't store sensitive data in localStorage
  • Clear session on logout if needed

Grant the narrowest set of sandbox permissions the widget actually needs. Note that combining allow-same-origin with allow-scripts lets sandboxed content reach back into its own origin and can effectively remove the sandbox's protection — only include allow-same-origin if the embedded page genuinely requires same-origin access, and prefer serving the widget from a separate origin so you can drop it:

<!-- Minimal: scripts + forms, cross-origin (no allow-same-origin) -->
<iframe sandbox="allow-scripts allow-forms allow-popups"></iframe>

4. Performance

  • Load chat only when user clicks button (lazy loading)
  • Clear iframe when chat closes to free memory
  • Limit localStorage size by removing old sessions

5. Accessibility

  • Add ARIA labels for screen readers
  • Ensure keyboard navigation works
  • Provide clear visual indicators
<button id="chat-button" aria-label="Open support chat">💬</button>

Troubleshooting

Chat doesn't load

Problem: iframe remains blank after clicking button

Solutions: - Check browser console for errors - Verify chat ID is correct - Ensure API endpoint is accessible - Check CORS configuration - Verify network connectivity

Session not persisting

Problem: Chat resets after page refresh

Solutions: - Check localStorage is enabled in browser - Verify storage key is correct - Ensure session URL is being saved - Check browser privacy settings

API returns 404, 400 or 403

Problem: The preview or session request is refused

Solutions: - 404 — verify the chat ID is correct and check the API URL format; test with cURL first - 400 — the chat has not been made public. Publish it in PrimeThink; having a shareable ID is not the same as being published - 403 — the chat has been archived by its owner and no longer takes visitors; unarchive it

API returns 429

Problem: A public-chat request fails with 429 Too Many Requests

Solutions: - Read the Retry-After response header and wait that many seconds before another attempt - Do not automatically retry in a tight loop - Reuse stored public sessions instead of creating a new session for every interaction or page load - If the problem affects many visitors, the public chat may have reached its shared per-chat budget

Multiple instances conflict

Problem: Multiple chat widgets on same page interfere

Solutions: - Use unique storage keys per widget - Namespace your class/variables - Consider using data attributes for configuration


Examples

The two complete implementations in this guide cover both patterns: - The minimal widget — single chat implementation - The multi-chat widget — multiple chats with city selection

Both examples include: - Full HTML, CSS, and JavaScript - Session persistence - Drag and drop - Minimize/maximize - Mobile responsive - Loading indicators


Support

For issues or questions: 1. Check browser console for errors 2. Verify API responses with cURL 3. Review localStorage in DevTools 4. Test with minimal example first

License

This implementation guide is provided for developers integrating Primethink chat into their applications.