botui/ui/suite/partials/chat.html
Rodrigo Rodriguez (Pragmatismo) a4e641a1d5
All checks were successful
BotUI CI / build (push) Successful in 1m59s
refactor: remove quick action chips (Full-Stack, Writing, Data Insight, Magic Design)
- Removed quick-actions-container from chat footer
- Keeps suggestions-container for dynamic two-row suggestions display
- Cleaner chat interface without fixed action chips
2026-03-03 09:46:17 -03:00

1536 lines
54 KiB
HTML

<link rel="stylesheet" href="/suite/chat/chat.css?v=3" />
<link rel="stylesheet" href="/suite/css/markdown-message.css" />
<link rel="stylesheet" href="/suite/css/chat-agent-mode.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>
<!-- Agent Mode: Left Sidebar -->
<aside class="agent-sidebar" id="agentSidebar">
<button
class="agent-sidebar-item active"
data-panel="chat"
title="Chat"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"
/>
</svg>
</button>
<button class="agent-sidebar-item" data-panel="tasks" title="Tasks">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M9 11l3 3L22 4" />
<path
d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"
/>
</svg>
</button>
<button
class="agent-sidebar-item"
data-panel="terminal"
title="Terminal"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline points="4 17 10 11 4 5" />
<line x1="12" y1="19" x2="20" y2="19" />
</svg>
<span
class="agent-sidebar-badge"
id="terminalBadge"
style="display: none"
>0</span
>
</button>
<button
class="agent-sidebar-item"
data-panel="explorer"
title="Explorer"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"
/>
</svg>
<span
class="agent-sidebar-badge"
id="explorerBadge"
style="display: none"
>0</span
>
</button>
<button class="agent-sidebar-item" data-panel="editor" title="Editor">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline points="16 18 22 12 16 6" />
<polyline points="8 6 2 12 8 18" />
</svg>
</button>
<button class="agent-sidebar-item" data-panel="browser" title="Browser">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="10" />
<line x1="2" y1="12" x2="22" y2="12" />
<path
d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"
/>
</svg>
</button>
</aside>
<main id="messages"></main>
<!-- Agent Mode: Browser Panel -->
<div class="agent-browser-panel" id="agentBrowserPanel">
<div class="browser-panel-header">
<span>// BROWSER</span>
<input
type="text"
class="browser-url-bar"
id="browserUrlBar"
value=""
readonly
placeholder="No preview active"
/>
</div>
<div class="browser-panel-content" id="browserPanelContent">
<div class="browser-panel-empty">Waiting for app preview...</div>
</div>
</div>
<!-- Agent Mode: Terminal Panel -->
<div class="agent-terminal-panel" id="agentTerminalPanel">
<div class="terminal-panel-header">
<span>// TERMINAL</span>
</div>
<div class="terminal-panel-content" id="terminalPanelContent"></div>
</div>
<!-- Agent Mode: Agent Info Card -->
<div class="agent-info-card" id="agentInfoCard">
<div class="agent-info-name">
<span class="agent-info-dot"></span>
<span id="agentNameDisplay">Agent #1</span>
</div>
<span class="agent-level-badge badge-evolved" id="agentLevelBadge"
>EVOLVED</span
>
<div class="agent-info-model" id="agentModelDisplay">
Claude Opus 4.5 &mdash; 99%
</div>
<div class="agent-info-toggles">
<div class="agent-toggle">
<span>Plan</span>
<button
class="agent-toggle-switch on"
id="togglePlan"
type="button"
></button>
</div>
<div class="agent-toggle">
<span>YOLO</span>
<button
class="agent-toggle-switch"
id="toggleYolo"
type="button"
></button>
</div>
</div>
</div>
<!-- Agent Mode: Step Counter Bar -->
<div class="agent-step-bar" id="agentStepBar">
<div class="step-counter">
<button class="step-nav-btn" id="stepPrev" type="button"></button>
<span id="stepCounterText">0 / 0</span>
<button class="step-nav-btn" id="stepNext" type="button"></button>
</div>
<div class="step-action-btns">
<button class="step-action-btn" title="Chat" type="button">
💬
</button>
<button class="step-action-btn" title="Edit" type="button">
✏️
</button>
<button class="step-action-btn" title="Code" type="button">
&lt;/&gt;
</button>
</div>
</div>
<footer>
<div class="suggestions-container" id="suggestions"></div>
<div class="mention-dropdown" id="mentionDropdown">
<div class="mention-header">
<span class="mention-title" data-i18n="chat-mention-title"
>Reference Entity</span
>
</div>
<div class="mention-results" id="mentionResults"></div>
</div>
<form class="input-container" id="chatForm">
<!-- Agent/Chat Mode Toggle (Z.ai style) -->
<div class="chat-mode-toggle" id="chatModeToggle">
<button
type="button"
class="chat-mode-btn active"
data-mode="agent"
id="modeAgentBtn"
>
Agent
</button>
<button
type="button"
class="chat-mode-btn"
data-mode="chat"
id="modeChatBtn"
>
Chat
</button>
</div>
<input
name="content"
id="messageInput"
type="text"
placeholder="Message... (type @ to mention)"
data-i18n-placeholder="chat-placeholder"
autofocus
autocomplete="off"
/>
<button
type="submit"
id="sendBtn"
title="Send"
data-i18n-title="chat-send"
>
</button>
</form>
</footer>
<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">
<div class="entity-card-header">
<span class="entity-card-type"></span>
<span class="entity-card-status"></span>
</div>
<div class="entity-card-title"></div>
<div class="entity-card-details"></div>
<div class="entity-card-actions">
<button
class="entity-card-btn"
data-action="view"
data-i18n="action-view"
>
View
</button>
</div>
</div>
<script>
(function () {
"use strict";
function notify(message, type) {
type = type || "info";
if (window.GBAlerts) {
if (type === "success") {
window.GBAlerts.info("Chat", message);
} else if (type === "error") {
window.GBAlerts.warning("Chat", message);
} else {
window.GBAlerts.info("Chat", message);
}
}
}
var WS_BASE_URL =
window.location.protocol === "https:" ? "wss://" : "ws://";
var WS_URL = WS_BASE_URL + window.location.host + "/ws";
var MessageType = {
EXTERNAL: 0,
USER: 1,
BOT_RESPONSE: 2,
CONTINUE: 3,
SUGGESTION: 4,
CONTEXT_CHANGE: 5,
};
var EntityTypes = {
lead: { icon: "👤", color: "#4CAF50", label: "Lead", route: "crm" },
opportunity: {
icon: "💰",
color: "#FF9800",
label: "Opportunity",
route: "crm",
},
account: {
icon: "🏢",
color: "#2196F3",
label: "Account",
route: "crm",
},
contact: {
icon: "📇",
color: "#9C27B0",
label: "Contact",
route: "crm",
},
invoice: {
icon: "📄",
color: "#F44336",
label: "Invoice",
route: "billing",
},
quote: {
icon: "📋",
color: "#607D8B",
label: "Quote",
route: "billing",
},
case: {
icon: "🎫",
color: "#E91E63",
label: "Case",
route: "tickets",
},
product: {
icon: "📦",
color: "#795548",
label: "Product",
route: "products",
},
service: {
icon: "⚙️",
color: "#00BCD4",
label: "Service",
route: "products",
},
};
var ws = null;
var currentSessionId = null;
var currentUserId = null;
var currentBotId = "default";
var currentBotName = "default";
var isStreaming = false;
var streamingMessageId = null;
var currentStreamingContent = "";
var reconnectAttempts = 0;
var maxReconnectAttempts = 5;
var disconnectNotified = false;
var isUserScrolling = false;
var mentionState = {
active: false,
query: "",
startPos: -1,
selectedIndex: 0,
results: [],
};
function escapeHtml(text) {
var div = document.createElement("div");
div.textContent = text;
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,
function (match, type, name) {
var entityType = EntityTypes[type.toLowerCase()];
if (entityType) {
return (
'<span class="mention-tag" data-type="' +
type +
'" data-name="' +
escapeHtml(name) +
'">' +
'<span class="mention-icon">' +
entityType.icon +
"</span>" +
'<span class="mention-text">@' +
type +
":" +
escapeHtml(name) +
"</span>" +
"</span>"
);
}
return match;
},
);
}
function addMessage(sender, content, msgId) {
var messages = document.getElementById("messages");
if (!messages) return;
var div = document.createElement("div");
div.className = "message " + sender;
if (msgId) div.id = msgId;
if (sender === "user") {
var processedContent = renderMentionInMessage(
escapeHtml(content),
);
div.innerHTML =
'<div class="message-content user-message">' +
processedContent +
"</div>";
} else {
var parsed =
typeof marked !== "undefined" && marked.parse
? marked.parse(content)
: escapeHtml(content);
parsed = renderMentionInMessage(parsed);
div.innerHTML =
'<div class="message-content bot-message">' +
parsed +
"</div>";
}
messages.appendChild(div);
// Auto-scroll to bottom unless user is manually scrolling
if (!isUserScrolling) {
scrollToBottom(true);
} else {
updateScrollButton();
}
setupMentionClickHandlers(div);
}
function setupMentionClickHandlers(container) {
var mentions = container.querySelectorAll(".mention-tag");
mentions.forEach(function (mention) {
mention.addEventListener("click", function (e) {
e.preventDefault();
var type = this.getAttribute("data-type");
var name = this.getAttribute("data-name");
navigateToEntity(type, name);
});
mention.addEventListener("mouseenter", function (e) {
var type = this.getAttribute("data-type");
var name = this.getAttribute("data-name");
showEntityCard(type, name, e.target);
});
mention.addEventListener("mouseleave", function () {
hideEntityCard();
});
});
}
function navigateToEntity(type, name) {
var entityType = EntityTypes[type.toLowerCase()];
if (entityType) {
var route = entityType.route;
window.location.hash = "#" + route;
var htmxLink = document.querySelector(
'a[data-section="' + route + '"]',
);
if (htmxLink) {
htmx.trigger(htmxLink, "click");
}
}
}
function showEntityCard(type, name, targetEl) {
var card = document.getElementById("entityCardTooltip");
var entityType = EntityTypes[type.toLowerCase()];
if (!card || !entityType) return;
card.querySelector(".entity-card-type").textContent =
entityType.label;
card.querySelector(".entity-card-type").style.background =
entityType.color;
card.querySelector(".entity-card-title").textContent =
entityType.icon + " " + name;
card.querySelector(".entity-card-status").textContent = "";
card.querySelector(".entity-card-details").textContent =
"Loading...";
var rect = targetEl.getBoundingClientRect();
card.style.left = rect.left + "px";
card.style.top = rect.top - card.offsetHeight - 8 + "px";
card.classList.add("visible");
fetchEntityDetails(type, name).then(function (details) {
if (card.classList.contains("visible")) {
card.querySelector(".entity-card-details").innerHTML =
details;
}
});
}
function hideEntityCard() {
var card = document.getElementById("entityCardTooltip");
if (card) {
card.classList.remove("visible");
}
}
function fetchEntityDetails(type, name) {
return fetch(
"/api/search/entity?type=" +
encodeURIComponent(type) +
"&name=" +
encodeURIComponent(name),
)
.then(function (r) {
return r.json();
})
.then(function (data) {
if (data && data.details) {
return data.details;
}
return "No additional details available";
})
.catch(function () {
return "Unable to load details";
});
}
function showMentionDropdown() {
var dropdown = document.getElementById("mentionDropdown");
if (dropdown) {
dropdown.classList.add("visible");
}
}
function hideMentionDropdown() {
var dropdown = document.getElementById("mentionDropdown");
if (dropdown) {
dropdown.classList.remove("visible");
}
mentionState.active = false;
mentionState.query = "";
mentionState.startPos = -1;
mentionState.selectedIndex = 0;
mentionState.results = [];
}
function searchEntities(query) {
if (!query || query.length < 1) {
var defaultResults = Object.keys(EntityTypes).map(
function (type) {
return {
type: type,
name: EntityTypes[type].label,
icon: EntityTypes[type].icon,
isTypeHint: true,
};
},
);
renderMentionResults(defaultResults);
return;
}
var colonIndex = query.indexOf(":");
if (colonIndex > 0) {
var entityType = query.substring(0, colonIndex).toLowerCase();
var searchTerm = query.substring(colonIndex + 1);
if (EntityTypes[entityType]) {
fetchEntitiesOfType(entityType, searchTerm);
return;
}
}
var filteredTypes = Object.keys(EntityTypes)
.filter(function (type) {
return (
type.toLowerCase().indexOf(query.toLowerCase()) === 0 ||
EntityTypes[type].label
.toLowerCase()
.indexOf(query.toLowerCase()) === 0
);
})
.map(function (type) {
return {
type: type,
name: EntityTypes[type].label,
icon: EntityTypes[type].icon,
isTypeHint: true,
};
});
renderMentionResults(filteredTypes);
}
function fetchEntitiesOfType(type, searchTerm) {
fetch(
"/api/search/entities?type=" +
encodeURIComponent(type) +
"&q=" +
encodeURIComponent(searchTerm || ""),
)
.then(function (r) {
return r.json();
})
.then(function (data) {
var results = (data.results || []).map(function (item) {
return {
type: type,
name: item.name || item.title || item.number,
id: item.id,
icon: EntityTypes[type].icon,
subtitle: item.subtitle || item.status || "",
isTypeHint: false,
};
});
if (results.length === 0) {
results = [
{
type: type,
name: "No results for '" + searchTerm + "'",
icon: "❌",
isTypeHint: false,
disabled: true,
},
];
}
renderMentionResults(results);
})
.catch(function () {
renderMentionResults([
{
type: type,
name: "Search unavailable",
icon: "⚠️",
isTypeHint: false,
disabled: true,
},
]);
});
}
function renderMentionResults(results) {
var container = document.getElementById("mentionResults");
if (!container) return;
mentionState.results = results;
mentionState.selectedIndex = 0;
container.innerHTML = results
.map(function (item, index) {
var classes = "mention-item";
if (index === mentionState.selectedIndex)
classes += " selected";
if (item.disabled) classes += " disabled";
var subtitle = item.subtitle
? '<span class="mention-item-subtitle">' +
escapeHtml(item.subtitle) +
"</span>"
: "";
var hint = item.isTypeHint
? '<span class="mention-item-hint">Type : to search</span>'
: "";
return (
'<div class="' +
classes +
'" data-index="' +
index +
'" data-type="' +
item.type +
'" data-name="' +
escapeHtml(item.name) +
'" data-is-type="' +
item.isTypeHint +
'">' +
'<span class="mention-item-icon">' +
item.icon +
"</span>" +
'<span class="mention-item-content">' +
'<span class="mention-item-name">' +
escapeHtml(item.name) +
"</span>" +
subtitle +
hint +
"</span>" +
"</div>"
);
})
.join("");
container
.querySelectorAll(".mention-item:not(.disabled)")
.forEach(function (item) {
item.addEventListener("click", function () {
selectMentionItem(
parseInt(this.getAttribute("data-index")),
);
});
});
}
function selectMentionItem(index) {
var item = mentionState.results[index];
if (!item || item.disabled) return;
var input = document.getElementById("messageInput");
if (!input) return;
var value = input.value;
var beforeMention = value.substring(0, mentionState.startPos);
var afterMention = value.substring(input.selectionStart);
var insertText;
if (item.isTypeHint) {
insertText = "@" + item.type + ":";
mentionState.query = item.type + ":";
mentionState.startPos = beforeMention.length;
input.value = beforeMention + insertText + afterMention;
input.setSelectionRange(
beforeMention.length + insertText.length,
beforeMention.length + insertText.length,
);
searchEntities(mentionState.query);
return;
} else {
insertText = "@" + item.type + ":" + item.name + " ";
input.value = beforeMention + insertText + afterMention;
input.setSelectionRange(
beforeMention.length + insertText.length,
beforeMention.length + insertText.length,
);
hideMentionDropdown();
}
input.focus();
}
function updateMentionSelection(direction) {
var enabledResults = mentionState.results.filter(function (r) {
return !r.disabled;
});
if (enabledResults.length === 0) return;
var currentEnabled = 0;
for (var i = 0; i < mentionState.selectedIndex; i++) {
if (!mentionState.results[i].disabled) currentEnabled++;
}
currentEnabled += direction;
if (currentEnabled < 0) currentEnabled = enabledResults.length - 1;
if (currentEnabled >= enabledResults.length) currentEnabled = 0;
var newIndex = 0;
var count = 0;
for (var j = 0; j < mentionState.results.length; j++) {
if (!mentionState.results[j].disabled) {
if (count === currentEnabled) {
newIndex = j;
break;
}
count++;
}
}
mentionState.selectedIndex = newIndex;
var items = document.querySelectorAll(
"#mentionResults .mention-item",
);
items.forEach(function (item, idx) {
item.classList.toggle("selected", idx === newIndex);
});
var selectedItem = document.querySelector(
"#mentionResults .mention-item.selected",
);
if (selectedItem) {
selectedItem.scrollIntoView({ block: "nearest" });
}
}
function handleMentionInput(e) {
var input = e.target;
var value = input.value;
var cursorPos = input.selectionStart;
var textBeforeCursor = value.substring(0, cursorPos);
var atIndex = textBeforeCursor.lastIndexOf("@");
if (atIndex >= 0) {
var charBeforeAt =
atIndex > 0 ? textBeforeCursor[atIndex - 1] : " ";
if (charBeforeAt === " " || atIndex === 0) {
var query = textBeforeCursor.substring(atIndex + 1);
if (!query.includes(" ")) {
mentionState.active = true;
mentionState.startPos = atIndex;
mentionState.query = query;
showMentionDropdown();
searchEntities(query);
return;
}
}
}
if (mentionState.active) {
hideMentionDropdown();
}
}
function handleMentionKeydown(e) {
if (!mentionState.active) return false;
if (e.key === "ArrowDown") {
e.preventDefault();
updateMentionSelection(1);
return true;
}
if (e.key === "ArrowUp") {
e.preventDefault();
updateMentionSelection(-1);
return true;
}
if (e.key === "Enter" || e.key === "Tab") {
e.preventDefault();
selectMentionItem(mentionState.selectedIndex);
return true;
}
if (e.key === "Escape") {
e.preventDefault();
hideMentionDropdown();
return true;
}
return false;
}
function updateStreaming(content) {
var el = document.getElementById(streamingMessageId);
if (el) {
var parsed =
typeof marked !== "undefined" && marked.parse
? marked.parse(content)
: escapeHtml(content);
parsed = renderMentionInMessage(parsed);
el.querySelector(".message-content").innerHTML = parsed;
}
}
function finalizeStreaming() {
var el = document.getElementById(streamingMessageId);
if (el) {
var parsed =
typeof marked !== "undefined" && marked.parse
? marked.parse(currentStreamingContent)
: escapeHtml(currentStreamingContent);
parsed = renderMentionInMessage(parsed);
el.querySelector(".message-content").innerHTML = parsed;
el.removeAttribute("id");
setupMentionClickHandlers(el);
}
streamingMessageId = null;
currentStreamingContent = "";
}
function processMessage(data) {
if (data.is_complete) {
if (isStreaming) {
finalizeStreaming();
} else {
if (data.content && data.content.trim() !== "") {
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;
streamingMessageId = "streaming-" + Date.now();
currentStreamingContent = data.content || "";
addMessage(
"bot",
currentStreamingContent,
streamingMessageId,
);
} else {
currentStreamingContent += data.content || "";
updateStreaming(currentStreamingContent);
}
}
}
// 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 display text so it shows correctly in chat
// The backend will recognize this as a tool request
window.sendMessage(sugg.text);
} 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;
}
// If no messageContent provided, read from input
var content = messageContent || input.value.trim();
if (!content) {
return;
}
// If called from input field (no messageContent provided), clear input
if (!messageContent) {
hideMentionDropdown();
input.value = "";
input.focus();
}
addMessage("user", content);
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(
JSON.stringify({
bot_id: currentBotId,
user_id: currentUserId,
session_id: currentSessionId,
channel: "web",
content: content,
message_type: MessageType.USER,
timestamp: new Date().toISOString(),
}),
);
} else {
notify("Not connected to server. Message not sent.", "warning");
}
}
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=" +
currentSessionId +
"&user_id=" +
currentUserId +
"&bot_name=" +
currentBotName;
ws = new WebSocket(url);
ws.onopen = function () {
console.log("WebSocket connected");
disconnectNotified = false;
updateConnectionStatus("connected");
};
ws.onmessage = function (event) {
try {
var data = JSON.parse(event.data);
console.log("Chat WebSocket received:", data);
// Ignore connection confirmation
if (data.type === "connected") {
reconnectAttempts = 0;
return;
}
// Process system events (theme changes, etc)
if (data.event) {
if (data.event === "change_theme") {
applyThemeData(data.data || {});
}
return;
}
// Check if content contains theme change events (JSON strings)
if (data.content && typeof data.content === "string") {
try {
var contentObj = JSON.parse(data.content);
if (contentObj.event === "change_theme") {
applyThemeData(contentObj.data || {});
return;
}
} catch (e) {
// Content is not JSON, continue processing
}
}
// Route agent-type messages to AgentMode handler
if (
window.AgentMode &&
data.type &&
[
"thought_process",
"terminal_output",
"browser_ready",
"step_progress",
"step_complete",
"todo_update",
"agent_status",
"file_created",
].indexOf(data.type) !== -1
) {
window.AgentMode.handleMessage(data);
}
// Only process bot responses
if (data.message_type === MessageType.BOT_RESPONSE) {
console.log("Processing bot response:", data);
processMessage(data);
} else {
console.log("Ignoring non-bot message:", data);
}
} catch (e) {
console.error("WS message error:", e);
}
};
ws.onclose = function () {
updateConnectionStatus("disconnected");
if (!disconnectNotified) {
notify("Disconnected from chat server", "error");
disconnectNotified = true;
}
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 getContrastYIQ(hexcolor) {
if (!hexcolor) return "#ffffff";
// Handle named colors and variables by letting the browser resolve them
var temp = document.createElement("div");
temp.style.color = hexcolor;
temp.style.display = "none";
document.body.appendChild(temp);
var style = window.getComputedStyle(temp).color;
document.body.removeChild(temp);
var rgb = style.match(/\d+/g);
if (!rgb || rgb.length < 3) return "#ffffff";
var r = parseInt(rgb[0]);
var g = parseInt(rgb[1]);
var b = parseInt(rgb[2]);
var yiq = (r * 299 + g * 587 + b * 114) / 1000;
return yiq >= 128 ? "#000000" : "#ffffff";
}
function applyThemeData(themeData) {
console.log("Applying theme data:", themeData);
var color1 = themeData.color1 || themeData.data?.color1 || "black";
var color2 = themeData.color2 || themeData.data?.color2 || "white";
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 on document element
document.documentElement.style.setProperty("--chat-color1", color1);
document.documentElement.style.setProperty("--chat-color2", color2);
document.documentElement.style.setProperty(
"--suggestion-color",
color1,
);
document.documentElement.style.setProperty(
"--suggestion-bg",
color2,
);
// Also set on root for better cascading
document.documentElement.style.setProperty("--color1", color1);
document.documentElement.style.setProperty("--color2", color2);
// Update suggestion button colors to match theme
document.documentElement.style.setProperty("--primary", color1);
document.documentElement.style.setProperty("--accent", color1);
document.documentElement.style.setProperty(
"--chat-fg1",
getContrastYIQ(color1),
);
document.documentElement.style.setProperty(
"--chat-fg2",
getContrastYIQ(color2),
);
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;
// Get the theme manager's theme for this bot to check if user selected a different theme
var botId = botName.toLowerCase();
var botThemeKey = "gb-theme-" + botId;
var botTheme = window.ThemeManager
? // Get bot-specific theme from theme manager's mapping
(window.ThemeManager.getAvailableThemes &&
window.ThemeManager.getAvailableThemes().find(
(t) => t.id === botId,
)) ||
// Fallback to localStorage
localStorage.getItem(botThemeKey)
: localStorage.getItem(botThemeKey);
// Check if bot config has a theme-base setting
var configThemeBase =
config.theme_base || config["theme-base"] || "light";
// Only use bot config colors if:
// 1. No theme has been explicitly selected by user (localStorage empty or default)
// 2. AND the bot config's theme-base matches the current theme
var localStorageTheme = localStorage.getItem(botThemeKey);
var useBotConfigColors =
!localStorageTheme ||
localStorageTheme === "default" ||
localStorageTheme === configThemeBase;
// Apply colors from config (API returns snake_case)
var color1 =
config.theme_color1 ||
config["theme-color1"] ||
config["Theme Color"] ||
"#3b82f6";
var color2 =
config.theme_color2 ||
config["theme-color2"] ||
"#f5deb3";
var title =
config.theme_title || config["theme-title"] || botName;
var logo = config.theme_logo || config["theme-logo"] || "";
// Only set bot config colors if user hasn't selected a different theme
if (useBotConfigColors) {
document.documentElement.setAttribute(
"data-has-bot-colors",
"true",
);
document.documentElement.style.setProperty(
"--chat-color1",
color1,
);
document.documentElement.style.setProperty(
"--chat-color2",
color2,
);
document.documentElement.style.setProperty(
"--suggestion-color",
color1,
);
document.documentElement.style.setProperty(
"--suggestion-bg",
color2,
);
document.documentElement.style.setProperty(
"--color1",
color1,
);
document.documentElement.style.setProperty(
"--color2",
color2,
);
document.documentElement.style.setProperty(
"--primary",
color1,
);
document.documentElement.style.setProperty(
"--accent",
color1,
);
document.documentElement.style.setProperty(
"--chat-fg1",
getContrastYIQ(color1),
);
document.documentElement.style.setProperty(
"--chat-fg2",
getContrastYIQ(color2),
);
console.log("Bot config colors applied:", {
color1: color1,
color2: color2,
});
} else {
console.log(
"Bot config colors skipped - user selected custom theme:",
localStorageTheme,
);
}
// Update logo if provided
if (logo) {
var logoImg = document.querySelector(".logo-icon-img");
if (logoImg) {
logoImg.src = logo;
logoImg.alt = title || botName;
logoImg.style.display = "block";
}
// Hide the SVG logo when image logo is used
var logoSvg = document.querySelector(".logo-icon-svg");
if (logoSvg) {
logoSvg.style.display = "none";
}
}
console.log("Bot config loaded:", {
color1: color1,
color2: color2,
title: title,
logo: logo,
});
})
.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();
}
function proceedWithChatInit() {
var botName = window.__INITIAL_BOT_NAME__ || "default";
fetch("/api/auth?bot_name=" + encodeURIComponent(botName))
.then(function (response) {
return response.json();
})
.then(function (auth) {
currentUserId = auth.user_id;
currentSessionId = auth.session_id;
currentBotId = auth.bot_id || "default";
currentBotName = botName;
console.log("Auth:", {
currentUserId: currentUserId,
currentSessionId: currentSessionId,
currentBotId: currentBotId,
currentBotName: currentBotName,
});
connectWebSocket();
})
.catch(function (e) {
console.error("Auth failed:", e);
notify("Failed to connect to chat server", "error");
setTimeout(proceedWithChatInit, 3000);
});
}
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");
var sendBtn = document.getElementById("sendBtn");
if (form) {
form.onsubmit = function (e) {
e.preventDefault();
sendMessage();
return false;
};
}
if (input) {
input.addEventListener("input", handleMentionInput);
input.onkeydown = function (e) {
if (handleMentionKeydown(e)) {
return;
}
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
};
}
if (sendBtn) {
sendBtn.onclick = function (e) {
e.preventDefault();
sendMessage();
};
}
document.addEventListener("click", function (e) {
if (
!e.target.closest("#mentionDropdown") &&
!e.target.closest("#messageInput")
) {
hideMentionDropdown();
}
});
}
setupEventHandlers();
initChat();
console.log("Chat module initialized with @ mentions support");
})();
</script>
<script src="/suite/js/chat-agent-mode.js"></script>