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 {
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>
<!-- 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>
<body class="min-h-screen">
<!-- Soft Professional Blue Header -->
@@ -399,8 +537,40 @@
// Determină dacă să afișeze prompt-urile inițiale sau nu
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({
chatflowid: "d4911620-07fe-41f8-adb4-f2f52d6ec766",
apiHost: "https://mutual-special-koala.ngrok-free.app",
@@ -412,7 +582,7 @@
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?',
errorMessage: 'Am o eroare! Revino mai tarziu',
backgroundColor: "#ffffff",
backgroundColor: themeColors.backgroundColor,
height: 700,
width: 500,
fontSize: 16,
@@ -420,22 +590,22 @@
starterPromptFontSize: 15,
clearChatOnReload: true,
botMessage: {
backgroundColor: "#f7f8ff",
textColor: "#303235",
backgroundColor: themeColors.botMessageBg,
textColor: themeColors.botMessageText,
showAvatar: true,
avatarSrc: "https://raw.githubusercontent.com/zahidkhawaja/langchain-chat-nextjs/main/public/parroticon.png",
},
userMessage: {
backgroundColor: "#3B81F6",
textColor: "#ffffff",
backgroundColor: themeColors.userMessageBg,
textColor: themeColors.userMessageText,
showAvatar: true,
avatarSrc: "https://raw.githubusercontent.com/zahidkhawaja/langchain-chat-nextjs/main/public/usericon.png",
},
textInput: {
placeholder: 'Scrieți întrebarea dvs.',
backgroundColor: '#ffffff',
textColor: '#303235',
sendButtonColor: '#3B81F6',
backgroundColor: themeColors.inputBg,
textColor: themeColors.inputText,
sendButtonColor: themeColors.sendButtonColor,
maxChars: 2000,
maxCharsWarningMessage: 'Ați depășit limita de caractere. Vă rugăm să introduceți mai puțin de 2000 de caractere.',
autoFocus: true,
@@ -443,10 +613,10 @@
receiveMessageSound: false,
},
feedback: {
color: '#303235',
color: themeColors.feedbackColor,
},
footer: {
textColor: '#303235',
textColor: themeColors.footerTextColor,
text: 'Powered by',
company: 'Romfast',
companyLink: 'https://www.romfast.ro',
@@ -457,6 +627,9 @@
// Funcție pentru a afișa răspunsul API direct în pagină
function displayApiResponse(question, response) {
// Ascunde indicatorul de loading
hideLoadingIndicator();
const container = document.createElement('div');
container.className = 'api-message-container';
@@ -538,18 +711,20 @@
// Funcție pentru a trimite mesajul direct prin API Flowise
async function sendMessageViaAPI(message) {
console.log('📡 sendMessageViaAPI started');
try {
const apiHost = "https://mutual-special-koala.ngrok-free.app";
const chatflowId = "d4911620-07fe-41f8-adb4-f2f52d6ec766";
const requestBody = {
question: message,
history: []
};
console.log('🔄 Sending request to API...');
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 30000);
const response = await fetch(`${apiHost}/api/v1/prediction/${chatflowId}`, {
method: 'POST',
headers: {
@@ -558,27 +733,31 @@
body: JSON.stringify(requestBody),
signal: controller.signal
});
clearTimeout(timeoutId);
if (response.ok) {
console.log('✅ API response received successfully');
const data = await response.json();
displayApiResponse(message, data);
return data;
} 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.`);
return null;
}
} 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.";
if (error.name === 'AbortError') {
errorMessage = "Request-ul a fost întrerupt din cauza timeout-ului. Vă rugăm să încercați din nou.";
}
displayApiResponse(message, errorMessage);
return null;
}
@@ -589,17 +768,17 @@
const toast = document.createElement('div');
toast.className = 'toast';
toast.textContent = message;
if (type === 'error') {
toast.classList.add('error');
}
document.body.appendChild(toast);
setTimeout(() => {
toast.classList.add('show');
}, 100);
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => {
@@ -609,6 +788,59 @@
}, 300);
}, 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
function waitForChatbotInput() {
@@ -875,24 +1107,30 @@
// Inițializează adăugarea butonului când pagina se încarcă
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;
// Încearcă imediat
addLinkButtonToInput();
// Încearcă din nou doar dacă butonul nu a fost adăugat
setTimeout(() => {
if (!linkButtonAdded) {
addLinkButtonToInput();
}
}, 2000);
setTimeout(() => {
if (!linkButtonAdded) {
addLinkButtonToInput();
}
}, 5000);
setTimeout(() => {
if (!linkButtonAdded) {
addLinkButtonToInput();
@@ -902,8 +1140,14 @@
// Adaugă și un event listener pentru 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;
setTimeout(() => {
if (!linkButtonAdded) {
addLinkButtonToInput();
@@ -912,44 +1156,60 @@
});
// Verifică periodic dacă butonul există și îl adaugă dacă lipsește
intervalId = setInterval(() => {
// Oprește intervalul dacă butonul a fost adăugat
if (linkButtonAdded) {
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();
// Nu rula intervalul dacă pagina a fost apelată cu parametru GET
if (!initialMessage || initialMessage.trim() === '') {
intervalId = setInterval(() => {
// Oprește intervalul dacă butonul a fost adăugat
if (linkButtonAdded) {
clearInterval(intervalId);
intervalId = null;
return;
}
} 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
// 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
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
if (initialMessage) {
console.log('📩 Initial message detected:', initialMessage.substring(0, 100) + '...');
if (initialMessage.trim() !== '') {
console.log('✅ Message is valid, proceeding with API call');
const chatbotElement = document.querySelector('flowise-fullchatbot');
if (chatbotElement) {
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);
} else {
console.error("❌ Mesajul initial este gol după decodare");
}
} else {
console.log(' No initial message parameter detected');
}
</script>
@@ -959,36 +1219,30 @@
document.addEventListener('DOMContentLoaded', function() {
// Initialize Lucide icons
lucide.createIcons();
// Theme toggle
const themeToggle = document.getElementById('theme-toggle');
const body = document.body;
const html = document.documentElement;
// Check for saved theme preference or default to 'light'
const currentTheme = localStorage.getItem('theme') || 'light';
if (currentTheme === 'dark') {
// Check if dark-mode was already set by inline script
const isDarkMode = html.classList.contains('dark-mode');
if (isDarkMode) {
// Dark mode already set by inline script, just add to body and update icon
body.classList.add('dark-mode');
html.classList.add('dark-mode');
themeToggle.innerHTML = '<i data-lucide="moon" class="w-5 h-5"></i>';
}
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
// Reinitialize icons to render the moon icon
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