feat(widget): Widget 配置、loader 与右下角悬浮入口
生成可嵌入站点的 loader 与悬浮按钮,支持展开收起和移动端;附独立宿主测试页。
This commit is contained in:
1 parent
556b650c7a
commit
0cc6299d1c
57 files changed
+32086
No files matched your search
@@ -0,0 +1,69 @@
|
||||
# ReplyPier Widget Test
|
||||
|
||||
This is a test project for the ReplyPier chat widget integration.
|
||||
|
||||
## UI options gallery
|
||||
|
||||
`ui-options.html` has one card per widget placement/behaviour (floating left/right,
|
||||
custom size and offsets, sidebar drawers, search-bar trigger, hidden launcher + own
|
||||
button, custom z-index, dashboard defaults, page-overrides-beat-dashboard, mobile).
|
||||
Each opens `ui-test.html` with that configuration plus buttons for the whole JS API
|
||||
(`open/close/toggle/isOpen`, `open({message})`, `hide/showLauncher`, `setPosition`,
|
||||
`data-replypier-open`, `trigger` selector) and an event log.
|
||||
|
||||
Use it in **Stub** mode (fake widget, no backend needed) or **Real** mode (set your
|
||||
widget ID + API URL on the gallery page). The Loader URL setting controls where
|
||||
`replypier.min.js` comes from — the frontend dev server by default.
|
||||
|
||||
## Setup
|
||||
|
||||
1. Install dependencies:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
2. Start the test server:
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
3. Open http://localhost:3000 in your browser
|
||||
|
||||
## Testing the Widget
|
||||
|
||||
1. Make sure the ReplyPier frontend development server is running (usually at http://localhost:5173)
|
||||
2. Click the "Initialize Widget" button on the test page
|
||||
3. Use the chat widget that appears in the bottom-right corner
|
||||
|
||||
## Development Notes
|
||||
|
||||
- The widget script is loaded from the local development server
|
||||
- The test page uses a dummy widget ID ('test-widget-123')
|
||||
- CORS is enabled to allow loading the widget script from a different origin
|
||||
|
||||
## Where things are served from
|
||||
|
||||
- **Loader** (`replypier.min.js`) — the **frontend** (`http://localhost:5173/webclient/…`),
|
||||
the same origin the dashboard's embed snippet points at. The backend does **not**
|
||||
serve it, and no copy is kept in this folder: a copy goes stale and you end up
|
||||
testing an old widget while the source looks correct.
|
||||
It is a build artifact — after editing `frontend/src/webclient/replypier.js` run
|
||||
`cd ../frontend && npm run build:webclient:prod`.
|
||||
- **Widget app** (`widget.js`) — the backend, from `backend/assets/`. Rebuild with
|
||||
`cd ../frontend && npm run build:widget:prod` after changing `WidgetBuilder.vue`
|
||||
or the widget composables.
|
||||
- **API** — set `window.replypierBaseUrl`. Without it the loader falls back to the URL
|
||||
baked in at build time, which for a production build is the cloud API, so a "local"
|
||||
test silently talks to api.replypier.site.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **"Chat can't load on 'localhost'"** — the origin isn't in your organization's allowed
|
||||
domains (Organization settings). The port matters: `localhost:3000` allowed does not
|
||||
permit `localhost:3010`.
|
||||
- **Dashboard placement settings not applying** — almost always a stale loader. Check the
|
||||
served file has the newer features: `curl -s <loader-url> | grep -c sidebar-left`.
|
||||
- **Buttons do nothing on the gallery pages** — the loader failed to load; the page shows
|
||||
a red banner saying so.
|
||||
@@ -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>ReplyPier 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>ReplyPier Widget Test Page</h1>
|
||||
|
||||
<div class="content">
|
||||
<h2>About ReplyPier</h2>
|
||||
<p>
|
||||
This is a test page demonstrating the ReplyPier 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 ReplyPier 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.replypierId = '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.replypierBaseUrl = 'http://localhost:8000/api/v1';
|
||||
</script>
|
||||
<script src="http://localhost:5173/webclient/replypier.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.replypierId = 'YOUR_WIDGET_ID';
|
||||
window.replypierToken = 'TOKEN_FROM_SERVER';
|
||||
window.replypierBaseUrl = 'http://localhost:8000/api/v1';
|
||||
</script>
|
||||
<script src="http://localhost:5173/webclient/replypier.min.js"></script>
|
||||
</code></pre>
|
||||
</div>
|
||||
|
||||
<!-- ReplyPier Widget Integration -->
|
||||
<script>
|
||||
// Constants for localStorage keys
|
||||
const WIDGET_ID_KEY = 'replypier_widget_id';
|
||||
const WIDGET_INITIALIZED_KEY = 'replypier_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.replypier.site.
|
||||
// Override in the console: localStorage.setItem('replypier_api_base', '...')
|
||||
const CM_API_BASE = localStorage.getItem('replypier_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('replypier_loader_url')
|
||||
|| `${window.location.protocol}//localhost:5173/webclient/replypier.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('replypier_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.replypierId = widgetId;
|
||||
window.replypierBaseUrl = CM_API_BASE;
|
||||
|
||||
// Remove existing script if any
|
||||
const existingScript = document.getElementById('replypier-script');
|
||||
if (existingScript) {
|
||||
existingScript.remove();
|
||||
}
|
||||
|
||||
// Wait a moment for cleanup
|
||||
setTimeout(() => {
|
||||
// Create and add new script
|
||||
const script = document.createElement('script');
|
||||
script.id = 'replypier-script';
|
||||
script.src = CM_LOADER_URL;
|
||||
script.onload = function() {
|
||||
// Initialize the widget once script loads
|
||||
if (window.ReplyPier) {
|
||||
window.ReplyPier.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('replypier-script');
|
||||
if (existingScript) {
|
||||
existingScript.remove();
|
||||
}
|
||||
|
||||
// Reset window properties to disable widget functionality
|
||||
if (window.replypierId) {
|
||||
delete window.replypierId;
|
||||
}
|
||||
if (window.ReplyPier) {
|
||||
// Call destroy method if it exists
|
||||
if (typeof window.ReplyPier.destroy === 'function') {
|
||||
window.ReplyPier.destroy();
|
||||
}
|
||||
delete window.ReplyPier;
|
||||
}
|
||||
|
||||
// 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('replypier') ||
|
||||
key.includes('widget') ||
|
||||
key === 'rpid' ||
|
||||
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('rpid');
|
||||
sessionStorage.removeItem('rpid');
|
||||
|
||||
// Clear any widget-related cookies
|
||||
let cookiesCleared = 0;
|
||||
document.cookie.split(";").forEach(function(c) {
|
||||
const cookie = c.trim();
|
||||
if (cookie.includes('replypier') ||
|
||||
cookie.includes('widget') ||
|
||||
cookie.includes('rpid') ||
|
||||
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('rpid');
|
||||
sessionStorage.removeItem('rpid');
|
||||
console.log('Final cleanup: Removed any remaining rpid 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('replypier_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('replypier_api_key', apiKey);
|
||||
localStorage.setItem(WIDGET_ID_KEY, widgetId);
|
||||
localStorage.setItem(WIDGET_INITIALIZED_KEY, 'true');
|
||||
|
||||
// Set the token for the widget
|
||||
window.replypierToken = data.data.token;
|
||||
window.replypierId = 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.replypierToken first
|
||||
window.replypierId = widgetId;
|
||||
window.replypierToken = token;
|
||||
window.replypierBaseUrl = CM_API_BASE;
|
||||
|
||||
// Remove existing script if any
|
||||
const existingScript = document.getElementById('replypier-script');
|
||||
if (existingScript) {
|
||||
existingScript.remove();
|
||||
}
|
||||
|
||||
// Also remove any existing widget elements to ensure fresh initialization
|
||||
const existingContainer = document.getElementById('replypier-container');
|
||||
const existingButton = document.getElementById('replypier-button');
|
||||
const existingClose = document.getElementById('replypier-mobile-close');
|
||||
const existingTopbar = document.getElementById('replypier-mobile-topbar');
|
||||
if (existingContainer) existingContainer.remove();
|
||||
if (existingButton) existingButton.remove();
|
||||
if (existingClose) existingClose.remove();
|
||||
if (existingTopbar) existingTopbar.remove();
|
||||
|
||||
// Clear ReplyPier instance
|
||||
if (window.ReplyPier) {
|
||||
delete window.ReplyPier;
|
||||
}
|
||||
|
||||
// Wait a moment for cleanup
|
||||
setTimeout(() => {
|
||||
// Create and add new script
|
||||
const script = document.createElement('script');
|
||||
script.id = 'replypier-script';
|
||||
script.src = CM_LOADER_URL;
|
||||
|
||||
script.onload = function() {
|
||||
if (window.ReplyPier) {
|
||||
window.ReplyPier.init({
|
||||
id: widgetId
|
||||
});
|
||||
}
|
||||
};
|
||||
document.body.appendChild(script);
|
||||
}, 200);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Generated
+912
@@ -0,0 +1,912 @@
|
||||
{
|
||||
"name": "replypier-test",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "replypier-test",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"express": "^5.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
||||
"integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-types": "^3.0.0",
|
||||
"negotiator": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz",
|
||||
"integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "^3.1.2",
|
||||
"content-type": "^2.0.0",
|
||||
"debug": "^4.4.3",
|
||||
"http-errors": "^2.0.1",
|
||||
"iconv-lite": "^0.7.2",
|
||||
"on-finished": "^2.4.1",
|
||||
"qs": "^6.15.2",
|
||||
"raw-body": "^3.0.2",
|
||||
"type-is": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser/node_modules/content-type": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
|
||||
"integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bound": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"get-intrinsic": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
|
||||
"integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/content-type": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
||||
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-signature": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
|
||||
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cors": {
|
||||
"version": "2.8.6",
|
||||
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
|
||||
"integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"object-assign": "^4",
|
||||
"vary": "^1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
|
||||
"integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/etag": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "^2.0.0",
|
||||
"body-parser": "^2.2.1",
|
||||
"content-disposition": "^1.0.0",
|
||||
"content-type": "^1.0.5",
|
||||
"cookie": "^0.7.1",
|
||||
"cookie-signature": "^1.2.1",
|
||||
"debug": "^4.4.0",
|
||||
"depd": "^2.0.0",
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"etag": "^1.8.1",
|
||||
"finalhandler": "^2.1.0",
|
||||
"fresh": "^2.0.0",
|
||||
"http-errors": "^2.0.0",
|
||||
"merge-descriptors": "^2.0.0",
|
||||
"mime-types": "^3.0.0",
|
||||
"on-finished": "^2.4.1",
|
||||
"once": "^1.4.0",
|
||||
"parseurl": "^1.3.3",
|
||||
"proxy-addr": "^2.0.7",
|
||||
"qs": "^6.14.0",
|
||||
"range-parser": "^1.2.1",
|
||||
"router": "^2.2.0",
|
||||
"send": "^1.1.0",
|
||||
"serve-static": "^2.2.0",
|
||||
"statuses": "^2.0.1",
|
||||
"type-is": "^2.0.1",
|
||||
"vary": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
|
||||
"integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.0",
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"on-finished": "^2.4.1",
|
||||
"parseurl": "^1.3.3",
|
||||
"statuses": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fresh": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
|
||||
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
||||
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"depd": "~2.0.0",
|
||||
"inherits": "~2.0.4",
|
||||
"setprototypeof": "~1.2.0",
|
||||
"statuses": "~2.0.2",
|
||||
"toidentifier": "~1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
|
||||
"integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/is-promise": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
|
||||
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/media-typer": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz",
|
||||
"integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-descriptors": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
|
||||
"integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.54.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
||||
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
|
||||
"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "^1.54.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz",
|
||||
"integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"content-type": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/negotiator/node_modules/content-type": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
|
||||
"integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ee-first": "1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "8.4.2",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
|
||||
"integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"forwarded": "0.2.0",
|
||||
"ipaddr.js": "1.9.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.16.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz",
|
||||
"integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"es-define-property": "^1.0.1",
|
||||
"side-channel": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/range-parser": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz",
|
||||
"integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/raw-body": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
|
||||
"integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "~3.1.2",
|
||||
"http-errors": "~2.0.1",
|
||||
"iconv-lite": "~0.7.0",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/router": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
|
||||
"integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.0",
|
||||
"depd": "^2.0.0",
|
||||
"is-promise": "^4.0.0",
|
||||
"parseurl": "^1.3.3",
|
||||
"path-to-regexp": "^8.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
|
||||
"integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.3",
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"etag": "^1.8.1",
|
||||
"fresh": "^2.0.0",
|
||||
"http-errors": "^2.0.1",
|
||||
"mime-types": "^3.0.2",
|
||||
"ms": "^2.1.3",
|
||||
"on-finished": "^2.4.1",
|
||||
"range-parser": "^1.2.1",
|
||||
"statuses": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-static": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
|
||||
"integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"parseurl": "^1.3.3",
|
||||
"send": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
|
||||
"integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.4",
|
||||
"side-channel-list": "^1.0.1",
|
||||
"side-channel-map": "^1.0.1",
|
||||
"side-channel-weakmap": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-list": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
|
||||
"integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-map": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-weakmap": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-map": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz",
|
||||
"integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"content-type": "^2.0.0",
|
||||
"media-typer": "^1.1.0",
|
||||
"mime-types": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/type-is/node_modules/content-type": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
|
||||
"integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"license": "ISC"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "replypier-test",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"express": "^5.2.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import express from "express";
|
||||
import { fileURLToPath } from "url";
|
||||
import { dirname, join } from "path";
|
||||
import cors from "cors";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const app = express();
|
||||
const port = process.env.PORT || 3000;
|
||||
|
||||
// This server only hosts the test pages. The widget loader is NOT kept here — the
|
||||
// pages load it from the frontend origin (vite dev server locally, the dashboard
|
||||
// origin in production), exactly like the embed snippet the dashboard generates.
|
||||
// A copy in this folder would go stale and silently test an old widget.
|
||||
const FRONTEND_ORIGIN = process.env.FRONTEND_ORIGIN || "http://localhost:5173";
|
||||
|
||||
// Enable CORS for the widget script
|
||||
app.use(cors());
|
||||
|
||||
// Serve static files
|
||||
app.use(express.static(__dirname));
|
||||
|
||||
// Serve index.html
|
||||
app.get("/", (req, res) => {
|
||||
res.sendFile(join(__dirname, "index.html"));
|
||||
});
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Test server running at http://localhost:${port}`);
|
||||
console.log(`UI options gallery: http://localhost:${port}/ui-options.html`);
|
||||
console.log(`Widget loader from: ${FRONTEND_ORIGIN}/webclient/replypier.min.js`);
|
||||
console.log(`(rebuild it after loader changes: cd ../frontend && npm run build:webclient:prod)`);
|
||||
});
|
||||
@@ -0,0 +1,148 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>ReplyPier UI options gallery</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { font-family: system-ui; margin: 0; padding: 32px; background: #f7f7f5; color: #1c1c1e; }
|
||||
h1 { margin: 0 0 4px; }
|
||||
.hint { color: #666; margin-bottom: 20px; }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 14px; }
|
||||
a.card { display: block; background: #fff; border: 1px solid #e2e2df; border-radius: 12px; padding: 16px; text-decoration: none; color: inherit; }
|
||||
a.card:hover { border-color: #f34611; }
|
||||
.card h3 { margin: 0 0 6px; font-size: 15px; }
|
||||
.card p { margin: 0; font-size: 13px; color: #666; line-height: 1.5; }
|
||||
code { background: #eee; padding: 1px 5px; border-radius: 4px; font-size: 12px; }
|
||||
.settings { background: #fff; border: 1px solid #e2e2df; border-radius: 12px; padding: 16px; margin-bottom: 24px; display: flex; gap: 12px; flex-wrap: wrap; align-items: end; }
|
||||
.settings label { display: flex; flex-direction: column; gap: 4px; font-size: 12px; color: #666; }
|
||||
.settings input, .settings select { padding: 6px 10px; border: 1px solid #ccc; border-radius: 8px; font-size: 13px; min-width: 230px; }
|
||||
.settings button { padding: 8px 16px; border: none; border-radius: 8px; background: #f34611; color: #fff; cursor: pointer; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>ReplyPier UI options gallery</h1>
|
||||
<p class="hint">
|
||||
One card per placement/behaviour. <strong>Stub</strong> mode fakes the widget (no backend
|
||||
needed — good for checking the loader UI alone). <strong>Real</strong> mode loads your
|
||||
actual widget: fill in the widget ID and API URL below.
|
||||
<br>
|
||||
The loader is loaded from the <strong>frontend</strong> (<code>npm run dev</code> in
|
||||
<code>frontend/</code>), the same origin the dashboard's embed snippet points at — the
|
||||
backend does not serve it, and no copy is kept in this folder. The loader is a build
|
||||
artifact: after changing its source run
|
||||
<code>cd frontend && npm run build:webclient:prod</code>.
|
||||
</p>
|
||||
|
||||
<div class="settings">
|
||||
<label>Mode
|
||||
<select id="mode">
|
||||
<option value="stub">Stub (no backend)</option>
|
||||
<option value="real">Real backend</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>Loader URL (served by the frontend)
|
||||
<input id="loaderUrl" placeholder="http://localhost:5173/webclient/replypier.min.js">
|
||||
</label>
|
||||
<label>Widget ID (real mode)
|
||||
<input id="widgetId" placeholder="widget uuid">
|
||||
</label>
|
||||
<label>API base URL (real mode)
|
||||
<input id="baseUrl" placeholder="http://localhost:8000/api/v1">
|
||||
</label>
|
||||
<button onclick="saveSettings()">Save</button>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<a class="card" data-config="">
|
||||
<h3>1. Default (zero config)</h3>
|
||||
<p>Legacy behaviour — floating bubble, bottom right. The regression baseline.</p>
|
||||
</a>
|
||||
<a class="card" data-config='{"position":{"side":"left"}}'>
|
||||
<h3>2. Floating, left side</h3>
|
||||
<p>Bubble and window hugging the bottom-left corner.</p>
|
||||
</a>
|
||||
<a class="card" data-config='{"width":520,"height":700}'>
|
||||
<h3>3. Floating, custom size</h3>
|
||||
<p>520 × 700 window instead of the 400 × 560 default.</p>
|
||||
</a>
|
||||
<a class="card" data-config='{"position":{"bottom":90,"offset":24}}'>
|
||||
<h3>4. Custom offsets</h3>
|
||||
<p>Launcher lifted 90px — e.g. to clear a fixed bottom nav bar.</p>
|
||||
</a>
|
||||
<a class="card" data-config='{"displayMode":"sidebar-right","sidebarWidth":460}'>
|
||||
<h3>5. Sidebar drawer, right</h3>
|
||||
<p>Full-height panel sliding in from the right edge, 460px wide.</p>
|
||||
</a>
|
||||
<a class="card" data-config='{"displayMode":"sidebar-left"}'>
|
||||
<h3>6. Sidebar drawer, left</h3>
|
||||
<p>Full-height panel from the left edge, default width.</p>
|
||||
</a>
|
||||
<a class="card" data-config='{"displayMode":"search-bar","searchBar":{"placeholder":"Ask our AI anything...","width":380}}'>
|
||||
<h3>7. Search bar trigger</h3>
|
||||
<p>The launcher becomes a search-style bar (desktop; bubble on mobile).</p>
|
||||
</a>
|
||||
<a class="card" data-config='{"launcher":false,"trigger":"#my-trigger"}'>
|
||||
<h3>8. Hidden launcher + own button</h3>
|
||||
<p>No built-in launcher. Open via the page’s buttons, the <code>trigger</code> selector, <code>data-replypier-open</code>, or the JS API.</p>
|
||||
</a>
|
||||
<a class="card" data-config='{"zIndex":50}'>
|
||||
<h3>9. Custom z-index</h3>
|
||||
<p>Widget at z-index 50 — the page’s fixed overlay (z-index 100) must cover it.</p>
|
||||
</a>
|
||||
<a class="card" data-dashboard='{"mode":"sidebar-right","sidebar_width":420,"offset_bottom":30,"offset_side":30}'>
|
||||
<h3>10. Dashboard defaults (stub only)</h3>
|
||||
<p>No page config; the fake widget delivers <code>widget_display</code> as saved in the dashboard’s Widget placement section.</p>
|
||||
</a>
|
||||
<a class="card" data-config='{"width":520}' data-dashboard='{"mode":"floating","width":320,"height":800,"side":"left"}'>
|
||||
<h3>11. Page config beats dashboard (stub only)</h3>
|
||||
<p>Dashboard says 320-wide, left; the page sets width 520. Expect 520 wide, left side, 800 tall.</p>
|
||||
</a>
|
||||
<a class="card" data-config='{"displayMode":"search-bar"}' data-style="ASK_ANYTHING">
|
||||
<h3>12. Ask AI palette (⌘K)</h3>
|
||||
<p>Ask Anything/Aurora open as a centered command palette over a dimmed page. Try ⌘K, Esc, and clicking the backdrop. Use Real mode to see the redesigned interior.</p>
|
||||
</a>
|
||||
<a class="card" data-config='{"displayMode":"sidebar-left"}' data-style="ASK_ANYTHING">
|
||||
<h3>13. Drawer + Ask Anything (stub only)</h3>
|
||||
<p>Ask Anything/Aurora draw no in-panel chevron, so the launcher must stay reachable beside the drawer. Every other style hides it while open.</p>
|
||||
</a>
|
||||
<a class="card" data-config='{"displayMode":"sidebar-right"}'>
|
||||
<h3>14. Mobile check</h3>
|
||||
<p>Open any card with device emulation / a narrow window: fullscreen chat must win in every mode.</p>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const S_KEY = 'rp-test-settings'
|
||||
const saved = JSON.parse(localStorage.getItem(S_KEY) || '{}')
|
||||
document.getElementById('mode').value = saved.mode || 'stub'
|
||||
// The frontend serves the loader (the backend does not). '/replypier.min.js' is a
|
||||
// stale saved value from an earlier version of this page — ignore it.
|
||||
const savedLoader = saved.loaderUrl === '/replypier.min.js' ? '' : saved.loaderUrl
|
||||
document.getElementById('loaderUrl').value = savedLoader
|
||||
|| `${location.protocol}//localhost:5173/webclient/replypier.min.js`
|
||||
document.getElementById('widgetId').value = saved.widgetId || ''
|
||||
document.getElementById('baseUrl').value = saved.baseUrl || 'http://localhost:8000/api/v1'
|
||||
|
||||
function saveSettings() {
|
||||
localStorage.setItem(S_KEY, JSON.stringify({
|
||||
mode: document.getElementById('mode').value,
|
||||
loaderUrl: document.getElementById('loaderUrl').value.trim(),
|
||||
widgetId: document.getElementById('widgetId').value.trim(),
|
||||
baseUrl: document.getElementById('baseUrl').value.trim(),
|
||||
}))
|
||||
alert('Saved')
|
||||
}
|
||||
|
||||
document.querySelectorAll('a.card').forEach(card => {
|
||||
const q = new URLSearchParams()
|
||||
if (card.dataset.config) q.set('config', card.dataset.config)
|
||||
if (card.dataset.dashboard) q.set('dashboard', card.dataset.dashboard)
|
||||
if (card.dataset.style) q.set('style', card.dataset.style)
|
||||
q.set('title', card.querySelector('h3').textContent)
|
||||
card.href = 'ui-test.html?' + q.toString()
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,169 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>ReplyPier UI test</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { font-family: system-ui; margin: 0; padding: 24px; background: #f7f7f5; color: #1c1c1e; }
|
||||
h1 { margin: 0 0 2px; font-size: 20px; }
|
||||
.hint { color: #666; font-size: 13px; margin-bottom: 16px; }
|
||||
.banner { display: none; background: #fdecea; border: 1px solid #f5c2bd; color: #8b2415; padding: 12px 14px; border-radius: 10px; margin-bottom: 14px; font-size: 13px; line-height: 1.6; }
|
||||
.banner code { background: rgba(0,0,0,.06); padding: 1px 5px; border-radius: 4px; }
|
||||
.controls button { margin: 3px; padding: 8px 14px; border: 1px solid #ccc; border-radius: 8px; background: #fff; cursor: pointer; }
|
||||
.controls button:hover { border-color: #f34611; }
|
||||
#log { margin-top: 14px; font: 12px/1.6 monospace; background: #fff; border: 1px solid #ddd; border-radius: 8px; padding: 10px; height: 170px; overflow: auto; white-space: pre-wrap; }
|
||||
.overlay { position: fixed; top: 30%; right: 40px; width: 220px; padding: 18px; background: rgba(110, 70, 220, 0.92); color: #fff; border-radius: 12px; z-index: 100; font-size: 13px; }
|
||||
.tall { height: 1400px; background: linear-gradient(#eee, #ddd); margin-top: 24px; border-radius: 12px; padding: 16px; color: #999; }
|
||||
a { color: #f34611; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1 id="title">ReplyPier UI test</h1>
|
||||
<p class="hint"><a href="ui-options.html">← all scenarios</a> · <span id="mode-label"></span></p>
|
||||
|
||||
<div class="banner" id="banner"></div>
|
||||
|
||||
<div class="controls">
|
||||
<button onclick="api('open')">open()</button>
|
||||
<button onclick="api('close')">close()</button>
|
||||
<button onclick="api('toggle')">toggle()</button>
|
||||
<button onclick="api('isOpen')">isOpen()</button>
|
||||
<button onclick="api('open', { message: 'Tell me about pricing' })">open({message})</button>
|
||||
<button onclick="api('hideLauncher')">hideLauncher()</button>
|
||||
<button onclick="api('showLauncher')">showLauncher()</button>
|
||||
<button onclick="api('setPosition', { side: 'left', bottom: 60, offset: 40 })">setPosition(left, 60, 40)</button>
|
||||
<button onclick="api('setPosition', { bottom: 20, right: 20 })">setPosition(legacy {bottom,right})</button>
|
||||
<button data-replypier-open>data-replypier-open</button>
|
||||
<button id="my-trigger">#my-trigger (trigger option)</button>
|
||||
</div>
|
||||
<div id="log"></div>
|
||||
<div class="overlay" id="z-overlay" hidden>Fixed overlay at z-index 100 — with the z-index card, the widget stays UNDER this box.</div>
|
||||
<div class="tall">Scroll filler — check the widget stays fixed while scrolling.</div>
|
||||
|
||||
<script>
|
||||
const params = new URLSearchParams(location.search)
|
||||
const settings = JSON.parse(localStorage.getItem('rp-test-settings') || '{}')
|
||||
const mode = settings.mode || 'stub'
|
||||
document.getElementById('title').textContent = params.get('title') || 'ReplyPier UI test'
|
||||
document.getElementById('mode-label').textContent = 'mode: ' + mode
|
||||
|
||||
function log(msg) {
|
||||
const el = document.getElementById('log')
|
||||
el.textContent += msg + '\n'
|
||||
el.scrollTop = el.scrollHeight
|
||||
console.log('[harness]', msg)
|
||||
}
|
||||
|
||||
function showBanner(html) {
|
||||
const el = document.getElementById('banner')
|
||||
el.innerHTML = html
|
||||
el.style.display = 'block'
|
||||
}
|
||||
|
||||
// Every control goes through here, so a loader that failed to load reports itself
|
||||
// instead of the buttons silently doing nothing.
|
||||
function api(method, arg) {
|
||||
if (!window.ReplyPier) {
|
||||
log('⚠ ReplyPier is not loaded — see the message above')
|
||||
return
|
||||
}
|
||||
const result = window.ReplyPier[method](arg)
|
||||
if (method === 'isOpen') log('isOpen: ' + result)
|
||||
}
|
||||
|
||||
if (params.get('config') && JSON.parse(params.get('config')).zIndex) {
|
||||
document.getElementById('z-overlay').hidden = false
|
||||
}
|
||||
|
||||
if (mode === 'stub') {
|
||||
// Fake widget served instead of the real /widgets/{id}/data. It runs inside the
|
||||
// loader's srcdoc iframe, so its messages pass the loader's source checks —
|
||||
// including the dashboard widget_display defaults used by scenarios 10 and 11.
|
||||
const dashboardDisplay = params.get('dashboard') || 'undefined'
|
||||
const fakeWidgetHtml = `<!DOCTYPE html><html><head><style>
|
||||
body { margin:0; font-family:system-ui; background:#123; color:#fff; display:flex; flex-direction:column; height:100vh; }
|
||||
#head { padding:10px; background:#0a1522; display:flex; justify-content:space-between; align-items:center; }
|
||||
#head button { border:none; border-radius:6px; padding:4px 10px; cursor:pointer; }
|
||||
#body { flex:1; padding:12px; font-size:13px; } #input { padding:12px; background:#0a1522; font-size:13px; }
|
||||
</style></head><body>
|
||||
<div id="head"><span>FAKE WIDGET</span><button onclick="parent.postMessage({type:'WIDGET_MINIMIZE'},'*')">minimize</button></div>
|
||||
<div id="body">waiting for WIDGET_DISPLAY…</div>
|
||||
<div id="input">input: <span id="prefill">(empty)</span></div>
|
||||
<scr` + `ipt>
|
||||
window.parent.postMessage({ type: 'CUSTOMIZATION_UPDATE', data: {
|
||||
chat_bubble_color: '#3366FF',
|
||||
chat_style: '${params.get('style') || 'CHATBOT'}',
|
||||
chat_initiation_messages: ['Hi from the fake widget!'],
|
||||
widget_display: ${dashboardDisplay}
|
||||
}}, '*')
|
||||
window.parent.postMessage({ type: 'UNREAD_COUNT', count: 3 }, '*')
|
||||
window.addEventListener('message', function (e) {
|
||||
if (!e.data) return
|
||||
if (e.data.type === 'PREFILL_MESSAGE') document.getElementById('prefill').textContent = e.data.text
|
||||
if (e.data.type === 'WIDGET_DISPLAY') document.getElementById('body').textContent = 'WIDGET_DISPLAY: ' + JSON.stringify(e.data)
|
||||
})
|
||||
</scr` + `ipt></body></html>`
|
||||
|
||||
const realFetch = window.fetch
|
||||
window.fetch = function (url) {
|
||||
if (String(url).includes('/widgets/')) {
|
||||
log('fetch stubbed: ' + url)
|
||||
return Promise.resolve(new Response(fakeWidgetHtml, { status: 200, headers: { 'Content-Type': 'text/html' } }))
|
||||
}
|
||||
return realFetch.apply(this, arguments)
|
||||
}
|
||||
window.replypierId = 'stub-widget-id'
|
||||
window.replypierBaseUrl = 'http://localhost:9/api/v1' // never hit — fetch is stubbed
|
||||
} else {
|
||||
window.replypierId = settings.widgetId || ''
|
||||
// Without this the loader falls back to the URL baked in at build time, which for
|
||||
// a production build is the cloud API — not your local backend.
|
||||
window.replypierBaseUrl = settings.baseUrl || 'http://localhost:8000/api/v1'
|
||||
log('real mode → ' + window.replypierBaseUrl)
|
||||
if (!window.replypierId) {
|
||||
showBanner('No <strong>Widget ID</strong> set for real mode. Add one on the <a href="ui-options.html">gallery page</a>, or switch back to Stub mode.')
|
||||
}
|
||||
}
|
||||
|
||||
// Scenario config. The harness always wires #my-trigger (unless the scenario sets
|
||||
// its own selector) so that button works on every card, not only the trigger one.
|
||||
const scenarioConfig = params.get('config') ? JSON.parse(params.get('config')) : {}
|
||||
if (!scenarioConfig.trigger) scenarioConfig.trigger = '#my-trigger'
|
||||
window.replypierConfig = scenarioConfig
|
||||
if (params.get('config')) log('replypierConfig: ' + params.get('config'))
|
||||
|
||||
// The loader comes from the FRONTEND origin (vite in dev, the dashboard origin in
|
||||
// production) — the same place the generated embed snippet points at. It is never
|
||||
// copied into this folder, where it would go stale and test an old widget.
|
||||
// ('/replypier.min.js' is a stale saved setting from an earlier version.)
|
||||
const savedLoader = settings.loaderUrl === '/replypier.min.js' ? '' : settings.loaderUrl
|
||||
// Scheme follows the page — a hardcoded http:// would be blocked as mixed content
|
||||
// if this harness is ever served over https.
|
||||
const loaderUrl = savedLoader || `${location.protocol}//localhost:5173/webclient/replypier.min.js`
|
||||
const s = document.createElement('script')
|
||||
s.src = loaderUrl
|
||||
s.onload = function () {
|
||||
if (!window.ReplyPier) {
|
||||
showBanner('Loaded <code>' + loaderUrl + '</code> but it never defined <code>window.ReplyPier</code> — is that file really the widget loader?')
|
||||
return
|
||||
}
|
||||
log('loader ready: ' + loaderUrl)
|
||||
;['ready', 'open', 'close'].forEach(function (ev) {
|
||||
ReplyPier.on(ev, function () { log('event: ' + ev) })
|
||||
})
|
||||
ReplyPier.on('unread', function (count) { log('event: unread ' + count) })
|
||||
}
|
||||
s.onerror = function () {
|
||||
showBanner(
|
||||
'Could not load the widget loader from <code>' + loaderUrl + '</code>, so every button on this page does nothing.<br>' +
|
||||
'Start the frontend with <code>cd ../frontend && npm run dev</code> (it serves the loader), ' +
|
||||
'and build the loader once with <code>npm run build:webclient:prod</code>. ' +
|
||||
'Or set a different Loader URL on the <a href="ui-options.html">gallery page</a>.'
|
||||
)
|
||||
}
|
||||
document.head.appendChild(s)
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user