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
This commit is contained in:
parent
123787378f
commit
a8bc5530b0
6 changed files with 718 additions and 45 deletions
|
|
@ -21,6 +21,8 @@
|
|||
<link rel="stylesheet" href="/css/app.css" />
|
||||
<link rel="stylesheet" href="/themes/sentient/sentient.css" />
|
||||
<link rel="stylesheet" href="/css/ai-panel.css" />
|
||||
<!-- Config color overrides - must load AFTER theme CSS -->
|
||||
<link rel="stylesheet" href="/css/config-colors.css" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- Skip navigation link for accessibility -->
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,18 @@
|
|||
<link rel="stylesheet" href="/suite/chat/chat.css" />
|
||||
|
||||
<div class="chat-layout" id="chat-app">
|
||||
<!-- Connection Status -->
|
||||
<div class="connection-status connecting" id="connectionStatus" style="display: none;">
|
||||
<span class="connection-status-dot"></span>
|
||||
<span class="connection-text">Connecting...</span>
|
||||
</div>
|
||||
|
||||
<!-- Bot Header with Logo -->
|
||||
<div class="chat-header" id="chatHeader">
|
||||
<img class="bot-logo" id="botLogo" src="" alt="Bot Logo" />
|
||||
<h1 class="bot-title" id="botTitle">Chat</h1>
|
||||
</div>
|
||||
|
||||
<main id="messages"></main>
|
||||
|
||||
<footer>
|
||||
|
|
@ -42,7 +54,11 @@
|
|||
</button>
|
||||
</form>
|
||||
</footer>
|
||||
<button class="scroll-to-bottom" id="scrollToBottom">↓</button>
|
||||
<button class="scroll-to-bottom" id="scrollToBottom" title="Scroll to bottom">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="6 9 12 15 18 9"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="entity-card-tooltip" id="entityCardTooltip">
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
16
ui/suite/css/config-colors.css
Normal file
16
ui/suite/css/config-colors.css
Normal file
|
|
@ -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 */
|
||||
}
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue