window.DIRECTWISE_SITE_ID='21111111-1111-1111-1111-111111111111';
window.DIRECTWISE_API_URL='https://directwise.forclus.net';
(function () {
// Get site_id and API URL injected by backend
const SITE_ID = window.DIRECTWISE_SITE_ID;
const API_BASE = window.DIRECTWISE_API_URL || '';
if (!SITE_ID) {
console.error('DirectWise Error: site_id not provided');
return;
}
if (!API_BASE) {
console.error('DirectWise Error: API base URL not provided');
return;
}
// --- CORE BUSINESS LOGIC (UNCHANGED) ---
function getSessionId() { return localStorage.getItem('directwise_session_id'); }
function setSessionId(sid) { localStorage.setItem('directwise_session_id', sid); }
// --- THEME MANAGEMENT (NEW) ---
function saveTheme(theme) { localStorage.setItem('directwise_theme', theme); }
function loadTheme() { return localStorage.getItem('directwise_theme') || 'light'; }
// --- UI & DOM SETUP ---
const container = document.createElement('div');
container.id = 'dw-container';
container.className = 'dw-widget-container';
container.setAttribute('data-dw-widget', '1');
// SVG Icons for a modern look
const ICONS = {
chat: ``,
close: ``,
send: ``,
settings: ``,
back: ``,
};
container.innerHTML = `
`;
document.body.appendChild(container);
const toggle = document.createElement('button');
toggle.id = 'dw-toggle-button';
toggle.setAttribute('aria-label', 'Open Chat');
toggle.innerHTML = `${ICONS.chat}
${ICONS.close}
`;
document.body.appendChild(toggle);
const style = document.createElement('style');
style.textContent = `
:root {
/* Light Theme (Default) */
--dw-font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--dw-bg: #ffffff;
--dw-header-bg: #f9fafb;
--dw-text: #1f2937;
--dw-text-muted: #6b7280;
--dw-border: #e5e7eb;
--dw-user-message-bg: #dbeafe;
--dw-user-message-text: #1e40af;
--dw-bot-message-bg: #f3f4f6;
--dw-bot-message-text: #1f2937;
--dw-input-bg: #ffffff;
--dw-accent: #2563eb;
--dw-accent-hover: #1d4ed8;
--dw-toggle-bg: var(--dw-accent);
--dw-toggle-bg-hover: var(--dw-accent-hover);
--dw-shadow: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1);
--dw-link: #1a66cc;
--dw-link-visited: #744fc7;
--dw-link-hover: #ee8800;
--dw-highlight-bg: #ffe870;
}
.dw-widget-container.dark-mode {
/* Dark Theme */
--dw-bg: #111827;
--dw-header-bg: #1f2937;
--dw-text: #f9fafb;
--dw-text-muted: #9ca3af;
--dw-border: #374151;
--dw-user-message-bg: #2563eb;
--dw-user-message-text: #ffffff;
--dw-bot-message-bg: #374151;
--dw-bot-message-text: #f3f4f6;
--dw-input-bg: #1f2937;
--dw-accent: #3b82f6;
--dw-accent-hover: #60a5fa;
--dw-link: #60a5fa;
--dw-link-visited: #a78bfa;
--dw-link-hover: #f59e0b;
--dw-highlight-bg: #b45309;
}
/* General Widget Styles */
#dw-container, #dw-toggle-button, .dw-highlight {
all: initial; /* CSS Reset */
font-family: var(--dw-font-family) !important;
}
#dw-container *, #dw-toggle-button * {
box-sizing: border-box;
}
#dw-toggle-button {
position: fixed; bottom: 20px; right: 20px;
width: 60px; height: 60px;
border-radius: 50%;
background-color: var(--dw-toggle-bg);
border: none;
box-shadow: var(--dw-shadow);
cursor: pointer;
z-index: 10000;
transition: transform 0.3s ease, background-color 0.2s, opacity 0.3s;
display: flex; align-items: center; justify-content: center;
padding: 0;
overflow: hidden;
}
#dw-toggle-button:hover { background-color: var(--dw-toggle-bg-hover); transform: scale(1.05); }
#dw-toggle-button svg { width: 32px; height: 32px; color: white; }
#dw-toggle-button .dw-icon-open, #dw-toggle-button .dw-icon-close {
transition: transform 0.3s ease, opacity 0.3s;
position: absolute;
}
#dw-toggle-button .dw-icon-close { transform: scale(0.7) translateY(100%); opacity: 0; }
#dw-container.open ~ #dw-toggle-button .dw-icon-open { transform: scale(0.7) translateY(-100%); opacity: 0; }
#dw-container.open ~ #dw-toggle-button .dw-icon-close { transform: scale(1) translateY(0); opacity: 1; }
#dw-container {
position: fixed; bottom: 90px; right: 20px;
width: 400px; height: 600px; max-height: calc(100vh - 110px);
background: var(--dw-bg); color: var(--dw-text);
border-radius: 16px;
box-shadow: var(--dw-shadow);
display: flex; flex-direction: column;
overflow: hidden; z-index: 9999;
transform-origin: bottom right;
transform: scale(0); opacity: 0;
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.2s ease;
}
#dw-container.open { transform: scale(1); opacity: 1; }
#dw-container .dw-view { display: none; flex-direction: column; width: 100%; height: 100%; background: var(--dw-bg); }
#dw-container .dw-chat-view { display: flex; } /* Shown by default inside container */
#dw-container.settings-active .dw-chat-view { display: none; }
#dw-container.settings-active .dw-settings-view { display: flex; }
#dw-container .dw-header {
display: flex; align-items: center; padding: 8px 12px;
border-bottom: 1px solid var(--dw-border); background: var(--dw-header-bg);
flex-shrink: 0; z-index: 10;
}
/* FIX 2: Increased specificity for title and icons to prevent overriding */
#dw-container .dw-title { flex-grow: 1; font-size: 16px; font-weight: 600; margin: 0; color: var(--dw-text); padding-left: 4px; }
#dw-container .dw-icon-button {
background: none; border: none; padding: 4px; margin-left: 8px;
cursor: pointer; border-radius: 50%; display: flex;
align-items: center; justify-content: center; transition: background-color 0.2s;
}
#dw-container .dw-icon-button:hover { background-color: rgba(0,0,0,0.05); }
#dw-container .dw-icon-button svg { width: 24px; height: 24px; color: var(--dw-text-muted); }
#dw-container.dark-mode .dw-icon-button:hover { background-color: rgba(255,255,255,0.1); }
#dw-container .dw-messages-wrapper {
flex: 1; position: relative; overflow: hidden; display: flex; flex-direction: column;
}
#dw-container #chat-messages {
flex: 1; overflow-y: auto; padding: 16px; display: flex; flex-direction: column;
scrollbar-width: thin; scrollbar-color: var(--dw-border) transparent;
}
#dw-container .message {
padding: 10px 14px; border-radius: 12px;
max-width: 80%; margin-bottom: 10px; line-height: 1.5;
opacity: 0; animation: fadeIn 0.4s ease forwards; word-wrap: break-word;
}
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
#dw-container .message.user { align-self: flex-end; background: var(--dw-user-message-bg); color: var(--dw-user-message-text); border-bottom-right-radius: 4px; }
#dw-container .message.bot { align-self: flex-start; background: var(--dw-bot-message-bg); color: var(--dw-bot-message-text); border-bottom-left-radius: 4px; }
#dw-container .dw-footer {
flex-shrink: 0;
padding: 8px 16px;
text-align: center;
font-size: 12px;
color: var(--dw-text-muted);
border-top: 1px solid var(--dw-border);
}
#dw-container .dw-footer a { color: var(--dw-text-muted); text-decoration: underline; }
#dw-container #chat-form {
display: flex; align-items: flex-end; padding: 10px;
background-color: var(--dw-header-bg);
border-top: 1px solid var(--dw-border); flex-shrink: 0;
}
#dw-container #chat-input {
flex: 1; resize: none !important; /* Disables the weird arrows */
padding: 10px 12px;
border: 1px solid var(--dw-border); border-radius: 20px;
outline: none; font-family: var(--dw-font-family); font-size: 14px;
max-height: 100px; overflow-y: auto; background-color: var(--dw-input-bg); color: var(--dw-text);
transition: border-color 0.2s;
}
#dw-container #chat-input:focus { border-color: var(--dw-accent); }
#dw-container #chat-send-btn {
background: var(--dw-accent); border: none; width: 40px; height: 40px;
border-radius: 50%; color: white; cursor: pointer;
margin-left: 10px; display: flex; align-items: center; justify-content: center;
transition: background-color 0.2s; padding:0; flex-shrink: 0;
}
#dw-container #chat-send-btn:hover { background: var(--dw-accent-hover); }
#dw-container #chat-send-btn svg { width: 20px; height: 20px; }
/* Fades */
#dw-container #chat-fade-top, #dw-container #chat-fade-bottom {
pointer-events: none; position: absolute; left: 0; width: 100%;
z-index: 5; opacity: 0; transition: opacity 0.25s;
}
#dw-container #chat-fade-top { top: 0; height: 36px; background: linear-gradient(to bottom, var(--dw-bg) 60%, transparent); }
#dw-container #chat-fade-bottom { bottom: 0; height: 30px; background: linear-gradient(to top, var(--dw-bg) 60%, transparent); }
/* Settings View */
#dw-container .dw-settings-content { padding: 16px; }
#dw-container .dw-setting-row { display: flex; justify-content: space-between; align-items: center; }
#dw-container .dw-setting-row label { color: var(--dw-text); font-size: 15px; }
#dw-container .dw-switch { position: relative; display: inline-block; width: 44px; height: 24px; }
#dw-container .dw-switch input { opacity: 0; width: 0; height: 0; }
#dw-container .dw-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; border-radius: 24px; }
#dw-container .dw-slider:before { position: absolute; content: ""; height: 18px; width: 18px; left: 3px; bottom: 3px; background-color: white; transition: .4s; border-radius: 50%; }
#dw-container input:checked + .dw-slider { background-color: var(--dw-accent); }
#dw-container input:checked + .dw-slider:before { transform: translateX(20px); }
/* Utility & Special States */
.thinking-anim { font-style: italic; color: var(--dw-text-muted); }
a.enriched-link, #chat-messages a, .dw-footer a { color: var(--dw-link) !important; text-decoration: underline !important; font-weight: 600 !important; }
a.enriched-link:visited, #chat-messages a:visited, .dw-footer a:visited { color: var(--dw-link-visited) !important; }
a.enriched-link:hover, #chat-messages a:hover, .dw-footer a:hover { color: var(--dw-link-hover) !important; }
.dw-highlight { background-color: var(--dw-highlight-bg) !important; color: var(--dw-text) !important; border-radius: 4px !important; padding: 2px 4px !important; }
/* === MOBILE RESPONSIVE (FLOATING WINDOW) === */
@media (max-width: 700px) {
#dw-container {
width: auto;
height: auto;
inset: 10px;
bottom: 75px;
max-height: calc(100vh - 85px);
transform-origin: bottom center;
border-radius: 16px !important;
}
#dw-toggle-button {
bottom: 15px; right: 15px;
width: 54px; height: 54px;
}
#dw-container .message { max-width: 85%; }
}
`;
document.head.appendChild(style);
// --- UI ELEMENT SELECTORS ---
const form = document.getElementById('chat-form');
const input = document.getElementById('chat-input');
const messages = document.getElementById('chat-messages');
const fadeTop = document.getElementById('chat-fade-top');
const fadeBottom = document.getElementById('chat-fade-bottom');
const closeButton = container.querySelector('.dw-close-button-widget');
const settingsButton = container.querySelector('.dw-settings-button');
const backButton = container.querySelector('.dw-back-button');
const themeToggle = document.getElementById('dw-theme-toggle');
// --- UI LOGIC ---
function toggleWidget(forceOpen = null) {
const shouldBeOpen = forceOpen !== null ? forceOpen : !container.classList.contains('open');
container.classList.toggle('open', shouldBeOpen);
toggle.setAttribute('aria-label', shouldBeOpen ? 'Close Chat' : 'Open Chat');
if (shouldBeOpen) {
input.focus();
} else {
container.classList.remove('settings-active');
}
}
toggle.addEventListener('click', () => toggleWidget());
closeButton.addEventListener('click', () => toggleWidget(false));
input.addEventListener('input', () => {
input.style.height = 'auto';
input.style.height = (input.scrollHeight) + 'px';
});
// FIX 1: Send message on Enter key press
input.addEventListener('keydown', (e) => {
// Send if Enter is pressed without the Shift key
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault(); // Prevents adding a new line
form.dispatchEvent(new Event('submit', { cancelable: true, bubbles: true }));
}
});
function scrollToBottom() {
messages.scrollTop = messages.scrollHeight;
updateFade();
}
function updateFade() {
fadeTop.style.opacity = messages.scrollTop > 5 ? '1' : '0';
const isScrolledToBottom = messages.scrollHeight - messages.clientHeight - messages.scrollTop < 5;
fadeBottom.style.opacity = isScrolledToBottom ? '0' : '1';
}
messages.addEventListener('scroll', updateFade, { passive: true });
// Settings Panel Logic
settingsButton.addEventListener('click', () => container.classList.add('settings-active'));
backButton.addEventListener('click', () => container.classList.remove('settings-active'));
// Theme Switch Logic
themeToggle.addEventListener('change', () => {
const theme = themeToggle.checked ? 'dark' : 'light';
container.classList.toggle('dark-mode', theme === 'dark');
saveTheme(theme);
});
function applyInitialTheme() {
const theme = loadTheme();
themeToggle.checked = theme === 'dark';
container.classList.toggle('dark-mode', theme === 'dark');
}
// --- MESSAGING AND API LOGIC (FUNCTIONALITY PRESERVED) ---
function addMessage(text, type) {
const div = document.createElement('div');
div.className = 'message ' + type;
div.textContent = text;
messages.appendChild(div);
scrollToBottom();
}
function addMessageHtml(html, type) {
const div = document.createElement('div');
div.className = 'message ' + type;
div.innerHTML = html;
messages.appendChild(div);
scrollToBottom();
}
function enrichReplyText(text) {
return text.replace(/\[([^\]]+)\]\(#highlight:([^:]+)::([^)]+)\)/g,
function (match, visible, url, fragment) {
const safeVisible = visible.replace(//g, ">");
const safeUrl = url.replace(/"/g, """);
const safeFragment = fragment.replace(/"/g, """);
return `${safeVisible}`;
});
}
function typeBotMessage(text) {
removeThinking();
const highlightRegex = /\[([^\]]+)\]\(#highlight:([^:]+)::([^)]+)\)/g;
let result, lastIndex = 0, blocks = [];
while ((result = highlightRegex.exec(text)) !== null) {
if (result.index > lastIndex) {
blocks.push({ type: 'normal', text: text.substring(lastIndex, result.index) });
}
blocks.push({ type: 'highlight', visible: result[1], url: result[2], fragment: result[3] });
lastIndex = highlightRegex.lastIndex;
}
if (lastIndex < text.length) blocks.push({ type: 'normal', text: text.substring(lastIndex) });
const div = document.createElement('div');
div.className = 'message bot';
messages.appendChild(div);
const spans = [];
blocks.forEach(block => {
let span;
if (block.type === 'normal') {
span = document.createElement('span');
} else if (block.type === 'highlight') {
span = document.createElement('a');
span.className = 'enriched-link';
span.href = 'javascript:void(0)';
span.dataset.url = block.url.replace(/"/g, """);
span.dataset.fragment = block.fragment.replace(/"/g, """);
}
div.appendChild(span);
spans.push({ span, block });
});
scrollToBottom();
let iBlock = 0;
function animate() {
if (iBlock >= blocks.length) return;
const { span, block } = spans[iBlock];
if (block.type === 'highlight') {
span.innerHTML = block.visible.replace(//g, ">");
iBlock++;
setTimeout(animate, 120);
} else {
let iChar = 0;
const interval = setInterval(() => {
if (iChar < block.text.length) {
span.textContent += block.text[iChar];
iChar++;
scrollToBottom();
} else {
clearInterval(interval);
iBlock++;
animate();
}
}, 18);
}
}
animate();
}
let thinkingInterval = null;
function showThinkingAnimated() {
removeThinking();
const div = document.createElement('div');
div.className = 'message bot thinking-anim';
div.id = 'thinking-message';
div.textContent = "Thinking";
messages.appendChild(div);
scrollToBottom();
let dots = 0;
thinkingInterval = setInterval(() => {
dots = (dots + 1) % 4;
div.textContent = "Thinking" + ".".repeat(dots);
}, 400);
}
function removeThinking() {
if (thinkingInterval) clearInterval(thinkingInterval);
thinkingInterval = null;
const div = document.getElementById('thinking-message');
if (div) div.remove();
}
form.addEventListener('submit', async (e) => {
e.preventDefault();
let text = input.value.trim();
if (!text) return;
const MAX_LEN = 350;
if (text.length > MAX_LEN) {
addMessageHtml(`Your message is too long (max ${MAX_LEN} chars).`, 'bot');
return;
}
addMessage(text, 'user');
input.value = '';
input.style.height = 'auto';
showThinkingAnimated();
let payload = { site_id: SITE_ID, message: text };
const sid = getSessionId();
if (sid) payload.session_id = sid;
try {
const res = await fetch(API_BASE + '/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
signal: AbortSignal.timeout(18000)
});
const data = await res.json();
if (data.session_id) setSessionId(data.session_id);
removeThinking();
if (data.reply) typeBotMessage(data.reply);
updateFade();
} catch (err) {
removeThinking();
if (err.name === 'TimeoutError') {
typeBotMessage('Sorry, the assistant took too long to respond. Please try again.');
} else {
typeBotMessage('An error occurred. Please try again later.');
}
console.error('DirectWise Error:', err);
}
});
function highlightFragment(fragment) {
let fragNorm = fragment.trim();
const els = Array.from(document.querySelectorAll('p, li, h1, h2, h3, h4, h5, span, div'));
for (const el of els) {
if (el.closest('[data-dw-widget]')) continue;
for (const node of el.childNodes) {
if (node.nodeType === 3) { // Text node
const idx = node.nodeValue.toLowerCase().indexOf(fragNorm.toLowerCase());
if (idx !== -1) {
const originalText = node.nodeValue;
const match = originalText.slice(idx, idx + fragNorm.length);
const highlightNode = document.createElement('span');
highlightNode.className = "dw-highlight";
highlightNode.textContent = match;
const beforeNode = document.createTextNode(originalText.slice(0, idx));
const afterNode = document.createTextNode(originalText.slice(idx + fragNorm.length));
const parent = node.parentNode;
parent.insertBefore(beforeNode, node);
parent.insertBefore(highlightNode, node);
parent.insertBefore(afterNode, node);
parent.removeChild(node);
highlightNode.scrollIntoView({ behavior: "smooth", block: "center" });
setTimeout(() => {
const fullTextNode = document.createTextNode(originalText);
parent.insertBefore(fullTextNode, beforeNode);
parent.removeChild(beforeNode);
parent.removeChild(highlightNode);
parent.removeChild(afterNode);
}, 3500);
return;
}
}
}
}
}
messages.addEventListener('click', function (e) {
const link = e.target.closest('a');
if (!link) return;
if (link.closest('.dw-footer')) {
return;
}
e.preventDefault();
const url = link.dataset.url;
const fragment = link.dataset.fragment;
if (url && window.location.pathname !== url) {
sessionStorage.setItem('dw-highlight', JSON.stringify({ url, fragment }));
window.location.href = url;
} else if (fragment) {
highlightFragment(fragment);
// FIX 3: Delay closing the widget to allow the scroll animation to be visible
setTimeout(() => {
toggleWidget(false);
}, 400); // 400ms delay to ensure scroll animation is seen
}
});
function loadHistory() {
const sid = getSessionId();
if (!sid) return;
fetch(API_BASE + '/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ site_id: SITE_ID, session_id: sid })
}).then(res => res.json()).then(data => {
if (data.history && data.history.length > 0) {
messages.innerHTML = '';
data.history.forEach(msg => {
if (msg.role === 'user') addMessage(msg.content, 'user');
else if (msg.role === 'assistant') addMessageHtml(enrichReplyText(msg.content), 'bot');
});
setTimeout(scrollToBottom, 100);
}
}).catch(err => console.error('DirectWise Error loading history:', err));
}
window.addEventListener('DOMContentLoaded', () => {
const data = sessionStorage.getItem('dw-highlight');
if (data) {
try {
const { url, fragment } = JSON.parse(data);
if (window.location.pathname === url && fragment) {
toggleWidget(true);
setTimeout(() => highlightFragment(fragment), 400);
}
} catch (e) {}
sessionStorage.removeItem('dw-highlight');
}
});
// --- INITIALIZATION ---
applyInitialTheme();
loadHistory();
})();