|
|
|
@@ -14,12 +14,21 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
|
|
|
|
|
const SS_ASK = 'kb-last-ask';
|
|
|
|
|
const SS_SEARCH = 'kb-last-search';
|
|
|
|
|
const LS_SPLIT = 'kb-split-pct'; // long-lived UI preference (not session)
|
|
|
|
|
// Cached results auto-expire after this long. The user explicitly asked
|
|
|
|
|
// for a clear button, but auto-clearing also handles the case where they
|
|
|
|
|
// come back hours later and the prior result is no longer relevant.
|
|
|
|
|
const STALE_AFTER_MS = 30 * 60 * 1000;
|
|
|
|
|
|
|
|
|
|
function _loadJson(storage, key, validator) {
|
|
|
|
|
try {
|
|
|
|
|
const raw = storage.getItem(key);
|
|
|
|
|
if (!raw) return null;
|
|
|
|
|
const parsed = JSON.parse(raw);
|
|
|
|
|
if (parsed && parsed.completedAt &&
|
|
|
|
|
(Date.now() - parsed.completedAt) > STALE_AFTER_MS) {
|
|
|
|
|
storage.removeItem(key);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return validator(parsed) ? parsed : null;
|
|
|
|
|
} catch (e) { return null; }
|
|
|
|
|
}
|
|
|
|
@@ -78,6 +87,42 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
|
|
|
|
|
const id = parseInt($(e.currentTarget).data('id'), 10);
|
|
|
|
|
if (id) this.openSource(id);
|
|
|
|
|
},
|
|
|
|
|
'click [data-action="clearResults"]': function (e) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
this.clearResults();
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Cancel any in-flight ask/search, drop the cached last-result for
|
|
|
|
|
// the current mode, empty the results pane, and refocus the input.
|
|
|
|
|
// Bound to the "חיפוש חדש" / "שאלה חדשה" button as well as the
|
|
|
|
|
// 30-min auto-expiry path in afterRender.
|
|
|
|
|
clearResults: function () {
|
|
|
|
|
if (this.mode === 'search') {
|
|
|
|
|
if (_activeSearch) {
|
|
|
|
|
// Promise will still resolve in the background, but the
|
|
|
|
|
// .then handler short-circuits when _activeSearch !== self.
|
|
|
|
|
_activeSearch = null;
|
|
|
|
|
}
|
|
|
|
|
_lastSearch = null;
|
|
|
|
|
_clear(sessionStorage, SS_SEARCH);
|
|
|
|
|
} else if (this.mode === 'ask') {
|
|
|
|
|
if (_activeAsk) {
|
|
|
|
|
try { if (_activeAsk.es) _activeAsk.es.close(); }
|
|
|
|
|
catch (e) { /* already closed */ }
|
|
|
|
|
_activeAsk = null;
|
|
|
|
|
}
|
|
|
|
|
_lastAsk = null;
|
|
|
|
|
_clear(sessionStorage, SS_ASK);
|
|
|
|
|
this.stopAskProgress();
|
|
|
|
|
}
|
|
|
|
|
this.setLoading(false);
|
|
|
|
|
this.$el.find('.kb-results').empty();
|
|
|
|
|
const $input = this.$el.find('input[data-name="query"]');
|
|
|
|
|
if ($input.length) {
|
|
|
|
|
$input.val('');
|
|
|
|
|
$input.trigger('focus');
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
setup: function () {
|
|
|
|
@@ -419,8 +464,8 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
|
|
|
|
|
}).join('');
|
|
|
|
|
|
|
|
|
|
$results.html(this._buildSplitShell({
|
|
|
|
|
leftHtml: `<div class="kb-search-list" style="height:100%;overflow-y:auto;padding-left:6px;">${listHtml}</div>`,
|
|
|
|
|
rightHtml: '<div class="kb-preview-slot" style="height:100%;"></div>',
|
|
|
|
|
primaryHtml: '<div class="kb-search-list" style="height:100%;overflow-y:auto;padding-right:6px;">' + listHtml + '</div>',
|
|
|
|
|
referenceHtml: '<div class="kb-preview-slot" style="height:100%;"></div>',
|
|
|
|
|
}));
|
|
|
|
|
this._wireSplitter($results.find('.kb-split'));
|
|
|
|
|
|
|
|
|
@@ -443,10 +488,19 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Returns the HTML for a flex-row split layout with a draggable
|
|
|
|
|
// 6px handle. The container's flex direction is forced LTR so the
|
|
|
|
|
// splitter math (clientX) doesn't get inverted by RTL — content
|
|
|
|
|
// inside each pane keeps its own direction.
|
|
|
|
|
_buildSplitShell: function ({leftHtml, rightHtml}) {
|
|
|
|
|
// 6px handle. In our Hebrew/RTL CRM users expect primary content
|
|
|
|
|
// (Shira's answer + sources, or the search hit list) on the visual
|
|
|
|
|
// RIGHT — matching how Hebrew reads — and the supporting reference
|
|
|
|
|
// (PDF iframe or section context) on the visual LEFT. We force
|
|
|
|
|
// direction:ltr on the container so the splitter's mouseX math
|
|
|
|
|
// doesn't get inverted by RTL inheritance; inner panes carry their
|
|
|
|
|
// own RTL direction as needed.
|
|
|
|
|
//
|
|
|
|
|
// Children render in DOM order [reference, handle, primary], which
|
|
|
|
|
// under direction:ltr maps to visual [LEFT, divider, RIGHT].
|
|
|
|
|
// pct is the LEFT (reference) pane's width %; default 58 matches
|
|
|
|
|
// the v0.1.9 col-md-7 PDF column.
|
|
|
|
|
_buildSplitShell: function ({primaryHtml, referenceHtml}) {
|
|
|
|
|
const pct = this._loadSplitPct();
|
|
|
|
|
return (
|
|
|
|
|
'<div class="kb-split" style="' +
|
|
|
|
@@ -454,7 +508,7 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
|
|
|
|
|
'gap:0;height:86vh;align-items:stretch;">' +
|
|
|
|
|
' <div class="kb-split-left" style="' +
|
|
|
|
|
'flex:0 0 ' + pct + '%;min-width:200px;overflow:hidden;">' +
|
|
|
|
|
leftHtml +
|
|
|
|
|
referenceHtml +
|
|
|
|
|
' </div>' +
|
|
|
|
|
' <div class="kb-split-handle" title="גרור כדי לשנות חלוקה" style="' +
|
|
|
|
|
'flex:0 0 6px;background:#dde1e7;cursor:col-resize;' +
|
|
|
|
@@ -462,7 +516,7 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
|
|
|
|
|
'transition:background 0.15s;"></div>' +
|
|
|
|
|
' <div class="kb-split-right" style="' +
|
|
|
|
|
'flex:1 1 auto;min-width:200px;overflow:hidden;">' +
|
|
|
|
|
rightHtml +
|
|
|
|
|
primaryHtml +
|
|
|
|
|
' </div>' +
|
|
|
|
|
'</div>'
|
|
|
|
|
);
|
|
|
|
@@ -473,7 +527,10 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
|
|
|
|
|
const v = parseFloat(localStorage.getItem(LS_SPLIT));
|
|
|
|
|
if (!isNaN(v) && v >= 15 && v <= 85) return v;
|
|
|
|
|
} catch (e) { /* ignore */ }
|
|
|
|
|
return 42;
|
|
|
|
|
// Default = 58% reference (PDF) on left, ~42% primary on right.
|
|
|
|
|
// Matches the visual proportions of v0.1.9's col-md-7/col-md-5
|
|
|
|
|
// when the CRM rendered Bootstrap rows in RTL order.
|
|
|
|
|
return 58;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
_saveSplitPct: function (pct) {
|
|
|
|
@@ -741,8 +798,8 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
|
|
|
|
|
+ encodeURIComponent(first.source_id)
|
|
|
|
|
+ '#page=' + encodeURIComponent(firstPage);
|
|
|
|
|
|
|
|
|
|
const leftHtml = `
|
|
|
|
|
<div style="height:100%;overflow-y:auto;padding-left:6px;">
|
|
|
|
|
const primaryHtml = `
|
|
|
|
|
<div style="height:100%;overflow-y:auto;padding-right:6px;">
|
|
|
|
|
<div class="kb-answer panel panel-info">
|
|
|
|
|
<div class="panel-body" style="direction:rtl;text-align:right;">
|
|
|
|
|
${answerHtml}
|
|
|
|
@@ -759,13 +816,13 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
const rightHtml = `
|
|
|
|
|
const referenceHtml = `
|
|
|
|
|
<iframe class="kb-pdf-iframe"
|
|
|
|
|
src="${initialUrl}"
|
|
|
|
|
style="width:100%;height:100%;border:1px solid #ddd;background:#fafafa;"
|
|
|
|
|
title="מקור"></iframe>
|
|
|
|
|
`;
|
|
|
|
|
$results.html(this._buildSplitShell({leftHtml, rightHtml}));
|
|
|
|
|
$results.html(this._buildSplitShell({primaryHtml, referenceHtml}));
|
|
|
|
|
this._wireSplitter($results.find('.kb-split'));
|
|
|
|
|
|
|
|
|
|
const self = this;
|
|
|
|
|