Add Phase 4 (@ Mentions) and Phase 5 (Business Reports)
Phase 4 - Chat @ Mentions: - Add mention dropdown with entity type search - Add mention tags rendering in messages - Add entity card tooltip on hover - Add navigation to entity on click - Support @lead, @opportunity, @account, @contact, @invoice, @case, @product, @service Phase 5 - Business Reports: - Create analytics/partials/business-reports.html - Add CRM reports: Sales Pipeline, Lead Conversion, Won/Lost, Forecast - Add Billing reports: Revenue Summary, Aging, Payment History, Monthly - Add Support reports: Cases by Priority, Resolution Time, Category, AI Rate - Add tabbed interface for report sections i18n: - Add chat mention translations (en, pt-BR, es)
This commit is contained in:
parent
c24ff23a07
commit
9a4c8bf6a6
3 changed files with 2091 additions and 31 deletions
1337
ui/suite/analytics/partials/business-reports.html
Normal file
1337
ui/suite/analytics/partials/business-reports.html
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -278,6 +278,211 @@ footer {
|
||||||
padding: 16px 0;
|
padding: 16px 0;
|
||||||
border-top: 1px solid var(--border, var(--border-color, #2a2a2a));
|
border-top: 1px solid var(--border, var(--border-color, #2a2a2a));
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mention Dropdown */
|
||||||
|
.mention-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
max-height: 300px;
|
||||||
|
background: var(--surface, var(--card, #1a1a24));
|
||||||
|
border: 1px solid var(--border, var(--border-color, #2a2a2a));
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.3);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention-dropdown.visible {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention-header {
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-bottom: 1px solid var(--border, var(--border-color, #2a2a2a));
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary, #888);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention-results {
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention-item:hover,
|
||||||
|
.mention-item.selected {
|
||||||
|
background: var(--hover, rgba(255, 255, 255, 0.05));
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention-item.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention-item-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--hover, rgba(255, 255, 255, 0.05));
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention-item-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention-item-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text, var(--text-primary, #fff));
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention-item-subtitle {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary, #888);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention-item-hint {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-tertiary, #666);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mention Tags in Messages */
|
||||||
|
.mention-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
background: var(--accent-glow, rgba(59, 130, 246, 0.15));
|
||||||
|
border: 1px solid var(--accent, var(--accent-color, #3b82f6));
|
||||||
|
border-radius: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
font-size: 13px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention-tag:hover {
|
||||||
|
background: var(--accent, var(--accent-color, #3b82f6));
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention-icon {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention-text {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Entity Card Tooltip */
|
||||||
|
.entity-card-tooltip {
|
||||||
|
position: fixed;
|
||||||
|
min-width: 250px;
|
||||||
|
max-width: 320px;
|
||||||
|
background: var(--surface, var(--card, #1a1a24));
|
||||||
|
border: 1px solid var(--border, var(--border-color, #2a2a2a));
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||||
|
padding: 12px;
|
||||||
|
z-index: 1000;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transform: translateY(8px);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entity-card-tooltip.visible {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
transform: translateY(0);
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entity-card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entity-card-type {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #fff;
|
||||||
|
background: var(--accent, #3b82f6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.entity-card-status {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-secondary, #888);
|
||||||
|
}
|
||||||
|
|
||||||
|
.entity-card-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text, var(--text-primary, #fff));
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entity-card-details {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-secondary, #aaa);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entity-card-actions {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid var(--border, var(--border-color, #2a2a2a));
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entity-card-btn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: var(--accent, var(--accent-color, #3b82f6));
|
||||||
|
color: #fff;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entity-card-btn:hover {
|
||||||
|
filter: brightness(1.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Suggestions */
|
/* Suggestions */
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,23 @@
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<div class="suggestions-container" id="suggestions"></div>
|
<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">
|
<form class="input-container" id="chatForm">
|
||||||
<input
|
<input
|
||||||
name="content"
|
name="content"
|
||||||
id="messageInput"
|
id="messageInput"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Message..."
|
placeholder="Message... (type @ to mention)"
|
||||||
data-i18n-placeholder="chat-placeholder"
|
data-i18n-placeholder="chat-placeholder"
|
||||||
autofocus
|
autofocus
|
||||||
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -35,13 +44,28 @@
|
||||||
<button class="scroll-to-bottom" id="scrollToBottom">↓</button>
|
<button class="scroll-to-bottom" id="scrollToBottom">↓</button>
|
||||||
</div>
|
</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>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
// ==========================================================================
|
|
||||||
// NOTIFICATION HELPER - Uses GBAlerts bell system
|
|
||||||
// ==========================================================================
|
|
||||||
function notify(message, type) {
|
function notify(message, type) {
|
||||||
type = type || "info";
|
type = type || "info";
|
||||||
if (window.GBAlerts) {
|
if (window.GBAlerts) {
|
||||||
|
|
@ -55,9 +79,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================================================
|
|
||||||
// CONFIGURATION
|
|
||||||
// ==========================================================================
|
|
||||||
var WS_BASE_URL =
|
var WS_BASE_URL =
|
||||||
window.location.protocol === "https:" ? "wss://" : "ws://";
|
window.location.protocol === "https:" ? "wss://" : "ws://";
|
||||||
var WS_URL = WS_BASE_URL + window.location.host;
|
var WS_URL = WS_BASE_URL + window.location.host;
|
||||||
|
|
@ -71,9 +92,58 @@
|
||||||
CONTEXT_CHANGE: 5,
|
CONTEXT_CHANGE: 5,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==========================================================================
|
var EntityTypes = {
|
||||||
// STATE
|
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 ws = null;
|
||||||
var currentSessionId = null;
|
var currentSessionId = null;
|
||||||
var currentUserId = null;
|
var currentUserId = null;
|
||||||
|
|
@ -84,15 +154,48 @@
|
||||||
var reconnectAttempts = 0;
|
var reconnectAttempts = 0;
|
||||||
var maxReconnectAttempts = 5;
|
var maxReconnectAttempts = 5;
|
||||||
|
|
||||||
// ==========================================================================
|
var mentionState = {
|
||||||
// MESSAGE FUNCTIONS
|
active: false,
|
||||||
// ==========================================================================
|
query: "",
|
||||||
|
startPos: -1,
|
||||||
|
selectedIndex: 0,
|
||||||
|
results: [],
|
||||||
|
};
|
||||||
|
|
||||||
function escapeHtml(text) {
|
function escapeHtml(text) {
|
||||||
var div = document.createElement("div");
|
var div = document.createElement("div");
|
||||||
div.textContent = text;
|
div.textContent = text;
|
||||||
return div.innerHTML;
|
return div.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
function addMessage(sender, content, msgId) {
|
||||||
var messages = document.getElementById("messages");
|
var messages = document.getElementById("messages");
|
||||||
if (!messages) return;
|
if (!messages) return;
|
||||||
|
|
@ -102,15 +205,19 @@
|
||||||
if (msgId) div.id = msgId;
|
if (msgId) div.id = msgId;
|
||||||
|
|
||||||
if (sender === "user") {
|
if (sender === "user") {
|
||||||
|
var processedContent = renderMentionInMessage(
|
||||||
|
escapeHtml(content),
|
||||||
|
);
|
||||||
div.innerHTML =
|
div.innerHTML =
|
||||||
'<div class="message-content user-message">' +
|
'<div class="message-content user-message">' +
|
||||||
escapeHtml(content) +
|
processedContent +
|
||||||
"</div>";
|
"</div>";
|
||||||
} else {
|
} else {
|
||||||
var parsed =
|
var parsed =
|
||||||
typeof marked !== "undefined" && marked.parse
|
typeof marked !== "undefined" && marked.parse
|
||||||
? marked.parse(content)
|
? marked.parse(content)
|
||||||
: escapeHtml(content);
|
: escapeHtml(content);
|
||||||
|
parsed = renderMentionInMessage(parsed);
|
||||||
div.innerHTML =
|
div.innerHTML =
|
||||||
'<div class="message-content bot-message">' +
|
'<div class="message-content bot-message">' +
|
||||||
parsed +
|
parsed +
|
||||||
|
|
@ -119,6 +226,415 @@
|
||||||
|
|
||||||
messages.appendChild(div);
|
messages.appendChild(div);
|
||||||
messages.scrollTop = messages.scrollHeight;
|
messages.scrollTop = messages.scrollHeight;
|
||||||
|
|
||||||
|
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) {
|
function updateStreaming(content) {
|
||||||
|
|
@ -128,6 +644,7 @@
|
||||||
typeof marked !== "undefined" && marked.parse
|
typeof marked !== "undefined" && marked.parse
|
||||||
? marked.parse(content)
|
? marked.parse(content)
|
||||||
: escapeHtml(content);
|
: escapeHtml(content);
|
||||||
|
parsed = renderMentionInMessage(parsed);
|
||||||
el.querySelector(".message-content").innerHTML = parsed;
|
el.querySelector(".message-content").innerHTML = parsed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -139,8 +656,10 @@
|
||||||
typeof marked !== "undefined" && marked.parse
|
typeof marked !== "undefined" && marked.parse
|
||||||
? marked.parse(currentStreamingContent)
|
? marked.parse(currentStreamingContent)
|
||||||
: escapeHtml(currentStreamingContent);
|
: escapeHtml(currentStreamingContent);
|
||||||
|
parsed = renderMentionInMessage(parsed);
|
||||||
el.querySelector(".message-content").innerHTML = parsed;
|
el.querySelector(".message-content").innerHTML = parsed;
|
||||||
el.removeAttribute("id");
|
el.removeAttribute("id");
|
||||||
|
setupMentionClickHandlers(el);
|
||||||
}
|
}
|
||||||
streamingMessageId = null;
|
streamingMessageId = null;
|
||||||
currentStreamingContent = "";
|
currentStreamingContent = "";
|
||||||
|
|
@ -171,9 +690,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================================================
|
|
||||||
// SEND MESSAGE - GLOBAL FUNCTION
|
|
||||||
// ==========================================================================
|
|
||||||
function sendMessage() {
|
function sendMessage() {
|
||||||
var input = document.getElementById("messageInput");
|
var input = document.getElementById("messageInput");
|
||||||
if (!input) {
|
if (!input) {
|
||||||
|
|
@ -186,12 +702,11 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always show user message locally
|
hideMentionDropdown();
|
||||||
addMessage("user", content);
|
addMessage("user", content);
|
||||||
input.value = "";
|
input.value = "";
|
||||||
input.focus();
|
input.focus();
|
||||||
|
|
||||||
// Try to send via WebSocket if connected
|
|
||||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
ws.send(
|
ws.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
|
|
@ -209,12 +724,8 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sendMessage globally available immediately
|
|
||||||
window.sendMessage = sendMessage;
|
window.sendMessage = sendMessage;
|
||||||
|
|
||||||
// ==========================================================================
|
|
||||||
// WEBSOCKET CONNECTION
|
|
||||||
// ==========================================================================
|
|
||||||
function connectWebSocket() {
|
function connectWebSocket() {
|
||||||
if (ws) {
|
if (ws) {
|
||||||
ws.close();
|
ws.close();
|
||||||
|
|
@ -258,9 +769,6 @@
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================================================
|
|
||||||
// INITIALIZATION
|
|
||||||
// ==========================================================================
|
|
||||||
function initChat() {
|
function initChat() {
|
||||||
var botName = "default";
|
var botName = "default";
|
||||||
fetch("/api/auth?bot_name=" + encodeURIComponent(botName))
|
fetch("/api/auth?bot_name=" + encodeURIComponent(botName))
|
||||||
|
|
@ -299,7 +807,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input) {
|
if (input) {
|
||||||
|
input.addEventListener("input", handleMentionInput);
|
||||||
|
|
||||||
input.onkeydown = function (e) {
|
input.onkeydown = function (e) {
|
||||||
|
if (handleMentionKeydown(e)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (e.key === "Enter" && !e.shiftKey) {
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
sendMessage();
|
sendMessage();
|
||||||
|
|
@ -313,15 +826,20 @@
|
||||||
sendMessage();
|
sendMessage();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.addEventListener("click", function (e) {
|
||||||
|
if (
|
||||||
|
!e.target.closest("#mentionDropdown") &&
|
||||||
|
!e.target.closest("#messageInput")
|
||||||
|
) {
|
||||||
|
hideMentionDropdown();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run setup immediately
|
|
||||||
setupEventHandlers();
|
setupEventHandlers();
|
||||||
initChat();
|
initChat();
|
||||||
|
|
||||||
console.log(
|
console.log("Chat module initialized with @ mentions support");
|
||||||
"Chat module initialized, sendMessage is:",
|
|
||||||
typeof window.sendMessage,
|
|
||||||
);
|
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue