Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0fc53b54b7 | |||
| 4c0bf3a67c | |||
| 35c57d36ae | |||
| 0f77631f75 | |||
| 81fa5f3a08 |
@@ -1,6 +1,6 @@
|
||||
# SmartAssistant - עוזר חכם
|
||||
|
||||
**גרסה:** 1.1.6 | **מחבר:** klear | **EspoCRM:** >= 8.0.0
|
||||
**גרסה:** 1.1.7 | **מחבר:** klear | **EspoCRM:** >= 8.0.0
|
||||
|
||||
## תיאור
|
||||
עוזר AI מאוחד למשרד עורכי דין. מספק ממשק צ'אט צף עם שני מצבי עבודה: **מצב משרד** (סקירה כללית, התראות, סטטיסטיקות) ו**מצב תיק** (סיוע מעמיק בתיק ספציפי). כולל מערכת זיכרון תיק מובנית, ביצוע פעולות באישור המשתמש, ואינטגרציה עם Stream של EspoCRM.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
@@ -4,12 +4,13 @@ define('modules/smart-assistant/views/floating-chat', ['view'], function (View)
|
||||
|
||||
templateContent: '' +
|
||||
'<div class="sa-fab" title="{{translate \'Smart Assistant\' scope=\'SmartAssistant\'}}">' +
|
||||
' <span class="fas fa-robot"></span>' +
|
||||
' <img src="client/custom/modules/smart-assistant/img/shira-avatar.png" class="sa-fab-avatar" alt="שירה">' +
|
||||
' <span class="sa-fab-badge" style="display:none;"></span>' +
|
||||
'</div>' +
|
||||
'<div class="sa-chat-panel" style="display:none;">' +
|
||||
' <div class="sa-chat-header">' +
|
||||
' <span class="sa-chat-title">{{translate \'Smart Assistant\' scope=\'SmartAssistant\'}}</span>' +
|
||||
' <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;">' +
|
||||
@@ -50,7 +51,7 @@ define('modules/smart-assistant/views/floating-chat', ['view'], function (View)
|
||||
' <div class="sa-chat-summary" style="display:none;"></div>' +
|
||||
' <div class="sa-chat-messages"></div>' +
|
||||
' <div class="sa-chat-input-row">' +
|
||||
' <input type="text" class="form-control sa-chat-input" placeholder="{{translate \'Ask the assistant...\' scope=\'SmartAssistant\'}}">' +
|
||||
' <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>' +
|
||||
@@ -59,8 +60,9 @@ define('modules/smart-assistant/views/floating-chat', ['view'], function (View)
|
||||
'</div>' +
|
||||
'<style>' +
|
||||
'.smart-assistant-fab-container { position: fixed; bottom: 24px; left: 24px; z-index: 1050; direction: rtl; font-family: inherit; }' +
|
||||
'.sa-fab { width: 56px; height: 56px; border-radius: 50%; background: #5c6bc0; color: #fff; 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, background 0.2s; font-size: 22px; position: relative; }' +
|
||||
'.sa-fab:hover { background: #3f51b5; transform: scale(1.08); }' +
|
||||
'.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; position: relative; }' +
|
||||
'.sa-fab:hover { transform: scale(1.08); }' +
|
||||
'.sa-fab-avatar { width: 100%; height: 100%; border-radius: 50%; }' +
|
||||
'.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; }' +
|
||||
'.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; }' +
|
||||
'@keyframes sa-slide-up { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: translateY(0); } }' +
|
||||
@@ -76,14 +78,20 @@ define('modules/smart-assistant/views/floating-chat', ['view'], function (View)
|
||||
'.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; }' +
|
||||
'.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; }' +
|
||||
'.sa-chat-input { flex: 1; border-radius: 20px; padding: 8px 14px; font-size: 13px; direction: rtl; }' +
|
||||
'.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; }' +
|
||||
@@ -141,11 +149,14 @@ define('modules/smart-assistant/views/floating-chat', ['view'], function (View)
|
||||
var cat = $(e.currentTarget).data('category');
|
||||
this.filterMemory(cat);
|
||||
},
|
||||
'keypress .sa-chat-input': function (e) {
|
||||
'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);
|
||||
@@ -168,7 +179,7 @@ define('modules/smart-assistant/views/floating-chat', ['view'], function (View)
|
||||
|
||||
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.translate('Chat with the assistant', 'labels', 'SmartAssistant') || 'שוחח/י עם שירה');
|
||||
self.loadAlertCount();
|
||||
self._setupRouteListener();
|
||||
}).catch(function () {
|
||||
@@ -417,12 +428,12 @@ define('modules/smart-assistant/views/floating-chat', ['view'], function (View)
|
||||
|
||||
$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>');
|
||||
this.escapeHtml(this.translate('Thinking', 'labels', 'SmartAssistant') || 'חושבת...') + '</div>');
|
||||
$messages.append($loading);
|
||||
this.scrollToBottom();
|
||||
|
||||
$btn.prop('disabled', true);
|
||||
$input.prop('disabled', true).val('');
|
||||
$input.prop('disabled', true).val('').css('height', 'auto');
|
||||
|
||||
var payload = {
|
||||
message: message,
|
||||
@@ -439,19 +450,12 @@ define('modules/smart-assistant/views/floating-chat', ['view'], function (View)
|
||||
|
||||
$messages.append('<div class="sa-msg sa-msg-assistant">' + self.formatResponse(response.text) + '</div>');
|
||||
|
||||
// Render action items
|
||||
if (response.actions && response.actions.length > 0) {
|
||||
response.actions.forEach(function (action) {
|
||||
var actionHtml = '<div class="sa-msg-action">' +
|
||||
'<span><span class="fas fa-bolt" style="color: #f57c00; margin-left: 4px;"></span>' +
|
||||
self.escapeHtml(action.displayText || action.tool) + '</span>' +
|
||||
'<div class="sa-action-btns">' +
|
||||
'<button class="btn btn-success btn-xs sa-action-approve" data-conversation-id="' + self.escapeHtml(response.conversationId) +
|
||||
'" data-action-id="' + self.escapeHtml(action.actionId) + '"><span class="fas fa-check"></span></button>' +
|
||||
'<button class="btn btn-danger btn-xs sa-action-reject" data-conversation-id="' + self.escapeHtml(response.conversationId) +
|
||||
'" data-action-id="' + self.escapeHtml(action.actionId) + '"><span class="fas fa-times"></span></button>' +
|
||||
'</div></div>';
|
||||
$messages.append(actionHtml);
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -480,6 +484,10 @@ define('modules/smart-assistant/views/floating-chat', ['view'], function (View)
|
||||
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();
|
||||
@@ -607,11 +615,101 @@ define('modules/smart-assistant/views/floating-chat', ['view'], function (View)
|
||||
|
||||
formatResponse: function (text) {
|
||||
if (!text) return '';
|
||||
text = this.escapeHtml(text);
|
||||
text = text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
||||
text = text.replace(/\n/g, '<br>');
|
||||
text = text.replace(/^- (.+)/gm, '• $1');
|
||||
return text;
|
||||
|
||||
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) {
|
||||
@@ -620,5 +718,37 @@ define('modules/smart-assistant/views/floating-chat', ['view'], function (View)
|
||||
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') {
|
||||
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();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
"Smart Assistant": "שירה",
|
||||
"Office Mode": "מצב משרד",
|
||||
"Case Mode": "מצב תיק",
|
||||
"Add Memory": "הוסף זיכרון",
|
||||
"Search Memory": "חפש בזיכרון",
|
||||
"Add Memory": "הוספת זיכרון",
|
||||
"Search Memory": "חיפוש בזיכרון",
|
||||
"New Conversation": "שיחה חדשה",
|
||||
"Conversation History": "היסטוריית שיחות",
|
||||
"Previous Conversations": "שיחות קודמות",
|
||||
"No previous conversations": "אין שיחות קודמות",
|
||||
"Ask the assistant...": "שאל/י את שירה...",
|
||||
"Thinking": "חושב...",
|
||||
"Thinking": "חושבת...",
|
||||
"Error": "שגיאה בתקשורת",
|
||||
"Back": "חזרה",
|
||||
"Chat with the assistant": "שוחח/י עם שירה",
|
||||
|
||||
@@ -76,26 +76,48 @@ class ActionExecutor
|
||||
|
||||
private function createTask(array $params, ?string $caseId, string $userId): array
|
||||
{
|
||||
$task = $this->entityManager->getNewEntity('Task');
|
||||
$task->set([
|
||||
$taskData = [
|
||||
'name' => $params['name'] ?? 'משימה חדשה',
|
||||
'status' => 'Not Started',
|
||||
'priority' => $params['priority'] ?? 'Normal',
|
||||
'dateEnd' => $this->normalizeDateTime($params['dateEnd'] ?? null),
|
||||
'description' => $params['description'] ?? null,
|
||||
'parentType' => 'Case', 'parentId' => $caseId,
|
||||
'assignedUserId' => $params['assignedUserId'] ?? $userId,
|
||||
]);
|
||||
];
|
||||
|
||||
if ($caseId) {
|
||||
$taskData['parentType'] = 'Case';
|
||||
$taskData['parentId'] = $caseId;
|
||||
}
|
||||
|
||||
$task = $this->entityManager->getNewEntity('Task');
|
||||
$task->set($taskData);
|
||||
$this->entityManager->saveEntity($task);
|
||||
return ['success' => true, 'message' => "משימה \"{$params['name']}\" נוצרה בהצלחה", 'entityType' => 'Task', 'entityId' => $task->get('id')];
|
||||
|
||||
$msg = "✅ משימה \"{$params['name']}\" נוצרה בהצלחה";
|
||||
if ($caseId) {
|
||||
$case = $this->entityManager->getEntityById('Case', $caseId);
|
||||
if ($case) {
|
||||
$msg .= " בתיק " . $case->get('name');
|
||||
}
|
||||
}
|
||||
|
||||
return ['success' => true, 'message' => $msg, 'entityType' => 'Task', 'entityId' => $task->get('id')];
|
||||
}
|
||||
|
||||
private function addNote(array $params, ?string $caseId, string $userId): array
|
||||
{
|
||||
$noteData = ['type' => 'Post', 'post' => $params['post'] ?? '', 'createdById' => $userId];
|
||||
|
||||
if ($caseId) {
|
||||
$noteData['parentType'] = 'Case';
|
||||
$noteData['parentId'] = $caseId;
|
||||
}
|
||||
|
||||
$note = $this->entityManager->getNewEntity('Note');
|
||||
$note->set(['type' => 'Post', 'post' => $params['post'] ?? '', 'parentType' => 'Case', 'parentId' => $caseId, 'createdById' => $userId]);
|
||||
$note->set($noteData);
|
||||
$this->entityManager->saveEntity($note);
|
||||
return ['success' => true, 'message' => 'הערה נוספה לתיק', 'entityType' => 'Note', 'entityId' => $note->get('id')];
|
||||
return ['success' => true, 'message' => '✅ הערה נוספה' . ($caseId ? ' לתיק' : ''), 'entityType' => 'Note', 'entityId' => $note->get('id')];
|
||||
}
|
||||
|
||||
private function changeStatus(array $params, ?string $caseId): array
|
||||
@@ -119,16 +141,23 @@ class ActionExecutor
|
||||
|
||||
private function createMeeting(array $params, ?string $caseId, string $userId): array
|
||||
{
|
||||
$meeting = $this->entityManager->getNewEntity('Meeting');
|
||||
$meeting->set([
|
||||
$meetingData = [
|
||||
'name' => $params['name'] ?? 'פגישה חדשה', 'status' => 'Planned',
|
||||
'dateStart' => $this->normalizeDateTime($params['dateStart'] ?? null),
|
||||
'dateEnd' => $this->normalizeDateTime($params['dateEnd'] ?? null),
|
||||
'description' => $params['description'] ?? null,
|
||||
'parentType' => 'Case', 'parentId' => $caseId, 'assignedUserId' => $userId,
|
||||
]);
|
||||
'assignedUserId' => $userId,
|
||||
];
|
||||
|
||||
if ($caseId) {
|
||||
$meetingData['parentType'] = 'Case';
|
||||
$meetingData['parentId'] = $caseId;
|
||||
}
|
||||
|
||||
$meeting = $this->entityManager->getNewEntity('Meeting');
|
||||
$meeting->set($meetingData);
|
||||
$this->entityManager->saveEntity($meeting);
|
||||
return ['success' => true, 'message' => "פגישה \"{$params['name']}\" נקבעה בהצלחה", 'entityType' => 'Meeting', 'entityId' => $meeting->get('id')];
|
||||
return ['success' => true, 'message' => "✅ פגישה \"{$params['name']}\" נקבעה בהצלחה", 'entityType' => 'Meeting', 'entityId' => $meeting->get('id')];
|
||||
}
|
||||
|
||||
private function scheduleHearing(array $params, ?string $caseId, string $userId): array
|
||||
|
||||
@@ -17,7 +17,8 @@ class SmartAssistantService
|
||||
public const NOTE_TYPE_RESPONSE = 'SmartResponse';
|
||||
public const NOTE_TYPE_ACTION = 'SmartAction';
|
||||
|
||||
private const READ_ONLY_TOOLS = ['list_documents', 'save_memory'];
|
||||
// All tools execute inline — Shira has full permissions
|
||||
private const TOOLS_REQUIRING_CASE = ['change_status', 'schedule_hearing', 'list_documents', 'analyze_document', 'save_memory'];
|
||||
|
||||
private EntityManager $entityManager;
|
||||
private InjectableFactory $injectableFactory;
|
||||
@@ -103,27 +104,59 @@ class SmartAssistantService
|
||||
// Call webhook
|
||||
$response = $this->callWebhook($message, $context, $conversationId, $conversationHistory, $mode);
|
||||
|
||||
// Process actions
|
||||
// Process actions — execute all inline (Shira has full permissions)
|
||||
$actions = $response['actions'] ?? [];
|
||||
$responseText = $response['text'] ?? '';
|
||||
$inlineResults = [];
|
||||
$remainingActions = [];
|
||||
$executedActions = [];
|
||||
|
||||
// Resolve effective caseId: explicit case mode, or drill-down detection in office mode
|
||||
$effectiveCaseId = $caseId;
|
||||
if (!$effectiveCaseId && !empty($context['drillDown']['case']['id'])) {
|
||||
$effectiveCaseId = $context['drillDown']['case']['id'];
|
||||
}
|
||||
|
||||
$executor = $this->injectableFactory->create(ActionExecutor::class);
|
||||
|
||||
foreach ($actions as $action) {
|
||||
$tool = $action['tool'] ?? '';
|
||||
if (in_array($tool, self::READ_ONLY_TOOLS, true)) {
|
||||
try {
|
||||
$executor = $this->injectableFactory->create(ActionExecutor::class);
|
||||
$result = $executor->execute($tool, $action['params'] ?? [], $caseId, $userId);
|
||||
if (!empty($result['message'])) {
|
||||
$inlineResults[] = $result['message'];
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->log->warning("SmartAssistant: Inline execution of {$tool} failed: " . $e->getMessage());
|
||||
$inlineResults[] = "שגיאה בביצוע {$tool}: " . $e->getMessage();
|
||||
$actionCaseId = $effectiveCaseId;
|
||||
|
||||
// Resolve case from action params if not available
|
||||
if (!$actionCaseId && !empty($action['params']['caseId'])) {
|
||||
$actionCaseId = $action['params']['caseId'];
|
||||
}
|
||||
|
||||
// Skip tools that require a case if no case context available
|
||||
if (!$actionCaseId && in_array($tool, self::TOOLS_REQUIRING_CASE, true)) {
|
||||
$inlineResults[] = "⚠️ לא ניתן לבצע {$tool} — לא זוהה תיק. נסה מתוך מסך התיק.";
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $executor->execute($tool, $action['params'] ?? [], $actionCaseId, $userId);
|
||||
if (!empty($result['message'])) {
|
||||
$inlineResults[] = $result['message'];
|
||||
}
|
||||
} else {
|
||||
$remainingActions[] = $action;
|
||||
$executedActions[] = [
|
||||
'tool' => $tool,
|
||||
'status' => 'executed',
|
||||
'entityType' => $result['entityType'] ?? null,
|
||||
'entityId' => $result['entityId'] ?? null,
|
||||
];
|
||||
|
||||
// Create stream note for executed action in case context
|
||||
if ($actionCaseId) {
|
||||
$this->createStreamNote(self::NOTE_TYPE_ACTION, $result['message'] ?? 'פעולה בוצעה', $actionCaseId, [
|
||||
'conversationId' => $conversationId,
|
||||
'status' => 'executed',
|
||||
'entityType' => $result['entityType'] ?? null,
|
||||
'entityId' => $result['entityId'] ?? null,
|
||||
]);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->log->warning("SmartAssistant: Execution of {$tool} failed: " . $e->getMessage());
|
||||
$inlineResults[] = "שגיאה בביצוע {$tool}: " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,25 +164,9 @@ class SmartAssistantService
|
||||
$responseText .= "\n\n" . implode("\n\n", $inlineResults);
|
||||
}
|
||||
|
||||
// Store pending actions
|
||||
if (!empty($remainingActions)) {
|
||||
self::$conversations[$conversationId] = [
|
||||
'caseId' => $caseId,
|
||||
'userId' => $userId,
|
||||
'actions' => $remainingActions,
|
||||
];
|
||||
|
||||
try {
|
||||
$this->getConversationRepo()->storePendingActions($conversationId, $caseId ?? '', $userId, $remainingActions);
|
||||
} catch (\Exception $e) {
|
||||
$this->log->warning('SmartAssistant: Failed to persist actions: ' . $e->getMessage());
|
||||
$this->saveConversationFile($conversationId, $caseId, $userId, $remainingActions);
|
||||
}
|
||||
}
|
||||
|
||||
// Persist response
|
||||
try {
|
||||
$this->getConversationRepo()->appendMessage($conversationId, 'assistant', $responseText, $remainingActions);
|
||||
$this->getConversationRepo()->appendMessage($conversationId, 'assistant', $responseText, $executedActions);
|
||||
} catch (\Exception $e) {
|
||||
$this->log->warning('SmartAssistant: Failed to persist response: ' . $e->getMessage());
|
||||
}
|
||||
@@ -158,14 +175,15 @@ class SmartAssistantService
|
||||
if ($mode === 'case' && $caseId) {
|
||||
$this->createStreamNote(self::NOTE_TYPE_RESPONSE, $responseText, $caseId, [
|
||||
'conversationId' => $conversationId,
|
||||
'actions' => $remainingActions,
|
||||
'actions' => $executedActions,
|
||||
]);
|
||||
}
|
||||
|
||||
return [
|
||||
'conversationId' => $conversationId,
|
||||
'text' => $responseText,
|
||||
'actions' => $remainingActions,
|
||||
'actions' => [],
|
||||
'executedActions' => $executedActions,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "SmartAssistant",
|
||||
"module": "SmartAssistant",
|
||||
"description": "Unified AI Assistant for Legal CRM — floating chat with case memory, office alerts, and n8n integration",
|
||||
"description": "Unified AI Assistant for Legal CRM — floating chat with case memory, office alerts, and AI Gateway integration",
|
||||
"author": "klear",
|
||||
"version": "1.1.6",
|
||||
"version": "2.1.1",
|
||||
"acceptableVersions": [
|
||||
">=8.0.0"
|
||||
],
|
||||
"releaseDate": "2026-03-29",
|
||||
"releaseDate": "2026-04-04",
|
||||
"php": [
|
||||
">=8.1"
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user