Add loading indicator and complete dark mode support for Maria chatbot

- Add professional loading spinner with "Se încarcă răspunsul..." text
- Implement full dark mode support for loading overlay and API response containers
- Add automatic theme detection for Flowise chatbot initialization
- Add inline script to set theme before page render for instant visibility
- Simplify theme toggle to reload page and reinitialize chatbot with new theme
- Prevent textarea search when page is called with GET parameter
- Fix theme toggle visibility in dark mode

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-15 16:22:33 +03:00
parent 16b1193a3e
commit 971744b66c

View File

@@ -222,8 +222,146 @@
.api-message-scrollable::-webkit-scrollbar-thumb:hover { .api-message-scrollable::-webkit-scrollbar-thumb:hover {
background: #a8a8a8; background: #a8a8a8;
} }
/* Professional loading overlay */
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.98);
backdrop-filter: blur(12px);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 99999;
opacity: 0;
transition: opacity 0.3s ease;
}
.loading-overlay.show {
opacity: 1;
}
.loading-spinner {
width: 56px;
height: 56px;
border: 5px solid rgba(59, 129, 246, 0.2);
border-top-color: #3B81F6;
border-radius: 50%;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-text {
margin-top: 24px;
font-size: 20px;
color: #000000 !important;
font-family: var(--font-sans) !important;
font-weight: 700;
text-shadow: none;
letter-spacing: 0.5px;
opacity: 1 !important;
}
/* Dark mode support for loading overlay */
.dark-mode .loading-overlay,
body.dark-mode .loading-overlay {
background-color: rgba(34, 40, 49, 0.98) !important;
}
.dark-mode .loading-spinner,
body.dark-mode .loading-spinner {
border-color: rgba(59, 129, 246, 0.3) !important;
border-top-color: #5B9BFF !important;
}
.dark-mode .loading-text,
body.dark-mode .loading-text {
color: #ffffff !important;
}
/* Dark mode support for API message container */
.dark-mode .api-message-container,
body.dark-mode .api-message-container {
background-color: rgba(34, 40, 49, 0.95) !important;
}
.dark-mode .api-message-box,
body.dark-mode .api-message-box {
background: #2a2a2a !important;
border-color: #3a3a3a !important;
}
.dark-mode .api-message-header,
body.dark-mode .api-message-header {
border-bottom-color: #3a3a3a !important;
}
.dark-mode .api-message-title,
body.dark-mode .api-message-title {
color: #ffffff !important;
}
.dark-mode .api-message-content,
body.dark-mode .api-message-content {
color: #e0e0e0 !important;
}
.dark-mode .api-message-footer,
body.dark-mode .api-message-footer {
border-top-color: #3a3a3a !important;
}
.dark-mode .user-query,
body.dark-mode .user-query {
background: #3a3a3a !important;
color: #e0e0e0 !important;
}
.dark-mode .api-message-scrollable::-webkit-scrollbar-track,
body.dark-mode .api-message-scrollable::-webkit-scrollbar-track {
background: #2a2a2a !important;
}
.dark-mode .api-message-scrollable::-webkit-scrollbar-thumb,
body.dark-mode .api-message-scrollable::-webkit-scrollbar-thumb {
background: #555555 !important;
}
.dark-mode .api-message-scrollable::-webkit-scrollbar-thumb:hover,
body.dark-mode .api-message-scrollbar-thumb:hover {
background: #666666 !important;
}
/* Dark mode support for header theme toggle button */
.dark-mode #theme-toggle,
body.dark-mode #theme-toggle {
color: #ffffff !important;
opacity: 1 !important;
}
.dark-mode #theme-toggle:hover,
body.dark-mode #theme-toggle:hover {
background-color: rgba(255, 255, 255, 0.2) !important;
}
</style> </style>
</style> </style>
<!-- Set theme BEFORE page renders to prevent flash and ensure toggle visibility -->
<script>
(function() {
const theme = localStorage.getItem('theme') || 'light';
if (theme === 'dark') {
document.documentElement.classList.add('dark-mode');
}
})();
</script>
</head> </head>
<body class="min-h-screen"> <body class="min-h-screen">
<!-- Soft Professional Blue Header --> <!-- Soft Professional Blue Header -->
@@ -399,8 +537,40 @@
// Determină dacă să afișeze prompt-urile inițiale sau nu // Determină dacă să afișeze prompt-urile inițiale sau nu
const starterPrompts = initialMessage ? [] : defaultStarterPrompts; const starterPrompts = initialMessage ? [] : defaultStarterPrompts;
// Inițializează chatbot-ul // Detectează tema activă (dark-mode sau light-mode)
const isDarkMode = document.body.classList.contains('dark-mode') ||
document.documentElement.classList.contains('dark-mode') ||
localStorage.getItem('theme') === 'dark';
console.log('🎨 Current theme:', isDarkMode ? 'dark' : 'light');
// Configurează culorile în funcție de temă
const themeColors = isDarkMode ? {
backgroundColor: "#2a2a2a",
botMessageBg: "#3a3a3a",
botMessageText: "#e0e0e0",
userMessageBg: "#5B9BFF",
userMessageText: "#ffffff",
inputBg: "#3a3a3a",
inputText: "#e0e0e0",
sendButtonColor: "#5B9BFF",
feedbackColor: "#e0e0e0",
footerTextColor: "#e0e0e0"
} : {
backgroundColor: "#ffffff",
botMessageBg: "#f7f8ff",
botMessageText: "#303235",
userMessageBg: "#3B81F6",
userMessageText: "#ffffff",
inputBg: "#ffffff",
inputText: "#303235",
sendButtonColor: "#3B81F6",
feedbackColor: "#303235",
footerTextColor: "#303235"
};
// Inițializează chatbot-ul cu tema detectată
const chatbotInstance = Chatbot.initFull({ const chatbotInstance = Chatbot.initFull({
chatflowid: "d4911620-07fe-41f8-adb4-f2f52d6ec766", chatflowid: "d4911620-07fe-41f8-adb4-f2f52d6ec766",
apiHost: "https://mutual-special-koala.ngrok-free.app", apiHost: "https://mutual-special-koala.ngrok-free.app",
@@ -412,7 +582,7 @@
showAgentMessages: true, showAgentMessages: true,
welcomeMessage: 'Bună! Eu sunt Maria, specialistul dvs. de suport tehnic Romfast. Sunt aici pentru a vă ajuta cu orice întrebări sau probleme tehnice legate de utilizarea sistemului ROA. Cum vă pot ajuta astăzi?', welcomeMessage: 'Bună! Eu sunt Maria, specialistul dvs. de suport tehnic Romfast. Sunt aici pentru a vă ajuta cu orice întrebări sau probleme tehnice legate de utilizarea sistemului ROA. Cum vă pot ajuta astăzi?',
errorMessage: 'Am o eroare! Revino mai tarziu', errorMessage: 'Am o eroare! Revino mai tarziu',
backgroundColor: "#ffffff", backgroundColor: themeColors.backgroundColor,
height: 700, height: 700,
width: 500, width: 500,
fontSize: 16, fontSize: 16,
@@ -420,22 +590,22 @@
starterPromptFontSize: 15, starterPromptFontSize: 15,
clearChatOnReload: true, clearChatOnReload: true,
botMessage: { botMessage: {
backgroundColor: "#f7f8ff", backgroundColor: themeColors.botMessageBg,
textColor: "#303235", textColor: themeColors.botMessageText,
showAvatar: true, showAvatar: true,
avatarSrc: "https://raw.githubusercontent.com/zahidkhawaja/langchain-chat-nextjs/main/public/parroticon.png", avatarSrc: "https://raw.githubusercontent.com/zahidkhawaja/langchain-chat-nextjs/main/public/parroticon.png",
}, },
userMessage: { userMessage: {
backgroundColor: "#3B81F6", backgroundColor: themeColors.userMessageBg,
textColor: "#ffffff", textColor: themeColors.userMessageText,
showAvatar: true, showAvatar: true,
avatarSrc: "https://raw.githubusercontent.com/zahidkhawaja/langchain-chat-nextjs/main/public/usericon.png", avatarSrc: "https://raw.githubusercontent.com/zahidkhawaja/langchain-chat-nextjs/main/public/usericon.png",
}, },
textInput: { textInput: {
placeholder: 'Scrieți întrebarea dvs.', placeholder: 'Scrieți întrebarea dvs.',
backgroundColor: '#ffffff', backgroundColor: themeColors.inputBg,
textColor: '#303235', textColor: themeColors.inputText,
sendButtonColor: '#3B81F6', sendButtonColor: themeColors.sendButtonColor,
maxChars: 2000, maxChars: 2000,
maxCharsWarningMessage: 'Ați depășit limita de caractere. Vă rugăm să introduceți mai puțin de 2000 de caractere.', maxCharsWarningMessage: 'Ați depășit limita de caractere. Vă rugăm să introduceți mai puțin de 2000 de caractere.',
autoFocus: true, autoFocus: true,
@@ -443,10 +613,10 @@
receiveMessageSound: false, receiveMessageSound: false,
}, },
feedback: { feedback: {
color: '#303235', color: themeColors.feedbackColor,
}, },
footer: { footer: {
textColor: '#303235', textColor: themeColors.footerTextColor,
text: 'Powered by', text: 'Powered by',
company: 'Romfast', company: 'Romfast',
companyLink: 'https://www.romfast.ro', companyLink: 'https://www.romfast.ro',
@@ -457,6 +627,9 @@
// Funcție pentru a afișa răspunsul API direct în pagină // Funcție pentru a afișa răspunsul API direct în pagină
function displayApiResponse(question, response) { function displayApiResponse(question, response) {
// Ascunde indicatorul de loading
hideLoadingIndicator();
const container = document.createElement('div'); const container = document.createElement('div');
container.className = 'api-message-container'; container.className = 'api-message-container';
@@ -538,18 +711,20 @@
// Funcție pentru a trimite mesajul direct prin API Flowise // Funcție pentru a trimite mesajul direct prin API Flowise
async function sendMessageViaAPI(message) { async function sendMessageViaAPI(message) {
console.log('📡 sendMessageViaAPI started');
try { try {
const apiHost = "https://mutual-special-koala.ngrok-free.app"; const apiHost = "https://mutual-special-koala.ngrok-free.app";
const chatflowId = "d4911620-07fe-41f8-adb4-f2f52d6ec766"; const chatflowId = "d4911620-07fe-41f8-adb4-f2f52d6ec766";
const requestBody = { const requestBody = {
question: message, question: message,
history: [] history: []
}; };
console.log('🔄 Sending request to API...');
const controller = new AbortController(); const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 30000); const timeoutId = setTimeout(() => controller.abort(), 30000);
const response = await fetch(`${apiHost}/api/v1/prediction/${chatflowId}`, { const response = await fetch(`${apiHost}/api/v1/prediction/${chatflowId}`, {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -558,27 +733,31 @@
body: JSON.stringify(requestBody), body: JSON.stringify(requestBody),
signal: controller.signal signal: controller.signal
}); });
clearTimeout(timeoutId); clearTimeout(timeoutId);
if (response.ok) { if (response.ok) {
console.log('✅ API response received successfully');
const data = await response.json(); const data = await response.json();
displayApiResponse(message, data); displayApiResponse(message, data);
return data; return data;
} else { } else {
console.error("Eroare la apelul API:", response.status); console.error("Eroare la apelul API:", response.status);
displayApiResponse(message, `Ne pare rău, a apărut o eroare în procesarea cererii dumneavoastră (cod: ${response.status}). Vă rugăm să încercați din nou mai târziu.`); displayApiResponse(message, `Ne pare rău, a apărut o eroare în procesarea cererii dumneavoastră (cod: ${response.status}). Vă rugăm să încercați din nou mai târziu.`);
return null; return null;
} }
} catch (error) { } catch (error) {
console.error("Eroare la trimiterea mesajului prin API:", error); console.error("Eroare la trimiterea mesajului prin API:", error);
// Ascunde indicatorul de loading în caz de eroare
hideLoadingIndicator();
let errorMessage = "Ne pare rău, a apărut o eroare în conectarea la serviciile noastre. Vă rugăm să verificați conexiunea la internet și să încercați din nou."; let errorMessage = "Ne pare rău, a apărut o eroare în conectarea la serviciile noastre. Vă rugăm să verificați conexiunea la internet și să încercați din nou.";
if (error.name === 'AbortError') { if (error.name === 'AbortError') {
errorMessage = "Request-ul a fost întrerupt din cauza timeout-ului. Vă rugăm să încercați din nou."; errorMessage = "Request-ul a fost întrerupt din cauza timeout-ului. Vă rugăm să încercați din nou.";
} }
displayApiResponse(message, errorMessage); displayApiResponse(message, errorMessage);
return null; return null;
} }
@@ -589,17 +768,17 @@
const toast = document.createElement('div'); const toast = document.createElement('div');
toast.className = 'toast'; toast.className = 'toast';
toast.textContent = message; toast.textContent = message;
if (type === 'error') { if (type === 'error') {
toast.classList.add('error'); toast.classList.add('error');
} }
document.body.appendChild(toast); document.body.appendChild(toast);
setTimeout(() => { setTimeout(() => {
toast.classList.add('show'); toast.classList.add('show');
}, 100); }, 100);
setTimeout(() => { setTimeout(() => {
toast.classList.remove('show'); toast.classList.remove('show');
setTimeout(() => { setTimeout(() => {
@@ -609,6 +788,59 @@
}, 300); }, 300);
}, 3000); }, 3000);
} }
// Funcție pentru a afișa indicatorul de loading
function showLoadingIndicator() {
console.log('🔄 showLoadingIndicator called');
// Verifică dacă există deja un indicator de loading
if (document.querySelector('.loading-overlay')) {
console.log('⚠️ Loading overlay already exists');
return;
}
const overlay = document.createElement('div');
overlay.className = 'loading-overlay';
overlay.id = 'loading-indicator';
const spinner = document.createElement('div');
spinner.className = 'loading-spinner';
const text = document.createElement('div');
text.className = 'loading-text';
text.textContent = 'Se încarcă răspunsul...';
overlay.appendChild(spinner);
overlay.appendChild(text);
document.body.appendChild(overlay);
console.log('✅ Loading overlay created and added to DOM');
// Activează animația de fade-in
setTimeout(() => {
overlay.classList.add('show');
console.log('✅ Loading overlay faded in');
}, 10);
}
// Funcție pentru a ascunde indicatorul de loading
function hideLoadingIndicator() {
console.log('🔽 hideLoadingIndicator called');
const overlay = document.getElementById('loading-indicator');
if (overlay) {
overlay.classList.remove('show');
console.log('✅ Loading overlay fade-out started');
setTimeout(() => {
if (document.body.contains(overlay)) {
document.body.removeChild(overlay);
console.log('✅ Loading overlay removed from DOM');
}
}, 300);
} else {
console.log('⚠️ No loading overlay found to hide');
}
}
// Funcție pentru a găsi textarea-ul chatbot-ului cu MutationObserver // Funcție pentru a găsi textarea-ul chatbot-ului cu MutationObserver
function waitForChatbotInput() { function waitForChatbotInput() {
@@ -875,24 +1107,30 @@
// Inițializează adăugarea butonului când pagina se încarcă // Inițializează adăugarea butonului când pagina se încarcă
window.addEventListener('load', () => { window.addEventListener('load', () => {
// Nu căuta textarea dacă pagina a fost apelată cu parametru GET
if (initialMessage && initialMessage.trim() !== '') {
console.log(' Page called with GET parameter - skipping textarea search');
return;
}
if (linkButtonAdded) return; if (linkButtonAdded) return;
// Încearcă imediat // Încearcă imediat
addLinkButtonToInput(); addLinkButtonToInput();
// Încearcă din nou doar dacă butonul nu a fost adăugat // Încearcă din nou doar dacă butonul nu a fost adăugat
setTimeout(() => { setTimeout(() => {
if (!linkButtonAdded) { if (!linkButtonAdded) {
addLinkButtonToInput(); addLinkButtonToInput();
} }
}, 2000); }, 2000);
setTimeout(() => { setTimeout(() => {
if (!linkButtonAdded) { if (!linkButtonAdded) {
addLinkButtonToInput(); addLinkButtonToInput();
} }
}, 5000); }, 5000);
setTimeout(() => { setTimeout(() => {
if (!linkButtonAdded) { if (!linkButtonAdded) {
addLinkButtonToInput(); addLinkButtonToInput();
@@ -902,8 +1140,14 @@
// Adaugă și un event listener pentru DOMContentLoaded // Adaugă și un event listener pentru DOMContentLoaded
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
// Nu căuta textarea dacă pagina a fost apelată cu parametru GET
if (initialMessage && initialMessage.trim() !== '') {
console.log(' DOMContentLoaded - Page called with GET parameter - skipping textarea search');
return;
}
if (linkButtonAdded) return; if (linkButtonAdded) return;
setTimeout(() => { setTimeout(() => {
if (!linkButtonAdded) { if (!linkButtonAdded) {
addLinkButtonToInput(); addLinkButtonToInput();
@@ -912,44 +1156,60 @@
}); });
// Verifică periodic dacă butonul există și îl adaugă dacă lipsește // Verifică periodic dacă butonul există și îl adaugă dacă lipsește
intervalId = setInterval(() => { // Nu rula intervalul dacă pagina a fost apelată cu parametru GET
// Oprește intervalul dacă butonul a fost adăugat if (!initialMessage || initialMessage.trim() === '') {
if (linkButtonAdded) { intervalId = setInterval(() => {
clearInterval(intervalId); // Oprește intervalul dacă butonul a fost adăugat
intervalId = null; if (linkButtonAdded) {
return; clearInterval(intervalId);
} intervalId = null;
return;
// Verifică dacă butonul există
const existingButton = document.querySelector('.link-button-small');
if (!existingButton) {
// Verifică dacă textarea-ul există
const textarea = document.querySelector('textarea') ||
document.querySelector('input[type="text"]');
if (textarea && textarea.offsetParent !== null) {
addLinkButtonToInput();
} }
} else {
// Dacă butonul există, marchează că a fost adăugat și oprește intervalul // Verifică dacă butonul există
linkButtonAdded = true; const existingButton = document.querySelector('.link-button-small');
clearInterval(intervalId); if (!existingButton) {
intervalId = null; // Verifică dacă textarea-ul există
} const textarea = document.querySelector('textarea') ||
}, 3000); // Verifică la fiecare 3 secunde document.querySelector('input[type="text"]');
if (textarea && textarea.offsetParent !== null) {
addLinkButtonToInput();
}
} else {
// Dacă butonul există, marchează că a fost adăugat și oprește intervalul
linkButtonAdded = true;
clearInterval(intervalId);
intervalId = null;
}
}, 3000); // Verifică la fiecare 3 secunde
} else {
console.log(' Periodic interval - Page called with GET parameter - skipping textarea search');
}
// Dacă există un mesaj inițial, trimite-l direct prin API // Dacă există un mesaj inițial, trimite-l direct prin API
if (initialMessage) { if (initialMessage) {
console.log('📩 Initial message detected:', initialMessage.substring(0, 100) + '...');
if (initialMessage.trim() !== '') { if (initialMessage.trim() !== '') {
console.log('✅ Message is valid, proceeding with API call');
const chatbotElement = document.querySelector('flowise-fullchatbot'); const chatbotElement = document.querySelector('flowise-fullchatbot');
if (chatbotElement) { if (chatbotElement) {
chatbotElement.style.display = 'none'; chatbotElement.style.display = 'none';
console.log('✅ Chatbot element hidden');
} }
// Afișează indicatorul de loading
console.log('🔄 About to show loading indicator...');
showLoadingIndicator();
console.log('📡 Sending message to API...');
sendMessageViaAPI(initialMessage); sendMessageViaAPI(initialMessage);
} else { } else {
console.error("❌ Mesajul initial este gol după decodare"); console.error("❌ Mesajul initial este gol după decodare");
} }
} else {
console.log(' No initial message parameter detected');
} }
</script> </script>
@@ -959,36 +1219,30 @@
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Initialize Lucide icons // Initialize Lucide icons
lucide.createIcons(); lucide.createIcons();
// Theme toggle // Theme toggle
const themeToggle = document.getElementById('theme-toggle'); const themeToggle = document.getElementById('theme-toggle');
const body = document.body; const body = document.body;
const html = document.documentElement; const html = document.documentElement;
// Check for saved theme preference or default to 'light' // Check if dark-mode was already set by inline script
const currentTheme = localStorage.getItem('theme') || 'light'; const isDarkMode = html.classList.contains('dark-mode');
if (currentTheme === 'dark') { if (isDarkMode) {
// Dark mode already set by inline script, just add to body and update icon
body.classList.add('dark-mode'); body.classList.add('dark-mode');
html.classList.add('dark-mode');
themeToggle.innerHTML = '<i data-lucide="moon" class="w-5 h-5"></i>'; themeToggle.innerHTML = '<i data-lucide="moon" class="w-5 h-5"></i>';
} // Reinitialize icons to render the moon icon
themeToggle.addEventListener('click', function() {
body.classList.toggle('dark-mode');
html.classList.toggle('dark-mode');
// Update icon
if (body.classList.contains('dark-mode')) {
themeToggle.innerHTML = '<i data-lucide="moon" class="w-5 h-5"></i>';
localStorage.setItem('theme', 'dark');
} else {
themeToggle.innerHTML = '<i data-lucide="sun" class="w-5 h-5"></i>';
localStorage.setItem('theme', 'light');
}
// Reinitialize icons
lucide.createIcons(); lucide.createIcons();
}
themeToggle.addEventListener('click', function() {
// Save the new theme preference
const newTheme = body.classList.contains('dark-mode') ? 'light' : 'dark';
localStorage.setItem('theme', newTheme);
// Reload page to reinitialize chatbot with new theme
window.location.reload();
}); });
// Mobile menu toggle // Mobile menu toggle