feat(widget): Widget 配置、loader 与右下角悬浮入口
生成可嵌入站点的 loader 与悬浮按钮,支持展开收起和移动端;附独立宿主测试页。
This commit is contained in:
1 parent
e24d3e2a65
commit
5d0a48505a
57 files changed
+32086
No files matched your search
@@ -0,0 +1,769 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ChatterMate Widget Test</title>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
line-height: 1.6;
|
||||
background: #ffffff;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #333;
|
||||
border-bottom: 2px solid #f34611;
|
||||
padding-bottom: 10px;
|
||||
font-size: clamp(1.5rem, 4vw, 2.5rem);
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #444;
|
||||
font-size: clamp(1.25rem, 3vw, 1.5rem);
|
||||
margin: 20px 0 15px 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
background: #f9f9f9;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
code {
|
||||
background: #f5f5f5;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 0.9em;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
pre {
|
||||
overflow-x: auto;
|
||||
background: #f5f5f5;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
font-size: 0.85em;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin: 40px 0;
|
||||
}
|
||||
|
||||
.widget-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.widget-input {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ccc;
|
||||
font-size: 16px;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.widget-input:focus {
|
||||
outline: none;
|
||||
border-color: #f34611;
|
||||
box-shadow: 0 0 0 2px rgba(243, 70, 17, 0.1);
|
||||
}
|
||||
|
||||
.demo-button {
|
||||
background: #f34611;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
white-space: nowrap;
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.demo-button:hover {
|
||||
background: #d93d0c;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(243, 70, 17, 0.3);
|
||||
}
|
||||
|
||||
.demo-button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.demo-button.clear-button {
|
||||
background: #dc3545;
|
||||
}
|
||||
|
||||
.demo-button.clear-button:hover {
|
||||
background: #c82333;
|
||||
box-shadow: 0 2px 8px rgba(220, 53, 69, 0.3);
|
||||
}
|
||||
|
||||
.demo-button.auth-button {
|
||||
background: #28a745;
|
||||
}
|
||||
|
||||
.demo-button.auth-button:hover {
|
||||
background: #218838;
|
||||
box-shadow: 0 2px 8px rgba(40, 167, 69, 0.3);
|
||||
}
|
||||
|
||||
.auth-section {
|
||||
margin-top: 30px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.auth-section h3 {
|
||||
font-size: 1.1rem;
|
||||
color: #555;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
.auth-section p {
|
||||
font-size: 0.9rem;
|
||||
color: #666;
|
||||
margin: 0 0 15px 0;
|
||||
}
|
||||
|
||||
.auth-status {
|
||||
font-size: 0.9rem;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.auth-status.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.auth-status.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.auth-status.loading {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
/* Responsive styles */
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 15px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin: 30px 0;
|
||||
}
|
||||
|
||||
.widget-controls {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.widget-input {
|
||||
min-width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.demo-button {
|
||||
width: 100%;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 10px;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
body {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.demo-button {
|
||||
padding: 14px 16px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.widget-input {
|
||||
padding: 14px 12px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark mode support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background: #1a1a1a;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
h1, h2 {
|
||||
color: #f0f0f0;
|
||||
}
|
||||
|
||||
.content {
|
||||
background: #2d2d2d;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
code, pre {
|
||||
background: #333;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.widget-input {
|
||||
background: #333;
|
||||
border-color: #555;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.widget-input:focus {
|
||||
border-color: #f34611;
|
||||
}
|
||||
|
||||
.auth-section {
|
||||
border-top-color: #444;
|
||||
}
|
||||
|
||||
.auth-section h3 {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.auth-section p {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.auth-status.success {
|
||||
background: #1e4620;
|
||||
color: #8fdf94;
|
||||
}
|
||||
|
||||
.auth-status.error {
|
||||
background: #4a1c1c;
|
||||
color: #f5a5a5;
|
||||
}
|
||||
|
||||
.auth-status.loading {
|
||||
background: #4a3c00;
|
||||
color: #ffd966;
|
||||
}
|
||||
}
|
||||
|
||||
/* Accessibility improvements */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>ChatterMate Widget Test Page</h1>
|
||||
|
||||
<div class="content">
|
||||
<h2>About ChatterMate</h2>
|
||||
<p>
|
||||
This is a test page demonstrating the ChatterMate chat widget integration.
|
||||
The widget provides real-time chat functionality with AI-powered responses
|
||||
and seamless human handoff capabilities.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2>Widget Controls</h2>
|
||||
<p>Enter your widget ID and click initialize to load the chat widget.</p>
|
||||
<div class="widget-controls">
|
||||
<input
|
||||
type="text"
|
||||
id="widgetIdInput"
|
||||
placeholder="Enter Widget ID"
|
||||
value=""
|
||||
class="widget-input"
|
||||
>
|
||||
<button class="demo-button" id="widgetButton" onclick="initializeWidget()">
|
||||
Initialize Widget
|
||||
</button>
|
||||
<button class="demo-button clear-button" id="clearButton" onclick="clearWidget()">
|
||||
Clear Widget
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- API Key Authentication Section -->
|
||||
<div class="auth-section">
|
||||
<h3>Token Authentication (Optional)</h3>
|
||||
<p>For agents with "Require Token Auth" enabled, provide an API key to authenticate.</p>
|
||||
<div class="widget-controls">
|
||||
<input
|
||||
type="password"
|
||||
id="apiKeyInput"
|
||||
placeholder="Enter API Key (from Widget Apps)"
|
||||
class="widget-input"
|
||||
>
|
||||
<input
|
||||
type="email"
|
||||
id="customerEmailInput"
|
||||
placeholder="Customer Email (optional)"
|
||||
class="widget-input"
|
||||
>
|
||||
</div>
|
||||
<div class="widget-controls" style="margin-top: 10px;">
|
||||
<button class="demo-button auth-button" id="authButton" onclick="authenticateAndInitialize()">
|
||||
Authenticate & Initialize
|
||||
</button>
|
||||
<span id="authStatus" class="auth-status"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<h2>Integration Code</h2>
|
||||
<p>To add the ChatterMate widget to your website, add the following code before the closing
|
||||
<code></body></code> tag:
|
||||
</p>
|
||||
|
||||
<h3 style="margin-top: 20px; font-size: 1rem;">Basic (No Authentication)</h3>
|
||||
<pre><code>
|
||||
<script>
|
||||
window.chattermateId = 'YOUR_WIDGET_ID';
|
||||
// Required for local testing — without it the widget uses the API URL baked
|
||||
// into the build (the cloud API for a production build).
|
||||
window.chattermateBaseUrl = 'http://localhost:8000/api/v1';
|
||||
</script>
|
||||
<script src="http://localhost:5173/webclient/chattermate.min.js"></script>
|
||||
</code></pre>
|
||||
|
||||
<h3 style="margin-top: 20px; font-size: 1rem;">With Token Authentication</h3>
|
||||
<p style="font-size: 0.9em; color: #666;">For agents with "Require Token Auth" enabled, generate a token server-side:</p>
|
||||
<pre><code>
|
||||
// Server-side: Generate token using your API key
|
||||
const response = await fetch('http://localhost:8000/api/v1/generate-token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer YOUR_API_KEY'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
widget_id: 'YOUR_WIDGET_ID',
|
||||
customer_email: 'user@example.com', // optional
|
||||
ttl_seconds: 3600
|
||||
})
|
||||
});
|
||||
const { data } = await response.json();
|
||||
// Pass data.token to frontend
|
||||
|
||||
// Client-side: Initialize widget with token
|
||||
<script>
|
||||
window.chattermateId = 'YOUR_WIDGET_ID';
|
||||
window.chattermateToken = 'TOKEN_FROM_SERVER';
|
||||
window.chattermateBaseUrl = 'http://localhost:8000/api/v1';
|
||||
</script>
|
||||
<script src="http://localhost:5173/webclient/chattermate.min.js"></script>
|
||||
</code></pre>
|
||||
</div>
|
||||
|
||||
<!-- ChatterMate Widget Integration -->
|
||||
<script>
|
||||
// Constants for localStorage keys
|
||||
const WIDGET_ID_KEY = 'chattermate_widget_id';
|
||||
const WIDGET_INITIALIZED_KEY = 'chattermate_initialized';
|
||||
|
||||
// Where the widget talks to. WITHOUT this the loader falls back to the URL
|
||||
// baked in at build time — which for a :prod build is the cloud API, so a
|
||||
// local test would silently chat with api.chattermate.chat.
|
||||
// Override in the console: localStorage.setItem('chattermate_api_base', '...')
|
||||
const CM_API_BASE = localStorage.getItem('chattermate_api_base') || 'http://localhost:8000/api/v1';
|
||||
// The loader is served by the FRONTEND (vite in dev, the dashboard origin in
|
||||
// production) — never copied into this folder, where it would go stale.
|
||||
// The backend does not serve it. Rebuild after loader changes:
|
||||
// cd ../frontend && npm run build:webclient:prod
|
||||
// Follow the page's own scheme rather than hardcoding http://: served over
|
||||
// https this page would otherwise pull executable code over an unencrypted
|
||||
// connection, which the browser blocks as mixed content anyway.
|
||||
const CM_LOADER_URL = localStorage.getItem('chattermate_loader_url')
|
||||
|| `${window.location.protocol}//localhost:5173/webclient/chattermate.min.js`;
|
||||
|
||||
// Load saved widget ID and initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadSavedWidgetId();
|
||||
autoInitializeWidget();
|
||||
});
|
||||
|
||||
function loadSavedWidgetId() {
|
||||
const savedWidgetId = localStorage.getItem(WIDGET_ID_KEY);
|
||||
const widgetInput = document.getElementById('widgetIdInput');
|
||||
const widgetButton = document.getElementById('widgetButton');
|
||||
|
||||
if (savedWidgetId) {
|
||||
widgetInput.value = savedWidgetId;
|
||||
widgetButton.textContent = 'Re-initialize Widget';
|
||||
updateButtonStates(true);
|
||||
} else {
|
||||
// Set default widget ID if none saved
|
||||
widgetInput.value = 'd63483f1-3954-4a94-a3f7-bb5039dda633';
|
||||
updateButtonStates(false);
|
||||
}
|
||||
}
|
||||
|
||||
function autoInitializeWidget() {
|
||||
const savedWidgetId = localStorage.getItem(WIDGET_ID_KEY);
|
||||
const wasInitialized = localStorage.getItem(WIDGET_INITIALIZED_KEY) === 'true';
|
||||
const hasApiKey = localStorage.getItem('chattermate_api_key');
|
||||
|
||||
// Don't auto-initialize if we have an API key saved (requires token auth)
|
||||
if (hasApiKey) {
|
||||
console.log('Skipping auto-initialize: API key detected, use Authenticate & Initialize button');
|
||||
return;
|
||||
}
|
||||
|
||||
if (savedWidgetId && wasInitialized) {
|
||||
console.log('Auto-initializing widget with saved ID:', savedWidgetId);
|
||||
performWidgetInitialization(savedWidgetId);
|
||||
}
|
||||
}
|
||||
|
||||
function initializeWidget() {
|
||||
const widgetId = document.getElementById('widgetIdInput').value;
|
||||
if (!widgetId) {
|
||||
alert('Please enter a widget ID');
|
||||
return;
|
||||
}
|
||||
|
||||
// Save widget ID to localStorage
|
||||
localStorage.setItem(WIDGET_ID_KEY, widgetId);
|
||||
localStorage.setItem(WIDGET_INITIALIZED_KEY, 'true');
|
||||
|
||||
performWidgetInitialization(widgetId);
|
||||
updateButtonStates(true);
|
||||
}
|
||||
|
||||
function performWidgetInitialization(widgetId) {
|
||||
// Set the widget ID
|
||||
window.chattermateId = widgetId;
|
||||
window.chattermateBaseUrl = CM_API_BASE;
|
||||
|
||||
// Remove existing script if any
|
||||
const existingScript = document.getElementById('chattermate-script');
|
||||
if (existingScript) {
|
||||
existingScript.remove();
|
||||
}
|
||||
|
||||
// Wait a moment for cleanup
|
||||
setTimeout(() => {
|
||||
// Create and add new script
|
||||
const script = document.createElement('script');
|
||||
script.id = 'chattermate-script';
|
||||
script.src = CM_LOADER_URL;
|
||||
script.onload = function() {
|
||||
// Initialize the widget once script loads
|
||||
if (window.ChatterMate) {
|
||||
window.ChatterMate.init({ id: widgetId });
|
||||
console.log('Widget initialized with ID:', widgetId);
|
||||
}
|
||||
};
|
||||
document.body.appendChild(script);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function clearWidget() {
|
||||
// Remove widget script first to prevent it from recreating storage items
|
||||
const existingScript = document.getElementById('chattermate-script');
|
||||
if (existingScript) {
|
||||
existingScript.remove();
|
||||
}
|
||||
|
||||
// Reset window properties to disable widget functionality
|
||||
if (window.chattermateId) {
|
||||
delete window.chattermateId;
|
||||
}
|
||||
if (window.ChatterMate) {
|
||||
// Call destroy method if it exists
|
||||
if (typeof window.ChatterMate.destroy === 'function') {
|
||||
window.ChatterMate.destroy();
|
||||
}
|
||||
delete window.ChatterMate;
|
||||
}
|
||||
|
||||
// Function to clear storage items with specific patterns
|
||||
function clearStorageItems(storage, storageType) {
|
||||
const keysToRemove = [];
|
||||
|
||||
// Collect all keys that match our patterns
|
||||
for (let i = 0; i < storage.length; i++) {
|
||||
const key = storage.key(i);
|
||||
if (key && (
|
||||
key.includes('chattermate') ||
|
||||
key.includes('widget') ||
|
||||
key === 'ctid' ||
|
||||
key.includes('chat') ||
|
||||
key.startsWith('cm_') ||
|
||||
key.includes('conversation') ||
|
||||
key.includes('session')
|
||||
)) {
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove all collected keys
|
||||
keysToRemove.forEach(key => {
|
||||
storage.removeItem(key);
|
||||
console.log(`Removed ${storageType} key:`, key);
|
||||
});
|
||||
|
||||
return keysToRemove.length;
|
||||
}
|
||||
|
||||
// Clear localStorage
|
||||
const localKeysRemoved = clearStorageItems(localStorage, 'localStorage');
|
||||
|
||||
// Clear sessionStorage
|
||||
const sessionKeysRemoved = clearStorageItems(sessionStorage, 'sessionStorage');
|
||||
|
||||
// Also remove our specific keys (backup)
|
||||
localStorage.removeItem(WIDGET_ID_KEY);
|
||||
localStorage.removeItem(WIDGET_INITIALIZED_KEY);
|
||||
localStorage.removeItem('ctid');
|
||||
sessionStorage.removeItem('ctid');
|
||||
|
||||
// Clear any widget-related cookies
|
||||
let cookiesCleared = 0;
|
||||
document.cookie.split(";").forEach(function(c) {
|
||||
const cookie = c.trim();
|
||||
if (cookie.includes('chattermate') ||
|
||||
cookie.includes('widget') ||
|
||||
cookie.includes('ctid') ||
|
||||
cookie.includes('chat')) {
|
||||
const eqPos = cookie.indexOf("=");
|
||||
const name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie;
|
||||
document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/";
|
||||
document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;domain=" + window.location.hostname;
|
||||
cookiesCleared++;
|
||||
console.log('Removed cookie:', name);
|
||||
}
|
||||
});
|
||||
|
||||
// Clear input field
|
||||
document.getElementById('widgetIdInput').value = '';
|
||||
|
||||
// Force a small delay and check again for any recreated items
|
||||
setTimeout(() => {
|
||||
localStorage.removeItem('ctid');
|
||||
sessionStorage.removeItem('ctid');
|
||||
console.log('Final cleanup: Removed any remaining ctid keys');
|
||||
}, 100);
|
||||
|
||||
updateButtonStates(false);
|
||||
console.log(`Widget cleared completely:
|
||||
- LocalStorage keys removed: ${localKeysRemoved}
|
||||
- SessionStorage keys removed: ${sessionKeysRemoved}
|
||||
- Cookies cleared: ${cookiesCleared}
|
||||
- Widget script removed and window properties cleaned`);
|
||||
}
|
||||
|
||||
function updateButtonStates(isInitialized) {
|
||||
const widgetButton = document.getElementById('widgetButton');
|
||||
const clearButton = document.getElementById('clearButton');
|
||||
|
||||
if (isInitialized) {
|
||||
widgetButton.textContent = 'Re-initialize Widget';
|
||||
clearButton.style.display = 'inline-block';
|
||||
} else {
|
||||
widgetButton.textContent = 'Initialize Widget';
|
||||
clearButton.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Update button state when input changes
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const widgetInput = document.getElementById('widgetIdInput');
|
||||
widgetInput.addEventListener('input', function() {
|
||||
const savedWidgetId = localStorage.getItem(WIDGET_ID_KEY);
|
||||
const currentValue = this.value;
|
||||
const widgetButton = document.getElementById('widgetButton');
|
||||
|
||||
if (savedWidgetId && currentValue === savedWidgetId) {
|
||||
widgetButton.textContent = 'Re-initialize Widget';
|
||||
} else {
|
||||
widgetButton.textContent = 'Initialize Widget';
|
||||
}
|
||||
});
|
||||
|
||||
// Load saved API key
|
||||
const savedApiKey = localStorage.getItem('chattermate_api_key');
|
||||
if (savedApiKey) {
|
||||
document.getElementById('apiKeyInput').value = savedApiKey;
|
||||
}
|
||||
});
|
||||
|
||||
// Authenticate with API key and initialize widget
|
||||
async function authenticateAndInitialize() {
|
||||
const widgetId = document.getElementById('widgetIdInput').value;
|
||||
const apiKey = document.getElementById('apiKeyInput').value;
|
||||
const customerEmail = document.getElementById('customerEmailInput').value;
|
||||
const authStatus = document.getElementById('authStatus');
|
||||
|
||||
if (!widgetId) {
|
||||
alert('Please enter a widget ID');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!apiKey) {
|
||||
alert('Please enter an API key');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
authStatus.textContent = 'Authenticating...';
|
||||
authStatus.className = 'auth-status loading';
|
||||
|
||||
try {
|
||||
// Call the generate-token endpoint
|
||||
const response = await fetch(`${CM_API_BASE}/generate-token`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${apiKey}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
widget_id: widgetId,
|
||||
customer_email: customerEmail || undefined,
|
||||
ttl_seconds: 3600
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || `HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.data?.token) {
|
||||
// Save API key for future use
|
||||
localStorage.setItem('chattermate_api_key', apiKey);
|
||||
localStorage.setItem(WIDGET_ID_KEY, widgetId);
|
||||
localStorage.setItem(WIDGET_INITIALIZED_KEY, 'true');
|
||||
|
||||
// Set the token for the widget
|
||||
window.chattermateToken = data.data.token;
|
||||
window.chattermateId = widgetId;
|
||||
|
||||
// Show success
|
||||
authStatus.textContent = `Authenticated! Token expires: ${new Date(data.data.expires_at).toLocaleString()}`;
|
||||
authStatus.className = 'auth-status success';
|
||||
|
||||
console.log('Token generated successfully:', {
|
||||
widget_id: data.data.widget_id,
|
||||
expires_at: data.data.expires_at,
|
||||
expires_in: data.data.expires_in
|
||||
});
|
||||
|
||||
// Initialize the widget with the token
|
||||
performAuthenticatedWidgetInitialization(widgetId, data.data.token);
|
||||
updateButtonStates(true);
|
||||
} else {
|
||||
throw new Error('Invalid response from server');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Authentication error:', error);
|
||||
authStatus.textContent = `Error: ${error.message}`;
|
||||
authStatus.className = 'auth-status error';
|
||||
}
|
||||
}
|
||||
|
||||
function performAuthenticatedWidgetInitialization(widgetId, token) {
|
||||
// Set widget configuration - widget loader checks window.chattermateToken first
|
||||
window.chattermateId = widgetId;
|
||||
window.chattermateToken = token;
|
||||
window.chattermateBaseUrl = CM_API_BASE;
|
||||
|
||||
// Remove existing script if any
|
||||
const existingScript = document.getElementById('chattermate-script');
|
||||
if (existingScript) {
|
||||
existingScript.remove();
|
||||
}
|
||||
|
||||
// Also remove any existing widget elements to ensure fresh initialization
|
||||
const existingContainer = document.getElementById('chattermate-container');
|
||||
const existingButton = document.getElementById('chattermate-button');
|
||||
const existingClose = document.getElementById('chattermate-mobile-close');
|
||||
const existingTopbar = document.getElementById('chattermate-mobile-topbar');
|
||||
if (existingContainer) existingContainer.remove();
|
||||
if (existingButton) existingButton.remove();
|
||||
if (existingClose) existingClose.remove();
|
||||
if (existingTopbar) existingTopbar.remove();
|
||||
|
||||
// Clear ChatterMate instance
|
||||
if (window.ChatterMate) {
|
||||
delete window.ChatterMate;
|
||||
}
|
||||
|
||||
// Wait a moment for cleanup
|
||||
setTimeout(() => {
|
||||
// Create and add new script
|
||||
const script = document.createElement('script');
|
||||
script.id = 'chattermate-script';
|
||||
script.src = CM_LOADER_URL;
|
||||
|
||||
script.onload = function() {
|
||||
if (window.ChatterMate) {
|
||||
window.ChatterMate.init({
|
||||
id: widgetId
|
||||
});
|
||||
}
|
||||
};
|
||||
document.body.appendChild(script);
|
||||
}, 200);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in new issue
Block a user