b810c9fcff
Add delete_meeting, delete_call, delete_note tools for Shira. Add expand/collapse toggle button to the floating chat window. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
785 lines
40 KiB
JavaScript
785 lines
40 KiB
JavaScript
define('modules/smart-assistant/views/floating-chat', ['view'], function (View) {
|
|
|
|
return View.extend({
|
|
|
|
templateContent: '' +
|
|
'<div class="sa-fab-wrapper">' +
|
|
' <div class="sa-fab" title="{{translate \'Smart Assistant\' scope=\'SmartAssistant\'}}">' +
|
|
' <img src="client/custom/modules/smart-assistant/img/shira-avatar.png" class="sa-fab-avatar" alt="שירה">' +
|
|
' </div>' +
|
|
' <span class="sa-fab-badge" style="display:none;"></span>' +
|
|
'</div>' +
|
|
'<div class="sa-chat-panel" style="display:none;">' +
|
|
' <div class="sa-chat-header">' +
|
|
' <img src="client/custom/modules/smart-assistant/img/shira-avatar.png" style="width: 26px; height: 26px; border-radius: 50%; margin-left: 8px;">' +
|
|
' <span class="sa-chat-title">שירה</span>' +
|
|
' <span class="sa-mode-badge"></span>' +
|
|
' <div class="sa-chat-header-actions">' +
|
|
' <button class="btn btn-link sa-memory-btn" type="button" title="{{translate \'Case Memory\' scope=\'SmartAssistant\'}}" style="display:none;">' +
|
|
' <span class="fas fa-brain"></span>' +
|
|
' </button>' +
|
|
' <button class="btn btn-link sa-history-btn" type="button" title="{{translate \'Previous Conversations\' scope=\'SmartAssistant\'}}">' +
|
|
' <span class="fas fa-list"></span>' +
|
|
' </button>' +
|
|
' <button class="btn btn-link sa-new-btn" type="button" title="{{translate \'New Conversation\' scope=\'SmartAssistant\'}}">' +
|
|
' <span class="fas fa-plus"></span>' +
|
|
' </button>' +
|
|
' <button class="btn btn-link sa-expand-btn" type="button" title="{{translate \'Expand\' scope=\'SmartAssistant\'}}">' +
|
|
' <span class="fas fa-expand-alt"></span>' +
|
|
' </button>' +
|
|
' <button class="btn btn-link sa-close-btn" type="button">' +
|
|
' <span class="fas fa-times"></span>' +
|
|
' </button>' +
|
|
' </div>' +
|
|
' </div>' +
|
|
' <div class="sa-history-view" style="display:none;">' +
|
|
' <div class="sa-history-header">' +
|
|
' <button class="btn btn-link sa-history-back" type="button">' +
|
|
' <span class="fas fa-arrow-right"></span> {{translate \'Back\' scope=\'SmartAssistant\'}}' +
|
|
' </button>' +
|
|
' </div>' +
|
|
' <div class="sa-history-list"></div>' +
|
|
' </div>' +
|
|
' <div class="sa-memory-view" style="display:none;">' +
|
|
' <div class="sa-memory-header">' +
|
|
' <button class="btn btn-link sa-memory-back" type="button">' +
|
|
' <span class="fas fa-arrow-right"></span> {{translate \'Back\' scope=\'SmartAssistant\'}}' +
|
|
' </button>' +
|
|
' <button class="btn btn-sm btn-default sa-add-memory-btn" type="button">' +
|
|
' <span class="fas fa-plus"></span> {{translate \'Add Memory\' scope=\'SmartAssistant\'}}' +
|
|
' </button>' +
|
|
' </div>' +
|
|
' <div class="sa-memory-tabs"></div>' +
|
|
' <div class="sa-memory-list"></div>' +
|
|
' </div>' +
|
|
' <div class="sa-chat-view">' +
|
|
' <div class="sa-chat-summary" style="display:none;"></div>' +
|
|
' <div class="sa-chat-messages"></div>' +
|
|
' <div class="sa-chat-input-row">' +
|
|
' <textarea class="form-control sa-chat-input" rows="1" placeholder="{{translate \'Ask the assistant...\' scope=\'SmartAssistant\'}}"></textarea>' +
|
|
' <button class="btn btn-primary sa-chat-send" type="button" disabled>' +
|
|
' <span class="fas fa-paper-plane"></span>' +
|
|
' </button>' +
|
|
' </div>' +
|
|
' </div>' +
|
|
'</div>' +
|
|
'<style>' +
|
|
'.smart-assistant-fab-container { position: fixed; bottom: 24px; left: 24px; z-index: 1050; direction: rtl; font-family: inherit; }' +
|
|
'.sa-fab-wrapper { position: relative; display: inline-block; }' +
|
|
'.sa-fab { width: 56px; height: 56px; border-radius: 50%; background: transparent; display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: 0 4px 12px rgba(0,0,0,0.25); transition: transform 0.2s; overflow: hidden; }' +
|
|
'.sa-fab:hover { transform: scale(1.08); }' +
|
|
'.sa-fab-avatar { width: 100%; height: 100%; object-fit: cover; }' +
|
|
'.sa-fab-badge { position: absolute; top: -4px; right: -4px; background: #c62828; color: #fff; border-radius: 10px; min-width: 20px; height: 20px; font-size: 11px; font-weight: bold; display: flex; align-items: center; justify-content: center; padding: 0 5px; z-index: 1; }' +
|
|
'.sa-chat-panel { position: absolute; bottom: 68px; left: 0; width: 400px; max-height: 600px; background: #fff; border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.18); display: flex; flex-direction: column; overflow: hidden; animation: sa-slide-up 0.25s ease-out; transition: width 0.3s ease, max-height 0.3s ease; }' +
|
|
'.sa-chat-panel.sa-expanded { width: 700px; max-height: 85vh; }' +
|
|
'.sa-chat-panel.sa-expanded .sa-chat-messages { max-height: calc(85vh - 180px); }' +
|
|
'.sa-chat-panel.sa-expanded .sa-history-list, .sa-chat-panel.sa-expanded .sa-memory-list { max-height: calc(85vh - 200px); }' +
|
|
'@keyframes sa-slide-up { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: translateY(0); } }' +
|
|
'.sa-chat-header { display: flex; align-items: center; padding: 10px 16px; background: #5c6bc0; color: #fff; gap: 8px; }' +
|
|
'.sa-chat-title { font-size: 15px; font-weight: 600; white-space: nowrap; }' +
|
|
'.sa-mode-badge { font-size: 11px; background: rgba(255,255,255,0.2); padding: 2px 8px; border-radius: 10px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 140px; }' +
|
|
'.sa-chat-header-actions { display: flex; align-items: center; gap: 2px; margin-right: auto; }' +
|
|
'.sa-chat-header-actions .btn-link { color: #fff; opacity: 0.8; font-size: 14px; padding: 2px 6px; }' +
|
|
'.sa-chat-header-actions .btn-link:hover { opacity: 1; color: #fff; }' +
|
|
'.sa-chat-summary { padding: 10px 14px; background: #fff3e0; border-bottom: 1px solid #ffe0b2; font-size: 13px; color: #e65100; direction: rtl; text-align: right; }' +
|
|
'.sa-chat-view { display: flex; flex-direction: column; flex: 1; overflow: hidden; }' +
|
|
'.sa-chat-messages { flex: 1; overflow-y: auto; padding: 12px 14px; max-height: 380px; min-height: 80px; direction: rtl; text-align: right; }' +
|
|
'.sa-chat-messages:empty::before { content: attr(data-empty-text); display: block; text-align: center; color: #999; padding: 30px 10px; font-size: 13px; }' +
|
|
'.sa-msg { margin-bottom: 10px; padding: 8px 12px; border-radius: 10px; font-size: 13px; line-height: 1.5; max-width: 90%; word-wrap: break-word; }' +
|
|
'.sa-msg-user { background: #e3f2fd; margin-left: auto; margin-right: 0; text-align: right; }' +
|
|
'.sa-msg-assistant { background: #f5f0ff; margin-right: auto; margin-left: 0; text-align: right; position: relative; padding-right: 36px; }' +
|
|
'.sa-msg-assistant::before { content: ""; position: absolute; right: 6px; top: 6px; width: 24px; height: 24px; border-radius: 50%; background: url(client/custom/modules/smart-assistant/img/shira-avatar.png) center/cover; }' +
|
|
'.sa-msg-error { background: #fce4ec; color: #c62828; }' +
|
|
'.sa-msg-loading { background: #f5f5f5; color: #999; }' +
|
|
'.sa-table { width: 100%; border-collapse: collapse; margin: 6px 0; font-size: 12px; direction: rtl; text-align: right; }' +
|
|
'.sa-table th, .sa-table td { border: 1px solid #ddd; padding: 5px 8px; white-space: nowrap; unicode-bidi: plaintext; }' +
|
|
'.sa-table th { background: #f0eef8; font-weight: 600; color: #333; }' +
|
|
'.sa-table tr:nth-child(even) { background: #fafafa; }' +
|
|
'.sa-table tr:hover { background: #f5f0ff; }' +
|
|
'.sa-msg-action { background: #fff8e1; padding: 6px 12px; border-radius: 8px; margin-bottom: 8px; font-size: 12px; display: flex; align-items: center; justify-content: space-between; gap: 8px; }' +
|
|
'.sa-action-btns { display: flex; gap: 4px; }' +
|
|
'.sa-action-btns .btn { padding: 2px 8px; font-size: 11px; border-radius: 4px; }' +
|
|
'.sa-chat-input-row { display: flex; gap: 8px; padding: 10px 14px; border-top: 1px solid #eee; direction: rtl; align-items: flex-end; }' +
|
|
'.sa-chat-input { flex: 1; border-radius: 16px; padding: 8px 14px; font-size: 13px; direction: rtl; resize: none; overflow-y: auto; max-height: 120px; min-height: 36px; line-height: 1.4; font-family: inherit; }' +
|
|
'.sa-chat-send { border-radius: 50%; width: 36px; height: 36px; padding: 0; display: flex; align-items: center; justify-content: center; }' +
|
|
'.sa-history-view, .sa-memory-view { flex: 1; overflow: hidden; display: flex; flex-direction: column; }' +
|
|
'.sa-history-header, .sa-memory-header { padding: 8px 14px; border-bottom: 1px solid #eee; direction: rtl; text-align: right; display: flex; justify-content: space-between; align-items: center; }' +
|
|
'.sa-history-header .btn-link, .sa-memory-header .btn-link { color: #5c6bc0; font-size: 13px; padding: 2px 0; text-decoration: none; }' +
|
|
'.sa-history-list, .sa-memory-list { flex: 1; overflow-y: auto; max-height: 440px; direction: rtl; }' +
|
|
'.sa-history-item { padding: 12px 16px; border-bottom: 1px solid #f0f0f0; cursor: pointer; transition: background 0.15s; }' +
|
|
'.sa-history-item:hover { background: #f5f5ff; }' +
|
|
'.sa-history-item-title { font-size: 13px; font-weight: 500; color: #333; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-align: right; }' +
|
|
'.sa-history-item-meta { font-size: 11px; color: #999; margin-top: 3px; display: flex; justify-content: space-between; }' +
|
|
'.sa-history-item-preview { font-size: 12px; color: #666; margin-top: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-align: right; }' +
|
|
'.sa-history-empty { text-align: center; color: #999; padding: 40px 20px; font-size: 13px; }' +
|
|
'.sa-memory-tabs { display: flex; gap: 4px; padding: 8px 14px; flex-wrap: wrap; direction: rtl; border-bottom: 1px solid #f0f0f0; }' +
|
|
'.sa-memory-tab { padding: 4px 10px; border-radius: 12px; font-size: 11px; cursor: pointer; background: #f5f5f5; color: #666; border: none; transition: all 0.15s; }' +
|
|
'.sa-memory-tab.active { background: #5c6bc0; color: #fff; }' +
|
|
'.sa-memory-entry { padding: 10px 16px; border-bottom: 1px solid #f5f5f5; direction: rtl; text-align: right; }' +
|
|
'.sa-memory-entry-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; }' +
|
|
'.sa-memory-entry-title { font-size: 13px; font-weight: 500; }' +
|
|
'.sa-memory-entry-badges { display: flex; gap: 4px; }' +
|
|
'.sa-memory-entry-badge { padding: 1px 6px; border-radius: 8px; font-size: 10px; }' +
|
|
'.sa-badge-critical { background: #ffcdd2; color: #c62828; }' +
|
|
'.sa-badge-high { background: #fff3e0; color: #e65100; }' +
|
|
'.sa-badge-normal { background: #e3f2fd; color: #1565c0; }' +
|
|
'.sa-badge-low { background: #f5f5f5; color: #999; }' +
|
|
'.sa-badge-pinned { background: #e8f5e9; color: #2e7d32; }' +
|
|
'.sa-badge-source { background: #f3e5f5; color: #7b1fa2; }' +
|
|
'.sa-memory-entry-content { font-size: 12px; color: #555; line-height: 1.4; max-height: 60px; overflow: hidden; }' +
|
|
'.sa-memory-entry-meta { font-size: 10px; color: #999; margin-top: 4px; }' +
|
|
'@media (max-width: 480px) { .sa-chat-panel { width: calc(100vw - 48px); left: 0; } .sa-chat-panel.sa-expanded { width: calc(100vw - 48px); } .smart-assistant-fab-container { bottom: 16px; left: 16px; } }' +
|
|
'</style>',
|
|
|
|
conversationId: null,
|
|
isOpen: false,
|
|
currentMode: 'office',
|
|
currentCaseId: null,
|
|
currentCaseName: null,
|
|
currentView: 'chat',
|
|
isExpanded: false,
|
|
currentMemoryCategory: null,
|
|
memoryData: null,
|
|
|
|
events: {
|
|
'click .sa-fab': function () { this.togglePanel(); },
|
|
'click .sa-close-btn': function () { this.closePanel(); },
|
|
'click .sa-expand-btn': function () { this.toggleExpand(); },
|
|
'click .sa-chat-send': function () { this.sendMessage(); },
|
|
'click .sa-history-btn': function () { this.showHistory(); },
|
|
'click .sa-new-btn': function () { this.startNewConversation(); },
|
|
'click .sa-history-back': function () { this.showChat(); },
|
|
'click .sa-memory-btn': function () { this.showMemory(); },
|
|
'click .sa-memory-back': function () { this.showChat(); },
|
|
'click .sa-add-memory-btn': function () { this.openAddMemoryModal(); },
|
|
'click .sa-history-item': function (e) {
|
|
var key = $(e.currentTarget).data('key');
|
|
if (key) this.loadConversation(key);
|
|
},
|
|
'click .sa-memory-tab': function (e) {
|
|
var cat = $(e.currentTarget).data('category');
|
|
this.filterMemory(cat);
|
|
},
|
|
'keydown .sa-chat-input': function (e) {
|
|
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); this.sendMessage(); }
|
|
},
|
|
'input .sa-chat-input': function (e) {
|
|
this.$el.find('.sa-chat-send').prop('disabled', !e.target.value.trim());
|
|
// Auto-grow textarea
|
|
e.target.style.height = 'auto';
|
|
e.target.style.height = Math.min(e.target.scrollHeight, 120) + 'px';
|
|
},
|
|
'click .sa-action-approve': function (e) {
|
|
var $btn = $(e.currentTarget);
|
|
this.handleAction($btn.data('conversation-id'), $btn.data('action-id'), true);
|
|
},
|
|
'click .sa-action-reject': function (e) {
|
|
var $btn = $(e.currentTarget);
|
|
this.handleAction($btn.data('conversation-id'), $btn.data('action-id'), false);
|
|
},
|
|
},
|
|
|
|
afterRender: function () {
|
|
var self = this;
|
|
|
|
Espo.Ajax.getRequest('SmartAssistant/action/status').then(function (result) {
|
|
if (!result.enabled) {
|
|
self.$el.find('.sa-fab').hide();
|
|
return;
|
|
}
|
|
|
|
self.$el.find('.sa-fab').show();
|
|
self.$el.find('.sa-chat-messages').attr('data-empty-text',
|
|
self.translate('Chat with the assistant', 'labels', 'SmartAssistant') || 'שוחח/י עם שירה');
|
|
self.loadAlertCount();
|
|
self._setupRouteListener();
|
|
}).catch(function () {
|
|
self.$el.find('.sa-fab').hide();
|
|
});
|
|
},
|
|
|
|
_setupRouteListener: function () {
|
|
var self = this;
|
|
if (this._routeListenerSet) return;
|
|
this._routeListenerSet = true;
|
|
|
|
// Check mode on route changes
|
|
if (Espo.router) {
|
|
this.listenTo(Espo.router, 'routed', function () {
|
|
self._detectMode();
|
|
});
|
|
}
|
|
|
|
this._detectMode();
|
|
},
|
|
|
|
_detectMode: function () {
|
|
var hash = window.location.hash || '';
|
|
var match = hash.match(/#Case\/view\/([a-zA-Z0-9]+)/);
|
|
|
|
if (match) {
|
|
this.currentMode = 'case';
|
|
this.currentCaseId = match[1];
|
|
this._loadCaseName(this.currentCaseId);
|
|
this.$el.find('.sa-memory-btn').show();
|
|
} else {
|
|
this.currentMode = 'office';
|
|
this.currentCaseId = null;
|
|
this.currentCaseName = null;
|
|
this.$el.find('.sa-memory-btn').hide();
|
|
}
|
|
|
|
this._updateModeIndicator();
|
|
},
|
|
|
|
_loadCaseName: function (caseId) {
|
|
var self = this;
|
|
Espo.Ajax.getRequest('Case/' + caseId, {select: 'name,number'}).then(function (data) {
|
|
self.currentCaseName = '#' + (data.number || '') + ' ' + (data.name || '');
|
|
self._updateModeIndicator();
|
|
}).catch(function () {});
|
|
},
|
|
|
|
_updateModeIndicator: function () {
|
|
var $badge = this.$el.find('.sa-mode-badge');
|
|
if (this.currentMode === 'case' && this.currentCaseName) {
|
|
$badge.text(this.currentCaseName).show();
|
|
} else if (this.currentMode === 'case') {
|
|
$badge.text(this.translate('Case Mode', 'labels', 'SmartAssistant')).show();
|
|
} else {
|
|
$badge.text(this.translate('Office Mode', 'labels', 'SmartAssistant')).show();
|
|
}
|
|
},
|
|
|
|
togglePanel: function () { this.isOpen ? this.closePanel() : this.openPanel(); },
|
|
|
|
openPanel: function () {
|
|
this.isOpen = true;
|
|
this._detectMode();
|
|
this.$el.find('.sa-chat-panel').css('display', 'flex');
|
|
|
|
if (!this._summaryLoaded && this.currentMode === 'office') this.loadSummary();
|
|
if (!this.conversationId && !this._historyChecked) {
|
|
this._historyChecked = true;
|
|
this.loadLatestConversation();
|
|
}
|
|
this.$el.find('.sa-chat-input').focus();
|
|
},
|
|
|
|
closePanel: function () {
|
|
this.isOpen = false;
|
|
this.isExpanded = false;
|
|
var $panel = this.$el.find('.sa-chat-panel');
|
|
$panel.removeClass('sa-expanded').css('display', 'none');
|
|
this.$el.find('.sa-expand-btn .fas').removeClass('fa-compress-alt').addClass('fa-expand-alt');
|
|
},
|
|
|
|
toggleExpand: function () {
|
|
this.isExpanded = !this.isExpanded;
|
|
var $panel = this.$el.find('.sa-chat-panel');
|
|
var $icon = this.$el.find('.sa-expand-btn .fas');
|
|
|
|
if (this.isExpanded) {
|
|
$panel.addClass('sa-expanded');
|
|
$icon.removeClass('fa-expand-alt').addClass('fa-compress-alt');
|
|
} else {
|
|
$panel.removeClass('sa-expanded');
|
|
$icon.removeClass('fa-compress-alt').addClass('fa-expand-alt');
|
|
}
|
|
|
|
this.scrollToBottom();
|
|
},
|
|
|
|
showChat: function () {
|
|
this.currentView = 'chat';
|
|
this.$el.find('.sa-history-view, .sa-memory-view').hide();
|
|
this.$el.find('.sa-chat-view').show();
|
|
this.$el.find('.sa-chat-input').focus();
|
|
},
|
|
|
|
showHistory: function () {
|
|
this.currentView = 'history';
|
|
this.$el.find('.sa-chat-view, .sa-memory-view').hide();
|
|
this.$el.find('.sa-history-view').show();
|
|
this.loadConversationsList();
|
|
},
|
|
|
|
showMemory: function () {
|
|
if (this.currentMode !== 'case' || !this.currentCaseId) return;
|
|
this.currentView = 'memory';
|
|
this.$el.find('.sa-chat-view, .sa-history-view').hide();
|
|
this.$el.find('.sa-memory-view').show();
|
|
this.loadMemory();
|
|
},
|
|
|
|
startNewConversation: function () {
|
|
this.conversationId = null;
|
|
this.$el.find('.sa-chat-messages').empty();
|
|
this.showChat();
|
|
},
|
|
|
|
loadConversationsList: function () {
|
|
var $list = this.$el.find('.sa-history-list');
|
|
var self = this;
|
|
|
|
$list.html('<div class="sa-history-empty"><span class="fas fa-spinner fa-spin"></span></div>');
|
|
|
|
Espo.Ajax.getRequest('SmartAssistant/action/conversations').then(function (conversations) {
|
|
self.renderConversationsList(conversations);
|
|
}).catch(function () {
|
|
$list.html('<div class="sa-history-empty">שגיאה בטעינת שיחות</div>');
|
|
});
|
|
},
|
|
|
|
renderConversationsList: function (conversations) {
|
|
var $list = this.$el.find('.sa-history-list');
|
|
var self = this;
|
|
|
|
if (!conversations || conversations.length === 0) {
|
|
$list.html('<div class="sa-history-empty">' + self.escapeHtml(self.translate('No previous conversations', 'labels', 'SmartAssistant')) + '</div>');
|
|
return;
|
|
}
|
|
|
|
var html = '';
|
|
conversations.forEach(function (conv) {
|
|
var timeStr = self.formatConversationTime(conv.lastMessageAt);
|
|
var typeIcon = conv.conversationType === 'case' ? 'fa-gavel' : 'fa-building';
|
|
|
|
html += '<div class="sa-history-item" data-key="' + self.escapeHtml(conv.conversationKey) + '">' +
|
|
'<div class="sa-history-item-title">' +
|
|
'<span class="fas ' + typeIcon + '" style="color: #5c6bc0; margin-left: 6px; font-size: 12px;"></span>' +
|
|
self.escapeHtml(conv.name || 'שיחה') +
|
|
'</div>' +
|
|
'<div class="sa-history-item-meta">' +
|
|
'<span>' + self.escapeHtml(timeStr) + '</span>' +
|
|
'<span>' + (conv.messageCount || 0) + ' הודעות</span>' +
|
|
'</div>';
|
|
if (conv.lastMessagePreview) {
|
|
html += '<div class="sa-history-item-preview">' + self.escapeHtml(conv.lastMessagePreview.substring(0, 80)) + '</div>';
|
|
}
|
|
html += '</div>';
|
|
});
|
|
|
|
$list.html(html);
|
|
},
|
|
|
|
loadConversation: function (conversationKey) {
|
|
var self = this;
|
|
this.conversationId = conversationKey;
|
|
var $messages = this.$el.find('.sa-chat-messages');
|
|
|
|
$messages.html('<div class="sa-msg sa-msg-loading"><span class="fas fa-spinner fa-spin" style="margin-left: 5px;"></span> טוען שיחה...</div>');
|
|
this.showChat();
|
|
|
|
Espo.Ajax.getRequest('SmartAssistant/action/conversationMessages', {conversationKey: conversationKey}).then(function (response) {
|
|
$messages.empty();
|
|
(response.messages || []).forEach(function (msg) {
|
|
var cssClass = msg.role === 'user' ? 'sa-msg-user' : 'sa-msg-assistant';
|
|
var content = msg.role === 'user' ? self.escapeHtml(msg.content) : self.formatResponse(msg.content);
|
|
$messages.append('<div class="sa-msg ' + cssClass + '">' + content + '</div>');
|
|
});
|
|
self.scrollToBottom();
|
|
}).catch(function () {
|
|
$messages.html('<div class="sa-msg sa-msg-error">שגיאה בטעינת שיחה</div>');
|
|
});
|
|
},
|
|
|
|
loadLatestConversation: function () {
|
|
var self = this;
|
|
Espo.Ajax.getRequest('SmartAssistant/action/history').then(function (response) {
|
|
if (!response || !response.conversationId || !response.messages || response.messages.length === 0) return;
|
|
|
|
var updatedAt = response.updatedAt;
|
|
if (updatedAt) {
|
|
var today = new Date().toISOString().substring(0, 10);
|
|
if (updatedAt.substring(0, 10) !== today) return;
|
|
}
|
|
|
|
self.conversationId = response.conversationId;
|
|
var $messages = self.$el.find('.sa-chat-messages');
|
|
|
|
response.messages.forEach(function (msg) {
|
|
var cssClass = msg.role === 'user' ? 'sa-msg-user' : 'sa-msg-assistant';
|
|
var content = msg.role === 'user' ? self.escapeHtml(msg.content) : self.formatResponse(msg.content);
|
|
$messages.append('<div class="sa-msg ' + cssClass + '">' + content + '</div>');
|
|
});
|
|
self.scrollToBottom();
|
|
}).catch(function () {});
|
|
},
|
|
|
|
loadAlertCount: function () {
|
|
var self = this;
|
|
Espo.Ajax.getRequest('SmartAssistant/action/summary').then(function (response) {
|
|
var summary = response.summary || {};
|
|
var count = (summary.criticalAlerts || 0) + (summary.warningAlerts || 0);
|
|
self.updateBadge(count);
|
|
}).catch(function () {});
|
|
},
|
|
|
|
updateBadge: function (count) {
|
|
var $badge = this.$el.find('.sa-fab-badge');
|
|
count > 0 ? $badge.text(count).show() : $badge.hide();
|
|
},
|
|
|
|
loadSummary: function () {
|
|
this._summaryLoaded = true;
|
|
var self = this;
|
|
var $summary = this.$el.find('.sa-chat-summary');
|
|
|
|
Espo.Ajax.getRequest('SmartAssistant/action/summary').then(function (response) {
|
|
var summary = response.summary || {};
|
|
var alerts = response.alerts || [];
|
|
var critical = alerts.filter(function (a) { return a.severity === 'critical'; });
|
|
|
|
if (critical.length > 0) {
|
|
var html = '<strong>' + self.escapeHtml(critical.length + ' התראות קריטיות') + '</strong>';
|
|
critical.slice(0, 2).forEach(function (a) {
|
|
html += '<div style="margin-top: 4px; font-size: 12px;">' + self.escapeHtml('• ' + a.message) + '</div>';
|
|
});
|
|
$summary.html(html).show();
|
|
} else if (summary.casesNeedingAttention > 0) {
|
|
$summary.html(self.escapeHtml(summary.casesNeedingAttention + ' תיקים דורשים תשומת לב')).show();
|
|
} else {
|
|
$summary.html('<span style="color: #4caf50;">אין התראות קריטיות</span>').show();
|
|
}
|
|
|
|
self.updateBadge((summary.criticalAlerts || 0) + (summary.warningAlerts || 0));
|
|
}).catch(function () { $summary.hide(); });
|
|
},
|
|
|
|
sendMessage: function () {
|
|
var $input = this.$el.find('.sa-chat-input');
|
|
var message = $input.val().trim();
|
|
if (!message) return;
|
|
|
|
var self = this;
|
|
var $messages = this.$el.find('.sa-chat-messages');
|
|
var $btn = this.$el.find('.sa-chat-send');
|
|
|
|
$messages.append('<div class="sa-msg sa-msg-user">' + this.escapeHtml(message) + '</div>');
|
|
var $loading = $('<div class="sa-msg sa-msg-loading"><span class="fas fa-spinner fa-spin" style="margin-left: 5px;"></span> ' +
|
|
this.escapeHtml(this.translate('Thinking', 'labels', 'SmartAssistant') || 'חושבת...') + '</div>');
|
|
$messages.append($loading);
|
|
this.scrollToBottom();
|
|
|
|
$btn.prop('disabled', true);
|
|
$input.prop('disabled', true).val('').css('height', 'auto');
|
|
|
|
var payload = {
|
|
message: message,
|
|
mode: this.currentMode,
|
|
conversationId: this.conversationId,
|
|
};
|
|
if (this.currentMode === 'case' && this.currentCaseId) {
|
|
payload.caseId = this.currentCaseId;
|
|
}
|
|
|
|
Espo.Ajax.postRequest('SmartAssistant/action/chat', payload).then(function (response) {
|
|
self.conversationId = response.conversationId;
|
|
$loading.remove();
|
|
|
|
$messages.append('<div class="sa-msg sa-msg-assistant">' + self.formatResponse(response.text) + '</div>');
|
|
|
|
// Auto-refresh panels if actions were executed
|
|
if (response.executedActions && response.executedActions.length > 0) {
|
|
response.executedActions.forEach(function (action) {
|
|
if (action.entityType) {
|
|
self.refreshCurrentView(action.entityType);
|
|
}
|
|
});
|
|
}
|
|
|
|
self.scrollToBottom();
|
|
$input.prop('disabled', false).focus();
|
|
}).catch(function () {
|
|
$loading.remove();
|
|
$messages.append('<div class="sa-msg sa-msg-error">' +
|
|
self.escapeHtml(self.translate('Error', 'labels', 'SmartAssistant') || 'שגיאה בתקשורת') + '</div>');
|
|
self.scrollToBottom();
|
|
$input.prop('disabled', false);
|
|
$btn.prop('disabled', false);
|
|
});
|
|
},
|
|
|
|
handleAction: function (conversationId, actionId, approved) {
|
|
var self = this;
|
|
var $messages = this.$el.find('.sa-chat-messages');
|
|
|
|
Espo.Ajax.postRequest('SmartAssistant/action/execute', {
|
|
conversationId: conversationId,
|
|
actionId: actionId,
|
|
approved: approved,
|
|
}).then(function (result) {
|
|
var msg = approved ? (result.message || 'פעולה בוצעה') : 'פעולה נדחתה';
|
|
var cls = approved ? (result.success ? 'sa-msg-assistant' : 'sa-msg-error') : 'sa-msg-loading';
|
|
$messages.append('<div class="sa-msg ' + cls + '">' + self.escapeHtml(msg) + '</div>');
|
|
self.scrollToBottom();
|
|
|
|
if (approved && result.success) {
|
|
self.refreshCurrentView(result.entityType);
|
|
}
|
|
}).catch(function () {
|
|
$messages.append('<div class="sa-msg sa-msg-error">שגיאה בביצוע הפעולה</div>');
|
|
self.scrollToBottom();
|
|
});
|
|
},
|
|
|
|
loadMemory: function () {
|
|
if (!this.currentCaseId) return;
|
|
var self = this;
|
|
var $list = this.$el.find('.sa-memory-list');
|
|
|
|
$list.html('<div class="sa-history-empty"><span class="fas fa-spinner fa-spin"></span></div>');
|
|
|
|
Espo.Ajax.getRequest('SmartAssistant/action/caseMemory', {caseId: this.currentCaseId}).then(function (data) {
|
|
self.memoryData = data;
|
|
self.renderMemoryTabs(data);
|
|
self.renderMemoryEntries(data, null);
|
|
}).catch(function () {
|
|
$list.html('<div class="sa-history-empty">שגיאה בטעינת זיכרון</div>');
|
|
});
|
|
},
|
|
|
|
renderMemoryTabs: function (data) {
|
|
var $tabs = this.$el.find('.sa-memory-tabs');
|
|
var categoryLabels = {
|
|
'key_facts': 'עובדות', 'strategy': 'אסטרטגיה', 'decisions': 'החלטות',
|
|
'contacts_notes': 'אנשי קשר', 'timeline': 'ציר זמן',
|
|
'documents_notes': 'מסמכים', 'billing_notes': 'חיוב'
|
|
};
|
|
|
|
var html = '<button class="sa-memory-tab active" data-category="">הכל</button>';
|
|
var categories = Object.keys(data).filter(function (k) { return k !== '_summary'; });
|
|
|
|
categories.forEach(function (cat) {
|
|
var label = categoryLabels[cat] || cat;
|
|
var count = data[cat] ? data[cat].length : 0;
|
|
html += '<button class="sa-memory-tab" data-category="' + cat + '">' + label + ' (' + count + ')</button>';
|
|
});
|
|
|
|
$tabs.html(html);
|
|
},
|
|
|
|
renderMemoryEntries: function (data, category) {
|
|
var $list = this.$el.find('.sa-memory-list');
|
|
var self = this;
|
|
var entries = [];
|
|
|
|
var importanceLabels = {critical: 'קריטי', high: 'גבוה', normal: 'רגיל', low: 'נמוך'};
|
|
var sourceLabels = {manual: 'ידני', assistant: 'AI', auto: 'אוטומטי'};
|
|
|
|
Object.keys(data).forEach(function (cat) {
|
|
if (cat === '_summary') return;
|
|
if (category && cat !== category) return;
|
|
(data[cat] || []).forEach(function (e) { entries.push(e); });
|
|
});
|
|
|
|
if (entries.length === 0) {
|
|
$list.html('<div class="sa-history-empty">' + self.escapeHtml(self.translate('No memories yet', 'labels', 'SmartAssistant')) + '</div>');
|
|
return;
|
|
}
|
|
|
|
var html = '';
|
|
entries.forEach(function (e) {
|
|
var badgeClass = 'sa-badge-' + (e.importance || 'normal');
|
|
html += '<div class="sa-memory-entry">' +
|
|
'<div class="sa-memory-entry-header">' +
|
|
'<span class="sa-memory-entry-title">' + self.escapeHtml(e.name) + '</span>' +
|
|
'<div class="sa-memory-entry-badges">';
|
|
if (e.isPinned) html += '<span class="sa-memory-entry-badge sa-badge-pinned"><span class="fas fa-thumbtack"></span></span>';
|
|
html += '<span class="sa-memory-entry-badge ' + badgeClass + '">' + (importanceLabels[e.importance] || e.importance) + '</span>';
|
|
html += '<span class="sa-memory-entry-badge sa-badge-source">' + (sourceLabels[e.source] || e.source) + '</span>';
|
|
html += '</div></div>' +
|
|
'<div class="sa-memory-entry-content">' + self.escapeHtml(e.content) + '</div>' +
|
|
'<div class="sa-memory-entry-meta">' + self.formatConversationTime(e.createdAt) + '</div>' +
|
|
'</div>';
|
|
});
|
|
|
|
$list.html(html);
|
|
},
|
|
|
|
filterMemory: function (category) {
|
|
this.$el.find('.sa-memory-tab').removeClass('active');
|
|
this.$el.find('.sa-memory-tab[data-category="' + (category || '') + '"]').addClass('active');
|
|
this.currentMemoryCategory = category || null;
|
|
|
|
if (this.memoryData) {
|
|
this.renderMemoryEntries(this.memoryData, this.currentMemoryCategory);
|
|
}
|
|
},
|
|
|
|
openAddMemoryModal: function () {
|
|
if (!this.currentCaseId) return;
|
|
var self = this;
|
|
|
|
this.createView('addMemoryModal', 'modules/smart-assistant/views/case/modals/add-memory', {
|
|
caseId: this.currentCaseId,
|
|
}, function (view) {
|
|
view.render();
|
|
|
|
self.listenToOnce(view, 'saved', function () {
|
|
self.loadMemory();
|
|
view.close();
|
|
});
|
|
});
|
|
},
|
|
|
|
scrollToBottom: function () {
|
|
var el = this.$el.find('.sa-chat-messages').get(0);
|
|
if (el) el.scrollTop = el.scrollHeight;
|
|
},
|
|
|
|
formatConversationTime: function (dateStr) {
|
|
if (!dateStr) return '';
|
|
var date = new Date(dateStr);
|
|
var now = new Date();
|
|
var today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
var yesterday = new Date(today.getTime() - 86400000);
|
|
var convDay = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
var timeStr = ('0' + date.getHours()).slice(-2) + ':' + ('0' + date.getMinutes()).slice(-2);
|
|
|
|
if (convDay.getTime() === today.getTime()) return 'היום ' + timeStr;
|
|
if (convDay.getTime() === yesterday.getTime()) return 'אתמול ' + timeStr;
|
|
return ('0' + date.getDate()).slice(-2) + '/' + ('0' + (date.getMonth() + 1)).slice(-2) + ' ' + timeStr;
|
|
},
|
|
|
|
formatResponse: function (text) {
|
|
if (!text) return '';
|
|
|
|
var self = this;
|
|
var LRM = '\u200E';
|
|
|
|
// Split into lines for table detection
|
|
var lines = text.split('\n');
|
|
var result = [];
|
|
var i = 0;
|
|
|
|
while (i < lines.length) {
|
|
// Detect markdown table: line with |, followed by separator |---|, followed by more | lines
|
|
if (lines[i].indexOf('|') !== -1 && i + 1 < lines.length && /^\|?[\s\-:|]+\|/.test(lines[i + 1])) {
|
|
var tableLines = [];
|
|
var j = i;
|
|
|
|
// Collect all contiguous lines that contain |
|
|
while (j < lines.length && lines[j].indexOf('|') !== -1) {
|
|
tableLines.push(lines[j]);
|
|
j++;
|
|
}
|
|
|
|
if (tableLines.length >= 2) {
|
|
result.push(self.renderMarkdownTable(tableLines, LRM));
|
|
i = j;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Regular line — escape and format
|
|
var line = self.escapeHtml(lines[i]);
|
|
line = line.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
|
|
|
// Headings
|
|
line = line.replace(/^### (.+)/, '<strong style="font-size: 14px;">$1</strong>');
|
|
line = line.replace(/^## (.+)/, '<strong style="font-size: 15px;">$1</strong>');
|
|
|
|
// List items
|
|
line = line.replace(/^[-*] (.+)/, '• $1');
|
|
|
|
// Numbered lists
|
|
line = line.replace(/^(\d+)\. (.+)/, '$1. $2');
|
|
|
|
result.push(line);
|
|
i++;
|
|
}
|
|
|
|
return result.join('<br>');
|
|
},
|
|
|
|
renderMarkdownTable: function (lines, LRM) {
|
|
var self = this;
|
|
|
|
var parseRow = function (line) {
|
|
// Remove leading/trailing |, split by |
|
|
var trimmed = line.replace(/^\|/, '').replace(/\|$/, '');
|
|
return trimmed.split('|').map(function (cell) { return cell.trim(); });
|
|
};
|
|
|
|
// First line = headers
|
|
var headers = parseRow(lines[0]);
|
|
|
|
// Skip separator line (line[1])
|
|
// Remaining lines = data rows
|
|
var rows = [];
|
|
for (var i = 2; i < lines.length; i++) {
|
|
// Skip if it looks like another separator
|
|
if (/^[\s\-:|]+$/.test(lines[i].replace(/\|/g, ''))) continue;
|
|
rows.push(parseRow(lines[i]));
|
|
}
|
|
|
|
// Build HTML table with LRM for BiDi safety
|
|
var html = '<table class="sa-table" dir="rtl">';
|
|
|
|
// Header
|
|
html += '<thead><tr>';
|
|
headers.forEach(function (h) {
|
|
var escaped = self.escapeHtml(h).replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
|
html += '<th>' + LRM + escaped + LRM + '</th>';
|
|
});
|
|
html += '</tr></thead>';
|
|
|
|
// Body
|
|
html += '<tbody>';
|
|
rows.forEach(function (row) {
|
|
html += '<tr>';
|
|
for (var c = 0; c < headers.length; c++) {
|
|
var cell = row[c] !== undefined ? row[c] : '';
|
|
var escaped = self.escapeHtml(cell).replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
|
html += '<td>' + LRM + escaped + LRM + '</td>';
|
|
}
|
|
html += '</tr>';
|
|
});
|
|
html += '</tbody></table>';
|
|
|
|
return html;
|
|
},
|
|
|
|
escapeHtml: function (text) {
|
|
if (!text) return '';
|
|
var div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
},
|
|
|
|
refreshCurrentView: function (entityType) {
|
|
var mainView = this.getParentView();
|
|
|
|
while (mainView && mainView.getParentView()) {
|
|
mainView = mainView.getParentView();
|
|
}
|
|
|
|
if (!mainView) return;
|
|
|
|
var currentView = mainView.getView('main');
|
|
|
|
if (!currentView) return;
|
|
|
|
var recordView = currentView.getView('record');
|
|
|
|
if (recordView && recordView.model) {
|
|
recordView.model.trigger('update-all');
|
|
}
|
|
|
|
if (entityType === 'Task' || entityType === 'Meeting' || entityType === 'Call') {
|
|
var sideView = currentView.getView('side');
|
|
|
|
if (sideView) {
|
|
var activities = sideView.getView('activities');
|
|
var tasks = sideView.getView('tasks');
|
|
|
|
if (activities && activities.actionRefresh) activities.actionRefresh();
|
|
if (tasks && tasks.actionRefresh) tasks.actionRefresh();
|
|
}
|
|
}
|
|
},
|
|
});
|
|
});
|