All checks were successful
BotUI CI / build (push) Successful in 1m59s
- Removed quick-actions-container from chat footer - Keeps suggestions-container for dynamic two-row suggestions display - Cleaner chat interface without fixed action chips
1536 lines
54 KiB
HTML
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 — 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">
|
|
</>
|
|
</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>
|