compliance-hub/index.html
2025-10-24 08:21:32 -05:00

1168 lines
No EOL
32 KiB
HTML
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Policy Chatbot</title>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<!-- MSAL.js library for Microsoft authentication -->
<script src="https://alcdn.msauth.net/browser/2.38.0/js/msal-browser.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Montserrat', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: linear-gradient(135deg, #2c2c2c 0%, #1a1a1a 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.chat-container {
width: 100%;
max-width: 800px;
height: 85vh;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px);
border-radius: 20px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.chat-header {
background: linear-gradient(135deg, #FFC407 0%, #e6b007 100%);
color: #1a1a1a;
padding: 20px 25px;
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
}
.chat-header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(45deg, transparent 30%, rgba(255,255,255,0.1) 50%, transparent 70%);
animation: shimmer 3s infinite;
}
@keyframes shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
.header-left {
display: flex;
align-items: center;
gap: 12px;
z-index: 1;
}
.ai-avatar {
width: 40px;
height: 40px;
background: linear-gradient(135deg, #FFC407, #ffcf33);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
.header-info h1 {
font-size: 20px;
font-weight: 600;
margin-bottom: 2px;
}
.status {
font-size: 12px;
opacity: 0.9;
display: flex;
align-items: center;
gap: 6px;
}
.status-dot {
width: 8px;
height: 8px;
background: #4ade80;
border-radius: 50%;
animation: blink 1.5s infinite;
}
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0.3; }
}
.chat-body {
flex: 1;
padding: 20px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 16px;
background: linear-gradient(180deg, #fafafa 0%, #f8fafc 100%);
}
.chat-body::-webkit-scrollbar {
width: 6px;
}
.chat-body::-webkit-scrollbar-track {
background: transparent;
}
.chat-body::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
border-radius: 3px;
}
.message {
display: flex;
align-items: flex-start;
gap: 12px;
animation: slideIn 0.3s ease-out;
opacity: 0;
animation-fill-mode: forwards;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.message-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
flex-shrink: 0;
margin-top: 4px;
}
.user .message-avatar {
background: linear-gradient(135deg, #2c2c2c, #1a1a1a);
color: white;
order: 2;
}
.ai .message-avatar {
background: linear-gradient(135deg, #FFC407, #e6b007);
color: #1a1a1a;
}
.message-content {
max-width: 70%;
padding: 16px 20px;
border-radius: 18px;
position: relative;
word-wrap: break-word;
line-height: 1.5;
}
.user .message-content {
background: linear-gradient(135deg, #2c2c2c, #1a1a1a);
color: white;
border-bottom-right-radius: 4px;
margin-left: auto;
order: 1;
}
.ai .message-content {
background: white;
color: #1f2937;
border-bottom-left-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(0, 0, 0, 0.05);
}
/* AI Response Formatting */
.bullet-point {
margin: 8px 0;
padding-left: 12px;
color: #374151;
line-height: 1.6;
}
.numbered-point {
margin: 8px 0;
padding-left: 8px;
color: #374151;
line-height: 1.6;
}
.numbered-point .number {
font-weight: 600;
color: #FFC407;
margin-right: 6px;
}
.section-header {
font-weight: 700;
color: #1f2937;
margin: 16px 0 8px 0;
font-size: 15px;
border-bottom: 2px solid #FFC407;
padding-bottom: 4px;
}
.sub-bullet {
margin: 4px 0;
padding-left: 24px;
color: #6b7280;
font-size: 14px;
line-height: 1.5;
}
.paragraph-break {
margin: 12px 0;
}
.policy-ref {
background: #f3f4f6;
color: #6b7280;
padding: 2px 6px;
border-radius: 4px;
font-size: 13px;
font-style: italic;
}
.message-content strong {
color: #1f2937;
font-weight: 600;
}
.message-content em {
color: #FFC407;
font-style: italic;
}
.user {
flex-direction: row-reverse;
}
.message-time {
font-size: 11px;
opacity: 0.6;
margin-top: 4px;
text-align: right;
}
.user .message-time {
color: rgba(255, 255, 255, 0.8);
}
.ai .message-time {
color: #6b7280;
text-align: left;
}
.chat-footer {
padding: 20px 25px;
background: white;
border-top: 1px solid rgba(0, 0, 0, 0.1);
display: flex;
gap: 12px;
align-items: flex-end;
}
.input-container {
flex: 1;
position: relative;
}
.chat-input {
width: 100%;
padding: 16px 50px 16px 20px;
border: 2px solid #e5e7eb;
border-radius: 25px;
font-size: 15px;
font-family: inherit;
outline: none;
transition: all 0.3s ease;
background: #f9fafb;
resize: none;
min-height: 50px;
max-height: 120px;
}
.chat-input:focus {
border-color: #FFC407;
background: white;
box-shadow: 0 0 0 3px rgba(255, 196, 7, 0.2);
}
.send-button {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
width: 36px;
height: 36px;
border: none;
border-radius: 50%;
background: linear-gradient(135deg, #FFC407, #e6b007);
color: #1a1a1a;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
font-size: 16px;
}
.send-button:hover {
transform: translateY(-50%) scale(1.1);
box-shadow: 0 4px 12px rgba(255, 196, 7, 0.4);
}
.send-button:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: translateY(-50%) scale(1);
}
.typing-indicator {
display: flex;
align-items: center;
gap: 12px;
padding: 16px 0;
opacity: 0;
transform: translateY(10px);
transition: all 0.3s ease;
}
.typing-indicator.show {
opacity: 1;
transform: translateY(0);
}
.typing-dots {
display: flex;
gap: 4px;
}
.typing-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #6b7280;
animation: typingAnimation 1.4s infinite ease-in-out;
}
.typing-dot:nth-child(1) { animation-delay: -0.32s; }
.typing-dot:nth-child(2) { animation-delay: -0.16s; }
.typing-dot:nth-child(3) { animation-delay: 0s; }
@keyframes typingAnimation {
0%, 80%, 100% {
transform: scale(0);
opacity: 0.5;
}
40% {
transform: scale(1);
opacity: 1;
}
}
.welcome-message {
text-align: center;
padding: 40px 20px;
color: #6b7280;
}
.welcome-message h2 {
font-size: 24px;
margin-bottom: 8px;
color: #1f2937;
}
.welcome-message p {
font-size: 16px;
opacity: 0.8;
}
/* Auth button styles */
.auth-container {
z-index: 1;
display: flex;
align-items: center;
gap: 12px;
}
.auth-button {
padding: 8px 20px;
border: 2px solid #1a1a1a;
border-radius: 20px;
background: white;
color: #1a1a1a;
font-weight: 600;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
}
.auth-button:hover {
background: #1a1a1a;
color: white;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.user-profile {
display: flex;
align-items: center;
gap: 10px;
padding: 6px 12px;
background: rgba(255, 255, 255, 0.9);
border-radius: 20px;
border: 2px solid #1a1a1a;
}
.user-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background: #1a1a1a;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 14px;
}
.user-info {
display: flex;
flex-direction: column;
gap: 2px;
}
.user-name {
font-weight: 600;
font-size: 14px;
color: #1a1a1a;
}
.user-email {
font-size: 11px;
color: #6b7280;
}
.logout-button {
padding: 6px 14px;
border: none;
border-radius: 15px;
background: #1a1a1a;
color: white;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.logout-button:hover {
background: #dc2626;
transform: scale(1.05);
}
.hidden {
display: none !important;
}
/* Login Screen Overlay */
.login-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, #2c2c2c 0%, #1a1a1a 100%);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
.login-card {
background: white;
padding: 60px 50px;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
text-align: center;
max-width: 450px;
width: 90%;
}
.login-card .logo {
font-size: 64px;
margin-bottom: 20px;
}
.login-card h1 {
font-size: 28px;
color: #1a1a1a;
margin-bottom: 10px;
font-weight: 700;
}
.login-card p {
font-size: 16px;
color: #6b7280;
margin-bottom: 40px;
line-height: 1.6;
}
.login-card .auth-button {
width: 100%;
padding: 16px 24px;
font-size: 16px;
justify-content: center;
margin-top: 20px;
}
.app-blur {
filter: blur(10px);
pointer-events: none;
}
/* Адаптивность */
@media (max-width: 768px) {
body { padding: 10px; }
.chat-container {
height: 95vh;
border-radius: 15px;
}
.chat-header { padding: 15px 20px; }
.header-info h1 { font-size: 18px; }
.chat-body { padding: 15px; }
.chat-footer { padding: 15px 20px; }
.message-content { max-width: 85%; }
}
@media (max-width: 480px) {
.chat-header { padding: 12px 16px; }
.header-info h1 { font-size: 16px; }
.chat-body { padding: 12px; }
.chat-footer { padding: 12px 16px; }
.message-content {
max-width: 90%;
padding: 12px 16px;
}
}
</style>
</head>
<body>
<!-- Login Screen Overlay -->
<div id="loginOverlay" class="login-overlay">
<div class="login-card">
<div class="logo">🛡️</div>
<h1>Oliver Compliance Hub</h1>
<p>Please sign in with your Microsoft account to access the compliance chat assistant.</p>
<p id="iframeNotice" style="font-size: 13px; color: #f59e0b; margin-bottom: 15px; display: none;">
Running in SharePoint. A popup window will open for authentication.
</p>
<button class="auth-button" onclick="signIn()">
<span>🔐</span>
<span>Sign in with Microsoft</span>
</button>
</div>
</div>
<div id="appContainer" class="chat-container app-blur">
<div class="chat-header">
<div class="header-left">
<div class="ai-avatar">🤖</div>
<div class="header-info">
<h1>Policy Chatbot</h1>
<div class="status">
<div class="status-dot"></div>
<span>Online</span>
</div>
</div>
</div>
<div class="auth-container">
<button id="loginButton" class="auth-button" onclick="signIn()">
<span>🔐</span>
<span>Sign in with Microsoft</span>
</button>
<div id="userProfile" class="user-profile hidden">
<div class="user-avatar" id="userAvatar"></div>
<div class="user-info">
<div class="user-name" id="userName"></div>
<div class="user-email" id="userEmail"></div>
</div>
<button class="logout-button" onclick="signOut()">Logout</button>
</div>
</div>
</div>
<div id="chat" class="chat-body">
<div class="welcome-message">
<h2>Welcome to Oliver Compliance Hub! 🛡️</h2>
<p>Your friendly policy guide is here! Ask me anything about Oliver's compliance policies and procedures.</p>
</div>
</div>
<div id="typingIndicator" class="typing-indicator">
<div class="message-avatar" style="background: linear-gradient(135deg, #FFC407, #e6b007); color: #1a1a1a;">🤖</div>
<div style="color: #6b7280;">
Policy Chatbot is typing
<div class="typing-dots">
<div class="typing-dot"></div>
<div class="typing-dot"></div>
<div class="typing-dot"></div>
</div>
</div>
</div>
<div class="chat-footer">
<div class="input-container">
<textarea
id="message"
class="chat-input"
placeholder="Type your message..."
rows="1"
></textarea>
<button id="sendBtn" class="send-button" onclick="sendMessage()">
</button>
</div>
</div>
</div>
<script>
// Check if running inside iframe
const isInIframe = window.self !== window.top;
console.log('Running in iframe:', isInIframe);
// Show iframe notice if applicable
if (isInIframe) {
const iframeNotice = document.getElementById('iframeNotice');
if (iframeNotice) {
iframeNotice.style.display = 'block';
}
}
// MSAL Configuration for Microsoft SSO with PKCE flow
// Using popup mode for iframe compatibility
const msalConfig = {
auth: {
clientId: "9079054c-9620-4757-a256-23413042f1ef",
authority: "https://login.microsoftonline.com/e519c2e6-bc6d-4fdf-8d9c-923c2f002385",
redirectUri: window.location.origin + window.location.pathname, // Use current page URL
navigateToLoginRequestUrl: false // Always disable to prevent navigation after popup
},
cache: {
cacheLocation: "sessionStorage", // Use sessionStorage for iframe compatibility
storeAuthStateInCookie: true // Store in cookies for cross-tab support
},
system: {
allowNativeBroker: false, // Disables WAM Broker
allowRedirectInIframe: false, // Explicitly disable redirects in iframe
windowHashTimeout: 60000, // Increase timeout for popup window (60s)
iframeHashTimeout: 6000,
loadFrameTimeout: 0,
asyncPopups: false, // Use synchronous popups for better control
loggerOptions: {
loggerCallback: (level, message, containsPii) => {
if (containsPii) return;
console.log(message);
},
piiLoggingEnabled: false,
logLevel: msal.LogLevel.Info
}
}
};
// Request scopes for login
const loginRequest = {
scopes: ["User.Read", "openid", "profile", "email"],
// Popup window configuration for better UX
popupWindowAttributes: {
popupSize: {
height: 600,
width: 500
},
popupPosition: {
top: 100,
left: 100
}
}
};
// Initialize MSAL instance
const msalInstance = new msal.PublicClientApplication(msalConfig);
// Initialize MSAL and handle redirect response
let currentAccount = null;
msalInstance.initialize().then(() => {
// Always handle redirect promise to process popup returns
return msalInstance.handleRedirectPromise();
}).then((response) => {
if (response) {
console.log('Redirect response received:', response);
currentAccount = response.account;
// If we're in a popup window (opened by parent for auth), just close
if (window.opener && window.opener !== window) {
console.log('Auth successful in popup, closing...');
window.close();
return;
}
updateUIForSignedInUser(currentAccount);
} else {
// Check if user is already signed in
const accounts = msalInstance.getAllAccounts();
if (accounts.length > 0) {
currentAccount = accounts[0];
updateUIForSignedInUser(currentAccount);
} else {
// No user signed in - keep login overlay visible
disableChatInterface();
}
}
}).catch((error) => {
console.error("MSAL initialization error:", error);
// If we're in a popup window, try to close on error too
if (window.opener && window.opener !== window) {
console.log('Error in popup, closing...');
window.close();
return;
}
disableChatInterface();
});
// Sign In function using popup (works in iframe)
async function signIn() {
try {
console.log('Starting login with popup...');
console.log('IsInIframe:', isInIframe);
const response = await msalInstance.loginPopup({
...loginRequest,
redirectUri: window.location.origin + window.location.pathname,
// Ensure popup stays open until auth completes
prompt: 'select_account'
});
if (response && response.account) {
console.log('Login successful!', response);
currentAccount = response.account;
updateUIForSignedInUser(currentAccount);
} else {
console.warn('Login response received but no account:', response);
}
} catch (error) {
console.error("Login error:", error);
// Better error handling for iframe/popup issues
if (error.errorCode === 'popup_window_error' || error.errorCode === 'empty_window_error') {
alert('Popup was blocked. Please allow popups for this site and try again.\n\nОкно авторизации было заблокировано. Пожалуйста, разрешите всплывающие окна для этого сайта и попробуйте снова.');
} else if (error.errorCode === 'user_cancelled') {
console.log('User cancelled login');
} else if (error.errorCode === 'monitor_window_timeout') {
alert('Authentication timed out. The popup window was closed before login completed.\n\nВремя авторизации истекло. Окно было закрыто до завершения входа.');
} else {
alert("Login failed: " + error.message + "\n\nError code: " + (error.errorCode || 'unknown'));
}
}
}
// Sign Out function - LOCAL LOGOUT ONLY (does not sign out of Microsoft account)
function signOut() {
// Remove account from MSAL cache (local logout only)
if (currentAccount) {
const logoutRequest = {
account: currentAccount
};
msalInstance.logoutPopup(logoutRequest).catch(() => {
// If popup fails, do manual cleanup
msalInstance.clearCache();
});
}
// Clear local state
currentAccount = null;
// Reset UI to login state
disableChatInterface();
// Clear chat history
const chat = document.getElementById("chat");
chat.innerHTML = '';
}
// Update UI for signed-in user
function updateUIForSignedInUser(account) {
if (account) {
const loginButton = document.getElementById("loginButton");
const userProfile = document.getElementById("userProfile");
const userName = document.getElementById("userName");
const userEmail = document.getElementById("userEmail");
const userAvatar = document.getElementById("userAvatar");
// Hide login button, show user profile
loginButton.classList.add("hidden");
userProfile.classList.remove("hidden");
// Populate user info
userName.textContent = account.name || "User";
userEmail.textContent = account.username || "";
// Set avatar with first letter of name
const firstLetter = account.name ? account.name.charAt(0).toUpperCase() : "U";
userAvatar.textContent = firstLetter;
// Enable app access
enableChatInterface();
}
}
// Enable chat interface after successful login
function enableChatInterface() {
const loginOverlay = document.getElementById("loginOverlay");
const appContainer = document.getElementById("appContainer");
const messageInput = document.getElementById("message");
const sendBtn = document.getElementById("sendBtn");
// Hide login overlay and show app
loginOverlay.classList.add("hidden");
appContainer.classList.remove("app-blur");
// Enable chat controls
messageInput.disabled = false;
sendBtn.disabled = false;
messageInput.focus();
}
// Disable chat interface (show login screen)
function disableChatInterface() {
const loginOverlay = document.getElementById("loginOverlay");
const appContainer = document.getElementById("appContainer");
const messageInput = document.getElementById("message");
const sendBtn = document.getElementById("sendBtn");
const loginButton = document.getElementById("loginButton");
const userProfile = document.getElementById("userProfile");
// Show login overlay and blur app
loginOverlay.classList.remove("hidden");
appContainer.classList.add("app-blur");
// Disable chat controls
messageInput.disabled = true;
sendBtn.disabled = true;
// Reset auth UI
loginButton.classList.remove("hidden");
userProfile.classList.add("hidden");
}
// Get access token silently
async function getAccessToken() {
if (!currentAccount) {
throw new Error("No user signed in");
}
const request = {
account: currentAccount,
scopes: loginRequest.scopes
};
try {
const response = await msalInstance.acquireTokenSilent(request);
return response.accessToken;
} catch (error) {
console.warn("Silent token acquisition failed, using popup");
const response = await msalInstance.acquireTokenPopup(request);
return response.accessToken;
}
}
const chat = document.getElementById("chat");
const messageInput = document.getElementById("message");
const sendBtn = document.getElementById("sendBtn");
const typingIndicator = document.getElementById("typingIndicator");
// Store conversation ID for continuity
let currentConversationId = null;
// Автоматическое изменение размера textarea
messageInput.addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = Math.min(this.scrollHeight, 120) + 'px';
});
// Отправка по Enter (Shift+Enter для новой строки)
messageInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
function getCurrentTime() {
return new Date().toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit'
});
}
function addMessage(content, isUser = false) {
const messageDiv = document.createElement('div');
messageDiv.className = `message ${isUser ? 'user' : 'ai'}`;
const avatar = document.createElement('div');
avatar.className = 'message-avatar';
avatar.textContent = isUser ? '👤' : '🤖';
const messageContent = document.createElement('div');
messageContent.className = 'message-content';
// Apply formatting for AI responses
if (!isUser) {
messageContent.innerHTML = formatAIResponse(content);
} else {
messageContent.innerHTML = content;
}
const time = document.createElement('div');
time.className = 'message-time';
time.textContent = getCurrentTime();
messageDiv.appendChild(avatar);
messageDiv.appendChild(messageContent);
messageContent.appendChild(time);
// Remove welcome message on first message
const welcomeMsg = chat.querySelector('.welcome-message');
if (welcomeMsg) {
welcomeMsg.remove();
}
chat.appendChild(messageDiv);
chat.scrollTop = chat.scrollHeight;
return messageDiv;
}
function showTypingIndicator() {
typingIndicator.classList.add('show');
chat.appendChild(typingIndicator);
chat.scrollTop = chat.scrollHeight;
}
function hideTypingIndicator() {
typingIndicator.classList.remove('show');
if (typingIndicator.parentNode) {
typingIndicator.parentNode.removeChild(typingIndicator);
}
}
async function sendMessage() {
// Check if user is authenticated
if (!currentAccount) {
alert("Please sign in to use the chat.");
return;
}
const msg = messageInput.value.trim();
if (!msg) return;
// Block interface
messageInput.disabled = true;
sendBtn.disabled = true;
// Add user message
addMessage(msg, true);
messageInput.value = "";
messageInput.style.height = 'auto';
// Show typing indicator
showTypingIndicator();
try {
// First try the webhook
const response = await fetch("https://hook.us1.make.celonis.com/msv2agehbph7w5cvdoeovcb8cclf2rxk", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-make-apikey": "123qwe"
},
body: JSON.stringify({
message: msg,
conversationId: currentConversationId,
userEmail: currentAccount?.username || "unknown",
userName: currentAccount?.name || "Unknown User"
}),
mode: 'cors'
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
// Simulate realistic delay
await new Promise(resolve => setTimeout(resolve, 800 + Math.random() * 1200));
// Get response as text first
const responseText = await response.text();
console.log('Raw response:', responseText);
// Parse JSON and extract reply
let aiReply;
let conversationId;
try {
// Clean the response text - remove conversation ID suffix if present
let cleanResponseText = responseText;
// Check if response has conversation ID appended (pattern: }conv_xxxxx)
const convIdMatch = cleanResponseText.match(/}(conv_[a-f0-9]+)$/);
if (convIdMatch) {
// Remove the conversation ID from the end
cleanResponseText = cleanResponseText.replace(convIdMatch[1], '');
conversationId = convIdMatch[1];
console.log('Extracted conversation ID:', conversationId);
}
const data = JSON.parse(cleanResponseText);
aiReply = data.reply || data.result || data.message || "Sorry, I couldn't process your request.";
// Clean up escaped characters and line breaks
if (aiReply) {
aiReply = aiReply
.replace(/\\n/g, '\n') // Convert \n to actual line breaks
.replace(/\\"/g, '"') // Convert \" to actual quotes
.replace(/\\\\/g, '\\') // Convert \\ to single backslash
.trim();
}
console.log('JSON parsed successfully');
} catch (e) {
console.log('JSON parse failed, treating as text');
// If JSON parsing fails, try to extract just the reply content
let cleanText = responseText;
// Try to extract content between "reply": " and the end
const replyMatch = cleanText.match(/"reply"\s*:\s*"([^"]*(?:\\.[^"]*)*)"/)
if (replyMatch) {
aiReply = replyMatch[1]
.replace(/\\n/g, '\n')
.replace(/\\"/g, '"')
.replace(/\\\\/g, '\\')
.trim();
} else {
aiReply = cleanText;
}
}
// Save conversation ID if present
if (conversationId) {
currentConversationId = conversationId;
console.log('Conversation ID saved:', currentConversationId);
}
// Hide typing indicator
hideTypingIndicator();
// Add AI response with clean text
addMessage(aiReply);
console.log('Message added to chat');
} catch (error) {
hideTypingIndicator();
// Enhanced error handling with fallback
console.error('API Error:', error);
if (error.name === 'TypeError' && error.message.includes('fetch')) {
// Network/CORS error - provide fallback response
addMessage(`🤖 I'm currently having connection issues with my main server. This might be due to CORS restrictions or network problems.
**Possible solutions:**
• Check if the webhook URL is accessible
• Verify CORS settings on the server
• Try accessing from a different network
• Use a CORS proxy for testing
For now, I'm running in demo mode. How can I help you today?`, false);
} else {
addMessage(`❌ **Error:** ${error.message}
*This might be a temporary connection issue. Please try again in a moment.*`, false);
}
} finally {
// Unblock interface
messageInput.disabled = false;
sendBtn.disabled = false;
messageInput.focus();
}
}
// Focus on input field when page loads
window.addEventListener('load', () => {
messageInput.focus();
});
// Format AI response for better readability
function formatAIResponse(text) {
return text
// Handle bullet points with dashes
.replace(/^- (.+)$/gm, '<div class="bullet-point">• $1</div>')
// Handle numbered lists
.replace(/^(\d+\.) (.+)$/gm, '<div class="numbered-point"><span class="number">$1</span> $2</div>')
// Handle bold text
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
// Handle section headers (lines that end with colon and start new section)
.replace(/^([A-Z][^:\n]*):$/gm, '<div class="section-header">$1</div>')
// Handle sub-bullets (lines starting with spaces and dashes)
.replace(/^\s+- (.+)$/gm, '<div class="sub-bullet">◦ $1</div>')
// Handle emphasis with underscores
.replace(/_(.+?)_/g, '<em>$1</em>')
// Convert line breaks to proper spacing
.replace(/\n\n/g, '<div class="paragraph-break"></div>')
.replace(/\n/g, '<br>')
// Handle special formatting for policy references
.replace(/\(([^)]+\.pdf|SharePoint|handbook)\)/g, '<span class="policy-ref">($1)</span>');
}
</script>
</body>
</html>