feat: SmartAssistant v2.1.0 — full permissions, multi-line input, BiDi tables, Shira avatar
- Shira executes all actions inline (create task, meeting, note) without approval buttons - Actions work from both case and office mode (auto-resolves case from drill-down) - Tasks/meetings can be created without a case parent - Chat input is now multi-line textarea with auto-grow (Shift+Enter for new line) - Markdown tables render as proper HTML tables with LRM marks for BiDi safety - Robot icon replaced with Shira's avatar in FAB, chat header, and message bubbles Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -4,12 +4,13 @@ define('modules/smart-assistant/views/floating-chat', ['view'], function (View)
|
|||||||
|
|
||||||
templateContent: '' +
|
templateContent: '' +
|
||||||
'<div class="sa-fab" title="{{translate \'Smart Assistant\' scope=\'SmartAssistant\'}}">' +
|
'<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>' +
|
' <span class="sa-fab-badge" style="display:none;"></span>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div class="sa-chat-panel" style="display:none;">' +
|
'<div class="sa-chat-panel" style="display:none;">' +
|
||||||
' <div class="sa-chat-header">' +
|
' <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>' +
|
' <span class="sa-mode-badge"></span>' +
|
||||||
' <div class="sa-chat-header-actions">' +
|
' <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;">' +
|
' <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-summary" style="display:none;"></div>' +
|
||||||
' <div class="sa-chat-messages"></div>' +
|
' <div class="sa-chat-messages"></div>' +
|
||||||
' <div class="sa-chat-input-row">' +
|
' <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>' +
|
' <button class="btn btn-primary sa-chat-send" type="button" disabled>' +
|
||||||
' <span class="fas fa-paper-plane"></span>' +
|
' <span class="fas fa-paper-plane"></span>' +
|
||||||
' </button>' +
|
' </button>' +
|
||||||
@@ -59,8 +60,9 @@ define('modules/smart-assistant/views/floating-chat', ['view'], function (View)
|
|||||||
'</div>' +
|
'</div>' +
|
||||||
'<style>' +
|
'<style>' +
|
||||||
'.smart-assistant-fab-container { position: fixed; bottom: 24px; left: 24px; z-index: 1050; direction: rtl; font-family: inherit; }' +
|
'.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 { width: 56px; height: 56px; border-radius: 50%; background: #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; position: relative; overflow: hidden; }' +
|
||||||
'.sa-fab:hover { background: #3f51b5; transform: scale(1.08); }' +
|
'.sa-fab:hover { transform: scale(1.08); }' +
|
||||||
|
'.sa-fab-avatar { width: 100%; height: 100%; object-fit: cover; 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-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; }' +
|
'.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); } }' +
|
'@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-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 { 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-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-error { background: #fce4ec; color: #c62828; }' +
|
||||||
'.sa-msg-loading { background: #f5f5f5; color: #999; }' +
|
'.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-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 { display: flex; gap: 4px; }' +
|
||||||
'.sa-action-btns .btn { padding: 2px 8px; font-size: 11px; border-radius: 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-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: 20px; padding: 8px 14px; font-size: 13px; direction: rtl; }' +
|
'.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-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-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, .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');
|
var cat = $(e.currentTarget).data('category');
|
||||||
this.filterMemory(cat);
|
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(); }
|
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); this.sendMessage(); }
|
||||||
},
|
},
|
||||||
'input .sa-chat-input': function (e) {
|
'input .sa-chat-input': function (e) {
|
||||||
this.$el.find('.sa-chat-send').prop('disabled', !e.target.value.trim());
|
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) {
|
'click .sa-action-approve': function (e) {
|
||||||
var $btn = $(e.currentTarget);
|
var $btn = $(e.currentTarget);
|
||||||
@@ -422,7 +433,7 @@ define('modules/smart-assistant/views/floating-chat', ['view'], function (View)
|
|||||||
this.scrollToBottom();
|
this.scrollToBottom();
|
||||||
|
|
||||||
$btn.prop('disabled', true);
|
$btn.prop('disabled', true);
|
||||||
$input.prop('disabled', true).val('');
|
$input.prop('disabled', true).val('').css('height', 'auto');
|
||||||
|
|
||||||
var payload = {
|
var payload = {
|
||||||
message: message,
|
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>');
|
$messages.append('<div class="sa-msg sa-msg-assistant">' + self.formatResponse(response.text) + '</div>');
|
||||||
|
|
||||||
// Render action items
|
// Auto-refresh panels if actions were executed
|
||||||
if (response.actions && response.actions.length > 0) {
|
if (response.executedActions && response.executedActions.length > 0) {
|
||||||
response.actions.forEach(function (action) {
|
response.executedActions.forEach(function (action) {
|
||||||
var actionHtml = '<div class="sa-msg-action">' +
|
if (action.entityType) {
|
||||||
'<span><span class="fas fa-bolt" style="color: #f57c00; margin-left: 4px;"></span>' +
|
self.refreshCurrentView(action.entityType);
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -611,11 +615,101 @@ define('modules/smart-assistant/views/floating-chat', ['view'], function (View)
|
|||||||
|
|
||||||
formatResponse: function (text) {
|
formatResponse: function (text) {
|
||||||
if (!text) return '';
|
if (!text) return '';
|
||||||
text = this.escapeHtml(text);
|
|
||||||
text = text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
var self = this;
|
||||||
text = text.replace(/\n/g, '<br>');
|
var LRM = '\u200E';
|
||||||
text = text.replace(/^- (.+)/gm, '• $1');
|
|
||||||
return text;
|
// 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) {
|
escapeHtml: function (text) {
|
||||||
|
|||||||
@@ -76,26 +76,48 @@ class ActionExecutor
|
|||||||
|
|
||||||
private function createTask(array $params, ?string $caseId, string $userId): array
|
private function createTask(array $params, ?string $caseId, string $userId): array
|
||||||
{
|
{
|
||||||
$task = $this->entityManager->getNewEntity('Task');
|
$taskData = [
|
||||||
$task->set([
|
|
||||||
'name' => $params['name'] ?? 'משימה חדשה',
|
'name' => $params['name'] ?? 'משימה חדשה',
|
||||||
'status' => 'Not Started',
|
'status' => 'Not Started',
|
||||||
'priority' => $params['priority'] ?? 'Normal',
|
'priority' => $params['priority'] ?? 'Normal',
|
||||||
'dateEnd' => $this->normalizeDateTime($params['dateEnd'] ?? null),
|
'dateEnd' => $this->normalizeDateTime($params['dateEnd'] ?? null),
|
||||||
'description' => $params['description'] ?? null,
|
'description' => $params['description'] ?? null,
|
||||||
'parentType' => 'Case', 'parentId' => $caseId,
|
|
||||||
'assignedUserId' => $params['assignedUserId'] ?? $userId,
|
'assignedUserId' => $params['assignedUserId'] ?? $userId,
|
||||||
]);
|
];
|
||||||
|
|
||||||
|
if ($caseId) {
|
||||||
|
$taskData['parentType'] = 'Case';
|
||||||
|
$taskData['parentId'] = $caseId;
|
||||||
|
}
|
||||||
|
|
||||||
|
$task = $this->entityManager->getNewEntity('Task');
|
||||||
|
$task->set($taskData);
|
||||||
$this->entityManager->saveEntity($task);
|
$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
|
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 = $this->entityManager->getNewEntity('Note');
|
||||||
$note->set(['type' => 'Post', 'post' => $params['post'] ?? '', 'parentType' => 'Case', 'parentId' => $caseId, 'createdById' => $userId]);
|
$note->set($noteData);
|
||||||
$this->entityManager->saveEntity($note);
|
$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
|
private function changeStatus(array $params, ?string $caseId): array
|
||||||
@@ -119,16 +141,23 @@ class ActionExecutor
|
|||||||
|
|
||||||
private function createMeeting(array $params, ?string $caseId, string $userId): array
|
private function createMeeting(array $params, ?string $caseId, string $userId): array
|
||||||
{
|
{
|
||||||
$meeting = $this->entityManager->getNewEntity('Meeting');
|
$meetingData = [
|
||||||
$meeting->set([
|
|
||||||
'name' => $params['name'] ?? 'פגישה חדשה', 'status' => 'Planned',
|
'name' => $params['name'] ?? 'פגישה חדשה', 'status' => 'Planned',
|
||||||
'dateStart' => $this->normalizeDateTime($params['dateStart'] ?? null),
|
'dateStart' => $this->normalizeDateTime($params['dateStart'] ?? null),
|
||||||
'dateEnd' => $this->normalizeDateTime($params['dateEnd'] ?? null),
|
'dateEnd' => $this->normalizeDateTime($params['dateEnd'] ?? null),
|
||||||
'description' => $params['description'] ?? 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);
|
$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
|
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_RESPONSE = 'SmartResponse';
|
||||||
public const NOTE_TYPE_ACTION = 'SmartAction';
|
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 EntityManager $entityManager;
|
||||||
private InjectableFactory $injectableFactory;
|
private InjectableFactory $injectableFactory;
|
||||||
@@ -103,27 +104,59 @@ class SmartAssistantService
|
|||||||
// Call webhook
|
// Call webhook
|
||||||
$response = $this->callWebhook($message, $context, $conversationId, $conversationHistory, $mode);
|
$response = $this->callWebhook($message, $context, $conversationId, $conversationHistory, $mode);
|
||||||
|
|
||||||
// Process actions
|
// Process actions — execute all inline (Shira has full permissions)
|
||||||
$actions = $response['actions'] ?? [];
|
$actions = $response['actions'] ?? [];
|
||||||
$responseText = $response['text'] ?? '';
|
$responseText = $response['text'] ?? '';
|
||||||
$inlineResults = [];
|
$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) {
|
foreach ($actions as $action) {
|
||||||
$tool = $action['tool'] ?? '';
|
$tool = $action['tool'] ?? '';
|
||||||
if (in_array($tool, self::READ_ONLY_TOOLS, true)) {
|
$actionCaseId = $effectiveCaseId;
|
||||||
try {
|
|
||||||
$executor = $this->injectableFactory->create(ActionExecutor::class);
|
// Resolve case from action params if not available
|
||||||
$result = $executor->execute($tool, $action['params'] ?? [], $caseId, $userId);
|
if (!$actionCaseId && !empty($action['params']['caseId'])) {
|
||||||
if (!empty($result['message'])) {
|
$actionCaseId = $action['params']['caseId'];
|
||||||
$inlineResults[] = $result['message'];
|
}
|
||||||
}
|
|
||||||
} catch (\Exception $e) {
|
// Skip tools that require a case if no case context available
|
||||||
$this->log->warning("SmartAssistant: Inline execution of {$tool} failed: " . $e->getMessage());
|
if (!$actionCaseId && in_array($tool, self::TOOLS_REQUIRING_CASE, true)) {
|
||||||
$inlineResults[] = "שגיאה בביצוע {$tool}: " . $e->getMessage();
|
$inlineResults[] = "⚠️ לא ניתן לבצע {$tool} — לא זוהה תיק. נסה מתוך מסך התיק.";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = $executor->execute($tool, $action['params'] ?? [], $actionCaseId, $userId);
|
||||||
|
if (!empty($result['message'])) {
|
||||||
|
$inlineResults[] = $result['message'];
|
||||||
}
|
}
|
||||||
} else {
|
$executedActions[] = [
|
||||||
$remainingActions[] = $action;
|
'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);
|
$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
|
// Persist response
|
||||||
try {
|
try {
|
||||||
$this->getConversationRepo()->appendMessage($conversationId, 'assistant', $responseText, $remainingActions);
|
$this->getConversationRepo()->appendMessage($conversationId, 'assistant', $responseText, $executedActions);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$this->log->warning('SmartAssistant: Failed to persist response: ' . $e->getMessage());
|
$this->log->warning('SmartAssistant: Failed to persist response: ' . $e->getMessage());
|
||||||
}
|
}
|
||||||
@@ -158,14 +175,15 @@ class SmartAssistantService
|
|||||||
if ($mode === 'case' && $caseId) {
|
if ($mode === 'case' && $caseId) {
|
||||||
$this->createStreamNote(self::NOTE_TYPE_RESPONSE, $responseText, $caseId, [
|
$this->createStreamNote(self::NOTE_TYPE_RESPONSE, $responseText, $caseId, [
|
||||||
'conversationId' => $conversationId,
|
'conversationId' => $conversationId,
|
||||||
'actions' => $remainingActions,
|
'actions' => $executedActions,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'conversationId' => $conversationId,
|
'conversationId' => $conversationId,
|
||||||
'text' => $responseText,
|
'text' => $responseText,
|
||||||
'actions' => $remainingActions,
|
'actions' => [],
|
||||||
|
'executedActions' => $executedActions,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
"module": "SmartAssistant",
|
"module": "SmartAssistant",
|
||||||
"description": "Unified AI Assistant for Legal CRM — floating chat with case memory, office alerts, and AI Gateway integration",
|
"description": "Unified AI Assistant for Legal CRM — floating chat with case memory, office alerts, and AI Gateway integration",
|
||||||
"author": "klear",
|
"author": "klear",
|
||||||
"version": "2.0.0",
|
"version": "2.1.0",
|
||||||
"acceptableVersions": [
|
"acceptableVersions": [
|
||||||
">=8.0.0"
|
">=8.0.0"
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user