From a8bc5530b012d83c1db2f88f72be3dbb337033bf Mon Sep 17 00:00:00 2001 From: Rodrigo Rodriguez Date: Tue, 10 Feb 2026 13:54:16 +0000 Subject: [PATCH] Add config-colors.css and update UI components - Add config-colors.css for dynamic color theming - Update base.html, chat components for better UX - Improve theme manager and HTMX app integration --- ui/suite/base.html | 2 + ui/suite/chat/chat.css | 334 ++++++++++++++++++++++++++++++--- ui/suite/chat/chat.html | 301 +++++++++++++++++++++++++++-- ui/suite/css/config-colors.css | 16 ++ ui/suite/js/htmx-app.js | 35 +++- ui/suite/js/theme-manager.js | 75 +++++++- 6 files changed, 718 insertions(+), 45 deletions(-) create mode 100644 ui/suite/css/config-colors.css diff --git a/ui/suite/base.html b/ui/suite/base.html index 27891a6..9fb8491 100644 --- a/ui/suite/base.html +++ b/ui/suite/base.html @@ -21,6 +21,8 @@ + + diff --git a/ui/suite/chat/chat.css b/ui/suite/chat/chat.css index 149e6e0..d21e732 100644 --- a/ui/suite/chat/chat.css +++ b/ui/suite/chat/chat.css @@ -94,6 +94,38 @@ box-sizing: border-box; } +/* Chat Header */ +.chat-header { + display: flex; + align-items: center; + gap: 16px; + padding: 20px 24px; + background: linear-gradient(135deg, var(--chat-color1, #8B4513) 0%, var(--chat-color2, #F5DEB3) 100%); + border-radius: 16px; + margin-bottom: 20px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); + border: 2px solid rgba(255, 255, 255, 0.1); +} + +.bot-logo { + width: 60px; + height: 60px; + object-fit: contain; + border-radius: 12px; + background: rgba(255, 255, 255, 0.95); + padding: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); +} + +.bot-title { + margin: 0; + font-size: 24px; + font-weight: 700; + color: #ffffff; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); + flex: 1; +} + /* Connection Status - use shared styles from app.css */ @keyframes pulse { @@ -112,32 +144,34 @@ #messages { flex: 1; overflow-y: auto; + overflow-x: hidden; padding: 20px 0; display: flex; flex-direction: column; gap: 16px; scrollbar-width: thin; scrollbar-color: var(--accent, #3b82f6) var(--surface, #1a1a24); + scroll-behavior: smooth; } -/* Custom scrollbar for markers */ +/* Enhanced custom scrollbar */ #messages::-webkit-scrollbar { width: 6px; } #messages::-webkit-scrollbar-track { - background: var(--surface, #1a1a24); + background: transparent; border-radius: 3px; } #messages::-webkit-scrollbar-thumb { - background: var(--accent, #3b82f6); + background: var(--border, rgba(255, 255, 255, 0.2)); border-radius: 3px; - border: 1px solid var(--surface, #1a1a24); + transition: background 0.2s; } #messages::-webkit-scrollbar-thumb:hover { - background: var(--accent-hover, #2563eb); + background: var(--accent, #3b82f6); } /* Scrollbar markers container */ @@ -603,23 +637,61 @@ footer { flex-wrap: wrap; gap: 8px; margin-bottom: 12px; + animation: slideIn 0.3s ease; } +.suggestion-chip, .suggestion-button { - padding: 6px 12px; - border-radius: 16px; - border: 1px solid var(--border-color, #e5e7eb); - background: var(--secondary-bg, #f9fafb); - color: var(--text-primary, #374151); - font-size: 13px; + padding: 10px 18px; + border-radius: 24px; + border: 2px solid var(--suggestion-color, #4a9eff); + background: rgba(255, 255, 255, 0.15); + color: #ffffff; + font-size: 14px; + font-weight: 600; cursor: pointer; - transition: all 0.2s; + transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: hidden; + min-height: 40px; + display: inline-flex; + align-items: center; + justify-content: center; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); } +.suggestion-chip::before, +.suggestion-button::before { + content: ""; + position: absolute; + inset: 0; + background: var(--accent, #3b82f6); + opacity: 0; + transition: opacity 0.25s cubic-bezier(0.4, 0, 0.2, 1); + border-radius: inherit; + z-index: -1; +} + +.suggestion-chip:hover, .suggestion-button:hover { - background: var(--accent-color, #3b82f6); - color: white; - border-color: var(--accent-color, #3b82f6); + background: var(--suggestion-color, #4a9eff); + color: #000000; + border-color: var(--suggestion-color, #6bb3ff); + transform: translateY(-2px) scale(1.02); + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4); + text-shadow: none; +} + +.suggestion-chip:hover::before, +.suggestion-button:hover::before { + opacity: 1; +} + +.suggestion-chip:active, +.suggestion-button:active { + transform: translateY(0); + box-shadow: 0 2px 6px rgba(59, 130, 246, 0.2); } /* Input Container */ @@ -737,28 +809,38 @@ form.input-container { position: fixed; bottom: 100px; right: 20px; - width: 40px; - height: 40px; + width: 44px; + height: 44px; border-radius: 50%; - border: 1px solid var(--border-color, #e5e7eb); - background: var(--primary-bg, #ffffff); - color: var(--text-primary, #374151); + border: none; + background: var(--accent, #3b82f6); + color: white; cursor: pointer; display: none; align-items: center; justify-content: center; - font-size: 18px; - transition: all 0.2s; - box-shadow: var(--shadow-sm, 0 2px 8px rgba(0, 0, 0, 0.1)); + font-size: 20px; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 4px 16px rgba(59, 130, 246, 0.3); z-index: 100; + opacity: 0; + transform: scale(0.8) translateY(10px); } .scroll-to-bottom.visible { display: flex; + opacity: 1; + transform: scale(1) translateY(0); } .scroll-to-bottom:hover { - background: var(--bg-hover, #f3f4f6); + background: var(--accent-hover, #2563eb); + transform: scale(1.1); + box-shadow: 0 6px 24px rgba(59, 130, 246, 0.4); +} + +.scroll-to-bottom:active { + transform: scale(0.95); } /* Responsive */ @@ -1246,3 +1328,207 @@ form.input-container { display: none; } } + +/* ===== Enhanced Chat Elements ===== */ + +/* Thinking Indicator */ +.thinking-indicator { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 12px 16px; + background: var(--surface, rgba(42, 42, 42, 0.8)); + border-radius: 16px; + animation: fadeIn 0.3s ease; +} + +.thinking-dots { + display: flex; + gap: 4px; +} + +.thinking-dot { + width: 8px; + height: 8px; + background: var(--accent, #3b82f6); + border-radius: 50%; + animation: thinkingBounce 1.4s infinite ease-in-out both; +} + +.thinking-dot:nth-child(1) { + animation-delay: -0.32s; +} + +.thinking-dot:nth-child(2) { + animation-delay: -0.16s; +} + +@keyframes thinkingBounce { + 0%, 80%, 100% { + transform: scale(0.8); + opacity: 0.5; + } + 40% { + transform: scale(1); + opacity: 1; + } +} + +/* Connection Status */ +.connection-status { + position: fixed; + top: 20px; + left: 50%; + transform: translateX(-50%); + display: flex; + align-items: center; + gap: 8px; + padding: 8px 16px; + background: var(--surface, rgba(26, 26, 36, 0.95)); + backdrop-filter: blur(10px); + border: 1px solid var(--border, rgba(42, 42, 42, 0.5)); + border-radius: 24px; + font-size: 13px; + color: var(--text, #ffffff); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); + z-index: 1000; + transition: all 0.3s ease; +} + +.connection-status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + animation: pulse 2s infinite; +} + +.connection-status.connected .connection-status-dot { + background: #22c55e; + box-shadow: 0 0 8px rgba(34, 197, 94, 0.6); +} + +.connection-status.disconnected .connection-status-dot { + background: #ef4444; + box-shadow: 0 0 8px rgba(239, 68, 68, 0.6); + animation: none; +} + +.connection-status.connecting .connection-status-dot { + background: #f59e0b; + box-shadow: 0 0 8px rgba(245, 158, 11, 0.6); +} + +/* Message Animations */ +@keyframes messageIn { + from { + opacity: 0; + transform: translateY(20px) scale(0.95); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +.message { + animation: messageIn 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +/* Bot Message Glow Effect */ +.message.bot .message-content { + position: relative; +} + +.message.bot .message-content::before { + content: ''; + position: absolute; + inset: -2px; + background: var(--accent-glow, rgba(59, 130, 246, 0.1)); + border-radius: 18px; + z-index: -1; + opacity: 0; + transition: opacity 0.3s; +} + +.message.bot:hover .message-content::before { + opacity: 1; +} + +/* Enhanced User Message */ +.message.user .message-content { + box-shadow: 0 2px 12px rgba(59, 130, 246, 0.2); +} + +.message.user:hover .message-content { + box-shadow: 0 4px 20px rgba(59, 130, 246, 0.3); + transform: translateY(-1px); + transition: all 0.2s; +} + +/* Smooth Scrolling */ +#messages { + scroll-behavior: smooth; +} + +/* New Message Indicator */ +.new-message-indicator { + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + padding: 6px 12px; + background: var(--accent, #3b82f6); + color: white; + border-radius: 12px; + font-size: 12px; + font-weight: 500; + white-space: nowrap; + animation: bounce 0.5s ease; + cursor: pointer; + box-shadow: 0 2px 12px rgba(59, 130, 246, 0.4); +} + +@keyframes bounce { + 0%, 100% { + transform: translateX(-50%) translateY(0); + } + 50% { + transform: translateX(-50%) translateY(-5px); + } +} + +/* Typing Indicator */ +.typing-indicator { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 8px 12px; + background: var(--surface, #2a2a2a); + border-radius: 12px; + margin-left: 12px; +} + +.typing-indicator span { + width: 6px; + height: 6px; + background: var(--text-secondary, #888); + border-radius: 50%; + animation: typing 1.4s infinite; +} + +.typing-indicator span:nth-child(2) { + animation-delay: 0.2s; +} + +.typing-indicator span:nth-child(3) { + animation-delay: 0.4s; +} + +@keyframes typing { + 0%, 60%, 100% { + transform: translateY(0); + } + 30% { + transform: translateY(-8px); + } +} diff --git a/ui/suite/chat/chat.html b/ui/suite/chat/chat.html index b23c9d1..14a152f 100644 --- a/ui/suite/chat/chat.html +++ b/ui/suite/chat/chat.html @@ -1,6 +1,18 @@
+ + + + +
+ +

Chat

+
+
@@ -42,7 +54,11 @@
- +
@@ -155,6 +171,7 @@ var currentStreamingContent = ""; var reconnectAttempts = 0; var maxReconnectAttempts = 5; + var isUserScrolling = false; var mentionState = { active: false, @@ -170,6 +187,61 @@ return div.innerHTML; } + // Scroll handling + function scrollToBottom(animate) { + var messages = document.getElementById("messages"); + if (messages) { + if (animate) { + messages.scrollTo({ + top: messages.scrollHeight, + behavior: "smooth", + }); + } else { + messages.scrollTop = messages.scrollHeight; + } + } + } + + function updateScrollButton() { + var messages = document.getElementById("messages"); + var scrollBtn = document.getElementById("scrollToBottom"); + if (!messages || !scrollBtn) return; + + var isNearBottom = + messages.scrollHeight - messages.scrollTop - messages.clientHeight < + 100; + + if (isNearBottom) { + scrollBtn.classList.remove("visible"); + } else { + scrollBtn.classList.add("visible"); + } + } + + // Scroll-to-bottom button click + var scrollBtn = document.getElementById("scrollToBottom"); + if (scrollBtn) { + scrollBtn.addEventListener("click", function () { + scrollToBottom(true); + isUserScrolling = false; + }); + } + + // Detect user scrolling + var messagesEl = document.getElementById("messages"); + if (messagesEl) { + messagesEl.addEventListener("scroll", function () { + isUserScrolling = true; + updateScrollButton(); + + // Reset isUserScrolling after 2 seconds of no scrolling + clearTimeout(messagesEl.scrollTimeout); + messagesEl.scrollTimeout = setTimeout(function () { + isUserScrolling = false; + }, 2000); + }); + } + function renderMentionInMessage(content) { return content.replace( /@(\w+):([^\s]+)/g, @@ -227,7 +299,13 @@ } messages.appendChild(div); - messages.scrollTop = messages.scrollHeight; + + // Auto-scroll to bottom unless user is manually scrolling + if (!isUserScrolling) { + scrollToBottom(true); + } else { + updateScrollButton(); + } setupMentionClickHandlers(div); } @@ -675,6 +753,11 @@ addMessage("bot", data.content); } isStreaming = false; + + // Render suggestions when message is complete + if (data.suggestions && Array.isArray(data.suggestions) && data.suggestions.length > 0) { + renderSuggestions(data.suggestions); + } } else { if (!isStreaming) { isStreaming = true; @@ -692,22 +775,83 @@ } } - function sendMessage() { + // Render suggestion buttons + function renderSuggestions(suggestions) { + var suggestionsEl = document.getElementById("suggestions"); + if (!suggestionsEl) { + console.warn("Suggestions container not found"); + return; + } + + // Clear existing suggestions + suggestionsEl.innerHTML = ""; + + console.log("Rendering " + suggestions.length + " suggestions"); + + suggestions.forEach(function (suggestion) { + var chip = document.createElement("button"); + chip.className = "suggestion-chip"; + chip.textContent = suggestion.text || "Suggestion"; + + // Use window.sendMessage which is already exposed + chip.onclick = (function(sugg) { + return function() { + console.log("Suggestion clicked:", sugg); + // Check if there's an action to parse + if (sugg.action) { + try { + var action = typeof sugg.action === "string" + ? JSON.parse(sugg.action) + : sugg.action; + + console.log("Parsed action:", action); + + if (action.type === "invoke_tool") { + // Send the tool name as text - the backend will handle tool invocation + window.sendMessage(action.tool); + } else if (action.type === "send_message") { + window.sendMessage(action.message || sugg.text); + } else if (action.type === "select_context") { + window.sendMessage(action.context); + } else { + window.sendMessage(sugg.text); + } + } catch (e) { + console.error("Failed to parse action:", e, "falling back to text"); + window.sendMessage(sugg.text); + } + } else { + // No action, just send the text + window.sendMessage(sugg.text); + } + }; + })(suggestion); + + suggestionsEl.appendChild(chip); + }); + } + + function sendMessage(messageContent) { var input = document.getElementById("messageInput"); if (!input) { console.error("Chat input not found"); return; } - var content = input.value.trim(); + // If no messageContent provided, read from input + var content = messageContent || input.value.trim(); if (!content) { return; } - hideMentionDropdown(); + // If called from input field (no messageContent provided), clear input + if (!messageContent) { + hideMentionDropdown(); + input.value = ""; + input.focus(); + } + addMessage("user", content); - input.value = ""; - input.focus(); if (ws && ws.readyState === WebSocket.OPEN) { ws.send( @@ -728,11 +872,24 @@ window.sendMessage = sendMessage; + // Expose session info for suggestion clicks + window.getChatSessionInfo = function() { + return { + ws: ws, + currentBotId: currentBotId, + currentUserId: currentUserId, + currentSessionId: currentSessionId, + currentBotName: currentBotName + }; + }; + function connectWebSocket() { if (ws) { ws.close(); } + updateConnectionStatus("connecting"); + var url = WS_URL + "?session_id=" + @@ -746,6 +903,7 @@ ws.onopen = function () { console.log("WebSocket connected"); reconnectAttempts = 0; + updateConnectionStatus("connected"); }; ws.onmessage = function (event) { @@ -756,13 +914,11 @@ // Ignore connection confirmation if (data.type === "connected") return; - // Ignore system events (theme changes, etc) + // Process system events (theme changes, etc) if (data.event) { - console.log( - "System event received, ignoring:", - data.event, - data, - ); + if (data.event === "change_theme") { + applyThemeData(data.data || {}); + } return; } @@ -771,10 +927,7 @@ try { var contentObj = JSON.parse(data.content); if (contentObj.event === "change_theme") { - console.log( - "Theme change event in content, ignoring:", - contentObj, - ); + applyThemeData(contentObj.data || {}); return; } } catch (e) { @@ -795,19 +948,108 @@ }; ws.onclose = function () { + updateConnectionStatus("disconnected"); notify("Disconnected from chat server", "error"); if (reconnectAttempts < maxReconnectAttempts) { reconnectAttempts++; + updateConnectionStatus("connecting"); setTimeout(connectWebSocket, 1000 * reconnectAttempts); } }; ws.onerror = function (e) { console.error("WebSocket error:", e); + updateConnectionStatus("disconnected"); }; } + // Apply theme data from WebSocket events + function applyThemeData(themeData) { + console.log("Applying theme data:", themeData); + + var color1 = themeData.color1 || themeData.data?.color1 || "#8B4513"; + var color2 = themeData.color2 || themeData.data?.color2 || "#F5DEB3"; + var logo = themeData.logo_url || themeData.data?.logo_url || ""; + var title = themeData.title || themeData.data?.title || window.__INITIAL_BOT_NAME__ || "Chat"; + + // Set CSS variables for colors + document.documentElement.style.setProperty("--chat-color1", color1); + document.documentElement.style.setProperty("--chat-color2", color2); + + // Update suggestion button colors to match theme + document.documentElement.style.setProperty("--suggestion-color", color1); + document.documentElement.style.setProperty("--suggestion-bg", color2); + + // Set logo + var logoEl = document.getElementById("botLogo"); + if (logoEl) { + if (logo) { + logoEl.src = logo; + logoEl.style.display = "block"; + } else { + logoEl.style.display = "none"; + } + } + + // Set title + var titleEl = document.getElementById("botTitle"); + if (titleEl) { + titleEl.textContent = title; + } + + console.log("Theme applied:", { color1: color1, color2: color2, logo: logo, title: title }); + } + + // Load bot config and apply colors/logo + function loadBotConfig() { + var botName = window.__INITIAL_BOT_NAME__ || "default"; + + fetch("/api/bot/config?bot_name=" + encodeURIComponent(botName)) + .then(function(response) { + return response.json(); + }) + .then(function(config) { + if (!config) return; + + // Apply colors from config + var color1 = config["theme-color1"] || config["Theme Color"] || "#8B4513"; + var color2 = config["theme-color2"] || "#F5DEB3"; + var logo = config["theme-logo"] || ""; + var title = config["theme-title"] || botName; + + // Set CSS variables for colors + document.documentElement.style.setProperty("--chat-color1", color1); + document.documentElement.style.setProperty("--chat-color2", color2); + + // Update suggestion button colors to match theme + document.documentElement.style.setProperty("--suggestion-color", color1); + document.documentElement.style.setProperty("--suggestion-bg", color2); + + // Set logo + var logoEl = document.getElementById("botLogo"); + if (logoEl && logo) { + logoEl.src = logo; + logoEl.style.display = "block"; + } else if (logoEl) { + logoEl.style.display = "none"; + } + + // Set title + var titleEl = document.getElementById("botTitle"); + if (titleEl) { + titleEl.textContent = title; + } + + console.log("Bot config loaded:", { color1: color1, color2: color2, logo: logo, title: title }); + }) + .catch(function(e) { + console.log("Could not load bot config:", e); + }); + } + function initChat() { + // Load bot config first + loadBotConfig(); // Just proceed with chat initialization - no auth check proceedWithChatInit(); } @@ -838,6 +1080,31 @@ }); } + function updateConnectionStatus(status) { + var statusEl = document.getElementById("connectionStatus"); + if (!statusEl) return; + + statusEl.className = "connection-status " + status; + + var statusText = statusEl.querySelector(".connection-text"); + if (statusText) { + switch (status) { + case "connected": + statusText.textContent = "Connected"; + statusEl.style.display = "none"; + break; + case "disconnected": + statusText.textContent = "Disconnected"; + statusEl.style.display = "flex"; + break; + case "connecting": + statusText.textContent = "Connecting..."; + statusEl.style.display = "flex"; + break; + } + } + } + function setupEventHandlers() { var form = document.getElementById("chatForm"); var input = document.getElementById("messageInput"); diff --git a/ui/suite/css/config-colors.css b/ui/suite/css/config-colors.css new file mode 100644 index 0000000..f77bfff --- /dev/null +++ b/ui/suite/css/config-colors.css @@ -0,0 +1,16 @@ +/* Config Color Overrides */ +/* Maps theme-color1 and theme-color2 from config.csv to actual theme variables */ + +:root { + /* Use --color1 and --color2 from config.csv, with fallback defaults */ + --sentient-accent: var(--color1, #3b82f6); + --primary: var(--color1, #3b82f6); + --primary-hover: color-mix(in srgb, var(--color1, #3b82f6) 85%, black); + --primary-light: color-mix(in srgb, var(--color1, #3b82f6) 10%, transparent); + --chart-1: var(--color1, #3b82f6); + --chart-2: var(--color2, #f59e0b); + --ring: var(--color1, #3b82f6); + + /* Background can use color2 for subtle tint */ + /* --sentient-bg-primary stays white/light for text readability */ +} diff --git a/ui/suite/js/htmx-app.js b/ui/suite/js/htmx-app.js index dade554..d58fe9f 100644 --- a/ui/suite/js/htmx-app.js +++ b/ui/suite/js/htmx-app.js @@ -204,10 +204,18 @@ // Handle WebSocket messages function handleWebSocketMessage(message) { const messageType = message.type || message.event; - + // Debug logging console.log("handleWebSocketMessage called with:", { messageType, message }); + // Handle suggestions array from BotResponse + if (message.suggestions && Array.isArray(message.suggestions) && message.suggestions.length > 0) { + clearSuggestions(); + message.suggestions.forEach(suggestion => { + addSuggestionButton(suggestion.text, suggestion.value || suggestion.text); + }); + } + switch (messageType) { case "message": appendMessage(message); @@ -246,6 +254,31 @@ } } + // Clear all suggestions + function clearSuggestions() { + const suggestionsEl = document.getElementById("suggestions"); + if (suggestionsEl) { + suggestionsEl.innerHTML = ''; + } + } + + // Add suggestion button with value + function addSuggestionButton(text, value) { + const suggestionsEl = document.getElementById("suggestions"); + if (!suggestionsEl) return; + + const chip = document.createElement("button"); + chip.className = "suggestion-chip"; + chip.textContent = text; + chip.setAttribute("hx-post", "/api/sessions/current/message"); + chip.setAttribute("hx-vals", JSON.stringify({ content: value })); + chip.setAttribute("hx-target", "#messages"); + chip.setAttribute("hx-swap", "beforeend"); + + suggestionsEl.appendChild(chip); + htmx.process(chip); + } + // Append message to chat function appendMessage(message) { const messagesEl = document.getElementById("messages"); diff --git a/ui/suite/js/theme-manager.js b/ui/suite/js/theme-manager.js index de46828..e2b7ca7 100644 --- a/ui/suite/js/theme-manager.js +++ b/ui/suite/js/theme-manager.js @@ -3,6 +3,27 @@ const ThemeManager = (() => { let currentThemeId = "default"; let subscribers = []; + // Bot ID to theme mapping (configured via config.csv theme-base field) + const botThemeMap = { + // Default bot uses light theme with brown accents + "default": "light", + // Cristo bot uses mellowgold theme with earth tones + "cristo": "mellowgold", + // Salesianos bot uses light theme with blue accents + "salesianos": "light", + }; + + // Detect current bot from URL path + function getCurrentBotId() { + const path = window.location.pathname; + // Match patterns like /bot/cristo, /cristo, etc. + const match = path.match(/(?:\/bot\/)?([a-z0-9-]+)/i); + if (match && match[1]) { + return match[1].toLowerCase(); + } + return "default"; + } + const themes = [ { id: "default", name: "🎨 Default", file: "light.css" }, { id: "light", name: "☀️ Light", file: "light.css" }, @@ -54,7 +75,7 @@ const ThemeManager = (() => { const link = document.createElement("link"); link.id = "theme-css"; link.rel = "stylesheet"; - link.href = `public/themes/${theme.file}`; + link.href = `/public/themes/${theme.file}`; link.onload = () => { console.log("✓ Theme loaded:", theme.name); currentThemeId = id; @@ -87,7 +108,19 @@ const ThemeManager = (() => { } function init() { - let saved = localStorage.getItem("gb-theme") || "default"; + // First, load saved bot theme from config.csv (if available) + loadSavedTheme(); + + // Then load the UI theme (CSS theme) + // Priority: 1) localStorage user preference, 2) bot-specific theme, 3) default + let saved = localStorage.getItem("gb-theme"); + if (!saved || !themes.find((t) => t.id === saved)) { + // No user preference, try bot-specific theme + const botId = getCurrentBotId(); + saved = botThemeMap[botId] || "light"; + // Save to localStorage so it persists + localStorage.setItem("gb-theme", saved); + } if (!themes.find((t) => t.id === saved)) saved = "default"; currentThemeId = saved; loadTheme(saved); @@ -99,21 +132,56 @@ const ThemeManager = (() => { } function setThemeFromServer(data) { + // Save theme to localStorage for persistence across page loads + localStorage.setItem("gb-theme-data", JSON.stringify(data)); + + // Load base theme if specified + if (data.theme_base) { + loadTheme(data.theme_base); + } + if (data.logo_url) { document .querySelectorAll(".logo-icon, .assistant-avatar") .forEach((el) => { el.style.backgroundImage = `url("${data.logo_url}")`; + el.style.backgroundSize = "contain"; + el.style.backgroundRepeat = "no-repeat"; + el.style.backgroundPosition = "center"; + // Clear emoji text content when logo image is applied + if (el.classList.contains("logo-icon")) { + el.textContent = ""; + } }); } + if (data.color1) { + document.documentElement.style.setProperty("--color1", data.color1); + } + if (data.color2) { + document.documentElement.style.setProperty("--color2", data.color2); + } if (data.title) document.title = data.title; if (data.logo_text) { - document.querySelectorAll(".logo-text").forEach((el) => { + document.querySelectorAll(".logo span, .logo-text").forEach((el) => { el.textContent = data.logo_text; }); } } + // Load saved theme from localStorage on page load + function loadSavedTheme() { + const savedTheme = localStorage.getItem("gb-theme-data"); + if (savedTheme) { + try { + const data = JSON.parse(savedTheme); + setThemeFromServer(data); + console.log("✓ Theme loaded from localStorage"); + } catch (e) { + console.warn("Failed to load saved theme:", e); + } + } + } + function applyCustomizations() { // Called by modules if needed } @@ -126,6 +194,7 @@ const ThemeManager = (() => { init, loadTheme, setThemeFromServer, + loadSavedTheme, applyCustomizations, subscribe, getAvailableThemes: () => themes,