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 @@
+
+
+
+ Connecting...
+
+
+
+
+
-
+
@@ -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,